From 466442d1121e71256b914ef105f20de2e4e74eab Mon Sep 17 00:00:00 2001 From: jlgearh Date: Sun, 26 Feb 2023 11:21:25 -0700 Subject: [PATCH 0001/3044] - Adding alternative solutions code from CI-MOR to the alternative_solutions contrib package. --- .../contrib/alternative_solutions/__init__.py | 0 .../alternative_solutions/aos_utils.py | 126 ++ pyomo/contrib/alternative_solutions/balas.py | 155 ++ .../alternative_solutions/comparison.py | 23 + .../contrib/alternative_solutions/lp_enum.py | 272 +++ pyomo/contrib/alternative_solutions/obbt.py | 207 ++ .../contrib/alternative_solutions/solnpool.py | 80 + .../contrib/alternative_solutions/solution.py | 28 + .../alternative_solutions/tests/__init__.py | 0 .../alternative_solutions/tests/balas_test.py | 46 + .../tests/knapsack_100_100_baseline.yaml | 1814 +++++++++++++++++ .../tests/knapsack_100_100_comp_baseline.yaml | 11 + .../tests/knapsack_100_10_baseline.yaml | 180 ++ .../tests/knapsack_100_10_comp_baseline.yaml | 8 + .../tests/knapsack_100_10_results.yaml | 180 ++ .../tests/knapsack_100_1_baseline.yaml | 18 + .../alternative_solutions/tests/obbt_test.py | 45 + .../tests/test_results.yaml | 180 ++ .../tests/test_solnpool.py | 81 + .../alternative_solutions/var_utils.py | 136 ++ 20 files changed, 3590 insertions(+) create mode 100644 pyomo/contrib/alternative_solutions/__init__.py create mode 100644 pyomo/contrib/alternative_solutions/aos_utils.py create mode 100644 pyomo/contrib/alternative_solutions/balas.py create mode 100644 pyomo/contrib/alternative_solutions/comparison.py create mode 100644 pyomo/contrib/alternative_solutions/lp_enum.py create mode 100644 pyomo/contrib/alternative_solutions/obbt.py create mode 100644 pyomo/contrib/alternative_solutions/solnpool.py create mode 100644 pyomo/contrib/alternative_solutions/solution.py create mode 100644 pyomo/contrib/alternative_solutions/tests/__init__.py create mode 100644 pyomo/contrib/alternative_solutions/tests/balas_test.py create mode 100644 pyomo/contrib/alternative_solutions/tests/knapsack_100_100_baseline.yaml create mode 100644 pyomo/contrib/alternative_solutions/tests/knapsack_100_100_comp_baseline.yaml create mode 100644 pyomo/contrib/alternative_solutions/tests/knapsack_100_10_baseline.yaml create mode 100644 pyomo/contrib/alternative_solutions/tests/knapsack_100_10_comp_baseline.yaml create mode 100644 pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml create mode 100644 pyomo/contrib/alternative_solutions/tests/knapsack_100_1_baseline.yaml create mode 100644 pyomo/contrib/alternative_solutions/tests/obbt_test.py create mode 100644 pyomo/contrib/alternative_solutions/tests/test_results.yaml create mode 100644 pyomo/contrib/alternative_solutions/tests/test_solnpool.py create mode 100644 pyomo/contrib/alternative_solutions/var_utils.py diff --git a/pyomo/contrib/alternative_solutions/__init__.py b/pyomo/contrib/alternative_solutions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py new file mode 100644 index 00000000000..bd0abf9e7b1 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Jun 30 16:12:23 2022 + +@author: jlgearh +""" +import sys + +from numpy.random import normal +from numpy.linalg import norm + +from pyomo.common.modeling import unique_component_name +from pyomo.common.collections import ComponentSet +import pyomo.environ as pe +import pyomo.util.vars_from_expressions as vfe +from pyomo.opt import SolverFactory +from pyomo.core.base.PyomoModel import ConcreteModel +from pyomo.contrib import appsi + +def _is_concrete_model(model): + assert isinstance(model, ConcreteModel), \ + "Parameter 'model' must be an instance of a Pyomo ConcreteModel" + +def _get_solver(solver, solver_options={}, use_persistent_solver=False): + if use_persistent_solver: + assert solver == 'gurobi', \ + "Persistent solver option requires the use of Gurobi." + opt = appsi.solvers.Gurobi() + opt.config.stream_solver = True + for parameter, value in solver_options.items(): + opt.set_gurobi_param(parameter, value) + else: + opt = SolverFactory(solver) + for parameter, value in solver_options.items(): + opt.options[parameter] = value + return opt + +def _get_active_objective(model): + ''' + Finds and returns the active objective function for a model. Assumes there + is exactly one active objective. + ''' + active_objs = [o for o in model.component_data_objects(pe.Objective, + active=True)] + assert len(active_objs) == 1, \ + "Model has more than one active objective function" + + return active_objs[0] + +def _add_aos_block(model, name='_aos_block'): + '''Adds an alternative optimal solution block with a unique name.''' + aos_block = pe.Block() + model.add_component(unique_component_name(model, name), aos_block) + return aos_block + +def _add_objective_constraint(aos_block, objective, objective_value, + rel_opt_gap, abs_gap): + ''' + Adds a relative and/or absolute objective function constraint to the + specified block. + ''' + if rel_opt_gap is not None or abs_gap is not None: + objective_is_min = objective.is_minimizing() + objective_expr = objective.expr + + objective_sense = -1 + if objective_is_min: + objective_sense = 1 + + if rel_opt_gap is not None: + objective_cutoff = objective_value * \ + (1 + objective_sense * rel_opt_gap) + + if objective_is_min: + aos_block.optimality_tol_rel = \ + pe.Constraint(expr=objective_expr <= \ + objective_cutoff) + else: + aos_block.optimality_tol_rel = \ + pe.Constraint(expr=objective_expr >= \ + objective_cutoff) + + if abs_gap is not None: + objective_cutoff = objective_value + objective_sense \ + * abs_gap + + if objective_is_min: + aos_block.optimality_tol_abs = \ + pe.Constraint(expr=objective_expr <= \ + objective_cutoff) + else: + aos_block.optimality_tol_abs = \ + pe.Constraint(expr=objective_expr >= \ + objective_cutoff) + +def _get_max_solutions(max_solutions): + assert isinstance(max_solutions, (int, type(None))), \ + 'max_solutions parameter must be an integer or None' + if isinstance(max_solutions, int): + assert max_solutions >= 1, \ + ('max_solutions parameter must be an integer greater than or equal' + ' to 1' + ) + num_solutions = max_solutions + if max_solutions is None: + num_solutions = sys.maxsize + return num_solutions + + + + +def get_solution(model, variables): + solution = [] + for var in variables: + solution.append((var, pe.value(var))) + return solution + +def _get_random_direction(num_dimensions): + idx = 0 + while idx < 100: + samples = normal(size=num_dimensions) + samples_norm = norm(samples) + if samples_norm > 1e-4: + return samples / samples_norm + idx += 1 + raise Exception diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py new file mode 100644 index 00000000000..2082f16d5de --- /dev/null +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +""" +Created on Wed Jun 22 21:49:54 2022 + +@author: jlgearh + +""" + +from numpy import dot + +from pyomo.core.base.PyomoModel import ConcreteModel +import pyomo.environ as pe +from pyomo.opt import SolverStatus, TerminationCondition +from pyomo.contrib.alternative_solutions import aos_utils, var_utils + +def enumerate_binary_solutions(model, max_solutions=10, variables='all', + rel_opt_gap=None, abs_gap=None, + search_mode='optimal', already_solved=False, + solver='gurobi', solver_options={}, tee=False): + '''Finds alternative optimal solutions for a binary problem. + + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model + max_solutions : int or None + The maximum number of solutions to generate. None indictes no upper + limit. Note, using None could lead to a large number of solutions. + variables: 'all', None, Block, or a Collection of Pyomo components + The binary variables for which alternative solutions will be + generated. 'all' or None indicates that all binary variables will + be included. + rel_opt_gap : float or None + The relative optimality gap for allowable alternative solutions. + None indicates that a relative gap constraint will not be added to + the model. + abs_gap : float or None + The absolute optimality gap for allowable alternative solutions. + None indicates that an absolute gap constraint will not be added to + the model. + search_mode : 'optimal', 'random', or 'hamming' + Indicates the mode that is used to generate alternative solutions. + The optimal mode finds the next best solution. The random mode + finds an alternative solution in the direction of a random ray. The + hamming mode iteratively finds solution that maximize the hamming + distance from previously discovered solutions. + already_solved : boolean + Indicates that the model has already been solved and that the + alternative solution search can start from the current solution. + solver : string + The solver to be used for alternative solution search. + solver_options : dict + Solver option-value pairs to be passed to the solver. + tee : boolean + Boolean indicating if the solver output should be displayed. + + Returns + ------- + solutions + A dictionary of alternative optimal solutions. + {solution_id: (objective_value,[variable, variable_value])} + ''' + + assert isinstance(model, ConcreteModel), \ + 'model parameter must be an instance of a Pyomo Concrete Model' + + # Find the maximum number of solutions to generate + num_solutions = aos_utils._get_max_solutions(max_solutions) + if variables == 'all': + binary_variables = var_utils.get_model_variables(model, 'all', + include_binary=True) + else: + variable_list = var_utils.check_variables(model, variables) + all_variables = var_utils.get_model_variables(model, 'all') + orig_objective = aos_utils._get_active_objective(model) + + aos_block = aos_utils._add_aos_block(model) + aos_block.no_good_cuts = pe.ConstraintList() + + opt = aos_utils._get_solver(solver, solver_options) + + # Repeat until all solutions are found + solution_number = 0 + solutions = {} + while solution_number < num_solutions: + + # Solve the model unless this is the first solution and the model was + # not already solved + if solution_number > 0 or not already_solved: + results = opt.solve(model, tee=tee) + + if (((results.solver.status == SolverStatus.ok) and + (results.solver.termination_condition == TerminationCondition.optimal)) + or (already_solved and solution_number == 0)): + objective_value = pe.value(orig_objective) + hamming_value = 0 + if solution_number > 0: + hamming_value = pe.value(aos_block.hamming_objective/solution_number) + print("Found solution #{}, objective = {}".format(solution_number, + hamming_value)) + + solutions[solution_number] = (objective_value, + aos_utils.get_solution(model, + all_variables)) + + if solution_number == 0: + aos_utils._add_objective_constraint(aos_block, orig_objective, + objective_value, + rel_opt_gap, abs_gap) + + if search_mode in ['random', 'hamming']: + orig_objective.deactivate() + + # Add the new solution to the list of previous solutions + expr = 0 + for var in binary_variables: + if var.value > 0.5: + expr += 1 - var + else: + expr += var + + aos_block.no_good_cuts.add(expr= expr >= 1) + + # TODO: Maybe rescale these + if search_mode == 'hamming': + if hasattr(aos_block, 'hamming_objective'): + aos_block.hamming_objective.expr += expr + else: + aos_block.hamming_objective = pe.Objective(expr=expr, + sense=pe.maximize) + + if search_mode == 'random': + if hasattr(aos_block, 'random_objective'): + aos_block.del_component('random_objective') + vector = aos_utils._get_random_direction(len(binary_variables)) + idx = 0 + expr = 0 + for var in binary_variables: + expr += vector[idx] * var + idx += 1 + aos_block.random_objective = \ + pe.Objective(expr=expr, sense=pe.maximize) + + solution_number += 1 + else: + print('Algorithm Stopped. Solver Status: {}. Solver Condition: {}.'\ + .format(results.solver.status, + results.solver.termination_condition)) + break + + aos_block.deactivate() + orig_objective.activate() + + return solutions + diff --git a/pyomo/contrib/alternative_solutions/comparison.py b/pyomo/contrib/alternative_solutions/comparison.py new file mode 100644 index 00000000000..9feff3b88d7 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/comparison.py @@ -0,0 +1,23 @@ +import math + + +def consensus(solutions, ignore_zeros=True): + # + # Summarize the average value of solution values + # + # This currently assumes all solutions have the same variables + # + nsolutions = len(solutions) + assert nsolutions > 1, "Need more than one solution to form a consensus pattern" + keys = list(sorted(solutions[0]['variables'].keys())) + + total = {key:solutions[0]['variables'][key] for key in keys} + for i in range(1, nsolutions): + total = {key:(total[key] + solutions[i]['variables'][key]) for key in keys} + + mean = {key:total[key]/nsolutions for key in keys} + + if ignore_zeros: + return {key:mean[key] for key in keys if math.fabs(mean[key]) > 1e-7} + else: + return mean diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py new file mode 100644 index 00000000000..d43ae8f3412 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Dec 1 11:18:04 2022 + +@author: jlgearh +""" + +import pyomo.environ as pe +from pyomo.opt import SolverStatus, TerminationCondition +from pyomo.gdp.util import clone_without_expression_components +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +import aos_utils + +# TODO set the variable values at the end + +model = pe.ConcreteModel() + +model.x = pe.Var(within=pe.PercentFraction) +model.y = pe.Var(within=pe.PercentFraction) + + +model.obj = pe.Objective(expr=model.x+model.y, sense=pe.maximize) + +model.wx_limit = pe.Constraint(expr=model.x+model.y<=2) + +# model = pe.ConcreteModel() + +# model.w = pe.Var(within=pe.NonNegativeReals) +# model.x = pe.Var(within=pe.Reals) +# model.y = pe.Var(within=pe.PercentFraction) +# model.z = pe.Var(within=pe.Reals, bounds=(0,1)) + + +# model.obj = pe.Objective(expr=model.w+model.x+model.y+model.z, sense=pe.maximize) + +# model.wx_limit = pe.Constraint(expr=model.w+model.x<=2) +# model.wu_limit = pe.Constraint(expr=model.w<=1) +# model.xl_limit = pe.Constraint(expr=model.x>=0) +# model.xu_limit = pe.Constraint(expr=model.x<=1) + +# model.b = pe.Block() +# model.b.yz_limit = pe.Constraint(expr=-model.y-model.z>=-2) +# model.b.wy = pe.Constraint(expr=model.w+model.y==1) + +# model = pe.ConcreteModel() + +# model.w = pe.Var(within=pe.PercentFraction) +# model.x = pe.Var(within=pe.PercentFraction) +# model.y = pe.Var(within=pe.PercentFraction) +# model.z = pe.Var(within=pe.PercentFraction) + + +# model.obj = pe.Objective(expr=model.w+model.x+model.y+model.z, sense=pe.maximize) + +# model.wx_limit = pe.Constraint(expr=model.w+model.x<=2) + + +# model.b = pe.Block() +# model.b.yz_limit = pe.Constraint(expr=-model.y-model.z>=-2) +# model.b.wy = pe.Constraint(expr=model.w+model.y==1) + +# Get a Pyomo concrete model + + +# Get all continuous variables in the model and check that they have finite +# bounds +# TODO handle fixed variables +model_vars = aos_utils.get_model_variables(model, 'all') +model_var_names = {} +model_var_names_bounds = {} +for mv in model_vars: + assert mv.is_continuous, 'Variable {} is not continuous'.format(mv.name) + assert not (mv.lb is None and mv.ub is None) + var_name = mv.name + model_var_names[id(mv)] = var_name + model_var_names_bounds[var_name] = (0,mv.ub - mv.lb) + +canon_lp = aos_utils._add_aos_block(model, name='canon_lp') + +# Replace original variables with shifted lower and upper bound "s" variables +# TODO use unique names + +canon_lp.var_index = pe.Set(initialize=model_var_names_bounds.keys()) + +canon_lp.var_lower = pe.Var(canon_lp.var_index, domain=pe.NonNegativeReals, + bounds=model_var_names_bounds) +canon_lp.var_upper = pe.Var(canon_lp.var_index, domain=pe.NonNegativeReals, + bounds=model_var_names_bounds) + +def link_vars_rule(model, var_index): + return model.var_lower[var_index] + model.var_upper[var_index] == \ + model.var_upper[var_index].ub +canon_lp.link_vars = pe.Constraint(canon_lp.var_index, rule=link_vars_rule) + +var_lower_map = {} +var_lower_bounds = {} +for mv in model_vars: + var_lower_map[id(mv)] = canon_lp.var_lower[model_var_names[id(mv)]] + var_lower_bounds[id(mv)] = mv.lb + +# Substitue the new s variables into the objective function +orig_objective = aos_utils._get_active_objective(model) +c_var_lower = clone_without_expression_components(orig_objective.expr, + substitute=var_lower_map) +c_fix_lower = clone_without_expression_components(orig_objective.expr, + substitute=var_lower_bounds) +canon_lp.objective = pe.Objective(expr=c_var_lower + c_fix_lower, + name=orig_objective.name + '_shifted', + sense=orig_objective.sense) + +new_constraints = {} +slacks = [] +for constraint in model.component_data_objects(pe.Constraint, active=None, + sort=False, + descend_into=pe.Block, + descent_order=None): + if constraint.parent_block() == canon_lp: + continue + if constraint.equality: + constraint_name = constraint.name + '_equal' + new_constraints[constraint_name] = (constraint,0) + else: + if constraint.lb is not None: + constraint_name = constraint.name + '_lower' + new_constraints[constraint_name] = (constraint,-1) + slacks.append(constraint_name) + if constraint.ub is not None: + constraint_name = constraint.name + '_upper' + new_constraints[constraint_name] = (constraint,1) + slacks.append(constraint_name) +canon_lp.constraint_index = pe.Set(initialize=new_constraints.keys()) +canon_lp.slack_index = pe.Set(initialize=slacks) +canon_lp.slack_vars = pe.Var(canon_lp.slack_index, domain=pe.NonNegativeReals) +canon_lp.constraints = pe.Constraint(canon_lp.constraint_index) + +constraint_map = {} +constraint_bounds = {} + +def set_slack_ub(expression, slack_var): + slack_lb, slack_ub = compute_bounds_on_expr(expression) + assert slack_lb == 0 and slack_ub >= 0 + slack_var.setub(slack_ub) + +for constraint_name, (constraint, constraint_type) in new_constraints.items(): + + a_sub_var_lower = clone_without_expression_components(constraint.body, + substitute=var_lower_map) + a_sub_fix_lower = clone_without_expression_components(constraint.body, + substitute=var_lower_bounds) + b_lower = constraint.lb + b_upper = constraint.ub + if constraint_type == 0: + expression = a_sub_var_lower + a_sub_fix_lower - b_lower == 0 + elif constraint_type == -1: + expression_rhs = a_sub_var_lower + a_sub_fix_lower - b_lower + expression = canon_lp.slack_vars[constraint_name] == expression_rhs + set_slack_ub(expression_rhs, canon_lp.slack_vars[constraint_name]) + elif constraint_type == 1: + expression_rhs = b_upper - a_sub_var_lower - a_sub_fix_lower + expression = canon_lp.slack_vars[constraint_name] == expression_rhs + set_slack_ub(expression_rhs, canon_lp.slack_vars[constraint_name]) + canon_lp.constraints[constraint_name] = expression + + +def enumerate_linear_solutions(model, max_solutions=10, variables='all', + rel_opt_gap=None, abs_gap=None, + search_mode='optimal', already_solved=False, + solver='cplex', solver_options={}, tee=False): + '''Finds alternative optimal solutions for a binary problem. + + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model + max_solutions : int or None + The maximum number of solutions to generate. None indictes no upper + limit. Note, using None could lead to a large number of solutions. + variables: 'all', None, Block, or a Collection of Pyomo components + The binary variables for which alternative solutions will be + generated. 'all' or None indicates that all binary variables will + be included. + rel_opt_gap : float or None + The relative optimality gap for allowable alternative solutions. + None indicates that a relative gap constraint will not be added to + the model. + abs_gap : float or None + The absolute optimality gap for allowable alternative solutions. + None indicates that an absolute gap constraint will not be added to + the model. + search_mode : 'optimal', 'random', or 'hamming' + Indicates the mode that is used to generate alternative solutions. + The optimal mode finds the next best solution. The random mode + finds an alternative solution in the direction of a random ray. The + hamming mode iteratively finds solution that maximize the hamming + distance from previously discovered solutions. + already_solved : boolean + Indicates that the model has already been solved and that the + alternative solution search can start from the current solution. + solver : string + The solver to be used for alternative solution search. + solver_options : dict + Solver option-value pairs to be passed to the solver. + tee : boolean + Boolean indicating if the solver output should be displayed. + + Returns + ------- + solutions + A dictionary of alternative optimal solutions. + {solution_id: (objective_value,[variable, variable_value])} + ''' + + + # Find the maximum number of solutions to generate + num_solutions = aos_utils._get_max_solutions(max_solutions) + opt = aos_utils._get_solver(solver, solver_options) + + model.iteration = pe.Set(dimen=1) + + model.basic_lower = pe.Var(pe.Any, domain=pe.Binary, dense=False) + model.basic_upper = pe.Var(pe.Any, domain=pe.Binary, dense=False) + model.basic_slack = pe.Var(pe.Any, domain=pe.Binary, dense=False) + + model.bound_lower = pe.Constraint(pe.Any) + model.bound_upper = pe.Constraint(pe.Any) + model.bound_slack = pe.Constraint(pe.Any) + model.cut_set = pe.Constraint(pe.Any) + + variable_groups = [(model.var_lower, model.basic_lower, model.bound_lower), + (model.var_upper, model.basic_upper, model.bound_upper), + (model.slack_vars, model.basic_slack, model.bound_slack)] + + # Repeat until all solutions are found + solution_number = 1 + solutions = {} + while solution_number < num_solutions: + + # Solve the model unless this is the first solution and the model was + # not already solved + if solution_number > 1 or not already_solved: + print('Iteration: {}'.format(solution_number)) + results = opt.solve(model, tee=tee) + + if (((results.solver.status == SolverStatus.ok) and + (results.solver.termination_condition == TerminationCondition.optimal)) + or (already_solved and solution_number == 0)): + #objective_value = pe.value(orig_objective) + + for variable in model.var_lower: + print('Var {} = {}'.format(variable, + pe.value(model.var_lower[variable]))) + + expr = 1 + num_non_zeros = 0 + + for continuous_var, binary_var, constraint in variable_groups: + for variable in continuous_var: + if pe.value(continuous_var[variable]) > 1e-5: + if variable not in binary_var: + model.basic_upper[variable] + constraint[variable] = continuous_var[variable] <= \ + continuous_var[variable].ub * binary_var[variable] + expr += binary_var[variable] + num_non_zeros += 1 + model.cut_set[solution_number] = expr <= num_non_zeros + solution_number += 1 + + else: + print('Algorithm Stopped. Solver Status: {}. Solver Condition: {}.'\ + .format(results.solver.status, + results.solver.termination_condition)) + break \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py new file mode 100644 index 00000000000..fb34e458300 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -0,0 +1,207 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +#import pandas as pd + +import pyomo.environ as pe +from pyomo.opt import SolverStatus, TerminationCondition +from pyomo.common.collections import ComponentMap + +import pyomo.contrib.alternative_solutions.aos_utils as aos_utils +import pyomo.contrib.alternative_solutions.variables as var_utils + +def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, + refine_bounds=False, warmstart=False, already_solved=False, + solver='gurobi', solver_options={}, + use_persistent_solver=False, tee=False): + ''' + Calculates the bounds on each variable by solving a series of min and max + optimization problems where each variable is used as the objective function + This can be applied to any class of problem supported by the selected + solver. + + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model. + variables: 'all' or a collection of Pyomo _GenereralVarData variables + The variables for which bounds will be generated. 'all' indicates + that all variables will be included. Alternatively, a collection of + _GenereralVarData variables can be provided. + rel_opt_gap : float or None + The relative optimality gap for the original objective for which + variable bounds will be found. None indicates that a relative gap + constraint will not be added to the model. + abs_gap : float or None + The absolute optimality gap for the original objective for which + variable bounds will be found. None indicates that an absolute gap + constraint will not be added to the model. + refine_bounds : boolean + Boolean indicating that new constraints should be added to the + model at each iteration to tighten the bounds for varaibles. + warmstart : boolean + Boolean indicating that previous solutions should be passed to the + solver as warmstart solutions. + already_solved : boolean + Indicates that the model has already been solved and that the + variable bound search can start from the current solution. + solver : string + The solver to be used. + solver_options : dict + Solver option-value pairs to be passed to the solver. + use_persistent_solver : boolean + Boolean indicating if the the APPSI persistent solver interface + should be used. Currently, only supported Gurobi is supported for + variable bound analysis with the persistent solver. + tee : boolean + Boolean indicating that the solver output should be displayed. + + Returns + ------- + variable_ranges + A Pyomo ComponentMap containing the bounds for each variable. + {variable: (lower_bound, upper_bound)} + ''' + + aos_utils._is_concrete_model(model) + assert isinstance(refine_bounds, bool), 'refine_bounds must be a Boolean' + assert isinstance(warmstart, bool), 'warmstart must be a Boolean' + assert isinstance(already_solved, bool), 'already_solved must be a Boolean' + assert isinstance(use_persistent_solver, bool), \ + 'use_persistent_solver must be a Boolean' + assert isinstance(tee, bool), 'tee must be a Boolean' + + if variables == 'all': + variable_list = var_utils.get_model_variables(model, variables, + include_fixed=False) + else: + variable_list = var_utils.check_variables(model, variables) + + orig_objective = aos_utils._get_active_objective(model) + aos_block = aos_utils._add_aos_block(model) + new_constraint = False + + opt = aos_utils._get_solver(solver, solver_options, use_persistent_solver) + + if not already_solved: + results = opt.solve(model)#, tee=tee) + status = results.solver.status + condition = results.solver.termination_condition + assert (status == SolverStatus.ok and + condition == TerminationCondition.optimal), \ + ('Model cannot be solved, SolverStatus = {}, ' + 'TerminationCondition = {}').format(status.value, + condition.value) + + orig_objective_value = pe.value(orig_objective) + aos_utils._add_objective_constraint(aos_block, orig_objective, + orig_objective_value, rel_opt_gap, + abs_gap) + if rel_opt_gap is not None or abs_gap is not None: + new_constraint = True + + orig_objective.deactivate() + + if use_persistent_solver: + opt.update_config.check_for_new_or_removed_constraints = new_constraint + opt.update_config.check_for_new_or_removed_vars = False + opt.update_config.check_for_new_or_removed_params = False + opt.update_config.check_for_new_objective = True + opt.update_config.update_constraints = False + opt.update_config.update_vars = False + opt.update_config.update_params = False + opt.update_config.update_named_expressions = False + opt.update_config.update_objective = False + opt.update_config.treat_fixed_vars_as_params = False + + variable_bounds = ComponentMap() + + senses = [pe.minimize, pe.maximize] + + iteration = 1 + total_iterations = len(senses) * len(variable_list) + for idx in range(2): + sense = senses[idx] + sense_name = 'min' + bound_dir = 'LB' + if sense == pe.maximize: + sense_name = 'max' + bound_dir = 'UB' + + for var in variable_list: + if idx == 0: + variable_bounds[var] = [None, None] + + if hasattr(aos_block, 'var_objective'): + aos_block.del_component('var_objective') + + aos_block.var_objective = pe.Objective(expr=var, sense=sense) + + # TODO: Updated solution pool + + if use_persistent_solver: + opt.update_config.check_for_new_or_removed_constraints = \ + new_constraint + results = opt.solve(model)#, tee=tee) + new_constraint = False + status = results.solver.status + condition = results.solver.termination_condition + if (status == SolverStatus.ok and + condition == TerminationCondition.optimal): + obj_val = pe.value(var) + variable_bounds[var][idx] = obj_val + + if refine_bounds and sense == pe.minimize and var.lb < obj_val: + bound_name = var.name + '_lb' + bound = pe.Constraint(expr= var >= obj_val) + setattr(aos_block, bound_name, bound) + new_constraint = True + + if refine_bounds and sense == pe.maximize and var.ub > obj_val: + bound_name = var.name + '_ub' + bound = pe.Constraint(expr= var <= obj_val) + setattr(aos_block, bound_name, bound) + new_constraint = True + # An infeasibleOrUnbounded status code will imply the problem is + # unbounded since feasibility has be established previously + elif (status == SolverStatus.ok and ( + condition == TerminationCondition.infeasibleOrUnbounded or + condition == TerminationCondition.unbounded)): + if sense == pe.minimize: + variable_bounds[var][idx] = float('-inf') + else: + variable_bounds[var][idx] = float('inf') + else: + print(('Unexpected solver status for variable {} {} problem.' + 'SolverStatus = {}, TerminationCondition = {}').\ + format(var.name, sense_name, status.value, + condition.value)) + + + print('It. {}/{}: {}_{} = {}'.format(iteration, total_iterations, + var.name, bound_dir, + variable_bounds[var][idx])) + + if idx == 1: + variable_bounds[var] = tuple(variable_bounds[var]) + + iteration += 1 + + + aos_block.deactivate + orig_objective.active + + return variable_bounds + +# def get_var_bound_dataframe(variable_bounds): +# '''Get a pandas DataFrame displaying the variable bound results.''' +# return pd.DataFrame.from_dict(variable_bounds,orient='index', +# columns=['LB','UB']) diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py new file mode 100644 index 00000000000..e8440c6e9e1 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -0,0 +1,80 @@ +from pyomo.contrib import appsi +from pyomo.contrib.alternative_solutions import aos_utils, var_utils, solution + +def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=0.0, + abs_opt_gap=0.0, search_mode=2, + round_discrete_vars=True, solver_options={}): + ''' + Finds alternative optimal solutions for discrete variables using Gurobi's + built-in Solution Pool capability. See the Gurobi Solution Pool + documentation for additional details. This function uses the Gurobi + Auto-Persistent Pyomo Solver interface (appsi). + + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model + max_solutions : int or None + The maximum number of solutions to generate. None indictes no upper + limit. Note, using None could lead to a large number of solutions. + This parameter maps to the PoolSolutions parameter in Gurobi. + rel_opt_gap : non-negative float + The relative optimality gap for allowable alternative solutions. + This parameter maps to the PoolGap parameter in Gurobi. + abs_opt_gap : non-negative float + The absolute optimality gap for allowable alternative solutions. + This parameter maps to the PoolGapAbs parameter in Gurobi. + search_mode : 0, 1, or 2 + Indicates the mode that is used to generate alternative solutions. + Mode 2 should typically be used as it finds the best n solutions. + Mode 0 finds a single optimal solution. Mode 1 will generate n + solutions without providing guarantees on their quality. This + parameter maps to the PoolSearchMode in Gurobi. + round_discrete_vars : boolean + Boolean indicating that discrete values should be rounded to the + nearest integer in the solutions results. + solver_options : dict + Solver option-value pairs to be passed to the solver. + + Returns + ------- + solutions + A list of solution dictionaries. + [solution] + ''' + + # Validate inputs + aos_utils._is_concrete_model(model) + num_solutions = aos_utils._get_max_solutions(max_solutions) + assert isinstance(rel_opt_gap, float) and rel_opt_gap >= 0, \ + 'rel_opt_gap must be a non-negative float' + assert isinstance(abs_opt_gap, float) and abs_opt_gap >= 0, \ + 'abs_opt_gap must be a non-negative float' + assert search_mode in [0, 1, 2], 'search_mode must be 0, 1, or 2' + assert isinstance(round_discrete_vars, bool), \ + 'round_discrete_vars must be a Boolean' + + # Configure solver and solve model + opt = appsi.solvers.Gurobi() + opt.config.stream_solver = True + opt.set_instance(model) + opt.set_gurobi_param('PoolSolutions', num_solutions) + opt.set_gurobi_param('PoolSearchMode', search_mode) + opt.set_gurobi_param('PoolGap', rel_opt_gap) + opt.set_gurobi_param('PoolGapAbs', abs_opt_gap) + results = opt.solve(model) + assert results.termination_condition == \ + appsi.base.TerminationCondition.optimal, \ + 'Solver terminated with conditions {}.'.format( + results.termination_condition) + + # Get model solutions + solution_count = opt.get_model_attr('SolCount') + print("Gurobi found {} solutions".format(solution_count)) + variables = var_utils.get_model_variables(model, 'all', include_fixed=True) + solutions = [] + for i in range(solution_count): + results.solution_loader.load_vars(solution_number=i) + solutions.append(solution.get_model_solution(model, variables)) + + return solutions \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/solution.py b/pyomo/contrib/alternative_solutions/solution.py new file mode 100644 index 00000000000..64c4b494045 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/solution.py @@ -0,0 +1,28 @@ +import pyomo.environ as pe +from pyomo.common.collections import ComponentMap, ComponentSet + +def get_model_solution(model, variable_list, ignore_fixed_vars=False, + round_discrete_vars=True): + solution = {} + variables = ComponentMap() + fixed_vars = ComponentSet() + for var in variable_list: + if ignore_fixed_vars and var.is_fixed(): + continue + if var.is_continuous() or not round_discrete_vars: + variables[var] = pe.value(var) + else: + variables[var] = round(pe.value(var)) + if var.is_fixed(): + fixed_vars.add(var) + + solution["variables"] = variables + solution["fixed_variables"] = fixed_vars + + objectives = ComponentMap() + for obj in model.component_data_objects(pe.Objective, active=True): + objectives[obj] = pe.value(obj) + solution["objectives"] = objectives + + return solution + diff --git a/pyomo/contrib/alternative_solutions/tests/__init__.py b/pyomo/contrib/alternative_solutions/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/alternative_solutions/tests/balas_test.py b/pyomo/contrib/alternative_solutions/tests/balas_test.py new file mode 100644 index 00000000000..90b7b230b34 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/balas_test.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue Jul 19 15:13:06 2022 + +@author: jlgearh +""" + +import random + +import pyomo.environ as pe + +import pyomo.contrib.alternative_solutions.balas as bls + + +def get_random_knapsack_model(num_x_vars, num_y_vars, budget_pct, seed=1000): + random.seed(seed) + + W = budget_pct * (num_x_vars + num_y_vars) / 2 + + + model = pe.ConcreteModel() + + model.X_INDEX = pe.RangeSet(1,num_x_vars) + model.Y_INDEX = pe.RangeSet(1,num_y_vars) + + model.wu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) + model.vu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) + model.x = pe.Var(model.X_INDEX, within=pe.Binary) + + model.b = pe.Block() + model.b.wl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) + model.b.vl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) + model.b.y = pe.Var(model.Y_INDEX, within=pe.Binary) + + model.o = pe.Objective(expr=sum(model.vu[i]*model.x[i] for i in model.X_INDEX) + \ + sum(model.b.vl[i]*model.b.y[i] for i in model.Y_INDEX), sense=pe.maximize) + model.c = pe.Constraint(expr=sum(model.wu[i]*model.x[i] for i in model.X_INDEX) + \ + sum(model.b.wl[i]*model.b.y[i] for i in model.Y_INDEX)<= W) + + return model + +model = get_random_knapsack_model(4, 4, 0.2) + +alternative_solutions = bls.enumerate_binary_solutions(model, + search_mode='hamming', + max_solutions = 99) \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/knapsack_100_100_baseline.yaml b/pyomo/contrib/alternative_solutions/tests/knapsack_100_100_baseline.yaml new file mode 100644 index 00000000000..58d107c0bbb --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/knapsack_100_100_baseline.yaml @@ -0,0 +1,1814 @@ +- objectives: {o: 26.456318046876152} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.453190757661194} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': 0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.437867999364702} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.43474071014975} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.9999999999999903, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -1.3877787807814457e-17, 'x[70]': -0.0, 'x[71]': 1.0, + 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, + 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, + 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, + 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, + 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, + 'x[9]': -0.0} +- objectives: {o: 26.418993719295166} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 0.9999999999999759, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': 0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 0.9999999999999698, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.415866430080232} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.408565569050655} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.40151889154858} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 0.9999999999999851, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': 0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': -0.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.398391602333636} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': 0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': -0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.387201703324} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0000000000000322, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': 0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 1.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.384074414109026} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 0.9999999999999856, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, + 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, + 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, + 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, + 'x[35]': 1.0, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, + 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 1.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.380858862661324} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.37912760160199} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': 0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': -0.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.000000000000013, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.376895724825804} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 0.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.376000312387024} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': -0.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': 0.0} +- objectives: {o: 26.372433826620064} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': -9.992007221626409e-15, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': 0.0} +- objectives: {o: 26.36930653740512} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.36922208250021} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 0.9999999999999856, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, + 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, + 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, + 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, + 'x[35]': 1.0000000000000333, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, + 'x[3]': 1.0, 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, + 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, + 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, + 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': 0.0, + 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, + 'x[67]': 0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, + 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, + 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, + 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, + 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, + 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, + 'x[9]': -0.0} +- objectives: {o: 26.36609479328526} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.36054240454575} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': -0.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0000000000000047, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': 0.0} +- objectives: {o: 26.358554759983083} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.35741511533079} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 0.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.355847810192255} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': 1.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.352896753744492} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': 4.135580766728708e-15, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, + 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, + 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, + 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, + 'x[35]': 0.9999999999999911, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, + 'x[3]': -0.0, 'x[40]': 1.0, 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, + 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, + 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, + 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, + 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, + 'x[67]': -0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, + 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, + 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, + 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, + 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, + 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, + 'x[9]': -0.0} +- objectives: {o: 26.35130175199731} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': 1.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.34976946452954} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 0.9999999999999979, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, + 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, + 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, + 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, + 'x[35]': 1.0, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, + 'x[40]': 1.0, 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.349189018436466} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.335876350606743} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 0.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.33510949903909} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -2.220446049250313e-16, 'x[28]': -0.0, + 'x[29]': 1.0, 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, + 'x[34]': 1.0, 'x[35]': 1.0, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, + 'x[3]': 1.0, 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, + 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, + 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, + 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, + 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 0.9999999999999762, + 'x[67]': -0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, + 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 4.440892098500626e-16, + 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, + 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, + 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, + 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, + 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.334214515081303} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.331982209824165} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': 1.5626389071599078e-14, 'x[27]': -0.0, 'x[28]': -0.0, + 'x[29]': 1.0, 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, + 'x[34]': 1.0, 'x[35]': 1.0, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, + 'x[3]': 1.0000000000000027, 'x[40]': 1.0, 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, + 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, + 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, + 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, + 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, + 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, + 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, + 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, + 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, + 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, + 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, + 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.33108722586635} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': 0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.328858948447326} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 0.9999999999999928, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.000000000000013, 'x[9]': -0.0} +- objectives: {o: 26.328517674626855} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999939, + 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.32573165923238} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.000000000000013, 'x[44]': 1.0, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.325390385411904} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.324900420324166} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': -0.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.323218076964753} + variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': -0.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': 1.1102230246251565e-16, 'x[28]': -0.0, + 'x[29]': 1.0, 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, + 'x[34]': 1.0, 'x[35]': 1.0, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, + 'x[3]': 1.0, 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, + 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, + 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, + 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, + 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 0.9999999999999708, + 'x[67]': -0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, + 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -2.220446049250313e-16, + 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, + 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, + 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, + 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, + 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.322205652166968} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': 0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': -0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.32165077182623} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': 1.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0000000000000195, + 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 0.9999999999999856, + 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, + 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, + 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, + 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, + 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, + 'x[9]': -0.0} +- objectives: {o: 26.320090787749823} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': -0.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.31852348261128} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': 1.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.3184307982028} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': 0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.31710471363129} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': 1.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.31397742441634} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': 1.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.313030636051298} + variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': 0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.9999999999999902, 'x[56]': -0.0, + 'x[57]': -0.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.31017670978414} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0000000000000207, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.30990334683634} + variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': 0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.9999999999999902, 'x[56]': -0.0, + 'x[57]': -0.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.308256831485775} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': -0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.307888463942373} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 1.0, 'x[99]': 1.0, 'x[9]': 0.0} +- objectives: {o: 26.307754997822837} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': 1.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': 0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.307049420569182} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.304627708607885} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': 1.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.30420650638189} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0000000000000029, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, + 'x[57]': -0.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.304123789748115} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': 0.0, + 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999967, + 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': 1.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.30280881840351} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': -0.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.302544670856452} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': 0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.29968152918855} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': -0.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.2994173816415} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': 0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.299394528628508} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 0.9999999999999889, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, + 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, + 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': 6.1825544683813405e-15, + 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, + 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, + 'x[39]': -0.0, 'x[3]': -1.4988010832439613e-15, 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, + 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, + 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, + 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, + 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, + 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': 1.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, + 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, + 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, + 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, + 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, + 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, + 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.29793339466293} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': -0.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.296974642405235} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': 0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.999999999999997, + 'x[40]': 1.0, 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': 0.0} +- objectives: {o: 26.296764294563094} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999845, + 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': -0.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.296267239413556} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': 0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': 1.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.29624787645341} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': 0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0000000000000207, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.295394464496976} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': 1.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 0.9999999999999857, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.29393964326118} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 0.9999999999999952, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, + 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, + 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, + 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, + 'x[35]': 1.0, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, + 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 1.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.293120587238445} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.293036132333544} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.292267175282024} + variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': 1.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 0.9999999999999857, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.292116092130943} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0000000000000233, + 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, + 'x[57]': -0.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 0.9999999999999923, + 'x[67]': -0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, + 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, + 'x[78]': 0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, + 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, + 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, + 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, + 'x[9]': -0.0} +- objectives: {o: 26.29179731358475} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.000000000000006, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': -0.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.290724091813463} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.289938643454757} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 0.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.289908843118592} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.285945469944696} + variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': 0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': 0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': 0.0, + 'x[25]': -0.0, 'x[26]': 1.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': 0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': 0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': 0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': 0.0, 'x[68]': 1.0, + 'x[69]': 0.0, 'x[6]': 0.0, 'x[70]': 0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': -0.0, 'x[9]': 0.0} +- objectives: {o: 26.28586554153917} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 0.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.285083220330915} + variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': 0.0, 'x[19]': -0.0, + 'x[1]': 0.0, 'x[20]': 0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': 0.0, + 'x[25]': -0.0, 'x[26]': 0.0, 'x[27]': 0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': 0.0, + 'x[30]': 0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': 0.0, 'x[57]': 1.0, + 'x[58]': 0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': 0.0, 'x[68]': 1.0, + 'x[69]': 0.0, 'x[6]': 0.0, 'x[70]': 0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': 0.0, 'x[83]': 1.0, 'x[84]': 0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': 0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': -0.0, 'x[9]': 0.0} +- objectives: {o: 26.285027480908408} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': 0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': -0.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.284356454379076} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': -0.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.284126579740313} + variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': 0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': 0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': 0.0, 'x[57]': 1.0, + 'x[58]': 0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': -0.0, 'x[67]': 0.0, 'x[68]': 1.0, + 'x[69]': 0.0, 'x[6]': -0.0, 'x[70]': 0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': 0.0} +- objectives: {o: 26.283351959271567} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': -0.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': 0.0, + 'x[30]': 0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': -0.0, 'x[3]': 1.0000000000000233, + 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 0.9999999999999859, + 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, + 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, + 'x[56]': -0.0, 'x[57]': -0.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, + 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, + 'x[67]': -0.0, 'x[68]': 1.0, 'x[69]': 0.0, 'x[6]': 0.0, 'x[70]': -0.0, 'x[71]': 1.0, + 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, + 'x[78]': 0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, + 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, + 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, + 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, + 'x[9]': -0.0} +- objectives: {o: 26.283089783625403} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 1.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.28258328273016} + variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': 0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': 0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': 0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': 0.0, + 'x[30]': 0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': 0.0, 'x[57]': 1.0, + 'x[58]': 0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': -0.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': 0.0, 'x[68]': 1.0, + 'x[69]': 0.0, 'x[6]': -0.0, 'x[70]': 0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': -0.0, 'x[9]': 0.0} +- objectives: {o: 26.281229165164124} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': -0.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': 0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.28099929052536} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': 0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': 0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': -0.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': 0.0, 'x[6]': -0.0, 'x[70]': 0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': -0.0, 'x[9]': 0.0} +- objectives: {o: 26.280224670056615} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': -0.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0000000000000113, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, + 'x[57]': -0.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 0.9999999999999742, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.279087311652358} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 0.9999999999999858, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, + 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, + 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, + 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, + 'x[35]': 0.9999999999999734, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, + 'x[3]': 0.0, 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, + 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, + 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, + 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, + 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, + 'x[67]': 5.112577028398846e-14, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, + 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, + 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, + 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, + 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, + 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, + 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.278506865559283} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.277585000750367} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': 1.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.276710803577817} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 0.9999999999999856, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.275960022437392} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 0.9999999999999856, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, + 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 0.0, + 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, + 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, + 'x[35]': 0.999999999999973, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, + 'x[3]': -0.0, 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, + 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, + 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, + 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, + 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, + 'x[67]': 0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, + 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, + 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, + 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, + 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, + 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, + 'x[9]': -0.0} +- objectives: {o: 26.27570630847033} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': -0.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.275561304586947} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 0.9999999999999857, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0000000000000084, + 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, + 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, + 'x[67]': -0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, + 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, + 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, + 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': -0.0, 'x[88]': -0.0, + 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, + 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, + 'x[9]': -0.0} +- objectives: {o: 26.27537957634433} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.274670539727} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.274321605031258} + variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': 1.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999969, + 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': 0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.273583514362873} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.272579019255378} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 0.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.272453945523264} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999943, + 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': 1.0000000000000013, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, + 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, + 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, + 'x[9]': -0.0} diff --git a/pyomo/contrib/alternative_solutions/tests/knapsack_100_100_comp_baseline.yaml b/pyomo/contrib/alternative_solutions/tests/knapsack_100_100_comp_baseline.yaml new file mode 100644 index 00000000000..392bdeabddd --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/knapsack_100_100_comp_baseline.yaml @@ -0,0 +1,11 @@ +{'x[100]': 1.0, 'x[11]': 0.18, 'x[12]': 1.0, 'x[14]': 0.030000000000000044, 'x[15]': 0.9099999999999991, + 'x[16]': 1.0, 'x[18]': 0.01, 'x[19]': 0.91, 'x[20]': 0.03, 'x[23]': 0.34, 'x[26]': 0.030000000000000155, + 'x[29]': 1.0, 'x[31]': 0.19, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 0.9899999999999998, + 'x[37]': 1.0, 'x[3]': 0.6100000000000003, 'x[40]': 1.0, 'x[41]': 0.18, 'x[43]': 0.49000000000000016, + 'x[44]': 0.9999999999999999, 'x[45]': 0.02, 'x[48]': 1.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 0.99, 'x[52]': 1.0, 'x[54]': 1.0, 'x[55]': 0.4699999999999999, 'x[57]': 0.88, + 'x[5]': 0.9999999999999999, 'x[61]': 1.0, 'x[62]': 0.98, 'x[66]': 0.9599999999999994, + 'x[67]': 0.030000000000000512, 'x[68]': 1.0, 'x[71]': 0.8999999999999996, 'x[76]': 0.010000000000000002, + 'x[79]': 0.99, 'x[80]': 1.0, 'x[83]': 0.9899999999999998, 'x[84]': 0.020000000000000014, + 'x[86]': 0.65, 'x[87]': 0.95, 'x[8]': 1.0, 'x[91]': 0.9999999999999997, 'x[93]': 1.0, + 'x[94]': 0.99, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.04, 'x[99]': 0.7900000000000001} diff --git a/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_baseline.yaml b/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_baseline.yaml new file mode 100644 index 00000000000..7d770d5f76e --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_baseline.yaml @@ -0,0 +1,180 @@ +- objectives: {o: 26.456318046876152} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.453190757661194} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.437867999364702} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.43474071014975} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': 0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.418993719295184} + variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': 0.0, 'x[12]': 1.0, 'x[13]': 0.0, + 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': 0.0, 'x[18]': 0.0, 'x[19]': 1.0, + 'x[1]': 0.0, 'x[20]': 0.0, 'x[21]': 0.0, 'x[22]': 0.0, 'x[23]': 1.0, 'x[24]': 0.0, + 'x[25]': 0.0, 'x[26]': 0.0, 'x[27]': 0.0, 'x[28]': 0.0, 'x[29]': 1.0, 'x[2]': 0.0, + 'x[30]': 0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': 0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': 0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': 0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': 0.0, 'x[46]': 0.0, + 'x[47]': 0.0, 'x[48]': 1.0, 'x[49]': 0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': 0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': 0.0, 'x[57]': 1.0, + 'x[58]': 0.0, 'x[59]': 0.0, 'x[5]': 1.0, 'x[60]': 0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': 0.0, 'x[64]': 0.0, 'x[65]': 0.0, 'x[66]': 1.0, 'x[67]': 0.0, 'x[68]': 1.0, + 'x[69]': 0.0, 'x[6]': 0.0, 'x[70]': 0.0, 'x[71]': 1.0, 'x[72]': 0.0, 'x[73]': 0.0, + 'x[74]': 0.0, 'x[75]': 0.0, 'x[76]': 0.0, 'x[77]': 0.0, 'x[78]': 0.0, 'x[79]': 1.0, + 'x[7]': 0.0, 'x[80]': 1.0, 'x[81]': 0.0, 'x[82]': 0.0, 'x[83]': 1.0, 'x[84]': 0.0, + 'x[85]': 0.0, 'x[86]': 0.0, 'x[87]': 1.0, 'x[88]': 0.0, 'x[89]': 0.0, 'x[8]': 1.0, + 'x[90]': 0.0, 'x[91]': 1.0, 'x[92]': 0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': 0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': 1.0, 'x[9]': 0.0} +- objectives: {o: 26.415866430080232} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.408565569050655} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999857, + 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.401518891548587} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.398391602333636} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.387201703323992} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 0.9999999999999898, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 1.0, 'x[99]': 1.0, 'x[9]': -0.0} diff --git a/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_comp_baseline.yaml b/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_comp_baseline.yaml new file mode 100644 index 00000000000..157f3497093 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_comp_baseline.yaml @@ -0,0 +1,8 @@ +{'x[100]': 1.0, 'x[11]': 0.1, 'x[12]': 1.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[19]': 1.0, + 'x[23]': 0.5, 'x[29]': 1.0, 'x[31]': 0.2, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, + 'x[35]': 1.0, 'x[37]': 1.0, 'x[3]': 0.7999999999999986, 'x[40]': 1.0, 'x[43]': 0.6, + 'x[44]': 1.0, 'x[48]': 1.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, + 'x[54]': 1.0, 'x[55]': 0.4, 'x[57]': 0.9999999999999989, 'x[5]': 1.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[66]': 1.0, 'x[68]': 1.0, 'x[71]': 1.0, 'x[79]': 1.0, 'x[80]': 1.0, + 'x[83]': 1.0, 'x[86]': 0.6, 'x[87]': 0.8, 'x[8]': 1.0, 'x[91]': 1.0, 'x[93]': 1.0, + 'x[94]': 1.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.1, 'x[99]': 0.9} diff --git a/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml b/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml new file mode 100644 index 00000000000..825df0bb064 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml @@ -0,0 +1,180 @@ +- objectives: {o: 26.456318046876152} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.453190757661194} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.437867999364702} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.43474071014975} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.418993719295177} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -9.769962616701378e-15, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.415866430080232} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.408565569050655} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999853, + 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.401518891548587} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 0.9999999999999931, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': -0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.398391602333636} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': 0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.387201703323992} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 0.9999999999999909, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 1.0, 'x[99]': 1.0, 'x[9]': -0.0} diff --git a/pyomo/contrib/alternative_solutions/tests/knapsack_100_1_baseline.yaml b/pyomo/contrib/alternative_solutions/tests/knapsack_100_1_baseline.yaml new file mode 100644 index 00000000000..e5902fa054f --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/knapsack_100_1_baseline.yaml @@ -0,0 +1,18 @@ +- objectives: {o: 26.456318046876152} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} diff --git a/pyomo/contrib/alternative_solutions/tests/obbt_test.py b/pyomo/contrib/alternative_solutions/tests/obbt_test.py new file mode 100644 index 00000000000..7565febab3d --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/obbt_test.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Aug 4 15:59:24 2022 + +@author: jlgearh +""" +import random + +import pyomo.environ as pe + +from pyomo.contrib.alternative_solutions.obbt import obbt_analysis + +def get_random_knapsack_model(num_x_vars, num_y_vars, budget_pct, seed=1000): + random.seed(seed) + + W = budget_pct * (num_x_vars + num_y_vars) / 2 + + + model = pe.ConcreteModel() + + model.X_INDEX = pe.RangeSet(1,num_x_vars) + model.Y_INDEX = pe.RangeSet(1,num_y_vars) + + model.wu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) + model.vu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) + model.x = pe.Var(model.X_INDEX, within=pe.NonNegativeIntegers) + + model.b = pe.Block() + model.b.wl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) + model.b.vl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) + model.b.y = pe.Var(model.Y_INDEX, within=pe.NonNegativeReals) + + model.o = pe.Objective(expr=sum(model.vu[i]*model.x[i] for i in model.X_INDEX) + \ + sum(model.b.vl[i]*model.b.y[i] for i in model.Y_INDEX), sense=pe.maximize) + model.c = pe.Constraint(expr=sum(model.wu[i]*model.x[i] for i in model.X_INDEX) + \ + sum(model.b.wl[i]*model.b.y[i] for i in model.Y_INDEX)<= W) + + return model + +model = get_random_knapsack_model(4, 4, 0.2) +result = obbt_analysis(model, variables='all', rel_opt_gap=None, + abs_gap=None, already_solved=False, + solver='gurobi', solver_options={}, + use_persistent_solver = False, tee=True, + refine_bounds=False) \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_results.yaml b/pyomo/contrib/alternative_solutions/tests/test_results.yaml new file mode 100644 index 00000000000..825df0bb064 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/test_results.yaml @@ -0,0 +1,180 @@ +- objectives: {o: 26.456318046876152} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.453190757661194} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.437867999364702} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.43474071014975} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.418993719295177} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -9.769962616701378e-15, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.415866430080232} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.408565569050655} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999853, + 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, + 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, + 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, + 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, + 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, + 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, + 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, + 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, + 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, + 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, + 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} +- objectives: {o: 26.401518891548587} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 0.9999999999999931, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': -0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.398391602333636} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': 0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} +- objectives: {o: 26.387201703323992} + variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, + 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, + 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, + 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, + 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 0.9999999999999909, + 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, + 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, + 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, + 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, + 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, + 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, + 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, + 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, + 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, + 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, + 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, + 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 1.0, 'x[99]': 1.0, 'x[9]': -0.0} diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py new file mode 100644 index 00000000000..d94c2a8fcd0 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -0,0 +1,81 @@ +import os +from os.path import join +import yaml +import pytest +import random + +from munch import unmunchify +import pyutilib.misc +import pyomo.environ as pe +from pyomo.contrib.alternative_solutions.solnpool import gurobi_generate_solutions +from pyomo.common.fileutils import this_file_dir +from pyomo.contrib.alternative_solutions.comparison import consensus + +currdir = this_file_dir() + + +def knapsack(N): + random.seed(1000) + + N = N + W = N/10.0 + + + model = pe.ConcreteModel() + + model.INDEX = pe.RangeSet(1,N) + + model.w = pe.Param(model.INDEX, initialize=lambda model, i : random.uniform(0.0,1.0), within=pe.Reals) + + model.v = pe.Param(model.INDEX, initialize=lambda model, i : random.uniform(0.0,1.0), within=pe.Reals) + + model.x = pe.Var(model.INDEX, within=pe.Boolean) + + model.o = pe.Objective(expr=sum(model.v[i]*model.x[i] for i in model.INDEX), sense=pe.maximize) + + model.c = pe.Constraint(expr=sum(model.w[i]*model.x[i] for i in model.INDEX) <= W) + + return model + + +def run(testname, model, N, debug=False): + solutions = gurobi_generate_solutions(model=model, max_solutions=N) + print(solutions) + # Verify final results + + results = [unmunchify(soln) for soln in solutions] + output = yaml.dump(results, default_flow_style=None) + outputfile = join(currdir, "{}_results.yaml".format(testname)) + with open(outputfile, "w") as OUTPUT: + OUTPUT.write(output) + + baselinefile = join(currdir, "{}_baseline.yaml".format(testname)) + tmp = pyutilib.misc.compare_file(outputfile, baselinefile, tolerance=1e-7) + assert tmp[0] == False, "Files differ: diff {} {}".format(outputfile, baselinefile) + os.remove(outputfile) + + if N>1: + # Verify consensus pattern + + comp = consensus(results) + output = yaml.dump(comp, default_flow_style=None) + outputfile = join(currdir, "{}_comp_results.yaml".format(testname)) + with open(outputfile, "w") as OUTPUT: + OUTPUT.write(output) + + baselinefile = join(currdir, "{}_comp_baseline.yaml".format(testname)) + tmp = pyutilib.misc.compare_file(outputfile, baselinefile, tolerance=1e-7) + assert tmp[0] == False, "Files differ: diff {} {}".format(outputfile, baselinefile) + os.remove(outputfile) + + + +def test_knapsack_100_1(): + run('knapsack_100_1', knapsack(100), 1) + +def test_knapsack_100_10(): + run('knapsack_100_10', knapsack(100), 10) + +def test_knapsack_100_100(): + run('knapsack_100_100', knapsack(100), 100) + diff --git a/pyomo/contrib/alternative_solutions/var_utils.py b/pyomo/contrib/alternative_solutions/var_utils.py new file mode 100644 index 00000000000..f5d5d7c9b83 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/var_utils.py @@ -0,0 +1,136 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.collections import ComponentSet +import pyomo.environ as pe +import pyomo.util.vars_from_expressions as vfe +import pyomo.contrib.alternative_solutions.aos_utils as aos_utils + +""" +This file provides a collection of utilites for gathering and filtering +variables from a model to support analysis of alternative solutions, and other +related tasks. +""" + +def _filter_model_variables(variable_set, var_generator, + include_continuous=True, include_binary=True, + include_integer=True, include_fixed=False): + """Filters variables from a variable generator and adds them to a set.""" + for var in var_generator: + if var in variable_set or var.is_fixed() and not include_fixed: + continue + if (var.is_continuous() and include_continuous or + var.is_binary() and include_binary or + var.is_integer() and include_integer): + variable_set.add(var) + +def get_model_variables(model, components='all', include_continuous=True, + include_binary=True, include_integer=True, + include_fixed=False): + ''' + Gathers and returns all or a subset of varaibles from a Pyomo model. + + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model. + components: 'all' or a collection Pyomo components + The components from which variables should be collected. 'all' + indicates that all variables will be included. Alternatively, a + collection of Pyomo Blocks, Constraints, or Variables (indexed or + non-indexed) from which variables will be gathered can be provided. + By default all variables in sub-Blocks will be added if a Block + element is provided. A tuple element with the format (Block, False) + indicates that only variables from the Block should be added but + not any of its sub-Blocks. + include_continuous : boolean + Boolean indicating that continuous variables should be included. + include_binary : boolean + Boolean indicating that binary variables should be included. + include_integer : boolean + Boolean indicating that integer variables should be included. + include_fixed : boolean + Boolean indicating that fixed variables should be included. + + Returns + ------- + variable_set + A Pyomo ComponentSet containing _GeneralVarData variables. + ''' + + aos_utils._is_concrete_model(model) + assert isinstance(include_continuous, bool), \ + 'include_continuous must be a Boolean' + assert isinstance(include_binary, bool), 'include_binary must be a Boolean' + assert isinstance(include_integer, bool), \ + 'include_integer must be a Boolean' + assert isinstance(include_fixed, bool), 'include_fixed must be a Boolean' + + variable_set = ComponentSet() + + if components == 'all': + var_generator = vfe.get_vars_from_components(model, pe.Constraint, + include_fixed=\ + include_fixed) + _filter_model_variables(variable_set, var_generator, + include_continuous, include_binary, + include_integer, include_fixed) + print('im here') + else: + assert hasattr(components, '__iter__'), \ + ('components parameters must be an iterable collection of Pyomo' + 'objects' + ) + + for comp in components: + if (hasattr(comp, 'ctype') and comp.ctype == pe.Block): + blocks = comp.values() if comp.is_indexed() else (comp,) + for item in blocks: + variables = vfe.get_vars_from_components(item, + pe.Constraint, include_fixed=include_fixed) + _filter_model_variables(variable_set, variables, + include_continuous, include_binary, include_integer, + include_fixed) + elif (isinstance(comp, tuple) and isinstance(comp[1], bool) and + hasattr(comp[0], 'ctype') and comp[0].ctype == pe.Block): + block = comp[0] + descend_into = pe.Block if comp[1] else False + blocks = block.values() if block.is_indexed() else (block,) + for item in blocks: + variables = vfe.get_vars_from_components(item, + pe.Constraint, include_fixed=include_fixed, + descend_into=descend_into) + _filter_model_variables(variable_set, variables, + include_continuous, include_binary, include_integer, + include_fixed) + elif hasattr(comp, 'ctype') and comp.ctype == pe.Constraint: + constraints = comp.values() if comp.is_indexed() else (comp,) + for item in constraints: + variables = pe.expr.identify_variables(item.expr, + include_fixed=include_fixed) + _filter_model_variables(variable_set, variables, + include_continuous, include_binary, include_integer, + include_fixed) + elif (hasattr(comp, 'ctype') and comp.ctype == pe.Var): + variables = comp.values() if comp.is_indexed() else (comp,) + _filter_model_variables(variable_set, variables, + include_continuous, include_binary, include_integer, + include_fixed) + else: + print(('No variables added for unrecognized component {}.'). + format(comp)) + + return variable_set + + + +def check_variables(model, variables): + pass \ No newline at end of file From 15db08e20aceb087d585e55a6ed3ed7d2c8758e4 Mon Sep 17 00:00:00 2001 From: jlgearh Date: Mon, 27 Feb 2023 21:17:26 -0700 Subject: [PATCH 0002/3044] - Completed an initial version of the solnpool wrapper. - Updated the solution.py to create a solution class. --- .../contrib/alternative_solutions/solnpool.py | 58 +- .../contrib/alternative_solutions/solution.py | 101 +- .../tests/knapsack_100_10_results.yaml | 5564 ++++++++++++++++- 3 files changed, 5500 insertions(+), 223 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index e8440c6e9e1..18fb3210448 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -1,33 +1,49 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib import appsi from pyomo.contrib.alternative_solutions import aos_utils, var_utils, solution -def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=0.0, - abs_opt_gap=0.0, search_mode=2, +def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=None, + abs_opt_gap=None, search_mode=2, round_discrete_vars=True, solver_options={}): ''' Finds alternative optimal solutions for discrete variables using Gurobi's built-in Solution Pool capability. See the Gurobi Solution Pool - documentation for additional details. This function uses the Gurobi - Auto-Persistent Pyomo Solver interface (appsi). + documentation for additional details. This function requires the use of + the Gurobi Auto-Persistent Pyomo Solver interface (appsi). Parameters ---------- model : ConcreteModel - A concrete Pyomo model + A concrete Pyomo model. max_solutions : int or None The maximum number of solutions to generate. None indictes no upper limit. Note, using None could lead to a large number of solutions. This parameter maps to the PoolSolutions parameter in Gurobi. - rel_opt_gap : non-negative float + rel_opt_gap : non-negative float or None The relative optimality gap for allowable alternative solutions. + None implies that there is no limit on the relative optimality gap + (i.e. that any feasible solution can be considered by Gurobi). This parameter maps to the PoolGap parameter in Gurobi. - abs_opt_gap : non-negative float + abs_opt_gap : non-negative float or None The absolute optimality gap for allowable alternative solutions. + None implies that there is no limit on the absolute optimality gap + (i.e. that any feasible solution can be considered by Gurobi). This parameter maps to the PoolGapAbs parameter in Gurobi. search_mode : 0, 1, or 2 - Indicates the mode that is used to generate alternative solutions. - Mode 2 should typically be used as it finds the best n solutions. - Mode 0 finds a single optimal solution. Mode 1 will generate n + Indicates the Solution Pool mode that is used to generate + alternative solutions in Gurobi. Mode 2 should typically be used as + it finds the best n solutions. Mode 0 finds a single optimal + solution (i.e. the standard mode in Gurobi). Mode 1 will generate n solutions without providing guarantees on their quality. This parameter maps to the PoolSearchMode in Gurobi. round_discrete_vars : boolean @@ -39,17 +55,19 @@ def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=0.0, Returns ------- solutions - A list of solution dictionaries. - [solution] + A list of Solution objects. + [Solution] ''' # Validate inputs aos_utils._is_concrete_model(model) num_solutions = aos_utils._get_max_solutions(max_solutions) - assert isinstance(rel_opt_gap, float) and rel_opt_gap >= 0, \ - 'rel_opt_gap must be a non-negative float' - assert isinstance(abs_opt_gap, float) and abs_opt_gap >= 0, \ - 'abs_opt_gap must be a non-negative float' + assert (isinstance(rel_opt_gap, float) and rel_opt_gap >= 0) or \ + isinstance(rel_opt_gap, type(None)), \ + 'rel_opt_gap must be a non-negative float or None' + assert (isinstance(abs_opt_gap, float) and abs_opt_gap >= 0) or \ + isinstance(abs_opt_gap, type(None)), \ + 'abs_opt_gap must be a non-negative float or None' assert search_mode in [0, 1, 2], 'search_mode must be 0, 1, or 2' assert isinstance(round_discrete_vars, bool), \ 'round_discrete_vars must be a Boolean' @@ -60,8 +78,10 @@ def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=0.0, opt.set_instance(model) opt.set_gurobi_param('PoolSolutions', num_solutions) opt.set_gurobi_param('PoolSearchMode', search_mode) - opt.set_gurobi_param('PoolGap', rel_opt_gap) - opt.set_gurobi_param('PoolGapAbs', abs_opt_gap) + if rel_opt_gap is not None: + opt.set_gurobi_param('PoolGap', rel_opt_gap) + if abs_opt_gap is not None: + opt.set_gurobi_param('PoolGapAbs', abs_opt_gap) results = opt.solve(model) assert results.termination_condition == \ appsi.base.TerminationCondition.optimal, \ @@ -75,6 +95,6 @@ def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=0.0, solutions = [] for i in range(solution_count): results.solution_loader.load_vars(solution_number=i) - solutions.append(solution.get_model_solution(model, variables)) + solutions.append(solution.Solution(model, variables)) return solutions \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/solution.py b/pyomo/contrib/alternative_solutions/solution.py index 64c4b494045..c83850f773b 100644 --- a/pyomo/contrib/alternative_solutions/solution.py +++ b/pyomo/contrib/alternative_solutions/solution.py @@ -1,28 +1,81 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pe from pyomo.common.collections import ComponentMap, ComponentSet -def get_model_solution(model, variable_list, ignore_fixed_vars=False, - round_discrete_vars=True): - solution = {} - variables = ComponentMap() - fixed_vars = ComponentSet() - for var in variable_list: - if ignore_fixed_vars and var.is_fixed(): - continue - if var.is_continuous() or not round_discrete_vars: - variables[var] = pe.value(var) - else: - variables[var] = round(pe.value(var)) - if var.is_fixed(): - fixed_vars.add(var) - - solution["variables"] = variables - solution["fixed_variables"] = fixed_vars - - objectives = ComponentMap() - for obj in model.component_data_objects(pe.Objective, active=True): - objectives[obj] = pe.value(obj) - solution["objectives"] = objectives - - return solution +class Solution: + """ + A class to store solutions from a Pyomo model. + + Attributes + ---------- + variables : ComponentMap + A map between Pyomo variable objects and their values for a solution. + fixed_vars : ComponentSet + The set of Pyomo variables that are fixed in a solution. + objectives : ComponentMap + A map between Pyomo objective objects and their values for a solution. + + Methods + ------- + pprint(): + Prints the solution. + """ + + def __init__(self, model, variable_list, ignore_fixed_vars=False, + round_discrete_vars=True): + """ + Constructs a Pyomo Solution object. + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model. + variables: A collection of Pyomo _GenereralVarData variables + The variables for which the solution will be stored. + ignore_fixed_vars : boolean + Boolean indicating that fixed variables should not be added to + the solution. + round_discrete_vars : boolean + Boolean indicating that discrete values should be rounded to + the nearest integer in the solutions results. + """ + + self.variables = ComponentMap() + self.fixed_vars = ComponentSet() + for var in variable_list: + if ignore_fixed_vars and var.is_fixed(): + continue + if var.is_continuous() or not round_discrete_vars: + self.variables[var] = pe.value(var) + else: + self.variables[var] = round(pe.value(var)) + if var.is_fixed(): + self.fixed_vars.add(var) + + self.objectives = ComponentMap() + for obj in model.component_data_objects(pe.Objective, active=True): + self.objectives[obj] = pe.value(obj) + + def pprint(self): + '''Print the solution variable and objective values.''' + fixed_string = "(fixed)" + print("Variable: Value") + for variable, value in self.variables.items(): + if variable in self.fixed_vars: + print("{}: {} {}".format(variable.name, value, fixed_string)) + else: + print("{}: {}".format(variable.name, value)) + print() + print("Objective: Value") + for objective, value in self.objectives.items(): + print("{}: {}".format(objective.name, value)) \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml b/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml index 825df0bb064..2318a501260 100644 --- a/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml +++ b/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml @@ -1,180 +1,5384 @@ -- objectives: {o: 26.456318046876152} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.453190757661194} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.437867999364702} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.43474071014975} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.418993719295177} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -9.769962616701378e-15, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.415866430080232} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.408565569050655} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999853, - 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.401518891548587} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 0.9999999999999931, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': -0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.398391602333636} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': 0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.387201703323992} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 0.9999999999999909, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 1.0, 'x[99]': 1.0, 'x[9]': -0.0} +- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution + fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet + _data: !!python/tuple [] + objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735881266400: !!python/tuple + - &id001 !!python/object/new:pyomo.core.base.objective.ScalarObjective + state: + - *id001 + - null + - true + - -1 + - &id119 !!python/object/new:pyomo.core.expr.numeric_expr.SumExpression + state: + - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.726594449656969 + - &id002 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - &id003 !!python/object/new:pyomo.core.base.var.IndexedVar + state: + - _constructed: true + _ctype: &id008 !!python/name:pyomo.core.base.var.Var '' + _data: + 1: *id002 + 2: &id013 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 2 + - -0.0 + - null + - null + - &id004 !!python/object/apply:pyomo.core.base.global_set._get_global_set [ + Boolean] + - false + - false + 3: &id014 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 3 + - 1.0 + - null + - null + - *id004 + - false + - false + 4: &id015 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 4 + - 1.0 + - null + - null + - *id004 + - false + - false + 5: &id016 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 5 + - 1.0 + - null + - null + - *id004 + - false + - false + 6: &id017 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 6 + - -0.0 + - null + - null + - *id004 + - false + - false + 7: &id018 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 7 + - -0.0 + - null + - null + - *id004 + - false + - false + 8: &id019 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 8 + - 1.0 + - null + - null + - *id004 + - false + - false + 9: &id020 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 9 + - -0.0 + - null + - null + - *id004 + - false + - false + 10: &id021 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 10 + - -0.0 + - null + - null + - *id004 + - false + - false + 11: &id022 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 11 + - -0.0 + - null + - null + - *id004 + - false + - false + 12: &id023 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 12 + - 1.0 + - null + - null + - *id004 + - false + - false + 13: &id024 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 13 + - -0.0 + - null + - null + - *id004 + - false + - false + 14: &id025 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 14 + - -0.0 + - null + - null + - *id004 + - false + - false + 15: &id026 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 15 + - 1.0 + - null + - null + - *id004 + - false + - false + 16: &id027 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 16 + - 1.0 + - null + - null + - *id004 + - false + - false + 17: &id028 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 17 + - -0.0 + - null + - null + - *id004 + - false + - false + 18: &id029 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 18 + - -0.0 + - null + - null + - *id004 + - false + - false + 19: &id030 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 19 + - 1.0 + - null + - null + - *id004 + - false + - false + 20: &id031 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 20 + - -0.0 + - null + - null + - *id004 + - false + - false + 21: &id032 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 21 + - -0.0 + - null + - null + - *id004 + - false + - false + 22: &id033 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 22 + - -0.0 + - null + - null + - *id004 + - false + - false + 23: &id034 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 23 + - 1.0 + - null + - null + - *id004 + - false + - false + 24: &id035 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 24 + - -0.0 + - null + - null + - *id004 + - false + - false + 25: &id036 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 25 + - -0.0 + - null + - null + - *id004 + - false + - false + 26: &id037 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 26 + - -0.0 + - null + - null + - *id004 + - false + - false + 27: &id038 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 27 + - -0.0 + - null + - null + - *id004 + - false + - false + 28: &id039 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 28 + - -0.0 + - null + - null + - *id004 + - false + - false + 29: &id040 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 29 + - 1.0 + - null + - null + - *id004 + - false + - false + 30: &id041 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 30 + - -0.0 + - null + - null + - *id004 + - false + - false + 31: &id042 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 31 + - -0.0 + - null + - null + - *id004 + - false + - false + 32: &id043 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 32 + - 1.0 + - null + - null + - *id004 + - false + - false + 33: &id044 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 33 + - 1.0 + - null + - null + - *id004 + - false + - false + 34: &id045 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 34 + - 1.0 + - null + - null + - *id004 + - false + - false + 35: &id046 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 35 + - 0.9999999999999909 + - null + - null + - *id004 + - false + - false + 36: &id047 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 36 + - -0.0 + - null + - null + - *id004 + - false + - false + 37: &id048 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 37 + - 1.0 + - null + - null + - *id004 + - false + - false + 38: &id049 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 38 + - -0.0 + - null + - null + - *id004 + - false + - false + 39: &id050 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 39 + - -0.0 + - null + - null + - *id004 + - false + - false + 40: &id051 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 40 + - 1.0 + - null + - null + - *id004 + - false + - false + 41: &id052 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 41 + - -0.0 + - null + - null + - *id004 + - false + - false + 42: &id053 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 42 + - -0.0 + - null + - null + - *id004 + - false + - false + 43: &id054 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 43 + - -0.0 + - null + - null + - *id004 + - false + - false + 44: &id055 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 44 + - 1.0 + - null + - null + - *id004 + - false + - false + 45: &id056 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 45 + - -0.0 + - null + - null + - *id004 + - false + - false + 46: &id057 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 46 + - -0.0 + - null + - null + - *id004 + - false + - false + 47: &id058 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 47 + - -0.0 + - null + - null + - *id004 + - false + - false + 48: &id059 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 48 + - 1.0 + - null + - null + - *id004 + - false + - false + 49: &id060 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 49 + - -0.0 + - null + - null + - *id004 + - false + - false + 50: &id061 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 50 + - 1.0 + - null + - null + - *id004 + - false + - false + 51: &id062 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 51 + - 1.0 + - null + - null + - *id004 + - false + - false + 52: &id063 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 52 + - 1.0 + - null + - null + - *id004 + - false + - false + 53: &id064 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 53 + - -0.0 + - null + - null + - *id004 + - false + - false + 54: &id065 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 54 + - 1.0 + - null + - null + - *id004 + - false + - false + 55: &id066 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 55 + - -0.0 + - null + - null + - *id004 + - false + - false + 56: &id067 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 56 + - -0.0 + - null + - null + - *id004 + - false + - false + 57: &id068 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 57 + - 1.0 + - null + - null + - *id004 + - false + - false + 58: &id069 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 58 + - -0.0 + - null + - null + - *id004 + - false + - false + 59: &id070 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 59 + - -0.0 + - null + - null + - *id004 + - false + - false + 60: &id071 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 60 + - -0.0 + - null + - null + - *id004 + - false + - false + 61: &id072 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 61 + - 1.0 + - null + - null + - *id004 + - false + - false + 62: &id073 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 62 + - 1.0 + - null + - null + - *id004 + - false + - false + 63: &id074 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 63 + - -0.0 + - null + - null + - *id004 + - false + - false + 64: &id075 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 64 + - -0.0 + - null + - null + - *id004 + - false + - false + 65: &id076 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 65 + - -0.0 + - null + - null + - *id004 + - false + - false + 66: &id077 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 66 + - 1.0 + - null + - null + - *id004 + - false + - false + 67: &id078 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 67 + - -0.0 + - null + - null + - *id004 + - false + - false + 68: &id079 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 68 + - 1.0 + - null + - null + - *id004 + - false + - false + 69: &id080 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 69 + - -0.0 + - null + - null + - *id004 + - false + - false + 70: &id081 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 70 + - -0.0 + - null + - null + - *id004 + - false + - false + 71: &id082 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 71 + - 1.0 + - null + - null + - *id004 + - false + - false + 72: &id083 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 72 + - -0.0 + - null + - null + - *id004 + - false + - false + 73: &id084 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 73 + - -0.0 + - null + - null + - *id004 + - false + - false + 74: &id085 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 74 + - -0.0 + - null + - null + - *id004 + - false + - false + 75: &id086 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 75 + - -0.0 + - null + - null + - *id004 + - false + - false + 76: &id087 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 76 + - -0.0 + - null + - null + - *id004 + - false + - false + 77: &id088 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 77 + - -0.0 + - null + - null + - *id004 + - false + - false + 78: &id089 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 78 + - -0.0 + - null + - null + - *id004 + - false + - false + 79: &id090 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 79 + - 1.0 + - null + - null + - *id004 + - false + - false + 80: &id091 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 80 + - 1.0 + - null + - null + - *id004 + - false + - false + 81: &id092 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 81 + - -0.0 + - null + - null + - *id004 + - false + - false + 82: &id093 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 82 + - -0.0 + - null + - null + - *id004 + - false + - false + 83: &id094 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 83 + - 1.0 + - null + - null + - *id004 + - false + - false + 84: &id095 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 84 + - -0.0 + - null + - null + - *id004 + - false + - false + 85: &id096 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 85 + - -0.0 + - null + - null + - *id004 + - false + - false + 86: &id097 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 86 + - 1.0 + - null + - null + - *id004 + - false + - false + 87: &id098 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 87 + - 1.0 + - null + - null + - *id004 + - false + - false + 88: &id099 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 88 + - -0.0 + - null + - null + - *id004 + - false + - false + 89: &id100 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 89 + - -0.0 + - null + - null + - *id004 + - false + - false + 90: &id101 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 90 + - -0.0 + - null + - null + - *id004 + - false + - false + 91: &id102 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 91 + - 1.0 + - null + - null + - *id004 + - false + - false + 92: &id103 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 92 + - -0.0 + - null + - null + - *id004 + - false + - false + 93: &id104 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 93 + - 1.0 + - null + - null + - *id004 + - false + - false + 94: &id105 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 94 + - 1.0 + - null + - null + - *id004 + - false + - false + 95: &id106 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 95 + - -0.0 + - null + - null + - *id004 + - false + - false + 96: &id107 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 96 + - 1.0 + - null + - null + - *id004 + - false + - false + 97: &id108 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 97 + - 1.0 + - null + - null + - *id004 + - false + - false + 98: &id109 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 98 + - 1.0 + - null + - null + - *id004 + - false + - false + 99: &id110 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 99 + - 1.0 + - null + - null + - *id004 + - false + - false + 100: &id111 !!python/object/new:pyomo.core.base.var._GeneralVarData + state: + - *id003 + - 100 + - 1.0 + - null + - null + - *id004 + - false + - false + _dense: true + _implicit_subsets: null + _index_set: &id005 !!python/object/new:pyomo.core.base.set.FiniteScalarRangeSet + state: + - *id005 + - null + - !!python/tuple + - !!python/object:pyomo.core.base.range.NumericRange + closed: !!python/tuple [true, true] + end: 100 + start: 1 + step: 1 + - _constructed: true + _ctype: &id007 !!python/name:pyomo.core.base.set.RangeSet '' + _init_bounds: null + _init_data: !!python/tuple + - !!python/tuple [1, 100] + - !!python/tuple [] + _init_filter: null + _init_validate: null + _name: INDEX + _parent: &id006 !!python/object/new:pyomo.core.base.PyomoModel.ConcreteModel + state: + - *id006 + - null + - true + - INDEX: *id005 + _constructed: true + _ctype: !!python/name:pyomo.core.base.block.Block '' + _ctypes: + *id007: [0, 0, 1] + ? &id009 !!python/name:pyomo.core.base.param.Param '' + : [1, 2, 2] + *id008: [3, 3, 1] + ? &id118 !!python/name:pyomo.core.base.objective.Objective '' + : [4, 4, 1] + ? &id113 !!python/name:pyomo.core.base.constraint.Constraint '' + : [5, 5, 1] + _data: + null: *id006 + _decl: {INDEX: 0, c: 5, o: 4, v: 2, w: 1, x: 3} + _decl_order: + - !!python/tuple + - *id005 + - null + - !!python/tuple + - &id117 !!python/object/new:pyomo.core.base.param.IndexedParam + state: + - _constructed: true + _ctype: *id009 + _data: {1: 0.7773566427005639, 2: 0.6698255595592497, + 3: 0.09913960392481702, 4: 0.35297051119014544, + 5: 0.4679077429008419, 6: 0.5346837414708775, + 7: 0.9783090609123973, 8: 0.13031535015865903, + 9: 0.6712434682302663, 10: 0.36422941594737557, + 11: 0.48883570716198577, 12: 0.20301221073405373, + 13: 0.6661983755713592, 14: 0.2276630312069321, + 15: 0.4580640582967631, 16: 0.040722397554957435, + 17: 0.9742897953778286, 18: 0.4874760742689066, + 19: 0.4616138636373597, 20: 0.7141471558082002, + 21: 0.4157281494999725, 22: 0.888011688001529, + 23: 0.023293448723771704, 24: 0.8335062677845465, + 25: 0.4684947409975081, 26: 0.8114798126442795, + 27: 0.9455914886158723, 28: 0.9830883781948988, + 29: 0.1761820755785306, 30: 0.698655759576308, + 31: 0.10885571131238292, 32: 0.16026373420620188, + 33: 0.09286027402458918, 34: 0.3140620798928404, + 35: 0.01653868433866723, 36: 0.8540491257363622, + 37: 0.2910160386456968, 38: 0.7800475863350328, + 39: 0.5480965161255696, 40: 0.19433067669976123, + 41: 0.2920382721805297, 42: 0.3194527773994337, + 43: 0.6585982235379076, 44: 0.23152103541222924, + 45: 0.6194303369953537, 46: 0.8953386098022104, + 47: 0.8694342085696831, 48: 0.2938069356684523, + 49: 0.45820480858054946, 50: 0.4849797978711191, + 51: 0.2803882693587225, 52: 0.32895694635060024, + 53: 0.9842424240265042, 54: 0.011944137920874343, + 55: 0.14290076829328524, 56: 0.6519772165446712, + 57: 0.07499317994693244, 58: 0.29207870228110877, + 59: 0.7934429721917705, 60: 0.9115931008709737, + 61: 0.3703917795895437, 62: 0.20528221118666345, + 63: 0.880081326784678, 64: 0.6325664501560831, + 65: 0.503514326058558, 66: 0.3308435596710889, + 67: 0.3474001835074456, 68: 0.2924115863324481, + 69: 0.7653974346433319, 70: 0.4784432998768373, + 71: 0.2015373401465821, 72: 0.8715627297687166, + 73: 0.7551785489449617, 74: 0.8675584511848858, + 75: 0.9323236929247266, 76: 0.24171326534063708, + 77: 0.8924504838872919, 78: 0.7659566844206285, + 79: 0.4146826922981828, 80: 0.32368260626724077, + 81: 0.5613052389019693, 82: 0.5908359788832377, + 83: 0.16558277680810296, 84: 0.4861970648764189, + 85: 0.9490216941921916, 86: 0.46819109749463483, + 87: 0.39662970244337636, 88: 0.9188065977724452, + 89: 0.9857276253270151, 90: 0.9392006613006973, + 91: 0.04763514581194506, 92: 0.8603759125000982, + 93: 0.2010458491996312, 94: 0.3436090514063087, + 95: 0.882532701944944, 96: 0.09841477926384423, + 97: 0.13326228818943153, 98: 0.26768957816772065, + 99: 0.20931290505166977, 100: 0.34066590743005254} + _default_val: &id010 !!python/name:pyomo.core.base.param.NoValue '' + _dense_initialize: false + _implicit_subsets: null + _index_set: *id005 + _mutable: false + _name: w + _parent: *id006 + _rule: !!python/object:pyomo.core.base.initializer.IndexedCallInitializer { + _fcn: !!python/name:__main__.%3Clambda%3E ''} + _units: null + _validate: null + doc: null + domain: &id011 !!python/object/apply:pyomo.core.base.global_set._get_global_set [ + Reals] + - 2 + - !!python/tuple + - &id116 !!python/object/new:pyomo.core.base.param.IndexedParam + state: + - _constructed: true + _ctype: *id009 + _data: {1: 0.726594449656969, 2: 0.6317864585819148, + 3: 0.15878068771234666, 4: 0.7662135567262747, + 5: 0.9238858846279976, 6: 0.5910784293538296, + 7: 0.16853534493879874, 8: 0.48868692874530517, + 9: 0.7183344306409591, 10: 0.21618775480519936, + 11: 0.6121437327275627, 12: 0.6203405113507279, + 13: 0.23751915806251056, 14: 0.1439876464243961, + 15: 0.7722139031503106, 16: 0.5305745417039281, + 17: 0.43869242990387713, 18: 0.47789976870816253, + 19: 0.7841053252246368, 20: 0.8368633875135679, + 21: 0.40626081603719466, 22: 0.7741855452373254, + 23: 0.0031272892149529774, 24: 0.8765518952066175, + 25: 0.03167937690551981, 26: 0.9817483012225143, + 27: 0.9952146839177622, 28: 0.6060300921061987, + 29: 0.8892431625943732, 30: 0.6763359337175552, + 31: 0.06864591686449029, 32: 0.7003268486520754, + 33: 0.9992472177618619, 34: 0.7179416689143338, + 35: 0.21861150878713143, 36: 0.4221034355860843, + 37: 0.8662113315051484, 38: 0.7945718190939539, + 39: 0.21412345267720012, 40: 0.8206543194524558, + 41: 0.3385603742499437, 42: 0.008816057617166528, + 43: 0.93420633498251, 44: 0.5824689923646437, + 45: 0.7419611633198233, 46: 0.13957389472570292, + 47: 0.5451432354739488, 48: 0.9235748236881961, + 49: 0.15047945733473922, 50: 0.9719077397032271, + 51: 0.6205150049379418, 52: 0.7251601420957609, + 53: 0.6916123582302697, 54: 0.7640087487149704, + 55: 0.19610501529331492, 56: 0.6027568046433773, + 57: 0.2359711329865154, 58: 0.10814602117665817, + 59: 0.31285007597629344, 60: 0.9099172750209824, + 61: 0.940761072700578, 62: 0.4853692161564098, + 63: 0.6164977119460898, 64: 0.18119073764701576, + 65: 0.2902506272248766, 66: 0.6457338960735598, + 67: 0.2850581491339579, 68: 0.7650274355828317, + 69: 0.8246278623440632, 70: 0.45002937783739716, + 71: 0.40523778589370574, 72: 0.16964652744651, + 73: 0.226105610010514, 74: 0.825814648409055, + 75: 0.2602611372742738, 76: 0.07577567805248797, + 77: 0.7376414354275203, 78: 0.8051307985409988, + 79: 0.8564929596470199, 80: 0.7550747332999559, + 81: 0.1451608072546512, 82: 0.47871763978951576, + 83: 0.4388308577208603, 84: 0.5077019534250237, + 85: 0.7042297016232173, 86: 0.6883296828942322, + 87: 0.7058045106408227, 88: 0.9445325523170363, + 89: 0.8038216540619805, 90: 0.77407902794402, + 91: 0.42460642017443284, 92: 0.8334296219965986, + 93: 0.9663697906197474, 94: 0.6803051313498966, + 95: 0.08824211661630754, 96: 0.6627243518817839, + 97: 0.5159087221318315, 98: 0.21408463611709205, + 99: 0.37356794166885243, 100: 0.7792012881552631} + _default_val: *id010 + _dense_initialize: false + _implicit_subsets: null + _index_set: *id005 + _mutable: false + _name: v + _parent: *id006 + _rule: !!python/object:pyomo.core.base.initializer.IndexedCallInitializer { + _fcn: !!python/name:__main__.%3Clambda%3E ''} + _units: null + _validate: null + doc: null + domain: *id011 + - null + - !!python/tuple + - *id003 + - null + - !!python/tuple + - *id001 + - null + - !!python/tuple + - &id012 !!python/object/new:pyomo.core.base.constraint.ScalarConstraint + state: + - *id012 + - null + - true + - &id112 !!python/object/new:pyomo.core.expr.numeric_expr.SumExpression + state: + - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7773566427005639 + - *id002 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6698255595592497 + - *id013 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.09913960392481702 + - *id014 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.35297051119014544 + - *id015 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.4679077429008419 + - *id016 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.5346837414708775 + - *id017 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9783090609123973 + - *id018 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.13031535015865903 + - *id019 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6712434682302663 + - *id020 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.36422941594737557 + - *id021 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.48883570716198577 + - *id022 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.20301221073405373 + - *id023 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6661983755713592 + - *id024 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.2276630312069321 + - *id025 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.4580640582967631 + - *id026 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.040722397554957435 + - *id027 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9742897953778286 + - *id028 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.4874760742689066 + - *id029 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.4616138636373597 + - *id030 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7141471558082002 + - *id031 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.4157281494999725 + - *id032 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.888011688001529 + - *id033 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.023293448723771704 + - *id034 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8335062677845465 + - *id035 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.4684947409975081 + - *id036 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8114798126442795 + - *id037 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9455914886158723 + - *id038 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9830883781948988 + - *id039 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.1761820755785306 + - *id040 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.698655759576308 + - *id041 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.10885571131238292 + - *id042 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.16026373420620188 + - *id043 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.09286027402458918 + - *id044 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.3140620798928404 + - *id045 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.01653868433866723 + - *id046 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8540491257363622 + - *id047 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.2910160386456968 + - *id048 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7800475863350328 + - *id049 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.5480965161255696 + - *id050 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.19433067669976123 + - *id051 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.2920382721805297 + - *id052 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.3194527773994337 + - *id053 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6585982235379076 + - *id054 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.23152103541222924 + - *id055 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6194303369953537 + - *id056 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8953386098022104 + - *id057 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8694342085696831 + - *id058 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.2938069356684523 + - *id059 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.45820480858054946 + - *id060 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.4849797978711191 + - *id061 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.2803882693587225 + - *id062 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.32895694635060024 + - *id063 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9842424240265042 + - *id064 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.011944137920874343 + - *id065 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.14290076829328524 + - *id066 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6519772165446712 + - *id067 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.07499317994693244 + - *id068 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.29207870228110877 + - *id069 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7934429721917705 + - *id070 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9115931008709737 + - *id071 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.3703917795895437 + - *id072 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.20528221118666345 + - *id073 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.880081326784678 + - *id074 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6325664501560831 + - *id075 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.503514326058558 + - *id076 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.3308435596710889 + - *id077 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.3474001835074456 + - *id078 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.2924115863324481 + - *id079 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7653974346433319 + - *id080 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.4784432998768373 + - *id081 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.2015373401465821 + - *id082 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8715627297687166 + - *id083 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7551785489449617 + - *id084 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8675584511848858 + - *id085 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9323236929247266 + - *id086 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.24171326534063708 + - *id087 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8924504838872919 + - *id088 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7659566844206285 + - *id089 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.4146826922981828 + - *id090 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.32368260626724077 + - *id091 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.5613052389019693 + - *id092 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.5908359788832377 + - *id093 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.16558277680810296 + - *id094 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.4861970648764189 + - *id095 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9490216941921916 + - *id096 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.46819109749463483 + - *id097 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.39662970244337636 + - *id098 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9188065977724452 + - *id099 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9857276253270151 + - *id100 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9392006613006973 + - *id101 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.04763514581194506 + - *id102 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8603759125000982 + - *id103 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.2010458491996312 + - *id104 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.3436090514063087 + - *id105 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.882532701944944 + - *id106 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.09841477926384423 + - *id107 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.13326228818943153 + - *id108 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.26768957816772065 + - *id109 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.20931290505166977 + - *id110 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.34066590743005254 + - *id111 + - 100 + - false + - null + - 10.0 + - &id114 !!python/object/new:pyomo.core.expr.relational_expr.InequalityExpression + state: + - !!python/tuple + - *id112 + - 10.0 + - false + - _constructed: true + _ctype: *id113 + _data: + null: *id012 + _implicit_subsets: null + _index_set: &id115 !!python/object/apply:pyomo.core.base.global_set._get_global_set [ + UnindexedComponent_set] + _name: c + _parent: *id006 + doc: null + rule: !!python/object:pyomo.core.base.initializer.ConstantInitializer + val: *id114 + verified: false + - null + _dense: true + _implicit_subsets: null + _index_set: *id115 + _name: unknown + _parent: null + _rule: null + _suppress_ctypes: !!set {} + c: *id012 + config: !!python/object:pyomo.core.base.PyomoModel.PyomoConfig { + _name_: PyomoConfig} + doc: null + o: *id001 + solutions: !!python/object:pyomo.core.base.PyomoModel.ModelSolutions + _instance: *id006 + index: null + solutions: [] + symbol_map: {} + statistics: !!python/object:pyomo.common.collections.bunch.Bunch { + _name_: Bunch} + v: *id116 + w: *id117 + x: *id003 + doc: null + _name: x + _parent: *id006 + _rule_bounds: null + _rule_domain: !!python/object:pyomo.core.base.set.SetInitializer + _set: !!python/object:pyomo.core.base.initializer.ConstantInitializer + val: *id004 + verified: false + verified: false + _rule_init: null + _units: null + doc: null + - 1 + - -0.0 + - null + - null + - *id004 + - false + - false + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6317864585819148 + - *id013 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.15878068771234666 + - *id014 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7662135567262747 + - *id015 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9238858846279976 + - *id016 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.5910784293538296 + - *id017 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.16853534493879874 + - *id018 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.48868692874530517 + - *id019 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7183344306409591 + - *id020 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.21618775480519936 + - *id021 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6121437327275627 + - *id022 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6203405113507279 + - *id023 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.23751915806251056 + - *id024 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.1439876464243961 + - *id025 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7722139031503106 + - *id026 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.5305745417039281 + - *id027 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.43869242990387713 + - *id028 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.47789976870816253 + - *id029 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7841053252246368 + - *id030 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8368633875135679 + - *id031 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.40626081603719466 + - *id032 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7741855452373254 + - *id033 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.0031272892149529774 + - *id034 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8765518952066175 + - *id035 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.03167937690551981 + - *id036 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9817483012225143 + - *id037 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9952146839177622 + - *id038 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6060300921061987 + - *id039 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8892431625943732 + - *id040 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6763359337175552 + - *id041 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.06864591686449029 + - *id042 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7003268486520754 + - *id043 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9992472177618619 + - *id044 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7179416689143338 + - *id045 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.21861150878713143 + - *id046 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.4221034355860843 + - *id047 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8662113315051484 + - *id048 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7945718190939539 + - *id049 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.21412345267720012 + - *id050 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8206543194524558 + - *id051 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.3385603742499437 + - *id052 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.008816057617166528 + - *id053 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.93420633498251 + - *id054 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.5824689923646437 + - *id055 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7419611633198233 + - *id056 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.13957389472570292 + - *id057 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.5451432354739488 + - *id058 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9235748236881961 + - *id059 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.15047945733473922 + - *id060 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9719077397032271 + - *id061 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6205150049379418 + - *id062 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7251601420957609 + - *id063 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6916123582302697 + - *id064 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7640087487149704 + - *id065 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.19610501529331492 + - *id066 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6027568046433773 + - *id067 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.2359711329865154 + - *id068 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.10814602117665817 + - *id069 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.31285007597629344 + - *id070 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9099172750209824 + - *id071 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.940761072700578 + - *id072 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.4853692161564098 + - *id073 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6164977119460898 + - *id074 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.18119073764701576 + - *id075 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.2902506272248766 + - *id076 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6457338960735598 + - *id077 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.2850581491339579 + - *id078 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7650274355828317 + - *id079 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8246278623440632 + - *id080 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.45002937783739716 + - *id081 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.40523778589370574 + - *id082 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.16964652744651 + - *id083 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.226105610010514 + - *id084 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.825814648409055 + - *id085 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.2602611372742738 + - *id086 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.07577567805248797 + - *id087 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7376414354275203 + - *id088 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8051307985409988 + - *id089 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8564929596470199 + - *id090 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7550747332999559 + - *id091 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.1451608072546512 + - *id092 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.47871763978951576 + - *id093 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.4388308577208603 + - *id094 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.5077019534250237 + - *id095 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7042297016232173 + - *id096 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6883296828942322 + - *id097 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7058045106408227 + - *id098 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9445325523170363 + - *id099 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8038216540619805 + - *id100 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.77407902794402 + - *id101 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.42460642017443284 + - *id102 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.8334296219965986 + - *id103 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.9663697906197474 + - *id104 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6803051313498966 + - *id105 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.08824211661630754 + - *id106 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.6627243518817839 + - *id107 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.5159087221318315 + - *id108 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.21408463611709205 + - *id109 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.37356794166885243 + - *id110 + - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression + state: + - !!python/tuple + - 0.7792012881552631 + - *id111 + - 100 + - false + - _constructed: true + _ctype: *id118 + _data: + null: *id001 + _implicit_subsets: null + _index_set: *id115 + _init_sense: !!python/object:pyomo.core.base.initializer.ConstantInitializer { + val: -1, verified: false} + _name: o + _parent: *id006 + doc: null + rule: !!python/object:pyomo.core.base.initializer.ConstantInitializer + val: *id119 + verified: false + - 26.456318046876152 + variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735933094128: !!python/tuple + - *id002 + - 0 + 1735933094464: !!python/tuple + - *id013 + - 0 + 1735933094576: !!python/tuple + - *id014 + - 0 + 1735933094688: !!python/tuple + - *id015 + - 1 + 1735933094800: !!python/tuple + - *id016 + - 1 + 1735933094912: !!python/tuple + - *id017 + - 0 + 1735933095024: !!python/tuple + - *id018 + - 0 + 1735933095136: !!python/tuple + - *id020 + - 0 + 1735933095248: !!python/tuple + - *id019 + - 1 + 1735933095360: !!python/tuple + - *id021 + - 0 + 1735933095472: !!python/tuple + - *id022 + - 0 + 1735933095584: !!python/tuple + - *id023 + - 1 + 1735933095696: !!python/tuple + - *id025 + - 0 + 1735933095808: !!python/tuple + - *id024 + - 0 + 1735933095920: !!python/tuple + - *id026 + - 1 + 1735933096032: !!python/tuple + - *id027 + - 1 + 1735933096144: !!python/tuple + - *id028 + - 0 + 1735933096256: !!python/tuple + - *id029 + - 0 + 1735933096368: !!python/tuple + - *id030 + - 1 + 1735933096480: !!python/tuple + - *id031 + - 0 + 1735933096592: !!python/tuple + - *id032 + - 0 + 1735933096704: !!python/tuple + - *id033 + - 0 + 1735933096816: !!python/tuple + - *id034 + - 1 + 1735933096928: !!python/tuple + - *id035 + - 0 + 1735933097040: !!python/tuple + - *id036 + - 0 + 1735933097152: !!python/tuple + - *id037 + - 0 + 1735933097264: !!python/tuple + - *id038 + - 0 + 1735933097376: !!python/tuple + - *id039 + - 0 + 1735933097488: !!python/tuple + - *id040 + - 1 + 1735933097600: !!python/tuple + - *id041 + - 0 + 1735933097712: !!python/tuple + - *id042 + - 0 + 1735933097824: !!python/tuple + - *id043 + - 1 + 1735933097936: !!python/tuple + - *id044 + - 1 + 1735933098048: !!python/tuple + - *id045 + - 1 + 1735933098160: !!python/tuple + - *id046 + - 1 + 1735933098272: !!python/tuple + - *id047 + - 0 + 1735933098384: !!python/tuple + - *id048 + - 1 + 1735933098496: !!python/tuple + - *id049 + - 0 + 1735933098608: !!python/tuple + - *id050 + - 0 + 1735933098720: !!python/tuple + - *id051 + - 1 + 1735933098832: !!python/tuple + - *id052 + - 0 + 1735933098944: !!python/tuple + - *id053 + - 0 + 1735933099056: !!python/tuple + - *id054 + - 1 + 1735933099168: !!python/tuple + - *id055 + - 1 + 1735933099280: !!python/tuple + - *id056 + - 0 + 1735933099392: !!python/tuple + - *id057 + - 0 + 1735933099504: !!python/tuple + - *id058 + - 0 + 1735933099616: !!python/tuple + - *id059 + - 1 + 1735933099728: !!python/tuple + - *id060 + - 0 + 1735933099840: !!python/tuple + - *id061 + - 1 + 1735940014144: !!python/tuple + - *id062 + - 1 + 1735940014256: !!python/tuple + - *id063 + - 1 + 1735940014368: !!python/tuple + - *id064 + - 0 + 1735940014480: !!python/tuple + - *id065 + - 1 + 1735940014592: !!python/tuple + - *id066 + - 1 + 1735940014704: !!python/tuple + - *id067 + - 0 + 1735940014816: !!python/tuple + - *id068 + - 1 + 1735940014928: !!python/tuple + - *id069 + - 0 + 1735940015040: !!python/tuple + - *id070 + - 0 + 1735940015152: !!python/tuple + - *id071 + - 0 + 1735940015264: !!python/tuple + - *id072 + - 1 + 1735940015376: !!python/tuple + - *id073 + - 1 + 1735940015488: !!python/tuple + - *id074 + - 0 + 1735940015600: !!python/tuple + - *id075 + - 0 + 1735940015712: !!python/tuple + - *id076 + - 0 + 1735940015824: !!python/tuple + - *id077 + - 1 + 1735940015936: !!python/tuple + - *id078 + - 0 + 1735940016048: !!python/tuple + - *id079 + - 1 + 1735940016160: !!python/tuple + - *id080 + - 0 + 1735940016272: !!python/tuple + - *id081 + - 0 + 1735940016384: !!python/tuple + - *id082 + - 1 + 1735940016496: !!python/tuple + - *id083 + - 0 + 1735940016608: !!python/tuple + - *id084 + - 0 + 1735940016720: !!python/tuple + - *id085 + - 0 + 1735940016832: !!python/tuple + - *id086 + - 0 + 1735940016944: !!python/tuple + - *id087 + - 0 + 1735940017056: !!python/tuple + - *id088 + - 0 + 1735940017168: !!python/tuple + - *id089 + - 0 + 1735940017280: !!python/tuple + - *id090 + - 1 + 1735940017392: !!python/tuple + - *id091 + - 1 + 1735940017504: !!python/tuple + - *id092 + - 0 + 1735940017616: !!python/tuple + - *id093 + - 0 + 1735940017728: !!python/tuple + - *id094 + - 1 + 1735940017840: !!python/tuple + - *id095 + - 0 + 1735940017952: !!python/tuple + - *id096 + - 0 + 1735940018064: !!python/tuple + - *id097 + - 0 + 1735940018176: !!python/tuple + - *id098 + - 1 + 1735940018288: !!python/tuple + - *id099 + - 0 + 1735940018400: !!python/tuple + - *id100 + - 0 + 1735940018512: !!python/tuple + - *id101 + - 0 + 1735940018624: !!python/tuple + - *id102 + - 1 + 1735940018736: !!python/tuple + - *id103 + - 0 + 1735940018848: !!python/tuple + - *id104 + - 1 + 1735940018960: !!python/tuple + - *id105 + - 1 + 1735940019072: !!python/tuple + - *id106 + - 0 + 1735940019184: !!python/tuple + - *id107 + - 1 + 1735940019296: !!python/tuple + - *id108 + - 1 + 1735940019408: !!python/tuple + - *id109 + - 0 + 1735940019520: !!python/tuple + - *id110 + - 1 + 1735940019632: !!python/tuple + - *id111 + - 1 +- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution + fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet + _data: !!python/tuple [] + objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735881266400: !!python/tuple + - *id001 + - 26.453190757661194 + variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735933094128: !!python/tuple + - *id002 + - 0 + 1735933094464: !!python/tuple + - *id013 + - 0 + 1735933094576: !!python/tuple + - *id014 + - 0 + 1735933094688: !!python/tuple + - *id015 + - 1 + 1735933094800: !!python/tuple + - *id016 + - 1 + 1735933094912: !!python/tuple + - *id017 + - 0 + 1735933095024: !!python/tuple + - *id018 + - 0 + 1735933095136: !!python/tuple + - *id020 + - 0 + 1735933095248: !!python/tuple + - *id019 + - 1 + 1735933095360: !!python/tuple + - *id021 + - 0 + 1735933095472: !!python/tuple + - *id022 + - 0 + 1735933095584: !!python/tuple + - *id023 + - 1 + 1735933095696: !!python/tuple + - *id025 + - 0 + 1735933095808: !!python/tuple + - *id024 + - 0 + 1735933095920: !!python/tuple + - *id026 + - 1 + 1735933096032: !!python/tuple + - *id027 + - 1 + 1735933096144: !!python/tuple + - *id028 + - 0 + 1735933096256: !!python/tuple + - *id029 + - 0 + 1735933096368: !!python/tuple + - *id030 + - 1 + 1735933096480: !!python/tuple + - *id031 + - 0 + 1735933096592: !!python/tuple + - *id032 + - 0 + 1735933096704: !!python/tuple + - *id033 + - 0 + 1735933096816: !!python/tuple + - *id034 + - 0 + 1735933096928: !!python/tuple + - *id035 + - 0 + 1735933097040: !!python/tuple + - *id036 + - 0 + 1735933097152: !!python/tuple + - *id037 + - 0 + 1735933097264: !!python/tuple + - *id038 + - 0 + 1735933097376: !!python/tuple + - *id039 + - 0 + 1735933097488: !!python/tuple + - *id040 + - 1 + 1735933097600: !!python/tuple + - *id041 + - 0 + 1735933097712: !!python/tuple + - *id042 + - 0 + 1735933097824: !!python/tuple + - *id043 + - 1 + 1735933097936: !!python/tuple + - *id044 + - 1 + 1735933098048: !!python/tuple + - *id045 + - 1 + 1735933098160: !!python/tuple + - *id046 + - 1 + 1735933098272: !!python/tuple + - *id047 + - 0 + 1735933098384: !!python/tuple + - *id048 + - 1 + 1735933098496: !!python/tuple + - *id049 + - 0 + 1735933098608: !!python/tuple + - *id050 + - 0 + 1735933098720: !!python/tuple + - *id051 + - 1 + 1735933098832: !!python/tuple + - *id052 + - 0 + 1735933098944: !!python/tuple + - *id053 + - 0 + 1735933099056: !!python/tuple + - *id054 + - 1 + 1735933099168: !!python/tuple + - *id055 + - 1 + 1735933099280: !!python/tuple + - *id056 + - 0 + 1735933099392: !!python/tuple + - *id057 + - 0 + 1735933099504: !!python/tuple + - *id058 + - 0 + 1735933099616: !!python/tuple + - *id059 + - 1 + 1735933099728: !!python/tuple + - *id060 + - 0 + 1735933099840: !!python/tuple + - *id061 + - 1 + 1735940014144: !!python/tuple + - *id062 + - 1 + 1735940014256: !!python/tuple + - *id063 + - 1 + 1735940014368: !!python/tuple + - *id064 + - 0 + 1735940014480: !!python/tuple + - *id065 + - 1 + 1735940014592: !!python/tuple + - *id066 + - 1 + 1735940014704: !!python/tuple + - *id067 + - 0 + 1735940014816: !!python/tuple + - *id068 + - 1 + 1735940014928: !!python/tuple + - *id069 + - 0 + 1735940015040: !!python/tuple + - *id070 + - 0 + 1735940015152: !!python/tuple + - *id071 + - 0 + 1735940015264: !!python/tuple + - *id072 + - 1 + 1735940015376: !!python/tuple + - *id073 + - 1 + 1735940015488: !!python/tuple + - *id074 + - 0 + 1735940015600: !!python/tuple + - *id075 + - 0 + 1735940015712: !!python/tuple + - *id076 + - 0 + 1735940015824: !!python/tuple + - *id077 + - 1 + 1735940015936: !!python/tuple + - *id078 + - 0 + 1735940016048: !!python/tuple + - *id079 + - 1 + 1735940016160: !!python/tuple + - *id080 + - 0 + 1735940016272: !!python/tuple + - *id081 + - 0 + 1735940016384: !!python/tuple + - *id082 + - 1 + 1735940016496: !!python/tuple + - *id083 + - 0 + 1735940016608: !!python/tuple + - *id084 + - 0 + 1735940016720: !!python/tuple + - *id085 + - 0 + 1735940016832: !!python/tuple + - *id086 + - 0 + 1735940016944: !!python/tuple + - *id087 + - 0 + 1735940017056: !!python/tuple + - *id088 + - 0 + 1735940017168: !!python/tuple + - *id089 + - 0 + 1735940017280: !!python/tuple + - *id090 + - 1 + 1735940017392: !!python/tuple + - *id091 + - 1 + 1735940017504: !!python/tuple + - *id092 + - 0 + 1735940017616: !!python/tuple + - *id093 + - 0 + 1735940017728: !!python/tuple + - *id094 + - 1 + 1735940017840: !!python/tuple + - *id095 + - 0 + 1735940017952: !!python/tuple + - *id096 + - 0 + 1735940018064: !!python/tuple + - *id097 + - 0 + 1735940018176: !!python/tuple + - *id098 + - 1 + 1735940018288: !!python/tuple + - *id099 + - 0 + 1735940018400: !!python/tuple + - *id100 + - 0 + 1735940018512: !!python/tuple + - *id101 + - 0 + 1735940018624: !!python/tuple + - *id102 + - 1 + 1735940018736: !!python/tuple + - *id103 + - 0 + 1735940018848: !!python/tuple + - *id104 + - 1 + 1735940018960: !!python/tuple + - *id105 + - 1 + 1735940019072: !!python/tuple + - *id106 + - 0 + 1735940019184: !!python/tuple + - *id107 + - 1 + 1735940019296: !!python/tuple + - *id108 + - 1 + 1735940019408: !!python/tuple + - *id109 + - 0 + 1735940019520: !!python/tuple + - *id110 + - 1 + 1735940019632: !!python/tuple + - *id111 + - 1 +- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution + fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet + _data: !!python/tuple [] + objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735881266400: !!python/tuple + - *id001 + - 26.437867999364702 + variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735933094128: !!python/tuple + - *id002 + - 0 + 1735933094464: !!python/tuple + - *id013 + - 0 + 1735933094576: !!python/tuple + - *id014 + - 1 + 1735933094688: !!python/tuple + - *id015 + - 1 + 1735933094800: !!python/tuple + - *id016 + - 1 + 1735933094912: !!python/tuple + - *id017 + - 0 + 1735933095024: !!python/tuple + - *id018 + - 0 + 1735933095136: !!python/tuple + - *id020 + - 0 + 1735933095248: !!python/tuple + - *id019 + - 1 + 1735933095360: !!python/tuple + - *id021 + - 0 + 1735933095472: !!python/tuple + - *id022 + - 0 + 1735933095584: !!python/tuple + - *id023 + - 1 + 1735933095696: !!python/tuple + - *id025 + - 0 + 1735933095808: !!python/tuple + - *id024 + - 0 + 1735933095920: !!python/tuple + - *id026 + - 1 + 1735933096032: !!python/tuple + - *id027 + - 1 + 1735933096144: !!python/tuple + - *id028 + - 0 + 1735933096256: !!python/tuple + - *id029 + - 0 + 1735933096368: !!python/tuple + - *id030 + - 1 + 1735933096480: !!python/tuple + - *id031 + - 0 + 1735933096592: !!python/tuple + - *id032 + - 0 + 1735933096704: !!python/tuple + - *id033 + - 0 + 1735933096816: !!python/tuple + - *id034 + - 1 + 1735933096928: !!python/tuple + - *id035 + - 0 + 1735933097040: !!python/tuple + - *id036 + - 0 + 1735933097152: !!python/tuple + - *id037 + - 0 + 1735933097264: !!python/tuple + - *id038 + - 0 + 1735933097376: !!python/tuple + - *id039 + - 0 + 1735933097488: !!python/tuple + - *id040 + - 1 + 1735933097600: !!python/tuple + - *id041 + - 0 + 1735933097712: !!python/tuple + - *id042 + - 1 + 1735933097824: !!python/tuple + - *id043 + - 1 + 1735933097936: !!python/tuple + - *id044 + - 1 + 1735933098048: !!python/tuple + - *id045 + - 1 + 1735933098160: !!python/tuple + - *id046 + - 1 + 1735933098272: !!python/tuple + - *id047 + - 0 + 1735933098384: !!python/tuple + - *id048 + - 1 + 1735933098496: !!python/tuple + - *id049 + - 0 + 1735933098608: !!python/tuple + - *id050 + - 0 + 1735933098720: !!python/tuple + - *id051 + - 1 + 1735933098832: !!python/tuple + - *id052 + - 0 + 1735933098944: !!python/tuple + - *id053 + - 0 + 1735933099056: !!python/tuple + - *id054 + - 0 + 1735933099168: !!python/tuple + - *id055 + - 1 + 1735933099280: !!python/tuple + - *id056 + - 0 + 1735933099392: !!python/tuple + - *id057 + - 0 + 1735933099504: !!python/tuple + - *id058 + - 0 + 1735933099616: !!python/tuple + - *id059 + - 1 + 1735933099728: !!python/tuple + - *id060 + - 0 + 1735933099840: !!python/tuple + - *id061 + - 1 + 1735940014144: !!python/tuple + - *id062 + - 1 + 1735940014256: !!python/tuple + - *id063 + - 1 + 1735940014368: !!python/tuple + - *id064 + - 0 + 1735940014480: !!python/tuple + - *id065 + - 1 + 1735940014592: !!python/tuple + - *id066 + - 1 + 1735940014704: !!python/tuple + - *id067 + - 0 + 1735940014816: !!python/tuple + - *id068 + - 1 + 1735940014928: !!python/tuple + - *id069 + - 0 + 1735940015040: !!python/tuple + - *id070 + - 0 + 1735940015152: !!python/tuple + - *id071 + - 0 + 1735940015264: !!python/tuple + - *id072 + - 1 + 1735940015376: !!python/tuple + - *id073 + - 1 + 1735940015488: !!python/tuple + - *id074 + - 0 + 1735940015600: !!python/tuple + - *id075 + - 0 + 1735940015712: !!python/tuple + - *id076 + - 0 + 1735940015824: !!python/tuple + - *id077 + - 1 + 1735940015936: !!python/tuple + - *id078 + - 0 + 1735940016048: !!python/tuple + - *id079 + - 1 + 1735940016160: !!python/tuple + - *id080 + - 0 + 1735940016272: !!python/tuple + - *id081 + - 0 + 1735940016384: !!python/tuple + - *id082 + - 1 + 1735940016496: !!python/tuple + - *id083 + - 0 + 1735940016608: !!python/tuple + - *id084 + - 0 + 1735940016720: !!python/tuple + - *id085 + - 0 + 1735940016832: !!python/tuple + - *id086 + - 0 + 1735940016944: !!python/tuple + - *id087 + - 0 + 1735940017056: !!python/tuple + - *id088 + - 0 + 1735940017168: !!python/tuple + - *id089 + - 0 + 1735940017280: !!python/tuple + - *id090 + - 1 + 1735940017392: !!python/tuple + - *id091 + - 1 + 1735940017504: !!python/tuple + - *id092 + - 0 + 1735940017616: !!python/tuple + - *id093 + - 0 + 1735940017728: !!python/tuple + - *id094 + - 1 + 1735940017840: !!python/tuple + - *id095 + - 0 + 1735940017952: !!python/tuple + - *id096 + - 0 + 1735940018064: !!python/tuple + - *id097 + - 1 + 1735940018176: !!python/tuple + - *id098 + - 1 + 1735940018288: !!python/tuple + - *id099 + - 0 + 1735940018400: !!python/tuple + - *id100 + - 0 + 1735940018512: !!python/tuple + - *id101 + - 0 + 1735940018624: !!python/tuple + - *id102 + - 1 + 1735940018736: !!python/tuple + - *id103 + - 0 + 1735940018848: !!python/tuple + - *id104 + - 1 + 1735940018960: !!python/tuple + - *id105 + - 1 + 1735940019072: !!python/tuple + - *id106 + - 0 + 1735940019184: !!python/tuple + - *id107 + - 1 + 1735940019296: !!python/tuple + - *id108 + - 1 + 1735940019408: !!python/tuple + - *id109 + - 0 + 1735940019520: !!python/tuple + - *id110 + - 1 + 1735940019632: !!python/tuple + - *id111 + - 1 +- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution + fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet + _data: !!python/tuple [] + objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735881266400: !!python/tuple + - *id001 + - 26.43474071014975 + variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735933094128: !!python/tuple + - *id002 + - 0 + 1735933094464: !!python/tuple + - *id013 + - 0 + 1735933094576: !!python/tuple + - *id014 + - 1 + 1735933094688: !!python/tuple + - *id015 + - 1 + 1735933094800: !!python/tuple + - *id016 + - 1 + 1735933094912: !!python/tuple + - *id017 + - 0 + 1735933095024: !!python/tuple + - *id018 + - 0 + 1735933095136: !!python/tuple + - *id020 + - 0 + 1735933095248: !!python/tuple + - *id019 + - 1 + 1735933095360: !!python/tuple + - *id021 + - 0 + 1735933095472: !!python/tuple + - *id022 + - 0 + 1735933095584: !!python/tuple + - *id023 + - 1 + 1735933095696: !!python/tuple + - *id025 + - 0 + 1735933095808: !!python/tuple + - *id024 + - 0 + 1735933095920: !!python/tuple + - *id026 + - 1 + 1735933096032: !!python/tuple + - *id027 + - 1 + 1735933096144: !!python/tuple + - *id028 + - 0 + 1735933096256: !!python/tuple + - *id029 + - 0 + 1735933096368: !!python/tuple + - *id030 + - 1 + 1735933096480: !!python/tuple + - *id031 + - 0 + 1735933096592: !!python/tuple + - *id032 + - 0 + 1735933096704: !!python/tuple + - *id033 + - 0 + 1735933096816: !!python/tuple + - *id034 + - 0 + 1735933096928: !!python/tuple + - *id035 + - 0 + 1735933097040: !!python/tuple + - *id036 + - 0 + 1735933097152: !!python/tuple + - *id037 + - 0 + 1735933097264: !!python/tuple + - *id038 + - 0 + 1735933097376: !!python/tuple + - *id039 + - 0 + 1735933097488: !!python/tuple + - *id040 + - 1 + 1735933097600: !!python/tuple + - *id041 + - 0 + 1735933097712: !!python/tuple + - *id042 + - 1 + 1735933097824: !!python/tuple + - *id043 + - 1 + 1735933097936: !!python/tuple + - *id044 + - 1 + 1735933098048: !!python/tuple + - *id045 + - 1 + 1735933098160: !!python/tuple + - *id046 + - 1 + 1735933098272: !!python/tuple + - *id047 + - 0 + 1735933098384: !!python/tuple + - *id048 + - 1 + 1735933098496: !!python/tuple + - *id049 + - 0 + 1735933098608: !!python/tuple + - *id050 + - 0 + 1735933098720: !!python/tuple + - *id051 + - 1 + 1735933098832: !!python/tuple + - *id052 + - 0 + 1735933098944: !!python/tuple + - *id053 + - 0 + 1735933099056: !!python/tuple + - *id054 + - 0 + 1735933099168: !!python/tuple + - *id055 + - 1 + 1735933099280: !!python/tuple + - *id056 + - 0 + 1735933099392: !!python/tuple + - *id057 + - 0 + 1735933099504: !!python/tuple + - *id058 + - 0 + 1735933099616: !!python/tuple + - *id059 + - 1 + 1735933099728: !!python/tuple + - *id060 + - 0 + 1735933099840: !!python/tuple + - *id061 + - 1 + 1735940014144: !!python/tuple + - *id062 + - 1 + 1735940014256: !!python/tuple + - *id063 + - 1 + 1735940014368: !!python/tuple + - *id064 + - 0 + 1735940014480: !!python/tuple + - *id065 + - 1 + 1735940014592: !!python/tuple + - *id066 + - 1 + 1735940014704: !!python/tuple + - *id067 + - 0 + 1735940014816: !!python/tuple + - *id068 + - 1 + 1735940014928: !!python/tuple + - *id069 + - 0 + 1735940015040: !!python/tuple + - *id070 + - 0 + 1735940015152: !!python/tuple + - *id071 + - 0 + 1735940015264: !!python/tuple + - *id072 + - 1 + 1735940015376: !!python/tuple + - *id073 + - 1 + 1735940015488: !!python/tuple + - *id074 + - 0 + 1735940015600: !!python/tuple + - *id075 + - 0 + 1735940015712: !!python/tuple + - *id076 + - 0 + 1735940015824: !!python/tuple + - *id077 + - 1 + 1735940015936: !!python/tuple + - *id078 + - 0 + 1735940016048: !!python/tuple + - *id079 + - 1 + 1735940016160: !!python/tuple + - *id080 + - 0 + 1735940016272: !!python/tuple + - *id081 + - 0 + 1735940016384: !!python/tuple + - *id082 + - 1 + 1735940016496: !!python/tuple + - *id083 + - 0 + 1735940016608: !!python/tuple + - *id084 + - 0 + 1735940016720: !!python/tuple + - *id085 + - 0 + 1735940016832: !!python/tuple + - *id086 + - 0 + 1735940016944: !!python/tuple + - *id087 + - 0 + 1735940017056: !!python/tuple + - *id088 + - 0 + 1735940017168: !!python/tuple + - *id089 + - 0 + 1735940017280: !!python/tuple + - *id090 + - 1 + 1735940017392: !!python/tuple + - *id091 + - 1 + 1735940017504: !!python/tuple + - *id092 + - 0 + 1735940017616: !!python/tuple + - *id093 + - 0 + 1735940017728: !!python/tuple + - *id094 + - 1 + 1735940017840: !!python/tuple + - *id095 + - 0 + 1735940017952: !!python/tuple + - *id096 + - 0 + 1735940018064: !!python/tuple + - *id097 + - 1 + 1735940018176: !!python/tuple + - *id098 + - 1 + 1735940018288: !!python/tuple + - *id099 + - 0 + 1735940018400: !!python/tuple + - *id100 + - 0 + 1735940018512: !!python/tuple + - *id101 + - 0 + 1735940018624: !!python/tuple + - *id102 + - 1 + 1735940018736: !!python/tuple + - *id103 + - 0 + 1735940018848: !!python/tuple + - *id104 + - 1 + 1735940018960: !!python/tuple + - *id105 + - 1 + 1735940019072: !!python/tuple + - *id106 + - 0 + 1735940019184: !!python/tuple + - *id107 + - 1 + 1735940019296: !!python/tuple + - *id108 + - 1 + 1735940019408: !!python/tuple + - *id109 + - 0 + 1735940019520: !!python/tuple + - *id110 + - 1 + 1735940019632: !!python/tuple + - *id111 + - 1 +- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution + fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet + _data: !!python/tuple [] + objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735881266400: !!python/tuple + - *id001 + - 26.418993719295177 + variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735933094128: !!python/tuple + - *id002 + - 0 + 1735933094464: !!python/tuple + - *id013 + - 0 + 1735933094576: !!python/tuple + - *id014 + - 1 + 1735933094688: !!python/tuple + - *id015 + - 1 + 1735933094800: !!python/tuple + - *id016 + - 1 + 1735933094912: !!python/tuple + - *id017 + - 0 + 1735933095024: !!python/tuple + - *id018 + - 0 + 1735933095136: !!python/tuple + - *id020 + - 0 + 1735933095248: !!python/tuple + - *id019 + - 1 + 1735933095360: !!python/tuple + - *id021 + - 0 + 1735933095472: !!python/tuple + - *id022 + - 0 + 1735933095584: !!python/tuple + - *id023 + - 1 + 1735933095696: !!python/tuple + - *id025 + - 0 + 1735933095808: !!python/tuple + - *id024 + - 0 + 1735933095920: !!python/tuple + - *id026 + - 1 + 1735933096032: !!python/tuple + - *id027 + - 1 + 1735933096144: !!python/tuple + - *id028 + - 0 + 1735933096256: !!python/tuple + - *id029 + - 0 + 1735933096368: !!python/tuple + - *id030 + - 1 + 1735933096480: !!python/tuple + - *id031 + - 0 + 1735933096592: !!python/tuple + - *id032 + - 0 + 1735933096704: !!python/tuple + - *id033 + - 0 + 1735933096816: !!python/tuple + - *id034 + - 1 + 1735933096928: !!python/tuple + - *id035 + - 0 + 1735933097040: !!python/tuple + - *id036 + - 0 + 1735933097152: !!python/tuple + - *id037 + - 0 + 1735933097264: !!python/tuple + - *id038 + - 0 + 1735933097376: !!python/tuple + - *id039 + - 0 + 1735933097488: !!python/tuple + - *id040 + - 1 + 1735933097600: !!python/tuple + - *id041 + - 0 + 1735933097712: !!python/tuple + - *id042 + - 0 + 1735933097824: !!python/tuple + - *id043 + - 1 + 1735933097936: !!python/tuple + - *id044 + - 1 + 1735933098048: !!python/tuple + - *id045 + - 1 + 1735933098160: !!python/tuple + - *id046 + - 1 + 1735933098272: !!python/tuple + - *id047 + - 0 + 1735933098384: !!python/tuple + - *id048 + - 1 + 1735933098496: !!python/tuple + - *id049 + - 0 + 1735933098608: !!python/tuple + - *id050 + - 0 + 1735933098720: !!python/tuple + - *id051 + - 1 + 1735933098832: !!python/tuple + - *id052 + - 0 + 1735933098944: !!python/tuple + - *id053 + - 0 + 1735933099056: !!python/tuple + - *id054 + - 1 + 1735933099168: !!python/tuple + - *id055 + - 1 + 1735933099280: !!python/tuple + - *id056 + - 0 + 1735933099392: !!python/tuple + - *id057 + - 0 + 1735933099504: !!python/tuple + - *id058 + - 0 + 1735933099616: !!python/tuple + - *id059 + - 1 + 1735933099728: !!python/tuple + - *id060 + - 0 + 1735933099840: !!python/tuple + - *id061 + - 1 + 1735940014144: !!python/tuple + - *id062 + - 1 + 1735940014256: !!python/tuple + - *id063 + - 1 + 1735940014368: !!python/tuple + - *id064 + - 0 + 1735940014480: !!python/tuple + - *id065 + - 1 + 1735940014592: !!python/tuple + - *id066 + - 0 + 1735940014704: !!python/tuple + - *id067 + - 0 + 1735940014816: !!python/tuple + - *id068 + - 1 + 1735940014928: !!python/tuple + - *id069 + - 0 + 1735940015040: !!python/tuple + - *id070 + - 0 + 1735940015152: !!python/tuple + - *id071 + - 0 + 1735940015264: !!python/tuple + - *id072 + - 1 + 1735940015376: !!python/tuple + - *id073 + - 1 + 1735940015488: !!python/tuple + - *id074 + - 0 + 1735940015600: !!python/tuple + - *id075 + - 0 + 1735940015712: !!python/tuple + - *id076 + - 0 + 1735940015824: !!python/tuple + - *id077 + - 1 + 1735940015936: !!python/tuple + - *id078 + - 0 + 1735940016048: !!python/tuple + - *id079 + - 1 + 1735940016160: !!python/tuple + - *id080 + - 0 + 1735940016272: !!python/tuple + - *id081 + - 0 + 1735940016384: !!python/tuple + - *id082 + - 1 + 1735940016496: !!python/tuple + - *id083 + - 0 + 1735940016608: !!python/tuple + - *id084 + - 0 + 1735940016720: !!python/tuple + - *id085 + - 0 + 1735940016832: !!python/tuple + - *id086 + - 0 + 1735940016944: !!python/tuple + - *id087 + - 0 + 1735940017056: !!python/tuple + - *id088 + - 0 + 1735940017168: !!python/tuple + - *id089 + - 0 + 1735940017280: !!python/tuple + - *id090 + - 1 + 1735940017392: !!python/tuple + - *id091 + - 1 + 1735940017504: !!python/tuple + - *id092 + - 0 + 1735940017616: !!python/tuple + - *id093 + - 0 + 1735940017728: !!python/tuple + - *id094 + - 1 + 1735940017840: !!python/tuple + - *id095 + - 0 + 1735940017952: !!python/tuple + - *id096 + - 0 + 1735940018064: !!python/tuple + - *id097 + - 0 + 1735940018176: !!python/tuple + - *id098 + - 1 + 1735940018288: !!python/tuple + - *id099 + - 0 + 1735940018400: !!python/tuple + - *id100 + - 0 + 1735940018512: !!python/tuple + - *id101 + - 0 + 1735940018624: !!python/tuple + - *id102 + - 1 + 1735940018736: !!python/tuple + - *id103 + - 0 + 1735940018848: !!python/tuple + - *id104 + - 1 + 1735940018960: !!python/tuple + - *id105 + - 1 + 1735940019072: !!python/tuple + - *id106 + - 0 + 1735940019184: !!python/tuple + - *id107 + - 1 + 1735940019296: !!python/tuple + - *id108 + - 1 + 1735940019408: !!python/tuple + - *id109 + - 0 + 1735940019520: !!python/tuple + - *id110 + - 1 + 1735940019632: !!python/tuple + - *id111 + - 1 +- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution + fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet + _data: !!python/tuple [] + objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735881266400: !!python/tuple + - *id001 + - 26.415866430080232 + variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735933094128: !!python/tuple + - *id002 + - 0 + 1735933094464: !!python/tuple + - *id013 + - 0 + 1735933094576: !!python/tuple + - *id014 + - 1 + 1735933094688: !!python/tuple + - *id015 + - 1 + 1735933094800: !!python/tuple + - *id016 + - 1 + 1735933094912: !!python/tuple + - *id017 + - 0 + 1735933095024: !!python/tuple + - *id018 + - 0 + 1735933095136: !!python/tuple + - *id020 + - 0 + 1735933095248: !!python/tuple + - *id019 + - 1 + 1735933095360: !!python/tuple + - *id021 + - 0 + 1735933095472: !!python/tuple + - *id022 + - 0 + 1735933095584: !!python/tuple + - *id023 + - 1 + 1735933095696: !!python/tuple + - *id025 + - 0 + 1735933095808: !!python/tuple + - *id024 + - 0 + 1735933095920: !!python/tuple + - *id026 + - 1 + 1735933096032: !!python/tuple + - *id027 + - 1 + 1735933096144: !!python/tuple + - *id028 + - 0 + 1735933096256: !!python/tuple + - *id029 + - 0 + 1735933096368: !!python/tuple + - *id030 + - 1 + 1735933096480: !!python/tuple + - *id031 + - 0 + 1735933096592: !!python/tuple + - *id032 + - 0 + 1735933096704: !!python/tuple + - *id033 + - 0 + 1735933096816: !!python/tuple + - *id034 + - 0 + 1735933096928: !!python/tuple + - *id035 + - 0 + 1735933097040: !!python/tuple + - *id036 + - 0 + 1735933097152: !!python/tuple + - *id037 + - 0 + 1735933097264: !!python/tuple + - *id038 + - 0 + 1735933097376: !!python/tuple + - *id039 + - 0 + 1735933097488: !!python/tuple + - *id040 + - 1 + 1735933097600: !!python/tuple + - *id041 + - 0 + 1735933097712: !!python/tuple + - *id042 + - 0 + 1735933097824: !!python/tuple + - *id043 + - 1 + 1735933097936: !!python/tuple + - *id044 + - 1 + 1735933098048: !!python/tuple + - *id045 + - 1 + 1735933098160: !!python/tuple + - *id046 + - 1 + 1735933098272: !!python/tuple + - *id047 + - 0 + 1735933098384: !!python/tuple + - *id048 + - 1 + 1735933098496: !!python/tuple + - *id049 + - 0 + 1735933098608: !!python/tuple + - *id050 + - 0 + 1735933098720: !!python/tuple + - *id051 + - 1 + 1735933098832: !!python/tuple + - *id052 + - 0 + 1735933098944: !!python/tuple + - *id053 + - 0 + 1735933099056: !!python/tuple + - *id054 + - 1 + 1735933099168: !!python/tuple + - *id055 + - 1 + 1735933099280: !!python/tuple + - *id056 + - 0 + 1735933099392: !!python/tuple + - *id057 + - 0 + 1735933099504: !!python/tuple + - *id058 + - 0 + 1735933099616: !!python/tuple + - *id059 + - 1 + 1735933099728: !!python/tuple + - *id060 + - 0 + 1735933099840: !!python/tuple + - *id061 + - 1 + 1735940014144: !!python/tuple + - *id062 + - 1 + 1735940014256: !!python/tuple + - *id063 + - 1 + 1735940014368: !!python/tuple + - *id064 + - 0 + 1735940014480: !!python/tuple + - *id065 + - 1 + 1735940014592: !!python/tuple + - *id066 + - 0 + 1735940014704: !!python/tuple + - *id067 + - 0 + 1735940014816: !!python/tuple + - *id068 + - 1 + 1735940014928: !!python/tuple + - *id069 + - 0 + 1735940015040: !!python/tuple + - *id070 + - 0 + 1735940015152: !!python/tuple + - *id071 + - 0 + 1735940015264: !!python/tuple + - *id072 + - 1 + 1735940015376: !!python/tuple + - *id073 + - 1 + 1735940015488: !!python/tuple + - *id074 + - 0 + 1735940015600: !!python/tuple + - *id075 + - 0 + 1735940015712: !!python/tuple + - *id076 + - 0 + 1735940015824: !!python/tuple + - *id077 + - 1 + 1735940015936: !!python/tuple + - *id078 + - 0 + 1735940016048: !!python/tuple + - *id079 + - 1 + 1735940016160: !!python/tuple + - *id080 + - 0 + 1735940016272: !!python/tuple + - *id081 + - 0 + 1735940016384: !!python/tuple + - *id082 + - 1 + 1735940016496: !!python/tuple + - *id083 + - 0 + 1735940016608: !!python/tuple + - *id084 + - 0 + 1735940016720: !!python/tuple + - *id085 + - 0 + 1735940016832: !!python/tuple + - *id086 + - 0 + 1735940016944: !!python/tuple + - *id087 + - 0 + 1735940017056: !!python/tuple + - *id088 + - 0 + 1735940017168: !!python/tuple + - *id089 + - 0 + 1735940017280: !!python/tuple + - *id090 + - 1 + 1735940017392: !!python/tuple + - *id091 + - 1 + 1735940017504: !!python/tuple + - *id092 + - 0 + 1735940017616: !!python/tuple + - *id093 + - 0 + 1735940017728: !!python/tuple + - *id094 + - 1 + 1735940017840: !!python/tuple + - *id095 + - 0 + 1735940017952: !!python/tuple + - *id096 + - 0 + 1735940018064: !!python/tuple + - *id097 + - 0 + 1735940018176: !!python/tuple + - *id098 + - 1 + 1735940018288: !!python/tuple + - *id099 + - 0 + 1735940018400: !!python/tuple + - *id100 + - 0 + 1735940018512: !!python/tuple + - *id101 + - 0 + 1735940018624: !!python/tuple + - *id102 + - 1 + 1735940018736: !!python/tuple + - *id103 + - 0 + 1735940018848: !!python/tuple + - *id104 + - 1 + 1735940018960: !!python/tuple + - *id105 + - 1 + 1735940019072: !!python/tuple + - *id106 + - 0 + 1735940019184: !!python/tuple + - *id107 + - 1 + 1735940019296: !!python/tuple + - *id108 + - 1 + 1735940019408: !!python/tuple + - *id109 + - 0 + 1735940019520: !!python/tuple + - *id110 + - 1 + 1735940019632: !!python/tuple + - *id111 + - 1 +- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution + fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet + _data: !!python/tuple [] + objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735881266400: !!python/tuple + - *id001 + - 26.408565569050655 + variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735933094128: !!python/tuple + - *id002 + - 0 + 1735933094464: !!python/tuple + - *id013 + - 0 + 1735933094576: !!python/tuple + - *id014 + - 1 + 1735933094688: !!python/tuple + - *id015 + - 1 + 1735933094800: !!python/tuple + - *id016 + - 1 + 1735933094912: !!python/tuple + - *id017 + - 0 + 1735933095024: !!python/tuple + - *id018 + - 0 + 1735933095136: !!python/tuple + - *id020 + - 0 + 1735933095248: !!python/tuple + - *id019 + - 1 + 1735933095360: !!python/tuple + - *id021 + - 0 + 1735933095472: !!python/tuple + - *id022 + - 1 + 1735933095584: !!python/tuple + - *id023 + - 1 + 1735933095696: !!python/tuple + - *id025 + - 0 + 1735933095808: !!python/tuple + - *id024 + - 0 + 1735933095920: !!python/tuple + - *id026 + - 1 + 1735933096032: !!python/tuple + - *id027 + - 1 + 1735933096144: !!python/tuple + - *id028 + - 0 + 1735933096256: !!python/tuple + - *id029 + - 0 + 1735933096368: !!python/tuple + - *id030 + - 1 + 1735933096480: !!python/tuple + - *id031 + - 0 + 1735933096592: !!python/tuple + - *id032 + - 0 + 1735933096704: !!python/tuple + - *id033 + - 0 + 1735933096816: !!python/tuple + - *id034 + - 0 + 1735933096928: !!python/tuple + - *id035 + - 0 + 1735933097040: !!python/tuple + - *id036 + - 0 + 1735933097152: !!python/tuple + - *id037 + - 0 + 1735933097264: !!python/tuple + - *id038 + - 0 + 1735933097376: !!python/tuple + - *id039 + - 0 + 1735933097488: !!python/tuple + - *id040 + - 1 + 1735933097600: !!python/tuple + - *id041 + - 0 + 1735933097712: !!python/tuple + - *id042 + - 0 + 1735933097824: !!python/tuple + - *id043 + - 1 + 1735933097936: !!python/tuple + - *id044 + - 1 + 1735933098048: !!python/tuple + - *id045 + - 1 + 1735933098160: !!python/tuple + - *id046 + - 1 + 1735933098272: !!python/tuple + - *id047 + - 0 + 1735933098384: !!python/tuple + - *id048 + - 1 + 1735933098496: !!python/tuple + - *id049 + - 0 + 1735933098608: !!python/tuple + - *id050 + - 0 + 1735933098720: !!python/tuple + - *id051 + - 1 + 1735933098832: !!python/tuple + - *id052 + - 0 + 1735933098944: !!python/tuple + - *id053 + - 0 + 1735933099056: !!python/tuple + - *id054 + - 0 + 1735933099168: !!python/tuple + - *id055 + - 1 + 1735933099280: !!python/tuple + - *id056 + - 0 + 1735933099392: !!python/tuple + - *id057 + - 0 + 1735933099504: !!python/tuple + - *id058 + - 0 + 1735933099616: !!python/tuple + - *id059 + - 1 + 1735933099728: !!python/tuple + - *id060 + - 0 + 1735933099840: !!python/tuple + - *id061 + - 1 + 1735940014144: !!python/tuple + - *id062 + - 1 + 1735940014256: !!python/tuple + - *id063 + - 1 + 1735940014368: !!python/tuple + - *id064 + - 0 + 1735940014480: !!python/tuple + - *id065 + - 1 + 1735940014592: !!python/tuple + - *id066 + - 0 + 1735940014704: !!python/tuple + - *id067 + - 0 + 1735940014816: !!python/tuple + - *id068 + - 1 + 1735940014928: !!python/tuple + - *id069 + - 0 + 1735940015040: !!python/tuple + - *id070 + - 0 + 1735940015152: !!python/tuple + - *id071 + - 0 + 1735940015264: !!python/tuple + - *id072 + - 1 + 1735940015376: !!python/tuple + - *id073 + - 1 + 1735940015488: !!python/tuple + - *id074 + - 0 + 1735940015600: !!python/tuple + - *id075 + - 0 + 1735940015712: !!python/tuple + - *id076 + - 0 + 1735940015824: !!python/tuple + - *id077 + - 1 + 1735940015936: !!python/tuple + - *id078 + - 0 + 1735940016048: !!python/tuple + - *id079 + - 1 + 1735940016160: !!python/tuple + - *id080 + - 0 + 1735940016272: !!python/tuple + - *id081 + - 0 + 1735940016384: !!python/tuple + - *id082 + - 1 + 1735940016496: !!python/tuple + - *id083 + - 0 + 1735940016608: !!python/tuple + - *id084 + - 0 + 1735940016720: !!python/tuple + - *id085 + - 0 + 1735940016832: !!python/tuple + - *id086 + - 0 + 1735940016944: !!python/tuple + - *id087 + - 0 + 1735940017056: !!python/tuple + - *id088 + - 0 + 1735940017168: !!python/tuple + - *id089 + - 0 + 1735940017280: !!python/tuple + - *id090 + - 1 + 1735940017392: !!python/tuple + - *id091 + - 1 + 1735940017504: !!python/tuple + - *id092 + - 0 + 1735940017616: !!python/tuple + - *id093 + - 0 + 1735940017728: !!python/tuple + - *id094 + - 1 + 1735940017840: !!python/tuple + - *id095 + - 0 + 1735940017952: !!python/tuple + - *id096 + - 0 + 1735940018064: !!python/tuple + - *id097 + - 1 + 1735940018176: !!python/tuple + - *id098 + - 1 + 1735940018288: !!python/tuple + - *id099 + - 0 + 1735940018400: !!python/tuple + - *id100 + - 0 + 1735940018512: !!python/tuple + - *id101 + - 0 + 1735940018624: !!python/tuple + - *id102 + - 1 + 1735940018736: !!python/tuple + - *id103 + - 0 + 1735940018848: !!python/tuple + - *id104 + - 1 + 1735940018960: !!python/tuple + - *id105 + - 1 + 1735940019072: !!python/tuple + - *id106 + - 0 + 1735940019184: !!python/tuple + - *id107 + - 1 + 1735940019296: !!python/tuple + - *id108 + - 1 + 1735940019408: !!python/tuple + - *id109 + - 0 + 1735940019520: !!python/tuple + - *id110 + - 0 + 1735940019632: !!python/tuple + - *id111 + - 1 +- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution + fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet + _data: !!python/tuple [] + objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735881266400: !!python/tuple + - *id001 + - 26.401518891548587 + variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735933094128: !!python/tuple + - *id002 + - 0 + 1735933094464: !!python/tuple + - *id013 + - 0 + 1735933094576: !!python/tuple + - *id014 + - 1 + 1735933094688: !!python/tuple + - *id015 + - 1 + 1735933094800: !!python/tuple + - *id016 + - 1 + 1735933094912: !!python/tuple + - *id017 + - 0 + 1735933095024: !!python/tuple + - *id018 + - 0 + 1735933095136: !!python/tuple + - *id020 + - 0 + 1735933095248: !!python/tuple + - *id019 + - 1 + 1735933095360: !!python/tuple + - *id021 + - 0 + 1735933095472: !!python/tuple + - *id022 + - 0 + 1735933095584: !!python/tuple + - *id023 + - 1 + 1735933095696: !!python/tuple + - *id025 + - 0 + 1735933095808: !!python/tuple + - *id024 + - 0 + 1735933095920: !!python/tuple + - *id026 + - 1 + 1735933096032: !!python/tuple + - *id027 + - 1 + 1735933096144: !!python/tuple + - *id028 + - 0 + 1735933096256: !!python/tuple + - *id029 + - 0 + 1735933096368: !!python/tuple + - *id030 + - 1 + 1735933096480: !!python/tuple + - *id031 + - 0 + 1735933096592: !!python/tuple + - *id032 + - 0 + 1735933096704: !!python/tuple + - *id033 + - 0 + 1735933096816: !!python/tuple + - *id034 + - 1 + 1735933096928: !!python/tuple + - *id035 + - 0 + 1735933097040: !!python/tuple + - *id036 + - 0 + 1735933097152: !!python/tuple + - *id037 + - 0 + 1735933097264: !!python/tuple + - *id038 + - 0 + 1735933097376: !!python/tuple + - *id039 + - 0 + 1735933097488: !!python/tuple + - *id040 + - 1 + 1735933097600: !!python/tuple + - *id041 + - 0 + 1735933097712: !!python/tuple + - *id042 + - 0 + 1735933097824: !!python/tuple + - *id043 + - 1 + 1735933097936: !!python/tuple + - *id044 + - 1 + 1735933098048: !!python/tuple + - *id045 + - 1 + 1735933098160: !!python/tuple + - *id046 + - 1 + 1735933098272: !!python/tuple + - *id047 + - 0 + 1735933098384: !!python/tuple + - *id048 + - 1 + 1735933098496: !!python/tuple + - *id049 + - 0 + 1735933098608: !!python/tuple + - *id050 + - 0 + 1735933098720: !!python/tuple + - *id051 + - 1 + 1735933098832: !!python/tuple + - *id052 + - 0 + 1735933098944: !!python/tuple + - *id053 + - 0 + 1735933099056: !!python/tuple + - *id054 + - 1 + 1735933099168: !!python/tuple + - *id055 + - 1 + 1735933099280: !!python/tuple + - *id056 + - 0 + 1735933099392: !!python/tuple + - *id057 + - 0 + 1735933099504: !!python/tuple + - *id058 + - 0 + 1735933099616: !!python/tuple + - *id059 + - 1 + 1735933099728: !!python/tuple + - *id060 + - 0 + 1735933099840: !!python/tuple + - *id061 + - 1 + 1735940014144: !!python/tuple + - *id062 + - 1 + 1735940014256: !!python/tuple + - *id063 + - 1 + 1735940014368: !!python/tuple + - *id064 + - 0 + 1735940014480: !!python/tuple + - *id065 + - 1 + 1735940014592: !!python/tuple + - *id066 + - 0 + 1735940014704: !!python/tuple + - *id067 + - 0 + 1735940014816: !!python/tuple + - *id068 + - 1 + 1735940014928: !!python/tuple + - *id069 + - 0 + 1735940015040: !!python/tuple + - *id070 + - 0 + 1735940015152: !!python/tuple + - *id071 + - 0 + 1735940015264: !!python/tuple + - *id072 + - 1 + 1735940015376: !!python/tuple + - *id073 + - 1 + 1735940015488: !!python/tuple + - *id074 + - 0 + 1735940015600: !!python/tuple + - *id075 + - 0 + 1735940015712: !!python/tuple + - *id076 + - 0 + 1735940015824: !!python/tuple + - *id077 + - 1 + 1735940015936: !!python/tuple + - *id078 + - 0 + 1735940016048: !!python/tuple + - *id079 + - 1 + 1735940016160: !!python/tuple + - *id080 + - 0 + 1735940016272: !!python/tuple + - *id081 + - 0 + 1735940016384: !!python/tuple + - *id082 + - 1 + 1735940016496: !!python/tuple + - *id083 + - 0 + 1735940016608: !!python/tuple + - *id084 + - 0 + 1735940016720: !!python/tuple + - *id085 + - 0 + 1735940016832: !!python/tuple + - *id086 + - 0 + 1735940016944: !!python/tuple + - *id087 + - 0 + 1735940017056: !!python/tuple + - *id088 + - 0 + 1735940017168: !!python/tuple + - *id089 + - 0 + 1735940017280: !!python/tuple + - *id090 + - 1 + 1735940017392: !!python/tuple + - *id091 + - 1 + 1735940017504: !!python/tuple + - *id092 + - 0 + 1735940017616: !!python/tuple + - *id093 + - 0 + 1735940017728: !!python/tuple + - *id094 + - 1 + 1735940017840: !!python/tuple + - *id095 + - 0 + 1735940017952: !!python/tuple + - *id096 + - 0 + 1735940018064: !!python/tuple + - *id097 + - 1 + 1735940018176: !!python/tuple + - *id098 + - 0 + 1735940018288: !!python/tuple + - *id099 + - 0 + 1735940018400: !!python/tuple + - *id100 + - 0 + 1735940018512: !!python/tuple + - *id101 + - 0 + 1735940018624: !!python/tuple + - *id102 + - 1 + 1735940018736: !!python/tuple + - *id103 + - 0 + 1735940018848: !!python/tuple + - *id104 + - 1 + 1735940018960: !!python/tuple + - *id105 + - 1 + 1735940019072: !!python/tuple + - *id106 + - 0 + 1735940019184: !!python/tuple + - *id107 + - 1 + 1735940019296: !!python/tuple + - *id108 + - 1 + 1735940019408: !!python/tuple + - *id109 + - 0 + 1735940019520: !!python/tuple + - *id110 + - 1 + 1735940019632: !!python/tuple + - *id111 + - 1 +- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution + fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet + _data: !!python/tuple [] + objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735881266400: !!python/tuple + - *id001 + - 26.398391602333636 + variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735933094128: !!python/tuple + - *id002 + - 0 + 1735933094464: !!python/tuple + - *id013 + - 0 + 1735933094576: !!python/tuple + - *id014 + - 1 + 1735933094688: !!python/tuple + - *id015 + - 1 + 1735933094800: !!python/tuple + - *id016 + - 1 + 1735933094912: !!python/tuple + - *id017 + - 0 + 1735933095024: !!python/tuple + - *id018 + - 0 + 1735933095136: !!python/tuple + - *id020 + - 0 + 1735933095248: !!python/tuple + - *id019 + - 1 + 1735933095360: !!python/tuple + - *id021 + - 0 + 1735933095472: !!python/tuple + - *id022 + - 0 + 1735933095584: !!python/tuple + - *id023 + - 1 + 1735933095696: !!python/tuple + - *id025 + - 0 + 1735933095808: !!python/tuple + - *id024 + - 0 + 1735933095920: !!python/tuple + - *id026 + - 1 + 1735933096032: !!python/tuple + - *id027 + - 1 + 1735933096144: !!python/tuple + - *id028 + - 0 + 1735933096256: !!python/tuple + - *id029 + - 0 + 1735933096368: !!python/tuple + - *id030 + - 1 + 1735933096480: !!python/tuple + - *id031 + - 0 + 1735933096592: !!python/tuple + - *id032 + - 0 + 1735933096704: !!python/tuple + - *id033 + - 0 + 1735933096816: !!python/tuple + - *id034 + - 0 + 1735933096928: !!python/tuple + - *id035 + - 0 + 1735933097040: !!python/tuple + - *id036 + - 0 + 1735933097152: !!python/tuple + - *id037 + - 0 + 1735933097264: !!python/tuple + - *id038 + - 0 + 1735933097376: !!python/tuple + - *id039 + - 0 + 1735933097488: !!python/tuple + - *id040 + - 1 + 1735933097600: !!python/tuple + - *id041 + - 0 + 1735933097712: !!python/tuple + - *id042 + - 0 + 1735933097824: !!python/tuple + - *id043 + - 1 + 1735933097936: !!python/tuple + - *id044 + - 1 + 1735933098048: !!python/tuple + - *id045 + - 1 + 1735933098160: !!python/tuple + - *id046 + - 1 + 1735933098272: !!python/tuple + - *id047 + - 0 + 1735933098384: !!python/tuple + - *id048 + - 1 + 1735933098496: !!python/tuple + - *id049 + - 0 + 1735933098608: !!python/tuple + - *id050 + - 0 + 1735933098720: !!python/tuple + - *id051 + - 1 + 1735933098832: !!python/tuple + - *id052 + - 0 + 1735933098944: !!python/tuple + - *id053 + - 0 + 1735933099056: !!python/tuple + - *id054 + - 1 + 1735933099168: !!python/tuple + - *id055 + - 1 + 1735933099280: !!python/tuple + - *id056 + - 0 + 1735933099392: !!python/tuple + - *id057 + - 0 + 1735933099504: !!python/tuple + - *id058 + - 0 + 1735933099616: !!python/tuple + - *id059 + - 1 + 1735933099728: !!python/tuple + - *id060 + - 0 + 1735933099840: !!python/tuple + - *id061 + - 1 + 1735940014144: !!python/tuple + - *id062 + - 1 + 1735940014256: !!python/tuple + - *id063 + - 1 + 1735940014368: !!python/tuple + - *id064 + - 0 + 1735940014480: !!python/tuple + - *id065 + - 1 + 1735940014592: !!python/tuple + - *id066 + - 0 + 1735940014704: !!python/tuple + - *id067 + - 0 + 1735940014816: !!python/tuple + - *id068 + - 1 + 1735940014928: !!python/tuple + - *id069 + - 0 + 1735940015040: !!python/tuple + - *id070 + - 0 + 1735940015152: !!python/tuple + - *id071 + - 0 + 1735940015264: !!python/tuple + - *id072 + - 1 + 1735940015376: !!python/tuple + - *id073 + - 1 + 1735940015488: !!python/tuple + - *id074 + - 0 + 1735940015600: !!python/tuple + - *id075 + - 0 + 1735940015712: !!python/tuple + - *id076 + - 0 + 1735940015824: !!python/tuple + - *id077 + - 1 + 1735940015936: !!python/tuple + - *id078 + - 0 + 1735940016048: !!python/tuple + - *id079 + - 1 + 1735940016160: !!python/tuple + - *id080 + - 0 + 1735940016272: !!python/tuple + - *id081 + - 0 + 1735940016384: !!python/tuple + - *id082 + - 1 + 1735940016496: !!python/tuple + - *id083 + - 0 + 1735940016608: !!python/tuple + - *id084 + - 0 + 1735940016720: !!python/tuple + - *id085 + - 0 + 1735940016832: !!python/tuple + - *id086 + - 0 + 1735940016944: !!python/tuple + - *id087 + - 0 + 1735940017056: !!python/tuple + - *id088 + - 0 + 1735940017168: !!python/tuple + - *id089 + - 0 + 1735940017280: !!python/tuple + - *id090 + - 1 + 1735940017392: !!python/tuple + - *id091 + - 1 + 1735940017504: !!python/tuple + - *id092 + - 0 + 1735940017616: !!python/tuple + - *id093 + - 0 + 1735940017728: !!python/tuple + - *id094 + - 1 + 1735940017840: !!python/tuple + - *id095 + - 0 + 1735940017952: !!python/tuple + - *id096 + - 0 + 1735940018064: !!python/tuple + - *id097 + - 1 + 1735940018176: !!python/tuple + - *id098 + - 0 + 1735940018288: !!python/tuple + - *id099 + - 0 + 1735940018400: !!python/tuple + - *id100 + - 0 + 1735940018512: !!python/tuple + - *id101 + - 0 + 1735940018624: !!python/tuple + - *id102 + - 1 + 1735940018736: !!python/tuple + - *id103 + - 0 + 1735940018848: !!python/tuple + - *id104 + - 1 + 1735940018960: !!python/tuple + - *id105 + - 1 + 1735940019072: !!python/tuple + - *id106 + - 0 + 1735940019184: !!python/tuple + - *id107 + - 1 + 1735940019296: !!python/tuple + - *id108 + - 1 + 1735940019408: !!python/tuple + - *id109 + - 0 + 1735940019520: !!python/tuple + - *id110 + - 1 + 1735940019632: !!python/tuple + - *id111 + - 1 +- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution + fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet + _data: !!python/tuple [] + objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735881266400: !!python/tuple + - *id001 + - 26.387201703323992 + variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap + state: + - 1735933094128: !!python/tuple + - *id002 + - 0 + 1735933094464: !!python/tuple + - *id013 + - 0 + 1735933094576: !!python/tuple + - *id014 + - 1 + 1735933094688: !!python/tuple + - *id015 + - 1 + 1735933094800: !!python/tuple + - *id016 + - 1 + 1735933094912: !!python/tuple + - *id017 + - 0 + 1735933095024: !!python/tuple + - *id018 + - 0 + 1735933095136: !!python/tuple + - *id020 + - 0 + 1735933095248: !!python/tuple + - *id019 + - 1 + 1735933095360: !!python/tuple + - *id021 + - 0 + 1735933095472: !!python/tuple + - *id022 + - 0 + 1735933095584: !!python/tuple + - *id023 + - 1 + 1735933095696: !!python/tuple + - *id025 + - 0 + 1735933095808: !!python/tuple + - *id024 + - 0 + 1735933095920: !!python/tuple + - *id026 + - 1 + 1735933096032: !!python/tuple + - *id027 + - 1 + 1735933096144: !!python/tuple + - *id028 + - 0 + 1735933096256: !!python/tuple + - *id029 + - 0 + 1735933096368: !!python/tuple + - *id030 + - 1 + 1735933096480: !!python/tuple + - *id031 + - 0 + 1735933096592: !!python/tuple + - *id032 + - 0 + 1735933096704: !!python/tuple + - *id033 + - 0 + 1735933096816: !!python/tuple + - *id034 + - 1 + 1735933096928: !!python/tuple + - *id035 + - 0 + 1735933097040: !!python/tuple + - *id036 + - 0 + 1735933097152: !!python/tuple + - *id037 + - 0 + 1735933097264: !!python/tuple + - *id038 + - 0 + 1735933097376: !!python/tuple + - *id039 + - 0 + 1735933097488: !!python/tuple + - *id040 + - 1 + 1735933097600: !!python/tuple + - *id041 + - 0 + 1735933097712: !!python/tuple + - *id042 + - 0 + 1735933097824: !!python/tuple + - *id043 + - 1 + 1735933097936: !!python/tuple + - *id044 + - 1 + 1735933098048: !!python/tuple + - *id045 + - 1 + 1735933098160: !!python/tuple + - *id046 + - 1 + 1735933098272: !!python/tuple + - *id047 + - 0 + 1735933098384: !!python/tuple + - *id048 + - 1 + 1735933098496: !!python/tuple + - *id049 + - 0 + 1735933098608: !!python/tuple + - *id050 + - 0 + 1735933098720: !!python/tuple + - *id051 + - 1 + 1735933098832: !!python/tuple + - *id052 + - 0 + 1735933098944: !!python/tuple + - *id053 + - 0 + 1735933099056: !!python/tuple + - *id054 + - 0 + 1735933099168: !!python/tuple + - *id055 + - 1 + 1735933099280: !!python/tuple + - *id056 + - 0 + 1735933099392: !!python/tuple + - *id057 + - 0 + 1735933099504: !!python/tuple + - *id058 + - 0 + 1735933099616: !!python/tuple + - *id059 + - 1 + 1735933099728: !!python/tuple + - *id060 + - 0 + 1735933099840: !!python/tuple + - *id061 + - 1 + 1735940014144: !!python/tuple + - *id062 + - 1 + 1735940014256: !!python/tuple + - *id063 + - 1 + 1735940014368: !!python/tuple + - *id064 + - 0 + 1735940014480: !!python/tuple + - *id065 + - 1 + 1735940014592: !!python/tuple + - *id066 + - 0 + 1735940014704: !!python/tuple + - *id067 + - 0 + 1735940014816: !!python/tuple + - *id068 + - 1 + 1735940014928: !!python/tuple + - *id069 + - 0 + 1735940015040: !!python/tuple + - *id070 + - 0 + 1735940015152: !!python/tuple + - *id071 + - 0 + 1735940015264: !!python/tuple + - *id072 + - 1 + 1735940015376: !!python/tuple + - *id073 + - 1 + 1735940015488: !!python/tuple + - *id074 + - 0 + 1735940015600: !!python/tuple + - *id075 + - 0 + 1735940015712: !!python/tuple + - *id076 + - 0 + 1735940015824: !!python/tuple + - *id077 + - 1 + 1735940015936: !!python/tuple + - *id078 + - 0 + 1735940016048: !!python/tuple + - *id079 + - 1 + 1735940016160: !!python/tuple + - *id080 + - 0 + 1735940016272: !!python/tuple + - *id081 + - 0 + 1735940016384: !!python/tuple + - *id082 + - 1 + 1735940016496: !!python/tuple + - *id083 + - 0 + 1735940016608: !!python/tuple + - *id084 + - 0 + 1735940016720: !!python/tuple + - *id085 + - 0 + 1735940016832: !!python/tuple + - *id086 + - 0 + 1735940016944: !!python/tuple + - *id087 + - 0 + 1735940017056: !!python/tuple + - *id088 + - 0 + 1735940017168: !!python/tuple + - *id089 + - 0 + 1735940017280: !!python/tuple + - *id090 + - 1 + 1735940017392: !!python/tuple + - *id091 + - 1 + 1735940017504: !!python/tuple + - *id092 + - 0 + 1735940017616: !!python/tuple + - *id093 + - 0 + 1735940017728: !!python/tuple + - *id094 + - 1 + 1735940017840: !!python/tuple + - *id095 + - 0 + 1735940017952: !!python/tuple + - *id096 + - 0 + 1735940018064: !!python/tuple + - *id097 + - 1 + 1735940018176: !!python/tuple + - *id098 + - 1 + 1735940018288: !!python/tuple + - *id099 + - 0 + 1735940018400: !!python/tuple + - *id100 + - 0 + 1735940018512: !!python/tuple + - *id101 + - 0 + 1735940018624: !!python/tuple + - *id102 + - 1 + 1735940018736: !!python/tuple + - *id103 + - 0 + 1735940018848: !!python/tuple + - *id104 + - 1 + 1735940018960: !!python/tuple + - *id105 + - 1 + 1735940019072: !!python/tuple + - *id106 + - 0 + 1735940019184: !!python/tuple + - *id107 + - 1 + 1735940019296: !!python/tuple + - *id108 + - 1 + 1735940019408: !!python/tuple + - *id109 + - 1 + 1735940019520: !!python/tuple + - *id110 + - 1 + 1735940019632: !!python/tuple + - *id111 + - 1 From 58d18b9d605461800c44190ce09c349fa5cbd988 Mon Sep 17 00:00:00 2001 From: jlgearh Date: Sun, 9 Apr 2023 19:20:31 -0600 Subject: [PATCH 0003/3044] - Refactoring the aos code to be more general for inclusion as a Pyomo contrib package --- .../alternative_solutions/aos_utils.py | 13 +---- .../contrib/alternative_solutions/solnpool.py | 10 ++-- .../contrib/alternative_solutions/solution.py | 47 +++++++++++++++---- .../tests/test_solnpool.py | 6 +-- .../alternative_solutions/var_utils.py | 6 +-- 5 files changed, 49 insertions(+), 33 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index bd0abf9e7b1..7fa1ef9466b 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -10,9 +10,7 @@ from numpy.linalg import norm from pyomo.common.modeling import unique_component_name -from pyomo.common.collections import ComponentSet import pyomo.environ as pe -import pyomo.util.vars_from_expressions as vfe from pyomo.opt import SolverFactory from pyomo.core.base.PyomoModel import ConcreteModel from pyomo.contrib import appsi @@ -43,7 +41,7 @@ def _get_active_objective(model): active_objs = [o for o in model.component_data_objects(pe.Objective, active=True)] assert len(active_objs) == 1, \ - "Model has more than one active objective function" + "Model has zero or more than one active objective function" return active_objs[0] @@ -106,15 +104,6 @@ def _get_max_solutions(max_solutions): num_solutions = sys.maxsize return num_solutions - - - -def get_solution(model, variables): - solution = [] - for var in variables: - solution.append((var, pe.value(var))) - return solution - def _get_random_direction(num_dimensions): idx = 0 while idx < 100: diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 18fb3210448..30c8413509b 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -40,7 +40,7 @@ def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=None, (i.e. that any feasible solution can be considered by Gurobi). This parameter maps to the PoolGapAbs parameter in Gurobi. search_mode : 0, 1, or 2 - Indicates the Solution Pool mode that is used to generate + Indicates the SolutionPool mode that is used to generate alternative solutions in Gurobi. Mode 2 should typically be used as it finds the best n solutions. Mode 0 finds a single optimal solution (i.e. the standard mode in Gurobi). Mode 1 will generate n @@ -50,7 +50,7 @@ def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=None, Boolean indicating that discrete values should be rounded to the nearest integer in the solutions results. solver_options : dict - Solver option-value pairs to be passed to the solver. + Solver option-value pairs to be passed to the Gurobi solver. Returns ------- @@ -90,11 +90,13 @@ def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=None, # Get model solutions solution_count = opt.get_model_attr('SolCount') - print("Gurobi found {} solutions".format(solution_count)) + print("Gurobi found {} solutions.".format(solution_count)) variables = var_utils.get_model_variables(model, 'all', include_fixed=True) solutions = [] for i in range(solution_count): results.solution_loader.load_vars(solution_number=i) - solutions.append(solution.Solution(model, variables)) + solutions.append(solution.Solution(model, variables, + round_discrete_vars=\ + round_discrete_vars)) return solutions \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/solution.py b/pyomo/contrib/alternative_solutions/solution.py index c83850f773b..54475593187 100644 --- a/pyomo/contrib/alternative_solutions/solution.py +++ b/pyomo/contrib/alternative_solutions/solution.py @@ -11,6 +11,7 @@ import pyomo.environ as pe from pyomo.common.collections import ComponentMap, ComponentSet +from pyomo.contrib.alternative_solutions import aos_utils, var_utils class Solution: """ @@ -19,16 +20,22 @@ class Solution: Attributes ---------- variables : ComponentMap - A map between Pyomo variable objects and their values for a solution. + A map between Pyomo variables and their values for a solution. fixed_vars : ComponentSet The set of Pyomo variables that are fixed in a solution. objectives : ComponentMap - A map between Pyomo objective objects and their values for a solution. + A map between Pyomo objectives and their values for a solution. Methods ------- pprint(): - Prints the solution. + Prints a solution. + get_variable_name_values(self, ignore_fixed_vars=False): + Get a dictionary of variable name-variable value pairs. + get_fixed_variable_names(self): + Get a list of fixed-variable names. + def get_objective_name_values(self): + Get a dictionary of objective name-objective value pairs. """ def __init__(self, model, variable_list, ignore_fixed_vars=False, @@ -49,7 +56,13 @@ def __init__(self, model, variable_list, ignore_fixed_vars=False, Boolean indicating that discrete values should be rounded to the nearest integer in the solutions results. """ - + + aos_utils._is_concrete_model(model) + assert isinstance(ignore_fixed_vars, bool), \ + 'ignore_fixed_vars must be a Boolean' + assert isinstance(round_discrete_vars, bool), \ + 'round_discrete_vars must be a Boolean' + self.variables = ComponentMap() self.fixed_vars = ComponentSet() for var in variable_list: @@ -63,19 +76,33 @@ def __init__(self, model, variable_list, ignore_fixed_vars=False, self.fixed_vars.add(var) self.objectives = ComponentMap() + # TODO: Should inactive objectives be included? for obj in model.component_data_objects(pe.Objective, active=True): self.objectives[obj] = pe.value(obj) def pprint(self): '''Print the solution variable and objective values.''' - fixed_string = "(fixed)" - print("Variable: Value") + fixed_string = "Yes" + print("Variable, Value, Fixed?") for variable, value in self.variables.items(): if variable in self.fixed_vars: - print("{}: {} {}".format(variable.name, value, fixed_string)) + print("{}, {}, {}".format(variable.name, value, fixed_string)) else: - print("{}: {}".format(variable.name, value)) + print("{}, {}".format(variable.name, value)) print() - print("Objective: Value") + print("Objective, Value") for objective, value in self.objectives.items(): - print("{}: {}".format(objective.name, value)) \ No newline at end of file + print("{}, {}".format(objective.name, value)) + + def get_variable_name_values(self, ignore_fixed_vars=False): + '''Get a dictionary of variable name-variable value pairs.''' + return {var.name: value for var, value in self.variables.items() if + not (ignore_fixed_vars and var in self.fixed_vars)} + + def get_fixed_variable_names(self): + '''Get a list of fixed-variable names.''' + return [var.name for var in self.fixed_vars] + + def get_objective_name_values(self): + '''Get a dictionary of objective name-objective value pairs.''' + return {obj.name: value for obj, value in self.objectives.items()} \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index d94c2a8fcd0..b45173f9aa8 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -4,10 +4,10 @@ import pytest import random -from munch import unmunchify import pyutilib.misc import pyomo.environ as pe -from pyomo.contrib.alternative_solutions.solnpool import gurobi_generate_solutions +from pyomo.contrib.alternative_solutions.solnpool import \ + gurobi_generate_solutions from pyomo.common.fileutils import this_file_dir from pyomo.contrib.alternative_solutions.comparison import consensus @@ -43,7 +43,7 @@ def run(testname, model, N, debug=False): print(solutions) # Verify final results - results = [unmunchify(soln) for soln in solutions] + results = [soln.get_variable_name_values() for soln in solutions] output = yaml.dump(results, default_flow_style=None) outputfile = join(currdir, "{}_results.yaml".format(testname)) with open(outputfile, "w") as OUTPUT: diff --git a/pyomo/contrib/alternative_solutions/var_utils.py b/pyomo/contrib/alternative_solutions/var_utils.py index f5d5d7c9b83..9b39b0a0228 100644 --- a/pyomo/contrib/alternative_solutions/var_utils.py +++ b/pyomo/contrib/alternative_solutions/var_utils.py @@ -66,6 +66,7 @@ def get_model_variables(model, components='all', include_continuous=True, A Pyomo ComponentSet containing _GeneralVarData variables. ''' + # Validate inputs aos_utils._is_concrete_model(model) assert isinstance(include_continuous, bool), \ 'include_continuous must be a Boolean' @@ -74,8 +75,8 @@ def get_model_variables(model, components='all', include_continuous=True, 'include_integer must be a Boolean' assert isinstance(include_fixed, bool), 'include_fixed must be a Boolean' + # Gather variables variable_set = ComponentSet() - if components == 'all': var_generator = vfe.get_vars_from_components(model, pe.Constraint, include_fixed=\ @@ -83,7 +84,6 @@ def get_model_variables(model, components='all', include_continuous=True, _filter_model_variables(variable_set, var_generator, include_continuous, include_binary, include_integer, include_fixed) - print('im here') else: assert hasattr(components, '__iter__'), \ ('components parameters must be an iterable collection of Pyomo' @@ -130,7 +130,5 @@ def get_model_variables(model, components='all', include_continuous=True, return variable_set - - def check_variables(model, variables): pass \ No newline at end of file From 5eeffde0b36302a6ab073bd5e3e6a5de2b6f6655 Mon Sep 17 00:00:00 2001 From: "Philipp Christophel (phchri)" Date: Fri, 23 Jun 2023 09:42:08 -0400 Subject: [PATCH 0004/3044] Initial version of the SAS solver interfaces and unit tests. --- pyomo/solvers/plugins/solvers/SAS.py | 700 ++++++++++++++++++++++ pyomo/solvers/plugins/solvers/__init__.py | 1 + pyomo/solvers/tests/checks/test_SAS.py | 462 ++++++++++++++ 3 files changed, 1163 insertions(+) create mode 100644 pyomo/solvers/plugins/solvers/SAS.py create mode 100644 pyomo/solvers/tests/checks/test_SAS.py diff --git a/pyomo/solvers/plugins/solvers/SAS.py b/pyomo/solvers/plugins/solvers/SAS.py new file mode 100644 index 00000000000..7f50b7a2970 --- /dev/null +++ b/pyomo/solvers/plugins/solvers/SAS.py @@ -0,0 +1,700 @@ +__all__ = ['SAS'] + +import logging +import sys +import os + +from io import StringIO +from abc import ABC, abstractmethod +from contextlib import redirect_stdout + +from pyomo.opt.base import ProblemFormat, ResultsFormat, OptSolver +from pyomo.opt.base.solvers import SolverFactory +from pyomo.common.collections import Bunch +from pyomo.opt.results import ( + SolverResults, + SolverStatus, + TerminationCondition, + SolutionStatus, + ProblemSense, +) +from pyomo.common.tempfiles import TempfileManager +from pyomo.core.base import Var +from pyomo.core.base.block import _BlockData +from pyomo.core.kernel.block import IBlock + + +logger = logging.getLogger('pyomo.solvers') + + +STATUS_TO_SOLVERSTATUS = { + "OK": SolverStatus.ok, + "SYNTAX_ERROR": SolverStatus.error, + "DATA_ERROR": SolverStatus.error, + "OUT_OF_MEMORY": SolverStatus.aborted, + "IO_ERROR": SolverStatus.error, + "ERROR": SolverStatus.error, +} + +# This combines all status codes from OPTLP/solvelp and OPTMILP/solvemilp +SOLSTATUS_TO_TERMINATIONCOND = { + "OPTIMAL": TerminationCondition.optimal, + "OPTIMAL_AGAP": TerminationCondition.optimal, + "OPTIMAL_RGAP": TerminationCondition.optimal, + "OPTIMAL_COND": TerminationCondition.optimal, + "TARGET": TerminationCondition.optimal, + "CONDITIONAL_OPTIMAL": TerminationCondition.optimal, + "FEASIBLE": TerminationCondition.feasible, + "INFEASIBLE": TerminationCondition.infeasible, + "UNBOUNDED": TerminationCondition.unbounded, + "INFEASIBLE_OR_UNBOUNDED": TerminationCondition.infeasibleOrUnbounded, + "SOLUTION_LIM": TerminationCondition.maxEvaluations, + "NODE_LIM_SOL": TerminationCondition.maxEvaluations, + "NODE_LIM_NOSOL": TerminationCondition.maxEvaluations, + "ITERATION_LIMIT_REACHED": TerminationCondition.maxIterations, + "TIME_LIM_SOL": TerminationCondition.maxTimeLimit, + "TIME_LIM_NOSOL": TerminationCondition.maxTimeLimit, + "TIME_LIMIT_REACHED": TerminationCondition.maxTimeLimit, + "ABORTED": TerminationCondition.userInterrupt, + "ABORT_SOL": TerminationCondition.userInterrupt, + "ABORT_NOSOL": TerminationCondition.userInterrupt, + "OUTMEM_SOL": TerminationCondition.solverFailure, + "OUTMEM_NOSOL": TerminationCondition.solverFailure, + "FAILED": TerminationCondition.solverFailure, + "FAIL_SOL": TerminationCondition.solverFailure, + "FAIL_NOSOL": TerminationCondition.solverFailure, +} + + +SOLSTATUS_TO_MESSAGE = { + "OPTIMAL": "The solution is optimal.", + "OPTIMAL_AGAP": "The solution is optimal within the absolute gap specified by the ABSOBJGAP= option.", + "OPTIMAL_RGAP": "The solution is optimal within the relative gap specified by the RELOBJGAP= option.", + "OPTIMAL_COND": "The solution is optimal, but some infeasibilities (primal, bound, or integer) exceed tolerances due to scaling or choice of a small INTTOL= value.", + "TARGET": "The solution is not worse than the target specified by the TARGET= option.", + "CONDITIONAL_OPTIMAL": "The solution is optimal, but some infeasibilities (primal, dual or bound) exceed tolerances due to scaling or preprocessing.", + "FEASIBLE": "The problem is feasible. This status is displayed when the IIS=TRUE option is specified and the problem is feasible.", + "INFEASIBLE": "The problem is infeasible.", + "UNBOUNDED": "The problem is unbounded.", + "INFEASIBLE_OR_UNBOUNDED": "The problem is infeasible or unbounded.", + "SOLUTION_LIM": "The solver reached the maximum number of solutions specified by the MAXSOLS= option.", + "NODE_LIM_SOL": "The solver reached the maximum number of nodes specified by the MAXNODES= option and found a solution.", + "NODE_LIM_NOSOL": "The solver reached the maximum number of nodes specified by the MAXNODES= option and did not find a solution.", + "ITERATION_LIMIT_REACHED": "The maximum allowable number of iterations was reached.", + "TIME_LIM_SOL": "The solver reached the execution time limit specified by the MAXTIME= option and found a solution.", + "TIME_LIM_NOSOL": "The solver reached the execution time limit specified by the MAXTIME= option and did not find a solution.", + "TIME_LIMIT_REACHED": "The solver reached its execution time limit.", + "ABORTED": "The solver was interrupted externally.", + "ABORT_SOL": "The solver was stopped by the user but still found a solution.", + "ABORT_NOSOL": "The solver was stopped by the user and did not find a solution.", + "OUTMEM_SOL": "The solver ran out of memory but still found a solution.", + "OUTMEM_NOSOL": "The solver ran out of memory and either did not find a solution or failed to output the solution due to insufficient memory.", + "FAILED": "The solver failed to converge, possibly due to numerical issues.", + "FAIL_SOL": "The solver stopped due to errors but still found a solution.", + "FAIL_NOSOL": "The solver stopped due to errors and did not find a solution.", +} + + +CAS_OPTION_NAMES = [ + "hostname", + "port", + "username", + "password", + "session", + "locale", + "name", + "nworkers", + "authinfo", + "protocol", + "path", + "ssl_ca_list", + "authcode", +] + + +@SolverFactory.register('sas', doc='The SAS LP/MIP solver') +class SAS(OptSolver): + """The SAS optimization solver""" + + def __new__(cls, *args, **kwds): + mode = kwds.pop('solver_io', None) + if mode != None: + return SolverFactory(mode) + else: + # Choose solver factory automatically + # bassed on what can be loaded. + s = SolverFactory('_sas94', **kwds) + if not s.available(): + s = SolverFactory('_sascas', **kwds) + return s + + +class SASAbc(ABC, OptSolver): + """Abstract base class for the SAS solver interfaces. Simply to avoid code duplication.""" + + def __init__(self, **kwds): + """Initialize the SAS solver interfaces.""" + kwds['type'] = 'sas' + super(SASAbc, self).__init__(**kwds) + + # + # Set up valid problem formats and valid results for each + # problem format + # + self._valid_problem_formats = [ProblemFormat.mps] + self._valid_result_formats = {ProblemFormat.mps: [ResultsFormat.soln]} + + self._keepfiles = False + self._capabilities.linear = True + self._capabilities.integer = True + + super(SASAbc, self).set_problem_format(ProblemFormat.mps) + + def _presolve(self, *args, **kwds): + """ "Set things up for the actual solve.""" + # create a context in the temporary file manager for + # this plugin - is "pop"ed in the _postsolve method. + TempfileManager.push() + + # Get the warmstart flag + self.warmstart_flag = kwds.pop('warmstart', False) + + # Call parent presolve function + super(SASAbc, self)._presolve(*args, **kwds) + + # Store the model, too bad this is not done in the base class + for arg in args: + if isinstance(arg, (_BlockData, IBlock)): + # Store the instance + self._instance = arg + self._vars = [] + for block in self._instance.block_data_objects(active=True): + for vardata in block.component_data_objects( + Var, active=True, descend_into=False + ): + self._vars.append(vardata) + # Store the symbal map, we need this for example when writing the warmstart file + if isinstance(self._instance, IBlock): + self._smap = getattr(self._instance, "._symbol_maps")[self._smap_id] + else: + self._smap = self._instance.solutions.symbol_map[self._smap_id] + + # Create the primalin data + if self.warmstart_flag: + filename = self._warm_start_file_name = TempfileManager.create_tempfile( + ".sol", text=True + ) + smap = self._smap + numWritten = 0 + with open(filename, 'w') as file: + file.write('_VAR_,_VALUE_\n') + for var in self._vars: + if (var.value is not None) and (id(var) in smap.byObject): + name = smap.byObject[id(var)] + file.write( + "{name},{value}\n".format(name=name, value=var.value) + ) + numWritten += 1 + if numWritten == 0: + # No solution available, disable warmstart + self.warmstart_flag = False + + def available(self, exception_flag=False): + """True if the solver is available""" + return self._python_api_exists + + def _has_integer_variables(self): + """True if the problem has integer variables.""" + for vardata in self._vars: + if vardata.is_binary() or vardata.is_integer(): + return True + return False + + def _create_results_from_status(self, status, solution_status): + """Create a results object and set the status code and messages.""" + results = SolverResults() + results.solver.name = "SAS" + results.solver.status = STATUS_TO_SOLVERSTATUS[status] + if results.solver.status == SolverStatus.ok: + results.solver.termination_condition = SOLSTATUS_TO_TERMINATIONCOND[ + solution_status + ] + results.solver.message = ( + results.solver.termination_message + ) = SOLSTATUS_TO_MESSAGE[solution_status] + results.solver.status = TerminationCondition.to_solver_status( + results.solver.termination_condition + ) + elif results.solver.status == SolverStatus.aborted: + results.solver.termination_condition = TerminationCondition.userInterrupt + results.solver.message = ( + results.solver.termination_message + ) = SOLSTATUS_TO_MESSAGE["ABORTED"] + else: + results.solver.termination_condition = TerminationCondition.error + results.solver.message = ( + results.solver.termination_message + ) = SOLSTATUS_TO_MESSAGE["FAILED"] + return results + + @abstractmethod + def _apply_solver(self): + """The routine that performs the solve""" + raise NotImplemented("This is an abstract function and thus not implemented!") + + def _postsolve(self): + """Clean up at the end, especially the temp files.""" + # Let the base class deal with returning results. + results = super(SASAbc, self)._postsolve() + + # Finally, clean any temporary files registered with the temp file + # manager, created populated *directly* by this plugin. does not + # include, for example, the execution script. but does include + # the warm-start file. + TempfileManager.pop(remove=not self._keepfiles) + + return results + + def warm_start_capable(self): + """True if the solver interface supports MILP warmstarting.""" + return True + + +@SolverFactory.register('_sas94', doc='SAS 9.4 interface') +class SAS94(SASAbc): + """ + Solver interface for SAS 9.4 using saspy. See the saspy documentation about + how to create a connection. + """ + + def __init__(self, **kwds): + """Initialize the solver interface and see if the saspy package is available.""" + super(SAS94, self).__init__(**kwds) + + try: + import saspy + + self._sas = saspy + except ImportError: + self._python_api_exists = False + except Exception as e: + self._python_api_exists = False + # For other exceptions, raise it so that it does not get lost + raise e + else: + self._python_api_exists = True + self._sas.logger.setLevel(logger.level) + + def _create_statement_str(self, statement): + """Helper function to create the strings for the statements of the proc OPTLP/OPTMILP code.""" + stmt = self.options.pop(statement, None) + if stmt: + return ( + statement.strip() + + " " + + " ".join(option + "=" + str(value) for option, value in stmt.items()) + + ";" + ) + else: + return "" + + def _apply_solver(self): + """ "Prepare the options and run the solver. Then store the data to be returned.""" + logger.debug("Running SAS") + + # Set return code to issue an error if we get interrupted + self._rc = -1 + + # Figure out if the problem has integer variables + with_opt = self.options.pop("with", None) + if with_opt == "lp": + proc = "OPTLP" + elif with_opt == "milp": + proc = "OPTMILP" + else: + # Check if there are integer variables, this might be slow + proc = "OPTMILP" if self._has_integer_variables() else "OPTLP" + + # Remove CAS options in case they were specified + for opt in CAS_OPTION_NAMES: + self.options.pop(opt, None) + + # Get the rootnode options + decomp_str = self._create_statement_str("decomp") + decompmaster_str = self._create_statement_str("decompmaster") + decompmasterip_str = self._create_statement_str("decompmasterip") + decompsubprob_str = self._create_statement_str("decompsubprob") + rootnode_str = self._create_statement_str("rootnode") + + # Handle warmstart + warmstart_str = "" + if self.warmstart_flag: + # Set the warmstart basis option + if proc != "OPTLP": + warmstart_str = """ + proc import datafile='{primalin}' + out=primalin + dbms=csv + replace; + getnames=yes; + run; + """.format( + primalin=self._warm_start_file_name + ) + self.options["primalin"] = "primalin" + + # Convert options to string + opt_str = " ".join( + option + "=" + str(value) for option, value in self.options.items() + ) + + # Start a SAS session, submit the code and return the results`` + with self._sas.SASsession() as sas: + # Find the version of 9.4 we are using + if sas.sasver.startswith("9.04.01M5"): + # In 9.4M5 we have to create an MPS data set from an MPS file first + # Earlier versions will not work because the MPS format in incompatible + res = sas.submit( + """ + {warmstart} + %MPS2SASD(MPSFILE="{mpsfile}", OUTDATA=mpsdata, MAXLEN=256, FORMAT=FREE); + proc {proc} data=mpsdata {options} primalout=primalout dualout=dualout; + {decomp} + {decompmaster} + {decompmasterip} + {decompsubprob} + {rootnode} + run; + """.format( + warmstart=warmstart_str, + proc=proc, + mpsfile=self._problem_files[0], + options=opt_str, + decomp=decomp_str, + decompmaster=decompmaster_str, + decompmasterip=decompmasterip_str, + decompsubprob=decompsubprob_str, + rootnode=rootnode_str, + ), + results="TEXT", + ) + else: + # Since 9.4M6+ optlp/optmilp can read mps files directly + res = sas.submit( + """ + {warmstart} + proc {proc} mpsfile=\"{mpsfile}\" {options} primalout=primalout dualout=dualout; + {decomp} + {decompmaster} + {decompmasterip} + {decompsubprob} + {rootnode} + run; + """.format( + warmstart=warmstart_str, + proc=proc, + mpsfile=self._problem_files[0], + options=opt_str, + decomp=decomp_str, + decompmaster=decompmaster_str, + decompmasterip=decompmasterip_str, + decompsubprob=decompsubprob_str, + rootnode=rootnode_str, + ), + results="TEXT", + ) + + # Store log and ODS output + self._log = res["LOG"] + self._lst = res["LST"] + # Print log if requested by the user + if self._tee: + print(self._log) + if "ERROR 22-322: Syntax error" in self._log: + raise ValueError( + "An option passed to the SAS solver caused a syntax error: {log}".format( + log=self._log + ) + ) + self._macro = dict( + (key.strip(), value.strip()) + for key, value in ( + pair.split("=") for pair in sas.symget("_OR" + proc + "_").split() + ) + ) + primal_out = sas.sd2df("primalout") + dual_out = sas.sd2df("dualout") + + # Prepare the solver results + results = self.results = self._create_results_from_status( + self._macro.get("STATUS", "ERROR"), self._macro.get("SOLUTION_STATUS","ERROR") + ) + + if "Objective Sense Maximization" in self._lst: + results.problem.sense = ProblemSense.maximize + else: + results.problem.sense = ProblemSense.minimize + + # Prepare the solution information + if results.solver.termination_condition == TerminationCondition.optimal: + sol = results.solution.add() + + # Store status in solution + sol.status = SolutionStatus.feasible + sol.termination_condition = TerminationCondition.optimal + + # Store objective value in solution + sol.objective['__default_objective__'] = {'Value': self._macro["OBJECTIVE"]} + + if proc == "OPTLP": + # Convert primal out data set to variable dictionary + # Use panda functions for efficiency + primal_out = primal_out[['_VAR_', '_VALUE_', '_STATUS_', '_R_COST_']] + primal_out = primal_out.set_index('_VAR_', drop=True) + primal_out = primal_out.rename( + {'_VALUE_': 'Value', '_STATUS_': 'Status', '_R_COST_': 'rc'}, + axis='columns', + ) + sol.variable = primal_out.to_dict('index') + + # Convert dual out data set to constraint dictionary + # Use pandas functions for efficiency + dual_out = dual_out[['_ROW_', '_VALUE_', '_STATUS_', '_ACTIVITY_']] + dual_out = dual_out.set_index('_ROW_', drop=True) + dual_out = dual_out.rename( + {'_VALUE_': 'dual', '_STATUS_': 'Status', '_ACTIVITY_': 'slack'}, + axis='columns', + ) + sol.constraint = dual_out.to_dict('index') + else: + # Convert primal out data set to variable dictionary + # Use pandas functions for efficiency + primal_out = primal_out[['_VAR_', '_VALUE_']] + primal_out = primal_out.set_index('_VAR_', drop=True) + primal_out = primal_out.rename({'_VALUE_': 'Value'}, axis='columns') + sol.variable = primal_out.to_dict('index') + + self._rc = 0 + return Bunch(rc=self._rc, log=self._log) + + +class SASLogWriter: + """Helper class to take the log from stdout and put it also in a StringIO.""" + + def __init__(self, tee): + """Set up the two outputs.""" + self.tee = tee + self._log = StringIO() + self.stdout = sys.stdout + + def write(self, message): + """If the tee options is specified, write to both outputs.""" + if self.tee: + self.stdout.write(message) + self._log.write(message) + + def flush(self): + """Nothing to do, just here for compatibility reasons.""" + # Do nothing since we flush right away + pass + + def log(self): + """ "Get the log as a string.""" + return self._log.getvalue() + + +@SolverFactory.register('_sascas', doc='SAS Viya CAS Server interface') +class SASCAS(SASAbc): + """ + Solver interface connection to a SAS Viya CAS server using swat. + See the documentation for the swat package about how to create a connection. + The swat connection options can be passed as options to the solve function. + """ + + def __init__(self, **kwds): + """Initialize and try to load the swat package.""" + super(SASCAS, self).__init__(**kwds) + + try: + import swat + + self._sas = swat + except ImportError: + self._python_api_exists = False + except Exception as e: + self._python_api_exists = False + # For other exceptions, raise it so that it does not get lost + raise e + else: + self._python_api_exists = True + + def _apply_solver(self): + """ "Prepare the options and run the solver. Then store the data to be returned.""" + logger.debug("Running SAS Viya") + + # Set return code to issue an error if we get interrupted + self._rc = -1 + + # Extract CAS connection options + cas_opts = {} + for opt in CAS_OPTION_NAMES: + val = self.options.pop(opt, None) + if val != None: + cas_opts[opt] = val + + # Figure out if the problem has integer variables + with_opt = self.options.pop("with", None) + if with_opt == "lp": + action = "solveLp" + elif with_opt == "milp": + action = "solveMilp" + else: + # Check if there are integer variables, this might be slow + action = "solveMilp" if self._has_integer_variables() else "solveLp" + + # Connect to CAS server + with redirect_stdout(SASLogWriter(self._tee)) as self._log_writer: + s = self._sas.CAS(**cas_opts) + try: + # Load the optimization action set + s.loadactionset('optimization') + + # Upload mps file to CAS + if os.stat(self._problem_files[0]).st_size >= 2 * 1024**3: + # For large files, use convertMPS, first create file for upload + mpsWithIdFileName = TempfileManager.create_tempfile( + ".mps.csv", text=True + ) + with open(mpsWithIdFileName, 'w') as mpsWithId: + mpsWithId.write('_ID_\tText\n') + with open(self._problem_files[0], 'r') as f: + id = 0 + for line in f: + id += 1 + mpsWithId.write(str(id) + '\t' + line.rstrip() + '\n') + + # Upload .mps.csv file + s.upload_file( + mpsWithIdFileName, + casout={"name": "mpscsv", "replace": True}, + importoptions={"filetype": "CSV", "delimiter": "\t"}, + ) + + # Convert .mps.csv file to .mps + s.optimization.convertMps( + data="mpscsv", + casOut={"name": "mpsdata", "replace": True}, + format="FREE", + ) + else: + # For small files, use loadMPS + with open(self._problem_files[0], 'r') as mps_file: + s.optimization.loadMps( + mpsFileString=mps_file.read(), + casout={"name": "mpsdata", "replace": True}, + format="FREE", + ) + + if self.warmstart_flag: + # Upload warmstart file to CAS + s.upload_file( + self._warm_start_file_name, + casout={"name": "primalin", "replace": True}, + importoptions={"filetype": "CSV"}, + ) + self.options["primalin"] = "primalin" + + # Solve the problem in CAS + if action == "solveMilp": + r = s.optimization.solveMilp( + data={"name": "mpsdata"}, + primalOut={"name": "primalout", "replace": True}, + **self.options + ) + else: + r = s.optimization.solveLp( + data={"name": "mpsdata"}, + primalOut={"name": "primalout", "replace": True}, + dualOut={"name": "dualout", "replace": True}, + **self.options + ) + + # Prepare the solver results + if r: + # Get back the primal and dual solution data sets + results = self.results = self._create_results_from_status( + r.get("status", "ERROR"), r.get("solutionStatus", "ERROR") + ) + + if r.ProblemSummary["cValue1"][1] == "Maximization": + results.problem.sense = ProblemSense.maximize + else: + results.problem.sense = ProblemSense.minimize + + # Prepare the solution information + if ( + results.solver.termination_condition + == TerminationCondition.optimal + ): + sol = results.solution.add() + + # Store status in solution + sol.status = SolutionStatus.feasible + sol.termination_condition = TerminationCondition.optimal + + # Store objective value in solution + sol.objective['__default_objective__'] = { + 'Value': r["objective"] + } + + if action == "solveMilp": + primal_out = s.CASTable(name="primalout") + # Use pandas functions for efficiency + primal_out = primal_out[['_VAR_', '_VALUE_']] + sol.variable = {} + for row in primal_out.itertuples(index=False): + sol.variable[row[0]] = {'Value': row[1]} + else: + # Convert primal out data set to variable dictionary + # Use panda functions for efficiency + primal_out = s.CASTable(name="primalout") + primal_out = primal_out[ + ['_VAR_', '_VALUE_', '_STATUS_', '_R_COST_'] + ] + sol.variable = {} + for row in primal_out.itertuples(index=False): + sol.variable[row[0]] = { + 'Value': row[1], + 'Status': row[2], + 'rc': row[3], + } + + # Convert dual out data set to constraint dictionary + # Use pandas functions for efficiency + dual_out = s.CASTable(name="dualout") + dual_out = dual_out[ + ['_ROW_', '_VALUE_', '_STATUS_', '_ACTIVITY_'] + ] + sol.constraint = {} + for row in dual_out.itertuples(index=False): + sol.constraint[row[0]] = { + 'dual': row[1], + 'Status': row[2], + 'slack': row[3], + } + else: + results = self.results = SolverResults() + results.solver.name = "SAS" + results.solver.status = SolverStatus.error + raise ValueError( + "An option passed to the SAS solver caused a syntax error." + ) + + finally: + s.close() + + self._log = self._log_writer.log() + if self._tee: + print(self._log) + self._rc = 0 + return Bunch(rc=self._rc, log=self._log) diff --git a/pyomo/solvers/plugins/solvers/__init__.py b/pyomo/solvers/plugins/solvers/__init__.py index c5fbfa97e42..23b7fe06526 100644 --- a/pyomo/solvers/plugins/solvers/__init__.py +++ b/pyomo/solvers/plugins/solvers/__init__.py @@ -30,3 +30,4 @@ import pyomo.solvers.plugins.solvers.mosek_persistent import pyomo.solvers.plugins.solvers.xpress_direct import pyomo.solvers.plugins.solvers.xpress_persistent +import pyomo.solvers.plugins.solvers.SAS diff --git a/pyomo/solvers/tests/checks/test_SAS.py b/pyomo/solvers/tests/checks/test_SAS.py new file mode 100644 index 00000000000..4592343b17f --- /dev/null +++ b/pyomo/solvers/tests/checks/test_SAS.py @@ -0,0 +1,462 @@ +import os +import pyomo.common.unittest as unittest +from pyomo.environ import ( + ConcreteModel, + Var, + Objective, + Constraint, + NonNegativeIntegers, + NonNegativeReals, + Reals, + Integers, + maximize, + minimize, + Suffix, +) +from pyomo.opt.results import ( + SolverStatus, + TerminationCondition, + ProblemSense, +) +from pyomo.opt import ( + SolverFactory, + check_available_solvers, +) + + +CAS_OPTIONS = { + "hostname": os.environ.get('CAS_SERVER', None), + "port": os.environ.get('CAS_PORT', None), + "authinfo": os.environ.get('CAS_AUTHINFO', None), +} + + +sas_available = check_available_solvers('sas') + + +class SASTestAbc: + solver_io = '_sas94' + base_options = {} + + def setObj(self): + X = self.instance.X + self.instance.Obj = Objective( + expr=2 * X[1] - 3 * X[2] - 4 * X[3], sense=minimize + ) + + def setX(self): + self.instance.X = Var([1, 2, 3], within=NonNegativeReals) + + def setUp(self): + instance = self.instance = ConcreteModel() + self.setX() + X = instance.X + instance.R1 = Constraint(expr=-2 * X[2] - 3 * X[3] >= -5) + instance.R2 = Constraint(expr=X[1] + X[2] + 2 * X[3] <= 4) + instance.R3 = Constraint(expr=X[1] + 2 * X[2] + 3 * X[3] <= 7) + self.setObj() + + # Declare suffixes for solution information + instance.status = Suffix(direction=Suffix.IMPORT) + instance.slack = Suffix(direction=Suffix.IMPORT) + instance.rc = Suffix(direction=Suffix.IMPORT) + instance.dual = Suffix(direction=Suffix.IMPORT) + + self.opt_sas = SolverFactory('sas', solver_io=self.solver_io) + + def tearDown(self): + del self.opt_sas + del self.instance + + def run_solver(self, **kwargs): + opt_sas = self.opt_sas + instance = self.instance + + # Add base options for connection data etc. + options = kwargs.get("options", {}) + if self.base_options: + kwargs["options"] = {**options, **self.base_options} + + # Call the solver + self.results = opt_sas.solve(instance, **kwargs) + + +class SASTestLP(SASTestAbc, unittest.TestCase): + def checkSolution(self): + instance = self.instance + results = self.results + # Get the objective sense, we use the same code for minimization and maximization tests + sense = instance.Obj.sense + + # Check status + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) + + # Check objective value + self.assertAlmostEqual(instance.Obj(), sense * -7.5) + + # Check primal solution values + self.assertAlmostEqual(instance.X[1].value, 0.0) + self.assertAlmostEqual(instance.X[2].value, 2.5) + self.assertAlmostEqual(instance.X[3].value, 0.0) + + # Check reduced cost + self.assertAlmostEqual(instance.rc[instance.X[1]], sense * 2.0) + self.assertAlmostEqual(instance.rc[instance.X[2]], sense * 0.0) + self.assertAlmostEqual(instance.rc[instance.X[3]], sense * 0.5) + + # Check slack + self.assertAlmostEqual(instance.slack[instance.R1], -5.0) + self.assertAlmostEqual(instance.slack[instance.R2], 2.5) + self.assertAlmostEqual(instance.slack[instance.R3], 5.0) + + # Check dual solution + self.assertAlmostEqual(instance.dual[instance.R1], sense * 1.5) + self.assertAlmostEqual(instance.dual[instance.R2], sense * 0.0) + self.assertAlmostEqual(instance.dual[instance.R3], sense * 0.0) + + # Check basis status + self.assertEqual(instance.status[instance.X[1]], 'L') + self.assertEqual(instance.status[instance.X[2]], 'B') + self.assertEqual(instance.status[instance.X[3]], 'L') + self.assertEqual(instance.status[instance.R1], 'U') + self.assertEqual(instance.status[instance.R2], 'B') + self.assertEqual(instance.status[instance.R3], 'B') + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_default(self): + self.run_solver() + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_primal(self): + self.run_solver(options={"algorithm": "ps"}) + self.assertIn("NOTE: The Primal Simplex algorithm is used.", self.opt_sas._log) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_ipm(self): + self.run_solver(options={"algorithm": "ip"}) + self.assertIn("NOTE: The Interior Point algorithm is used.", self.opt_sas._log) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_intoption(self): + self.run_solver(options={"maxiter": 20}) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_invalidoption(self): + with self.assertRaisesRegex(ValueError, "syntax error"): + self.run_solver(options={"foo": "bar"}) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_max(self): + X = self.instance.X + self.instance.Obj.set_value(expr=-2 * X[1] + 3 * X[2] + 4 * X[3]) + self.instance.Obj.sense = maximize + self.run_solver() + self.checkSolution() + self.assertEqual(self.results.problem.sense, ProblemSense.maximize) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_infeasible(self): + instance = self.instance + X = instance.X + instance.R4 = Constraint(expr=-2 * X[2] - 3 * X[3] <= -6) + self.run_solver() + results = self.results + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.infeasible + ) + self.assertEqual(results.solver.message, "The problem is infeasible.") + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_infeasible_or_unbounded(self): + self.instance.X.domain = Reals + self.run_solver() + results = self.results + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, + TerminationCondition.infeasibleOrUnbounded, + ) + self.assertEqual( + results.solver.message, "The problem is infeasible or unbounded." + ) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_unbounded(self): + self.instance.X.domain = Reals + self.run_solver(options={"presolver": "none", "algorithm": "primal"}) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.unbounded + ) + self.assertEqual(results.solver.message, "The problem is unbounded.") + + def checkSolutionDecomp(self): + instance = self.instance + results = self.results + # Get the objective sense, we use the same code for minimization and maximization tests + sense = instance.Obj.sense + + # Check status + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) + + # Check objective value + self.assertAlmostEqual(instance.Obj(), sense * -7.5) + + # Check primal solution values + self.assertAlmostEqual(instance.X[1].value, 0.0) + self.assertAlmostEqual(instance.X[2].value, 2.5) + self.assertAlmostEqual(instance.X[3].value, 0.0) + + # Check reduced cost + self.assertAlmostEqual(instance.rc[instance.X[1]], sense * 2.0) + self.assertAlmostEqual(instance.rc[instance.X[2]], sense * 0.0) + self.assertAlmostEqual(instance.rc[instance.X[3]], sense * 0.5) + + # Check slack + self.assertAlmostEqual(instance.slack[instance.R1], -5.0) + self.assertAlmostEqual(instance.slack[instance.R2], 2.5) + self.assertAlmostEqual(instance.slack[instance.R3], 5.0) + + # Check dual solution + self.assertAlmostEqual(instance.dual[instance.R1], sense * 1.5) + self.assertAlmostEqual(instance.dual[instance.R2], sense * 0.0) + self.assertAlmostEqual(instance.dual[instance.R3], sense * 0.0) + + # Don't check basis status for decomp + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_decomp(self): + self.run_solver( + options={ + "decomp": {"absobjgap": 0.0}, + "decompmaster": {"algorithm": "dual"}, + "decompsubprob": {"presolver": "none"}, + } + ) + self.assertIn( + "NOTE: The DECOMP method value DEFAULT is applied.", self.opt_sas._log + ) + self.checkSolutionDecomp() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_iis(self): + self.run_solver(options={"iis": "true"}) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.feasible + ) + self.assertIn("NOTE: The IIS= option is enabled.", self.opt_sas._log) + self.assertEqual( + results.solver.message, + "The problem is feasible. This status is displayed when the IIS=TRUE option is specified and the problem is feasible.", + ) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_maxiter(self): + self.run_solver(options={"maxiter": 1}) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.maxIterations + ) + self.assertEqual( + results.solver.message, + "The maximum allowable number of iterations was reached.", + ) + + +class SASTestLPCAS(SASTestLP): + solver_io = '_sascas' + base_options = CAS_OPTIONS + + +class SASTestMILP(SASTestAbc, unittest.TestCase): + def setX(self): + self.instance.X = Var([1, 2, 3], within=NonNegativeIntegers) + + def checkSolution(self): + instance = self.instance + results = self.results + + # Get the objective sense, we use the same code for minimization and maximization tests + sense = instance.Obj.sense + + # Check status + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) + + # Check objective value + self.assertAlmostEqual(instance.Obj(), sense * -7) + + # Check primal solution values + self.assertAlmostEqual(instance.X[1].value, 0.0) + self.assertAlmostEqual(instance.X[2].value, 1.0) + self.assertAlmostEqual(instance.X[3].value, 1.0) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_default(self): + self.run_solver(options={}) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_presolve(self): + self.run_solver(options={"presolver": "none"}) + self.assertIn( + "NOTE: The MILP presolver value NONE is applied.", self.opt_sas._log + ) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_intoption(self): + self.run_solver(options={"maxnodes": 20}) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_invalidoption(self): + with self.assertRaisesRegex(ValueError, "syntax error"): + self.run_solver(options={"foo": "bar"}) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_max(self): + X = self.instance.X + self.instance.Obj.set_value(expr=-2 * X[1] + 3 * X[2] + 4 * X[3]) + self.instance.Obj.sense = maximize + self.run_solver() + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_infeasible(self): + instance = self.instance + X = instance.X + instance.R4 = Constraint(expr=-2 * X[2] - 3 * X[3] <= -6) + self.run_solver() + results = self.results + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.infeasible + ) + self.assertEqual(results.solver.message, "The problem is infeasible.") + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + @unittest.skip("Returns wrong status for some versions.") + def test_solver_infeasible_or_unbounded(self): + self.instance.X.domain = Integers + self.run_solver() + results = self.results + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, + TerminationCondition.infeasibleOrUnbounded, + ) + self.assertEqual( + results.solver.message, "The problem is infeasible or unbounded." + ) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_unbounded(self): + self.instance.X.domain = Integers + self.run_solver( + options={"presolver": "none", "rootnode": {"algorithm": "primal"}} + ) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.unbounded + ) + self.assertEqual(results.solver.message, "The problem is unbounded.") + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_decomp(self): + self.run_solver( + options={ + "decomp": {"hybrid": "off"}, + "decompmaster": {"algorithm": "dual"}, + "decompmasterip": {"presolver": "none"}, + "decompsubprob": {"presolver": "none"}, + } + ) + self.assertIn( + "NOTE: The DECOMP method value DEFAULT is applied.", self.opt_sas._log + ) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_rootnode(self): + self.run_solver(options={"rootnode": {"presolver": "automatic"}}) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_maxnodes(self): + self.run_solver(options={"maxnodes": 0}) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.maxEvaluations + ) + self.assertEqual( + results.solver.message, + "The solver reached the maximum number of nodes specified by the MAXNODES= option and found a solution.", + ) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_maxsols(self): + self.run_solver(options={"maxsols": 1}) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.maxEvaluations + ) + self.assertEqual( + results.solver.message, + "The solver reached the maximum number of solutions specified by the MAXSOLS= option.", + ) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_target(self): + self.run_solver(options={"target": -6.0}) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) + self.assertEqual( + results.solver.message, + "The solution is not worse than the target specified by the TARGET= option.", + ) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_primalin(self): + X = self.instance.X + X[1] = None + X[2] = 3 + X[3] = 7 + self.run_solver(warmstart=True) + self.checkSolution() + self.assertIn( + "NOTE: The input solution is infeasible or incomplete. Repair heuristics are applied.", + self.opt_sas._log, + ) + + +class SASTestMILPCAS(SASTestMILP): + solver_io = '_sascas' + base_options = CAS_OPTIONS + + +if __name__ == '__main__': + unittest.main() From be423b99adfa97a6cc3d3cb20985398891acecbe Mon Sep 17 00:00:00 2001 From: "Philipp Christophel (phchri)" Date: Fri, 23 Jun 2023 09:56:54 -0400 Subject: [PATCH 0005/3044] Just some black adjustments --- pyomo/solvers/plugins/solvers/SAS.py | 3 ++- pyomo/solvers/tests/checks/test_SAS.py | 11 ++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/SAS.py b/pyomo/solvers/plugins/solvers/SAS.py index 7f50b7a2970..ed0e63d44d6 100644 --- a/pyomo/solvers/plugins/solvers/SAS.py +++ b/pyomo/solvers/plugins/solvers/SAS.py @@ -427,7 +427,8 @@ def _apply_solver(self): # Prepare the solver results results = self.results = self._create_results_from_status( - self._macro.get("STATUS", "ERROR"), self._macro.get("SOLUTION_STATUS","ERROR") + self._macro.get("STATUS", "ERROR"), + self._macro.get("SOLUTION_STATUS", "ERROR"), ) if "Objective Sense Maximization" in self._lst: diff --git a/pyomo/solvers/tests/checks/test_SAS.py b/pyomo/solvers/tests/checks/test_SAS.py index 4592343b17f..654820f5060 100644 --- a/pyomo/solvers/tests/checks/test_SAS.py +++ b/pyomo/solvers/tests/checks/test_SAS.py @@ -13,15 +13,8 @@ minimize, Suffix, ) -from pyomo.opt.results import ( - SolverStatus, - TerminationCondition, - ProblemSense, -) -from pyomo.opt import ( - SolverFactory, - check_available_solvers, -) +from pyomo.opt.results import SolverStatus, TerminationCondition, ProblemSense +from pyomo.opt import SolverFactory, check_available_solvers CAS_OPTIONS = { From 284ab98e470cf67c5273837a28c11afbb2b153a5 Mon Sep 17 00:00:00 2001 From: "Philipp Christophel (phchri)" Date: Fri, 23 Jun 2023 09:42:08 -0400 Subject: [PATCH 0006/3044] Initial version of the SAS solver interfaces and unit tests. --- pyomo/solvers/plugins/solvers/SAS.py | 700 ++++++++++++++++++++++ pyomo/solvers/plugins/solvers/__init__.py | 1 + pyomo/solvers/tests/checks/test_SAS.py | 462 ++++++++++++++ 3 files changed, 1163 insertions(+) create mode 100644 pyomo/solvers/plugins/solvers/SAS.py create mode 100644 pyomo/solvers/tests/checks/test_SAS.py diff --git a/pyomo/solvers/plugins/solvers/SAS.py b/pyomo/solvers/plugins/solvers/SAS.py new file mode 100644 index 00000000000..7f50b7a2970 --- /dev/null +++ b/pyomo/solvers/plugins/solvers/SAS.py @@ -0,0 +1,700 @@ +__all__ = ['SAS'] + +import logging +import sys +import os + +from io import StringIO +from abc import ABC, abstractmethod +from contextlib import redirect_stdout + +from pyomo.opt.base import ProblemFormat, ResultsFormat, OptSolver +from pyomo.opt.base.solvers import SolverFactory +from pyomo.common.collections import Bunch +from pyomo.opt.results import ( + SolverResults, + SolverStatus, + TerminationCondition, + SolutionStatus, + ProblemSense, +) +from pyomo.common.tempfiles import TempfileManager +from pyomo.core.base import Var +from pyomo.core.base.block import _BlockData +from pyomo.core.kernel.block import IBlock + + +logger = logging.getLogger('pyomo.solvers') + + +STATUS_TO_SOLVERSTATUS = { + "OK": SolverStatus.ok, + "SYNTAX_ERROR": SolverStatus.error, + "DATA_ERROR": SolverStatus.error, + "OUT_OF_MEMORY": SolverStatus.aborted, + "IO_ERROR": SolverStatus.error, + "ERROR": SolverStatus.error, +} + +# This combines all status codes from OPTLP/solvelp and OPTMILP/solvemilp +SOLSTATUS_TO_TERMINATIONCOND = { + "OPTIMAL": TerminationCondition.optimal, + "OPTIMAL_AGAP": TerminationCondition.optimal, + "OPTIMAL_RGAP": TerminationCondition.optimal, + "OPTIMAL_COND": TerminationCondition.optimal, + "TARGET": TerminationCondition.optimal, + "CONDITIONAL_OPTIMAL": TerminationCondition.optimal, + "FEASIBLE": TerminationCondition.feasible, + "INFEASIBLE": TerminationCondition.infeasible, + "UNBOUNDED": TerminationCondition.unbounded, + "INFEASIBLE_OR_UNBOUNDED": TerminationCondition.infeasibleOrUnbounded, + "SOLUTION_LIM": TerminationCondition.maxEvaluations, + "NODE_LIM_SOL": TerminationCondition.maxEvaluations, + "NODE_LIM_NOSOL": TerminationCondition.maxEvaluations, + "ITERATION_LIMIT_REACHED": TerminationCondition.maxIterations, + "TIME_LIM_SOL": TerminationCondition.maxTimeLimit, + "TIME_LIM_NOSOL": TerminationCondition.maxTimeLimit, + "TIME_LIMIT_REACHED": TerminationCondition.maxTimeLimit, + "ABORTED": TerminationCondition.userInterrupt, + "ABORT_SOL": TerminationCondition.userInterrupt, + "ABORT_NOSOL": TerminationCondition.userInterrupt, + "OUTMEM_SOL": TerminationCondition.solverFailure, + "OUTMEM_NOSOL": TerminationCondition.solverFailure, + "FAILED": TerminationCondition.solverFailure, + "FAIL_SOL": TerminationCondition.solverFailure, + "FAIL_NOSOL": TerminationCondition.solverFailure, +} + + +SOLSTATUS_TO_MESSAGE = { + "OPTIMAL": "The solution is optimal.", + "OPTIMAL_AGAP": "The solution is optimal within the absolute gap specified by the ABSOBJGAP= option.", + "OPTIMAL_RGAP": "The solution is optimal within the relative gap specified by the RELOBJGAP= option.", + "OPTIMAL_COND": "The solution is optimal, but some infeasibilities (primal, bound, or integer) exceed tolerances due to scaling or choice of a small INTTOL= value.", + "TARGET": "The solution is not worse than the target specified by the TARGET= option.", + "CONDITIONAL_OPTIMAL": "The solution is optimal, but some infeasibilities (primal, dual or bound) exceed tolerances due to scaling or preprocessing.", + "FEASIBLE": "The problem is feasible. This status is displayed when the IIS=TRUE option is specified and the problem is feasible.", + "INFEASIBLE": "The problem is infeasible.", + "UNBOUNDED": "The problem is unbounded.", + "INFEASIBLE_OR_UNBOUNDED": "The problem is infeasible or unbounded.", + "SOLUTION_LIM": "The solver reached the maximum number of solutions specified by the MAXSOLS= option.", + "NODE_LIM_SOL": "The solver reached the maximum number of nodes specified by the MAXNODES= option and found a solution.", + "NODE_LIM_NOSOL": "The solver reached the maximum number of nodes specified by the MAXNODES= option and did not find a solution.", + "ITERATION_LIMIT_REACHED": "The maximum allowable number of iterations was reached.", + "TIME_LIM_SOL": "The solver reached the execution time limit specified by the MAXTIME= option and found a solution.", + "TIME_LIM_NOSOL": "The solver reached the execution time limit specified by the MAXTIME= option and did not find a solution.", + "TIME_LIMIT_REACHED": "The solver reached its execution time limit.", + "ABORTED": "The solver was interrupted externally.", + "ABORT_SOL": "The solver was stopped by the user but still found a solution.", + "ABORT_NOSOL": "The solver was stopped by the user and did not find a solution.", + "OUTMEM_SOL": "The solver ran out of memory but still found a solution.", + "OUTMEM_NOSOL": "The solver ran out of memory and either did not find a solution or failed to output the solution due to insufficient memory.", + "FAILED": "The solver failed to converge, possibly due to numerical issues.", + "FAIL_SOL": "The solver stopped due to errors but still found a solution.", + "FAIL_NOSOL": "The solver stopped due to errors and did not find a solution.", +} + + +CAS_OPTION_NAMES = [ + "hostname", + "port", + "username", + "password", + "session", + "locale", + "name", + "nworkers", + "authinfo", + "protocol", + "path", + "ssl_ca_list", + "authcode", +] + + +@SolverFactory.register('sas', doc='The SAS LP/MIP solver') +class SAS(OptSolver): + """The SAS optimization solver""" + + def __new__(cls, *args, **kwds): + mode = kwds.pop('solver_io', None) + if mode != None: + return SolverFactory(mode) + else: + # Choose solver factory automatically + # bassed on what can be loaded. + s = SolverFactory('_sas94', **kwds) + if not s.available(): + s = SolverFactory('_sascas', **kwds) + return s + + +class SASAbc(ABC, OptSolver): + """Abstract base class for the SAS solver interfaces. Simply to avoid code duplication.""" + + def __init__(self, **kwds): + """Initialize the SAS solver interfaces.""" + kwds['type'] = 'sas' + super(SASAbc, self).__init__(**kwds) + + # + # Set up valid problem formats and valid results for each + # problem format + # + self._valid_problem_formats = [ProblemFormat.mps] + self._valid_result_formats = {ProblemFormat.mps: [ResultsFormat.soln]} + + self._keepfiles = False + self._capabilities.linear = True + self._capabilities.integer = True + + super(SASAbc, self).set_problem_format(ProblemFormat.mps) + + def _presolve(self, *args, **kwds): + """ "Set things up for the actual solve.""" + # create a context in the temporary file manager for + # this plugin - is "pop"ed in the _postsolve method. + TempfileManager.push() + + # Get the warmstart flag + self.warmstart_flag = kwds.pop('warmstart', False) + + # Call parent presolve function + super(SASAbc, self)._presolve(*args, **kwds) + + # Store the model, too bad this is not done in the base class + for arg in args: + if isinstance(arg, (_BlockData, IBlock)): + # Store the instance + self._instance = arg + self._vars = [] + for block in self._instance.block_data_objects(active=True): + for vardata in block.component_data_objects( + Var, active=True, descend_into=False + ): + self._vars.append(vardata) + # Store the symbal map, we need this for example when writing the warmstart file + if isinstance(self._instance, IBlock): + self._smap = getattr(self._instance, "._symbol_maps")[self._smap_id] + else: + self._smap = self._instance.solutions.symbol_map[self._smap_id] + + # Create the primalin data + if self.warmstart_flag: + filename = self._warm_start_file_name = TempfileManager.create_tempfile( + ".sol", text=True + ) + smap = self._smap + numWritten = 0 + with open(filename, 'w') as file: + file.write('_VAR_,_VALUE_\n') + for var in self._vars: + if (var.value is not None) and (id(var) in smap.byObject): + name = smap.byObject[id(var)] + file.write( + "{name},{value}\n".format(name=name, value=var.value) + ) + numWritten += 1 + if numWritten == 0: + # No solution available, disable warmstart + self.warmstart_flag = False + + def available(self, exception_flag=False): + """True if the solver is available""" + return self._python_api_exists + + def _has_integer_variables(self): + """True if the problem has integer variables.""" + for vardata in self._vars: + if vardata.is_binary() or vardata.is_integer(): + return True + return False + + def _create_results_from_status(self, status, solution_status): + """Create a results object and set the status code and messages.""" + results = SolverResults() + results.solver.name = "SAS" + results.solver.status = STATUS_TO_SOLVERSTATUS[status] + if results.solver.status == SolverStatus.ok: + results.solver.termination_condition = SOLSTATUS_TO_TERMINATIONCOND[ + solution_status + ] + results.solver.message = ( + results.solver.termination_message + ) = SOLSTATUS_TO_MESSAGE[solution_status] + results.solver.status = TerminationCondition.to_solver_status( + results.solver.termination_condition + ) + elif results.solver.status == SolverStatus.aborted: + results.solver.termination_condition = TerminationCondition.userInterrupt + results.solver.message = ( + results.solver.termination_message + ) = SOLSTATUS_TO_MESSAGE["ABORTED"] + else: + results.solver.termination_condition = TerminationCondition.error + results.solver.message = ( + results.solver.termination_message + ) = SOLSTATUS_TO_MESSAGE["FAILED"] + return results + + @abstractmethod + def _apply_solver(self): + """The routine that performs the solve""" + raise NotImplemented("This is an abstract function and thus not implemented!") + + def _postsolve(self): + """Clean up at the end, especially the temp files.""" + # Let the base class deal with returning results. + results = super(SASAbc, self)._postsolve() + + # Finally, clean any temporary files registered with the temp file + # manager, created populated *directly* by this plugin. does not + # include, for example, the execution script. but does include + # the warm-start file. + TempfileManager.pop(remove=not self._keepfiles) + + return results + + def warm_start_capable(self): + """True if the solver interface supports MILP warmstarting.""" + return True + + +@SolverFactory.register('_sas94', doc='SAS 9.4 interface') +class SAS94(SASAbc): + """ + Solver interface for SAS 9.4 using saspy. See the saspy documentation about + how to create a connection. + """ + + def __init__(self, **kwds): + """Initialize the solver interface and see if the saspy package is available.""" + super(SAS94, self).__init__(**kwds) + + try: + import saspy + + self._sas = saspy + except ImportError: + self._python_api_exists = False + except Exception as e: + self._python_api_exists = False + # For other exceptions, raise it so that it does not get lost + raise e + else: + self._python_api_exists = True + self._sas.logger.setLevel(logger.level) + + def _create_statement_str(self, statement): + """Helper function to create the strings for the statements of the proc OPTLP/OPTMILP code.""" + stmt = self.options.pop(statement, None) + if stmt: + return ( + statement.strip() + + " " + + " ".join(option + "=" + str(value) for option, value in stmt.items()) + + ";" + ) + else: + return "" + + def _apply_solver(self): + """ "Prepare the options and run the solver. Then store the data to be returned.""" + logger.debug("Running SAS") + + # Set return code to issue an error if we get interrupted + self._rc = -1 + + # Figure out if the problem has integer variables + with_opt = self.options.pop("with", None) + if with_opt == "lp": + proc = "OPTLP" + elif with_opt == "milp": + proc = "OPTMILP" + else: + # Check if there are integer variables, this might be slow + proc = "OPTMILP" if self._has_integer_variables() else "OPTLP" + + # Remove CAS options in case they were specified + for opt in CAS_OPTION_NAMES: + self.options.pop(opt, None) + + # Get the rootnode options + decomp_str = self._create_statement_str("decomp") + decompmaster_str = self._create_statement_str("decompmaster") + decompmasterip_str = self._create_statement_str("decompmasterip") + decompsubprob_str = self._create_statement_str("decompsubprob") + rootnode_str = self._create_statement_str("rootnode") + + # Handle warmstart + warmstart_str = "" + if self.warmstart_flag: + # Set the warmstart basis option + if proc != "OPTLP": + warmstart_str = """ + proc import datafile='{primalin}' + out=primalin + dbms=csv + replace; + getnames=yes; + run; + """.format( + primalin=self._warm_start_file_name + ) + self.options["primalin"] = "primalin" + + # Convert options to string + opt_str = " ".join( + option + "=" + str(value) for option, value in self.options.items() + ) + + # Start a SAS session, submit the code and return the results`` + with self._sas.SASsession() as sas: + # Find the version of 9.4 we are using + if sas.sasver.startswith("9.04.01M5"): + # In 9.4M5 we have to create an MPS data set from an MPS file first + # Earlier versions will not work because the MPS format in incompatible + res = sas.submit( + """ + {warmstart} + %MPS2SASD(MPSFILE="{mpsfile}", OUTDATA=mpsdata, MAXLEN=256, FORMAT=FREE); + proc {proc} data=mpsdata {options} primalout=primalout dualout=dualout; + {decomp} + {decompmaster} + {decompmasterip} + {decompsubprob} + {rootnode} + run; + """.format( + warmstart=warmstart_str, + proc=proc, + mpsfile=self._problem_files[0], + options=opt_str, + decomp=decomp_str, + decompmaster=decompmaster_str, + decompmasterip=decompmasterip_str, + decompsubprob=decompsubprob_str, + rootnode=rootnode_str, + ), + results="TEXT", + ) + else: + # Since 9.4M6+ optlp/optmilp can read mps files directly + res = sas.submit( + """ + {warmstart} + proc {proc} mpsfile=\"{mpsfile}\" {options} primalout=primalout dualout=dualout; + {decomp} + {decompmaster} + {decompmasterip} + {decompsubprob} + {rootnode} + run; + """.format( + warmstart=warmstart_str, + proc=proc, + mpsfile=self._problem_files[0], + options=opt_str, + decomp=decomp_str, + decompmaster=decompmaster_str, + decompmasterip=decompmasterip_str, + decompsubprob=decompsubprob_str, + rootnode=rootnode_str, + ), + results="TEXT", + ) + + # Store log and ODS output + self._log = res["LOG"] + self._lst = res["LST"] + # Print log if requested by the user + if self._tee: + print(self._log) + if "ERROR 22-322: Syntax error" in self._log: + raise ValueError( + "An option passed to the SAS solver caused a syntax error: {log}".format( + log=self._log + ) + ) + self._macro = dict( + (key.strip(), value.strip()) + for key, value in ( + pair.split("=") for pair in sas.symget("_OR" + proc + "_").split() + ) + ) + primal_out = sas.sd2df("primalout") + dual_out = sas.sd2df("dualout") + + # Prepare the solver results + results = self.results = self._create_results_from_status( + self._macro.get("STATUS", "ERROR"), self._macro.get("SOLUTION_STATUS","ERROR") + ) + + if "Objective Sense Maximization" in self._lst: + results.problem.sense = ProblemSense.maximize + else: + results.problem.sense = ProblemSense.minimize + + # Prepare the solution information + if results.solver.termination_condition == TerminationCondition.optimal: + sol = results.solution.add() + + # Store status in solution + sol.status = SolutionStatus.feasible + sol.termination_condition = TerminationCondition.optimal + + # Store objective value in solution + sol.objective['__default_objective__'] = {'Value': self._macro["OBJECTIVE"]} + + if proc == "OPTLP": + # Convert primal out data set to variable dictionary + # Use panda functions for efficiency + primal_out = primal_out[['_VAR_', '_VALUE_', '_STATUS_', '_R_COST_']] + primal_out = primal_out.set_index('_VAR_', drop=True) + primal_out = primal_out.rename( + {'_VALUE_': 'Value', '_STATUS_': 'Status', '_R_COST_': 'rc'}, + axis='columns', + ) + sol.variable = primal_out.to_dict('index') + + # Convert dual out data set to constraint dictionary + # Use pandas functions for efficiency + dual_out = dual_out[['_ROW_', '_VALUE_', '_STATUS_', '_ACTIVITY_']] + dual_out = dual_out.set_index('_ROW_', drop=True) + dual_out = dual_out.rename( + {'_VALUE_': 'dual', '_STATUS_': 'Status', '_ACTIVITY_': 'slack'}, + axis='columns', + ) + sol.constraint = dual_out.to_dict('index') + else: + # Convert primal out data set to variable dictionary + # Use pandas functions for efficiency + primal_out = primal_out[['_VAR_', '_VALUE_']] + primal_out = primal_out.set_index('_VAR_', drop=True) + primal_out = primal_out.rename({'_VALUE_': 'Value'}, axis='columns') + sol.variable = primal_out.to_dict('index') + + self._rc = 0 + return Bunch(rc=self._rc, log=self._log) + + +class SASLogWriter: + """Helper class to take the log from stdout and put it also in a StringIO.""" + + def __init__(self, tee): + """Set up the two outputs.""" + self.tee = tee + self._log = StringIO() + self.stdout = sys.stdout + + def write(self, message): + """If the tee options is specified, write to both outputs.""" + if self.tee: + self.stdout.write(message) + self._log.write(message) + + def flush(self): + """Nothing to do, just here for compatibility reasons.""" + # Do nothing since we flush right away + pass + + def log(self): + """ "Get the log as a string.""" + return self._log.getvalue() + + +@SolverFactory.register('_sascas', doc='SAS Viya CAS Server interface') +class SASCAS(SASAbc): + """ + Solver interface connection to a SAS Viya CAS server using swat. + See the documentation for the swat package about how to create a connection. + The swat connection options can be passed as options to the solve function. + """ + + def __init__(self, **kwds): + """Initialize and try to load the swat package.""" + super(SASCAS, self).__init__(**kwds) + + try: + import swat + + self._sas = swat + except ImportError: + self._python_api_exists = False + except Exception as e: + self._python_api_exists = False + # For other exceptions, raise it so that it does not get lost + raise e + else: + self._python_api_exists = True + + def _apply_solver(self): + """ "Prepare the options and run the solver. Then store the data to be returned.""" + logger.debug("Running SAS Viya") + + # Set return code to issue an error if we get interrupted + self._rc = -1 + + # Extract CAS connection options + cas_opts = {} + for opt in CAS_OPTION_NAMES: + val = self.options.pop(opt, None) + if val != None: + cas_opts[opt] = val + + # Figure out if the problem has integer variables + with_opt = self.options.pop("with", None) + if with_opt == "lp": + action = "solveLp" + elif with_opt == "milp": + action = "solveMilp" + else: + # Check if there are integer variables, this might be slow + action = "solveMilp" if self._has_integer_variables() else "solveLp" + + # Connect to CAS server + with redirect_stdout(SASLogWriter(self._tee)) as self._log_writer: + s = self._sas.CAS(**cas_opts) + try: + # Load the optimization action set + s.loadactionset('optimization') + + # Upload mps file to CAS + if os.stat(self._problem_files[0]).st_size >= 2 * 1024**3: + # For large files, use convertMPS, first create file for upload + mpsWithIdFileName = TempfileManager.create_tempfile( + ".mps.csv", text=True + ) + with open(mpsWithIdFileName, 'w') as mpsWithId: + mpsWithId.write('_ID_\tText\n') + with open(self._problem_files[0], 'r') as f: + id = 0 + for line in f: + id += 1 + mpsWithId.write(str(id) + '\t' + line.rstrip() + '\n') + + # Upload .mps.csv file + s.upload_file( + mpsWithIdFileName, + casout={"name": "mpscsv", "replace": True}, + importoptions={"filetype": "CSV", "delimiter": "\t"}, + ) + + # Convert .mps.csv file to .mps + s.optimization.convertMps( + data="mpscsv", + casOut={"name": "mpsdata", "replace": True}, + format="FREE", + ) + else: + # For small files, use loadMPS + with open(self._problem_files[0], 'r') as mps_file: + s.optimization.loadMps( + mpsFileString=mps_file.read(), + casout={"name": "mpsdata", "replace": True}, + format="FREE", + ) + + if self.warmstart_flag: + # Upload warmstart file to CAS + s.upload_file( + self._warm_start_file_name, + casout={"name": "primalin", "replace": True}, + importoptions={"filetype": "CSV"}, + ) + self.options["primalin"] = "primalin" + + # Solve the problem in CAS + if action == "solveMilp": + r = s.optimization.solveMilp( + data={"name": "mpsdata"}, + primalOut={"name": "primalout", "replace": True}, + **self.options + ) + else: + r = s.optimization.solveLp( + data={"name": "mpsdata"}, + primalOut={"name": "primalout", "replace": True}, + dualOut={"name": "dualout", "replace": True}, + **self.options + ) + + # Prepare the solver results + if r: + # Get back the primal and dual solution data sets + results = self.results = self._create_results_from_status( + r.get("status", "ERROR"), r.get("solutionStatus", "ERROR") + ) + + if r.ProblemSummary["cValue1"][1] == "Maximization": + results.problem.sense = ProblemSense.maximize + else: + results.problem.sense = ProblemSense.minimize + + # Prepare the solution information + if ( + results.solver.termination_condition + == TerminationCondition.optimal + ): + sol = results.solution.add() + + # Store status in solution + sol.status = SolutionStatus.feasible + sol.termination_condition = TerminationCondition.optimal + + # Store objective value in solution + sol.objective['__default_objective__'] = { + 'Value': r["objective"] + } + + if action == "solveMilp": + primal_out = s.CASTable(name="primalout") + # Use pandas functions for efficiency + primal_out = primal_out[['_VAR_', '_VALUE_']] + sol.variable = {} + for row in primal_out.itertuples(index=False): + sol.variable[row[0]] = {'Value': row[1]} + else: + # Convert primal out data set to variable dictionary + # Use panda functions for efficiency + primal_out = s.CASTable(name="primalout") + primal_out = primal_out[ + ['_VAR_', '_VALUE_', '_STATUS_', '_R_COST_'] + ] + sol.variable = {} + for row in primal_out.itertuples(index=False): + sol.variable[row[0]] = { + 'Value': row[1], + 'Status': row[2], + 'rc': row[3], + } + + # Convert dual out data set to constraint dictionary + # Use pandas functions for efficiency + dual_out = s.CASTable(name="dualout") + dual_out = dual_out[ + ['_ROW_', '_VALUE_', '_STATUS_', '_ACTIVITY_'] + ] + sol.constraint = {} + for row in dual_out.itertuples(index=False): + sol.constraint[row[0]] = { + 'dual': row[1], + 'Status': row[2], + 'slack': row[3], + } + else: + results = self.results = SolverResults() + results.solver.name = "SAS" + results.solver.status = SolverStatus.error + raise ValueError( + "An option passed to the SAS solver caused a syntax error." + ) + + finally: + s.close() + + self._log = self._log_writer.log() + if self._tee: + print(self._log) + self._rc = 0 + return Bunch(rc=self._rc, log=self._log) diff --git a/pyomo/solvers/plugins/solvers/__init__.py b/pyomo/solvers/plugins/solvers/__init__.py index c5fbfa97e42..23b7fe06526 100644 --- a/pyomo/solvers/plugins/solvers/__init__.py +++ b/pyomo/solvers/plugins/solvers/__init__.py @@ -30,3 +30,4 @@ import pyomo.solvers.plugins.solvers.mosek_persistent import pyomo.solvers.plugins.solvers.xpress_direct import pyomo.solvers.plugins.solvers.xpress_persistent +import pyomo.solvers.plugins.solvers.SAS diff --git a/pyomo/solvers/tests/checks/test_SAS.py b/pyomo/solvers/tests/checks/test_SAS.py new file mode 100644 index 00000000000..4592343b17f --- /dev/null +++ b/pyomo/solvers/tests/checks/test_SAS.py @@ -0,0 +1,462 @@ +import os +import pyomo.common.unittest as unittest +from pyomo.environ import ( + ConcreteModel, + Var, + Objective, + Constraint, + NonNegativeIntegers, + NonNegativeReals, + Reals, + Integers, + maximize, + minimize, + Suffix, +) +from pyomo.opt.results import ( + SolverStatus, + TerminationCondition, + ProblemSense, +) +from pyomo.opt import ( + SolverFactory, + check_available_solvers, +) + + +CAS_OPTIONS = { + "hostname": os.environ.get('CAS_SERVER', None), + "port": os.environ.get('CAS_PORT', None), + "authinfo": os.environ.get('CAS_AUTHINFO', None), +} + + +sas_available = check_available_solvers('sas') + + +class SASTestAbc: + solver_io = '_sas94' + base_options = {} + + def setObj(self): + X = self.instance.X + self.instance.Obj = Objective( + expr=2 * X[1] - 3 * X[2] - 4 * X[3], sense=minimize + ) + + def setX(self): + self.instance.X = Var([1, 2, 3], within=NonNegativeReals) + + def setUp(self): + instance = self.instance = ConcreteModel() + self.setX() + X = instance.X + instance.R1 = Constraint(expr=-2 * X[2] - 3 * X[3] >= -5) + instance.R2 = Constraint(expr=X[1] + X[2] + 2 * X[3] <= 4) + instance.R3 = Constraint(expr=X[1] + 2 * X[2] + 3 * X[3] <= 7) + self.setObj() + + # Declare suffixes for solution information + instance.status = Suffix(direction=Suffix.IMPORT) + instance.slack = Suffix(direction=Suffix.IMPORT) + instance.rc = Suffix(direction=Suffix.IMPORT) + instance.dual = Suffix(direction=Suffix.IMPORT) + + self.opt_sas = SolverFactory('sas', solver_io=self.solver_io) + + def tearDown(self): + del self.opt_sas + del self.instance + + def run_solver(self, **kwargs): + opt_sas = self.opt_sas + instance = self.instance + + # Add base options for connection data etc. + options = kwargs.get("options", {}) + if self.base_options: + kwargs["options"] = {**options, **self.base_options} + + # Call the solver + self.results = opt_sas.solve(instance, **kwargs) + + +class SASTestLP(SASTestAbc, unittest.TestCase): + def checkSolution(self): + instance = self.instance + results = self.results + # Get the objective sense, we use the same code for minimization and maximization tests + sense = instance.Obj.sense + + # Check status + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) + + # Check objective value + self.assertAlmostEqual(instance.Obj(), sense * -7.5) + + # Check primal solution values + self.assertAlmostEqual(instance.X[1].value, 0.0) + self.assertAlmostEqual(instance.X[2].value, 2.5) + self.assertAlmostEqual(instance.X[3].value, 0.0) + + # Check reduced cost + self.assertAlmostEqual(instance.rc[instance.X[1]], sense * 2.0) + self.assertAlmostEqual(instance.rc[instance.X[2]], sense * 0.0) + self.assertAlmostEqual(instance.rc[instance.X[3]], sense * 0.5) + + # Check slack + self.assertAlmostEqual(instance.slack[instance.R1], -5.0) + self.assertAlmostEqual(instance.slack[instance.R2], 2.5) + self.assertAlmostEqual(instance.slack[instance.R3], 5.0) + + # Check dual solution + self.assertAlmostEqual(instance.dual[instance.R1], sense * 1.5) + self.assertAlmostEqual(instance.dual[instance.R2], sense * 0.0) + self.assertAlmostEqual(instance.dual[instance.R3], sense * 0.0) + + # Check basis status + self.assertEqual(instance.status[instance.X[1]], 'L') + self.assertEqual(instance.status[instance.X[2]], 'B') + self.assertEqual(instance.status[instance.X[3]], 'L') + self.assertEqual(instance.status[instance.R1], 'U') + self.assertEqual(instance.status[instance.R2], 'B') + self.assertEqual(instance.status[instance.R3], 'B') + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_default(self): + self.run_solver() + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_primal(self): + self.run_solver(options={"algorithm": "ps"}) + self.assertIn("NOTE: The Primal Simplex algorithm is used.", self.opt_sas._log) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_ipm(self): + self.run_solver(options={"algorithm": "ip"}) + self.assertIn("NOTE: The Interior Point algorithm is used.", self.opt_sas._log) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_intoption(self): + self.run_solver(options={"maxiter": 20}) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_invalidoption(self): + with self.assertRaisesRegex(ValueError, "syntax error"): + self.run_solver(options={"foo": "bar"}) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_max(self): + X = self.instance.X + self.instance.Obj.set_value(expr=-2 * X[1] + 3 * X[2] + 4 * X[3]) + self.instance.Obj.sense = maximize + self.run_solver() + self.checkSolution() + self.assertEqual(self.results.problem.sense, ProblemSense.maximize) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_infeasible(self): + instance = self.instance + X = instance.X + instance.R4 = Constraint(expr=-2 * X[2] - 3 * X[3] <= -6) + self.run_solver() + results = self.results + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.infeasible + ) + self.assertEqual(results.solver.message, "The problem is infeasible.") + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_infeasible_or_unbounded(self): + self.instance.X.domain = Reals + self.run_solver() + results = self.results + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, + TerminationCondition.infeasibleOrUnbounded, + ) + self.assertEqual( + results.solver.message, "The problem is infeasible or unbounded." + ) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_unbounded(self): + self.instance.X.domain = Reals + self.run_solver(options={"presolver": "none", "algorithm": "primal"}) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.unbounded + ) + self.assertEqual(results.solver.message, "The problem is unbounded.") + + def checkSolutionDecomp(self): + instance = self.instance + results = self.results + # Get the objective sense, we use the same code for minimization and maximization tests + sense = instance.Obj.sense + + # Check status + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) + + # Check objective value + self.assertAlmostEqual(instance.Obj(), sense * -7.5) + + # Check primal solution values + self.assertAlmostEqual(instance.X[1].value, 0.0) + self.assertAlmostEqual(instance.X[2].value, 2.5) + self.assertAlmostEqual(instance.X[3].value, 0.0) + + # Check reduced cost + self.assertAlmostEqual(instance.rc[instance.X[1]], sense * 2.0) + self.assertAlmostEqual(instance.rc[instance.X[2]], sense * 0.0) + self.assertAlmostEqual(instance.rc[instance.X[3]], sense * 0.5) + + # Check slack + self.assertAlmostEqual(instance.slack[instance.R1], -5.0) + self.assertAlmostEqual(instance.slack[instance.R2], 2.5) + self.assertAlmostEqual(instance.slack[instance.R3], 5.0) + + # Check dual solution + self.assertAlmostEqual(instance.dual[instance.R1], sense * 1.5) + self.assertAlmostEqual(instance.dual[instance.R2], sense * 0.0) + self.assertAlmostEqual(instance.dual[instance.R3], sense * 0.0) + + # Don't check basis status for decomp + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_decomp(self): + self.run_solver( + options={ + "decomp": {"absobjgap": 0.0}, + "decompmaster": {"algorithm": "dual"}, + "decompsubprob": {"presolver": "none"}, + } + ) + self.assertIn( + "NOTE: The DECOMP method value DEFAULT is applied.", self.opt_sas._log + ) + self.checkSolutionDecomp() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_iis(self): + self.run_solver(options={"iis": "true"}) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.feasible + ) + self.assertIn("NOTE: The IIS= option is enabled.", self.opt_sas._log) + self.assertEqual( + results.solver.message, + "The problem is feasible. This status is displayed when the IIS=TRUE option is specified and the problem is feasible.", + ) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_maxiter(self): + self.run_solver(options={"maxiter": 1}) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.maxIterations + ) + self.assertEqual( + results.solver.message, + "The maximum allowable number of iterations was reached.", + ) + + +class SASTestLPCAS(SASTestLP): + solver_io = '_sascas' + base_options = CAS_OPTIONS + + +class SASTestMILP(SASTestAbc, unittest.TestCase): + def setX(self): + self.instance.X = Var([1, 2, 3], within=NonNegativeIntegers) + + def checkSolution(self): + instance = self.instance + results = self.results + + # Get the objective sense, we use the same code for minimization and maximization tests + sense = instance.Obj.sense + + # Check status + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) + + # Check objective value + self.assertAlmostEqual(instance.Obj(), sense * -7) + + # Check primal solution values + self.assertAlmostEqual(instance.X[1].value, 0.0) + self.assertAlmostEqual(instance.X[2].value, 1.0) + self.assertAlmostEqual(instance.X[3].value, 1.0) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_default(self): + self.run_solver(options={}) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_presolve(self): + self.run_solver(options={"presolver": "none"}) + self.assertIn( + "NOTE: The MILP presolver value NONE is applied.", self.opt_sas._log + ) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_intoption(self): + self.run_solver(options={"maxnodes": 20}) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_invalidoption(self): + with self.assertRaisesRegex(ValueError, "syntax error"): + self.run_solver(options={"foo": "bar"}) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_max(self): + X = self.instance.X + self.instance.Obj.set_value(expr=-2 * X[1] + 3 * X[2] + 4 * X[3]) + self.instance.Obj.sense = maximize + self.run_solver() + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_infeasible(self): + instance = self.instance + X = instance.X + instance.R4 = Constraint(expr=-2 * X[2] - 3 * X[3] <= -6) + self.run_solver() + results = self.results + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.infeasible + ) + self.assertEqual(results.solver.message, "The problem is infeasible.") + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + @unittest.skip("Returns wrong status for some versions.") + def test_solver_infeasible_or_unbounded(self): + self.instance.X.domain = Integers + self.run_solver() + results = self.results + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, + TerminationCondition.infeasibleOrUnbounded, + ) + self.assertEqual( + results.solver.message, "The problem is infeasible or unbounded." + ) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_unbounded(self): + self.instance.X.domain = Integers + self.run_solver( + options={"presolver": "none", "rootnode": {"algorithm": "primal"}} + ) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.warning) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.unbounded + ) + self.assertEqual(results.solver.message, "The problem is unbounded.") + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_decomp(self): + self.run_solver( + options={ + "decomp": {"hybrid": "off"}, + "decompmaster": {"algorithm": "dual"}, + "decompmasterip": {"presolver": "none"}, + "decompsubprob": {"presolver": "none"}, + } + ) + self.assertIn( + "NOTE: The DECOMP method value DEFAULT is applied.", self.opt_sas._log + ) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_rootnode(self): + self.run_solver(options={"rootnode": {"presolver": "automatic"}}) + self.checkSolution() + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_maxnodes(self): + self.run_solver(options={"maxnodes": 0}) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.maxEvaluations + ) + self.assertEqual( + results.solver.message, + "The solver reached the maximum number of nodes specified by the MAXNODES= option and found a solution.", + ) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_maxsols(self): + self.run_solver(options={"maxsols": 1}) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.maxEvaluations + ) + self.assertEqual( + results.solver.message, + "The solver reached the maximum number of solutions specified by the MAXSOLS= option.", + ) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_target(self): + self.run_solver(options={"target": -6.0}) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.ok) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) + self.assertEqual( + results.solver.message, + "The solution is not worse than the target specified by the TARGET= option.", + ) + + @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_primalin(self): + X = self.instance.X + X[1] = None + X[2] = 3 + X[3] = 7 + self.run_solver(warmstart=True) + self.checkSolution() + self.assertIn( + "NOTE: The input solution is infeasible or incomplete. Repair heuristics are applied.", + self.opt_sas._log, + ) + + +class SASTestMILPCAS(SASTestMILP): + solver_io = '_sascas' + base_options = CAS_OPTIONS + + +if __name__ == '__main__': + unittest.main() From bc0e1feb794f553d0e7a859120aeeabd297708e9 Mon Sep 17 00:00:00 2001 From: "Philipp Christophel (phchri)" Date: Fri, 23 Jun 2023 09:56:54 -0400 Subject: [PATCH 0007/3044] Just some black adjustments --- pyomo/solvers/plugins/solvers/SAS.py | 3 ++- pyomo/solvers/tests/checks/test_SAS.py | 11 ++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/SAS.py b/pyomo/solvers/plugins/solvers/SAS.py index 7f50b7a2970..ed0e63d44d6 100644 --- a/pyomo/solvers/plugins/solvers/SAS.py +++ b/pyomo/solvers/plugins/solvers/SAS.py @@ -427,7 +427,8 @@ def _apply_solver(self): # Prepare the solver results results = self.results = self._create_results_from_status( - self._macro.get("STATUS", "ERROR"), self._macro.get("SOLUTION_STATUS","ERROR") + self._macro.get("STATUS", "ERROR"), + self._macro.get("SOLUTION_STATUS", "ERROR"), ) if "Objective Sense Maximization" in self._lst: diff --git a/pyomo/solvers/tests/checks/test_SAS.py b/pyomo/solvers/tests/checks/test_SAS.py index 4592343b17f..654820f5060 100644 --- a/pyomo/solvers/tests/checks/test_SAS.py +++ b/pyomo/solvers/tests/checks/test_SAS.py @@ -13,15 +13,8 @@ minimize, Suffix, ) -from pyomo.opt.results import ( - SolverStatus, - TerminationCondition, - ProblemSense, -) -from pyomo.opt import ( - SolverFactory, - check_available_solvers, -) +from pyomo.opt.results import SolverStatus, TerminationCondition, ProblemSense +from pyomo.opt import SolverFactory, check_available_solvers CAS_OPTIONS = { From 168d7beef2b686a2c10d7abcac78184ac0b4786c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 28 Jul 2023 14:42:14 -0600 Subject: [PATCH 0008/3044] Design discussion: Solver refactor - APPSI review --- pyomo/contrib/appsi/base.py | 167 ++++++++++++++++++++++++++++++------ 1 file changed, 140 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index ca7255d5628..00f8982349c 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -42,14 +42,42 @@ from pyomo.core.expr.numvalue import NumericConstant +# # TerminationCondition + +# We currently have: Termination condition, solver status, and solution status. +# LL: Michael was trying to go for simplicity. All three conditions can be confusing. +# It is likely okay to have termination condition and solver status. + +# ## Open Questions (User Perspective) +# - Did I (the user) get a reasonable answer back from the solver? +# - If the answer is not reasonable, can I figure out why? + +# ## Our Goal +# Solvers normally tell you what they did and hope the users understand that. +# *We* want to try to return that information but also _help_ the user. + +# ## Proposals +# PROPOSAL 1: PyomoCondition and SolverCondition +# - SolverCondition: what the solver said +# - PyomoCondition: what we interpret that the solver said + +# PROPOSAL 2: TerminationCondition contains... +# - Some finite list of conditions +# - Two flags: why did it exit (TerminationCondition)? how do we interpret the result (SolutionStatus)? +# - Replace `optimal` with `normal` or `ok` for the termination flag; `optimal` can be used differently for the solver flag +# - You can use something else like `local`, `global`, `feasible` for solution status + class TerminationCondition(enum.Enum): """ An enumeration for checking the termination condition of solvers """ - unknown = 0 + unknown = 42 """unknown serves as both a default value, and it is used when no other enum member makes sense""" + ok = 0 + """The solver exited with the optimal solution""" + maxTimeLimit = 1 """The solver exited due to a time limit""" @@ -62,46 +90,78 @@ class TerminationCondition(enum.Enum): minStepLength = 4 """The solver exited due to a minimum step length""" - optimal = 5 - """The solver exited with the optimal solution""" - - unbounded = 8 + unbounded = 5 """The solver exited because the problem is unbounded""" - infeasible = 9 + infeasible = 6 """The solver exited because the problem is infeasible""" - infeasibleOrUnbounded = 10 + infeasibleOrUnbounded = 7 """The solver exited because the problem is either infeasible or unbounded""" - error = 11 + error = 8 """The solver exited due to an error""" - interrupted = 12 + interrupted = 9 """The solver exited because it was interrupted""" - licensingProblems = 13 + licensingProblems = 10 """The solver exited due to licensing problems""" -class SolverConfig(ConfigDict): +class SolutionStatus(enum.Enum): + # We may want to not use enum.Enum; we may want to use the flavor that allows sets + noSolution = 0 + locallyOptimal = 1 + globallyOptimal = 2 + feasible = 3 + + +# # SolverConfig + +# The idea here (currently / in theory) is that a call to solve will have a keyword argument `solver_config`: +# ``` +# solve(model, solver_config=...) +# config = self.config(solver_config) +# ``` + +# We have several flavors of options: +# - Solver options +# - Standardized options +# - Wrapper options +# - Interface options +# - potentially... more? + +# ## The Options + +# There are three basic structures: flat, doubly-nested, separate dicts. +# We need to pick between these three structures (and stick with it). + +# **Flat: Clear interface; ambiguous about what goes where; better solve interface.** <- WINNER +# Doubly: More obscure interface; less ambiguity; better programmatic interface. +# SepDicts: Clear delineation; **kwargs becomes confusing (what maps to what?) (NOT HAPPENING) + + +class InterfaceConfig(ConfigDict): """ Attributes ---------- - time_limit: float + time_limit: float - sent to solver Time limit for the solver - stream_solver: bool + stream_solver: bool - wrapper If True, then the solver log goes to stdout - load_solution: bool + load_solution: bool - wrapper If False, then the values of the primal variables will not be loaded into the model - symbolic_solver_labels: bool + symbolic_solver_labels: bool - sent to solver If True, the names given to the solver will reflect the names of the pyomo components. Cannot be changed after set_instance is called. - report_timing: bool + report_timing: bool - wrapper If True, then some timing information will be printed at the end of the solve. + solver_options: ConfigDict or dict + The "raw" solver options to be passed to the solver. """ def __init__( @@ -112,7 +172,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(SolverConfig, self).__init__( + super(InterfaceConfig, self).__init__( description=description, doc=doc, implicit=implicit, @@ -120,20 +180,19 @@ def __init__( visibility=visibility, ) - self.declare('time_limit', ConfigValue(domain=NonNegativeFloat)) self.declare('stream_solver', ConfigValue(domain=bool)) self.declare('load_solution', ConfigValue(domain=bool)) self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) self.declare('report_timing', ConfigValue(domain=bool)) - self.time_limit: Optional[float] = None + self.time_limit: Optional[float] = self.declare('time_limit', ConfigValue(domain=NonNegativeFloat)) self.stream_solver: bool = False self.load_solution: bool = True self.symbolic_solver_labels: bool = False self.report_timing: bool = False -class MIPSolverConfig(SolverConfig): +class MIPSolverConfig(InterfaceConfig): """ Attributes ---------- @@ -167,6 +226,21 @@ def __init__( self.relax_integrality: bool = False +# # SolutionLoaderBase + +# This is an attempt to answer the issue of persistent/non-persistent solution +# loading. This is an attribute of the results object (not the solver). + +# You wouldn't ask the solver to load a solution into a model. You would +# ask the result to load the solution - into the model you solved. +# The results object points to relevant elements; elements do NOT point to +# the results object. + +# Per Michael: This may be a bit clunky; but it works. +# Per Siirola: We may want to rethink `load_vars` and `get_primals`. In particular, +# this is for efficiency - don't create a dictionary you don't need to. And what is +# the client use-case for `get_primals`? + class SolutionLoaderBase(abc.ABC): def load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None @@ -584,6 +658,46 @@ def __init__( self.treat_fixed_vars_as_params: bool = True +# # Solver + +# ## Open Question: What does 'solve' look like? + +# We may want to use the 80/20 rule here - we support 80% of the cases; anything +# fancier than that is going to require "writing code." The 80% would be offerings +# that are supported as part of the `pyomo` script. + +# ## Configs + +# We will likely have two configs for `solve`: standardized config (processes `**kwargs`) +# and implicit ConfigDict with some specialized options. + +# These have to be separated because there is a set that need to be passed +# directly to the solver. The other is Pyomo options / our standardized options +# (a few of which might be passed directly to solver, e.g., time_limit). + +# ## Contained Methods + +# We do not like `symbol_map`; it's keyed towards file-based interfaces. That +# is the `lp` writer; the `nl` writer doesn't need that (and in fact, it's +# obnoxious). The new `nl` writer returns back more meaningful things to the `nl` +# interface. + +# If the writer needs a symbol map, it will return it. But it is _not_ a +# solver thing. So it does not need to continue to exist in the solver interface. + +# All other options are reasonable. + +# ## Other (maybe should be contained) Methods + +# There are other methods in other solvers such as `warmstart`, `sos`; do we +# want to continue to support and/or offer those features? + +# The solver interface is not responsible for telling the client what +# it can do, e.g., `supports_sos2`. This is actually a contract between +# the solver and its writer. + +# End game: we are not supporting a `has_Xcapability` interface (CHECK BOOK). + class Solver(abc.ABC): class Availability(enum.IntEnum): NotFound = 0 @@ -610,7 +724,7 @@ def __str__(self): return self.name @abc.abstractmethod - def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> Results: + def solve(self, model: _BlockData, tee = False, timer: HierarchicalTimer = None, **kwargs) -> Results: """ Solve a Pyomo model. @@ -618,8 +732,12 @@ def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> Results: ---------- model: _BlockData The Pyomo model to be solved + tee: bool + Show solver output in the terminal timer: HierarchicalTimer An option timer for reporting timing + **kwargs + Additional keyword arguments (including solver_options - passthrough options; delivered directly to the solver (with no validation)) Returns ------- @@ -672,17 +790,12 @@ def config(self): Returns ------- - SolverConfig + InterfaceConfig An object for configuring pyomo solve options such as the time limit. These options are mostly independent of the solver. """ pass - @property - @abc.abstractmethod - def symbol_map(self): - pass - def is_persistent(self): """ Returns From 55ee816fc8247750ee049a8c5ddec91aa47f204b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 1 Aug 2023 10:44:30 -0600 Subject: [PATCH 0009/3044] Finish conversion from optimal to ok --- pyomo/contrib/appsi/base.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 00f8982349c..9ab4d5020a7 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -14,7 +14,7 @@ from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint from pyomo.core.base.var import _GeneralVarData, Var from pyomo.core.base.param import _ParamData, Param -from pyomo.core.base.block import _BlockData, Block +from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.collections import ComponentMap from .utils.get_objective import get_objective @@ -36,7 +36,6 @@ ) from pyomo.core.kernel.objective import minimize from pyomo.core.base import SymbolMap -import weakref from .cmodel import cmodel, cmodel_available from pyomo.core.staleflag import StaleFlagManager from pyomo.core.expr.numvalue import NumericConstant @@ -460,7 +459,7 @@ class Results(object): >>> opt = appsi.solvers.Ipopt() >>> opt.config.load_solution = False >>> results = opt.solve(m) #doctest:+SKIP - >>> if results.termination_condition == appsi.base.TerminationCondition.optimal: #doctest:+SKIP + >>> if results.termination_condition == appsi.base.TerminationCondition.ok: #doctest:+SKIP ... print('optimal solution found: ', results.best_feasible_objective) #doctest:+SKIP ... results.solution_loader.load_vars() #doctest:+SKIP ... print('the optimal value of x is ', m.x.value) #doctest:+SKIP @@ -1583,7 +1582,7 @@ def update(self, timer: HierarchicalTimer = None): TerminationCondition.maxIterations: LegacyTerminationCondition.maxIterations, TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, - TerminationCondition.optimal: LegacyTerminationCondition.optimal, + TerminationCondition.ok: LegacyTerminationCondition.optimal, TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, TerminationCondition.infeasible: LegacyTerminationCondition.infeasible, TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, @@ -1599,7 +1598,7 @@ def update(self, timer: HierarchicalTimer = None): TerminationCondition.maxIterations: LegacySolverStatus.aborted, TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, TerminationCondition.minStepLength: LegacySolverStatus.error, - TerminationCondition.optimal: LegacySolverStatus.ok, + TerminationCondition.ok: LegacySolverStatus.ok, TerminationCondition.unbounded: LegacySolverStatus.error, TerminationCondition.infeasible: LegacySolverStatus.error, TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, @@ -1615,7 +1614,7 @@ def update(self, timer: HierarchicalTimer = None): TerminationCondition.maxIterations: LegacySolutionStatus.stoppedByLimit, TerminationCondition.objectiveLimit: LegacySolutionStatus.stoppedByLimit, TerminationCondition.minStepLength: LegacySolutionStatus.error, - TerminationCondition.optimal: LegacySolutionStatus.optimal, + TerminationCondition.ok: LegacySolutionStatus.optimal, TerminationCondition.unbounded: LegacySolutionStatus.unbounded, TerminationCondition.infeasible: LegacySolutionStatus.infeasible, TerminationCondition.infeasibleOrUnbounded: LegacySolutionStatus.unsure, From a30477ce113b4fe9d87c4fd29b797542da14d8fe Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 1 Aug 2023 10:58:15 -0600 Subject: [PATCH 0010/3044] Change call to InterfaceConfig --- pyomo/contrib/appsi/base.py | 6 +++--- pyomo/contrib/appsi/solvers/cbc.py | 4 ++-- pyomo/contrib/appsi/solvers/cplex.py | 4 ++-- pyomo/contrib/appsi/solvers/gurobi.py | 4 ++-- pyomo/contrib/appsi/solvers/highs.py | 4 ++-- pyomo/contrib/appsi/solvers/ipopt.py | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 9ab4d5020a7..630aefbbd29 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -116,7 +116,7 @@ class SolutionStatus(enum.Enum): feasible = 3 -# # SolverConfig +# # InterfaceConfig # The idea here (currently / in theory) is that a call to solve will have a keyword argument `solver_config`: # ``` @@ -191,7 +191,7 @@ def __init__( self.report_timing: bool = False -class MIPSolverConfig(InterfaceConfig): +class MIPInterfaceConfig(InterfaceConfig): """ Attributes ---------- @@ -210,7 +210,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(MIPSolverConfig, self).__init__( + super(MIPInterfaceConfig, self).__init__( description=description, doc=doc, implicit=implicit, diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index b31a96dbf8a..833ef54b2cf 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -4,7 +4,7 @@ PersistentSolver, Results, TerminationCondition, - SolverConfig, + InterfaceConfig, PersistentSolutionLoader, ) from pyomo.contrib.appsi.writers import LPWriter @@ -33,7 +33,7 @@ logger = logging.getLogger(__name__) -class CbcConfig(SolverConfig): +class CbcConfig(InterfaceConfig): def __init__( self, description=None, diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 6c5e281ffac..7b51d6611c2 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -3,7 +3,7 @@ PersistentSolver, Results, TerminationCondition, - MIPSolverConfig, + MIPInterfaceConfig, PersistentSolutionLoader, ) from pyomo.contrib.appsi.writers import LPWriter @@ -29,7 +29,7 @@ logger = logging.getLogger(__name__) -class CplexConfig(MIPSolverConfig): +class CplexConfig(MIPInterfaceConfig): def __init__( self, description=None, diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 2362612e9ee..0d99089fbab 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -23,7 +23,7 @@ PersistentSolver, Results, TerminationCondition, - MIPSolverConfig, + MIPInterfaceConfig, PersistentBase, PersistentSolutionLoader, ) @@ -53,7 +53,7 @@ class DegreeError(PyomoException): pass -class GurobiConfig(MIPSolverConfig): +class GurobiConfig(MIPInterfaceConfig): def __init__( self, description=None, diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 9de5accfb91..63c799b0f61 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -20,7 +20,7 @@ PersistentSolver, Results, TerminationCondition, - MIPSolverConfig, + MIPInterfaceConfig, PersistentBase, PersistentSolutionLoader, ) @@ -38,7 +38,7 @@ class DegreeError(PyomoException): pass -class HighsConfig(MIPSolverConfig): +class HighsConfig(MIPInterfaceConfig): def __init__( self, description=None, diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index fde4c55073d..047ce09a533 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -4,7 +4,7 @@ PersistentSolver, Results, TerminationCondition, - SolverConfig, + InterfaceConfig, PersistentSolutionLoader, ) from pyomo.contrib.appsi.writers import NLWriter @@ -36,7 +36,7 @@ logger = logging.getLogger(__name__) -class IpoptConfig(SolverConfig): +class IpoptConfig(InterfaceConfig): def __init__( self, description=None, From 75cc8c4c654d0c15a7d0920c9b990eece9f99f6f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 1 Aug 2023 11:24:57 -0600 Subject: [PATCH 0011/3044] Change test checks --- pyomo/contrib/appsi/base.py | 15 ++++-- .../contrib/appsi/examples/getting_started.py | 2 +- pyomo/contrib/appsi/solvers/cbc.py | 10 ++-- pyomo/contrib/appsi/solvers/cplex.py | 4 +- pyomo/contrib/appsi/solvers/gurobi.py | 4 +- pyomo/contrib/appsi/solvers/highs.py | 6 +-- pyomo/contrib/appsi/solvers/ipopt.py | 10 ++-- .../solvers/tests/test_gurobi_persistent.py | 2 +- .../solvers/tests/test_persistent_solvers.py | 50 +++++++++---------- 9 files changed, 55 insertions(+), 48 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 630aefbbd29..fe0b3ee2999 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -62,10 +62,11 @@ # PROPOSAL 2: TerminationCondition contains... # - Some finite list of conditions -# - Two flags: why did it exit (TerminationCondition)? how do we interpret the result (SolutionStatus)? +# - Two flags: why did it exit (TerminationCondition)? how do we interpret the result (SolutionStatus)? # - Replace `optimal` with `normal` or `ok` for the termination flag; `optimal` can be used differently for the solver flag # - You can use something else like `local`, `global`, `feasible` for solution status + class TerminationCondition(enum.Enum): """ An enumeration for checking the termination condition of solvers @@ -184,7 +185,9 @@ def __init__( self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) self.declare('report_timing', ConfigValue(domain=bool)) - self.time_limit: Optional[float] = self.declare('time_limit', ConfigValue(domain=NonNegativeFloat)) + self.time_limit: Optional[float] = self.declare( + 'time_limit', ConfigValue(domain=NonNegativeFloat) + ) self.stream_solver: bool = False self.load_solution: bool = True self.symbolic_solver_labels: bool = False @@ -240,6 +243,7 @@ def __init__( # this is for efficiency - don't create a dictionary you don't need to. And what is # the client use-case for `get_primals`? + class SolutionLoaderBase(abc.ABC): def load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None @@ -689,7 +693,7 @@ def __init__( # ## Other (maybe should be contained) Methods # There are other methods in other solvers such as `warmstart`, `sos`; do we -# want to continue to support and/or offer those features? +# want to continue to support and/or offer those features? # The solver interface is not responsible for telling the client what # it can do, e.g., `supports_sos2`. This is actually a contract between @@ -697,6 +701,7 @@ def __init__( # End game: we are not supporting a `has_Xcapability` interface (CHECK BOOK). + class Solver(abc.ABC): class Availability(enum.IntEnum): NotFound = 0 @@ -723,7 +728,9 @@ def __str__(self): return self.name @abc.abstractmethod - def solve(self, model: _BlockData, tee = False, timer: HierarchicalTimer = None, **kwargs) -> Results: + def solve( + self, model: _BlockData, tee=False, timer: HierarchicalTimer = None, **kwargs + ) -> Results: """ Solve a Pyomo model. diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index de22d28e0a4..5cbac7c81e3 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -31,7 +31,7 @@ def main(plot=True, n_points=200): for p_val in p_values: m.p.value = p_val res = opt.solve(m, timer=timer) - assert res.termination_condition == appsi.base.TerminationCondition.optimal + assert res.termination_condition == appsi.base.TerminationCondition.ok obj_values.append(res.best_feasible_objective) opt.load_vars([m.x]) x_values.append(m.x.value) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 833ef54b2cf..641a90c3ae7 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -232,7 +232,7 @@ def _parse_soln(self): termination_line = all_lines[0].lower() obj_val = None if termination_line.startswith('optimal'): - results.termination_condition = TerminationCondition.optimal + results.termination_condition = TerminationCondition.ok obj_val = float(termination_line.split()[-1]) elif 'infeasible' in termination_line: results.termination_condition = TerminationCondition.infeasible @@ -307,7 +307,7 @@ def _parse_soln(self): self._reduced_costs[v_id] = (v, -rc_val) if ( - results.termination_condition == TerminationCondition.optimal + results.termination_condition == TerminationCondition.ok and self.config.load_solution ): for v_id, (v, val) in self._primal_sol.items(): @@ -316,7 +316,7 @@ def _parse_soln(self): results.best_feasible_objective = None else: results.best_feasible_objective = obj_val - elif results.termination_condition == TerminationCondition.optimal: + elif results.termination_condition == TerminationCondition.ok: if self._writer.get_active_objective() is None: results.best_feasible_objective = None else: @@ -451,7 +451,7 @@ def get_duals(self, cons_to_load=None): if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.optimal + != TerminationCondition.ok ): raise RuntimeError( 'Solver does not currently have valid duals. Please ' @@ -469,7 +469,7 @@ def get_reduced_costs( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.optimal + != TerminationCondition.ok ): raise RuntimeError( 'Solver does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 7b51d6611c2..2ea051c58b4 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -284,7 +284,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): status = cpxprob.solution.get_status() if status in [1, 101, 102]: - results.termination_condition = TerminationCondition.optimal + results.termination_condition = TerminationCondition.ok elif status in [2, 40, 118, 133, 134]: results.termination_condition = TerminationCondition.unbounded elif status in [4, 119, 134]: @@ -336,7 +336,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): 'results.best_feasible_objective before loading a solution.' ) else: - if results.termination_condition != TerminationCondition.optimal: + if results.termination_condition != TerminationCondition.ok: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 0d99089fbab..af17a398845 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -874,7 +874,7 @@ def _postsolve(self, timer: HierarchicalTimer): if status == grb.LOADED: # problem is loaded, but no solution results.termination_condition = TerminationCondition.unknown elif status == grb.OPTIMAL: # optimal - results.termination_condition = TerminationCondition.optimal + results.termination_condition = TerminationCondition.ok elif status == grb.INFEASIBLE: results.termination_condition = TerminationCondition.infeasible elif status == grb.INF_OR_UNBD: @@ -925,7 +925,7 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start('load solution') if config.load_solution: if gprob.SolCount > 0: - if results.termination_condition != TerminationCondition.optimal: + if results.termination_condition != TerminationCondition.ok: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 63c799b0f61..8de873635a5 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -610,7 +610,7 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == highspy.HighsModelStatus.kModelEmpty: results.termination_condition = TerminationCondition.unknown elif status == highspy.HighsModelStatus.kOptimal: - results.termination_condition = TerminationCondition.optimal + results.termination_condition = TerminationCondition.ok elif status == highspy.HighsModelStatus.kInfeasible: results.termination_condition = TerminationCondition.infeasible elif status == highspy.HighsModelStatus.kUnboundedOrInfeasible: @@ -633,7 +633,7 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start('load solution') self._sol = highs.getSolution() has_feasible_solution = False - if results.termination_condition == TerminationCondition.optimal: + if results.termination_condition == TerminationCondition.ok: has_feasible_solution = True elif results.termination_condition in { TerminationCondition.objectiveLimit, @@ -645,7 +645,7 @@ def _postsolve(self, timer: HierarchicalTimer): if config.load_solution: if has_feasible_solution: - if results.termination_condition != TerminationCondition.optimal: + if results.termination_condition != TerminationCondition.ok: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 047ce09a533..2d8cfe40b32 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -303,7 +303,7 @@ def _parse_sol(self): termination_line = all_lines[1] if 'Optimal Solution Found' in termination_line: - results.termination_condition = TerminationCondition.optimal + results.termination_condition = TerminationCondition.ok elif 'Problem may be infeasible' in termination_line: results.termination_condition = TerminationCondition.infeasible elif 'problem might be unbounded' in termination_line: @@ -384,7 +384,7 @@ def _parse_sol(self): self._reduced_costs[var] = 0 if ( - results.termination_condition == TerminationCondition.optimal + results.termination_condition == TerminationCondition.ok and self.config.load_solution ): for v, val in self._primal_sol.items(): @@ -395,7 +395,7 @@ def _parse_sol(self): results.best_feasible_objective = value( self._writer.get_active_objective().expr ) - elif results.termination_condition == TerminationCondition.optimal: + elif results.termination_condition == TerminationCondition.ok: if self._writer.get_active_objective() is None: results.best_feasible_objective = None else: @@ -526,7 +526,7 @@ def get_duals( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.optimal + != TerminationCondition.ok ): raise RuntimeError( 'Solver does not currently have valid duals. Please ' @@ -544,7 +544,7 @@ def get_reduced_costs( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.optimal + != TerminationCondition.ok ): raise RuntimeError( 'Solver does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index 6366077642d..03042bbb5f4 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -160,7 +160,7 @@ def test_lp(self): res = opt.solve(self.m) self.assertAlmostEqual(x + y, res.best_feasible_objective) self.assertAlmostEqual(x + y, res.best_objective_bound) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertTrue(res.best_feasible_objective is not None) self.assertAlmostEqual(x, self.m.x.value) self.assertAlmostEqual(y, self.m.y.value) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index bafccb3527c..88a278bfe3b 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -122,13 +122,13 @@ def test_range_constraint(self, name: str, opt_class: Type[PersistentSolver]): m.obj = pe.Objective(expr=m.x) m.c = pe.Constraint(expr=(-1, m.x, 1)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, -1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, 1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) @@ -143,7 +143,7 @@ def test_reduced_costs(self, name: str, opt_class: Type[PersistentSolver]): m.y = pe.Var(bounds=(-2, 2)) m.obj = pe.Objective(expr=3 * m.x + 4 * m.y) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, -2) rc = opt.get_reduced_costs() @@ -159,13 +159,13 @@ def test_reduced_costs2(self, name: str, opt_class: Type[PersistentSolver]): m.x = pe.Var(bounds=(-1, 1)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, -1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, 1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) @@ -193,7 +193,7 @@ def test_param_changes(self, name: str, opt_class: Type[PersistentSolver]): m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -229,7 +229,7 @@ def test_immutable_param(self, name: str, opt_class: Type[PersistentSolver]): m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -261,7 +261,7 @@ def test_equality(self, name: str, opt_class: Type[PersistentSolver]): m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -299,7 +299,7 @@ def test_linear_expression(self, name: str, opt_class: Type[PersistentSolver]): m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) @@ -327,7 +327,7 @@ def test_no_objective(self, name: str, opt_class: Type[PersistentSolver]): m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertEqual(res.best_feasible_objective, None) @@ -354,7 +354,7 @@ def test_add_remove_cons(self, name: str, opt_class: Type[PersistentSolver]): m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -365,7 +365,7 @@ def test_add_remove_cons(self, name: str, opt_class: Type[PersistentSolver]): m.c3 = pe.Constraint(expr=m.y >= a3 * m.x + b3) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b3 - b1) / (a1 - a3)) self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -377,7 +377,7 @@ def test_add_remove_cons(self, name: str, opt_class: Type[PersistentSolver]): del m.c3 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -401,7 +401,7 @@ def test_results_infeasible(self, name: str, opt_class: Type[PersistentSolver]): res = opt.solve(m) opt.config.load_solution = False res = opt.solve(m) - self.assertNotEqual(res.termination_condition, TerminationCondition.optimal) + self.assertNotEqual(res.termination_condition, TerminationCondition.ok) if opt_class is Ipopt: acceptable_termination_conditions = { TerminationCondition.infeasible, @@ -685,7 +685,7 @@ def test_mutable_param_with_range( m.c2.value = float(c2) m.obj.sense = sense res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) if sense is pe.minimize: self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) @@ -720,7 +720,7 @@ def test_add_and_remove_vars(self, name: str, opt_class: Type[PersistentSolver]) opt.update_config.check_for_new_or_removed_vars = False opt.config.load_solution = False res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) opt.load_vars() self.assertAlmostEqual(m.y.value, -1) m.x = pe.Var() @@ -733,7 +733,7 @@ def test_add_and_remove_vars(self, name: str, opt_class: Type[PersistentSolver]) opt.add_variables([m.x]) opt.add_constraints([m.c1, m.c2]) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) opt.load_vars() self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @@ -741,7 +741,7 @@ def test_add_and_remove_vars(self, name: str, opt_class: Type[PersistentSolver]) opt.remove_variables([m.x]) m.x.value = None res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) opt.load_vars() self.assertEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, -1) @@ -800,7 +800,7 @@ def test_with_numpy(self, name: str, opt_class: Type[PersistentSolver]): ) ) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @@ -1126,14 +1126,14 @@ def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]) m.b.c2 = pe.Constraint(expr=m.y >= -m.x) res = opt.solve(m.b) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(res.best_feasible_objective, 1) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, 1) m.x.setlb(0) res = opt.solve(m.b) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(res.best_feasible_objective, 2) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 2) @@ -1156,7 +1156,7 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolver] m.c4 = pe.Constraint(expr=m.y >= -m.z + 1) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(res.best_feasible_objective, 1) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) @@ -1166,7 +1166,7 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolver] del m.c3 del m.c4 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(res.best_feasible_objective, 0) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) @@ -1188,12 +1188,12 @@ def test_bug_1(self, name: str, opt_class: Type[PersistentSolver]): m.c = pe.Constraint(expr=m.y >= m.p * m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(res.best_feasible_objective, 0) m.p.value = 1 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(res.best_feasible_objective, 3) From e4b9313f417d177125b66a9ff0576ac0a70e334a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 1 Aug 2023 13:59:12 -0600 Subject: [PATCH 0012/3044] Update docs --- pyomo/contrib/appsi/base.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index fe0b3ee2999..e4e6a915cbd 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -110,10 +110,22 @@ class TerminationCondition(enum.Enum): class SolutionStatus(enum.Enum): - # We may want to not use enum.Enum; we may want to use the flavor that allows sets + """ + An enumeration for interpreting the result of a termination + + TODO: We may want to not use enum.Enum; we may want to use the flavor that allows sets + """ + + """No solution found""" noSolution = 0 + + """Locally optimal solution identified""" locallyOptimal = 1 + + """Globally optimal solution identified""" globallyOptimal = 2 + + """Feasible solution identified""" feasible = 3 @@ -160,8 +172,6 @@ class InterfaceConfig(ConfigDict): report_timing: bool - wrapper If True, then some timing information will be printed at the end of the solve. - solver_options: ConfigDict or dict - The "raw" solver options to be passed to the solver. """ def __init__( From cda764d85ff7b37d72f8206c599f1592a0b477de Mon Sep 17 00:00:00 2001 From: "Philipp Christophel (phchri)" Date: Mon, 7 Aug 2023 09:44:00 -0400 Subject: [PATCH 0013/3044] Updated version for remote testing --- .github/workflows/typos.toml | 2 + .gitignore | 5 +- pyomo/solvers/plugins/solvers/SAS.py | 257 +++++++++++++++++-------- pyomo/solvers/tests/checks/test_SAS.py | 183 ++++++++++++------ 4 files changed, 307 insertions(+), 140 deletions(-) diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index c9fe9e804a2..71d9ad0355f 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -38,4 +38,6 @@ caf = "caf" WRONLY = "WRONLY" # Ignore the name Hax Hax = "Hax" +# Ignore dout (short for dual output in SAS solvers) +dout = "dout" # AS NEEDED: Add More Words Below diff --git a/.gitignore b/.gitignore index 09069552990..7309ff1e8a8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ .spyder* .ropeproject .vscode +.env +venv +sascfg_personal.py # Python generates numerous files when byte compiling / installing packages *.pyx *.pyc @@ -24,4 +27,4 @@ gurobi.log # Jupyterhub/Jupyterlab checkpoints .ipynb_checkpoints -cplex.log \ No newline at end of file +cplex.log diff --git a/pyomo/solvers/plugins/solvers/SAS.py b/pyomo/solvers/plugins/solvers/SAS.py index ed0e63d44d6..87a6a18c1f4 100644 --- a/pyomo/solvers/plugins/solvers/SAS.py +++ b/pyomo/solvers/plugins/solvers/SAS.py @@ -1,8 +1,9 @@ -__all__ = ['SAS'] +__all__ = ["SAS"] import logging import sys -import os +from os import stat +import uuid from io import StringIO from abc import ABC, abstractmethod @@ -24,7 +25,7 @@ from pyomo.core.kernel.block import IBlock -logger = logging.getLogger('pyomo.solvers') +logger = logging.getLogger("pyomo.solvers") STATUS_TO_SOLVERSTATUS = { @@ -112,20 +113,20 @@ ] -@SolverFactory.register('sas', doc='The SAS LP/MIP solver') +@SolverFactory.register("sas", doc="The SAS LP/MIP solver") class SAS(OptSolver): """The SAS optimization solver""" def __new__(cls, *args, **kwds): - mode = kwds.pop('solver_io', None) + mode = kwds.pop("solver_io", None) if mode != None: return SolverFactory(mode) else: # Choose solver factory automatically - # bassed on what can be loaded. - s = SolverFactory('_sas94', **kwds) + # based on what can be loaded. + s = SolverFactory("_sas94", **kwds) if not s.available(): - s = SolverFactory('_sascas', **kwds) + s = SolverFactory("_sascas", **kwds) return s @@ -134,7 +135,7 @@ class SASAbc(ABC, OptSolver): def __init__(self, **kwds): """Initialize the SAS solver interfaces.""" - kwds['type'] = 'sas' + kwds["type"] = "sas" super(SASAbc, self).__init__(**kwds) # @@ -157,7 +158,7 @@ def _presolve(self, *args, **kwds): TempfileManager.push() # Get the warmstart flag - self.warmstart_flag = kwds.pop('warmstart', False) + self.warmstart_flag = kwds.pop("warmstart", False) # Call parent presolve function super(SASAbc, self)._presolve(*args, **kwds) @@ -173,7 +174,7 @@ def _presolve(self, *args, **kwds): Var, active=True, descend_into=False ): self._vars.append(vardata) - # Store the symbal map, we need this for example when writing the warmstart file + # Store the symbol map, we need this for example when writing the warmstart file if isinstance(self._instance, IBlock): self._smap = getattr(self._instance, "._symbol_maps")[self._smap_id] else: @@ -186,8 +187,8 @@ def _presolve(self, *args, **kwds): ) smap = self._smap numWritten = 0 - with open(filename, 'w') as file: - file.write('_VAR_,_VALUE_\n') + with open(filename, "w") as file: + file.write("_VAR_,_VALUE_\n") for var in self._vars: if (var.value is not None) and (id(var) in smap.byObject): name = smap.byObject[id(var)] @@ -239,8 +240,7 @@ def _create_results_from_status(self, status, solution_status): @abstractmethod def _apply_solver(self): - """The routine that performs the solve""" - raise NotImplemented("This is an abstract function and thus not implemented!") + pass def _postsolve(self): """Clean up at the end, especially the temp files.""" @@ -260,7 +260,7 @@ def warm_start_capable(self): return True -@SolverFactory.register('_sas94', doc='SAS 9.4 interface') +@SolverFactory.register("_sas94", doc="SAS 9.4 interface") class SAS94(SASAbc): """ Solver interface for SAS 9.4 using saspy. See the saspy documentation about @@ -298,6 +298,9 @@ def _create_statement_str(self, statement): else: return "" + def sas_version(self): + return self._sasver + def _apply_solver(self): """ "Prepare the options and run the solver. Then store the data to be returned.""" logger.debug("Running SAS") @@ -326,39 +329,81 @@ def _apply_solver(self): decompsubprob_str = self._create_statement_str("decompsubprob") rootnode_str = self._create_statement_str("rootnode") + # Get a unique identifier, always use the same with different prefixes + unique = uuid.uuid4().hex[:16] + + # Create unique filename for output datasets + primalout_dataset_name = "pout" + unique + dualout_dataset_name = "dout" + unique + primalin_dataset_name = None + # Handle warmstart warmstart_str = "" if self.warmstart_flag: # Set the warmstart basis option + primalin_dataset_name = "pin" + unique if proc != "OPTLP": warmstart_str = """ proc import datafile='{primalin}' - out=primalin + out={primalin_dataset_name} dbms=csv replace; getnames=yes; run; """.format( - primalin=self._warm_start_file_name + primalin=self._warm_start_file_name, + primalin_dataset_name=primalin_dataset_name, ) - self.options["primalin"] = "primalin" + self.options["primalin"] = primalin_dataset_name # Convert options to string opt_str = " ".join( option + "=" + str(value) for option, value in self.options.items() ) - # Start a SAS session, submit the code and return the results`` + # Set some SAS options to make the log more clean + sas_options = "option notes nonumber nodate nosource pagesize=max;" + + # Start a SAS session, submit the code and return the results with self._sas.SASsession() as sas: # Find the version of 9.4 we are using - if sas.sasver.startswith("9.04.01M5"): + self._sasver = sas.sasver + + # Upload files, only if not accessible locally + upload_mps = False + if not sas.file_info(self._problem_files[0], quiet=True): + sas.upload( + self._problem_files[0], self._problem_files[0], overwrite=True + ) + upload_mps = True + + upload_pin = False + if self.warmstart_flag and not sas.file_info( + self._warm_start_file_name, quiet=True + ): + sas.upload( + self._warm_start_file_name, + self._warm_start_file_name, + overwrite=True, + ) + upload_pin = True + + # Using a function call to make it easier to moch the version check + version = self.sas_version().split("M", 1)[1][0] + if int(version) < 5: + raise NotImplementedError( + "Support for SAS 9.4 M4 and earlier is no implemented." + ) + elif int(version) == 5: # In 9.4M5 we have to create an MPS data set from an MPS file first # Earlier versions will not work because the MPS format in incompatible + mps_dataset_name = "mps" + unique res = sas.submit( """ + {sas_options} {warmstart} - %MPS2SASD(MPSFILE="{mpsfile}", OUTDATA=mpsdata, MAXLEN=256, FORMAT=FREE); - proc {proc} data=mpsdata {options} primalout=primalout dualout=dualout; + %MPS2SASD(MPSFILE="{mpsfile}", OUTDATA={mps_dataset_name}, MAXLEN=256, FORMAT=FREE); + proc {proc} data={mps_dataset_name} {options} primalout={primalout_dataset_name} dualout={dualout_dataset_name}; {decomp} {decompmaster} {decompmasterip} @@ -366,10 +411,14 @@ def _apply_solver(self): {rootnode} run; """.format( + sas_options=sas_options, warmstart=warmstart_str, proc=proc, mpsfile=self._problem_files[0], + mps_dataset_name=mps_dataset_name, options=opt_str, + primalout_dataset_name=primalout_dataset_name, + dualout_dataset_name=dualout_dataset_name, decomp=decomp_str, decompmaster=decompmaster_str, decompmasterip=decompmasterip_str, @@ -378,12 +427,14 @@ def _apply_solver(self): ), results="TEXT", ) + sas.sasdata(mps_dataset_name).delete(quiet=True) else: # Since 9.4M6+ optlp/optmilp can read mps files directly res = sas.submit( """ + {sas_options} {warmstart} - proc {proc} mpsfile=\"{mpsfile}\" {options} primalout=primalout dualout=dualout; + proc {proc} mpsfile=\"{mpsfile}\" {options} primalout={primalout_dataset_name} dualout={dualout_dataset_name}; {decomp} {decompmaster} {decompmasterip} @@ -391,10 +442,13 @@ def _apply_solver(self): {rootnode} run; """.format( + sas_options=sas_options, warmstart=warmstart_str, proc=proc, mpsfile=self._problem_files[0], options=opt_str, + primalout_dataset_name=primalout_dataset_name, + dualout_dataset_name=dualout_dataset_name, decomp=decomp_str, decompmaster=decompmaster_str, decompmasterip=decompmasterip_str, @@ -404,26 +458,40 @@ def _apply_solver(self): results="TEXT", ) + # Delete uploaded file + if upload_mps: + sas.file_delete(self._problem_files[0], quiet=True) + if self.warmstart_flag and upload_pin: + sas.file_delete(self._warm_start_file_name, quiet=True) + # Store log and ODS output self._log = res["LOG"] self._lst = res["LST"] - # Print log if requested by the user - if self._tee: - print(self._log) if "ERROR 22-322: Syntax error" in self._log: raise ValueError( "An option passed to the SAS solver caused a syntax error: {log}".format( log=self._log ) ) + else: + # Print log if requested by the user, only if we did not already print it + if self._tee: + print(self._log) self._macro = dict( (key.strip(), value.strip()) for key, value in ( pair.split("=") for pair in sas.symget("_OR" + proc + "_").split() ) ) - primal_out = sas.sd2df("primalout") - dual_out = sas.sd2df("dualout") + if self._macro.get("STATUS", "ERROR") == "OK": + primal_out = sas.sd2df(primalout_dataset_name) + dual_out = sas.sd2df(dualout_dataset_name) + + # Delete data sets, they will go away automatically, but does not hurt to delete them + if primalin_dataset_name: + sas.sasdata(primalin_dataset_name).delete(quiet=True) + sas.sasdata(primalout_dataset_name).delete(quiet=True) + sas.sasdata(dualout_dataset_name).delete(quiet=True) # Prepare the solver results results = self.results = self._create_results_from_status( @@ -445,35 +513,35 @@ def _apply_solver(self): sol.termination_condition = TerminationCondition.optimal # Store objective value in solution - sol.objective['__default_objective__'] = {'Value': self._macro["OBJECTIVE"]} + sol.objective["__default_objective__"] = {"Value": self._macro["OBJECTIVE"]} if proc == "OPTLP": # Convert primal out data set to variable dictionary # Use panda functions for efficiency - primal_out = primal_out[['_VAR_', '_VALUE_', '_STATUS_', '_R_COST_']] - primal_out = primal_out.set_index('_VAR_', drop=True) + primal_out = primal_out[["_VAR_", "_VALUE_", "_STATUS_", "_R_COST_"]] + primal_out = primal_out.set_index("_VAR_", drop=True) primal_out = primal_out.rename( - {'_VALUE_': 'Value', '_STATUS_': 'Status', '_R_COST_': 'rc'}, - axis='columns', + {"_VALUE_": "Value", "_STATUS_": "Status", "_R_COST_": "rc"}, + axis="columns", ) - sol.variable = primal_out.to_dict('index') + sol.variable = primal_out.to_dict("index") # Convert dual out data set to constraint dictionary # Use pandas functions for efficiency - dual_out = dual_out[['_ROW_', '_VALUE_', '_STATUS_', '_ACTIVITY_']] - dual_out = dual_out.set_index('_ROW_', drop=True) + dual_out = dual_out[["_ROW_", "_VALUE_", "_STATUS_", "_ACTIVITY_"]] + dual_out = dual_out.set_index("_ROW_", drop=True) dual_out = dual_out.rename( - {'_VALUE_': 'dual', '_STATUS_': 'Status', '_ACTIVITY_': 'slack'}, - axis='columns', + {"_VALUE_": "dual", "_STATUS_": "Status", "_ACTIVITY_": "slack"}, + axis="columns", ) - sol.constraint = dual_out.to_dict('index') + sol.constraint = dual_out.to_dict("index") else: # Convert primal out data set to variable dictionary # Use pandas functions for efficiency - primal_out = primal_out[['_VAR_', '_VALUE_']] - primal_out = primal_out.set_index('_VAR_', drop=True) - primal_out = primal_out.rename({'_VALUE_': 'Value'}, axis='columns') - sol.variable = primal_out.to_dict('index') + primal_out = primal_out[["_VAR_", "_VALUE_"]] + primal_out = primal_out.set_index("_VAR_", drop=True) + primal_out = primal_out.rename({"_VALUE_": "Value"}, axis="columns") + sol.variable = primal_out.to_dict("index") self._rc = 0 return Bunch(rc=self._rc, log=self._log) @@ -504,7 +572,7 @@ def log(self): return self._log.getvalue() -@SolverFactory.register('_sascas', doc='SAS Viya CAS Server interface') +@SolverFactory.register("_sascas", doc="SAS Viya CAS Server interface") class SASCAS(SASAbc): """ Solver interface connection to a SAS Viya CAS server using swat. @@ -553,70 +621,89 @@ def _apply_solver(self): # Check if there are integer variables, this might be slow action = "solveMilp" if self._has_integer_variables() else "solveLp" + # Get a unique identifier, always use the same with different prefixes + unique = uuid.uuid4().hex[:16] + # Connect to CAS server with redirect_stdout(SASLogWriter(self._tee)) as self._log_writer: s = self._sas.CAS(**cas_opts) try: # Load the optimization action set - s.loadactionset('optimization') + s.loadactionset("optimization") + + # Declare a unique table name for the mps table + mpsdata_table_name = "mps" + unique # Upload mps file to CAS - if os.stat(self._problem_files[0]).st_size >= 2 * 1024**3: - # For large files, use convertMPS, first create file for upload + if stat(self._problem_files[0]).st_size >= 2 * 1024**3: + # For files larger than 2 GB (this is a limitation of the loadMps action used in the else part). + # Use convertMPS, first create file for upload. mpsWithIdFileName = TempfileManager.create_tempfile( ".mps.csv", text=True ) - with open(mpsWithIdFileName, 'w') as mpsWithId: - mpsWithId.write('_ID_\tText\n') - with open(self._problem_files[0], 'r') as f: + with open(mpsWithIdFileName, "w") as mpsWithId: + mpsWithId.write("_ID_\tText\n") + with open(self._problem_files[0], "r") as f: id = 0 for line in f: id += 1 - mpsWithId.write(str(id) + '\t' + line.rstrip() + '\n') + mpsWithId.write(str(id) + "\t" + line.rstrip() + "\n") # Upload .mps.csv file + mpscsv_table_name = "csv" + unique s.upload_file( mpsWithIdFileName, - casout={"name": "mpscsv", "replace": True}, + casout={"name": mpscsv_table_name, "replace": True}, importoptions={"filetype": "CSV", "delimiter": "\t"}, ) # Convert .mps.csv file to .mps s.optimization.convertMps( - data="mpscsv", - casOut={"name": "mpsdata", "replace": True}, + data=mpscsv_table_name, + casOut={"name": mpsdata_table_name, "replace": True}, format="FREE", ) + + # Delete the table we don't need anymore + if mpscsv_table_name: + s.dropTable(name=mpscsv_table_name, quiet=True) else: - # For small files, use loadMPS - with open(self._problem_files[0], 'r') as mps_file: + # For small files (less than 2 GB), use loadMps + with open(self._problem_files[0], "r") as mps_file: s.optimization.loadMps( mpsFileString=mps_file.read(), - casout={"name": "mpsdata", "replace": True}, + casout={"name": mpsdata_table_name, "replace": True}, format="FREE", ) + primalin_table_name = None if self.warmstart_flag: + primalin_table_name = "pin" + unique # Upload warmstart file to CAS s.upload_file( self._warm_start_file_name, - casout={"name": "primalin", "replace": True}, + casout={"name": primalin_table_name, "replace": True}, importoptions={"filetype": "CSV"}, ) - self.options["primalin"] = "primalin" + self.options["primalin"] = primalin_table_name + + # Define output table names + primalout_table_name = "pout" + unique + dualout_table_name = None # Solve the problem in CAS if action == "solveMilp": r = s.optimization.solveMilp( - data={"name": "mpsdata"}, - primalOut={"name": "primalout", "replace": True}, + data={"name": mpsdata_table_name}, + primalOut={"name": primalout_table_name, "replace": True}, **self.options ) else: + dualout_table_name = "dout" + unique r = s.optimization.solveLp( - data={"name": "mpsdata"}, - primalOut={"name": "primalout", "replace": True}, - dualOut={"name": "dualout", "replace": True}, + data={"name": mpsdata_table_name}, + primalOut={"name": primalout_table_name, "replace": True}, + dualOut={"name": dualout_table_name, "replace": True}, **self.options ) @@ -644,44 +731,44 @@ def _apply_solver(self): sol.termination_condition = TerminationCondition.optimal # Store objective value in solution - sol.objective['__default_objective__'] = { - 'Value': r["objective"] + sol.objective["__default_objective__"] = { + "Value": r["objective"] } if action == "solveMilp": - primal_out = s.CASTable(name="primalout") + primal_out = s.CASTable(name=primalout_table_name) # Use pandas functions for efficiency - primal_out = primal_out[['_VAR_', '_VALUE_']] + primal_out = primal_out[["_VAR_", "_VALUE_"]] sol.variable = {} for row in primal_out.itertuples(index=False): - sol.variable[row[0]] = {'Value': row[1]} + sol.variable[row[0]] = {"Value": row[1]} else: # Convert primal out data set to variable dictionary # Use panda functions for efficiency - primal_out = s.CASTable(name="primalout") + primal_out = s.CASTable(name=primalout_table_name) primal_out = primal_out[ - ['_VAR_', '_VALUE_', '_STATUS_', '_R_COST_'] + ["_VAR_", "_VALUE_", "_STATUS_", "_R_COST_"] ] sol.variable = {} for row in primal_out.itertuples(index=False): sol.variable[row[0]] = { - 'Value': row[1], - 'Status': row[2], - 'rc': row[3], + "Value": row[1], + "Status": row[2], + "rc": row[3], } # Convert dual out data set to constraint dictionary # Use pandas functions for efficiency - dual_out = s.CASTable(name="dualout") + dual_out = s.CASTable(name=dualout_table_name) dual_out = dual_out[ - ['_ROW_', '_VALUE_', '_STATUS_', '_ACTIVITY_'] + ["_ROW_", "_VALUE_", "_STATUS_", "_ACTIVITY_"] ] sol.constraint = {} for row in dual_out.itertuples(index=False): sol.constraint[row[0]] = { - 'dual': row[1], - 'Status': row[2], - 'slack': row[3], + "dual": row[1], + "Status": row[2], + "slack": row[3], } else: results = self.results = SolverResults() @@ -692,10 +779,16 @@ def _apply_solver(self): ) finally: + if mpsdata_table_name: + s.dropTable(name=mpsdata_table_name, quiet=True) + if primalin_table_name: + s.dropTable(name=primalin_table_name, quiet=True) + if primalout_table_name: + s.dropTable(name=primalout_table_name, quiet=True) + if dualout_table_name: + s.dropTable(name=dualout_table_name, quiet=True) s.close() self._log = self._log_writer.log() - if self._tee: - print(self._log) self._rc = 0 return Bunch(rc=self._rc, log=self._log) diff --git a/pyomo/solvers/tests/checks/test_SAS.py b/pyomo/solvers/tests/checks/test_SAS.py index 654820f5060..3a63e258600 100644 --- a/pyomo/solvers/tests/checks/test_SAS.py +++ b/pyomo/solvers/tests/checks/test_SAS.py @@ -1,5 +1,6 @@ import os import pyomo.common.unittest as unittest +from unittest import mock from pyomo.environ import ( ConcreteModel, Var, @@ -15,20 +16,20 @@ ) from pyomo.opt.results import SolverStatus, TerminationCondition, ProblemSense from pyomo.opt import SolverFactory, check_available_solvers - +import warnings CAS_OPTIONS = { - "hostname": os.environ.get('CAS_SERVER', None), - "port": os.environ.get('CAS_PORT', None), - "authinfo": os.environ.get('CAS_AUTHINFO', None), + "hostname": os.environ.get("CASHOST", None), + "port": os.environ.get("CASPORT", None), + "authinfo": os.environ.get("CASAUTHINFO", None), } -sas_available = check_available_solvers('sas') +sas_available = check_available_solvers("sas") class SASTestAbc: - solver_io = '_sas94' + solver_io = "_sas94" base_options = {} def setObj(self): @@ -41,6 +42,8 @@ def setX(self): self.instance.X = Var([1, 2, 3], within=NonNegativeReals) def setUp(self): + # Disable resource warnings + warnings.filterwarnings("ignore", category=ResourceWarning) instance = self.instance = ConcreteModel() self.setX() X = instance.X @@ -55,7 +58,7 @@ def setUp(self): instance.rc = Suffix(direction=Suffix.IMPORT) instance.dual = Suffix(direction=Suffix.IMPORT) - self.opt_sas = SolverFactory('sas', solver_io=self.solver_io) + self.opt_sas = SolverFactory("sas", solver_io=self.solver_io) def tearDown(self): del self.opt_sas @@ -74,7 +77,7 @@ def run_solver(self, **kwargs): self.results = opt_sas.solve(instance, **kwargs) -class SASTestLP(SASTestAbc, unittest.TestCase): +class SASTestLP(SASTestAbc): def checkSolution(self): instance = self.instance results = self.results @@ -111,41 +114,39 @@ def checkSolution(self): self.assertAlmostEqual(instance.dual[instance.R3], sense * 0.0) # Check basis status - self.assertEqual(instance.status[instance.X[1]], 'L') - self.assertEqual(instance.status[instance.X[2]], 'B') - self.assertEqual(instance.status[instance.X[3]], 'L') - self.assertEqual(instance.status[instance.R1], 'U') - self.assertEqual(instance.status[instance.R2], 'B') - self.assertEqual(instance.status[instance.R3], 'B') - - @unittest.skipIf(not sas_available, "The SAS solver is not available") + self.assertEqual(instance.status[instance.X[1]], "L") + self.assertEqual(instance.status[instance.X[2]], "B") + self.assertEqual(instance.status[instance.X[3]], "L") + self.assertEqual(instance.status[instance.R1], "U") + self.assertEqual(instance.status[instance.R2], "B") + self.assertEqual(instance.status[instance.R3], "B") + def test_solver_default(self): self.run_solver() self.checkSolution() - @unittest.skipIf(not sas_available, "The SAS solver is not available") + def test_solver_tee(self): + self.run_solver(tee=True) + self.checkSolution() + def test_solver_primal(self): self.run_solver(options={"algorithm": "ps"}) self.assertIn("NOTE: The Primal Simplex algorithm is used.", self.opt_sas._log) self.checkSolution() - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_ipm(self): self.run_solver(options={"algorithm": "ip"}) self.assertIn("NOTE: The Interior Point algorithm is used.", self.opt_sas._log) self.checkSolution() - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_intoption(self): self.run_solver(options={"maxiter": 20}) self.checkSolution() - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_invalidoption(self): with self.assertRaisesRegex(ValueError, "syntax error"): self.run_solver(options={"foo": "bar"}) - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_max(self): X = self.instance.X self.instance.Obj.set_value(expr=-2 * X[1] + 3 * X[2] + 4 * X[3]) @@ -154,7 +155,6 @@ def test_solver_max(self): self.checkSolution() self.assertEqual(self.results.problem.sense, ProblemSense.maximize) - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_infeasible(self): instance = self.instance X = instance.X @@ -167,21 +167,23 @@ def test_solver_infeasible(self): ) self.assertEqual(results.solver.message, "The problem is infeasible.") - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_infeasible_or_unbounded(self): self.instance.X.domain = Reals self.run_solver() results = self.results self.assertEqual(results.solver.status, SolverStatus.warning) - self.assertEqual( + self.assertIn( results.solver.termination_condition, - TerminationCondition.infeasibleOrUnbounded, + [ + TerminationCondition.infeasibleOrUnbounded, + TerminationCondition.unbounded, + ], ) - self.assertEqual( - results.solver.message, "The problem is infeasible or unbounded." + self.assertIn( + results.solver.message, + ["The problem is infeasible or unbounded.", "The problem is unbounded."], ) - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_unbounded(self): self.instance.X.domain = Reals self.run_solver(options={"presolver": "none", "algorithm": "primal"}) @@ -229,7 +231,6 @@ def checkSolutionDecomp(self): # Don't check basis status for decomp - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_decomp(self): self.run_solver( options={ @@ -243,7 +244,6 @@ def test_solver_decomp(self): ) self.checkSolutionDecomp() - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_iis(self): self.run_solver(options={"iis": "true"}) results = self.results @@ -257,7 +257,6 @@ def test_solver_iis(self): "The problem is feasible. This status is displayed when the IIS=TRUE option is specified and the problem is feasible.", ) - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_maxiter(self): self.run_solver(options={"maxiter": 1}) results = self.results @@ -270,13 +269,59 @@ def test_solver_maxiter(self): "The maximum allowable number of iterations was reached.", ) + def test_solver_with_milp(self): + self.run_solver(options={"with": "milp"}) + self.assertIn( + "WARNING: The problem has no integer variables.", self.opt_sas._log + ) + -class SASTestLPCAS(SASTestLP): - solver_io = '_sascas' +@unittest.skipIf(not sas_available, "The SAS solver is not available") +class SASTestLP94(SASTestLP, unittest.TestCase): + @mock.patch( + "pyomo.solvers.plugins.solvers.SAS.SAS94.sas_version", + return_value="2sd45s39M4234232", + ) + def test_solver_versionM4(self, sas): + with self.assertRaises(NotImplementedError): + self.run_solver() + + @mock.patch( + "pyomo.solvers.plugins.solvers.SAS.SAS94.sas_version", + return_value="234897293M5324u98", + ) + def test_solver_versionM5(self, sas): + self.run_solver() + self.checkSolution() + + @mock.patch("saspy.SASsession.submit", return_value={"LOG": "", "LST": ""}) + @mock.patch("saspy.SASsession.symget", return_value="STATUS=OUT_OF_MEMORY") + def test_solver_out_of_memory(self, submit_mock, symget_mocks): + self.run_solver(load_solutions=False) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.aborted) + + @mock.patch("saspy.SASsession.submit", return_value={"LOG": "", "LST": ""}) + @mock.patch("saspy.SASsession.symget", return_value="STATUS=ERROR") + def test_solver_error(self, submit_mock, symget_mock): + self.run_solver(load_solutions=False) + results = self.results + self.assertEqual(results.solver.status, SolverStatus.error) + + +@unittest.skipIf(not sas_available, "The SAS solver is not available") +class SASTestLPCAS(SASTestLP, unittest.TestCase): + solver_io = "_sascas" base_options = CAS_OPTIONS + @mock.patch("pyomo.solvers.plugins.solvers.SAS.stat") + def test_solver_large_file(self, os_stat): + os_stat.return_value.st_size = 3 * 1024**3 + self.run_solver() + self.checkSolution() + -class SASTestMILP(SASTestAbc, unittest.TestCase): +class SASTestMILP(SASTestAbc): def setX(self): self.instance.X = Var([1, 2, 3], within=NonNegativeIntegers) @@ -301,12 +346,14 @@ def checkSolution(self): self.assertAlmostEqual(instance.X[2].value, 1.0) self.assertAlmostEqual(instance.X[3].value, 1.0) - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_default(self): - self.run_solver(options={}) + self.run_solver() + self.checkSolution() + + def test_solver_tee(self): + self.run_solver(tee=True) self.checkSolution() - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_presolve(self): self.run_solver(options={"presolver": "none"}) self.assertIn( @@ -314,17 +361,14 @@ def test_solver_presolve(self): ) self.checkSolution() - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_intoption(self): self.run_solver(options={"maxnodes": 20}) self.checkSolution() - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_invalidoption(self): with self.assertRaisesRegex(ValueError, "syntax error"): self.run_solver(options={"foo": "bar"}) - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_max(self): X = self.instance.X self.instance.Obj.set_value(expr=-2 * X[1] + 3 * X[2] + 4 * X[3]) @@ -332,7 +376,6 @@ def test_solver_max(self): self.run_solver() self.checkSolution() - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_infeasible(self): instance = self.instance X = instance.X @@ -345,22 +388,23 @@ def test_solver_infeasible(self): ) self.assertEqual(results.solver.message, "The problem is infeasible.") - @unittest.skipIf(not sas_available, "The SAS solver is not available") - @unittest.skip("Returns wrong status for some versions.") def test_solver_infeasible_or_unbounded(self): self.instance.X.domain = Integers self.run_solver() results = self.results self.assertEqual(results.solver.status, SolverStatus.warning) - self.assertEqual( + self.assertIn( results.solver.termination_condition, - TerminationCondition.infeasibleOrUnbounded, + [ + TerminationCondition.infeasibleOrUnbounded, + TerminationCondition.unbounded, + ], ) - self.assertEqual( - results.solver.message, "The problem is infeasible or unbounded." + self.assertIn( + results.solver.message, + ["The problem is infeasible or unbounded.", "The problem is unbounded."], ) - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_unbounded(self): self.instance.X.domain = Integers self.run_solver( @@ -373,7 +417,6 @@ def test_solver_unbounded(self): ) self.assertEqual(results.solver.message, "The problem is unbounded.") - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_decomp(self): self.run_solver( options={ @@ -388,12 +431,10 @@ def test_solver_decomp(self): ) self.checkSolution() - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_rootnode(self): self.run_solver(options={"rootnode": {"presolver": "automatic"}}) self.checkSolution() - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_maxnodes(self): self.run_solver(options={"maxnodes": 0}) results = self.results @@ -406,7 +447,6 @@ def test_solver_maxnodes(self): "The solver reached the maximum number of nodes specified by the MAXNODES= option and found a solution.", ) - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_maxsols(self): self.run_solver(options={"maxsols": 1}) results = self.results @@ -419,7 +459,6 @@ def test_solver_maxsols(self): "The solver reached the maximum number of solutions specified by the MAXSOLS= option.", ) - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_target(self): self.run_solver(options={"target": -6.0}) results = self.results @@ -432,7 +471,6 @@ def test_solver_target(self): "The solution is not worse than the target specified by the TARGET= option.", ) - @unittest.skipIf(not sas_available, "The SAS solver is not available") def test_solver_primalin(self): X = self.instance.X X[1] = None @@ -445,11 +483,42 @@ def test_solver_primalin(self): self.opt_sas._log, ) + def test_solver_primalin_nosol(self): + X = self.instance.X + X[1] = None + X[2] = None + X[3] = None + self.run_solver(warmstart=True) + self.checkSolution() + + @mock.patch("pyomo.solvers.plugins.solvers.SAS.stat") + def test_solver_large_file(self, os_stat): + os_stat.return_value.st_size = 3 * 1024**3 + self.run_solver() + self.checkSolution() + + def test_solver_with_lp(self): + self.run_solver(options={"with": "lp"}) + self.assertIn( + "contains integer variables; the linear relaxation will be solved.", + self.opt_sas._log, + ) + + def test_solver_warmstart_capable(self): + self.run_solver() + self.assertTrue(self.opt_sas.warm_start_capable()) + + +@unittest.skipIf(not sas_available, "The SAS solver is not available") +class SASTestMILP94(SASTestMILP, unittest.TestCase): + pass + -class SASTestMILPCAS(SASTestMILP): - solver_io = '_sascas' +@unittest.skipIf(not sas_available, "The SAS solver is not available") +class SASTestMILPCAS(SASTestMILP, unittest.TestCase): + solver_io = "_sascas" base_options = CAS_OPTIONS -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 4b6298da177aa6c16a1bf581dc6e0508e9ee6084 Mon Sep 17 00:00:00 2001 From: "Philipp Christophel (phchri)" Date: Mon, 7 Aug 2023 11:26:10 -0400 Subject: [PATCH 0014/3044] Change SAS solver interfaces to keep the SAS connection --- pyomo/solvers/plugins/solvers/SAS.py | 266 +++++++++++++------------ pyomo/solvers/tests/checks/test_SAS.py | 11 +- 2 files changed, 149 insertions(+), 128 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/SAS.py b/pyomo/solvers/plugins/solvers/SAS.py index 87a6a18c1f4..a5f74849813 100644 --- a/pyomo/solvers/plugins/solvers/SAS.py +++ b/pyomo/solvers/plugins/solvers/SAS.py @@ -285,6 +285,14 @@ def __init__(self, **kwds): self._python_api_exists = True self._sas.logger.setLevel(logger.level) + # Create the session only as its needed + self._sas_session = None + + def __del__(self): + # Close the session, if we created one + if self._sas_session: + self._sas_session.endsas() + def _create_statement_str(self, statement): """Helper function to create the strings for the statements of the proc OPTLP/OPTMILP code.""" stmt = self.options.pop(statement, None) @@ -364,134 +372,133 @@ def _apply_solver(self): # Set some SAS options to make the log more clean sas_options = "option notes nonumber nodate nosource pagesize=max;" - # Start a SAS session, submit the code and return the results - with self._sas.SASsession() as sas: - # Find the version of 9.4 we are using - self._sasver = sas.sasver - - # Upload files, only if not accessible locally - upload_mps = False - if not sas.file_info(self._problem_files[0], quiet=True): - sas.upload( - self._problem_files[0], self._problem_files[0], overwrite=True - ) - upload_mps = True - - upload_pin = False - if self.warmstart_flag and not sas.file_info( - self._warm_start_file_name, quiet=True - ): - sas.upload( - self._warm_start_file_name, - self._warm_start_file_name, - overwrite=True, - ) - upload_pin = True + # Get the current SAS session, submit the code and return the results + sas = self._sas_session + if sas == None: + sas = self._sas_session = self._sas.SASsession() + + # Find the version of 9.4 we are using + self._sasver = sas.sasver + + # Upload files, only if not accessible locally + upload_mps = False + if not sas.file_info(self._problem_files[0], quiet=True): + sas.upload(self._problem_files[0], self._problem_files[0], overwrite=True) + upload_mps = True + + upload_pin = False + if self.warmstart_flag and not sas.file_info( + self._warm_start_file_name, quiet=True + ): + sas.upload( + self._warm_start_file_name, self._warm_start_file_name, overwrite=True + ) + upload_pin = True - # Using a function call to make it easier to moch the version check - version = self.sas_version().split("M", 1)[1][0] - if int(version) < 5: - raise NotImplementedError( - "Support for SAS 9.4 M4 and earlier is no implemented." - ) - elif int(version) == 5: - # In 9.4M5 we have to create an MPS data set from an MPS file first - # Earlier versions will not work because the MPS format in incompatible - mps_dataset_name = "mps" + unique - res = sas.submit( - """ - {sas_options} - {warmstart} - %MPS2SASD(MPSFILE="{mpsfile}", OUTDATA={mps_dataset_name}, MAXLEN=256, FORMAT=FREE); - proc {proc} data={mps_dataset_name} {options} primalout={primalout_dataset_name} dualout={dualout_dataset_name}; - {decomp} - {decompmaster} - {decompmasterip} - {decompsubprob} - {rootnode} - run; - """.format( - sas_options=sas_options, - warmstart=warmstart_str, - proc=proc, - mpsfile=self._problem_files[0], - mps_dataset_name=mps_dataset_name, - options=opt_str, - primalout_dataset_name=primalout_dataset_name, - dualout_dataset_name=dualout_dataset_name, - decomp=decomp_str, - decompmaster=decompmaster_str, - decompmasterip=decompmasterip_str, - decompsubprob=decompsubprob_str, - rootnode=rootnode_str, - ), - results="TEXT", - ) - sas.sasdata(mps_dataset_name).delete(quiet=True) - else: - # Since 9.4M6+ optlp/optmilp can read mps files directly - res = sas.submit( - """ - {sas_options} - {warmstart} - proc {proc} mpsfile=\"{mpsfile}\" {options} primalout={primalout_dataset_name} dualout={dualout_dataset_name}; - {decomp} - {decompmaster} - {decompmasterip} - {decompsubprob} - {rootnode} - run; - """.format( - sas_options=sas_options, - warmstart=warmstart_str, - proc=proc, - mpsfile=self._problem_files[0], - options=opt_str, - primalout_dataset_name=primalout_dataset_name, - dualout_dataset_name=dualout_dataset_name, - decomp=decomp_str, - decompmaster=decompmaster_str, - decompmasterip=decompmasterip_str, - decompsubprob=decompsubprob_str, - rootnode=rootnode_str, - ), - results="TEXT", - ) + # Using a function call to make it easier to moch the version check + version = self.sas_version().split("M", 1)[1][0] + if int(version) < 5: + raise NotImplementedError( + "Support for SAS 9.4 M4 and earlier is no implemented." + ) + elif int(version) == 5: + # In 9.4M5 we have to create an MPS data set from an MPS file first + # Earlier versions will not work because the MPS format in incompatible + mps_dataset_name = "mps" + unique + res = sas.submit( + """ + {sas_options} + {warmstart} + %MPS2SASD(MPSFILE="{mpsfile}", OUTDATA={mps_dataset_name}, MAXLEN=256, FORMAT=FREE); + proc {proc} data={mps_dataset_name} {options} primalout={primalout_dataset_name} dualout={dualout_dataset_name}; + {decomp} + {decompmaster} + {decompmasterip} + {decompsubprob} + {rootnode} + run; + """.format( + sas_options=sas_options, + warmstart=warmstart_str, + proc=proc, + mpsfile=self._problem_files[0], + mps_dataset_name=mps_dataset_name, + options=opt_str, + primalout_dataset_name=primalout_dataset_name, + dualout_dataset_name=dualout_dataset_name, + decomp=decomp_str, + decompmaster=decompmaster_str, + decompmasterip=decompmasterip_str, + decompsubprob=decompsubprob_str, + rootnode=rootnode_str, + ), + results="TEXT", + ) + sas.sasdata(mps_dataset_name).delete(quiet=True) + else: + # Since 9.4M6+ optlp/optmilp can read mps files directly + res = sas.submit( + """ + {sas_options} + {warmstart} + proc {proc} mpsfile=\"{mpsfile}\" {options} primalout={primalout_dataset_name} dualout={dualout_dataset_name}; + {decomp} + {decompmaster} + {decompmasterip} + {decompsubprob} + {rootnode} + run; + """.format( + sas_options=sas_options, + warmstart=warmstart_str, + proc=proc, + mpsfile=self._problem_files[0], + options=opt_str, + primalout_dataset_name=primalout_dataset_name, + dualout_dataset_name=dualout_dataset_name, + decomp=decomp_str, + decompmaster=decompmaster_str, + decompmasterip=decompmasterip_str, + decompsubprob=decompsubprob_str, + rootnode=rootnode_str, + ), + results="TEXT", + ) - # Delete uploaded file - if upload_mps: - sas.file_delete(self._problem_files[0], quiet=True) - if self.warmstart_flag and upload_pin: - sas.file_delete(self._warm_start_file_name, quiet=True) - - # Store log and ODS output - self._log = res["LOG"] - self._lst = res["LST"] - if "ERROR 22-322: Syntax error" in self._log: - raise ValueError( - "An option passed to the SAS solver caused a syntax error: {log}".format( - log=self._log - ) - ) - else: - # Print log if requested by the user, only if we did not already print it - if self._tee: - print(self._log) - self._macro = dict( - (key.strip(), value.strip()) - for key, value in ( - pair.split("=") for pair in sas.symget("_OR" + proc + "_").split() + # Delete uploaded file + if upload_mps: + sas.file_delete(self._problem_files[0], quiet=True) + if self.warmstart_flag and upload_pin: + sas.file_delete(self._warm_start_file_name, quiet=True) + + # Store log and ODS output + self._log = res["LOG"] + self._lst = res["LST"] + if "ERROR 22-322: Syntax error" in self._log: + raise ValueError( + "An option passed to the SAS solver caused a syntax error: {log}".format( + log=self._log ) ) - if self._macro.get("STATUS", "ERROR") == "OK": - primal_out = sas.sd2df(primalout_dataset_name) - dual_out = sas.sd2df(dualout_dataset_name) + else: + # Print log if requested by the user, only if we did not already print it + if self._tee: + print(self._log) + self._macro = dict( + (key.strip(), value.strip()) + for key, value in ( + pair.split("=") for pair in sas.symget("_OR" + proc + "_").split() + ) + ) + if self._macro.get("STATUS", "ERROR") == "OK": + primal_out = sas.sd2df(primalout_dataset_name) + dual_out = sas.sd2df(dualout_dataset_name) - # Delete data sets, they will go away automatically, but does not hurt to delete them - if primalin_dataset_name: - sas.sasdata(primalin_dataset_name).delete(quiet=True) - sas.sasdata(primalout_dataset_name).delete(quiet=True) - sas.sasdata(dualout_dataset_name).delete(quiet=True) + # Delete data sets, they will go away automatically, but does not hurt to delete them + if primalin_dataset_name: + sas.sasdata(primalin_dataset_name).delete(quiet=True) + sas.sasdata(primalout_dataset_name).delete(quiet=True) + sas.sasdata(dualout_dataset_name).delete(quiet=True) # Prepare the solver results results = self.results = self._create_results_from_status( @@ -597,6 +604,14 @@ def __init__(self, **kwds): else: self._python_api_exists = True + # Create the session only as its needed + self._sas_session = None + + def __del__(self): + # Close the session, if we created one + if self._sas_session: + self._sas_session.close() + def _apply_solver(self): """ "Prepare the options and run the solver. Then store the data to be returned.""" logger.debug("Running SAS Viya") @@ -626,7 +641,9 @@ def _apply_solver(self): # Connect to CAS server with redirect_stdout(SASLogWriter(self._tee)) as self._log_writer: - s = self._sas.CAS(**cas_opts) + s = self._sas_session + if s == None: + s = self._sas_session = self._sas.CAS(**cas_opts) try: # Load the optimization action set s.loadactionset("optimization") @@ -787,7 +804,6 @@ def _apply_solver(self): s.dropTable(name=primalout_table_name, quiet=True) if dualout_table_name: s.dropTable(name=dualout_table_name, quiet=True) - s.close() self._log = self._log_writer.log() self._rc = 0 diff --git a/pyomo/solvers/tests/checks/test_SAS.py b/pyomo/solvers/tests/checks/test_SAS.py index 3a63e258600..1a6bbd80f1d 100644 --- a/pyomo/solvers/tests/checks/test_SAS.py +++ b/pyomo/solvers/tests/checks/test_SAS.py @@ -32,6 +32,14 @@ class SASTestAbc: solver_io = "_sas94" base_options = {} + @classmethod + def setUpClass(cls): + cls.opt_sas = SolverFactory("sas", solver_io=cls.solver_io) + + @classmethod + def tearDownClass(cls): + del cls.opt_sas + def setObj(self): X = self.instance.X self.instance.Obj = Objective( @@ -58,10 +66,7 @@ def setUp(self): instance.rc = Suffix(direction=Suffix.IMPORT) instance.dual = Suffix(direction=Suffix.IMPORT) - self.opt_sas = SolverFactory("sas", solver_io=self.solver_io) - def tearDown(self): - del self.opt_sas del self.instance def run_solver(self, **kwargs): From 619edf9805639ef2c95088c90f09d3eb5e8972c4 Mon Sep 17 00:00:00 2001 From: "Philipp Christophel (phchri)" Date: Mon, 7 Aug 2023 14:31:58 -0400 Subject: [PATCH 0015/3044] Fix formatting issue in comment --- pyomo/solvers/plugins/solvers/SAS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solvers/plugins/solvers/SAS.py b/pyomo/solvers/plugins/solvers/SAS.py index a5f74849813..f5840b5d6f3 100644 --- a/pyomo/solvers/plugins/solvers/SAS.py +++ b/pyomo/solvers/plugins/solvers/SAS.py @@ -152,7 +152,7 @@ def __init__(self, **kwds): super(SASAbc, self).set_problem_format(ProblemFormat.mps) def _presolve(self, *args, **kwds): - """ "Set things up for the actual solve.""" + """Set things up for the actual solve.""" # create a context in the temporary file manager for # this plugin - is "pop"ed in the _postsolve method. TempfileManager.push() From faddef1a12afdc8953a14ce78146a3d8ab7cef89 Mon Sep 17 00:00:00 2001 From: "Philipp Christophel (phchri)" Date: Fri, 11 Aug 2023 03:23:50 -0400 Subject: [PATCH 0016/3044] Reset .gitignore to original state. --- .gitignore | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 7309ff1e8a8..09069552990 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,6 @@ .spyder* .ropeproject .vscode -.env -venv -sascfg_personal.py # Python generates numerous files when byte compiling / installing packages *.pyx *.pyc @@ -27,4 +24,4 @@ gurobi.log # Jupyterhub/Jupyterlab checkpoints .ipynb_checkpoints -cplex.log +cplex.log \ No newline at end of file From 4c9af82c7dd2226412de5ca50ed8c85be66cc67f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 11 Aug 2023 10:12:40 -0600 Subject: [PATCH 0017/3044] Adjust tests to check for 'ok' instead of 'optimal' --- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 4a5c816394f..ec9f397bdc4 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -91,7 +91,7 @@ def test_remove_variable_and_objective( m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, 2) del m.x @@ -99,7 +99,7 @@ def test_remove_variable_and_objective( m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.ok) self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) From cdcfeff95efc8881376986c031bb7b0357b3c3ef Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 11 Aug 2023 12:55:34 -0600 Subject: [PATCH 0018/3044] Much discussion with @jsiirola resulted in a small change --- pyomo/contrib/appsi/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index fd8f432a7fa..a715fabf1f6 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -75,7 +75,7 @@ class TerminationCondition(enum.Enum): unknown = 42 """unknown serves as both a default value, and it is used when no other enum member makes sense""" - ok = 0 + convergenceCriteriaSatisfied = 0 """The solver exited with the optimal solution""" maxTimeLimit = 1 From 92480c386a23e692eaac9bee3097ce9fce1fdd3b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 15 Aug 2023 11:13:38 -0600 Subject: [PATCH 0019/3044] Small refactor in APPSI to improve performance; small rework of TerminationCondition/SolverStatus --- pyomo/contrib/appsi/base.py | 200 ++++++++---------- pyomo/contrib/appsi/build.py | 4 +- .../contrib/appsi/examples/getting_started.py | 4 +- .../appsi/examples/tests/test_examples.py | 2 +- pyomo/contrib/appsi/fbbt.py | 16 +- pyomo/contrib/appsi/solvers/cbc.py | 18 +- pyomo/contrib/appsi/solvers/cplex.py | 10 +- pyomo/contrib/appsi/solvers/gurobi.py | 66 +++--- pyomo/contrib/appsi/solvers/highs.py | 70 +++--- pyomo/contrib/appsi/solvers/ipopt.py | 8 +- .../solvers/tests/test_gurobi_persistent.py | 2 +- .../solvers/tests/test_ipopt_persistent.py | 2 +- .../solvers/tests/test_persistent_solvers.py | 8 +- pyomo/contrib/appsi/tests/test_base.py | 8 +- pyomo/contrib/appsi/tests/test_interval.py | 4 +- .../utils/collect_vars_and_named_exprs.py | 8 +- pyomo/contrib/appsi/writers/config.py | 2 +- pyomo/contrib/appsi/writers/lp_writer.py | 14 +- pyomo/contrib/appsi/writers/nl_writer.py | 18 +- .../appsi/writers/tests/test_nl_writer.py | 2 +- 20 files changed, 227 insertions(+), 239 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index a715fabf1f6..805e96dacf1 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -72,61 +72,66 @@ class TerminationCondition(enum.Enum): An enumeration for checking the termination condition of solvers """ - unknown = 42 """unknown serves as both a default value, and it is used when no other enum member makes sense""" + unknown = 42 + """The solver exited because the convergence criteria were satisfied""" convergenceCriteriaSatisfied = 0 - """The solver exited with the optimal solution""" - maxTimeLimit = 1 """The solver exited due to a time limit""" + maxTimeLimit = 1 - maxIterations = 2 - """The solver exited due to an iteration limit """ + """The solver exited due to an iteration limit""" + iterationLimit = 2 - objectiveLimit = 3 """The solver exited due to an objective limit""" + objectiveLimit = 3 - minStepLength = 4 """The solver exited due to a minimum step length""" + minStepLength = 4 - unbounded = 5 """The solver exited because the problem is unbounded""" + unbounded = 5 - infeasible = 6 - """The solver exited because the problem is infeasible""" + """The solver exited because the problem is proven infeasible""" + provenInfeasible = 6 + + """The solver exited because the problem was found to be locally infeasible""" + locallyInfeasible = 7 - infeasibleOrUnbounded = 7 """The solver exited because the problem is either infeasible or unbounded""" + infeasibleOrUnbounded = 8 - error = 8 """The solver exited due to an error""" + error = 9 - interrupted = 9 """The solver exited because it was interrupted""" + interrupted = 10 - licensingProblems = 10 """The solver exited due to licensing problems""" + licensingProblems = 11 -class SolutionStatus(enum.Enum): +class SolutionStatus(enum.IntEnum): """ - An enumeration for interpreting the result of a termination - - TODO: We may want to not use enum.Enum; we may want to use the flavor that allows sets + An enumeration for interpreting the result of a termination. This describes the designated + status by the solver to be loaded back into the model. + + For now, we are choosing to use IntEnum such that return values are numerically + assigned in increasing order. """ - """No solution found""" + """No (single) solution found; possible that a population of solutions was returned""" noSolution = 0 - """Locally optimal solution identified""" - locallyOptimal = 1 - - """Globally optimal solution identified""" - globallyOptimal = 2 + """Solution point does not satisfy some domains and/or constraints""" + infeasible = 10 """Feasible solution identified""" - feasible = 3 + feasible = 20 + + """Optimal solution identified""" + optimal = 30 # # InterfaceConfig @@ -182,7 +187,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(InterfaceConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -223,7 +228,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(MIPInterfaceConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -404,9 +409,9 @@ def get_duals( 'for the given problem type.' ) if cons_to_load is None: - duals = dict(self._duals) + duals = {self._duals} else: - duals = dict() + duals = {} for c in cons_to_load: duals[c] = self._duals[c] return duals @@ -421,9 +426,9 @@ def get_slacks( 'for the given problem type.' ) if cons_to_load is None: - slacks = dict(self._slacks) + slacks = {self._slacks} else: - slacks = dict() + slacks = {} for c in cons_to_load: slacks[c] = self._slacks[c] return slacks @@ -446,7 +451,7 @@ def get_reduced_costs( return rc -class Results(object): +class Results(): """ Attributes ---------- @@ -526,7 +531,7 @@ def __init__( ): if doc is None: doc = 'Configuration options to detect changes in model between solves' - super(UpdateConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -1031,23 +1036,23 @@ def invalidate(self): class PersistentBase(abc.ABC): def __init__(self, only_child_vars=False): self._model = None - self._active_constraints = dict() # maps constraint to (lower, body, upper) - self._vars = dict() # maps var id to (var, lb, ub, fixed, domain, value) - self._params = dict() # maps param id to param + self._active_constraints = {} # maps constraint to (lower, body, upper) + self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) + self._params = {} # maps param id to param self._objective = None self._objective_expr = None self._objective_sense = None self._named_expressions = ( - dict() + {} ) # maps constraint to list of tuples (named_expr, named_expr.expr) self._external_functions = ComponentMap() - self._obj_named_expressions = list() + self._obj_named_expressions = [] self._update_config = UpdateConfig() self._referenced_variables = ( - dict() + {} ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] - self._vars_referenced_by_con = dict() - self._vars_referenced_by_obj = list() + self._vars_referenced_by_con = {} + self._vars_referenced_by_obj = [] self._expr_types = None self.use_extensions = False self._only_child_vars = only_child_vars @@ -1081,7 +1086,7 @@ def add_variables(self, variables: List[_GeneralVarData]): raise ValueError( 'variable {name} has already been added'.format(name=v.name) ) - self._referenced_variables[id(v)] = [dict(), dict(), None] + self._referenced_variables[id(v)] = [{}, {}, None] self._vars[id(v)] = ( v, v._lb, @@ -1106,24 +1111,24 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): pass def _check_for_new_vars(self, variables: List[_GeneralVarData]): - new_vars = dict() + new_vars = {} for v in variables: v_id = id(v) if v_id not in self._referenced_variables: new_vars[v_id] = v - self.add_variables(list(new_vars.values())) + self.add_variables([new_vars.values()]) def _check_to_remove_vars(self, variables: List[_GeneralVarData]): - vars_to_remove = dict() + vars_to_remove = {} for v in variables: v_id = id(v) ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: vars_to_remove[v_id] = v - self.remove_variables(list(vars_to_remove.values())) + self.remove_variables([vars_to_remove.values()]) def add_constraints(self, cons: List[_GeneralConstraintData]): - all_fixed_vars = dict() + all_fixed_vars = {} for con in cons: if con in self._named_expressions: raise ValueError( @@ -1165,7 +1170,7 @@ def add_sos_constraints(self, cons: List[_SOSConstraintData]): variables = con.get_variables() if not self._only_child_vars: self._check_for_new_vars(variables) - self._named_expressions[con] = list() + self._named_expressions[con] = [] self._vars_referenced_by_con[con] = variables for v in variables: self._referenced_variables[id(v)][1][con] = None @@ -1206,20 +1211,20 @@ def set_objective(self, obj: _GeneralObjectiveData): for v in fixed_vars: v.fix() else: - self._vars_referenced_by_obj = list() + self._vars_referenced_by_obj = [] self._objective = None self._objective_expr = None self._objective_sense = None - self._obj_named_expressions = list() + self._obj_named_expressions = [] self._set_objective(obj) def add_block(self, block): - param_dict = dict() + param_dict = {} for p in block.component_objects(Param, descend_into=True): if p.mutable: for _p in p.values(): param_dict[id(_p)] = _p - self.add_params(list(param_dict.values())) + self.add_params([param_dict.values()]) if self._only_child_vars: self.add_variables( list( @@ -1230,20 +1235,10 @@ def add_block(self, block): ) ) self.add_constraints( - [ - con - for con in block.component_data_objects( - Constraint, descend_into=True, active=True - ) - ] + list(block.component_data_objects(Constraint, descend_into=True, active=True)) ) self.add_sos_constraints( - [ - con - for con in block.component_data_objects( - SOSConstraint, descend_into=True, active=True - ) - ] + list(block.component_data_objects(SOSConstraint, descend_into=True, active=True)) ) obj = get_objective(block) if obj is not None: @@ -1326,20 +1321,10 @@ def remove_params(self, params: List[_ParamData]): def remove_block(self, block): self.remove_constraints( - [ - con - for con in block.component_data_objects( - ctype=Constraint, descend_into=True, active=True - ) - ] + list(block.component_data_objects(ctype=Constraint, descend_into=True, active=True)) ) self.remove_sos_constraints( - [ - con - for con in block.component_data_objects( - ctype=SOSConstraint, descend_into=True, active=True - ) - ] + list(block.component_data_objects(ctype=SOSConstraint, descend_into=True, active=True)) ) if self._only_child_vars: self.remove_variables( @@ -1387,17 +1372,17 @@ def update(self, timer: HierarchicalTimer = None): if timer is None: timer = HierarchicalTimer() config = self.update_config - new_vars = list() - old_vars = list() - new_params = list() - old_params = list() - new_cons = list() - old_cons = list() - old_sos = list() - new_sos = list() - current_vars_dict = dict() - current_cons_dict = dict() - current_sos_dict = dict() + new_vars = [] + old_vars = [] + new_params = [] + old_params = [] + new_cons = [] + old_cons = [] + old_sos = [] + new_sos = [] + current_vars_dict = {} + current_cons_dict = {} + current_sos_dict = {} timer.start('vars') if self._only_child_vars and ( config.check_for_new_or_removed_vars or config.update_vars @@ -1417,7 +1402,7 @@ def update(self, timer: HierarchicalTimer = None): timer.stop('vars') timer.start('params') if config.check_for_new_or_removed_params: - current_params_dict = dict() + current_params_dict = {} for p in self._model.component_objects(Param, descend_into=True): if p.mutable: for _p in p.values(): @@ -1482,11 +1467,11 @@ def update(self, timer: HierarchicalTimer = None): new_cons_set = set(new_cons) new_sos_set = set(new_sos) new_vars_set = set(id(v) for v in new_vars) - cons_to_remove_and_add = dict() + cons_to_remove_and_add = {} need_to_set_objective = False if config.update_constraints: - cons_to_update = list() - sos_to_update = list() + cons_to_update = [] + sos_to_update = [] for c in current_cons_dict.keys(): if c not in new_cons_set: cons_to_update.append(c) @@ -1524,7 +1509,7 @@ def update(self, timer: HierarchicalTimer = None): timer.stop('cons') timer.start('vars') if self._only_child_vars and config.update_vars: - vars_to_check = list() + vars_to_check = [] for v_id, v in current_vars_dict.items(): if v_id not in new_vars_set: vars_to_check.append(v) @@ -1532,7 +1517,7 @@ def update(self, timer: HierarchicalTimer = None): end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] if config.update_vars: - vars_to_update = list() + vars_to_update = [] for v in vars_to_check: _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] if lb is not v._lb: @@ -1557,7 +1542,7 @@ def update(self, timer: HierarchicalTimer = None): timer.stop('cons') timer.start('named expressions') if config.update_named_expressions: - cons_to_update = list() + cons_to_update = [] for c, expr_list in self._named_expressions.items(): if c in new_cons_set: continue @@ -1599,12 +1584,13 @@ def update(self, timer: HierarchicalTimer = None): legacy_termination_condition_map = { TerminationCondition.unknown: LegacyTerminationCondition.unknown, TerminationCondition.maxTimeLimit: LegacyTerminationCondition.maxTimeLimit, - TerminationCondition.maxIterations: LegacyTerminationCondition.maxIterations, + TerminationCondition.iterationLimit: LegacyTerminationCondition.maxIterations, TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, - TerminationCondition.ok: LegacyTerminationCondition.optimal, + TerminationCondition.convergenceCriteriaSatisfied: LegacyTerminationCondition.optimal, TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, - TerminationCondition.infeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.provenInfeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.locallyInfeasible: LegacyTerminationCondition.infeasible, TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, TerminationCondition.error: LegacyTerminationCondition.error, TerminationCondition.interrupted: LegacyTerminationCondition.resourceInterrupt, @@ -1615,12 +1601,13 @@ def update(self, timer: HierarchicalTimer = None): legacy_solver_status_map = { TerminationCondition.unknown: LegacySolverStatus.unknown, TerminationCondition.maxTimeLimit: LegacySolverStatus.aborted, - TerminationCondition.maxIterations: LegacySolverStatus.aborted, + TerminationCondition.iterationLimit: LegacySolverStatus.aborted, TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, TerminationCondition.minStepLength: LegacySolverStatus.error, - TerminationCondition.ok: LegacySolverStatus.ok, + TerminationCondition.convergenceCriteriaSatisfied: LegacySolverStatus.ok, TerminationCondition.unbounded: LegacySolverStatus.error, - TerminationCondition.infeasible: LegacySolverStatus.error, + TerminationCondition.provenInfeasible: LegacySolverStatus.error, + TerminationCondition.locallyInfeasible: LegacySolverStatus.error, TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, TerminationCondition.error: LegacySolverStatus.error, TerminationCondition.interrupted: LegacySolverStatus.aborted, @@ -1631,12 +1618,13 @@ def update(self, timer: HierarchicalTimer = None): legacy_solution_status_map = { TerminationCondition.unknown: LegacySolutionStatus.unknown, TerminationCondition.maxTimeLimit: LegacySolutionStatus.stoppedByLimit, - TerminationCondition.maxIterations: LegacySolutionStatus.stoppedByLimit, + TerminationCondition.iterationLimit: LegacySolutionStatus.stoppedByLimit, TerminationCondition.objectiveLimit: LegacySolutionStatus.stoppedByLimit, TerminationCondition.minStepLength: LegacySolutionStatus.error, - TerminationCondition.ok: LegacySolutionStatus.optimal, + TerminationCondition.convergenceCriteriaSatisfied: LegacySolutionStatus.optimal, TerminationCondition.unbounded: LegacySolutionStatus.unbounded, - TerminationCondition.infeasible: LegacySolutionStatus.infeasible, + TerminationCondition.provenInfeasible: LegacySolutionStatus.infeasible, + TerminationCondition.locallyInfeasible: LegacySolutionStatus.infeasible, TerminationCondition.infeasibleOrUnbounded: LegacySolutionStatus.unsure, TerminationCondition.error: LegacySolutionStatus.error, TerminationCondition.interrupted: LegacySolutionStatus.error, @@ -1644,7 +1632,7 @@ def update(self, timer: HierarchicalTimer = None): } -class LegacySolverInterface(object): +class LegacySolverInterface(): def solve( self, model: _BlockData, @@ -1683,7 +1671,7 @@ def solve( if options is not None: self.options = options - results: Results = super(LegacySolverInterface, self).solve(model) + results: Results = super().solve(model) legacy_results = LegacySolverResults() legacy_soln = LegacySolution() @@ -1760,7 +1748,7 @@ def solve( return legacy_results def available(self, exception_flag=True): - ans = super(LegacySolverInterface, self).available() + ans = super().available() if exception_flag and not ans: raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).') return bool(ans) diff --git a/pyomo/contrib/appsi/build.py b/pyomo/contrib/appsi/build.py index 2a4e7bb785e..6146272978c 100644 --- a/pyomo/contrib/appsi/build.py +++ b/pyomo/contrib/appsi/build.py @@ -80,7 +80,7 @@ def run(self): print("Building in '%s'" % tmpdir) os.chdir(tmpdir) try: - super(appsi_build_ext, self).run() + super().run() if not self.inplace: library = glob.glob("build/*/appsi_cmodel.*")[0] target = os.path.join( @@ -117,7 +117,7 @@ def run(self): pybind11.setup_helpers.MACOS = original_pybind11_setup_helpers_macos -class AppsiBuilder(object): +class AppsiBuilder(): def __call__(self, parallel): return build_appsi() diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index 5cbac7c81e3..6d2cce76925 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -24,8 +24,8 @@ def main(plot=True, n_points=200): # write a for loop to vary the value of parameter p from 1 to 10 p_values = [float(i) for i in np.linspace(1, 10, n_points)] - obj_values = list() - x_values = list() + obj_values = [] + x_values = [] timer = HierarchicalTimer() # create a timer for some basic profiling timer.start('p loop') for p_val in p_values: diff --git a/pyomo/contrib/appsi/examples/tests/test_examples.py b/pyomo/contrib/appsi/examples/tests/test_examples.py index d2c88224a7d..ffcecaf0c5f 100644 --- a/pyomo/contrib/appsi/examples/tests/test_examples.py +++ b/pyomo/contrib/appsi/examples/tests/test_examples.py @@ -1,5 +1,5 @@ from pyomo.contrib.appsi.examples import getting_started -import pyomo.common.unittest as unittest +from pyomo.common import unittest import pyomo.environ as pe from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.contrib import appsi diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 92a0e0c8cbc..22badd83d12 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -35,7 +35,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(IntervalConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -62,14 +62,14 @@ def __init__( class IntervalTightener(PersistentBase): def __init__(self): - super(IntervalTightener, self).__init__() + super().__init__() self._config = IntervalConfig() self._cmodel = None - self._var_map = dict() - self._con_map = dict() - self._param_map = dict() - self._rvar_map = dict() - self._rcon_map = dict() + self._var_map = {} + self._con_map = {} + self._param_map = {} + self._rvar_map = {} + self._rcon_map = {} self._pyomo_expr_types = cmodel.PyomoExprTypes() self._symbolic_solver_labels: bool = False self._symbol_map = SymbolMap() @@ -254,7 +254,7 @@ def _update_pyomo_var_bounds(self): self._vars[v_id] = (_v, _lb, cv_ub, _fixed, _domain, _value) def _deactivate_satisfied_cons(self): - cons_to_deactivate = list() + cons_to_deactivate = [] if self.config.deactivate_satisfied_constraints: for c, cc in self._con_map.items(): if not cc.active: diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 6fd01fb9149..84a38ec3cdb 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -42,7 +42,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(CbcConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -66,12 +66,12 @@ def __init__( class Cbc(PersistentSolver): def __init__(self, only_child_vars=False): self._config = CbcConfig() - self._solver_options = dict() + self._solver_options = {} self._writer = LPWriter(only_child_vars=only_child_vars) self._filename = None - self._dual_sol = dict() - self._primal_sol = dict() - self._reduced_costs = dict() + self._dual_sol = {} + self._primal_sol = {} + self._reduced_costs = {} self._last_results_object: Optional[Results] = None def available(self): @@ -261,9 +261,9 @@ def _parse_soln(self): first_var_line = ndx last_var_line = len(all_lines) - 1 - self._dual_sol = dict() - self._primal_sol = dict() - self._reduced_costs = dict() + self._dual_sol = {} + self._primal_sol = {} + self._reduced_costs = {} symbol_map = self._writer.symbol_map @@ -362,7 +362,7 @@ def _check_and_escape_options(): yield tmp_k, tmp_v cmd = [str(config.executable)] - action_options = list() + action_options = [] if config.time_limit is not None: cmd.extend(['-sec', str(config.time_limit)]) cmd.extend(['-timeMode', 'elapsed']) diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index f007573639b..47042586d0b 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -38,7 +38,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(CplexConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -59,7 +59,7 @@ def __init__( class CplexResults(Results): def __init__(self, solver): - super(CplexResults, self).__init__() + super().__init__() self.wallclock_time = None self.solution_loader = PersistentSolutionLoader(solver=solver) @@ -69,7 +69,7 @@ class Cplex(PersistentSolver): def __init__(self, only_child_vars=False): self._config = CplexConfig() - self._solver_options = dict() + self._solver_options = {} self._writer = LPWriter(only_child_vars=only_child_vars) self._filename = None self._last_results_object: Optional[CplexResults] = None @@ -400,7 +400,7 @@ def get_duals( con_names = self._cplex_model.linear_constraints.get_names() dual_values = self._cplex_model.solution.get_dual_values() else: - con_names = list() + con_names = [] for con in cons_to_load: orig_name = symbol_map.byObject[id(con)] if con.equality: @@ -412,7 +412,7 @@ def get_duals( con_names.append(orig_name + '_ub') dual_values = self._cplex_model.solution.get_dual_values(con_names) - res = dict() + res = {} for name, val in zip(con_names, dual_values): orig_name = name[:-3] if orig_name == 'obj_const_con': diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 999b542ad70..23a87e06f1c 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -62,7 +62,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(GurobiConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -95,12 +95,12 @@ def get_primals(self, vars_to_load=None, solution_number=0): class GurobiResults(Results): def __init__(self, solver): - super(GurobiResults, self).__init__() + super().__init__() self.wallclock_time = None self.solution_loader = GurobiSolutionLoader(solver=solver) -class _MutableLowerBound(object): +class _MutableLowerBound(): def __init__(self, expr): self.var = None self.expr = expr @@ -109,7 +109,7 @@ def update(self): self.var.setAttr('lb', value(self.expr)) -class _MutableUpperBound(object): +class _MutableUpperBound(): def __init__(self, expr): self.var = None self.expr = expr @@ -118,7 +118,7 @@ def update(self): self.var.setAttr('ub', value(self.expr)) -class _MutableLinearCoefficient(object): +class _MutableLinearCoefficient(): def __init__(self): self.expr = None self.var = None @@ -129,7 +129,7 @@ def update(self): self.gurobi_model.chgCoeff(self.con, self.var, value(self.expr)) -class _MutableRangeConstant(object): +class _MutableRangeConstant(): def __init__(self): self.lhs_expr = None self.rhs_expr = None @@ -145,7 +145,7 @@ def update(self): slack.ub = rhs_val - lhs_val -class _MutableConstant(object): +class _MutableConstant(): def __init__(self): self.expr = None self.con = None @@ -154,7 +154,7 @@ def update(self): self.con.rhs = value(self.expr) -class _MutableQuadraticConstraint(object): +class _MutableQuadraticConstraint(): def __init__( self, gurobi_model, gurobi_con, constant, linear_coefs, quadratic_coefs ): @@ -189,7 +189,7 @@ def get_updated_rhs(self): return value(self.constant.expr) -class _MutableObjective(object): +class _MutableObjective(): def __init__(self, gurobi_model, constant, linear_coefs, quadratic_coefs): self.gurobi_model = gurobi_model self.constant = constant @@ -217,7 +217,7 @@ def get_updated_expression(self): return gurobi_expr -class _MutableQuadraticCoefficient(object): +class _MutableQuadraticCoefficient(): def __init__(self): self.expr = None self.var1 = None @@ -233,21 +233,21 @@ class Gurobi(PersistentBase, PersistentSolver): _num_instances = 0 def __init__(self, only_child_vars=False): - super(Gurobi, self).__init__(only_child_vars=only_child_vars) + super().__init__(only_child_vars=only_child_vars) self._num_instances += 1 self._config = GurobiConfig() - self._solver_options = dict() + self._solver_options = {} self._solver_model = None self._symbol_map = SymbolMap() self._labeler = None - self._pyomo_var_to_solver_var_map = dict() - self._pyomo_con_to_solver_con_map = dict() - self._solver_con_to_pyomo_con_map = dict() - self._pyomo_sos_to_solver_sos_map = dict() + self._pyomo_var_to_solver_var_map = {} + self._pyomo_con_to_solver_con_map = {} + self._solver_con_to_pyomo_con_map = {} + self._pyomo_sos_to_solver_sos_map = {} self._range_constraints = OrderedSet() - self._mutable_helpers = dict() - self._mutable_bounds = dict() - self._mutable_quadratic_helpers = dict() + self._mutable_helpers = {} + self._mutable_bounds = {} + self._mutable_quadratic_helpers = {} self._mutable_objective = None self._needs_updated = True self._callback = None @@ -448,12 +448,12 @@ def _process_domain_and_bounds( return lb, ub, vtype def _add_variables(self, variables: List[_GeneralVarData]): - var_names = list() - vtypes = list() - lbs = list() - ubs = list() - mutable_lbs = dict() - mutable_ubs = dict() + var_names = [] + vtypes = [] + lbs = [] + ubs = [] + mutable_lbs = {} + mutable_ubs = {} for ndx, var in enumerate(variables): varname = self._symbol_map.getSymbol(var, self._labeler) lb, ub, vtype = self._process_domain_and_bounds( @@ -519,8 +519,8 @@ def set_instance(self, model): self.set_objective(None) def _get_expr_from_pyomo_expr(self, expr): - mutable_linear_coefficients = list() - mutable_quadratic_coefficients = list() + mutable_linear_coefficients = [] + mutable_quadratic_coefficients = [] repn = generate_standard_repn(expr, quadratic=True, compute_values=False) degree = repn.polynomial_degree() @@ -530,7 +530,7 @@ def _get_expr_from_pyomo_expr(self, expr): ) if len(repn.linear_vars) > 0: - linear_coef_vals = list() + linear_coef_vals = [] for ndx, coef in enumerate(repn.linear_coefs): if not is_constant(coef): mutable_linear_coefficient = _MutableLinearCoefficient() @@ -824,8 +824,8 @@ def _set_objective(self, obj): sense = gurobipy.GRB.MINIMIZE gurobi_expr = 0 repn_constant = 0 - mutable_linear_coefficients = list() - mutable_quadratic_coefficients = list() + mutable_linear_coefficients = [] + mutable_quadratic_coefficients = [] else: if obj.sense == minimize: sense = gurobipy.GRB.MINIMIZE @@ -1047,7 +1047,7 @@ def get_duals(self, cons_to_load=None): con_map = self._pyomo_con_to_solver_con_map reverse_con_map = self._solver_con_to_pyomo_con_map - dual = dict() + dual = {} if cons_to_load is None: linear_cons_to_load = self._solver_model.getConstrs() @@ -1090,7 +1090,7 @@ def get_slacks(self, cons_to_load=None): con_map = self._pyomo_con_to_solver_con_map reverse_con_map = self._solver_con_to_pyomo_con_map - slack = dict() + slack = {} gurobi_range_con_vars = OrderedSet(self._solver_model.getVars()) - OrderedSet( self._pyomo_var_to_solver_var_map.values() @@ -1140,7 +1140,7 @@ def get_slacks(self, cons_to_load=None): def update(self, timer: HierarchicalTimer = None): if self._needs_updated: self._update_gurobi_model() - super(Gurobi, self).update(timer=timer) + super().update(timer=timer) self._update_gurobi_model() def _update_gurobi_model(self): diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 23f49a057a7..528fb2f3087 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -47,7 +47,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(HighsConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -71,7 +71,7 @@ def __init__(self, solver): self.solution_loader = PersistentSolutionLoader(solver=solver) -class _MutableVarBounds(object): +class _MutableVarBounds(): def __init__(self, lower_expr, upper_expr, pyomo_var_id, var_map, highs): self.pyomo_var_id = pyomo_var_id self.lower_expr = lower_expr @@ -86,7 +86,7 @@ def update(self): self.highs.changeColBounds(col_ndx, lb, ub) -class _MutableLinearCoefficient(object): +class _MutableLinearCoefficient(): def __init__(self, pyomo_con, pyomo_var_id, con_map, var_map, expr, highs): self.expr = expr self.highs = highs @@ -101,7 +101,7 @@ def update(self): self.highs.changeCoeff(row_ndx, col_ndx, value(self.expr)) -class _MutableObjectiveCoefficient(object): +class _MutableObjectiveCoefficient(): def __init__(self, pyomo_var_id, var_map, expr, highs): self.expr = expr self.highs = highs @@ -113,7 +113,7 @@ def update(self): self.highs.changeColCost(col_ndx, value(self.expr)) -class _MutableObjectiveOffset(object): +class _MutableObjectiveOffset(): def __init__(self, expr, highs): self.expr = expr self.highs = highs @@ -122,7 +122,7 @@ def update(self): self.highs.changeObjectiveOffset(value(self.expr)) -class _MutableConstraintBounds(object): +class _MutableConstraintBounds(): def __init__(self, lower_expr, upper_expr, pyomo_con, con_map, highs): self.lower_expr = lower_expr self.upper_expr = upper_expr @@ -147,14 +147,14 @@ class Highs(PersistentBase, PersistentSolver): def __init__(self, only_child_vars=False): super().__init__(only_child_vars=only_child_vars) self._config = HighsConfig() - self._solver_options = dict() + self._solver_options = {} self._solver_model = None - self._pyomo_var_to_solver_var_map = dict() - self._pyomo_con_to_solver_con_map = dict() - self._solver_con_to_pyomo_con_map = dict() - self._mutable_helpers = dict() - self._mutable_bounds = dict() - self._objective_helpers = list() + self._pyomo_var_to_solver_var_map = {} + self._pyomo_con_to_solver_con_map = {} + self._solver_con_to_pyomo_con_map = {} + self._mutable_helpers = {} + self._mutable_bounds = {} + self._objective_helpers = [] self._last_results_object: Optional[HighsResults] = None self._sol = None @@ -301,10 +301,10 @@ def _add_variables(self, variables: List[_GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - lbs = list() - ubs = list() - indices = list() - vtypes = list() + lbs = [] + ubs = [] + indices = [] + vtypes = [] current_num_vars = len(self._pyomo_var_to_solver_var_map) for v in variables: @@ -360,11 +360,11 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() current_num_cons = len(self._pyomo_con_to_solver_con_map) - lbs = list() - ubs = list() - starts = list() - var_indices = list() - coef_values = list() + lbs = [] + ubs = [] + starts = [] + var_indices = [] + coef_values = [] for con in cons: repn = generate_standard_repn( @@ -390,7 +390,7 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): highs=self._solver_model, ) if con not in self._mutable_helpers: - self._mutable_helpers[con] = list() + self._mutable_helpers[con] = [] self._mutable_helpers[con].append(mutable_linear_coefficient) if coef_val == 0: continue @@ -445,7 +445,7 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - indices_to_remove = list() + indices_to_remove = [] for con in cons: con_ndx = self._pyomo_con_to_solver_con_map.pop(con) del self._solver_con_to_pyomo_con_map[con_ndx] @@ -455,7 +455,7 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): len(indices_to_remove), np.array(indices_to_remove) ) con_ndx = 0 - new_con_map = dict() + new_con_map = {} for c in self._pyomo_con_to_solver_con_map.keys(): new_con_map[c] = con_ndx con_ndx += 1 @@ -474,7 +474,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - indices_to_remove = list() + indices_to_remove = [] for v in variables: v_id = id(v) v_ndx = self._pyomo_var_to_solver_var_map.pop(v_id) @@ -484,7 +484,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): len(indices_to_remove), np.array(indices_to_remove) ) v_ndx = 0 - new_var_map = dict() + new_var_map = {} for v_id in self._pyomo_var_to_solver_var_map.keys(): new_var_map[v_id] = v_ndx v_ndx += 1 @@ -497,10 +497,10 @@ def _update_variables(self, variables: List[_GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - indices = list() - lbs = list() - ubs = list() - vtypes = list() + indices = [] + lbs = [] + ubs = [] + vtypes = [] for v in variables: v_id = id(v) @@ -541,7 +541,7 @@ def _set_objective(self, obj): n = len(self._pyomo_var_to_solver_var_map) indices = np.arange(n) costs = np.zeros(n, dtype=np.double) - self._objective_helpers = list() + self._objective_helpers = [] if obj is None: sense = highspy.ObjSense.kMinimize self._solver_model.changeObjectiveOffset(0) @@ -692,7 +692,7 @@ def get_primals(self, vars_to_load=None, solution_number=0): res = ComponentMap() if vars_to_load is None: - var_ids_to_load = list() + var_ids_to_load = [] for v, ref_info in self._referenced_variables.items(): using_cons, using_sos, using_obj = ref_info if using_cons or using_sos or (using_obj is not None): @@ -737,7 +737,7 @@ def get_duals(self, cons_to_load=None): 'check the termination condition.' ) - res = dict() + res = {} if cons_to_load is None: cons_to_load = list(self._pyomo_con_to_solver_con_map.keys()) @@ -756,7 +756,7 @@ def get_slacks(self, cons_to_load=None): 'check the termination condition.' ) - res = dict() + res = {} if cons_to_load is None: cons_to_load = list(self._pyomo_con_to_solver_con_map.keys()) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 8c0716c6e1e..c03f6e145f3 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -45,7 +45,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super(IpoptConfig, self).__init__( + super().__init__( description=description, doc=doc, implicit=implicit, @@ -129,10 +129,10 @@ def __init__( class Ipopt(PersistentSolver): def __init__(self, only_child_vars=False): self._config = IpoptConfig() - self._solver_options = dict() + self._solver_options = {} self._writer = NLWriter(only_child_vars=only_child_vars) self._filename = None - self._dual_sol = dict() + self._dual_sol = {} self._primal_sol = ComponentMap() self._reduced_costs = ComponentMap() self._last_results_object: Optional[Results] = None @@ -347,7 +347,7 @@ def _parse_sol(self): + n_rc_lower ] - self._dual_sol = dict() + self._dual_sol = {} self._primal_sol = ComponentMap() self._reduced_costs = ComponentMap() diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index de82b211092..2727cf2313b 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -1,5 +1,5 @@ from pyomo.common.errors import PyomoException -import pyomo.common.unittest as unittest +from pyomo.common import unittest import pyomo.environ as pe from pyomo.contrib.appsi.solvers.gurobi import Gurobi from pyomo.contrib.appsi.base import TerminationCondition diff --git a/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py index 6b86deaa535..ce73b94ab74 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py @@ -1,5 +1,5 @@ import pyomo.environ as pe -import pyomo.common.unittest as unittest +from pyomo.common import unittest from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.common.gsl import find_GSL diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index ec9f397bdc4..82db5f6286f 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1,6 +1,6 @@ import pyomo.environ as pe from pyomo.common.dependencies import attempt_import -import pyomo.common.unittest as unittest +from pyomo.common import unittest parameterized, param_available = attempt_import('parameterized') parameterized = parameterized.parameterized @@ -68,7 +68,7 @@ def _load_tests(solver_list, only_child_vars_list): - res = list() + res = [] for solver_name, solver in solver_list: for child_var_option in only_child_vars_list: test_name = f"{solver_name}_only_child_vars_{child_var_option}" @@ -979,8 +979,8 @@ def test_time_limit( m.x = pe.Var(m.jobs, m.tasks, bounds=(0, 1)) random.seed(0) - coefs = list() - lin_vars = list() + coefs = [] + lin_vars = [] for j in m.jobs: for t in m.tasks: coefs.append(random.uniform(0, 10)) diff --git a/pyomo/contrib/appsi/tests/test_base.py b/pyomo/contrib/appsi/tests/test_base.py index 0d67ca4d01a..82a04b29e56 100644 --- a/pyomo/contrib/appsi/tests/test_base.py +++ b/pyomo/contrib/appsi/tests/test_base.py @@ -37,16 +37,16 @@ def test_results(self): m.c1 = pe.Constraint(expr=m.x == 1) m.c2 = pe.Constraint(expr=m.y == 2) - primals = dict() + primals = {} primals[id(m.x)] = (m.x, 1) primals[id(m.y)] = (m.y, 2) - duals = dict() + duals = {} duals[m.c1] = 3 duals[m.c2] = 4 - rc = dict() + rc = {} rc[id(m.x)] = (m.x, 5) rc[id(m.y)] = (m.y, 6) - slacks = dict() + slacks = {} slacks[m.c1] = 7 slacks[m.c2] = 8 diff --git a/pyomo/contrib/appsi/tests/test_interval.py b/pyomo/contrib/appsi/tests/test_interval.py index 7963cc31665..0924e3bbeed 100644 --- a/pyomo/contrib/appsi/tests/test_interval.py +++ b/pyomo/contrib/appsi/tests/test_interval.py @@ -1,5 +1,5 @@ from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available -import pyomo.common.unittest as unittest +from pyomo.common import unittest import math from pyomo.contrib.fbbt.tests.test_interval import IntervalTestBase @@ -7,7 +7,7 @@ @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') class TestInterval(IntervalTestBase, unittest.TestCase): def setUp(self): - super(TestInterval, self).setUp() + super().setUp() self.add = cmodel.py_interval_add self.sub = cmodel.py_interval_sub self.mul = cmodel.py_interval_mul diff --git a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py index 9027080f08c..bfbbf5aecdf 100644 --- a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py +++ b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py @@ -4,10 +4,10 @@ class _VarAndNamedExprCollector(ExpressionValueVisitor): def __init__(self): - self.named_expressions = dict() - self.variables = dict() - self.fixed_vars = dict() - self._external_functions = dict() + self.named_expressions = {} + self.variables = {} + self.fixed_vars = {} + self._external_functions = {} def visit(self, node, values): pass diff --git a/pyomo/contrib/appsi/writers/config.py b/pyomo/contrib/appsi/writers/config.py index 7a7faadaabe..4376b9284fa 100644 --- a/pyomo/contrib/appsi/writers/config.py +++ b/pyomo/contrib/appsi/writers/config.py @@ -1,3 +1,3 @@ -class WriterConfig(object): +class WriterConfig(): def __init__(self): self.symbolic_solver_labels = False diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 8a76fa5f9eb..6a4a4ab2ff7 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -17,7 +17,7 @@ class LPWriter(PersistentBase): def __init__(self, only_child_vars=False): - super(LPWriter, self).__init__(only_child_vars=only_child_vars) + super().__init__(only_child_vars=only_child_vars) self._config = WriterConfig() self._writer = None self._symbol_map = SymbolMap() @@ -25,11 +25,11 @@ def __init__(self, only_child_vars=False): self._con_labeler = None self._param_labeler = None self._obj_labeler = None - self._pyomo_var_to_solver_var_map = dict() - self._pyomo_con_to_solver_con_map = dict() - self._solver_var_to_pyomo_var_map = dict() - self._solver_con_to_pyomo_con_map = dict() - self._pyomo_param_to_solver_param_map = dict() + self._pyomo_var_to_solver_var_map = {} + self._pyomo_con_to_solver_con_map = {} + self._solver_var_to_pyomo_var_map = {} + self._solver_con_to_pyomo_con_map = {} + self._pyomo_param_to_solver_param_map = {} self._expr_types = None @property @@ -89,7 +89,7 @@ def _add_params(self, params: List[_ParamData]): self._pyomo_param_to_solver_param_map[id(p)] = cp def _add_constraints(self, cons: List[_GeneralConstraintData]): - cmodel.process_lp_constraints(cons, self) + cmodel.process_lp_constraints() def _add_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) != 0: diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 9c739fd6ebb..d0bb443508d 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -20,18 +20,18 @@ class NLWriter(PersistentBase): def __init__(self, only_child_vars=False): - super(NLWriter, self).__init__(only_child_vars=only_child_vars) + super().__init__(only_child_vars=only_child_vars) self._config = WriterConfig() self._writer = None self._symbol_map = SymbolMap() self._var_labeler = None self._con_labeler = None self._param_labeler = None - self._pyomo_var_to_solver_var_map = dict() - self._pyomo_con_to_solver_con_map = dict() - self._solver_var_to_pyomo_var_map = dict() - self._solver_con_to_pyomo_con_map = dict() - self._pyomo_param_to_solver_param_map = dict() + self._pyomo_var_to_solver_var_map = {} + self._pyomo_con_to_solver_con_map = {} + self._solver_var_to_pyomo_var_map = {} + self._solver_con_to_pyomo_con_map = {} + self._pyomo_param_to_solver_param_map = {} self._expr_types = None @property @@ -172,8 +172,8 @@ def update_params(self): def _set_objective(self, obj: _GeneralObjectiveData): if obj is None: const = cmodel.Constant(0) - lin_vars = list() - lin_coef = list() + lin_vars = [] + lin_coef = [] nonlin = cmodel.Constant(0) sense = 0 else: @@ -240,7 +240,7 @@ def write(self, model: _BlockData, filename: str, timer: HierarchicalTimer = Non timer.stop('write file') def update(self, timer: HierarchicalTimer = None): - super(NLWriter, self).update(timer=timer) + super().update(timer=timer) self._set_pyomo_amplfunc_env() def get_ordered_vars(self): diff --git a/pyomo/contrib/appsi/writers/tests/test_nl_writer.py b/pyomo/contrib/appsi/writers/tests/test_nl_writer.py index 3b61a5901c3..297bc3d7617 100644 --- a/pyomo/contrib/appsi/writers/tests/test_nl_writer.py +++ b/pyomo/contrib/appsi/writers/tests/test_nl_writer.py @@ -1,4 +1,4 @@ -import pyomo.common.unittest as unittest +from pyomo.common import unittest from pyomo.common.tempfiles import TempfileManager import pyomo.environ as pe from pyomo.contrib import appsi From 561e5bc461e76ab973d05e5240b4e4c9194919cb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 15 Aug 2023 13:29:23 -0600 Subject: [PATCH 0020/3044] Revert accidental list/dict changes --- pyomo/contrib/appsi/base.py | 10 +++++----- pyomo/contrib/appsi/build.py | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 805e96dacf1..86268b04dbd 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -409,7 +409,7 @@ def get_duals( 'for the given problem type.' ) if cons_to_load is None: - duals = {self._duals} + duals = dict(self._duals) else: duals = {} for c in cons_to_load: @@ -426,7 +426,7 @@ def get_slacks( 'for the given problem type.' ) if cons_to_load is None: - slacks = {self._slacks} + slacks = dict(self._slacks) else: slacks = {} for c in cons_to_load: @@ -1116,7 +1116,7 @@ def _check_for_new_vars(self, variables: List[_GeneralVarData]): v_id = id(v) if v_id not in self._referenced_variables: new_vars[v_id] = v - self.add_variables([new_vars.values()]) + self.add_variables(list(new_vars.values())) def _check_to_remove_vars(self, variables: List[_GeneralVarData]): vars_to_remove = {} @@ -1125,7 +1125,7 @@ def _check_to_remove_vars(self, variables: List[_GeneralVarData]): ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: vars_to_remove[v_id] = v - self.remove_variables([vars_to_remove.values()]) + self.remove_variables(list(vars_to_remove.values())) def add_constraints(self, cons: List[_GeneralConstraintData]): all_fixed_vars = {} @@ -1224,7 +1224,7 @@ def add_block(self, block): if p.mutable: for _p in p.values(): param_dict[id(_p)] = _p - self.add_params([param_dict.values()]) + self.add_params(list(param_dict.values())) if self._only_child_vars: self.add_variables( list( diff --git a/pyomo/contrib/appsi/build.py b/pyomo/contrib/appsi/build.py index 6146272978c..37826cf85fb 100644 --- a/pyomo/contrib/appsi/build.py +++ b/pyomo/contrib/appsi/build.py @@ -63,8 +63,7 @@ def get_appsi_extension(in_setup=False, appsi_root=None): def build_appsi(args=[]): print('\n\n**** Building APPSI ****') - import setuptools - from distutils.dist import Distribution + from setuptools.dist import Distribution from pybind11.setup_helpers import build_ext import pybind11.setup_helpers from pyomo.common.envvar import PYOMO_CONFIG_DIR From 154518ea537944ab6aa976d00fc0544b0a929764 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 15 Aug 2023 14:01:43 -0600 Subject: [PATCH 0021/3044] Fix references to TerminationCondition.ok --- pyomo/contrib/appsi/base.py | 2 +- .../contrib/appsi/examples/getting_started.py | 2 +- pyomo/contrib/appsi/solvers/cbc.py | 10 ++-- pyomo/contrib/appsi/solvers/cplex.py | 4 +- pyomo/contrib/appsi/solvers/gurobi.py | 4 +- pyomo/contrib/appsi/solvers/highs.py | 6 +- pyomo/contrib/appsi/solvers/ipopt.py | 10 ++-- .../solvers/tests/test_gurobi_persistent.py | 2 +- .../solvers/tests/test_persistent_solvers.py | 55 +++++++++---------- 9 files changed, 47 insertions(+), 48 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 86268b04dbd..d85e1ef9dfe 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -478,7 +478,7 @@ class Results(): >>> opt = appsi.solvers.Ipopt() >>> opt.config.load_solution = False >>> results = opt.solve(m) #doctest:+SKIP - >>> if results.termination_condition == appsi.base.TerminationCondition.ok: #doctest:+SKIP + >>> if results.termination_condition == appsi.base.TerminationCondition.convergenceCriteriaSatisfied: #doctest:+SKIP ... print('optimal solution found: ', results.best_feasible_objective) #doctest:+SKIP ... results.solution_loader.load_vars() #doctest:+SKIP ... print('the optimal value of x is ', m.x.value) #doctest:+SKIP diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index 6d2cce76925..04092601c91 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -31,7 +31,7 @@ def main(plot=True, n_points=200): for p_val in p_values: m.p.value = p_val res = opt.solve(m, timer=timer) - assert res.termination_condition == appsi.base.TerminationCondition.ok + assert res.termination_condition == appsi.base.TerminationCondition.convergenceCriteriaSatisfied obj_values.append(res.best_feasible_objective) opt.load_vars([m.x]) x_values.append(m.x.value) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 84a38ec3cdb..74b9aa8ba8e 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -232,7 +232,7 @@ def _parse_soln(self): termination_line = all_lines[0].lower() obj_val = None if termination_line.startswith('optimal'): - results.termination_condition = TerminationCondition.ok + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied obj_val = float(termination_line.split()[-1]) elif 'infeasible' in termination_line: results.termination_condition = TerminationCondition.infeasible @@ -307,7 +307,7 @@ def _parse_soln(self): self._reduced_costs[v_id] = (v, -rc_val) if ( - results.termination_condition == TerminationCondition.ok + results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied and self.config.load_solution ): for v_id, (v, val) in self._primal_sol.items(): @@ -316,7 +316,7 @@ def _parse_soln(self): results.best_feasible_objective = None else: results.best_feasible_objective = obj_val - elif results.termination_condition == TerminationCondition.ok: + elif results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied: if self._writer.get_active_objective() is None: results.best_feasible_objective = None else: @@ -451,7 +451,7 @@ def get_duals(self, cons_to_load=None): if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.ok + != TerminationCondition.convergenceCriteriaSatisfied ): raise RuntimeError( 'Solver does not currently have valid duals. Please ' @@ -469,7 +469,7 @@ def get_reduced_costs( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.ok + != TerminationCondition.convergenceCriteriaSatisfied ): raise RuntimeError( 'Solver does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 47042586d0b..c459effe325 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -284,7 +284,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): status = cpxprob.solution.get_status() if status in [1, 101, 102]: - results.termination_condition = TerminationCondition.ok + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif status in [2, 40, 118, 133, 134]: results.termination_condition = TerminationCondition.unbounded elif status in [4, 119, 134]: @@ -336,7 +336,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): 'results.best_feasible_objective before loading a solution.' ) else: - if results.termination_condition != TerminationCondition.ok: + if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 23a87e06f1c..2f79a8515a3 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -874,7 +874,7 @@ def _postsolve(self, timer: HierarchicalTimer): if status == grb.LOADED: # problem is loaded, but no solution results.termination_condition = TerminationCondition.unknown elif status == grb.OPTIMAL: # optimal - results.termination_condition = TerminationCondition.ok + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif status == grb.INFEASIBLE: results.termination_condition = TerminationCondition.infeasible elif status == grb.INF_OR_UNBD: @@ -925,7 +925,7 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start('load solution') if config.load_solution: if gprob.SolCount > 0: - if results.termination_condition != TerminationCondition.ok: + if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 528fb2f3087..cd17f5d90e8 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -610,7 +610,7 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == highspy.HighsModelStatus.kModelEmpty: results.termination_condition = TerminationCondition.unknown elif status == highspy.HighsModelStatus.kOptimal: - results.termination_condition = TerminationCondition.ok + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif status == highspy.HighsModelStatus.kInfeasible: results.termination_condition = TerminationCondition.infeasible elif status == highspy.HighsModelStatus.kUnboundedOrInfeasible: @@ -633,7 +633,7 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start('load solution') self._sol = highs.getSolution() has_feasible_solution = False - if results.termination_condition == TerminationCondition.ok: + if results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied: has_feasible_solution = True elif results.termination_condition in { TerminationCondition.objectiveLimit, @@ -645,7 +645,7 @@ def _postsolve(self, timer: HierarchicalTimer): if config.load_solution: if has_feasible_solution: - if results.termination_condition != TerminationCondition.ok: + if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index c03f6e145f3..e19a68f6d85 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -303,7 +303,7 @@ def _parse_sol(self): termination_line = all_lines[1] if 'Optimal Solution Found' in termination_line: - results.termination_condition = TerminationCondition.ok + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif 'Problem may be infeasible' in termination_line: results.termination_condition = TerminationCondition.infeasible elif 'problem might be unbounded' in termination_line: @@ -384,7 +384,7 @@ def _parse_sol(self): self._reduced_costs[var] = 0 if ( - results.termination_condition == TerminationCondition.ok + results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied and self.config.load_solution ): for v, val in self._primal_sol.items(): @@ -395,7 +395,7 @@ def _parse_sol(self): results.best_feasible_objective = value( self._writer.get_active_objective().expr ) - elif results.termination_condition == TerminationCondition.ok: + elif results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied: if self._writer.get_active_objective() is None: results.best_feasible_objective = None else: @@ -526,7 +526,7 @@ def get_duals( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.ok + != TerminationCondition.convergenceCriteriaSatisfied ): raise RuntimeError( 'Solver does not currently have valid duals. Please ' @@ -544,7 +544,7 @@ def get_reduced_costs( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.ok + != TerminationCondition.convergenceCriteriaSatisfied ): raise RuntimeError( 'Solver does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index 2727cf2313b..fcff8916b5b 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -160,7 +160,7 @@ def test_lp(self): res = opt.solve(self.m) self.assertAlmostEqual(x + y, res.best_feasible_objective) self.assertAlmostEqual(x + y, res.best_objective_bound) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertTrue(res.best_feasible_objective is not None) self.assertAlmostEqual(x, self.m.x.value) self.assertAlmostEqual(y, self.m.y.value) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 82db5f6286f..9e7abf04e08 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -9,7 +9,6 @@ from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression -import os numpy, numpy_available = attempt_import('numpy') import random @@ -91,7 +90,7 @@ def test_remove_variable_and_objective( m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, 2) del m.x @@ -99,7 +98,7 @@ def test_remove_variable_and_objective( m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) @@ -159,13 +158,13 @@ def test_range_constraint( m.obj = pe.Objective(expr=m.x) m.c = pe.Constraint(expr=(-1, m.x, 1)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, -1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, 1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) @@ -182,7 +181,7 @@ def test_reduced_costs( m.y = pe.Var(bounds=(-2, 2)) m.obj = pe.Objective(expr=3 * m.x + 4 * m.y) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, -2) rc = opt.get_reduced_costs() @@ -200,13 +199,13 @@ def test_reduced_costs2( m.x = pe.Var(bounds=(-1, 1)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, -1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, 1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) @@ -236,7 +235,7 @@ def test_param_changes( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -274,7 +273,7 @@ def test_immutable_param( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -308,7 +307,7 @@ def test_equality( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -348,7 +347,7 @@ def test_linear_expression( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) @@ -378,7 +377,7 @@ def test_no_objective( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertEqual(res.best_feasible_objective, None) @@ -407,7 +406,7 @@ def test_add_remove_cons( m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -418,7 +417,7 @@ def test_add_remove_cons( m.c3 = pe.Constraint(expr=m.y >= a3 * m.x + b3) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b3 - b1) / (a1 - a3)) self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -430,7 +429,7 @@ def test_add_remove_cons( del m.c3 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -456,7 +455,7 @@ def test_results_infeasible( res = opt.solve(m) opt.config.load_solution = False res = opt.solve(m) - self.assertNotEqual(res.termination_condition, TerminationCondition.ok) + self.assertNotEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) if opt_class is Ipopt: acceptable_termination_conditions = { TerminationCondition.infeasible, @@ -748,7 +747,7 @@ def test_mutable_param_with_range( m.c2.value = float(c2) m.obj.sense = sense res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) if sense is pe.minimize: self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) @@ -785,7 +784,7 @@ def test_add_and_remove_vars( opt.update_config.check_for_new_or_removed_vars = False opt.config.load_solution = False res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) opt.load_vars() self.assertAlmostEqual(m.y.value, -1) m.x = pe.Var() @@ -799,7 +798,7 @@ def test_add_and_remove_vars( opt.add_variables([m.x]) opt.add_constraints([m.c1, m.c2]) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) opt.load_vars() self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @@ -808,7 +807,7 @@ def test_add_and_remove_vars( opt.remove_variables([m.x]) m.x.value = None res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) opt.load_vars() self.assertEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, -1) @@ -869,7 +868,7 @@ def test_with_numpy( ) ) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @@ -1211,14 +1210,14 @@ def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]) m.b.c2 = pe.Constraint(expr=m.y >= -m.x) res = opt.solve(m.b) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(res.best_feasible_objective, 1) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, 1) m.x.setlb(0) res = opt.solve(m.b) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(res.best_feasible_objective, 2) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 2) @@ -1241,7 +1240,7 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolver] m.c4 = pe.Constraint(expr=m.y >= -m.z + 1) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(res.best_feasible_objective, 1) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) @@ -1251,7 +1250,7 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolver] del m.c3 del m.c4 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(res.best_feasible_objective, 0) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) @@ -1273,12 +1272,12 @@ def test_bug_1(self, name: str, opt_class: Type[PersistentSolver], only_child_va m.c = pe.Constraint(expr=m.y >= m.p * m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(res.best_feasible_objective, 0) m.p.value = 1 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.ok) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) self.assertAlmostEqual(res.best_feasible_objective, 3) From 7f0f73f5b7bd49a31a8bbcf101e4177bfeee03b4 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 16 Aug 2023 09:41:54 -0600 Subject: [PATCH 0022/3044] SAVING STATE --- doc/OnlineDocs/conf.py | 7 ------- .../developer_reference/solvers.rst | 20 +++++++++++++++++++ pyomo/common/config.py | 2 +- pyomo/contrib/appsi/base.py | 2 +- pyomo/contrib/appsi/solvers/cbc.py | 2 +- pyomo/contrib/appsi/solvers/cplex.py | 2 +- pyomo/contrib/appsi/solvers/gurobi.py | 4 ++-- pyomo/contrib/appsi/solvers/highs.py | 6 +++--- pyomo/contrib/appsi/solvers/ipopt.py | 6 +++--- .../solvers/tests/test_persistent_solvers.py | 2 +- 10 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 doc/OnlineDocs/developer_reference/solvers.rst diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 43df1263f82..d8939cf61dd 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -146,13 +146,6 @@ html_theme = 'sphinx_rtd_theme' -# Force HTML4: If we don't explicitly force HTML4, then the background -# of the Parameters/Returns/Return type headers is shaded the same as the -# method prototype (tested 15 April 21 with Sphinx=3.5.4 and -# sphinx-rtd-theme=0.5.2). -html4_writer = True -# html5_writer = True - if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst new file mode 100644 index 00000000000..374ba4fbee8 --- /dev/null +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -0,0 +1,20 @@ +Solver Interfaces +================= + +Pyomo offers interfaces into multiple solvers, both commercial and open source. + + +Termination Conditions +---------------------- + +Pyomo offers a standard set of termination conditions to map to solver +returns. + +.. currentmodule:: pyomo.contrib.appsi.base + +.. autosummary:: + + TerminationCondition + + + diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 1b44d555b91..7bbcd693a72 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -2019,7 +2019,7 @@ def generate_documentation( ) if item_body is not None: deprecation_warning( - f"Overriding 'item_body' by passing strings to " + f"Overriding '{item_body}' by passing strings to " "generate_documentation is deprecated. Create an instance of a " "StringConfigFormatter and pass it as the 'format' argument.", version='6.6.0', diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index d85e1ef9dfe..5116b59322a 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -486,7 +486,7 @@ class Results(): ... print('sub-optimal but feasible solution found: ', results.best_feasible_objective) #doctest:+SKIP ... results.solution_loader.load_vars(vars_to_load=[m.x]) #doctest:+SKIP ... print('The value of x in the feasible solution is ', m.x.value) #doctest:+SKIP - ... elif results.termination_condition in {appsi.base.TerminationCondition.maxIterations, appsi.base.TerminationCondition.maxTimeLimit}: #doctest:+SKIP + ... elif results.termination_condition in {appsi.base.TerminationCondition.iterationLimit, appsi.base.TerminationCondition.maxTimeLimit}: #doctest:+SKIP ... print('No feasible solution was found. The best lower bound found was ', results.best_objective_bound) #doctest:+SKIP ... else: #doctest:+SKIP ... print('The following termination condition was encountered: ', results.termination_condition) #doctest:+SKIP diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 74b9aa8ba8e..12e7555535e 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -242,7 +242,7 @@ def _parse_soln(self): results.termination_condition = TerminationCondition.maxTimeLimit obj_val = float(termination_line.split()[-1]) elif termination_line.startswith('stopped on iterations'): - results.termination_condition = TerminationCondition.maxIterations + results.termination_condition = TerminationCondition.iterationLimit obj_val = float(termination_line.split()[-1]) else: results.termination_condition = TerminationCondition.unknown diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index c459effe325..08d6b11fc76 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -292,7 +292,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): elif status in [3, 103]: results.termination_condition = TerminationCondition.infeasible elif status in [10]: - results.termination_condition = TerminationCondition.maxIterations + results.termination_condition = TerminationCondition.iterationLimit elif status in [11, 25, 107, 131]: results.termination_condition = TerminationCondition.maxTimeLimit else: diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 2f79a8515a3..dfe3b441cd8 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -884,9 +884,9 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == grb.CUTOFF: results.termination_condition = TerminationCondition.objectiveLimit elif status == grb.ITERATION_LIMIT: - results.termination_condition = TerminationCondition.maxIterations + results.termination_condition = TerminationCondition.iterationLimit elif status == grb.NODE_LIMIT: - results.termination_condition = TerminationCondition.maxIterations + results.termination_condition = TerminationCondition.iterationLimit elif status == grb.TIME_LIMIT: results.termination_condition = TerminationCondition.maxTimeLimit elif status == grb.SOLUTION_LIMIT: diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index cd17f5d90e8..4ec4ebeffb1 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -612,7 +612,7 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == highspy.HighsModelStatus.kOptimal: results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif status == highspy.HighsModelStatus.kInfeasible: - results.termination_condition = TerminationCondition.infeasible + results.termination_condition = TerminationCondition.provenInfeasible elif status == highspy.HighsModelStatus.kUnboundedOrInfeasible: results.termination_condition = TerminationCondition.infeasibleOrUnbounded elif status == highspy.HighsModelStatus.kUnbounded: @@ -624,7 +624,7 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == highspy.HighsModelStatus.kTimeLimit: results.termination_condition = TerminationCondition.maxTimeLimit elif status == highspy.HighsModelStatus.kIterationLimit: - results.termination_condition = TerminationCondition.maxIterations + results.termination_condition = TerminationCondition.iterationLimit elif status == highspy.HighsModelStatus.kUnknown: results.termination_condition = TerminationCondition.unknown else: @@ -637,7 +637,7 @@ def _postsolve(self, timer: HierarchicalTimer): has_feasible_solution = True elif results.termination_condition in { TerminationCondition.objectiveLimit, - TerminationCondition.maxIterations, + TerminationCondition.iterationLimit, TerminationCondition.maxTimeLimit, }: if self._sol.value_valid: diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index e19a68f6d85..6580c9a004a 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -16,7 +16,7 @@ from pyomo.common.collections import ComponentMap from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions -from typing import Optional, Sequence, NoReturn, List, Mapping +from typing import Optional, Sequence, List, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.block import _BlockData @@ -305,11 +305,11 @@ def _parse_sol(self): if 'Optimal Solution Found' in termination_line: results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif 'Problem may be infeasible' in termination_line: - results.termination_condition = TerminationCondition.infeasible + results.termination_condition = TerminationCondition.locallyInfeasible elif 'problem might be unbounded' in termination_line: results.termination_condition = TerminationCondition.unbounded elif 'Maximum Number of Iterations Exceeded' in termination_line: - results.termination_condition = TerminationCondition.maxIterations + results.termination_condition = TerminationCondition.iterationLimit elif 'Maximum CPU Time Exceeded' in termination_line: results.termination_condition = TerminationCondition.maxTimeLimit else: diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 9e7abf04e08..23236827a11 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1014,7 +1014,7 @@ def test_time_limit( if type(opt) is Cbc: # I can't figure out why CBC is reporting max iter... self.assertIn( res.termination_condition, - {TerminationCondition.maxIterations, TerminationCondition.maxTimeLimit}, + {TerminationCondition.iterationLimit, TerminationCondition.maxTimeLimit}, ) else: self.assertEqual( From cda74ae09f83345a78ac513e0db42dd55e7f97a5 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 17 Aug 2023 08:53:57 -0600 Subject: [PATCH 0023/3044] Begin documentation for sovlers --- doc/OnlineDocs/developer_reference/index.rst | 1 + doc/OnlineDocs/developer_reference/solvers.rst | 7 +++---- pyomo/common/config.py | 2 +- pyomo/contrib/appsi/solvers/ipopt.py | 5 ++--- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/index.rst b/doc/OnlineDocs/developer_reference/index.rst index 8c29150015c..0f0f636abee 100644 --- a/doc/OnlineDocs/developer_reference/index.rst +++ b/doc/OnlineDocs/developer_reference/index.rst @@ -12,3 +12,4 @@ scripts using Pyomo. config.rst deprecation.rst expressions/index.rst + solvers.rst diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 374ba4fbee8..d48e270cc7c 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -10,11 +10,10 @@ Termination Conditions Pyomo offers a standard set of termination conditions to map to solver returns. -.. currentmodule:: pyomo.contrib.appsi.base +.. currentmodule:: pyomo.contrib.appsi -.. autosummary:: - - TerminationCondition +.. autoclass:: pyomo.contrib.appsi.base.TerminationCondition + :noindex: diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 7bbcd693a72..61e4f682a2a 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -2019,7 +2019,7 @@ def generate_documentation( ) if item_body is not None: deprecation_warning( - f"Overriding '{item_body}' by passing strings to " + "Overriding 'item_body' by passing strings to " "generate_documentation is deprecated. Create an instance of a " "StringConfigFormatter and pass it as the 'format' argument.", version='6.6.0', diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 6580c9a004a..68dcdae2492 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -297,9 +297,8 @@ def _parse_sol(self): solve_cons = self._writer.get_ordered_cons() results = Results() - f = open(self._filename + '.sol', 'r') - all_lines = list(f.readlines()) - f.close() + with open(self._filename + '.sol', 'r') as f: + all_lines = list(f.readlines()) termination_line = all_lines[1] if 'Optimal Solution Found' in termination_line: From bb6bf5e67b31820760325f2f5f3e9c6c83094d3f Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Fri, 25 Aug 2023 13:58:03 -0400 Subject: [PATCH 0024/3044] add highs support --- pyomo/contrib/mindtpy/config_options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/mindtpy/config_options.py b/pyomo/contrib/mindtpy/config_options.py index ed0c86baae9..713ab539660 100644 --- a/pyomo/contrib/mindtpy/config_options.py +++ b/pyomo/contrib/mindtpy/config_options.py @@ -538,7 +538,7 @@ def _add_subsolver_configs(CONFIG): 'cplex_persistent', 'appsi_cplex', 'appsi_gurobi', - # 'appsi_highs', TODO: feasibility pump now fails with appsi_highs #2951 + 'appsi_highs' ] ), description='MIP subsolver name', @@ -620,7 +620,7 @@ def _add_subsolver_configs(CONFIG): 'cplex_persistent', 'appsi_cplex', 'appsi_gurobi', - # 'appsi_highs', + 'appsi_highs', ] ), description='MIP subsolver for regularization problem', From da8a56e837fb43dec321af088fd498c980429db5 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Fri, 25 Aug 2023 14:03:07 -0400 Subject: [PATCH 0025/3044] change test mip solver to highs --- pyomo/contrib/mindtpy/tests/test_mindtpy.py | 2 +- pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py | 2 +- pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy.py b/pyomo/contrib/mindtpy/tests/test_mindtpy.py index e872eccc670..4efd9493b8a 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy.py @@ -56,7 +56,7 @@ QCP_model._generate_model() extreme_model_list = [LP_model.model, QCP_model.model] -required_solvers = ('ipopt', 'glpk') +required_solvers = ('ipopt', 'appsi_highs') if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py index b5bfbe62553..95516af11fd 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py @@ -12,7 +12,7 @@ from pyomo.environ import SolverFactory, value from pyomo.opt import TerminationCondition -required_solvers = ('ipopt', 'glpk') +required_solvers = ('ipopt', 'appsi_highs') if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py index 697a63d17c8..18b7a420674 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py @@ -17,8 +17,7 @@ from pyomo.contrib.mindtpy.tests.feasibility_pump1 import FeasPump1 from pyomo.contrib.mindtpy.tests.feasibility_pump2 import FeasPump2 -required_solvers = ('ipopt', 'cplex') -# TODO: 'appsi_highs' will fail here. +required_solvers = ('ipopt', 'appsi_highs') if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: From 95f4cc2a281fdf235800ae9d82bc27c11d6f1f18 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Fri, 25 Aug 2023 14:13:24 -0400 Subject: [PATCH 0026/3044] black format --- pyomo/contrib/mindtpy/config_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/config_options.py b/pyomo/contrib/mindtpy/config_options.py index 713ab539660..2769d336e31 100644 --- a/pyomo/contrib/mindtpy/config_options.py +++ b/pyomo/contrib/mindtpy/config_options.py @@ -538,7 +538,7 @@ def _add_subsolver_configs(CONFIG): 'cplex_persistent', 'appsi_cplex', 'appsi_gurobi', - 'appsi_highs' + 'appsi_highs', ] ), description='MIP subsolver name', From b0f6747208fe453bc459f22c58035fc5169b2cd8 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 10:17:50 -0600 Subject: [PATCH 0027/3044] Fix termination conditions --- .../contrib/appsi/solvers/tests/test_persistent_solvers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 23236827a11..df2eccd5eef 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -458,12 +458,14 @@ def test_results_infeasible( self.assertNotEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) if opt_class is Ipopt: acceptable_termination_conditions = { - TerminationCondition.infeasible, + TerminationCondition.provenInfeasible, + TerminationCondition.locallyInfeasible, TerminationCondition.unbounded, } else: acceptable_termination_conditions = { - TerminationCondition.infeasible, + TerminationCondition.provenInfeasible, + TerminationCondition.locallyInfeasible, TerminationCondition.infeasibleOrUnbounded, } self.assertIn(res.termination_condition, acceptable_termination_conditions) From 2b7d62f18c091aa264b0ee8b2e48301a85c86a18 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 10:22:46 -0600 Subject: [PATCH 0028/3044] Modify termination conditions for solvers --- pyomo/contrib/appsi/solvers/cbc.py | 2 +- pyomo/contrib/appsi/solvers/cplex.py | 2 +- pyomo/contrib/appsi/solvers/gurobi.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 12e7555535e..9a4f098d08b 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -235,7 +235,7 @@ def _parse_soln(self): results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied obj_val = float(termination_line.split()[-1]) elif 'infeasible' in termination_line: - results.termination_condition = TerminationCondition.infeasible + results.termination_condition = TerminationCondition.provenInfeasible elif 'unbounded' in termination_line: results.termination_condition = TerminationCondition.unbounded elif termination_line.startswith('stopped on time'): diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 08d6b11fc76..9c7683b81cf 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -290,7 +290,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): elif status in [4, 119, 134]: results.termination_condition = TerminationCondition.infeasibleOrUnbounded elif status in [3, 103]: - results.termination_condition = TerminationCondition.infeasible + results.termination_condition = TerminationCondition.provenInfeasible elif status in [10]: results.termination_condition = TerminationCondition.iterationLimit elif status in [11, 25, 107, 131]: diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index dfe3b441cd8..339c001369e 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -876,7 +876,7 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == grb.OPTIMAL: # optimal results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif status == grb.INFEASIBLE: - results.termination_condition = TerminationCondition.infeasible + results.termination_condition = TerminationCondition.provenInfeasible elif status == grb.INF_OR_UNBD: results.termination_condition = TerminationCondition.infeasibleOrUnbounded elif status == grb.UNBOUNDED: From 3b94b79df27f20fbd5d7276e2e4e60d538ef3344 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 10:53:07 -0600 Subject: [PATCH 0029/3044] Update params for Solver base class --- pyomo/contrib/appsi/base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 5116b59322a..f50a1e6135f 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -20,7 +20,7 @@ from .utils.get_objective import get_objective from .utils.collect_vars_and_named_exprs import collect_vars_and_named_exprs from pyomo.common.timing import HierarchicalTimer -from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat +from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat, NonNegativeInt from pyomo.common.errors import ApplicationError from pyomo.opt.base import SolverFactory as LegacySolverFactory from pyomo.common.factory import Factory @@ -177,6 +177,8 @@ class InterfaceConfig(ConfigDict): report_timing: bool - wrapper If True, then some timing information will be printed at the end of the solve. + threads: integer - sent to solver + Number of threads to be used by a solver. """ def __init__( @@ -199,6 +201,7 @@ def __init__( self.declare('load_solution', ConfigValue(domain=bool)) self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) self.declare('report_timing', ConfigValue(domain=bool)) + self.declare('threads', ConfigValue(domain=NonNegativeInt, default=None)) self.time_limit: Optional[float] = self.declare( 'time_limit', ConfigValue(domain=NonNegativeFloat) @@ -744,7 +747,7 @@ def __str__(self): @abc.abstractmethod def solve( - self, model: _BlockData, tee=False, timer: HierarchicalTimer = None, **kwargs + self, model: _BlockData, tee: bool = False, timer: HierarchicalTimer = None, **kwargs ) -> Results: """ Solve a Pyomo model. From 49f226d247b30bf8071c091171075dce0a7153c5 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 11:13:07 -0600 Subject: [PATCH 0030/3044] Change stream_solver back to tee --- pyomo/contrib/appsi/base.py | 16 +++++++--------- pyomo/contrib/appsi/solvers/cbc.py | 2 +- pyomo/contrib/appsi/solvers/cplex.py | 2 +- pyomo/contrib/appsi/solvers/gurobi.py | 4 ++-- pyomo/contrib/appsi/solvers/highs.py | 2 +- pyomo/contrib/appsi/solvers/ipopt.py | 2 +- .../solvers/tests/test_persistent_solvers.py | 2 +- 7 files changed, 14 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index f50a1e6135f..62c87c5a0c9 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -165,7 +165,7 @@ class InterfaceConfig(ConfigDict): ---------- time_limit: float - sent to solver Time limit for the solver - stream_solver: bool - wrapper + tee: bool - wrapper If True, then the solver log goes to stdout load_solution: bool - wrapper If False, then the values of the primal variables will not be @@ -197,7 +197,7 @@ def __init__( visibility=visibility, ) - self.declare('stream_solver', ConfigValue(domain=bool)) + self.declare('tee', ConfigValue(domain=bool)) self.declare('load_solution', ConfigValue(domain=bool)) self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) self.declare('report_timing', ConfigValue(domain=bool)) @@ -206,7 +206,7 @@ def __init__( self.time_limit: Optional[float] = self.declare( 'time_limit', ConfigValue(domain=NonNegativeFloat) ) - self.stream_solver: bool = False + self.tee: bool = False self.load_solution: bool = True self.symbolic_solver_labels: bool = False self.report_timing: bool = False @@ -454,7 +454,7 @@ def get_reduced_costs( return rc -class Results(): +class Results: """ Attributes ---------- @@ -747,7 +747,7 @@ def __str__(self): @abc.abstractmethod def solve( - self, model: _BlockData, tee: bool = False, timer: HierarchicalTimer = None, **kwargs + self, model: _BlockData, timer: HierarchicalTimer = None, **kwargs ) -> Results: """ Solve a Pyomo model. @@ -756,8 +756,6 @@ def solve( ---------- model: _BlockData The Pyomo model to be solved - tee: bool - Show solver output in the terminal timer: HierarchicalTimer An option timer for reporting timing **kwargs @@ -1635,7 +1633,7 @@ def update(self, timer: HierarchicalTimer = None): } -class LegacySolverInterface(): +class LegacySolverInterface: def solve( self, model: _BlockData, @@ -1653,7 +1651,7 @@ def solve( ): original_config = self.config self.config = self.config() - self.config.stream_solver = tee + self.config.tee = tee self.config.load_solution = load_solutions self.config.symbolic_solver_labels = symbolic_solver_labels self.config.time_limit = timelimit diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 9a4f098d08b..35071ab17ea 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -383,7 +383,7 @@ def _check_and_escape_options(): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.stream_solver: + if self.config.tee: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 9c7683b81cf..7f9844fc21d 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -245,7 +245,7 @@ def _apply_solver(self, timer: HierarchicalTimer): log_stream = LogStream( level=self.config.log_level, logger=self.config.solver_output_logger ) - if config.stream_solver: + if config.tee: def _process_stream(arg): sys.stdout.write(arg) diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 339c001369e..3f8eab638b0 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -353,7 +353,7 @@ def _solve(self, timer: HierarchicalTimer): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.stream_solver: + if self.config.tee: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: @@ -1384,7 +1384,7 @@ def set_callback(self, func=None): >>> _c = _add_cut(4) # this is an arbitrary choice >>> >>> opt = appsi.solvers.Gurobi() - >>> opt.config.stream_solver = True + >>> opt.config.tee = True >>> opt.set_instance(m) # doctest:+SKIP >>> opt.gurobi_options['PreCrush'] = 1 >>> opt.gurobi_options['LazyConstraints'] = 1 diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 4ec4ebeffb1..e5c43d27c8d 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -211,7 +211,7 @@ def _solve(self, timer: HierarchicalTimer): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.stream_solver: + if self.config.tee: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 68dcdae2492..da42fc0be41 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -430,7 +430,7 @@ def _apply_solver(self, timer: HierarchicalTimer): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.stream_solver: + if self.config.tee: ostreams.append(sys.stdout) cmd = [ diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index df2eccd5eef..2d579611761 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -368,7 +368,7 @@ def test_no_objective( m.b2 = pe.Param(mutable=True) m.c1 = pe.Constraint(expr=m.y == m.a1 * m.x + m.b1) m.c2 = pe.Constraint(expr=m.y == m.a2 * m.x + m.b2) - opt.config.stream_solver = True + opt.config.tee = True params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] for a1, a2, b1, b2 in params_to_test: From 50f64526a7187061c696433bd09c6765bfbbd822 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 11:26:06 -0600 Subject: [PATCH 0031/3044] Isolate tests to just APPSI for speed --- .github/workflows/test_branches.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 99d5f7fc1a8..d1d6f73870d 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -598,8 +598,7 @@ jobs: run: | $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ - pyomo `pwd`/pyomo-model-libraries \ - `pwd`/examples/pyomobook --junitxml="TEST-pyomo.xml" + pyomo/contrib/appsi --junitxml="TEST-pyomo.xml" - name: Run Pyomo MPI tests if: matrix.mpi != 0 From 2e3ad3ad20389c8d656855f3bf27074276174b99 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 12:09:02 -0600 Subject: [PATCH 0032/3044] Remove kwargs for now --- pyomo/contrib/appsi/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 62c87c5a0c9..5ce5421ee86 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -747,7 +747,7 @@ def __str__(self): @abc.abstractmethod def solve( - self, model: _BlockData, timer: HierarchicalTimer = None, **kwargs + self, model: _BlockData, timer: HierarchicalTimer = None, ) -> Results: """ Solve a Pyomo model. From 0a0a67da17eb25c76761e59d45b5ed5c9b432ec2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 12:21:28 -0600 Subject: [PATCH 0033/3044] Per Michael Bynum, remove cbc and cplex tests as C++ lp_writer won't be sustained --- .../solvers/tests/test_persistent_solvers.py | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 2d579611761..135f36d3695 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -6,7 +6,7 @@ parameterized = parameterized.parameterized from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available -from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs +from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Highs from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression @@ -21,14 +21,12 @@ all_solvers = [ ('gurobi', Gurobi), ('ipopt', Ipopt), - ('cplex', Cplex), - ('cbc', Cbc), ('highs', Highs), ] -mip_solvers = [('gurobi', Gurobi), ('cplex', Cplex), ('cbc', Cbc), ('highs', Highs)] +mip_solvers = [('gurobi', Gurobi), ('highs', Highs)] nlp_solvers = [('ipopt', Ipopt)] -qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt), ('cplex', Cplex)] -miqcqp_solvers = [('gurobi', Gurobi), ('cplex', Cplex)] +qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] +miqcqp_solvers = [('gurobi', Gurobi)] only_child_vars_options = [True, False] @@ -1013,15 +1011,9 @@ def test_time_limit( opt.config.time_limit = 0 opt.config.load_solution = False res = opt.solve(m) - if type(opt) is Cbc: # I can't figure out why CBC is reporting max iter... - self.assertIn( - res.termination_condition, - {TerminationCondition.iterationLimit, TerminationCondition.maxTimeLimit}, - ) - else: - self.assertEqual( - res.termination_condition, TerminationCondition.maxTimeLimit - ) + self.assertEqual( + res.termination_condition, TerminationCondition.maxTimeLimit + ) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_objective_changes( From af5ee141afa06d3c7fafdffd8b01d362c604c826 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 12:27:00 -0600 Subject: [PATCH 0034/3044] Turn off test_examples; uses cplex --- pyomo/contrib/appsi/examples/tests/test_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/examples/tests/test_examples.py b/pyomo/contrib/appsi/examples/tests/test_examples.py index ffcecaf0c5f..db6e2910b77 100644 --- a/pyomo/contrib/appsi/examples/tests/test_examples.py +++ b/pyomo/contrib/appsi/examples/tests/test_examples.py @@ -5,7 +5,7 @@ from pyomo.contrib import appsi -@unittest.skipUnless(cmodel_available, 'appsi extensions are not available') +@unittest.skip('Currently turning off cplex support') class TestExamples(unittest.TestCase): def test_getting_started(self): try: From 3028138884b3b9125ffe6bebfc38058a8759c70c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 12:39:33 -0600 Subject: [PATCH 0035/3044] Allow macOS IPOPT download; update ubuntu download; try using ipopt instead of cplex for getting_started --- pyomo/contrib/appsi/examples/getting_started.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index 04092601c91..d907283f663 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -16,7 +16,7 @@ def main(plot=True, n_points=200): m.c1 = pe.Constraint(expr=m.y >= (m.x + 1) ** 2) m.c2 = pe.Constraint(expr=m.y >= (m.x - m.p) ** 2) - opt = appsi.solvers.Cplex() # create an APPSI solver interface + opt = appsi.solvers.Ipopt() # create an APPSI solver interface opt.config.load_solution = False # modify the config options # change how automatic updates are handled opt.update_config.check_for_new_or_removed_vars = False From ad7d9e028f9fdd68f7057fb4f45aea78dc0ebf75 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 12:43:44 -0600 Subject: [PATCH 0036/3044] Allow macOS IPOPT download; update ubuntu download; try using ipopt instead of cplex for getting_started --- .github/workflows/test_branches.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index d1d6f73870d..9640171a7c1 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -390,10 +390,10 @@ jobs: IPOPT_TAR=${DOWNLOAD_DIR}/ipopt.tar.gz if test ! -e $IPOPT_TAR; then echo "...downloading Ipopt" - if test "${{matrix.TARGET}}" == osx; then - echo "IDAES Ipopt not available on OSX" - exit 0 - fi + # if test "${{matrix.TARGET}}" == osx; then + # echo "IDAES Ipopt not available on OSX" + # exit 0 + # fi URL=https://github.com/IDAES/idaes-ext RELEASE=$(curl --max-time 150 --retry 8 \ -L -s -H 'Accept: application/json' ${URL}/releases/latest) @@ -401,7 +401,11 @@ jobs: URL=${URL}/releases/download/$VER if test "${{matrix.TARGET}}" == linux; then curl --max-time 150 --retry 8 \ - -L $URL/idaes-solvers-ubuntu2004-x86_64.tar.gz \ + -L $URL/idaes-solvers-ubuntu2204-x86_64.tar.gz \ + > $IPOPT_TAR + elseif test "${{matrix.TARGET}}" == osx; then + curl --max-time 150 --retry 8 \ + -L $URL/idaes-solvers-darwin-x86_64.tar.gz \ > $IPOPT_TAR else curl --max-time 150 --retry 8 \ From cbe8b9902070df5c7ac0809bf4ae7965bb400299 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 12:56:46 -0600 Subject: [PATCH 0037/3044] Fix broken bash --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 9640171a7c1..ed0f9350206 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -403,7 +403,7 @@ jobs: curl --max-time 150 --retry 8 \ -L $URL/idaes-solvers-ubuntu2204-x86_64.tar.gz \ > $IPOPT_TAR - elseif test "${{matrix.TARGET}}" == osx; then + elif test "${{matrix.TARGET}}" == osx; then curl --max-time 150 --retry 8 \ -L $URL/idaes-solvers-darwin-x86_64.tar.gz \ > $IPOPT_TAR From 2d55e658b6b1a9effe5983dae21af75bcce319ed Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 13:06:22 -0600 Subject: [PATCH 0038/3044] Change untar command --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index ed0f9350206..76bdcfd0587 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -414,7 +414,7 @@ jobs: fi fi cd $IPOPT_DIR - tar -xzi < $IPOPT_TAR + tar -xzf < $IPOPT_TAR echo "" echo "$IPOPT_DIR" ls -l $IPOPT_DIR From 630662942f30c2fd70bca7e1392c532057e2ab8c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 13:11:14 -0600 Subject: [PATCH 0039/3044] Trying a different untar command --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 76bdcfd0587..0ac37747a65 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -414,7 +414,7 @@ jobs: fi fi cd $IPOPT_DIR - tar -xzf < $IPOPT_TAR + tar -xz < $IPOPT_TAR echo "" echo "$IPOPT_DIR" ls -l $IPOPT_DIR From 0e1c8c750981f76828d5cb3283b1468c9f439541 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 13:16:41 -0600 Subject: [PATCH 0040/3044] Turning on examples test --- pyomo/contrib/appsi/examples/tests/test_examples.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/appsi/examples/tests/test_examples.py b/pyomo/contrib/appsi/examples/tests/test_examples.py index db6e2910b77..2ea089a8cc6 100644 --- a/pyomo/contrib/appsi/examples/tests/test_examples.py +++ b/pyomo/contrib/appsi/examples/tests/test_examples.py @@ -5,14 +5,14 @@ from pyomo.contrib import appsi -@unittest.skip('Currently turning off cplex support') +@unittest.skipUnless(cmodel_available, 'appsi extensions are not available') class TestExamples(unittest.TestCase): def test_getting_started(self): try: import numpy as np except: raise unittest.SkipTest('numpy is not available') - opt = appsi.solvers.Cplex() + opt = appsi.solvers.Ipopt() if not opt.available(): - raise unittest.SkipTest('cplex is not available') + raise unittest.SkipTest('ipopt is not available') getting_started.main(plot=False, n_points=10) From d2b91264275b5611d7b577dc91d2b6c38e5b7db1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 14:37:13 -0600 Subject: [PATCH 0041/3044] Change test_examples skipping --- pyomo/contrib/appsi/examples/tests/test_examples.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/examples/tests/test_examples.py b/pyomo/contrib/appsi/examples/tests/test_examples.py index 2ea089a8cc6..7c577366c41 100644 --- a/pyomo/contrib/appsi/examples/tests/test_examples.py +++ b/pyomo/contrib/appsi/examples/tests/test_examples.py @@ -1,17 +1,16 @@ from pyomo.contrib.appsi.examples import getting_started from pyomo.common import unittest -import pyomo.environ as pe +from pyomo.common.dependencies import attempt_import from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.contrib import appsi +numpy, numpy_available = attempt_import('numpy') + @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') +@unittest.skipUnless(numpy_available, 'numpy is not available') class TestExamples(unittest.TestCase): def test_getting_started(self): - try: - import numpy as np - except: - raise unittest.SkipTest('numpy is not available') opt = appsi.solvers.Ipopt() if not opt.available(): raise unittest.SkipTest('ipopt is not available') From d0cb12542307dbe0dae2320853c1054a552e9dd1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 29 Aug 2023 16:38:28 -0600 Subject: [PATCH 0042/3044] SAVE POINT: starting to move items out of appsi --- pyomo/contrib/appsi/base.py | 31 +- pyomo/solver/__init__.py | 14 + pyomo/solver/base.py | 1240 +++++++++++++++++++++++++++ pyomo/solver/config.py | 270 ++++++ pyomo/solver/solution.py | 256 ++++++ pyomo/solver/tests/test_base.py | 0 pyomo/solver/tests/test_config.py | 0 pyomo/solver/tests/test_solution.py | 0 pyomo/solver/tests/test_util.py | 0 pyomo/solver/util.py | 23 + 10 files changed, 1830 insertions(+), 4 deletions(-) create mode 100644 pyomo/solver/__init__.py create mode 100644 pyomo/solver/base.py create mode 100644 pyomo/solver/config.py create mode 100644 pyomo/solver/solution.py create mode 100644 pyomo/solver/tests/test_base.py create mode 100644 pyomo/solver/tests/test_config.py create mode 100644 pyomo/solver/tests/test_solution.py create mode 100644 pyomo/solver/tests/test_util.py create mode 100644 pyomo/solver/util.py diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 5ce5421ee86..aa17489c4d5 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -165,7 +165,7 @@ class InterfaceConfig(ConfigDict): ---------- time_limit: float - sent to solver Time limit for the solver - tee: bool - wrapper + tee: bool If True, then the solver log goes to stdout load_solution: bool - wrapper If False, then the values of the primal variables will not be @@ -720,7 +720,7 @@ def __init__( # End game: we are not supporting a `has_Xcapability` interface (CHECK BOOK). -class Solver(abc.ABC): +class SolverBase(abc.ABC): class Availability(enum.IntEnum): NotFound = 0 BadVersion = -1 @@ -747,7 +747,7 @@ def __str__(self): @abc.abstractmethod def solve( - self, model: _BlockData, timer: HierarchicalTimer = None, + self, model: _BlockData, timer: HierarchicalTimer = None, **kwargs ) -> Results: """ Solve a Pyomo model. @@ -827,8 +827,31 @@ def is_persistent(self): """ return False +# In a non-persistent interface, when the solver dies, it'll return +# everthing it is going to return. And when you parse, you'll parse everything, +# whether or not you needed it. -class PersistentSolver(Solver): +# In a persistent interface, if all I really care about is to keep going +# until the objective gets better. I may not need to parse the dual or state +# vars. If I only need the objective, why waste time bringing that extra +# cruft back? Why not just return what you ask for when you ask for it? + +# All the `gets_` is to be able to retrieve from the solver. Because the +# persistent interface is still holding onto the solver's definition, +# it saves time. Also helps avoid assuming that you are loading a model. + +# There is an argument whether or not the get methods could be called load. + +# For non-persistent, there are also questions about how we load everything. +# We tend to just load everything because it might disappear otherwise. +# In the file interface, we tend to parse everything, and the option is to turn +# it all off. We still parse everything... + +# IDEAL SITUATION -- +# load_solutions = True -> straight into model; otherwise, into results object + + +class PersistentSolver(SolverBase): def is_persistent(self): return True diff --git a/pyomo/solver/__init__.py b/pyomo/solver/__init__.py new file mode 100644 index 00000000000..64c6452d06d --- /dev/null +++ b/pyomo/solver/__init__.py @@ -0,0 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from . import util +from . import base +from . import solution diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py new file mode 100644 index 00000000000..b6d9e1592cb --- /dev/null +++ b/pyomo/solver/base.py @@ -0,0 +1,1240 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import abc +import enum +from typing import ( + Sequence, + Dict, + Optional, + Mapping, + NoReturn, + List, + Tuple, +) +from pyomo.core.base.constraint import _GeneralConstraintData, Constraint +from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint +from pyomo.core.base.var import _GeneralVarData, Var +from pyomo.core.base.param import _ParamData, Param +from pyomo.core.base.block import _BlockData +from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.common.collections import ComponentMap +from .utils.get_objective import get_objective +from .utils.collect_vars_and_named_exprs import collect_vars_and_named_exprs +from pyomo.common.timing import HierarchicalTimer +from pyomo.common.errors import ApplicationError +from pyomo.opt.base import SolverFactory as LegacySolverFactory +from pyomo.common.factory import Factory +import os +from pyomo.opt.results.results_ import SolverResults as LegacySolverResults +from pyomo.opt.results.solution import ( + Solution as LegacySolution, + SolutionStatus as LegacySolutionStatus, +) +from pyomo.opt.results.solver import ( + TerminationCondition as LegacyTerminationCondition, + SolverStatus as LegacySolverStatus, +) +from pyomo.core.kernel.objective import minimize +from pyomo.core.base import SymbolMap +from .cmodel import cmodel, cmodel_available +from pyomo.core.staleflag import StaleFlagManager +from pyomo.core.expr.numvalue import NumericConstant +from pyomo.solver import ( + SolutionLoader, + SolutionLoaderBase, + UpdateConfig +) + + +class TerminationCondition(enum.Enum): + """ + An enumeration for checking the termination condition of solvers + """ + + """unknown serves as both a default value, and it is used when no other enum member makes sense""" + unknown = 42 + + """The solver exited because the convergence criteria were satisfied""" + convergenceCriteriaSatisfied = 0 + + """The solver exited due to a time limit""" + maxTimeLimit = 1 + + """The solver exited due to an iteration limit""" + iterationLimit = 2 + + """The solver exited due to an objective limit""" + objectiveLimit = 3 + + """The solver exited due to a minimum step length""" + minStepLength = 4 + + """The solver exited because the problem is unbounded""" + unbounded = 5 + + """The solver exited because the problem is proven infeasible""" + provenInfeasible = 6 + + """The solver exited because the problem was found to be locally infeasible""" + locallyInfeasible = 7 + + """The solver exited because the problem is either infeasible or unbounded""" + infeasibleOrUnbounded = 8 + + """The solver exited due to an error""" + error = 9 + + """The solver exited because it was interrupted""" + interrupted = 10 + + """The solver exited due to licensing problems""" + licensingProblems = 11 + + +class SolutionStatus(enum.IntEnum): + """ + An enumeration for interpreting the result of a termination. This describes the designated + status by the solver to be loaded back into the model. + + For now, we are choosing to use IntEnum such that return values are numerically + assigned in increasing order. + """ + + """No (single) solution found; possible that a population of solutions was returned""" + noSolution = 0 + + """Solution point does not satisfy some domains and/or constraints""" + infeasible = 10 + + """Feasible solution identified""" + feasible = 20 + + """Optimal solution identified""" + optimal = 30 + + +class Results: + """ + Attributes + ---------- + termination_condition: TerminationCondition + The reason the solver exited. This is a member of the + TerminationCondition enum. + best_feasible_objective: float + If a feasible solution was found, this is the objective value of + the best solution found. If no feasible solution was found, this is + None. + best_objective_bound: float + The best objective bound found. For minimization problems, this is + the lower bound. For maximization problems, this is the upper bound. + For solvers that do not provide an objective bound, this should be -inf + (minimization) or inf (maximization) + + Here is an example workflow: + + >>> import pyomo.environ as pe + >>> from pyomo.contrib import appsi + >>> m = pe.ConcreteModel() + >>> m.x = pe.Var() + >>> m.obj = pe.Objective(expr=m.x**2) + >>> opt = appsi.solvers.Ipopt() + >>> opt.config.load_solution = False + >>> results = opt.solve(m) #doctest:+SKIP + >>> if results.termination_condition == appsi.base.TerminationCondition.convergenceCriteriaSatisfied: #doctest:+SKIP + ... print('optimal solution found: ', results.best_feasible_objective) #doctest:+SKIP + ... results.solution_loader.load_vars() #doctest:+SKIP + ... print('the optimal value of x is ', m.x.value) #doctest:+SKIP + ... elif results.best_feasible_objective is not None: #doctest:+SKIP + ... print('sub-optimal but feasible solution found: ', results.best_feasible_objective) #doctest:+SKIP + ... results.solution_loader.load_vars(vars_to_load=[m.x]) #doctest:+SKIP + ... print('The value of x in the feasible solution is ', m.x.value) #doctest:+SKIP + ... elif results.termination_condition in {appsi.base.TerminationCondition.iterationLimit, appsi.base.TerminationCondition.maxTimeLimit}: #doctest:+SKIP + ... print('No feasible solution was found. The best lower bound found was ', results.best_objective_bound) #doctest:+SKIP + ... else: #doctest:+SKIP + ... print('The following termination condition was encountered: ', results.termination_condition) #doctest:+SKIP + """ + + def __init__(self): + self.solution_loader: SolutionLoaderBase = SolutionLoader( + None, None, None, None + ) + self.termination_condition: TerminationCondition = TerminationCondition.unknown + self.best_feasible_objective: Optional[float] = None + self.best_objective_bound: Optional[float] = None + + def __str__(self): + s = '' + s += 'termination_condition: ' + str(self.termination_condition) + '\n' + s += 'best_feasible_objective: ' + str(self.best_feasible_objective) + '\n' + s += 'best_objective_bound: ' + str(self.best_objective_bound) + return s + + +class SolverBase(abc.ABC): + class Availability(enum.IntEnum): + NotFound = 0 + BadVersion = -1 + BadLicense = -2 + FullLicense = 1 + LimitedLicense = 2 + NeedsCompiledExtension = -3 + + def __bool__(self): + return self._value_ > 0 + + def __format__(self, format_spec): + # We want general formatting of this Enum to return the + # formatted string value and not the int (which is the + # default implementation from IntEnum) + return format(self.name, format_spec) + + def __str__(self): + # Note: Python 3.11 changed the core enums so that the + # "mixin" type for standard enums overrides the behavior + # specified in __format__. We will override str() here to + # preserve the previous behavior + return self.name + + @abc.abstractmethod + def solve( + self, model: _BlockData, timer: HierarchicalTimer = None, **kwargs + ) -> Results: + """ + Solve a Pyomo model. + + Parameters + ---------- + model: _BlockData + The Pyomo model to be solved + timer: HierarchicalTimer + An option timer for reporting timing + **kwargs + Additional keyword arguments (including solver_options - passthrough options; delivered directly to the solver (with no validation)) + + Returns + ------- + results: Results + A results object + """ + pass + + @abc.abstractmethod + def available(self): + """Test if the solver is available on this system. + + Nominally, this will return True if the solver interface is + valid and can be used to solve problems and False if it cannot. + + Note that for licensed solvers there are a number of "levels" of + available: depending on the license, the solver may be available + with limitations on problem size or runtime (e.g., 'demo' + vs. 'community' vs. 'full'). In these cases, the solver may + return a subclass of enum.IntEnum, with members that resolve to + True if the solver is available (possibly with limitations). + The Enum may also have multiple members that all resolve to + False indicating the reason why the interface is not available + (not found, bad license, unsupported version, etc). + + Returns + ------- + available: Solver.Availability + An enum that indicates "how available" the solver is. + Note that the enum can be cast to bool, which will + be True if the solver is runable at all and False + otherwise. + """ + pass + + @abc.abstractmethod + def version(self) -> Tuple: + """ + Returns + ------- + version: tuple + A tuple representing the version + """ + + @property + @abc.abstractmethod + def config(self): + """ + An object for configuring solve options. + + Returns + ------- + InterfaceConfig + An object for configuring pyomo solve options such as the time limit. + These options are mostly independent of the solver. + """ + pass + + def is_persistent(self): + """ + Returns + ------- + is_persistent: bool + True if the solver is a persistent solver. + """ + return False + + +class PersistentSolver(SolverBase): + def is_persistent(self): + return True + + def load_vars( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> NoReturn: + """ + Load the solution of the primal variables into the value attribute of the variables. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution + to all primal variables will be loaded. + """ + for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + @abc.abstractmethod + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + pass + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Declare sign convention in docstring here. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all + constraints will be loaded. + + Returns + ------- + duals: dict + Maps constraints to dual values + """ + raise NotImplementedError( + '{0} does not support the get_duals method'.format(type(self)) + ) + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Parameters + ---------- + cons_to_load: list + A list of the constraints whose slacks should be loaded. If cons_to_load is None, then the slacks for all + constraints will be loaded. + + Returns + ------- + slacks: dict + Maps constraints to slack values + """ + raise NotImplementedError( + '{0} does not support the get_slacks method'.format(type(self)) + ) + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + """ + Parameters + ---------- + vars_to_load: list + A list of the variables whose reduced cost should be loaded. If vars_to_load is None, then all reduced costs + will be loaded. + + Returns + ------- + reduced_costs: ComponentMap + Maps variable to reduced cost + """ + raise NotImplementedError( + '{0} does not support the get_reduced_costs method'.format(type(self)) + ) + + @property + @abc.abstractmethod + def update_config(self) -> UpdateConfig: + pass + + @abc.abstractmethod + def set_instance(self, model): + pass + + @abc.abstractmethod + def add_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def add_params(self, params: List[_ParamData]): + pass + + @abc.abstractmethod + def add_constraints(self, cons: List[_GeneralConstraintData]): + pass + + @abc.abstractmethod + def add_block(self, block: _BlockData): + pass + + @abc.abstractmethod + def remove_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def remove_params(self, params: List[_ParamData]): + pass + + @abc.abstractmethod + def remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + @abc.abstractmethod + def remove_block(self, block: _BlockData): + pass + + @abc.abstractmethod + def set_objective(self, obj: _GeneralObjectiveData): + pass + + @abc.abstractmethod + def update_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def update_params(self): + pass + + + +""" +What can change in a pyomo model? +- variables added or removed +- constraints added or removed +- objective changed +- objective expr changed +- params added or removed +- variable modified + - lb + - ub + - fixed or unfixed + - domain + - value +- constraint modified + - lower + - upper + - body + - active or not +- named expressions modified + - expr +- param modified + - value + +Ideas: +- Consider explicitly handling deactivated constraints; favor deactivation over removal + and activation over addition + +Notes: +- variable bounds cannot be updated with mutable params; you must call update_variables +""" + + +class PersistentBase(abc.ABC): + def __init__(self, only_child_vars=False): + self._model = None + self._active_constraints = {} # maps constraint to (lower, body, upper) + self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) + self._params = {} # maps param id to param + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._named_expressions = ( + {} + ) # maps constraint to list of tuples (named_expr, named_expr.expr) + self._external_functions = ComponentMap() + self._obj_named_expressions = [] + self._update_config = UpdateConfig() + self._referenced_variables = ( + {} + ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] + self._vars_referenced_by_con = {} + self._vars_referenced_by_obj = [] + self._expr_types = None + self.use_extensions = False + self._only_child_vars = only_child_vars + + @property + def update_config(self): + return self._update_config + + @update_config.setter + def update_config(self, val: UpdateConfig): + self._update_config = val + + def set_instance(self, model): + saved_update_config = self.update_config + self.__init__() + self.update_config = saved_update_config + self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() + self.add_block(model) + if self._objective is None: + self.set_objective(None) + + @abc.abstractmethod + def _add_variables(self, variables: List[_GeneralVarData]): + pass + + def add_variables(self, variables: List[_GeneralVarData]): + for v in variables: + if id(v) in self._referenced_variables: + raise ValueError( + 'variable {name} has already been added'.format(name=v.name) + ) + self._referenced_variables[id(v)] = [{}, {}, None] + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._add_variables(variables) + + @abc.abstractmethod + def _add_params(self, params: List[_ParamData]): + pass + + def add_params(self, params: List[_ParamData]): + for p in params: + self._params[id(p)] = p + self._add_params(params) + + @abc.abstractmethod + def _add_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def _check_for_new_vars(self, variables: List[_GeneralVarData]): + new_vars = {} + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + new_vars[v_id] = v + self.add_variables(list(new_vars.values())) + + def _check_to_remove_vars(self, variables: List[_GeneralVarData]): + vars_to_remove = {} + for v in variables: + v_id = id(v) + ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] + if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: + vars_to_remove[v_id] = v + self.remove_variables(list(vars_to_remove.values())) + + def add_constraints(self, cons: List[_GeneralConstraintData]): + all_fixed_vars = {} + for con in cons: + if con in self._named_expressions: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = (con.lower, con.body, con.upper) + if self.use_extensions and cmodel_available: + tmp = cmodel.prep_for_repn(con.body, self._expr_types) + else: + tmp = collect_vars_and_named_exprs(con.body) + named_exprs, variables, fixed_vars, external_functions = tmp + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._named_expressions[con] = [(e, e.expr) for e in named_exprs] + if len(external_functions) > 0: + self._external_functions[con] = external_functions + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][0][con] = None + if not self.update_config.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + all_fixed_vars[id(v)] = v + self._add_constraints(cons) + for v in all_fixed_vars.values(): + v.fix() + + @abc.abstractmethod + def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def add_sos_constraints(self, cons: List[_SOSConstraintData]): + for con in cons: + if con in self._vars_referenced_by_con: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = tuple() + variables = con.get_variables() + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._named_expressions[con] = [] + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][1][con] = None + self._add_sos_constraints(cons) + + @abc.abstractmethod + def _set_objective(self, obj: _GeneralObjectiveData): + pass + + def set_objective(self, obj: _GeneralObjectiveData): + if self._objective is not None: + for v in self._vars_referenced_by_obj: + self._referenced_variables[id(v)][2] = None + if not self._only_child_vars: + self._check_to_remove_vars(self._vars_referenced_by_obj) + self._external_functions.pop(self._objective, None) + if obj is not None: + self._objective = obj + self._objective_expr = obj.expr + self._objective_sense = obj.sense + if self.use_extensions and cmodel_available: + tmp = cmodel.prep_for_repn(obj.expr, self._expr_types) + else: + tmp = collect_vars_and_named_exprs(obj.expr) + named_exprs, variables, fixed_vars, external_functions = tmp + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._obj_named_expressions = [(i, i.expr) for i in named_exprs] + if len(external_functions) > 0: + self._external_functions[obj] = external_functions + self._vars_referenced_by_obj = variables + for v in variables: + self._referenced_variables[id(v)][2] = obj + if not self.update_config.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + self._set_objective(obj) + for v in fixed_vars: + v.fix() + else: + self._vars_referenced_by_obj = [] + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._obj_named_expressions = [] + self._set_objective(obj) + + def add_block(self, block): + param_dict = {} + for p in block.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + param_dict[id(_p)] = _p + self.add_params(list(param_dict.values())) + if self._only_child_vars: + self.add_variables( + list( + dict( + (id(var), var) + for var in block.component_data_objects(Var, descend_into=True) + ).values() + ) + ) + self.add_constraints( + list(block.component_data_objects(Constraint, descend_into=True, active=True)) + ) + self.add_sos_constraints( + list(block.component_data_objects(SOSConstraint, descend_into=True, active=True)) + ) + obj = get_objective(block) + if obj is not None: + self.set_objective(obj) + + @abc.abstractmethod + def _remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def remove_constraints(self, cons: List[_GeneralConstraintData]): + self._remove_constraints(cons) + for con in cons: + if con not in self._named_expressions: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][0].pop(con) + if not self._only_child_vars: + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + self._external_functions.pop(con, None) + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def remove_sos_constraints(self, cons: List[_SOSConstraintData]): + self._remove_sos_constraints(cons) + for con in cons: + if con not in self._vars_referenced_by_con: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][1].pop(con) + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_variables(self, variables: List[_GeneralVarData]): + pass + + def remove_variables(self, variables: List[_GeneralVarData]): + self._remove_variables(variables) + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + raise ValueError( + 'cannot remove variable {name} - it has not been added'.format( + name=v.name + ) + ) + cons_using, sos_using, obj_using = self._referenced_variables[v_id] + if cons_using or sos_using or (obj_using is not None): + raise ValueError( + 'cannot remove variable {name} - it is still being used by constraints or the objective'.format( + name=v.name + ) + ) + del self._referenced_variables[v_id] + del self._vars[v_id] + + @abc.abstractmethod + def _remove_params(self, params: List[_ParamData]): + pass + + def remove_params(self, params: List[_ParamData]): + self._remove_params(params) + for p in params: + del self._params[id(p)] + + def remove_block(self, block): + self.remove_constraints( + list(block.component_data_objects(ctype=Constraint, descend_into=True, active=True)) + ) + self.remove_sos_constraints( + list(block.component_data_objects(ctype=SOSConstraint, descend_into=True, active=True)) + ) + if self._only_child_vars: + self.remove_variables( + list( + dict( + (id(var), var) + for var in block.component_data_objects( + ctype=Var, descend_into=True + ) + ).values() + ) + ) + self.remove_params( + list( + dict( + (id(p), p) + for p in block.component_data_objects( + ctype=Param, descend_into=True + ) + ).values() + ) + ) + + @abc.abstractmethod + def _update_variables(self, variables: List[_GeneralVarData]): + pass + + def update_variables(self, variables: List[_GeneralVarData]): + for v in variables: + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._update_variables(variables) + + @abc.abstractmethod + def update_params(self): + pass + + def update(self, timer: HierarchicalTimer = None): + if timer is None: + timer = HierarchicalTimer() + config = self.update_config + new_vars = [] + old_vars = [] + new_params = [] + old_params = [] + new_cons = [] + old_cons = [] + old_sos = [] + new_sos = [] + current_vars_dict = {} + current_cons_dict = {} + current_sos_dict = {} + timer.start('vars') + if self._only_child_vars and ( + config.check_for_new_or_removed_vars or config.update_vars + ): + current_vars_dict = { + id(v): v + for v in self._model.component_data_objects(Var, descend_into=True) + } + for v_id, v in current_vars_dict.items(): + if v_id not in self._vars: + new_vars.append(v) + for v_id, v_tuple in self._vars.items(): + if v_id not in current_vars_dict: + old_vars.append(v_tuple[0]) + elif config.update_vars: + start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + timer.stop('vars') + timer.start('params') + if config.check_for_new_or_removed_params: + current_params_dict = {} + for p in self._model.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + current_params_dict[id(_p)] = _p + for p_id, p in current_params_dict.items(): + if p_id not in self._params: + new_params.append(p) + for p_id, p in self._params.items(): + if p_id not in current_params_dict: + old_params.append(p) + timer.stop('params') + timer.start('cons') + if config.check_for_new_or_removed_constraints or config.update_constraints: + current_cons_dict = { + c: None + for c in self._model.component_data_objects( + Constraint, descend_into=True, active=True + ) + } + current_sos_dict = { + c: None + for c in self._model.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + } + for c in current_cons_dict.keys(): + if c not in self._vars_referenced_by_con: + new_cons.append(c) + for c in current_sos_dict.keys(): + if c not in self._vars_referenced_by_con: + new_sos.append(c) + for c in self._vars_referenced_by_con.keys(): + if c not in current_cons_dict and c not in current_sos_dict: + if (c.ctype is Constraint) or ( + c.ctype is None and isinstance(c, _GeneralConstraintData) + ): + old_cons.append(c) + else: + assert (c.ctype is SOSConstraint) or ( + c.ctype is None and isinstance(c, _SOSConstraintData) + ) + old_sos.append(c) + self.remove_constraints(old_cons) + self.remove_sos_constraints(old_sos) + timer.stop('cons') + timer.start('params') + self.remove_params(old_params) + + # sticking this between removal and addition + # is important so that we don't do unnecessary work + if config.update_params: + self.update_params() + + self.add_params(new_params) + timer.stop('params') + timer.start('vars') + self.add_variables(new_vars) + timer.stop('vars') + timer.start('cons') + self.add_constraints(new_cons) + self.add_sos_constraints(new_sos) + new_cons_set = set(new_cons) + new_sos_set = set(new_sos) + new_vars_set = set(id(v) for v in new_vars) + cons_to_remove_and_add = {} + need_to_set_objective = False + if config.update_constraints: + cons_to_update = [] + sos_to_update = [] + for c in current_cons_dict.keys(): + if c not in new_cons_set: + cons_to_update.append(c) + for c in current_sos_dict.keys(): + if c not in new_sos_set: + sos_to_update.append(c) + for c in cons_to_update: + lower, body, upper = self._active_constraints[c] + new_lower, new_body, new_upper = c.lower, c.body, c.upper + if new_body is not body: + cons_to_remove_and_add[c] = None + continue + if new_lower is not lower: + if ( + type(new_lower) is NumericConstant + and type(lower) is NumericConstant + and new_lower.value == lower.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + if new_upper is not upper: + if ( + type(new_upper) is NumericConstant + and type(upper) is NumericConstant + and new_upper.value == upper.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + self.remove_sos_constraints(sos_to_update) + self.add_sos_constraints(sos_to_update) + timer.stop('cons') + timer.start('vars') + if self._only_child_vars and config.update_vars: + vars_to_check = [] + for v_id, v in current_vars_dict.items(): + if v_id not in new_vars_set: + vars_to_check.append(v) + elif config.update_vars: + end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] + if config.update_vars: + vars_to_update = [] + for v in vars_to_check: + _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] + if lb is not v._lb: + vars_to_update.append(v) + elif ub is not v._ub: + vars_to_update.append(v) + elif (fixed is not v.fixed) or (fixed and (value != v.value)): + vars_to_update.append(v) + if self.update_config.treat_fixed_vars_as_params: + for c in self._referenced_variables[id(v)][0]: + cons_to_remove_and_add[c] = None + if self._referenced_variables[id(v)][2] is not None: + need_to_set_objective = True + elif domain_interval != v.domain.get_interval(): + vars_to_update.append(v) + self.update_variables(vars_to_update) + timer.stop('vars') + timer.start('cons') + cons_to_remove_and_add = list(cons_to_remove_and_add.keys()) + self.remove_constraints(cons_to_remove_and_add) + self.add_constraints(cons_to_remove_and_add) + timer.stop('cons') + timer.start('named expressions') + if config.update_named_expressions: + cons_to_update = [] + for c, expr_list in self._named_expressions.items(): + if c in new_cons_set: + continue + for named_expr, old_expr in expr_list: + if named_expr.expr is not old_expr: + cons_to_update.append(c) + break + self.remove_constraints(cons_to_update) + self.add_constraints(cons_to_update) + for named_expr, old_expr in self._obj_named_expressions: + if named_expr.expr is not old_expr: + need_to_set_objective = True + break + timer.stop('named expressions') + timer.start('objective') + if self.update_config.check_for_new_objective: + pyomo_obj = get_objective(self._model) + if pyomo_obj is not self._objective: + need_to_set_objective = True + else: + pyomo_obj = self._objective + if self.update_config.update_objective: + if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: + need_to_set_objective = True + elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: + # we can definitely do something faster here than resetting the whole objective + need_to_set_objective = True + if need_to_set_objective: + self.set_objective(pyomo_obj) + timer.stop('objective') + + # this has to be done after the objective and constraints in case the + # old objective/constraints use old variables + timer.start('vars') + self.remove_variables(old_vars) + timer.stop('vars') + + +# Everything below here preserves backwards compatibility + +legacy_termination_condition_map = { + TerminationCondition.unknown: LegacyTerminationCondition.unknown, + TerminationCondition.maxTimeLimit: LegacyTerminationCondition.maxTimeLimit, + TerminationCondition.iterationLimit: LegacyTerminationCondition.maxIterations, + TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, + TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, + TerminationCondition.convergenceCriteriaSatisfied: LegacyTerminationCondition.optimal, + TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, + TerminationCondition.provenInfeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.locallyInfeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, + TerminationCondition.error: LegacyTerminationCondition.error, + TerminationCondition.interrupted: LegacyTerminationCondition.resourceInterrupt, + TerminationCondition.licensingProblems: LegacyTerminationCondition.licensingProblems, +} + + +legacy_solver_status_map = { + TerminationCondition.unknown: LegacySolverStatus.unknown, + TerminationCondition.maxTimeLimit: LegacySolverStatus.aborted, + TerminationCondition.iterationLimit: LegacySolverStatus.aborted, + TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, + TerminationCondition.minStepLength: LegacySolverStatus.error, + TerminationCondition.convergenceCriteriaSatisfied: LegacySolverStatus.ok, + TerminationCondition.unbounded: LegacySolverStatus.error, + TerminationCondition.provenInfeasible: LegacySolverStatus.error, + TerminationCondition.locallyInfeasible: LegacySolverStatus.error, + TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, + TerminationCondition.error: LegacySolverStatus.error, + TerminationCondition.interrupted: LegacySolverStatus.aborted, + TerminationCondition.licensingProblems: LegacySolverStatus.error, +} + + +legacy_solution_status_map = { + TerminationCondition.unknown: LegacySolutionStatus.unknown, + TerminationCondition.maxTimeLimit: LegacySolutionStatus.stoppedByLimit, + TerminationCondition.iterationLimit: LegacySolutionStatus.stoppedByLimit, + TerminationCondition.objectiveLimit: LegacySolutionStatus.stoppedByLimit, + TerminationCondition.minStepLength: LegacySolutionStatus.error, + TerminationCondition.convergenceCriteriaSatisfied: LegacySolutionStatus.optimal, + TerminationCondition.unbounded: LegacySolutionStatus.unbounded, + TerminationCondition.provenInfeasible: LegacySolutionStatus.infeasible, + TerminationCondition.locallyInfeasible: LegacySolutionStatus.infeasible, + TerminationCondition.infeasibleOrUnbounded: LegacySolutionStatus.unsure, + TerminationCondition.error: LegacySolutionStatus.error, + TerminationCondition.interrupted: LegacySolutionStatus.error, + TerminationCondition.licensingProblems: LegacySolutionStatus.error, +} + + +class LegacySolverInterface: + def solve( + self, + model: _BlockData, + tee: bool = False, + load_solutions: bool = True, + logfile: Optional[str] = None, + solnfile: Optional[str] = None, + timelimit: Optional[float] = None, + report_timing: bool = False, + solver_io: Optional[str] = None, + suffixes: Optional[Sequence] = None, + options: Optional[Dict] = None, + keepfiles: bool = False, + symbolic_solver_labels: bool = False, + ): + original_config = self.config + self.config = self.config() + self.config.tee = tee + self.config.load_solution = load_solutions + self.config.symbolic_solver_labels = symbolic_solver_labels + self.config.time_limit = timelimit + self.config.report_timing = report_timing + if solver_io is not None: + raise NotImplementedError('Still working on this') + if suffixes is not None: + raise NotImplementedError('Still working on this') + if logfile is not None: + raise NotImplementedError('Still working on this') + if 'keepfiles' in self.config: + self.config.keepfiles = keepfiles + if solnfile is not None: + if 'filename' in self.config: + filename = os.path.splitext(solnfile)[0] + self.config.filename = filename + original_options = self.options + if options is not None: + self.options = options + + results: Results = super().solve(model) + + legacy_results = LegacySolverResults() + legacy_soln = LegacySolution() + legacy_results.solver.status = legacy_solver_status_map[ + results.termination_condition + ] + legacy_results.solver.termination_condition = legacy_termination_condition_map[ + results.termination_condition + ] + legacy_soln.status = legacy_solution_status_map[results.termination_condition] + legacy_results.solver.termination_message = str(results.termination_condition) + + obj = get_objective(model) + legacy_results.problem.sense = obj.sense + + if obj.sense == minimize: + legacy_results.problem.lower_bound = results.best_objective_bound + legacy_results.problem.upper_bound = results.best_feasible_objective + else: + legacy_results.problem.upper_bound = results.best_objective_bound + legacy_results.problem.lower_bound = results.best_feasible_objective + if ( + results.best_feasible_objective is not None + and results.best_objective_bound is not None + ): + legacy_soln.gap = abs( + results.best_feasible_objective - results.best_objective_bound + ) + else: + legacy_soln.gap = None + + symbol_map = SymbolMap() + symbol_map.byObject = dict(self.symbol_map.byObject) + symbol_map.bySymbol = dict(self.symbol_map.bySymbol) + symbol_map.aliases = dict(self.symbol_map.aliases) + symbol_map.default_labeler = self.symbol_map.default_labeler + model.solutions.add_symbol_map(symbol_map) + legacy_results._smap_id = id(symbol_map) + + delete_legacy_soln = True + if load_solutions: + if hasattr(model, 'dual') and model.dual.import_enabled(): + for c, val in results.solution_loader.get_duals().items(): + model.dual[c] = val + if hasattr(model, 'slack') and model.slack.import_enabled(): + for c, val in results.solution_loader.get_slacks().items(): + model.slack[c] = val + if hasattr(model, 'rc') and model.rc.import_enabled(): + for v, val in results.solution_loader.get_reduced_costs().items(): + model.rc[v] = val + elif results.best_feasible_objective is not None: + delete_legacy_soln = False + for v, val in results.solution_loader.get_primals().items(): + legacy_soln.variable[symbol_map.getSymbol(v)] = {'Value': val} + if hasattr(model, 'dual') and model.dual.import_enabled(): + for c, val in results.solution_loader.get_duals().items(): + legacy_soln.constraint[symbol_map.getSymbol(c)] = {'Dual': val} + if hasattr(model, 'slack') and model.slack.import_enabled(): + for c, val in results.solution_loader.get_slacks().items(): + symbol = symbol_map.getSymbol(c) + if symbol in legacy_soln.constraint: + legacy_soln.constraint[symbol]['Slack'] = val + if hasattr(model, 'rc') and model.rc.import_enabled(): + for v, val in results.solution_loader.get_reduced_costs().items(): + legacy_soln.variable['Rc'] = val + + legacy_results.solution.insert(legacy_soln) + if delete_legacy_soln: + legacy_results.solution.delete(0) + + self.config = original_config + self.options = original_options + + return legacy_results + + def available(self, exception_flag=True): + ans = super().available() + if exception_flag and not ans: + raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).') + return bool(ans) + + def license_is_valid(self) -> bool: + """Test if the solver license is valid on this system. + + Note that this method is included for compatibility with the + legacy SolverFactory interface. Unlicensed or open source + solvers will return True by definition. Licensed solvers will + return True if a valid license is found. + + Returns + ------- + available: bool + True if the solver license is valid. Otherwise, False. + + """ + return bool(self.available()) + + @property + def options(self): + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + if hasattr(self, solver_name + '_options'): + return getattr(self, solver_name + '_options') + raise NotImplementedError('Could not find the correct options') + + @options.setter + def options(self, val): + found = False + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + if hasattr(self, solver_name + '_options'): + setattr(self, solver_name + '_options', val) + found = True + if not found: + raise NotImplementedError('Could not find the correct options') + + def __enter__(self): + return self + + def __exit__(self, t, v, traceback): + pass + + +class SolverFactoryClass(Factory): + def register(self, name, doc=None): + def decorator(cls): + self._cls[name] = cls + self._doc[name] = doc + + class LegacySolver(LegacySolverInterface, cls): + pass + + LegacySolverFactory.register(name, doc)(LegacySolver) + + return cls + + return decorator + + +SolverFactory = SolverFactoryClass() diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py new file mode 100644 index 00000000000..ab9c30a0549 --- /dev/null +++ b/pyomo/solver/config.py @@ -0,0 +1,270 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from typing import Optional +from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat, NonNegativeInt + + +class InterfaceConfig(ConfigDict): + """ + Attributes + ---------- + time_limit: float - sent to solver + Time limit for the solver + tee: bool + If True, then the solver log goes to stdout + load_solution: bool - wrapper + If False, then the values of the primal variables will not be + loaded into the model + symbolic_solver_labels: bool - sent to solver + If True, the names given to the solver will reflect the names + of the pyomo components. Cannot be changed after set_instance + is called. + report_timing: bool - wrapper + If True, then some timing information will be printed at the + end of the solve. + threads: integer - sent to solver + Number of threads to be used by a solver. + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare('tee', ConfigValue(domain=bool)) + self.declare('load_solution', ConfigValue(domain=bool)) + self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) + self.declare('report_timing', ConfigValue(domain=bool)) + self.declare('threads', ConfigValue(domain=NonNegativeInt, default=None)) + + self.time_limit: Optional[float] = self.declare( + 'time_limit', ConfigValue(domain=NonNegativeFloat) + ) + self.tee: bool = False + self.load_solution: bool = True + self.symbolic_solver_labels: bool = False + self.report_timing: bool = False + + +class MIPInterfaceConfig(InterfaceConfig): + """ + Attributes + ---------- + mip_gap: float + Solver will terminate if the mip gap is less than mip_gap + relax_integrality: bool + If True, all integer variables will be relaxed to continuous + variables before solving + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare('mip_gap', ConfigValue(domain=NonNegativeFloat)) + self.declare('relax_integrality', ConfigValue(domain=bool)) + + self.mip_gap: Optional[float] = None + self.relax_integrality: bool = False + + +class UpdateConfig(ConfigDict): + """ + This is necessary for persistent solvers. + + Attributes + ---------- + check_for_new_or_removed_constraints: bool + check_for_new_or_removed_vars: bool + check_for_new_or_removed_params: bool + update_constraints: bool + update_vars: bool + update_params: bool + update_named_expressions: bool + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + if doc is None: + doc = 'Configuration options to detect changes in model between solves' + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare( + 'check_for_new_or_removed_constraints', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old constraints will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.add_constraints() + and opt.remove_constraints() or when you are certain constraints are not being + added to/removed from the model.""", + ), + ) + self.declare( + 'check_for_new_or_removed_vars', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old variables will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.add_variables() and + opt.remove_variables() or when you are certain variables are not being added to / + removed from the model.""", + ), + ) + self.declare( + 'check_for_new_or_removed_params', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old parameters will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.add_params() and + opt.remove_params() or when you are certain parameters are not being added to / + removed from the model.""", + ), + ) + self.declare( + 'check_for_new_objective', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old objectives will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.set_objective() or + when you are certain objectives are not being added to / removed from the model.""", + ), + ) + self.declare( + 'update_constraints', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to existing constraints will not be automatically detected on + subsequent solves. This includes changes to the lower, body, and upper attributes of + constraints. Use False only when manually updating the solver with + opt.remove_constraints() and opt.add_constraints() or when you are certain constraints + are not being modified.""", + ), + ) + self.declare( + 'update_vars', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to existing variables will not be automatically detected on + subsequent solves. This includes changes to the lb, ub, domain, and fixed + attributes of variables. Use False only when manually updating the solver with + opt.update_variables() or when you are certain variables are not being modified.""", + ), + ) + self.declare( + 'update_params', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to parameter values will not be automatically detected on + subsequent solves. Use False only when manually updating the solver with + opt.update_params() or when you are certain parameters are not being modified.""", + ), + ) + self.declare( + 'update_named_expressions', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to Expressions will not be automatically detected on + subsequent solves. Use False only when manually updating the solver with + opt.remove_constraints() and opt.add_constraints() or when you are certain + Expressions are not being modified.""", + ), + ) + self.declare( + 'update_objective', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to objectives will not be automatically detected on + subsequent solves. This includes the expr and sense attributes of objectives. Use + False only when manually updating the solver with opt.set_objective() or when you are + certain objectives are not being modified.""", + ), + ) + self.declare( + 'treat_fixed_vars_as_params', + ConfigValue( + domain=bool, + default=True, + doc=""" + This is an advanced option that should only be used in special circumstances. + With the default setting of True, fixed variables will be treated like parameters. + This means that z == x*y will be linear if x or y is fixed and the constraint + can be written to an LP file. If the value of the fixed variable gets changed, we have + to completely reprocess all constraints using that variable. If + treat_fixed_vars_as_params is False, then constraints will be processed as if fixed + variables are not fixed, and the solver will be told the variable is fixed. This means + z == x*y could not be written to an LP file even if x and/or y is fixed. However, + updating the values of fixed variables is much faster this way.""", + ), + ) + + self.check_for_new_or_removed_constraints: bool = True + self.check_for_new_or_removed_vars: bool = True + self.check_for_new_or_removed_params: bool = True + self.check_for_new_objective: bool = True + self.update_constraints: bool = True + self.update_vars: bool = True + self.update_params: bool = True + self.update_named_expressions: bool = True + self.update_objective: bool = True + self.treat_fixed_vars_as_params: bool = True diff --git a/pyomo/solver/solution.py b/pyomo/solver/solution.py new file mode 100644 index 00000000000..2d422736f2c --- /dev/null +++ b/pyomo/solver/solution.py @@ -0,0 +1,256 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import abc +from typing import ( + Sequence, + Dict, + Optional, + Mapping, + MutableMapping, + NoReturn, +) + +from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.var import _GeneralVarData +from pyomo.common.collections import ComponentMap +from pyomo.core.staleflag import StaleFlagManager + + +class SolutionLoaderBase(abc.ABC): + def load_vars( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> NoReturn: + """ + Load the solution of the primal variables into the value attribute of the variables. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution + to all primal variables will be loaded. + """ + for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + @abc.abstractmethod + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + """ + Returns a ComponentMap mapping variable to var value. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution value should be retrieved. If vars_to_load is None, + then the values for all variables will be retrieved. + + Returns + ------- + primals: ComponentMap + Maps variables to solution values + """ + pass + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Returns a dictionary mapping constraint to dual value. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be retrieved. If cons_to_load is None, then the duals for all + constraints will be retrieved. + + Returns + ------- + duals: dict + Maps constraints to dual values + """ + raise NotImplementedError(f'{type(self)} does not support the get_duals method') + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Returns a dictionary mapping constraint to slack. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all + constraints will be loaded. + + Returns + ------- + slacks: dict + Maps constraints to slacks + """ + raise NotImplementedError( + f'{type(self)} does not support the get_slacks method' + ) + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + """ + Returns a ComponentMap mapping variable to reduced cost. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose reduced cost should be retrieved. If vars_to_load is None, then the + reduced costs for all variables will be loaded. + + Returns + ------- + reduced_costs: ComponentMap + Maps variables to reduced costs + """ + raise NotImplementedError( + f'{type(self)} does not support the get_reduced_costs method' + ) + + +class SolutionLoader(SolutionLoaderBase): + def __init__( + self, + primals: Optional[MutableMapping], + duals: Optional[MutableMapping], + slacks: Optional[MutableMapping], + reduced_costs: Optional[MutableMapping], + ): + """ + Parameters + ---------- + primals: dict + maps id(Var) to (var, value) + duals: dict + maps Constraint to dual value + slacks: dict + maps Constraint to slack value + reduced_costs: dict + maps id(Var) to (var, reduced_cost) + """ + self._primals = primals + self._duals = duals + self._slacks = slacks + self._reduced_costs = reduced_costs + + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + if self._primals is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + if vars_to_load is None: + return ComponentMap(self._primals.values()) + else: + primals = ComponentMap() + for v in vars_to_load: + primals[v] = self._primals[id(v)][1] + return primals + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + if self._duals is None: + raise RuntimeError( + 'Solution loader does not currently have valid duals. Please ' + 'check the termination condition and ensure the solver returns duals ' + 'for the given problem type.' + ) + if cons_to_load is None: + duals = dict(self._duals) + else: + duals = {} + for c in cons_to_load: + duals[c] = self._duals[c] + return duals + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + if self._slacks is None: + raise RuntimeError( + 'Solution loader does not currently have valid slacks. Please ' + 'check the termination condition and ensure the solver returns slacks ' + 'for the given problem type.' + ) + if cons_to_load is None: + slacks = dict(self._slacks) + else: + slacks = {} + for c in cons_to_load: + slacks[c] = self._slacks[c] + return slacks + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + if self._reduced_costs is None: + raise RuntimeError( + 'Solution loader does not currently have valid reduced costs. Please ' + 'check the termination condition and ensure the solver returns reduced ' + 'costs for the given problem type.' + ) + if vars_to_load is None: + rc = ComponentMap(self._reduced_costs.values()) + else: + rc = ComponentMap() + for v in vars_to_load: + rc[v] = self._reduced_costs[id(v)][1] + return rc + + +class PersistentSolutionLoader(SolutionLoaderBase): + def __init__(self, solver): + self._solver = solver + self._valid = True + + def _assert_solution_still_valid(self): + if not self._valid: + raise RuntimeError('The results in the solver are no longer valid.') + + def get_primals(self, vars_to_load=None): + self._assert_solution_still_valid() + return self._solver.get_primals(vars_to_load=vars_to_load) + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + self._assert_solution_still_valid() + return self._solver.get_duals(cons_to_load=cons_to_load) + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + self._assert_solution_still_valid() + return self._solver.get_slacks(cons_to_load=cons_to_load) + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + self._assert_solution_still_valid() + return self._solver.get_reduced_costs(vars_to_load=vars_to_load) + + def invalidate(self): + self._valid = False + + + + diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/solver/tests/test_config.py b/pyomo/solver/tests/test_config.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/solver/tests/test_solution.py b/pyomo/solver/tests/test_solution.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/solver/tests/test_util.py b/pyomo/solver/tests/test_util.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py new file mode 100644 index 00000000000..8c768061678 --- /dev/null +++ b/pyomo/solver/util.py @@ -0,0 +1,23 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +class SolverUtils: + pass + +class SubprocessSolverUtils: + pass + +class DirectSolverUtils: + pass + +class PersistentSolverUtils: + pass + From 434ff9d984ee5446c150fbcf8af82df6716b4f30 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 08:59:25 -0600 Subject: [PATCH 0043/3044] Separate base class from APPSI --- pyomo/contrib/appsi/__init__.py | 1 - pyomo/contrib/appsi/base.py | 1836 ----------------- .../contrib/appsi/examples/getting_started.py | 3 +- pyomo/contrib/appsi/fbbt.py | 5 +- pyomo/contrib/appsi/solvers/cbc.py | 23 +- pyomo/contrib/appsi/solvers/cplex.py | 24 +- pyomo/contrib/appsi/solvers/gurobi.py | 25 +- pyomo/contrib/appsi/solvers/highs.py | 14 +- pyomo/contrib/appsi/solvers/ipopt.py | 24 +- .../solvers/tests/test_gurobi_persistent.py | 5 +- .../solvers/tests/test_persistent_solvers.py | 2 +- pyomo/contrib/appsi/tests/test_base.py | 91 - pyomo/contrib/appsi/writers/lp_writer.py | 6 +- pyomo/contrib/appsi/writers/nl_writer.py | 12 +- pyomo/environ/__init__.py | 1 + pyomo/solver/__init__.py | 4 +- pyomo/solver/base.py | 70 +- pyomo/solver/tests/test_base.py | 91 + 18 files changed, 159 insertions(+), 2078 deletions(-) delete mode 100644 pyomo/contrib/appsi/base.py delete mode 100644 pyomo/contrib/appsi/tests/test_base.py diff --git a/pyomo/contrib/appsi/__init__.py b/pyomo/contrib/appsi/__init__.py index df3ba212448..0134a96f363 100644 --- a/pyomo/contrib/appsi/__init__.py +++ b/pyomo/contrib/appsi/__init__.py @@ -1,4 +1,3 @@ -from . import base from . import solvers from . import writers from . import fbbt diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py deleted file mode 100644 index aa17489c4d5..00000000000 --- a/pyomo/contrib/appsi/base.py +++ /dev/null @@ -1,1836 +0,0 @@ -import abc -import enum -from typing import ( - Sequence, - Dict, - Optional, - Mapping, - NoReturn, - List, - Tuple, - MutableMapping, -) -from pyomo.core.base.constraint import _GeneralConstraintData, Constraint -from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint -from pyomo.core.base.var import _GeneralVarData, Var -from pyomo.core.base.param import _ParamData, Param -from pyomo.core.base.block import _BlockData -from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.common.collections import ComponentMap -from .utils.get_objective import get_objective -from .utils.collect_vars_and_named_exprs import collect_vars_and_named_exprs -from pyomo.common.timing import HierarchicalTimer -from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat, NonNegativeInt -from pyomo.common.errors import ApplicationError -from pyomo.opt.base import SolverFactory as LegacySolverFactory -from pyomo.common.factory import Factory -import os -from pyomo.opt.results.results_ import SolverResults as LegacySolverResults -from pyomo.opt.results.solution import ( - Solution as LegacySolution, - SolutionStatus as LegacySolutionStatus, -) -from pyomo.opt.results.solver import ( - TerminationCondition as LegacyTerminationCondition, - SolverStatus as LegacySolverStatus, -) -from pyomo.core.kernel.objective import minimize -from pyomo.core.base import SymbolMap -from .cmodel import cmodel, cmodel_available -from pyomo.core.staleflag import StaleFlagManager -from pyomo.core.expr.numvalue import NumericConstant - - -# # TerminationCondition - -# We currently have: Termination condition, solver status, and solution status. -# LL: Michael was trying to go for simplicity. All three conditions can be confusing. -# It is likely okay to have termination condition and solver status. - -# ## Open Questions (User Perspective) -# - Did I (the user) get a reasonable answer back from the solver? -# - If the answer is not reasonable, can I figure out why? - -# ## Our Goal -# Solvers normally tell you what they did and hope the users understand that. -# *We* want to try to return that information but also _help_ the user. - -# ## Proposals -# PROPOSAL 1: PyomoCondition and SolverCondition -# - SolverCondition: what the solver said -# - PyomoCondition: what we interpret that the solver said - -# PROPOSAL 2: TerminationCondition contains... -# - Some finite list of conditions -# - Two flags: why did it exit (TerminationCondition)? how do we interpret the result (SolutionStatus)? -# - Replace `optimal` with `normal` or `ok` for the termination flag; `optimal` can be used differently for the solver flag -# - You can use something else like `local`, `global`, `feasible` for solution status - - -class TerminationCondition(enum.Enum): - """ - An enumeration for checking the termination condition of solvers - """ - - """unknown serves as both a default value, and it is used when no other enum member makes sense""" - unknown = 42 - - """The solver exited because the convergence criteria were satisfied""" - convergenceCriteriaSatisfied = 0 - - """The solver exited due to a time limit""" - maxTimeLimit = 1 - - """The solver exited due to an iteration limit""" - iterationLimit = 2 - - """The solver exited due to an objective limit""" - objectiveLimit = 3 - - """The solver exited due to a minimum step length""" - minStepLength = 4 - - """The solver exited because the problem is unbounded""" - unbounded = 5 - - """The solver exited because the problem is proven infeasible""" - provenInfeasible = 6 - - """The solver exited because the problem was found to be locally infeasible""" - locallyInfeasible = 7 - - """The solver exited because the problem is either infeasible or unbounded""" - infeasibleOrUnbounded = 8 - - """The solver exited due to an error""" - error = 9 - - """The solver exited because it was interrupted""" - interrupted = 10 - - """The solver exited due to licensing problems""" - licensingProblems = 11 - - -class SolutionStatus(enum.IntEnum): - """ - An enumeration for interpreting the result of a termination. This describes the designated - status by the solver to be loaded back into the model. - - For now, we are choosing to use IntEnum such that return values are numerically - assigned in increasing order. - """ - - """No (single) solution found; possible that a population of solutions was returned""" - noSolution = 0 - - """Solution point does not satisfy some domains and/or constraints""" - infeasible = 10 - - """Feasible solution identified""" - feasible = 20 - - """Optimal solution identified""" - optimal = 30 - - -# # InterfaceConfig - -# The idea here (currently / in theory) is that a call to solve will have a keyword argument `solver_config`: -# ``` -# solve(model, solver_config=...) -# config = self.config(solver_config) -# ``` - -# We have several flavors of options: -# - Solver options -# - Standardized options -# - Wrapper options -# - Interface options -# - potentially... more? - -# ## The Options - -# There are three basic structures: flat, doubly-nested, separate dicts. -# We need to pick between these three structures (and stick with it). - -# **Flat: Clear interface; ambiguous about what goes where; better solve interface.** <- WINNER -# Doubly: More obscure interface; less ambiguity; better programmatic interface. -# SepDicts: Clear delineation; **kwargs becomes confusing (what maps to what?) (NOT HAPPENING) - - -class InterfaceConfig(ConfigDict): - """ - Attributes - ---------- - time_limit: float - sent to solver - Time limit for the solver - tee: bool - If True, then the solver log goes to stdout - load_solution: bool - wrapper - If False, then the values of the primal variables will not be - loaded into the model - symbolic_solver_labels: bool - sent to solver - If True, the names given to the solver will reflect the names - of the pyomo components. Cannot be changed after set_instance - is called. - report_timing: bool - wrapper - If True, then some timing information will be printed at the - end of the solve. - threads: integer - sent to solver - Number of threads to be used by a solver. - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.declare('tee', ConfigValue(domain=bool)) - self.declare('load_solution', ConfigValue(domain=bool)) - self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) - self.declare('report_timing', ConfigValue(domain=bool)) - self.declare('threads', ConfigValue(domain=NonNegativeInt, default=None)) - - self.time_limit: Optional[float] = self.declare( - 'time_limit', ConfigValue(domain=NonNegativeFloat) - ) - self.tee: bool = False - self.load_solution: bool = True - self.symbolic_solver_labels: bool = False - self.report_timing: bool = False - - -class MIPInterfaceConfig(InterfaceConfig): - """ - Attributes - ---------- - mip_gap: float - Solver will terminate if the mip gap is less than mip_gap - relax_integrality: bool - If True, all integer variables will be relaxed to continuous - variables before solving - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.declare('mip_gap', ConfigValue(domain=NonNegativeFloat)) - self.declare('relax_integrality', ConfigValue(domain=bool)) - - self.mip_gap: Optional[float] = None - self.relax_integrality: bool = False - - -# # SolutionLoaderBase - -# This is an attempt to answer the issue of persistent/non-persistent solution -# loading. This is an attribute of the results object (not the solver). - -# You wouldn't ask the solver to load a solution into a model. You would -# ask the result to load the solution - into the model you solved. -# The results object points to relevant elements; elements do NOT point to -# the results object. - -# Per Michael: This may be a bit clunky; but it works. -# Per Siirola: We may want to rethink `load_vars` and `get_primals`. In particular, -# this is for efficiency - don't create a dictionary you don't need to. And what is -# the client use-case for `get_primals`? - - -class SolutionLoaderBase(abc.ABC): - def load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> NoReturn: - """ - Load the solution of the primal variables into the value attribute of the variables. - - Parameters - ---------- - vars_to_load: list - A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution - to all primal variables will be loaded. - """ - for v, val in self.get_primals(vars_to_load=vars_to_load).items(): - v.set_value(val, skip_validation=True) - StaleFlagManager.mark_all_as_stale(delayed=True) - - @abc.abstractmethod - def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - """ - Returns a ComponentMap mapping variable to var value. - - Parameters - ---------- - vars_to_load: list - A list of the variables whose solution value should be retrieved. If vars_to_load is None, - then the values for all variables will be retrieved. - - Returns - ------- - primals: ComponentMap - Maps variables to solution values - """ - pass - - def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Returns a dictionary mapping constraint to dual value. - - Parameters - ---------- - cons_to_load: list - A list of the constraints whose duals should be retrieved. If cons_to_load is None, then the duals for all - constraints will be retrieved. - - Returns - ------- - duals: dict - Maps constraints to dual values - """ - raise NotImplementedError(f'{type(self)} does not support the get_duals method') - - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Returns a dictionary mapping constraint to slack. - - Parameters - ---------- - cons_to_load: list - A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all - constraints will be loaded. - - Returns - ------- - slacks: dict - Maps constraints to slacks - """ - raise NotImplementedError( - f'{type(self)} does not support the get_slacks method' - ) - - def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - """ - Returns a ComponentMap mapping variable to reduced cost. - - Parameters - ---------- - vars_to_load: list - A list of the variables whose reduced cost should be retrieved. If vars_to_load is None, then the - reduced costs for all variables will be loaded. - - Returns - ------- - reduced_costs: ComponentMap - Maps variables to reduced costs - """ - raise NotImplementedError( - f'{type(self)} does not support the get_reduced_costs method' - ) - - -class SolutionLoader(SolutionLoaderBase): - def __init__( - self, - primals: Optional[MutableMapping], - duals: Optional[MutableMapping], - slacks: Optional[MutableMapping], - reduced_costs: Optional[MutableMapping], - ): - """ - Parameters - ---------- - primals: dict - maps id(Var) to (var, value) - duals: dict - maps Constraint to dual value - slacks: dict - maps Constraint to slack value - reduced_costs: dict - maps id(Var) to (var, reduced_cost) - """ - self._primals = primals - self._duals = duals - self._slacks = slacks - self._reduced_costs = reduced_costs - - def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - if self._primals is None: - raise RuntimeError( - 'Solution loader does not currently have a valid solution. Please ' - 'check the termination condition.' - ) - if vars_to_load is None: - return ComponentMap(self._primals.values()) - else: - primals = ComponentMap() - for v in vars_to_load: - primals[v] = self._primals[id(v)][1] - return primals - - def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - if self._duals is None: - raise RuntimeError( - 'Solution loader does not currently have valid duals. Please ' - 'check the termination condition and ensure the solver returns duals ' - 'for the given problem type.' - ) - if cons_to_load is None: - duals = dict(self._duals) - else: - duals = {} - for c in cons_to_load: - duals[c] = self._duals[c] - return duals - - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - if self._slacks is None: - raise RuntimeError( - 'Solution loader does not currently have valid slacks. Please ' - 'check the termination condition and ensure the solver returns slacks ' - 'for the given problem type.' - ) - if cons_to_load is None: - slacks = dict(self._slacks) - else: - slacks = {} - for c in cons_to_load: - slacks[c] = self._slacks[c] - return slacks - - def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - if self._reduced_costs is None: - raise RuntimeError( - 'Solution loader does not currently have valid reduced costs. Please ' - 'check the termination condition and ensure the solver returns reduced ' - 'costs for the given problem type.' - ) - if vars_to_load is None: - rc = ComponentMap(self._reduced_costs.values()) - else: - rc = ComponentMap() - for v in vars_to_load: - rc[v] = self._reduced_costs[id(v)][1] - return rc - - -class Results: - """ - Attributes - ---------- - termination_condition: TerminationCondition - The reason the solver exited. This is a member of the - TerminationCondition enum. - best_feasible_objective: float - If a feasible solution was found, this is the objective value of - the best solution found. If no feasible solution was found, this is - None. - best_objective_bound: float - The best objective bound found. For minimization problems, this is - the lower bound. For maximization problems, this is the upper bound. - For solvers that do not provide an objective bound, this should be -inf - (minimization) or inf (maximization) - - Here is an example workflow: - - >>> import pyomo.environ as pe - >>> from pyomo.contrib import appsi - >>> m = pe.ConcreteModel() - >>> m.x = pe.Var() - >>> m.obj = pe.Objective(expr=m.x**2) - >>> opt = appsi.solvers.Ipopt() - >>> opt.config.load_solution = False - >>> results = opt.solve(m) #doctest:+SKIP - >>> if results.termination_condition == appsi.base.TerminationCondition.convergenceCriteriaSatisfied: #doctest:+SKIP - ... print('optimal solution found: ', results.best_feasible_objective) #doctest:+SKIP - ... results.solution_loader.load_vars() #doctest:+SKIP - ... print('the optimal value of x is ', m.x.value) #doctest:+SKIP - ... elif results.best_feasible_objective is not None: #doctest:+SKIP - ... print('sub-optimal but feasible solution found: ', results.best_feasible_objective) #doctest:+SKIP - ... results.solution_loader.load_vars(vars_to_load=[m.x]) #doctest:+SKIP - ... print('The value of x in the feasible solution is ', m.x.value) #doctest:+SKIP - ... elif results.termination_condition in {appsi.base.TerminationCondition.iterationLimit, appsi.base.TerminationCondition.maxTimeLimit}: #doctest:+SKIP - ... print('No feasible solution was found. The best lower bound found was ', results.best_objective_bound) #doctest:+SKIP - ... else: #doctest:+SKIP - ... print('The following termination condition was encountered: ', results.termination_condition) #doctest:+SKIP - """ - - def __init__(self): - self.solution_loader: SolutionLoaderBase = SolutionLoader( - None, None, None, None - ) - self.termination_condition: TerminationCondition = TerminationCondition.unknown - self.best_feasible_objective: Optional[float] = None - self.best_objective_bound: Optional[float] = None - - def __str__(self): - s = '' - s += 'termination_condition: ' + str(self.termination_condition) + '\n' - s += 'best_feasible_objective: ' + str(self.best_feasible_objective) + '\n' - s += 'best_objective_bound: ' + str(self.best_objective_bound) - return s - - -class UpdateConfig(ConfigDict): - """ - Attributes - ---------- - check_for_new_or_removed_constraints: bool - check_for_new_or_removed_vars: bool - check_for_new_or_removed_params: bool - update_constraints: bool - update_vars: bool - update_params: bool - update_named_expressions: bool - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - if doc is None: - doc = 'Configuration options to detect changes in model between solves' - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.declare( - 'check_for_new_or_removed_constraints', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, new/old constraints will not be automatically detected on subsequent - solves. Use False only when manually updating the solver with opt.add_constraints() - and opt.remove_constraints() or when you are certain constraints are not being - added to/removed from the model.""", - ), - ) - self.declare( - 'check_for_new_or_removed_vars', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, new/old variables will not be automatically detected on subsequent - solves. Use False only when manually updating the solver with opt.add_variables() and - opt.remove_variables() or when you are certain variables are not being added to / - removed from the model.""", - ), - ) - self.declare( - 'check_for_new_or_removed_params', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, new/old parameters will not be automatically detected on subsequent - solves. Use False only when manually updating the solver with opt.add_params() and - opt.remove_params() or when you are certain parameters are not being added to / - removed from the model.""", - ), - ) - self.declare( - 'check_for_new_objective', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, new/old objectives will not be automatically detected on subsequent - solves. Use False only when manually updating the solver with opt.set_objective() or - when you are certain objectives are not being added to / removed from the model.""", - ), - ) - self.declare( - 'update_constraints', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, changes to existing constraints will not be automatically detected on - subsequent solves. This includes changes to the lower, body, and upper attributes of - constraints. Use False only when manually updating the solver with - opt.remove_constraints() and opt.add_constraints() or when you are certain constraints - are not being modified.""", - ), - ) - self.declare( - 'update_vars', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, changes to existing variables will not be automatically detected on - subsequent solves. This includes changes to the lb, ub, domain, and fixed - attributes of variables. Use False only when manually updating the solver with - opt.update_variables() or when you are certain variables are not being modified.""", - ), - ) - self.declare( - 'update_params', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, changes to parameter values will not be automatically detected on - subsequent solves. Use False only when manually updating the solver with - opt.update_params() or when you are certain parameters are not being modified.""", - ), - ) - self.declare( - 'update_named_expressions', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, changes to Expressions will not be automatically detected on - subsequent solves. Use False only when manually updating the solver with - opt.remove_constraints() and opt.add_constraints() or when you are certain - Expressions are not being modified.""", - ), - ) - self.declare( - 'update_objective', - ConfigValue( - domain=bool, - default=True, - doc=""" - If False, changes to objectives will not be automatically detected on - subsequent solves. This includes the expr and sense attributes of objectives. Use - False only when manually updating the solver with opt.set_objective() or when you are - certain objectives are not being modified.""", - ), - ) - self.declare( - 'treat_fixed_vars_as_params', - ConfigValue( - domain=bool, - default=True, - doc=""" - This is an advanced option that should only be used in special circumstances. - With the default setting of True, fixed variables will be treated like parameters. - This means that z == x*y will be linear if x or y is fixed and the constraint - can be written to an LP file. If the value of the fixed variable gets changed, we have - to completely reprocess all constraints using that variable. If - treat_fixed_vars_as_params is False, then constraints will be processed as if fixed - variables are not fixed, and the solver will be told the variable is fixed. This means - z == x*y could not be written to an LP file even if x and/or y is fixed. However, - updating the values of fixed variables is much faster this way.""", - ), - ) - - self.check_for_new_or_removed_constraints: bool = True - self.check_for_new_or_removed_vars: bool = True - self.check_for_new_or_removed_params: bool = True - self.check_for_new_objective: bool = True - self.update_constraints: bool = True - self.update_vars: bool = True - self.update_params: bool = True - self.update_named_expressions: bool = True - self.update_objective: bool = True - self.treat_fixed_vars_as_params: bool = True - - -# # Solver - -# ## Open Question: What does 'solve' look like? - -# We may want to use the 80/20 rule here - we support 80% of the cases; anything -# fancier than that is going to require "writing code." The 80% would be offerings -# that are supported as part of the `pyomo` script. - -# ## Configs - -# We will likely have two configs for `solve`: standardized config (processes `**kwargs`) -# and implicit ConfigDict with some specialized options. - -# These have to be separated because there is a set that need to be passed -# directly to the solver. The other is Pyomo options / our standardized options -# (a few of which might be passed directly to solver, e.g., time_limit). - -# ## Contained Methods - -# We do not like `symbol_map`; it's keyed towards file-based interfaces. That -# is the `lp` writer; the `nl` writer doesn't need that (and in fact, it's -# obnoxious). The new `nl` writer returns back more meaningful things to the `nl` -# interface. - -# If the writer needs a symbol map, it will return it. But it is _not_ a -# solver thing. So it does not need to continue to exist in the solver interface. - -# All other options are reasonable. - -# ## Other (maybe should be contained) Methods - -# There are other methods in other solvers such as `warmstart`, `sos`; do we -# want to continue to support and/or offer those features? - -# The solver interface is not responsible for telling the client what -# it can do, e.g., `supports_sos2`. This is actually a contract between -# the solver and its writer. - -# End game: we are not supporting a `has_Xcapability` interface (CHECK BOOK). - - -class SolverBase(abc.ABC): - class Availability(enum.IntEnum): - NotFound = 0 - BadVersion = -1 - BadLicense = -2 - FullLicense = 1 - LimitedLicense = 2 - NeedsCompiledExtension = -3 - - def __bool__(self): - return self._value_ > 0 - - def __format__(self, format_spec): - # We want general formatting of this Enum to return the - # formatted string value and not the int (which is the - # default implementation from IntEnum) - return format(self.name, format_spec) - - def __str__(self): - # Note: Python 3.11 changed the core enums so that the - # "mixin" type for standard enums overrides the behavior - # specified in __format__. We will override str() here to - # preserve the previous behavior - return self.name - - @abc.abstractmethod - def solve( - self, model: _BlockData, timer: HierarchicalTimer = None, **kwargs - ) -> Results: - """ - Solve a Pyomo model. - - Parameters - ---------- - model: _BlockData - The Pyomo model to be solved - timer: HierarchicalTimer - An option timer for reporting timing - **kwargs - Additional keyword arguments (including solver_options - passthrough options; delivered directly to the solver (with no validation)) - - Returns - ------- - results: Results - A results object - """ - pass - - @abc.abstractmethod - def available(self): - """Test if the solver is available on this system. - - Nominally, this will return True if the solver interface is - valid and can be used to solve problems and False if it cannot. - - Note that for licensed solvers there are a number of "levels" of - available: depending on the license, the solver may be available - with limitations on problem size or runtime (e.g., 'demo' - vs. 'community' vs. 'full'). In these cases, the solver may - return a subclass of enum.IntEnum, with members that resolve to - True if the solver is available (possibly with limitations). - The Enum may also have multiple members that all resolve to - False indicating the reason why the interface is not available - (not found, bad license, unsupported version, etc). - - Returns - ------- - available: Solver.Availability - An enum that indicates "how available" the solver is. - Note that the enum can be cast to bool, which will - be True if the solver is runable at all and False - otherwise. - """ - pass - - @abc.abstractmethod - def version(self) -> Tuple: - """ - Returns - ------- - version: tuple - A tuple representing the version - """ - - @property - @abc.abstractmethod - def config(self): - """ - An object for configuring solve options. - - Returns - ------- - InterfaceConfig - An object for configuring pyomo solve options such as the time limit. - These options are mostly independent of the solver. - """ - pass - - def is_persistent(self): - """ - Returns - ------- - is_persistent: bool - True if the solver is a persistent solver. - """ - return False - -# In a non-persistent interface, when the solver dies, it'll return -# everthing it is going to return. And when you parse, you'll parse everything, -# whether or not you needed it. - -# In a persistent interface, if all I really care about is to keep going -# until the objective gets better. I may not need to parse the dual or state -# vars. If I only need the objective, why waste time bringing that extra -# cruft back? Why not just return what you ask for when you ask for it? - -# All the `gets_` is to be able to retrieve from the solver. Because the -# persistent interface is still holding onto the solver's definition, -# it saves time. Also helps avoid assuming that you are loading a model. - -# There is an argument whether or not the get methods could be called load. - -# For non-persistent, there are also questions about how we load everything. -# We tend to just load everything because it might disappear otherwise. -# In the file interface, we tend to parse everything, and the option is to turn -# it all off. We still parse everything... - -# IDEAL SITUATION -- -# load_solutions = True -> straight into model; otherwise, into results object - - -class PersistentSolver(SolverBase): - def is_persistent(self): - return True - - def load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> NoReturn: - """ - Load the solution of the primal variables into the value attribute of the variables. - - Parameters - ---------- - vars_to_load: list - A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution - to all primal variables will be loaded. - """ - for v, val in self.get_primals(vars_to_load=vars_to_load).items(): - v.set_value(val, skip_validation=True) - StaleFlagManager.mark_all_as_stale(delayed=True) - - @abc.abstractmethod - def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - pass - - def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Declare sign convention in docstring here. - - Parameters - ---------- - cons_to_load: list - A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all - constraints will be loaded. - - Returns - ------- - duals: dict - Maps constraints to dual values - """ - raise NotImplementedError( - '{0} does not support the get_duals method'.format(type(self)) - ) - - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Parameters - ---------- - cons_to_load: list - A list of the constraints whose slacks should be loaded. If cons_to_load is None, then the slacks for all - constraints will be loaded. - - Returns - ------- - slacks: dict - Maps constraints to slack values - """ - raise NotImplementedError( - '{0} does not support the get_slacks method'.format(type(self)) - ) - - def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - """ - Parameters - ---------- - vars_to_load: list - A list of the variables whose reduced cost should be loaded. If vars_to_load is None, then all reduced costs - will be loaded. - - Returns - ------- - reduced_costs: ComponentMap - Maps variable to reduced cost - """ - raise NotImplementedError( - '{0} does not support the get_reduced_costs method'.format(type(self)) - ) - - @property - @abc.abstractmethod - def update_config(self) -> UpdateConfig: - pass - - @abc.abstractmethod - def set_instance(self, model): - pass - - @abc.abstractmethod - def add_variables(self, variables: List[_GeneralVarData]): - pass - - @abc.abstractmethod - def add_params(self, params: List[_ParamData]): - pass - - @abc.abstractmethod - def add_constraints(self, cons: List[_GeneralConstraintData]): - pass - - @abc.abstractmethod - def add_block(self, block: _BlockData): - pass - - @abc.abstractmethod - def remove_variables(self, variables: List[_GeneralVarData]): - pass - - @abc.abstractmethod - def remove_params(self, params: List[_ParamData]): - pass - - @abc.abstractmethod - def remove_constraints(self, cons: List[_GeneralConstraintData]): - pass - - @abc.abstractmethod - def remove_block(self, block: _BlockData): - pass - - @abc.abstractmethod - def set_objective(self, obj: _GeneralObjectiveData): - pass - - @abc.abstractmethod - def update_variables(self, variables: List[_GeneralVarData]): - pass - - @abc.abstractmethod - def update_params(self): - pass - - -class PersistentSolutionLoader(SolutionLoaderBase): - def __init__(self, solver: PersistentSolver): - self._solver = solver - self._valid = True - - def _assert_solution_still_valid(self): - if not self._valid: - raise RuntimeError('The results in the solver are no longer valid.') - - def get_primals(self, vars_to_load=None): - self._assert_solution_still_valid() - return self._solver.get_primals(vars_to_load=vars_to_load) - - def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - self._assert_solution_still_valid() - return self._solver.get_duals(cons_to_load=cons_to_load) - - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - self._assert_solution_still_valid() - return self._solver.get_slacks(cons_to_load=cons_to_load) - - def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - self._assert_solution_still_valid() - return self._solver.get_reduced_costs(vars_to_load=vars_to_load) - - def invalidate(self): - self._valid = False - - -""" -What can change in a pyomo model? -- variables added or removed -- constraints added or removed -- objective changed -- objective expr changed -- params added or removed -- variable modified - - lb - - ub - - fixed or unfixed - - domain - - value -- constraint modified - - lower - - upper - - body - - active or not -- named expressions modified - - expr -- param modified - - value - -Ideas: -- Consider explicitly handling deactivated constraints; favor deactivation over removal - and activation over addition - -Notes: -- variable bounds cannot be updated with mutable params; you must call update_variables -""" - - -class PersistentBase(abc.ABC): - def __init__(self, only_child_vars=False): - self._model = None - self._active_constraints = {} # maps constraint to (lower, body, upper) - self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) - self._params = {} # maps param id to param - self._objective = None - self._objective_expr = None - self._objective_sense = None - self._named_expressions = ( - {} - ) # maps constraint to list of tuples (named_expr, named_expr.expr) - self._external_functions = ComponentMap() - self._obj_named_expressions = [] - self._update_config = UpdateConfig() - self._referenced_variables = ( - {} - ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] - self._vars_referenced_by_con = {} - self._vars_referenced_by_obj = [] - self._expr_types = None - self.use_extensions = False - self._only_child_vars = only_child_vars - - @property - def update_config(self): - return self._update_config - - @update_config.setter - def update_config(self, val: UpdateConfig): - self._update_config = val - - def set_instance(self, model): - saved_update_config = self.update_config - self.__init__() - self.update_config = saved_update_config - self._model = model - if self.use_extensions and cmodel_available: - self._expr_types = cmodel.PyomoExprTypes() - self.add_block(model) - if self._objective is None: - self.set_objective(None) - - @abc.abstractmethod - def _add_variables(self, variables: List[_GeneralVarData]): - pass - - def add_variables(self, variables: List[_GeneralVarData]): - for v in variables: - if id(v) in self._referenced_variables: - raise ValueError( - 'variable {name} has already been added'.format(name=v.name) - ) - self._referenced_variables[id(v)] = [{}, {}, None] - self._vars[id(v)] = ( - v, - v._lb, - v._ub, - v.fixed, - v.domain.get_interval(), - v.value, - ) - self._add_variables(variables) - - @abc.abstractmethod - def _add_params(self, params: List[_ParamData]): - pass - - def add_params(self, params: List[_ParamData]): - for p in params: - self._params[id(p)] = p - self._add_params(params) - - @abc.abstractmethod - def _add_constraints(self, cons: List[_GeneralConstraintData]): - pass - - def _check_for_new_vars(self, variables: List[_GeneralVarData]): - new_vars = {} - for v in variables: - v_id = id(v) - if v_id not in self._referenced_variables: - new_vars[v_id] = v - self.add_variables(list(new_vars.values())) - - def _check_to_remove_vars(self, variables: List[_GeneralVarData]): - vars_to_remove = {} - for v in variables: - v_id = id(v) - ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] - if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: - vars_to_remove[v_id] = v - self.remove_variables(list(vars_to_remove.values())) - - def add_constraints(self, cons: List[_GeneralConstraintData]): - all_fixed_vars = {} - for con in cons: - if con in self._named_expressions: - raise ValueError( - 'constraint {name} has already been added'.format(name=con.name) - ) - self._active_constraints[con] = (con.lower, con.body, con.upper) - if self.use_extensions and cmodel_available: - tmp = cmodel.prep_for_repn(con.body, self._expr_types) - else: - tmp = collect_vars_and_named_exprs(con.body) - named_exprs, variables, fixed_vars, external_functions = tmp - if not self._only_child_vars: - self._check_for_new_vars(variables) - self._named_expressions[con] = [(e, e.expr) for e in named_exprs] - if len(external_functions) > 0: - self._external_functions[con] = external_functions - self._vars_referenced_by_con[con] = variables - for v in variables: - self._referenced_variables[id(v)][0][con] = None - if not self.update_config.treat_fixed_vars_as_params: - for v in fixed_vars: - v.unfix() - all_fixed_vars[id(v)] = v - self._add_constraints(cons) - for v in all_fixed_vars.values(): - v.fix() - - @abc.abstractmethod - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): - pass - - def add_sos_constraints(self, cons: List[_SOSConstraintData]): - for con in cons: - if con in self._vars_referenced_by_con: - raise ValueError( - 'constraint {name} has already been added'.format(name=con.name) - ) - self._active_constraints[con] = tuple() - variables = con.get_variables() - if not self._only_child_vars: - self._check_for_new_vars(variables) - self._named_expressions[con] = [] - self._vars_referenced_by_con[con] = variables - for v in variables: - self._referenced_variables[id(v)][1][con] = None - self._add_sos_constraints(cons) - - @abc.abstractmethod - def _set_objective(self, obj: _GeneralObjectiveData): - pass - - def set_objective(self, obj: _GeneralObjectiveData): - if self._objective is not None: - for v in self._vars_referenced_by_obj: - self._referenced_variables[id(v)][2] = None - if not self._only_child_vars: - self._check_to_remove_vars(self._vars_referenced_by_obj) - self._external_functions.pop(self._objective, None) - if obj is not None: - self._objective = obj - self._objective_expr = obj.expr - self._objective_sense = obj.sense - if self.use_extensions and cmodel_available: - tmp = cmodel.prep_for_repn(obj.expr, self._expr_types) - else: - tmp = collect_vars_and_named_exprs(obj.expr) - named_exprs, variables, fixed_vars, external_functions = tmp - if not self._only_child_vars: - self._check_for_new_vars(variables) - self._obj_named_expressions = [(i, i.expr) for i in named_exprs] - if len(external_functions) > 0: - self._external_functions[obj] = external_functions - self._vars_referenced_by_obj = variables - for v in variables: - self._referenced_variables[id(v)][2] = obj - if not self.update_config.treat_fixed_vars_as_params: - for v in fixed_vars: - v.unfix() - self._set_objective(obj) - for v in fixed_vars: - v.fix() - else: - self._vars_referenced_by_obj = [] - self._objective = None - self._objective_expr = None - self._objective_sense = None - self._obj_named_expressions = [] - self._set_objective(obj) - - def add_block(self, block): - param_dict = {} - for p in block.component_objects(Param, descend_into=True): - if p.mutable: - for _p in p.values(): - param_dict[id(_p)] = _p - self.add_params(list(param_dict.values())) - if self._only_child_vars: - self.add_variables( - list( - dict( - (id(var), var) - for var in block.component_data_objects(Var, descend_into=True) - ).values() - ) - ) - self.add_constraints( - list(block.component_data_objects(Constraint, descend_into=True, active=True)) - ) - self.add_sos_constraints( - list(block.component_data_objects(SOSConstraint, descend_into=True, active=True)) - ) - obj = get_objective(block) - if obj is not None: - self.set_objective(obj) - - @abc.abstractmethod - def _remove_constraints(self, cons: List[_GeneralConstraintData]): - pass - - def remove_constraints(self, cons: List[_GeneralConstraintData]): - self._remove_constraints(cons) - for con in cons: - if con not in self._named_expressions: - raise ValueError( - 'cannot remove constraint {name} - it was not added'.format( - name=con.name - ) - ) - for v in self._vars_referenced_by_con[con]: - self._referenced_variables[id(v)][0].pop(con) - if not self._only_child_vars: - self._check_to_remove_vars(self._vars_referenced_by_con[con]) - del self._active_constraints[con] - del self._named_expressions[con] - self._external_functions.pop(con, None) - del self._vars_referenced_by_con[con] - - @abc.abstractmethod - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): - pass - - def remove_sos_constraints(self, cons: List[_SOSConstraintData]): - self._remove_sos_constraints(cons) - for con in cons: - if con not in self._vars_referenced_by_con: - raise ValueError( - 'cannot remove constraint {name} - it was not added'.format( - name=con.name - ) - ) - for v in self._vars_referenced_by_con[con]: - self._referenced_variables[id(v)][1].pop(con) - self._check_to_remove_vars(self._vars_referenced_by_con[con]) - del self._active_constraints[con] - del self._named_expressions[con] - del self._vars_referenced_by_con[con] - - @abc.abstractmethod - def _remove_variables(self, variables: List[_GeneralVarData]): - pass - - def remove_variables(self, variables: List[_GeneralVarData]): - self._remove_variables(variables) - for v in variables: - v_id = id(v) - if v_id not in self._referenced_variables: - raise ValueError( - 'cannot remove variable {name} - it has not been added'.format( - name=v.name - ) - ) - cons_using, sos_using, obj_using = self._referenced_variables[v_id] - if cons_using or sos_using or (obj_using is not None): - raise ValueError( - 'cannot remove variable {name} - it is still being used by constraints or the objective'.format( - name=v.name - ) - ) - del self._referenced_variables[v_id] - del self._vars[v_id] - - @abc.abstractmethod - def _remove_params(self, params: List[_ParamData]): - pass - - def remove_params(self, params: List[_ParamData]): - self._remove_params(params) - for p in params: - del self._params[id(p)] - - def remove_block(self, block): - self.remove_constraints( - list(block.component_data_objects(ctype=Constraint, descend_into=True, active=True)) - ) - self.remove_sos_constraints( - list(block.component_data_objects(ctype=SOSConstraint, descend_into=True, active=True)) - ) - if self._only_child_vars: - self.remove_variables( - list( - dict( - (id(var), var) - for var in block.component_data_objects( - ctype=Var, descend_into=True - ) - ).values() - ) - ) - self.remove_params( - list( - dict( - (id(p), p) - for p in block.component_data_objects( - ctype=Param, descend_into=True - ) - ).values() - ) - ) - - @abc.abstractmethod - def _update_variables(self, variables: List[_GeneralVarData]): - pass - - def update_variables(self, variables: List[_GeneralVarData]): - for v in variables: - self._vars[id(v)] = ( - v, - v._lb, - v._ub, - v.fixed, - v.domain.get_interval(), - v.value, - ) - self._update_variables(variables) - - @abc.abstractmethod - def update_params(self): - pass - - def update(self, timer: HierarchicalTimer = None): - if timer is None: - timer = HierarchicalTimer() - config = self.update_config - new_vars = [] - old_vars = [] - new_params = [] - old_params = [] - new_cons = [] - old_cons = [] - old_sos = [] - new_sos = [] - current_vars_dict = {} - current_cons_dict = {} - current_sos_dict = {} - timer.start('vars') - if self._only_child_vars and ( - config.check_for_new_or_removed_vars or config.update_vars - ): - current_vars_dict = { - id(v): v - for v in self._model.component_data_objects(Var, descend_into=True) - } - for v_id, v in current_vars_dict.items(): - if v_id not in self._vars: - new_vars.append(v) - for v_id, v_tuple in self._vars.items(): - if v_id not in current_vars_dict: - old_vars.append(v_tuple[0]) - elif config.update_vars: - start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} - timer.stop('vars') - timer.start('params') - if config.check_for_new_or_removed_params: - current_params_dict = {} - for p in self._model.component_objects(Param, descend_into=True): - if p.mutable: - for _p in p.values(): - current_params_dict[id(_p)] = _p - for p_id, p in current_params_dict.items(): - if p_id not in self._params: - new_params.append(p) - for p_id, p in self._params.items(): - if p_id not in current_params_dict: - old_params.append(p) - timer.stop('params') - timer.start('cons') - if config.check_for_new_or_removed_constraints or config.update_constraints: - current_cons_dict = { - c: None - for c in self._model.component_data_objects( - Constraint, descend_into=True, active=True - ) - } - current_sos_dict = { - c: None - for c in self._model.component_data_objects( - SOSConstraint, descend_into=True, active=True - ) - } - for c in current_cons_dict.keys(): - if c not in self._vars_referenced_by_con: - new_cons.append(c) - for c in current_sos_dict.keys(): - if c not in self._vars_referenced_by_con: - new_sos.append(c) - for c in self._vars_referenced_by_con.keys(): - if c not in current_cons_dict and c not in current_sos_dict: - if (c.ctype is Constraint) or ( - c.ctype is None and isinstance(c, _GeneralConstraintData) - ): - old_cons.append(c) - else: - assert (c.ctype is SOSConstraint) or ( - c.ctype is None and isinstance(c, _SOSConstraintData) - ) - old_sos.append(c) - self.remove_constraints(old_cons) - self.remove_sos_constraints(old_sos) - timer.stop('cons') - timer.start('params') - self.remove_params(old_params) - - # sticking this between removal and addition - # is important so that we don't do unnecessary work - if config.update_params: - self.update_params() - - self.add_params(new_params) - timer.stop('params') - timer.start('vars') - self.add_variables(new_vars) - timer.stop('vars') - timer.start('cons') - self.add_constraints(new_cons) - self.add_sos_constraints(new_sos) - new_cons_set = set(new_cons) - new_sos_set = set(new_sos) - new_vars_set = set(id(v) for v in new_vars) - cons_to_remove_and_add = {} - need_to_set_objective = False - if config.update_constraints: - cons_to_update = [] - sos_to_update = [] - for c in current_cons_dict.keys(): - if c not in new_cons_set: - cons_to_update.append(c) - for c in current_sos_dict.keys(): - if c not in new_sos_set: - sos_to_update.append(c) - for c in cons_to_update: - lower, body, upper = self._active_constraints[c] - new_lower, new_body, new_upper = c.lower, c.body, c.upper - if new_body is not body: - cons_to_remove_and_add[c] = None - continue - if new_lower is not lower: - if ( - type(new_lower) is NumericConstant - and type(lower) is NumericConstant - and new_lower.value == lower.value - ): - pass - else: - cons_to_remove_and_add[c] = None - continue - if new_upper is not upper: - if ( - type(new_upper) is NumericConstant - and type(upper) is NumericConstant - and new_upper.value == upper.value - ): - pass - else: - cons_to_remove_and_add[c] = None - continue - self.remove_sos_constraints(sos_to_update) - self.add_sos_constraints(sos_to_update) - timer.stop('cons') - timer.start('vars') - if self._only_child_vars and config.update_vars: - vars_to_check = [] - for v_id, v in current_vars_dict.items(): - if v_id not in new_vars_set: - vars_to_check.append(v) - elif config.update_vars: - end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} - vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] - if config.update_vars: - vars_to_update = [] - for v in vars_to_check: - _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] - if lb is not v._lb: - vars_to_update.append(v) - elif ub is not v._ub: - vars_to_update.append(v) - elif (fixed is not v.fixed) or (fixed and (value != v.value)): - vars_to_update.append(v) - if self.update_config.treat_fixed_vars_as_params: - for c in self._referenced_variables[id(v)][0]: - cons_to_remove_and_add[c] = None - if self._referenced_variables[id(v)][2] is not None: - need_to_set_objective = True - elif domain_interval != v.domain.get_interval(): - vars_to_update.append(v) - self.update_variables(vars_to_update) - timer.stop('vars') - timer.start('cons') - cons_to_remove_and_add = list(cons_to_remove_and_add.keys()) - self.remove_constraints(cons_to_remove_and_add) - self.add_constraints(cons_to_remove_and_add) - timer.stop('cons') - timer.start('named expressions') - if config.update_named_expressions: - cons_to_update = [] - for c, expr_list in self._named_expressions.items(): - if c in new_cons_set: - continue - for named_expr, old_expr in expr_list: - if named_expr.expr is not old_expr: - cons_to_update.append(c) - break - self.remove_constraints(cons_to_update) - self.add_constraints(cons_to_update) - for named_expr, old_expr in self._obj_named_expressions: - if named_expr.expr is not old_expr: - need_to_set_objective = True - break - timer.stop('named expressions') - timer.start('objective') - if self.update_config.check_for_new_objective: - pyomo_obj = get_objective(self._model) - if pyomo_obj is not self._objective: - need_to_set_objective = True - else: - pyomo_obj = self._objective - if self.update_config.update_objective: - if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: - need_to_set_objective = True - elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: - # we can definitely do something faster here than resetting the whole objective - need_to_set_objective = True - if need_to_set_objective: - self.set_objective(pyomo_obj) - timer.stop('objective') - - # this has to be done after the objective and constraints in case the - # old objective/constraints use old variables - timer.start('vars') - self.remove_variables(old_vars) - timer.stop('vars') - - -legacy_termination_condition_map = { - TerminationCondition.unknown: LegacyTerminationCondition.unknown, - TerminationCondition.maxTimeLimit: LegacyTerminationCondition.maxTimeLimit, - TerminationCondition.iterationLimit: LegacyTerminationCondition.maxIterations, - TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, - TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, - TerminationCondition.convergenceCriteriaSatisfied: LegacyTerminationCondition.optimal, - TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, - TerminationCondition.provenInfeasible: LegacyTerminationCondition.infeasible, - TerminationCondition.locallyInfeasible: LegacyTerminationCondition.infeasible, - TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, - TerminationCondition.error: LegacyTerminationCondition.error, - TerminationCondition.interrupted: LegacyTerminationCondition.resourceInterrupt, - TerminationCondition.licensingProblems: LegacyTerminationCondition.licensingProblems, -} - - -legacy_solver_status_map = { - TerminationCondition.unknown: LegacySolverStatus.unknown, - TerminationCondition.maxTimeLimit: LegacySolverStatus.aborted, - TerminationCondition.iterationLimit: LegacySolverStatus.aborted, - TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, - TerminationCondition.minStepLength: LegacySolverStatus.error, - TerminationCondition.convergenceCriteriaSatisfied: LegacySolverStatus.ok, - TerminationCondition.unbounded: LegacySolverStatus.error, - TerminationCondition.provenInfeasible: LegacySolverStatus.error, - TerminationCondition.locallyInfeasible: LegacySolverStatus.error, - TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, - TerminationCondition.error: LegacySolverStatus.error, - TerminationCondition.interrupted: LegacySolverStatus.aborted, - TerminationCondition.licensingProblems: LegacySolverStatus.error, -} - - -legacy_solution_status_map = { - TerminationCondition.unknown: LegacySolutionStatus.unknown, - TerminationCondition.maxTimeLimit: LegacySolutionStatus.stoppedByLimit, - TerminationCondition.iterationLimit: LegacySolutionStatus.stoppedByLimit, - TerminationCondition.objectiveLimit: LegacySolutionStatus.stoppedByLimit, - TerminationCondition.minStepLength: LegacySolutionStatus.error, - TerminationCondition.convergenceCriteriaSatisfied: LegacySolutionStatus.optimal, - TerminationCondition.unbounded: LegacySolutionStatus.unbounded, - TerminationCondition.provenInfeasible: LegacySolutionStatus.infeasible, - TerminationCondition.locallyInfeasible: LegacySolutionStatus.infeasible, - TerminationCondition.infeasibleOrUnbounded: LegacySolutionStatus.unsure, - TerminationCondition.error: LegacySolutionStatus.error, - TerminationCondition.interrupted: LegacySolutionStatus.error, - TerminationCondition.licensingProblems: LegacySolutionStatus.error, -} - - -class LegacySolverInterface: - def solve( - self, - model: _BlockData, - tee: bool = False, - load_solutions: bool = True, - logfile: Optional[str] = None, - solnfile: Optional[str] = None, - timelimit: Optional[float] = None, - report_timing: bool = False, - solver_io: Optional[str] = None, - suffixes: Optional[Sequence] = None, - options: Optional[Dict] = None, - keepfiles: bool = False, - symbolic_solver_labels: bool = False, - ): - original_config = self.config - self.config = self.config() - self.config.tee = tee - self.config.load_solution = load_solutions - self.config.symbolic_solver_labels = symbolic_solver_labels - self.config.time_limit = timelimit - self.config.report_timing = report_timing - if solver_io is not None: - raise NotImplementedError('Still working on this') - if suffixes is not None: - raise NotImplementedError('Still working on this') - if logfile is not None: - raise NotImplementedError('Still working on this') - if 'keepfiles' in self.config: - self.config.keepfiles = keepfiles - if solnfile is not None: - if 'filename' in self.config: - filename = os.path.splitext(solnfile)[0] - self.config.filename = filename - original_options = self.options - if options is not None: - self.options = options - - results: Results = super().solve(model) - - legacy_results = LegacySolverResults() - legacy_soln = LegacySolution() - legacy_results.solver.status = legacy_solver_status_map[ - results.termination_condition - ] - legacy_results.solver.termination_condition = legacy_termination_condition_map[ - results.termination_condition - ] - legacy_soln.status = legacy_solution_status_map[results.termination_condition] - legacy_results.solver.termination_message = str(results.termination_condition) - - obj = get_objective(model) - legacy_results.problem.sense = obj.sense - - if obj.sense == minimize: - legacy_results.problem.lower_bound = results.best_objective_bound - legacy_results.problem.upper_bound = results.best_feasible_objective - else: - legacy_results.problem.upper_bound = results.best_objective_bound - legacy_results.problem.lower_bound = results.best_feasible_objective - if ( - results.best_feasible_objective is not None - and results.best_objective_bound is not None - ): - legacy_soln.gap = abs( - results.best_feasible_objective - results.best_objective_bound - ) - else: - legacy_soln.gap = None - - symbol_map = SymbolMap() - symbol_map.byObject = dict(self.symbol_map.byObject) - symbol_map.bySymbol = dict(self.symbol_map.bySymbol) - symbol_map.aliases = dict(self.symbol_map.aliases) - symbol_map.default_labeler = self.symbol_map.default_labeler - model.solutions.add_symbol_map(symbol_map) - legacy_results._smap_id = id(symbol_map) - - delete_legacy_soln = True - if load_solutions: - if hasattr(model, 'dual') and model.dual.import_enabled(): - for c, val in results.solution_loader.get_duals().items(): - model.dual[c] = val - if hasattr(model, 'slack') and model.slack.import_enabled(): - for c, val in results.solution_loader.get_slacks().items(): - model.slack[c] = val - if hasattr(model, 'rc') and model.rc.import_enabled(): - for v, val in results.solution_loader.get_reduced_costs().items(): - model.rc[v] = val - elif results.best_feasible_objective is not None: - delete_legacy_soln = False - for v, val in results.solution_loader.get_primals().items(): - legacy_soln.variable[symbol_map.getSymbol(v)] = {'Value': val} - if hasattr(model, 'dual') and model.dual.import_enabled(): - for c, val in results.solution_loader.get_duals().items(): - legacy_soln.constraint[symbol_map.getSymbol(c)] = {'Dual': val} - if hasattr(model, 'slack') and model.slack.import_enabled(): - for c, val in results.solution_loader.get_slacks().items(): - symbol = symbol_map.getSymbol(c) - if symbol in legacy_soln.constraint: - legacy_soln.constraint[symbol]['Slack'] = val - if hasattr(model, 'rc') and model.rc.import_enabled(): - for v, val in results.solution_loader.get_reduced_costs().items(): - legacy_soln.variable['Rc'] = val - - legacy_results.solution.insert(legacy_soln) - if delete_legacy_soln: - legacy_results.solution.delete(0) - - self.config = original_config - self.options = original_options - - return legacy_results - - def available(self, exception_flag=True): - ans = super().available() - if exception_flag and not ans: - raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).') - return bool(ans) - - def license_is_valid(self) -> bool: - """Test if the solver license is valid on this system. - - Note that this method is included for compatibility with the - legacy SolverFactory interface. Unlicensed or open source - solvers will return True by definition. Licensed solvers will - return True if a valid license is found. - - Returns - ------- - available: bool - True if the solver license is valid. Otherwise, False. - - """ - return bool(self.available()) - - @property - def options(self): - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: - if hasattr(self, solver_name + '_options'): - return getattr(self, solver_name + '_options') - raise NotImplementedError('Could not find the correct options') - - @options.setter - def options(self, val): - found = False - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: - if hasattr(self, solver_name + '_options'): - setattr(self, solver_name + '_options', val) - found = True - if not found: - raise NotImplementedError('Could not find the correct options') - - def __enter__(self): - return self - - def __exit__(self, t, v, traceback): - pass - - -class SolverFactoryClass(Factory): - def register(self, name, doc=None): - def decorator(cls): - self._cls[name] = cls - self._doc[name] = doc - - class LegacySolver(LegacySolverInterface, cls): - pass - - LegacySolverFactory.register(name, doc)(LegacySolver) - - return cls - - return decorator - - -SolverFactory = SolverFactoryClass() diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index d907283f663..d65430e3c23 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -1,6 +1,7 @@ import pyomo.environ as pe from pyomo.contrib import appsi from pyomo.common.timing import HierarchicalTimer +from pyomo.solver import base as solver_base def main(plot=True, n_points=200): @@ -31,7 +32,7 @@ def main(plot=True, n_points=200): for p_val in p_values: m.p.value = p_val res = opt.solve(m, timer=timer) - assert res.termination_condition == appsi.base.TerminationCondition.convergenceCriteriaSatisfied + assert res.termination_condition == solver_base.TerminationCondition.convergenceCriteriaSatisfied obj_values.append(res.best_feasible_objective) opt.load_vars([m.x]) x_values.append(m.x.value) diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 22badd83d12..78137e790b6 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -1,4 +1,4 @@ -from pyomo.contrib.appsi.base import PersistentBase +from pyomo.solver.base import PersistentBase from pyomo.common.config import ( ConfigDict, ConfigValue, @@ -11,10 +11,9 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData, minimize, maximize +from pyomo.core.base.objective import _GeneralObjectiveData, minimize from pyomo.core.base.block import _BlockData from pyomo.core.base import SymbolMap, TextLabeler -from pyomo.common.errors import InfeasibleConstraintException class IntervalConfig(ConfigDict): diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 35071ab17ea..dd00089e84a 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -1,20 +1,16 @@ +import logging +import math +import subprocess +import sys +from typing import Optional, Sequence, Dict, List, Mapping + + from pyomo.common.tempfiles import TempfileManager from pyomo.common.fileutils import Executable -from pyomo.contrib.appsi.base import ( - PersistentSolver, - Results, - TerminationCondition, - InterfaceConfig, - PersistentSolutionLoader, -) from pyomo.contrib.appsi.writers import LPWriter from pyomo.common.log import LogStream -import logging -import subprocess from pyomo.core.kernel.objective import minimize, maximize -import math from pyomo.common.collections import ComponentMap -from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.block import _BlockData @@ -22,12 +18,13 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream -import sys -from typing import Dict from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager +from pyomo.solver.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.config import InterfaceConfig +from pyomo.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 7f9844fc21d..9f39528b0b0 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -1,29 +1,27 @@ -from pyomo.common.tempfiles import TempfileManager -from pyomo.contrib.appsi.base import ( - PersistentSolver, - Results, - TerminationCondition, - MIPInterfaceConfig, - PersistentSolutionLoader, -) -from pyomo.contrib.appsi.writers import LPWriter import logging import math +import sys +import time +from typing import Optional, Sequence, Dict, List, Mapping + + +from pyomo.common.tempfiles import TempfileManager +from pyomo.contrib.appsi.writers import LPWriter +from pyomo.common.log import LogStream from pyomo.common.collections import ComponentMap -from typing import Optional, Sequence, NoReturn, List, Mapping, Dict from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.block import _BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer -import sys -import time -from pyomo.common.log import LogStream from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager +from pyomo.solver.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 3f8eab638b0..8aaae4e31d4 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -1,7 +1,9 @@ from collections.abc import Iterable import logging import math +import sys from typing import List, Dict, Optional + from pyomo.common.collections import ComponentSet, ComponentMap, OrderedSet from pyomo.common.log import LogStream from pyomo.common.dependencies import attempt_import @@ -12,24 +14,19 @@ from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.base.var import Var, _GeneralVarData +from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression -from pyomo.contrib.appsi.base import ( - PersistentSolver, - Results, - TerminationCondition, - MIPInterfaceConfig, - PersistentBase, - PersistentSolutionLoader, -) from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.core.staleflag import StaleFlagManager -import sys +from pyomo.solver.base import TerminationCondition, Results, PersistentSolver, PersistentBase +from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.solution import PersistentSolutionLoader + logger = logging.getLogger(__name__) @@ -1196,8 +1193,8 @@ def set_linear_constraint_attr(self, con, attr, val): if attr in {'Sense', 'RHS', 'ConstrName'}: raise ValueError( 'Linear constraint attr {0} cannot be set with' - + ' the set_linear_constraint_attr method. Please use' - + ' the remove_constraint and add_constraint methods.'.format(attr) + ' the set_linear_constraint_attr method. Please use' + ' the remove_constraint and add_constraint methods.'.format(attr) ) self._pyomo_con_to_solver_con_map[con].setAttr(attr, val) self._needs_updated = True @@ -1225,8 +1222,8 @@ def set_var_attr(self, var, attr, val): if attr in {'LB', 'UB', 'VType', 'VarName'}: raise ValueError( 'Var attr {0} cannot be set with' - + ' the set_var_attr method. Please use' - + ' the update_var method.'.format(attr) + ' the set_var_attr method. Please use' + ' the update_var method.'.format(attr) ) if attr == 'Obj': raise ValueError( diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index e5c43d27c8d..c93d69527d8 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -1,5 +1,7 @@ import logging +import sys from typing import List, Dict, Optional + from pyomo.common.collections import ComponentMap from pyomo.common.dependencies import attempt_import from pyomo.common.errors import PyomoException @@ -16,18 +18,12 @@ from pyomo.core.expr.numvalue import value, is_constant from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression -from pyomo.contrib.appsi.base import ( - PersistentSolver, - Results, - TerminationCondition, - MIPInterfaceConfig, - PersistentBase, - PersistentSolutionLoader, -) from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.common.dependencies import numpy as np from pyomo.core.staleflag import StaleFlagManager -import sys +from pyomo.solver.base import TerminationCondition, Results, PersistentSolver, PersistentBase +from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index da42fc0be41..f754b5e85c0 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -1,18 +1,16 @@ +import math +import os +import sys +from typing import Dict +import logging +import subprocess + + from pyomo.common.tempfiles import TempfileManager from pyomo.common.fileutils import Executable -from pyomo.contrib.appsi.base import ( - PersistentSolver, - Results, - TerminationCondition, - InterfaceConfig, - PersistentSolutionLoader, -) from pyomo.contrib.appsi.writers import NLWriter from pyomo.common.log import LogStream -import logging -import subprocess from pyomo.core.kernel.objective import minimize -import math from pyomo.common.collections import ComponentMap from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions @@ -24,13 +22,13 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream -import sys -from typing import Dict from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.errors import PyomoException -import os from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager +from pyomo.solver.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.config import InterfaceConfig +from pyomo.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index fcff8916b5b..877d0971f2b 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -1,11 +1,8 @@ -from pyomo.common.errors import PyomoException from pyomo.common import unittest import pyomo.environ as pe from pyomo.contrib.appsi.solvers.gurobi import Gurobi -from pyomo.contrib.appsi.base import TerminationCondition -from pyomo.core.expr.numeric_expr import LinearExpression +from pyomo.solver.base import TerminationCondition from pyomo.core.expr.taylor_series import taylor_series_expansion -from pyomo.contrib.appsi.cmodel import cmodel_available opt = Gurobi() diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 135f36d3695..fc97ba43fd0 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -4,7 +4,7 @@ parameterized, param_available = attempt_import('parameterized') parameterized = parameterized.parameterized -from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Highs from typing import Type diff --git a/pyomo/contrib/appsi/tests/test_base.py b/pyomo/contrib/appsi/tests/test_base.py deleted file mode 100644 index 82a04b29e56..00000000000 --- a/pyomo/contrib/appsi/tests/test_base.py +++ /dev/null @@ -1,91 +0,0 @@ -from pyomo.common import unittest -from pyomo.contrib import appsi -import pyomo.environ as pe -from pyomo.core.base.var import ScalarVar - - -class TestResults(unittest.TestCase): - def test_uninitialized(self): - res = appsi.base.Results() - self.assertIsNone(res.best_feasible_objective) - self.assertIsNone(res.best_objective_bound) - self.assertEqual( - res.termination_condition, appsi.base.TerminationCondition.unknown - ) - - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have a valid solution.*' - ): - res.solution_loader.load_vars() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid duals.*' - ): - res.solution_loader.get_duals() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid reduced costs.*' - ): - res.solution_loader.get_reduced_costs() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid slacks.*' - ): - res.solution_loader.get_slacks() - - def test_results(self): - m = pe.ConcreteModel() - m.x = ScalarVar() - m.y = ScalarVar() - m.c1 = pe.Constraint(expr=m.x == 1) - m.c2 = pe.Constraint(expr=m.y == 2) - - primals = {} - primals[id(m.x)] = (m.x, 1) - primals[id(m.y)] = (m.y, 2) - duals = {} - duals[m.c1] = 3 - duals[m.c2] = 4 - rc = {} - rc[id(m.x)] = (m.x, 5) - rc[id(m.y)] = (m.y, 6) - slacks = {} - slacks[m.c1] = 7 - slacks[m.c2] = 8 - - res = appsi.base.Results() - res.solution_loader = appsi.base.SolutionLoader( - primals=primals, duals=duals, slacks=slacks, reduced_costs=rc - ) - - res.solution_loader.load_vars() - self.assertAlmostEqual(m.x.value, 1) - self.assertAlmostEqual(m.y.value, 2) - - m.x.value = None - m.y.value = None - - res.solution_loader.load_vars([m.y]) - self.assertIsNone(m.x.value) - self.assertAlmostEqual(m.y.value, 2) - - duals2 = res.solution_loader.get_duals() - self.assertAlmostEqual(duals[m.c1], duals2[m.c1]) - self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) - - duals2 = res.solution_loader.get_duals([m.c2]) - self.assertNotIn(m.c1, duals2) - self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) - - rc2 = res.solution_loader.get_reduced_costs() - self.assertAlmostEqual(rc[id(m.x)][1], rc2[m.x]) - self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) - - rc2 = res.solution_loader.get_reduced_costs([m.y]) - self.assertNotIn(m.x, rc2) - self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) - - slacks2 = res.solution_loader.get_slacks() - self.assertAlmostEqual(slacks[m.c1], slacks2[m.c1]) - self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) - - slacks2 = res.solution_loader.get_slacks([m.c2]) - self.assertNotIn(m.c1, slacks2) - self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 6a4a4ab2ff7..6ebc26b7b31 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -5,12 +5,10 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import _BlockData -from pyomo.repn.standard_repn import generate_standard_repn -from pyomo.core.expr.numvalue import value -from pyomo.contrib.appsi.base import PersistentBase from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.timing import HierarchicalTimer -from pyomo.core.kernel.objective import minimize, maximize +from pyomo.core.kernel.objective import minimize +from pyomo.solver.base import PersistentBase from .config import WriterConfig from ..cmodel import cmodel, cmodel_available diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index d0bb443508d..39aed3732aa 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -1,4 +1,6 @@ +import os from typing import List + from pyomo.core.base.param import _ParamData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData @@ -6,17 +8,15 @@ from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import _BlockData from pyomo.repn.standard_repn import generate_standard_repn -from pyomo.core.expr.numvalue import value -from pyomo.contrib.appsi.base import PersistentBase -from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler +from pyomo.core.base import SymbolMap, TextLabeler from pyomo.common.timing import HierarchicalTimer from pyomo.core.kernel.objective import minimize -from .config import WriterConfig from pyomo.common.collections import OrderedSet -import os -from ..cmodel import cmodel, cmodel_available from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env +from pyomo.solver.base import PersistentBase +from .config import WriterConfig +from ..cmodel import cmodel, cmodel_available class NLWriter(PersistentBase): def __init__(self, only_child_vars=False): diff --git a/pyomo/environ/__init__.py b/pyomo/environ/__init__.py index 51c68449247..2cd562edb2b 100644 --- a/pyomo/environ/__init__.py +++ b/pyomo/environ/__init__.py @@ -30,6 +30,7 @@ def _do_import(pkg_name): 'pyomo.repn', 'pyomo.neos', 'pyomo.solvers', + 'pyomo.solver', 'pyomo.gdp', 'pyomo.mpec', 'pyomo.dae', diff --git a/pyomo/solver/__init__.py b/pyomo/solver/__init__.py index 64c6452d06d..13b8b463662 100644 --- a/pyomo/solver/__init__.py +++ b/pyomo/solver/__init__.py @@ -9,6 +9,8 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from . import util from . import base +from . import config from . import solution +from . import util + diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index b6d9e1592cb..8b39f387c92 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -45,7 +45,6 @@ ) from pyomo.core.kernel.objective import minimize from pyomo.core.base import SymbolMap -from .cmodel import cmodel, cmodel_available from pyomo.core.staleflag import StaleFlagManager from pyomo.core.expr.numvalue import NumericConstant from pyomo.solver import ( @@ -138,29 +137,6 @@ class Results: the lower bound. For maximization problems, this is the upper bound. For solvers that do not provide an objective bound, this should be -inf (minimization) or inf (maximization) - - Here is an example workflow: - - >>> import pyomo.environ as pe - >>> from pyomo.contrib import appsi - >>> m = pe.ConcreteModel() - >>> m.x = pe.Var() - >>> m.obj = pe.Objective(expr=m.x**2) - >>> opt = appsi.solvers.Ipopt() - >>> opt.config.load_solution = False - >>> results = opt.solve(m) #doctest:+SKIP - >>> if results.termination_condition == appsi.base.TerminationCondition.convergenceCriteriaSatisfied: #doctest:+SKIP - ... print('optimal solution found: ', results.best_feasible_objective) #doctest:+SKIP - ... results.solution_loader.load_vars() #doctest:+SKIP - ... print('the optimal value of x is ', m.x.value) #doctest:+SKIP - ... elif results.best_feasible_objective is not None: #doctest:+SKIP - ... print('sub-optimal but feasible solution found: ', results.best_feasible_objective) #doctest:+SKIP - ... results.solution_loader.load_vars(vars_to_load=[m.x]) #doctest:+SKIP - ... print('The value of x in the feasible solution is ', m.x.value) #doctest:+SKIP - ... elif results.termination_condition in {appsi.base.TerminationCondition.iterationLimit, appsi.base.TerminationCondition.maxTimeLimit}: #doctest:+SKIP - ... print('No feasible solution was found. The best lower bound found was ', results.best_objective_bound) #doctest:+SKIP - ... else: #doctest:+SKIP - ... print('The following termination condition was encountered: ', results.termination_condition) #doctest:+SKIP """ def __init__(self): @@ -426,39 +402,6 @@ def update_params(self): pass - -""" -What can change in a pyomo model? -- variables added or removed -- constraints added or removed -- objective changed -- objective expr changed -- params added or removed -- variable modified - - lb - - ub - - fixed or unfixed - - domain - - value -- constraint modified - - lower - - upper - - body - - active or not -- named expressions modified - - expr -- param modified - - value - -Ideas: -- Consider explicitly handling deactivated constraints; favor deactivation over removal - and activation over addition - -Notes: -- variable bounds cannot be updated with mutable params; you must call update_variables -""" - - class PersistentBase(abc.ABC): def __init__(self, only_child_vars=False): self._model = None @@ -480,7 +423,6 @@ def __init__(self, only_child_vars=False): self._vars_referenced_by_con = {} self._vars_referenced_by_obj = [] self._expr_types = None - self.use_extensions = False self._only_child_vars = only_child_vars @property @@ -496,8 +438,6 @@ def set_instance(self, model): self.__init__() self.update_config = saved_update_config self._model = model - if self.use_extensions and cmodel_available: - self._expr_types = cmodel.PyomoExprTypes() self.add_block(model) if self._objective is None: self.set_objective(None) @@ -561,10 +501,7 @@ def add_constraints(self, cons: List[_GeneralConstraintData]): 'constraint {name} has already been added'.format(name=con.name) ) self._active_constraints[con] = (con.lower, con.body, con.upper) - if self.use_extensions and cmodel_available: - tmp = cmodel.prep_for_repn(con.body, self._expr_types) - else: - tmp = collect_vars_and_named_exprs(con.body) + tmp = collect_vars_and_named_exprs(con.body) named_exprs, variables, fixed_vars, external_functions = tmp if not self._only_child_vars: self._check_for_new_vars(variables) @@ -617,10 +554,7 @@ def set_objective(self, obj: _GeneralObjectiveData): self._objective = obj self._objective_expr = obj.expr self._objective_sense = obj.sense - if self.use_extensions and cmodel_available: - tmp = cmodel.prep_for_repn(obj.expr, self._expr_types) - else: - tmp = collect_vars_and_named_exprs(obj.expr) + tmp = collect_vars_and_named_exprs(obj.expr) named_exprs, variables, fixed_vars, external_functions = tmp if not self._only_child_vars: self._check_for_new_vars(variables) diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index e69de29bb2d..b5fcc4c4242 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -0,0 +1,91 @@ +from pyomo.common import unittest +from pyomo.solver import base +import pyomo.environ as pe +from pyomo.core.base.var import ScalarVar + + +class TestResults(unittest.TestCase): + def test_uninitialized(self): + res = base.Results() + self.assertIsNone(res.best_feasible_objective) + self.assertIsNone(res.best_objective_bound) + self.assertEqual( + res.termination_condition, base.TerminationCondition.unknown + ) + + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have a valid solution.*' + ): + res.solution_loader.load_vars() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid duals.*' + ): + res.solution_loader.get_duals() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid reduced costs.*' + ): + res.solution_loader.get_reduced_costs() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid slacks.*' + ): + res.solution_loader.get_slacks() + + def test_results(self): + m = pe.ConcreteModel() + m.x = ScalarVar() + m.y = ScalarVar() + m.c1 = pe.Constraint(expr=m.x == 1) + m.c2 = pe.Constraint(expr=m.y == 2) + + primals = {} + primals[id(m.x)] = (m.x, 1) + primals[id(m.y)] = (m.y, 2) + duals = {} + duals[m.c1] = 3 + duals[m.c2] = 4 + rc = {} + rc[id(m.x)] = (m.x, 5) + rc[id(m.y)] = (m.y, 6) + slacks = {} + slacks[m.c1] = 7 + slacks[m.c2] = 8 + + res = base.Results() + res.solution_loader = base.SolutionLoader( + primals=primals, duals=duals, slacks=slacks, reduced_costs=rc + ) + + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 2) + + m.x.value = None + m.y.value = None + + res.solution_loader.load_vars([m.y]) + self.assertIsNone(m.x.value) + self.assertAlmostEqual(m.y.value, 2) + + duals2 = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], duals2[m.c1]) + self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) + + duals2 = res.solution_loader.get_duals([m.c2]) + self.assertNotIn(m.c1, duals2) + self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) + + rc2 = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[id(m.x)][1], rc2[m.x]) + self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) + + rc2 = res.solution_loader.get_reduced_costs([m.y]) + self.assertNotIn(m.x, rc2) + self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) + + slacks2 = res.solution_loader.get_slacks() + self.assertAlmostEqual(slacks[m.c1], slacks2[m.c1]) + self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) + + slacks2 = res.solution_loader.get_slacks([m.c2]) + self.assertNotIn(m.c1, slacks2) + self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) From 04d71cff3c3834c144b1462846c96b2fe6586953 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 09:03:25 -0600 Subject: [PATCH 0044/3044] Fix broken imports --- pyomo/solver/base.py | 13 +++++---- pyomo/solver/util.py | 64 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 8b39f387c92..7a451e59c11 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -27,8 +27,7 @@ from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.collections import ComponentMap -from .utils.get_objective import get_objective -from .utils.collect_vars_and_named_exprs import collect_vars_and_named_exprs + from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError from pyomo.opt.base import SolverFactory as LegacySolverFactory @@ -47,11 +46,11 @@ from pyomo.core.base import SymbolMap from pyomo.core.staleflag import StaleFlagManager from pyomo.core.expr.numvalue import NumericConstant -from pyomo.solver import ( - SolutionLoader, - SolutionLoaderBase, - UpdateConfig -) + +from pyomo.solver.config import UpdateConfig +from pyomo.solver.solution import SolutionLoader, SolutionLoaderBase +from pyomo.solver.util import get_objective, collect_vars_and_named_exprs + class TerminationCondition(enum.Enum): diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py index 8c768061678..4b8acf0de2e 100644 --- a/pyomo/solver/util.py +++ b/pyomo/solver/util.py @@ -9,6 +9,70 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from pyomo.core.base.objective import Objective +from pyomo.core.expr.visitor import ExpressionValueVisitor, nonpyomo_leaf_types +import pyomo.core.expr as EXPR + + +def get_objective(block): + obj = None + for o in block.component_data_objects( + Objective, descend_into=True, active=True, sort=True + ): + if obj is not None: + raise ValueError('Multiple active objectives found') + obj = o + return obj + + +class _VarAndNamedExprCollector(ExpressionValueVisitor): + def __init__(self): + self.named_expressions = {} + self.variables = {} + self.fixed_vars = {} + self._external_functions = {} + + def visit(self, node, values): + pass + + def visiting_potential_leaf(self, node): + if type(node) in nonpyomo_leaf_types: + return True, None + + if node.is_variable_type(): + self.variables[id(node)] = node + if node.is_fixed(): + self.fixed_vars[id(node)] = node + return True, None + + if node.is_named_expression_type(): + self.named_expressions[id(node)] = node + return False, None + + if type(node) is EXPR.ExternalFunctionExpression: + self._external_functions[id(node)] = node + return False, None + + if node.is_expression_type(): + return False, None + + return True, None + + +_visitor = _VarAndNamedExprCollector() + + +def collect_vars_and_named_exprs(expr): + _visitor.__init__() + _visitor.dfs_postorder_stack(expr) + return ( + list(_visitor.named_expressions.values()), + list(_visitor.variables.values()), + list(_visitor.fixed_vars.values()), + list(_visitor._external_functions.values()), + ) + + class SolverUtils: pass From fe9cea4b8d2ec14a8b26ca0b566a5665f66640fb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 09:05:22 -0600 Subject: [PATCH 0045/3044] Trying to fix broken imports again --- pyomo/environ/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/environ/__init__.py b/pyomo/environ/__init__.py index 2cd562edb2b..51c68449247 100644 --- a/pyomo/environ/__init__.py +++ b/pyomo/environ/__init__.py @@ -30,7 +30,6 @@ def _do_import(pkg_name): 'pyomo.repn', 'pyomo.neos', 'pyomo.solvers', - 'pyomo.solver', 'pyomo.gdp', 'pyomo.mpec', 'pyomo.dae', From b41cf0089700e33741cd862f269fec668e6af490 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 09:07:59 -0600 Subject: [PATCH 0046/3044] Update plugins --- pyomo/contrib/appsi/plugins.py | 1 - pyomo/environ/__init__.py | 1 + pyomo/solver/plugins.py | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 pyomo/solver/plugins.py diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index 5333158239e..75161e3548c 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,5 +1,4 @@ from pyomo.common.extensions import ExtensionBuilderFactory -from .base import SolverFactory from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs from .build import AppsiBuilder diff --git a/pyomo/environ/__init__.py b/pyomo/environ/__init__.py index 51c68449247..2cd562edb2b 100644 --- a/pyomo/environ/__init__.py +++ b/pyomo/environ/__init__.py @@ -30,6 +30,7 @@ def _do_import(pkg_name): 'pyomo.repn', 'pyomo.neos', 'pyomo.solvers', + 'pyomo.solver', 'pyomo.gdp', 'pyomo.mpec', 'pyomo.dae', diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py new file mode 100644 index 00000000000..926ac346f32 --- /dev/null +++ b/pyomo/solver/plugins.py @@ -0,0 +1 @@ +from .base import SolverFactory From 6a84fcf77aff7ef00206035a408130dc8a200ac5 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 09:10:20 -0600 Subject: [PATCH 0047/3044] Trying again with plugins --- pyomo/solver/plugins.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index 926ac346f32..e15d1a585b1 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -1 +1,5 @@ from .base import SolverFactory + +def load(): + pass + From 2e1529828b3cfd6533f09936feccb5a27e92d1ad Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 09:12:39 -0600 Subject: [PATCH 0048/3044] PPlugins are my bane --- pyomo/contrib/appsi/plugins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index 75161e3548c..86dcd298a93 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,4 +1,5 @@ from pyomo.common.extensions import ExtensionBuilderFactory +from pyomo.solver.base import SolverFactory from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs from .build import AppsiBuilder From 94be7450f7f35ace9ece2d78c4ffcf4d167ac891 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 09:27:01 -0600 Subject: [PATCH 0049/3044] Remove use_extensions attribute --- pyomo/contrib/appsi/solvers/gurobi.py | 2 -- pyomo/contrib/appsi/solvers/highs.py | 2 -- .../contrib/appsi/solvers/tests/test_persistent_solvers.py | 7 ------- 3 files changed, 11 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 8aaae4e31d4..a02b8c55170 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -498,8 +498,6 @@ def set_instance(self, model): ) self._reinit() self._model = model - if self.use_extensions and cmodel_available: - self._expr_types = cmodel.PyomoExprTypes() if self.config.symbolic_solver_labels: self._labeler = TextLabeler() diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index c93d69527d8..a1477125ca9 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -343,8 +343,6 @@ def set_instance(self, model): ) self._reinit() self._model = model - if self.use_extensions and cmodel_available: - self._expr_types = cmodel.PyomoExprTypes() self._solver_model = highspy.Highs() self.add_block(model) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index fc97ba43fd0..3629aeceb1e 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1182,13 +1182,6 @@ def test_with_gdp( self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) - opt.use_extensions = True - res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) - self.assertAlmostEqual(m.x.value, 0) - self.assertAlmostEqual(m.y.value, 1) - @parameterized.expand(input=all_solvers) def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]): opt: PersistentSolver = opt_class(only_child_vars=False) From 4017abcc127da5fb05e1dbfd9377cf544205e2d4 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 09:40:00 -0600 Subject: [PATCH 0050/3044] Turn on pyomo.solver tests --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 0ac37747a65..ff8b5901189 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -602,7 +602,7 @@ jobs: run: | $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ - pyomo/contrib/appsi --junitxml="TEST-pyomo.xml" + pyomo/contrib/appsi pyomo/solver --junitxml="TEST-pyomo.xml" - name: Run Pyomo MPI tests if: matrix.mpi != 0 From ee064d2f09fdf372f5dc76ac1b7395031b2dd842 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 11:17:41 -0600 Subject: [PATCH 0051/3044] SAVE POINT: about to mess with persistent base --- pyomo/solver/base.py | 279 ++++++++++++++++---------------- pyomo/solver/tests/test_base.py | 66 ++++++++ 2 files changed, 206 insertions(+), 139 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 7a451e59c11..510b61f7479 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -262,145 +262,6 @@ def is_persistent(self): return False -class PersistentSolver(SolverBase): - def is_persistent(self): - return True - - def load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> NoReturn: - """ - Load the solution of the primal variables into the value attribute of the variables. - - Parameters - ---------- - vars_to_load: list - A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution - to all primal variables will be loaded. - """ - for v, val in self.get_primals(vars_to_load=vars_to_load).items(): - v.set_value(val, skip_validation=True) - StaleFlagManager.mark_all_as_stale(delayed=True) - - @abc.abstractmethod - def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - pass - - def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Declare sign convention in docstring here. - - Parameters - ---------- - cons_to_load: list - A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all - constraints will be loaded. - - Returns - ------- - duals: dict - Maps constraints to dual values - """ - raise NotImplementedError( - '{0} does not support the get_duals method'.format(type(self)) - ) - - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Parameters - ---------- - cons_to_load: list - A list of the constraints whose slacks should be loaded. If cons_to_load is None, then the slacks for all - constraints will be loaded. - - Returns - ------- - slacks: dict - Maps constraints to slack values - """ - raise NotImplementedError( - '{0} does not support the get_slacks method'.format(type(self)) - ) - - def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - """ - Parameters - ---------- - vars_to_load: list - A list of the variables whose reduced cost should be loaded. If vars_to_load is None, then all reduced costs - will be loaded. - - Returns - ------- - reduced_costs: ComponentMap - Maps variable to reduced cost - """ - raise NotImplementedError( - '{0} does not support the get_reduced_costs method'.format(type(self)) - ) - - @property - @abc.abstractmethod - def update_config(self) -> UpdateConfig: - pass - - @abc.abstractmethod - def set_instance(self, model): - pass - - @abc.abstractmethod - def add_variables(self, variables: List[_GeneralVarData]): - pass - - @abc.abstractmethod - def add_params(self, params: List[_ParamData]): - pass - - @abc.abstractmethod - def add_constraints(self, cons: List[_GeneralConstraintData]): - pass - - @abc.abstractmethod - def add_block(self, block: _BlockData): - pass - - @abc.abstractmethod - def remove_variables(self, variables: List[_GeneralVarData]): - pass - - @abc.abstractmethod - def remove_params(self, params: List[_ParamData]): - pass - - @abc.abstractmethod - def remove_constraints(self, cons: List[_GeneralConstraintData]): - pass - - @abc.abstractmethod - def remove_block(self, block: _BlockData): - pass - - @abc.abstractmethod - def set_objective(self, obj: _GeneralObjectiveData): - pass - - @abc.abstractmethod - def update_variables(self, variables: List[_GeneralVarData]): - pass - - @abc.abstractmethod - def update_params(self): - pass - - class PersistentBase(abc.ABC): def __init__(self, only_child_vars=False): self._model = None @@ -940,6 +801,146 @@ def update(self, timer: HierarchicalTimer = None): timer.stop('vars') +class PersistentSolver(SolverBase): + def is_persistent(self): + return True + + def load_vars( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> NoReturn: + """ + Load the solution of the primal variables into the value attribute of the variables. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution + to all primal variables will be loaded. + """ + for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + @abc.abstractmethod + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + pass + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Declare sign convention in docstring here. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all + constraints will be loaded. + + Returns + ------- + duals: dict + Maps constraints to dual values + """ + raise NotImplementedError( + '{0} does not support the get_duals method'.format(type(self)) + ) + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Parameters + ---------- + cons_to_load: list + A list of the constraints whose slacks should be loaded. If cons_to_load is None, then the slacks for all + constraints will be loaded. + + Returns + ------- + slacks: dict + Maps constraints to slack values + """ + raise NotImplementedError( + '{0} does not support the get_slacks method'.format(type(self)) + ) + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + """ + Parameters + ---------- + vars_to_load: list + A list of the variables whose reduced cost should be loaded. If vars_to_load is None, then all reduced costs + will be loaded. + + Returns + ------- + reduced_costs: ComponentMap + Maps variable to reduced cost + """ + raise NotImplementedError( + '{0} does not support the get_reduced_costs method'.format(type(self)) + ) + + @property + @abc.abstractmethod + def update_config(self) -> UpdateConfig: + pass + + @abc.abstractmethod + def set_instance(self, model): + pass + + @abc.abstractmethod + def add_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def add_params(self, params: List[_ParamData]): + pass + + @abc.abstractmethod + def add_constraints(self, cons: List[_GeneralConstraintData]): + pass + + @abc.abstractmethod + def add_block(self, block: _BlockData): + pass + + @abc.abstractmethod + def remove_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def remove_params(self, params: List[_ParamData]): + pass + + @abc.abstractmethod + def remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + @abc.abstractmethod + def remove_block(self, block: _BlockData): + pass + + @abc.abstractmethod + def set_objective(self, obj: _GeneralObjectiveData): + pass + + @abc.abstractmethod + def update_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def update_params(self): + pass + + + # Everything below here preserves backwards compatibility legacy_termination_condition_map = { diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index b5fcc4c4242..3c389175d08 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -4,6 +4,72 @@ from pyomo.core.base.var import ScalarVar +class TestTerminationCondition(unittest.TestCase): + def test_member_list(self): + member_list = base.TerminationCondition._member_names_ + expected_list = ['unknown', + 'convergenceCriteriaSatisfied', + 'maxTimeLimit', + 'iterationLimit', + 'objectiveLimit', + 'minStepLength', + 'unbounded', + 'provenInfeasible', + 'locallyInfeasible', + 'infeasibleOrUnbounded', + 'error', + 'interrupted', + 'licensingProblems'] + self.assertEqual(member_list, expected_list) + + def test_codes(self): + self.assertEqual(base.TerminationCondition.unknown.value, 42) + self.assertEqual(base.TerminationCondition.convergenceCriteriaSatisfied.value, 0) + self.assertEqual(base.TerminationCondition.maxTimeLimit.value, 1) + self.assertEqual(base.TerminationCondition.iterationLimit.value, 2) + self.assertEqual(base.TerminationCondition.objectiveLimit.value, 3) + self.assertEqual(base.TerminationCondition.minStepLength.value, 4) + self.assertEqual(base.TerminationCondition.unbounded.value, 5) + self.assertEqual(base.TerminationCondition.provenInfeasible.value, 6) + self.assertEqual(base.TerminationCondition.locallyInfeasible.value, 7) + self.assertEqual(base.TerminationCondition.infeasibleOrUnbounded.value, 8) + self.assertEqual(base.TerminationCondition.error.value, 9) + self.assertEqual(base.TerminationCondition.interrupted.value, 10) + self.assertEqual(base.TerminationCondition.licensingProblems.value, 11) + + +class TestSolutionStatus(unittest.TestCase): + def test_member_list(self): + member_list = base.SolutionStatus._member_names_ + expected_list = ['noSolution', 'infeasible', 'feasible', 'optimal'] + self.assertEqual(member_list, expected_list) + + def test_codes(self): + self.assertEqual(base.SolutionStatus.noSolution.value, 0) + self.assertEqual(base.SolutionStatus.infeasible.value, 10) + self.assertEqual(base.SolutionStatus.feasible.value, 20) + self.assertEqual(base.SolutionStatus.optimal.value, 30) + + +class TestSolverBase(unittest.TestCase): + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) + def test_solver_base(self): + self.instance = base.SolverBase() + self.assertFalse(self.instance.is_persistent()) + self.assertEqual(self.instance.version(), None) + self.assertEqual(self.instance.config, None) + self.assertEqual(self.instance.solve(None), None) + self.assertEqual(self.instance.available(), None) + + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) + def test_solver_availability(self): + self.instance = base.SolverBase() + self.instance.Availability._value_ = 1 + self.assertTrue(self.instance.Availability.__bool__(self.instance.Availability)) + self.instance.Availability._value_ = -1 + self.assertFalse(self.instance.Availability.__bool__(self.instance.Availability)) + + class TestResults(unittest.TestCase): def test_uninitialized(self): res = base.Results() From 9bcec5d863e9bb529b09e00096fea87f2b1fd784 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 12:06:10 -0600 Subject: [PATCH 0052/3044] Rename PersistentSolver to PersistentSolverBase; PersistentBase to PersistentSolverUtils --- pyomo/contrib/appsi/fbbt.py | 4 +- pyomo/contrib/appsi/solvers/cbc.py | 4 +- pyomo/contrib/appsi/solvers/cplex.py | 4 +- pyomo/contrib/appsi/solvers/gurobi.py | 6 +- pyomo/contrib/appsi/solvers/highs.py | 6 +- pyomo/contrib/appsi/solvers/ipopt.py | 4 +- .../solvers/tests/test_persistent_solvers.py | 142 ++--- pyomo/contrib/appsi/writers/lp_writer.py | 4 +- pyomo/contrib/appsi/writers/nl_writer.py | 4 +- pyomo/solver/base.py | 554 +---------------- pyomo/solver/util.py | 555 +++++++++++++++++- 11 files changed, 646 insertions(+), 641 deletions(-) diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 78137e790b6..cff1085de0d 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -1,4 +1,4 @@ -from pyomo.solver.base import PersistentBase +from pyomo.solver.util import PersistentSolverUtils from pyomo.common.config import ( ConfigDict, ConfigValue, @@ -59,7 +59,7 @@ def __init__( ) -class IntervalTightener(PersistentBase): +class IntervalTightener(PersistentSolverUtils): def __init__(self): super().__init__() self._config = IntervalConfig() diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index dd00089e84a..30250f66a86 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -22,7 +22,7 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase from pyomo.solver.config import InterfaceConfig from pyomo.solver.solution import PersistentSolutionLoader @@ -60,7 +60,7 @@ def __init__( self.log_level = logging.INFO -class Cbc(PersistentSolver): +class Cbc(PersistentSolverBase): def __init__(self, only_child_vars=False): self._config = CbcConfig() self._solver_options = {} diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 9f39528b0b0..0b1bd552370 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -19,7 +19,7 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase from pyomo.solver.config import MIPInterfaceConfig from pyomo.solver.solution import PersistentSolutionLoader @@ -62,7 +62,7 @@ def __init__(self, solver): self.solution_loader = PersistentSolutionLoader(solver=solver) -class Cplex(PersistentSolver): +class Cplex(PersistentSolverBase): _available = None def __init__(self, only_child_vars=False): diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index a02b8c55170..ba89c3e5d57 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -21,11 +21,11 @@ from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression -from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolver, PersistentBase +from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase from pyomo.solver.config import MIPInterfaceConfig from pyomo.solver.solution import PersistentSolutionLoader +from pyomo.solver.util import PersistentSolverUtils logger = logging.getLogger(__name__) @@ -221,7 +221,7 @@ def __init__(self): self.var2 = None -class Gurobi(PersistentBase, PersistentSolver): +class Gurobi(PersistentSolverUtils, PersistentSolverBase): """ Interface to Gurobi """ diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index a1477125ca9..4a23d7c309a 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -18,12 +18,12 @@ from pyomo.core.expr.numvalue import value, is_constant from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression -from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.common.dependencies import numpy as np from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolver, PersistentBase +from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase from pyomo.solver.config import MIPInterfaceConfig from pyomo.solver.solution import PersistentSolutionLoader +from pyomo.solver.util import PersistentSolverUtils logger = logging.getLogger(__name__) @@ -133,7 +133,7 @@ def update(self): self.highs.changeRowBounds(row_ndx, lb, ub) -class Highs(PersistentBase, PersistentSolver): +class Highs(PersistentSolverUtils, PersistentSolverBase): """ Interface to HiGHS """ diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index f754b5e85c0..467040a0967 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -26,7 +26,7 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase from pyomo.solver.config import InterfaceConfig from pyomo.solver.solution import PersistentSolutionLoader @@ -124,7 +124,7 @@ def __init__( } -class Ipopt(PersistentSolver): +class Ipopt(PersistentSolverBase): def __init__(self, only_child_vars=False): self._config = IpoptConfig() self._solver_options = {} diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 3629aeceb1e..1f357acf209 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -4,7 +4,7 @@ parameterized, param_available = attempt_import('parameterized') parameterized = parameterized.parameterized -from pyomo.solver.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Highs from typing import Type @@ -78,10 +78,10 @@ def _load_tests(solver_list, only_child_vars_list): class TestSolvers(unittest.TestCase): @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_remove_variable_and_objective( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): # this test is for issue #2888 - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -101,9 +101,9 @@ def test_remove_variable_and_objective( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_stale_vars( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -146,9 +146,9 @@ def test_stale_vars( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_range_constraint( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -169,9 +169,9 @@ def test_range_constraint( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -188,9 +188,9 @@ def test_reduced_costs( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs2( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -210,9 +210,9 @@ def test_reduced_costs2( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_param_changes( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -244,13 +244,13 @@ def test_param_changes( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_immutable_param( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): """ This test is important because component_data_objects returns immutable params as floats. We want to make sure we process these correctly. """ - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -282,9 +282,9 @@ def test_immutable_param( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_equality( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -316,9 +316,9 @@ def test_equality( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_linear_expression( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -352,9 +352,9 @@ def test_linear_expression( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_no_objective( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -386,9 +386,9 @@ def test_no_objective( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_remove_cons( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -438,9 +438,9 @@ def test_add_remove_cons( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_results_infeasible( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -485,8 +485,8 @@ def test_results_infeasible( res.solution_loader.get_reduced_costs() @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + def test_duals(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -509,9 +509,9 @@ def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_va @parameterized.expand(input=_load_tests(qcp_solvers, only_child_vars_options)) def test_mutable_quadratic_coefficient( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -533,9 +533,9 @@ def test_mutable_quadratic_coefficient( @parameterized.expand(input=_load_tests(qcp_solvers, only_child_vars_options)) def test_mutable_quadratic_objective( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -560,9 +560,9 @@ def test_mutable_quadratic_objective( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_vars( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) for treat_fixed_vars_as_params in [True, False]: opt.update_config.treat_fixed_vars_as_params = treat_fixed_vars_as_params if not opt.available(): @@ -600,9 +600,9 @@ def test_fixed_vars( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_vars_2( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest @@ -639,9 +639,9 @@ def test_fixed_vars_2( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_vars_3( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest @@ -656,9 +656,9 @@ def test_fixed_vars_3( @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) def test_fixed_vars_4( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest @@ -677,9 +677,9 @@ def test_fixed_vars_4( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_mutable_param_with_range( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest try: @@ -767,7 +767,7 @@ def test_mutable_param_with_range( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_and_remove_vars( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): @@ -815,7 +815,7 @@ def test_add_and_remove_vars( opt.load_vars([m.x]) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) - def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): + def test_exp(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -829,7 +829,7 @@ def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars self.assertAlmostEqual(m.y.value, 0.6529186341994245) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) - def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): + def test_log(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -844,9 +844,9 @@ def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_with_numpy( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -874,9 +874,9 @@ def test_with_numpy( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_bounds_with_params( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -908,9 +908,9 @@ def test_bounds_with_params( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_solution_loader( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -961,9 +961,9 @@ def test_solution_loader( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_time_limit( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest from sys import platform @@ -1017,9 +1017,9 @@ def test_time_limit( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_objective_changes( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -1078,9 +1078,9 @@ def test_objective_changes( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_domain( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -1104,9 +1104,9 @@ def test_domain( @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_domain_with_integers( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -1130,9 +1130,9 @@ def test_domain_with_integers( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_binaries( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -1147,7 +1147,7 @@ def test_fixed_binaries( res = opt.solve(m) self.assertAlmostEqual(res.best_feasible_objective, 1) - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) @@ -1158,9 +1158,9 @@ def test_fixed_binaries( @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( - self, name: str, opt_class: Type[PersistentSolver], only_child_vars + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars ): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -1183,8 +1183,8 @@ def test_with_gdp( self.assertAlmostEqual(m.y.value, 1) @parameterized.expand(input=all_solvers) - def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]): - opt: PersistentSolver = opt_class(only_child_vars=False) + def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolverBase]): + opt: PersistentSolverBase = opt_class(only_child_vars=False) if not opt.available(): raise unittest.SkipTest @@ -1210,8 +1210,8 @@ def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]) self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=all_solvers) - def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolver]): - opt: PersistentSolver = opt_class(only_child_vars=False) + def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolverBase]): + opt: PersistentSolverBase = opt_class(only_child_vars=False) if not opt.available(): raise unittest.SkipTest @@ -1245,8 +1245,8 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolver] self.assertNotIn(m.z, sol) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_bug_1(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): - opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + def test_bug_1(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): + opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -1271,7 +1271,7 @@ def test_bug_1(self, name: str, opt_class: Type[PersistentSolver], only_child_va @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') class TestLegacySolverInterface(unittest.TestCase): @parameterized.expand(input=all_solvers) - def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): + def test_param_updates(self, name: str, opt_class: Type[PersistentSolverBase]): opt = pe.SolverFactory('appsi_' + name) if not opt.available(exception_flag=False): raise unittest.SkipTest @@ -1301,7 +1301,7 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=all_solvers) - def test_load_solutions(self, name: str, opt_class: Type[PersistentSolver]): + def test_load_solutions(self, name: str, opt_class: Type[PersistentSolverBase]): opt = pe.SolverFactory('appsi_' + name) if not opt.available(exception_flag=False): raise unittest.SkipTest diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 6ebc26b7b31..8deb92640c1 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -8,12 +8,12 @@ from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.timing import HierarchicalTimer from pyomo.core.kernel.objective import minimize -from pyomo.solver.base import PersistentBase +from pyomo.solver.util import PersistentSolverUtils from .config import WriterConfig from ..cmodel import cmodel, cmodel_available -class LPWriter(PersistentBase): +class LPWriter(PersistentSolverUtils): def __init__(self, only_child_vars=False): super().__init__(only_child_vars=only_child_vars) self._config = WriterConfig() diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 39aed3732aa..e853e22c96f 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -13,12 +13,12 @@ from pyomo.core.kernel.objective import minimize from pyomo.common.collections import OrderedSet from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env -from pyomo.solver.base import PersistentBase +from pyomo.solver.base import PersistentSolverUtils from .config import WriterConfig from ..cmodel import cmodel, cmodel_available -class NLWriter(PersistentBase): +class NLWriter(PersistentSolverUtils): def __init__(self, only_child_vars=False): super().__init__(only_child_vars=only_child_vars) self._config = WriterConfig() diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 510b61f7479..332f8cddff4 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -20,14 +20,11 @@ List, Tuple, ) -from pyomo.core.base.constraint import _GeneralConstraintData, Constraint -from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint -from pyomo.core.base.var import _GeneralVarData, Var -from pyomo.core.base.param import _ParamData, Param +from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.common.collections import ComponentMap - from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError from pyomo.opt.base import SolverFactory as LegacySolverFactory @@ -45,11 +42,9 @@ from pyomo.core.kernel.objective import minimize from pyomo.core.base import SymbolMap from pyomo.core.staleflag import StaleFlagManager -from pyomo.core.expr.numvalue import NumericConstant - from pyomo.solver.config import UpdateConfig from pyomo.solver.solution import SolutionLoader, SolutionLoaderBase -from pyomo.solver.util import get_objective, collect_vars_and_named_exprs +from pyomo.solver.util import get_objective @@ -262,546 +257,7 @@ def is_persistent(self): return False -class PersistentBase(abc.ABC): - def __init__(self, only_child_vars=False): - self._model = None - self._active_constraints = {} # maps constraint to (lower, body, upper) - self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) - self._params = {} # maps param id to param - self._objective = None - self._objective_expr = None - self._objective_sense = None - self._named_expressions = ( - {} - ) # maps constraint to list of tuples (named_expr, named_expr.expr) - self._external_functions = ComponentMap() - self._obj_named_expressions = [] - self._update_config = UpdateConfig() - self._referenced_variables = ( - {} - ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] - self._vars_referenced_by_con = {} - self._vars_referenced_by_obj = [] - self._expr_types = None - self._only_child_vars = only_child_vars - - @property - def update_config(self): - return self._update_config - - @update_config.setter - def update_config(self, val: UpdateConfig): - self._update_config = val - - def set_instance(self, model): - saved_update_config = self.update_config - self.__init__() - self.update_config = saved_update_config - self._model = model - self.add_block(model) - if self._objective is None: - self.set_objective(None) - - @abc.abstractmethod - def _add_variables(self, variables: List[_GeneralVarData]): - pass - - def add_variables(self, variables: List[_GeneralVarData]): - for v in variables: - if id(v) in self._referenced_variables: - raise ValueError( - 'variable {name} has already been added'.format(name=v.name) - ) - self._referenced_variables[id(v)] = [{}, {}, None] - self._vars[id(v)] = ( - v, - v._lb, - v._ub, - v.fixed, - v.domain.get_interval(), - v.value, - ) - self._add_variables(variables) - - @abc.abstractmethod - def _add_params(self, params: List[_ParamData]): - pass - - def add_params(self, params: List[_ParamData]): - for p in params: - self._params[id(p)] = p - self._add_params(params) - - @abc.abstractmethod - def _add_constraints(self, cons: List[_GeneralConstraintData]): - pass - - def _check_for_new_vars(self, variables: List[_GeneralVarData]): - new_vars = {} - for v in variables: - v_id = id(v) - if v_id not in self._referenced_variables: - new_vars[v_id] = v - self.add_variables(list(new_vars.values())) - - def _check_to_remove_vars(self, variables: List[_GeneralVarData]): - vars_to_remove = {} - for v in variables: - v_id = id(v) - ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] - if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: - vars_to_remove[v_id] = v - self.remove_variables(list(vars_to_remove.values())) - - def add_constraints(self, cons: List[_GeneralConstraintData]): - all_fixed_vars = {} - for con in cons: - if con in self._named_expressions: - raise ValueError( - 'constraint {name} has already been added'.format(name=con.name) - ) - self._active_constraints[con] = (con.lower, con.body, con.upper) - tmp = collect_vars_and_named_exprs(con.body) - named_exprs, variables, fixed_vars, external_functions = tmp - if not self._only_child_vars: - self._check_for_new_vars(variables) - self._named_expressions[con] = [(e, e.expr) for e in named_exprs] - if len(external_functions) > 0: - self._external_functions[con] = external_functions - self._vars_referenced_by_con[con] = variables - for v in variables: - self._referenced_variables[id(v)][0][con] = None - if not self.update_config.treat_fixed_vars_as_params: - for v in fixed_vars: - v.unfix() - all_fixed_vars[id(v)] = v - self._add_constraints(cons) - for v in all_fixed_vars.values(): - v.fix() - - @abc.abstractmethod - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): - pass - - def add_sos_constraints(self, cons: List[_SOSConstraintData]): - for con in cons: - if con in self._vars_referenced_by_con: - raise ValueError( - 'constraint {name} has already been added'.format(name=con.name) - ) - self._active_constraints[con] = tuple() - variables = con.get_variables() - if not self._only_child_vars: - self._check_for_new_vars(variables) - self._named_expressions[con] = [] - self._vars_referenced_by_con[con] = variables - for v in variables: - self._referenced_variables[id(v)][1][con] = None - self._add_sos_constraints(cons) - - @abc.abstractmethod - def _set_objective(self, obj: _GeneralObjectiveData): - pass - - def set_objective(self, obj: _GeneralObjectiveData): - if self._objective is not None: - for v in self._vars_referenced_by_obj: - self._referenced_variables[id(v)][2] = None - if not self._only_child_vars: - self._check_to_remove_vars(self._vars_referenced_by_obj) - self._external_functions.pop(self._objective, None) - if obj is not None: - self._objective = obj - self._objective_expr = obj.expr - self._objective_sense = obj.sense - tmp = collect_vars_and_named_exprs(obj.expr) - named_exprs, variables, fixed_vars, external_functions = tmp - if not self._only_child_vars: - self._check_for_new_vars(variables) - self._obj_named_expressions = [(i, i.expr) for i in named_exprs] - if len(external_functions) > 0: - self._external_functions[obj] = external_functions - self._vars_referenced_by_obj = variables - for v in variables: - self._referenced_variables[id(v)][2] = obj - if not self.update_config.treat_fixed_vars_as_params: - for v in fixed_vars: - v.unfix() - self._set_objective(obj) - for v in fixed_vars: - v.fix() - else: - self._vars_referenced_by_obj = [] - self._objective = None - self._objective_expr = None - self._objective_sense = None - self._obj_named_expressions = [] - self._set_objective(obj) - - def add_block(self, block): - param_dict = {} - for p in block.component_objects(Param, descend_into=True): - if p.mutable: - for _p in p.values(): - param_dict[id(_p)] = _p - self.add_params(list(param_dict.values())) - if self._only_child_vars: - self.add_variables( - list( - dict( - (id(var), var) - for var in block.component_data_objects(Var, descend_into=True) - ).values() - ) - ) - self.add_constraints( - list(block.component_data_objects(Constraint, descend_into=True, active=True)) - ) - self.add_sos_constraints( - list(block.component_data_objects(SOSConstraint, descend_into=True, active=True)) - ) - obj = get_objective(block) - if obj is not None: - self.set_objective(obj) - - @abc.abstractmethod - def _remove_constraints(self, cons: List[_GeneralConstraintData]): - pass - - def remove_constraints(self, cons: List[_GeneralConstraintData]): - self._remove_constraints(cons) - for con in cons: - if con not in self._named_expressions: - raise ValueError( - 'cannot remove constraint {name} - it was not added'.format( - name=con.name - ) - ) - for v in self._vars_referenced_by_con[con]: - self._referenced_variables[id(v)][0].pop(con) - if not self._only_child_vars: - self._check_to_remove_vars(self._vars_referenced_by_con[con]) - del self._active_constraints[con] - del self._named_expressions[con] - self._external_functions.pop(con, None) - del self._vars_referenced_by_con[con] - - @abc.abstractmethod - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): - pass - - def remove_sos_constraints(self, cons: List[_SOSConstraintData]): - self._remove_sos_constraints(cons) - for con in cons: - if con not in self._vars_referenced_by_con: - raise ValueError( - 'cannot remove constraint {name} - it was not added'.format( - name=con.name - ) - ) - for v in self._vars_referenced_by_con[con]: - self._referenced_variables[id(v)][1].pop(con) - self._check_to_remove_vars(self._vars_referenced_by_con[con]) - del self._active_constraints[con] - del self._named_expressions[con] - del self._vars_referenced_by_con[con] - - @abc.abstractmethod - def _remove_variables(self, variables: List[_GeneralVarData]): - pass - - def remove_variables(self, variables: List[_GeneralVarData]): - self._remove_variables(variables) - for v in variables: - v_id = id(v) - if v_id not in self._referenced_variables: - raise ValueError( - 'cannot remove variable {name} - it has not been added'.format( - name=v.name - ) - ) - cons_using, sos_using, obj_using = self._referenced_variables[v_id] - if cons_using or sos_using or (obj_using is not None): - raise ValueError( - 'cannot remove variable {name} - it is still being used by constraints or the objective'.format( - name=v.name - ) - ) - del self._referenced_variables[v_id] - del self._vars[v_id] - - @abc.abstractmethod - def _remove_params(self, params: List[_ParamData]): - pass - - def remove_params(self, params: List[_ParamData]): - self._remove_params(params) - for p in params: - del self._params[id(p)] - - def remove_block(self, block): - self.remove_constraints( - list(block.component_data_objects(ctype=Constraint, descend_into=True, active=True)) - ) - self.remove_sos_constraints( - list(block.component_data_objects(ctype=SOSConstraint, descend_into=True, active=True)) - ) - if self._only_child_vars: - self.remove_variables( - list( - dict( - (id(var), var) - for var in block.component_data_objects( - ctype=Var, descend_into=True - ) - ).values() - ) - ) - self.remove_params( - list( - dict( - (id(p), p) - for p in block.component_data_objects( - ctype=Param, descend_into=True - ) - ).values() - ) - ) - - @abc.abstractmethod - def _update_variables(self, variables: List[_GeneralVarData]): - pass - - def update_variables(self, variables: List[_GeneralVarData]): - for v in variables: - self._vars[id(v)] = ( - v, - v._lb, - v._ub, - v.fixed, - v.domain.get_interval(), - v.value, - ) - self._update_variables(variables) - - @abc.abstractmethod - def update_params(self): - pass - - def update(self, timer: HierarchicalTimer = None): - if timer is None: - timer = HierarchicalTimer() - config = self.update_config - new_vars = [] - old_vars = [] - new_params = [] - old_params = [] - new_cons = [] - old_cons = [] - old_sos = [] - new_sos = [] - current_vars_dict = {} - current_cons_dict = {} - current_sos_dict = {} - timer.start('vars') - if self._only_child_vars and ( - config.check_for_new_or_removed_vars or config.update_vars - ): - current_vars_dict = { - id(v): v - for v in self._model.component_data_objects(Var, descend_into=True) - } - for v_id, v in current_vars_dict.items(): - if v_id not in self._vars: - new_vars.append(v) - for v_id, v_tuple in self._vars.items(): - if v_id not in current_vars_dict: - old_vars.append(v_tuple[0]) - elif config.update_vars: - start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} - timer.stop('vars') - timer.start('params') - if config.check_for_new_or_removed_params: - current_params_dict = {} - for p in self._model.component_objects(Param, descend_into=True): - if p.mutable: - for _p in p.values(): - current_params_dict[id(_p)] = _p - for p_id, p in current_params_dict.items(): - if p_id not in self._params: - new_params.append(p) - for p_id, p in self._params.items(): - if p_id not in current_params_dict: - old_params.append(p) - timer.stop('params') - timer.start('cons') - if config.check_for_new_or_removed_constraints or config.update_constraints: - current_cons_dict = { - c: None - for c in self._model.component_data_objects( - Constraint, descend_into=True, active=True - ) - } - current_sos_dict = { - c: None - for c in self._model.component_data_objects( - SOSConstraint, descend_into=True, active=True - ) - } - for c in current_cons_dict.keys(): - if c not in self._vars_referenced_by_con: - new_cons.append(c) - for c in current_sos_dict.keys(): - if c not in self._vars_referenced_by_con: - new_sos.append(c) - for c in self._vars_referenced_by_con.keys(): - if c not in current_cons_dict and c not in current_sos_dict: - if (c.ctype is Constraint) or ( - c.ctype is None and isinstance(c, _GeneralConstraintData) - ): - old_cons.append(c) - else: - assert (c.ctype is SOSConstraint) or ( - c.ctype is None and isinstance(c, _SOSConstraintData) - ) - old_sos.append(c) - self.remove_constraints(old_cons) - self.remove_sos_constraints(old_sos) - timer.stop('cons') - timer.start('params') - self.remove_params(old_params) - - # sticking this between removal and addition - # is important so that we don't do unnecessary work - if config.update_params: - self.update_params() - - self.add_params(new_params) - timer.stop('params') - timer.start('vars') - self.add_variables(new_vars) - timer.stop('vars') - timer.start('cons') - self.add_constraints(new_cons) - self.add_sos_constraints(new_sos) - new_cons_set = set(new_cons) - new_sos_set = set(new_sos) - new_vars_set = set(id(v) for v in new_vars) - cons_to_remove_and_add = {} - need_to_set_objective = False - if config.update_constraints: - cons_to_update = [] - sos_to_update = [] - for c in current_cons_dict.keys(): - if c not in new_cons_set: - cons_to_update.append(c) - for c in current_sos_dict.keys(): - if c not in new_sos_set: - sos_to_update.append(c) - for c in cons_to_update: - lower, body, upper = self._active_constraints[c] - new_lower, new_body, new_upper = c.lower, c.body, c.upper - if new_body is not body: - cons_to_remove_and_add[c] = None - continue - if new_lower is not lower: - if ( - type(new_lower) is NumericConstant - and type(lower) is NumericConstant - and new_lower.value == lower.value - ): - pass - else: - cons_to_remove_and_add[c] = None - continue - if new_upper is not upper: - if ( - type(new_upper) is NumericConstant - and type(upper) is NumericConstant - and new_upper.value == upper.value - ): - pass - else: - cons_to_remove_and_add[c] = None - continue - self.remove_sos_constraints(sos_to_update) - self.add_sos_constraints(sos_to_update) - timer.stop('cons') - timer.start('vars') - if self._only_child_vars and config.update_vars: - vars_to_check = [] - for v_id, v in current_vars_dict.items(): - if v_id not in new_vars_set: - vars_to_check.append(v) - elif config.update_vars: - end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} - vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] - if config.update_vars: - vars_to_update = [] - for v in vars_to_check: - _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] - if lb is not v._lb: - vars_to_update.append(v) - elif ub is not v._ub: - vars_to_update.append(v) - elif (fixed is not v.fixed) or (fixed and (value != v.value)): - vars_to_update.append(v) - if self.update_config.treat_fixed_vars_as_params: - for c in self._referenced_variables[id(v)][0]: - cons_to_remove_and_add[c] = None - if self._referenced_variables[id(v)][2] is not None: - need_to_set_objective = True - elif domain_interval != v.domain.get_interval(): - vars_to_update.append(v) - self.update_variables(vars_to_update) - timer.stop('vars') - timer.start('cons') - cons_to_remove_and_add = list(cons_to_remove_and_add.keys()) - self.remove_constraints(cons_to_remove_and_add) - self.add_constraints(cons_to_remove_and_add) - timer.stop('cons') - timer.start('named expressions') - if config.update_named_expressions: - cons_to_update = [] - for c, expr_list in self._named_expressions.items(): - if c in new_cons_set: - continue - for named_expr, old_expr in expr_list: - if named_expr.expr is not old_expr: - cons_to_update.append(c) - break - self.remove_constraints(cons_to_update) - self.add_constraints(cons_to_update) - for named_expr, old_expr in self._obj_named_expressions: - if named_expr.expr is not old_expr: - need_to_set_objective = True - break - timer.stop('named expressions') - timer.start('objective') - if self.update_config.check_for_new_objective: - pyomo_obj = get_objective(self._model) - if pyomo_obj is not self._objective: - need_to_set_objective = True - else: - pyomo_obj = self._objective - if self.update_config.update_objective: - if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: - need_to_set_objective = True - elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: - # we can definitely do something faster here than resetting the whole objective - need_to_set_objective = True - if need_to_set_objective: - self.set_objective(pyomo_obj) - timer.stop('objective') - - # this has to be done after the objective and constraints in case the - # old objective/constraints use old variables - timer.start('vars') - self.remove_variables(old_vars) - timer.stop('vars') - - -class PersistentSolver(SolverBase): +class PersistentSolverBase(SolverBase): def is_persistent(self): return True diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py index 4b8acf0de2e..fa2782f6bc4 100644 --- a/pyomo/solver/util.py +++ b/pyomo/solver/util.py @@ -9,9 +9,20 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.core.base.objective import Objective +import abc +from typing import List + from pyomo.core.expr.visitor import ExpressionValueVisitor, nonpyomo_leaf_types import pyomo.core.expr as EXPR +from pyomo.core.base.constraint import _GeneralConstraintData, Constraint +from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint +from pyomo.core.base.var import _GeneralVarData, Var +from pyomo.core.base.param import _ParamData, Param +from pyomo.core.base.objective import Objective, _GeneralObjectiveData +from pyomo.common.collections import ComponentMap +from pyomo.common.timing import HierarchicalTimer +from pyomo.core.expr.numvalue import NumericConstant +from pyomo.solver.config import UpdateConfig def get_objective(block): @@ -76,12 +87,550 @@ def collect_vars_and_named_exprs(expr): class SolverUtils: pass + class SubprocessSolverUtils: pass + class DirectSolverUtils: pass -class PersistentSolverUtils: - pass + +class PersistentSolverUtils(abc.ABC): + def __init__(self, only_child_vars=False): + self._model = None + self._active_constraints = {} # maps constraint to (lower, body, upper) + self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) + self._params = {} # maps param id to param + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._named_expressions = ( + {} + ) # maps constraint to list of tuples (named_expr, named_expr.expr) + self._external_functions = ComponentMap() + self._obj_named_expressions = [] + self._update_config = UpdateConfig() + self._referenced_variables = ( + {} + ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] + self._vars_referenced_by_con = {} + self._vars_referenced_by_obj = [] + self._expr_types = None + self._only_child_vars = only_child_vars + + @property + def update_config(self): + return self._update_config + + @update_config.setter + def update_config(self, val: UpdateConfig): + self._update_config = val + + def set_instance(self, model): + saved_update_config = self.update_config + self.__init__() + self.update_config = saved_update_config + self._model = model + self.add_block(model) + if self._objective is None: + self.set_objective(None) + + @abc.abstractmethod + def _add_variables(self, variables: List[_GeneralVarData]): + pass + + def add_variables(self, variables: List[_GeneralVarData]): + for v in variables: + if id(v) in self._referenced_variables: + raise ValueError( + 'variable {name} has already been added'.format(name=v.name) + ) + self._referenced_variables[id(v)] = [{}, {}, None] + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._add_variables(variables) + + @abc.abstractmethod + def _add_params(self, params: List[_ParamData]): + pass + + def add_params(self, params: List[_ParamData]): + for p in params: + self._params[id(p)] = p + self._add_params(params) + + @abc.abstractmethod + def _add_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def _check_for_new_vars(self, variables: List[_GeneralVarData]): + new_vars = {} + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + new_vars[v_id] = v + self.add_variables(list(new_vars.values())) + + def _check_to_remove_vars(self, variables: List[_GeneralVarData]): + vars_to_remove = {} + for v in variables: + v_id = id(v) + ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] + if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: + vars_to_remove[v_id] = v + self.remove_variables(list(vars_to_remove.values())) + + def add_constraints(self, cons: List[_GeneralConstraintData]): + all_fixed_vars = {} + for con in cons: + if con in self._named_expressions: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = (con.lower, con.body, con.upper) + tmp = collect_vars_and_named_exprs(con.body) + named_exprs, variables, fixed_vars, external_functions = tmp + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._named_expressions[con] = [(e, e.expr) for e in named_exprs] + if len(external_functions) > 0: + self._external_functions[con] = external_functions + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][0][con] = None + if not self.update_config.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + all_fixed_vars[id(v)] = v + self._add_constraints(cons) + for v in all_fixed_vars.values(): + v.fix() + + @abc.abstractmethod + def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def add_sos_constraints(self, cons: List[_SOSConstraintData]): + for con in cons: + if con in self._vars_referenced_by_con: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = tuple() + variables = con.get_variables() + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._named_expressions[con] = [] + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][1][con] = None + self._add_sos_constraints(cons) + + @abc.abstractmethod + def _set_objective(self, obj: _GeneralObjectiveData): + pass + + def set_objective(self, obj: _GeneralObjectiveData): + if self._objective is not None: + for v in self._vars_referenced_by_obj: + self._referenced_variables[id(v)][2] = None + if not self._only_child_vars: + self._check_to_remove_vars(self._vars_referenced_by_obj) + self._external_functions.pop(self._objective, None) + if obj is not None: + self._objective = obj + self._objective_expr = obj.expr + self._objective_sense = obj.sense + tmp = collect_vars_and_named_exprs(obj.expr) + named_exprs, variables, fixed_vars, external_functions = tmp + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._obj_named_expressions = [(i, i.expr) for i in named_exprs] + if len(external_functions) > 0: + self._external_functions[obj] = external_functions + self._vars_referenced_by_obj = variables + for v in variables: + self._referenced_variables[id(v)][2] = obj + if not self.update_config.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + self._set_objective(obj) + for v in fixed_vars: + v.fix() + else: + self._vars_referenced_by_obj = [] + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._obj_named_expressions = [] + self._set_objective(obj) + + def add_block(self, block): + param_dict = {} + for p in block.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + param_dict[id(_p)] = _p + self.add_params(list(param_dict.values())) + if self._only_child_vars: + self.add_variables( + list( + dict( + (id(var), var) + for var in block.component_data_objects(Var, descend_into=True) + ).values() + ) + ) + self.add_constraints( + list(block.component_data_objects(Constraint, descend_into=True, active=True)) + ) + self.add_sos_constraints( + list(block.component_data_objects(SOSConstraint, descend_into=True, active=True)) + ) + obj = get_objective(block) + if obj is not None: + self.set_objective(obj) + + @abc.abstractmethod + def _remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def remove_constraints(self, cons: List[_GeneralConstraintData]): + self._remove_constraints(cons) + for con in cons: + if con not in self._named_expressions: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][0].pop(con) + if not self._only_child_vars: + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + self._external_functions.pop(con, None) + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def remove_sos_constraints(self, cons: List[_SOSConstraintData]): + self._remove_sos_constraints(cons) + for con in cons: + if con not in self._vars_referenced_by_con: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][1].pop(con) + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_variables(self, variables: List[_GeneralVarData]): + pass + + def remove_variables(self, variables: List[_GeneralVarData]): + self._remove_variables(variables) + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + raise ValueError( + 'cannot remove variable {name} - it has not been added'.format( + name=v.name + ) + ) + cons_using, sos_using, obj_using = self._referenced_variables[v_id] + if cons_using or sos_using or (obj_using is not None): + raise ValueError( + 'cannot remove variable {name} - it is still being used by constraints or the objective'.format( + name=v.name + ) + ) + del self._referenced_variables[v_id] + del self._vars[v_id] + + @abc.abstractmethod + def _remove_params(self, params: List[_ParamData]): + pass + + def remove_params(self, params: List[_ParamData]): + self._remove_params(params) + for p in params: + del self._params[id(p)] + + def remove_block(self, block): + self.remove_constraints( + list(block.component_data_objects(ctype=Constraint, descend_into=True, active=True)) + ) + self.remove_sos_constraints( + list(block.component_data_objects(ctype=SOSConstraint, descend_into=True, active=True)) + ) + if self._only_child_vars: + self.remove_variables( + list( + dict( + (id(var), var) + for var in block.component_data_objects( + ctype=Var, descend_into=True + ) + ).values() + ) + ) + self.remove_params( + list( + dict( + (id(p), p) + for p in block.component_data_objects( + ctype=Param, descend_into=True + ) + ).values() + ) + ) + + @abc.abstractmethod + def _update_variables(self, variables: List[_GeneralVarData]): + pass + + def update_variables(self, variables: List[_GeneralVarData]): + for v in variables: + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._update_variables(variables) + + @abc.abstractmethod + def update_params(self): + pass + + def update(self, timer: HierarchicalTimer = None): + if timer is None: + timer = HierarchicalTimer() + config = self.update_config + new_vars = [] + old_vars = [] + new_params = [] + old_params = [] + new_cons = [] + old_cons = [] + old_sos = [] + new_sos = [] + current_vars_dict = {} + current_cons_dict = {} + current_sos_dict = {} + timer.start('vars') + if self._only_child_vars and ( + config.check_for_new_or_removed_vars or config.update_vars + ): + current_vars_dict = { + id(v): v + for v in self._model.component_data_objects(Var, descend_into=True) + } + for v_id, v in current_vars_dict.items(): + if v_id not in self._vars: + new_vars.append(v) + for v_id, v_tuple in self._vars.items(): + if v_id not in current_vars_dict: + old_vars.append(v_tuple[0]) + elif config.update_vars: + start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + timer.stop('vars') + timer.start('params') + if config.check_for_new_or_removed_params: + current_params_dict = {} + for p in self._model.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + current_params_dict[id(_p)] = _p + for p_id, p in current_params_dict.items(): + if p_id not in self._params: + new_params.append(p) + for p_id, p in self._params.items(): + if p_id not in current_params_dict: + old_params.append(p) + timer.stop('params') + timer.start('cons') + if config.check_for_new_or_removed_constraints or config.update_constraints: + current_cons_dict = { + c: None + for c in self._model.component_data_objects( + Constraint, descend_into=True, active=True + ) + } + current_sos_dict = { + c: None + for c in self._model.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + } + for c in current_cons_dict.keys(): + if c not in self._vars_referenced_by_con: + new_cons.append(c) + for c in current_sos_dict.keys(): + if c not in self._vars_referenced_by_con: + new_sos.append(c) + for c in self._vars_referenced_by_con.keys(): + if c not in current_cons_dict and c not in current_sos_dict: + if (c.ctype is Constraint) or ( + c.ctype is None and isinstance(c, _GeneralConstraintData) + ): + old_cons.append(c) + else: + assert (c.ctype is SOSConstraint) or ( + c.ctype is None and isinstance(c, _SOSConstraintData) + ) + old_sos.append(c) + self.remove_constraints(old_cons) + self.remove_sos_constraints(old_sos) + timer.stop('cons') + timer.start('params') + self.remove_params(old_params) + + # sticking this between removal and addition + # is important so that we don't do unnecessary work + if config.update_params: + self.update_params() + + self.add_params(new_params) + timer.stop('params') + timer.start('vars') + self.add_variables(new_vars) + timer.stop('vars') + timer.start('cons') + self.add_constraints(new_cons) + self.add_sos_constraints(new_sos) + new_cons_set = set(new_cons) + new_sos_set = set(new_sos) + new_vars_set = set(id(v) for v in new_vars) + cons_to_remove_and_add = {} + need_to_set_objective = False + if config.update_constraints: + cons_to_update = [] + sos_to_update = [] + for c in current_cons_dict.keys(): + if c not in new_cons_set: + cons_to_update.append(c) + for c in current_sos_dict.keys(): + if c not in new_sos_set: + sos_to_update.append(c) + for c in cons_to_update: + lower, body, upper = self._active_constraints[c] + new_lower, new_body, new_upper = c.lower, c.body, c.upper + if new_body is not body: + cons_to_remove_and_add[c] = None + continue + if new_lower is not lower: + if ( + type(new_lower) is NumericConstant + and type(lower) is NumericConstant + and new_lower.value == lower.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + if new_upper is not upper: + if ( + type(new_upper) is NumericConstant + and type(upper) is NumericConstant + and new_upper.value == upper.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + self.remove_sos_constraints(sos_to_update) + self.add_sos_constraints(sos_to_update) + timer.stop('cons') + timer.start('vars') + if self._only_child_vars and config.update_vars: + vars_to_check = [] + for v_id, v in current_vars_dict.items(): + if v_id not in new_vars_set: + vars_to_check.append(v) + elif config.update_vars: + end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] + if config.update_vars: + vars_to_update = [] + for v in vars_to_check: + _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] + if lb is not v._lb: + vars_to_update.append(v) + elif ub is not v._ub: + vars_to_update.append(v) + elif (fixed is not v.fixed) or (fixed and (value != v.value)): + vars_to_update.append(v) + if self.update_config.treat_fixed_vars_as_params: + for c in self._referenced_variables[id(v)][0]: + cons_to_remove_and_add[c] = None + if self._referenced_variables[id(v)][2] is not None: + need_to_set_objective = True + elif domain_interval != v.domain.get_interval(): + vars_to_update.append(v) + self.update_variables(vars_to_update) + timer.stop('vars') + timer.start('cons') + cons_to_remove_and_add = list(cons_to_remove_and_add.keys()) + self.remove_constraints(cons_to_remove_and_add) + self.add_constraints(cons_to_remove_and_add) + timer.stop('cons') + timer.start('named expressions') + if config.update_named_expressions: + cons_to_update = [] + for c, expr_list in self._named_expressions.items(): + if c in new_cons_set: + continue + for named_expr, old_expr in expr_list: + if named_expr.expr is not old_expr: + cons_to_update.append(c) + break + self.remove_constraints(cons_to_update) + self.add_constraints(cons_to_update) + for named_expr, old_expr in self._obj_named_expressions: + if named_expr.expr is not old_expr: + need_to_set_objective = True + break + timer.stop('named expressions') + timer.start('objective') + if self.update_config.check_for_new_objective: + pyomo_obj = get_objective(self._model) + if pyomo_obj is not self._objective: + need_to_set_objective = True + else: + pyomo_obj = self._objective + if self.update_config.update_objective: + if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: + need_to_set_objective = True + elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: + # we can definitely do something faster here than resetting the whole objective + need_to_set_objective = True + if need_to_set_objective: + self.set_objective(pyomo_obj) + timer.stop('objective') + + # this has to be done after the objective and constraints in case the + # old objective/constraints use old variables + timer.start('vars') + self.remove_variables(old_vars) + timer.stop('vars') From 54fa01c9d1d280ae24e207e8407752322a0ed69a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 12:09:40 -0600 Subject: [PATCH 0053/3044] Correct broken import --- pyomo/contrib/appsi/writers/nl_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index e853e22c96f..f6edc076b04 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -13,7 +13,7 @@ from pyomo.core.kernel.objective import minimize from pyomo.common.collections import OrderedSet from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env -from pyomo.solver.base import PersistentSolverUtils +from pyomo.solver.util import PersistentSolverUtils from .config import WriterConfig from ..cmodel import cmodel, cmodel_available From 511a54cfa75a63bba431aebd7cd13b1b531f3767 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 12:44:36 -0600 Subject: [PATCH 0054/3044] Add in util tests; reformat with black --- pyomo/contrib/appsi/build.py | 2 +- .../contrib/appsi/examples/getting_started.py | 5 +- pyomo/contrib/appsi/solvers/cbc.py | 12 +- pyomo/contrib/appsi/solvers/cplex.py | 9 +- pyomo/contrib/appsi/solvers/gurobi.py | 25 +-- pyomo/contrib/appsi/solvers/highs.py | 24 ++- pyomo/contrib/appsi/solvers/ipopt.py | 12 +- .../solvers/tests/test_gurobi_persistent.py | 4 +- .../solvers/tests/test_persistent_solvers.py | 148 +++++++++++++----- pyomo/contrib/appsi/writers/config.py | 2 +- pyomo/contrib/appsi/writers/nl_writer.py | 1 + pyomo/solver/__init__.py | 1 - pyomo/solver/base.py | 14 +- pyomo/solver/config.py | 7 +- pyomo/solver/plugins.py | 2 +- pyomo/solver/solution.py | 13 +- pyomo/solver/tests/test_base.py | 51 +++--- pyomo/solver/tests/test_config.py | 10 ++ pyomo/solver/tests/test_solution.py | 10 ++ pyomo/solver/tests/test_util.py | 75 +++++++++ pyomo/solver/util.py | 23 ++- 21 files changed, 329 insertions(+), 121 deletions(-) diff --git a/pyomo/contrib/appsi/build.py b/pyomo/contrib/appsi/build.py index 37826cf85fb..3d37135665a 100644 --- a/pyomo/contrib/appsi/build.py +++ b/pyomo/contrib/appsi/build.py @@ -116,7 +116,7 @@ def run(self): pybind11.setup_helpers.MACOS = original_pybind11_setup_helpers_macos -class AppsiBuilder(): +class AppsiBuilder: def __call__(self, parallel): return build_appsi() diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index d65430e3c23..de5357776f4 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -32,7 +32,10 @@ def main(plot=True, n_points=200): for p_val in p_values: m.p.value = p_val res = opt.solve(m, timer=timer) - assert res.termination_condition == solver_base.TerminationCondition.convergenceCriteriaSatisfied + assert ( + res.termination_condition + == solver_base.TerminationCondition.convergenceCriteriaSatisfied + ) obj_values.append(res.best_feasible_objective) opt.load_vars([m.x]) x_values.append(m.x.value) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 30250f66a86..021ff76217d 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -229,7 +229,9 @@ def _parse_soln(self): termination_line = all_lines[0].lower() obj_val = None if termination_line.startswith('optimal'): - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) obj_val = float(termination_line.split()[-1]) elif 'infeasible' in termination_line: results.termination_condition = TerminationCondition.provenInfeasible @@ -304,7 +306,8 @@ def _parse_soln(self): self._reduced_costs[v_id] = (v, -rc_val) if ( - results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition + == TerminationCondition.convergenceCriteriaSatisfied and self.config.load_solution ): for v_id, (v, val) in self._primal_sol.items(): @@ -313,7 +316,10 @@ def _parse_soln(self): results.best_feasible_objective = None else: results.best_feasible_objective = obj_val - elif results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied: + elif ( + results.termination_condition + == TerminationCondition.convergenceCriteriaSatisfied + ): if self._writer.get_active_objective() is None: results.best_feasible_objective = None else: diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 0b1bd552370..759bd7ff9d5 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -282,7 +282,9 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): status = cpxprob.solution.get_status() if status in [1, 101, 102]: - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) elif status in [2, 40, 118, 133, 134]: results.termination_condition = TerminationCondition.unbounded elif status in [4, 119, 134]: @@ -334,7 +336,10 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): 'results.best_feasible_objective before loading a solution.' ) else: - if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied: + if ( + results.termination_condition + != TerminationCondition.convergenceCriteriaSatisfied + ): logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index ba89c3e5d57..c2db835922d 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -97,7 +97,7 @@ def __init__(self, solver): self.solution_loader = GurobiSolutionLoader(solver=solver) -class _MutableLowerBound(): +class _MutableLowerBound: def __init__(self, expr): self.var = None self.expr = expr @@ -106,7 +106,7 @@ def update(self): self.var.setAttr('lb', value(self.expr)) -class _MutableUpperBound(): +class _MutableUpperBound: def __init__(self, expr): self.var = None self.expr = expr @@ -115,7 +115,7 @@ def update(self): self.var.setAttr('ub', value(self.expr)) -class _MutableLinearCoefficient(): +class _MutableLinearCoefficient: def __init__(self): self.expr = None self.var = None @@ -126,7 +126,7 @@ def update(self): self.gurobi_model.chgCoeff(self.con, self.var, value(self.expr)) -class _MutableRangeConstant(): +class _MutableRangeConstant: def __init__(self): self.lhs_expr = None self.rhs_expr = None @@ -142,7 +142,7 @@ def update(self): slack.ub = rhs_val - lhs_val -class _MutableConstant(): +class _MutableConstant: def __init__(self): self.expr = None self.con = None @@ -151,7 +151,7 @@ def update(self): self.con.rhs = value(self.expr) -class _MutableQuadraticConstraint(): +class _MutableQuadraticConstraint: def __init__( self, gurobi_model, gurobi_con, constant, linear_coefs, quadratic_coefs ): @@ -186,7 +186,7 @@ def get_updated_rhs(self): return value(self.constant.expr) -class _MutableObjective(): +class _MutableObjective: def __init__(self, gurobi_model, constant, linear_coefs, quadratic_coefs): self.gurobi_model = gurobi_model self.constant = constant @@ -214,7 +214,7 @@ def get_updated_expression(self): return gurobi_expr -class _MutableQuadraticCoefficient(): +class _MutableQuadraticCoefficient: def __init__(self): self.expr = None self.var1 = None @@ -869,7 +869,9 @@ def _postsolve(self, timer: HierarchicalTimer): if status == grb.LOADED: # problem is loaded, but no solution results.termination_condition = TerminationCondition.unknown elif status == grb.OPTIMAL: # optimal - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) elif status == grb.INFEASIBLE: results.termination_condition = TerminationCondition.provenInfeasible elif status == grb.INF_OR_UNBD: @@ -920,7 +922,10 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start('load solution') if config.load_solution: if gprob.SolCount > 0: - if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied: + if ( + results.termination_condition + != TerminationCondition.convergenceCriteriaSatisfied + ): logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 4a23d7c309a..3b7c92ed9e8 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -67,7 +67,7 @@ def __init__(self, solver): self.solution_loader = PersistentSolutionLoader(solver=solver) -class _MutableVarBounds(): +class _MutableVarBounds: def __init__(self, lower_expr, upper_expr, pyomo_var_id, var_map, highs): self.pyomo_var_id = pyomo_var_id self.lower_expr = lower_expr @@ -82,7 +82,7 @@ def update(self): self.highs.changeColBounds(col_ndx, lb, ub) -class _MutableLinearCoefficient(): +class _MutableLinearCoefficient: def __init__(self, pyomo_con, pyomo_var_id, con_map, var_map, expr, highs): self.expr = expr self.highs = highs @@ -97,7 +97,7 @@ def update(self): self.highs.changeCoeff(row_ndx, col_ndx, value(self.expr)) -class _MutableObjectiveCoefficient(): +class _MutableObjectiveCoefficient: def __init__(self, pyomo_var_id, var_map, expr, highs): self.expr = expr self.highs = highs @@ -109,7 +109,7 @@ def update(self): self.highs.changeColCost(col_ndx, value(self.expr)) -class _MutableObjectiveOffset(): +class _MutableObjectiveOffset: def __init__(self, expr, highs): self.expr = expr self.highs = highs @@ -118,7 +118,7 @@ def update(self): self.highs.changeObjectiveOffset(value(self.expr)) -class _MutableConstraintBounds(): +class _MutableConstraintBounds: def __init__(self, lower_expr, upper_expr, pyomo_con, con_map, highs): self.lower_expr = lower_expr self.upper_expr = upper_expr @@ -604,7 +604,9 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == highspy.HighsModelStatus.kModelEmpty: results.termination_condition = TerminationCondition.unknown elif status == highspy.HighsModelStatus.kOptimal: - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) elif status == highspy.HighsModelStatus.kInfeasible: results.termination_condition = TerminationCondition.provenInfeasible elif status == highspy.HighsModelStatus.kUnboundedOrInfeasible: @@ -627,7 +629,10 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start('load solution') self._sol = highs.getSolution() has_feasible_solution = False - if results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied: + if ( + results.termination_condition + == TerminationCondition.convergenceCriteriaSatisfied + ): has_feasible_solution = True elif results.termination_condition in { TerminationCondition.objectiveLimit, @@ -639,7 +644,10 @@ def _postsolve(self, timer: HierarchicalTimer): if config.load_solution: if has_feasible_solution: - if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied: + if ( + results.termination_condition + != TerminationCondition.convergenceCriteriaSatisfied + ): logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 467040a0967..6c4b7601d2c 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -300,7 +300,9 @@ def _parse_sol(self): termination_line = all_lines[1] if 'Optimal Solution Found' in termination_line: - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) elif 'Problem may be infeasible' in termination_line: results.termination_condition = TerminationCondition.locallyInfeasible elif 'problem might be unbounded' in termination_line: @@ -381,7 +383,8 @@ def _parse_sol(self): self._reduced_costs[var] = 0 if ( - results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition + == TerminationCondition.convergenceCriteriaSatisfied and self.config.load_solution ): for v, val in self._primal_sol.items(): @@ -392,7 +395,10 @@ def _parse_sol(self): results.best_feasible_objective = value( self._writer.get_active_objective().expr ) - elif results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied: + elif ( + results.termination_condition + == TerminationCondition.convergenceCriteriaSatisfied + ): if self._writer.get_active_objective() is None: results.best_feasible_objective = None else: diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index 877d0971f2b..9fdce87b8de 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -157,7 +157,9 @@ def test_lp(self): res = opt.solve(self.m) self.assertAlmostEqual(x + y, res.best_feasible_objective) self.assertAlmostEqual(x + y, res.best_objective_bound) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertTrue(res.best_feasible_objective is not None) self.assertAlmostEqual(x, self.m.x.value) self.assertAlmostEqual(y, self.m.y.value) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 1f357acf209..bf92244ec36 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -18,11 +18,7 @@ if not param_available: raise unittest.SkipTest('Parameterized is not available.') -all_solvers = [ - ('gurobi', Gurobi), - ('ipopt', Ipopt), - ('highs', Highs), -] +all_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt), ('highs', Highs)] mip_solvers = [('gurobi', Gurobi), ('highs', Highs)] nlp_solvers = [('ipopt', Ipopt)] qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] @@ -88,7 +84,9 @@ def test_remove_variable_and_objective( m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, 2) del m.x @@ -96,7 +94,9 @@ def test_remove_variable_and_objective( m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) @@ -156,13 +156,17 @@ def test_range_constraint( m.obj = pe.Objective(expr=m.x) m.c = pe.Constraint(expr=(-1, m.x, 1)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, -1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, 1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) @@ -179,7 +183,9 @@ def test_reduced_costs( m.y = pe.Var(bounds=(-2, 2)) m.obj = pe.Objective(expr=3 * m.x + 4 * m.y) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, -2) rc = opt.get_reduced_costs() @@ -197,13 +203,17 @@ def test_reduced_costs2( m.x = pe.Var(bounds=(-1, 1)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, -1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, 1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) @@ -233,7 +243,10 @@ def test_param_changes( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, + TerminationCondition.convergenceCriteriaSatisfied, + ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -271,7 +284,10 @@ def test_immutable_param( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, + TerminationCondition.convergenceCriteriaSatisfied, + ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -305,7 +321,10 @@ def test_equality( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, + TerminationCondition.convergenceCriteriaSatisfied, + ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -345,7 +364,10 @@ def test_linear_expression( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, + TerminationCondition.convergenceCriteriaSatisfied, + ) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) @@ -375,7 +397,10 @@ def test_no_objective( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, + TerminationCondition.convergenceCriteriaSatisfied, + ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertEqual(res.best_feasible_objective, None) @@ -404,7 +429,9 @@ def test_add_remove_cons( m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -415,7 +442,9 @@ def test_add_remove_cons( m.c3 = pe.Constraint(expr=m.y >= a3 * m.x + b3) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, (b3 - b1) / (a1 - a3)) self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -427,7 +456,9 @@ def test_add_remove_cons( del m.c3 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) @@ -453,7 +484,9 @@ def test_results_infeasible( res = opt.solve(m) opt.config.load_solution = False res = opt.solve(m) - self.assertNotEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertNotEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) if opt_class is Ipopt: acceptable_termination_conditions = { TerminationCondition.provenInfeasible, @@ -485,7 +518,9 @@ def test_results_infeasible( res.solution_loader.get_reduced_costs() @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_duals(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): + def test_duals( + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + ): opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -747,7 +782,10 @@ def test_mutable_param_with_range( m.c2.value = float(c2) m.obj.sense = sense res: Results = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, + TerminationCondition.convergenceCriteriaSatisfied, + ) if sense is pe.minimize: self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) @@ -784,7 +822,9 @@ def test_add_and_remove_vars( opt.update_config.check_for_new_or_removed_vars = False opt.config.load_solution = False res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) opt.load_vars() self.assertAlmostEqual(m.y.value, -1) m.x = pe.Var() @@ -798,7 +838,9 @@ def test_add_and_remove_vars( opt.add_variables([m.x]) opt.add_constraints([m.c1, m.c2]) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) opt.load_vars() self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @@ -807,7 +849,9 @@ def test_add_and_remove_vars( opt.remove_variables([m.x]) m.x.value = None res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) opt.load_vars() self.assertEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, -1) @@ -815,7 +859,9 @@ def test_add_and_remove_vars( opt.load_vars([m.x]) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) - def test_exp(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): + def test_exp( + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + ): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -829,7 +875,9 @@ def test_exp(self, name: str, opt_class: Type[PersistentSolverBase], only_child_ self.assertAlmostEqual(m.y.value, 0.6529186341994245) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) - def test_log(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): + def test_log( + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + ): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -868,7 +916,9 @@ def test_with_numpy( ) ) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @@ -1011,9 +1061,7 @@ def test_time_limit( opt.config.time_limit = 0 opt.config.load_solution = False res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.maxTimeLimit - ) + self.assertEqual(res.termination_condition, TerminationCondition.maxTimeLimit) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_objective_changes( @@ -1183,7 +1231,9 @@ def test_with_gdp( self.assertAlmostEqual(m.y.value, 1) @parameterized.expand(input=all_solvers) - def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolverBase]): + def test_variables_elsewhere( + self, name: str, opt_class: Type[PersistentSolverBase] + ): opt: PersistentSolverBase = opt_class(only_child_vars=False) if not opt.available(): raise unittest.SkipTest @@ -1197,20 +1247,26 @@ def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolverBa m.b.c2 = pe.Constraint(expr=m.y >= -m.x) res = opt.solve(m.b) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(res.best_feasible_objective, 1) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, 1) m.x.setlb(0) res = opt.solve(m.b) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(res.best_feasible_objective, 2) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=all_solvers) - def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolverBase]): + def test_variables_elsewhere2( + self, name: str, opt_class: Type[PersistentSolverBase] + ): opt: PersistentSolverBase = opt_class(only_child_vars=False) if not opt.available(): raise unittest.SkipTest @@ -1227,7 +1283,9 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolverB m.c4 = pe.Constraint(expr=m.y >= -m.z + 1) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(res.best_feasible_objective, 1) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) @@ -1237,7 +1295,9 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolverB del m.c3 del m.c4 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(res.best_feasible_objective, 0) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) @@ -1245,7 +1305,9 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolverB self.assertNotIn(m.z, sol) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_bug_1(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): + def test_bug_1( + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + ): opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -1259,12 +1321,16 @@ def test_bug_1(self, name: str, opt_class: Type[PersistentSolverBase], only_chil m.c = pe.Constraint(expr=m.y >= m.p * m.x) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(res.best_feasible_objective, 0) m.p.value = 1 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertAlmostEqual(res.best_feasible_objective, 3) diff --git a/pyomo/contrib/appsi/writers/config.py b/pyomo/contrib/appsi/writers/config.py index 4376b9284fa..2a4e638f097 100644 --- a/pyomo/contrib/appsi/writers/config.py +++ b/pyomo/contrib/appsi/writers/config.py @@ -1,3 +1,3 @@ -class WriterConfig(): +class WriterConfig: def __init__(self): self.symbolic_solver_labels = False diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index f6edc076b04..1be657ba762 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -18,6 +18,7 @@ from .config import WriterConfig from ..cmodel import cmodel, cmodel_available + class NLWriter(PersistentSolverUtils): def __init__(self, only_child_vars=False): super().__init__(only_child_vars=only_child_vars) diff --git a/pyomo/solver/__init__.py b/pyomo/solver/__init__.py index 13b8b463662..a3c2e0e95e8 100644 --- a/pyomo/solver/__init__.py +++ b/pyomo/solver/__init__.py @@ -13,4 +13,3 @@ from . import config from . import solution from . import util - diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 332f8cddff4..f0a07d0aca3 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -11,15 +11,7 @@ import abc import enum -from typing import ( - Sequence, - Dict, - Optional, - Mapping, - NoReturn, - List, - Tuple, -) +from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData @@ -47,7 +39,6 @@ from pyomo.solver.util import get_objective - class TerminationCondition(enum.Enum): """ An enumeration for checking the termination condition of solvers @@ -97,7 +88,7 @@ class SolutionStatus(enum.IntEnum): """ An enumeration for interpreting the result of a termination. This describes the designated status by the solver to be loaded back into the model. - + For now, we are choosing to use IntEnum such that return values are numerically assigned in increasing order. """ @@ -396,7 +387,6 @@ def update_params(self): pass - # Everything below here preserves backwards compatibility legacy_termination_condition_map = { diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index ab9c30a0549..f446dc714db 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -10,7 +10,12 @@ # ___________________________________________________________________________ from typing import Optional -from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat, NonNegativeInt +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + NonNegativeFloat, + NonNegativeInt, +) class InterfaceConfig(ConfigDict): diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index e15d1a585b1..7e479474605 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -1,5 +1,5 @@ from .base import SolverFactory + def load(): pass - diff --git a/pyomo/solver/solution.py b/pyomo/solver/solution.py index 2d422736f2c..1ef79050701 100644 --- a/pyomo/solver/solution.py +++ b/pyomo/solver/solution.py @@ -10,14 +10,7 @@ # ___________________________________________________________________________ import abc -from typing import ( - Sequence, - Dict, - Optional, - Mapping, - MutableMapping, - NoReturn, -) +from typing import Sequence, Dict, Optional, Mapping, MutableMapping, NoReturn from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData @@ -250,7 +243,3 @@ def get_reduced_costs( def invalidate(self): self._valid = False - - - - diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index 3c389175d08..dcbe13e8230 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common import unittest from pyomo.solver import base import pyomo.environ as pe @@ -7,24 +18,28 @@ class TestTerminationCondition(unittest.TestCase): def test_member_list(self): member_list = base.TerminationCondition._member_names_ - expected_list = ['unknown', - 'convergenceCriteriaSatisfied', - 'maxTimeLimit', - 'iterationLimit', - 'objectiveLimit', - 'minStepLength', - 'unbounded', - 'provenInfeasible', - 'locallyInfeasible', - 'infeasibleOrUnbounded', - 'error', - 'interrupted', - 'licensingProblems'] + expected_list = [ + 'unknown', + 'convergenceCriteriaSatisfied', + 'maxTimeLimit', + 'iterationLimit', + 'objectiveLimit', + 'minStepLength', + 'unbounded', + 'provenInfeasible', + 'locallyInfeasible', + 'infeasibleOrUnbounded', + 'error', + 'interrupted', + 'licensingProblems', + ] self.assertEqual(member_list, expected_list) def test_codes(self): self.assertEqual(base.TerminationCondition.unknown.value, 42) - self.assertEqual(base.TerminationCondition.convergenceCriteriaSatisfied.value, 0) + self.assertEqual( + base.TerminationCondition.convergenceCriteriaSatisfied.value, 0 + ) self.assertEqual(base.TerminationCondition.maxTimeLimit.value, 1) self.assertEqual(base.TerminationCondition.iterationLimit.value, 2) self.assertEqual(base.TerminationCondition.objectiveLimit.value, 3) @@ -67,7 +82,9 @@ def test_solver_availability(self): self.instance.Availability._value_ = 1 self.assertTrue(self.instance.Availability.__bool__(self.instance.Availability)) self.instance.Availability._value_ = -1 - self.assertFalse(self.instance.Availability.__bool__(self.instance.Availability)) + self.assertFalse( + self.instance.Availability.__bool__(self.instance.Availability) + ) class TestResults(unittest.TestCase): @@ -75,9 +92,7 @@ def test_uninitialized(self): res = base.Results() self.assertIsNone(res.best_feasible_objective) self.assertIsNone(res.best_objective_bound) - self.assertEqual( - res.termination_condition, base.TerminationCondition.unknown - ) + self.assertEqual(res.termination_condition, base.TerminationCondition.unknown) with self.assertRaisesRegex( RuntimeError, '.*does not currently have a valid solution.*' diff --git a/pyomo/solver/tests/test_config.py b/pyomo/solver/tests/test_config.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/solver/tests/test_config.py +++ b/pyomo/solver/tests/test_config.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/solver/tests/test_solution.py b/pyomo/solver/tests/test_solution.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/solver/tests/test_solution.py +++ b/pyomo/solver/tests/test_solution.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/solver/tests/test_util.py b/pyomo/solver/tests/test_util.py index e69de29bb2d..737a271d603 100644 --- a/pyomo/solver/tests/test_util.py +++ b/pyomo/solver/tests/test_util.py @@ -0,0 +1,75 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest +import pyomo.environ as pyo +from pyomo.solver.util import collect_vars_and_named_exprs, get_objective +from typing import Callable +from pyomo.common.gsl import find_GSL + + +class TestGenericUtils(unittest.TestCase): + def basics_helper(self, collector: Callable, *args): + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.y = pyo.Var() + m.z = pyo.Var() + m.E = pyo.Expression(expr=2 * m.z + 1) + m.y.fix(3) + e = m.x * m.y + m.x * m.E + named_exprs, var_list, fixed_vars, external_funcs = collector(e, *args) + self.assertEqual([m.E], named_exprs) + self.assertEqual([m.x, m.y, m.z], var_list) + self.assertEqual([m.y], fixed_vars) + self.assertEqual([], external_funcs) + + def test_collect_vars_basics(self): + self.basics_helper(collect_vars_and_named_exprs) + + def external_func_helper(self, collector: Callable, *args): + DLL = find_GSL() + if not DLL: + self.skipTest('Could not find amplgsl.dll library') + + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.y = pyo.Var() + m.z = pyo.Var() + m.hypot = pyo.ExternalFunction(library=DLL, function='gsl_hypot') + func = m.hypot(m.x, m.x * m.y) + m.E = pyo.Expression(expr=2 * func) + m.y.fix(3) + e = m.z + m.x * m.E + named_exprs, var_list, fixed_vars, external_funcs = collector(e, *args) + self.assertEqual([m.E], named_exprs) + self.assertEqual([m.z, m.x, m.y], var_list) + self.assertEqual([m.y], fixed_vars) + self.assertEqual([func], external_funcs) + + def test_collect_vars_external(self): + self.external_func_helper(collect_vars_and_named_exprs) + + def simple_model(self): + model = pyo.ConcreteModel() + model.x = pyo.Var([1, 2], domain=pyo.NonNegativeReals) + model.OBJ = pyo.Objective(expr=2 * model.x[1] + 3 * model.x[2]) + model.Constraint1 = pyo.Constraint(expr=3 * model.x[1] + 4 * model.x[2] >= 1) + return model + + def test_get_objective_success(self): + model = self.simple_model() + self.assertEqual(model.OBJ, get_objective(model)) + + def test_get_objective_raise(self): + model = self.simple_model() + model.OBJ2 = pyo.Objective(expr=model.x[1] - 4 * model.x[2]) + with self.assertRaises(ValueError): + get_objective(model) diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py index fa2782f6bc4..1fb1738470b 100644 --- a/pyomo/solver/util.py +++ b/pyomo/solver/util.py @@ -289,10 +289,16 @@ def add_block(self, block): ) ) self.add_constraints( - list(block.component_data_objects(Constraint, descend_into=True, active=True)) + list( + block.component_data_objects(Constraint, descend_into=True, active=True) + ) ) self.add_sos_constraints( - list(block.component_data_objects(SOSConstraint, descend_into=True, active=True)) + list( + block.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + ) ) obj = get_objective(block) if obj is not None: @@ -375,10 +381,18 @@ def remove_params(self, params: List[_ParamData]): def remove_block(self, block): self.remove_constraints( - list(block.component_data_objects(ctype=Constraint, descend_into=True, active=True)) + list( + block.component_data_objects( + ctype=Constraint, descend_into=True, active=True + ) + ) ) self.remove_sos_constraints( - list(block.component_data_objects(ctype=SOSConstraint, descend_into=True, active=True)) + list( + block.component_data_objects( + ctype=SOSConstraint, descend_into=True, active=True + ) + ) ) if self._only_child_vars: self.remove_variables( @@ -633,4 +647,3 @@ def update(self, timer: HierarchicalTimer = None): timer.start('vars') self.remove_variables(old_vars) timer.stop('vars') - From 6cf3f8221761e6f97ebe0b92695debf78312ebf6 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 13:41:44 -0600 Subject: [PATCH 0055/3044] Add more unit tests --- pyomo/solver/tests/test_base.py | 49 +++++++++++++++++++++++++++++++ pyomo/solver/tests/test_config.py | 47 +++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index dcbe13e8230..355941a1eb1 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -87,6 +87,55 @@ def test_solver_availability(self): ) +class TestPersistentSolverBase(unittest.TestCase): + def test_abstract_member_list(self): + expected_list = ['remove_params', + 'version', + 'config', + 'update_variables', + 'remove_variables', + 'add_constraints', + 'get_primals', + 'set_instance', + 'set_objective', + 'update_params', + 'remove_block', + 'add_block', + 'available', + 'update_config', + 'add_params', + 'remove_constraints', + 'add_variables', + 'solve'] + member_list = list(base.PersistentSolverBase.__abstractmethods__) + self.assertEqual(sorted(expected_list), sorted(member_list)) + + @unittest.mock.patch.multiple(base.PersistentSolverBase, __abstractmethods__=set()) + def test_persistent_solver_base(self): + self.instance = base.PersistentSolverBase() + self.assertTrue(self.instance.is_persistent()) + self.assertEqual(self.instance.get_primals(), None) + self.assertEqual(self.instance.update_config, None) + self.assertEqual(self.instance.set_instance(None), None) + self.assertEqual(self.instance.add_variables(None), None) + self.assertEqual(self.instance.add_params(None), None) + self.assertEqual(self.instance.add_constraints(None), None) + self.assertEqual(self.instance.add_block(None), None) + self.assertEqual(self.instance.remove_variables(None), None) + self.assertEqual(self.instance.remove_params(None), None) + self.assertEqual(self.instance.remove_constraints(None), None) + self.assertEqual(self.instance.remove_block(None), None) + self.assertEqual(self.instance.set_objective(None), None) + self.assertEqual(self.instance.update_variables(None), None) + self.assertEqual(self.instance.update_params(), None) + with self.assertRaises(NotImplementedError): + self.instance.get_duals() + with self.assertRaises(NotImplementedError): + self.instance.get_slacks() + with self.assertRaises(NotImplementedError): + self.instance.get_reduced_costs() + + class TestResults(unittest.TestCase): def test_uninitialized(self): res = base.Results() diff --git a/pyomo/solver/tests/test_config.py b/pyomo/solver/tests/test_config.py index d93cfd77b3c..378facb58d2 100644 --- a/pyomo/solver/tests/test_config.py +++ b/pyomo/solver/tests/test_config.py @@ -8,3 +8,50 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.solver.config import InterfaceConfig, MIPInterfaceConfig + +class TestInterfaceConfig(unittest.TestCase): + + def test_interface_default_instantiation(self): + config = InterfaceConfig() + self.assertEqual(config._description, None) + self.assertEqual(config._visibility, 0) + self.assertFalse(config.tee) + self.assertTrue(config.load_solution) + self.assertFalse(config.symbolic_solver_labels) + self.assertFalse(config.report_timing) + + def test_interface_custom_instantiation(self): + config = InterfaceConfig(description="A description") + config.tee = True + self.assertTrue(config.tee) + self.assertEqual(config._description, "A description") + self.assertFalse(config.time_limit) + config.time_limit = 1.0 + self.assertEqual(config.time_limit, 1.0) + + +class TestMIPInterfaceConfig(unittest.TestCase): + def test_interface_default_instantiation(self): + config = MIPInterfaceConfig() + self.assertEqual(config._description, None) + self.assertEqual(config._visibility, 0) + self.assertFalse(config.tee) + self.assertTrue(config.load_solution) + self.assertFalse(config.symbolic_solver_labels) + self.assertFalse(config.report_timing) + self.assertEqual(config.mip_gap, None) + self.assertFalse(config.relax_integrality) + + def test_interface_custom_instantiation(self): + config = MIPInterfaceConfig(description="A description") + config.tee = True + self.assertTrue(config.tee) + self.assertEqual(config._description, "A description") + self.assertFalse(config.time_limit) + config.time_limit = 1.0 + self.assertEqual(config.time_limit, 1.0) + config.mip_gap = 2.5 + self.assertEqual(config.mip_gap, 2.5) From 4d3191aa11d5f69695e4ea469655a896b09f35d1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 13:49:07 -0600 Subject: [PATCH 0056/3044] Add more unit tests --- pyomo/solver/tests/test_solution.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pyomo/solver/tests/test_solution.py b/pyomo/solver/tests/test_solution.py index d93cfd77b3c..c4c2f790b55 100644 --- a/pyomo/solver/tests/test_solution.py +++ b/pyomo/solver/tests/test_solution.py @@ -8,3 +8,23 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.solver import solution + +class TestPersistentSolverBase(unittest.TestCase): + def test_abstract_member_list(self): + expected_list = ['get_primals'] + member_list = list(solution.SolutionLoaderBase.__abstractmethods__) + self.assertEqual(sorted(expected_list), sorted(member_list)) + + @unittest.mock.patch.multiple(solution.SolutionLoaderBase, __abstractmethods__=set()) + def test_solution_loader_base(self): + self.instance = solution.SolutionLoaderBase() + self.assertEqual(self.instance.get_primals(), None) + with self.assertRaises(NotImplementedError): + self.instance.get_duals() + with self.assertRaises(NotImplementedError): + self.instance.get_slacks() + with self.assertRaises(NotImplementedError): + self.instance.get_reduced_costs() From 9dffd2605bd6e7316a3e7952cbca5793cb4af7db Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 13:50:48 -0600 Subject: [PATCH 0057/3044] Remove APPSI utils -> have been moved to pyomo.solver.util --- pyomo/contrib/appsi/utils/__init__.py | 2 - .../utils/collect_vars_and_named_exprs.py | 50 ----------------- pyomo/contrib/appsi/utils/get_objective.py | 12 ---- pyomo/contrib/appsi/utils/tests/__init__.py | 0 .../test_collect_vars_and_named_exprs.py | 56 ------------------- 5 files changed, 120 deletions(-) delete mode 100644 pyomo/contrib/appsi/utils/__init__.py delete mode 100644 pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py delete mode 100644 pyomo/contrib/appsi/utils/get_objective.py delete mode 100644 pyomo/contrib/appsi/utils/tests/__init__.py delete mode 100644 pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py diff --git a/pyomo/contrib/appsi/utils/__init__.py b/pyomo/contrib/appsi/utils/__init__.py deleted file mode 100644 index f665736fd4a..00000000000 --- a/pyomo/contrib/appsi/utils/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .get_objective import get_objective -from .collect_vars_and_named_exprs import collect_vars_and_named_exprs diff --git a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py deleted file mode 100644 index bfbbf5aecdf..00000000000 --- a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py +++ /dev/null @@ -1,50 +0,0 @@ -from pyomo.core.expr.visitor import ExpressionValueVisitor, nonpyomo_leaf_types -import pyomo.core.expr as EXPR - - -class _VarAndNamedExprCollector(ExpressionValueVisitor): - def __init__(self): - self.named_expressions = {} - self.variables = {} - self.fixed_vars = {} - self._external_functions = {} - - def visit(self, node, values): - pass - - def visiting_potential_leaf(self, node): - if type(node) in nonpyomo_leaf_types: - return True, None - - if node.is_variable_type(): - self.variables[id(node)] = node - if node.is_fixed(): - self.fixed_vars[id(node)] = node - return True, None - - if node.is_named_expression_type(): - self.named_expressions[id(node)] = node - return False, None - - if type(node) is EXPR.ExternalFunctionExpression: - self._external_functions[id(node)] = node - return False, None - - if node.is_expression_type(): - return False, None - - return True, None - - -_visitor = _VarAndNamedExprCollector() - - -def collect_vars_and_named_exprs(expr): - _visitor.__init__() - _visitor.dfs_postorder_stack(expr) - return ( - list(_visitor.named_expressions.values()), - list(_visitor.variables.values()), - list(_visitor.fixed_vars.values()), - list(_visitor._external_functions.values()), - ) diff --git a/pyomo/contrib/appsi/utils/get_objective.py b/pyomo/contrib/appsi/utils/get_objective.py deleted file mode 100644 index 30dd911f9c8..00000000000 --- a/pyomo/contrib/appsi/utils/get_objective.py +++ /dev/null @@ -1,12 +0,0 @@ -from pyomo.core.base.objective import Objective - - -def get_objective(block): - obj = None - for o in block.component_data_objects( - Objective, descend_into=True, active=True, sort=True - ): - if obj is not None: - raise ValueError('Multiple active objectives found') - obj = o - return obj diff --git a/pyomo/contrib/appsi/utils/tests/__init__.py b/pyomo/contrib/appsi/utils/tests/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py deleted file mode 100644 index 4c2a167a017..00000000000 --- a/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py +++ /dev/null @@ -1,56 +0,0 @@ -from pyomo.common import unittest -import pyomo.environ as pe -from pyomo.contrib.appsi.utils import collect_vars_and_named_exprs -from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available -from typing import Callable -from pyomo.common.gsl import find_GSL - - -class TestCollectVarsAndNamedExpressions(unittest.TestCase): - def basics_helper(self, collector: Callable, *args): - m = pe.ConcreteModel() - m.x = pe.Var() - m.y = pe.Var() - m.z = pe.Var() - m.E = pe.Expression(expr=2 * m.z + 1) - m.y.fix(3) - e = m.x * m.y + m.x * m.E - named_exprs, var_list, fixed_vars, external_funcs = collector(e, *args) - self.assertEqual([m.E], named_exprs) - self.assertEqual([m.x, m.y, m.z], var_list) - self.assertEqual([m.y], fixed_vars) - self.assertEqual([], external_funcs) - - def test_basics(self): - self.basics_helper(collect_vars_and_named_exprs) - - @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') - def test_basics_cmodel(self): - self.basics_helper(cmodel.prep_for_repn, cmodel.PyomoExprTypes()) - - def external_func_helper(self, collector: Callable, *args): - DLL = find_GSL() - if not DLL: - self.skipTest('Could not find amplgsl.dll library') - - m = pe.ConcreteModel() - m.x = pe.Var() - m.y = pe.Var() - m.z = pe.Var() - m.hypot = pe.ExternalFunction(library=DLL, function='gsl_hypot') - func = m.hypot(m.x, m.x * m.y) - m.E = pe.Expression(expr=2 * func) - m.y.fix(3) - e = m.z + m.x * m.E - named_exprs, var_list, fixed_vars, external_funcs = collector(e, *args) - self.assertEqual([m.E], named_exprs) - self.assertEqual([m.z, m.x, m.y], var_list) - self.assertEqual([m.y], fixed_vars) - self.assertEqual([func], external_funcs) - - def test_external(self): - self.external_func_helper(collect_vars_and_named_exprs) - - @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') - def test_external_cmodel(self): - self.basics_helper(cmodel.prep_for_repn, cmodel.PyomoExprTypes()) From 793fb38df3f98b04afde959f302e6e5185e33b6d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 13:57:34 -0600 Subject: [PATCH 0058/3044] Reverting test_branches file --- .github/workflows/test_branches.yml | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index ff8b5901189..99d5f7fc1a8 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -390,10 +390,10 @@ jobs: IPOPT_TAR=${DOWNLOAD_DIR}/ipopt.tar.gz if test ! -e $IPOPT_TAR; then echo "...downloading Ipopt" - # if test "${{matrix.TARGET}}" == osx; then - # echo "IDAES Ipopt not available on OSX" - # exit 0 - # fi + if test "${{matrix.TARGET}}" == osx; then + echo "IDAES Ipopt not available on OSX" + exit 0 + fi URL=https://github.com/IDAES/idaes-ext RELEASE=$(curl --max-time 150 --retry 8 \ -L -s -H 'Accept: application/json' ${URL}/releases/latest) @@ -401,11 +401,7 @@ jobs: URL=${URL}/releases/download/$VER if test "${{matrix.TARGET}}" == linux; then curl --max-time 150 --retry 8 \ - -L $URL/idaes-solvers-ubuntu2204-x86_64.tar.gz \ - > $IPOPT_TAR - elif test "${{matrix.TARGET}}" == osx; then - curl --max-time 150 --retry 8 \ - -L $URL/idaes-solvers-darwin-x86_64.tar.gz \ + -L $URL/idaes-solvers-ubuntu2004-x86_64.tar.gz \ > $IPOPT_TAR else curl --max-time 150 --retry 8 \ @@ -414,7 +410,7 @@ jobs: fi fi cd $IPOPT_DIR - tar -xz < $IPOPT_TAR + tar -xzi < $IPOPT_TAR echo "" echo "$IPOPT_DIR" ls -l $IPOPT_DIR @@ -602,7 +598,8 @@ jobs: run: | $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ - pyomo/contrib/appsi pyomo/solver --junitxml="TEST-pyomo.xml" + pyomo `pwd`/pyomo-model-libraries \ + `pwd`/examples/pyomobook --junitxml="TEST-pyomo.xml" - name: Run Pyomo MPI tests if: matrix.mpi != 0 From 35e921ad611d0b8d9bbab1bffb488c6e17263e10 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 14:31:06 -0600 Subject: [PATCH 0059/3044] Add __init__ to test directory --- pyomo/solver/tests/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 pyomo/solver/tests/__init__.py diff --git a/pyomo/solver/tests/__init__.py b/pyomo/solver/tests/__init__.py new file mode 100644 index 00000000000..9a63db93d6a --- /dev/null +++ b/pyomo/solver/tests/__init__.py @@ -0,0 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + From 7861cdac0a88e0d43cea0d89f6e270b262c61310 Mon Sep 17 00:00:00 2001 From: jlgearh Date: Wed, 30 Aug 2023 16:32:39 -0600 Subject: [PATCH 0060/3044] - Cleaning up aos code and adding test cases --- .../alternative_solutions/aos_utils.py | 30 +- pyomo/contrib/alternative_solutions/balas.py | 4 +- pyomo/contrib/alternative_solutions/obbt.py | 18 +- .../contrib/alternative_solutions/solnpool.py | 26 +- .../tests/knapsack_100_10_results.yaml | 5534 +---------------- .../alternative_solutions/tests/obbt_test.py | 145 +- .../tests/test_case.xlsx | Bin 0 -> 9880 bytes .../alternative_solutions/tests/test_cases.py | 52 + .../alternative_solutions/var_utils.py | 4 +- 9 files changed, 349 insertions(+), 5464 deletions(-) create mode 100644 pyomo/contrib/alternative_solutions/tests/test_case.xlsx create mode 100644 pyomo/contrib/alternative_solutions/tests/test_cases.py diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index 7fa1ef9466b..f18ffde9df8 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -1,9 +1,14 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Jun 30 16:12:23 2022 +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ -@author: jlgearh -""" import sys from numpy.random import normal @@ -12,14 +17,10 @@ from pyomo.common.modeling import unique_component_name import pyomo.environ as pe from pyomo.opt import SolverFactory -from pyomo.core.base.PyomoModel import ConcreteModel from pyomo.contrib import appsi -def _is_concrete_model(model): - assert isinstance(model, ConcreteModel), \ - "Parameter 'model' must be an instance of a Pyomo ConcreteModel" - -def _get_solver(solver, solver_options={}, use_persistent_solver=False): +def _get_solver(solver='gurobi', solver_options={}, + use_persistent_solver=False): if use_persistent_solver: assert solver == 'gurobi', \ "Persistent solver option requires the use of Gurobi." @@ -35,13 +36,14 @@ def _get_solver(solver, solver_options={}, use_persistent_solver=False): def _get_active_objective(model): ''' - Finds and returns the active objective function for a model. Assumes there - is exactly one active objective. + Finds and returns the active objective function for a model. Currently + assume that there is exactly one active objective. ''' active_objs = [o for o in model.component_data_objects(pe.Objective, active=True)] assert len(active_objs) == 1, \ - "Model has zero or more than one active objective function" + "Model has {} active objective functions, exactly one is required.".\ + format(len(active_objs)) return active_objs[0] diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index 2082f16d5de..1b89fe8e6f3 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -61,8 +61,8 @@ def enumerate_binary_solutions(model, max_solutions=10, variables='all', {solution_id: (objective_value,[variable, variable_value])} ''' - assert isinstance(model, ConcreteModel), \ - 'model parameter must be an instance of a Pyomo Concrete Model' + #assert isinstance(model, ConcreteModel), \ + # 'model parameter must be an instance of a Pyomo Concrete Model' # Find the maximum number of solutions to generate num_solutions = aos_utils._get_max_solutions(max_solutions) diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index fb34e458300..4208a86905a 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -9,14 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -#import pandas as pd - import pyomo.environ as pe from pyomo.opt import SolverStatus, TerminationCondition from pyomo.common.collections import ComponentMap import pyomo.contrib.alternative_solutions.aos_utils as aos_utils -import pyomo.contrib.alternative_solutions.variables as var_utils +import pyomo.contrib.alternative_solutions.var_utils as var_utils def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, refine_bounds=False, warmstart=False, already_solved=False, @@ -71,7 +69,7 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, {variable: (lower_bound, upper_bound)} ''' - aos_utils._is_concrete_model(model) + aos_utils._check_concrete_model(model) assert isinstance(refine_bounds, bool), 'refine_bounds must be a Boolean' assert isinstance(warmstart, bool), 'warmstart must be a Boolean' assert isinstance(already_solved, bool), 'already_solved must be a Boolean' @@ -83,7 +81,8 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, variable_list = var_utils.get_model_variables(model, variables, include_fixed=False) else: - variable_list = var_utils.check_variables(model, variables) + variable_list = var_utils.check_variables(model, variables, + include_fixed=False) orig_objective = aos_utils._get_active_objective(model) aos_block = aos_utils._add_aos_block(model) @@ -92,7 +91,7 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, opt = aos_utils._get_solver(solver, solver_options, use_persistent_solver) if not already_solved: - results = opt.solve(model)#, tee=tee) + results = opt.solve(model, tee=tee) status = results.solver.status condition = results.solver.termination_condition assert (status == SolverStatus.ok and @@ -150,7 +149,7 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, if use_persistent_solver: opt.update_config.check_for_new_or_removed_constraints = \ new_constraint - results = opt.solve(model)#, tee=tee) + results = opt.solve(model, tee=tee) new_constraint = False status = results.solver.status condition = results.solver.termination_condition @@ -200,8 +199,3 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, orig_objective.active return variable_bounds - -# def get_var_bound_dataframe(variable_bounds): -# '''Get a pandas DataFrame displaying the variable bound results.''' -# return pd.DataFrame.from_dict(variable_bounds,orient='index', -# columns=['LB','UB']) diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 30c8413509b..59218c844f9 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -14,13 +14,11 @@ def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=None, abs_opt_gap=None, search_mode=2, - round_discrete_vars=True, solver_options={}): + solver_options={}): ''' Finds alternative optimal solutions for discrete variables using Gurobi's built-in Solution Pool capability. See the Gurobi Solution Pool - documentation for additional details. This function requires the use of - the Gurobi Auto-Persistent Pyomo Solver interface (appsi). - + documentation for additional details. Parameters ---------- model : ConcreteModel @@ -42,13 +40,10 @@ def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=None, search_mode : 0, 1, or 2 Indicates the SolutionPool mode that is used to generate alternative solutions in Gurobi. Mode 2 should typically be used as - it finds the best n solutions. Mode 0 finds a single optimal + it finds the top n solutions. Mode 0 finds a single optimal solution (i.e. the standard mode in Gurobi). Mode 1 will generate n solutions without providing guarantees on their quality. This parameter maps to the PoolSearchMode in Gurobi. - round_discrete_vars : boolean - Boolean indicating that discrete values should be rounded to the - nearest integer in the solutions results. solver_options : dict Solver option-value pairs to be passed to the Gurobi solver. @@ -59,18 +54,15 @@ def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=None, [Solution] ''' - # Validate inputs - aos_utils._is_concrete_model(model) + # Input validation num_solutions = aos_utils._get_max_solutions(max_solutions) - assert (isinstance(rel_opt_gap, float) and rel_opt_gap >= 0) or \ + assert (isinstance(rel_opt_gap, (float, int)) and rel_opt_gap >= 0) or \ isinstance(rel_opt_gap, type(None)), \ 'rel_opt_gap must be a non-negative float or None' - assert (isinstance(abs_opt_gap, float) and abs_opt_gap >= 0) or \ + assert (isinstance(abs_opt_gap, (float, int)) and abs_opt_gap >= 0) or \ isinstance(abs_opt_gap, type(None)), \ 'abs_opt_gap must be a non-negative float or None' assert search_mode in [0, 1, 2], 'search_mode must be 0, 1, or 2' - assert isinstance(round_discrete_vars, bool), \ - 'round_discrete_vars must be a Boolean' # Configure solver and solve model opt = appsi.solvers.Gurobi() @@ -82,6 +74,8 @@ def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=None, opt.set_gurobi_param('PoolGap', rel_opt_gap) if abs_opt_gap is not None: opt.set_gurobi_param('PoolGapAbs', abs_opt_gap) + for parameter, value in solver_options.items(): + opt.set_gurobi_param(parameter, abs_opt_gap) results = opt.solve(model) assert results.termination_condition == \ appsi.base.TerminationCondition.optimal, \ @@ -95,8 +89,6 @@ def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=None, solutions = [] for i in range(solution_count): results.solution_loader.load_vars(solution_number=i) - solutions.append(solution.Solution(model, variables, - round_discrete_vars=\ - round_discrete_vars)) + solutions.append(solution.Solution(model, variables)) return solutions \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml b/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml index 2318a501260..4bea27bd8cc 100644 --- a/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml +++ b/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml @@ -1,5384 +1,150 @@ -- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution - fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet - _data: !!python/tuple [] - objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735881266400: !!python/tuple - - &id001 !!python/object/new:pyomo.core.base.objective.ScalarObjective - state: - - *id001 - - null - - true - - -1 - - &id119 !!python/object/new:pyomo.core.expr.numeric_expr.SumExpression - state: - - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.726594449656969 - - &id002 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - &id003 !!python/object/new:pyomo.core.base.var.IndexedVar - state: - - _constructed: true - _ctype: &id008 !!python/name:pyomo.core.base.var.Var '' - _data: - 1: *id002 - 2: &id013 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 2 - - -0.0 - - null - - null - - &id004 !!python/object/apply:pyomo.core.base.global_set._get_global_set [ - Boolean] - - false - - false - 3: &id014 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 3 - - 1.0 - - null - - null - - *id004 - - false - - false - 4: &id015 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 4 - - 1.0 - - null - - null - - *id004 - - false - - false - 5: &id016 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 5 - - 1.0 - - null - - null - - *id004 - - false - - false - 6: &id017 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 6 - - -0.0 - - null - - null - - *id004 - - false - - false - 7: &id018 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 7 - - -0.0 - - null - - null - - *id004 - - false - - false - 8: &id019 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 8 - - 1.0 - - null - - null - - *id004 - - false - - false - 9: &id020 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 9 - - -0.0 - - null - - null - - *id004 - - false - - false - 10: &id021 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 10 - - -0.0 - - null - - null - - *id004 - - false - - false - 11: &id022 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 11 - - -0.0 - - null - - null - - *id004 - - false - - false - 12: &id023 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 12 - - 1.0 - - null - - null - - *id004 - - false - - false - 13: &id024 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 13 - - -0.0 - - null - - null - - *id004 - - false - - false - 14: &id025 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 14 - - -0.0 - - null - - null - - *id004 - - false - - false - 15: &id026 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 15 - - 1.0 - - null - - null - - *id004 - - false - - false - 16: &id027 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 16 - - 1.0 - - null - - null - - *id004 - - false - - false - 17: &id028 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 17 - - -0.0 - - null - - null - - *id004 - - false - - false - 18: &id029 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 18 - - -0.0 - - null - - null - - *id004 - - false - - false - 19: &id030 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 19 - - 1.0 - - null - - null - - *id004 - - false - - false - 20: &id031 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 20 - - -0.0 - - null - - null - - *id004 - - false - - false - 21: &id032 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 21 - - -0.0 - - null - - null - - *id004 - - false - - false - 22: &id033 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 22 - - -0.0 - - null - - null - - *id004 - - false - - false - 23: &id034 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 23 - - 1.0 - - null - - null - - *id004 - - false - - false - 24: &id035 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 24 - - -0.0 - - null - - null - - *id004 - - false - - false - 25: &id036 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 25 - - -0.0 - - null - - null - - *id004 - - false - - false - 26: &id037 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 26 - - -0.0 - - null - - null - - *id004 - - false - - false - 27: &id038 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 27 - - -0.0 - - null - - null - - *id004 - - false - - false - 28: &id039 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 28 - - -0.0 - - null - - null - - *id004 - - false - - false - 29: &id040 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 29 - - 1.0 - - null - - null - - *id004 - - false - - false - 30: &id041 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 30 - - -0.0 - - null - - null - - *id004 - - false - - false - 31: &id042 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 31 - - -0.0 - - null - - null - - *id004 - - false - - false - 32: &id043 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 32 - - 1.0 - - null - - null - - *id004 - - false - - false - 33: &id044 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 33 - - 1.0 - - null - - null - - *id004 - - false - - false - 34: &id045 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 34 - - 1.0 - - null - - null - - *id004 - - false - - false - 35: &id046 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 35 - - 0.9999999999999909 - - null - - null - - *id004 - - false - - false - 36: &id047 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 36 - - -0.0 - - null - - null - - *id004 - - false - - false - 37: &id048 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 37 - - 1.0 - - null - - null - - *id004 - - false - - false - 38: &id049 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 38 - - -0.0 - - null - - null - - *id004 - - false - - false - 39: &id050 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 39 - - -0.0 - - null - - null - - *id004 - - false - - false - 40: &id051 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 40 - - 1.0 - - null - - null - - *id004 - - false - - false - 41: &id052 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 41 - - -0.0 - - null - - null - - *id004 - - false - - false - 42: &id053 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 42 - - -0.0 - - null - - null - - *id004 - - false - - false - 43: &id054 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 43 - - -0.0 - - null - - null - - *id004 - - false - - false - 44: &id055 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 44 - - 1.0 - - null - - null - - *id004 - - false - - false - 45: &id056 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 45 - - -0.0 - - null - - null - - *id004 - - false - - false - 46: &id057 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 46 - - -0.0 - - null - - null - - *id004 - - false - - false - 47: &id058 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 47 - - -0.0 - - null - - null - - *id004 - - false - - false - 48: &id059 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 48 - - 1.0 - - null - - null - - *id004 - - false - - false - 49: &id060 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 49 - - -0.0 - - null - - null - - *id004 - - false - - false - 50: &id061 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 50 - - 1.0 - - null - - null - - *id004 - - false - - false - 51: &id062 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 51 - - 1.0 - - null - - null - - *id004 - - false - - false - 52: &id063 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 52 - - 1.0 - - null - - null - - *id004 - - false - - false - 53: &id064 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 53 - - -0.0 - - null - - null - - *id004 - - false - - false - 54: &id065 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 54 - - 1.0 - - null - - null - - *id004 - - false - - false - 55: &id066 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 55 - - -0.0 - - null - - null - - *id004 - - false - - false - 56: &id067 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 56 - - -0.0 - - null - - null - - *id004 - - false - - false - 57: &id068 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 57 - - 1.0 - - null - - null - - *id004 - - false - - false - 58: &id069 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 58 - - -0.0 - - null - - null - - *id004 - - false - - false - 59: &id070 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 59 - - -0.0 - - null - - null - - *id004 - - false - - false - 60: &id071 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 60 - - -0.0 - - null - - null - - *id004 - - false - - false - 61: &id072 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 61 - - 1.0 - - null - - null - - *id004 - - false - - false - 62: &id073 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 62 - - 1.0 - - null - - null - - *id004 - - false - - false - 63: &id074 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 63 - - -0.0 - - null - - null - - *id004 - - false - - false - 64: &id075 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 64 - - -0.0 - - null - - null - - *id004 - - false - - false - 65: &id076 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 65 - - -0.0 - - null - - null - - *id004 - - false - - false - 66: &id077 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 66 - - 1.0 - - null - - null - - *id004 - - false - - false - 67: &id078 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 67 - - -0.0 - - null - - null - - *id004 - - false - - false - 68: &id079 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 68 - - 1.0 - - null - - null - - *id004 - - false - - false - 69: &id080 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 69 - - -0.0 - - null - - null - - *id004 - - false - - false - 70: &id081 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 70 - - -0.0 - - null - - null - - *id004 - - false - - false - 71: &id082 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 71 - - 1.0 - - null - - null - - *id004 - - false - - false - 72: &id083 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 72 - - -0.0 - - null - - null - - *id004 - - false - - false - 73: &id084 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 73 - - -0.0 - - null - - null - - *id004 - - false - - false - 74: &id085 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 74 - - -0.0 - - null - - null - - *id004 - - false - - false - 75: &id086 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 75 - - -0.0 - - null - - null - - *id004 - - false - - false - 76: &id087 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 76 - - -0.0 - - null - - null - - *id004 - - false - - false - 77: &id088 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 77 - - -0.0 - - null - - null - - *id004 - - false - - false - 78: &id089 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 78 - - -0.0 - - null - - null - - *id004 - - false - - false - 79: &id090 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 79 - - 1.0 - - null - - null - - *id004 - - false - - false - 80: &id091 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 80 - - 1.0 - - null - - null - - *id004 - - false - - false - 81: &id092 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 81 - - -0.0 - - null - - null - - *id004 - - false - - false - 82: &id093 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 82 - - -0.0 - - null - - null - - *id004 - - false - - false - 83: &id094 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 83 - - 1.0 - - null - - null - - *id004 - - false - - false - 84: &id095 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 84 - - -0.0 - - null - - null - - *id004 - - false - - false - 85: &id096 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 85 - - -0.0 - - null - - null - - *id004 - - false - - false - 86: &id097 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 86 - - 1.0 - - null - - null - - *id004 - - false - - false - 87: &id098 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 87 - - 1.0 - - null - - null - - *id004 - - false - - false - 88: &id099 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 88 - - -0.0 - - null - - null - - *id004 - - false - - false - 89: &id100 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 89 - - -0.0 - - null - - null - - *id004 - - false - - false - 90: &id101 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 90 - - -0.0 - - null - - null - - *id004 - - false - - false - 91: &id102 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 91 - - 1.0 - - null - - null - - *id004 - - false - - false - 92: &id103 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 92 - - -0.0 - - null - - null - - *id004 - - false - - false - 93: &id104 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 93 - - 1.0 - - null - - null - - *id004 - - false - - false - 94: &id105 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 94 - - 1.0 - - null - - null - - *id004 - - false - - false - 95: &id106 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 95 - - -0.0 - - null - - null - - *id004 - - false - - false - 96: &id107 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 96 - - 1.0 - - null - - null - - *id004 - - false - - false - 97: &id108 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 97 - - 1.0 - - null - - null - - *id004 - - false - - false - 98: &id109 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 98 - - 1.0 - - null - - null - - *id004 - - false - - false - 99: &id110 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 99 - - 1.0 - - null - - null - - *id004 - - false - - false - 100: &id111 !!python/object/new:pyomo.core.base.var._GeneralVarData - state: - - *id003 - - 100 - - 1.0 - - null - - null - - *id004 - - false - - false - _dense: true - _implicit_subsets: null - _index_set: &id005 !!python/object/new:pyomo.core.base.set.FiniteScalarRangeSet - state: - - *id005 - - null - - !!python/tuple - - !!python/object:pyomo.core.base.range.NumericRange - closed: !!python/tuple [true, true] - end: 100 - start: 1 - step: 1 - - _constructed: true - _ctype: &id007 !!python/name:pyomo.core.base.set.RangeSet '' - _init_bounds: null - _init_data: !!python/tuple - - !!python/tuple [1, 100] - - !!python/tuple [] - _init_filter: null - _init_validate: null - _name: INDEX - _parent: &id006 !!python/object/new:pyomo.core.base.PyomoModel.ConcreteModel - state: - - *id006 - - null - - true - - INDEX: *id005 - _constructed: true - _ctype: !!python/name:pyomo.core.base.block.Block '' - _ctypes: - *id007: [0, 0, 1] - ? &id009 !!python/name:pyomo.core.base.param.Param '' - : [1, 2, 2] - *id008: [3, 3, 1] - ? &id118 !!python/name:pyomo.core.base.objective.Objective '' - : [4, 4, 1] - ? &id113 !!python/name:pyomo.core.base.constraint.Constraint '' - : [5, 5, 1] - _data: - null: *id006 - _decl: {INDEX: 0, c: 5, o: 4, v: 2, w: 1, x: 3} - _decl_order: - - !!python/tuple - - *id005 - - null - - !!python/tuple - - &id117 !!python/object/new:pyomo.core.base.param.IndexedParam - state: - - _constructed: true - _ctype: *id009 - _data: {1: 0.7773566427005639, 2: 0.6698255595592497, - 3: 0.09913960392481702, 4: 0.35297051119014544, - 5: 0.4679077429008419, 6: 0.5346837414708775, - 7: 0.9783090609123973, 8: 0.13031535015865903, - 9: 0.6712434682302663, 10: 0.36422941594737557, - 11: 0.48883570716198577, 12: 0.20301221073405373, - 13: 0.6661983755713592, 14: 0.2276630312069321, - 15: 0.4580640582967631, 16: 0.040722397554957435, - 17: 0.9742897953778286, 18: 0.4874760742689066, - 19: 0.4616138636373597, 20: 0.7141471558082002, - 21: 0.4157281494999725, 22: 0.888011688001529, - 23: 0.023293448723771704, 24: 0.8335062677845465, - 25: 0.4684947409975081, 26: 0.8114798126442795, - 27: 0.9455914886158723, 28: 0.9830883781948988, - 29: 0.1761820755785306, 30: 0.698655759576308, - 31: 0.10885571131238292, 32: 0.16026373420620188, - 33: 0.09286027402458918, 34: 0.3140620798928404, - 35: 0.01653868433866723, 36: 0.8540491257363622, - 37: 0.2910160386456968, 38: 0.7800475863350328, - 39: 0.5480965161255696, 40: 0.19433067669976123, - 41: 0.2920382721805297, 42: 0.3194527773994337, - 43: 0.6585982235379076, 44: 0.23152103541222924, - 45: 0.6194303369953537, 46: 0.8953386098022104, - 47: 0.8694342085696831, 48: 0.2938069356684523, - 49: 0.45820480858054946, 50: 0.4849797978711191, - 51: 0.2803882693587225, 52: 0.32895694635060024, - 53: 0.9842424240265042, 54: 0.011944137920874343, - 55: 0.14290076829328524, 56: 0.6519772165446712, - 57: 0.07499317994693244, 58: 0.29207870228110877, - 59: 0.7934429721917705, 60: 0.9115931008709737, - 61: 0.3703917795895437, 62: 0.20528221118666345, - 63: 0.880081326784678, 64: 0.6325664501560831, - 65: 0.503514326058558, 66: 0.3308435596710889, - 67: 0.3474001835074456, 68: 0.2924115863324481, - 69: 0.7653974346433319, 70: 0.4784432998768373, - 71: 0.2015373401465821, 72: 0.8715627297687166, - 73: 0.7551785489449617, 74: 0.8675584511848858, - 75: 0.9323236929247266, 76: 0.24171326534063708, - 77: 0.8924504838872919, 78: 0.7659566844206285, - 79: 0.4146826922981828, 80: 0.32368260626724077, - 81: 0.5613052389019693, 82: 0.5908359788832377, - 83: 0.16558277680810296, 84: 0.4861970648764189, - 85: 0.9490216941921916, 86: 0.46819109749463483, - 87: 0.39662970244337636, 88: 0.9188065977724452, - 89: 0.9857276253270151, 90: 0.9392006613006973, - 91: 0.04763514581194506, 92: 0.8603759125000982, - 93: 0.2010458491996312, 94: 0.3436090514063087, - 95: 0.882532701944944, 96: 0.09841477926384423, - 97: 0.13326228818943153, 98: 0.26768957816772065, - 99: 0.20931290505166977, 100: 0.34066590743005254} - _default_val: &id010 !!python/name:pyomo.core.base.param.NoValue '' - _dense_initialize: false - _implicit_subsets: null - _index_set: *id005 - _mutable: false - _name: w - _parent: *id006 - _rule: !!python/object:pyomo.core.base.initializer.IndexedCallInitializer { - _fcn: !!python/name:__main__.%3Clambda%3E ''} - _units: null - _validate: null - doc: null - domain: &id011 !!python/object/apply:pyomo.core.base.global_set._get_global_set [ - Reals] - - 2 - - !!python/tuple - - &id116 !!python/object/new:pyomo.core.base.param.IndexedParam - state: - - _constructed: true - _ctype: *id009 - _data: {1: 0.726594449656969, 2: 0.6317864585819148, - 3: 0.15878068771234666, 4: 0.7662135567262747, - 5: 0.9238858846279976, 6: 0.5910784293538296, - 7: 0.16853534493879874, 8: 0.48868692874530517, - 9: 0.7183344306409591, 10: 0.21618775480519936, - 11: 0.6121437327275627, 12: 0.6203405113507279, - 13: 0.23751915806251056, 14: 0.1439876464243961, - 15: 0.7722139031503106, 16: 0.5305745417039281, - 17: 0.43869242990387713, 18: 0.47789976870816253, - 19: 0.7841053252246368, 20: 0.8368633875135679, - 21: 0.40626081603719466, 22: 0.7741855452373254, - 23: 0.0031272892149529774, 24: 0.8765518952066175, - 25: 0.03167937690551981, 26: 0.9817483012225143, - 27: 0.9952146839177622, 28: 0.6060300921061987, - 29: 0.8892431625943732, 30: 0.6763359337175552, - 31: 0.06864591686449029, 32: 0.7003268486520754, - 33: 0.9992472177618619, 34: 0.7179416689143338, - 35: 0.21861150878713143, 36: 0.4221034355860843, - 37: 0.8662113315051484, 38: 0.7945718190939539, - 39: 0.21412345267720012, 40: 0.8206543194524558, - 41: 0.3385603742499437, 42: 0.008816057617166528, - 43: 0.93420633498251, 44: 0.5824689923646437, - 45: 0.7419611633198233, 46: 0.13957389472570292, - 47: 0.5451432354739488, 48: 0.9235748236881961, - 49: 0.15047945733473922, 50: 0.9719077397032271, - 51: 0.6205150049379418, 52: 0.7251601420957609, - 53: 0.6916123582302697, 54: 0.7640087487149704, - 55: 0.19610501529331492, 56: 0.6027568046433773, - 57: 0.2359711329865154, 58: 0.10814602117665817, - 59: 0.31285007597629344, 60: 0.9099172750209824, - 61: 0.940761072700578, 62: 0.4853692161564098, - 63: 0.6164977119460898, 64: 0.18119073764701576, - 65: 0.2902506272248766, 66: 0.6457338960735598, - 67: 0.2850581491339579, 68: 0.7650274355828317, - 69: 0.8246278623440632, 70: 0.45002937783739716, - 71: 0.40523778589370574, 72: 0.16964652744651, - 73: 0.226105610010514, 74: 0.825814648409055, - 75: 0.2602611372742738, 76: 0.07577567805248797, - 77: 0.7376414354275203, 78: 0.8051307985409988, - 79: 0.8564929596470199, 80: 0.7550747332999559, - 81: 0.1451608072546512, 82: 0.47871763978951576, - 83: 0.4388308577208603, 84: 0.5077019534250237, - 85: 0.7042297016232173, 86: 0.6883296828942322, - 87: 0.7058045106408227, 88: 0.9445325523170363, - 89: 0.8038216540619805, 90: 0.77407902794402, - 91: 0.42460642017443284, 92: 0.8334296219965986, - 93: 0.9663697906197474, 94: 0.6803051313498966, - 95: 0.08824211661630754, 96: 0.6627243518817839, - 97: 0.5159087221318315, 98: 0.21408463611709205, - 99: 0.37356794166885243, 100: 0.7792012881552631} - _default_val: *id010 - _dense_initialize: false - _implicit_subsets: null - _index_set: *id005 - _mutable: false - _name: v - _parent: *id006 - _rule: !!python/object:pyomo.core.base.initializer.IndexedCallInitializer { - _fcn: !!python/name:__main__.%3Clambda%3E ''} - _units: null - _validate: null - doc: null - domain: *id011 - - null - - !!python/tuple - - *id003 - - null - - !!python/tuple - - *id001 - - null - - !!python/tuple - - &id012 !!python/object/new:pyomo.core.base.constraint.ScalarConstraint - state: - - *id012 - - null - - true - - &id112 !!python/object/new:pyomo.core.expr.numeric_expr.SumExpression - state: - - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7773566427005639 - - *id002 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6698255595592497 - - *id013 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.09913960392481702 - - *id014 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.35297051119014544 - - *id015 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.4679077429008419 - - *id016 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.5346837414708775 - - *id017 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9783090609123973 - - *id018 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.13031535015865903 - - *id019 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6712434682302663 - - *id020 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.36422941594737557 - - *id021 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.48883570716198577 - - *id022 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.20301221073405373 - - *id023 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6661983755713592 - - *id024 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.2276630312069321 - - *id025 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.4580640582967631 - - *id026 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.040722397554957435 - - *id027 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9742897953778286 - - *id028 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.4874760742689066 - - *id029 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.4616138636373597 - - *id030 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7141471558082002 - - *id031 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.4157281494999725 - - *id032 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.888011688001529 - - *id033 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.023293448723771704 - - *id034 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8335062677845465 - - *id035 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.4684947409975081 - - *id036 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8114798126442795 - - *id037 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9455914886158723 - - *id038 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9830883781948988 - - *id039 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.1761820755785306 - - *id040 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.698655759576308 - - *id041 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.10885571131238292 - - *id042 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.16026373420620188 - - *id043 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.09286027402458918 - - *id044 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.3140620798928404 - - *id045 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.01653868433866723 - - *id046 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8540491257363622 - - *id047 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.2910160386456968 - - *id048 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7800475863350328 - - *id049 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.5480965161255696 - - *id050 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.19433067669976123 - - *id051 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.2920382721805297 - - *id052 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.3194527773994337 - - *id053 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6585982235379076 - - *id054 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.23152103541222924 - - *id055 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6194303369953537 - - *id056 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8953386098022104 - - *id057 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8694342085696831 - - *id058 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.2938069356684523 - - *id059 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.45820480858054946 - - *id060 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.4849797978711191 - - *id061 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.2803882693587225 - - *id062 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.32895694635060024 - - *id063 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9842424240265042 - - *id064 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.011944137920874343 - - *id065 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.14290076829328524 - - *id066 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6519772165446712 - - *id067 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.07499317994693244 - - *id068 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.29207870228110877 - - *id069 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7934429721917705 - - *id070 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9115931008709737 - - *id071 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.3703917795895437 - - *id072 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.20528221118666345 - - *id073 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.880081326784678 - - *id074 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6325664501560831 - - *id075 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.503514326058558 - - *id076 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.3308435596710889 - - *id077 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.3474001835074456 - - *id078 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.2924115863324481 - - *id079 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7653974346433319 - - *id080 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.4784432998768373 - - *id081 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.2015373401465821 - - *id082 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8715627297687166 - - *id083 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7551785489449617 - - *id084 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8675584511848858 - - *id085 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9323236929247266 - - *id086 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.24171326534063708 - - *id087 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8924504838872919 - - *id088 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7659566844206285 - - *id089 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.4146826922981828 - - *id090 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.32368260626724077 - - *id091 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.5613052389019693 - - *id092 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.5908359788832377 - - *id093 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.16558277680810296 - - *id094 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.4861970648764189 - - *id095 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9490216941921916 - - *id096 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.46819109749463483 - - *id097 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.39662970244337636 - - *id098 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9188065977724452 - - *id099 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9857276253270151 - - *id100 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9392006613006973 - - *id101 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.04763514581194506 - - *id102 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8603759125000982 - - *id103 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.2010458491996312 - - *id104 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.3436090514063087 - - *id105 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.882532701944944 - - *id106 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.09841477926384423 - - *id107 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.13326228818943153 - - *id108 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.26768957816772065 - - *id109 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.20931290505166977 - - *id110 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.34066590743005254 - - *id111 - - 100 - - false - - null - - 10.0 - - &id114 !!python/object/new:pyomo.core.expr.relational_expr.InequalityExpression - state: - - !!python/tuple - - *id112 - - 10.0 - - false - - _constructed: true - _ctype: *id113 - _data: - null: *id012 - _implicit_subsets: null - _index_set: &id115 !!python/object/apply:pyomo.core.base.global_set._get_global_set [ - UnindexedComponent_set] - _name: c - _parent: *id006 - doc: null - rule: !!python/object:pyomo.core.base.initializer.ConstantInitializer - val: *id114 - verified: false - - null - _dense: true - _implicit_subsets: null - _index_set: *id115 - _name: unknown - _parent: null - _rule: null - _suppress_ctypes: !!set {} - c: *id012 - config: !!python/object:pyomo.core.base.PyomoModel.PyomoConfig { - _name_: PyomoConfig} - doc: null - o: *id001 - solutions: !!python/object:pyomo.core.base.PyomoModel.ModelSolutions - _instance: *id006 - index: null - solutions: [] - symbol_map: {} - statistics: !!python/object:pyomo.common.collections.bunch.Bunch { - _name_: Bunch} - v: *id116 - w: *id117 - x: *id003 - doc: null - _name: x - _parent: *id006 - _rule_bounds: null - _rule_domain: !!python/object:pyomo.core.base.set.SetInitializer - _set: !!python/object:pyomo.core.base.initializer.ConstantInitializer - val: *id004 - verified: false - verified: false - _rule_init: null - _units: null - doc: null - - 1 - - -0.0 - - null - - null - - *id004 - - false - - false - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6317864585819148 - - *id013 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.15878068771234666 - - *id014 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7662135567262747 - - *id015 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9238858846279976 - - *id016 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.5910784293538296 - - *id017 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.16853534493879874 - - *id018 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.48868692874530517 - - *id019 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7183344306409591 - - *id020 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.21618775480519936 - - *id021 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6121437327275627 - - *id022 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6203405113507279 - - *id023 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.23751915806251056 - - *id024 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.1439876464243961 - - *id025 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7722139031503106 - - *id026 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.5305745417039281 - - *id027 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.43869242990387713 - - *id028 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.47789976870816253 - - *id029 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7841053252246368 - - *id030 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8368633875135679 - - *id031 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.40626081603719466 - - *id032 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7741855452373254 - - *id033 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.0031272892149529774 - - *id034 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8765518952066175 - - *id035 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.03167937690551981 - - *id036 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9817483012225143 - - *id037 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9952146839177622 - - *id038 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6060300921061987 - - *id039 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8892431625943732 - - *id040 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6763359337175552 - - *id041 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.06864591686449029 - - *id042 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7003268486520754 - - *id043 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9992472177618619 - - *id044 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7179416689143338 - - *id045 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.21861150878713143 - - *id046 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.4221034355860843 - - *id047 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8662113315051484 - - *id048 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7945718190939539 - - *id049 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.21412345267720012 - - *id050 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8206543194524558 - - *id051 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.3385603742499437 - - *id052 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.008816057617166528 - - *id053 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.93420633498251 - - *id054 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.5824689923646437 - - *id055 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7419611633198233 - - *id056 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.13957389472570292 - - *id057 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.5451432354739488 - - *id058 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9235748236881961 - - *id059 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.15047945733473922 - - *id060 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9719077397032271 - - *id061 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6205150049379418 - - *id062 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7251601420957609 - - *id063 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6916123582302697 - - *id064 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7640087487149704 - - *id065 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.19610501529331492 - - *id066 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6027568046433773 - - *id067 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.2359711329865154 - - *id068 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.10814602117665817 - - *id069 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.31285007597629344 - - *id070 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9099172750209824 - - *id071 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.940761072700578 - - *id072 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.4853692161564098 - - *id073 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6164977119460898 - - *id074 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.18119073764701576 - - *id075 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.2902506272248766 - - *id076 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6457338960735598 - - *id077 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.2850581491339579 - - *id078 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7650274355828317 - - *id079 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8246278623440632 - - *id080 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.45002937783739716 - - *id081 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.40523778589370574 - - *id082 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.16964652744651 - - *id083 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.226105610010514 - - *id084 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.825814648409055 - - *id085 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.2602611372742738 - - *id086 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.07577567805248797 - - *id087 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7376414354275203 - - *id088 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8051307985409988 - - *id089 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8564929596470199 - - *id090 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7550747332999559 - - *id091 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.1451608072546512 - - *id092 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.47871763978951576 - - *id093 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.4388308577208603 - - *id094 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.5077019534250237 - - *id095 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7042297016232173 - - *id096 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6883296828942322 - - *id097 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7058045106408227 - - *id098 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9445325523170363 - - *id099 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8038216540619805 - - *id100 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.77407902794402 - - *id101 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.42460642017443284 - - *id102 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.8334296219965986 - - *id103 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.9663697906197474 - - *id104 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6803051313498966 - - *id105 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.08824211661630754 - - *id106 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.6627243518817839 - - *id107 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.5159087221318315 - - *id108 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.21408463611709205 - - *id109 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.37356794166885243 - - *id110 - - !!python/object/new:pyomo.core.expr.numeric_expr.MonomialTermExpression - state: - - !!python/tuple - - 0.7792012881552631 - - *id111 - - 100 - - false - - _constructed: true - _ctype: *id118 - _data: - null: *id001 - _implicit_subsets: null - _index_set: *id115 - _init_sense: !!python/object:pyomo.core.base.initializer.ConstantInitializer { - val: -1, verified: false} - _name: o - _parent: *id006 - doc: null - rule: !!python/object:pyomo.core.base.initializer.ConstantInitializer - val: *id119 - verified: false - - 26.456318046876152 - variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735933094128: !!python/tuple - - *id002 - - 0 - 1735933094464: !!python/tuple - - *id013 - - 0 - 1735933094576: !!python/tuple - - *id014 - - 0 - 1735933094688: !!python/tuple - - *id015 - - 1 - 1735933094800: !!python/tuple - - *id016 - - 1 - 1735933094912: !!python/tuple - - *id017 - - 0 - 1735933095024: !!python/tuple - - *id018 - - 0 - 1735933095136: !!python/tuple - - *id020 - - 0 - 1735933095248: !!python/tuple - - *id019 - - 1 - 1735933095360: !!python/tuple - - *id021 - - 0 - 1735933095472: !!python/tuple - - *id022 - - 0 - 1735933095584: !!python/tuple - - *id023 - - 1 - 1735933095696: !!python/tuple - - *id025 - - 0 - 1735933095808: !!python/tuple - - *id024 - - 0 - 1735933095920: !!python/tuple - - *id026 - - 1 - 1735933096032: !!python/tuple - - *id027 - - 1 - 1735933096144: !!python/tuple - - *id028 - - 0 - 1735933096256: !!python/tuple - - *id029 - - 0 - 1735933096368: !!python/tuple - - *id030 - - 1 - 1735933096480: !!python/tuple - - *id031 - - 0 - 1735933096592: !!python/tuple - - *id032 - - 0 - 1735933096704: !!python/tuple - - *id033 - - 0 - 1735933096816: !!python/tuple - - *id034 - - 1 - 1735933096928: !!python/tuple - - *id035 - - 0 - 1735933097040: !!python/tuple - - *id036 - - 0 - 1735933097152: !!python/tuple - - *id037 - - 0 - 1735933097264: !!python/tuple - - *id038 - - 0 - 1735933097376: !!python/tuple - - *id039 - - 0 - 1735933097488: !!python/tuple - - *id040 - - 1 - 1735933097600: !!python/tuple - - *id041 - - 0 - 1735933097712: !!python/tuple - - *id042 - - 0 - 1735933097824: !!python/tuple - - *id043 - - 1 - 1735933097936: !!python/tuple - - *id044 - - 1 - 1735933098048: !!python/tuple - - *id045 - - 1 - 1735933098160: !!python/tuple - - *id046 - - 1 - 1735933098272: !!python/tuple - - *id047 - - 0 - 1735933098384: !!python/tuple - - *id048 - - 1 - 1735933098496: !!python/tuple - - *id049 - - 0 - 1735933098608: !!python/tuple - - *id050 - - 0 - 1735933098720: !!python/tuple - - *id051 - - 1 - 1735933098832: !!python/tuple - - *id052 - - 0 - 1735933098944: !!python/tuple - - *id053 - - 0 - 1735933099056: !!python/tuple - - *id054 - - 1 - 1735933099168: !!python/tuple - - *id055 - - 1 - 1735933099280: !!python/tuple - - *id056 - - 0 - 1735933099392: !!python/tuple - - *id057 - - 0 - 1735933099504: !!python/tuple - - *id058 - - 0 - 1735933099616: !!python/tuple - - *id059 - - 1 - 1735933099728: !!python/tuple - - *id060 - - 0 - 1735933099840: !!python/tuple - - *id061 - - 1 - 1735940014144: !!python/tuple - - *id062 - - 1 - 1735940014256: !!python/tuple - - *id063 - - 1 - 1735940014368: !!python/tuple - - *id064 - - 0 - 1735940014480: !!python/tuple - - *id065 - - 1 - 1735940014592: !!python/tuple - - *id066 - - 1 - 1735940014704: !!python/tuple - - *id067 - - 0 - 1735940014816: !!python/tuple - - *id068 - - 1 - 1735940014928: !!python/tuple - - *id069 - - 0 - 1735940015040: !!python/tuple - - *id070 - - 0 - 1735940015152: !!python/tuple - - *id071 - - 0 - 1735940015264: !!python/tuple - - *id072 - - 1 - 1735940015376: !!python/tuple - - *id073 - - 1 - 1735940015488: !!python/tuple - - *id074 - - 0 - 1735940015600: !!python/tuple - - *id075 - - 0 - 1735940015712: !!python/tuple - - *id076 - - 0 - 1735940015824: !!python/tuple - - *id077 - - 1 - 1735940015936: !!python/tuple - - *id078 - - 0 - 1735940016048: !!python/tuple - - *id079 - - 1 - 1735940016160: !!python/tuple - - *id080 - - 0 - 1735940016272: !!python/tuple - - *id081 - - 0 - 1735940016384: !!python/tuple - - *id082 - - 1 - 1735940016496: !!python/tuple - - *id083 - - 0 - 1735940016608: !!python/tuple - - *id084 - - 0 - 1735940016720: !!python/tuple - - *id085 - - 0 - 1735940016832: !!python/tuple - - *id086 - - 0 - 1735940016944: !!python/tuple - - *id087 - - 0 - 1735940017056: !!python/tuple - - *id088 - - 0 - 1735940017168: !!python/tuple - - *id089 - - 0 - 1735940017280: !!python/tuple - - *id090 - - 1 - 1735940017392: !!python/tuple - - *id091 - - 1 - 1735940017504: !!python/tuple - - *id092 - - 0 - 1735940017616: !!python/tuple - - *id093 - - 0 - 1735940017728: !!python/tuple - - *id094 - - 1 - 1735940017840: !!python/tuple - - *id095 - - 0 - 1735940017952: !!python/tuple - - *id096 - - 0 - 1735940018064: !!python/tuple - - *id097 - - 0 - 1735940018176: !!python/tuple - - *id098 - - 1 - 1735940018288: !!python/tuple - - *id099 - - 0 - 1735940018400: !!python/tuple - - *id100 - - 0 - 1735940018512: !!python/tuple - - *id101 - - 0 - 1735940018624: !!python/tuple - - *id102 - - 1 - 1735940018736: !!python/tuple - - *id103 - - 0 - 1735940018848: !!python/tuple - - *id104 - - 1 - 1735940018960: !!python/tuple - - *id105 - - 1 - 1735940019072: !!python/tuple - - *id106 - - 0 - 1735940019184: !!python/tuple - - *id107 - - 1 - 1735940019296: !!python/tuple - - *id108 - - 1 - 1735940019408: !!python/tuple - - *id109 - - 0 - 1735940019520: !!python/tuple - - *id110 - - 1 - 1735940019632: !!python/tuple - - *id111 - - 1 -- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution - fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet - _data: !!python/tuple [] - objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735881266400: !!python/tuple - - *id001 - - 26.453190757661194 - variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735933094128: !!python/tuple - - *id002 - - 0 - 1735933094464: !!python/tuple - - *id013 - - 0 - 1735933094576: !!python/tuple - - *id014 - - 0 - 1735933094688: !!python/tuple - - *id015 - - 1 - 1735933094800: !!python/tuple - - *id016 - - 1 - 1735933094912: !!python/tuple - - *id017 - - 0 - 1735933095024: !!python/tuple - - *id018 - - 0 - 1735933095136: !!python/tuple - - *id020 - - 0 - 1735933095248: !!python/tuple - - *id019 - - 1 - 1735933095360: !!python/tuple - - *id021 - - 0 - 1735933095472: !!python/tuple - - *id022 - - 0 - 1735933095584: !!python/tuple - - *id023 - - 1 - 1735933095696: !!python/tuple - - *id025 - - 0 - 1735933095808: !!python/tuple - - *id024 - - 0 - 1735933095920: !!python/tuple - - *id026 - - 1 - 1735933096032: !!python/tuple - - *id027 - - 1 - 1735933096144: !!python/tuple - - *id028 - - 0 - 1735933096256: !!python/tuple - - *id029 - - 0 - 1735933096368: !!python/tuple - - *id030 - - 1 - 1735933096480: !!python/tuple - - *id031 - - 0 - 1735933096592: !!python/tuple - - *id032 - - 0 - 1735933096704: !!python/tuple - - *id033 - - 0 - 1735933096816: !!python/tuple - - *id034 - - 0 - 1735933096928: !!python/tuple - - *id035 - - 0 - 1735933097040: !!python/tuple - - *id036 - - 0 - 1735933097152: !!python/tuple - - *id037 - - 0 - 1735933097264: !!python/tuple - - *id038 - - 0 - 1735933097376: !!python/tuple - - *id039 - - 0 - 1735933097488: !!python/tuple - - *id040 - - 1 - 1735933097600: !!python/tuple - - *id041 - - 0 - 1735933097712: !!python/tuple - - *id042 - - 0 - 1735933097824: !!python/tuple - - *id043 - - 1 - 1735933097936: !!python/tuple - - *id044 - - 1 - 1735933098048: !!python/tuple - - *id045 - - 1 - 1735933098160: !!python/tuple - - *id046 - - 1 - 1735933098272: !!python/tuple - - *id047 - - 0 - 1735933098384: !!python/tuple - - *id048 - - 1 - 1735933098496: !!python/tuple - - *id049 - - 0 - 1735933098608: !!python/tuple - - *id050 - - 0 - 1735933098720: !!python/tuple - - *id051 - - 1 - 1735933098832: !!python/tuple - - *id052 - - 0 - 1735933098944: !!python/tuple - - *id053 - - 0 - 1735933099056: !!python/tuple - - *id054 - - 1 - 1735933099168: !!python/tuple - - *id055 - - 1 - 1735933099280: !!python/tuple - - *id056 - - 0 - 1735933099392: !!python/tuple - - *id057 - - 0 - 1735933099504: !!python/tuple - - *id058 - - 0 - 1735933099616: !!python/tuple - - *id059 - - 1 - 1735933099728: !!python/tuple - - *id060 - - 0 - 1735933099840: !!python/tuple - - *id061 - - 1 - 1735940014144: !!python/tuple - - *id062 - - 1 - 1735940014256: !!python/tuple - - *id063 - - 1 - 1735940014368: !!python/tuple - - *id064 - - 0 - 1735940014480: !!python/tuple - - *id065 - - 1 - 1735940014592: !!python/tuple - - *id066 - - 1 - 1735940014704: !!python/tuple - - *id067 - - 0 - 1735940014816: !!python/tuple - - *id068 - - 1 - 1735940014928: !!python/tuple - - *id069 - - 0 - 1735940015040: !!python/tuple - - *id070 - - 0 - 1735940015152: !!python/tuple - - *id071 - - 0 - 1735940015264: !!python/tuple - - *id072 - - 1 - 1735940015376: !!python/tuple - - *id073 - - 1 - 1735940015488: !!python/tuple - - *id074 - - 0 - 1735940015600: !!python/tuple - - *id075 - - 0 - 1735940015712: !!python/tuple - - *id076 - - 0 - 1735940015824: !!python/tuple - - *id077 - - 1 - 1735940015936: !!python/tuple - - *id078 - - 0 - 1735940016048: !!python/tuple - - *id079 - - 1 - 1735940016160: !!python/tuple - - *id080 - - 0 - 1735940016272: !!python/tuple - - *id081 - - 0 - 1735940016384: !!python/tuple - - *id082 - - 1 - 1735940016496: !!python/tuple - - *id083 - - 0 - 1735940016608: !!python/tuple - - *id084 - - 0 - 1735940016720: !!python/tuple - - *id085 - - 0 - 1735940016832: !!python/tuple - - *id086 - - 0 - 1735940016944: !!python/tuple - - *id087 - - 0 - 1735940017056: !!python/tuple - - *id088 - - 0 - 1735940017168: !!python/tuple - - *id089 - - 0 - 1735940017280: !!python/tuple - - *id090 - - 1 - 1735940017392: !!python/tuple - - *id091 - - 1 - 1735940017504: !!python/tuple - - *id092 - - 0 - 1735940017616: !!python/tuple - - *id093 - - 0 - 1735940017728: !!python/tuple - - *id094 - - 1 - 1735940017840: !!python/tuple - - *id095 - - 0 - 1735940017952: !!python/tuple - - *id096 - - 0 - 1735940018064: !!python/tuple - - *id097 - - 0 - 1735940018176: !!python/tuple - - *id098 - - 1 - 1735940018288: !!python/tuple - - *id099 - - 0 - 1735940018400: !!python/tuple - - *id100 - - 0 - 1735940018512: !!python/tuple - - *id101 - - 0 - 1735940018624: !!python/tuple - - *id102 - - 1 - 1735940018736: !!python/tuple - - *id103 - - 0 - 1735940018848: !!python/tuple - - *id104 - - 1 - 1735940018960: !!python/tuple - - *id105 - - 1 - 1735940019072: !!python/tuple - - *id106 - - 0 - 1735940019184: !!python/tuple - - *id107 - - 1 - 1735940019296: !!python/tuple - - *id108 - - 1 - 1735940019408: !!python/tuple - - *id109 - - 0 - 1735940019520: !!python/tuple - - *id110 - - 1 - 1735940019632: !!python/tuple - - *id111 - - 1 -- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution - fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet - _data: !!python/tuple [] - objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735881266400: !!python/tuple - - *id001 - - 26.437867999364702 - variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735933094128: !!python/tuple - - *id002 - - 0 - 1735933094464: !!python/tuple - - *id013 - - 0 - 1735933094576: !!python/tuple - - *id014 - - 1 - 1735933094688: !!python/tuple - - *id015 - - 1 - 1735933094800: !!python/tuple - - *id016 - - 1 - 1735933094912: !!python/tuple - - *id017 - - 0 - 1735933095024: !!python/tuple - - *id018 - - 0 - 1735933095136: !!python/tuple - - *id020 - - 0 - 1735933095248: !!python/tuple - - *id019 - - 1 - 1735933095360: !!python/tuple - - *id021 - - 0 - 1735933095472: !!python/tuple - - *id022 - - 0 - 1735933095584: !!python/tuple - - *id023 - - 1 - 1735933095696: !!python/tuple - - *id025 - - 0 - 1735933095808: !!python/tuple - - *id024 - - 0 - 1735933095920: !!python/tuple - - *id026 - - 1 - 1735933096032: !!python/tuple - - *id027 - - 1 - 1735933096144: !!python/tuple - - *id028 - - 0 - 1735933096256: !!python/tuple - - *id029 - - 0 - 1735933096368: !!python/tuple - - *id030 - - 1 - 1735933096480: !!python/tuple - - *id031 - - 0 - 1735933096592: !!python/tuple - - *id032 - - 0 - 1735933096704: !!python/tuple - - *id033 - - 0 - 1735933096816: !!python/tuple - - *id034 - - 1 - 1735933096928: !!python/tuple - - *id035 - - 0 - 1735933097040: !!python/tuple - - *id036 - - 0 - 1735933097152: !!python/tuple - - *id037 - - 0 - 1735933097264: !!python/tuple - - *id038 - - 0 - 1735933097376: !!python/tuple - - *id039 - - 0 - 1735933097488: !!python/tuple - - *id040 - - 1 - 1735933097600: !!python/tuple - - *id041 - - 0 - 1735933097712: !!python/tuple - - *id042 - - 1 - 1735933097824: !!python/tuple - - *id043 - - 1 - 1735933097936: !!python/tuple - - *id044 - - 1 - 1735933098048: !!python/tuple - - *id045 - - 1 - 1735933098160: !!python/tuple - - *id046 - - 1 - 1735933098272: !!python/tuple - - *id047 - - 0 - 1735933098384: !!python/tuple - - *id048 - - 1 - 1735933098496: !!python/tuple - - *id049 - - 0 - 1735933098608: !!python/tuple - - *id050 - - 0 - 1735933098720: !!python/tuple - - *id051 - - 1 - 1735933098832: !!python/tuple - - *id052 - - 0 - 1735933098944: !!python/tuple - - *id053 - - 0 - 1735933099056: !!python/tuple - - *id054 - - 0 - 1735933099168: !!python/tuple - - *id055 - - 1 - 1735933099280: !!python/tuple - - *id056 - - 0 - 1735933099392: !!python/tuple - - *id057 - - 0 - 1735933099504: !!python/tuple - - *id058 - - 0 - 1735933099616: !!python/tuple - - *id059 - - 1 - 1735933099728: !!python/tuple - - *id060 - - 0 - 1735933099840: !!python/tuple - - *id061 - - 1 - 1735940014144: !!python/tuple - - *id062 - - 1 - 1735940014256: !!python/tuple - - *id063 - - 1 - 1735940014368: !!python/tuple - - *id064 - - 0 - 1735940014480: !!python/tuple - - *id065 - - 1 - 1735940014592: !!python/tuple - - *id066 - - 1 - 1735940014704: !!python/tuple - - *id067 - - 0 - 1735940014816: !!python/tuple - - *id068 - - 1 - 1735940014928: !!python/tuple - - *id069 - - 0 - 1735940015040: !!python/tuple - - *id070 - - 0 - 1735940015152: !!python/tuple - - *id071 - - 0 - 1735940015264: !!python/tuple - - *id072 - - 1 - 1735940015376: !!python/tuple - - *id073 - - 1 - 1735940015488: !!python/tuple - - *id074 - - 0 - 1735940015600: !!python/tuple - - *id075 - - 0 - 1735940015712: !!python/tuple - - *id076 - - 0 - 1735940015824: !!python/tuple - - *id077 - - 1 - 1735940015936: !!python/tuple - - *id078 - - 0 - 1735940016048: !!python/tuple - - *id079 - - 1 - 1735940016160: !!python/tuple - - *id080 - - 0 - 1735940016272: !!python/tuple - - *id081 - - 0 - 1735940016384: !!python/tuple - - *id082 - - 1 - 1735940016496: !!python/tuple - - *id083 - - 0 - 1735940016608: !!python/tuple - - *id084 - - 0 - 1735940016720: !!python/tuple - - *id085 - - 0 - 1735940016832: !!python/tuple - - *id086 - - 0 - 1735940016944: !!python/tuple - - *id087 - - 0 - 1735940017056: !!python/tuple - - *id088 - - 0 - 1735940017168: !!python/tuple - - *id089 - - 0 - 1735940017280: !!python/tuple - - *id090 - - 1 - 1735940017392: !!python/tuple - - *id091 - - 1 - 1735940017504: !!python/tuple - - *id092 - - 0 - 1735940017616: !!python/tuple - - *id093 - - 0 - 1735940017728: !!python/tuple - - *id094 - - 1 - 1735940017840: !!python/tuple - - *id095 - - 0 - 1735940017952: !!python/tuple - - *id096 - - 0 - 1735940018064: !!python/tuple - - *id097 - - 1 - 1735940018176: !!python/tuple - - *id098 - - 1 - 1735940018288: !!python/tuple - - *id099 - - 0 - 1735940018400: !!python/tuple - - *id100 - - 0 - 1735940018512: !!python/tuple - - *id101 - - 0 - 1735940018624: !!python/tuple - - *id102 - - 1 - 1735940018736: !!python/tuple - - *id103 - - 0 - 1735940018848: !!python/tuple - - *id104 - - 1 - 1735940018960: !!python/tuple - - *id105 - - 1 - 1735940019072: !!python/tuple - - *id106 - - 0 - 1735940019184: !!python/tuple - - *id107 - - 1 - 1735940019296: !!python/tuple - - *id108 - - 1 - 1735940019408: !!python/tuple - - *id109 - - 0 - 1735940019520: !!python/tuple - - *id110 - - 1 - 1735940019632: !!python/tuple - - *id111 - - 1 -- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution - fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet - _data: !!python/tuple [] - objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735881266400: !!python/tuple - - *id001 - - 26.43474071014975 - variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735933094128: !!python/tuple - - *id002 - - 0 - 1735933094464: !!python/tuple - - *id013 - - 0 - 1735933094576: !!python/tuple - - *id014 - - 1 - 1735933094688: !!python/tuple - - *id015 - - 1 - 1735933094800: !!python/tuple - - *id016 - - 1 - 1735933094912: !!python/tuple - - *id017 - - 0 - 1735933095024: !!python/tuple - - *id018 - - 0 - 1735933095136: !!python/tuple - - *id020 - - 0 - 1735933095248: !!python/tuple - - *id019 - - 1 - 1735933095360: !!python/tuple - - *id021 - - 0 - 1735933095472: !!python/tuple - - *id022 - - 0 - 1735933095584: !!python/tuple - - *id023 - - 1 - 1735933095696: !!python/tuple - - *id025 - - 0 - 1735933095808: !!python/tuple - - *id024 - - 0 - 1735933095920: !!python/tuple - - *id026 - - 1 - 1735933096032: !!python/tuple - - *id027 - - 1 - 1735933096144: !!python/tuple - - *id028 - - 0 - 1735933096256: !!python/tuple - - *id029 - - 0 - 1735933096368: !!python/tuple - - *id030 - - 1 - 1735933096480: !!python/tuple - - *id031 - - 0 - 1735933096592: !!python/tuple - - *id032 - - 0 - 1735933096704: !!python/tuple - - *id033 - - 0 - 1735933096816: !!python/tuple - - *id034 - - 0 - 1735933096928: !!python/tuple - - *id035 - - 0 - 1735933097040: !!python/tuple - - *id036 - - 0 - 1735933097152: !!python/tuple - - *id037 - - 0 - 1735933097264: !!python/tuple - - *id038 - - 0 - 1735933097376: !!python/tuple - - *id039 - - 0 - 1735933097488: !!python/tuple - - *id040 - - 1 - 1735933097600: !!python/tuple - - *id041 - - 0 - 1735933097712: !!python/tuple - - *id042 - - 1 - 1735933097824: !!python/tuple - - *id043 - - 1 - 1735933097936: !!python/tuple - - *id044 - - 1 - 1735933098048: !!python/tuple - - *id045 - - 1 - 1735933098160: !!python/tuple - - *id046 - - 1 - 1735933098272: !!python/tuple - - *id047 - - 0 - 1735933098384: !!python/tuple - - *id048 - - 1 - 1735933098496: !!python/tuple - - *id049 - - 0 - 1735933098608: !!python/tuple - - *id050 - - 0 - 1735933098720: !!python/tuple - - *id051 - - 1 - 1735933098832: !!python/tuple - - *id052 - - 0 - 1735933098944: !!python/tuple - - *id053 - - 0 - 1735933099056: !!python/tuple - - *id054 - - 0 - 1735933099168: !!python/tuple - - *id055 - - 1 - 1735933099280: !!python/tuple - - *id056 - - 0 - 1735933099392: !!python/tuple - - *id057 - - 0 - 1735933099504: !!python/tuple - - *id058 - - 0 - 1735933099616: !!python/tuple - - *id059 - - 1 - 1735933099728: !!python/tuple - - *id060 - - 0 - 1735933099840: !!python/tuple - - *id061 - - 1 - 1735940014144: !!python/tuple - - *id062 - - 1 - 1735940014256: !!python/tuple - - *id063 - - 1 - 1735940014368: !!python/tuple - - *id064 - - 0 - 1735940014480: !!python/tuple - - *id065 - - 1 - 1735940014592: !!python/tuple - - *id066 - - 1 - 1735940014704: !!python/tuple - - *id067 - - 0 - 1735940014816: !!python/tuple - - *id068 - - 1 - 1735940014928: !!python/tuple - - *id069 - - 0 - 1735940015040: !!python/tuple - - *id070 - - 0 - 1735940015152: !!python/tuple - - *id071 - - 0 - 1735940015264: !!python/tuple - - *id072 - - 1 - 1735940015376: !!python/tuple - - *id073 - - 1 - 1735940015488: !!python/tuple - - *id074 - - 0 - 1735940015600: !!python/tuple - - *id075 - - 0 - 1735940015712: !!python/tuple - - *id076 - - 0 - 1735940015824: !!python/tuple - - *id077 - - 1 - 1735940015936: !!python/tuple - - *id078 - - 0 - 1735940016048: !!python/tuple - - *id079 - - 1 - 1735940016160: !!python/tuple - - *id080 - - 0 - 1735940016272: !!python/tuple - - *id081 - - 0 - 1735940016384: !!python/tuple - - *id082 - - 1 - 1735940016496: !!python/tuple - - *id083 - - 0 - 1735940016608: !!python/tuple - - *id084 - - 0 - 1735940016720: !!python/tuple - - *id085 - - 0 - 1735940016832: !!python/tuple - - *id086 - - 0 - 1735940016944: !!python/tuple - - *id087 - - 0 - 1735940017056: !!python/tuple - - *id088 - - 0 - 1735940017168: !!python/tuple - - *id089 - - 0 - 1735940017280: !!python/tuple - - *id090 - - 1 - 1735940017392: !!python/tuple - - *id091 - - 1 - 1735940017504: !!python/tuple - - *id092 - - 0 - 1735940017616: !!python/tuple - - *id093 - - 0 - 1735940017728: !!python/tuple - - *id094 - - 1 - 1735940017840: !!python/tuple - - *id095 - - 0 - 1735940017952: !!python/tuple - - *id096 - - 0 - 1735940018064: !!python/tuple - - *id097 - - 1 - 1735940018176: !!python/tuple - - *id098 - - 1 - 1735940018288: !!python/tuple - - *id099 - - 0 - 1735940018400: !!python/tuple - - *id100 - - 0 - 1735940018512: !!python/tuple - - *id101 - - 0 - 1735940018624: !!python/tuple - - *id102 - - 1 - 1735940018736: !!python/tuple - - *id103 - - 0 - 1735940018848: !!python/tuple - - *id104 - - 1 - 1735940018960: !!python/tuple - - *id105 - - 1 - 1735940019072: !!python/tuple - - *id106 - - 0 - 1735940019184: !!python/tuple - - *id107 - - 1 - 1735940019296: !!python/tuple - - *id108 - - 1 - 1735940019408: !!python/tuple - - *id109 - - 0 - 1735940019520: !!python/tuple - - *id110 - - 1 - 1735940019632: !!python/tuple - - *id111 - - 1 -- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution - fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet - _data: !!python/tuple [] - objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735881266400: !!python/tuple - - *id001 - - 26.418993719295177 - variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735933094128: !!python/tuple - - *id002 - - 0 - 1735933094464: !!python/tuple - - *id013 - - 0 - 1735933094576: !!python/tuple - - *id014 - - 1 - 1735933094688: !!python/tuple - - *id015 - - 1 - 1735933094800: !!python/tuple - - *id016 - - 1 - 1735933094912: !!python/tuple - - *id017 - - 0 - 1735933095024: !!python/tuple - - *id018 - - 0 - 1735933095136: !!python/tuple - - *id020 - - 0 - 1735933095248: !!python/tuple - - *id019 - - 1 - 1735933095360: !!python/tuple - - *id021 - - 0 - 1735933095472: !!python/tuple - - *id022 - - 0 - 1735933095584: !!python/tuple - - *id023 - - 1 - 1735933095696: !!python/tuple - - *id025 - - 0 - 1735933095808: !!python/tuple - - *id024 - - 0 - 1735933095920: !!python/tuple - - *id026 - - 1 - 1735933096032: !!python/tuple - - *id027 - - 1 - 1735933096144: !!python/tuple - - *id028 - - 0 - 1735933096256: !!python/tuple - - *id029 - - 0 - 1735933096368: !!python/tuple - - *id030 - - 1 - 1735933096480: !!python/tuple - - *id031 - - 0 - 1735933096592: !!python/tuple - - *id032 - - 0 - 1735933096704: !!python/tuple - - *id033 - - 0 - 1735933096816: !!python/tuple - - *id034 - - 1 - 1735933096928: !!python/tuple - - *id035 - - 0 - 1735933097040: !!python/tuple - - *id036 - - 0 - 1735933097152: !!python/tuple - - *id037 - - 0 - 1735933097264: !!python/tuple - - *id038 - - 0 - 1735933097376: !!python/tuple - - *id039 - - 0 - 1735933097488: !!python/tuple - - *id040 - - 1 - 1735933097600: !!python/tuple - - *id041 - - 0 - 1735933097712: !!python/tuple - - *id042 - - 0 - 1735933097824: !!python/tuple - - *id043 - - 1 - 1735933097936: !!python/tuple - - *id044 - - 1 - 1735933098048: !!python/tuple - - *id045 - - 1 - 1735933098160: !!python/tuple - - *id046 - - 1 - 1735933098272: !!python/tuple - - *id047 - - 0 - 1735933098384: !!python/tuple - - *id048 - - 1 - 1735933098496: !!python/tuple - - *id049 - - 0 - 1735933098608: !!python/tuple - - *id050 - - 0 - 1735933098720: !!python/tuple - - *id051 - - 1 - 1735933098832: !!python/tuple - - *id052 - - 0 - 1735933098944: !!python/tuple - - *id053 - - 0 - 1735933099056: !!python/tuple - - *id054 - - 1 - 1735933099168: !!python/tuple - - *id055 - - 1 - 1735933099280: !!python/tuple - - *id056 - - 0 - 1735933099392: !!python/tuple - - *id057 - - 0 - 1735933099504: !!python/tuple - - *id058 - - 0 - 1735933099616: !!python/tuple - - *id059 - - 1 - 1735933099728: !!python/tuple - - *id060 - - 0 - 1735933099840: !!python/tuple - - *id061 - - 1 - 1735940014144: !!python/tuple - - *id062 - - 1 - 1735940014256: !!python/tuple - - *id063 - - 1 - 1735940014368: !!python/tuple - - *id064 - - 0 - 1735940014480: !!python/tuple - - *id065 - - 1 - 1735940014592: !!python/tuple - - *id066 - - 0 - 1735940014704: !!python/tuple - - *id067 - - 0 - 1735940014816: !!python/tuple - - *id068 - - 1 - 1735940014928: !!python/tuple - - *id069 - - 0 - 1735940015040: !!python/tuple - - *id070 - - 0 - 1735940015152: !!python/tuple - - *id071 - - 0 - 1735940015264: !!python/tuple - - *id072 - - 1 - 1735940015376: !!python/tuple - - *id073 - - 1 - 1735940015488: !!python/tuple - - *id074 - - 0 - 1735940015600: !!python/tuple - - *id075 - - 0 - 1735940015712: !!python/tuple - - *id076 - - 0 - 1735940015824: !!python/tuple - - *id077 - - 1 - 1735940015936: !!python/tuple - - *id078 - - 0 - 1735940016048: !!python/tuple - - *id079 - - 1 - 1735940016160: !!python/tuple - - *id080 - - 0 - 1735940016272: !!python/tuple - - *id081 - - 0 - 1735940016384: !!python/tuple - - *id082 - - 1 - 1735940016496: !!python/tuple - - *id083 - - 0 - 1735940016608: !!python/tuple - - *id084 - - 0 - 1735940016720: !!python/tuple - - *id085 - - 0 - 1735940016832: !!python/tuple - - *id086 - - 0 - 1735940016944: !!python/tuple - - *id087 - - 0 - 1735940017056: !!python/tuple - - *id088 - - 0 - 1735940017168: !!python/tuple - - *id089 - - 0 - 1735940017280: !!python/tuple - - *id090 - - 1 - 1735940017392: !!python/tuple - - *id091 - - 1 - 1735940017504: !!python/tuple - - *id092 - - 0 - 1735940017616: !!python/tuple - - *id093 - - 0 - 1735940017728: !!python/tuple - - *id094 - - 1 - 1735940017840: !!python/tuple - - *id095 - - 0 - 1735940017952: !!python/tuple - - *id096 - - 0 - 1735940018064: !!python/tuple - - *id097 - - 0 - 1735940018176: !!python/tuple - - *id098 - - 1 - 1735940018288: !!python/tuple - - *id099 - - 0 - 1735940018400: !!python/tuple - - *id100 - - 0 - 1735940018512: !!python/tuple - - *id101 - - 0 - 1735940018624: !!python/tuple - - *id102 - - 1 - 1735940018736: !!python/tuple - - *id103 - - 0 - 1735940018848: !!python/tuple - - *id104 - - 1 - 1735940018960: !!python/tuple - - *id105 - - 1 - 1735940019072: !!python/tuple - - *id106 - - 0 - 1735940019184: !!python/tuple - - *id107 - - 1 - 1735940019296: !!python/tuple - - *id108 - - 1 - 1735940019408: !!python/tuple - - *id109 - - 0 - 1735940019520: !!python/tuple - - *id110 - - 1 - 1735940019632: !!python/tuple - - *id111 - - 1 -- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution - fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet - _data: !!python/tuple [] - objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735881266400: !!python/tuple - - *id001 - - 26.415866430080232 - variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735933094128: !!python/tuple - - *id002 - - 0 - 1735933094464: !!python/tuple - - *id013 - - 0 - 1735933094576: !!python/tuple - - *id014 - - 1 - 1735933094688: !!python/tuple - - *id015 - - 1 - 1735933094800: !!python/tuple - - *id016 - - 1 - 1735933094912: !!python/tuple - - *id017 - - 0 - 1735933095024: !!python/tuple - - *id018 - - 0 - 1735933095136: !!python/tuple - - *id020 - - 0 - 1735933095248: !!python/tuple - - *id019 - - 1 - 1735933095360: !!python/tuple - - *id021 - - 0 - 1735933095472: !!python/tuple - - *id022 - - 0 - 1735933095584: !!python/tuple - - *id023 - - 1 - 1735933095696: !!python/tuple - - *id025 - - 0 - 1735933095808: !!python/tuple - - *id024 - - 0 - 1735933095920: !!python/tuple - - *id026 - - 1 - 1735933096032: !!python/tuple - - *id027 - - 1 - 1735933096144: !!python/tuple - - *id028 - - 0 - 1735933096256: !!python/tuple - - *id029 - - 0 - 1735933096368: !!python/tuple - - *id030 - - 1 - 1735933096480: !!python/tuple - - *id031 - - 0 - 1735933096592: !!python/tuple - - *id032 - - 0 - 1735933096704: !!python/tuple - - *id033 - - 0 - 1735933096816: !!python/tuple - - *id034 - - 0 - 1735933096928: !!python/tuple - - *id035 - - 0 - 1735933097040: !!python/tuple - - *id036 - - 0 - 1735933097152: !!python/tuple - - *id037 - - 0 - 1735933097264: !!python/tuple - - *id038 - - 0 - 1735933097376: !!python/tuple - - *id039 - - 0 - 1735933097488: !!python/tuple - - *id040 - - 1 - 1735933097600: !!python/tuple - - *id041 - - 0 - 1735933097712: !!python/tuple - - *id042 - - 0 - 1735933097824: !!python/tuple - - *id043 - - 1 - 1735933097936: !!python/tuple - - *id044 - - 1 - 1735933098048: !!python/tuple - - *id045 - - 1 - 1735933098160: !!python/tuple - - *id046 - - 1 - 1735933098272: !!python/tuple - - *id047 - - 0 - 1735933098384: !!python/tuple - - *id048 - - 1 - 1735933098496: !!python/tuple - - *id049 - - 0 - 1735933098608: !!python/tuple - - *id050 - - 0 - 1735933098720: !!python/tuple - - *id051 - - 1 - 1735933098832: !!python/tuple - - *id052 - - 0 - 1735933098944: !!python/tuple - - *id053 - - 0 - 1735933099056: !!python/tuple - - *id054 - - 1 - 1735933099168: !!python/tuple - - *id055 - - 1 - 1735933099280: !!python/tuple - - *id056 - - 0 - 1735933099392: !!python/tuple - - *id057 - - 0 - 1735933099504: !!python/tuple - - *id058 - - 0 - 1735933099616: !!python/tuple - - *id059 - - 1 - 1735933099728: !!python/tuple - - *id060 - - 0 - 1735933099840: !!python/tuple - - *id061 - - 1 - 1735940014144: !!python/tuple - - *id062 - - 1 - 1735940014256: !!python/tuple - - *id063 - - 1 - 1735940014368: !!python/tuple - - *id064 - - 0 - 1735940014480: !!python/tuple - - *id065 - - 1 - 1735940014592: !!python/tuple - - *id066 - - 0 - 1735940014704: !!python/tuple - - *id067 - - 0 - 1735940014816: !!python/tuple - - *id068 - - 1 - 1735940014928: !!python/tuple - - *id069 - - 0 - 1735940015040: !!python/tuple - - *id070 - - 0 - 1735940015152: !!python/tuple - - *id071 - - 0 - 1735940015264: !!python/tuple - - *id072 - - 1 - 1735940015376: !!python/tuple - - *id073 - - 1 - 1735940015488: !!python/tuple - - *id074 - - 0 - 1735940015600: !!python/tuple - - *id075 - - 0 - 1735940015712: !!python/tuple - - *id076 - - 0 - 1735940015824: !!python/tuple - - *id077 - - 1 - 1735940015936: !!python/tuple - - *id078 - - 0 - 1735940016048: !!python/tuple - - *id079 - - 1 - 1735940016160: !!python/tuple - - *id080 - - 0 - 1735940016272: !!python/tuple - - *id081 - - 0 - 1735940016384: !!python/tuple - - *id082 - - 1 - 1735940016496: !!python/tuple - - *id083 - - 0 - 1735940016608: !!python/tuple - - *id084 - - 0 - 1735940016720: !!python/tuple - - *id085 - - 0 - 1735940016832: !!python/tuple - - *id086 - - 0 - 1735940016944: !!python/tuple - - *id087 - - 0 - 1735940017056: !!python/tuple - - *id088 - - 0 - 1735940017168: !!python/tuple - - *id089 - - 0 - 1735940017280: !!python/tuple - - *id090 - - 1 - 1735940017392: !!python/tuple - - *id091 - - 1 - 1735940017504: !!python/tuple - - *id092 - - 0 - 1735940017616: !!python/tuple - - *id093 - - 0 - 1735940017728: !!python/tuple - - *id094 - - 1 - 1735940017840: !!python/tuple - - *id095 - - 0 - 1735940017952: !!python/tuple - - *id096 - - 0 - 1735940018064: !!python/tuple - - *id097 - - 0 - 1735940018176: !!python/tuple - - *id098 - - 1 - 1735940018288: !!python/tuple - - *id099 - - 0 - 1735940018400: !!python/tuple - - *id100 - - 0 - 1735940018512: !!python/tuple - - *id101 - - 0 - 1735940018624: !!python/tuple - - *id102 - - 1 - 1735940018736: !!python/tuple - - *id103 - - 0 - 1735940018848: !!python/tuple - - *id104 - - 1 - 1735940018960: !!python/tuple - - *id105 - - 1 - 1735940019072: !!python/tuple - - *id106 - - 0 - 1735940019184: !!python/tuple - - *id107 - - 1 - 1735940019296: !!python/tuple - - *id108 - - 1 - 1735940019408: !!python/tuple - - *id109 - - 0 - 1735940019520: !!python/tuple - - *id110 - - 1 - 1735940019632: !!python/tuple - - *id111 - - 1 -- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution - fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet - _data: !!python/tuple [] - objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735881266400: !!python/tuple - - *id001 - - 26.408565569050655 - variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735933094128: !!python/tuple - - *id002 - - 0 - 1735933094464: !!python/tuple - - *id013 - - 0 - 1735933094576: !!python/tuple - - *id014 - - 1 - 1735933094688: !!python/tuple - - *id015 - - 1 - 1735933094800: !!python/tuple - - *id016 - - 1 - 1735933094912: !!python/tuple - - *id017 - - 0 - 1735933095024: !!python/tuple - - *id018 - - 0 - 1735933095136: !!python/tuple - - *id020 - - 0 - 1735933095248: !!python/tuple - - *id019 - - 1 - 1735933095360: !!python/tuple - - *id021 - - 0 - 1735933095472: !!python/tuple - - *id022 - - 1 - 1735933095584: !!python/tuple - - *id023 - - 1 - 1735933095696: !!python/tuple - - *id025 - - 0 - 1735933095808: !!python/tuple - - *id024 - - 0 - 1735933095920: !!python/tuple - - *id026 - - 1 - 1735933096032: !!python/tuple - - *id027 - - 1 - 1735933096144: !!python/tuple - - *id028 - - 0 - 1735933096256: !!python/tuple - - *id029 - - 0 - 1735933096368: !!python/tuple - - *id030 - - 1 - 1735933096480: !!python/tuple - - *id031 - - 0 - 1735933096592: !!python/tuple - - *id032 - - 0 - 1735933096704: !!python/tuple - - *id033 - - 0 - 1735933096816: !!python/tuple - - *id034 - - 0 - 1735933096928: !!python/tuple - - *id035 - - 0 - 1735933097040: !!python/tuple - - *id036 - - 0 - 1735933097152: !!python/tuple - - *id037 - - 0 - 1735933097264: !!python/tuple - - *id038 - - 0 - 1735933097376: !!python/tuple - - *id039 - - 0 - 1735933097488: !!python/tuple - - *id040 - - 1 - 1735933097600: !!python/tuple - - *id041 - - 0 - 1735933097712: !!python/tuple - - *id042 - - 0 - 1735933097824: !!python/tuple - - *id043 - - 1 - 1735933097936: !!python/tuple - - *id044 - - 1 - 1735933098048: !!python/tuple - - *id045 - - 1 - 1735933098160: !!python/tuple - - *id046 - - 1 - 1735933098272: !!python/tuple - - *id047 - - 0 - 1735933098384: !!python/tuple - - *id048 - - 1 - 1735933098496: !!python/tuple - - *id049 - - 0 - 1735933098608: !!python/tuple - - *id050 - - 0 - 1735933098720: !!python/tuple - - *id051 - - 1 - 1735933098832: !!python/tuple - - *id052 - - 0 - 1735933098944: !!python/tuple - - *id053 - - 0 - 1735933099056: !!python/tuple - - *id054 - - 0 - 1735933099168: !!python/tuple - - *id055 - - 1 - 1735933099280: !!python/tuple - - *id056 - - 0 - 1735933099392: !!python/tuple - - *id057 - - 0 - 1735933099504: !!python/tuple - - *id058 - - 0 - 1735933099616: !!python/tuple - - *id059 - - 1 - 1735933099728: !!python/tuple - - *id060 - - 0 - 1735933099840: !!python/tuple - - *id061 - - 1 - 1735940014144: !!python/tuple - - *id062 - - 1 - 1735940014256: !!python/tuple - - *id063 - - 1 - 1735940014368: !!python/tuple - - *id064 - - 0 - 1735940014480: !!python/tuple - - *id065 - - 1 - 1735940014592: !!python/tuple - - *id066 - - 0 - 1735940014704: !!python/tuple - - *id067 - - 0 - 1735940014816: !!python/tuple - - *id068 - - 1 - 1735940014928: !!python/tuple - - *id069 - - 0 - 1735940015040: !!python/tuple - - *id070 - - 0 - 1735940015152: !!python/tuple - - *id071 - - 0 - 1735940015264: !!python/tuple - - *id072 - - 1 - 1735940015376: !!python/tuple - - *id073 - - 1 - 1735940015488: !!python/tuple - - *id074 - - 0 - 1735940015600: !!python/tuple - - *id075 - - 0 - 1735940015712: !!python/tuple - - *id076 - - 0 - 1735940015824: !!python/tuple - - *id077 - - 1 - 1735940015936: !!python/tuple - - *id078 - - 0 - 1735940016048: !!python/tuple - - *id079 - - 1 - 1735940016160: !!python/tuple - - *id080 - - 0 - 1735940016272: !!python/tuple - - *id081 - - 0 - 1735940016384: !!python/tuple - - *id082 - - 1 - 1735940016496: !!python/tuple - - *id083 - - 0 - 1735940016608: !!python/tuple - - *id084 - - 0 - 1735940016720: !!python/tuple - - *id085 - - 0 - 1735940016832: !!python/tuple - - *id086 - - 0 - 1735940016944: !!python/tuple - - *id087 - - 0 - 1735940017056: !!python/tuple - - *id088 - - 0 - 1735940017168: !!python/tuple - - *id089 - - 0 - 1735940017280: !!python/tuple - - *id090 - - 1 - 1735940017392: !!python/tuple - - *id091 - - 1 - 1735940017504: !!python/tuple - - *id092 - - 0 - 1735940017616: !!python/tuple - - *id093 - - 0 - 1735940017728: !!python/tuple - - *id094 - - 1 - 1735940017840: !!python/tuple - - *id095 - - 0 - 1735940017952: !!python/tuple - - *id096 - - 0 - 1735940018064: !!python/tuple - - *id097 - - 1 - 1735940018176: !!python/tuple - - *id098 - - 1 - 1735940018288: !!python/tuple - - *id099 - - 0 - 1735940018400: !!python/tuple - - *id100 - - 0 - 1735940018512: !!python/tuple - - *id101 - - 0 - 1735940018624: !!python/tuple - - *id102 - - 1 - 1735940018736: !!python/tuple - - *id103 - - 0 - 1735940018848: !!python/tuple - - *id104 - - 1 - 1735940018960: !!python/tuple - - *id105 - - 1 - 1735940019072: !!python/tuple - - *id106 - - 0 - 1735940019184: !!python/tuple - - *id107 - - 1 - 1735940019296: !!python/tuple - - *id108 - - 1 - 1735940019408: !!python/tuple - - *id109 - - 0 - 1735940019520: !!python/tuple - - *id110 - - 0 - 1735940019632: !!python/tuple - - *id111 - - 1 -- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution - fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet - _data: !!python/tuple [] - objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735881266400: !!python/tuple - - *id001 - - 26.401518891548587 - variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735933094128: !!python/tuple - - *id002 - - 0 - 1735933094464: !!python/tuple - - *id013 - - 0 - 1735933094576: !!python/tuple - - *id014 - - 1 - 1735933094688: !!python/tuple - - *id015 - - 1 - 1735933094800: !!python/tuple - - *id016 - - 1 - 1735933094912: !!python/tuple - - *id017 - - 0 - 1735933095024: !!python/tuple - - *id018 - - 0 - 1735933095136: !!python/tuple - - *id020 - - 0 - 1735933095248: !!python/tuple - - *id019 - - 1 - 1735933095360: !!python/tuple - - *id021 - - 0 - 1735933095472: !!python/tuple - - *id022 - - 0 - 1735933095584: !!python/tuple - - *id023 - - 1 - 1735933095696: !!python/tuple - - *id025 - - 0 - 1735933095808: !!python/tuple - - *id024 - - 0 - 1735933095920: !!python/tuple - - *id026 - - 1 - 1735933096032: !!python/tuple - - *id027 - - 1 - 1735933096144: !!python/tuple - - *id028 - - 0 - 1735933096256: !!python/tuple - - *id029 - - 0 - 1735933096368: !!python/tuple - - *id030 - - 1 - 1735933096480: !!python/tuple - - *id031 - - 0 - 1735933096592: !!python/tuple - - *id032 - - 0 - 1735933096704: !!python/tuple - - *id033 - - 0 - 1735933096816: !!python/tuple - - *id034 - - 1 - 1735933096928: !!python/tuple - - *id035 - - 0 - 1735933097040: !!python/tuple - - *id036 - - 0 - 1735933097152: !!python/tuple - - *id037 - - 0 - 1735933097264: !!python/tuple - - *id038 - - 0 - 1735933097376: !!python/tuple - - *id039 - - 0 - 1735933097488: !!python/tuple - - *id040 - - 1 - 1735933097600: !!python/tuple - - *id041 - - 0 - 1735933097712: !!python/tuple - - *id042 - - 0 - 1735933097824: !!python/tuple - - *id043 - - 1 - 1735933097936: !!python/tuple - - *id044 - - 1 - 1735933098048: !!python/tuple - - *id045 - - 1 - 1735933098160: !!python/tuple - - *id046 - - 1 - 1735933098272: !!python/tuple - - *id047 - - 0 - 1735933098384: !!python/tuple - - *id048 - - 1 - 1735933098496: !!python/tuple - - *id049 - - 0 - 1735933098608: !!python/tuple - - *id050 - - 0 - 1735933098720: !!python/tuple - - *id051 - - 1 - 1735933098832: !!python/tuple - - *id052 - - 0 - 1735933098944: !!python/tuple - - *id053 - - 0 - 1735933099056: !!python/tuple - - *id054 - - 1 - 1735933099168: !!python/tuple - - *id055 - - 1 - 1735933099280: !!python/tuple - - *id056 - - 0 - 1735933099392: !!python/tuple - - *id057 - - 0 - 1735933099504: !!python/tuple - - *id058 - - 0 - 1735933099616: !!python/tuple - - *id059 - - 1 - 1735933099728: !!python/tuple - - *id060 - - 0 - 1735933099840: !!python/tuple - - *id061 - - 1 - 1735940014144: !!python/tuple - - *id062 - - 1 - 1735940014256: !!python/tuple - - *id063 - - 1 - 1735940014368: !!python/tuple - - *id064 - - 0 - 1735940014480: !!python/tuple - - *id065 - - 1 - 1735940014592: !!python/tuple - - *id066 - - 0 - 1735940014704: !!python/tuple - - *id067 - - 0 - 1735940014816: !!python/tuple - - *id068 - - 1 - 1735940014928: !!python/tuple - - *id069 - - 0 - 1735940015040: !!python/tuple - - *id070 - - 0 - 1735940015152: !!python/tuple - - *id071 - - 0 - 1735940015264: !!python/tuple - - *id072 - - 1 - 1735940015376: !!python/tuple - - *id073 - - 1 - 1735940015488: !!python/tuple - - *id074 - - 0 - 1735940015600: !!python/tuple - - *id075 - - 0 - 1735940015712: !!python/tuple - - *id076 - - 0 - 1735940015824: !!python/tuple - - *id077 - - 1 - 1735940015936: !!python/tuple - - *id078 - - 0 - 1735940016048: !!python/tuple - - *id079 - - 1 - 1735940016160: !!python/tuple - - *id080 - - 0 - 1735940016272: !!python/tuple - - *id081 - - 0 - 1735940016384: !!python/tuple - - *id082 - - 1 - 1735940016496: !!python/tuple - - *id083 - - 0 - 1735940016608: !!python/tuple - - *id084 - - 0 - 1735940016720: !!python/tuple - - *id085 - - 0 - 1735940016832: !!python/tuple - - *id086 - - 0 - 1735940016944: !!python/tuple - - *id087 - - 0 - 1735940017056: !!python/tuple - - *id088 - - 0 - 1735940017168: !!python/tuple - - *id089 - - 0 - 1735940017280: !!python/tuple - - *id090 - - 1 - 1735940017392: !!python/tuple - - *id091 - - 1 - 1735940017504: !!python/tuple - - *id092 - - 0 - 1735940017616: !!python/tuple - - *id093 - - 0 - 1735940017728: !!python/tuple - - *id094 - - 1 - 1735940017840: !!python/tuple - - *id095 - - 0 - 1735940017952: !!python/tuple - - *id096 - - 0 - 1735940018064: !!python/tuple - - *id097 - - 1 - 1735940018176: !!python/tuple - - *id098 - - 0 - 1735940018288: !!python/tuple - - *id099 - - 0 - 1735940018400: !!python/tuple - - *id100 - - 0 - 1735940018512: !!python/tuple - - *id101 - - 0 - 1735940018624: !!python/tuple - - *id102 - - 1 - 1735940018736: !!python/tuple - - *id103 - - 0 - 1735940018848: !!python/tuple - - *id104 - - 1 - 1735940018960: !!python/tuple - - *id105 - - 1 - 1735940019072: !!python/tuple - - *id106 - - 0 - 1735940019184: !!python/tuple - - *id107 - - 1 - 1735940019296: !!python/tuple - - *id108 - - 1 - 1735940019408: !!python/tuple - - *id109 - - 0 - 1735940019520: !!python/tuple - - *id110 - - 1 - 1735940019632: !!python/tuple - - *id111 - - 1 -- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution - fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet - _data: !!python/tuple [] - objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735881266400: !!python/tuple - - *id001 - - 26.398391602333636 - variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735933094128: !!python/tuple - - *id002 - - 0 - 1735933094464: !!python/tuple - - *id013 - - 0 - 1735933094576: !!python/tuple - - *id014 - - 1 - 1735933094688: !!python/tuple - - *id015 - - 1 - 1735933094800: !!python/tuple - - *id016 - - 1 - 1735933094912: !!python/tuple - - *id017 - - 0 - 1735933095024: !!python/tuple - - *id018 - - 0 - 1735933095136: !!python/tuple - - *id020 - - 0 - 1735933095248: !!python/tuple - - *id019 - - 1 - 1735933095360: !!python/tuple - - *id021 - - 0 - 1735933095472: !!python/tuple - - *id022 - - 0 - 1735933095584: !!python/tuple - - *id023 - - 1 - 1735933095696: !!python/tuple - - *id025 - - 0 - 1735933095808: !!python/tuple - - *id024 - - 0 - 1735933095920: !!python/tuple - - *id026 - - 1 - 1735933096032: !!python/tuple - - *id027 - - 1 - 1735933096144: !!python/tuple - - *id028 - - 0 - 1735933096256: !!python/tuple - - *id029 - - 0 - 1735933096368: !!python/tuple - - *id030 - - 1 - 1735933096480: !!python/tuple - - *id031 - - 0 - 1735933096592: !!python/tuple - - *id032 - - 0 - 1735933096704: !!python/tuple - - *id033 - - 0 - 1735933096816: !!python/tuple - - *id034 - - 0 - 1735933096928: !!python/tuple - - *id035 - - 0 - 1735933097040: !!python/tuple - - *id036 - - 0 - 1735933097152: !!python/tuple - - *id037 - - 0 - 1735933097264: !!python/tuple - - *id038 - - 0 - 1735933097376: !!python/tuple - - *id039 - - 0 - 1735933097488: !!python/tuple - - *id040 - - 1 - 1735933097600: !!python/tuple - - *id041 - - 0 - 1735933097712: !!python/tuple - - *id042 - - 0 - 1735933097824: !!python/tuple - - *id043 - - 1 - 1735933097936: !!python/tuple - - *id044 - - 1 - 1735933098048: !!python/tuple - - *id045 - - 1 - 1735933098160: !!python/tuple - - *id046 - - 1 - 1735933098272: !!python/tuple - - *id047 - - 0 - 1735933098384: !!python/tuple - - *id048 - - 1 - 1735933098496: !!python/tuple - - *id049 - - 0 - 1735933098608: !!python/tuple - - *id050 - - 0 - 1735933098720: !!python/tuple - - *id051 - - 1 - 1735933098832: !!python/tuple - - *id052 - - 0 - 1735933098944: !!python/tuple - - *id053 - - 0 - 1735933099056: !!python/tuple - - *id054 - - 1 - 1735933099168: !!python/tuple - - *id055 - - 1 - 1735933099280: !!python/tuple - - *id056 - - 0 - 1735933099392: !!python/tuple - - *id057 - - 0 - 1735933099504: !!python/tuple - - *id058 - - 0 - 1735933099616: !!python/tuple - - *id059 - - 1 - 1735933099728: !!python/tuple - - *id060 - - 0 - 1735933099840: !!python/tuple - - *id061 - - 1 - 1735940014144: !!python/tuple - - *id062 - - 1 - 1735940014256: !!python/tuple - - *id063 - - 1 - 1735940014368: !!python/tuple - - *id064 - - 0 - 1735940014480: !!python/tuple - - *id065 - - 1 - 1735940014592: !!python/tuple - - *id066 - - 0 - 1735940014704: !!python/tuple - - *id067 - - 0 - 1735940014816: !!python/tuple - - *id068 - - 1 - 1735940014928: !!python/tuple - - *id069 - - 0 - 1735940015040: !!python/tuple - - *id070 - - 0 - 1735940015152: !!python/tuple - - *id071 - - 0 - 1735940015264: !!python/tuple - - *id072 - - 1 - 1735940015376: !!python/tuple - - *id073 - - 1 - 1735940015488: !!python/tuple - - *id074 - - 0 - 1735940015600: !!python/tuple - - *id075 - - 0 - 1735940015712: !!python/tuple - - *id076 - - 0 - 1735940015824: !!python/tuple - - *id077 - - 1 - 1735940015936: !!python/tuple - - *id078 - - 0 - 1735940016048: !!python/tuple - - *id079 - - 1 - 1735940016160: !!python/tuple - - *id080 - - 0 - 1735940016272: !!python/tuple - - *id081 - - 0 - 1735940016384: !!python/tuple - - *id082 - - 1 - 1735940016496: !!python/tuple - - *id083 - - 0 - 1735940016608: !!python/tuple - - *id084 - - 0 - 1735940016720: !!python/tuple - - *id085 - - 0 - 1735940016832: !!python/tuple - - *id086 - - 0 - 1735940016944: !!python/tuple - - *id087 - - 0 - 1735940017056: !!python/tuple - - *id088 - - 0 - 1735940017168: !!python/tuple - - *id089 - - 0 - 1735940017280: !!python/tuple - - *id090 - - 1 - 1735940017392: !!python/tuple - - *id091 - - 1 - 1735940017504: !!python/tuple - - *id092 - - 0 - 1735940017616: !!python/tuple - - *id093 - - 0 - 1735940017728: !!python/tuple - - *id094 - - 1 - 1735940017840: !!python/tuple - - *id095 - - 0 - 1735940017952: !!python/tuple - - *id096 - - 0 - 1735940018064: !!python/tuple - - *id097 - - 1 - 1735940018176: !!python/tuple - - *id098 - - 0 - 1735940018288: !!python/tuple - - *id099 - - 0 - 1735940018400: !!python/tuple - - *id100 - - 0 - 1735940018512: !!python/tuple - - *id101 - - 0 - 1735940018624: !!python/tuple - - *id102 - - 1 - 1735940018736: !!python/tuple - - *id103 - - 0 - 1735940018848: !!python/tuple - - *id104 - - 1 - 1735940018960: !!python/tuple - - *id105 - - 1 - 1735940019072: !!python/tuple - - *id106 - - 0 - 1735940019184: !!python/tuple - - *id107 - - 1 - 1735940019296: !!python/tuple - - *id108 - - 1 - 1735940019408: !!python/tuple - - *id109 - - 0 - 1735940019520: !!python/tuple - - *id110 - - 1 - 1735940019632: !!python/tuple - - *id111 - - 1 -- !!python/object:pyomo.contrib.alternative_solutions.solution.Solution - fixed_vars: !!python/object:pyomo.common.collections.component_set.ComponentSet - _data: !!python/tuple [] - objectives: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735881266400: !!python/tuple - - *id001 - - 26.387201703323992 - variables: !!python/object/new:pyomo.common.collections.component_map.ComponentMap - state: - - 1735933094128: !!python/tuple - - *id002 - - 0 - 1735933094464: !!python/tuple - - *id013 - - 0 - 1735933094576: !!python/tuple - - *id014 - - 1 - 1735933094688: !!python/tuple - - *id015 - - 1 - 1735933094800: !!python/tuple - - *id016 - - 1 - 1735933094912: !!python/tuple - - *id017 - - 0 - 1735933095024: !!python/tuple - - *id018 - - 0 - 1735933095136: !!python/tuple - - *id020 - - 0 - 1735933095248: !!python/tuple - - *id019 - - 1 - 1735933095360: !!python/tuple - - *id021 - - 0 - 1735933095472: !!python/tuple - - *id022 - - 0 - 1735933095584: !!python/tuple - - *id023 - - 1 - 1735933095696: !!python/tuple - - *id025 - - 0 - 1735933095808: !!python/tuple - - *id024 - - 0 - 1735933095920: !!python/tuple - - *id026 - - 1 - 1735933096032: !!python/tuple - - *id027 - - 1 - 1735933096144: !!python/tuple - - *id028 - - 0 - 1735933096256: !!python/tuple - - *id029 - - 0 - 1735933096368: !!python/tuple - - *id030 - - 1 - 1735933096480: !!python/tuple - - *id031 - - 0 - 1735933096592: !!python/tuple - - *id032 - - 0 - 1735933096704: !!python/tuple - - *id033 - - 0 - 1735933096816: !!python/tuple - - *id034 - - 1 - 1735933096928: !!python/tuple - - *id035 - - 0 - 1735933097040: !!python/tuple - - *id036 - - 0 - 1735933097152: !!python/tuple - - *id037 - - 0 - 1735933097264: !!python/tuple - - *id038 - - 0 - 1735933097376: !!python/tuple - - *id039 - - 0 - 1735933097488: !!python/tuple - - *id040 - - 1 - 1735933097600: !!python/tuple - - *id041 - - 0 - 1735933097712: !!python/tuple - - *id042 - - 0 - 1735933097824: !!python/tuple - - *id043 - - 1 - 1735933097936: !!python/tuple - - *id044 - - 1 - 1735933098048: !!python/tuple - - *id045 - - 1 - 1735933098160: !!python/tuple - - *id046 - - 1 - 1735933098272: !!python/tuple - - *id047 - - 0 - 1735933098384: !!python/tuple - - *id048 - - 1 - 1735933098496: !!python/tuple - - *id049 - - 0 - 1735933098608: !!python/tuple - - *id050 - - 0 - 1735933098720: !!python/tuple - - *id051 - - 1 - 1735933098832: !!python/tuple - - *id052 - - 0 - 1735933098944: !!python/tuple - - *id053 - - 0 - 1735933099056: !!python/tuple - - *id054 - - 0 - 1735933099168: !!python/tuple - - *id055 - - 1 - 1735933099280: !!python/tuple - - *id056 - - 0 - 1735933099392: !!python/tuple - - *id057 - - 0 - 1735933099504: !!python/tuple - - *id058 - - 0 - 1735933099616: !!python/tuple - - *id059 - - 1 - 1735933099728: !!python/tuple - - *id060 - - 0 - 1735933099840: !!python/tuple - - *id061 - - 1 - 1735940014144: !!python/tuple - - *id062 - - 1 - 1735940014256: !!python/tuple - - *id063 - - 1 - 1735940014368: !!python/tuple - - *id064 - - 0 - 1735940014480: !!python/tuple - - *id065 - - 1 - 1735940014592: !!python/tuple - - *id066 - - 0 - 1735940014704: !!python/tuple - - *id067 - - 0 - 1735940014816: !!python/tuple - - *id068 - - 1 - 1735940014928: !!python/tuple - - *id069 - - 0 - 1735940015040: !!python/tuple - - *id070 - - 0 - 1735940015152: !!python/tuple - - *id071 - - 0 - 1735940015264: !!python/tuple - - *id072 - - 1 - 1735940015376: !!python/tuple - - *id073 - - 1 - 1735940015488: !!python/tuple - - *id074 - - 0 - 1735940015600: !!python/tuple - - *id075 - - 0 - 1735940015712: !!python/tuple - - *id076 - - 0 - 1735940015824: !!python/tuple - - *id077 - - 1 - 1735940015936: !!python/tuple - - *id078 - - 0 - 1735940016048: !!python/tuple - - *id079 - - 1 - 1735940016160: !!python/tuple - - *id080 - - 0 - 1735940016272: !!python/tuple - - *id081 - - 0 - 1735940016384: !!python/tuple - - *id082 - - 1 - 1735940016496: !!python/tuple - - *id083 - - 0 - 1735940016608: !!python/tuple - - *id084 - - 0 - 1735940016720: !!python/tuple - - *id085 - - 0 - 1735940016832: !!python/tuple - - *id086 - - 0 - 1735940016944: !!python/tuple - - *id087 - - 0 - 1735940017056: !!python/tuple - - *id088 - - 0 - 1735940017168: !!python/tuple - - *id089 - - 0 - 1735940017280: !!python/tuple - - *id090 - - 1 - 1735940017392: !!python/tuple - - *id091 - - 1 - 1735940017504: !!python/tuple - - *id092 - - 0 - 1735940017616: !!python/tuple - - *id093 - - 0 - 1735940017728: !!python/tuple - - *id094 - - 1 - 1735940017840: !!python/tuple - - *id095 - - 0 - 1735940017952: !!python/tuple - - *id096 - - 0 - 1735940018064: !!python/tuple - - *id097 - - 1 - 1735940018176: !!python/tuple - - *id098 - - 1 - 1735940018288: !!python/tuple - - *id099 - - 0 - 1735940018400: !!python/tuple - - *id100 - - 0 - 1735940018512: !!python/tuple - - *id101 - - 0 - 1735940018624: !!python/tuple - - *id102 - - 1 - 1735940018736: !!python/tuple - - *id103 - - 0 - 1735940018848: !!python/tuple - - *id104 - - 1 - 1735940018960: !!python/tuple - - *id105 - - 1 - 1735940019072: !!python/tuple - - *id106 - - 0 - 1735940019184: !!python/tuple - - *id107 - - 1 - 1735940019296: !!python/tuple - - *id108 - - 1 - 1735940019408: !!python/tuple - - *id109 - - 1 - 1735940019520: !!python/tuple - - *id110 - - 1 - 1735940019632: !!python/tuple - - *id111 - - 1 +- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, + 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, + 'x[22]': 0, 'x[23]': 1, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, + 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, + 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 0, 'x[40]': 1, + 'x[41]': 0, 'x[42]': 0, 'x[43]': 1, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, + 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, + 'x[54]': 1, 'x[55]': 1, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, + 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, + 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, + 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, + 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, + 'x[86]': 0, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, + 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, + 'x[99]': 1, 'x[9]': 0} +- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, + 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, + 'x[22]': 0, 'x[23]': 0, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, + 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, + 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 0, 'x[40]': 1, + 'x[41]': 0, 'x[42]': 0, 'x[43]': 1, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, + 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, + 'x[54]': 1, 'x[55]': 1, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, + 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, + 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, + 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, + 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, + 'x[86]': 0, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, + 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, + 'x[99]': 1, 'x[9]': 0} +- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, + 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, + 'x[22]': 0, 'x[23]': 1, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, + 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 1, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, + 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, + 'x[41]': 0, 'x[42]': 0, 'x[43]': 0, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, + 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, + 'x[54]': 1, 'x[55]': 1, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, + 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, + 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, + 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, + 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, + 'x[86]': 1, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, + 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, + 'x[99]': 1, 'x[9]': 0} +- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, + 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, + 'x[22]': 0, 'x[23]': 0, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, + 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 1, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, + 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, + 'x[41]': 0, 'x[42]': 0, 'x[43]': 0, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, + 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, + 'x[54]': 1, 'x[55]': 1, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, + 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, + 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, + 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, + 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, + 'x[86]': 1, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, + 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, + 'x[99]': 1, 'x[9]': 0} +- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, + 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, + 'x[22]': 0, 'x[23]': 1, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, + 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, + 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, + 'x[41]': 0, 'x[42]': 0, 'x[43]': 1, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, + 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, + 'x[54]': 1, 'x[55]': 0, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, + 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, + 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, + 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, + 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, + 'x[86]': 0, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, + 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, + 'x[99]': 1, 'x[9]': 0} +- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, + 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, + 'x[22]': 0, 'x[23]': 0, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, + 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, + 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, + 'x[41]': 0, 'x[42]': 0, 'x[43]': 1, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, + 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, + 'x[54]': 1, 'x[55]': 0, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, + 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, + 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, + 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, + 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, + 'x[86]': 0, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, + 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, + 'x[99]': 1, 'x[9]': 0} +- {'x[100]': 1, 'x[10]': 0, 'x[11]': 1, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, + 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, + 'x[22]': 0, 'x[23]': 0, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, + 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, + 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, + 'x[41]': 0, 'x[42]': 0, 'x[43]': 0, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, + 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, + 'x[54]': 1, 'x[55]': 0, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, + 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, + 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, + 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, + 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, + 'x[86]': 1, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, + 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, + 'x[99]': 0, 'x[9]': 0} +- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, + 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, + 'x[22]': 0, 'x[23]': 1, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, + 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, + 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, + 'x[41]': 0, 'x[42]': 0, 'x[43]': 1, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, + 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, + 'x[54]': 1, 'x[55]': 0, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, + 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, + 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, + 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, + 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, + 'x[86]': 1, 'x[87]': 0, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, + 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, + 'x[99]': 1, 'x[9]': 0} +- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, + 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, + 'x[22]': 0, 'x[23]': 0, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, + 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, + 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, + 'x[41]': 0, 'x[42]': 0, 'x[43]': 1, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, + 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, + 'x[54]': 1, 'x[55]': 0, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, + 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, + 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, + 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, + 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, + 'x[86]': 1, 'x[87]': 0, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, + 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, + 'x[99]': 1, 'x[9]': 0} +- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, + 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, + 'x[22]': 0, 'x[23]': 1, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, + 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, + 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, + 'x[41]': 0, 'x[42]': 0, 'x[43]': 0, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, + 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, + 'x[54]': 1, 'x[55]': 0, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, + 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, + 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, + 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, + 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, + 'x[86]': 1, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, + 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 1, + 'x[99]': 1, 'x[9]': 0} diff --git a/pyomo/contrib/alternative_solutions/tests/obbt_test.py b/pyomo/contrib/alternative_solutions/tests/obbt_test.py index 7565febab3d..43946b4afbb 100644 --- a/pyomo/contrib/alternative_solutions/tests/obbt_test.py +++ b/pyomo/contrib/alternative_solutions/tests/obbt_test.py @@ -1,45 +1,124 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Aug 4 15:59:24 2022 -@author: jlgearh -""" -import random -import pyomo.environ as pe +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ -from pyomo.contrib.alternative_solutions.obbt import obbt_analysis +"""Tests for the GDPopt solver plugin.""" -def get_random_knapsack_model(num_x_vars, num_y_vars, budget_pct, seed=1000): - random.seed(seed) +# from contextlib import redirect_stdout +# from io import StringIO +# import logging +# from math import fabs +# from os.path import join, normpath + +# import pyomo.common.unittest as unittest +# from pyomo.common.log import LoggingIntercept +# from pyomo.common.collections import Bunch +# from pyomo.common.config import ConfigDict, ConfigValue +# from pyomo.common.fileutils import import_file, PYOMO_ROOT_DIR +# from pyomo.contrib.appsi.solvers.gurobi import Gurobi +# from pyomo.contrib.gdpopt.create_oa_subproblems import ( +# add_util_block, add_disjunct_list, add_constraints_by_disjunct, +# add_global_constraint_list) +# import pyomo.contrib.gdpopt.tests.common_tests as ct +# from pyomo.contrib.gdpopt.util import is_feasible, time_code +# from pyomo.contrib.mcpp.pyomo_mcpp import mcpp_available +# from pyomo.contrib.gdpopt.solve_discrete_problem import ( +# solve_MILP_discrete_problem, distinguish_mip_infeasible_or_unbounded) +# from pyomo.environ import ( +# Block, ConcreteModel, Constraint, Integers, LogicalConstraint, maximize, +# Objective, RangeSet, TransformationFactory, SolverFactory, sqrt, value, Var) +# from pyomo.gdp import Disjunct, Disjunction +# from pyomo.gdp.tests import models +# from pyomo.opt import TerminationCondition + +class TestGDPoptUnit(unittest.TestCase): + """Real unit tests for GDPopt""" + + #@unittest.skipUnless(SolverFactory(mip_solver).available(), + # "MIP solver not available") + def test_continuous_2d(self): + m = ConcreteModel() + m.GDPopt_utils = Block() + m.x = Var(bounds=(-1, 10)) + m.y = Var(bounds=(2, 3)) + m.z = Var() + # Include a disjunction so that we don't default to just a MIP solver + m.d = Disjunction(expr=[ + [m.x + m.y >= 5], [m.x - m.y <= 3] + ]) + m.o = Objective(expr=m.z) + m.GDPopt_utils.variable_list = [m.x, m.y, m.z] + m.GDPopt_utils.disjunct_list = [m.d._autodisjuncts[0], + m.d._autodisjuncts[1]] + output = StringIO() + with LoggingIntercept(output, 'pyomo.contrib.gdpopt', logging.WARNING): + solver = SolverFactory('gdpopt.loa') + dummy = Block() + dummy.timing = Bunch() + with time_code(dummy.timing, 'main', is_main_timer=True): + tc = solve_MILP_discrete_problem( + m.GDPopt_utils, + dummy, + solver.CONFIG(dict(mip_solver=mip_solver))) + self.assertIn("Discrete problem was unbounded. Re-solving with " + "arbitrary bound values", output.getvalue().strip()) + self.assertIs(tc, TerminationCondition.unbounded) + +if __name__ == '__main__': + unittest.main() + + +# # -*- coding: utf-8 -*- +# """ +# Created on Thu Aug 4 15:59:24 2022 + +# @author: jlgearh +# """ +# import random + +# import pyomo.environ as pe + +# from pyomo.contrib.alternative_solutions.obbt import obbt_analysis + +# def get_random_knapsack_model(num_x_vars, num_y_vars, budget_pct, seed=1000): +# random.seed(seed) - W = budget_pct * (num_x_vars + num_y_vars) / 2 +# W = budget_pct * (num_x_vars + num_y_vars) / 2 - model = pe.ConcreteModel() +# model = pe.ConcreteModel() - model.X_INDEX = pe.RangeSet(1,num_x_vars) - model.Y_INDEX = pe.RangeSet(1,num_y_vars) +# model.X_INDEX = pe.RangeSet(1,num_x_vars) +# model.Y_INDEX = pe.RangeSet(1,num_y_vars) - model.wu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) - model.vu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) - model.x = pe.Var(model.X_INDEX, within=pe.NonNegativeIntegers) +# model.wu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) +# model.vu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) +# model.x = pe.Var(model.X_INDEX, within=pe.NonNegativeIntegers) - model.b = pe.Block() - model.b.wl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) - model.b.vl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) - model.b.y = pe.Var(model.Y_INDEX, within=pe.NonNegativeReals) +# model.b = pe.Block() +# model.b.wl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) +# model.b.vl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) +# model.b.y = pe.Var(model.Y_INDEX, within=pe.NonNegativeReals) - model.o = pe.Objective(expr=sum(model.vu[i]*model.x[i] for i in model.X_INDEX) + \ - sum(model.b.vl[i]*model.b.y[i] for i in model.Y_INDEX), sense=pe.maximize) - model.c = pe.Constraint(expr=sum(model.wu[i]*model.x[i] for i in model.X_INDEX) + \ - sum(model.b.wl[i]*model.b.y[i] for i in model.Y_INDEX)<= W) +# model.o = pe.Objective(expr=sum(model.vu[i]*model.x[i] for i in model.X_INDEX) + \ +# sum(model.b.vl[i]*model.b.y[i] for i in model.Y_INDEX), sense=pe.maximize) +# model.c = pe.Constraint(expr=sum(model.wu[i]*model.x[i] for i in model.X_INDEX) + \ +# sum(model.b.wl[i]*model.b.y[i] for i in model.Y_INDEX)<= W) - return model - -model = get_random_knapsack_model(4, 4, 0.2) -result = obbt_analysis(model, variables='all', rel_opt_gap=None, - abs_gap=None, already_solved=False, - solver='gurobi', solver_options={}, - use_persistent_solver = False, tee=True, - refine_bounds=False) \ No newline at end of file +# return model + +# model = get_random_knapsack_model(4, 4, 0.2) +# result = obbt_analysis(model, variables='all', rel_opt_gap=None, +# abs_gap=None, already_solved=False, +# solver='gurobi', solver_options={}, +# use_persistent_solver = False, tee=True, +# refine_bounds=False) \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_case.xlsx b/pyomo/contrib/alternative_solutions/tests/test_case.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..99d46d84d6e54e7dd993a6cae5e5d385b003a0d0 GIT binary patch literal 9880 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 RyDX?s?U1= m.y) + m.c4 = pe.Constraint(expr= -1/2 * m.x + 3 >= m.y) + m.c5 = pe.Constraint(expr= 2.30769230769231 >= m.y) + + return m + +def knapsack(N): + random.seed(1000) + + N = N + W = N/10.0 + + + model = pe.ConcreteModel() + + model.INDEX = pe.RangeSet(1,N) + + model.w = pe.Param(model.INDEX, initialize=lambda model, i : random.uniform(0.0,1.0), within=pe.Reals) + + model.v = pe.Param(model.INDEX, initialize=lambda model, i : random.uniform(0.0,1.0), within=pe.Reals) + + model.x = pe.Var(model.INDEX, within=pe.Binary) + + model.o = pe.Objective(expr=sum(model.v[i]*model.x[i] for i in model.INDEX), sense=pe.maximize) + + model.c = pe.Constraint(expr=sum(model.w[i]*model.x[i] for i in model.INDEX) <= W) + + return model \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/var_utils.py b/pyomo/contrib/alternative_solutions/var_utils.py index 9b39b0a0228..c387f2ffd93 100644 --- a/pyomo/contrib/alternative_solutions/var_utils.py +++ b/pyomo/contrib/alternative_solutions/var_utils.py @@ -67,7 +67,7 @@ def get_model_variables(model, components='all', include_continuous=True, ''' # Validate inputs - aos_utils._is_concrete_model(model) + aos_utils._check_concrete_model(model) assert isinstance(include_continuous, bool), \ 'include_continuous must be a Boolean' assert isinstance(include_binary, bool), 'include_binary must be a Boolean' @@ -130,5 +130,5 @@ def get_model_variables(model, components='all', include_continuous=True, return variable_set -def check_variables(model, variables): +def check_variables(model, variables, include_fixed=False): pass \ No newline at end of file From 3489dfcfa3a39d732596d17bcbdc58bb46233710 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 16:32:58 -0600 Subject: [PATCH 0061/3044] Update the results object --- .../contrib/appsi/examples/getting_started.py | 2 +- pyomo/contrib/appsi/solvers/cbc.py | 24 ++--- pyomo/contrib/appsi/solvers/cplex.py | 20 ++-- pyomo/contrib/appsi/solvers/gurobi.py | 22 ++-- pyomo/contrib/appsi/solvers/highs.py | 14 +-- pyomo/contrib/appsi/solvers/ipopt.py | 22 ++-- .../solvers/tests/test_gurobi_persistent.py | 22 ++-- .../solvers/tests/test_persistent_solvers.py | 102 +++++++++--------- pyomo/solver/base.py | 65 +++++++---- pyomo/solver/config.py | 12 +-- pyomo/solver/tests/test_base.py | 4 +- 11 files changed, 166 insertions(+), 143 deletions(-) diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index de5357776f4..79f1aa845b3 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -36,7 +36,7 @@ def main(plot=True, n_points=200): res.termination_condition == solver_base.TerminationCondition.convergenceCriteriaSatisfied ) - obj_values.append(res.best_feasible_objective) + obj_values.append(res.incumbent_objective) opt.load_vars([m.x]) x_values.append(m.x.value) timer.stop('p loop') diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 021ff76217d..9ae1ecba1f2 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -313,23 +313,23 @@ def _parse_soln(self): for v_id, (v, val) in self._primal_sol.items(): v.set_value(val, skip_validation=True) if self._writer.get_active_objective() is None: - results.best_feasible_objective = None + results.incumbent_objective = None else: - results.best_feasible_objective = obj_val + results.incumbent_objective = obj_val elif ( results.termination_condition == TerminationCondition.convergenceCriteriaSatisfied ): if self._writer.get_active_objective() is None: - results.best_feasible_objective = None + results.incumbent_objective = None else: - results.best_feasible_objective = obj_val + results.incumbent_objective = obj_val elif self.config.load_solution: raise RuntimeError( 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) return results @@ -406,24 +406,24 @@ def _check_and_escape_options(): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) results = Results() results.termination_condition = TerminationCondition.error - results.best_feasible_objective = None + results.incumbent_objective = None else: timer.start('parse solution') results = self._parse_soln() timer.stop('parse solution') if self._writer.get_active_objective() is None: - results.best_feasible_objective = None - results.best_objective_bound = None + results.incumbent_objective = None + results.objective_bound = None else: if self._writer.get_active_objective().sense == minimize: - results.best_objective_bound = -math.inf + results.objective_bound = -math.inf else: - results.best_objective_bound = math.inf + results.objective_bound = math.inf results.solution_loader = PersistentSolutionLoader(solver=self) @@ -434,7 +434,7 @@ def get_primals( ) -> Mapping[_GeneralVarData, float]: if ( self._last_results_object is None - or self._last_results_object.best_feasible_objective is None + or self._last_results_object.incumbent_objective is None ): raise RuntimeError( 'Solver does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 759bd7ff9d5..bab6afd7375 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -299,33 +299,33 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): results.termination_condition = TerminationCondition.unknown if self._writer.get_active_objective() is None: - results.best_feasible_objective = None - results.best_objective_bound = None + results.incumbent_objective = None + results.objective_bound = None else: if cpxprob.solution.get_solution_type() != cpxprob.solution.type.none: if ( cpxprob.variables.get_num_binary() + cpxprob.variables.get_num_integer() ) == 0: - results.best_feasible_objective = ( + results.incumbent_objective = ( cpxprob.solution.get_objective_value() ) - results.best_objective_bound = ( + results.objective_bound = ( cpxprob.solution.get_objective_value() ) else: - results.best_feasible_objective = ( + results.incumbent_objective = ( cpxprob.solution.get_objective_value() ) - results.best_objective_bound = ( + results.objective_bound = ( cpxprob.solution.MIP.get_best_objective() ) else: - results.best_feasible_objective = None + results.incumbent_objective = None if cpxprob.objective.get_sense() == cpxprob.objective.sense.minimize: - results.best_objective_bound = -math.inf + results.objective_bound = -math.inf else: - results.best_objective_bound = math.inf + results.objective_bound = math.inf if config.load_solution: if cpxprob.solution.get_solution_type() == cpxprob.solution.type.none: @@ -333,7 +333,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): 'A feasible solution was not found, so no solution can be loades. ' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) else: if ( diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index c2db835922d..8691151f475 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -899,25 +899,25 @@ def _postsolve(self, timer: HierarchicalTimer): else: results.termination_condition = TerminationCondition.unknown - results.best_feasible_objective = None - results.best_objective_bound = None + results.incumbent_objective = None + results.objective_bound = None if self._objective is not None: try: - results.best_feasible_objective = gprob.ObjVal + results.incumbent_objective = gprob.ObjVal except (gurobipy.GurobiError, AttributeError): - results.best_feasible_objective = None + results.incumbent_objective = None try: - results.best_objective_bound = gprob.ObjBound + results.objective_bound = gprob.ObjBound except (gurobipy.GurobiError, AttributeError): if self._objective.sense == minimize: - results.best_objective_bound = -math.inf + results.objective_bound = -math.inf else: - results.best_objective_bound = math.inf + results.objective_bound = math.inf - if results.best_feasible_objective is not None and not math.isfinite( - results.best_feasible_objective + if results.incumbent_objective is not None and not math.isfinite( + results.incumbent_objective ): - results.best_feasible_objective = None + results.incumbent_objective = None timer.start('load solution') if config.load_solution: @@ -938,7 +938,7 @@ def _postsolve(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) timer.stop('load solution') diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 3b7c92ed9e8..7b973a297f6 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -660,23 +660,23 @@ def _postsolve(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) timer.stop('load solution') info = highs.getInfo() - results.best_objective_bound = None - results.best_feasible_objective = None + results.objective_bound = None + results.incumbent_objective = None if self._objective is not None: if has_feasible_solution: - results.best_feasible_objective = info.objective_function_value + results.incumbent_objective = info.objective_function_value if info.mip_node_count == -1: if has_feasible_solution: - results.best_objective_bound = info.objective_function_value + results.objective_bound = info.objective_function_value else: - results.best_objective_bound = None + results.objective_bound = None else: - results.best_objective_bound = info.mip_dual_bound + results.objective_bound = info.mip_dual_bound return results diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 6c4b7601d2c..0249d97258f 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -390,9 +390,9 @@ def _parse_sol(self): for v, val in self._primal_sol.items(): v.set_value(val, skip_validation=True) if self._writer.get_active_objective() is None: - results.best_feasible_objective = None + results.incumbent_objective = None else: - results.best_feasible_objective = value( + results.incumbent_objective = value( self._writer.get_active_objective().expr ) elif ( @@ -400,7 +400,7 @@ def _parse_sol(self): == TerminationCondition.convergenceCriteriaSatisfied ): if self._writer.get_active_objective() is None: - results.best_feasible_objective = None + results.incumbent_objective = None else: obj_expr_evaluated = replace_expressions( self._writer.get_active_objective().expr, @@ -410,13 +410,13 @@ def _parse_sol(self): descend_into_named_expressions=True, remove_named_expressions=True, ) - results.best_feasible_objective = value(obj_expr_evaluated) + results.incumbent_objective = value(obj_expr_evaluated) elif self.config.load_solution: raise RuntimeError( 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) return results @@ -480,23 +480,23 @@ def _apply_solver(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) results = Results() results.termination_condition = TerminationCondition.error - results.best_feasible_objective = None + results.incumbent_objective = None else: timer.start('parse solution') results = self._parse_sol() timer.stop('parse solution') if self._writer.get_active_objective() is None: - results.best_objective_bound = None + results.objective_bound = None else: if self._writer.get_active_objective().sense == minimize: - results.best_objective_bound = -math.inf + results.objective_bound = -math.inf else: - results.best_objective_bound = math.inf + results.objective_bound = math.inf results.solution_loader = PersistentSolutionLoader(solver=self) @@ -507,7 +507,7 @@ def get_primals( ) -> Mapping[_GeneralVarData, float]: if ( self._last_results_object is None - or self._last_results_object.best_feasible_objective is None + or self._last_results_object.incumbent_objective is None ): raise RuntimeError( 'Solver does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index 9fdce87b8de..7e1d3e37af6 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -155,12 +155,12 @@ def test_lp(self): x, y = self.get_solution() opt = Gurobi() res = opt.solve(self.m) - self.assertAlmostEqual(x + y, res.best_feasible_objective) - self.assertAlmostEqual(x + y, res.best_objective_bound) + self.assertAlmostEqual(x + y, res.incumbent_objective) + self.assertAlmostEqual(x + y, res.objective_bound) self.assertEqual( res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied ) - self.assertTrue(res.best_feasible_objective is not None) + self.assertTrue(res.incumbent_objective is not None) self.assertAlmostEqual(x, self.m.x.value) self.assertAlmostEqual(y, self.m.y.value) @@ -196,11 +196,11 @@ def test_nonconvex_qcp_objective_bound_1(self): opt.gurobi_options['BestBdStop'] = -8 opt.config.load_solution = False res = opt.solve(m) - self.assertEqual(res.best_feasible_objective, None) - self.assertAlmostEqual(res.best_objective_bound, -8) + self.assertEqual(res.incumbent_objective, None) + self.assertAlmostEqual(res.objective_bound, -8) def test_nonconvex_qcp_objective_bound_2(self): - # the goal of this test is to ensure we can best_objective_bound properly + # the goal of this test is to ensure we can objective_bound properly # for nonconvex but continuous problems when the solver terminates with a nonzero gap # # This is a fragile test because it could fail if Gurobi's algorithms change @@ -214,8 +214,8 @@ def test_nonconvex_qcp_objective_bound_2(self): opt.gurobi_options['nonconvex'] = 2 opt.gurobi_options['MIPGap'] = 0.5 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, -4) - self.assertAlmostEqual(res.best_objective_bound, -6) + self.assertAlmostEqual(res.incumbent_objective, -4) + self.assertAlmostEqual(res.objective_bound, -6) def test_range_constraints(self): m = pe.ConcreteModel() @@ -282,7 +282,7 @@ def test_quadratic_objective(self): res = opt.solve(m) self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) self.assertAlmostEqual( - res.best_feasible_objective, + res.incumbent_objective, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value, ) @@ -292,7 +292,7 @@ def test_quadratic_objective(self): res = opt.solve(m) self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) self.assertAlmostEqual( - res.best_feasible_objective, + res.incumbent_objective, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value, ) @@ -467,7 +467,7 @@ def test_zero_time_limit(self): # what we are trying to test. Unfortunately, I'm # not sure of a good way to guarantee that if num_solutions == 0: - self.assertIsNone(res.best_feasible_objective) + self.assertIsNone(res.incumbent_objective) class TestManualModel(unittest.TestCase): diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index bf92244ec36..352f93b7ad1 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -249,8 +249,8 @@ def test_param_changes( ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value) - self.assertTrue(res.best_objective_bound <= m.y.value) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -290,8 +290,8 @@ def test_immutable_param( ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value) - self.assertTrue(res.best_objective_bound <= m.y.value) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -327,8 +327,8 @@ def test_equality( ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value) - self.assertTrue(res.best_objective_bound <= m.y.value) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @@ -369,8 +369,8 @@ def test_linear_expression( TerminationCondition.convergenceCriteriaSatisfied, ) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value) - self.assertTrue(res.best_objective_bound <= m.y.value) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound <= m.y.value) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_no_objective( @@ -403,8 +403,8 @@ def test_no_objective( ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertEqual(res.best_feasible_objective, None) - self.assertEqual(res.best_objective_bound, None) + self.assertEqual(res.incumbent_objective, None) + self.assertEqual(res.objective_bound, None) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], 0) self.assertAlmostEqual(duals[m.c2], 0) @@ -434,8 +434,8 @@ def test_add_remove_cons( ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value) - self.assertTrue(res.best_objective_bound <= m.y.value) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -447,8 +447,8 @@ def test_add_remove_cons( ) self.assertAlmostEqual(m.x.value, (b3 - b1) / (a1 - a3)) self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value) - self.assertTrue(res.best_objective_bound <= m.y.value) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) self.assertAlmostEqual(duals[m.c2], 0) @@ -461,8 +461,8 @@ def test_add_remove_cons( ) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value) - self.assertTrue(res.best_objective_bound <= m.y.value) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -502,7 +502,7 @@ def test_results_infeasible( self.assertIn(res.termination_condition, acceptable_termination_conditions) self.assertAlmostEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, None) - self.assertTrue(res.best_feasible_objective is None) + self.assertTrue(res.incumbent_objective is None) with self.assertRaisesRegex( RuntimeError, '.*does not currently have a valid solution.*' @@ -789,16 +789,16 @@ def test_mutable_param_with_range( if sense is pe.minimize: self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) - self.assertTrue(res.best_objective_bound <= m.y.value + 1e-12) + self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) + self.assertTrue(res.objective_bound <= m.y.value + 1e-12) duals = opt.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) else: self.assertAlmostEqual(m.x.value, (c2 - c1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) - self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) - self.assertTrue(res.best_objective_bound >= m.y.value - 1e-12) + self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) + self.assertTrue(res.objective_bound >= m.y.value - 1e-12) duals = opt.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @@ -1077,13 +1077,13 @@ def test_objective_changes( m.c2 = pe.Constraint(expr=m.y >= -m.x + 1) m.obj = pe.Objective(expr=m.y) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) m.obj = pe.Objective(expr=2 * m.y) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 2) + self.assertAlmostEqual(res.incumbent_objective, 2) m.obj.expr = 3 * m.y res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 3) + self.assertAlmostEqual(res.incumbent_objective, 3) m.obj.sense = pe.maximize opt.config.load_solution = False res = opt.solve(m) @@ -1099,30 +1099,30 @@ def test_objective_changes( m.obj = pe.Objective(expr=m.x * m.y) m.x.fix(2) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 6, 6) + self.assertAlmostEqual(res.incumbent_objective, 6, 6) m.x.fix(3) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 12, 6) + self.assertAlmostEqual(res.incumbent_objective, 12, 6) m.x.unfix() m.y.fix(2) m.x.setlb(-3) m.x.setub(5) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, -2, 6) + self.assertAlmostEqual(res.incumbent_objective, -2, 6) m.y.unfix() m.x.setlb(None) m.x.setub(None) m.e = pe.Expression(expr=2) m.obj = pe.Objective(expr=m.e * m.y) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 2) + self.assertAlmostEqual(res.incumbent_objective, 2) m.e.expr = 3 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 3) + self.assertAlmostEqual(res.incumbent_objective, 3) opt.update_config.check_for_new_objective = False m.e.expr = 4 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 4) + self.assertAlmostEqual(res.incumbent_objective, 4) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_domain( @@ -1135,20 +1135,20 @@ def test_domain( m.x = pe.Var(bounds=(1, None), domain=pe.NonNegativeReals) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) m.x.setlb(-1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) m.x.setlb(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) m.x.setlb(-1) m.x.domain = pe.Reals res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, -1) + self.assertAlmostEqual(res.incumbent_objective, -1) m.x.domain = pe.NonNegativeReals res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_domain_with_integers( @@ -1161,20 +1161,20 @@ def test_domain_with_integers( m.x = pe.Var(bounds=(-1, None), domain=pe.NonNegativeIntegers) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) m.x.setlb(0.5) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) m.x.setlb(-5.5) m.x.domain = pe.Integers res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, -5) + self.assertAlmostEqual(res.incumbent_objective, -5) m.x.domain = pe.Binary res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) m.x.setlb(0.5) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_binaries( @@ -1190,19 +1190,19 @@ def test_fixed_binaries( m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( @@ -1226,7 +1226,7 @@ def test_with_gdp( pe.TransformationFactory("gdp.bigm").apply_to(m) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) @@ -1250,7 +1250,7 @@ def test_variables_elsewhere( self.assertEqual( res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied ) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, 1) @@ -1259,7 +1259,7 @@ def test_variables_elsewhere( self.assertEqual( res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied ) - self.assertAlmostEqual(res.best_feasible_objective, 2) + self.assertAlmostEqual(res.incumbent_objective, 2) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 2) @@ -1286,7 +1286,7 @@ def test_variables_elsewhere2( self.assertEqual( res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied ) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) self.assertIn(m.y, sol) @@ -1298,7 +1298,7 @@ def test_variables_elsewhere2( self.assertEqual( res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied ) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) self.assertIn(m.y, sol) @@ -1324,14 +1324,14 @@ def test_bug_1( self.assertEqual( res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied ) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.incumbent_objective, 0) m.p.value = 1 res = opt.solve(m) self.assertEqual( res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied ) - self.assertAlmostEqual(res.best_feasible_objective, 3) + self.assertAlmostEqual(res.incumbent_objective, 3) @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index f0a07d0aca3..efce7b09f54 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -11,12 +11,14 @@ import abc import enum +from datetime import datetime from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeInt, In, NonNegativeFloat from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError from pyomo.opt.base import SolverFactory as LegacySolverFactory @@ -106,37 +108,62 @@ class SolutionStatus(enum.IntEnum): optimal = 30 -class Results: +class Results(ConfigDict): """ Attributes ---------- termination_condition: TerminationCondition The reason the solver exited. This is a member of the TerminationCondition enum. - best_feasible_objective: float + incumbent_objective: float If a feasible solution was found, this is the objective value of the best solution found. If no feasible solution was found, this is None. - best_objective_bound: float + objective_bound: float The best objective bound found. For minimization problems, this is the lower bound. For maximization problems, this is the upper bound. For solvers that do not provide an objective bound, this should be -inf (minimization) or inf (maximization) """ - def __init__(self): - self.solution_loader: SolutionLoaderBase = SolutionLoader( - None, None, None, None + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, ) - self.termination_condition: TerminationCondition = TerminationCondition.unknown - self.best_feasible_objective: Optional[float] = None - self.best_objective_bound: Optional[float] = None + + self.declare('solution_loader', ConfigValue(domain=In(SolutionLoaderBase), default=SolutionLoader( + None, None, None, None + ))) + self.declare('termination_condition', ConfigValue(domain=In(TerminationCondition), default=TerminationCondition.unknown)) + self.declare('solution_status', ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution)) + self.incumbent_objective: Optional[float] = self.declare('incumbent_objective', ConfigValue(domain=float)) + self.objective_bound: Optional[float] = self.declare('objective_bound', ConfigValue(domain=float)) + self.declare('solver_name', ConfigValue(domain=str)) + self.declare('solver_version', ConfigValue(domain=tuple)) + self.declare('termination_message', ConfigValue(domain=str)) + self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) + self.declare('timing_info', ConfigDict()) + self.timing_info.declare('start', ConfigValue=In(datetime)) + self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) + self.timing_info.declare('solver_wall_time', ConfigValue(domain=NonNegativeFloat)) + self.declare('extra_info', ConfigDict(implicit=True)) def __str__(self): s = '' s += 'termination_condition: ' + str(self.termination_condition) + '\n' - s += 'best_feasible_objective: ' + str(self.best_feasible_objective) + '\n' - s += 'best_objective_bound: ' + str(self.best_objective_bound) + s += 'incumbent_objective: ' + str(self.incumbent_objective) + '\n' + s += 'objective_bound: ' + str(self.objective_bound) return s @@ -496,17 +523,17 @@ def solve( legacy_results.problem.sense = obj.sense if obj.sense == minimize: - legacy_results.problem.lower_bound = results.best_objective_bound - legacy_results.problem.upper_bound = results.best_feasible_objective + legacy_results.problem.lower_bound = results.objective_bound + legacy_results.problem.upper_bound = results.incumbent_objective else: - legacy_results.problem.upper_bound = results.best_objective_bound - legacy_results.problem.lower_bound = results.best_feasible_objective + legacy_results.problem.upper_bound = results.objective_bound + legacy_results.problem.lower_bound = results.incumbent_objective if ( - results.best_feasible_objective is not None - and results.best_objective_bound is not None + results.incumbent_objective is not None + and results.objective_bound is not None ): legacy_soln.gap = abs( - results.best_feasible_objective - results.best_objective_bound + results.incumbent_objective - results.objective_bound ) else: legacy_soln.gap = None @@ -530,7 +557,7 @@ def solve( if hasattr(model, 'rc') and model.rc.import_enabled(): for v, val in results.solution_loader.get_reduced_costs().items(): model.rc[v] = val - elif results.best_feasible_objective is not None: + elif results.incumbent_objective is not None: delete_legacy_soln = False for v, val in results.solution_loader.get_primals().items(): legacy_soln.variable[symbol_map.getSymbol(v)] = {'Value': val} diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index f446dc714db..32f6e1d5da0 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -56,19 +56,15 @@ def __init__( visibility=visibility, ) - self.declare('tee', ConfigValue(domain=bool)) - self.declare('load_solution', ConfigValue(domain=bool)) - self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) - self.declare('report_timing', ConfigValue(domain=bool)) + self.declare('tee', ConfigValue(domain=bool, default=False)) + self.declare('load_solution', ConfigValue(domain=bool, default=True)) + self.declare('symbolic_solver_labels', ConfigValue(domain=bool, default=False)) + self.declare('report_timing', ConfigValue(domain=bool, default=False)) self.declare('threads', ConfigValue(domain=NonNegativeInt, default=None)) self.time_limit: Optional[float] = self.declare( 'time_limit', ConfigValue(domain=NonNegativeFloat) ) - self.tee: bool = False - self.load_solution: bool = True - self.symbolic_solver_labels: bool = False - self.report_timing: bool = False class MIPInterfaceConfig(InterfaceConfig): diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index 355941a1eb1..41b768520c1 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -139,8 +139,8 @@ def test_persistent_solver_base(self): class TestResults(unittest.TestCase): def test_uninitialized(self): res = base.Results() - self.assertIsNone(res.best_feasible_objective) - self.assertIsNone(res.best_objective_bound) + self.assertIsNone(res.incumbent_objective) + self.assertIsNone(res.objective_bound) self.assertEqual(res.termination_condition, base.TerminationCondition.unknown) with self.assertRaisesRegex( From 146fa0a10e70995ca3b82b29897a5efcc2e26fad Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 16:39:34 -0600 Subject: [PATCH 0062/3044] Back to only running appsi/solver test --- .github/workflows/test_branches.yml | 3 +-- pyomo/solver/base.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 99d5f7fc1a8..a944bbdd645 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -598,8 +598,7 @@ jobs: run: | $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ - pyomo `pwd`/pyomo-model-libraries \ - `pwd`/examples/pyomobook --junitxml="TEST-pyomo.xml" + pyomo/contrib/appsi pyomo/solver --junitxml="TEST-pyomo.xml" - name: Run Pyomo MPI tests if: matrix.mpi != 0 diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index efce7b09f54..2b5f81bef82 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -147,8 +147,8 @@ def __init__( ))) self.declare('termination_condition', ConfigValue(domain=In(TerminationCondition), default=TerminationCondition.unknown)) self.declare('solution_status', ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution)) - self.incumbent_objective: Optional[float] = self.declare('incumbent_objective', ConfigValue(domain=float)) - self.objective_bound: Optional[float] = self.declare('objective_bound', ConfigValue(domain=float)) + self.incumbent_objective: Optional[float] = self.declare('incumbent_objective', ConfigValue(domain=NonNegativeFloat)) + self.objective_bound: Optional[float] = self.declare('objective_bound', ConfigValue(domain=NonNegativeFloat)) self.declare('solver_name', ConfigValue(domain=str)) self.declare('solver_version', ConfigValue(domain=tuple)) self.declare('termination_message', ConfigValue(domain=str)) From e2d0592ec5f9714082959870eca482946f998c5d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 16:50:55 -0600 Subject: [PATCH 0063/3044] Remove domain specification --- pyomo/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 2b5f81bef82..e39e47264ad 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -142,7 +142,7 @@ def __init__( visibility=visibility, ) - self.declare('solution_loader', ConfigValue(domain=In(SolutionLoaderBase), default=SolutionLoader( + self.declare('solution_loader', ConfigValue(default=SolutionLoader( None, None, None, None ))) self.declare('termination_condition', ConfigValue(domain=In(TerminationCondition), default=TerminationCondition.unknown)) From b40ff29f023852963c50f27886068b5d07baf47d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 30 Aug 2023 16:57:40 -0600 Subject: [PATCH 0064/3044] Fix domain typo --- pyomo/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index e39e47264ad..1872011bcd9 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -154,7 +154,7 @@ def __init__( self.declare('termination_message', ConfigValue(domain=str)) self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) self.declare('timing_info', ConfigDict()) - self.timing_info.declare('start', ConfigValue=In(datetime)) + self.timing_info.declare('start', ConfigValue(domain=In(datetime))) self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) self.timing_info.declare('solver_wall_time', ConfigValue(domain=NonNegativeFloat)) self.declare('extra_info', ConfigDict(implicit=True)) From 050ceb661d48ab3d2ce825b63a2a600745e8b217 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 08:15:10 -0600 Subject: [PATCH 0065/3044] Allow negative floats --- pyomo/solver/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 1872011bcd9..0c77839e358 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -147,8 +147,8 @@ def __init__( ))) self.declare('termination_condition', ConfigValue(domain=In(TerminationCondition), default=TerminationCondition.unknown)) self.declare('solution_status', ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution)) - self.incumbent_objective: Optional[float] = self.declare('incumbent_objective', ConfigValue(domain=NonNegativeFloat)) - self.objective_bound: Optional[float] = self.declare('objective_bound', ConfigValue(domain=NonNegativeFloat)) + self.incumbent_objective: Optional[float] = self.declare('incumbent_objective', ConfigValue(domain=float)) + self.objective_bound: Optional[float] = self.declare('objective_bound', ConfigValue(domain=float)) self.declare('solver_name', ConfigValue(domain=str)) self.declare('solver_version', ConfigValue(domain=tuple)) self.declare('termination_message', ConfigValue(domain=str)) From ceb858a1bf17cc0a0e2935809b78f2fc5fb4e90c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 08:16:10 -0600 Subject: [PATCH 0066/3044] Remove type checking for start_time --- pyomo/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 0c77839e358..846431ed918 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -154,7 +154,7 @@ def __init__( self.declare('termination_message', ConfigValue(domain=str)) self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) self.declare('timing_info', ConfigDict()) - self.timing_info.declare('start', ConfigValue(domain=In(datetime))) + self.timing_info.declare('start_time') self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) self.timing_info.declare('solver_wall_time', ConfigValue(domain=NonNegativeFloat)) self.declare('extra_info', ConfigDict(implicit=True)) From cc0ad9b33dcfb1d3f7db2db3791301778bf5c125 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 08:25:11 -0600 Subject: [PATCH 0067/3044] Add empty config value to start_time --- pyomo/contrib/appsi/solvers/cplex.py | 16 +++------- pyomo/solver/base.py | 45 ++++++++++++++++++++-------- pyomo/solver/tests/__init__.py | 2 -- pyomo/solver/tests/test_base.py | 38 ++++++++++++----------- pyomo/solver/tests/test_config.py | 2 +- pyomo/solver/tests/test_solution.py | 5 +++- 6 files changed, 61 insertions(+), 47 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index bab6afd7375..ac9eaab471f 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -307,19 +307,11 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): cpxprob.variables.get_num_binary() + cpxprob.variables.get_num_integer() ) == 0: - results.incumbent_objective = ( - cpxprob.solution.get_objective_value() - ) - results.objective_bound = ( - cpxprob.solution.get_objective_value() - ) + results.incumbent_objective = cpxprob.solution.get_objective_value() + results.objective_bound = cpxprob.solution.get_objective_value() else: - results.incumbent_objective = ( - cpxprob.solution.get_objective_value() - ) - results.objective_bound = ( - cpxprob.solution.MIP.get_best_objective() - ) + results.incumbent_objective = cpxprob.solution.get_objective_value() + results.objective_bound = cpxprob.solution.MIP.get_best_objective() else: results.incumbent_objective = None if cpxprob.objective.get_sense() == cpxprob.objective.sense.minimize: diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 846431ed918..2e34747884d 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -18,7 +18,13 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeInt, In, NonNegativeFloat +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + NonNegativeInt, + In, + NonNegativeFloat, +) from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError from pyomo.opt.base import SolverFactory as LegacySolverFactory @@ -142,21 +148,36 @@ def __init__( visibility=visibility, ) - self.declare('solution_loader', ConfigValue(default=SolutionLoader( - None, None, None, None - ))) - self.declare('termination_condition', ConfigValue(domain=In(TerminationCondition), default=TerminationCondition.unknown)) - self.declare('solution_status', ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution)) - self.incumbent_objective: Optional[float] = self.declare('incumbent_objective', ConfigValue(domain=float)) - self.objective_bound: Optional[float] = self.declare('objective_bound', ConfigValue(domain=float)) + self.declare( + 'solution_loader', + ConfigValue(default=SolutionLoader(None, None, None, None)), + ) + self.declare( + 'termination_condition', + ConfigValue( + domain=In(TerminationCondition), default=TerminationCondition.unknown + ), + ) + self.declare( + 'solution_status', + ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution), + ) + self.incumbent_objective: Optional[float] = self.declare( + 'incumbent_objective', ConfigValue(domain=float) + ) + self.objective_bound: Optional[float] = self.declare( + 'objective_bound', ConfigValue(domain=float) + ) self.declare('solver_name', ConfigValue(domain=str)) self.declare('solver_version', ConfigValue(domain=tuple)) self.declare('termination_message', ConfigValue(domain=str)) self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) self.declare('timing_info', ConfigDict()) - self.timing_info.declare('start_time') + self.timing_info.declare('start_time', ConfigValue()) self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) - self.timing_info.declare('solver_wall_time', ConfigValue(domain=NonNegativeFloat)) + self.timing_info.declare( + 'solver_wall_time', ConfigValue(domain=NonNegativeFloat) + ) self.declare('extra_info', ConfigDict(implicit=True)) def __str__(self): @@ -532,9 +553,7 @@ def solve( results.incumbent_objective is not None and results.objective_bound is not None ): - legacy_soln.gap = abs( - results.incumbent_objective - results.objective_bound - ) + legacy_soln.gap = abs(results.incumbent_objective - results.objective_bound) else: legacy_soln.gap = None diff --git a/pyomo/solver/tests/__init__.py b/pyomo/solver/tests/__init__.py index 9a63db93d6a..d93cfd77b3c 100644 --- a/pyomo/solver/tests/__init__.py +++ b/pyomo/solver/tests/__init__.py @@ -8,5 +8,3 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - - diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index 41b768520c1..34d5c47d11c 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -89,24 +89,26 @@ def test_solver_availability(self): class TestPersistentSolverBase(unittest.TestCase): def test_abstract_member_list(self): - expected_list = ['remove_params', - 'version', - 'config', - 'update_variables', - 'remove_variables', - 'add_constraints', - 'get_primals', - 'set_instance', - 'set_objective', - 'update_params', - 'remove_block', - 'add_block', - 'available', - 'update_config', - 'add_params', - 'remove_constraints', - 'add_variables', - 'solve'] + expected_list = [ + 'remove_params', + 'version', + 'config', + 'update_variables', + 'remove_variables', + 'add_constraints', + 'get_primals', + 'set_instance', + 'set_objective', + 'update_params', + 'remove_block', + 'add_block', + 'available', + 'update_config', + 'add_params', + 'remove_constraints', + 'add_variables', + 'solve', + ] member_list = list(base.PersistentSolverBase.__abstractmethods__) self.assertEqual(sorted(expected_list), sorted(member_list)) diff --git a/pyomo/solver/tests/test_config.py b/pyomo/solver/tests/test_config.py index 378facb58d2..49d26513e2e 100644 --- a/pyomo/solver/tests/test_config.py +++ b/pyomo/solver/tests/test_config.py @@ -12,8 +12,8 @@ from pyomo.common import unittest from pyomo.solver.config import InterfaceConfig, MIPInterfaceConfig -class TestInterfaceConfig(unittest.TestCase): +class TestInterfaceConfig(unittest.TestCase): def test_interface_default_instantiation(self): config = InterfaceConfig() self.assertEqual(config._description, None) diff --git a/pyomo/solver/tests/test_solution.py b/pyomo/solver/tests/test_solution.py index c4c2f790b55..f4c33a60c84 100644 --- a/pyomo/solver/tests/test_solution.py +++ b/pyomo/solver/tests/test_solution.py @@ -12,13 +12,16 @@ from pyomo.common import unittest from pyomo.solver import solution + class TestPersistentSolverBase(unittest.TestCase): def test_abstract_member_list(self): expected_list = ['get_primals'] member_list = list(solution.SolutionLoaderBase.__abstractmethods__) self.assertEqual(sorted(expected_list), sorted(member_list)) - @unittest.mock.patch.multiple(solution.SolutionLoaderBase, __abstractmethods__=set()) + @unittest.mock.patch.multiple( + solution.SolutionLoaderBase, __abstractmethods__=set() + ) def test_solution_loader_base(self): self.instance = solution.SolutionLoaderBase() self.assertEqual(self.instance.get_primals(), None) From 74a459e6ca0cbfd76099540710e1a3c19ef0772e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 08:34:15 -0600 Subject: [PATCH 0068/3044] Change result attribute in HiGHS to match new standard --- pyomo/contrib/appsi/solvers/highs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 7b973a297f6..f8003599387 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -63,7 +63,7 @@ def __init__( class HighsResults(Results): def __init__(self, solver): super().__init__() - self.wallclock_time = None + self.timing_info.wall_time = None self.solution_loader = PersistentSolutionLoader(solver=solver) From 154b43803a7e4eef4c88891cf0c0cfd7702852f3 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 08:41:18 -0600 Subject: [PATCH 0069/3044] Replace all other instances of wallclock_time --- pyomo/contrib/appsi/solvers/cplex.py | 4 ++-- pyomo/contrib/appsi/solvers/gurobi.py | 4 ++-- pyomo/contrib/appsi/solvers/highs.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index ac9eaab471f..34ad88aeb7a 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -58,7 +58,7 @@ def __init__( class CplexResults(Results): def __init__(self, solver): super().__init__() - self.wallclock_time = None + self.timing_info.wall_time = None self.solution_loader = PersistentSolutionLoader(solver=solver) @@ -278,7 +278,7 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): cpxprob = self._cplex_model results = CplexResults(solver=self) - results.wallclock_time = solve_time + results.timing_info.wall_time = solve_time status = cpxprob.solution.get_status() if status in [1, 101, 102]: diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 8691151f475..cd116bcbefa 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -93,7 +93,7 @@ def get_primals(self, vars_to_load=None, solution_number=0): class GurobiResults(Results): def __init__(self, solver): super().__init__() - self.wallclock_time = None + self.timing_info.wall_time = None self.solution_loader = GurobiSolutionLoader(solver=solver) @@ -864,7 +864,7 @@ def _postsolve(self, timer: HierarchicalTimer): status = gprob.Status results = GurobiResults(self) - results.wallclock_time = gprob.Runtime + results.timing_info.wall_time = gprob.Runtime if status == grb.LOADED: # problem is loaded, but no solution results.termination_condition = TerminationCondition.unknown diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index f8003599387..a29dc2a597f 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -587,7 +587,7 @@ def _postsolve(self, timer: HierarchicalTimer): status = highs.getModelStatus() results = HighsResults(self) - results.wallclock_time = highs.getRunTime() + results.timing_info.wall_time = highs.getRunTime() if status == highspy.HighsModelStatus.kNotset: results.termination_condition = TerminationCondition.unknown From b6f1e2a63aa2c8ad235afb57f73a9da9aa6a36a6 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 08:58:39 -0600 Subject: [PATCH 0070/3044] Update unit tests for Results object --- pyomo/solver/tests/test_base.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index 34d5c47d11c..0e0780fd6fe 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -10,6 +10,7 @@ # ___________________________________________________________________________ from pyomo.common import unittest +from pyomo.common.config import ConfigDict from pyomo.solver import base import pyomo.environ as pe from pyomo.core.base.var import ScalarVar @@ -130,20 +131,51 @@ def test_persistent_solver_base(self): self.assertEqual(self.instance.set_objective(None), None) self.assertEqual(self.instance.update_variables(None), None) self.assertEqual(self.instance.update_params(), None) + with self.assertRaises(NotImplementedError): self.instance.get_duals() + with self.assertRaises(NotImplementedError): self.instance.get_slacks() + with self.assertRaises(NotImplementedError): self.instance.get_reduced_costs() class TestResults(unittest.TestCase): + def test_declared_items(self): + res = base.Results() + expected_declared = { + 'extra_info', + 'incumbent_objective', + 'iteration_count', + 'objective_bound', + 'solution_loader', + 'solution_status', + 'solver_name', + 'solver_version', + 'termination_condition', + 'termination_message', + 'timing_info', + } + actual_declared = res._declared + self.assertEqual(expected_declared, actual_declared) + def test_uninitialized(self): res = base.Results() self.assertIsNone(res.incumbent_objective) self.assertIsNone(res.objective_bound) self.assertEqual(res.termination_condition, base.TerminationCondition.unknown) + self.assertEqual(res.solution_status, base.SolutionStatus.noSolution) + self.assertIsNone(res.solver_name) + self.assertIsNone(res.solver_version) + self.assertIsNone(res.termination_message) + self.assertIsNone(res.iteration_count) + self.assertIsInstance(res.timing_info, ConfigDict) + self.assertIsInstance(res.extra_info, ConfigDict) + self.assertIsNone(res.timing_info.start_time) + self.assertIsNone(res.timing_info.wall_time) + self.assertIsNone(res.timing_info.solver_wall_time) with self.assertRaisesRegex( RuntimeError, '.*does not currently have a valid solution.*' From 072ac658c18d71aa7f68331d790db11e3c892a71 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 09:20:37 -0600 Subject: [PATCH 0071/3044] Update solution status map --- pyomo/solver/base.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 2e34747884d..fa52883f3e2 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -43,7 +43,7 @@ from pyomo.core.base import SymbolMap from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.config import UpdateConfig -from pyomo.solver.solution import SolutionLoader, SolutionLoaderBase +from pyomo.solver.solution import SolutionLoader from pyomo.solver.util import get_objective @@ -173,6 +173,7 @@ def __init__( self.declare('termination_message', ConfigValue(domain=str)) self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) self.declare('timing_info', ConfigDict()) + # TODO: Set up type checking for start_time self.timing_info.declare('start_time', ConfigValue()) self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) self.timing_info.declare( @@ -183,6 +184,7 @@ def __init__( def __str__(self): s = '' s += 'termination_condition: ' + str(self.termination_condition) + '\n' + s += 'solution_status: ' + str(self.solution_status) + '\n' s += 'incumbent_objective: ' + str(self.incumbent_objective) + '\n' s += 'objective_bound: ' + str(self.objective_bound) return s @@ -472,19 +474,18 @@ def update_params(self): legacy_solution_status_map = { - TerminationCondition.unknown: LegacySolutionStatus.unknown, - TerminationCondition.maxTimeLimit: LegacySolutionStatus.stoppedByLimit, - TerminationCondition.iterationLimit: LegacySolutionStatus.stoppedByLimit, - TerminationCondition.objectiveLimit: LegacySolutionStatus.stoppedByLimit, - TerminationCondition.minStepLength: LegacySolutionStatus.error, - TerminationCondition.convergenceCriteriaSatisfied: LegacySolutionStatus.optimal, - TerminationCondition.unbounded: LegacySolutionStatus.unbounded, - TerminationCondition.provenInfeasible: LegacySolutionStatus.infeasible, - TerminationCondition.locallyInfeasible: LegacySolutionStatus.infeasible, - TerminationCondition.infeasibleOrUnbounded: LegacySolutionStatus.unsure, - TerminationCondition.error: LegacySolutionStatus.error, - TerminationCondition.interrupted: LegacySolutionStatus.error, - TerminationCondition.licensingProblems: LegacySolutionStatus.error, + SolutionStatus.noSolution: LegacySolutionStatus.unknown, + SolutionStatus.noSolution: LegacySolutionStatus.stoppedByLimit, + SolutionStatus.noSolution: LegacySolutionStatus.error, + SolutionStatus.noSolution: LegacySolutionStatus.other, + SolutionStatus.noSolution: LegacySolutionStatus.unsure, + SolutionStatus.noSolution: LegacySolutionStatus.unbounded, + SolutionStatus.optimal: LegacySolutionStatus.locallyOptimal, + SolutionStatus.optimal: LegacySolutionStatus.globallyOptimal, + SolutionStatus.optimal: LegacySolutionStatus.optimal, + SolutionStatus.infeasible: LegacySolutionStatus.infeasible, + SolutionStatus.feasible: LegacySolutionStatus.feasible, + SolutionStatus.feasible: LegacySolutionStatus.bestSoFar, } From eeceb8667a9d01c49c9c7f3076a847a63829e677 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 09:37:32 -0600 Subject: [PATCH 0072/3044] Refactor Results to be in its own file --- pyomo/contrib/appsi/solvers/cbc.py | 3 +- pyomo/contrib/appsi/solvers/cplex.py | 3 +- pyomo/contrib/appsi/solvers/gurobi.py | 3 +- pyomo/contrib/appsi/solvers/highs.py | 3 +- pyomo/contrib/appsi/solvers/ipopt.py | 3 +- .../solvers/tests/test_persistent_solvers.py | 3 +- pyomo/solver/base.py | 213 +---------------- pyomo/solver/results.py | 225 ++++++++++++++++++ pyomo/solver/tests/test_base.py | 167 ------------- pyomo/solver/tests/test_results.py | 180 ++++++++++++++ 10 files changed, 420 insertions(+), 383 deletions(-) create mode 100644 pyomo/solver/results.py create mode 100644 pyomo/solver/tests/test_results.py diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 9ae1ecba1f2..c2686475b15 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -22,8 +22,9 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase +from pyomo.solver.base import PersistentSolverBase from pyomo.solver.config import InterfaceConfig +from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 34ad88aeb7a..86d50f1b82a 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -19,8 +19,9 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase +from pyomo.solver.base import PersistentSolverBase from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index cd116bcbefa..1f295dfcb49 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -22,8 +22,9 @@ from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase +from pyomo.solver.base import PersistentSolverBase from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader from pyomo.solver.util import PersistentSolverUtils diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index a29dc2a597f..b5b2cc3b694 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -20,8 +20,9 @@ from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression from pyomo.common.dependencies import numpy as np from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase +from pyomo.solver.base import PersistentSolverBase from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader from pyomo.solver.util import PersistentSolverUtils diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 0249d97258f..b16ca4dc792 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -26,8 +26,9 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase +from pyomo.solver.base import PersistentSolverBase from pyomo.solver.config import InterfaceConfig +from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 352f93b7ad1..5ef6dd7ba50 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -4,7 +4,8 @@ parameterized, param_available = attempt_import('parameterized') parameterized = parameterized.parameterized -from pyomo.solver.base import TerminationCondition, Results, PersistentSolverBase +from pyomo.solver.base import PersistentSolverBase +from pyomo.solver.results import TerminationCondition, Results from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Highs from typing import Type diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index fa52883f3e2..2d5bde41329 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -11,183 +11,25 @@ import abc import enum -from datetime import datetime from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.common.config import ( - ConfigDict, - ConfigValue, - NonNegativeInt, - In, - NonNegativeFloat, -) from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError from pyomo.opt.base import SolverFactory as LegacySolverFactory from pyomo.common.factory import Factory import os from pyomo.opt.results.results_ import SolverResults as LegacySolverResults -from pyomo.opt.results.solution import ( - Solution as LegacySolution, - SolutionStatus as LegacySolutionStatus, -) -from pyomo.opt.results.solver import ( - TerminationCondition as LegacyTerminationCondition, - SolverStatus as LegacySolverStatus, -) +from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize from pyomo.core.base import SymbolMap from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.config import UpdateConfig -from pyomo.solver.solution import SolutionLoader from pyomo.solver.util import get_objective - - -class TerminationCondition(enum.Enum): - """ - An enumeration for checking the termination condition of solvers - """ - - """unknown serves as both a default value, and it is used when no other enum member makes sense""" - unknown = 42 - - """The solver exited because the convergence criteria were satisfied""" - convergenceCriteriaSatisfied = 0 - - """The solver exited due to a time limit""" - maxTimeLimit = 1 - - """The solver exited due to an iteration limit""" - iterationLimit = 2 - - """The solver exited due to an objective limit""" - objectiveLimit = 3 - - """The solver exited due to a minimum step length""" - minStepLength = 4 - - """The solver exited because the problem is unbounded""" - unbounded = 5 - - """The solver exited because the problem is proven infeasible""" - provenInfeasible = 6 - - """The solver exited because the problem was found to be locally infeasible""" - locallyInfeasible = 7 - - """The solver exited because the problem is either infeasible or unbounded""" - infeasibleOrUnbounded = 8 - - """The solver exited due to an error""" - error = 9 - - """The solver exited because it was interrupted""" - interrupted = 10 - - """The solver exited due to licensing problems""" - licensingProblems = 11 - - -class SolutionStatus(enum.IntEnum): - """ - An enumeration for interpreting the result of a termination. This describes the designated - status by the solver to be loaded back into the model. - - For now, we are choosing to use IntEnum such that return values are numerically - assigned in increasing order. - """ - - """No (single) solution found; possible that a population of solutions was returned""" - noSolution = 0 - - """Solution point does not satisfy some domains and/or constraints""" - infeasible = 10 - - """Feasible solution identified""" - feasible = 20 - - """Optimal solution identified""" - optimal = 30 - - -class Results(ConfigDict): - """ - Attributes - ---------- - termination_condition: TerminationCondition - The reason the solver exited. This is a member of the - TerminationCondition enum. - incumbent_objective: float - If a feasible solution was found, this is the objective value of - the best solution found. If no feasible solution was found, this is - None. - objective_bound: float - The best objective bound found. For minimization problems, this is - the lower bound. For maximization problems, this is the upper bound. - For solvers that do not provide an objective bound, this should be -inf - (minimization) or inf (maximization) - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.declare( - 'solution_loader', - ConfigValue(default=SolutionLoader(None, None, None, None)), - ) - self.declare( - 'termination_condition', - ConfigValue( - domain=In(TerminationCondition), default=TerminationCondition.unknown - ), - ) - self.declare( - 'solution_status', - ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution), - ) - self.incumbent_objective: Optional[float] = self.declare( - 'incumbent_objective', ConfigValue(domain=float) - ) - self.objective_bound: Optional[float] = self.declare( - 'objective_bound', ConfigValue(domain=float) - ) - self.declare('solver_name', ConfigValue(domain=str)) - self.declare('solver_version', ConfigValue(domain=tuple)) - self.declare('termination_message', ConfigValue(domain=str)) - self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) - self.declare('timing_info', ConfigDict()) - # TODO: Set up type checking for start_time - self.timing_info.declare('start_time', ConfigValue()) - self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) - self.timing_info.declare( - 'solver_wall_time', ConfigValue(domain=NonNegativeFloat) - ) - self.declare('extra_info', ConfigDict(implicit=True)) - - def __str__(self): - s = '' - s += 'termination_condition: ' + str(self.termination_condition) + '\n' - s += 'solution_status: ' + str(self.solution_status) + '\n' - s += 'incumbent_objective: ' + str(self.incumbent_objective) + '\n' - s += 'objective_bound: ' + str(self.objective_bound) - return s +from pyomo.solver.results import Results, legacy_solver_status_map, legacy_termination_condition_map, legacy_solution_status_map class SolverBase(abc.ABC): @@ -437,56 +279,7 @@ def update_params(self): pass -# Everything below here preserves backwards compatibility - -legacy_termination_condition_map = { - TerminationCondition.unknown: LegacyTerminationCondition.unknown, - TerminationCondition.maxTimeLimit: LegacyTerminationCondition.maxTimeLimit, - TerminationCondition.iterationLimit: LegacyTerminationCondition.maxIterations, - TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, - TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, - TerminationCondition.convergenceCriteriaSatisfied: LegacyTerminationCondition.optimal, - TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, - TerminationCondition.provenInfeasible: LegacyTerminationCondition.infeasible, - TerminationCondition.locallyInfeasible: LegacyTerminationCondition.infeasible, - TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, - TerminationCondition.error: LegacyTerminationCondition.error, - TerminationCondition.interrupted: LegacyTerminationCondition.resourceInterrupt, - TerminationCondition.licensingProblems: LegacyTerminationCondition.licensingProblems, -} - - -legacy_solver_status_map = { - TerminationCondition.unknown: LegacySolverStatus.unknown, - TerminationCondition.maxTimeLimit: LegacySolverStatus.aborted, - TerminationCondition.iterationLimit: LegacySolverStatus.aborted, - TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, - TerminationCondition.minStepLength: LegacySolverStatus.error, - TerminationCondition.convergenceCriteriaSatisfied: LegacySolverStatus.ok, - TerminationCondition.unbounded: LegacySolverStatus.error, - TerminationCondition.provenInfeasible: LegacySolverStatus.error, - TerminationCondition.locallyInfeasible: LegacySolverStatus.error, - TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, - TerminationCondition.error: LegacySolverStatus.error, - TerminationCondition.interrupted: LegacySolverStatus.aborted, - TerminationCondition.licensingProblems: LegacySolverStatus.error, -} - - -legacy_solution_status_map = { - SolutionStatus.noSolution: LegacySolutionStatus.unknown, - SolutionStatus.noSolution: LegacySolutionStatus.stoppedByLimit, - SolutionStatus.noSolution: LegacySolutionStatus.error, - SolutionStatus.noSolution: LegacySolutionStatus.other, - SolutionStatus.noSolution: LegacySolutionStatus.unsure, - SolutionStatus.noSolution: LegacySolutionStatus.unbounded, - SolutionStatus.optimal: LegacySolutionStatus.locallyOptimal, - SolutionStatus.optimal: LegacySolutionStatus.globallyOptimal, - SolutionStatus.optimal: LegacySolutionStatus.optimal, - SolutionStatus.infeasible: LegacySolutionStatus.infeasible, - SolutionStatus.feasible: LegacySolutionStatus.feasible, - SolutionStatus.feasible: LegacySolutionStatus.bestSoFar, -} + class LegacySolverInterface: diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py new file mode 100644 index 00000000000..0b6fdcafbc4 --- /dev/null +++ b/pyomo/solver/results.py @@ -0,0 +1,225 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import enum +from typing import Optional +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + NonNegativeInt, + In, + NonNegativeFloat, +) +from pyomo.solver.solution import SolutionLoader +from pyomo.opt.results.solution import ( + SolutionStatus as LegacySolutionStatus, +) +from pyomo.opt.results.solver import ( + TerminationCondition as LegacyTerminationCondition, + SolverStatus as LegacySolverStatus, +) + + +class TerminationCondition(enum.Enum): + """ + An enumeration for checking the termination condition of solvers + """ + + """unknown serves as both a default value, and it is used when no other enum member makes sense""" + unknown = 42 + + """The solver exited because the convergence criteria were satisfied""" + convergenceCriteriaSatisfied = 0 + + """The solver exited due to a time limit""" + maxTimeLimit = 1 + + """The solver exited due to an iteration limit""" + iterationLimit = 2 + + """The solver exited due to an objective limit""" + objectiveLimit = 3 + + """The solver exited due to a minimum step length""" + minStepLength = 4 + + """The solver exited because the problem is unbounded""" + unbounded = 5 + + """The solver exited because the problem is proven infeasible""" + provenInfeasible = 6 + + """The solver exited because the problem was found to be locally infeasible""" + locallyInfeasible = 7 + + """The solver exited because the problem is either infeasible or unbounded""" + infeasibleOrUnbounded = 8 + + """The solver exited due to an error""" + error = 9 + + """The solver exited because it was interrupted""" + interrupted = 10 + + """The solver exited due to licensing problems""" + licensingProblems = 11 + + +class SolutionStatus(enum.IntEnum): + """ + An enumeration for interpreting the result of a termination. This describes the designated + status by the solver to be loaded back into the model. + + For now, we are choosing to use IntEnum such that return values are numerically + assigned in increasing order. + """ + + """No (single) solution found; possible that a population of solutions was returned""" + noSolution = 0 + + """Solution point does not satisfy some domains and/or constraints""" + infeasible = 10 + + """Feasible solution identified""" + feasible = 20 + + """Optimal solution identified""" + optimal = 30 + + +class Results(ConfigDict): + """ + Attributes + ---------- + termination_condition: TerminationCondition + The reason the solver exited. This is a member of the + TerminationCondition enum. + incumbent_objective: float + If a feasible solution was found, this is the objective value of + the best solution found. If no feasible solution was found, this is + None. + objective_bound: float + The best objective bound found. For minimization problems, this is + the lower bound. For maximization problems, this is the upper bound. + For solvers that do not provide an objective bound, this should be -inf + (minimization) or inf (maximization) + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare( + 'solution_loader', + ConfigValue(default=SolutionLoader(None, None, None, None)), + ) + self.declare( + 'termination_condition', + ConfigValue( + domain=In(TerminationCondition), default=TerminationCondition.unknown + ), + ) + self.declare( + 'solution_status', + ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution), + ) + self.incumbent_objective: Optional[float] = self.declare( + 'incumbent_objective', ConfigValue(domain=float) + ) + self.objective_bound: Optional[float] = self.declare( + 'objective_bound', ConfigValue(domain=float) + ) + self.declare('solver_name', ConfigValue(domain=str)) + self.declare('solver_version', ConfigValue(domain=tuple)) + self.declare('termination_message', ConfigValue(domain=str)) + self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) + self.declare('timing_info', ConfigDict()) + # TODO: Set up type checking for start_time + self.timing_info.declare('start_time', ConfigValue()) + self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) + self.timing_info.declare( + 'solver_wall_time', ConfigValue(domain=NonNegativeFloat) + ) + self.declare('extra_info', ConfigDict(implicit=True)) + + def __str__(self): + s = '' + s += 'termination_condition: ' + str(self.termination_condition) + '\n' + s += 'solution_status: ' + str(self.solution_status) + '\n' + s += 'incumbent_objective: ' + str(self.incumbent_objective) + '\n' + s += 'objective_bound: ' + str(self.objective_bound) + return s + + +# Everything below here preserves backwards compatibility + +legacy_termination_condition_map = { + TerminationCondition.unknown: LegacyTerminationCondition.unknown, + TerminationCondition.maxTimeLimit: LegacyTerminationCondition.maxTimeLimit, + TerminationCondition.iterationLimit: LegacyTerminationCondition.maxIterations, + TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, + TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, + TerminationCondition.convergenceCriteriaSatisfied: LegacyTerminationCondition.optimal, + TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, + TerminationCondition.provenInfeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.locallyInfeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, + TerminationCondition.error: LegacyTerminationCondition.error, + TerminationCondition.interrupted: LegacyTerminationCondition.resourceInterrupt, + TerminationCondition.licensingProblems: LegacyTerminationCondition.licensingProblems, +} + + +legacy_solver_status_map = { + TerminationCondition.unknown: LegacySolverStatus.unknown, + TerminationCondition.maxTimeLimit: LegacySolverStatus.aborted, + TerminationCondition.iterationLimit: LegacySolverStatus.aborted, + TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, + TerminationCondition.minStepLength: LegacySolverStatus.error, + TerminationCondition.convergenceCriteriaSatisfied: LegacySolverStatus.ok, + TerminationCondition.unbounded: LegacySolverStatus.error, + TerminationCondition.provenInfeasible: LegacySolverStatus.error, + TerminationCondition.locallyInfeasible: LegacySolverStatus.error, + TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, + TerminationCondition.error: LegacySolverStatus.error, + TerminationCondition.interrupted: LegacySolverStatus.aborted, + TerminationCondition.licensingProblems: LegacySolverStatus.error, +} + + +legacy_solution_status_map = { + SolutionStatus.noSolution: LegacySolutionStatus.unknown, + SolutionStatus.noSolution: LegacySolutionStatus.stoppedByLimit, + SolutionStatus.noSolution: LegacySolutionStatus.error, + SolutionStatus.noSolution: LegacySolutionStatus.other, + SolutionStatus.noSolution: LegacySolutionStatus.unsure, + SolutionStatus.noSolution: LegacySolutionStatus.unbounded, + SolutionStatus.optimal: LegacySolutionStatus.locallyOptimal, + SolutionStatus.optimal: LegacySolutionStatus.globallyOptimal, + SolutionStatus.optimal: LegacySolutionStatus.optimal, + SolutionStatus.infeasible: LegacySolutionStatus.infeasible, + SolutionStatus.feasible: LegacySolutionStatus.feasible, + SolutionStatus.feasible: LegacySolutionStatus.bestSoFar, +} + + diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/test_base.py index 0e0780fd6fe..d8084e9b5b7 100644 --- a/pyomo/solver/tests/test_base.py +++ b/pyomo/solver/tests/test_base.py @@ -10,61 +10,7 @@ # ___________________________________________________________________________ from pyomo.common import unittest -from pyomo.common.config import ConfigDict from pyomo.solver import base -import pyomo.environ as pe -from pyomo.core.base.var import ScalarVar - - -class TestTerminationCondition(unittest.TestCase): - def test_member_list(self): - member_list = base.TerminationCondition._member_names_ - expected_list = [ - 'unknown', - 'convergenceCriteriaSatisfied', - 'maxTimeLimit', - 'iterationLimit', - 'objectiveLimit', - 'minStepLength', - 'unbounded', - 'provenInfeasible', - 'locallyInfeasible', - 'infeasibleOrUnbounded', - 'error', - 'interrupted', - 'licensingProblems', - ] - self.assertEqual(member_list, expected_list) - - def test_codes(self): - self.assertEqual(base.TerminationCondition.unknown.value, 42) - self.assertEqual( - base.TerminationCondition.convergenceCriteriaSatisfied.value, 0 - ) - self.assertEqual(base.TerminationCondition.maxTimeLimit.value, 1) - self.assertEqual(base.TerminationCondition.iterationLimit.value, 2) - self.assertEqual(base.TerminationCondition.objectiveLimit.value, 3) - self.assertEqual(base.TerminationCondition.minStepLength.value, 4) - self.assertEqual(base.TerminationCondition.unbounded.value, 5) - self.assertEqual(base.TerminationCondition.provenInfeasible.value, 6) - self.assertEqual(base.TerminationCondition.locallyInfeasible.value, 7) - self.assertEqual(base.TerminationCondition.infeasibleOrUnbounded.value, 8) - self.assertEqual(base.TerminationCondition.error.value, 9) - self.assertEqual(base.TerminationCondition.interrupted.value, 10) - self.assertEqual(base.TerminationCondition.licensingProblems.value, 11) - - -class TestSolutionStatus(unittest.TestCase): - def test_member_list(self): - member_list = base.SolutionStatus._member_names_ - expected_list = ['noSolution', 'infeasible', 'feasible', 'optimal'] - self.assertEqual(member_list, expected_list) - - def test_codes(self): - self.assertEqual(base.SolutionStatus.noSolution.value, 0) - self.assertEqual(base.SolutionStatus.infeasible.value, 10) - self.assertEqual(base.SolutionStatus.feasible.value, 20) - self.assertEqual(base.SolutionStatus.optimal.value, 30) class TestSolverBase(unittest.TestCase): @@ -140,116 +86,3 @@ def test_persistent_solver_base(self): with self.assertRaises(NotImplementedError): self.instance.get_reduced_costs() - - -class TestResults(unittest.TestCase): - def test_declared_items(self): - res = base.Results() - expected_declared = { - 'extra_info', - 'incumbent_objective', - 'iteration_count', - 'objective_bound', - 'solution_loader', - 'solution_status', - 'solver_name', - 'solver_version', - 'termination_condition', - 'termination_message', - 'timing_info', - } - actual_declared = res._declared - self.assertEqual(expected_declared, actual_declared) - - def test_uninitialized(self): - res = base.Results() - self.assertIsNone(res.incumbent_objective) - self.assertIsNone(res.objective_bound) - self.assertEqual(res.termination_condition, base.TerminationCondition.unknown) - self.assertEqual(res.solution_status, base.SolutionStatus.noSolution) - self.assertIsNone(res.solver_name) - self.assertIsNone(res.solver_version) - self.assertIsNone(res.termination_message) - self.assertIsNone(res.iteration_count) - self.assertIsInstance(res.timing_info, ConfigDict) - self.assertIsInstance(res.extra_info, ConfigDict) - self.assertIsNone(res.timing_info.start_time) - self.assertIsNone(res.timing_info.wall_time) - self.assertIsNone(res.timing_info.solver_wall_time) - - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have a valid solution.*' - ): - res.solution_loader.load_vars() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid duals.*' - ): - res.solution_loader.get_duals() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid reduced costs.*' - ): - res.solution_loader.get_reduced_costs() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid slacks.*' - ): - res.solution_loader.get_slacks() - - def test_results(self): - m = pe.ConcreteModel() - m.x = ScalarVar() - m.y = ScalarVar() - m.c1 = pe.Constraint(expr=m.x == 1) - m.c2 = pe.Constraint(expr=m.y == 2) - - primals = {} - primals[id(m.x)] = (m.x, 1) - primals[id(m.y)] = (m.y, 2) - duals = {} - duals[m.c1] = 3 - duals[m.c2] = 4 - rc = {} - rc[id(m.x)] = (m.x, 5) - rc[id(m.y)] = (m.y, 6) - slacks = {} - slacks[m.c1] = 7 - slacks[m.c2] = 8 - - res = base.Results() - res.solution_loader = base.SolutionLoader( - primals=primals, duals=duals, slacks=slacks, reduced_costs=rc - ) - - res.solution_loader.load_vars() - self.assertAlmostEqual(m.x.value, 1) - self.assertAlmostEqual(m.y.value, 2) - - m.x.value = None - m.y.value = None - - res.solution_loader.load_vars([m.y]) - self.assertIsNone(m.x.value) - self.assertAlmostEqual(m.y.value, 2) - - duals2 = res.solution_loader.get_duals() - self.assertAlmostEqual(duals[m.c1], duals2[m.c1]) - self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) - - duals2 = res.solution_loader.get_duals([m.c2]) - self.assertNotIn(m.c1, duals2) - self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) - - rc2 = res.solution_loader.get_reduced_costs() - self.assertAlmostEqual(rc[id(m.x)][1], rc2[m.x]) - self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) - - rc2 = res.solution_loader.get_reduced_costs([m.y]) - self.assertNotIn(m.x, rc2) - self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) - - slacks2 = res.solution_loader.get_slacks() - self.assertAlmostEqual(slacks[m.c1], slacks2[m.c1]) - self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) - - slacks2 = res.solution_loader.get_slacks([m.c2]) - self.assertNotIn(m.c1, slacks2) - self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/test_results.py new file mode 100644 index 00000000000..74c0f9f2256 --- /dev/null +++ b/pyomo/solver/tests/test_results.py @@ -0,0 +1,180 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.common.config import ConfigDict +from pyomo.solver import results +import pyomo.environ as pyo +from pyomo.core.base.var import ScalarVar + + +class TestTerminationCondition(unittest.TestCase): + def test_member_list(self): + member_list = results.TerminationCondition._member_names_ + expected_list = [ + 'unknown', + 'convergenceCriteriaSatisfied', + 'maxTimeLimit', + 'iterationLimit', + 'objectiveLimit', + 'minStepLength', + 'unbounded', + 'provenInfeasible', + 'locallyInfeasible', + 'infeasibleOrUnbounded', + 'error', + 'interrupted', + 'licensingProblems', + ] + self.assertEqual(member_list, expected_list) + + def test_codes(self): + self.assertEqual(results.TerminationCondition.unknown.value, 42) + self.assertEqual( + results.TerminationCondition.convergenceCriteriaSatisfied.value, 0 + ) + self.assertEqual(results.TerminationCondition.maxTimeLimit.value, 1) + self.assertEqual(results.TerminationCondition.iterationLimit.value, 2) + self.assertEqual(results.TerminationCondition.objectiveLimit.value, 3) + self.assertEqual(results.TerminationCondition.minStepLength.value, 4) + self.assertEqual(results.TerminationCondition.unbounded.value, 5) + self.assertEqual(results.TerminationCondition.provenInfeasible.value, 6) + self.assertEqual(results.TerminationCondition.locallyInfeasible.value, 7) + self.assertEqual(results.TerminationCondition.infeasibleOrUnbounded.value, 8) + self.assertEqual(results.TerminationCondition.error.value, 9) + self.assertEqual(results.TerminationCondition.interrupted.value, 10) + self.assertEqual(results.TerminationCondition.licensingProblems.value, 11) + + +class TestSolutionStatus(unittest.TestCase): + def test_member_list(self): + member_list = results.SolutionStatus._member_names_ + expected_list = ['noSolution', 'infeasible', 'feasible', 'optimal'] + self.assertEqual(member_list, expected_list) + + def test_codes(self): + self.assertEqual(results.SolutionStatus.noSolution.value, 0) + self.assertEqual(results.SolutionStatus.infeasible.value, 10) + self.assertEqual(results.SolutionStatus.feasible.value, 20) + self.assertEqual(results.SolutionStatus.optimal.value, 30) + + +class TestResults(unittest.TestCase): + def test_declared_items(self): + res = results.Results() + expected_declared = { + 'extra_info', + 'incumbent_objective', + 'iteration_count', + 'objective_bound', + 'solution_loader', + 'solution_status', + 'solver_name', + 'solver_version', + 'termination_condition', + 'termination_message', + 'timing_info', + } + actual_declared = res._declared + self.assertEqual(expected_declared, actual_declared) + + def test_uninitialized(self): + res = results.Results() + self.assertIsNone(res.incumbent_objective) + self.assertIsNone(res.objective_bound) + self.assertEqual(res.termination_condition, results.TerminationCondition.unknown) + self.assertEqual(res.solution_status, results.SolutionStatus.noSolution) + self.assertIsNone(res.solver_name) + self.assertIsNone(res.solver_version) + self.assertIsNone(res.termination_message) + self.assertIsNone(res.iteration_count) + self.assertIsInstance(res.timing_info, ConfigDict) + self.assertIsInstance(res.extra_info, ConfigDict) + self.assertIsNone(res.timing_info.start_time) + self.assertIsNone(res.timing_info.wall_time) + self.assertIsNone(res.timing_info.solver_wall_time) + + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have a valid solution.*' + ): + res.solution_loader.load_vars() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid duals.*' + ): + res.solution_loader.get_duals() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid reduced costs.*' + ): + res.solution_loader.get_reduced_costs() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid slacks.*' + ): + res.solution_loader.get_slacks() + + def test_results(self): + m = pyo.ConcreteModel() + m.x = ScalarVar() + m.y = ScalarVar() + m.c1 = pyo.Constraint(expr=m.x == 1) + m.c2 = pyo.Constraint(expr=m.y == 2) + + primals = {} + primals[id(m.x)] = (m.x, 1) + primals[id(m.y)] = (m.y, 2) + duals = {} + duals[m.c1] = 3 + duals[m.c2] = 4 + rc = {} + rc[id(m.x)] = (m.x, 5) + rc[id(m.y)] = (m.y, 6) + slacks = {} + slacks[m.c1] = 7 + slacks[m.c2] = 8 + + res = results.Results() + res.solution_loader = results.SolutionLoader( + primals=primals, duals=duals, slacks=slacks, reduced_costs=rc + ) + + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 2) + + m.x.value = None + m.y.value = None + + res.solution_loader.load_vars([m.y]) + self.assertIsNone(m.x.value) + self.assertAlmostEqual(m.y.value, 2) + + duals2 = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], duals2[m.c1]) + self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) + + duals2 = res.solution_loader.get_duals([m.c2]) + self.assertNotIn(m.c1, duals2) + self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) + + rc2 = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[id(m.x)][1], rc2[m.x]) + self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) + + rc2 = res.solution_loader.get_reduced_costs([m.y]) + self.assertNotIn(m.x, rc2) + self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) + + slacks2 = res.solution_loader.get_slacks() + self.assertAlmostEqual(slacks[m.c1], slacks2[m.c1]) + self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) + + slacks2 = res.solution_loader.get_slacks([m.c2]) + self.assertNotIn(m.c1, slacks2) + self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) From f0a942cba9e33b1272e3f8a5296ab4a439ce2d0a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 09:40:38 -0600 Subject: [PATCH 0073/3044] Update key value for solution status --- pyomo/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 2d5bde41329..ea49f7e26e4 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -331,7 +331,7 @@ def solve( legacy_results.solver.termination_condition = legacy_termination_condition_map[ results.termination_condition ] - legacy_soln.status = legacy_solution_status_map[results.termination_condition] + legacy_soln.status = legacy_solution_status_map[results.solution_status] legacy_results.solver.termination_message = str(results.termination_condition) obj = get_objective(model) From 51b97f5bc837689f5eafc8db2ca74bfeabcca556 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 09:45:54 -0600 Subject: [PATCH 0074/3044] Fix broken import statement --- pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index 7e1d3e37af6..c1825879dbe 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -1,7 +1,7 @@ from pyomo.common import unittest import pyomo.environ as pe from pyomo.contrib.appsi.solvers.gurobi import Gurobi -from pyomo.solver.base import TerminationCondition +from pyomo.solver.results import TerminationCondition from pyomo.core.expr.taylor_series import taylor_series_expansion From 678df6fc48984ac81d399b8990320f8a00a40381 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 09:51:24 -0600 Subject: [PATCH 0075/3044] Correct one more broken import statement; apply black --- pyomo/contrib/appsi/examples/getting_started.py | 4 ++-- pyomo/solver/base.py | 10 ++++++---- pyomo/solver/results.py | 6 +----- pyomo/solver/tests/test_results.py | 4 +++- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index 79f1aa845b3..52f4992b37b 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -1,7 +1,7 @@ import pyomo.environ as pe from pyomo.contrib import appsi from pyomo.common.timing import HierarchicalTimer -from pyomo.solver import base as solver_base +from pyomo.solver import results def main(plot=True, n_points=200): @@ -34,7 +34,7 @@ def main(plot=True, n_points=200): res = opt.solve(m, timer=timer) assert ( res.termination_condition - == solver_base.TerminationCondition.convergenceCriteriaSatisfied + == results.TerminationCondition.convergenceCriteriaSatisfied ) obj_values.append(res.incumbent_objective) opt.load_vars([m.x]) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index ea49f7e26e4..9a7e19d7c85 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -29,7 +29,12 @@ from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.config import UpdateConfig from pyomo.solver.util import get_objective -from pyomo.solver.results import Results, legacy_solver_status_map, legacy_termination_condition_map, legacy_solution_status_map +from pyomo.solver.results import ( + Results, + legacy_solver_status_map, + legacy_termination_condition_map, + legacy_solution_status_map, +) class SolverBase(abc.ABC): @@ -279,9 +284,6 @@ def update_params(self): pass - - - class LegacySolverInterface: def solve( self, diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 0b6fdcafbc4..d51efa38168 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -19,9 +19,7 @@ NonNegativeFloat, ) from pyomo.solver.solution import SolutionLoader -from pyomo.opt.results.solution import ( - SolutionStatus as LegacySolutionStatus, -) +from pyomo.opt.results.solution import SolutionStatus as LegacySolutionStatus from pyomo.opt.results.solver import ( TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus, @@ -221,5 +219,3 @@ def __str__(self): SolutionStatus.feasible: LegacySolutionStatus.feasible, SolutionStatus.feasible: LegacySolutionStatus.bestSoFar, } - - diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/test_results.py index 74c0f9f2256..7dea76c856f 100644 --- a/pyomo/solver/tests/test_results.py +++ b/pyomo/solver/tests/test_results.py @@ -90,7 +90,9 @@ def test_uninitialized(self): res = results.Results() self.assertIsNone(res.incumbent_objective) self.assertIsNone(res.objective_bound) - self.assertEqual(res.termination_condition, results.TerminationCondition.unknown) + self.assertEqual( + res.termination_condition, results.TerminationCondition.unknown + ) self.assertEqual(res.solution_status, results.SolutionStatus.noSolution) self.assertIsNone(res.solver_name) self.assertIsNone(res.solver_version) From f0d843cb62f9a5b1e5501c3b3db8a570505a4b8d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 10:10:50 -0600 Subject: [PATCH 0076/3044] Reorder imports for prettiness --- pyomo/solver/base.py | 3 ++- pyomo/solver/config.py | 1 + pyomo/solver/results.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 9a7e19d7c85..2bd6245feda 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -12,6 +12,8 @@ import abc import enum from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple +import os + from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData @@ -21,7 +23,6 @@ from pyomo.common.errors import ApplicationError from pyomo.opt.base import SolverFactory as LegacySolverFactory from pyomo.common.factory import Factory -import os from pyomo.opt.results.results_ import SolverResults as LegacySolverResults from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index 32f6e1d5da0..d80e78eb2f2 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -10,6 +10,7 @@ # ___________________________________________________________________________ from typing import Optional + from pyomo.common.config import ( ConfigDict, ConfigValue, diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index d51efa38168..17e9416862e 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -11,6 +11,7 @@ import enum from typing import Optional + from pyomo.common.config import ( ConfigDict, ConfigValue, From 46d3c9095241f505540ec8e45069344021533e11 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 10:15:04 -0600 Subject: [PATCH 0077/3044] Copyright added; init updated --- pyomo/solver/__init__.py | 1 + pyomo/solver/config.py | 2 +- pyomo/solver/plugins.py | 13 ++++++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/__init__.py b/pyomo/solver/__init__.py index a3c2e0e95e8..e3eafa991cc 100644 --- a/pyomo/solver/__init__.py +++ b/pyomo/solver/__init__.py @@ -11,5 +11,6 @@ from . import base from . import config +from . import results from . import solution from . import util diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index d80e78eb2f2..22399caa35e 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -61,7 +61,7 @@ def __init__( self.declare('load_solution', ConfigValue(domain=bool, default=True)) self.declare('symbolic_solver_labels', ConfigValue(domain=bool, default=False)) self.declare('report_timing', ConfigValue(domain=bool, default=False)) - self.declare('threads', ConfigValue(domain=NonNegativeInt, default=None)) + self.declare('threads', ConfigValue(domain=NonNegativeInt)) self.time_limit: Optional[float] = self.declare( 'time_limit', ConfigValue(domain=NonNegativeFloat) diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index 7e479474605..229488742cd 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -1,5 +1,16 @@ -from .base import SolverFactory +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from .base import SolverFactory def load(): pass From a619aca4ef54401d3e0658006fa569f35e6784e2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 11:07:12 -0600 Subject: [PATCH 0078/3044] Change several names; add type checking --- pyomo/contrib/appsi/solvers/cbc.py | 4 +-- pyomo/contrib/appsi/solvers/cplex.py | 8 +++--- pyomo/contrib/appsi/solvers/gurobi.py | 8 +++--- pyomo/contrib/appsi/solvers/highs.py | 8 +++--- pyomo/contrib/appsi/solvers/ipopt.py | 4 +-- pyomo/solver/base.py | 2 +- pyomo/solver/config.py | 40 ++++++++++++++++----------- pyomo/solver/plugins.py | 1 + pyomo/solver/results.py | 40 +++++++++++++++------------ pyomo/solver/solution.py | 2 ++ pyomo/solver/tests/test_config.py | 18 ++++++------ 11 files changed, 76 insertions(+), 59 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index c2686475b15..62404890d0b 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -23,7 +23,7 @@ from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import InterfaceConfig +from pyomo.solver.config import SolverConfig from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader @@ -31,7 +31,7 @@ logger = logging.getLogger(__name__) -class CbcConfig(InterfaceConfig): +class CbcConfig(SolverConfig): def __init__( self, description=None, diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 86d50f1b82a..1837b5690a0 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -20,7 +20,7 @@ from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.config import BranchAndBoundConfig from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader @@ -28,7 +28,7 @@ logger = logging.getLogger(__name__) -class CplexConfig(MIPInterfaceConfig): +class CplexConfig(BranchAndBoundConfig): def __init__( self, description=None, @@ -263,8 +263,8 @@ def _process_stream(arg): if config.time_limit is not None: cplex_model.parameters.timelimit.set(config.time_limit) - if config.mip_gap is not None: - cplex_model.parameters.mip.tolerances.mipgap.set(config.mip_gap) + if config.rel_gap is not None: + cplex_model.parameters.mip.tolerances.mipgap.set(config.rel_gap) timer.start('cplex solve') t0 = time.time() diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 1f295dfcb49..99fa19820a5 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -23,7 +23,7 @@ from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.config import BranchAndBoundConfig from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader from pyomo.solver.util import PersistentSolverUtils @@ -51,7 +51,7 @@ class DegreeError(PyomoException): pass -class GurobiConfig(MIPInterfaceConfig): +class GurobiConfig(BranchAndBoundConfig): def __init__( self, description=None, @@ -364,8 +364,8 @@ def _solve(self, timer: HierarchicalTimer): if config.time_limit is not None: self._solver_model.setParam('TimeLimit', config.time_limit) - if config.mip_gap is not None: - self._solver_model.setParam('MIPGap', config.mip_gap) + if config.rel_gap is not None: + self._solver_model.setParam('MIPGap', config.rel_gap) for key, option in options.items(): self._solver_model.setParam(key, option) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index b5b2cc3b694..f62304563fc 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -21,7 +21,7 @@ from pyomo.common.dependencies import numpy as np from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import MIPInterfaceConfig +from pyomo.solver.config import BranchAndBoundConfig from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader from pyomo.solver.util import PersistentSolverUtils @@ -35,7 +35,7 @@ class DegreeError(PyomoException): pass -class HighsConfig(MIPInterfaceConfig): +class HighsConfig(BranchAndBoundConfig): def __init__( self, description=None, @@ -219,8 +219,8 @@ def _solve(self, timer: HierarchicalTimer): if config.time_limit is not None: self._solver_model.setOptionValue('time_limit', config.time_limit) - if config.mip_gap is not None: - self._solver_model.setOptionValue('mip_rel_gap', config.mip_gap) + if config.rel_gap is not None: + self._solver_model.setOptionValue('mip_rel_gap', config.rel_gap) for key, option in options.items(): self._solver_model.setOptionValue(key, option) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index b16ca4dc792..569bb98457f 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -27,7 +27,7 @@ from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import InterfaceConfig +from pyomo.solver.config import SolverConfig from pyomo.solver.results import TerminationCondition, Results from pyomo.solver.solution import PersistentSolutionLoader @@ -35,7 +35,7 @@ logger = logging.getLogger(__name__) -class IpoptConfig(InterfaceConfig): +class IpoptConfig(SolverConfig): def __init__( self, description=None, diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 2bd6245feda..ba25b23a354 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -130,7 +130,7 @@ def config(self): Returns ------- - InterfaceConfig + SolverConfig An object for configuring pyomo solve options such as the time limit. These options are mostly independent of the solver. """ diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index 22399caa35e..2f6f9b5bcc8 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from typing import Optional - from pyomo.common.config import ( ConfigDict, ConfigValue, @@ -19,7 +17,7 @@ ) -class InterfaceConfig(ConfigDict): +class SolverConfig(ConfigDict): """ Attributes ---------- @@ -57,18 +55,24 @@ def __init__( visibility=visibility, ) - self.declare('tee', ConfigValue(domain=bool, default=False)) - self.declare('load_solution', ConfigValue(domain=bool, default=True)) - self.declare('symbolic_solver_labels', ConfigValue(domain=bool, default=False)) - self.declare('report_timing', ConfigValue(domain=bool, default=False)) - self.declare('threads', ConfigValue(domain=NonNegativeInt)) - - self.time_limit: Optional[float] = self.declare( + # TODO: Add in type-hinting everywhere + self.tee: bool = self.declare('tee', ConfigValue(domain=bool, default=False)) + self.load_solution: bool = self.declare( + 'load_solution', ConfigValue(domain=bool, default=True) + ) + self.symbolic_solver_labels: bool = self.declare( + 'symbolic_solver_labels', ConfigValue(domain=bool, default=False) + ) + self.report_timing: bool = self.declare( + 'report_timing', ConfigValue(domain=bool, default=False) + ) + self.threads = self.declare('threads', ConfigValue(domain=NonNegativeInt)) + self.time_limit: NonNegativeFloat = self.declare( 'time_limit', ConfigValue(domain=NonNegativeFloat) ) -class MIPInterfaceConfig(InterfaceConfig): +class BranchAndBoundConfig(SolverConfig): """ Attributes ---------- @@ -95,11 +99,15 @@ def __init__( visibility=visibility, ) - self.declare('mip_gap', ConfigValue(domain=NonNegativeFloat)) - self.declare('relax_integrality', ConfigValue(domain=bool)) - - self.mip_gap: Optional[float] = None - self.relax_integrality: bool = False + self.rel_gap: NonNegativeFloat = self.declare( + 'rel_gap', ConfigValue(domain=NonNegativeFloat) + ) + self.abs_gap: NonNegativeFloat = self.declare( + 'abs_gap', ConfigValue(domain=NonNegativeFloat) + ) + self.relax_integrality: bool = self.declare( + 'relax_integrality', ConfigValue(domain=bool, default=False) + ) class UpdateConfig(ConfigDict): diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index 229488742cd..5120bc9dd36 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -12,5 +12,6 @@ from .base import SolverFactory + def load(): pass diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 17e9416862e..fa0080c5a7b 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -19,7 +19,6 @@ In, NonNegativeFloat, ) -from pyomo.solver.solution import SolutionLoader from pyomo.opt.results.solution import SolutionStatus as LegacySolutionStatus from pyomo.opt.results.solver import ( TerminationCondition as LegacyTerminationCondition, @@ -128,17 +127,14 @@ def __init__( visibility=visibility, ) - self.declare( - 'solution_loader', - ConfigValue(default=SolutionLoader(None, None, None, None)), - ) - self.declare( + self.solution_loader = self.declare('solution_loader', ConfigValue()) + self.termination_condition: In(TerminationCondition) = self.declare( 'termination_condition', ConfigValue( domain=In(TerminationCondition), default=TerminationCondition.unknown ), ) - self.declare( + self.solution_status: In(SolutionStatus) = self.declare( 'solution_status', ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution), ) @@ -148,18 +144,28 @@ def __init__( self.objective_bound: Optional[float] = self.declare( 'objective_bound', ConfigValue(domain=float) ) - self.declare('solver_name', ConfigValue(domain=str)) - self.declare('solver_version', ConfigValue(domain=tuple)) - self.declare('termination_message', ConfigValue(domain=str)) - self.declare('iteration_count', ConfigValue(domain=NonNegativeInt)) - self.declare('timing_info', ConfigDict()) - # TODO: Set up type checking for start_time - self.timing_info.declare('start_time', ConfigValue()) - self.timing_info.declare('wall_time', ConfigValue(domain=NonNegativeFloat)) - self.timing_info.declare( + self.solver_name: Optional[str] = self.declare( + 'solver_name', ConfigValue(domain=str) + ) + self.solver_version: Optional[tuple] = self.declare( + 'solver_version', ConfigValue(domain=tuple) + ) + self.iteration_count: NonNegativeInt = self.declare( + 'iteration_count', ConfigValue(domain=NonNegativeInt) + ) + self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict()) + self.timing_info.start_time = self.timing_info.declare( + 'start_time', ConfigValue() + ) + self.timing_info.wall_time: NonNegativeFloat = self.timing_info.declare( + 'wall_time', ConfigValue(domain=NonNegativeFloat) + ) + self.timing_info.solver_wall_time: NonNegativeFloat = self.timing_info.declare( 'solver_wall_time', ConfigValue(domain=NonNegativeFloat) ) - self.declare('extra_info', ConfigDict(implicit=True)) + self.extra_info: ConfigDict = self.declare( + 'extra_info', ConfigDict(implicit=True) + ) def __str__(self): s = '' diff --git a/pyomo/solver/solution.py b/pyomo/solver/solution.py index 1ef79050701..6c4b7431746 100644 --- a/pyomo/solver/solution.py +++ b/pyomo/solver/solution.py @@ -117,6 +117,8 @@ def get_reduced_costs( ) +# TODO: This is for development uses only; not to be released to the wild +# May turn into documentation someday class SolutionLoader(SolutionLoaderBase): def __init__( self, diff --git a/pyomo/solver/tests/test_config.py b/pyomo/solver/tests/test_config.py index 49d26513e2e..0686a3249d1 100644 --- a/pyomo/solver/tests/test_config.py +++ b/pyomo/solver/tests/test_config.py @@ -10,12 +10,12 @@ # ___________________________________________________________________________ from pyomo.common import unittest -from pyomo.solver.config import InterfaceConfig, MIPInterfaceConfig +from pyomo.solver.config import SolverConfig, BranchAndBoundConfig -class TestInterfaceConfig(unittest.TestCase): +class TestSolverConfig(unittest.TestCase): def test_interface_default_instantiation(self): - config = InterfaceConfig() + config = SolverConfig() self.assertEqual(config._description, None) self.assertEqual(config._visibility, 0) self.assertFalse(config.tee) @@ -24,7 +24,7 @@ def test_interface_default_instantiation(self): self.assertFalse(config.report_timing) def test_interface_custom_instantiation(self): - config = InterfaceConfig(description="A description") + config = SolverConfig(description="A description") config.tee = True self.assertTrue(config.tee) self.assertEqual(config._description, "A description") @@ -33,9 +33,9 @@ def test_interface_custom_instantiation(self): self.assertEqual(config.time_limit, 1.0) -class TestMIPInterfaceConfig(unittest.TestCase): +class TestBranchAndBoundConfig(unittest.TestCase): def test_interface_default_instantiation(self): - config = MIPInterfaceConfig() + config = BranchAndBoundConfig() self.assertEqual(config._description, None) self.assertEqual(config._visibility, 0) self.assertFalse(config.tee) @@ -46,12 +46,12 @@ def test_interface_default_instantiation(self): self.assertFalse(config.relax_integrality) def test_interface_custom_instantiation(self): - config = MIPInterfaceConfig(description="A description") + config = BranchAndBoundConfig(description="A description") config.tee = True self.assertTrue(config.tee) self.assertEqual(config._description, "A description") self.assertFalse(config.time_limit) config.time_limit = 1.0 self.assertEqual(config.time_limit, 1.0) - config.mip_gap = 2.5 - self.assertEqual(config.mip_gap, 2.5) + config.rel_gap = 2.5 + self.assertEqual(config.rel_gap, 2.5) From 5eb95a4f819751cce631a4bc114ee0e7bf90a29a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 11:15:23 -0600 Subject: [PATCH 0079/3044] Fix problematic attribute changes --- pyomo/solver/tests/test_config.py | 3 ++- pyomo/solver/tests/test_results.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/solver/tests/test_config.py b/pyomo/solver/tests/test_config.py index 0686a3249d1..c705c7cb8ac 100644 --- a/pyomo/solver/tests/test_config.py +++ b/pyomo/solver/tests/test_config.py @@ -42,7 +42,8 @@ def test_interface_default_instantiation(self): self.assertTrue(config.load_solution) self.assertFalse(config.symbolic_solver_labels) self.assertFalse(config.report_timing) - self.assertEqual(config.mip_gap, None) + self.assertEqual(config.rel_gap, None) + self.assertEqual(config.abs_gap, None) self.assertFalse(config.relax_integrality) def test_interface_custom_instantiation(self): diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/test_results.py index 7dea76c856f..60b14d77521 100644 --- a/pyomo/solver/tests/test_results.py +++ b/pyomo/solver/tests/test_results.py @@ -12,6 +12,7 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict from pyomo.solver import results +from pyomo.solver import solution import pyomo.environ as pyo from pyomo.core.base.var import ScalarVar @@ -80,7 +81,6 @@ def test_declared_items(self): 'solver_name', 'solver_version', 'termination_condition', - 'termination_message', 'timing_info', } actual_declared = res._declared @@ -96,7 +96,6 @@ def test_uninitialized(self): self.assertEqual(res.solution_status, results.SolutionStatus.noSolution) self.assertIsNone(res.solver_name) self.assertIsNone(res.solver_version) - self.assertIsNone(res.termination_message) self.assertIsNone(res.iteration_count) self.assertIsInstance(res.timing_info, ConfigDict) self.assertIsInstance(res.extra_info, ConfigDict) @@ -142,7 +141,7 @@ def test_results(self): slacks[m.c2] = 8 res = results.Results() - res.solution_loader = results.SolutionLoader( + res.solution_loader = solution.SolutionLoader( primals=primals, duals=duals, slacks=slacks, reduced_costs=rc ) From 198d0b1fb8d37d791f4fd89270772d4e56387b4c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 11:25:07 -0600 Subject: [PATCH 0080/3044] Change type hinting for several config/results objects --- pyomo/solver/config.py | 44 +++++++++++++++++------------------------ pyomo/solver/results.py | 23 ++++++++++++--------- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index 2f6f9b5bcc8..efd2a1bac16 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -9,6 +9,8 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from typing import Optional + from pyomo.common.config import ( ConfigDict, ConfigValue, @@ -55,7 +57,6 @@ def __init__( visibility=visibility, ) - # TODO: Add in type-hinting everywhere self.tee: bool = self.declare('tee', ConfigValue(domain=bool, default=False)) self.load_solution: bool = self.declare( 'load_solution', ConfigValue(domain=bool, default=True) @@ -66,8 +67,10 @@ def __init__( self.report_timing: bool = self.declare( 'report_timing', ConfigValue(domain=bool, default=False) ) - self.threads = self.declare('threads', ConfigValue(domain=NonNegativeInt)) - self.time_limit: NonNegativeFloat = self.declare( + self.threads: Optional[int] = self.declare( + 'threads', ConfigValue(domain=NonNegativeInt) + ) + self.time_limit: Optional[float] = self.declare( 'time_limit', ConfigValue(domain=NonNegativeFloat) ) @@ -99,10 +102,10 @@ def __init__( visibility=visibility, ) - self.rel_gap: NonNegativeFloat = self.declare( + self.rel_gap: Optional[float] = self.declare( 'rel_gap', ConfigValue(domain=NonNegativeFloat) ) - self.abs_gap: NonNegativeFloat = self.declare( + self.abs_gap: Optional[float] = self.declare( 'abs_gap', ConfigValue(domain=NonNegativeFloat) ) self.relax_integrality: bool = self.declare( @@ -143,7 +146,7 @@ def __init__( visibility=visibility, ) - self.declare( + self.check_for_new_or_removed_constraints: bool = self.declare( 'check_for_new_or_removed_constraints', ConfigValue( domain=bool, @@ -155,7 +158,7 @@ def __init__( added to/removed from the model.""", ), ) - self.declare( + self.check_for_new_or_removed_vars: bool = self.declare( 'check_for_new_or_removed_vars', ConfigValue( domain=bool, @@ -167,7 +170,7 @@ def __init__( removed from the model.""", ), ) - self.declare( + self.check_for_new_or_removed_params: bool = self.declare( 'check_for_new_or_removed_params', ConfigValue( domain=bool, @@ -179,7 +182,7 @@ def __init__( removed from the model.""", ), ) - self.declare( + self.check_for_new_objective: bool = self.declare( 'check_for_new_objective', ConfigValue( domain=bool, @@ -190,7 +193,7 @@ def __init__( when you are certain objectives are not being added to / removed from the model.""", ), ) - self.declare( + self.update_constraints: bool = self.declare( 'update_constraints', ConfigValue( domain=bool, @@ -203,7 +206,7 @@ def __init__( are not being modified.""", ), ) - self.declare( + self.update_vars: bool = self.declare( 'update_vars', ConfigValue( domain=bool, @@ -215,7 +218,7 @@ def __init__( opt.update_variables() or when you are certain variables are not being modified.""", ), ) - self.declare( + self.update_params: bool = self.declare( 'update_params', ConfigValue( domain=bool, @@ -226,7 +229,7 @@ def __init__( opt.update_params() or when you are certain parameters are not being modified.""", ), ) - self.declare( + self.update_named_expressions: bool = self.declare( 'update_named_expressions', ConfigValue( domain=bool, @@ -238,7 +241,7 @@ def __init__( Expressions are not being modified.""", ), ) - self.declare( + self.update_objective: bool = self.declare( 'update_objective', ConfigValue( domain=bool, @@ -250,7 +253,7 @@ def __init__( certain objectives are not being modified.""", ), ) - self.declare( + self.treat_fixed_vars_as_params: bool = self.declare( 'treat_fixed_vars_as_params', ConfigValue( domain=bool, @@ -267,14 +270,3 @@ def __init__( updating the values of fixed variables is much faster this way.""", ), ) - - self.check_for_new_or_removed_constraints: bool = True - self.check_for_new_or_removed_vars: bool = True - self.check_for_new_or_removed_params: bool = True - self.check_for_new_objective: bool = True - self.update_constraints: bool = True - self.update_vars: bool = True - self.update_params: bool = True - self.update_named_expressions: bool = True - self.update_objective: bool = True - self.treat_fixed_vars_as_params: bool = True diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index fa0080c5a7b..02a898f2df5 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -10,7 +10,8 @@ # ___________________________________________________________________________ import enum -from typing import Optional +from typing import Optional, Tuple +from datetime import datetime from pyomo.common.config import ( ConfigDict, @@ -24,6 +25,7 @@ TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus, ) +from pyomo.solver.solution import SolutionLoaderBase class TerminationCondition(enum.Enum): @@ -127,14 +129,16 @@ def __init__( visibility=visibility, ) - self.solution_loader = self.declare('solution_loader', ConfigValue()) - self.termination_condition: In(TerminationCondition) = self.declare( + self.solution_loader: SolutionLoaderBase = self.declare( + 'solution_loader', ConfigValue() + ) + self.termination_condition: TerminationCondition = self.declare( 'termination_condition', ConfigValue( domain=In(TerminationCondition), default=TerminationCondition.unknown ), ) - self.solution_status: In(SolutionStatus) = self.declare( + self.solution_status: SolutionStatus = self.declare( 'solution_status', ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution), ) @@ -147,20 +151,21 @@ def __init__( self.solver_name: Optional[str] = self.declare( 'solver_name', ConfigValue(domain=str) ) - self.solver_version: Optional[tuple] = self.declare( + self.solver_version: Optional[Tuple[int, ...]] = self.declare( 'solver_version', ConfigValue(domain=tuple) ) - self.iteration_count: NonNegativeInt = self.declare( + self.iteration_count: Optional[int] = self.declare( 'iteration_count', ConfigValue(domain=NonNegativeInt) ) self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict()) - self.timing_info.start_time = self.timing_info.declare( + # TODO: Implement type checking for datetime + self.timing_info.start_time: datetime = self.timing_info.declare( 'start_time', ConfigValue() ) - self.timing_info.wall_time: NonNegativeFloat = self.timing_info.declare( + self.timing_info.wall_time: Optional[float] = self.timing_info.declare( 'wall_time', ConfigValue(domain=NonNegativeFloat) ) - self.timing_info.solver_wall_time: NonNegativeFloat = self.timing_info.declare( + self.timing_info.solver_wall_time: Optional[float] = self.timing_info.declare( 'solver_wall_time', ConfigValue(domain=NonNegativeFloat) ) self.extra_info: ConfigDict = self.declare( From 6dbc84d3f0064cb133fff46a62b0c14a0495dfd2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 11:35:34 -0600 Subject: [PATCH 0081/3044] Change un-init test to assign an empty SolutionLoader --- pyomo/solver/tests/test_results.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/test_results.py index 60b14d77521..f43b2b50ef4 100644 --- a/pyomo/solver/tests/test_results.py +++ b/pyomo/solver/tests/test_results.py @@ -102,6 +102,7 @@ def test_uninitialized(self): self.assertIsNone(res.timing_info.start_time) self.assertIsNone(res.timing_info.wall_time) self.assertIsNone(res.timing_info.solver_wall_time) + res.solution_loader = solution.SolutionLoader(None, None, None, None) with self.assertRaisesRegex( RuntimeError, '.*does not currently have a valid solution.*' From d09c88040db806eeb91eac478b3267e07474ad01 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 14:51:58 -0600 Subject: [PATCH 0082/3044] SAVE POINT: Starting work on IPOPT solver re-write --- pyomo/contrib/appsi/plugins.py | 2 +- pyomo/solver/IPOPT.py | 123 +++++++++++++++++++++++ pyomo/solver/__init__.py | 1 + pyomo/solver/base.py | 22 +--- pyomo/solver/config.py | 3 + pyomo/solver/factory.py | 33 ++++++ pyomo/solver/plugins.py | 2 +- pyomo/solver/tests/solvers/test_ipopt.py | 48 +++++++++ pyomo/solver/util.py | 9 ++ 9 files changed, 220 insertions(+), 23 deletions(-) create mode 100644 pyomo/solver/IPOPT.py create mode 100644 pyomo/solver/factory.py create mode 100644 pyomo/solver/tests/solvers/test_ipopt.py diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index 86dcd298a93..3a132b74395 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,5 +1,5 @@ from pyomo.common.extensions import ExtensionBuilderFactory -from pyomo.solver.base import SolverFactory +from pyomo.solver.factory import SolverFactory from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs from .build import AppsiBuilder diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py new file mode 100644 index 00000000000..3f5fa0e1df6 --- /dev/null +++ b/pyomo/solver/IPOPT.py @@ -0,0 +1,123 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import os +import subprocess + +from pyomo.common import Executable +from pyomo.common.config import ConfigValue +from pyomo.common.tempfiles import TempfileManager +from pyomo.opt import WriterFactory +from pyomo.solver.base import SolverBase +from pyomo.solver.config import SolverConfig +from pyomo.solver.factory import SolverFactory +from pyomo.solver.results import Results, TerminationCondition, SolutionStatus +from pyomo.solver.solution import SolutionLoaderBase +from pyomo.solver.util import SolverSystemError + +import logging + +logger = logging.getLogger(__name__) + + +class IPOPTConfig(SolverConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.executable = self.declare( + 'executable', ConfigValue(default=Executable('ipopt')) + ) + self.save_solver_io: bool = self.declare( + 'save_solver_io', ConfigValue(domain=bool, default=False) + ) + + +class IPOPTSolutionLoader(SolutionLoaderBase): + pass + + +@SolverFactory.register('ipopt', doc='The IPOPT NLP solver (new interface)') +class IPOPT(SolverBase): + CONFIG = IPOPTConfig() + + def __init__(self, **kwds): + self.config = self.CONFIG(kwds) + + def available(self): + if self.config.executable.path() is None: + return self.Availability.NotFound + return self.Availability.FullLicense + + def version(self): + results = subprocess.run( + [str(self.config.executable), '--version'], + timeout=1, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + ) + version = results.stdout.splitlines()[0] + version = version.split(' ')[1] + version = version.strip() + version = tuple(int(i) for i in version.split('.')) + return version + + @property + def config(self): + return self._config + + @config.setter + def config(self, val): + self._config = val + + def solve(self, model, **kwds): + # Check if solver is available + avail = self.available() + if not avail: + raise SolverSystemError( + f'Solver {self.__class__} is not available ({avail}).' + ) + # Update configuration options, based on keywords passed to solve + config = self.config(kwds.pop('options', {})) + config.set_value(kwds) + # Write the model to an nl file + nl_writer = WriterFactory('nl') + # Need to add check for symbolic_solver_labels; may need to generate up + # to three files for nl, row, col, if ssl == True + # What we have here may or may not work with IPOPT; will find out when + # we try to run it. + with TempfileManager.new_context() as tempfile: + dname = tempfile.mkdtemp() + with open(os.path.join(dname, model.name + '.nl')) as nl_file, open( + os.path.join(dname, model.name + '.row') + ) as row_file, open(os.path.join(dname, model.name + '.col')) as col_file: + info = nl_writer.write( + model, + nl_file, + row_file, + col_file, + symbolic_solver_labels=config.symbolic_solver_labels, + ) + # Call IPOPT - passing the files via the subprocess + subprocess.run() diff --git a/pyomo/solver/__init__.py b/pyomo/solver/__init__.py index e3eafa991cc..1ab9f975f0b 100644 --- a/pyomo/solver/__init__.py +++ b/pyomo/solver/__init__.py @@ -11,6 +11,7 @@ from . import base from . import config +from . import factory from . import results from . import solution from . import util diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index ba25b23a354..f7e5c4c58c5 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -21,8 +21,7 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError -from pyomo.opt.base import SolverFactory as LegacySolverFactory -from pyomo.common.factory import Factory + from pyomo.opt.results.results_ import SolverResults as LegacySolverResults from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize @@ -442,22 +441,3 @@ def __enter__(self): def __exit__(self, t, v, traceback): pass - - -class SolverFactoryClass(Factory): - def register(self, name, doc=None): - def decorator(cls): - self._cls[name] = cls - self._doc[name] = doc - - class LegacySolver(LegacySolverInterface, cls): - pass - - LegacySolverFactory.register(name, doc)(LegacySolver) - - return cls - - return decorator - - -SolverFactory = SolverFactoryClass() diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index efd2a1bac16..ed9008b7e1f 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -73,6 +73,9 @@ def __init__( self.time_limit: Optional[float] = self.declare( 'time_limit', ConfigValue(domain=NonNegativeFloat) ) + self.solver_options: ConfigDict = self.declare( + 'solver_options', ConfigDict(implicit=True) + ) class BranchAndBoundConfig(SolverConfig): diff --git a/pyomo/solver/factory.py b/pyomo/solver/factory.py new file mode 100644 index 00000000000..1a49ea92e40 --- /dev/null +++ b/pyomo/solver/factory.py @@ -0,0 +1,33 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.opt.base import SolverFactory as LegacySolverFactory +from pyomo.common.factory import Factory +from pyomo.solver.base import LegacySolverInterface + + +class SolverFactoryClass(Factory): + def register(self, name, doc=None): + def decorator(cls): + self._cls[name] = cls + self._doc[name] = doc + + class LegacySolver(LegacySolverInterface, cls): + pass + + LegacySolverFactory.register(name, doc)(LegacySolver) + + return cls + + return decorator + + +SolverFactory = SolverFactoryClass() diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index 5120bc9dd36..5dfd4bce1eb 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ -from .base import SolverFactory +from .factory import SolverFactory def load(): diff --git a/pyomo/solver/tests/solvers/test_ipopt.py b/pyomo/solver/tests/solvers/test_ipopt.py new file mode 100644 index 00000000000..afe2dbbe531 --- /dev/null +++ b/pyomo/solver/tests/solvers/test_ipopt.py @@ -0,0 +1,48 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +import pyomo.environ as pyo +from pyomo.common.fileutils import ExecutableData +from pyomo.common.config import ConfigDict +from pyomo.solver.IPOPT import IPOPTConfig +from pyomo.solver.factory import SolverFactory +from pyomo.common import unittest + + +class TestIPOPT(unittest.TestCase): + def create_model(self): + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(m): + return (1.0 - m.x) ** 2 + 100.0 * (m.y - m.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + return model + + def test_IPOPT_config(self): + # Test default initialization + config = IPOPTConfig() + self.assertTrue(config.load_solution) + self.assertIsInstance(config.solver_options, ConfigDict) + print(type(config.executable)) + self.assertIsInstance(config.executable, ExecutableData) + + # Test custom initialization + solver = SolverFactory('ipopt', save_solver_io=True) + self.assertTrue(solver.config.save_solver_io) + self.assertFalse(solver.config.tee) + + # Change value on a solve call + # model = self.create_model() + # result = solver.solve(model, tee=True) diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py index 1fb1738470b..79abee1b689 100644 --- a/pyomo/solver/util.py +++ b/pyomo/solver/util.py @@ -20,11 +20,20 @@ from pyomo.core.base.param import _ParamData, Param from pyomo.core.base.objective import Objective, _GeneralObjectiveData from pyomo.common.collections import ComponentMap +from pyomo.common.errors import PyomoException from pyomo.common.timing import HierarchicalTimer from pyomo.core.expr.numvalue import NumericConstant from pyomo.solver.config import UpdateConfig +class SolverSystemError(PyomoException): + """ + General exception to catch solver system errors + """ + + pass + + def get_objective(block): obj = None for o in block.component_data_objects( From f474d49008342b108e91bfde111beaf65434592f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 15:29:21 -0600 Subject: [PATCH 0083/3044] Change SolverFactory to remove legacy solver references --- pyomo/solver/factory.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/solver/factory.py b/pyomo/solver/factory.py index 1a49ea92e40..84b6cf02eac 100644 --- a/pyomo/solver/factory.py +++ b/pyomo/solver/factory.py @@ -20,10 +20,10 @@ def decorator(cls): self._cls[name] = cls self._doc[name] = doc - class LegacySolver(LegacySolverInterface, cls): - pass + # class LegacySolver(LegacySolverInterface, cls): + # pass - LegacySolverFactory.register(name, doc)(LegacySolver) + # LegacySolverFactory.register(name, doc)(LegacySolver) return cls From 88aeba6c00154a397570956524e9c0479c99024b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 31 Aug 2023 15:54:36 -0600 Subject: [PATCH 0084/3044] SAVE POINT: Stopping for end of sprin --- pyomo/solver/IPOPT.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 3f5fa0e1df6..384b2173840 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -101,6 +101,14 @@ def solve(self, model, **kwds): # Update configuration options, based on keywords passed to solve config = self.config(kwds.pop('options', {})) config.set_value(kwds) + # Get a copy of the environment to pass to the subprocess + env = os.environ.copy() + if 'PYOMO_AMPLFUNC' in env: + env['AMPLFUNC'] = "\n".join( + filter( + None, (env.get('AMPLFUNC', None), env.get('PYOMO_AMPLFUNC', None)) + ) + ) # Write the model to an nl file nl_writer = WriterFactory('nl') # Need to add check for symbolic_solver_labels; may need to generate up @@ -112,7 +120,7 @@ def solve(self, model, **kwds): with open(os.path.join(dname, model.name + '.nl')) as nl_file, open( os.path.join(dname, model.name + '.row') ) as row_file, open(os.path.join(dname, model.name + '.col')) as col_file: - info = nl_writer.write( + self.info = nl_writer.write( model, nl_file, row_file, @@ -120,4 +128,29 @@ def solve(self, model, **kwds): symbolic_solver_labels=config.symbolic_solver_labels, ) # Call IPOPT - passing the files via the subprocess - subprocess.run() + cmd = [str(config.executable), nl_file, '-AMPL'] + if config.time_limit is not None: + config.solver_options['max_cpu_time'] = config.time_limit + for key, val in config.solver_options.items(): + cmd.append(key + '=' + val) + process = subprocess.run(cmd, timeout=config.time_limit, + env=env, + universal_newlines=True) + + if process.returncode != 0: + if self.config.load_solution: + raise RuntimeError( + 'A feasible solution was not found, so no solution can be loaded.' + 'Please set config.load_solution=False and check ' + 'results.termination_condition and ' + 'results.incumbent_objective before loading a solution.' + ) + results = Results() + results.termination_condition = TerminationCondition.error + else: + results = self._parse_solution() + + def _parse_solution(self): + # STOPPING POINT: The suggestion here is to look at the original + # parser, which hasn't failed yet, and rework it to be ... better? + pass From 553c8bb45741c9a8c55a9ee68f4aa349aaa144aa Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Fri, 1 Sep 2023 15:34:14 -0400 Subject: [PATCH 0085/3044] add deactivate_trivial_constraints for feasibility subproblem --- pyomo/contrib/mindtpy/algorithm_base_class.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 7def1dcaab3..584d796d069 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1311,6 +1311,20 @@ def solve_feasibility_subproblem(self): update_solver_timelimit( self.feasibility_nlp_opt, config.nlp_solver, self.timing, config ) + try: + TransformationFactory('contrib.deactivate_trivial_constraints').apply_to( + self.fixed_nlp, + tmp=True, + ignore_infeasible=False, + tolerance=config.constraint_tolerance, + ) + except InfeasibleConstraintException as e: + config.logger.error( + str(e) + '\nInfeasibility detected in deactivate_trivial_constraints.' + ) + results = SolverResults() + results.solver.termination_condition = tc.infeasible + return self.fixed_nlp, results with SuppressInfeasibleWarning(): try: with time_code(self.timing, 'feasibility subproblem'): @@ -1341,6 +1355,9 @@ def solve_feasibility_subproblem(self): self.handle_feasibility_subproblem_tc( feas_soln.solver.termination_condition, MindtPy ) + TransformationFactory('contrib.deactivate_trivial_constraints').revert( + self.fixed_nlp + ) MindtPy.feas_opt.deactivate() for constr in MindtPy.nonlinear_constraint_list: constr.activate() From a698d319179a11efa0214fdff3c96114bcbc7a11 Mon Sep 17 00:00:00 2001 From: jlgearh Date: Tue, 5 Sep 2023 14:41:37 -0600 Subject: [PATCH 0086/3044] - Updating OBBT --- .../alternative_solutions/aos_utils.py | 2 +- pyomo/contrib/alternative_solutions/obbt.py | 36 +++++++------------ .../alternative_solutions/var_utils.py | 1 - 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index f18ffde9df8..8940d517993 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -39,7 +39,7 @@ def _get_active_objective(model): Finds and returns the active objective function for a model. Currently assume that there is exactly one active objective. ''' - active_objs = [o for o in model.component_data_objects(pe.Objective, + active_objs = [o for o in model.component_data_objects(cytpe=pe.Objective, active=True)] assert len(active_objs) == 1, \ "Model has {} active objective functions, exactly one is required.".\ diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 4208a86905a..28a7ff5937e 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -17,8 +17,7 @@ import pyomo.contrib.alternative_solutions.var_utils as var_utils def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, - refine_bounds=False, warmstart=False, already_solved=False, - solver='gurobi', solver_options={}, + refine_bounds=True, solver='gurobi', solver_options={}, use_persistent_solver=False, tee=False): ''' Calculates the bounds on each variable by solving a series of min and max @@ -45,20 +44,14 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, refine_bounds : boolean Boolean indicating that new constraints should be added to the model at each iteration to tighten the bounds for varaibles. - warmstart : boolean - Boolean indicating that previous solutions should be passed to the - solver as warmstart solutions. - already_solved : boolean - Indicates that the model has already been solved and that the - variable bound search can start from the current solution. solver : string The solver to be used. solver_options : dict Solver option-value pairs to be passed to the solver. use_persistent_solver : boolean Boolean indicating if the the APPSI persistent solver interface - should be used. Currently, only supported Gurobi is supported for - variable bound analysis with the persistent solver. + should be used. Currently, only Gurobi is supported for variable + bound analysis with the persistent solver. tee : boolean Boolean indicating that the solver output should be displayed. @@ -69,10 +62,7 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, {variable: (lower_bound, upper_bound)} ''' - aos_utils._check_concrete_model(model) assert isinstance(refine_bounds, bool), 'refine_bounds must be a Boolean' - assert isinstance(warmstart, bool), 'warmstart must be a Boolean' - assert isinstance(already_solved, bool), 'already_solved must be a Boolean' assert isinstance(use_persistent_solver, bool), \ 'use_persistent_solver must be a Boolean' assert isinstance(tee, bool), 'tee must be a Boolean' @@ -85,20 +75,18 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, include_fixed=False) orig_objective = aos_utils._get_active_objective(model) - aos_block = aos_utils._add_aos_block(model) - new_constraint = False + aos_block = aos_utils._add_aos_block(model, name='_obbt_block') opt = aos_utils._get_solver(solver, solver_options, use_persistent_solver) - if not already_solved: - results = opt.solve(model, tee=tee) - status = results.solver.status - condition = results.solver.termination_condition - assert (status == SolverStatus.ok and - condition == TerminationCondition.optimal), \ - ('Model cannot be solved, SolverStatus = {}, ' - 'TerminationCondition = {}').format(status.value, - condition.value) + results = opt.solve(model, tee=tee) + status = results.solver.status + condition = results.solver.termination_condition + assert (status == SolverStatus.ok and + condition == TerminationCondition.optimal), \ + ('Model cannot be solved, SolverStatus = {}, ' + 'TerminationCondition = {}').format(status.value, + condition.value) orig_objective_value = pe.value(orig_objective) aos_utils._add_objective_constraint(aos_block, orig_objective, diff --git a/pyomo/contrib/alternative_solutions/var_utils.py b/pyomo/contrib/alternative_solutions/var_utils.py index c387f2ffd93..f8b37b40ebd 100644 --- a/pyomo/contrib/alternative_solutions/var_utils.py +++ b/pyomo/contrib/alternative_solutions/var_utils.py @@ -67,7 +67,6 @@ def get_model_variables(model, components='all', include_continuous=True, ''' # Validate inputs - aos_utils._check_concrete_model(model) assert isinstance(include_continuous, bool), \ 'include_continuous must be a Boolean' assert isinstance(include_binary, bool), 'include_binary must be a Boolean' From b7a446c70dcbadd43a4da5e330a8e39e785f0d71 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 8 Sep 2023 11:08:59 -0600 Subject: [PATCH 0087/3044] Update documentation on solver interfaces results --- .../developer_reference/solvers.rst | 36 +++++- pyomo/solver/IPOPT.py | 6 +- pyomo/solver/results.py | 116 ++++++++++++++---- 3 files changed, 130 insertions(+), 28 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index d48e270cc7c..1dcd2f66da7 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -3,17 +3,47 @@ Solver Interfaces Pyomo offers interfaces into multiple solvers, both commercial and open source. +.. currentmodule:: pyomo.solver + + +Results +------- + +Every solver, at the end of a ``solve`` call, will return a ``Results`` object. +This object is a :py:class:`pyomo.common.config.ConfigDict`, which can be manipulated similar +to a standard ``dict`` in Python. + +.. autoclass:: pyomo.solver.results.Results + :show-inheritance: + :members: + :undoc-members: + Termination Conditions ---------------------- Pyomo offers a standard set of termination conditions to map to solver -returns. +returns. The intent of ``TerminationCondition`` is to notify the user of why +the solver exited. The user is expected to inspect the ``Results`` object or any +returned solver messages or logs for more information. -.. currentmodule:: pyomo.contrib.appsi -.. autoclass:: pyomo.contrib.appsi.base.TerminationCondition + +.. autoclass:: pyomo.solver.results.TerminationCondition + :show-inheritance: :noindex: +Solution Status +--------------- + +Pyomo offers a standard set of solution statuses to map to solver output. The +intent of ``SolutionStatus`` is to notify the user of what the solver returned +at a high level. The user is expected to inspect the ``Results`` object or any +returned solver messages or logs for more information. + +.. autoclass:: pyomo.solver.results.SolutionStatus + :show-inheritance: + :noindex: + diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 384b2173840..cce0017c5be 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -133,9 +133,9 @@ def solve(self, model, **kwds): config.solver_options['max_cpu_time'] = config.time_limit for key, val in config.solver_options.items(): cmd.append(key + '=' + val) - process = subprocess.run(cmd, timeout=config.time_limit, - env=env, - universal_newlines=True) + process = subprocess.run( + cmd, timeout=config.time_limit, env=env, universal_newlines=True + ) if process.returncode != 0: if self.config.load_solution: diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 02a898f2df5..6a940860661 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -30,68 +30,104 @@ class TerminationCondition(enum.Enum): """ - An enumeration for checking the termination condition of solvers - """ + An Enum that enumerates all possible exit statuses for a solver call. - """unknown serves as both a default value, and it is used when no other enum member makes sense""" - unknown = 42 + Attributes + ---------- + convergenceCriteriaSatisfied: 0 + The solver exited because convergence criteria of the problem were + satisfied. + maxTimeLimit: 1 + The solver exited due to reaching a specified time limit. + iterationLimit: 2 + The solver exited due to reaching a specified iteration limit. + objectiveLimit: 3 + The solver exited due to reaching an objective limit. For example, + in Gurobi, the exit message "Optimal objective for model was proven to + be worse than the value specified in the Cutoff parameter" would map + to objectiveLimit. + minStepLength: 4 + The solver exited due to a minimum step length. + Minimum step length reached may mean that the problem is infeasible or + that the problem is feasible but the solver could not converge. + unbounded: 5 + The solver exited because the problem has been found to be unbounded. + provenInfeasible: 6 + The solver exited because the problem has been proven infeasible. + locallyInfeasible: 7 + The solver exited because no feasible solution was found to the + submitted problem, but it could not be proven that no such solution exists. + infeasibleOrUnbounded: 8 + Some solvers do not specify between infeasibility or unboundedness and + instead return that one or the other has occurred. For example, in + Gurobi, this may occur because there are some steps in presolve that + prevent Gurobi from distinguishing between infeasibility and unboundedness. + error: 9 + The solver exited with some error. The error message will also be + captured and returned. + interrupted: 10 + The solver was interrupted while running. + licensingProblems: 11 + The solver experienced issues with licensing. This could be that no + license was found, the license is of the wrong type for the problem (e.g., + problem is too big for type of license), or there was an issue contacting + a licensing server. + unknown: 42 + All other unrecognized exit statuses fall in this category. + """ - """The solver exited because the convergence criteria were satisfied""" convergenceCriteriaSatisfied = 0 - """The solver exited due to a time limit""" maxTimeLimit = 1 - """The solver exited due to an iteration limit""" iterationLimit = 2 - """The solver exited due to an objective limit""" objectiveLimit = 3 - """The solver exited due to a minimum step length""" minStepLength = 4 - """The solver exited because the problem is unbounded""" unbounded = 5 - """The solver exited because the problem is proven infeasible""" provenInfeasible = 6 - """The solver exited because the problem was found to be locally infeasible""" locallyInfeasible = 7 - """The solver exited because the problem is either infeasible or unbounded""" infeasibleOrUnbounded = 8 - """The solver exited due to an error""" error = 9 - """The solver exited because it was interrupted""" interrupted = 10 - """The solver exited due to licensing problems""" licensingProblems = 11 + unknown = 42 + class SolutionStatus(enum.IntEnum): """ An enumeration for interpreting the result of a termination. This describes the designated status by the solver to be loaded back into the model. - For now, we are choosing to use IntEnum such that return values are numerically - assigned in increasing order. + Attributes + ---------- + noSolution: 0 + No (single) solution was found; possible that a population of solutions + was returned. + infeasible: 10 + Solution point does not satisfy some domains and/or constraints. + feasible: 20 + A solution for which all of the constraints in the model are satisfied. + optimal: 30 + A feasible solution where the objective function reaches its specified + sense (e.g., maximum, minimum) """ - """No (single) solution found; possible that a population of solutions was returned""" noSolution = 0 - """Solution point does not satisfy some domains and/or constraints""" infeasible = 10 - """Feasible solution identified""" feasible = 20 - """Optimal solution identified""" optimal = 30 @@ -99,9 +135,14 @@ class Results(ConfigDict): """ Attributes ---------- + solution_loader: SolutionLoaderBase + Object for loading the solution back into the model. termination_condition: TerminationCondition The reason the solver exited. This is a member of the TerminationCondition enum. + solution_status: SolutionStatus + The result of the solve call. This is a member of the SolutionStatus + enum. incumbent_objective: float If a feasible solution was found, this is the objective value of the best solution found. If no feasible solution was found, this is @@ -111,6 +152,19 @@ class Results(ConfigDict): the lower bound. For maximization problems, this is the upper bound. For solvers that do not provide an objective bound, this should be -inf (minimization) or inf (maximization) + solver_name: str + The name of the solver in use. + solver_version: tuple + A tuple representing the version of the solver in use. + iteration_count: int + The total number of iterations. + timing_info: ConfigDict + A ConfigDict containing three pieces of information: + start_time: UTC timestamp of when run was initiated + wall_time: elapsed wall clock time for entire process + solver_wall_time: elapsed wall clock time for solve call + extra_info: ConfigDict + A ConfigDict to store extra information such as solver messages. """ def __init__( @@ -181,6 +235,24 @@ def __str__(self): return s +class ResultsReader: + pass + + +def parse_sol_file(filename, results): + if results is None: + results = Results() + pass + + +def parse_yaml(): + pass + + +def parse_json(): + pass + + # Everything below here preserves backwards compatibility legacy_termination_condition_map = { From b3af7ac380aa58d9d40393890b0436438e7305ff Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 8 Sep 2023 11:14:10 -0600 Subject: [PATCH 0088/3044] Add in TODOs for documentation --- doc/OnlineDocs/developer_reference/solvers.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 1dcd2f66da7..75d95fc36db 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -6,6 +6,12 @@ Pyomo offers interfaces into multiple solvers, both commercial and open source. .. currentmodule:: pyomo.solver +Interface Implementation +------------------------ + +TBD: How to add a new interface; the pieces. + + Results ------- @@ -20,7 +26,7 @@ to a standard ``dict`` in Python. Termination Conditions ----------------------- +^^^^^^^^^^^^^^^^^^^^^^ Pyomo offers a standard set of termination conditions to map to solver returns. The intent of ``TerminationCondition`` is to notify the user of why @@ -35,7 +41,7 @@ returned solver messages or logs for more information. Solution Status ---------------- +^^^^^^^^^^^^^^^ Pyomo offers a standard set of solution statuses to map to solver output. The intent of ``SolutionStatus`` is to notify the user of what the solver returned @@ -47,3 +53,7 @@ returned solver messages or logs for more information. :noindex: +Solution +-------- + +TBD: How to load/parse a solution. From 5c05c7880f8281e52cf8cc8cd8407374e5313998 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 12 Sep 2023 12:44:11 -0600 Subject: [PATCH 0089/3044] SAVE POINT: Start the termination conditions/etc. --- pyomo/solver/results.py | 68 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 6a940860661..c8c92109040 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -26,6 +26,7 @@ SolverStatus as LegacySolverStatus, ) from pyomo.solver.solution import SolutionLoaderBase +from pyomo.solver.util import SolverSystemError class TerminationCondition(enum.Enum): @@ -239,10 +240,73 @@ class ResultsReader: pass -def parse_sol_file(filename, results): +def parse_sol_file(file, results): + # The original reader for sol files is in pyomo.opt.plugins.sol. + # Per my original complaint, it has "magic numbers" that I just don't + # know how to test. It's apparently less fragile than that in APPSI. + # NOTE: The Results object now also holds the solution loader, so we do + # not need pass in a solution like we did previously. if results is None: results = Results() - pass + + # For backwards compatibility and general safety, we will parse all + # lines until "Options" appears. Anything before "Options" we will + # consider to be the solver message. + message = [] + for line in file: + if not line: + break + line = line.strip() + if "Options" in line: + break + message.append(line) + message = '\n'.join(message) + # Once "Options" appears, we must now read the content under it. + model_objects = [] + if "Options" in line: + line = file.readline() + number_of_options = int(line) + need_tolerance = False + if number_of_options > 4: # MRM: Entirely unclear why this is necessary, or if it even is + number_of_options -= 2 + need_tolerance = True + for i in range(number_of_options + 4): + line = file.readline() + model_objects.append(int(line)) + if need_tolerance: # MRM: Entirely unclear why this is necessary, or if it even is + line = file.readline() + model_objects.append(float(line)) + else: + raise SolverSystemError("ERROR READING `sol` FILE. No 'Options' line found.") + # Identify the total number of variables and constraints + number_of_cons = model_objects[number_of_options + 1] + number_of_vars = model_objects[number_of_options + 3] + constraints = [] + variables = [] + # Parse through the constraint lines and capture the constraints + i = 0 + while i < number_of_cons: + line = file.readline() + constraints.append(float(line)) + # Parse through the variable lines and capture the variables + i = 0 + while i < number_of_vars: + line = file.readline() + variables.append(float(line)) + exit_code = [0, 0] + line = file.readline() + if line and ('objno' in line): + exit_code_line = line.split() + if (len(exit_code_line) != 3): + raise SolverSystemError(f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}.") + exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] + else: + raise SolverSystemError(f"ERROR READING `sol` FILE. Expected `objno`; received {line}.") + results.extra_info.solver_message = message.strip().replace('\n', '; ') + # Not sure if next two lines are needed + # if isinstance(res.solver.message, str): + # res.solver.message = res.solver.message.replace(':', '\\x3a') + def parse_yaml(): From f36faa1025cfc2a344237ab00f1fe9bc5e1ea36a Mon Sep 17 00:00:00 2001 From: jlgearh Date: Wed, 13 Sep 2023 16:33:26 -0600 Subject: [PATCH 0090/3044] - Finalizing OBBT code --- .../alternative_solutions/aos_utils.py | 23 +-- pyomo/contrib/alternative_solutions/obbt.py | 194 +++++++++++------- 2 files changed, 121 insertions(+), 96 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index 8940d517993..409522cacd2 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -16,30 +16,13 @@ from pyomo.common.modeling import unique_component_name import pyomo.environ as pe -from pyomo.opt import SolverFactory -from pyomo.contrib import appsi - -def _get_solver(solver='gurobi', solver_options={}, - use_persistent_solver=False): - if use_persistent_solver: - assert solver == 'gurobi', \ - "Persistent solver option requires the use of Gurobi." - opt = appsi.solvers.Gurobi() - opt.config.stream_solver = True - for parameter, value in solver_options.items(): - opt.set_gurobi_param(parameter, value) - else: - opt = SolverFactory(solver) - for parameter, value in solver_options.items(): - opt.options[parameter] = value - return opt def _get_active_objective(model): ''' Finds and returns the active objective function for a model. Currently assume that there is exactly one active objective. ''' - active_objs = [o for o in model.component_data_objects(cytpe=pe.Objective, + active_objs = [o for o in model.component_data_objects(pe.Objective, active=True)] assert len(active_objs) == 1, \ "Model has {} active objective functions, exactly one is required.".\ @@ -59,6 +42,7 @@ def _add_objective_constraint(aos_block, objective, objective_value, Adds a relative and/or absolute objective function constraint to the specified block. ''' + objective_constraints = [] if rel_opt_gap is not None or abs_gap is not None: objective_is_min = objective.is_minimizing() objective_expr = objective.expr @@ -79,6 +63,7 @@ def _add_objective_constraint(aos_block, objective, objective_value, aos_block.optimality_tol_rel = \ pe.Constraint(expr=objective_expr >= \ objective_cutoff) + objective_constraints.append(aos_block.optimality_tol_rel) if abs_gap is not None: objective_cutoff = objective_value + objective_sense \ @@ -92,6 +77,8 @@ def _add_objective_constraint(aos_block, objective, objective_value, aos_block.optimality_tol_abs = \ pe.Constraint(expr=objective_expr >= \ objective_cutoff) + objective_constraints.append(aos_block.optimality_tol_abs) + return objective_constraints def _get_max_solutions(max_solutions): assert isinstance(max_solutions, (int, type(None))), \ diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 28a7ff5937e..21267159463 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -10,15 +10,11 @@ # ___________________________________________________________________________ import pyomo.environ as pe -from pyomo.opt import SolverStatus, TerminationCondition -from pyomo.common.collections import ComponentMap +from pyomo.contrib.alternative_solutions import aos_utils, var_utils -import pyomo.contrib.alternative_solutions.aos_utils as aos_utils -import pyomo.contrib.alternative_solutions.var_utils as var_utils - -def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, - refine_bounds=True, solver='gurobi', solver_options={}, - use_persistent_solver=False, tee=False): +def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, + refine_discrete_bounds=True, warmstart=True, solver='gurobi', + solver_options={}, tee=False): ''' Calculates the bounds on each variable by solving a series of min and max optimization problems where each variable is used as the objective function @@ -37,21 +33,26 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, The relative optimality gap for the original objective for which variable bounds will be found. None indicates that a relative gap constraint will not be added to the model. - abs_gap : float or None + abs_opt_gap : float or None The absolute optimality gap for the original objective for which variable bounds will be found. None indicates that an absolute gap constraint will not be added to the model. - refine_bounds : boolean + refine_discrete_bounds : boolean Boolean indicating that new constraints should be added to the - model at each iteration to tighten the bounds for varaibles. + model at each iteration to tighten the bounds for discrete + variables. + warmstart : boolean + Boolean indicating that the solver should be warmstarted from the + best previously discovered solution. solver : string The solver to be used. solver_options : dict Solver option-value pairs to be passed to the solver. - use_persistent_solver : boolean + use_appsi : boolean Boolean indicating if the the APPSI persistent solver interface - should be used. Currently, only Gurobi is supported for variable - bound analysis with the persistent solver. + should be used. To use APPSI pass the base solver name and set this + input to true. E.g., passing 'gurobi' as the solver and a value of + true will create an instance of an 'appsi_gurobi' solver. tee : boolean Boolean indicating that the solver output should be displayed. @@ -59,45 +60,60 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, ------- variable_ranges A Pyomo ComponentMap containing the bounds for each variable. - {variable: (lower_bound, upper_bound)} + {variable: (lower_bound, upper_bound)}. A None value indicates + the solver encountered an issue. ''' - - assert isinstance(refine_bounds, bool), 'refine_bounds must be a Boolean' - assert isinstance(use_persistent_solver, bool), \ - 'use_persistent_solver must be a Boolean' - assert isinstance(tee, bool), 'tee must be a Boolean' - - if variables == 'all': - variable_list = var_utils.get_model_variables(model, variables, + + print('STARTING OBBT ANALYSIS') + if variables == 'all' or warmstart: + all_variables = var_utils.get_model_variables(model, 'all', include_fixed=False) + if warmstart: + solutions = pe.ComponentMap() + for var in all_variables: + solutions[var] = [] + if variables == 'all': + variable_list = all_variables else: variable_list = var_utils.check_variables(model, variables, include_fixed=False) + num_vars = len(variable_list) + print('Analyzing {} variables ({} total solves).'.format(num_vars, + 2 * num_vars)) orig_objective = aos_utils._get_active_objective(model) - aos_block = aos_utils._add_aos_block(model, name='_obbt_block') - - opt = aos_utils._get_solver(solver, solver_options, use_persistent_solver) - - results = opt.solve(model, tee=tee) + + opt = pe.SolverFactory(solver) + for parameter, value in solver_options.items(): + opt.options[parameter] = value + use_appsi = False + if 'appsi' in solver: + use_appsi = True + print('Peforming initial solve of model.') + results = opt.solve(model, warmstart=warmstart, tee=tee) status = results.solver.status condition = results.solver.termination_condition - assert (status == SolverStatus.ok and - condition == TerminationCondition.optimal), \ - ('Model cannot be solved, SolverStatus = {}, ' - 'TerminationCondition = {}').format(status.value, - condition.value) - + if condition != pe.TerminationCondition.optimal: + raise Exception(('OBBT cannot be applied, SolverStatus = {}, ' + 'TerminationCondition = {}').format(status.value, + condition.value)) + if warmstart: + _add_solution(solutions) orig_objective_value = pe.value(orig_objective) - aos_utils._add_objective_constraint(aos_block, orig_objective, - orig_objective_value, rel_opt_gap, - abs_gap) - if rel_opt_gap is not None or abs_gap is not None: + print('Found optimal solution, value = {}.'.format(orig_objective_value)) + aos_block = aos_utils._add_aos_block(model, name='_obbt') + print('Added block {} to the model.'.format(aos_block)) + obj_constraints = aos_utils._add_objective_constraint(aos_block, + orig_objective, + orig_objective_value, + rel_opt_gap, + abs_opt_gap) + new_constraint = False + if len(obj_constraints) > 0: new_constraint = True - orig_objective.deactivate() - if use_persistent_solver: + if use_appsi: opt.update_config.check_for_new_or_removed_constraints = new_constraint opt.update_config.check_for_new_or_removed_vars = False opt.update_config.check_for_new_or_removed_params = False @@ -109,19 +125,15 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, opt.update_config.update_objective = False opt.update_config.treat_fixed_vars_as_params = False - variable_bounds = ComponentMap() + variable_bounds = pe.ComponentMap() - senses = [pe.minimize, pe.maximize] + senses = [(pe.minimize, 'LB'), (pe.maximize, 'UB')] iteration = 1 - total_iterations = len(senses) * len(variable_list) - for idx in range(2): - sense = senses[idx] - sense_name = 'min' - bound_dir = 'LB' - if sense == pe.maximize: - sense_name = 'max' - bound_dir = 'UB' + total_iterations = len(senses) * num_vars + for idx in range(len(senses)): + sense = senses[idx][0] + bound_dir = senses[idx][1] for var in variable_list: if idx == 0: @@ -132,50 +144,55 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, aos_block.var_objective = pe.Objective(expr=var, sense=sense) - # TODO: Updated solution pool + if warmstart: + _update_values(var, bound_dir, solutions) - if use_persistent_solver: + if use_appsi: opt.update_config.check_for_new_or_removed_constraints = \ new_constraint - results = opt.solve(model, tee=tee) + results = opt.solve(model, warmstart=warmstart, tee=tee) new_constraint = False status = results.solver.status condition = results.solver.termination_condition - if (status == SolverStatus.ok and - condition == TerminationCondition.optimal): + if condition == pe.TerminationCondition.optimal: + if warmstart: + _add_solution(solutions) obj_val = pe.value(var) variable_bounds[var][idx] = obj_val - if refine_bounds and sense == pe.minimize and var.lb < obj_val: - bound_name = var.name + '_lb' - bound = pe.Constraint(expr= var >= obj_val) - setattr(aos_block, bound_name, bound) - new_constraint = True + if refine_discrete_bounds and not var.is_continuous(): + if sense == pe.minimize and var.lb < obj_val: + bound_name = var.name + '_' + str.lower(bound_dir) + bound = pe.Constraint(expr= var >= obj_val) + setattr(aos_block, bound_name, bound) + new_constraint = True + + if sense == pe.maximize and var.ub > obj_val: + bound_name = var.name + '_' + str.lower(bound_dir) + bound = pe.Constraint(expr= var <= obj_val) + setattr(aos_block, bound_name, bound) + new_constraint = True - if refine_bounds and sense == pe.maximize and var.ub > obj_val: - bound_name = var.name + '_ub' - bound = pe.Constraint(expr= var <= obj_val) - setattr(aos_block, bound_name, bound) - new_constraint = True # An infeasibleOrUnbounded status code will imply the problem is - # unbounded since feasibility has be established previously - elif (status == SolverStatus.ok and ( - condition == TerminationCondition.infeasibleOrUnbounded or - condition == TerminationCondition.unbounded)): + # unbounded since feasibility has been established previously + elif (condition == pe.TerminationCondition.infeasibleOrUnbounded or + condition == pe.TerminationCondition.unbounded): if sense == pe.minimize: variable_bounds[var][idx] = float('-inf') else: variable_bounds[var][idx] = float('inf') else: - print(('Unexpected solver status for variable {} {} problem.' + print(('Unexpected condition for the variable {} {} problem.' 'SolverStatus = {}, TerminationCondition = {}').\ - format(var.name, sense_name, status.value, + format(var.name, bound_dir, status.value, condition.value)) - - print('It. {}/{}: {}_{} = {}'.format(iteration, total_iterations, - var.name, bound_dir, - variable_bounds[var][idx])) + var_value = variable_bounds[var][idx] + print('Iteration {}/{}: {}_{} = {}'.format(iteration, + total_iterations, + var.name, + bound_dir, + var_value)) if idx == 1: variable_bounds[var] = tuple(variable_bounds[var]) @@ -183,7 +200,28 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_gap=None, iteration += 1 - aos_block.deactivate - orig_objective.active + aos_block.deactivate() + orig_objective.activate() - return variable_bounds + print('COMPLETED OBBT ANALYSIS') + + return variable_bounds, solutions + +def _add_solution(solutions): + '''Add the current variable values to the solution list.''' + for var in solutions: + solutions[var].append(pe.value(var)) + +def _update_values(var, bound_dir, solutions): + ''' + Set the values of all variables to the best solution seen previously for + the current objective function. + ''' + if bound_dir == 'LB': + value = min(solutions[var]) + else: + value = max(solutions[var]) + idx = solutions[var].index(value) + for variable in solutions: + variable.set_value(solutions[variable][idx]) + \ No newline at end of file From af46d29641856800ad32e59bb236e68643dc4961 Mon Sep 17 00:00:00 2001 From: jlgearh Date: Wed, 13 Sep 2023 16:33:48 -0600 Subject: [PATCH 0091/3044] - Adding test files --- .../alternative_solutions/obbt_test.py | 12 ++++++ .../tests/soln_pool_test.py | 40 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 pyomo/contrib/alternative_solutions/obbt_test.py create mode 100644 pyomo/contrib/alternative_solutions/tests/soln_pool_test.py diff --git a/pyomo/contrib/alternative_solutions/obbt_test.py b/pyomo/contrib/alternative_solutions/obbt_test.py new file mode 100644 index 00000000000..911d4ff1e07 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/obbt_test.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Sep 7 13:07:51 2023 + +@author: jlgearh +""" + +from obbt import obbt_analysis +from tests.test_cases import get_continuous_prob_1 + +m = get_continuous_prob_1() +results, solutions = obbt_analysis(m, warmstart=True, solver='cplex') \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/soln_pool_test.py b/pyomo/contrib/alternative_solutions/tests/soln_pool_test.py new file mode 100644 index 00000000000..61d3aef7a0a --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/soln_pool_test.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +""" +Created on Wed Sep 6 15:21:06 2023 + +@author: jlgearh +""" + +import test_cases + +model = test_cases.knapsack(10) + +ast = '*'*10 + +# print(ast,'Start APPSI',ast) +# from pyomo.contrib import appsi +# opt = appsi.solvers.Gurobi() +# opt.config.stream_solver = True +# #opt.set_instance(model) +# opt.gurobi_options['PoolSolutions'] = 10 +# opt.gurobi_options['PoolSearchMode'] = 2 +# #opt.set_gurobi_param('PoolSolutions', 10) +# #opt.set_gurobi_param('PoolSearchMode', 2) +# results = opt.solve(model) +# print(ast,'END APPSI',ast) + +# print(ast,'Start Solve Factory',ast) +# from pyomo.opt import SolverFactory +# opt2 = SolverFactory('gurobi') +# opt.gurobi_options['PoolSolutions'] = 10 +# opt.gurobi_options['PoolSearchMode'] = 2 +# opt2.solve(model, tee=True) +# print(ast,'End Solve Factory',ast) + +print(ast,'Start Solve Factory',ast) +from pyomo.opt import SolverFactory +opt3 = SolverFactory('appsi_gurobi') +opt3.gurobi_options['PoolSolutions'] = 10 +opt3.gurobi_options['PoolSearchMode'] = 2 +opt3.solve(model, tee=True) +print(ast,'End Solve Factory',ast) \ No newline at end of file From 6d442b24542d524c0ff414a6009408d27deb39df Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 20 Sep 2023 22:38:14 -0400 Subject: [PATCH 0092/3044] disable ipopt warmstart for feasibility subproblem solver --- pyomo/contrib/mindtpy/algorithm_base_class.py | 2 +- pyomo/contrib/mindtpy/util.py | 21 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index f3877304adb..c254c5d3f3d 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -2574,7 +2574,7 @@ def initialize_subsolvers(self): self.nlp_opt, config.nlp_solver, config ) set_solver_constraint_violation_tolerance( - self.feasibility_nlp_opt, config.nlp_solver, config + self.feasibility_nlp_opt, config.nlp_solver, config, warm_start=False ) self.set_appsi_solver_update_config() diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index e336715cc8f..068cd61aba1 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -566,7 +566,7 @@ def set_solver_mipgap(opt, solver_name, config): opt.options['add_options'].append('option optcr=%s;' % config.mip_solver_mipgap) -def set_solver_constraint_violation_tolerance(opt, solver_name, config): +def set_solver_constraint_violation_tolerance(opt, solver_name, config, warm_start=True): """Set constraint violation tolerance for solvers. Parameters @@ -600,15 +600,16 @@ def set_solver_constraint_violation_tolerance(opt, solver_name, config): opt.options['add_options'].append( 'constr_viol_tol ' + str(config.zero_tolerance) ) - # Ipopt warmstart options - opt.options['add_options'].append( - 'warm_start_init_point yes\n' - 'warm_start_bound_push 1e-9\n' - 'warm_start_bound_frac 1e-9\n' - 'warm_start_slack_bound_frac 1e-9\n' - 'warm_start_slack_bound_push 1e-9\n' - 'warm_start_mult_bound_push 1e-9\n' - ) + if warm_start: + # Ipopt warmstart options + opt.options['add_options'].append( + 'warm_start_init_point yes\n' + 'warm_start_bound_push 1e-9\n' + 'warm_start_bound_frac 1e-9\n' + 'warm_start_slack_bound_frac 1e-9\n' + 'warm_start_slack_bound_push 1e-9\n' + 'warm_start_mult_bound_push 1e-9\n' + ) elif config.nlp_solver_args['solver'] == 'conopt': opt.options['add_options'].append( 'RTNWMA ' + str(config.zero_tolerance) From a6e92c53e63b9febad79e8a19a5230c8c308328a Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 21 Sep 2023 01:44:41 -0400 Subject: [PATCH 0093/3044] create new copy_var_list_values function --- pyomo/contrib/mindtpy/algorithm_base_class.py | 4 ++- pyomo/contrib/mindtpy/single_tree.py | 3 +- pyomo/contrib/mindtpy/util.py | 29 +++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index c254c5d3f3d..836df9fff78 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -56,7 +56,6 @@ SuppressInfeasibleWarning, _DoNothing, lower_logger_level_to, - copy_var_list_values, get_main_elapsed_time, time_code, ) @@ -81,6 +80,7 @@ set_solver_mipgap, set_solver_constraint_violation_tolerance, update_solver_timelimit, + copy_var_list_values ) single_tree, single_tree_available = attempt_import('pyomo.contrib.mindtpy.single_tree') @@ -866,12 +866,14 @@ def init_rNLP(self, add_oa_cuts=True): self.rnlp.MindtPy_utils.variable_list, self.mip.MindtPy_utils.variable_list, config, + ignore_integrality=True ) if config.init_strategy == 'FP': copy_var_list_values( self.rnlp.MindtPy_utils.variable_list, self.working_model.MindtPy_utils.variable_list, config, + ignore_integrality=True ) self.add_cuts( dual_values=dual_values, diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 9776920f434..f3be27cbc4c 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -16,9 +16,8 @@ from pyomo.repn import generate_standard_repn import pyomo.core.expr as EXPR from math import copysign -from pyomo.contrib.mindtpy.util import get_integer_solution +from pyomo.contrib.mindtpy.util import get_integer_solution, copy_var_list_values from pyomo.contrib.gdpopt.util import ( - copy_var_list_values, get_main_elapsed_time, time_code, ) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 068cd61aba1..59490248e49 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -23,6 +23,7 @@ RangeSet, ConstraintList, TransformationFactory, + value ) from pyomo.repn import generate_standard_repn from pyomo.contrib.mcpp.pyomo_mcpp import mcpp_available, McCormick @@ -964,3 +965,31 @@ def generate_norm_constraint(fp_nlp_model, mip_model, config): mip_model.MindtPy_utils.discrete_variable_list, ): fp_nlp_model.norm_constraint.add(nlp_var - mip_var.value <= rhs) + +def copy_var_list_values(from_list, to_list, config, + skip_stale=False, skip_fixed=True, + ignore_integrality=False): + """Copy variable values from one list to another. + Rounds to Binary/Integer if necessary + Sets to zero for NonNegativeReals if necessary + """ + for v_from, v_to in zip(from_list, to_list): + if skip_stale and v_from.stale: + continue # Skip stale variable values. + if skip_fixed and v_to.is_fixed(): + continue # Skip fixed variables. + var_val = value(v_from, exception=False) + rounded_val = int(round(var_val)) + if var_val in v_to.domain: + v_to.set_value(value(v_from, exception=False)) + elif ignore_integrality and v_to.is_integer(): + v_to.set_value(value(v_from, exception=False)) + elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: + v_to.set_value(0) + elif v_to.is_integer() and (math.fabs(var_val - rounded_val) <= + config.integer_tolerance): + print('var_val', var_val) + v_to.pprint() + v_to.set_value(rounded_val) + else: + raise From f766c0a9d6f66da8fea8b11f277bb719ef94f48f Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 21 Sep 2023 19:23:03 -0400 Subject: [PATCH 0094/3044] update log format --- pyomo/contrib/mindtpy/algorithm_base_class.py | 30 +++++++++++++++++-- pyomo/contrib/mindtpy/util.py | 2 -- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 836df9fff78..514c2edaedb 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -124,6 +124,9 @@ def __init__(self, **kwds): self.fixed_nlp_log_formatter = ( '{:1}{:>9} {:>15} {:>15g} {:>12g} {:>12g} {:>7.2%} {:>7.2f}' ) + self.infeasible_fixed_nlp_log_formatter = ( + '{:1}{:>9} {:>15} {:>15} {:>12g} {:>12g} {:>7.2%} {:>7.2f}' + ) self.log_note_formatter = ' {:>9} {:>15} {:>15}' # Flag indicating whether the solution improved in the past @@ -1210,7 +1213,18 @@ def handle_subproblem_infeasible(self, fixed_nlp, cb_opt=None): # TODO try something else? Reinitialize with different initial # value? config = self.config - config.logger.info('NLP subproblem was locally infeasible.') + config.logger.info( + self.infeasible_fixed_nlp_log_formatter.format( + ' ', + self.nlp_iter, + 'Fixed NLP', + 'Infeasible', + self.primal_bound, + self.dual_bound, + self.rel_gap, + get_main_elapsed_time(self.timing), + ) + ) self.nlp_infeasible_counter += 1 if config.calculate_dual_at_solution: for c in fixed_nlp.MindtPy_utils.constraint_list: @@ -1232,7 +1246,7 @@ def handle_subproblem_infeasible(self, fixed_nlp, cb_opt=None): # elif var.has_lb() and abs(value(var) - var.lb) < config.absolute_bound_tolerance: # fixed_nlp.ipopt_zU_out[var] = -1 - config.logger.info('Solving feasibility problem') + # config.logger.info('Solving feasibility problem') feas_subproblem, feas_subproblem_results = self.solve_feasibility_subproblem() # TODO: do we really need this? if self.should_terminate: @@ -1366,6 +1380,18 @@ def solve_feasibility_subproblem(self): self.handle_feasibility_subproblem_tc( feas_soln.solver.termination_condition, MindtPy ) + config.logger.info( + self.fixed_nlp_log_formatter.format( + ' ', + self.nlp_iter, + 'Feasibility NLP', + value(feas_subproblem.MindtPy_utils.feas_obj), + self.primal_bound, + self.dual_bound, + self.rel_gap, + get_main_elapsed_time(self.timing), + ) + ) MindtPy.feas_opt.deactivate() for constr in MindtPy.nonlinear_constraint_list: constr.activate() diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 59490248e49..4a4b77767a9 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -988,8 +988,6 @@ def copy_var_list_values(from_list, to_list, config, v_to.set_value(0) elif v_to.is_integer() and (math.fabs(var_val - rounded_val) <= config.integer_tolerance): - print('var_val', var_val) - v_to.pprint() v_to.set_value(rounded_val) else: raise From 83069253086f5199c35ee22aadf1b1ea85fdf891 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Sat, 23 Sep 2023 15:08:00 -0400 Subject: [PATCH 0095/3044] add update_solver_timelimit --- pyomo/contrib/mindtpy/algorithm_base_class.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 514c2edaedb..0eb602bdf7e 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1618,6 +1618,7 @@ def solve_main(self): # setup main problem self.setup_main() mip_args = self.set_up_mip_solver() + update_solver_timelimit(self.mip_opt, config.mip_solver, self.timing, config) try: main_mip_results = self.mip_opt.solve( @@ -1675,6 +1676,9 @@ def solve_fp_main(self): config = self.config self.setup_fp_main() mip_args = self.set_up_mip_solver() + update_solver_timelimit( + self.mip_opt, config.mip_solver, self.timing, config + ) main_mip_results = self.mip_opt.solve( self.mip, From 472b5dfee6a58497b566d3d692d1fac209c6b4b3 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 26 Sep 2023 08:27:44 -0600 Subject: [PATCH 0096/3044] Save point: working on writer --- pyomo/solver/IPOPT.py | 3 +-- pyomo/solver/results.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index cce0017c5be..6501154d7ac 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -78,8 +78,7 @@ def version(self): universal_newlines=True, ) version = results.stdout.splitlines()[0] - version = version.split(' ')[1] - version = version.strip() + version = version.split(' ')[1].strip() version = tuple(int(i) for i in version.split('.')) return version diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index c8c92109040..2fa62027e6c 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -293,6 +293,7 @@ def parse_sol_file(file, results): while i < number_of_vars: line = file.readline() variables.append(float(line)) + # Parse the exit code line and capture it exit_code = [0, 0] line = file.readline() if line and ('objno' in line): @@ -306,8 +307,34 @@ def parse_sol_file(file, results): # Not sure if next two lines are needed # if isinstance(res.solver.message, str): # res.solver.message = res.solver.message.replace(':', '\\x3a') + if (exit_code[1] >= 0) and (exit_code[1] <= 99): + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.solution_status = SolutionStatus.optimal + elif (exit_code[1] >= 100) and (exit_code[1] <= 199): + exit_code_message = "Optimal solution indicated, but ERROR LIKELY!" + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.solution_status = SolutionStatus.optimal + if results.extra_info.solver_message: + results.extra_info.solver_message += '; ' + exit_code_message + else: + results.extra_info.solver_message = exit_code_message + elif (exit_code[1] >= 200) and (exit_code[1] <= 299): + results.termination_condition = TerminationCondition.locallyInfeasible + results.solution_status = SolutionStatus.infeasible + elif (exit_code[1] >= 300) and (exit_code[1] <= 399): + results.termination_condition = TerminationCondition.unbounded + results.solution_status = SolutionStatus.infeasible + elif (exit_code[1] >= 400) and (exit_code[1] <= 499): + results.solver.termination_condition = TerminationCondition.iterationLimit + elif (exit_code[1] >= 500) and (exit_code[1] <= 599): + exit_code_message = ( + "FAILURE: the solver stopped by an error condition " + "in the solver routines!" + ) + results.solver.termination_condition = TerminationCondition.error + return results - + return results def parse_yaml(): pass From 4061af9df05e2565931f93f3366adf08e9e6f998 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 2 Oct 2023 15:14:41 -0600 Subject: [PATCH 0097/3044] SAVE POINT: Adding Datetime checker --- pyomo/common/config.py | 11 +++++++++++ pyomo/solver/results.py | 9 +++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 61e4f682a2a..1e11fbdc431 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -18,6 +18,7 @@ import argparse import builtins +import datetime import enum import importlib import inspect @@ -203,6 +204,16 @@ def NonNegativeFloat(val): return ans +def Datetime(val): + """Domain validation function to check for datetime.datetime type. + + This domain will return the original object, assuming it is of the right type. + """ + if not isinstance(val, datetime.datetime): + raise ValueError(f"Expected datetime object, but received {type(val)}.") + return val + + class In(object): """In(domain, cast=None) Domain validation class admitting a Container of possible values diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 2fa62027e6c..8e4b6cf21a7 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -16,6 +16,7 @@ from pyomo.common.config import ( ConfigDict, ConfigValue, + Datetime, NonNegativeInt, In, NonNegativeFloat, @@ -213,9 +214,9 @@ def __init__( 'iteration_count', ConfigValue(domain=NonNegativeInt) ) self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict()) - # TODO: Implement type checking for datetime + self.timing_info.start_time: datetime = self.timing_info.declare( - 'start_time', ConfigValue() + 'start_time', ConfigValue(domain=Datetime) ) self.timing_info.wall_time: Optional[float] = self.timing_info.declare( 'wall_time', ConfigValue(domain=NonNegativeFloat) @@ -331,6 +332,10 @@ def parse_sol_file(file, results): "FAILURE: the solver stopped by an error condition " "in the solver routines!" ) + if results.extra_info.solver_message: + results.extra_info.solver_message += '; ' + exit_code_message + else: + results.extra_info.solver_message = exit_code_message results.solver.termination_condition = TerminationCondition.error return results From b5af408e17d0b65f4b270397817be5343f6edd3e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 2 Oct 2023 15:16:39 -0600 Subject: [PATCH 0098/3044] Swap FullLicense and LimitedLicense --- pyomo/solver/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index f7e5c4c58c5..07f19fbb58c 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -39,11 +39,11 @@ class SolverBase(abc.ABC): class Availability(enum.IntEnum): + FullLicense = 2 + LimitedLicense = 1 NotFound = 0 BadVersion = -1 BadLicense = -2 - FullLicense = 1 - LimitedLicense = 2 NeedsCompiledExtension = -3 def __bool__(self): From ef2c135862b65380ab02412839f892cfcafb711d Mon Sep 17 00:00:00 2001 From: jlgearh Date: Mon, 2 Oct 2023 20:38:15 -0600 Subject: [PATCH 0099/3044] - Updating aos_utils.py and adding test cases - Combined var_utils.py with aos_utils.py --- .../alternative_solutions/aos_utils.py | 196 +++++++++++++----- pyomo/contrib/alternative_solutions/obbt.py | 6 +- .../tests/test_aos_utils.py | 146 +++++++++++++ .../alternative_solutions/var_utils.py | 133 ------------ 4 files changed, 295 insertions(+), 186 deletions(-) create mode 100644 pyomo/contrib/alternative_solutions/tests/test_aos_utils.py delete mode 100644 pyomo/contrib/alternative_solutions/var_utils.py diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index 409522cacd2..94b91963722 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -14,16 +14,22 @@ from numpy.random import normal from numpy.linalg import norm -from pyomo.common.modeling import unique_component_name import pyomo.environ as pe +from pyomo.common.modeling import unique_component_name +from pyomo.common.collections import ComponentSet +import pyomo.util.vars_from_expressions as vfe def _get_active_objective(model): ''' Finds and returns the active objective function for a model. Currently assume that there is exactly one active objective. ''' - active_objs = [o for o in model.component_data_objects(pe.Objective, - active=True)] + + active_objs = [] + for o in model.component_data_objects(pe.Objective, active=True): + objs = o.values() if o.is_indexed() else (o,) + for obj in objs: + active_objs.append(obj) assert len(active_objs) == 1, \ "Model has {} active objective functions, exactly one is required.".\ format(len(active_objs)) @@ -37,63 +43,61 @@ def _add_aos_block(model, name='_aos_block'): return aos_block def _add_objective_constraint(aos_block, objective, objective_value, - rel_opt_gap, abs_gap): + rel_opt_gap, abs_opt_gap): ''' Adds a relative and/or absolute objective function constraint to the specified block. ''' + + assert rel_opt_gap is None or rel_opt_gap >= 0.0, \ + 'rel_opt_gap must be None of >= 0.0' + assert abs_opt_gap is None or abs_opt_gap >= 0.0, \ + 'abs_opt_gap must be None of >= 0.0' + objective_constraints = [] - if rel_opt_gap is not None or abs_gap is not None: - objective_is_min = objective.is_minimizing() - objective_expr = objective.expr - objective_sense = -1 + objective_is_min = objective.is_minimizing() + objective_expr = objective.expr + + objective_sense = -1 + if objective_is_min: + objective_sense = 1 + + if rel_opt_gap is not None: + objective_cutoff = objective_value + objective_sense * rel_opt_gap *\ + abs(objective_value) + if objective_is_min: - objective_sense = 1 - - if rel_opt_gap is not None: - objective_cutoff = objective_value * \ - (1 + objective_sense * rel_opt_gap) + aos_block.optimality_tol_rel = \ + pe.Constraint(expr=objective_expr <= \ + objective_cutoff) + else: + aos_block.optimality_tol_rel = \ + pe.Constraint(expr=objective_expr >= \ + objective_cutoff) + objective_constraints.append(aos_block.optimality_tol_rel) - if objective_is_min: - aos_block.optimality_tol_rel = \ - pe.Constraint(expr=objective_expr <= \ - objective_cutoff) - else: - aos_block.optimality_tol_rel = \ - pe.Constraint(expr=objective_expr >= \ - objective_cutoff) - objective_constraints.append(aos_block.optimality_tol_rel) + if abs_opt_gap is not None: + objective_cutoff = objective_value + objective_sense \ + * abs_opt_gap + + if objective_is_min: + aos_block.optimality_tol_abs = \ + pe.Constraint(expr=objective_expr <= \ + objective_cutoff) + else: + aos_block.optimality_tol_abs = \ + pe.Constraint(expr=objective_expr >= \ + objective_cutoff) + objective_constraints.append(aos_block.optimality_tol_abs) - if abs_gap is not None: - objective_cutoff = objective_value + objective_sense \ - * abs_gap - - if objective_is_min: - aos_block.optimality_tol_abs = \ - pe.Constraint(expr=objective_expr <= \ - objective_cutoff) - else: - aos_block.optimality_tol_abs = \ - pe.Constraint(expr=objective_expr >= \ - objective_cutoff) - objective_constraints.append(aos_block.optimality_tol_abs) return objective_constraints -def _get_max_solutions(max_solutions): - assert isinstance(max_solutions, (int, type(None))), \ - 'max_solutions parameter must be an integer or None' - if isinstance(max_solutions, int): - assert max_solutions >= 1, \ - ('max_solutions parameter must be an integer greater than or equal' - ' to 1' - ) - num_solutions = max_solutions - if max_solutions is None: - num_solutions = sys.maxsize - return num_solutions - def _get_random_direction(num_dimensions): + ''' + Get a unit vector of dimension num_dimensions by sampling from and + normalizing a standard multivariate Gaussian distribution. + ''' idx = 0 while idx < 100: samples = normal(size=num_dimensions) @@ -102,3 +106,99 @@ def _get_random_direction(num_dimensions): return samples / samples_norm idx += 1 raise Exception + +def _filter_model_variables(variable_set, var_generator, + include_continuous=True, include_binary=True, + include_integer=True, include_fixed=False): + '''Filters variables from a variable generator and adds them to a set.''' + for var in var_generator: + if var in variable_set or var.is_fixed() and not include_fixed: + continue + if (var.is_continuous() and include_continuous or + var.is_binary() and include_binary or + var.is_integer() and include_integer): + variable_set.add(var) + +def get_model_variables(model, components='all', include_continuous=True, + include_binary=True, include_integer=True, + include_fixed=False): + ''' + Gathers and returns all variables or a subset of variables from a Pyomo + model. + + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model. + components: 'all' or a collection Pyomo components + The components from which variables should be collected. 'all' + indicates that all variables will be included. Alternatively, a + collection of Pyomo Blocks, Constraints, or Variables (indexed or + non-indexed) from which variables will be gathered can be provided. + By default all variables in sub-Blocks will be added if a Block + element is provided. A tuple element with the format (Block, False) + indicates that only variables from the Block should be added but + not any of its sub-Blocks. + include_continuous : boolean + Boolean indicating that continuous variables should be included. + include_binary : boolean + Boolean indicating that binary variables should be included. + include_integer : boolean + Boolean indicating that integer variables should be included. + include_fixed : boolean + Boolean indicating that fixed variables should be included. + + Returns + ------- + variable_set + A Pyomo ComponentSet containing _GeneralVarData variables. + ''' + + variable_set = ComponentSet() + if components == 'all': + var_generator = vfe.get_vars_from_components(model, pe.Constraint, + include_fixed=\ + include_fixed) + _filter_model_variables(variable_set, var_generator, + include_continuous, include_binary, + include_integer, include_fixed) + else: + for comp in components: + if (hasattr(comp, 'ctype') and comp.ctype == pe.Block): + blocks = comp.values() if comp.is_indexed() else (comp,) + for item in blocks: + variables = vfe.get_vars_from_components(item, + pe.Constraint, include_fixed=include_fixed) + _filter_model_variables(variable_set, variables, + include_continuous, include_binary, include_integer, + include_fixed) + elif (isinstance(comp, tuple) and isinstance(comp[1], bool) and + hasattr(comp[0], 'ctype') and comp[0].ctype == pe.Block): + block = comp[0] + descend_into = pe.Block if comp[1] else False + blocks = block.values() if block.is_indexed() else (block,) + for item in blocks: + variables = vfe.get_vars_from_components(item, + pe.Constraint, include_fixed=include_fixed, + descend_into=descend_into) + _filter_model_variables(variable_set, variables, + include_continuous, include_binary, include_integer, + include_fixed) + elif hasattr(comp, 'ctype') and comp.ctype == pe.Constraint: + constraints = comp.values() if comp.is_indexed() else (comp,) + for item in constraints: + variables = pe.expr.identify_variables(item.expr, + include_fixed=include_fixed) + _filter_model_variables(variable_set, variables, + include_continuous, include_binary, include_integer, + include_fixed) + elif (hasattr(comp, 'ctype') and comp.ctype == pe.Var): + variables = comp.values() if comp.is_indexed() else (comp,) + _filter_model_variables(variable_set, variables, + include_continuous, include_binary, include_integer, + include_fixed) + else: + print(('No variables added for unrecognized component {}.'). + format(comp)) + + return variable_set \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 21267159463..c9ad93ced41 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -68,15 +68,11 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, if variables == 'all' or warmstart: all_variables = var_utils.get_model_variables(model, 'all', include_fixed=False) + variable_list = all_variables if warmstart: solutions = pe.ComponentMap() for var in all_variables: solutions[var] = [] - if variables == 'all': - variable_list = all_variables - else: - variable_list = var_utils.check_variables(model, variables, - include_fixed=False) num_vars = len(variable_list) print('Analyzing {} variables ({} total solves).'.format(num_vars, diff --git a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py new file mode 100644 index 00000000000..810b4d6dea4 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py @@ -0,0 +1,146 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.environ as pe +import pyomo.common.unittest as unittest +import pyomo.contrib.alternative_solutions.aos_utils as au + +class TestAOSUtilsUnit(unittest.TestCase): + def get_two_objective_model(self): + m = pe.ConcreteModel() + m.b1 = pe.Block() + m.b2 = pe.Block() + m.x = pe.Var() + m.y = pe.Var() + m.b1.o = pe.Objective(expr=m.x) + m.b2.o = pe.Objective([0,1]) + m.b2.o[0] = pe.Objective(expr=m.y) + m.b2.o[1] = pe.Objective(expr=m.x+m.y) + return m + + def test_multiple_objectives(self): + m = self.get_two_objective_model() + assert_text = ("Model has 3 active objective functions, exactly one " + "is required.") + with self.assertRaisesRegex(AssertionError, assert_text): + au._get_active_objective(m) + + def test_no_objectives(self): + m = self.get_two_objective_model() + m.b1.o.deactivate() + m.b2.o.deactivate() + assert_text = ("Model has 0 active objective functions, exactly one " + "is required.") + with self.assertRaisesRegex(AssertionError, assert_text): + au._get_active_objective(m) + + def test_one_objective(self): + m = self.get_two_objective_model() + m.b1.o.deactivate() + m.b2.o[0].deactivate() + self.assertEqual(m.b2.o[1], au._get_active_objective(m)) + + def test_aos_block(self): + m = self.get_two_objective_model() + block_name = 'test_block' + b = au._add_aos_block(m, block_name) + self.assertEqual(b.name, block_name) + self.assertEqual(b.ctype, pe.Block) + + def get_simple_model(self, sense = pe.minimize): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.o = pe.Objective(expr=m.x+m.y, sense=sense) + return m + + def test_no_obj_constraint(self): + m = self.get_simple_model() + cons = au._add_objective_constraint(m, m.o, 2, None, None) + self.assertEqual(cons, []) + self.assertEqual(m.find_component('optimality_tol_rel'), None) + self.assertEqual(m.find_component('optimality_tol_abs'), None) + + def test_min_rel_obj_constraint(self): + m = self.get_simple_model() + cons = au._add_objective_constraint(m, m.o, 2, 0.1, None) + self.assertEqual(len(cons), 1) + self.assertEqual(m.find_component('optimality_tol_rel'), cons[0]) + self.assertEqual(m.find_component('optimality_tol_abs'), None) + self.assertEqual(2.2, cons[0].upper) + self.assertEqual(None, cons[0].lower) + + def test_min_abs_obj_constraint(self): + m = self.get_simple_model() + cons = au._add_objective_constraint(m, m.o, 2, None, 1) + self.assertEqual(len(cons), 1) + self.assertEqual(m.find_component('optimality_tol_rel'), None) + self.assertEqual(m.find_component('optimality_tol_abs'), cons[0]) + self.assertEqual(3, cons[0].upper) + self.assertEqual(None, cons[0].lower) + + def test_min_both_obj_constraint(self): + m = self.get_simple_model() + cons = au._add_objective_constraint(m, m.o, -10, 0.3, 5) + m.pprint() + self.assertEqual(len(cons), 2) + self.assertEqual(m.find_component('optimality_tol_rel'), cons[0]) + self.assertEqual(m.find_component('optimality_tol_abs'), cons[1]) + self.assertEqual(-7, cons[0].upper) + self.assertEqual(None, cons[0].lower) + self.assertEqual(-5, cons[1].upper) + self.assertEqual(None, cons[1].lower) + + def test_max_both_obj_constraint(self): + m = self.get_simple_model(sense=pe.maximize) + cons = au._add_objective_constraint(m, m.o, -1, 0.3, 1) + self.assertEqual(len(cons), 2) + self.assertEqual(m.find_component('optimality_tol_rel'), cons[0]) + self.assertEqual(m.find_component('optimality_tol_abs'), cons[1]) + self.assertEqual(None, cons[0].upper) + self.assertEqual(-1.3, cons[0].lower) + self.assertEqual(None, cons[1].upper) + self.assertEqual(-2, cons[1].lower) + + def test_max_both_obj_constraint2(self): + m = self.get_simple_model(sense=pe.maximize) + cons = au._add_objective_constraint(m, m.o, 20, 0.5, 11) + self.assertEqual(len(cons), 2) + self.assertEqual(m.find_component('optimality_tol_rel'), cons[0]) + self.assertEqual(m.find_component('optimality_tol_abs'), cons[1]) + self.assertEqual(None, cons[0].upper) + self.assertEqual(10, cons[0].lower) + self.assertEqual(None, cons[1].upper) + self.assertEqual(9, cons[1].lower) + + def get_var_model(self): + m = pe.ConcreteModel() + m.b1 = pe.Block() + m.b2 = pe.Block() + m.b1.sb = pe.Block() + m.b2.sb = pe.Block() + m.c = pe.Var(domain=pe.Reals) + m.b = pe.Var(domain=pe.Binary) + m.i = pe.var(domain=pe.Integers) + m.c_f = pe.Var(domain=pe.Reals) + m.b_f = pe.Var(domain=pe.Binary) + m.i_f = pe.var(domain=pe.Integers) + m.c_f.fix(0) + m.b_f.fix(0) + m.i_f.fix(0) + m.b1.o = pe.Objective(expr=m.x) + m.b2.o = pe.Objective([0,1]) + m.b2.o[0] = pe.Objective(expr=m.y) + m.b2.o[1] = pe.Objective(expr=m.x+m.y) + return m + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/var_utils.py b/pyomo/contrib/alternative_solutions/var_utils.py deleted file mode 100644 index f8b37b40ebd..00000000000 --- a/pyomo/contrib/alternative_solutions/var_utils.py +++ /dev/null @@ -1,133 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.common.collections import ComponentSet -import pyomo.environ as pe -import pyomo.util.vars_from_expressions as vfe -import pyomo.contrib.alternative_solutions.aos_utils as aos_utils - -""" -This file provides a collection of utilites for gathering and filtering -variables from a model to support analysis of alternative solutions, and other -related tasks. -""" - -def _filter_model_variables(variable_set, var_generator, - include_continuous=True, include_binary=True, - include_integer=True, include_fixed=False): - """Filters variables from a variable generator and adds them to a set.""" - for var in var_generator: - if var in variable_set or var.is_fixed() and not include_fixed: - continue - if (var.is_continuous() and include_continuous or - var.is_binary() and include_binary or - var.is_integer() and include_integer): - variable_set.add(var) - -def get_model_variables(model, components='all', include_continuous=True, - include_binary=True, include_integer=True, - include_fixed=False): - ''' - Gathers and returns all or a subset of varaibles from a Pyomo model. - - Parameters - ---------- - model : ConcreteModel - A concrete Pyomo model. - components: 'all' or a collection Pyomo components - The components from which variables should be collected. 'all' - indicates that all variables will be included. Alternatively, a - collection of Pyomo Blocks, Constraints, or Variables (indexed or - non-indexed) from which variables will be gathered can be provided. - By default all variables in sub-Blocks will be added if a Block - element is provided. A tuple element with the format (Block, False) - indicates that only variables from the Block should be added but - not any of its sub-Blocks. - include_continuous : boolean - Boolean indicating that continuous variables should be included. - include_binary : boolean - Boolean indicating that binary variables should be included. - include_integer : boolean - Boolean indicating that integer variables should be included. - include_fixed : boolean - Boolean indicating that fixed variables should be included. - - Returns - ------- - variable_set - A Pyomo ComponentSet containing _GeneralVarData variables. - ''' - - # Validate inputs - assert isinstance(include_continuous, bool), \ - 'include_continuous must be a Boolean' - assert isinstance(include_binary, bool), 'include_binary must be a Boolean' - assert isinstance(include_integer, bool), \ - 'include_integer must be a Boolean' - assert isinstance(include_fixed, bool), 'include_fixed must be a Boolean' - - # Gather variables - variable_set = ComponentSet() - if components == 'all': - var_generator = vfe.get_vars_from_components(model, pe.Constraint, - include_fixed=\ - include_fixed) - _filter_model_variables(variable_set, var_generator, - include_continuous, include_binary, - include_integer, include_fixed) - else: - assert hasattr(components, '__iter__'), \ - ('components parameters must be an iterable collection of Pyomo' - 'objects' - ) - - for comp in components: - if (hasattr(comp, 'ctype') and comp.ctype == pe.Block): - blocks = comp.values() if comp.is_indexed() else (comp,) - for item in blocks: - variables = vfe.get_vars_from_components(item, - pe.Constraint, include_fixed=include_fixed) - _filter_model_variables(variable_set, variables, - include_continuous, include_binary, include_integer, - include_fixed) - elif (isinstance(comp, tuple) and isinstance(comp[1], bool) and - hasattr(comp[0], 'ctype') and comp[0].ctype == pe.Block): - block = comp[0] - descend_into = pe.Block if comp[1] else False - blocks = block.values() if block.is_indexed() else (block,) - for item in blocks: - variables = vfe.get_vars_from_components(item, - pe.Constraint, include_fixed=include_fixed, - descend_into=descend_into) - _filter_model_variables(variable_set, variables, - include_continuous, include_binary, include_integer, - include_fixed) - elif hasattr(comp, 'ctype') and comp.ctype == pe.Constraint: - constraints = comp.values() if comp.is_indexed() else (comp,) - for item in constraints: - variables = pe.expr.identify_variables(item.expr, - include_fixed=include_fixed) - _filter_model_variables(variable_set, variables, - include_continuous, include_binary, include_integer, - include_fixed) - elif (hasattr(comp, 'ctype') and comp.ctype == pe.Var): - variables = comp.values() if comp.is_indexed() else (comp,) - _filter_model_variables(variable_set, variables, - include_continuous, include_binary, include_integer, - include_fixed) - else: - print(('No variables added for unrecognized component {}.'). - format(comp)) - - return variable_set - -def check_variables(model, variables, include_fixed=False): - pass \ No newline at end of file From 0b72a2148f3fab5a1037be0e0405e37cb105f6db Mon Sep 17 00:00:00 2001 From: jlgearh Date: Wed, 4 Oct 2023 20:39:39 -0600 Subject: [PATCH 0100/3044] - Added test cases for AOS utils --- .../alternative_solutions/aos_utils.py | 21 ++-- .../tests/test_aos_utils.py | 119 +++++++++++++++--- 2 files changed, 116 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index 94b91963722..c7102c15690 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -98,14 +98,18 @@ def _get_random_direction(num_dimensions): Get a unit vector of dimension num_dimensions by sampling from and normalizing a standard multivariate Gaussian distribution. ''' + + iterations = 1000 + min_norm = 1e-4 idx = 0 - while idx < 100: + while idx < iterations: samples = normal(size=num_dimensions) samples_norm = norm(samples) if samples_norm > 1e-4: return samples / samples_norm idx += 1 - raise Exception + raise Exception(("Generated {} sequential Gaussian draws with a norm of " + "less than {}.".format(iterations, min_norm))) def _filter_model_variables(variable_set, var_generator, include_continuous=True, include_binary=True, @@ -154,9 +158,10 @@ def get_model_variables(model, components='all', include_continuous=True, A Pyomo ComponentSet containing _GeneralVarData variables. ''' + component_list = (pe.Objective, pe.Constraint) variable_set = ComponentSet() if components == 'all': - var_generator = vfe.get_vars_from_components(model, pe.Constraint, + var_generator = vfe.get_vars_from_components(model, component_list, include_fixed=\ include_fixed) _filter_model_variables(variable_set, var_generator, @@ -168,23 +173,23 @@ def get_model_variables(model, components='all', include_continuous=True, blocks = comp.values() if comp.is_indexed() else (comp,) for item in blocks: variables = vfe.get_vars_from_components(item, - pe.Constraint, include_fixed=include_fixed) + component_list, include_fixed=include_fixed) _filter_model_variables(variable_set, variables, include_continuous, include_binary, include_integer, include_fixed) - elif (isinstance(comp, tuple) and isinstance(comp[1], bool) and - hasattr(comp[0], 'ctype') and comp[0].ctype == pe.Block): + elif (isinstance(comp, tuple) and hasattr(comp[0], 'ctype') \ + and comp[0].ctype == pe.Block): block = comp[0] descend_into = pe.Block if comp[1] else False blocks = block.values() if block.is_indexed() else (block,) for item in blocks: variables = vfe.get_vars_from_components(item, - pe.Constraint, include_fixed=include_fixed, + component_list, include_fixed=include_fixed, descend_into=descend_into) _filter_model_variables(variable_set, variables, include_continuous, include_binary, include_integer, include_fixed) - elif hasattr(comp, 'ctype') and comp.ctype == pe.Constraint: + elif hasattr(comp, 'ctype') and comp.ctype in component_list: constraints = comp.values() if comp.is_indexed() else (comp,) for item in constraints: variables = pe.expr.identify_variables(item.expr, diff --git a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py index 810b4d6dea4..6cb76b6e5b8 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py +++ b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py @@ -10,6 +10,7 @@ # ___________________________________________________________________________ import pyomo.environ as pe +from pyomo.common.collections import ComponentSet import pyomo.common.unittest as unittest import pyomo.contrib.alternative_solutions.aos_utils as au @@ -90,7 +91,6 @@ def test_min_abs_obj_constraint(self): def test_min_both_obj_constraint(self): m = self.get_simple_model() cons = au._add_objective_constraint(m, m.o, -10, 0.3, 5) - m.pprint() self.assertEqual(len(cons), 2) self.assertEqual(m.find_component('optimality_tol_rel'), cons[0]) self.assertEqual(m.find_component('optimality_tol_abs'), cons[1]) @@ -122,25 +122,112 @@ def test_max_both_obj_constraint2(self): self.assertEqual(9, cons[1].lower) def get_var_model(self): + + indices = [0,1,2,3] + m = pe.ConcreteModel() + m.b1 = pe.Block() m.b2 = pe.Block() - m.b1.sb = pe.Block() - m.b2.sb = pe.Block() - m.c = pe.Var(domain=pe.Reals) - m.b = pe.Var(domain=pe.Binary) - m.i = pe.var(domain=pe.Integers) - m.c_f = pe.Var(domain=pe.Reals) - m.b_f = pe.Var(domain=pe.Binary) - m.i_f = pe.var(domain=pe.Integers) - m.c_f.fix(0) - m.b_f.fix(0) - m.i_f.fix(0) - m.b1.o = pe.Objective(expr=m.x) - m.b2.o = pe.Objective([0,1]) - m.b2.o[0] = pe.Objective(expr=m.y) - m.b2.o[1] = pe.Objective(expr=m.x+m.y) + m.b1.sb1 = pe.Block() + m.b2.sb2 = pe.Block() + + m.x = pe.Var(domain=pe.Reals) + m.b1.y = pe.Var(domain=pe.Binary) + m.b2.z = pe.Var(domain=pe.Integers) + + m.x_f = pe.Var(domain=pe.Reals) + m.b1.y_f = pe.Var(domain=pe.Binary) + m.b2.z_f = pe.Var(domain=pe.Integers) + m.x_f.fix(0) + m.b1.y_f.fix(0) + m.b2.z_f.fix(0) + + m.b1.sb1.x_l = pe.Var(indices, domain=pe.Reals) + m.b1.sb1.y_l = pe.Var(indices, domain=pe.Binary) + m.b2.sb2.z_l = pe.Var(indices, domain=pe.Integers) + + m.b1.sb1.x_l[3].fix(0) + m.b1.sb1.y_l[3].fix(0) + m.b2.sb2.z_l[3].fix(0) + + vars_minus_x = [m.b1.y, m.b2.z, m.x_f, m.b1.y_f, m.b2.z_f] + \ + [m.b1.sb1.x_l[i] for i in indices] + \ + [m.b1.sb1.y_l[i] for i in indices] + \ + [m.b2.sb2.z_l[i] for i in indices] + + m.con = pe.Constraint(expr=sum(v for v in vars_minus_x) <= 1) + m.obj = pe.Objective(expr=m.x) + + m.all_vars = ComponentSet([m.x] + vars_minus_x) + m.unfixed_vars = ComponentSet([var for var in m.all_vars \ + if not var.is_fixed()]) + return m + + def test_get_all_variables_unfixed(self): + m = self.get_var_model() + var = au.get_model_variables(m) + self.assertEqual(var, m.unfixed_vars) + + def test_get_all_variables(self): + m = self.get_var_model() + var = au.get_model_variables(m, include_fixed=True) + self.assertEqual(var, m.all_vars) + + def test_get_all_continuous(self): + m = self.get_var_model() + var = au.get_model_variables(m, + include_continuous=True, + include_binary=False, + include_integer=False) + continuous_vars = ComponentSet(var for var in m.unfixed_vars \ + if var.is_continuous()) + self.assertEqual(var, continuous_vars) + + def test_get_all_binary(self): + m = self.get_var_model() + var = au.get_model_variables(m, + include_continuous=False, + include_binary=True, + include_integer=False) + binary_vars = ComponentSet(var for var in m.unfixed_vars \ + if var.is_binary()) + self.assertEqual(var, binary_vars) + + def test_get_all_integer(self): + m = self.get_var_model() + var = au.get_model_variables(m, + include_continuous=False, + include_binary=False, + include_integer=True) + continuous_vars = ComponentSet(var for var in m.unfixed_vars \ + if var.is_integer()) + self.assertEqual(var, continuous_vars) + + def test_get_specific_vars(self): + m = self.get_var_model() + components = [m.x, m.b1.sb1.y_l[0], m.b2.sb2.z_l] + var = au.get_model_variables(m, components=components) + specific_vars = ComponentSet([m.x, m.b1.sb1.y_l[0], m.b2.sb2.z_l[0], + m.b2.sb2.z_l[1], m.b2.sb2.z_l[2]]) + self.assertEqual(var, specific_vars) + + def test_get_block_vars(self): + m = self.get_var_model() + components = [m.b2.sb2.z_l, (m.b1, False)] + var = au.get_model_variables(m, components=components) + specific_vars = ComponentSet([m.b1.y, m.b2.sb2.z_l[0], + m.b2.sb2.z_l[1], m.b2.sb2.z_l[2]]) + self.assertEqual(var, specific_vars) + + def test_get_constraint_vars(self): + m = self.get_var_model() + components = [m.con, m.obj] + var = au.get_model_variables(m, components=components) + print(var) + print(m.unfixed_vars) + self.assertEqual(var, m.unfixed_vars) if __name__ == '__main__': unittest.main() \ No newline at end of file From 3a98410c3e3d88fcd5fc936fd29d31c49f168efb Mon Sep 17 00:00:00 2001 From: jlgearh Date: Thu, 5 Oct 2023 16:46:47 -0600 Subject: [PATCH 0101/3044] - Finished aos_utils and solution test cases - Working on obbt and solution pool code and tests --- .../alternative_solutions/aos_utils.py | 10 +- .../alternative_solutions/comparison.py | 23 ---- pyomo/contrib/alternative_solutions/obbt.py | 18 ++- .../alternative_solutions/obbt_test.py | 12 -- .../contrib/alternative_solutions/solnpool.py | 73 +++++------ .../contrib/alternative_solutions/solution.py | 81 +++++------- .../alternative_solutions/tests/obbt_test.py | 124 ------------------ .../tests/test_aos_utils.py | 4 +- .../alternative_solutions/tests/test_cases.py | 88 ++++++++++--- .../alternative_solutions/tests/test_obbt.py | 38 ++++++ 10 files changed, 190 insertions(+), 281 deletions(-) delete mode 100644 pyomo/contrib/alternative_solutions/comparison.py delete mode 100644 pyomo/contrib/alternative_solutions/obbt_test.py delete mode 100644 pyomo/contrib/alternative_solutions/tests/obbt_test.py create mode 100644 pyomo/contrib/alternative_solutions/tests/test_obbt.py diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index c7102c15690..6867c570669 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import sys - from numpy.random import normal from numpy.linalg import norm @@ -139,10 +137,10 @@ def get_model_variables(model, components='all', include_continuous=True, indicates that all variables will be included. Alternatively, a collection of Pyomo Blocks, Constraints, or Variables (indexed or non-indexed) from which variables will be gathered can be provided. - By default all variables in sub-Blocks will be added if a Block - element is provided. A tuple element with the format (Block, False) - indicates that only variables from the Block should be added but - not any of its sub-Blocks. + If a Block is provided, all variables associated with constraints + in that that block and its sub-blocks will be returned. To exclude + sub-blocks, a tuple element with the format (Block, False) can be + used. include_continuous : boolean Boolean indicating that continuous variables should be included. include_binary : boolean diff --git a/pyomo/contrib/alternative_solutions/comparison.py b/pyomo/contrib/alternative_solutions/comparison.py deleted file mode 100644 index 9feff3b88d7..00000000000 --- a/pyomo/contrib/alternative_solutions/comparison.py +++ /dev/null @@ -1,23 +0,0 @@ -import math - - -def consensus(solutions, ignore_zeros=True): - # - # Summarize the average value of solution values - # - # This currently assumes all solutions have the same variables - # - nsolutions = len(solutions) - assert nsolutions > 1, "Need more than one solution to form a consensus pattern" - keys = list(sorted(solutions[0]['variables'].keys())) - - total = {key:solutions[0]['variables'][key] for key in keys} - for i in range(1, nsolutions): - total = {key:(total[key] + solutions[i]['variables'][key]) for key in keys} - - mean = {key:total[key]/nsolutions for key in keys} - - if ignore_zeros: - return {key:mean[key] for key in keys if math.fabs(mean[key]) > 1e-7} - else: - return mean diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index c9ad93ced41..22727b3564c 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -10,11 +10,11 @@ # ___________________________________________________________________________ import pyomo.environ as pe -from pyomo.contrib.alternative_solutions import aos_utils, var_utils +from pyomo.contrib.alternative_solutions import aos_utils def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, - refine_discrete_bounds=True, warmstart=True, solver='gurobi', - solver_options={}, tee=False): + refine_discrete_bounds=False, warmstart=True, + solver='gurobi', solver_options={}, tee=False): ''' Calculates the bounds on each variable by solving a series of min and max optimization problems where each variable is used as the objective function @@ -25,7 +25,7 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, ---------- model : ConcreteModel A concrete Pyomo model. - variables: 'all' or a collection of Pyomo _GenereralVarData variables + variables: 'all' or a collection of Pyomo _GeneralVarData variables The variables for which bounds will be generated. 'all' indicates that all variables will be included. Alternatively, a collection of _GenereralVarData variables can be provided. @@ -48,11 +48,6 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, The solver to be used. solver_options : dict Solver option-value pairs to be passed to the solver. - use_appsi : boolean - Boolean indicating if the the APPSI persistent solver interface - should be used. To use APPSI pass the base solver name and set this - input to true. E.g., passing 'gurobi' as the solver and a value of - true will create an instance of an 'appsi_gurobi' solver. tee : boolean Boolean indicating that the solver output should be displayed. @@ -66,7 +61,7 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, print('STARTING OBBT ANALYSIS') if variables == 'all' or warmstart: - all_variables = var_utils.get_model_variables(model, 'all', + all_variables = aos_utils.get_model_variables(model, 'all', include_fixed=False) variable_list = all_variables if warmstart: @@ -89,6 +84,9 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, results = opt.solve(model, warmstart=warmstart, tee=tee) status = results.solver.status condition = results.solver.termination_condition + print('OBBT cannot be applied, SolverStatus = {}, ' + 'TerminationCondition = {}'.format(status.value, + condition.value)) if condition != pe.TerminationCondition.optimal: raise Exception(('OBBT cannot be applied, SolverStatus = {}, ' 'TerminationCondition = {}').format(status.value, diff --git a/pyomo/contrib/alternative_solutions/obbt_test.py b/pyomo/contrib/alternative_solutions/obbt_test.py deleted file mode 100644 index 911d4ff1e07..00000000000 --- a/pyomo/contrib/alternative_solutions/obbt_test.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Sep 7 13:07:51 2023 - -@author: jlgearh -""" - -from obbt import obbt_analysis -from tests.test_cases import get_continuous_prob_1 - -m = get_continuous_prob_1() -results, solutions = obbt_analysis(m, warmstart=True, solver='cplex') \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 59218c844f9..de510ba541b 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -9,12 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.contrib import appsi -from pyomo.contrib.alternative_solutions import aos_utils, var_utils, solution +import pyomo.environ as pe +from pyomo.contrib.alternative_solutions import aos_utils, solution -def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=None, +def gurobi_generate_solutions(model, num_solutions=10, rel_opt_gap=None, abs_opt_gap=None, search_mode=2, - solver_options={}): + solver_options={}, tee=True): ''' Finds alternative optimal solutions for discrete variables using Gurobi's built-in Solution Pool capability. See the Gurobi Solution Pool @@ -23,10 +23,9 @@ def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=None, ---------- model : ConcreteModel A concrete Pyomo model. - max_solutions : int or None - The maximum number of solutions to generate. None indictes no upper - limit. Note, using None could lead to a large number of solutions. - This parameter maps to the PoolSolutions parameter in Gurobi. + num_solutions : int + The maximum number of solutions to generate. This parameter maps to + the PoolSolutions parameter in Gurobi. rel_opt_gap : non-negative float or None The relative optimality gap for allowable alternative solutions. None implies that there is no limit on the relative optimality gap @@ -46,6 +45,8 @@ def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=None, parameter maps to the PoolSearchMode in Gurobi. solver_options : dict Solver option-value pairs to be passed to the Gurobi solver. + tee : boolean + Boolean indicating that the solver output should be displayed. Returns ------- @@ -53,42 +54,34 @@ def gurobi_generate_solutions(model, max_solutions=10, rel_opt_gap=None, A list of Solution objects. [Solution] ''' - - # Input validation - num_solutions = aos_utils._get_max_solutions(max_solutions) - assert (isinstance(rel_opt_gap, (float, int)) and rel_opt_gap >= 0) or \ - isinstance(rel_opt_gap, type(None)), \ - 'rel_opt_gap must be a non-negative float or None' - assert (isinstance(abs_opt_gap, (float, int)) and abs_opt_gap >= 0) or \ - isinstance(abs_opt_gap, type(None)), \ - 'abs_opt_gap must be a non-negative float or None' - assert search_mode in [0, 1, 2], 'search_mode must be 0, 1, or 2' - # Configure solver and solve model - opt = appsi.solvers.Gurobi() - opt.config.stream_solver = True - opt.set_instance(model) - opt.set_gurobi_param('PoolSolutions', num_solutions) - opt.set_gurobi_param('PoolSearchMode', search_mode) + opt = pe.SolverFactory('gurobi_appsi') + + for parameter, value in solver_options.items(): + opt.options[parameter] = value + opt.options('PoolSolutions', num_solutions) + opt.options('PoolSearchMode', search_mode) if rel_opt_gap is not None: - opt.set_gurobi_param('PoolGap', rel_opt_gap) + opt.options('PoolGap', rel_opt_gap) if abs_opt_gap is not None: - opt.set_gurobi_param('PoolGapAbs', abs_opt_gap) - for parameter, value in solver_options.items(): - opt.set_gurobi_param(parameter, abs_opt_gap) - results = opt.solve(model) - assert results.termination_condition == \ - appsi.base.TerminationCondition.optimal, \ - 'Solver terminated with conditions {}.'.format( - results.termination_condition) + opt.options('PoolGapAbs', abs_opt_gap) + + results = opt.solve(model, tee=tee) + status = results.solver.status + condition = results.solver.termination_condition - # Get model solutions - solution_count = opt.get_model_attr('SolCount') - print("Gurobi found {} solutions.".format(solution_count)) - variables = var_utils.get_model_variables(model, 'all', include_fixed=True) solutions = [] - for i in range(solution_count): - results.solution_loader.load_vars(solution_number=i) - solutions.append(solution.Solution(model, variables)) + if condition == pe.TerminationCondition.optimal: + solution_count = opt.get_model_attr('SolCount') + print("{} solutions found.".format(solution_count)) + variables = aos_utils.get_model_variables(model, 'all', + include_fixed=True) + for i in range(solution_count): + results.solution_loader.load_vars(solution_number=i) + solutions.append(solution.Solution(model, variables)) + else: + print(('Model cannot be solved, SolverStatus = {}, ' + 'TerminationCondition = {}').format(status.value, + condition.value)) return solutions \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/solution.py b/pyomo/contrib/alternative_solutions/solution.py index 54475593187..753f09cc9d0 100644 --- a/pyomo/contrib/alternative_solutions/solution.py +++ b/pyomo/contrib/alternative_solutions/solution.py @@ -11,7 +11,7 @@ import pyomo.environ as pe from pyomo.common.collections import ComponentMap, ComponentSet -from pyomo.contrib.alternative_solutions import aos_utils, var_utils +from pyomo.contrib.alternative_solutions import aos_utils class Solution: """ @@ -38,8 +38,7 @@ def get_objective_name_values(self): Get a dictionary of objective name-objective value pairs. """ - def __init__(self, model, variable_list, ignore_fixed_vars=False, - round_discrete_vars=True): + def __init__(self, model, variable_list, include_fixed=True): """ Constructs a Pyomo Solution object. @@ -47,62 +46,48 @@ def __init__(self, model, variable_list, ignore_fixed_vars=False, ---------- model : ConcreteModel A concrete Pyomo model. - variables: A collection of Pyomo _GenereralVarData variables + variable_list: A collection of Pyomo _GenereralVarData variables The variables for which the solution will be stored. - ignore_fixed_vars : boolean - Boolean indicating that fixed variables should not be added to - the solution. - round_discrete_vars : boolean - Boolean indicating that discrete values should be rounded to - the nearest integer in the solutions results. + include_fixed : boolean + Boolean indicating that fixed variables should be added to the + solution. """ - - aos_utils._is_concrete_model(model) - assert isinstance(ignore_fixed_vars, bool), \ - 'ignore_fixed_vars must be a Boolean' - assert isinstance(round_discrete_vars, bool), \ - 'round_discrete_vars must be a Boolean' - + self.variables = ComponentMap() self.fixed_vars = ComponentSet() for var in variable_list: - if ignore_fixed_vars and var.is_fixed(): - continue - if var.is_continuous() or not round_discrete_vars: - self.variables[var] = pe.value(var) - else: - self.variables[var] = round(pe.value(var)) - if var.is_fixed(): + is_fixed = var.is_fixed() + if is_fixed: self.fixed_vars.add(var) - - self.objectives = ComponentMap() - # TODO: Should inactive objectives be included? - for obj in model.component_data_objects(pe.Objective, active=True): - self.objectives[obj] = pe.value(obj) + if include_fixed or not is_fixed: + self.variables[var] = pe.value(var) - def pprint(self): - '''Print the solution variable and objective values.''' - fixed_string = "Yes" - print("Variable, Value, Fixed?") + obj = aos_utils._get_active_objective(model) + self.objective = (obj, pe.value(obj)) + + def _round_variable_value(self, variable, value, round_discrete=True): + return value if not round_discrete or variable.is_continuous() \ + else round(value) + + def pprint(self, round_discrete=True): + '''Print the solution variables and objective values.''' + fixed_string = " (Fixed)" + print() + print("Variable\tValue") for variable, value in self.variables.items(): - if variable in self.fixed_vars: - print("{}, {}, {}".format(variable.name, value, fixed_string)) - else: - print("{}, {}".format(variable.name, value)) + fxd = fixed_string if variable in self.fixed_vars else "" + val = self._round_variable_value(variable, value, round_discrete) + print("{}\t\t\t{}{}".format(variable.name, val, fxd)) print() - print("Objective, Value") - for objective, value in self.objectives.items(): - print("{}, {}".format(objective.name, value)) + print("Objective value for {} = {}".format(*self.objective)) - def get_variable_name_values(self, ignore_fixed_vars=False): + def get_variable_name_values(self, include_fixed=True, + round_discrete=True): '''Get a dictionary of variable name-variable value pairs.''' - return {var.name: value for var, value in self.variables.items() if - not (ignore_fixed_vars and var in self.fixed_vars)} + return {var.name: self._round_variable_value(var, val, round_discrete) + for var, val in self.variables.items() \ + if include_fixed or not var in self.fixed_vars} def get_fixed_variable_names(self): '''Get a list of fixed-variable names.''' - return [var.name for var in self.fixed_vars] - - def get_objective_name_values(self): - '''Get a dictionary of objective name-objective value pairs.''' - return {obj.name: value for obj, value in self.objectives.items()} \ No newline at end of file + return [var.name for var in self.fixed_vars] \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/obbt_test.py b/pyomo/contrib/alternative_solutions/tests/obbt_test.py deleted file mode 100644 index 43946b4afbb..00000000000 --- a/pyomo/contrib/alternative_solutions/tests/obbt_test.py +++ /dev/null @@ -1,124 +0,0 @@ - - -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -"""Tests for the GDPopt solver plugin.""" - -# from contextlib import redirect_stdout -# from io import StringIO -# import logging -# from math import fabs -# from os.path import join, normpath - -# import pyomo.common.unittest as unittest -# from pyomo.common.log import LoggingIntercept -# from pyomo.common.collections import Bunch -# from pyomo.common.config import ConfigDict, ConfigValue -# from pyomo.common.fileutils import import_file, PYOMO_ROOT_DIR -# from pyomo.contrib.appsi.solvers.gurobi import Gurobi -# from pyomo.contrib.gdpopt.create_oa_subproblems import ( -# add_util_block, add_disjunct_list, add_constraints_by_disjunct, -# add_global_constraint_list) -# import pyomo.contrib.gdpopt.tests.common_tests as ct -# from pyomo.contrib.gdpopt.util import is_feasible, time_code -# from pyomo.contrib.mcpp.pyomo_mcpp import mcpp_available -# from pyomo.contrib.gdpopt.solve_discrete_problem import ( -# solve_MILP_discrete_problem, distinguish_mip_infeasible_or_unbounded) -# from pyomo.environ import ( -# Block, ConcreteModel, Constraint, Integers, LogicalConstraint, maximize, -# Objective, RangeSet, TransformationFactory, SolverFactory, sqrt, value, Var) -# from pyomo.gdp import Disjunct, Disjunction -# from pyomo.gdp.tests import models -# from pyomo.opt import TerminationCondition - -class TestGDPoptUnit(unittest.TestCase): - """Real unit tests for GDPopt""" - - #@unittest.skipUnless(SolverFactory(mip_solver).available(), - # "MIP solver not available") - def test_continuous_2d(self): - m = ConcreteModel() - m.GDPopt_utils = Block() - m.x = Var(bounds=(-1, 10)) - m.y = Var(bounds=(2, 3)) - m.z = Var() - # Include a disjunction so that we don't default to just a MIP solver - m.d = Disjunction(expr=[ - [m.x + m.y >= 5], [m.x - m.y <= 3] - ]) - m.o = Objective(expr=m.z) - m.GDPopt_utils.variable_list = [m.x, m.y, m.z] - m.GDPopt_utils.disjunct_list = [m.d._autodisjuncts[0], - m.d._autodisjuncts[1]] - output = StringIO() - with LoggingIntercept(output, 'pyomo.contrib.gdpopt', logging.WARNING): - solver = SolverFactory('gdpopt.loa') - dummy = Block() - dummy.timing = Bunch() - with time_code(dummy.timing, 'main', is_main_timer=True): - tc = solve_MILP_discrete_problem( - m.GDPopt_utils, - dummy, - solver.CONFIG(dict(mip_solver=mip_solver))) - self.assertIn("Discrete problem was unbounded. Re-solving with " - "arbitrary bound values", output.getvalue().strip()) - self.assertIs(tc, TerminationCondition.unbounded) - -if __name__ == '__main__': - unittest.main() - - -# # -*- coding: utf-8 -*- -# """ -# Created on Thu Aug 4 15:59:24 2022 - -# @author: jlgearh -# """ -# import random - -# import pyomo.environ as pe - -# from pyomo.contrib.alternative_solutions.obbt import obbt_analysis - -# def get_random_knapsack_model(num_x_vars, num_y_vars, budget_pct, seed=1000): -# random.seed(seed) - -# W = budget_pct * (num_x_vars + num_y_vars) / 2 - - -# model = pe.ConcreteModel() - -# model.X_INDEX = pe.RangeSet(1,num_x_vars) -# model.Y_INDEX = pe.RangeSet(1,num_y_vars) - -# model.wu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) -# model.vu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) -# model.x = pe.Var(model.X_INDEX, within=pe.NonNegativeIntegers) - -# model.b = pe.Block() -# model.b.wl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) -# model.b.vl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) -# model.b.y = pe.Var(model.Y_INDEX, within=pe.NonNegativeReals) - -# model.o = pe.Objective(expr=sum(model.vu[i]*model.x[i] for i in model.X_INDEX) + \ -# sum(model.b.vl[i]*model.b.y[i] for i in model.Y_INDEX), sense=pe.maximize) -# model.c = pe.Constraint(expr=sum(model.wu[i]*model.x[i] for i in model.X_INDEX) + \ -# sum(model.b.wl[i]*model.b.y[i] for i in model.Y_INDEX)<= W) - -# return model - -# model = get_random_knapsack_model(4, 4, 0.2) -# result = obbt_analysis(model, variables='all', rel_opt_gap=None, -# abs_gap=None, already_solved=False, -# solver='gurobi', solver_options={}, -# use_persistent_solver = False, tee=True, -# refine_bounds=False) \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py index 6cb76b6e5b8..365b6ff1fae 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py +++ b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py @@ -157,6 +157,8 @@ def get_var_model(self): [m.b2.sb2.z_l[i] for i in indices] m.con = pe.Constraint(expr=sum(v for v in vars_minus_x) <= 1) + m.b1.con = pe.Constraint(expr=m.b1.y<= 1) + m.b1.sb1.con = pe.Constraint(expr=m.b1.sb1.y_l[0]<= 1) m.obj = pe.Objective(expr=m.x) m.all_vars = ComponentSet([m.x] + vars_minus_x) @@ -225,8 +227,6 @@ def test_get_constraint_vars(self): m = self.get_var_model() components = [m.con, m.obj] var = au.get_model_variables(m, components=components) - print(var) - print(m.unfixed_vars) self.assertEqual(var, m.unfixed_vars) if __name__ == '__main__': diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index f5f775d8054..d577d001e50 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -10,10 +10,13 @@ # ___________________________________________________________________________ import random +from itertools import product + +import numpy as np import pyomo.environ as pe -def get_continuous_prob_1(discrete_x=False, discrete_y=False): +def get_2d_diamond_problem(discrete_x=False, discrete_y=False): m = pe.ConcreteModel() m.x = pe.Var(within=pe.Integers if discrete_x else pe.Reals) m.y = pe.Var(within=pe.Integers if discrete_y else pe.Reals) @@ -24,29 +27,82 @@ def get_continuous_prob_1(discrete_x=False, discrete_y=False): m.c2 = pe.Constraint(expr= 5/9 * m.x - 5 <= m.y) m.c3 = pe.Constraint(expr= 2/9 * m.x + 2 >= m.y) m.c4 = pe.Constraint(expr= -1/2 * m.x + 3 >= m.y) - m.c5 = pe.Constraint(expr= 2.30769230769231 >= m.y) - - return m + #m.c5 = pe.Constraint(expr= 2.30769230769231 >= m.y) -def knapsack(N): - random.seed(1000) + m.extreme_points = {(0.737704918, -4.590163934), + (-5.869565217, 0.695652174), + (1.384615385, 2.307692308), + (7.578947368, -0.789473684)} - N = N - W = N/10.0 + m.continuous_bounds = pe.ComponentMap() + m.continuous_bounds[m.x] = (-5.869565217, 7.578947368) + m.continuous_bounds[m.y] = (-4.590163934, 2.307692308) + return m - model = pe.ConcreteModel() +def get_aos_test_knapsack(var_max, weights, values, capacity_fraction): + assert len(weights) == len(values), \ + 'weights and values must be the same length.' + assert 0 <= capacity_fraction and capacity_fraction <= 1, \ + 'capacity_fraction must be between 0 and 1.' + + num_vars = len(weights) + capacity = sum(weights) * var_max * capacity_fraction + + m = pe.ConcreteModel() + m.i = pe.RangeSet(0,num_vars-1) + m.x = pe.Var(m.i, within=pe.NonNegativeIntegers, bounds=(0,var_max)) - model.INDEX = pe.RangeSet(1,N) + m.o = pe.Objective(expr=sum(values[i]*m.x[i] for i in m.i), + sense=pe.maximize) - model.w = pe.Param(model.INDEX, initialize=lambda model, i : random.uniform(0.0,1.0), within=pe.Reals) + m.c = pe.Constraint(expr=sum(weights[i]*m.x[i] for i in m.i) <= capacity) + + var_domain = var_values = range(var_max+1) + all_combos = product(var_domain, repeat=num_vars) + + feasible_sols = [] + for sol in all_combos: + if np.dot(sol, weights) <= capacity: + feasible_sols.append((sol, np.dot(sol, values))) + sorted(feasible_sols, key=lambda sol: sol[1], reverse=False) + print(feasible_sols) + return m - model.v = pe.Param(model.INDEX, initialize=lambda model, i : random.uniform(0.0,1.0), within=pe.Reals) - model.x = pe.Var(model.INDEX, within=pe.Binary) - model.o = pe.Objective(expr=sum(model.v[i]*model.x[i] for i in model.INDEX), sense=pe.maximize) +# from pyomo.contrib.alternative_solutions.obbt import obbt_analysis - model.c = pe.Constraint(expr=sum(model.w[i]*model.x[i] for i in model.INDEX) <= W) +# def get_random_knapsack_model(num_x_vars, num_y_vars, budget_pct, seed=1000): +# random.seed(seed) + +# W = budget_pct * (num_x_vars + num_y_vars) / 2 + + +# model = pe.ConcreteModel() + +# model.X_INDEX = pe.RangeSet(1,num_x_vars) +# model.Y_INDEX = pe.RangeSet(1,num_y_vars) + +# model.wu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) +# model.vu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) +# model.x = pe.Var(model.X_INDEX, within=pe.NonNegativeIntegers) + +# model.b = pe.Block() +# model.b.wl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) +# model.b.vl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) +# model.b.y = pe.Var(model.Y_INDEX, within=pe.NonNegativeReals) + +# model.o = pe.Objective(expr=sum(model.vu[i]*model.x[i] for i in model.X_INDEX) + \ +# sum(model.b.vl[i]*model.b.y[i] for i in model.Y_INDEX), sense=pe.maximize) +# model.c = pe.Constraint(expr=sum(model.wu[i]*model.x[i] for i in model.X_INDEX) + \ +# sum(model.b.wl[i]*model.b.y[i] for i in model.Y_INDEX)<= W) + +# return model - return model \ No newline at end of file +# model = get_random_knapsack_model(4, 4, 0.2) +# result = obbt_analysis(model, variables='all', rel_opt_gap=None, +# abs_gap=None, already_solved=False, +# solver='gurobi', solver_options={}, +# use_persistent_solver = False, tee=True, +# refine_bounds=False) \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py new file mode 100644 index 00000000000..0d844670717 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -0,0 +1,38 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from numpy.testing import assert_array_almost_equal + +import pyomo.environ as pe +from pyomo.common.collections import ComponentSet +import pyomo.common.unittest as unittest +import pyomo.contrib.alternative_solutions.aos_utils as au + +from pyomo.contrib.alternative_solutions.obbt import obbt_analysis +from pyomo.contrib.alternative_solutions.tests.test_cases \ + import get_2d_diamond_problem + +class TestOBBTUnit(unittest.TestCase): + def test_obbt_continuous(self): + m = get_2d_diamond_problem() + results, solutions = obbt_analysis(m, solver='cplex') + self.assertEqual(results.keys(), m.continuous_bounds.keys()) + for var, bounds in results.items(): + assert_array_almost_equal(bounds, m.continuous_bounds[var]) + + def test_obbt_infeasible(self): + m = get_2d_diamond_problem() + m.infeasible_constraint = pe.Constraint(expr=m.x>=10) + with self.assertRaises(Exception): + results, solutions = obbt_analysis(m, solver='cplex') + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From be7d07e4d4c812103f63ff99879f283ee7c0385a Mon Sep 17 00:00:00 2001 From: jlgearh Date: Fri, 6 Oct 2023 11:49:15 -0600 Subject: [PATCH 0102/3044] - Updated balas.py to match the pattern in obbt.py and solnpool.py - Removed old files - Updated test cases --- pyomo/contrib/alternative_solutions/balas.py | 268 +-- pyomo/contrib/alternative_solutions/obbt.py | 6 +- .../contrib/alternative_solutions/solution.py | 14 +- .../tests/knapsack_100_100_baseline.yaml | 1814 ----------------- .../tests/knapsack_100_100_comp_baseline.yaml | 11 - .../tests/knapsack_100_10_baseline.yaml | 180 -- .../tests/knapsack_100_10_comp_baseline.yaml | 8 - .../tests/knapsack_100_10_results.yaml | 150 -- .../tests/knapsack_100_1_baseline.yaml | 18 - .../tests/soln_pool_test.py | 40 - .../tests/{balas_test.py => test_balas.py} | 0 .../alternative_solutions/tests/test_cases.py | 70 +- .../tests/test_results.yaml | 180 -- .../tests/test_solnpool.py | 68 +- .../tests/test_solution.py | 52 + 15 files changed, 278 insertions(+), 2601 deletions(-) delete mode 100644 pyomo/contrib/alternative_solutions/tests/knapsack_100_100_baseline.yaml delete mode 100644 pyomo/contrib/alternative_solutions/tests/knapsack_100_100_comp_baseline.yaml delete mode 100644 pyomo/contrib/alternative_solutions/tests/knapsack_100_10_baseline.yaml delete mode 100644 pyomo/contrib/alternative_solutions/tests/knapsack_100_10_comp_baseline.yaml delete mode 100644 pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml delete mode 100644 pyomo/contrib/alternative_solutions/tests/knapsack_100_1_baseline.yaml delete mode 100644 pyomo/contrib/alternative_solutions/tests/soln_pool_test.py rename pyomo/contrib/alternative_solutions/tests/{balas_test.py => test_balas.py} (100%) delete mode 100644 pyomo/contrib/alternative_solutions/tests/test_results.yaml create mode 100644 pyomo/contrib/alternative_solutions/tests/test_solution.py diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index 1b89fe8e6f3..e2873fa8ff7 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -1,155 +1,195 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Jun 22 21:49:54 2022 +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ -@author: jlgearh - -""" - -from numpy import dot - -from pyomo.core.base.PyomoModel import ConcreteModel import pyomo.environ as pe -from pyomo.opt import SolverStatus, TerminationCondition -from pyomo.contrib.alternative_solutions import aos_utils, var_utils +from pyomo.common.collections import ComponentSet +from pyomo.contrib.alternative_solutions import aos_utils, solution -def enumerate_binary_solutions(model, max_solutions=10, variables='all', - rel_opt_gap=None, abs_gap=None, - search_mode='optimal', already_solved=False, - solver='gurobi', solver_options={}, tee=False): - '''Finds alternative optimal solutions for a binary problem. +def enumerate_binary_solutions(model, num_solutions=10, variables='all', + rel_opt_gap=None, abs_opt_gap=None, + search_mode='optimal', solver='gurobi', + solver_options={}, tee=False): + ''' + Finds alternative optimal solutions for a binary problem using no-good + cuts. Parameters ---------- model : ConcreteModel A concrete Pyomo model - max_solutions : int or None - The maximum number of solutions to generate. None indictes no upper - limit. Note, using None could lead to a large number of solutions. - variables: 'all', None, Block, or a Collection of Pyomo components - The binary variables for which alternative solutions will be - generated. 'all' or None indicates that all binary variables will - be included. + num_solutions : int + The maximum number of solutions to generate. + variables: 'all' or a collection of Pyomo _GeneralVarData variables + The variables for which bounds will be generated. 'all' indicates + that all variables will be included. Alternatively, a collection of + _GenereralVarData variables can be provided. rel_opt_gap : float or None - The relative optimality gap for allowable alternative solutions. - None indicates that a relative gap constraint will not be added to - the model. - abs_gap : float or None - The absolute optimality gap for allowable alternative solutions. - None indicates that an absolute gap constraint will not be added to - the model. + The relative optimality gap for the original objective for which + variable bounds will be found. None indicates that a relative gap + constraint will not be added to the model. + abs_opt_gap : float or None + The absolute optimality gap for the original objective for which + variable bounds will be found. None indicates that an absolute gap + constraint will not be added to the model. search_mode : 'optimal', 'random', or 'hamming' Indicates the mode that is used to generate alternative solutions. The optimal mode finds the next best solution. The random mode finds an alternative solution in the direction of a random ray. The hamming mode iteratively finds solution that maximize the hamming distance from previously discovered solutions. - already_solved : boolean - Indicates that the model has already been solved and that the - alternative solution search can start from the current solution. solver : string - The solver to be used for alternative solution search. + The solver to be used. solver_options : dict Solver option-value pairs to be passed to the solver. tee : boolean - Boolean indicating if the solver output should be displayed. + Boolean indicating that the solver output should be displayed. Returns ------- solutions - A dictionary of alternative optimal solutions. - {solution_id: (objective_value,[variable, variable_value])} + A list of Solution objects. + [Solution] ''' - #assert isinstance(model, ConcreteModel), \ - # 'model parameter must be an instance of a Pyomo Concrete Model' - - # Find the maximum number of solutions to generate - num_solutions = aos_utils._get_max_solutions(max_solutions) + print('STARTING NO-GOOD CUT ANALYSIS') + + assert search_mode in ['optimal', 'random', 'hamming'], \ + 'search mode must be "optimal", "random", or "hamming".' + if variables == 'all': - binary_variables = var_utils.get_model_variables(model, 'all', - include_binary=True) + binary_variables = aos_utils.get_model_variables(model, 'all', + include_continuous=False, + include_integer=False) else: - variable_list = var_utils.check_variables(model, variables) - all_variables = var_utils.get_model_variables(model, 'all') + binary_variables = ComponentSet() + non_binary_variables = [] + for var in variables: + if var.is_binary(): + binary_variables.append(var) + else: + non_binary_variables.append(var.name) + if len(non_binary_variables) > 0: + print(('Warning: The following non-binary variables were included' + 'in the variable list and will be ignored:')) + print(", ".join(non_binary_variables)) + all_variables = aos_utils.get_model_variables(model, 'all', + include_fixed=True) + orig_objective = aos_utils._get_active_objective(model) - - aos_block = aos_utils._add_aos_block(model) + + opt = pe.SolverFactory(solver) + for parameter, value in solver_options.items(): + opt.options[parameter] = value + + use_appsi = False + if 'appsi' in solver: + use_appsi = True + opt.update_config.check_for_new_or_removed_constraints = False + opt.update_config.check_for_new_or_removed_vars = False + opt.update_config.check_for_new_or_removed_params = False + opt.update_config.update_vars = False + opt.update_config.update_params = False + opt.update_config.update_named_expressions = False + opt.update_config.treat_fixed_vars_as_params = False + + if search_mode == 'hamming': + opt.update_config.check_for_new_objective = True + opt.update_config.update_objective = True + elif search_mode == 'random': + opt.update_config.check_for_new_objective = True + opt.update_config.update_objective = False + else: + opt.update_config.check_for_new_objective = False + opt.update_config.update_objective = False + opt.update_config.update_constraints = True + + print('Peforming initial solve of model.') + results = opt.solve(model, tee=tee) + status = results.solver.status + condition = results.solver.termination_condition + if condition != pe.TerminationCondition.optimal: + raise Exception(('No-good cut analysis cannot be applied, ' + 'SolverStatus = {}, ' + 'TerminationCondition = {}').format(status.value, + condition.value)) + + orig_objective_value = pe.value(orig_objective) + print('Found optimal solution, value = {}.'.format(orig_objective_value)) + solutions = [solution.Solution(model, all_variables)] + + aos_block = aos_utils._add_aos_block(model, name='_balas') + print('Added block {} to the model.'.format(aos_block)) aos_block.no_good_cuts = pe.ConstraintList() + aos_utils._add_objective_constraint(aos_block, orig_objective, + orig_objective_value, rel_opt_gap, + abs_opt_gap) + + if search_mode in ['random', 'hamming']: + orig_objective.deactivate() + + solution_number = 2 + while solution_number <= num_solutions: - opt = aos_utils._get_solver(solver, solver_options) + expr = 0 + for var in binary_variables: + if var.value > 0.5: + expr += 1 - var + else: + expr += var + + aos_block.no_good_cuts.add(expr= expr >= 1) - # Repeat until all solutions are found - solution_number = 0 - solutions = {} - while solution_number < num_solutions: - - # Solve the model unless this is the first solution and the model was - # not already solved - if solution_number > 0 or not already_solved: - results = opt.solve(model, tee=tee) - - if (((results.solver.status == SolverStatus.ok) and - (results.solver.termination_condition == TerminationCondition.optimal)) - or (already_solved and solution_number == 0)): - objective_value = pe.value(orig_objective) - hamming_value = 0 - if solution_number > 0: - hamming_value = pe.value(aos_block.hamming_objective/solution_number) - print("Found solution #{}, objective = {}".format(solution_number, - hamming_value)) - - solutions[solution_number] = (objective_value, - aos_utils.get_solution(model, - all_variables)) + if search_mode == 'hamming': + if hasattr(aos_block, 'hamming_objective'): + aos_block.hamming_objective.expr += expr + if use_appsi and opt.update_config.check_for_new_objective: + opt.update_config.check_for_new_objective = False + else: + aos_block.hamming_objective = pe.Objective(expr=expr, + sense=pe.maximize) - if solution_number == 0: - aos_utils._add_objective_constraint(aos_block, orig_objective, - objective_value, - rel_opt_gap, abs_gap) - - if search_mode in ['random', 'hamming']: - orig_objective.deactivate() - - # Add the new solution to the list of previous solutions + if search_mode == 'random': + if hasattr(aos_block, 'random_objective'): + aos_block.del_component('random_objective') + vector = aos_utils._get_random_direction(len(binary_variables)) + idx = 0 expr = 0 for var in binary_variables: - if var.value > 0.5: - expr += 1 - var - else: - expr += var - - aos_block.no_good_cuts.add(expr= expr >= 1) - - # TODO: Maybe rescale these - if search_mode == 'hamming': - if hasattr(aos_block, 'hamming_objective'): - aos_block.hamming_objective.expr += expr - else: - aos_block.hamming_objective = pe.Objective(expr=expr, - sense=pe.maximize) + expr += vector[idx] * var + idx += 1 + aos_block.random_objective = \ + pe.Objective(expr=expr, sense=pe.maximize) - if search_mode == 'random': - if hasattr(aos_block, 'random_objective'): - aos_block.del_component('random_objective') - vector = aos_utils._get_random_direction(len(binary_variables)) - idx = 0 - expr = 0 - for var in binary_variables: - expr += vector[idx] * var - idx += 1 - aos_block.random_objective = \ - pe.Objective(expr=expr, sense=pe.maximize) - + results = opt.solve(model, tee=tee) + status = results.solver.status + condition = results.solver.termination_condition + if condition == pe.TerminationCondition.optimal: + orig_obj_val = pe.value(orig_objective) + print("Iteration {}: objective = {}".format(solution_number, + orig_obj_val)) + solutions.append(solution.Solution(model, all_variables)) solution_number += 1 + elif (condition == pe.TerminationCondition.infeasibleOrUnbounded or + condition == pe.TerminationCondition.infeasible): + print("Iteration {}: Infeasible, no additional binary solutions.") + break else: - print('Algorithm Stopped. Solver Status: {}. Solver Condition: {}.'\ - .format(results.solver.status, - results.solver.termination_condition)) + print(("Iteration {}: Unexpected condition, SolverStatus = {}, " + "TerminationCondition = {}").format(solution_number, + status.value, + condition.value)) break - + aos_block.deactivate() orig_objective.activate() + print('COMPLETED NO-GOOD CUT ANALYSIS') - return solutions - + return solutions \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 22727b3564c..f898303bec5 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -84,9 +84,7 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, results = opt.solve(model, warmstart=warmstart, tee=tee) status = results.solver.status condition = results.solver.termination_condition - print('OBBT cannot be applied, SolverStatus = {}, ' - 'TerminationCondition = {}'.format(status.value, - condition.value)) + if condition != pe.TerminationCondition.optimal: raise Exception(('OBBT cannot be applied, SolverStatus = {}, ' 'TerminationCondition = {}').format(status.value, @@ -199,7 +197,7 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, print('COMPLETED OBBT ANALYSIS') - return variable_bounds, solutions + return variable_bounds def _add_solution(solutions): '''Add the current variable values to the solution list.''' diff --git a/pyomo/contrib/alternative_solutions/solution.py b/pyomo/contrib/alternative_solutions/solution.py index 753f09cc9d0..1f98c2f4548 100644 --- a/pyomo/contrib/alternative_solutions/solution.py +++ b/pyomo/contrib/alternative_solutions/solution.py @@ -38,7 +38,8 @@ def get_objective_name_values(self): Get a dictionary of objective name-objective value pairs. """ - def __init__(self, model, variable_list, include_fixed=True): + def __init__(self, model, variable_list, include_fixed=True, + objective=None): """ Constructs a Pyomo Solution object. @@ -51,6 +52,10 @@ def __init__(self, model, variable_list, include_fixed=True): include_fixed : boolean Boolean indicating that fixed variables should be added to the solution. + objective: None or Objective + The objective functions for which the value will be saved. None + indicates that the active objective should be used, but a + different objective can be stored as well. """ self.variables = ComponentMap() @@ -61,9 +66,10 @@ def __init__(self, model, variable_list, include_fixed=True): self.fixed_vars.add(var) if include_fixed or not is_fixed: self.variables[var] = pe.value(var) - - obj = aos_utils._get_active_objective(model) - self.objective = (obj, pe.value(obj)) + + if objective is None: + objective = aos_utils._get_active_objective(model) + self.objective = (objective, pe.value(objective)) def _round_variable_value(self, variable, value, round_discrete=True): return value if not round_discrete or variable.is_continuous() \ diff --git a/pyomo/contrib/alternative_solutions/tests/knapsack_100_100_baseline.yaml b/pyomo/contrib/alternative_solutions/tests/knapsack_100_100_baseline.yaml deleted file mode 100644 index 58d107c0bbb..00000000000 --- a/pyomo/contrib/alternative_solutions/tests/knapsack_100_100_baseline.yaml +++ /dev/null @@ -1,1814 +0,0 @@ -- objectives: {o: 26.456318046876152} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.453190757661194} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': 0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.437867999364702} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.43474071014975} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.9999999999999903, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -1.3877787807814457e-17, 'x[70]': -0.0, 'x[71]': 1.0, - 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, - 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, - 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, - 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, - 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, - 'x[9]': -0.0} -- objectives: {o: 26.418993719295166} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 0.9999999999999759, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': 0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 0.9999999999999698, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.415866430080232} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.408565569050655} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.40151889154858} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 0.9999999999999851, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': 0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': -0.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.398391602333636} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': 0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': -0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.387201703324} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0000000000000322, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': 0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 1.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.384074414109026} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 0.9999999999999856, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, - 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, - 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, - 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, - 'x[35]': 1.0, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, - 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 1.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.380858862661324} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.37912760160199} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': 0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': -0.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.000000000000013, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.376895724825804} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 0.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.376000312387024} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': -0.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': 0.0} -- objectives: {o: 26.372433826620064} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': -9.992007221626409e-15, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': 0.0} -- objectives: {o: 26.36930653740512} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.36922208250021} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 0.9999999999999856, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, - 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, - 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, - 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, - 'x[35]': 1.0000000000000333, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, - 'x[3]': 1.0, 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, - 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, - 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, - 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': 0.0, - 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, - 'x[67]': 0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, - 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, - 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, - 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, - 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, - 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, - 'x[9]': -0.0} -- objectives: {o: 26.36609479328526} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.36054240454575} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': -0.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0000000000000047, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': 0.0} -- objectives: {o: 26.358554759983083} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.35741511533079} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 0.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.355847810192255} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': 1.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.352896753744492} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': 4.135580766728708e-15, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, - 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, - 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, - 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, - 'x[35]': 0.9999999999999911, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, - 'x[3]': -0.0, 'x[40]': 1.0, 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, - 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, - 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, - 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, - 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, - 'x[67]': -0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, - 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, - 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, - 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, - 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, - 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, - 'x[9]': -0.0} -- objectives: {o: 26.35130175199731} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': 1.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.34976946452954} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 0.9999999999999979, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, - 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, - 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, - 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, - 'x[35]': 1.0, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, - 'x[40]': 1.0, 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.349189018436466} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.335876350606743} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 0.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.33510949903909} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -2.220446049250313e-16, 'x[28]': -0.0, - 'x[29]': 1.0, 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, - 'x[34]': 1.0, 'x[35]': 1.0, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, - 'x[3]': 1.0, 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, - 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, - 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, - 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, - 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 0.9999999999999762, - 'x[67]': -0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, - 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 4.440892098500626e-16, - 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, - 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, - 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, - 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, - 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.334214515081303} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.331982209824165} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': 1.5626389071599078e-14, 'x[27]': -0.0, 'x[28]': -0.0, - 'x[29]': 1.0, 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, - 'x[34]': 1.0, 'x[35]': 1.0, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, - 'x[3]': 1.0000000000000027, 'x[40]': 1.0, 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, - 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, - 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, - 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, - 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, - 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, - 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, - 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, - 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, - 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, - 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, - 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.33108722586635} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': 0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.328858948447326} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 0.9999999999999928, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.000000000000013, 'x[9]': -0.0} -- objectives: {o: 26.328517674626855} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999939, - 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.32573165923238} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.000000000000013, 'x[44]': 1.0, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.325390385411904} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.324900420324166} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': -0.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.323218076964753} - variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': -0.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': 1.1102230246251565e-16, 'x[28]': -0.0, - 'x[29]': 1.0, 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, - 'x[34]': 1.0, 'x[35]': 1.0, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, - 'x[3]': 1.0, 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, - 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, - 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, - 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, - 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 0.9999999999999708, - 'x[67]': -0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, - 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -2.220446049250313e-16, - 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, - 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, - 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, - 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, - 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.322205652166968} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': 0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': -0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.32165077182623} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': 1.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0000000000000195, - 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 0.9999999999999856, - 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, - 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, - 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, - 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, - 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, - 'x[9]': -0.0} -- objectives: {o: 26.320090787749823} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': -0.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.31852348261128} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': 1.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.3184307982028} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': 0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.31710471363129} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': 1.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.31397742441634} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': 1.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.313030636051298} - variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': 0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.9999999999999902, 'x[56]': -0.0, - 'x[57]': -0.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.31017670978414} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0000000000000207, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.30990334683634} - variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': 0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.9999999999999902, 'x[56]': -0.0, - 'x[57]': -0.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.308256831485775} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': -0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.307888463942373} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 1.0, 'x[99]': 1.0, 'x[9]': 0.0} -- objectives: {o: 26.307754997822837} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': 1.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': 0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.307049420569182} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.304627708607885} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': 1.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.30420650638189} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0000000000000029, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, - 'x[57]': -0.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.304123789748115} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': 0.0, - 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999967, - 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': 1.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.30280881840351} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': -0.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.302544670856452} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': 0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.29968152918855} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': -0.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.2994173816415} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': 0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.299394528628508} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 0.9999999999999889, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, - 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, - 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': 6.1825544683813405e-15, - 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, - 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, - 'x[39]': -0.0, 'x[3]': -1.4988010832439613e-15, 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, - 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, - 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, - 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, - 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, - 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': 1.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, - 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, - 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, - 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, - 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, - 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, - 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.29793339466293} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': -0.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.296974642405235} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': 0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.999999999999997, - 'x[40]': 1.0, 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': 0.0} -- objectives: {o: 26.296764294563094} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999845, - 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': -0.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.296267239413556} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': 0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': 1.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.29624787645341} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': 0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0000000000000207, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.295394464496976} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': 1.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 0.9999999999999857, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.29393964326118} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 0.9999999999999952, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, - 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, - 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, - 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, - 'x[35]': 1.0, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, - 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 1.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.293120587238445} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.293036132333544} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.292267175282024} - variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': 1.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 0.9999999999999857, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.292116092130943} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0000000000000233, - 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, - 'x[57]': -0.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 0.9999999999999923, - 'x[67]': -0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, - 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, - 'x[78]': 0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, - 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, - 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, - 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, - 'x[9]': -0.0} -- objectives: {o: 26.29179731358475} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.000000000000006, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': -0.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.290724091813463} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.289938643454757} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 0.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.289908843118592} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.285945469944696} - variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': 0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': 0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': 0.0, - 'x[25]': -0.0, 'x[26]': 1.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': 0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': 0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': 0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': 0.0, 'x[68]': 1.0, - 'x[69]': 0.0, 'x[6]': 0.0, 'x[70]': 0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': -0.0, 'x[9]': 0.0} -- objectives: {o: 26.28586554153917} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 0.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.285083220330915} - variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': 0.0, 'x[19]': -0.0, - 'x[1]': 0.0, 'x[20]': 0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': 0.0, - 'x[25]': -0.0, 'x[26]': 0.0, 'x[27]': 0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': 0.0, - 'x[30]': 0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': 0.0, 'x[57]': 1.0, - 'x[58]': 0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': 0.0, 'x[68]': 1.0, - 'x[69]': 0.0, 'x[6]': 0.0, 'x[70]': 0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': 0.0, 'x[83]': 1.0, 'x[84]': 0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': 0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': -0.0, 'x[9]': 0.0} -- objectives: {o: 26.285027480908408} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': 0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': -0.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.284356454379076} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': -0.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.284126579740313} - variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': 0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': 0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': 0.0, 'x[57]': 1.0, - 'x[58]': 0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': -0.0, 'x[67]': 0.0, 'x[68]': 1.0, - 'x[69]': 0.0, 'x[6]': -0.0, 'x[70]': 0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': 0.0} -- objectives: {o: 26.283351959271567} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': -0.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': 0.0, - 'x[30]': 0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': -0.0, 'x[3]': 1.0000000000000233, - 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 0.9999999999999859, - 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, - 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, - 'x[56]': -0.0, 'x[57]': -0.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, - 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, - 'x[67]': -0.0, 'x[68]': 1.0, 'x[69]': 0.0, 'x[6]': 0.0, 'x[70]': -0.0, 'x[71]': 1.0, - 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, - 'x[78]': 0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, - 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, - 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, - 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, - 'x[9]': -0.0} -- objectives: {o: 26.283089783625403} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 1.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.28258328273016} - variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': 0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': 0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': 0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': 0.0, - 'x[30]': 0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': 0.0, 'x[57]': 1.0, - 'x[58]': 0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': -0.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': 0.0, 'x[68]': 1.0, - 'x[69]': 0.0, 'x[6]': -0.0, 'x[70]': 0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': -0.0, 'x[9]': 0.0} -- objectives: {o: 26.281229165164124} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': -0.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': 0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.28099929052536} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': 0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': 0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': -0.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': 0.0, 'x[6]': -0.0, 'x[70]': 0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': 0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': 0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': -0.0, 'x[9]': 0.0} -- objectives: {o: 26.280224670056615} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': -0.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0000000000000113, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, - 'x[57]': -0.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 0.9999999999999742, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.279087311652358} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 0.9999999999999858, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, - 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, - 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, - 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, - 'x[35]': 0.9999999999999734, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, - 'x[3]': 0.0, 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, - 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, - 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, - 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, - 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, - 'x[67]': 5.112577028398846e-14, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, - 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, - 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, - 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, - 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, - 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, - 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.278506865559283} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.277585000750367} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': 1.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.276710803577817} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 0.9999999999999856, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.275960022437392} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 0.9999999999999856, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, - 'x[19]': 1.0, 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 0.0, - 'x[24]': -0.0, 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, - 'x[2]': -0.0, 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, - 'x[35]': 0.999999999999973, 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, - 'x[3]': -0.0, 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, - 'x[45]': -0.0, 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, - 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, - 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, - 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, - 'x[67]': 0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, - 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, - 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, - 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, - 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, - 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, - 'x[9]': -0.0} -- objectives: {o: 26.27570630847033} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': -0.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.275561304586947} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 0.9999999999999857, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0000000000000084, - 'x[56]': -0.0, 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, - 'x[61]': 1.0, 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, - 'x[67]': -0.0, 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, - 'x[72]': -0.0, 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, - 'x[78]': -0.0, 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, - 'x[83]': 1.0, 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': -0.0, 'x[88]': -0.0, - 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, - 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, - 'x[9]': -0.0} -- objectives: {o: 26.27537957634433} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.274670539727} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': -0.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.274321605031258} - variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': 1.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999969, - 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': 0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': 0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.273583514362873} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.272579019255378} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 1.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 0.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.272453945523264} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999943, - 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': -0.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': 1.0000000000000013, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, - 'x[89]': -0.0, 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, - 'x[94]': 1.0, 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, - 'x[9]': -0.0} diff --git a/pyomo/contrib/alternative_solutions/tests/knapsack_100_100_comp_baseline.yaml b/pyomo/contrib/alternative_solutions/tests/knapsack_100_100_comp_baseline.yaml deleted file mode 100644 index 392bdeabddd..00000000000 --- a/pyomo/contrib/alternative_solutions/tests/knapsack_100_100_comp_baseline.yaml +++ /dev/null @@ -1,11 +0,0 @@ -{'x[100]': 1.0, 'x[11]': 0.18, 'x[12]': 1.0, 'x[14]': 0.030000000000000044, 'x[15]': 0.9099999999999991, - 'x[16]': 1.0, 'x[18]': 0.01, 'x[19]': 0.91, 'x[20]': 0.03, 'x[23]': 0.34, 'x[26]': 0.030000000000000155, - 'x[29]': 1.0, 'x[31]': 0.19, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 0.9899999999999998, - 'x[37]': 1.0, 'x[3]': 0.6100000000000003, 'x[40]': 1.0, 'x[41]': 0.18, 'x[43]': 0.49000000000000016, - 'x[44]': 0.9999999999999999, 'x[45]': 0.02, 'x[48]': 1.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 0.99, 'x[52]': 1.0, 'x[54]': 1.0, 'x[55]': 0.4699999999999999, 'x[57]': 0.88, - 'x[5]': 0.9999999999999999, 'x[61]': 1.0, 'x[62]': 0.98, 'x[66]': 0.9599999999999994, - 'x[67]': 0.030000000000000512, 'x[68]': 1.0, 'x[71]': 0.8999999999999996, 'x[76]': 0.010000000000000002, - 'x[79]': 0.99, 'x[80]': 1.0, 'x[83]': 0.9899999999999998, 'x[84]': 0.020000000000000014, - 'x[86]': 0.65, 'x[87]': 0.95, 'x[8]': 1.0, 'x[91]': 0.9999999999999997, 'x[93]': 1.0, - 'x[94]': 0.99, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.04, 'x[99]': 0.7900000000000001} diff --git a/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_baseline.yaml b/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_baseline.yaml deleted file mode 100644 index 7d770d5f76e..00000000000 --- a/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_baseline.yaml +++ /dev/null @@ -1,180 +0,0 @@ -- objectives: {o: 26.456318046876152} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.453190757661194} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.437867999364702} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.43474071014975} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': 0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.418993719295184} - variables: {'x[100]': 1.0, 'x[10]': 0.0, 'x[11]': 0.0, 'x[12]': 1.0, 'x[13]': 0.0, - 'x[14]': 0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': 0.0, 'x[18]': 0.0, 'x[19]': 1.0, - 'x[1]': 0.0, 'x[20]': 0.0, 'x[21]': 0.0, 'x[22]': 0.0, 'x[23]': 1.0, 'x[24]': 0.0, - 'x[25]': 0.0, 'x[26]': 0.0, 'x[27]': 0.0, 'x[28]': 0.0, 'x[29]': 1.0, 'x[2]': 0.0, - 'x[30]': 0.0, 'x[31]': 0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': 0.0, 'x[37]': 1.0, 'x[38]': 0.0, 'x[39]': 0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': 0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': 0.0, 'x[46]': 0.0, - 'x[47]': 0.0, 'x[48]': 1.0, 'x[49]': 0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': 0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': 0.0, 'x[57]': 1.0, - 'x[58]': 0.0, 'x[59]': 0.0, 'x[5]': 1.0, 'x[60]': 0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': 0.0, 'x[64]': 0.0, 'x[65]': 0.0, 'x[66]': 1.0, 'x[67]': 0.0, 'x[68]': 1.0, - 'x[69]': 0.0, 'x[6]': 0.0, 'x[70]': 0.0, 'x[71]': 1.0, 'x[72]': 0.0, 'x[73]': 0.0, - 'x[74]': 0.0, 'x[75]': 0.0, 'x[76]': 0.0, 'x[77]': 0.0, 'x[78]': 0.0, 'x[79]': 1.0, - 'x[7]': 0.0, 'x[80]': 1.0, 'x[81]': 0.0, 'x[82]': 0.0, 'x[83]': 1.0, 'x[84]': 0.0, - 'x[85]': 0.0, 'x[86]': 0.0, 'x[87]': 1.0, 'x[88]': 0.0, 'x[89]': 0.0, 'x[8]': 1.0, - 'x[90]': 0.0, 'x[91]': 1.0, 'x[92]': 0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': 0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.0, 'x[99]': 1.0, 'x[9]': 0.0} -- objectives: {o: 26.415866430080232} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.408565569050655} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999857, - 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.401518891548587} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.398391602333636} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.387201703323992} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 0.9999999999999898, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 1.0, 'x[99]': 1.0, 'x[9]': -0.0} diff --git a/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_comp_baseline.yaml b/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_comp_baseline.yaml deleted file mode 100644 index 157f3497093..00000000000 --- a/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_comp_baseline.yaml +++ /dev/null @@ -1,8 +0,0 @@ -{'x[100]': 1.0, 'x[11]': 0.1, 'x[12]': 1.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[19]': 1.0, - 'x[23]': 0.5, 'x[29]': 1.0, 'x[31]': 0.2, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, - 'x[35]': 1.0, 'x[37]': 1.0, 'x[3]': 0.7999999999999986, 'x[40]': 1.0, 'x[43]': 0.6, - 'x[44]': 1.0, 'x[48]': 1.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, 'x[52]': 1.0, - 'x[54]': 1.0, 'x[55]': 0.4, 'x[57]': 0.9999999999999989, 'x[5]': 1.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[66]': 1.0, 'x[68]': 1.0, 'x[71]': 1.0, 'x[79]': 1.0, 'x[80]': 1.0, - 'x[83]': 1.0, 'x[86]': 0.6, 'x[87]': 0.8, 'x[8]': 1.0, 'x[91]': 1.0, 'x[93]': 1.0, - 'x[94]': 1.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 0.1, 'x[99]': 0.9} diff --git a/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml b/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml deleted file mode 100644 index 4bea27bd8cc..00000000000 --- a/pyomo/contrib/alternative_solutions/tests/knapsack_100_10_results.yaml +++ /dev/null @@ -1,150 +0,0 @@ -- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, - 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, - 'x[22]': 0, 'x[23]': 1, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, - 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, - 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 0, 'x[40]': 1, - 'x[41]': 0, 'x[42]': 0, 'x[43]': 1, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, - 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, - 'x[54]': 1, 'x[55]': 1, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, - 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, - 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, - 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, - 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, - 'x[86]': 0, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, - 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, - 'x[99]': 1, 'x[9]': 0} -- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, - 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, - 'x[22]': 0, 'x[23]': 0, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, - 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, - 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 0, 'x[40]': 1, - 'x[41]': 0, 'x[42]': 0, 'x[43]': 1, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, - 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, - 'x[54]': 1, 'x[55]': 1, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, - 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, - 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, - 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, - 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, - 'x[86]': 0, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, - 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, - 'x[99]': 1, 'x[9]': 0} -- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, - 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, - 'x[22]': 0, 'x[23]': 1, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, - 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 1, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, - 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, - 'x[41]': 0, 'x[42]': 0, 'x[43]': 0, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, - 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, - 'x[54]': 1, 'x[55]': 1, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, - 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, - 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, - 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, - 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, - 'x[86]': 1, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, - 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, - 'x[99]': 1, 'x[9]': 0} -- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, - 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, - 'x[22]': 0, 'x[23]': 0, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, - 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 1, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, - 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, - 'x[41]': 0, 'x[42]': 0, 'x[43]': 0, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, - 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, - 'x[54]': 1, 'x[55]': 1, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, - 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, - 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, - 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, - 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, - 'x[86]': 1, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, - 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, - 'x[99]': 1, 'x[9]': 0} -- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, - 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, - 'x[22]': 0, 'x[23]': 1, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, - 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, - 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, - 'x[41]': 0, 'x[42]': 0, 'x[43]': 1, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, - 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, - 'x[54]': 1, 'x[55]': 0, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, - 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, - 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, - 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, - 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, - 'x[86]': 0, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, - 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, - 'x[99]': 1, 'x[9]': 0} -- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, - 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, - 'x[22]': 0, 'x[23]': 0, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, - 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, - 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, - 'x[41]': 0, 'x[42]': 0, 'x[43]': 1, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, - 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, - 'x[54]': 1, 'x[55]': 0, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, - 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, - 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, - 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, - 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, - 'x[86]': 0, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, - 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, - 'x[99]': 1, 'x[9]': 0} -- {'x[100]': 1, 'x[10]': 0, 'x[11]': 1, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, - 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, - 'x[22]': 0, 'x[23]': 0, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, - 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, - 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, - 'x[41]': 0, 'x[42]': 0, 'x[43]': 0, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, - 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, - 'x[54]': 1, 'x[55]': 0, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, - 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, - 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, - 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, - 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, - 'x[86]': 1, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, - 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, - 'x[99]': 0, 'x[9]': 0} -- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, - 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, - 'x[22]': 0, 'x[23]': 1, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, - 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, - 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, - 'x[41]': 0, 'x[42]': 0, 'x[43]': 1, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, - 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, - 'x[54]': 1, 'x[55]': 0, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, - 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, - 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, - 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, - 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, - 'x[86]': 1, 'x[87]': 0, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, - 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, - 'x[99]': 1, 'x[9]': 0} -- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, - 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, - 'x[22]': 0, 'x[23]': 0, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, - 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, - 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, - 'x[41]': 0, 'x[42]': 0, 'x[43]': 1, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, - 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, - 'x[54]': 1, 'x[55]': 0, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, - 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, - 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, - 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, - 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, - 'x[86]': 1, 'x[87]': 0, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, - 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 0, - 'x[99]': 1, 'x[9]': 0} -- {'x[100]': 1, 'x[10]': 0, 'x[11]': 0, 'x[12]': 1, 'x[13]': 0, 'x[14]': 0, 'x[15]': 1, - 'x[16]': 1, 'x[17]': 0, 'x[18]': 0, 'x[19]': 1, 'x[1]': 0, 'x[20]': 0, 'x[21]': 0, - 'x[22]': 0, 'x[23]': 1, 'x[24]': 0, 'x[25]': 0, 'x[26]': 0, 'x[27]': 0, 'x[28]': 0, - 'x[29]': 1, 'x[2]': 0, 'x[30]': 0, 'x[31]': 0, 'x[32]': 1, 'x[33]': 1, 'x[34]': 1, - 'x[35]': 1, 'x[36]': 0, 'x[37]': 1, 'x[38]': 0, 'x[39]': 0, 'x[3]': 1, 'x[40]': 1, - 'x[41]': 0, 'x[42]': 0, 'x[43]': 0, 'x[44]': 1, 'x[45]': 0, 'x[46]': 0, 'x[47]': 0, - 'x[48]': 1, 'x[49]': 0, 'x[4]': 1, 'x[50]': 1, 'x[51]': 1, 'x[52]': 1, 'x[53]': 0, - 'x[54]': 1, 'x[55]': 0, 'x[56]': 0, 'x[57]': 1, 'x[58]': 0, 'x[59]': 0, 'x[5]': 1, - 'x[60]': 0, 'x[61]': 1, 'x[62]': 1, 'x[63]': 0, 'x[64]': 0, 'x[65]': 0, 'x[66]': 1, - 'x[67]': 0, 'x[68]': 1, 'x[69]': 0, 'x[6]': 0, 'x[70]': 0, 'x[71]': 1, 'x[72]': 0, - 'x[73]': 0, 'x[74]': 0, 'x[75]': 0, 'x[76]': 0, 'x[77]': 0, 'x[78]': 0, 'x[79]': 1, - 'x[7]': 0, 'x[80]': 1, 'x[81]': 0, 'x[82]': 0, 'x[83]': 1, 'x[84]': 0, 'x[85]': 0, - 'x[86]': 1, 'x[87]': 1, 'x[88]': 0, 'x[89]': 0, 'x[8]': 1, 'x[90]': 0, 'x[91]': 1, - 'x[92]': 0, 'x[93]': 1, 'x[94]': 1, 'x[95]': 0, 'x[96]': 1, 'x[97]': 1, 'x[98]': 1, - 'x[99]': 1, 'x[9]': 0} diff --git a/pyomo/contrib/alternative_solutions/tests/knapsack_100_1_baseline.yaml b/pyomo/contrib/alternative_solutions/tests/knapsack_100_1_baseline.yaml deleted file mode 100644 index e5902fa054f..00000000000 --- a/pyomo/contrib/alternative_solutions/tests/knapsack_100_1_baseline.yaml +++ /dev/null @@ -1,18 +0,0 @@ -- objectives: {o: 26.456318046876152} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': -0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} diff --git a/pyomo/contrib/alternative_solutions/tests/soln_pool_test.py b/pyomo/contrib/alternative_solutions/tests/soln_pool_test.py deleted file mode 100644 index 61d3aef7a0a..00000000000 --- a/pyomo/contrib/alternative_solutions/tests/soln_pool_test.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed Sep 6 15:21:06 2023 - -@author: jlgearh -""" - -import test_cases - -model = test_cases.knapsack(10) - -ast = '*'*10 - -# print(ast,'Start APPSI',ast) -# from pyomo.contrib import appsi -# opt = appsi.solvers.Gurobi() -# opt.config.stream_solver = True -# #opt.set_instance(model) -# opt.gurobi_options['PoolSolutions'] = 10 -# opt.gurobi_options['PoolSearchMode'] = 2 -# #opt.set_gurobi_param('PoolSolutions', 10) -# #opt.set_gurobi_param('PoolSearchMode', 2) -# results = opt.solve(model) -# print(ast,'END APPSI',ast) - -# print(ast,'Start Solve Factory',ast) -# from pyomo.opt import SolverFactory -# opt2 = SolverFactory('gurobi') -# opt.gurobi_options['PoolSolutions'] = 10 -# opt.gurobi_options['PoolSearchMode'] = 2 -# opt2.solve(model, tee=True) -# print(ast,'End Solve Factory',ast) - -print(ast,'Start Solve Factory',ast) -from pyomo.opt import SolverFactory -opt3 = SolverFactory('appsi_gurobi') -opt3.gurobi_options['PoolSolutions'] = 10 -opt3.gurobi_options['PoolSearchMode'] = 2 -opt3.solve(model, tee=True) -print(ast,'End Solve Factory',ast) \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/balas_test.py b/pyomo/contrib/alternative_solutions/tests/test_balas.py similarity index 100% rename from pyomo/contrib/alternative_solutions/tests/balas_test.py rename to pyomo/contrib/alternative_solutions/tests/test_balas.py diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index d577d001e50..f0fc4b45247 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -9,7 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import random from itertools import product import numpy as np @@ -40,6 +39,25 @@ def get_2d_diamond_problem(discrete_x=False, discrete_y=False): return m + +def get_triangle_ip(): + m = pe.ConcreteModel() + var_max = 5 + m.x = pe.Var(within=pe.NonNegativeIntegers, bounds=(0,var_max)) + m.y = pe.Var(within=pe.NonNegativeIntegers, bounds=(0,var_max)) + + m.o = pe.Objective(expr=m.x + m.y, sense=pe.maximize) + m.c = pe.Constraint(expr= m.x + m.y <= var_max) + + feasible_sols = [] + for i in range(var_max + 1): + for j in range(var_max + 1): + if i + j <= var_max: + feasible_sols.append(((i, j), i + j)) + feasible_sols = sorted(feasible_sols, key=lambda sol: sol[1], reverse=True) + return m, feasible_sols + + def get_aos_test_knapsack(var_max, weights, values, capacity_fraction): assert len(weights) == len(values), \ 'weights and values must be the same length.' @@ -51,58 +69,24 @@ def get_aos_test_knapsack(var_max, weights, values, capacity_fraction): m = pe.ConcreteModel() m.i = pe.RangeSet(0,num_vars-1) - m.x = pe.Var(m.i, within=pe.NonNegativeIntegers, bounds=(0,var_max)) + + if var_max == 1: + m.x = pe.Var(m.i, within=pe.Binary) + else: + m.x = pe.Var(m.i, within=pe.NonNegativeIntegers, bounds=(0,var_max)) m.o = pe.Objective(expr=sum(values[i]*m.x[i] for i in m.i), sense=pe.maximize) m.c = pe.Constraint(expr=sum(weights[i]*m.x[i] for i in m.i) <= capacity) - var_domain = var_values = range(var_max+1) + var_domain = range(var_max+1) all_combos = product(var_domain, repeat=num_vars) feasible_sols = [] for sol in all_combos: if np.dot(sol, weights) <= capacity: feasible_sols.append((sol, np.dot(sol, values))) - sorted(feasible_sols, key=lambda sol: sol[1], reverse=False) + feasible_sols = sorted(feasible_sols, key=lambda sol: sol[1], reverse=True) print(feasible_sols) - return m - - - -# from pyomo.contrib.alternative_solutions.obbt import obbt_analysis - -# def get_random_knapsack_model(num_x_vars, num_y_vars, budget_pct, seed=1000): -# random.seed(seed) - -# W = budget_pct * (num_x_vars + num_y_vars) / 2 - - -# model = pe.ConcreteModel() - -# model.X_INDEX = pe.RangeSet(1,num_x_vars) -# model.Y_INDEX = pe.RangeSet(1,num_y_vars) - -# model.wu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) -# model.vu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) -# model.x = pe.Var(model.X_INDEX, within=pe.NonNegativeIntegers) - -# model.b = pe.Block() -# model.b.wl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) -# model.b.vl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) -# model.b.y = pe.Var(model.Y_INDEX, within=pe.NonNegativeReals) - -# model.o = pe.Objective(expr=sum(model.vu[i]*model.x[i] for i in model.X_INDEX) + \ -# sum(model.b.vl[i]*model.b.y[i] for i in model.Y_INDEX), sense=pe.maximize) -# model.c = pe.Constraint(expr=sum(model.wu[i]*model.x[i] for i in model.X_INDEX) + \ -# sum(model.b.wl[i]*model.b.y[i] for i in model.Y_INDEX)<= W) - -# return model - -# model = get_random_knapsack_model(4, 4, 0.2) -# result = obbt_analysis(model, variables='all', rel_opt_gap=None, -# abs_gap=None, already_solved=False, -# solver='gurobi', solver_options={}, -# use_persistent_solver = False, tee=True, -# refine_bounds=False) \ No newline at end of file + return m \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_results.yaml b/pyomo/contrib/alternative_solutions/tests/test_results.yaml deleted file mode 100644 index 825df0bb064..00000000000 --- a/pyomo/contrib/alternative_solutions/tests/test_results.yaml +++ /dev/null @@ -1,180 +0,0 @@ -- objectives: {o: 26.456318046876152} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.453190757661194} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.437867999364702} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.43474071014975} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': 0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': 1.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 1.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.418993719295177} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -9.769962616701378e-15, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.415866430080232} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': -0.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.408565569050655} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': 1.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 0.9999999999999853, - 'x[40]': 1.0, 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, - 'x[46]': -0.0, 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, - 'x[51]': 1.0, 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': 0.0, 'x[56]': -0.0, - 'x[57]': 1.0, 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, - 'x[62]': 1.0, 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, - 'x[68]': 1.0, 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, - 'x[73]': -0.0, 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, - 'x[79]': 1.0, 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, - 'x[84]': -0.0, 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, - 'x[8]': 1.0, 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, - 'x[95]': -0.0, 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': -0.0, 'x[9]': -0.0} -- objectives: {o: 26.401518891548587} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 0.9999999999999931, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': -0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.398391602333636} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': -0.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 1.0, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': 0.0, 'x[42]': -0.0, 'x[43]': 1.0, 'x[44]': 1.0, 'x[45]': 0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 0.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': -0.0, 'x[99]': 1.0, 'x[9]': -0.0} -- objectives: {o: 26.387201703323992} - variables: {'x[100]': 1.0, 'x[10]': -0.0, 'x[11]': -0.0, 'x[12]': 1.0, 'x[13]': -0.0, - 'x[14]': -0.0, 'x[15]': 1.0, 'x[16]': 1.0, 'x[17]': -0.0, 'x[18]': -0.0, 'x[19]': 1.0, - 'x[1]': -0.0, 'x[20]': -0.0, 'x[21]': -0.0, 'x[22]': -0.0, 'x[23]': 1.0, 'x[24]': -0.0, - 'x[25]': -0.0, 'x[26]': -0.0, 'x[27]': -0.0, 'x[28]': -0.0, 'x[29]': 1.0, 'x[2]': -0.0, - 'x[30]': -0.0, 'x[31]': -0.0, 'x[32]': 1.0, 'x[33]': 1.0, 'x[34]': 1.0, 'x[35]': 0.9999999999999909, - 'x[36]': -0.0, 'x[37]': 1.0, 'x[38]': -0.0, 'x[39]': -0.0, 'x[3]': 1.0, 'x[40]': 1.0, - 'x[41]': -0.0, 'x[42]': -0.0, 'x[43]': -0.0, 'x[44]': 1.0, 'x[45]': -0.0, 'x[46]': -0.0, - 'x[47]': -0.0, 'x[48]': 1.0, 'x[49]': -0.0, 'x[4]': 1.0, 'x[50]': 1.0, 'x[51]': 1.0, - 'x[52]': 1.0, 'x[53]': -0.0, 'x[54]': 1.0, 'x[55]': -0.0, 'x[56]': -0.0, 'x[57]': 1.0, - 'x[58]': -0.0, 'x[59]': -0.0, 'x[5]': 1.0, 'x[60]': -0.0, 'x[61]': 1.0, 'x[62]': 1.0, - 'x[63]': -0.0, 'x[64]': -0.0, 'x[65]': -0.0, 'x[66]': 1.0, 'x[67]': -0.0, 'x[68]': 1.0, - 'x[69]': -0.0, 'x[6]': -0.0, 'x[70]': -0.0, 'x[71]': 1.0, 'x[72]': -0.0, 'x[73]': -0.0, - 'x[74]': -0.0, 'x[75]': -0.0, 'x[76]': -0.0, 'x[77]': -0.0, 'x[78]': -0.0, 'x[79]': 1.0, - 'x[7]': -0.0, 'x[80]': 1.0, 'x[81]': -0.0, 'x[82]': -0.0, 'x[83]': 1.0, 'x[84]': -0.0, - 'x[85]': -0.0, 'x[86]': 1.0, 'x[87]': 1.0, 'x[88]': -0.0, 'x[89]': -0.0, 'x[8]': 1.0, - 'x[90]': -0.0, 'x[91]': 1.0, 'x[92]': -0.0, 'x[93]': 1.0, 'x[94]': 1.0, 'x[95]': -0.0, - 'x[96]': 1.0, 'x[97]': 1.0, 'x[98]': 1.0, 'x[99]': 1.0, 'x[9]': -0.0} diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index b45173f9aa8..728f3dd7951 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -1,8 +1,6 @@ import os from os.path import join import yaml -import pytest -import random import pyutilib.misc import pyomo.environ as pe @@ -14,30 +12,6 @@ currdir = this_file_dir() -def knapsack(N): - random.seed(1000) - - N = N - W = N/10.0 - - - model = pe.ConcreteModel() - - model.INDEX = pe.RangeSet(1,N) - - model.w = pe.Param(model.INDEX, initialize=lambda model, i : random.uniform(0.0,1.0), within=pe.Reals) - - model.v = pe.Param(model.INDEX, initialize=lambda model, i : random.uniform(0.0,1.0), within=pe.Reals) - - model.x = pe.Var(model.INDEX, within=pe.Boolean) - - model.o = pe.Objective(expr=sum(model.v[i]*model.x[i] for i in model.INDEX), sense=pe.maximize) - - model.c = pe.Constraint(expr=sum(model.w[i]*model.x[i] for i in model.INDEX) <= W) - - return model - - def run(testname, model, N, debug=False): solutions = gurobi_generate_solutions(model=model, max_solutions=N) print(solutions) @@ -70,12 +44,36 @@ def run(testname, model, N, debug=False): -def test_knapsack_100_1(): - run('knapsack_100_1', knapsack(100), 1) - -def test_knapsack_100_10(): - run('knapsack_100_10', knapsack(100), 10) - -def test_knapsack_100_100(): - run('knapsack_100_100', knapsack(100), 100) - +import test_cases + +model = test_cases.knapsack(10) + +ast = '*'*10 + +# print(ast,'Start APPSI',ast) +# from pyomo.contrib import appsi +# opt = appsi.solvers.Gurobi() +# opt.config.stream_solver = True +# #opt.set_instance(model) +# opt.gurobi_options['PoolSolutions'] = 10 +# opt.gurobi_options['PoolSearchMode'] = 2 +# #opt.set_gurobi_param('PoolSolutions', 10) +# #opt.set_gurobi_param('PoolSearchMode', 2) +# results = opt.solve(model) +# print(ast,'END APPSI',ast) + +# print(ast,'Start Solve Factory',ast) +# from pyomo.opt import SolverFactory +# opt2 = SolverFactory('gurobi') +# opt.gurobi_options['PoolSolutions'] = 10 +# opt.gurobi_options['PoolSearchMode'] = 2 +# opt2.solve(model, tee=True) +# print(ast,'End Solve Factory',ast) + +print(ast,'Start Solve Factory',ast) +from pyomo.opt import SolverFactory +opt3 = SolverFactory('appsi_gurobi') +opt3.gurobi_options['PoolSolutions'] = 10 +opt3.gurobi_options['PoolSearchMode'] = 2 +opt3.solve(model, tee=True) +print(ast,'End Solve Factory',ast) \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_solution.py b/pyomo/contrib/alternative_solutions/tests/test_solution.py new file mode 100644 index 00000000000..e5f87878852 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/test_solution.py @@ -0,0 +1,52 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.environ as pe +from pyomo.common.collections import ComponentSet +import pyomo.common.unittest as unittest +import pyomo.contrib.alternative_solutions.aos_utils as au +import pyomo.contrib.alternative_solutions.solution as sol + +class TestSolutionUnit(unittest.TestCase): + def get_model(self): + m = pe.ConcreteModel() + m.x = pe.Var(domain=pe.NonNegativeReals) + m.y = pe.Var(domain=pe.Binary) + m.z = pe.Var(domain=pe.NonNegativeIntegers) + m.f = pe.Var(domain=pe.Reals) + + m.f.fix(1) + m.obj = pe.Objective(expr=m.x + m.y + m.z + m.f, sense=pe.maximize) + + m.con_x = pe.Constraint(expr=m.x <= 1.5) + m.con_y = pe.Constraint(expr=m.y <= 1) + m.con_z = pe.Constraint(expr=m.z <= 3) + return m + + def test_multiple_objectives(self): + model = self.get_model() + opt = pe.SolverFactory('cplex') + opt.solve(model) + all_vars = au.get_model_variables(model, include_fixed=True) + + solution = sol.Solution(model, all_vars, include_fixed=False) + solution.pprint() + + solution = sol.Solution(model, all_vars) + solution.pprint(round_discrete=True) + + sol_val = solution.get_variable_name_values(include_fixed=True, + round_discrete=True) + self.assertEqual(set(sol_val.keys()), {'x','y','z','f'}) + self.assertEqual(set(solution.get_fixed_variable_names()), {'f'}) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 53b8bb1313b0f47ac8b36d875d6b646afb75b2e9 Mon Sep 17 00:00:00 2001 From: jlgearh Date: Sun, 8 Oct 2023 21:22:53 -0600 Subject: [PATCH 0103/3044] - Updating test cases --- .../contrib/alternative_solutions/solnpool.py | 10 +- .../alternative_solutions/tests/test_balas.py | 61 ++++-------- .../alternative_solutions/tests/test_cases.py | 18 ++++ .../alternative_solutions/tests/test_obbt.py | 11 ++- .../tests/test_solnpool.py | 94 ++++--------------- .../tests/test_solution.py | 3 +- 6 files changed, 72 insertions(+), 125 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index de510ba541b..a0c01e2adc6 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -55,16 +55,16 @@ def gurobi_generate_solutions(model, num_solutions=10, rel_opt_gap=None, [Solution] ''' - opt = pe.SolverFactory('gurobi_appsi') + opt = pe.SolverFactory('appsi_gurobi') for parameter, value in solver_options.items(): opt.options[parameter] = value - opt.options('PoolSolutions', num_solutions) - opt.options('PoolSearchMode', search_mode) + opt.options['PoolSolutions'] = num_solutions + opt.options['PoolSearchMode'] = search_mode if rel_opt_gap is not None: - opt.options('PoolGap', rel_opt_gap) + opt.options['PoolGap'] = rel_opt_gap if abs_opt_gap is not None: - opt.options('PoolGapAbs', abs_opt_gap) + opt.options['PoolGapAbs'] = abs_opt_gap results = opt.solve(model, tee=tee) status = results.solver.status diff --git a/pyomo/contrib/alternative_solutions/tests/test_balas.py b/pyomo/contrib/alternative_solutions/tests/test_balas.py index 90b7b230b34..57534a4328f 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_balas.py +++ b/pyomo/contrib/alternative_solutions/tests/test_balas.py @@ -1,46 +1,25 @@ -# -*- coding: utf-8 -*- -""" -Created on Tue Jul 19 15:13:06 2022 - -@author: jlgearh -""" - -import random +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ import pyomo.environ as pe +import pyomo.common.unittest as unittest -import pyomo.contrib.alternative_solutions.balas as bls - - -def get_random_knapsack_model(num_x_vars, num_y_vars, budget_pct, seed=1000): - random.seed(seed) - - W = budget_pct * (num_x_vars + num_y_vars) / 2 - - - model = pe.ConcreteModel() - - model.X_INDEX = pe.RangeSet(1,num_x_vars) - model.Y_INDEX = pe.RangeSet(1,num_y_vars) - - model.wu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) - model.vu = pe.Param(model.X_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) - model.x = pe.Var(model.X_INDEX, within=pe.Binary) - - model.b = pe.Block() - model.b.wl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) - model.b.vl = pe.Param(model.Y_INDEX, initialize=lambda model, i : round(random.uniform(0.0,1.0), 2), within=pe.Reals) - model.b.y = pe.Var(model.Y_INDEX, within=pe.Binary) - - model.o = pe.Objective(expr=sum(model.vu[i]*model.x[i] for i in model.X_INDEX) + \ - sum(model.b.vl[i]*model.b.y[i] for i in model.Y_INDEX), sense=pe.maximize) - model.c = pe.Constraint(expr=sum(model.wu[i]*model.x[i] for i in model.X_INDEX) + \ - sum(model.b.wl[i]*model.b.y[i] for i in model.Y_INDEX)<= W) - - return model +from pyomo.contrib.alternative_solutions.balas \ + import enumerate_binary_solutions +from pyomo.contrib.alternative_solutions.tests.test_cases \ + import get_aos_test_knapsack -model = get_random_knapsack_model(4, 4, 0.2) +class TestBalasUnit(unittest.TestCase): + def test_(self): + pass -alternative_solutions = bls.enumerate_binary_solutions(model, - search_mode='hamming', - max_solutions = 99) \ No newline at end of file +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index f0fc4b45247..4d0aaf496f9 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -40,6 +40,24 @@ def get_2d_diamond_problem(discrete_x=False, discrete_y=False): return m +def get_2d_unbounded_problem(): + m = pe.ConcreteModel() + m.x = pe.Var(within=pe.Reals) + m.y = pe.Var(within=pe.Reals) + + m.o = pe.Objective(expr = m.x + m.y) + + m.c1 = pe.Constraint(expr= m.x <= 4) + m.c2 = pe.Constraint(expr= m.y >= 2) + + m.extreme_points = {(4, 2)} + + m.continuous_bounds = pe.ComponentMap() + m.continuous_bounds[m.x] = (float('-inf'), 4) + m.continuous_bounds[m.y] = (2, float('inf')) + + return m + def get_triangle_ip(): m = pe.ConcreteModel() var_max = 5 diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index 0d844670717..993dfd63e48 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -12,13 +12,11 @@ from numpy.testing import assert_array_almost_equal import pyomo.environ as pe -from pyomo.common.collections import ComponentSet import pyomo.common.unittest as unittest -import pyomo.contrib.alternative_solutions.aos_utils as au from pyomo.contrib.alternative_solutions.obbt import obbt_analysis from pyomo.contrib.alternative_solutions.tests.test_cases \ - import get_2d_diamond_problem + import get_2d_diamond_problem, get_2d_unbounded_problem class TestOBBTUnit(unittest.TestCase): def test_obbt_continuous(self): @@ -27,6 +25,13 @@ def test_obbt_continuous(self): self.assertEqual(results.keys(), m.continuous_bounds.keys()) for var, bounds in results.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) + + def test_obbt_unbounded(self): + m = get_2d_unbounded_problem() + results, solutions = obbt_analysis(m, solver='cplex') + self.assertEqual(results.keys(), m.continuous_bounds.keys()) + for var, bounds in results.items(): + assert_array_almost_equal(bounds, m.continuous_bounds[var]) def test_obbt_infeasible(self): m = get_2d_diamond_problem() diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index 728f3dd7951..23e4553838d 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -1,79 +1,25 @@ -import os -from os.path import join -import yaml +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ -import pyutilib.misc import pyomo.environ as pe -from pyomo.contrib.alternative_solutions.solnpool import \ - gurobi_generate_solutions -from pyomo.common.fileutils import this_file_dir -from pyomo.contrib.alternative_solutions.comparison import consensus +import pyomo.common.unittest as unittest -currdir = this_file_dir() +from pyomo.contrib.alternative_solutions.solnpool \ + import gurobi_generate_solutions +from pyomo.contrib.alternative_solutions.tests.test_cases \ + import get_aos_test_knapsack, get_triangle_ip +class TestSolnPoolUnit(unittest.TestCase): + def test_(self): + pass -def run(testname, model, N, debug=False): - solutions = gurobi_generate_solutions(model=model, max_solutions=N) - print(solutions) - # Verify final results - - results = [soln.get_variable_name_values() for soln in solutions] - output = yaml.dump(results, default_flow_style=None) - outputfile = join(currdir, "{}_results.yaml".format(testname)) - with open(outputfile, "w") as OUTPUT: - OUTPUT.write(output) - - baselinefile = join(currdir, "{}_baseline.yaml".format(testname)) - tmp = pyutilib.misc.compare_file(outputfile, baselinefile, tolerance=1e-7) - assert tmp[0] == False, "Files differ: diff {} {}".format(outputfile, baselinefile) - os.remove(outputfile) - - if N>1: - # Verify consensus pattern - - comp = consensus(results) - output = yaml.dump(comp, default_flow_style=None) - outputfile = join(currdir, "{}_comp_results.yaml".format(testname)) - with open(outputfile, "w") as OUTPUT: - OUTPUT.write(output) - - baselinefile = join(currdir, "{}_comp_baseline.yaml".format(testname)) - tmp = pyutilib.misc.compare_file(outputfile, baselinefile, tolerance=1e-7) - assert tmp[0] == False, "Files differ: diff {} {}".format(outputfile, baselinefile) - os.remove(outputfile) - - - -import test_cases - -model = test_cases.knapsack(10) - -ast = '*'*10 - -# print(ast,'Start APPSI',ast) -# from pyomo.contrib import appsi -# opt = appsi.solvers.Gurobi() -# opt.config.stream_solver = True -# #opt.set_instance(model) -# opt.gurobi_options['PoolSolutions'] = 10 -# opt.gurobi_options['PoolSearchMode'] = 2 -# #opt.set_gurobi_param('PoolSolutions', 10) -# #opt.set_gurobi_param('PoolSearchMode', 2) -# results = opt.solve(model) -# print(ast,'END APPSI',ast) - -# print(ast,'Start Solve Factory',ast) -# from pyomo.opt import SolverFactory -# opt2 = SolverFactory('gurobi') -# opt.gurobi_options['PoolSolutions'] = 10 -# opt.gurobi_options['PoolSearchMode'] = 2 -# opt2.solve(model, tee=True) -# print(ast,'End Solve Factory',ast) - -print(ast,'Start Solve Factory',ast) -from pyomo.opt import SolverFactory -opt3 = SolverFactory('appsi_gurobi') -opt3.gurobi_options['PoolSolutions'] = 10 -opt3.gurobi_options['PoolSearchMode'] = 2 -opt3.solve(model, tee=True) -print(ast,'End Solve Factory',ast) \ No newline at end of file +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_solution.py b/pyomo/contrib/alternative_solutions/tests/test_solution.py index e5f87878852..4a124310089 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solution.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solution.py @@ -10,7 +10,6 @@ # ___________________________________________________________________________ import pyomo.environ as pe -from pyomo.common.collections import ComponentSet import pyomo.common.unittest as unittest import pyomo.contrib.alternative_solutions.aos_utils as au import pyomo.contrib.alternative_solutions.solution as sol @@ -31,7 +30,7 @@ def get_model(self): m.con_z = pe.Constraint(expr=m.z <= 3) return m - def test_multiple_objectives(self): + def test_solution(self): model = self.get_model() opt = pe.SolverFactory('cplex') opt.solve(model) From 407e7c8e73014cf1f46d7ca8fa0e5e846d94d431 Mon Sep 17 00:00:00 2001 From: jlgearh Date: Mon, 9 Oct 2023 12:12:13 -0600 Subject: [PATCH 0104/3044] - Simplified solnpool.py by setting pool mode to 2 - Updates tests cases with some new tests and some TODOs --- .../contrib/alternative_solutions/solnpool.py | 12 +-- .../tests/test_aos_utils.py | 45 +++++++++-- .../alternative_solutions/tests/test_balas.py | 18 ++++- .../tests/test_case.xlsx | Bin 9880 -> 9876 bytes .../alternative_solutions/tests/test_cases.py | 71 +++++++++++++++++- .../alternative_solutions/tests/test_obbt.py | 51 +++++++++++-- .../tests/test_solnpool.py | 28 +++++-- .../tests/test_solution.py | 16 +++- 8 files changed, 203 insertions(+), 38 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index a0c01e2adc6..0185e5d382a 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -13,8 +13,7 @@ from pyomo.contrib.alternative_solutions import aos_utils, solution def gurobi_generate_solutions(model, num_solutions=10, rel_opt_gap=None, - abs_opt_gap=None, search_mode=2, - solver_options={}, tee=True): + abs_opt_gap=None, solver_options={}, tee=True): ''' Finds alternative optimal solutions for discrete variables using Gurobi's built-in Solution Pool capability. See the Gurobi Solution Pool @@ -36,13 +35,6 @@ def gurobi_generate_solutions(model, num_solutions=10, rel_opt_gap=None, None implies that there is no limit on the absolute optimality gap (i.e. that any feasible solution can be considered by Gurobi). This parameter maps to the PoolGapAbs parameter in Gurobi. - search_mode : 0, 1, or 2 - Indicates the SolutionPool mode that is used to generate - alternative solutions in Gurobi. Mode 2 should typically be used as - it finds the top n solutions. Mode 0 finds a single optimal - solution (i.e. the standard mode in Gurobi). Mode 1 will generate n - solutions without providing guarantees on their quality. This - parameter maps to the PoolSearchMode in Gurobi. solver_options : dict Solver option-value pairs to be passed to the Gurobi solver. tee : boolean @@ -60,7 +52,7 @@ def gurobi_generate_solutions(model, num_solutions=10, rel_opt_gap=None, for parameter, value in solver_options.items(): opt.options[parameter] = value opt.options['PoolSolutions'] = num_solutions - opt.options['PoolSearchMode'] = search_mode + opt.options['PoolSearchMode'] = 2 if rel_opt_gap is not None: opt.options['PoolGap'] = rel_opt_gap if abs_opt_gap is not None: diff --git a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py index 365b6ff1fae..2963f195d17 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py +++ b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py @@ -15,7 +15,8 @@ import pyomo.contrib.alternative_solutions.aos_utils as au class TestAOSUtilsUnit(unittest.TestCase): - def get_two_objective_model(self): + def get_multiple_objective_model(self): + '''Create a simple model with three objectives.''' m = pe.ConcreteModel() m.b1 = pe.Block() m.b2 = pe.Block() @@ -28,14 +29,16 @@ def get_two_objective_model(self): return m def test_multiple_objectives(self): - m = self.get_two_objective_model() + '''Check that an error is thrown with multiple objectives.''' + m = self.get_multiple_objective_model() assert_text = ("Model has 3 active objective functions, exactly one " "is required.") with self.assertRaisesRegex(AssertionError, assert_text): au._get_active_objective(m) def test_no_objectives(self): - m = self.get_two_objective_model() + '''Check that an error is thrown with no objectives.''' + m = self.get_multiple_objective_model() m.b1.o.deactivate() m.b2.o.deactivate() assert_text = ("Model has 0 active objective functions, exactly one " @@ -44,19 +47,25 @@ def test_no_objectives(self): au._get_active_objective(m) def test_one_objective(self): - m = self.get_two_objective_model() + ''' + Check that the active objective is returned, when there is just one + objective. + ''' + m = self.get_multiple_objective_model() m.b1.o.deactivate() m.b2.o[0].deactivate() self.assertEqual(m.b2.o[1], au._get_active_objective(m)) def test_aos_block(self): - m = self.get_two_objective_model() + '''Ensure that an alternative solution block is added.''' + m = self.get_multiple_objective_model() block_name = 'test_block' b = au._add_aos_block(m, block_name) self.assertEqual(b.name, block_name) self.assertEqual(b.ctype, pe.Block) def get_simple_model(self, sense = pe.minimize): + '''Create a simple 2d linear program with an objective.''' m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -64,6 +73,7 @@ def get_simple_model(self, sense = pe.minimize): return m def test_no_obj_constraint(self): + '''Ensure that no objective constraints are added.''' m = self.get_simple_model() cons = au._add_objective_constraint(m, m.o, 2, None, None) self.assertEqual(cons, []) @@ -71,6 +81,7 @@ def test_no_obj_constraint(self): self.assertEqual(m.find_component('optimality_tol_abs'), None) def test_min_rel_obj_constraint(self): + '''Ensure that the correct relative objective constraint is added.''' m = self.get_simple_model() cons = au._add_objective_constraint(m, m.o, 2, 0.1, None) self.assertEqual(len(cons), 1) @@ -80,6 +91,7 @@ def test_min_rel_obj_constraint(self): self.assertEqual(None, cons[0].lower) def test_min_abs_obj_constraint(self): + '''Ensure that the correct absolute objective constraint is added.''' m = self.get_simple_model() cons = au._add_objective_constraint(m, m.o, 2, None, 1) self.assertEqual(len(cons), 1) @@ -100,6 +112,10 @@ def test_min_both_obj_constraint(self): self.assertEqual(None, cons[1].lower) def test_max_both_obj_constraint(self): + ''' + Ensure that the correct relative and absolute objective constraints are + added. + ''' m = self.get_simple_model(sense=pe.maximize) cons = au._add_objective_constraint(m, m.o, -1, 0.3, 1) self.assertEqual(len(cons), 2) @@ -111,6 +127,10 @@ def test_max_both_obj_constraint(self): self.assertEqual(-2, cons[1].lower) def test_max_both_obj_constraint2(self): + ''' + Ensure that the correct relative and absolute objective constraints are + added. + ''' m = self.get_simple_model(sense=pe.maximize) cons = au._add_objective_constraint(m, m.o, 20, 0.5, 11) self.assertEqual(len(cons), 2) @@ -122,6 +142,10 @@ def test_max_both_obj_constraint2(self): self.assertEqual(9, cons[1].lower) def get_var_model(self): + ''' + Create a model with multiple variables that are nested over several + layers of blocks. + ''' indices = [0,1,2,3] @@ -168,16 +192,19 @@ def get_var_model(self): return m def test_get_all_variables_unfixed(self): + '''Check that all unfixed variables are gathered.''' m = self.get_var_model() var = au.get_model_variables(m) self.assertEqual(var, m.unfixed_vars) def test_get_all_variables(self): + '''Check that all fixed and unfixed variables are gathered.''' m = self.get_var_model() var = au.get_model_variables(m, include_fixed=True) self.assertEqual(var, m.all_vars) def test_get_all_continuous(self): + '''Check that all continuous variables are gathered.''' m = self.get_var_model() var = au.get_model_variables(m, include_continuous=True, @@ -188,6 +215,7 @@ def test_get_all_continuous(self): self.assertEqual(var, continuous_vars) def test_get_all_binary(self): + '''Check that all binary variables are gathered.''' m = self.get_var_model() var = au.get_model_variables(m, include_continuous=False, @@ -198,6 +226,7 @@ def test_get_all_binary(self): self.assertEqual(var, binary_vars) def test_get_all_integer(self): + '''Check that all integer variables are gathered.''' m = self.get_var_model() var = au.get_model_variables(m, include_continuous=False, @@ -208,6 +237,7 @@ def test_get_all_integer(self): self.assertEqual(var, continuous_vars) def test_get_specific_vars(self): + '''Check that all variables from a list are gathered.''' m = self.get_var_model() components = [m.x, m.b1.sb1.y_l[0], m.b2.sb2.z_l] var = au.get_model_variables(m, components=components) @@ -216,6 +246,10 @@ def test_get_specific_vars(self): self.assertEqual(var, specific_vars) def test_get_block_vars(self): + ''' + Check that all variables from block are gathered (without + descending into subblocks). + ''' m = self.get_var_model() components = [m.b2.sb2.z_l, (m.b1, False)] var = au.get_model_variables(m, components=components) @@ -224,6 +258,7 @@ def test_get_block_vars(self): self.assertEqual(var, specific_vars) def test_get_constraint_vars(self): + '''Check that all variables constraints and objectives are gathered.''' m = self.get_var_model() components = [m.con, m.obj] var = au.get_model_variables(m, components=components) diff --git a/pyomo/contrib/alternative_solutions/tests/test_balas.py b/pyomo/contrib/alternative_solutions/tests/test_balas.py index 57534a4328f..11e50a4a8b9 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_balas.py +++ b/pyomo/contrib/alternative_solutions/tests/test_balas.py @@ -12,12 +12,22 @@ import pyomo.environ as pe import pyomo.common.unittest as unittest -from pyomo.contrib.alternative_solutions.balas \ - import enumerate_binary_solutions -from pyomo.contrib.alternative_solutions.tests.test_cases \ - import get_aos_test_knapsack +import pyomo.contrib.alternative_solutions.balas +import pyomo.contrib.alternative_solutions.tests.test_cases as tc class TestBalasUnit(unittest.TestCase): + + #TODO: Add test cases + ''' + Repeat a lot of the test from solnpool to check that the various arguments work correct. + The main difference will be that we will only want to check binary problems here. + The knapsack problem should be useful (just set the bounds to 0-1). + + The only other thing to test is the different search modes. They should still enumerate + all of the solutions, just in a different sequence. + + ''' + def test_(self): pass diff --git a/pyomo/contrib/alternative_solutions/tests/test_case.xlsx b/pyomo/contrib/alternative_solutions/tests/test_case.xlsx index 99d46d84d6e54e7dd993a6cae5e5d385b003a0d0..024a59ce443e0471b606a58318f37e2f225bf4ab 100644 GIT binary patch delta 1906 zcmV-&2aWicO_WWr*9Hl~Io9fq0{{S>li&sze{FB0I1v7R()|a-cZ$th0&2Pn3FsoV zyH%TQzeK7WaDp#@Ib)hs)&BPznULxnQZ|uow^>0e zf6`v?hSp$NaZwQoOR=`wMo^Ns1*NhoZ3m&it_Z8m9w8FvVDQz7Wpv83uA;TsIq9buDKw~62jhL-`ENG1@Mm%49?+A zU^;nK#WKNLzLFOJZTF;4;>XCw_(KYIf2TP(56QCyeP*hLM^+;EB&*2AxI==D*VT?;jMNK4~@LGZn_i_EI<}~)Cf8d~S zXuA-jVGqo`x~8;w`A|FhKm({Ql;Pywqe z@qy88`z9&1GnLU; z^VJR9be#ADD-N+^d4UJ1@Ud$loTPq|PW?$5Yex1vO8W8zYI-(m-7xp+LdI~*t63-6 zG(@D{A!a$&MLZnKNSQ%{s7X6QqMcBA(4D+)rJg~X4CrAGjRz?7ER?vu-wmVNA!POf1$cXqQh57u`Gy?b3w8Ve9P|WDkd!mst*YGz9$>|!x*_h z(Mxe^c{oH?90#7|PgB<);OQjsQzfRJtO|ei$qQ^~>(i9T4w_r_wO&VRO!vFT%~ij# zuhWO#i|(c>@B7pL=6Da$m-I~M^x;g$$=93t&6&Q}$^7Xt)k6Bm4QOp$4K->F*@qfl z{SA}B3>1@#Bo4Em3N``>^uCzs7zF?To|E1TA%9zs+At7>-z)8Zi2NQ*LSRWmSS7Hi z`&N~<@5Y%B3tzIfxvA=Z?<5FzC0@`}E-~iSMqjM4sh9m&ZO(pa; zC%=EK!YK)yXS!n2XvoPK9C?{PeVZMP-MI$92T0H^Cyn=QMycZsDCP#H1&v>7V-@rG zYJa!XwH8>_|D+_1hrQ8qvBdP4P!?irL-1 z4Y^S*A~r$_f9|0qP&|9vYGc_(qVH3bFdm#NJ~6J*Ri@v+Q&NIk<4o-bNT!#W@9n2F zrHtQ5_x>NkL*BFWZMLg%wSKYs)q-KRS6Y_kRl z+yiPvBUx2fCWgIlXZmc?!6bmI$KAhPKMreKG#>xRV5$m zA2z6n&Wp6+s32#7|nKR$*S`b9LJcV{2(49^*;c!oDL8J2cb2y|Nrt2v%nF~ z3vUl5Wf`v>m8SkPQSYifIv?yh+w+VX9mf;Kq)^RO)QEEug0W4ai z*H^Kqm?#ZAo3aAqadO=@Kne^6DA3MgI->a3>%b^`aiCQOPL;bu zlUF281cV9z0F$8#8Y@ss0Rk-m6aWSQ2mk;8AprEgnCTb=005p4000;O0000000000 z00000$dm3QMFAF*G$lX*m6LTPIss&puq8ndQ5^sPcx*3YVQgbVXklq?lK~_Y8%FpX sbWs5S0Cxfa01*HH00000000000001alQ1Se0RfY5CL;#GBLDyZ0JjZ?uK)l5 delta 1885 zcmV-j2cr0tO_)uv*9HlocsEpy0{{S;li&sze`{}}I28T9(*6g;cM9f}fSQiVOB1P` zsoG5YB~s;p6FdRT+NMcW&3~V364G`uT4g&M5gR`^oOAEF=KRyPuCyn@1!c`dM`xC< zktSyaZB`Tg+blE2x+WxU3S6;4$e7#_7tpPwICVDAl8=9ubOHyNT zf5uwUz{-;G8cP`Rl_^?Ia3M-UWL=rIW%*`}X`}BDLVgMcTP|r%5|(#0Y2*%slL||q zFG|{q0j@t*CtNL3ORp!|AGF94{)6y+xk3<#74%M;0{p@FrL~ zdR0wx&IDV^Gk`XCq>uC?%S7m12)4&De>e@vH92{rDu)MF-2Wge|A6Jcf+6dV(GXax z_eu!C`yiTk;AZO==Sy0V`<-<)+_qnEtuj^VT7`v73o1!5(MK?38}e>~cWurXw`W5*=}e}`+g zhxobTP+!`t*rB3RZP}@ysQ}NX*~38XuR?fABTVRj^Em)=tsx{@2KXFdh!x zy^&ml`)P6o`MbkUkWKJRu`l*1f20dJk4?@)^!SoFPU5Fy+X%)eFx>Iw$OsU!3?~lk zI2}2e8~Hy#4EMvF;ZBx&qbTT!?t&|hmag%3SV30U(c+cHi?#n5s{f@K)_y3#>PWn& zWFuZJrH0z}fi?x(fP}FHqMnB#+;94o2U^Gy3^@Y0VeT`bs}krU&-GOGe{fsTn&>Z% z?_`&$H!`9C*@o-8t`T_1F)Wm3UYaFdoJBny^9`T=o*3-vI8ZVY~_`ZeGkT@Ko7ar<;V(L!%7_wk`vg**mZ0p zb`u*|r%4)lDmm)Q5&j?Ne`jAG;qXaPEKAJgjN^O_AF^ArjIaPV?F0eew`3!499a%f zbTgD0E($CoihS4blFac&D2Y=qdnK#nFF$yCjm`dPf@KGlt%|xIhpNr?vxmv$ZepLO zcZC<;CMxdxr~hr^9jLF!iJPe@l!_-!&Tq?Tx-4U55D@P&?D_|phr^X_#PG@M1gxOvcCtF{}SY?G0$(~i{5I;^ml`5AZX}L zxF7oP4fv*YUU=f2_EE{|(-JK68THbmc;XBH2eYCM5CarNYqoQZ*jn!1Z5Zd0lFk0# z(LVsQxe?9`1xewJ?M}0d9wY&OjFMkV!!Q)Z-vz%z$+KRudh<32&*)#bfrMs0*vQK(h#OHw6j(bj1Aj?peAq*>BLQcr3JO_sJB>A z_DQ#Z<1id!0hV%VDQWS!xvW7eWOLSuy?bwp-dNXlP$z8+mp{Vq%f(Au)h}eTD+VY}IU`I1wUy4Ffa)F9_Qrdr z8o0LrNbx{#c3Dzmmu~Gzn*#X#;qu72&Arl=>Yj3F7n5|QPRd*I@VK6%JPzZ;4<~*c zuA?a#MPxMoXj1%{Z(?^B;+W-s#5g9&l-y1R#1kM-@!40&f0Mxs6|)Z^I|B)zcsEpy z0{{S;lT##40fUpGBrO|7nsu=t1pol~5C8xe0000000000000000LPQ+Bt-!olQ1Pf z0i2U@B{~9Y9Fu??7Lx@f5R=m-8Ua|7?j<7|N#Tv{P5}S_cLD$a5dZ)H0000000000 X004lKEG9ky1(RzgBL>7H00000#TA2l diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index 4d0aaf496f9..159b9d0bf51 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -15,7 +15,30 @@ import pyomo.environ as pe +# TODO: Add more test probelms as needed. +''' +This script has collection of test cases that can be used to enumerate solutions. +That is, simple problems where the alternative solutions can be found manually. + +I started on a few problems here. I tired to enumerate all of the solutions for +disrete cases. This should make it easy to find all feasible points, and/or +all points within some percent/value of optimality. + +I created some pure continuous problems and found bounds and extreme points for those +but more work is needed to be able to find the bounds and extreme points within some +threshold optimality. + +I have not done any mixed cases yet, but an case with those would be useful. +get_2d_diamond_problem does let make x or y discrete, but I have not found the bounds +and extreme points for these cases yet. + +Other cases come to mind? A quadtratic maybe? + +''' + + def get_2d_diamond_problem(discrete_x=False, discrete_y=False): + '''Simple 2d problem where the feasible is diamond-shaped.''' m = pe.ConcreteModel() m.x = pe.Var(within=pe.Integers if discrete_x else pe.Reals) m.y = pe.Var(within=pe.Integers if discrete_y else pe.Reals) @@ -26,7 +49,6 @@ def get_2d_diamond_problem(discrete_x=False, discrete_y=False): m.c2 = pe.Constraint(expr= 5/9 * m.x - 5 <= m.y) m.c3 = pe.Constraint(expr= 2/9 * m.x + 2 >= m.y) m.c4 = pe.Constraint(expr= -1/2 * m.x + 3 >= m.y) - #m.c5 = pe.Constraint(expr= 2.30769230769231 >= m.y) m.extreme_points = {(0.737704918, -4.590163934), (-5.869565217, 0.695652174), @@ -41,11 +63,14 @@ def get_2d_diamond_problem(discrete_x=False, discrete_y=False): def get_2d_unbounded_problem(): + ''' + Simple 2d problem where the feasible region is unbounded, but the problem + has an optimal solution.''' m = pe.ConcreteModel() m.x = pe.Var(within=pe.Reals) m.y = pe.Var(within=pe.Reals) - m.o = pe.Objective(expr = m.x + m.y) + m.o = pe.Objective(expr = m.y - m.x) m.c1 = pe.Constraint(expr= m.x <= 4) m.c2 = pe.Constraint(expr= m.y >= 2) @@ -59,6 +84,10 @@ def get_2d_unbounded_problem(): return m def get_triangle_ip(): + ''' + Simple 2d discrete problem where the feasible region looks like a 90-45-45 + right triangle and the optimal solutions fall along the hypotenuse. + ''' m = pe.ConcreteModel() var_max = 5 m.x = pe.Var(within=pe.NonNegativeIntegers, bounds=(0,var_max)) @@ -72,11 +101,45 @@ def get_triangle_ip(): for j in range(var_max + 1): if i + j <= var_max: feasible_sols.append(((i, j), i + j)) - feasible_sols = sorted(feasible_sols, key=lambda sol: sol[1], reverse=True) - return m, feasible_sols + feasible_sols = sorted(feasible_sols, key=lambda sol: sol[1], reverse=True) + m.feasible_sols = feasible_sols + + return m + +def get_implied_bound_ip(): + ''' + 2d discrete problem where the bounds of z are impled by x and y. This + facilitate testing cases where the impled bounds are tighter than the + given bounds for the variable. + ''' + m = pe.ConcreteModel() + m.x = pe.Var(within=pe.NonNegativeIntegers, bounds=(0,5)) + m.y = pe.Var(within=pe.NonNegativeIntegers, bounds=(0,5)) + m.z = pe.Var(within=pe.NonNegativeIntegers, bounds=(0,5)) + + m.o = pe.Objective(expr = m.x + m.z) + + m.c1 = pe.Constraint(expr= m.x + m.y == 3) + m.c2 = pe.Constraint(expr= m.x + m.y + m.z <= 5) + + m.extreme_points = {(4, 2)} + + m.var_bounds = pe.ComponentMap() + m.var_bounds[m.x] = (0, 3) + m.var_bounds[m.y] = (0, 3) + m.var_bounds[m.z] = (0, 2) + + return m def get_aos_test_knapsack(var_max, weights, values, capacity_fraction): + ''' + Creates a knapsack problem, given arrays of weights and values, and + returns all feasible solutions. The capacity represents the percent of the + total max weight that can be selected (sum weights * var_max). The var_max + parameter sets the upper bound on all variables, teh max number of times + they can be selected. + ''' assert len(weights) == len(values), \ 'weights and values must be the same length.' assert 0 <= capacity_fraction and capacity_fraction <= 1, \ diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index 993dfd63e48..fced29adea8 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -15,29 +15,64 @@ import pyomo.common.unittest as unittest from pyomo.contrib.alternative_solutions.obbt import obbt_analysis -from pyomo.contrib.alternative_solutions.tests.test_cases \ - import get_2d_diamond_problem, get_2d_unbounded_problem +import pyomo.contrib.alternative_solutions.tests.test_cases as tc + +mip_solver = 'cplex' class TestOBBTUnit(unittest.TestCase): + + #TODO: Add more test cases + ''' + So far I have added test cases for the feasibility problems, we should test cases + where we but objective constraints in as well based on the absolute and relative difference. + + Add a case where bounds are only found for a subset of variables. + + Try cases where refine_discrete_bounds is set to true to ensure that new constraints are + added to refine the bounds. I created the problem get_implied_bound_ip to facilitate this + + Check to see that warm starting works for a MIP and MILP case + + We should also check that warmstarting and refining bounds works for gurobi and appsi_gurobi + + We should pass at least one solver_options to ensure this work (e.g. time limit) + + I only looked at linear cases here, so you think others are worth testing, some simple non-linear (convex) cases? + + ''' + def test_obbt_continuous(self): - m = get_2d_diamond_problem() - results, solutions = obbt_analysis(m, solver='cplex') + '''Check that the correct bounds are found for a continuous problem.''' + m = tc.get_2d_diamond_problem() + results = obbt_analysis(m, solver=mip_solver) self.assertEqual(results.keys(), m.continuous_bounds.keys()) for var, bounds in results.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) def test_obbt_unbounded(self): - m = get_2d_unbounded_problem() - results, solutions = obbt_analysis(m, solver='cplex') + '''Check that the correct bounds are found for an unbounded problem.''' + m = tc.get_2d_unbounded_problem() + results = obbt_analysis(m, solver=mip_solver) self.assertEqual(results.keys(), m.continuous_bounds.keys()) for var, bounds in results.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) + + def test_bound_tightening(self): + ''' + Check that the correct bounds are found for a discrete problem where + more restrictive bounds are implied by the constraints.''' + m = tc.get_implied_bound_ip() + results = obbt_analysis(m, solver=mip_solver) + self.assertEqual(results.keys(), m.var_bounds.keys()) + for var, bounds in results.items(): + assert_array_almost_equal(bounds, m.var_bounds[var]) def test_obbt_infeasible(self): - m = get_2d_diamond_problem() + '''Check that code catches cases where the problem is infeasible.''' + m = tc.get_2d_diamond_problem() m.infeasible_constraint = pe.Constraint(expr=m.x>=10) with self.assertRaises(Exception): - results, solutions = obbt_analysis(m, solver='cplex') + obbt_analysis(m, solver=mip_solver) if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index 23e4553838d..82481d06a28 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -12,14 +12,32 @@ import pyomo.environ as pe import pyomo.common.unittest as unittest -from pyomo.contrib.alternative_solutions.solnpool \ - import gurobi_generate_solutions -from pyomo.contrib.alternative_solutions.tests.test_cases \ - import get_aos_test_knapsack, get_triangle_ip +import pyomo.contrib.alternative_solutions.solnpool as sp +import pyomo.contrib.alternative_solutions.tests.test_cases as tc class TestSolnPoolUnit(unittest.TestCase): + + #TODO: Add test cases. + ''' + Cases to cover: + MIP feasability, + MILP feasability, + LP feasability (for an LP just one solution should be returned since gurobi cant enumerate over continuous vars) + For a MIP or MILP we should check that num solutions, rel_opt_gap and abs_opt_gap work + Pass at least one solver option to make sure that work, e.g. time limit + + I have the triagnle problem which should be easy to test with, there is + also the knapsack problem. For the LP case we can use the 2d diamond problem + I don't really have MILP case worked out though, so we may need to create one. + + We probably also need a utility to check that a two sets of solutions are the same. + Maybe this should be an AOS utility since it may be a thing we will want to do often. + + ''' def test_(self): - pass + m = tc.get_triangle_ip() + solutions = sp.gurobi_generate_solutions(m, 11) + if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_solution.py b/pyomo/contrib/alternative_solutions/tests/test_solution.py index 4a124310089..006a5f756b7 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solution.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solution.py @@ -14,8 +14,14 @@ import pyomo.contrib.alternative_solutions.aos_utils as au import pyomo.contrib.alternative_solutions.solution as sol +mip_solver = 'cplex' + class TestSolutionUnit(unittest.TestCase): def get_model(self): + ''' + Simple model with all variable types and fixed variables to test the + Solution code. + ''' m = pe.ConcreteModel() m.x = pe.Var(domain=pe.NonNegativeReals) m.y = pe.Var(domain=pe.Binary) @@ -29,10 +35,16 @@ def get_model(self): m.con_y = pe.Constraint(expr=m.y <= 1) m.con_z = pe.Constraint(expr=m.z <= 3) return m - + + @unittest.skipUnless(pe.SolverFactory(mip_solver).available(), + "MIP solver not available") def test_solution(self): + ''' + Create a Solution Object, call its functions, and ensure the correct + data is returned. + ''' model = self.get_model() - opt = pe.SolverFactory('cplex') + opt = pe.SolverFactory(mip_solver) opt.solve(model) all_vars = au.get_model_variables(model, include_fixed=True) From 816e3d66492da945e01bad7a906810b400e90f71 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 9 Oct 2023 18:16:09 -0400 Subject: [PATCH 0105/3044] handle appsi solver unbounded situation --- pyomo/contrib/mindtpy/algorithm_base_class.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 0eb602bdf7e..89575f5c4f2 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1644,7 +1644,8 @@ def solve_main(self): "No-good cuts are added and GOA algorithm doesn't converge within the time limit. " 'No integer solution is found, so the CPLEX solver will report an error status. ' ) - return None, None + # Value error will be raised if the MIP problem is unbounded and appsi solver is used when loading solutions. Although the problem is unbounded, a valid result is provided and we do not return None to let the algorithm continue. + return self.mip, main_mip_results if config.solution_pool: main_mip_results._solver_model = self.mip_opt._solver_model main_mip_results._pyomo_var_to_solver_var_map = ( From 6b60993b4f0a90cb39c4db01811933afb2fc7568 Mon Sep 17 00:00:00 2001 From: jlgearh Date: Tue, 10 Oct 2023 11:54:19 -0600 Subject: [PATCH 0106/3044] - Added code to enumerate the discrete feasiable points for the diamond problem and found the extreme points and domain for a particular objective constraint --- .../tests/test_case.xlsx | Bin 9876 -> 11330 bytes .../alternative_solutions/tests/test_cases.py | 60 ++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/pyomo/contrib/alternative_solutions/tests/test_case.xlsx b/pyomo/contrib/alternative_solutions/tests/test_case.xlsx index 024a59ce443e0471b606a58318f37e2f225bf4ab..4fa4ee1045a7dc6a06d1ce15896cd0e6dd26a0cc 100644 GIT binary patch delta 5902 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 diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index 159b9d0bf51..108e64b79b3 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -10,6 +10,7 @@ # ___________________________________________________________________________ from itertools import product +from math import ceil, floor import numpy as np @@ -37,6 +38,14 @@ ''' +def _is_satified(constraint, feasability_tol=1e-6): + value = pe.value(constraint.body) + if constraint.has_lb() and value < constraint.lb - feasability_tol: + return False + if constraint.has_ub() and value > constraint.ub + feasability_tol: + return False + return True + def get_2d_diamond_problem(discrete_x=False, discrete_y=False): '''Simple 2d problem where the feasible is diamond-shaped.''' m = pe.ConcreteModel() @@ -50,6 +59,7 @@ def get_2d_diamond_problem(discrete_x=False, discrete_y=False): m.c3 = pe.Constraint(expr= 2/9 * m.x + 2 >= m.y) m.c4 = pe.Constraint(expr= -1/2 * m.x + 3 >= m.y) + # Continuous exteme points and bounds m.extreme_points = {(0.737704918, -4.590163934), (-5.869565217, 0.695652174), (1.384615385, 2.307692308), @@ -59,6 +69,56 @@ def get_2d_diamond_problem(discrete_x=False, discrete_y=False): m.continuous_bounds[m.x] = (-5.869565217, 7.578947368) m.continuous_bounds[m.y] = (-4.590163934, 2.307692308) + # Continuous exteme points and bounds for the case where an objective + # constraint is added within a 100% relative gap of optimality or an + # absolute gap of 6.789473684 + + m.extreme_points_cut = {(45/14, -45/14), + (-18/11, 18/11), + (1.384615385, 2.307692308), + (7.578947368, -0.789473684)} + + m.continuous_bounds_cut = pe.ComponentMap() + m.continuous_bounds_cut[m.x] = (-18/11, 7.578947368) + m.continuous_bounds_cut[m.y] = (-45/14, 2.307692308) + + # Discrete feasible solutions and bounds + feasible_sols = [] + x_lower_bound = None + x_upper_bound = None + y_lower_bound = None + y_upper_bound = None + + x_lower = ceil(m.continuous_bounds[m.x][0]) + x_upper = floor(m.continuous_bounds[m.x][1]) + y_lower = ceil(m.continuous_bounds[m.y][0]) + y_upper = floor(m.continuous_bounds[m.y][1]) + cons = [m.c1, m.c2, m.c3, m.c4] + for x_value in range(x_lower, x_upper+1): + for y_value in range(y_lower, y_upper+1): + m.x.set_value(x_value) + m.y.set_value(y_value) + is_feasible = True + for con in cons: + if not _is_satified(con): + is_feasible = False + break + if is_feasible: + if x_lower_bound is None or x_value < x_lower_bound: + x_lower_bound = x_value + if x_upper_bound is None or x_value > x_upper_bound: + x_upper_bound = x_value + if y_lower_bound is None or y_value < y_lower_bound: + y_lower_bound = y_value + if y_upper_bound is None or y_value > y_upper_bound: + y_upper_bound = y_value + feasible_sols.append(((x_value, y_value), x_value + y_value)) + m.discrete_feasible = sorted(feasible_sols, key=lambda sol: sol[1], + reverse=True) + m.discrete_bounds = pe.ComponentMap() + m.discrete_bounds[m.x] = (x_lower_bound, x_upper_bound) + m.discrete_bounds[m.y] = (y_lower_bound, y_upper_bound) + return m From 5c47040d5b0fee4bb54656e49dff54f670ec6680 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 10 Oct 2023 15:57:39 -0400 Subject: [PATCH 0107/3044] add skip_validation when ignore integrality --- pyomo/contrib/mindtpy/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 4a4b77767a9..4dfb912e611 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -983,7 +983,7 @@ def copy_var_list_values(from_list, to_list, config, if var_val in v_to.domain: v_to.set_value(value(v_from, exception=False)) elif ignore_integrality and v_to.is_integer(): - v_to.set_value(value(v_from, exception=False)) + v_to.set_value(value(v_from, exception=False), skip_validation=True) elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: v_to.set_value(0) elif v_to.is_integer() and (math.fabs(var_val - rounded_val) <= From a767f281acd08629aea99f6c616e59b11372d2f5 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 10 Oct 2023 16:28:58 -0400 Subject: [PATCH 0108/3044] add special handle for rnlp infeasible --- pyomo/contrib/mindtpy/algorithm_base_class.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 89575f5c4f2..f6192ad4687 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -829,6 +829,23 @@ def init_rNLP(self, add_oa_cuts=True): if len(results.solution) > 0: self.rnlp.solutions.load_from(results) subprob_terminate_cond = results.solver.termination_condition + + # Sometimes, the NLP solver might be trapped in a infeasible solution if the objective function is nonlinear and partition_obj_nonlinear_terms is True. If this happens, we will use the original objective function instead. + if subprob_terminate_cond == tc.infeasible and config.partition_obj_nonlinear_terms: + config.logger.info( + 'Initial relaxed NLP problem is infeasible. This might be related to partition_obj_nonlinear_terms. Try to solve it again without partitioning nonlinear objective function.') + self.rnlp.MindtPy_utils.objective.deactivate() + self.rnlp.MindtPy_utils.objective_list[0].activate() + results = self.nlp_opt.solve( + self.rnlp, + tee=config.nlp_solver_tee, + load_solutions=config.load_solutions, + **nlp_args, + ) + if len(results.solution) > 0: + self.rnlp.solutions.load_from(results) + subprob_terminate_cond = results.solver.termination_condition + if subprob_terminate_cond in {tc.optimal, tc.feasible, tc.locallyOptimal}: main_objective = MindtPy.objective_list[-1] if subprob_terminate_cond == tc.optimal: From a30cf823fbe9e370f0feb2c7ce5ed659d5db112e Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 10 Oct 2023 16:30:08 -0400 Subject: [PATCH 0109/3044] fix bug --- pyomo/contrib/mindtpy/algorithm_base_class.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index f6192ad4687..c254a8db72b 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1662,7 +1662,10 @@ def solve_main(self): 'No integer solution is found, so the CPLEX solver will report an error status. ' ) # Value error will be raised if the MIP problem is unbounded and appsi solver is used when loading solutions. Although the problem is unbounded, a valid result is provided and we do not return None to let the algorithm continue. - return self.mip, main_mip_results + if 'main_mip_results' in dir(): + return self.mip, main_mip_results + else: + return None, None if config.solution_pool: main_mip_results._solver_model = self.mip_opt._solver_model main_mip_results._pyomo_var_to_solver_var_map = ( From 2a6f1d7c9b41f3d018a6095c86d4d5f54dd2bbdf Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 11 Oct 2023 23:12:28 -0400 Subject: [PATCH 0110/3044] add comments --- pyomo/contrib/mindtpy/single_tree.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index f3be27cbc4c..8b8e171c577 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -707,8 +707,8 @@ def __call__(self): # Reference: https://www.ibm.com/docs/en/icos/22.1.1?topic=SSSA5P_22.1.1/ilog.odms.cplex.help/refpythoncplex/html/cplex.callbacks.SolutionSource-class.htm # Another solution source is user_solution = 118, but it will not be encountered in LazyConstraintCallback. - config.logger.debug( - "Solution source: %s (111 node_solution, 117 heuristic_solution, 119 mipstart_solution)".format( + config.logger.info( + "Solution source: {} (111 node_solution, 117 heuristic_solution, 119 mipstart_solution)".format( self.get_solution_source() ) ) @@ -717,6 +717,7 @@ def __call__(self): # Lazy constraints separated when processing a MIP start will be discarded after that MIP start has been processed. # This means that the callback may have to separate the same constraint again for the next MIP start or for a solution that is found later in the solution process. # https://www.ibm.com/docs/en/icos/22.1.1?topic=SSSA5P_22.1.1/ilog.odms.cplex.help/refpythoncplex/html/cplex.callbacks.LazyConstraintCallback-class.htm + # For the MINLP3_simple example, all the solutions are obtained from mip_start (solution source). Therefore, it will not go to a branch and bound process.Cause an error output. if ( self.get_solution_source() != cplex.callbacks.SolutionSource.mipstart_solution From ed4b04e61425135852f0f0899821085f64f7ec40 Mon Sep 17 00:00:00 2001 From: Arguello Date: Tue, 17 Oct 2023 17:47:04 -0600 Subject: [PATCH 0111/3044] added some tests --- pyomo/contrib/alternative_solutions/obbt.py | 1 + .../contrib/alternative_solutions/solnpool.py | 32 ++++++----- .../alternative_solutions/tests/test_cases.py | 53 ++++++++++++++++++- .../alternative_solutions/tests/test_obbt.py | 50 ++++++++++++++--- .../tests/test_solnpool.py | 53 ++++++++++++++++--- 5 files changed, 160 insertions(+), 29 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index f898303bec5..5f7f057a573 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -11,6 +11,7 @@ import pyomo.environ as pe from pyomo.contrib.alternative_solutions import aos_utils +import pdb def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, refine_discrete_bounds=False, warmstart=True, diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 0185e5d382a..5360f66c3d0 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -10,7 +10,10 @@ # ___________________________________________________________________________ import pyomo.environ as pe +from pyomo.contrib import appsi from pyomo.contrib.alternative_solutions import aos_utils, solution +import gurobipy +import pdb def gurobi_generate_solutions(model, num_solutions=10, rel_opt_gap=None, abs_opt_gap=None, solver_options={}, tee=True): @@ -47,23 +50,26 @@ def gurobi_generate_solutions(model, num_solutions=10, rel_opt_gap=None, [Solution] ''' - opt = pe.SolverFactory('appsi_gurobi') - + #opt = pe.SolverFactory('appsi_gurobi') + opt = appsi.solvers.Gurobi() for parameter, value in solver_options.items(): - opt.options[parameter] = value - opt.options['PoolSolutions'] = num_solutions - opt.options['PoolSearchMode'] = 2 + opt.gurobi_options[parameter] = value + #opt.options['PoolSolutions'] = num_solutions + #opt.options['PoolSearchMode'] = 2 + opt.gurobi_options['PoolSolutions'] = num_solutions + opt.gurobi_options['PoolSearchMode'] = 2 + opt.config.stream_solver = tee if rel_opt_gap is not None: - opt.options['PoolGap'] = rel_opt_gap + opt.gurobi_options['PoolGap'] = rel_opt_gap if abs_opt_gap is not None: - opt.options['PoolGapAbs'] = abs_opt_gap - - results = opt.solve(model, tee=tee) - status = results.solver.status - condition = results.solver.termination_condition - + opt.gurobi_options['PoolGapAbs'] = abs_opt_gap + results = opt.solve(model)#, tee=tee) + #status = results.solver.status + status = results.termination_condition + #condition = results.solver.termination_condition + condition = results.termination_condition solutions = [] - if condition == pe.TerminationCondition.optimal: + if condition == appsi.base.TerminationCondition.optimal: solution_count = opt.get_model_attr('SolCount') print("{} solutions found.".format(solution_count)) variables = aos_utils.get_model_variables(model, 'all', diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index 108e64b79b3..40f2b771dfa 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -13,8 +13,10 @@ from math import ceil, floor import numpy as np +from collections import Counter import pyomo.environ as pe +import pdb # TODO: Add more test probelms as needed. ''' @@ -148,8 +150,8 @@ def get_triangle_ip(): Simple 2d discrete problem where the feasible region looks like a 90-45-45 right triangle and the optimal solutions fall along the hypotenuse. ''' - m = pe.ConcreteModel() var_max = 5 + m = pe.ConcreteModel() m.x = pe.Var(within=pe.NonNegativeIntegers, bounds=(0,var_max)) m.y = pe.Var(within=pe.NonNegativeIntegers, bounds=(0,var_max)) @@ -163,6 +165,7 @@ def get_triangle_ip(): feasible_sols.append(((i, j), i + j)) feasible_sols = sorted(feasible_sols, key=lambda sol: sol[1], reverse=True) m.feasible_sols = feasible_sols + m.num_ranked_solns = [6,5,4,3,2,1] return m @@ -229,5 +232,51 @@ def get_aos_test_knapsack(var_max, weights, values, capacity_fraction): if np.dot(sol, weights) <= capacity: feasible_sols.append((sol, np.dot(sol, values))) feasible_sols = sorted(feasible_sols, key=lambda sol: sol[1], reverse=True) - print(feasible_sols) + return m + +def get_hexagonal_pyramid_mip(): + ''' + Pentagonal pyramid with integer coordinates in the first two dimensions and + a third continuous dimension. + + ''' + var_max = 5 + m = pe.ConcreteModel() + m.x = pe.Var(within=pe.Integers, bounds=(-var_max,var_max)) + m.y = pe.Var(within=pe.Integers, bounds=(-var_max,var_max)) + m.z = pe.Var(within=pe.NonNegativeReals, bounds=(0,var_max)) + m.o = pe.Objective(expr=m.z, sense=pe.maximize) + base_points = np.array([[0, var_max, 0], [var_max, 0, 0], [var_max/2.0, -var_max, 0], [-var_max/2.0, -var_max, 0], [-var_max, 0, 0]]) + apex_point = np.array([0, 0, var_max]) + + m.c = pe.ConstraintList() + for i in range(5): + vec_1 = base_points[i] - apex_point + vec_2 = base_points[(i+1) % var_max] - base_points[i] + n = np.cross(vec_1, vec_2) + m.c.add(n[0]*(m.x - apex_point[0]) + n[1]*(m.y - apex_point[1]) + n[2]*(m.z - apex_point[2]) >= 0) + m.num_ranked_solns = [1, 4, 2, 8, 2, 12, 4, 16, 4, 20] + return m + +def get_bloated_hexagonal_pyramid_mip(): + ''' + Pentagonal pyramid with integer coordinates in the first two dimensions and + a third continuous dimension. Bounds are artificially widened for obbt testing purposes + ''' + var_max = 5 + m = pe.ConcreteModel() + m.x = pe.Var(within=pe.Integers, bounds=(-2*var_max, 2*var_max)) + m.y = pe.Var(within=pe.Integers, bounds=(-2*var_max, var_max)) + m.z = pe.Var(within=pe.NonNegativeReals, bounds=(0, 2*var_max)) + m.var_bounds = pe.ComponentMap() + m.o = pe.Objective(expr=m.z, sense=pe.maximize) + base_points = np.array([[0, var_max, 0], [var_max, 0, 0], [var_max/2.0, -var_max, 0], [-var_max/2.0, -var_max, 0], [-var_max, 0, 0]]) + apex_point = np.array([0, 0, var_max]) + + m.c = pe.ConstraintList() + for i in range(5): + vec_1 = base_points[i] - apex_point + vec_2 = base_points[(i+1) % var_max] - base_points[i] + n = np.cross(vec_1, vec_2) + m.c.add(n[0]*(m.x - apex_point[0]) + n[1]*(m.y - apex_point[1]) + n[2]*(m.z - apex_point[2]) >= 0) return m \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index fced29adea8..ea379751707 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -16,24 +16,25 @@ from pyomo.contrib.alternative_solutions.obbt import obbt_analysis import pyomo.contrib.alternative_solutions.tests.test_cases as tc +import pdb -mip_solver = 'cplex' +mip_solver = 'gurobi' class TestOBBTUnit(unittest.TestCase): #TODO: Add more test cases ''' So far I have added test cases for the feasibility problems, we should test cases - where we but objective constraints in as well based on the absolute and relative difference. + where we put TODO: objective constraints in as well based on the absolute and relative difference. Add a case where bounds are only found for a subset of variables. Try cases where refine_discrete_bounds is set to true to ensure that new constraints are added to refine the bounds. I created the problem get_implied_bound_ip to facilitate this - Check to see that warm starting works for a MIP and MILP case + TODO: Check to see that warm starting works for a MIP and MILP case - We should also check that warmstarting and refining bounds works for gurobi and appsi_gurobi + TODO: We should also check that warmstarting and refining bounds works for gurobi and appsi_gurobi We should pass at least one solver_options to ensure this work (e.g. time limit) @@ -41,7 +42,7 @@ class TestOBBTUnit(unittest.TestCase): ''' - def test_obbt_continuous(self): + def obbt_continuous(self): '''Check that the correct bounds are found for a continuous problem.''' m = tc.get_2d_diamond_problem() results = obbt_analysis(m, solver=mip_solver) @@ -49,7 +50,28 @@ def test_obbt_continuous(self): for var, bounds in results.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - def test_obbt_unbounded(self): + def test_obbt_mip(self): + '''Check that bound tightening only occurs for a subset of variables.''' + m = tc.get_bloated_hexagonal_pyramid_mip() + m.x = 0 + m.y = 0 + m.z = 5 + results = obbt_analysis(m, solver=mip_solver, tee = True, warmstart = True) + bounds_tightened = False + bounds_not_tightned = False + for var, bounds in results.items(): + if bounds[0] > var.lb: + bounds_tightened = True + else: + bounds_not_tightened = True + if bounds[1] < var.ub: + bounds_tightened = True + else: + bounds_not_tightened = True + self.assertTrue(bounds_tightened) + self.assertTrue(bounds_not_tightened) + + def obbt_unbounded(self): '''Check that the correct bounds are found for an unbounded problem.''' m = tc.get_2d_unbounded_problem() results = obbt_analysis(m, solver=mip_solver) @@ -57,7 +79,7 @@ def test_obbt_unbounded(self): for var, bounds in results.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - def test_bound_tightening(self): + def bound_tightening(self): ''' Check that the correct bounds are found for a discrete problem where more restrictive bounds are implied by the constraints.''' @@ -66,8 +88,20 @@ def test_bound_tightening(self): self.assertEqual(results.keys(), m.var_bounds.keys()) for var, bounds in results.items(): assert_array_almost_equal(bounds, m.var_bounds[var]) + + def bound_refinement(self): + ''' + Check that the correct bounds are found for a discrete problem where + more restrictive bounds are implied by the constraints.''' + m = tc.get_implied_bound_ip() + results = obbt_analysis(m, solver=mip_solver, refine_discrete_bounds=True) + for var, bounds in results.items(): + if m.var_bounds[var][0] > var.lb: + self.assertTrue(hasattr(m._obbt, var.name + "_lb")) + if m.var_bounds[var][1] < var.ub: + self.assertTrue(hasattr(m._obbt, var.name + "_ub")) - def test_obbt_infeasible(self): + def obbt_infeasible(self): '''Check that code catches cases where the problem is infeasible.''' m = tc.get_2d_diamond_problem() m.infeasible_constraint = pe.Constraint(expr=m.x>=10) diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index 82481d06a28..77c84110f4f 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -9,14 +9,20 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import unittest +from numpy.testing import assert_array_almost_equal + import pyomo.environ as pe import pyomo.common.unittest as unittest import pyomo.contrib.alternative_solutions.solnpool as sp import pyomo.contrib.alternative_solutions.tests.test_cases as tc +from collections import Counter +import pdb + +mip_solver = 'gurobi' class TestSolnPoolUnit(unittest.TestCase): - #TODO: Add test cases. ''' Cases to cover: @@ -32,12 +38,47 @@ class TestSolnPoolUnit(unittest.TestCase): We probably also need a utility to check that a two sets of solutions are the same. Maybe this should be an AOS utility since it may be a thing we will want to do often. - ''' - def test_(self): + + def test_ip_feasibility(self): + ''' + COMMENTS''' m = tc.get_triangle_ip() - solutions = sp.gurobi_generate_solutions(m, 11) - + results = sp.gurobi_generate_solutions(m, 100) + objectives = [round(result.objective[1], 2) for result in results] + actual_solns_by_obj = m.num_ranked_solns + unique_solns_by_obj = [val for val in Counter(objectives).values()] + assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) + + def test_mip_feasibility(self): + ''' + COMMENTS''' + m = tc.get_hexagonal_pyramid_mip() + results = sp.gurobi_generate_solutions(m, 100) + objectives = [round(result.objective[1], 2) for result in results] + actual_solns_by_obj = m.num_ranked_solns + unique_solns_by_obj = [val for val in Counter(objectives).values()] + assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) + + def test_mip_rel_feasibility(self): + ''' + COMMENTS''' + m = tc.get_hexagonal_pyramid_mip() + results = sp.gurobi_generate_solutions(m, 100, rel_opt_gap=.2) + objectives = [round(result.objective[1], 2) for result in results] + actual_solns_by_obj = m.num_ranked_solns[0:2] + unique_solns_by_obj = [val for val in Counter(objectives).values()] + assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) + + def test_mip_abs_feasibility(self): + ''' + COMMENTS''' + m = tc.get_hexagonal_pyramid_mip() + results = sp.gurobi_generate_solutions(m, 100, abs_opt_gap=1.99) + objectives = [round(result.objective[1], 2) for result in results] + actual_solns_by_obj = m.num_ranked_solns[0:3] + unique_solns_by_obj = [val for val in Counter(objectives).values()] + assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() From b512c4ca8744f6ce1c14f168c84b26e957812565 Mon Sep 17 00:00:00 2001 From: jlgearh Date: Thu, 19 Oct 2023 16:57:53 -0600 Subject: [PATCH 0112/3044] - Updating LP Enumeration Code --- pyomo/contrib/alternative_solutions/balas.py | 4 +- .../alternative_solutions/canonical_lp.py | 137 ++++++++ .../contrib/alternative_solutions/lp_enum.py | 310 +++++++----------- 3 files changed, 258 insertions(+), 193 deletions(-) create mode 100644 pyomo/contrib/alternative_solutions/canonical_lp.py diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index e2873fa8ff7..ece1db0b20d 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -92,7 +92,8 @@ def enumerate_binary_solutions(model, num_solutions=10, variables='all', use_appsi = False if 'appsi' in solver: use_appsi = True - opt.update_config.check_for_new_or_removed_constraints = False + opt.update_config.update_constraints = False + opt.update_config.check_for_new_or_removed_constraints = True opt.update_config.check_for_new_or_removed_vars = False opt.update_config.check_for_new_or_removed_params = False opt.update_config.update_vars = False @@ -109,7 +110,6 @@ def enumerate_binary_solutions(model, num_solutions=10, variables='all', else: opt.update_config.check_for_new_objective = False opt.update_config.update_objective = False - opt.update_config.update_constraints = True print('Peforming initial solve of model.') results = opt.solve(model, tee=tee) diff --git a/pyomo/contrib/alternative_solutions/canonical_lp.py b/pyomo/contrib/alternative_solutions/canonical_lp.py new file mode 100644 index 00000000000..7613f738a46 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/canonical_lp.py @@ -0,0 +1,137 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.environ as pe + +from pyomo.common.collections import ComponentMap +from pyomo.gdp.util import clone_without_expression_components +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +import aos_utils + +m = pe.ConcreteModel() + +m.x = pe.Var(within=pe.Reals, bounds=(-1,3)) +m.y = pe.Var(within=pe.Reals, bounds=(-3,2)) + +m.obj = pe.Objective(expr=m.x+2*m.y, sense=pe.maximize) + +m.con1 = pe.Constraint(expr=m.x+m.y<=3) +m.con2 = pe.Constraint(expr=m.x+2*m.y<=5) + +model = m + +def _set_slack_ub(expression, slack_var): + slack_lb, slack_ub = compute_bounds_on_expr(expression) + assert slack_ub >= 0 + slack_var.setub(slack_ub) + +# def get_canonical_lp(model, block): +block = None +if block is None: + block = model + +# Gather all variable and confirm the model is a bounded LP +all_variables = aos_utils.get_model_variables(model, 'all') +var_names = {} +var_name_bounds = {} +var_map = ComponentMap() +for var in all_variables: + assert var.is_continuous(), ('Variable {} is not continuous. Model must be' + ' a linear program.'.format(var.name)) + assert var.lb is not None , ('Variable {} does not have a lower bound. ' + 'Variables must be bounded.'.format(var.name)) + assert var.ub is not None , ('Variable {} does not have an upper bound. ' + 'Variables must be bounded.'.format(var.name)) + var_name = var.name + #TODO: Need to make names unique + var_names[id(var)] = var_name + var_name_bounds[var_name] = (0,var.ub - var.lb) + +canon_lp = aos_utils._add_aos_block(block, name='_canon_lp') + +# Replace original variables with shifted lower and upper bound "s" variables +canon_lp.var_index = pe.Set(initialize=var_name_bounds.keys()) +canon_lp.var_lower = pe.Var(canon_lp.var_index, domain=pe.NonNegativeReals, + bounds=var_name_bounds) +canon_lp.var_upper = pe.Var(canon_lp.var_index, domain=pe.NonNegativeReals, + bounds=var_name_bounds) + +# Link the shifted lower and upper bound "s" variables +def link_vars_rule(m, var_index): + return m.var_lower[var_index] + m.var_upper[var_index] == \ + m.var_upper[var_index].ub +canon_lp.link_vars = pe.Constraint(canon_lp.var_index, rule=link_vars_rule) + + +# Link the original and shifted lower bound variables, and get the original +# lower bound +var_lower_map = {} +var_lower_bounds = {} +for var in all_variables: + var_lower_map[id(var)] = canon_lp.var_lower[var_names[id(var)]] + var_lower_bounds[id(var)] = var.lb + +# Substitute the new s variables into the objective function +active_objective = aos_utils._get_active_objective(model) +c_var_lower = clone_without_expression_components(active_objective.expr, + substitute=var_lower_map) +c_fix_lower = clone_without_expression_components(active_objective.expr, + substitute=var_lower_bounds) +canon_lp.objective = pe.Objective(expr=c_var_lower + c_fix_lower, + name=active_objective.name + '_shifted', + sense=active_objective.sense) + +# Identify all of the shifted constraints and associated slack variables +# that will need to be created +new_constraints = {} +slacks = [] +for constraint in model.component_data_objects(pe.Constraint, active=True): + if constraint.parent_block() == canon_lp: + continue + if constraint.equality: + constraint_name = constraint.name + '_equal' + new_constraints[constraint_name] = (constraint,0) + else: + if constraint.lb is not None: + constraint_name = constraint.name + '_lower' + new_constraints[constraint_name] = (constraint,-1) + slacks.append(constraint_name) + if constraint.ub is not None: + constraint_name = constraint.name + '_upper' + new_constraints[constraint_name] = (constraint,1) + slacks.append(constraint_name) +canon_lp.constraint_index = pe.Set(initialize=new_constraints.keys()) +canon_lp.slack_index = pe.Set(initialize=slacks) +canon_lp.slack_vars = pe.Var(canon_lp.slack_index, domain=pe.NonNegativeReals) +canon_lp.constraints = pe.Constraint(canon_lp.constraint_index) + +constraint_map = {} +constraint_bounds = {} + +for constraint_name, (constraint, constraint_type) in new_constraints.items(): + + a_sub_var_lower = clone_without_expression_components(constraint.body, + substitute=var_lower_map) + a_sub_fix_lower = clone_without_expression_components(constraint.body, + substitute=var_lower_bounds) + b_lower = constraint.lb + b_upper = constraint.ub + if constraint_type == 0: + expression = a_sub_var_lower + a_sub_fix_lower - b_lower == 0 + elif constraint_type == -1: + expression_rhs = a_sub_var_lower + a_sub_fix_lower - b_lower + expression = canon_lp.slack_vars[constraint_name] == expression_rhs + _set_slack_ub(expression_rhs, canon_lp.slack_vars[constraint_name]) + elif constraint_type == 1: + expression_rhs = b_upper - a_sub_var_lower - a_sub_fix_lower + expression = canon_lp.slack_vars[constraint_name] == expression_rhs + _set_slack_ub(expression_rhs, canon_lp.slack_vars[constraint_name]) + canon_lp.constraints[constraint_name] = expression \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index d43ae8f3412..0c53f00c41e 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -1,9 +1,13 @@ -# -*- coding: utf-8 -*- -""" -Created on Thu Dec 1 11:18:04 2022 - -@author: jlgearh -""" +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ import pyomo.environ as pe from pyomo.opt import SolverStatus, TerminationCondition @@ -11,210 +15,134 @@ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr import aos_utils -# TODO set the variable values at the end - -model = pe.ConcreteModel() - -model.x = pe.Var(within=pe.PercentFraction) -model.y = pe.Var(within=pe.PercentFraction) - - -model.obj = pe.Objective(expr=model.x+model.y, sense=pe.maximize) - -model.wx_limit = pe.Constraint(expr=model.x+model.y<=2) - -# model = pe.ConcreteModel() - -# model.w = pe.Var(within=pe.NonNegativeReals) -# model.x = pe.Var(within=pe.Reals) -# model.y = pe.Var(within=pe.PercentFraction) -# model.z = pe.Var(within=pe.Reals, bounds=(0,1)) - - -# model.obj = pe.Objective(expr=model.w+model.x+model.y+model.z, sense=pe.maximize) - -# model.wx_limit = pe.Constraint(expr=model.w+model.x<=2) -# model.wu_limit = pe.Constraint(expr=model.w<=1) -# model.xl_limit = pe.Constraint(expr=model.x>=0) -# model.xu_limit = pe.Constraint(expr=model.x<=1) - -# model.b = pe.Block() -# model.b.yz_limit = pe.Constraint(expr=-model.y-model.z>=-2) -# model.b.wy = pe.Constraint(expr=model.w+model.y==1) - -# model = pe.ConcreteModel() - -# model.w = pe.Var(within=pe.PercentFraction) -# model.x = pe.Var(within=pe.PercentFraction) -# model.y = pe.Var(within=pe.PercentFraction) -# model.z = pe.Var(within=pe.PercentFraction) - - -# model.obj = pe.Objective(expr=model.w+model.x+model.y+model.z, sense=pe.maximize) - -# model.wx_limit = pe.Constraint(expr=model.w+model.x<=2) - - -# model.b = pe.Block() -# model.b.yz_limit = pe.Constraint(expr=-model.y-model.z>=-2) -# model.b.wy = pe.Constraint(expr=model.w+model.y==1) - -# Get a Pyomo concrete model - - -# Get all continuous variables in the model and check that they have finite -# bounds -# TODO handle fixed variables -model_vars = aos_utils.get_model_variables(model, 'all') -model_var_names = {} -model_var_names_bounds = {} -for mv in model_vars: - assert mv.is_continuous, 'Variable {} is not continuous'.format(mv.name) - assert not (mv.lb is None and mv.ub is None) - var_name = mv.name - model_var_names[id(mv)] = var_name - model_var_names_bounds[var_name] = (0,mv.ub - mv.lb) - -canon_lp = aos_utils._add_aos_block(model, name='canon_lp') - -# Replace original variables with shifted lower and upper bound "s" variables -# TODO use unique names - -canon_lp.var_index = pe.Set(initialize=model_var_names_bounds.keys()) - -canon_lp.var_lower = pe.Var(canon_lp.var_index, domain=pe.NonNegativeReals, - bounds=model_var_names_bounds) -canon_lp.var_upper = pe.Var(canon_lp.var_index, domain=pe.NonNegativeReals, - bounds=model_var_names_bounds) - -def link_vars_rule(model, var_index): - return model.var_lower[var_index] + model.var_upper[var_index] == \ - model.var_upper[var_index].ub -canon_lp.link_vars = pe.Constraint(canon_lp.var_index, rule=link_vars_rule) - -var_lower_map = {} -var_lower_bounds = {} -for mv in model_vars: - var_lower_map[id(mv)] = canon_lp.var_lower[model_var_names[id(mv)]] - var_lower_bounds[id(mv)] = mv.lb - -# Substitue the new s variables into the objective function -orig_objective = aos_utils._get_active_objective(model) -c_var_lower = clone_without_expression_components(orig_objective.expr, - substitute=var_lower_map) -c_fix_lower = clone_without_expression_components(orig_objective.expr, - substitute=var_lower_bounds) -canon_lp.objective = pe.Objective(expr=c_var_lower + c_fix_lower, - name=orig_objective.name + '_shifted', - sense=orig_objective.sense) - -new_constraints = {} -slacks = [] -for constraint in model.component_data_objects(pe.Constraint, active=None, - sort=False, - descend_into=pe.Block, - descent_order=None): - if constraint.parent_block() == canon_lp: - continue - if constraint.equality: - constraint_name = constraint.name + '_equal' - new_constraints[constraint_name] = (constraint,0) - else: - if constraint.lb is not None: - constraint_name = constraint.name + '_lower' - new_constraints[constraint_name] = (constraint,-1) - slacks.append(constraint_name) - if constraint.ub is not None: - constraint_name = constraint.name + '_upper' - new_constraints[constraint_name] = (constraint,1) - slacks.append(constraint_name) -canon_lp.constraint_index = pe.Set(initialize=new_constraints.keys()) -canon_lp.slack_index = pe.Set(initialize=slacks) -canon_lp.slack_vars = pe.Var(canon_lp.slack_index, domain=pe.NonNegativeReals) -canon_lp.constraints = pe.Constraint(canon_lp.constraint_index) - -constraint_map = {} -constraint_bounds = {} - -def set_slack_ub(expression, slack_var): - slack_lb, slack_ub = compute_bounds_on_expr(expression) - assert slack_lb == 0 and slack_ub >= 0 - slack_var.setub(slack_ub) - -for constraint_name, (constraint, constraint_type) in new_constraints.items(): - - a_sub_var_lower = clone_without_expression_components(constraint.body, - substitute=var_lower_map) - a_sub_fix_lower = clone_without_expression_components(constraint.body, - substitute=var_lower_bounds) - b_lower = constraint.lb - b_upper = constraint.ub - if constraint_type == 0: - expression = a_sub_var_lower + a_sub_fix_lower - b_lower == 0 - elif constraint_type == -1: - expression_rhs = a_sub_var_lower + a_sub_fix_lower - b_lower - expression = canon_lp.slack_vars[constraint_name] == expression_rhs - set_slack_ub(expression_rhs, canon_lp.slack_vars[constraint_name]) - elif constraint_type == 1: - expression_rhs = b_upper - a_sub_var_lower - a_sub_fix_lower - expression = canon_lp.slack_vars[constraint_name] == expression_rhs - set_slack_ub(expression_rhs, canon_lp.slack_vars[constraint_name]) - canon_lp.constraints[constraint_name] = expression - - -def enumerate_linear_solutions(model, max_solutions=10, variables='all', - rel_opt_gap=None, abs_gap=None, - search_mode='optimal', already_solved=False, - solver='cplex', solver_options={}, tee=False): +def enumerate_linear_solutions(model, num_solutions=10, variables='all', + rel_opt_gap=None, abs_gap=None, + search_mode='optimal', solver='cplex', + solver_options={}, tee=False): '''Finds alternative optimal solutions for a binary problem. Parameters ---------- model : ConcreteModel A concrete Pyomo model - max_solutions : int or None - The maximum number of solutions to generate. None indictes no upper - limit. Note, using None could lead to a large number of solutions. - variables: 'all', None, Block, or a Collection of Pyomo components - The binary variables for which alternative solutions will be - generated. 'all' or None indicates that all binary variables will - be included. + num_solutions : int + The maximum number of solutions to generate. + variables: 'all' or a collection of Pyomo _GeneralVarData variables + The variables for which bounds will be generated. 'all' indicates + that all variables will be included. Alternatively, a collection of + _GenereralVarData variables can be provided. rel_opt_gap : float or None - The relative optimality gap for allowable alternative solutions. - None indicates that a relative gap constraint will not be added to - the model. - abs_gap : float or None - The absolute optimality gap for allowable alternative solutions. - None indicates that an absolute gap constraint will not be added to - the model. - search_mode : 'optimal', 'random', or 'hamming' + The relative optimality gap for the original objective for which + variable bounds will be found. None indicates that a relative gap + constraint will not be added to the model. + abs_opt_gap : float or None + The absolute optimality gap for the original objective for which + variable bounds will be found. None indicates that an absolute gap + constraint will not be added to the model. + search_mode : 'optimal', 'random', or 'norm' Indicates the mode that is used to generate alternative solutions. The optimal mode finds the next best solution. The random mode finds an alternative solution in the direction of a random ray. The - hamming mode iteratively finds solution that maximize the hamming - distance from previously discovered solutions. - already_solved : boolean - Indicates that the model has already been solved and that the - alternative solution search can start from the current solution. + norm mode iteratively finds solution that maximize the L2 distance + from previously discovered solutions. solver : string - The solver to be used for alternative solution search. + The solver to be used. solver_options : dict Solver option-value pairs to be passed to the solver. tee : boolean - Boolean indicating if the solver output should be displayed. + Boolean indicating that the solver output should be displayed. Returns ------- solutions - A dictionary of alternative optimal solutions. - {solution_id: (objective_value,[variable, variable_value])} + A list of Solution objects. + [Solution] ''' - - - # Find the maximum number of solutions to generate - num_solutions = aos_utils._get_max_solutions(max_solutions) - opt = aos_utils._get_solver(solver, solver_options) - + print('STARTING LP ENUMERATION ANALYSIS') + + # For now keeping things simple + assert variables == 'all' + + assert search_mode in ['optimal', 'random', 'norm'], \ + 'search mode must be "optimal", "random", or "norm".' + + if variables == 'all': + all_variables = aos_utils.get_model_variables(model, 'all') + # else: + # binary_variables = ComponentSet() + # non_binary_variables = [] + # for var in variables: + # if var.is_binary(): + # binary_variables.append(var) + # else: + # non_binary_variables.append(var.name) + # if len(non_binary_variables) > 0: + # print(('Warning: The following non-binary variables were included' + # 'in the variable list and will be ignored:')) + # print(", ".join(non_binary_variables)) + # all_variables = aos_utils.get_model_variables(model, 'all', + # include_fixed=True) + + for var in all_variables: + assert var.is_continuous(), 'Model must be an LP' + + orig_objective = aos_utils._get_active_objective(model) + + opt = pe.SolverFactory(solver) + for parameter, value in solver_options.items(): + opt.options[parameter] = value + + use_appsi = False + # TODO Check all this once implemented + if 'appsi' in solver: + use_appsi = True + opt.update_config.check_for_new_or_removed_constraints = True + opt.update_config.update_constraints = False + opt.update_config.check_for_new_or_removed_vars = True + opt.update_config.check_for_new_or_removed_params = False + opt.update_config.update_vars = False + opt.update_config.update_params = False + opt.update_config.update_named_expressions = False + opt.update_config.treat_fixed_vars_as_params = False + + if search_mode == 'norm': + opt.update_config.check_for_new_objective = True + opt.update_config.update_objective = True + elif search_mode == 'random': + opt.update_config.check_for_new_objective = True + opt.update_config.update_objective = False + else: + opt.update_config.check_for_new_objective = False + opt.update_config.update_objective = False + + print('Peforming initial solve of model.') + results = opt.solve(model, tee=tee) + status = results.solver.status + condition = results.solver.termination_condition + if condition != pe.TerminationCondition.optimal: + raise Exception(('LP enumeration analysis cannot be applied, ' + 'SolverStatus = {}, ' + 'TerminationCondition = {}').format(status.value, + condition.value)) + + orig_objective_value = pe.value(orig_objective) + print('Found optimal solution, value = {}.'.format(orig_objective_value)) + + aos_block = aos_utils._add_aos_block(model, name='_lp_enum') + print('Added block {} to the model.'.format(aos_block)) + aos_utils._add_objective_constraint(aos_block, orig_objective, + orig_objective_value, rel_opt_gap, + abs_opt_gap) + + canon_block = get_canonical_lp(model) + + + solution_number = 2 + + orig_objective.deactivate() + solutions = [solution.Solution(model, all_variables)] + + while solution_number <= num_solutions: model.iteration = pe.Set(dimen=1) model.basic_lower = pe.Var(pe.Any, domain=pe.Binary, dense=False) From 8cf9c955f6f755341637608691741a8eb97d1c7a Mon Sep 17 00:00:00 2001 From: jlgearh Date: Fri, 20 Oct 2023 15:34:08 -0600 Subject: [PATCH 0113/3044] - Renamed the canonical_lp.py file to shifted_lp.py, and completed initial development of code needed to put an LP in standard form. - Create an initial working version of the lp_enum code. --- pyomo/contrib/alternative_solutions/balas.py | 3 +- .../alternative_solutions/canonical_lp.py | 137 ------------ .../contrib/alternative_solutions/lp_enum.py | 161 +++++++++------ .../alternative_solutions/shifted_lp.py | 195 ++++++++++++++++++ .../alternative_solutions/tests/test_cases.py | 17 ++ 5 files changed, 309 insertions(+), 204 deletions(-) delete mode 100644 pyomo/contrib/alternative_solutions/canonical_lp.py create mode 100644 pyomo/contrib/alternative_solutions/shifted_lp.py diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index ece1db0b20d..2498a8e0651 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -179,7 +179,8 @@ def enumerate_binary_solutions(model, num_solutions=10, variables='all', solution_number += 1 elif (condition == pe.TerminationCondition.infeasibleOrUnbounded or condition == pe.TerminationCondition.infeasible): - print("Iteration {}: Infeasible, no additional binary solutions.") + print("Iteration {}: Infeasible, no additional binary solutions.".\ + format(solution_number)) break else: print(("Iteration {}: Unexpected condition, SolverStatus = {}, " diff --git a/pyomo/contrib/alternative_solutions/canonical_lp.py b/pyomo/contrib/alternative_solutions/canonical_lp.py deleted file mode 100644 index 7613f738a46..00000000000 --- a/pyomo/contrib/alternative_solutions/canonical_lp.py +++ /dev/null @@ -1,137 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pyomo.environ as pe - -from pyomo.common.collections import ComponentMap -from pyomo.gdp.util import clone_without_expression_components -from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -import aos_utils - -m = pe.ConcreteModel() - -m.x = pe.Var(within=pe.Reals, bounds=(-1,3)) -m.y = pe.Var(within=pe.Reals, bounds=(-3,2)) - -m.obj = pe.Objective(expr=m.x+2*m.y, sense=pe.maximize) - -m.con1 = pe.Constraint(expr=m.x+m.y<=3) -m.con2 = pe.Constraint(expr=m.x+2*m.y<=5) - -model = m - -def _set_slack_ub(expression, slack_var): - slack_lb, slack_ub = compute_bounds_on_expr(expression) - assert slack_ub >= 0 - slack_var.setub(slack_ub) - -# def get_canonical_lp(model, block): -block = None -if block is None: - block = model - -# Gather all variable and confirm the model is a bounded LP -all_variables = aos_utils.get_model_variables(model, 'all') -var_names = {} -var_name_bounds = {} -var_map = ComponentMap() -for var in all_variables: - assert var.is_continuous(), ('Variable {} is not continuous. Model must be' - ' a linear program.'.format(var.name)) - assert var.lb is not None , ('Variable {} does not have a lower bound. ' - 'Variables must be bounded.'.format(var.name)) - assert var.ub is not None , ('Variable {} does not have an upper bound. ' - 'Variables must be bounded.'.format(var.name)) - var_name = var.name - #TODO: Need to make names unique - var_names[id(var)] = var_name - var_name_bounds[var_name] = (0,var.ub - var.lb) - -canon_lp = aos_utils._add_aos_block(block, name='_canon_lp') - -# Replace original variables with shifted lower and upper bound "s" variables -canon_lp.var_index = pe.Set(initialize=var_name_bounds.keys()) -canon_lp.var_lower = pe.Var(canon_lp.var_index, domain=pe.NonNegativeReals, - bounds=var_name_bounds) -canon_lp.var_upper = pe.Var(canon_lp.var_index, domain=pe.NonNegativeReals, - bounds=var_name_bounds) - -# Link the shifted lower and upper bound "s" variables -def link_vars_rule(m, var_index): - return m.var_lower[var_index] + m.var_upper[var_index] == \ - m.var_upper[var_index].ub -canon_lp.link_vars = pe.Constraint(canon_lp.var_index, rule=link_vars_rule) - - -# Link the original and shifted lower bound variables, and get the original -# lower bound -var_lower_map = {} -var_lower_bounds = {} -for var in all_variables: - var_lower_map[id(var)] = canon_lp.var_lower[var_names[id(var)]] - var_lower_bounds[id(var)] = var.lb - -# Substitute the new s variables into the objective function -active_objective = aos_utils._get_active_objective(model) -c_var_lower = clone_without_expression_components(active_objective.expr, - substitute=var_lower_map) -c_fix_lower = clone_without_expression_components(active_objective.expr, - substitute=var_lower_bounds) -canon_lp.objective = pe.Objective(expr=c_var_lower + c_fix_lower, - name=active_objective.name + '_shifted', - sense=active_objective.sense) - -# Identify all of the shifted constraints and associated slack variables -# that will need to be created -new_constraints = {} -slacks = [] -for constraint in model.component_data_objects(pe.Constraint, active=True): - if constraint.parent_block() == canon_lp: - continue - if constraint.equality: - constraint_name = constraint.name + '_equal' - new_constraints[constraint_name] = (constraint,0) - else: - if constraint.lb is not None: - constraint_name = constraint.name + '_lower' - new_constraints[constraint_name] = (constraint,-1) - slacks.append(constraint_name) - if constraint.ub is not None: - constraint_name = constraint.name + '_upper' - new_constraints[constraint_name] = (constraint,1) - slacks.append(constraint_name) -canon_lp.constraint_index = pe.Set(initialize=new_constraints.keys()) -canon_lp.slack_index = pe.Set(initialize=slacks) -canon_lp.slack_vars = pe.Var(canon_lp.slack_index, domain=pe.NonNegativeReals) -canon_lp.constraints = pe.Constraint(canon_lp.constraint_index) - -constraint_map = {} -constraint_bounds = {} - -for constraint_name, (constraint, constraint_type) in new_constraints.items(): - - a_sub_var_lower = clone_without_expression_components(constraint.body, - substitute=var_lower_map) - a_sub_fix_lower = clone_without_expression_components(constraint.body, - substitute=var_lower_bounds) - b_lower = constraint.lb - b_upper = constraint.ub - if constraint_type == 0: - expression = a_sub_var_lower + a_sub_fix_lower - b_lower == 0 - elif constraint_type == -1: - expression_rhs = a_sub_var_lower + a_sub_fix_lower - b_lower - expression = canon_lp.slack_vars[constraint_name] == expression_rhs - _set_slack_ub(expression_rhs, canon_lp.slack_vars[constraint_name]) - elif constraint_type == 1: - expression_rhs = b_upper - a_sub_var_lower - a_sub_fix_lower - expression = canon_lp.slack_vars[constraint_name] == expression_rhs - _set_slack_ub(expression_rhs, canon_lp.slack_vars[constraint_name]) - canon_lp.constraints[constraint_name] = expression \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index 0c53f00c41e..efa4e9acb21 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -10,16 +10,14 @@ # ___________________________________________________________________________ import pyomo.environ as pe -from pyomo.opt import SolverStatus, TerminationCondition -from pyomo.gdp.util import clone_without_expression_components -from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -import aos_utils +from pyomo.contrib.alternative_solutions import aos_utils, shifted_lp, solution def enumerate_linear_solutions(model, num_solutions=10, variables='all', - rel_opt_gap=None, abs_gap=None, + rel_opt_gap=None, abs_opt_gap=None, search_mode='optimal', solver='cplex', solver_options={}, tee=False): - '''Finds alternative optimal solutions for a binary problem. + ''' + Finds alternative optimal solutions for a binary problem. Parameters ---------- @@ -58,9 +56,12 @@ def enumerate_linear_solutions(model, num_solutions=10, variables='all', A list of Solution objects. [Solution] ''' + # TODO: Set this intelligently + zero_threshold = 1e-5 print('STARTING LP ENUMERATION ANALYSIS') # For now keeping things simple + # TODO: Relax this assert variables == 'all' assert search_mode in ['optimal', 'random', 'norm'], \ @@ -83,11 +84,10 @@ def enumerate_linear_solutions(model, num_solutions=10, variables='all', # all_variables = aos_utils.get_model_variables(model, 'all', # include_fixed=True) + # TODO: Relax this if possible for var in all_variables: assert var.is_continuous(), 'Model must be an LP' - - orig_objective = aos_utils._get_active_objective(model) - + opt = pe.SolverFactory(solver) for parameter, value in solver_options.items(): opt.options[parameter] = value @@ -120,11 +120,12 @@ def enumerate_linear_solutions(model, num_solutions=10, variables='all', status = results.solver.status condition = results.solver.termination_condition if condition != pe.TerminationCondition.optimal: - raise Exception(('LP enumeration analysis cannot be applied, ' - 'SolverStatus = {}, ' + raise Exception(('Model could not be solve. LP enumeration analysis ' + 'cannot be applied, SolverStatus = {}, ' 'TerminationCondition = {}').format(status.value, condition.value)) + orig_objective = aos_utils._get_active_objective(model) orig_objective_value = pe.value(orig_objective) print('Found optimal solution, value = {}.'.format(orig_objective_value)) @@ -134,67 +135,95 @@ def enumerate_linear_solutions(model, num_solutions=10, variables='all', orig_objective_value, rel_opt_gap, abs_opt_gap) - canon_block = get_canonical_lp(model) - - - solution_number = 2 - - orig_objective.deactivate() - solutions = [solution.Solution(model, all_variables)] - - while solution_number <= num_solutions: - model.iteration = pe.Set(dimen=1) + canon_block = shifted_lp.get_shifted_linear_model(model) + cb = canon_block + + # Set K + cb.iteration = pe.Set(pe.PositiveIntegers) - model.basic_lower = pe.Var(pe.Any, domain=pe.Binary, dense=False) - model.basic_upper = pe.Var(pe.Any, domain=pe.Binary, dense=False) - model.basic_slack = pe.Var(pe.Any, domain=pe.Binary, dense=False) + # w variables + cb.basic_lower = pe.Var(pe.Any, domain=pe.Binary, dense=False) + cb.basic_upper = pe.Var(pe.Any, domain=pe.Binary, dense=False) + cb.basic_slack = pe.Var(pe.Any, domain=pe.Binary, dense=False) - model.bound_lower = pe.Constraint(pe.Any) - model.bound_upper = pe.Constraint(pe.Any) - model.bound_slack = pe.Constraint(pe.Any) - model.cut_set = pe.Constraint(pe.Any) - - variable_groups = [(model.var_lower, model.basic_lower, model.bound_lower), - (model.var_upper, model.basic_upper, model.bound_upper), - (model.slack_vars, model.basic_slack, model.bound_slack)] + # w upper bounds constraints + cb.bound_lower = pe.Constraint(pe.Any) + cb.bound_upper = pe.Constraint(pe.Any) + cb.bound_slack = pe.Constraint(pe.Any) - # Repeat until all solutions are found - solution_number = 1 - solutions = {} - while solution_number < num_solutions: + # non-zero basic variable no-good cut set + cb.cut_set = pe.Constraint(pe.PositiveIntegers) - # Solve the model unless this is the first solution and the model was - # not already solved - if solution_number > 1 or not already_solved: - print('Iteration: {}'.format(solution_number)) - results = opt.solve(model, tee=tee) - - if (((results.solver.status == SolverStatus.ok) and - (results.solver.termination_condition == TerminationCondition.optimal)) - or (already_solved and solution_number == 0)): - #objective_value = pe.value(orig_objective) + variable_groups = [(cb.var_lower, cb.basic_lower, cb.bound_lower), + (cb.var_upper, cb.basic_upper, cb.bound_upper), + (cb.slack_vars, cb.basic_slack, cb.bound_slack)] - for variable in model.var_lower: - print('Var {} = {}'.format(variable, - pe.value(model.var_lower[variable]))) + solution_number = 1 + solutions = [] + while solution_number <= num_solutions: + print('Solving Iteration {}: '.format(solution_number), end='') + results = opt.solve(cb, tee=tee) + status = results.solver.status + condition = results.solver.termination_condition + if condition == pe.TerminationCondition.optimal: + for var, index in cb.var_map.items(): + var.set_value(var.lb + cb.var_lower[index].value) + sol = solution.Solution(model, all_variables, + objective=orig_objective) + solutions.append(sol) + orig_objective_value = sol.objective[1] + print('Solved, objective = {}'.format(orig_objective_value)) + for var, index in cb.var_map.items(): + print('{} = {}'.format(var.name, var.lb + cb.var_lower[index].value)) + if hasattr(cb, 'force_out'): + cb.del_component('force_out') + if hasattr(cb, 'link_in_out'): + cb.del_component('link_in_out') - expr = 1 - num_non_zeros = 0 + if hasattr(cb, 'basic_last_lower'): + cb.del_component('basic_last_lower') + if hasattr(cb, 'basic_last_upper'): + cb.del_component('basic_last_upper') + if hasattr(cb, 'basic_last_slack'): + cb.del_component('basic_last_slack') + + cb.link_in_out = pe.Constraint(pe.Any) + cb.basic_last_lower = pe.Var(pe.Any, domain=pe.Binary, dense=False) + cb.basic_last_upper = pe.Var(pe.Any, domain=pe.Binary, dense=False) + cb.basic_last_slack = pe.Var(pe.Any, domain=pe.Binary, dense=False) + basic_last_list = [cb.basic_last_lower, cb.basic_last_upper, + cb.basic_last_slack] - for continuous_var, binary_var, constraint in variable_groups: - for variable in continuous_var: - if pe.value(continuous_var[variable]) > 1e-5: - if variable not in binary_var: - model.basic_upper[variable] - constraint[variable] = continuous_var[variable] <= \ - continuous_var[variable].ub * binary_var[variable] - expr += binary_var[variable] - num_non_zeros += 1 - model.cut_set[solution_number] = expr <= num_non_zeros - solution_number += 1 + num_non_zero = 0 + force_out_expr = -1 + non_zero_basic_expr = 1 + for idx in range(len(variable_groups)): + continuous_var, binary_var, constraint = variable_groups[idx] + for var in continuous_var: + if continuous_var[var].value > zero_threshold: + num_non_zero += 1 + if var not in binary_var: + binary_var[var] + constraint[var] = continuous_var[var] <= \ + continuous_var[var].ub * binary_var[var] + non_zero_basic_expr += binary_var[var] + basic_var = basic_last_list[idx][var] + force_out_expr += basic_var + cb.link_in_out[var] = basic_var + binary_var[var] <= 1 + cb.cut_set[solution_number] = non_zero_basic_expr <= num_non_zero + solution_number += 1 + elif (condition == pe.TerminationCondition.infeasibleOrUnbounded or + condition == pe.TerminationCondition.infeasible): + print("Infeasible, all alternative solutions have been found.") + break else: - print('Algorithm Stopped. Solver Status: {}. Solver Condition: {}.'\ - .format(results.solver.status, - results.solver.termination_condition)) - break \ No newline at end of file + print(("Unexpected solver condition. Stopping LP enumeration. " + "SolverStatus = {}, TerminationCondition = {}").format( + status.value, condition.value)) + break + + aos_block.deactivate() + print('COMPLETED LP ENUMERATION ANALYSIS') + + return solutions \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/shifted_lp.py b/pyomo/contrib/alternative_solutions/shifted_lp.py new file mode 100644 index 00000000000..bfa8ea65374 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/shifted_lp.py @@ -0,0 +1,195 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.environ as pe + +from pyomo.common.collections import ComponentMap +from pyomo.gdp.util import clone_without_expression_components +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.contrib.alternative_solutions import aos_utils + +def _get_unique_name(collection, name): + '''Create a unique name for an item that will be added to a collection.''' + if name not in collection: + return name + else: + i = 1 + while '{}_{}'.format(name, i) not in collection: + i += 1 + return '{}_{}'.format(name, i) + +def _set_slack_ub(expression, slack_var): + ''' + Use FBBT to compute an upper bound for a slack variable on an equality + expression.''' + slack_lb, slack_ub = compute_bounds_on_expr(expression) + assert slack_ub >= 0 + slack_var.setub(slack_ub) + +def get_shifted_linear_model(model, block=None): + ''' + Converts an (MI)LP with bounded (discrete and) continuous variables + (l <= x <= u) into a standard form where where all continuous variables + are non-negative reals and all contraints are equalities. For a pure LP of + the form, + + min/max cx + s.t. + A_1 * x = b_1 + A_2 * x <= b_2 + l <= x <= u + + a problem of the form, + + min/max c'z + s.t. + Bz = q + z >= 0 + + will be created and added to the returned block. z consists of var_lower + and var_upper variables that are substituted into the original x variables, + and slack_vars that are used to convert the original inequalities to + equalities. Bounds are provided on all variables in z. For MILPs, only the + continuous part of the problem is converted. + + See Lee, Sangbum., C. Phalakornkule, M. Domach, I. Grossmann, Recursive + MILP model for finding all the alternate optima in LP models for metabolic + networks, Computers & Chemical Engineering, Volume 24, Issues 2–7, 2000, + page 712 for additional details. + + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model + block : Block + The Pyomo block that the new model should be added to. + + Returns + ------- + block + The block that holds the reformulated model. + ''' + + # Gather all variables and confirm the model is bounded + all_vars = aos_utils.get_model_variables(model, 'all') + new_vars = {} + var_map = ComponentMap() + var_range = {} + for var in all_vars: + assert var.lb is not None , ('Variable {} does not have a ' + 'lower bound. All variables must be ' + 'bounded.'.format(var.name)) + assert var.ub is not None , ('Variable {} does not have an ' + 'upper bound. All variables must be ' + 'bounded.'.format(var.name)) + if var.is_continuous(): + var_name = _get_unique_name(new_vars.keys(), var.name) + new_vars[var_name] = var + var_map[var] = var_name + var_range[var_name] = (0,var.ub-var.lb) + + if block is None: + block = model + shifted_lp = aos_utils._add_aos_block(block, name='_shifted_lp') + + # Replace original variables with shifted lower and upper variables + shifted_lp.var_lower = pe.Var(new_vars.keys(), domain=pe.NonNegativeReals, + bounds=var_range) + shifted_lp.var_upper = pe.Var(new_vars.keys(), domain=pe.NonNegativeReals, + bounds=var_range) + + # Link the shifted lower and upper variables + def link_vars_rule(m, var_index): + return m.var_lower[var_index] + m.var_upper[var_index] == \ + m.var_upper[var_index].ub + shifted_lp.link_vars = pe.Constraint(new_vars.keys(), rule=link_vars_rule) + + # Map the lower and upper variables to the original variables and their + # lower bounds. This will be used to substitute x with var_lower + x.lb. + var_lower_map = {id(var): shifted_lp.var_lower[i] for i, var in \ + new_vars.items()} + var_lower_bounds = {id(var): var.lb for var in new_vars.values()} + + # Substitute the new s variables into the objective function + active_objective = aos_utils._get_active_objective(model) + c_var_lower = clone_without_expression_components(active_objective.expr, + substitute=var_lower_map) + c_fix_lower = clone_without_expression_components(active_objective.expr, + substitute=var_lower_bounds) + shifted_lp.objective = pe.Objective(expr=c_var_lower + c_fix_lower, + name=active_objective.name + '_shifted', + sense=active_objective.sense) + + # Identify all of the shifted constraints and associated slack variables + # that will need to be created + new_constraints = {} + constraint_map = ComponentMap() + constraint_type = {} + slacks = [] + for constraint in model.component_data_objects(pe.Constraint, active=True): + if constraint.parent_block() == shifted_lp: + continue + if constraint.equality: + constraint_name = constraint.name + '_equal' + constraint_name = _get_unique_name(new_constraints.keys(), + constraint.name) + new_constraints[constraint_name] = constraint + constraint_map[constraint] = constraint_name + constraint_type[constraint_name] = 0 + else: + if constraint.lb is not None: + constraint_name = constraint.name + '_lower' + constraint_name = _get_unique_name(new_constraints.keys(), + constraint.name) + new_constraints[constraint_name] = constraint + constraint_map[constraint] = constraint_name + constraint_type[constraint_name] = -1 + slacks.append(constraint_name) + if constraint.ub is not None: + constraint_name = constraint.name + '_upper' + constraint_name = _get_unique_name(new_constraints.keys(), + constraint.name) + new_constraints[constraint_name] = constraint + constraint_map[constraint] = constraint_name + constraint_type[constraint_name] = 1 + slacks.append(constraint_name) + shifted_lp.constraint_index = pe.Set(initialize=new_constraints.keys()) + shifted_lp.slack_index = pe.Set(initialize=slacks) + shifted_lp.slack_vars = pe.Var(shifted_lp.slack_index, + domain=pe.NonNegativeReals) + shifted_lp.constraints = pe.Constraint(shifted_lp.constraint_index) + + for constraint_name, constraint in new_constraints.items(): + a_sub_var_lower = clone_without_expression_components(constraint.body, + substitute=var_lower_map) + a_sub_fix_lower = clone_without_expression_components(constraint.body, + substitute=var_lower_bounds) + b_lower = constraint.lb + b_upper = constraint.ub + con_type = constraint_type[constraint_name] + if con_type == 0: + expr = a_sub_var_lower + a_sub_fix_lower - b_lower == 0 + elif con_type == -1: + expr_rhs = a_sub_var_lower + a_sub_fix_lower - b_lower + expr = shifted_lp.slack_vars[constraint_name] == expr_rhs + _set_slack_ub(expr_rhs, shifted_lp.slack_vars[constraint_name]) + elif con_type == 1: + expr_rhs = b_upper - a_sub_var_lower - a_sub_fix_lower + expr = shifted_lp.slack_vars[constraint_name] == expr_rhs + _set_slack_ub(expr_rhs, shifted_lp.slack_vars[constraint_name]) + shifted_lp.constraints[constraint_name] = expr + + shifted_lp.var_map = var_map + shifted_lp.new_vars = new_vars + shifted_lp.constraint_map = constraint_map + shifted_lp.new_constraints = new_constraints + + return shifted_lp \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index 40f2b771dfa..85715e996aa 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -145,6 +145,23 @@ def get_2d_unbounded_problem(): return m +def get_2d_degenerate_lp(): + ''' + Simple 2d problem that includes a redundant contraint such that three + constraints are active at optimality.''' + m = pe.ConcreteModel() + + m.x = pe.Var(within=pe.Reals, bounds=(-1,3)) + m.y = pe.Var(within=pe.Reals, bounds=(-3,2)) + + m.obj = pe.Objective(expr=m.x+2*m.y, sense=pe.maximize) + + m.con1 = pe.Constraint(expr=m.x+m.y<=3) + m.con2 = pe.Constraint(expr=m.x+2*m.y<=5) + m.con3 = pe.Constraint(expr=m.x+m.y>=-1) + + return m + def get_triangle_ip(): ''' Simple 2d discrete problem where the feasible region looks like a 90-45-45 From 546959f645ab9125384f9e519ce8fd52be67b030 Mon Sep 17 00:00:00 2001 From: jlgearh Date: Fri, 20 Oct 2023 15:41:06 -0600 Subject: [PATCH 0114/3044] - Added missing constraint from Lee paper --- pyomo/contrib/alternative_solutions/lp_enum.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index efa4e9acb21..a9cb36803e4 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -210,6 +210,7 @@ def enumerate_linear_solutions(model, num_solutions=10, variables='all', basic_var = basic_last_list[idx][var] force_out_expr += basic_var cb.link_in_out[var] = basic_var + binary_var[var] <= 1 + cb.force_out = pe.Constraint(expr=force_out_expr >= 0) cb.cut_set[solution_number] = non_zero_basic_expr <= num_non_zero solution_number += 1 From c100bf917e9a31b90611041c707e5902d0cc04da Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 23 Oct 2023 14:34:46 -0600 Subject: [PATCH 0115/3044] Resolve broken import --- pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py index 6451db18087..1fb9b87de2e 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py @@ -7,7 +7,6 @@ from pyomo.common.log import LoggingIntercept from pyomo.common.tee import capture_output from pyomo.contrib.appsi.solvers.highs import Highs -from pyomo.contrib.appsi.base import TerminationCondition opt = Highs() From 332d28694e1acc8247262539e5a03b87f52596e1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 23 Oct 2023 14:51:57 -0600 Subject: [PATCH 0116/3044] Resolving conflicts again: stream_solver -> tee --- pyomo/contrib/appsi/solvers/highs.py | 2 +- pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index da4ea8c130a..3d2104cdbfa 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -349,7 +349,7 @@ def set_instance(self, model): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.stream_solver: + if self.config.tee: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: with capture_output(output=t.STDOUT, capture_fd=True): diff --git a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py index 1fb9b87de2e..da39a5c3d55 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py @@ -94,7 +94,7 @@ def test_capture_highs_output(self): model[-2:-1] = [ 'opt = Highs()', - 'opt.config.stream_solver = True', + 'opt.config.tee = True', 'result = opt.solve(m)', ] with LoggingIntercept() as LOG, capture_output(capture_fd=True) as OUT: From 0b348a0ac8a73fdaae9e628aefa17446c23b9584 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 23 Oct 2023 15:09:40 -0600 Subject: [PATCH 0117/3044] Resolve convergence of APPSI and new Results object --- pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py | 6 +++--- .../contrib/appsi/solvers/tests/test_persistent_solvers.py | 4 ++-- pyomo/solver/tests/test_results.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py index da39a5c3d55..25b7ae91b86 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py @@ -37,7 +37,7 @@ def test_mutable_params_with_remove_cons(self): del m.c1 m.p2.value = 2 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, -8) + self.assertAlmostEqual(res.incumbent_objective, -8) def test_mutable_params_with_remove_vars(self): m = pe.ConcreteModel() @@ -59,14 +59,14 @@ def test_mutable_params_with_remove_vars(self): opt = Highs() res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) del m.c1 del m.c2 m.p1.value = -9 m.p2.value = 9 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, -9) + self.assertAlmostEqual(res.incumbent_objective, -9) def test_capture_highs_output(self): # tests issue #3003 diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 1b9f5c3b0a2..299a5bd5b7e 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1354,13 +1354,13 @@ def test_bug_2(self, name: str, opt_class: Type[PersistentSolverBase], only_chil m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 2, 5) + self.assertAlmostEqual(res.incumbent_objective, 2, 5) m.x.unfix() m.x.setlb(-9) m.x.setub(9) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, -18, 5) + self.assertAlmostEqual(res.incumbent_objective, -18, 5) @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/test_results.py index f43b2b50ef4..5392c1135f8 100644 --- a/pyomo/solver/tests/test_results.py +++ b/pyomo/solver/tests/test_results.py @@ -35,7 +35,7 @@ def test_member_list(self): 'interrupted', 'licensingProblems', ] - self.assertEqual(member_list, expected_list) + self.assertEqual(member_list.sort(), expected_list.sort()) def test_codes(self): self.assertEqual(results.TerminationCondition.unknown.value, 42) From 457c5993dcfe73ee95064a54ac9e41a33fe9e0e8 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 23 Oct 2023 15:19:55 -0600 Subject: [PATCH 0118/3044] Resolve one more convergence error --- pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py index 25b7ae91b86..cd65783c566 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py @@ -32,7 +32,7 @@ def test_mutable_params_with_remove_cons(self): opt = Highs() res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.incumbent_objective, 1) del m.c1 m.p2.value = 2 From 0641565d6bf4f8ef571f4c72a7d0381fa320ad95 Mon Sep 17 00:00:00 2001 From: jlgearh Date: Tue, 24 Oct 2023 21:52:38 -0600 Subject: [PATCH 0119/3044] - Added script to run lp enumeration --- .../alternative_solutions/tests/run_lp_enum.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 pyomo/contrib/alternative_solutions/tests/run_lp_enum.py diff --git a/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py b/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py new file mode 100644 index 00000000000..cb63a4df7ed --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +""" +Created on Fri Oct 20 11:55:46 2023 + +@author: jlgearh +""" + +import pyomo.contrib.alternative_solutions.tests.test_cases as tc +from pyomo.contrib.alternative_solutions import lp_enum + +m = tc.get_2d_degenerate_lp() +sols = lp_enum.enumerate_linear_solutions(m) \ No newline at end of file From a58dc41a3d15eec8cda39cc5642b34ad39fd45f0 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 26 Oct 2023 08:39:42 -0600 Subject: [PATCH 0120/3044] Obvious bug fix --- pyomo/solver/IPOPT.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 6501154d7ac..875f8710b10 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -84,11 +84,11 @@ def version(self): @property def config(self): - return self._config + return self.config @config.setter def config(self, val): - self._config = val + self.config = val def solve(self, model, **kwds): # Check if solver is available From 6bcdadbfcdb9b81abe987aed665c5137c45b7c07 Mon Sep 17 00:00:00 2001 From: Zedong Peng Date: Thu, 26 Oct 2023 21:12:46 -0400 Subject: [PATCH 0121/3044] fix bug --- pyomo/contrib/mindtpy/algorithm_base_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index c254a8db72b..c0afea58a99 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -831,7 +831,7 @@ def init_rNLP(self, add_oa_cuts=True): subprob_terminate_cond = results.solver.termination_condition # Sometimes, the NLP solver might be trapped in a infeasible solution if the objective function is nonlinear and partition_obj_nonlinear_terms is True. If this happens, we will use the original objective function instead. - if subprob_terminate_cond == tc.infeasible and config.partition_obj_nonlinear_terms: + if subprob_terminate_cond == tc.infeasible and config.partition_obj_nonlinear_terms and self.rnlp.MindtPy_utils.objective_list[0].expr.polynomial_degree() not in self.mip_objective_polynomial_degree: config.logger.info( 'Initial relaxed NLP problem is infeasible. This might be related to partition_obj_nonlinear_terms. Try to solve it again without partitioning nonlinear objective function.') self.rnlp.MindtPy_utils.objective.deactivate() From a66f955500686fef09d8605571ef6f5c238101f8 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Sun, 29 Oct 2023 15:04:27 -0400 Subject: [PATCH 0122/3044] improve copy_var_list_values function --- pyomo/contrib/mindtpy/single_tree.py | 76 ++++++++++++------------- pyomo/contrib/mindtpy/util.py | 83 +++++++++++++++------------- 2 files changed, 83 insertions(+), 76 deletions(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 8b8e171c577..2174d093009 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -24,6 +24,7 @@ from pyomo.opt import TerminationCondition as tc from pyomo.core import minimize, value from pyomo.core.expr import identify_variables +import math cplex, cplex_available = attempt_import('cplex') @@ -41,7 +42,6 @@ def copy_lazy_var_list_values( config, skip_stale=False, skip_fixed=True, - ignore_integrality=False, ): """This function copies variable values from one list to another. @@ -71,43 +71,43 @@ def copy_lazy_var_list_values( if skip_fixed and v_to.is_fixed(): continue # Skip fixed variables. v_val = self.get_values(opt._pyomo_var_to_solver_var_map[v_from]) - try: - # We don't want to trigger the reset of the global stale - # indicator, so we will set this variable to be "stale", - # knowing that set_value will switch it back to "not - # stale" - v_to.stale = True - # NOTE: PEP 2180 changes the var behavior so that domain - # / bounds violations no longer generate exceptions (and - # instead log warnings). This means that the following - # will always succeed and the ValueError should never be - # raised. - v_to.set_value(v_val, skip_validation=True) - except ValueError as e: - # Snap the value to the bounds - config.logger.error(e) - if ( - v_to.has_lb() - and v_val < v_to.lb - and v_to.lb - v_val <= config.variable_tolerance - ): - v_to.set_value(v_to.lb, skip_validation=True) - elif ( - v_to.has_ub() - and v_val > v_to.ub - and v_val - v_to.ub <= config.variable_tolerance - ): - v_to.set_value(v_to.ub, skip_validation=True) - # ... or the nearest integer - elif v_to.is_integer(): - rounded_val = int(round(v_val)) - if ( - ignore_integrality - or abs(v_val - rounded_val) <= config.integer_tolerance - ) and rounded_val in v_to.domain: - v_to.set_value(rounded_val, skip_validation=True) - else: - raise + rounded_val = int(round(v_val)) + # We don't want to trigger the reset of the global stale + # indicator, so we will set this variable to be "stale", + # knowing that set_value will switch it back to "not + # stale" + v_to.stale = True + # NOTE: PEP 2180 changes the var behavior so that domain + # / bounds violations no longer generate exceptions (and + # instead log warnings). This means that the following + # will always succeed and the ValueError should never be + # raised. + if v_val in v_to.domain \ + and not ((v_to.has_lb() and v_val < v_to.lb)) \ + and not ((v_to.has_ub() and v_val > v_to.ub)): + v_to.set_value(v_val) + # Snap the value to the bounds + # TODO: check the performance of + # v_to.lb - v_val <= config.variable_tolerance + elif ( + v_to.has_lb() + and v_val < v_to.lb + # and v_to.lb - v_val <= config.variable_tolerance + ): + v_to.set_value(v_to.lb) + elif ( + v_to.has_ub() + and v_val > v_to.ub + # and v_val - v_to.ub <= config.variable_tolerance + ): + v_to.set_value(v_to.ub) + # ... or the nearest integer + elif v_to.is_integer() and math.fabs(v_val - rounded_val) <= config.integer_tolerance: # and rounded_val in v_to.domain: + v_to.set_value(rounded_val) + elif abs(v_val) <= config.zero_tolerance and 0 in v_to.domain: + v_to.set_value(0) + else: + raise ValueError('copy_lazy_var_list_values failed.') def add_lazy_oa_cuts( self, diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 4dfb912e611..da1534b49ac 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -684,41 +684,42 @@ def copy_var_list_values_from_solution_pool( Whether to ignore the integrality of integer variables, by default False. """ for v_from, v_to in zip(from_list, to_list): - try: - if config.mip_solver == 'cplex_persistent': - var_val = solver_model.solution.pool.get_values( - solution_name, var_map[v_from] - ) - elif config.mip_solver == 'gurobi_persistent': - solver_model.setParam(gurobipy.GRB.Param.SolutionNumber, solution_name) - var_val = var_map[v_from].Xn - # We don't want to trigger the reset of the global stale - # indicator, so we will set this variable to be "stale", - # knowing that set_value will switch it back to "not - # stale" - v_to.stale = True - # NOTE: PEP 2180 changes the var behavior so that domain / - # bounds violations no longer generate exceptions (and - # instead log warnings). This means that the following will - # always succeed and the ValueError should never be raised. + if config.mip_solver == 'cplex_persistent': + var_val = solver_model.solution.pool.get_values( + solution_name, var_map[v_from] + ) + elif config.mip_solver == 'gurobi_persistent': + solver_model.setParam(gurobipy.GRB.Param.SolutionNumber, solution_name) + var_val = var_map[v_from].Xn + # We don't want to trigger the reset of the global stale + # indicator, so we will set this variable to be "stale", + # knowing that set_value will switch it back to "not + # stale" + v_to.stale = True + rounded_val = int(round(var_val)) + # NOTE: PEP 2180 changes the var behavior so that domain / + # bounds violations no longer generate exceptions (and + # instead log warnings). This means that the following will + # always succeed and the ValueError should never be raised. + if var_val in v_to.domain \ + and not ((v_to.has_lb() and var_val < v_to.lb)) \ + and not ((v_to.has_ub() and var_val > v_to.ub)): v_to.set_value(var_val, skip_validation=True) - except ValueError as e: - config.logger.error(e) - rounded_val = int(round(var_val)) - # Check to see if this is just a tolerance issue - if ignore_integrality and v_to.is_integer(): - v_to.set_value(var_val, skip_validation=True) - elif v_to.is_integer() and ( - abs(var_val - rounded_val) <= config.integer_tolerance - ): - v_to.set_value(rounded_val, skip_validation=True) - elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: - v_to.set_value(0, skip_validation=True) - else: - config.logger.error( - 'Unknown validation domain error setting variable %s' % (v_to.name,) - ) - raise + elif v_to.has_lb() and var_val < v_to.lb: + v_to.set_value(v_to.lb) + elif v_to.has_ub() and var_val > v_to.ub: + v_to.set_value(v_to.ub) + # Check to see if this is just a tolerance issue + elif ignore_integrality and v_to.is_integer(): + v_to.set_value(var_val, skip_validation=True) + elif v_to.is_integer() and ( + abs(var_val - rounded_val) <= config.integer_tolerance + ): + v_to.set_value(rounded_val, skip_validation=True) + elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: + v_to.set_value(0, skip_validation=True) + else: + raise ValueError("copy_var_list_values_from_solution_pool failed.") class GurobiPersistent4MindtPy(GurobiPersistent): @@ -980,14 +981,20 @@ def copy_var_list_values(from_list, to_list, config, continue # Skip fixed variables. var_val = value(v_from, exception=False) rounded_val = int(round(var_val)) - if var_val in v_to.domain: + if var_val in v_to.domain \ + and not ((v_to.has_lb() and var_val < v_to.lb)) \ + and not ((v_to.has_ub() and var_val > v_to.ub)): v_to.set_value(value(v_from, exception=False)) + elif v_to.has_lb() and var_val < v_to.lb: + v_to.set_value(v_to.lb) + elif v_to.has_ub() and var_val > v_to.ub: + v_to.set_value(v_to.ub) elif ignore_integrality and v_to.is_integer(): v_to.set_value(value(v_from, exception=False), skip_validation=True) - elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: - v_to.set_value(0) elif v_to.is_integer() and (math.fabs(var_val - rounded_val) <= config.integer_tolerance): v_to.set_value(rounded_val) + elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: + v_to.set_value(0) else: - raise + raise ValueError("copy_var_list_values failed.") From cefd4a66a06711e90005d20cd470efca762754a6 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Sun, 29 Oct 2023 15:12:02 -0400 Subject: [PATCH 0123/3044] fix FP bug --- pyomo/contrib/mindtpy/algorithm_base_class.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index c254a8db72b..3e53559b3df 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -2384,6 +2384,7 @@ def handle_fp_subproblem_optimal(self, fp_nlp): fp_nlp.MindtPy_utils.variable_list, self.working_model.MindtPy_utils.variable_list, self.config, + ignore_integrality=True ) add_orthogonality_cuts(self.working_model, self.mip, self.config) From 905503b13907a610a1e7f9d8620ee88127551ec5 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 1 Nov 2023 00:42:53 -0400 Subject: [PATCH 0124/3044] fix gurobi single tree termination check bug --- pyomo/contrib/mindtpy/single_tree.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 2174d093009..9595a9fc9be 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -910,19 +910,7 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): if mindtpy_solver.dual_bound != mindtpy_solver.dual_bound_progress[0]: mindtpy_solver.add_regularization() - if ( - abs(mindtpy_solver.primal_bound - mindtpy_solver.dual_bound) - <= config.absolute_bound_tolerance - ): - config.logger.info( - 'MindtPy exiting on bound convergence. ' - '|Primal Bound: {} - Dual Bound: {}| <= (absolute tolerance {}) \n'.format( - mindtpy_solver.primal_bound, - mindtpy_solver.dual_bound, - config.absolute_bound_tolerance, - ) - ) - mindtpy_solver.results.solver.termination_condition = tc.optimal + if mindtpy_solver.bounds_converged() or mindtpy_solver.reached_time_limit(): cb_opt._solver_model.terminate() return From 52cb54f9418b081f44f1a3887e6dae03ebf9710f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 1 Nov 2023 11:45:20 -0600 Subject: [PATCH 0125/3044] Fixing some places where we mix up binaries and Booleans in the hull tests --- pyomo/gdp/tests/test_hull.py | 72 ++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 09f65765fe6..b7b5a11e28c 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -510,10 +510,10 @@ def test_disaggregatedVar_mappings(self): for i in [0, 1]: mappings = ComponentMap() mappings[m.x] = disjBlock[i].disaggregatedVars.x - if i == 1: # this disjunct as x, w, and no y + if i == 1: # this disjunct has x, w, and no y mappings[m.w] = disjBlock[i].disaggregatedVars.w mappings[m.y] = transBlock._disaggregatedVars[0] - elif i == 0: # this disjunct as x, y, and no w + elif i == 0: # this disjunct has x, y, and no w mappings[m.y] = disjBlock[i].disaggregatedVars.y mappings[m.w] = transBlock._disaggregatedVars[1] @@ -1427,16 +1427,16 @@ def test_relaxation_feasibility(self): solver = SolverFactory(linear_solvers[0]) cases = [ - (1, 1, 1, 1, None), - (0, 0, 0, 0, None), - (1, 0, 0, 0, None), - (0, 1, 0, 0, 1.1), - (0, 0, 1, 0, None), - (0, 0, 0, 1, None), - (1, 1, 0, 0, None), - (1, 0, 1, 0, 1.2), - (1, 0, 0, 1, 1.3), - (1, 0, 1, 1, None), + (True, True, True, True, None), + (False, False, False, False, None), + (True, False, False, False, None), + (False, True, False, False, 1.1), + (False, False, True, False, None), + (False, False, False, True, None), + (True, True, False, False, None), + (True, False, True, False, 1.2), + (True, False, False, True, 1.3), + (True, False, True, True, None), ] for case in cases: m.d1.indicator_var.fix(case[0]) @@ -1468,16 +1468,16 @@ def test_relaxation_feasibility_transform_inner_first(self): solver = SolverFactory(linear_solvers[0]) cases = [ - (1, 1, 1, 1, None), - (0, 0, 0, 0, None), - (1, 0, 0, 0, None), - (0, 1, 0, 0, 1.1), - (0, 0, 1, 0, None), - (0, 0, 0, 1, None), - (1, 1, 0, 0, None), - (1, 0, 1, 0, 1.2), - (1, 0, 0, 1, 1.3), - (1, 0, 1, 1, None), + (True, True, True, True, None), + (False, False, False, False, None), + (True, False, False, False, None), + (False, True, False, False, 1.1), + (False, False, True, False, None), + (False, False, False, True, None), + (True, True, False, False, None), + (True, False, True, False, 1.2), + (True, False, False, True, 1.3), + (True, False, True, True, None), ] for case in cases: m.d1.indicator_var.fix(case[0]) @@ -1722,10 +1722,10 @@ def test_disaggregated_vars_are_set_to_0_correctly(self): hull.apply_to(m) # this should be a feasible integer solution - m.d1.indicator_var.fix(0) - m.d2.indicator_var.fix(1) - m.d3.indicator_var.fix(0) - m.d4.indicator_var.fix(0) + m.d1.indicator_var.fix(False) + m.d2.indicator_var.fix(True) + m.d3.indicator_var.fix(False) + m.d4.indicator_var.fix(False) results = SolverFactory(linear_solvers[0]).solve(m) self.assertEqual( @@ -1739,10 +1739,10 @@ def test_disaggregated_vars_are_set_to_0_correctly(self): self.assertEqual(value(hull.get_disaggregated_var(m.x, m.d4)), 0) # and what if one of the inner disjuncts is true? - m.d1.indicator_var.fix(1) - m.d2.indicator_var.fix(0) - m.d3.indicator_var.fix(1) - m.d4.indicator_var.fix(0) + m.d1.indicator_var.fix(True) + m.d2.indicator_var.fix(False) + m.d3.indicator_var.fix(True) + m.d4.indicator_var.fix(False) results = SolverFactory(linear_solvers[0]).solve(m) self.assertEqual( @@ -2398,12 +2398,12 @@ def OneCentroidPerPt(m, i): TransformationFactory('gdp.hull').apply_to(m) # fix an optimal solution - m.AssignPoint[1, 1].indicator_var.fix(1) - m.AssignPoint[1, 2].indicator_var.fix(0) - m.AssignPoint[2, 1].indicator_var.fix(0) - m.AssignPoint[2, 2].indicator_var.fix(1) - m.AssignPoint[3, 1].indicator_var.fix(1) - m.AssignPoint[3, 2].indicator_var.fix(0) + m.AssignPoint[1, 1].indicator_var.fix(True) + m.AssignPoint[1, 2].indicator_var.fix(False) + m.AssignPoint[2, 1].indicator_var.fix(False) + m.AssignPoint[2, 2].indicator_var.fix(True) + m.AssignPoint[3, 1].indicator_var.fix(True) + m.AssignPoint[3, 2].indicator_var.fix(False) m.cluster_center[1].fix(0.3059) m.cluster_center[2].fix(0.8043) From 45b61d111b9d28b3862baa39bc1d89fe8007244f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 1 Nov 2023 14:51:46 -0600 Subject: [PATCH 0126/3044] Adding some new test for edge cases with nested GDP in hull --- pyomo/gdp/tests/test_hull.py | 97 ++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index b7b5a11e28c..118ee4ca69a 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -1832,6 +1832,103 @@ def d_r(e): cons = hull.get_disaggregation_constraint(m.x, m.d_r.inner_disj) assertExpressionsEqual(self, cons.expr, x2 == x3 + x4) + def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): + m = ConcreteModel() + m.x = Var(bounds=(0, 10)) + m.y = Var(bounds=(-4, 5)) + m.parent1 = Disjunct() + m.parent2 = Disjunct() + m.parent2.c = Constraint(expr=m.x == 0) + m.parent_disjunction = Disjunction(expr=[m.parent1, m.parent2]) + m.child1 = Disjunct() + m.child1.c = Constraint(expr=m.x <= 8) + m.child2 = Disjunct() + m.child2.c = Constraint(expr=m.x + m.y <= 3) + m.child3 = Disjunct() + m.child3.c = Constraint(expr=m.x <= 7) + m.parent1.disjunction = Disjunction(expr=[m.child1, m.child2, m.child3]) + + hull = TransformationFactory('gdp.hull') + hull.apply_to(m) + + y_c2 = hull.get_disaggregated_var(m.y, m.child2) + self.assertEqual(y_c2.bounds, (-4, 5)) + other_y = hull.get_disaggregated_var(m.y, m.child1) + self.assertEqual(other_y.bounds, (-4, 5)) + other_other_y = hull.get_disaggregated_var(m.y, m.child3) + self.assertIs(other_y, other_other_y) + y_p1 = hull.get_disaggregated_var(m.y, m.parent1) + self.assertEqual(y_p1.bounds, (-4, 5)) + y_p2 = hull.get_disaggregated_var(m.y, m.parent2) + self.assertEqual(y_p2.bounds, (-4, 5)) + y_cons = hull.get_disaggregation_constraint(m.y, m.parent1.disjunction) + # check that the disaggregated ys in the nested just sum to the original + assertExpressionsEqual(self, y_cons.expr, y_p1 == other_y + y_c2) + y_cons = hull.get_disaggregation_constraint(m.y, m.parent_disjunction) + assertExpressionsEqual(self, y_cons.expr, m.y == y_p1 + y_p2) + + x_c1 = hull.get_disaggregated_var(m.x, m.child1) + x_c2 = hull.get_disaggregated_var(m.x, m.child2) + x_c3 = hull.get_disaggregated_var(m.x, m.child3) + x_p1 = hull.get_disaggregated_var(m.x, m.parent1) + x_p2 = hull.get_disaggregated_var(m.x, m.parent2) + x_cons_parent = hull.get_disaggregation_constraint(m.x, m.parent_disjunction) + assertExpressionsEqual(self, x_cons_parent.expr, m.x == x_p1 + x_p2) + x_cons_child = hull.get_disaggregation_constraint(m.x, m.parent1.disjunction) + assertExpressionsEqual(self, x_cons_child.expr, x_p1 == x_c1 + x_c2 + x_c3) + + def test_nested_with_var_that_skips_a_level(self): + m = ConcreteModel() + + m.x = Var(bounds=(-2, 9)) + m.y = Var(bounds=(-3, 8)) + + m.y1 = Disjunct() + m.y1.c1 = Constraint(expr=m.x >= 4) + m.y1.z1 = Disjunct() + m.y1.z1.c1 = Constraint(expr=m.y == 0) + m.y1.z1.w1 = Disjunct() + m.y1.z1.w1.c1 = Constraint(expr=m.x == 0) + m.y1.z1.w2 = Disjunct() + m.y1.z1.w2.c1 = Constraint(expr=m.x >= 1) + m.y1.z1.disjunction = Disjunction(expr=[m.y1.z1.w1, m.y1.z1.w2]) + m.y1.z2 = Disjunct() + m.y1.z2.c1 = Constraint(expr=m.y == 1) + m.y1.disjunction = Disjunction(expr=[m.y1.z1, m.y1.z2]) + m.y2 = Disjunct() + m.y2.c1 = Constraint(expr=m.x == 0) + m.disjunction = Disjunction(expr=[m.y1, m.y2]) + + hull = TransformationFactory('gdp.hull') + hull.apply_to(m) + + x_y1 = hull.get_disaggregated_var(m.x, m.y1) + x_y2 = hull.get_disaggregated_var(m.x, m.y2) + x_z1 = hull.get_disaggregated_var(m.x, m.y1.z1) + x_z2 = hull.get_disaggregated_var(m.x, m.y1.z2) + x_w1 = hull.get_disaggregated_var(m.x, m.y1.z1.w1) + x_w2 = hull.get_disaggregated_var(m.x, m.y1.z1.w2) + + y_z1 = hull.get_disaggregated_var(m.y, m.y1.z1) + y_z2 = hull.get_disaggregated_var(m.y, m.y1.z2) + y_y1 = hull.get_disaggregated_var(m.y, m.y1) + y_y2 = hull.get_disaggregated_var(m.y, m.y2) + + cons = hull.get_disaggregation_constraint(m.x, m.y1.z1.disjunction) + assertExpressionsEqual(self, cons.expr, x_z1 == x_w1 + x_w2) + cons = hull.get_disaggregation_constraint(m.x, m.y1.disjunction) + assertExpressionsEqual(self, cons.expr, x_y1 == x_z2 + x_z1) + cons = hull.get_disaggregation_constraint(m.x, m.disjunction) + assertExpressionsEqual(self, cons.expr, m.x == x_y1 + x_y2) + + cons = hull.get_disaggregation_constraint(m.y, m.y1.z1.disjunction, + raise_exception=False) + self.assertIsNone(cons) + cons = hull.get_disaggregation_constraint(m.y, m.y1.disjunction) + assertExpressionsEqual(self, cons.expr, y_y1 == y_z1 + y_z2) + cons = hull.get_disaggregation_constraint(m.y, m.disjunction) + assertExpressionsEqual(self, cons.expr, m.y == y_y2 + y_y1) + class TestSpecialCases(unittest.TestCase): def test_local_vars(self): From b741882fa52f052c3cccf4e4c2a530cbfabfa209 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 1 Nov 2023 14:53:09 -0600 Subject: [PATCH 0127/3044] Adding option to not raise an exception when looking for disaggregated vars and constraints on transformed model --- pyomo/gdp/plugins/hull.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index b8e2b3e3699..6086bd61ad1 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -885,7 +885,7 @@ def _add_local_var_suffix(self, disjunct): % (disjunct.getname(fully_qualified=True), localSuffix.ctype) ) - def get_disaggregated_var(self, v, disjunct): + def get_disaggregated_var(self, v, disjunct, raise_exception=True): """ Returns the disaggregated variable corresponding to the Var v and the Disjunct disjunct. @@ -903,11 +903,13 @@ def get_disaggregated_var(self, v, disjunct): try: return transBlock._disaggregatedVarMap['disaggregatedVar'][disjunct][v] except: - logger.error( - "It does not appear '%s' is a " - "variable that appears in disjunct '%s'" % (v.name, disjunct.name) - ) - raise + if raise_exception: + logger.error( + "It does not appear '%s' is a " + "variable that appears in disjunct '%s'" % (v.name, disjunct.name) + ) + raise + return none def get_src_var(self, disaggregated_var): """ @@ -944,7 +946,8 @@ def get_src_var(self, disaggregated_var): # retrieves the disaggregation constraint for original_var resulting from # transforming disjunction - def get_disaggregation_constraint(self, original_var, disjunction): + def get_disaggregation_constraint(self, original_var, disjunction, + raise_exception=True): """ Returns the disaggregation (re-aggregation?) constraint (which links the disaggregated variables to their original) @@ -974,12 +977,14 @@ def get_disaggregation_constraint(self, original_var, disjunction): ._disaggregationConstraintMap[original_var][disjunction] ) except: - logger.error( - "It doesn't appear that '%s' is a variable that was " - "disaggregated by Disjunction '%s'" - % (original_var.name, disjunction.name) - ) - raise + if raise_exception: + logger.error( + "It doesn't appear that '%s' is a variable that was " + "disaggregated by Disjunction '%s'" + % (original_var.name, disjunction.name) + ) + raise + return None def get_var_bounds_constraint(self, v): """ From b1bd5d5aeead1bd1d676968cc2ff6556154f8a6b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 1 Nov 2023 16:18:31 -0600 Subject: [PATCH 0128/3044] Putting transformed components on parent Block always, a lot of performance improvements in the variable gathering logic --- pyomo/gdp/plugins/hull.py | 50 +++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 6086bd61ad1..fcb992ed6c7 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -262,7 +262,6 @@ def _apply_to_impl(self, instance, **kwds): t, t.index(), parent_disjunct=gdp_tree.parent(t), - root_disjunct=gdp_tree.root_disjunct(t), ) # We skip disjuncts now, because we need information from the # disjunctions to transform them (which variables to disaggregate), @@ -298,9 +297,7 @@ def _add_transformation_block(self, to_block): return transBlock, True - def _transform_disjunctionData( - self, obj, index, parent_disjunct=None, root_disjunct=None - ): + def _transform_disjunctionData(self, obj, index, parent_disjunct=None): # Hull reformulation doesn't work if this is an OR constraint. So if # xor is false, give up if not obj.xor: @@ -310,8 +307,12 @@ def _transform_disjunctionData( "Must be an XOR!" % obj.name ) + # We put *all* transformed things on the parent Block of this + # disjunction. We'll mark the disaggregated Vars as local, but beyond + # that, we actually need everything to get transformed again as we go up + # the nested hierarchy (if there is one) transBlock, xorConstraint = self._setup_transform_disjunctionData( - obj, root_disjunct + obj, root_disjunct=None ) disaggregationConstraint = transBlock.disaggregationConstraints @@ -325,7 +326,8 @@ def _transform_disjunctionData( varOrder = [] varsByDisjunct = ComponentMap() localVarsByDisjunct = ComponentMap() - include_fixed_vars = not self._config.assume_fixed_vars_permanent + disjunctsVarAppearsIn = ComponentMap() + setOfDisjunctsVarAppearsIn = ComponentMap() for disjunct in obj.disjuncts: if not disjunct.active: continue @@ -338,7 +340,7 @@ def _transform_disjunctionData( Constraint, active=True, sort=SortComponents.deterministic, - descend_into=(Block, Disjunct), + descend_into=Block, ): # [ESJ 02/14/2020] By default, we disaggregate fixed variables # on the philosophy that fixing is not a promise for the future @@ -348,8 +350,8 @@ def _transform_disjunctionData( # assume_fixed_vars_permanent to True in which case we will skip # them for var in EXPR.identify_variables( - cons.body, include_fixed=include_fixed_vars - ): + cons.body, include_fixed=not + self._config.assume_fixed_vars_permanent): # Note the use of a list so that we will # eventually disaggregate the vars in a # deterministic order (the order that we found @@ -358,6 +360,12 @@ def _transform_disjunctionData( if not var in varOrder_set: varOrder.append(var) varOrder_set.add(var) + disjunctsVarAppearsIn[var] = [disjunct] + setOfDisjunctsVarAppearsIn[var] = ComponentSet([disjunct]) + else: + if disjunct not in setOfDisjunctsVarAppearsIn[var]: + disjunctsVarAppearsIn[var].append(disjunct) + setOfDisjunctsVarAppearsIn[var].add(disjunct) # check for LocalVars Suffix localVarsByDisjunct = self._get_local_var_suffixes( @@ -368,7 +376,6 @@ def _transform_disjunctionData( # being local. Since we transform from leaf to root, we are implicitly # treating our own disaggregated variables as local, so they will not be # re-disaggregated. - varSet = [] varSet = {disj: [] for disj in obj.disjuncts} # Note that variables are local with respect to a Disjunct. We deal with # them here to do some error checking (if something is obviously not @@ -379,11 +386,8 @@ def _transform_disjunctionData( # localVars of a Disjunct later) localVars = ComponentMap() varsToDisaggregate = [] - disjunctsVarAppearsIn = ComponentMap() for var in varOrder: - disjuncts = disjunctsVarAppearsIn[var] = [ - d for d in varsByDisjunct if var in varsByDisjunct[d] - ] + disjuncts = disjunctsVarAppearsIn[var] # clearly not local if used in more than one disjunct if len(disjuncts) > 1: if self._generate_debug_messages: @@ -398,8 +402,7 @@ def _transform_disjunctionData( # disjuncts is a list of length 1 elif localVarsByDisjunct.get(disjuncts[0]) is not None: if var in localVarsByDisjunct[disjuncts[0]]: - localVars_thisDisjunct = localVars.get(disjuncts[0]) - if localVars_thisDisjunct is not None: + if localVars.get(disjuncts[0]) is not None: localVars[disjuncts[0]].append(var) else: localVars[disjuncts[0]] = [var] @@ -408,7 +411,8 @@ def _transform_disjunctionData( varSet[disjuncts[0]].append(var) varsToDisaggregate.append(var) else: - # We don't even have have any local vars for this Disjunct. + # The user didn't declare any local vars for this Disjunct, so + # we know we're disaggregating it varSet[disjuncts[0]].append(var) varsToDisaggregate.append(var) @@ -497,18 +501,8 @@ def _transform_disjunctionData( ) disaggregatedExpr += disaggregatedVar - # We equate the sum of the disaggregated vars to var (the original) - # if parent_disjunct is None, else it needs to be the disaggregated - # var corresponding to var on the parent disjunct. This is the - # reason we transform from root to leaf: This constraint is now - # correct regardless of how nested something may have been. - parent_var = ( - var - if parent_disjunct is None - else self.get_disaggregated_var(var, parent_disjunct) - ) cons_idx = len(disaggregationConstraint) - disaggregationConstraint.add(cons_idx, parent_var == disaggregatedExpr) + disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) # and update the map so that we can find this later. We index by # variable and the particular disjunction because there is a # different one for each disjunction From 207874428016f3e94459a148881786b05c370d32 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 1 Nov 2023 16:31:01 -0600 Subject: [PATCH 0129/3044] A few more performance things --- pyomo/gdp/plugins/hull.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index fcb992ed6c7..2469ba9c93c 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -464,14 +464,15 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): (idx, 'ub'), var_free, ) - # maintain the mappings + # For every Disjunct the Var does not appear in, we want to map + # that this new variable is its disaggreggated variable. for disj in obj.disjuncts: # Because we called _transform_disjunct above, we know that # if this isn't transformed it is because it was cleanly # deactivated, and we can just skip it. if ( disj._transformation_block is not None - and disj not in disjunctsVarAppearsIn[var] + and disj not in setOfDisjunctsVarAppearsIn[var] ): relaxationBlock = disj._transformation_block().parent_block() relaxationBlock._bigMConstraintMap[ @@ -488,12 +489,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): else: disaggregatedExpr = 0 for disjunct in disjunctsVarAppearsIn[var]: - if disjunct._transformation_block is None: - # Because we called _transform_disjunct above, we know that - # if this isn't transformed it is because it was cleanly - # deactivated, and we can just skip it. - continue - + # We know this Disjunct was active, so it has been transformed now. disaggregatedVar = ( disjunct._transformation_block() .parent_block() @@ -502,6 +498,8 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): disaggregatedExpr += disaggregatedVar cons_idx = len(disaggregationConstraint) + # We always aggregate to the original var. If this is nested, this + # constraint will be transformed again. disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) # and update the map so that we can find this later. We index by # variable and the particular disjunction because there is a From 563168f085d3ccb3d1291e0ad6afddca7a729a3f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 1 Nov 2023 16:34:06 -0600 Subject: [PATCH 0130/3044] Transform from leaf to root in hull --- pyomo/gdp/plugins/hull.py | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 2469ba9c93c..25a0606dc1c 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -253,7 +253,10 @@ def _apply_to_impl(self, instance, **kwds): # Preprocess in order to find what disjunctive components need # transformation gdp_tree = self._get_gdp_tree_from_targets(instance, targets) - preprocessed_targets = gdp_tree.topological_sort() + # Transform from leaf to root: This is important for hull because for + # nested GDPs, we will introduce variables that need disaggregating into + # parent Disjuncts as we transform their child Disjunctions. + preprocessed_targets = gdp_tree.reverse_topological_sort() self._targets_set = set(preprocessed_targets) for t in preprocessed_targets: @@ -565,8 +568,8 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) ) for var in localVars: - # we don't need to disaggregated, we can use this Var, but we do - # need to set up its bounds constraints. + # we don't need to disaggregate, i.e., we can use this Var, but we + # do need to set up its bounds constraints. # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we @@ -671,24 +674,6 @@ def _get_local_var_set(self, disjunction): return local_var_set - def _warn_for_active_disjunct( - self, innerdisjunct, outerdisjunct, var_substitute_map, zero_substitute_map - ): - # We override the base class method because in hull, it might just be - # that we haven't gotten here yet. - disjuncts = ( - innerdisjunct.values() if innerdisjunct.is_indexed() else (innerdisjunct,) - ) - for disj in disjuncts: - if disj in self._targets_set: - # We're getting to this, have some patience. - continue - else: - # But if it wasn't in the targets after preprocessing, it - # doesn't belong in an active Disjunction that we are - # transforming and we should be confused. - _warn_for_active_disjunct(innerdisjunct, outerdisjunct) - def _transform_constraint( self, obj, disjunct, var_substitute_map, zero_substitute_map ): From 3b01104f586f9a33d1386ff89c5c3e57b3ae7fc7 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 1 Nov 2023 21:24:39 -0400 Subject: [PATCH 0131/3044] fix Gurobi single tree cycle handling --- pyomo/contrib/mindtpy/algorithm_base_class.py | 3 +++ pyomo/contrib/mindtpy/single_tree.py | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index a0216b5d054..33b2f2c1d04 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -106,6 +106,8 @@ def __init__(self, **kwds): self.curr_int_sol = [] self.should_terminate = False self.integer_list = [] + # dictionary {integer solution (list): cuts index (list)} + self.int_sol_2_cuts_ind = dict() # Set up iteration counters self.nlp_iter = 0 @@ -794,6 +796,7 @@ def MindtPy_initialization(self): self.integer_list.append(self.curr_int_sol) fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) + self.int_sol_2_cuts_ind[self.curr_int_sol] = list(range(1, len(self.mip.MindtPy_utils.cuts.oa_cuts) + 1)) elif config.init_strategy == 'FP': self.init_rNLP() self.fp_loop() diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 9595a9fc9be..dacde73a79e 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -941,15 +941,26 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): ) return elif config.strategy == 'OA': + # Refer to the official document of GUROBI. + # Your callback should be prepared to cut off solutions that violate any of your lazy constraints, including those that have already been added. Node solutions will usually respect previously added lazy constraints, but not always. + # https://www.gurobi.com/documentation/current/refman/cs_cb_addlazy.html + # If this happens, MindtPy will look for the index of corresponding cuts, instead of solving the fixed-NLP again. + for ind in mindtpy_solver.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol]: + cb_opt.cbLazy(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts[ind]) return else: mindtpy_solver.integer_list.append(mindtpy_solver.curr_int_sol) + if config.strategy == 'OA': + cut_ind = len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts) # solve subproblem # The constraint linearization happens in the handlers fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() mindtpy_solver.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result, cb_opt) + if config.strategy == 'OA': + # store the cut index corresponding to current integer solution. + mindtpy_solver.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol] = list(range(cut_ind + 1, len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts) + 1)) def handle_lazy_main_feasible_solution_gurobi(cb_m, cb_opt, mindtpy_solver, config): From 35c119bf90f78e05593ae75305a1fef9ec833553 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 2 Nov 2023 14:15:33 -0600 Subject: [PATCH 0132/3044] Clarifying a lot in hull's local var suffix handling, starting to update some tests, fixing a bug in GDPTree.parent_disjunct method --- pyomo/gdp/plugins/hull.py | 94 ++++++++++++++++++------------------ pyomo/gdp/tests/test_hull.py | 21 +++++++- pyomo/gdp/util.py | 5 +- 3 files changed, 71 insertions(+), 49 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 25a0606dc1c..86bd738eb09 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -204,16 +204,16 @@ def __init__(self): super().__init__(logger) self._targets = set() - def _add_local_vars(self, block, local_var_dict): + def _collect_local_vars_from_block(self, block, local_var_dict): localVars = block.component('LocalVars') - if type(localVars) is Suffix: + if localVars is not None and localVars.ctype is Suffix: for disj, var_list in localVars.items(): - if local_var_dict.get(disj) is None: - local_var_dict[disj] = ComponentSet(var_list) - else: + if disj in local_var_dict: local_var_dict[disj].update(var_list) + else: + local_var_dict[disj] = ComponentSet(var_list) - def _get_local_var_suffixes(self, block, local_var_dict): + def _get_local_vars_from_suffixes(self, block, local_var_dict): # You can specify suffixes on any block (disjuncts included). This # method starts from a Disjunct (presumably) and checks for a LocalVar # suffixes going both up and down the tree, adding them into the @@ -222,16 +222,14 @@ def _get_local_var_suffixes(self, block, local_var_dict): # first look beneath where we are (there could be Blocks on this # disjunct) for b in block.component_data_objects( - Block, descend_into=(Block), active=True, sort=SortComponents.deterministic + Block, descend_into=Block, active=True, sort=SortComponents.deterministic ): - self._add_local_vars(b, local_var_dict) + self._collect_local_vars_from_block(b, local_var_dict) # now traverse upwards and get what's above while block is not None: - self._add_local_vars(block, local_var_dict) + self._collect_local_vars_from_block(block, local_var_dict) block = block.parent_block() - return local_var_dict - def _apply_to(self, instance, **kwds): try: self._apply_to_impl(instance, **kwds) @@ -239,7 +237,6 @@ def _apply_to(self, instance, **kwds): self._restore_state() self._transformation_blocks.clear() self._algebraic_constraints.clear() - self._targets_set = set() def _apply_to_impl(self, instance, **kwds): self._process_arguments(instance, **kwds) @@ -257,14 +254,13 @@ def _apply_to_impl(self, instance, **kwds): # nested GDPs, we will introduce variables that need disaggregating into # parent Disjuncts as we transform their child Disjunctions. preprocessed_targets = gdp_tree.reverse_topological_sort() - self._targets_set = set(preprocessed_targets) for t in preprocessed_targets: if t.ctype is Disjunction: self._transform_disjunctionData( t, t.index(), - parent_disjunct=gdp_tree.parent(t), + gdp_tree.parent(t), ) # We skip disjuncts now, because we need information from the # disjunctions to transform them (which variables to disaggregate), @@ -300,7 +296,7 @@ def _add_transformation_block(self, to_block): return transBlock, True - def _transform_disjunctionData(self, obj, index, parent_disjunct=None): + def _transform_disjunctionData(self, obj, index, parent_disjunct): # Hull reformulation doesn't work if this is an OR constraint. So if # xor is false, give up if not obj.xor: @@ -371,9 +367,12 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): setOfDisjunctsVarAppearsIn[var].add(disjunct) # check for LocalVars Suffix - localVarsByDisjunct = self._get_local_var_suffixes( - disjunct, localVarsByDisjunct - ) + # [ESJ 11/2/23] TODO: This could be a lot more efficient if we + # centralized it. Right now we walk up the tree to the root model + # for each Disjunct, which is pretty dumb. We could get + # user-speficied suffixes once, and then we know where we will + # create ours, or we can just track what we create. + self._get_local_vars_from_suffixes(disjunct, localVarsByDisjunct) # We will disaggregate all variables that are not explicitly declared as # being local. Since we transform from leaf to root, we are implicitly @@ -387,7 +386,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): # transform the Disjuncts: Values of localVarsByDisjunct are # ComponentSets, so we need this for determinism (we iterate through the # localVars of a Disjunct later) - localVars = ComponentMap() + localVars = {disj: [] for disj in obj.disjuncts} varsToDisaggregate = [] for var in varOrder: disjuncts = disjunctsVarAppearsIn[var] @@ -405,10 +404,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): # disjuncts is a list of length 1 elif localVarsByDisjunct.get(disjuncts[0]) is not None: if var in localVarsByDisjunct[disjuncts[0]]: - if localVars.get(disjuncts[0]) is not None: - localVars[disjuncts[0]].append(var) - else: - localVars[disjuncts[0]] = [var] + localVars[disjuncts[0]].append(var) else: # It's not local to this Disjunct varSet[disjuncts[0]].append(var) @@ -421,7 +417,10 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. - local_var_set = self._get_local_var_set(obj) + print("obj: %s" % obj) + print("parent disjunct: %s" % parent_disjunct) + parent_local_var_list = self._get_local_var_list(parent_disjunct) + print("parent_local_var_list: %s" % parent_local_var_list) or_expr = 0 for disjunct in obj.disjuncts: or_expr += disjunct.indicator_var.get_associated_binary() @@ -429,11 +428,10 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): disjunct, transBlock, varSet[disjunct], - localVars.get(disjunct, []), - local_var_set, + localVars[disjunct], + parent_local_var_list, ) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var - xorConstraint.add(index, (or_expr, rhs)) + xorConstraint.add(index, (or_expr, 1)) # map the DisjunctionData to its XOR constraint to mark it as # transformed obj._algebraic_constraint = weakref_ref(xorConstraint[index]) @@ -452,8 +450,8 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): disaggregated_var = disaggregatedVars[idx] # mark this as local because we won't re-disaggregate if this is # a nested disjunction - if local_var_set is not None: - local_var_set.append(disaggregated_var) + if parent_local_var_list is not None: + parent_local_var_list.append(disaggregated_var) var_free = 1 - sum( disj.indicator_var.get_associated_binary() for disj in disjunctsVarAppearsIn[var] @@ -518,7 +516,8 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct=None): # deactivate for the writers obj.deactivate() - def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set): + def _transform_disjunct(self, obj, transBlock, varSet, localVars, + parent_local_var_list): # We're not using the preprocessed list here, so this could be # inactive. We've already done the error checking in preprocessing, so # we just skip it here. @@ -535,6 +534,7 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) # add the disaggregated variables and their bigm constraints # to the relaxationBlock for var in varSet: + print("disaggregating %s" % var) disaggregatedVar = Var(within=Reals, initialize=var.value) # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we @@ -547,8 +547,8 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) ) # mark this as local because we won't re-disaggregate if this is a # nested disjunction - if local_var_set is not None: - local_var_set.append(disaggregatedVar) + if parent_local_var_list is not None: + parent_local_var_list.append(disaggregatedVar) # add the bigm constraint bigmConstraint = Constraint(transBlock.lbub) @@ -568,6 +568,7 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, local_var_set) ) for var in localVars: + print("we knew %s was local" % var) # we don't need to disaggregate, i.e., we can use this Var, but we # do need to set up its bounds constraints. @@ -652,27 +653,23 @@ def _declare_disaggregated_var_bounds( transBlock._disaggregatedVarMap['srcVar'][disaggregatedVar] = original_var transBlock._bigMConstraintMap[disaggregatedVar] = bigmConstraint - def _get_local_var_set(self, disjunction): - # add Suffix to the relaxation block that disaggregated variables are - # local (in case this is nested in another Disjunct) - local_var_set = None - parent_disjunct = disjunction.parent_block() - while parent_disjunct is not None: - if parent_disjunct.ctype is Disjunct: - break - parent_disjunct = parent_disjunct.parent_block() + def _get_local_var_list(self, parent_disjunct): + # Add or retrieve Suffix from parent_disjunct so that, if this is + # nested, we can use it to declare that the disaggregated variables are + # local. We return the list so that we can add to it. + local_var_list = None if parent_disjunct is not None: # This limits the cases that a user is allowed to name something # (other than a Suffix) 'LocalVars' on a Disjunct. But I am assuming # that the Suffix has to be somewhere above the disjunct in the # tree, so I can't put it on a Block that I own. And if I'm coopting # something of theirs, it may as well be here. - self._add_local_var_suffix(parent_disjunct) + self._get_local_var_suffix(parent_disjunct) if parent_disjunct.LocalVars.get(parent_disjunct) is None: parent_disjunct.LocalVars[parent_disjunct] = [] - local_var_set = parent_disjunct.LocalVars[parent_disjunct] + local_var_list = parent_disjunct.LocalVars[parent_disjunct] - return local_var_set + return local_var_list def _transform_constraint( self, obj, disjunct, var_substitute_map, zero_substitute_map @@ -847,7 +844,7 @@ def _transform_constraint( # deactivate now that we have transformed obj.deactivate() - def _add_local_var_suffix(self, disjunct): + def _get_local_var_suffix(self, disjunct): # If the Suffix is there, we will borrow it. If not, we make it. If it's # something else, we complain. localSuffix = disjunct.component("LocalVars") @@ -948,7 +945,7 @@ def get_disaggregation_constraint(self, original_var, disjunction, ) try: - return ( + cons = ( transBlock() .parent_block() ._disaggregationConstraintMap[original_var][disjunction] @@ -962,6 +959,9 @@ def get_disaggregation_constraint(self, original_var, disjunction, ) raise return None + while not cons.active: + cons = self.get_transformed_constraints(cons)[0] + return cons def get_var_bounds_constraint(self, v): """ diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 118ee4ca69a..b8aa332174b 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -41,6 +41,7 @@ import pyomo.core.expr as EXPR from pyomo.core.base import constraint from pyomo.repn import generate_standard_repn +from pyomo.repn.linear import LinearRepnVisitor from pyomo.gdp import Disjunct, Disjunction, GDP_Error import pyomo.gdp.tests.models as models @@ -1877,6 +1878,15 @@ def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): x_cons_child = hull.get_disaggregation_constraint(m.x, m.parent1.disjunction) assertExpressionsEqual(self, x_cons_child.expr, x_p1 == x_c1 + x_c2 + x_c3) + def simplify_cons(self, cons): + visitor = LinearRepnVisitor({}, {}, {}) + lb = cons.lower + ub = cons.upper + self.assertEqual(cons.lb, cons.ub) + repn = visitor.walk_expression(cons.body) + self.assertIsNone(repn.nonlinear) + return repn.to_expression(visitor) == lb + def test_nested_with_var_that_skips_a_level(self): m = ConcreteModel() @@ -1915,18 +1925,27 @@ def test_nested_with_var_that_skips_a_level(self): y_y2 = hull.get_disaggregated_var(m.y, m.y2) cons = hull.get_disaggregation_constraint(m.x, m.y1.z1.disjunction) - assertExpressionsEqual(self, cons.expr, x_z1 == x_w1 + x_w2) + self.assertTrue(cons.active) + cons_expr = self.simplify_cons(cons) + print(cons_expr) + print("") + print(x_z1 - x_w2 - x_w1 == 0) + assertExpressionsEqual(self, cons_expr, x_z1 - x_w2 - x_w1 == 0) cons = hull.get_disaggregation_constraint(m.x, m.y1.disjunction) + self.assertTrue(cons.active) assertExpressionsEqual(self, cons.expr, x_y1 == x_z2 + x_z1) cons = hull.get_disaggregation_constraint(m.x, m.disjunction) + self.assertTrue(cons.active) assertExpressionsEqual(self, cons.expr, m.x == x_y1 + x_y2) cons = hull.get_disaggregation_constraint(m.y, m.y1.z1.disjunction, raise_exception=False) self.assertIsNone(cons) cons = hull.get_disaggregation_constraint(m.y, m.y1.disjunction) + self.assertTrue(cons.active) assertExpressionsEqual(self, cons.expr, y_y1 == y_z1 + y_z2) cons = hull.get_disaggregation_constraint(m.y, m.disjunction) + self.assertTrue(cons.active) assertExpressionsEqual(self, cons.expr, m.y == y_y2 + y_y1) diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index b460a3d691c..b5e74f73c38 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -169,7 +169,10 @@ def parent_disjunct(self, u): Arg: u : A node in the forest """ - return self.parent(self.parent(u)) + if isinstance(u, _DisjunctData) or u.ctype is Disjunct: + return self.parent(self.parent(u)) + else: + return self.parent(u) def root_disjunct(self, u): """Returns the highest parent Disjunct in the hierarchy, or None if From 88cae4ab976a0eda0b5ef652e9629dd13e9458b8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 2 Nov 2023 16:45:21 -0600 Subject: [PATCH 0133/3044] Cleaning up a lot of mess using the fact that ComponentSets are ordered, simplifying how we deal with local vars significantly. --- pyomo/gdp/plugins/hull.py | 363 ++++++++++++++++++++------------------ 1 file changed, 190 insertions(+), 173 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 86bd738eb09..80ac55d45fe 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -11,6 +11,8 @@ import logging +from collections import defaultdict + import pyomo.common.config as cfg from pyomo.common import deprecated from pyomo.common.collections import ComponentMap, ComponentSet @@ -39,6 +41,7 @@ Binary, ) from pyomo.gdp import Disjunct, Disjunction, GDP_Error +from pyomo.gdp.disjunct import _DisjunctData from pyomo.gdp.plugins.gdp_to_mip_transformation import GDP_to_MIP_Transformation from pyomo.gdp.transformed_disjunct import _TransformedDisjunct from pyomo.gdp.util import ( @@ -208,27 +211,51 @@ def _collect_local_vars_from_block(self, block, local_var_dict): localVars = block.component('LocalVars') if localVars is not None and localVars.ctype is Suffix: for disj, var_list in localVars.items(): - if disj in local_var_dict: - local_var_dict[disj].update(var_list) - else: - local_var_dict[disj] = ComponentSet(var_list) - - def _get_local_vars_from_suffixes(self, block, local_var_dict): - # You can specify suffixes on any block (disjuncts included). This - # method starts from a Disjunct (presumably) and checks for a LocalVar - # suffixes going both up and down the tree, adding them into the - # dictionary that is the second argument. - - # first look beneath where we are (there could be Blocks on this - # disjunct) - for b in block.component_data_objects( - Block, descend_into=Block, active=True, sort=SortComponents.deterministic - ): - self._collect_local_vars_from_block(b, local_var_dict) - # now traverse upwards and get what's above - while block is not None: - self._collect_local_vars_from_block(block, local_var_dict) - block = block.parent_block() + local_var_dict[disj].update(var_list) + + def _get_user_defined_local_vars(self, targets): + user_defined_local_vars = defaultdict(lambda: ComponentSet()) + seen_blocks = set() + # we go through the targets looking both up and down the hierarchy, but + # we cache what Blocks/Disjuncts we've already looked on so that we + # don't duplicate effort. + for t in targets: + if t.ctype is Disjunct or isinstance(t, _DisjunctData): + # first look beneath where we are (there could be Blocks on this + # disjunct) + for b in t.component_data_objects(Block, descend_into=Block, + active=True, + sort=SortComponents.deterministic + ): + if b not in seen_blocks: + self._collect_local_vars_from_block(b, user_defined_local_vars) + seen_blocks.add(b) + # now look up in the tree + blk = t + while blk is not None: + if blk not in seen_blocks: + self._collect_local_vars_from_block(blk, + user_defined_local_vars) + seen_blocks.add(blk) + blk = blk.parent_block() + return user_defined_local_vars + + # def _get_local_vars_from_suffixes(self, block, local_var_dict): + # # You can specify suffixes on any block (disjuncts included). This + # # method starts from a Disjunct (presumably) and checks for a LocalVar + # # suffixes going both up and down the tree, adding them into the + # # dictionary that is the second argument. + + # # first look beneath where we are (there could be Blocks on this + # # disjunct) + # for b in block.component_data_objects( + # Block, descend_into=Block, active=True, sort=SortComponents.deterministic + # ): + # self._collect_local_vars_from_block(b, local_var_dict) + # # now traverse upwards and get what's above + # while block is not None: + # self._collect_local_vars_from_block(block, local_var_dict) + # block = block.parent_block() def _apply_to(self, instance, **kwds): try: @@ -254,6 +281,8 @@ def _apply_to_impl(self, instance, **kwds): # nested GDPs, we will introduce variables that need disaggregating into # parent Disjuncts as we transform their child Disjunctions. preprocessed_targets = gdp_tree.reverse_topological_sort() + local_vars_by_disjunct = self._get_user_defined_local_vars( + preprocessed_targets) for t in preprocessed_targets: if t.ctype is Disjunction: @@ -261,6 +290,7 @@ def _apply_to_impl(self, instance, **kwds): t, t.index(), gdp_tree.parent(t), + local_vars_by_disjunct ) # We skip disjuncts now, because we need information from the # disjunctions to transform them (which variables to disaggregate), @@ -296,7 +326,8 @@ def _add_transformation_block(self, to_block): return transBlock, True - def _transform_disjunctionData(self, obj, index, parent_disjunct): + def _transform_disjunctionData(self, obj, index, parent_disjunct, + local_vars_by_disjunct): # Hull reformulation doesn't work if this is an OR constraint. So if # xor is false, give up if not obj.xor: @@ -321,16 +352,11 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct): # We first go through and collect all the variables that we # are going to disaggregate. - varOrder_set = ComponentSet() - varOrder = [] - varsByDisjunct = ComponentMap() - localVarsByDisjunct = ComponentMap() - disjunctsVarAppearsIn = ComponentMap() - setOfDisjunctsVarAppearsIn = ComponentMap() + var_order = ComponentSet() + disjuncts_var_appears_in = ComponentMap() for disjunct in obj.disjuncts: if not disjunct.active: continue - disjunctVars = varsByDisjunct[disjunct] = ComponentSet() # create the key for each disjunct now transBlock._disaggregatedVarMap['disaggregatedVar'][ disjunct @@ -351,45 +377,22 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct): for var in EXPR.identify_variables( cons.body, include_fixed=not self._config.assume_fixed_vars_permanent): - # Note the use of a list so that we will - # eventually disaggregate the vars in a - # deterministic order (the order that we found - # them) - disjunctVars.add(var) - if not var in varOrder_set: - varOrder.append(var) - varOrder_set.add(var) - disjunctsVarAppearsIn[var] = [disjunct] - setOfDisjunctsVarAppearsIn[var] = ComponentSet([disjunct]) + # Note that, because ComponentSets are ordered, we will + # eventually disaggregate the vars in a deterministic order + # (the order that we found them) + if var not in var_order: + var_order.add(var) + disjuncts_var_appears_in[var] = ComponentSet([disjunct]) else: - if disjunct not in setOfDisjunctsVarAppearsIn[var]: - disjunctsVarAppearsIn[var].append(disjunct) - setOfDisjunctsVarAppearsIn[var].add(disjunct) - - # check for LocalVars Suffix - # [ESJ 11/2/23] TODO: This could be a lot more efficient if we - # centralized it. Right now we walk up the tree to the root model - # for each Disjunct, which is pretty dumb. We could get - # user-speficied suffixes once, and then we know where we will - # create ours, or we can just track what we create. - self._get_local_vars_from_suffixes(disjunct, localVarsByDisjunct) + disjuncts_var_appears_in[var].add(disjunct) # We will disaggregate all variables that are not explicitly declared as # being local. Since we transform from leaf to root, we are implicitly # treating our own disaggregated variables as local, so they will not be # re-disaggregated. - varSet = {disj: [] for disj in obj.disjuncts} - # Note that variables are local with respect to a Disjunct. We deal with - # them here to do some error checking (if something is obviously not - # local since it is used in multiple Disjuncts in this Disjunction) and - # also to get a deterministic order in which to process them when we - # transform the Disjuncts: Values of localVarsByDisjunct are - # ComponentSets, so we need this for determinism (we iterate through the - # localVars of a Disjunct later) - localVars = {disj: [] for disj in obj.disjuncts} - varsToDisaggregate = [] - for var in varOrder: - disjuncts = disjunctsVarAppearsIn[var] + vars_to_disaggregate = {disj: ComponentSet() for disj in obj.disjuncts} + for var in var_order: + disjuncts = disjuncts_var_appears_in[var] # clearly not local if used in more than one disjunct if len(disjuncts) > 1: if self._generate_debug_messages: @@ -399,21 +402,18 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct): % var.getname(fully_qualified=True) ) for disj in disjuncts: - varSet[disj].append(var) - varsToDisaggregate.append(var) - # disjuncts is a list of length 1 - elif localVarsByDisjunct.get(disjuncts[0]) is not None: - if var in localVarsByDisjunct[disjuncts[0]]: - localVars[disjuncts[0]].append(var) + vars_to_disaggregate[disj].add(var) + else: # disjuncts is a set of length 1 + disjunct = next(iter(disjuncts)) + if disjunct in local_vars_by_disjunct: + if var not in local_vars_by_disjunct[disjunct]: + # It's not declared local to this Disjunct, so we + # disaggregate + vars_to_disaggregate[disjunct].add(var) else: - # It's not local to this Disjunct - varSet[disjuncts[0]].append(var) - varsToDisaggregate.append(var) - else: - # The user didn't declare any local vars for this Disjunct, so - # we know we're disaggregating it - varSet[disjuncts[0]].append(var) - varsToDisaggregate.append(var) + # The user didn't declare any local vars for this + # Disjunct, so we know we're disaggregating it + vars_to_disaggregate[disjunct].add(var) # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. @@ -424,106 +424,111 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct): or_expr = 0 for disjunct in obj.disjuncts: or_expr += disjunct.indicator_var.get_associated_binary() - self._transform_disjunct( - disjunct, - transBlock, - varSet[disjunct], - localVars[disjunct], - parent_local_var_list, - ) + if obj.active: + self._transform_disjunct( + disjunct, + transBlock, + vars_to_disaggregate[disjunct], + local_vars_by_disjunct.get(disjunct, []), + parent_local_var_list, + local_vars_by_disjunct[parent_disjunct] + ) xorConstraint.add(index, (or_expr, 1)) # map the DisjunctionData to its XOR constraint to mark it as # transformed obj._algebraic_constraint = weakref_ref(xorConstraint[index]) # add the reaggregation constraints - for i, var in enumerate(varsToDisaggregate): - # There are two cases here: Either the var appeared in every - # disjunct in the disjunction, or it didn't. If it did, there's - # nothing special to do: All of the disaggregated variables have - # been created, and we can just proceed and make this constraint. If - # it didn't, we need one more disaggregated variable, correctly - # defined. And then we can make the constraint. - if len(disjunctsVarAppearsIn[var]) < len(obj.disjuncts): - # create one more disaggregated var - idx = len(disaggregatedVars) - disaggregated_var = disaggregatedVars[idx] - # mark this as local because we won't re-disaggregate if this is - # a nested disjunction - if parent_local_var_list is not None: - parent_local_var_list.append(disaggregated_var) - var_free = 1 - sum( - disj.indicator_var.get_associated_binary() - for disj in disjunctsVarAppearsIn[var] - ) - self._declare_disaggregated_var_bounds( - var, - disaggregated_var, - obj, - disaggregated_var_bounds, - (idx, 'lb'), - (idx, 'ub'), - var_free, - ) - # For every Disjunct the Var does not appear in, we want to map - # that this new variable is its disaggreggated variable. - for disj in obj.disjuncts: - # Because we called _transform_disjunct above, we know that - # if this isn't transformed it is because it was cleanly - # deactivated, and we can just skip it. - if ( - disj._transformation_block is not None - and disj not in setOfDisjunctsVarAppearsIn[var] - ): - relaxationBlock = disj._transformation_block().parent_block() - relaxationBlock._bigMConstraintMap[ - disaggregated_var - ] = Reference(disaggregated_var_bounds[idx, :]) - relaxationBlock._disaggregatedVarMap['srcVar'][ - disaggregated_var - ] = var - relaxationBlock._disaggregatedVarMap['disaggregatedVar'][disj][ - var - ] = disaggregated_var - - disaggregatedExpr = disaggregated_var - else: - disaggregatedExpr = 0 - for disjunct in disjunctsVarAppearsIn[var]: - # We know this Disjunct was active, so it has been transformed now. - disaggregatedVar = ( - disjunct._transformation_block() - .parent_block() - ._disaggregatedVarMap['disaggregatedVar'][disjunct][var] - ) - disaggregatedExpr += disaggregatedVar - - cons_idx = len(disaggregationConstraint) - # We always aggregate to the original var. If this is nested, this - # constraint will be transformed again. - disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) - # and update the map so that we can find this later. We index by - # variable and the particular disjunction because there is a - # different one for each disjunction - if disaggregationConstraintMap.get(var) is not None: - disaggregationConstraintMap[var][obj] = disaggregationConstraint[ - cons_idx - ] - else: - thismap = disaggregationConstraintMap[var] = ComponentMap() - thismap[obj] = disaggregationConstraint[cons_idx] + i = 0 + for disj in obj.disjuncts: + if not disj.active: + continue + for var in vars_to_disaggregate[disj]: + # There are two cases here: Either the var appeared in every + # disjunct in the disjunction, or it didn't. If it did, there's + # nothing special to do: All of the disaggregated variables have + # been created, and we can just proceed and make this constraint. If + # it didn't, we need one more disaggregated variable, correctly + # defined. And then we can make the constraint. + if len(disjuncts_var_appears_in[var]) < len(obj.disjuncts): + # create one more disaggregated var + idx = len(disaggregatedVars) + disaggregated_var = disaggregatedVars[idx] + # mark this as local because we won't re-disaggregate if this is + # a nested disjunction + if parent_local_var_list is not None: + parent_local_var_list.append(disaggregated_var) + local_vars_by_disjunct[parent_disjunct].add(disaggregated_var) + var_free = 1 - sum( + disj.indicator_var.get_associated_binary() + for disj in disjuncts_var_appears_in[var] + ) + self._declare_disaggregated_var_bounds( + var, + disaggregated_var, + obj, + disaggregated_var_bounds, + (idx, 'lb'), + (idx, 'ub'), + var_free, + ) + # For every Disjunct the Var does not appear in, we want to map + # that this new variable is its disaggreggated variable. + for disj in obj.disjuncts: + # Because we called _transform_disjunct above, we know that + # if this isn't transformed it is because it was cleanly + # deactivated, and we can just skip it. + if ( + disj._transformation_block is not None + and disj not in disjuncts_var_appears_in[var] + ): + relaxationBlock = disj._transformation_block().\ + parent_block() + relaxationBlock._bigMConstraintMap[ + disaggregated_var + ] = Reference(disaggregated_var_bounds[idx, :]) + relaxationBlock._disaggregatedVarMap['srcVar'][ + disaggregated_var + ] = var + relaxationBlock._disaggregatedVarMap[ + 'disaggregatedVar'][disj][ + var + ] = disaggregated_var + + disaggregatedExpr = disaggregated_var + else: + disaggregatedExpr = 0 + for disjunct in disjuncts_var_appears_in[var]: + # We know this Disjunct was active, so it has been transformed now. + disaggregatedVar = ( + disjunct._transformation_block() + .parent_block() + ._disaggregatedVarMap['disaggregatedVar'][disjunct][var] + ) + disaggregatedExpr += disaggregatedVar + + cons_idx = len(disaggregationConstraint) + # We always aggregate to the original var. If this is nested, this + # constraint will be transformed again. + disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) + # and update the map so that we can find this later. We index by + # variable and the particular disjunction because there is a + # different one for each disjunction + if disaggregationConstraintMap.get(var) is not None: + disaggregationConstraintMap[var][obj] = disaggregationConstraint[ + cons_idx + ] + else: + thismap = disaggregationConstraintMap[var] = ComponentMap() + thismap[obj] = disaggregationConstraint[cons_idx] + + i += 1 # deactivate for the writers obj.deactivate() - def _transform_disjunct(self, obj, transBlock, varSet, localVars, - parent_local_var_list): - # We're not using the preprocessed list here, so this could be - # inactive. We've already done the error checking in preprocessing, so - # we just skip it here. - if not obj.active: - return - + def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, + parent_local_var_suffix, parent_disjunct_local_vars): relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) # Put the disaggregated variables all on their own block so that we can @@ -533,7 +538,7 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, # add the disaggregated variables and their bigm constraints # to the relaxationBlock - for var in varSet: + for var in vars_to_disaggregate: print("disaggregating %s" % var) disaggregatedVar = Var(within=Reals, initialize=var.value) # naming conflicts are possible here since this is a bunch @@ -545,10 +550,13 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, relaxationBlock.disaggregatedVars.add_component( disaggregatedVarName, disaggregatedVar ) - # mark this as local because we won't re-disaggregate if this is a - # nested disjunction - if parent_local_var_list is not None: - parent_local_var_list.append(disaggregatedVar) + # mark this as local via the Suffix in case this is a partial + # transformation: + if parent_local_var_suffix is not None: + parent_local_var_suffix.append(disaggregatedVar) + # Record that it's local for our own bookkeeping in case we're in a + # nested situation in *this* transformation + parent_disjunct_local_vars.add(disaggregatedVar) # add the bigm constraint bigmConstraint = Constraint(transBlock.lbub) @@ -567,7 +575,13 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, transBlock, ) - for var in localVars: + for var in local_vars: + if var in vars_to_disaggregate: + logger.warning( + "Var '%s' was declared as a local Var for Disjunct '%s', " + "but it appeared in multiple Disjuncts, so it will be " + "disaggregated." % (var.name, obj.name)) + continue print("we knew %s was local" % var) # we don't need to disaggregate, i.e., we can use this Var, but we # do need to set up its bounds constraints. @@ -604,13 +618,16 @@ def _transform_disjunct(self, obj, transBlock, varSet, localVars, obj ].items() ) - zero_substitute_map.update((id(v), ZeroConstant) for v in localVars) + zero_substitute_map.update((id(v), ZeroConstant) for v in local_vars) # Transform each component within this disjunct self._transform_block_components( obj, obj, var_substitute_map, zero_substitute_map ) + # Anything that was local to this Disjunct is also local to the parent, + # and just got "promoted" up there, so to speak. + parent_disjunct_local_vars.update(local_vars) # deactivate disjunct so writers can be happy obj._deactivate_without_fixing_indicator() From a54bb122afff2c187ef93e88ee34f53c28e010ce Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 3 Nov 2023 10:43:45 -0600 Subject: [PATCH 0134/3044] Fixing a bug where we use Disjunct active status after we've transformed them, which is useless becuase we've deactivated them --- pyomo/gdp/plugins/hull.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 80ac55d45fe..35656b2ff0a 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -54,6 +54,7 @@ logger = logging.getLogger('pyomo.gdp.hull') +from pytest import set_trace @TransformationFactory.register( 'gdp.hull', doc="Relax disjunctive model by forming the hull reformulation." @@ -336,6 +337,10 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, "Disjunction '%s' with OR constraint. " "Must be an XOR!" % obj.name ) + # collect the Disjuncts we are going to transform now because we will + # change their active status when we transform them, but still need this + # list after the fact. + active_disjuncts = [disj for disj in obj.disjuncts if disj.active] # We put *all* transformed things on the parent Block of this # disjunction. We'll mark the disaggregated Vars as local, but beyond @@ -354,9 +359,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # are going to disaggregate. var_order = ComponentSet() disjuncts_var_appears_in = ComponentMap() - for disjunct in obj.disjuncts: - if not disjunct.active: - continue + for disjunct in active_disjuncts: # create the key for each disjunct now transBlock._disaggregatedVarMap['disaggregatedVar'][ disjunct @@ -440,9 +443,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # add the reaggregation constraints i = 0 - for disj in obj.disjuncts: - if not disj.active: - continue + for disj in active_disjuncts: for var in vars_to_disaggregate[disj]: # There are two cases here: Either the var appeared in every # disjunct in the disjunction, or it didn't. If it did, there's @@ -510,6 +511,9 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, cons_idx = len(disaggregationConstraint) # We always aggregate to the original var. If this is nested, this # constraint will be transformed again. + print("Adding disaggregation constraint for '%s' on Disjunction '%s' " + "to Block '%s'" % + (var, obj, disaggregationConstraint.parent_block())) disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) # and update the map so that we can find this later. We index by # variable and the particular disjunction because there is a @@ -951,7 +955,7 @@ def get_disaggregation_constraint(self, original_var, disjunction, disjunction: a transformed Disjunction containing original_var """ for disjunct in disjunction.disjuncts: - transBlock = disjunct._transformation_block + transBlock = disjunct.transformation_block if transBlock is not None: break if transBlock is None: @@ -963,7 +967,7 @@ def get_disaggregation_constraint(self, original_var, disjunction, try: cons = ( - transBlock() + transBlock .parent_block() ._disaggregationConstraintMap[original_var][disjunction] ) From da7ee79045bfec48f4ba4b85b46827cb5b0c9f14 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 3 Nov 2023 12:58:00 -0600 Subject: [PATCH 0135/3044] Modifying APIs for getting transformed from original to account for the fact that constraints might get transformed multiple times. --- pyomo/gdp/plugins/hull.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 35656b2ff0a..b6e8065ba67 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -1011,10 +1011,30 @@ def get_var_bounds_constraint(self, v): logger.error(msg) raise try: - return transBlock._bigMConstraintMap[v] + cons = transBlock._bigMConstraintMap[v] except: logger.error(msg) raise + transformed_cons = {key: con for key, con in cons.items()} + def is_active(cons): + return all(c.active for c in cons.values()) + while not is_active(transformed_cons): + if 'lb' in transformed_cons: + transformed_cons['lb'] = self.get_transformed_constraints( + transformed_cons['lb'])[0] + if 'ub' in transformed_cons: + transformed_cons['ub'] = self.get_transformed_constraints( + transformed_cons['ub'])[0] + return transformed_cons + + def get_transformed_constraints(self, cons): + cons = super().get_transformed_constraints(cons) + while not cons[0].active: + transformed_cons = [] + for con in cons: + transformed_cons += super().get_transformed_constraints(con) + cons = transformed_cons + return cons @TransformationFactory.register( From 7fc03e1ce63c7888aa547f86f8c678e722869693 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 3 Nov 2023 12:58:16 -0600 Subject: [PATCH 0136/3044] Rewriting simple nested test --- pyomo/gdp/tests/test_hull.py | 276 ++++++++++++++++++++--------------- 1 file changed, 157 insertions(+), 119 deletions(-) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index b8aa332174b..3ef57c73274 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -1551,154 +1551,184 @@ def check_transformed_constraint(self, cons, dis, lb, ind_var): def test_transformed_model_nestedDisjuncts(self): # This test tests *everything* for a simple nested disjunction case. m = models.makeNestedDisjunctions_NestedDisjuncts() - + m.LocalVars = Suffix(direction=Suffix.LOCAL) + m.LocalVars[m.d1] = [ + m.d1.binary_indicator_var, + m.d1.d3.binary_indicator_var, + m.d1.d4.binary_indicator_var + ] + hull = TransformationFactory('gdp.hull') hull.apply_to(m) transBlock = m._pyomo_gdp_hull_reformulation self.assertTrue(transBlock.active) - # outer xor should be on this block + # check outer xor xor = transBlock.disj_xor self.assertIsInstance(xor, Constraint) - self.assertTrue(xor.active) - self.assertEqual(xor.lower, 1) - self.assertEqual(xor.upper, 1) - repn = generate_standard_repn(xor.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef(self, repn, m.d1.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, m.d2.binary_indicator_var, 1) + ct.check_obj_in_active_tree(self, xor) + assertExpressionsEqual( + self, + xor.expr, + m.d1.binary_indicator_var + m.d2.binary_indicator_var == 1 + ) self.assertIs(xor, m.disj.algebraic_constraint) self.assertIs(m.disj, hull.get_src_disjunction(xor)) - # inner xor should be on this block + # check inner xor xor = m.d1.disj2.algebraic_constraint - self.assertIs(xor.parent_block(), transBlock) - self.assertIsInstance(xor, Constraint) - self.assertTrue(xor.active) - self.assertEqual(xor.lower, 0) - self.assertEqual(xor.upper, 0) - repn = generate_standard_repn(xor.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef(self, repn, m.d1.d3.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, m.d1.d4.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, m.d1.binary_indicator_var, -1) self.assertIs(m.d1.disj2, hull.get_src_disjunction(xor)) - - # so should both disaggregation constraints - dis = transBlock.disaggregationConstraints - self.assertIsInstance(dis, Constraint) - self.assertTrue(dis.active) - self.assertEqual(len(dis), 2) - self.check_outer_disaggregation_constraint(dis[0], m.x, m.d1, m.d2) - self.assertIs(hull.get_disaggregation_constraint(m.x, m.disj), dis[0]) - self.check_outer_disaggregation_constraint( - dis[1], m.x, m.d1.d3, m.d1.d4, rhs=hull.get_disaggregated_var(m.x, m.d1) - ) - self.assertIs(hull.get_disaggregation_constraint(m.x, m.d1.disj2), dis[1]) - - # we should have four disjunct transformation blocks - disjBlocks = transBlock.relaxedDisjuncts - self.assertTrue(disjBlocks.active) - self.assertEqual(len(disjBlocks), 4) - - ## d1's transformation block - - disj1 = disjBlocks[0] - self.assertTrue(disj1.active) - self.assertIs(disj1, m.d1.transformation_block) - self.assertIs(m.d1, hull.get_src_disjunct(disj1)) - # check the disaggregated x is here - self.assertIsInstance(disj1.disaggregatedVars.x, Var) - self.assertEqual(disj1.disaggregatedVars.x.lb, 0) - self.assertEqual(disj1.disaggregatedVars.x.ub, 2) - self.assertIs(disj1.disaggregatedVars.x, hull.get_disaggregated_var(m.x, m.d1)) - self.assertIs(m.x, hull.get_src_var(disj1.disaggregatedVars.x)) - # check the bounds constraints - self.check_bounds_constraint_ub( - disj1.x_bounds, 2, disj1.disaggregatedVars.x, m.d1.indicator_var - ) - # transformed constraint x >= 1 - cons = hull.get_transformed_constraints(m.d1.c) - self.check_transformed_constraint( - cons, disj1.disaggregatedVars.x, 1, m.d1.indicator_var + xor = hull.get_transformed_constraints(xor) + self.assertEqual(len(xor), 1) + xor = xor[0] + ct.check_obj_in_active_tree(self, xor) + xor_expr = self.simplify_cons(xor) + assertExpressionsEqual( + self, + xor_expr, + m.d1.d3.binary_indicator_var + + m.d1.d4.binary_indicator_var - + m.d1.binary_indicator_var == 0.0 + ) + + # check disaggregation constraints + x_d3 = hull.get_disaggregated_var(m.x, m.d1.d3) + x_d4 = hull.get_disaggregated_var(m.x, m.d1.d4) + x_d1 = hull.get_disaggregated_var(m.x, m.d1) + x_d2 = hull.get_disaggregated_var(m.x, m.d2) + for x in [x_d1, x_d2, x_d3, x_d4]: + self.assertEqual(x.lb, 0) + self.assertEqual(x.ub, 2) + # Inner disjunction + cons = hull.get_disaggregation_constraint(m.x, m.d1.disj2) + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual( + self, + cons_expr, + x_d1 - x_d3 - x_d4 == 0.0 + ) + # Outer disjunction + cons = hull.get_disaggregation_constraint(m.x, m.disj) + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual( + self, + cons_expr, + m.x - x_d1 - x_d2 == 0.0 ) - ## d2's transformation block + ## Bound constraints - disj2 = disjBlocks[1] - self.assertTrue(disj2.active) - self.assertIs(disj2, m.d2.transformation_block) - self.assertIs(m.d2, hull.get_src_disjunct(disj2)) - # disaggregated var - x2 = disj2.disaggregatedVars.x - self.assertIsInstance(x2, Var) - self.assertEqual(x2.lb, 0) - self.assertEqual(x2.ub, 2) - self.assertIs(hull.get_disaggregated_var(m.x, m.d2), x2) - self.assertIs(hull.get_src_var(x2), m.x) - # bounds constraint - x_bounds = disj2.x_bounds - self.check_bounds_constraint_ub(x_bounds, 2, x2, m.d2.binary_indicator_var) - # transformed constraint x >= 1.1 - cons = hull.get_transformed_constraints(m.d2.c) - self.check_transformed_constraint(cons, x2, 1.1, m.d2.binary_indicator_var) - - ## d1.d3's transformation block - - disj3 = disjBlocks[2] - self.assertTrue(disj3.active) - self.assertIs(disj3, m.d1.d3.transformation_block) - self.assertIs(m.d1.d3, hull.get_src_disjunct(disj3)) - # disaggregated var - x3 = disj3.disaggregatedVars.x - self.assertIsInstance(x3, Var) - self.assertEqual(x3.lb, 0) - self.assertEqual(x3.ub, 2) - self.assertIs(hull.get_disaggregated_var(m.x, m.d1.d3), x3) - self.assertIs(hull.get_src_var(x3), m.x) - # bounds constraints - self.check_bounds_constraint_ub( - disj3.x_bounds, 2, x3, m.d1.d3.binary_indicator_var - ) - # transformed x >= 1.2 + ## Transformed constraints cons = hull.get_transformed_constraints(m.d1.d3.c) - self.check_transformed_constraint(cons, x3, 1.2, m.d1.d3.binary_indicator_var) - - ## d1.d4's transformation block - - disj4 = disjBlocks[3] - self.assertTrue(disj4.active) - self.assertIs(disj4, m.d1.d4.transformation_block) - self.assertIs(m.d1.d4, hull.get_src_disjunct(disj4)) - # disaggregated var - x4 = disj4.disaggregatedVars.x - self.assertIsInstance(x4, Var) - self.assertEqual(x4.lb, 0) - self.assertEqual(x4.ub, 2) - self.assertIs(hull.get_disaggregated_var(m.x, m.d1.d4), x4) - self.assertIs(hull.get_src_var(x4), m.x) - # bounds constraints - self.check_bounds_constraint_ub( - disj4.x_bounds, 2, x4, m.d1.d4.binary_indicator_var - ) - # transformed x >= 1.3 + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual( + self, + cons_expr, + 1.2*m.d1.d3.binary_indicator_var - x_d3 <= 0.0 + ) + cons = hull.get_transformed_constraints(m.d1.d4.c) - self.check_transformed_constraint(cons, x4, 1.3, m.d1.d4.binary_indicator_var) + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual( + self, + cons_expr, + 1.3*m.d1.d4.binary_indicator_var - x_d4 <= 0.0 + ) + + cons = hull.get_transformed_constraints(m.d1.c) + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual( + self, + cons_expr, + 1.0*m.d1.binary_indicator_var - x_d1 <= 0.0 + ) + + cons = hull.get_transformed_constraints(m.d2.c) + self.assertEqual(len(cons), 1) + cons = cons[0] + ct.check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_leq_cons(cons) + assertExpressionsEqual( + self, + cons_expr, + 1.1*m.d2.binary_indicator_var - x_d2 <= 0.0 + ) + + ## Bounds constraints + cons = hull.get_var_bounds_constraint(x_d1) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + ct.check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, + cons_expr, + x_d1 - 2*m.d1.binary_indicator_var <= 0.0 + ) + cons = hull.get_var_bounds_constraint(x_d2) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + ct.check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, + cons_expr, + x_d2 - 2*m.d2.binary_indicator_var <= 0.0 + ) + cons = hull.get_var_bounds_constraint(x_d3) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + ct.check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, + cons_expr, + x_d3 - 2*m.d1.d3.binary_indicator_var <= 0.0 + ) + cons = hull.get_var_bounds_constraint(x_d4) + # the lb is trivial in this case, so we just have 1 + self.assertEqual(len(cons), 1) + ct.check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, + cons_expr, + x_d4 - 2*m.d1.d4.binary_indicator_var <= 0.0 + ) @unittest.skipIf(not linear_solvers, "No linear solver available") def test_solve_nested_model(self): # This is really a test that our variable references have all been moved # up correctly. m = models.makeNestedDisjunctions_NestedDisjuncts() - + m.LocalVars = Suffix(direction=Suffix.LOCAL) + m.LocalVars[m.d1] = [ + m.d1.binary_indicator_var, + m.d1.d3.binary_indicator_var, + m.d1.d4.binary_indicator_var + ] hull = TransformationFactory('gdp.hull') m_hull = hull.create_using(m) SolverFactory(linear_solvers[0]).solve(m_hull) + print("MODEL") + for cons in m_hull.component_data_objects(Constraint, active=True, + descend_into=Block): + print(cons.expr) + # check solution self.assertEqual(value(m_hull.d1.binary_indicator_var), 0) self.assertEqual(value(m_hull.d2.binary_indicator_var), 1) @@ -1887,6 +1917,14 @@ def simplify_cons(self, cons): self.assertIsNone(repn.nonlinear) return repn.to_expression(visitor) == lb + def simplify_leq_cons(self, cons): + visitor = LinearRepnVisitor({}, {}, {}) + self.assertIsNone(cons.lower) + ub = cons.upper + repn = visitor.walk_expression(cons.body) + self.assertIsNone(repn.nonlinear) + return repn.to_expression(visitor) <= ub + def test_nested_with_var_that_skips_a_level(self): m = ConcreteModel() From 8d1e68e533df01dda7ac4c4f9911db2ab388b7cf Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 8 Nov 2023 07:41:44 -0700 Subject: [PATCH 0137/3044] working on simplification contrib package --- pyomo/contrib/simplification/__init__.py | 0 pyomo/contrib/simplification/build.py | 33 +++++++++++++++++++ .../simplification/ginac_interface.cpp | 0 pyomo/contrib/simplification/simplify.py | 12 +++++++ 4 files changed, 45 insertions(+) create mode 100644 pyomo/contrib/simplification/__init__.py create mode 100644 pyomo/contrib/simplification/build.py create mode 100644 pyomo/contrib/simplification/ginac_interface.cpp create mode 100644 pyomo/contrib/simplification/simplify.py diff --git a/pyomo/contrib/simplification/__init__.py b/pyomo/contrib/simplification/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py new file mode 100644 index 00000000000..0b0b9828cd3 --- /dev/null +++ b/pyomo/contrib/simplification/build.py @@ -0,0 +1,33 @@ +from pybind11.setup_helpers import Pybind11Extension, build_ext +from pyomo.common.fileutils import this_file_dir +import os +from distutils.dist import Distribution +import sys + + +def build_ginac_interface(args=[]): + dname = this_file_dir() + _sources = [ + 'ginac_interface.cpp', + ] + sources = list() + for fname in _sources: + sources.append(os.path.join(dname, fname)) + extra_args = ['-std=c++11'] + ext = Pybind11Extension('ginac_interface', sources, extra_compile_args=extra_args) + + package_config = { + 'name': 'ginac_interface', + 'packages': [], + 'ext_modules': [ext], + 'cmdclass': {"build_ext": build_ext}, + } + + dist = Distribution(package_config) + dist.script_args = ['build_ext'] + args + dist.parse_command_line() + dist.run_command('build_ext') + + +if __name__ == '__main__': + build_ginac_interface(sys.argv[1:]) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py new file mode 100644 index 00000000000..70d5dfcd9ac --- /dev/null +++ b/pyomo/contrib/simplification/simplify.py @@ -0,0 +1,12 @@ +from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression +from pyomo.core.expr.numeric_expr import NumericExpression +from pyomo.core.expr.numvalue import is_fixed, value + + +def simplify_with_sympy(expr: NumericExpression): + om, se = sympyify_expression(expr) + se = se.simplify() + new_expr = sympy2pyomo_expression(se, om) + if is_fixed(new_expr): + new_expr = value(new_expr) + return new_expr \ No newline at end of file From 8015d7ece05ee4608c8ddd6924218f40fd635f89 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 8 Nov 2023 11:39:43 -0700 Subject: [PATCH 0138/3044] working on simplification contrib package --- pyomo/contrib/simplification/build.py | 65 ++++++- .../simplification/ginac_interface.cpp | 149 ++++++++++++++++ .../simplification/ginac_interface.hpp | 165 ++++++++++++++++++ 3 files changed, 376 insertions(+), 3 deletions(-) create mode 100644 pyomo/contrib/simplification/ginac_interface.hpp diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 0b0b9828cd3..6f16607e22b 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -1,8 +1,12 @@ from pybind11.setup_helpers import Pybind11Extension, build_ext -from pyomo.common.fileutils import this_file_dir +from pyomo.common.fileutils import this_file_dir, find_library import os from distutils.dist import Distribution import sys +import shutil +import glob +import tempfile +from pyomo.common.envvar import PYOMO_CONFIG_DIR def build_ginac_interface(args=[]): @@ -13,14 +17,69 @@ def build_ginac_interface(args=[]): sources = list() for fname in _sources: sources.append(os.path.join(dname, fname)) + + ginac_lib = find_library('ginac') + if ginac_lib is None: + raise RuntimeError('could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable') + ginac_lib_dir = os.path.dirname(ginac_lib) + ginac_build_dir = os.path.dirname(ginac_lib_dir) + ginac_include_dir = os.path.join(ginac_build_dir, 'include') + if not os.path.exists(os.path.join(ginac_include_dir, 'ginac', 'ginac.h')): + raise RuntimeError('could not find GiNaC include directory') + + cln_lib = find_library('cln') + if cln_lib is None: + raise RuntimeError('could not find CLN library; please make sure it is in the LD_LIBRARY_PATH environment variable') + cln_lib_dir = os.path.dirname(cln_lib) + cln_build_dir = os.path.dirname(cln_lib_dir) + cln_include_dir = os.path.join(cln_build_dir, 'include') + if not os.path.exists(os.path.join(cln_include_dir, 'cln', 'cln.h')): + raise RuntimeError('could not find CLN include directory') + extra_args = ['-std=c++11'] - ext = Pybind11Extension('ginac_interface', sources, extra_compile_args=extra_args) + ext = Pybind11Extension( + 'ginac_interface', + sources=sources, + language='c++', + include_dirs=[cln_include_dir, ginac_include_dir], + library_dirs=[cln_lib_dir, ginac_lib_dir], + libraries=['cln', 'ginac'], + extra_compile_args=extra_args, + ) + + class ginac_build_ext(build_ext): + def run(self): + basedir = os.path.abspath(os.path.curdir) + if self.inplace: + tmpdir = this_file_dir() + else: + tmpdir = os.path.abspath(tempfile.mkdtemp()) + print("Building in '%s'" % tmpdir) + os.chdir(tmpdir) + try: + super(ginac_build_ext, self).run() + if not self.inplace: + library = glob.glob("build/*/ginac_interface.*")[0] + target = os.path.join( + PYOMO_CONFIG_DIR, + 'lib', + 'python%s.%s' % sys.version_info[:2], + 'site-packages', + '.', + ) + if not os.path.exists(target): + os.makedirs(target) + shutil.copy(library, target) + finally: + os.chdir(basedir) + if not self.inplace: + shutil.rmtree(tmpdir, onerror=handleReadonly) package_config = { 'name': 'ginac_interface', 'packages': [], 'ext_modules': [ext], - 'cmdclass': {"build_ext": build_ext}, + 'cmdclass': {"build_ext": ginac_build_ext}, } dist = Distribution(package_config) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index e69de29bb2d..ccbc98d3586 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -0,0 +1,149 @@ +#include "ginac_interface.hpp" + +ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &leaf_map, PyomoExprTypes &expr_types) { + ex res; + ExprType tmp_type = + expr_types.expr_type_map[py::type::of(expr)].cast(); + + switch (tmp_type) { + case py_float: { + res = numeric(expr.cast()); + break; + } + case var: { + long expr_id = expr_types.id(expr).cast(); + if (leaf_map.count(expr_id) == 0) { + leaf_map[expr_id] = symbol("x" + std::to_string(expr_id)); + } + res = leaf_map[expr_id]; + break; + } + case param: { + long expr_id = expr_types.id(expr).cast(); + if (leaf_map.count(expr_id) == 0) { + leaf_map[expr_id] = symbol("p" + std::to_string(expr_id)); + } + res = leaf_map[expr_id]; + break; + } + case product: { + py::list pyomo_args = expr.attr("args"); + res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types) * ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types); + break; + } + case sum: { + py::list pyomo_args = expr.attr("args"); + for (py::handle arg : pyomo_args) { + res += ginac_expr_from_pyomo_node(arg, leaf_map, expr_types); + } + break; + } + case negation: { + py::list pyomo_args = expr.attr("args"); + res = - ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types); + break; + } + case external_func: { + long expr_id = expr_types.id(expr).cast(); + if (leaf_map.count(expr_id) == 0) { + leaf_map[expr_id] = symbol("f" + std::to_string(expr_id)); + } + res = leaf_map[expr_id]; + break; + } + case ExprType::power: { + py::list pyomo_args = expr.attr("args"); + res = pow(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types), ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types)); + break; + } + case division: { + py::list pyomo_args = expr.attr("args"); + res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types) / ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types); + break; + } + case unary_func: { + std::string function_name = expr.attr("getname")().cast(); + py::list pyomo_args = expr.attr("args"); + if (function_name == "exp") + res = exp(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "log") + res = log(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "sin") + res = sin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "cos") + res = cos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "tan") + res = tan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "asin") + res = asin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "acos") + res = acos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "atan") + res = atan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "sqrt") + res = sqrt(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else + throw py::value_error("Unrecognized expression type: " + function_name); + break; + } + case linear: { + py::list pyomo_args = expr.attr("args"); + for (py::handle arg : pyomo_args) { + res += ginac_expr_from_pyomo_node(arg, leaf_map, expr_types); + } + break; + } + case named_expr: { + res = ginac_expr_from_pyomo_node(expr.attr("expr"), leaf_map, expr_types); + break; + } + case numeric_constant: { + res = numeric(expr.attr("value").cast()); + break; + } + case pyomo_unit: { + res = numeric(1.0); + break; + } + case unary_abs: { + py::list pyomo_args = expr.attr("args"); + res = abs(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + break; + } + default: { + throw py::value_error("Unrecognized expression type: " + + expr_types.builtins.attr("str")(py::type::of(expr)) + .cast()); + break; + } + } + return res; +} + +ex ginac_expr_from_pyomo_expr(py::handle expr, PyomoExprTypes &expr_types) { + std::unordered_map leaf_map; + ex res = ginac_expr_from_pyomo_node(expr, leaf_map, expr_types); + return res; +} + + +PYBIND11_MODULE(ginac_interface, m) { + m.def("ginac_expr_from_pyomo_expr", &ginac_expr_from_pyomo_expr); + py::class_(m, "PyomoExprTypes").def(py::init<>()); + py::class_(m, "ex"); + py::enum_(m, "ExprType") + .value("py_float", ExprType::py_float) + .value("var", ExprType::var) + .value("param", ExprType::param) + .value("product", ExprType::product) + .value("sum", ExprType::sum) + .value("negation", ExprType::negation) + .value("external_func", ExprType::external_func) + .value("power", ExprType::power) + .value("division", ExprType::division) + .value("unary_func", ExprType::unary_func) + .value("linear", ExprType::linear) + .value("named_expr", ExprType::named_expr) + .value("numeric_constant", ExprType::numeric_constant) + .export_values(); +} diff --git a/pyomo/contrib/simplification/ginac_interface.hpp b/pyomo/contrib/simplification/ginac_interface.hpp new file mode 100644 index 00000000000..de77e66d0c7 --- /dev/null +++ b/pyomo/contrib/simplification/ginac_interface.hpp @@ -0,0 +1,165 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define PYBIND11_DETAILED_ERROR_MESSAGES + +namespace py = pybind11; +using namespace pybind11::literals; +using namespace GiNaC; + +enum ExprType { + py_float = 0, + var = 1, + param = 2, + product = 3, + sum = 4, + negation = 5, + external_func = 6, + power = 7, + division = 8, + unary_func = 9, + linear = 10, + named_expr = 11, + numeric_constant = 12, + pyomo_unit = 13, + unary_abs = 14 +}; + +class PyomoExprTypes { +public: + PyomoExprTypes() { + expr_type_map[int_] = py_float; + expr_type_map[float_] = py_float; + expr_type_map[np_int16] = py_float; + expr_type_map[np_int32] = py_float; + expr_type_map[np_int64] = py_float; + expr_type_map[np_longlong] = py_float; + expr_type_map[np_uint16] = py_float; + expr_type_map[np_uint32] = py_float; + expr_type_map[np_uint64] = py_float; + expr_type_map[np_ulonglong] = py_float; + expr_type_map[np_float16] = py_float; + expr_type_map[np_float32] = py_float; + expr_type_map[np_float64] = py_float; + expr_type_map[ScalarVar] = var; + expr_type_map[_GeneralVarData] = var; + expr_type_map[AutoLinkedBinaryVar] = var; + expr_type_map[ScalarParam] = param; + expr_type_map[_ParamData] = param; + expr_type_map[MonomialTermExpression] = product; + expr_type_map[ProductExpression] = product; + expr_type_map[NPV_ProductExpression] = product; + expr_type_map[SumExpression] = sum; + expr_type_map[NPV_SumExpression] = sum; + expr_type_map[NegationExpression] = negation; + expr_type_map[NPV_NegationExpression] = negation; + expr_type_map[ExternalFunctionExpression] = external_func; + expr_type_map[NPV_ExternalFunctionExpression] = external_func; + expr_type_map[PowExpression] = ExprType::power; + expr_type_map[NPV_PowExpression] = ExprType::power; + expr_type_map[DivisionExpression] = division; + expr_type_map[NPV_DivisionExpression] = division; + expr_type_map[UnaryFunctionExpression] = unary_func; + expr_type_map[NPV_UnaryFunctionExpression] = unary_func; + expr_type_map[LinearExpression] = linear; + expr_type_map[_GeneralExpressionData] = named_expr; + expr_type_map[ScalarExpression] = named_expr; + expr_type_map[Integral] = named_expr; + expr_type_map[ScalarIntegral] = named_expr; + expr_type_map[NumericConstant] = numeric_constant; + expr_type_map[_PyomoUnit] = pyomo_unit; + expr_type_map[AbsExpression] = unary_abs; + expr_type_map[NPV_AbsExpression] = unary_abs; + } + ~PyomoExprTypes() = default; + py::int_ ione = 1; + py::float_ fone = 1.0; + py::type int_ = py::type::of(ione); + py::type float_ = py::type::of(fone); + py::object np = py::module_::import("numpy"); + py::type np_int16 = np.attr("int16"); + py::type np_int32 = np.attr("int32"); + py::type np_int64 = np.attr("int64"); + py::type np_longlong = np.attr("longlong"); + py::type np_uint16 = np.attr("uint16"); + py::type np_uint32 = np.attr("uint32"); + py::type np_uint64 = np.attr("uint64"); + py::type np_ulonglong = np.attr("ulonglong"); + py::type np_float16 = np.attr("float16"); + py::type np_float32 = np.attr("float32"); + py::type np_float64 = np.attr("float64"); + py::object ScalarParam = + py::module_::import("pyomo.core.base.param").attr("ScalarParam"); + py::object _ParamData = + py::module_::import("pyomo.core.base.param").attr("_ParamData"); + py::object ScalarVar = + py::module_::import("pyomo.core.base.var").attr("ScalarVar"); + py::object _GeneralVarData = + py::module_::import("pyomo.core.base.var").attr("_GeneralVarData"); + py::object AutoLinkedBinaryVar = + py::module_::import("pyomo.gdp.disjunct").attr("AutoLinkedBinaryVar"); + py::object numeric_expr = py::module_::import("pyomo.core.expr.numeric_expr"); + py::object NegationExpression = numeric_expr.attr("NegationExpression"); + py::object NPV_NegationExpression = + numeric_expr.attr("NPV_NegationExpression"); + py::object ExternalFunctionExpression = + numeric_expr.attr("ExternalFunctionExpression"); + py::object NPV_ExternalFunctionExpression = + numeric_expr.attr("NPV_ExternalFunctionExpression"); + py::object PowExpression = numeric_expr.attr("PowExpression"); + py::object NPV_PowExpression = numeric_expr.attr("NPV_PowExpression"); + py::object ProductExpression = numeric_expr.attr("ProductExpression"); + py::object NPV_ProductExpression = numeric_expr.attr("NPV_ProductExpression"); + py::object MonomialTermExpression = + numeric_expr.attr("MonomialTermExpression"); + py::object DivisionExpression = numeric_expr.attr("DivisionExpression"); + py::object NPV_DivisionExpression = + numeric_expr.attr("NPV_DivisionExpression"); + py::object SumExpression = numeric_expr.attr("SumExpression"); + py::object NPV_SumExpression = numeric_expr.attr("NPV_SumExpression"); + py::object UnaryFunctionExpression = + numeric_expr.attr("UnaryFunctionExpression"); + py::object AbsExpression = numeric_expr.attr("AbsExpression"); + py::object NPV_AbsExpression = numeric_expr.attr("NPV_AbsExpression"); + py::object NPV_UnaryFunctionExpression = + numeric_expr.attr("NPV_UnaryFunctionExpression"); + py::object LinearExpression = numeric_expr.attr("LinearExpression"); + py::object NumericConstant = + py::module_::import("pyomo.core.expr.numvalue").attr("NumericConstant"); + py::object expr_module = py::module_::import("pyomo.core.base.expression"); + py::object _GeneralExpressionData = + expr_module.attr("_GeneralExpressionData"); + py::object ScalarExpression = expr_module.attr("ScalarExpression"); + py::object ScalarIntegral = + py::module_::import("pyomo.dae.integral").attr("ScalarIntegral"); + py::object Integral = + py::module_::import("pyomo.dae.integral").attr("Integral"); + py::object _PyomoUnit = + py::module_::import("pyomo.core.base.units_container").attr("_PyomoUnit"); + py::object builtins = py::module_::import("builtins"); + py::object id = builtins.attr("id"); + py::object len = builtins.attr("len"); + py::dict expr_type_map; +}; + +ex ginac_expr_from_pyomo_expr(py::handle expr, PyomoExprTypes &expr_types); From d43765c5bf9609c5f74e1a58b2ab1b32d595f101 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 8 Nov 2023 15:11:29 -0700 Subject: [PATCH 0139/3044] SAVE STATE --- pyomo/solver/results.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 8e4b6cf21a7..d7505a7ed95 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -289,11 +289,13 @@ def parse_sol_file(file, results): while i < number_of_cons: line = file.readline() constraints.append(float(line)) + i += 1 # Parse through the variable lines and capture the variables i = 0 while i < number_of_vars: line = file.readline() variables.append(float(line)) + i += 1 # Parse the exit code line and capture it exit_code = [0, 0] line = file.readline() @@ -315,30 +317,29 @@ def parse_sol_file(file, results): exit_code_message = "Optimal solution indicated, but ERROR LIKELY!" results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied results.solution_status = SolutionStatus.optimal - if results.extra_info.solver_message: - results.extra_info.solver_message += '; ' + exit_code_message - else: - results.extra_info.solver_message = exit_code_message elif (exit_code[1] >= 200) and (exit_code[1] <= 299): + exit_code_message = "INFEASIBLE SOLUTION: constraints cannot be satisfied!" results.termination_condition = TerminationCondition.locallyInfeasible results.solution_status = SolutionStatus.infeasible elif (exit_code[1] >= 300) and (exit_code[1] <= 399): + exit_code_message = "UNBOUNDED PROBLEM: the objective can be improved without limit!" results.termination_condition = TerminationCondition.unbounded results.solution_status = SolutionStatus.infeasible elif (exit_code[1] >= 400) and (exit_code[1] <= 499): + exit_code_message = ("EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " + "was stopped by a limit that you set!") results.solver.termination_condition = TerminationCondition.iterationLimit elif (exit_code[1] >= 500) and (exit_code[1] <= 599): exit_code_message = ( "FAILURE: the solver stopped by an error condition " "in the solver routines!" ) - if results.extra_info.solver_message: - results.extra_info.solver_message += '; ' + exit_code_message - else: - results.extra_info.solver_message = exit_code_message results.solver.termination_condition = TerminationCondition.error - return results - + + if results.extra_info.solver_message: + results.extra_info.solver_message += '; ' + exit_code_message + else: + results.extra_info.solver_message = exit_code_message return results def parse_yaml(): From 1c55221b83b0679db7e51099343bd36b54682977 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Mon, 17 Jul 2023 11:36:32 -0600 Subject: [PATCH 0140/3044] add initial work on nested inner repn pw to gdp transformation. identify variables mode does not work, gives infeasible models --- .../tests/test_nested_inner_repn_gdp.py | 36 ++++ .../piecewise/transform/nested_inner_repn.py | 171 ++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py create mode 100644 pyomo/contrib/piecewise/transform/nested_inner_repn.py diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py new file mode 100644 index 00000000000..48357c828df --- /dev/null +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -0,0 +1,36 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.piecewise.tests import models +import pyomo.contrib.piecewise.tests.common_tests as ct +from pyomo.core.base import TransformationFactory +from pyomo.core.expr.compare import ( + assertExpressionsEqual, + assertExpressionsStructurallyEqual, +) +from pyomo.gdp import Disjunct, Disjunction +from pyomo.environ import Constraint, SolverFactory, Var + +from pyomo.contrib.piecewise.transform.nested_inner_repn import NestedInnerRepresentationGDPTransformation + +class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): + + def test_solve_log_model(self): + m = models.make_log_x_model() + TransformationFactory( + 'contrib.piecewise.nested_inner_repn_gdp' + ).apply_to(m) + TransformationFactory( + 'gdp.bigm' + ).apply_to(m) + SolverFactory('gurobi').solve(m) + ct.check_log_x_model_soln(self, m) \ No newline at end of file diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py new file mode 100644 index 00000000000..b25ca3981a8 --- /dev/null +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -0,0 +1,171 @@ +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( + PiecewiseLinearToGDP, +) +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core.base import TransformationFactory +from pyomo.gdp import Disjunct, Disjunction +from pyomo.common.errors import DeveloperError +from pyomo.core.expr.visitor import SimpleExpressionVisitor +from pyomo.core.expr.current import identify_components + +@TransformationFactory.register( + 'contrib.piecewise.nested_inner_repn_gdp', + doc="TODO document", +) +class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): + """ + Represent a piecewise linear function "logarithmically" by using a nested + GDP to determine which polytope a point is in, then representing it as a + convex combination of extreme points, with multipliers "local" to that + particular polytope, i.e., not shared with neighbors. This method of + logarithmically formulating the piecewise linear function imposes no + restrictions on the family of polytopes. We rely on the identification of + variables to make this logarithmic in the number of binaries. This method + is due to Vielma et al., 2010. + """ + CONFIG = PiecewiseLinearToGDP.CONFIG() + _transformation_name = 'pw_linear_nested_inner_repn' + + # Implement to use PiecewiseLinearToGDP. This function returns the Var + # that replaces the transformed piecewise linear expr + def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): + self.DEBUG = True + identify_vars = True + # Get a new Block() in transformation_block.transformed_functions, which + # is a Block(Any) + transBlock = transformation_block.transformed_functions[ + len(transformation_block.transformed_functions) + ] + + # these copy-pasted lines (from inner_representation_gdp) seem useful + # adding some of this stuff to self so I don't have to pass it around + self.pw_linear_func = pw_linear_func + # map number -> list of Disjuncts which contain Disjunctions at that level + self.disjunct_levels = {} + self.dimension = pw_expr.nargs() + substitute_var = transBlock.substitute_var = Var() + pw_linear_func.map_transformation_var(pw_expr, substitute_var) + self.substitute_var_lb = float('inf') + self.substitute_var_ub = -float('inf') + + choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) + + if self.DEBUG: + print(f"dimension is {self.dimension}") + + # Add the disjunction + transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) + + # Widen bounds as determined when setting up the disjunction + if self.substitute_var_lb < float('inf'): + transBlock.substitute_var.setlb(self.substitute_var_lb) + if self.substitute_var_ub > -float('inf'): + transBlock.substitute_var.setub(self.substitute_var_ub) + + if self.DEBUG: + print(f"lb is {self.substitute_var_lb}, ub is {self.substitute_var_ub}") + + if identify_vars: + if self.DEBUG: + print("Now identifying variables") + for i in self.disjunct_levels.keys(): + print(f"level {i}: {len(self.disjunct_levels[i])} disjuncts") + transBlock.var_identifications_l = Constraint(NonNegativeIntegers, NonNegativeIntegers) + transBlock.var_identifications_r = Constraint(NonNegativeIntegers, NonNegativeIntegers) + for k in self.disjunct_levels.keys(): + disj_0 = self.disjunct_levels[k][0] + for i, disj in enumerate(self.disjunct_levels[k][1:]): + transBlock.var_identifications_l[k, i] = disj.d_l.binary_indicator_var == disj_0.d_l.binary_indicator_var + transBlock.var_identifications_r[k, i] = disj.d_r.binary_indicator_var == disj_0.d_r.binary_indicator_var + return substitute_var + + # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up + # the stack, since the whole point is that we'll only go logarithmically + # many calls deep. + def _get_disjunction(self, choices, parent_block, pw_expr, root_block, level): + size = len(choices) + if self.DEBUG: + print(f"calling _get_disjunction with size={size}") + # Our base cases will be 3 and 2, since it would be silly to construct + # a Disjunction containing only one Disjunct. We can ensure that size + # is never 1 unless it was only passsed a single choice from the start, + # which we can handle before calling. + if size > 3: + half = size // 2 # (integer divide) + # This tree will be slightly heavier on the right side + choices_l = choices[:half] + choices_r = choices[half:] + # Is this valid Pyomo? + @parent_block.Disjunct() + def d_l(b): + b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block, level + 1) + @parent_block.Disjunct() + def d_r(b): + b.inner_disjunction_r = self._get_disjunction(choices_r, b, pw_expr, root_block, level + 1) + if level not in self.disjunct_levels.keys(): + self.disjunct_levels[level] = [] + self.disjunct_levels[level].append(parent_block.d_l) + self.disjunct_levels[level].append(parent_block.d_r) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) + elif size == 3: + # Let's stay heavier on the right side for consistency. So the left + # Disjunct will be the one to contain constraints, rather than a + # Disjunction + @parent_block.Disjunct() + def d_l(b): + simplex, linear_func = choices[0] + self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + @parent_block.Disjunct() + def d_r(b): + b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block, level + 1) + if level not in self.disjunct_levels.keys(): + self.disjunct_levels[level] = [] + self.disjunct_levels[level].append(parent_block.d_r) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) + elif size == 2: + # In this case both sides are regular Disjuncts + @parent_block.Disjunct() + def d_l(b): + simplex, linear_func = choices[0] + self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + @parent_block.Disjunct() + def d_r(b): + simplex, linear_func = choices[1] + self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) + else: + raise DeveloperError("Unreachable: 1 or 0 choices were passed to " + "_get_disjunction in nested_inner_repn.py.") + + def _set_disjunct_block_constraints(self, b, simplex, linear_func, pw_expr, root_block): + # Define the lambdas sparsely like in the version I'm copying, + # only the first few will participate in constraints + b.lambdas = Var(NonNegativeIntegers, dense=False, bounds=(0, 1)) + # Get the extreme points to add up + extreme_pts = [] + for idx in simplex: + extreme_pts.append(self.pw_linear_func._points[idx]) + # Constrain sum(lambda_i) = 1 + b.convex_combo = Constraint( + expr=sum(b.lambdas[i] for i in range(len(extreme_pts))) == 1 + ) + linear_func_expr = linear_func(*pw_expr.args) + # Make the substitute Var equal the PWLE + b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) + # Widen the variable bounds to those of this linear func expression + (lb, ub) = compute_bounds_on_expr(linear_func_expr) + if lb is not None and lb < self.substitute_var_lb: + self.substitute_var_lb = lb + if ub is not None and ub > self.substitute_var_ub: + self.substitute_var_ub = ub + # Constrain x = \sum \lambda_i v_i + @b.Constraint(range(self.dimension)) + def linear_combo(d, i): + return pw_expr.args[i] == sum( + d.lambdas[j] * pt[i] for j, pt in enumerate(extreme_pts) + ) + # Mark the lambdas as local in order to prevent disagreggating multiple + # times in the hull transformation + b.LocalVars = Suffix(direction=Suffix.LOCAL) + b.LocalVars[b] = [v for v in b.lambdas.values()] From 91ed57e8c7c49ebe2d547a8f6b065f5231cf2164 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 1 Aug 2023 12:48:46 -0600 Subject: [PATCH 0141/3044] wip: working on some other pw linear representations --- .../transform/disagreggated_logarithmic.py | 102 ++++++++++++++++++ .../piecewise/transform/nested_inner_repn.py | 7 +- 2 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py new file mode 100644 index 00000000000..fceb02d4d8c --- /dev/null +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -0,0 +1,102 @@ +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( + PiecewiseLinearToGDP, +) +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core.base import TransformationFactory +from pyomo.gdp import Disjunct, Disjunction +from pyomo.common.errors import DeveloperError +from pyomo.core.expr.visitor import SimpleExpressionVisitor +from pyomo.core.expr.current import identify_components +from math import ceil, log2 + +@TransformationFactory.register( + 'contrib.piecewise.disaggregated_logarithmic', + doc="TODO document", +) +class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): + """ + Represent a piecewise linear function "logarithmically" by using a MIP with + log_2(|P|) binary decision variables. This method of logarithmically + formulating the piecewise linear function imposes no restrictions on the + family of polytopes. This method is due to Vielma et al., 2010. + """ + CONFIG = PiecewiseLinearToGDP.CONFIG() + _transformation_name = 'pw_linear_disaggregated_log' + + # Implement to use PiecewiseLinearToGDP. This function returns the Var + # that replaces the transformed piecewise linear expr + def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): + self.DEBUG = False + # Get a new Block() in transformation_block.transformed_functions, which + # is a Block(Any) + transBlock = transformation_block.transformed_functions[ + len(transformation_block.transformed_functions) + ] + + dimension = pw_expr.nargs() + substitute_var = transBlock.substitute_var = Var() + pw_linear_func.map_transformation_var(pw_expr, substitute_var) + self.substitute_var_lb = float('inf') + self.substitute_var_ub = -float('inf') + + simplices = pw_linear_func._simplices + num_simplices = len(simplices) + simplex_indices = range(num_simplices) + # Assumption: the simplices are really simplices and all have the same number of points + simplex_point_indices = range(len(simplices[0])) + + choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) + + log_dimension = ceil(log2(num_simplices)) + binaries = transBlock.binaries = Var(range(log_dimension), domain=Binary) + + # injective function \mathcal{P} -> ceil(log_2(|P|)) used to identify simplices + # (really just polytopes are required) with binary vectors + B = {} + for i, p in enumerate(simplices): + B[id(p)] = self._get_binary_vector(i, log_dimension) + + # The lambdas \lambda_{P,v} + lambdas = transBlock.lambdas = Var(simplex_indices, simplex_point_indices, bounds=(0, 1)) + transBlock.convex_combo = Constraint(sum(lambdas[P, v] for P in simplex_indices for v in simplex_point_indices) == 1) + + # The branching rules, establishing using the binaries that only one simplex's lambdas + # may be nonzero + @transBlock.Constraint(range(log_dimension)) + def simplex_choice_1(b, l): + return ( + sum(lambdas[P, v] for P in self._P_plus(B, l) for v in simplex_point_indices) <= binaries[l] + ) + @transBlock.Constraint(range(log_dimension)) + def simplex_choice_2(b, l): + return ( + sum(lambdas[P, v] for P in self._P_0(B, l) for v in simplex_point_indices) <= 1 - binaries[l] + ) + + #for i, (simplex, pwlf) in enumerate(choices): + # x_i = sum(lambda_P,v v_i) + @transBlock.Constraint(range(dimension)) + def x_constraint(b, i): + return sum([stuff] for ) + + + #linear_func_expr = linear_func(*pw_expr.args) + ## Make the substitute Var equal the PWLE + #b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) + + # Not a gray code, just a regular binary representation + # TODO this is probably not optimal, test the gray codes too + def _get_binary_vector(self, num, length): + if ceil(log2(num)) > length: + raise DeveloperError("Invalid input in _get_binary_vector") + # Use python's string formatting instead of bothering with modular + # arithmetic. May be slow. + return (int(x) for x in format(num, f'0{length}b')) + + # Return {P \in \mathcal{P} | B(P)_l = 0} + def _P_0(B, l, simplices): + return [p for p in simplices if B[id(p)][l] == 0] + # Return {P \in \mathcal{P} | B(P)_l = 1} + def _P_plus(B, l, simplices): + return [p for p in simplices if B[id(p)][l] == 1] \ No newline at end of file diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index b25ca3981a8..fc5761de434 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -30,8 +30,8 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - self.DEBUG = True - identify_vars = True + self.DEBUG = False + identify_vars = False # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ @@ -66,6 +66,9 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc if self.DEBUG: print(f"lb is {self.substitute_var_lb}, ub is {self.substitute_var_ub}") + # NOTE - This functionality does not work. Even when we can choose the indicator + # variables, it seems that infeasibilities will always be generated. We may need + # to just directly transform to mip :( if identify_vars: if self.DEBUG: print("Now identifying variables") From d9be75ec758aa2b5173fe419008707a8267d471f Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 23 Aug 2023 16:34:57 -0400 Subject: [PATCH 0142/3044] properly handle one-simplex case instead of ignoring --- .../piecewise/transform/nested_inner_repn.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index fc5761de434..1e86a1406b4 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -54,8 +54,17 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc if self.DEBUG: print(f"dimension is {self.dimension}") - # Add the disjunction - transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) + # If there was only one choice, don't bother making a disjunction, just + # use the linear function directly (but still use the substitute_var for + # consistency). + if len(choices) == 1: + (_, linear_func) = choices[0] # simplex isn't important in this case + linear_func_expr = linear_func(*pw_expr.args) + transBlock.set_substitute = Constraint(expr=substitute_var == linear_func_expr) + (self.substitute_var_lb, self.substitute_var_ub) = compute_bounds_on_expr(linear_func_expr) + else: + # Add the disjunction + transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) # Widen bounds as determined when setting up the disjunction if self.substitute_var_lb < float('inf'): From 949e043d685a53cd7eb05df63935b8c3045a7a80 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 23 Aug 2023 17:03:43 -0400 Subject: [PATCH 0143/3044] nested inner repn: remove non-working variable identification code --- .../piecewise/transform/nested_inner_repn.py | 55 +++++-------------- 1 file changed, 14 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 1e86a1406b4..aaa0e03c79b 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -2,27 +2,25 @@ from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( PiecewiseLinearToGDP, ) -from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var from pyomo.core.base import TransformationFactory -from pyomo.gdp import Disjunct, Disjunction +from pyomo.gdp import Disjunction from pyomo.common.errors import DeveloperError -from pyomo.core.expr.visitor import SimpleExpressionVisitor -from pyomo.core.expr.current import identify_components @TransformationFactory.register( 'contrib.piecewise.nested_inner_repn_gdp', - doc="TODO document", + doc="TODO document", # TODO ) class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): """ - Represent a piecewise linear function "logarithmically" by using a nested - GDP to determine which polytope a point is in, then representing it as a - convex combination of extreme points, with multipliers "local" to that - particular polytope, i.e., not shared with neighbors. This method of - logarithmically formulating the piecewise linear function imposes no - restrictions on the family of polytopes. We rely on the identification of - variables to make this logarithmic in the number of binaries. This method - is due to Vielma et al., 2010. + Represent a piecewise linear function by using a nested GDP to determine + which polytope a point is in, then representing it as a convex combination + of extreme points, with multipliers "local" to that particular polytope, + i.e., not shared with neighbors. This method of formulating the piecewise + linear function imposes no restrictions on the family of polytopes. Note + that this is NOT a logarithmic formulation - it has linearly many binaries. + This method was, however, inspired by the disagreggated logarithmic + formulation of Vielma et al., 2010. """ CONFIG = PiecewiseLinearToGDP.CONFIG() _transformation_name = 'pw_linear_nested_inner_repn' @@ -31,7 +29,6 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): self.DEBUG = False - identify_vars = False # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ @@ -41,8 +38,6 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # these copy-pasted lines (from inner_representation_gdp) seem useful # adding some of this stuff to self so I don't have to pass it around self.pw_linear_func = pw_linear_func - # map number -> list of Disjuncts which contain Disjunctions at that level - self.disjunct_levels = {} self.dimension = pw_expr.nargs() substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) @@ -64,7 +59,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc (self.substitute_var_lb, self.substitute_var_ub) = compute_bounds_on_expr(linear_func_expr) else: # Add the disjunction - transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) + transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock) # Widen bounds as determined when setting up the disjunction if self.substitute_var_lb < float('inf'): @@ -75,21 +70,6 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc if self.DEBUG: print(f"lb is {self.substitute_var_lb}, ub is {self.substitute_var_ub}") - # NOTE - This functionality does not work. Even when we can choose the indicator - # variables, it seems that infeasibilities will always be generated. We may need - # to just directly transform to mip :( - if identify_vars: - if self.DEBUG: - print("Now identifying variables") - for i in self.disjunct_levels.keys(): - print(f"level {i}: {len(self.disjunct_levels[i])} disjuncts") - transBlock.var_identifications_l = Constraint(NonNegativeIntegers, NonNegativeIntegers) - transBlock.var_identifications_r = Constraint(NonNegativeIntegers, NonNegativeIntegers) - for k in self.disjunct_levels.keys(): - disj_0 = self.disjunct_levels[k][0] - for i, disj in enumerate(self.disjunct_levels[k][1:]): - transBlock.var_identifications_l[k, i] = disj.d_l.binary_indicator_var == disj_0.d_l.binary_indicator_var - transBlock.var_identifications_r[k, i] = disj.d_r.binary_indicator_var == disj_0.d_r.binary_indicator_var return substitute_var # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up @@ -111,14 +91,10 @@ def _get_disjunction(self, choices, parent_block, pw_expr, root_block, level): # Is this valid Pyomo? @parent_block.Disjunct() def d_l(b): - b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block, level + 1) + b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block) @parent_block.Disjunct() def d_r(b): - b.inner_disjunction_r = self._get_disjunction(choices_r, b, pw_expr, root_block, level + 1) - if level not in self.disjunct_levels.keys(): - self.disjunct_levels[level] = [] - self.disjunct_levels[level].append(parent_block.d_l) - self.disjunct_levels[level].append(parent_block.d_r) + b.inner_disjunction_r = self._get_disjunction(choices_r, b, pw_expr, root_block) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 3: # Let's stay heavier on the right side for consistency. So the left @@ -131,9 +107,6 @@ def d_l(b): @parent_block.Disjunct() def d_r(b): b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block, level + 1) - if level not in self.disjunct_levels.keys(): - self.disjunct_levels[level] = [] - self.disjunct_levels[level].append(parent_block.d_r) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 2: # In this case both sides are regular Disjuncts From 0c54a1f270963e0f361b5dae93871b6bdce404e4 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 23 Aug 2023 17:13:15 -0400 Subject: [PATCH 0144/3044] fix errors --- pyomo/contrib/piecewise/transform/nested_inner_repn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index aaa0e03c79b..6c551818c84 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -75,7 +75,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up # the stack, since the whole point is that we'll only go logarithmically # many calls deep. - def _get_disjunction(self, choices, parent_block, pw_expr, root_block, level): + def _get_disjunction(self, choices, parent_block, pw_expr, root_block): size = len(choices) if self.DEBUG: print(f"calling _get_disjunction with size={size}") @@ -106,7 +106,7 @@ def d_l(b): self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) @parent_block.Disjunct() def d_r(b): - b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block, level + 1) + b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 2: # In this case both sides are regular Disjuncts From cde25fc924eec83bbdbb2357feaeac1e3c4349f9 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 12 Oct 2023 00:38:54 -0400 Subject: [PATCH 0145/3044] disaggregated logarithmic reworking --- .../transform/disagreggated_logarithmic.py | 186 +++++++++++++----- 1 file changed, 142 insertions(+), 44 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index fceb02d4d8c..e0b6d75d0e4 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -2,7 +2,7 @@ from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( PiecewiseLinearToGDP, ) -from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet from pyomo.core.base import TransformationFactory from pyomo.gdp import Disjunct, Disjunction from pyomo.common.errors import DeveloperError @@ -10,93 +10,191 @@ from pyomo.core.expr.current import identify_components from math import ceil, log2 + @TransformationFactory.register( - 'contrib.piecewise.disaggregated_logarithmic', - doc="TODO document", -) -class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): - """ + "contrib.piecewise.disaggregated_logarithmic", + doc=""" Represent a piecewise linear function "logarithmically" by using a MIP with log_2(|P|) binary decision variables. This method of logarithmically formulating the piecewise linear function imposes no restrictions on the family of polytopes. This method is due to Vielma et al., 2010. + """, +) +class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): + """ + Represent a piecewise linear function "logarithmically" by using a MIP with + log_2(|P|) binary decision variables. This method of logarithmically + formulating the piecewise linear function imposes no restrictions on the + family of polytopes. This method is due to Vielma et al., 2010. """ + CONFIG = PiecewiseLinearToGDP.CONFIG() - _transformation_name = 'pw_linear_disaggregated_log' - + _transformation_name = "pw_linear_disaggregated_log" + # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): self.DEBUG = False + # Get a new Block() in transformation_block.transformed_functions, which - # is a Block(Any) + # is a Block(Any). This is where we will put our new components. transBlock = transformation_block.transformed_functions[ len(transformation_block.transformed_functions) ] + # Dimensionality of the PWLF dimension = pw_expr.nargs() + print(f"DIMENSIOn={dimension}") + transBlock.dimension_indices = RangeSet(0, dimension - 1) + + # Substitute Var that will hold the value of the PWLE substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) - self.substitute_var_lb = float('inf') - self.substitute_var_ub = -float('inf') + # Bounds for the substitute_var that we will tighten + self.substitute_var_lb = float("inf") + self.substitute_var_ub = -float("inf") + + # Simplices are tuples of indices of points. Give them their own indices, too simplices = pw_linear_func._simplices num_simplices = len(simplices) - simplex_indices = range(num_simplices) - # Assumption: the simplices are really simplices and all have the same number of points - simplex_point_indices = range(len(simplices[0])) + transBlock.simplex_indices = RangeSet(0, num_simplices - 1) + # Assumption: the simplices are really simplices and all have the same number of points, + # which is dimension + 1 + transBlock.simplex_point_indices = RangeSet(0, dimension) + + # Enumeration of simplices, map from simplex number to simplex object + self.idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} + # Inverse of previous enumeration + self.simplex_to_idx = {v: k for k, v in self.idx_to_simplex.items()} + + # List of tuples of simplices with their linear function + simplices_and_lin_funcs = list(zip(simplices, pw_linear_func._linear_functions)) - choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) + print("a") + print(f"Num_simplices: {num_simplices}") log_dimension = ceil(log2(num_simplices)) - binaries = transBlock.binaries = Var(range(log_dimension), domain=Binary) + transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) + binaries = transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) - # injective function \mathcal{P} -> ceil(log_2(|P|)) used to identify simplices - # (really just polytopes are required) with binary vectors + # Injective function \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices + # (really just polytopes are required) with binary vectors. Any injective function + # is valid. B = {} - for i, p in enumerate(simplices): - B[id(p)] = self._get_binary_vector(i, log_dimension) - - # The lambdas \lambda_{P,v} - lambdas = transBlock.lambdas = Var(simplex_indices, simplex_point_indices, bounds=(0, 1)) - transBlock.convex_combo = Constraint(sum(lambdas[P, v] for P in simplex_indices for v in simplex_point_indices) == 1) + for i in transBlock.simplex_indices: + # map index(P) -> corresponding vector in {0, 1}^n + B[i] = self._get_binary_vector(i, log_dimension) + print(f"after construction, B = {B}") + + print("b") + # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it + transBlock.lambdas = Var(transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1)) + print("b1") + + # Sum of all lambdas is one (6b) + transBlock.convex_combo = Constraint( + expr=sum( + transBlock.lambdas[P, v] + for P in transBlock.simplex_indices + for v in transBlock.simplex_point_indices + ) + == 1 + ) + + print("c") # The branching rules, establishing using the binaries that only one simplex's lambdas # may be nonzero - @transBlock.Constraint(range(log_dimension)) + @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) def simplex_choice_1(b, l): + print("entering constraint generator") + print(f"thing={self._P_plus(B, l, simplices)}") + print("returning") return ( - sum(lambdas[P, v] for P in self._P_plus(B, l) for v in simplex_point_indices) <= binaries[l] + sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + for P in self._P_plus(B, l, simplices) + for v in transBlock.simplex_point_indices + ) + <= binaries[l] ) - @transBlock.Constraint(range(log_dimension)) + + print("c1") + + @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) def simplex_choice_2(b, l): return ( - sum(lambdas[P, v] for P in self._P_0(B, l) for v in simplex_point_indices) <= 1 - binaries[l] + sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + for P in self._P_0(B, l, simplices) + for v in transBlock.simplex_point_indices + ) + <= 1 - binaries[l] ) - - #for i, (simplex, pwlf) in enumerate(choices): - # x_i = sum(lambda_P,v v_i) - @transBlock.Constraint(range(dimension)) + + print("d") + + # for i, (simplex, pwlf) in enumerate(choices): + # x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P)) + @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) def x_constraint(b, i): - return sum([stuff] for ) + print(f"simplices are {[P for P in simplices]}") + print(f"points are {pw_linear_func._points}") + print(f"simplex_point_indices is {list(transBlock.simplex_point_indices)}") + print(f"i={i}") + + return pw_expr.args[i] == sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + * pw_linear_func._points[P[v]][i] + for P in simplices + for v in transBlock.simplex_point_indices + ) + + # Make the substitute Var equal the PWLE (6a.2) + for P, linear_func in simplices_and_lin_funcs: + print(f"P, linear_func = {P}, {linear_func}") + for v in transBlock.simplex_point_indices: + print(f" v={v}") + print(f" pt={pw_linear_func._points[P[v]]}") + print( + f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" + ) + transBlock.set_substitute = Constraint( + expr=substitute_var + == sum( + sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + * linear_func(*pw_linear_func._points[P[v]]) + for v in transBlock.simplex_point_indices + ) + for (P, linear_func) in simplices_and_lin_funcs + ) + ) + + print("f") + return substitute_var - #linear_func_expr = linear_func(*pw_expr.args) - ## Make the substitute Var equal the PWLE - #b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) - # Not a gray code, just a regular binary representation # TODO this is probably not optimal, test the gray codes too def _get_binary_vector(self, num, length): - if ceil(log2(num)) > length: + if num != 0 and ceil(log2(num)) > length: raise DeveloperError("Invalid input in _get_binary_vector") - # Use python's string formatting instead of bothering with modular + # Hack: use python's string formatting instead of bothering with modular # arithmetic. May be slow. - return (int(x) for x in format(num, f'0{length}b')) + return tuple(int(x) for x in format(num, f"0{length}b")) # Return {P \in \mathcal{P} | B(P)_l = 0} - def _P_0(B, l, simplices): - return [p for p in simplices if B[id(p)][l] == 0] + def _P_0(self, B, l, simplices): + return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 0] + # Return {P \in \mathcal{P} | B(P)_l = 1} - def _P_plus(B, l, simplices): - return [p for p in simplices if B[id(p)][l] == 1] \ No newline at end of file + def _P_plus(self, B, l, simplices): + print(f"p plus: B={B}, l={l}, simplices={simplices}") + for p in simplices: + print(f"for p={p}, simplex_to_idx[p]={self.simplex_to_idx[p]}") + print( + f"returning {[p for p in simplices if B[self.simplex_to_idx[p]][l] == 1]}" + ) + return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 1] From 3a072f7f080a3a327b057e6321ea6d8385e3e2f0 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 12 Oct 2023 01:34:14 -0400 Subject: [PATCH 0146/3044] remove printf debugging --- .../transform/disagreggated_logarithmic.py | 67 +++++++------------ 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index e0b6d75d0e4..00fb1546412 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -34,7 +34,6 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - self.DEBUG = False # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any). This is where we will put our new components. @@ -44,14 +43,13 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Dimensionality of the PWLF dimension = pw_expr.nargs() - print(f"DIMENSIOn={dimension}") transBlock.dimension_indices = RangeSet(0, dimension - 1) # Substitute Var that will hold the value of the PWLE substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) - # Bounds for the substitute_var that we will tighten + # Bounds for the substitute_var that we will widen self.substitute_var_lb = float("inf") self.substitute_var_ub = -float("inf") @@ -71,26 +69,35 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # List of tuples of simplices with their linear function simplices_and_lin_funcs = list(zip(simplices, pw_linear_func._linear_functions)) - print("a") - print(f"Num_simplices: {num_simplices}") + # We don't seem to get a convenient opportunity later, so let's just widen + # the bounds here. All we need to do is go through the corners of each simplex. + for P, linear_func in simplices_and_lin_funcs: + for v in transBlock.simplex_point_indices: + val = linear_func(*pw_linear_func._points[P[v]]) + if val < self.substitute_var_lb: + self.substitute_var_lb = val + if val > self.substitute_var_ub: + self.substitute_var_ub = val + # Now set those bounds + if self.substitute_var_lb < float('inf'): + transBlock.substitute_var.setlb(self.substitute_var_lb) + if self.substitute_var_ub > -float('inf'): + transBlock.substitute_var.setub(self.substitute_var_ub) log_dimension = ceil(log2(num_simplices)) transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) binaries = transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) - # Injective function \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices + # Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices # (really just polytopes are required) with binary vectors. Any injective function - # is valid. + # is enough here. B = {} for i in transBlock.simplex_indices: # map index(P) -> corresponding vector in {0, 1}^n B[i] = self._get_binary_vector(i, log_dimension) - print(f"after construction, B = {B}") - print("b") # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it transBlock.lambdas = Var(transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1)) - print("b1") # Sum of all lambdas is one (6b) transBlock.convex_combo = Constraint( @@ -102,15 +109,10 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc == 1 ) - print("c") - # The branching rules, establishing using the binaries that only one simplex's lambdas # may be nonzero @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) def simplex_choice_1(b, l): - print("entering constraint generator") - print(f"thing={self._P_plus(B, l, simplices)}") - print("returning") return ( sum( transBlock.lambdas[self.simplex_to_idx[P], v] @@ -120,8 +122,6 @@ def simplex_choice_1(b, l): <= binaries[l] ) - print("c1") - @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) def simplex_choice_2(b, l): return ( @@ -133,18 +133,10 @@ def simplex_choice_2(b, l): <= 1 - binaries[l] ) - print("d") - # for i, (simplex, pwlf) in enumerate(choices): # x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P)) @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) def x_constraint(b, i): - - print(f"simplices are {[P for P in simplices]}") - print(f"points are {pw_linear_func._points}") - print(f"simplex_point_indices is {list(transBlock.simplex_point_indices)}") - print(f"i={i}") - return pw_expr.args[i] == sum( transBlock.lambdas[self.simplex_to_idx[P], v] * pw_linear_func._points[P[v]][i] @@ -153,14 +145,14 @@ def x_constraint(b, i): ) # Make the substitute Var equal the PWLE (6a.2) - for P, linear_func in simplices_and_lin_funcs: - print(f"P, linear_func = {P}, {linear_func}") - for v in transBlock.simplex_point_indices: - print(f" v={v}") - print(f" pt={pw_linear_func._points[P[v]]}") - print( - f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" - ) + #for P, linear_func in simplices_and_lin_funcs: + # print(f"P, linear_func = {P}, {linear_func}") + # for v in transBlock.simplex_point_indices: + # print(f" v={v}") + # print(f" pt={pw_linear_func._points[P[v]]}") + # print( + # f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" + # ) transBlock.set_substitute = Constraint( expr=substitute_var == sum( @@ -173,11 +165,10 @@ def x_constraint(b, i): ) ) - print("f") return substitute_var # Not a gray code, just a regular binary representation - # TODO this is probably not optimal, test the gray codes too + # TODO this may not be optimal, test the gray codes too def _get_binary_vector(self, num, length): if num != 0 and ceil(log2(num)) > length: raise DeveloperError("Invalid input in _get_binary_vector") @@ -191,10 +182,4 @@ def _P_0(self, B, l, simplices): # Return {P \in \mathcal{P} | B(P)_l = 1} def _P_plus(self, B, l, simplices): - print(f"p plus: B={B}, l={l}, simplices={simplices}") - for p in simplices: - print(f"for p={p}, simplex_to_idx[p]={self.simplex_to_idx[p]}") - print( - f"returning {[p for p in simplices if B[self.simplex_to_idx[p]][l] == 1]}" - ) return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 1] From 3f684add395284912902558ca7e34ab081aae083 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 12 Oct 2023 02:23:00 -0400 Subject: [PATCH 0147/3044] fix strange reverse indexing --- .../transform/disagreggated_logarithmic.py | 70 +++++++++---------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index 00fb1546412..e86d5539367 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -15,17 +15,19 @@ "contrib.piecewise.disaggregated_logarithmic", doc=""" Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This method of logarithmically - formulating the piecewise linear function imposes no restrictions on the - family of polytopes. This method is due to Vielma et al., 2010. + log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; + GDP is not used. This method of logarithmically formulating the piecewise + linear function imposes no restrictions on the family of polytopes, but we + assume we have simplces in this code. This method is due to Vielma et al., 2010. """, ) class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): """ Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This method of logarithmically - formulating the piecewise linear function imposes no restrictions on the - family of polytopes. This method is due to Vielma et al., 2010. + log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; + GDP is not used. This method of logarithmically formulating the piecewise + linear function imposes no restrictions on the family of polytopes, but we + assume we have simplces in this code. This method is due to Vielma et al., 2010. """ CONFIG = PiecewiseLinearToGDP.CONFIG() @@ -35,8 +37,8 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - # Get a new Block() in transformation_block.transformed_functions, which - # is a Block(Any). This is where we will put our new components. + # Get a new Block for our transformationin transformation_block.transformed_functions, + # which is a Block(Any). This is where we will put our new components. transBlock = transformation_block.transformed_functions[ len(transformation_block.transformed_functions) ] @@ -61,32 +63,28 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # which is dimension + 1 transBlock.simplex_point_indices = RangeSet(0, dimension) - # Enumeration of simplices, map from simplex number to simplex object + # Enumeration of simplices: map from simplex number to simplex object self.idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} - # Inverse of previous enumeration - self.simplex_to_idx = {v: k for k, v in self.idx_to_simplex.items()} - # List of tuples of simplices with their linear function - simplices_and_lin_funcs = list(zip(simplices, pw_linear_func._linear_functions)) + # List of tuples of simplex indices with their linear function + simplex_indices_and_lin_funcs = list(zip(transBlock.simplex_indices, pw_linear_func._linear_functions)) # We don't seem to get a convenient opportunity later, so let's just widen # the bounds here. All we need to do is go through the corners of each simplex. - for P, linear_func in simplices_and_lin_funcs: + for P, linear_func in simplex_indices_and_lin_funcs: for v in transBlock.simplex_point_indices: - val = linear_func(*pw_linear_func._points[P[v]]) + val = linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) if val < self.substitute_var_lb: self.substitute_var_lb = val if val > self.substitute_var_ub: self.substitute_var_ub = val # Now set those bounds - if self.substitute_var_lb < float('inf'): - transBlock.substitute_var.setlb(self.substitute_var_lb) - if self.substitute_var_ub > -float('inf'): - transBlock.substitute_var.setub(self.substitute_var_ub) + transBlock.substitute_var.setlb(self.substitute_var_lb) + transBlock.substitute_var.setub(self.substitute_var_ub) log_dimension = ceil(log2(num_simplices)) transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) - binaries = transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) + transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) # Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices # (really just polytopes are required) with binary vectors. Any injective function @@ -115,22 +113,22 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc def simplex_choice_1(b, l): return ( sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - for P in self._P_plus(B, l, simplices) + transBlock.lambdas[P, v] + for P in self._P_plus(B, l, transBlock.simplex_indices) for v in transBlock.simplex_point_indices ) - <= binaries[l] + <= transBlock.binaries[l] ) @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) def simplex_choice_2(b, l): return ( sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - for P in self._P_0(B, l, simplices) + transBlock.lambdas[P, v] + for P in self._P_0(B, l, transBlock.simplex_indices) for v in transBlock.simplex_point_indices ) - <= 1 - binaries[l] + <= 1 - transBlock.binaries[l] ) # for i, (simplex, pwlf) in enumerate(choices): @@ -138,9 +136,9 @@ def simplex_choice_2(b, l): @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) def x_constraint(b, i): return pw_expr.args[i] == sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - * pw_linear_func._points[P[v]][i] - for P in simplices + transBlock.lambdas[P, v] + * pw_linear_func._points[self.idx_to_simplex[P][v]][i] + for P in transBlock.simplex_indices for v in transBlock.simplex_point_indices ) @@ -157,11 +155,11 @@ def x_constraint(b, i): expr=substitute_var == sum( sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - * linear_func(*pw_linear_func._points[P[v]]) + transBlock.lambdas[P, v] + * linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) for v in transBlock.simplex_point_indices ) - for (P, linear_func) in simplices_and_lin_funcs + for (P, linear_func) in simplex_indices_and_lin_funcs ) ) @@ -177,9 +175,9 @@ def _get_binary_vector(self, num, length): return tuple(int(x) for x in format(num, f"0{length}b")) # Return {P \in \mathcal{P} | B(P)_l = 0} - def _P_0(self, B, l, simplices): - return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 0] + def _P_0(self, B, l, simplex_indices): + return [p for p in simplex_indices if B[p][l] == 0] # Return {P \in \mathcal{P} | B(P)_l = 1} - def _P_plus(self, B, l, simplices): - return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 1] + def _P_plus(self, B, l, simplex_indices): + return [p for p in simplex_indices if B[p][l] == 1] From 7dce14b335539d4523146a51beb926f44352e321 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 12 Oct 2023 02:24:42 -0400 Subject: [PATCH 0148/3044] incremental transform: initial --- .../piecewise/transform/incremental.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 pyomo/contrib/piecewise/transform/incremental.py diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py new file mode 100644 index 00000000000..0551f38d8f8 --- /dev/null +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -0,0 +1,41 @@ +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( + PiecewiseLinearToGDP, +) +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet +from pyomo.core.base import TransformationFactory +from pyomo.gdp import Disjunct, Disjunction +from pyomo.common.errors import DeveloperError +from pyomo.core.expr.visitor import SimpleExpressionVisitor +from pyomo.core.expr.current import identify_components +from math import ceil, log2 + +@TransformationFactory.register( + 'contrib.piecewise.incremental', + doc= + """ + TODO document + """, +) +class IncrementalInnerGDPTransformation(PiecewiseLinearToGDP): + """ + TODO document + """ + CONFIG = PiecewiseLinearToGDP.CONFIG() + _transformation_name = 'pw_linear_incremental' + + # Implement to use PiecewiseLinearToGDP. This function returns the Var + # that replaces the transformed piecewise linear expr + def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): + self.DEBUG = False + # Get a new Block() in transformation_block.transformed_functions, which + # is a Block(Any) + transBlock = transformation_block.transformed_functions[ + len(transformation_block.transformed_functions) + ] + + dimension = pw_expr.nargs() + substitute_var = transBlock.substitute_var = Var() + pw_linear_func.map_transformation_var(pw_expr, substitute_var) + self.substitute_var_lb = float('inf') + self.substitute_var_ub = -float('inf') From 82df834ba5010f97340b7c8c23bedc9cada4a377 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 26 Oct 2023 10:22:32 -0400 Subject: [PATCH 0149/3044] continue incremental transform implementation --- .../piecewise/transform/incremental.py | 126 ++++++++++++++++-- 1 file changed, 118 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 0551f38d8f8..92d1a9f8e04 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -2,7 +2,7 @@ from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( PiecewiseLinearToGDP, ) -from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet, Param from pyomo.core.base import TransformationFactory from pyomo.gdp import Disjunct, Disjunction from pyomo.common.errors import DeveloperError @@ -10,10 +10,10 @@ from pyomo.core.expr.current import identify_components from math import ceil, log2 + @TransformationFactory.register( - 'contrib.piecewise.incremental', - doc= - """ + "contrib.piecewise.incremental", + doc=""" TODO document """, ) @@ -21,9 +21,10 @@ class IncrementalInnerGDPTransformation(PiecewiseLinearToGDP): """ TODO document """ + CONFIG = PiecewiseLinearToGDP.CONFIG() - _transformation_name = 'pw_linear_incremental' - + _transformation_name = "pw_linear_incremental" + # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): @@ -34,8 +35,117 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc len(transformation_block.transformed_functions) ] + # Dimensionality of the PWLF dimension = pw_expr.nargs() + transBlock.dimension_indices = RangeSet(0, dimension - 1) + + # Substitute Var that will hold the value of the PWLE substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) - self.substitute_var_lb = float('inf') - self.substitute_var_ub = -float('inf') + + # Bounds for the substitute_var that we will widen + self.substitute_var_lb = float("inf") + self.substitute_var_ub = -float("inf") + + # Simplices are tuples of indices of points. Give them their own indices, too + simplices = pw_linear_func._simplices + num_simplices = len(simplices) + transBlock.simplex_indices = RangeSet(0, num_simplices - 1) + transBlock.simplex_indices_except_last = RangeSet(0, num_simplices - 2) + # Assumption: the simplices are really simplices and all have the same number of points, + # which is dimension + 1 + transBlock.simplex_point_indices = RangeSet(0, dimension) + transBlock.nonzero_simplex_point_indices = RangeSet(1, dimension) + transBlock.last_simplex_point_index = Param(dimension) + + + # Ordering of simplices to follow Vielma + # TODO: this enumeration must satisfy O1 (Vielma): each T_i \cap T_{i-1} is nonempty + self.simplex_ordering = { + n: n for n in transBlock.simplex_indices + } + + # Enumeration of simplices: map from simplex number to correct simplex object + self.idx_to_simplex = { + n: simplices[m] for n, m in self.simplex_ordering + } + # Associate simplex indices with correct linear functions + self.idx_to_lin_func = { + n: pw_linear_func._linear_functions[m] for n, m in self.simplex_ordering + } + + # For each individual simplex, the points need to be permuted in a way that + # satisfies O1 and O2 (Vielma). TODO TODO TODO + self.vertex_ordering = { + (T, n): n + for T in transBlock.simplex_indices + for n in transBlock.simplex_point_indices + } + + # Inital vertex (v_0^0 in Vielma) + self.initial_vertex = pw_linear_func._points[self.index_to_simplex[0][self.vertex_ordering[0, 0]]] + + # delta_i^j = delta[simplex][point] + transBlock.delta = Var( + transBlock.simplex_indices, + transBlock.nonzero_simplex_point_indices, + bounds=(0, 1), + ) + transBlock.delta_one_constraint = Constraint( + # figure out if this needs to be 0 or 1 + expr=sum( + transBlock.delta[0, j] for j in transBlock.nonzero_simplex_point_indices + ) + <= 1 + ) + # Set up the binary y_i variables, which interleave with the delta_i^j in + # an odd way + transBlock.y_binaries = Var( + transBlock.simplex_indices, + domain=Binary + ) + + # If the delta for the final point in simplex i is not one, y_i must be zero. That is, + # y_i is one for and only for simplices that are completely "used" + @transBlock.Constraint(transBlock.simplex_indices_except_last) + def y_below_delta(m, i): + return (transBlock.y_binaries[i] <= transBlock.delta[i, transBlock.last_simplex_point_index]) + + # The sum of the deltas for simplex i+1 should be less than y_i. The overall + # effect of these two constraints is that for simplices with y_i=1, the final + # delta being one and others zero is enforced. For the first simplex with y_i=0, + # the choice of deltas is free except that they must add to one. For following + # simplices with y_i=0, all deltas are fixed at zero. + @transBlock.Constraint(transBlock.simplex_indices_except_last) + def deltas_below_y(m, i): + return (sum(transBlock.delta[i + 1, j] for j in transBlock.nonzero_simplex_point_indices) <= transBlock.y_binaries[i]) + + # Now we can relate the deltas and x. x is a sum along differences of points, + # weighted by deltas (12a.1) + @transBlock.Constraint(transBlock.dimension_indices) + def x_constraint(b, n): + return (pw_expr.args[n] == + self.initial_vertex[n] + sum( + sum( + # delta_i^j * (v_i^j - v_i^0) + transBlock.delta[i, j] * (pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[i, j]]][n] + - pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[i, 0]]][n]) + for j in transBlock.nonzero_simplex_point_indices + ) + for i in transBlock.simplex_indices + ) + ) + + # Now we can set the substitute Var for the PWLE (12a.2) + transBlock.set_substitute = Constraint( + expr=substitute_var + == self.idx_to_lin_func[0](*self.initial_vertex) + sum( + sum( + # delta_i^j * (f(v_i^j) - f(v_i^0)) + transBlock.delta[i, j] * (self.idx_to_lin_func[i](*pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[i, j]]]) + - self.idx_to_lin_func[i](*pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[i, 0]]])) + for j in transBlock.nonzero_simplex_point_indices + ) + for i in transBlock.simplex_indices + ) + ) \ No newline at end of file From 16bc3bc61fddcfc9a568598a9fb0f5d5a4cf603f Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 26 Oct 2023 10:40:51 -0400 Subject: [PATCH 0150/3044] fix some incremental transform errors --- pyomo/contrib/piecewise/transform/incremental.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 92d1a9f8e04..58aadbc31a3 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -56,7 +56,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # which is dimension + 1 transBlock.simplex_point_indices = RangeSet(0, dimension) transBlock.nonzero_simplex_point_indices = RangeSet(1, dimension) - transBlock.last_simplex_point_index = Param(dimension) + transBlock.last_simplex_point_index = Param(initialize=dimension) # Ordering of simplices to follow Vielma @@ -67,11 +67,11 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Enumeration of simplices: map from simplex number to correct simplex object self.idx_to_simplex = { - n: simplices[m] for n, m in self.simplex_ordering + n: simplices[m] for n, m in self.simplex_ordering.items() } # Associate simplex indices with correct linear functions self.idx_to_lin_func = { - n: pw_linear_func._linear_functions[m] for n, m in self.simplex_ordering + n: pw_linear_func._linear_functions[m] for n, m in self.simplex_ordering.items() } # For each individual simplex, the points need to be permuted in a way that @@ -83,7 +83,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc } # Inital vertex (v_0^0 in Vielma) - self.initial_vertex = pw_linear_func._points[self.index_to_simplex[0][self.vertex_ordering[0, 0]]] + self.initial_vertex = pw_linear_func._points[self.idx_to_simplex[0][self.vertex_ordering[0, 0]]] # delta_i^j = delta[simplex][point] transBlock.delta = Var( @@ -128,8 +128,8 @@ def x_constraint(b, n): self.initial_vertex[n] + sum( sum( # delta_i^j * (v_i^j - v_i^0) - transBlock.delta[i, j] * (pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[i, j]]][n] - - pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[i, 0]]][n]) + transBlock.delta[i, j] * (pw_linear_func._points[self.idx_to_simplex[i][self.vertex_ordering[i, j]]][n] + - pw_linear_func._points[self.idx_to_simplex[i][self.vertex_ordering[i, 0]]][n]) for j in transBlock.nonzero_simplex_point_indices ) for i in transBlock.simplex_indices @@ -142,8 +142,8 @@ def x_constraint(b, n): == self.idx_to_lin_func[0](*self.initial_vertex) + sum( sum( # delta_i^j * (f(v_i^j) - f(v_i^0)) - transBlock.delta[i, j] * (self.idx_to_lin_func[i](*pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[i, j]]]) - - self.idx_to_lin_func[i](*pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[i, 0]]])) + transBlock.delta[i, j] * (self.idx_to_lin_func[i](*pw_linear_func._points[self.idx_to_simplex[i][self.vertex_ordering[i, j]]]) + - self.idx_to_lin_func[i](*pw_linear_func._points[self.idx_to_simplex[i][self.vertex_ordering[i, 0]]])) for j in transBlock.nonzero_simplex_point_indices ) for i in transBlock.simplex_indices From ee3b3166a5372a1879e3bcb333405c9e6b9afb46 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 26 Oct 2023 12:00:09 -0400 Subject: [PATCH 0151/3044] minor pw linear changes --- .../piecewise/tests/test_incremental.py | 74 +++++++++++++++++++ .../tests/test_nested_inner_repn_gdp.py | 13 ++++ .../piecewise/transform/incremental.py | 2 +- .../piecewise/transform/nested_inner_repn.py | 2 +- 4 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 pyomo/contrib/piecewise/tests/test_incremental.py diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py new file mode 100644 index 00000000000..ea1b5158b1a --- /dev/null +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -0,0 +1,74 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.piecewise.tests import models +import pyomo.contrib.piecewise.tests.common_tests as ct +from pyomo.core.base import TransformationFactory +from pyomo.core.expr.compare import ( + assertExpressionsEqual, + assertExpressionsStructurallyEqual, +) +from pyomo.gdp import Disjunct, Disjunction +from pyomo.environ import Constraint, SolverFactory, Var, ConcreteModel, Objective, log, value +from pyomo.contrib.piecewise import PiecewiseLinearFunction + +from pyomo.contrib.piecewise.transform.incremental import IncrementalInnerGDPTransformation + +class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): + + #def test_solve_log_model(self): + # m = models.make_log_x_model() + # TransformationFactory( + # 'contrib.piecewise.incremental' + # ).apply_to(m) + # TransformationFactory( + # 'gdp.bigm' + # ).apply_to(m) + # SolverFactory('gurobi').solve(m) + # ct.check_log_x_model_soln(self, m) + + def test_solve_univariate_log_model(self): + m = ConcreteModel() + m.x = Var(bounds=(1, 10)) + m.pw_log = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=log) + + # Here are the linear functions, for safe keeping. + def f1(x): + return (log(3) / 2) * x - log(3) / 2 + + m.f1 = f1 + + def f2(x): + return (log(2) / 3) * x + log(3 / 2) + + m.f2 = f2 + + def f3(x): + return (log(5 / 3) / 4) * x + log(6 / ((5 / 3) ** (3 / 2))) + + m.f3 = f3 + + m.log_expr = m.pw_log(m.x) + m.obj = Objective(expr=m.log_expr) + + TransformationFactory( + 'contrib.piecewise.incremental' + ).apply_to(m) + m.pprint() + TransformationFactory( + 'gdp.hull' + ).apply_to(m) + print('####### PPRINTNG AGAIN AFTER BIGM #######') + m.pprint() + # log is increasing so the optimal value should be log(10) + SolverFactory('gurobi').solve(m) + self.assertTrue(abs(value(m.obj) - log(10)) < 0.001) \ No newline at end of file diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py index 48357c828df..2a87c86f6b4 100644 --- a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -21,6 +21,8 @@ from pyomo.environ import Constraint, SolverFactory, Var from pyomo.contrib.piecewise.transform.nested_inner_repn import NestedInnerRepresentationGDPTransformation +from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import DisaggregatedLogarithmicInnerGDPTransformation +from pyomo.contrib.piecewise.transform.incremental import IncrementalInnerGDPTransformation class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): @@ -33,4 +35,15 @@ def test_solve_log_model(self): 'gdp.bigm' ).apply_to(m) SolverFactory('gurobi').solve(m) + ct.check_log_x_model_soln(self, m) + + def test_solve_log_model_2(self): + m = models.make_log_x_model() + TransformationFactory( + 'contrib.piecewise.disaggregated_logarithmic' + ).apply_to(m) + TransformationFactory( + 'gdp.bigm' + ).apply_to(m) + SolverFactory('gurobi').solve(m) ct.check_log_x_model_soln(self, m) \ No newline at end of file diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 58aadbc31a3..11de6c09011 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -101,7 +101,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Set up the binary y_i variables, which interleave with the delta_i^j in # an odd way transBlock.y_binaries = Var( - transBlock.simplex_indices, + transBlock.simplex_indices_except_last, domain=Binary ) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 6c551818c84..d57a99600fe 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -61,7 +61,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Add the disjunction transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock) - # Widen bounds as determined when setting up the disjunction + # Set bounds as determined when setting up the disjunction if self.substitute_var_lb < float('inf'): transBlock.substitute_var.setlb(self.substitute_var_lb) if self.substitute_var_ub > -float('inf'): From 3d47204e9072015926515830ffbbaf0e016ced7d Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 9 Nov 2023 01:00:47 -0500 Subject: [PATCH 0152/3044] incremental: fix obvious bug --- .../piecewise/tests/test_incremental.py | 11 ++++++++--- .../contrib/piecewise/transform/incremental.py | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index ea1b5158b1a..36bade33381 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -18,10 +18,13 @@ assertExpressionsStructurallyEqual, ) from pyomo.gdp import Disjunct, Disjunction -from pyomo.environ import Constraint, SolverFactory, Var, ConcreteModel, Objective, log, value +from pyomo.environ import Constraint, SolverFactory, Var, ConcreteModel, Objective, log, value, maximize from pyomo.contrib.piecewise import PiecewiseLinearFunction from pyomo.contrib.piecewise.transform.incremental import IncrementalInnerGDPTransformation +from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import ( + DisaggregatedLogarithmicInnerGDPTransformation +) class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): @@ -58,17 +61,19 @@ def f3(x): m.f3 = f3 m.log_expr = m.pw_log(m.x) - m.obj = Objective(expr=m.log_expr) + m.obj = Objective(expr=m.log_expr, sense=maximize) TransformationFactory( 'contrib.piecewise.incremental' + #'contrib.piecewise.disaggregated_logarithmic' ).apply_to(m) m.pprint() TransformationFactory( - 'gdp.hull' + 'gdp.bigm' ).apply_to(m) print('####### PPRINTNG AGAIN AFTER BIGM #######') m.pprint() # log is increasing so the optimal value should be log(10) SolverFactory('gurobi').solve(m) + print(f"optimal value is {value(m.obj)}") self.assertTrue(abs(value(m.obj) - log(10)) < 0.001) \ No newline at end of file diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 11de6c09011..4287a2c5230 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -10,7 +10,6 @@ from pyomo.core.expr.current import identify_components from math import ceil, log2 - @TransformationFactory.register( "contrib.piecewise.incremental", doc=""" @@ -58,6 +57,19 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc transBlock.nonzero_simplex_point_indices = RangeSet(1, dimension) transBlock.last_simplex_point_index = Param(initialize=dimension) + # We don't seem to get a convenient opportunity later, so let's just widen + # the bounds here. All we need to do is go through the corners of each simplex. + for P, linear_func in zip(transBlock.simplex_indices, pw_linear_func._linear_functions): + for v in transBlock.simplex_point_indices: + val = linear_func(*pw_linear_func._points[simplices[P][v]]) + if val < self.substitute_var_lb: + self.substitute_var_lb = val + if val > self.substitute_var_ub: + self.substitute_var_ub = val + # Now set those bounds + transBlock.substitute_var.setlb(self.substitute_var_lb) + transBlock.substitute_var.setub(self.substitute_var_ub) + # Ordering of simplices to follow Vielma # TODO: this enumeration must satisfy O1 (Vielma): each T_i \cap T_{i-1} is nonempty @@ -148,4 +160,6 @@ def x_constraint(b, n): ) for i in transBlock.simplex_indices ) - ) \ No newline at end of file + ) + + return substitute_var \ No newline at end of file From 3d47029b9cea14660fd6612092eff1333bfb29cc Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 9 Nov 2023 18:12:26 -0700 Subject: [PATCH 0153/3044] Fixing a bug with adding multiple identical reaggregation constraints --- pyomo/gdp/plugins/hull.py | 177 ++++++++++++++++++-------------------- 1 file changed, 84 insertions(+), 93 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index b6e8065ba67..7a5d752bdbb 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -355,8 +355,9 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, disaggregatedVars = transBlock._disaggregatedVars disaggregated_var_bounds = transBlock._boundsConstraints - # We first go through and collect all the variables that we - # are going to disaggregate. + # We first go through and collect all the variables that we are going to + # disaggregate. We do this in its own pass because we want to know all + # the Disjuncts that each Var appears in. var_order = ComponentSet() disjuncts_var_appears_in = ComponentMap() for disjunct in active_disjuncts: @@ -390,9 +391,8 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, disjuncts_var_appears_in[var].add(disjunct) # We will disaggregate all variables that are not explicitly declared as - # being local. Since we transform from leaf to root, we are implicitly - # treating our own disaggregated variables as local, so they will not be - # re-disaggregated. + # being local. We have marked our own disaggregated variables as local, + # so they will not be re-disaggregated. vars_to_disaggregate = {disj: ComponentSet() for disj in obj.disjuncts} for var in var_order: disjuncts = disjuncts_var_appears_in[var] @@ -420,10 +420,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. - print("obj: %s" % obj) - print("parent disjunct: %s" % parent_disjunct) parent_local_var_list = self._get_local_var_list(parent_disjunct) - print("parent_local_var_list: %s" % parent_local_var_list) or_expr = 0 for disjunct in obj.disjuncts: or_expr += disjunct.indicator_var.get_associated_binary() @@ -443,90 +440,86 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # add the reaggregation constraints i = 0 - for disj in active_disjuncts: - for var in vars_to_disaggregate[disj]: - # There are two cases here: Either the var appeared in every - # disjunct in the disjunction, or it didn't. If it did, there's - # nothing special to do: All of the disaggregated variables have - # been created, and we can just proceed and make this constraint. If - # it didn't, we need one more disaggregated variable, correctly - # defined. And then we can make the constraint. - if len(disjuncts_var_appears_in[var]) < len(obj.disjuncts): - # create one more disaggregated var - idx = len(disaggregatedVars) - disaggregated_var = disaggregatedVars[idx] - # mark this as local because we won't re-disaggregate if this is - # a nested disjunction - if parent_local_var_list is not None: - parent_local_var_list.append(disaggregated_var) - local_vars_by_disjunct[parent_disjunct].add(disaggregated_var) - var_free = 1 - sum( - disj.indicator_var.get_associated_binary() - for disj in disjuncts_var_appears_in[var] - ) - self._declare_disaggregated_var_bounds( - var, - disaggregated_var, - obj, - disaggregated_var_bounds, - (idx, 'lb'), - (idx, 'ub'), - var_free, - ) - # For every Disjunct the Var does not appear in, we want to map - # that this new variable is its disaggreggated variable. - for disj in obj.disjuncts: - # Because we called _transform_disjunct above, we know that - # if this isn't transformed it is because it was cleanly - # deactivated, and we can just skip it. - if ( - disj._transformation_block is not None - and disj not in disjuncts_var_appears_in[var] - ): - relaxationBlock = disj._transformation_block().\ - parent_block() - relaxationBlock._bigMConstraintMap[ - disaggregated_var - ] = Reference(disaggregated_var_bounds[idx, :]) - relaxationBlock._disaggregatedVarMap['srcVar'][ - disaggregated_var - ] = var - relaxationBlock._disaggregatedVarMap[ - 'disaggregatedVar'][disj][ - var - ] = disaggregated_var - - disaggregatedExpr = disaggregated_var - else: - disaggregatedExpr = 0 - for disjunct in disjuncts_var_appears_in[var]: - # We know this Disjunct was active, so it has been transformed now. - disaggregatedVar = ( - disjunct._transformation_block() - .parent_block() - ._disaggregatedVarMap['disaggregatedVar'][disjunct][var] - ) - disaggregatedExpr += disaggregatedVar - - cons_idx = len(disaggregationConstraint) - # We always aggregate to the original var. If this is nested, this - # constraint will be transformed again. - print("Adding disaggregation constraint for '%s' on Disjunction '%s' " - "to Block '%s'" % - (var, obj, disaggregationConstraint.parent_block())) - disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) - # and update the map so that we can find this later. We index by - # variable and the particular disjunction because there is a - # different one for each disjunction - if disaggregationConstraintMap.get(var) is not None: - disaggregationConstraintMap[var][obj] = disaggregationConstraint[ - cons_idx - ] - else: - thismap = disaggregationConstraintMap[var] = ComponentMap() - thismap[obj] = disaggregationConstraint[cons_idx] + for var in var_order: + # There are two cases here: Either the var appeared in every + # disjunct in the disjunction, or it didn't. If it did, there's + # nothing special to do: All of the disaggregated variables have + # been created, and we can just proceed and make this constraint. If + # it didn't, we need one more disaggregated variable, correctly + # defined. And then we can make the constraint. + if len(disjuncts_var_appears_in[var]) < len(active_disjuncts): + # create one more disaggregated var + idx = len(disaggregatedVars) + disaggregated_var = disaggregatedVars[idx] + # mark this as local because we won't re-disaggregate if this is + # a nested disjunction + if parent_local_var_list is not None: + parent_local_var_list.append(disaggregated_var) + local_vars_by_disjunct[parent_disjunct].add(disaggregated_var) + var_free = 1 - sum( + disj.indicator_var.get_associated_binary() + for disj in disjuncts_var_appears_in[var] + ) + self._declare_disaggregated_var_bounds( + var, + disaggregated_var, + obj, + disaggregated_var_bounds, + (idx, 'lb'), + (idx, 'ub'), + var_free, + ) + # For every Disjunct the Var does not appear in, we want to map + # that this new variable is its disaggreggated variable. + for disj in active_disjuncts: + # Because we called _transform_disjunct above, we know that + # if this isn't transformed it is because it was cleanly + # deactivated, and we can just skip it. + if ( + disj._transformation_block is not None + and disj not in disjuncts_var_appears_in[var] + ): + relaxationBlock = disj._transformation_block().\ + parent_block() + relaxationBlock._bigMConstraintMap[ + disaggregated_var + ] = Reference(disaggregated_var_bounds[idx, :]) + relaxationBlock._disaggregatedVarMap['srcVar'][ + disaggregated_var + ] = var + relaxationBlock._disaggregatedVarMap[ + 'disaggregatedVar'][disj][ + var + ] = disaggregated_var + + disaggregatedExpr = disaggregated_var + else: + disaggregatedExpr = 0 + for disjunct in disjuncts_var_appears_in[var]: + # We know this Disjunct was active, so it has been transformed now. + disaggregatedVar = ( + disjunct._transformation_block() + .parent_block() + ._disaggregatedVarMap['disaggregatedVar'][disjunct][var] + ) + disaggregatedExpr += disaggregatedVar + + cons_idx = len(disaggregationConstraint) + # We always aggregate to the original var. If this is nested, this + # constraint will be transformed again. + disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) + # and update the map so that we can find this later. We index by + # variable and the particular disjunction because there is a + # different one for each disjunction + if disaggregationConstraintMap.get(var) is not None: + disaggregationConstraintMap[var][obj] = disaggregationConstraint[ + cons_idx + ] + else: + thismap = disaggregationConstraintMap[var] = ComponentMap() + thismap[obj] = disaggregationConstraint[cons_idx] - i += 1 + i += 1 # deactivate for the writers obj.deactivate() @@ -543,7 +536,6 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, # add the disaggregated variables and their bigm constraints # to the relaxationBlock for var in vars_to_disaggregate: - print("disaggregating %s" % var) disaggregatedVar = Var(within=Reals, initialize=var.value) # naming conflicts are possible here since this is a bunch # of variables from different blocks coming together, so we @@ -586,7 +578,6 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, "but it appeared in multiple Disjuncts, so it will be " "disaggregated." % (var.name, obj.name)) continue - print("we knew %s was local" % var) # we don't need to disaggregate, i.e., we can use this Var, but we # do need to set up its bounds constraints. From bb464908051c4580360098350a1828405eb5f434 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 9 Nov 2023 18:12:50 -0700 Subject: [PATCH 0154/3044] Fixing a couple nested GDP tests --- pyomo/gdp/tests/test_hull.py | 43 +++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 3ef57c73274..b224385bec0 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -52,6 +52,9 @@ import os from os.path import abspath, dirname, join +##DEBUG +from pytest import set_trace + currdir = dirname(abspath(__file__)) from filecmp import cmp @@ -1724,11 +1727,6 @@ def test_solve_nested_model(self): SolverFactory(linear_solvers[0]).solve(m_hull) - print("MODEL") - for cons in m_hull.component_data_objects(Constraint, active=True, - descend_into=Block): - print(cons.expr) - # check solution self.assertEqual(value(m_hull.d1.binary_indicator_var), 0) self.assertEqual(value(m_hull.d2.binary_indicator_var), 1) @@ -1892,11 +1890,14 @@ def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): self.assertEqual(y_p1.bounds, (-4, 5)) y_p2 = hull.get_disaggregated_var(m.y, m.parent2) self.assertEqual(y_p2.bounds, (-4, 5)) + y_cons = hull.get_disaggregation_constraint(m.y, m.parent1.disjunction) # check that the disaggregated ys in the nested just sum to the original - assertExpressionsEqual(self, y_cons.expr, y_p1 == other_y + y_c2) + y_cons_expr = self.simplify_cons(y_cons) + assertExpressionsEqual(self, y_cons_expr, y_p1 - other_y - y_c2 == 0.0) y_cons = hull.get_disaggregation_constraint(m.y, m.parent_disjunction) - assertExpressionsEqual(self, y_cons.expr, m.y == y_p1 + y_p2) + y_cons_expr = self.simplify_cons(y_cons) + assertExpressionsEqual(self, y_cons_expr, m.y - y_p2 - y_p1 == 0.0) x_c1 = hull.get_disaggregated_var(m.x, m.child1) x_c2 = hull.get_disaggregated_var(m.x, m.child2) @@ -1906,7 +1907,9 @@ def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): x_cons_parent = hull.get_disaggregation_constraint(m.x, m.parent_disjunction) assertExpressionsEqual(self, x_cons_parent.expr, m.x == x_p1 + x_p2) x_cons_child = hull.get_disaggregation_constraint(m.x, m.parent1.disjunction) - assertExpressionsEqual(self, x_cons_child.expr, x_p1 == x_c1 + x_c2 + x_c3) + x_cons_child_expr = self.simplify_cons(x_cons_child) + assertExpressionsEqual(self, x_cons_child_expr, x_p1 - x_c1 - x_c2 - + x_c3 == 0.0) def simplify_cons(self, cons): visitor = LinearRepnVisitor({}, {}, {}) @@ -1934,9 +1937,9 @@ def test_nested_with_var_that_skips_a_level(self): m.y1 = Disjunct() m.y1.c1 = Constraint(expr=m.x >= 4) m.y1.z1 = Disjunct() - m.y1.z1.c1 = Constraint(expr=m.y == 0) + m.y1.z1.c1 = Constraint(expr=m.y == 2) m.y1.z1.w1 = Disjunct() - m.y1.z1.w1.c1 = Constraint(expr=m.x == 0) + m.y1.z1.w1.c1 = Constraint(expr=m.x == 3) m.y1.z1.w2 = Disjunct() m.y1.z1.w2.c1 = Constraint(expr=m.x >= 1) m.y1.z1.disjunction = Disjunction(expr=[m.y1.z1.w1, m.y1.z1.w2]) @@ -1944,7 +1947,7 @@ def test_nested_with_var_that_skips_a_level(self): m.y1.z2.c1 = Constraint(expr=m.y == 1) m.y1.disjunction = Disjunction(expr=[m.y1.z1, m.y1.z2]) m.y2 = Disjunct() - m.y2.c1 = Constraint(expr=m.x == 0) + m.y2.c1 = Constraint(expr=m.x == 4) m.disjunction = Disjunction(expr=[m.y1, m.y2]) hull = TransformationFactory('gdp.hull') @@ -1965,26 +1968,26 @@ def test_nested_with_var_that_skips_a_level(self): cons = hull.get_disaggregation_constraint(m.x, m.y1.z1.disjunction) self.assertTrue(cons.active) cons_expr = self.simplify_cons(cons) - print(cons_expr) - print("") - print(x_z1 - x_w2 - x_w1 == 0) - assertExpressionsEqual(self, cons_expr, x_z1 - x_w2 - x_w1 == 0) + assertExpressionsEqual(self, cons_expr, x_z1 - x_w1 - x_w2 == 0.0) cons = hull.get_disaggregation_constraint(m.x, m.y1.disjunction) self.assertTrue(cons.active) - assertExpressionsEqual(self, cons.expr, x_y1 == x_z2 + x_z1) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, x_y1 - x_z2 - x_z1 == 0.0) cons = hull.get_disaggregation_constraint(m.x, m.disjunction) self.assertTrue(cons.active) - assertExpressionsEqual(self, cons.expr, m.x == x_y1 + x_y2) - + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, m.x - x_y1 - x_y2 == 0.0) cons = hull.get_disaggregation_constraint(m.y, m.y1.z1.disjunction, raise_exception=False) self.assertIsNone(cons) cons = hull.get_disaggregation_constraint(m.y, m.y1.disjunction) self.assertTrue(cons.active) - assertExpressionsEqual(self, cons.expr, y_y1 == y_z1 + y_z2) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, y_y1 - y_z1 - y_z2 == 0.0) cons = hull.get_disaggregation_constraint(m.y, m.disjunction) self.assertTrue(cons.active) - assertExpressionsEqual(self, cons.expr, m.y == y_y2 + y_y1) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, m.y - y_y2 - y_y1 == 0.0) class TestSpecialCases(unittest.TestCase): From 906fff7d18e9cc98c6a7d7e0a1b9d4657aaa9be9 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 13 Nov 2023 10:59:38 -0500 Subject: [PATCH 0155/3044] black format --- pyomo/contrib/mindtpy/algorithm_base_class.py | 26 ++++++++----- pyomo/contrib/mindtpy/single_tree.py | 38 +++++++++--------- pyomo/contrib/mindtpy/util.py | 39 ++++++++++++------- 3 files changed, 62 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 33b2f2c1d04..9771c04fc62 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -80,7 +80,7 @@ set_solver_mipgap, set_solver_constraint_violation_tolerance, update_solver_timelimit, - copy_var_list_values + copy_var_list_values, ) single_tree, single_tree_available = attempt_import('pyomo.contrib.mindtpy.single_tree') @@ -796,7 +796,9 @@ def MindtPy_initialization(self): self.integer_list.append(self.curr_int_sol) fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) - self.int_sol_2_cuts_ind[self.curr_int_sol] = list(range(1, len(self.mip.MindtPy_utils.cuts.oa_cuts) + 1)) + self.int_sol_2_cuts_ind[self.curr_int_sol] = list( + range(1, len(self.mip.MindtPy_utils.cuts.oa_cuts) + 1) + ) elif config.init_strategy == 'FP': self.init_rNLP() self.fp_loop() @@ -834,9 +836,15 @@ def init_rNLP(self, add_oa_cuts=True): subprob_terminate_cond = results.solver.termination_condition # Sometimes, the NLP solver might be trapped in a infeasible solution if the objective function is nonlinear and partition_obj_nonlinear_terms is True. If this happens, we will use the original objective function instead. - if subprob_terminate_cond == tc.infeasible and config.partition_obj_nonlinear_terms and self.rnlp.MindtPy_utils.objective_list[0].expr.polynomial_degree() not in self.mip_objective_polynomial_degree: + if ( + subprob_terminate_cond == tc.infeasible + and config.partition_obj_nonlinear_terms + and self.rnlp.MindtPy_utils.objective_list[0].expr.polynomial_degree() + not in self.mip_objective_polynomial_degree + ): config.logger.info( - 'Initial relaxed NLP problem is infeasible. This might be related to partition_obj_nonlinear_terms. Try to solve it again without partitioning nonlinear objective function.') + 'Initial relaxed NLP problem is infeasible. This might be related to partition_obj_nonlinear_terms. Try to solve it again without partitioning nonlinear objective function.' + ) self.rnlp.MindtPy_utils.objective.deactivate() self.rnlp.MindtPy_utils.objective_list[0].activate() results = self.nlp_opt.solve( @@ -889,14 +897,14 @@ def init_rNLP(self, add_oa_cuts=True): self.rnlp.MindtPy_utils.variable_list, self.mip.MindtPy_utils.variable_list, config, - ignore_integrality=True + ignore_integrality=True, ) if config.init_strategy == 'FP': copy_var_list_values( self.rnlp.MindtPy_utils.variable_list, self.working_model.MindtPy_utils.variable_list, config, - ignore_integrality=True + ignore_integrality=True, ) self.add_cuts( dual_values=dual_values, @@ -1700,9 +1708,7 @@ def solve_fp_main(self): config = self.config self.setup_fp_main() mip_args = self.set_up_mip_solver() - update_solver_timelimit( - self.mip_opt, config.mip_solver, self.timing, config - ) + update_solver_timelimit(self.mip_opt, config.mip_solver, self.timing, config) main_mip_results = self.mip_opt.solve( self.mip, @@ -2387,7 +2393,7 @@ def handle_fp_subproblem_optimal(self, fp_nlp): fp_nlp.MindtPy_utils.variable_list, self.working_model.MindtPy_utils.variable_list, self.config, - ignore_integrality=True + ignore_integrality=True, ) add_orthogonality_cuts(self.working_model, self.mip, self.config) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index dacde73a79e..66435c2587f 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -17,10 +17,7 @@ import pyomo.core.expr as EXPR from math import copysign from pyomo.contrib.mindtpy.util import get_integer_solution, copy_var_list_values -from pyomo.contrib.gdpopt.util import ( - get_main_elapsed_time, - time_code, -) +from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code from pyomo.opt import TerminationCondition as tc from pyomo.core import minimize, value from pyomo.core.expr import identify_variables @@ -35,13 +32,7 @@ class LazyOACallback_cplex( """Inherent class in CPLEX to call Lazy callback.""" def copy_lazy_var_list_values( - self, - opt, - from_list, - to_list, - config, - skip_stale=False, - skip_fixed=True, + self, opt, from_list, to_list, config, skip_stale=False, skip_fixed=True ): """This function copies variable values from one list to another. @@ -82,12 +73,14 @@ def copy_lazy_var_list_values( # instead log warnings). This means that the following # will always succeed and the ValueError should never be # raised. - if v_val in v_to.domain \ - and not ((v_to.has_lb() and v_val < v_to.lb)) \ - and not ((v_to.has_ub() and v_val > v_to.ub)): + if ( + v_val in v_to.domain + and not ((v_to.has_lb() and v_val < v_to.lb)) + and not ((v_to.has_ub() and v_val > v_to.ub)) + ): v_to.set_value(v_val) # Snap the value to the bounds - # TODO: check the performance of + # TODO: check the performance of # v_to.lb - v_val <= config.variable_tolerance elif ( v_to.has_lb() @@ -102,7 +95,10 @@ def copy_lazy_var_list_values( ): v_to.set_value(v_to.ub) # ... or the nearest integer - elif v_to.is_integer() and math.fabs(v_val - rounded_val) <= config.integer_tolerance: # and rounded_val in v_to.domain: + elif ( + v_to.is_integer() + and math.fabs(v_val - rounded_val) <= config.integer_tolerance + ): # and rounded_val in v_to.domain: v_to.set_value(rounded_val) elif abs(v_val) <= config.zero_tolerance and 0 in v_to.domain: v_to.set_value(0) @@ -945,7 +941,9 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): # Your callback should be prepared to cut off solutions that violate any of your lazy constraints, including those that have already been added. Node solutions will usually respect previously added lazy constraints, but not always. # https://www.gurobi.com/documentation/current/refman/cs_cb_addlazy.html # If this happens, MindtPy will look for the index of corresponding cuts, instead of solving the fixed-NLP again. - for ind in mindtpy_solver.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol]: + for ind in mindtpy_solver.int_sol_2_cuts_ind[ + mindtpy_solver.curr_int_sol + ]: cb_opt.cbLazy(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts[ind]) return else: @@ -960,7 +958,11 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): mindtpy_solver.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result, cb_opt) if config.strategy == 'OA': # store the cut index corresponding to current integer solution. - mindtpy_solver.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol] = list(range(cut_ind + 1, len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts) + 1)) + mindtpy_solver.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol] = list( + range( + cut_ind + 1, len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts) + 1 + ) + ) def handle_lazy_main_feasible_solution_gurobi(cb_m, cb_opt, mindtpy_solver, config): diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index da1534b49ac..48c8aab31c4 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -23,7 +23,7 @@ RangeSet, ConstraintList, TransformationFactory, - value + value, ) from pyomo.repn import generate_standard_repn from pyomo.contrib.mcpp.pyomo_mcpp import mcpp_available, McCormick @@ -567,7 +567,9 @@ def set_solver_mipgap(opt, solver_name, config): opt.options['add_options'].append('option optcr=%s;' % config.mip_solver_mipgap) -def set_solver_constraint_violation_tolerance(opt, solver_name, config, warm_start=True): +def set_solver_constraint_violation_tolerance( + opt, solver_name, config, warm_start=True +): """Set constraint violation tolerance for solvers. Parameters @@ -701,9 +703,11 @@ def copy_var_list_values_from_solution_pool( # bounds violations no longer generate exceptions (and # instead log warnings). This means that the following will # always succeed and the ValueError should never be raised. - if var_val in v_to.domain \ - and not ((v_to.has_lb() and var_val < v_to.lb)) \ - and not ((v_to.has_ub() and var_val > v_to.ub)): + if ( + var_val in v_to.domain + and not ((v_to.has_lb() and var_val < v_to.lb)) + and not ((v_to.has_ub() and var_val > v_to.ub)) + ): v_to.set_value(var_val, skip_validation=True) elif v_to.has_lb() and var_val < v_to.lb: v_to.set_value(v_to.lb) @@ -967,9 +971,15 @@ def generate_norm_constraint(fp_nlp_model, mip_model, config): ): fp_nlp_model.norm_constraint.add(nlp_var - mip_var.value <= rhs) -def copy_var_list_values(from_list, to_list, config, - skip_stale=False, skip_fixed=True, - ignore_integrality=False): + +def copy_var_list_values( + from_list, + to_list, + config, + skip_stale=False, + skip_fixed=True, + ignore_integrality=False, +): """Copy variable values from one list to another. Rounds to Binary/Integer if necessary Sets to zero for NonNegativeReals if necessary @@ -981,9 +991,11 @@ def copy_var_list_values(from_list, to_list, config, continue # Skip fixed variables. var_val = value(v_from, exception=False) rounded_val = int(round(var_val)) - if var_val in v_to.domain \ - and not ((v_to.has_lb() and var_val < v_to.lb)) \ - and not ((v_to.has_ub() and var_val > v_to.ub)): + if ( + var_val in v_to.domain + and not ((v_to.has_lb() and var_val < v_to.lb)) + and not ((v_to.has_ub() and var_val > v_to.ub)) + ): v_to.set_value(value(v_from, exception=False)) elif v_to.has_lb() and var_val < v_to.lb: v_to.set_value(v_to.lb) @@ -991,8 +1003,9 @@ def copy_var_list_values(from_list, to_list, config, v_to.set_value(v_to.ub) elif ignore_integrality and v_to.is_integer(): v_to.set_value(value(v_from, exception=False), skip_validation=True) - elif v_to.is_integer() and (math.fabs(var_val - rounded_val) <= - config.integer_tolerance): + elif v_to.is_integer() and ( + math.fabs(var_val - rounded_val) <= config.integer_tolerance + ): v_to.set_value(rounded_val) elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: v_to.set_value(0) From 60fee49b98e630baa3a1829e08bbff75e6b657ea Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 13 Nov 2023 13:57:36 -0700 Subject: [PATCH 0156/3044] Push changes from pair-programming --- pyomo/opt/plugins/sol.py | 1 + pyomo/solver/IPOPT.py | 4 +++- pyomo/solver/config.py | 1 + pyomo/solver/results.py | 36 ++++++++++++++---------------------- 4 files changed, 19 insertions(+), 23 deletions(-) diff --git a/pyomo/opt/plugins/sol.py b/pyomo/opt/plugins/sol.py index 6e1ca666633..255df117399 100644 --- a/pyomo/opt/plugins/sol.py +++ b/pyomo/opt/plugins/sol.py @@ -189,6 +189,7 @@ def _load(self, fin, res, soln, suffixes): if line == "": continue line = line.split() + # Some sort of garbage we tag onto the solver message, assuming we are past the suffixes if line[0] != 'suffix': # We assume this is the start of a # section like kestrel_option, which diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 875f8710b10..90c8a6d1bce 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -147,7 +147,9 @@ def solve(self, model, **kwds): results = Results() results.termination_condition = TerminationCondition.error else: - results = self._parse_solution() + # TODO: Make a context manager out of this and open the file + # to pass to the results, instead of doing this thing. + results = self._parse_solution(os.path.join(dname, model.name + '.sol'), self.info) def _parse_solution(self): # STOPPING POINT: The suggestion here is to look at the original diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index ed9008b7e1f..3f4424a8806 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -61,6 +61,7 @@ def __init__( self.load_solution: bool = self.declare( 'load_solution', ConfigValue(domain=bool, default=True) ) + self.raise_exception_on_nonoptimal_result: bool = self.declare('raise_exception_on_nonoptimal_result', ConfigValue(domain=bool, default=True)) self.symbolic_solver_labels: bool = self.declare( 'symbolic_solver_labels', ConfigValue(domain=bool, default=False) ) diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index d7505a7ed95..9aa2869b414 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -241,20 +241,20 @@ class ResultsReader: pass -def parse_sol_file(file, results): +def parse_sol_file(sol_file, nl_info): # The original reader for sol files is in pyomo.opt.plugins.sol. # Per my original complaint, it has "magic numbers" that I just don't # know how to test. It's apparently less fragile than that in APPSI. # NOTE: The Results object now also holds the solution loader, so we do # not need pass in a solution like we did previously. - if results is None: - results = Results() + # nl_info is an NLWriterInfo object that has vars, cons, etc. + results = Results() # For backwards compatibility and general safety, we will parse all # lines until "Options" appears. Anything before "Options" we will # consider to be the solver message. message = [] - for line in file: + for line in sol_file: if not line: break line = line.strip() @@ -265,40 +265,32 @@ def parse_sol_file(file, results): # Once "Options" appears, we must now read the content under it. model_objects = [] if "Options" in line: - line = file.readline() + line = sol_file.readline() number_of_options = int(line) need_tolerance = False if number_of_options > 4: # MRM: Entirely unclear why this is necessary, or if it even is number_of_options -= 2 need_tolerance = True for i in range(number_of_options + 4): - line = file.readline() + line = sol_file.readline() model_objects.append(int(line)) if need_tolerance: # MRM: Entirely unclear why this is necessary, or if it even is - line = file.readline() + line = sol_file.readline() model_objects.append(float(line)) else: raise SolverSystemError("ERROR READING `sol` FILE. No 'Options' line found.") # Identify the total number of variables and constraints number_of_cons = model_objects[number_of_options + 1] number_of_vars = model_objects[number_of_options + 3] - constraints = [] - variables = [] - # Parse through the constraint lines and capture the constraints - i = 0 - while i < number_of_cons: - line = file.readline() - constraints.append(float(line)) - i += 1 - # Parse through the variable lines and capture the variables - i = 0 - while i < number_of_vars: - line = file.readline() - variables.append(float(line)) - i += 1 + assert number_of_cons == len(nl_info.constraints) + assert number_of_vars == len(nl_info.variables) + + duals = [float(sol_file.readline()) for i in range(number_of_cons)] + variable_vals = [float(sol_file.readline()) for i in range(number_of_vars)] + # Parse the exit code line and capture it exit_code = [0, 0] - line = file.readline() + line = sol_file.readline() if line and ('objno' in line): exit_code_line = line.split() if (len(exit_code_line) != 3): From 124cdf41e8c98a233483e8578016658fec41501b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 13 Nov 2023 16:06:20 -0700 Subject: [PATCH 0157/3044] Fixing a bug where we were accidentally ignoring local vars and disaggregating them anyway --- pyomo/gdp/plugins/hull.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 7a5d752bdbb..1a76f08ff60 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -394,6 +394,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # being local. We have marked our own disaggregated variables as local, # so they will not be re-disaggregated. vars_to_disaggregate = {disj: ComponentSet() for disj in obj.disjuncts} + all_vars_to_disaggregate = ComponentSet() for var in var_order: disjuncts = disjuncts_var_appears_in[var] # clearly not local if used in more than one disjunct @@ -406,6 +407,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, ) for disj in disjuncts: vars_to_disaggregate[disj].add(var) + all_vars_to_disaggregate.add(var) else: # disjuncts is a set of length 1 disjunct = next(iter(disjuncts)) if disjunct in local_vars_by_disjunct: @@ -413,10 +415,12 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # It's not declared local to this Disjunct, so we # disaggregate vars_to_disaggregate[disjunct].add(var) + all_vars_to_disaggregate.add(var) else: # The user didn't declare any local vars for this # Disjunct, so we know we're disaggregating it vars_to_disaggregate[disjunct].add(var) + all_vars_to_disaggregate.add(var) # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. @@ -440,7 +444,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # add the reaggregation constraints i = 0 - for var in var_order: + for var in all_vars_to_disaggregate: # There are two cases here: Either the var appeared in every # disjunct in the disjunction, or it didn't. If it did, there's # nothing special to do: All of the disaggregated variables have @@ -526,6 +530,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, parent_local_var_suffix, parent_disjunct_local_vars): + print("\nTransforming '%s'" % obj.name) relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) # Put the disaggregated variables all on their own block so that we can @@ -560,6 +565,7 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, disaggregatedVarName + "_bounds", bigmConstraint ) + print("Adding bounds constraints for '%s'" % var) self._declare_disaggregated_var_bounds( var, disaggregatedVar, @@ -590,6 +596,9 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, bigmConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(conName, bigmConstraint) + print("Adding bounds constraints for local var '%s'" % var) + # TODO: This gets mapped in a place where we can't find it if we ask + # for it from the local var itself. self._declare_disaggregated_var_bounds( var, var, @@ -984,7 +993,7 @@ def get_var_bounds_constraint(self, v): Parameters ---------- - v: a Var which was created by the hull transformation as a + v: a Var that was created by the hull transformation as a disaggregated variable (and so appears on a transformation block of some Disjunct) """ From 688a3b17cfba8e8aeb93b4d03323fc0af84e4b1d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 13 Nov 2023 16:06:59 -0700 Subject: [PATCH 0158/3044] Generalizing the nested test, starting to test for not having more constraints than we expect (which currently we do) --- pyomo/gdp/tests/common_tests.py | 38 +++++++++++++++++++--- pyomo/gdp/tests/test_hull.py | 56 +++++++++++++++++++++++++++++---- 2 files changed, 83 insertions(+), 11 deletions(-) diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index b475334981b..2eff67a8826 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -1703,17 +1703,45 @@ def check_all_components_transformed(self, m): def check_transformation_blocks_nestedDisjunctions(self, m, transformation): disjunctionTransBlock = m.disj.algebraic_constraint.parent_block() transBlocks = disjunctionTransBlock.relaxedDisjuncts - self.assertEqual(len(transBlocks), 4) if transformation == 'bigm': + self.assertEqual(len(transBlocks), 4) self.assertIs(transBlocks[0], m.d1.d3.transformation_block) self.assertIs(transBlocks[1], m.d1.d4.transformation_block) self.assertIs(transBlocks[2], m.d1.transformation_block) self.assertIs(transBlocks[3], m.d2.transformation_block) if transformation == 'hull': - self.assertIs(transBlocks[2], m.d1.d3.transformation_block) - self.assertIs(transBlocks[3], m.d1.d4.transformation_block) - self.assertIs(transBlocks[0], m.d1.transformation_block) - self.assertIs(transBlocks[1], m.d2.transformation_block) + # This is a much more comprehensive test that doesn't depend on + # transformation Block structure, so just reuse it: + hull = TransformationFactory('gdp.hull') + d3 = hull.get_disaggregated_var(m.d1.d3.indicator_var, m.d1) + d4 = hull.get_disaggregated_var(m.d1.d4.indicator_var, m.d1) + self.check_transformed_model_nestedDisjuncts(m, d3, d4) + + # check the disaggregated indicator var bound constraints too + cons = hull.get_var_bounds_constraint(d3) + self.assertEqual(len(cons), 1) + check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, + cons_expr, + d3 - m.d1.binary_indicator_var <= 0.0 + ) + + cons = hull.get_var_bounds_constraint(d4) + self.assertEqual(len(cons), 1) + check_obj_in_active_tree(self, cons['ub']) + cons_expr = self.simplify_leq_cons(cons['ub']) + assertExpressionsEqual( + self, + cons_expr, + d4 - m.d1.binary_indicator_var <= 0.0 + ) + + num_cons = len(m.component_data_objects(Constraint, + active=True, + descend_into=Block)) + self.assertEqual(num_cons, 10) def check_nested_disjunction_target(self, transformation): diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index b224385bec0..cfd0feac2b2 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -1564,6 +1564,36 @@ def test_transformed_model_nestedDisjuncts(self): hull = TransformationFactory('gdp.hull') hull.apply_to(m) + self.check_transformed_model_nestedDisjuncts(m, m.d1.d3.binary_indicator_var, + m.d1.d4.binary_indicator_var) + + # Last, check that there aren't things we weren't expecting + + all_cons = list(m.component_data_objects(Constraint, active=True, + descend_into=Block)) + num_cons = len(all_cons) + # TODO: I shouldn't have d1.binary_indicator_var in the local list + # above, but I think if I do it should be ignored when it doesn't appear + # in any Disjuncts... + + # TODO: We get duplicate bounds constraints for inner disaggregated Vars + # because we declare bounds constraints for local vars every time. We + # should actually track them separately so that we don't duplicate + # bounds constraints over and over again. + for idx, cons in enumerate(all_cons): + print(idx) + print(cons.name) + print(cons.expr) + print("") + # 2 disaggregation constraints for x 0,3 + # + 4 bounds constraints for x 6,8,9,13, These are dumb: 10,14,16 + # + 2 bounds constraints for inner indicator vars 11, 12 + # + 2 exactly-one constraints 1,4 + # + 4 transformed constraints 2,5,7,15 + self.assertEqual(num_cons, 14) + + def check_transformed_model_nestedDisjuncts(self, m, d3, d4): + hull = TransformationFactory('gdp.hull') transBlock = m._pyomo_gdp_hull_reformulation self.assertTrue(transBlock.active) @@ -1590,8 +1620,8 @@ def test_transformed_model_nestedDisjuncts(self): assertExpressionsEqual( self, xor_expr, - m.d1.d3.binary_indicator_var + - m.d1.d4.binary_indicator_var - + d3 + + d4 - m.d1.binary_indicator_var == 0.0 ) @@ -1622,8 +1652,6 @@ def test_transformed_model_nestedDisjuncts(self): m.x - x_d1 - x_d2 == 0.0 ) - ## Bound constraints - ## Transformed constraints cons = hull.get_transformed_constraints(m.d1.d3.c) self.assertEqual(len(cons), 1) @@ -1698,7 +1726,7 @@ def test_transformed_model_nestedDisjuncts(self): assertExpressionsEqual( self, cons_expr, - x_d3 - 2*m.d1.d3.binary_indicator_var <= 0.0 + x_d3 - 2*d3 <= 0.0 ) cons = hull.get_var_bounds_constraint(x_d4) # the lb is trivial in this case, so we just have 1 @@ -1708,7 +1736,23 @@ def test_transformed_model_nestedDisjuncts(self): assertExpressionsEqual( self, cons_expr, - x_d4 - 2*m.d1.d4.binary_indicator_var <= 0.0 + x_d4 - 2*d4 <= 0.0 + ) + + # Bounds constraints for local vars + cons = hull.get_var_bounds_constraint(m.d1.d3.binary_indicator_var) + ct.check_obj_in_active_tree(self, cons['ub']) + assertExpressionsEqual( + self, + cons['ub'].expr, + m.d1.d3.binary_indicator_var <= m.d1.binary_indicator_var + ) + cons = hull.get_var_bounds_constraint(m.d1.d4.binary_indicator_var) + ct.check_obj_in_active_tree(self, cons['ub']) + assertExpressionsEqual( + self, + cons['ub'].expr, + m.d1.d4.binary_indicator_var <= m.d1.binary_indicator_var ) @unittest.skipIf(not linear_solvers, "No linear solver available") From 444e4abad71181dbccde53d9346a8f5f17f2120b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 15 Nov 2023 13:48:53 -0700 Subject: [PATCH 0159/3044] Explicitly collecting local vars so that we don't do anything silly with Vars declared as local that don't actually appear on the Disjunct, realizing that bound constraints at each level of the GDP tree matter. --- pyomo/gdp/plugins/hull.py | 22 +++++++++++++--------- pyomo/gdp/tests/test_hull.py | 15 ++++----------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 1a76f08ff60..54332ebc666 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -334,7 +334,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, if not obj.xor: raise GDP_Error( "Cannot do hull reformulation for " - "Disjunction '%s' with OR constraint. " + "Disjunction '%s' with OR constraint. " "Must be an XOR!" % obj.name ) # collect the Disjuncts we are going to transform now because we will @@ -395,6 +395,11 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # so they will not be re-disaggregated. vars_to_disaggregate = {disj: ComponentSet() for disj in obj.disjuncts} all_vars_to_disaggregate = ComponentSet() + # We will ignore variables declared as local in a Disjunct that don't + # actually appear in any Constraints on that Disjunct, but in order to + # do this, we will explicitly collect the set of local_vars in this + # loop. + local_vars = defaultdict(lambda: ComponentSet()) for var in var_order: disjuncts = disjuncts_var_appears_in[var] # clearly not local if used in more than one disjunct @@ -411,7 +416,9 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, else: # disjuncts is a set of length 1 disjunct = next(iter(disjuncts)) if disjunct in local_vars_by_disjunct: - if var not in local_vars_by_disjunct[disjunct]: + if var in local_vars_by_disjunct[disjunct]: + local_vars[disjunct].add(var) + else: # It's not declared local to this Disjunct, so we # disaggregate vars_to_disaggregate[disjunct].add(var) @@ -424,6 +431,9 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. + + # Get the list of local variables for the parent Disjunct so that we can + # add the disaggregated variables we're about to make to it: parent_local_var_list = self._get_local_var_list(parent_disjunct) or_expr = 0 for disjunct in obj.disjuncts: @@ -433,7 +443,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, disjunct, transBlock, vars_to_disaggregate[disjunct], - local_vars_by_disjunct.get(disjunct, []), + local_vars[disjunct], parent_local_var_list, local_vars_by_disjunct[parent_disjunct] ) @@ -578,12 +588,6 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, ) for var in local_vars: - if var in vars_to_disaggregate: - logger.warning( - "Var '%s' was declared as a local Var for Disjunct '%s', " - "but it appeared in multiple Disjuncts, so it will be " - "disaggregated." % (var.name, obj.name)) - continue # we don't need to disaggregate, i.e., we can use this Var, but we # do need to set up its bounds constraints. diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index cfd0feac2b2..436367b3a89 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -406,7 +406,7 @@ def test_error_for_or(self): self.assertRaisesRegex( GDP_Error, "Cannot do hull reformulation for Disjunction " - "'disjunction' with OR constraint. Must be an XOR!*", + "'disjunction' with OR constraint. Must be an XOR!*", TransformationFactory('gdp.hull').apply_to, m, ) @@ -1572,25 +1572,18 @@ def test_transformed_model_nestedDisjuncts(self): all_cons = list(m.component_data_objects(Constraint, active=True, descend_into=Block)) num_cons = len(all_cons) - # TODO: I shouldn't have d1.binary_indicator_var in the local list - # above, but I think if I do it should be ignored when it doesn't appear - # in any Disjuncts... - - # TODO: We get duplicate bounds constraints for inner disaggregated Vars - # because we declare bounds constraints for local vars every time. We - # should actually track them separately so that we don't duplicate - # bounds constraints over and over again. + for idx, cons in enumerate(all_cons): print(idx) print(cons.name) print(cons.expr) print("") # 2 disaggregation constraints for x 0,3 - # + 4 bounds constraints for x 6,8,9,13, These are dumb: 10,14,16 + # + 6 bounds constraints for x 6,8,9,13,14,16 These are dumb: 10,14,16 # + 2 bounds constraints for inner indicator vars 11, 12 # + 2 exactly-one constraints 1,4 # + 4 transformed constraints 2,5,7,15 - self.assertEqual(num_cons, 14) + self.assertEqual(num_cons, 16) def check_transformed_model_nestedDisjuncts(self, m, d3, d4): hull = TransformationFactory('gdp.hull') From 98e8f9c8393a3981473e3ddf511a0f637f4cc8ab Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 15 Nov 2023 15:23:06 -0700 Subject: [PATCH 0160/3044] solver refactor: sol parsing --- pyomo/repn/plugins/nl_writer.py | 2 +- pyomo/solver/IPOPT.py | 243 +++++++++++++++++++++++++---- pyomo/solver/results.py | 268 +++++++++++++++++++++----------- 3 files changed, 389 insertions(+), 124 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 6a282bdeab4..ff0af67e273 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -283,7 +283,7 @@ def __call__(self, model, filename, solver_capability, io_options): return filename, symbol_map @document_kwargs_from_configdict(CONFIG) - def write(self, model, ostream, rowstream=None, colstream=None, **options): + def write(self, model, ostream, rowstream=None, colstream=None, **options) -> NLWriterInfo: """Write a model in NL format. Returns diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 90c8a6d1bce..0c61a0117bd 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -11,17 +11,25 @@ import os import subprocess +import io +import sys +from typing import Mapping from pyomo.common import Executable -from pyomo.common.config import ConfigValue +from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.tempfiles import TempfileManager from pyomo.opt import WriterFactory +from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo from pyomo.solver.base import SolverBase from pyomo.solver.config import SolverConfig from pyomo.solver.factory import SolverFactory -from pyomo.solver.results import Results, TerminationCondition, SolutionStatus -from pyomo.solver.solution import SolutionLoaderBase +from pyomo.solver.results import Results, TerminationCondition, SolutionStatus, SolFileData, parse_sol_file +from pyomo.solver.solution import SolutionLoaderBase, SolutionLoader from pyomo.solver.util import SolverSystemError +from pyomo.common.tee import TeeStream +from pyomo.common.log import LogStream +from pyomo.core.expr.visitor import replace_expressions +from pyomo.core.expr.numvalue import value import logging @@ -51,12 +59,81 @@ def __init__( self.save_solver_io: bool = self.declare( 'save_solver_io', ConfigValue(domain=bool, default=False) ) + self.temp_dir: str = self.declare( + 'temp_dir', ConfigValue(domain=str, default=None) + ) + self.solver_output_logger = self.declare( + 'solver_output_logger', ConfigValue(default=logger) + ) + self.log_level = self.declare( + 'log_level', ConfigValue(domain=NonNegativeInt, default=logging.INFO) + ) class IPOPTSolutionLoader(SolutionLoaderBase): pass +ipopt_command_line_options = { + 'acceptable_compl_inf_tol', + 'acceptable_constr_viol_tol', + 'acceptable_dual_inf_tol', + 'acceptable_tol', + 'alpha_for_y', + 'bound_frac', + 'bound_mult_init_val', + 'bound_push', + 'bound_relax_factor', + 'compl_inf_tol', + 'constr_mult_init_max', + 'constr_viol_tol', + 'diverging_iterates_tol', + 'dual_inf_tol', + 'expect_infeasible_problem', + 'file_print_level', + 'halt_on_ampl_error', + 'hessian_approximation', + 'honor_original_bounds', + 'linear_scaling_on_demand', + 'linear_solver', + 'linear_system_scaling', + 'ma27_pivtol', + 'ma27_pivtolmax', + 'ma57_pivot_order', + 'ma57_pivtol', + 'ma57_pivtolmax', + 'max_cpu_time', + 'max_iter', + 'max_refinement_steps', + 'max_soc', + 'maxit', + 'min_refinement_steps', + 'mu_init', + 'mu_max', + 'mu_oracle', + 'mu_strategy', + 'nlp_scaling_max_gradient', + 'nlp_scaling_method', + 'obj_scaling_factor', + 'option_file_name', + 'outlev', + 'output_file', + 'pardiso_matching_strategy', + 'print_level', + 'print_options_documentation', + 'print_user_options', + 'required_infeasibility_reduction', + 'slack_bound_frac', + 'slack_bound_push', + 'tol', + 'wantsol', + 'warm_start_bound_push', + 'warm_start_init_point', + 'warm_start_mult_bound_push', + 'watchdog_shortened_iter_trigger', +} + + @SolverFactory.register('ipopt', doc='The IPOPT NLP solver (new interface)') class IPOPT(SolverBase): CONFIG = IPOPTConfig() @@ -90,6 +167,32 @@ def config(self): def config(self, val): self.config = val + def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): + f = ostream + for k, val in options.items(): + if k not in ipopt_command_line_options: + f.write(str(k) + ' ' + str(val) + '\n') + + def _create_command_line(self, basename: str, config: IPOPTConfig): + cmd = [ + str(config.executable), + basename + '.nl', + '-AMPL', + 'option_file_name=' + basename + '.opt', + ] + if 'option_file_name' in config.solver_options: + raise ValueError( + 'Use IPOPT.config.temp_dir to specify the name of the options file. ' + 'Do not use IPOPT.config.solver_options["option_file_name"].' + ) + ipopt_options = dict(config.solver_options) + if config.time_limit is not None and 'max_cpu_time' not in ipopt_options: + ipopt_options['max_cpu_time'] = config.time_limit + for k, v in ipopt_options.items(): + cmd.append(str(k) + '=' + str(v)) + + return cmd + def solve(self, model, **kwds): # Check if solver is available avail = self.available() @@ -98,7 +201,7 @@ def solve(self, model, **kwds): f'Solver {self.__class__} is not available ({avail}).' ) # Update configuration options, based on keywords passed to solve - config = self.config(kwds.pop('options', {})) + config: IPOPTConfig = self.config(kwds.pop('options', {})) config.set_value(kwds) # Get a copy of the environment to pass to the subprocess env = os.environ.copy() @@ -109,16 +212,26 @@ def solve(self, model, **kwds): ) ) # Write the model to an nl file - nl_writer = WriterFactory('nl') + nl_writer = NLWriter() # Need to add check for symbolic_solver_labels; may need to generate up # to three files for nl, row, col, if ssl == True # What we have here may or may not work with IPOPT; will find out when # we try to run it. with TempfileManager.new_context() as tempfile: - dname = tempfile.mkdtemp() - with open(os.path.join(dname, model.name + '.nl')) as nl_file, open( - os.path.join(dname, model.name + '.row') - ) as row_file, open(os.path.join(dname, model.name + '.col')) as col_file: + if config.temp_dir is None: + dname = tempfile.mkdtemp() + else: + dname = config.temp_dir + if not os.path.exists(dname): + os.mkdir(dname) + basename = os.path.join(dname, model.name) + if os.path.exists(basename + '.nl'): + raise RuntimeError(f"NL file with the same name {basename + '.nl'} already exists!") + with ( + open(basename + '.nl') as nl_file, + open(basename + '.row') as row_file, + open(basename + '.col') as col_file, + ): self.info = nl_writer.write( model, nl_file, @@ -126,32 +239,96 @@ def solve(self, model, **kwds): col_file, symbolic_solver_labels=config.symbolic_solver_labels, ) + with open(basename + '.opt') as opt_file: + self._write_options_file(ostream=opt_file, options=config.solver_options) # Call IPOPT - passing the files via the subprocess - cmd = [str(config.executable), nl_file, '-AMPL'] + cmd = self._create_command_line(basename=basename, config=config) + + # this seems silly, but we have to give the subprocess slightly longer to finish than + # ipopt if config.time_limit is not None: - config.solver_options['max_cpu_time'] = config.time_limit - for key, val in config.solver_options.items(): - cmd.append(key + '=' + val) - process = subprocess.run( - cmd, timeout=config.time_limit, env=env, universal_newlines=True + timeout = config.time_limit + min(max(1.0, 0.01 * config.time_limit), 100) + else: + timeout = None + + ostreams = [ + LogStream( + level=self.config.log_level, logger=self.config.solver_output_logger + ) + ] + if self.config.tee: + ostreams.append(sys.stdout) + with TeeStream(*ostreams) as t: + process = subprocess.run( + cmd, timeout=timeout, env=env, universal_newlines=True, stdout=t.STDOUT, stderr=t.STDERR, + ) + + if process.returncode != 0: + results = Results() + results.termination_condition = TerminationCondition.error + results.solution_status = SolutionStatus.noSolution + results.solution_loader = SolutionLoader(None, None, None, None) + else: + # TODO: Make a context manager out of this and open the file + # to pass to the results, instead of doing this thing. + with open(basename + '.sol') as sol_file: + results = self._parse_solution(sol_file, self.info) + + if config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal: + raise RuntimeError('Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.') + + results.solver_name = 'ipopt' + results.solver_version = self.version() + if config.load_solution and results.solution_status == SolutionStatus.noSolution: + raise RuntimeError( + 'A feasible solution was not found, so no solution can be loaded.' + 'Please set config.load_solution=False to bypass this error.' ) + + if config.load_solution: + results.solution_loader.load_vars() - if process.returncode != 0: - if self.config.load_solution: - raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set config.load_solution=False and check ' - 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' - ) - results = Results() - results.termination_condition = TerminationCondition.error + if results.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal}: + if config.load_solution: + results.incumbent_objective = value(self.info.objectives[0]) else: - # TODO: Make a context manager out of this and open the file - # to pass to the results, instead of doing this thing. - results = self._parse_solution(os.path.join(dname, model.name + '.sol'), self.info) - - def _parse_solution(self): - # STOPPING POINT: The suggestion here is to look at the original - # parser, which hasn't failed yet, and rework it to be ... better? - pass + results.incumbent_objective = replace_expressions( + self.info.objectives[0].expr, + substitution_map={ + id(v): val for v, val in results.solution_loader.get_primals().items() + }, + descend_into_named_expressions=True, + remove_named_expressions=True, + ) + + return results + + + def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): + suffixes_to_read = ['dual', 'ipopt_zL_out', 'ipopt_zU_out'] + res, sol_data = parse_sol_file(sol_file=instream, nl_info=nl_info, suffixes_to_read=suffixes_to_read) + + if res.solution_status == SolutionStatus.noSolution: + res.solution_loader = SolutionLoader(None, None, None, None) + else: + rc = dict() + for v in nl_info.variables: + v_id = id(v) + rc[v_id] = (v, 0) + if v_id in sol_data.var_suffixes['ipopt_zL_out']: + zl = sol_data.var_suffixes['ipopt_zL_out'][v_id][1] + if abs(zl) > abs(rc[v_id][1]): + rc[v_id] = (v, zl) + if v_id in sol_data.var_suffixes['ipopt_zU_out']: + zu = sol_data.var_suffixes['ipopt_zU_out'][v_id][1] + if abs(zu) > abs(rc[v_id][1]): + rc[v_id] = (v, zu) + + res.solution_loader = SolutionLoader( + primals=sol_data.primals, + duals=sol_data.duals, + slacks=None, + reduced_costs=rc, + ) + + return res diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 9aa2869b414..01a56d526c6 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -10,8 +10,10 @@ # ___________________________________________________________________________ import enum -from typing import Optional, Tuple +import re +from typing import Optional, Tuple, Dict, Any, Sequence, List from datetime import datetime +import io from pyomo.common.config import ( ConfigDict, @@ -21,6 +23,10 @@ In, NonNegativeFloat, ) +from pyomo.common.collections import ComponentMap +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.constraint import _ConstraintData +from pyomo.core.base.objective import _ObjectiveData from pyomo.opt.results.solution import SolutionStatus as LegacySolutionStatus from pyomo.opt.results.solver import ( TerminationCondition as LegacyTerminationCondition, @@ -28,6 +34,7 @@ ) from pyomo.solver.solution import SolutionLoaderBase from pyomo.solver.util import SolverSystemError +from pyomo.repn.plugins.nl_writer import NLWriterInfo class TerminationCondition(enum.Enum): @@ -199,10 +206,10 @@ def __init__( ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution), ) self.incumbent_objective: Optional[float] = self.declare( - 'incumbent_objective', ConfigValue(domain=float) + 'incumbent_objective', ConfigValue(domain=float, default=None) ) self.objective_bound: Optional[float] = self.declare( - 'objective_bound', ConfigValue(domain=float) + 'objective_bound', ConfigValue(domain=float, default=None) ) self.solver_name: Optional[str] = self.declare( 'solver_name', ConfigValue(domain=str) @@ -211,7 +218,7 @@ def __init__( 'solver_version', ConfigValue(domain=tuple) ) self.iteration_count: Optional[int] = self.declare( - 'iteration_count', ConfigValue(domain=NonNegativeInt) + 'iteration_count', ConfigValue(domain=NonNegativeInt, default=None) ) self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict()) @@ -227,6 +234,10 @@ def __init__( self.extra_info: ConfigDict = self.declare( 'extra_info', ConfigDict(implicit=True) ) + self.solver_message: Optional[str] = self.declare( + 'solver_message', + ConfigValue(domain=str, default=None), + ) def __str__(self): s = '' @@ -241,98 +252,175 @@ class ResultsReader: pass -def parse_sol_file(sol_file, nl_info): - # The original reader for sol files is in pyomo.opt.plugins.sol. - # Per my original complaint, it has "magic numbers" that I just don't - # know how to test. It's apparently less fragile than that in APPSI. - # NOTE: The Results object now also holds the solution loader, so we do - # not need pass in a solution like we did previously. - # nl_info is an NLWriterInfo object that has vars, cons, etc. - results = Results() - - # For backwards compatibility and general safety, we will parse all - # lines until "Options" appears. Anything before "Options" we will - # consider to be the solver message. - message = [] - for line in sol_file: +class SolFileData(object): + def __init__(self) -> None: + self.primals: Dict[int, Tuple[_GeneralVarData, float]] = dict() + self.duals: Dict[_ConstraintData, float] = dict() + self.var_suffixes: Dict[str, Dict[int, Tuple[_GeneralVarData, Any]]] = dict() + self.con_suffixes: Dict[str, Dict[_ConstraintData, Any]] = dict() + self.obj_suffixes: Dict[str, Dict[int, Tuple[_ObjectiveData, Any]]] = dict() + self.problem_suffixes: Dict[str, List[Any]] = dict() + + +def parse_sol_file(sol_file: io.TextIOBase, nl_info: NLWriterInfo, suffixes_to_read: Sequence[str]) -> Tuple[Results, SolFileData]: + suffixes_to_read = set(suffixes_to_read) + res = Results() + sol_data = SolFileData() + + fin = sol_file + # + # Some solvers (minto) do not write a message. We will assume + # all non-blank lines up the 'Options' line is the message. + msg = [] + while True: + line = fin.readline() if not line: + # EOF break line = line.strip() - if "Options" in line: + if line == 'Options': break - message.append(line) - message = '\n'.join(message) - # Once "Options" appears, we must now read the content under it. - model_objects = [] - if "Options" in line: - line = sol_file.readline() - number_of_options = int(line) - need_tolerance = False - if number_of_options > 4: # MRM: Entirely unclear why this is necessary, or if it even is - number_of_options -= 2 - need_tolerance = True - for i in range(number_of_options + 4): - line = sol_file.readline() - model_objects.append(int(line)) - if need_tolerance: # MRM: Entirely unclear why this is necessary, or if it even is - line = sol_file.readline() - model_objects.append(float(line)) - else: - raise SolverSystemError("ERROR READING `sol` FILE. No 'Options' line found.") - # Identify the total number of variables and constraints - number_of_cons = model_objects[number_of_options + 1] - number_of_vars = model_objects[number_of_options + 3] - assert number_of_cons == len(nl_info.constraints) - assert number_of_vars == len(nl_info.variables) - - duals = [float(sol_file.readline()) for i in range(number_of_cons)] - variable_vals = [float(sol_file.readline()) for i in range(number_of_vars)] - - # Parse the exit code line and capture it - exit_code = [0, 0] - line = sol_file.readline() - if line and ('objno' in line): - exit_code_line = line.split() - if (len(exit_code_line) != 3): - raise SolverSystemError(f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}.") - exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] + if line: + msg.append(line) + msg = '\n'.join(msg) + z = [] + if line[:7] == "Options": + line = fin.readline() + nopts = int(line) + need_vbtol = False + if nopts > 4: # WEH - when is this true? + nopts -= 2 + need_vbtol = True + for i in range(nopts + 4): + line = fin.readline() + z += [int(line)] + if need_vbtol: # WEH - when is this true? + line = fin.readline() + z += [float(line)] else: - raise SolverSystemError(f"ERROR READING `sol` FILE. Expected `objno`; received {line}.") - results.extra_info.solver_message = message.strip().replace('\n', '; ') - # Not sure if next two lines are needed - # if isinstance(res.solver.message, str): - # res.solver.message = res.solver.message.replace(':', '\\x3a') - if (exit_code[1] >= 0) and (exit_code[1] <= 99): - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied - results.solution_status = SolutionStatus.optimal - elif (exit_code[1] >= 100) and (exit_code[1] <= 199): - exit_code_message = "Optimal solution indicated, but ERROR LIKELY!" - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied - results.solution_status = SolutionStatus.optimal - elif (exit_code[1] >= 200) and (exit_code[1] <= 299): - exit_code_message = "INFEASIBLE SOLUTION: constraints cannot be satisfied!" - results.termination_condition = TerminationCondition.locallyInfeasible - results.solution_status = SolutionStatus.infeasible - elif (exit_code[1] >= 300) and (exit_code[1] <= 399): - exit_code_message = "UNBOUNDED PROBLEM: the objective can be improved without limit!" - results.termination_condition = TerminationCondition.unbounded - results.solution_status = SolutionStatus.infeasible - elif (exit_code[1] >= 400) and (exit_code[1] <= 499): - exit_code_message = ("EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " - "was stopped by a limit that you set!") - results.solver.termination_condition = TerminationCondition.iterationLimit - elif (exit_code[1] >= 500) and (exit_code[1] <= 599): - exit_code_message = ( - "FAILURE: the solver stopped by an error condition " - "in the solver routines!" - ) - results.solver.termination_condition = TerminationCondition.error + raise ValueError("no Options line found") + n = z[nopts + 3] # variables + m = z[nopts + 1] # constraints + x = [] + y = [] + i = 0 + while i < m: + line = fin.readline() + y.append(float(line)) + i += 1 + i = 0 + while i < n: + line = fin.readline() + x.append(float(line)) + i += 1 + objno = [0, 0] + line = fin.readline() + if line: # WEH - when is this true? + if line[:5] != "objno": # pragma:nocover + raise ValueError("expected 'objno', found '%s'" % (line)) + t = line.split() + if len(t) != 3: + raise ValueError( + "expected two numbers in objno line, but found '%s'" % (line) + ) + objno = [int(t[1]), int(t[2])] + res.solver_message = msg.strip().replace("\n", "; ") + res.solution_status = SolutionStatus.noSolution + res.termination_condition = TerminationCondition.unknown + if (objno[1] >= 0) and (objno[1] <= 99): + res.solution_status = SolutionStatus.optimal + res.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + elif (objno[1] >= 100) and (objno[1] <= 199): + res.solution_status = SolutionStatus.feasible + res.termination_condition = TerminationCondition.error + elif (objno[1] >= 200) and (objno[1] <= 299): + res.solution_status = SolutionStatus.infeasible + # TODO: this is solver dependent + res.termination_condition = TerminationCondition.locallyInfeasible + elif (objno[1] >= 300) and (objno[1] <= 399): + res.solution_status = SolutionStatus.noSolution + res.termination_condition = TerminationCondition.unbounded + elif (objno[1] >= 400) and (objno[1] <= 499): + # TODO: this is solver dependent + res.solution_status = SolutionStatus.infeasible + res.termination_condition = TerminationCondition.iterationLimit + elif (objno[1] >= 500) and (objno[1] <= 599): + res.solution_status = SolutionStatus.noSolution + res.termination_condition = TerminationCondition.error + if res.solution_status != SolutionStatus.noSolution: + for v, val in zip(nl_info.variables, x): + sol_data[id(v)] = (v, val) + if "dual" in suffixes_to_read: + for c, val in zip(nl_info.constraints, y): + sol_data[c] = val + ### Read suffixes ### + line = fin.readline() + while line: + line = line.strip() + if line == "": + continue + line = line.split() + # Some sort of garbage we tag onto the solver message, assuming we are past the suffixes + if line[0] != 'suffix': + # We assume this is the start of a + # section like kestrel_option, which + # comes after all suffixes. + remaining = "" + line = fin.readline() + while line: + remaining += line.strip() + "; " + line = fin.readline() + res.solver_message += remaining + break + unmasked_kind = int(line[1]) + kind = unmasked_kind & 3 # 0-var, 1-con, 2-obj, 3-prob + convert_function = int + if (unmasked_kind & 4) == 4: + convert_function = float + nvalues = int(line[2]) + # namelen = int(line[3]) + # tablen = int(line[4]) + tabline = int(line[5]) + suffix_name = fin.readline().strip() + if suffix_name in suffixes_to_read: + # ignore translation of the table number to string value for now, + # this information can be obtained from the solver documentation + for n in range(tabline): + fin.readline() + if kind == 0: # Var + sol_data.var_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = fin.readline().split() + var_ndx = int(suf_line[0]) + var = nl_info.variables[var_ndx] + sol_data.var_suffixes[suffix_name][id(var)] = (var, convert_function(suf_line[1])) + elif kind == 1: # Con + sol_data.con_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = fin.readline().split() + con_ndx = int(suf_line[0]) + con = nl_info.constraints[con_ndx] + sol_data.con_suffixes[suffix_name][con] = convert_function(suf_line[1]) + elif kind == 2: # Obj + sol_data.obj_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = fin.readline().split() + obj_ndx = int(suf_line[0]) + obj = nl_info.objectives[obj_ndx] + sol_data.obj_suffixes[suffix_name][id(obj)] = (obj, convert_function(suf_line[1])) + elif kind == 3: # Prob + sol_data.problem_suffixes[suffix_name] = list() + for cnt in range(nvalues): + suf_line = fin.readline().split() + sol_data.problem_suffixes[suffix_name].append(convert_function(suf_line[1])) + else: + # do not store the suffix in the solution object + for cnt in range(nvalues): + fin.readline() + line = fin.readline() + + return res, sol_data - if results.extra_info.solver_message: - results.extra_info.solver_message += '; ' + exit_code_message - else: - results.extra_info.solver_message = exit_code_message - return results def parse_yaml(): pass From 0ee1d7bdb227147c6c156c69c1bbe3e9a74025af Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 15 Nov 2023 16:54:26 -0700 Subject: [PATCH 0161/3044] solver refactor: sol parsing --- pyomo/repn/plugins/nl_writer.py | 6 ++--- pyomo/solver/IPOPT.py | 39 +++++++++++++++++++-------------- pyomo/solver/results.py | 6 ++--- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index ff0af67e273..aec6bc036ab 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1346,9 +1346,9 @@ def write(self, model): # Generate the return information info = NLWriterInfo( - variables, - constraints, - objectives, + [i[0] for i in variables], + [i[0] for i in constraints], + [i[0] for i in objectives], sorted(amplfunc_libraries), row_labels, col_labels, diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 0c61a0117bd..f9eded4a62b 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -30,6 +30,7 @@ from pyomo.common.log import LogStream from pyomo.core.expr.visitor import replace_expressions from pyomo.core.expr.numvalue import value +from pyomo.core.base.suffix import Suffix import logging @@ -139,7 +140,7 @@ class IPOPT(SolverBase): CONFIG = IPOPTConfig() def __init__(self, **kwds): - self.config = self.CONFIG(kwds) + self._config = self.CONFIG(kwds) def available(self): if self.config.executable.path() is None: @@ -161,11 +162,11 @@ def version(self): @property def config(self): - return self.config + return self._config @config.setter def config(self, val): - self.config = val + self._config = val def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): f = ostream @@ -228,9 +229,9 @@ def solve(self, model, **kwds): if os.path.exists(basename + '.nl'): raise RuntimeError(f"NL file with the same name {basename + '.nl'} already exists!") with ( - open(basename + '.nl') as nl_file, - open(basename + '.row') as row_file, - open(basename + '.col') as col_file, + open(basename + '.nl', 'w') as nl_file, + open(basename + '.row', 'w') as row_file, + open(basename + '.col', 'w') as col_file, ): self.info = nl_writer.write( model, @@ -239,7 +240,7 @@ def solve(self, model, **kwds): col_file, symbolic_solver_labels=config.symbolic_solver_labels, ) - with open(basename + '.opt') as opt_file: + with open(basename + '.opt', 'w') as opt_file: self._write_options_file(ostream=opt_file, options=config.solver_options) # Call IPOPT - passing the files via the subprocess cmd = self._create_command_line(basename=basename, config=config) @@ -263,16 +264,16 @@ def solve(self, model, **kwds): cmd, timeout=timeout, env=env, universal_newlines=True, stdout=t.STDOUT, stderr=t.STDERR, ) - if process.returncode != 0: - results = Results() - results.termination_condition = TerminationCondition.error - results.solution_status = SolutionStatus.noSolution - results.solution_loader = SolutionLoader(None, None, None, None) - else: - # TODO: Make a context manager out of this and open the file - # to pass to the results, instead of doing this thing. - with open(basename + '.sol') as sol_file: - results = self._parse_solution(sol_file, self.info) + if process.returncode != 0: + results = Results() + results.termination_condition = TerminationCondition.error + results.solution_status = SolutionStatus.noSolution + results.solution_loader = SolutionLoader(None, None, None, None) + else: + # TODO: Make a context manager out of this and open the file + # to pass to the results, instead of doing this thing. + with open(basename + '.sol', 'r') as sol_file: + results = self._parse_solution(sol_file, self.info) if config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal: raise RuntimeError('Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.') @@ -287,6 +288,10 @@ def solve(self, model, **kwds): if config.load_solution: results.solution_loader.load_vars() + if hasattr(model, 'dual') and isinstance(model.dual, Suffix) and model.dual.import_enabled(): + model.dual.update(results.solution_loader.get_duals()) + if hasattr(model, 'rc') and isinstance(model.rc, Suffix) and model.rc.import_enabled(): + model.rc.update(results.solution_loader.get_reduced_costs()) if results.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal}: if config.load_solution: diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 01a56d526c6..17397b9aba0 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -112,7 +112,7 @@ class TerminationCondition(enum.Enum): unknown = 42 -class SolutionStatus(enum.IntEnum): +class SolutionStatus(enum.Enum): """ An enumeration for interpreting the result of a termination. This describes the designated status by the solver to be loaded back into the model. @@ -349,10 +349,10 @@ def parse_sol_file(sol_file: io.TextIOBase, nl_info: NLWriterInfo, suffixes_to_r res.termination_condition = TerminationCondition.error if res.solution_status != SolutionStatus.noSolution: for v, val in zip(nl_info.variables, x): - sol_data[id(v)] = (v, val) + sol_data.primals[id(v)] = (v, val) if "dual" in suffixes_to_read: for c, val in zip(nl_info.constraints, y): - sol_data[c] = val + sol_data.duals[c] = val ### Read suffixes ### line = fin.readline() while line: From 97352bd197405174d35cc007e9c1afb0ab4e2496 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 07:56:10 -0700 Subject: [PATCH 0162/3044] Merge Michael's changes; apply black --- pyomo/common/formatting.py | 3 +- pyomo/contrib/appsi/solvers/highs.py | 1 - .../solvers/tests/test_persistent_solvers.py | 4 +- pyomo/repn/plugins/nl_writer.py | 4 +- pyomo/solver/IPOPT.py | 73 ++++++++++++++----- pyomo/solver/config.py | 5 +- pyomo/solver/results.py | 25 +++++-- pyomo/solver/solution.py | 21 ++++++ 8 files changed, 104 insertions(+), 32 deletions(-) diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index 5c2b329ce21..f76d16880df 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -257,8 +257,7 @@ def writelines(self, sequence): r'|(?:\[\s*[A-Za-z0-9\.]+\s*\] +)' # [PASS]|[FAIL]|[ OK ] ) _verbatim_line_start = re.compile( - r'(\| )' # line blocks - r'|(\+((-{3,})|(={3,}))\+)' # grid table + r'(\| )' r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table ) _verbatim_line = re.compile( r'(={3,}[ =]+)' # simple tables, ======== sections diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 3d2104cdbfa..b270e4f2700 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -343,7 +343,6 @@ def set_instance(self, model): f'({self.available()}).' ) - ostreams = [ LogStream( level=self.config.log_level, logger=self.config.solver_output_logger diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 299a5bd5b7e..b50a072abbd 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1335,7 +1335,9 @@ def test_bug_1( self.assertAlmostEqual(res.incumbent_objective, 3) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_bug_2(self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars): + def test_bug_2( + self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + ): """ This test is for a bug where an objective containing a fixed variable does not get updated properly when the variable is unfixed. diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index aec6bc036ab..e745fabba33 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -283,7 +283,9 @@ def __call__(self, model, filename, solver_capability, io_options): return filename, symbol_map @document_kwargs_from_configdict(CONFIG) - def write(self, model, ostream, rowstream=None, colstream=None, **options) -> NLWriterInfo: + def write( + self, model, ostream, rowstream=None, colstream=None, **options + ) -> NLWriterInfo: """Write a model in NL format. Returns diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index f9eded4a62b..24896e626a5 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -23,7 +23,13 @@ from pyomo.solver.base import SolverBase from pyomo.solver.config import SolverConfig from pyomo.solver.factory import SolverFactory -from pyomo.solver.results import Results, TerminationCondition, SolutionStatus, SolFileData, parse_sol_file +from pyomo.solver.results import ( + Results, + TerminationCondition, + SolutionStatus, + SolFileData, + parse_sol_file, +) from pyomo.solver.solution import SolutionLoaderBase, SolutionLoader from pyomo.solver.util import SolverSystemError from pyomo.common.tee import TeeStream @@ -65,10 +71,10 @@ def __init__( ) self.solver_output_logger = self.declare( 'solver_output_logger', ConfigValue(default=logger) - ) + ) self.log_level = self.declare( 'log_level', ConfigValue(domain=NonNegativeInt, default=logging.INFO) - ) + ) class IPOPTSolutionLoader(SolutionLoaderBase): @@ -227,10 +233,12 @@ def solve(self, model, **kwds): os.mkdir(dname) basename = os.path.join(dname, model.name) if os.path.exists(basename + '.nl'): - raise RuntimeError(f"NL file with the same name {basename + '.nl'} already exists!") + raise RuntimeError( + f"NL file with the same name {basename + '.nl'} already exists!" + ) with ( - open(basename + '.nl', 'w') as nl_file, - open(basename + '.row', 'w') as row_file, + open(basename + '.nl', 'w') as nl_file, + open(basename + '.row', 'w') as row_file, open(basename + '.col', 'w') as col_file, ): self.info = nl_writer.write( @@ -241,14 +249,18 @@ def solve(self, model, **kwds): symbolic_solver_labels=config.symbolic_solver_labels, ) with open(basename + '.opt', 'w') as opt_file: - self._write_options_file(ostream=opt_file, options=config.solver_options) + self._write_options_file( + ostream=opt_file, options=config.solver_options + ) # Call IPOPT - passing the files via the subprocess cmd = self._create_command_line(basename=basename, config=config) # this seems silly, but we have to give the subprocess slightly longer to finish than # ipopt if config.time_limit is not None: - timeout = config.time_limit + min(max(1.0, 0.01 * config.time_limit), 100) + timeout = config.time_limit + min( + max(1.0, 0.01 * config.time_limit), 100 + ) else: timeout = None @@ -261,7 +273,12 @@ def solve(self, model, **kwds): ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: process = subprocess.run( - cmd, timeout=timeout, env=env, universal_newlines=True, stdout=t.STDOUT, stderr=t.STDERR, + cmd, + timeout=timeout, + env=env, + universal_newlines=True, + stdout=t.STDOUT, + stderr=t.STDERR, ) if process.returncode != 0: @@ -274,23 +291,39 @@ def solve(self, model, **kwds): # to pass to the results, instead of doing this thing. with open(basename + '.sol', 'r') as sol_file: results = self._parse_solution(sol_file, self.info) - - if config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal: - raise RuntimeError('Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.') + + if ( + config.raise_exception_on_nonoptimal_result + and results.solution_status != SolutionStatus.optimal + ): + raise RuntimeError( + 'Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' + ) results.solver_name = 'ipopt' results.solver_version = self.version() - if config.load_solution and results.solution_status == SolutionStatus.noSolution: + if ( + config.load_solution + and results.solution_status == SolutionStatus.noSolution + ): raise RuntimeError( 'A feasible solution was not found, so no solution can be loaded.' 'Please set config.load_solution=False to bypass this error.' ) - + if config.load_solution: results.solution_loader.load_vars() - if hasattr(model, 'dual') and isinstance(model.dual, Suffix) and model.dual.import_enabled(): + if ( + hasattr(model, 'dual') + and isinstance(model.dual, Suffix) + and model.dual.import_enabled() + ): model.dual.update(results.solution_loader.get_duals()) - if hasattr(model, 'rc') and isinstance(model.rc, Suffix) and model.rc.import_enabled(): + if ( + hasattr(model, 'rc') + and isinstance(model.rc, Suffix) + and model.rc.import_enabled() + ): model.rc.update(results.solution_loader.get_reduced_costs()) if results.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal}: @@ -300,7 +333,8 @@ def solve(self, model, **kwds): results.incumbent_objective = replace_expressions( self.info.objectives[0].expr, substitution_map={ - id(v): val for v, val in results.solution_loader.get_primals().items() + id(v): val + for v, val in results.solution_loader.get_primals().items() }, descend_into_named_expressions=True, remove_named_expressions=True, @@ -308,10 +342,11 @@ def solve(self, model, **kwds): return results - def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): suffixes_to_read = ['dual', 'ipopt_zL_out', 'ipopt_zU_out'] - res, sol_data = parse_sol_file(sol_file=instream, nl_info=nl_info, suffixes_to_read=suffixes_to_read) + res, sol_data = parse_sol_file( + sol_file=instream, nl_info=nl_info, suffixes_to_read=suffixes_to_read + ) if res.solution_status == SolutionStatus.noSolution: res.solution_loader = SolutionLoader(None, None, None, None) diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index 3f4424a8806..551f59ccd9a 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -61,7 +61,10 @@ def __init__( self.load_solution: bool = self.declare( 'load_solution', ConfigValue(domain=bool, default=True) ) - self.raise_exception_on_nonoptimal_result: bool = self.declare('raise_exception_on_nonoptimal_result', ConfigValue(domain=bool, default=True)) + self.raise_exception_on_nonoptimal_result: bool = self.declare( + 'raise_exception_on_nonoptimal_result', + ConfigValue(domain=bool, default=True), + ) self.symbolic_solver_labels: bool = self.declare( 'symbolic_solver_labels', ConfigValue(domain=bool, default=False) ) diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 17397b9aba0..cda8b68f715 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -235,8 +235,7 @@ def __init__( 'extra_info', ConfigDict(implicit=True) ) self.solver_message: Optional[str] = self.declare( - 'solver_message', - ConfigValue(domain=str, default=None), + 'solver_message', ConfigValue(domain=str, default=None) ) def __str__(self): @@ -262,7 +261,9 @@ def __init__(self) -> None: self.problem_suffixes: Dict[str, List[Any]] = dict() -def parse_sol_file(sol_file: io.TextIOBase, nl_info: NLWriterInfo, suffixes_to_read: Sequence[str]) -> Tuple[Results, SolFileData]: +def parse_sol_file( + sol_file: io.TextIOBase, nl_info: NLWriterInfo, suffixes_to_read: Sequence[str] +) -> Tuple[Results, SolFileData]: suffixes_to_read = set(suffixes_to_read) res = Results() sol_data = SolFileData() @@ -393,26 +394,36 @@ def parse_sol_file(sol_file: io.TextIOBase, nl_info: NLWriterInfo, suffixes_to_r suf_line = fin.readline().split() var_ndx = int(suf_line[0]) var = nl_info.variables[var_ndx] - sol_data.var_suffixes[suffix_name][id(var)] = (var, convert_function(suf_line[1])) + sol_data.var_suffixes[suffix_name][id(var)] = ( + var, + convert_function(suf_line[1]), + ) elif kind == 1: # Con sol_data.con_suffixes[suffix_name] = dict() for cnt in range(nvalues): suf_line = fin.readline().split() con_ndx = int(suf_line[0]) con = nl_info.constraints[con_ndx] - sol_data.con_suffixes[suffix_name][con] = convert_function(suf_line[1]) + sol_data.con_suffixes[suffix_name][con] = convert_function( + suf_line[1] + ) elif kind == 2: # Obj sol_data.obj_suffixes[suffix_name] = dict() for cnt in range(nvalues): suf_line = fin.readline().split() obj_ndx = int(suf_line[0]) obj = nl_info.objectives[obj_ndx] - sol_data.obj_suffixes[suffix_name][id(obj)] = (obj, convert_function(suf_line[1])) + sol_data.obj_suffixes[suffix_name][id(obj)] = ( + obj, + convert_function(suf_line[1]), + ) elif kind == 3: # Prob sol_data.problem_suffixes[suffix_name] = list() for cnt in range(nvalues): suf_line = fin.readline().split() - sol_data.problem_suffixes[suffix_name].append(convert_function(suf_line[1])) + sol_data.problem_suffixes[suffix_name].append( + convert_function(suf_line[1]) + ) else: # do not store the suffix in the solution object for cnt in range(nvalues): diff --git a/pyomo/solver/solution.py b/pyomo/solver/solution.py index 6c4b7431746..068677ea580 100644 --- a/pyomo/solver/solution.py +++ b/pyomo/solver/solution.py @@ -17,6 +17,27 @@ from pyomo.common.collections import ComponentMap from pyomo.core.staleflag import StaleFlagManager +# CHANGES: +# - `load` method: should just load the whole thing back into the model; load_solution = True +# - `load_variables` +# - `get_variables` +# - `get_constraints` +# - `get_objective` +# - `get_slacks` +# - `get_reduced_costs` + +# duals is how much better you could get if you weren't constrained. +# dual value of 0 means that the constraint isn't actively constraining anything. +# high dual value means that it is costing us a lot in the objective. +# can also be called "shadow price" + +# bounds on variables are implied constraints. +# getting a dual on the bound of a variable is the reduced cost. +# IPOPT calls these the bound multipliers (normally they are reduced costs, though). ZL, ZU + +# slacks are... something that I don't understand +# but they are necessary somewhere? I guess? + class SolutionLoaderBase(abc.ABC): def load_vars( From 56e8ac84e72bbd59d57e6307ef62b6dbabe09e37 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 20 Nov 2023 11:43:42 -0700 Subject: [PATCH 0163/3044] working on ginac interface for simplification --- .../simplification/ginac_interface.cpp | 213 ++++++++++++++++-- .../simplification/ginac_interface.hpp | 27 ++- 2 files changed, 214 insertions(+), 26 deletions(-) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index ccbc98d3586..9a84521ff91 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -1,6 +1,12 @@ #include "ginac_interface.hpp" -ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &leaf_map, PyomoExprTypes &expr_types) { +ex ginac_expr_from_pyomo_node( + py::handle expr, + std::unordered_map &leaf_map, + std::unordered_map &ginac_pyomo_map, + PyomoExprTypes &expr_types, + bool symbolic_solver_labels + ) { ex res; ExprType tmp_type = expr_types.expr_type_map[py::type::of(expr)].cast(); @@ -13,7 +19,21 @@ ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &lea case var: { long expr_id = expr_types.id(expr).cast(); if (leaf_map.count(expr_id) == 0) { - leaf_map[expr_id] = symbol("x" + std::to_string(expr_id)); + std::string vname; + if (symbolic_solver_labels) { + vname = expr.attr("name").cast(); + } + else { + vname = "x" + std::to_string(expr_id); + } + py::object lb = expr.attr("lb"); + if (lb.is_none() || lb.cast() < 0) { + leaf_map[expr_id] = realsymbol(vname); + } + else { + leaf_map[expr_id] = possymbol(vname); + } + ginac_pyomo_map[leaf_map[expr_id]] = expr.cast(); } res = leaf_map[expr_id]; break; @@ -21,67 +41,76 @@ ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &lea case param: { long expr_id = expr_types.id(expr).cast(); if (leaf_map.count(expr_id) == 0) { - leaf_map[expr_id] = symbol("p" + std::to_string(expr_id)); + std::string pname; + if (symbolic_solver_labels) { + pname = expr.attr("name").cast(); + } + else { + pname = "p" + std::to_string(expr_id); + } + leaf_map[expr_id] = realsymbol(pname); + ginac_pyomo_map[leaf_map[expr_id]] = expr.cast(); } res = leaf_map[expr_id]; break; } case product: { py::list pyomo_args = expr.attr("args"); - res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types) * ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types); + res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels) * ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); break; } case sum: { py::list pyomo_args = expr.attr("args"); for (py::handle arg : pyomo_args) { - res += ginac_expr_from_pyomo_node(arg, leaf_map, expr_types); + res += ginac_expr_from_pyomo_node(arg, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); } break; } case negation: { py::list pyomo_args = expr.attr("args"); - res = - ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types); + res = - ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); break; } case external_func: { long expr_id = expr_types.id(expr).cast(); if (leaf_map.count(expr_id) == 0) { - leaf_map[expr_id] = symbol("f" + std::to_string(expr_id)); + leaf_map[expr_id] = realsymbol("f" + std::to_string(expr_id)); + ginac_pyomo_map[leaf_map[expr_id]] = expr.cast(); } res = leaf_map[expr_id]; break; } case ExprType::power: { py::list pyomo_args = expr.attr("args"); - res = pow(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types), ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types)); + res = pow(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels), ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); break; } case division: { py::list pyomo_args = expr.attr("args"); - res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types) / ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types); + res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels) / ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); break; } case unary_func: { std::string function_name = expr.attr("getname")().cast(); py::list pyomo_args = expr.attr("args"); if (function_name == "exp") - res = exp(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = exp(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "log") - res = log(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = log(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "sin") - res = sin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = sin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "cos") - res = cos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = cos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "tan") - res = tan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = tan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "asin") - res = asin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = asin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "acos") - res = acos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = acos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "atan") - res = atan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = atan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "sqrt") - res = sqrt(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = sqrt(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else throw py::value_error("Unrecognized expression type: " + function_name); break; @@ -89,12 +118,12 @@ ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &lea case linear: { py::list pyomo_args = expr.attr("args"); for (py::handle arg : pyomo_args) { - res += ginac_expr_from_pyomo_node(arg, leaf_map, expr_types); + res += ginac_expr_from_pyomo_node(arg, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); } break; } case named_expr: { - res = ginac_expr_from_pyomo_node(expr.attr("expr"), leaf_map, expr_types); + res = ginac_expr_from_pyomo_node(expr.attr("expr"), leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); break; } case numeric_constant: { @@ -107,7 +136,7 @@ ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &lea } case unary_abs: { py::list pyomo_args = expr.attr("args"); - res = abs(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = abs(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); break; } default: { @@ -120,17 +149,151 @@ ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &lea return res; } -ex ginac_expr_from_pyomo_expr(py::handle expr, PyomoExprTypes &expr_types) { +ex pyomo_expr_to_ginac_expr( + py::handle expr, + std::unordered_map &leaf_map, + std::unordered_map &ginac_pyomo_map, + PyomoExprTypes &expr_types, + bool symbolic_solver_labels + ) { + ex res = ginac_expr_from_pyomo_node(expr, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); + return res; + } + +ex pyomo_to_ginac(py::handle expr, PyomoExprTypes &expr_types) { std::unordered_map leaf_map; - ex res = ginac_expr_from_pyomo_node(expr, leaf_map, expr_types); + std::unordered_map ginac_pyomo_map; + ex res = ginac_expr_from_pyomo_node(expr, leaf_map, ginac_pyomo_map, expr_types, true); return res; } +class GinacToPyomoVisitor +: public visitor, + public symbol::visitor, + public numeric::visitor, + public add::visitor, + public mul::visitor, + public GiNaC::power::visitor, + public function::visitor, + public basic::visitor +{ + public: + std::unordered_map *leaf_map; + std::unordered_map node_map; + PyomoExprTypes *expr_types; + + GinacToPyomoVisitor(std::unordered_map *_leaf_map, PyomoExprTypes *_expr_types) : leaf_map(_leaf_map), expr_types(_expr_types) {} + ~GinacToPyomoVisitor() = default; + + void visit(const symbol& e) { + node_map[e] = leaf_map->at(e); + } + + void visit(const numeric& e) { + double val = e.to_double(); + node_map[e] = expr_types->NumericConstant(py::cast(val)); + } + + void visit(const add& e) { + size_t n = e.nops(); + py::object pe = node_map[e.op(0)]; + for (unsigned long ndx=1; ndx < n; ++ndx) { + pe = pe.attr("__add__")(node_map[e.op(ndx)]); + } + node_map[e] = pe; + } + + void visit(const mul& e) { + size_t n = e.nops(); + py::object pe = node_map[e.op(0)]; + for (unsigned long ndx=1; ndx < n; ++ndx) { + pe = pe.attr("__mul__")(node_map[e.op(ndx)]); + } + node_map[e] = pe; + } + + void visit(const GiNaC::power& e) { + py::object arg1 = node_map[e.op(0)]; + py::object arg2 = node_map[e.op(1)]; + py::object pe = arg1.attr("__pow__")(arg2); + node_map[e] = pe; + } + + void visit(const function& e) { + py::object arg = node_map[e.op(0)]; + std::string func_type = e.get_name(); + py::object pe; + if (func_type == "exp") { + pe = expr_types->exp(arg); + } + else if (func_type == "log") { + pe = expr_types->log(arg); + } + else if (func_type == "sin") { + pe = expr_types->sin(arg); + } + else if (func_type == "cos") { + pe = expr_types->cos(arg); + } + else if (func_type == "tan") { + pe = expr_types->tan(arg); + } + else if (func_type == "asin") { + pe = expr_types->asin(arg); + } + else if (func_type == "acos") { + pe = expr_types->acos(arg); + } + else if (func_type == "atan") { + pe = expr_types->atan(arg); + } + else if (func_type == "sqrt") { + pe = expr_types->sqrt(arg); + } + else { + throw py::value_error("unrecognized unary function: " + func_type); + } + node_map[e] = pe; + } + + void visit(const basic& e) { + throw py::value_error("unrecognized ginac expression type"); + } +}; + + +ex GinacInterface::to_ginac(py::handle expr) { + return pyomo_expr_to_ginac_expr(expr, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); +} + +py::object GinacInterface::from_ginac(ex &ge) { + GinacToPyomoVisitor v(&ginac_pyomo_map, &expr_types); + ge.traverse_postorder(v); + return v.node_map[ge]; +} + PYBIND11_MODULE(ginac_interface, m) { - m.def("ginac_expr_from_pyomo_expr", &ginac_expr_from_pyomo_expr); + m.def("pyomo_to_ginac", &pyomo_to_ginac); py::class_(m, "PyomoExprTypes").def(py::init<>()); - py::class_(m, "ex"); + py::class_(m, "ginac_expression") + .def("expand", [](ex &ge) { + // exmap m; + // ex q; + // q = ge.to_polynomial(m).normal(); + // return q.subs(m); + // return factor(ge.normal()); + return ge.expand(); + }) + .def("__str__", [](ex &ge) { + std::ostringstream stream; + stream << ge; + return stream.str(); + }); + py::class_(m, "GinacInterface") + .def(py::init()) + .def("to_ginac", &GinacInterface::to_ginac) + .def("from_ginac", &GinacInterface::from_ginac); py::enum_(m, "ExprType") .value("py_float", ExprType::py_float) .value("var", ExprType::var) diff --git a/pyomo/contrib/simplification/ginac_interface.hpp b/pyomo/contrib/simplification/ginac_interface.hpp index de77e66d0c7..bc5b0d7b6fc 100644 --- a/pyomo/contrib/simplification/ginac_interface.hpp +++ b/pyomo/contrib/simplification/ginac_interface.hpp @@ -156,10 +156,35 @@ class PyomoExprTypes { py::module_::import("pyomo.dae.integral").attr("Integral"); py::object _PyomoUnit = py::module_::import("pyomo.core.base.units_container").attr("_PyomoUnit"); + py::object exp = numeric_expr.attr("exp"); + py::object log = numeric_expr.attr("log"); + py::object sin = numeric_expr.attr("sin"); + py::object cos = numeric_expr.attr("cos"); + py::object tan = numeric_expr.attr("tan"); + py::object asin = numeric_expr.attr("asin"); + py::object acos = numeric_expr.attr("acos"); + py::object atan = numeric_expr.attr("atan"); + py::object sqrt = numeric_expr.attr("sqrt"); py::object builtins = py::module_::import("builtins"); py::object id = builtins.attr("id"); py::object len = builtins.attr("len"); py::dict expr_type_map; }; -ex ginac_expr_from_pyomo_expr(py::handle expr, PyomoExprTypes &expr_types); +ex pyomo_to_ginac(py::handle expr, PyomoExprTypes &expr_types); + + +class GinacInterface { + public: + std::unordered_map leaf_map; + std::unordered_map ginac_pyomo_map; + PyomoExprTypes expr_types; + bool symbolic_solver_labels = false; + + GinacInterface() = default; + GinacInterface(bool _symbolic_solver_labels) : symbolic_solver_labels(_symbolic_solver_labels) {} + ~GinacInterface() = default; + + ex to_ginac(py::handle expr); + py::object from_ginac(ex &ginac_expr); +}; From 932d3d6a8a7a1cd95f2e227fe9f3a63849ad02a4 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 20 Nov 2023 13:07:37 -0700 Subject: [PATCH 0164/3044] ginac interface improvements --- .../simplification/ginac_interface.cpp | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index 9a84521ff91..690885dc513 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -1,5 +1,11 @@ #include "ginac_interface.hpp" + +bool is_integer(double x) { + return std::floor(x) == x; +} + + ex ginac_expr_from_pyomo_node( py::handle expr, std::unordered_map &leaf_map, @@ -13,7 +19,13 @@ ex ginac_expr_from_pyomo_node( switch (tmp_type) { case py_float: { - res = numeric(expr.cast()); + double val = expr.cast(); + if (is_integer(val)) { + res = numeric(expr.cast()); + } + else { + res = numeric(val); + } break; } case var: { @@ -278,13 +290,9 @@ PYBIND11_MODULE(ginac_interface, m) { py::class_(m, "PyomoExprTypes").def(py::init<>()); py::class_(m, "ginac_expression") .def("expand", [](ex &ge) { - // exmap m; - // ex q; - // q = ge.to_polynomial(m).normal(); - // return q.subs(m); - // return factor(ge.normal()); return ge.expand(); }) + .def("normal", &ex::normal) .def("__str__", [](ex &ge) { std::ostringstream stream; stream << ge; From 208a5dac01ca7dab7830f70119eeb0e1ba94918e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 20 Nov 2023 13:27:31 -0700 Subject: [PATCH 0165/3044] simplification interface --- pyomo/contrib/simplification/simplify.py | 35 +++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 70d5dfcd9ac..1de228fb444 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -1,6 +1,17 @@ from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression from pyomo.core.expr.numeric_expr import NumericExpression from pyomo.core.expr.numvalue import is_fixed, value +import logging +import warnings +try: + from pyomo.contrib.simplification.ginac_interface import GinacInterface + ginac_available = True +except: + GinacInterface = None + ginac_available = False + + +logger = logging.getLogger(__name__) def simplify_with_sympy(expr: NumericExpression): @@ -9,4 +20,26 @@ def simplify_with_sympy(expr: NumericExpression): new_expr = sympy2pyomo_expression(se, om) if is_fixed(new_expr): new_expr = value(new_expr) - return new_expr \ No newline at end of file + return new_expr + + +def simplify_with_ginac(expr: NumericExpression, ginac_interface): + gi = ginac_interface + return gi.from_ginac(gi.to_ginac(expr).normal()) + + +class Simplifier(object): + def __init__(self, supress_no_ginac_warnings: bool = False) -> None: + if ginac_available: + self.gi = GinacInterface() + self.suppress_no_ginac_warnings = supress_no_ginac_warnings + + def simplify(self, expr: NumericExpression): + if ginac_available: + return simplify_with_ginac(expr, self.gi) + else: + if not self.suppress_no_ginac_warnings: + msg = f"GiNaC does not seem to be available. Using SymPy. Note that the GiNac interface is significantly faster." + logger.warning(msg) + warnings.warn(msg) + return simplify_with_sympy(expr) From 6d945dad9c51736441586aca5fe52e8b21325804 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 13:27:51 -0700 Subject: [PATCH 0166/3044] Apply black --- pyomo/solver/results.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index d0ed270924c..3287cbd704b 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -291,13 +291,17 @@ def parse_sol_file( line = sol_file.readline() number_of_options = int(line) need_tolerance = False - if number_of_options > 4: # MRM: Entirely unclear why this is necessary, or if it even is + if ( + number_of_options > 4 + ): # MRM: Entirely unclear why this is necessary, or if it even is number_of_options -= 2 need_tolerance = True for i in range(number_of_options + 4): line = sol_file.readline() model_objects.append(int(line)) - if need_tolerance: # MRM: Entirely unclear why this is necessary, or if it even is + if ( + need_tolerance + ): # MRM: Entirely unclear why this is necessary, or if it even is line = sol_file.readline() model_objects.append(float(line)) else: @@ -316,11 +320,15 @@ def parse_sol_file( line = sol_file.readline() if line and ('objno' in line): exit_code_line = line.split() - if (len(exit_code_line) != 3): - raise SolverSystemError(f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}.") + if len(exit_code_line) != 3: + raise SolverSystemError( + f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}." + ) exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] else: - raise SolverSystemError(f"ERROR READING `sol` FILE. Expected `objno`; received {line}.") + raise SolverSystemError( + f"ERROR READING `sol` FILE. Expected `objno`; received {line}." + ) results.extra_info.solver_message = message.strip().replace('\n', '; ') if (exit_code[1] >= 0) and (exit_code[1] <= 99): res.solution_status = SolutionStatus.optimal @@ -336,12 +344,16 @@ def parse_sol_file( # But this was the way in the previous version - and has been fine thus far? res.termination_condition = TerminationCondition.locallyInfeasible elif (exit_code[1] >= 300) and (exit_code[1] <= 399): - exit_code_message = "UNBOUNDED PROBLEM: the objective can be improved without limit!" + exit_code_message = ( + "UNBOUNDED PROBLEM: the objective can be improved without limit!" + ) res.solution_status = SolutionStatus.noSolution res.termination_condition = TerminationCondition.unbounded elif (exit_code[1] >= 400) and (exit_code[1] <= 499): - exit_code_message = ("EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " - "was stopped by a limit that you set!") + exit_code_message = ( + "EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " + "was stopped by a limit that you set!" + ) # TODO: this is solver dependent # But this was the way in the previous version - and has been fine thus far? res.solution_status = SolutionStatus.infeasible From 2562d47d6f23f1bb85aee5affc64a49cf812356e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 13:37:26 -0700 Subject: [PATCH 0167/3044] Run black, try to fix errors --- pyomo/common/formatting.py | 3 ++- pyomo/solver/IPOPT.py | 7 +++---- pyomo/solver/results.py | 5 +---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index f76d16880df..f17fa247ad0 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -257,7 +257,8 @@ def writelines(self, sequence): r'|(?:\[\s*[A-Za-z0-9\.]+\s*\] +)' # [PASS]|[FAIL]|[ OK ] ) _verbatim_line_start = re.compile( - r'(\| )' r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table + r'(\| )' + r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table ) _verbatim_line = re.compile( r'(={3,}[ =]+)' # simple tables, ======== sections diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 1e5c1019005..5973b24b917 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -146,7 +146,7 @@ class IPOPT(SolverBase): CONFIG = IPOPTConfig() def __init__(self, **kwds): - self._config = self.CONFIG(kwds) + self.config = self.CONFIG(kwds) def available(self): if self.config.executable.path() is None: @@ -168,11 +168,11 @@ def version(self): @property def config(self): - return self._config + return self.config @config.setter def config(self, val): - self._config = val + self.config = val def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): f = ostream @@ -284,7 +284,6 @@ def solve(self, model, **kwds): if process.returncode != 0: results = Results() results.termination_condition = TerminationCondition.error - results.solution_status = SolutionStatus.noSolution results.solution_loader = SolutionLoader(None, None, None, None) else: # TODO: Make a context manager out of this and open the file diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 3287cbd704b..515735acafe 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -234,9 +234,6 @@ def __init__( self.extra_info: ConfigDict = self.declare( 'extra_info', ConfigDict(implicit=True) ) - self.solver_message: Optional[str] = self.declare( - 'solver_message', ConfigValue(domain=str, default=None) - ) def __str__(self): s = '' @@ -251,7 +248,7 @@ class ResultsReader: pass -class SolFileData(object): +class SolFileData: def __init__(self) -> None: self.primals: Dict[int, Tuple[_GeneralVarData, float]] = dict() self.duals: Dict[_ConstraintData, float] = dict() From 7c30a257dbc6578ab995488709e78c14bfa0af0d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 13:39:26 -0700 Subject: [PATCH 0168/3044] Fix IPOPT version reference in test --- pyomo/solver/tests/solvers/test_ipopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/tests/solvers/test_ipopt.py b/pyomo/solver/tests/solvers/test_ipopt.py index afe2dbbe531..8cf046fcfef 100644 --- a/pyomo/solver/tests/solvers/test_ipopt.py +++ b/pyomo/solver/tests/solvers/test_ipopt.py @@ -39,7 +39,7 @@ def test_IPOPT_config(self): self.assertIsInstance(config.executable, ExecutableData) # Test custom initialization - solver = SolverFactory('ipopt', save_solver_io=True) + solver = SolverFactory('ipopt_v2', save_solver_io=True) self.assertTrue(solver.config.save_solver_io) self.assertFalse(solver.config.tee) From 7bf0f7f5d429d3a2f3b4be65e81beefb7e6b4fca Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 13:49:45 -0700 Subject: [PATCH 0169/3044] Try to fix recursive config problem --- pyomo/solver/IPOPT.py | 6 +++--- pyomo/solver/results.py | 39 +++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 5973b24b917..d0ef744aa76 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -146,7 +146,7 @@ class IPOPT(SolverBase): CONFIG = IPOPTConfig() def __init__(self, **kwds): - self.config = self.CONFIG(kwds) + self._config = self.CONFIG(kwds) def available(self): if self.config.executable.path() is None: @@ -168,11 +168,11 @@ def version(self): @property def config(self): - return self.config + return self._config @config.setter def config(self, val): - self.config = val + self._config = val def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): f = ostream diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 515735acafe..6718f954a94 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -262,13 +262,12 @@ def parse_sol_file( sol_file: io.TextIOBase, nl_info: NLWriterInfo, suffixes_to_read: Sequence[str] ) -> Tuple[Results, SolFileData]: suffixes_to_read = set(suffixes_to_read) - res = Results() sol_data = SolFileData() # # Some solvers (minto) do not write a message. We will assume # all non-blank lines up the 'Options' line is the message. - results = Results() + result = Results() # For backwards compatibility and general safety, we will parse all # lines until "Options" appears. Anything before "Options" we will @@ -326,26 +325,26 @@ def parse_sol_file( raise SolverSystemError( f"ERROR READING `sol` FILE. Expected `objno`; received {line}." ) - results.extra_info.solver_message = message.strip().replace('\n', '; ') + result.extra_info.solver_message = message.strip().replace('\n', '; ') if (exit_code[1] >= 0) and (exit_code[1] <= 99): - res.solution_status = SolutionStatus.optimal - res.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + result.solution_status = SolutionStatus.optimal + result.termination_condition = TerminationCondition.convergenceCriteriaSatisfied elif (exit_code[1] >= 100) and (exit_code[1] <= 199): exit_code_message = "Optimal solution indicated, but ERROR LIKELY!" - res.solution_status = SolutionStatus.feasible - res.termination_condition = TerminationCondition.error + result.solution_status = SolutionStatus.feasible + result.termination_condition = TerminationCondition.error elif (exit_code[1] >= 200) and (exit_code[1] <= 299): exit_code_message = "INFEASIBLE SOLUTION: constraints cannot be satisfied!" - res.solution_status = SolutionStatus.infeasible + result.solution_status = SolutionStatus.infeasible # TODO: this is solver dependent # But this was the way in the previous version - and has been fine thus far? - res.termination_condition = TerminationCondition.locallyInfeasible + result.termination_condition = TerminationCondition.locallyInfeasible elif (exit_code[1] >= 300) and (exit_code[1] <= 399): exit_code_message = ( "UNBOUNDED PROBLEM: the objective can be improved without limit!" ) - res.solution_status = SolutionStatus.noSolution - res.termination_condition = TerminationCondition.unbounded + result.solution_status = SolutionStatus.noSolution + result.termination_condition = TerminationCondition.unbounded elif (exit_code[1] >= 400) and (exit_code[1] <= 499): exit_code_message = ( "EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " @@ -353,21 +352,21 @@ def parse_sol_file( ) # TODO: this is solver dependent # But this was the way in the previous version - and has been fine thus far? - res.solution_status = SolutionStatus.infeasible - res.termination_condition = TerminationCondition.iterationLimit + result.solution_status = SolutionStatus.infeasible + result.termination_condition = TerminationCondition.iterationLimit elif (exit_code[1] >= 500) and (exit_code[1] <= 599): exit_code_message = ( "FAILURE: the solver stopped by an error condition " "in the solver routines!" ) - res.termination_condition = TerminationCondition.error + result.termination_condition = TerminationCondition.error - if results.extra_info.solver_message: - results.extra_info.solver_message += '; ' + exit_code_message + if result.extra_info.solver_message: + result.extra_info.solver_message += '; ' + exit_code_message else: - results.extra_info.solver_message = exit_code_message + result.extra_info.solver_message = exit_code_message - if res.solution_status != SolutionStatus.noSolution: + if result.solution_status != SolutionStatus.noSolution: for v, val in zip(nl_info.variables, variable_vals): sol_data.primals[id(v)] = (v, val) if "dual" in suffixes_to_read: @@ -390,7 +389,7 @@ def parse_sol_file( while line: remaining += line.strip() + "; " line = sol_file.readline() - res.solver_message += remaining + result.solver_message += remaining break unmasked_kind = int(line[1]) kind = unmasked_kind & 3 # 0-var, 1-con, 2-obj, 3-prob @@ -449,7 +448,7 @@ def parse_sol_file( sol_file.readline() line = sol_file.readline() - return res, sol_data + return result, sol_data def parse_yaml(): From 1edc3b51715e7a734a71135f3f97798c7cd0dbf5 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 14:03:15 -0700 Subject: [PATCH 0170/3044] Correct context manage file opening --- pyomo/solver/IPOPT.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index d0ef744aa76..79c33abcd6e 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -237,9 +237,9 @@ def solve(self, model, **kwds): f"NL file with the same name {basename + '.nl'} already exists!" ) with ( - open(basename + '.nl', 'w') as nl_file, - open(basename + '.row', 'w') as row_file, - open(basename + '.col', 'w') as col_file, + open(os.path.join(basename, '.nl'), 'w') as nl_file, + open(os.path.join(basename, '.row'), 'w') as row_file, + open(os.path.join(basename, '.col'), 'w') as col_file, ): self.info = nl_writer.write( model, From f23ed71ef662ad4553fe99b4f8e8bf9c34252de7 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 14:24:05 -0700 Subject: [PATCH 0171/3044] More instances to be replaced --- pyomo/solver/IPOPT.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 79c33abcd6e..0029d638290 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -183,9 +183,9 @@ def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): def _create_command_line(self, basename: str, config: IPOPTConfig): cmd = [ str(config.executable), - basename + '.nl', + os.path.join(basename, '.nl'), '-AMPL', - 'option_file_name=' + basename + '.opt', + 'option_file_name=' + os.path.join(basename, '.opt'), ] if 'option_file_name' in config.solver_options: raise ValueError( @@ -232,10 +232,11 @@ def solve(self, model, **kwds): if not os.path.exists(dname): os.mkdir(dname) basename = os.path.join(dname, model.name) - if os.path.exists(basename + '.nl'): + if os.path.exists(os.path.join(basename, '.nl')): raise RuntimeError( f"NL file with the same name {basename + '.nl'} already exists!" ) + print(basename, os.path.join(basename, '.nl')) with ( open(os.path.join(basename, '.nl'), 'w') as nl_file, open(os.path.join(basename, '.row'), 'w') as row_file, @@ -248,7 +249,7 @@ def solve(self, model, **kwds): col_file, symbolic_solver_labels=config.symbolic_solver_labels, ) - with open(basename + '.opt', 'w') as opt_file: + with open(os.path.join(basename, '.opt'), 'w') as opt_file: self._write_options_file( ostream=opt_file, options=config.solver_options ) @@ -288,7 +289,7 @@ def solve(self, model, **kwds): else: # TODO: Make a context manager out of this and open the file # to pass to the results, instead of doing this thing. - with open(basename + '.sol', 'r') as sol_file: + with open(os.path.join(basename, '.sol'), 'r') as sol_file: results = self._parse_solution(sol_file, self.info) if ( From dcd7cab7fde48e318a02a3dcca1d7252f93f6727 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 14:26:04 -0700 Subject: [PATCH 0172/3044] Revert - previous version was fine --- pyomo/solver/IPOPT.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 0029d638290..d0ef744aa76 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -183,9 +183,9 @@ def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): def _create_command_line(self, basename: str, config: IPOPTConfig): cmd = [ str(config.executable), - os.path.join(basename, '.nl'), + basename + '.nl', '-AMPL', - 'option_file_name=' + os.path.join(basename, '.opt'), + 'option_file_name=' + basename + '.opt', ] if 'option_file_name' in config.solver_options: raise ValueError( @@ -232,15 +232,14 @@ def solve(self, model, **kwds): if not os.path.exists(dname): os.mkdir(dname) basename = os.path.join(dname, model.name) - if os.path.exists(os.path.join(basename, '.nl')): + if os.path.exists(basename + '.nl'): raise RuntimeError( f"NL file with the same name {basename + '.nl'} already exists!" ) - print(basename, os.path.join(basename, '.nl')) with ( - open(os.path.join(basename, '.nl'), 'w') as nl_file, - open(os.path.join(basename, '.row'), 'w') as row_file, - open(os.path.join(basename, '.col'), 'w') as col_file, + open(basename + '.nl', 'w') as nl_file, + open(basename + '.row', 'w') as row_file, + open(basename + '.col', 'w') as col_file, ): self.info = nl_writer.write( model, @@ -249,7 +248,7 @@ def solve(self, model, **kwds): col_file, symbolic_solver_labels=config.symbolic_solver_labels, ) - with open(os.path.join(basename, '.opt'), 'w') as opt_file: + with open(basename + '.opt', 'w') as opt_file: self._write_options_file( ostream=opt_file, options=config.solver_options ) @@ -289,7 +288,7 @@ def solve(self, model, **kwds): else: # TODO: Make a context manager out of this and open the file # to pass to the results, instead of doing this thing. - with open(os.path.join(basename, '.sol'), 'r') as sol_file: + with open(basename + '.sol', 'r') as sol_file: results = self._parse_solution(sol_file, self.info) if ( From 883c2aba24a5ff4b10a6057aa57993bd5e031095 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 14:58:18 -0700 Subject: [PATCH 0173/3044] Attempt to resolve syntax issue --- pyomo/solver/IPOPT.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index d0ef744aa76..b0749cba67b 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -236,11 +236,7 @@ def solve(self, model, **kwds): raise RuntimeError( f"NL file with the same name {basename + '.nl'} already exists!" ) - with ( - open(basename + '.nl', 'w') as nl_file, - open(basename + '.row', 'w') as row_file, - open(basename + '.col', 'w') as col_file, - ): + with open(basename + '.nl', 'w') as nl_file, open(basename + '.row', 'w') as row_file, open(basename + '.col', 'w') as col_file: self.info = nl_writer.write( model, nl_file, From 03d3aab254c5efe5b6ecf952577a0ed3ced16f66 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 20 Nov 2023 15:04:30 -0700 Subject: [PATCH 0174/3044] Apply black to context manager --- pyomo/solver/IPOPT.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index b0749cba67b..6df5914c485 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -236,7 +236,9 @@ def solve(self, model, **kwds): raise RuntimeError( f"NL file with the same name {basename + '.nl'} already exists!" ) - with open(basename + '.nl', 'w') as nl_file, open(basename + '.row', 'w') as row_file, open(basename + '.col', 'w') as col_file: + with open(basename + '.nl', 'w') as nl_file, open( + basename + '.row', 'w' + ) as row_file, open(basename + '.col', 'w') as col_file: self.info = nl_writer.write( model, nl_file, From 0e06806cd7b9515e3b641feae79a681dbf7bd1d4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 20 Nov 2023 16:41:04 -0700 Subject: [PATCH 0175/3044] Adding all_different and count_if expression nodes to the logical expression system --- pyomo/core/__init__.py | 2 + pyomo/core/expr/__init__.py | 2 + pyomo/core/expr/logical_expr.py | 73 ++++++++++++++++++- .../tests/unit/test_logical_expr_expanded.py | 46 +++++++++++- pyomo/environ/__init__.py | 2 + 5 files changed, 121 insertions(+), 4 deletions(-) diff --git a/pyomo/core/__init__.py b/pyomo/core/__init__.py index 5cbebcee9ec..b119c6357d0 100644 --- a/pyomo/core/__init__.py +++ b/pyomo/core/__init__.py @@ -33,6 +33,8 @@ exactly, atleast, atmost, + all_different, + count_if, implies, lnot, xor, diff --git a/pyomo/core/expr/__init__.py b/pyomo/core/expr/__init__.py index 5e30fceeeaa..de2228189f9 100644 --- a/pyomo/core/expr/__init__.py +++ b/pyomo/core/expr/__init__.py @@ -79,6 +79,8 @@ exactly, atleast, atmost, + all_different, + count_if, implies, ) from .numeric_expr import ( diff --git a/pyomo/core/expr/logical_expr.py b/pyomo/core/expr/logical_expr.py index e5a2f411a6e..2b261278ee9 100644 --- a/pyomo/core/expr/logical_expr.py +++ b/pyomo/core/expr/logical_expr.py @@ -10,10 +10,8 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from __future__ import division - import types -from itertools import islice +from itertools import combinations, islice import logging import traceback @@ -37,6 +35,7 @@ from .base import ExpressionBase from .boolean_value import BooleanValue, BooleanConstant from .expr_common import _and, _or, _equiv, _inv, _xor, _impl, ExpressionType +from .numeric_expr import NumericExpression import operator @@ -240,6 +239,26 @@ def atleast(n, *args): return result +def all_different(*args): + """Creates a new AllDifferentExpression + + Requires all of the arguments to take on a different value + + Usage: all_different(m.X1, m.X2, ...) + """ + return AllDifferentExpression(list(_flattened(args))) + + +def count_if(*args): + """Creates a new CountIfExpression + + Counts the number of True-valued arguments + + Usage: count_if(m.Y1, m.Y2, ...) + """ + return CountIfExpression(list(_flattened(args))) + + class UnaryBooleanExpression(BooleanExpression): """ Abstract class for single-argument logical expressions. @@ -512,4 +531,52 @@ def _apply_operation(self, result): return sum(result[1:]) >= result[0] +class AllDifferentExpression(NaryBooleanExpression): + """ + Logical expression that all of the N child statements have different values. + All arguments are expected to be discrete-valued. + """ + __slots__ = () + + PRECEDENCE = 9 # TODO: maybe? + + def getname(self, *arg, **kwd): + return 'all_different' + + def _to_string(self, values, verbose, smap): + return "all_different(%s)" % (", ".join(values)) + + def _apply_operation(self, result): + for val1, val2 in combinations(result, 2): + if val1 == val2: + return False + return True + +class CountIfExpression(NumericExpression): + """ + Logical expression that returns the number of True child statements. + All arguments are expected to be Boolean-valued. + """ + __slots__ = () + PRECEDENCE = 10 # TODO: maybe? + + def __init__(self, args): + # require a list, a la SumExpression + if args.__class__ is not list: + args = list(args) + self._args_ = args + + # NumericExpression assumes binary operator, so we have to override. + def nargs(self): + return len(self._args_) + + def getname(self, *arg, **kwd): + return 'count_if' + + def _to_string(self, values, verbose, smap): + return "count_if(%s)" % (", ".join(values)) + + def _apply_operation(self, result): + return sum(r for r in result) + special_boolean_atom_types = {ExactlyExpression, AtMostExpression, AtLeastExpression} diff --git a/pyomo/core/tests/unit/test_logical_expr_expanded.py b/pyomo/core/tests/unit/test_logical_expr_expanded.py index f5b86d59cbd..d494c13c83c 100644 --- a/pyomo/core/tests/unit/test_logical_expr_expanded.py +++ b/pyomo/core/tests/unit/test_logical_expr_expanded.py @@ -15,7 +15,7 @@ """ from __future__ import division import operator -from itertools import product +from itertools import permutations, product import pyomo.common.unittest as unittest @@ -23,6 +23,8 @@ from pyomo.core.expr.sympy_tools import sympy_available from pyomo.core.expr.visitor import identify_variables from pyomo.environ import ( + all_different, + count_if, land, atleast, atmost, @@ -39,6 +41,8 @@ BooleanVar, lnot, xor, + Var, + Integers ) @@ -234,6 +238,42 @@ def test_nary_atleast(self): ) self.assertEqual(value(atleast(ntrue, m.Y)), correct_value) + def test_nary_all_diff(self): + m = ConcreteModel() + m.x = Var(range(4), domain=Integers, bounds=(0, 3)) + for vals in permutations(range(4)): + self.assertTrue(value(all_different(*vals))) + for i, v in enumerate(vals): + m.x[i] = v + self.assertTrue(value(all_different(m.x))) + self.assertFalse(value(all_different(1, 1, 2, 3))) + m.x[0] = 1 + m.x[1] = 1 + m.x[2] = 2 + m.x[3] = 3 + self.assertFalse(value(all_different(m.x))) + + def test_count_if(self): + nargs = 3 + m = ConcreteModel() + m.s = RangeSet(nargs) + m.Y = BooleanVar(m.s) + m.x = Var(domain=Integers, bounds=(0, 3)) + for truth_combination in _generate_possible_truth_inputs(nargs): + for ntrue in range(nargs + 1): + m.Y.set_values(dict(enumerate(truth_combination, 1))) + correct_value = sum(truth_combination) + self.assertEqual( + value(count_if(*(m.Y[i] for i in m.s))), correct_value + ) + self.assertEqual(value(count_if(m.Y)), correct_value) + m.x = 2 + self.assertEqual(value(count_if([m.Y[i] for i in m.s] + [m.x == 3,])), + correct_value) + m.x = 3 + self.assertEqual(value(count_if([m.Y[i] for i in m.s] + [m.x == 3,])), + correct_value + 1) + def test_to_string(self): m = ConcreteModel() m.Y1 = BooleanVar() @@ -249,6 +289,8 @@ def test_to_string(self): self.assertEqual(str(atleast(1, m.Y1, m.Y2)), "atleast(1: [Y1, Y2])") self.assertEqual(str(atmost(1, m.Y1, m.Y2)), "atmost(1: [Y1, Y2])") self.assertEqual(str(exactly(1, m.Y1, m.Y2)), "exactly(1: [Y1, Y2])") + self.assertEqual(str(all_different(m.Y1, m.Y2)), "all_different(Y1, Y2)") + self.assertEqual(str(count_if(m.Y1, m.Y2)), "count_if(Y1, Y2)") # Precedence checks self.assertEqual(str(m.Y1.implies(m.Y2).lor(m.Y3)), "(Y1 --> Y2) ∨ Y3") @@ -271,6 +313,8 @@ def test_node_types(self): self.assertTrue(lnot(m.Y1).is_expression_type()) self.assertTrue(equivalent(m.Y1, m.Y2).is_expression_type()) self.assertTrue(atmost(1, [m.Y1, m.Y2, m.Y3]).is_expression_type()) + self.assertTrue(all_different(m.Y1, m.Y2, m.Y3).is_expression_type()) + self.assertTrue(count_if(m.Y1, m.Y2, m.Y3).is_expression_type()) def test_numeric_invalid(self): m = ConcreteModel() diff --git a/pyomo/environ/__init__.py b/pyomo/environ/__init__.py index 51c68449247..c3fb3ec4a85 100644 --- a/pyomo/environ/__init__.py +++ b/pyomo/environ/__init__.py @@ -114,6 +114,8 @@ def _import_packages(): exactly, atleast, atmost, + all_different, + count_if, implies, lnot, xor, From b3bc17bc493a5b644543847a938d94a5b19bc7ee Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 20 Nov 2023 16:44:58 -0700 Subject: [PATCH 0176/3044] black --- pyomo/core/expr/logical_expr.py | 8 ++++++-- .../tests/unit/test_logical_expr_expanded.py | 16 ++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/pyomo/core/expr/logical_expr.py b/pyomo/core/expr/logical_expr.py index 2b261278ee9..45f6cfaf7eb 100644 --- a/pyomo/core/expr/logical_expr.py +++ b/pyomo/core/expr/logical_expr.py @@ -536,9 +536,10 @@ class AllDifferentExpression(NaryBooleanExpression): Logical expression that all of the N child statements have different values. All arguments are expected to be discrete-valued. """ + __slots__ = () - PRECEDENCE = 9 # TODO: maybe? + PRECEDENCE = 9 # TODO: maybe? def getname(self, *arg, **kwd): return 'all_different' @@ -552,13 +553,15 @@ def _apply_operation(self, result): return False return True + class CountIfExpression(NumericExpression): """ Logical expression that returns the number of True child statements. All arguments are expected to be Boolean-valued. """ + __slots__ = () - PRECEDENCE = 10 # TODO: maybe? + PRECEDENCE = 10 # TODO: maybe? def __init__(self, args): # require a list, a la SumExpression @@ -579,4 +582,5 @@ def _to_string(self, values, verbose, smap): def _apply_operation(self, result): return sum(r for r in result) + special_boolean_atom_types = {ExactlyExpression, AtMostExpression, AtLeastExpression} diff --git a/pyomo/core/tests/unit/test_logical_expr_expanded.py b/pyomo/core/tests/unit/test_logical_expr_expanded.py index d494c13c83c..9e68fee441f 100644 --- a/pyomo/core/tests/unit/test_logical_expr_expanded.py +++ b/pyomo/core/tests/unit/test_logical_expr_expanded.py @@ -42,7 +42,7 @@ lnot, xor, Var, - Integers + Integers, ) @@ -263,16 +263,16 @@ def test_count_if(self): for ntrue in range(nargs + 1): m.Y.set_values(dict(enumerate(truth_combination, 1))) correct_value = sum(truth_combination) - self.assertEqual( - value(count_if(*(m.Y[i] for i in m.s))), correct_value - ) + self.assertEqual(value(count_if(*(m.Y[i] for i in m.s))), correct_value) self.assertEqual(value(count_if(m.Y)), correct_value) m.x = 2 - self.assertEqual(value(count_if([m.Y[i] for i in m.s] + [m.x == 3,])), - correct_value) + self.assertEqual( + value(count_if([m.Y[i] for i in m.s] + [m.x == 3])), correct_value + ) m.x = 3 - self.assertEqual(value(count_if([m.Y[i] for i in m.s] + [m.x == 3,])), - correct_value + 1) + self.assertEqual( + value(count_if([m.Y[i] for i in m.s] + [m.x == 3])), correct_value + 1 + ) def test_to_string(self): m = ConcreteModel() From 6ef89144c5b088574736e86553cb8c22d2dd9645 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 20 Nov 2023 21:08:25 -0700 Subject: [PATCH 0177/3044] bugs --- pyomo/contrib/simplification/__init__.py | 1 + pyomo/contrib/simplification/simplify.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/__init__.py b/pyomo/contrib/simplification/__init__.py index e69de29bb2d..c09e8b8b5e5 100644 --- a/pyomo/contrib/simplification/__init__.py +++ b/pyomo/contrib/simplification/__init__.py @@ -0,0 +1 @@ +from .simplify import Simplifier \ No newline at end of file diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 1de228fb444..938bff6b4b9 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -31,7 +31,7 @@ def simplify_with_ginac(expr: NumericExpression, ginac_interface): class Simplifier(object): def __init__(self, supress_no_ginac_warnings: bool = False) -> None: if ginac_available: - self.gi = GinacInterface() + self.gi = GinacInterface(False) self.suppress_no_ginac_warnings = supress_no_ginac_warnings def simplify(self, expr: NumericExpression): From af47ef791888b6327c295a06544c4c0771e712d9 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 21 Nov 2023 00:48:31 -0700 Subject: [PATCH 0178/3044] simplification tests --- .../contrib/simplification/tests/__init__.py | 0 .../tests/test_simplification.py | 62 +++++++++++++++++++ pyomo/core/expr/compare.py | 14 ++++- 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 pyomo/contrib/simplification/tests/__init__.py create mode 100644 pyomo/contrib/simplification/tests/test_simplification.py diff --git a/pyomo/contrib/simplification/tests/__init__.py b/pyomo/contrib/simplification/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py new file mode 100644 index 00000000000..02107ba1d6c --- /dev/null +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -0,0 +1,62 @@ +from pyomo.common.unittest import TestCase +from pyomo.contrib.simplification import Simplifier +from pyomo.core.expr.compare import assertExpressionsEqual, compare_expressions +import pyomo.environ as pe +from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd + + +class TestSimplification(TestCase): + def test_simplify(self): + m = pe.ConcreteModel() + x = m.x = pe.Var(bounds=(0, None)) + e = x*pe.log(x) + der1 = reverse_sd(e)[x] + der2 = reverse_sd(der1)[x] + simp = Simplifier() + der2_simp = simp.simplify(der2) + expected = x**-1.0 + assertExpressionsEqual(self, expected, der2_simp) + + def test_param(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + p = m.p = pe.Param(mutable=True) + e1 = p*x**2 + p*x + p*x**2 + simp = Simplifier() + e2 = simp.simplify(e1) + exp1 = p*x**2.0*2.0 + p*x + exp2 = p*x + p*x**2.0*2.0 + self.assertTrue( + compare_expressions(e2, exp1) + or compare_expressions(e2, exp2) + or compare_expressions(e2, p*x + x**2.0*p*2.0) + or compare_expressions(e2, x**2.0*p*2.0 + p*x) + ) + + def test_mul(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = 2*x + simp = Simplifier() + e2 = simp.simplify(e) + expected = 2.0*x + assertExpressionsEqual(self, expected, e2) + + def test_sum(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = 2 + x + simp = Simplifier() + e2 = simp.simplify(e) + expected = x + 2.0 + assertExpressionsEqual(self, expected, e2) + + def test_neg(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = -pe.log(x) + simp = Simplifier() + e2 = simp.simplify(e) + expected = pe.log(x)*(-1.0) + assertExpressionsEqual(self, expected, e2) + diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py index ec8d56896b8..96913f1de39 100644 --- a/pyomo/core/expr/compare.py +++ b/pyomo/core/expr/compare.py @@ -195,7 +195,19 @@ def compare_expressions(expr1, expr2, include_named_exprs=True): expr2, include_named_exprs=include_named_exprs ) try: - res = pn1 == pn2 + res = True + if len(pn1) != len(pn2): + res = False + if res: + for a, b in zip(pn1, pn2): + if a.__class__ is not b.__class__: + res = False + break + if a == b: + continue + else: + res = False + break except PyomoException: res = False return res From d750dfb3a6be955a4827c6d23c49afa11f1a5d22 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 21 Nov 2023 10:12:14 -0700 Subject: [PATCH 0179/3044] Fix minor error in exit_code_message; change to safe_dump --- pyomo/common/config.py | 7 +++++-- pyomo/solver/results.py | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 1e11fbdc431..b79f2cfad25 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -1037,8 +1037,11 @@ class will still create ``c`` instances that only have the single def _dump(*args, **kwds): + # TODO: Change the default behavior to no longer be YAML. + # This was a legacy decision that may no longer be the best + # decision, given changes to technology over the years. try: - from yaml import dump + from yaml import safe_dump as dump except ImportError: # dump = lambda x,**y: str(x) # YAML uses lowercase True/False @@ -1099,7 +1102,7 @@ def _value2string(prefix, value, obj): try: _data = value._data if value is obj else value if getattr(builtins, _data.__class__.__name__, None) is not None: - _str += _dump(_data, default_flow_style=True).rstrip() + _str += _dump(_data, default_flow_style=True, allow_unicode=True).rstrip() if _str.endswith("..."): _str = _str[:-3].rstrip() else: diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 6718f954a94..8165e6c6310 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -326,6 +326,7 @@ def parse_sol_file( f"ERROR READING `sol` FILE. Expected `objno`; received {line}." ) result.extra_info.solver_message = message.strip().replace('\n', '; ') + exit_code_message = '' if (exit_code[1] >= 0) and (exit_code[1] <= 99): result.solution_status = SolutionStatus.optimal result.termination_condition = TerminationCondition.convergenceCriteriaSatisfied @@ -362,7 +363,8 @@ def parse_sol_file( result.termination_condition = TerminationCondition.error if result.extra_info.solver_message: - result.extra_info.solver_message += '; ' + exit_code_message + if exit_code_message: + result.extra_info.solver_message += '; ' + exit_code_message else: result.extra_info.solver_message = exit_code_message From 4b624f4ea00c012d99c3436b8b944d8945f35949 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 21 Nov 2023 10:14:24 -0700 Subject: [PATCH 0180/3044] Apply black --- pyomo/common/config.py | 4 +++- pyomo/common/formatting.py | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index b79f2cfad25..d85df9f8286 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -1102,7 +1102,9 @@ def _value2string(prefix, value, obj): try: _data = value._data if value is obj else value if getattr(builtins, _data.__class__.__name__, None) is not None: - _str += _dump(_data, default_flow_style=True, allow_unicode=True).rstrip() + _str += _dump( + _data, default_flow_style=True, allow_unicode=True + ).rstrip() if _str.endswith("..."): _str = _str[:-3].rstrip() else: diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index f17fa247ad0..f76d16880df 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -257,8 +257,7 @@ def writelines(self, sequence): r'|(?:\[\s*[A-Za-z0-9\.]+\s*\] +)' # [PASS]|[FAIL]|[ OK ] ) _verbatim_line_start = re.compile( - r'(\| )' - r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table + r'(\| )' r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table ) _verbatim_line = re.compile( r'(={3,}[ =]+)' # simple tables, ======== sections From 131a1425f083de72ce9e088b3cd2dc8b4aa919f1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 21 Nov 2023 12:55:01 -0700 Subject: [PATCH 0181/3044] Remove custom SolverFactory --- pyomo/common/formatting.py | 3 +- pyomo/contrib/appsi/plugins.py | 2 +- pyomo/solver/IPOPT.py | 18 +++++++----- pyomo/solver/__init__.py | 1 - pyomo/solver/base.py | 3 +- pyomo/solver/factory.py | 33 ---------------------- pyomo/solver/plugins.py | 7 +++-- pyomo/solver/results.py | 18 ++++++++---- pyomo/solver/util.py | 51 ++++++++++++++++++++++++++++------ 9 files changed, 75 insertions(+), 61 deletions(-) delete mode 100644 pyomo/solver/factory.py diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index f76d16880df..f17fa247ad0 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -257,7 +257,8 @@ def writelines(self, sequence): r'|(?:\[\s*[A-Za-z0-9\.]+\s*\] +)' # [PASS]|[FAIL]|[ OK ] ) _verbatim_line_start = re.compile( - r'(\| )' r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table + r'(\| )' + r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table ) _verbatim_line = re.compile( r'(={3,}[ =]+)' # simple tables, ======== sections diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index 3a132b74395..a8f4390972f 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,5 +1,5 @@ from pyomo.common.extensions import ExtensionBuilderFactory -from pyomo.solver.factory import SolverFactory +from pyomo.opt.base.solvers import SolverFactory from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs from .build import AppsiBuilder diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 6df5914c485..11a6a7c4cb8 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -17,21 +17,19 @@ from pyomo.common import Executable from pyomo.common.config import ConfigValue, NonNegativeInt +from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager -from pyomo.opt import WriterFactory from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo from pyomo.solver.base import SolverBase from pyomo.solver.config import SolverConfig -from pyomo.solver.factory import SolverFactory +from pyomo.opt.base.solvers import SolverFactory from pyomo.solver.results import ( Results, TerminationCondition, SolutionStatus, - SolFileData, parse_sol_file, ) from pyomo.solver.solution import SolutionLoaderBase, SolutionLoader -from pyomo.solver.util import SolverSystemError from pyomo.common.tee import TeeStream from pyomo.common.log import LogStream from pyomo.core.expr.visitor import replace_expressions @@ -43,6 +41,14 @@ logger = logging.getLogger(__name__) +class SolverError(PyomoException): + """ + General exception to catch solver system errors + """ + + pass + + class IPOPTConfig(SolverConfig): def __init__( self, @@ -204,9 +210,7 @@ def solve(self, model, **kwds): # Check if solver is available avail = self.available() if not avail: - raise SolverSystemError( - f'Solver {self.__class__} is not available ({avail}).' - ) + raise SolverError(f'Solver {self.__class__} is not available ({avail}).') # Update configuration options, based on keywords passed to solve config: IPOPTConfig = self.config(kwds.pop('options', {})) config.set_value(kwds) diff --git a/pyomo/solver/__init__.py b/pyomo/solver/__init__.py index 1ab9f975f0b..e3eafa991cc 100644 --- a/pyomo/solver/__init__.py +++ b/pyomo/solver/__init__.py @@ -11,7 +11,6 @@ from . import base from . import config -from . import factory from . import results from . import solution from . import util diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 07f19fbb58c..8f0bd8c116f 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -76,7 +76,8 @@ def solve( timer: HierarchicalTimer An option timer for reporting timing **kwargs - Additional keyword arguments (including solver_options - passthrough options; delivered directly to the solver (with no validation)) + Additional keyword arguments (including solver_options - passthrough + options; delivered directly to the solver (with no validation)) Returns ------- diff --git a/pyomo/solver/factory.py b/pyomo/solver/factory.py deleted file mode 100644 index 84b6cf02eac..00000000000 --- a/pyomo/solver/factory.py +++ /dev/null @@ -1,33 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.opt.base import SolverFactory as LegacySolverFactory -from pyomo.common.factory import Factory -from pyomo.solver.base import LegacySolverInterface - - -class SolverFactoryClass(Factory): - def register(self, name, doc=None): - def decorator(cls): - self._cls[name] = cls - self._doc[name] = doc - - # class LegacySolver(LegacySolverInterface, cls): - # pass - - # LegacySolverFactory.register(name, doc)(LegacySolver) - - return cls - - return decorator - - -SolverFactory = SolverFactoryClass() diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index 5dfd4bce1eb..1dfcb6d2fe5 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -10,8 +10,11 @@ # ___________________________________________________________________________ -from .factory import SolverFactory +from pyomo.opt.base.solvers import SolverFactory +from .IPOPT import IPOPT def load(): - pass + SolverFactory.register(name='ipopt_v2', doc='The IPOPT NLP solver (new interface)')( + IPOPT + ) diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 8165e6c6310..404977e8a1a 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -10,7 +10,6 @@ # ___________________________________________________________________________ import enum -import re from typing import Optional, Tuple, Dict, Any, Sequence, List from datetime import datetime import io @@ -23,7 +22,7 @@ In, NonNegativeFloat, ) -from pyomo.common.collections import ComponentMap +from pyomo.common.errors import PyomoException from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _ConstraintData from pyomo.core.base.objective import _ObjectiveData @@ -33,10 +32,17 @@ SolverStatus as LegacySolverStatus, ) from pyomo.solver.solution import SolutionLoaderBase -from pyomo.solver.util import SolverSystemError from pyomo.repn.plugins.nl_writer import NLWriterInfo +class SolverResultsError(PyomoException): + """ + General exception to catch solver system errors + """ + + pass + + class TerminationCondition(enum.Enum): """ An Enum that enumerates all possible exit statuses for a solver call. @@ -301,7 +307,7 @@ def parse_sol_file( line = sol_file.readline() model_objects.append(float(line)) else: - raise SolverSystemError("ERROR READING `sol` FILE. No 'Options' line found.") + raise SolverResultsError("ERROR READING `sol` FILE. No 'Options' line found.") # Identify the total number of variables and constraints number_of_cons = model_objects[number_of_options + 1] number_of_vars = model_objects[number_of_options + 3] @@ -317,12 +323,12 @@ def parse_sol_file( if line and ('objno' in line): exit_code_line = line.split() if len(exit_code_line) != 3: - raise SolverSystemError( + raise SolverResultsError( f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}." ) exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] else: - raise SolverSystemError( + raise SolverResultsError( f"ERROR READING `sol` FILE. Expected `objno`; received {line}." ) result.extra_info.solver_message = message.strip().replace('\n', '; ') diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py index 79abee1b689..16d7c4d7cd4 100644 --- a/pyomo/solver/util.py +++ b/pyomo/solver/util.py @@ -20,18 +20,10 @@ from pyomo.core.base.param import _ParamData, Param from pyomo.core.base.objective import Objective, _GeneralObjectiveData from pyomo.common.collections import ComponentMap -from pyomo.common.errors import PyomoException from pyomo.common.timing import HierarchicalTimer from pyomo.core.expr.numvalue import NumericConstant from pyomo.solver.config import UpdateConfig - - -class SolverSystemError(PyomoException): - """ - General exception to catch solver system errors - """ - - pass +from pyomo.solver.results import TerminationCondition, SolutionStatus def get_objective(block): @@ -45,6 +37,47 @@ def get_objective(block): return obj +def check_optimal_termination(results): + """ + This function returns True if the termination condition for the solver + is 'optimal', 'locallyOptimal', or 'globallyOptimal', and the status is 'ok' + + Parameters + ---------- + results : Pyomo Results object returned from solver.solve + + Returns + ------- + `bool` + """ + if results.solution_status == SolutionStatus.optimal and ( + results.termination_condition + == TerminationCondition.convergenceCriteriaSatisfied + ): + return True + return False + + +def assert_optimal_termination(results): + """ + This function checks if the termination condition for the solver + is 'optimal', 'locallyOptimal', or 'globallyOptimal', and the status is 'ok' + and it raises a RuntimeError exception if this is not true. + + Parameters + ---------- + results : Pyomo Results object returned from solver.solve + """ + if not check_optimal_termination(results): + msg = ( + 'Solver failed to return an optimal solution. ' + 'Solution status: {}, Termination condition: {}'.format( + results.solution_status, results.termination_condition + ) + ) + raise RuntimeError(msg) + + class _VarAndNamedExprCollector(ExpressionValueVisitor): def __init__(self): self.named_expressions = {} From f261fdc146d1de7b75c13d7fbc2d8a3e97a0fe7f Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 21 Nov 2023 15:19:10 -0500 Subject: [PATCH 0182/3044] fix load_solutions bug --- pyomo/contrib/mindtpy/algorithm_base_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index d485dc4651f..4ea492bd7c9 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -864,7 +864,7 @@ def init_rNLP(self, add_oa_cuts=True): results = self.nlp_opt.solve( self.rnlp, tee=config.nlp_solver_tee, - load_solutions=config.load_solutions, + load_solutions=self.load_solutions, **nlp_args, ) if len(results.solution) > 0: From 52b8b6b0670a7742a30a3b1a92d42591b648fa06 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 21 Nov 2023 13:24:03 -0700 Subject: [PATCH 0183/3044] Revert Factory changes; clean up some nonsense --- pyomo/contrib/appsi/plugins.py | 2 +- pyomo/solver/IPOPT.py | 2 +- pyomo/solver/base.py | 1 - pyomo/solver/factory.py | 33 +++++++++++++++++++++++++++++++++ pyomo/solver/plugins.py | 2 +- 5 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 pyomo/solver/factory.py diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index a8f4390972f..3a132b74395 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,5 +1,5 @@ from pyomo.common.extensions import ExtensionBuilderFactory -from pyomo.opt.base.solvers import SolverFactory +from pyomo.solver.factory import SolverFactory from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs from .build import AppsiBuilder diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 11a6a7c4cb8..30f4cfc60a9 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -22,7 +22,7 @@ from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo from pyomo.solver.base import SolverBase from pyomo.solver.config import SolverConfig -from pyomo.opt.base.solvers import SolverFactory +from pyomo.solver.factory import SolverFactory from pyomo.solver.results import ( Results, TerminationCondition, diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 8f0bd8c116f..8c6ef0bddef 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -21,7 +21,6 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError - from pyomo.opt.results.results_ import SolverResults as LegacySolverResults from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize diff --git a/pyomo/solver/factory.py b/pyomo/solver/factory.py new file mode 100644 index 00000000000..84b6cf02eac --- /dev/null +++ b/pyomo/solver/factory.py @@ -0,0 +1,33 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.opt.base import SolverFactory as LegacySolverFactory +from pyomo.common.factory import Factory +from pyomo.solver.base import LegacySolverInterface + + +class SolverFactoryClass(Factory): + def register(self, name, doc=None): + def decorator(cls): + self._cls[name] = cls + self._doc[name] = doc + + # class LegacySolver(LegacySolverInterface, cls): + # pass + + # LegacySolverFactory.register(name, doc)(LegacySolver) + + return cls + + return decorator + + +SolverFactory = SolverFactoryClass() diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index 1dfcb6d2fe5..2f95ca9f410 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ -from pyomo.opt.base.solvers import SolverFactory +from .factory import SolverFactory from .IPOPT import IPOPT From a6802a53882df831a8e7c0c91b18f972c608d385 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 21 Nov 2023 14:58:57 -0700 Subject: [PATCH 0184/3044] Adding all_different and count_if to docplex writer, testing them, fixing a bug with evaluation of count_if --- pyomo/contrib/cp/repn/docplex_writer.py | 19 ++- pyomo/contrib/cp/tests/test_docplex_walker.py | 58 ++++++++- pyomo/contrib/cp/tests/test_docplex_writer.py | 116 ++++++++++++++++++ pyomo/core/expr/__init__.py | 2 + pyomo/core/expr/logical_expr.py | 2 +- 5 files changed, 194 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 51c3f66140e..c5b219ae9fd 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -64,7 +64,7 @@ IndexedBooleanVar, ) from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData -from pyomo.core.base.param import IndexedParam, ScalarParam +from pyomo.core.base.param import IndexedParam, ScalarParam, _ParamData from pyomo.core.base.var import ScalarVar, _GeneralVarData, IndexedVar import pyomo.core.expr as EXPR from pyomo.core.expr.visitor import StreamBasedExpressionVisitor, identify_variables @@ -805,6 +805,20 @@ def _handle_at_least_node(visitor, node, *args): ) +def _handle_all_diff_node(visitor, node, *args): + return ( + _GENERAL, + cp.all_diff(_get_int_valued_expr(arg) for arg in args), + ) + + +def _handle_count_if_node(visitor, node, *args): + return ( + _GENERAL, + cp.count((_get_bool_valued_expr(arg) for arg in args), 1), + ) + + ## CallExpression handllers @@ -932,6 +946,8 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): EXPR.ExactlyExpression: _handle_exactly_node, EXPR.AtMostExpression: _handle_at_most_node, EXPR.AtLeastExpression: _handle_at_least_node, + EXPR.AllDifferentExpression: _handle_all_diff_node, + EXPR.CountIfExpression: _handle_count_if_node, EXPR.EqualityExpression: _handle_equality_node, EXPR.NotEqualExpression: _handle_not_equal_node, EXPR.InequalityExpression: _handle_inequality_node, @@ -960,6 +976,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): ScalarExpression: _before_named_expression, IndexedParam: _before_indexed_param, # Because of indirection ScalarParam: _before_param, + _ParamData: _before_param, } def __init__(self, cpx_model, symbolic_solver_labels=False): diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 97bc538c827..f8c5f7f766f 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -21,7 +21,9 @@ from pyomo.core.base.range import NumericRange from pyomo.core.expr.numeric_expr import MinExpression, MaxExpression -from pyomo.core.expr.logical_expr import equivalent, exactly, atleast, atmost +from pyomo.core.expr.logical_expr import ( + equivalent, exactly, atleast, atmost, all_different, count_if +) from pyomo.core.expr.relational_expr import NotEqualExpression from pyomo.environ import ( @@ -401,6 +403,60 @@ def test_atmost_expression(self): expr[1].equals(cp.less_or_equal(cp.count([a[i] == 4 for i in m.I], 1), 3)) ) + def test_all_diff_expression(self): + m = self.get_model() + m.a.domain = Integers + m.a.bounds = (11, 20) + m.c = LogicalConstraint(expr=all_different(m.a)) + + visitor = self.get_visitor() + expr = visitor.walk_expression((m.c.body, m.c, 0)) + + a = {} + for i in m.I: + self.assertIn(id(m.a[i]), visitor.var_map) + a[i] = visitor.var_map[id(m.a[i])] + + self.assertTrue( + expr[1].equals(cp.all_diff(a[i] for i in m.I)) + ) + + def test_Boolean_args_in_all_diff_expression(self): + m = self.get_model() + m.a.domain = Integers + m.a.bounds = (11, 20) + m.c = LogicalConstraint(expr=all_different(m.a[1] == 13, m.b)) + + visitor = self.get_visitor() + expr = visitor.walk_expression((m.c.body, m.c, 0)) + + self.assertIn(id(m.a[1]), visitor.var_map) + a0 = visitor.var_map[id(m.a[1])] + self.assertIn(id(m.b), visitor.var_map) + b = visitor.var_map[id(m.b)] + + self.assertTrue( + expr[1].equals(cp.all_diff(a0 == 13, b)) + ) + + def test_count_if_expression(self): + m = self.get_model() + m.a.domain = Integers + m.a.bounds = (11, 20) + m.c = Constraint(expr=count_if(m.a[i] == i for i in m.I) == 5) + + visitor = self.get_visitor() + expr = visitor.walk_expression((m.c.expr, m.c, 0)) + + a = {} + for i in m.I: + self.assertIn(id(m.a[i]), visitor.var_map) + a[i] = visitor.var_map[id(m.a[i])] + + self.assertTrue( + expr[1].equals(cp.count((a[i] == i for i in m.I), 1) == 5) + ) + def test_interval_var_is_present(self): m = self.get_model() m.a.domain = Integers diff --git a/pyomo/contrib/cp/tests/test_docplex_writer.py b/pyomo/contrib/cp/tests/test_docplex_writer.py index d569ef2e696..566b4084daa 100644 --- a/pyomo/contrib/cp/tests/test_docplex_writer.py +++ b/pyomo/contrib/cp/tests/test_docplex_writer.py @@ -15,10 +15,13 @@ from pyomo.contrib.cp import IntervalVar, Pulse, Step, AlwaysIn from pyomo.contrib.cp.repn.docplex_writer import LogicalToDoCplex from pyomo.environ import ( + all_different, + count_if, ConcreteModel, Set, Var, Integers, + Param, LogicalConstraint, implies, value, @@ -254,3 +257,116 @@ def x_bounds(m, i): self.assertEqual(results.problem.sense, minimize) self.assertEqual(results.problem.lower_bound, 6) self.assertEqual(results.problem.upper_bound, 6) + + def test_matching_problem(self): + m = ConcreteModel() + + m.People = Set(initialize=['P1', 'P2', 'P3', 'P4', 'P5', 'P6', 'P7']) + m.Languages = Set(initialize=['English', 'Spanish', 'Hindi', 'Swedish']) + # People have integer names because we don't have categorical vars yet. + m.Names = Set(initialize=range(len(m.People))) + + m.Observed = Param(m.Names, m.Names, m.Languages, + initialize={ + (0, 1, 'English'): 1, + (1, 0, 'English'): 1, + (0, 2, 'English'): 1, + (2, 0, 'English'): 1, + (0, 3, 'English'): 1, + (3, 0, 'English'): 1, + (0, 4, 'English'): 1, + (4, 0, 'English'): 1, + (0, 5, 'English'): 1, + (5, 0, 'English'): 1, + (0, 6, 'English'): 1, + (6, 0, 'English'): 1, + (1, 2, 'Spanish'): 1, + (2, 1, 'Spanish'): 1, + (1, 5, 'Hindi'): 1, + (5, 1, 'Hindi'): 1, + (1, 6, 'Hindi'): 1, + (6, 1, 'Hindi'): 1, + (2, 3, 'Swedish'): 1, + (3, 2, 'Swedish'): 1, + (3, 4, 'English'): 1, + (4, 3, 'English'): 1, + }, default=0, mutable=True)# TODO: shouldn't need to + # be mutable, but waiting + # on #3045 + + m.Expected = Param(m.People, m.People, m.Languages, initialize={ + ('P1', 'P2', 'English') : 1, + ('P2', 'P1', 'English') : 1, + ('P1', 'P3', 'English') : 1, + ('P3', 'P1', 'English') : 1, + ('P1', 'P4', 'English') : 1, + ('P4', 'P1', 'English') : 1, + ('P1', 'P5', 'English') : 1, + ('P5', 'P1', 'English') : 1, + ('P1', 'P6', 'English') : 1, + ('P6', 'P1', 'English') : 1, + ('P1', 'P7', 'English') : 1, + ('P7', 'P1', 'English') : 1, + ('P2', 'P3', 'Spanish') : 1, + ('P3', 'P2', 'Spanish') : 1, + ('P2', 'P6', 'Hindi') : 1, + ('P6', 'P2', 'Hindi') : 1, + ('P2', 'P7', 'Hindi') : 1, + ('P7', 'P2', 'Hindi') : 1, + ('P3', 'P4', 'Swedish') : 1, + ('P4', 'P3', 'Swedish') : 1, + ('P4', 'P5', 'English') : 1, + ('P5', 'P4', 'English') : 1, + }, default=0, mutable=True)# TODO: shouldn't need to be mutable, but + # waiting on #3045 + + m.person_name = Var(m.People, bounds=(0, max(m.Names)), domain=Integers) + + m.one_to_one = LogicalConstraint(expr=all_different(m.person_name[person] for + person in m.People)) + + + m.obj = Objective(expr=count_if(m.Observed[m.person_name[p1], + m.person_name[p2], l] == + m.Expected[p1, p2, l] for p1 + in m.People for p2 in + m.People for l in + m.Languages), sense=maximize) + + results = SolverFactory('cp_optimizer').solve(m) + + # we can get one of two perfect matches: + perfect = 7*7*4 + self.assertEqual(results.problem.lower_bound, perfect) + self.assertEqual(results.problem.upper_bound, perfect) + self.assertEqual(results.solver.termination_condition, + TerminationCondition.optimal) + self.assertEqual(value(m.obj), perfect) + m.person_name.pprint() + self.assertEqual(value(m.person_name['P1']), 0) + self.assertEqual(value(m.person_name['P2']), 1) + self.assertEqual(value(m.person_name['P3']), 2) + self.assertEqual(value(m.person_name['P4']), 3) + self.assertEqual(value(m.person_name['P5']), 4) + # We can't distinguish P6 and P7, so they could each have either of + # names 5 and 6 + self.assertTrue(value(m.person_name['P6']) == 5 or + value(m.person_name['P6']) == 6) + self.assertTrue(value(m.person_name['P7']) == 5 or + value(m.person_name['P7']) == 6) + + m.person_name['P6'].fix(5) + m.person_name['P7'].fix(6) + + results = SolverFactory('cp_optimizer').solve(m) + self.assertEqual(results.solver.termination_condition, + TerminationCondition.optimal) + self.assertEqual(value(m.obj), perfect) + + m.person_name['P6'].fix(6) + m.person_name['P7'].fix(5) + + results = SolverFactory('cp_optimizer').solve(m) + self.assertEqual(results.solver.termination_condition, + TerminationCondition.optimal) + self.assertEqual(value(m.obj), perfect) diff --git a/pyomo/core/expr/__init__.py b/pyomo/core/expr/__init__.py index de2228189f9..bd6d1b995a1 100644 --- a/pyomo/core/expr/__init__.py +++ b/pyomo/core/expr/__init__.py @@ -70,6 +70,8 @@ ExactlyExpression, AtMostExpression, AtLeastExpression, + AllDifferentExpression, + CountIfExpression, # land, lnot, diff --git a/pyomo/core/expr/logical_expr.py b/pyomo/core/expr/logical_expr.py index 45f6cfaf7eb..31082293a71 100644 --- a/pyomo/core/expr/logical_expr.py +++ b/pyomo/core/expr/logical_expr.py @@ -580,7 +580,7 @@ def _to_string(self, values, verbose, smap): return "count_if(%s)" % (", ".join(values)) def _apply_operation(self, result): - return sum(r for r in result) + return sum(value(r) for r in result) special_boolean_atom_types = {ExactlyExpression, AtMostExpression, AtLeastExpression} From f70001b63198b6457c63e717b9b37933c999acfc Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 21 Nov 2023 14:59:32 -0700 Subject: [PATCH 0185/3044] Blackify --- pyomo/contrib/cp/repn/docplex_writer.py | 10 +- pyomo/contrib/cp/tests/test_docplex_walker.py | 19 +- pyomo/contrib/cp/tests/test_docplex_writer.py | 168 ++++++++++-------- 3 files changed, 106 insertions(+), 91 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index c5b219ae9fd..c2687662fe8 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -806,17 +806,11 @@ def _handle_at_least_node(visitor, node, *args): def _handle_all_diff_node(visitor, node, *args): - return ( - _GENERAL, - cp.all_diff(_get_int_valued_expr(arg) for arg in args), - ) + return (_GENERAL, cp.all_diff(_get_int_valued_expr(arg) for arg in args)) def _handle_count_if_node(visitor, node, *args): - return ( - _GENERAL, - cp.count((_get_bool_valued_expr(arg) for arg in args), 1), - ) + return (_GENERAL, cp.count((_get_bool_valued_expr(arg) for arg in args), 1)) ## CallExpression handllers diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index f8c5f7f766f..0f1c73cd3b1 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -22,7 +22,12 @@ from pyomo.core.base.range import NumericRange from pyomo.core.expr.numeric_expr import MinExpression, MaxExpression from pyomo.core.expr.logical_expr import ( - equivalent, exactly, atleast, atmost, all_different, count_if + equivalent, + exactly, + atleast, + atmost, + all_different, + count_if, ) from pyomo.core.expr.relational_expr import NotEqualExpression @@ -417,9 +422,7 @@ def test_all_diff_expression(self): self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] - self.assertTrue( - expr[1].equals(cp.all_diff(a[i] for i in m.I)) - ) + self.assertTrue(expr[1].equals(cp.all_diff(a[i] for i in m.I))) def test_Boolean_args_in_all_diff_expression(self): m = self.get_model() @@ -435,9 +438,7 @@ def test_Boolean_args_in_all_diff_expression(self): self.assertIn(id(m.b), visitor.var_map) b = visitor.var_map[id(m.b)] - self.assertTrue( - expr[1].equals(cp.all_diff(a0 == 13, b)) - ) + self.assertTrue(expr[1].equals(cp.all_diff(a0 == 13, b))) def test_count_if_expression(self): m = self.get_model() @@ -453,9 +454,7 @@ def test_count_if_expression(self): self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] - self.assertTrue( - expr[1].equals(cp.count((a[i] == i for i in m.I), 1) == 5) - ) + self.assertTrue(expr[1].equals(cp.count((a[i] == i for i in m.I), 1) == 5)) def test_interval_var_is_present(self): m = self.get_model() diff --git a/pyomo/contrib/cp/tests/test_docplex_writer.py b/pyomo/contrib/cp/tests/test_docplex_writer.py index 566b4084daa..b563052ef3a 100644 --- a/pyomo/contrib/cp/tests/test_docplex_writer.py +++ b/pyomo/contrib/cp/tests/test_docplex_writer.py @@ -266,81 +266,99 @@ def test_matching_problem(self): # People have integer names because we don't have categorical vars yet. m.Names = Set(initialize=range(len(m.People))) - m.Observed = Param(m.Names, m.Names, m.Languages, - initialize={ - (0, 1, 'English'): 1, - (1, 0, 'English'): 1, - (0, 2, 'English'): 1, - (2, 0, 'English'): 1, - (0, 3, 'English'): 1, - (3, 0, 'English'): 1, - (0, 4, 'English'): 1, - (4, 0, 'English'): 1, - (0, 5, 'English'): 1, - (5, 0, 'English'): 1, - (0, 6, 'English'): 1, - (6, 0, 'English'): 1, - (1, 2, 'Spanish'): 1, - (2, 1, 'Spanish'): 1, - (1, 5, 'Hindi'): 1, - (5, 1, 'Hindi'): 1, - (1, 6, 'Hindi'): 1, - (6, 1, 'Hindi'): 1, - (2, 3, 'Swedish'): 1, - (3, 2, 'Swedish'): 1, - (3, 4, 'English'): 1, - (4, 3, 'English'): 1, - }, default=0, mutable=True)# TODO: shouldn't need to - # be mutable, but waiting - # on #3045 - - m.Expected = Param(m.People, m.People, m.Languages, initialize={ - ('P1', 'P2', 'English') : 1, - ('P2', 'P1', 'English') : 1, - ('P1', 'P3', 'English') : 1, - ('P3', 'P1', 'English') : 1, - ('P1', 'P4', 'English') : 1, - ('P4', 'P1', 'English') : 1, - ('P1', 'P5', 'English') : 1, - ('P5', 'P1', 'English') : 1, - ('P1', 'P6', 'English') : 1, - ('P6', 'P1', 'English') : 1, - ('P1', 'P7', 'English') : 1, - ('P7', 'P1', 'English') : 1, - ('P2', 'P3', 'Spanish') : 1, - ('P3', 'P2', 'Spanish') : 1, - ('P2', 'P6', 'Hindi') : 1, - ('P6', 'P2', 'Hindi') : 1, - ('P2', 'P7', 'Hindi') : 1, - ('P7', 'P2', 'Hindi') : 1, - ('P3', 'P4', 'Swedish') : 1, - ('P4', 'P3', 'Swedish') : 1, - ('P4', 'P5', 'English') : 1, - ('P5', 'P4', 'English') : 1, - }, default=0, mutable=True)# TODO: shouldn't need to be mutable, but - # waiting on #3045 + m.Observed = Param( + m.Names, + m.Names, + m.Languages, + initialize={ + (0, 1, 'English'): 1, + (1, 0, 'English'): 1, + (0, 2, 'English'): 1, + (2, 0, 'English'): 1, + (0, 3, 'English'): 1, + (3, 0, 'English'): 1, + (0, 4, 'English'): 1, + (4, 0, 'English'): 1, + (0, 5, 'English'): 1, + (5, 0, 'English'): 1, + (0, 6, 'English'): 1, + (6, 0, 'English'): 1, + (1, 2, 'Spanish'): 1, + (2, 1, 'Spanish'): 1, + (1, 5, 'Hindi'): 1, + (5, 1, 'Hindi'): 1, + (1, 6, 'Hindi'): 1, + (6, 1, 'Hindi'): 1, + (2, 3, 'Swedish'): 1, + (3, 2, 'Swedish'): 1, + (3, 4, 'English'): 1, + (4, 3, 'English'): 1, + }, + default=0, + mutable=True, + ) # TODO: shouldn't need to + # be mutable, but waiting + # on #3045 + + m.Expected = Param( + m.People, + m.People, + m.Languages, + initialize={ + ('P1', 'P2', 'English'): 1, + ('P2', 'P1', 'English'): 1, + ('P1', 'P3', 'English'): 1, + ('P3', 'P1', 'English'): 1, + ('P1', 'P4', 'English'): 1, + ('P4', 'P1', 'English'): 1, + ('P1', 'P5', 'English'): 1, + ('P5', 'P1', 'English'): 1, + ('P1', 'P6', 'English'): 1, + ('P6', 'P1', 'English'): 1, + ('P1', 'P7', 'English'): 1, + ('P7', 'P1', 'English'): 1, + ('P2', 'P3', 'Spanish'): 1, + ('P3', 'P2', 'Spanish'): 1, + ('P2', 'P6', 'Hindi'): 1, + ('P6', 'P2', 'Hindi'): 1, + ('P2', 'P7', 'Hindi'): 1, + ('P7', 'P2', 'Hindi'): 1, + ('P3', 'P4', 'Swedish'): 1, + ('P4', 'P3', 'Swedish'): 1, + ('P4', 'P5', 'English'): 1, + ('P5', 'P4', 'English'): 1, + }, + default=0, + mutable=True, + ) # TODO: shouldn't need to be mutable, but + # waiting on #3045 m.person_name = Var(m.People, bounds=(0, max(m.Names)), domain=Integers) - m.one_to_one = LogicalConstraint(expr=all_different(m.person_name[person] for - person in m.People)) - + m.one_to_one = LogicalConstraint( + expr=all_different(m.person_name[person] for person in m.People) + ) - m.obj = Objective(expr=count_if(m.Observed[m.person_name[p1], - m.person_name[p2], l] == - m.Expected[p1, p2, l] for p1 - in m.People for p2 in - m.People for l in - m.Languages), sense=maximize) + m.obj = Objective( + expr=count_if( + m.Observed[m.person_name[p1], m.person_name[p2], l] + == m.Expected[p1, p2, l] + for p1 in m.People + for p2 in m.People + for l in m.Languages + ), + sense=maximize, + ) results = SolverFactory('cp_optimizer').solve(m) # we can get one of two perfect matches: - perfect = 7*7*4 + perfect = 7 * 7 * 4 self.assertEqual(results.problem.lower_bound, perfect) self.assertEqual(results.problem.upper_bound, perfect) - self.assertEqual(results.solver.termination_condition, - TerminationCondition.optimal) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) self.assertEqual(value(m.obj), perfect) m.person_name.pprint() self.assertEqual(value(m.person_name['P1']), 0) @@ -350,23 +368,27 @@ def test_matching_problem(self): self.assertEqual(value(m.person_name['P5']), 4) # We can't distinguish P6 and P7, so they could each have either of # names 5 and 6 - self.assertTrue(value(m.person_name['P6']) == 5 or - value(m.person_name['P6']) == 6) - self.assertTrue(value(m.person_name['P7']) == 5 or - value(m.person_name['P7']) == 6) + self.assertTrue( + value(m.person_name['P6']) == 5 or value(m.person_name['P6']) == 6 + ) + self.assertTrue( + value(m.person_name['P7']) == 5 or value(m.person_name['P7']) == 6 + ) m.person_name['P6'].fix(5) m.person_name['P7'].fix(6) results = SolverFactory('cp_optimizer').solve(m) - self.assertEqual(results.solver.termination_condition, - TerminationCondition.optimal) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) self.assertEqual(value(m.obj), perfect) m.person_name['P6'].fix(6) m.person_name['P7'].fix(5) results = SolverFactory('cp_optimizer').solve(m) - self.assertEqual(results.solver.termination_condition, - TerminationCondition.optimal) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) self.assertEqual(value(m.obj), perfect) From 63af991b68d2d18bbabd0e22126e59638540c34c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 21 Nov 2023 15:15:29 -0700 Subject: [PATCH 0186/3044] Push changes: pyomo --help solvers tracks all solvers, v1, v2, and appsi --- pyomo/solver/IPOPT.py | 3 ++- pyomo/solver/base.py | 10 ++++++++++ pyomo/solver/factory.py | 7 ++++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 30f4cfc60a9..74b0ae25358 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -13,7 +13,7 @@ import subprocess import io import sys -from typing import Mapping +from typing import Mapping, Dict from pyomo.common import Executable from pyomo.common.config import ConfigValue, NonNegativeInt @@ -153,6 +153,7 @@ class IPOPT(SolverBase): def __init__(self, **kwds): self._config = self.CONFIG(kwds) + self.ipopt_options = ipopt_command_line_options def available(self): if self.config.executable.path() is None: diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 8c6ef0bddef..d4b46ebe5d4 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -37,6 +37,16 @@ class SolverBase(abc.ABC): + # + # Support "with" statements. Forgetting to call deactivate + # on Plugins is a common source of memory leaks + # + def __enter__(self): + return self + + def __exit__(self, t, v, traceback): + pass + class Availability(enum.IntEnum): FullLicense = 2 LimitedLicense = 1 diff --git a/pyomo/solver/factory.py b/pyomo/solver/factory.py index 84b6cf02eac..23a66acd9cb 100644 --- a/pyomo/solver/factory.py +++ b/pyomo/solver/factory.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ + from pyomo.opt.base import SolverFactory as LegacySolverFactory from pyomo.common.factory import Factory from pyomo.solver.base import LegacySolverInterface @@ -20,10 +21,10 @@ def decorator(cls): self._cls[name] = cls self._doc[name] = doc - # class LegacySolver(LegacySolverInterface, cls): - # pass + class LegacySolver(LegacySolverInterface, cls): + pass - # LegacySolverFactory.register(name, doc)(LegacySolver) + LegacySolverFactory.register(name, doc)(LegacySolver) return cls From 5e78aa162fff7f7ca82fcd98364fb89c774cd82a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 21 Nov 2023 16:13:08 -0700 Subject: [PATCH 0187/3044] Certify backwards compatibility --- pyomo/solver/IPOPT.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 74b0ae25358..55c97687b05 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -19,8 +19,9 @@ from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager +from pyomo.core.base.label import NumericLabeler from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo -from pyomo.solver.base import SolverBase +from pyomo.solver.base import SolverBase, SymbolMap from pyomo.solver.config import SolverConfig from pyomo.solver.factory import SolverFactory from pyomo.solver.results import ( @@ -153,7 +154,8 @@ class IPOPT(SolverBase): def __init__(self, **kwds): self._config = self.CONFIG(kwds) - self.ipopt_options = ipopt_command_line_options + self._writer = NLWriter() + self.ipopt_options = self._config.solver_options def available(self): if self.config.executable.path() is None: @@ -181,6 +183,10 @@ def config(self): def config(self, val): self._config = val + @property + def symbol_map(self): + return self._symbol_map + def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): f = ostream for k, val in options.items(): @@ -199,10 +205,10 @@ def _create_command_line(self, basename: str, config: IPOPTConfig): 'Use IPOPT.config.temp_dir to specify the name of the options file. ' 'Do not use IPOPT.config.solver_options["option_file_name"].' ) - ipopt_options = dict(config.solver_options) - if config.time_limit is not None and 'max_cpu_time' not in ipopt_options: - ipopt_options['max_cpu_time'] = config.time_limit - for k, v in ipopt_options.items(): + self.ipopt_options = dict(config.solver_options) + if config.time_limit is not None and 'max_cpu_time' not in self.ipopt_options: + self.ipopt_options['max_cpu_time'] = config.time_limit + for k, v in self.ipopt_options.items(): cmd.append(str(k) + '=' + str(v)) return cmd @@ -223,8 +229,6 @@ def solve(self, model, **kwds): None, (env.get('AMPLFUNC', None), env.get('PYOMO_AMPLFUNC', None)) ) ) - # Write the model to an nl file - nl_writer = NLWriter() # Need to add check for symbolic_solver_labels; may need to generate up # to three files for nl, row, col, if ssl == True # What we have here may or may not work with IPOPT; will find out when @@ -244,13 +248,19 @@ def solve(self, model, **kwds): with open(basename + '.nl', 'w') as nl_file, open( basename + '.row', 'w' ) as row_file, open(basename + '.col', 'w') as col_file: - self.info = nl_writer.write( + self.info = self._writer.write( model, nl_file, row_file, col_file, symbolic_solver_labels=config.symbolic_solver_labels, ) + symbol_map = self._symbol_map = SymbolMap() + labeler = NumericLabeler('component') + for v in self.info.variables: + symbol_map.getSymbol(v, labeler) + for c in self.info.constraints: + symbol_map.getSymbol(c, labeler) with open(basename + '.opt', 'w') as opt_file: self._write_options_file( ostream=opt_file, options=config.solver_options From 015ebaf8593d0f21336af323ac8d0d440e17c9f4 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 27 Nov 2023 14:41:36 -0500 Subject: [PATCH 0188/3044] fix typo: change try to trying --- pyomo/contrib/mindtpy/algorithm_base_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 4ea492bd7c9..a7a8a41cd70 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -857,7 +857,7 @@ def init_rNLP(self, add_oa_cuts=True): not in self.mip_objective_polynomial_degree ): config.logger.info( - 'Initial relaxed NLP problem is infeasible. This might be related to partition_obj_nonlinear_terms. Try to solve it again without partitioning nonlinear objective function.' + 'Initial relaxed NLP problem is infeasible. This might be related to partition_obj_nonlinear_terms. Trying to solve it again without partitioning nonlinear objective function.' ) self.rnlp.MindtPy_utils.objective.deactivate() self.rnlp.MindtPy_utils.objective_list[0].activate() From 3472d0dff53ae89808adbf387fd1974de2299c90 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 27 Nov 2023 12:48:27 -0700 Subject: [PATCH 0189/3044] Commit other changes --- pyomo/solver/IPOPT.py | 2 +- pyomo/solver/results.py | 2 +- pyomo/solver/util.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 55c97687b05..90a92b0de24 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -13,7 +13,7 @@ import subprocess import io import sys -from typing import Mapping, Dict +from typing import Mapping from pyomo.common import Executable from pyomo.common.config import ConfigValue, NonNegativeInt diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 404977e8a1a..728e47fc7a1 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -397,7 +397,7 @@ def parse_sol_file( while line: remaining += line.strip() + "; " line = sol_file.readline() - result.solver_message += remaining + result.extra_info.solver_message += remaining break unmasked_kind = int(line[1]) kind = unmasked_kind & 3 # 0-var, 1-con, 2-obj, 3-prob diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py index 16d7c4d7cd4..ec59f7e80f7 100644 --- a/pyomo/solver/util.py +++ b/pyomo/solver/util.py @@ -38,6 +38,8 @@ def get_objective(block): def check_optimal_termination(results): + # TODO: Make work for legacy and new results objects. + # Look at the original version of this function to make that happen. """ This function returns True if the termination condition for the solver is 'optimal', 'locallyOptimal', or 'globallyOptimal', and the status is 'ok' From a6079d50b319cc0288ee8612bf58e2f02a88fe09 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 27 Nov 2023 15:37:57 -0500 Subject: [PATCH 0190/3044] add more details of the error in copy_var_list_values --- pyomo/contrib/mindtpy/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 48c8aab31c4..ea2136b0589 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -1010,4 +1010,5 @@ def copy_var_list_values( elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: v_to.set_value(0) else: - raise ValueError("copy_var_list_values failed.") + raise ValueError("copy_var_list_values failed with variable {}, value = {} and rounded value = {}" + "".format(v_to.name, var_val, rounded_val)) From e8b3b72df0d5c869be0a169ea5310940da342049 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 27 Nov 2023 18:26:12 -0500 Subject: [PATCH 0191/3044] create copy_var_value function --- pyomo/contrib/mindtpy/algorithm_base_class.py | 1 - pyomo/contrib/mindtpy/single_tree.py | 53 +------- pyomo/contrib/mindtpy/tests/test_mindtpy.py | 1 + pyomo/contrib/mindtpy/util.py | 121 ++++++++++-------- 4 files changed, 74 insertions(+), 102 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index a7a8a41cd70..92e1075fe90 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1853,7 +1853,6 @@ def handle_main_optimal(self, main_mip, update_bound=True): f"Integer variable {var.name} not initialized. " "Setting it to its lower bound" ) - # nlp_var.bounds[0] var.set_value(var.lb, skip_validation=True) # warm start for the nlp subproblem copy_var_list_values( diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 66435c2587f..a5d4401d623 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -16,12 +16,11 @@ from pyomo.repn import generate_standard_repn import pyomo.core.expr as EXPR from math import copysign -from pyomo.contrib.mindtpy.util import get_integer_solution, copy_var_list_values +from pyomo.contrib.mindtpy.util import get_integer_solution, copy_var_list_values, copy_var_value from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code from pyomo.opt import TerminationCondition as tc from pyomo.core import minimize, value from pyomo.core.expr import identify_variables -import math cplex, cplex_available = attempt_import('cplex') @@ -35,7 +34,6 @@ def copy_lazy_var_list_values( self, opt, from_list, to_list, config, skip_stale=False, skip_fixed=True ): """This function copies variable values from one list to another. - Rounds to Binary/Integer if necessary. Sets to zero for NonNegativeReals if necessary. @@ -44,17 +42,15 @@ def copy_lazy_var_list_values( opt : SolverFactory The cplex_persistent solver. from_list : list - The variables that provides the values to copy from. + The variable list that provides the values to copy from. to_list : list - The variables that need to set value. + The variable list that needs to set value. config : ConfigBlock The specific configurations for MindtPy. skip_stale : bool, optional Whether to skip the stale variables, by default False. skip_fixed : bool, optional Whether to skip the fixed variables, by default True. - ignore_integrality : bool, optional - Whether to ignore the integrality of integer variables, by default False. """ for v_from, v_to in zip(from_list, to_list): if skip_stale and v_from.stale: @@ -62,48 +58,7 @@ def copy_lazy_var_list_values( if skip_fixed and v_to.is_fixed(): continue # Skip fixed variables. v_val = self.get_values(opt._pyomo_var_to_solver_var_map[v_from]) - rounded_val = int(round(v_val)) - # We don't want to trigger the reset of the global stale - # indicator, so we will set this variable to be "stale", - # knowing that set_value will switch it back to "not - # stale" - v_to.stale = True - # NOTE: PEP 2180 changes the var behavior so that domain - # / bounds violations no longer generate exceptions (and - # instead log warnings). This means that the following - # will always succeed and the ValueError should never be - # raised. - if ( - v_val in v_to.domain - and not ((v_to.has_lb() and v_val < v_to.lb)) - and not ((v_to.has_ub() and v_val > v_to.ub)) - ): - v_to.set_value(v_val) - # Snap the value to the bounds - # TODO: check the performance of - # v_to.lb - v_val <= config.variable_tolerance - elif ( - v_to.has_lb() - and v_val < v_to.lb - # and v_to.lb - v_val <= config.variable_tolerance - ): - v_to.set_value(v_to.lb) - elif ( - v_to.has_ub() - and v_val > v_to.ub - # and v_val - v_to.ub <= config.variable_tolerance - ): - v_to.set_value(v_to.ub) - # ... or the nearest integer - elif ( - v_to.is_integer() - and math.fabs(v_val - rounded_val) <= config.integer_tolerance - ): # and rounded_val in v_to.domain: - v_to.set_value(rounded_val) - elif abs(v_val) <= config.zero_tolerance and 0 in v_to.domain: - v_to.set_value(0) - else: - raise ValueError('copy_lazy_var_list_values failed.') + copy_var_value(v_from, v_to, v_val, config, ignore_integrality=False) def add_lazy_oa_cuts( self, diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy.py b/pyomo/contrib/mindtpy/tests/test_mindtpy.py index e872eccc670..ae531f9bd84 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy.py @@ -327,6 +327,7 @@ def test_OA_APPSI_ipopt(self): value(model.objective.expr), model.optimal_value, places=1 ) + # CYIPOPT will raise WARNING (W1002) during loading solution. @unittest.skipUnless( SolverFactory('cyipopt').available(exception_flag=False), "APPSI_IPOPT not available.", diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index ea2136b0589..2970a805540 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -693,37 +693,7 @@ def copy_var_list_values_from_solution_pool( elif config.mip_solver == 'gurobi_persistent': solver_model.setParam(gurobipy.GRB.Param.SolutionNumber, solution_name) var_val = var_map[v_from].Xn - # We don't want to trigger the reset of the global stale - # indicator, so we will set this variable to be "stale", - # knowing that set_value will switch it back to "not - # stale" - v_to.stale = True - rounded_val = int(round(var_val)) - # NOTE: PEP 2180 changes the var behavior so that domain / - # bounds violations no longer generate exceptions (and - # instead log warnings). This means that the following will - # always succeed and the ValueError should never be raised. - if ( - var_val in v_to.domain - and not ((v_to.has_lb() and var_val < v_to.lb)) - and not ((v_to.has_ub() and var_val > v_to.ub)) - ): - v_to.set_value(var_val, skip_validation=True) - elif v_to.has_lb() and var_val < v_to.lb: - v_to.set_value(v_to.lb) - elif v_to.has_ub() and var_val > v_to.ub: - v_to.set_value(v_to.ub) - # Check to see if this is just a tolerance issue - elif ignore_integrality and v_to.is_integer(): - v_to.set_value(var_val, skip_validation=True) - elif v_to.is_integer() and ( - abs(var_val - rounded_val) <= config.integer_tolerance - ): - v_to.set_value(rounded_val, skip_validation=True) - elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: - v_to.set_value(0, skip_validation=True) - else: - raise ValueError("copy_var_list_values_from_solution_pool failed.") + copy_var_value(v_from, v_to, var_val, config, ignore_integrality) class GurobiPersistent4MindtPy(GurobiPersistent): @@ -983,6 +953,19 @@ def copy_var_list_values( """Copy variable values from one list to another. Rounds to Binary/Integer if necessary Sets to zero for NonNegativeReals if necessary + + from_list : list + The variables that provides the values to copy from. + to_list : list + The variables that need to set value. + config : ConfigBlock + The specific configurations for MindtPy. + skip_stale : bool, optional + Whether to skip the stale variables, by default False. + skip_fixed : bool, optional + Whether to skip the fixed variables, by default True. + ignore_integrality : bool, optional + Whether to ignore the integrality of integer variables, by default False. """ for v_from, v_to in zip(from_list, to_list): if skip_stale and v_from.stale: @@ -990,25 +973,59 @@ def copy_var_list_values( if skip_fixed and v_to.is_fixed(): continue # Skip fixed variables. var_val = value(v_from, exception=False) - rounded_val = int(round(var_val)) - if ( - var_val in v_to.domain - and not ((v_to.has_lb() and var_val < v_to.lb)) - and not ((v_to.has_ub() and var_val > v_to.ub)) - ): - v_to.set_value(value(v_from, exception=False)) - elif v_to.has_lb() and var_val < v_to.lb: - v_to.set_value(v_to.lb) - elif v_to.has_ub() and var_val > v_to.ub: - v_to.set_value(v_to.ub) - elif ignore_integrality and v_to.is_integer(): - v_to.set_value(value(v_from, exception=False), skip_validation=True) - elif v_to.is_integer() and ( - math.fabs(var_val - rounded_val) <= config.integer_tolerance + copy_var_value(v_from, v_to, var_val, config, ignore_integrality) + + +def copy_var_value(v_from, v_to, var_val, config, ignore_integrality): + """This function copies variable value from one to another. + Rounds to Binary/Integer if necessary. + Sets to zero for NonNegativeReals if necessary. + + NOTE: PEP 2180 changes the var behavior so that domain / + bounds violations no longer generate exceptions (and + instead log warnings). This means that the following will + always succeed and the ValueError should never be raised. + + Parameters + ---------- + v_from : Var + The variable that provides the values to copy from. + v_to : Var + The variable that needs to set value. + var_val : float + The value of v_to variable. + config : ConfigBlock + The specific configurations for MindtPy. + ignore_integrality : bool, optional + Whether to ignore the integrality of integer variables, by default False. + + Raises + ------ + ValueError + Cannot successfully set the value to variable v_to. + """ + # We don't want to trigger the reset of the global stale + # indicator, so we will set this variable to be "stale", + # knowing that set_value will switch it back to "not stale". + v_to.stale = True + rounded_val = int(round(var_val)) + if (var_val in v_to.domain + and not ((v_to.has_lb() and var_val < v_to.lb)) + and not ((v_to.has_ub() and var_val > v_to.ub)) ): - v_to.set_value(rounded_val) - elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: - v_to.set_value(0) - else: - raise ValueError("copy_var_list_values failed with variable {}, value = {} and rounded value = {}" - "".format(v_to.name, var_val, rounded_val)) + v_to.set_value(var_val) + elif v_to.has_lb() and var_val < v_to.lb: + v_to.set_value(v_to.lb) + elif v_to.has_ub() and var_val > v_to.ub: + v_to.set_value(v_to.ub) + elif ignore_integrality and v_to.is_integer(): + v_to.set_value(var_val, skip_validation=True) + elif v_to.is_integer() and ( + math.fabs(var_val - rounded_val) <= config.integer_tolerance + ): + v_to.set_value(rounded_val) + elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: + v_to.set_value(0) + else: + raise ValueError("copy_var_list_values failed with variable {}, value = {} and rounded value = {}" + "".format(v_to.name, var_val, rounded_val)) From dc41b8e969490e15d1c7948e386a8f2ba3ebedb8 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 27 Nov 2023 20:07:43 -0500 Subject: [PATCH 0192/3044] add exc_info for the error message --- pyomo/contrib/mindtpy/algorithm_base_class.py | 17 ++++++++++------- pyomo/contrib/mindtpy/cut_generation.py | 11 +++++++---- pyomo/contrib/mindtpy/extended_cutting_plane.py | 4 ++-- .../mindtpy/global_outer_approximation.py | 3 ++- pyomo/contrib/mindtpy/single_tree.py | 9 +++++---- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 92e1075fe90..141e7f9f09f 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -802,7 +802,7 @@ def MindtPy_initialization(self): try: self.curr_int_sol = get_integer_solution(self.working_model) except TypeError as e: - config.logger.error(e) + config.logger.error(e, exc_info=True) raise ValueError( 'The initial integer combination is not provided or not complete. ' 'Please provide the complete integer combination or use other initialization strategy.' @@ -1083,7 +1083,7 @@ def solve_subproblem(self): 0, c_geq * (rhs - value(c.body)) ) except (ValueError, OverflowError) as e: - config.logger.error(e) + config.logger.error(e, exc_info=True) self.fixed_nlp.tmp_duals[c] = None evaluation_error = True if evaluation_error: @@ -1100,8 +1100,9 @@ def solve_subproblem(self): tolerance=config.constraint_tolerance, ) except InfeasibleConstraintException as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) + '\nInfeasibility detected in deactivate_trivial_constraints.' + 'Infeasibility detected in deactivate_trivial_constraints.' ) results = SolverResults() results.solver.termination_condition = tc.infeasible @@ -1401,7 +1402,7 @@ def solve_feasibility_subproblem(self): if len(feas_soln.solution) > 0: feas_subproblem.solutions.load_from(feas_soln) except (ValueError, OverflowError) as e: - config.logger.error(e) + config.logger.error(e, exc_info=True) for nlp_var, orig_val in zip( MindtPy.variable_list, self.initial_var_values ): @@ -1542,8 +1543,9 @@ def fix_dual_bound(self, last_iter_cuts): try: self.dual_bound = self.stored_bound[self.primal_bound] except KeyError as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) + '\nNo stored bound found. Bound fix failed.' + 'No stored bound found. Bound fix failed.' ) else: config.logger.info( @@ -1670,7 +1672,7 @@ def solve_main(self): if len(main_mip_results.solution) > 0: self.mip.solutions.load_from(main_mip_results) except (ValueError, AttributeError, RuntimeError) as e: - config.logger.error(e) + config.logger.error(e, exc_info=True) if config.single_tree: config.logger.warning('Single tree terminate.') if get_main_elapsed_time(self.timing) >= config.time_limit: @@ -2369,8 +2371,9 @@ def solve_fp_subproblem(self): tolerance=config.constraint_tolerance, ) except InfeasibleConstraintException as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) + '\nInfeasibility detected in deactivate_trivial_constraints.' + 'Infeasibility detected in deactivate_trivial_constraints.' ) results = SolverResults() results.solver.termination_condition = tc.infeasible diff --git a/pyomo/contrib/mindtpy/cut_generation.py b/pyomo/contrib/mindtpy/cut_generation.py index 28d302104a3..343170aabac 100644 --- a/pyomo/contrib/mindtpy/cut_generation.py +++ b/pyomo/contrib/mindtpy/cut_generation.py @@ -271,8 +271,9 @@ def add_ecp_cuts( try: upper_slack = constr.uslack() except (ValueError, OverflowError) as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) + '\nConstraint {} has caused either a ' + 'Constraint {} has caused either a ' 'ValueError or OverflowError.' '\n'.format(constr) ) @@ -300,8 +301,9 @@ def add_ecp_cuts( try: lower_slack = constr.lslack() except (ValueError, OverflowError) as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) + '\nConstraint {} has caused either a ' + 'Constraint {} has caused either a ' 'ValueError or OverflowError.' '\n'.format(constr) ) @@ -424,9 +426,10 @@ def add_affine_cuts(target_model, config, timing): try: mc_eqn = mc(constr.body) except MCPP_Error as e: + config.logger.error(e, exc_info=True) config.logger.error( - '\nSkipping constraint %s due to MCPP error %s' - % (constr.name, str(e)) + 'Skipping constraint %s due to MCPP error' + % (constr.name) ) continue # skip to the next constraint diff --git a/pyomo/contrib/mindtpy/extended_cutting_plane.py b/pyomo/contrib/mindtpy/extended_cutting_plane.py index 446304b1361..3a09af155a0 100644 --- a/pyomo/contrib/mindtpy/extended_cutting_plane.py +++ b/pyomo/contrib/mindtpy/extended_cutting_plane.py @@ -140,7 +140,7 @@ def all_nonlinear_constraint_satisfied(self): lower_slack = nlc.lslack() except (ValueError, OverflowError) as e: # Set lower_slack (upper_slack below) less than -config.ecp_tolerance in this case. - config.logger.error(e) + config.logger.error(e, exc_info=True) lower_slack = -10 * config.ecp_tolerance if lower_slack < -config.ecp_tolerance: config.logger.debug( @@ -153,7 +153,7 @@ def all_nonlinear_constraint_satisfied(self): try: upper_slack = nlc.uslack() except (ValueError, OverflowError) as e: - config.logger.error(e) + config.logger.error(e, exc_info=True) upper_slack = -10 * config.ecp_tolerance if upper_slack < -config.ecp_tolerance: config.logger.debug( diff --git a/pyomo/contrib/mindtpy/global_outer_approximation.py b/pyomo/contrib/mindtpy/global_outer_approximation.py index dfb7ef54630..817fb0bf4a8 100644 --- a/pyomo/contrib/mindtpy/global_outer_approximation.py +++ b/pyomo/contrib/mindtpy/global_outer_approximation.py @@ -108,4 +108,5 @@ def deactivate_no_good_cuts_when_fixing_bound(self, no_good_cuts): if self.config.use_tabu_list: self.integer_list = self.integer_list[:valid_no_good_cuts_num] except KeyError as e: - self.config.logger.error(str(e) + '\nDeactivating no-good cuts failed.') + self.config.logger.error(e, exc_info=True) + self.config.logger.error('Deactivating no-good cuts failed.') diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index a5d4401d623..5485e0298f2 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -259,9 +259,10 @@ def add_lazy_affine_cuts(self, mindtpy_solver, config, opt): try: mc_eqn = mc(constr.body) except MCPP_Error as e: + config.logger.error(e, exc_info=True) config.logger.debug( - 'Skipping constraint %s due to MCPP error %s' - % (constr.name, str(e)) + 'Skipping constraint %s due to MCPP error' + % (constr.name) ) continue # skip to the next constraint # TODO: check if the value of ccSlope and cvSlope is not Nan or inf. If so, we skip this. @@ -696,9 +697,9 @@ def __call__(self): mindtpy_solver.mip, None, mindtpy_solver, config, opt ) except ValueError as e: + config.logger.error(e, exc_info=True) config.logger.error( - str(e) - + "\nUsually this error is caused by the MIP start solution causing a math domain error. " + "Usually this error is caused by the MIP start solution causing a math domain error. " "We will skip it." ) return From dbe9f490fd49e47ea5634c8425eeddd019d731ce Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 27 Nov 2023 20:34:04 -0500 Subject: [PATCH 0193/3044] black format --- pyomo/contrib/mindtpy/algorithm_base_class.py | 4 +--- pyomo/contrib/mindtpy/cut_generation.py | 3 +-- pyomo/contrib/mindtpy/single_tree.py | 9 ++++++--- pyomo/contrib/mindtpy/util.py | 11 +++++++---- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 141e7f9f09f..b06a4c730b4 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1544,9 +1544,7 @@ def fix_dual_bound(self, last_iter_cuts): self.dual_bound = self.stored_bound[self.primal_bound] except KeyError as e: config.logger.error(e, exc_info=True) - config.logger.error( - 'No stored bound found. Bound fix failed.' - ) + config.logger.error('No stored bound found. Bound fix failed.') else: config.logger.info( 'Solve the main problem without the last no_good cut to fix the bound.' diff --git a/pyomo/contrib/mindtpy/cut_generation.py b/pyomo/contrib/mindtpy/cut_generation.py index 343170aabac..e57cfd2eada 100644 --- a/pyomo/contrib/mindtpy/cut_generation.py +++ b/pyomo/contrib/mindtpy/cut_generation.py @@ -428,8 +428,7 @@ def add_affine_cuts(target_model, config, timing): except MCPP_Error as e: config.logger.error(e, exc_info=True) config.logger.error( - 'Skipping constraint %s due to MCPP error' - % (constr.name) + 'Skipping constraint %s due to MCPP error' % (constr.name) ) continue # skip to the next constraint diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 5485e0298f2..5e4e378d6c5 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -16,7 +16,11 @@ from pyomo.repn import generate_standard_repn import pyomo.core.expr as EXPR from math import copysign -from pyomo.contrib.mindtpy.util import get_integer_solution, copy_var_list_values, copy_var_value +from pyomo.contrib.mindtpy.util import ( + get_integer_solution, + copy_var_list_values, + copy_var_value, +) from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code from pyomo.opt import TerminationCondition as tc from pyomo.core import minimize, value @@ -261,8 +265,7 @@ def add_lazy_affine_cuts(self, mindtpy_solver, config, opt): except MCPP_Error as e: config.logger.error(e, exc_info=True) config.logger.debug( - 'Skipping constraint %s due to MCPP error' - % (constr.name) + 'Skipping constraint %s due to MCPP error' % (constr.name) ) continue # skip to the next constraint # TODO: check if the value of ccSlope and cvSlope is not Nan or inf. If so, we skip this. diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 2970a805540..7e3fbe415d4 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -1009,10 +1009,11 @@ def copy_var_value(v_from, v_to, var_val, config, ignore_integrality): # knowing that set_value will switch it back to "not stale". v_to.stale = True rounded_val = int(round(var_val)) - if (var_val in v_to.domain + if ( + var_val in v_to.domain and not ((v_to.has_lb() and var_val < v_to.lb)) and not ((v_to.has_ub() and var_val > v_to.ub)) - ): + ): v_to.set_value(var_val) elif v_to.has_lb() and var_val < v_to.lb: v_to.set_value(v_to.lb) @@ -1027,5 +1028,7 @@ def copy_var_value(v_from, v_to, var_val, config, ignore_integrality): elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: v_to.set_value(0) else: - raise ValueError("copy_var_list_values failed with variable {}, value = {} and rounded value = {}" - "".format(v_to.name, var_val, rounded_val)) + raise ValueError( + "copy_var_list_values failed with variable {}, value = {} and rounded value = {}" + "".format(v_to.name, var_val, rounded_val) + ) From 4ac390e12fcfa3277a9808ff7f7325bfde808124 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 28 Nov 2023 09:34:22 -0500 Subject: [PATCH 0194/3044] change dir() to locals() --- pyomo/contrib/mindtpy/algorithm_base_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index b06a4c730b4..d5d015d180d 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1684,7 +1684,7 @@ def solve_main(self): 'No integer solution is found, so the CPLEX solver will report an error status. ' ) # Value error will be raised if the MIP problem is unbounded and appsi solver is used when loading solutions. Although the problem is unbounded, a valid result is provided and we do not return None to let the algorithm continue. - if 'main_mip_results' in dir(): + if 'main_mip_results' in locals(): return self.mip, main_mip_results else: return None, None From a755067a6276e62569308d5ce80ef47574eaf63b Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 28 Nov 2023 10:51:56 -0500 Subject: [PATCH 0195/3044] improve int_sol_2_cuts_ind --- pyomo/contrib/mindtpy/algorithm_base_class.py | 9 +++++---- pyomo/contrib/mindtpy/single_tree.py | 14 +++++++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index d5d015d180d..2eec150453f 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -108,7 +108,7 @@ def __init__(self, **kwds): self.curr_int_sol = [] self.should_terminate = False self.integer_list = [] - # dictionary {integer solution (list): cuts index (list)} + # dictionary {integer solution (list): [cuts begin index, cuts end index] (list)} self.int_sol_2_cuts_ind = dict() # Set up iteration counters @@ -810,9 +810,10 @@ def MindtPy_initialization(self): self.integer_list.append(self.curr_int_sol) fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) - self.int_sol_2_cuts_ind[self.curr_int_sol] = list( - range(1, len(self.mip.MindtPy_utils.cuts.oa_cuts) + 1) - ) + self.int_sol_2_cuts_ind[self.curr_int_sol] = [ + 1, + len(self.mip.MindtPy_utils.cuts.oa_cuts), + ] elif config.init_strategy == 'FP': self.init_rNLP() self.fp_loop() diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 5e4e378d6c5..4733843d6a2 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -900,9 +900,10 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): # Your callback should be prepared to cut off solutions that violate any of your lazy constraints, including those that have already been added. Node solutions will usually respect previously added lazy constraints, but not always. # https://www.gurobi.com/documentation/current/refman/cs_cb_addlazy.html # If this happens, MindtPy will look for the index of corresponding cuts, instead of solving the fixed-NLP again. - for ind in mindtpy_solver.int_sol_2_cuts_ind[ + begin_index, end_index = mindtpy_solver.int_sol_2_cuts_ind[ mindtpy_solver.curr_int_sol - ]: + ] + for ind in range(begin_index, end_index + 1): cb_opt.cbLazy(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts[ind]) return else: @@ -917,11 +918,10 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): mindtpy_solver.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result, cb_opt) if config.strategy == 'OA': # store the cut index corresponding to current integer solution. - mindtpy_solver.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol] = list( - range( - cut_ind + 1, len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts) + 1 - ) - ) + mindtpy_solver.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol] = [ + cut_ind + 1, + len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts), + ] def handle_lazy_main_feasible_solution_gurobi(cb_m, cb_opt, mindtpy_solver, config): From d9d29bf04806a3d666cae8f6e20773440ed07928 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 28 Nov 2023 15:40:32 -0500 Subject: [PATCH 0196/3044] rename copy_var_value to set_var_value --- pyomo/contrib/mindtpy/single_tree.py | 10 +++++++-- pyomo/contrib/mindtpy/util.py | 32 ++++++++++++++++++---------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 4733843d6a2..481ff38df8f 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -19,7 +19,7 @@ from pyomo.contrib.mindtpy.util import ( get_integer_solution, copy_var_list_values, - copy_var_value, + set_var_value, ) from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code from pyomo.opt import TerminationCondition as tc @@ -62,7 +62,13 @@ def copy_lazy_var_list_values( if skip_fixed and v_to.is_fixed(): continue # Skip fixed variables. v_val = self.get_values(opt._pyomo_var_to_solver_var_map[v_from]) - copy_var_value(v_from, v_to, v_val, config, ignore_integrality=False) + set_var_value( + v_to, + v_val, + config.integer_tolerance, + config.zero_tolerance, + ignore_integrality=False, + ) def add_lazy_oa_cuts( self, diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 7e3fbe415d4..ea22eb1ec3a 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -693,7 +693,13 @@ def copy_var_list_values_from_solution_pool( elif config.mip_solver == 'gurobi_persistent': solver_model.setParam(gurobipy.GRB.Param.SolutionNumber, solution_name) var_val = var_map[v_from].Xn - copy_var_value(v_from, v_to, var_val, config, ignore_integrality) + set_var_value( + v_to, + var_val, + config.integer_tolerance, + config.zero_tolerance, + ignore_integrality, + ) class GurobiPersistent4MindtPy(GurobiPersistent): @@ -973,10 +979,16 @@ def copy_var_list_values( if skip_fixed and v_to.is_fixed(): continue # Skip fixed variables. var_val = value(v_from, exception=False) - copy_var_value(v_from, v_to, var_val, config, ignore_integrality) + set_var_value( + v_to, + var_val, + config.integer_tolerance, + config.zero_tolerance, + ignore_integrality, + ) -def copy_var_value(v_from, v_to, var_val, config, ignore_integrality): +def set_var_value(v_to, var_val, integer_tolerance, zero_tolerance, ignore_integrality): """This function copies variable value from one to another. Rounds to Binary/Integer if necessary. Sets to zero for NonNegativeReals if necessary. @@ -988,14 +1000,14 @@ def copy_var_value(v_from, v_to, var_val, config, ignore_integrality): Parameters ---------- - v_from : Var - The variable that provides the values to copy from. v_to : Var The variable that needs to set value. var_val : float The value of v_to variable. - config : ConfigBlock - The specific configurations for MindtPy. + integer_tolerance: float + Tolerance on integral values. + zero_tolerance: float + Tolerance on variable equal to zero. ignore_integrality : bool, optional Whether to ignore the integrality of integer variables, by default False. @@ -1021,11 +1033,9 @@ def copy_var_value(v_from, v_to, var_val, config, ignore_integrality): v_to.set_value(v_to.ub) elif ignore_integrality and v_to.is_integer(): v_to.set_value(var_val, skip_validation=True) - elif v_to.is_integer() and ( - math.fabs(var_val - rounded_val) <= config.integer_tolerance - ): + elif v_to.is_integer() and (math.fabs(var_val - rounded_val) <= integer_tolerance): v_to.set_value(rounded_val) - elif abs(var_val) <= config.zero_tolerance and 0 in v_to.domain: + elif abs(var_val) <= zero_tolerance and 0 in v_to.domain: v_to.set_value(0) else: raise ValueError( From f04424e0d747d4d6986b9224e3ca8d7e4ad246ac Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 28 Nov 2023 15:57:37 -0500 Subject: [PATCH 0197/3044] add unit test for mindtpy --- pyomo/contrib/mindtpy/tests/unit_test.py | 70 ++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 pyomo/contrib/mindtpy/tests/unit_test.py diff --git a/pyomo/contrib/mindtpy/tests/unit_test.py b/pyomo/contrib/mindtpy/tests/unit_test.py new file mode 100644 index 00000000000..d9b2e494ab0 --- /dev/null +++ b/pyomo/contrib/mindtpy/tests/unit_test.py @@ -0,0 +1,70 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.mindtpy.util import set_var_value + +from pyomo.environ import Var, Integers, ConcreteModel, Integers + + +class UnitTestMindtPy(unittest.TestCase): + def test_set_var_value(self): + m = ConcreteModel() + m.x1 = Var(within=Integers, bounds=(-1, 4), initialize=0) + + set_var_value( + m.x1, + var_val=5, + integer_tolerance=1e-6, + zero_tolerance=1e-6, + ignore_integrality=False, + ) + self.assertEqual(m.x1.value, 4) + + set_var_value( + m.x1, + var_val=-2, + integer_tolerance=1e-6, + zero_tolerance=1e-6, + ignore_integrality=False, + ) + self.assertEqual(m.x1.value, -1) + + set_var_value( + m.x1, + var_val=1.1, + integer_tolerance=1e-6, + zero_tolerance=1e-6, + ignore_integrality=True, + ) + self.assertEqual(m.x1.value, 1.1) + + set_var_value( + m.x1, + var_val=2.00000001, + integer_tolerance=1e-6, + zero_tolerance=1e-6, + ignore_integrality=False, + ) + self.assertEqual(m.x1.value, 2) + + set_var_value( + m.x1, + var_val=0.0000001, + integer_tolerance=1e-9, + zero_tolerance=1e-6, + ignore_integrality=False, + ) + self.assertEqual(m.x1.value, 0) + + +if __name__ == '__main__': + unittest.main() From ef6666085071f235458a36dbe3cb62192e391a2f Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 29 Nov 2023 17:28:51 -0500 Subject: [PATCH 0198/3044] improve var_val description --- pyomo/contrib/mindtpy/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index ea22eb1ec3a..51ed59e80a2 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -1003,7 +1003,7 @@ def set_var_value(v_to, var_val, integer_tolerance, zero_tolerance, ignore_integ v_to : Var The variable that needs to set value. var_val : float - The value of v_to variable. + The desired value to set for Var v_to. integer_tolerance: float Tolerance on integral values. zero_tolerance: float From 04ea15effcc83213c49a82b1230ffa3c0a945211 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 29 Nov 2023 17:31:55 -0500 Subject: [PATCH 0199/3044] rename set_var_value to set_var_valid_value --- pyomo/contrib/mindtpy/single_tree.py | 4 ++-- pyomo/contrib/mindtpy/tests/unit_test.py | 14 +++++++------- pyomo/contrib/mindtpy/util.py | 8 +++++--- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 481ff38df8f..c4d49e3afd6 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -19,7 +19,7 @@ from pyomo.contrib.mindtpy.util import ( get_integer_solution, copy_var_list_values, - set_var_value, + set_var_valid_value, ) from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code from pyomo.opt import TerminationCondition as tc @@ -62,7 +62,7 @@ def copy_lazy_var_list_values( if skip_fixed and v_to.is_fixed(): continue # Skip fixed variables. v_val = self.get_values(opt._pyomo_var_to_solver_var_map[v_from]) - set_var_value( + set_var_valid_value( v_to, v_val, config.integer_tolerance, diff --git a/pyomo/contrib/mindtpy/tests/unit_test.py b/pyomo/contrib/mindtpy/tests/unit_test.py index d9b2e494ab0..baf5e16bb4b 100644 --- a/pyomo/contrib/mindtpy/tests/unit_test.py +++ b/pyomo/contrib/mindtpy/tests/unit_test.py @@ -10,17 +10,17 @@ # ___________________________________________________________________________ import pyomo.common.unittest as unittest -from pyomo.contrib.mindtpy.util import set_var_value +from pyomo.contrib.mindtpy.util import set_var_valid_value from pyomo.environ import Var, Integers, ConcreteModel, Integers class UnitTestMindtPy(unittest.TestCase): - def test_set_var_value(self): + def test_set_var_valid_value(self): m = ConcreteModel() m.x1 = Var(within=Integers, bounds=(-1, 4), initialize=0) - set_var_value( + set_var_valid_value( m.x1, var_val=5, integer_tolerance=1e-6, @@ -29,7 +29,7 @@ def test_set_var_value(self): ) self.assertEqual(m.x1.value, 4) - set_var_value( + set_var_valid_value( m.x1, var_val=-2, integer_tolerance=1e-6, @@ -38,7 +38,7 @@ def test_set_var_value(self): ) self.assertEqual(m.x1.value, -1) - set_var_value( + set_var_valid_value( m.x1, var_val=1.1, integer_tolerance=1e-6, @@ -47,7 +47,7 @@ def test_set_var_value(self): ) self.assertEqual(m.x1.value, 1.1) - set_var_value( + set_var_valid_value( m.x1, var_val=2.00000001, integer_tolerance=1e-6, @@ -56,7 +56,7 @@ def test_set_var_value(self): ) self.assertEqual(m.x1.value, 2) - set_var_value( + set_var_valid_value( m.x1, var_val=0.0000001, integer_tolerance=1e-9, diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 51ed59e80a2..f6cc0567286 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -693,7 +693,7 @@ def copy_var_list_values_from_solution_pool( elif config.mip_solver == 'gurobi_persistent': solver_model.setParam(gurobipy.GRB.Param.SolutionNumber, solution_name) var_val = var_map[v_from].Xn - set_var_value( + set_var_valid_value( v_to, var_val, config.integer_tolerance, @@ -979,7 +979,7 @@ def copy_var_list_values( if skip_fixed and v_to.is_fixed(): continue # Skip fixed variables. var_val = value(v_from, exception=False) - set_var_value( + set_var_valid_value( v_to, var_val, config.integer_tolerance, @@ -988,7 +988,9 @@ def copy_var_list_values( ) -def set_var_value(v_to, var_val, integer_tolerance, zero_tolerance, ignore_integrality): +def set_var_valid_value( + v_to, var_val, integer_tolerance, zero_tolerance, ignore_integrality +): """This function copies variable value from one to another. Rounds to Binary/Integer if necessary. Sets to zero for NonNegativeReals if necessary. From f84ff8d3429eb88bcd50021a8f4d22bcc691f2fb Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 29 Nov 2023 17:39:52 -0500 Subject: [PATCH 0200/3044] change v_to to var --- pyomo/contrib/mindtpy/util.py | 42 +++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index f6cc0567286..a9802a8bd1e 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -989,9 +989,9 @@ def copy_var_list_values( def set_var_valid_value( - v_to, var_val, integer_tolerance, zero_tolerance, ignore_integrality + var, var_val, integer_tolerance, zero_tolerance, ignore_integrality ): - """This function copies variable value from one to another. + """This function tries to set a valid value for variable with the given input. Rounds to Binary/Integer if necessary. Sets to zero for NonNegativeReals if necessary. @@ -1002,10 +1002,10 @@ def set_var_valid_value( Parameters ---------- - v_to : Var + var : Var The variable that needs to set value. var_val : float - The desired value to set for Var v_to. + The desired value to set for var. integer_tolerance: float Tolerance on integral values. zero_tolerance: float @@ -1016,31 +1016,31 @@ def set_var_valid_value( Raises ------ ValueError - Cannot successfully set the value to variable v_to. + Cannot successfully set the value to the variable. """ # We don't want to trigger the reset of the global stale # indicator, so we will set this variable to be "stale", # knowing that set_value will switch it back to "not stale". - v_to.stale = True + var.stale = True rounded_val = int(round(var_val)) if ( - var_val in v_to.domain - and not ((v_to.has_lb() and var_val < v_to.lb)) - and not ((v_to.has_ub() and var_val > v_to.ub)) + var_val in var.domain + and not ((var.has_lb() and var_val < var.lb)) + and not ((var.has_ub() and var_val > var.ub)) ): - v_to.set_value(var_val) - elif v_to.has_lb() and var_val < v_to.lb: - v_to.set_value(v_to.lb) - elif v_to.has_ub() and var_val > v_to.ub: - v_to.set_value(v_to.ub) - elif ignore_integrality and v_to.is_integer(): - v_to.set_value(var_val, skip_validation=True) - elif v_to.is_integer() and (math.fabs(var_val - rounded_val) <= integer_tolerance): - v_to.set_value(rounded_val) - elif abs(var_val) <= zero_tolerance and 0 in v_to.domain: - v_to.set_value(0) + var.set_value(var_val) + elif var.has_lb() and var_val < var.lb: + var.set_value(var.lb) + elif var.has_ub() and var_val > var.ub: + var.set_value(var.ub) + elif ignore_integrality and var.is_integer(): + var.set_value(var_val, skip_validation=True) + elif var.is_integer() and (math.fabs(var_val - rounded_val) <= integer_tolerance): + var.set_value(rounded_val) + elif abs(var_val) <= zero_tolerance and 0 in var.domain: + var.set_value(0) else: raise ValueError( "copy_var_list_values failed with variable {}, value = {} and rounded value = {}" - "".format(v_to.name, var_val, rounded_val) + "".format(var.name, var_val, rounded_val) ) From 83b28cb0d4216b337d8308d07ea090092a71a880 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 29 Nov 2023 17:42:10 -0500 Subject: [PATCH 0201/3044] move NOTE from docstring to comment --- pyomo/contrib/mindtpy/util.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index a9802a8bd1e..afcb129e40e 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -995,11 +995,6 @@ def set_var_valid_value( Rounds to Binary/Integer if necessary. Sets to zero for NonNegativeReals if necessary. - NOTE: PEP 2180 changes the var behavior so that domain / - bounds violations no longer generate exceptions (and - instead log warnings). This means that the following will - always succeed and the ValueError should never be raised. - Parameters ---------- var : Var @@ -1018,6 +1013,11 @@ def set_var_valid_value( ValueError Cannot successfully set the value to the variable. """ + # NOTE: PEP 2180 changes the var behavior so that domain + # bounds violations no longer generate exceptions (and + # instead log warnings). This means that the set_value method + # will always succeed and the ValueError should never be raised. + # We don't want to trigger the reset of the global stale # indicator, so we will set this variable to be "stale", # knowing that set_value will switch it back to "not stale". From 355df8b4a112597d4c1c45b0b6cd6a4ca13ff6af Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 30 Nov 2023 10:44:36 -0500 Subject: [PATCH 0202/3044] remove redundant test --- pyomo/repn/tests/ampl/test_nlv2.py | 34 ------------------------------ 1 file changed, 34 deletions(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 460c45b4ebb..fe5f422d323 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -1055,40 +1055,6 @@ def test_log_timing(self): re.sub(r'\d\.\d\d\]', '#.##]', LOG.getvalue()), ) - def test_log_timing(self): - # This tests an error possibly reported by #2810 - m = ConcreteModel() - m.x = Var(range(6)) - m.x[0].domain = pyo.Binary - m.x[1].domain = pyo.Integers - m.x[2].domain = pyo.Integers - m.p = Param(initialize=5, mutable=True) - m.o1 = Objective([1, 2], rule=lambda m, i: 1) - m.o2 = Objective(expr=m.x[1] * m.x[2]) - m.c1 = Constraint([1, 2], rule=lambda m, i: sum(m.x.values()) == 1) - m.c2 = Constraint(expr=m.p * m.x[1] ** 2 + m.x[2] ** 3 <= 100) - - self.maxDiff = None - OUT = io.StringIO() - with capture_output() as LOG: - with report_timing(level=logging.DEBUG): - nl_writer.NLWriter().write(m, OUT) - self.assertEqual( - """ [+ #.##] Initialized column order - [+ #.##] Collected suffixes - [+ #.##] Objective o1 - [+ #.##] Objective o2 - [+ #.##] Constraint c1 - [+ #.##] Constraint c2 - [+ #.##] Categorized model variables: 14 nnz - [+ #.##] Set row / column ordering: 6 var [3, 1, 2 R/B/Z], 3 con [2, 1 L/NL] - [+ #.##] Generated row/col labels & comments - [+ #.##] Wrote NL stream - [ #.##] Generated NL representation -""", - re.sub(r'\d\.\d\d\]', '#.##]', LOG.getvalue()), - ) - def test_linear_constraint_npv_const(self): # This tests an error possibly reported by #2810 m = ConcreteModel() From a7a01c229738fe680ad8d2f7f6814ab0e2c38a0c Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 30 Nov 2023 18:28:06 -0500 Subject: [PATCH 0203/3044] add test_add_var_bound --- pyomo/contrib/mindtpy/tests/unit_test.py | 31 ++++++++++++++++++++++++ pyomo/contrib/mindtpy/util.py | 4 +-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/mindtpy/tests/unit_test.py b/pyomo/contrib/mindtpy/tests/unit_test.py index baf5e16bb4b..a1ceadda41e 100644 --- a/pyomo/contrib/mindtpy/tests/unit_test.py +++ b/pyomo/contrib/mindtpy/tests/unit_test.py @@ -13,6 +13,10 @@ from pyomo.contrib.mindtpy.util import set_var_valid_value from pyomo.environ import Var, Integers, ConcreteModel, Integers +from pyomo.contrib.mindtpy.algorithm_base_class import _MindtPyAlgorithm +from pyomo.contrib.mindtpy.config_options import _get_MindtPy_OA_config +from pyomo.contrib.mindtpy.tests.MINLP5_simple import SimpleMINLP5 +from pyomo.contrib.mindtpy.util import add_var_bound class UnitTestMindtPy(unittest.TestCase): @@ -65,6 +69,33 @@ def test_set_var_valid_value(self): ) self.assertEqual(m.x1.value, 0) + def test_add_var_bound(self): + m = SimpleMINLP5().clone() + m.x.lb = None + m.x.ub = None + m.y.lb = None + m.y.ub = None + solver_object = _MindtPyAlgorithm() + solver_object.config = _get_MindtPy_OA_config() + solver_object.set_up_solve_data(m) + solver_object.create_utility_block(solver_object.working_model, 'MindtPy_utils') + add_var_bound(solver_object.working_model, solver_object.config) + self.assertEqual( + solver_object.working_model.x.lower, + -solver_object.config.continuous_var_bound - 1, + ) + self.assertEqual( + solver_object.working_model.x.upper, + solver_object.config.continuous_var_bound, + ) + self.assertEqual( + solver_object.working_model.y.lower, + -solver_object.config.integer_var_bound - 1, + ) + self.assertEqual( + solver_object.working_model.y.upper, solver_object.config.integer_var_bound + ) + if __name__ == '__main__': unittest.main() diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index afcb129e40e..1173dfe0cca 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -134,12 +134,12 @@ def add_var_bound(model, config): for var in EXPR.identify_variables(c.body): if var.has_lb() and var.has_ub(): continue - elif not var.has_lb(): + if not var.has_lb(): if var.is_integer(): var.setlb(-config.integer_var_bound - 1) else: var.setlb(-config.continuous_var_bound - 1) - elif not var.has_ub(): + if not var.has_ub(): if var.is_integer(): var.setub(config.integer_var_bound) else: From 875269fb7b7d5cdb3396ff1b7a2e5e2b5fc4e0d2 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 30 Nov 2023 18:29:28 -0500 Subject: [PATCH 0204/3044] delete redundant set_up_logger function --- pyomo/contrib/mindtpy/util.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 1173dfe0cca..ec2829c6a18 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -724,25 +724,6 @@ def f(gurobi_model, where): return f -def set_up_logger(config): - """Set up the formatter and handler for logger. - - Parameters - ---------- - config : ConfigBlock - The specific configurations for MindtPy. - """ - config.logger.handlers.clear() - config.logger.propagate = False - ch = logging.StreamHandler() - ch.setLevel(config.logging_level) - # create formatter and add it to the handlers - formatter = logging.Formatter('%(message)s') - ch.setFormatter(formatter) - # add the handlers to logger - config.logger.addHandler(ch) - - def epigraph_reformulation(exp, slack_var_list, constraint_list, use_mcpp, sense): """Epigraph reformulation. From 65e58f531c40fa466da60954b4bc5cc9b508055d Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 30 Nov 2023 19:50:14 -0500 Subject: [PATCH 0205/3044] add test_FP_L1_norm --- .../mindtpy/tests/test_mindtpy_feas_pump.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py index 697a63d17c8..dcb5c4bce75 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py @@ -17,7 +17,7 @@ from pyomo.contrib.mindtpy.tests.feasibility_pump1 import FeasPump1 from pyomo.contrib.mindtpy.tests.feasibility_pump2 import FeasPump2 -required_solvers = ('ipopt', 'cplex') +required_solvers = ('ipopt', 'glpk') # TODO: 'appsi_highs' will fail here. if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True @@ -69,6 +69,22 @@ def test_FP(self): log_infeasible_constraints(model) self.assertTrue(is_feasible(model, self.get_config(opt))) + def test_FP_L1_norm(self): + """Test the feasibility pump algorithm.""" + with SolverFactory('mindtpy') as opt: + for model in model_list: + model = model.clone() + results = opt.solve( + model, + strategy='FP', + mip_solver=required_solvers[1], + nlp_solver=required_solvers[0], + absolute_bound_tolerance=1e-5, + fp_main_norm='L1', + ) + log_infeasible_constraints(model) + self.assertTrue(is_feasible(model, self.get_config(opt))) + def test_FP_OA_8PP(self): """Test the FP-OA algorithm.""" with SolverFactory('mindtpy') as opt: From c8eead976a96ee87e79ce22bf8866e6b28abeb66 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 30 Nov 2023 20:08:16 -0500 Subject: [PATCH 0206/3044] improve mindtpy logging --- pyomo/contrib/mindtpy/algorithm_base_class.py | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 2eec150453f..78250d1ba59 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -125,6 +125,9 @@ def __init__(self, **kwds): self.log_formatter = ( ' {:>9} {:>15} {:>15g} {:>12g} {:>12g} {:>7.2%} {:>7.2f}' ) + self.termination_condition_log_formatter = ( + ' {:>9} {:>15} {:>15} {:>12g} {:>12g} {:>7.2%} {:>7.2f}' + ) self.fixed_nlp_log_formatter = ( '{:1}{:>9} {:>15} {:>15g} {:>12g} {:>12g} {:>7.2%} {:>7.2f}' ) @@ -1919,11 +1922,6 @@ def handle_main_max_timelimit(self, main_mip, main_mip_results): """ # If we have found a valid feasible solution, we take that. If not, we can at least use the dual bound. MindtPy = main_mip.MindtPy_utils - self.config.logger.info( - 'Unable to optimize MILP main problem ' - 'within time limit. ' - 'Using current solver feasible solution.' - ) copy_var_list_values( main_mip.MindtPy_utils.variable_list, self.fixed_nlp.MindtPy_utils.variable_list, @@ -1932,10 +1930,10 @@ def handle_main_max_timelimit(self, main_mip, main_mip_results): ) self.update_suboptimal_dual_bound(main_mip_results) self.config.logger.info( - self.log_formatter.format( + self.termination_condition_log_formatter.format( self.mip_iter, 'MILP', - value(MindtPy.mip_obj.expr), + 'maxTimeLimit', self.primal_bound, self.dual_bound, self.rel_gap, @@ -1962,8 +1960,18 @@ def handle_main_unbounded(self, main_mip): # to the constraints, and deactivated for the linear main problem. config = self.config MindtPy = main_mip.MindtPy_utils + config.logger.info( + self.termination_condition_log_formatter.format( + self.mip_iter, + 'MILP', + 'Unbounded', + self.primal_bound, + self.dual_bound, + self.rel_gap, + get_main_elapsed_time(self.timing), + ) + ) config.logger.warning( - 'main MILP was unbounded. ' 'Resolving with arbitrary bound values of (-{0:.10g}, {0:.10g}) on the objective. ' 'You can change this bound with the option obj_bound.'.format( config.obj_bound From 6114a3c9048863bc372c5d47644c75f5a2ed49b4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 1 Dec 2023 09:32:56 -0700 Subject: [PATCH 0207/3044] Hack around #3045 by just ignoring things that don't have a ctype when I collect components in the docplex writer --- pyomo/contrib/cp/repn/docplex_writer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 51c3f66140e..50a2d72aed8 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -1005,6 +1005,9 @@ def collect_valid_components(model, active=True, sort=None, valid=set(), targets unrecognized = {} components = {k: [] for k in targets} for obj in model.component_data_objects(active=True, descend_into=True, sort=sort): + # HACK around #3045 + if not hasattr(obj, 'ctype'): + continue ctype = obj.ctype if ctype in components: components[ctype].append(obj) From 1a3ddbb831fafecc156d2f144a7cf308b8340e1b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 1 Dec 2023 15:11:10 -0700 Subject: [PATCH 0208/3044] Adding SequenceVar component and a couple tests --- pyomo/contrib/cp/sequence_var.py | 133 ++++++++++++++++++++ pyomo/contrib/cp/tests/test_sequence_var.py | 56 +++++++++ 2 files changed, 189 insertions(+) create mode 100644 pyomo/contrib/cp/sequence_var.py create mode 100644 pyomo/contrib/cp/tests/test_sequence_var.py diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py new file mode 100644 index 00000000000..d5553cacd20 --- /dev/null +++ b/pyomo/contrib/cp/sequence_var.py @@ -0,0 +1,133 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import logging + +from pyomo.common.log import is_debug_set +from pyomo.common.modeling import NOTSET +from pyomo.contrib.cp import IntervalVar +from pyomo.core import ModelComponentFactory +from pyomo.core.base.component import ActiveComponentData +from pyomo.core.base.global_set import UnindexedComponent_index +from pyomo.core.base.indexed_component import ActiveIndexedComponent + +import sys +from weakref import ref as weakref_ref + +logger = logging.getLogger(__name__) + + +class _SequenceVarData(ActiveComponentData): + """This class defines the abstract interface for a single sequence variable.""" + __slots__ = ('interval_vars',) + def __init__(self, component=None): + # in-lining ActiveComponentData and ComponentData constructors, as is + # traditional: + self._component = weakref_ref(component) if (component is not None) else None + self._index = NOTSET + self._active = True + + # This thing is really just an ordered set of interval vars that we can + # write constraints over. + self.interval_vars = [] + + def set_value(self, expr): + # We'll demand expr be a list for now--it needs to be ordered so this + # doesn't seem like too much to ask + if expr.__class__ is not list: + raise ValueError( + "'expr' for SequenceVar must be a list of IntervalVars. " + "Encountered type '%s' constructing '%s'" % (type(expr), + self.name)) + for v in expr: + if not hasattr(v, 'ctype') or v.ctype is not IntervalVar: + raise ValueError( + "The SequenceVar 'expr' argument must be a list of " + "IntervalVars. The 'expr' for SequenceVar '%s' included " + "an object of type '%s'" % (self.name, type(v))) + self.interval_vars.append(v) + + +@ModelComponentFactory.register("Sequences of IntervalVars") +class SequenceVar(ActiveIndexedComponent): + _ComponentDataClass = _SequenceVarData + + def __new__(cls, *args, **kwds): + if cls != SequenceVar: + return super(SequenceVar, cls).__new__(cls) + if args == (): + return ScalarSequenceVar.__new__(ScalarSequenceVar) + else: + return IndexedSequenceVar.__new__(IndexedSequenceVar) + + def __init__(self, *args, **kwargs): + self._init_rule = kwargs.pop('rule', None) + self._init_expr = kwargs.pop('expr', None) + kwargs.setdefault('ctype', SequenceVar) + super(SequenceVar, self).__init__(*args, **kwargs) + + if self._init_expr is not None and self._init_rule is not None: + raise ValueError( + "Cannot specify both rule= and expr= for SequenceVar %s" % (self.name,) + ) + + def _getitem_when_not_present(self, index): + if index is None and not self.is_indexed(): + obj = self._data[index] = self + else: + obj = self._data[index] = self._ComponentDataClass(component=self) + parent = self.parent_block() + obj._index = index + + if self._init_rule is not None: + obj.interval_vars = self._init_rule(parent, index) + if self._init_expr is not None: + obj.interval_vars = self._init_expr + + return obj + + def construct(self, data=None): + """ + Construct the _SequenceVarData objects for this SequenceVar + """ + if self._constructed: + return + self._constructed = True + + if is_debug_set(logger): + logger.debug("Constructing SequenceVar %s" % self.name) + + # Initialize index in case we hit the exception below + index = None + try: + if not self.is_indexed(): + self._getitem_when_not_present(None) + if self._init_rule is not None: + for index in self.index_set(): + self._getitem_when_not_present(index) + except Exception: + err = sys.exc_info()[1] + logger.error( + "Rule failed when initializing sequence variable for " + "SequenceVar %s with index %s:\n%s: %s" + % (self.name, str(index), type(err).__name__, err) + ) + raise + +class ScalarSequenceVar(_SequenceVarData, SequenceVar): + def __init__(self, *args, **kwds): + _SequenceVarData.__init__(self, component=self) + SequenceVar.__init__(self, *args, **kwds) + self._index = UnindexedComponent_index + + +class IndexedSequenceVar(SequenceVar): + pass diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py new file mode 100644 index 00000000000..9a2278d2de3 --- /dev/null +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -0,0 +1,56 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.cp.interval_var import IntervalVar +from pyomo.contrib.cp.sequence_var import SequenceVar, IndexedSequenceVar +from pyomo.environ import ConcreteModel, Integers, Set, value, Var + + +class TestScalarSequenceVar(unittest.TestCase): + def test_initialize_with_no_data(self): + m = ConcreteModel() + m.i = SequenceVar() + + self.assertIsInstance(m.i, SequenceVar) + self.assertIsInstance(m.i.interval_vars, list) + self.assertEqual(len(m.i.interval_vars), 0) + + def test_initialize_with_expr(self): + m = ConcreteModel() + m.S = Set(initialize=range(3)) + m.i = IntervalVar(m.S, start=(0, 5)) + m.seq = SequenceVar(expr=[m.i[j] for j in m.S]) + self.assertEqual(len(m.seq.interval_vars), 3) + for j in m.S: + self.assertIs(m.seq.interval_vars[j], m.i[j]) + + +class TestIndexedSequenceVar(unittest.TestCase): + def test_initialize_with_rule(self): + m = ConcreteModel() + m.alph = Set(initialize=['a', 'b']) + m.num = Set(initialize=[1, 2]) + m.i = IntervalVar(m.alph, m.num) + + def the_rule(m, j): + return [m.i[j, k] for k in m.num] + m.seq = SequenceVar(m.alph, rule=the_rule) + m.seq.pprint() + + self.assertIsInstance(m.seq, IndexedSequenceVar) + self.assertEqual(len(m.seq), 2) + for j in m.alph: + self.assertTrue(j in m.seq) + self.assertEqual(len(m.seq[j].interval_vars), 2) + for k in m.num: + self.assertIs(m.seq[j].interval_vars[k - 1], m.i[j, k]) + From 9dc5aa6bab909504c6042e2a0c2bd793506896c4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 1 Dec 2023 15:30:49 -0700 Subject: [PATCH 0209/3044] Adding a pretty pprint --- pyomo/contrib/cp/sequence_var.py | 15 ++++++ pyomo/contrib/cp/tests/test_sequence_var.py | 51 +++++++++++++++++++-- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py index d5553cacd20..412c34e9176 100644 --- a/pyomo/contrib/cp/sequence_var.py +++ b/pyomo/contrib/cp/sequence_var.py @@ -122,6 +122,21 @@ def construct(self, data=None): ) raise + def _pprint(self): + """Print component information.""" + headers = [ + ("Size", len(self)), + ("Index", self._index_set if self.is_indexed() else None), + ] + return ( + headers, + self._data.items(), + ("IntervalVars",), + lambda k, v: [ + '[' + ', '.join(iv.name for iv in v.interval_vars) + ']', + ] + ) + class ScalarSequenceVar(_SequenceVarData, SequenceVar): def __init__(self, *args, **kwds): _SequenceVarData.__init__(self, component=self) diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index 9a2278d2de3..da9b5a298d3 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from io import StringIO import pyomo.common.unittest as unittest from pyomo.contrib.cp.interval_var import IntervalVar from pyomo.contrib.cp.sequence_var import SequenceVar, IndexedSequenceVar @@ -24,18 +25,44 @@ def test_initialize_with_no_data(self): self.assertIsInstance(m.i.interval_vars, list) self.assertEqual(len(m.i.interval_vars), 0) - def test_initialize_with_expr(self): + def get_model(self): m = ConcreteModel() m.S = Set(initialize=range(3)) m.i = IntervalVar(m.S, start=(0, 5)) m.seq = SequenceVar(expr=[m.i[j] for j in m.S]) + + return m + + def test_initialize_with_expr(self): + m = self.get_model() self.assertEqual(len(m.seq.interval_vars), 3) for j in m.S: self.assertIs(m.seq.interval_vars[j], m.i[j]) + def test_pprint(self): + m = self.get_model() + buf = StringIO() + m.seq.pprint(ostream=buf) + self.assertEqual( + buf.getvalue().strip(), + """ +seq : Size=1, Index=None + Key : IntervalVars + None : [i[0], i[1], i[2]] + """.strip() + ) class TestIndexedSequenceVar(unittest.TestCase): - def test_initialize_with_rule(self): + def test_initialize_with_not_data(self): + m = ConcreteModel() + m.i = SequenceVar([1, 2]) + + self.assertIsInstance(m.i, IndexedSequenceVar) + for j in [1, 2]: + self.assertIsInstance(m.i[j].interval_vars, list) + self.assertEqual(len(m.i[j].interval_vars), 0) + + def make_model(self): m = ConcreteModel() m.alph = Set(initialize=['a', 'b']) m.num = Set(initialize=[1, 2]) @@ -44,7 +71,11 @@ def test_initialize_with_rule(self): def the_rule(m, j): return [m.i[j, k] for k in m.num] m.seq = SequenceVar(m.alph, rule=the_rule) - m.seq.pprint() + + return m + + def test_initialize_with_rule(self): + m = self.make_model() self.assertIsInstance(m.seq, IndexedSequenceVar) self.assertEqual(len(m.seq), 2) @@ -54,3 +85,17 @@ def the_rule(m, j): for k in m.num: self.assertIs(m.seq[j].interval_vars[k - 1], m.i[j, k]) + def test_pprint(self): + m = self.make_model() + m.seq.pprint() + + buf = StringIO() + m.seq.pprint(ostream=buf) + self.assertEqual( + buf.getvalue().strip(), + """ +seq : Size=2, Index=alph + Key : IntervalVars + a : [i[a,1], i[a,2]] + b : [i[b,1], i[b,2]]""".strip() + ) From 2e6ce49469f3cc868d940aa1b65fb629d69cc719 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 1 Dec 2023 15:41:33 -0700 Subject: [PATCH 0210/3044] pyomo.solver.ipopt: account for presolve when loading results --- pyomo/contrib/appsi/solvers/wntr.py | 15 ++++++--------- pyomo/solver/IPOPT.py | 27 ++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 0a358c6aedf..3d1d36586e0 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -1,11 +1,8 @@ -from pyomo.contrib.appsi.base import ( - PersistentBase, - PersistentSolver, - SolverConfig, - Results, - TerminationCondition, - PersistentSolutionLoader, -) +from pyomo.solver.base import PersistentSolverBase +from pyomo.solver.util import PersistentSolverUtils +from pyomo.solver.config import SolverConfig, ConfigValue +from pyomo.solver.results import Results, TerminationCondition +from pyomo.solver.solution import PersistentSolutionLoader from pyomo.core.expr.numeric_expr import ( ProductExpression, DivisionExpression, @@ -73,7 +70,7 @@ def __init__(self, solver): self.solution_loader = PersistentSolutionLoader(solver=solver) -class Wntr(PersistentBase, PersistentSolver): +class Wntr(PersistentSolverUtils, PersistentSolverBase): def __init__(self, only_child_vars=True): super().__init__(only_child_vars=only_child_vars) self._config = WntrConfig() diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 90a92b0de24..c22b0e39857 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -20,7 +20,7 @@ from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager from pyomo.core.base.label import NumericLabeler -from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo +from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo, AMPLRepn from pyomo.solver.base import SolverBase, SymbolMap from pyomo.solver.config import SolverConfig from pyomo.solver.factory import SolverFactory @@ -155,6 +155,8 @@ class IPOPT(SolverBase): def __init__(self, **kwds): self._config = self.CONFIG(kwds) self._writer = NLWriter() + self._writer.config.skip_trivial_constraints = True + self._writer.config.linear_presolve = True self.ipopt_options = self._config.solver_options def available(self): @@ -279,10 +281,10 @@ def solve(self, model, **kwds): ostreams = [ LogStream( - level=self.config.log_level, logger=self.config.solver_output_logger + level=config.log_level, logger=config.solver_output_logger ) ] - if self.config.tee: + if config.tee: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: process = subprocess.run( @@ -376,6 +378,14 @@ def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): if abs(zu) > abs(rc[v_id][1]): rc[v_id] = (v, zu) + if len(nl_info.eliminated_vars) > 0: + sub_map = {k: v[1] for k, v in sol_data.primals.items()} + for v, v_expr in nl_info.eliminated_vars: + val = evaluate_ampl_repn(v_expr, sub_map) + v_id = id(v) + sub_map[v_id] = val + sol_data.primals[v_id] = (v, val) + res.solution_loader = SolutionLoader( primals=sol_data.primals, duals=sol_data.duals, @@ -384,3 +394,14 @@ def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): ) return res + + +def evaluate_ampl_repn(repn: AMPLRepn, sub_map): + assert not repn.nonlinear + assert repn.nl is None + val = repn.const + if repn.linear is not None: + for v_id, v_coef in repn.linear.items(): + val += v_coef * sub_map[v_id] + val *= repn.mult + return val \ No newline at end of file From 749b4e81dd725a919cc945af039d02baad145af0 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 4 Dec 2023 07:21:08 -0700 Subject: [PATCH 0211/3044] Adding some sequence var tests, making sure we hit set_value when we want error checking --- pyomo/contrib/cp/sequence_var.py | 6 ++-- pyomo/contrib/cp/tests/test_sequence_var.py | 40 +++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py index 412c34e9176..b0691fbd74a 100644 --- a/pyomo/contrib/cp/sequence_var.py +++ b/pyomo/contrib/cp/sequence_var.py @@ -42,7 +42,7 @@ def __init__(self, component=None): def set_value(self, expr): # We'll demand expr be a list for now--it needs to be ordered so this # doesn't seem like too much to ask - if expr.__class__ is not list: + if not hasattr(expr, '__iter__'): raise ValueError( "'expr' for SequenceVar must be a list of IntervalVars. " "Encountered type '%s' constructing '%s'" % (type(expr), @@ -88,9 +88,9 @@ def _getitem_when_not_present(self, index): obj._index = index if self._init_rule is not None: - obj.interval_vars = self._init_rule(parent, index) + obj.set_value(self._init_rule(parent, index)) if self._init_expr is not None: - obj.interval_vars = self._init_expr + obj.set_value(self._init_expr) return obj diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index da9b5a298d3..852d9f2134a 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -25,6 +25,15 @@ def test_initialize_with_no_data(self): self.assertIsInstance(m.i.interval_vars, list) self.assertEqual(len(m.i.interval_vars), 0) + m.iv1 = IntervalVar() + m.iv2 = IntervalVar() + m.i.set_value(expr=[m.iv1, m.iv2]) + + self.assertIsInstance(m.i.interval_vars, list) + self.assertEqual(len(m.i.interval_vars), 2) + self.assertIs(m.i.interval_vars[0], m.iv1) + self.assertIs(m.i.interval_vars[1], m.iv2) + def get_model(self): m = ConcreteModel() m.S = Set(initialize=range(3)) @@ -52,6 +61,27 @@ def test_pprint(self): """.strip() ) + def test_interval_vars_not_a_list(self): + m = self.get_model() + + with self.assertRaisesRegex( + ValueError, + "'expr' for SequenceVar must be a list of IntervalVars. " + "Encountered type '' constructing 'seq2'" + ): + m.seq2 = SequenceVar(expr=1) + + def test_interval_vars_list_includes_things_that_are_not_interval_vars(self): + m = self.get_model() + + with self.assertRaisesRegex( + ValueError, + "The SequenceVar 'expr' argument must be a list of " + "IntervalVars. The 'expr' for SequenceVar 'seq2' included " + "an object of type ''" + ): + m.seq2 = SequenceVar(expr=m.i) + class TestIndexedSequenceVar(unittest.TestCase): def test_initialize_with_not_data(self): m = ConcreteModel() @@ -62,6 +92,16 @@ def test_initialize_with_not_data(self): self.assertIsInstance(m.i[j].interval_vars, list) self.assertEqual(len(m.i[j].interval_vars), 0) + m.iv = IntervalVar() + m.iv2 = IntervalVar([0, 1]) + m.i[2] = [m.iv] + [m.iv2[i] for i in [0, 1]] + + self.assertEqual(len(m.i[2].interval_vars), 3) + self.assertEqual(len(m.i[1].interval_vars), 0) + self.assertIs(m.i[2].interval_vars[0], m.iv) + for i in [0, 1]: + self.assertIs(m.i[2].interval_vars[i + 1], m.iv2[i]) + def make_model(self): m = ConcreteModel() m.alph = Set(initialize=['a', 'b']) From 3f3b3c364fb20c740f83982081d4deccb50ffc2a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 09:44:11 -0700 Subject: [PATCH 0212/3044] Add timing capture and iteration/log parsing --- pyomo/solver/IPOPT.py | 109 +++++++++++++++++++++++++++------------- pyomo/solver/plugins.py | 4 +- pyomo/solver/results.py | 6 ++- 3 files changed, 79 insertions(+), 40 deletions(-) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/IPOPT.py index 90a92b0de24..5e145a5743f 100644 --- a/pyomo/solver/IPOPT.py +++ b/pyomo/solver/IPOPT.py @@ -11,6 +11,7 @@ import os import subprocess +import datetime import io import sys from typing import Mapping @@ -50,7 +51,7 @@ class SolverError(PyomoException): pass -class IPOPTConfig(SolverConfig): +class ipoptConfig(SolverConfig): def __init__( self, description=None, @@ -84,7 +85,7 @@ def __init__( ) -class IPOPTSolutionLoader(SolutionLoaderBase): +class ipoptSolutionLoader(SolutionLoaderBase): pass @@ -148,9 +149,9 @@ class IPOPTSolutionLoader(SolutionLoaderBase): } -@SolverFactory.register('ipopt_v2', doc='The IPOPT NLP solver (new interface)') -class IPOPT(SolverBase): - CONFIG = IPOPTConfig() +@SolverFactory.register('ipopt_v2', doc='The ipopt NLP solver (new interface)') +class ipopt(SolverBase): + CONFIG = ipoptConfig() def __init__(self, **kwds): self._config = self.CONFIG(kwds) @@ -193,7 +194,7 @@ def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): if k not in ipopt_command_line_options: f.write(str(k) + ' ' + str(val) + '\n') - def _create_command_line(self, basename: str, config: IPOPTConfig): + def _create_command_line(self, basename: str, config: ipoptConfig): cmd = [ str(config.executable), basename + '.nl', @@ -202,8 +203,8 @@ def _create_command_line(self, basename: str, config: IPOPTConfig): ] if 'option_file_name' in config.solver_options: raise ValueError( - 'Use IPOPT.config.temp_dir to specify the name of the options file. ' - 'Do not use IPOPT.config.solver_options["option_file_name"].' + 'Use ipopt.config.temp_dir to specify the name of the options file. ' + 'Do not use ipopt.config.solver_options["option_file_name"].' ) self.ipopt_options = dict(config.solver_options) if config.time_limit is not None and 'max_cpu_time' not in self.ipopt_options: @@ -214,25 +215,15 @@ def _create_command_line(self, basename: str, config: IPOPTConfig): return cmd def solve(self, model, **kwds): + # Begin time tracking + start_timestamp = datetime.datetime.now(datetime.timezone.utc) # Check if solver is available avail = self.available() if not avail: raise SolverError(f'Solver {self.__class__} is not available ({avail}).') # Update configuration options, based on keywords passed to solve - config: IPOPTConfig = self.config(kwds.pop('options', {})) + config: ipoptConfig = self.config(kwds.pop('options', {})) config.set_value(kwds) - # Get a copy of the environment to pass to the subprocess - env = os.environ.copy() - if 'PYOMO_AMPLFUNC' in env: - env['AMPLFUNC'] = "\n".join( - filter( - None, (env.get('AMPLFUNC', None), env.get('PYOMO_AMPLFUNC', None)) - ) - ) - # Need to add check for symbolic_solver_labels; may need to generate up - # to three files for nl, row, col, if ssl == True - # What we have here may or may not work with IPOPT; will find out when - # we try to run it. with TempfileManager.new_context() as tempfile: if config.temp_dir is None: dname = tempfile.mkdtemp() @@ -248,24 +239,30 @@ def solve(self, model, **kwds): with open(basename + '.nl', 'w') as nl_file, open( basename + '.row', 'w' ) as row_file, open(basename + '.col', 'w') as col_file: - self.info = self._writer.write( + nl_info = self._writer.write( model, nl_file, row_file, col_file, symbolic_solver_labels=config.symbolic_solver_labels, ) + # Get a copy of the environment to pass to the subprocess + env = os.environ.copy() + if nl_info.external_function_libraries: + if env.get('AMPLFUNC'): + nl_info.external_function_libraries.append(env.get('AMPLFUNC')) + env['AMPLFUNC'] = "\n".join(nl_info.external_function_libraries) symbol_map = self._symbol_map = SymbolMap() labeler = NumericLabeler('component') - for v in self.info.variables: + for v in nl_info.variables: symbol_map.getSymbol(v, labeler) - for c in self.info.constraints: + for c in nl_info.constraints: symbol_map.getSymbol(c, labeler) with open(basename + '.opt', 'w') as opt_file: self._write_options_file( ostream=opt_file, options=config.solver_options ) - # Call IPOPT - passing the files via the subprocess + # Call ipopt - passing the files via the subprocess cmd = self._create_command_line(basename=basename, config=config) # this seems silly, but we have to give the subprocess slightly longer to finish than @@ -277,13 +274,15 @@ def solve(self, model, **kwds): else: timeout = None - ostreams = [ - LogStream( - level=self.config.log_level, logger=self.config.solver_output_logger - ) - ] - if self.config.tee: + ostreams = [io.StringIO()] + if config.tee: ostreams.append(sys.stdout) + else: + ostreams.append( + LogStream( + level=config.log_level, logger=config.solver_output_logger + ) + ) with TeeStream(*ostreams) as t: process = subprocess.run( cmd, @@ -293,16 +292,19 @@ def solve(self, model, **kwds): stdout=t.STDOUT, stderr=t.STDERR, ) + # This is the stuff we need to parse to get the iterations + # and time + iters, solver_time = self._parse_ipopt_output(ostreams[0]) if process.returncode != 0: results = Results() results.termination_condition = TerminationCondition.error results.solution_loader = SolutionLoader(None, None, None, None) else: - # TODO: Make a context manager out of this and open the file - # to pass to the results, instead of doing this thing. with open(basename + '.sol', 'r') as sol_file: - results = self._parse_solution(sol_file, self.info) + results = self._parse_solution(sol_file, nl_info) + results.iteration_count = iters + results.timing_info.solver_wall_time = solver_time if ( config.raise_exception_on_nonoptimal_result @@ -340,10 +342,10 @@ def solve(self, model, **kwds): if results.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal}: if config.load_solution: - results.incumbent_objective = value(self.info.objectives[0]) + results.incumbent_objective = value(nl_info.objectives[0]) else: results.incumbent_objective = replace_expressions( - self.info.objectives[0].expr, + nl_info.objectives[0].expr, substitution_map={ id(v): val for v, val in results.solution_loader.get_primals().items() @@ -352,8 +354,43 @@ def solve(self, model, **kwds): remove_named_expressions=True, ) + # Capture/record end-time / wall-time + end_timestamp = datetime.datetime.now(datetime.timezone.utc) + results.timing_info.start_timestamp = start_timestamp + results.timing_info.wall_time = ( + end_timestamp - start_timestamp + ).total_seconds() return results + def _parse_ipopt_output(self, stream: io.StringIO): + """ + Parse an IPOPT output file and return: + + * number of iterations + * time in IPOPT + + """ + + iters = None + time = None + # parse the output stream to get the iteration count and solver time + for line in stream.getvalue().splitlines(): + if line.startswith("Number of Iterations....:"): + tokens = line.split() + iters = int(tokens[3]) + elif line.startswith( + "Total CPU secs in IPOPT (w/o function evaluations) =" + ): + tokens = line.split() + time = float(tokens[9]) + elif line.startswith( + "Total CPU secs in NLP function evaluations =" + ): + tokens = line.split() + time += float(tokens[8]) + + return iters, time + def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): suffixes_to_read = ['dual', 'ipopt_zL_out', 'ipopt_zU_out'] res, sol_data = parse_sol_file( diff --git a/pyomo/solver/plugins.py b/pyomo/solver/plugins.py index 2f95ca9f410..54d03eaf74b 100644 --- a/pyomo/solver/plugins.py +++ b/pyomo/solver/plugins.py @@ -11,10 +11,10 @@ from .factory import SolverFactory -from .IPOPT import IPOPT +from .ipopt import ipopt def load(): SolverFactory.register(name='ipopt_v2', doc='The IPOPT NLP solver (new interface)')( - IPOPT + ipopt ) diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 728e47fc7a1..71e92a1539f 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -228,9 +228,11 @@ def __init__( ) self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict()) - self.timing_info.start_time: datetime = self.timing_info.declare( - 'start_time', ConfigValue(domain=Datetime) + self.timing_info.start_timestamp: datetime = self.timing_info.declare( + 'start_timestamp', ConfigValue(domain=Datetime) ) + # wall_time is the actual standard (until Michael complains) that is + # required for everyone. This is from entry->exit of the solve method. self.timing_info.wall_time: Optional[float] = self.timing_info.declare( 'wall_time', ConfigValue(domain=NonNegativeFloat) ) From 5138ae8c6f7fdcef1ac954db2b617e2417b9cd26 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 09:55:37 -0700 Subject: [PATCH 0213/3044] Change to lowercase ipopt --- pyomo/solver/{IPOPT.py => ipopt.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pyomo/solver/{IPOPT.py => ipopt.py} (100%) diff --git a/pyomo/solver/IPOPT.py b/pyomo/solver/ipopt.py similarity index 100% rename from pyomo/solver/IPOPT.py rename to pyomo/solver/ipopt.py From 5a9e5e598660f99a85adac16e29d7b69ce2c012c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 09:58:16 -0700 Subject: [PATCH 0214/3044] Blackify --- pyomo/solver/ipopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index 6196d230ec3..8f987c0e3c7 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -441,4 +441,4 @@ def evaluate_ampl_repn(repn: AMPLRepn, sub_map): for v_id, v_coef in repn.linear.items(): val += v_coef * sub_map[v_id] val *= repn.mult - return val \ No newline at end of file + return val From a503d45b7989a1c8363a518bbdeeed65a9bb946d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 10:04:02 -0700 Subject: [PATCH 0215/3044] Test file needed updated --- pyomo/solver/tests/solvers/test_ipopt.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/solver/tests/solvers/test_ipopt.py b/pyomo/solver/tests/solvers/test_ipopt.py index 8cf046fcfef..abf7287489f 100644 --- a/pyomo/solver/tests/solvers/test_ipopt.py +++ b/pyomo/solver/tests/solvers/test_ipopt.py @@ -13,12 +13,12 @@ import pyomo.environ as pyo from pyomo.common.fileutils import ExecutableData from pyomo.common.config import ConfigDict -from pyomo.solver.IPOPT import IPOPTConfig +from pyomo.solver.ipopt import ipoptConfig from pyomo.solver.factory import SolverFactory from pyomo.common import unittest -class TestIPOPT(unittest.TestCase): +class TestIpopt(unittest.TestCase): def create_model(self): model = pyo.ConcreteModel() model.x = pyo.Var(initialize=1.5) @@ -30,9 +30,9 @@ def rosenbrock(m): model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) return model - def test_IPOPT_config(self): + def test_ipopt_config(self): # Test default initialization - config = IPOPTConfig() + config = ipoptConfig() self.assertTrue(config.load_solution) self.assertIsInstance(config.solver_options, ConfigDict) print(type(config.executable)) From d04e1f8d7d5f39143022f62688ff06cbba1f5e30 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 10:10:20 -0700 Subject: [PATCH 0216/3044] Anotther test was incorrect --- pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py index d250923f104..1644eab4008 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py @@ -1,6 +1,6 @@ import pyomo.environ as pe import pyomo.common.unittest as unittest -from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver +from pyomo.solver.results import TerminationCondition from pyomo.contrib.appsi.solvers.wntr import Wntr, wntr_available import math From 6588232f7f93345da5e159a2eddc05d95db347b2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 10:19:01 -0700 Subject: [PATCH 0217/3044] Remove cmodel extensions --- pyomo/contrib/appsi/solvers/wntr.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 3d1d36586e0..5b7f2de8592 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -1,6 +1,6 @@ from pyomo.solver.base import PersistentSolverBase from pyomo.solver.util import PersistentSolverUtils -from pyomo.solver.config import SolverConfig, ConfigValue +from pyomo.solver.config import SolverConfig from pyomo.solver.results import Results, TerminationCondition from pyomo.solver.solution import PersistentSolutionLoader from pyomo.core.expr.numeric_expr import ( @@ -33,7 +33,6 @@ from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.dependencies import attempt_import from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available wntr, wntr_available = attempt_import('wntr') import logging @@ -209,8 +208,6 @@ def set_instance(self, model): ) self._reinit() self._model = model - if self.use_extensions and cmodel_available: - self._expr_types = cmodel.PyomoExprTypes() if self.config.symbolic_solver_labels: self._labeler = TextLabeler() From 77f65969bf0b16baff7989927fa67f9223b95c0f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 10:30:57 -0700 Subject: [PATCH 0218/3044] More conversion in Wntr needed --- pyomo/contrib/appsi/solvers/wntr.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 5b7f2de8592..649b9aa2479 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -1,7 +1,7 @@ from pyomo.solver.base import PersistentSolverBase from pyomo.solver.util import PersistentSolverUtils from pyomo.solver.config import SolverConfig -from pyomo.solver.results import Results, TerminationCondition +from pyomo.solver.results import Results, TerminationCondition, SolutionStatus from pyomo.solver.solution import PersistentSolutionLoader from pyomo.core.expr.numeric_expr import ( ProductExpression, @@ -122,7 +122,7 @@ def _solve(self, timer: HierarchicalTimer): options.update(self.wntr_options) opt = wntr.sim.solvers.NewtonSolver(options) - if self.config.stream_solver: + if self.config.tee: ostream = sys.stdout else: ostream = None @@ -139,13 +139,12 @@ def _solve(self, timer: HierarchicalTimer): tf = time.time() results = WntrResults(self) - results.wallclock_time = tf - t0 + results.timing_info.wall_time = tf - t0 if status == wntr.sim.solvers.SolverStatus.converged: - results.termination_condition = TerminationCondition.optimal + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.solution_status = SolutionStatus.optimal else: results.termination_condition = TerminationCondition.error - results.best_feasible_objective = None - results.best_objective_bound = None if self.config.load_solution: if status == wntr.sim.solvers.SolverStatus.converged: @@ -157,7 +156,7 @@ def _solve(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.best_feasible_objective before loading a solution.' + 'results.incumbent_objective before loading a solution.' ) return results From 7f76ff4bf0b1ae62d1166b003e8a1cff9cd13aa6 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 10:33:56 -0700 Subject: [PATCH 0219/3044] Update Results test --- pyomo/solver/tests/test_results.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/test_results.py index 5392c1135f8..bf822594002 100644 --- a/pyomo/solver/tests/test_results.py +++ b/pyomo/solver/tests/test_results.py @@ -99,7 +99,7 @@ def test_uninitialized(self): self.assertIsNone(res.iteration_count) self.assertIsInstance(res.timing_info, ConfigDict) self.assertIsInstance(res.extra_info, ConfigDict) - self.assertIsNone(res.timing_info.start_time) + self.assertIsNone(res.timing_info.start_timestamp) self.assertIsNone(res.timing_info.wall_time) self.assertIsNone(res.timing_info.solver_wall_time) res.solution_loader = solution.SolutionLoader(None, None, None, None) From 09e3fe946f49b92263116413de298817cb88a431 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 10:36:56 -0700 Subject: [PATCH 0220/3044] Blackify --- pyomo/contrib/appsi/solvers/wntr.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 649b9aa2479..70af135e681 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -141,7 +141,9 @@ def _solve(self, timer: HierarchicalTimer): results = WntrResults(self) results.timing_info.wall_time = tf - t0 if status == wntr.sim.solvers.SolverStatus.converged: - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) results.solution_status = SolutionStatus.optimal else: results.termination_condition = TerminationCondition.error From f26625eaf8cd9b345132d9d2bb106accb820cdce Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 10:43:52 -0700 Subject: [PATCH 0221/3044] wallclock attribute no longer valid --- pyomo/contrib/appsi/solvers/wntr.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 70af135e681..aaa130f8631 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -65,7 +65,6 @@ def __init__( class WntrResults(Results): def __init__(self, solver): super().__init__() - self.wallclock_time = None self.solution_loader = PersistentSolutionLoader(solver=solver) From a2efd8d8931e85644d50ca07163b807c096cde31 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 11:08:57 -0700 Subject: [PATCH 0222/3044] Update TerminationCondition and SolutionStatus checks --- .../solvers/tests/test_wntr_persistent.py | 59 ++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py index 1644eab4008..50058262488 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py @@ -1,6 +1,6 @@ import pyomo.environ as pe import pyomo.common.unittest as unittest -from pyomo.solver.results import TerminationCondition +from pyomo.solver.results import TerminationCondition, SolutionStatus from pyomo.contrib.appsi.solvers.wntr import Wntr, wntr_available import math @@ -18,12 +18,14 @@ def test_param_updates(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 1) m.p.value = 2 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 2) def test_remove_add_constraint(self): @@ -36,7 +38,8 @@ def test_remove_add_constraint(self): opt.config.symbolic_solver_labels = True opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) @@ -45,7 +48,8 @@ def test_remove_add_constraint(self): m.x.value = 0.5 m.y.value = 0.5 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 0) @@ -58,21 +62,24 @@ def test_fixed_var(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) self.assertAlmostEqual(m.y.value, 0.25) m.x.unfix() m.c2 = pe.Constraint(expr=m.y == pe.exp(m.x)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) m.x.fix(0.5) del m.c2 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) self.assertAlmostEqual(m.y.value, 0.25) @@ -89,7 +96,8 @@ def test_remove_variables_params(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 1) self.assertAlmostEqual(m.z.value, 0) @@ -100,14 +108,16 @@ def test_remove_variables_params(self): m.z.value = 2 m.px.value = 2 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 2) self.assertAlmostEqual(m.z.value, 2) del m.z m.px.value = 3 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 3) def test_get_primals(self): @@ -120,7 +130,8 @@ def test_get_primals(self): opt.config.load_solution = False opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, None) primals = opt.get_primals() @@ -134,49 +145,57 @@ def test_operators(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 2) del m.c1 m.x.value = 0 m.c1 = pe.Constraint(expr=pe.sin(m.x) == math.sin(math.pi / 4)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, math.pi / 4) del m.c1 m.c1 = pe.Constraint(expr=pe.cos(m.x) == 0) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, math.pi / 2) del m.c1 m.c1 = pe.Constraint(expr=pe.tan(m.x) == 1) m.x.value = 0 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, math.pi / 4) del m.c1 m.c1 = pe.Constraint(expr=pe.asin(m.x) == math.asin(0.5)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) del m.c1 m.c1 = pe.Constraint(expr=pe.acos(m.x) == math.acos(0.6)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.6) del m.c1 m.c1 = pe.Constraint(expr=pe.atan(m.x) == math.atan(0.5)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) del m.c1 m.c1 = pe.Constraint(expr=pe.sqrt(m.x) == math.sqrt(0.6)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.6) From 682c8b8aff32599d09d47478fba76c972e8ea706 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Dec 2023 11:10:54 -0700 Subject: [PATCH 0223/3044] Blackify... again --- .../solvers/tests/test_wntr_persistent.py | 76 ++++++++++++++----- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py index 50058262488..971305001a9 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py @@ -18,13 +18,17 @@ def test_param_updates(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 1) m.p.value = 2 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 2) @@ -38,7 +42,9 @@ def test_remove_add_constraint(self): opt.config.symbolic_solver_labels = True opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) @@ -48,7 +54,9 @@ def test_remove_add_constraint(self): m.x.value = 0.5 m.y.value = 0.5 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 0) @@ -62,7 +70,9 @@ def test_fixed_var(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) self.assertAlmostEqual(m.y.value, 0.25) @@ -70,7 +80,9 @@ def test_fixed_var(self): m.x.unfix() m.c2 = pe.Constraint(expr=m.y == pe.exp(m.x)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) @@ -78,7 +90,9 @@ def test_fixed_var(self): m.x.fix(0.5) del m.c2 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) self.assertAlmostEqual(m.y.value, 0.25) @@ -96,7 +110,9 @@ def test_remove_variables_params(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 1) @@ -108,7 +124,9 @@ def test_remove_variables_params(self): m.z.value = 2 m.px.value = 2 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 2) self.assertAlmostEqual(m.z.value, 2) @@ -116,7 +134,9 @@ def test_remove_variables_params(self): del m.z m.px.value = 3 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 3) @@ -130,7 +150,9 @@ def test_get_primals(self): opt.config.load_solution = False opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, None) @@ -145,7 +167,9 @@ def test_operators(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 2) @@ -153,14 +177,18 @@ def test_operators(self): m.x.value = 0 m.c1 = pe.Constraint(expr=pe.sin(m.x) == math.sin(math.pi / 4)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, math.pi / 4) del m.c1 m.c1 = pe.Constraint(expr=pe.cos(m.x) == 0) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, math.pi / 2) @@ -168,34 +196,44 @@ def test_operators(self): m.c1 = pe.Constraint(expr=pe.tan(m.x) == 1) m.x.value = 0 res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, math.pi / 4) del m.c1 m.c1 = pe.Constraint(expr=pe.asin(m.x) == math.asin(0.5)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) del m.c1 m.c1 = pe.Constraint(expr=pe.acos(m.x) == math.acos(0.6)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.6) del m.c1 m.c1 = pe.Constraint(expr=pe.atan(m.x) == math.atan(0.5)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.5) del m.c1 m.c1 = pe.Constraint(expr=pe.sqrt(m.x) == math.sqrt(0.6)) res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied) + self.assertEqual( + res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied + ) self.assertEqual(res.solution_status, SolutionStatus.optimal) self.assertAlmostEqual(m.x.value, 0.6) From 39eb268829ebb2184cc8d62d60d16c3acdc9079e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 5 Dec 2023 14:43:43 -0700 Subject: [PATCH 0224/3044] Apply options files updates --- pyomo/common/tests/test_config.py | 2 +- pyomo/solver/ipopt.py | 126 +++++++++++++++++++++--------- pyomo/solver/results.py | 12 +-- 3 files changed, 92 insertions(+), 48 deletions(-) diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 9bafd852eb9..bf6786ba2a0 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -1473,7 +1473,7 @@ def test_parseDisplay_userdata_add_block_nonDefault(self): self.config.add("bar", ConfigDict(implicit=True)).add("baz", ConfigDict()) test = _display(self.config, 'userdata') sys.stdout.write(test) - self.assertEqual(yaml_load(test), {'bar': {'baz': None}, foo: 0}) + self.assertEqual(yaml_load(test), {'bar': {'baz': None}, 'foo': 0}) @unittest.skipIf(not yaml_available, "Test requires PyYAML") def test_parseDisplay_userdata_add_block(self): diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index 8f987c0e3c7..fbe7c0f5604 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -14,10 +14,10 @@ import datetime import io import sys -from typing import Mapping +from typing import Mapping, Optional from pyomo.common import Executable -from pyomo.common.config import ConfigValue, NonNegativeInt +from pyomo.common.config import ConfigValue, NonNegativeInt, NonNegativeFloat from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager from pyomo.core.base.label import NumericLabeler @@ -43,7 +43,7 @@ logger = logging.getLogger(__name__) -class SolverError(PyomoException): +class ipoptSolverError(PyomoException): """ General exception to catch solver system errors """ @@ -74,6 +74,7 @@ def __init__( self.save_solver_io: bool = self.declare( 'save_solver_io', ConfigValue(domain=bool, default=False) ) + # TODO: Add in a deprecation here for keepfiles self.temp_dir: str = self.declare( 'temp_dir', ConfigValue(domain=str, default=None) ) @@ -85,6 +86,34 @@ def __init__( ) +class ipoptResults(Results): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + self.timing_info.no_function_solve_time: Optional[ + float + ] = self.timing_info.declare( + 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) + ) + self.timing_info.function_solve_time: Optional[ + float + ] = self.timing_info.declare( + 'function_solve_time', ConfigValue(domain=NonNegativeFloat) + ) + + class ipoptSolutionLoader(SolutionLoaderBase): pass @@ -190,30 +219,37 @@ def config(self, val): def symbol_map(self): return self._symbol_map - def _write_options_file(self, ostream: io.TextIOBase, options: Mapping): - f = ostream - for k, val in options.items(): - if k not in ipopt_command_line_options: - f.write(str(k) + ' ' + str(val) + '\n') - - def _create_command_line(self, basename: str, config: ipoptConfig): - cmd = [ - str(config.executable), - basename + '.nl', - '-AMPL', - 'option_file_name=' + basename + '.opt', - ] + def _write_options_file(self, filename: str, options: Mapping): + # First we need to determine if we even need to create a file. + # If options is empty, then we return False + opt_file_exists = False + if not options: + return False + # If it has options in it, parse them and write them to a file. + # If they are command line options, ignore them; they will be + # parsed during _create_command_line + with open(filename + '.opt', 'w') as opt_file: + for k, val in options.items(): + if k not in ipopt_command_line_options: + opt_file_exists = True + opt_file.write(str(k) + ' ' + str(val) + '\n') + return opt_file_exists + + def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: bool): + cmd = [str(config.executable), basename + '.nl', '-AMPL'] + if opt_file: + cmd.append('option_file_name=' + basename + '.opt') if 'option_file_name' in config.solver_options: raise ValueError( - 'Use ipopt.config.temp_dir to specify the name of the options file. ' - 'Do not use ipopt.config.solver_options["option_file_name"].' + 'Pyomo generates the ipopt options file as part of the solve method. ' + 'Add all options to ipopt.config.solver_options instead.' ) self.ipopt_options = dict(config.solver_options) if config.time_limit is not None and 'max_cpu_time' not in self.ipopt_options: self.ipopt_options['max_cpu_time'] = config.time_limit - for k, v in self.ipopt_options.items(): - cmd.append(str(k) + '=' + str(v)) - + for k, val in self.ipopt_options.items(): + if k in ipopt_command_line_options: + cmd.append(str(k) + '=' + str(val)) return cmd def solve(self, model, **kwds): @@ -222,10 +258,13 @@ def solve(self, model, **kwds): # Check if solver is available avail = self.available() if not avail: - raise SolverError(f'Solver {self.__class__} is not available ({avail}).') + raise ipoptSolverError( + f'Solver {self.__class__} is not available ({avail}).' + ) # Update configuration options, based on keywords passed to solve config: ipoptConfig = self.config(kwds.pop('options', {})) config.set_value(kwds) + results = ipoptResults() with TempfileManager.new_context() as tempfile: if config.temp_dir is None: dname = tempfile.mkdtemp() @@ -260,13 +299,15 @@ def solve(self, model, **kwds): symbol_map.getSymbol(v, labeler) for c in nl_info.constraints: symbol_map.getSymbol(c, labeler) - with open(basename + '.opt', 'w') as opt_file: - self._write_options_file( - ostream=opt_file, options=config.solver_options - ) + # Write the opt_file, if there should be one; return a bool to say + # whether or not we have one (so we can correctly build the command line) + opt_file = self._write_options_file( + filename=basename, options=config.solver_options + ) # Call ipopt - passing the files via the subprocess - cmd = self._create_command_line(basename=basename, config=config) - + cmd = self._create_command_line( + basename=basename, config=config, opt_file=opt_file + ) # this seems silly, but we have to give the subprocess slightly longer to finish than # ipopt if config.time_limit is not None: @@ -296,18 +337,19 @@ def solve(self, model, **kwds): ) # This is the stuff we need to parse to get the iterations # and time - iters, solver_time = self._parse_ipopt_output(ostreams[0]) + iters, ipopt_time_nofunc, ipopt_time_func = self._parse_ipopt_output( + ostreams[0] + ) if process.returncode != 0: - results = Results() results.termination_condition = TerminationCondition.error results.solution_loader = SolutionLoader(None, None, None, None) else: with open(basename + '.sol', 'r') as sol_file: - results = self._parse_solution(sol_file, nl_info) + results = self._parse_solution(sol_file, nl_info, results) results.iteration_count = iters - results.timing_info.solver_wall_time = solver_time - + results.timing_info.no_function_solve_time = ipopt_time_nofunc + results.timing_info.function_solve_time = ipopt_time_func if ( config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal @@ -374,7 +416,8 @@ def _parse_ipopt_output(self, stream: io.StringIO): """ iters = None - time = None + nofunc_time = None + func_time = None # parse the output stream to get the iteration count and solver time for line in stream.getvalue().splitlines(): if line.startswith("Number of Iterations....:"): @@ -384,19 +427,24 @@ def _parse_ipopt_output(self, stream: io.StringIO): "Total CPU secs in IPOPT (w/o function evaluations) =" ): tokens = line.split() - time = float(tokens[9]) + nofunc_time = float(tokens[9]) elif line.startswith( "Total CPU secs in NLP function evaluations =" ): tokens = line.split() - time += float(tokens[8]) + func_time = float(tokens[8]) - return iters, time + return iters, nofunc_time, func_time - def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): + def _parse_solution( + self, instream: io.TextIOBase, nl_info: NLWriterInfo, result: ipoptResults + ): suffixes_to_read = ['dual', 'ipopt_zL_out', 'ipopt_zU_out'] res, sol_data = parse_sol_file( - sol_file=instream, nl_info=nl_info, suffixes_to_read=suffixes_to_read + sol_file=instream, + nl_info=nl_info, + suffixes_to_read=suffixes_to_read, + result=result, ) if res.solution_status == SolutionStatus.noSolution: diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 71e92a1539f..0aa78bef6bc 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -231,14 +231,9 @@ def __init__( self.timing_info.start_timestamp: datetime = self.timing_info.declare( 'start_timestamp', ConfigValue(domain=Datetime) ) - # wall_time is the actual standard (until Michael complains) that is - # required for everyone. This is from entry->exit of the solve method. self.timing_info.wall_time: Optional[float] = self.timing_info.declare( 'wall_time', ConfigValue(domain=NonNegativeFloat) ) - self.timing_info.solver_wall_time: Optional[float] = self.timing_info.declare( - 'solver_wall_time', ConfigValue(domain=NonNegativeFloat) - ) self.extra_info: ConfigDict = self.declare( 'extra_info', ConfigDict(implicit=True) ) @@ -267,7 +262,10 @@ def __init__(self) -> None: def parse_sol_file( - sol_file: io.TextIOBase, nl_info: NLWriterInfo, suffixes_to_read: Sequence[str] + sol_file: io.TextIOBase, + nl_info: NLWriterInfo, + suffixes_to_read: Sequence[str], + result: Results, ) -> Tuple[Results, SolFileData]: suffixes_to_read = set(suffixes_to_read) sol_data = SolFileData() @@ -275,8 +273,6 @@ def parse_sol_file( # # Some solvers (minto) do not write a message. We will assume # all non-blank lines up the 'Options' line is the message. - result = Results() - # For backwards compatibility and general safety, we will parse all # lines until "Options" appears. Anything before "Options" we will # consider to be the solver message. From 396ebce1e962fe2423e89b6c512aa6fe859317bb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 5 Dec 2023 14:54:06 -0700 Subject: [PATCH 0225/3044] Fix tests; add TODO notes --- pyomo/solver/ipopt.py | 1 + pyomo/solver/tests/solvers/test_ipopt.py | 9 +++++++++ pyomo/solver/tests/test_results.py | 1 - 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index fbe7c0f5604..092b279269b 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -186,6 +186,7 @@ def __init__(self, **kwds): self._config = self.CONFIG(kwds) self._writer = NLWriter() self._writer.config.skip_trivial_constraints = True + # TODO: Make this an option; not always turned on self._writer.config.linear_presolve = True self.ipopt_options = self._config.solver_options diff --git a/pyomo/solver/tests/solvers/test_ipopt.py b/pyomo/solver/tests/solvers/test_ipopt.py index abf7287489f..e157321b4cc 100644 --- a/pyomo/solver/tests/solvers/test_ipopt.py +++ b/pyomo/solver/tests/solvers/test_ipopt.py @@ -18,6 +18,15 @@ from pyomo.common import unittest +""" +TODO: + - Test unique configuration options + - Test unique results options + - Ensure that `*.opt` file is only created when needed + - Ensure options are correctly parsing to env or opt file + - Failures at appropriate times +""" + class TestIpopt(unittest.TestCase): def create_model(self): model = pyo.ConcreteModel() diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/test_results.py index bf822594002..0c0b4bb18db 100644 --- a/pyomo/solver/tests/test_results.py +++ b/pyomo/solver/tests/test_results.py @@ -101,7 +101,6 @@ def test_uninitialized(self): self.assertIsInstance(res.extra_info, ConfigDict) self.assertIsNone(res.timing_info.start_timestamp) self.assertIsNone(res.timing_info.wall_time) - self.assertIsNone(res.timing_info.solver_wall_time) res.solution_loader = solution.SolutionLoader(None, None, None, None) with self.assertRaisesRegex( From e19d440800260b973847fdc51f5c88ea7ac1dfb3 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 5 Dec 2023 14:56:08 -0700 Subject: [PATCH 0226/3044] Blackify - adding a single empty space --- pyomo/solver/tests/solvers/test_ipopt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/solver/tests/solvers/test_ipopt.py b/pyomo/solver/tests/solvers/test_ipopt.py index e157321b4cc..d9fccbb84fc 100644 --- a/pyomo/solver/tests/solvers/test_ipopt.py +++ b/pyomo/solver/tests/solvers/test_ipopt.py @@ -27,6 +27,7 @@ - Failures at appropriate times """ + class TestIpopt(unittest.TestCase): def create_model(self): model = pyo.ConcreteModel() From d22946164ff018f41d42d9f554090b46fedac371 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 7 Dec 2023 13:29:39 -0500 Subject: [PATCH 0227/3044] fix greybox cuts bug --- pyomo/contrib/mindtpy/cut_generation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/cut_generation.py b/pyomo/contrib/mindtpy/cut_generation.py index e57cfd2eada..4ee7a6ff07b 100644 --- a/pyomo/contrib/mindtpy/cut_generation.py +++ b/pyomo/contrib/mindtpy/cut_generation.py @@ -210,8 +210,8 @@ def add_oa_cuts_for_grey_box( target_model_grey_box.inputs.values() ) ) + - (output - value(output)) ) - - (output - value(output)) - (slack_var if config.add_slack else 0) <= 0 ) From 3d1db1363f5e96069ef5f6deafe458a83f0fb0fe Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 7 Dec 2023 16:21:59 -0500 Subject: [PATCH 0228/3044] redesign calc_jacobians function --- pyomo/contrib/mindtpy/extended_cutting_plane.py | 4 +++- pyomo/contrib/mindtpy/feasibility_pump.py | 4 +++- pyomo/contrib/mindtpy/outer_approximation.py | 4 +++- pyomo/contrib/mindtpy/util.py | 10 +++++----- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/mindtpy/extended_cutting_plane.py b/pyomo/contrib/mindtpy/extended_cutting_plane.py index 3a09af155a0..08c89a4c5f0 100644 --- a/pyomo/contrib/mindtpy/extended_cutting_plane.py +++ b/pyomo/contrib/mindtpy/extended_cutting_plane.py @@ -86,7 +86,9 @@ def check_config(self): def initialize_mip_problem(self): '''Deactivate the nonlinear constraints to create the MIP problem.''' super().initialize_mip_problem() - self.jacobians = calc_jacobians(self.mip, self.config) # preload jacobians + self.jacobians = calc_jacobians( + self.mip, self.config.differentiate_mode + ) # preload jacobians self.mip.MindtPy_utils.cuts.ecp_cuts = ConstraintList( doc='Extended Cutting Planes' ) diff --git a/pyomo/contrib/mindtpy/feasibility_pump.py b/pyomo/contrib/mindtpy/feasibility_pump.py index 990f56b8f93..bf6fb8f84bb 100644 --- a/pyomo/contrib/mindtpy/feasibility_pump.py +++ b/pyomo/contrib/mindtpy/feasibility_pump.py @@ -46,7 +46,9 @@ def check_config(self): def initialize_mip_problem(self): '''Deactivate the nonlinear constraints to create the MIP problem.''' super().initialize_mip_problem() - self.jacobians = calc_jacobians(self.mip, self.config) # preload jacobians + self.jacobians = calc_jacobians( + self.mip, self.config.differentiate_mode + ) # preload jacobians self.mip.MindtPy_utils.cuts.oa_cuts = ConstraintList( doc='Outer approximation cuts' ) diff --git a/pyomo/contrib/mindtpy/outer_approximation.py b/pyomo/contrib/mindtpy/outer_approximation.py index 6cf0b26cb37..4fd140a0bba 100644 --- a/pyomo/contrib/mindtpy/outer_approximation.py +++ b/pyomo/contrib/mindtpy/outer_approximation.py @@ -96,7 +96,9 @@ def check_config(self): def initialize_mip_problem(self): '''Deactivate the nonlinear constraints to create the MIP problem.''' super().initialize_mip_problem() - self.jacobians = calc_jacobians(self.mip, self.config) # preload jacobians + self.jacobians = calc_jacobians( + self.mip, self.config.differentiate_mode + ) # preload jacobians self.mip.MindtPy_utils.cuts.oa_cuts = ConstraintList( doc='Outer approximation cuts' ) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index ec2829c6a18..5ca4604d37e 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -41,7 +41,7 @@ numpy = attempt_import('numpy')[0] -def calc_jacobians(model, config): +def calc_jacobians(model, differentiate_mode): """Generates a map of jacobians for the variables in the model. This function generates a map of jacobians corresponding to the variables in the @@ -51,15 +51,15 @@ def calc_jacobians(model, config): ---------- model : Pyomo model Target model to calculate jacobian. - config : ConfigBlock - The specific configurations for MindtPy. + differentiate_mode : String + The differentiate mode to calculate Jacobians. """ # Map nonlinear_constraint --> Map( # variable --> jacobian of constraint w.r.t. variable) jacobians = ComponentMap() - if config.differentiate_mode == 'reverse_symbolic': + if differentiate_mode == 'reverse_symbolic': mode = EXPR.differentiate.Modes.reverse_symbolic - elif config.differentiate_mode == 'sympy': + elif differentiate_mode == 'sympy': mode = EXPR.differentiate.Modes.sympy for c in model.MindtPy_utils.nonlinear_constraint_list: vars_in_constr = list(EXPR.identify_variables(c.body)) From 7e694136a8ac8cd197c7b56f2613a40597acbb59 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 7 Dec 2023 16:26:05 -0500 Subject: [PATCH 0229/3044] redesign initialize_feas_subproblem function --- pyomo/contrib/mindtpy/algorithm_base_class.py | 2 +- pyomo/contrib/mindtpy/util.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 78250d1ba59..05f1e4389d3 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -2629,7 +2629,7 @@ def initialize_mip_problem(self): self.fixed_nlp = self.working_model.clone() TransformationFactory('core.fix_integer_vars').apply_to(self.fixed_nlp) - initialize_feas_subproblem(self.fixed_nlp, config) + initialize_feas_subproblem(self.fixed_nlp, config.feasibility_norm) def initialize_subsolvers(self): """Initialize and set options for MIP and NLP subsolvers.""" diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 5ca4604d37e..551945dfc67 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -70,7 +70,7 @@ def calc_jacobians(model, differentiate_mode): return jacobians -def initialize_feas_subproblem(m, config): +def initialize_feas_subproblem(m, feasibility_norm): """Adds feasibility slack variables according to config.feasibility_norm (given an infeasible problem). Defines the objective function of the feasibility subproblem. @@ -78,14 +78,14 @@ def initialize_feas_subproblem(m, config): ---------- m : Pyomo model The feasbility NLP subproblem. - config : ConfigBlock - The specific configurations for MindtPy. + feasibility_norm : String + The norm used to generate the objective function. """ MindtPy = m.MindtPy_utils # generate new constraints for i, constr in enumerate(MindtPy.nonlinear_constraint_list, 1): if constr.has_ub(): - if config.feasibility_norm in {'L1', 'L2'}: + if feasibility_norm in {'L1', 'L2'}: MindtPy.feas_opt.feas_constraints.add( constr.body - constr.upper <= MindtPy.feas_opt.slack_var[i] ) @@ -94,7 +94,7 @@ def initialize_feas_subproblem(m, config): constr.body - constr.upper <= MindtPy.feas_opt.slack_var ) if constr.has_lb(): - if config.feasibility_norm in {'L1', 'L2'}: + if feasibility_norm in {'L1', 'L2'}: MindtPy.feas_opt.feas_constraints.add( constr.body - constr.lower >= -MindtPy.feas_opt.slack_var[i] ) @@ -103,11 +103,11 @@ def initialize_feas_subproblem(m, config): constr.body - constr.lower >= -MindtPy.feas_opt.slack_var ) # Setup objective function for the feasibility subproblem. - if config.feasibility_norm == 'L1': + if feasibility_norm == 'L1': MindtPy.feas_obj = Objective( expr=sum(s for s in MindtPy.feas_opt.slack_var.values()), sense=minimize ) - elif config.feasibility_norm == 'L2': + elif feasibility_norm == 'L2': MindtPy.feas_obj = Objective( expr=sum(s * s for s in MindtPy.feas_opt.slack_var.values()), sense=minimize ) From c9f788849c0c15c197e85a8ce7e10df52bce2c98 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 7 Dec 2023 16:34:26 -0500 Subject: [PATCH 0230/3044] redesign calc_jacobians function --- pyomo/contrib/mindtpy/extended_cutting_plane.py | 3 ++- pyomo/contrib/mindtpy/feasibility_pump.py | 3 ++- pyomo/contrib/mindtpy/outer_approximation.py | 3 ++- pyomo/contrib/mindtpy/util.py | 10 +++++----- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/mindtpy/extended_cutting_plane.py b/pyomo/contrib/mindtpy/extended_cutting_plane.py index 08c89a4c5f0..f5fa205e091 100644 --- a/pyomo/contrib/mindtpy/extended_cutting_plane.py +++ b/pyomo/contrib/mindtpy/extended_cutting_plane.py @@ -87,7 +87,8 @@ def initialize_mip_problem(self): '''Deactivate the nonlinear constraints to create the MIP problem.''' super().initialize_mip_problem() self.jacobians = calc_jacobians( - self.mip, self.config.differentiate_mode + self.mip.MindtPy_utils.nonlinear_constraint_list, + self.config.differentiate_mode, ) # preload jacobians self.mip.MindtPy_utils.cuts.ecp_cuts = ConstraintList( doc='Extended Cutting Planes' diff --git a/pyomo/contrib/mindtpy/feasibility_pump.py b/pyomo/contrib/mindtpy/feasibility_pump.py index bf6fb8f84bb..9d5be89bab5 100644 --- a/pyomo/contrib/mindtpy/feasibility_pump.py +++ b/pyomo/contrib/mindtpy/feasibility_pump.py @@ -47,7 +47,8 @@ def initialize_mip_problem(self): '''Deactivate the nonlinear constraints to create the MIP problem.''' super().initialize_mip_problem() self.jacobians = calc_jacobians( - self.mip, self.config.differentiate_mode + self.mip.MindtPy_utils.nonlinear_constraint_list, + self.config.differentiate_mode, ) # preload jacobians self.mip.MindtPy_utils.cuts.oa_cuts = ConstraintList( doc='Outer approximation cuts' diff --git a/pyomo/contrib/mindtpy/outer_approximation.py b/pyomo/contrib/mindtpy/outer_approximation.py index 4fd140a0bba..6d790ce70d0 100644 --- a/pyomo/contrib/mindtpy/outer_approximation.py +++ b/pyomo/contrib/mindtpy/outer_approximation.py @@ -97,7 +97,8 @@ def initialize_mip_problem(self): '''Deactivate the nonlinear constraints to create the MIP problem.''' super().initialize_mip_problem() self.jacobians = calc_jacobians( - self.mip, self.config.differentiate_mode + self.mip.MindtPy_utils.nonlinear_constraint_list, + self.config.differentiate_mode, ) # preload jacobians self.mip.MindtPy_utils.cuts.oa_cuts = ConstraintList( doc='Outer approximation cuts' diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 551945dfc67..5845b3047f5 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -41,16 +41,16 @@ numpy = attempt_import('numpy')[0] -def calc_jacobians(model, differentiate_mode): +def calc_jacobians(constraint_list, differentiate_mode): """Generates a map of jacobians for the variables in the model. This function generates a map of jacobians corresponding to the variables in the - model. + constraint list. Parameters ---------- - model : Pyomo model - Target model to calculate jacobian. + constraint_list : List + The list of constraints to calculate Jacobians. differentiate_mode : String The differentiate mode to calculate Jacobians. """ @@ -61,7 +61,7 @@ def calc_jacobians(model, differentiate_mode): mode = EXPR.differentiate.Modes.reverse_symbolic elif differentiate_mode == 'sympy': mode = EXPR.differentiate.Modes.sympy - for c in model.MindtPy_utils.nonlinear_constraint_list: + for c in constraint_list: vars_in_constr = list(EXPR.identify_variables(c.body)) jac_list = EXPR.differentiate(c.body, wrt_list=vars_in_constr, mode=mode) jacobians[c] = ComponentMap( From 23c1ce3e2e869ab7bc6388c6c35cecb53c92492a Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 10 Dec 2023 13:00:29 -0700 Subject: [PATCH 0231/3044] Adding some expression nodes involving sequence vars and some tests for them --- pyomo/contrib/cp/__init__.py | 8 + .../scheduling_expr/sequence_expressions.py | 173 ++++++++++++++++++ .../cp/tests/test_sequence_expressions.py | 104 +++++++++++ 3 files changed, 285 insertions(+) create mode 100644 pyomo/contrib/cp/scheduling_expr/sequence_expressions.py create mode 100644 pyomo/contrib/cp/tests/test_sequence_expressions.py diff --git a/pyomo/contrib/cp/__init__.py b/pyomo/contrib/cp/__init__.py index c51160bf931..03196537446 100644 --- a/pyomo/contrib/cp/__init__.py +++ b/pyomo/contrib/cp/__init__.py @@ -6,6 +6,14 @@ IntervalVarPresence, ) from pyomo.contrib.cp.repn.docplex_writer import DocplexWriter, CPOptimizerSolver +from pyomo.contrib.cp.sequence_var import SequenceVar +from pyomo.contrib.cp.scheduling_expr.sequence_expressions import ( + no_overlap, + first_in_sequence, + last_in_sequence, + before_in_sequence, + predecessor_to, +) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, Step, diff --git a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py new file mode 100644 index 00000000000..0a49198c1d1 --- /dev/null +++ b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py @@ -0,0 +1,173 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.core.expr.logical_expr import BooleanExpression + +# ESJ TODO: The naming in this file needs more thought, and it appears I do not +# need the base class. + +class SequenceVarExpression(BooleanExpression): + pass + +class NoOverlapExpression(SequenceVarExpression): + """ + Expression representing that none of the IntervalVars in a SequenceVar overlap + (if they are scheduled) + + args: + args (tuple): Child node of type SequenceVar + """ + def nargs(self): + return 1 + + def _to_string(self, values, verbose, smap): + return "no_overlap(%s)" % values[0] + + +class FirstInSequenceExpression(SequenceVarExpression): + """ + Expression representing that the specified IntervalVar is the first in the + sequence specified by SequenceVar (if it is scheduled) + + args: + args (tuple): Child nodes, the first of type IntervalVar, the second of type + SequenceVar + """ + def nargs(self): + return 2 + + def _to_string(self, values, verbose, smap): + return "first_in(%s, %s)" % (values[0], values[1]) + + +class LastInSequenceExpression(SequenceVarExpression): + """ + Expression representing that the specified IntervalVar is the last in the + sequence specified by SequenceVar (if it is scheduled) + + args: + args (tuple): Child nodes, the first of type IntervalVar, the second of type + SequenceVar + """ + def nargs(self): + return 2 + + def _to_string(self, values, verbose, smap): + return "last_in(%s, %s)" % (values[0], values[1]) + + +class BeforeInSequenceExpression(SequenceVarExpression): + """ + Expression representing that one IntervalVar occurs before another in the + sequence specified by the given SequenceVar (if both are scheduled) + + args: + args (tuple): Child nodes, the IntervalVar that must be before, the + IntervalVar that must be after, and the SequenceVar + """ + def nargs(self): + return 3 + + def _to_string(self, values, verbose, smap): + return "before_in(%s, %s, %s)" % (values[0], values[1], values[2]) + + +class PredecessorToExpression(SequenceVarExpression): + """ + Expression representing that one IntervalVar is a direct predecessor to another + in the sequence specified by the given SequenceVar (if both are scheduled) + + args: + args (tuple): Child nodes, the predecessor IntervalVar, the successor + IntervalVar, and the SequenceVar + """ + def nargs(self): + return 3 + + def _to_string(self, values, verbose, smap): + return "predecessor_to(%s, %s, %s)" % (values[0], values[1], values[2]) + + +def no_overlap(sequence_var): + """ + Creates a new NoOverlapExpression + + Requires that none of the scheduled intervals in the SequenceVar overlap each other + + args: + sequence_var: A SequenceVar + """ + return NoOverlapExpression((sequence_var,)) + + +def first_in_sequence(interval_var, sequence_var): + """ + Creates a new FirstInSequenceExpression + + Requires that 'interval_var' be the first in the sequence specified by + 'sequence_var' if it is scheduled + + args: + interval_var (IntervalVar): The activity that should be scheduled first + if it is scheduled at all + sequence_var (SequenceVar): The sequence of activities + """ + return FirstInSequenceExpression((interval_var, sequence_var,)) + + +def last_in_sequence(interval_var, sequence_var): + """ + Creates a new LastInSequenceExpression + + Requires that 'interval_var' be the last in the sequence specified by + 'sequence_var' if it is scheduled + + args: + interval_var (IntervalVar): The activity that should be scheduled last + if it is scheduled at all + sequence_var (SequenceVar): The sequence of activities + """ + + return LastInSequenceExpression((interval_var, sequence_var,)) + + +def before_in_sequence(before_var, after_var, sequence_var): + """ + Creates a new BeforeInSequenceExpression + + Requires that 'before_var' be scheduled to start before 'after_var' in the + sequence spcified bv 'sequence_var', if both are scheduled + + args: + before_var (IntervalVar): The activity that should be scheduled earlier in + the sequence + after_var (IntervalVar): The activity that should be scheduled later in the + sequence + sequence_var (SequenceVar): The sequence of activities + """ + return BeforeInSequenceExpression((before_var, after_var, sequence_var,)) + + +def predecessor_to(before_var, after_var, sequence_var): + """ + Creates a new PredecessorToExpression + + Requires that 'before_var' be a direct predecessor to 'after_var' in the + sequence specified by 'sequence_var', if both are scheduled + + args: + before_var (IntervalVar): The activity that should be scheduled as the + predecessor + after_var (IntervalVar): The activity that should be scheduled as the + successor + sequence_var (SequenceVar): The sequence of activities + """ + return PredecessorToExpression((before_var, after_var, sequence_var,)) diff --git a/pyomo/contrib/cp/tests/test_sequence_expressions.py b/pyomo/contrib/cp/tests/test_sequence_expressions.py new file mode 100644 index 00000000000..a35eb9b67af --- /dev/null +++ b/pyomo/contrib/cp/tests/test_sequence_expressions.py @@ -0,0 +1,104 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from io import StringIO +import pyomo.common.unittest as unittest +from pyomo.contrib.cp.interval_var import IntervalVar +from pyomo.contrib.cp.scheduling_expr.sequence_expressions import ( + NoOverlapExpression, + FirstInSequenceExpression, + LastInSequenceExpression, + BeforeInSequenceExpression, + PredecessorToExpression, + no_overlap, + predecessor_to, + before_in_sequence, + first_in_sequence, + last_in_sequence, +) +from pyomo.contrib.cp.sequence_var import SequenceVar, IndexedSequenceVar +from pyomo.environ import ConcreteModel, Integers, LogicalConstraint, Set, value, Var + + +class TestSequenceVarExpressions(unittest.TestCase): + def get_model(self): + m = ConcreteModel() + m.S = Set(initialize=range(3)) + m.i = IntervalVar(m.S, start=(0, 5)) + m.seq = SequenceVar(expr=[m.i[j] for j in m.S]) + + return m + + def test_no_overlap(self): + m = self.get_model() + m.c = LogicalConstraint(expr=no_overlap(m.seq)) + e = m.c.expr + + self.assertIsInstance(e, NoOverlapExpression) + self.assertEqual(e.nargs(), 1) + self.assertEqual(len(e.args), 1) + self.assertIs(e.args[0], m.seq) + + self.assertEqual(str(e), "no_overlap(seq)") + + def test_first_in_sequence(self): + m = self.get_model() + m.c = LogicalConstraint(expr=first_in_sequence(m.i[2], m.seq)) + e = m.c.expr + + self.assertIsInstance(e, FirstInSequenceExpression) + self.assertEqual(e.nargs(), 2) + self.assertEqual(len(e.args), 2) + self.assertIs(e.args[0], m.i[2]) + self.assertIs(e.args[1], m.seq) + + self.assertEqual(str(e), "first_in(i[2], seq)") + + def test_last_in_sequence(self): + m = self.get_model() + m.c = LogicalConstraint(expr=last_in_sequence(m.i[0], m.seq)) + e = m.c.expr + + self.assertIsInstance(e, LastInSequenceExpression) + self.assertEqual(e.nargs(), 2) + self.assertEqual(len(e.args), 2) + self.assertIs(e.args[0], m.i[0]) + self.assertIs(e.args[1], m.seq) + + self.assertEqual(str(e), "last_in(i[0], seq)") + + def test_before_in_sequence(self): + m = self.get_model() + m.c = LogicalConstraint(expr=before_in_sequence(m.i[1], m.i[0], m.seq)) + e = m.c.expr + + self.assertIsInstance(e, BeforeInSequenceExpression) + self.assertEqual(e.nargs(), 3) + self.assertEqual(len(e.args), 3) + self.assertIs(e.args[0], m.i[1]) + self.assertIs(e.args[1], m.i[0]) + self.assertIs(e.args[2], m.seq) + + self.assertEqual(str(e), "before_in(i[1], i[0], seq)") + + def test_predecessor_in_sequence(self): + m = self.get_model() + m.c = LogicalConstraint(expr=predecessor_to(m.i[0], m.i[1], m.seq)) + e = m.c.expr + + self.assertIsInstance(e, PredecessorToExpression) + self.assertEqual(e.nargs(), 3) + self.assertEqual(len(e.args), 3) + self.assertIs(e.args[0], m.i[0]) + self.assertIs(e.args[1], m.i[1]) + self.assertIs(e.args[2], m.seq) + + self.assertEqual(str(e), "predecessor_to(i[0], i[1], seq)") From 05d9020e292b2d3013b532a566f86addba6b22d3 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 11 Dec 2023 15:12:53 -0700 Subject: [PATCH 0232/3044] Adding handling for sequence var expressions in the docplex writer --- pyomo/contrib/cp/repn/docplex_writer.py | 83 +++++++++++++++++++ pyomo/contrib/cp/tests/test_docplex_walker.py | 78 ++++++++++++++++- 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 50a2d72aed8..38e2a0ae94f 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -30,10 +30,22 @@ IntervalVarData, IndexedIntervalVar, ) +from pyomo.contrib.cp.sequence_var import( + ScalarSequenceVar, + IndexedSequenceVar, + _SequenceVarData, +) from pyomo.contrib.cp.scheduling_expr.precedence_expressions import ( BeforeExpression, AtExpression, ) +from pyomo.contrib.cp.scheduling_expr.sequence_expressions import ( + NoOverlapExpression, + FirstInSequenceExpression, + LastInSequenceExpression, + BeforeInSequenceExpression, + PredecessorToExpression, +) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, StepAt, @@ -491,6 +503,16 @@ def _create_docplex_interval_var(visitor, interval_var): return cpx_interval_var +def _create_docplex_sequence_var(visitor, sequence_var): + nm = sequence_var.name if visitor.symbolic_solver_labels else None + + cpx_seq_var = cp.sequence_var(name=nm, + vars=[_get_docplex_interval_var(visitor, v) + for v in sequence_var.interval_vars]) + visitor.var_map[id(sequence_var)] = cpx_seq_var + return cpx_seq_var + + def _get_docplex_interval_var(visitor, interval_var): # We might already have the interval_var and just need to retrieve it if id(interval_var) in visitor.var_map: @@ -501,6 +523,37 @@ def _get_docplex_interval_var(visitor, interval_var): return cpx_interval_var +def _get_docplex_sequence_var(visitor, sequence_var): + if id(sequence_var) in visitor.var_map: + cpx_seq_var = visitor.var_map[id(sequence_var)] + else: + cpx_seq_var = _create_docplex_sequence_var(visitor, sequence_var) + visitor.cpx.add(cpx_seq_var) + return cpx_seq_var + + +def _before_sequence_var(visitor, child): + _id = id(child) + if _id not in visitor.var_map: + cpx_seq_var = _get_docplex_sequence_var(visitor, child) + visitor.var_map[_id] = cpx_seq_var + visitor.pyomo_to_docplex[child] = cpx_seq_var + + return False, (_GENERAL, visitor.var_map[_id]) + + +def _before_indexed_sequence_var(visitor, child): + # ESJ TODO: I'm not sure we can encounter an indexed sequence var in an + # expression right now? + cpx_vars = {} + for i, v in child.items(): + cpx_sequence_var = _get_docplex_sequence_var(visitor, v) + visitor.var_map[id(v)] = cpx_sequence_var + visitor.pyomo_to_docplex[v] = cpx_sequence_var + cpx_vars[i] = cpx_sequence_var + return False, (_GENERAL, cpx_vars) + + def _before_interval_var(visitor, child): _id = id(child) if _id not in visitor.var_map: @@ -902,6 +955,28 @@ def _handle_always_in_node(visitor, node, cumul_func, lb, ub, start, end): ) +def _handle_no_overlap_expression_node(visitor, node, seq_var): + return _GENERAL, cp.no_overlap(seq_var[1]) + + +def _handle_first_in_sequence_expression_node(visitor, node, interval_var, seq_var): + return _GENERAL, cp.first(seq_var[1], interval_var[1]) + + +def _handle_last_in_sequence_expression_node(visitor, node, interval_var, seq_var): + return _GENERAL, cp.last(seq_var[1], interval_var[1]) + + +def _handle_before_in_sequence_expression_node(visitor, node, before_var, + after_var, seq_var): + return _GENERAL, cp.before(seq_var[1], before_var[1], after_var[1]) + + +def _handle_predecessor_to_expression_node(visitor, node, before_var, after_var, + seq_var): + return _GENERAL, cp.previous(seq_var[1], before_var[1], after_var[1]) + + class LogicalToDoCplex(StreamBasedExpressionVisitor): _operator_handles = { EXPR.GetItemExpression: _handle_getitem, @@ -941,6 +1016,11 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): AlwaysIn: _handle_always_in_node, _GeneralExpressionData: _handle_named_expression_node, ScalarExpression: _handle_named_expression_node, + NoOverlapExpression: _handle_no_overlap_expression_node, + FirstInSequenceExpression: _handle_first_in_sequence_expression_node, + LastInSequenceExpression: _handle_last_in_sequence_expression_node, + BeforeInSequenceExpression: _handle_before_in_sequence_expression_node, + PredecessorToExpression: _handle_predecessor_to_expression_node, } _var_handles = { IntervalVarStartTime: _before_interval_var_start_time, @@ -950,6 +1030,9 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): ScalarIntervalVar: _before_interval_var, IntervalVarData: _before_interval_var, IndexedIntervalVar: _before_indexed_interval_var, + ScalarSequenceVar: _before_sequence_var, + _SequenceVarData: _before_sequence_var, + IndexedSequenceVar: _before_indexed_sequence_var, ScalarVar: _before_var, _GeneralVarData: _before_var, IndexedVar: _before_indexed_var, diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 97bc538c827..dc35a60050c 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -11,7 +11,15 @@ import pyomo.common.unittest as unittest -from pyomo.contrib.cp import IntervalVar +from pyomo.contrib.cp import ( + IntervalVar, + SequenceVar, + no_overlap, + first_in_sequence, + last_in_sequence, + before_in_sequence, + predecessor_to, +) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, Step, @@ -769,6 +777,74 @@ def test_interval_var_fixed_start_and_end(self): self.assertEqual(i.get_end(), (6, 6)) +@unittest.skipIf(not docplex_available, "docplex is not available") +class TestCPExpressionWalker_SequenceVars(CommonTest): + def get_model(self): + m = super().get_model() + m.seq = SequenceVar(expr=[m.i, m.i2[1], m.i2[2]]) + + return m + + def check_scalar_sequence_var(self, m, visitor): + self.assertIn(id(m.seq), visitor.var_map) + seq = visitor.var_map[id(m.seq)] + + i = visitor.var_map[id(m.i)] + i21 = visitor.var_map[id(m.i2[1])] + i22 = visitor.var_map[id(m.i2[2])] + + ivs = seq.get_interval_variables() + self.assertEqual(len(ivs), 3) + self.assertIs(ivs[0], i) + self.assertIs(ivs[1], i21) + self.assertIs(ivs[2], i22) + + return seq, i, i21, i22 + + def test_scalar_sequence_var(self): + m = self.get_model() + + visitor = self.get_visitor() + expr = visitor.walk_expression((m.seq, m.seq, 0)) + self.check_scalar_sequence_var(m, visitor) + + def test_no_overlap(self): + m = self.get_model() + e = no_overlap(m.seq) + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + seq, i, i21, i22 = self.check_scalar_sequence_var(m, visitor) + self.assertTrue(expr[1].equals(cp.no_overlap(seq))) + + def test_first_in_sequence(self): + m = self.get_model() + e = first_in_sequence(m.i2[1], m.seq) + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + seq, i, i21, i22 = self.check_scalar_sequence_var(m, visitor) + self.assertTrue(expr[1].equals(cp.first(seq, i21))) + + def test_before_in_sequence(self): + m = self.get_model() + e = last_in_sequence(m.i, m.seq) + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + seq, i, i21, i22 = self.check_scalar_sequence_var(m, visitor) + self.assertTrue(expr[1].equals(cp.last(seq, i))) + + def test_last_in_sequence(self): + m = self.get_model() + e = last_in_sequence(m.i2[1], m.seq) + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + seq, i, i21, i22 = self.check_scalar_sequence_var(m, visitor) + self.assertTrue(expr[1].equals(cp.last(seq, i21))) + + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_PrecedenceExpressions(CommonTest): def test_start_before_start(self): From 1cbadc533368a1394a229dfee2bfe759953c96f3 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 11 Dec 2023 15:15:31 -0700 Subject: [PATCH 0233/3044] Blackify --- pyomo/contrib/cp/repn/docplex_writer.py | 21 ++++++++++------- .../scheduling_expr/sequence_expressions.py | 23 ++++++++++++------- pyomo/contrib/cp/sequence_var.py | 16 +++++++------ .../cp/tests/test_sequence_expressions.py | 6 ++--- pyomo/contrib/cp/tests/test_sequence_var.py | 20 ++++++++-------- 5 files changed, 51 insertions(+), 35 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 38e2a0ae94f..fb816240c1e 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -30,7 +30,7 @@ IntervalVarData, IndexedIntervalVar, ) -from pyomo.contrib.cp.sequence_var import( +from pyomo.contrib.cp.sequence_var import ( ScalarSequenceVar, IndexedSequenceVar, _SequenceVarData, @@ -506,9 +506,12 @@ def _create_docplex_interval_var(visitor, interval_var): def _create_docplex_sequence_var(visitor, sequence_var): nm = sequence_var.name if visitor.symbolic_solver_labels else None - cpx_seq_var = cp.sequence_var(name=nm, - vars=[_get_docplex_interval_var(visitor, v) - for v in sequence_var.interval_vars]) + cpx_seq_var = cp.sequence_var( + name=nm, + vars=[ + _get_docplex_interval_var(visitor, v) for v in sequence_var.interval_vars + ], + ) visitor.var_map[id(sequence_var)] = cpx_seq_var return cpx_seq_var @@ -967,13 +970,15 @@ def _handle_last_in_sequence_expression_node(visitor, node, interval_var, seq_va return _GENERAL, cp.last(seq_var[1], interval_var[1]) -def _handle_before_in_sequence_expression_node(visitor, node, before_var, - after_var, seq_var): +def _handle_before_in_sequence_expression_node( + visitor, node, before_var, after_var, seq_var +): return _GENERAL, cp.before(seq_var[1], before_var[1], after_var[1]) -def _handle_predecessor_to_expression_node(visitor, node, before_var, after_var, - seq_var): +def _handle_predecessor_to_expression_node( + visitor, node, before_var, after_var, seq_var +): return _GENERAL, cp.previous(seq_var[1], before_var[1], after_var[1]) diff --git a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py index 0a49198c1d1..b39322322da 100644 --- a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py @@ -14,9 +14,11 @@ # ESJ TODO: The naming in this file needs more thought, and it appears I do not # need the base class. + class SequenceVarExpression(BooleanExpression): pass + class NoOverlapExpression(SequenceVarExpression): """ Expression representing that none of the IntervalVars in a SequenceVar overlap @@ -25,6 +27,7 @@ class NoOverlapExpression(SequenceVarExpression): args: args (tuple): Child node of type SequenceVar """ + def nargs(self): return 1 @@ -41,6 +44,7 @@ class FirstInSequenceExpression(SequenceVarExpression): args (tuple): Child nodes, the first of type IntervalVar, the second of type SequenceVar """ + def nargs(self): return 2 @@ -57,6 +61,7 @@ class LastInSequenceExpression(SequenceVarExpression): args (tuple): Child nodes, the first of type IntervalVar, the second of type SequenceVar """ + def nargs(self): return 2 @@ -70,9 +75,10 @@ class BeforeInSequenceExpression(SequenceVarExpression): sequence specified by the given SequenceVar (if both are scheduled) args: - args (tuple): Child nodes, the IntervalVar that must be before, the + args (tuple): Child nodes, the IntervalVar that must be before, the IntervalVar that must be after, and the SequenceVar """ + def nargs(self): return 3 @@ -86,9 +92,10 @@ class PredecessorToExpression(SequenceVarExpression): in the sequence specified by the given SequenceVar (if both are scheduled) args: - args (tuple): Child nodes, the predecessor IntervalVar, the successor + args (tuple): Child nodes, the predecessor IntervalVar, the successor IntervalVar, and the SequenceVar """ + def nargs(self): return 3 @@ -120,7 +127,7 @@ def first_in_sequence(interval_var, sequence_var): if it is scheduled at all sequence_var (SequenceVar): The sequence of activities """ - return FirstInSequenceExpression((interval_var, sequence_var,)) + return FirstInSequenceExpression((interval_var, sequence_var)) def last_in_sequence(interval_var, sequence_var): @@ -136,24 +143,24 @@ def last_in_sequence(interval_var, sequence_var): sequence_var (SequenceVar): The sequence of activities """ - return LastInSequenceExpression((interval_var, sequence_var,)) + return LastInSequenceExpression((interval_var, sequence_var)) def before_in_sequence(before_var, after_var, sequence_var): """ Creates a new BeforeInSequenceExpression - Requires that 'before_var' be scheduled to start before 'after_var' in the + Requires that 'before_var' be scheduled to start before 'after_var' in the sequence spcified bv 'sequence_var', if both are scheduled args: - before_var (IntervalVar): The activity that should be scheduled earlier in + before_var (IntervalVar): The activity that should be scheduled earlier in the sequence after_var (IntervalVar): The activity that should be scheduled later in the sequence sequence_var (SequenceVar): The sequence of activities """ - return BeforeInSequenceExpression((before_var, after_var, sequence_var,)) + return BeforeInSequenceExpression((before_var, after_var, sequence_var)) def predecessor_to(before_var, after_var, sequence_var): @@ -170,4 +177,4 @@ def predecessor_to(before_var, after_var, sequence_var): successor sequence_var (SequenceVar): The sequence of activities """ - return PredecessorToExpression((before_var, after_var, sequence_var,)) + return PredecessorToExpression((before_var, after_var, sequence_var)) diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py index b0691fbd74a..a77f4c2c415 100644 --- a/pyomo/contrib/cp/sequence_var.py +++ b/pyomo/contrib/cp/sequence_var.py @@ -27,7 +27,9 @@ class _SequenceVarData(ActiveComponentData): """This class defines the abstract interface for a single sequence variable.""" + __slots__ = ('interval_vars',) + def __init__(self, component=None): # in-lining ActiveComponentData and ComponentData constructors, as is # traditional: @@ -45,14 +47,15 @@ def set_value(self, expr): if not hasattr(expr, '__iter__'): raise ValueError( "'expr' for SequenceVar must be a list of IntervalVars. " - "Encountered type '%s' constructing '%s'" % (type(expr), - self.name)) + "Encountered type '%s' constructing '%s'" % (type(expr), self.name) + ) for v in expr: if not hasattr(v, 'ctype') or v.ctype is not IntervalVar: raise ValueError( "The SequenceVar 'expr' argument must be a list of " "IntervalVars. The 'expr' for SequenceVar '%s' included " - "an object of type '%s'" % (self.name, type(v))) + "an object of type '%s'" % (self.name, type(v)) + ) self.interval_vars.append(v) @@ -101,7 +104,7 @@ def construct(self, data=None): if self._constructed: return self._constructed = True - + if is_debug_set(logger): logger.debug("Constructing SequenceVar %s" % self.name) @@ -132,11 +135,10 @@ def _pprint(self): headers, self._data.items(), ("IntervalVars",), - lambda k, v: [ - '[' + ', '.join(iv.name for iv in v.interval_vars) + ']', - ] + lambda k, v: ['[' + ', '.join(iv.name for iv in v.interval_vars) + ']'], ) + class ScalarSequenceVar(_SequenceVarData, SequenceVar): def __init__(self, *args, **kwds): _SequenceVarData.__init__(self, component=self) diff --git a/pyomo/contrib/cp/tests/test_sequence_expressions.py b/pyomo/contrib/cp/tests/test_sequence_expressions.py index a35eb9b67af..0ef2a9e3072 100644 --- a/pyomo/contrib/cp/tests/test_sequence_expressions.py +++ b/pyomo/contrib/cp/tests/test_sequence_expressions.py @@ -74,7 +74,7 @@ def test_last_in_sequence(self): self.assertIs(e.args[1], m.seq) self.assertEqual(str(e), "last_in(i[0], seq)") - + def test_before_in_sequence(self): m = self.get_model() m.c = LogicalConstraint(expr=before_in_sequence(m.i[1], m.i[0], m.seq)) @@ -93,12 +93,12 @@ def test_predecessor_in_sequence(self): m = self.get_model() m.c = LogicalConstraint(expr=predecessor_to(m.i[0], m.i[1], m.seq)) e = m.c.expr - + self.assertIsInstance(e, PredecessorToExpression) self.assertEqual(e.nargs(), 3) self.assertEqual(len(e.args), 3) self.assertIs(e.args[0], m.i[0]) self.assertIs(e.args[1], m.i[1]) self.assertIs(e.args[2], m.seq) - + self.assertEqual(str(e), "predecessor_to(i[0], i[1], seq)") diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index 852d9f2134a..385ad2dd7ec 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -58,16 +58,16 @@ def test_pprint(self): seq : Size=1, Index=None Key : IntervalVars None : [i[0], i[1], i[2]] - """.strip() + """.strip(), ) def test_interval_vars_not_a_list(self): m = self.get_model() with self.assertRaisesRegex( - ValueError, - "'expr' for SequenceVar must be a list of IntervalVars. " - "Encountered type '' constructing 'seq2'" + ValueError, + "'expr' for SequenceVar must be a list of IntervalVars. " + "Encountered type '' constructing 'seq2'", ): m.seq2 = SequenceVar(expr=1) @@ -75,13 +75,14 @@ def test_interval_vars_list_includes_things_that_are_not_interval_vars(self): m = self.get_model() with self.assertRaisesRegex( - ValueError, - "The SequenceVar 'expr' argument must be a list of " - "IntervalVars. The 'expr' for SequenceVar 'seq2' included " - "an object of type ''" + ValueError, + "The SequenceVar 'expr' argument must be a list of " + "IntervalVars. The 'expr' for SequenceVar 'seq2' included " + "an object of type ''", ): m.seq2 = SequenceVar(expr=m.i) + class TestIndexedSequenceVar(unittest.TestCase): def test_initialize_with_not_data(self): m = ConcreteModel() @@ -110,6 +111,7 @@ def make_model(self): def the_rule(m, j): return [m.i[j, k] for k in m.num] + m.seq = SequenceVar(m.alph, rule=the_rule) return m @@ -137,5 +139,5 @@ def test_pprint(self): seq : Size=2, Index=alph Key : IntervalVars a : [i[a,1], i[a,2]] - b : [i[b,1], i[b,2]]""".strip() + b : [i[b,1], i[b,2]]""".strip(), ) From 70a4c5ef956140566b992e95149bf064e4ffb4e3 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 11 Dec 2023 17:10:23 -0700 Subject: [PATCH 0234/3044] Fixing a typo --- pyomo/contrib/cp/scheduling_expr/sequence_expressions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py index b39322322da..d88504ac7e4 100644 --- a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py @@ -151,7 +151,7 @@ def before_in_sequence(before_var, after_var, sequence_var): Creates a new BeforeInSequenceExpression Requires that 'before_var' be scheduled to start before 'after_var' in the - sequence spcified bv 'sequence_var', if both are scheduled + sequence specified bv 'sequence_var', if both are scheduled args: before_var (IntervalVar): The activity that should be scheduled earlier in From f1ad1d0ecead8e42f8bdf289f21507224a1f1e5e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 11 Dec 2023 17:11:40 -0700 Subject: [PATCH 0235/3044] The linter won't let me name something 'alph' --- pyomo/contrib/cp/tests/test_sequence_var.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index 385ad2dd7ec..8167c9f5b3b 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -105,14 +105,14 @@ def test_initialize_with_not_data(self): def make_model(self): m = ConcreteModel() - m.alph = Set(initialize=['a', 'b']) - m.num = Set(initialize=[1, 2]) - m.i = IntervalVar(m.alph, m.num) + m.alphabetic = Set(initialize=['a', 'b']) + m.numeric = Set(initialize=[1, 2]) + m.i = IntervalVar(m.alphabetic, m.numeric) def the_rule(m, j): - return [m.i[j, k] for k in m.num] + return [m.i[j, k] for k in m.numeric] - m.seq = SequenceVar(m.alph, rule=the_rule) + m.seq = SequenceVar(m.alphabetic, rule=the_rule) return m @@ -121,10 +121,10 @@ def test_initialize_with_rule(self): self.assertIsInstance(m.seq, IndexedSequenceVar) self.assertEqual(len(m.seq), 2) - for j in m.alph: + for j in m.alphabetic: self.assertTrue(j in m.seq) self.assertEqual(len(m.seq[j].interval_vars), 2) - for k in m.num: + for k in m.numeric: self.assertIs(m.seq[j].interval_vars[k - 1], m.i[j, k]) def test_pprint(self): @@ -136,7 +136,7 @@ def test_pprint(self): self.assertEqual( buf.getvalue().strip(), """ -seq : Size=2, Index=alph +seq : Size=2, Index=alphabetic Key : IntervalVars a : [i[a,1], i[a,2]] b : [i[b,1], i[b,2]]""".strip(), From c571f5c2d1952db77ea980d472a90d64463a355f Mon Sep 17 00:00:00 2001 From: robbybp Date: Tue, 12 Dec 2023 08:19:04 -0700 Subject: [PATCH 0236/3044] initial implementation of identify-via-amplrepn --- pyomo/contrib/incidence_analysis/incidence.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index 1852cf75648..974153984d2 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -16,6 +16,8 @@ from pyomo.core.expr.visitor import identify_variables from pyomo.core.expr.numvalue import value as pyo_value from pyomo.repn import generate_standard_repn +from pyomo.repn.nl_writer import AMPLRepnVisitor, AMPLRepn, text_nl_template +from pyomo.repn.util import FileDeterminism, FileDeterminism_to_SortComponents from pyomo.util.subsystems import TemporarySubsystemManager from pyomo.contrib.incidence_analysis.config import IncidenceMethod, IncidenceConfig @@ -74,6 +76,45 @@ def _get_incident_via_standard_repn(expr, include_fixed, linear_only): return unique_variables +def _get_incident_via_amplrepn(expr, linear_only): + subexpression_cache = {} + subexpression_order = [] + external_functions = {} + var_map = {} + used_named_expressions = set() + symbolic_solver_labels = False + export_defined_variabels = True + sorter = FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) + visitor = AMPLRepnVisitor( + text_nl_template, + subexpression_cache, + subexpression_order, + external_functions, + var_map, + used_named_expressions, + symbolic_solver_labels, + export_defined_variables, + sorter, + ) + AMPLRepn.ActiveVisitor = visitor + try: + repn = visitor.walk_expression((expr, None, 0, 1.0)) + finally: + AMPLRepn.ActiveVisitor = None + + nonlinear_vars = [var_map[v_id] for v_id in repn.nonlinear[1]] + nonlinear_vid_set = set(repn.nonlinear[1]) + linear_only_vars = [ + var_map[v_id] for v_id, coef in repn.linear.items() + if coef != 0.0 and v_id not in nonlinear_vid_set + ] + if linear_only: + return linear_only_vars + else: + variables = linear_only_vars + nonlinear_vars + return variables + + def get_incident_variables(expr, **kwds): """Get variables that participate in an expression From 07c65e940a32a5fae3c6509c4a055c28ef4b146b Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 12 Dec 2023 09:53:14 -0700 Subject: [PATCH 0237/3044] add IncidenceMethod.ampl_repn option --- pyomo/contrib/incidence_analysis/config.py | 3 +++ pyomo/contrib/incidence_analysis/incidence.py | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/config.py b/pyomo/contrib/incidence_analysis/config.py index 56841617cac..60acc53abfc 100644 --- a/pyomo/contrib/incidence_analysis/config.py +++ b/pyomo/contrib/incidence_analysis/config.py @@ -24,6 +24,9 @@ class IncidenceMethod(enum.Enum): standard_repn = 1 """Use ``pyomo.repn.standard_repn.generate_standard_repn``""" + ampl_repn = 2 + """Use ``pyomo.repn.plugins.nl_writer.AMPLRepnVisitor``""" + _include_fixed = ConfigValue( default=False, diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index 974153984d2..f16b248463c 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -16,7 +16,7 @@ from pyomo.core.expr.visitor import identify_variables from pyomo.core.expr.numvalue import value as pyo_value from pyomo.repn import generate_standard_repn -from pyomo.repn.nl_writer import AMPLRepnVisitor, AMPLRepn, text_nl_template +from pyomo.repn.plugins.nl_writer import AMPLRepnVisitor, AMPLRepn, text_nl_template from pyomo.repn.util import FileDeterminism, FileDeterminism_to_SortComponents from pyomo.util.subsystems import TemporarySubsystemManager from pyomo.contrib.incidence_analysis.config import IncidenceMethod, IncidenceConfig @@ -76,14 +76,14 @@ def _get_incident_via_standard_repn(expr, include_fixed, linear_only): return unique_variables -def _get_incident_via_amplrepn(expr, linear_only): +def _get_incident_via_ampl_repn(expr, linear_only): subexpression_cache = {} subexpression_order = [] external_functions = {} var_map = {} used_named_expressions = set() symbolic_solver_labels = False - export_defined_variabels = True + export_defined_variables = True sorter = FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) visitor = AMPLRepnVisitor( text_nl_template, @@ -102,8 +102,9 @@ def _get_incident_via_amplrepn(expr, linear_only): finally: AMPLRepn.ActiveVisitor = None - nonlinear_vars = [var_map[v_id] for v_id in repn.nonlinear[1]] - nonlinear_vid_set = set(repn.nonlinear[1]) + nonlinear_var_ids = [] if repn.nonlinear is None else repn.nonlinear[1] + nonlinear_vars = [var_map[v_id] for v_id in nonlinear_var_ids] + nonlinear_vid_set = set(nonlinear_var_ids) linear_only_vars = [ var_map[v_id] for v_id, coef in repn.linear.items() if coef != 0.0 and v_id not in nonlinear_vid_set @@ -161,10 +162,16 @@ def get_incident_variables(expr, **kwds): raise RuntimeError( "linear_only=True is not supported when using identify_variables" ) + if include_fixed and method is IncidenceMethod.ampl_repn: + raise RuntimeError( + "include_fixed=True is not supported when using ampl_repn" + ) if method is IncidenceMethod.identify_variables: return _get_incident_via_identify_variables(expr, include_fixed) elif method is IncidenceMethod.standard_repn: return _get_incident_via_standard_repn(expr, include_fixed, linear_only) + elif method is IncidenceMethod.ampl_repn: + return _get_incident_via_ampl_repn(expr, linear_only) else: raise ValueError( f"Unrecognized value {method} for the method used to identify incident" From 5c88a9794ad2d05595eb849d53af3d18a50747ab Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 12 Dec 2023 09:53:57 -0700 Subject: [PATCH 0238/3044] refactor tests and test ampl_repn option --- .../tests/test_incidence.py | 124 ++++++++++++------ 1 file changed, 83 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/tests/test_incidence.py b/pyomo/contrib/incidence_analysis/tests/test_incidence.py index 7f57dd904a7..e37e4f97691 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_incidence.py +++ b/pyomo/contrib/incidence_analysis/tests/test_incidence.py @@ -63,37 +63,37 @@ def test_incidence_with_fixed_variable(self): var_set = ComponentSet(variables) self.assertEqual(var_set, ComponentSet([m.x[1], m.x[3]])) - def test_incidence_with_mutable_parameter(self): + +class _TestIncidenceLinearOnly(object): + def _get_incident_variables(self, expr): + raise NotImplementedError( + "_TestIncidenceLinearOnly should not be used directly" + ) + + def test_linear_only(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2, 3]) - m.p = pyo.Param(mutable=True, initialize=None) - expr = m.x[1] + m.p * m.x[1] * m.x[2] + m.x[1] * pyo.exp(m.x[3]) - variables = self._get_incident_variables(expr) - self.assertEqual(ComponentSet(variables), ComponentSet(m.x[:])) + expr = 2 * m.x[1] + 4 * m.x[2] * m.x[1] - m.x[1] * pyo.exp(m.x[3]) + variables = self._get_incident_variables(expr, linear_only=True) + self.assertEqual(len(variables), 0) -class TestIncidenceStandardRepn(unittest.TestCase, _TestIncidence): - def _get_incident_variables(self, expr, **kwds): - method = IncidenceMethod.standard_repn - return get_incident_variables(expr, method=method, **kwds) + expr = 2 * m.x[1] + 2 * m.x[2] * m.x[3] + 3 * m.x[2] + variables = self._get_incident_variables(expr, linear_only=True) + self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1]])) - def test_assumed_standard_repn_behavior(self): - m = pyo.ConcreteModel() - m.x = pyo.Var([1, 2]) - m.p = pyo.Param(initialize=0.0) + m.x[3].fix(2.5) + expr = 2 * m.x[1] + 2 * m.x[2] * m.x[3] + 3 * m.x[2] + variables = self._get_incident_variables(expr, linear_only=True) + self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1], m.x[2]])) - # We rely on variables with constant coefficients of zero not appearing - # in the standard repn (as opposed to appearing with explicit - # coefficients of zero). - expr = m.x[1] + 0 * m.x[2] - repn = generate_standard_repn(expr) - self.assertEqual(len(repn.linear_vars), 1) - self.assertIs(repn.linear_vars[0], m.x[1]) - expr = m.p * m.x[1] + m.x[2] - repn = generate_standard_repn(expr) - self.assertEqual(len(repn.linear_vars), 1) - self.assertIs(repn.linear_vars[0], m.x[2]) +class _TestIncidenceLinearCancellation(object): + """Tests for methods that perform linear cancellation""" + def _get_incident_variables(self, expr): + raise NotImplementedError( + "_TestIncidenceLinearCancellation should not be used directly" + ) def test_zero_coef(self): m = pyo.ConcreteModel() @@ -113,23 +113,6 @@ def test_variable_minus_itself(self): var_set = ComponentSet(variables) self.assertEqual(var_set, ComponentSet([m.x[2], m.x[3]])) - def test_linear_only(self): - m = pyo.ConcreteModel() - m.x = pyo.Var([1, 2, 3]) - - expr = 2 * m.x[1] + 4 * m.x[2] * m.x[1] - m.x[1] * pyo.exp(m.x[3]) - variables = self._get_incident_variables(expr, linear_only=True) - self.assertEqual(len(variables), 0) - - expr = 2 * m.x[1] + 2 * m.x[2] * m.x[3] + 3 * m.x[2] - variables = self._get_incident_variables(expr, linear_only=True) - self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1]])) - - m.x[3].fix(2.5) - expr = 2 * m.x[1] + 2 * m.x[2] * m.x[3] + 3 * m.x[2] - variables = self._get_incident_variables(expr, linear_only=True) - self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1], m.x[2]])) - def test_fixed_zero_linear_coefficient(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2, 3]) @@ -148,6 +131,9 @@ def test_fixed_zero_linear_coefficient(self): variables = self._get_incident_variables(expr) self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1], m.x[2]])) + # NOTE: This test assumes that all methods that support linear cancellation + # accept a linear_only argument. If this changes, this test wil need to be + # moved. def test_fixed_zero_coefficient_linear_only(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2, 3]) @@ -159,6 +145,35 @@ def test_fixed_zero_coefficient_linear_only(self): self.assertEqual(len(variables), 1) self.assertIs(variables[0], m.x[3]) + +class TestIncidenceStandardRepn( + unittest.TestCase, + _TestIncidence, + _TestIncidenceLinearOnly, + _TestIncidenceLinearCancellation, +): + def _get_incident_variables(self, expr, **kwds): + method = IncidenceMethod.standard_repn + return get_incident_variables(expr, method=method, **kwds) + + def test_assumed_standard_repn_behavior(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2]) + m.p = pyo.Param(initialize=0.0) + + # We rely on variables with constant coefficients of zero not appearing + # in the standard repn (as opposed to appearing with explicit + # coefficients of zero). + expr = m.x[1] + 0 * m.x[2] + repn = generate_standard_repn(expr) + self.assertEqual(len(repn.linear_vars), 1) + self.assertIs(repn.linear_vars[0], m.x[1]) + + expr = m.p * m.x[1] + m.x[2] + repn = generate_standard_repn(expr) + self.assertEqual(len(repn.linear_vars), 1) + self.assertIs(repn.linear_vars[0], m.x[2]) + def test_fixed_none_linear_coefficient(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2, 3]) @@ -168,6 +183,14 @@ def test_fixed_none_linear_coefficient(self): variables = self._get_incident_variables(expr) self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1], m.x[2]])) + def test_incidence_with_mutable_parameter(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3]) + m.p = pyo.Param(mutable=True, initialize=None) + expr = m.x[1] + m.p * m.x[1] * m.x[2] + m.x[1] * pyo.exp(m.x[3]) + variables = self._get_incident_variables(expr) + self.assertEqual(ComponentSet(variables), ComponentSet(m.x[:])) + class TestIncidenceIdentifyVariables(unittest.TestCase, _TestIncidence): def _get_incident_variables(self, expr, **kwds): @@ -192,6 +215,25 @@ def test_variable_minus_itself(self): var_set = ComponentSet(variables) self.assertEqual(var_set, ComponentSet(m.x[:])) + def test_incidence_with_mutable_parameter(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3]) + m.p = pyo.Param(mutable=True, initialize=None) + expr = m.x[1] + m.p * m.x[1] * m.x[2] + m.x[1] * pyo.exp(m.x[3]) + variables = self._get_incident_variables(expr) + self.assertEqual(ComponentSet(variables), ComponentSet(m.x[:])) + + +class TestIncidenceAmplRepn( + unittest.TestCase, + _TestIncidence, + _TestIncidenceLinearOnly, + _TestIncidenceLinearCancellation, +): + def _get_incident_variables(self, expr, **kwds): + method = IncidenceMethod.ampl_repn + return get_incident_variables(expr, method=method, **kwds) + if __name__ == "__main__": unittest.main() From 515f7e59705ec6b95f6784f42a69ed2b1517161f Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 12 Dec 2023 12:09:44 -0700 Subject: [PATCH 0239/3044] apply black --- pyomo/contrib/incidence_analysis/incidence.py | 9 ++++----- pyomo/contrib/incidence_analysis/tests/test_incidence.py | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index f16b248463c..5ac7b49fa1f 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -103,10 +103,11 @@ def _get_incident_via_ampl_repn(expr, linear_only): AMPLRepn.ActiveVisitor = None nonlinear_var_ids = [] if repn.nonlinear is None else repn.nonlinear[1] - nonlinear_vars = [var_map[v_id] for v_id in nonlinear_var_ids] + nonlinear_vars = [var_map[v_id] for v_id in nonlinear_var_ids] nonlinear_vid_set = set(nonlinear_var_ids) linear_only_vars = [ - var_map[v_id] for v_id, coef in repn.linear.items() + var_map[v_id] + for v_id, coef in repn.linear.items() if coef != 0.0 and v_id not in nonlinear_vid_set ] if linear_only: @@ -163,9 +164,7 @@ def get_incident_variables(expr, **kwds): "linear_only=True is not supported when using identify_variables" ) if include_fixed and method is IncidenceMethod.ampl_repn: - raise RuntimeError( - "include_fixed=True is not supported when using ampl_repn" - ) + raise RuntimeError("include_fixed=True is not supported when using ampl_repn") if method is IncidenceMethod.identify_variables: return _get_incident_via_identify_variables(expr, include_fixed) elif method is IncidenceMethod.standard_repn: diff --git a/pyomo/contrib/incidence_analysis/tests/test_incidence.py b/pyomo/contrib/incidence_analysis/tests/test_incidence.py index e37e4f97691..bcf867c619a 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_incidence.py +++ b/pyomo/contrib/incidence_analysis/tests/test_incidence.py @@ -90,6 +90,7 @@ def test_linear_only(self): class _TestIncidenceLinearCancellation(object): """Tests for methods that perform linear cancellation""" + def _get_incident_variables(self, expr): raise NotImplementedError( "_TestIncidenceLinearCancellation should not be used directly" From 8d5c737f551b3a513e13499957cc20dba86ec771 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 12 Dec 2023 12:10:58 -0700 Subject: [PATCH 0240/3044] add docstring to TestLinearOnly helper class --- pyomo/contrib/incidence_analysis/tests/test_incidence.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/incidence_analysis/tests/test_incidence.py b/pyomo/contrib/incidence_analysis/tests/test_incidence.py index bcf867c619a..78493ecc651 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_incidence.py +++ b/pyomo/contrib/incidence_analysis/tests/test_incidence.py @@ -65,6 +65,8 @@ def test_incidence_with_fixed_variable(self): class _TestIncidenceLinearOnly(object): + """Tests for methods that support linear_only""" + def _get_incident_variables(self, expr): raise NotImplementedError( "_TestIncidenceLinearOnly should not be used directly" From 45eb8616c67058b895ce49ebca9aa61877731976 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 12 Dec 2023 12:12:35 -0700 Subject: [PATCH 0241/3044] fix typo --- pyomo/contrib/incidence_analysis/tests/test_incidence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/tests/test_incidence.py b/pyomo/contrib/incidence_analysis/tests/test_incidence.py index 78493ecc651..87a9178dc1a 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_incidence.py +++ b/pyomo/contrib/incidence_analysis/tests/test_incidence.py @@ -135,7 +135,7 @@ def test_fixed_zero_linear_coefficient(self): self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1], m.x[2]])) # NOTE: This test assumes that all methods that support linear cancellation - # accept a linear_only argument. If this changes, this test wil need to be + # accept a linear_only argument. If this changes, this test will need to be # moved. def test_fixed_zero_coefficient_linear_only(self): m = pyo.ConcreteModel() From 682e054a217cbc37a8c444efd38d2df491603cd3 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 12 Dec 2023 13:48:44 -0700 Subject: [PATCH 0242/3044] set export_defined_variables=False and add TODO comment about exploiting this later --- pyomo/contrib/incidence_analysis/incidence.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index 5ac7b49fa1f..b2cb23dc8c7 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -83,7 +83,10 @@ def _get_incident_via_ampl_repn(expr, linear_only): var_map = {} used_named_expressions = set() symbolic_solver_labels = False - export_defined_variables = True + # TODO: Explore potential performance benefit of exporting defined variables. + # This likely only shows up if we can preserve the subexpression cache across + # multiple constraint expressions. + export_defined_variables = False sorter = FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) visitor = AMPLRepnVisitor( text_nl_template, From c8ed1cda1a562788348157e72f90e103421af5d7 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 12 Dec 2023 13:53:09 -0700 Subject: [PATCH 0243/3044] add test that uses named expression --- pyomo/contrib/incidence_analysis/tests/test_incidence.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyomo/contrib/incidence_analysis/tests/test_incidence.py b/pyomo/contrib/incidence_analysis/tests/test_incidence.py index 87a9178dc1a..2354b0efc39 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_incidence.py +++ b/pyomo/contrib/incidence_analysis/tests/test_incidence.py @@ -63,6 +63,15 @@ def test_incidence_with_fixed_variable(self): var_set = ComponentSet(variables) self.assertEqual(var_set, ComponentSet([m.x[1], m.x[3]])) + def test_incidence_with_named_expression(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3]) + m.subexpr = pyo.Expression(pyo.Integers) + m.subexpr[1] = m.x[1] * pyo.exp(m.x[3]) + expr = m.x[1] + m.x[1] * m.x[2] + m.subexpr[1] + variables = self._get_incident_variables(expr) + self.assertEqual(ComponentSet(variables), ComponentSet(m.x[:])) + class _TestIncidenceLinearOnly(object): """Tests for methods that support linear_only""" From 4354754edb50c88d1671ecd7678237821455eeab Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 13 Dec 2023 12:48:31 -0700 Subject: [PATCH 0244/3044] Adding a test for multidimensional indexed sequence vars --- pyomo/contrib/cp/tests/test_sequence_var.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index 8167c9f5b3b..37190ebca89 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -141,3 +141,17 @@ def test_pprint(self): a : [i[a,1], i[a,2]] b : [i[b,1], i[b,2]]""".strip(), ) + + def test_multidimensional_index(self): + m = self.make_model() + @m.SequenceVar(m.alphabetic, m.numeric) + def s(m, i, j): + return [m.i[i, j],] + + self.assertIsInstance(m.s, IndexedSequenceVar) + self.assertEqual(len(m.s), 4) + for i in m.alphabetic: + for j in m.numeric: + self.assertTrue((i, j) in m.s) + self.assertEqual(len(m.s[i, j]), 1) + self.assertIs(m.s[i, j].interval_vars[0], m.i[i, j]) From 6675566fe7f4c7bf19832a5a72c09e7235cb6c8e Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 13 Dec 2023 13:19:16 -0700 Subject: [PATCH 0245/3044] re-use visitor when iterating over constraints --- pyomo/contrib/incidence_analysis/incidence.py | 61 ++++++++++--------- pyomo/contrib/incidence_analysis/interface.py | 39 ++++++++++-- 2 files changed, 67 insertions(+), 33 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index b2cb23dc8c7..7632f81e38a 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -76,34 +76,38 @@ def _get_incident_via_standard_repn(expr, include_fixed, linear_only): return unique_variables -def _get_incident_via_ampl_repn(expr, linear_only): - subexpression_cache = {} - subexpression_order = [] - external_functions = {} - var_map = {} - used_named_expressions = set() - symbolic_solver_labels = False - # TODO: Explore potential performance benefit of exporting defined variables. - # This likely only shows up if we can preserve the subexpression cache across - # multiple constraint expressions. - export_defined_variables = False - sorter = FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) - visitor = AMPLRepnVisitor( - text_nl_template, - subexpression_cache, - subexpression_order, - external_functions, - var_map, - used_named_expressions, - symbolic_solver_labels, - export_defined_variables, - sorter, - ) - AMPLRepn.ActiveVisitor = visitor - try: +def _get_incident_via_ampl_repn(expr, linear_only, visitor=None): + if visitor is None: + subexpression_cache = {} + subexpression_order = [] + external_functions = {} + var_map = {} + used_named_expressions = set() + symbolic_solver_labels = False + # TODO: Explore potential performance benefit of exporting defined variables. + # This likely only shows up if we can preserve the subexpression cache across + # multiple constraint expressions. + export_defined_variables = False + sorter = FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) + visitor = AMPLRepnVisitor( + text_nl_template, + subexpression_cache, + subexpression_order, + external_functions, + var_map, + used_named_expressions, + symbolic_solver_labels, + export_defined_variables, + sorter, + ) + AMPLRepn.ActiveVisitor = visitor + try: + repn = visitor.walk_expression((expr, None, 0, 1.0)) + finally: + AMPLRepn.ActiveVisitor = None + else: + var_map = visitor.var_map repn = visitor.walk_expression((expr, None, 0, 1.0)) - finally: - AMPLRepn.ActiveVisitor = None nonlinear_var_ids = [] if repn.nonlinear is None else repn.nonlinear[1] nonlinear_vars = [var_map[v_id] for v_id in nonlinear_var_ids] @@ -158,6 +162,7 @@ def get_incident_variables(expr, **kwds): ['x[1]', 'x[2]'] """ + visitor = kwds.pop("visitor", None) config = IncidenceConfig(kwds) method = config.method include_fixed = config.include_fixed @@ -173,7 +178,7 @@ def get_incident_variables(expr, **kwds): elif method is IncidenceMethod.standard_repn: return _get_incident_via_standard_repn(expr, include_fixed, linear_only) elif method is IncidenceMethod.ampl_repn: - return _get_incident_via_ampl_repn(expr, linear_only) + return _get_incident_via_ampl_repn(expr, linear_only, visitor=visitor) else: raise ValueError( f"Unrecognized value {method} for the method used to identify incident" diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index e922551c6a4..60e77d26f7a 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -29,7 +29,7 @@ plotly, ) from pyomo.common.deprecation import deprecated -from pyomo.contrib.incidence_analysis.config import IncidenceConfig +from pyomo.contrib.incidence_analysis.config import IncidenceConfig, IncidenceMethod from pyomo.contrib.incidence_analysis.matching import maximum_matching from pyomo.contrib.incidence_analysis.connected import get_independent_submatrices from pyomo.contrib.incidence_analysis.triangularize import ( @@ -45,6 +45,8 @@ ) from pyomo.contrib.incidence_analysis.incidence import get_incident_variables from pyomo.contrib.pynumero.asl import AmplInterface +from pyomo.repn.plugins.nl_writer import AMPLRepnVisitor, AMPLRepn, text_nl_template +from pyomo.repn.util import FileDeterminism, FileDeterminism_to_SortComponents pyomo_nlp, pyomo_nlp_available = attempt_import( 'pyomo.contrib.pynumero.interfaces.pyomo_nlp' @@ -99,10 +101,37 @@ def get_bipartite_incidence_graph(variables, constraints, **kwds): graph.add_nodes_from(range(M), bipartite=0) graph.add_nodes_from(range(M, M + N), bipartite=1) var_node_map = ComponentMap((v, M + i) for i, v in enumerate(variables)) - for i, con in enumerate(constraints): - for var in get_incident_variables(con.body, **config): - if var in var_node_map: - graph.add_edge(i, var_node_map[var]) + + if config.method == IncidenceMethod.ampl_repn: + subexpression_cache = {} + subexpression_order = [] + external_functions = {} + used_named_expressions = set() + symbolic_solver_labels = False + export_defined_variables = False + sorter = FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) + visitor = AMPLRepnVisitor( + text_nl_template, + subexpression_cache, + subexpression_order, + external_functions, + var_map, + used_named_expressions, + symbolic_solver_labels, + export_defined_variables, + sorter, + ) + else: + visitor = None + + AMPLRepn.ActiveVisitor = visitor + try: + for i, con in enumerate(constraints): + for var in get_incident_variables(con.body, visitor=visitor, **config): + if var in var_node_map: + graph.add_edge(i, var_node_map[var]) + finally: + AMPLRepn.ActiveVisitor = None return graph From bcd2435ebca336996893bfcb1243a54f3055d7f0 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 13 Dec 2023 13:40:40 -0700 Subject: [PATCH 0246/3044] add IncidenceMethod.standard_repn_compute_values option --- pyomo/contrib/incidence_analysis/config.py | 5 + pyomo/contrib/incidence_analysis/incidence.py | 16 ++- .../tests/test_incidence.py | 132 ++++++++++++------ 3 files changed, 111 insertions(+), 42 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/config.py b/pyomo/contrib/incidence_analysis/config.py index a107792a9cd..62856047121 100644 --- a/pyomo/contrib/incidence_analysis/config.py +++ b/pyomo/contrib/incidence_analysis/config.py @@ -24,6 +24,11 @@ class IncidenceMethod(enum.Enum): standard_repn = 1 """Use ``pyomo.repn.standard_repn.generate_standard_repn``""" + standard_repn_compute_values = 2 + """Use ``pyomo.repn.standard_repn.generate_standard_repn`` with + ``compute_values=True`` + """ + _include_fixed = ConfigValue( default=False, diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index 1852cf75648..b8dcd27c685 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -29,7 +29,9 @@ def _get_incident_via_identify_variables(expr, include_fixed): return list(identify_variables(expr, include_fixed=include_fixed)) -def _get_incident_via_standard_repn(expr, include_fixed, linear_only): +def _get_incident_via_standard_repn( + expr, include_fixed, linear_only, compute_values=False +): if include_fixed: to_unfix = [ var for var in identify_variables(expr, include_fixed=True) if var.fixed @@ -39,7 +41,9 @@ def _get_incident_via_standard_repn(expr, include_fixed, linear_only): context = nullcontext() with context: - repn = generate_standard_repn(expr, compute_values=False, quadratic=False) + repn = generate_standard_repn( + expr, compute_values=compute_values, quadratic=False + ) linear_vars = [] # Check coefficients to make sure we don't include linear variables with @@ -123,7 +127,13 @@ def get_incident_variables(expr, **kwds): if method is IncidenceMethod.identify_variables: return _get_incident_via_identify_variables(expr, include_fixed) elif method is IncidenceMethod.standard_repn: - return _get_incident_via_standard_repn(expr, include_fixed, linear_only) + return _get_incident_via_standard_repn( + expr, include_fixed, linear_only, compute_values=False + ) + elif method is IncidenceMethod.standard_repn_compute_values: + return _get_incident_via_standard_repn( + expr, include_fixed, linear_only, compute_values=True + ) else: raise ValueError( f"Unrecognized value {method} for the method used to identify incident" diff --git a/pyomo/contrib/incidence_analysis/tests/test_incidence.py b/pyomo/contrib/incidence_analysis/tests/test_incidence.py index 7f57dd904a7..b1a8ef1b14c 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_incidence.py +++ b/pyomo/contrib/incidence_analysis/tests/test_incidence.py @@ -56,44 +56,56 @@ def test_basic_incidence(self): def test_incidence_with_fixed_variable(self): m = pyo.ConcreteModel() - m.x = pyo.Var([1, 2, 3]) + m.x = pyo.Var([1, 2, 3], initialize=1.0) expr = m.x[1] + m.x[1] * m.x[2] + m.x[1] * pyo.exp(m.x[3]) m.x[2].fix() variables = self._get_incident_variables(expr) var_set = ComponentSet(variables) self.assertEqual(var_set, ComponentSet([m.x[1], m.x[3]])) - def test_incidence_with_mutable_parameter(self): + def test_incidence_with_named_expression(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2, 3]) - m.p = pyo.Param(mutable=True, initialize=None) - expr = m.x[1] + m.p * m.x[1] * m.x[2] + m.x[1] * pyo.exp(m.x[3]) + m.subexpr = pyo.Expression(pyo.Integers) + m.subexpr[1] = m.x[1] * pyo.exp(m.x[3]) + expr = m.x[1] + m.x[1] * m.x[2] + m.subexpr[1] variables = self._get_incident_variables(expr) self.assertEqual(ComponentSet(variables), ComponentSet(m.x[:])) -class TestIncidenceStandardRepn(unittest.TestCase, _TestIncidence): - def _get_incident_variables(self, expr, **kwds): - method = IncidenceMethod.standard_repn - return get_incident_variables(expr, method=method, **kwds) +class _TestIncidenceLinearOnly(object): + """Tests for methods that support linear_only""" - def test_assumed_standard_repn_behavior(self): + def _get_incident_variables(self, expr): + raise NotImplementedError( + "_TestIncidenceLinearOnly should not be used directly" + ) + + def test_linear_only(self): m = pyo.ConcreteModel() - m.x = pyo.Var([1, 2]) - m.p = pyo.Param(initialize=0.0) + m.x = pyo.Var([1, 2, 3]) - # We rely on variables with constant coefficients of zero not appearing - # in the standard repn (as opposed to appearing with explicit - # coefficients of zero). - expr = m.x[1] + 0 * m.x[2] - repn = generate_standard_repn(expr) - self.assertEqual(len(repn.linear_vars), 1) - self.assertIs(repn.linear_vars[0], m.x[1]) + expr = 2 * m.x[1] + 4 * m.x[2] * m.x[1] - m.x[1] * pyo.exp(m.x[3]) + variables = self._get_incident_variables(expr, linear_only=True) + self.assertEqual(len(variables), 0) - expr = m.p * m.x[1] + m.x[2] - repn = generate_standard_repn(expr) - self.assertEqual(len(repn.linear_vars), 1) - self.assertIs(repn.linear_vars[0], m.x[2]) + expr = 2 * m.x[1] + 2 * m.x[2] * m.x[3] + 3 * m.x[2] + variables = self._get_incident_variables(expr, linear_only=True) + self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1]])) + + m.x[3].fix(2.5) + expr = 2 * m.x[1] + 2 * m.x[2] * m.x[3] + 3 * m.x[2] + variables = self._get_incident_variables(expr, linear_only=True) + self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1], m.x[2]])) + + +class _TestIncidenceLinearCancellation(object): + """Tests for methods that perform linear cancellation""" + + def _get_incident_variables(self, expr): + raise NotImplementedError( + "_TestIncidenceLinearCancellation should not be used directly" + ) def test_zero_coef(self): m = pyo.ConcreteModel() @@ -113,23 +125,6 @@ def test_variable_minus_itself(self): var_set = ComponentSet(variables) self.assertEqual(var_set, ComponentSet([m.x[2], m.x[3]])) - def test_linear_only(self): - m = pyo.ConcreteModel() - m.x = pyo.Var([1, 2, 3]) - - expr = 2 * m.x[1] + 4 * m.x[2] * m.x[1] - m.x[1] * pyo.exp(m.x[3]) - variables = self._get_incident_variables(expr, linear_only=True) - self.assertEqual(len(variables), 0) - - expr = 2 * m.x[1] + 2 * m.x[2] * m.x[3] + 3 * m.x[2] - variables = self._get_incident_variables(expr, linear_only=True) - self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1]])) - - m.x[3].fix(2.5) - expr = 2 * m.x[1] + 2 * m.x[2] * m.x[3] + 3 * m.x[2] - variables = self._get_incident_variables(expr, linear_only=True) - self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1], m.x[2]])) - def test_fixed_zero_linear_coefficient(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2, 3]) @@ -148,6 +143,9 @@ def test_fixed_zero_linear_coefficient(self): variables = self._get_incident_variables(expr) self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1], m.x[2]])) + # NOTE: This test assumes that all methods that support linear cancellation + # accept a linear_only argument. If this changes, this test will need to be + # moved. def test_fixed_zero_coefficient_linear_only(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2, 3]) @@ -159,6 +157,35 @@ def test_fixed_zero_coefficient_linear_only(self): self.assertEqual(len(variables), 1) self.assertIs(variables[0], m.x[3]) + +class TestIncidenceStandardRepn( + unittest.TestCase, + _TestIncidence, + _TestIncidenceLinearOnly, + _TestIncidenceLinearCancellation, +): + def _get_incident_variables(self, expr, **kwds): + method = IncidenceMethod.standard_repn + return get_incident_variables(expr, method=method, **kwds) + + def test_assumed_standard_repn_behavior(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2]) + m.p = pyo.Param(initialize=0.0) + + # We rely on variables with constant coefficients of zero not appearing + # in the standard repn (as opposed to appearing with explicit + # coefficients of zero). + expr = m.x[1] + 0 * m.x[2] + repn = generate_standard_repn(expr) + self.assertEqual(len(repn.linear_vars), 1) + self.assertIs(repn.linear_vars[0], m.x[1]) + + expr = m.p * m.x[1] + m.x[2] + repn = generate_standard_repn(expr) + self.assertEqual(len(repn.linear_vars), 1) + self.assertIs(repn.linear_vars[0], m.x[2]) + def test_fixed_none_linear_coefficient(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2, 3]) @@ -168,6 +195,14 @@ def test_fixed_none_linear_coefficient(self): variables = self._get_incident_variables(expr) self.assertEqual(ComponentSet(variables), ComponentSet([m.x[1], m.x[2]])) + def test_incidence_with_mutable_parameter(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3]) + m.p = pyo.Param(mutable=True, initialize=None) + expr = m.x[1] + m.p * m.x[1] * m.x[2] + m.x[1] * pyo.exp(m.x[3]) + variables = self._get_incident_variables(expr) + self.assertEqual(ComponentSet(variables), ComponentSet(m.x[:])) + class TestIncidenceIdentifyVariables(unittest.TestCase, _TestIncidence): def _get_incident_variables(self, expr, **kwds): @@ -192,6 +227,25 @@ def test_variable_minus_itself(self): var_set = ComponentSet(variables) self.assertEqual(var_set, ComponentSet(m.x[:])) + def test_incidence_with_mutable_parameter(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3]) + m.p = pyo.Param(mutable=True, initialize=None) + expr = m.x[1] + m.p * m.x[1] * m.x[2] + m.x[1] * pyo.exp(m.x[3]) + variables = self._get_incident_variables(expr) + self.assertEqual(ComponentSet(variables), ComponentSet(m.x[:])) + + +class TestIncidenceStandardRepnComputeValues( + unittest.TestCase, + _TestIncidence, + _TestIncidenceLinearOnly, + _TestIncidenceLinearCancellation, +): + def _get_incident_variables(self, expr, **kwds): + method = IncidenceMethod.standard_repn_compute_values + return get_incident_variables(expr, method=method, **kwds) + if __name__ == "__main__": unittest.main() From ecaf0530ba1c85f75afd82a3662abe639d1bf8bf Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 13 Dec 2023 13:59:26 -0700 Subject: [PATCH 0247/3044] re-add var_map local variable --- pyomo/contrib/incidence_analysis/interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 60e77d26f7a..726398f7750 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -106,6 +106,7 @@ def get_bipartite_incidence_graph(variables, constraints, **kwds): subexpression_cache = {} subexpression_order = [] external_functions = {} + var_map = {} used_named_expressions = set() symbolic_solver_labels = False export_defined_variables = False From 9236f4f1d1d0e8b49c7cbb3da37135ab0a2ad8e8 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 13 Dec 2023 13:59:50 -0700 Subject: [PATCH 0248/3044] filter duplicates from list of nonlinear vars --- pyomo/contrib/incidence_analysis/incidence.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index 7632f81e38a..feb8689a7c3 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -110,12 +110,18 @@ def _get_incident_via_ampl_repn(expr, linear_only, visitor=None): repn = visitor.walk_expression((expr, None, 0, 1.0)) nonlinear_var_ids = [] if repn.nonlinear is None else repn.nonlinear[1] - nonlinear_vars = [var_map[v_id] for v_id in nonlinear_var_ids] - nonlinear_vid_set = set(nonlinear_var_ids) + nonlinear_var_id_set = set() + unique_nonlinear_var_ids = [] + for v_id in nonlinear_var_ids: + if v_id not in nonlinear_var_id_set: + nonlinear_var_id_set.add(v_id) + unique_nonlinear_var_ids.append(v_id) + + nonlinear_vars = [var_map[v_id] for v_id in unique_nonlinear_var_ids] linear_only_vars = [ var_map[v_id] for v_id, coef in repn.linear.items() - if coef != 0.0 and v_id not in nonlinear_vid_set + if coef != 0.0 and v_id not in nonlinear_var_id_set ] if linear_only: return linear_only_vars From c04264005f6c7729b7a2054e8c7a96c389f13ef8 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 13 Dec 2023 14:31:33 -0700 Subject: [PATCH 0249/3044] re-use visitor in _generate_variables_in_constraints --- pyomo/contrib/incidence_analysis/interface.py | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 726398f7750..ce5f4780210 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -194,12 +194,45 @@ def extract_bipartite_subgraph(graph, nodes0, nodes1): def _generate_variables_in_constraints(constraints, **kwds): config = IncidenceConfig(kwds) - known_vars = ComponentSet() - for con in constraints: - for var in get_incident_variables(con.body, **config): - if var not in known_vars: - known_vars.add(var) - yield var + + if config.method == IncidenceMethod.ampl_repn: + subexpression_cache = {} + subexpression_order = [] + external_functions = {} + var_map = {} + used_named_expressions = set() + symbolic_solver_labels = False + export_defined_variables = False + sorter = FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) + visitor = AMPLRepnVisitor( + text_nl_template, + subexpression_cache, + subexpression_order, + external_functions, + var_map, + used_named_expressions, + symbolic_solver_labels, + export_defined_variables, + sorter, + ) + else: + visitor = None + + AMPLRepn.ActiveVisitor = visitor + try: + known_vars = ComponentSet() + for con in constraints: + for var in get_incident_variables(con.body, visitor=visitor, **config): + if var not in known_vars: + known_vars.add(var) + yield var + finally: + # NOTE: I believe this is only guaranteed to be called when the + # generator is garbage collected. This could lead to some nasty + # bug where ActiveVisitor is set for longer than we intend. + # TODO: Convert this into a function. (or yield from variables + # after this try/finally. + AMPLRepn.ActiveVisitor = None def get_structural_incidence_matrix(variables, constraints, **kwds): From a167c4e2568ea4e30bed4ce19d1565816ad71f3a Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 14 Dec 2023 13:40:10 -0700 Subject: [PATCH 0250/3044] Removing unused import --- pyomo/gdp/plugins/multiple_bigm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 18f159c7ca2..6a45c9ebc73 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -31,7 +31,6 @@ NonNegativeIntegers, Objective, Param, - RangeSet, Set, SetOf, SortComponents, From 78b225807637e5db3f9fa8bf1aef7aee934996e1 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 14 Dec 2023 13:58:42 -0700 Subject: [PATCH 0251/3044] Fixing a silly indentation bug that happens when there are empty constraint containers on Disjuncts --- pyomo/gdp/plugins/multiple_bigm.py | 4 +-- pyomo/gdp/tests/test_mbigm.py | 57 +++++++++++++++++++++--------- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 6a45c9ebc73..e66dcb3bb88 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -426,8 +426,8 @@ def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): constraintMap, ) - # deactivate now that we have transformed - c.deactivate() + # deactivate now that we have transformed + c.deactivate() def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): # first we're just going to find all of them diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index f067e1da5af..7ab34153468 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -49,8 +49,24 @@ ) exdir = normpath(join(PYOMO_ROOT_DIR, 'examples', 'gdp')) +class CommonTests(unittest.TestCase): + def check_pretty_bound_constraints(self, cons, var, bounds, lb): + self.assertEqual(value(cons.upper), 0) + self.assertIsNone(cons.lower) + repn = generate_standard_repn(cons.body) + self.assertTrue(repn.is_linear()) + self.assertEqual(len(repn.linear_vars), len(bounds) + 1) + self.assertEqual(repn.constant, 0) + if lb: + check_linear_coef(self, repn, var, -1) + for disj, bnd in bounds.items(): + check_linear_coef(self, repn, disj.binary_indicator_var, bnd) + else: + check_linear_coef(self, repn, var, 1) + for disj, bnd in bounds.items(): + check_linear_coef(self, repn, disj.binary_indicator_var, -bnd) -class LinearModelDecisionTreeExample(unittest.TestCase): +class LinearModelDecisionTreeExample(CommonTests): def make_model(self): m = ConcreteModel() m.x1 = Var(bounds=(-10, 10)) @@ -381,22 +397,6 @@ def test_algebraic_constraints(self): check_linear_coef(self, repn, m.d3.binary_indicator_var, 1) check_obj_in_active_tree(self, xor) - def check_pretty_bound_constraints(self, cons, var, bounds, lb): - self.assertEqual(value(cons.upper), 0) - self.assertIsNone(cons.lower) - repn = generate_standard_repn(cons.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(len(repn.linear_vars), len(bounds) + 1) - self.assertEqual(repn.constant, 0) - if lb: - check_linear_coef(self, repn, var, -1) - for disj, bnd in bounds.items(): - check_linear_coef(self, repn, disj.binary_indicator_var, bnd) - else: - check_linear_coef(self, repn, var, 1) - for disj, bnd in bounds.items(): - check_linear_coef(self, repn, disj.binary_indicator_var, -bnd) - def test_bounds_constraints_correct(self): m = self.make_model() @@ -876,6 +876,29 @@ class NestedDisjunctsInFlatGDP(unittest.TestCase): def test_declare_disjuncts_in_disjunction_rule(self): check_nested_disjuncts_in_flat_gdp(self, 'bigm') +class IndexedDisjunctiveConstraints(CommonTests): + def test_empty_constraint_container_on_Disjunct(self): + m = ConcreteModel() + m.d = Disjunct() + m.e = Disjunct() + m.d.c = Constraint(['s', 'i', 'l', 'L', 'y']) + m.x = Var(bounds=(2, 3)) + m.e.c = Constraint(expr=m.x == 2.7) + m.disjunction = Disjunction(expr=[m.d, m.e]) + + mbm = TransformationFactory('gdp.mbigm') + mbm.apply_to(m) + + cons = mbm.get_transformed_constraints(m.e.c) + self.assertEqual(len(cons), 2) + self.check_pretty_bound_constraints( + cons[0], m.x, {m.d: 2, m.e: 2.7}, lb=True + + ) + self.check_pretty_bound_constraints( + cons[1], m.x, {m.d: 3, m.e: 2.7}, lb=False + ) + @unittest.skipUnless(gurobi_available, "Gurobi is not available") class IndexedDisjunction(unittest.TestCase): From 7fe251e05a6a7a333d29d80a49ccef21312de7e4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 14 Dec 2023 14:00:44 -0700 Subject: [PATCH 0252/3044] Taking out the Suffix paranoia in mbigm--it can just ignore Suffixes --- pyomo/gdp/plugins/multiple_bigm.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index e66dcb3bb88..b2f4b5f6e12 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -34,7 +34,6 @@ Set, SetOf, SortComponents, - Suffix, value, Var, ) @@ -200,7 +199,6 @@ class MultipleBigMTransformation(GDP_to_MIP_Transformation, _BigM_MixIn): def __init__(self): super().__init__(logger) - self.handlers[Suffix] = self._warn_for_active_suffix self._arg_list = {} self._set_up_expr_bound_visitor() @@ -345,13 +343,6 @@ def _transform_disjunct(self, obj, transBlock, active_disjuncts, Ms): # deactivate disjunct so writers can be happy obj._deactivate_without_fixing_indicator() - def _warn_for_active_suffix(self, obj, disjunct, active_disjuncts, Ms): - raise GDP_Error( - "Found active Suffix '{0}' on Disjunct '{1}'. " - "The multiple bigM transformation does not currently " - "support Suffixes.".format(obj.name, disjunct.name) - ) - def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): # we will put a new transformed constraint on the relaxation block. relaxationBlock = disjunct._transformation_block() From dbabb67fe65294088f1fe959b7b1203882374ca1 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 14 Dec 2023 14:30:08 -0700 Subject: [PATCH 0253/3044] Revert "Taking out the Suffix paranoia in mbigm--it can just ignore Suffixes" This reverts commit 7fe251e05a6a7a333d29d80a49ccef21312de7e4. --- pyomo/gdp/plugins/multiple_bigm.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index b2f4b5f6e12..e66dcb3bb88 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -34,6 +34,7 @@ Set, SetOf, SortComponents, + Suffix, value, Var, ) @@ -199,6 +200,7 @@ class MultipleBigMTransformation(GDP_to_MIP_Transformation, _BigM_MixIn): def __init__(self): super().__init__(logger) + self.handlers[Suffix] = self._warn_for_active_suffix self._arg_list = {} self._set_up_expr_bound_visitor() @@ -343,6 +345,13 @@ def _transform_disjunct(self, obj, transBlock, active_disjuncts, Ms): # deactivate disjunct so writers can be happy obj._deactivate_without_fixing_indicator() + def _warn_for_active_suffix(self, obj, disjunct, active_disjuncts, Ms): + raise GDP_Error( + "Found active Suffix '{0}' on Disjunct '{1}'. " + "The multiple bigM transformation does not currently " + "support Suffixes.".format(obj.name, disjunct.name) + ) + def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): # we will put a new transformed constraint on the relaxation block. relaxationBlock = disjunct._transformation_block() From 3d84f38d0f3aacbaf8a7f021bb5fdb436af6c699 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 18 Dec 2023 14:57:52 -0700 Subject: [PATCH 0254/3044] Testing that we log a more polite warning about BigM Suffixes, though we don't yet if they are on the model --- pyomo/gdp/plugins/multiple_bigm.py | 12 +++++++----- pyomo/gdp/tests/test_mbigm.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index e66dcb3bb88..d40486406ab 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -346,11 +346,13 @@ def _transform_disjunct(self, obj, transBlock, active_disjuncts, Ms): obj._deactivate_without_fixing_indicator() def _warn_for_active_suffix(self, obj, disjunct, active_disjuncts, Ms): - raise GDP_Error( - "Found active Suffix '{0}' on Disjunct '{1}'. " - "The multiple bigM transformation does not currently " - "support Suffixes.".format(obj.name, disjunct.name) - ) + if obj.name == 'BigM': + logger.warning( + "Found active 'BigM' Suffix on '{0}'. " + "The multiple bigM transformation does not currently " + "support specifying M's with Suffixes and is ignoring " + "this Suffix.".format(disjunct.name) + ) def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): # we will put a new transformed constraint on the relaxation block. diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index 7ab34153468..d2c49958df6 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -494,6 +494,24 @@ def test_Ms_specified_as_args_honored(self): cons[1], m.x2, m.d2, m.disjunction, {m.d1: 3, m.d3: 110}, upper=10 ) + def test_log_warning_for_bigm_suffixes(self): + m = self.make_model() + m.BigM = Suffix(direction=Suffix.LOCAL) + m.BigM[m.d2.x2_bounds] = (-100, 100) + + out = StringIO() + with LoggingIntercept(out, 'pyomo.gdp.mbigm'): + TransformationFactory('gdp.mbigm').apply_to(m) + + warnings = out.getvalue() + self.assertIn( + "Found active 'BigM' Suffix on 'unknown'. " + "The multiple bigM transformation does not currently " + "support specifying M's with Suffixes and is ignoring " + "this Suffix.", + warnings, + ) + # TODO: If Suffixes allow tuple keys then we can support them and it will # look something like this: # def test_Ms_specified_as_suffixes_honored(self): From bafe936a17ba632aae1b0ddc9e661e4c12ff72e4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 18 Dec 2023 15:43:13 -0700 Subject: [PATCH 0255/3044] Changing my mind on the warning for Suffix issue again. --- pyomo/gdp/plugins/multiple_bigm.py | 10 ---------- pyomo/gdp/tests/test_mbigm.py | 18 ------------------ 2 files changed, 28 deletions(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index d40486406ab..2fa26479908 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -200,7 +200,6 @@ class MultipleBigMTransformation(GDP_to_MIP_Transformation, _BigM_MixIn): def __init__(self): super().__init__(logger) - self.handlers[Suffix] = self._warn_for_active_suffix self._arg_list = {} self._set_up_expr_bound_visitor() @@ -345,15 +344,6 @@ def _transform_disjunct(self, obj, transBlock, active_disjuncts, Ms): # deactivate disjunct so writers can be happy obj._deactivate_without_fixing_indicator() - def _warn_for_active_suffix(self, obj, disjunct, active_disjuncts, Ms): - if obj.name == 'BigM': - logger.warning( - "Found active 'BigM' Suffix on '{0}'. " - "The multiple bigM transformation does not currently " - "support specifying M's with Suffixes and is ignoring " - "this Suffix.".format(disjunct.name) - ) - def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): # we will put a new transformed constraint on the relaxation block. relaxationBlock = disjunct._transformation_block() diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index d2c49958df6..46b57d3256d 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -493,24 +493,6 @@ def test_Ms_specified_as_args_honored(self): self.check_untightened_bounds_constraint( cons[1], m.x2, m.d2, m.disjunction, {m.d1: 3, m.d3: 110}, upper=10 ) - - def test_log_warning_for_bigm_suffixes(self): - m = self.make_model() - m.BigM = Suffix(direction=Suffix.LOCAL) - m.BigM[m.d2.x2_bounds] = (-100, 100) - - out = StringIO() - with LoggingIntercept(out, 'pyomo.gdp.mbigm'): - TransformationFactory('gdp.mbigm').apply_to(m) - - warnings = out.getvalue() - self.assertIn( - "Found active 'BigM' Suffix on 'unknown'. " - "The multiple bigM transformation does not currently " - "support specifying M's with Suffixes and is ignoring " - "this Suffix.", - warnings, - ) # TODO: If Suffixes allow tuple keys then we can support them and it will # look something like this: From 3ef0e62b4f984fdce46b644165d1017a0307d2bf Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 18 Dec 2023 15:48:33 -0700 Subject: [PATCH 0256/3044] Adding a warning about silently ignoring BigM Suffix in GDP docs, to assuage my guilt --- doc/OnlineDocs/modeling_extensions/gdp/solving.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/OnlineDocs/modeling_extensions/gdp/solving.rst b/doc/OnlineDocs/modeling_extensions/gdp/solving.rst index 2f3076862e6..9fea90ebf5f 100644 --- a/doc/OnlineDocs/modeling_extensions/gdp/solving.rst +++ b/doc/OnlineDocs/modeling_extensions/gdp/solving.rst @@ -140,6 +140,10 @@ For example, to apply the transformation and store the M values, use: From the Pyomo command line, include the ``--transform pyomo.gdp.mbigm`` option. +.. warning:: + The Multiple Big-M transformation does not currently support Suffixes and will + ignore "BigM" Suffixes. + Hull Reformulation (HR) ^^^^^^^^^^^^^^^^^^^^^^^ From 5d7b74036d188845e3653c848f179acb2d44936f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 19 Dec 2023 08:53:59 -0700 Subject: [PATCH 0257/3044] Begin cleaning up documentation; move tests to more logical configuration --- pyomo/solver/base.py | 67 ++++++++++++++----- pyomo/solver/tests/{ => unit}/test_base.py | 0 pyomo/solver/tests/{ => unit}/test_config.py | 0 pyomo/solver/tests/{ => unit}/test_results.py | 0 .../solver/tests/{ => unit}/test_solution.py | 0 pyomo/solver/tests/{ => unit}/test_util.py | 0 6 files changed, 52 insertions(+), 15 deletions(-) rename pyomo/solver/tests/{ => unit}/test_base.py (100%) rename pyomo/solver/tests/{ => unit}/test_config.py (100%) rename pyomo/solver/tests/{ => unit}/test_results.py (100%) rename pyomo/solver/tests/{ => unit}/test_solution.py (100%) rename pyomo/solver/tests/{ => unit}/test_util.py (100%) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index d4b46ebe5d4..fc361bdaf5e 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -37,6 +37,18 @@ class SolverBase(abc.ABC): + """ + Base class upon which direct solver interfaces can be built. + + This base class contains the required methods for all direct solvers: + - available: Determines whether the solver is able to be run, combining + both whether it can be found on the system and if the license is valid. + - config: The configuration method for solver objects. + - solve: The main method of every solver + - version: The version of the solver + - is_persistent: Set to false for all direct solvers. + """ + # # Support "with" statements. Forgetting to call deactivate # on Plugins is a common source of memory leaks @@ -45,9 +57,14 @@ def __enter__(self): return self def __exit__(self, t, v, traceback): - pass + """Exit statement - enables `with` statements.""" class Availability(enum.IntEnum): + """ + Class to capture different statuses in which a solver can exist in + order to record its availability for use. + """ + FullLicense = 2 LimitedLicense = 1 NotFound = 0 @@ -56,7 +73,7 @@ class Availability(enum.IntEnum): NeedsCompiledExtension = -3 def __bool__(self): - return self._value_ > 0 + return self.real > 0 def __format__(self, format_spec): # We want general formatting of this Enum to return the @@ -93,7 +110,6 @@ def solve( results: Results A results object """ - pass @abc.abstractmethod def available(self): @@ -120,7 +136,6 @@ def available(self): be True if the solver is runable at all and False otherwise. """ - pass @abc.abstractmethod def version(self) -> Tuple: @@ -143,7 +158,6 @@ def config(self): An object for configuring pyomo solve options such as the time limit. These options are mostly independent of the solver. """ - pass def is_persistent(self): """ @@ -156,7 +170,21 @@ def is_persistent(self): class PersistentSolverBase(SolverBase): + """ + Base class upon which persistent solvers can be built. This inherits the + methods from the direct solver base and adds those methods that are necessary + for persistent solvers. + + Example usage can be seen in solvers within APPSI. + """ + def is_persistent(self): + """ + Returns + ------- + is_persistent: bool + True if the solver is a persistent solver. + """ return True def load_vars( @@ -179,7 +207,20 @@ def load_vars( def get_primals( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: - pass + """ + Get mapping of variables to primals. + + Parameters + ---------- + vars_to_load : Optional[Sequence[_GeneralVarData]], optional + Which vars to be populated into the map. The default is None. + + Returns + ------- + Mapping[_GeneralVarData, float] + A map of variables to primals. + + """ def get_duals( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None @@ -198,9 +239,6 @@ def get_duals( duals: dict Maps constraints to dual values """ - raise NotImplementedError( - '{0} does not support the get_duals method'.format(type(self)) - ) def get_slacks( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None @@ -217,9 +255,6 @@ def get_slacks( slacks: dict Maps constraints to slack values """ - raise NotImplementedError( - '{0} does not support the get_slacks method'.format(type(self)) - ) def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None @@ -236,9 +271,6 @@ def get_reduced_costs( reduced_costs: ComponentMap Maps variable to reduced cost """ - raise NotImplementedError( - '{0} does not support the get_reduced_costs method'.format(type(self)) - ) @property @abc.abstractmethod @@ -295,6 +327,11 @@ def update_params(self): class LegacySolverInterface: + """ + Class to map the new solver interface features into the legacy solver + interface. Necessary for backwards compatibility. + """ + def solve( self, model: _BlockData, diff --git a/pyomo/solver/tests/test_base.py b/pyomo/solver/tests/unit/test_base.py similarity index 100% rename from pyomo/solver/tests/test_base.py rename to pyomo/solver/tests/unit/test_base.py diff --git a/pyomo/solver/tests/test_config.py b/pyomo/solver/tests/unit/test_config.py similarity index 100% rename from pyomo/solver/tests/test_config.py rename to pyomo/solver/tests/unit/test_config.py diff --git a/pyomo/solver/tests/test_results.py b/pyomo/solver/tests/unit/test_results.py similarity index 100% rename from pyomo/solver/tests/test_results.py rename to pyomo/solver/tests/unit/test_results.py diff --git a/pyomo/solver/tests/test_solution.py b/pyomo/solver/tests/unit/test_solution.py similarity index 100% rename from pyomo/solver/tests/test_solution.py rename to pyomo/solver/tests/unit/test_solution.py diff --git a/pyomo/solver/tests/test_util.py b/pyomo/solver/tests/unit/test_util.py similarity index 100% rename from pyomo/solver/tests/test_util.py rename to pyomo/solver/tests/unit/test_util.py From 2dee6b7fee8cf394c1502b83e4989217ad95413f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 19 Dec 2023 09:29:04 -0700 Subject: [PATCH 0258/3044] Reinstantiate NotImplementedErrors for PersistentBase; update tests --- pyomo/solver/base.py | 106 +++++++++++++++++++++------ pyomo/solver/tests/unit/test_base.py | 4 +- 2 files changed, 88 insertions(+), 22 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index fc361bdaf5e..c025e2028ce 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -73,7 +73,7 @@ class Availability(enum.IntEnum): NeedsCompiledExtension = -3 def __bool__(self): - return self.real > 0 + return self._value_ > 0 def __format__(self, format_spec): # We want general formatting of this Enum to return the @@ -219,8 +219,10 @@ def get_primals( ------- Mapping[_GeneralVarData, float] A map of variables to primals. - """ + raise NotImplementedError( + '{0} does not support the get_primals method'.format(type(self)) + ) def get_duals( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None @@ -239,6 +241,9 @@ def get_duals( duals: dict Maps constraints to dual values """ + raise NotImplementedError( + '{0} does not support the get_duals method'.format(type(self)) + ) def get_slacks( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None @@ -255,6 +260,9 @@ def get_slacks( slacks: dict Maps constraints to slack values """ + raise NotImplementedError( + '{0} does not support the get_slacks method'.format(type(self)) + ) def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None @@ -271,59 +279,88 @@ def get_reduced_costs( reduced_costs: ComponentMap Maps variable to reduced cost """ + raise NotImplementedError( + '{0} does not support the get_reduced_costs method'.format(type(self)) + ) @property @abc.abstractmethod def update_config(self) -> UpdateConfig: - pass + """ + Updates the solver config + """ @abc.abstractmethod def set_instance(self, model): - pass + """ + Set an instance of the model + """ @abc.abstractmethod def add_variables(self, variables: List[_GeneralVarData]): - pass + """ + Add variables to the model + """ @abc.abstractmethod def add_params(self, params: List[_ParamData]): - pass + """ + Add parameters to the model + """ @abc.abstractmethod def add_constraints(self, cons: List[_GeneralConstraintData]): - pass + """ + Add constraints to the model + """ @abc.abstractmethod def add_block(self, block: _BlockData): - pass + """ + Add a block to the model + """ @abc.abstractmethod def remove_variables(self, variables: List[_GeneralVarData]): - pass + """ + Remove variables from the model + """ @abc.abstractmethod def remove_params(self, params: List[_ParamData]): - pass + """ + Remove parameters from the model + """ @abc.abstractmethod def remove_constraints(self, cons: List[_GeneralConstraintData]): - pass + """ + Remove constraints from the model + """ @abc.abstractmethod def remove_block(self, block: _BlockData): - pass + """ + Remove a block from the model + """ @abc.abstractmethod def set_objective(self, obj: _GeneralObjectiveData): - pass + """ + Set current objective for the model + """ @abc.abstractmethod def update_variables(self, variables: List[_GeneralVarData]): - pass + """ + Update variables on the model + """ @abc.abstractmethod def update_params(self): - pass + """ + Update parameters on the model + """ class LegacySolverInterface: @@ -332,6 +369,10 @@ class LegacySolverInterface: interface. Necessary for backwards compatibility. """ + def __init__(self): + self.original_config = self.config + self.config = self.config() + def solve( self, model: _BlockData, @@ -347,7 +388,15 @@ def solve( keepfiles: bool = False, symbolic_solver_labels: bool = False, ): - original_config = self.config + """ + Solve method: maps new solve method style to backwards compatible version. + + Returns + ------- + legacy_results + Legacy results object + + """ self.config = self.config() self.config.tee = tee self.config.load_solution = load_solutions @@ -401,10 +450,10 @@ def solve( legacy_soln.gap = None symbol_map = SymbolMap() - symbol_map.byObject = dict(self.symbol_map.byObject) - symbol_map.bySymbol = dict(self.symbol_map.bySymbol) - symbol_map.aliases = dict(self.symbol_map.aliases) - symbol_map.default_labeler = self.symbol_map.default_labeler + symbol_map.byObject = dict(symbol_map.byObject) + symbol_map.bySymbol = dict(symbol_map.bySymbol) + symbol_map.aliases = dict(symbol_map.aliases) + symbol_map.default_labeler = symbol_map.default_labeler model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) @@ -439,12 +488,16 @@ def solve( if delete_legacy_soln: legacy_results.solution.delete(0) - self.config = original_config + self.config = self.original_config self.options = original_options return legacy_results def available(self, exception_flag=True): + """ + Returns a bool determining whether the requested solver is available + on the system. + """ ans = super().available() if exception_flag and not ans: raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).') @@ -468,6 +521,14 @@ def license_is_valid(self) -> bool: @property def options(self): + """ + Read the options for the dictated solver. + + NOTE: Only the set of solvers for which the LegacySolverInterface is compatible + are accounted for within this property. + Not all solvers are currently covered by this backwards compatibility + class. + """ for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: if hasattr(self, solver_name + '_options'): return getattr(self, solver_name + '_options') @@ -475,6 +536,9 @@ def options(self): @options.setter def options(self, val): + """ + Set the options for the dictated solver. + """ found = False for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: if hasattr(self, solver_name + '_options'): diff --git a/pyomo/solver/tests/unit/test_base.py b/pyomo/solver/tests/unit/test_base.py index d8084e9b5b7..b501f8d3dd3 100644 --- a/pyomo/solver/tests/unit/test_base.py +++ b/pyomo/solver/tests/unit/test_base.py @@ -63,7 +63,6 @@ def test_abstract_member_list(self): def test_persistent_solver_base(self): self.instance = base.PersistentSolverBase() self.assertTrue(self.instance.is_persistent()) - self.assertEqual(self.instance.get_primals(), None) self.assertEqual(self.instance.update_config, None) self.assertEqual(self.instance.set_instance(None), None) self.assertEqual(self.instance.add_variables(None), None) @@ -78,6 +77,9 @@ def test_persistent_solver_base(self): self.assertEqual(self.instance.update_variables(None), None) self.assertEqual(self.instance.update_params(), None) + with self.assertRaises(NotImplementedError): + self.instance.get_primals() + with self.assertRaises(NotImplementedError): self.instance.get_duals() From f0c0a07e42e05733d90d41b92db89f5e05194bd1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 19 Dec 2023 09:36:09 -0700 Subject: [PATCH 0259/3044] Revert config changes --- pyomo/solver/base.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index c025e2028ce..1d459450bab 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -369,10 +369,6 @@ class LegacySolverInterface: interface. Necessary for backwards compatibility. """ - def __init__(self): - self.original_config = self.config - self.config = self.config() - def solve( self, model: _BlockData, @@ -397,6 +393,7 @@ def solve( Legacy results object """ + original_config = self.config self.config = self.config() self.config.tee = tee self.config.load_solution = load_solutions @@ -488,7 +485,7 @@ def solve( if delete_legacy_soln: legacy_results.solution.delete(0) - self.config = self.original_config + self.config = original_config self.options = original_options return legacy_results From 35e5ff0d5033ccb50b0e38a33c3f3d91c26626de Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 19 Dec 2023 10:57:46 -0700 Subject: [PATCH 0260/3044] bug fix --- pyomo/solver/util.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/solver/util.py b/pyomo/solver/util.py index ec59f7e80f7..c0c99a00747 100644 --- a/pyomo/solver/util.py +++ b/pyomo/solver/util.py @@ -173,7 +173,7 @@ def update_config(self, val: UpdateConfig): def set_instance(self, model): saved_update_config = self.update_config - self.__init__() + self.__init__(only_child_vars=self._only_child_vars) self.update_config = saved_update_config self._model = model self.add_block(model) @@ -632,17 +632,17 @@ def update(self, timer: HierarchicalTimer = None): vars_to_update = [] for v in vars_to_check: _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] - if lb is not v._lb: - vars_to_update.append(v) - elif ub is not v._ub: - vars_to_update.append(v) - elif (fixed is not v.fixed) or (fixed and (value != v.value)): + if (fixed != v.fixed) or (fixed and (value != v.value)): vars_to_update.append(v) if self.update_config.treat_fixed_vars_as_params: for c in self._referenced_variables[id(v)][0]: cons_to_remove_and_add[c] = None if self._referenced_variables[id(v)][2] is not None: need_to_set_objective = True + elif lb is not v._lb: + vars_to_update.append(v) + elif ub is not v._ub: + vars_to_update.append(v) elif domain_interval != v.domain.get_interval(): vars_to_update.append(v) self.update_variables(vars_to_update) From 84896f39fd1dead0be2afda4e0894358c732786c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 19 Dec 2023 11:18:02 -0700 Subject: [PATCH 0261/3044] Add descriptions; incorporate missing option into ipopt --- pyomo/solver/config.py | 69 +++++++++++++++++++++++++---------------- pyomo/solver/ipopt.py | 15 ++++++--- pyomo/solver/results.py | 6 ++++ 3 files changed, 60 insertions(+), 30 deletions(-) diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index 551f59ccd9a..54a497cee0c 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -21,24 +21,7 @@ class SolverConfig(ConfigDict): """ - Attributes - ---------- - time_limit: float - sent to solver - Time limit for the solver - tee: bool - If True, then the solver log goes to stdout - load_solution: bool - wrapper - If False, then the values of the primal variables will not be - loaded into the model - symbolic_solver_labels: bool - sent to solver - If True, the names given to the solver will reflect the names - of the pyomo components. Cannot be changed after set_instance - is called. - report_timing: bool - wrapper - If True, then some timing information will be printed at the - end of the solve. - threads: integer - sent to solver - Number of threads to be used by a solver. + Base config values for all solver interfaces """ def __init__( @@ -57,28 +40,62 @@ def __init__( visibility=visibility, ) - self.tee: bool = self.declare('tee', ConfigValue(domain=bool, default=False)) + self.tee: bool = self.declare( + 'tee', + ConfigValue( + domain=bool, + default=False, + description="If True, the solver log prints to stdout.", + ), + ) self.load_solution: bool = self.declare( - 'load_solution', ConfigValue(domain=bool, default=True) + 'load_solution', + ConfigValue( + domain=bool, + default=True, + description="If True, the values of the primal variables will be loaded into the model.", + ), ) self.raise_exception_on_nonoptimal_result: bool = self.declare( 'raise_exception_on_nonoptimal_result', - ConfigValue(domain=bool, default=True), + ConfigValue( + domain=bool, + default=True, + description="If False, the `solve` method will continue processing even if the returned result is nonoptimal.", + ), ) self.symbolic_solver_labels: bool = self.declare( - 'symbolic_solver_labels', ConfigValue(domain=bool, default=False) + 'symbolic_solver_labels', + ConfigValue( + domain=bool, + default=False, + description="If True, the names given to the solver will reflect the names of the Pyomo components. Cannot be changed after set_instance is called.", + ), ) self.report_timing: bool = self.declare( - 'report_timing', ConfigValue(domain=bool, default=False) + 'report_timing', + ConfigValue( + domain=bool, + default=False, + description="If True, timing information will be printed at the end of a solve call.", + ), ) self.threads: Optional[int] = self.declare( - 'threads', ConfigValue(domain=NonNegativeInt) + 'threads', + ConfigValue( + domain=NonNegativeInt, + description="Number of threads to be used by a solver.", + ), ) self.time_limit: Optional[float] = self.declare( - 'time_limit', ConfigValue(domain=NonNegativeFloat) + 'time_limit', + ConfigValue( + domain=NonNegativeFloat, description="Time limit applied to the solver." + ), ) self.solver_options: ConfigDict = self.declare( - 'solver_options', ConfigDict(implicit=True) + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), ) diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index 092b279269b..68ba4989ff4 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -48,8 +48,6 @@ class ipoptSolverError(PyomoException): General exception to catch solver system errors """ - pass - class ipoptConfig(SolverConfig): def __init__( @@ -84,6 +82,9 @@ def __init__( self.log_level = self.declare( 'log_level', ConfigValue(domain=NonNegativeInt, default=logging.INFO) ) + self.presolve: bool = self.declare( + 'presolve', ConfigValue(domain=bool, default=True) + ) class ipoptResults(Results): @@ -186,8 +187,6 @@ def __init__(self, **kwds): self._config = self.CONFIG(kwds) self._writer = NLWriter() self._writer.config.skip_trivial_constraints = True - # TODO: Make this an option; not always turned on - self._writer.config.linear_presolve = True self.ipopt_options = self._config.solver_options def available(self): @@ -265,6 +264,12 @@ def solve(self, model, **kwds): # Update configuration options, based on keywords passed to solve config: ipoptConfig = self.config(kwds.pop('options', {})) config.set_value(kwds) + self._writer.config.linear_presolve = config.presolve + if config.threads: + logger.log( + logging.INFO, + msg="The `threads` option was utilized, but this has not yet been implemented for {self.__class__}.", + ) results = ipoptResults() with TempfileManager.new_context() as tempfile: if config.temp_dir is None: @@ -405,6 +410,8 @@ def solve(self, model, **kwds): results.timing_info.wall_time = ( end_timestamp - start_timestamp ).total_seconds() + if config.report_timing: + results.report_timing() return results def _parse_ipopt_output(self, stream: io.StringIO): diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index 0aa78bef6bc..ae909986ed0 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -246,6 +246,12 @@ def __str__(self): s += 'objective_bound: ' + str(self.objective_bound) return s + def report_timing(self): + print('Timing Information: ') + print('-' * 50) + self.timing_info.display() + print('-' * 50) + class ResultsReader: pass From c114ee3b991540485bd868415ce3a9ed25e73e7b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 19 Dec 2023 16:17:37 -0700 Subject: [PATCH 0262/3044] Bug fix: Case where there is no active objective --- pyomo/contrib/trustregion/TRF.py | 2 +- pyomo/core/base/PyomoModel.py | 4 ++-- pyomo/solver/base.py | 17 +++++++++-------- pyomo/solver/ipopt.py | 10 +++++++++- 4 files changed, 21 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/trustregion/TRF.py b/pyomo/contrib/trustregion/TRF.py index 45e60df7658..24254599609 100644 --- a/pyomo/contrib/trustregion/TRF.py +++ b/pyomo/contrib/trustregion/TRF.py @@ -35,7 +35,7 @@ logger = logging.getLogger('pyomo.contrib.trustregion') -__version__ = '0.2.0' +__version__ = (0, 2, 0) def trust_region_method(model, decision_variables, ext_fcn_surrogate_map_rule, config): diff --git a/pyomo/core/base/PyomoModel.py b/pyomo/core/base/PyomoModel.py index 6aacabeb183..44bc5302217 100644 --- a/pyomo/core/base/PyomoModel.py +++ b/pyomo/core/base/PyomoModel.py @@ -789,7 +789,7 @@ def _load_model_data(self, modeldata, namespaces, **kwds): profile_memory = kwds.get('profile_memory', 0) if profile_memory >= 2 and pympler_available: - mem_used = pympler.muppy.get_size(muppy.get_objects()) + mem_used = pympler.muppy.get_size(pympler.muppy.get_objects()) print("") print( " Total memory = %d bytes prior to model " @@ -798,7 +798,7 @@ def _load_model_data(self, modeldata, namespaces, **kwds): if profile_memory >= 3: gc.collect() - mem_used = pympler.muppy.get_size(muppy.get_objects()) + mem_used = pympler.muppy.get_size(pympler.muppy.get_objects()) print( " Total memory = %d bytes prior to model " "construction (after garbage collection)" % mem_used diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 1d459450bab..72f63e0a1a0 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -430,14 +430,15 @@ def solve( legacy_results.solver.termination_message = str(results.termination_condition) obj = get_objective(model) - legacy_results.problem.sense = obj.sense - - if obj.sense == minimize: - legacy_results.problem.lower_bound = results.objective_bound - legacy_results.problem.upper_bound = results.incumbent_objective - else: - legacy_results.problem.upper_bound = results.objective_bound - legacy_results.problem.lower_bound = results.incumbent_objective + if obj: + legacy_results.problem.sense = obj.sense + + if obj.sense == minimize: + legacy_results.problem.lower_bound = results.objective_bound + legacy_results.problem.upper_bound = results.incumbent_objective + else: + legacy_results.problem.upper_bound = results.objective_bound + legacy_results.problem.lower_bound = results.incumbent_objective if ( results.incumbent_objective is not None and results.objective_bound is not None diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index 68ba4989ff4..e85b726ba9b 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -20,6 +20,7 @@ from pyomo.common.config import ConfigValue, NonNegativeInt, NonNegativeFloat from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager +from pyomo.core.base import Objective from pyomo.core.base.label import NumericLabeler from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo, AMPLRepn from pyomo.solver.base import SolverBase, SymbolMap @@ -390,7 +391,14 @@ def solve(self, model, **kwds): ): model.rc.update(results.solution_loader.get_reduced_costs()) - if results.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal}: + if results.solution_status in { + SolutionStatus.feasible, + SolutionStatus.optimal, + } and len( + list( + model.component_data_objects(Objective, descend_into=True, active=True) + ) + ): if config.load_solution: results.incumbent_objective = value(nl_info.objectives[0]) else: From 13631448a72c4678de81838a86b73cd6a8142f6d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 20 Dec 2023 08:00:59 -0700 Subject: [PATCH 0263/3044] Bug fix: bcannot convert obj to bool --- pyomo/solver/base.py | 6 +++++- pyomo/solver/results.py | 2 -- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 72f63e0a1a0..48adc44c4d7 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -369,6 +369,10 @@ class LegacySolverInterface: interface. Necessary for backwards compatibility. """ + def set_config(self, config): + # TODO: Make a mapping from new config -> old config + pass + def solve( self, model: _BlockData, @@ -430,7 +434,7 @@ def solve( legacy_results.solver.termination_message = str(results.termination_condition) obj = get_objective(model) - if obj: + if len(list(obj)) > 0: legacy_results.problem.sense = obj.sense if obj.sense == minimize: diff --git a/pyomo/solver/results.py b/pyomo/solver/results.py index ae909986ed0..e99db52073b 100644 --- a/pyomo/solver/results.py +++ b/pyomo/solver/results.py @@ -40,8 +40,6 @@ class SolverResultsError(PyomoException): General exception to catch solver system errors """ - pass - class TerminationCondition(enum.Enum): """ From d5a2fba9cece7982d551be89e70737355e954ba2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 20 Dec 2023 15:17:59 -0700 Subject: [PATCH 0264/3044] Fix f-string warning --- pyomo/solver/ipopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index e85b726ba9b..1b4c0eb36cb 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -268,8 +268,8 @@ def solve(self, model, **kwds): self._writer.config.linear_presolve = config.presolve if config.threads: logger.log( - logging.INFO, - msg="The `threads` option was utilized, but this has not yet been implemented for {self.__class__}.", + logging.WARNING, + msg=f"The `threads` option was specified, but this has not yet been implemented for {self.__class__}.", ) results = ipoptResults() with TempfileManager.new_context() as tempfile: From 428f17f85e54462edc73d18618fad94512dbc94b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 21 Dec 2023 13:40:57 -0700 Subject: [PATCH 0265/3044] Adding proper handling for when a bigm solve comes back infeasible --- pyomo/gdp/plugins/multiple_bigm.py | 30 ++++++++++++--- pyomo/gdp/tests/test_mbigm.py | 59 ++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 2fa26479908..f363922d539 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -627,8 +627,15 @@ def _calculate_missing_M_values( if lower_M is None: scratch.obj.expr = constraint.body - constraint.lower scratch.obj.sense = minimize - results = self._config.solver.solve(other_disjunct) - if ( + results = self._config.solver.solve(other_disjunct, + load_solutions=False) + if (results.solver.termination_condition is + TerminationCondition.infeasible): + logger.debug("Disjunct '%s' is infeasible, deactivating." + % other_disjunct.name) + other_disjunct.deactivate() + lower_M = 0 + elif ( results.solver.termination_condition is not TerminationCondition.optimal ): @@ -638,14 +645,23 @@ def _calculate_missing_M_values( "Disjunct '%s' is selected." % (constraint.name, disjunct.name, other_disjunct.name) ) - lower_M = value(scratch.obj.expr) + else: + other_disjunct.solutions.load_from(results) + lower_M = value(scratch.obj.expr) if constraint.upper is not None and upper_M is None: # last resort: calculate if upper_M is None: scratch.obj.expr = constraint.body - constraint.upper scratch.obj.sense = maximize - results = self._config.solver.solve(other_disjunct) - if ( + results = self._config.solver.solve(other_disjunct, + load_solutions=False) + if (results.solver.termination_condition is + TerminationCondition.infeasible): + logger.debug("Disjunct '%s' is infeasible, deactivating." + % other_disjunct.name) + other_disjunct.deactivate() + upper_M = 0 + elif ( results.solver.termination_condition is not TerminationCondition.optimal ): @@ -655,7 +671,9 @@ def _calculate_missing_M_values( "Disjunct '%s' is selected." % (constraint.name, disjunct.name, other_disjunct.name) ) - upper_M = value(scratch.obj.expr) + else: + other_disjunct.solutions.load_from(results) + upper_M = value(scratch.obj.expr) arg_Ms[constraint, other_disjunct] = (lower_M, upper_M) transBlock._mbm_values[constraint, other_disjunct] = (lower_M, upper_M) diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index 46b57d3256d..8d7c2456e3b 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -10,6 +10,7 @@ # ___________________________________________________________________________ from io import StringIO +import logging from os.path import join, normpath import pickle @@ -953,3 +954,61 @@ def test_two_term_indexed_disjunction(self): self.assertEqual(len(cons_again), 2) self.assertIs(cons_again[0], cons[0]) self.assertIs(cons_again[1], cons[1]) + +class EdgeCases(unittest.TestCase): + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_calculate_Ms_infeasible_Disjunct(self): + m = ConcreteModel() + m.x = Var(bounds=(1, 12)) + m.y = Var(bounds=(19, 22)) + m.disjunction = Disjunction(expr=[ + [m.x >= 3 + m.y, m.y == 19.75], # infeasible given bounds + [m.y >= 21 + m.x], # unique solution + [m.x == m.y - 9], # x in interval [10, 12] + ]) + + out = StringIO() + mbm = TransformationFactory('gdp.mbigm') + with LoggingIntercept(out, 'pyomo.gdp.mbigm', logging.DEBUG): + mbm.apply_to(m, reduce_bound_constraints=False) + + # We mentioned the infeasibility at the DEBUG level + self.assertIn( + r"Disjunct 'disjunction_disjuncts[0]' is infeasible, deactivating", + out.getvalue().strip(), + ) + + # We just fixed the infeasible by to False + self.assertFalse(m.disjunction.disjuncts[0].active) + self.assertTrue(m.disjunction.disjuncts[0].indicator_var.fixed) + self.assertFalse(value(m.disjunction.disjuncts[0].indicator_var)) + + # the remaining constraints are transformed correctly. + cons = mbm.get_transformed_constraints( + m.disjunction.disjuncts[1].constraint[1]) + self.assertEqual(len(cons), 1) + assertExpressionsEqual( + self, + cons[0].expr, + 21 + m.x - m.y <= 0*m.disjunction.disjuncts[0].binary_indicator_var + + 12.0*m.disjunction.disjuncts[2].binary_indicator_var + ) + + cons = mbm.get_transformed_constraints( + m.disjunction.disjuncts[2].constraint[1]) + self.assertEqual(len(cons), 2) + print(cons[0].expr) + print(cons[1].expr) + assertExpressionsEqual( + self, + cons[0].expr, + 0.0*m.disjunction_disjuncts[0].binary_indicator_var - + 12.0*m.disjunction_disjuncts[1].binary_indicator_var <= m.x - (m.y - 9) + ) + assertExpressionsEqual( + self, + cons[1].expr, + m.x - (m.y - 9) <= 0.0*m.disjunction_disjuncts[0].binary_indicator_var - + 12.0*m.disjunction_disjuncts[1].binary_indicator_var + ) + From 4f0cc01c5872a2089d6dc1bb9a7500a1a3434d1e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 21 Dec 2023 13:42:26 -0700 Subject: [PATCH 0266/3044] NFC: Adding whitespace --- pyomo/gdp/plugins/multiple_bigm.py | 34 +++++++++++------- pyomo/gdp/tests/test_mbigm.py | 55 +++++++++++++++--------------- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index f363922d539..48ec1177fe5 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -627,12 +627,17 @@ def _calculate_missing_M_values( if lower_M is None: scratch.obj.expr = constraint.body - constraint.lower scratch.obj.sense = minimize - results = self._config.solver.solve(other_disjunct, - load_solutions=False) - if (results.solver.termination_condition is - TerminationCondition.infeasible): - logger.debug("Disjunct '%s' is infeasible, deactivating." - % other_disjunct.name) + results = self._config.solver.solve( + other_disjunct, load_solutions=False + ) + if ( + results.solver.termination_condition + is TerminationCondition.infeasible + ): + logger.debug( + "Disjunct '%s' is infeasible, deactivating." + % other_disjunct.name + ) other_disjunct.deactivate() lower_M = 0 elif ( @@ -653,12 +658,17 @@ def _calculate_missing_M_values( if upper_M is None: scratch.obj.expr = constraint.body - constraint.upper scratch.obj.sense = maximize - results = self._config.solver.solve(other_disjunct, - load_solutions=False) - if (results.solver.termination_condition is - TerminationCondition.infeasible): - logger.debug("Disjunct '%s' is infeasible, deactivating." - % other_disjunct.name) + results = self._config.solver.solve( + other_disjunct, load_solutions=False + ) + if ( + results.solver.termination_condition + is TerminationCondition.infeasible + ): + logger.debug( + "Disjunct '%s' is infeasible, deactivating." + % other_disjunct.name + ) other_disjunct.deactivate() upper_M = 0 elif ( diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index 8d7c2456e3b..0cdf004a445 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -50,6 +50,7 @@ ) exdir = normpath(join(PYOMO_ROOT_DIR, 'examples', 'gdp')) + class CommonTests(unittest.TestCase): def check_pretty_bound_constraints(self, cons, var, bounds, lb): self.assertEqual(value(cons.upper), 0) @@ -67,6 +68,7 @@ def check_pretty_bound_constraints(self, cons, var, bounds, lb): for disj, bnd in bounds.items(): check_linear_coef(self, repn, disj.binary_indicator_var, -bnd) + class LinearModelDecisionTreeExample(CommonTests): def make_model(self): m = ConcreteModel() @@ -494,7 +496,7 @@ def test_Ms_specified_as_args_honored(self): self.check_untightened_bounds_constraint( cons[1], m.x2, m.d2, m.disjunction, {m.d1: 3, m.d3: 110}, upper=10 ) - + # TODO: If Suffixes allow tuple keys then we can support them and it will # look something like this: # def test_Ms_specified_as_suffixes_honored(self): @@ -877,6 +879,7 @@ class NestedDisjunctsInFlatGDP(unittest.TestCase): def test_declare_disjuncts_in_disjunction_rule(self): check_nested_disjuncts_in_flat_gdp(self, 'bigm') + class IndexedDisjunctiveConstraints(CommonTests): def test_empty_constraint_container_on_Disjunct(self): m = ConcreteModel() @@ -889,17 +892,12 @@ def test_empty_constraint_container_on_Disjunct(self): mbm = TransformationFactory('gdp.mbigm') mbm.apply_to(m) - + cons = mbm.get_transformed_constraints(m.e.c) self.assertEqual(len(cons), 2) - self.check_pretty_bound_constraints( - cons[0], m.x, {m.d: 2, m.e: 2.7}, lb=True - - ) - self.check_pretty_bound_constraints( - cons[1], m.x, {m.d: 3, m.e: 2.7}, lb=False - ) - + self.check_pretty_bound_constraints(cons[0], m.x, {m.d: 2, m.e: 2.7}, lb=True) + self.check_pretty_bound_constraints(cons[1], m.x, {m.d: 3, m.e: 2.7}, lb=False) + @unittest.skipUnless(gurobi_available, "Gurobi is not available") class IndexedDisjunction(unittest.TestCase): @@ -955,17 +953,20 @@ def test_two_term_indexed_disjunction(self): self.assertIs(cons_again[0], cons[0]) self.assertIs(cons_again[1], cons[1]) + class EdgeCases(unittest.TestCase): @unittest.skipUnless(gurobi_available, "Gurobi is not available") def test_calculate_Ms_infeasible_Disjunct(self): m = ConcreteModel() m.x = Var(bounds=(1, 12)) m.y = Var(bounds=(19, 22)) - m.disjunction = Disjunction(expr=[ - [m.x >= 3 + m.y, m.y == 19.75], # infeasible given bounds - [m.y >= 21 + m.x], # unique solution - [m.x == m.y - 9], # x in interval [10, 12] - ]) + m.disjunction = Disjunction( + expr=[ + [m.x >= 3 + m.y, m.y == 19.75], # infeasible given bounds + [m.y >= 21 + m.x], # unique solution + [m.x == m.y - 9], # x in interval [10, 12] + ] + ) out = StringIO() mbm = TransformationFactory('gdp.mbigm') @@ -982,33 +983,33 @@ def test_calculate_Ms_infeasible_Disjunct(self): self.assertFalse(m.disjunction.disjuncts[0].active) self.assertTrue(m.disjunction.disjuncts[0].indicator_var.fixed) self.assertFalse(value(m.disjunction.disjuncts[0].indicator_var)) - + # the remaining constraints are transformed correctly. - cons = mbm.get_transformed_constraints( - m.disjunction.disjuncts[1].constraint[1]) + cons = mbm.get_transformed_constraints(m.disjunction.disjuncts[1].constraint[1]) self.assertEqual(len(cons), 1) assertExpressionsEqual( self, cons[0].expr, - 21 + m.x - m.y <= 0*m.disjunction.disjuncts[0].binary_indicator_var + - 12.0*m.disjunction.disjuncts[2].binary_indicator_var + 21 + m.x - m.y + <= 0 * m.disjunction.disjuncts[0].binary_indicator_var + + 12.0 * m.disjunction.disjuncts[2].binary_indicator_var, ) - cons = mbm.get_transformed_constraints( - m.disjunction.disjuncts[2].constraint[1]) + cons = mbm.get_transformed_constraints(m.disjunction.disjuncts[2].constraint[1]) self.assertEqual(len(cons), 2) print(cons[0].expr) print(cons[1].expr) assertExpressionsEqual( self, cons[0].expr, - 0.0*m.disjunction_disjuncts[0].binary_indicator_var - - 12.0*m.disjunction_disjuncts[1].binary_indicator_var <= m.x - (m.y - 9) + 0.0 * m.disjunction_disjuncts[0].binary_indicator_var + - 12.0 * m.disjunction_disjuncts[1].binary_indicator_var + <= m.x - (m.y - 9), ) assertExpressionsEqual( self, cons[1].expr, - m.x - (m.y - 9) <= 0.0*m.disjunction_disjuncts[0].binary_indicator_var - - 12.0*m.disjunction_disjuncts[1].binary_indicator_var + m.x - (m.y - 9) + <= 0.0 * m.disjunction_disjuncts[0].binary_indicator_var + - 12.0 * m.disjunction_disjuncts[1].binary_indicator_var, ) - From d24286a3c2068772e6f900e5e843e70e2546a2a6 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 21 Dec 2023 14:10:53 -0700 Subject: [PATCH 0267/3044] Fixing a bug for multi-dimensionally indexed SequenceVars --- pyomo/contrib/cp/sequence_var.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py index a77f4c2c415..587106c8107 100644 --- a/pyomo/contrib/cp/sequence_var.py +++ b/pyomo/contrib/cp/sequence_var.py @@ -91,7 +91,7 @@ def _getitem_when_not_present(self, index): obj._index = index if self._init_rule is not None: - obj.set_value(self._init_rule(parent, index)) + obj.set_value(self._init_rule(parent, *index)) if self._init_expr is not None: obj.set_value(self._init_expr) From d5fcd687f16ca09e0e89f3eddad4a970abbaaaaf Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 21 Dec 2023 14:11:04 -0700 Subject: [PATCH 0268/3044] Fixing a typo in a test --- pyomo/contrib/cp/tests/test_sequence_var.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index 37190ebca89..e3d69153355 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -153,5 +153,5 @@ def s(m, i, j): for i in m.alphabetic: for j in m.numeric: self.assertTrue((i, j) in m.s) - self.assertEqual(len(m.s[i, j]), 1) + self.assertEqual(len(m.s[i, j].interval_vars), 1) self.assertIs(m.s[i, j].interval_vars[0], m.i[i, j]) From 0547fbc7081a98f0c4efb94853b848dbae7a1468 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 21 Dec 2023 14:11:33 -0700 Subject: [PATCH 0269/3044] NFC: black --- pyomo/contrib/cp/tests/test_sequence_var.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index e3d69153355..404e21ca39c 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -144,9 +144,10 @@ def test_pprint(self): def test_multidimensional_index(self): m = self.make_model() + @m.SequenceVar(m.alphabetic, m.numeric) def s(m, i, j): - return [m.i[i, j],] + return [m.i[i, j]] self.assertIsInstance(m.s, IndexedSequenceVar) self.assertEqual(len(m.s), 4) From 17f4837e1cddd77e4dee31cc5201c810d0e5dec4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 21 Dec 2023 15:27:33 -0700 Subject: [PATCH 0270/3044] Remembering that initializers exist --- pyomo/contrib/cp/sequence_var.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py index 587106c8107..b242b362f9d 100644 --- a/pyomo/contrib/cp/sequence_var.py +++ b/pyomo/contrib/cp/sequence_var.py @@ -18,6 +18,7 @@ from pyomo.core.base.component import ActiveComponentData from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import ActiveIndexedComponent +from pyomo.core.base.initializer import Initializer import sys from weakref import ref as weakref_ref @@ -72,7 +73,7 @@ def __new__(cls, *args, **kwds): return IndexedSequenceVar.__new__(IndexedSequenceVar) def __init__(self, *args, **kwargs): - self._init_rule = kwargs.pop('rule', None) + self._init_rule = Initializer(kwargs.pop('rule', None)) self._init_expr = kwargs.pop('expr', None) kwargs.setdefault('ctype', SequenceVar) super(SequenceVar, self).__init__(*args, **kwargs) @@ -91,7 +92,7 @@ def _getitem_when_not_present(self, index): obj._index = index if self._init_rule is not None: - obj.set_value(self._init_rule(parent, *index)) + obj.set_value(self._init_rule(parent, index)) if self._init_expr is not None: obj.set_value(self._init_expr) From b68cfe2c9061a6d305726612bf4037cc9b040ea8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 21 Dec 2023 15:49:00 -0700 Subject: [PATCH 0271/3044] Updating baselines to reflect support for anonymous sets --- .../contributed_packages/mpc/overview.rst | 2 +- doc/OnlineDocs/contributed_packages/pyros.rst | 2 +- .../modeling_extensions/gdp/modeling.rst | 2 +- .../working_abstractmodels/data/raw_dicts.rst | 9 +-- examples/pyomo/tutorials/data.out | 32 ++------ examples/pyomo/tutorials/excel.out | 32 ++------ examples/pyomo/tutorials/param.out | 19 ++--- examples/pyomo/tutorials/set.out | 34 ++------ examples/pyomo/tutorials/table.out | 32 ++------ examples/pyomobook/blocks-ch/blocks_gen.txt | 76 +++++++----------- .../blocks-ch/lotsizing_uncertain.txt | 29 ++----- examples/pyomobook/dae-ch/path_constraint.txt | 7 +- .../optimization-ch/ConcHLinScript.txt | 4 +- .../pyomobook/optimization-ch/ConcreteH.txt | 9 +-- .../optimization-ch/ConcreteHLinear.txt | 9 +-- .../overview-ch/wl_concrete_script.txt | 2 +- examples/pyomobook/overview-ch/wl_excel.txt | 2 +- examples/pyomobook/overview-ch/wl_list.txt | 30 ++----- .../pyomo-components-ch/con_declaration.txt | 46 +++-------- .../pyomo-components-ch/examples.txt | 9 +-- .../pyomo-components-ch/expr_declaration.txt | 14 +--- .../pyomo-components-ch/obj_declaration.txt | 10 +-- .../pyomo-components-ch/param_declaration.txt | 9 +-- .../param_initialization.txt | 24 ++---- .../pyomo-components-ch/set_declaration.txt | 14 +--- .../set_initialization.txt | 19 ++--- .../pyomobook/scripts-ch/warehouse_cuts.txt | 12 +-- .../pyomobook/scripts-ch/warehouse_script.txt | 40 ++-------- pyomo/contrib/pyros/tests/test_grcs.py | 4 - pyomo/core/base/reference.py | 8 +- pyomo/core/tests/unit/test_block.py | 11 +-- pyomo/core/tests/unit/test_componentuid.py | 16 +--- pyomo/core/tests/unit/test_connector.py | 32 ++++---- pyomo/core/tests/unit/test_expr5.txt | 9 +-- pyomo/core/tests/unit/test_expression.py | 12 +-- pyomo/core/tests/unit/test_reference.py | 11 ++- pyomo/core/tests/unit/test_set.py | 78 ++++++++----------- pyomo/core/tests/unit/test_visitor.py | 1 - pyomo/core/tests/unit/varpprint.txt | 14 +--- pyomo/dae/tests/test_diffvar.py | 1 - pyomo/mpec/tests/cov2_None.txt | 2 +- pyomo/mpec/tests/cov2_mpec.nl.txt | 9 +-- .../tests/cov2_mpec.simple_disjunction.txt | 2 +- .../mpec/tests/cov2_mpec.simple_nonlinear.txt | 2 +- pyomo/mpec/tests/cov2_mpec.standard_form.txt | 2 +- pyomo/mpec/tests/list1_None.txt | 2 +- pyomo/mpec/tests/list1_mpec.nl.txt | 9 +-- .../tests/list1_mpec.simple_disjunction.txt | 2 +- .../tests/list1_mpec.simple_nonlinear.txt | 2 +- pyomo/mpec/tests/list1_mpec.standard_form.txt | 2 +- pyomo/mpec/tests/list2_None.txt | 2 +- pyomo/mpec/tests/list2_mpec.nl.txt | 9 +-- .../tests/list2_mpec.simple_disjunction.txt | 2 +- .../tests/list2_mpec.simple_nonlinear.txt | 2 +- pyomo/mpec/tests/list2_mpec.standard_form.txt | 2 +- pyomo/mpec/tests/list5_None.txt | 2 +- pyomo/mpec/tests/list5_mpec.nl.txt | 9 +-- .../tests/list5_mpec.simple_disjunction.txt | 2 +- .../tests/list5_mpec.simple_nonlinear.txt | 2 +- pyomo/mpec/tests/list5_mpec.standard_form.txt | 2 +- pyomo/mpec/tests/t10_None.txt | 2 +- pyomo/mpec/tests/t10_mpec.nl.txt | 9 +-- .../tests/t10_mpec.simple_disjunction.txt | 2 +- .../mpec/tests/t10_mpec.simple_nonlinear.txt | 2 +- pyomo/mpec/tests/t10_mpec.standard_form.txt | 2 +- pyomo/mpec/tests/t13_None.txt | 2 +- pyomo/mpec/tests/t13_mpec.nl.txt | 9 +-- .../tests/t13_mpec.simple_disjunction.txt | 2 +- .../mpec/tests/t13_mpec.simple_nonlinear.txt | 2 +- pyomo/mpec/tests/t13_mpec.standard_form.txt | 2 +- pyomo/network/tests/test_arc.py | 20 ++--- 71 files changed, 262 insertions(+), 586 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/mpc/overview.rst b/doc/OnlineDocs/contributed_packages/mpc/overview.rst index f5dbe85e523..f3bc7504b59 100644 --- a/doc/OnlineDocs/contributed_packages/mpc/overview.rst +++ b/doc/OnlineDocs/contributed_packages/mpc/overview.rst @@ -189,7 +189,7 @@ a tracking cost expression. >>> m.setpoint_idx = var_set >>> m.tracking_cost = tr_cost >>> m.tracking_cost.pprint() - tracking_cost : Size=6, Index=tracking_cost_index + tracking_cost : Size=6, Index=setpoint_idx*time Key : Expression (0, 0) : (var[0,A] - 0.5)**2 (0, 1) : (var[1,A] - 0.5)**2 diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index 133258fb9b8..f8aa7e36e37 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -539,7 +539,7 @@ correspond to first-stage degrees of freedom. ... load_solution=False, ... ) ============================================================================== - PyROS: The Pyomo Robust Optimization Solver. + PyROS: The Pyomo Robust Optimization Solver... ... ------------------------------------------------------------------------------ Robust optimal solution identified. diff --git a/doc/OnlineDocs/modeling_extensions/gdp/modeling.rst b/doc/OnlineDocs/modeling_extensions/gdp/modeling.rst index b70e37d5935..996ebcb0366 100644 --- a/doc/OnlineDocs/modeling_extensions/gdp/modeling.rst +++ b/doc/OnlineDocs/modeling_extensions/gdp/modeling.rst @@ -166,7 +166,7 @@ Usage: >>> TransformationFactory('core.logical_to_linear').apply_to(m) >>> # constraint auto-generated by transformation >>> m.logic_to_linear.transformed_constraints.pprint() - transformed_constraints : Size=1, Index=logic_to_linear.transformed_constraints_index, Active=True + transformed_constraints : Size=1, Index={1}, Active=True Key : Lower : Body : Upper : Active 1 : 3.0 : Y_asbinary[1] + Y_asbinary[2] + Y_asbinary[3] + Y_asbinary[4] : +Inf : True diff --git a/doc/OnlineDocs/working_abstractmodels/data/raw_dicts.rst b/doc/OnlineDocs/working_abstractmodels/data/raw_dicts.rst index e10042b3ceb..f78e349c28b 100644 --- a/doc/OnlineDocs/working_abstractmodels/data/raw_dicts.rst +++ b/doc/OnlineDocs/working_abstractmodels/data/raw_dicts.rst @@ -28,13 +28,10 @@ components, the required data dictionary maps the implicit index ... }} >>> i = m.create_instance(data) >>> i.pprint() - 2 Set Declarations + 1 Set Declarations I : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} - r_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : I*I : 9 : {(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)} 3 Param Declarations p : Size=1, Index=None, Domain=Any, Default=None, Mutable=False @@ -45,12 +42,12 @@ components, the required data dictionary maps the implicit index 1 : 10 2 : 20 3 : 30 - r : Size=9, Index=r_index, Domain=Any, Default=0, Mutable=False + r : Size=9, Index=I*I, Domain=Any, Default=0, Mutable=False Key : Value (1, 1) : 110 (1, 2) : 120 (2, 3) : 230 - 5 Declarations: I p q r_index r + 4 Declarations: I p q r diff --git a/examples/pyomo/tutorials/data.out b/examples/pyomo/tutorials/data.out index d1353f87858..7dce6012e2f 100644 --- a/examples/pyomo/tutorials/data.out +++ b/examples/pyomo/tutorials/data.out @@ -1,4 +1,4 @@ -20 Set Declarations +14 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'A1', 'A2', 'A3'} @@ -9,30 +9,18 @@ Key : Dimen : Domain : Size : Members None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} D : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : D_domain : 3 : {('A1', 1), ('A2', 2), ('A3', 3)} - D_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} + None : 2 : A*B : 3 : {('A1', 1), ('A2', 2), ('A3', 3)} E : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 3 : E_domain : 6 : {('A1', 1, 'A1'), ('A1', 1, 'A2'), ('A2', 2, 'A2'), ('A2', 2, 'A3'), ('A3', 3, 'A1'), ('A3', 3, 'A3')} - E_domain : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 3 : E_domain_index_0*A : 27 : {('A1', 1, 'A1'), ('A1', 1, 'A2'), ('A1', 1, 'A3'), ('A1', 2, 'A1'), ('A1', 2, 'A2'), ('A1', 2, 'A3'), ('A1', 3, 'A1'), ('A1', 3, 'A2'), ('A1', 3, 'A3'), ('A2', 1, 'A1'), ('A2', 1, 'A2'), ('A2', 1, 'A3'), ('A2', 2, 'A1'), ('A2', 2, 'A2'), ('A2', 2, 'A3'), ('A2', 3, 'A1'), ('A2', 3, 'A2'), ('A2', 3, 'A3'), ('A3', 1, 'A1'), ('A3', 1, 'A2'), ('A3', 1, 'A3'), ('A3', 2, 'A1'), ('A3', 2, 'A2'), ('A3', 2, 'A3'), ('A3', 3, 'A1'), ('A3', 3, 'A2'), ('A3', 3, 'A3')} - E_domain_index_0 : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} + None : 3 : A*B*A : 6 : {('A1', 1, 'A1'), ('A1', 1, 'A2'), ('A2', 2, 'A2'), ('A2', 2, 'A3'), ('A3', 3, 'A1'), ('A3', 3, 'A3')} F : Size=3, Index=A, Ordered=Insertion Key : Dimen : Domain : Size : Members A1 : 1 : Any : 3 : {1, 3, 5} A2 : 1 : Any : 3 : {2, 4, 6} A3 : 1 : Any : 3 : {3, 5, 7} - G : Size=0, Index=G_index, Ordered=Insertion + G : Size=0, Index=A*B, Ordered=Insertion Key : Dimen : Domain : Size : Members - G_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} H : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'H1', 'H2', 'H3'} @@ -45,12 +33,6 @@ K : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} - T_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*I : 12 : {('A1', 'I1'), ('A1', 'I2'), ('A1', 'I3'), ('A1', 'I4'), ('A2', 'I1'), ('A2', 'I2'), ('A2', 'I3'), ('A2', 'I4'), ('A3', 'I1'), ('A3', 'I2'), ('A3', 'I3'), ('A3', 'I4')} - U_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : I*A : 12 : {('I1', 'A1'), ('I1', 'A2'), ('I1', 'A3'), ('I2', 'A1'), ('I2', 'A2'), ('I2', 'A3'), ('I3', 'A1'), ('I3', 'A2'), ('I3', 'A3'), ('I4', 'A1'), ('I4', 'A2'), ('I4', 'A3')} x : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'A1', 'A2', 'A3'} @@ -116,7 +98,7 @@ Key : Value A1 : 3.3 A3 : 3.5 - T : Size=12, Index=T_index, Domain=Any, Default=None, Mutable=False + T : Size=12, Index=A*I, Domain=Any, Default=None, Mutable=False Key : Value ('A1', 'I1') : 1.3 ('A1', 'I2') : 1.4 @@ -130,7 +112,7 @@ ('A3', 'I2') : 3.4 ('A3', 'I3') : 3.5 ('A3', 'I4') : 3.6 - U : Size=12, Index=U_index, Domain=Any, Default=None, Mutable=False + U : Size=12, Index=I*A, Domain=Any, Default=None, Mutable=False Key : Value ('I1', 'A1') : 1.3 ('I1', 'A2') : 2.3 @@ -166,4 +148,4 @@ Key : Value None : 2 -38 Declarations: A B C D_domain D E_domain_index_0 E_domain E F G_index G H I J K Z ZZ Y X W U_index U T_index T S R Q P PP O z y x M N MM MMM NNN +32 Declarations: A B C D E F G H I J K Z ZZ Y X W U T S R Q P PP O z y x M N MM MMM NNN diff --git a/examples/pyomo/tutorials/excel.out b/examples/pyomo/tutorials/excel.out index 5064d4fa511..5e30827f7ae 100644 --- a/examples/pyomo/tutorials/excel.out +++ b/examples/pyomo/tutorials/excel.out @@ -1,4 +1,4 @@ -16 Set Declarations +10 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'A1', 'A2', 'A3'} @@ -9,27 +9,15 @@ Key : Dimen : Domain : Size : Members None : 2 : A*B : 9 : {('A1', 1.0), ('A1', 2.0), ('A1', 3.0), ('A2', 1.0), ('A2', 2.0), ('A2', 3.0), ('A3', 1.0), ('A3', 2.0), ('A3', 3.0)} D : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : D_domain : 3 : {('A1', 1.0), ('A2', 2.0), ('A3', 3.0)} - D_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1.0), ('A1', 2.0), ('A1', 3.0), ('A2', 1.0), ('A2', 2.0), ('A2', 3.0), ('A3', 1.0), ('A3', 2.0), ('A3', 3.0)} + None : 2 : A*B : 3 : {('A1', 1.0), ('A2', 2.0), ('A3', 3.0)} E : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 3 : E_domain : 6 : {('A1', 1.0, 'A1'), ('A1', 1.0, 'A2'), ('A2', 2.0, 'A2'), ('A2', 2.0, 'A3'), ('A3', 3.0, 'A1'), ('A3', 3.0, 'A3')} - E_domain : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 3 : E_domain_index_0*A : 27 : {('A1', 1.0, 'A1'), ('A1', 1.0, 'A2'), ('A1', 1.0, 'A3'), ('A1', 2.0, 'A1'), ('A1', 2.0, 'A2'), ('A1', 2.0, 'A3'), ('A1', 3.0, 'A1'), ('A1', 3.0, 'A2'), ('A1', 3.0, 'A3'), ('A2', 1.0, 'A1'), ('A2', 1.0, 'A2'), ('A2', 1.0, 'A3'), ('A2', 2.0, 'A1'), ('A2', 2.0, 'A2'), ('A2', 2.0, 'A3'), ('A2', 3.0, 'A1'), ('A2', 3.0, 'A2'), ('A2', 3.0, 'A3'), ('A3', 1.0, 'A1'), ('A3', 1.0, 'A2'), ('A3', 1.0, 'A3'), ('A3', 2.0, 'A1'), ('A3', 2.0, 'A2'), ('A3', 2.0, 'A3'), ('A3', 3.0, 'A1'), ('A3', 3.0, 'A2'), ('A3', 3.0, 'A3')} - E_domain_index_0 : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1.0), ('A1', 2.0), ('A1', 3.0), ('A2', 1.0), ('A2', 2.0), ('A2', 3.0), ('A3', 1.0), ('A3', 2.0), ('A3', 3.0)} + None : 3 : A*B : 6 : {('A1', 1.0, 'A1'), ('A1', 1.0, 'A2'), ('A2', 2.0, 'A2'), ('A2', 2.0, 'A3'), ('A3', 3.0, 'A1'), ('A3', 3.0, 'A3')} F : Size=0, Index=A, Ordered=Insertion Key : Dimen : Domain : Size : Members - G : Size=0, Index=G_index, Ordered=Insertion + G : Size=0, Index=A*B, Ordered=Insertion Key : Dimen : Domain : Size : Members - G_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1.0), ('A1', 2.0), ('A1', 3.0), ('A2', 1.0), ('A2', 2.0), ('A2', 3.0), ('A3', 1.0), ('A3', 2.0), ('A3', 3.0)} H : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'H1', 'H2', 'H3'} @@ -39,12 +27,6 @@ J : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} - T_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*I : 12 : {('A1', 'I1'), ('A1', 'I2'), ('A1', 'I3'), ('A1', 'I4'), ('A2', 'I1'), ('A2', 'I2'), ('A2', 'I3'), ('A2', 'I4'), ('A3', 'I1'), ('A3', 'I2'), ('A3', 'I3'), ('A3', 'I4')} - U_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : I*A : 12 : {('I1', 'A1'), ('I1', 'A2'), ('I1', 'A3'), ('I2', 'A1'), ('I2', 'A2'), ('I2', 'A3'), ('I3', 'A1'), ('I3', 'A2'), ('I3', 'A3'), ('I4', 'A1'), ('I4', 'A2'), ('I4', 'A3')} 12 Param Declarations O : Size=3, Index=J, Domain=Reals, Default=None, Mutable=False @@ -76,7 +58,7 @@ Key : Value A1 : 3.3 A3 : 3.5 - T : Size=12, Index=T_index, Domain=Any, Default=None, Mutable=False + T : Size=12, Index=A*I, Domain=Any, Default=None, Mutable=False Key : Value ('A1', 'I1') : 1.3 ('A1', 'I2') : 1.4 @@ -90,7 +72,7 @@ ('A3', 'I2') : 3.4 ('A3', 'I3') : 3.5 ('A3', 'I4') : 3.6 - U : Size=12, Index=U_index, Domain=Any, Default=None, Mutable=False + U : Size=12, Index=I*A, Domain=Any, Default=None, Mutable=False Key : Value ('I1', 'A1') : 1.3 ('I1', 'A2') : 2.3 @@ -123,4 +105,4 @@ Key : Value None : 1.01 -28 Declarations: A B C D_domain D E_domain_index_0 E_domain E F G_index G H I J Z Y X W U_index U T_index T S R Q P PP O +22 Declarations: A B C D E F G H I J Z Y X W U T S R Q P PP O diff --git a/examples/pyomo/tutorials/param.out b/examples/pyomo/tutorials/param.out index 57e6a752ea5..ea258f5b493 100644 --- a/examples/pyomo/tutorials/param.out +++ b/examples/pyomo/tutorials/param.out @@ -1,22 +1,13 @@ -5 Set Declarations +2 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 4 : {2, 4, 6, 8} B : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} - R_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 12 : {(2, 1), (2, 2), (2, 3), (4, 1), (4, 2), (4, 3), (6, 1), (6, 2), (6, 3), (8, 1), (8, 2), (8, 3)} - W_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 12 : {(2, 1), (2, 2), (2, 3), (4, 1), (4, 2), (4, 3), (6, 1), (6, 2), (6, 3), (8, 1), (8, 2), (8, 3)} - X_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 12 : {(2, 1), (2, 2), (2, 3), (4, 1), (4, 2), (4, 3), (6, 1), (6, 2), (6, 3), (8, 1), (8, 2), (8, 3)} 9 Param Declarations - R : Size=12, Index=R_index, Domain=Any, Default=99.0, Mutable=False + R : Size=12, Index=A*B, Domain=Any, Default=99.0, Mutable=False Key : Value (2, 1) : 1 (2, 2) : 1 @@ -35,7 +26,7 @@ 1 : 1 2 : 2 3 : 9 - W : Size=12, Index=W_index, Domain=Any, Default=None, Mutable=False + W : Size=12, Index=A*B, Domain=Any, Default=None, Mutable=False Key : Value (2, 1) : 2 (2, 2) : 4 @@ -49,7 +40,7 @@ (8, 1) : 8 (8, 2) : 16 (8, 3) : 24 - X : Size=12, Index=X_index, Domain=Any, Default=None, Mutable=False + X : Size=12, Index=A*B, Domain=Any, Default=None, Mutable=False Key : Value (2, 1) : 1.3 (2, 2) : 1.4 @@ -73,4 +64,4 @@ Key : Value None : 1.1 -14 Declarations: A B Z Y X_index X W_index W V U T S R_index R +11 Declarations: A B Z Y X W V U T S R diff --git a/examples/pyomo/tutorials/set.out b/examples/pyomo/tutorials/set.out index b01b666c012..818977f6155 100644 --- a/examples/pyomo/tutorials/set.out +++ b/examples/pyomo/tutorials/set.out @@ -1,15 +1,12 @@ -28 Set Declarations +23 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} B : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 4 : {2, 3, 4, 5} - C : Size=0, Index=C_index, Ordered=Insertion + C : Size=0, Index=A*B, Ordered=Insertion Key : Dimen : Domain : Size : Members - C_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 12 : {(1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5), (3, 2), (3, 3), (3, 4), (3, 5)} D : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members None : 1 : A | B : 5 : {1, 2, 3, 4, 5} @@ -26,15 +23,9 @@ Key : Dimen : Domain : Size : Members None : 2 : A*B : 12 : {(1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5), (3, 2), (3, 3), (3, 4), (3, 5)} Hsub : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Hsub_domain : 3 : {(1, 2), (1, 3), (3, 3)} - Hsub_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 12 : {(1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5), (3, 2), (3, 3), (3, 4), (3, 5)} + None : 2 : A*B : 3 : {(1, 2), (1, 3), (3, 3)} I : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : I_domain : 12 : {(1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5), (3, 2), (3, 3), (3, 4), (3, 5)} - I_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members None : 2 : A*B : 12 : {(1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5), (3, 2), (3, 3), (3, 4), (3, 5)} J : Size=1, Index=None, Ordered=Insertion @@ -53,15 +44,12 @@ Key : Dimen : Domain : Size : Members None : 1 : Any : 2 : {1, 3} N : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : N_domain : 0 : {} - N_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 12 : {(1, 2), (1, 3), (1, 4), (1, 5), (2, 2), (2, 3), (2, 4), (2, 5), (3, 2), (3, 3), (3, 4), (3, 5)} + None : 2 : A*B : 0 : {} O : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : -- : Any : 0 : {} - P : Size=16, Index=P_index, Ordered=Insertion + P : Size=16, Index=B*B, Ordered=Insertion Key : Dimen : Domain : Size : Members (2, 2) : 1 : Any : 4 : {0, 1, 2, 3} (2, 3) : 1 : Any : 6 : {0, 1, 2, 3, 4, 5} @@ -79,9 +67,6 @@ (5, 3) : 1 : Any : 15 : {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14} (5, 4) : 1 : Any : 20 : {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19} (5, 5) : 1 : Any : 25 : {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24} - P_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : B*B : 16 : {(2, 2), (2, 3), (2, 4), (2, 5), (3, 2), (3, 3), (3, 4), (3, 5), (4, 2), (4, 3), (4, 4), (4, 5), (5, 2), (5, 3), (5, 4), (5, 5)} R : Size=3, Index=B, Ordered=Insertion Key : Dimen : Domain : Size : Members 2 : 1 : Any : 3 : {1, 3, 5} @@ -98,16 +83,11 @@ U : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 5 : {1, 2, 6, 24, 120} - V : Size=4, Index=V_index, Ordered=Insertion + V : Size=4, Index=[1:4], Ordered=Insertion Key : Dimen : Domain : Size : Members 1 : 1 : Any : 5 : {1, 2, 3, 4, 5} 2 : 1 : Any : 5 : {1, 3, 5, 7, 9} 3 : 1 : Any : 5 : {1, 4, 7, 10, 13} 4 : 1 : Any : 5 : {1, 5, 9, 13, 17} -1 RangeSet Declarations - V_index : Dimen=1, Size=4, Bounds=(1, 4) - Key : Finite : Members - None : True : [1:4] - -29 Declarations: A B C_index C D E F G H Hsub_domain Hsub I_domain I J K K_2 L M N_domain N O P_index P R S T U V_index V +23 Declarations: A B C D E F G H Hsub I J K K_2 L M N O P R S T U V diff --git a/examples/pyomo/tutorials/table.out b/examples/pyomo/tutorials/table.out index 1eba28afd19..75e2b0aee33 100644 --- a/examples/pyomo/tutorials/table.out +++ b/examples/pyomo/tutorials/table.out @@ -1,4 +1,4 @@ -16 Set Declarations +10 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'A1', 'A2', 'A3'} @@ -9,27 +9,15 @@ Key : Dimen : Domain : Size : Members None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} D : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : D_domain : 3 : {('A1', 1), ('A2', 2), ('A3', 3)} - D_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} + None : 2 : A*B : 3 : {('A1', 1), ('A2', 2), ('A3', 3)} E : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 3 : E_domain : 6 : {('A1', 1, 'A1'), ('A1', 1, 'A2'), ('A2', 2, 'A2'), ('A2', 2, 'A3'), ('A3', 3, 'A1'), ('A3', 3, 'A3')} - E_domain : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 3 : E_domain_index_0*A : 27 : {('A1', 1, 'A1'), ('A1', 1, 'A2'), ('A1', 1, 'A3'), ('A1', 2, 'A1'), ('A1', 2, 'A2'), ('A1', 2, 'A3'), ('A1', 3, 'A1'), ('A1', 3, 'A2'), ('A1', 3, 'A3'), ('A2', 1, 'A1'), ('A2', 1, 'A2'), ('A2', 1, 'A3'), ('A2', 2, 'A1'), ('A2', 2, 'A2'), ('A2', 2, 'A3'), ('A2', 3, 'A1'), ('A2', 3, 'A2'), ('A2', 3, 'A3'), ('A3', 1, 'A1'), ('A3', 1, 'A2'), ('A3', 1, 'A3'), ('A3', 2, 'A1'), ('A3', 2, 'A2'), ('A3', 2, 'A3'), ('A3', 3, 'A1'), ('A3', 3, 'A2'), ('A3', 3, 'A3')} - E_domain_index_0 : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} + None : 3 : A*B*A : 6 : {('A1', 1, 'A1'), ('A1', 1, 'A2'), ('A2', 2, 'A2'), ('A2', 2, 'A3'), ('A3', 3, 'A1'), ('A3', 3, 'A3')} F : Size=0, Index=A, Ordered=Insertion Key : Dimen : Domain : Size : Members - G : Size=0, Index=G_index, Ordered=Insertion + G : Size=0, Index=A*B, Ordered=Insertion Key : Dimen : Domain : Size : Members - G_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} H : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'H1', 'H2', 'H3'} @@ -39,12 +27,6 @@ J : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} - T_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*I : 12 : {('A1', 'I1'), ('A1', 'I2'), ('A1', 'I3'), ('A1', 'I4'), ('A2', 'I1'), ('A2', 'I2'), ('A2', 'I3'), ('A2', 'I4'), ('A3', 'I1'), ('A3', 'I2'), ('A3', 'I3'), ('A3', 'I4')} - U_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : I*A : 12 : {('I1', 'A1'), ('I1', 'A2'), ('I1', 'A3'), ('I2', 'A1'), ('I2', 'A2'), ('I2', 'A3'), ('I3', 'A1'), ('I3', 'A2'), ('I3', 'A3'), ('I4', 'A1'), ('I4', 'A2'), ('I4', 'A3')} 12 Param Declarations O : Size=3, Index=J, Domain=Reals, Default=None, Mutable=False @@ -76,7 +58,7 @@ Key : Value A1 : 3.3 A3 : 3.5 - T : Size=12, Index=T_index, Domain=Any, Default=None, Mutable=False + T : Size=12, Index=A*I, Domain=Any, Default=None, Mutable=False Key : Value ('A1', 'I1') : 1.3 ('A1', 'I2') : 1.4 @@ -90,7 +72,7 @@ ('A3', 'I2') : 3.4 ('A3', 'I3') : 3.5 ('A3', 'I4') : 3.6 - U : Size=12, Index=U_index, Domain=Any, Default=None, Mutable=False + U : Size=12, Index=I*A, Domain=Any, Default=None, Mutable=False Key : Value ('I1', 'A1') : 1.3 ('I1', 'A2') : 2.3 @@ -123,4 +105,4 @@ Key : Value None : 1.01 -28 Declarations: A B C D_domain D E_domain_index_0 E_domain E F G_index G H I J Z Y X W U_index U T_index T S R Q P PP O +22 Declarations: A B C D E F G H I J Z Y X W U T S R Q P PP O diff --git a/examples/pyomobook/blocks-ch/blocks_gen.txt b/examples/pyomobook/blocks-ch/blocks_gen.txt index 63d634b3b95..1636f7e4590 100644 --- a/examples/pyomobook/blocks-ch/blocks_gen.txt +++ b/examples/pyomobook/blocks-ch/blocks_gen.txt @@ -9,13 +9,8 @@ 1 Block Declarations Generator : Size=2, Index=GEN_UNITS, Active=True Generator[G_EAST] : Active=True - 1 Set Declarations - CostCoef_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - 3 Param Declarations - CostCoef : Size=0, Index=Generator[G_EAST].CostCoef_index, Domain=Any, Default=None, Mutable=False + CostCoef : Size=0, Index={1, 2}, Domain=Any, Default=None, Mutable=False Key : Value MaxPower : Size=1, Index=None, Domain=NonNegativeReals, Default=None, Mutable=False Key : Value @@ -27,11 +22,11 @@ 2 Var Declarations Power : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain - 0 : 0 : 120.0 : 500 : False : False : Reals - 1 : 0 : 145.0 : 500 : False : False : Reals - 2 : 0 : 119.0 : 500 : False : False : Reals - 3 : 0 : 42.0 : 500 : False : False : Reals - 4 : 0 : 190.0 : 500 : False : False : Reals + 0 : 0 : 120.0 : 500.0 : False : False : Reals + 1 : 0 : 145.0 : 500.0 : False : False : Reals + 2 : 0 : 119.0 : 500.0 : False : False : Reals + 3 : 0 : 42.0 : 500.0 : False : False : Reals + 4 : 0 : 190.0 : 500.0 : False : False : Reals UnitOn : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 1 : False : True : Binary @@ -57,15 +52,10 @@ 3 : -50.0 : Generator[G_EAST].Power[3] - Generator[G_EAST].Power[2] : Generator[G_EAST].RampLimit : True 4 : -50.0 : Generator[G_EAST].Power[4] - Generator[G_EAST].Power[3] : Generator[G_EAST].RampLimit : True - 8 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef_index CostCoef Cost + 7 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef Cost Generator[G_MAIN] : Active=True - 1 Set Declarations - CostCoef_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - 3 Param Declarations - CostCoef : Size=0, Index=Generator[G_MAIN].CostCoef_index, Domain=Any, Default=None, Mutable=False + CostCoef : Size=0, Index={1, 2}, Domain=Any, Default=None, Mutable=False Key : Value MaxPower : Size=1, Index=None, Domain=NonNegativeReals, Default=None, Mutable=False Key : Value @@ -77,11 +67,11 @@ 2 Var Declarations Power : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain - 0 : 0 : 120.0 : 500 : False : False : Reals - 1 : 0 : 145.0 : 500 : False : False : Reals - 2 : 0 : 119.0 : 500 : False : False : Reals - 3 : 0 : 42.0 : 500 : False : False : Reals - 4 : 0 : 190.0 : 500 : False : False : Reals + 0 : 0 : 120.0 : 500.0 : False : False : Reals + 1 : 0 : 145.0 : 500.0 : False : False : Reals + 2 : 0 : 119.0 : 500.0 : False : False : Reals + 3 : 0 : 42.0 : 500.0 : False : False : Reals + 4 : 0 : 190.0 : 500.0 : False : False : Reals UnitOn : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 1 : False : True : Binary @@ -107,7 +97,7 @@ 3 : -50.0 : Generator[G_MAIN].Power[3] - Generator[G_MAIN].Power[2] : Generator[G_MAIN].RampLimit : True 4 : -50.0 : Generator[G_MAIN].Power[4] - Generator[G_MAIN].Power[3] : Generator[G_MAIN].RampLimit : True - 8 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef_index CostCoef Cost + 7 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef Cost 3 Declarations: TIME GEN_UNITS Generator 2 Set Declarations @@ -121,13 +111,8 @@ 1 Block Declarations Generator : Size=2, Index=GEN_UNITS, Active=True Generator[G_EAST] : Active=True - 1 Set Declarations - CostCoef_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - 3 Param Declarations - CostCoef : Size=0, Index=Generator[G_EAST].CostCoef_index, Domain=Any, Default=None, Mutable=False + CostCoef : Size=0, Index={1, 2}, Domain=Any, Default=None, Mutable=False Key : Value MaxPower : Size=1, Index=None, Domain=NonNegativeReals, Default=None, Mutable=False Key : Value @@ -139,11 +124,11 @@ 2 Var Declarations Power : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain - 0 : 0 : 120.0 : 500 : False : False : Reals - 1 : 0 : 145.0 : 500 : False : False : Reals - 2 : 0 : 119.0 : 500 : False : False : Reals - 3 : 0 : 42.0 : 500 : False : False : Reals - 4 : 0 : 190.0 : 500 : False : False : Reals + 0 : 0 : 120.0 : 500.0 : False : False : Reals + 1 : 0 : 145.0 : 500.0 : False : False : Reals + 2 : 0 : 119.0 : 500.0 : False : False : Reals + 3 : 0 : 42.0 : 500.0 : False : False : Reals + 4 : 0 : 190.0 : 500.0 : False : False : Reals UnitOn : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 1 : False : True : Binary @@ -169,15 +154,10 @@ 3 : -50.0 : Generator[G_EAST].Power[3] - Generator[G_EAST].Power[2] : Generator[G_EAST].RampLimit : True 4 : -50.0 : Generator[G_EAST].Power[4] - Generator[G_EAST].Power[3] : Generator[G_EAST].RampLimit : True - 8 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef_index CostCoef Cost + 7 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef Cost Generator[G_MAIN] : Active=True - 1 Set Declarations - CostCoef_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - 3 Param Declarations - CostCoef : Size=0, Index=Generator[G_MAIN].CostCoef_index, Domain=Any, Default=None, Mutable=False + CostCoef : Size=0, Index={1, 2}, Domain=Any, Default=None, Mutable=False Key : Value MaxPower : Size=1, Index=None, Domain=NonNegativeReals, Default=None, Mutable=False Key : Value @@ -189,11 +169,11 @@ 2 Var Declarations Power : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain - 0 : 0 : 120.0 : 500 : False : False : Reals - 1 : 0 : 145.0 : 500 : False : False : Reals - 2 : 0 : 119.0 : 500 : False : False : Reals - 3 : 0 : 42.0 : 500 : False : False : Reals - 4 : 0 : 190.0 : 500 : False : False : Reals + 0 : 0 : 120.0 : 500.0 : False : False : Reals + 1 : 0 : 145.0 : 500.0 : False : False : Reals + 2 : 0 : 119.0 : 500.0 : False : False : Reals + 3 : 0 : 42.0 : 500.0 : False : False : Reals + 4 : 0 : 190.0 : 500.0 : False : False : Reals UnitOn : Size=5, Index=TIME Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : 1 : False : True : Binary @@ -219,7 +199,7 @@ 3 : -50.0 : Generator[G_MAIN].Power[3] - Generator[G_MAIN].Power[2] : Generator[G_MAIN].RampLimit : True 4 : -50.0 : Generator[G_MAIN].Power[4] - Generator[G_MAIN].Power[3] : Generator[G_MAIN].RampLimit : True - 8 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef_index CostCoef Cost + 7 Declarations: MaxPower RampLimit Power UnitOn limit_ramp CostCoef Cost 3 Declarations: TIME GEN_UNITS Generator Generator[G_MAIN].Power[4] = 190.0 diff --git a/examples/pyomobook/blocks-ch/lotsizing_uncertain.txt b/examples/pyomobook/blocks-ch/lotsizing_uncertain.txt index db9eee79cc3..08f92ae9262 100644 --- a/examples/pyomobook/blocks-ch/lotsizing_uncertain.txt +++ b/examples/pyomobook/blocks-ch/lotsizing_uncertain.txt @@ -1,20 +1,3 @@ -5 Set Declarations - i_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : T*S : 25 : {(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)} - i_neg_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : T*S : 25 : {(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)} - i_pos_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : T*S : 25 : {(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)} - x_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : T*S : 25 : {(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)} - y_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : T*S : 25 : {(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (5, 1), (5, 2), (5, 3), (5, 4), (5, 5)} - 2 RangeSet Declarations S : Dimen=1, Size=5, Bounds=(1, 5) Key : Finite : Members @@ -24,7 +7,7 @@ None : True : [1:5] 5 Var Declarations - i : Size=25, Index=i_index + i : Size=25, Index=T*S Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 1) : None : None : None : False : True : Reals (1, 2) : None : None : None : False : True : Reals @@ -51,7 +34,7 @@ (5, 3) : None : None : None : False : True : Reals (5, 4) : None : None : None : False : True : Reals (5, 5) : None : None : None : False : True : Reals - i_neg : Size=25, Index=i_neg_index + i_neg : Size=25, Index=T*S Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 1) : 0 : None : None : False : True : NonNegativeReals (1, 2) : 0 : None : None : False : True : NonNegativeReals @@ -78,7 +61,7 @@ (5, 3) : 0 : None : None : False : True : NonNegativeReals (5, 4) : 0 : None : None : False : True : NonNegativeReals (5, 5) : 0 : None : None : False : True : NonNegativeReals - i_pos : Size=25, Index=i_pos_index + i_pos : Size=25, Index=T*S Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 1) : 0 : None : None : False : True : NonNegativeReals (1, 2) : 0 : None : None : False : True : NonNegativeReals @@ -105,7 +88,7 @@ (5, 3) : 0 : None : None : False : True : NonNegativeReals (5, 4) : 0 : None : None : False : True : NonNegativeReals (5, 5) : 0 : None : None : False : True : NonNegativeReals - x : Size=25, Index=x_index + x : Size=25, Index=T*S Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 1) : 0 : None : None : False : True : NonNegativeReals (1, 2) : 0 : None : None : False : True : NonNegativeReals @@ -132,7 +115,7 @@ (5, 3) : 0 : None : None : False : True : NonNegativeReals (5, 4) : 0 : None : None : False : True : NonNegativeReals (5, 5) : 0 : None : None : False : True : NonNegativeReals - y : Size=25, Index=y_index + y : Size=25, Index=T*S Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 1) : 0 : None : 1 : False : True : Binary (1, 2) : 0 : None : 1 : False : True : Binary @@ -160,4 +143,4 @@ (5, 4) : 0 : None : 1 : False : True : Binary (5, 5) : 0 : None : 1 : False : True : Binary -12 Declarations: T S y_index y x_index x i_index i i_pos_index i_pos i_neg_index i_neg +7 Declarations: T S y x i i_pos i_neg diff --git a/examples/pyomobook/dae-ch/path_constraint.txt b/examples/pyomobook/dae-ch/path_constraint.txt index 421692b33e9..97e56ab8816 100644 --- a/examples/pyomobook/dae-ch/path_constraint.txt +++ b/examples/pyomobook/dae-ch/path_constraint.txt @@ -1,8 +1,3 @@ -1 RangeSet Declarations - t_domain : Dimen=1, Size=Inf, Bounds=(0, 1) - Key : Finite : Members - None : False : [0..1] - 1 Param Declarations tf : Size=1, Index=None, Domain=Any, Default=None, Mutable=False Key : Value @@ -68,4 +63,4 @@ 0 : None : None : None : False : True : Reals 1 : None : None : None : False : True : Reals -15 Declarations: tf t_domain t u x1 x2 x3 dx1 dx2 dx3 x1dotcon x2dotcon x3dotcon obj con +14 Declarations: tf t u x1 x2 x3 dx1 dx2 dx3 x1dotcon x2dotcon x3dotcon obj con diff --git a/examples/pyomobook/optimization-ch/ConcHLinScript.txt b/examples/pyomobook/optimization-ch/ConcHLinScript.txt index c04591c94dc..0d34868ed99 100644 --- a/examples/pyomobook/optimization-ch/ConcHLinScript.txt +++ b/examples/pyomobook/optimization-ch/ConcHLinScript.txt @@ -1,7 +1,7 @@ Model 'Linear (H)' Variables: - x : Size=2, Index=x_index + x : Size=2, Index={I_C_Scoops, Peanuts} Key : Lower : Value : Upper : Fixed : Stale : Domain I_C_Scoops : 0 : 0.0 : 100 : False : False : Reals Peanuts : 0 : 40.6 : 40.6 : False : False : Reals @@ -9,7 +9,7 @@ Model 'Linear (H)' Objectives: z : Size=1, Index=None, Active=True Key : Active : Value - None : True : 3.83388751715 + None : True : 3.8338875171467763 Constraints: budgetconstr : Size=1 diff --git a/examples/pyomobook/optimization-ch/ConcreteH.txt b/examples/pyomobook/optimization-ch/ConcreteH.txt index 5e669ff71e0..04bbbdab857 100644 --- a/examples/pyomobook/optimization-ch/ConcreteH.txt +++ b/examples/pyomobook/optimization-ch/ConcreteH.txt @@ -1,10 +1,5 @@ -1 Set Declarations - x_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {'I_C_Scoops', 'Peanuts'} - 1 Var Declarations - x : Size=2, Index=x_index + x : Size=2, Index={I_C_Scoops, Peanuts} Key : Lower : Value : Upper : Fixed : Stale : Domain I_C_Scoops : 0 : None : 100 : False : True : Reals Peanuts : 0 : None : 40.6 : False : True : Reals @@ -19,4 +14,4 @@ Key : Lower : Body : Upper : Active None : -Inf : 3.14*x[I_C_Scoops] + 0.2718*x[Peanuts] : 12.0 : True -4 Declarations: x_index x z budgetconstr +3 Declarations: x z budgetconstr diff --git a/examples/pyomobook/optimization-ch/ConcreteHLinear.txt b/examples/pyomobook/optimization-ch/ConcreteHLinear.txt index 2e778c2bd1b..7f19aca87ec 100644 --- a/examples/pyomobook/optimization-ch/ConcreteHLinear.txt +++ b/examples/pyomobook/optimization-ch/ConcreteHLinear.txt @@ -1,10 +1,5 @@ -1 Set Declarations - x_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {'I_C_Scoops', 'Peanuts'} - 1 Var Declarations - x : Size=2, Index=x_index + x : Size=2, Index={I_C_Scoops, Peanuts} Key : Lower : Value : Upper : Fixed : Stale : Domain I_C_Scoops : 0 : None : 100 : False : True : Reals Peanuts : 0 : None : 40.6 : False : True : Reals @@ -19,4 +14,4 @@ Key : Lower : Body : Upper : Active None : -Inf : 3.14*x[I_C_Scoops] + 0.2718*x[Peanuts] : 12.0 : True -4 Declarations: x_index x z budgetconstr +3 Declarations: x z budgetconstr diff --git a/examples/pyomobook/overview-ch/wl_concrete_script.txt b/examples/pyomobook/overview-ch/wl_concrete_script.txt index dae31e1a035..165289552d3 100644 --- a/examples/pyomobook/overview-ch/wl_concrete_script.txt +++ b/examples/pyomobook/overview-ch/wl_concrete_script.txt @@ -1,4 +1,4 @@ -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 1.0 : 1 : False : False : Binary Harlingen : 0 : 1.0 : 1 : False : False : Binary diff --git a/examples/pyomobook/overview-ch/wl_excel.txt b/examples/pyomobook/overview-ch/wl_excel.txt index dae31e1a035..165289552d3 100644 --- a/examples/pyomobook/overview-ch/wl_excel.txt +++ b/examples/pyomobook/overview-ch/wl_excel.txt @@ -1,4 +1,4 @@ -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 1.0 : 1 : False : False : Binary Harlingen : 0 : 1.0 : 1 : False : False : Binary diff --git a/examples/pyomobook/overview-ch/wl_list.txt b/examples/pyomobook/overview-ch/wl_list.txt index 2054efe153d..c0d44f1a0c9 100644 --- a/examples/pyomobook/overview-ch/wl_list.txt +++ b/examples/pyomobook/overview-ch/wl_list.txt @@ -1,25 +1,5 @@ -6 Set Declarations - demand_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 4 : {1, 2, 3, 4} - warehouse_active_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 12 : {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} - x_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : x_index_0*x_index_1 : 12 : {('Harlingen', 'NYC'), ('Harlingen', 'LA'), ('Harlingen', 'Chicago'), ('Harlingen', 'Houston'), ('Memphis', 'NYC'), ('Memphis', 'LA'), ('Memphis', 'Chicago'), ('Memphis', 'Houston'), ('Ashland', 'NYC'), ('Ashland', 'LA'), ('Ashland', 'Chicago'), ('Ashland', 'Houston')} - x_index_0 : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'Harlingen', 'Memphis', 'Ashland'} - x_index_1 : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 4 : {'NYC', 'LA', 'Chicago', 'Houston'} - y_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'Harlingen', 'Memphis', 'Ashland'} - 2 Var Declarations - x : Size=12, Index=x_index + x : Size=12, Index={Harlingen, Memphis, Ashland}*{NYC, LA, Chicago, Houston} Key : Lower : Value : Upper : Fixed : Stale : Domain ('Ashland', 'Chicago') : 0 : None : 1 : False : True : Reals ('Ashland', 'Houston') : 0 : None : 1 : False : True : Reals @@ -33,7 +13,7 @@ ('Memphis', 'Houston') : 0 : None : 1 : False : True : Reals ('Memphis', 'LA') : 0 : None : 1 : False : True : Reals ('Memphis', 'NYC') : 0 : None : 1 : False : True : Reals - y : Size=3, Index=y_index + y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : None : 1 : False : True : Binary Harlingen : 0 : None : 1 : False : True : Binary @@ -45,7 +25,7 @@ None : True : minimize : 1956*x[Harlingen,NYC] + 1606*x[Harlingen,LA] + 1410*x[Harlingen,Chicago] + 330*x[Harlingen,Houston] + 1096*x[Memphis,NYC] + 1792*x[Memphis,LA] + 531*x[Memphis,Chicago] + 567*x[Memphis,Houston] + 485*x[Ashland,NYC] + 2322*x[Ashland,LA] + 324*x[Ashland,Chicago] + 1236*x[Ashland,Houston] 3 Constraint Declarations - demand : Size=4, Index=demand_index, Active=True + demand : Size=4, Index={1, 2, 3, 4}, Active=True Key : Lower : Body : Upper : Active 1 : 1.0 : x[Harlingen,NYC] + x[Memphis,NYC] + x[Ashland,NYC] : 1.0 : True 2 : 1.0 : x[Harlingen,LA] + x[Memphis,LA] + x[Ashland,LA] : 1.0 : True @@ -54,7 +34,7 @@ num_warehouses : Size=1, Index=None, Active=True Key : Lower : Body : Upper : Active None : -Inf : y[Harlingen] + y[Memphis] + y[Ashland] : 2.0 : True - warehouse_active : Size=12, Index=warehouse_active_index, Active=True + warehouse_active : Size=12, Index={1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, Active=True Key : Lower : Body : Upper : Active 1 : -Inf : x[Harlingen,NYC] - y[Harlingen] : 0.0 : True 2 : -Inf : x[Harlingen,LA] - y[Harlingen] : 0.0 : True @@ -69,4 +49,4 @@ 11 : -Inf : x[Ashland,Chicago] - y[Ashland] : 0.0 : True 12 : -Inf : x[Ashland,Houston] - y[Ashland] : 0.0 : True -12 Declarations: x_index_0 x_index_1 x_index x y_index y obj demand_index demand warehouse_active_index warehouse_active num_warehouses +6 Declarations: x y obj demand warehouse_active num_warehouses diff --git a/examples/pyomobook/pyomo-components-ch/con_declaration.txt b/examples/pyomobook/pyomo-components-ch/con_declaration.txt index 019cd448eb0..b4709bd5490 100644 --- a/examples/pyomobook/pyomo-components-ch/con_declaration.txt +++ b/examples/pyomobook/pyomo-components-ch/con_declaration.txt @@ -1,10 +1,5 @@ -1 Set Declarations - x_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - 1 Var Declarations - x : Size=2, Index=x_index + x : Size=2, Index={1, 2} Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : 1.0 : None : False : False : Reals 2 : None : 1.0 : None : False : False : Reals @@ -14,14 +9,9 @@ Key : Lower : Body : Upper : Active None : -Inf : x[2] - x[1] : 7.5 : True -3 Declarations: x_index x diff -1 Set Declarations - x_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - +2 Declarations: x diff 1 Var Declarations - x : Size=2, Index=x_index + x : Size=2, Index={1, 2} Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : 1.0 : None : False : False : Reals 2 : None : 1.0 : None : False : False : Reals @@ -31,40 +21,24 @@ Key : Lower : Body : Upper : Active None : -Inf : x[2] - x[1] : 7.5 : True -3 Declarations: x_index x diff -2 Set Declarations - CoverConstr_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - y_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - +2 Declarations: x diff 1 Var Declarations - y : Size=3, Index=y_index + y : Size=3, Index={1, 2, 3} Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : 0 : 0.0 : None : False : False : NonNegativeReals 2 : 0 : 0.0 : None : False : False : NonNegativeReals 3 : 0 : 0.0 : None : False : False : NonNegativeReals 1 Constraint Declarations - CoverConstr : Size=3, Index=CoverConstr_index, Active=True + CoverConstr : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 1.0 : y[1] : +Inf : True 2 : 2.9 : 3.1*y[2] : +Inf : True 3 : 3.1 : 4.5*y[3] : +Inf : True -4 Declarations: y_index y CoverConstr_index CoverConstr -2 Set Declarations - Pred_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 5 : {1, 2, 3, 4, 5} - StartTime_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 5 : {1, 2, 3, 4, 5} - +2 Declarations: y CoverConstr 1 Var Declarations - StartTime : Size=5, Index=StartTime_index + StartTime : Size=5, Index={1, 2, 3, 4, 5} Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : 1.0 : None : False : False : Reals 2 : None : 1.0 : None : False : False : Reals @@ -73,14 +47,14 @@ 5 : None : 1.0 : None : False : False : Reals 1 Constraint Declarations - Pred : Size=4, Index=Pred_index, Active=True + Pred : Size=4, Index={1, 2, 3, 4, 5}, Active=True Key : Lower : Body : Upper : Active 1 : -Inf : StartTime[1] - StartTime[2] : 0.0 : True 2 : -Inf : StartTime[2] - StartTime[3] : 0.0 : True 3 : -Inf : StartTime[3] - StartTime[4] : 0.0 : True 4 : -Inf : StartTime[4] - StartTime[5] : 0.0 : True -4 Declarations: StartTime_index StartTime Pred_index Pred +2 Declarations: StartTime Pred 0.0 inf 7.5 diff --git a/examples/pyomobook/pyomo-components-ch/examples.txt b/examples/pyomobook/pyomo-components-ch/examples.txt index 635b988cbcd..27ea1ba130b 100644 --- a/examples/pyomobook/pyomo-components-ch/examples.txt +++ b/examples/pyomobook/pyomo-components-ch/examples.txt @@ -1,20 +1,17 @@ indexed1 -3 Set Declarations +2 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} B : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 2 : {'Q', 'R'} - y_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 6 : {(1, 'Q'), (1, 'R'), (2, 'Q'), (2, 'R'), (3, 'Q'), (3, 'R')} 2 Var Declarations x : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain None : None : None : None : False : True : Reals - y : Size=6, Index=y_index + y : Size=6, Index=A*B Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 'Q') : None : None : None : False : True : Reals (1, 'R') : None : None : None : False : True : Reals @@ -38,4 +35,4 @@ indexed1 2 : -Inf : 2*x : 0.0 : True 3 : -Inf : 3*x : 0.0 : True -8 Declarations: A B x y_index y o c d +7 Declarations: A B x y o c d diff --git a/examples/pyomobook/pyomo-components-ch/expr_declaration.txt b/examples/pyomobook/pyomo-components-ch/expr_declaration.txt index 66c99f6502a..86e0feac27f 100644 --- a/examples/pyomobook/pyomo-components-ch/expr_declaration.txt +++ b/examples/pyomobook/pyomo-components-ch/expr_declaration.txt @@ -18,28 +18,20 @@ None : x + 2 3 Declarations: x e1 e2 -2 Set Declarations - e_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - x_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - 1 Var Declarations - x : Size=3, Index=x_index + x : Size=3, Index={1, 2, 3} Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : None : None : False : True : Reals 2 : None : None : None : False : True : Reals 3 : None : None : None : False : True : Reals 1 Expression Declarations - e : Size=2, Index=e_index + e : Size=2, Index={1, 2, 3} Key : Expression 2 : x[2]**2 3 : x[3]**2 -4 Declarations: x_index x e_index e +2 Declarations: x e 1 Var Declarations x : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain diff --git a/examples/pyomobook/pyomo-components-ch/obj_declaration.txt b/examples/pyomobook/pyomo-components-ch/obj_declaration.txt index e43134b8d92..607586a1fb3 100644 --- a/examples/pyomobook/pyomo-components-ch/obj_declaration.txt +++ b/examples/pyomobook/pyomo-components-ch/obj_declaration.txt @@ -14,7 +14,7 @@ declexprrule Model unknown Variables: - x : Size=2, Index=x_index + x : Size=2, Index={1, 2} Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : 1.0 : None : False : False : Reals 2 : None : 1.0 : None : False : False : Reals @@ -34,19 +34,19 @@ declskip Model unknown Variables: - x : Size=3, Index=x_index + x : Size=3, Index={Q, R, S} Key : Lower : Value : Upper : Fixed : Stale : Domain Q : None : 1.0 : None : False : False : Reals R : None : 1.0 : None : False : False : Reals S : None : 1.0 : None : False : False : Reals Objectives: - d : Size=3, Index=d_index, Active=True + d : Size=3, Index={Q, R, S}, Active=True Key : Active : Value Q : True : 1.0 R : True : 1.0 S : True : 1.0 - e : Size=2, Index=e_index, Active=True + e : Size=2, Index={Q, R, S}, Active=True Key : Active : Value Q : True : 1.0 S : True : 1.0 @@ -60,7 +60,7 @@ x[Q] + 2*x[R] Model unknown Variables: - x : Size=2, Index=x_index + x : Size=2, Index={Q, R} Key : Lower : Value : Upper : Fixed : Stale : Domain Q : None : 1.5 : None : False : False : Reals R : None : 2.5 : None : False : False : Reals diff --git a/examples/pyomobook/pyomo-components-ch/param_declaration.txt b/examples/pyomobook/pyomo-components-ch/param_declaration.txt index 9b8ce9cacdb..8c8a49eedc6 100644 --- a/examples/pyomobook/pyomo-components-ch/param_declaration.txt +++ b/examples/pyomobook/pyomo-components-ch/param_declaration.txt @@ -1,16 +1,13 @@ -3 Set Declarations +2 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} B : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 2 : {'A', 'B'} - T_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 6 : {(1, 'A'), (1, 'B'), (2, 'A'), (2, 'B'), (3, 'A'), (3, 'B')} 3 Param Declarations - T : Size=3, Index=T_index, Domain=Any, Default=None, Mutable=False + T : Size=3, Index=A*B, Domain=Any, Default=None, Mutable=False Key : Value (1, 'A') : 10 (2, 'B') : 20 @@ -24,4 +21,4 @@ Key : Value None : 32 -6 Declarations: Z A B U T_index T +5 Declarations: Z A B U T diff --git a/examples/pyomobook/pyomo-components-ch/param_initialization.txt b/examples/pyomobook/pyomo-components-ch/param_initialization.txt index d1ac6aba989..e0bcdf11a71 100644 --- a/examples/pyomobook/pyomo-components-ch/param_initialization.txt +++ b/examples/pyomobook/pyomo-components-ch/param_initialization.txt @@ -1,27 +1,15 @@ -6 Set Declarations +2 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} B : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} - T_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)} - U_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*A : 9 : {(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)} - XX_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*A : 9 : {(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)} - X_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*A : 9 : {(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)} 5 Param Declarations - T : Size=0, Index=T_index, Domain=Any, Default=None, Mutable=False + T : Size=0, Index=A*B, Domain=Any, Default=None, Mutable=False Key : Value - U : Size=9, Index=U_index, Domain=Any, Default=0, Mutable=False + U : Size=9, Index=A*A, Domain=Any, Default=0, Mutable=False Key : Value (1, 1) : 10 (2, 2) : 20 @@ -30,7 +18,7 @@ Key : Value 1 : 10 3 : 30 - X : Size=9, Index=X_index, Domain=Any, Default=None, Mutable=False + X : Size=9, Index=A*A, Domain=Any, Default=None, Mutable=False Key : Value (1, 1) : 1 (1, 2) : 2 @@ -41,7 +29,7 @@ (3, 1) : 3 (3, 2) : 6 (3, 3) : 9 - XX : Size=9, Index=XX_index, Domain=Any, Default=None, Mutable=False + XX : Size=9, Index=A*A, Domain=Any, Default=None, Mutable=False Key : Value (1, 1) : 1 (1, 2) : 2 @@ -53,7 +41,7 @@ (3, 2) : 8 (3, 3) : 14 -11 Declarations: A X_index X XX_index XX B W U_index U T_index T +7 Declarations: A X XX B W U T 2 3 False diff --git a/examples/pyomobook/pyomo-components-ch/set_declaration.txt b/examples/pyomobook/pyomo-components-ch/set_declaration.txt index bdbb7376de4..a588e5601b6 100644 --- a/examples/pyomobook/pyomo-components-ch/set_declaration.txt +++ b/examples/pyomobook/pyomo-components-ch/set_declaration.txt @@ -5,22 +5,16 @@ 1 Declarations: A 0 Declarations: -4 Set Declarations - E : Size=1, Index=E_index, Ordered=Insertion +2 Set Declarations + E : Size=1, Index={1, 2, 3}, Ordered=Insertion Key : Dimen : Domain : Size : Members 2 : 1 : Any : 3 : {21, 22, 23} - E_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - F : Size=2, Index=F_index, Ordered=Insertion + F : Size=2, Index={1, 2, 3}, Ordered=Insertion Key : Dimen : Domain : Size : Members 1 : 1 : Any : 3 : {11, 12, 13} 3 : 1 : Any : 3 : {31, 32, 33} - F_index : Size=1, Index=None, Ordered=False - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} -4 Declarations: E_index E F_index F +2 Declarations: E F 6 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members diff --git a/examples/pyomobook/pyomo-components-ch/set_initialization.txt b/examples/pyomobook/pyomo-components-ch/set_initialization.txt index af2ba54a8d2..29900ccb7b2 100644 --- a/examples/pyomobook/pyomo-components-ch/set_initialization.txt +++ b/examples/pyomobook/pyomo-components-ch/set_initialization.txt @@ -1,19 +1,16 @@ -10 Set Declarations +7 Set Declarations B : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {2, 3, 4} C : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 2 : Any : 2 : {(1, 4), (9, 16)} - F : Size=3, Index=F_index, Ordered=Insertion + F : Size=3, Index={2, 3, 4}, Ordered=Insertion Key : Dimen : Domain : Size : Members 2 : 1 : Any : 3 : {1, 3, 5} 3 : 1 : Any : 3 : {2, 4, 6} 4 : 1 : Any : 3 : {3, 5, 7} - F_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {2, 3, 4} - J : Size=9, Index=J_index, Ordered=Insertion + J : Size=9, Index=B*B, Ordered=Insertion Key : Dimen : Domain : Size : Members (2, 2) : 1 : Any : 4 : {0, 1, 2, 3} (2, 3) : 1 : Any : 6 : {0, 1, 2, 3, 4, 5} @@ -24,21 +21,15 @@ (4, 2) : 1 : Any : 8 : {0, 1, 2, 3, 4, 5, 6, 7} (4, 3) : 1 : Any : 12 : {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} (4, 4) : 1 : Any : 16 : {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} - J_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : B*B : 9 : {(2, 2), (2, 3), (2, 4), (3, 2), (3, 3), (3, 4), (4, 2), (4, 3), (4, 4)} P : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 5 : {1, 2, 3, 5, 7} Q : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 4 : {4, 6, 8, 9} - R : Size=2, Index=R_index, Ordered=Insertion + R : Size=2, Index={1, 2, 3}, Ordered=Insertion Key : Dimen : Domain : Size : Members 1 : 1 : Any : 1 : {1,} 2 : 1 : Any : 2 : {1, 2} - R_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} -10 Declarations: B C F_index F J_index J P Q R_index R +7 Declarations: B C F J P Q R diff --git a/examples/pyomobook/scripts-ch/warehouse_cuts.txt b/examples/pyomobook/scripts-ch/warehouse_cuts.txt index 9afe6c4e944..1f097e06cea 100644 --- a/examples/pyomobook/scripts-ch/warehouse_cuts.txt +++ b/examples/pyomobook/scripts-ch/warehouse_cuts.txt @@ -1,7 +1,7 @@ --- Solver Status: optimal --- Optimal Obj. Value = 2745.0 -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 1.0 : 1 : False : False : Binary Harlingen : 0 : 1.0 : 1 : False : False : Binary @@ -9,7 +9,7 @@ y : Size=3, Index=y_index --- Solver Status: optimal --- Optimal Obj. Value = 3168.0 -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 1.0 : 1 : False : False : Binary Harlingen : 0 : 0.0 : 1 : False : False : Binary @@ -17,7 +17,7 @@ y : Size=3, Index=y_index --- Solver Status: optimal --- Optimal Obj. Value = 3563.0 -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 0.0 : 1 : False : False : Binary Harlingen : 0 : 1.0 : 1 : False : False : Binary @@ -25,7 +25,7 @@ y : Size=3, Index=y_index --- Solver Status: optimal --- Optimal Obj. Value = 3986.0 -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 0.0 : 1 : False : False : Binary Harlingen : 0 : 0.0 : 1 : False : False : Binary @@ -33,7 +33,7 @@ y : Size=3, Index=y_index --- Solver Status: optimal --- Optimal Obj. Value = 4367.0 -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 1.0 : 1 : False : False : Binary Harlingen : 0 : 0.0 : 1 : False : False : Binary @@ -41,7 +41,7 @@ y : Size=3, Index=y_index --- Solver Status: optimal --- Optimal Obj. Value = 5302.0 -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 0.0 : 1 : False : False : Binary Harlingen : 0 : 1.0 : 1 : False : False : Binary diff --git a/examples/pyomobook/scripts-ch/warehouse_script.txt b/examples/pyomobook/scripts-ch/warehouse_script.txt index b922643dd2b..fac3aef0880 100644 --- a/examples/pyomobook/scripts-ch/warehouse_script.txt +++ b/examples/pyomobook/scripts-ch/warehouse_script.txt @@ -1,36 +1,10 @@ -y : Size=3, Index=y_index +y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 1.0 : 1 : False : False : Binary Harlingen : 0 : 1.0 : 1 : False : False : Binary Memphis : 0 : 0.0 : 1 : False : False : Binary -8 Set Declarations - one_per_cust_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 4 : {'NYC', 'LA', 'Chicago', 'Houston'} - warehouse_active_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : warehouse_active_index_0*warehouse_active_index_1 : 12 : {('Harlingen', 'NYC'), ('Harlingen', 'LA'), ('Harlingen', 'Chicago'), ('Harlingen', 'Houston'), ('Memphis', 'NYC'), ('Memphis', 'LA'), ('Memphis', 'Chicago'), ('Memphis', 'Houston'), ('Ashland', 'NYC'), ('Ashland', 'LA'), ('Ashland', 'Chicago'), ('Ashland', 'Houston')} - warehouse_active_index_0 : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'Harlingen', 'Memphis', 'Ashland'} - warehouse_active_index_1 : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 4 : {'NYC', 'LA', 'Chicago', 'Houston'} - x_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : x_index_0*x_index_1 : 12 : {('Harlingen', 'NYC'), ('Harlingen', 'LA'), ('Harlingen', 'Chicago'), ('Harlingen', 'Houston'), ('Memphis', 'NYC'), ('Memphis', 'LA'), ('Memphis', 'Chicago'), ('Memphis', 'Houston'), ('Ashland', 'NYC'), ('Ashland', 'LA'), ('Ashland', 'Chicago'), ('Ashland', 'Houston')} - x_index_0 : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'Harlingen', 'Memphis', 'Ashland'} - x_index_1 : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 4 : {'NYC', 'LA', 'Chicago', 'Houston'} - y_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'Harlingen', 'Memphis', 'Ashland'} - 2 Var Declarations - x : Size=12, Index=x_index + x : Size=12, Index={Harlingen, Memphis, Ashland}*{NYC, LA, Chicago, Houston} Key : Lower : Value : Upper : Fixed : Stale : Domain ('Ashland', 'Chicago') : 0 : 1.0 : 1 : False : False : Reals ('Ashland', 'Houston') : 0 : 0.0 : 1 : False : False : Reals @@ -40,11 +14,11 @@ y : Size=3, Index=y_index ('Harlingen', 'Houston') : 0 : 1.0 : 1 : False : False : Reals ('Harlingen', 'LA') : 0 : 1.0 : 1 : False : False : Reals ('Harlingen', 'NYC') : 0 : 0.0 : 1 : False : False : Reals - ('Memphis', 'Chicago') : 0 : -0.0 : 1 : False : False : Reals + ('Memphis', 'Chicago') : 0 : 0.0 : 1 : False : False : Reals ('Memphis', 'Houston') : 0 : 0.0 : 1 : False : False : Reals ('Memphis', 'LA') : 0 : 0.0 : 1 : False : False : Reals ('Memphis', 'NYC') : 0 : 0.0 : 1 : False : False : Reals - y : Size=3, Index=y_index + y : Size=3, Index={Harlingen, Memphis, Ashland} Key : Lower : Value : Upper : Fixed : Stale : Domain Ashland : 0 : 1.0 : 1 : False : False : Binary Harlingen : 0 : 1.0 : 1 : False : False : Binary @@ -59,13 +33,13 @@ y : Size=3, Index=y_index num_warehouses : Size=1, Index=None, Active=True Key : Lower : Body : Upper : Active None : -Inf : y[Harlingen] + y[Memphis] + y[Ashland] : 2.0 : True - one_per_cust : Size=4, Index=one_per_cust_index, Active=True + one_per_cust : Size=4, Index={NYC, LA, Chicago, Houston}, Active=True Key : Lower : Body : Upper : Active Chicago : 1.0 : x[Harlingen,Chicago] + x[Memphis,Chicago] + x[Ashland,Chicago] : 1.0 : True Houston : 1.0 : x[Harlingen,Houston] + x[Memphis,Houston] + x[Ashland,Houston] : 1.0 : True LA : 1.0 : x[Harlingen,LA] + x[Memphis,LA] + x[Ashland,LA] : 1.0 : True NYC : 1.0 : x[Harlingen,NYC] + x[Memphis,NYC] + x[Ashland,NYC] : 1.0 : True - warehouse_active : Size=12, Index=warehouse_active_index, Active=True + warehouse_active : Size=12, Index={Harlingen, Memphis, Ashland}*{NYC, LA, Chicago, Houston}, Active=True Key : Lower : Body : Upper : Active ('Ashland', 'Chicago') : -Inf : x[Ashland,Chicago] - y[Ashland] : 0.0 : True ('Ashland', 'Houston') : -Inf : x[Ashland,Houston] - y[Ashland] : 0.0 : True @@ -80,4 +54,4 @@ y : Size=3, Index=y_index ('Memphis', 'LA') : -Inf : x[Memphis,LA] - y[Memphis] : 0.0 : True ('Memphis', 'NYC') : -Inf : x[Memphis,NYC] - y[Memphis] : 0.0 : True -14 Declarations: x_index_0 x_index_1 x_index x y_index y obj one_per_cust_index one_per_cust warehouse_active_index_0 warehouse_active_index_1 warehouse_active_index warehouse_active num_warehouses +6 Declarations: x y obj one_per_cust warehouse_active num_warehouses diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 46af1277ba5..8c592dc6580 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -296,8 +296,6 @@ def test_add_decision_rule_vars_positive_case(self): m.working_model.del_component(m.working_model.decision_rule_var_0) m.working_model.del_component(m.working_model.decision_rule_var_1) - m.working_model.del_component(m.working_model.decision_rule_var_0_index) - m.working_model.del_component(m.working_model.decision_rule_var_1_index) config.decision_rule_order = 2 @@ -395,8 +393,6 @@ def test_correct_number_of_decision_rule_constraints(self): # === Decision rule vars have been added m.working_model.del_component(m.working_model.decision_rule_var_0) m.working_model.del_component(m.working_model.decision_rule_var_1) - m.working_model.del_component(m.working_model.decision_rule_var_0_index) - m.working_model.del_component(m.working_model.decision_rule_var_1_index) m.working_model.decision_rule_var_0 = Var([0, 1, 2, 3, 4, 5], initialize=0) m.working_model.decision_rule_var_1 = Var([0, 1, 2, 3, 4, 5], initialize=0) diff --git a/pyomo/core/base/reference.py b/pyomo/core/base/reference.py index 79ae83b97be..62f7a813b5b 100644 --- a/pyomo/core/base/reference.py +++ b/pyomo/core/base/reference.py @@ -612,7 +612,7 @@ def Reference(reference, ctype=NOTSET): ... >>> m.r1 = Reference(m.b[:,:].x) >>> m.r1.pprint() - r1 : Size=4, Index=r1_index, ReferenceTo=b[:, :].x + r1 : Size=4, Index={1, 2}*{3, 4}, ReferenceTo=b[:, :].x Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 3) : 1 : None : 3 : False : True : Reals (1, 4) : 1 : None : 4 : False : True : Reals @@ -625,7 +625,7 @@ def Reference(reference, ctype=NOTSET): >>> m.r2 = Reference(m.b[:,3].x) >>> m.r2.pprint() - r2 : Size=2, Index=b_index_0, ReferenceTo=b[:, 3].x + r2 : Size=2, Index={1, 2}, ReferenceTo=b[:, 3].x Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : 1 : None : 3 : False : True : Reals 2 : 2 : None : 3 : False : True : Reals @@ -642,7 +642,7 @@ def Reference(reference, ctype=NOTSET): ... >>> m.r3 = Reference(m.b[:].x[:]) >>> m.r3.pprint() - r3 : Size=4, Index=r3_index, ReferenceTo=b[:].x[:] + r3 : Size=4, Index=ReferenceSet(b[:].x[:]), ReferenceTo=b[:].x[:] Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 3) : 1 : None : None : False : True : Reals (1, 4) : 1 : None : None : False : True : Reals @@ -657,7 +657,7 @@ def Reference(reference, ctype=NOTSET): >>> m.r3[1,4] = 10 >>> m.b[1].x.pprint() - x : Size=2, Index=b[1].x_index + x : Size=2, Index={3, 4} Key : Lower : Value : Upper : Fixed : Stale : Domain 3 : 1 : None : None : False : True : Reals 4 : 1 : 10 : None : False : False : Reals diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index f68850d9421..50e08d4c616 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -2626,19 +2626,16 @@ def test_pprint(self): m = HierarchicalModel().model buf = StringIO() m.pprint(ostream=buf) - ref = """3 Set Declarations + ref = """2 Set Declarations a1_IDX : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 2 : {5, 4} a3_IDX : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 2 : {6, 7} - a_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} 3 Block Declarations - a : Size=3, Index=a_index, Active=True + a : Size=3, Index={1, 2, 3}, Active=True a[1] : Active=True 2 Block Declarations c : Size=2, Index=a1_IDX, Active=True @@ -2668,9 +2665,9 @@ def test_pprint(self): c : Size=1, Index=None, Active=True 0 Declarations: -6 Declarations: a1_IDX a3_IDX c a_index a b +5 Declarations: a1_IDX a3_IDX c a b """ - print(buf.getvalue()) + self.maxDiff = None self.assertEqual(ref, buf.getvalue()) @unittest.skipIf(not 'glpk' in solvers, "glpk solver is not available") diff --git a/pyomo/core/tests/unit/test_componentuid.py b/pyomo/core/tests/unit/test_componentuid.py index 1c9b3c444bf..2273869104f 100644 --- a/pyomo/core/tests/unit/test_componentuid.py +++ b/pyomo/core/tests/unit/test_componentuid.py @@ -601,31 +601,26 @@ def test_generate_cuid_string_map(self): ComponentUID.generate_cuid_string_map(model, repr_version=1), ComponentUID.generate_cuid_string_map(model), ) - self.assertEqual(len(cuids[0]), 29) - self.assertEqual(len(cuids[1]), 29) + self.assertEqual(len(cuids[0]), 24) + self.assertEqual(len(cuids[1]), 24) for obj in [ model, model.x, model.y, - model.y_index, model.y[1], model.y[2], model.V, - model.V_index, model.V['a', 'b'], model.V[1, '2'], model.V[3, 4], model.b, model.b.z, - model.b.z_index, model.b.z[1], model.b.z['2'], getattr(model.b, '.H'), - getattr(model.b, '.H_index'), getattr(model.b, '.H')['a'], getattr(model.b, '.H')[2], model.B, - model.B_index, model.B['a'], getattr(model.B['a'], '.k'), model.B[2], @@ -642,23 +637,20 @@ def test_generate_cuid_string_map(self): ), ComponentUID.generate_cuid_string_map(model, descend_into=False), ) - self.assertEqual(len(cuids[0]), 18) - self.assertEqual(len(cuids[1]), 18) + self.assertEqual(len(cuids[0]), 15) + self.assertEqual(len(cuids[1]), 15) for obj in [ model, model.x, model.y, - model.y_index, model.y[1], model.y[2], model.V, - model.V_index, model.V['a', 'b'], model.V[1, '2'], model.V[3, 4], model.b, model.B, - model.B_index, model.B['a'], model.B[2], model.component('c tuple')[(1,)], diff --git a/pyomo/core/tests/unit/test_connector.py b/pyomo/core/tests/unit/test_connector.py index 1dde9f3af24..0af07de50e3 100644 --- a/pyomo/core/tests/unit/test_connector.py +++ b/pyomo/core/tests/unit/test_connector.py @@ -301,7 +301,7 @@ def test_expand_single_scalar(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=1, Index='c.expanded_index', Active=True + """c.expanded : Size=1, Index={1}, Active=True Key : Lower : Body : Upper : Active 1 : 1.0 : x : 1.0 : True """, @@ -336,7 +336,7 @@ def test_expand_scalar(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=2, Index='c.expanded_index', Active=True + """c.expanded : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 1.0 : x : 1.0 : True 2 : 1.0 : y : 1.0 : True @@ -372,7 +372,7 @@ def test_expand_expression(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=2, Index='c.expanded_index', Active=True + """c.expanded : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 1.0 : - x : 1.0 : True 2 : 1.0 : 1 + y : 1.0 : True @@ -408,7 +408,7 @@ def test_expand_indexed(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=3, Index='c.expanded_index', Active=True + """c.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 1.0 : x[1] : 1.0 : True 2 : 1.0 : x[2] : 1.0 : True @@ -451,7 +451,7 @@ def test_expand_empty_scalar(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=2, Index='c.expanded_index', Active=True + """c.expanded : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x - 'ECON.auto.x' : 0.0 : True 2 : 0.0 : y - 'ECON.auto.y' : 0.0 : True @@ -488,7 +488,7 @@ def test_expand_empty_expression(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=2, Index='c.expanded_index', Active=True + """c.expanded : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : - x - 'ECON.auto.x' : 0.0 : True 2 : 0.0 : 1 + y - 'ECON.auto.y' : 0.0 : True @@ -533,7 +533,7 @@ def test_expand_empty_indexed(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=3, Index='c.expanded_index', Active=True + """c.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - 'ECON.auto.x'[1] : 0.0 : True 2 : 0.0 : x[2] - 'ECON.auto.x'[2] : 0.0 : True @@ -590,7 +590,7 @@ def test_expand_multiple_empty_indexed(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=3, Index='c.expanded_index', Active=True + """c.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - 'ECON1.auto.x'[1] : 0.0 : True 2 : 0.0 : x[2] - 'ECON1.auto.x'[2] : 0.0 : True @@ -602,7 +602,7 @@ def test_expand_multiple_empty_indexed(self): m.component('d.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """d.expanded : Size=3, Index='d.expanded_index', Active=True + """d.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : 'ECON2.auto.x'[1] - 'ECON1.auto.x'[1] : 0.0 : True 2 : 0.0 : 'ECON2.auto.x'[2] - 'ECON1.auto.x'[2] : 0.0 : True @@ -653,7 +653,7 @@ def test_expand_multiple_indexed(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=3, Index='c.expanded_index', Active=True + """c.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - a2[1] : 0.0 : True 2 : 0.0 : x[2] - a2[2] : 0.0 : True @@ -665,7 +665,7 @@ def test_expand_multiple_indexed(self): m.component('d.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """d.expanded : Size=3, Index='d.expanded_index', Active=True + """d.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : a1[1] - a2[1] : 0.0 : True 2 : 0.0 : a1[2] - a2[2] : 0.0 : True @@ -734,7 +734,7 @@ def test_expand_implicit_indexed(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=3, Index='c.expanded_index', Active=True + """c.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - a2[1] : 0.0 : True 2 : 0.0 : x[2] - a2[2] : 0.0 : True @@ -746,7 +746,7 @@ def test_expand_implicit_indexed(self): m.component('d.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """d.expanded : Size=3, Index='d.expanded_index', Active=True + """d.expanded : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : 'ECON2.auto.x'[1] - x[1] : 0.0 : True 2 : 0.0 : 'ECON2.auto.x'[2] - x[2] : 0.0 : True @@ -789,7 +789,7 @@ def test_varlist_aggregator(self): m.component('c.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """c.expanded : Size=2, Index='c.expanded_index', Active=True + """c.expanded : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : flow[1] - 'ECON1.auto.flow' : 0.0 : True 2 : 0.0 : phase - 'ECON1.auto.phase' : 0.0 : True @@ -800,7 +800,7 @@ def test_varlist_aggregator(self): m.component('d.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """d.expanded : Size=2, Index='d.expanded_index', Active=True + """d.expanded : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : 'ECON2.auto.flow' - flow[2] : 0.0 : True 2 : 0.0 : 'ECON2.auto.phase' - phase : 0.0 : True @@ -844,7 +844,7 @@ def test_indexed_connector(self): m.component('eq.expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """eq.expanded : Size=1, Index='eq.expanded_index', Active=True + """eq.expanded : Size=1, Index={1}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x - y : 0.0 : True """, diff --git a/pyomo/core/tests/unit/test_expr5.txt b/pyomo/core/tests/unit/test_expr5.txt index a5fc934bd77..2bf78cb4985 100644 --- a/pyomo/core/tests/unit/test_expr5.txt +++ b/pyomo/core/tests/unit/test_expr5.txt @@ -1,11 +1,8 @@ -2 Set Declarations +1 Set Declarations A : set A Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} - c3_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 1 : {1,} 2 Param Declarations B : param B @@ -49,8 +46,8 @@ 2 : -Inf : B[2]*x[2] : 1.0 : True 3 : -Inf : B[3]*x[3] : 1.0 : True c3 : con c3 - Size=1, Index=c3_index, Active=True + Size=1, Index={1}, Active=True Key : Lower : Body : Upper : Active 1 : -Inf : y : 0.0 : True -10 Declarations: A B C x y o c1 c2 c3_index c3 +9 Declarations: A B C x y o c1 c2 c3 diff --git a/pyomo/core/tests/unit/test_expression.py b/pyomo/core/tests/unit/test_expression.py index 8dca0062dd0..bd5a0aad6c9 100644 --- a/pyomo/core/tests/unit/test_expression.py +++ b/pyomo/core/tests/unit/test_expression.py @@ -742,7 +742,7 @@ def test_pprint_oldStyle(self): e : Size=1, Index=None Key : Expression None : sum(mon(1, x), 2) -E : Size=2, Index=E_index +E : Size=2, Index={1, 2} Key : Expression 1 : sum(pow(x, 2), 1) 2 : sum(pow(x, 2), 1) @@ -761,7 +761,7 @@ def test_pprint_oldStyle(self): e : Size=1, Index=None Key : Expression None : 1.0 -E : Size=2, Index=E_index +E : Size=2, Index={1, 2} Key : Expression 1 : 2.0 2 : sum(pow(x, 2), 1) @@ -780,7 +780,7 @@ def test_pprint_oldStyle(self): e : Size=1, Index=None Key : Expression None : Undefined -E : Size=2, Index=E_index +E : Size=2, Index={1, 2} Key : Expression 1 : Undefined 2 : sum(pow(x, 2), 1) @@ -806,7 +806,7 @@ def test_pprint_newStyle(self): e : Size=1, Index=None Key : Expression None : x + 2 -E : Size=2, Index=E_index +E : Size=2, Index={1, 2} Key : Expression 1 : x**2 + 1 2 : x**2 + 1 @@ -830,7 +830,7 @@ def test_pprint_newStyle(self): e : Size=1, Index=None Key : Expression None : 1.0 -E : Size=2, Index=E_index +E : Size=2, Index={1, 2} Key : Expression 1 : 2.0 2 : x**2 + 1 @@ -849,7 +849,7 @@ def test_pprint_newStyle(self): e : Size=1, Index=None Key : Expression None : Undefined -E : Size=2, Index=E_index +E : Size=2, Index={1, 2} Key : Expression 1 : Undefined 2 : x**2 + 1 diff --git a/pyomo/core/tests/unit/test_reference.py b/pyomo/core/tests/unit/test_reference.py index a7a470b1a3b..6c2e1d28053 100644 --- a/pyomo/core/tests/unit/test_reference.py +++ b/pyomo/core/tests/unit/test_reference.py @@ -729,7 +729,6 @@ def test_component_data_reference(self): self.assertIs(m.r.ctype, Var) self.assertIsNot(m.r.index_set(), m.y.index_set()) - self.assertIs(m.y.index_set(), m.y_index) self.assertIs(m.r.index_set(), UnindexedComponent_ReferenceSet) self.assertEqual(len(m.r), 1) self.assertTrue(m.r.is_reference()) @@ -773,7 +772,7 @@ def test_reference_var_pprint(self): m.r.pprint(ostream=buf) self.assertEqual( buf.getvalue(), - """r : Size=2, Index=x_index, ReferenceTo=x + """r : Size=2, Index={1, 2}, ReferenceTo=x Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : 4 : None : False : False : Reals 2 : None : 8 : None : False : False : Reals @@ -784,7 +783,7 @@ def test_reference_var_pprint(self): m.s.pprint(ostream=buf) self.assertEqual( buf.getvalue(), - """s : Size=2, Index=x_index, ReferenceTo=x[:, ...] + """s : Size=2, Index={1, 2}, ReferenceTo=x[:, ...] Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : 4 : None : False : False : Reals 2 : None : 8 : None : False : False : Reals @@ -799,7 +798,7 @@ def test_reference_indexedcomponent_pprint(self): m.r.pprint(ostream=buf) self.assertEqual( buf.getvalue(), - """r : Size=2, Index=x_index, ReferenceTo=x + """r : Size=2, Index={1, 2}, ReferenceTo=x Key : Object 1 : 2 : @@ -810,7 +809,7 @@ def test_reference_indexedcomponent_pprint(self): m.s.pprint(ostream=buf) self.assertEqual( buf.getvalue(), - """s : Size=2, Index=x_index, ReferenceTo=x[:, ...] + """s : Size=2, Index={1, 2}, ReferenceTo=x[:, ...] Key : Object 1 : 2 : @@ -1380,7 +1379,7 @@ def b(b, i): self.assertEqual( buf.getvalue().strip(), """ -r : Size=4, Index=r_index, ReferenceTo=b[:].x[:] +r : Size=4, Index=ReferenceSet(b[:].x[:]), ReferenceTo=b[:].x[:] Key : Lower : Value : Upper : Fixed : Stale : Domain (1, 3) : 1 : None : None : False : True : Reals (1, 4) : 1 : None : None : False : True : Reals diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index a1072e7156c..6c7511359a1 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -993,9 +993,7 @@ def __ge__(self, other): output = StringIO() with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): i = SetOf([1, 2, 3]) - self.assertEqual(output.getvalue(), "") - i.construct() - ref = 'Constructing SetOf, name=OrderedSetOf, from data=None\n' + ref = 'Constructing SetOf, name=[1, 2, 3], from data=None\n' self.assertEqual(output.getvalue(), ref) # Calling construct() twice bypasses construction the second # time around @@ -1811,7 +1809,7 @@ def test_check_values(self): class Test_SetOperator(unittest.TestCase): def test_construct(self): p = Param(initialize=3) - a = RangeSet(p) + a = RangeSet(p, name='a') output = StringIO() with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): i = a * a @@ -1820,12 +1818,9 @@ def test_construct(self): with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): i.construct() ref = ( - 'Constructing SetOperator, name=SetProduct_OrderedSet, ' - 'from data=None\n' - 'Constructing RangeSet, name=FiniteScalarRangeSet, ' - 'from data=None\n' - 'Constructing Set, name=SetProduct_OrderedSet, ' - 'from data=None\n' + 'Constructing SetOperator, name=a*a, from data=None\n' + 'Constructing Set, name=a*a, from data=None\n' + 'Constructing RangeSet, name=a, from data=None\n' ) self.assertEqual(output.getvalue(), ref) # Calling construct() twice bypasses construction the second @@ -1937,8 +1932,8 @@ def test_domain_and_pprint(self): m.A.pprint(ostream=output) ref = """ A : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 1 : I | A_index_0 : 4 : {1, 2, 3, 4} + Key : Dimen : Domain : Size : Members + None : 1 : I | {3, 4} : 4 : {1, 2, 3, 4} """.strip() self.assertEqual(output.getvalue().strip(), ref) @@ -2213,8 +2208,8 @@ def test_domain_and_pprint(self): m.A.pprint(ostream=output) ref = """ A : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 1 : I & A_index_0 : 0 : {} + Key : Dimen : Domain : Size : Members + None : 1 : I & {3, 4} : 0 : {} """.strip() self.assertEqual(output.getvalue().strip(), ref) @@ -2491,8 +2486,8 @@ def test_domain_and_pprint(self): m.A.pprint(ostream=output) ref = """ A : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 1 : I - A_index_0 : 2 : {1, 2} + Key : Dimen : Domain : Size : Members + None : 1 : I - {3, 4} : 2 : {1, 2} """.strip() self.assertEqual(output.getvalue().strip(), ref) @@ -2720,8 +2715,8 @@ def test_domain_and_pprint(self): m.A.pprint(ostream=output) ref = """ A : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 1 : I ^ A_index_0 : 4 : {1, 2, 3, 4} + Key : Dimen : Domain : Size : Members + None : 1 : I ^ {3, 4} : 4 : {1, 2, 3, 4} """.strip() self.assertEqual(output.getvalue().strip(), ref) @@ -2982,8 +2977,8 @@ def test_domain_and_pprint(self): m.A.pprint(ostream=output) ref = """ A : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : I*A_index_0 : 4 : {(1, 3), (1, 4), (2, 3), (2, 4)} + Key : Dimen : Domain : Size : Members + None : 2 : I*{3, 4} : 4 : {(1, 3), (1, 4), (2, 3), (2, 4)} """.strip() self.assertEqual(output.getvalue().strip(), ref) @@ -4406,17 +4401,17 @@ def test_domain(self): self.assertEqual(list(m.I), [0, 2.0, 4]) with self.assertRaisesRegex( ValueError, - 'The value is not in the domain ' r'\(Integers & I_domain_index_0_index_1', + r'The value is not in the domain \(Integers & \[0:inf:2\]\) & \[0..9\]', ): m.I.add(1.5) with self.assertRaisesRegex( ValueError, - 'The value is not in the domain ' r'\(Integers & I_domain_index_0_index_1', + r'The value is not in the domain \(Integers & \[0:inf:2\]\) & \[0..9\]', ): m.I.add(1) with self.assertRaisesRegex( ValueError, - 'The value is not in the domain ' r'\(Integers & I_domain_index_0_index_1', + r'The value is not in the domain \(Integers & \[0:inf:2\]\) & \[0..9\]', ): m.I.add(10) @@ -4454,8 +4449,8 @@ def myFcn(x): Key : Dimen : Domain : Size : Members None : 2 : Any : 2 : {(3, 4), (1, 2)} M : Size=1, Index=None, Ordered=False - Key : Dimen : Domain : Size : Members - None : 1 : Reals - M_index_1 : Inf : ([-inf..0) | (0..inf]) + Key : Dimen : Domain : Size : Members + None : 1 : Reals - [0] : Inf : ([-inf..0) | (0..inf]) N : Size=1, Index=None, Ordered=False Key : Dimen : Domain : Size : Members None : 1 : Integers - Reals : Inf : [] @@ -4465,12 +4460,7 @@ def myFcn(x): Key : Finite : Members None : True : [1:3] -1 SetOf Declarations - M_index_1 : Dimen=1, Size=1, Bounds=(0, 0) - Key : Ordered : Members - None : True : [0] - -8 Declarations: I_index I J K L M_index_1 M N""".strip(), +7 Declarations: I_index I J K L M N""".strip(), ) def test_pickle(self): @@ -4556,11 +4546,11 @@ def test_construction(self): ref = """ I : Size=0, Index=None, Ordered=Insertion Not constructed -II : Size=0, Index=II_index, Ordered=Insertion +II : Size=0, Index={1, 2, 3}, Ordered=Insertion Not constructed J : Size=0, Index=None, Ordered=Insertion Not constructed -JJ : Size=0, Index=JJ_index, Ordered=Insertion +JJ : Size=0, Index={1, 2, 3}, Ordered=Insertion Not constructed""".strip() self.assertEqual(output.getvalue().strip(), ref) @@ -4827,7 +4817,7 @@ def _i_init(m, i): output = StringIO() m.I.pprint(ostream=output) ref = """ -I : Size=2, Index=I_index, Ordered=Insertion +I : Size=2, Index={1, 2, 3, 4, 5}, Ordered=Insertion Key : Dimen : Domain : Size : Members 2 : 1 : Any : 2 : {0, 1} 4 : 1 : Any : 4 : {0, 1, 2, 3} @@ -6301,14 +6291,11 @@ def objective_rule(model_arg): output = StringIO() m.pprint(ostream=output) ref = """ -3 Set Declarations +2 Set Declarations arc_keys : Set of arcs Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : arc_keys_domain : 2 : {(0, 0), (0, 1)} - arc_keys_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : 2 : node_keys*node_keys : 4 : {(0, 0), (0, 1), (1, 0), (1, 1)} + None : 2 : node_keys*node_keys : 2 : {(0, 0), (0, 1)} node_keys : Set of nodes Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members @@ -6325,7 +6312,7 @@ def objective_rule(model_arg): Key : Active : Sense : Expression None : True : minimize : arc_variables[0,0] + arc_variables[0,1] -5 Declarations: node_keys arc_keys_domain arc_keys arc_variables obj +4 Declarations: node_keys arc_keys arc_variables obj """.strip() self.assertEqual(output.getvalue().strip(), ref) @@ -6334,18 +6321,15 @@ def objective_rule(model_arg): output = StringIO() m.pprint(ostream=output) ref = """ -3 Set Declarations +2 Set Declarations arc_keys : Set of arcs Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : None : arc_keys_domain : 2 : {ArcKey(node_from=NodeKey(id=0), node_to=NodeKey(id=0)), ArcKey(node_from=NodeKey(id=0), node_to=NodeKey(id=1))} - arc_keys_domain : Size=1, Index=None, Ordered=True Key : Dimen : Domain : Size : Members - None : None : node_keys*node_keys : 4 : {(NodeKey(id=0), NodeKey(id=0)), (NodeKey(id=0), NodeKey(id=1)), (NodeKey(id=1), NodeKey(id=0)), (NodeKey(id=1), NodeKey(id=1))} + None : 2 : node_keys*node_keys : 2 : {ArcKey(node_from=NodeKey(id=0), node_to=NodeKey(id=0)), ArcKey(node_from=NodeKey(id=0), node_to=NodeKey(id=1))} node_keys : Set of nodes Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members - None : None : Any : 2 : {NodeKey(id=0), NodeKey(id=1)} + None : 1 : Any : 2 : {NodeKey(id=0), NodeKey(id=1)} 1 Var Declarations arc_variables : Size=2, Index=arc_keys @@ -6358,7 +6342,7 @@ def objective_rule(model_arg): Key : Active : Sense : Expression None : True : minimize : arc_variables[ArcKey(node_from=NodeKey(id=0), node_to=NodeKey(id=0))] + arc_variables[ArcKey(node_from=NodeKey(id=0), node_to=NodeKey(id=1))] -5 Declarations: node_keys arc_keys_domain arc_keys arc_variables obj +4 Declarations: node_keys arc_keys arc_variables obj """.strip() self.assertEqual(output.getvalue().strip(), ref) diff --git a/pyomo/core/tests/unit/test_visitor.py b/pyomo/core/tests/unit/test_visitor.py index 086c57aa560..5625b63f272 100644 --- a/pyomo/core/tests/unit/test_visitor.py +++ b/pyomo/core/tests/unit/test_visitor.py @@ -405,7 +405,6 @@ def test_replacement_walker0(self): ) del M.w - del M.w_index M.w = VarList() e = 2 * sum_product(M.z, M.x) walker = ReplacementWalkerTest1(M) diff --git a/pyomo/core/tests/unit/varpprint.txt b/pyomo/core/tests/unit/varpprint.txt index bd49b881417..a8c33c6b007 100644 --- a/pyomo/core/tests/unit/varpprint.txt +++ b/pyomo/core/tests/unit/varpprint.txt @@ -1,13 +1,7 @@ -3 Set Declarations +1 Set Declarations a : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} - cl_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 10 : {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} - o3_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : a*a : 9 : {(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)} 2 Param Declarations A : Size=1, Index=None, Domain=Any, Default=-1, Mutable=True @@ -37,7 +31,7 @@ 1 : True : minimize : b[1] 2 : True : minimize : b[2] 3 : True : minimize : b[3] - o3 : Size=0, Index=o3_index, Active=True + o3 : Size=0, Index=a*a, Active=True Key : Active : Sense : Expression 19 Constraint Declarations @@ -97,7 +91,7 @@ c9b : Size=1, Index=None, Active=True Key : Lower : Body : Upper : Active None : -Inf : c : A + A : True - cl : Size=10, Index=cl_index, Active=True + cl : Size=10, Index={1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, Active=True Key : Lower : Body : Upper : Active 1 : -Inf : d - c : 0.0 : True 2 : -Inf : d - 2*c : 0.0 : True @@ -110,4 +104,4 @@ 9 : -Inf : d - 9*c : 0.0 : True 10 : -Inf : d - 10*c : 0.0 : True -30 Declarations: a b c d e A B o2 o3_index o3 c1 c2 c3 c4 c5 c6a c7a c7b c8 c9a c9b c10a c11 c15a c16a c12 c13a c14a cl_index cl +28 Declarations: a b c d e A B o2 o3 c1 c2 c3 c4 c5 c6a c7a c7b c8 c9a c9b c10a c11 c15a c16a c12 c13a c14a cl diff --git a/pyomo/dae/tests/test_diffvar.py b/pyomo/dae/tests/test_diffvar.py index 718781d5916..7ac54445f5c 100644 --- a/pyomo/dae/tests/test_diffvar.py +++ b/pyomo/dae/tests/test_diffvar.py @@ -69,7 +69,6 @@ def test_valid(self): del m.dv del m.dv2 del m.v - del m.v_index m.v = Var(m.x, m.t) m.dv = DerivativeVar(m.v, wrt=m.x) diff --git a/pyomo/mpec/tests/cov2_None.txt b/pyomo/mpec/tests/cov2_None.txt index 2f7d59572a8..c3c0baeeb9e 100644 --- a/pyomo/mpec/tests/cov2_None.txt +++ b/pyomo/mpec/tests/cov2_None.txt @@ -1,2 +1,2 @@ -cc : Size=0, Index=cc_index, Active=True +cc : Size=0, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active diff --git a/pyomo/mpec/tests/cov2_mpec.nl.txt b/pyomo/mpec/tests/cov2_mpec.nl.txt index a526784344b..9b7b9ed53f4 100644 --- a/pyomo/mpec/tests/cov2_mpec.nl.txt +++ b/pyomo/mpec/tests/cov2_mpec.nl.txt @@ -1,8 +1,3 @@ -1 Set Declarations - cc_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {0, 1, 2} - 4 Var Declarations x1 : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain @@ -23,7 +18,7 @@ None : 0.5 : x1 : 0.5 : True 1 Block Declarations - cc : Size=0, Index=cc_index, Active=True + cc : Size=0, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active -7 Declarations: y x1 x2 x3 cc_index cc keep_var_con +6 Declarations: y x1 x2 x3 cc keep_var_con diff --git a/pyomo/mpec/tests/cov2_mpec.simple_disjunction.txt b/pyomo/mpec/tests/cov2_mpec.simple_disjunction.txt index 2f7d59572a8..c3c0baeeb9e 100644 --- a/pyomo/mpec/tests/cov2_mpec.simple_disjunction.txt +++ b/pyomo/mpec/tests/cov2_mpec.simple_disjunction.txt @@ -1,2 +1,2 @@ -cc : Size=0, Index=cc_index, Active=True +cc : Size=0, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active diff --git a/pyomo/mpec/tests/cov2_mpec.simple_nonlinear.txt b/pyomo/mpec/tests/cov2_mpec.simple_nonlinear.txt index 2f7d59572a8..c3c0baeeb9e 100644 --- a/pyomo/mpec/tests/cov2_mpec.simple_nonlinear.txt +++ b/pyomo/mpec/tests/cov2_mpec.simple_nonlinear.txt @@ -1,2 +1,2 @@ -cc : Size=0, Index=cc_index, Active=True +cc : Size=0, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active diff --git a/pyomo/mpec/tests/cov2_mpec.standard_form.txt b/pyomo/mpec/tests/cov2_mpec.standard_form.txt index 2f7d59572a8..c3c0baeeb9e 100644 --- a/pyomo/mpec/tests/cov2_mpec.standard_form.txt +++ b/pyomo/mpec/tests/cov2_mpec.standard_form.txt @@ -1,2 +1,2 @@ -cc : Size=0, Index=cc_index, Active=True +cc : Size=0, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active diff --git a/pyomo/mpec/tests/list1_None.txt b/pyomo/mpec/tests/list1_None.txt index 8e849242bcd..34c358a1521 100644 --- a/pyomo/mpec/tests/list1_None.txt +++ b/pyomo/mpec/tests/list1_None.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={1, 2}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/mpec/tests/list1_mpec.nl.txt b/pyomo/mpec/tests/list1_mpec.nl.txt index 16310c59317..62edc488b47 100644 --- a/pyomo/mpec/tests/list1_mpec.nl.txt +++ b/pyomo/mpec/tests/list1_mpec.nl.txt @@ -1,8 +1,3 @@ -1 Set Declarations - cc_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - 4 Var Declarations x1 : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain @@ -18,7 +13,7 @@ None : None : None : None : False : True : Reals 1 Block Declarations - cc : Size=2, Index=cc_index, Active=True + cc : Size=2, Index={1, 2}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True @@ -37,4 +32,4 @@ 1 Declarations: c -6 Declarations: y x1 x2 x3 cc_index cc +5 Declarations: y x1 x2 x3 cc diff --git a/pyomo/mpec/tests/list1_mpec.simple_disjunction.txt b/pyomo/mpec/tests/list1_mpec.simple_disjunction.txt index 816e56af56c..c2bfe5e0399 100644 --- a/pyomo/mpec/tests/list1_mpec.simple_disjunction.txt +++ b/pyomo/mpec/tests/list1_mpec.simple_disjunction.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={1, 2}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/mpec/tests/list1_mpec.simple_nonlinear.txt b/pyomo/mpec/tests/list1_mpec.simple_nonlinear.txt index 816e56af56c..c2bfe5e0399 100644 --- a/pyomo/mpec/tests/list1_mpec.simple_nonlinear.txt +++ b/pyomo/mpec/tests/list1_mpec.simple_nonlinear.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={1, 2}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/mpec/tests/list1_mpec.standard_form.txt b/pyomo/mpec/tests/list1_mpec.standard_form.txt index 816e56af56c..c2bfe5e0399 100644 --- a/pyomo/mpec/tests/list1_mpec.standard_form.txt +++ b/pyomo/mpec/tests/list1_mpec.standard_form.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={1, 2}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/mpec/tests/list2_None.txt b/pyomo/mpec/tests/list2_None.txt index cc84321fe3e..465bc347766 100644 --- a/pyomo/mpec/tests/list2_None.txt +++ b/pyomo/mpec/tests/list2_None.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/list2_mpec.nl.txt b/pyomo/mpec/tests/list2_mpec.nl.txt index c8c461e08e8..6dc49cef8dd 100644 --- a/pyomo/mpec/tests/list2_mpec.nl.txt +++ b/pyomo/mpec/tests/list2_mpec.nl.txt @@ -1,8 +1,3 @@ -1 Set Declarations - cc_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - 4 Var Declarations x1 : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain @@ -18,7 +13,7 @@ None : None : None : None : False : True : Reals 1 Block Declarations - cc : Size=3, Index=cc_index, Active=True + cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : False @@ -40,4 +35,4 @@ 1 Declarations: c -6 Declarations: y x1 x2 x3 cc_index cc +5 Declarations: y x1 x2 x3 cc diff --git a/pyomo/mpec/tests/list2_mpec.simple_disjunction.txt b/pyomo/mpec/tests/list2_mpec.simple_disjunction.txt index 82688e8f017..c71d6461d22 100644 --- a/pyomo/mpec/tests/list2_mpec.simple_disjunction.txt +++ b/pyomo/mpec/tests/list2_mpec.simple_disjunction.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/list2_mpec.simple_nonlinear.txt b/pyomo/mpec/tests/list2_mpec.simple_nonlinear.txt index 82688e8f017..c71d6461d22 100644 --- a/pyomo/mpec/tests/list2_mpec.simple_nonlinear.txt +++ b/pyomo/mpec/tests/list2_mpec.simple_nonlinear.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/list2_mpec.standard_form.txt b/pyomo/mpec/tests/list2_mpec.standard_form.txt index 82688e8f017..c71d6461d22 100644 --- a/pyomo/mpec/tests/list2_mpec.standard_form.txt +++ b/pyomo/mpec/tests/list2_mpec.standard_form.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/list5_None.txt b/pyomo/mpec/tests/list5_None.txt index 8e6ed9a8164..962ee6cbc3a 100644 --- a/pyomo/mpec/tests/list5_None.txt +++ b/pyomo/mpec/tests/list5_None.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : True diff --git a/pyomo/mpec/tests/list5_mpec.nl.txt b/pyomo/mpec/tests/list5_mpec.nl.txt index adb64af0457..93ee89f3389 100644 --- a/pyomo/mpec/tests/list5_mpec.nl.txt +++ b/pyomo/mpec/tests/list5_mpec.nl.txt @@ -1,8 +1,3 @@ -1 Set Declarations - cc_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - 4 Var Declarations x1 : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain @@ -18,7 +13,7 @@ None : None : None : None : False : True : Reals 1 Block Declarations - cc : Size=3, Index=cc_index, Active=True + cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : True @@ -45,4 +40,4 @@ 1 Declarations: c -6 Declarations: y x1 x2 x3 cc_index cc +5 Declarations: y x1 x2 x3 cc diff --git a/pyomo/mpec/tests/list5_mpec.simple_disjunction.txt b/pyomo/mpec/tests/list5_mpec.simple_disjunction.txt index 69178523d96..15622fa84e1 100644 --- a/pyomo/mpec/tests/list5_mpec.simple_disjunction.txt +++ b/pyomo/mpec/tests/list5_mpec.simple_disjunction.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : True diff --git a/pyomo/mpec/tests/list5_mpec.simple_nonlinear.txt b/pyomo/mpec/tests/list5_mpec.simple_nonlinear.txt index 69178523d96..15622fa84e1 100644 --- a/pyomo/mpec/tests/list5_mpec.simple_nonlinear.txt +++ b/pyomo/mpec/tests/list5_mpec.simple_nonlinear.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : True diff --git a/pyomo/mpec/tests/list5_mpec.standard_form.txt b/pyomo/mpec/tests/list5_mpec.standard_form.txt index 69178523d96..15622fa84e1 100644 --- a/pyomo/mpec/tests/list5_mpec.standard_form.txt +++ b/pyomo/mpec/tests/list5_mpec.standard_form.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={1, 2, 3}, Active=True Key : Arg0 : Arg1 : Active 1 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 1 : True diff --git a/pyomo/mpec/tests/t10_None.txt b/pyomo/mpec/tests/t10_None.txt index afc38166ab3..7d6b4c429cc 100644 --- a/pyomo/mpec/tests/t10_None.txt +++ b/pyomo/mpec/tests/t10_None.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 1 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/t10_mpec.nl.txt b/pyomo/mpec/tests/t10_mpec.nl.txt index a4a16713eaa..12db893ddba 100644 --- a/pyomo/mpec/tests/t10_mpec.nl.txt +++ b/pyomo/mpec/tests/t10_mpec.nl.txt @@ -1,8 +1,3 @@ -1 Set Declarations - cc_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {0, 1, 2} - 4 Var Declarations x1 : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain @@ -18,7 +13,7 @@ None : None : None : None : False : True : Reals 1 Block Declarations - cc : Size=3, Index=cc_index, Active=True + cc : Size=3, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 1 : y + x3 : x1 + 2*x2 == 1 : False @@ -40,4 +35,4 @@ 1 Declarations: c -6 Declarations: y x1 x2 x3 cc_index cc +5 Declarations: y x1 x2 x3 cc diff --git a/pyomo/mpec/tests/t10_mpec.simple_disjunction.txt b/pyomo/mpec/tests/t10_mpec.simple_disjunction.txt index c53c1b8e62b..37aaaafcf68 100644 --- a/pyomo/mpec/tests/t10_mpec.simple_disjunction.txt +++ b/pyomo/mpec/tests/t10_mpec.simple_disjunction.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 1 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/t10_mpec.simple_nonlinear.txt b/pyomo/mpec/tests/t10_mpec.simple_nonlinear.txt index c53c1b8e62b..37aaaafcf68 100644 --- a/pyomo/mpec/tests/t10_mpec.simple_nonlinear.txt +++ b/pyomo/mpec/tests/t10_mpec.simple_nonlinear.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 1 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/t10_mpec.standard_form.txt b/pyomo/mpec/tests/t10_mpec.standard_form.txt index c53c1b8e62b..37aaaafcf68 100644 --- a/pyomo/mpec/tests/t10_mpec.standard_form.txt +++ b/pyomo/mpec/tests/t10_mpec.standard_form.txt @@ -1,4 +1,4 @@ -cc : Size=3, Index=cc_index, Active=True +cc : Size=3, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 1 : y + x3 : x1 + 2*x2 == 1 : False diff --git a/pyomo/mpec/tests/t13_None.txt b/pyomo/mpec/tests/t13_None.txt index b2e24eb1166..fde3cc15a18 100644 --- a/pyomo/mpec/tests/t13_None.txt +++ b/pyomo/mpec/tests/t13_None.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/mpec/tests/t13_mpec.nl.txt b/pyomo/mpec/tests/t13_mpec.nl.txt index dc47767efb7..9e709e35b6f 100644 --- a/pyomo/mpec/tests/t13_mpec.nl.txt +++ b/pyomo/mpec/tests/t13_mpec.nl.txt @@ -1,8 +1,3 @@ -1 Set Declarations - cc_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {0, 1, 2} - 4 Var Declarations x1 : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain @@ -18,7 +13,7 @@ None : None : None : None : False : True : Reals 1 Block Declarations - cc : Size=2, Index=cc_index, Active=True + cc : Size=2, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True @@ -37,4 +32,4 @@ 1 Declarations: c -6 Declarations: y x1 x2 x3 cc_index cc +5 Declarations: y x1 x2 x3 cc diff --git a/pyomo/mpec/tests/t13_mpec.simple_disjunction.txt b/pyomo/mpec/tests/t13_mpec.simple_disjunction.txt index 1ff09babad8..9b361c7e503 100644 --- a/pyomo/mpec/tests/t13_mpec.simple_disjunction.txt +++ b/pyomo/mpec/tests/t13_mpec.simple_disjunction.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/mpec/tests/t13_mpec.simple_nonlinear.txt b/pyomo/mpec/tests/t13_mpec.simple_nonlinear.txt index 1ff09babad8..9b361c7e503 100644 --- a/pyomo/mpec/tests/t13_mpec.simple_nonlinear.txt +++ b/pyomo/mpec/tests/t13_mpec.simple_nonlinear.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/mpec/tests/t13_mpec.standard_form.txt b/pyomo/mpec/tests/t13_mpec.standard_form.txt index 1ff09babad8..9b361c7e503 100644 --- a/pyomo/mpec/tests/t13_mpec.standard_form.txt +++ b/pyomo/mpec/tests/t13_mpec.standard_form.txt @@ -1,4 +1,4 @@ -cc : Size=2, Index=cc_index, Active=True +cc : Size=2, Index={0, 1, 2}, Active=True Key : Arg0 : Arg1 : Active 0 : y + x3 : x1 + 2*x2 == 0 : True 2 : y + x3 : x1 + 2*x2 == 2 : True diff --git a/pyomo/network/tests/test_arc.py b/pyomo/network/tests/test_arc.py index cd340cace7a..3ea1aeeb380 100644 --- a/pyomo/network/tests/test_arc.py +++ b/pyomo/network/tests/test_arc.py @@ -504,11 +504,11 @@ def test_expand_indexed(self): os.getvalue(), """c_expanded : Size=1, Index=None, Active=True 3 Constraint Declarations - a_equality : Size=2, Index=x_index, Active=True + a_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - t[1] : 0.0 : True 2 : 0.0 : x[2] - t[2] : 0.0 : True - b_equality : Size=4, Index=y_index, Active=True + b_equality : Size=4, Index={1, 2}*{1, 2}, Active=True Key : Lower : Body : Upper : Active (1, 1) : 0.0 : y[1,1] - u[1,1] : 0.0 : True (1, 2) : 0.0 : y[1,2] - u[1,2] : 0.0 : True @@ -677,7 +677,7 @@ def test_expand_empty_indexed(self): os.getvalue(), """c_expanded : Size=1, Index=None, Active=True 2 Constraint Declarations - x_equality : Size=2, Index=x_index, Active=True + x_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - EPRT_auto_x[1] : 0.0 : True 2 : 0.0 : x[2] - EPRT_auto_x[2] : 0.0 : True @@ -739,7 +739,7 @@ def test_expand_multiple_empty_indexed(self): os.getvalue(), """c_expanded : Size=1, Index=None, Active=True 2 Constraint Declarations - x_equality : Size=2, Index=x_index, Active=True + x_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - EPRT1_auto_x[1] : 0.0 : True 2 : 0.0 : x[2] - EPRT1_auto_x[2] : 0.0 : True @@ -757,7 +757,7 @@ def test_expand_multiple_empty_indexed(self): os.getvalue(), """d_expanded : Size=1, Index=None, Active=True 2 Constraint Declarations - x_equality : Size=2, Index=x_index, Active=True + x_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : EPRT2_auto_x[1] - EPRT1_auto_x[1] : 0.0 : True 2 : 0.0 : EPRT2_auto_x[2] - EPRT1_auto_x[2] : 0.0 : True @@ -812,7 +812,7 @@ def test_expand_multiple_indexed(self): os.getvalue(), """c_expanded : Size=1, Index=None, Active=True 2 Constraint Declarations - x_equality : Size=2, Index=x_index, Active=True + x_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : x[1] - a1[1] : 0.0 : True 2 : 0.0 : x[2] - a1[2] : 0.0 : True @@ -830,7 +830,7 @@ def test_expand_multiple_indexed(self): os.getvalue(), """d_expanded : Size=1, Index=None, Active=True 2 Constraint Declarations - x_equality : Size=2, Index=x_index, Active=True + x_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : a2[1] - a1[1] : 0.0 : True 2 : 0.0 : a2[2] - a1[2] : 0.0 : True @@ -903,7 +903,7 @@ def test_expand_implicit_indexed(self): os.getvalue(), """c_expanded : Size=1, Index=None, Active=True 2 Constraint Declarations - x_equality : Size=2, Index=a2_index, Active=True + x_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : a2[1] - x[1] : 0.0 : True 2 : 0.0 : a2[2] - x[2] : 0.0 : True @@ -921,7 +921,7 @@ def test_expand_implicit_indexed(self): os.getvalue(), """d_expanded : Size=1, Index=None, Active=True 2 Constraint Declarations - x_equality : Size=2, Index=a2_index, Active=True + x_equality : Size=2, Index={1, 2}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : EPRT2_auto_x[1] - x[1] : 0.0 : True 2 : 0.0 : EPRT2_auto_x[2] - x[2] : 0.0 : True @@ -964,7 +964,7 @@ def rule(m, i): m.component('eq_expanded').pprint(ostream=os) self.assertEqual( os.getvalue(), - """eq_expanded : Size=2, Index=eq_index, Active=True + """eq_expanded : Size=2, Index={1, 2}, Active=True eq_expanded[1] : Active=True 1 Constraint Declarations v_equality : Size=1, Index=None, Active=True From 687f754c1073e943b7d7280ad653a2d81328f8f5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 21 Dec 2023 16:03:49 -0700 Subject: [PATCH 0272/3044] Fixing some bugs with SequenceVars in the writer and solution parsing --- pyomo/contrib/cp/repn/docplex_writer.py | 8 +++++- pyomo/contrib/cp/tests/test_docplex_writer.py | 27 +++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 002039b46dd..b1b8708c757 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -31,6 +31,7 @@ IndexedIntervalVar, ) from pyomo.contrib.cp.sequence_var import ( + SequenceVar, ScalarSequenceVar, IndexedSequenceVar, _SequenceVarData, @@ -1157,7 +1158,8 @@ def write(self, model, **options): RangeSet, Port, }, - targets={Objective, Constraint, LogicalConstraint, IntervalVar}, + targets={Objective, Constraint, LogicalConstraint, IntervalVar, + SequenceVar}, ) if unknown: raise ValueError( @@ -1386,6 +1388,10 @@ def solve(self, model, **kwds): ) else: sol = sol.get_value() + if py_var.ctype is SequenceVar: + # They don't actually have values--the IntervalVars will get + # set. + continue if py_var.ctype is IntervalVar: if len(sol) == 0: # The interval_var is absent diff --git a/pyomo/contrib/cp/tests/test_docplex_writer.py b/pyomo/contrib/cp/tests/test_docplex_writer.py index b563052ef3a..a3326b19cf4 100644 --- a/pyomo/contrib/cp/tests/test_docplex_writer.py +++ b/pyomo/contrib/cp/tests/test_docplex_writer.py @@ -12,7 +12,10 @@ import pyomo.common.unittest as unittest from pyomo.common.fileutils import Executable -from pyomo.contrib.cp import IntervalVar, Pulse, Step, AlwaysIn +from pyomo.contrib.cp import ( + IntervalVar, SequenceVar, Pulse, Step, AlwaysIn, + first_in_sequence, predecessor_to, no_overlap +) from pyomo.contrib.cp.repn.docplex_writer import LogicalToDoCplex from pyomo.environ import ( all_different, @@ -360,7 +363,6 @@ def test_matching_problem(self): results.solver.termination_condition, TerminationCondition.optimal ) self.assertEqual(value(m.obj), perfect) - m.person_name.pprint() self.assertEqual(value(m.person_name['P1']), 0) self.assertEqual(value(m.person_name['P2']), 1) self.assertEqual(value(m.person_name['P3']), 2) @@ -392,3 +394,24 @@ def test_matching_problem(self): results.solver.termination_condition, TerminationCondition.optimal ) self.assertEqual(value(m.obj), perfect) + + def test_scheduling_with_sequence_vars(self): + m = ConcreteModel() + m.Steps = Set(initialize=[1, 2, 3]) + def length_rule(m, j): + return 2*j + m.i = IntervalVar(m.Steps, start=(0, 12), end=(0, 12), length=length_rule) + m.seq = SequenceVar(expr=[m.i[j] for j in m.Steps]) + m.first = LogicalConstraint(expr=first_in_sequence(m.i[1], m.seq)) + m.seq_order1 = LogicalConstraint(expr=predecessor_to(m.i[1], m.i[2], m.seq)) + m.seq_order2 = LogicalConstraint(expr=predecessor_to(m.i[2], m.i[3], m.seq)) + m.no_ovlerpa = LogicalConstraint(expr=no_overlap(m.seq)) + + results = SolverFactory('cp_optimizer').solve(m) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.feasible + ) + self.assertEqual(value(m.i[1].start_time), 0) + self.assertEqual(value(m.i[2].start_time), 2) + self.assertEqual(value(m.i[3].start_time), 6) + From ff23a4d7db83adbf78029095522a4bbb0c3f0a07 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 21 Dec 2023 16:04:30 -0700 Subject: [PATCH 0273/3044] NFC: black --- pyomo/contrib/cp/repn/docplex_writer.py | 9 +++++++-- pyomo/contrib/cp/tests/test_docplex_writer.py | 15 +++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index b1b8708c757..27e2f7ef8b9 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -1158,8 +1158,13 @@ def write(self, model, **options): RangeSet, Port, }, - targets={Objective, Constraint, LogicalConstraint, IntervalVar, - SequenceVar}, + targets={ + Objective, + Constraint, + LogicalConstraint, + IntervalVar, + SequenceVar, + }, ) if unknown: raise ValueError( diff --git a/pyomo/contrib/cp/tests/test_docplex_writer.py b/pyomo/contrib/cp/tests/test_docplex_writer.py index a3326b19cf4..20511a8aa3d 100644 --- a/pyomo/contrib/cp/tests/test_docplex_writer.py +++ b/pyomo/contrib/cp/tests/test_docplex_writer.py @@ -13,8 +13,14 @@ from pyomo.common.fileutils import Executable from pyomo.contrib.cp import ( - IntervalVar, SequenceVar, Pulse, Step, AlwaysIn, - first_in_sequence, predecessor_to, no_overlap + IntervalVar, + SequenceVar, + Pulse, + Step, + AlwaysIn, + first_in_sequence, + predecessor_to, + no_overlap, ) from pyomo.contrib.cp.repn.docplex_writer import LogicalToDoCplex from pyomo.environ import ( @@ -398,8 +404,10 @@ def test_matching_problem(self): def test_scheduling_with_sequence_vars(self): m = ConcreteModel() m.Steps = Set(initialize=[1, 2, 3]) + def length_rule(m, j): - return 2*j + return 2 * j + m.i = IntervalVar(m.Steps, start=(0, 12), end=(0, 12), length=length_rule) m.seq = SequenceVar(expr=[m.i[j] for j in m.Steps]) m.first = LogicalConstraint(expr=first_in_sequence(m.i[1], m.seq)) @@ -414,4 +422,3 @@ def length_rule(m, j): self.assertEqual(value(m.i[1].start_time), 0) self.assertEqual(value(m.i[2].start_time), 2) self.assertEqual(value(m.i[3].start_time), 6) - From 14b1c5defa98b55f34425aacff48bf82f82b03a8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 22 Dec 2023 11:32:44 -0700 Subject: [PATCH 0274/3044] Fixing a bug with printing precedence expressions with Param-valued delays. --- .../cp/scheduling_expr/precedence_expressions.py | 10 +++++----- .../cp/tests/test_precedence_constraints.py | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py b/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py index 5340583a216..1b7693605c9 100644 --- a/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py @@ -21,13 +21,13 @@ def delay(self): return self._args_[2] def _to_string_impl(self, values, relation): - delay = int(values[2]) - if delay == 0: + delay = values[2] + if delay == '0': first = values[0] - elif delay > 0: - first = "%s + %s" % (values[0], delay) + elif delay[0] in '-+': + first = "%s %s %s" % (values[0], delay[0], delay[1:]) else: - first = "%s - %s" % (values[0], abs(delay)) + first = "%s + %s" % (values[0], delay) return "%s %s %s" % (first, relation, values[1]) diff --git a/pyomo/contrib/cp/tests/test_precedence_constraints.py b/pyomo/contrib/cp/tests/test_precedence_constraints.py index 461dabf564c..471b5bca512 100644 --- a/pyomo/contrib/cp/tests/test_precedence_constraints.py +++ b/pyomo/contrib/cp/tests/test_precedence_constraints.py @@ -15,7 +15,7 @@ BeforeExpression, AtExpression, ) -from pyomo.environ import ConcreteModel, LogicalConstraint +from pyomo.environ import ConcreteModel, LogicalConstraint, Param class TestPrecedenceRelationships(unittest.TestCase): @@ -173,3 +173,17 @@ def test_end_after_end(self): self.assertEqual(m.c.expr.delay, 0) self.assertEqual(str(m.c.expr), "b.end_time <= a.end_time") + + def test_end_before_start_param_delay(self): + m = self.get_model() + m.PrepTime = Param(initialize=5) + m.c = LogicalConstraint(expr=m.a.end_time.before(m.b.start_time, + delay=m.PrepTime)) + self.assertIsInstance(m.c.expr, BeforeExpression) + self.assertEqual(len(m.c.expr.args), 3) + self.assertIs(m.c.expr.args[0], m.a.end_time) + self.assertIs(m.c.expr.args[1], m.b.start_time) + self.assertIs(m.c.expr.delay, m.PrepTime) + + self.assertEqual(str(m.c.expr), "a.end_time + PrepTime <= b.start_time") + From 171f2db4fd703b888dd447abbe60a25d7fba0774 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 22 Dec 2023 13:08:11 -0700 Subject: [PATCH 0275/3044] Fixing a bug with single-step-function cumulative functions in alwaysin --- pyomo/contrib/cp/repn/docplex_writer.py | 14 ++++++----- pyomo/contrib/cp/tests/test_docplex_walker.py | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 27e2f7ef8b9..2bfc96faa7d 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -621,22 +621,22 @@ def _before_interval_var_presence(visitor, child): def _handle_step_at_node(visitor, node): - return cp.step_at(node._time, node._height) + return False, (_GENERAL, cp.step_at(node._time, node._height)) def _handle_step_at_start_node(visitor, node): cpx_var = _get_docplex_interval_var(visitor, node._time) - return cp.step_at_start(cpx_var, node._height) + return False, (_GENERAL, cp.step_at_start(cpx_var, node._height)) def _handle_step_at_end_node(visitor, node): cpx_var = _get_docplex_interval_var(visitor, node._time) - return cp.step_at_end(cpx_var, node._height) + return False, (_GENERAL, cp.step_at_end(cpx_var, node._height)) def _handle_pulse_node(visitor, node): cpx_var = _get_docplex_interval_var(visitor, node._interval_var) - return cp.pulse(cpx_var, node._height) + return False, (_GENERAL, cp.pulse(cpx_var, node._height)) def _handle_negated_step_function_node(visitor, node): @@ -647,9 +647,9 @@ def _handle_cumulative_function(visitor, node): expr = 0 for arg in node.args: if arg.__class__ is NegatedStepFunction: - expr -= _handle_negated_step_function_node(visitor, arg) + expr -= _handle_negated_step_function_node(visitor, arg)[1][1] else: - expr += _step_function_handles[arg.__class__](visitor, arg) + expr += _step_function_handles[arg.__class__](visitor, arg)[1][1] return False, (_GENERAL, expr) @@ -1223,6 +1223,8 @@ def write(self, model, **options): # Write logical constraints for cons in components[LogicalConstraint]: + print(cons) + print(cons.expr) expr = visitor.walk_expression((cons.expr, cons, 0)) if expr[0] is _ELEMENT_CONSTRAINT: # Make the expression into a docplex-approved boolean-valued diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 560142ff410..dcc86033cc1 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -1358,6 +1358,29 @@ def test_always_in(self): ) ) + def test_always_in_single_pulse(self): + # This is a bit silly as you can tell whether or not it is feasible + # structurally, but there's not reason it couldn't happen. + m = self.get_model() + f = Pulse((m.i, 3)) + m.c = LogicalConstraint(expr=f.within((0, 3), (0, 10))) + visitor = self.get_visitor() + expr = visitor.walk_expression((m.c.expr, m.c, 0)) + + self.assertIn(id(m.i), visitor.var_map) + + i = visitor.var_map[id(m.i)] + + self.assertTrue( + expr[1].equals( + cp.always_in( + cp.pulse(i, 3), + interval=(0, 10), + min=0, + max=3, + ) + ) + ) @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_NamedExpressions(CommonTest): From a75c9c6db7dbf9ccacd51c6e3058e0fce2b50310 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 22 Dec 2023 13:11:11 -0700 Subject: [PATCH 0276/3044] Removing debugging --- pyomo/contrib/cp/repn/docplex_writer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 2bfc96faa7d..de71e4e98dd 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -1223,8 +1223,6 @@ def write(self, model, **options): # Write logical constraints for cons in components[LogicalConstraint]: - print(cons) - print(cons.expr) expr = visitor.walk_expression((cons.expr, cons, 0)) if expr[0] is _ELEMENT_CONSTRAINT: # Make the expression into a docplex-approved boolean-valued From b5db4422bf4959e1628ec98817dd30b377ec171d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 22 Dec 2023 15:19:27 -0700 Subject: [PATCH 0277/3044] Removing a *very* old (Pyomo 4.0) deprecation message that IntervalVars hit for convoluted reasons--but basically because they have no kwd args named 'rule' --- pyomo/core/base/block.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index fd5322ba686..ba23a6af654 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -1112,26 +1112,13 @@ def add_component(self, name, val): # Error, for disabled support implicit rule names # if '_rule' in val.__dict__ and val._rule is None: - _found = False try: _test = val.local_name + '_rule' for i in (1, 2): frame = sys._getframe(i) - _found |= _test in frame.f_locals except: pass - if _found: - # JDS: Do not blindly reformat this message. The - # formatter inserts arbitrarily-long names(), which can - # cause the resulting logged message to be very poorly - # formatted due to long lines. - logger.warning( - """As of Pyomo 4.0, Pyomo components no longer support implicit rules. -You defined a component (%s) that appears -to rely on an implicit rule (%s). -Components must now specify their rules explicitly using 'rule=' keywords.""" - % (val.name, _test) - ) + # # Don't reconstruct if this component has already been constructed. # This allows a user to move a component from one block to From f9e62781d6a5afb4edeb75b0a9191f843eeb0fc8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 22 Dec 2023 15:22:17 -0700 Subject: [PATCH 0278/3044] NFC: Would you believe that black doesn't approve --- pyomo/contrib/cp/tests/test_docplex_walker.py | 10 ++-------- pyomo/contrib/cp/tests/test_precedence_constraints.py | 6 +++--- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index dcc86033cc1..0b2057217c0 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -1372,16 +1372,10 @@ def test_always_in_single_pulse(self): i = visitor.var_map[id(m.i)] self.assertTrue( - expr[1].equals( - cp.always_in( - cp.pulse(i, 3), - interval=(0, 10), - min=0, - max=3, - ) - ) + expr[1].equals(cp.always_in(cp.pulse(i, 3), interval=(0, 10), min=0, max=3)) ) + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_NamedExpressions(CommonTest): def test_named_expression(self): diff --git a/pyomo/contrib/cp/tests/test_precedence_constraints.py b/pyomo/contrib/cp/tests/test_precedence_constraints.py index 471b5bca512..b4b9b8fee40 100644 --- a/pyomo/contrib/cp/tests/test_precedence_constraints.py +++ b/pyomo/contrib/cp/tests/test_precedence_constraints.py @@ -177,8 +177,9 @@ def test_end_after_end(self): def test_end_before_start_param_delay(self): m = self.get_model() m.PrepTime = Param(initialize=5) - m.c = LogicalConstraint(expr=m.a.end_time.before(m.b.start_time, - delay=m.PrepTime)) + m.c = LogicalConstraint( + expr=m.a.end_time.before(m.b.start_time, delay=m.PrepTime) + ) self.assertIsInstance(m.c.expr, BeforeExpression) self.assertEqual(len(m.c.expr.args), 3) self.assertIs(m.c.expr.args[0], m.a.end_time) @@ -186,4 +187,3 @@ def test_end_before_start_param_delay(self): self.assertIs(m.c.expr.delay, m.PrepTime) self.assertEqual(str(m.c.expr), "a.end_time + PrepTime <= b.start_time") - From 34ec87ccef8ae9bbddfbfd6105a8821ccd728baf Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 11:49:06 -0700 Subject: [PATCH 0279/3044] Additional baseline update --- pyomo/core/tests/unit/test_set.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 6c7511359a1..f04a8229cf0 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -1819,7 +1819,6 @@ def test_construct(self): i.construct() ref = ( 'Constructing SetOperator, name=a*a, from data=None\n' - 'Constructing Set, name=a*a, from data=None\n' 'Constructing RangeSet, name=a, from data=None\n' ) self.assertEqual(output.getvalue(), ref) From 5ea9c15af2090077fd64b2a143bddc0b97328a0f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 11:56:15 -0700 Subject: [PATCH 0280/3044] Replace _implicit_subsets with _anonymous_sets --- pyomo/core/base/block.py | 69 ++++----------- pyomo/core/base/boolean_var.py | 4 + pyomo/core/base/constraint.py | 10 ++- pyomo/core/base/expression.py | 4 + pyomo/core/base/indexed_component.py | 33 ++++--- pyomo/core/base/logical_constraint.py | 9 +- pyomo/core/base/objective.py | 10 ++- pyomo/core/base/param.py | 6 +- pyomo/core/base/set.py | 121 +++++++++++++++++--------- pyomo/core/base/var.py | 18 ++-- pyomo/gdp/disjunct.py | 4 + pyomo/mpec/complementarity.py | 11 ++- pyomo/network/arc.py | 12 ++- pyomo/network/port.py | 12 ++- 14 files changed, 185 insertions(+), 138 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index fd5322ba686..43418089826 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -51,7 +51,7 @@ from pyomo.core.base.enums import SortComponents, TraversalStrategy from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.componentuid import ComponentUID -from pyomo.core.base.set import Any, GlobalSetBase, _SetDataBase +from pyomo.core.base.set import Any from pyomo.core.base.var import Var from pyomo.core.base.initializer import Initializer from pyomo.core.base.indexed_component import ( @@ -846,47 +846,6 @@ def transfer_attributes_from(self, src): ): setattr(self, k, v) - def _add_implicit_sets(self, val): - """TODO: This method has known issues (see tickets) and needs to be - reviewed. [JDS 9/2014]""" - - _component_sets = getattr(val, '_implicit_subsets', None) - # - # FIXME: The name attribute should begin with "_", and None - # should replace "_unknown_" - # - if _component_sets is not None: - for ctr, tset in enumerate(_component_sets): - if tset.parent_component().parent_block() is None and not isinstance( - tset.parent_component(), GlobalSetBase - ): - self.add_component("%s_index_%d" % (val.local_name, ctr), tset) - if ( - getattr(val, '_index_set', None) is not None - and isinstance(val._index_set, _SetDataBase) - and val._index_set.parent_component().parent_block() is None - and not isinstance(val._index_set.parent_component(), GlobalSetBase) - ): - self.add_component( - "%s_index" % (val.local_name,), val._index_set.parent_component() - ) - if ( - getattr(val, 'initialize', None) is not None - and isinstance(val.initialize, _SetDataBase) - and val.initialize.parent_component().parent_block() is None - and not isinstance(val.initialize.parent_component(), GlobalSetBase) - ): - self.add_component( - "%s_index_init" % (val.local_name,), val.initialize.parent_component() - ) - if ( - getattr(val, 'domain', None) is not None - and isinstance(val.domain, _SetDataBase) - and val.domain.parent_block() is None - and not isinstance(val.domain, GlobalSetBase) - ): - self.add_component("%s_domain" % (val.local_name,), val.domain) - def collect_ctypes(self, active=None, descend_into=True): """ Count all component types stored on or under this @@ -1066,16 +1025,11 @@ def add_component(self, name, val): val._parent = weakref.ref(self) val._name = name # - # We want to add the temporary / implicit sets first so that - # they get constructed before this component - # - # FIXME: This is sloppy and wasteful (most components trigger - # this, even when there is no need for it). We should - # reconsider the whole _implicit_subsets logic to defer this - # kind of thing to an "update_parent()" method on the - # components. + # Update the context of any anonymous sets # - self._add_implicit_sets(val) + if getattr(val, '_anonymous_sets', None) is not None: + for _set in val._anonymous_sets: + _set._parent = val._parent # # Add the component to the underlying Component store # @@ -1148,9 +1102,8 @@ def add_component(self, name, val): # added to the class by Block.__init__() # if getattr(_component, '_constructed', False): - # NB: we don't have to construct the temporary / implicit - # sets here: if necessary, that happens when - # _add_implicit_sets() calls add_component(). + # NB: we don't have to construct the anonymous sets here: if + # necessary, that happens in component.construct() if _BlockConstruction.data: data = _BlockConstruction.data.get(id(self), None) if data is not None: @@ -1236,6 +1189,10 @@ def del_component(self, name_or_object): # Clear the _parent attribute obj._parent = None + # Update the context of any anonymous sets + if getattr(obj, '_anonymous_sets', None) is not None: + for _set in obj._anonymous_sets: + _set._parent = None # Now that this component is not in the _decl map, we can call # delattr as usual. @@ -2150,6 +2107,10 @@ def construct(self, data=None): timer = ConstructionTimer(self) self._constructed = True + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + # Constructing blocks is tricky. Scalar blocks are already # partially constructed (they have _data[None] == self) in order # to support Abstract blocks. The block may therefore already diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index e2aebb4e466..aae132a5abf 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -383,6 +383,10 @@ def construct(self, data=None): timer = ConstructionTimer(self) self._constructed = True + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + # # Construct _BooleanVarData objects for all index values # diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 53afa35c70c..aafacaebdaf 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -717,8 +717,6 @@ class Constraint(ActiveIndexedComponent): A dictionary from the index set to component data objects _index The set of valid indices - _implicit_subsets - A tuple of set objects that represents the index set _model A weakref to the model that owns this component _parent @@ -772,6 +770,10 @@ def construct(self, data=None): if is_debug_set(logger): logger.debug("Constructing constraint %s" % (self.name)) + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + rule = self.rule try: # We do not (currently) accept data for constructing Constraints @@ -1068,7 +1070,9 @@ def construct(self, data=None): if is_debug_set(logger): logger.debug("Constructing constraint list %s" % (self.name)) - self.index_set().construct() + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() if self.rule is not None: _rule = self.rule(self.parent_block(), ()) diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index df9abf0a5a5..83ee2864180 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -394,6 +394,10 @@ def construct(self, data=None): % (self.name, str(data)) ) + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + try: # We do not (currently) accept data for constructing Constraints assert data is None diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index b474281f5b9..34df06845be 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -32,6 +32,7 @@ from pyomo.core.pyomoobject import PyomoObject from pyomo.common import DeveloperError from pyomo.common.autoslots import fast_deepcopy +from pyomo.common.collections import ComponentSet from pyomo.common.dependencies import numpy as np, numpy_available from pyomo.common.deprecation import deprecated, deprecation_warning from pyomo.common.errors import DeveloperError, TemplateExpressionError @@ -304,37 +305,33 @@ def __init__(self, *args, **kwds): # self._data = {} # - if len(args) == 0 or (len(args) == 1 and args[0] is UnindexedComponent_set): + if len(args) == 0 or (args[0] is UnindexedComponent_set and len(args) == 1): # # If no indexing sets are provided, generate a dummy index # - self._implicit_subsets = None self._index_set = UnindexedComponent_set + self._anonymous_sets = None elif len(args) == 1: # # If a single indexing set is provided, just process it. # - self._implicit_subsets = None - self._index_set = BASE.set.process_setarg(args[0]) + self._index_set, self._anonymous_sets = BASE.set.process_setarg(args[0]) else: # # If multiple indexing sets are provided, process them all, - # and store the cross-product of these sets. The individual - # sets need to stored in the Pyomo model, so the - # _implicit_subsets class data is used for this temporary - # storage. + # and store the cross-product of these sets. # - # Example: Pyomo allows things like - # "Param([1,2,3], range(100), initialize=0)". This - # needs to create *3* sets: two SetOf components and then - # the SetProduct. That means that the component needs to - # hold on to the implicit SetOf objects until the component - # is assigned to a model (where the implicit subsets can be - # "transferred" to the model). + # Example: Pyomo allows things like "Param([1,2,3], + # range(100), initialize=0)". This needs to create *3* + # sets: two SetOf components and then the SetProduct. As + # the user declined to name any of these sets, we will not + # make up names and instead store them on the model as + # "anonymous components" # - tmp = [BASE.set.process_setarg(x) for x in args] - self._implicit_subsets = tmp - self._index_set = tmp[0].cross(*tmp[1:]) + self._index_set = BASE.set.SetProduct(*args) + self._anonymous_sets = ComponentSet((self._index_set,)) + if self._index_set._anonymous_sets is not None: + self._anonymous_sets.update(self._index_set._anonymous_sets) def _create_objects_for_deepcopy(self, memo, component_list): _new = self.__class__.__new__(self.__class__) diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index 6d553c66fed..dd2f9f95cb9 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -210,8 +210,6 @@ class LogicalConstraint(ActiveIndexedComponent): A dictionary from the index set to component data objects _index_set The set of valid indices - _implicit_subsets - A tuple of set objects that represents the index set _model A weakref to the model that owns this component _parent @@ -280,6 +278,10 @@ def construct(self, data=None): timer = ConstructionTimer(self) self._constructed = True + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + _init_expr = self._init_expr _init_rule = self.rule # @@ -532,6 +534,9 @@ def construct(self, data=None): if self._constructed: return self._constructed = True + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() assert self._init_expr is None _init_rule = self.rule diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index 3c625d81c2d..c4491504a31 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -242,8 +242,6 @@ class Objective(ActiveIndexedComponent): A dictionary from the index set to component data objects _index The set of valid indices - _implicit_subsets - A tuple of set objects that represents the index set _model A weakref to the model that owns this component _parent @@ -291,6 +289,10 @@ def construct(self, data=None): if is_debug_set(logger): logger.debug("Constructing objective %s" % (self.name)) + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + rule = self.rule try: # We do not (currently) accept data for constructing Objectives @@ -586,7 +588,9 @@ def construct(self, data=None): if is_debug_set(logger): logger.debug("Constructing objective list %s" % (self.name)) - self.index_set().construct() + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() if self.rule is not None: _rule = self.rule(self.parent_block(), ()) diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index a6b893ec2c9..495117ce8dd 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -331,7 +331,7 @@ def __init__(self, *args, **kwd): if _domain_rule is None: self.domain = _ImplicitAny(owner=self, name='Any') else: - self.domain = SetInitializer(_domain_rule)(self.parent_block(), None) + self.domain = SetInitializer(_domain_rule)(self.parent_block(), None, self) # After IndexedComponent.__init__ so we can call is_indexed(). self._rule = Initializer( _init, @@ -784,6 +784,10 @@ def construct(self, data=None): ) self._mutable = True + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + try: # # If the default value is a simple type, we check it versus diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 6dfc3f07427..57903975183 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -17,6 +17,7 @@ import weakref from pyomo.common.pyomo_typing import overload +from pyomo.common.collections import ComponentSet from pyomo.common.deprecation import deprecated, deprecation_warning, RenamedClass from pyomo.common.errors import DeveloperError, PyomoException from pyomo.common.log import is_debug_set @@ -125,7 +126,17 @@ def process_setarg(arg): if isinstance(arg, _SetDataBase): - return arg + if ( + getattr(arg, '_parent', None) is not None + or getattr(arg, '_anonymous_sets', None) is GlobalSetBase + or arg.parent_component()._parent is not None + ): + return arg, None + _anonymous = ComponentSet((arg,)) + if getattr(arg, '_anonymous_sets', None) is not None: + _anonymous.update(arg._anonymous_sets) + return arg, _anonymous + elif isinstance(arg, _ComponentBase): if isinstance(arg, IndexedComponent) and arg.is_indexed(): raise TypeError( @@ -168,7 +179,7 @@ def process_setarg(arg): ) ): ans.construct() - return ans + return process_setarg(ans) # TBD: should lists/tuples be copied into Sets, or # should we preserve the reference using SetOf? @@ -188,19 +199,20 @@ def process_setarg(arg): # create the Set: # _defer_construct = False - if inspect.isgenerator(arg): - _ordered = True - _defer_construct = True - elif inspect.isfunction(arg): - _ordered = True - _defer_construct = True - elif not hasattr(arg, '__contains__'): - raise TypeError( - "Cannot create a Set from data that does not support " - "__contains__. Expected set-like object supporting " - "collections.abc.Collection interface, but received '%s'." - % (type(arg).__name__,) - ) + if not hasattr(arg, '__contains__'): + if inspect.isgenerator(arg): + _ordered = True + _defer_construct = True + elif inspect.isfunction(arg): + _ordered = True + _defer_construct = True + else: + raise TypeError( + "Cannot create a Set from data that does not support " + "__contains__. Expected set-like object supporting " + "collections.abc.Collection interface, but received '%s'." + % (type(arg).__name__,) + ) elif arg.__class__ is type: # This catches the (deprecated) RealSet API. return process_setarg(arg()) @@ -221,7 +233,10 @@ def process_setarg(arg): # Or we can do the simple thing and just use SetOf: # # ans = SetOf(arg) - return ans + _anonymous = ComponentSet((ans,)) + if getattr(ans, '_anonymous_sets', None) is not None: + _anonymous.update(_anonymous_sets) + return ans, _anonymous @deprecated( @@ -308,11 +323,22 @@ def intersect(self, other): else: self._set = SetIntersectInitializer(self._set, other) - def __call__(self, parent, idx): + def __call__(self, parent, idx, obj): if self._set is None: return Any - else: - return process_setarg(self._set(parent, idx)) + _ans, _anonymous = process_setarg(self._set(parent, idx)) + if _anonymous: + pc = obj.parent_component() + if getattr(pc, '_anonymous_sets', None) is None: + pc._anonymous_sets = _anonymous + else: + pc._anonymous_sets.update(_anonymous) + for _set in _anonymous: + _set._parent = pc._parent + if pc._constructed: + for _set in _anonymous: + _set.construct() + return _ans def constant(self): return self._set is None or self._set.constant() @@ -2089,7 +2115,7 @@ def __init__(self, *args, **kwds): # order to correctly parse the data stream. if not self.is_indexed(): if self._init_domain.constant(): - self._domain = self._init_domain(self.parent_block(), None) + self._domain = self._init_domain(self.parent_block(), None, self) if self._init_dimen.constant(): self._dimen = self._init_dimen(self.parent_block(), None) @@ -2109,6 +2135,11 @@ def construct(self, data=None): if is_debug_set(logger): logger.debug("Constructing Set, name=%s, from data=%r" % (self.name, data)) self._constructed = True + + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + if data is not None: # Data supplied to construct() should override data provided # to the constructor @@ -2163,7 +2194,9 @@ def _getitem_when_not_present(self, index): ) _d = None - domain = self._init_domain(_block, index) + domain = self._init_domain(_block, index, self) + if domain is not None: + domain.construct() if _d is UnknownSetDimen and domain is not None and domain.dimen is not None: _d = domain.dimen @@ -2187,11 +2220,9 @@ def _getitem_when_not_present(self, index): else: obj = self._data[index] = self._ComponentDataClass(component=self) obj._index = index + obj._domain = domain if _d is not UnknownSetDimen: obj._dimen = _d - if domain is not None: - obj._domain = domain - domain.parent_component().construct() if self._init_validate is not None: try: obj._validate = Initializer(self._init_validate(_block, index)) @@ -3232,31 +3263,37 @@ class SetOperator(_SetData, Set): def __init__(self, *args, **kwds): _SetData.__init__(self, component=self) Set.__init__(self, **kwds) - implicit = [] - sets = [] - for _set in args: - _new_set = process_setarg(_set) - sets.append(_new_set) - if _new_set is not _set or _new_set.parent_block() is None: - implicit.append(_new_set) - self._sets = tuple(sets) - self._implicit_subsets = tuple(implicit) - # We will implicitly construct all set operators if the operands - # are all constructed. + self._sets, _anonymous = zip(*(process_setarg(_set) for _set in args)) + _anonymous = tuple(filter(None, _anonymous)) + if _anonymous: + self._anonymous_sets = ComponentSet() + for _set in _anonymous: + self._anonymous_sets.update(_set) + # We will immediately construct all set operators if the operands + # are all themselves constructed. if all(_.parent_component()._constructed for _ in self._sets): self.construct() def construct(self, data=None): if self._constructed: return + self._constructed = True + timer = ConstructionTimer(self) if is_debug_set(logger): logger.debug( - "Constructing SetOperator, name=%s, from data=%r" % (self.name, data) + "Constructing SetOperator, name=%s, from data=%r" % (self, data) ) - for s in self._sets: - s.parent_component().construct() - super(SetOperator, self).construct() + + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + + # This ensures backwards compatibility by causing all scalar + # sets (including set operators) to be initialized (and + # potentially empty) after construct(). + self._getitem_when_not_present(None) + if data: deprecation_warning( "Providing construction data to SetOperator objects is " @@ -4350,7 +4387,11 @@ def __new__(cls, *args, **kwds): name = base_set.name else: name = cls_name - ans = RangeSet(ranges=list(range_init(None, None).ranges()), name=name) + tmp = Set() + ans = RangeSet( + ranges=list(range_init(None, None, tmp).ranges()), name=name + ) + ans._anonymous_sets = tmp._anonymous_sets if name_kwd is None and (cls_name is not None or bounds is not None): ans._name += str(ans.bounds()) else: diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index e7e9e4f8f2f..8d5b93f3ace 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -436,7 +436,9 @@ def domain(self): @domain.setter def domain(self, domain): try: - self._domain = SetInitializer(domain)(self.parent_block(), self.index()) + self._domain = SetInitializer(domain)( + self.parent_block(), self.index(), self + ) except: logger.error( "%s is not a valid domain. Variable domains must be an " @@ -774,6 +776,10 @@ def construct(self, data=None): if is_debug_set(logger): logger.debug("Constructing Variable %s" % (self.name,)) + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + # Note: define 'index' to avoid 'variable referenced before # assignment' in the error message generated in the 'except:' # block below. @@ -854,7 +860,7 @@ def construct(self, data=None): # We can directly set the attribute (not the # property) because the SetInitializer ensures # that the value is a proper Set. - obj._domain = self._rule_domain(block, index) + obj._domain = self._rule_domain(block, index, self) if call_bounds_rule: for index, obj in self._data.items(): obj.lower, obj.upper = self._rule_bounds(block, index) @@ -891,7 +897,7 @@ def _getitem_when_not_present(self, index): obj._index = index # We can directly set the attribute (not the property) because # the SetInitializer ensures that the value is a proper Set. - obj._domain = self._rule_domain(parent, index) + obj._domain = self._rule_domain(parent, index, self) if self._rule_bounds is not None: obj.lower, obj.upper = self._rule_bounds(parent, index) if self._rule_init is not None: @@ -1013,17 +1019,17 @@ def domain(self, domain): try: domain_rule = SetInitializer(domain) if domain_rule.constant(): - domain = domain_rule(self.parent_block(), None) + domain = domain_rule(self.parent_block(), None, self) for vardata in self.values(): vardata._domain = domain elif domain_rule.contains_indices(): parent = self.parent_block() for index in domain_rule.indices(): - self[index]._domain = domain_rule(parent, index) + self[index]._domain = domain_rule(parent, index, self) else: parent = self.parent_block() for index, vardata in self.items(): - vardata._domain = domain_rule(parent, index) + vardata._domain = domain_rule(parent, index, self) except: logger.error( "%s is not a valid domain. Variable domains must be an " diff --git a/pyomo/gdp/disjunct.py b/pyomo/gdp/disjunct.py index eca6d93d732..842388e7502 100644 --- a/pyomo/gdp/disjunct.py +++ b/pyomo/gdp/disjunct.py @@ -700,6 +700,10 @@ def construct(self, data=None): timer = ConstructionTimer(self) self._constructed = True + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + _self_parent = self.parent_block() if not self.is_indexed(): if self._init_rule is not None: diff --git a/pyomo/mpec/complementarity.py b/pyomo/mpec/complementarity.py index df991ce9686..4eccd453cbc 100644 --- a/pyomo/mpec/complementarity.py +++ b/pyomo/mpec/complementarity.py @@ -357,13 +357,18 @@ def construct(self, data=None): """ Construct the expression(s) for this complementarity condition. """ - if is_debug_set(logger): - logger.debug("Constructing complementarity list %s", self.name) if self._constructed: return - timer = ConstructionTimer(self) self._constructed = True + timer = ConstructionTimer(self) + if is_debug_set(logger): + logger.debug("Constructing complementarity list %s", self.name) + + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() + if self._init_rule is not None: _init = self._init_rule(self.parent_block(), ()) for cc in iter(_init): diff --git a/pyomo/network/arc.py b/pyomo/network/arc.py index ff1874b0274..04d96a2c531 100644 --- a/pyomo/network/arc.py +++ b/pyomo/network/arc.py @@ -296,14 +296,18 @@ def __init__(self, *args, **kwds): def construct(self, data=None): """Initialize the Arc""" - if is_debug_set(logger): - logger.debug("Constructing Arc %s" % self.name) - if self._constructed: return + self._constructed = True + + if is_debug_set(logger): + logger.debug("Constructing Arc %s" % self.name) timer = ConstructionTimer(self) - self._constructed = True + + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() if self._rule is None and self._init_vals is None: # No construction rule or values specified diff --git a/pyomo/network/port.py b/pyomo/network/port.py index 4afb0e23ed0..c0e40e090f5 100644 --- a/pyomo/network/port.py +++ b/pyomo/network/port.py @@ -346,14 +346,18 @@ def _getitem_when_not_present(self, idx): return tmp def construct(self, data=None): - if is_debug_set(logger): # pragma:nocover - logger.debug("Constructing Port, name=%s, from data=%s" % (self.name, data)) - if self._constructed: return + self._constructed = True timer = ConstructionTimer(self) - self._constructed = True + + if is_debug_set(logger): # pragma:nocover + logger.debug("Constructing Port, name=%s, from data=%s" % (self.name, data)) + + if self._anonymous_sets is not None: + for _set in self._anonymous_sets: + _set.construct() # Construct _PortData objects for all index values if self.is_indexed(): From fa64b1e20b49f05d43a56c66998d4a6750455618 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 12:31:43 -0700 Subject: [PATCH 0281/3044] Track changes to SetInitializer API --- pyomo/core/tests/unit/test_set.py | 42 ++++++++++++++++++------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index f04a8229cf0..ed01dff568b 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -112,17 +112,19 @@ class Test_SetInitializer(unittest.TestCase): def test_single_set(self): + tmp = Set() # a placeholder to accumulate _anonymous_sets references + a = SetInitializer(None) self.assertIs(type(a), SetInitializer) self.assertIsNone(a._set) - self.assertIs(a(None, None), Any) + self.assertIs(a(None, None, tmp), Any) self.assertTrue(a.constant()) self.assertFalse(a.verified) a = SetInitializer(Reals) self.assertIs(type(a), SetInitializer) self.assertIs(type(a._set), ConstantInitializer) - self.assertIs(a(None, None), Reals) + self.assertIs(a(None, None, tmp), Reals) self.assertIs(a._set.val, Reals) self.assertTrue(a.constant()) self.assertFalse(a.verified) @@ -130,18 +132,20 @@ def test_single_set(self): a = SetInitializer({1: Reals}) self.assertIs(type(a), SetInitializer) self.assertIs(type(a._set), ItemInitializer) - self.assertIs(a(None, 1), Reals) + self.assertIs(a(None, 1, tmp), Reals) self.assertFalse(a.constant()) self.assertFalse(a.verified) def test_intersect(self): + tmp = Set() # a placeholder to accumulate _anonymous_sets references + a = SetInitializer(None) a.intersect(SetInitializer(None)) self.assertIs(type(a), SetInitializer) self.assertIsNone(a._set) self.assertTrue(a.constant()) self.assertFalse(a.verified) - self.assertIs(a(None, None), Any) + self.assertIs(a(None, None, tmp), Any) a = SetInitializer(None) a.intersect(SetInitializer(Reals)) @@ -150,7 +154,7 @@ def test_intersect(self): self.assertIs(a._set.val, Reals) self.assertTrue(a.constant()) self.assertFalse(a.verified) - self.assertIs(a(None, None), Reals) + self.assertIs(a(None, None, tmp), Reals) a = SetInitializer(None) a.intersect(BoundsInitializer(5, default_step=1)) @@ -158,7 +162,7 @@ def test_intersect(self): self.assertIs(type(a._set), BoundsInitializer) self.assertTrue(a.constant()) self.assertFalse(a.verified) - self.assertEqual(a(None, None), RangeSet(5)) + self.assertEqual(a(None, None, tmp), RangeSet(5)) a = SetInitializer(Reals) a.intersect(SetInitializer(None)) @@ -167,7 +171,7 @@ def test_intersect(self): self.assertIs(a._set.val, Reals) self.assertTrue(a.constant()) self.assertFalse(a.verified) - self.assertIs(a(None, None), Reals) + self.assertIs(a(None, None, tmp), Reals) a = SetInitializer(Reals) a.intersect(SetInitializer(Integers)) @@ -179,7 +183,7 @@ def test_intersect(self): self.assertIs(a._set._B.val, Integers) self.assertTrue(a.constant()) self.assertFalse(a.verified) - s = a(None, None) + s = a(None, None, tmp) self.assertIs(type(s), SetIntersection_InfiniteSet) self.assertIs(s._sets[0], Reals) self.assertIs(s._sets[1], Integers) @@ -195,7 +199,7 @@ def test_intersect(self): self.assertIs(a._set._A._B.val, Integers) self.assertTrue(a.constant()) self.assertFalse(a.verified) - s = a(None, None) + s = a(None, None, tmp) self.assertIs(type(s), SetIntersection_OrderedSet) self.assertIs(type(s._sets[0]), SetIntersection_InfiniteSet) self.assertIsInstance(s._sets[1], RangeSet) @@ -212,7 +216,7 @@ def test_intersect(self): self.assertIs(a._set._A._B.val, Integers) self.assertTrue(a.constant()) self.assertFalse(a.verified) - s = a(None, None) + s = a(None, None, tmp) self.assertIs(type(s), SetIntersection_InfiniteSet) p.construct() s.construct() @@ -236,8 +240,8 @@ def test_intersect(self): self.assertFalse(a.constant()) self.assertFalse(a.verified) with self.assertRaises(KeyError): - a(None, None) - s = a(None, 1) + a(None, None, tmp) + s = a(None, 1, tmp) self.assertIs(type(s), SetIntersection_InfiniteSet) p.construct() s.construct() @@ -304,15 +308,17 @@ def test_boundsinit(self): self.assertEqual(s, RangeSet(0, 5)) def test_setdefault(self): + tmp = Set() # a placeholder to accumulate _anonymous_sets references + a = SetInitializer(None) - self.assertIs(a(None, None), Any) + self.assertIs(a(None, None, tmp), Any) a.setdefault(Reals) - self.assertIs(a(None, None), Reals) + self.assertIs(a(None, None, tmp), Reals) a = SetInitializer(Integers) - self.assertIs(a(None, None), Integers) + self.assertIs(a(None, None, tmp), Integers) a.setdefault(Reals) - self.assertIs(a(None, None), Integers) + self.assertIs(a(None, None, tmp), Integers) a = BoundsInitializer(5, default_step=1) self.assertEqual(a(None, None), RangeSet(5)) @@ -321,9 +327,9 @@ def test_setdefault(self): a = SetInitializer(Reals) a.intersect(SetInitializer(Integers)) - self.assertIs(type(a(None, None)), SetIntersection_InfiniteSet) + self.assertIs(type(a(None, None, tmp)), SetIntersection_InfiniteSet) a.setdefault(RangeSet(5)) - self.assertIs(type(a(None, None)), SetIntersection_InfiniteSet) + self.assertIs(type(a(None, None, tmp)), SetIntersection_InfiniteSet) def test_indices(self): a = SetInitializer(None) From 85496b85d41509be91f823b48530e6f9b8a63513 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 12:32:16 -0700 Subject: [PATCH 0282/3044] Track changes to SetProduct dimen when not flatting --- pyomo/core/tests/unit/test_set.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index ed01dff568b..a344cbd585c 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -3098,7 +3098,7 @@ def test_no_normalize_index(self): x = I * J normalize_index.flatten = False - self.assertIs(x.dimen, None) + self.assertIs(x.dimen, 2) self.assertIn(((1, 2), 3), x) self.assertIn((1, (2, 3)), x) # if we are not flattening, then lookup must match the @@ -3273,7 +3273,7 @@ def test_ordered_multidim_setproduct(self): ((3, 4), (7, 8)), ] self.assertEqual(list(x), ref) - self.assertEqual(x.dimen, None) + self.assertEqual(x.dimen, 2) finally: SetModule.FLATTEN_CROSS_PRODUCT = origFlattenCross @@ -3317,7 +3317,7 @@ def test_ordered_nondim_setproduct(self): (1, (2, 3), 5), ] self.assertEqual(list(x), ref) - self.assertEqual(x.dimen, None) + self.assertEqual(x.dimen, 3) finally: SetModule.FLATTEN_CROSS_PRODUCT = origFlattenCross @@ -3369,7 +3369,7 @@ def test_ordered_nondim_setproduct(self): self.assertEqual(list(x), ref) for i, v in enumerate(ref): self.assertEqual(x[i + 1], v) - self.assertEqual(x.dimen, None) + self.assertEqual(x.dimen, 4) finally: SetModule.FLATTEN_CROSS_PRODUCT = origFlattenCross @@ -5252,7 +5252,7 @@ def test_no_normalize_index(self): m.I = Set() self.assertIs(m.I._dimen, UnknownSetDimen) self.assertTrue(m.I.add((1, (2, 3)))) - self.assertIs(m.I._dimen, None) + self.assertIs(m.I._dimen, 2) self.assertNotIn(((1, 2), 3), m.I) self.assertIn((1, (2, 3)), m.I) self.assertNotIn((1, 2, 3), m.I) From 1120943f7e3d002470ca6c2f9c960910e47ed569 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 12:33:10 -0700 Subject: [PATCH 0283/3044] Make flatten tests more robust to test failures (guarantee normalize_index.flatten state is restored) --- pyomo/dae/tests/test_flatten.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyomo/dae/tests/test_flatten.py b/pyomo/dae/tests/test_flatten.py index a6ea824c3ef..d228b2bdd62 100644 --- a/pyomo/dae/tests/test_flatten.py +++ b/pyomo/dae/tests/test_flatten.py @@ -49,6 +49,12 @@ class TestAssumedBehavior(unittest.TestCase): immediately obvious would be the case. """ + def setUp(self): + self._orig_flatten = normalize_index.flatten + + def tearDown(self): + normalize_index.flatten = self._orig_flatten + def test_cross(self): m = ConcreteModel() m.s1 = Set(initialize=[1, 2]) @@ -313,6 +319,12 @@ def c_rule(m, t): class TestFlatten(_TestFlattenBase, unittest.TestCase): + def setUp(self): + self._orig_flatten = normalize_index.flatten + + def tearDown(self): + normalize_index.flatten = self._orig_flatten + def _model1_1d_sets(self): # One-dimensional sets, no skipping. m = ConcreteModel() From 4c6ca65cbbe228363f958485846a2b8d821dcc88 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 12:39:23 -0700 Subject: [PATCH 0284/3044] Standardize structure of component.construct() methods --- pyomo/core/base/block.py | 9 +++++---- pyomo/core/base/constraint.py | 3 +-- pyomo/core/base/logical_constraint.py | 10 +++++----- pyomo/core/base/objective.py | 3 +-- pyomo/core/base/set.py | 25 ++++++++++++++++--------- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 43418089826..ca1edba2a6f 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2095,6 +2095,11 @@ def construct(self, data=None): """ Initialize the block """ + if self._constructed: + return + self._constructed = True + + timer = ConstructionTimer(self) if is_debug_set(logger): logger.debug( "Constructing %s '%s', from data=%s", @@ -2102,10 +2107,6 @@ def construct(self, data=None): self.name, str(data), ) - if self._constructed: - return - timer = ConstructionTimer(self) - self._constructed = True if self._anonymous_sets is not None: for _set in self._anonymous_sets: diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index aafacaebdaf..9f39ac873a1 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -1047,8 +1047,7 @@ def __init__(self, **kwargs): _rule = kwargs.pop('rule', None) self._starting_index = kwargs.pop('starting_index', 1) - args = (Set(dimen=1),) - super(ConstraintList, self).__init__(*args, **kwargs) + super(ConstraintList, self).__init__(Set(dimen=1), **kwargs) self.rule = Initializer( _rule, treat_sequences_as_mappings=False, allow_generators=True diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index dd2f9f95cb9..2cfb68f4a5d 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -518,22 +518,22 @@ class LogicalConstraintList(IndexedLogicalConstraint): def __init__(self, **kwargs): """Constructor""" - args = (Set(),) if 'expr' in kwargs: raise ValueError("LogicalConstraintList does not accept the 'expr' keyword") - LogicalConstraint.__init__(self, *args, **kwargs) + LogicalConstraint.__init__(self, Set(dimen=1), **kwargs) def construct(self, data=None): """ Construct the expression(s) for this logical constraint. """ + if self._constructed: + return + self._constructed = True + generate_debug_messages = is_debug_set(logger) if generate_debug_messages: logger.debug("Constructing logical constraint list %s" % self.name) - if self._constructed: - return - self._constructed = True if self._anonymous_sets is not None: for _set in self._anonymous_sets: _set.construct() diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index c4491504a31..b72d0bd5d1b 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -567,8 +567,7 @@ def __init__(self, **kwargs): _rule = kwargs.pop('rule', None) self._starting_index = kwargs.pop('starting_index', 1) - args = (Set(dimen=1),) - super().__init__(*args, **kwargs) + super().__init__(Set(dimen=1), **kwargs) self.rule = Initializer(_rule, allow_generators=True) # HACK to make the "counted call" syntax work. We wait until diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 57903975183..6b15905dd14 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -2131,10 +2131,11 @@ def check_values(self): def construct(self, data=None): if self._constructed: return + self._constructed = True + timer = ConstructionTimer(self) if is_debug_set(logger): - logger.debug("Constructing Set, name=%s, from data=%r" % (self.name, data)) - self._constructed = True + logger.debug("Constructing Set, name=%s, from data=%r" % (self, data)) if self._anonymous_sets is not None: for _set in self._anonymous_sets: @@ -2479,6 +2480,7 @@ def __init__(self, reference, **kwds): kwds.setdefault('ctype', SetOf) Component.__init__(self, **kwds) self._ref = reference + self.construct() def __str__(self): if self.parent_block() is not None: @@ -2488,12 +2490,11 @@ def __str__(self): def construct(self, data=None): if self._constructed: return + self._constructed = True + timer = ConstructionTimer(self) if is_debug_set(logger): - logger.debug( - "Constructing SetOf, name=%s, from data=%r" % (self.name, data) - ) - self._constructed = True + logger.debug("Constructing SetOf, name=%s, from data=%r" % (self, data)) timer.report() @property @@ -2985,11 +2986,16 @@ def __str__(self): def construct(self, data=None): if self._constructed: return + timer = ConstructionTimer(self) if is_debug_set(logger): - logger.debug( - "Constructing RangeSet, name=%s, from data=%r" % (self.name, data) - ) + logger.debug("Constructing RangeSet, name=%s, from data=%r" % (self, data)) + # Note: we cannot set the constructed flag until after we have + # generated the debug message: the debug message needs the name, + # which in turn may need ranges(), which has not been + # constructed. + self._constructed = True + if data is not None: raise ValueError( "RangeSet.construct() does not support the data= argument.\n" @@ -4260,6 +4266,7 @@ class _EmptySet(_FiniteSetMixin, _SetData, Set): def __init__(self, **kwds): _SetData.__init__(self, component=self) Set.__init__(self, **kwds) + self.construct() def get(self, val, default=None): return default From 2d7757a8c9b7b6daae96e84de4afca80967b0a48 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 12:59:03 -0700 Subject: [PATCH 0285/3044] Do not explicitly assign floating component names to class type --- pyomo/core/base/component.py | 4 +++- pyomo/core/base/set.py | 46 ++++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index a8550f8f469..1c59da15cec 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -501,7 +501,7 @@ def __init__(self, **kwds): # self._ctype = kwds.pop('ctype', None) self.doc = kwds.pop('doc', None) - self._name = kwds.pop('name', str(type(self).__name__)) + self._name = kwds.pop('name', None) if kwds: raise ValueError( "Unexpected keyword options found while constructing '%s':\n\t%s" @@ -625,6 +625,8 @@ def getname(self, fully_qualified=False, name_buffer=None, relative_to=None): Generate fully_qualified names relative to the specified block. """ local_name = self._name + if local_name is None: + local_name = type(self).__name__ if fully_qualified: pb = self.parent_block() if relative_to is None: diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 6b15905dd14..3be207288d0 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1344,7 +1344,7 @@ def __len__(self): return len(self._values) def __str__(self): - if self.parent_block() is not None: + if self.parent_component()._name is not None: return self.name if not self.parent_component()._constructed: return type(self).__name__ @@ -2483,7 +2483,7 @@ def __init__(self, reference, **kwds): self.construct() def __str__(self): - if self.parent_block() is not None: + if self._name is not None: return self.name return str(self._ref) @@ -2966,14 +2966,12 @@ def __init__(self, *args, **kwds): pass def __str__(self): - if self.parent_block() is not None: + # Named, components should return their name e.g., Reals + if self._name is not None: return self.name # Unconstructed floating components return their type if not self._constructed: return type(self).__name__ - # Named, constructed components should return their name e.g., Reals - if type(self).__name__ != self._name: - return self.name # Floating, unnamed constructed components return their ranges() ans = ' | '.join(str(_) for _ in self.ranges()) if ' | ' in ans: @@ -3003,19 +3001,9 @@ def construct(self, data=None): "as numbers, constants, or Params to the RangeSet() " "declaration" ) - self._constructed = True args, ranges = self._init_data - if any(not is_constant(arg) for arg in args): - logger.warning( - "Constructing RangeSet '%s' from non-constant data (e.g., " - "Var or mutable Param). The linkage between this RangeSet " - "and the original source data will be broken, so updating " - "the data value in the future will not be reflected in this " - "RangeSet. To suppress this warning, explicitly convert " - "the source data to a constant type (e.g., float, int, or " - "immutable Param)" % (self.name,) - ) + nonconstant_data_warning = any(not is_constant(arg) for arg in args) args = tuple(value(arg) for arg in args) if type(ranges) is not tuple: ranges = tuple(ranges) @@ -3176,6 +3164,22 @@ def construct(self, data=None): "Set %s" % (val, self.name) ) + # Defer the warning about non-constant args until after the + # component has been constructed, so that the conversion of the + # component to a rational string will work (anonymous RangeSets + # will report their ranges, which aren't present until + # construction is over) + if nonconstant_data_warning: + logger.warning( + "Constructing RangeSet '%s' from non-constant data (e.g., " + "Var or mutable Param). The linkage between this RangeSet " + "and the original source data will be broken, so updating " + "the data value in the future will not be reflected in this " + "RangeSet. To suppress this warning, explicitly convert " + "the source data to a constant type (e.g., float, int, or " + "immutable Param)" % (self,) + ) + timer.report() # @@ -3320,7 +3324,7 @@ def construct(self, data=None): if fail: raise ValueError( "Constructing SetOperator %s with incompatible data " - "(data=%s}" % (self.name, data) + "(data=%s}" % (self, data) ) timer.report() @@ -3346,7 +3350,7 @@ def __len__(self): ) def __str__(self): - if self.parent_block() is not None: + if self._name is not None: return self.name return self._expression_str() @@ -4245,7 +4249,7 @@ def domain(self): return Any def __str__(self): - if self.parent_block() is not None: + if self._name is not None: return self.name return type(self).__name__ @@ -4291,7 +4295,7 @@ def domain(self): return EmptySet def __str__(self): - if self.parent_block() is not None: + if self._name is not None: return self.name return type(self).__name__ From 5c916d4896a225af7a3d7fbb733be46a39cd9d89 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 12:59:30 -0700 Subject: [PATCH 0286/3044] Mock up additional Set API for UnindexedComponent_set --- pyomo/core/base/global_set.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyomo/core/base/global_set.py b/pyomo/core/base/global_set.py index f4d97403308..f9a6dddc33b 100644 --- a/pyomo/core/base/global_set.py +++ b/pyomo/core/base/global_set.py @@ -72,8 +72,11 @@ def _parent(self, val): class _UnindexedComponent_set(GlobalSetBase): local_name = 'UnindexedComponent_set' + _anonymous_sets = GlobalSetBase + def __init__(self, name): self.name = name + self._constructed = True def __contains__(self, val): return val is None @@ -180,6 +183,12 @@ def prev(self, item, step=1): def prevw(self, item, step=1): return self.nextw(item, -step) + def parent_block(self): + return None + + def parent_component(self): + return self + UnindexedComponent_set = _UnindexedComponent_set('UnindexedComponent_set') GlobalSets[UnindexedComponent_set.local_name] = UnindexedComponent_set From f1116c07b75ce3d15c708429a3d864658eeae157 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 13:01:19 -0700 Subject: [PATCH 0287/3044] SetProduct should report a meaningful dimen even when not flattening indices --- pyomo/core/base/indexed_component.py | 6 ++++-- pyomo/core/base/indexed_component_slice.py | 3 +-- pyomo/core/base/set.py | 6 ++++-- pyomo/dae/flatten.py | 24 ++++++++++++++++++++-- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index 34df06845be..d29ae3cd43f 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -1001,11 +1001,13 @@ def _processUnhashableIndex(self, idx): slice_dim -= 1 if normalize_index.flatten: set_dim = self.dim() - elif self._implicit_subsets is None: + elif not self.is_indexed(): # Scalar component. set_dim = 0 else: - set_dim = len(self._implicit_subsets) + set_dim = self.index_set().dimen + if set_dim is None: + set_dim = 1 structurally_valid = False if slice_dim == set_dim or set_dim is None: diff --git a/pyomo/core/base/indexed_component_slice.py b/pyomo/core/base/indexed_component_slice.py index 9779711a19b..8fd625bfeaa 100644 --- a/pyomo/core/base/indexed_component_slice.py +++ b/pyomo/core/base/indexed_component_slice.py @@ -402,8 +402,7 @@ def __init__(self, component, fixed, sliced, ellipsis, iter_over_index, sort): self.last_index = () self.tuplize_unflattened_index = ( - self.component._implicit_subsets is None - or len(self.component._implicit_subsets) == 1 + len(list(self.component.index_set().subsets())) <= 1 ) if fixed is None and sliced is None and ellipsis is None: diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 3be207288d0..32ae08fca23 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1382,8 +1382,10 @@ def add(self, *values): else: # If we are not normalizing indices, then we cannot reliably # infer the set dimen + _d = 1 + if isinstance(value, Sequence) and self.dimen != 1: + _d = len(value) _value = value - _d = None if _value not in self._domain: raise ValueError( "Cannot add value %s to Set %s.\n" @@ -3944,7 +3946,7 @@ def bounds(self): @property def dimen(self): if not (FLATTEN_CROSS_PRODUCT and normalize_index.flatten): - return None + return len(self._sets) # By convention, "None" trumps UnknownSetDimen. That is, a set # product is "non-dimentioned" if any term is non-dimentioned, # even if we do not yet know the dimentionality of another term. diff --git a/pyomo/dae/flatten.py b/pyomo/dae/flatten.py index 595f90b3dc7..d6da8bb84d5 100644 --- a/pyomo/dae/flatten.py +++ b/pyomo/dae/flatten.py @@ -200,8 +200,28 @@ def slice_component_along_sets(component, sets, context_slice=None, normalize=No # # Note that c_slice is not necessarily a slice. # We enter this loop even if no sets need slicing. - temp_slice = c_slice.duplicate() - next(iter(temp_slice)) + try: + next(iter(c_slice.duplicate())) + except IndexError: + if normalize_index.flatten: + raise + # There is an edge case where when we are not + # flattening indices the dimensionality of an + # index can change between a SetProduct and the + # member Sets: the member set can have dimen>1 + # (or even None!), but the dimen of that portion + # of the SetProduct is always 1. Since we are + # just checking that the c_slice isn't + # completely empty, we will allow matching with + # an Ellipsis + _empty = True + try: + next(iter(base_component[...])) + _empty = False + except: + pass + if _empty: + raise if (normalize is None and normalize_index.flatten) or normalize: # Most users probably want this index to be normalized, # so they can more conveniently use it as a key in a From 1d3c761dc4d1cf76cfe3373a2e54f344aec7ffa3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 13:03:53 -0700 Subject: [PATCH 0288/3044] Simplify definition of reverse Set operators --- pyomo/core/base/set.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 32ae08fca23..5af30bfb3a2 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1152,33 +1152,23 @@ def cross(self, *args): def __ror__(self, other): # See the discussion of Set vs SetOf in process_setarg above - # - # return SetOf(other) | self - return process_setarg(other) | self + return SetUnion(other, self) def __rand__(self, other): # See the discussion of Set vs SetOf in process_setarg above - # - # return SetOf(other) & self - return process_setarg(other) & self + return SetIntersection(other, self) def __rsub__(self, other): # See the discussion of Set vs SetOf in process_setarg above - # - # return SetOf(other) - self - return process_setarg(other) - self + return SetDifference(other, self) def __rxor__(self, other): # See the discussion of Set vs SetOf in process_setarg above - # - # return SetOf(other) ^ self - return process_setarg(other) ^ self + return SetSymmetricDifference(other, self) def __rmul__(self, other): # See the discussion of Set vs SetOf in process_setarg above - # - # return SetOf(other) * self - return process_setarg(other) * self + return SetProduct(other, self) def __lt__(self, other): """ From 61aab8507e2328544c2c77056babcf287c059e99 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 13:05:40 -0700 Subject: [PATCH 0289/3044] Add a flag to _anonymous_sets to more easily detect GlobalSets --- pyomo/core/base/set.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 5af30bfb3a2..5ba42bbb554 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -4424,6 +4424,9 @@ def get_interval(self): # Cache the set bounds / interval _set._bounds = obj.bounds() _set._interval = obj.get_interval() + # Now that the set is constructed, override the _anonymous_sets to + # mark the set as a global set (used by process_setarg) + _set._anonymous_sets = GlobalSetBase return _set From ffdd9c8bf4aa73d4b5af330fc9c3b5e3adec777a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 13:05:57 -0700 Subject: [PATCH 0290/3044] Track move to anonymous sets --- pyomo/contrib/benders/benders_cuts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/benders/benders_cuts.py b/pyomo/contrib/benders/benders_cuts.py index 5eb2e91cc82..3f63e1d5cbe 100644 --- a/pyomo/contrib/benders/benders_cuts.py +++ b/pyomo/contrib/benders/benders_cuts.py @@ -335,7 +335,6 @@ def generate_cut(self): subproblem_solver.remove_constraint(c) subproblem_solver.remove_constraint(subproblem.fix_eta) del subproblem.fix_complicating_vars - del subproblem.fix_complicating_vars_index del subproblem.fix_eta total_num_subproblems = self.global_num_subproblems() From c5fec87b7520a3366034690e7e4f8a7d318765e8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 13:06:19 -0700 Subject: [PATCH 0291/3044] NFC: update documentation --- pyomo/core/base/indexed_component.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index d29ae3cd43f..11cfc923b28 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -256,8 +256,7 @@ def rule_wrapper(rule, wrapping_fcn, positional_arg_map=None): class IndexedComponent(Component): - """ - This is the base class for all indexed modeling components. + """This is the base class for all indexed modeling components. This class stores a dictionary, self._data, that maps indices to component data objects. The object self._index_set defines valid keys for this dictionary, and the dictionary keys may be a @@ -279,11 +278,16 @@ class IndexedComponent(Component): doc A text string describing this component Private class attributes: - _data A dictionary from the index set to - component data objects - _index_set The set of valid indices - _implicit_subsets A temporary data element that stores - sets that are transferred to the model + + _data: A dictionary from the index set to component data objects + + _index_set: The set of valid indices + + _anonymous_sets: A ComponentSet of "anonymous" sets used by this + component. Anonymous sets are Set / SetOperator / RangeSet + that compose attributes like _index_set, but are not + themselves explicitly assigned (and named) on any Block + """ class Skip(object): From 453b955f4791353ae0ba5eb5e05fdc52a9e10bda Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 13:08:00 -0700 Subject: [PATCH 0292/3044] Guard use of constructed Set API for unconstructed Sets --- pyomo/core/base/set.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 5ba42bbb554..3361cc05fe0 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -589,6 +589,8 @@ def __eq__(self, other): # ranges (or no ranges). We will re-generate non-finite sets to # make sure we get an accurate "finiteness" flag. if hasattr(other, 'isfinite'): + if not other.parent_component().is_constructed(): + return False other_isfinite = other.isfinite() if not other_isfinite: try: From 147202bcc2eaa321c9d3389e9401a862ef5f5776 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 13:08:25 -0700 Subject: [PATCH 0293/3044] Ensure Any sets are fully constructed --- pyomo/core/base/set.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 3361cc05fe0..3106d183b8e 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -4216,6 +4216,7 @@ def __init__(self, **kwds): # accept (and ignore) this value. kwds.setdefault('domain', self) Set.__init__(self, **kwds) + self.construct() def get(self, val, default=None): return val if val is not Ellipsis else default From d9d88ef3c17db3a48794bd800a0861e828f60244 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 13:09:17 -0700 Subject: [PATCH 0294/3044] Now that _implicit_subsets has been removed, we can disable domain on AbstractScalarVar --- pyomo/core/base/var.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index 8d5b93f3ace..40613f78554 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -66,8 +66,7 @@ + [(_, False) for _ in integer_global_set_ids] ) _VARDATA_API = ( - # including 'domain' runs afoul of logic in Block._add_implicit_sets() - # 'domain', + 'domain', 'bounds', 'lower', 'upper', From 032534660755a175422b3d687348648cb74d3f25 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Dec 2023 13:09:35 -0700 Subject: [PATCH 0295/3044] Update benders tests to use common.dependencies, relax dependency on CPLEX --- pyomo/contrib/benders/tests/test_benders.py | 37 ++++++++------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/benders/tests/test_benders.py b/pyomo/contrib/benders/tests/test_benders.py index 26a2a0b7910..f1d4be32494 100644 --- a/pyomo/contrib/benders/tests/test_benders.py +++ b/pyomo/contrib/benders/tests/test_benders.py @@ -10,35 +10,24 @@ # ___________________________________________________________________________ import pyomo.common.unittest as unittest -from pyomo.contrib.benders.benders_cuts import BendersCutGenerator import pyomo.environ as pyo -try: - import mpi4py - - mpi4py_available = True -except: - mpi4py_available = False -try: - import numpy as np - - numpy_available = True -except: - numpy_available = False - +from pyomo.common.dependencies import mpi4py_available, numpy_available +from pyomo.contrib.benders.benders_cuts import BendersCutGenerator -ipopt_opt = pyo.SolverFactory('ipopt') -ipopt_available = ipopt_opt.available(exception_flag=False) +ipopt_available = pyo.SolverFactory('ipopt').available(exception_flag=False) -cplex_opt = pyo.SolverFactory('cplex_direct') -cplex_available = cplex_opt.available(exception_flag=False) +for mip_name in ('cplex_direct', 'gurobi_direct', 'gurobi', 'cplex', 'glpk', 'cbc'): + mip_available = pyo.SolverFactory(mip_name).available(exception_flag=False) + if mip_available: + break @unittest.pytest.mark.mpi class MPITestBenders(unittest.TestCase): @unittest.skipIf(not mpi4py_available, 'mpi4py is not available.') @unittest.skipIf(not numpy_available, 'numpy is not available.') - @unittest.skipIf(not cplex_available, 'cplex is not available.') + @unittest.skipIf(not mip_available, 'MIP solver is not available.') def test_farmer(self): class Farmer(object): def __init__(self): @@ -200,9 +189,9 @@ def EnforceQuotas_rule(m, i): subproblem_fn=create_subproblem, subproblem_fn_kwargs=subproblem_fn_kwargs, root_eta=m.eta[s], - subproblem_solver='cplex_direct', + subproblem_solver=mip_name, ) - opt = pyo.SolverFactory('cplex_direct') + opt = pyo.SolverFactory(mip_name) for i in range(30): res = opt.solve(m, tee=False) @@ -261,7 +250,7 @@ def create_subproblem(root): @unittest.skipIf(not mpi4py_available, 'mpi4py is not available.') @unittest.skipIf(not numpy_available, 'numpy is not available.') - @unittest.skipIf(not cplex_available, 'cplex is not available.') + @unittest.skipIf(not mip_available, 'MIP solver is not available.') def test_four_scen_farmer(self): class FourScenFarmer(object): def __init__(self): @@ -430,9 +419,9 @@ def EnforceQuotas_rule(m, i): subproblem_fn=create_subproblem, subproblem_fn_kwargs=subproblem_fn_kwargs, root_eta=m.eta[s], - subproblem_solver='cplex_direct', + subproblem_solver=mip_name, ) - opt = pyo.SolverFactory('cplex_direct') + opt = pyo.SolverFactory(mip_name) for i in range(30): res = opt.solve(m, tee=False) From 7187fc291dfb59373c4e73f4bf7bff880d9373ed Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 4 Jan 2024 15:50:21 -0700 Subject: [PATCH 0296/3044] Backwards compability: Process legacy options. --- pyomo/solver/base.py | 8 ++++---- pyomo/solver/ipopt.py | 23 +++++++++++++++-------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 48adc44c4d7..48b48db14b9 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -532,8 +532,8 @@ def options(self): class. """ for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: - if hasattr(self, solver_name + '_options'): - return getattr(self, solver_name + '_options') + if hasattr(self, 'solver_options'): + return getattr(self, 'solver_options') raise NotImplementedError('Could not find the correct options') @options.setter @@ -543,8 +543,8 @@ def options(self, val): """ found = False for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: - if hasattr(self, solver_name + '_options'): - setattr(self, solver_name + '_options', val) + if hasattr(self, 'solver_options'): + setattr(self, 'solver_options', val) found = True if not found: raise NotImplementedError('Could not find the correct options') diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index 1b4c0eb36cb..406f4291c44 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -14,7 +14,7 @@ import datetime import io import sys -from typing import Mapping, Optional +from typing import Mapping, Optional, Dict from pyomo.common import Executable from pyomo.common.config import ConfigValue, NonNegativeInt, NonNegativeFloat @@ -188,7 +188,7 @@ def __init__(self, **kwds): self._config = self.CONFIG(kwds) self._writer = NLWriter() self._writer.config.skip_trivial_constraints = True - self.ipopt_options = self._config.solver_options + self._solver_options = self._config.solver_options def available(self): if self.config.executable.path() is None: @@ -216,6 +216,14 @@ def config(self): def config(self, val): self._config = val + @property + def solver_options(self): + return self._solver_options + + @solver_options.setter + def solver_options(self, val: Dict): + self._solver_options = val + @property def symbol_map(self): return self._symbol_map @@ -240,15 +248,14 @@ def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: boo cmd = [str(config.executable), basename + '.nl', '-AMPL'] if opt_file: cmd.append('option_file_name=' + basename + '.opt') - if 'option_file_name' in config.solver_options: + if 'option_file_name' in self.solver_options: raise ValueError( 'Pyomo generates the ipopt options file as part of the solve method. ' 'Add all options to ipopt.config.solver_options instead.' ) - self.ipopt_options = dict(config.solver_options) - if config.time_limit is not None and 'max_cpu_time' not in self.ipopt_options: - self.ipopt_options['max_cpu_time'] = config.time_limit - for k, val in self.ipopt_options.items(): + if config.time_limit is not None and 'max_cpu_time' not in self.solver_options: + self.solver_options['max_cpu_time'] = config.time_limit + for k, val in self.solver_options.items(): if k in ipopt_command_line_options: cmd.append(str(k) + '=' + str(val)) return cmd @@ -309,7 +316,7 @@ def solve(self, model, **kwds): # Write the opt_file, if there should be one; return a bool to say # whether or not we have one (so we can correctly build the command line) opt_file = self._write_options_file( - filename=basename, options=config.solver_options + filename=basename, options=self.solver_options ) # Call ipopt - passing the files via the subprocess cmd = self._create_command_line( From 6fb37ce225905bcd5d59924c6a404142f114b30b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 4 Jan 2024 16:04:27 -0700 Subject: [PATCH 0297/3044] Add new option to legacy interface for forwards compability --- pyomo/solver/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 48b48db14b9..9b52c61f642 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -387,6 +387,7 @@ def solve( options: Optional[Dict] = None, keepfiles: bool = False, symbolic_solver_labels: bool = False, + raise_exception_on_nonoptimal_result: bool = False ): """ Solve method: maps new solve method style to backwards compatible version. @@ -404,6 +405,9 @@ def solve( self.config.symbolic_solver_labels = symbolic_solver_labels self.config.time_limit = timelimit self.config.report_timing = report_timing + # This is a new flag in the interface. To preserve backwards compability, + # its default is set to "False" + self.config.raise_exception_on_nonoptimal_result = raise_exception_on_nonoptimal_result if solver_io is not None: raise NotImplementedError('Still working on this') if suffixes is not None: From a79f34856be419de027f1f59abed357850bfb758 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 4 Jan 2024 16:09:03 -0700 Subject: [PATCH 0298/3044] Apply black --- pyomo/solver/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 9b52c61f642..202b0422cee 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -387,7 +387,7 @@ def solve( options: Optional[Dict] = None, keepfiles: bool = False, symbolic_solver_labels: bool = False, - raise_exception_on_nonoptimal_result: bool = False + raise_exception_on_nonoptimal_result: bool = False, ): """ Solve method: maps new solve method style to backwards compatible version. @@ -407,7 +407,9 @@ def solve( self.config.report_timing = report_timing # This is a new flag in the interface. To preserve backwards compability, # its default is set to "False" - self.config.raise_exception_on_nonoptimal_result = raise_exception_on_nonoptimal_result + self.config.raise_exception_on_nonoptimal_result = ( + raise_exception_on_nonoptimal_result + ) if solver_io is not None: raise NotImplementedError('Still working on this') if suffixes is not None: From 76fee13fae1bd0888dbadee083aa13d3bb437786 Mon Sep 17 00:00:00 2001 From: robbybp Date: Sat, 6 Jan 2024 15:52:52 -0700 Subject: [PATCH 0299/3044] move AMPLRepnVisitor construction into ConfigValue validation --- pyomo/contrib/incidence_analysis/config.py | 95 ++++++++++++++++++- pyomo/contrib/incidence_analysis/incidence.py | 42 ++------ pyomo/contrib/incidence_analysis/interface.py | 85 +++-------------- 3 files changed, 116 insertions(+), 106 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/config.py b/pyomo/contrib/incidence_analysis/config.py index db9accbddc4..31b2bd3fc22 100644 --- a/pyomo/contrib/incidence_analysis/config.py +++ b/pyomo/contrib/incidence_analysis/config.py @@ -13,6 +13,9 @@ import enum from pyomo.common.config import ConfigDict, ConfigValue, InEnum +from pyomo.common.modeling import NOTSET +from pyomo.repn.plugins.nl_writer import AMPLRepnVisitor, AMPLRepn, text_nl_template +from pyomo.repn.util import FileDeterminism, FileDeterminism_to_SortComponents class IncidenceMethod(enum.Enum): @@ -62,7 +65,92 @@ class IncidenceMethod(enum.Enum): ) -IncidenceConfig = ConfigDict() +class _ReconstructVisitor: + pass + + +def _amplrepnvisitor_validator(visitor=_ReconstructVisitor): + # This checks for and returns a valid AMPLRepnVisitor, but I don't want + # to construct this if we're not using IncidenceMethod.ampl_repn. + # It is not necessarily the end of the world if we construct this, however, + # as the code should still work. + if visitor is _ReconstructVisitor: + subexpression_cache = {} + subexpression_order = [] + external_functions = {} + var_map = {} + used_named_expressions = set() + symbolic_solver_labels = False + # TODO: Explore potential performance benefit of exporting defined variables. + # This likely only shows up if we can preserve the subexpression cache across + # multiple constraint expressions. + export_defined_variables = False + sorter = FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) + amplvisitor = AMPLRepnVisitor( + text_nl_template, + subexpression_cache, + subexpression_order, + external_functions, + var_map, + used_named_expressions, + symbolic_solver_labels, + export_defined_variables, + sorter, + ) + elif not isinstance(visitor, AMPLRepnVisitor): + raise TypeError( + "'visitor' config argument should be an instance of AMPLRepnVisitor" + ) + else: + amplvisitor = visitor + return amplvisitor + + +_ampl_repn_visitor = ConfigValue( + default=_ReconstructVisitor, + domain=_amplrepnvisitor_validator, + description="Visitor used to generate AMPLRepn of each constraint", +) + + +class _IncidenceConfigDict(ConfigDict): + + def __call__( + self, + value=NOTSET, + default=NOTSET, + domain=NOTSET, + description=NOTSET, + doc=NOTSET, + visibility=NOTSET, + implicit=NOTSET, + implicit_domain=NOTSET, + preserve_implicit=False, + ): + init_value = value + new = super().__call__( + value=value, + default=default, + domain=domain, + description=description, + doc=doc, + visibility=visibility, + implicit=implicit, + implicit_domain=implicit_domain, + preserve_implicit=preserve_implicit, + ) + + if ( + new.method == IncidenceMethod.ampl_repn + and "ampl_repn_visitor" not in init_value + ): + new.ampl_repn_visitor = _ReconstructVisitor + + return new + + + +IncidenceConfig = _IncidenceConfigDict() """Options for incidence graph generation - ``include_fixed`` -- Flag indicating whether fixed variables should be included @@ -71,6 +159,8 @@ class IncidenceMethod(enum.Enum): should be included. - ``method`` -- Method used to identify incident variables. Must be a value of the ``IncidenceMethod`` enum. +- ``ampl_repn_visitor`` -- Expression visitor used to generate ``AMPLRepn`` of each + constraint. Must be an instance of ``AMPLRepnVisitor``. """ @@ -82,3 +172,6 @@ class IncidenceMethod(enum.Enum): IncidenceConfig.declare("method", _method) + + +IncidenceConfig.declare("ampl_repn_visitor", _ampl_repn_visitor) diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index 62ba7a0aec7..17307e89600 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -80,38 +80,14 @@ def _get_incident_via_standard_repn( return unique_variables -def _get_incident_via_ampl_repn(expr, linear_only, visitor=None): - if visitor is None: - subexpression_cache = {} - subexpression_order = [] - external_functions = {} - var_map = {} - used_named_expressions = set() - symbolic_solver_labels = False - # TODO: Explore potential performance benefit of exporting defined variables. - # This likely only shows up if we can preserve the subexpression cache across - # multiple constraint expressions. - export_defined_variables = False - sorter = FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) - visitor = AMPLRepnVisitor( - text_nl_template, - subexpression_cache, - subexpression_order, - external_functions, - var_map, - used_named_expressions, - symbolic_solver_labels, - export_defined_variables, - sorter, - ) - AMPLRepn.ActiveVisitor = visitor - try: - repn = visitor.walk_expression((expr, None, 0, 1.0)) - finally: - AMPLRepn.ActiveVisitor = None - else: - var_map = visitor.var_map +def _get_incident_via_ampl_repn(expr, linear_only, visitor): + var_map = visitor.var_map + orig_activevisitor = AMPLRepn.ActiveVisitor + AMPLRepn.ActiveVisitor = visitor + try: repn = visitor.walk_expression((expr, None, 0, 1.0)) + finally: + AMPLRepn.ActiveVisitor = orig_activevisitor nonlinear_var_ids = [] if repn.nonlinear is None else repn.nonlinear[1] nonlinear_var_id_set = set() @@ -172,11 +148,11 @@ def get_incident_variables(expr, **kwds): ['x[1]', 'x[2]'] """ - visitor = kwds.pop("visitor", None) config = IncidenceConfig(kwds) method = config.method include_fixed = config.include_fixed linear_only = config.linear_only + amplrepnvisitor = config.ampl_repn_visitor if linear_only and method is IncidenceMethod.identify_variables: raise RuntimeError( "linear_only=True is not supported when using identify_variables" @@ -194,7 +170,7 @@ def get_incident_variables(expr, **kwds): expr, include_fixed, linear_only, compute_values=True ) elif method is IncidenceMethod.ampl_repn: - return _get_incident_via_ampl_repn(expr, linear_only, visitor=visitor) + return _get_incident_via_ampl_repn(expr, linear_only, amplrepnvisitor) else: raise ValueError( f"Unrecognized value {method} for the method used to identify incident" diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index ce5f4780210..b8a6c1275f9 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -93,6 +93,8 @@ def get_bipartite_incidence_graph(variables, constraints, **kwds): ``networkx.Graph`` """ + # Note that this ConfigDict contains the visitor that we will re-use + # when constructing constraints. config = IncidenceConfig(kwds) _check_unindexed(variables + constraints) N = len(variables) @@ -101,38 +103,10 @@ def get_bipartite_incidence_graph(variables, constraints, **kwds): graph.add_nodes_from(range(M), bipartite=0) graph.add_nodes_from(range(M, M + N), bipartite=1) var_node_map = ComponentMap((v, M + i) for i, v in enumerate(variables)) - - if config.method == IncidenceMethod.ampl_repn: - subexpression_cache = {} - subexpression_order = [] - external_functions = {} - var_map = {} - used_named_expressions = set() - symbolic_solver_labels = False - export_defined_variables = False - sorter = FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) - visitor = AMPLRepnVisitor( - text_nl_template, - subexpression_cache, - subexpression_order, - external_functions, - var_map, - used_named_expressions, - symbolic_solver_labels, - export_defined_variables, - sorter, - ) - else: - visitor = None - - AMPLRepn.ActiveVisitor = visitor - try: - for i, con in enumerate(constraints): - for var in get_incident_variables(con.body, visitor=visitor, **config): - if var in var_node_map: - graph.add_edge(i, var_node_map[var]) - finally: - AMPLRepn.ActiveVisitor = None + for i, con in enumerate(constraints): + for var in get_incident_variables(con.body, **config): + if var in var_node_map: + graph.add_edge(i, var_node_map[var]) return graph @@ -193,46 +167,14 @@ def extract_bipartite_subgraph(graph, nodes0, nodes1): def _generate_variables_in_constraints(constraints, **kwds): + # Note: We construct a visitor here config = IncidenceConfig(kwds) - - if config.method == IncidenceMethod.ampl_repn: - subexpression_cache = {} - subexpression_order = [] - external_functions = {} - var_map = {} - used_named_expressions = set() - symbolic_solver_labels = False - export_defined_variables = False - sorter = FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) - visitor = AMPLRepnVisitor( - text_nl_template, - subexpression_cache, - subexpression_order, - external_functions, - var_map, - used_named_expressions, - symbolic_solver_labels, - export_defined_variables, - sorter, - ) - else: - visitor = None - - AMPLRepn.ActiveVisitor = visitor - try: - known_vars = ComponentSet() - for con in constraints: - for var in get_incident_variables(con.body, visitor=visitor, **config): - if var not in known_vars: - known_vars.add(var) - yield var - finally: - # NOTE: I believe this is only guaranteed to be called when the - # generator is garbage collected. This could lead to some nasty - # bug where ActiveVisitor is set for longer than we intend. - # TODO: Convert this into a function. (or yield from variables - # after this try/finally. - AMPLRepn.ActiveVisitor = None + known_vars = ComponentSet() + for con in constraints: + for var in get_incident_variables(con.body, **config): + if var not in known_vars: + known_vars.add(var) + yield var def get_structural_incidence_matrix(variables, constraints, **kwds): @@ -329,7 +271,6 @@ class IncidenceGraphInterface(object): ``evaluate_jacobian_eq`` method instead of ``evaluate_jacobian`` rather than checking constraint expression types. - """ def __init__(self, model=None, active=True, include_inequality=True, **kwds): From e3ddb015059458899c4e1cc492dbe88d08e8a7ad Mon Sep 17 00:00:00 2001 From: robbybp Date: Sat, 6 Jan 2024 15:54:57 -0700 Subject: [PATCH 0300/3044] remove whitespace --- pyomo/contrib/incidence_analysis/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/config.py b/pyomo/contrib/incidence_analysis/config.py index 31b2bd3fc22..036c563ae75 100644 --- a/pyomo/contrib/incidence_analysis/config.py +++ b/pyomo/contrib/incidence_analysis/config.py @@ -114,7 +114,6 @@ def _amplrepnvisitor_validator(visitor=_ReconstructVisitor): class _IncidenceConfigDict(ConfigDict): - def __call__( self, value=NOTSET, @@ -149,7 +148,6 @@ def __call__( return new - IncidenceConfig = _IncidenceConfigDict() """Options for incidence graph generation From 175ea1e808d7ee116902684fe279d7cc461eb667 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 7 Jan 2024 20:27:51 -0700 Subject: [PATCH 0301/3044] Updating baseline to reflect anonymous set changes --- examples/pyomobook/performance-ch/wl.txt | 112 ++++++++++++----------- 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/examples/pyomobook/performance-ch/wl.txt b/examples/pyomobook/performance-ch/wl.txt index fbbd11fa32a..b4f16ac5294 100644 --- a/examples/pyomobook/performance-ch/wl.txt +++ b/examples/pyomobook/performance-ch/wl.txt @@ -3,96 +3,102 @@ Building model 0 seconds to construct Block ConcreteModel; 1 index total 0 seconds to construct Set Any; 1 index total 0 seconds to construct Param P; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0.15 seconds to construct Var x; 40000 indices total + 0.10 seconds to construct Var x; 40000 indices total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total 0 seconds to construct Var y; 200 indices total - 0.26 seconds to construct Objective obj; 1 index total + 0.15 seconds to construct Objective obj; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total - 0.13 seconds to construct Constraint demand; 200 indices total + 0.14 seconds to construct Constraint demand; 200 indices total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0.82 seconds to construct Constraint warehouse_active; 40000 indices total + 0.40 seconds to construct Constraint warehouse_active; 40000 indices total 0 seconds to construct Constraint num_warehouses; 1 index total Building model with LinearExpression ------------------------------------ 0 seconds to construct Block ConcreteModel; 1 index total 0 seconds to construct Set Any; 1 index total 0 seconds to construct Param P; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0.08 seconds to construct Var x; 40000 indices total + 0.16 seconds to construct Var x; 40000 indices total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total 0 seconds to construct Var y; 200 indices total - 0.33 seconds to construct Objective obj; 1 index total + 0.06 seconds to construct Objective obj; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total - 0.13 seconds to construct Constraint demand; 200 indices total + 0.05 seconds to construct Constraint demand; 200 indices total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total + 0 seconds to construct SetOf OrderedSetOf 0 seconds to construct Set OrderedScalarSet; 1 index total 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0 seconds to construct Set SetProduct_OrderedSet; 1 index total - 0.59 seconds to construct Constraint warehouse_active; 40000 indices total + 0.52 seconds to construct Constraint warehouse_active; 40000 indices total 0 seconds to construct Constraint num_warehouses; 1 index total [ 0.00] start -[+ 1.74] Built model -[+ 7.39] Wrote LP file and solved -[+ 11.36] finished parameter sweep - 14919301 function calls (14916699 primitive calls) in 15.948 seconds +[+ 0.84] Built model +[+ 2.56] Wrote LP file and solved +[+ 14.55] Finished parameter sweep + 7371718 function calls (7368022 primitive calls) in 17.474 seconds Ordered by: cumulative time - List reduced from 590 to 15 due to restriction <15> + List reduced from 671 to 15 due to restriction <15> ncalls tottime percall cumtime percall filename:lineno(function) - 1 0.002 0.002 15.948 15.948 /export/home/dlwoodruff/Documents/BookIII/trunk/pyomo/examples/doc/pyomobook/performance-ch/wl.py:112(solve_parametric) - 30 0.007 0.000 15.721 0.524 /export/home/dlwoodruff/software/pyomo/pyomo/opt/base/solvers.py:511(solve) - 30 0.001 0.000 9.150 0.305 /export/home/dlwoodruff/software/pyomo/pyomo/solvers/plugins/solvers/GUROBI.py:191(_presolve) - 30 0.001 0.000 9.149 0.305 /export/home/dlwoodruff/software/pyomo/pyomo/opt/solver/shellcmd.py:188(_presolve) - 30 0.001 0.000 9.134 0.304 /export/home/dlwoodruff/software/pyomo/pyomo/opt/base/solvers.py:651(_presolve) - 30 0.000 0.000 9.133 0.304 /export/home/dlwoodruff/software/pyomo/pyomo/opt/base/solvers.py:719(_convert_problem) - 30 0.002 0.000 9.133 0.304 /export/home/dlwoodruff/software/pyomo/pyomo/opt/base/convert.py:31(convert_problem) - 30 0.001 0.000 9.093 0.303 /export/home/dlwoodruff/software/pyomo/pyomo/solvers/plugins/converter/model.py:43(apply) - 30 0.001 0.000 9.080 0.303 /export/home/dlwoodruff/software/pyomo/pyomo/core/base/block.py:1756(write) - 30 0.008 0.000 9.077 0.303 /export/home/dlwoodruff/software/pyomo/pyomo/repn/plugins/cpxlp.py:81(__call__) - 30 1.308 0.044 9.065 0.302 /export/home/dlwoodruff/software/pyomo/pyomo/repn/plugins/cpxlp.py:377(_print_model_LP) - 30 0.002 0.000 5.016 0.167 /export/home/dlwoodruff/software/pyomo/pyomo/opt/solver/shellcmd.py:223(_apply_solver) - 30 0.002 0.000 5.013 0.167 /export/home/dlwoodruff/software/pyomo/pyomo/opt/solver/shellcmd.py:289(_execute_command) - 30 0.006 0.000 5.011 0.167 /export/home/dlwoodruff/software/pyutilib/pyutilib/subprocess/processmngr.py:433(run_command) - 30 0.001 0.000 4.388 0.146 /export/home/dlwoodruff/software/pyutilib/pyutilib/subprocess/processmngr.py:829(wait) + 1 0.001 0.001 17.474 17.474 /home/jdsiiro/Research/pyomo/examples/pyomobook/performance-ch/wl.py:132(solve_parametric) + 30 0.002 0.000 17.397 0.580 /home/jdsiiro/Research/pyomo/pyomo/opt/base/solvers.py:530(solve) + 30 0.001 0.000 14.176 0.473 /home/jdsiiro/Research/pyomo/pyomo/opt/solver/shellcmd.py:247(_apply_solver) + 30 0.002 0.000 14.173 0.472 /home/jdsiiro/Research/pyomo/pyomo/opt/solver/shellcmd.py:310(_execute_command) + 30 0.001 0.000 14.152 0.472 /projects/sems/install/rhel7-x86_64/pyomo/compiler/python/3.11.6/lib/python3.11/subprocess.py:506(run) + 30 0.000 0.000 14.050 0.468 /projects/sems/install/rhel7-x86_64/pyomo/compiler/python/3.11.6/lib/python3.11/subprocess.py:1165(communicate) + 60 0.000 0.000 14.050 0.234 /projects/sems/install/rhel7-x86_64/pyomo/compiler/python/3.11.6/lib/python3.11/subprocess.py:1259(wait) + 60 0.001 0.000 14.049 0.234 /projects/sems/install/rhel7-x86_64/pyomo/compiler/python/3.11.6/lib/python3.11/subprocess.py:2014(_wait) + 30 0.000 0.000 14.049 0.468 /projects/sems/install/rhel7-x86_64/pyomo/compiler/python/3.11.6/lib/python3.11/subprocess.py:2001(_try_wait) + 30 14.048 0.468 14.048 0.468 {built-in method posix.waitpid} + 30 0.000 0.000 2.147 0.072 /home/jdsiiro/Research/pyomo/pyomo/solvers/plugins/solvers/GUROBI.py:214(_presolve) + 30 0.000 0.000 2.147 0.072 /home/jdsiiro/Research/pyomo/pyomo/opt/solver/shellcmd.py:215(_presolve) + 30 0.000 0.000 2.139 0.071 /home/jdsiiro/Research/pyomo/pyomo/opt/base/solvers.py:687(_presolve) + 30 0.000 0.000 2.138 0.071 /home/jdsiiro/Research/pyomo/pyomo/opt/base/solvers.py:756(_convert_problem) + 30 0.001 0.000 2.138 0.071 /home/jdsiiro/Research/pyomo/pyomo/opt/base/convert.py:27(convert_problem) - 14919301 function calls (14916699 primitive calls) in 15.948 seconds + 7371718 function calls (7368022 primitive calls) in 17.474 seconds Ordered by: internal time - List reduced from 590 to 15 due to restriction <15> + List reduced from 671 to 15 due to restriction <15> ncalls tottime percall cumtime percall filename:lineno(function) - 30 4.381 0.146 4.381 0.146 {built-in method posix.waitpid} - 30 1.308 0.044 9.065 0.302 /export/home/dlwoodruff/software/pyomo/pyomo/repn/plugins/cpxlp.py:377(_print_model_LP) - 76560 0.703 0.000 1.165 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/repn/plugins/cpxlp.py:178(_print_expr_canonical) - 76560 0.682 0.000 0.858 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/repn/standard_repn.py:424(_collect_sum) - 30 0.544 0.018 0.791 0.026 /export/home/dlwoodruff/software/pyomo/pyomo/solvers/plugins/solvers/GUROBI.py:365(process_soln_file) - 76560 0.539 0.000 1.691 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/repn/standard_repn.py:973(_generate_standard_repn) - 306000 0.507 0.000 0.893 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/core/base/set.py:581(bounds) - 30 0.367 0.012 0.367 0.012 {built-in method posix.read} - 76560 0.323 0.000 2.291 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/repn/standard_repn.py:245(generate_standard_repn) - 76560 0.263 0.000 2.923 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/repn/plugins/cpxlp.py:569(constraint_generator) - 225090 0.262 0.000 0.336 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/core/base/constraint.py:228(has_ub) - 153060 0.249 0.000 0.422 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/core/expr/symbol_map.py:82(createSymbol) - 77220 0.220 0.000 0.457 0.000 {built-in method builtins.sorted} - 30 0.201 0.007 0.202 0.007 {built-in method _posixsubprocess.fork_exec} - 153000 0.185 0.000 0.690 0.000 /export/home/dlwoodruff/software/pyomo/pyomo/core/base/var.py:407(ub) + 30 14.048 0.468 14.048 0.468 {built-in method posix.waitpid} + 30 0.324 0.011 2.101 0.070 /home/jdsiiro/Research/pyomo/pyomo/repn/plugins/lp_writer.py:250(write) + 76560 0.278 0.000 0.666 0.000 /home/jdsiiro/Research/pyomo/pyomo/repn/plugins/lp_writer.py:576(write_expression) + 30 0.258 0.009 0.524 0.017 /home/jdsiiro/Research/pyomo/pyomo/solvers/plugins/solvers/GUROBI.py:394(process_soln_file) + 76560 0.230 0.000 0.412 0.000 /home/jdsiiro/Research/pyomo/pyomo/repn/linear.py:664(_before_linear) + 301530 0.128 0.000 0.176 0.000 /home/jdsiiro/Research/pyomo/pyomo/core/expr/symbol_map.py:133(getSymbol) + 30 0.121 0.004 0.196 0.007 /home/jdsiiro/Research/pyomo/pyomo/core/base/PyomoModel.py:461(select) + 77190 0.119 0.000 0.165 0.000 /home/jdsiiro/Research/pyomo/pyomo/solvers/plugins/solvers/GUROBI.py:451() + 30 0.118 0.004 0.290 0.010 /home/jdsiiro/Research/pyomo/pyomo/core/base/PyomoModel.py:337(add_solution) + 30 0.094 0.003 0.094 0.003 {built-in method _posixsubprocess.fork_exec} + 239550 0.083 0.000 0.083 0.000 /home/jdsiiro/Research/pyomo/pyomo/core/base/indexed_component.py:612(__getitem__) + 76530 0.082 0.000 0.109 0.000 /home/jdsiiro/Research/pyomo/pyomo/core/expr/symbol_map.py:63(addSymbol) + 1062470 0.082 0.000 0.082 0.000 {built-in method builtins.id} + 76560 0.075 0.000 0.081 0.000 /home/jdsiiro/Research/pyomo/pyomo/repn/linear.py:834(finalizeResult) + 163050 0.074 0.000 0.131 0.000 /home/jdsiiro/Research/pyomo/pyomo/core/base/var.py:1050(__getitem__) -[ 36.46] Resetting the tic/toc delta timer -Using license file /export/home/dlwoodruff/software/gurobi900/linux64/../lic/gurobi.lic -Academic license - for non-commercial use only -[+ 1.21] finished parameter sweep with persistent interface +[ 0.00] Resetting the tic/toc delta timer +[+ 0.49] Finished parameter sweep with persistent interface From 3c241df82f88b8f1e0e238d2fad7491f2b85f97b Mon Sep 17 00:00:00 2001 From: Arguello Date: Mon, 8 Jan 2024 09:36:35 -0700 Subject: [PATCH 0302/3044] adding testes --- pyomo/contrib/alternative_solutions/obbt.py | 61 +++++++++++++------ .../alternative_solutions/shifted_lp.py | 3 +- .../contrib/alternative_solutions/solnpool.py | 10 +-- .../tests/run_lp_enum.py | 17 +++++- .../alternative_solutions/tests/test_cases.py | 57 ++++++++++++++++- .../alternative_solutions/tests/test_obbt.py | 55 ++++++++++++----- .../tests/test_shifted_lp.py | 52 ++++++++++++++++ .../tests/test_solnpool.py | 17 +++--- 8 files changed, 217 insertions(+), 55 deletions(-) create mode 100644 pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 5f7f057a573..3a1b4f1a8cd 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -11,6 +11,7 @@ import pyomo.environ as pe from pyomo.contrib.alternative_solutions import aos_utils +from pyomo.contrib import appsi import pdb def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, @@ -75,21 +76,32 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, 2 * num_vars)) orig_objective = aos_utils._get_active_objective(model) - opt = pe.SolverFactory(solver) - for parameter, value in solver_options.items(): - opt.options[parameter] = value use_appsi = False if 'appsi' in solver: - use_appsi = True + opt = appsi.solvers.Gurobi() + for parameter, value in solver_options.items(): + opt.gurobi_options[parameter] = var_value + opt.config.stream_solver = tee + results = opt.solve(model) + condition = results.termination_condition + optimal_tc = appsi.base.TerminationCondition.optimal + infeas_or_unbdd_tc = appsi.base.TerminationCondition.infeasibleOrUnbounded + unbdd_tc = appsi.base.TerminationCondition.unbounded + use_appsi = True + else: + opt = pe.SolverFactory(solver) + for parameter, value in solver_options.items(): + opt.options[parameter] = value + results = opt.solve(model, warmstart=warmstart, tee=tee) + condition = results.solver.termination_condition + optimal_tc = pe.TerminationCondition.optimal + infeas_or_unbdd_tc = pe.TerminationCondition.infeasibleOrUnbounded + unbdd_tc = pe.TerminationCondition.unbounded print('Peforming initial solve of model.') - results = opt.solve(model, warmstart=warmstart, tee=tee) - status = results.solver.status - condition = results.solver.termination_condition - if condition != pe.TerminationCondition.optimal: - raise Exception(('OBBT cannot be applied, SolverStatus = {}, ' - 'TerminationCondition = {}').format(status.value, - condition.value)) + if condition != optimal_tc: + raise Exception(('OBBT cannot be applied, ' + 'TerminationCondition = {}').format(condition.value)) if warmstart: _add_solution(solutions) orig_objective_value = pe.value(orig_objective) @@ -143,11 +155,22 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, if use_appsi: opt.update_config.check_for_new_or_removed_constraints = \ new_constraint - results = opt.solve(model, warmstart=warmstart, tee=tee) + if use_appsi: + opt.config.stream_solver = tee + try: + results = opt.solve(model) + condition = results.termination_condition + except: + pass + else: + try: + results = opt.solve(model, warmstart=warmstart, tee=tee) + condition = results.solver.termination_condition + except: + pass new_constraint = False - status = results.solver.status - condition = results.solver.termination_condition - if condition == pe.TerminationCondition.optimal: + + if condition == optimal_tc: if warmstart: _add_solution(solutions) obj_val = pe.value(var) @@ -168,16 +191,16 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, # An infeasibleOrUnbounded status code will imply the problem is # unbounded since feasibility has been established previously - elif (condition == pe.TerminationCondition.infeasibleOrUnbounded or - condition == pe.TerminationCondition.unbounded): + elif (condition == infeas_or_unbdd_tc or + condition == unbdd_tc): if sense == pe.minimize: variable_bounds[var][idx] = float('-inf') else: variable_bounds[var][idx] = float('inf') else: print(('Unexpected condition for the variable {} {} problem.' - 'SolverStatus = {}, TerminationCondition = {}').\ - format(var.name, bound_dir, status.value, + 'TerminationCondition = {}').\ + format(var.name, bound_dir, condition.value)) var_value = variable_bounds[var][idx] diff --git a/pyomo/contrib/alternative_solutions/shifted_lp.py b/pyomo/contrib/alternative_solutions/shifted_lp.py index bfa8ea65374..3c8baee2a48 100644 --- a/pyomo/contrib/alternative_solutions/shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/shifted_lp.py @@ -15,6 +15,7 @@ from pyomo.gdp.util import clone_without_expression_components from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.alternative_solutions import aos_utils +import pdb def _get_unique_name(collection, name): '''Create a unique name for an item that will be added to a collection.''' @@ -45,7 +46,7 @@ def get_shifted_linear_model(model, block=None): s.t. A_1 * x = b_1 A_2 * x <= b_2 - l <= x <= u + l <= x <= uf a problem of the form, diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 5360f66c3d0..9158cb8f838 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -50,12 +50,10 @@ def gurobi_generate_solutions(model, num_solutions=10, rel_opt_gap=None, [Solution] ''' - #opt = pe.SolverFactory('appsi_gurobi') opt = appsi.solvers.Gurobi() for parameter, value in solver_options.items(): opt.gurobi_options[parameter] = value - #opt.options['PoolSolutions'] = num_solutions - #opt.options['PoolSearchMode'] = 2 + opt.gurobi_options['PoolSolutions'] = num_solutions opt.gurobi_options['PoolSearchMode'] = 2 opt.config.stream_solver = tee @@ -63,10 +61,8 @@ def gurobi_generate_solutions(model, num_solutions=10, rel_opt_gap=None, opt.gurobi_options['PoolGap'] = rel_opt_gap if abs_opt_gap is not None: opt.gurobi_options['PoolGapAbs'] = abs_opt_gap - results = opt.solve(model)#, tee=tee) - #status = results.solver.status - status = results.termination_condition - #condition = results.solver.termination_condition + results = opt.solve(model) + condition = results.termination_condition solutions = [] if condition == appsi.base.TerminationCondition.optimal: diff --git a/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py b/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py index cb63a4df7ed..98759a2d3bf 100644 --- a/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py +++ b/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py @@ -7,6 +7,19 @@ import pyomo.contrib.alternative_solutions.tests.test_cases as tc from pyomo.contrib.alternative_solutions import lp_enum +import pyomo.environ as pe +import pdb -m = tc.get_2d_degenerate_lp() -sols = lp_enum.enumerate_linear_solutions(m) \ No newline at end of file +m = tc.get_3d_polyhedron_problem() +m.o.deactivate() +m.obj = pe.Objective(expr = m.x[0] + m.x[1] + m.x[2]) +sols = lp_enum.enumerate_linear_solutions(m, solver='gurobi') + + +n = tc.get_pentagonal_pyramid_mip() +n.o.sense = pe.minimize +n.x.domain = pe.Reals +n.y.domain = pe.Reals +sols = lp_enum.enumerate_linear_solutions(n, solver='gurobi') +n.pprint() +pdb.set_trace() \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index 85715e996aa..c8eb34f2c0a 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -123,6 +123,34 @@ def get_2d_diamond_problem(discrete_x=False, discrete_y=False): return m +def get_3d_polyhedron_problem(): + ''' + Simple 3d polyhedron that is expressed using all types of linear constraints + ''' + m = pe.ConcreteModel() + m.x = pe.Var([0,1,2], within=pe.Reals) + m.x[0].setlb(-1) + m.x[0].setub(1) + m.x[1].setlb(-2) + m.x[1].setub(2) + m.x[2].setlb(1) + m.x[2].setub(2) + + def _constraint_switch_rule(m, i): + if i == 0: + return m.x[0] + m.x[1] <= 2 + elif i == 1: + return -m.x[0] + m.x[1] <= 2 + elif i == 2: + return m.x[0] + m.x[1] >= -2 + elif i == 3: + return -m.x[0] + m.x[1] >= -2 + elif i == 4: + return m.x[0] + m.x[1] + m.x[2] == 4 + m.c = pe.Constraint([i for i in range(5)], rule = _constraint_switch_rule) + + m.o = pe.Objective(expr=m.x[0] + m.x[2], sense=pe.maximize) + return m def get_2d_unbounded_problem(): ''' @@ -142,7 +170,6 @@ def get_2d_unbounded_problem(): m.continuous_bounds = pe.ComponentMap() m.continuous_bounds[m.x] = (float('-inf'), 4) m.continuous_bounds[m.y] = (2, float('inf')) - return m def get_2d_degenerate_lp(): @@ -251,7 +278,7 @@ def get_aos_test_knapsack(var_max, weights, values, capacity_fraction): feasible_sols = sorted(feasible_sols, key=lambda sol: sol[1], reverse=True) return m -def get_hexagonal_pyramid_mip(): +def get_pentagonal_pyramid_mip(): ''' Pentagonal pyramid with integer coordinates in the first two dimensions and a third continuous dimension. @@ -275,7 +302,31 @@ def get_hexagonal_pyramid_mip(): m.num_ranked_solns = [1, 4, 2, 8, 2, 12, 4, 16, 4, 20] return m -def get_bloated_hexagonal_pyramid_mip(): +def get_indexed_pentagonal_pyramid_mip(): + ''' + Pentagonal pyramid with integer coordinates in the first two dimensions and + a third continuous dimension. + + ''' + var_max = 5 + m = pe.ConcreteModel() + m.x = pe.Var([1,2], within=pe.Integers, bounds=(-var_max,var_max)) + m.z = pe.Var(within=pe.NonNegativeReals, bounds=(0,var_max)) + m.o = pe.Objective(expr=m.z, sense=pe.maximize) + base_points = np.array([[0, var_max, 0], [var_max, 0, 0], [var_max/2.0, -var_max, 0], [-var_max/2.0, -var_max, 0], [-var_max, 0, 0]]) + apex_point = np.array([0, 0, var_max]) + + def _con_rule(m, i): + vec_1 = base_points[i] - apex_point + vec_2 = base_points[(i+1) % var_max] - base_points[i] + n = np.cross(vec_1, vec_2) + expr = n[0]*(m.x[1] - apex_point[0]) + n[1]*(m.x[2] - apex_point[1]) + n[2]*(m.z - apex_point[2]) + return expr >= 0 + m.c = pe.Constraint([i for i in range(5)], rule=_con_rule) + m.num_ranked_solns = [1, 4, 2, 8, 2, 12, 4, 16, 4, 20] + return m + +def get_bloated_pentagonal_pyramid_mip(): ''' Pentagonal pyramid with integer coordinates in the first two dimensions and a third continuous dimension. Bounds are artificially widened for obbt testing purposes diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index ea379751707..753f6a254a4 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -18,23 +18,24 @@ import pyomo.contrib.alternative_solutions.tests.test_cases as tc import pdb -mip_solver = 'gurobi' +mip_solver = 'gurobi_appsi' +#mip_solver = 'gurobi' class TestOBBTUnit(unittest.TestCase): #TODO: Add more test cases ''' So far I have added test cases for the feasibility problems, we should test cases - where we put TODO: objective constraints in as well based on the absolute and relative difference. + where we put objective constraints in as well based on the absolute and relative difference. Add a case where bounds are only found for a subset of variables. Try cases where refine_discrete_bounds is set to true to ensure that new constraints are added to refine the bounds. I created the problem get_implied_bound_ip to facilitate this - TODO: Check to see that warm starting works for a MIP and MILP case + Check to see that warm starting works for a MIP and MILP case - TODO: We should also check that warmstarting and refining bounds works for gurobi and appsi_gurobi + We should also check that warmstarting and refining bounds works for gurobi and appsi_gurobi We should pass at least one solver_options to ensure this work (e.g. time limit) @@ -42,7 +43,7 @@ class TestOBBTUnit(unittest.TestCase): ''' - def obbt_continuous(self): + def ttest_obbt_continuous(self): '''Check that the correct bounds are found for a continuous problem.''' m = tc.get_2d_diamond_problem() results = obbt_analysis(m, solver=mip_solver) @@ -50,13 +51,34 @@ def obbt_continuous(self): for var, bounds in results.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - def test_obbt_mip(self): - '''Check that bound tightening only occurs for a subset of variables.''' + def ttest_mip_rel_objective(self): + '''Check that relative mip gap constraints are added for a mip with indexed vars and constraints''' + m = tc.get_indexed_hexagonal_pyramid_mip() + results = obbt_analysis(m, rel_opt_gap=0.5) + self.assertAlmostEqual(m._obbt.optimality_tol_rel.lb, 2.5) + + + def ttest_mip_abs_objective(self): + '''Check that absolute mip gap constraints are added''' + m = tc.get_hexagonal_pyramid_mip() + results = obbt_analysis(m, abs_opt_gap=1.99) + self.assertAlmostEqual(m._obbt.optimality_tol_abs.lb, 3.01) + + def ttest_obbt_warmstart(self): + '''Check that warmstarting works.''' + m = tc.get_2d_diamond_problem() + m.x.value = 0 + m.y.value = 0 + results = obbt_analysis(m, solver=mip_solver, warmstart = True, tee = True) + self.assertEqual(results.keys(), m.continuous_bounds.keys()) + for var, bounds in results.items(): + assert_array_almost_equal(bounds, m.continuous_bounds[var]) + + def ttest_obbt_mip(self): + '''Check that bound tightening only occurs for continuous variables + that can be tightened.''' m = tc.get_bloated_hexagonal_pyramid_mip() - m.x = 0 - m.y = 0 - m.z = 5 - results = obbt_analysis(m, solver=mip_solver, tee = True, warmstart = True) + results = obbt_analysis(m, solver=mip_solver, tee = True) bounds_tightened = False bounds_not_tightned = False for var, bounds in results.items(): @@ -71,7 +93,7 @@ def test_obbt_mip(self): self.assertTrue(bounds_tightened) self.assertTrue(bounds_not_tightened) - def obbt_unbounded(self): + def test_obbt_unbounded(self): '''Check that the correct bounds are found for an unbounded problem.''' m = tc.get_2d_unbounded_problem() results = obbt_analysis(m, solver=mip_solver) @@ -79,7 +101,7 @@ def obbt_unbounded(self): for var, bounds in results.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - def bound_tightening(self): + def ttest_bound_tightening(self): ''' Check that the correct bounds are found for a discrete problem where more restrictive bounds are implied by the constraints.''' @@ -89,10 +111,11 @@ def bound_tightening(self): for var, bounds in results.items(): assert_array_almost_equal(bounds, m.var_bounds[var]) - def bound_refinement(self): + def ttest_bound_refinement(self): ''' Check that the correct bounds are found for a discrete problem where - more restrictive bounds are implied by the constraints.''' + more restrictive bounds are implied by the constraints and constraints + are added.''' m = tc.get_implied_bound_ip() results = obbt_analysis(m, solver=mip_solver, refine_discrete_bounds=True) for var, bounds in results.items(): @@ -101,7 +124,7 @@ def bound_refinement(self): if m.var_bounds[var][1] < var.ub: self.assertTrue(hasattr(m._obbt, var.name + "_ub")) - def obbt_infeasible(self): + def ttest_obbt_infeasible(self): '''Check that code catches cases where the problem is infeasible.''' m = tc.get_2d_diamond_problem() m.infeasible_constraint = pe.Constraint(expr=m.x>=10) diff --git a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py new file mode 100644 index 00000000000..9ca2c1383b3 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py @@ -0,0 +1,52 @@ +from numpy.testing import assert_array_almost_equal + +import pyomo.environ as pe +import pyomo.common.unittest as unittest + +import pyomo.contrib.alternative_solutions.tests.test_cases as tc +from pyomo.contrib.alternative_solutions import shifted_lp +import pdb + + +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +mip_solver = 'gurobi_appsi' +#mip_solver = 'gurobi' + +class TestShiftedIP(unittest.TestCase): + + def mip_abs_objective(self): + '''COMMENT''' + m = tc.get_indexed_hexagonal_pyramid_mip() + m.x.domain = pe.Reals + opt = pe.SolverFactory('gurobi') + old_results = opt.solve(m, tee = True) + old_obj = pe.value(m.o) + new_model = shifted_lp.get_shifted_linear_model(m) + new_results = opt.solve(new_model, tee = True) + new_obj = pe.value(new_model.objective) + self.assertAlmostEqual(old_obj, new_obj) + pdb.set_trace() + + def test_polyhedron(self): + m = tc.get_3d_polyhedron_problem() + opt = pe.SolverFactory('gurobi') + old_results = opt.solve(m, tee = True) + old_obj = pe.value(m.o) + new_model = shifted_lp.get_shifted_linear_model(m) + new_results = opt.solve(new_model, tee = True) + new_obj = pe.value(new_model.objective) + pdb.set_trace() + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index 77c84110f4f..abfc5750ed3 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -41,8 +41,8 @@ class TestSolnPoolUnit(unittest.TestCase): ''' def test_ip_feasibility(self): - ''' - COMMENTS''' + '''Check that the correct number of alternate solutions are found for + each objective value in an ip with known solutions''' m = tc.get_triangle_ip() results = sp.gurobi_generate_solutions(m, 100) objectives = [round(result.objective[1], 2) for result in results] @@ -52,9 +52,10 @@ def test_ip_feasibility(self): def test_mip_feasibility(self): ''' - COMMENTS''' - m = tc.get_hexagonal_pyramid_mip() - results = sp.gurobi_generate_solutions(m, 100) + Check that the correct number of alternate solutions are found for + each objective value in a mip with known solutions''' + m = tc.get_indexed_hexagonal_pyramid_mip() + results = sp.gurobi_generate_solutions(m, 100, tee = True) objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns unique_solns_by_obj = [val for val in Counter(objectives).values()] @@ -62,7 +63,8 @@ def test_mip_feasibility(self): def test_mip_rel_feasibility(self): ''' - COMMENTS''' + Check that relative mip gap constraints are added and the correct + number of alternative solutions are found''' m = tc.get_hexagonal_pyramid_mip() results = sp.gurobi_generate_solutions(m, 100, rel_opt_gap=.2) objectives = [round(result.objective[1], 2) for result in results] @@ -72,7 +74,8 @@ def test_mip_rel_feasibility(self): def test_mip_abs_feasibility(self): ''' - COMMENTS''' + Check that absolute mip gap constraints are added and the correct + number of alternative solutions are found''' m = tc.get_hexagonal_pyramid_mip() results = sp.gurobi_generate_solutions(m, 100, abs_opt_gap=1.99) objectives = [round(result.objective[1], 2) for result in results] From e889912eea041cc4f07337588151fb219d0d2d16 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 9 Jan 2024 12:19:41 -0700 Subject: [PATCH 0303/3044] Added interface to ensure parmest backwards compatiblity. --- .../parmest/examples_deprecated/__init__.py | 10 + .../reaction_kinetics/__init__.py | 10 + .../simple_reaction_parmest_example.py | 118 ++ .../reactor_design/__init__.py | 10 + .../reactor_design/bootstrap_example.py | 60 + .../reactor_design/datarec_example.py | 100 ++ .../reactor_design/leaveNout_example.py | 98 ++ .../likelihood_ratio_example.py | 64 + .../multisensor_data_example.py | 51 + .../parameter_estimation_example.py | 58 + .../reactor_design/reactor_data.csv | 20 + .../reactor_data_multisensor.csv | 20 + .../reactor_data_timeseries.csv | 20 + .../reactor_design/reactor_design.py | 104 ++ .../reactor_design/timeseries_data_example.py | 55 + .../rooney_biegler/__init__.py | 10 + .../rooney_biegler/bootstrap_example.py | 57 + .../likelihood_ratio_example.py | 62 + .../parameter_estimation_example.py | 60 + .../rooney_biegler/rooney_biegler.py | 60 + .../rooney_biegler_with_constraint.py | 63 + .../examples_deprecated/semibatch/__init__.py | 10 + .../semibatch/bootstrap_theta.csv | 101 ++ .../semibatch/obj_at_theta.csv | 1009 ++++++++++++ .../semibatch/parallel_example.py | 57 + .../semibatch/parameter_estimation_example.py | 42 + .../semibatch/scenario_example.py | 52 + .../semibatch/scenarios.csv | 11 + .../semibatch/semibatch.py | 287 ++++ pyomo/contrib/parmest/parmest.py | 101 +- pyomo/contrib/parmest/parmest_deprecated.py | 1366 +++++++++++++++++ pyomo/contrib/parmest/scenariocreator.py | 28 +- .../parmest/scenariocreator_deprecated.py | 166 ++ 33 files changed, 4328 insertions(+), 12 deletions(-) create mode 100644 pyomo/contrib/parmest/examples_deprecated/__init__.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/__init__.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/simple_reaction_parmest_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/__init__.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/bootstrap_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/datarec_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/leaveNout_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/likelihood_ratio_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/multisensor_data_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/parameter_estimation_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data.csv create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_multisensor.csv create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_timeseries.csv create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_design.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/reactor_design/timeseries_data_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/rooney_biegler/__init__.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/rooney_biegler/bootstrap_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/rooney_biegler/likelihood_ratio_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/rooney_biegler/parameter_estimation_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler_with_constraint.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/__init__.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/bootstrap_theta.csv create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/obj_at_theta.csv create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/parallel_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/parameter_estimation_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/scenario_example.py create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/scenarios.csv create mode 100644 pyomo/contrib/parmest/examples_deprecated/semibatch/semibatch.py create mode 100644 pyomo/contrib/parmest/parmest_deprecated.py create mode 100644 pyomo/contrib/parmest/scenariocreator_deprecated.py diff --git a/pyomo/contrib/parmest/examples_deprecated/__init__.py b/pyomo/contrib/parmest/examples_deprecated/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/__init__.py b/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/simple_reaction_parmest_example.py new file mode 100644 index 00000000000..719a930251c --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/simple_reaction_parmest_example.py @@ -0,0 +1,118 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +''' +Example from Y. Bard, "Nonlinear Parameter Estimation", (pg. 124) + +This example shows: +1. How to define the unknown (to be regressed parameters) with an index +2. How to call parmest to only estimate some of the parameters (and fix the rest) + +Code provided by Paul Akula. +''' + +from pyomo.environ import ( + ConcreteModel, + Param, + Var, + PositiveReals, + Objective, + Constraint, + RangeSet, + Expression, + minimize, + exp, + value, +) +import pyomo.contrib.parmest.parmest as parmest + + +def simple_reaction_model(data): + # Create the concrete model + model = ConcreteModel() + + model.x1 = Param(initialize=float(data['x1'])) + model.x2 = Param(initialize=float(data['x2'])) + + # Rate constants + model.rxn = RangeSet(2) + initial_guess = {1: 750, 2: 1200} + model.k = Var(model.rxn, initialize=initial_guess, within=PositiveReals) + + # reaction product + model.y = Expression(expr=exp(-model.k[1] * model.x1 * exp(-model.k[2] / model.x2))) + + # fix all of the regressed parameters + model.k.fix() + + # =================================================================== + # Stage-specific cost computations + def ComputeFirstStageCost_rule(model): + return 0 + + model.FirstStageCost = Expression(rule=ComputeFirstStageCost_rule) + + def AllMeasurements(m): + return (float(data['y']) - m.y) ** 2 + + model.SecondStageCost = Expression(rule=AllMeasurements) + + def total_cost_rule(m): + return m.FirstStageCost + m.SecondStageCost + + model.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) + + return model + + +def main(): + # Data from Table 5.2 in Y. Bard, "Nonlinear Parameter Estimation", (pg. 124) + data = [ + {'experiment': 1, 'x1': 0.1, 'x2': 100, 'y': 0.98}, + {'experiment': 2, 'x1': 0.2, 'x2': 100, 'y': 0.983}, + {'experiment': 3, 'x1': 0.3, 'x2': 100, 'y': 0.955}, + {'experiment': 4, 'x1': 0.4, 'x2': 100, 'y': 0.979}, + {'experiment': 5, 'x1': 0.5, 'x2': 100, 'y': 0.993}, + {'experiment': 6, 'x1': 0.05, 'x2': 200, 'y': 0.626}, + {'experiment': 7, 'x1': 0.1, 'x2': 200, 'y': 0.544}, + {'experiment': 8, 'x1': 0.15, 'x2': 200, 'y': 0.455}, + {'experiment': 9, 'x1': 0.2, 'x2': 200, 'y': 0.225}, + {'experiment': 10, 'x1': 0.25, 'x2': 200, 'y': 0.167}, + {'experiment': 11, 'x1': 0.02, 'x2': 300, 'y': 0.566}, + {'experiment': 12, 'x1': 0.04, 'x2': 300, 'y': 0.317}, + {'experiment': 13, 'x1': 0.06, 'x2': 300, 'y': 0.034}, + {'experiment': 14, 'x1': 0.08, 'x2': 300, 'y': 0.016}, + {'experiment': 15, 'x1': 0.1, 'x2': 300, 'y': 0.006}, + ] + + # ======================================================================= + # Parameter estimation without covariance estimate + # Only estimate the parameter k[1]. The parameter k[2] will remain fixed + # at its initial value + theta_names = ['k[1]'] + pest = parmest.Estimator(simple_reaction_model, data, theta_names) + obj, theta = pest.theta_est() + print(obj) + print(theta) + print() + + # ======================================================================= + # Estimate both k1 and k2 and compute the covariance matrix + theta_names = ['k'] + pest = parmest.Estimator(simple_reaction_model, data, theta_names) + n = 15 # total number of data points used in the objective (y in 15 scenarios) + obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) + print(obj) + print(theta) + print(cov) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/__init__.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/bootstrap_example.py new file mode 100644 index 00000000000..e2d172f34f6 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/bootstrap_example.py @@ -0,0 +1,60 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pandas as pd +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, +) + + +def main(): + # Vars to estimate + theta_names = ["k1", "k2", "k3"] + + # Data + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "reactor_data.csv")) + data = pd.read_csv(file_name) + + # Sum of squared error function + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + # Create an instance of the parmest estimator + pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + # Parameter estimation + obj, theta = pest.theta_est() + + # Parameter estimation with bootstrap resampling + bootstrap_theta = pest.theta_est_bootstrap(50) + + # Plot results + parmest.graphics.pairwise_plot(bootstrap_theta, title="Bootstrap theta") + parmest.graphics.pairwise_plot( + bootstrap_theta, + theta, + 0.8, + ["MVN", "KDE", "Rect"], + title="Bootstrap theta with confidence regions", + ) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/datarec_example.py new file mode 100644 index 00000000000..cfd3891c00e --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/datarec_example.py @@ -0,0 +1,100 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import numpy as np +import pandas as pd +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, +) + +np.random.seed(1234) + + +def reactor_design_model_for_datarec(data): + # Unfix inlet concentration for data rec + model = reactor_design_model(data) + model.caf.fixed = False + + return model + + +def generate_data(): + ### Generate data based on real sv, caf, ca, cb, cc, and cd + sv_real = 1.05 + caf_real = 10000 + ca_real = 3458.4 + cb_real = 1060.8 + cc_real = 1683.9 + cd_real = 1898.5 + + data = pd.DataFrame() + ndata = 200 + # Normal distribution, mean = 3400, std = 500 + data["ca"] = 500 * np.random.randn(ndata) + 3400 + # Random distribution between 500 and 1500 + data["cb"] = np.random.rand(ndata) * 1000 + 500 + # Lognormal distribution + data["cc"] = np.random.lognormal(np.log(1600), 0.25, ndata) + # Triangular distribution between 1000 and 2000 + data["cd"] = np.random.triangular(1000, 1800, 3000, size=ndata) + + data["sv"] = sv_real + data["caf"] = caf_real + + return data + + +def main(): + # Generate data + data = generate_data() + data_std = data.std() + + # Define sum of squared error objective function for data rec + def SSE(model, data): + expr = ( + ((float(data.iloc[0]["ca"]) - model.ca) / float(data_std["ca"])) ** 2 + + ((float(data.iloc[0]["cb"]) - model.cb) / float(data_std["cb"])) ** 2 + + ((float(data.iloc[0]["cc"]) - model.cc) / float(data_std["cc"])) ** 2 + + ((float(data.iloc[0]["cd"]) - model.cd) / float(data_std["cd"])) ** 2 + ) + return expr + + ### Data reconciliation + theta_names = [] # no variables to estimate, use initialized values + + pest = parmest.Estimator(reactor_design_model_for_datarec, data, theta_names, SSE) + + obj, theta, data_rec = pest.theta_est(return_values=["ca", "cb", "cc", "cd", "caf"]) + print(obj) + print(theta) + + parmest.graphics.grouped_boxplot( + data[["ca", "cb", "cc", "cd"]], + data_rec[["ca", "cb", "cc", "cd"]], + group_names=["Data", "Data Rec"], + ) + + ### Parameter estimation using reconciled data + theta_names = ["k1", "k2", "k3"] + data_rec["sv"] = data["sv"] + + pest = parmest.Estimator(reactor_design_model, data_rec, theta_names, SSE) + obj, theta = pest.theta_est() + print(obj) + print(theta) + + theta_real = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} + print(theta_real) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/leaveNout_example.py new file mode 100644 index 00000000000..6952a7fc733 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/leaveNout_example.py @@ -0,0 +1,98 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import numpy as np +import pandas as pd +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, +) + + +def main(): + # Vars to estimate + theta_names = ["k1", "k2", "k3"] + + # Data + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "reactor_data.csv")) + data = pd.read_csv(file_name) + + # Create more data for the example + N = 50 + df_std = data.std().to_frame().transpose() + df_rand = pd.DataFrame(np.random.normal(size=N)) + df_sample = data.sample(N, replace=True).reset_index(drop=True) + data = df_sample + df_rand.dot(df_std) / 10 + + # Sum of squared error function + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + # Create an instance of the parmest estimator + pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + # Parameter estimation + obj, theta = pest.theta_est() + print(obj) + print(theta) + + ### Parameter estimation with 'leave-N-out' + # Example use case: For each combination of data where one data point is left + # out, estimate theta + lNo_theta = pest.theta_est_leaveNout(1) + print(lNo_theta.head()) + + parmest.graphics.pairwise_plot(lNo_theta, theta) + + ### Leave one out/boostrap analysis + # Example use case: leave 25 data points out, run 20 bootstrap samples with the + # remaining points, determine if the theta estimate using the points left out + # is inside or outside an alpha region based on the bootstrap samples, repeat + # 5 times. Results are stored as a list of tuples, see API docs for information. + lNo = 25 + lNo_samples = 5 + bootstrap_samples = 20 + dist = "MVN" + alphas = [0.7, 0.8, 0.9] + + results = pest.leaveNout_bootstrap_test( + lNo, lNo_samples, bootstrap_samples, dist, alphas, seed=524 + ) + + # Plot results for a single value of alpha + alpha = 0.8 + for i in range(lNo_samples): + theta_est_N = results[i][1] + bootstrap_results = results[i][2] + parmest.graphics.pairwise_plot( + bootstrap_results, + theta_est_N, + alpha, + ["MVN"], + title="Alpha: " + str(alpha) + ", " + str(theta_est_N.loc[0, alpha]), + ) + + # Extract the percent of points that are within the alpha region + r = [results[i][1].loc[0, alpha] for i in range(lNo_samples)] + percent_true = sum(r) / len(r) + print(percent_true) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/likelihood_ratio_example.py new file mode 100644 index 00000000000..a0fe6f22305 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/likelihood_ratio_example.py @@ -0,0 +1,64 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import numpy as np +import pandas as pd +from itertools import product +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, +) + + +def main(): + # Vars to estimate + theta_names = ["k1", "k2", "k3"] + + # Data + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "reactor_data.csv")) + data = pd.read_csv(file_name) + + # Sum of squared error function + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + # Create an instance of the parmest estimator + pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + # Parameter estimation + obj, theta = pest.theta_est() + + # Find the objective value at each theta estimate + k1 = [0.8, 0.85, 0.9] + k2 = [1.6, 1.65, 1.7] + k3 = [0.00016, 0.000165, 0.00017] + theta_vals = pd.DataFrame(list(product(k1, k2, k3)), columns=["k1", "k2", "k3"]) + obj_at_theta = pest.objective_at_theta(theta_vals) + + # Run the likelihood ratio test + LR = pest.likelihood_ratio_test(obj_at_theta, obj, [0.8, 0.85, 0.9, 0.95]) + + # Plot results + parmest.graphics.pairwise_plot( + LR, theta, 0.9, title="LR results within 90% confidence region" + ) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/multisensor_data_example.py new file mode 100644 index 00000000000..a92ac626fae --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/multisensor_data_example.py @@ -0,0 +1,51 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pandas as pd +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, +) + + +def main(): + # Parameter estimation using multisensor data + + # Vars to estimate + theta_names = ["k1", "k2", "k3"] + + # Data, includes multiple sensors for ca and cc + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "reactor_data_multisensor.csv")) + data = pd.read_csv(file_name) + + # Sum of squared error function + def SSE_multisensor(model, data): + expr = ( + ((float(data.iloc[0]["ca1"]) - model.ca) ** 2) * (1 / 3) + + ((float(data.iloc[0]["ca2"]) - model.ca) ** 2) * (1 / 3) + + ((float(data.iloc[0]["ca3"]) - model.ca) ** 2) * (1 / 3) + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + ((float(data.iloc[0]["cc1"]) - model.cc) ** 2) * (1 / 2) + + ((float(data.iloc[0]["cc2"]) - model.cc) ** 2) * (1 / 2) + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE_multisensor) + obj, theta = pest.theta_est() + print(obj) + print(theta) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/parameter_estimation_example.py new file mode 100644 index 00000000000..581d3904c04 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/parameter_estimation_example.py @@ -0,0 +1,58 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pandas as pd +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, +) + + +def main(): + # Vars to estimate + theta_names = ["k1", "k2", "k3"] + + # Data + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "reactor_data.csv")) + data = pd.read_csv(file_name) + + # Sum of squared error function + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + # Create an instance of the parmest estimator + pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + # Parameter estimation + obj, theta = pest.theta_est() + + # Assert statements compare parameter estimation (theta) to an expected value + k1_expected = 5.0 / 6.0 + k2_expected = 5.0 / 3.0 + k3_expected = 1.0 / 6000.0 + relative_error = abs(theta["k1"] - k1_expected) / k1_expected + assert relative_error < 0.05 + relative_error = abs(theta["k2"] - k2_expected) / k2_expected + assert relative_error < 0.05 + relative_error = abs(theta["k3"] - k3_expected) / k3_expected + assert relative_error < 0.05 + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data.csv b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data.csv new file mode 100644 index 00000000000..c0695c049c4 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data.csv @@ -0,0 +1,20 @@ +sv,caf,ca,cb,cc,cd +1.06,10010,3407.4,945.4,1717.1,1931.9 +1.11,10010,3631.6,1247.2,1694.1,1960.6 +1.16,10010,3645.3,971.4,1552.3,1898.8 +1.21,10002,3536.2,1225.9,1351.1,1757.0 +1.26,10002,3755.6,1263.8,1562.3,1952.2 +1.30,10007,3598.3,1153.4,1413.4,1903.3 +1.35,10007,3939.0,971.4,1416.9,1794.9 +1.41,10009,4227.9,986.3,1188.7,1821.5 +1.45,10001,4163.1,972.5,1085.6,1908.7 +1.50,10002,3896.3,977.3,1132.9,2080.5 +1.56,10004,3801.6,1040.6,1157.7,1780.0 +1.60,10008,4128.4,1198.6,1150.0,1581.9 +1.66,10002,4385.4,1158.7,970.0,1629.8 +1.70,10007,3960.8,1194.9,1091.2,1835.5 +1.76,10007,4180.8,1244.2,1034.8,1739.5 +1.80,10001,4212.3,1240.7,1010.3,1739.6 +1.85,10004,4200.2,1164.0,931.5,1783.7 +1.90,10009,4748.6,1037.9,1065.9,1685.6 +1.96,10009,4941.3,1038.5,996.0,1855.7 diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_multisensor.csv b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_multisensor.csv new file mode 100644 index 00000000000..9df745a8422 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_multisensor.csv @@ -0,0 +1,20 @@ +sv,caf,ca1,ca2,ca3,cb,cc1,cc2,cd +1.06,10010,3407.4,3363.1,3759.1,945.4,1717.1,1695.1,1931.9 +1.11,10010,3631.6,3345.2,3906.0,1247.2,1694.1,1536.7,1960.6 +1.16,10010,3645.3,3784.9,3301.3,971.4,1552.3,1496.2,1898.8 +1.21,10002,3536.2,3718.3,3678.5,1225.9,1351.1,1549.7,1757.0 +1.26,10002,3755.6,3731.8,3854.7,1263.8,1562.3,1410.1,1952.2 +1.30,10007,3598.3,3751.6,3722.5,1153.4,1413.4,1291.6,1903.3 +1.35,10007,3939.0,3969.5,3827.2,971.4,1416.9,1276.8,1794.9 +1.41,10009,4227.9,3721.3,4046.7,986.3,1188.7,1221.0,1821.5 +1.45,10001,4163.1,4142.7,4512.1,972.5,1085.6,1212.1,1908.7 +1.50,10002,3896.3,3953.7,4028.0,977.3,1132.9,1167.7,2080.5 +1.56,10004,3801.6,4263.3,4015.3,1040.6,1157.7,1236.5,1780.0 +1.60,10008,4128.4,4061.1,3914.8,1198.6,1150.0,1032.2,1581.9 +1.66,10002,4385.4,4344.7,4006.8,1158.7,970.0,1155.1,1629.8 +1.70,10007,3960.8,4259.1,4274.7,1194.9,1091.2,958.6,1835.5 +1.76,10007,4180.8,4071.1,4598.7,1244.2,1034.8,1086.8,1739.5 +1.80,10001,4212.3,4541.8,4440.0,1240.7,1010.3,920.8,1739.6 +1.85,10004,4200.2,4444.9,4667.2,1164.0,931.5,850.7,1783.7 +1.90,10009,4748.6,4813.4,4753.2,1037.9,1065.9,898.5,1685.6 +1.96,10009,4941.3,4511.8,4405.4,1038.5,996.0,921.9,1855.7 diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_timeseries.csv b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_timeseries.csv new file mode 100644 index 00000000000..1421cfef6a0 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_timeseries.csv @@ -0,0 +1,20 @@ +experiment,time,sv,caf,ca,cb,cc,cd +0,18000,1.075,10008,3537.5,1077.2,1591.2,1938.7 +0,18060,1.121,10002,3547.7,1186.2,1766.3,1946.9 +0,18120,1.095,10005,3614.4,1009.9,1702.9,1841.8 +0,18180,1.102,10007,3443.7,863.1,1666.2,1918.7 +0,18240,1.105,10002,3687.1,1052.1,1501.7,1905.0 +0,18300,1.084,10008,3452.7,1000.5,1512.0,2043.4 +1,18360,1.159,10009,3427.8,1133.1,1481.1,1837.1 +1,18420,1.432,10010,4029.8,1058.8,1213.0,1911.1 +1,18480,1.413,10005,3953.1,960.1,1304.8,1754.3 +1,18540,1.475,10008,4034.8,1121.2,1351.0,1992.0 +1,18600,1.433,10002,4029.8,1100.6,1199.5,1713.9 +1,18660,1.488,10006,3972.8,1148.0,1380.7,1992.1 +1,18720,1.456,10003,4031.2,1145.2,1133.1,1812.6 +2,18780,1.821,10008,4499.1,980.8,924.7,1840.9 +2,18840,1.856,10005,4370.9,1000.7,833.4,1848.4 +2,18900,1.846,10002,4438.6,1038.6,1042.8,1703.3 +2,18960,1.852,10002,4468.4,1151.8,1119.1,1564.8 +2,19020,1.865,10009,4341.6,1060.5,844.2,1974.8 +2,19080,1.872,10002,4427.0,964.6,840.2,1928.5 diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_design.py new file mode 100644 index 00000000000..16f65e236eb --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_design.py @@ -0,0 +1,104 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +""" +Continuously stirred tank reactor model, based on +pyomo/examples/doc/pyomobook/nonlinear-ch/react_design/ReactorDesign.py +""" +import pandas as pd +from pyomo.environ import ( + ConcreteModel, + Param, + Var, + PositiveReals, + Objective, + Constraint, + maximize, + SolverFactory, +) + + +def reactor_design_model(data): + # Create the concrete model + model = ConcreteModel() + + # Rate constants + model.k1 = Param(initialize=5.0 / 6.0, within=PositiveReals, mutable=True) # min^-1 + model.k2 = Param(initialize=5.0 / 3.0, within=PositiveReals, mutable=True) # min^-1 + model.k3 = Param( + initialize=1.0 / 6000.0, within=PositiveReals, mutable=True + ) # m^3/(gmol min) + + # Inlet concentration of A, gmol/m^3 + if isinstance(data, dict) or isinstance(data, pd.Series): + model.caf = Param(initialize=float(data["caf"]), within=PositiveReals) + elif isinstance(data, pd.DataFrame): + model.caf = Param(initialize=float(data.iloc[0]["caf"]), within=PositiveReals) + else: + raise ValueError("Unrecognized data type.") + + # Space velocity (flowrate/volume) + if isinstance(data, dict) or isinstance(data, pd.Series): + model.sv = Param(initialize=float(data["sv"]), within=PositiveReals) + elif isinstance(data, pd.DataFrame): + model.sv = Param(initialize=float(data.iloc[0]["sv"]), within=PositiveReals) + else: + raise ValueError("Unrecognized data type.") + + # Outlet concentration of each component + model.ca = Var(initialize=5000.0, within=PositiveReals) + model.cb = Var(initialize=2000.0, within=PositiveReals) + model.cc = Var(initialize=2000.0, within=PositiveReals) + model.cd = Var(initialize=1000.0, within=PositiveReals) + + # Objective + model.obj = Objective(expr=model.cb, sense=maximize) + + # Constraints + model.ca_bal = Constraint( + expr=( + 0 + == model.sv * model.caf + - model.sv * model.ca + - model.k1 * model.ca + - 2.0 * model.k3 * model.ca**2.0 + ) + ) + + model.cb_bal = Constraint( + expr=(0 == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb) + ) + + model.cc_bal = Constraint(expr=(0 == -model.sv * model.cc + model.k2 * model.cb)) + + model.cd_bal = Constraint( + expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) + ) + + return model + + +def main(): + # For a range of sv values, return ca, cb, cc, and cd + results = [] + sv_values = [1.0 + v * 0.05 for v in range(1, 20)] + caf = 10000 + for sv in sv_values: + model = reactor_design_model(pd.DataFrame(data={"caf": [caf], "sv": [sv]})) + solver = SolverFactory("ipopt") + solver.solve(model) + results.append([sv, caf, model.ca(), model.cb(), model.cc(), model.cd()]) + + results = pd.DataFrame(results, columns=["sv", "caf", "ca", "cb", "cc", "cd"]) + print(results) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples_deprecated/reactor_design/timeseries_data_example.py new file mode 100644 index 00000000000..da2ab1874c9 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/reactor_design/timeseries_data_example.py @@ -0,0 +1,55 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pandas as pd +from os.path import join, abspath, dirname + +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, +) + + +def main(): + # Parameter estimation using timeseries data + + # Vars to estimate + theta_names = ['k1', 'k2', 'k3'] + + # Data, includes multiple sensors for ca and cc + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, 'reactor_data_timeseries.csv')) + data = pd.read_csv(file_name) + + # Group time series data into experiments, return the mean value for sv and caf + # Returns a list of dictionaries + data_ts = parmest.group_data(data, 'experiment', ['sv', 'caf']) + + def SSE_timeseries(model, data): + expr = 0 + for val in data['ca']: + expr = expr + ((float(val) - model.ca) ** 2) * (1 / len(data['ca'])) + for val in data['cb']: + expr = expr + ((float(val) - model.cb) ** 2) * (1 / len(data['cb'])) + for val in data['cc']: + expr = expr + ((float(val) - model.cc) ** 2) * (1 / len(data['cc'])) + for val in data['cd']: + expr = expr + ((float(val) - model.cd) ** 2) * (1 / len(data['cd'])) + return expr + + pest = parmest.Estimator(reactor_design_model, data_ts, theta_names, SSE_timeseries) + obj, theta = pest.theta_est() + print(obj) + print(theta) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/__init__.py b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/bootstrap_example.py new file mode 100644 index 00000000000..f686bbd933d --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/bootstrap_example.py @@ -0,0 +1,57 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pandas as pd +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( + rooney_biegler_model, +) + + +def main(): + # Vars to estimate + theta_names = ['asymptote', 'rate_constant'] + + # Data + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + # Sum of squared error function + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index + ) + return expr + + # Create an instance of the parmest estimator + pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) + + # Parameter estimation + obj, theta = pest.theta_est() + + # Parameter estimation with bootstrap resampling + bootstrap_theta = pest.theta_est_bootstrap(50, seed=4581) + + # Plot results + parmest.graphics.pairwise_plot(bootstrap_theta, title='Bootstrap theta') + parmest.graphics.pairwise_plot( + bootstrap_theta, + theta, + 0.8, + ['MVN', 'KDE', 'Rect'], + title='Bootstrap theta with confidence regions', + ) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/likelihood_ratio_example.py new file mode 100644 index 00000000000..5e54a33abda --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/likelihood_ratio_example.py @@ -0,0 +1,62 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import numpy as np +import pandas as pd +from itertools import product +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( + rooney_biegler_model, +) + + +def main(): + # Vars to estimate + theta_names = ['asymptote', 'rate_constant'] + + # Data + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + # Sum of squared error function + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index + ) + return expr + + # Create an instance of the parmest estimator + pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) + + # Parameter estimation + obj, theta = pest.theta_est() + + # Find the objective value at each theta estimate + asym = np.arange(10, 30, 2) + rate = np.arange(0, 1.5, 0.1) + theta_vals = pd.DataFrame( + list(product(asym, rate)), columns=['asymptote', 'rate_constant'] + ) + obj_at_theta = pest.objective_at_theta(theta_vals) + + # Run the likelihood ratio test + LR = pest.likelihood_ratio_test(obj_at_theta, obj, [0.8, 0.85, 0.9, 0.95]) + + # Plot results + parmest.graphics.pairwise_plot( + LR, theta, 0.8, title='LR results within 80% confidence region' + ) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/parameter_estimation_example.py new file mode 100644 index 00000000000..9af33217fe4 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/parameter_estimation_example.py @@ -0,0 +1,60 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pandas as pd +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( + rooney_biegler_model, +) + + +def main(): + # Vars to estimate + theta_names = ['asymptote', 'rate_constant'] + + # Data + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + # Sum of squared error function + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index + ) + return expr + + # Create an instance of the parmest estimator + pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) + + # Parameter estimation and covariance + n = 6 # total number of data points used in the objective (y in 6 scenarios) + obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) + + # Plot theta estimates using a multivariate Gaussian distribution + parmest.graphics.pairwise_plot( + (theta, cov, 100), + theta_star=theta, + alpha=0.8, + distributions=['MVN'], + title='Theta estimates within 80% confidence region', + ) + + # Assert statements compare parameter estimation (theta) to an expected value + relative_error = abs(theta['asymptote'] - 19.1426) / 19.1426 + assert relative_error < 0.01 + relative_error = abs(theta['rate_constant'] - 0.5311) / 0.5311 + assert relative_error < 0.01 + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler.py new file mode 100644 index 00000000000..5a0e1238e85 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler.py @@ -0,0 +1,60 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +""" +Rooney Biegler model, based on Rooney, W. C. and Biegler, L. T. (2001). Design for +model parameter uncertainty using nonlinear confidence regions. AIChE Journal, +47(8), 1794-1804. +""" + +import pandas as pd +import pyomo.environ as pyo + + +def rooney_biegler_model(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Var(initialize=15) + model.rate_constant = pyo.Var(initialize=0.5) + + def response_rule(m, h): + expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + def SSE_rule(m): + return sum( + (data.y[i] - m.response_function[data.hour[i]]) ** 2 for i in data.index + ) + + model.SSE = pyo.Objective(rule=SSE_rule, sense=pyo.minimize) + + return model + + +def main(): + # These were taken from Table A1.4 in Bates and Watts (1988). + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + model = rooney_biegler_model(data) + solver = pyo.SolverFactory('ipopt') + solver.solve(model) + + print('asymptote = ', model.asymptote()) + print('rate constant = ', model.rate_constant()) + + +if __name__ == '__main__': + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler_with_constraint.py new file mode 100644 index 00000000000..2582e3fe928 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler_with_constraint.py @@ -0,0 +1,63 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +""" +Rooney Biegler model, based on Rooney, W. C. and Biegler, L. T. (2001). Design for +model parameter uncertainty using nonlinear confidence regions. AIChE Journal, +47(8), 1794-1804. +""" + +import pandas as pd +import pyomo.environ as pyo + + +def rooney_biegler_model_with_constraint(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Var(initialize=15) + model.rate_constant = pyo.Var(initialize=0.5) + model.response_function = pyo.Var(data.hour, initialize=0.0) + + # changed from expression to constraint + def response_rule(m, h): + return m.response_function[h] == m.asymptote * ( + 1 - pyo.exp(-m.rate_constant * h) + ) + + model.response_function_constraint = pyo.Constraint(data.hour, rule=response_rule) + + def SSE_rule(m): + return sum( + (data.y[i] - m.response_function[data.hour[i]]) ** 2 for i in data.index + ) + + model.SSE = pyo.Objective(rule=SSE_rule, sense=pyo.minimize) + + return model + + +def main(): + # These were taken from Table A1.4 in Bates and Watts (1988). + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + model = rooney_biegler_model_with_constraint(data) + solver = pyo.SolverFactory('ipopt') + solver.solve(model) + + print('asymptote = ', model.asymptote()) + print('rate constant = ', model.rate_constant()) + + +if __name__ == '__main__': + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/__init__.py b/pyomo/contrib/parmest/examples_deprecated/semibatch/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/bootstrap_theta.csv b/pyomo/contrib/parmest/examples_deprecated/semibatch/bootstrap_theta.csv new file mode 100644 index 00000000000..29923a782c5 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/bootstrap_theta.csv @@ -0,0 +1,101 @@ +,k1,k2,E1,E2 +0,23.8359813557911,149.99999125263844,31164.260824269295,41489.69422529956 +1,19.251987486659512,105.3374117880675,30505.86059307485,40516.897897740404 +2,19.31940450911214,105.78105886426505,30509.636888745794,40539.53548872927 +3,8.754357429283429,149.99988037658665,28334.500331107014,41482.01554893696 +4,23.016722464092286,80.03743984792878,31091.61503716734,39770.08415278276 +5,6.612337410520649,140.0259411600077,27521.46259880474,41302.159495413296 +6,14.29348509961158,147.0817016641302,29605.749859593245,41443.12009807534 +7,14.152069480386153,149.9914759675382,29676.633227079245,41483.41029455195 +8,19.081046896092914,125.55106106390114,30586.57857977985,41005.60243351924 +9,3.063566173952205,149.9999684548014,25473.079370305273,41483.426370389796 +10,17.79494440791066,108.52425726918327,30316.710830136202,40618.63715404914 +11,97.307579412204,149.99998972597675,35084.37589956093,41485.835559276136 +12,20.793042577945116,91.124365144131,30782.17494940993,40138.215713547994 +13,12.740540794730641,89.86327635412908,29396.65520336387,40086.14665722912 +14,6.930810780299319,149.99999327266906,27667.0240033497,41480.188987754496 +15,20.29404799567638,101.07539817765885,30697.087258737916,40443.316578889426 +16,85.77501788788223,149.99996482096984,34755.77375009206,41499.23448818336 +17,24.13150325243255,77.06876294766496,31222.03914354306,39658.418332258894 +18,16.026645517712712,149.99993094015056,30015.46332620076,41490.69892111652 +19,31.020018442708537,62.11558789585982,31971.311996398897,39089.828285017575 +20,20.815008037484656,87.35968459422139,30788.643843293,40007.78137819648 +21,19.007148519616447,96.44320176694993,30516.36933261116,40284.23312198372 +22,22.232021812057308,89.71692873746096,30956.252845068626,40095.009765519 +23,16.830765834427297,120.65209863104229,30139.92208332896,40912.673450399234 +24,15.274799190396566,129.82767733073857,29780.055282261117,41078.04749417758 +25,22.37343657709118,82.32861355430458,31013.57952062852,39853.06284595207 +26,9.055694749134819,149.99987339406314,28422.482259116612,41504.97564187301 +27,19.909770949417275,86.5634026379812,30705.60369894775,39996.134938503914 +28,20.604557306290886,87.96473948102359,30786.467003867263,40051.28176004557 +29,21.94101237923462,88.18216423767153,30942.372558158557,40051.20357069738 +30,3.200663718121338,149.99997712051055,25472.46099917771,41450.884180452646 +31,20.5812467558026,86.36098672832426,30802.74421085271,40010.76777825347 +32,18.776139793586893,108.99943042186453,30432.474809193136,40641.48011315501 +33,17.14246930769276,112.29370332257908,30164.332101438307,40684.867629869856 +34,20.52146255576043,99.7078140453859,30727.90573864389,40401.20730725967 +35,17.05073306185531,66.00385439687035,30257.075479935145,39247.26647870223 +36,7.1238843213074015,51.05163218895348,27811.250260416655,38521.11199236329 +37,10.54291332571747,76.74902426944477,28763.52244085013,39639.92644514267 +38,16.329028964122656,107.60037882134996,30073.5111433796,40592.825374177235 +39,18.0923131790489,107.75659679748213,30355.62290415686,40593.10521263782 +40,15.477264179087811,149.99995828085014,29948.62617372307,41490.770726165414 +41,23.190670255199933,76.5654091811839,31107.96477489951,39635.650879492074 +42,20.34720227734719,90.07051780196629,30716.131795936217,40096.932765428995 +43,23.60627359054596,80.0847207027996,31130.449736501876,39756.06693747353 +44,22.54968153535252,83.72995448206636,31038.51932262643,39906.60181934743 +45,24.951320839961582,67.97010976959977,31356.00147390564,39307.75709154711 +46,61.216667588824386,149.9999967830529,33730.22100500659,41474.80665231048 +47,9.797300324197744,136.33054557076974,28588.83540859912,41222.22413163186 +48,21.75078861615545,139.82641444329093,30894.847060525986,41290.16131583715 +49,21.76324066920255,99.57885291658233,30860.292260186063,40386.00605205238 +50,20.244262248110417,86.2553098058883,30742.054735645124,39981.83946305757 +51,21.859217291379004,72.89837327878459,30999.703939831277,39514.23768439393 +52,20.902111153308944,88.36862895882298,30782.76240691508,40033.44884393017 +53,59.58504995089654,149.9999677447201,33771.647879014425,41496.69202917452 +54,21.63994234351529,80.9641923004028,30933.578583737795,39809.523930207484 +55,9.804873383156298,149.9995892138235,28729.93818644509,41500.94496844104 +56,9.517359502437172,149.99308840029815,28505.329315103318,41470.65218792529 +57,19.923610217578116,88.23847592895486,30636.024864041487,40020.79650218989 +58,20.366495228182394,85.1991151089578,30752.560133063143,39947.719888972904 +59,12.242715793208157,149.99998097746882,29308.42752633667,41512.25071862387 +60,19.677765799324447,97.30674967097808,30618.37668428642,40323.0499230797 +61,19.03651315222424,109.20775378637025,30455.39615515442,40614.722801684395 +62,21.37660531151217,149.99999616215425,30806.121697474813,41479.3976433347 +63,21.896838392882998,86.86206456282005,30918.823491874144,39986.262281131254 +64,5.030122322262226,149.99991736085678,26792.302062236955,41480.579525893794 +65,17.851755694421776,53.33521102556455,30419.017295420916,38644.47349861614 +66,20.963796542255896,90.72302887846234,30795.751244616677,40114.19163802526 +67,23.082992539267945,77.24345020180209,31107.07485019312,39665.22410226011 +68,18.953050386839383,90.80802949182345,30529.280393040182,40113.73467038244 +69,20.710937910951355,83.16996057131982,30805.892332796295,39876.270184728084 +70,18.18549080794899,65.72657652078952,30416.294615296756,39223.21339606898 +71,12.147892028456324,45.12945045196771,29302.888575028635,38194.144730342545 +72,4.929663537166405,133.89086200105797,26635.8524254091,41163.82082194103 +73,20.512731504598662,106.98199797354127,30660.67479570742,40560.70063653076 +74,21.006700520199008,93.35471748418676,30761.272887418058,40178.10564855804 +75,19.73635577733317,98.75362910260881,30599.64039254174,40346.31274388047 +76,3.6393630101175565,149.99998305638113,25806.925407145678,41446.42489819377 +77,14.430958212981363,149.9999928114441,29710.277666486683,41478.96029884101 +78,21.138173237661093,90.73414659450283,30833.36092609432,40128.61898313504 +79,19.294823672883208,104.69324605284973,30510.371654343133,40510.84889949937 +80,2.607050470695225,69.22680095813037,25000.001468502505,39333.142090801295 +81,16.949842823156228,118.76691429120146,30074.04126731665,40824.66852388976 +82,21.029588811317897,95.27115352081795,30770.753828753943,40243.47156167542 +83,18.862418349044077,111.08370690591005,30421.17882623639,40670.941374189555 +84,24.708015660945147,76.24225941680999,31286.7038829574,39632.545034540664 +85,21.58937721477476,92.6329553952883,30871.989108388123,40181.7478528116 +86,21.091322126816706,96.07721666941696,30765.91144819689,40265.321194575095 +87,19.337815749868728,96.50567420686403,30604.551156564357,40318.12321325275 +88,17.77732130279279,108.5062535737451,30287.456682982094,40602.76307166587 +89,15.259532609396405,134.79914728383426,29793.69015375863,41199.11159557717 +90,21.616910309091583,90.65235108674251,30848.137718134392,40096.0776408459 +91,3.3372937891220475,149.99991062247588,25630.388452101062,41483.30064805118 +92,20.652437906744403,97.86062128528714,30747.864718937744,40330.11871286893 +93,22.134113060054425,73.68464943802763,31013.225174702933,39535.65213713519 +94,20.297310066178802,93.79207093658654,30684.309981457223,40191.747572763874 +95,6.007958386675472,149.99997175883215,27126.707007542,41465.75099589974 +96,16.572749402536758,40.75746000309888,30154.396795028595,37923.85448825053 +97,21.235697111801056,98.97798760165126,30807.097617165928,40373.550932032136 +98,20.10350615639414,96.19608053749371,30632.029399836003,40258.3813340696 +99,18.274272179970747,96.49060573948069,30456.872524151822,40305.258325587834 diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/obj_at_theta.csv b/pyomo/contrib/parmest/examples_deprecated/semibatch/obj_at_theta.csv new file mode 100644 index 00000000000..79f03e07dcd --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/obj_at_theta.csv @@ -0,0 +1,1009 @@ +,k1,k2,E1,E2,obj +0,4,40,29000,38000,667.4023645794207 +1,4,40,29000,38500,665.8312183437167 +2,4,40,29000,39000,672.7539769993407 +3,4,40,29000,39500,684.9503752463216 +4,4,40,29000,40000,699.985589093255 +5,4,40,29000,40500,716.1241770970677 +6,4,40,29000,41000,732.2023201586336 +7,4,40,29000,41500,747.4931745925483 +8,4,40,29500,38000,907.4405527163311 +9,4,40,29500,38500,904.2229271927299 +10,4,40,29500,39000,907.6942345285257 +11,4,40,29500,39500,915.4570013614677 +12,4,40,29500,40000,925.65401444575 +13,4,40,29500,40500,936.9348578520337 +14,4,40,29500,41000,948.3759339765711 +15,4,40,29500,41500,959.386491783636 +16,4,40,30000,38000,1169.8685711377334 +17,4,40,30000,38500,1166.2211505723928 +18,4,40,30000,39000,1167.702295374574 +19,4,40,30000,39500,1172.5517020611685 +20,4,40,30000,40000,1179.3820406408263 +21,4,40,30000,40500,1187.1698633839655 +22,4,40,30000,41000,1195.2047840919602 +23,4,40,30000,41500,1203.0241101248102 +24,4,40,30500,38000,1445.9591944684807 +25,4,40,30500,38500,1442.6632745483 +26,4,40,30500,39000,1443.1982444457385 +27,4,40,30500,39500,1446.2833842279929 +28,4,40,30500,40000,1450.9012120934779 +29,4,40,30500,40500,1456.295140290636 +30,4,40,30500,41000,1461.9350767569827 +31,4,40,30500,41500,1467.4715014446226 +32,4,40,31000,38000,1726.8744994061449 +33,4,40,31000,38500,1724.2679845375048 +34,4,40,31000,39000,1724.4550886870552 +35,4,40,31000,39500,1726.5124587129135 +36,4,40,31000,40000,1729.7061680616455 +37,4,40,31000,40500,1733.48893482641 +38,4,40,31000,41000,1737.4753558920438 +39,4,40,31000,41500,1741.4093763605517 +40,4,40,31500,38000,2004.1978135112938 +41,4,40,31500,38500,2002.2807839860222 +42,4,40,31500,39000,2002.3676405166086 +43,4,40,31500,39500,2003.797808439923 +44,4,40,31500,40000,2006.048051591001 +45,4,40,31500,40500,2008.7281679153625 +46,4,40,31500,41000,2011.5626384878237 +47,4,40,31500,41500,2014.3675286347284 +48,4,80,29000,38000,845.8197358579285 +49,4,80,29000,38500,763.5039795545781 +50,4,80,29000,39000,709.8529964173656 +51,4,80,29000,39500,679.4215539491266 +52,4,80,29000,40000,666.4876088521157 +53,4,80,29000,40500,665.978271760966 +54,4,80,29000,41000,673.7240200504901 +55,4,80,29000,41500,686.4763909417914 +56,4,80,29500,38000,1042.519415429413 +57,4,80,29500,38500,982.8097210678039 +58,4,80,29500,39000,942.2990207573541 +59,4,80,29500,39500,917.9550916645245 +60,4,80,29500,40000,906.3116029967189 +61,4,80,29500,40500,904.0326666308792 +62,4,80,29500,41000,908.1964630052729 +63,4,80,29500,41500,916.4222043837499 +64,4,80,30000,38000,1271.1030403496538 +65,4,80,30000,38500,1227.7527550544085 +66,4,80,30000,39000,1197.433957624904 +67,4,80,30000,39500,1178.447676126182 +68,4,80,30000,40000,1168.645219243497 +69,4,80,30000,40500,1165.7995210546096 +70,4,80,30000,41000,1167.8586496250396 +71,4,80,30000,41500,1173.0949214020527 +72,4,80,30500,38000,1520.8220402652044 +73,4,80,30500,38500,1489.2563260709424 +74,4,80,30500,39000,1466.8099189128857 +75,4,80,30500,39500,1452.4352624958806 +76,4,80,30500,40000,1444.7074679423818 +77,4,80,30500,40500,1442.0820578624343 +78,4,80,30500,41000,1443.099006489627 +79,4,80,30500,41500,1446.5106517200784 +80,4,80,31000,38000,1781.149136032395 +81,4,80,31000,38500,1758.2414369536502 +82,4,80,31000,39000,1741.891639711003 +83,4,80,31000,39500,1731.358661496594 +84,4,80,31000,40000,1725.6231647999593 +85,4,80,31000,40500,1723.5757174297378 +86,4,80,31000,41000,1724.1680229486278 +87,4,80,31000,41500,1726.5050840601884 +88,4,80,31500,38000,2042.8335948845602 +89,4,80,31500,38500,2026.3067503042414 +90,4,80,31500,39000,2014.5720701940838 +91,4,80,31500,39500,2007.0463766643977 +92,4,80,31500,40000,2002.9647983728314 +93,4,80,31500,40500,2001.5163951989875 +94,4,80,31500,41000,2001.9474217001339 +95,4,80,31500,41500,2003.6204088755821 +96,4,120,29000,38000,1176.0713512305115 +97,4,120,29000,38500,1016.8213383282462 +98,4,120,29000,39000,886.0136231565133 +99,4,120,29000,39500,789.0101180066036 +100,4,120,29000,40000,724.5420056133441 +101,4,120,29000,40500,686.6877602625062 +102,4,120,29000,41000,668.8129085873959 +103,4,120,29000,41500,665.1167761036883 +104,4,120,29500,38000,1263.887274509128 +105,4,120,29500,38500,1155.6528408872423 +106,4,120,29500,39000,1066.393539894248 +107,4,120,29500,39500,998.9931006471243 +108,4,120,29500,40000,952.36314487701 +109,4,120,29500,40500,923.4000293372077 +110,4,120,29500,41000,908.407361383214 +111,4,120,29500,41500,903.8136176328255 +112,4,120,30000,38000,1421.1418235449091 +113,4,120,30000,38500,1347.114022652679 +114,4,120,30000,39000,1285.686103704643 +115,4,120,30000,39500,1238.2456448658272 +116,4,120,30000,40000,1204.3526810790904 +117,4,120,30000,40500,1182.4272879027071 +118,4,120,30000,41000,1170.3447810121902 +119,4,120,30000,41500,1165.8422968073423 +120,4,120,30500,38000,1625.5588911535713 +121,4,120,30500,38500,1573.5546642859429 +122,4,120,30500,39000,1530.1592840718379 +123,4,120,30500,39500,1496.2087139473604 +124,4,120,30500,40000,1471.525855239756 +125,4,120,30500,40500,1455.2084749904016 +126,4,120,30500,41000,1445.9160840082027 +127,4,120,30500,41500,1442.1255377330835 +128,4,120,31000,38000,1855.8467211183756 +129,4,120,31000,38500,1818.4368412235558 +130,4,120,31000,39000,1787.25956706785 +131,4,120,31000,39500,1762.8169908546402 +132,4,120,31000,40000,1744.9825741661596 +133,4,120,31000,40500,1733.136625016882 +134,4,120,31000,41000,1726.3352245899828 +135,4,120,31000,41500,1723.492199933745 +136,4,120,31500,38000,2096.6479813687533 +137,4,120,31500,38500,2069.3606691038876 +138,4,120,31500,39000,2046.792043575205 +139,4,120,31500,39500,2029.2128703900223 +140,4,120,31500,40000,2016.4664599897606 +141,4,120,31500,40500,2008.054814885348 +142,4,120,31500,41000,2003.2622557140814 +143,4,120,31500,41500,2001.289784483679 +144,7,40,29000,38000,149.32898706737052 +145,7,40,29000,38500,161.04814413969586 +146,7,40,29000,39000,187.87801343005242 +147,7,40,29000,39500,223.00789161520424 +148,7,40,29000,40000,261.66779887964003 +149,7,40,29000,40500,300.676316191238 +150,7,40,29000,41000,338.04021206995765 +151,7,40,29000,41500,372.6191631389286 +152,7,40,29500,38000,276.6495061185777 +153,7,40,29500,38500,282.1304583501965 +154,7,40,29500,39000,300.91417483065254 +155,7,40,29500,39500,327.24304394350395 +156,7,40,29500,40000,357.0561976596432 +157,7,40,29500,40500,387.61662064170207 +158,7,40,29500,41000,417.1836349752378 +159,7,40,29500,41500,444.73705844573243 +160,7,40,30000,38000,448.0380830353589 +161,7,40,30000,38500,448.8094536459122 +162,7,40,30000,39000,460.77530593327293 +163,7,40,30000,39500,479.342874472736 +164,7,40,30000,40000,501.20694459059405 +165,7,40,30000,40500,524.0971649678811 +166,7,40,30000,41000,546.539334134893 +167,7,40,30000,41500,567.6447156158981 +168,7,40,30500,38000,657.9909416906933 +169,7,40,30500,38500,655.7465129488842 +170,7,40,30500,39000,662.5420970804985 +171,7,40,30500,39500,674.8914651553109 +172,7,40,30500,40000,690.2111920703564 +173,7,40,30500,40500,706.6833639709198 +174,7,40,30500,41000,723.0994507096715 +175,7,40,30500,41500,738.7096013891406 +176,7,40,31000,38000,899.1769906655776 +177,7,40,31000,38500,895.4391505892945 +178,7,40,31000,39000,898.7695629120826 +179,7,40,31000,39500,906.603316771593 +180,7,40,31000,40000,916.9811481373996 +181,7,40,31000,40500,928.4913367709245 +182,7,40,31000,41000,940.1744934710283 +183,7,40,31000,41500,951.4199286075984 +184,7,40,31500,38000,1163.093373675207 +185,7,40,31500,38500,1159.0457727559028 +186,7,40,31500,39000,1160.3831770028223 +187,7,40,31500,39500,1165.2451698296604 +188,7,40,31500,40000,1172.1768190340001 +189,7,40,31500,40500,1180.1105659428963 +190,7,40,31500,41000,1188.3083929833688 +191,7,40,31500,41500,1196.29112579565 +192,7,80,29000,38000,514.0332369183081 +193,7,80,29000,38500,329.3645784712966 +194,7,80,29000,39000,215.73000998706416 +195,7,80,29000,39500,162.37338399591852 +196,7,80,29000,40000,149.8401793263549 +197,7,80,29000,40500,162.96125998112578 +198,7,80,29000,41000,191.173279165834 +199,7,80,29000,41500,227.2781971491003 +200,7,80,29500,38000,623.559246695578 +201,7,80,29500,38500,448.60620511421484 +202,7,80,29500,39000,344.21940687907573 +203,7,80,29500,39500,292.9758707105001 +204,7,80,29500,40000,277.07670134364804 +205,7,80,29500,40500,283.5158840045542 +206,7,80,29500,41000,303.33951582820265 +207,7,80,29500,41500,330.43357046741954 +208,7,80,30000,38000,732.5907387079073 +209,7,80,30000,38500,593.1926567994672 +210,7,80,30000,39000,508.5638538704666 +211,7,80,30000,39500,464.47881763522037 +212,7,80,30000,40000,448.0394620671692 +213,7,80,30000,40500,449.64309860415494 +214,7,80,30000,41000,462.4490598612332 +215,7,80,30000,41500,481.6323506247537 +216,7,80,30500,38000,871.1163930229344 +217,7,80,30500,38500,771.1320563649375 +218,7,80,30500,39000,707.8872660015606 +219,7,80,30500,39500,672.6612145133173 +220,7,80,30500,40000,657.4974157809264 +221,7,80,30500,40500,656.0835852491216 +222,7,80,30500,41000,663.6006958125331 +223,7,80,30500,41500,676.460675405631 +224,7,80,31000,38000,1053.1852617390061 +225,7,80,31000,38500,984.3647109805877 +226,7,80,31000,39000,938.6158531749268 +227,7,80,31000,39500,911.4268280093535 +228,7,80,31000,40000,898.333365348419 +229,7,80,31000,40500,895.3996527486954 +230,7,80,31000,41000,899.3556288533885 +231,7,80,31000,41500,907.6180684887955 +232,7,80,31500,38000,1274.2255948763498 +233,7,80,31500,38500,1226.5236809533717 +234,7,80,31500,39000,1193.4538731398666 +235,7,80,31500,39500,1172.8105398345213 +236,7,80,31500,40000,1162.0692230240734 +237,7,80,31500,40500,1158.7461521476607 +238,7,80,31500,41000,1160.6173577210805 +239,7,80,31500,41500,1165.840315694716 +240,7,120,29000,38000,1325.2409732290193 +241,7,120,29000,38500,900.8063148840154 +242,7,120,29000,39000,629.9300352098937 +243,7,120,29000,39500,413.81648033893424 +244,7,120,29000,40000,257.3116751690404 +245,7,120,29000,40500,177.89217179438947 +246,7,120,29000,41000,151.58366848473491 +247,7,120,29000,41500,157.56967437251706 +248,7,120,29500,38000,1211.2807882170853 +249,7,120,29500,38500,956.936161969002 +250,7,120,29500,39000,753.3050086992201 +251,7,120,29500,39500,528.2452647799327 +252,7,120,29500,40000,382.62610532894917 +253,7,120,29500,40500,308.44199089882375 +254,7,120,29500,41000,280.3893024671524 +255,7,120,29500,41500,280.4028092582749 +256,7,120,30000,38000,1266.5740351143413 +257,7,120,30000,38500,1084.3028700477778 +258,7,120,30000,39000,834.2392498526193 +259,7,120,30000,39500,650.7560171314304 +260,7,120,30000,40000,537.7846910878052 +261,7,120,30000,40500,477.3001078155485 +262,7,120,30000,41000,451.6865380286754 +263,7,120,30000,41500,448.14911508024613 +264,7,120,30500,38000,1319.6603196780936 +265,7,120,30500,38500,1102.3027489012372 +266,7,120,30500,39000,931.2523583659847 +267,7,120,30500,39500,807.0833484596384 +268,7,120,30500,40000,727.4852710400268 +269,7,120,30500,40500,682.1437030344305 +270,7,120,30500,41000,660.7859329989657 +271,7,120,30500,41500,655.6001132492668 +272,7,120,31000,38000,1330.5306924865326 +273,7,120,31000,38500,1195.9190861202942 +274,7,120,31000,39000,1086.0328080422887 +275,7,120,31000,39500,1005.4160637517409 +276,7,120,31000,40000,951.2021706290612 +277,7,120,31000,40500,918.1457644271304 +278,7,120,31000,41000,901.0511005554887 +279,7,120,31000,41500,895.4599964465793 +280,7,120,31500,38000,1447.8365822059013 +281,7,120,31500,38500,1362.3417347939844 +282,7,120,31500,39000,1292.382727215108 +283,7,120,31500,39500,1239.1826828976662 +284,7,120,31500,40000,1201.6474412465277 +285,7,120,31500,40500,1177.5235955796813 +286,7,120,31500,41000,1164.1761722345295 +287,7,120,31500,41500,1158.9997785002718 +288,10,40,29000,38000,33.437068437082054 +289,10,40,29000,38500,58.471249815534996 +290,10,40,29000,39000,101.41937628542912 +291,10,40,29000,39500,153.80690200519626 +292,10,40,29000,40000,209.66451461551316 +293,10,40,29000,40500,265.03070792175197 +294,10,40,29000,41000,317.46079310177566 +295,10,40,29000,41500,365.59950388342645 +296,10,40,29500,38000,70.26818405688635 +297,10,40,29500,38500,87.96463718548947 +298,10,40,29500,39000,122.58188233160993 +299,10,40,29500,39500,166.2478945807132 +300,10,40,29500,40000,213.48669617414316 +301,10,40,29500,40500,260.67953961944477 +302,10,40,29500,41000,305.5877041218316 +303,10,40,29500,41500,346.95612213021155 +304,10,40,30000,38000,153.67588703371362 +305,10,40,30000,38500,164.07504103479005 +306,10,40,30000,39000,190.0800160661499 +307,10,40,30000,39500,224.61382980242837 +308,10,40,30000,40000,262.79232847382445 +309,10,40,30000,40500,301.38687703450415 +310,10,40,30000,41000,338.38536686093164 +311,10,40,30000,41500,372.6399011703545 +312,10,40,30500,38000,284.2936286531718 +313,10,40,30500,38500,288.4690608277705 +314,10,40,30500,39000,306.44667517621144 +315,10,40,30500,39500,332.20122250191986 +316,10,40,30500,40000,361.5566690083291 +317,10,40,30500,40500,391.72755224929614 +318,10,40,30500,41000,420.95317535960476 +319,10,40,30500,41500,448.2049230608669 +320,10,40,31000,38000,459.03140021766137 +321,10,40,31000,38500,458.71477027519967 +322,10,40,31000,39000,469.9910751800656 +323,10,40,31000,39500,488.05850105225426 +324,10,40,31000,40000,509.5204701455629 +325,10,40,31000,40500,532.0674969691778 +326,10,40,31000,41000,554.2088430693509 +327,10,40,31000,41500,575.0485839499048 +328,10,40,31500,38000,672.2476845983564 +329,10,40,31500,38500,669.2240508488649 +330,10,40,31500,39000,675.4956226836405 +331,10,40,31500,39500,687.447764319295 +332,10,40,31500,40000,702.4395430742891 +333,10,40,31500,40500,718.6279487347668 +334,10,40,31500,41000,734.793684592168 +335,10,40,31500,41500,750.1821072409286 +336,10,80,29000,38000,387.7617282731497 +337,10,80,29000,38500,195.33642612593002 +338,10,80,29000,39000,82.7306931465102 +339,10,80,29000,39500,35.13436471793541 +340,10,80,29000,40000,33.521138659248706 +341,10,80,29000,40500,61.47395975053128 +342,10,80,29000,41000,106.71403229340167 +343,10,80,29000,41500,160.56068704487473 +344,10,80,29500,38000,459.63404601804103 +345,10,80,29500,38500,258.7453720995899 +346,10,80,29500,39000,135.96435731320256 +347,10,80,29500,39500,80.2685095017944 +348,10,80,29500,40000,70.86302366453106 +349,10,80,29500,40500,90.43203026480438 +350,10,80,29500,41000,126.7844695901737 +351,10,80,29500,41500,171.63682876805044 +352,10,80,30000,38000,564.1463320344325 +353,10,80,30000,38500,360.75718124523866 +354,10,80,30000,39000,231.70119191254307 +355,10,80,30000,39500,170.74752201483128 +356,10,80,30000,40000,154.7149036950422 +357,10,80,30000,40500,166.10596450541493 +358,10,80,30000,41000,193.3351721194443 +359,10,80,30000,41500,228.78394172417038 +360,10,80,30500,38000,689.6797223218513 +361,10,80,30500,38500,484.8023695265838 +362,10,80,30500,39000,363.5979340028588 +363,10,80,30500,39500,304.67857102688225 +364,10,80,30500,40000,285.29210000833734 +365,10,80,30500,40500,290.0135917456113 +366,10,80,30500,41000,308.8672169492536 +367,10,80,30500,41500,335.3210332569182 +368,10,80,31000,38000,789.946106942773 +369,10,80,31000,38500,625.7722360026959 +370,10,80,31000,39000,528.6063264942235 +371,10,80,31000,39500,478.6863763478618 +372,10,80,31000,40000,459.5026243189753 +373,10,80,31000,40500,459.6982093164963 +374,10,80,31000,41000,471.6790024321937 +375,10,80,31000,41500,490.3034492109124 +376,10,80,31500,38000,912.3540488244158 +377,10,80,31500,38500,798.2135101409633 +378,10,80,31500,39000,727.746684419146 +379,10,80,31500,39500,689.0119464356724 +380,10,80,31500,40000,672.0757202772029 +381,10,80,31500,40500,669.678339553036 +382,10,80,31500,41000,676.5761221409929 +383,10,80,31500,41500,688.9934449650118 +384,10,120,29000,38000,1155.1165164624408 +385,10,120,29000,38500,840.2641727088946 +386,10,120,29000,39000,506.9102636732852 +387,10,120,29000,39500,265.5278912452038 +388,10,120,29000,40000,116.39516513179322 +389,10,120,29000,40500,45.2088092745619 +390,10,120,29000,41000,30.22267557153353 +391,10,120,29000,41500,51.06063746392809 +392,10,120,29500,38000,1343.7868459826054 +393,10,120,29500,38500,977.9852373227346 +394,10,120,29500,39000,594.632756549817 +395,10,120,29500,39500,346.2478773329187 +396,10,120,29500,40000,180.23082247413407 +397,10,120,29500,40500,95.81649989178923 +398,10,120,29500,41000,71.0837801649128 +399,10,120,29500,41500,82.84289818279714 +400,10,120,30000,38000,1532.9333545384934 +401,10,120,30000,38500,1012.2223350568845 +402,10,120,30000,39000,688.4884716222766 +403,10,120,30000,39500,464.6206903113392 +404,10,120,30000,40000,283.5644748300334 +405,10,120,30000,40500,190.27593217865416 +406,10,120,30000,41000,158.0192279691727 +407,10,120,30000,41500,161.3611926772337 +408,10,120,30500,38000,1349.3785399811063 +409,10,120,30500,38500,1014.785480110738 +410,10,120,30500,39000,843.0316833766408 +411,10,120,30500,39500,589.4543896730125 +412,10,120,30500,40000,412.3358512291996 +413,10,120,30500,40500,324.11715620464133 +414,10,120,30500,41000,290.17588242984766 +415,10,120,30500,41500,287.56857384673356 +416,10,120,31000,38000,1328.0973931040146 +417,10,120,31000,38500,1216.5659656437845 +418,10,120,31000,39000,928.4831767181619 +419,10,120,31000,39500,700.3115484040329 +420,10,120,31000,40000,565.0876352458171 +421,10,120,31000,40500,494.44016026435037 +422,10,120,31000,41000,464.38005437182983 +423,10,120,31000,41500,458.7614573733091 +424,10,120,31500,38000,1473.1154650008834 +425,10,120,31500,38500,1195.943614951571 +426,10,120,31500,39000,990.2486604382486 +427,10,120,31500,39500,843.1390407497395 +428,10,120,31500,40000,751.2746391170706 +429,10,120,31500,40500,700.215375503209 +430,10,120,31500,41000,676.1585052687219 +431,10,120,31500,41500,669.5907920932743 +432,13,40,29000,38000,49.96352152045025 +433,13,40,29000,38500,83.75104994958261 +434,13,40,29000,39000,136.8176091795391 +435,13,40,29000,39500,199.91486685466407 +436,13,40,29000,40000,266.4367154860076 +437,13,40,29000,40500,331.97224579940524 +438,13,40,29000,41000,393.8001583706036 +439,13,40,29000,41500,450.42425363084493 +440,13,40,29500,38000,29.775721038786923 +441,13,40,29500,38500,57.37673742631121 +442,13,40,29500,39000,103.49161398239501 +443,13,40,29500,39500,159.3058253852367 +444,13,40,29500,40000,218.60083223764073 +445,13,40,29500,40500,277.2507278183831 +446,13,40,29500,41000,332.7141278886951 +447,13,40,29500,41500,383.58832292300576 +448,13,40,30000,38000,47.72263852005472 +449,13,40,30000,38500,68.07581028940402 +450,13,40,30000,39000,106.13974628945516 +451,13,40,30000,39500,153.58449949683063 +452,13,40,30000,40000,204.62393623358633 +453,13,40,30000,40500,255.44513025602419 +454,13,40,30000,41000,303.69954914051766 +455,13,40,30000,41500,348.0803709720354 +456,13,40,30500,38000,110.9331168284094 +457,13,40,30500,38500,123.63361262704746 +458,13,40,30500,39000,153.02654433825705 +459,13,40,30500,39500,191.40769947472756 +460,13,40,30500,40000,233.503841403055 +461,13,40,30500,40500,275.8557790922913 +462,13,40,30500,41000,316.32529882763697 +463,13,40,30500,41500,353.7060432094809 +464,13,40,31000,38000,221.90608823073939 +465,13,40,31000,38500,227.67026441593657 +466,13,40,31000,39000,248.62107049869064 +467,13,40,31000,39500,277.9507605389158 +468,13,40,31000,40000,311.0267471957685 +469,13,40,31000,40500,344.8024031161673 +470,13,40,31000,41000,377.3761144228052 +471,13,40,31000,41500,407.6529635071056 +472,13,40,31500,38000,378.8738382757093 +473,13,40,31500,38500,379.39748335944216 +474,13,40,31500,39000,393.01223361732553 +475,13,40,31500,39500,414.10238059122855 +476,13,40,31500,40000,438.8024282436204 +477,13,40,31500,40500,464.5348067190265 +478,13,40,31500,41000,489.6621039898805 +479,13,40,31500,41500,513.2163939332803 +480,13,80,29000,38000,364.387588581215 +481,13,80,29000,38500,184.2902007673634 +482,13,80,29000,39000,81.57192155036655 +483,13,80,29000,39500,42.54811210095659 +484,13,80,29000,40000,49.897338772663076 +485,13,80,29000,40500,87.84229516509882 +486,13,80,29000,41000,143.85451969447664 +487,13,80,29000,41500,208.71467984917848 +488,13,80,29500,38000,382.5794635435733 +489,13,80,29500,38500,188.38619353711718 +490,13,80,29500,39000,75.75749359688277 +491,13,80,29500,39500,29.27891251986562 +492,13,80,29500,40000,29.794874961934568 +493,13,80,29500,40500,60.654888662698205 +494,13,80,29500,41000,109.25801388824325 +495,13,80,29500,41500,166.6311093454692 +496,13,80,30000,38000,448.97795526074816 +497,13,80,30000,38500,238.44530107604737 +498,13,80,30000,39000,112.34545890264337 +499,13,80,30000,39500,56.125871791222835 +500,13,80,30000,40000,48.29987461781518 +501,13,80,30000,40500,70.7900626637678 +502,13,80,30000,41000,110.76865376691964 +503,13,80,30000,41500,159.50197316936024 +504,13,80,30500,38000,547.7818730461195 +505,13,80,30500,38500,332.92604070423494 +506,13,80,30500,39000,193.80760050280742 +507,13,80,30500,39500,128.3457644087917 +508,13,80,30500,40000,112.23915895822442 +509,13,80,30500,40500,125.96369396512564 +510,13,80,30500,41000,156.67918617660013 +511,13,80,30500,41500,196.05195109523765 +512,13,80,31000,38000,682.8591931963246 +513,13,80,31000,38500,457.56562267948556 +514,13,80,31000,39000,313.6380169123524 +515,13,80,31000,39500,245.13531819580908 +516,13,80,31000,40000,223.54473391202873 +517,13,80,31000,40500,229.60752111202834 +518,13,80,31000,41000,251.42377424735136 +519,13,80,31000,41500,281.48720903016886 +520,13,80,31500,38000,807.925638050234 +521,13,80,31500,38500,588.686585641994 +522,13,80,31500,39000,464.0488586698228 +523,13,80,31500,39500,402.69214492641095 +524,13,80,31500,40000,380.13626165363934 +525,13,80,31500,40500,380.8064948609387 +526,13,80,31500,41000,395.05186915919086 +527,13,80,31500,41500,416.70193045600774 +528,13,120,29000,38000,1068.8279454397398 +529,13,120,29000,38500,743.0012805963486 +530,13,120,29000,39000,451.2538301167544 +531,13,120,29000,39500,235.4154251166075 +532,13,120,29000,40000,104.73720814447498 +533,13,120,29000,40500,46.91983990671749 +534,13,120,29000,41000,42.81092192562316 +535,13,120,29000,41500,74.33530639171506 +536,13,120,29500,38000,1133.1178848710972 +537,13,120,29500,38500,824.0745323788527 +538,13,120,29500,39000,499.10867111401996 +539,13,120,29500,39500,256.1626809904186 +540,13,120,29500,40000,107.68599585294751 +541,13,120,29500,40500,38.18533662516749 +542,13,120,29500,41000,25.499608203619154 +543,13,120,29500,41500,49.283537699300375 +544,13,120,30000,38000,1292.409871290162 +545,13,120,30000,38500,994.669572829704 +546,13,120,30000,39000,598.9783697712826 +547,13,120,30000,39500,327.47348408537925 +548,13,120,30000,40000,156.82634841081907 +549,13,120,30000,40500,71.30833688875883 +550,13,120,30000,41000,47.72389750130817 +551,13,120,30000,41500,62.1982461882982 +552,13,120,30500,38000,1585.8797221278146 +553,13,120,30500,38500,1144.66688416451 +554,13,120,30500,39000,692.6651441690645 +555,13,120,30500,39500,441.98837639874046 +556,13,120,30500,40000,251.56311435857728 +557,13,120,30500,40500,149.79670413140468 +558,13,120,30500,41000,115.52645596043719 +559,13,120,30500,41500,120.44019473389324 +560,13,120,31000,38000,1702.7625866892163 +561,13,120,31000,38500,1071.7854750250656 +562,13,120,31000,39000,807.8943299034604 +563,13,120,31000,39500,588.672223513561 +564,13,120,31000,40000,376.44658358671404 +565,13,120,31000,40500,269.2159719426485 +566,13,120,31000,41000,229.41660529009877 +567,13,120,31000,41500,226.78274707181976 +568,13,120,31500,38000,1331.3523701291767 +569,13,120,31500,38500,1151.2055268669133 +570,13,120,31500,39000,1006.811285091974 +571,13,120,31500,39500,702.0053094629535 +572,13,120,31500,40000,515.9081891614829 +573,13,120,31500,40500,423.8652275555525 +574,13,120,31500,41000,386.4939696097151 +575,13,120,31500,41500,379.8118453367429 +576,16,40,29000,38000,106.1025746852808 +577,16,40,29000,38500,145.32590128581407 +578,16,40,29000,39000,204.74804378224422 +579,16,40,29000,39500,274.6339266648551 +580,16,40,29000,40000,347.9667393938497 +581,16,40,29000,40500,420.03753452490974 +582,16,40,29000,41000,487.9353932879741 +583,16,40,29000,41500,550.0623063219693 +584,16,40,29500,38000,54.65040870471303 +585,16,40,29500,38500,88.94089091627293 +586,16,40,29500,39000,142.72223808288405 +587,16,40,29500,39500,206.63598763907422 +588,16,40,29500,40000,273.99851593521134 +589,16,40,29500,40500,340.34861536649436 +590,16,40,29500,41000,402.935270882596 +591,16,40,29500,41500,460.2471155081633 +592,16,40,30000,38000,29.788548081995298 +593,16,40,30000,38500,57.96323252610644 +594,16,40,30000,39000,104.92815906834525 +595,16,40,30000,39500,161.71867032726158 +596,16,40,30000,40000,222.01677586338877 +597,16,40,30000,40500,281.6349465235367 +598,16,40,30000,41000,337.99683241119567 +599,16,40,30000,41500,389.68271710858414 +600,16,40,30500,38000,42.06569536892785 +601,16,40,30500,38500,62.95145274276575 +602,16,40,30500,39000,101.93860830594608 +603,16,40,30500,39500,150.47910837525734 +604,16,40,30500,40000,202.65388851823258 +605,16,40,30500,40500,254.5724108541227 +606,16,40,30500,41000,303.84403622726694 +607,16,40,30500,41500,349.1422884543064 +608,16,40,31000,38000,99.21707896667829 +609,16,40,31000,38500,112.24153596941301 +610,16,40,31000,39000,142.5186177618655 +611,16,40,31000,39500,182.02836955332134 +612,16,40,31000,40000,225.3201896575212 +613,16,40,31000,40500,268.83705389232614 +614,16,40,31000,41000,310.3895932135811 +615,16,40,31000,41500,348.7480165565453 +616,16,40,31500,38000,204.30418825821732 +617,16,40,31500,38500,210.0759235359138 +618,16,40,31500,39000,231.7643258544752 +619,16,40,31500,39500,262.1512494310348 +620,16,40,31500,40000,296.3864127264238 +621,16,40,31500,40500,331.30743171999035 +622,16,40,31500,41000,364.95322314895554 +623,16,40,31500,41500,396.20142191205844 +624,16,80,29000,38000,399.5975649320935 +625,16,80,29000,38500,225.6318269911425 +626,16,80,29000,39000,127.97354075513151 +627,16,80,29000,39500,93.73584101549991 +628,16,80,29000,40000,106.43084032022394 +629,16,80,29000,40500,150.51245762256931 +630,16,80,29000,41000,213.24213500046466 +631,16,80,29000,41500,285.0426423013882 +632,16,80,29500,38000,371.37706087096393 +633,16,80,29500,38500,189.77150413822454 +634,16,80,29500,39000,86.22375488959844 +635,16,80,29500,39500,46.98714814001572 +636,16,80,29500,40000,54.596900621760675 +637,16,80,29500,40500,93.12033833747024 +638,16,80,29500,41000,149.89341227947025 +639,16,80,29500,41500,215.5937000584367 +640,16,80,30000,38000,388.43657991253195 +641,16,80,30000,38500,190.77121362008674 +642,16,80,30000,39000,76.28535232335287 +643,16,80,30000,39500,29.152860363695716 +644,16,80,30000,40000,29.820972887404942 +645,16,80,30000,40500,61.320203047752464 +646,16,80,30000,41000,110.82086782062603 +647,16,80,30000,41500,169.197767615573 +648,16,80,30500,38000,458.8964339917103 +649,16,80,30500,38500,239.547928886725 +650,16,80,30500,39000,109.02338779317503 +651,16,80,30500,39500,50.888746196140914 +652,16,80,30500,40000,42.73606982375976 +653,16,80,30500,40500,65.75935122724029 +654,16,80,30500,41000,106.68884313872147 +655,16,80,30500,41500,156.54100549486617 +656,16,80,31000,38000,561.7385153195615 +657,16,80,31000,38500,335.5692026144635 +658,16,80,31000,39000,188.0383015831574 +659,16,80,31000,39500,118.2318539104416 +660,16,80,31000,40000,100.81000168801492 +661,16,80,31000,40500,114.72014539486217 +662,16,80,31000,41000,146.2992492326178 +663,16,80,31000,41500,186.8074429488408 +664,16,80,31500,38000,697.9937997454152 +665,16,80,31500,38500,466.42234442578484 +666,16,80,31500,39000,306.52125608515166 +667,16,80,31500,39500,230.54692639209762 +668,16,80,31500,40000,206.461121102699 +669,16,80,31500,40500,212.23429887269359 +670,16,80,31500,41000,234.70913795495554 +671,16,80,31500,41500,265.8143069252357 +672,16,120,29000,38000,1085.688903883652 +673,16,120,29000,38500,750.2887000017752 +674,16,120,29000,39000,469.92662852990964 +675,16,120,29000,39500,267.1560282754928 +676,16,120,29000,40000,146.06299930062625 +677,16,120,29000,40500,95.28836772053619 +678,16,120,29000,41000,97.41466545178946 +679,16,120,29000,41500,135.3804131941845 +680,16,120,29500,38000,1079.5576154477903 +681,16,120,29500,38500,751.2932384998761 +682,16,120,29500,39000,458.27083477307207 +683,16,120,29500,39500,240.9658024131812 +684,16,120,29500,40000,109.3801465044384 +685,16,120,29500,40500,51.274139057659724 +686,16,120,29500,41000,47.36446629605638 +687,16,120,29500,41500,79.42944320845996 +688,16,120,30000,38000,1139.3792936518537 +689,16,120,30000,38500,833.7979589668842 +690,16,120,30000,39000,507.805443202025 +691,16,120,30000,39500,259.93892964607977 +692,16,120,30000,40000,108.7341499557062 +693,16,120,30000,40500,38.152937143498605 +694,16,120,30000,41000,25.403985123518716 +695,16,120,30000,41500,49.72822589160786 +696,16,120,30500,38000,1285.0396277304772 +697,16,120,30500,38500,1025.254169031627 +698,16,120,30500,39000,622.5890550779666 +699,16,120,30500,39500,333.3353043756717 +700,16,120,30500,40000,155.70268128051293 +701,16,120,30500,40500,66.84125446522368 +702,16,120,30500,41000,42.25187049753978 +703,16,120,30500,41500,56.98314898830595 +704,16,120,31000,38000,1595.7993459811262 +705,16,120,31000,38500,1252.8886556470425 +706,16,120,31000,39000,731.4408383874198 +707,16,120,31000,39500,451.0090473423308 +708,16,120,31000,40000,251.5086563526081 +709,16,120,31000,40500,141.8915050063955 +710,16,120,31000,41000,104.67474675582574 +711,16,120,31000,41500,109.1609567535697 +712,16,120,31500,38000,1942.3896021770768 +713,16,120,31500,38500,1197.207050908449 +714,16,120,31500,39000,812.6818768064074 +715,16,120,31500,39500,611.45532452889 +716,16,120,31500,40000,380.63642711770643 +717,16,120,31500,40500,258.5514125337487 +718,16,120,31500,41000,213.48518421250665 +719,16,120,31500,41500,209.58134396574906 +720,19,40,29000,38000,169.3907733115706 +721,19,40,29000,38500,212.23331960093145 +722,19,40,29000,39000,275.9376503672959 +723,19,40,29000,39500,350.4301397081139 +724,19,40,29000,40000,428.40863665493924 +725,19,40,29000,40500,504.955113902399 +726,19,40,29000,41000,577.023450987656 +727,19,40,29000,41500,642.9410032211753 +728,19,40,29500,38000,102.40889356493292 +729,19,40,29500,38500,141.19036226103668 +730,19,40,29500,39000,200.19333708701748 +731,19,40,29500,39500,269.6750686488757 +732,19,40,29500,40000,342.6217886299377 +733,19,40,29500,40500,414.33044375626207 +734,19,40,29500,41000,481.89521316730713 +735,19,40,29500,41500,543.7211700546151 +736,19,40,30000,38000,51.95330426445395 +737,19,40,30000,38500,85.69656829127965 +738,19,40,30000,39000,138.98376466247876 +739,19,40,30000,39500,202.43251598105033 +740,19,40,30000,40000,269.3557903452929 +741,19,40,30000,40500,335.2960133312316 +742,19,40,30000,41000,397.50658847538665 +743,19,40,30000,41500,454.47903112410967 +744,19,40,30500,38000,28.864802790801026 +745,19,40,30500,38500,56.32899754732796 +746,19,40,30500,39000,102.69825523352162 +747,19,40,30500,39500,158.95118263535466 +748,19,40,30500,40000,218.75241957992617 +749,19,40,30500,40500,277.9122290233915 +750,19,40,30500,41000,333.8561815041273 +751,19,40,30500,41500,385.1662652901447 +752,19,40,31000,38000,43.72359701781447 +753,19,40,31000,38500,63.683967347844224 +754,19,40,31000,39000,101.95579433282329 +755,19,40,31000,39500,149.8826019475827 +756,19,40,31000,40000,201.50605279789198 +757,19,40,31000,40500,252.92391570754876 +758,19,40,31000,41000,301.7431453727685 +759,19,40,31000,41500,346.6368192781496 +760,19,40,31500,38000,104.05710998615942 +761,19,40,31500,38500,115.95783594434451 +762,19,40,31500,39000,145.42181873662554 +763,19,40,31500,39500,184.26373455825217 +764,19,40,31500,40000,226.97066340897095 +765,19,40,31500,40500,269.96403356902357 +766,19,40,31500,41000,311.04753558871505 +767,19,40,31500,41500,348.98866332680115 +768,19,80,29000,38000,453.1314944429312 +769,19,80,29000,38500,281.24067760117225 +770,19,80,29000,39000,185.83730378881882 +771,19,80,29000,39500,154.25726305915472 +772,19,80,29000,40000,170.2912737797755 +773,19,80,29000,40500,218.38979299191152 +774,19,80,29000,41000,285.604024444273 +775,19,80,29000,41500,362.0858325427657 +776,19,80,29500,38000,400.06299682217264 +777,19,80,29500,38500,224.41725666435008 +778,19,80,29500,39000,125.58476107530382 +779,19,80,29500,39500,90.55733834394478 +780,19,80,29500,40000,102.67519971027264 +781,19,80,29500,40500,146.27807815967392 +782,19,80,29500,41000,208.57372904155937 +783,19,80,29500,41500,279.9669583078214 +784,19,80,30000,38000,376.1594584816549 +785,19,80,30000,38500,191.30452808298463 +786,19,80,30000,39000,85.63116084217559 +787,19,80,30000,39500,45.10487847849711 +788,19,80,30000,40000,51.88389644342952 +789,19,80,30000,40500,89.78942817703852 +790,19,80,30000,41000,146.0393555385696 +791,19,80,30000,41500,211.26567367707352 +792,19,80,30500,38000,401.874315275947 +793,19,80,30500,38500,197.55305366608133 +794,19,80,30500,39000,79.00348967857379 +795,19,80,30500,39500,29.602719961568614 +796,19,80,30500,40000,28.980451378502487 +797,19,80,30500,40500,59.63541802023186 +798,19,80,30500,41000,108.48607655362268 +799,19,80,30500,41500,166.30589286399507 +800,19,80,31000,38000,484.930958445979 +801,19,80,31000,38500,254.27552635537404 +802,19,80,31000,39000,116.75543721560439 +803,19,80,31000,39500,54.77547840250418 +804,19,80,31000,40000,44.637472658824976 +805,19,80,31000,40500,66.50466903927668 +806,19,80,31000,41000,106.62737262508298 +807,19,80,31000,41500,155.8310688191254 +808,19,80,31500,38000,595.6094306603337 +809,19,80,31500,38500,359.60040819463063 +810,19,80,31500,39000,201.85328967228585 +811,19,80,31500,39500,126.24442464793601 +812,19,80,31500,40000,106.07388975142673 +813,19,80,31500,40500,118.52358345403363 +814,19,80,31500,41000,149.1597537162607 +815,19,80,31500,41500,188.94964975523197 +816,19,120,29000,38000,1133.9213841599772 +817,19,120,29000,38500,793.9759807804692 +818,19,120,29000,39000,516.5580425563733 +819,19,120,29000,39500,318.60172051726147 +820,19,120,29000,40000,201.662212274693 +821,19,120,29000,40500,154.47522945829064 +822,19,120,29000,41000,160.28049502033574 +823,19,120,29000,41500,202.35345983501588 +824,19,120,29500,38000,1091.6343400395158 +825,19,120,29500,38500,754.9332443184217 +826,19,120,29500,39000,472.1777992591152 +827,19,120,29500,39500,267.03951846894995 +828,19,120,29500,40000,144.25558152688114 +829,19,120,29500,40500,92.40384156679512 +830,19,120,29500,41000,93.81833253459942 +831,19,120,29500,41500,131.24753560710644 +832,19,120,30000,38000,1092.719296892266 +833,19,120,30000,38500,764.7065490850255 +834,19,120,30000,39000,467.2268758064373 +835,19,120,30000,39500,244.9367732985332 +836,19,120,30000,40000,110.00996333393202 +837,19,120,30000,40500,49.96381544207811 +838,19,120,30000,41000,44.9298739569088 +839,19,120,30000,41500,76.25447129089613 +840,19,120,30500,38000,1160.6160120981158 +841,19,120,30500,38500,865.5953188304933 +842,19,120,30500,39000,531.1657093741892 +843,19,120,30500,39500,271.98520008106277 +844,19,120,30500,40000,114.03616090967407 +845,19,120,30500,40500,39.74252227099571 +846,19,120,30500,41000,25.07176465285551 +847,19,120,30500,41500,48.298794094852724 +848,19,120,31000,38000,1304.8870694342509 +849,19,120,31000,38500,1089.6854636757826 +850,19,120,31000,39000,668.6632735260521 +851,19,120,31000,39500,356.7751012890747 +852,19,120,31000,40000,168.32491564142487 +853,19,120,31000,40500,72.82648063377391 +854,19,120,31000,41000,45.02326687759286 +855,19,120,31000,41500,58.13111530831655 +856,19,120,31500,38000,1645.2697164013964 +857,19,120,31500,38500,1373.859712069864 +858,19,120,31500,39000,787.3948673670299 +859,19,120,31500,39500,483.60546305948367 +860,19,120,31500,40000,273.4285373433001 +861,19,120,31500,40500,153.21079535396908 +862,19,120,31500,41000,111.21299419905313 +863,19,120,31500,41500,113.52006337929113 +864,22,40,29000,38000,229.2032513971666 +865,22,40,29000,38500,274.65023153674116 +866,22,40,29000,39000,341.4424739822062 +867,22,40,29000,39500,419.2624324130753 +868,22,40,29000,40000,500.6022690006133 +869,22,40,29000,40500,580.3923016374031 +870,22,40,29000,41000,655.4874207991389 +871,22,40,29000,41500,724.1595537770351 +872,22,40,29500,38000,155.45206306046595 +873,22,40,29500,38500,197.41588482427002 +874,22,40,29500,39000,260.1641484982308 +875,22,40,29500,39500,333.666918810689 +876,22,40,29500,40000,410.66541588422854 +877,22,40,29500,40500,486.276072112155 +878,22,40,29500,41000,557.4760464927683 +879,22,40,29500,41500,622.6057687448293 +880,22,40,30000,38000,90.70026588811803 +881,22,40,30000,38500,128.41239603755494 +882,22,40,30000,39000,186.27261386900233 +883,22,40,30000,39500,254.5802373859711 +884,22,40,30000,40000,326.3686182341553 +885,22,40,30000,40500,396.9735001502319 +886,22,40,30000,41000,463.5155278718613 +887,22,40,30000,41500,524.414569320113 +888,22,40,30500,38000,44.551475763397946 +889,22,40,30500,38500,76.95264448905411 +890,22,40,30500,39000,128.85898727872572 +891,22,40,30500,39500,190.91422001003792 +892,22,40,30500,40000,256.4755613806196 +893,22,40,30500,40500,321.125224208803 +894,22,40,30500,41000,382.14434919800453 +895,22,40,30500,41500,438.03974322333033 +896,22,40,31000,38000,28.101321546315717 +897,22,40,31000,38500,53.867829756398805 +898,22,40,31000,39000,98.57619184859544 +899,22,40,31000,39500,153.19473192134507 +900,22,40,31000,40000,211.4202434313414 +901,22,40,31000,40500,269.09905982026265 +902,22,40,31000,41000,323.68306330754416 +903,22,40,31000,41500,373.76836451736045 +904,22,40,31500,38000,51.648288279447364 +905,22,40,31500,38500,69.56074881661863 +906,22,40,31500,39000,105.91402675097291 +907,22,40,31500,39500,151.99456204656389 +908,22,40,31500,40000,201.85995274525234 +909,22,40,31500,40500,251.63807959916412 +910,22,40,31500,41000,298.9593498669657 +911,22,40,31500,41500,342.50888994628025 +912,22,80,29000,38000,507.5440336860194 +913,22,80,29000,38500,336.42019672232965 +914,22,80,29000,39000,242.21016116765423 +915,22,80,29000,39500,212.33396533224905 +916,22,80,29000,40000,230.67632355958136 +917,22,80,29000,40500,281.6224662955561 +918,22,80,29000,41000,352.0457411487133 +919,22,80,29000,41500,431.89288175778637 +920,22,80,29500,38000,443.2889283037078 +921,22,80,29500,38500,270.0648237630224 +922,22,80,29500,39000,173.57666711629645 +923,22,80,29500,39500,141.06258420240613 +924,22,80,29500,40000,156.18412870159142 +925,22,80,29500,40500,203.33105261575707 +926,22,80,29500,41000,269.5552387411201 +927,22,80,29500,41500,345.03801326123767 +928,22,80,30000,38000,395.34177505602497 +929,22,80,30000,38500,217.11094192826982 +930,22,80,30000,39000,116.38535634181476 +931,22,80,30000,39500,79.94742924888467 +932,22,80,30000,40000,90.84706550421288 +933,22,80,30000,40500,133.26308067939766 +934,22,80,30000,41000,194.36064414396228 +935,22,80,30000,41500,264.56059537656466 +936,22,80,30500,38000,382.0341866812038 +937,22,80,30500,38500,191.65621311671836 +938,22,80,30500,39000,82.3318677587146 +939,22,80,30500,39500,39.44606931321677 +940,22,80,30500,40000,44.476166488763134 +941,22,80,30500,40500,80.84561981845566 +942,22,80,30500,41000,135.62459431793735 +943,22,80,30500,41500,199.42208168600175 +944,22,80,31000,38000,425.5181957619983 +945,22,80,31000,38500,210.2667219741389 +946,22,80,31000,39000,84.97041062888985 +947,22,80,31000,39500,31.593073529038755 +948,22,80,31000,40000,28.407154164211214 +949,22,80,31000,40500,57.05446633976857 +950,22,80,31000,41000,104.10423883907688 +951,22,80,31000,41500,160.23135976433713 +952,22,80,31500,38000,527.5015417150911 +953,22,80,31500,38500,282.29650611769665 +954,22,80,31500,39000,134.62881845323489 +955,22,80,31500,39500,66.62736532046851 +956,22,80,31500,40000,52.9918858786988 +957,22,80,31500,40500,72.36913743145999 +958,22,80,31500,41000,110.38003828747726 +959,22,80,31500,41500,157.65470091455973 +960,22,120,29000,38000,1186.823326813257 +961,22,120,29000,38500,844.3317816964005 +962,22,120,29000,39000,567.7367986440256 +963,22,120,29000,39500,371.79782508970567 +964,22,120,29000,40000,256.9261857702517 +965,22,120,29000,40500,211.85466060592006 +966,22,120,29000,41000,220.09534855737033 +967,22,120,29000,41500,265.02731793490034 +968,22,120,29500,38000,1128.4568915685559 +969,22,120,29500,38500,787.7709648712951 +970,22,120,29500,39000,508.4832626962424 +971,22,120,29500,39500,308.52654841064975 +972,22,120,29500,40000,190.01030358402707 +973,22,120,29500,40500,141.62663282114926 +974,22,120,29500,41000,146.40704203984612 +975,22,120,29500,41500,187.48734389188584 +976,22,120,30000,38000,1094.7007205604846 +977,22,120,30000,38500,757.7313528729464 +978,22,120,30000,39000,471.282561364766 +979,22,120,30000,39500,262.0412520036699 +980,22,120,30000,40000,136.26956239282435 +981,22,120,30000,40500,82.4268827471484 +982,22,120,30000,41000,82.3695177584498 +983,22,120,30000,41500,118.51210034475737 +984,22,120,30500,38000,1111.0872182758205 +985,22,120,30500,38500,787.2204655558988 +986,22,120,30500,39000,481.85960605002055 +987,22,120,30500,39500,250.28740868446397 +988,22,120,30500,40000,109.21968920710272 +989,22,120,30500,40500,45.51600269221681 +990,22,120,30500,41000,38.172157811051115 +991,22,120,30500,41500,67.73748641348168 +992,22,120,31000,38000,1193.3958874354898 +993,22,120,31000,38500,923.0731791194576 +994,22,120,31000,39000,573.4457650536078 +995,22,120,31000,39500,294.2980811757103 +996,22,120,31000,40000,124.86249624679849 +997,22,120,31000,40500,43.948524347749846 +998,22,120,31000,41000,25.582084045731808 +999,22,120,31000,41500,46.36268252714472 +1000,22,120,31500,38000,1336.0993444856913 +1001,22,120,31500,38500,1194.893001664831 +1002,22,120,31500,39000,740.6584250286721 +1003,22,120,31500,39500,397.18127104230757 +1004,22,120,31500,40000,194.20390582893873 +1005,22,120,31500,40500,88.22588964369922 +1006,22,120,31500,41000,54.97797247760634 +1007,22,120,31500,41500,64.88195101638016 diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/parallel_example.py b/pyomo/contrib/parmest/examples_deprecated/semibatch/parallel_example.py new file mode 100644 index 00000000000..ff1287811cf --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/parallel_example.py @@ -0,0 +1,57 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +""" +The following script can be used to run semibatch parameter estimation in +parallel and save results to files for later analysis and graphics. +Example command: mpiexec -n 4 python parallel_example.py +""" +import numpy as np +import pandas as pd +from itertools import product +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model + + +def main(): + # Vars to estimate + theta_names = ['k1', 'k2', 'E1', 'E2'] + + # Data, list of json file names + data = [] + file_dirname = dirname(abspath(str(__file__))) + for exp_num in range(10): + file_name = abspath(join(file_dirname, 'exp' + str(exp_num + 1) + '.out')) + data.append(file_name) + + # Note, the model already includes a 'SecondStageCost' expression + # for sum of squared error that will be used in parameter estimation + + pest = parmest.Estimator(generate_model, data, theta_names) + + ### Parameter estimation with bootstrap resampling + bootstrap_theta = pest.theta_est_bootstrap(100) + bootstrap_theta.to_csv('bootstrap_theta.csv') + + ### Compute objective at theta for likelihood ratio test + k1 = np.arange(4, 24, 3) + k2 = np.arange(40, 160, 40) + E1 = np.arange(29000, 32000, 500) + E2 = np.arange(38000, 42000, 500) + theta_vals = pd.DataFrame(list(product(k1, k2, E1, E2)), columns=theta_names) + + obj_at_theta = pest.objective_at_theta(theta_vals) + obj_at_theta.to_csv('obj_at_theta.csv') + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/parameter_estimation_example.py b/pyomo/contrib/parmest/examples_deprecated/semibatch/parameter_estimation_example.py new file mode 100644 index 00000000000..fc4c9f5c675 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/parameter_estimation_example.py @@ -0,0 +1,42 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import json +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model + + +def main(): + # Vars to estimate + theta_names = ['k1', 'k2', 'E1', 'E2'] + + # Data, list of dictionaries + data = [] + file_dirname = dirname(abspath(str(__file__))) + for exp_num in range(10): + file_name = abspath(join(file_dirname, 'exp' + str(exp_num + 1) + '.out')) + with open(file_name, 'r') as infile: + d = json.load(infile) + data.append(d) + + # Note, the model already includes a 'SecondStageCost' expression + # for sum of squared error that will be used in parameter estimation + + pest = parmest.Estimator(generate_model, data, theta_names) + + obj, theta = pest.theta_est() + print(obj) + print(theta) + + +if __name__ == '__main__': + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/scenario_example.py b/pyomo/contrib/parmest/examples_deprecated/semibatch/scenario_example.py new file mode 100644 index 00000000000..071e53236c4 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/scenario_example.py @@ -0,0 +1,52 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import json +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model +import pyomo.contrib.parmest.scenariocreator as sc + + +def main(): + # Vars to estimate in parmest + theta_names = ['k1', 'k2', 'E1', 'E2'] + + # Data: list of dictionaries + data = [] + file_dirname = dirname(abspath(str(__file__))) + for exp_num in range(10): + fname = join(file_dirname, 'exp' + str(exp_num + 1) + '.out') + with open(fname, 'r') as infile: + d = json.load(infile) + data.append(d) + + pest = parmest.Estimator(generate_model, data, theta_names) + + scenmaker = sc.ScenarioCreator(pest, "ipopt") + + # Make one scenario per experiment and write to a csv file + output_file = "scenarios.csv" + experimentscens = sc.ScenarioSet("Experiments") + scenmaker.ScenariosFromExperiments(experimentscens) + experimentscens.write_csv(output_file) + + # Use the bootstrap to make 3 scenarios and print + bootscens = sc.ScenarioSet("Bootstrap") + scenmaker.ScenariosFromBootstrap(bootscens, 3) + for s in bootscens.ScensIterator(): + print("{}, {}".format(s.name, s.probability)) + for n, v in s.ThetaVals.items(): + print(" {}={}".format(n, v)) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/scenarios.csv b/pyomo/contrib/parmest/examples_deprecated/semibatch/scenarios.csv new file mode 100644 index 00000000000..22f9a651bc3 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/scenarios.csv @@ -0,0 +1,11 @@ +Name,Probability,k1,k2,E1,E2 +ExpScen0,0.1,25.800350800448314,14.14421520525348,31505.74905064048,35000.0 +ExpScen1,0.1,25.128373083865036,149.99999951481198,31452.336651974012,41938.781301641866 +ExpScen2,0.1,22.225574065344002,130.92739780265404,30948.669111672247,41260.15420929141 +ExpScen3,0.1,100.0,149.99999970011854,35182.73130744844,41444.52600373733 +ExpScen4,0.1,82.99114366189944,45.95424665995078,34810.857217141674,38300.633349887314 +ExpScen5,0.1,100.0,150.0,35142.20219150486,41495.41105795494 +ExpScen6,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 +ExpScen7,0.1,2.754580914035567,14.381786096822475,25000.0,35000.0 +ExpScen8,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 +ExpScen9,0.1,2.669780822294865,150.0,25000.0,41514.7476113499 diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/semibatch.py b/pyomo/contrib/parmest/examples_deprecated/semibatch/semibatch.py new file mode 100644 index 00000000000..6762531a338 --- /dev/null +++ b/pyomo/contrib/parmest/examples_deprecated/semibatch/semibatch.py @@ -0,0 +1,287 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +""" +Semibatch model, based on Nicholson et al. (2018). pyomo.dae: A modeling and +automatic discretization framework for optimization with di +erential and +algebraic equations. Mathematical Programming Computation, 10(2), 187-223. +""" +import json +from os.path import join, abspath, dirname +from pyomo.environ import ( + ConcreteModel, + Set, + Param, + Var, + Constraint, + ConstraintList, + Expression, + Objective, + TransformationFactory, + SolverFactory, + exp, + minimize, +) +from pyomo.dae import ContinuousSet, DerivativeVar + + +def generate_model(data): + # if data is a file name, then load file first + if isinstance(data, str): + file_name = data + try: + with open(file_name, "r") as infile: + data = json.load(infile) + except: + raise RuntimeError(f"Could not read {file_name} as json") + + # unpack and fix the data + cameastemp = data["Ca_meas"] + cbmeastemp = data["Cb_meas"] + ccmeastemp = data["Cc_meas"] + trmeastemp = data["Tr_meas"] + + cameas = {} + cbmeas = {} + ccmeas = {} + trmeas = {} + for i in cameastemp.keys(): + cameas[float(i)] = cameastemp[i] + cbmeas[float(i)] = cbmeastemp[i] + ccmeas[float(i)] = ccmeastemp[i] + trmeas[float(i)] = trmeastemp[i] + + m = ConcreteModel() + + # + # Measurement Data + # + m.measT = Set(initialize=sorted(cameas.keys())) + m.Ca_meas = Param(m.measT, initialize=cameas) + m.Cb_meas = Param(m.measT, initialize=cbmeas) + m.Cc_meas = Param(m.measT, initialize=ccmeas) + m.Tr_meas = Param(m.measT, initialize=trmeas) + + # + # Parameters for semi-batch reactor model + # + m.R = Param(initialize=8.314) # kJ/kmol/K + m.Mwa = Param(initialize=50.0) # kg/kmol + m.rhor = Param(initialize=1000.0) # kg/m^3 + m.cpr = Param(initialize=3.9) # kJ/kg/K + m.Tf = Param(initialize=300) # K + m.deltaH1 = Param(initialize=-40000.0) # kJ/kmol + m.deltaH2 = Param(initialize=-50000.0) # kJ/kmol + m.alphaj = Param(initialize=0.8) # kJ/s/m^2/K + m.alphac = Param(initialize=0.7) # kJ/s/m^2/K + m.Aj = Param(initialize=5.0) # m^2 + m.Ac = Param(initialize=3.0) # m^2 + m.Vj = Param(initialize=0.9) # m^3 + m.Vc = Param(initialize=0.07) # m^3 + m.rhow = Param(initialize=700.0) # kg/m^3 + m.cpw = Param(initialize=3.1) # kJ/kg/K + m.Ca0 = Param(initialize=data["Ca0"]) # kmol/m^3) + m.Cb0 = Param(initialize=data["Cb0"]) # kmol/m^3) + m.Cc0 = Param(initialize=data["Cc0"]) # kmol/m^3) + m.Tr0 = Param(initialize=300.0) # K + m.Vr0 = Param(initialize=1.0) # m^3 + + m.time = ContinuousSet(bounds=(0, 21600), initialize=m.measT) # Time in seconds + + # + # Control Inputs + # + def _initTc(m, t): + if t < 10800: + return data["Tc1"] + else: + return data["Tc2"] + + m.Tc = Param( + m.time, initialize=_initTc, default=_initTc + ) # bounds= (288,432) Cooling coil temp, control input + + def _initFa(m, t): + if t < 10800: + return data["Fa1"] + else: + return data["Fa2"] + + m.Fa = Param( + m.time, initialize=_initFa, default=_initFa + ) # bounds=(0,0.05) Inlet flow rate, control input + + # + # Parameters being estimated + # + m.k1 = Var(initialize=14, bounds=(2, 100)) # 1/s Actual: 15.01 + m.k2 = Var(initialize=90, bounds=(2, 150)) # 1/s Actual: 85.01 + m.E1 = Var(initialize=27000.0, bounds=(25000, 40000)) # kJ/kmol Actual: 30000 + m.E2 = Var(initialize=45000.0, bounds=(35000, 50000)) # kJ/kmol Actual: 40000 + # m.E1.fix(30000) + # m.E2.fix(40000) + + # + # Time dependent variables + # + m.Ca = Var(m.time, initialize=m.Ca0, bounds=(0, 25)) + m.Cb = Var(m.time, initialize=m.Cb0, bounds=(0, 25)) + m.Cc = Var(m.time, initialize=m.Cc0, bounds=(0, 25)) + m.Vr = Var(m.time, initialize=m.Vr0) + m.Tr = Var(m.time, initialize=m.Tr0) + m.Tj = Var( + m.time, initialize=310.0, bounds=(288, None) + ) # Cooling jacket temp, follows coil temp until failure + + # + # Derivatives in the model + # + m.dCa = DerivativeVar(m.Ca) + m.dCb = DerivativeVar(m.Cb) + m.dCc = DerivativeVar(m.Cc) + m.dVr = DerivativeVar(m.Vr) + m.dTr = DerivativeVar(m.Tr) + + # + # Differential Equations in the model + # + + def _dCacon(m, t): + if t == 0: + return Constraint.Skip + return ( + m.dCa[t] + == m.Fa[t] / m.Vr[t] - m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] + ) + + m.dCacon = Constraint(m.time, rule=_dCacon) + + def _dCbcon(m, t): + if t == 0: + return Constraint.Skip + return ( + m.dCb[t] + == m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] + - m.k2 * exp(-m.E2 / (m.R * m.Tr[t])) * m.Cb[t] + ) + + m.dCbcon = Constraint(m.time, rule=_dCbcon) + + def _dCccon(m, t): + if t == 0: + return Constraint.Skip + return m.dCc[t] == m.k2 * exp(-m.E2 / (m.R * m.Tr[t])) * m.Cb[t] + + m.dCccon = Constraint(m.time, rule=_dCccon) + + def _dVrcon(m, t): + if t == 0: + return Constraint.Skip + return m.dVr[t] == m.Fa[t] * m.Mwa / m.rhor + + m.dVrcon = Constraint(m.time, rule=_dVrcon) + + def _dTrcon(m, t): + if t == 0: + return Constraint.Skip + return m.rhor * m.cpr * m.dTr[t] == m.Fa[t] * m.Mwa * m.cpr / m.Vr[t] * ( + m.Tf - m.Tr[t] + ) - m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] * m.deltaH1 - m.k2 * exp( + -m.E2 / (m.R * m.Tr[t]) + ) * m.Cb[ + t + ] * m.deltaH2 + m.alphaj * m.Aj / m.Vr0 * ( + m.Tj[t] - m.Tr[t] + ) + m.alphac * m.Ac / m.Vr0 * ( + m.Tc[t] - m.Tr[t] + ) + + m.dTrcon = Constraint(m.time, rule=_dTrcon) + + def _singlecooling(m, t): + return m.Tc[t] == m.Tj[t] + + m.singlecooling = Constraint(m.time, rule=_singlecooling) + + # Initial Conditions + def _initcon(m): + yield m.Ca[m.time.first()] == m.Ca0 + yield m.Cb[m.time.first()] == m.Cb0 + yield m.Cc[m.time.first()] == m.Cc0 + yield m.Vr[m.time.first()] == m.Vr0 + yield m.Tr[m.time.first()] == m.Tr0 + + m.initcon = ConstraintList(rule=_initcon) + + # + # Stage-specific cost computations + # + def ComputeFirstStageCost_rule(model): + return 0 + + m.FirstStageCost = Expression(rule=ComputeFirstStageCost_rule) + + def AllMeasurements(m): + return sum( + (m.Ca[t] - m.Ca_meas[t]) ** 2 + + (m.Cb[t] - m.Cb_meas[t]) ** 2 + + (m.Cc[t] - m.Cc_meas[t]) ** 2 + + 0.01 * (m.Tr[t] - m.Tr_meas[t]) ** 2 + for t in m.measT + ) + + def MissingMeasurements(m): + if data["experiment"] == 1: + return sum( + (m.Ca[t] - m.Ca_meas[t]) ** 2 + + (m.Cb[t] - m.Cb_meas[t]) ** 2 + + (m.Cc[t] - m.Cc_meas[t]) ** 2 + + (m.Tr[t] - m.Tr_meas[t]) ** 2 + for t in m.measT + ) + elif data["experiment"] == 2: + return sum((m.Tr[t] - m.Tr_meas[t]) ** 2 for t in m.measT) + else: + return sum( + (m.Cb[t] - m.Cb_meas[t]) ** 2 + (m.Tr[t] - m.Tr_meas[t]) ** 2 + for t in m.measT + ) + + m.SecondStageCost = Expression(rule=MissingMeasurements) + + def total_cost_rule(model): + return model.FirstStageCost + model.SecondStageCost + + m.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) + + # Discretize model + disc = TransformationFactory("dae.collocation") + disc.apply_to(m, nfe=20, ncp=4) + return m + + +def main(): + # Data loaded from files + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "exp2.out")) + with open(file_name, "r") as infile: + data = json.load(infile) + data["experiment"] = 2 + + model = generate_model(data) + solver = SolverFactory("ipopt") + solver.solve(model) + print("k1 = ", model.k1()) + print("E1 = ", model.E1()) + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index cbdc9179f35..dc747217b31 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -63,6 +63,8 @@ import pyomo.contrib.parmest.graphics as graphics from pyomo.dae import ContinuousSet +import pyomo.contrib.parmest.parmest_deprecated as parmest_deprecated + parmest_available = numpy_available & pandas_available & scipy_available inverse_reduced_hessian, inverse_reduced_hessian_available = attempt_import( @@ -336,16 +338,32 @@ class Estimator(object): Provides options to the solver (also the name of an attribute) """ - def __init__( - self, - model_function, - data, - theta_names, - obj_function=None, - tee=False, - diagnostic_mode=False, - solver_options=None, - ): + # backwards compatible constructor will accept the old inputs + # from parmest_deprecated as well as the new inputs using experiment lists + def __init__(self, *args, **kwargs): + + # use deprecated interface + self.pest_deprecated = None + if len(args) > 1: + logger.warning('Using deprecated parmest inputs (model_function, ' + + 'data, theta_names), please use experiment lists instead.') + self.pest_deprecated = parmest_deprecated.Estimator(*args, **kwargs) + return + + print("New parmest interface using Experiment lists coming soon!") + exit() + + # def __init__( + # self, + # model_function, + # data, + # theta_names, + # obj_function=None, + # tee=False, + # diagnostic_mode=False, + # solver_options=None, + # ): + self.model_function = model_function assert isinstance( @@ -906,6 +924,15 @@ def theta_est( cov: pd.DataFrame Covariance matrix of the fitted parameters (only for solver='ef_ipopt') """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.theta_est( + solver=solver, + return_values=return_values, + calc_cov=calc_cov, + cov_n=cov_n) + assert isinstance(solver, str) assert isinstance(return_values, list) assert isinstance(calc_cov, bool) @@ -956,6 +983,16 @@ def theta_est_bootstrap( Theta values for each sample and (if return_samples = True) the sample numbers used in each estimation """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.theta_est_bootstrap( + bootstrap_samples, + samplesize=samplesize, + replacement=replacement, + seed=seed, + return_samples=return_samples) + assert isinstance(bootstrap_samples, int) assert isinstance(samplesize, (type(None), int)) assert isinstance(replacement, bool) @@ -1011,6 +1048,15 @@ def theta_est_leaveNout( Theta values for each sample and (if return_samples = True) the sample numbers left out of each estimation """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.theta_est_leaveNout( + lNo, + lNo_samples=lNo_samples, + seed=seed, + return_samples=return_samples) + assert isinstance(lNo, int) assert isinstance(lNo_samples, (type(None), int)) assert isinstance(seed, (type(None), int)) @@ -1084,6 +1130,16 @@ def leaveNout_bootstrap_test( indicates if the theta estimate is in (True) or out (False) of the alpha region for a given distribution (based on the bootstrap results) """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.leaveNout_bootstrap_test( + lNo, + lNo_samples, + bootstrap_samples, + distribution, alphas, + seed=seed) + assert isinstance(lNo, int) assert isinstance(lNo_samples, (type(None), int)) assert isinstance(bootstrap_samples, int) @@ -1144,6 +1200,13 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): Objective value for each theta (infeasible solutions are omitted). """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.objective_at_theta( + theta_values=theta_values, + initialize_parmest_model=initialize_parmest_model) + if len(self.theta_names) == 1 and self.theta_names[0] == 'parmest_dummy_var': pass # skip assertion if model has no fitted parameters else: @@ -1258,6 +1321,15 @@ def likelihood_ratio_test( thresholds: pd.Series If return_threshold = True, the thresholds are also returned. """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.likelihood_ratio_test( + obj_at_theta, + obj_value, + alphas, + return_thresholds=return_thresholds) + assert isinstance(obj_at_theta, pd.DataFrame) assert isinstance(obj_value, (int, float)) assert isinstance(alphas, list) @@ -1310,6 +1382,15 @@ def confidence_region_test( If test_theta_values is not None, returns test theta value along with True (inside) or False (outside) for each alpha """ + + # check if we are using deprecated parmest + if self.pest_deprecated is not None: + return self.pest_deprecated.confidence_region_test( + theta_values, + distribution, + alphas, + test_theta_values=test_theta_values) + assert isinstance(theta_values, pd.DataFrame) assert distribution in ['Rect', 'MVN', 'KDE'] assert isinstance(alphas, list) diff --git a/pyomo/contrib/parmest/parmest_deprecated.py b/pyomo/contrib/parmest/parmest_deprecated.py new file mode 100644 index 00000000000..cbdc9179f35 --- /dev/null +++ b/pyomo/contrib/parmest/parmest_deprecated.py @@ -0,0 +1,1366 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +#### Using mpi-sppy instead of PySP; May 2020 +#### Adding option for "local" EF starting Sept 2020 +#### Wrapping mpi-sppy functionality and local option Jan 2021, Feb 2021 + +# TODO: move use_mpisppy to a Pyomo configuration option +# +# False implies always use the EF that is local to parmest +use_mpisppy = True # Use it if we can but use local if not. +if use_mpisppy: + try: + # MPI-SPPY has an unfortunate side effect of outputting + # "[ 0.00] Initializing mpi-sppy" when it is imported. This can + # cause things like doctests to fail. We will suppress that + # information here. + from pyomo.common.tee import capture_output + + with capture_output(): + import mpisppy.utils.sputils as sputils + except ImportError: + use_mpisppy = False # we can't use it +if use_mpisppy: + # These things should be outside the try block. + sputils.disable_tictoc_output() + import mpisppy.opt.ef as st + import mpisppy.scenario_tree as scenario_tree +else: + import pyomo.contrib.parmest.utils.create_ef as local_ef + import pyomo.contrib.parmest.utils.scenario_tree as scenario_tree + +import re +import importlib as im +import logging +import types +import json +from itertools import combinations + +from pyomo.common.dependencies import ( + attempt_import, + numpy as np, + numpy_available, + pandas as pd, + pandas_available, + scipy, + scipy_available, +) + +import pyomo.environ as pyo + +from pyomo.opt import SolverFactory +from pyomo.environ import Block, ComponentUID + +import pyomo.contrib.parmest.utils as utils +import pyomo.contrib.parmest.graphics as graphics +from pyomo.dae import ContinuousSet + +parmest_available = numpy_available & pandas_available & scipy_available + +inverse_reduced_hessian, inverse_reduced_hessian_available = attempt_import( + 'pyomo.contrib.interior_point.inverse_reduced_hessian' +) + +logger = logging.getLogger(__name__) + + +def ef_nonants(ef): + # Wrapper to call someone's ef_nonants + # (the function being called is very short, but it might be changed) + if use_mpisppy: + return sputils.ef_nonants(ef) + else: + return local_ef.ef_nonants(ef) + + +def _experiment_instance_creation_callback( + scenario_name, node_names=None, cb_data=None +): + """ + This is going to be called by mpi-sppy or the local EF and it will call into + the user's model's callback. + + Parameters: + ----------- + scenario_name: `str` Scenario name should end with a number + node_names: `None` ( Not used here ) + cb_data : dict with ["callback"], ["BootList"], + ["theta_names"], ["cb_data"], etc. + "cb_data" is passed through to user's callback function + that is the "callback" value. + "BootList" is None or bootstrap experiment number list. + (called cb_data by mpisppy) + + + Returns: + -------- + instance: `ConcreteModel` + instantiated scenario + + Note: + ---- + There is flexibility both in how the function is passed and its signature. + """ + assert cb_data is not None + outer_cb_data = cb_data + scen_num_str = re.compile(r'(\d+)$').search(scenario_name).group(1) + scen_num = int(scen_num_str) + basename = scenario_name[: -len(scen_num_str)] # to reconstruct name + + CallbackFunction = outer_cb_data["callback"] + + if callable(CallbackFunction): + callback = CallbackFunction + else: + cb_name = CallbackFunction + + if "CallbackModule" not in outer_cb_data: + raise RuntimeError( + "Internal Error: need CallbackModule in parmest callback" + ) + else: + modname = outer_cb_data["CallbackModule"] + + if isinstance(modname, str): + cb_module = im.import_module(modname, package=None) + elif isinstance(modname, types.ModuleType): + cb_module = modname + else: + print("Internal Error: bad CallbackModule") + raise + + try: + callback = getattr(cb_module, cb_name) + except: + print("Error getting function=" + cb_name + " from module=" + str(modname)) + raise + + if "BootList" in outer_cb_data: + bootlist = outer_cb_data["BootList"] + # print("debug in callback: using bootlist=",str(bootlist)) + # assuming bootlist itself is zero based + exp_num = bootlist[scen_num] + else: + exp_num = scen_num + + scen_name = basename + str(exp_num) + + cb_data = outer_cb_data["cb_data"] # cb_data might be None. + + # at least three signatures are supported. The first is preferred + try: + instance = callback(experiment_number=exp_num, cb_data=cb_data) + except TypeError: + raise RuntimeError( + "Only one callback signature is supported: " + "callback(experiment_number, cb_data) " + ) + """ + try: + instance = callback(scenario_tree_model, scen_name, node_names) + except TypeError: # deprecated signature? + try: + instance = callback(scen_name, node_names) + except: + print("Failed to create instance using callback; TypeError+") + raise + except: + print("Failed to create instance using callback.") + raise + """ + if hasattr(instance, "_mpisppy_node_list"): + raise RuntimeError(f"scenario for experiment {exp_num} has _mpisppy_node_list") + nonant_list = [ + instance.find_component(vstr) for vstr in outer_cb_data["theta_names"] + ] + if use_mpisppy: + instance._mpisppy_node_list = [ + scenario_tree.ScenarioNode( + name="ROOT", + cond_prob=1.0, + stage=1, + cost_expression=instance.FirstStageCost, + nonant_list=nonant_list, + scen_model=instance, + ) + ] + else: + instance._mpisppy_node_list = [ + scenario_tree.ScenarioNode( + name="ROOT", + cond_prob=1.0, + stage=1, + cost_expression=instance.FirstStageCost, + scen_name_list=None, + nonant_list=nonant_list, + scen_model=instance, + ) + ] + + if "ThetaVals" in outer_cb_data: + thetavals = outer_cb_data["ThetaVals"] + + # dlw august 2018: see mea code for more general theta + for vstr in thetavals: + theta_cuid = ComponentUID(vstr) + theta_object = theta_cuid.find_component_on(instance) + if thetavals[vstr] is not None: + # print("Fixing",vstr,"at",str(thetavals[vstr])) + theta_object.fix(thetavals[vstr]) + else: + # print("Freeing",vstr) + theta_object.unfix() + + return instance + + +# ============================================= +def _treemaker(scenlist): + """ + Makes a scenario tree (avoids dependence on daps) + + Parameters + ---------- + scenlist (list of `int`): experiment (i.e. scenario) numbers + + Returns + ------- + a `ConcreteModel` that is the scenario tree + """ + + num_scenarios = len(scenlist) + m = scenario_tree.tree_structure_model.CreateAbstractScenarioTreeModel() + m = m.create_instance() + m.Stages.add('Stage1') + m.Stages.add('Stage2') + m.Nodes.add('RootNode') + for i in scenlist: + m.Nodes.add('LeafNode_Experiment' + str(i)) + m.Scenarios.add('Experiment' + str(i)) + m.NodeStage['RootNode'] = 'Stage1' + m.ConditionalProbability['RootNode'] = 1.0 + for node in m.Nodes: + if node != 'RootNode': + m.NodeStage[node] = 'Stage2' + m.Children['RootNode'].add(node) + m.Children[node].clear() + m.ConditionalProbability[node] = 1.0 / num_scenarios + m.ScenarioLeafNode[node.replace('LeafNode_', '')] = node + + return m + + +def group_data(data, groupby_column_name, use_mean=None): + """ + Group data by scenario + + Parameters + ---------- + data: DataFrame + Data + groupby_column_name: strings + Name of data column which contains scenario numbers + use_mean: list of column names or None, optional + Name of data columns which should be reduced to a single value per + scenario by taking the mean + + Returns + ---------- + grouped_data: list of dictionaries + Grouped data + """ + if use_mean is None: + use_mean_list = [] + else: + use_mean_list = use_mean + + grouped_data = [] + for exp_num, group in data.groupby(data[groupby_column_name]): + d = {} + for col in group.columns: + if col in use_mean_list: + d[col] = group[col].mean() + else: + d[col] = list(group[col]) + grouped_data.append(d) + + return grouped_data + + +class _SecondStageCostExpr(object): + """ + Class to pass objective expression into the Pyomo model + """ + + def __init__(self, ssc_function, data): + self._ssc_function = ssc_function + self._data = data + + def __call__(self, model): + return self._ssc_function(model, self._data) + + +class Estimator(object): + """ + Parameter estimation class + + Parameters + ---------- + model_function: function + Function that generates an instance of the Pyomo model using 'data' + as the input argument + data: pd.DataFrame, list of dictionaries, list of dataframes, or list of json file names + Data that is used to build an instance of the Pyomo model and build + the objective function + theta_names: list of strings + List of Var names to estimate + obj_function: function, optional + Function used to formulate parameter estimation objective, generally + sum of squared error between measurements and model variables. + If no function is specified, the model is used + "as is" and should be defined with a "FirstStageCost" and + "SecondStageCost" expression that are used to build an objective. + tee: bool, optional + Indicates that ef solver output should be teed + diagnostic_mode: bool, optional + If True, print diagnostics from the solver + solver_options: dict, optional + Provides options to the solver (also the name of an attribute) + """ + + def __init__( + self, + model_function, + data, + theta_names, + obj_function=None, + tee=False, + diagnostic_mode=False, + solver_options=None, + ): + self.model_function = model_function + + assert isinstance( + data, (list, pd.DataFrame) + ), "Data must be a list or DataFrame" + # convert dataframe into a list of dataframes, each row = one scenario + if isinstance(data, pd.DataFrame): + self.callback_data = [ + data.loc[i, :].to_frame().transpose() for i in data.index + ] + else: + self.callback_data = data + assert isinstance( + self.callback_data[0], (dict, pd.DataFrame, str) + ), "The scenarios in data must be a dictionary, DataFrame or filename" + + if len(theta_names) == 0: + self.theta_names = ['parmest_dummy_var'] + else: + self.theta_names = theta_names + + self.obj_function = obj_function + self.tee = tee + self.diagnostic_mode = diagnostic_mode + self.solver_options = solver_options + + self._second_stage_cost_exp = "SecondStageCost" + # boolean to indicate if model is initialized using a square solve + self.model_initialized = False + + def _return_theta_names(self): + """ + Return list of fitted model parameter names + """ + # if fitted model parameter names differ from theta_names created when Estimator object is created + if hasattr(self, 'theta_names_updated'): + return self.theta_names_updated + + else: + return ( + self.theta_names + ) # default theta_names, created when Estimator object is created + + def _create_parmest_model(self, data): + """ + Modify the Pyomo model for parameter estimation + """ + model = self.model_function(data) + + if (len(self.theta_names) == 1) and ( + self.theta_names[0] == 'parmest_dummy_var' + ): + model.parmest_dummy_var = pyo.Var(initialize=1.0) + + # Add objective function (optional) + if self.obj_function: + for obj in model.component_objects(pyo.Objective): + if obj.name in ["Total_Cost_Objective"]: + raise RuntimeError( + "Parmest will not override the existing model Objective named " + + obj.name + ) + obj.deactivate() + + for expr in model.component_data_objects(pyo.Expression): + if expr.name in ["FirstStageCost", "SecondStageCost"]: + raise RuntimeError( + "Parmest will not override the existing model Expression named " + + expr.name + ) + model.FirstStageCost = pyo.Expression(expr=0) + model.SecondStageCost = pyo.Expression( + rule=_SecondStageCostExpr(self.obj_function, data) + ) + + def TotalCost_rule(model): + return model.FirstStageCost + model.SecondStageCost + + model.Total_Cost_Objective = pyo.Objective( + rule=TotalCost_rule, sense=pyo.minimize + ) + + # Convert theta Params to Vars, and unfix theta Vars + model = utils.convert_params_to_vars(model, self.theta_names) + + # Update theta names list to use CUID string representation + for i, theta in enumerate(self.theta_names): + var_cuid = ComponentUID(theta) + var_validate = var_cuid.find_component_on(model) + if var_validate is None: + logger.warning( + "theta_name[%s] (%s) was not found on the model", (i, theta) + ) + else: + try: + # If the component is not a variable, + # this will generate an exception (and the warning + # in the 'except') + var_validate.unfix() + self.theta_names[i] = repr(var_cuid) + except: + logger.warning(theta + ' is not a variable') + + self.parmest_model = model + + return model + + def _instance_creation_callback(self, experiment_number=None, cb_data=None): + # cb_data is a list of dictionaries, list of dataframes, OR list of json file names + exp_data = cb_data[experiment_number] + if isinstance(exp_data, (dict, pd.DataFrame)): + pass + elif isinstance(exp_data, str): + try: + with open(exp_data, 'r') as infile: + exp_data = json.load(infile) + except: + raise RuntimeError(f'Could not read {exp_data} as json') + else: + raise RuntimeError(f'Unexpected data format for cb_data={cb_data}') + model = self._create_parmest_model(exp_data) + + return model + + def _Q_opt( + self, + ThetaVals=None, + solver="ef_ipopt", + return_values=[], + bootlist=None, + calc_cov=False, + cov_n=None, + ): + """ + Set up all thetas as first stage Vars, return resulting theta + values as well as the objective function value. + + """ + if solver == "k_aug": + raise RuntimeError("k_aug no longer supported.") + + # (Bootstrap scenarios will use indirection through the bootlist) + if bootlist is None: + scenario_numbers = list(range(len(self.callback_data))) + scen_names = ["Scenario{}".format(i) for i in scenario_numbers] + else: + scen_names = ["Scenario{}".format(i) for i in range(len(bootlist))] + + # tree_model.CallbackModule = None + outer_cb_data = dict() + outer_cb_data["callback"] = self._instance_creation_callback + if ThetaVals is not None: + outer_cb_data["ThetaVals"] = ThetaVals + if bootlist is not None: + outer_cb_data["BootList"] = bootlist + outer_cb_data["cb_data"] = self.callback_data # None is OK + outer_cb_data["theta_names"] = self.theta_names + + options = {"solver": "ipopt"} + scenario_creator_options = {"cb_data": outer_cb_data} + if use_mpisppy: + ef = sputils.create_EF( + scen_names, + _experiment_instance_creation_callback, + EF_name="_Q_opt", + suppress_warnings=True, + scenario_creator_kwargs=scenario_creator_options, + ) + else: + ef = local_ef.create_EF( + scen_names, + _experiment_instance_creation_callback, + EF_name="_Q_opt", + suppress_warnings=True, + scenario_creator_kwargs=scenario_creator_options, + ) + self.ef_instance = ef + + # Solve the extensive form with ipopt + if solver == "ef_ipopt": + if not calc_cov: + # Do not calculate the reduced hessian + + solver = SolverFactory('ipopt') + if self.solver_options is not None: + for key in self.solver_options: + solver.options[key] = self.solver_options[key] + + solve_result = solver.solve(self.ef_instance, tee=self.tee) + + # The import error will be raised when we attempt to use + # inv_reduced_hessian_barrier below. + # + # elif not asl_available: + # raise ImportError("parmest requires ASL to calculate the " + # "covariance matrix with solver 'ipopt'") + else: + # parmest makes the fitted parameters stage 1 variables + ind_vars = [] + for ndname, Var, solval in ef_nonants(ef): + ind_vars.append(Var) + # calculate the reduced hessian + ( + solve_result, + inv_red_hes, + ) = inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, + ) + + if self.diagnostic_mode: + print( + ' Solver termination condition = ', + str(solve_result.solver.termination_condition), + ) + + # assume all first stage are thetas... + thetavals = {} + for ndname, Var, solval in ef_nonants(ef): + # process the name + # the scenarios are blocks, so strip the scenario name + vname = Var.name[Var.name.find(".") + 1 :] + thetavals[vname] = solval + + objval = pyo.value(ef.EF_Obj) + + if calc_cov: + # Calculate the covariance matrix + + # Number of data points considered + n = cov_n + + # Extract number of fitted parameters + l = len(thetavals) + + # Assumption: Objective value is sum of squared errors + sse = objval + + '''Calculate covariance assuming experimental observation errors are + independent and follow a Gaussian + distribution with constant variance. + + The formula used in parmest was verified against equations (7-5-15) and + (7-5-16) in "Nonlinear Parameter Estimation", Y. Bard, 1974. + + This formula is also applicable if the objective is scaled by a constant; + the constant cancels out. (was scaled by 1/n because it computes an + expected value.) + ''' + cov = 2 * sse / (n - l) * inv_red_hes + cov = pd.DataFrame( + cov, index=thetavals.keys(), columns=thetavals.keys() + ) + + thetavals = pd.Series(thetavals) + + if len(return_values) > 0: + var_values = [] + if len(scen_names) > 1: # multiple scenarios + block_objects = self.ef_instance.component_objects( + Block, descend_into=False + ) + else: # single scenario + block_objects = [self.ef_instance] + for exp_i in block_objects: + vals = {} + for var in return_values: + exp_i_var = exp_i.find_component(str(var)) + if ( + exp_i_var is None + ): # we might have a block such as _mpisppy_data + continue + # if value to return is ContinuousSet + if type(exp_i_var) == ContinuousSet: + temp = list(exp_i_var) + else: + temp = [pyo.value(_) for _ in exp_i_var.values()] + if len(temp) == 1: + vals[var] = temp[0] + else: + vals[var] = temp + if len(vals) > 0: + var_values.append(vals) + var_values = pd.DataFrame(var_values) + if calc_cov: + return objval, thetavals, var_values, cov + else: + return objval, thetavals, var_values + + if calc_cov: + return objval, thetavals, cov + else: + return objval, thetavals + + else: + raise RuntimeError("Unknown solver in Q_Opt=" + solver) + + def _Q_at_theta(self, thetavals, initialize_parmest_model=False): + """ + Return the objective function value with fixed theta values. + + Parameters + ---------- + thetavals: dict + A dictionary of theta values. + + initialize_parmest_model: boolean + If True: Solve square problem instance, build extensive form of the model for + parameter estimation, and set flag model_initialized to True + + Returns + ------- + objectiveval: float + The objective function value. + thetavals: dict + A dictionary of all values for theta that were input. + solvertermination: Pyomo TerminationCondition + Tries to return the "worst" solver status across the scenarios. + pyo.TerminationCondition.optimal is the best and + pyo.TerminationCondition.infeasible is the worst. + """ + + optimizer = pyo.SolverFactory('ipopt') + + if len(thetavals) > 0: + dummy_cb = { + "callback": self._instance_creation_callback, + "ThetaVals": thetavals, + "theta_names": self._return_theta_names(), + "cb_data": self.callback_data, + } + else: + dummy_cb = { + "callback": self._instance_creation_callback, + "theta_names": self._return_theta_names(), + "cb_data": self.callback_data, + } + + if self.diagnostic_mode: + if len(thetavals) > 0: + print(' Compute objective at theta = ', str(thetavals)) + else: + print(' Compute objective at initial theta') + + # start block of code to deal with models with no constraints + # (ipopt will crash or complain on such problems without special care) + instance = _experiment_instance_creation_callback("FOO0", None, dummy_cb) + try: # deal with special problems so Ipopt will not crash + first = next(instance.component_objects(pyo.Constraint, active=True)) + active_constraints = True + except: + active_constraints = False + # end block of code to deal with models with no constraints + + WorstStatus = pyo.TerminationCondition.optimal + totobj = 0 + scenario_numbers = list(range(len(self.callback_data))) + if initialize_parmest_model: + # create dictionary to store pyomo model instances (scenarios) + scen_dict = dict() + + for snum in scenario_numbers: + sname = "scenario_NODE" + str(snum) + instance = _experiment_instance_creation_callback(sname, None, dummy_cb) + + if initialize_parmest_model: + # list to store fitted parameter names that will be unfixed + # after initialization + theta_init_vals = [] + # use appropriate theta_names member + theta_ref = self._return_theta_names() + + for i, theta in enumerate(theta_ref): + # Use parser in ComponentUID to locate the component + var_cuid = ComponentUID(theta) + var_validate = var_cuid.find_component_on(instance) + if var_validate is None: + logger.warning( + "theta_name %s was not found on the model", (theta) + ) + else: + try: + if len(thetavals) == 0: + var_validate.fix() + else: + var_validate.fix(thetavals[theta]) + theta_init_vals.append(var_validate) + except: + logger.warning( + 'Unable to fix model parameter value for %s (not a Pyomo model Var)', + (theta), + ) + + if active_constraints: + if self.diagnostic_mode: + print(' Experiment = ', snum) + print(' First solve with special diagnostics wrapper') + ( + status_obj, + solved, + iters, + time, + regu, + ) = utils.ipopt_solve_with_stats( + instance, optimizer, max_iter=500, max_cpu_time=120 + ) + print( + " status_obj, solved, iters, time, regularization_stat = ", + str(status_obj), + str(solved), + str(iters), + str(time), + str(regu), + ) + + results = optimizer.solve(instance) + if self.diagnostic_mode: + print( + 'standard solve solver termination condition=', + str(results.solver.termination_condition), + ) + + if ( + results.solver.termination_condition + != pyo.TerminationCondition.optimal + ): + # DLW: Aug2018: not distinguishing "middlish" conditions + if WorstStatus != pyo.TerminationCondition.infeasible: + WorstStatus = results.solver.termination_condition + if initialize_parmest_model: + if self.diagnostic_mode: + print( + "Scenario {:d} infeasible with initialized parameter values".format( + snum + ) + ) + else: + if initialize_parmest_model: + if self.diagnostic_mode: + print( + "Scenario {:d} initialization successful with initial parameter values".format( + snum + ) + ) + if initialize_parmest_model: + # unfix parameters after initialization + for theta in theta_init_vals: + theta.unfix() + scen_dict[sname] = instance + else: + if initialize_parmest_model: + # unfix parameters after initialization + for theta in theta_init_vals: + theta.unfix() + scen_dict[sname] = instance + + objobject = getattr(instance, self._second_stage_cost_exp) + objval = pyo.value(objobject) + totobj += objval + + retval = totobj / len(scenario_numbers) # -1?? + if initialize_parmest_model and not hasattr(self, 'ef_instance'): + # create extensive form of the model using scenario dictionary + if len(scen_dict) > 0: + for scen in scen_dict.values(): + scen._mpisppy_probability = 1 / len(scen_dict) + + if use_mpisppy: + EF_instance = sputils._create_EF_from_scen_dict( + scen_dict, + EF_name="_Q_at_theta", + # suppress_warnings=True + ) + else: + EF_instance = local_ef._create_EF_from_scen_dict( + scen_dict, EF_name="_Q_at_theta", nonant_for_fixed_vars=True + ) + + self.ef_instance = EF_instance + # set self.model_initialized flag to True to skip extensive form model + # creation using theta_est() + self.model_initialized = True + + # return initialized theta values + if len(thetavals) == 0: + # use appropriate theta_names member + theta_ref = self._return_theta_names() + for i, theta in enumerate(theta_ref): + thetavals[theta] = theta_init_vals[i]() + + return retval, thetavals, WorstStatus + + def _get_sample_list(self, samplesize, num_samples, replacement=True): + samplelist = list() + + scenario_numbers = list(range(len(self.callback_data))) + + if num_samples is None: + # This could get very large + for i, l in enumerate(combinations(scenario_numbers, samplesize)): + samplelist.append((i, np.sort(l))) + else: + for i in range(num_samples): + attempts = 0 + unique_samples = 0 # check for duplicates in each sample + duplicate = False # check for duplicates between samples + while (unique_samples <= len(self._return_theta_names())) and ( + not duplicate + ): + sample = np.random.choice( + scenario_numbers, samplesize, replace=replacement + ) + sample = np.sort(sample).tolist() + unique_samples = len(np.unique(sample)) + if sample in samplelist: + duplicate = True + + attempts += 1 + if attempts > num_samples: # arbitrary timeout limit + raise RuntimeError( + """Internal error: timeout constructing + a sample, the dim of theta may be too + close to the samplesize""" + ) + + samplelist.append((i, sample)) + + return samplelist + + def theta_est( + self, solver="ef_ipopt", return_values=[], calc_cov=False, cov_n=None + ): + """ + Parameter estimation using all scenarios in the data + + Parameters + ---------- + solver: string, optional + Currently only "ef_ipopt" is supported. Default is "ef_ipopt". + return_values: list, optional + List of Variable names, used to return values from the model for data reconciliation + calc_cov: boolean, optional + If True, calculate and return the covariance matrix (only for "ef_ipopt" solver) + cov_n: int, optional + If calc_cov=True, then the user needs to supply the number of datapoints + that are used in the objective function + + Returns + ------- + objectiveval: float + The objective function value + thetavals: pd.Series + Estimated values for theta + variable values: pd.DataFrame + Variable values for each variable name in return_values (only for solver='ef_ipopt') + cov: pd.DataFrame + Covariance matrix of the fitted parameters (only for solver='ef_ipopt') + """ + assert isinstance(solver, str) + assert isinstance(return_values, list) + assert isinstance(calc_cov, bool) + if calc_cov: + assert isinstance( + cov_n, int + ), "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" + assert cov_n > len( + self._return_theta_names() + ), "The number of datapoints must be greater than the number of parameters to estimate" + + return self._Q_opt( + solver=solver, + return_values=return_values, + bootlist=None, + calc_cov=calc_cov, + cov_n=cov_n, + ) + + def theta_est_bootstrap( + self, + bootstrap_samples, + samplesize=None, + replacement=True, + seed=None, + return_samples=False, + ): + """ + Parameter estimation using bootstrap resampling of the data + + Parameters + ---------- + bootstrap_samples: int + Number of bootstrap samples to draw from the data + samplesize: int or None, optional + Size of each bootstrap sample. If samplesize=None, samplesize will be + set to the number of samples in the data + replacement: bool, optional + Sample with or without replacement + seed: int or None, optional + Random seed + return_samples: bool, optional + Return a list of sample numbers used in each bootstrap estimation + + Returns + ------- + bootstrap_theta: pd.DataFrame + Theta values for each sample and (if return_samples = True) + the sample numbers used in each estimation + """ + assert isinstance(bootstrap_samples, int) + assert isinstance(samplesize, (type(None), int)) + assert isinstance(replacement, bool) + assert isinstance(seed, (type(None), int)) + assert isinstance(return_samples, bool) + + if samplesize is None: + samplesize = len(self.callback_data) + + if seed is not None: + np.random.seed(seed) + + global_list = self._get_sample_list(samplesize, bootstrap_samples, replacement) + + task_mgr = utils.ParallelTaskManager(bootstrap_samples) + local_list = task_mgr.global_to_local_data(global_list) + + bootstrap_theta = list() + for idx, sample in local_list: + objval, thetavals = self._Q_opt(bootlist=list(sample)) + thetavals['samples'] = sample + bootstrap_theta.append(thetavals) + + global_bootstrap_theta = task_mgr.allgather_global_data(bootstrap_theta) + bootstrap_theta = pd.DataFrame(global_bootstrap_theta) + + if not return_samples: + del bootstrap_theta['samples'] + + return bootstrap_theta + + def theta_est_leaveNout( + self, lNo, lNo_samples=None, seed=None, return_samples=False + ): + """ + Parameter estimation where N data points are left out of each sample + + Parameters + ---------- + lNo: int + Number of data points to leave out for parameter estimation + lNo_samples: int + Number of leave-N-out samples. If lNo_samples=None, the maximum + number of combinations will be used + seed: int or None, optional + Random seed + return_samples: bool, optional + Return a list of sample numbers that were left out + + Returns + ------- + lNo_theta: pd.DataFrame + Theta values for each sample and (if return_samples = True) + the sample numbers left out of each estimation + """ + assert isinstance(lNo, int) + assert isinstance(lNo_samples, (type(None), int)) + assert isinstance(seed, (type(None), int)) + assert isinstance(return_samples, bool) + + samplesize = len(self.callback_data) - lNo + + if seed is not None: + np.random.seed(seed) + + global_list = self._get_sample_list(samplesize, lNo_samples, replacement=False) + + task_mgr = utils.ParallelTaskManager(len(global_list)) + local_list = task_mgr.global_to_local_data(global_list) + + lNo_theta = list() + for idx, sample in local_list: + objval, thetavals = self._Q_opt(bootlist=list(sample)) + lNo_s = list(set(range(len(self.callback_data))) - set(sample)) + thetavals['lNo'] = np.sort(lNo_s) + lNo_theta.append(thetavals) + + global_bootstrap_theta = task_mgr.allgather_global_data(lNo_theta) + lNo_theta = pd.DataFrame(global_bootstrap_theta) + + if not return_samples: + del lNo_theta['lNo'] + + return lNo_theta + + def leaveNout_bootstrap_test( + self, lNo, lNo_samples, bootstrap_samples, distribution, alphas, seed=None + ): + """ + Leave-N-out bootstrap test to compare theta values where N data points are + left out to a bootstrap analysis using the remaining data, + results indicate if theta is within a confidence region + determined by the bootstrap analysis + + Parameters + ---------- + lNo: int + Number of data points to leave out for parameter estimation + lNo_samples: int + Leave-N-out sample size. If lNo_samples=None, the maximum number + of combinations will be used + bootstrap_samples: int: + Bootstrap sample size + distribution: string + Statistical distribution used to define a confidence region, + options = 'MVN' for multivariate_normal, 'KDE' for gaussian_kde, + and 'Rect' for rectangular. + alphas: list + List of alpha values used to determine if theta values are inside + or outside the region. + seed: int or None, optional + Random seed + + Returns + ---------- + List of tuples with one entry per lNo_sample: + + * The first item in each tuple is the list of N samples that are left + out. + * The second item in each tuple is a DataFrame of theta estimated using + the N samples. + * The third item in each tuple is a DataFrame containing results from + the bootstrap analysis using the remaining samples. + + For each DataFrame a column is added for each value of alpha which + indicates if the theta estimate is in (True) or out (False) of the + alpha region for a given distribution (based on the bootstrap results) + """ + assert isinstance(lNo, int) + assert isinstance(lNo_samples, (type(None), int)) + assert isinstance(bootstrap_samples, int) + assert distribution in ['Rect', 'MVN', 'KDE'] + assert isinstance(alphas, list) + assert isinstance(seed, (type(None), int)) + + if seed is not None: + np.random.seed(seed) + + data = self.callback_data.copy() + + global_list = self._get_sample_list(lNo, lNo_samples, replacement=False) + + results = [] + for idx, sample in global_list: + # Reset callback_data to only include the sample + self.callback_data = [data[i] for i in sample] + + obj, theta = self.theta_est() + + # Reset callback_data to include all scenarios except the sample + self.callback_data = [data[i] for i in range(len(data)) if i not in sample] + + bootstrap_theta = self.theta_est_bootstrap(bootstrap_samples) + + training, test = self.confidence_region_test( + bootstrap_theta, + distribution=distribution, + alphas=alphas, + test_theta_values=theta, + ) + + results.append((sample, test, training)) + + # Reset callback_data (back to full data set) + self.callback_data = data + + return results + + def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): + """ + Objective value for each theta + + Parameters + ---------- + theta_values: pd.DataFrame, columns=theta_names + Values of theta used to compute the objective + + initialize_parmest_model: boolean + If True: Solve square problem instance, build extensive form of the model for + parameter estimation, and set flag model_initialized to True + + + Returns + ------- + obj_at_theta: pd.DataFrame + Objective value for each theta (infeasible solutions are + omitted). + """ + if len(self.theta_names) == 1 and self.theta_names[0] == 'parmest_dummy_var': + pass # skip assertion if model has no fitted parameters + else: + # create a local instance of the pyomo model to access model variables and parameters + model_temp = self._create_parmest_model(self.callback_data[0]) + model_theta_list = [] # list to store indexed and non-indexed parameters + # iterate over original theta_names + for theta_i in self.theta_names: + var_cuid = ComponentUID(theta_i) + var_validate = var_cuid.find_component_on(model_temp) + # check if theta in theta_names are indexed + try: + # get component UID of Set over which theta is defined + set_cuid = ComponentUID(var_validate.index_set()) + # access and iterate over the Set to generate theta names as they appear + # in the pyomo model + set_validate = set_cuid.find_component_on(model_temp) + for s in set_validate: + self_theta_temp = repr(var_cuid) + "[" + repr(s) + "]" + # generate list of theta names + model_theta_list.append(self_theta_temp) + # if theta is not indexed, copy theta name to list as-is + except AttributeError: + self_theta_temp = repr(var_cuid) + model_theta_list.append(self_theta_temp) + except: + raise + # if self.theta_names is not the same as temp model_theta_list, + # create self.theta_names_updated + if set(self.theta_names) == set(model_theta_list) and len( + self.theta_names + ) == set(model_theta_list): + pass + else: + self.theta_names_updated = model_theta_list + + if theta_values is None: + all_thetas = {} # dictionary to store fitted variables + # use appropriate theta names member + theta_names = self._return_theta_names() + else: + assert isinstance(theta_values, pd.DataFrame) + # for parallel code we need to use lists and dicts in the loop + theta_names = theta_values.columns + # # check if theta_names are in model + for theta in list(theta_names): + theta_temp = theta.replace("'", "") # cleaning quotes from theta_names + + assert theta_temp in [ + t.replace("'", "") for t in model_theta_list + ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( + theta_temp, model_theta_list + ) + assert len(list(theta_names)) == len(model_theta_list) + + all_thetas = theta_values.to_dict('records') + + if all_thetas: + task_mgr = utils.ParallelTaskManager(len(all_thetas)) + local_thetas = task_mgr.global_to_local_data(all_thetas) + else: + if initialize_parmest_model: + task_mgr = utils.ParallelTaskManager( + 1 + ) # initialization performed using just 1 set of theta values + # walk over the mesh, return objective function + all_obj = list() + if len(all_thetas) > 0: + for Theta in local_thetas: + obj, thetvals, worststatus = self._Q_at_theta( + Theta, initialize_parmest_model=initialize_parmest_model + ) + if worststatus != pyo.TerminationCondition.infeasible: + all_obj.append(list(Theta.values()) + [obj]) + # DLW, Aug2018: should we also store the worst solver status? + else: + obj, thetvals, worststatus = self._Q_at_theta( + thetavals={}, initialize_parmest_model=initialize_parmest_model + ) + if worststatus != pyo.TerminationCondition.infeasible: + all_obj.append(list(thetvals.values()) + [obj]) + + global_all_obj = task_mgr.allgather_global_data(all_obj) + dfcols = list(theta_names) + ['obj'] + obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) + return obj_at_theta + + def likelihood_ratio_test( + self, obj_at_theta, obj_value, alphas, return_thresholds=False + ): + r""" + Likelihood ratio test to identify theta values within a confidence + region using the :math:`\chi^2` distribution + + Parameters + ---------- + obj_at_theta: pd.DataFrame, columns = theta_names + 'obj' + Objective values for each theta value (returned by + objective_at_theta) + obj_value: int or float + Objective value from parameter estimation using all data + alphas: list + List of alpha values to use in the chi2 test + return_thresholds: bool, optional + Return the threshold value for each alpha + + Returns + ------- + LR: pd.DataFrame + Objective values for each theta value along with True or False for + each alpha + thresholds: pd.Series + If return_threshold = True, the thresholds are also returned. + """ + assert isinstance(obj_at_theta, pd.DataFrame) + assert isinstance(obj_value, (int, float)) + assert isinstance(alphas, list) + assert isinstance(return_thresholds, bool) + + LR = obj_at_theta.copy() + S = len(self.callback_data) + thresholds = {} + for a in alphas: + chi2_val = scipy.stats.chi2.ppf(a, 2) + thresholds[a] = obj_value * ((chi2_val / (S - 2)) + 1) + LR[a] = LR['obj'] < thresholds[a] + + thresholds = pd.Series(thresholds) + + if return_thresholds: + return LR, thresholds + else: + return LR + + def confidence_region_test( + self, theta_values, distribution, alphas, test_theta_values=None + ): + """ + Confidence region test to determine if theta values are within a + rectangular, multivariate normal, or Gaussian kernel density distribution + for a range of alpha values + + Parameters + ---------- + theta_values: pd.DataFrame, columns = theta_names + Theta values used to generate a confidence region + (generally returned by theta_est_bootstrap) + distribution: string + Statistical distribution used to define a confidence region, + options = 'MVN' for multivariate_normal, 'KDE' for gaussian_kde, + and 'Rect' for rectangular. + alphas: list + List of alpha values used to determine if theta values are inside + or outside the region. + test_theta_values: pd.Series or pd.DataFrame, keys/columns = theta_names, optional + Additional theta values that are compared to the confidence region + to determine if they are inside or outside. + + Returns + training_results: pd.DataFrame + Theta value used to generate the confidence region along with True + (inside) or False (outside) for each alpha + test_results: pd.DataFrame + If test_theta_values is not None, returns test theta value along + with True (inside) or False (outside) for each alpha + """ + assert isinstance(theta_values, pd.DataFrame) + assert distribution in ['Rect', 'MVN', 'KDE'] + assert isinstance(alphas, list) + assert isinstance( + test_theta_values, (type(None), dict, pd.Series, pd.DataFrame) + ) + + if isinstance(test_theta_values, (dict, pd.Series)): + test_theta_values = pd.Series(test_theta_values).to_frame().transpose() + + training_results = theta_values.copy() + + if test_theta_values is not None: + test_result = test_theta_values.copy() + + for a in alphas: + if distribution == 'Rect': + lb, ub = graphics.fit_rect_dist(theta_values, a) + training_results[a] = (theta_values > lb).all(axis=1) & ( + theta_values < ub + ).all(axis=1) + + if test_theta_values is not None: + # use upper and lower bound from the training set + test_result[a] = (test_theta_values > lb).all(axis=1) & ( + test_theta_values < ub + ).all(axis=1) + + elif distribution == 'MVN': + dist = graphics.fit_mvn_dist(theta_values) + Z = dist.pdf(theta_values) + score = scipy.stats.scoreatpercentile(Z, (1 - a) * 100) + training_results[a] = Z >= score + + if test_theta_values is not None: + # use score from the training set + Z = dist.pdf(test_theta_values) + test_result[a] = Z >= score + + elif distribution == 'KDE': + dist = graphics.fit_kde_dist(theta_values) + Z = dist.pdf(theta_values.transpose()) + score = scipy.stats.scoreatpercentile(Z, (1 - a) * 100) + training_results[a] = Z >= score + + if test_theta_values is not None: + # use score from the training set + Z = dist.pdf(test_theta_values.transpose()) + test_result[a] = Z >= score + + if test_theta_values is not None: + return training_results, test_result + else: + return training_results diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 58d2d4da722..18c27ad1c86 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -14,6 +14,10 @@ import pyomo.environ as pyo +import pyomo.contrib.parmest.scenariocreator_deprecated as scen_deprecated + +import logging +logger = logging.getLogger(__name__) class ScenarioSet(object): """ @@ -119,8 +123,17 @@ class ScenarioCreator(object): """ def __init__(self, pest, solvername): - self.pest = pest - self.solvername = solvername + + # is this a deprecated pest object? + self.scen_deprecated = None + if pest.pest_deprecated is not None: + logger.warning("Using a deprecated parmest object for scenario " + + "creator, please recreate object using experiment lists.") + self.scen_deprecated = scen_deprecated.ScenarioCreator( + pest.pest_deprecated, solvername) + else: + self.pest = pest + self.solvername = solvername def ScenariosFromExperiments(self, addtoSet): """Creates new self.Scenarios list using the experiments only. @@ -131,6 +144,11 @@ def ScenariosFromExperiments(self, addtoSet): a ScenarioSet """ + # check if using deprecated pest object + if self.scen_deprecated is not None: + self.scen_deprecated.ScenariosFromExperiments(addtoSet) + return + assert isinstance(addtoSet, ScenarioSet) scenario_numbers = list(range(len(self.pest.callback_data))) @@ -160,6 +178,12 @@ def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): numtomake (int) : number of scenarios to create """ + # check if using deprecated pest object + if self.scen_deprecated is not None: + self.scen_deprecated.ScenariosFromBootstrap( + addtoSet, numtomake, seed=seed) + return + assert isinstance(addtoSet, ScenarioSet) bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) diff --git a/pyomo/contrib/parmest/scenariocreator_deprecated.py b/pyomo/contrib/parmest/scenariocreator_deprecated.py new file mode 100644 index 00000000000..af084d0712c --- /dev/null +++ b/pyomo/contrib/parmest/scenariocreator_deprecated.py @@ -0,0 +1,166 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# ScenariosCreator.py - Class to create and deliver scenarios using parmest +# DLW March 2020 + +import pyomo.environ as pyo + + +class ScenarioSet(object): + """ + Class to hold scenario sets + + Args: + name (str): name of the set (might be "") + + """ + + def __init__(self, name): + # Note: If there was a use-case, the list could be a dataframe. + self._scens = list() # use a df instead? + self.name = name # might be "" + + def _firstscen(self): + # Return the first scenario for testing and to get Theta names. + assert len(self._scens) > 0 + return self._scens[0] + + def ScensIterator(self): + """Usage: for scenario in ScensIterator()""" + return iter(self._scens) + + def ScenarioNumber(self, scennum): + """Returns the scenario with the given, zero-based number""" + return self._scens[scennum] + + def addone(self, scen): + """Add a scenario to the set + + Args: + scen (ParmestScen): the scenario to add + """ + assert isinstance(self._scens, list) + self._scens.append(scen) + + def append_bootstrap(self, bootstrap_theta): + """Append a bootstrap theta df to the scenario set; equally likely + + Args: + bootstrap_theta (dataframe): created by the bootstrap + Note: this can be cleaned up a lot with the list becomes a df, + which is why I put it in the ScenarioSet class. + """ + assert len(bootstrap_theta) > 0 + prob = 1.0 / len(bootstrap_theta) + + # dict of ThetaVal dicts + dfdict = bootstrap_theta.to_dict(orient='index') + + for index, ThetaVals in dfdict.items(): + name = "Bootstrap" + str(index) + self.addone(ParmestScen(name, ThetaVals, prob)) + + def write_csv(self, filename): + """write a csv file with the scenarios in the set + + Args: + filename (str): full path and full name of file + """ + if len(self._scens) == 0: + print("Empty scenario set, not writing file={}".format(filename)) + return + with open(filename, "w") as f: + f.write("Name,Probability") + for n in self._firstscen().ThetaVals.keys(): + f.write(",{}".format(n)) + f.write('\n') + for s in self.ScensIterator(): + f.write("{},{}".format(s.name, s.probability)) + for v in s.ThetaVals.values(): + f.write(",{}".format(v)) + f.write('\n') + + +class ParmestScen(object): + """A little container for scenarios; the Args are the attributes. + + Args: + name (str): name for reporting; might be "" + ThetaVals (dict): ThetaVals[name]=val + probability (float): probability of occurrence "near" these ThetaVals + """ + + def __init__(self, name, ThetaVals, probability): + self.name = name + assert isinstance(ThetaVals, dict) + self.ThetaVals = ThetaVals + self.probability = probability + + +############################################################ + + +class ScenarioCreator(object): + """Create scenarios from parmest. + + Args: + pest (Estimator): the parmest object + solvername (str): name of the solver (e.g. "ipopt") + + """ + + def __init__(self, pest, solvername): + self.pest = pest + self.solvername = solvername + + def ScenariosFromExperiments(self, addtoSet): + """Creates new self.Scenarios list using the experiments only. + + Args: + addtoSet (ScenarioSet): the scenarios will be added to this set + Returns: + a ScenarioSet + """ + + # assert isinstance(addtoSet, ScenarioSet) + + scenario_numbers = list(range(len(self.pest.callback_data))) + + prob = 1.0 / len(scenario_numbers) + for exp_num in scenario_numbers: + ##print("Experiment number=", exp_num) + model = self.pest._instance_creation_callback( + exp_num, self.pest.callback_data + ) + opt = pyo.SolverFactory(self.solvername) + results = opt.solve(model) # solves and updates model + ## pyo.check_termination_optimal(results) + ThetaVals = dict() + for theta in self.pest.theta_names: + tvar = eval('model.' + theta) + tval = pyo.value(tvar) + ##print(" theta, tval=", tvar, tval) + ThetaVals[theta] = tval + addtoSet.addone(ParmestScen("ExpScen" + str(exp_num), ThetaVals, prob)) + + def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): + """Creates new self.Scenarios list using the experiments only. + + Args: + addtoSet (ScenarioSet): the scenarios will be added to this set + numtomake (int) : number of scenarios to create + """ + + # assert isinstance(addtoSet, ScenarioSet) + + bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) + addtoSet.append_bootstrap(bootstrap_thetas) From 87aa19df3deebbb0bf987fa6628d6acb3f86a24c Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 9 Jan 2024 14:00:25 -0700 Subject: [PATCH 0304/3044] Moved parmest deprecated files to folder. --- .../examples}/__init__.py | 0 .../examples}/reaction_kinetics/__init__.py | 0 .../simple_reaction_parmest_example.py | 0 .../examples}/reactor_design/__init__.py | 0 .../reactor_design/bootstrap_example.py | 0 .../reactor_design/datarec_example.py | 0 .../reactor_design/leaveNout_example.py | 0 .../likelihood_ratio_example.py | 0 .../multisensor_data_example.py | 0 .../parameter_estimation_example.py | 0 .../examples}/reactor_design/reactor_data.csv | 0 .../reactor_data_multisensor.csv | 0 .../reactor_data_timeseries.csv | 0 .../reactor_design/reactor_design.py | 0 .../reactor_design/timeseries_data_example.py | 0 .../examples}/rooney_biegler/__init__.py | 0 .../rooney_biegler/bootstrap_example.py | 0 .../likelihood_ratio_example.py | 0 .../parameter_estimation_example.py | 0 .../rooney_biegler/rooney_biegler.py | 0 .../rooney_biegler_with_constraint.py | 0 .../examples}/semibatch/__init__.py | 0 .../examples}/semibatch/bootstrap_theta.csv | 0 .../examples}/semibatch/obj_at_theta.csv | 0 .../examples}/semibatch/parallel_example.py | 0 .../semibatch/parameter_estimation_example.py | 0 .../examples}/semibatch/scenario_example.py | 0 .../examples}/semibatch/scenarios.csv | 0 .../examples}/semibatch/semibatch.py | 0 .../parmest.py} | 0 .../scenariocreator.py} | 0 .../parmest/deprecated/tests/__init__.py | 10 + .../parmest/deprecated/tests/scenarios.csv | 11 + .../parmest/deprecated/tests/test_examples.py | 192 ++++ .../parmest/deprecated/tests/test_graphics.py | 68 ++ .../parmest/deprecated/tests/test_parmest.py | 958 ++++++++++++++++++ .../deprecated/tests/test_scenariocreator.py | 146 +++ .../parmest/deprecated/tests/test_solver.py | 75 ++ .../parmest/deprecated/tests/test_utils.py | 68 ++ pyomo/contrib/parmest/parmest.py | 21 +- pyomo/contrib/parmest/scenariocreator.py | 2 +- 41 files changed, 1543 insertions(+), 8 deletions(-) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/__init__.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reaction_kinetics/__init__.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reaction_kinetics/simple_reaction_parmest_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/__init__.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/bootstrap_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/datarec_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/leaveNout_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/likelihood_ratio_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/multisensor_data_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/parameter_estimation_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/reactor_data.csv (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/reactor_data_multisensor.csv (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/reactor_data_timeseries.csv (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/reactor_design.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/reactor_design/timeseries_data_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/rooney_biegler/__init__.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/rooney_biegler/bootstrap_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/rooney_biegler/likelihood_ratio_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/rooney_biegler/parameter_estimation_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/rooney_biegler/rooney_biegler.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/rooney_biegler/rooney_biegler_with_constraint.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/__init__.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/bootstrap_theta.csv (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/obj_at_theta.csv (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/parallel_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/parameter_estimation_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/scenario_example.py (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/scenarios.csv (100%) rename pyomo/contrib/parmest/{examples_deprecated => deprecated/examples}/semibatch/semibatch.py (100%) rename pyomo/contrib/parmest/{parmest_deprecated.py => deprecated/parmest.py} (100%) rename pyomo/contrib/parmest/{scenariocreator_deprecated.py => deprecated/scenariocreator.py} (100%) create mode 100644 pyomo/contrib/parmest/deprecated/tests/__init__.py create mode 100644 pyomo/contrib/parmest/deprecated/tests/scenarios.csv create mode 100644 pyomo/contrib/parmest/deprecated/tests/test_examples.py create mode 100644 pyomo/contrib/parmest/deprecated/tests/test_graphics.py create mode 100644 pyomo/contrib/parmest/deprecated/tests/test_parmest.py create mode 100644 pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py create mode 100644 pyomo/contrib/parmest/deprecated/tests/test_solver.py create mode 100644 pyomo/contrib/parmest/deprecated/tests/test_utils.py diff --git a/pyomo/contrib/parmest/examples_deprecated/__init__.py b/pyomo/contrib/parmest/deprecated/examples/__init__.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/__init__.py rename to pyomo/contrib/parmest/deprecated/examples/__init__.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/__init__.py b/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/__init__.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/__init__.py rename to pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/__init__.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/simple_reaction_parmest_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reaction_kinetics/simple_reaction_parmest_example.py rename to pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/simple_reaction_parmest_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/__init__.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/__init__.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/__init__.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/__init__.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/bootstrap_example.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/datarec_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/datarec_example.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/leaveNout_example.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/likelihood_ratio_example.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/multisensor_data_example.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/parameter_estimation_example.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data.csv b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data.csv similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data.csv rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data.csv diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_multisensor.csv b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_multisensor.csv similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_multisensor.csv rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_multisensor.csv diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_timeseries.csv b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_timeseries.csv similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_data_timeseries.csv rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_timeseries.csv diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_design.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_design.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/reactor_design.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_design.py diff --git a/pyomo/contrib/parmest/examples_deprecated/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/reactor_design/timeseries_data_example.py rename to pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/__init__.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/__init__.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/rooney_biegler/__init__.py rename to pyomo/contrib/parmest/deprecated/examples/rooney_biegler/__init__.py diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/bootstrap_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/rooney_biegler/bootstrap_example.py rename to pyomo/contrib/parmest/deprecated/examples/rooney_biegler/bootstrap_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/likelihood_ratio_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/rooney_biegler/likelihood_ratio_example.py rename to pyomo/contrib/parmest/deprecated/examples/rooney_biegler/likelihood_ratio_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/parameter_estimation_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/rooney_biegler/parameter_estimation_example.py rename to pyomo/contrib/parmest/deprecated/examples/rooney_biegler/parameter_estimation_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler.py rename to pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler.py diff --git a/pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler_with_constraint.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/rooney_biegler/rooney_biegler_with_constraint.py rename to pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler_with_constraint.py diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/__init__.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/__init__.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/__init__.py rename to pyomo/contrib/parmest/deprecated/examples/semibatch/__init__.py diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/bootstrap_theta.csv b/pyomo/contrib/parmest/deprecated/examples/semibatch/bootstrap_theta.csv similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/bootstrap_theta.csv rename to pyomo/contrib/parmest/deprecated/examples/semibatch/bootstrap_theta.csv diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/obj_at_theta.csv b/pyomo/contrib/parmest/deprecated/examples/semibatch/obj_at_theta.csv similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/obj_at_theta.csv rename to pyomo/contrib/parmest/deprecated/examples/semibatch/obj_at_theta.csv diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/parallel_example.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/parallel_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/parallel_example.py rename to pyomo/contrib/parmest/deprecated/examples/semibatch/parallel_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/parameter_estimation_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/parameter_estimation_example.py rename to pyomo/contrib/parmest/deprecated/examples/semibatch/parameter_estimation_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/scenario_example.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/scenario_example.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/scenario_example.py rename to pyomo/contrib/parmest/deprecated/examples/semibatch/scenario_example.py diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/scenarios.csv b/pyomo/contrib/parmest/deprecated/examples/semibatch/scenarios.csv similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/scenarios.csv rename to pyomo/contrib/parmest/deprecated/examples/semibatch/scenarios.csv diff --git a/pyomo/contrib/parmest/examples_deprecated/semibatch/semibatch.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/semibatch.py similarity index 100% rename from pyomo/contrib/parmest/examples_deprecated/semibatch/semibatch.py rename to pyomo/contrib/parmest/deprecated/examples/semibatch/semibatch.py diff --git a/pyomo/contrib/parmest/parmest_deprecated.py b/pyomo/contrib/parmest/deprecated/parmest.py similarity index 100% rename from pyomo/contrib/parmest/parmest_deprecated.py rename to pyomo/contrib/parmest/deprecated/parmest.py diff --git a/pyomo/contrib/parmest/scenariocreator_deprecated.py b/pyomo/contrib/parmest/deprecated/scenariocreator.py similarity index 100% rename from pyomo/contrib/parmest/scenariocreator_deprecated.py rename to pyomo/contrib/parmest/deprecated/scenariocreator.py diff --git a/pyomo/contrib/parmest/deprecated/tests/__init__.py b/pyomo/contrib/parmest/deprecated/tests/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/tests/scenarios.csv b/pyomo/contrib/parmest/deprecated/tests/scenarios.csv new file mode 100644 index 00000000000..22f9a651bc3 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/scenarios.csv @@ -0,0 +1,11 @@ +Name,Probability,k1,k2,E1,E2 +ExpScen0,0.1,25.800350800448314,14.14421520525348,31505.74905064048,35000.0 +ExpScen1,0.1,25.128373083865036,149.99999951481198,31452.336651974012,41938.781301641866 +ExpScen2,0.1,22.225574065344002,130.92739780265404,30948.669111672247,41260.15420929141 +ExpScen3,0.1,100.0,149.99999970011854,35182.73130744844,41444.52600373733 +ExpScen4,0.1,82.99114366189944,45.95424665995078,34810.857217141674,38300.633349887314 +ExpScen5,0.1,100.0,150.0,35142.20219150486,41495.41105795494 +ExpScen6,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 +ExpScen7,0.1,2.754580914035567,14.381786096822475,25000.0,35000.0 +ExpScen8,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 +ExpScen9,0.1,2.669780822294865,150.0,25000.0,41514.7476113499 diff --git a/pyomo/contrib/parmest/deprecated/tests/test_examples.py b/pyomo/contrib/parmest/deprecated/tests/test_examples.py new file mode 100644 index 00000000000..67e06130384 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/test_examples.py @@ -0,0 +1,192 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.graphics import matplotlib_available, seaborn_available +from pyomo.opt import SolverFactory + +ipopt_available = SolverFactory("ipopt").available() + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestRooneyBieglerExamples(unittest.TestCase): + @classmethod + def setUpClass(self): + pass + + @classmethod + def tearDownClass(self): + pass + + def test_model(self): + from pyomo.contrib.parmest.examples.rooney_biegler import rooney_biegler + + rooney_biegler.main() + + def test_model_with_constraint(self): + from pyomo.contrib.parmest.examples.rooney_biegler import ( + rooney_biegler_with_constraint, + ) + + rooney_biegler_with_constraint.main() + + @unittest.skipUnless(seaborn_available, "test requires seaborn") + def test_parameter_estimation_example(self): + from pyomo.contrib.parmest.examples.rooney_biegler import ( + parameter_estimation_example, + ) + + parameter_estimation_example.main() + + @unittest.skipUnless(seaborn_available, "test requires seaborn") + def test_bootstrap_example(self): + from pyomo.contrib.parmest.examples.rooney_biegler import bootstrap_example + + bootstrap_example.main() + + @unittest.skipUnless(seaborn_available, "test requires seaborn") + def test_likelihood_ratio_example(self): + from pyomo.contrib.parmest.examples.rooney_biegler import ( + likelihood_ratio_example, + ) + + likelihood_ratio_example.main() + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestReactionKineticsExamples(unittest.TestCase): + @classmethod + def setUpClass(self): + pass + + @classmethod + def tearDownClass(self): + pass + + def test_example(self): + from pyomo.contrib.parmest.examples.reaction_kinetics import ( + simple_reaction_parmest_example, + ) + + simple_reaction_parmest_example.main() + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestSemibatchExamples(unittest.TestCase): + @classmethod + def setUpClass(self): + pass + + @classmethod + def tearDownClass(self): + pass + + def test_model(self): + from pyomo.contrib.parmest.examples.semibatch import semibatch + + semibatch.main() + + def test_parameter_estimation_example(self): + from pyomo.contrib.parmest.examples.semibatch import ( + parameter_estimation_example, + ) + + parameter_estimation_example.main() + + def test_scenario_example(self): + from pyomo.contrib.parmest.examples.semibatch import scenario_example + + scenario_example.main() + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestReactorDesignExamples(unittest.TestCase): + @classmethod + def setUpClass(self): + pass + + @classmethod + def tearDownClass(self): + pass + + @unittest.pytest.mark.expensive + def test_model(self): + from pyomo.contrib.parmest.examples.reactor_design import reactor_design + + reactor_design.main() + + def test_parameter_estimation_example(self): + from pyomo.contrib.parmest.examples.reactor_design import ( + parameter_estimation_example, + ) + + parameter_estimation_example.main() + + @unittest.skipUnless(seaborn_available, "test requires seaborn") + def test_bootstrap_example(self): + from pyomo.contrib.parmest.examples.reactor_design import bootstrap_example + + bootstrap_example.main() + + @unittest.pytest.mark.expensive + def test_likelihood_ratio_example(self): + from pyomo.contrib.parmest.examples.reactor_design import ( + likelihood_ratio_example, + ) + + likelihood_ratio_example.main() + + @unittest.pytest.mark.expensive + def test_leaveNout_example(self): + from pyomo.contrib.parmest.examples.reactor_design import leaveNout_example + + leaveNout_example.main() + + def test_timeseries_data_example(self): + from pyomo.contrib.parmest.examples.reactor_design import ( + timeseries_data_example, + ) + + timeseries_data_example.main() + + def test_multisensor_data_example(self): + from pyomo.contrib.parmest.examples.reactor_design import ( + multisensor_data_example, + ) + + multisensor_data_example.main() + + @unittest.skipUnless(matplotlib_available, "test requires matplotlib") + def test_datarec_example(self): + from pyomo.contrib.parmest.examples.reactor_design import datarec_example + + datarec_example.main() + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_graphics.py b/pyomo/contrib/parmest/deprecated/tests/test_graphics.py new file mode 100644 index 00000000000..c18659e9948 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/test_graphics.py @@ -0,0 +1,68 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import ( + numpy as np, + numpy_available, + pandas as pd, + pandas_available, + scipy, + scipy_available, + matplotlib, + matplotlib_available, +) + +import platform + +is_osx = platform.mac_ver()[0] != '' + +import pyomo.common.unittest as unittest +import sys +import os + +import pyomo.contrib.parmest.parmest as parmest +import pyomo.contrib.parmest.graphics as graphics + +testdir = os.path.dirname(os.path.abspath(__file__)) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf( + not graphics.imports_available, "parmest.graphics imports are unavailable" +) +@unittest.skipIf( + is_osx, + "Disabling graphics tests on OSX due to issue in Matplotlib, see Pyomo PR #1337", +) +class TestGraphics(unittest.TestCase): + def setUp(self): + self.A = pd.DataFrame( + np.random.randint(0, 100, size=(100, 4)), columns=list('ABCD') + ) + self.B = pd.DataFrame( + np.random.randint(0, 100, size=(100, 4)), columns=list('ABCD') + ) + + def test_pairwise_plot(self): + graphics.pairwise_plot(self.A, alpha=0.8, distributions=['Rect', 'MVN', 'KDE']) + + def test_grouped_boxplot(self): + graphics.grouped_boxplot(self.A, self.B, normalize=True, group_names=['A', 'B']) + + def test_grouped_violinplot(self): + graphics.grouped_violinplot(self.A, self.B) + + +if __name__ == '__main__': + unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_parmest.py b/pyomo/contrib/parmest/deprecated/tests/test_parmest.py new file mode 100644 index 00000000000..7e692989b0c --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/test_parmest.py @@ -0,0 +1,958 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import ( + numpy as np, + numpy_available, + pandas as pd, + pandas_available, + scipy, + scipy_available, + matplotlib, + matplotlib_available, +) + +import platform + +is_osx = platform.mac_ver()[0] != "" + +import pyomo.common.unittest as unittest +import sys +import os +import subprocess +from itertools import product + +import pyomo.contrib.parmest.parmest as parmest +import pyomo.contrib.parmest.graphics as graphics +import pyomo.contrib.parmest as parmestbase +import pyomo.environ as pyo +import pyomo.dae as dae + +from pyomo.opt import SolverFactory + +ipopt_available = SolverFactory("ipopt").available() + +from pyomo.common.fileutils import find_library + +pynumero_ASL_available = False if find_library("pynumero_ASL") is None else True + +testdir = os.path.dirname(os.path.abspath(__file__)) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestRooneyBiegler(unittest.TestCase): + def setUp(self): + from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( + rooney_biegler_model, + ) + + # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=["hour", "y"], + ) + + theta_names = ["asymptote", "rate_constant"] + + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + return expr + + solver_options = {"tol": 1e-8} + + self.data = data + self.pest = parmest.Estimator( + rooney_biegler_model, + data, + theta_names, + SSE, + solver_options=solver_options, + tee=True, + ) + + def test_theta_est(self): + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + @unittest.skipIf( + not graphics.imports_available, "parmest.graphics imports are unavailable" + ) + def test_bootstrap(self): + objval, thetavals = self.pest.theta_est() + + num_bootstraps = 10 + theta_est = self.pest.theta_est_bootstrap(num_bootstraps, return_samples=True) + + num_samples = theta_est["samples"].apply(len) + self.assertTrue(len(theta_est.index), 10) + self.assertTrue(num_samples.equals(pd.Series([6] * 10))) + + del theta_est["samples"] + + # apply confidence region test + CR = self.pest.confidence_region_test(theta_est, "MVN", [0.5, 0.75, 1.0]) + + self.assertTrue(set(CR.columns) >= set([0.5, 0.75, 1.0])) + self.assertTrue(CR[0.5].sum() == 5) + self.assertTrue(CR[0.75].sum() == 7) + self.assertTrue(CR[1.0].sum() == 10) # all true + + graphics.pairwise_plot(theta_est) + graphics.pairwise_plot(theta_est, thetavals) + graphics.pairwise_plot(theta_est, thetavals, 0.8, ["MVN", "KDE", "Rect"]) + + @unittest.skipIf( + not graphics.imports_available, "parmest.graphics imports are unavailable" + ) + def test_likelihood_ratio(self): + objval, thetavals = self.pest.theta_est() + + asym = np.arange(10, 30, 2) + rate = np.arange(0, 1.5, 0.25) + theta_vals = pd.DataFrame( + list(product(asym, rate)), columns=self.pest._return_theta_names() + ) + + obj_at_theta = self.pest.objective_at_theta(theta_vals) + + LR = self.pest.likelihood_ratio_test(obj_at_theta, objval, [0.8, 0.9, 1.0]) + + self.assertTrue(set(LR.columns) >= set([0.8, 0.9, 1.0])) + self.assertTrue(LR[0.8].sum() == 6) + self.assertTrue(LR[0.9].sum() == 10) + self.assertTrue(LR[1.0].sum() == 60) # all true + + graphics.pairwise_plot(LR, thetavals, 0.8) + + def test_leaveNout(self): + lNo_theta = self.pest.theta_est_leaveNout(1) + self.assertTrue(lNo_theta.shape == (6, 2)) + + results = self.pest.leaveNout_bootstrap_test( + 1, None, 3, "Rect", [0.5, 1.0], seed=5436 + ) + self.assertTrue(len(results) == 6) # 6 lNo samples + i = 1 + samples = results[i][0] # list of N samples that are left out + lno_theta = results[i][1] + bootstrap_theta = results[i][2] + self.assertTrue(samples == [1]) # sample 1 was left out + self.assertTrue(lno_theta.shape[0] == 1) # lno estimate for sample 1 + self.assertTrue(set(lno_theta.columns) >= set([0.5, 1.0])) + self.assertTrue(lno_theta[1.0].sum() == 1) # all true + self.assertTrue(bootstrap_theta.shape[0] == 3) # bootstrap for sample 1 + self.assertTrue(bootstrap_theta[1.0].sum() == 3) # all true + + def test_diagnostic_mode(self): + self.pest.diagnostic_mode = True + + objval, thetavals = self.pest.theta_est() + + asym = np.arange(10, 30, 2) + rate = np.arange(0, 1.5, 0.25) + theta_vals = pd.DataFrame( + list(product(asym, rate)), columns=self.pest._return_theta_names() + ) + + obj_at_theta = self.pest.objective_at_theta(theta_vals) + + self.pest.diagnostic_mode = False + + @unittest.skip("Presently having trouble with mpiexec on appveyor") + def test_parallel_parmest(self): + """use mpiexec and mpi4py""" + p = str(parmestbase.__path__) + l = p.find("'") + r = p.find("'", l + 1) + parmestpath = p[l + 1 : r] + rbpath = ( + parmestpath + + os.sep + + "examples" + + os.sep + + "rooney_biegler" + + os.sep + + "rooney_biegler_parmest.py" + ) + rbpath = os.path.abspath(rbpath) # paranoia strikes deep... + rlist = ["mpiexec", "--allow-run-as-root", "-n", "2", sys.executable, rbpath] + if sys.version_info >= (3, 5): + ret = subprocess.run(rlist) + retcode = ret.returncode + else: + retcode = subprocess.call(rlist) + assert retcode == 0 + + @unittest.skip("Most folks don't have k_aug installed") + def test_theta_k_aug_for_Hessian(self): + # this will fail if k_aug is not installed + objval, thetavals, Hessian = self.pest.theta_est(solver="k_aug") + self.assertAlmostEqual(objval, 4.4675, places=2) + + @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") + @unittest.skipIf( + not parmest.inverse_reduced_hessian_available, + "Cannot test covariance matrix: required ASL dependency is missing", + ) + def test_theta_est_cov(self): + objval, thetavals, cov = self.pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + # Covariance matrix + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual(cov.iloc[1, 1], 0.04124, places=2) # 0.04124 from paper + + """ Why does the covariance matrix from parmest not match the paper? Parmest is + calculating the exact reduced Hessian. The paper (Rooney and Bielger, 2001) likely + employed the first order approximation common for nonlinear regression. The paper + values were verified with Scipy, which uses the same first order approximation. + The formula used in parmest was verified against equations (7-5-15) and (7-5-16) in + "Nonlinear Parameter Estimation", Y. Bard, 1974. + """ + + def test_cov_scipy_least_squares_comparison(self): + """ + Scipy results differ in the 3rd decimal place from the paper. It is possible + the paper used an alternative finite difference approximation for the Jacobian. + """ + + def model(theta, t): + """ + Model to be fitted y = model(theta, t) + Arguments: + theta: vector of fitted parameters + t: independent variable [hours] + + Returns: + y: model predictions [need to check paper for units] + """ + asymptote = theta[0] + rate_constant = theta[1] + + return asymptote * (1 - np.exp(-rate_constant * t)) + + def residual(theta, t, y): + """ + Calculate residuals + Arguments: + theta: vector of fitted parameters + t: independent variable [hours] + y: dependent variable [?] + """ + return y - model(theta, t) + + # define data + t = self.data["hour"].to_numpy() + y = self.data["y"].to_numpy() + + # define initial guess + theta_guess = np.array([15, 0.5]) + + ## solve with optimize.least_squares + sol = scipy.optimize.least_squares( + residual, theta_guess, method="trf", args=(t, y), verbose=2 + ) + theta_hat = sol.x + + self.assertAlmostEqual( + theta_hat[0], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual(theta_hat[1], 0.5311, places=2) # 0.5311 from the paper + + # calculate residuals + r = residual(theta_hat, t, y) + + # calculate variance of the residuals + # -2 because there are 2 fitted parameters + sigre = np.matmul(r.T, r / (len(y) - 2)) + + # approximate covariance + # Need to divide by 2 because optimize.least_squares scaled the objective by 1/2 + cov = sigre * np.linalg.inv(np.matmul(sol.jac.T, sol.jac)) + + self.assertAlmostEqual(cov[0, 0], 6.22864, places=2) # 6.22864 from paper + self.assertAlmostEqual(cov[0, 1], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 0], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper + + def test_cov_scipy_curve_fit_comparison(self): + """ + Scipy results differ in the 3rd decimal place from the paper. It is possible + the paper used an alternative finite difference approximation for the Jacobian. + """ + + ## solve with optimize.curve_fit + def model(t, asymptote, rate_constant): + return asymptote * (1 - np.exp(-rate_constant * t)) + + # define data + t = self.data["hour"].to_numpy() + y = self.data["y"].to_numpy() + + # define initial guess + theta_guess = np.array([15, 0.5]) + + theta_hat, cov = scipy.optimize.curve_fit(model, t, y, p0=theta_guess) + + self.assertAlmostEqual( + theta_hat[0], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual(theta_hat[1], 0.5311, places=2) # 0.5311 from the paper + + self.assertAlmostEqual(cov[0, 0], 6.22864, places=2) # 6.22864 from paper + self.assertAlmostEqual(cov[0, 1], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 0], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestModelVariants(unittest.TestCase): + def setUp(self): + self.data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=["hour", "y"], + ) + + def rooney_biegler_params(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Param(initialize=15, mutable=True) + model.rate_constant = pyo.Param(initialize=0.5, mutable=True) + + def response_rule(m, h): + expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def rooney_biegler_indexed_params(data): + model = pyo.ConcreteModel() + + model.param_names = pyo.Set(initialize=["asymptote", "rate_constant"]) + model.theta = pyo.Param( + model.param_names, + initialize={"asymptote": 15, "rate_constant": 0.5}, + mutable=True, + ) + + def response_rule(m, h): + expr = m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * h) + ) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def rooney_biegler_vars(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Var(initialize=15) + model.rate_constant = pyo.Var(initialize=0.5) + model.asymptote.fixed = True # parmest will unfix theta variables + model.rate_constant.fixed = True + + def response_rule(m, h): + expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def rooney_biegler_indexed_vars(data): + model = pyo.ConcreteModel() + + model.var_names = pyo.Set(initialize=["asymptote", "rate_constant"]) + model.theta = pyo.Var( + model.var_names, initialize={"asymptote": 15, "rate_constant": 0.5} + ) + model.theta[ + "asymptote" + ].fixed = ( + True # parmest will unfix theta variables, even when they are indexed + ) + model.theta["rate_constant"].fixed = True + + def response_rule(m, h): + expr = m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * h) + ) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + return expr + + self.objective_function = SSE + + theta_vals = pd.DataFrame([20, 1], index=["asymptote", "rate_constant"]).T + theta_vals_index = pd.DataFrame( + [20, 1], index=["theta['asymptote']", "theta['rate_constant']"] + ).T + + self.input = { + "param": { + "model": rooney_biegler_params, + "theta_names": ["asymptote", "rate_constant"], + "theta_vals": theta_vals, + }, + "param_index": { + "model": rooney_biegler_indexed_params, + "theta_names": ["theta"], + "theta_vals": theta_vals_index, + }, + "vars": { + "model": rooney_biegler_vars, + "theta_names": ["asymptote", "rate_constant"], + "theta_vals": theta_vals, + }, + "vars_index": { + "model": rooney_biegler_indexed_vars, + "theta_names": ["theta"], + "theta_vals": theta_vals_index, + }, + "vars_quoted_index": { + "model": rooney_biegler_indexed_vars, + "theta_names": ["theta['asymptote']", "theta['rate_constant']"], + "theta_vals": theta_vals_index, + }, + "vars_str_index": { + "model": rooney_biegler_indexed_vars, + "theta_names": ["theta[asymptote]", "theta[rate_constant]"], + "theta_vals": theta_vals_index, + }, + } + + @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") + @unittest.skipIf( + not parmest.inverse_reduced_hessian_available, + "Cannot test covariance matrix: required ASL dependency is missing", + ) + def test_parmest_basics(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + obj_at_theta = pest.objective_at_theta(parmest_input["theta_vals"]) + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + + def test_parmest_basics_with_initialize_parmest_model_option(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + obj_at_theta = pest.objective_at_theta( + parmest_input["theta_vals"], initialize_parmest_model=True + ) + + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + + def test_parmest_basics_with_square_problem_solve(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + obj_at_theta = pest.objective_at_theta( + parmest_input["theta_vals"], initialize_parmest_model=True + ) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + + def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + obj_at_theta = pest.objective_at_theta(initialize_parmest_model=True) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestReactorDesign(unittest.TestCase): + def setUp(self): + from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, + ) + + # Data from the design + data = pd.DataFrame( + data=[ + [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], + [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], + [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], + [1.20, 10000, 3680.7, 1070.0, 1486.1, 1881.6], + [1.25, 10000, 3750.0, 1071.4, 1428.6, 1875.0], + [1.30, 10000, 3817.1, 1072.2, 1374.6, 1868.0], + [1.35, 10000, 3882.2, 1072.4, 1324.0, 1860.7], + [1.40, 10000, 3945.4, 1072.1, 1276.3, 1853.1], + [1.45, 10000, 4006.7, 1071.3, 1231.4, 1845.3], + [1.50, 10000, 4066.4, 1070.1, 1189.0, 1837.3], + [1.55, 10000, 4124.4, 1068.5, 1148.9, 1829.1], + [1.60, 10000, 4180.9, 1066.5, 1111.0, 1820.8], + [1.65, 10000, 4235.9, 1064.3, 1075.0, 1812.4], + [1.70, 10000, 4289.5, 1061.8, 1040.9, 1803.9], + [1.75, 10000, 4341.8, 1059.0, 1008.5, 1795.3], + [1.80, 10000, 4392.8, 1056.0, 977.7, 1786.7], + [1.85, 10000, 4442.6, 1052.8, 948.4, 1778.1], + [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], + [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], + ], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], + ) + + theta_names = ["k1", "k2", "k3"] + + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + solver_options = {"max_iter": 6000} + + self.pest = parmest.Estimator( + reactor_design_model, data, theta_names, SSE, solver_options=solver_options + ) + + def test_theta_est(self): + # used in data reconciliation + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(thetavals["k1"], 5.0 / 6.0, places=4) + self.assertAlmostEqual(thetavals["k2"], 5.0 / 3.0, places=4) + self.assertAlmostEqual(thetavals["k3"], 1.0 / 6000.0, places=7) + + def test_return_values(self): + objval, thetavals, data_rec = self.pest.theta_est( + return_values=["ca", "cb", "cc", "cd", "caf"] + ) + self.assertAlmostEqual(data_rec["cc"].loc[18], 893.84924, places=3) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestReactorDesign_DAE(unittest.TestCase): + # Based on a reactor example in `Chemical Reactor Analysis and Design Fundamentals`, + # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/ + # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/fig-html/appendix/fig-A-10.html + + def setUp(self): + def ABC_model(data): + ca_meas = data["ca"] + cb_meas = data["cb"] + cc_meas = data["cc"] + + if isinstance(data, pd.DataFrame): + meas_t = data.index # time index + else: # dictionary + meas_t = list(ca_meas.keys()) # nested dictionary + + ca0 = 1.0 + cb0 = 0.0 + cc0 = 0.0 + + m = pyo.ConcreteModel() + + m.k1 = pyo.Var(initialize=0.5, bounds=(1e-4, 10)) + m.k2 = pyo.Var(initialize=3.0, bounds=(1e-4, 10)) + + m.time = dae.ContinuousSet(bounds=(0.0, 5.0), initialize=meas_t) + + # initialization and bounds + m.ca = pyo.Var(m.time, initialize=ca0, bounds=(-1e-3, ca0 + 1e-3)) + m.cb = pyo.Var(m.time, initialize=cb0, bounds=(-1e-3, ca0 + 1e-3)) + m.cc = pyo.Var(m.time, initialize=cc0, bounds=(-1e-3, ca0 + 1e-3)) + + m.dca = dae.DerivativeVar(m.ca, wrt=m.time) + m.dcb = dae.DerivativeVar(m.cb, wrt=m.time) + m.dcc = dae.DerivativeVar(m.cc, wrt=m.time) + + def _dcarate(m, t): + if t == 0: + return pyo.Constraint.Skip + else: + return m.dca[t] == -m.k1 * m.ca[t] + + m.dcarate = pyo.Constraint(m.time, rule=_dcarate) + + def _dcbrate(m, t): + if t == 0: + return pyo.Constraint.Skip + else: + return m.dcb[t] == m.k1 * m.ca[t] - m.k2 * m.cb[t] + + m.dcbrate = pyo.Constraint(m.time, rule=_dcbrate) + + def _dccrate(m, t): + if t == 0: + return pyo.Constraint.Skip + else: + return m.dcc[t] == m.k2 * m.cb[t] + + m.dccrate = pyo.Constraint(m.time, rule=_dccrate) + + def ComputeFirstStageCost_rule(m): + return 0 + + m.FirstStageCost = pyo.Expression(rule=ComputeFirstStageCost_rule) + + def ComputeSecondStageCost_rule(m): + return sum( + (m.ca[t] - ca_meas[t]) ** 2 + + (m.cb[t] - cb_meas[t]) ** 2 + + (m.cc[t] - cc_meas[t]) ** 2 + for t in meas_t + ) + + m.SecondStageCost = pyo.Expression(rule=ComputeSecondStageCost_rule) + + def total_cost_rule(model): + return model.FirstStageCost + model.SecondStageCost + + m.Total_Cost_Objective = pyo.Objective( + rule=total_cost_rule, sense=pyo.minimize + ) + + disc = pyo.TransformationFactory("dae.collocation") + disc.apply_to(m, nfe=20, ncp=2) + + return m + + # This example tests data formatted in 3 ways + # Each format holds 1 scenario + # 1. dataframe with time index + # 2. nested dictionary {ca: {t, val pairs}, ... } + data = [ + [0.000, 0.957, -0.031, -0.015], + [0.263, 0.557, 0.330, 0.044], + [0.526, 0.342, 0.512, 0.156], + [0.789, 0.224, 0.499, 0.310], + [1.053, 0.123, 0.428, 0.454], + [1.316, 0.079, 0.396, 0.556], + [1.579, 0.035, 0.303, 0.651], + [1.842, 0.029, 0.287, 0.658], + [2.105, 0.025, 0.221, 0.750], + [2.368, 0.017, 0.148, 0.854], + [2.632, -0.002, 0.182, 0.845], + [2.895, 0.009, 0.116, 0.893], + [3.158, -0.023, 0.079, 0.942], + [3.421, 0.006, 0.078, 0.899], + [3.684, 0.016, 0.059, 0.942], + [3.947, 0.014, 0.036, 0.991], + [4.211, -0.009, 0.014, 0.988], + [4.474, -0.030, 0.036, 0.941], + [4.737, 0.004, 0.036, 0.971], + [5.000, -0.024, 0.028, 0.985], + ] + data = pd.DataFrame(data, columns=["t", "ca", "cb", "cc"]) + data_df = data.set_index("t") + data_dict = { + "ca": {k: v for (k, v) in zip(data.t, data.ca)}, + "cb": {k: v for (k, v) in zip(data.t, data.cb)}, + "cc": {k: v for (k, v) in zip(data.t, data.cc)}, + } + + theta_names = ["k1", "k2"] + + self.pest_df = parmest.Estimator(ABC_model, [data_df], theta_names) + self.pest_dict = parmest.Estimator(ABC_model, [data_dict], theta_names) + + # Estimator object with multiple scenarios + self.pest_df_multiple = parmest.Estimator( + ABC_model, [data_df, data_df], theta_names + ) + self.pest_dict_multiple = parmest.Estimator( + ABC_model, [data_dict, data_dict], theta_names + ) + + # Create an instance of the model + self.m_df = ABC_model(data_df) + self.m_dict = ABC_model(data_dict) + + def test_dataformats(self): + obj1, theta1 = self.pest_df.theta_est() + obj2, theta2 = self.pest_dict.theta_est() + + self.assertAlmostEqual(obj1, obj2, places=6) + self.assertAlmostEqual(theta1["k1"], theta2["k1"], places=6) + self.assertAlmostEqual(theta1["k2"], theta2["k2"], places=6) + + def test_return_continuous_set(self): + """ + test if ContinuousSet elements are returned correctly from theta_est() + """ + obj1, theta1, return_vals1 = self.pest_df.theta_est(return_values=["time"]) + obj2, theta2, return_vals2 = self.pest_dict.theta_est(return_values=["time"]) + self.assertAlmostEqual(return_vals1["time"].loc[0][18], 2.368, places=3) + self.assertAlmostEqual(return_vals2["time"].loc[0][18], 2.368, places=3) + + def test_return_continuous_set_multiple_datasets(self): + """ + test if ContinuousSet elements are returned correctly from theta_est() + """ + obj1, theta1, return_vals1 = self.pest_df_multiple.theta_est( + return_values=["time"] + ) + obj2, theta2, return_vals2 = self.pest_dict_multiple.theta_est( + return_values=["time"] + ) + self.assertAlmostEqual(return_vals1["time"].loc[1][18], 2.368, places=3) + self.assertAlmostEqual(return_vals2["time"].loc[1][18], 2.368, places=3) + + def test_covariance(self): + from pyomo.contrib.interior_point.inverse_reduced_hessian import ( + inv_reduced_hessian_barrier, + ) + + # Number of datapoints. + # 3 data components (ca, cb, cc), 20 timesteps, 1 scenario = 60 + # In this example, this is the number of data points in data_df, but that's + # only because the data is indexed by time and contains no additional information. + n = 60 + + # Compute covariance using parmest + obj, theta, cov = self.pest_df.theta_est(calc_cov=True, cov_n=n) + + # Compute covariance using interior_point + vars_list = [self.m_df.k1, self.m_df.k2] + solve_result, inv_red_hes = inv_reduced_hessian_barrier( + self.m_df, independent_variables=vars_list, tee=True + ) + l = len(vars_list) + cov_interior_point = 2 * obj / (n - l) * inv_red_hes + cov_interior_point = pd.DataFrame( + cov_interior_point, ["k1", "k2"], ["k1", "k2"] + ) + + cov_diff = (cov - cov_interior_point).abs().sum().sum() + + self.assertTrue(cov.loc["k1", "k1"] > 0) + self.assertTrue(cov.loc["k2", "k2"] > 0) + self.assertAlmostEqual(cov_diff, 0, places=6) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestSquareInitialization_RooneyBiegler(unittest.TestCase): + def setUp(self): + from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler_with_constraint import ( + rooney_biegler_model_with_constraint, + ) + + # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=["hour", "y"], + ) + + theta_names = ["asymptote", "rate_constant"] + + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + return expr + + solver_options = {"tol": 1e-8} + + self.data = data + self.pest = parmest.Estimator( + rooney_biegler_model_with_constraint, + data, + theta_names, + SSE, + solver_options=solver_options, + tee=True, + ) + + def test_theta_est_with_square_initialization(self): + obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + def test_theta_est_with_square_initialization_and_custom_init_theta(self): + theta_vals_init = pd.DataFrame( + data=[[19.0, 0.5]], columns=["asymptote", "rate_constant"] + ) + obj_init = self.pest.objective_at_theta( + theta_values=theta_vals_init, initialize_parmest_model=True + ) + objval, thetavals = self.pest.theta_est() + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + def test_theta_est_with_square_initialization_diagnostic_mode_true(self): + self.pest.diagnostic_mode = True + obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + self.pest.diagnostic_mode = False + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py b/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py new file mode 100644 index 00000000000..22a851ae32e --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py @@ -0,0 +1,146 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import pandas as pd, pandas_available + +uuid_available = True +try: + import uuid +except: + uuid_available = False + +import pyomo.common.unittest as unittest +import os +import pyomo.contrib.parmest.parmest as parmest +import pyomo.contrib.parmest.scenariocreator as sc +import pyomo.environ as pyo +from pyomo.environ import SolverFactory + +ipopt_available = SolverFactory("ipopt").available() + +testdir = os.path.dirname(os.path.abspath(__file__)) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestScenarioReactorDesign(unittest.TestCase): + def setUp(self): + from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, + ) + + # Data from the design + data = pd.DataFrame( + data=[ + [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], + [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], + [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], + [1.20, 10000, 3680.7, 1070.0, 1486.1, 1881.6], + [1.25, 10000, 3750.0, 1071.4, 1428.6, 1875.0], + [1.30, 10000, 3817.1, 1072.2, 1374.6, 1868.0], + [1.35, 10000, 3882.2, 1072.4, 1324.0, 1860.7], + [1.40, 10000, 3945.4, 1072.1, 1276.3, 1853.1], + [1.45, 10000, 4006.7, 1071.3, 1231.4, 1845.3], + [1.50, 10000, 4066.4, 1070.1, 1189.0, 1837.3], + [1.55, 10000, 4124.4, 1068.5, 1148.9, 1829.1], + [1.60, 10000, 4180.9, 1066.5, 1111.0, 1820.8], + [1.65, 10000, 4235.9, 1064.3, 1075.0, 1812.4], + [1.70, 10000, 4289.5, 1061.8, 1040.9, 1803.9], + [1.75, 10000, 4341.8, 1059.0, 1008.5, 1795.3], + [1.80, 10000, 4392.8, 1056.0, 977.7, 1786.7], + [1.85, 10000, 4442.6, 1052.8, 948.4, 1778.1], + [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], + [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], + ], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], + ) + + theta_names = ["k1", "k2", "k3"] + + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + self.pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + def test_scen_from_exps(self): + scenmaker = sc.ScenarioCreator(self.pest, "ipopt") + experimentscens = sc.ScenarioSet("Experiments") + scenmaker.ScenariosFromExperiments(experimentscens) + experimentscens.write_csv("delme_exp_csv.csv") + df = pd.read_csv("delme_exp_csv.csv") + os.remove("delme_exp_csv.csv") + # March '20: all reactor_design experiments have the same theta values! + k1val = df.loc[5].at["k1"] + self.assertAlmostEqual(k1val, 5.0 / 6.0, places=2) + tval = experimentscens.ScenarioNumber(0).ThetaVals["k1"] + self.assertAlmostEqual(tval, 5.0 / 6.0, places=2) + + @unittest.skipIf(not uuid_available, "The uuid module is not available") + def test_no_csv_if_empty(self): + # low level test of scenario sets + # verify that nothing is written, but no errors with empty set + + emptyset = sc.ScenarioSet("empty") + tfile = uuid.uuid4().hex + ".csv" + emptyset.write_csv(tfile) + self.assertFalse( + os.path.exists(tfile), "ScenarioSet wrote csv in spite of empty set" + ) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestScenarioSemibatch(unittest.TestCase): + def setUp(self): + import pyomo.contrib.parmest.examples.semibatch.semibatch as sb + import json + + # Vars to estimate in parmest + theta_names = ["k1", "k2", "E1", "E2"] + + self.fbase = os.path.join(testdir, "..", "examples", "semibatch") + # Data, list of dictionaries + data = [] + for exp_num in range(10): + fname = "exp" + str(exp_num + 1) + ".out" + fullname = os.path.join(self.fbase, fname) + with open(fullname, "r") as infile: + d = json.load(infile) + data.append(d) + + # Note, the model already includes a 'SecondStageCost' expression + # for the sum of squared error that will be used in parameter estimation + + self.pest = parmest.Estimator(sb.generate_model, data, theta_names) + + def test_semibatch_bootstrap(self): + scenmaker = sc.ScenarioCreator(self.pest, "ipopt") + bootscens = sc.ScenarioSet("Bootstrap") + numtomake = 2 + scenmaker.ScenariosFromBootstrap(bootscens, numtomake, seed=1134) + tval = bootscens.ScenarioNumber(0).ThetaVals["k1"] + self.assertAlmostEqual(tval, 20.64, places=1) + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_solver.py b/pyomo/contrib/parmest/deprecated/tests/test_solver.py new file mode 100644 index 00000000000..eb655023b9b --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/test_solver.py @@ -0,0 +1,75 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import ( + numpy as np, + numpy_available, + pandas as pd, + pandas_available, + scipy, + scipy_available, + matplotlib, + matplotlib_available, +) + +import platform + +is_osx = platform.mac_ver()[0] != '' + +import pyomo.common.unittest as unittest +import os + +import pyomo.contrib.parmest.parmest as parmest +import pyomo.contrib.parmest as parmestbase +import pyomo.environ as pyo + +from pyomo.opt import SolverFactory + +ipopt_available = SolverFactory('ipopt').available() + +from pyomo.common.fileutils import find_library + +pynumero_ASL_available = False if find_library('pynumero_ASL') is None else True + +testdir = os.path.dirname(os.path.abspath(__file__)) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestSolver(unittest.TestCase): + def setUp(self): + pass + + def test_ipopt_solve_with_stats(self): + from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( + rooney_biegler_model, + ) + from pyomo.contrib.parmest.utils import ipopt_solve_with_stats + + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + model = rooney_biegler_model(data) + solver = pyo.SolverFactory('ipopt') + solver.solve(model) + + status_obj, solved, iters, time, regu = ipopt_solve_with_stats(model, solver) + + self.assertEqual(solved, True) + + +if __name__ == '__main__': + unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_utils.py b/pyomo/contrib/parmest/deprecated/tests/test_utils.py new file mode 100644 index 00000000000..514c14b1e82 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/tests/test_utils.py @@ -0,0 +1,68 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import pandas as pd, pandas_available + +import pyomo.environ as pyo +import pyomo.common.unittest as unittest +import pyomo.contrib.parmest.parmest as parmest +from pyomo.opt import SolverFactory + +ipopt_available = SolverFactory("ipopt").available() + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestUtils(unittest.TestCase): + @classmethod + def setUpClass(self): + pass + + @classmethod + def tearDownClass(self): + pass + + @unittest.pytest.mark.expensive + def test_convert_param_to_var(self): + from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + reactor_design_model, + ) + + data = pd.DataFrame( + data=[ + [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], + [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], + [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], + ], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], + ) + + theta_names = ["k1", "k2", "k3"] + + instance = reactor_design_model(data.loc[0]) + solver = pyo.SolverFactory("ipopt") + solver.solve(instance) + + instance_vars = parmest.utils.convert_params_to_vars( + instance, theta_names, fix_vars=True + ) + solver.solve(instance_vars) + + assert instance.k1() == instance_vars.k1() + assert instance.k2() == instance_vars.k2() + assert instance.k3() == instance_vars.k3() + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index dc747217b31..1f9b8b645b8 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -63,7 +63,7 @@ import pyomo.contrib.parmest.graphics as graphics from pyomo.dae import ContinuousSet -import pyomo.contrib.parmest.parmest_deprecated as parmest_deprecated +import pyomo.contrib.parmest.deprecated.parmest as parmest_deprecated parmest_available = numpy_available & pandas_available & scipy_available @@ -398,14 +398,21 @@ def _return_theta_names(self): """ Return list of fitted model parameter names """ - # if fitted model parameter names differ from theta_names created when Estimator object is created - if hasattr(self, 'theta_names_updated'): - return self.theta_names_updated + # check for deprecated inputs + if self.pest_deprecated is not None: + + # if fitted model parameter names differ from theta_names + # created when Estimator object is created + if hasattr(self, 'theta_names_updated'): + return self.pest_deprecated.theta_names_updated + + else: + return ( + self.pest_deprecated.theta_names + ) # default theta_names, created when Estimator object is created else: - return ( - self.theta_names - ) # default theta_names, created when Estimator object is created + return None def _create_parmest_model(self, data): """ diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 18c27ad1c86..b849bfdfd5b 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -14,7 +14,7 @@ import pyomo.environ as pyo -import pyomo.contrib.parmest.scenariocreator_deprecated as scen_deprecated +import pyomo.contrib.parmest.deprecated.scenariocreator as scen_deprecated import logging logger = logging.getLogger(__name__) From f4ce9b9e775132a2c8b19823d43f5737e3fca376 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 9 Jan 2024 17:49:41 -0700 Subject: [PATCH 0305/3044] GDP transformation to MINLP --- pyomo/gdp/plugins/__init__.py | 1 + pyomo/gdp/plugins/gdp_to_minlp.py | 208 ++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 pyomo/gdp/plugins/gdp_to_minlp.py diff --git a/pyomo/gdp/plugins/__init__.py b/pyomo/gdp/plugins/__init__.py index 1222ce500f1..39761697a0f 100644 --- a/pyomo/gdp/plugins/__init__.py +++ b/pyomo/gdp/plugins/__init__.py @@ -22,3 +22,4 @@ def load(): import pyomo.gdp.plugins.multiple_bigm import pyomo.gdp.plugins.transform_current_disjunctive_state import pyomo.gdp.plugins.bound_pretransformation + import pyomo.gdp.plugins.gdp_to_minlp diff --git a/pyomo/gdp/plugins/gdp_to_minlp.py b/pyomo/gdp/plugins/gdp_to_minlp.py new file mode 100644 index 00000000000..e2be6789aae --- /dev/null +++ b/pyomo/gdp/plugins/gdp_to_minlp.py @@ -0,0 +1,208 @@ +from .gdp_to_mip_transformation import GDP_to_MIP_Transformation +from pyomo.common.config import ConfigDict, ConfigValue +from pyomo.core.base import TransformationFactory +from pyomo.core.util import target_list +from pyomo.core import ( + Block, + BooleanVar, + Connector, + Constraint, + Param, + Set, + SetOf, + Var, + Expression, + SortComponents, + TraversalStrategy, + value, + RangeSet, + NonNegativeIntegers, + Binary, + Any, +) +from pyomo.core.base import TransformationFactory, Reference +import pyomo.core.expr as EXPR +from pyomo.gdp import Disjunct, Disjunction, GDP_Error +from pyomo.gdp.plugins.bigm_mixin import ( + _BigM_MixIn, + _get_bigM_suffix_list, + _warn_for_unused_bigM_args, +) +from pyomo.gdp.plugins.gdp_to_mip_transformation import GDP_to_MIP_Transformation +from pyomo.gdp.transformed_disjunct import _TransformedDisjunct +from pyomo.gdp.util import is_child_of, _get_constraint_transBlock, _to_dict +from pyomo.core.util import target_list +from pyomo.network import Port +from pyomo.repn import generate_standard_repn +from weakref import ref as weakref_ref, ReferenceType +import logging +from pyomo.gdp import GDP_Error +from pyomo.common.collections import ComponentSet +from pyomo.contrib.fbbt.expression_bounds_walker import ExpressionBoundsVisitor +import pyomo.contrib.fbbt.interval as interval +from pyomo.core import Suffix + + +logger = logging.getLogger('pyomo.gdp.gdp_to_minlp') + + +@TransformationFactory.register( + 'gdp.gdp_to_minlp', doc="Reformulate the GDP as an MINLP." +) +class GDPToMINLPTransformation(GDP_to_MIP_Transformation): + CONFIG = ConfigDict("gdp.gdp_to_minlp") + CONFIG.declare( + 'targets', + ConfigValue( + default=None, + domain=target_list, + description="target or list of targets that will be relaxed", + doc=""" + + This specifies the list of components to relax. If None (default), the + entire model is transformed. Note that if the transformation is done out + of place, the list of targets should be attached to the model before it + is cloned, and the list will specify the targets on the cloned + instance.""", + ), + ) + + transformation_name = 'gdp_to_minlp' + + def __init__(self): + super().__init__(logger) + + def _apply_to(self, instance, **kwds): + try: + self._apply_to_impl(instance, **kwds) + finally: + self._restore_state() + + def _apply_to_impl(self, instance, **kwds): + self._process_arguments(instance, **kwds) + + # filter out inactive targets and handle case where targets aren't + # specified. + targets = self._filter_targets(instance) + # transform logical constraints based on targets + self._transform_logical_constraints(instance, targets) + # we need to preprocess targets to make sure that if there are any + # disjunctions in targets that their disjuncts appear before them in + # the list. + gdp_tree = self._get_gdp_tree_from_targets(instance, targets) + preprocessed_targets = gdp_tree.reverse_topological_sort() + + for t in preprocessed_targets: + if t.ctype is Disjunction: + self._transform_disjunctionData( + t, + t.index(), + parent_disjunct=gdp_tree.parent(t), + root_disjunct=gdp_tree.root_disjunct(t), + ) + + def _transform_disjunctionData( + self, obj, index, parent_disjunct=None, root_disjunct=None + ): + (transBlock, xorConstraint) = self._setup_transform_disjunctionData( + obj, root_disjunct + ) + + # add or (or xor) constraint + or_expr = 0 + for disjunct in obj.disjuncts: + or_expr += disjunct.binary_indicator_var + self._transform_disjunct(disjunct, transBlock) + + rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var + if obj.xor: + xorConstraint[index] = or_expr == rhs + else: + xorConstraint[index] = or_expr >= rhs + # Mark the DisjunctionData as transformed by mapping it to its XOR + # constraint. + obj._algebraic_constraint = weakref_ref(xorConstraint[index]) + + # and deactivate for the writers + obj.deactivate() + + def _transform_disjunct(self, obj, transBlock): + # We're not using the preprocessed list here, so this could be + # inactive. We've already done the error checking in preprocessing, so + # we just skip it here. + if not obj.active: + return + + relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) + + # Transform each component within this disjunct + self._transform_block_components(obj, obj) + + # deactivate disjunct to keep the writers happy + obj._deactivate_without_fixing_indicator() + + def _transform_constraint( + self, obj, disjunct + ): + # add constraint to the transformation block, we'll transform it there. + transBlock = disjunct._transformation_block() + constraintMap = transBlock._constraintMap + + disjunctionRelaxationBlock = transBlock.parent_block() + + # We will make indexes from ({obj.local_name} x obj.index_set() x ['lb', + # 'ub']), but don't bother construct that set here, as taking Cartesian + # products is kind of expensive (and redundant since we have the + # original model) + newConstraint = transBlock.transformedConstraints + + for i in sorted(obj.keys()): + c = obj[i] + if not c.active: + continue + + self._add_constraint_expressions( + c, i, disjunct.binary_indicator_var, newConstraint, constraintMap + ) + + # deactivate because we relaxed + c.deactivate() + + def _add_constraint_expressions( + self, c, i, indicator_var, newConstraint, constraintMap + ): + # Since we are both combining components from multiple blocks and using + # local names, we need to make sure that the first index for + # transformedConstraints is guaranteed to be unique. We just grab the + # current length of the list here since that will be monotonically + # increasing and hence unique. We'll append it to the + # slightly-more-human-readable constraint name for something familiar + # but unique. (Note that we really could do this outside of the loop + # over the constraint indices, but I don't think it matters a lot.) + unique = len(newConstraint) + name = c.local_name + "_%s" % unique + + lb, ub = c.lower, c.upper + if (c.equality or lb is ub) and lb is not None: + # equality + newConstraint.add((name, i, 'eq'), lb * indicator_var == c.body * indicator_var) + constraintMap['transformedConstraints'][c] = [newConstraint[name, i, 'eq']] + constraintMap['srcConstraints'][newConstraint[name, i, 'eq']] = c + else: + # inequality + if lb is not None: + newConstraint.add((name, i, 'lb'), lb * indicator_var <= c.body * indicator_var) + constraintMap['transformedConstraints'][c] = [newConstraint[name, i, 'lb']] + constraintMap['srcConstraints'][newConstraint[name, i, 'lb']] = c + if ub is not None: + newConstraint.add((name, i, 'ub'), c.body * indicator_var <= ub * indicator_var) + transformed = constraintMap['transformedConstraints'].get(c) + if transformed is not None: + constraintMap['transformedConstraints'][c].append( + newConstraint[name, i, 'ub'] + ) + else: + constraintMap['transformedConstraints'][c] = [ + newConstraint[name, i, 'ub'] + ] + constraintMap['srcConstraints'][newConstraint[name, i, 'ub']] = c From d093e2e03d8137cf7ffaa765909cdb9ea2e7fc20 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 08:37:42 -0700 Subject: [PATCH 0306/3044] tests for GDP transformation to MINLP --- pyomo/gdp/plugins/gdp_to_minlp.py | 44 +--- pyomo/gdp/tests/common_tests.py | 18 ++ pyomo/gdp/tests/test_gdp_to_minlp.py | 300 +++++++++++++++++++++++++++ 3 files changed, 323 insertions(+), 39 deletions(-) create mode 100644 pyomo/gdp/tests/test_gdp_to_minlp.py diff --git a/pyomo/gdp/plugins/gdp_to_minlp.py b/pyomo/gdp/plugins/gdp_to_minlp.py index e2be6789aae..b599d866ac7 100644 --- a/pyomo/gdp/plugins/gdp_to_minlp.py +++ b/pyomo/gdp/plugins/gdp_to_minlp.py @@ -2,45 +2,11 @@ from pyomo.common.config import ConfigDict, ConfigValue from pyomo.core.base import TransformationFactory from pyomo.core.util import target_list -from pyomo.core import ( - Block, - BooleanVar, - Connector, - Constraint, - Param, - Set, - SetOf, - Var, - Expression, - SortComponents, - TraversalStrategy, - value, - RangeSet, - NonNegativeIntegers, - Binary, - Any, -) -from pyomo.core.base import TransformationFactory, Reference -import pyomo.core.expr as EXPR -from pyomo.gdp import Disjunct, Disjunction, GDP_Error -from pyomo.gdp.plugins.bigm_mixin import ( - _BigM_MixIn, - _get_bigM_suffix_list, - _warn_for_unused_bigM_args, -) +from pyomo.gdp import Disjunction from pyomo.gdp.plugins.gdp_to_mip_transformation import GDP_to_MIP_Transformation -from pyomo.gdp.transformed_disjunct import _TransformedDisjunct -from pyomo.gdp.util import is_child_of, _get_constraint_transBlock, _to_dict from pyomo.core.util import target_list -from pyomo.network import Port -from pyomo.repn import generate_standard_repn -from weakref import ref as weakref_ref, ReferenceType +from weakref import ref as weakref_ref import logging -from pyomo.gdp import GDP_Error -from pyomo.common.collections import ComponentSet -from pyomo.contrib.fbbt.expression_bounds_walker import ExpressionBoundsVisitor -import pyomo.contrib.fbbt.interval as interval -from pyomo.core import Suffix logger = logging.getLogger('pyomo.gdp.gdp_to_minlp') @@ -185,17 +151,17 @@ def _add_constraint_expressions( lb, ub = c.lower, c.upper if (c.equality or lb is ub) and lb is not None: # equality - newConstraint.add((name, i, 'eq'), lb * indicator_var == c.body * indicator_var) + newConstraint.add((name, i, 'eq'), c.body * indicator_var - lb * indicator_var == 0) constraintMap['transformedConstraints'][c] = [newConstraint[name, i, 'eq']] constraintMap['srcConstraints'][newConstraint[name, i, 'eq']] = c else: # inequality if lb is not None: - newConstraint.add((name, i, 'lb'), lb * indicator_var <= c.body * indicator_var) + newConstraint.add((name, i, 'lb'), 0 <= c.body * indicator_var - lb * indicator_var) constraintMap['transformedConstraints'][c] = [newConstraint[name, i, 'lb']] constraintMap['srcConstraints'][newConstraint[name, i, 'lb']] = c if ub is not None: - newConstraint.add((name, i, 'ub'), c.body * indicator_var <= ub * indicator_var) + newConstraint.add((name, i, 'ub'), c.body * indicator_var - ub * indicator_var <= 0) transformed = constraintMap['transformedConstraints'].get(c) if transformed is not None: constraintMap['transformedConstraints'][c].append( diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index b475334981b..354c64a6386 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -58,6 +58,24 @@ def check_linear_coef(self, repn, var, coef): self.assertAlmostEqual(repn.linear_coefs[var_id], coef) +def check_quadratic_coef(self, repn, v1, v2, coef): + if isinstance(v1, BooleanVar): + v1 = v1.get_associated_binary() + if isinstance(v2, BooleanVar): + v2 = v2.get_associated_binary() + + v1id = id(v1) + v2id = id(v2) + + qcoef_map = dict() + for (_v1, _v2), _coef in zip(repn.quadratic_vars, repn.quadratic_coefs): + qcoef_map[id(_v1), id(_v2)] = _coef + qcoef_map[id(_v2), id(_v1)] = _coef + + self.assertIn((v1id, v2id), qcoef_map) + self.assertAlmostEqual(qcoef_map[v1id, v2id], coef) + + def check_squared_term_coef(self, repn, var, coef): var_id = None for i, (v1, v2) in enumerate(repn.quadratic_vars): diff --git a/pyomo/gdp/tests/test_gdp_to_minlp.py b/pyomo/gdp/tests/test_gdp_to_minlp.py new file mode 100644 index 00000000000..acf04fd7b53 --- /dev/null +++ b/pyomo/gdp/tests/test_gdp_to_minlp.py @@ -0,0 +1,300 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest + +from pyomo.environ import ( + TransformationFactory, + Block, + Constraint, + ConcreteModel, + Var, + Any, +) +from pyomo.gdp import Disjunct, Disjunction +from pyomo.core.expr.compare import ( + assertExpressionsEqual, +) +from pyomo.repn import generate_standard_repn + +import pyomo.core.expr as EXPR +import pyomo.gdp.tests.models as models +import pyomo.gdp.tests.common_tests as ct + +import random + + +class CommonTests: + def diff_apply_to_and_create_using(self, model): + ct.diff_apply_to_and_create_using(self, model, 'gdp.gdp_to_minlp') + + +class TwoTermDisj(unittest.TestCase, CommonTests): + def setUp(self): + # set seed so we can test name collisions predictably + random.seed(666) + + def test_new_block_created(self): + m = models.makeTwoTermDisj() + TransformationFactory('gdp.gdp_to_minlp').apply_to(m) + + # we have a transformation block + transBlock = m.component("_pyomo_gdp_gdp_to_minlp_reformulation") + self.assertIsInstance(transBlock, Block) + + disjBlock = transBlock.component("relaxedDisjuncts") + self.assertIsInstance(disjBlock, Block) + self.assertEqual(len(disjBlock), 2) + # it has the disjuncts on it + self.assertIs(m.d[0].transformation_block, disjBlock[0]) + self.assertIs(m.d[1].transformation_block, disjBlock[1]) + + def test_disjunction_deactivated(self): + ct.check_disjunction_deactivated(self, 'gdp_to_minlp') + + def test_disjunctDatas_deactivated(self): + ct.check_disjunctDatas_deactivated(self, 'gdp_to_minlp') + + def test_do_not_transform_twice_if_disjunction_reactivated(self): + ct.check_do_not_transform_twice_if_disjunction_reactivated(self, 'gdp_to_minlp') + + def test_xor_constraint_mapping(self): + ct.check_xor_constraint_mapping(self, 'gdp_to_minlp') + + def test_xor_constraint_mapping_two_disjunctions(self): + ct.check_xor_constraint_mapping_two_disjunctions(self, 'gdp_to_minlp') + + def test_disjunct_mapping(self): + ct.check_disjunct_mapping(self, 'gdp_to_minlp') + + def test_disjunct_and_constraint_maps(self): + """Tests the actual data structures used to store the maps.""" + m = models.makeTwoTermDisj() + gdp_to_minlp = TransformationFactory('gdp.gdp_to_minlp') + gdp_to_minlp.apply_to(m) + disjBlock = m._pyomo_gdp_gdp_to_minlp_reformulation.relaxedDisjuncts + oldblock = m.component("d") + + # we are counting on the fact that the disjuncts get relaxed in the + # same order every time. + for i in [0, 1]: + self.assertIs(oldblock[i].transformation_block, disjBlock[i]) + self.assertIs(gdp_to_minlp.get_src_disjunct(disjBlock[i]), oldblock[i]) + + # check constraint dict has right mapping + c1_list = gdp_to_minlp.get_transformed_constraints(oldblock[1].c1) + # this is an equality + self.assertEqual(len(c1_list), 1) + self.assertIs(c1_list[0].parent_block(), disjBlock[1]) + self.assertIs(gdp_to_minlp.get_src_constraint(c1_list[0]), oldblock[1].c1) + + c2_list = gdp_to_minlp.get_transformed_constraints(oldblock[1].c2) + # just ub + self.assertEqual(len(c2_list), 1) + self.assertIs(c2_list[0].parent_block(), disjBlock[1]) + self.assertIs(gdp_to_minlp.get_src_constraint(c2_list[0]), oldblock[1].c2) + + c_list = gdp_to_minlp.get_transformed_constraints(oldblock[0].c) + # just lb + self.assertEqual(len(c_list), 1) + self.assertIs(c_list[0].parent_block(), disjBlock[0]) + self.assertIs(gdp_to_minlp.get_src_constraint(c_list[0]), oldblock[0].c) + + def test_new_block_nameCollision(self): + ct.check_transformation_block_name_collision(self, 'gdp_to_minlp') + + def test_indicator_vars(self): + ct.check_indicator_vars(self, 'gdp_to_minlp') + + def test_xor_constraints(self): + ct.check_xor_constraint(self, 'gdp_to_minlp') + + def test_or_constraints(self): + m = models.makeTwoTermDisj() + m.disjunction.xor = False + TransformationFactory('gdp.gdp_to_minlp').apply_to(m) + + # check or constraint is an or (upper bound is None) + orcons = m._pyomo_gdp_gdp_to_minlp_reformulation.component("disjunction_xor") + self.assertIsInstance(orcons, Constraint) + assertExpressionsEqual( + self, + orcons.body, + EXPR.LinearExpression( + [ + EXPR.MonomialTermExpression((1, m.d[0].binary_indicator_var)), + EXPR.MonomialTermExpression((1, m.d[1].binary_indicator_var)), + ] + ), + ) + self.assertEqual(orcons.lower, 1) + self.assertIsNone(orcons.upper) + + def test_deactivated_constraints(self): + ct.check_deactivated_constraints(self, 'gdp_to_minlp') + + def test_transformed_constraints(self): + m = models.makeTwoTermDisj() + gdp_to_minlp = TransformationFactory('gdp.gdp_to_minlp') + gdp_to_minlp.apply_to(m) + self.check_transformed_constraints(m, gdp_to_minlp, -3, 2, 7, 2) + + def test_do_not_transform_userDeactivated_disjuncts(self): + ct.check_user_deactivated_disjuncts(self, 'gdp_to_minlp') + + def test_improperly_deactivated_disjuncts(self): + ct.check_improperly_deactivated_disjuncts(self, 'gdp_to_minlp') + + def test_do_not_transform_userDeactivated_IndexedDisjunction(self): + ct.check_do_not_transform_userDeactivated_indexedDisjunction(self, 'gdp_to_minlp') + + # helper method to check the M values in all of the transformed + # constraints (m, M) is the tuple for M. This also relies on the + # disjuncts being transformed in the same order every time. + def check_transformed_constraints(self, model, gdp_to_minlp, cons1lb, cons2lb, cons2ub, cons3ub): + disjBlock = model._pyomo_gdp_gdp_to_minlp_reformulation.relaxedDisjuncts + + # first constraint + c = gdp_to_minlp.get_transformed_constraints(model.d[0].c) + self.assertEqual(len(c), 1) + c_lb = c[0] + self.assertTrue(c[0].active) + repn = generate_standard_repn(c[0].body) + self.assertIsNone(repn.nonlinear_expr) + self.assertEqual(len(repn.quadratic_coefs), 1) + self.assertEqual(len(repn.linear_coefs), 1) + ind_var = model.d[0].indicator_var + ct.check_quadratic_coef(self, repn, model.a, ind_var, 1) + ct.check_linear_coef(self, repn, ind_var, -model.d[0].c.lower) + self.assertEqual(repn.constant, 0) + self.assertEqual(c[0].lower, 0) + self.assertIsNone(c[0].upper) + + # second constraint + c = gdp_to_minlp.get_transformed_constraints(model.d[1].c1) + self.assertEqual(len(c), 1) + c_eq = c[0] + self.assertTrue(c[0].active) + repn = generate_standard_repn(c[0].body) + self.assertTrue(repn.nonlinear_expr is None) + self.assertEqual(len(repn.linear_coefs), 0) + self.assertEqual(len(repn.quadratic_coefs), 1) + ind_var = model.d[1].indicator_var + ct.check_quadratic_coef(self, repn, model.a, ind_var, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(c[0].lower, 0) + self.assertEqual(c[0].upper, 0) + + # third constraint + c = gdp_to_minlp.get_transformed_constraints(model.d[1].c2) + self.assertEqual(len(c), 1) + c_ub = c[0] + self.assertTrue(c_ub.active) + repn = generate_standard_repn(c_ub.body) + self.assertIsNone(repn.nonlinear_expr) + self.assertEqual(len(repn.linear_coefs), 1) + self.assertEqual(len(repn.quadratic_coefs), 1) + ct.check_quadratic_coef(self, repn, model.x, ind_var, 1) + ct.check_linear_coef(self, repn, ind_var, -model.d[1].c2.upper) + self.assertEqual(repn.constant, 0) + self.assertIsNone(c_ub.lower) + self.assertEqual(c_ub.upper, 0) + + def test_create_using(self): + m = models.makeTwoTermDisj() + self.diff_apply_to_and_create_using(m) + + def test_indexed_constraints_in_disjunct(self): + m = ConcreteModel() + m.I = [1, 2, 3] + m.x = Var(m.I, bounds=(0, 10)) + + def c_rule(b, i): + m = b.model() + return m.x[i] >= i + + def d_rule(d, j): + m = d.model() + d.c = Constraint(m.I[:j], rule=c_rule) + + m.d = Disjunct(m.I, rule=d_rule) + m.disjunction = Disjunction(expr=[m.d[i] for i in m.I]) + + TransformationFactory('gdp.gdp_to_minlp').apply_to(m) + transBlock = m._pyomo_gdp_gdp_to_minlp_reformulation + + # 2 blocks: the original Disjunct and the transformation block + self.assertEqual(len(list(m.component_objects(Block, descend_into=False))), 1) + self.assertEqual(len(list(m.component_objects(Disjunct))), 1) + + # Each relaxed disjunct should have 1 var (the reference to the + # indicator var), and i "d[i].c" Constraints + for i in [1, 2, 3]: + relaxed = transBlock.relaxedDisjuncts[i - 1] + self.assertEqual(len(list(relaxed.component_objects(Var))), 1) + self.assertEqual(len(list(relaxed.component_data_objects(Var))), 1) + self.assertEqual(len(list(relaxed.component_objects(Constraint))), 1) + self.assertEqual(len(list(relaxed.component_data_objects(Constraint))), i) + + def test_virtual_indexed_constraints_in_disjunct(self): + m = ConcreteModel() + m.I = [1, 2, 3] + m.x = Var(m.I, bounds=(0, 10)) + + def d_rule(d, j): + m = d.model() + d.c = Constraint(Any) + for k in range(j): + d.c[k + 1] = m.x[k + 1] >= k + 1 + + m.d = Disjunct(m.I, rule=d_rule) + m.disjunction = Disjunction(expr=[m.d[i] for i in m.I]) + + TransformationFactory('gdp.gdp_to_minlp').apply_to(m) + transBlock = m._pyomo_gdp_gdp_to_minlp_reformulation + + # 2 blocks: the original Disjunct and the transformation block + self.assertEqual(len(list(m.component_objects(Block, descend_into=False))), 1) + self.assertEqual(len(list(m.component_objects(Disjunct))), 1) + + # Each relaxed disjunct should have 1 var (the reference to the + # indicator var), and i "d[i].c" Constraints + for i in [1, 2, 3]: + relaxed = transBlock.relaxedDisjuncts[i - 1] + self.assertEqual(len(list(relaxed.component_objects(Var))), 1) + self.assertEqual(len(list(relaxed.component_data_objects(Var))), 1) + self.assertEqual(len(list(relaxed.component_objects(Constraint))), 1) + self.assertEqual(len(list(relaxed.component_data_objects(Constraint))), i) + + def test_local_var(self): + m = models.localVar() + gdp_to_minlp = TransformationFactory('gdp.gdp_to_minlp') + gdp_to_minlp.apply_to(m) + + # we just need to make sure that constraint was transformed correctly, + # which just means that the M values were correct. + transformedC = gdp_to_minlp.get_transformed_constraints(m.disj2.cons) + self.assertEqual(len(transformedC), 1) + eq = transformedC[0] + repn = generate_standard_repn(eq.body) + self.assertIsNone(repn.nonlinear_expr) + self.assertEqual(len(repn.linear_coefs), 1) + self.assertEqual(len(repn.quadratic_coefs), 2) + ct.check_linear_coef(self, repn, m.disj2.indicator_var, -3) + ct.check_quadratic_coef(self, repn, m.x, m.disj2.indicator_var, 1) + ct.check_quadratic_coef(self, repn, m.disj2.y, m.disj2.indicator_var, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(eq.lb, 0) + self.assertEqual(eq.ub, 0) + + +if __name__ == '__main__': + unittest.main() From e1d1d60512250db3c11257c3eddf5809019afed2 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 08:42:08 -0700 Subject: [PATCH 0307/3044] run black --- pyomo/gdp/plugins/gdp_to_minlp.py | 20 +++++++++++++------- pyomo/gdp/tests/common_tests.py | 2 +- pyomo/gdp/tests/test_gdp_to_minlp.py | 12 +++++++----- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/pyomo/gdp/plugins/gdp_to_minlp.py b/pyomo/gdp/plugins/gdp_to_minlp.py index b599d866ac7..bec9160ceca 100644 --- a/pyomo/gdp/plugins/gdp_to_minlp.py +++ b/pyomo/gdp/plugins/gdp_to_minlp.py @@ -107,9 +107,7 @@ def _transform_disjunct(self, obj, transBlock): # deactivate disjunct to keep the writers happy obj._deactivate_without_fixing_indicator() - def _transform_constraint( - self, obj, disjunct - ): + def _transform_constraint(self, obj, disjunct): # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() constraintMap = transBlock._constraintMap @@ -151,17 +149,25 @@ def _add_constraint_expressions( lb, ub = c.lower, c.upper if (c.equality or lb is ub) and lb is not None: # equality - newConstraint.add((name, i, 'eq'), c.body * indicator_var - lb * indicator_var == 0) + newConstraint.add( + (name, i, 'eq'), c.body * indicator_var - lb * indicator_var == 0 + ) constraintMap['transformedConstraints'][c] = [newConstraint[name, i, 'eq']] constraintMap['srcConstraints'][newConstraint[name, i, 'eq']] = c else: # inequality if lb is not None: - newConstraint.add((name, i, 'lb'), 0 <= c.body * indicator_var - lb * indicator_var) - constraintMap['transformedConstraints'][c] = [newConstraint[name, i, 'lb']] + newConstraint.add( + (name, i, 'lb'), 0 <= c.body * indicator_var - lb * indicator_var + ) + constraintMap['transformedConstraints'][c] = [ + newConstraint[name, i, 'lb'] + ] constraintMap['srcConstraints'][newConstraint[name, i, 'lb']] = c if ub is not None: - newConstraint.add((name, i, 'ub'), c.body * indicator_var - ub * indicator_var <= 0) + newConstraint.add( + (name, i, 'ub'), c.body * indicator_var - ub * indicator_var <= 0 + ) transformed = constraintMap['transformedConstraints'].get(c) if transformed is not None: constraintMap['transformedConstraints'][c].append( diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index 354c64a6386..4a772a7ae56 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -63,7 +63,7 @@ def check_quadratic_coef(self, repn, v1, v2, coef): v1 = v1.get_associated_binary() if isinstance(v2, BooleanVar): v2 = v2.get_associated_binary() - + v1id = id(v1) v2id = id(v2) diff --git a/pyomo/gdp/tests/test_gdp_to_minlp.py b/pyomo/gdp/tests/test_gdp_to_minlp.py index acf04fd7b53..532922ee1cc 100644 --- a/pyomo/gdp/tests/test_gdp_to_minlp.py +++ b/pyomo/gdp/tests/test_gdp_to_minlp.py @@ -20,9 +20,7 @@ Any, ) from pyomo.gdp import Disjunct, Disjunction -from pyomo.core.expr.compare import ( - assertExpressionsEqual, -) +from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.repn import generate_standard_repn import pyomo.core.expr as EXPR @@ -154,12 +152,16 @@ def test_improperly_deactivated_disjuncts(self): ct.check_improperly_deactivated_disjuncts(self, 'gdp_to_minlp') def test_do_not_transform_userDeactivated_IndexedDisjunction(self): - ct.check_do_not_transform_userDeactivated_indexedDisjunction(self, 'gdp_to_minlp') + ct.check_do_not_transform_userDeactivated_indexedDisjunction( + self, 'gdp_to_minlp' + ) # helper method to check the M values in all of the transformed # constraints (m, M) is the tuple for M. This also relies on the # disjuncts being transformed in the same order every time. - def check_transformed_constraints(self, model, gdp_to_minlp, cons1lb, cons2lb, cons2ub, cons3ub): + def check_transformed_constraints( + self, model, gdp_to_minlp, cons1lb, cons2lb, cons2ub, cons3ub + ): disjBlock = model._pyomo_gdp_gdp_to_minlp_reformulation.relaxedDisjuncts # first constraint From 5e15a7f23ce547b6cb58498d9acf2702bdbc4408 Mon Sep 17 00:00:00 2001 From: jlgearh Date: Wed, 10 Jan 2024 10:18:14 -0700 Subject: [PATCH 0308/3044] - Added an additional test case --- .../alternative_solutions/tests/test_aos_utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py index 2963f195d17..1fad3e8fcb8 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py +++ b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py @@ -9,6 +9,8 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from numpy.linalg import norm + import pyomo.environ as pe from pyomo.common.collections import ComponentSet import pyomo.common.unittest as unittest @@ -140,6 +142,13 @@ def test_max_both_obj_constraint2(self): self.assertEqual(10, cons[0].lower) self.assertEqual(None, cons[1].upper) self.assertEqual(9, cons[1].lower) + + def test_random_direction(self): + ''' + Ensure that _get_random_direction returns a normal vector. + ''' + vector = au._get_random_direction(10) + self.assertAlmostEqual(1.0, norm(vector)) def get_var_model(self): ''' From 475ec06fb243b9afa4f59a8cefbbacd56d6634f3 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 10:35:33 -0700 Subject: [PATCH 0309/3044] simplification tests --- .../simplification/ginac_interface.cpp | 2 +- pyomo/contrib/simplification/simplify.py | 5 +- .../tests/test_simplification.py | 79 ++++++++++++++++--- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index 690885dc513..32bea8dadd0 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -21,7 +21,7 @@ ex ginac_expr_from_pyomo_node( case py_float: { double val = expr.cast(); if (is_integer(val)) { - res = numeric(expr.cast()); + res = numeric((long) val); } else { res = numeric(val); diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 938bff6b4b9..66a3dad0b06 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -25,7 +25,10 @@ def simplify_with_sympy(expr: NumericExpression): def simplify_with_ginac(expr: NumericExpression, ginac_interface): gi = ginac_interface - return gi.from_ginac(gi.to_ginac(expr).normal()) + ginac_expr = gi.to_ginac(expr) + ginac_expr = ginac_expr.normal() + new_expr = gi.from_ginac(ginac_expr) + return new_expr class Simplifier(object): diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 02107ba1d6c..4d9b0cec0d2 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -6,6 +6,14 @@ class TestSimplification(TestCase): + def compare_against_possible_results(self, got, expected_list): + success = False + for exp in expected_list: + if compare_expressions(got, exp): + success = True + break + self.assertTrue(success) + def test_simplify(self): m = pe.ConcreteModel() x = m.x = pe.Var(bounds=(0, None)) @@ -24,13 +32,16 @@ def test_param(self): e1 = p*x**2 + p*x + p*x**2 simp = Simplifier() e2 = simp.simplify(e1) - exp1 = p*x**2.0*2.0 + p*x - exp2 = p*x + p*x**2.0*2.0 - self.assertTrue( - compare_expressions(e2, exp1) - or compare_expressions(e2, exp2) - or compare_expressions(e2, p*x + x**2.0*p*2.0) - or compare_expressions(e2, x**2.0*p*2.0 + p*x) + self.compare_against_possible_results( + e2, + [ + p*x**2.0*2.0 + p*x, + p*x + p*x**2.0*2.0, + 2.0*p*x**2.0 + p*x, + p*x + 2.0*p*x**2.0, + x**2.0*p*2.0 + p*x, + p*x + x**2.0*p*2.0 + ] ) def test_mul(self): @@ -48,8 +59,13 @@ def test_sum(self): e = 2 + x simp = Simplifier() e2 = simp.simplify(e) - expected = x + 2.0 - assertExpressionsEqual(self, expected, e2) + self.compare_against_possible_results( + e2, + [ + 2.0 + x, + x + 2.0, + ] + ) def test_neg(self): m = pe.ConcreteModel() @@ -57,6 +73,47 @@ def test_neg(self): e = -pe.log(x) simp = Simplifier() e2 = simp.simplify(e) - expected = pe.log(x)*(-1.0) - assertExpressionsEqual(self, expected, e2) + self.compare_against_possible_results( + e2, + [ + (-1.0)*pe.log(x), + pe.log(x)*(-1.0), + -pe.log(x), + ] + ) + + def test_pow(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = x**2.0 + simp = Simplifier() + e2 = simp.simplify(e) + assertExpressionsEqual(self, e, e2) + def test_div(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + y = m.y = pe.Var() + e = x/y + y/x - x/y + simp = Simplifier() + e2 = simp.simplify(e) + print(e2) + self.compare_against_possible_results( + e2, + [ + y/x, + y*(1.0/x), + y*x**-1.0, + x**-1.0 * y, + ], + ) + + def test_unary(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + func_list = [pe.log, pe.sin, pe.cos, pe.tan, pe.asin, pe.acos, pe.atan] + for func in func_list: + e = func(x) + simp = Simplifier() + e2 = simp.simplify(e) + assertExpressionsEqual(self, e, e2) From a93d793e49f9f209f188d5a7ee1a72829a7423c4 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 10 Jan 2024 14:33:02 -0500 Subject: [PATCH 0310/3044] change all ''' to """ --- pyomo/contrib/mindtpy/algorithm_base_class.py | 6 +++--- pyomo/contrib/mindtpy/extended_cutting_plane.py | 2 +- pyomo/contrib/mindtpy/feasibility_pump.py | 2 +- pyomo/contrib/mindtpy/global_outer_approximation.py | 2 +- pyomo/contrib/mindtpy/outer_approximation.py | 2 +- pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 05f1e4389d3..d732e95c422 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -519,9 +519,9 @@ def get_primal_integral(self): return primal_integral def get_integral_info(self): - ''' + """ Obtain primal integral, dual integral and primal dual gap integral. - ''' + """ self.primal_integral = self.get_primal_integral() self.dual_integral = self.get_dual_integral() self.primal_dual_gap_integral = self.primal_integral + self.dual_integral @@ -2598,7 +2598,7 @@ def fp_loop(self): self.working_model.MindtPy_utils.cuts.del_component('fp_orthogonality_cuts') def initialize_mip_problem(self): - '''Deactivate the nonlinear constraints to create the MIP problem.''' + """Deactivate the nonlinear constraints to create the MIP problem.""" # if single tree is activated, we need to add bounds for unbounded variables in nonlinear constraints to avoid unbounded main problem. config = self.config if config.single_tree: diff --git a/pyomo/contrib/mindtpy/extended_cutting_plane.py b/pyomo/contrib/mindtpy/extended_cutting_plane.py index f5fa205e091..ac13e352e35 100644 --- a/pyomo/contrib/mindtpy/extended_cutting_plane.py +++ b/pyomo/contrib/mindtpy/extended_cutting_plane.py @@ -84,7 +84,7 @@ def check_config(self): super().check_config() def initialize_mip_problem(self): - '''Deactivate the nonlinear constraints to create the MIP problem.''' + """Deactivate the nonlinear constraints to create the MIP problem.""" super().initialize_mip_problem() self.jacobians = calc_jacobians( self.mip.MindtPy_utils.nonlinear_constraint_list, diff --git a/pyomo/contrib/mindtpy/feasibility_pump.py b/pyomo/contrib/mindtpy/feasibility_pump.py index 9d5be89bab5..a34cceb014c 100644 --- a/pyomo/contrib/mindtpy/feasibility_pump.py +++ b/pyomo/contrib/mindtpy/feasibility_pump.py @@ -44,7 +44,7 @@ def check_config(self): super().check_config() def initialize_mip_problem(self): - '''Deactivate the nonlinear constraints to create the MIP problem.''' + """Deactivate the nonlinear constraints to create the MIP problem.""" super().initialize_mip_problem() self.jacobians = calc_jacobians( self.mip.MindtPy_utils.nonlinear_constraint_list, diff --git a/pyomo/contrib/mindtpy/global_outer_approximation.py b/pyomo/contrib/mindtpy/global_outer_approximation.py index 817fb0bf4a8..70fc4cffb90 100644 --- a/pyomo/contrib/mindtpy/global_outer_approximation.py +++ b/pyomo/contrib/mindtpy/global_outer_approximation.py @@ -67,7 +67,7 @@ def check_config(self): super().check_config() def initialize_mip_problem(self): - '''Deactivate the nonlinear constraints to create the MIP problem.''' + """Deactivate the nonlinear constraints to create the MIP problem.""" super().initialize_mip_problem() self.mip.MindtPy_utils.cuts.aff_cuts = ConstraintList(doc='Affine cuts') diff --git a/pyomo/contrib/mindtpy/outer_approximation.py b/pyomo/contrib/mindtpy/outer_approximation.py index 6d790ce70d0..f6e6147724e 100644 --- a/pyomo/contrib/mindtpy/outer_approximation.py +++ b/pyomo/contrib/mindtpy/outer_approximation.py @@ -94,7 +94,7 @@ def check_config(self): _MindtPyAlgorithm.check_config(self) def initialize_mip_problem(self): - '''Deactivate the nonlinear constraints to create the MIP problem.''' + """Deactivate the nonlinear constraints to create the MIP problem.""" super().initialize_mip_problem() self.jacobians = calc_jacobians( self.mip.MindtPy_utils.nonlinear_constraint_list, diff --git a/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py b/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py index 547efc0a74c..9c1f33e80cc 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py +++ b/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py @@ -114,7 +114,7 @@ def evaluate_jacobian_equality_constraints(self): """Evaluate the Jacobian of the equality constraints.""" return None - ''' + """ def _extract_and_assemble_fim(self): M = np.zeros((self.n_parameters, self.n_parameters)) for i in range(self.n_parameters): @@ -122,7 +122,7 @@ def _extract_and_assemble_fim(self): M[i,k] = self._input_values[self.ele_to_order[(i,k)]] return M - ''' + """ def evaluate_jacobian_outputs(self): """Evaluate the Jacobian of the outputs.""" From 666cbaa4e776947032e86887473f1296a9baacff Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 10 Jan 2024 14:39:32 -0500 Subject: [PATCH 0311/3044] rename int_sol_2_cuts_ind to integer_solution_to_cuts_index --- pyomo/contrib/mindtpy/algorithm_base_class.py | 8 ++++---- pyomo/contrib/mindtpy/single_tree.py | 9 ++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index d732e95c422..7e8d390976c 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -102,14 +102,14 @@ def __init__(self, **kwds): self.fixed_nlp = None # We store bounds, timing info, iteration count, incumbent, and the - # expression of the original (possibly nonlinear) objective function. + # Expression of the original (possibly nonlinear) objective function. self.results = SolverResults() self.timing = Bunch() self.curr_int_sol = [] self.should_terminate = False self.integer_list = [] - # dictionary {integer solution (list): [cuts begin index, cuts end index] (list)} - self.int_sol_2_cuts_ind = dict() + # Dictionary {integer solution (list): [cuts begin index, cuts end index] (list)} + self.integer_solution_to_cuts_index = dict() # Set up iteration counters self.nlp_iter = 0 @@ -813,7 +813,7 @@ def MindtPy_initialization(self): self.integer_list.append(self.curr_int_sol) fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) - self.int_sol_2_cuts_ind[self.curr_int_sol] = [ + self.integer_solution_to_cuts_index[self.curr_int_sol] = [ 1, len(self.mip.MindtPy_utils.cuts.oa_cuts), ] diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index c4d49e3afd6..10b1f21ec5c 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -906,7 +906,7 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): # Your callback should be prepared to cut off solutions that violate any of your lazy constraints, including those that have already been added. Node solutions will usually respect previously added lazy constraints, but not always. # https://www.gurobi.com/documentation/current/refman/cs_cb_addlazy.html # If this happens, MindtPy will look for the index of corresponding cuts, instead of solving the fixed-NLP again. - begin_index, end_index = mindtpy_solver.int_sol_2_cuts_ind[ + begin_index, end_index = mindtpy_solver.integer_solution_to_cuts_index[ mindtpy_solver.curr_int_sol ] for ind in range(begin_index, end_index + 1): @@ -924,10 +924,9 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): mindtpy_solver.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result, cb_opt) if config.strategy == 'OA': # store the cut index corresponding to current integer solution. - mindtpy_solver.int_sol_2_cuts_ind[mindtpy_solver.curr_int_sol] = [ - cut_ind + 1, - len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts), - ] + mindtpy_solver.integer_solution_to_cuts_index[ + mindtpy_solver.curr_int_sol + ] = [cut_ind + 1, len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts)] def handle_lazy_main_feasible_solution_gurobi(cb_m, cb_opt, mindtpy_solver, config): From 14ebdcdc8a03c947164b64729a24e53c4b1b02db Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 10 Jan 2024 15:46:05 -0500 Subject: [PATCH 0312/3044] add one more comment to CPLEX lazy constraint callback --- pyomo/contrib/mindtpy/single_tree.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 10b1f21ec5c..145d85e0d37 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -666,6 +666,7 @@ def __call__(self): main_mip = self.main_mip mindtpy_solver = self.mindtpy_solver + # The lazy constraint callback may be invoked during MIP start processing. In that case get_solution_source returns mip_start_solution. # Reference: https://www.ibm.com/docs/en/icos/22.1.1?topic=SSSA5P_22.1.1/ilog.odms.cplex.help/refpythoncplex/html/cplex.callbacks.SolutionSource-class.htm # Another solution source is user_solution = 118, but it will not be encountered in LazyConstraintCallback. config.logger.info( From b143e87de6eb464cfec2b190114c6f068c9433ae Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 10 Jan 2024 15:46:51 -0500 Subject: [PATCH 0313/3044] remove the finished TODO --- pyomo/contrib/mindtpy/single_tree.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 145d85e0d37..09b5e704f75 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -274,7 +274,6 @@ def add_lazy_affine_cuts(self, mindtpy_solver, config, opt): 'Skipping constraint %s due to MCPP error' % (constr.name) ) continue # skip to the next constraint - # TODO: check if the value of ccSlope and cvSlope is not Nan or inf. If so, we skip this. ccSlope = mc_eqn.subcc() cvSlope = mc_eqn.subcv() ccStart = mc_eqn.concave() From 09bda47d460033736c2789e0a8e6d5dbaf581d6a Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 10 Jan 2024 15:48:44 -0500 Subject: [PATCH 0314/3044] add TODO for self.abort() --- pyomo/contrib/mindtpy/single_tree.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 09b5e704f75..228810a8f90 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -689,6 +689,7 @@ def __call__(self): mindtpy_solver.mip_start_lazy_oa_cuts = [] if mindtpy_solver.should_terminate: + # TODO: check the performance difference if we don't use self.abort() and let cplex terminate by itself. self.abort() return self.handle_lazy_main_feasible_solution(main_mip, mindtpy_solver, config, opt) @@ -744,6 +745,7 @@ def __call__(self): ) ) mindtpy_solver.results.solver.termination_condition = tc.optimal + # TODO: check the performance difference if we don't use self.abort() and let cplex terminate by itself. self.abort() return From 491db9f6793dc6d84fc3b771073d00d938b3a2f2 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 15:57:34 -0700 Subject: [PATCH 0315/3044] update GHA to install ginac --- .github/workflows/test_pr_and_main.yml | 21 ++++++++++++++++++- .../tests/test_simplification.py | 2 ++ setup.cfg | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 2885fd107a8..12dc7c1daac 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -98,7 +98,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest'" + category: "-m 'neos or importtest or simplification'" skip_doctest: 1 TARGET: linux PYENV: pip @@ -179,6 +179,25 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} + - name: install ginac + if: ${{ matrix.other == "singletest" }} + run: | + pwd + cd .. + curl https://www.ginac.de/CLN/cln-1.3.6.tar.bz2 >cln-1.3.6.tar.bz2 + tar -xvf cln-1.3.6.tar.bz2 + cd cln-1.3.6 + ./configure + make + make install + cd + curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 + tar -xvf ginac-1.8.7.tar.bz2 + cd ginac-1.8.7 + ./configure + make + make install + - name: TPL package download cache uses: actions/cache@v3 if: ${{ ! matrix.slim }} diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 4d9b0cec0d2..ed59064022c 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -1,10 +1,12 @@ from pyomo.common.unittest import TestCase +from pyomo.common import unittest from pyomo.contrib.simplification import Simplifier from pyomo.core.expr.compare import assertExpressionsEqual, compare_expressions import pyomo.environ as pe from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd +@unittest.pytest.mark.simplification class TestSimplification(TestCase): def compare_against_possible_results(self, got, expected_list): success = False diff --git a/setup.cfg b/setup.cfg index b606138f38c..a431e0cd601 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,3 +22,4 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests + simplification: marks simplification tests that have expensive (to install) dependencies From b3a1ff9b06e3fb2e8fd944bb27d2bf736a9cfd54 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:02:28 -0700 Subject: [PATCH 0316/3044] run black --- pyomo/contrib/simplification/build.py | 16 +++--- pyomo/contrib/simplification/simplify.py | 2 + .../tests/test_simplification.py | 49 ++++++------------- 3 files changed, 27 insertions(+), 40 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 6f16607e22b..e8bd645756b 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -11,16 +11,16 @@ def build_ginac_interface(args=[]): dname = this_file_dir() - _sources = [ - 'ginac_interface.cpp', - ] + _sources = ['ginac_interface.cpp'] sources = list() for fname in _sources: sources.append(os.path.join(dname, fname)) ginac_lib = find_library('ginac') if ginac_lib is None: - raise RuntimeError('could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable') + raise RuntimeError( + 'could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable' + ) ginac_lib_dir = os.path.dirname(ginac_lib) ginac_build_dir = os.path.dirname(ginac_lib_dir) ginac_include_dir = os.path.join(ginac_build_dir, 'include') @@ -29,7 +29,9 @@ def build_ginac_interface(args=[]): cln_lib = find_library('cln') if cln_lib is None: - raise RuntimeError('could not find CLN library; please make sure it is in the LD_LIBRARY_PATH environment variable') + raise RuntimeError( + 'could not find CLN library; please make sure it is in the LD_LIBRARY_PATH environment variable' + ) cln_lib_dir = os.path.dirname(cln_lib) cln_build_dir = os.path.dirname(cln_lib_dir) cln_include_dir = os.path.join(cln_build_dir, 'include') @@ -38,8 +40,8 @@ def build_ginac_interface(args=[]): extra_args = ['-std=c++11'] ext = Pybind11Extension( - 'ginac_interface', - sources=sources, + 'ginac_interface', + sources=sources, language='c++', include_dirs=[cln_include_dir, ginac_include_dir], library_dirs=[cln_lib_dir, ginac_lib_dir], diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 66a3dad0b06..8f7f15f3826 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -3,8 +3,10 @@ from pyomo.core.expr.numvalue import is_fixed, value import logging import warnings + try: from pyomo.contrib.simplification.ginac_interface import GinacInterface + ginac_available = True except: GinacInterface = None diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index ed59064022c..cc278db4d43 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -19,7 +19,7 @@ def compare_against_possible_results(self, got, expected_list): def test_simplify(self): m = pe.ConcreteModel() x = m.x = pe.Var(bounds=(0, None)) - e = x*pe.log(x) + e = x * pe.log(x) der1 = reverse_sd(e)[x] der2 = reverse_sd(der1)[x] simp = Simplifier() @@ -31,28 +31,28 @@ def test_param(self): m = pe.ConcreteModel() x = m.x = pe.Var() p = m.p = pe.Param(mutable=True) - e1 = p*x**2 + p*x + p*x**2 + e1 = p * x**2 + p * x + p * x**2 simp = Simplifier() e2 = simp.simplify(e1) self.compare_against_possible_results( - e2, + e2, [ - p*x**2.0*2.0 + p*x, - p*x + p*x**2.0*2.0, - 2.0*p*x**2.0 + p*x, - p*x + 2.0*p*x**2.0, - x**2.0*p*2.0 + p*x, - p*x + x**2.0*p*2.0 - ] + p * x**2.0 * 2.0 + p * x, + p * x + p * x**2.0 * 2.0, + 2.0 * p * x**2.0 + p * x, + p * x + 2.0 * p * x**2.0, + x**2.0 * p * 2.0 + p * x, + p * x + x**2.0 * p * 2.0, + ], ) def test_mul(self): m = pe.ConcreteModel() x = m.x = pe.Var() - e = 2*x + e = 2 * x simp = Simplifier() e2 = simp.simplify(e) - expected = 2.0*x + expected = 2.0 * x assertExpressionsEqual(self, expected, e2) def test_sum(self): @@ -61,13 +61,7 @@ def test_sum(self): e = 2 + x simp = Simplifier() e2 = simp.simplify(e) - self.compare_against_possible_results( - e2, - [ - 2.0 + x, - x + 2.0, - ] - ) + self.compare_against_possible_results(e2, [2.0 + x, x + 2.0]) def test_neg(self): m = pe.ConcreteModel() @@ -76,12 +70,7 @@ def test_neg(self): simp = Simplifier() e2 = simp.simplify(e) self.compare_against_possible_results( - e2, - [ - (-1.0)*pe.log(x), - pe.log(x)*(-1.0), - -pe.log(x), - ] + e2, [(-1.0) * pe.log(x), pe.log(x) * (-1.0), -pe.log(x)] ) def test_pow(self): @@ -96,18 +85,12 @@ def test_div(self): m = pe.ConcreteModel() x = m.x = pe.Var() y = m.y = pe.Var() - e = x/y + y/x - x/y + e = x / y + y / x - x / y simp = Simplifier() e2 = simp.simplify(e) print(e2) self.compare_against_possible_results( - e2, - [ - y/x, - y*(1.0/x), - y*x**-1.0, - x**-1.0 * y, - ], + e2, [y / x, y * (1.0 / x), y * x**-1.0, x**-1.0 * y] ) def test_unary(self): From de8743a84c4902867a802756ab64fbd62eed920e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:03:42 -0700 Subject: [PATCH 0317/3044] syntax --- .github/workflows/test_pr_and_main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 12dc7c1daac..7345fd45e10 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -180,7 +180,7 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: install ginac - if: ${{ matrix.other == "singletest" }} + if: ${{ matrix.other == 'singletest' }} run: | pwd cd .. From ee9f830984ad20446b28c8aa65674d5cbf264ab3 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:07:25 -0700 Subject: [PATCH 0318/3044] install ginac in GHA --- .github/workflows/test_branches.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index e773587ec85..3270b3e8a95 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -94,6 +94,14 @@ jobs: PYENV: conda PACKAGES: mpi4py + - os: ubuntu-latest + python: 3.11 + other: /singletest + category: "-m 'neos or importtest or simplification'" + skip_doctest: 1 + TARGET: linux + PYENV: pip + - os: ubuntu-latest python: '3.10' other: /cython @@ -149,6 +157,25 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} + - name: install ginac + if: ${{ matrix.other == 'singletest' }} + run: | + pwd + cd .. + curl https://www.ginac.de/CLN/cln-1.3.6.tar.bz2 >cln-1.3.6.tar.bz2 + tar -xvf cln-1.3.6.tar.bz2 + cd cln-1.3.6 + ./configure + make + make install + cd + curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 + tar -xvf ginac-1.8.7.tar.bz2 + cd ginac-1.8.7 + ./configure + make + make install + - name: TPL package download cache uses: actions/cache@v3 if: ${{ ! matrix.slim }} From 36cfd6388d16d6f2077d51b9035c9e363b4e4e28 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:09:53 -0700 Subject: [PATCH 0319/3044] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 3270b3e8a95..97b9c6ed1dc 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -158,7 +158,7 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: install ginac - if: ${{ matrix.other == 'singletest' }} + if: matrix.TARGET == 'singletest' run: | pwd cd .. From 8269539ff67b48e4c7b652b8feabb14c64634fc6 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:11:27 -0700 Subject: [PATCH 0320/3044] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 97b9c6ed1dc..c56ec398134 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -158,7 +158,7 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: install ginac - if: matrix.TARGET == 'singletest' + if: matrix.other == 'singletest' run: | pwd cd .. From 7bb0ff501344dfc9bb162f9ed339293ad320d0d1 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:14:07 -0700 Subject: [PATCH 0321/3044] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- pyomo/contrib/simplification/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index c56ec398134..bea57f314e5 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -158,7 +158,7 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: install ginac - if: matrix.other == 'singletest' + if: matrix.other == '/singletest' run: | pwd cd .. diff --git a/pyomo/contrib/simplification/__init__.py b/pyomo/contrib/simplification/__init__.py index c09e8b8b5e5..3abe5a25ba0 100644 --- a/pyomo/contrib/simplification/__init__.py +++ b/pyomo/contrib/simplification/__init__.py @@ -1 +1 @@ -from .simplify import Simplifier \ No newline at end of file +from .simplify import Simplifier From 546dad1d46cc1a60c6fae711a6dbe8710f53220c Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:23:06 -0700 Subject: [PATCH 0322/3044] install ginac in GHA --- .github/workflows/test_branches.yml | 10 ++++++++-- pyomo/contrib/simplification/simplify.py | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index bea57f314e5..16ce6a44003 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -167,14 +167,14 @@ jobs: cd cln-1.3.6 ./configure make - make install + sudo make install cd curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 tar -xvf ginac-1.8.7.tar.bz2 cd ginac-1.8.7 ./configure make - make install + sudo make install - name: TPL package download cache uses: actions/cache@v3 @@ -630,6 +630,12 @@ jobs: echo "" pyomo build-extensions --parallel 2 + - name: Install GiNaC Interface + if: matrix.other == '/singletest' + run: | + cd pyomo/contrib/simplification/ + $PYTHON_EXE build.py --inplace + - name: Report pyomo plugin information run: | echo "$PATH" diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 8f7f15f3826..4002f1a233f 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -34,10 +34,10 @@ def simplify_with_ginac(expr: NumericExpression, ginac_interface): class Simplifier(object): - def __init__(self, supress_no_ginac_warnings: bool = False) -> None: + def __init__(self, suppress_no_ginac_warnings: bool = False) -> None: if ginac_available: self.gi = GinacInterface(False) - self.suppress_no_ginac_warnings = supress_no_ginac_warnings + self.suppress_no_ginac_warnings = suppress_no_ginac_warnings def simplify(self, expr: NumericExpression): if ginac_available: From 37a955bdfaccec8508887dbf037eb5ea72b5b5cb Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:24:16 -0700 Subject: [PATCH 0323/3044] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 16ce6a44003..477361683ac 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -634,7 +634,7 @@ jobs: if: matrix.other == '/singletest' run: | cd pyomo/contrib/simplification/ - $PYTHON_EXE build.py --inplace + $PYTHON_EXE build.py --inplace - name: Report pyomo plugin information run: | From 05134ce49621f1efd16d961dc20e34c7cff23475 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:45:55 -0700 Subject: [PATCH 0324/3044] install ginac in GHA --- .github/workflows/test_branches.yml | 1 + pyomo/contrib/simplification/build.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 477361683ac..7933aa522d8 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -633,6 +633,7 @@ jobs: - name: Install GiNaC Interface if: matrix.other == '/singletest' run: | + ls /usr/local/include/ginac/ cd pyomo/contrib/simplification/ $PYTHON_EXE build.py --inplace diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index e8bd645756b..39742e1e351 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -17,6 +17,7 @@ def build_ginac_interface(args=[]): sources.append(os.path.join(dname, fname)) ginac_lib = find_library('ginac') + print(ginac_lib) if ginac_lib is None: raise RuntimeError( 'could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable' From 1151927df204f6969704ec7b671e25a1d9083031 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 17:51:34 -0700 Subject: [PATCH 0325/3044] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 7933aa522d8..f2bf057da51 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -633,7 +633,7 @@ jobs: - name: Install GiNaC Interface if: matrix.other == '/singletest' run: | - ls /usr/local/include/ginac/ + export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH cd pyomo/contrib/simplification/ $PYTHON_EXE build.py --inplace From 2c4fdbee83ab76b35a863748048ddf0d091f2f3c Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 18:19:05 -0700 Subject: [PATCH 0326/3044] skip tests when dependencies are not available --- pyomo/contrib/simplification/tests/test_simplification.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index cc278db4d43..c50a906afe7 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -1,11 +1,17 @@ from pyomo.common.unittest import TestCase from pyomo.common import unittest from pyomo.contrib.simplification import Simplifier +from pyomo.contrib.simplification.simplify import ginac_available from pyomo.core.expr.compare import assertExpressionsEqual, compare_expressions import pyomo.environ as pe from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd +from pyomo.common.dependencies import attempt_import +sympy, sympy_available = attempt_import('sympy') + + +@unittest.skipIf((not sympy_available) and (not ginac_available), 'neither sympy nor ginac are available') @unittest.pytest.mark.simplification class TestSimplification(TestCase): def compare_against_possible_results(self, got, expected_list): From 9ebd79b898aea6827232220f59c615c771686ddb Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 18:26:40 -0700 Subject: [PATCH 0327/3044] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index f2bf057da51..b97b3a682af 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -168,7 +168,7 @@ jobs: ./configure make sudo make install - cd + cd .. curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 tar -xvf ginac-1.8.7.tar.bz2 cd ginac-1.8.7 diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 7345fd45e10..b99d39cf6c7 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -180,23 +180,22 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: install ginac - if: ${{ matrix.other == 'singletest' }} + if: matrix.other == '/singletest' run: | - pwd cd .. curl https://www.ginac.de/CLN/cln-1.3.6.tar.bz2 >cln-1.3.6.tar.bz2 tar -xvf cln-1.3.6.tar.bz2 cd cln-1.3.6 ./configure make - make install - cd + sudo make install + cd .. curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 tar -xvf ginac-1.8.7.tar.bz2 cd ginac-1.8.7 ./configure make - make install + sudo make install - name: TPL package download cache uses: actions/cache@v3 @@ -652,6 +651,13 @@ jobs: echo "" pyomo build-extensions --parallel 2 + - name: Install GiNaC Interface + if: matrix.other == '/singletest' + run: | + export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH + cd pyomo/contrib/simplification/ + $PYTHON_EXE build.py --inplace + - name: Report pyomo plugin information run: | echo "$PATH" From 0bd156351b09d93288d618715817fcc4c530114a Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 18:28:39 -0700 Subject: [PATCH 0328/3044] update simplification tests --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- pyomo/contrib/simplification/tests/test_simplification.py | 1 - setup.cfg | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index b97b3a682af..13a5653f9f5 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -97,7 +97,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest or simplification'" + category: "-m 'neos or importtest'" skip_doctest: 1 TARGET: linux PYENV: pip diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index b99d39cf6c7..8a8a9b08030 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -98,7 +98,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest or simplification'" + category: "-m 'neos or importtest'" skip_doctest: 1 TARGET: linux PYENV: pip diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index c50a906afe7..f3bce9cee54 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -12,7 +12,6 @@ @unittest.skipIf((not sympy_available) and (not ginac_available), 'neither sympy nor ginac are available') -@unittest.pytest.mark.simplification class TestSimplification(TestCase): def compare_against_possible_results(self, got, expected_list): success = False diff --git a/setup.cfg b/setup.cfg index a431e0cd601..b606138f38c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,4 +22,3 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests - simplification: marks simplification tests that have expensive (to install) dependencies From 26007ac42688cbe7554807198ceb20e9d121d68e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 18:30:57 -0700 Subject: [PATCH 0329/3044] run black --- pyomo/contrib/simplification/tests/test_simplification.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index f3bce9cee54..e6b5ae863f6 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -11,7 +11,10 @@ sympy, sympy_available = attempt_import('sympy') -@unittest.skipIf((not sympy_available) and (not ginac_available), 'neither sympy nor ginac are available') +@unittest.skipIf( + (not sympy_available) and (not ginac_available), + 'neither sympy nor ginac are available', +) class TestSimplification(TestCase): def compare_against_possible_results(self, got, expected_list): success = False From fc36411c1c57aaf4720938cf7926cfcf7048bc75 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 18:56:21 -0700 Subject: [PATCH 0330/3044] add pytest marker for simplification --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- pyomo/contrib/simplification/tests/test_simplification.py | 1 + setup.cfg | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 13a5653f9f5..b97b3a682af 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -97,7 +97,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest'" + category: "-m 'neos or importtest or simplification'" skip_doctest: 1 TARGET: linux PYENV: pip diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 8a8a9b08030..b99d39cf6c7 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -98,7 +98,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest'" + category: "-m 'neos or importtest or simplification'" skip_doctest: 1 TARGET: linux PYENV: pip diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index e6b5ae863f6..152db93a358 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -15,6 +15,7 @@ (not sympy_available) and (not ginac_available), 'neither sympy nor ginac are available', ) +@unittest.pytest.mark.simplification class TestSimplification(TestCase): def compare_against_possible_results(self, got, expected_list): success = False diff --git a/setup.cfg b/setup.cfg index b606138f38c..855717490b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,3 +22,4 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests + simplification: tests for expression simplification that have expensive (to install) dependencies From 5ba03e215c51fe2feba6a5cff7b2baa162fcb87f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 19:30:50 -0700 Subject: [PATCH 0331/3044] update GHA --- .github/workflows/test_branches.yml | 1 + .github/workflows/test_pr_and_main.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 243f57ea7aa..b73a9cabc81 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -641,6 +641,7 @@ jobs: if: matrix.other == '/singletest' run: | export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH + echo "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV cd pyomo/contrib/simplification/ $PYTHON_EXE build.py --inplace diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 93919ca6bc3..1c36b89710c 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -662,6 +662,7 @@ jobs: if: matrix.other == '/singletest' run: | export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH + echo "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV cd pyomo/contrib/simplification/ $PYTHON_EXE build.py --inplace From 90cdeba86b2deaa76ce7b62cd9f21f906b057462 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 19:32:33 -0700 Subject: [PATCH 0332/3044] update GHA --- .github/workflows/test_branches.yml | 4 ++-- .github/workflows/test_pr_and_main.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index b73a9cabc81..e3e0c3e6caf 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -166,14 +166,14 @@ jobs: tar -xvf cln-1.3.6.tar.bz2 cd cln-1.3.6 ./configure - make + make -j 2 sudo make install cd .. curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 tar -xvf ginac-1.8.7.tar.bz2 cd ginac-1.8.7 ./configure - make + make -j 2 sudo make install - name: TPL package download cache diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 1c36b89710c..9edb8b1c65f 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -187,14 +187,14 @@ jobs: tar -xvf cln-1.3.6.tar.bz2 cd cln-1.3.6 ./configure - make + make -j 2 sudo make install cd .. curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 tar -xvf ginac-1.8.7.tar.bz2 cd ginac-1.8.7 ./configure - make + make -j 2 sudo make install - name: TPL package download cache From 17cb11d31d72d7ac9ac9f37e1ec306f8d114cec4 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 19:50:50 -0700 Subject: [PATCH 0333/3044] debugging GHA --- .github/workflows/test_branches.yml | 1 + .github/workflows/test_pr_and_main.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index e3e0c3e6caf..4e3c14cb70e 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -655,6 +655,7 @@ jobs: - name: Run Pyomo tests if: matrix.mpi == 0 run: | + $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface" $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 9edb8b1c65f..1626964a7e9 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -676,6 +676,7 @@ jobs: - name: Run Pyomo tests if: matrix.mpi == 0 run: | + $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface" $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ From d1fe24400ed424afdfdf65e3f9918faefc98db96 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 20:02:34 -0700 Subject: [PATCH 0334/3044] test simplification with ginac and sympy --- .../simplification/tests/test_simplification.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 152db93a358..096d776460d 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -12,11 +12,10 @@ @unittest.skipIf( - (not sympy_available) and (not ginac_available), - 'neither sympy nor ginac are available', + (not sympy_available) or (ginac_available), + 'sympy is not available', ) -@unittest.pytest.mark.simplification -class TestSimplification(TestCase): +class TestSimplificationSympy(TestCase): def compare_against_possible_results(self, got, expected_list): success = False for exp in expected_list: @@ -111,3 +110,12 @@ def test_unary(self): simp = Simplifier() e2 = simp.simplify(e) assertExpressionsEqual(self, e, e2) + + +@unittest.skipIf( + not ginac_available, + 'GiNaC is not available', +) +@unittest.pytest.mark.simplification +class TestSimplificationGiNaC(TestSimplificationSympy): + pass From 9159c3c5854c7a9d5437f3308321a9fd4a61be6f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 20:03:18 -0700 Subject: [PATCH 0335/3044] test simplification with ginac and sympy --- .../tests/test_simplification.py | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 096d776460d..3124d856784 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -35,25 +35,6 @@ def test_simplify(self): expected = x**-1.0 assertExpressionsEqual(self, expected, der2_simp) - def test_param(self): - m = pe.ConcreteModel() - x = m.x = pe.Var() - p = m.p = pe.Param(mutable=True) - e1 = p * x**2 + p * x + p * x**2 - simp = Simplifier() - e2 = simp.simplify(e1) - self.compare_against_possible_results( - e2, - [ - p * x**2.0 * 2.0 + p * x, - p * x + p * x**2.0 * 2.0, - 2.0 * p * x**2.0 + p * x, - p * x + 2.0 * p * x**2.0, - x**2.0 * p * 2.0 + p * x, - p * x + x**2.0 * p * 2.0, - ], - ) - def test_mul(self): m = pe.ConcreteModel() x = m.x = pe.Var() @@ -118,4 +99,21 @@ def test_unary(self): ) @unittest.pytest.mark.simplification class TestSimplificationGiNaC(TestSimplificationSympy): - pass + def test_param(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + p = m.p = pe.Param(mutable=True) + e1 = p * x**2 + p * x + p * x**2 + simp = Simplifier() + e2 = simp.simplify(e1) + self.compare_against_possible_results( + e2, + [ + p * x**2.0 * 2.0 + p * x, + p * x + p * x**2.0 * 2.0, + 2.0 * p * x**2.0 + p * x, + p * x + 2.0 * p * x**2.0, + x**2.0 * p * 2.0 + p * x, + p * x + x**2.0 * p * 2.0, + ], + ) From 457f2b378b772eae16b47fa789423bca70de43c5 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 22:17:46 -0700 Subject: [PATCH 0336/3044] fixing simplification tests --- .github/workflows/test_branches.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 4e3c14cb70e..0eb6fe166d9 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -655,7 +655,8 @@ jobs: - name: Run Pyomo tests if: matrix.mpi == 0 run: | - $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface" + $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface; print(GinacInterface)" + $PYTHON_EXE -c "from pyomo.contrib.simplification.simplify import ginac_available; print(ginac_available)" $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ From 1e7c3f1b2ecf562000088df80f215ddf6d992420 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 22:44:54 -0700 Subject: [PATCH 0337/3044] fixing simplification tests --- .../simplification/tests/test_simplification.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 3124d856784..6badc76b957 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -11,11 +11,7 @@ sympy, sympy_available = attempt_import('sympy') -@unittest.skipIf( - (not sympy_available) or (ginac_available), - 'sympy is not available', -) -class TestSimplificationSympy(TestCase): +class SimplificationMixin: def compare_against_possible_results(self, got, expected_list): success = False for exp in expected_list: @@ -93,12 +89,20 @@ def test_unary(self): assertExpressionsEqual(self, e, e2) +@unittest.skipIf( + (not sympy_available) or (ginac_available), + 'sympy is not available', +) +class TestSimplificationSympy(TestCase, SimplificationMixin): + pass + + @unittest.skipIf( not ginac_available, 'GiNaC is not available', ) @unittest.pytest.mark.simplification -class TestSimplificationGiNaC(TestSimplificationSympy): +class TestSimplificationGiNaC(TestCase, SimplificationMixin): def test_param(self): m = pe.ConcreteModel() x = m.x = pe.Var() From b2d969f73e4dbf1e80d453dd7be4dcd474fc32be Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 22:46:36 -0700 Subject: [PATCH 0338/3044] fixing simplification tests --- .../simplification/tests/test_simplification.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 6badc76b957..e3c60cb02ca 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -89,18 +89,12 @@ def test_unary(self): assertExpressionsEqual(self, e, e2) -@unittest.skipIf( - (not sympy_available) or (ginac_available), - 'sympy is not available', -) +@unittest.skipIf((not sympy_available) or (ginac_available), 'sympy is not available') class TestSimplificationSympy(TestCase, SimplificationMixin): pass -@unittest.skipIf( - not ginac_available, - 'GiNaC is not available', -) +@unittest.skipIf(not ginac_available, 'GiNaC is not available') @unittest.pytest.mark.simplification class TestSimplificationGiNaC(TestCase, SimplificationMixin): def test_param(self): From da2fe3d714b34c7b03a41a2c63af8590db87d2e4 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 23:04:15 -0700 Subject: [PATCH 0339/3044] fixing simplification tests --- .github/workflows/test_branches.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 0eb6fe166d9..1124a253ac8 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -657,6 +657,7 @@ jobs: run: | $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface; print(GinacInterface)" $PYTHON_EXE -c "from pyomo.contrib.simplification.simplify import ginac_available; print(ginac_available)" + pytest -v pyomo/contrib/simplification/tests/test_simplification.py $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ From 317dae89cdb4eac00e006bc808d32d887a14d787 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 11 Jan 2024 14:30:08 -0500 Subject: [PATCH 0340/3044] update the version of MindtPy --- pyomo/contrib/mindtpy/MindtPy.py | 8 ++++++++ pyomo/contrib/mindtpy/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/MindtPy.py b/pyomo/contrib/mindtpy/MindtPy.py index 6eb27c4c649..bd873d950fd 100644 --- a/pyomo/contrib/mindtpy/MindtPy.py +++ b/pyomo/contrib/mindtpy/MindtPy.py @@ -50,6 +50,14 @@ - Add single-tree implementation. - Add support for cplex_persistent solver. - Fix bug in OA cut expression in cut_generation.py. + +24.1.11 changes: +- fix gurobi single tree termination check bug +- fix Gurobi single tree cycle handling +- fix bug in feasibility pump method +- add special handling for infeasible relaxed NLP +- update the log format of infeasible fixed NLP subproblems +- create a new copy_var_list_values function """ from pyomo.contrib.mindtpy import __version__ diff --git a/pyomo/contrib/mindtpy/__init__.py b/pyomo/contrib/mindtpy/__init__.py index 8e2c2d9eaa4..8dcd085211f 100644 --- a/pyomo/contrib/mindtpy/__init__.py +++ b/pyomo/contrib/mindtpy/__init__.py @@ -1 +1 @@ -__version__ = (0, 1, 0) +__version__ = (1, 0, 0) From 55b77d83db3b03556b5cd8484fa7311e72195c37 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 11 Jan 2024 12:41:18 -0700 Subject: [PATCH 0341/3044] factor out the binary variable in gdp to minlp transformation --- pyomo/gdp/plugins/gdp_to_minlp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/gdp/plugins/gdp_to_minlp.py b/pyomo/gdp/plugins/gdp_to_minlp.py index bec9160ceca..5add18fd1cc 100644 --- a/pyomo/gdp/plugins/gdp_to_minlp.py +++ b/pyomo/gdp/plugins/gdp_to_minlp.py @@ -150,7 +150,7 @@ def _add_constraint_expressions( if (c.equality or lb is ub) and lb is not None: # equality newConstraint.add( - (name, i, 'eq'), c.body * indicator_var - lb * indicator_var == 0 + (name, i, 'eq'), (c.body - lb) * indicator_var == 0 ) constraintMap['transformedConstraints'][c] = [newConstraint[name, i, 'eq']] constraintMap['srcConstraints'][newConstraint[name, i, 'eq']] = c @@ -158,7 +158,7 @@ def _add_constraint_expressions( # inequality if lb is not None: newConstraint.add( - (name, i, 'lb'), 0 <= c.body * indicator_var - lb * indicator_var + (name, i, 'lb'), 0 <= (c.body - lb) * indicator_var ) constraintMap['transformedConstraints'][c] = [ newConstraint[name, i, 'lb'] @@ -166,7 +166,7 @@ def _add_constraint_expressions( constraintMap['srcConstraints'][newConstraint[name, i, 'lb']] = c if ub is not None: newConstraint.add( - (name, i, 'ub'), c.body * indicator_var - ub * indicator_var <= 0 + (name, i, 'ub'), (c.body - ub) * indicator_var <= 0 ) transformed = constraintMap['transformedConstraints'].get(c) if transformed is not None: From fdeef16f8327be0cbde3e58248e5a78fb36650b1 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 12 Jan 2024 10:36:51 -0700 Subject: [PATCH 0342/3044] rename gdp_to_minlp to binary_multiplication --- pyomo/gdp/plugins/gdp_to_minlp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/gdp/plugins/gdp_to_minlp.py b/pyomo/gdp/plugins/gdp_to_minlp.py index 5add18fd1cc..1ab6fd6b768 100644 --- a/pyomo/gdp/plugins/gdp_to_minlp.py +++ b/pyomo/gdp/plugins/gdp_to_minlp.py @@ -9,14 +9,14 @@ import logging -logger = logging.getLogger('pyomo.gdp.gdp_to_minlp') +logger = logging.getLogger('pyomo.gdp.binary_multiplication') @TransformationFactory.register( - 'gdp.gdp_to_minlp', doc="Reformulate the GDP as an MINLP." + 'gdp.binary_multiplication', doc="Reformulate the GDP as an MINLP by multiplying f(x) <= 0 by y to get f(x) * y <= 0." ) class GDPToMINLPTransformation(GDP_to_MIP_Transformation): - CONFIG = ConfigDict("gdp.gdp_to_minlp") + CONFIG = ConfigDict("gdp.binary_multiplication") CONFIG.declare( 'targets', ConfigValue( @@ -33,7 +33,7 @@ class GDPToMINLPTransformation(GDP_to_MIP_Transformation): ), ) - transformation_name = 'gdp_to_minlp' + transformation_name = 'binary_multiplication' def __init__(self): super().__init__(logger) From e16fa151b20bca0d3d0fa798e25ea94d37348134 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 12 Jan 2024 10:37:38 -0700 Subject: [PATCH 0343/3044] rename gdp_to_minlp to binary_multiplication --- pyomo/gdp/plugins/{gdp_to_minlp.py => binary_multiplication.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pyomo/gdp/plugins/{gdp_to_minlp.py => binary_multiplication.py} (100%) diff --git a/pyomo/gdp/plugins/gdp_to_minlp.py b/pyomo/gdp/plugins/binary_multiplication.py similarity index 100% rename from pyomo/gdp/plugins/gdp_to_minlp.py rename to pyomo/gdp/plugins/binary_multiplication.py From 94c2200b8e1bf6c841ecf1803eb0fc08ecd33e07 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 12 Jan 2024 10:39:58 -0700 Subject: [PATCH 0344/3044] rename gdp_to_minlp to binary_multiplication --- pyomo/gdp/plugins/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/gdp/plugins/__init__.py b/pyomo/gdp/plugins/__init__.py index 39761697a0f..2edb99bbe1b 100644 --- a/pyomo/gdp/plugins/__init__.py +++ b/pyomo/gdp/plugins/__init__.py @@ -22,4 +22,4 @@ def load(): import pyomo.gdp.plugins.multiple_bigm import pyomo.gdp.plugins.transform_current_disjunctive_state import pyomo.gdp.plugins.bound_pretransformation - import pyomo.gdp.plugins.gdp_to_minlp + import pyomo.gdp.plugins.binary_multiplication From bf64515bd9f404f3da3e29d4fa9d6bc98b52dcff Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 12 Jan 2024 10:42:46 -0700 Subject: [PATCH 0345/3044] rename gdp_to_minlp to binary_multiplication --- .../tests/{test_gdp_to_minlp.py => test_binary_multiplication.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pyomo/gdp/tests/{test_gdp_to_minlp.py => test_binary_multiplication.py} (100%) diff --git a/pyomo/gdp/tests/test_gdp_to_minlp.py b/pyomo/gdp/tests/test_binary_multiplication.py similarity index 100% rename from pyomo/gdp/tests/test_gdp_to_minlp.py rename to pyomo/gdp/tests/test_binary_multiplication.py From 71c4f1faa35ef6c5dd43f5b563fdf0e7d0f5be86 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 12 Jan 2024 10:49:19 -0700 Subject: [PATCH 0346/3044] rename gdp_to_minlp to binary_multiplication --- pyomo/gdp/tests/test_binary_multiplication.py | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/pyomo/gdp/tests/test_binary_multiplication.py b/pyomo/gdp/tests/test_binary_multiplication.py index 532922ee1cc..2c7d20f91a9 100644 --- a/pyomo/gdp/tests/test_binary_multiplication.py +++ b/pyomo/gdp/tests/test_binary_multiplication.py @@ -32,7 +32,7 @@ class CommonTests: def diff_apply_to_and_create_using(self, model): - ct.diff_apply_to_and_create_using(self, model, 'gdp.gdp_to_minlp') + ct.diff_apply_to_and_create_using(self, model, 'gdp.binary_multiplication') class TwoTermDisj(unittest.TestCase, CommonTests): @@ -42,10 +42,10 @@ def setUp(self): def test_new_block_created(self): m = models.makeTwoTermDisj() - TransformationFactory('gdp.gdp_to_minlp').apply_to(m) + TransformationFactory('gdp.binary_multiplication').apply_to(m) # we have a transformation block - transBlock = m.component("_pyomo_gdp_gdp_to_minlp_reformulation") + transBlock = m.component("_pyomo_gdp_binary_multiplication_reformulation") self.assertIsInstance(transBlock, Block) disjBlock = transBlock.component("relaxedDisjuncts") @@ -56,72 +56,72 @@ def test_new_block_created(self): self.assertIs(m.d[1].transformation_block, disjBlock[1]) def test_disjunction_deactivated(self): - ct.check_disjunction_deactivated(self, 'gdp_to_minlp') + ct.check_disjunction_deactivated(self, 'binary_multiplication') def test_disjunctDatas_deactivated(self): - ct.check_disjunctDatas_deactivated(self, 'gdp_to_minlp') + ct.check_disjunctDatas_deactivated(self, 'binary_multiplication') def test_do_not_transform_twice_if_disjunction_reactivated(self): - ct.check_do_not_transform_twice_if_disjunction_reactivated(self, 'gdp_to_minlp') + ct.check_do_not_transform_twice_if_disjunction_reactivated(self, 'binary_multiplication') def test_xor_constraint_mapping(self): - ct.check_xor_constraint_mapping(self, 'gdp_to_minlp') + ct.check_xor_constraint_mapping(self, 'binary_multiplication') def test_xor_constraint_mapping_two_disjunctions(self): - ct.check_xor_constraint_mapping_two_disjunctions(self, 'gdp_to_minlp') + ct.check_xor_constraint_mapping_two_disjunctions(self, 'binary_multiplication') def test_disjunct_mapping(self): - ct.check_disjunct_mapping(self, 'gdp_to_minlp') + ct.check_disjunct_mapping(self, 'binary_multiplication') def test_disjunct_and_constraint_maps(self): """Tests the actual data structures used to store the maps.""" m = models.makeTwoTermDisj() - gdp_to_minlp = TransformationFactory('gdp.gdp_to_minlp') - gdp_to_minlp.apply_to(m) - disjBlock = m._pyomo_gdp_gdp_to_minlp_reformulation.relaxedDisjuncts + binary_multiplication = TransformationFactory('gdp.binary_multiplication') + binary_multiplication.apply_to(m) + disjBlock = m._pyomo_gdp_binary_multiplication_reformulation.relaxedDisjuncts oldblock = m.component("d") # we are counting on the fact that the disjuncts get relaxed in the # same order every time. for i in [0, 1]: self.assertIs(oldblock[i].transformation_block, disjBlock[i]) - self.assertIs(gdp_to_minlp.get_src_disjunct(disjBlock[i]), oldblock[i]) + self.assertIs(binary_multiplication.get_src_disjunct(disjBlock[i]), oldblock[i]) # check constraint dict has right mapping - c1_list = gdp_to_minlp.get_transformed_constraints(oldblock[1].c1) + c1_list = binary_multiplication.get_transformed_constraints(oldblock[1].c1) # this is an equality self.assertEqual(len(c1_list), 1) self.assertIs(c1_list[0].parent_block(), disjBlock[1]) - self.assertIs(gdp_to_minlp.get_src_constraint(c1_list[0]), oldblock[1].c1) + self.assertIs(binary_multiplication.get_src_constraint(c1_list[0]), oldblock[1].c1) - c2_list = gdp_to_minlp.get_transformed_constraints(oldblock[1].c2) + c2_list = binary_multiplication.get_transformed_constraints(oldblock[1].c2) # just ub self.assertEqual(len(c2_list), 1) self.assertIs(c2_list[0].parent_block(), disjBlock[1]) - self.assertIs(gdp_to_minlp.get_src_constraint(c2_list[0]), oldblock[1].c2) + self.assertIs(binary_multiplication.get_src_constraint(c2_list[0]), oldblock[1].c2) - c_list = gdp_to_minlp.get_transformed_constraints(oldblock[0].c) + c_list = binary_multiplication.get_transformed_constraints(oldblock[0].c) # just lb self.assertEqual(len(c_list), 1) self.assertIs(c_list[0].parent_block(), disjBlock[0]) - self.assertIs(gdp_to_minlp.get_src_constraint(c_list[0]), oldblock[0].c) + self.assertIs(binary_multiplication.get_src_constraint(c_list[0]), oldblock[0].c) def test_new_block_nameCollision(self): - ct.check_transformation_block_name_collision(self, 'gdp_to_minlp') + ct.check_transformation_block_name_collision(self, 'binary_multiplication') def test_indicator_vars(self): - ct.check_indicator_vars(self, 'gdp_to_minlp') + ct.check_indicator_vars(self, 'binary_multiplication') def test_xor_constraints(self): - ct.check_xor_constraint(self, 'gdp_to_minlp') + ct.check_xor_constraint(self, 'binary_multiplication') def test_or_constraints(self): m = models.makeTwoTermDisj() m.disjunction.xor = False - TransformationFactory('gdp.gdp_to_minlp').apply_to(m) + TransformationFactory('gdp.binary_multiplication').apply_to(m) # check or constraint is an or (upper bound is None) - orcons = m._pyomo_gdp_gdp_to_minlp_reformulation.component("disjunction_xor") + orcons = m._pyomo_gdp_binary_multiplication_reformulation.component("disjunction_xor") self.assertIsInstance(orcons, Constraint) assertExpressionsEqual( self, @@ -137,35 +137,35 @@ def test_or_constraints(self): self.assertIsNone(orcons.upper) def test_deactivated_constraints(self): - ct.check_deactivated_constraints(self, 'gdp_to_minlp') + ct.check_deactivated_constraints(self, 'binary_multiplication') def test_transformed_constraints(self): m = models.makeTwoTermDisj() - gdp_to_minlp = TransformationFactory('gdp.gdp_to_minlp') - gdp_to_minlp.apply_to(m) - self.check_transformed_constraints(m, gdp_to_minlp, -3, 2, 7, 2) + binary_multiplication = TransformationFactory('gdp.binary_multiplication') + binary_multiplication.apply_to(m) + self.check_transformed_constraints(m, binary_multiplication, -3, 2, 7, 2) def test_do_not_transform_userDeactivated_disjuncts(self): - ct.check_user_deactivated_disjuncts(self, 'gdp_to_minlp') + ct.check_user_deactivated_disjuncts(self, 'binary_multiplication') def test_improperly_deactivated_disjuncts(self): - ct.check_improperly_deactivated_disjuncts(self, 'gdp_to_minlp') + ct.check_improperly_deactivated_disjuncts(self, 'binary_multiplication') def test_do_not_transform_userDeactivated_IndexedDisjunction(self): ct.check_do_not_transform_userDeactivated_indexedDisjunction( - self, 'gdp_to_minlp' + self, 'binary_multiplication' ) # helper method to check the M values in all of the transformed # constraints (m, M) is the tuple for M. This also relies on the # disjuncts being transformed in the same order every time. def check_transformed_constraints( - self, model, gdp_to_minlp, cons1lb, cons2lb, cons2ub, cons3ub + self, model, binary_multiplication, cons1lb, cons2lb, cons2ub, cons3ub ): - disjBlock = model._pyomo_gdp_gdp_to_minlp_reformulation.relaxedDisjuncts + disjBlock = model._pyomo_gdp_binary_multiplication_reformulation.relaxedDisjuncts # first constraint - c = gdp_to_minlp.get_transformed_constraints(model.d[0].c) + c = binary_multiplication.get_transformed_constraints(model.d[0].c) self.assertEqual(len(c), 1) c_lb = c[0] self.assertTrue(c[0].active) @@ -181,7 +181,7 @@ def check_transformed_constraints( self.assertIsNone(c[0].upper) # second constraint - c = gdp_to_minlp.get_transformed_constraints(model.d[1].c1) + c = binary_multiplication.get_transformed_constraints(model.d[1].c1) self.assertEqual(len(c), 1) c_eq = c[0] self.assertTrue(c[0].active) @@ -196,7 +196,7 @@ def check_transformed_constraints( self.assertEqual(c[0].upper, 0) # third constraint - c = gdp_to_minlp.get_transformed_constraints(model.d[1].c2) + c = binary_multiplication.get_transformed_constraints(model.d[1].c2) self.assertEqual(len(c), 1) c_ub = c[0] self.assertTrue(c_ub.active) @@ -230,8 +230,8 @@ def d_rule(d, j): m.d = Disjunct(m.I, rule=d_rule) m.disjunction = Disjunction(expr=[m.d[i] for i in m.I]) - TransformationFactory('gdp.gdp_to_minlp').apply_to(m) - transBlock = m._pyomo_gdp_gdp_to_minlp_reformulation + TransformationFactory('gdp.binary_multiplication').apply_to(m) + transBlock = m._pyomo_gdp_binary_multiplication_reformulation # 2 blocks: the original Disjunct and the transformation block self.assertEqual(len(list(m.component_objects(Block, descend_into=False))), 1) @@ -260,8 +260,8 @@ def d_rule(d, j): m.d = Disjunct(m.I, rule=d_rule) m.disjunction = Disjunction(expr=[m.d[i] for i in m.I]) - TransformationFactory('gdp.gdp_to_minlp').apply_to(m) - transBlock = m._pyomo_gdp_gdp_to_minlp_reformulation + TransformationFactory('gdp.binary_multiplication').apply_to(m) + transBlock = m._pyomo_gdp_binary_multiplication_reformulation # 2 blocks: the original Disjunct and the transformation block self.assertEqual(len(list(m.component_objects(Block, descend_into=False))), 1) @@ -278,12 +278,12 @@ def d_rule(d, j): def test_local_var(self): m = models.localVar() - gdp_to_minlp = TransformationFactory('gdp.gdp_to_minlp') - gdp_to_minlp.apply_to(m) + binary_multiplication = TransformationFactory('gdp.binary_multiplication') + binary_multiplication.apply_to(m) # we just need to make sure that constraint was transformed correctly, # which just means that the M values were correct. - transformedC = gdp_to_minlp.get_transformed_constraints(m.disj2.cons) + transformedC = binary_multiplication.get_transformed_constraints(m.disj2.cons) self.assertEqual(len(transformedC), 1) eq = transformedC[0] repn = generate_standard_repn(eq.body) From 0f2d6439e3e8fa9ead232e42f854fd22743ad9a0 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 12 Jan 2024 11:21:33 -0700 Subject: [PATCH 0347/3044] rename gdp_to_minlp to binary_multiplication --- pyomo/gdp/plugins/binary_multiplication.py | 15 ++++------ pyomo/gdp/tests/test_binary_multiplication.py | 28 ++++++++++++++----- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/pyomo/gdp/plugins/binary_multiplication.py b/pyomo/gdp/plugins/binary_multiplication.py index 1ab6fd6b768..2305f244f29 100644 --- a/pyomo/gdp/plugins/binary_multiplication.py +++ b/pyomo/gdp/plugins/binary_multiplication.py @@ -13,7 +13,8 @@ @TransformationFactory.register( - 'gdp.binary_multiplication', doc="Reformulate the GDP as an MINLP by multiplying f(x) <= 0 by y to get f(x) * y <= 0." + 'gdp.binary_multiplication', + doc="Reformulate the GDP as an MINLP by multiplying f(x) <= 0 by y to get f(x) * y <= 0.", ) class GDPToMINLPTransformation(GDP_to_MIP_Transformation): CONFIG = ConfigDict("gdp.binary_multiplication") @@ -149,25 +150,19 @@ def _add_constraint_expressions( lb, ub = c.lower, c.upper if (c.equality or lb is ub) and lb is not None: # equality - newConstraint.add( - (name, i, 'eq'), (c.body - lb) * indicator_var == 0 - ) + newConstraint.add((name, i, 'eq'), (c.body - lb) * indicator_var == 0) constraintMap['transformedConstraints'][c] = [newConstraint[name, i, 'eq']] constraintMap['srcConstraints'][newConstraint[name, i, 'eq']] = c else: # inequality if lb is not None: - newConstraint.add( - (name, i, 'lb'), 0 <= (c.body - lb) * indicator_var - ) + newConstraint.add((name, i, 'lb'), 0 <= (c.body - lb) * indicator_var) constraintMap['transformedConstraints'][c] = [ newConstraint[name, i, 'lb'] ] constraintMap['srcConstraints'][newConstraint[name, i, 'lb']] = c if ub is not None: - newConstraint.add( - (name, i, 'ub'), (c.body - ub) * indicator_var <= 0 - ) + newConstraint.add((name, i, 'ub'), (c.body - ub) * indicator_var <= 0) transformed = constraintMap['transformedConstraints'].get(c) if transformed is not None: constraintMap['transformedConstraints'][c].append( diff --git a/pyomo/gdp/tests/test_binary_multiplication.py b/pyomo/gdp/tests/test_binary_multiplication.py index 2c7d20f91a9..2c6e045f853 100644 --- a/pyomo/gdp/tests/test_binary_multiplication.py +++ b/pyomo/gdp/tests/test_binary_multiplication.py @@ -62,7 +62,9 @@ def test_disjunctDatas_deactivated(self): ct.check_disjunctDatas_deactivated(self, 'binary_multiplication') def test_do_not_transform_twice_if_disjunction_reactivated(self): - ct.check_do_not_transform_twice_if_disjunction_reactivated(self, 'binary_multiplication') + ct.check_do_not_transform_twice_if_disjunction_reactivated( + self, 'binary_multiplication' + ) def test_xor_constraint_mapping(self): ct.check_xor_constraint_mapping(self, 'binary_multiplication') @@ -85,26 +87,34 @@ def test_disjunct_and_constraint_maps(self): # same order every time. for i in [0, 1]: self.assertIs(oldblock[i].transformation_block, disjBlock[i]) - self.assertIs(binary_multiplication.get_src_disjunct(disjBlock[i]), oldblock[i]) + self.assertIs( + binary_multiplication.get_src_disjunct(disjBlock[i]), oldblock[i] + ) # check constraint dict has right mapping c1_list = binary_multiplication.get_transformed_constraints(oldblock[1].c1) # this is an equality self.assertEqual(len(c1_list), 1) self.assertIs(c1_list[0].parent_block(), disjBlock[1]) - self.assertIs(binary_multiplication.get_src_constraint(c1_list[0]), oldblock[1].c1) + self.assertIs( + binary_multiplication.get_src_constraint(c1_list[0]), oldblock[1].c1 + ) c2_list = binary_multiplication.get_transformed_constraints(oldblock[1].c2) # just ub self.assertEqual(len(c2_list), 1) self.assertIs(c2_list[0].parent_block(), disjBlock[1]) - self.assertIs(binary_multiplication.get_src_constraint(c2_list[0]), oldblock[1].c2) + self.assertIs( + binary_multiplication.get_src_constraint(c2_list[0]), oldblock[1].c2 + ) c_list = binary_multiplication.get_transformed_constraints(oldblock[0].c) # just lb self.assertEqual(len(c_list), 1) self.assertIs(c_list[0].parent_block(), disjBlock[0]) - self.assertIs(binary_multiplication.get_src_constraint(c_list[0]), oldblock[0].c) + self.assertIs( + binary_multiplication.get_src_constraint(c_list[0]), oldblock[0].c + ) def test_new_block_nameCollision(self): ct.check_transformation_block_name_collision(self, 'binary_multiplication') @@ -121,7 +131,9 @@ def test_or_constraints(self): TransformationFactory('gdp.binary_multiplication').apply_to(m) # check or constraint is an or (upper bound is None) - orcons = m._pyomo_gdp_binary_multiplication_reformulation.component("disjunction_xor") + orcons = m._pyomo_gdp_binary_multiplication_reformulation.component( + "disjunction_xor" + ) self.assertIsInstance(orcons, Constraint) assertExpressionsEqual( self, @@ -162,7 +174,9 @@ def test_do_not_transform_userDeactivated_IndexedDisjunction(self): def check_transformed_constraints( self, model, binary_multiplication, cons1lb, cons2lb, cons2ub, cons3ub ): - disjBlock = model._pyomo_gdp_binary_multiplication_reformulation.relaxedDisjuncts + disjBlock = ( + model._pyomo_gdp_binary_multiplication_reformulation.relaxedDisjuncts + ) # first constraint c = binary_multiplication.get_transformed_constraints(model.d[0].c) From ac7f3a28f28d4397273627b792e8c56fdf6be83e Mon Sep 17 00:00:00 2001 From: Arguello Date: Fri, 12 Jan 2024 14:50:35 -0700 Subject: [PATCH 0348/3044] making sure tests run and changing hexagonal cases to pentagonal cases --- .../alternative_solutions/tests/test_obbt.py | 22 +++++++++---------- .../tests/test_shifted_lp.py | 3 +-- .../tests/test_solnpool.py | 6 ++--- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index 753f6a254a4..2f98f4a37bb 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -43,7 +43,7 @@ class TestOBBTUnit(unittest.TestCase): ''' - def ttest_obbt_continuous(self): + def test_obbt_continuous(self): '''Check that the correct bounds are found for a continuous problem.''' m = tc.get_2d_diamond_problem() results = obbt_analysis(m, solver=mip_solver) @@ -51,20 +51,20 @@ def ttest_obbt_continuous(self): for var, bounds in results.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - def ttest_mip_rel_objective(self): + def test_mip_rel_objective(self): '''Check that relative mip gap constraints are added for a mip with indexed vars and constraints''' - m = tc.get_indexed_hexagonal_pyramid_mip() + m = tc.get_indexed_pentagonal_pyramid_mip() results = obbt_analysis(m, rel_opt_gap=0.5) self.assertAlmostEqual(m._obbt.optimality_tol_rel.lb, 2.5) - def ttest_mip_abs_objective(self): + def test_mip_abs_objective(self): '''Check that absolute mip gap constraints are added''' - m = tc.get_hexagonal_pyramid_mip() + m = tc.get_pentagonal_pyramid_mip() results = obbt_analysis(m, abs_opt_gap=1.99) self.assertAlmostEqual(m._obbt.optimality_tol_abs.lb, 3.01) - def ttest_obbt_warmstart(self): + def test_obbt_warmstart(self): '''Check that warmstarting works.''' m = tc.get_2d_diamond_problem() m.x.value = 0 @@ -74,10 +74,10 @@ def ttest_obbt_warmstart(self): for var, bounds in results.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - def ttest_obbt_mip(self): + def test_obbt_mip(self): '''Check that bound tightening only occurs for continuous variables that can be tightened.''' - m = tc.get_bloated_hexagonal_pyramid_mip() + m = tc.get_bloated_pentagonal_pyramid_mip() results = obbt_analysis(m, solver=mip_solver, tee = True) bounds_tightened = False bounds_not_tightned = False @@ -101,7 +101,7 @@ def test_obbt_unbounded(self): for var, bounds in results.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - def ttest_bound_tightening(self): + def test_bound_tightening(self): ''' Check that the correct bounds are found for a discrete problem where more restrictive bounds are implied by the constraints.''' @@ -111,7 +111,7 @@ def ttest_bound_tightening(self): for var, bounds in results.items(): assert_array_almost_equal(bounds, m.var_bounds[var]) - def ttest_bound_refinement(self): + def test_bound_refinement(self): ''' Check that the correct bounds are found for a discrete problem where more restrictive bounds are implied by the constraints and constraints @@ -124,7 +124,7 @@ def ttest_bound_refinement(self): if m.var_bounds[var][1] < var.ub: self.assertTrue(hasattr(m._obbt, var.name + "_ub")) - def ttest_obbt_infeasible(self): + def test_obbt_infeasible(self): '''Check that code catches cases where the problem is infeasible.''' m = tc.get_2d_diamond_problem() m.infeasible_constraint = pe.Constraint(expr=m.x>=10) diff --git a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py index 9ca2c1383b3..8774b09c506 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py @@ -27,7 +27,7 @@ class TestShiftedIP(unittest.TestCase): def mip_abs_objective(self): '''COMMENT''' - m = tc.get_indexed_hexagonal_pyramid_mip() + m = tc.get_indexed_pentagonal_pyramid_mip() m.x.domain = pe.Reals opt = pe.SolverFactory('gurobi') old_results = opt.solve(m, tee = True) @@ -46,7 +46,6 @@ def test_polyhedron(self): new_model = shifted_lp.get_shifted_linear_model(m) new_results = opt.solve(new_model, tee = True) new_obj = pe.value(new_model.objective) - pdb.set_trace() if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index abfc5750ed3..5bcef9a9792 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -54,7 +54,7 @@ def test_mip_feasibility(self): ''' Check that the correct number of alternate solutions are found for each objective value in a mip with known solutions''' - m = tc.get_indexed_hexagonal_pyramid_mip() + m = tc.get_indexed_pentagonal_pyramid_mip() results = sp.gurobi_generate_solutions(m, 100, tee = True) objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns @@ -65,7 +65,7 @@ def test_mip_rel_feasibility(self): ''' Check that relative mip gap constraints are added and the correct number of alternative solutions are found''' - m = tc.get_hexagonal_pyramid_mip() + m = tc.get_pentagonal_pyramid_mip() results = sp.gurobi_generate_solutions(m, 100, rel_opt_gap=.2) objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns[0:2] @@ -76,7 +76,7 @@ def test_mip_abs_feasibility(self): ''' Check that absolute mip gap constraints are added and the correct number of alternative solutions are found''' - m = tc.get_hexagonal_pyramid_mip() + m = tc.get_pentagonal_pyramid_mip() results = sp.gurobi_generate_solutions(m, 100, abs_opt_gap=1.99) objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns[0:3] From 7828311676ecad4ae60600104333eac0a9c01a2a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 15 Jan 2024 11:49:14 -0700 Subject: [PATCH 0349/3044] Typo correction --- pyomo/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solver/base.py b/pyomo/solver/base.py index 202b0422cee..d7f4adabf56 100644 --- a/pyomo/solver/base.py +++ b/pyomo/solver/base.py @@ -405,7 +405,7 @@ def solve( self.config.symbolic_solver_labels = symbolic_solver_labels self.config.time_limit = timelimit self.config.report_timing = report_timing - # This is a new flag in the interface. To preserve backwards compability, + # This is a new flag in the interface. To preserve backwards compatibility, # its default is set to "False" self.config.raise_exception_on_nonoptimal_result = ( raise_exception_on_nonoptimal_result From 5c452be23adc9cb9864c56176b60da32e068d2bc Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 15 Jan 2024 12:07:54 -0700 Subject: [PATCH 0350/3044] minor updates --- pyomo/solver/config.py | 1 + pyomo/solver/ipopt.py | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pyomo/solver/config.py b/pyomo/solver/config.py index 54a497cee0c..ef0114ba439 100644 --- a/pyomo/solver/config.py +++ b/pyomo/solver/config.py @@ -85,6 +85,7 @@ def __init__( ConfigValue( domain=NonNegativeInt, description="Number of threads to be used by a solver.", + default=None, ), ) self.time_limit: Optional[float] = self.declare( diff --git a/pyomo/solver/ipopt.py b/pyomo/solver/ipopt.py index 406f4291c44..51f48ec4881 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/solver/ipopt.py @@ -22,6 +22,7 @@ from pyomo.common.tempfiles import TempfileManager from pyomo.core.base import Objective from pyomo.core.base.label import NumericLabeler +from pyomo.core.staleflag import StaleFlagManager from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo, AMPLRepn from pyomo.solver.base import SolverBase, SymbolMap from pyomo.solver.config import SolverConfig @@ -83,9 +84,6 @@ def __init__( self.log_level = self.declare( 'log_level', ConfigValue(domain=NonNegativeInt, default=logging.INFO) ) - self.presolve: bool = self.declare( - 'presolve', ConfigValue(domain=bool, default=True) - ) class ipoptResults(Results): @@ -208,6 +206,10 @@ def version(self): version = tuple(int(i) for i in version.split('.')) return version + @property + def writer(self): + return self._writer + @property def config(self): return self._config @@ -269,14 +271,14 @@ def solve(self, model, **kwds): raise ipoptSolverError( f'Solver {self.__class__} is not available ({avail}).' ) + StaleFlagManager.mark_all_as_stale() # Update configuration options, based on keywords passed to solve config: ipoptConfig = self.config(kwds.pop('options', {})) config.set_value(kwds) - self._writer.config.linear_presolve = config.presolve if config.threads: logger.log( logging.WARNING, - msg=f"The `threads` option was specified, but this has not yet been implemented for {self.__class__}.", + msg=f"The `threads` option was specified, but but is not used by {self.__class__}.", ) results = ipoptResults() with TempfileManager.new_context() as tempfile: From 8e168cfc3d6a55460f3a92f779d0340f649c7c6e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 15 Jan 2024 15:21:44 -0700 Subject: [PATCH 0351/3044] MOVE: Shift pyomo.solver to pyomo.contrib.solver --- pyomo/contrib/appsi/fbbt.py | 2 +- pyomo/contrib/appsi/plugins.py | 2 +- pyomo/contrib/appsi/solvers/cbc.py | 8 ++++---- pyomo/contrib/appsi/solvers/cplex.py | 8 ++++---- pyomo/contrib/appsi/solvers/gurobi.py | 10 +++++----- pyomo/contrib/appsi/solvers/highs.py | 10 +++++----- pyomo/contrib/appsi/solvers/ipopt.py | 8 ++++---- .../appsi/solvers/tests/test_gurobi_persistent.py | 2 +- .../appsi/solvers/tests/test_persistent_solvers.py | 4 ++-- .../appsi/solvers/tests/test_wntr_persistent.py | 2 +- pyomo/contrib/appsi/solvers/wntr.py | 10 +++++----- pyomo/contrib/appsi/writers/lp_writer.py | 2 +- pyomo/contrib/appsi/writers/nl_writer.py | 2 +- pyomo/{ => contrib}/solver/__init__.py | 0 pyomo/{ => contrib}/solver/base.py | 6 +++--- pyomo/{ => contrib}/solver/config.py | 0 pyomo/{ => contrib}/solver/factory.py | 2 +- pyomo/{ => contrib}/solver/ipopt.py | 12 ++++++------ pyomo/{ => contrib}/solver/plugins.py | 0 pyomo/{ => contrib}/solver/results.py | 2 +- pyomo/{ => contrib}/solver/solution.py | 0 pyomo/{ => contrib}/solver/tests/__init__.py | 0 .../{ => contrib}/solver/tests/solvers/test_ipopt.py | 4 ++-- pyomo/{ => contrib}/solver/tests/unit/test_base.py | 0 pyomo/{ => contrib}/solver/tests/unit/test_config.py | 2 +- .../{ => contrib}/solver/tests/unit/test_results.py | 0 .../{ => contrib}/solver/tests/unit/test_solution.py | 0 pyomo/{ => contrib}/solver/tests/unit/test_util.py | 2 +- pyomo/{ => contrib}/solver/util.py | 4 ++-- pyomo/environ/__init__.py | 1 - 30 files changed, 52 insertions(+), 53 deletions(-) rename pyomo/{ => contrib}/solver/__init__.py (100%) rename pyomo/{ => contrib}/solver/base.py (99%) rename pyomo/{ => contrib}/solver/config.py (100%) rename pyomo/{ => contrib}/solver/factory.py (94%) rename pyomo/{ => contrib}/solver/ipopt.py (97%) rename pyomo/{ => contrib}/solver/plugins.py (100%) rename pyomo/{ => contrib}/solver/results.py (99%) rename pyomo/{ => contrib}/solver/solution.py (100%) rename pyomo/{ => contrib}/solver/tests/__init__.py (100%) rename pyomo/{ => contrib}/solver/tests/solvers/test_ipopt.py (94%) rename pyomo/{ => contrib}/solver/tests/unit/test_base.py (100%) rename pyomo/{ => contrib}/solver/tests/unit/test_config.py (96%) rename pyomo/{ => contrib}/solver/tests/unit/test_results.py (100%) rename pyomo/{ => contrib}/solver/tests/unit/test_solution.py (100%) rename pyomo/{ => contrib}/solver/tests/unit/test_util.py (97%) rename pyomo/{ => contrib}/solver/util.py (99%) diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index cff1085de0d..ccbb3819554 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -1,4 +1,4 @@ -from pyomo.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.util import PersistentSolverUtils from pyomo.common.config import ( ConfigDict, ConfigValue, diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index 3a132b74395..ebccba09ab2 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,5 +1,5 @@ from pyomo.common.extensions import ExtensionBuilderFactory -from pyomo.solver.factory import SolverFactory +from pyomo.contrib.solver.factory import SolverFactory from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs from .build import AppsiBuilder diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 62404890d0b..141c6de57bd 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -22,10 +22,10 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import SolverConfig -from pyomo.solver.results import TerminationCondition, Results -from pyomo.solver.solution import PersistentSolutionLoader +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.config import SolverConfig +from pyomo.contrib.solver.results import TerminationCondition, Results +from pyomo.contrib.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 1837b5690a0..6f02ac12eb1 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -19,10 +19,10 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import BranchAndBoundConfig -from pyomo.solver.results import TerminationCondition, Results -from pyomo.solver.solution import PersistentSolutionLoader +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.config import BranchAndBoundConfig +from pyomo.contrib.solver.results import TerminationCondition, Results +from pyomo.contrib.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 99fa19820a5..a947c8d7d7d 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -22,11 +22,11 @@ from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import BranchAndBoundConfig -from pyomo.solver.results import TerminationCondition, Results -from pyomo.solver.solution import PersistentSolutionLoader -from pyomo.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.config import BranchAndBoundConfig +from pyomo.contrib.solver.results import TerminationCondition, Results +from pyomo.contrib.solver.solution import PersistentSolutionLoader +from pyomo.contrib.solver.util import PersistentSolverUtils logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index b270e4f2700..1680831471c 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -20,11 +20,11 @@ from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression from pyomo.common.dependencies import numpy as np from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import BranchAndBoundConfig -from pyomo.solver.results import TerminationCondition, Results -from pyomo.solver.solution import PersistentSolutionLoader -from pyomo.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.config import BranchAndBoundConfig +from pyomo.contrib.solver.results import TerminationCondition, Results +from pyomo.contrib.solver.solution import PersistentSolutionLoader +from pyomo.contrib.solver.util import PersistentSolverUtils logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 569bb98457f..ec59b827192 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -26,10 +26,10 @@ from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.config import SolverConfig -from pyomo.solver.results import TerminationCondition, Results -from pyomo.solver.solution import PersistentSolutionLoader +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.config import SolverConfig +from pyomo.contrib.solver.results import TerminationCondition, Results +from pyomo.contrib.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index c1825879dbe..4619a1c5452 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -1,7 +1,7 @@ from pyomo.common import unittest import pyomo.environ as pe from pyomo.contrib.appsi.solvers.gurobi import Gurobi -from pyomo.solver.results import TerminationCondition +from pyomo.contrib.solver.results import TerminationCondition from pyomo.core.expr.taylor_series import taylor_series_expansion diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index b50a072abbd..6731eb645fa 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -4,8 +4,8 @@ parameterized, param_available = attempt_import('parameterized') parameterized = parameterized.parameterized -from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.results import TerminationCondition, Results +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.results import TerminationCondition, Results from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Highs from typing import Type diff --git a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py index 971305001a9..e09865294eb 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py @@ -1,6 +1,6 @@ import pyomo.environ as pe import pyomo.common.unittest as unittest -from pyomo.solver.results import TerminationCondition, SolutionStatus +from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus from pyomo.contrib.appsi.solvers.wntr import Wntr, wntr_available import math diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index aaa130f8631..04f54530c1b 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -1,8 +1,8 @@ -from pyomo.solver.base import PersistentSolverBase -from pyomo.solver.util import PersistentSolverUtils -from pyomo.solver.config import SolverConfig -from pyomo.solver.results import Results, TerminationCondition, SolutionStatus -from pyomo.solver.solution import PersistentSolutionLoader +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.config import SolverConfig +from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus +from pyomo.contrib.solver.solution import PersistentSolutionLoader from pyomo.core.expr.numeric_expr import ( ProductExpression, DivisionExpression, diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 8deb92640c1..9d0b71fe794 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -8,7 +8,7 @@ from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.timing import HierarchicalTimer from pyomo.core.kernel.objective import minimize -from pyomo.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.util import PersistentSolverUtils from .config import WriterConfig from ..cmodel import cmodel, cmodel_available diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 1be657ba762..a9b44e63f36 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -13,7 +13,7 @@ from pyomo.core.kernel.objective import minimize from pyomo.common.collections import OrderedSet from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env -from pyomo.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.util import PersistentSolverUtils from .config import WriterConfig from ..cmodel import cmodel, cmodel_available diff --git a/pyomo/solver/__init__.py b/pyomo/contrib/solver/__init__.py similarity index 100% rename from pyomo/solver/__init__.py rename to pyomo/contrib/solver/__init__.py diff --git a/pyomo/solver/base.py b/pyomo/contrib/solver/base.py similarity index 99% rename from pyomo/solver/base.py rename to pyomo/contrib/solver/base.py index d7f4adabf56..69ad921b182 100644 --- a/pyomo/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -26,9 +26,9 @@ from pyomo.core.kernel.objective import minimize from pyomo.core.base import SymbolMap from pyomo.core.staleflag import StaleFlagManager -from pyomo.solver.config import UpdateConfig -from pyomo.solver.util import get_objective -from pyomo.solver.results import ( +from pyomo.contrib.solver.config import UpdateConfig +from pyomo.contrib.solver.util import get_objective +from pyomo.contrib.solver.results import ( Results, legacy_solver_status_map, legacy_termination_condition_map, diff --git a/pyomo/solver/config.py b/pyomo/contrib/solver/config.py similarity index 100% rename from pyomo/solver/config.py rename to pyomo/contrib/solver/config.py diff --git a/pyomo/solver/factory.py b/pyomo/contrib/solver/factory.py similarity index 94% rename from pyomo/solver/factory.py rename to pyomo/contrib/solver/factory.py index 23a66acd9cb..fa3e2611667 100644 --- a/pyomo/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -12,7 +12,7 @@ from pyomo.opt.base import SolverFactory as LegacySolverFactory from pyomo.common.factory import Factory -from pyomo.solver.base import LegacySolverInterface +from pyomo.contrib.solver.base import LegacySolverInterface class SolverFactoryClass(Factory): diff --git a/pyomo/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py similarity index 97% rename from pyomo/solver/ipopt.py rename to pyomo/contrib/solver/ipopt.py index 51f48ec4881..cb70938a074 100644 --- a/pyomo/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -24,16 +24,16 @@ from pyomo.core.base.label import NumericLabeler from pyomo.core.staleflag import StaleFlagManager from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo, AMPLRepn -from pyomo.solver.base import SolverBase, SymbolMap -from pyomo.solver.config import SolverConfig -from pyomo.solver.factory import SolverFactory -from pyomo.solver.results import ( +from pyomo.contrib.solver.base import SolverBase, SymbolMap +from pyomo.contrib.solver.config import SolverConfig +from pyomo.contrib.solver.factory import SolverFactory +from pyomo.contrib.solver.results import ( Results, TerminationCondition, SolutionStatus, parse_sol_file, ) -from pyomo.solver.solution import SolutionLoaderBase, SolutionLoader +from pyomo.contrib.solver.solution import SolutionLoaderBase, SolutionLoader from pyomo.common.tee import TeeStream from pyomo.common.log import LogStream from pyomo.core.expr.visitor import replace_expressions @@ -278,7 +278,7 @@ def solve(self, model, **kwds): if config.threads: logger.log( logging.WARNING, - msg=f"The `threads` option was specified, but but is not used by {self.__class__}.", + msg=f"The `threads` option was specified, but this is not used by {self.__class__}.", ) results = ipoptResults() with TempfileManager.new_context() as tempfile: diff --git a/pyomo/solver/plugins.py b/pyomo/contrib/solver/plugins.py similarity index 100% rename from pyomo/solver/plugins.py rename to pyomo/contrib/solver/plugins.py diff --git a/pyomo/solver/results.py b/pyomo/contrib/solver/results.py similarity index 99% rename from pyomo/solver/results.py rename to pyomo/contrib/solver/results.py index e99db52073b..c24053e6358 100644 --- a/pyomo/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -31,7 +31,7 @@ TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus, ) -from pyomo.solver.solution import SolutionLoaderBase +from pyomo.contrib.solver.solution import SolutionLoaderBase from pyomo.repn.plugins.nl_writer import NLWriterInfo diff --git a/pyomo/solver/solution.py b/pyomo/contrib/solver/solution.py similarity index 100% rename from pyomo/solver/solution.py rename to pyomo/contrib/solver/solution.py diff --git a/pyomo/solver/tests/__init__.py b/pyomo/contrib/solver/tests/__init__.py similarity index 100% rename from pyomo/solver/tests/__init__.py rename to pyomo/contrib/solver/tests/__init__.py diff --git a/pyomo/solver/tests/solvers/test_ipopt.py b/pyomo/contrib/solver/tests/solvers/test_ipopt.py similarity index 94% rename from pyomo/solver/tests/solvers/test_ipopt.py rename to pyomo/contrib/solver/tests/solvers/test_ipopt.py index d9fccbb84fc..c1aecba05fc 100644 --- a/pyomo/solver/tests/solvers/test_ipopt.py +++ b/pyomo/contrib/solver/tests/solvers/test_ipopt.py @@ -13,8 +13,8 @@ import pyomo.environ as pyo from pyomo.common.fileutils import ExecutableData from pyomo.common.config import ConfigDict -from pyomo.solver.ipopt import ipoptConfig -from pyomo.solver.factory import SolverFactory +from pyomo.contrib.solver.ipopt import ipoptConfig +from pyomo.contrib.solver.factory import SolverFactory from pyomo.common import unittest diff --git a/pyomo/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py similarity index 100% rename from pyomo/solver/tests/unit/test_base.py rename to pyomo/contrib/solver/tests/unit/test_base.py diff --git a/pyomo/solver/tests/unit/test_config.py b/pyomo/contrib/solver/tests/unit/test_config.py similarity index 96% rename from pyomo/solver/tests/unit/test_config.py rename to pyomo/contrib/solver/tests/unit/test_config.py index c705c7cb8ac..1051825f4e5 100644 --- a/pyomo/solver/tests/unit/test_config.py +++ b/pyomo/contrib/solver/tests/unit/test_config.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.common import unittest -from pyomo.solver.config import SolverConfig, BranchAndBoundConfig +from pyomo.contrib.solver.config import SolverConfig, BranchAndBoundConfig class TestSolverConfig(unittest.TestCase): diff --git a/pyomo/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py similarity index 100% rename from pyomo/solver/tests/unit/test_results.py rename to pyomo/contrib/solver/tests/unit/test_results.py diff --git a/pyomo/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py similarity index 100% rename from pyomo/solver/tests/unit/test_solution.py rename to pyomo/contrib/solver/tests/unit/test_solution.py diff --git a/pyomo/solver/tests/unit/test_util.py b/pyomo/contrib/solver/tests/unit/test_util.py similarity index 97% rename from pyomo/solver/tests/unit/test_util.py rename to pyomo/contrib/solver/tests/unit/test_util.py index 737a271d603..9bf92af72cf 100644 --- a/pyomo/solver/tests/unit/test_util.py +++ b/pyomo/contrib/solver/tests/unit/test_util.py @@ -11,7 +11,7 @@ from pyomo.common import unittest import pyomo.environ as pyo -from pyomo.solver.util import collect_vars_and_named_exprs, get_objective +from pyomo.contrib.solver.util import collect_vars_and_named_exprs, get_objective from typing import Callable from pyomo.common.gsl import find_GSL diff --git a/pyomo/solver/util.py b/pyomo/contrib/solver/util.py similarity index 99% rename from pyomo/solver/util.py rename to pyomo/contrib/solver/util.py index c0c99a00747..9f0c607a0db 100644 --- a/pyomo/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -22,8 +22,8 @@ from pyomo.common.collections import ComponentMap from pyomo.common.timing import HierarchicalTimer from pyomo.core.expr.numvalue import NumericConstant -from pyomo.solver.config import UpdateConfig -from pyomo.solver.results import TerminationCondition, SolutionStatus +from pyomo.contrib.solver.config import UpdateConfig +from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus def get_objective(block): diff --git a/pyomo/environ/__init__.py b/pyomo/environ/__init__.py index 2cd562edb2b..51c68449247 100644 --- a/pyomo/environ/__init__.py +++ b/pyomo/environ/__init__.py @@ -30,7 +30,6 @@ def _do_import(pkg_name): 'pyomo.repn', 'pyomo.neos', 'pyomo.solvers', - 'pyomo.solver', 'pyomo.gdp', 'pyomo.mpec', 'pyomo.dae', From aa28193717ea2f93b920fe8e9104832a98f079b4 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 15 Jan 2024 15:31:42 -0700 Subject: [PATCH 0352/3044] Missed several imports --- pyomo/contrib/appsi/examples/getting_started.py | 2 +- pyomo/contrib/solver/tests/unit/test_base.py | 2 +- pyomo/contrib/solver/tests/unit/test_results.py | 4 ++-- pyomo/contrib/solver/tests/unit/test_solution.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index 52f4992b37b..15c3fcb2058 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -1,7 +1,7 @@ import pyomo.environ as pe from pyomo.contrib import appsi from pyomo.common.timing import HierarchicalTimer -from pyomo.solver import results +from pyomo.contrib.solver import results def main(plot=True, n_points=200): diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index b501f8d3dd3..71690b7aa0e 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.common import unittest -from pyomo.solver import base +from pyomo.contrib.solver import base class TestSolverBase(unittest.TestCase): diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 0c0b4bb18db..e7d02751f7d 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -11,8 +11,8 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict -from pyomo.solver import results -from pyomo.solver import solution +from pyomo.contrib.solver import results +from pyomo.contrib.solver import solution import pyomo.environ as pyo from pyomo.core.base.var import ScalarVar diff --git a/pyomo/contrib/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py index f4c33a60c84..dc53f1e4543 100644 --- a/pyomo/contrib/solver/tests/unit/test_solution.py +++ b/pyomo/contrib/solver/tests/unit/test_solution.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.common import unittest -from pyomo.solver import solution +from pyomo.contrib.solver import solution class TestPersistentSolverBase(unittest.TestCase): From f0d9685b006d7ab40cceeeaaf2a118e778add56d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 15 Jan 2024 15:52:34 -0700 Subject: [PATCH 0353/3044] Missing init files --- pyomo/contrib/solver/tests/solvers/__init__.py | 11 +++++++++++ pyomo/contrib/solver/tests/unit/__init__.py | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 pyomo/contrib/solver/tests/solvers/__init__.py create mode 100644 pyomo/contrib/solver/tests/unit/__init__.py diff --git a/pyomo/contrib/solver/tests/solvers/__init__.py b/pyomo/contrib/solver/tests/solvers/__init__.py new file mode 100644 index 00000000000..9320e403e95 --- /dev/null +++ b/pyomo/contrib/solver/tests/solvers/__init__.py @@ -0,0 +1,11 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + diff --git a/pyomo/contrib/solver/tests/unit/__init__.py b/pyomo/contrib/solver/tests/unit/__init__.py new file mode 100644 index 00000000000..9320e403e95 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/__init__.py @@ -0,0 +1,11 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + From 7a1a0d50c7c83204faf343d6524eaede0fad8e1f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 15 Jan 2024 16:26:09 -0700 Subject: [PATCH 0354/3044] Black --- pyomo/contrib/solver/tests/solvers/__init__.py | 1 - pyomo/contrib/solver/tests/unit/__init__.py | 1 - 2 files changed, 2 deletions(-) diff --git a/pyomo/contrib/solver/tests/solvers/__init__.py b/pyomo/contrib/solver/tests/solvers/__init__.py index 9320e403e95..d93cfd77b3c 100644 --- a/pyomo/contrib/solver/tests/solvers/__init__.py +++ b/pyomo/contrib/solver/tests/solvers/__init__.py @@ -8,4 +8,3 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - diff --git a/pyomo/contrib/solver/tests/unit/__init__.py b/pyomo/contrib/solver/tests/unit/__init__.py index 9320e403e95..d93cfd77b3c 100644 --- a/pyomo/contrib/solver/tests/unit/__init__.py +++ b/pyomo/contrib/solver/tests/unit/__init__.py @@ -8,4 +8,3 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - From 4087d1adb3cfea55c6e85547ce0697b5b860601d Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 15 Jan 2024 23:49:20 -0700 Subject: [PATCH 0355/3044] solver refactor: various updates --- pyomo/contrib/solver/base.py | 61 ++++++++++---------------------- pyomo/contrib/solver/config.py | 15 ++++---- pyomo/contrib/solver/ipopt.py | 57 +++++++++++++---------------- pyomo/contrib/solver/results.py | 21 +++-------- pyomo/contrib/solver/solution.py | 50 ++------------------------ pyomo/repn/plugins/nl_writer.py | 2 +- 6 files changed, 58 insertions(+), 148 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 69ad921b182..961187179f2 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -14,6 +14,8 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple import os +from .config import SolverConfig + from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData @@ -49,6 +51,11 @@ class SolverBase(abc.ABC): - is_persistent: Set to false for all direct solvers. """ + CONFIG = SolverConfig() + + def __init__(self, **kwds) -> None: + self.config = self.CONFIG(value=kwds) + # # Support "with" statements. Forgetting to call deactivate # on Plugins is a common source of memory leaks @@ -146,19 +153,6 @@ def version(self) -> Tuple: A tuple representing the version """ - @property - @abc.abstractmethod - def config(self): - """ - An object for configuring solve options. - - Returns - ------- - SolverConfig - An object for configuring pyomo solve options such as the time limit. - These options are mostly independent of the solver. - """ - def is_persistent(self): """ Returns @@ -187,7 +181,7 @@ def is_persistent(self): """ return True - def load_vars( + def _load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> NoReturn: """ @@ -199,12 +193,12 @@ def load_vars( A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution to all primal variables will be loaded. """ - for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + for v, val in self._get_primals(vars_to_load=vars_to_load).items(): v.set_value(val, skip_validation=True) StaleFlagManager.mark_all_as_stale(delayed=True) @abc.abstractmethod - def get_primals( + def _get_primals( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: """ @@ -224,7 +218,7 @@ def get_primals( '{0} does not support the get_primals method'.format(type(self)) ) - def get_duals( + def _get_duals( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None ) -> Dict[_GeneralConstraintData, float]: """ @@ -245,26 +239,7 @@ def get_duals( '{0} does not support the get_duals method'.format(type(self)) ) - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Parameters - ---------- - cons_to_load: list - A list of the constraints whose slacks should be loaded. If cons_to_load is None, then the slacks for all - constraints will be loaded. - - Returns - ------- - slacks: dict - Maps constraints to slack values - """ - raise NotImplementedError( - '{0} does not support the get_slacks method'.format(type(self)) - ) - - def get_reduced_costs( + def _get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: """ @@ -296,6 +271,12 @@ def set_instance(self, model): Set an instance of the model """ + @abc.abstractmethod + def set_objective(self, obj: _GeneralObjectiveData): + """ + Set current objective for the model + """ + @abc.abstractmethod def add_variables(self, variables: List[_GeneralVarData]): """ @@ -344,12 +325,6 @@ def remove_block(self, block: _BlockData): Remove a block from the model """ - @abc.abstractmethod - def set_objective(self, obj: _GeneralObjectiveData): - """ - Set current objective for the model - """ - @abc.abstractmethod def update_variables(self, variables: List[_GeneralVarData]): """ diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index ef0114ba439..738338d3718 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -16,7 +16,9 @@ ConfigValue, NonNegativeFloat, NonNegativeInt, + ADVANCED_OPTION, ) +from pyomo.common.timing import HierarchicalTimer class SolverConfig(ConfigDict): @@ -72,12 +74,11 @@ def __init__( description="If True, the names given to the solver will reflect the names of the Pyomo components. Cannot be changed after set_instance is called.", ), ) - self.report_timing: bool = self.declare( - 'report_timing', + self.timer: HierarchicalTimer = self.declare( + 'timer', ConfigValue( - domain=bool, - default=False, - description="If True, timing information will be printed at the end of a solve call.", + default=None, + description="A HierarchicalTimer.", ), ) self.threads: Optional[int] = self.declare( @@ -133,9 +134,6 @@ def __init__( self.abs_gap: Optional[float] = self.declare( 'abs_gap', ConfigValue(domain=NonNegativeFloat) ) - self.relax_integrality: bool = self.declare( - 'relax_integrality', ConfigValue(domain=bool, default=False) - ) class UpdateConfig(ConfigDict): @@ -283,6 +281,7 @@ def __init__( ConfigValue( domain=bool, default=True, + visibility=ADVANCED_OPTION, doc=""" This is an advanced option that should only be used in special circumstances. With the default setting of True, fixed variables will be treated like parameters. diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index cb70938a074..475ce6e6f0b 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -20,6 +20,7 @@ from pyomo.common.config import ConfigValue, NonNegativeInt, NonNegativeFloat from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager +from pyomo.common.timing import HierarchicalTimer from pyomo.core.base import Objective from pyomo.core.base.label import NumericLabeler from pyomo.core.staleflag import StaleFlagManager @@ -84,6 +85,10 @@ def __init__( self.log_level = self.declare( 'log_level', ConfigValue(domain=NonNegativeInt, default=logging.INFO) ) + self.writer_config = self.declare( + 'writer_config', + ConfigValue(default=NLWriter.CONFIG()) + ) class ipoptResults(Results): @@ -183,10 +188,8 @@ class ipopt(SolverBase): CONFIG = ipoptConfig() def __init__(self, **kwds): - self._config = self.CONFIG(kwds) + super().__init__(**kwds) self._writer = NLWriter() - self._writer.config.skip_trivial_constraints = True - self._solver_options = self._config.solver_options def available(self): if self.config.executable.path() is None: @@ -206,26 +209,6 @@ def version(self): version = tuple(int(i) for i in version.split('.')) return version - @property - def writer(self): - return self._writer - - @property - def config(self): - return self._config - - @config.setter - def config(self, val): - self._config = val - - @property - def solver_options(self): - return self._solver_options - - @solver_options.setter - def solver_options(self, val: Dict): - self._solver_options = val - @property def symbol_map(self): return self._symbol_map @@ -250,14 +233,14 @@ def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: boo cmd = [str(config.executable), basename + '.nl', '-AMPL'] if opt_file: cmd.append('option_file_name=' + basename + '.opt') - if 'option_file_name' in self.solver_options: + if 'option_file_name' in config.solver_options: raise ValueError( 'Pyomo generates the ipopt options file as part of the solve method. ' 'Add all options to ipopt.config.solver_options instead.' ) - if config.time_limit is not None and 'max_cpu_time' not in self.solver_options: - self.solver_options['max_cpu_time'] = config.time_limit - for k, val in self.solver_options.items(): + if config.time_limit is not None and 'max_cpu_time' not in config.solver_options: + config.solver_options['max_cpu_time'] = config.time_limit + for k, val in config.solver_options.items(): if k in ipopt_command_line_options: cmd.append(str(k) + '=' + str(val)) return cmd @@ -271,15 +254,18 @@ def solve(self, model, **kwds): raise ipoptSolverError( f'Solver {self.__class__} is not available ({avail}).' ) - StaleFlagManager.mark_all_as_stale() # Update configuration options, based on keywords passed to solve - config: ipoptConfig = self.config(kwds.pop('options', {})) - config.set_value(kwds) + config: ipoptConfig = self.config(value=kwds) if config.threads: logger.log( logging.WARNING, msg=f"The `threads` option was specified, but this is not used by {self.__class__}.", ) + if config.timer is None: + timer = HierarchicalTimer() + else: + timer = config.timer + StaleFlagManager.mark_all_as_stale() results = ipoptResults() with TempfileManager.new_context() as tempfile: if config.temp_dir is None: @@ -296,6 +282,8 @@ def solve(self, model, **kwds): with open(basename + '.nl', 'w') as nl_file, open( basename + '.row', 'w' ) as row_file, open(basename + '.col', 'w') as col_file: + timer.start('write_nl_file') + self._writer.config.set_value(config.writer_config) nl_info = self._writer.write( model, nl_file, @@ -303,6 +291,7 @@ def solve(self, model, **kwds): col_file, symbolic_solver_labels=config.symbolic_solver_labels, ) + timer.stop('write_nl_file') # Get a copy of the environment to pass to the subprocess env = os.environ.copy() if nl_info.external_function_libraries: @@ -318,7 +307,7 @@ def solve(self, model, **kwds): # Write the opt_file, if there should be one; return a bool to say # whether or not we have one (so we can correctly build the command line) opt_file = self._write_options_file( - filename=basename, options=self.solver_options + filename=basename, options=config.solver_options ) # Call ipopt - passing the files via the subprocess cmd = self._create_command_line( @@ -343,6 +332,7 @@ def solve(self, model, **kwds): ) ) with TeeStream(*ostreams) as t: + timer.start('subprocess') process = subprocess.run( cmd, timeout=timeout, @@ -351,6 +341,7 @@ def solve(self, model, **kwds): stdout=t.STDOUT, stderr=t.STDERR, ) + timer.stop('subprocess') # This is the stuff we need to parse to get the iterations # and time iters, ipopt_time_nofunc, ipopt_time_func = self._parse_ipopt_output( @@ -362,7 +353,9 @@ def solve(self, model, **kwds): results.solution_loader = SolutionLoader(None, None, None, None) else: with open(basename + '.sol', 'r') as sol_file: + timer.start('parse_sol') results = self._parse_solution(sol_file, nl_info, results) + timer.stop('parse_sol') results.iteration_count = iters results.timing_info.no_function_solve_time = ipopt_time_nofunc results.timing_info.function_solve_time = ipopt_time_func @@ -427,8 +420,6 @@ def solve(self, model, **kwds): results.timing_info.wall_time = ( end_timestamp - start_timestamp ).total_seconds() - if config.report_timing: - results.report_timing() return results def _parse_ipopt_output(self, stream: io.StringIO): diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index c24053e6358..2f839580a43 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ import enum -from typing import Optional, Tuple, Dict, Any, Sequence, List +from typing import Optional, Tuple, Dict, Any, Sequence, List, Type from datetime import datetime import io @@ -21,6 +21,7 @@ NonNegativeInt, In, NonNegativeFloat, + ADVANCED_OPTION, ) from pyomo.common.errors import PyomoException from pyomo.core.base.var import _GeneralVarData @@ -224,7 +225,7 @@ def __init__( self.iteration_count: Optional[int] = self.declare( 'iteration_count', ConfigValue(domain=NonNegativeInt, default=None) ) - self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict()) + self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict(implicit=True)) self.timing_info.start_timestamp: datetime = self.timing_info.declare( 'start_timestamp', ConfigValue(domain=Datetime) @@ -235,20 +236,8 @@ def __init__( self.extra_info: ConfigDict = self.declare( 'extra_info', ConfigDict(implicit=True) ) - - def __str__(self): - s = '' - s += 'termination_condition: ' + str(self.termination_condition) + '\n' - s += 'solution_status: ' + str(self.solution_status) + '\n' - s += 'incumbent_objective: ' + str(self.incumbent_objective) + '\n' - s += 'objective_bound: ' + str(self.objective_bound) - return s - - def report_timing(self): - print('Timing Information: ') - print('-' * 50) - self.timing_info.display() - print('-' * 50) + self.solver_configuration: ConfigDict = self.declare('solver_configuration', ConfigDict(doc="A copy of the config object used in the solve", visibility=ADVANCED_OPTION)) + self.solver_log: str = self.declare('solver_log', ConfigValue(domain=str, default=None, visibility=ADVANCED_OPTION)) class ResultsReader: diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 068677ea580..4ec3f98cd08 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -95,27 +95,6 @@ def get_duals( """ raise NotImplementedError(f'{type(self)} does not support the get_duals method') - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - """ - Returns a dictionary mapping constraint to slack. - - Parameters - ---------- - cons_to_load: list - A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all - constraints will be loaded. - - Returns - ------- - slacks: dict - Maps constraints to slacks - """ - raise NotImplementedError( - f'{type(self)} does not support the get_slacks method' - ) - def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: @@ -198,23 +177,6 @@ def get_duals( duals[c] = self._duals[c] return duals - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - if self._slacks is None: - raise RuntimeError( - 'Solution loader does not currently have valid slacks. Please ' - 'check the termination condition and ensure the solver returns slacks ' - 'for the given problem type.' - ) - if cons_to_load is None: - slacks = dict(self._slacks) - else: - slacks = {} - for c in cons_to_load: - slacks[c] = self._slacks[c] - return slacks - def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: @@ -244,25 +206,19 @@ def _assert_solution_still_valid(self): def get_primals(self, vars_to_load=None): self._assert_solution_still_valid() - return self._solver.get_primals(vars_to_load=vars_to_load) + return self._solver._get_primals(vars_to_load=vars_to_load) def get_duals( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None ) -> Dict[_GeneralConstraintData, float]: self._assert_solution_still_valid() - return self._solver.get_duals(cons_to_load=cons_to_load) - - def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - self._assert_solution_still_valid() - return self._solver.get_slacks(cons_to_load=cons_to_load) + return self._solver._get_duals(cons_to_load=cons_to_load) def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: self._assert_solution_still_valid() - return self._solver.get_reduced_costs(vars_to_load=vars_to_load) + return self._solver._get_reduced_costs(vars_to_load=vars_to_load) def invalidate(self): self._valid = False diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 187d3176bb7..3b94963e858 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -214,7 +214,7 @@ class NLWriter(object): CONFIG.declare( 'skip_trivial_constraints', ConfigValue( - default=False, + default=True, domain=bool, description='Skip writing constraints whose body is constant', ), From e5c46edc0dbee83b2e75569ec0105cd750fe1e0e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 00:00:18 -0700 Subject: [PATCH 0356/3044] solver refactor: various updates --- pyomo/contrib/solver/results.py | 90 +++++++++++++++------------------ 1 file changed, 41 insertions(+), 49 deletions(-) diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index 2f839580a43..c7edc8c2f2e 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -257,10 +257,8 @@ def __init__(self) -> None: def parse_sol_file( sol_file: io.TextIOBase, nl_info: NLWriterInfo, - suffixes_to_read: Sequence[str], result: Results, ) -> Tuple[Results, SolFileData]: - suffixes_to_read = set(suffixes_to_read) sol_data = SolFileData() # @@ -368,9 +366,8 @@ def parse_sol_file( if result.solution_status != SolutionStatus.noSolution: for v, val in zip(nl_info.variables, variable_vals): sol_data.primals[id(v)] = (v, val) - if "dual" in suffixes_to_read: - for c, val in zip(nl_info.constraints, duals): - sol_data.duals[c] = val + for c, val in zip(nl_info.constraints, duals): + sol_data.duals[c] = val ### Read suffixes ### line = sol_file.readline() while line: @@ -400,51 +397,46 @@ def parse_sol_file( # tablen = int(line[4]) tabline = int(line[5]) suffix_name = sol_file.readline().strip() - if suffix_name in suffixes_to_read: - # ignore translation of the table number to string value for now, - # this information can be obtained from the solver documentation - for n in range(tabline): - sol_file.readline() - if kind == 0: # Var - sol_data.var_suffixes[suffix_name] = dict() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - var_ndx = int(suf_line[0]) - var = nl_info.variables[var_ndx] - sol_data.var_suffixes[suffix_name][id(var)] = ( - var, - convert_function(suf_line[1]), - ) - elif kind == 1: # Con - sol_data.con_suffixes[suffix_name] = dict() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - con_ndx = int(suf_line[0]) - con = nl_info.constraints[con_ndx] - sol_data.con_suffixes[suffix_name][con] = convert_function( - suf_line[1] - ) - elif kind == 2: # Obj - sol_data.obj_suffixes[suffix_name] = dict() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - obj_ndx = int(suf_line[0]) - obj = nl_info.objectives[obj_ndx] - sol_data.obj_suffixes[suffix_name][id(obj)] = ( - obj, - convert_function(suf_line[1]), - ) - elif kind == 3: # Prob - sol_data.problem_suffixes[suffix_name] = list() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - sol_data.problem_suffixes[suffix_name].append( - convert_function(suf_line[1]) - ) - else: - # do not store the suffix in the solution object + # ignore translation of the table number to string value for now, + # this information can be obtained from the solver documentation + for n in range(tabline): + sol_file.readline() + if kind == 0: # Var + sol_data.var_suffixes[suffix_name] = dict() for cnt in range(nvalues): - sol_file.readline() + suf_line = sol_file.readline().split() + var_ndx = int(suf_line[0]) + var = nl_info.variables[var_ndx] + sol_data.var_suffixes[suffix_name][id(var)] = ( + var, + convert_function(suf_line[1]), + ) + elif kind == 1: # Con + sol_data.con_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = sol_file.readline().split() + con_ndx = int(suf_line[0]) + con = nl_info.constraints[con_ndx] + sol_data.con_suffixes[suffix_name][con] = convert_function( + suf_line[1] + ) + elif kind == 2: # Obj + sol_data.obj_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = sol_file.readline().split() + obj_ndx = int(suf_line[0]) + obj = nl_info.objectives[obj_ndx] + sol_data.obj_suffixes[suffix_name][id(obj)] = ( + obj, + convert_function(suf_line[1]), + ) + elif kind == 3: # Prob + sol_data.problem_suffixes[suffix_name] = list() + for cnt in range(nvalues): + suf_line = sol_file.readline().split() + sol_data.problem_suffixes[suffix_name].append( + convert_function(suf_line[1]) + ) line = sol_file.readline() return result, sol_data From dac6bef9459e38be840428005b59ab0947ffaaa2 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 00:14:27 -0700 Subject: [PATCH 0357/3044] solver refactor: use caching in available and version --- pyomo/contrib/solver/ipopt.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 475ce6e6f0b..878fe7cb264 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -190,24 +190,31 @@ class ipopt(SolverBase): def __init__(self, **kwds): super().__init__(**kwds) self._writer = NLWriter() + self._available_cache = None + self._version_cache = None def available(self): - if self.config.executable.path() is None: - return self.Availability.NotFound - return self.Availability.FullLicense + if self._available_cache is None: + if self.config.executable.path() is None: + self._available_cache = self.Availability.NotFound + else: + self._available_cache = self.Availability.FullLicense + return self._available_cache def version(self): - results = subprocess.run( - [str(self.config.executable), '--version'], - timeout=1, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=True, - ) - version = results.stdout.splitlines()[0] - version = version.split(' ')[1].strip() - version = tuple(int(i) for i in version.split('.')) - return version + if self._version_cache is None: + results = subprocess.run( + [str(self.config.executable), '--version'], + timeout=1, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + ) + version = results.stdout.splitlines()[0] + version = version.split(' ')[1].strip() + version = tuple(int(i) for i in version.split('.')) + self._version_cache = version + return self._version_cache @property def symbol_map(self): From 4df0a8dd7f6518e12f24920d60b5e1fc10114d3f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 00:26:32 -0700 Subject: [PATCH 0358/3044] solver refactor: config updates --- pyomo/contrib/solver/base.py | 7 - pyomo/contrib/solver/config.py | 272 +++++++++++++++++++-------------- 2 files changed, 156 insertions(+), 123 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 961187179f2..216bf28ac4a 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -258,13 +258,6 @@ def _get_reduced_costs( '{0} does not support the get_reduced_costs method'.format(type(self)) ) - @property - @abc.abstractmethod - def update_config(self) -> UpdateConfig: - """ - Updates the solver config - """ - @abc.abstractmethod def set_instance(self, model): """ diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 738338d3718..0a2478d44ff 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -21,122 +21,7 @@ from pyomo.common.timing import HierarchicalTimer -class SolverConfig(ConfigDict): - """ - Base config values for all solver interfaces - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.tee: bool = self.declare( - 'tee', - ConfigValue( - domain=bool, - default=False, - description="If True, the solver log prints to stdout.", - ), - ) - self.load_solution: bool = self.declare( - 'load_solution', - ConfigValue( - domain=bool, - default=True, - description="If True, the values of the primal variables will be loaded into the model.", - ), - ) - self.raise_exception_on_nonoptimal_result: bool = self.declare( - 'raise_exception_on_nonoptimal_result', - ConfigValue( - domain=bool, - default=True, - description="If False, the `solve` method will continue processing even if the returned result is nonoptimal.", - ), - ) - self.symbolic_solver_labels: bool = self.declare( - 'symbolic_solver_labels', - ConfigValue( - domain=bool, - default=False, - description="If True, the names given to the solver will reflect the names of the Pyomo components. Cannot be changed after set_instance is called.", - ), - ) - self.timer: HierarchicalTimer = self.declare( - 'timer', - ConfigValue( - default=None, - description="A HierarchicalTimer.", - ), - ) - self.threads: Optional[int] = self.declare( - 'threads', - ConfigValue( - domain=NonNegativeInt, - description="Number of threads to be used by a solver.", - default=None, - ), - ) - self.time_limit: Optional[float] = self.declare( - 'time_limit', - ConfigValue( - domain=NonNegativeFloat, description="Time limit applied to the solver." - ), - ) - self.solver_options: ConfigDict = self.declare( - 'solver_options', - ConfigDict(implicit=True, description="Options to pass to the solver."), - ) - - -class BranchAndBoundConfig(SolverConfig): - """ - Attributes - ---------- - mip_gap: float - Solver will terminate if the mip gap is less than mip_gap - relax_integrality: bool - If True, all integer variables will be relaxed to continuous - variables before solving - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.rel_gap: Optional[float] = self.declare( - 'rel_gap', ConfigValue(domain=NonNegativeFloat) - ) - self.abs_gap: Optional[float] = self.declare( - 'abs_gap', ConfigValue(domain=NonNegativeFloat) - ) - - -class UpdateConfig(ConfigDict): +class AutoUpdateConfig(ConfigDict): """ This is necessary for persistent solvers. @@ -294,3 +179,158 @@ def __init__( updating the values of fixed variables is much faster this way.""", ), ) + + +class SolverConfig(ConfigDict): + """ + Base config values for all solver interfaces + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.tee: bool = self.declare( + 'tee', + ConfigValue( + domain=bool, + default=False, + description="If True, the solver log prints to stdout.", + ), + ) + self.load_solution: bool = self.declare( + 'load_solution', + ConfigValue( + domain=bool, + default=True, + description="If True, the values of the primal variables will be loaded into the model.", + ), + ) + self.raise_exception_on_nonoptimal_result: bool = self.declare( + 'raise_exception_on_nonoptimal_result', + ConfigValue( + domain=bool, + default=True, + description="If False, the `solve` method will continue processing even if the returned result is nonoptimal.", + ), + ) + self.symbolic_solver_labels: bool = self.declare( + 'symbolic_solver_labels', + ConfigValue( + domain=bool, + default=False, + description="If True, the names given to the solver will reflect the names of the Pyomo components. Cannot be changed after set_instance is called.", + ), + ) + self.timer: HierarchicalTimer = self.declare( + 'timer', + ConfigValue( + default=None, + description="A HierarchicalTimer.", + ), + ) + self.threads: Optional[int] = self.declare( + 'threads', + ConfigValue( + domain=NonNegativeInt, + description="Number of threads to be used by a solver.", + default=None, + ), + ) + self.time_limit: Optional[float] = self.declare( + 'time_limit', + ConfigValue( + domain=NonNegativeFloat, description="Time limit applied to the solver." + ), + ) + self.solver_options: ConfigDict = self.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + + +class BranchAndBoundConfig(SolverConfig): + """ + Attributes + ---------- + mip_gap: float + Solver will terminate if the mip gap is less than mip_gap + relax_integrality: bool + If True, all integer variables will be relaxed to continuous + variables before solving + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.rel_gap: Optional[float] = self.declare( + 'rel_gap', ConfigValue(domain=NonNegativeFloat) + ) + self.abs_gap: Optional[float] = self.declare( + 'abs_gap', ConfigValue(domain=NonNegativeFloat) + ) + + +class PersistentSolverConfig(SolverConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.auto_updats: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) + + +class PersistentBranchAndBoundConfig(BranchAndBoundConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.auto_updats: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) From b94477913d92c5c346906bf0ca297862ac40baae Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 00:27:33 -0700 Subject: [PATCH 0359/3044] solver refactor: typo --- pyomo/contrib/solver/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 0a2478d44ff..8fe627cbcc1 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -313,7 +313,7 @@ def __init__( visibility=visibility, ) - self.auto_updats: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) + self.auto_updates: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) class PersistentBranchAndBoundConfig(BranchAndBoundConfig): @@ -333,4 +333,4 @@ def __init__( visibility=visibility, ) - self.auto_updats: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) + self.auto_updates: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) From fe41e220167ac95381bd5ef40852d1180cdb466f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 00:44:33 -0700 Subject: [PATCH 0360/3044] solver refactor: various fixes --- pyomo/contrib/solver/base.py | 1 - pyomo/contrib/solver/ipopt.py | 5 +++-- pyomo/contrib/solver/results.py | 2 +- pyomo/contrib/solver/util.py | 26 ++++++++------------------ 4 files changed, 12 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 216bf28ac4a..56292859b1a 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -28,7 +28,6 @@ from pyomo.core.kernel.objective import minimize from pyomo.core.base import SymbolMap from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.solver.config import UpdateConfig from pyomo.contrib.solver.util import get_objective from pyomo.contrib.solver.results import ( Results, diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 878fe7cb264..5176291ab42 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -421,6 +421,9 @@ def solve(self, model, **kwds): remove_named_expressions=True, ) + results.solver_configuration = config + results.solver_log = ostreams[0].getvalue() + # Capture/record end-time / wall-time end_timestamp = datetime.datetime.now(datetime.timezone.utc) results.timing_info.start_timestamp = start_timestamp @@ -462,11 +465,9 @@ def _parse_ipopt_output(self, stream: io.StringIO): def _parse_solution( self, instream: io.TextIOBase, nl_info: NLWriterInfo, result: ipoptResults ): - suffixes_to_read = ['dual', 'ipopt_zL_out', 'ipopt_zU_out'] res, sol_data = parse_sol_file( sol_file=instream, nl_info=nl_info, - suffixes_to_read=suffixes_to_read, result=result, ) diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index c7edc8c2f2e..e63aa351f64 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -236,7 +236,7 @@ def __init__( self.extra_info: ConfigDict = self.declare( 'extra_info', ConfigDict(implicit=True) ) - self.solver_configuration: ConfigDict = self.declare('solver_configuration', ConfigDict(doc="A copy of the config object used in the solve", visibility=ADVANCED_OPTION)) + self.solver_configuration: ConfigDict = self.declare('solver_configuration', ConfigValue(doc="A copy of the config object used in the solve", visibility=ADVANCED_OPTION)) self.solver_log: str = self.declare('solver_log', ConfigValue(domain=str, default=None, visibility=ADVANCED_OPTION)) diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index 9f0c607a0db..727d9c354e2 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -22,7 +22,6 @@ from pyomo.common.collections import ComponentMap from pyomo.common.timing import HierarchicalTimer from pyomo.core.expr.numvalue import NumericConstant -from pyomo.contrib.solver.config import UpdateConfig from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus @@ -154,7 +153,6 @@ def __init__(self, only_child_vars=False): ) # maps constraint to list of tuples (named_expr, named_expr.expr) self._external_functions = ComponentMap() self._obj_named_expressions = [] - self._update_config = UpdateConfig() self._referenced_variables = ( {} ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] @@ -163,18 +161,10 @@ def __init__(self, only_child_vars=False): self._expr_types = None self._only_child_vars = only_child_vars - @property - def update_config(self): - return self._update_config - - @update_config.setter - def update_config(self, val: UpdateConfig): - self._update_config = val - def set_instance(self, model): - saved_update_config = self.update_config + saved_config = self.config self.__init__(only_child_vars=self._only_child_vars) - self.update_config = saved_update_config + self.config = saved_config self._model = model self.add_block(model) if self._objective is None: @@ -249,7 +239,7 @@ def add_constraints(self, cons: List[_GeneralConstraintData]): self._vars_referenced_by_con[con] = variables for v in variables: self._referenced_variables[id(v)][0][con] = None - if not self.update_config.treat_fixed_vars_as_params: + if not self.config.auto_updates.treat_fixed_vars_as_params: for v in fixed_vars: v.unfix() all_fixed_vars[id(v)] = v @@ -302,7 +292,7 @@ def set_objective(self, obj: _GeneralObjectiveData): self._vars_referenced_by_obj = variables for v in variables: self._referenced_variables[id(v)][2] = obj - if not self.update_config.treat_fixed_vars_as_params: + if not self.config.auto_updates.treat_fixed_vars_as_params: for v in fixed_vars: v.unfix() self._set_objective(obj) @@ -483,7 +473,7 @@ def update_params(self): def update(self, timer: HierarchicalTimer = None): if timer is None: timer = HierarchicalTimer() - config = self.update_config + config = self.config.auto_updates new_vars = [] old_vars = [] new_params = [] @@ -634,7 +624,7 @@ def update(self, timer: HierarchicalTimer = None): _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] if (fixed != v.fixed) or (fixed and (value != v.value)): vars_to_update.append(v) - if self.update_config.treat_fixed_vars_as_params: + if self.config.auto_updates.treat_fixed_vars_as_params: for c in self._referenced_variables[id(v)][0]: cons_to_remove_and_add[c] = None if self._referenced_variables[id(v)][2] is not None: @@ -670,13 +660,13 @@ def update(self, timer: HierarchicalTimer = None): break timer.stop('named expressions') timer.start('objective') - if self.update_config.check_for_new_objective: + if self.config.auto_updates.check_for_new_objective: pyomo_obj = get_objective(self._model) if pyomo_obj is not self._objective: need_to_set_objective = True else: pyomo_obj = self._objective - if self.update_config.update_objective: + if self.config.auto_updates.update_objective: if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: need_to_set_objective = True elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: From 3d32f4a1aa098e06d3193a3258164101599fffe0 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 01:45:57 -0700 Subject: [PATCH 0361/3044] move sol reader to separate file --- pyomo/contrib/solver/ipopt.py | 2 +- pyomo/contrib/solver/results.py | 205 +--------------------------- pyomo/contrib/solver/sol_reader.py | 206 +++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 205 deletions(-) create mode 100644 pyomo/contrib/solver/sol_reader.py diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 5176291ab42..c7a932eb883 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -32,8 +32,8 @@ Results, TerminationCondition, SolutionStatus, - parse_sol_file, ) +from .sol_reader import parse_sol_file from pyomo.contrib.solver.solution import SolutionLoaderBase, SolutionLoader from pyomo.common.tee import TeeStream from pyomo.common.log import LogStream diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index e63aa351f64..3beb3aede81 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -10,9 +10,8 @@ # ___________________________________________________________________________ import enum -from typing import Optional, Tuple, Dict, Any, Sequence, List, Type +from typing import Optional, Tuple from datetime import datetime -import io from pyomo.common.config import ( ConfigDict, @@ -24,16 +23,12 @@ ADVANCED_OPTION, ) from pyomo.common.errors import PyomoException -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _ConstraintData -from pyomo.core.base.objective import _ObjectiveData from pyomo.opt.results.solution import SolutionStatus as LegacySolutionStatus from pyomo.opt.results.solver import ( TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus, ) from pyomo.contrib.solver.solution import SolutionLoaderBase -from pyomo.repn.plugins.nl_writer import NLWriterInfo class SolverResultsError(PyomoException): @@ -244,204 +239,6 @@ class ResultsReader: pass -class SolFileData: - def __init__(self) -> None: - self.primals: Dict[int, Tuple[_GeneralVarData, float]] = dict() - self.duals: Dict[_ConstraintData, float] = dict() - self.var_suffixes: Dict[str, Dict[int, Tuple[_GeneralVarData, Any]]] = dict() - self.con_suffixes: Dict[str, Dict[_ConstraintData, Any]] = dict() - self.obj_suffixes: Dict[str, Dict[int, Tuple[_ObjectiveData, Any]]] = dict() - self.problem_suffixes: Dict[str, List[Any]] = dict() - - -def parse_sol_file( - sol_file: io.TextIOBase, - nl_info: NLWriterInfo, - result: Results, -) -> Tuple[Results, SolFileData]: - sol_data = SolFileData() - - # - # Some solvers (minto) do not write a message. We will assume - # all non-blank lines up the 'Options' line is the message. - # For backwards compatibility and general safety, we will parse all - # lines until "Options" appears. Anything before "Options" we will - # consider to be the solver message. - message = [] - for line in sol_file: - if not line: - break - line = line.strip() - if "Options" in line: - break - message.append(line) - message = '\n'.join(message) - # Once "Options" appears, we must now read the content under it. - model_objects = [] - if "Options" in line: - line = sol_file.readline() - number_of_options = int(line) - need_tolerance = False - if ( - number_of_options > 4 - ): # MRM: Entirely unclear why this is necessary, or if it even is - number_of_options -= 2 - need_tolerance = True - for i in range(number_of_options + 4): - line = sol_file.readline() - model_objects.append(int(line)) - if ( - need_tolerance - ): # MRM: Entirely unclear why this is necessary, or if it even is - line = sol_file.readline() - model_objects.append(float(line)) - else: - raise SolverResultsError("ERROR READING `sol` FILE. No 'Options' line found.") - # Identify the total number of variables and constraints - number_of_cons = model_objects[number_of_options + 1] - number_of_vars = model_objects[number_of_options + 3] - assert number_of_cons == len(nl_info.constraints) - assert number_of_vars == len(nl_info.variables) - - duals = [float(sol_file.readline()) for i in range(number_of_cons)] - variable_vals = [float(sol_file.readline()) for i in range(number_of_vars)] - - # Parse the exit code line and capture it - exit_code = [0, 0] - line = sol_file.readline() - if line and ('objno' in line): - exit_code_line = line.split() - if len(exit_code_line) != 3: - raise SolverResultsError( - f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}." - ) - exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] - else: - raise SolverResultsError( - f"ERROR READING `sol` FILE. Expected `objno`; received {line}." - ) - result.extra_info.solver_message = message.strip().replace('\n', '; ') - exit_code_message = '' - if (exit_code[1] >= 0) and (exit_code[1] <= 99): - result.solution_status = SolutionStatus.optimal - result.termination_condition = TerminationCondition.convergenceCriteriaSatisfied - elif (exit_code[1] >= 100) and (exit_code[1] <= 199): - exit_code_message = "Optimal solution indicated, but ERROR LIKELY!" - result.solution_status = SolutionStatus.feasible - result.termination_condition = TerminationCondition.error - elif (exit_code[1] >= 200) and (exit_code[1] <= 299): - exit_code_message = "INFEASIBLE SOLUTION: constraints cannot be satisfied!" - result.solution_status = SolutionStatus.infeasible - # TODO: this is solver dependent - # But this was the way in the previous version - and has been fine thus far? - result.termination_condition = TerminationCondition.locallyInfeasible - elif (exit_code[1] >= 300) and (exit_code[1] <= 399): - exit_code_message = ( - "UNBOUNDED PROBLEM: the objective can be improved without limit!" - ) - result.solution_status = SolutionStatus.noSolution - result.termination_condition = TerminationCondition.unbounded - elif (exit_code[1] >= 400) and (exit_code[1] <= 499): - exit_code_message = ( - "EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " - "was stopped by a limit that you set!" - ) - # TODO: this is solver dependent - # But this was the way in the previous version - and has been fine thus far? - result.solution_status = SolutionStatus.infeasible - result.termination_condition = TerminationCondition.iterationLimit - elif (exit_code[1] >= 500) and (exit_code[1] <= 599): - exit_code_message = ( - "FAILURE: the solver stopped by an error condition " - "in the solver routines!" - ) - result.termination_condition = TerminationCondition.error - - if result.extra_info.solver_message: - if exit_code_message: - result.extra_info.solver_message += '; ' + exit_code_message - else: - result.extra_info.solver_message = exit_code_message - - if result.solution_status != SolutionStatus.noSolution: - for v, val in zip(nl_info.variables, variable_vals): - sol_data.primals[id(v)] = (v, val) - for c, val in zip(nl_info.constraints, duals): - sol_data.duals[c] = val - ### Read suffixes ### - line = sol_file.readline() - while line: - line = line.strip() - if line == "": - continue - line = line.split() - # Some sort of garbage we tag onto the solver message, assuming we are past the suffixes - if line[0] != 'suffix': - # We assume this is the start of a - # section like kestrel_option, which - # comes after all suffixes. - remaining = "" - line = sol_file.readline() - while line: - remaining += line.strip() + "; " - line = sol_file.readline() - result.extra_info.solver_message += remaining - break - unmasked_kind = int(line[1]) - kind = unmasked_kind & 3 # 0-var, 1-con, 2-obj, 3-prob - convert_function = int - if (unmasked_kind & 4) == 4: - convert_function = float - nvalues = int(line[2]) - # namelen = int(line[3]) - # tablen = int(line[4]) - tabline = int(line[5]) - suffix_name = sol_file.readline().strip() - # ignore translation of the table number to string value for now, - # this information can be obtained from the solver documentation - for n in range(tabline): - sol_file.readline() - if kind == 0: # Var - sol_data.var_suffixes[suffix_name] = dict() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - var_ndx = int(suf_line[0]) - var = nl_info.variables[var_ndx] - sol_data.var_suffixes[suffix_name][id(var)] = ( - var, - convert_function(suf_line[1]), - ) - elif kind == 1: # Con - sol_data.con_suffixes[suffix_name] = dict() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - con_ndx = int(suf_line[0]) - con = nl_info.constraints[con_ndx] - sol_data.con_suffixes[suffix_name][con] = convert_function( - suf_line[1] - ) - elif kind == 2: # Obj - sol_data.obj_suffixes[suffix_name] = dict() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - obj_ndx = int(suf_line[0]) - obj = nl_info.objectives[obj_ndx] - sol_data.obj_suffixes[suffix_name][id(obj)] = ( - obj, - convert_function(suf_line[1]), - ) - elif kind == 3: # Prob - sol_data.problem_suffixes[suffix_name] = list() - for cnt in range(nvalues): - suf_line = sol_file.readline().split() - sol_data.problem_suffixes[suffix_name].append( - convert_function(suf_line[1]) - ) - line = sol_file.readline() - - return result, sol_data - - def parse_yaml(): pass diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py new file mode 100644 index 00000000000..f1fc7998179 --- /dev/null +++ b/pyomo/contrib/solver/sol_reader.py @@ -0,0 +1,206 @@ +from typing import Tuple, Dict, Any, List +import io + +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.constraint import _ConstraintData +from pyomo.core.base.objective import _ObjectiveData +from pyomo.repn.plugins.nl_writer import NLWriterInfo +from .results import Results, SolverResultsError, SolutionStatus, TerminationCondition + + +class SolFileData: + def __init__(self) -> None: + self.primals: Dict[int, Tuple[_GeneralVarData, float]] = dict() + self.duals: Dict[_ConstraintData, float] = dict() + self.var_suffixes: Dict[str, Dict[int, Tuple[_GeneralVarData, Any]]] = dict() + self.con_suffixes: Dict[str, Dict[_ConstraintData, Any]] = dict() + self.obj_suffixes: Dict[str, Dict[int, Tuple[_ObjectiveData, Any]]] = dict() + self.problem_suffixes: Dict[str, List[Any]] = dict() + + +def parse_sol_file( + sol_file: io.TextIOBase, + nl_info: NLWriterInfo, + result: Results, +) -> Tuple[Results, SolFileData]: + sol_data = SolFileData() + + # + # Some solvers (minto) do not write a message. We will assume + # all non-blank lines up the 'Options' line is the message. + # For backwards compatibility and general safety, we will parse all + # lines until "Options" appears. Anything before "Options" we will + # consider to be the solver message. + message = [] + for line in sol_file: + if not line: + break + line = line.strip() + if "Options" in line: + break + message.append(line) + message = '\n'.join(message) + # Once "Options" appears, we must now read the content under it. + model_objects = [] + if "Options" in line: + line = sol_file.readline() + number_of_options = int(line) + need_tolerance = False + if ( + number_of_options > 4 + ): # MRM: Entirely unclear why this is necessary, or if it even is + number_of_options -= 2 + need_tolerance = True + for i in range(number_of_options + 4): + line = sol_file.readline() + model_objects.append(int(line)) + if ( + need_tolerance + ): # MRM: Entirely unclear why this is necessary, or if it even is + line = sol_file.readline() + model_objects.append(float(line)) + else: + raise SolverResultsError("ERROR READING `sol` FILE. No 'Options' line found.") + # Identify the total number of variables and constraints + number_of_cons = model_objects[number_of_options + 1] + number_of_vars = model_objects[number_of_options + 3] + assert number_of_cons == len(nl_info.constraints) + assert number_of_vars == len(nl_info.variables) + + duals = [float(sol_file.readline()) for i in range(number_of_cons)] + variable_vals = [float(sol_file.readline()) for i in range(number_of_vars)] + + # Parse the exit code line and capture it + exit_code = [0, 0] + line = sol_file.readline() + if line and ('objno' in line): + exit_code_line = line.split() + if len(exit_code_line) != 3: + raise SolverResultsError( + f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}." + ) + exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] + else: + raise SolverResultsError( + f"ERROR READING `sol` FILE. Expected `objno`; received {line}." + ) + result.extra_info.solver_message = message.strip().replace('\n', '; ') + exit_code_message = '' + if (exit_code[1] >= 0) and (exit_code[1] <= 99): + result.solution_status = SolutionStatus.optimal + result.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + elif (exit_code[1] >= 100) and (exit_code[1] <= 199): + exit_code_message = "Optimal solution indicated, but ERROR LIKELY!" + result.solution_status = SolutionStatus.feasible + result.termination_condition = TerminationCondition.error + elif (exit_code[1] >= 200) and (exit_code[1] <= 299): + exit_code_message = "INFEASIBLE SOLUTION: constraints cannot be satisfied!" + result.solution_status = SolutionStatus.infeasible + # TODO: this is solver dependent + # But this was the way in the previous version - and has been fine thus far? + result.termination_condition = TerminationCondition.locallyInfeasible + elif (exit_code[1] >= 300) and (exit_code[1] <= 399): + exit_code_message = ( + "UNBOUNDED PROBLEM: the objective can be improved without limit!" + ) + result.solution_status = SolutionStatus.noSolution + result.termination_condition = TerminationCondition.unbounded + elif (exit_code[1] >= 400) and (exit_code[1] <= 499): + exit_code_message = ( + "EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " + "was stopped by a limit that you set!" + ) + # TODO: this is solver dependent + # But this was the way in the previous version - and has been fine thus far? + result.solution_status = SolutionStatus.infeasible + result.termination_condition = TerminationCondition.iterationLimit + elif (exit_code[1] >= 500) and (exit_code[1] <= 599): + exit_code_message = ( + "FAILURE: the solver stopped by an error condition " + "in the solver routines!" + ) + result.termination_condition = TerminationCondition.error + + if result.extra_info.solver_message: + if exit_code_message: + result.extra_info.solver_message += '; ' + exit_code_message + else: + result.extra_info.solver_message = exit_code_message + + if result.solution_status != SolutionStatus.noSolution: + for v, val in zip(nl_info.variables, variable_vals): + sol_data.primals[id(v)] = (v, val) + for c, val in zip(nl_info.constraints, duals): + sol_data.duals[c] = val + ### Read suffixes ### + line = sol_file.readline() + while line: + line = line.strip() + if line == "": + continue + line = line.split() + # Some sort of garbage we tag onto the solver message, assuming we are past the suffixes + if line[0] != 'suffix': + # We assume this is the start of a + # section like kestrel_option, which + # comes after all suffixes. + remaining = "" + line = sol_file.readline() + while line: + remaining += line.strip() + "; " + line = sol_file.readline() + result.extra_info.solver_message += remaining + break + unmasked_kind = int(line[1]) + kind = unmasked_kind & 3 # 0-var, 1-con, 2-obj, 3-prob + convert_function = int + if (unmasked_kind & 4) == 4: + convert_function = float + nvalues = int(line[2]) + # namelen = int(line[3]) + # tablen = int(line[4]) + tabline = int(line[5]) + suffix_name = sol_file.readline().strip() + # ignore translation of the table number to string value for now, + # this information can be obtained from the solver documentation + for n in range(tabline): + sol_file.readline() + if kind == 0: # Var + sol_data.var_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = sol_file.readline().split() + var_ndx = int(suf_line[0]) + var = nl_info.variables[var_ndx] + sol_data.var_suffixes[suffix_name][id(var)] = ( + var, + convert_function(suf_line[1]), + ) + elif kind == 1: # Con + sol_data.con_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = sol_file.readline().split() + con_ndx = int(suf_line[0]) + con = nl_info.constraints[con_ndx] + sol_data.con_suffixes[suffix_name][con] = convert_function( + suf_line[1] + ) + elif kind == 2: # Obj + sol_data.obj_suffixes[suffix_name] = dict() + for cnt in range(nvalues): + suf_line = sol_file.readline().split() + obj_ndx = int(suf_line[0]) + obj = nl_info.objectives[obj_ndx] + sol_data.obj_suffixes[suffix_name][id(obj)] = ( + obj, + convert_function(suf_line[1]), + ) + elif kind == 3: # Prob + sol_data.problem_suffixes[suffix_name] = list() + for cnt in range(nvalues): + suf_line = sol_file.readline().split() + sol_data.problem_suffixes[suffix_name].append( + convert_function(suf_line[1]) + ) + line = sol_file.readline() + + return result, sol_data From 6c5d83c5ff23c175229e511088d2fe8b569daa48 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 01:59:52 -0700 Subject: [PATCH 0362/3044] remove symbol map when it is not necessary --- pyomo/contrib/solver/base.py | 6 ++---- pyomo/contrib/solver/ipopt.py | 13 +------------ 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 56292859b1a..962d35582a1 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -27,6 +27,7 @@ from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize from pyomo.core.base import SymbolMap +from pyomo.core.base.label import NumericLabeler from pyomo.core.staleflag import StaleFlagManager from pyomo.contrib.solver.util import get_objective from pyomo.contrib.solver.results import ( @@ -425,10 +426,7 @@ def solve( legacy_soln.gap = None symbol_map = SymbolMap() - symbol_map.byObject = dict(symbol_map.byObject) - symbol_map.bySymbol = dict(symbol_map.bySymbol) - symbol_map.aliases = dict(symbol_map.aliases) - symbol_map.default_labeler = symbol_map.default_labeler + symbol_map.default_labeler = NumericLabeler('x') model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index c7a932eb883..5e84fd8796c 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -22,10 +22,9 @@ from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer from pyomo.core.base import Objective -from pyomo.core.base.label import NumericLabeler from pyomo.core.staleflag import StaleFlagManager from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo, AMPLRepn -from pyomo.contrib.solver.base import SolverBase, SymbolMap +from pyomo.contrib.solver.base import SolverBase from pyomo.contrib.solver.config import SolverConfig from pyomo.contrib.solver.factory import SolverFactory from pyomo.contrib.solver.results import ( @@ -216,10 +215,6 @@ def version(self): self._version_cache = version return self._version_cache - @property - def symbol_map(self): - return self._symbol_map - def _write_options_file(self, filename: str, options: Mapping): # First we need to determine if we even need to create a file. # If options is empty, then we return False @@ -305,12 +300,6 @@ def solve(self, model, **kwds): if env.get('AMPLFUNC'): nl_info.external_function_libraries.append(env.get('AMPLFUNC')) env['AMPLFUNC'] = "\n".join(nl_info.external_function_libraries) - symbol_map = self._symbol_map = SymbolMap() - labeler = NumericLabeler('component') - for v in nl_info.variables: - symbol_map.getSymbol(v, labeler) - for c in nl_info.constraints: - symbol_map.getSymbol(c, labeler) # Write the opt_file, if there should be one; return a bool to say # whether or not we have one (so we can correctly build the command line) opt_file = self._write_options_file( From c800776eebfdebbe1988be3d9dec15bf9763103c Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 02:12:06 -0700 Subject: [PATCH 0363/3044] reorg --- pyomo/contrib/solver/ipopt.py | 25 +------------------------ pyomo/contrib/solver/sol_reader.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 5e84fd8796c..23464e40cb2 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -392,11 +392,7 @@ def solve(self, model, **kwds): if results.solution_status in { SolutionStatus.feasible, SolutionStatus.optimal, - } and len( - list( - model.component_data_objects(Objective, descend_into=True, active=True) - ) - ): + } and len(nl_info.objectives) > 0: if config.load_solution: results.incumbent_objective = value(nl_info.objectives[0]) else: @@ -476,14 +472,6 @@ def _parse_solution( if abs(zu) > abs(rc[v_id][1]): rc[v_id] = (v, zu) - if len(nl_info.eliminated_vars) > 0: - sub_map = {k: v[1] for k, v in sol_data.primals.items()} - for v, v_expr in nl_info.eliminated_vars: - val = evaluate_ampl_repn(v_expr, sub_map) - v_id = id(v) - sub_map[v_id] = val - sol_data.primals[v_id] = (v, val) - res.solution_loader = SolutionLoader( primals=sol_data.primals, duals=sol_data.duals, @@ -492,14 +480,3 @@ def _parse_solution( ) return res - - -def evaluate_ampl_repn(repn: AMPLRepn, sub_map): - assert not repn.nonlinear - assert repn.nl is None - val = repn.const - if repn.linear is not None: - for v_id, v_coef in repn.linear.items(): - val += v_coef * sub_map[v_id] - val *= repn.mult - return val diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index f1fc7998179..93fb6d39da3 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -6,6 +6,18 @@ from pyomo.core.base.objective import _ObjectiveData from pyomo.repn.plugins.nl_writer import NLWriterInfo from .results import Results, SolverResultsError, SolutionStatus, TerminationCondition +from pyomo.repn.plugins.nl_writer import AMPLRepn + + +def evaluate_ampl_repn(repn: AMPLRepn, sub_map): + assert not repn.nonlinear + assert repn.nl is None + val = repn.const + if repn.linear is not None: + for v_id, v_coef in repn.linear.items(): + val += v_coef * sub_map[v_id] + val *= repn.mult + return val class SolFileData: @@ -203,4 +215,12 @@ def parse_sol_file( ) line = sol_file.readline() + if len(nl_info.eliminated_vars) > 0: + sub_map = {k: v[1] for k, v in sol_data.primals.items()} + for v, v_expr in nl_info.eliminated_vars: + val = evaluate_ampl_repn(v_expr, sub_map) + v_id = id(v) + sub_map[v_id] = val + sol_data.primals[v_id] = (v, val) + return result, sol_data From 952e8e77bca81c65d914fc35fc2a43fdea69a9cc Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 07:44:41 -0700 Subject: [PATCH 0364/3044] solver refactor: various fixes --- pyomo/contrib/solver/ipopt.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 23464e40cb2..2705abec7c6 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -71,9 +71,6 @@ def __init__( self.executable = self.declare( 'executable', ConfigValue(default=Executable('ipopt')) ) - self.save_solver_io: bool = self.declare( - 'save_solver_io', ConfigValue(domain=bool, default=False) - ) # TODO: Add in a deprecation here for keepfiles self.temp_dir: str = self.declare( 'temp_dir', ConfigValue(domain=str, default=None) From 1b1875c49600d4cdbf4f4e0d8536dd5d2b6895b3 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 08:24:46 -0700 Subject: [PATCH 0365/3044] gdp.binary_multiplication: cleanup --- pyomo/gdp/plugins/binary_multiplication.py | 15 +++++------ pyomo/gdp/tests/test_binary_multiplication.py | 27 +++---------------- 2 files changed, 11 insertions(+), 31 deletions(-) diff --git a/pyomo/gdp/plugins/binary_multiplication.py b/pyomo/gdp/plugins/binary_multiplication.py index 2305f244f29..8489fa04808 100644 --- a/pyomo/gdp/plugins/binary_multiplication.py +++ b/pyomo/gdp/plugins/binary_multiplication.py @@ -3,30 +3,28 @@ from pyomo.core.base import TransformationFactory from pyomo.core.util import target_list from pyomo.gdp import Disjunction -from pyomo.gdp.plugins.gdp_to_mip_transformation import GDP_to_MIP_Transformation -from pyomo.core.util import target_list from weakref import ref as weakref_ref import logging -logger = logging.getLogger('pyomo.gdp.binary_multiplication') +logger = logging.getLogger(__name__) @TransformationFactory.register( 'gdp.binary_multiplication', - doc="Reformulate the GDP as an MINLP by multiplying f(x) <= 0 by y to get f(x) * y <= 0.", + doc="Reformulate the GDP as an MINLP by multiplying f(x) <= 0 by y to get f(x) * y <= 0 where y is the binary corresponding to the Boolean indicator var of the Disjunct containing f(x) <= 0.", ) -class GDPToMINLPTransformation(GDP_to_MIP_Transformation): +class GDPBinaryMultiplicationTransformation(GDP_to_MIP_Transformation): CONFIG = ConfigDict("gdp.binary_multiplication") CONFIG.declare( 'targets', ConfigValue( default=None, domain=target_list, - description="target or list of targets that will be relaxed", + description="target or list of targets that will be transformed", doc=""" - This specifies the list of components to relax. If None (default), the + This specifies the list of components to transform. If None (default), the entire model is transformed. Note that if the transformation is done out of place, the list of targets should be attached to the model before it is cloned, and the list will specify the targets on the cloned @@ -81,7 +79,8 @@ def _transform_disjunctionData( or_expr += disjunct.binary_indicator_var self._transform_disjunct(disjunct, transBlock) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var + # rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var + rhs = 1 if obj.xor: xorConstraint[index] = or_expr == rhs else: diff --git a/pyomo/gdp/tests/test_binary_multiplication.py b/pyomo/gdp/tests/test_binary_multiplication.py index 2c6e045f853..9a515ef830a 100644 --- a/pyomo/gdp/tests/test_binary_multiplication.py +++ b/pyomo/gdp/tests/test_binary_multiplication.py @@ -22,6 +22,7 @@ from pyomo.gdp import Disjunct, Disjunction from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.repn import generate_standard_repn +from pyomo.core.expr.compare import assertExpressionsEqual import pyomo.core.expr as EXPR import pyomo.gdp.tests.models as models @@ -168,9 +169,6 @@ def test_do_not_transform_userDeactivated_IndexedDisjunction(self): self, 'binary_multiplication' ) - # helper method to check the M values in all of the transformed - # constraints (m, M) is the tuple for M. This also relies on the - # disjuncts being transformed in the same order every time. def check_transformed_constraints( self, model, binary_multiplication, cons1lb, cons2lb, cons2ub, cons3ub ): @@ -183,14 +181,8 @@ def check_transformed_constraints( self.assertEqual(len(c), 1) c_lb = c[0] self.assertTrue(c[0].active) - repn = generate_standard_repn(c[0].body) - self.assertIsNone(repn.nonlinear_expr) - self.assertEqual(len(repn.quadratic_coefs), 1) - self.assertEqual(len(repn.linear_coefs), 1) ind_var = model.d[0].indicator_var - ct.check_quadratic_coef(self, repn, model.a, ind_var, 1) - ct.check_linear_coef(self, repn, ind_var, -model.d[0].c.lower) - self.assertEqual(repn.constant, 0) + assertExpressionsEqual(self, c[0].body, (model.a - model.d[0].c.lower)*ind_var) self.assertEqual(c[0].lower, 0) self.assertIsNone(c[0].upper) @@ -199,13 +191,8 @@ def check_transformed_constraints( self.assertEqual(len(c), 1) c_eq = c[0] self.assertTrue(c[0].active) - repn = generate_standard_repn(c[0].body) - self.assertTrue(repn.nonlinear_expr is None) - self.assertEqual(len(repn.linear_coefs), 0) - self.assertEqual(len(repn.quadratic_coefs), 1) ind_var = model.d[1].indicator_var - ct.check_quadratic_coef(self, repn, model.a, ind_var, 1) - self.assertEqual(repn.constant, 0) + assertExpressionsEqual(self, c[0].body, model.a*ind_var) self.assertEqual(c[0].lower, 0) self.assertEqual(c[0].upper, 0) @@ -214,13 +201,7 @@ def check_transformed_constraints( self.assertEqual(len(c), 1) c_ub = c[0] self.assertTrue(c_ub.active) - repn = generate_standard_repn(c_ub.body) - self.assertIsNone(repn.nonlinear_expr) - self.assertEqual(len(repn.linear_coefs), 1) - self.assertEqual(len(repn.quadratic_coefs), 1) - ct.check_quadratic_coef(self, repn, model.x, ind_var, 1) - ct.check_linear_coef(self, repn, ind_var, -model.d[1].c2.upper) - self.assertEqual(repn.constant, 0) + assertExpressionsEqual(self, c_ub.body, (model.x - model.d[1].c2.upper)*ind_var) self.assertIsNone(c_ub.lower) self.assertEqual(c_ub.upper, 0) From 15c521794ff53fd38409096eaa89dba0020534e0 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 08:31:26 -0700 Subject: [PATCH 0366/3044] run black --- pyomo/gdp/tests/test_binary_multiplication.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyomo/gdp/tests/test_binary_multiplication.py b/pyomo/gdp/tests/test_binary_multiplication.py index 9a515ef830a..6b3ba87fa21 100644 --- a/pyomo/gdp/tests/test_binary_multiplication.py +++ b/pyomo/gdp/tests/test_binary_multiplication.py @@ -182,7 +182,9 @@ def check_transformed_constraints( c_lb = c[0] self.assertTrue(c[0].active) ind_var = model.d[0].indicator_var - assertExpressionsEqual(self, c[0].body, (model.a - model.d[0].c.lower)*ind_var) + assertExpressionsEqual( + self, c[0].body, (model.a - model.d[0].c.lower) * ind_var + ) self.assertEqual(c[0].lower, 0) self.assertIsNone(c[0].upper) @@ -192,7 +194,7 @@ def check_transformed_constraints( c_eq = c[0] self.assertTrue(c[0].active) ind_var = model.d[1].indicator_var - assertExpressionsEqual(self, c[0].body, model.a*ind_var) + assertExpressionsEqual(self, c[0].body, model.a * ind_var) self.assertEqual(c[0].lower, 0) self.assertEqual(c[0].upper, 0) @@ -201,7 +203,9 @@ def check_transformed_constraints( self.assertEqual(len(c), 1) c_ub = c[0] self.assertTrue(c_ub.active) - assertExpressionsEqual(self, c_ub.body, (model.x - model.d[1].c2.upper)*ind_var) + assertExpressionsEqual( + self, c_ub.body, (model.x - model.d[1].c2.upper) * ind_var + ) self.assertIsNone(c_ub.lower) self.assertEqual(c_ub.upper, 0) From e256ab19c56375bbe760fc1f81db3f8e57a27b6b Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 08:48:00 -0700 Subject: [PATCH 0367/3044] make skip_trivial_constraints True by default --- pyomo/repn/plugins/nl_writer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 3b94963e858..897b906c181 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -338,6 +338,9 @@ def __call__(self, model, filename, solver_capability, io_options): config.scale_model = False config.linear_presolve = False + # just for backwards compatibility + config.skip_trivial_constraints = False + if config.symbolic_solver_labels: _open = lambda fname: open(fname, 'w') else: From 5b7919ec202c1b16ae0ab8adf6f479eac5b2fb36 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 16 Jan 2024 09:24:35 -0700 Subject: [PATCH 0368/3044] Apply black; convert doc -> description for ConfigDicts --- pyomo/contrib/solver/config.py | 254 ++++++++++++++--------------- pyomo/contrib/solver/ipopt.py | 26 ++- pyomo/contrib/solver/results.py | 17 +- pyomo/contrib/solver/sol_reader.py | 4 +- 4 files changed, 153 insertions(+), 148 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 8fe627cbcc1..84b1c2d2c87 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -21,6 +21,117 @@ from pyomo.common.timing import HierarchicalTimer +class SolverConfig(ConfigDict): + """ + Base config values for all solver interfaces + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.tee: bool = self.declare( + 'tee', + ConfigValue( + domain=bool, + default=False, + description="If True, the solver log prints to stdout.", + ), + ) + self.load_solution: bool = self.declare( + 'load_solution', + ConfigValue( + domain=bool, + default=True, + description="If True, the values of the primal variables will be loaded into the model.", + ), + ) + self.raise_exception_on_nonoptimal_result: bool = self.declare( + 'raise_exception_on_nonoptimal_result', + ConfigValue( + domain=bool, + default=True, + description="If False, the `solve` method will continue processing even if the returned result is nonoptimal.", + ), + ) + self.symbolic_solver_labels: bool = self.declare( + 'symbolic_solver_labels', + ConfigValue( + domain=bool, + default=False, + description="If True, the names given to the solver will reflect the names of the Pyomo components. Cannot be changed after set_instance is called.", + ), + ) + self.timer: HierarchicalTimer = self.declare( + 'timer', ConfigValue(default=None, description="A HierarchicalTimer.") + ) + self.threads: Optional[int] = self.declare( + 'threads', + ConfigValue( + domain=NonNegativeInt, + description="Number of threads to be used by a solver.", + default=None, + ), + ) + self.time_limit: Optional[float] = self.declare( + 'time_limit', + ConfigValue( + domain=NonNegativeFloat, description="Time limit applied to the solver." + ), + ) + self.solver_options: ConfigDict = self.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + + +class BranchAndBoundConfig(SolverConfig): + """ + Attributes + ---------- + mip_gap: float + Solver will terminate if the mip gap is less than mip_gap + relax_integrality: bool + If True, all integer variables will be relaxed to continuous + variables before solving + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.rel_gap: Optional[float] = self.declare( + 'rel_gap', ConfigValue(domain=NonNegativeFloat) + ) + self.abs_gap: Optional[float] = self.declare( + 'abs_gap', ConfigValue(domain=NonNegativeFloat) + ) + + class AutoUpdateConfig(ConfigDict): """ This is necessary for persistent solvers. @@ -59,7 +170,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, new/old constraints will not be automatically detected on subsequent solves. Use False only when manually updating the solver with opt.add_constraints() and opt.remove_constraints() or when you are certain constraints are not being @@ -71,7 +182,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, new/old variables will not be automatically detected on subsequent solves. Use False only when manually updating the solver with opt.add_variables() and opt.remove_variables() or when you are certain variables are not being added to / @@ -83,7 +194,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, new/old parameters will not be automatically detected on subsequent solves. Use False only when manually updating the solver with opt.add_params() and opt.remove_params() or when you are certain parameters are not being added to / @@ -95,7 +206,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, new/old objectives will not be automatically detected on subsequent solves. Use False only when manually updating the solver with opt.set_objective() or when you are certain objectives are not being added to / removed from the model.""", @@ -106,7 +217,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, changes to existing constraints will not be automatically detected on subsequent solves. This includes changes to the lower, body, and upper attributes of constraints. Use False only when manually updating the solver with @@ -119,7 +230,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, changes to existing variables will not be automatically detected on subsequent solves. This includes changes to the lb, ub, domain, and fixed attributes of variables. Use False only when manually updating the solver with @@ -131,7 +242,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, changes to parameter values will not be automatically detected on subsequent solves. Use False only when manually updating the solver with opt.update_params() or when you are certain parameters are not being modified.""", @@ -142,7 +253,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, changes to Expressions will not be automatically detected on subsequent solves. Use False only when manually updating the solver with opt.remove_constraints() and opt.add_constraints() or when you are certain @@ -154,7 +265,7 @@ def __init__( ConfigValue( domain=bool, default=True, - doc=""" + description=""" If False, changes to objectives will not be automatically detected on subsequent solves. This includes the expr and sense attributes of objectives. Use False only when manually updating the solver with opt.set_objective() or when you are @@ -167,7 +278,7 @@ def __init__( domain=bool, default=True, visibility=ADVANCED_OPTION, - doc=""" + description=""" This is an advanced option that should only be used in special circumstances. With the default setting of True, fixed variables will be treated like parameters. This means that z == x*y will be linear if x or y is fixed and the constraint @@ -181,121 +292,6 @@ def __init__( ) -class SolverConfig(ConfigDict): - """ - Base config values for all solver interfaces - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.tee: bool = self.declare( - 'tee', - ConfigValue( - domain=bool, - default=False, - description="If True, the solver log prints to stdout.", - ), - ) - self.load_solution: bool = self.declare( - 'load_solution', - ConfigValue( - domain=bool, - default=True, - description="If True, the values of the primal variables will be loaded into the model.", - ), - ) - self.raise_exception_on_nonoptimal_result: bool = self.declare( - 'raise_exception_on_nonoptimal_result', - ConfigValue( - domain=bool, - default=True, - description="If False, the `solve` method will continue processing even if the returned result is nonoptimal.", - ), - ) - self.symbolic_solver_labels: bool = self.declare( - 'symbolic_solver_labels', - ConfigValue( - domain=bool, - default=False, - description="If True, the names given to the solver will reflect the names of the Pyomo components. Cannot be changed after set_instance is called.", - ), - ) - self.timer: HierarchicalTimer = self.declare( - 'timer', - ConfigValue( - default=None, - description="A HierarchicalTimer.", - ), - ) - self.threads: Optional[int] = self.declare( - 'threads', - ConfigValue( - domain=NonNegativeInt, - description="Number of threads to be used by a solver.", - default=None, - ), - ) - self.time_limit: Optional[float] = self.declare( - 'time_limit', - ConfigValue( - domain=NonNegativeFloat, description="Time limit applied to the solver." - ), - ) - self.solver_options: ConfigDict = self.declare( - 'solver_options', - ConfigDict(implicit=True, description="Options to pass to the solver."), - ) - - -class BranchAndBoundConfig(SolverConfig): - """ - Attributes - ---------- - mip_gap: float - Solver will terminate if the mip gap is less than mip_gap - relax_integrality: bool - If True, all integer variables will be relaxed to continuous - variables before solving - """ - - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - - self.rel_gap: Optional[float] = self.declare( - 'rel_gap', ConfigValue(domain=NonNegativeFloat) - ) - self.abs_gap: Optional[float] = self.declare( - 'abs_gap', ConfigValue(domain=NonNegativeFloat) - ) - - class PersistentSolverConfig(SolverConfig): def __init__( self, @@ -313,7 +309,9 @@ def __init__( visibility=visibility, ) - self.auto_updates: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) + self.auto_updates: AutoUpdateConfig = self.declare( + 'auto_updates', AutoUpdateConfig() + ) class PersistentBranchAndBoundConfig(BranchAndBoundConfig): @@ -333,4 +331,6 @@ def __init__( visibility=visibility, ) - self.auto_updates: AutoUpdateConfig = self.declare('auto_updates', AutoUpdateConfig()) + self.auto_updates: AutoUpdateConfig = self.declare( + 'auto_updates', AutoUpdateConfig() + ) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 2705abec7c6..63dca0af0d9 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -27,11 +27,7 @@ from pyomo.contrib.solver.base import SolverBase from pyomo.contrib.solver.config import SolverConfig from pyomo.contrib.solver.factory import SolverFactory -from pyomo.contrib.solver.results import ( - Results, - TerminationCondition, - SolutionStatus, -) +from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus from .sol_reader import parse_sol_file from pyomo.contrib.solver.solution import SolutionLoaderBase, SolutionLoader from pyomo.common.tee import TeeStream @@ -82,8 +78,7 @@ def __init__( 'log_level', ConfigValue(domain=NonNegativeInt, default=logging.INFO) ) self.writer_config = self.declare( - 'writer_config', - ConfigValue(default=NLWriter.CONFIG()) + 'writer_config', ConfigValue(default=NLWriter.CONFIG()) ) @@ -237,7 +232,10 @@ def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: boo 'Pyomo generates the ipopt options file as part of the solve method. ' 'Add all options to ipopt.config.solver_options instead.' ) - if config.time_limit is not None and 'max_cpu_time' not in config.solver_options: + if ( + config.time_limit is not None + and 'max_cpu_time' not in config.solver_options + ): config.solver_options['max_cpu_time'] = config.time_limit for k, val in config.solver_options.items(): if k in ipopt_command_line_options: @@ -386,10 +384,10 @@ def solve(self, model, **kwds): ): model.rc.update(results.solution_loader.get_reduced_costs()) - if results.solution_status in { - SolutionStatus.feasible, - SolutionStatus.optimal, - } and len(nl_info.objectives) > 0: + if ( + results.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal} + and len(nl_info.objectives) > 0 + ): if config.load_solution: results.incumbent_objective = value(nl_info.objectives[0]) else: @@ -448,9 +446,7 @@ def _parse_solution( self, instream: io.TextIOBase, nl_info: NLWriterInfo, result: ipoptResults ): res, sol_data = parse_sol_file( - sol_file=instream, - nl_info=nl_info, - result=result, + sol_file=instream, nl_info=nl_info, result=result ) if res.solution_status == SolutionStatus.noSolution: diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index 3beb3aede81..e21adcc35cc 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -220,7 +220,9 @@ def __init__( self.iteration_count: Optional[int] = self.declare( 'iteration_count', ConfigValue(domain=NonNegativeInt, default=None) ) - self.timing_info: ConfigDict = self.declare('timing_info', ConfigDict(implicit=True)) + self.timing_info: ConfigDict = self.declare( + 'timing_info', ConfigDict(implicit=True) + ) self.timing_info.start_timestamp: datetime = self.timing_info.declare( 'start_timestamp', ConfigValue(domain=Datetime) @@ -231,8 +233,17 @@ def __init__( self.extra_info: ConfigDict = self.declare( 'extra_info', ConfigDict(implicit=True) ) - self.solver_configuration: ConfigDict = self.declare('solver_configuration', ConfigValue(doc="A copy of the config object used in the solve", visibility=ADVANCED_OPTION)) - self.solver_log: str = self.declare('solver_log', ConfigValue(domain=str, default=None, visibility=ADVANCED_OPTION)) + self.solver_configuration: ConfigDict = self.declare( + 'solver_configuration', + ConfigValue( + description="A copy of the config object used in the solve", + visibility=ADVANCED_OPTION, + ), + ) + self.solver_log: str = self.declare( + 'solver_log', + ConfigValue(domain=str, default=None, visibility=ADVANCED_OPTION), + ) class ResultsReader: diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index 93fb6d39da3..92761246241 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -31,9 +31,7 @@ def __init__(self) -> None: def parse_sol_file( - sol_file: io.TextIOBase, - nl_info: NLWriterInfo, - result: Results, + sol_file: io.TextIOBase, nl_info: NLWriterInfo, result: Results ) -> Tuple[Results, SolFileData]: sol_data = SolFileData() From 78c08d09c8396ccc85b480116c698568b8c088b4 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 10:06:20 -0700 Subject: [PATCH 0369/3044] restore changes to appsi --- pyomo/contrib/appsi/__init__.py | 1 + pyomo/contrib/appsi/base.py | 1695 +++++++++++++++++ pyomo/contrib/appsi/build.py | 6 +- .../contrib/appsi/examples/getting_started.py | 14 +- .../appsi/examples/tests/test_examples.py | 15 +- pyomo/contrib/appsi/fbbt.py | 23 +- pyomo/contrib/appsi/plugins.py | 2 +- pyomo/contrib/appsi/solvers/cbc.py | 90 +- pyomo/contrib/appsi/solvers/cplex.py | 90 +- pyomo/contrib/appsi/solvers/gurobi.py | 149 +- pyomo/contrib/appsi/solvers/highs.py | 139 +- pyomo/contrib/appsi/solvers/ipopt.py | 86 +- .../solvers/tests/test_gurobi_persistent.py | 33 +- .../solvers/tests/test_highs_persistent.py | 11 +- .../solvers/tests/test_ipopt_persistent.py | 6 +- .../solvers/tests/test_persistent_solvers.py | 431 ++--- .../solvers/tests/test_wntr_persistent.py | 97 +- pyomo/contrib/appsi/solvers/wntr.py | 32 +- pyomo/contrib/appsi/tests/test_base.py | 91 + pyomo/contrib/appsi/tests/test_interval.py | 4 +- pyomo/contrib/appsi/utils/__init__.py | 2 + .../utils/collect_vars_and_named_exprs.py | 50 + pyomo/contrib/appsi/utils/get_objective.py | 12 + pyomo/contrib/appsi/utils/tests/__init__.py | 0 .../test_collect_vars_and_named_exprs.py | 56 + pyomo/contrib/appsi/writers/config.py | 2 +- pyomo/contrib/appsi/writers/lp_writer.py | 22 +- pyomo/contrib/appsi/writers/nl_writer.py | 33 +- .../appsi/writers/tests/test_nl_writer.py | 2 +- 29 files changed, 2493 insertions(+), 701 deletions(-) create mode 100644 pyomo/contrib/appsi/base.py create mode 100644 pyomo/contrib/appsi/tests/test_base.py create mode 100644 pyomo/contrib/appsi/utils/__init__.py create mode 100644 pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py create mode 100644 pyomo/contrib/appsi/utils/get_objective.py create mode 100644 pyomo/contrib/appsi/utils/tests/__init__.py create mode 100644 pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py diff --git a/pyomo/contrib/appsi/__init__.py b/pyomo/contrib/appsi/__init__.py index 0134a96f363..df3ba212448 100644 --- a/pyomo/contrib/appsi/__init__.py +++ b/pyomo/contrib/appsi/__init__.py @@ -1,3 +1,4 @@ +from . import base from . import solvers from . import writers from . import fbbt diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py new file mode 100644 index 00000000000..e6186eeedd2 --- /dev/null +++ b/pyomo/contrib/appsi/base.py @@ -0,0 +1,1695 @@ +import abc +import enum +from typing import ( + Sequence, + Dict, + Optional, + Mapping, + NoReturn, + List, + Tuple, + MutableMapping, +) +from pyomo.core.base.constraint import _GeneralConstraintData, Constraint +from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint +from pyomo.core.base.var import _GeneralVarData, Var +from pyomo.core.base.param import _ParamData, Param +from pyomo.core.base.block import _BlockData, Block +from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.common.collections import ComponentMap +from .utils.get_objective import get_objective +from .utils.collect_vars_and_named_exprs import collect_vars_and_named_exprs +from pyomo.common.timing import HierarchicalTimer +from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat +from pyomo.common.errors import ApplicationError +from pyomo.opt.base import SolverFactory as LegacySolverFactory +from pyomo.common.factory import Factory +import os +from pyomo.opt.results.results_ import SolverResults as LegacySolverResults +from pyomo.opt.results.solution import ( + Solution as LegacySolution, + SolutionStatus as LegacySolutionStatus, +) +from pyomo.opt.results.solver import ( + TerminationCondition as LegacyTerminationCondition, + SolverStatus as LegacySolverStatus, +) +from pyomo.core.kernel.objective import minimize +from pyomo.core.base import SymbolMap +import weakref +from .cmodel import cmodel, cmodel_available +from pyomo.core.staleflag import StaleFlagManager +from pyomo.core.expr.numvalue import NumericConstant + + +class TerminationCondition(enum.Enum): + """ + An enumeration for checking the termination condition of solvers + """ + + unknown = 0 + """unknown serves as both a default value, and it is used when no other enum member makes sense""" + + maxTimeLimit = 1 + """The solver exited due to a time limit""" + + maxIterations = 2 + """The solver exited due to an iteration limit """ + + objectiveLimit = 3 + """The solver exited due to an objective limit""" + + minStepLength = 4 + """The solver exited due to a minimum step length""" + + optimal = 5 + """The solver exited with the optimal solution""" + + unbounded = 8 + """The solver exited because the problem is unbounded""" + + infeasible = 9 + """The solver exited because the problem is infeasible""" + + infeasibleOrUnbounded = 10 + """The solver exited because the problem is either infeasible or unbounded""" + + error = 11 + """The solver exited due to an error""" + + interrupted = 12 + """The solver exited because it was interrupted""" + + licensingProblems = 13 + """The solver exited due to licensing problems""" + + +class SolverConfig(ConfigDict): + """ + Attributes + ---------- + time_limit: float + Time limit for the solver + stream_solver: bool + If True, then the solver log goes to stdout + load_solution: bool + If False, then the values of the primal variables will not be + loaded into the model + symbolic_solver_labels: bool + If True, the names given to the solver will reflect the names + of the pyomo components. Cannot be changed after set_instance + is called. + report_timing: bool + If True, then some timing information will be printed at the + end of the solve. + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(SolverConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare('time_limit', ConfigValue(domain=NonNegativeFloat)) + self.declare('stream_solver', ConfigValue(domain=bool)) + self.declare('load_solution', ConfigValue(domain=bool)) + self.declare('symbolic_solver_labels', ConfigValue(domain=bool)) + self.declare('report_timing', ConfigValue(domain=bool)) + + self.time_limit: Optional[float] = None + self.stream_solver: bool = False + self.load_solution: bool = True + self.symbolic_solver_labels: bool = False + self.report_timing: bool = False + + +class MIPSolverConfig(SolverConfig): + """ + Attributes + ---------- + mip_gap: float + Solver will terminate if the mip gap is less than mip_gap + relax_integrality: bool + If True, all integer variables will be relaxed to continuous + variables before solving + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(MIPSolverConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare('mip_gap', ConfigValue(domain=NonNegativeFloat)) + self.declare('relax_integrality', ConfigValue(domain=bool)) + + self.mip_gap: Optional[float] = None + self.relax_integrality: bool = False + + +class SolutionLoaderBase(abc.ABC): + def load_vars( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> NoReturn: + """ + Load the solution of the primal variables into the value attribute of the variables. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution + to all primal variables will be loaded. + """ + for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + @abc.abstractmethod + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + """ + Returns a ComponentMap mapping variable to var value. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution value should be retrieved. If vars_to_load is None, + then the values for all variables will be retrieved. + + Returns + ------- + primals: ComponentMap + Maps variables to solution values + """ + pass + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Returns a dictionary mapping constraint to dual value. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be retrieved. If cons_to_load is None, then the duals for all + constraints will be retrieved. + + Returns + ------- + duals: dict + Maps constraints to dual values + """ + raise NotImplementedError(f'{type(self)} does not support the get_duals method') + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Returns a dictionary mapping constraint to slack. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all + constraints will be loaded. + + Returns + ------- + slacks: dict + Maps constraints to slacks + """ + raise NotImplementedError( + f'{type(self)} does not support the get_slacks method' + ) + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + """ + Returns a ComponentMap mapping variable to reduced cost. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose reduced cost should be retrieved. If vars_to_load is None, then the + reduced costs for all variables will be loaded. + + Returns + ------- + reduced_costs: ComponentMap + Maps variables to reduced costs + """ + raise NotImplementedError( + f'{type(self)} does not support the get_reduced_costs method' + ) + + +class SolutionLoader(SolutionLoaderBase): + def __init__( + self, + primals: Optional[MutableMapping], + duals: Optional[MutableMapping], + slacks: Optional[MutableMapping], + reduced_costs: Optional[MutableMapping], + ): + """ + Parameters + ---------- + primals: dict + maps id(Var) to (var, value) + duals: dict + maps Constraint to dual value + slacks: dict + maps Constraint to slack value + reduced_costs: dict + maps id(Var) to (var, reduced_cost) + """ + self._primals = primals + self._duals = duals + self._slacks = slacks + self._reduced_costs = reduced_costs + + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + if self._primals is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + if vars_to_load is None: + return ComponentMap(self._primals.values()) + else: + primals = ComponentMap() + for v in vars_to_load: + primals[v] = self._primals[id(v)][1] + return primals + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + if self._duals is None: + raise RuntimeError( + 'Solution loader does not currently have valid duals. Please ' + 'check the termination condition and ensure the solver returns duals ' + 'for the given problem type.' + ) + if cons_to_load is None: + duals = dict(self._duals) + else: + duals = dict() + for c in cons_to_load: + duals[c] = self._duals[c] + return duals + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + if self._slacks is None: + raise RuntimeError( + 'Solution loader does not currently have valid slacks. Please ' + 'check the termination condition and ensure the solver returns slacks ' + 'for the given problem type.' + ) + if cons_to_load is None: + slacks = dict(self._slacks) + else: + slacks = dict() + for c in cons_to_load: + slacks[c] = self._slacks[c] + return slacks + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + if self._reduced_costs is None: + raise RuntimeError( + 'Solution loader does not currently have valid reduced costs. Please ' + 'check the termination condition and ensure the solver returns reduced ' + 'costs for the given problem type.' + ) + if vars_to_load is None: + rc = ComponentMap(self._reduced_costs.values()) + else: + rc = ComponentMap() + for v in vars_to_load: + rc[v] = self._reduced_costs[id(v)][1] + return rc + + +class Results(object): + """ + Attributes + ---------- + termination_condition: TerminationCondition + The reason the solver exited. This is a member of the + TerminationCondition enum. + best_feasible_objective: float + If a feasible solution was found, this is the objective value of + the best solution found. If no feasible solution was found, this is + None. + best_objective_bound: float + The best objective bound found. For minimization problems, this is + the lower bound. For maximization problems, this is the upper bound. + For solvers that do not provide an objective bound, this should be -inf + (minimization) or inf (maximization) + + Here is an example workflow: + + >>> import pyomo.environ as pe + >>> from pyomo.contrib import appsi + >>> m = pe.ConcreteModel() + >>> m.x = pe.Var() + >>> m.obj = pe.Objective(expr=m.x**2) + >>> opt = appsi.solvers.Ipopt() + >>> opt.config.load_solution = False + >>> results = opt.solve(m) #doctest:+SKIP + >>> if results.termination_condition == appsi.base.TerminationCondition.optimal: #doctest:+SKIP + ... print('optimal solution found: ', results.best_feasible_objective) #doctest:+SKIP + ... results.solution_loader.load_vars() #doctest:+SKIP + ... print('the optimal value of x is ', m.x.value) #doctest:+SKIP + ... elif results.best_feasible_objective is not None: #doctest:+SKIP + ... print('sub-optimal but feasible solution found: ', results.best_feasible_objective) #doctest:+SKIP + ... results.solution_loader.load_vars(vars_to_load=[m.x]) #doctest:+SKIP + ... print('The value of x in the feasible solution is ', m.x.value) #doctest:+SKIP + ... elif results.termination_condition in {appsi.base.TerminationCondition.maxIterations, appsi.base.TerminationCondition.maxTimeLimit}: #doctest:+SKIP + ... print('No feasible solution was found. The best lower bound found was ', results.best_objective_bound) #doctest:+SKIP + ... else: #doctest:+SKIP + ... print('The following termination condition was encountered: ', results.termination_condition) #doctest:+SKIP + """ + + def __init__(self): + self.solution_loader: SolutionLoaderBase = SolutionLoader( + None, None, None, None + ) + self.termination_condition: TerminationCondition = TerminationCondition.unknown + self.best_feasible_objective: Optional[float] = None + self.best_objective_bound: Optional[float] = None + + def __str__(self): + s = '' + s += 'termination_condition: ' + str(self.termination_condition) + '\n' + s += 'best_feasible_objective: ' + str(self.best_feasible_objective) + '\n' + s += 'best_objective_bound: ' + str(self.best_objective_bound) + return s + + +class UpdateConfig(ConfigDict): + """ + Attributes + ---------- + check_for_new_or_removed_constraints: bool + check_for_new_or_removed_vars: bool + check_for_new_or_removed_params: bool + update_constraints: bool + update_vars: bool + update_params: bool + update_named_expressions: bool + """ + + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + if doc is None: + doc = 'Configuration options to detect changes in model between solves' + super(UpdateConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare( + 'check_for_new_or_removed_constraints', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old constraints will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.add_constraints() + and opt.remove_constraints() or when you are certain constraints are not being + added to/removed from the model.""", + ), + ) + self.declare( + 'check_for_new_or_removed_vars', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old variables will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.add_variables() and + opt.remove_variables() or when you are certain variables are not being added to / + removed from the model.""", + ), + ) + self.declare( + 'check_for_new_or_removed_params', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old parameters will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.add_params() and + opt.remove_params() or when you are certain parameters are not being added to / + removed from the model.""", + ), + ) + self.declare( + 'check_for_new_objective', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, new/old objectives will not be automatically detected on subsequent + solves. Use False only when manually updating the solver with opt.set_objective() or + when you are certain objectives are not being added to / removed from the model.""", + ), + ) + self.declare( + 'update_constraints', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to existing constraints will not be automatically detected on + subsequent solves. This includes changes to the lower, body, and upper attributes of + constraints. Use False only when manually updating the solver with + opt.remove_constraints() and opt.add_constraints() or when you are certain constraints + are not being modified.""", + ), + ) + self.declare( + 'update_vars', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to existing variables will not be automatically detected on + subsequent solves. This includes changes to the lb, ub, domain, and fixed + attributes of variables. Use False only when manually updating the solver with + opt.update_variables() or when you are certain variables are not being modified.""", + ), + ) + self.declare( + 'update_params', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to parameter values will not be automatically detected on + subsequent solves. Use False only when manually updating the solver with + opt.update_params() or when you are certain parameters are not being modified.""", + ), + ) + self.declare( + 'update_named_expressions', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to Expressions will not be automatically detected on + subsequent solves. Use False only when manually updating the solver with + opt.remove_constraints() and opt.add_constraints() or when you are certain + Expressions are not being modified.""", + ), + ) + self.declare( + 'update_objective', + ConfigValue( + domain=bool, + default=True, + doc=""" + If False, changes to objectives will not be automatically detected on + subsequent solves. This includes the expr and sense attributes of objectives. Use + False only when manually updating the solver with opt.set_objective() or when you are + certain objectives are not being modified.""", + ), + ) + self.declare( + 'treat_fixed_vars_as_params', + ConfigValue( + domain=bool, + default=True, + doc=""" + This is an advanced option that should only be used in special circumstances. + With the default setting of True, fixed variables will be treated like parameters. + This means that z == x*y will be linear if x or y is fixed and the constraint + can be written to an LP file. If the value of the fixed variable gets changed, we have + to completely reprocess all constraints using that variable. If + treat_fixed_vars_as_params is False, then constraints will be processed as if fixed + variables are not fixed, and the solver will be told the variable is fixed. This means + z == x*y could not be written to an LP file even if x and/or y is fixed. However, + updating the values of fixed variables is much faster this way.""", + ), + ) + + self.check_for_new_or_removed_constraints: bool = True + self.check_for_new_or_removed_vars: bool = True + self.check_for_new_or_removed_params: bool = True + self.check_for_new_objective: bool = True + self.update_constraints: bool = True + self.update_vars: bool = True + self.update_params: bool = True + self.update_named_expressions: bool = True + self.update_objective: bool = True + self.treat_fixed_vars_as_params: bool = True + + +class Solver(abc.ABC): + class Availability(enum.IntEnum): + NotFound = 0 + BadVersion = -1 + BadLicense = -2 + FullLicense = 1 + LimitedLicense = 2 + NeedsCompiledExtension = -3 + + def __bool__(self): + return self._value_ > 0 + + def __format__(self, format_spec): + # We want general formatting of this Enum to return the + # formatted string value and not the int (which is the + # default implementation from IntEnum) + return format(self.name, format_spec) + + def __str__(self): + # Note: Python 3.11 changed the core enums so that the + # "mixin" type for standard enums overrides the behavior + # specified in __format__. We will override str() here to + # preserve the previous behavior + return self.name + + @abc.abstractmethod + def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> Results: + """ + Solve a Pyomo model. + + Parameters + ---------- + model: _BlockData + The Pyomo model to be solved + timer: HierarchicalTimer + An option timer for reporting timing + + Returns + ------- + results: Results + A results object + """ + pass + + @abc.abstractmethod + def available(self): + """Test if the solver is available on this system. + + Nominally, this will return True if the solver interface is + valid and can be used to solve problems and False if it cannot. + + Note that for licensed solvers there are a number of "levels" of + available: depending on the license, the solver may be available + with limitations on problem size or runtime (e.g., 'demo' + vs. 'community' vs. 'full'). In these cases, the solver may + return a subclass of enum.IntEnum, with members that resolve to + True if the solver is available (possibly with limitations). + The Enum may also have multiple members that all resolve to + False indicating the reason why the interface is not available + (not found, bad license, unsupported version, etc). + + Returns + ------- + available: Solver.Availability + An enum that indicates "how available" the solver is. + Note that the enum can be cast to bool, which will + be True if the solver is runable at all and False + otherwise. + """ + pass + + @abc.abstractmethod + def version(self) -> Tuple: + """ + Returns + ------- + version: tuple + A tuple representing the version + """ + + @property + @abc.abstractmethod + def config(self): + """ + An object for configuring solve options. + + Returns + ------- + SolverConfig + An object for configuring pyomo solve options such as the time limit. + These options are mostly independent of the solver. + """ + pass + + @property + @abc.abstractmethod + def symbol_map(self): + pass + + def is_persistent(self): + """ + Returns + ------- + is_persistent: bool + True if the solver is a persistent solver. + """ + return False + + +class PersistentSolver(Solver): + def is_persistent(self): + return True + + def load_vars( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> NoReturn: + """ + Load the solution of the primal variables into the value attribute of the variables. + + Parameters + ---------- + vars_to_load: list + A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution + to all primal variables will be loaded. + """ + for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + @abc.abstractmethod + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + pass + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Declare sign convention in docstring here. + + Parameters + ---------- + cons_to_load: list + A list of the constraints whose duals should be loaded. If cons_to_load is None, then the duals for all + constraints will be loaded. + + Returns + ------- + duals: dict + Maps constraints to dual values + """ + raise NotImplementedError( + '{0} does not support the get_duals method'.format(type(self)) + ) + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + """ + Parameters + ---------- + cons_to_load: list + A list of the constraints whose slacks should be loaded. If cons_to_load is None, then the slacks for all + constraints will be loaded. + + Returns + ------- + slacks: dict + Maps constraints to slack values + """ + raise NotImplementedError( + '{0} does not support the get_slacks method'.format(type(self)) + ) + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + """ + Parameters + ---------- + vars_to_load: list + A list of the variables whose reduced cost should be loaded. If vars_to_load is None, then all reduced costs + will be loaded. + + Returns + ------- + reduced_costs: ComponentMap + Maps variable to reduced cost + """ + raise NotImplementedError( + '{0} does not support the get_reduced_costs method'.format(type(self)) + ) + + @property + @abc.abstractmethod + def update_config(self) -> UpdateConfig: + pass + + @abc.abstractmethod + def set_instance(self, model): + pass + + @abc.abstractmethod + def add_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def add_params(self, params: List[_ParamData]): + pass + + @abc.abstractmethod + def add_constraints(self, cons: List[_GeneralConstraintData]): + pass + + @abc.abstractmethod + def add_block(self, block: _BlockData): + pass + + @abc.abstractmethod + def remove_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def remove_params(self, params: List[_ParamData]): + pass + + @abc.abstractmethod + def remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + @abc.abstractmethod + def remove_block(self, block: _BlockData): + pass + + @abc.abstractmethod + def set_objective(self, obj: _GeneralObjectiveData): + pass + + @abc.abstractmethod + def update_variables(self, variables: List[_GeneralVarData]): + pass + + @abc.abstractmethod + def update_params(self): + pass + + +class PersistentSolutionLoader(SolutionLoaderBase): + def __init__(self, solver: PersistentSolver): + self._solver = solver + self._valid = True + + def _assert_solution_still_valid(self): + if not self._valid: + raise RuntimeError('The results in the solver are no longer valid.') + + def get_primals(self, vars_to_load=None): + self._assert_solution_still_valid() + return self._solver.get_primals(vars_to_load=vars_to_load) + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + self._assert_solution_still_valid() + return self._solver.get_duals(cons_to_load=cons_to_load) + + def get_slacks( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + self._assert_solution_still_valid() + return self._solver.get_slacks(cons_to_load=cons_to_load) + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + self._assert_solution_still_valid() + return self._solver.get_reduced_costs(vars_to_load=vars_to_load) + + def invalidate(self): + self._valid = False + + +""" +What can change in a pyomo model? +- variables added or removed +- constraints added or removed +- objective changed +- objective expr changed +- params added or removed +- variable modified + - lb + - ub + - fixed or unfixed + - domain + - value +- constraint modified + - lower + - upper + - body + - active or not +- named expressions modified + - expr +- param modified + - value + +Ideas: +- Consider explicitly handling deactivated constraints; favor deactivation over removal + and activation over addition + +Notes: +- variable bounds cannot be updated with mutable params; you must call update_variables +""" + + +class PersistentBase(abc.ABC): + def __init__(self, only_child_vars=False): + self._model = None + self._active_constraints = dict() # maps constraint to (lower, body, upper) + self._vars = dict() # maps var id to (var, lb, ub, fixed, domain, value) + self._params = dict() # maps param id to param + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._named_expressions = ( + dict() + ) # maps constraint to list of tuples (named_expr, named_expr.expr) + self._external_functions = ComponentMap() + self._obj_named_expressions = list() + self._update_config = UpdateConfig() + self._referenced_variables = ( + dict() + ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] + self._vars_referenced_by_con = dict() + self._vars_referenced_by_obj = list() + self._expr_types = None + self.use_extensions = False + self._only_child_vars = only_child_vars + + @property + def update_config(self): + return self._update_config + + @update_config.setter + def update_config(self, val: UpdateConfig): + self._update_config = val + + def set_instance(self, model): + saved_update_config = self.update_config + self.__init__(only_child_vars=self._only_child_vars) + self.update_config = saved_update_config + self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() + self.add_block(model) + if self._objective is None: + self.set_objective(None) + + @abc.abstractmethod + def _add_variables(self, variables: List[_GeneralVarData]): + pass + + def add_variables(self, variables: List[_GeneralVarData]): + for v in variables: + if id(v) in self._referenced_variables: + raise ValueError( + 'variable {name} has already been added'.format(name=v.name) + ) + self._referenced_variables[id(v)] = [dict(), dict(), None] + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._add_variables(variables) + + @abc.abstractmethod + def _add_params(self, params: List[_ParamData]): + pass + + def add_params(self, params: List[_ParamData]): + for p in params: + self._params[id(p)] = p + self._add_params(params) + + @abc.abstractmethod + def _add_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def _check_for_new_vars(self, variables: List[_GeneralVarData]): + new_vars = dict() + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + new_vars[v_id] = v + self.add_variables(list(new_vars.values())) + + def _check_to_remove_vars(self, variables: List[_GeneralVarData]): + vars_to_remove = dict() + for v in variables: + v_id = id(v) + ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] + if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: + vars_to_remove[v_id] = v + self.remove_variables(list(vars_to_remove.values())) + + def add_constraints(self, cons: List[_GeneralConstraintData]): + all_fixed_vars = dict() + for con in cons: + if con in self._named_expressions: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = (con.lower, con.body, con.upper) + if self.use_extensions and cmodel_available: + tmp = cmodel.prep_for_repn(con.body, self._expr_types) + else: + tmp = collect_vars_and_named_exprs(con.body) + named_exprs, variables, fixed_vars, external_functions = tmp + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._named_expressions[con] = [(e, e.expr) for e in named_exprs] + if len(external_functions) > 0: + self._external_functions[con] = external_functions + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][0][con] = None + if not self.update_config.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + all_fixed_vars[id(v)] = v + self._add_constraints(cons) + for v in all_fixed_vars.values(): + v.fix() + + @abc.abstractmethod + def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def add_sos_constraints(self, cons: List[_SOSConstraintData]): + for con in cons: + if con in self._vars_referenced_by_con: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = tuple() + variables = con.get_variables() + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._named_expressions[con] = list() + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][1][con] = None + self._add_sos_constraints(cons) + + @abc.abstractmethod + def _set_objective(self, obj: _GeneralObjectiveData): + pass + + def set_objective(self, obj: _GeneralObjectiveData): + if self._objective is not None: + for v in self._vars_referenced_by_obj: + self._referenced_variables[id(v)][2] = None + if not self._only_child_vars: + self._check_to_remove_vars(self._vars_referenced_by_obj) + self._external_functions.pop(self._objective, None) + if obj is not None: + self._objective = obj + self._objective_expr = obj.expr + self._objective_sense = obj.sense + if self.use_extensions and cmodel_available: + tmp = cmodel.prep_for_repn(obj.expr, self._expr_types) + else: + tmp = collect_vars_and_named_exprs(obj.expr) + named_exprs, variables, fixed_vars, external_functions = tmp + if not self._only_child_vars: + self._check_for_new_vars(variables) + self._obj_named_expressions = [(i, i.expr) for i in named_exprs] + if len(external_functions) > 0: + self._external_functions[obj] = external_functions + self._vars_referenced_by_obj = variables + for v in variables: + self._referenced_variables[id(v)][2] = obj + if not self.update_config.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + self._set_objective(obj) + for v in fixed_vars: + v.fix() + else: + self._vars_referenced_by_obj = list() + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._obj_named_expressions = list() + self._set_objective(obj) + + def add_block(self, block): + param_dict = dict() + for p in block.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + param_dict[id(_p)] = _p + self.add_params(list(param_dict.values())) + if self._only_child_vars: + self.add_variables( + list( + dict( + (id(var), var) + for var in block.component_data_objects(Var, descend_into=True) + ).values() + ) + ) + self.add_constraints( + [ + con + for con in block.component_data_objects( + Constraint, descend_into=True, active=True + ) + ] + ) + self.add_sos_constraints( + [ + con + for con in block.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + ] + ) + obj = get_objective(block) + if obj is not None: + self.set_objective(obj) + + @abc.abstractmethod + def _remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def remove_constraints(self, cons: List[_GeneralConstraintData]): + self._remove_constraints(cons) + for con in cons: + if con not in self._named_expressions: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][0].pop(con) + if not self._only_child_vars: + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + self._external_functions.pop(con, None) + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def remove_sos_constraints(self, cons: List[_SOSConstraintData]): + self._remove_sos_constraints(cons) + for con in cons: + if con not in self._vars_referenced_by_con: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][1].pop(con) + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_variables(self, variables: List[_GeneralVarData]): + pass + + def remove_variables(self, variables: List[_GeneralVarData]): + self._remove_variables(variables) + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + raise ValueError( + 'cannot remove variable {name} - it has not been added'.format( + name=v.name + ) + ) + cons_using, sos_using, obj_using = self._referenced_variables[v_id] + if cons_using or sos_using or (obj_using is not None): + raise ValueError( + 'cannot remove variable {name} - it is still being used by constraints or the objective'.format( + name=v.name + ) + ) + del self._referenced_variables[v_id] + del self._vars[v_id] + + @abc.abstractmethod + def _remove_params(self, params: List[_ParamData]): + pass + + def remove_params(self, params: List[_ParamData]): + self._remove_params(params) + for p in params: + del self._params[id(p)] + + def remove_block(self, block): + self.remove_constraints( + [ + con + for con in block.component_data_objects( + ctype=Constraint, descend_into=True, active=True + ) + ] + ) + self.remove_sos_constraints( + [ + con + for con in block.component_data_objects( + ctype=SOSConstraint, descend_into=True, active=True + ) + ] + ) + if self._only_child_vars: + self.remove_variables( + list( + dict( + (id(var), var) + for var in block.component_data_objects( + ctype=Var, descend_into=True + ) + ).values() + ) + ) + self.remove_params( + list( + dict( + (id(p), p) + for p in block.component_data_objects( + ctype=Param, descend_into=True + ) + ).values() + ) + ) + + @abc.abstractmethod + def _update_variables(self, variables: List[_GeneralVarData]): + pass + + def update_variables(self, variables: List[_GeneralVarData]): + for v in variables: + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._update_variables(variables) + + @abc.abstractmethod + def update_params(self): + pass + + def update(self, timer: HierarchicalTimer = None): + if timer is None: + timer = HierarchicalTimer() + config = self.update_config + new_vars = list() + old_vars = list() + new_params = list() + old_params = list() + new_cons = list() + old_cons = list() + old_sos = list() + new_sos = list() + current_vars_dict = dict() + current_cons_dict = dict() + current_sos_dict = dict() + timer.start('vars') + if self._only_child_vars and ( + config.check_for_new_or_removed_vars or config.update_vars + ): + current_vars_dict = { + id(v): v + for v in self._model.component_data_objects(Var, descend_into=True) + } + for v_id, v in current_vars_dict.items(): + if v_id not in self._vars: + new_vars.append(v) + for v_id, v_tuple in self._vars.items(): + if v_id not in current_vars_dict: + old_vars.append(v_tuple[0]) + elif config.update_vars: + start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + timer.stop('vars') + timer.start('params') + if config.check_for_new_or_removed_params: + current_params_dict = dict() + for p in self._model.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + current_params_dict[id(_p)] = _p + for p_id, p in current_params_dict.items(): + if p_id not in self._params: + new_params.append(p) + for p_id, p in self._params.items(): + if p_id not in current_params_dict: + old_params.append(p) + timer.stop('params') + timer.start('cons') + if config.check_for_new_or_removed_constraints or config.update_constraints: + current_cons_dict = { + c: None + for c in self._model.component_data_objects( + Constraint, descend_into=True, active=True + ) + } + current_sos_dict = { + c: None + for c in self._model.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + } + for c in current_cons_dict.keys(): + if c not in self._vars_referenced_by_con: + new_cons.append(c) + for c in current_sos_dict.keys(): + if c not in self._vars_referenced_by_con: + new_sos.append(c) + for c in self._vars_referenced_by_con.keys(): + if c not in current_cons_dict and c not in current_sos_dict: + if (c.ctype is Constraint) or ( + c.ctype is None and isinstance(c, _GeneralConstraintData) + ): + old_cons.append(c) + else: + assert (c.ctype is SOSConstraint) or ( + c.ctype is None and isinstance(c, _SOSConstraintData) + ) + old_sos.append(c) + self.remove_constraints(old_cons) + self.remove_sos_constraints(old_sos) + timer.stop('cons') + timer.start('params') + self.remove_params(old_params) + + # sticking this between removal and addition + # is important so that we don't do unnecessary work + if config.update_params: + self.update_params() + + self.add_params(new_params) + timer.stop('params') + timer.start('vars') + self.add_variables(new_vars) + timer.stop('vars') + timer.start('cons') + self.add_constraints(new_cons) + self.add_sos_constraints(new_sos) + new_cons_set = set(new_cons) + new_sos_set = set(new_sos) + new_vars_set = set(id(v) for v in new_vars) + cons_to_remove_and_add = dict() + need_to_set_objective = False + if config.update_constraints: + cons_to_update = list() + sos_to_update = list() + for c in current_cons_dict.keys(): + if c not in new_cons_set: + cons_to_update.append(c) + for c in current_sos_dict.keys(): + if c not in new_sos_set: + sos_to_update.append(c) + for c in cons_to_update: + lower, body, upper = self._active_constraints[c] + new_lower, new_body, new_upper = c.lower, c.body, c.upper + if new_body is not body: + cons_to_remove_and_add[c] = None + continue + if new_lower is not lower: + if ( + type(new_lower) is NumericConstant + and type(lower) is NumericConstant + and new_lower.value == lower.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + if new_upper is not upper: + if ( + type(new_upper) is NumericConstant + and type(upper) is NumericConstant + and new_upper.value == upper.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + self.remove_sos_constraints(sos_to_update) + self.add_sos_constraints(sos_to_update) + timer.stop('cons') + timer.start('vars') + if self._only_child_vars and config.update_vars: + vars_to_check = list() + for v_id, v in current_vars_dict.items(): + if v_id not in new_vars_set: + vars_to_check.append(v) + elif config.update_vars: + end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] + if config.update_vars: + vars_to_update = list() + for v in vars_to_check: + _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] + if (fixed != v.fixed) or (fixed and (value != v.value)): + vars_to_update.append(v) + if self.update_config.treat_fixed_vars_as_params: + for c in self._referenced_variables[id(v)][0]: + cons_to_remove_and_add[c] = None + if self._referenced_variables[id(v)][2] is not None: + need_to_set_objective = True + elif lb is not v._lb: + vars_to_update.append(v) + elif ub is not v._ub: + vars_to_update.append(v) + elif domain_interval != v.domain.get_interval(): + vars_to_update.append(v) + self.update_variables(vars_to_update) + timer.stop('vars') + timer.start('cons') + cons_to_remove_and_add = list(cons_to_remove_and_add.keys()) + self.remove_constraints(cons_to_remove_and_add) + self.add_constraints(cons_to_remove_and_add) + timer.stop('cons') + timer.start('named expressions') + if config.update_named_expressions: + cons_to_update = list() + for c, expr_list in self._named_expressions.items(): + if c in new_cons_set: + continue + for named_expr, old_expr in expr_list: + if named_expr.expr is not old_expr: + cons_to_update.append(c) + break + self.remove_constraints(cons_to_update) + self.add_constraints(cons_to_update) + for named_expr, old_expr in self._obj_named_expressions: + if named_expr.expr is not old_expr: + need_to_set_objective = True + break + timer.stop('named expressions') + timer.start('objective') + if self.update_config.check_for_new_objective: + pyomo_obj = get_objective(self._model) + if pyomo_obj is not self._objective: + need_to_set_objective = True + else: + pyomo_obj = self._objective + if self.update_config.update_objective: + if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: + need_to_set_objective = True + elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: + # we can definitely do something faster here than resetting the whole objective + need_to_set_objective = True + if need_to_set_objective: + self.set_objective(pyomo_obj) + timer.stop('objective') + + # this has to be done after the objective and constraints in case the + # old objective/constraints use old variables + timer.start('vars') + self.remove_variables(old_vars) + timer.stop('vars') + + +legacy_termination_condition_map = { + TerminationCondition.unknown: LegacyTerminationCondition.unknown, + TerminationCondition.maxTimeLimit: LegacyTerminationCondition.maxTimeLimit, + TerminationCondition.maxIterations: LegacyTerminationCondition.maxIterations, + TerminationCondition.objectiveLimit: LegacyTerminationCondition.minFunctionValue, + TerminationCondition.minStepLength: LegacyTerminationCondition.minStepLength, + TerminationCondition.optimal: LegacyTerminationCondition.optimal, + TerminationCondition.unbounded: LegacyTerminationCondition.unbounded, + TerminationCondition.infeasible: LegacyTerminationCondition.infeasible, + TerminationCondition.infeasibleOrUnbounded: LegacyTerminationCondition.infeasibleOrUnbounded, + TerminationCondition.error: LegacyTerminationCondition.error, + TerminationCondition.interrupted: LegacyTerminationCondition.resourceInterrupt, + TerminationCondition.licensingProblems: LegacyTerminationCondition.licensingProblems, +} + + +legacy_solver_status_map = { + TerminationCondition.unknown: LegacySolverStatus.unknown, + TerminationCondition.maxTimeLimit: LegacySolverStatus.aborted, + TerminationCondition.maxIterations: LegacySolverStatus.aborted, + TerminationCondition.objectiveLimit: LegacySolverStatus.aborted, + TerminationCondition.minStepLength: LegacySolverStatus.error, + TerminationCondition.optimal: LegacySolverStatus.ok, + TerminationCondition.unbounded: LegacySolverStatus.error, + TerminationCondition.infeasible: LegacySolverStatus.error, + TerminationCondition.infeasibleOrUnbounded: LegacySolverStatus.error, + TerminationCondition.error: LegacySolverStatus.error, + TerminationCondition.interrupted: LegacySolverStatus.aborted, + TerminationCondition.licensingProblems: LegacySolverStatus.error, +} + + +legacy_solution_status_map = { + TerminationCondition.unknown: LegacySolutionStatus.unknown, + TerminationCondition.maxTimeLimit: LegacySolutionStatus.stoppedByLimit, + TerminationCondition.maxIterations: LegacySolutionStatus.stoppedByLimit, + TerminationCondition.objectiveLimit: LegacySolutionStatus.stoppedByLimit, + TerminationCondition.minStepLength: LegacySolutionStatus.error, + TerminationCondition.optimal: LegacySolutionStatus.optimal, + TerminationCondition.unbounded: LegacySolutionStatus.unbounded, + TerminationCondition.infeasible: LegacySolutionStatus.infeasible, + TerminationCondition.infeasibleOrUnbounded: LegacySolutionStatus.unsure, + TerminationCondition.error: LegacySolutionStatus.error, + TerminationCondition.interrupted: LegacySolutionStatus.error, + TerminationCondition.licensingProblems: LegacySolutionStatus.error, +} + + +class LegacySolverInterface(object): + def solve( + self, + model: _BlockData, + tee: bool = False, + load_solutions: bool = True, + logfile: Optional[str] = None, + solnfile: Optional[str] = None, + timelimit: Optional[float] = None, + report_timing: bool = False, + solver_io: Optional[str] = None, + suffixes: Optional[Sequence] = None, + options: Optional[Dict] = None, + keepfiles: bool = False, + symbolic_solver_labels: bool = False, + ): + original_config = self.config + self.config = self.config() + self.config.stream_solver = tee + self.config.load_solution = load_solutions + self.config.symbolic_solver_labels = symbolic_solver_labels + self.config.time_limit = timelimit + self.config.report_timing = report_timing + if solver_io is not None: + raise NotImplementedError('Still working on this') + if suffixes is not None: + raise NotImplementedError('Still working on this') + if logfile is not None: + raise NotImplementedError('Still working on this') + if 'keepfiles' in self.config: + self.config.keepfiles = keepfiles + if solnfile is not None: + if 'filename' in self.config: + filename = os.path.splitext(solnfile)[0] + self.config.filename = filename + original_options = self.options + if options is not None: + self.options = options + + results: Results = super(LegacySolverInterface, self).solve(model) + + legacy_results = LegacySolverResults() + legacy_soln = LegacySolution() + legacy_results.solver.status = legacy_solver_status_map[ + results.termination_condition + ] + legacy_results.solver.termination_condition = legacy_termination_condition_map[ + results.termination_condition + ] + legacy_soln.status = legacy_solution_status_map[results.termination_condition] + legacy_results.solver.termination_message = str(results.termination_condition) + + obj = get_objective(model) + legacy_results.problem.sense = obj.sense + + if obj.sense == minimize: + legacy_results.problem.lower_bound = results.best_objective_bound + legacy_results.problem.upper_bound = results.best_feasible_objective + else: + legacy_results.problem.upper_bound = results.best_objective_bound + legacy_results.problem.lower_bound = results.best_feasible_objective + if ( + results.best_feasible_objective is not None + and results.best_objective_bound is not None + ): + legacy_soln.gap = abs( + results.best_feasible_objective - results.best_objective_bound + ) + else: + legacy_soln.gap = None + + symbol_map = SymbolMap() + symbol_map.byObject = dict(self.symbol_map.byObject) + symbol_map.bySymbol = dict(self.symbol_map.bySymbol) + symbol_map.aliases = dict(self.symbol_map.aliases) + symbol_map.default_labeler = self.symbol_map.default_labeler + model.solutions.add_symbol_map(symbol_map) + legacy_results._smap_id = id(symbol_map) + + delete_legacy_soln = True + if load_solutions: + if hasattr(model, 'dual') and model.dual.import_enabled(): + for c, val in results.solution_loader.get_duals().items(): + model.dual[c] = val + if hasattr(model, 'slack') and model.slack.import_enabled(): + for c, val in results.solution_loader.get_slacks().items(): + model.slack[c] = val + if hasattr(model, 'rc') and model.rc.import_enabled(): + for v, val in results.solution_loader.get_reduced_costs().items(): + model.rc[v] = val + elif results.best_feasible_objective is not None: + delete_legacy_soln = False + for v, val in results.solution_loader.get_primals().items(): + legacy_soln.variable[symbol_map.getSymbol(v)] = {'Value': val} + if hasattr(model, 'dual') and model.dual.import_enabled(): + for c, val in results.solution_loader.get_duals().items(): + legacy_soln.constraint[symbol_map.getSymbol(c)] = {'Dual': val} + if hasattr(model, 'slack') and model.slack.import_enabled(): + for c, val in results.solution_loader.get_slacks().items(): + symbol = symbol_map.getSymbol(c) + if symbol in legacy_soln.constraint: + legacy_soln.constraint[symbol]['Slack'] = val + if hasattr(model, 'rc') and model.rc.import_enabled(): + for v, val in results.solution_loader.get_reduced_costs().items(): + legacy_soln.variable['Rc'] = val + + legacy_results.solution.insert(legacy_soln) + if delete_legacy_soln: + legacy_results.solution.delete(0) + + self.config = original_config + self.options = original_options + + return legacy_results + + def available(self, exception_flag=True): + ans = super(LegacySolverInterface, self).available() + if exception_flag and not ans: + raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).') + return bool(ans) + + def license_is_valid(self) -> bool: + """Test if the solver license is valid on this system. + + Note that this method is included for compatibility with the + legacy SolverFactory interface. Unlicensed or open source + solvers will return True by definition. Licensed solvers will + return True if a valid license is found. + + Returns + ------- + available: bool + True if the solver license is valid. Otherwise, False. + + """ + return bool(self.available()) + + @property + def options(self): + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + if hasattr(self, solver_name + '_options'): + return getattr(self, solver_name + '_options') + raise NotImplementedError('Could not find the correct options') + + @options.setter + def options(self, val): + found = False + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + if hasattr(self, solver_name + '_options'): + setattr(self, solver_name + '_options', val) + found = True + if not found: + raise NotImplementedError('Could not find the correct options') + + def __enter__(self): + return self + + def __exit__(self, t, v, traceback): + pass + + +class SolverFactoryClass(Factory): + def register(self, name, doc=None): + def decorator(cls): + self._cls[name] = cls + self._doc[name] = doc + + class LegacySolver(LegacySolverInterface, cls): + pass + + LegacySolverFactory.register(name, doc)(LegacySolver) + + return cls + + return decorator + + +SolverFactory = SolverFactoryClass() diff --git a/pyomo/contrib/appsi/build.py b/pyomo/contrib/appsi/build.py index c00da19eae8..2c8d02dd3ac 100644 --- a/pyomo/contrib/appsi/build.py +++ b/pyomo/contrib/appsi/build.py @@ -9,9 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import errno import shutil -import stat import glob import os import sys @@ -81,7 +79,7 @@ def run(self): print("Building in '%s'" % tmpdir) os.chdir(tmpdir) try: - super().run() + super(appsi_build_ext, self).run() if not self.inplace: library = glob.glob("build/*/appsi_cmodel.*")[0] target = os.path.join( @@ -118,7 +116,7 @@ def run(self): pybind11.setup_helpers.MACOS = original_pybind11_setup_helpers_macos -class AppsiBuilder: +class AppsiBuilder(object): def __call__(self, parallel): return build_appsi() diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index 15c3fcb2058..de22d28e0a4 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -1,7 +1,6 @@ import pyomo.environ as pe from pyomo.contrib import appsi from pyomo.common.timing import HierarchicalTimer -from pyomo.contrib.solver import results def main(plot=True, n_points=200): @@ -17,7 +16,7 @@ def main(plot=True, n_points=200): m.c1 = pe.Constraint(expr=m.y >= (m.x + 1) ** 2) m.c2 = pe.Constraint(expr=m.y >= (m.x - m.p) ** 2) - opt = appsi.solvers.Ipopt() # create an APPSI solver interface + opt = appsi.solvers.Cplex() # create an APPSI solver interface opt.config.load_solution = False # modify the config options # change how automatic updates are handled opt.update_config.check_for_new_or_removed_vars = False @@ -25,18 +24,15 @@ def main(plot=True, n_points=200): # write a for loop to vary the value of parameter p from 1 to 10 p_values = [float(i) for i in np.linspace(1, 10, n_points)] - obj_values = [] - x_values = [] + obj_values = list() + x_values = list() timer = HierarchicalTimer() # create a timer for some basic profiling timer.start('p loop') for p_val in p_values: m.p.value = p_val res = opt.solve(m, timer=timer) - assert ( - res.termination_condition - == results.TerminationCondition.convergenceCriteriaSatisfied - ) - obj_values.append(res.incumbent_objective) + assert res.termination_condition == appsi.base.TerminationCondition.optimal + obj_values.append(res.best_feasible_objective) opt.load_vars([m.x]) x_values.append(m.x.value) timer.stop('p loop') diff --git a/pyomo/contrib/appsi/examples/tests/test_examples.py b/pyomo/contrib/appsi/examples/tests/test_examples.py index 7c577366c41..d2c88224a7d 100644 --- a/pyomo/contrib/appsi/examples/tests/test_examples.py +++ b/pyomo/contrib/appsi/examples/tests/test_examples.py @@ -1,17 +1,18 @@ from pyomo.contrib.appsi.examples import getting_started -from pyomo.common import unittest -from pyomo.common.dependencies import attempt_import +import pyomo.common.unittest as unittest +import pyomo.environ as pe from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.contrib import appsi -numpy, numpy_available = attempt_import('numpy') - @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') -@unittest.skipUnless(numpy_available, 'numpy is not available') class TestExamples(unittest.TestCase): def test_getting_started(self): - opt = appsi.solvers.Ipopt() + try: + import numpy as np + except: + raise unittest.SkipTest('numpy is not available') + opt = appsi.solvers.Cplex() if not opt.available(): - raise unittest.SkipTest('ipopt is not available') + raise unittest.SkipTest('cplex is not available') getting_started.main(plot=False, n_points=10) diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index ccbb3819554..92a0e0c8cbc 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -1,4 +1,4 @@ -from pyomo.contrib.solver.util import PersistentSolverUtils +from pyomo.contrib.appsi.base import PersistentBase from pyomo.common.config import ( ConfigDict, ConfigValue, @@ -11,9 +11,10 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData, minimize +from pyomo.core.base.objective import _GeneralObjectiveData, minimize, maximize from pyomo.core.base.block import _BlockData from pyomo.core.base import SymbolMap, TextLabeler +from pyomo.common.errors import InfeasibleConstraintException class IntervalConfig(ConfigDict): @@ -34,7 +35,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super().__init__( + super(IntervalConfig, self).__init__( description=description, doc=doc, implicit=implicit, @@ -59,16 +60,16 @@ def __init__( ) -class IntervalTightener(PersistentSolverUtils): +class IntervalTightener(PersistentBase): def __init__(self): - super().__init__() + super(IntervalTightener, self).__init__() self._config = IntervalConfig() self._cmodel = None - self._var_map = {} - self._con_map = {} - self._param_map = {} - self._rvar_map = {} - self._rcon_map = {} + self._var_map = dict() + self._con_map = dict() + self._param_map = dict() + self._rvar_map = dict() + self._rcon_map = dict() self._pyomo_expr_types = cmodel.PyomoExprTypes() self._symbolic_solver_labels: bool = False self._symbol_map = SymbolMap() @@ -253,7 +254,7 @@ def _update_pyomo_var_bounds(self): self._vars[v_id] = (_v, _lb, cv_ub, _fixed, _domain, _value) def _deactivate_satisfied_cons(self): - cons_to_deactivate = [] + cons_to_deactivate = list() if self.config.deactivate_satisfied_constraints: for c, cc in self._con_map.items(): if not cc.active: diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index ebccba09ab2..5333158239e 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,5 +1,5 @@ from pyomo.common.extensions import ExtensionBuilderFactory -from pyomo.contrib.solver.factory import SolverFactory +from .base import SolverFactory from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs from .build import AppsiBuilder diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 141c6de57bd..a3aae2a9213 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -1,16 +1,20 @@ -import logging -import math -import subprocess -import sys -from typing import Optional, Sequence, Dict, List, Mapping - - from pyomo.common.tempfiles import TempfileManager from pyomo.common.fileutils import Executable +from pyomo.contrib.appsi.base import ( + PersistentSolver, + Results, + TerminationCondition, + SolverConfig, + PersistentSolutionLoader, +) from pyomo.contrib.appsi.writers import LPWriter from pyomo.common.log import LogStream +import logging +import subprocess from pyomo.core.kernel.objective import minimize, maximize +import math from pyomo.common.collections import ComponentMap +from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.block import _BlockData @@ -18,14 +22,12 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream +import sys +from typing import Dict from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.solver.base import PersistentSolverBase -from pyomo.contrib.solver.config import SolverConfig -from pyomo.contrib.solver.results import TerminationCondition, Results -from pyomo.contrib.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) @@ -40,7 +42,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super().__init__( + super(CbcConfig, self).__init__( description=description, doc=doc, implicit=implicit, @@ -61,15 +63,15 @@ def __init__( self.log_level = logging.INFO -class Cbc(PersistentSolverBase): +class Cbc(PersistentSolver): def __init__(self, only_child_vars=False): self._config = CbcConfig() - self._solver_options = {} + self._solver_options = dict() self._writer = LPWriter(only_child_vars=only_child_vars) self._filename = None - self._dual_sol = {} - self._primal_sol = {} - self._reduced_costs = {} + self._dual_sol = dict() + self._primal_sol = dict() + self._reduced_costs = dict() self._last_results_object: Optional[Results] = None def available(self): @@ -230,19 +232,17 @@ def _parse_soln(self): termination_line = all_lines[0].lower() obj_val = None if termination_line.startswith('optimal'): - results.termination_condition = ( - TerminationCondition.convergenceCriteriaSatisfied - ) + results.termination_condition = TerminationCondition.optimal obj_val = float(termination_line.split()[-1]) elif 'infeasible' in termination_line: - results.termination_condition = TerminationCondition.provenInfeasible + results.termination_condition = TerminationCondition.infeasible elif 'unbounded' in termination_line: results.termination_condition = TerminationCondition.unbounded elif termination_line.startswith('stopped on time'): results.termination_condition = TerminationCondition.maxTimeLimit obj_val = float(termination_line.split()[-1]) elif termination_line.startswith('stopped on iterations'): - results.termination_condition = TerminationCondition.iterationLimit + results.termination_condition = TerminationCondition.maxIterations obj_val = float(termination_line.split()[-1]) else: results.termination_condition = TerminationCondition.unknown @@ -261,9 +261,9 @@ def _parse_soln(self): first_var_line = ndx last_var_line = len(all_lines) - 1 - self._dual_sol = {} - self._primal_sol = {} - self._reduced_costs = {} + self._dual_sol = dict() + self._primal_sol = dict() + self._reduced_costs = dict() symbol_map = self._writer.symbol_map @@ -307,30 +307,26 @@ def _parse_soln(self): self._reduced_costs[v_id] = (v, -rc_val) if ( - results.termination_condition - == TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition == TerminationCondition.optimal and self.config.load_solution ): for v_id, (v, val) in self._primal_sol.items(): v.set_value(val, skip_validation=True) if self._writer.get_active_objective() is None: - results.incumbent_objective = None + results.best_feasible_objective = None else: - results.incumbent_objective = obj_val - elif ( - results.termination_condition - == TerminationCondition.convergenceCriteriaSatisfied - ): + results.best_feasible_objective = obj_val + elif results.termination_condition == TerminationCondition.optimal: if self._writer.get_active_objective() is None: - results.incumbent_objective = None + results.best_feasible_objective = None else: - results.incumbent_objective = obj_val + results.best_feasible_objective = obj_val elif self.config.load_solution: raise RuntimeError( 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) return results @@ -366,7 +362,7 @@ def _check_and_escape_options(): yield tmp_k, tmp_v cmd = [str(config.executable)] - action_options = [] + action_options = list() if config.time_limit is not None: cmd.extend(['-sec', str(config.time_limit)]) cmd.extend(['-timeMode', 'elapsed']) @@ -387,7 +383,7 @@ def _check_and_escape_options(): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.tee: + if self.config.stream_solver: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: @@ -407,24 +403,24 @@ def _check_and_escape_options(): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) results = Results() results.termination_condition = TerminationCondition.error - results.incumbent_objective = None + results.best_feasible_objective = None else: timer.start('parse solution') results = self._parse_soln() timer.stop('parse solution') if self._writer.get_active_objective() is None: - results.incumbent_objective = None - results.objective_bound = None + results.best_feasible_objective = None + results.best_objective_bound = None else: if self._writer.get_active_objective().sense == minimize: - results.objective_bound = -math.inf + results.best_objective_bound = -math.inf else: - results.objective_bound = math.inf + results.best_objective_bound = math.inf results.solution_loader = PersistentSolutionLoader(solver=self) @@ -435,7 +431,7 @@ def get_primals( ) -> Mapping[_GeneralVarData, float]: if ( self._last_results_object is None - or self._last_results_object.incumbent_objective is None + or self._last_results_object.best_feasible_objective is None ): raise RuntimeError( 'Solver does not currently have a valid solution. Please ' @@ -455,7 +451,7 @@ def get_duals(self, cons_to_load=None): if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.convergenceCriteriaSatisfied + != TerminationCondition.optimal ): raise RuntimeError( 'Solver does not currently have valid duals. Please ' @@ -473,7 +469,7 @@ def get_reduced_costs( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.convergenceCriteriaSatisfied + != TerminationCondition.optimal ): raise RuntimeError( 'Solver does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 6f02ac12eb1..f03bee6ecc5 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -1,34 +1,35 @@ -import logging -import math -import sys -import time -from typing import Optional, Sequence, Dict, List, Mapping - - from pyomo.common.tempfiles import TempfileManager +from pyomo.contrib.appsi.base import ( + PersistentSolver, + Results, + TerminationCondition, + MIPSolverConfig, + PersistentSolutionLoader, +) from pyomo.contrib.appsi.writers import LPWriter -from pyomo.common.log import LogStream +import logging +import math from pyomo.common.collections import ComponentMap +from typing import Optional, Sequence, NoReturn, List, Mapping, Dict from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.block import _BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer +import sys +import time +from pyomo.common.log import LogStream from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.errors import PyomoException from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.solver.base import PersistentSolverBase -from pyomo.contrib.solver.config import BranchAndBoundConfig -from pyomo.contrib.solver.results import TerminationCondition, Results -from pyomo.contrib.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) -class CplexConfig(BranchAndBoundConfig): +class CplexConfig(MIPSolverConfig): def __init__( self, description=None, @@ -37,7 +38,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super().__init__( + super(CplexConfig, self).__init__( description=description, doc=doc, implicit=implicit, @@ -58,17 +59,17 @@ def __init__( class CplexResults(Results): def __init__(self, solver): - super().__init__() - self.timing_info.wall_time = None + super(CplexResults, self).__init__() + self.wallclock_time = None self.solution_loader = PersistentSolutionLoader(solver=solver) -class Cplex(PersistentSolverBase): +class Cplex(PersistentSolver): _available = None def __init__(self, only_child_vars=False): self._config = CplexConfig() - self._solver_options = {} + self._solver_options = dict() self._writer = LPWriter(only_child_vars=only_child_vars) self._filename = None self._last_results_object: Optional[CplexResults] = None @@ -244,7 +245,7 @@ def _apply_solver(self, timer: HierarchicalTimer): log_stream = LogStream( level=self.config.log_level, logger=self.config.solver_output_logger ) - if config.tee: + if config.stream_solver: def _process_stream(arg): sys.stdout.write(arg) @@ -263,8 +264,8 @@ def _process_stream(arg): if config.time_limit is not None: cplex_model.parameters.timelimit.set(config.time_limit) - if config.rel_gap is not None: - cplex_model.parameters.mip.tolerances.mipgap.set(config.rel_gap) + if config.mip_gap is not None: + cplex_model.parameters.mip.tolerances.mipgap.set(config.mip_gap) timer.start('cplex solve') t0 = time.time() @@ -279,46 +280,52 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): cpxprob = self._cplex_model results = CplexResults(solver=self) - results.timing_info.wall_time = solve_time + results.wallclock_time = solve_time status = cpxprob.solution.get_status() if status in [1, 101, 102]: - results.termination_condition = ( - TerminationCondition.convergenceCriteriaSatisfied - ) + results.termination_condition = TerminationCondition.optimal elif status in [2, 40, 118, 133, 134]: results.termination_condition = TerminationCondition.unbounded elif status in [4, 119, 134]: results.termination_condition = TerminationCondition.infeasibleOrUnbounded elif status in [3, 103]: - results.termination_condition = TerminationCondition.provenInfeasible + results.termination_condition = TerminationCondition.infeasible elif status in [10]: - results.termination_condition = TerminationCondition.iterationLimit + results.termination_condition = TerminationCondition.maxIterations elif status in [11, 25, 107, 131]: results.termination_condition = TerminationCondition.maxTimeLimit else: results.termination_condition = TerminationCondition.unknown if self._writer.get_active_objective() is None: - results.incumbent_objective = None - results.objective_bound = None + results.best_feasible_objective = None + results.best_objective_bound = None else: if cpxprob.solution.get_solution_type() != cpxprob.solution.type.none: if ( cpxprob.variables.get_num_binary() + cpxprob.variables.get_num_integer() ) == 0: - results.incumbent_objective = cpxprob.solution.get_objective_value() - results.objective_bound = cpxprob.solution.get_objective_value() + results.best_feasible_objective = ( + cpxprob.solution.get_objective_value() + ) + results.best_objective_bound = ( + cpxprob.solution.get_objective_value() + ) else: - results.incumbent_objective = cpxprob.solution.get_objective_value() - results.objective_bound = cpxprob.solution.MIP.get_best_objective() + results.best_feasible_objective = ( + cpxprob.solution.get_objective_value() + ) + results.best_objective_bound = ( + cpxprob.solution.MIP.get_best_objective() + ) else: - results.incumbent_objective = None + results.best_feasible_objective = None if cpxprob.objective.get_sense() == cpxprob.objective.sense.minimize: - results.objective_bound = -math.inf + results.best_objective_bound = -math.inf else: - results.objective_bound = math.inf + results.best_objective_bound = math.inf if config.load_solution: if cpxprob.solution.get_solution_type() == cpxprob.solution.type.none: @@ -326,13 +333,10 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): 'A feasible solution was not found, so no solution can be loades. ' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) else: - if ( - results.termination_condition - != TerminationCondition.convergenceCriteriaSatisfied - ): + if results.termination_condition != TerminationCondition.optimal: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' @@ -396,7 +400,7 @@ def get_duals( con_names = self._cplex_model.linear_constraints.get_names() dual_values = self._cplex_model.solution.get_dual_values() else: - con_names = [] + con_names = list() for con in cons_to_load: orig_name = symbol_map.byObject[id(con)] if con.equality: @@ -408,7 +412,7 @@ def get_duals( con_names.append(orig_name + '_ub') dual_values = self._cplex_model.solution.get_dual_values(con_names) - res = {} + res = dict() for name, val in zip(con_names, dual_values): orig_name = name[:-3] if orig_name == 'obj_const_con': diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index a947c8d7d7d..a173c69abc6 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -1,9 +1,7 @@ from collections.abc import Iterable import logging import math -import sys from typing import List, Dict, Optional - from pyomo.common.collections import ComponentSet, ComponentMap, OrderedSet from pyomo.common.log import LogStream from pyomo.common.dependencies import attempt_import @@ -14,20 +12,24 @@ from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import Var, _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression +from pyomo.contrib.appsi.base import ( + PersistentSolver, + Results, + TerminationCondition, + MIPSolverConfig, + PersistentBase, + PersistentSolutionLoader, +) +from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.solver.base import PersistentSolverBase -from pyomo.contrib.solver.config import BranchAndBoundConfig -from pyomo.contrib.solver.results import TerminationCondition, Results -from pyomo.contrib.solver.solution import PersistentSolutionLoader -from pyomo.contrib.solver.util import PersistentSolverUtils - +import sys logger = logging.getLogger(__name__) @@ -51,7 +53,7 @@ class DegreeError(PyomoException): pass -class GurobiConfig(BranchAndBoundConfig): +class GurobiConfig(MIPSolverConfig): def __init__( self, description=None, @@ -60,7 +62,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super().__init__( + super(GurobiConfig, self).__init__( description=description, doc=doc, implicit=implicit, @@ -93,12 +95,12 @@ def get_primals(self, vars_to_load=None, solution_number=0): class GurobiResults(Results): def __init__(self, solver): - super().__init__() - self.timing_info.wall_time = None + super(GurobiResults, self).__init__() + self.wallclock_time = None self.solution_loader = GurobiSolutionLoader(solver=solver) -class _MutableLowerBound: +class _MutableLowerBound(object): def __init__(self, expr): self.var = None self.expr = expr @@ -107,7 +109,7 @@ def update(self): self.var.setAttr('lb', value(self.expr)) -class _MutableUpperBound: +class _MutableUpperBound(object): def __init__(self, expr): self.var = None self.expr = expr @@ -116,7 +118,7 @@ def update(self): self.var.setAttr('ub', value(self.expr)) -class _MutableLinearCoefficient: +class _MutableLinearCoefficient(object): def __init__(self): self.expr = None self.var = None @@ -127,7 +129,7 @@ def update(self): self.gurobi_model.chgCoeff(self.con, self.var, value(self.expr)) -class _MutableRangeConstant: +class _MutableRangeConstant(object): def __init__(self): self.lhs_expr = None self.rhs_expr = None @@ -143,7 +145,7 @@ def update(self): slack.ub = rhs_val - lhs_val -class _MutableConstant: +class _MutableConstant(object): def __init__(self): self.expr = None self.con = None @@ -152,7 +154,7 @@ def update(self): self.con.rhs = value(self.expr) -class _MutableQuadraticConstraint: +class _MutableQuadraticConstraint(object): def __init__( self, gurobi_model, gurobi_con, constant, linear_coefs, quadratic_coefs ): @@ -187,7 +189,7 @@ def get_updated_rhs(self): return value(self.constant.expr) -class _MutableObjective: +class _MutableObjective(object): def __init__(self, gurobi_model, constant, linear_coefs, quadratic_coefs): self.gurobi_model = gurobi_model self.constant = constant @@ -215,14 +217,14 @@ def get_updated_expression(self): return gurobi_expr -class _MutableQuadraticCoefficient: +class _MutableQuadraticCoefficient(object): def __init__(self): self.expr = None self.var1 = None self.var2 = None -class Gurobi(PersistentSolverUtils, PersistentSolverBase): +class Gurobi(PersistentBase, PersistentSolver): """ Interface to Gurobi """ @@ -231,21 +233,21 @@ class Gurobi(PersistentSolverUtils, PersistentSolverBase): _num_instances = 0 def __init__(self, only_child_vars=False): - super().__init__(only_child_vars=only_child_vars) + super(Gurobi, self).__init__(only_child_vars=only_child_vars) self._num_instances += 1 self._config = GurobiConfig() - self._solver_options = {} + self._solver_options = dict() self._solver_model = None self._symbol_map = SymbolMap() self._labeler = None - self._pyomo_var_to_solver_var_map = {} - self._pyomo_con_to_solver_con_map = {} - self._solver_con_to_pyomo_con_map = {} - self._pyomo_sos_to_solver_sos_map = {} + self._pyomo_var_to_solver_var_map = dict() + self._pyomo_con_to_solver_con_map = dict() + self._solver_con_to_pyomo_con_map = dict() + self._pyomo_sos_to_solver_sos_map = dict() self._range_constraints = OrderedSet() - self._mutable_helpers = {} - self._mutable_bounds = {} - self._mutable_quadratic_helpers = {} + self._mutable_helpers = dict() + self._mutable_bounds = dict() + self._mutable_quadratic_helpers = dict() self._mutable_objective = None self._needs_updated = True self._callback = None @@ -351,7 +353,7 @@ def _solve(self, timer: HierarchicalTimer): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.tee: + if self.config.stream_solver: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: @@ -364,8 +366,8 @@ def _solve(self, timer: HierarchicalTimer): if config.time_limit is not None: self._solver_model.setParam('TimeLimit', config.time_limit) - if config.rel_gap is not None: - self._solver_model.setParam('MIPGap', config.rel_gap) + if config.mip_gap is not None: + self._solver_model.setParam('MIPGap', config.mip_gap) for key, option in options.items(): self._solver_model.setParam(key, option) @@ -446,12 +448,12 @@ def _process_domain_and_bounds( return lb, ub, vtype def _add_variables(self, variables: List[_GeneralVarData]): - var_names = [] - vtypes = [] - lbs = [] - ubs = [] - mutable_lbs = {} - mutable_ubs = {} + var_names = list() + vtypes = list() + lbs = list() + ubs = list() + mutable_lbs = dict() + mutable_ubs = dict() for ndx, var in enumerate(variables): varname = self._symbol_map.getSymbol(var, self._labeler) lb, ub, vtype = self._process_domain_and_bounds( @@ -499,6 +501,8 @@ def set_instance(self, model): ) self._reinit() self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() if self.config.symbolic_solver_labels: self._labeler = TextLabeler() @@ -515,8 +519,8 @@ def set_instance(self, model): self.set_objective(None) def _get_expr_from_pyomo_expr(self, expr): - mutable_linear_coefficients = [] - mutable_quadratic_coefficients = [] + mutable_linear_coefficients = list() + mutable_quadratic_coefficients = list() repn = generate_standard_repn(expr, quadratic=True, compute_values=False) degree = repn.polynomial_degree() @@ -526,7 +530,7 @@ def _get_expr_from_pyomo_expr(self, expr): ) if len(repn.linear_vars) > 0: - linear_coef_vals = [] + linear_coef_vals = list() for ndx, coef in enumerate(repn.linear_coefs): if not is_constant(coef): mutable_linear_coefficient = _MutableLinearCoefficient() @@ -820,8 +824,8 @@ def _set_objective(self, obj): sense = gurobipy.GRB.MINIMIZE gurobi_expr = 0 repn_constant = 0 - mutable_linear_coefficients = [] - mutable_quadratic_coefficients = [] + mutable_linear_coefficients = list() + mutable_quadratic_coefficients = list() else: if obj.sense == minimize: sense = gurobipy.GRB.MINIMIZE @@ -865,16 +869,14 @@ def _postsolve(self, timer: HierarchicalTimer): status = gprob.Status results = GurobiResults(self) - results.timing_info.wall_time = gprob.Runtime + results.wallclock_time = gprob.Runtime if status == grb.LOADED: # problem is loaded, but no solution results.termination_condition = TerminationCondition.unknown elif status == grb.OPTIMAL: # optimal - results.termination_condition = ( - TerminationCondition.convergenceCriteriaSatisfied - ) + results.termination_condition = TerminationCondition.optimal elif status == grb.INFEASIBLE: - results.termination_condition = TerminationCondition.provenInfeasible + results.termination_condition = TerminationCondition.infeasible elif status == grb.INF_OR_UNBD: results.termination_condition = TerminationCondition.infeasibleOrUnbounded elif status == grb.UNBOUNDED: @@ -882,9 +884,9 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == grb.CUTOFF: results.termination_condition = TerminationCondition.objectiveLimit elif status == grb.ITERATION_LIMIT: - results.termination_condition = TerminationCondition.iterationLimit + results.termination_condition = TerminationCondition.maxIterations elif status == grb.NODE_LIMIT: - results.termination_condition = TerminationCondition.iterationLimit + results.termination_condition = TerminationCondition.maxIterations elif status == grb.TIME_LIMIT: results.termination_condition = TerminationCondition.maxTimeLimit elif status == grb.SOLUTION_LIMIT: @@ -900,33 +902,30 @@ def _postsolve(self, timer: HierarchicalTimer): else: results.termination_condition = TerminationCondition.unknown - results.incumbent_objective = None - results.objective_bound = None + results.best_feasible_objective = None + results.best_objective_bound = None if self._objective is not None: try: - results.incumbent_objective = gprob.ObjVal + results.best_feasible_objective = gprob.ObjVal except (gurobipy.GurobiError, AttributeError): - results.incumbent_objective = None + results.best_feasible_objective = None try: - results.objective_bound = gprob.ObjBound + results.best_objective_bound = gprob.ObjBound except (gurobipy.GurobiError, AttributeError): if self._objective.sense == minimize: - results.objective_bound = -math.inf + results.best_objective_bound = -math.inf else: - results.objective_bound = math.inf + results.best_objective_bound = math.inf - if results.incumbent_objective is not None and not math.isfinite( - results.incumbent_objective + if results.best_feasible_objective is not None and not math.isfinite( + results.best_feasible_objective ): - results.incumbent_objective = None + results.best_feasible_objective = None timer.start('load solution') if config.load_solution: if gprob.SolCount > 0: - if ( - results.termination_condition - != TerminationCondition.convergenceCriteriaSatisfied - ): + if results.termination_condition != TerminationCondition.optimal: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' @@ -939,7 +938,7 @@ def _postsolve(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) timer.stop('load solution') @@ -1048,7 +1047,7 @@ def get_duals(self, cons_to_load=None): con_map = self._pyomo_con_to_solver_con_map reverse_con_map = self._solver_con_to_pyomo_con_map - dual = {} + dual = dict() if cons_to_load is None: linear_cons_to_load = self._solver_model.getConstrs() @@ -1091,7 +1090,7 @@ def get_slacks(self, cons_to_load=None): con_map = self._pyomo_con_to_solver_con_map reverse_con_map = self._solver_con_to_pyomo_con_map - slack = {} + slack = dict() gurobi_range_con_vars = OrderedSet(self._solver_model.getVars()) - OrderedSet( self._pyomo_var_to_solver_var_map.values() @@ -1141,7 +1140,7 @@ def get_slacks(self, cons_to_load=None): def update(self, timer: HierarchicalTimer = None): if self._needs_updated: self._update_gurobi_model() - super().update(timer=timer) + super(Gurobi, self).update(timer=timer) self._update_gurobi_model() def _update_gurobi_model(self): @@ -1197,8 +1196,8 @@ def set_linear_constraint_attr(self, con, attr, val): if attr in {'Sense', 'RHS', 'ConstrName'}: raise ValueError( 'Linear constraint attr {0} cannot be set with' - ' the set_linear_constraint_attr method. Please use' - ' the remove_constraint and add_constraint methods.'.format(attr) + + ' the set_linear_constraint_attr method. Please use' + + ' the remove_constraint and add_constraint methods.'.format(attr) ) self._pyomo_con_to_solver_con_map[con].setAttr(attr, val) self._needs_updated = True @@ -1226,8 +1225,8 @@ def set_var_attr(self, var, attr, val): if attr in {'LB', 'UB', 'VType', 'VarName'}: raise ValueError( 'Var attr {0} cannot be set with' - ' the set_var_attr method. Please use' - ' the update_var method.'.format(attr) + + ' the set_var_attr method. Please use' + + ' the update_var method.'.format(attr) ) if attr == 'Obj': raise ValueError( @@ -1385,7 +1384,7 @@ def set_callback(self, func=None): >>> _c = _add_cut(4) # this is an arbitrary choice >>> >>> opt = appsi.solvers.Gurobi() - >>> opt.config.tee = True + >>> opt.config.stream_solver = True >>> opt.set_instance(m) # doctest:+SKIP >>> opt.gurobi_options['PreCrush'] = 1 >>> opt.gurobi_options['LazyConstraints'] = 1 diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 1680831471c..3d498f9388e 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -1,7 +1,5 @@ import logging -import sys from typing import List, Dict, Optional - from pyomo.common.collections import ComponentMap from pyomo.common.dependencies import attempt_import from pyomo.common.errors import PyomoException @@ -18,13 +16,18 @@ from pyomo.core.expr.numvalue import value, is_constant from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression +from pyomo.contrib.appsi.base import ( + PersistentSolver, + Results, + TerminationCondition, + MIPSolverConfig, + PersistentBase, + PersistentSolutionLoader, +) +from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.common.dependencies import numpy as np from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.solver.base import PersistentSolverBase -from pyomo.contrib.solver.config import BranchAndBoundConfig -from pyomo.contrib.solver.results import TerminationCondition, Results -from pyomo.contrib.solver.solution import PersistentSolutionLoader -from pyomo.contrib.solver.util import PersistentSolverUtils +import sys logger = logging.getLogger(__name__) @@ -35,7 +38,7 @@ class DegreeError(PyomoException): pass -class HighsConfig(BranchAndBoundConfig): +class HighsConfig(MIPSolverConfig): def __init__( self, description=None, @@ -44,7 +47,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super().__init__( + super(HighsConfig, self).__init__( description=description, doc=doc, implicit=implicit, @@ -64,11 +67,11 @@ def __init__( class HighsResults(Results): def __init__(self, solver): super().__init__() - self.timing_info.wall_time = None + self.wallclock_time = None self.solution_loader = PersistentSolutionLoader(solver=solver) -class _MutableVarBounds: +class _MutableVarBounds(object): def __init__(self, lower_expr, upper_expr, pyomo_var_id, var_map, highs): self.pyomo_var_id = pyomo_var_id self.lower_expr = lower_expr @@ -83,7 +86,7 @@ def update(self): self.highs.changeColBounds(col_ndx, lb, ub) -class _MutableLinearCoefficient: +class _MutableLinearCoefficient(object): def __init__(self, pyomo_con, pyomo_var_id, con_map, var_map, expr, highs): self.expr = expr self.highs = highs @@ -98,7 +101,7 @@ def update(self): self.highs.changeCoeff(row_ndx, col_ndx, value(self.expr)) -class _MutableObjectiveCoefficient: +class _MutableObjectiveCoefficient(object): def __init__(self, pyomo_var_id, var_map, expr, highs): self.expr = expr self.highs = highs @@ -110,7 +113,7 @@ def update(self): self.highs.changeColCost(col_ndx, value(self.expr)) -class _MutableObjectiveOffset: +class _MutableObjectiveOffset(object): def __init__(self, expr, highs): self.expr = expr self.highs = highs @@ -119,7 +122,7 @@ def update(self): self.highs.changeObjectiveOffset(value(self.expr)) -class _MutableConstraintBounds: +class _MutableConstraintBounds(object): def __init__(self, lower_expr, upper_expr, pyomo_con, con_map, highs): self.lower_expr = lower_expr self.upper_expr = upper_expr @@ -134,7 +137,7 @@ def update(self): self.highs.changeRowBounds(row_ndx, lb, ub) -class Highs(PersistentSolverUtils, PersistentSolverBase): +class Highs(PersistentBase, PersistentSolver): """ Interface to HiGHS """ @@ -144,14 +147,14 @@ class Highs(PersistentSolverUtils, PersistentSolverBase): def __init__(self, only_child_vars=False): super().__init__(only_child_vars=only_child_vars) self._config = HighsConfig() - self._solver_options = {} + self._solver_options = dict() self._solver_model = None - self._pyomo_var_to_solver_var_map = {} - self._pyomo_con_to_solver_con_map = {} - self._solver_con_to_pyomo_con_map = {} - self._mutable_helpers = {} - self._mutable_bounds = {} - self._objective_helpers = [] + self._pyomo_var_to_solver_var_map = dict() + self._pyomo_con_to_solver_con_map = dict() + self._solver_con_to_pyomo_con_map = dict() + self._mutable_helpers = dict() + self._mutable_bounds = dict() + self._objective_helpers = list() self._last_results_object: Optional[HighsResults] = None self._sol = None @@ -208,7 +211,7 @@ def _solve(self, timer: HierarchicalTimer): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.tee: + if self.config.stream_solver: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: @@ -219,8 +222,8 @@ def _solve(self, timer: HierarchicalTimer): if config.time_limit is not None: self._solver_model.setOptionValue('time_limit', config.time_limit) - if config.rel_gap is not None: - self._solver_model.setOptionValue('mip_rel_gap', config.rel_gap) + if config.mip_gap is not None: + self._solver_model.setOptionValue('mip_rel_gap', config.mip_gap) for key, option in options.items(): self._solver_model.setOptionValue(key, option) @@ -298,10 +301,10 @@ def _add_variables(self, variables: List[_GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - lbs = [] - ubs = [] - indices = [] - vtypes = [] + lbs = list() + ubs = list() + indices = list() + vtypes = list() current_num_vars = len(self._pyomo_var_to_solver_var_map) for v in variables: @@ -348,12 +351,14 @@ def set_instance(self, model): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.tee: + if self.config.stream_solver: ostreams.append(sys.stdout) with TeeStream(*ostreams) as t: with capture_output(output=t.STDOUT, capture_fd=True): self._reinit() self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() self._solver_model = highspy.Highs() self.add_block(model) @@ -365,11 +370,11 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() current_num_cons = len(self._pyomo_con_to_solver_con_map) - lbs = [] - ubs = [] - starts = [] - var_indices = [] - coef_values = [] + lbs = list() + ubs = list() + starts = list() + var_indices = list() + coef_values = list() for con in cons: repn = generate_standard_repn( @@ -395,7 +400,7 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): highs=self._solver_model, ) if con not in self._mutable_helpers: - self._mutable_helpers[con] = [] + self._mutable_helpers[con] = list() self._mutable_helpers[con].append(mutable_linear_coefficient) if coef_val == 0: continue @@ -450,7 +455,7 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - indices_to_remove = [] + indices_to_remove = list() for con in cons: con_ndx = self._pyomo_con_to_solver_con_map.pop(con) del self._solver_con_to_pyomo_con_map[con_ndx] @@ -460,7 +465,7 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): len(indices_to_remove), np.array(indices_to_remove) ) con_ndx = 0 - new_con_map = {} + new_con_map = dict() for c in self._pyomo_con_to_solver_con_map.keys(): new_con_map[c] = con_ndx con_ndx += 1 @@ -481,7 +486,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - indices_to_remove = [] + indices_to_remove = list() for v in variables: v_id = id(v) v_ndx = self._pyomo_var_to_solver_var_map.pop(v_id) @@ -492,7 +497,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): len(indices_to_remove), np.array(indices_to_remove) ) v_ndx = 0 - new_var_map = {} + new_var_map = dict() for v_id in self._pyomo_var_to_solver_var_map.keys(): new_var_map[v_id] = v_ndx v_ndx += 1 @@ -506,10 +511,10 @@ def _update_variables(self, variables: List[_GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() - indices = [] - lbs = [] - ubs = [] - vtypes = [] + indices = list() + lbs = list() + ubs = list() + vtypes = list() for v in variables: v_id = id(v) @@ -550,7 +555,7 @@ def _set_objective(self, obj): n = len(self._pyomo_var_to_solver_var_map) indices = np.arange(n) costs = np.zeros(n, dtype=np.double) - self._objective_helpers = [] + self._objective_helpers = list() if obj is None: sense = highspy.ObjSense.kMinimize self._solver_model.changeObjectiveOffset(0) @@ -602,7 +607,7 @@ def _postsolve(self, timer: HierarchicalTimer): status = highs.getModelStatus() results = HighsResults(self) - results.timing_info.wall_time = highs.getRunTime() + results.wallclock_time = highs.getRunTime() if status == highspy.HighsModelStatus.kNotset: results.termination_condition = TerminationCondition.unknown @@ -619,11 +624,9 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == highspy.HighsModelStatus.kModelEmpty: results.termination_condition = TerminationCondition.unknown elif status == highspy.HighsModelStatus.kOptimal: - results.termination_condition = ( - TerminationCondition.convergenceCriteriaSatisfied - ) + results.termination_condition = TerminationCondition.optimal elif status == highspy.HighsModelStatus.kInfeasible: - results.termination_condition = TerminationCondition.provenInfeasible + results.termination_condition = TerminationCondition.infeasible elif status == highspy.HighsModelStatus.kUnboundedOrInfeasible: results.termination_condition = TerminationCondition.infeasibleOrUnbounded elif status == highspy.HighsModelStatus.kUnbounded: @@ -635,7 +638,7 @@ def _postsolve(self, timer: HierarchicalTimer): elif status == highspy.HighsModelStatus.kTimeLimit: results.termination_condition = TerminationCondition.maxTimeLimit elif status == highspy.HighsModelStatus.kIterationLimit: - results.termination_condition = TerminationCondition.iterationLimit + results.termination_condition = TerminationCondition.maxIterations elif status == highspy.HighsModelStatus.kUnknown: results.termination_condition = TerminationCondition.unknown else: @@ -644,14 +647,11 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start('load solution') self._sol = highs.getSolution() has_feasible_solution = False - if ( - results.termination_condition - == TerminationCondition.convergenceCriteriaSatisfied - ): + if results.termination_condition == TerminationCondition.optimal: has_feasible_solution = True elif results.termination_condition in { TerminationCondition.objectiveLimit, - TerminationCondition.iterationLimit, + TerminationCondition.maxIterations, TerminationCondition.maxTimeLimit, }: if self._sol.value_valid: @@ -659,10 +659,7 @@ def _postsolve(self, timer: HierarchicalTimer): if config.load_solution: if has_feasible_solution: - if ( - results.termination_condition - != TerminationCondition.convergenceCriteriaSatisfied - ): + if results.termination_condition != TerminationCondition.optimal: logger.warning( 'Loading a feasible but suboptimal solution. ' 'Please set load_solution=False and check ' @@ -675,23 +672,23 @@ def _postsolve(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) timer.stop('load solution') info = highs.getInfo() - results.objective_bound = None - results.incumbent_objective = None + results.best_objective_bound = None + results.best_feasible_objective = None if self._objective is not None: if has_feasible_solution: - results.incumbent_objective = info.objective_function_value + results.best_feasible_objective = info.objective_function_value if info.mip_node_count == -1: if has_feasible_solution: - results.objective_bound = info.objective_function_value + results.best_objective_bound = info.objective_function_value else: - results.objective_bound = None + results.best_objective_bound = None else: - results.objective_bound = info.mip_dual_bound + results.best_objective_bound = info.mip_dual_bound return results @@ -709,7 +706,7 @@ def get_primals(self, vars_to_load=None, solution_number=0): res = ComponentMap() if vars_to_load is None: - var_ids_to_load = [] + var_ids_to_load = list() for v, ref_info in self._referenced_variables.items(): using_cons, using_sos, using_obj = ref_info if using_cons or using_sos or (using_obj is not None): @@ -754,7 +751,7 @@ def get_duals(self, cons_to_load=None): 'check the termination condition.' ) - res = {} + res = dict() if cons_to_load is None: cons_to_load = list(self._pyomo_con_to_solver_con_map.keys()) @@ -773,7 +770,7 @@ def get_slacks(self, cons_to_load=None): 'check the termination condition.' ) - res = {} + res = dict() if cons_to_load is None: cons_to_load = list(self._pyomo_con_to_solver_con_map.keys()) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index ec59b827192..d38a836a2ac 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -1,20 +1,22 @@ -import math -import os -import sys -from typing import Dict -import logging -import subprocess - - from pyomo.common.tempfiles import TempfileManager from pyomo.common.fileutils import Executable +from pyomo.contrib.appsi.base import ( + PersistentSolver, + Results, + TerminationCondition, + SolverConfig, + PersistentSolutionLoader, +) from pyomo.contrib.appsi.writers import NLWriter from pyomo.common.log import LogStream +import logging +import subprocess from pyomo.core.kernel.objective import minimize +import math from pyomo.common.collections import ComponentMap from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions -from typing import Optional, Sequence, List, Mapping +from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.block import _BlockData @@ -22,14 +24,13 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream +import sys +from typing import Dict from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.common.errors import PyomoException +import os from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.solver.base import PersistentSolverBase -from pyomo.contrib.solver.config import SolverConfig -from pyomo.contrib.solver.results import TerminationCondition, Results -from pyomo.contrib.solver.solution import PersistentSolutionLoader logger = logging.getLogger(__name__) @@ -44,7 +45,7 @@ def __init__( implicit_domain=None, visibility=0, ): - super().__init__( + super(IpoptConfig, self).__init__( description=description, doc=doc, implicit=implicit, @@ -125,13 +126,13 @@ def __init__( } -class Ipopt(PersistentSolverBase): +class Ipopt(PersistentSolver): def __init__(self, only_child_vars=False): self._config = IpoptConfig() - self._solver_options = {} + self._solver_options = dict() self._writer = NLWriter(only_child_vars=only_child_vars) self._filename = None - self._dual_sol = {} + self._dual_sol = dict() self._primal_sol = ComponentMap() self._reduced_costs = ComponentMap() self._last_results_object: Optional[Results] = None @@ -296,20 +297,19 @@ def _parse_sol(self): solve_cons = self._writer.get_ordered_cons() results = Results() - with open(self._filename + '.sol', 'r') as f: - all_lines = list(f.readlines()) + f = open(self._filename + '.sol', 'r') + all_lines = list(f.readlines()) + f.close() termination_line = all_lines[1] if 'Optimal Solution Found' in termination_line: - results.termination_condition = ( - TerminationCondition.convergenceCriteriaSatisfied - ) + results.termination_condition = TerminationCondition.optimal elif 'Problem may be infeasible' in termination_line: - results.termination_condition = TerminationCondition.locallyInfeasible + results.termination_condition = TerminationCondition.infeasible elif 'problem might be unbounded' in termination_line: results.termination_condition = TerminationCondition.unbounded elif 'Maximum Number of Iterations Exceeded' in termination_line: - results.termination_condition = TerminationCondition.iterationLimit + results.termination_condition = TerminationCondition.maxIterations elif 'Maximum CPU Time Exceeded' in termination_line: results.termination_condition = TerminationCondition.maxTimeLimit else: @@ -347,7 +347,7 @@ def _parse_sol(self): + n_rc_lower ] - self._dual_sol = {} + self._dual_sol = dict() self._primal_sol = ComponentMap() self._reduced_costs = ComponentMap() @@ -384,24 +384,20 @@ def _parse_sol(self): self._reduced_costs[var] = 0 if ( - results.termination_condition - == TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition == TerminationCondition.optimal and self.config.load_solution ): for v, val in self._primal_sol.items(): v.set_value(val, skip_validation=True) if self._writer.get_active_objective() is None: - results.incumbent_objective = None + results.best_feasible_objective = None else: - results.incumbent_objective = value( + results.best_feasible_objective = value( self._writer.get_active_objective().expr ) - elif ( - results.termination_condition - == TerminationCondition.convergenceCriteriaSatisfied - ): + elif results.termination_condition == TerminationCondition.optimal: if self._writer.get_active_objective() is None: - results.incumbent_objective = None + results.best_feasible_objective = None else: obj_expr_evaluated = replace_expressions( self._writer.get_active_objective().expr, @@ -411,13 +407,13 @@ def _parse_sol(self): descend_into_named_expressions=True, remove_named_expressions=True, ) - results.incumbent_objective = value(obj_expr_evaluated) + results.best_feasible_objective = value(obj_expr_evaluated) elif self.config.load_solution: raise RuntimeError( 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) return results @@ -435,7 +431,7 @@ def _apply_solver(self, timer: HierarchicalTimer): level=self.config.log_level, logger=self.config.solver_output_logger ) ] - if self.config.tee: + if self.config.stream_solver: ostreams.append(sys.stdout) cmd = [ @@ -481,23 +477,23 @@ def _apply_solver(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) results = Results() results.termination_condition = TerminationCondition.error - results.incumbent_objective = None + results.best_feasible_objective = None else: timer.start('parse solution') results = self._parse_sol() timer.stop('parse solution') if self._writer.get_active_objective() is None: - results.objective_bound = None + results.best_objective_bound = None else: if self._writer.get_active_objective().sense == minimize: - results.objective_bound = -math.inf + results.best_objective_bound = -math.inf else: - results.objective_bound = math.inf + results.best_objective_bound = math.inf results.solution_loader = PersistentSolutionLoader(solver=self) @@ -508,7 +504,7 @@ def get_primals( ) -> Mapping[_GeneralVarData, float]: if ( self._last_results_object is None - or self._last_results_object.incumbent_objective is None + or self._last_results_object.best_feasible_objective is None ): raise RuntimeError( 'Solver does not currently have a valid solution. Please ' @@ -530,7 +526,7 @@ def get_duals( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.convergenceCriteriaSatisfied + != TerminationCondition.optimal ): raise RuntimeError( 'Solver does not currently have valid duals. Please ' @@ -548,7 +544,7 @@ def get_reduced_costs( if ( self._last_results_object is None or self._last_results_object.termination_condition - != TerminationCondition.convergenceCriteriaSatisfied + != TerminationCondition.optimal ): raise RuntimeError( 'Solver does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index 4619a1c5452..b032f5c827e 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -1,8 +1,11 @@ -from pyomo.common import unittest +from pyomo.common.errors import PyomoException +import pyomo.common.unittest as unittest import pyomo.environ as pe from pyomo.contrib.appsi.solvers.gurobi import Gurobi -from pyomo.contrib.solver.results import TerminationCondition +from pyomo.contrib.appsi.base import TerminationCondition +from pyomo.core.expr.numeric_expr import LinearExpression from pyomo.core.expr.taylor_series import taylor_series_expansion +from pyomo.contrib.appsi.cmodel import cmodel_available opt = Gurobi() @@ -155,12 +158,10 @@ def test_lp(self): x, y = self.get_solution() opt = Gurobi() res = opt.solve(self.m) - self.assertAlmostEqual(x + y, res.incumbent_objective) - self.assertAlmostEqual(x + y, res.objective_bound) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertTrue(res.incumbent_objective is not None) + self.assertAlmostEqual(x + y, res.best_feasible_objective) + self.assertAlmostEqual(x + y, res.best_objective_bound) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertTrue(res.best_feasible_objective is not None) self.assertAlmostEqual(x, self.m.x.value) self.assertAlmostEqual(y, self.m.y.value) @@ -196,11 +197,11 @@ def test_nonconvex_qcp_objective_bound_1(self): opt.gurobi_options['BestBdStop'] = -8 opt.config.load_solution = False res = opt.solve(m) - self.assertEqual(res.incumbent_objective, None) - self.assertAlmostEqual(res.objective_bound, -8) + self.assertEqual(res.best_feasible_objective, None) + self.assertAlmostEqual(res.best_objective_bound, -8) def test_nonconvex_qcp_objective_bound_2(self): - # the goal of this test is to ensure we can objective_bound properly + # the goal of this test is to ensure we can best_objective_bound properly # for nonconvex but continuous problems when the solver terminates with a nonzero gap # # This is a fragile test because it could fail if Gurobi's algorithms change @@ -214,8 +215,8 @@ def test_nonconvex_qcp_objective_bound_2(self): opt.gurobi_options['nonconvex'] = 2 opt.gurobi_options['MIPGap'] = 0.5 res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, -4) - self.assertAlmostEqual(res.objective_bound, -6) + self.assertAlmostEqual(res.best_feasible_objective, -4) + self.assertAlmostEqual(res.best_objective_bound, -6) def test_range_constraints(self): m = pe.ConcreteModel() @@ -282,7 +283,7 @@ def test_quadratic_objective(self): res = opt.solve(m) self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) self.assertAlmostEqual( - res.incumbent_objective, + res.best_feasible_objective, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value, ) @@ -292,7 +293,7 @@ def test_quadratic_objective(self): res = opt.solve(m) self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) self.assertAlmostEqual( - res.incumbent_objective, + res.best_feasible_objective, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value, ) @@ -467,7 +468,7 @@ def test_zero_time_limit(self): # what we are trying to test. Unfortunately, I'm # not sure of a good way to guarantee that if num_solutions == 0: - self.assertIsNone(res.incumbent_objective) + self.assertIsNone(res.best_feasible_objective) class TestManualModel(unittest.TestCase): diff --git a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py index cd65783c566..6451db18087 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py @@ -7,6 +7,7 @@ from pyomo.common.log import LoggingIntercept from pyomo.common.tee import capture_output from pyomo.contrib.appsi.solvers.highs import Highs +from pyomo.contrib.appsi.base import TerminationCondition opt = Highs() @@ -32,12 +33,12 @@ def test_mutable_params_with_remove_cons(self): opt = Highs() res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) del m.c1 m.p2.value = 2 res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, -8) + self.assertAlmostEqual(res.best_feasible_objective, -8) def test_mutable_params_with_remove_vars(self): m = pe.ConcreteModel() @@ -59,14 +60,14 @@ def test_mutable_params_with_remove_vars(self): opt = Highs() res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) del m.c1 del m.c2 m.p1.value = -9 m.p2.value = 9 res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, -9) + self.assertAlmostEqual(res.best_feasible_objective, -9) def test_capture_highs_output(self): # tests issue #3003 @@ -94,7 +95,7 @@ def test_capture_highs_output(self): model[-2:-1] = [ 'opt = Highs()', - 'opt.config.tee = True', + 'opt.config.stream_solver = True', 'result = opt.solve(m)', ] with LoggingIntercept() as LOG, capture_output(capture_fd=True) as OUT: diff --git a/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py index 70e70fa65c5..6b86deaa535 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py @@ -1,5 +1,5 @@ import pyomo.environ as pe -from pyomo.common import unittest +import pyomo.common.unittest as unittest from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.common.gsl import find_GSL @@ -11,7 +11,7 @@ def test_external_function(self): if not DLL: self.skipTest('Could not find the amplgls.dll library') - opt = pe.SolverFactory('ipopt_v2') + opt = pe.SolverFactory('appsi_ipopt') if not opt.available(exception_flag=False): raise unittest.SkipTest @@ -31,7 +31,7 @@ def test_external_function_in_objective(self): if not DLL: self.skipTest('Could not find the amplgls.dll library') - opt = pe.SolverFactory('ipopt_v2') + opt = pe.SolverFactory('appsi_ipopt') if not opt.available(exception_flag=False): raise unittest.SkipTest diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 6731eb645fa..33f6877aaf8 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1,15 +1,15 @@ import pyomo.environ as pe from pyomo.common.dependencies import attempt_import -from pyomo.common import unittest +import pyomo.common.unittest as unittest parameterized, param_available = attempt_import('parameterized') parameterized = parameterized.parameterized -from pyomo.contrib.solver.base import PersistentSolverBase -from pyomo.contrib.solver.results import TerminationCondition, Results +from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available -from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Highs +from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression +import os numpy, numpy_available = attempt_import('numpy') import random @@ -19,11 +19,17 @@ if not param_available: raise unittest.SkipTest('Parameterized is not available.') -all_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt), ('highs', Highs)] -mip_solvers = [('gurobi', Gurobi), ('highs', Highs)] +all_solvers = [ + ('gurobi', Gurobi), + ('ipopt', Ipopt), + ('cplex', Cplex), + ('cbc', Cbc), + ('highs', Highs), +] +mip_solvers = [('gurobi', Gurobi), ('cplex', Cplex), ('cbc', Cbc), ('highs', Highs)] nlp_solvers = [('ipopt', Ipopt)] -qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] -miqcqp_solvers = [('gurobi', Gurobi)] +qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt), ('cplex', Cplex)] +miqcqp_solvers = [('gurobi', Gurobi), ('cplex', Cplex)] only_child_vars_options = [True, False] @@ -62,7 +68,7 @@ def _load_tests(solver_list, only_child_vars_list): - res = [] + res = list() for solver_name, solver in solver_list: for child_var_option in only_child_vars_list: test_name = f"{solver_name}_only_child_vars_{child_var_option}" @@ -75,19 +81,17 @@ def _load_tests(solver_list, only_child_vars_list): class TestSolvers(unittest.TestCase): @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_remove_variable_and_objective( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): # this test is for issue #2888 - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 2) del m.x @@ -95,16 +99,14 @@ def test_remove_variable_and_objective( m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_stale_vars( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -147,9 +149,9 @@ def test_stale_vars( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_range_constraint( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -157,26 +159,22 @@ def test_range_constraint( m.obj = pe.Objective(expr=m.x) m.c = pe.Constraint(expr=(-1, m.x, 1)) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -184,9 +182,7 @@ def test_reduced_costs( m.y = pe.Var(bounds=(-2, 2)) m.obj = pe.Objective(expr=3 * m.x + 4 * m.y) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, -2) rc = opt.get_reduced_costs() @@ -195,35 +191,31 @@ def test_reduced_costs( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs2( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, 1)) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_param_changes( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -244,27 +236,24 @@ def test_param_changes( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual( - res.termination_condition, - TerminationCondition.convergenceCriteriaSatisfied, - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.incumbent_objective, m.y.value) - self.assertTrue(res.objective_bound <= m.y.value) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value) + self.assertTrue(res.best_objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_immutable_param( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): """ This test is important because component_data_objects returns immutable params as floats. We want to make sure we process these correctly. """ - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -285,23 +274,20 @@ def test_immutable_param( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual( - res.termination_condition, - TerminationCondition.convergenceCriteriaSatisfied, - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.incumbent_objective, m.y.value) - self.assertTrue(res.objective_bound <= m.y.value) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value) + self.assertTrue(res.best_objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_equality( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -322,23 +308,20 @@ def test_equality( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual( - res.termination_condition, - TerminationCondition.convergenceCriteriaSatisfied, - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.incumbent_objective, m.y.value) - self.assertTrue(res.objective_bound <= m.y.value) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value) + self.assertTrue(res.best_objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_linear_expression( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -365,19 +348,16 @@ def test_linear_expression( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual( - res.termination_condition, - TerminationCondition.convergenceCriteriaSatisfied, - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.incumbent_objective, m.y.value) - self.assertTrue(res.objective_bound <= m.y.value) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value) + self.assertTrue(res.best_objective_bound <= m.y.value) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_no_objective( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -389,7 +369,7 @@ def test_no_objective( m.b2 = pe.Param(mutable=True) m.c1 = pe.Constraint(expr=m.y == m.a1 * m.x + m.b1) m.c2 = pe.Constraint(expr=m.y == m.a2 * m.x + m.b2) - opt.config.tee = True + opt.config.stream_solver = True params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] for a1, a2, b1, b2 in params_to_test: @@ -398,23 +378,20 @@ def test_no_objective( m.b1.value = b1 m.b2.value = b2 res: Results = opt.solve(m) - self.assertEqual( - res.termination_condition, - TerminationCondition.convergenceCriteriaSatisfied, - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertEqual(res.incumbent_objective, None) - self.assertEqual(res.objective_bound, None) + self.assertEqual(res.best_feasible_objective, None) + self.assertEqual(res.best_objective_bound, None) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], 0) self.assertAlmostEqual(duals[m.c2], 0) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_remove_cons( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -430,26 +407,22 @@ def test_add_remove_cons( m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.incumbent_objective, m.y.value) - self.assertTrue(res.objective_bound <= m.y.value) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value) + self.assertTrue(res.best_objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) m.c3 = pe.Constraint(expr=m.y >= a3 * m.x + b3) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b3 - b1) / (a1 - a3)) self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) - self.assertAlmostEqual(res.incumbent_objective, m.y.value) - self.assertTrue(res.objective_bound <= m.y.value) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value) + self.assertTrue(res.best_objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) self.assertAlmostEqual(duals[m.c2], 0) @@ -457,22 +430,20 @@ def test_add_remove_cons( del m.c3 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(res.incumbent_objective, m.y.value) - self.assertTrue(res.objective_bound <= m.y.value) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value) + self.assertTrue(res.best_objective_bound <= m.y.value) duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_results_infeasible( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -485,25 +456,21 @@ def test_results_infeasible( res = opt.solve(m) opt.config.load_solution = False res = opt.solve(m) - self.assertNotEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertNotEqual(res.termination_condition, TerminationCondition.optimal) if opt_class is Ipopt: acceptable_termination_conditions = { - TerminationCondition.provenInfeasible, - TerminationCondition.locallyInfeasible, + TerminationCondition.infeasible, TerminationCondition.unbounded, } else: acceptable_termination_conditions = { - TerminationCondition.provenInfeasible, - TerminationCondition.locallyInfeasible, + TerminationCondition.infeasible, TerminationCondition.infeasibleOrUnbounded, } self.assertIn(res.termination_condition, acceptable_termination_conditions) self.assertAlmostEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, None) - self.assertTrue(res.incumbent_objective is None) + self.assertTrue(res.best_feasible_objective is None) with self.assertRaisesRegex( RuntimeError, '.*does not currently have a valid solution.*' @@ -519,10 +486,8 @@ def test_results_infeasible( res.solution_loader.get_reduced_costs() @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_duals( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars - ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -545,9 +510,9 @@ def test_duals( @parameterized.expand(input=_load_tests(qcp_solvers, only_child_vars_options)) def test_mutable_quadratic_coefficient( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -569,9 +534,9 @@ def test_mutable_quadratic_coefficient( @parameterized.expand(input=_load_tests(qcp_solvers, only_child_vars_options)) def test_mutable_quadratic_objective( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -596,10 +561,10 @@ def test_mutable_quadratic_objective( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_vars( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): for treat_fixed_vars_as_params in [True, False]: - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = treat_fixed_vars_as_params if not opt.available(): raise unittest.SkipTest @@ -636,9 +601,9 @@ def test_fixed_vars( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_vars_2( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest @@ -675,9 +640,9 @@ def test_fixed_vars_2( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_vars_3( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest @@ -692,9 +657,9 @@ def test_fixed_vars_3( @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) def test_fixed_vars_4( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest @@ -713,9 +678,9 @@ def test_fixed_vars_4( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_mutable_param_with_range( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest try: @@ -783,30 +748,27 @@ def test_mutable_param_with_range( m.c2.value = float(c2) m.obj.sense = sense res: Results = opt.solve(m) - self.assertEqual( - res.termination_condition, - TerminationCondition.convergenceCriteriaSatisfied, - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) if sense is pe.minimize: self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) - self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) - self.assertTrue(res.objective_bound <= m.y.value + 1e-12) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) + self.assertTrue(res.best_objective_bound <= m.y.value + 1e-12) duals = opt.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) else: self.assertAlmostEqual(m.x.value, (c2 - c1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) - self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) - self.assertTrue(res.objective_bound >= m.y.value - 1e-12) + self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) + self.assertTrue(res.best_objective_bound >= m.y.value - 1e-12) duals = opt.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_and_remove_vars( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): @@ -823,9 +785,7 @@ def test_add_and_remove_vars( opt.update_config.check_for_new_or_removed_vars = False opt.config.load_solution = False res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) opt.load_vars() self.assertAlmostEqual(m.y.value, -1) m.x = pe.Var() @@ -839,9 +799,7 @@ def test_add_and_remove_vars( opt.add_variables([m.x]) opt.add_constraints([m.c1, m.c2]) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) opt.load_vars() self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @@ -850,9 +808,7 @@ def test_add_and_remove_vars( opt.remove_variables([m.x]) m.x.value = None res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) opt.load_vars() self.assertEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, -1) @@ -860,9 +816,7 @@ def test_add_and_remove_vars( opt.load_vars([m.x]) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) - def test_exp( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars - ): + def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -876,9 +830,7 @@ def test_exp( self.assertAlmostEqual(m.y.value, 0.6529186341994245) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) - def test_log( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars - ): + def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): opt = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -893,9 +845,9 @@ def test_log( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_with_numpy( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -917,17 +869,15 @@ def test_with_numpy( ) ) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_bounds_with_params( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -959,9 +909,9 @@ def test_bounds_with_params( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_solution_loader( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -1012,9 +962,9 @@ def test_solution_loader( @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_time_limit( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest from sys import platform @@ -1029,8 +979,8 @@ def test_time_limit( m.x = pe.Var(m.jobs, m.tasks, bounds=(0, 1)) random.seed(0) - coefs = [] - lin_vars = [] + coefs = list() + lin_vars = list() for j in m.jobs: for t in m.tasks: coefs.append(random.uniform(0, 10)) @@ -1062,13 +1012,21 @@ def test_time_limit( opt.config.time_limit = 0 opt.config.load_solution = False res = opt.solve(m) - self.assertEqual(res.termination_condition, TerminationCondition.maxTimeLimit) + if type(opt) is Cbc: # I can't figure out why CBC is reporting max iter... + self.assertIn( + res.termination_condition, + {TerminationCondition.maxIterations, TerminationCondition.maxTimeLimit}, + ) + else: + self.assertEqual( + res.termination_condition, TerminationCondition.maxTimeLimit + ) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_objective_changes( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -1078,13 +1036,13 @@ def test_objective_changes( m.c2 = pe.Constraint(expr=m.y >= -m.x + 1) m.obj = pe.Objective(expr=m.y) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) m.obj = pe.Objective(expr=2 * m.y) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 2) + self.assertAlmostEqual(res.best_feasible_objective, 2) m.obj.expr = 3 * m.y res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 3) + self.assertAlmostEqual(res.best_feasible_objective, 3) m.obj.sense = pe.maximize opt.config.load_solution = False res = opt.solve(m) @@ -1100,88 +1058,88 @@ def test_objective_changes( m.obj = pe.Objective(expr=m.x * m.y) m.x.fix(2) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 6, 6) + self.assertAlmostEqual(res.best_feasible_objective, 6, 6) m.x.fix(3) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 12, 6) + self.assertAlmostEqual(res.best_feasible_objective, 12, 6) m.x.unfix() m.y.fix(2) m.x.setlb(-3) m.x.setub(5) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, -2, 6) + self.assertAlmostEqual(res.best_feasible_objective, -2, 6) m.y.unfix() m.x.setlb(None) m.x.setub(None) m.e = pe.Expression(expr=2) m.obj = pe.Objective(expr=m.e * m.y) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 2) + self.assertAlmostEqual(res.best_feasible_objective, 2) m.e.expr = 3 res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 3) + self.assertAlmostEqual(res.best_feasible_objective, 3) opt.update_config.check_for_new_objective = False m.e.expr = 4 res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 4) + self.assertAlmostEqual(res.best_feasible_objective, 4) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_domain( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var(bounds=(1, None), domain=pe.NonNegativeReals) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) m.x.setlb(-1) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.setlb(1) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) m.x.setlb(-1) m.x.domain = pe.Reals res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, -1) + self.assertAlmostEqual(res.best_feasible_objective, -1) m.x.domain = pe.NonNegativeReals res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_domain_with_integers( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, None), domain=pe.NonNegativeIntegers) m.obj = pe.Objective(expr=m.x) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.setlb(0.5) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) m.x.setlb(-5.5) m.x.domain = pe.Integers res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, -5) + self.assertAlmostEqual(res.best_feasible_objective, -5) m.x.domain = pe.Binary res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.setlb(0.5) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_fixed_binaries( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -1191,25 +1149,25 @@ def test_fixed_binaries( m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars + self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -1227,15 +1185,20 @@ def test_with_gdp( pe.TransformationFactory("gdp.bigm").apply_to(m) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + opt.use_extensions = True + res = opt.solve(m) + self.assertAlmostEqual(res.best_feasible_objective, 1) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) @parameterized.expand(input=all_solvers) - def test_variables_elsewhere( - self, name: str, opt_class: Type[PersistentSolverBase] - ): - opt: PersistentSolverBase = opt_class(only_child_vars=False) + def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]): + opt: PersistentSolver = opt_class(only_child_vars=False) if not opt.available(): raise unittest.SkipTest @@ -1248,27 +1211,21 @@ def test_variables_elsewhere( m.b.c2 = pe.Constraint(expr=m.y >= -m.x) res = opt.solve(m.b) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(res.best_feasible_objective, 1) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, 1) m.x.setlb(0) res = opt.solve(m.b) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertAlmostEqual(res.incumbent_objective, 2) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(res.best_feasible_objective, 2) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=all_solvers) - def test_variables_elsewhere2( - self, name: str, opt_class: Type[PersistentSolverBase] - ): - opt: PersistentSolverBase = opt_class(only_child_vars=False) + def test_variables_elsewhere2(self, name: str, opt_class: Type[PersistentSolver]): + opt: PersistentSolver = opt_class(only_child_vars=False) if not opt.available(): raise unittest.SkipTest @@ -1284,10 +1241,8 @@ def test_variables_elsewhere2( m.c4 = pe.Constraint(expr=m.y >= -m.z + 1) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(res.best_feasible_objective, 1) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) self.assertIn(m.y, sol) @@ -1296,20 +1251,16 @@ def test_variables_elsewhere2( del m.c3 del m.c4 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(res.best_feasible_objective, 0) sol = res.solution_loader.get_primals() self.assertIn(m.x, sol) self.assertIn(m.y, sol) self.assertNotIn(m.z, sol) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_bug_1( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars - ): - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + def test_bug_1(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest @@ -1322,28 +1273,22 @@ def test_bug_1( m.c = pe.Constraint(expr=m.y >= m.p * m.x) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.p.value = 1 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertAlmostEqual(res.incumbent_objective, 3) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(res.best_feasible_objective, 3) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) - def test_bug_2( - self, name: str, opt_class: Type[PersistentSolverBase], only_child_vars - ): + def test_bug_2(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): """ This test is for a bug where an objective containing a fixed variable does not get updated properly when the variable is unfixed. """ for fixed_var_option in [True, False]: - opt: PersistentSolverBase = opt_class(only_child_vars=only_child_vars) + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) if not opt.available(): raise unittest.SkipTest opt.update_config.treat_fixed_vars_as_params = fixed_var_option @@ -1356,19 +1301,19 @@ def test_bug_2( m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, 2, 5) + self.assertAlmostEqual(res.best_feasible_objective, 2, 5) m.x.unfix() m.x.setlb(-9) m.x.setub(9) res = opt.solve(m) - self.assertAlmostEqual(res.incumbent_objective, -18, 5) + self.assertAlmostEqual(res.best_feasible_objective, -18, 5) @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') class TestLegacySolverInterface(unittest.TestCase): @parameterized.expand(input=all_solvers) - def test_param_updates(self, name: str, opt_class: Type[PersistentSolverBase]): + def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): opt = pe.SolverFactory('appsi_' + name) if not opt.available(exception_flag=False): raise unittest.SkipTest @@ -1398,7 +1343,7 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolverBase]): self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=all_solvers) - def test_load_solutions(self, name: str, opt_class: Type[PersistentSolverBase]): + def test_load_solutions(self, name: str, opt_class: Type[PersistentSolver]): opt = pe.SolverFactory('appsi_' + name) if not opt.available(exception_flag=False): raise unittest.SkipTest diff --git a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py index e09865294eb..d250923f104 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py @@ -1,6 +1,6 @@ import pyomo.environ as pe import pyomo.common.unittest as unittest -from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus +from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.solvers.wntr import Wntr, wntr_available import math @@ -18,18 +18,12 @@ def test_param_updates(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) m.p.value = 2 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 2) def test_remove_add_constraint(self): @@ -42,10 +36,7 @@ def test_remove_add_constraint(self): opt.config.symbolic_solver_labels = True opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) @@ -54,10 +45,7 @@ def test_remove_add_constraint(self): m.x.value = 0.5 m.y.value = 0.5 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 0) @@ -70,30 +58,21 @@ def test_fixed_var(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0.5) self.assertAlmostEqual(m.y.value, 0.25) m.x.unfix() m.c2 = pe.Constraint(expr=m.y == pe.exp(m.x)) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) m.x.fix(0.5) del m.c2 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0.5) self.assertAlmostEqual(m.y.value, 0.25) @@ -110,10 +89,7 @@ def test_remove_variables_params(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 1) self.assertAlmostEqual(m.z.value, 0) @@ -124,20 +100,14 @@ def test_remove_variables_params(self): m.z.value = 2 m.px.value = 2 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 2) self.assertAlmostEqual(m.z.value, 2) del m.z m.px.value = 3 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 3) def test_get_primals(self): @@ -150,10 +120,7 @@ def test_get_primals(self): opt.config.load_solution = False opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, None) self.assertAlmostEqual(m.y.value, None) primals = opt.get_primals() @@ -167,73 +134,49 @@ def test_operators(self): opt = Wntr() opt.wntr_options.update(_default_wntr_options) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 2) del m.c1 m.x.value = 0 m.c1 = pe.Constraint(expr=pe.sin(m.x) == math.sin(math.pi / 4)) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, math.pi / 4) del m.c1 m.c1 = pe.Constraint(expr=pe.cos(m.x) == 0) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, math.pi / 2) del m.c1 m.c1 = pe.Constraint(expr=pe.tan(m.x) == 1) m.x.value = 0 res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, math.pi / 4) del m.c1 m.c1 = pe.Constraint(expr=pe.asin(m.x) == math.asin(0.5)) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0.5) del m.c1 m.c1 = pe.Constraint(expr=pe.acos(m.x) == math.acos(0.6)) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0.6) del m.c1 m.c1 = pe.Constraint(expr=pe.atan(m.x) == math.atan(0.5)) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0.5) del m.c1 m.c1 = pe.Constraint(expr=pe.sqrt(m.x) == math.sqrt(0.6)) res = opt.solve(m) - self.assertEqual( - res.termination_condition, TerminationCondition.convergenceCriteriaSatisfied - ) - self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 0.6) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 04f54530c1b..0a358c6aedf 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -1,8 +1,11 @@ -from pyomo.contrib.solver.base import PersistentSolverBase -from pyomo.contrib.solver.util import PersistentSolverUtils -from pyomo.contrib.solver.config import SolverConfig -from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus -from pyomo.contrib.solver.solution import PersistentSolutionLoader +from pyomo.contrib.appsi.base import ( + PersistentBase, + PersistentSolver, + SolverConfig, + Results, + TerminationCondition, + PersistentSolutionLoader, +) from pyomo.core.expr.numeric_expr import ( ProductExpression, DivisionExpression, @@ -33,6 +36,7 @@ from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.dependencies import attempt_import from pyomo.core.staleflag import StaleFlagManager +from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available wntr, wntr_available = attempt_import('wntr') import logging @@ -65,10 +69,11 @@ def __init__( class WntrResults(Results): def __init__(self, solver): super().__init__() + self.wallclock_time = None self.solution_loader = PersistentSolutionLoader(solver=solver) -class Wntr(PersistentSolverUtils, PersistentSolverBase): +class Wntr(PersistentBase, PersistentSolver): def __init__(self, only_child_vars=True): super().__init__(only_child_vars=only_child_vars) self._config = WntrConfig() @@ -121,7 +126,7 @@ def _solve(self, timer: HierarchicalTimer): options.update(self.wntr_options) opt = wntr.sim.solvers.NewtonSolver(options) - if self.config.tee: + if self.config.stream_solver: ostream = sys.stdout else: ostream = None @@ -138,14 +143,13 @@ def _solve(self, timer: HierarchicalTimer): tf = time.time() results = WntrResults(self) - results.timing_info.wall_time = tf - t0 + results.wallclock_time = tf - t0 if status == wntr.sim.solvers.SolverStatus.converged: - results.termination_condition = ( - TerminationCondition.convergenceCriteriaSatisfied - ) - results.solution_status = SolutionStatus.optimal + results.termination_condition = TerminationCondition.optimal else: results.termination_condition = TerminationCondition.error + results.best_feasible_objective = None + results.best_objective_bound = None if self.config.load_solution: if status == wntr.sim.solvers.SolverStatus.converged: @@ -157,7 +161,7 @@ def _solve(self, timer: HierarchicalTimer): 'A feasible solution was not found, so no solution can be loaded.' 'Please set opt.config.load_solution=False and check ' 'results.termination_condition and ' - 'results.incumbent_objective before loading a solution.' + 'results.best_feasible_objective before loading a solution.' ) return results @@ -208,6 +212,8 @@ def set_instance(self, model): ) self._reinit() self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() if self.config.symbolic_solver_labels: self._labeler = TextLabeler() diff --git a/pyomo/contrib/appsi/tests/test_base.py b/pyomo/contrib/appsi/tests/test_base.py new file mode 100644 index 00000000000..0d67ca4d01a --- /dev/null +++ b/pyomo/contrib/appsi/tests/test_base.py @@ -0,0 +1,91 @@ +from pyomo.common import unittest +from pyomo.contrib import appsi +import pyomo.environ as pe +from pyomo.core.base.var import ScalarVar + + +class TestResults(unittest.TestCase): + def test_uninitialized(self): + res = appsi.base.Results() + self.assertIsNone(res.best_feasible_objective) + self.assertIsNone(res.best_objective_bound) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.unknown + ) + + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have a valid solution.*' + ): + res.solution_loader.load_vars() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid duals.*' + ): + res.solution_loader.get_duals() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid reduced costs.*' + ): + res.solution_loader.get_reduced_costs() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid slacks.*' + ): + res.solution_loader.get_slacks() + + def test_results(self): + m = pe.ConcreteModel() + m.x = ScalarVar() + m.y = ScalarVar() + m.c1 = pe.Constraint(expr=m.x == 1) + m.c2 = pe.Constraint(expr=m.y == 2) + + primals = dict() + primals[id(m.x)] = (m.x, 1) + primals[id(m.y)] = (m.y, 2) + duals = dict() + duals[m.c1] = 3 + duals[m.c2] = 4 + rc = dict() + rc[id(m.x)] = (m.x, 5) + rc[id(m.y)] = (m.y, 6) + slacks = dict() + slacks[m.c1] = 7 + slacks[m.c2] = 8 + + res = appsi.base.Results() + res.solution_loader = appsi.base.SolutionLoader( + primals=primals, duals=duals, slacks=slacks, reduced_costs=rc + ) + + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 2) + + m.x.value = None + m.y.value = None + + res.solution_loader.load_vars([m.y]) + self.assertIsNone(m.x.value) + self.assertAlmostEqual(m.y.value, 2) + + duals2 = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], duals2[m.c1]) + self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) + + duals2 = res.solution_loader.get_duals([m.c2]) + self.assertNotIn(m.c1, duals2) + self.assertAlmostEqual(duals[m.c2], duals2[m.c2]) + + rc2 = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[id(m.x)][1], rc2[m.x]) + self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) + + rc2 = res.solution_loader.get_reduced_costs([m.y]) + self.assertNotIn(m.x, rc2) + self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) + + slacks2 = res.solution_loader.get_slacks() + self.assertAlmostEqual(slacks[m.c1], slacks2[m.c1]) + self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) + + slacks2 = res.solution_loader.get_slacks([m.c2]) + self.assertNotIn(m.c1, slacks2) + self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) diff --git a/pyomo/contrib/appsi/tests/test_interval.py b/pyomo/contrib/appsi/tests/test_interval.py index 0924e3bbeed..7963cc31665 100644 --- a/pyomo/contrib/appsi/tests/test_interval.py +++ b/pyomo/contrib/appsi/tests/test_interval.py @@ -1,5 +1,5 @@ from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available -from pyomo.common import unittest +import pyomo.common.unittest as unittest import math from pyomo.contrib.fbbt.tests.test_interval import IntervalTestBase @@ -7,7 +7,7 @@ @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') class TestInterval(IntervalTestBase, unittest.TestCase): def setUp(self): - super().setUp() + super(TestInterval, self).setUp() self.add = cmodel.py_interval_add self.sub = cmodel.py_interval_sub self.mul = cmodel.py_interval_mul diff --git a/pyomo/contrib/appsi/utils/__init__.py b/pyomo/contrib/appsi/utils/__init__.py new file mode 100644 index 00000000000..f665736fd4a --- /dev/null +++ b/pyomo/contrib/appsi/utils/__init__.py @@ -0,0 +1,2 @@ +from .get_objective import get_objective +from .collect_vars_and_named_exprs import collect_vars_and_named_exprs diff --git a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py new file mode 100644 index 00000000000..9027080f08c --- /dev/null +++ b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py @@ -0,0 +1,50 @@ +from pyomo.core.expr.visitor import ExpressionValueVisitor, nonpyomo_leaf_types +import pyomo.core.expr as EXPR + + +class _VarAndNamedExprCollector(ExpressionValueVisitor): + def __init__(self): + self.named_expressions = dict() + self.variables = dict() + self.fixed_vars = dict() + self._external_functions = dict() + + def visit(self, node, values): + pass + + def visiting_potential_leaf(self, node): + if type(node) in nonpyomo_leaf_types: + return True, None + + if node.is_variable_type(): + self.variables[id(node)] = node + if node.is_fixed(): + self.fixed_vars[id(node)] = node + return True, None + + if node.is_named_expression_type(): + self.named_expressions[id(node)] = node + return False, None + + if type(node) is EXPR.ExternalFunctionExpression: + self._external_functions[id(node)] = node + return False, None + + if node.is_expression_type(): + return False, None + + return True, None + + +_visitor = _VarAndNamedExprCollector() + + +def collect_vars_and_named_exprs(expr): + _visitor.__init__() + _visitor.dfs_postorder_stack(expr) + return ( + list(_visitor.named_expressions.values()), + list(_visitor.variables.values()), + list(_visitor.fixed_vars.values()), + list(_visitor._external_functions.values()), + ) diff --git a/pyomo/contrib/appsi/utils/get_objective.py b/pyomo/contrib/appsi/utils/get_objective.py new file mode 100644 index 00000000000..30dd911f9c8 --- /dev/null +++ b/pyomo/contrib/appsi/utils/get_objective.py @@ -0,0 +1,12 @@ +from pyomo.core.base.objective import Objective + + +def get_objective(block): + obj = None + for o in block.component_data_objects( + Objective, descend_into=True, active=True, sort=True + ): + if obj is not None: + raise ValueError('Multiple active objectives found') + obj = o + return obj diff --git a/pyomo/contrib/appsi/utils/tests/__init__.py b/pyomo/contrib/appsi/utils/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py new file mode 100644 index 00000000000..4c2a167a017 --- /dev/null +++ b/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py @@ -0,0 +1,56 @@ +from pyomo.common import unittest +import pyomo.environ as pe +from pyomo.contrib.appsi.utils import collect_vars_and_named_exprs +from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available +from typing import Callable +from pyomo.common.gsl import find_GSL + + +class TestCollectVarsAndNamedExpressions(unittest.TestCase): + def basics_helper(self, collector: Callable, *args): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.E = pe.Expression(expr=2 * m.z + 1) + m.y.fix(3) + e = m.x * m.y + m.x * m.E + named_exprs, var_list, fixed_vars, external_funcs = collector(e, *args) + self.assertEqual([m.E], named_exprs) + self.assertEqual([m.x, m.y, m.z], var_list) + self.assertEqual([m.y], fixed_vars) + self.assertEqual([], external_funcs) + + def test_basics(self): + self.basics_helper(collect_vars_and_named_exprs) + + @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') + def test_basics_cmodel(self): + self.basics_helper(cmodel.prep_for_repn, cmodel.PyomoExprTypes()) + + def external_func_helper(self, collector: Callable, *args): + DLL = find_GSL() + if not DLL: + self.skipTest('Could not find amplgsl.dll library') + + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.hypot = pe.ExternalFunction(library=DLL, function='gsl_hypot') + func = m.hypot(m.x, m.x * m.y) + m.E = pe.Expression(expr=2 * func) + m.y.fix(3) + e = m.z + m.x * m.E + named_exprs, var_list, fixed_vars, external_funcs = collector(e, *args) + self.assertEqual([m.E], named_exprs) + self.assertEqual([m.z, m.x, m.y], var_list) + self.assertEqual([m.y], fixed_vars) + self.assertEqual([func], external_funcs) + + def test_external(self): + self.external_func_helper(collect_vars_and_named_exprs) + + @unittest.skipUnless(cmodel_available, 'appsi extensions are not available') + def test_external_cmodel(self): + self.basics_helper(cmodel.prep_for_repn, cmodel.PyomoExprTypes()) diff --git a/pyomo/contrib/appsi/writers/config.py b/pyomo/contrib/appsi/writers/config.py index 2a4e638f097..7a7faadaabe 100644 --- a/pyomo/contrib/appsi/writers/config.py +++ b/pyomo/contrib/appsi/writers/config.py @@ -1,3 +1,3 @@ -class WriterConfig: +class WriterConfig(object): def __init__(self): self.symbolic_solver_labels = False diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 9d0b71fe794..8a76fa5f9eb 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -5,17 +5,19 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import _BlockData +from pyomo.repn.standard_repn import generate_standard_repn +from pyomo.core.expr.numvalue import value +from pyomo.contrib.appsi.base import PersistentBase from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.timing import HierarchicalTimer -from pyomo.core.kernel.objective import minimize -from pyomo.contrib.solver.util import PersistentSolverUtils +from pyomo.core.kernel.objective import minimize, maximize from .config import WriterConfig from ..cmodel import cmodel, cmodel_available -class LPWriter(PersistentSolverUtils): +class LPWriter(PersistentBase): def __init__(self, only_child_vars=False): - super().__init__(only_child_vars=only_child_vars) + super(LPWriter, self).__init__(only_child_vars=only_child_vars) self._config = WriterConfig() self._writer = None self._symbol_map = SymbolMap() @@ -23,11 +25,11 @@ def __init__(self, only_child_vars=False): self._con_labeler = None self._param_labeler = None self._obj_labeler = None - self._pyomo_var_to_solver_var_map = {} - self._pyomo_con_to_solver_con_map = {} - self._solver_var_to_pyomo_var_map = {} - self._solver_con_to_pyomo_con_map = {} - self._pyomo_param_to_solver_param_map = {} + self._pyomo_var_to_solver_var_map = dict() + self._pyomo_con_to_solver_con_map = dict() + self._solver_var_to_pyomo_var_map = dict() + self._solver_con_to_pyomo_con_map = dict() + self._pyomo_param_to_solver_param_map = dict() self._expr_types = None @property @@ -87,7 +89,7 @@ def _add_params(self, params: List[_ParamData]): self._pyomo_param_to_solver_param_map[id(p)] = cp def _add_constraints(self, cons: List[_GeneralConstraintData]): - cmodel.process_lp_constraints() + cmodel.process_lp_constraints(cons, self) def _add_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) != 0: diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index a9b44e63f36..9c739fd6ebb 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -1,6 +1,4 @@ -import os from typing import List - from pyomo.core.base.param import _ParamData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData @@ -8,31 +6,32 @@ from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import _BlockData from pyomo.repn.standard_repn import generate_standard_repn -from pyomo.core.base import SymbolMap, TextLabeler +from pyomo.core.expr.numvalue import value +from pyomo.contrib.appsi.base import PersistentBase +from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.timing import HierarchicalTimer from pyomo.core.kernel.objective import minimize -from pyomo.common.collections import OrderedSet -from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env -from pyomo.contrib.solver.util import PersistentSolverUtils - from .config import WriterConfig +from pyomo.common.collections import OrderedSet +import os from ..cmodel import cmodel, cmodel_available +from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env -class NLWriter(PersistentSolverUtils): +class NLWriter(PersistentBase): def __init__(self, only_child_vars=False): - super().__init__(only_child_vars=only_child_vars) + super(NLWriter, self).__init__(only_child_vars=only_child_vars) self._config = WriterConfig() self._writer = None self._symbol_map = SymbolMap() self._var_labeler = None self._con_labeler = None self._param_labeler = None - self._pyomo_var_to_solver_var_map = {} - self._pyomo_con_to_solver_con_map = {} - self._solver_var_to_pyomo_var_map = {} - self._solver_con_to_pyomo_con_map = {} - self._pyomo_param_to_solver_param_map = {} + self._pyomo_var_to_solver_var_map = dict() + self._pyomo_con_to_solver_con_map = dict() + self._solver_var_to_pyomo_var_map = dict() + self._solver_con_to_pyomo_con_map = dict() + self._pyomo_param_to_solver_param_map = dict() self._expr_types = None @property @@ -173,8 +172,8 @@ def update_params(self): def _set_objective(self, obj: _GeneralObjectiveData): if obj is None: const = cmodel.Constant(0) - lin_vars = [] - lin_coef = [] + lin_vars = list() + lin_coef = list() nonlin = cmodel.Constant(0) sense = 0 else: @@ -241,7 +240,7 @@ def write(self, model: _BlockData, filename: str, timer: HierarchicalTimer = Non timer.stop('write file') def update(self, timer: HierarchicalTimer = None): - super().update(timer=timer) + super(NLWriter, self).update(timer=timer) self._set_pyomo_amplfunc_env() def get_ordered_vars(self): diff --git a/pyomo/contrib/appsi/writers/tests/test_nl_writer.py b/pyomo/contrib/appsi/writers/tests/test_nl_writer.py index 297bc3d7617..3b61a5901c3 100644 --- a/pyomo/contrib/appsi/writers/tests/test_nl_writer.py +++ b/pyomo/contrib/appsi/writers/tests/test_nl_writer.py @@ -1,4 +1,4 @@ -from pyomo.common import unittest +import pyomo.common.unittest as unittest from pyomo.common.tempfiles import TempfileManager import pyomo.environ as pe from pyomo.contrib import appsi From c0d35302b00a5b41752fd641a2f9c098fb5b8b3e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 16 Jan 2024 10:10:48 -0700 Subject: [PATCH 0370/3044] Explicitly register contrib.solver --- pyomo/environ/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/environ/__init__.py b/pyomo/environ/__init__.py index 51c68449247..5d488bda290 100644 --- a/pyomo/environ/__init__.py +++ b/pyomo/environ/__init__.py @@ -50,6 +50,7 @@ def _do_import(pkg_name): 'pyomo.contrib.multistart', 'pyomo.contrib.preprocessing', 'pyomo.contrib.pynumero', + 'pyomo.contrib.solver', 'pyomo.contrib.trustregion', ] From a1be778f394f2a92200e7ec5fb1ef8ea9c7b4ba1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 16 Jan 2024 10:12:49 -0700 Subject: [PATCH 0371/3044] Fix linking in online docs --- doc/OnlineDocs/developer_reference/solvers.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 75d95fc36db..10e7e829463 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -3,7 +3,7 @@ Solver Interfaces Pyomo offers interfaces into multiple solvers, both commercial and open source. -.. currentmodule:: pyomo.solver +.. currentmodule:: pyomo.contrib.solver Interface Implementation @@ -19,7 +19,7 @@ Every solver, at the end of a ``solve`` call, will return a ``Results`` object. This object is a :py:class:`pyomo.common.config.ConfigDict`, which can be manipulated similar to a standard ``dict`` in Python. -.. autoclass:: pyomo.solver.results.Results +.. autoclass:: pyomo.contrib.solver.results.Results :show-inheritance: :members: :undoc-members: @@ -35,7 +35,7 @@ returned solver messages or logs for more information. -.. autoclass:: pyomo.solver.results.TerminationCondition +.. autoclass:: pyomo.contrib.solver.results.TerminationCondition :show-inheritance: :noindex: @@ -48,7 +48,7 @@ intent of ``SolutionStatus`` is to notify the user of what the solver returned at a high level. The user is expected to inspect the ``Results`` object or any returned solver messages or logs for more information. -.. autoclass:: pyomo.solver.results.SolutionStatus +.. autoclass:: pyomo.contrib.solver.results.SolutionStatus :show-inheritance: :noindex: From c77eb247b0910b45c986d27ee9194a3b5926193f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 14:18:20 -0700 Subject: [PATCH 0372/3044] rework ipopt solution loader --- pyomo/contrib/solver/ipopt.py | 64 +++++++++++++++++++----------- pyomo/contrib/solver/sol_reader.py | 50 +++++------------------ pyomo/contrib/solver/solution.py | 44 +++++++++++++++++++- 3 files changed, 92 insertions(+), 66 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 63dca0af0d9..4e2dcdf83ab 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -14,27 +14,28 @@ import datetime import io import sys -from typing import Mapping, Optional, Dict +from typing import Mapping, Optional, Sequence from pyomo.common import Executable from pyomo.common.config import ConfigValue, NonNegativeInt, NonNegativeFloat from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer -from pyomo.core.base import Objective +from pyomo.core.base.var import _GeneralVarData from pyomo.core.staleflag import StaleFlagManager -from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo, AMPLRepn +from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo from pyomo.contrib.solver.base import SolverBase from pyomo.contrib.solver.config import SolverConfig from pyomo.contrib.solver.factory import SolverFactory from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus from .sol_reader import parse_sol_file -from pyomo.contrib.solver.solution import SolutionLoaderBase, SolutionLoader +from pyomo.contrib.solver.solution import SolSolutionLoader, SolutionLoader from pyomo.common.tee import TeeStream from pyomo.common.log import LogStream from pyomo.core.expr.visitor import replace_expressions from pyomo.core.expr.numvalue import value from pyomo.core.base.suffix import Suffix +from pyomo.common.collections import ComponentMap import logging @@ -110,8 +111,38 @@ def __init__( ) -class ipoptSolutionLoader(SolutionLoaderBase): - pass +class ipoptSolutionLoader(SolSolutionLoader): + def get_reduced_costs(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> Mapping[_GeneralVarData, float]: + sol_data = self._sol_data + nl_info = self._nl_info + zl_map = sol_data.var_suffixes['ipopt_zL_out'] + zu_map = sol_data.var_suffixes['ipopt_zU_out'] + rc = dict() + for v in nl_info.variables: + v_id = id(v) + rc[v_id] = (v, 0) + if v_id in zl_map: + zl = zl_map[v_id][1] + if abs(zl) > abs(rc[v_id][1]): + rc[v_id] = (v, zl) + if v_id in zu_map: + zu = zu_map[v_id][1] + if abs(zu) > abs(rc[v_id][1]): + rc[v_id] = (v, zu) + + if vars_to_load is None: + res = ComponentMap(rc.values()) + for v, _ in nl_info.eliminated_vars: + res[v] = 0 + else: + res = ComponentMap() + for v in vars_to_load: + if id(v) in rc: + res[v] = rc[id(v)][1] + else: + # eliminated vars + res[v] = 0 + return res ipopt_command_line_options = { @@ -452,24 +483,9 @@ def _parse_solution( if res.solution_status == SolutionStatus.noSolution: res.solution_loader = SolutionLoader(None, None, None, None) else: - rc = dict() - for v in nl_info.variables: - v_id = id(v) - rc[v_id] = (v, 0) - if v_id in sol_data.var_suffixes['ipopt_zL_out']: - zl = sol_data.var_suffixes['ipopt_zL_out'][v_id][1] - if abs(zl) > abs(rc[v_id][1]): - rc[v_id] = (v, zl) - if v_id in sol_data.var_suffixes['ipopt_zU_out']: - zu = sol_data.var_suffixes['ipopt_zU_out'][v_id][1] - if abs(zu) > abs(rc[v_id][1]): - rc[v_id] = (v, zu) - - res.solution_loader = SolutionLoader( - primals=sol_data.primals, - duals=sol_data.duals, - slacks=None, - reduced_costs=rc, + res.solution_loader = ipoptSolutionLoader( + sol_data=sol_data, + nl_info=nl_info, ) return res diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index 92761246241..28fe0100015 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -9,24 +9,13 @@ from pyomo.repn.plugins.nl_writer import AMPLRepn -def evaluate_ampl_repn(repn: AMPLRepn, sub_map): - assert not repn.nonlinear - assert repn.nl is None - val = repn.const - if repn.linear is not None: - for v_id, v_coef in repn.linear.items(): - val += v_coef * sub_map[v_id] - val *= repn.mult - return val - - class SolFileData: def __init__(self) -> None: - self.primals: Dict[int, Tuple[_GeneralVarData, float]] = dict() - self.duals: Dict[_ConstraintData, float] = dict() - self.var_suffixes: Dict[str, Dict[int, Tuple[_GeneralVarData, Any]]] = dict() - self.con_suffixes: Dict[str, Dict[_ConstraintData, Any]] = dict() - self.obj_suffixes: Dict[str, Dict[int, Tuple[_ObjectiveData, Any]]] = dict() + self.primals: List[float] = list() + self.duals: List[float] = list() + self.var_suffixes: Dict[str, Dict[int, Any]] = dict() + self.con_suffixes: Dict[str, Dict[Any]] = dict() + self.obj_suffixes: Dict[str, Dict[int, Any]] = dict() self.problem_suffixes: Dict[str, List[Any]] = dict() @@ -138,10 +127,8 @@ def parse_sol_file( result.extra_info.solver_message = exit_code_message if result.solution_status != SolutionStatus.noSolution: - for v, val in zip(nl_info.variables, variable_vals): - sol_data.primals[id(v)] = (v, val) - for c, val in zip(nl_info.constraints, duals): - sol_data.duals[c] = val + sol_data.primals = variable_vals + sol_data.duals = duals ### Read suffixes ### line = sol_file.readline() while line: @@ -180,18 +167,13 @@ def parse_sol_file( for cnt in range(nvalues): suf_line = sol_file.readline().split() var_ndx = int(suf_line[0]) - var = nl_info.variables[var_ndx] - sol_data.var_suffixes[suffix_name][id(var)] = ( - var, - convert_function(suf_line[1]), - ) + sol_data.var_suffixes[suffix_name][var_ndx] = convert_function(suf_line[1]) elif kind == 1: # Con sol_data.con_suffixes[suffix_name] = dict() for cnt in range(nvalues): suf_line = sol_file.readline().split() con_ndx = int(suf_line[0]) - con = nl_info.constraints[con_ndx] - sol_data.con_suffixes[suffix_name][con] = convert_function( + sol_data.con_suffixes[suffix_name][con_ndx] = convert_function( suf_line[1] ) elif kind == 2: # Obj @@ -199,11 +181,7 @@ def parse_sol_file( for cnt in range(nvalues): suf_line = sol_file.readline().split() obj_ndx = int(suf_line[0]) - obj = nl_info.objectives[obj_ndx] - sol_data.obj_suffixes[suffix_name][id(obj)] = ( - obj, - convert_function(suf_line[1]), - ) + sol_data.obj_suffixes[suffix_name][obj_ndx] = convert_function(suf_line[1]) elif kind == 3: # Prob sol_data.problem_suffixes[suffix_name] = list() for cnt in range(nvalues): @@ -213,12 +191,4 @@ def parse_sol_file( ) line = sol_file.readline() - if len(nl_info.eliminated_vars) > 0: - sub_map = {k: v[1] for k, v in sol_data.primals.items()} - for v, v_expr in nl_info.eliminated_vars: - val = evaluate_ampl_repn(v_expr, sub_map) - v_id = id(v) - sub_map[v_id] = val - sol_data.primals[v_id] = (v, val) - return result, sol_data diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 4ec3f98cd08..7ea26c5f484 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -16,6 +16,10 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.common.collections import ComponentMap from pyomo.core.staleflag import StaleFlagManager +from .sol_reader import SolFileData +from pyomo.repn.plugins.nl_writer import NLWriterInfo, AMPLRepn +from pyomo.core.expr.numvalue import value +from pyomo.core.expr.visitor import replace_expressions # CHANGES: # - `load` method: should just load the whole thing back into the model; load_solution = True @@ -49,8 +53,9 @@ def load_vars( Parameters ---------- vars_to_load: list - A list of the variables whose solution should be loaded. If vars_to_load is None, then the solution - to all primal variables will be loaded. + The minimum set of variables whose solution should be loaded. If vars_to_load is None, then the solution + to all primal variables will be loaded. Even if vars_to_load is specified, the values of other + variables may also be loaded depending on the interface. """ for v, val in self.get_primals(vars_to_load=vars_to_load).items(): v.set_value(val, skip_validation=True) @@ -195,6 +200,41 @@ def get_reduced_costs( return rc +class SolSolutionLoader(SolutionLoaderBase): + def __init__(self, sol_data: SolFileData, nl_info: NLWriterInfo) -> None: + self._sol_data = sol_data + self._nl_info = nl_info + + def load_vars( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> NoReturn: + for v, val in zip(self._nl_info.variables, self._sol_data.primals): + v.set_value(val, skip_validation=True) + + for v, v_expr in self._nl_info.eliminated_vars: + v.set_value(value(v_expr), skip_validation=True) + + StaleFlagManager.mark_all_as_stale(delayed=True) + + def get_primals(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> Mapping[_GeneralVarData, float]: + val_map = dict(zip([id(v) for v in self._nl_info.variables], self._sol_data.primals)) + + for v, v_expr in self._nl_info.eliminated_vars: + val = replace_expressions(v_expr, substitution_map=val_map) + v_id = id(v) + val_map[v_id] = val + + res = ComponentMap() + for v in vars_to_load: + res[v] = val_map[id(v)] + + return res + + def get_duals(self, cons_to_load: Sequence[_GeneralConstraintData] | None = None) -> Dict[_GeneralConstraintData, float]: + cons_to_load = set(cons_to_load) + return {c: val for c, val in zip(self._nl_info.constraints, self._sol_data.duals) if c in cons_to_load} + + class PersistentSolutionLoader(SolutionLoaderBase): def __init__(self, solver): self._solver = solver From a5e3873e6c37b54e115baed35a2999f6cbd99f98 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 14:32:36 -0700 Subject: [PATCH 0373/3044] fix imports --- pyomo/contrib/solver/results.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index e21adcc35cc..b4a30da0b35 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -28,7 +28,6 @@ TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus, ) -from pyomo.contrib.solver.solution import SolutionLoaderBase class SolverResultsError(PyomoException): @@ -192,7 +191,7 @@ def __init__( visibility=visibility, ) - self.solution_loader: SolutionLoaderBase = self.declare( + self.solution_loader = self.declare( 'solution_loader', ConfigValue() ) self.termination_condition: TerminationCondition = self.declare( From f1bd6821001233423d7bb7ac123f5560c6340c7d Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 14:45:41 -0700 Subject: [PATCH 0374/3044] override display() --- pyomo/contrib/solver/results.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index b4a30da0b35..fa543d0aeaa 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -244,6 +244,9 @@ def __init__( ConfigValue(domain=str, default=None, visibility=ADVANCED_OPTION), ) + def display(self, content_filter=None, indent_spacing=2, ostream=None, visibility=0): + return super().display(content_filter, indent_spacing, ostream, visibility) + class ResultsReader: pass From a44929ce6ff58adf9a638895261ba25b8a21e2b4 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 15:19:45 -0700 Subject: [PATCH 0375/3044] better handling of solver output --- pyomo/contrib/solver/config.py | 8 ++++++++ pyomo/contrib/solver/ipopt.py | 11 +++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 84b1c2d2c87..d053356a684 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -50,6 +50,14 @@ def __init__( description="If True, the solver log prints to stdout.", ), ) + self.log_solver_output: bool = self.declare( + 'log_solver_output', + ConfigValue( + domain=bool, + default=False, + description="If True, the solver output gets logged.", + ), + ) self.load_solution: bool = self.declare( 'load_solution', ConfigValue( diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 4e2dcdf83ab..1b091638343 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -69,15 +69,10 @@ def __init__( 'executable', ConfigValue(default=Executable('ipopt')) ) # TODO: Add in a deprecation here for keepfiles + # M.B.: Is the above TODO still relevant? self.temp_dir: str = self.declare( 'temp_dir', ConfigValue(domain=str, default=None) ) - self.solver_output_logger = self.declare( - 'solver_output_logger', ConfigValue(default=logger) - ) - self.log_level = self.declare( - 'log_level', ConfigValue(domain=NonNegativeInt, default=logging.INFO) - ) self.writer_config = self.declare( 'writer_config', ConfigValue(default=NLWriter.CONFIG()) ) @@ -347,10 +342,10 @@ def solve(self, model, **kwds): ostreams = [io.StringIO()] if config.tee: ostreams.append(sys.stdout) - else: + if config.log_solver_output: ostreams.append( LogStream( - level=config.log_level, logger=config.solver_output_logger + level=logging.INFO, logger=logger ) ) with TeeStream(*ostreams) as t: From a3fe00381b83c7cd98797da62c967651594efb90 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 16 Jan 2024 16:35:20 -0700 Subject: [PATCH 0376/3044] handle scaling when loading results --- pyomo/contrib/solver/ipopt.py | 15 ++++++++++----- pyomo/contrib/solver/solution.py | 33 +++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 1b091638343..47436d9d11f 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -108,20 +108,25 @@ def __init__( class ipoptSolutionLoader(SolSolutionLoader): def get_reduced_costs(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> Mapping[_GeneralVarData, float]: + if self._nl_info.scaling is None: + scale_list = [1] * len(self._nl_info.variables) + else: + scale_list = self._nl_info.scaling.variables sol_data = self._sol_data nl_info = self._nl_info zl_map = sol_data.var_suffixes['ipopt_zL_out'] zu_map = sol_data.var_suffixes['ipopt_zU_out'] rc = dict() - for v in nl_info.variables: + for ndx, v in enumerate(nl_info.variables): + scale = scale_list[ndx] v_id = id(v) rc[v_id] = (v, 0) - if v_id in zl_map: - zl = zl_map[v_id][1] + if ndx in zl_map: + zl = zl_map[ndx] * scale if abs(zl) > abs(rc[v_id][1]): rc[v_id] = (v, zl) - if v_id in zu_map: - zu = zu_map[v_id][1] + if ndx in zu_map: + zu = zu_map[ndx] * scale if abs(zu) > abs(rc[v_id][1]): rc[v_id] = (v, zu) diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 7ea26c5f484..ae47491c310 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -208,8 +208,12 @@ def __init__(self, sol_data: SolFileData, nl_info: NLWriterInfo) -> None: def load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> NoReturn: - for v, val in zip(self._nl_info.variables, self._sol_data.primals): - v.set_value(val, skip_validation=True) + if self._nl_info.scaling is None: + scale_list = [1] * len(self._nl_info.variables) + else: + scale_list = self._nl_info.scaling.variables + for v, val, scale in zip(self._nl_info.variables, self._sol_data.primals, scale_list): + v.set_value(val/scale, skip_validation=True) for v, v_expr in self._nl_info.eliminated_vars: v.set_value(value(v_expr), skip_validation=True) @@ -217,7 +221,13 @@ def load_vars( StaleFlagManager.mark_all_as_stale(delayed=True) def get_primals(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> Mapping[_GeneralVarData, float]: - val_map = dict(zip([id(v) for v in self._nl_info.variables], self._sol_data.primals)) + if self._nl_info.scaling is None: + scale_list = [1] * len(self._nl_info.variables) + else: + scale_list = self._nl_info.scaling.variables + val_map = dict() + for v, val, scale in zip(self._nl_info.variables, self._sol_data.primals, scale_list): + val_map[id(v)] = val / scale for v, v_expr in self._nl_info.eliminated_vars: val = replace_expressions(v_expr, substitution_map=val_map) @@ -225,14 +235,27 @@ def get_primals(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> val_map[v_id] = val res = ComponentMap() + if vars_to_load is None: + vars_to_load = self._nl_info.variables + [v for v, _ in self._nl_info.eliminated_vars] for v in vars_to_load: res[v] = val_map[id(v)] return res def get_duals(self, cons_to_load: Sequence[_GeneralConstraintData] | None = None) -> Dict[_GeneralConstraintData, float]: - cons_to_load = set(cons_to_load) - return {c: val for c, val in zip(self._nl_info.constraints, self._sol_data.duals) if c in cons_to_load} + if self._nl_info.scaling is None: + scale_list = [1] * len(self._nl_info.constraints) + else: + scale_list = self._nl_info.scaling.constraints + if cons_to_load is None: + cons_to_load = set(self._nl_info.constraints) + else: + cons_to_load = set(cons_to_load) + res = dict() + for c, val, scale in zip(self._nl_info.constraints, self._sol_data.duals, scale_list): + if c in cons_to_load: + res[c] = val * scale + return res class PersistentSolutionLoader(SolutionLoaderBase): From da2e58f48c2043b01844fb2909793d2512107624 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Jan 2024 07:53:42 -0700 Subject: [PATCH 0377/3044] Save state: fixing broken tests --- pyomo/contrib/solver/config.py | 3 +++ pyomo/contrib/solver/tests/unit/test_base.py | 16 +++++----------- pyomo/contrib/solver/tests/unit/test_config.py | 17 ++++++++++------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 84b1c2d2c87..f5aa1e2c5c7 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -141,10 +141,13 @@ class AutoUpdateConfig(ConfigDict): check_for_new_or_removed_constraints: bool check_for_new_or_removed_vars: bool check_for_new_or_removed_params: bool + check_for_new_objective: bool update_constraints: bool update_vars: bool update_params: bool update_named_expressions: bool + update_objective: bool + treat_fixed_vars_as_params: bool """ def __init__( diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 71690b7aa0e..e3a8999d8c5 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -19,7 +19,7 @@ def test_solver_base(self): self.instance = base.SolverBase() self.assertFalse(self.instance.is_persistent()) self.assertEqual(self.instance.version(), None) - self.assertEqual(self.instance.config, None) + self.assertEqual(self.instance.CONFIG, self.instance.config) self.assertEqual(self.instance.solve(None), None) self.assertEqual(self.instance.available(), None) @@ -39,18 +39,16 @@ def test_abstract_member_list(self): expected_list = [ 'remove_params', 'version', - 'config', 'update_variables', 'remove_variables', 'add_constraints', - 'get_primals', + '_get_primals', 'set_instance', 'set_objective', 'update_params', 'remove_block', 'add_block', 'available', - 'update_config', 'add_params', 'remove_constraints', 'add_variables', @@ -63,7 +61,6 @@ def test_abstract_member_list(self): def test_persistent_solver_base(self): self.instance = base.PersistentSolverBase() self.assertTrue(self.instance.is_persistent()) - self.assertEqual(self.instance.update_config, None) self.assertEqual(self.instance.set_instance(None), None) self.assertEqual(self.instance.add_variables(None), None) self.assertEqual(self.instance.add_params(None), None) @@ -78,13 +75,10 @@ def test_persistent_solver_base(self): self.assertEqual(self.instance.update_params(), None) with self.assertRaises(NotImplementedError): - self.instance.get_primals() + self.instance._get_primals() with self.assertRaises(NotImplementedError): - self.instance.get_duals() + self.instance._get_duals() with self.assertRaises(NotImplementedError): - self.instance.get_slacks() - - with self.assertRaises(NotImplementedError): - self.instance.get_reduced_costs() + self.instance._get_reduced_costs() diff --git a/pyomo/contrib/solver/tests/unit/test_config.py b/pyomo/contrib/solver/tests/unit/test_config.py index 1051825f4e5..3ad8319343b 100644 --- a/pyomo/contrib/solver/tests/unit/test_config.py +++ b/pyomo/contrib/solver/tests/unit/test_config.py @@ -16,12 +16,15 @@ class TestSolverConfig(unittest.TestCase): def test_interface_default_instantiation(self): config = SolverConfig() - self.assertEqual(config._description, None) + self.assertIsNone(config._description) self.assertEqual(config._visibility, 0) self.assertFalse(config.tee) self.assertTrue(config.load_solution) + self.assertTrue(config.raise_exception_on_nonoptimal_result) self.assertFalse(config.symbolic_solver_labels) - self.assertFalse(config.report_timing) + self.assertIsNone(config.timer) + self.assertIsNone(config.threads) + self.assertIsNone(config.time_limit) def test_interface_custom_instantiation(self): config = SolverConfig(description="A description") @@ -31,20 +34,19 @@ def test_interface_custom_instantiation(self): self.assertFalse(config.time_limit) config.time_limit = 1.0 self.assertEqual(config.time_limit, 1.0) + self.assertIsInstance(config.time_limit, float) class TestBranchAndBoundConfig(unittest.TestCase): def test_interface_default_instantiation(self): config = BranchAndBoundConfig() - self.assertEqual(config._description, None) + self.assertIsNone(config._description) self.assertEqual(config._visibility, 0) self.assertFalse(config.tee) self.assertTrue(config.load_solution) self.assertFalse(config.symbolic_solver_labels) - self.assertFalse(config.report_timing) - self.assertEqual(config.rel_gap, None) - self.assertEqual(config.abs_gap, None) - self.assertFalse(config.relax_integrality) + self.assertIsNone(config.rel_gap) + self.assertIsNone(config.abs_gap) def test_interface_custom_instantiation(self): config = BranchAndBoundConfig(description="A description") @@ -54,5 +56,6 @@ def test_interface_custom_instantiation(self): self.assertFalse(config.time_limit) config.time_limit = 1.0 self.assertEqual(config.time_limit, 1.0) + self.assertIsInstance(config.time_limit, float) config.rel_gap = 2.5 self.assertEqual(config.rel_gap, 2.5) From 7fa02de0cc8b87afa6e13b392eb2651af88ee9bb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Jan 2024 08:11:03 -0700 Subject: [PATCH 0378/3044] Remove slack referrences; fix broken unit tests --- pyomo/contrib/solver/base.py | 8 ------ pyomo/contrib/solver/ipopt.py | 4 +-- pyomo/contrib/solver/solution.py | 27 +------------------ .../solver/tests/solvers/test_ipopt.py | 5 ++-- .../contrib/solver/tests/unit/test_results.py | 20 +++----------- .../solver/tests/unit/test_solution.py | 2 -- 6 files changed, 9 insertions(+), 57 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 962d35582a1..0b33f8a5648 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -435,9 +435,6 @@ def solve( if hasattr(model, 'dual') and model.dual.import_enabled(): for c, val in results.solution_loader.get_duals().items(): model.dual[c] = val - if hasattr(model, 'slack') and model.slack.import_enabled(): - for c, val in results.solution_loader.get_slacks().items(): - model.slack[c] = val if hasattr(model, 'rc') and model.rc.import_enabled(): for v, val in results.solution_loader.get_reduced_costs().items(): model.rc[v] = val @@ -448,11 +445,6 @@ def solve( if hasattr(model, 'dual') and model.dual.import_enabled(): for c, val in results.solution_loader.get_duals().items(): legacy_soln.constraint[symbol_map.getSymbol(c)] = {'Dual': val} - if hasattr(model, 'slack') and model.slack.import_enabled(): - for c, val in results.solution_loader.get_slacks().items(): - symbol = symbol_map.getSymbol(c) - if symbol in legacy_soln.constraint: - legacy_soln.constraint[symbol]['Slack'] = val if hasattr(model, 'rc') and model.rc.import_enabled(): for v, val in results.solution_loader.get_reduced_costs().items(): legacy_soln.variable['Rc'] = val diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 47436d9d11f..6ab40fd3924 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -372,7 +372,7 @@ def solve(self, model, **kwds): if process.returncode != 0: results.termination_condition = TerminationCondition.error - results.solution_loader = SolutionLoader(None, None, None, None) + results.solution_loader = SolutionLoader(None, None, None) else: with open(basename + '.sol', 'r') as sol_file: timer.start('parse_sol') @@ -481,7 +481,7 @@ def _parse_solution( ) if res.solution_status == SolutionStatus.noSolution: - res.solution_loader = SolutionLoader(None, None, None, None) + res.solution_loader = SolutionLoader(None, None, None) else: res.solution_loader = ipoptSolutionLoader( sol_data=sol_data, diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index ae47491c310..18fd96759cf 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -17,31 +17,10 @@ from pyomo.common.collections import ComponentMap from pyomo.core.staleflag import StaleFlagManager from .sol_reader import SolFileData -from pyomo.repn.plugins.nl_writer import NLWriterInfo, AMPLRepn +from pyomo.repn.plugins.nl_writer import NLWriterInfo from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions -# CHANGES: -# - `load` method: should just load the whole thing back into the model; load_solution = True -# - `load_variables` -# - `get_variables` -# - `get_constraints` -# - `get_objective` -# - `get_slacks` -# - `get_reduced_costs` - -# duals is how much better you could get if you weren't constrained. -# dual value of 0 means that the constraint isn't actively constraining anything. -# high dual value means that it is costing us a lot in the objective. -# can also be called "shadow price" - -# bounds on variables are implied constraints. -# getting a dual on the bound of a variable is the reduced cost. -# IPOPT calls these the bound multipliers (normally they are reduced costs, though). ZL, ZU - -# slacks are... something that I don't understand -# but they are necessary somewhere? I guess? - class SolutionLoaderBase(abc.ABC): def load_vars( @@ -129,7 +108,6 @@ def __init__( self, primals: Optional[MutableMapping], duals: Optional[MutableMapping], - slacks: Optional[MutableMapping], reduced_costs: Optional[MutableMapping], ): """ @@ -139,14 +117,11 @@ def __init__( maps id(Var) to (var, value) duals: dict maps Constraint to dual value - slacks: dict - maps Constraint to slack value reduced_costs: dict maps id(Var) to (var, reduced_cost) """ self._primals = primals self._duals = duals - self._slacks = slacks self._reduced_costs = reduced_costs def get_primals( diff --git a/pyomo/contrib/solver/tests/solvers/test_ipopt.py b/pyomo/contrib/solver/tests/solvers/test_ipopt.py index c1aecba05fc..9638d94bdda 100644 --- a/pyomo/contrib/solver/tests/solvers/test_ipopt.py +++ b/pyomo/contrib/solver/tests/solvers/test_ipopt.py @@ -45,13 +45,12 @@ def test_ipopt_config(self): config = ipoptConfig() self.assertTrue(config.load_solution) self.assertIsInstance(config.solver_options, ConfigDict) - print(type(config.executable)) self.assertIsInstance(config.executable, ExecutableData) # Test custom initialization - solver = SolverFactory('ipopt_v2', save_solver_io=True) - self.assertTrue(solver.config.save_solver_io) + solver = SolverFactory('ipopt_v2', executable='/path/to/exe') self.assertFalse(solver.config.tee) + self.assertTrue(solver.config.executable.startswith('/path')) # Change value on a solve call # model = self.create_model() diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index e7d02751f7d..927ab64ee12 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -82,6 +82,8 @@ def test_declared_items(self): 'solver_version', 'termination_condition', 'timing_info', + 'solver_log', + 'solver_configuration' } actual_declared = res._declared self.assertEqual(expected_declared, actual_declared) @@ -101,7 +103,7 @@ def test_uninitialized(self): self.assertIsInstance(res.extra_info, ConfigDict) self.assertIsNone(res.timing_info.start_timestamp) self.assertIsNone(res.timing_info.wall_time) - res.solution_loader = solution.SolutionLoader(None, None, None, None) + res.solution_loader = solution.SolutionLoader(None, None, None) with self.assertRaisesRegex( RuntimeError, '.*does not currently have a valid solution.*' @@ -115,10 +117,6 @@ def test_uninitialized(self): RuntimeError, '.*does not currently have valid reduced costs.*' ): res.solution_loader.get_reduced_costs() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid slacks.*' - ): - res.solution_loader.get_slacks() def test_results(self): m = pyo.ConcreteModel() @@ -136,13 +134,10 @@ def test_results(self): rc = {} rc[id(m.x)] = (m.x, 5) rc[id(m.y)] = (m.y, 6) - slacks = {} - slacks[m.c1] = 7 - slacks[m.c2] = 8 res = results.Results() res.solution_loader = solution.SolutionLoader( - primals=primals, duals=duals, slacks=slacks, reduced_costs=rc + primals=primals, duals=duals, reduced_costs=rc ) res.solution_loader.load_vars() @@ -172,10 +167,3 @@ def test_results(self): self.assertNotIn(m.x, rc2) self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) - slacks2 = res.solution_loader.get_slacks() - self.assertAlmostEqual(slacks[m.c1], slacks2[m.c1]) - self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) - - slacks2 = res.solution_loader.get_slacks([m.c2]) - self.assertNotIn(m.c1, slacks2) - self.assertAlmostEqual(slacks[m.c2], slacks2[m.c2]) diff --git a/pyomo/contrib/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py index dc53f1e4543..1ecba45b32a 100644 --- a/pyomo/contrib/solver/tests/unit/test_solution.py +++ b/pyomo/contrib/solver/tests/unit/test_solution.py @@ -27,7 +27,5 @@ def test_solution_loader_base(self): self.assertEqual(self.instance.get_primals(), None) with self.assertRaises(NotImplementedError): self.instance.get_duals() - with self.assertRaises(NotImplementedError): - self.instance.get_slacks() with self.assertRaises(NotImplementedError): self.instance.get_reduced_costs() From efb1eee6d471c88a18670ed91c7baea0edbdd491 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Jan 2024 08:11:51 -0700 Subject: [PATCH 0379/3044] Apply black --- pyomo/contrib/solver/ipopt.py | 13 ++++---- pyomo/contrib/solver/results.py | 8 ++--- pyomo/contrib/solver/sol_reader.py | 8 +++-- pyomo/contrib/solver/solution.py | 30 +++++++++++++------ .../contrib/solver/tests/unit/test_results.py | 3 +- 5 files changed, 37 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 6ab40fd3924..516c0fd7f4b 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -107,7 +107,9 @@ def __init__( class ipoptSolutionLoader(SolSolutionLoader): - def get_reduced_costs(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> Mapping[_GeneralVarData, float]: + def get_reduced_costs( + self, vars_to_load: Sequence[_GeneralVarData] | None = None + ) -> Mapping[_GeneralVarData, float]: if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.variables) else: @@ -348,11 +350,7 @@ def solve(self, model, **kwds): if config.tee: ostreams.append(sys.stdout) if config.log_solver_output: - ostreams.append( - LogStream( - level=logging.INFO, logger=logger - ) - ) + ostreams.append(LogStream(level=logging.INFO, logger=logger)) with TeeStream(*ostreams) as t: timer.start('subprocess') process = subprocess.run( @@ -484,8 +482,7 @@ def _parse_solution( res.solution_loader = SolutionLoader(None, None, None) else: res.solution_loader = ipoptSolutionLoader( - sol_data=sol_data, - nl_info=nl_info, + sol_data=sol_data, nl_info=nl_info ) return res diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index fa543d0aeaa..1fa9d653d01 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -191,9 +191,7 @@ def __init__( visibility=visibility, ) - self.solution_loader = self.declare( - 'solution_loader', ConfigValue() - ) + self.solution_loader = self.declare('solution_loader', ConfigValue()) self.termination_condition: TerminationCondition = self.declare( 'termination_condition', ConfigValue( @@ -244,7 +242,9 @@ def __init__( ConfigValue(domain=str, default=None, visibility=ADVANCED_OPTION), ) - def display(self, content_filter=None, indent_spacing=2, ostream=None, visibility=0): + def display( + self, content_filter=None, indent_spacing=2, ostream=None, visibility=0 + ): return super().display(content_filter, indent_spacing, ostream, visibility) diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index 28fe0100015..a51f2cf9015 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -167,7 +167,9 @@ def parse_sol_file( for cnt in range(nvalues): suf_line = sol_file.readline().split() var_ndx = int(suf_line[0]) - sol_data.var_suffixes[suffix_name][var_ndx] = convert_function(suf_line[1]) + sol_data.var_suffixes[suffix_name][var_ndx] = convert_function( + suf_line[1] + ) elif kind == 1: # Con sol_data.con_suffixes[suffix_name] = dict() for cnt in range(nvalues): @@ -181,7 +183,9 @@ def parse_sol_file( for cnt in range(nvalues): suf_line = sol_file.readline().split() obj_ndx = int(suf_line[0]) - sol_data.obj_suffixes[suffix_name][obj_ndx] = convert_function(suf_line[1]) + sol_data.obj_suffixes[suffix_name][obj_ndx] = convert_function( + suf_line[1] + ) elif kind == 3: # Prob sol_data.problem_suffixes[suffix_name] = list() for cnt in range(nvalues): diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 18fd96759cf..7dd882d9745 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -33,7 +33,7 @@ def load_vars( ---------- vars_to_load: list The minimum set of variables whose solution should be loaded. If vars_to_load is None, then the solution - to all primal variables will be loaded. Even if vars_to_load is specified, the values of other + to all primal variables will be loaded. Even if vars_to_load is specified, the values of other variables may also be loaded depending on the interface. """ for v, val in self.get_primals(vars_to_load=vars_to_load).items(): @@ -187,21 +187,27 @@ def load_vars( scale_list = [1] * len(self._nl_info.variables) else: scale_list = self._nl_info.scaling.variables - for v, val, scale in zip(self._nl_info.variables, self._sol_data.primals, scale_list): - v.set_value(val/scale, skip_validation=True) + for v, val, scale in zip( + self._nl_info.variables, self._sol_data.primals, scale_list + ): + v.set_value(val / scale, skip_validation=True) for v, v_expr in self._nl_info.eliminated_vars: v.set_value(value(v_expr), skip_validation=True) StaleFlagManager.mark_all_as_stale(delayed=True) - def get_primals(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> Mapping[_GeneralVarData, float]: + def get_primals( + self, vars_to_load: Sequence[_GeneralVarData] | None = None + ) -> Mapping[_GeneralVarData, float]: if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.variables) else: scale_list = self._nl_info.scaling.variables val_map = dict() - for v, val, scale in zip(self._nl_info.variables, self._sol_data.primals, scale_list): + for v, val, scale in zip( + self._nl_info.variables, self._sol_data.primals, scale_list + ): val_map[id(v)] = val / scale for v, v_expr in self._nl_info.eliminated_vars: @@ -211,13 +217,17 @@ def get_primals(self, vars_to_load: Sequence[_GeneralVarData] | None = None) -> res = ComponentMap() if vars_to_load is None: - vars_to_load = self._nl_info.variables + [v for v, _ in self._nl_info.eliminated_vars] + vars_to_load = self._nl_info.variables + [ + v for v, _ in self._nl_info.eliminated_vars + ] for v in vars_to_load: res[v] = val_map[id(v)] return res - - def get_duals(self, cons_to_load: Sequence[_GeneralConstraintData] | None = None) -> Dict[_GeneralConstraintData, float]: + + def get_duals( + self, cons_to_load: Sequence[_GeneralConstraintData] | None = None + ) -> Dict[_GeneralConstraintData, float]: if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.constraints) else: @@ -227,7 +237,9 @@ def get_duals(self, cons_to_load: Sequence[_GeneralConstraintData] | None = None else: cons_to_load = set(cons_to_load) res = dict() - for c, val, scale in zip(self._nl_info.constraints, self._sol_data.duals, scale_list): + for c, val, scale in zip( + self._nl_info.constraints, self._sol_data.duals, scale_list + ): if c in cons_to_load: res[c] = val * scale return res diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 927ab64ee12..23c2c32f819 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -83,7 +83,7 @@ def test_declared_items(self): 'termination_condition', 'timing_info', 'solver_log', - 'solver_configuration' + 'solver_configuration', } actual_declared = res._declared self.assertEqual(expected_declared, actual_declared) @@ -166,4 +166,3 @@ def test_results(self): rc2 = res.solution_loader.get_reduced_costs([m.y]) self.assertNotIn(m.x, rc2) self.assertAlmostEqual(rc[id(m.y)][1], rc2[m.y]) - From 142dc30f8b0379be599defa8fc20d63ccf9f12c9 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Jan 2024 08:44:22 -0700 Subject: [PATCH 0380/3044] Convert typing to spre-3.10 supported syntax --- pyomo/contrib/solver/solution.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 7dd882d9745..33a3b1c939c 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -198,7 +198,7 @@ def load_vars( StaleFlagManager.mark_all_as_stale(delayed=True) def get_primals( - self, vars_to_load: Sequence[_GeneralVarData] | None = None + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.variables) @@ -226,7 +226,7 @@ def get_primals( return res def get_duals( - self, cons_to_load: Sequence[_GeneralConstraintData] | None = None + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None ) -> Dict[_GeneralConstraintData, float]: if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.constraints) From 7fd0c98ed8c1afdf2f5cbf4c24f42b7ef2aae7a8 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Jan 2024 08:52:42 -0700 Subject: [PATCH 0381/3044] Add in DevError check for number of options --- pyomo/contrib/solver/sol_reader.py | 40 +++++++++++++++++++----------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index a51f2cf9015..68654a4e9d7 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -1,12 +1,21 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + from typing import Tuple, Dict, Any, List import io -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _ConstraintData -from pyomo.core.base.objective import _ObjectiveData +from pyomo.common.errors import DeveloperError from pyomo.repn.plugins.nl_writer import NLWriterInfo from .results import Results, SolverResultsError, SolutionStatus, TerminationCondition -from pyomo.repn.plugins.nl_writer import AMPLRepn class SolFileData: @@ -44,20 +53,21 @@ def parse_sol_file( if "Options" in line: line = sol_file.readline() number_of_options = int(line) - need_tolerance = False - if ( - number_of_options > 4 - ): # MRM: Entirely unclear why this is necessary, or if it even is - number_of_options -= 2 - need_tolerance = True + # We are adding in this DeveloperError to see if the alternative case + # is ever actually hit in the wild. In a previous iteration of the sol + # reader, there was logic to check for the number of options, but it + # was uncovered by tests and unclear if actually necessary. + if number_of_options > 4: + raise DeveloperError( + """ +The sol file reader has hit an unexpected error while parsing. The number of +options recorded is greater than 4. Please report this error to the Pyomo +developers. + """ + ) for i in range(number_of_options + 4): line = sol_file.readline() model_objects.append(int(line)) - if ( - need_tolerance - ): # MRM: Entirely unclear why this is necessary, or if it even is - line = sol_file.readline() - model_objects.append(float(line)) else: raise SolverResultsError("ERROR READING `sol` FILE. No 'Options' line found.") # Identify the total number of variables and constraints From f456f3736cb06b36919ec9d7834b318020cd18c8 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Jan 2024 08:59:16 -0700 Subject: [PATCH 0382/3044] Missed pre-3.10 syntax error --- pyomo/contrib/solver/ipopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 516c0fd7f4b..1a153422eb1 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -108,7 +108,7 @@ def __init__( class ipoptSolutionLoader(SolSolutionLoader): def get_reduced_costs( - self, vars_to_load: Sequence[_GeneralVarData] | None = None + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.variables) From 2b1596b7ccda4b3a68747d5c6916b0c7570dae85 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 04:50:02 -0700 Subject: [PATCH 0383/3044] fixing simplification tests --- .github/workflows/test_branches.yml | 2 +- pyomo/contrib/simplification/simplify.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 89cb12d3eac..9743e45be41 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -655,7 +655,7 @@ jobs: run: | $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface; print(GinacInterface)" $PYTHON_EXE -c "from pyomo.contrib.simplification.simplify import ginac_available; print(ginac_available)" - pytest -v pyomo/contrib/simplification/tests/test_simplification.py + pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 4002f1a233f..5c0c5b859e7 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -6,7 +6,6 @@ try: from pyomo.contrib.simplification.ginac_interface import GinacInterface - ginac_available = True except: GinacInterface = None From d75c5f892a5dce5d6841c11c9dc02494c61e87b5 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 04:55:31 -0700 Subject: [PATCH 0384/3044] fixing simplification tests --- .github/workflows/test_branches.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 9743e45be41..2384ea58e2a 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -162,9 +162,9 @@ jobs: run: | pwd cd .. - curl https://www.ginac.de/CLN/cln-1.3.6.tar.bz2 >cln-1.3.6.tar.bz2 - tar -xvf cln-1.3.6.tar.bz2 - cd cln-1.3.6 + curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 + tar -xvf cln-1.3.7.tar.bz2 + cd cln-1.3.7 ./configure make -j 2 sudo make install From dae02054827c2ae23d608e07a2e6913a15180631 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 05:00:16 -0700 Subject: [PATCH 0385/3044] run black --- pyomo/contrib/simplification/simplify.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 5c0c5b859e7..4002f1a233f 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -6,6 +6,7 @@ try: from pyomo.contrib.simplification.ginac_interface import GinacInterface + ginac_available = True except: GinacInterface = None From 313108d131f04e80deb59862e95099a731c84006 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 05:15:38 -0700 Subject: [PATCH 0386/3044] fixing simplification tests --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 2384ea58e2a..60f971c0c51 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -657,7 +657,7 @@ jobs: $PYTHON_EXE -c "from pyomo.contrib.simplification.simplify import ginac_available; print(ginac_available)" pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py $PYTHON_EXE -m pytest -v \ - -W ignore::Warning ${{matrix.category}} \ + ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" From 5ff4d4d3b03370580da4578e141b98d2bdaaadf0 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 05:24:22 -0700 Subject: [PATCH 0387/3044] fixing simplification tests --- .github/workflows/test_branches.yml | 2 +- setup.cfg | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 60f971c0c51..5ed5f908d28 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -97,7 +97,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest or simplification'" + category: "-m 'simplification'" skip_doctest: 1 TARGET: linux PYENV: pip diff --git a/setup.cfg b/setup.cfg index 855717490b3..e8b6933bbbc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,6 @@ license_files = LICENSE.md universal=1 [tool:pytest] -filterwarnings = ignore::RuntimeWarning junit_family = xunit2 markers = default: mark a test that should always run by default From 010a997ff78390af504d2dc0493cf8e96ee5fd7f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 05:47:50 -0700 Subject: [PATCH 0388/3044] fixing simplification tests --- .github/workflows/test_branches.yml | 9 +++------ setup.cfg | 1 + 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 5ed5f908d28..b766583e259 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -97,7 +97,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'simplification'" + category: "-m 'neos or importtest or simplification'" skip_doctest: 1 TARGET: linux PYENV: pip @@ -653,11 +653,8 @@ jobs: - name: Run Pyomo tests if: matrix.mpi == 0 run: | - $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface; print(GinacInterface)" - $PYTHON_EXE -c "from pyomo.contrib.simplification.simplify import ginac_available; print(ginac_available)" - pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py - $PYTHON_EXE -m pytest -v \ - ${{matrix.category}} \ + pytest -v \ + -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" diff --git a/setup.cfg b/setup.cfg index e8b6933bbbc..855717490b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,6 +5,7 @@ license_files = LICENSE.md universal=1 [tool:pytest] +filterwarnings = ignore::RuntimeWarning junit_family = xunit2 markers = default: mark a test that should always run by default From 2c59b2930d2d1b41dde9f919810a3028f248ed1d Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 06:22:27 -0700 Subject: [PATCH 0389/3044] fixing simplification tests --- .github/workflows/test_branches.yml | 9 +++++++-- pyomo/core/expr/compare.py | 14 +------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index b766583e259..15880896961 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -97,7 +97,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest or simplification'" + category: "-m 'neos or importtest'" skip_doctest: 1 TARGET: linux PYENV: pip @@ -653,11 +653,16 @@ jobs: - name: Run Pyomo tests if: matrix.mpi == 0 run: | - pytest -v \ + $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" + - name: Run Simplification Tests + if: matrix.other == '/singletest' + run: | + pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" + - name: Run Pyomo MPI tests if: matrix.mpi != 0 run: | diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py index 96913f1de39..ec8d56896b8 100644 --- a/pyomo/core/expr/compare.py +++ b/pyomo/core/expr/compare.py @@ -195,19 +195,7 @@ def compare_expressions(expr1, expr2, include_named_exprs=True): expr2, include_named_exprs=include_named_exprs ) try: - res = True - if len(pn1) != len(pn2): - res = False - if res: - for a, b in zip(pn1, pn2): - if a.__class__ is not b.__class__: - res = False - break - if a == b: - continue - else: - res = False - break + res = pn1 == pn2 except PyomoException: res = False return res From d67d90d8c1226d0afa96ff700e173819eb822b7f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 06:25:16 -0700 Subject: [PATCH 0390/3044] fixing simplification tests --- .github/workflows/test_branches.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 15880896961..5eac447fd02 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -650,6 +650,11 @@ jobs: pyomo help --transformations || exit 1 pyomo help --writers || exit 1 + - name: Run Simplification Tests + if: matrix.other == '/singletest' + run: | + pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" + - name: Run Pyomo tests if: matrix.mpi == 0 run: | @@ -658,11 +663,6 @@ jobs: pyomo `pwd`/pyomo-model-libraries \ `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" - - name: Run Simplification Tests - if: matrix.other == '/singletest' - run: | - pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" - - name: Run Pyomo MPI tests if: matrix.mpi != 0 run: | From 3fcec359e1f70842af03fe9fdb8b0629785a1b64 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 06:31:28 -0700 Subject: [PATCH 0391/3044] fixing simplification tests --- .github/workflows/test_branches.yml | 1 - .github/workflows/test_pr_and_main.yml | 14 +++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 5eac447fd02..d66451a00a5 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -160,7 +160,6 @@ jobs: - name: install ginac if: matrix.other == '/singletest' run: | - pwd cd .. curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 tar -xvf cln-1.3.7.tar.bz2 diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 9b82c565c32..bda6014b352 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -98,7 +98,7 @@ jobs: - os: ubuntu-latest python: '3.11' other: /singletest - category: "-m 'neos or importtest or simplification'" + category: "-m 'neos or importtest'" skip_doctest: 1 TARGET: linux PYENV: pip @@ -183,9 +183,9 @@ jobs: if: matrix.other == '/singletest' run: | cd .. - curl https://www.ginac.de/CLN/cln-1.3.6.tar.bz2 >cln-1.3.6.tar.bz2 - tar -xvf cln-1.3.6.tar.bz2 - cd cln-1.3.6 + curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 + tar -xvf cln-1.3.7.tar.bz2 + cd cln-1.3.7 ./configure make -j 2 sudo make install @@ -671,10 +671,14 @@ jobs: pyomo help --transformations || exit 1 pyomo help --writers || exit 1 + - name: Run Simplification Tests + if: matrix.other == '/singletest' + run: | + pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" + - name: Run Pyomo tests if: matrix.mpi == 0 run: | - $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface" $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ From b5550fecb393d2ee9eeebba0e3df66b7360a10d9 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 06:38:34 -0700 Subject: [PATCH 0392/3044] fixing simplification tests --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index d66451a00a5..83e652cbef8 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -652,7 +652,7 @@ jobs: - name: Run Simplification Tests if: matrix.other == '/singletest' run: | - pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" + pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" - name: Run Pyomo tests if: matrix.mpi == 0 diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index bda6014b352..6df28fbadc9 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -674,7 +674,7 @@ jobs: - name: Run Simplification Tests if: matrix.other == '/singletest' run: | - pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" + pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" - name: Run Pyomo tests if: matrix.mpi == 0 From 8984389e71d57fe7b9d56c80331ea207166f1290 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 06:52:04 -0700 Subject: [PATCH 0393/3044] fixing simplification tests --- pyomo/core/expr/compare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py index ec8d56896b8..61ff8660a8b 100644 --- a/pyomo/core/expr/compare.py +++ b/pyomo/core/expr/compare.py @@ -196,7 +196,7 @@ def compare_expressions(expr1, expr2, include_named_exprs=True): ) try: res = pn1 == pn2 - except PyomoException: + except (PyomoException, AttributeError): res = False return res From 7d64e1725b1af5173c9ff94cb885913ee1c834c2 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 22 Jan 2024 15:10:07 -0700 Subject: [PATCH 0394/3044] add get_config_from_kwds to use instead of hacking ConfigDict --- pyomo/contrib/incidence_analysis/config.py | 90 +++++++++++-------- pyomo/contrib/incidence_analysis/incidence.py | 18 ++-- pyomo/contrib/incidence_analysis/interface.py | 12 +-- 3 files changed, 70 insertions(+), 50 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/config.py b/pyomo/contrib/incidence_analysis/config.py index 036c563ae75..4ab086da508 100644 --- a/pyomo/contrib/incidence_analysis/config.py +++ b/pyomo/contrib/incidence_analysis/config.py @@ -69,45 +69,16 @@ class _ReconstructVisitor: pass -def _amplrepnvisitor_validator(visitor=_ReconstructVisitor): - # This checks for and returns a valid AMPLRepnVisitor, but I don't want - # to construct this if we're not using IncidenceMethod.ampl_repn. - # It is not necessarily the end of the world if we construct this, however, - # as the code should still work. - if visitor is _ReconstructVisitor: - subexpression_cache = {} - subexpression_order = [] - external_functions = {} - var_map = {} - used_named_expressions = set() - symbolic_solver_labels = False - # TODO: Explore potential performance benefit of exporting defined variables. - # This likely only shows up if we can preserve the subexpression cache across - # multiple constraint expressions. - export_defined_variables = False - sorter = FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) - amplvisitor = AMPLRepnVisitor( - text_nl_template, - subexpression_cache, - subexpression_order, - external_functions, - var_map, - used_named_expressions, - symbolic_solver_labels, - export_defined_variables, - sorter, - ) - elif not isinstance(visitor, AMPLRepnVisitor): +def _amplrepnvisitor_validator(visitor): + if not isinstance(visitor, AMPLRepnVisitor): raise TypeError( "'visitor' config argument should be an instance of AMPLRepnVisitor" ) - else: - amplvisitor = visitor - return amplvisitor + return visitor _ampl_repn_visitor = ConfigValue( - default=_ReconstructVisitor, + default=None, domain=_amplrepnvisitor_validator, description="Visitor used to generate AMPLRepn of each constraint", ) @@ -141,14 +112,14 @@ def __call__( if ( new.method == IncidenceMethod.ampl_repn - and "ampl_repn_visitor" not in init_value + and "_ampl_repn_visitor" not in init_value ): - new.ampl_repn_visitor = _ReconstructVisitor + new._ampl_repn_visitor = _ReconstructVisitor return new -IncidenceConfig = _IncidenceConfigDict() +IncidenceConfig = ConfigDict() """Options for incidence graph generation - ``include_fixed`` -- Flag indicating whether fixed variables should be included @@ -157,8 +128,9 @@ def __call__( should be included. - ``method`` -- Method used to identify incident variables. Must be a value of the ``IncidenceMethod`` enum. -- ``ampl_repn_visitor`` -- Expression visitor used to generate ``AMPLRepn`` of each - constraint. Must be an instance of ``AMPLRepnVisitor``. +- ``_ampl_repn_visitor`` -- Expression visitor used to generate ``AMPLRepn`` of each + constraint. Must be an instance of ``AMPLRepnVisitor``. *This option is constructed + automatically when needed and should not be set by users!* """ @@ -172,4 +144,44 @@ def __call__( IncidenceConfig.declare("method", _method) -IncidenceConfig.declare("ampl_repn_visitor", _ampl_repn_visitor) +IncidenceConfig.declare("_ampl_repn_visitor", _ampl_repn_visitor) + + +def get_config_from_kwds(**kwds): + """Get an instance of IncidenceConfig from provided keyword arguments. + + If the ``method`` argument is ``IncidenceMethod.ampl_repn`` and no + ``AMPLRepnVisitor`` has been provided, a new ``AMPLRepnVisitor`` is + constructed. This function should generally be used by callers such + as ``IncidenceGraphInterface`` to ensure that a visitor is created then + re-used when calling ``get_incident_variables`` in a loop. + + """ + if ( + kwds.get("method", None) is IncidenceMethod.ampl_repn + and kwds.get("_ampl_repn_visitor", None) is None + ): + subexpression_cache = {} + subexpression_order = [] + external_functions = {} + var_map = {} + used_named_expressions = set() + symbolic_solver_labels = False + # TODO: Explore potential performance benefit of exporting defined variables. + # This likely only shows up if we can preserve the subexpression cache across + # multiple constraint expressions. + export_defined_variables = False + sorter = FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) + amplvisitor = AMPLRepnVisitor( + text_nl_template, + subexpression_cache, + subexpression_order, + external_functions, + var_map, + used_named_expressions, + symbolic_solver_labels, + export_defined_variables, + sorter, + ) + kwds["_ampl_repn_visitor"] = amplvisitor + return IncidenceConfig(kwds) diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index 17307e89600..1fc3380fe6b 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -19,7 +19,9 @@ from pyomo.repn.plugins.nl_writer import AMPLRepnVisitor, AMPLRepn, text_nl_template from pyomo.repn.util import FileDeterminism, FileDeterminism_to_SortComponents from pyomo.util.subsystems import TemporarySubsystemManager -from pyomo.contrib.incidence_analysis.config import IncidenceMethod, IncidenceConfig +from pyomo.contrib.incidence_analysis.config import ( + IncidenceMethod, get_config_from_kwds +) # @@ -148,17 +150,24 @@ def get_incident_variables(expr, **kwds): ['x[1]', 'x[2]'] """ - config = IncidenceConfig(kwds) + config = get_config_from_kwds(**kwds) method = config.method include_fixed = config.include_fixed linear_only = config.linear_only - amplrepnvisitor = config.ampl_repn_visitor + amplrepnvisitor = config._ampl_repn_visitor + + # Check compatibility of arguments if linear_only and method is IncidenceMethod.identify_variables: raise RuntimeError( "linear_only=True is not supported when using identify_variables" ) if include_fixed and method is IncidenceMethod.ampl_repn: raise RuntimeError("include_fixed=True is not supported when using ampl_repn") + if method is IncidenceMethod.ampl_repn and amplrepnvisitor is None: + # Developer error, this should never happen! + raise RuntimeError("_ampl_repn_visitor must be provided when using ampl_repn") + + # Dispatch to correct method if method is IncidenceMethod.identify_variables: return _get_incident_via_identify_variables(expr, include_fixed) elif method is IncidenceMethod.standard_repn: @@ -174,6 +183,5 @@ def get_incident_variables(expr, **kwds): else: raise ValueError( f"Unrecognized value {method} for the method used to identify incident" - f" variables. Valid options are {IncidenceMethod.identify_variables}" - f" and {IncidenceMethod.standard_repn}." + f" variables. See the IncidenceMethod enum for valid methods." ) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index b8a6c1275f9..41f0ece3a75 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -29,7 +29,7 @@ plotly, ) from pyomo.common.deprecation import deprecated -from pyomo.contrib.incidence_analysis.config import IncidenceConfig, IncidenceMethod +from pyomo.contrib.incidence_analysis.config import get_config_from_kwds from pyomo.contrib.incidence_analysis.matching import maximum_matching from pyomo.contrib.incidence_analysis.connected import get_independent_submatrices from pyomo.contrib.incidence_analysis.triangularize import ( @@ -64,7 +64,7 @@ def _check_unindexed(complist): def get_incidence_graph(variables, constraints, **kwds): - config = IncidenceConfig(kwds) + config = get_config_from_kwds(**kwds) return get_bipartite_incidence_graph(variables, constraints, **config) @@ -95,7 +95,7 @@ def get_bipartite_incidence_graph(variables, constraints, **kwds): """ # Note that this ConfigDict contains the visitor that we will re-use # when constructing constraints. - config = IncidenceConfig(kwds) + config = get_config_from_kwds(**kwds) _check_unindexed(variables + constraints) N = len(variables) M = len(constraints) @@ -168,7 +168,7 @@ def extract_bipartite_subgraph(graph, nodes0, nodes1): def _generate_variables_in_constraints(constraints, **kwds): # Note: We construct a visitor here - config = IncidenceConfig(kwds) + config = get_config_from_kwds(**kwds) known_vars = ComponentSet() for con in constraints: for var in get_incident_variables(con.body, **config): @@ -196,7 +196,7 @@ def get_structural_incidence_matrix(variables, constraints, **kwds): Entries are 1.0. """ - config = IncidenceConfig(kwds) + config = get_config_from_kwds(**kwds) _check_unindexed(variables + constraints) N, M = len(variables), len(constraints) var_idx_map = ComponentMap((v, i) for i, v in enumerate(variables)) @@ -279,7 +279,7 @@ def __init__(self, model=None, active=True, include_inequality=True, **kwds): # to cache the incidence graph for fast analysis later on. # WARNING: This cache will become invalid if the user alters their # model. - self._config = IncidenceConfig(kwds) + self._config = get_config_from_kwds(**kwds) if model is None: self._incidence_graph = None self._variables = None From 57d3134725a82f150e4b63da2f32b93ade02d201 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 22 Jan 2024 15:11:22 -0700 Subject: [PATCH 0395/3044] remove now-unused ConfigDict hack --- pyomo/contrib/incidence_analysis/config.py | 39 ---------------------- 1 file changed, 39 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/config.py b/pyomo/contrib/incidence_analysis/config.py index 4ab086da508..d055be478fe 100644 --- a/pyomo/contrib/incidence_analysis/config.py +++ b/pyomo/contrib/incidence_analysis/config.py @@ -65,10 +65,6 @@ class IncidenceMethod(enum.Enum): ) -class _ReconstructVisitor: - pass - - def _amplrepnvisitor_validator(visitor): if not isinstance(visitor, AMPLRepnVisitor): raise TypeError( @@ -84,41 +80,6 @@ def _amplrepnvisitor_validator(visitor): ) -class _IncidenceConfigDict(ConfigDict): - def __call__( - self, - value=NOTSET, - default=NOTSET, - domain=NOTSET, - description=NOTSET, - doc=NOTSET, - visibility=NOTSET, - implicit=NOTSET, - implicit_domain=NOTSET, - preserve_implicit=False, - ): - init_value = value - new = super().__call__( - value=value, - default=default, - domain=domain, - description=description, - doc=doc, - visibility=visibility, - implicit=implicit, - implicit_domain=implicit_domain, - preserve_implicit=preserve_implicit, - ) - - if ( - new.method == IncidenceMethod.ampl_repn - and "_ampl_repn_visitor" not in init_value - ): - new._ampl_repn_visitor = _ReconstructVisitor - - return new - - IncidenceConfig = ConfigDict() """Options for incidence graph generation From f279fed36fe137a7d7bb33ebf48c4bd05d9f7619 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 22 Jan 2024 15:22:42 -0700 Subject: [PATCH 0396/3044] split imports onto separate lines --- pyomo/contrib/incidence_analysis/incidence.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index 1fc3380fe6b..636a400def4 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -20,7 +20,8 @@ from pyomo.repn.util import FileDeterminism, FileDeterminism_to_SortComponents from pyomo.util.subsystems import TemporarySubsystemManager from pyomo.contrib.incidence_analysis.config import ( - IncidenceMethod, get_config_from_kwds + IncidenceMethod, + get_config_from_kwds, ) From 0dff86021bcf420d1e8cdc9731b86bdb8c338dcf Mon Sep 17 00:00:00 2001 From: jlgearh Date: Tue, 23 Jan 2024 14:00:56 -0700 Subject: [PATCH 0397/3044] - Fixed issue where constants in expressions were being double counted in shifted_lp.py --- .../contrib/alternative_solutions/lp_enum.py | 160 +++++++++++++++++- pyomo/contrib/alternative_solutions/obbt.py | 3 +- .../alternative_solutions/shifted_lp.py | 33 +++- .../tests/run_lp_enum.py | 4 +- .../tests/test_shifted_lp.py | 3 +- .../tests/test_solution.py | 2 +- 6 files changed, 184 insertions(+), 21 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index a9cb36803e4..21418fad1ce 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -10,14 +10,164 @@ # ___________________________________________________________________________ import pyomo.environ as pe -from pyomo.contrib.alternative_solutions import aos_utils, shifted_lp, solution +from pyomo.contrib.alternative_solutions import aos_utils, shifted_lp, \ + solution, solnpool +def enumerate_linear_solutions_soln_pool(model, num_solutions=10, + variables='all', rel_opt_gap=None, + abs_opt_gap=None, + solver_options={}, tee=False): + ''' + Finds alternative optimal solutions a (mixed-integer) linear program using + Gurobi's solution pool feature. + + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model + num_solutions : int + The maximum number of solutions to generate. + variables: 'all' or a collection of Pyomo _GeneralVarData variables + The variables for which bounds will be generated. 'all' indicates + that all variables will be included. Alternatively, a collection of + _GenereralVarData variables can be provided. + rel_opt_gap : float or None + The relative optimality gap for the original objective for which + variable bounds will be found. None indicates that a relative gap + constraint will not be added to the model. + abs_opt_gap : float or None + The absolute optimality gap for the original objective for which + variable bounds will be found. None indicates that an absolute gap + constraint will not be added to the model. + solver_options : dict + Solver option-value pairs to be passed to the solver. + tee : boolean + Boolean indicating that the solver output should be displayed. + + Returns + ------- + solutions + A list of Solution objects. + [Solution] + ''' + opt = pe.SolverFactory('gurobi') + print('STARTING LP ENUMERATION ANALYSIS USING GUROBI SOLUTION POOL') + + # For now keeping things simple + # TODO: Relax this + assert variables == 'all' + + opt = pe.SolverFactory('gurobi') + for parameter, value in solver_options.items(): + opt.options[parameter] = value + + print('Peforming initial solve of model.') + results = opt.solve(model, tee=tee) + status = results.solver.status + condition = results.solver.termination_condition + if condition != pe.TerminationCondition.optimal: + raise Exception(('Model could not be solve. LP enumeration analysis ' + 'cannot be applied, SolverStatus = {}, ' + 'TerminationCondition = {}').format(status.value, + condition.value)) + + orig_objective = aos_utils._get_active_objective(model) + orig_objective_value = pe.value(orig_objective) + print('Found optimal solution, value = {}.'.format(orig_objective_value)) + + aos_block = aos_utils._add_aos_block(model, name='_lp_enum') + print('Added block {} to the model.'.format(aos_block)) + aos_utils._add_objective_constraint(aos_block, orig_objective, + orig_objective_value, rel_opt_gap, + abs_opt_gap) + + cannonical_block = shifted_lp.get_shifted_linear_model(model) + cb = cannonical_block + + # w variables + cb.basic_lower = pe.Var(cb.var_lower_index, domain=pe.Binary) + cb.basic_upper = pe.Var(cb.var_upper_index, domain=pe.Binary) + cb.basic_slack = pe.Var(cb.slack_index, domain=pe.Binary) + + # w upper bounds constraints + def bound_lower_rule(m, var_index): + return m.var_lower[var_index] <= m.var_lower[var_index].ub \ + * m.basic_lower[var_index] + cb.bound_lower = pe.Constraint(cb.var_lower_index,rule=bound_lower_rule) + + def bound_upper_rule(m, var_index): + return m.var_upper[var_index] <= m.var_upper[var_index].ub \ + * m.basic_upper[var_index] + cb.bound_upper = pe.Constraint(cb.var_upper_index,rule=bound_upper_rule) + + def bound_slack_rule(m, var_index): + return m.slack_vars[var_index] <= m.slack_vars[var_index].ub \ + * m.basic_slack[var_index] + cb.bound_slack = pe.Constraint(cb.slack_index,rule=bound_slack_rule) + cb.pprint() + results = solnpool.gurobi_generate_solutions(cb, num_solutions) + + # print('Solving Iteration {}: '.format(solution_number), end='') + # results = opt.solve(cb, tee=tee) + # status = results.solver.status + # condition = results.solver.termination_condition + # if condition == pe.TerminationCondition.optimal: + # for var, index in cb.var_map.items(): + # var.set_value(var.lb + cb.var_lower[index].value) + # sol = solution.Solution(model, all_variables, + # objective=orig_objective) + # solutions.append(sol) + # orig_objective_value = sol.objective[1] + # print('Solved, objective = {}'.format(orig_objective_value)) + # for var, index in cb.var_map.items(): + # print('{} = {}'.format(var.name, var.lb + cb.var_lower[index].value)) + # if hasattr(cb, 'force_out'): + # cb.del_component('force_out') + # if hasattr(cb, 'link_in_out'): + # cb.del_component('link_in_out') + + # if hasattr(cb, 'basic_last_lower'): + # cb.del_component('basic_last_lower') + # if hasattr(cb, 'basic_last_upper'): + # cb.del_component('basic_last_upper') + # if hasattr(cb, 'basic_last_slack'): + # cb.del_component('basic_last_slack') + + # cb.link_in_out = pe.Constraint(pe.Any) + # cb.basic_last_lower = pe.Var(pe.Any, domain=pe.Binary, dense=False) + # cb.basic_last_upper = pe.Var(pe.Any, domain=pe.Binary, dense=False) + # cb.basic_last_slack = pe.Var(pe.Any, domain=pe.Binary, dense=False) + # basic_last_list = [cb.basic_last_lower, cb.basic_last_upper, + # cb.basic_last_slack] + + # num_non_zero = 0 + # force_out_expr = -1 + # non_zero_basic_expr = 1 + # for idx in range(len(variable_groups)): + # continuous_var, binary_var, constraint = variable_groups[idx] + # for var in continuous_var: + # if continuous_var[var].value > zero_threshold: + # num_non_zero += 1 + # if var not in binary_var: + # binary_var[var] + # constraint[var] = continuous_var[var] <= \ + # continuous_var[var].ub * binary_var[var] + # non_zero_basic_expr += binary_var[var] + # basic_var = basic_last_list[idx][var] + # force_out_expr += basic_var + # cb.link_in_out[var] = basic_var + binary_var[var] <= 1 + + # aos_block.deactivate() + # print('COMPLETED LP ENUMERATION ANALYSIS') + + # return solutions + def enumerate_linear_solutions(model, num_solutions=10, variables='all', rel_opt_gap=None, abs_opt_gap=None, - search_mode='optimal', solver='cplex', + search_mode='optimal', solver='gurobi', solver_options={}, tee=False): ''' - Finds alternative optimal solutions for a binary problem. + Finds alternative optimal solutions a (mixed-integer) linear program. Parameters ---------- @@ -61,7 +211,7 @@ def enumerate_linear_solutions(model, num_solutions=10, variables='all', print('STARTING LP ENUMERATION ANALYSIS') # For now keeping things simple - # TODO: Relax this + # TODO: See if this can be relaxed assert variables == 'all' assert search_mode in ['optimal', 'random', 'norm'], \ @@ -120,7 +270,7 @@ def enumerate_linear_solutions(model, num_solutions=10, variables='all', status = results.solver.status condition = results.solver.termination_condition if condition != pe.TerminationCondition.optimal: - raise Exception(('Model could not be solve. LP enumeration analysis ' + raise Exception(('Model could not be solved. LP enumeration analysis ' 'cannot be applied, SolverStatus = {}, ' 'TerminationCondition = {}').format(status.value, condition.value)) diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 3a1b4f1a8cd..691078bf51f 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -85,7 +85,8 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, results = opt.solve(model) condition = results.termination_condition optimal_tc = appsi.base.TerminationCondition.optimal - infeas_or_unbdd_tc = appsi.base.TerminationCondition.infeasibleOrUnbounded + infeas_or_unbdd_tc = appsi.base.TerminationCondition.\ + infeasibleOrUnbounded unbdd_tc = appsi.base.TerminationCondition.unbounded use_appsi = True else: diff --git a/pyomo/contrib/alternative_solutions/shifted_lp.py b/pyomo/contrib/alternative_solutions/shifted_lp.py index 3c8baee2a48..f182ddb7157 100644 --- a/pyomo/contrib/alternative_solutions/shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/shifted_lp.py @@ -10,12 +10,10 @@ # ___________________________________________________________________________ import pyomo.environ as pe - from pyomo.common.collections import ComponentMap from pyomo.gdp.util import clone_without_expression_components from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.alternative_solutions import aos_utils -import pdb def _get_unique_name(collection, name): '''Create a unique name for an item that will be added to a collection.''' @@ -46,7 +44,7 @@ def get_shifted_linear_model(model, block=None): s.t. A_1 * x = b_1 A_2 * x <= b_2 - l <= x <= uf + l <= x <= u a problem of the form, @@ -82,6 +80,7 @@ def get_shifted_linear_model(model, block=None): # Gather all variables and confirm the model is bounded all_vars = aos_utils.get_model_variables(model, 'all') new_vars = {} + all_vars_new = {} var_map = ComponentMap() var_range = {} for var in all_vars: @@ -94,8 +93,11 @@ def get_shifted_linear_model(model, block=None): if var.is_continuous(): var_name = _get_unique_name(new_vars.keys(), var.name) new_vars[var_name] = var + all_vars_new[var_name] = var var_map[var] = var_name var_range[var_name] = (0,var.ub-var.lb) + else: + all_vars_new[var.name] = var if block is None: block = model @@ -118,16 +120,22 @@ def link_vars_rule(m, var_index): var_lower_map = {id(var): shifted_lp.var_lower[i] for i, var in \ new_vars.items()} var_lower_bounds = {id(var): var.lb for var in new_vars.values()} + var_zeros = {id(var): 0 for var in all_vars_new.values()} # Substitute the new s variables into the objective function + # The c_fix_zeros calculation is used to find any constant terms that exist + # in the objective expression to avoid double counting active_objective = aos_utils._get_active_objective(model) c_var_lower = clone_without_expression_components(active_objective.expr, substitute=var_lower_map) c_fix_lower = clone_without_expression_components(active_objective.expr, substitute=var_lower_bounds) - shifted_lp.objective = pe.Objective(expr=c_var_lower + c_fix_lower, - name=active_objective.name + '_shifted', - sense=active_objective.sense) + c_fix_zeros = clone_without_expression_components(active_objective.expr, + substitute=var_zeros) + shifted_lp.objective = pe.Objective(expr=c_var_lower - c_fix_zeros + \ + c_fix_lower, + name=active_objective.name + '_shifted', + sense=active_objective.sense) # Identify all of the shifted constraints and associated slack variables # that will need to be created @@ -169,21 +177,28 @@ def link_vars_rule(m, var_index): shifted_lp.constraints = pe.Constraint(shifted_lp.constraint_index) for constraint_name, constraint in new_constraints.items(): + # The c_fix_zeros calculation is used to find any constant terms that + # exist in the constraint expression to avoid double counting a_sub_var_lower = clone_without_expression_components(constraint.body, substitute=var_lower_map) a_sub_fix_lower = clone_without_expression_components(constraint.body, substitute=var_lower_bounds) + a_sub_fix_zeros = clone_without_expression_components(constraint.body, + substitute=var_zeros) b_lower = constraint.lb b_upper = constraint.ub con_type = constraint_type[constraint_name] if con_type == 0: - expr = a_sub_var_lower + a_sub_fix_lower - b_lower == 0 + expr = a_sub_var_lower - a_sub_fix_zeros + a_sub_fix_lower \ + - b_lower == 0 elif con_type == -1: - expr_rhs = a_sub_var_lower + a_sub_fix_lower - b_lower + expr_rhs = a_sub_var_lower - a_sub_fix_zeros + a_sub_fix_lower \ + - b_lower expr = shifted_lp.slack_vars[constraint_name] == expr_rhs _set_slack_ub(expr_rhs, shifted_lp.slack_vars[constraint_name]) elif con_type == 1: - expr_rhs = b_upper - a_sub_var_lower - a_sub_fix_lower + expr_rhs = b_upper - a_sub_var_lower + a_sub_fix_zeros \ + - a_sub_fix_lower expr = shifted_lp.slack_vars[constraint_name] == expr_rhs _set_slack_ub(expr_rhs, shifted_lp.slack_vars[constraint_name]) shifted_lp.constraints[constraint_name] = expr diff --git a/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py b/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py index 98759a2d3bf..e69ce95fb75 100644 --- a/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py +++ b/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py @@ -8,7 +8,6 @@ import pyomo.contrib.alternative_solutions.tests.test_cases as tc from pyomo.contrib.alternative_solutions import lp_enum import pyomo.environ as pe -import pdb m = tc.get_3d_polyhedron_problem() m.o.deactivate() @@ -21,5 +20,4 @@ n.x.domain = pe.Reals n.y.domain = pe.Reals sols = lp_enum.enumerate_linear_solutions(n, solver='gurobi') -n.pprint() -pdb.set_trace() \ No newline at end of file +n.pprint() \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py index 8774b09c506..d2fc9f061f8 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py @@ -25,7 +25,7 @@ class TestShiftedIP(unittest.TestCase): - def mip_abs_objective(self): + def test_mip_abs_objective(self): '''COMMENT''' m = tc.get_indexed_pentagonal_pyramid_mip() m.x.domain = pe.Reals @@ -36,7 +36,6 @@ def mip_abs_objective(self): new_results = opt.solve(new_model, tee = True) new_obj = pe.value(new_model.objective) self.assertAlmostEqual(old_obj, new_obj) - pdb.set_trace() def test_polyhedron(self): m = tc.get_3d_polyhedron_problem() diff --git a/pyomo/contrib/alternative_solutions/tests/test_solution.py b/pyomo/contrib/alternative_solutions/tests/test_solution.py index 006a5f756b7..19c35c8ca5c 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solution.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solution.py @@ -14,7 +14,7 @@ import pyomo.contrib.alternative_solutions.aos_utils as au import pyomo.contrib.alternative_solutions.solution as sol -mip_solver = 'cplex' +mip_solver = 'gurobi' class TestSolutionUnit(unittest.TestCase): def get_model(self): From a49eb25f03380f6b871303d61591b0cc7be0b110 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 24 Jan 2024 13:01:06 -0500 Subject: [PATCH 0398/3044] add mindtpy call_before_subproblem_solve --- pyomo/contrib/mindtpy/algorithm_base_class.py | 13 +++++++++++++ pyomo/contrib/mindtpy/config_options.py | 9 +++++++++ pyomo/contrib/mindtpy/single_tree.py | 6 ++++++ 3 files changed, 28 insertions(+) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 7e8d390976c..f7f5e7601e5 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -2958,6 +2958,10 @@ def MindtPy_iteration_loop(self): skip_fixed=False, ) if self.curr_int_sol not in set(self.integer_list): + # Call the NLP pre-solve callback + with time_code(self.timing, 'Call after subproblem solve'): + config.call_before_subproblem_solve(self.fixed_nlp) + fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) @@ -2969,6 +2973,10 @@ def MindtPy_iteration_loop(self): # Solve NLP subproblem # The constraint linearization happens in the handlers if not config.solution_pool: + # Call the NLP pre-solve callback + with time_code(self.timing, 'Call after subproblem solve'): + config.call_before_subproblem_solve(self.fixed_nlp) + fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) @@ -3001,6 +3009,11 @@ def MindtPy_iteration_loop(self): continue else: self.integer_list.append(self.curr_int_sol) + + # Call the NLP pre-solve callback + with time_code(self.timing, 'Call after subproblem solve'): + config.call_before_subproblem_solve(self.fixed_nlp) + fixed_nlp, fixed_nlp_result = self.solve_subproblem() self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) diff --git a/pyomo/contrib/mindtpy/config_options.py b/pyomo/contrib/mindtpy/config_options.py index ed0c86baae9..b6dbbedd79c 100644 --- a/pyomo/contrib/mindtpy/config_options.py +++ b/pyomo/contrib/mindtpy/config_options.py @@ -312,6 +312,15 @@ def _add_common_configs(CONFIG): doc='Callback hook after a solution of the main problem.', ), ) + CONFIG.declare( + 'call_before_subproblem_solve', + ConfigValue( + default=_DoNothing(), + domain=None, + description='Function to be executed after every subproblem', + doc='Callback hook after a solution of the nonlinear subproblem.', + ), + ) CONFIG.declare( 'call_after_subproblem_solve', ConfigValue( diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 228810a8f90..a82bb1ce541 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -774,6 +774,9 @@ def __call__(self): mindtpy_solver.integer_list.append(mindtpy_solver.curr_int_sol) # solve subproblem + # Call the NLP pre-solve callback + with time_code(self.timing, 'Call after subproblem solve'): + config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp) # The constraint linearization happens in the handlers fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() # add oa cuts @@ -920,6 +923,9 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): cut_ind = len(mindtpy_solver.mip.MindtPy_utils.cuts.oa_cuts) # solve subproblem + # Call the NLP pre-solve callback + with time_code(self.timing, 'Call after subproblem solve'): + config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp) # The constraint linearization happens in the handlers fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() From 3dc499592476144c69790c8fcead1da63958b874 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 24 Jan 2024 16:58:41 -0500 Subject: [PATCH 0399/3044] fix bug --- pyomo/contrib/mindtpy/single_tree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index a82bb1ce541..77740ff15e3 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -775,7 +775,7 @@ def __call__(self): # solve subproblem # Call the NLP pre-solve callback - with time_code(self.timing, 'Call after subproblem solve'): + with time_code(mindtpy_solver.timing, 'Call after subproblem solve'): config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp) # The constraint linearization happens in the handlers fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() @@ -924,7 +924,7 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): # solve subproblem # Call the NLP pre-solve callback - with time_code(self.timing, 'Call after subproblem solve'): + with time_code(mindtpy_solver.timing, 'Call after subproblem solve'): config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp) # The constraint linearization happens in the handlers fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() From 19ed448f228647a2bb44dd47d1df1124dc14b3c0 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 24 Jan 2024 16:34:40 -0700 Subject: [PATCH 0400/3044] Finished parmest reactor_design example using new interface. --- .../reactor_design/bootstrap_example.py | 2 +- .../reactor_design/datarec_example.py | 2 +- .../reactor_design/leaveNout_example.py | 2 +- .../likelihood_ratio_example.py | 2 +- .../multisensor_data_example.py | 2 +- .../parameter_estimation_example.py | 6 +- .../reactor_design/timeseries_data_example.py | 5 +- .../reactor_design/bootstrap_example.py | 32 +- .../confidence_region_example.py | 49 +++ .../reactor_design/datarec_example.py | 98 ++++- .../reactor_design/leaveNout_example.py | 29 +- .../likelihood_ratio_example.py | 32 +- .../multisensor_data_example.py | 73 +++- .../parameter_estimation_example.py | 53 +-- .../examples/reactor_design/reactor_design.py | 137 ++++-- .../reactor_design/timeseries_data_example.py | 96 +++- pyomo/contrib/parmest/experiment.py | 15 + pyomo/contrib/parmest/parmest.py | 411 +++++++++--------- 18 files changed, 647 insertions(+), 399 deletions(-) create mode 100644 pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py create mode 100644 pyomo/contrib/parmest/experiment.py diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py index e2d172f34f6..3820b78c9b1 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py @@ -12,7 +12,7 @@ import pandas as pd from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( +from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py index cfd3891c00e..bae538f364c 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py @@ -12,7 +12,7 @@ import numpy as np import pandas as pd import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( +from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py index 6952a7fc733..d4ca9651753 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py @@ -13,7 +13,7 @@ import pandas as pd from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( +from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py index a0fe6f22305..c47acf7f932 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py @@ -14,7 +14,7 @@ from itertools import product from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( +from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py index a92ac626fae..84c4abdf92a 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py @@ -12,7 +12,7 @@ import pandas as pd from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( +from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py index 581d3904c04..67b69c73555 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py @@ -12,7 +12,7 @@ import pandas as pd from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( +from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) @@ -41,7 +41,9 @@ def SSE(model, data): # Parameter estimation obj, theta = pest.theta_est() - + print (obj) + print(theta) + # Assert statements compare parameter estimation (theta) to an expected value k1_expected = 5.0 / 6.0 k2_expected = 5.0 / 3.0 diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py index da2ab1874c9..e7acefc2224 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py @@ -13,9 +13,10 @@ from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( +from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) +from pyomo.contrib.parmest.deprecated.parmest import group_data def main(): @@ -31,7 +32,7 @@ def main(): # Group time series data into experiments, return the mean value for sv and caf # Returns a list of dictionaries - data_ts = parmest.group_data(data, 'experiment', ['sv', 'caf']) + data_ts = group_data(data, 'experiment', ['sv', 'caf']) def SSE_timeseries(model, data): expr = 0 diff --git a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py index e2d172f34f6..b5cb4196456 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py @@ -13,31 +13,26 @@ from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) - def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - # Data + # Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + + pest = parmest.Estimator(exp_list, obj_function='SSE') # Parameter estimation obj, theta = pest.theta_est() @@ -55,6 +50,5 @@ def SSE(model, data): title="Bootstrap theta with confidence regions", ) - if __name__ == "__main__": main() diff --git a/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py b/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py new file mode 100644 index 00000000000..ff84279018d --- /dev/null +++ b/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py @@ -0,0 +1,49 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pandas as pd +from os.path import join, abspath, dirname +import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + ReactorDesignExperiment, +) + +def main(): + + # Read in data + file_dirname = dirname(abspath(str(__file__))) + file_name = abspath(join(file_dirname, "reactor_data.csv")) + data = pd.read_csv(file_name) + + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + + pest = parmest.Estimator(exp_list, obj_function='SSE') + + # Parameter estimation + obj, theta = pest.theta_est() + + # Bootstrapping + bootstrap_theta = pest.theta_est_bootstrap(10) + print(bootstrap_theta) + + # Confidence region test + CR = pest.confidence_region_test(bootstrap_theta, "MVN", [0.5, 0.75, 1.0]) + print(CR) + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index cfd3891c00e..26185290ea6 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -11,23 +11,80 @@ import numpy as np import pandas as pd +import pyomo.environ as pyo import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( reactor_design_model, + ReactorDesignExperiment, ) np.random.seed(1234) -def reactor_design_model_for_datarec(data): +def reactor_design_model_for_datarec(): + # Unfix inlet concentration for data rec - model = reactor_design_model(data) + model = reactor_design_model() model.caf.fixed = False return model +class ReactorDesignExperimentPreDataRec(ReactorDesignExperiment): + + def __init__(self, data, data_std, experiment_number): + + super().__init__(data, experiment_number) + self.data_std = data_std + + def create_model(self): + self.model = m = reactor_design_model_for_datarec() + return m + + def label_model(self): + + m = self.model + + # experiment outputs + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.ca, self.data_i['ca'])]) + m.experiment_outputs.update([(m.cb, self.data_i['cb'])]) + m.experiment_outputs.update([(m.cc, self.data_i['cc'])]) + m.experiment_outputs.update([(m.cd, self.data_i['cd'])]) + + # experiment standard deviations + m.experiment_outputs_std = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs_std.update([(m.ca, self.data_std['ca'])]) + m.experiment_outputs_std.update([(m.cb, self.data_std['cb'])]) + m.experiment_outputs_std.update([(m.cc, self.data_std['cc'])]) + m.experiment_outputs_std.update([(m.cd, self.data_std['cd'])]) + + # no unknowns (theta names) + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + + return m + +class ReactorDesignExperimentPostDataRec(ReactorDesignExperiment): + + def __init__(self, data, data_std, experiment_number): + + super().__init__(data, experiment_number) + self.data_std = data_std + + def label_model(self): + + m = super().label_model() + + # add experiment standard deviations + m.experiment_outputs_std = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs_std.update([(m.ca, self.data_std['ca'])]) + m.experiment_outputs_std.update([(m.cb, self.data_std['cb'])]) + m.experiment_outputs_std.update([(m.cc, self.data_std['cc'])]) + m.experiment_outputs_std.update([(m.cd, self.data_std['cd'])]) + + return m def generate_data(): + ### Generate data based on real sv, caf, ca, cb, cc, and cd sv_real = 1.05 caf_real = 10000 @@ -54,29 +111,34 @@ def generate_data(): def main(): + # Generate data data = generate_data() data_std = data.std() + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperimentPreDataRec(data, data_std, i)) + # Define sum of squared error objective function for data rec - def SSE(model, data): - expr = ( - ((float(data.iloc[0]["ca"]) - model.ca) / float(data_std["ca"])) ** 2 - + ((float(data.iloc[0]["cb"]) - model.cb) / float(data_std["cb"])) ** 2 - + ((float(data.iloc[0]["cc"]) - model.cc) / float(data_std["cc"])) ** 2 - + ((float(data.iloc[0]["cd"]) - model.cd) / float(data_std["cd"])) ** 2 - ) + def SSE(model): + expr = sum(((y - yhat)/model.experiment_outputs_std[y])**2 + for y, yhat in model.experiment_outputs.items()) return expr - ### Data reconciliation - theta_names = [] # no variables to estimate, use initialized values + # View one model & SSE + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # print(SSE(exp0_model)) - pest = parmest.Estimator(reactor_design_model_for_datarec, data, theta_names, SSE) + ### Data reconciliation + pest = parmest.Estimator(exp_list, obj_function=SSE) obj, theta, data_rec = pest.theta_est(return_values=["ca", "cb", "cc", "cd", "caf"]) print(obj) print(theta) - + parmest.graphics.grouped_boxplot( data[["ca", "cb", "cc", "cd"]], data_rec[["ca", "cb", "cc", "cd"]], @@ -84,14 +146,18 @@ def SSE(model, data): ) ### Parameter estimation using reconciled data - theta_names = ["k1", "k2", "k3"] data_rec["sv"] = data["sv"] - pest = parmest.Estimator(reactor_design_model, data_rec, theta_names, SSE) + # make a new list of experiments using reconciled data + exp_list= [] + for i in range(data_rec.shape[0]): + exp_list.append(ReactorDesignExperimentPostDataRec(data_rec, data_std, i)) + + pest = parmest.Estimator(exp_list, obj_function=SSE) obj, theta = pest.theta_est() print(obj) print(theta) - + theta_real = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} print(theta_real) diff --git a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py index 6952a7fc733..549233d8a84 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py @@ -14,19 +14,17 @@ from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - # Data + # Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - + # Create more data for the example N = 50 df_std = data.std().to_frame().transpose() @@ -34,18 +32,16 @@ def main(): df_sample = data.sample(N, replace=True).reset_index(drop=True) data = df_sample + df_rand.dot(df_std) / 10 - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + pest = parmest.Estimator(exp_list, obj_function='SSE') # Parameter estimation obj, theta = pest.theta_est() @@ -93,6 +89,5 @@ def SSE(model, data): percent_true = sum(r) / len(r) print(percent_true) - if __name__ == "__main__": main() diff --git a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py index a0fe6f22305..8b6d9fcfecc 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py @@ -15,31 +15,27 @@ from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - - # Data + +# Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + + pest = parmest.Estimator(exp_list, obj_function='SSE') # Parameter estimation obj, theta = pest.theta_est() diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index a92ac626fae..f731032368e 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -11,37 +11,76 @@ import pandas as pd from os.path import join, abspath, dirname +import pyomo.environ as pyo import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) +class MultisensorReactorDesignExperiment(ReactorDesignExperiment): + + def finalize_model(self): + + m = self.model + + # Experiment inputs values + m.sv = self.data_i['sv'] + m.caf = self.data_i['caf'] + + # Experiment output values + m.ca = (self.data_i['ca1'] + self.data_i['ca2'] + self.data_i['ca3']) * (1/3) + m.cb = self.data_i['cb'] + m.cc = (self.data_i['cc1'] + self.data_i['cc2']) * (1/2) + m.cd = self.data_i['cd'] + + return m + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']])]) + m.experiment_outputs.update([(m.cb, [self.data_i['cb']])]) + m.experiment_outputs.update([(m.cc, [self.data_i['cc1'], self.data_i['cc2']])]) + m.experiment_outputs.update([(m.cd, [self.data_i['cd']])]) + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.k1, m.k2, m.k3]) + + return m + + def main(): # Parameter estimation using multisensor data - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - - # Data, includes multiple sensors for ca and cc + # Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data_multisensor.csv")) data = pd.read_csv(file_name) + + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(MultisensorReactorDesignExperiment(data, i)) - # Sum of squared error function - def SSE_multisensor(model, data): - expr = ( - ((float(data.iloc[0]["ca1"]) - model.ca) ** 2) * (1 / 3) - + ((float(data.iloc[0]["ca2"]) - model.ca) ** 2) * (1 / 3) - + ((float(data.iloc[0]["ca3"]) - model.ca) ** 2) * (1 / 3) - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + ((float(data.iloc[0]["cc1"]) - model.cc) ** 2) * (1 / 2) - + ((float(data.iloc[0]["cc2"]) - model.cc) ** 2) * (1 / 2) - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) + # Define sum of squared error + def SSE_multisensor(model): + expr = 0 + for y, yhat in model.experiment_outputs.items(): + num_outputs = len(yhat) + for i in range(num_outputs): + expr += ((y - yhat[i])**2) * (1 / num_outputs) return expr + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # print(SSE_multisensor(exp0_model)) - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE_multisensor) + pest = parmest.Estimator(exp_list, obj_function=SSE_multisensor) obj, theta = pest.theta_est() print(obj) print(theta) diff --git a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py index 581d3904c04..76744984cce 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py @@ -13,46 +13,29 @@ from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - # Data + # Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) - - # Parameter estimation - obj, theta = pest.theta_est() - - # Assert statements compare parameter estimation (theta) to an expected value - k1_expected = 5.0 / 6.0 - k2_expected = 5.0 / 3.0 - k3_expected = 1.0 / 6000.0 - relative_error = abs(theta["k1"] - k1_expected) / k1_expected - assert relative_error < 0.05 - relative_error = abs(theta["k2"] - k2_expected) / k2_expected - assert relative_error < 0.05 - relative_error = abs(theta["k3"] - k3_expected) / k3_expected - assert relative_error < 0.05 - - -if __name__ == "__main__": - main() + + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + + pest = parmest.Estimator(exp_list, obj_function='SSE') + + # Parameter estimation with covariance + obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=17) + print(obj) + print(theta) \ No newline at end of file diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index 16f65e236eb..1479009abcc 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -12,57 +12,42 @@ Continuously stirred tank reactor model, based on pyomo/examples/doc/pyomobook/nonlinear-ch/react_design/ReactorDesign.py """ +from os.path import join, abspath, dirname +from itertools import product import pandas as pd -from pyomo.environ import ( - ConcreteModel, - Param, - Var, - PositiveReals, - Objective, - Constraint, - maximize, - SolverFactory, -) - - -def reactor_design_model(data): + +import pyomo.environ as pyo +import pyomo.contrib.parmest.parmest as parmest + +from pyomo.contrib.parmest.experiment import Experiment + +def reactor_design_model(): + # Create the concrete model - model = ConcreteModel() + model = pyo.ConcreteModel() # Rate constants - model.k1 = Param(initialize=5.0 / 6.0, within=PositiveReals, mutable=True) # min^-1 - model.k2 = Param(initialize=5.0 / 3.0, within=PositiveReals, mutable=True) # min^-1 - model.k3 = Param( - initialize=1.0 / 6000.0, within=PositiveReals, mutable=True - ) # m^3/(gmol min) + model.k1 = pyo.Param(initialize=5.0 / 6.0, within=pyo.PositiveReals, mutable=True) # min^-1 + model.k2 = pyo.Param(initialize=5.0 / 3.0, within=pyo.PositiveReals, mutable=True) # min^-1 + model.k3 = pyo.Param(initialize=1.0 / 6000.0, within=pyo.PositiveReals, mutable=True) # m^3/(gmol min) # Inlet concentration of A, gmol/m^3 - if isinstance(data, dict) or isinstance(data, pd.Series): - model.caf = Param(initialize=float(data["caf"]), within=PositiveReals) - elif isinstance(data, pd.DataFrame): - model.caf = Param(initialize=float(data.iloc[0]["caf"]), within=PositiveReals) - else: - raise ValueError("Unrecognized data type.") - + model.caf = pyo.Param(initialize=10000, within=pyo.PositiveReals, mutable=True) + # Space velocity (flowrate/volume) - if isinstance(data, dict) or isinstance(data, pd.Series): - model.sv = Param(initialize=float(data["sv"]), within=PositiveReals) - elif isinstance(data, pd.DataFrame): - model.sv = Param(initialize=float(data.iloc[0]["sv"]), within=PositiveReals) - else: - raise ValueError("Unrecognized data type.") + model.sv = pyo.Param(initialize=1.0, within=pyo.PositiveReals, mutable=True) # Outlet concentration of each component - model.ca = Var(initialize=5000.0, within=PositiveReals) - model.cb = Var(initialize=2000.0, within=PositiveReals) - model.cc = Var(initialize=2000.0, within=PositiveReals) - model.cd = Var(initialize=1000.0, within=PositiveReals) + model.ca = pyo.Var(initialize=5000.0, within=pyo.PositiveReals) + model.cb = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cc = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cd = pyo.Var(initialize=1000.0, within=pyo.PositiveReals) # Objective - model.obj = Objective(expr=model.cb, sense=maximize) + model.obj = pyo.Objective(expr=model.cb, sense=pyo.maximize) # Constraints - model.ca_bal = Constraint( + model.ca_bal = pyo.Constraint( expr=( 0 == model.sv * model.caf @@ -72,33 +57,89 @@ def reactor_design_model(data): ) ) - model.cb_bal = Constraint( + model.cb_bal = pyo.Constraint( expr=(0 == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb) ) - model.cc_bal = Constraint(expr=(0 == -model.sv * model.cc + model.k2 * model.cb)) + model.cc_bal = pyo.Constraint(expr=(0 == -model.sv * model.cc + model.k2 * model.cb)) - model.cd_bal = Constraint( + model.cd_bal = pyo.Constraint( expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) ) return model + +class ReactorDesignExperiment(Experiment): + + def __init__(self, data, experiment_number): + self.data = data + self.experiment_number = experiment_number + self.data_i = data.loc[experiment_number,:] + self.model = None + + def create_model(self): + self.model = m = reactor_design_model() + return m + + def finalize_model(self): + m = self.model + + # Experiment inputs values + m.sv = self.data_i['sv'] + m.caf = self.data_i['caf'] + + # Experiment output values + m.ca = self.data_i['ca'] + m.cb = self.data_i['cb'] + m.cc = self.data_i['cc'] + m.cd = self.data_i['cd'] + + return m + + def label_model(self): + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.ca, self.data_i['ca'])]) + m.experiment_outputs.update([(m.cb, self.data_i['cb'])]) + m.experiment_outputs.update([(m.cc, self.data_i['cc'])]) + m.experiment_outputs.update([(m.cd, self.data_i['cd'])]) + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.k1, m.k2, m.k3]) + + return m + + def get_labeled_model(self): + m = self.create_model() + m = self.finalize_model() + m = self.label_model() + + return m +if __name__ == "__main__": -def main(): # For a range of sv values, return ca, cb, cc, and cd results = [] sv_values = [1.0 + v * 0.05 for v in range(1, 20)] caf = 10000 for sv in sv_values: - model = reactor_design_model(pd.DataFrame(data={"caf": [caf], "sv": [sv]})) - solver = SolverFactory("ipopt") + + # make model + model = reactor_design_model() + + # add caf, sv + model.caf = caf + model.sv = sv + + # solve model + solver = pyo.SolverFactory("ipopt") solver.solve(model) + + # save results results.append([sv, caf, model.ca(), model.cb(), model.cc(), model.cd()]) results = pd.DataFrame(results, columns=["sv", "caf", "ca", "cb", "cc", "cd"]) print(results) - - -if __name__ == "__main__": - main() + \ No newline at end of file diff --git a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py index da2ab1874c9..a9a5ab20b54 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py @@ -14,16 +14,74 @@ import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) +class TimeSeriesReactorDesignExperiment(ReactorDesignExperiment): + + def __init__(self, data, experiment_number): + self.data = data + self.experiment_number = experiment_number + self.data_i = data[experiment_number] + self.model = None + + def finalize_model(self): + m = self.model + + # Experiment inputs values + m.sv = self.data_i['sv'] + m.caf = self.data_i['caf'] + + # Experiment output values + m.ca = self.data_i['ca'][0] + m.cb = self.data_i['cb'][0] + m.cc = self.data_i['cc'][0] + m.cd = self.data_i['cd'][0] + + return m + + +def group_data(data, groupby_column_name, use_mean=None): + """ + Group data by scenario + + Parameters + ---------- + data: DataFrame + Data + groupby_column_name: strings + Name of data column which contains scenario numbers + use_mean: list of column names or None, optional + Name of data columns which should be reduced to a single value per + scenario by taking the mean + + Returns + ---------- + grouped_data: list of dictionaries + Grouped data + """ + if use_mean is None: + use_mean_list = [] + else: + use_mean_list = use_mean + + grouped_data = [] + for exp_num, group in data.groupby(data[groupby_column_name]): + d = {} + for col in group.columns: + if col in use_mean_list: + d[col] = group[col].mean() + else: + d[col] = list(group[col]) + grouped_data.append(d) + + return grouped_data + + def main(): # Parameter estimation using timeseries data - # Vars to estimate - theta_names = ['k1', 'k2', 'k3'] - # Data, includes multiple sensors for ca and cc file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, 'reactor_data_timeseries.csv')) @@ -31,21 +89,29 @@ def main(): # Group time series data into experiments, return the mean value for sv and caf # Returns a list of dictionaries - data_ts = parmest.group_data(data, 'experiment', ['sv', 'caf']) + data_ts = group_data(data, 'experiment', ['sv', 'caf']) + + # Create an experiment list + exp_list= [] + for i in range(len(data_ts)): + exp_list.append(TimeSeriesReactorDesignExperiment(data_ts, i)) + + def SSE_timeseries(model): - def SSE_timeseries(model, data): expr = 0 - for val in data['ca']: - expr = expr + ((float(val) - model.ca) ** 2) * (1 / len(data['ca'])) - for val in data['cb']: - expr = expr + ((float(val) - model.cb) ** 2) * (1 / len(data['cb'])) - for val in data['cc']: - expr = expr + ((float(val) - model.cc) ** 2) * (1 / len(data['cc'])) - for val in data['cd']: - expr = expr + ((float(val) - model.cd) ** 2) * (1 / len(data['cd'])) + for y, yhat in model.experiment_outputs.items(): + num_time_points = len(yhat) + for i in range(num_time_points): + expr += ((y - yhat[i])**2) * (1 / num_time_points) + return expr - pest = parmest.Estimator(reactor_design_model, data_ts, theta_names, SSE_timeseries) + # View one model & SSE + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # print(SSE_timeseries(exp0_model)) + + pest = parmest.Estimator(exp_list, obj_function=SSE_timeseries) obj, theta = pest.theta_est() print(obj) print(theta) diff --git a/pyomo/contrib/parmest/experiment.py b/pyomo/contrib/parmest/experiment.py new file mode 100644 index 00000000000..73b18bb5975 --- /dev/null +++ b/pyomo/contrib/parmest/experiment.py @@ -0,0 +1,15 @@ +# The experiment class is a template for making experiment lists +# to pass to parmest. An experiment is a pyomo model "m" which has +# additional suffixes: +# m.experiment_outputs -- which variables are experiment outputs +# m.unknown_parameters -- which variables are parameters to estimate +# The experiment class has only one required method: +# get_labeled_model() +# which returns the labeled pyomo model. + +class Experiment: + def __init__(self, model=None): + self.model = model + + def get_labeled_model(self): + return self.model \ No newline at end of file diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 1f9b8b645b8..a00671c2ea6 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -11,9 +11,22 @@ #### Using mpi-sppy instead of PySP; May 2020 #### Adding option for "local" EF starting Sept 2020 #### Wrapping mpi-sppy functionality and local option Jan 2021, Feb 2021 +#### Redesign with Experiment class Dec 2023 # TODO: move use_mpisppy to a Pyomo configuration option -# + +# Redesign TODOS +# TODO: remove group_data,this is only used in 1 example and should be handled by the user in Experiment +# TODO: _treemaker is not used in parmest, the code could be moved to scenario tree if needed +# TODO: Create additional built in objective expressions in an Enum class which includes SSE (see SSE function below) +# TODO: Clean up the use of theta_names through out the code. The Experiment returns the CUID of each theta and this can be used directly (instead of the name) +# TODO: Clean up the use of updated_theta_names, model_theta_names, estimator_theta_names. Not sure if estimator_theta_names is the union or intersect of thetas in each model +# TODO: _return_theta_names should no longer be needed +# TODO: generally, theta ordering is not preserved by pyomo, so we should check that ordering +# matches values for each function, otherwise results will be wrong and/or inconsistent +# TODO: return model object (m.k1) and CUIDs in dataframes instead of names ("k1") + + # False implies always use the EF that is local to parmest use_mpisppy = True # Use it if we can but use local if not. if use_mpisppy: @@ -224,90 +237,92 @@ def _experiment_instance_creation_callback( return instance -# ============================================= -def _treemaker(scenlist): - """ - Makes a scenario tree (avoids dependence on daps) - - Parameters - ---------- - scenlist (list of `int`): experiment (i.e. scenario) numbers - - Returns - ------- - a `ConcreteModel` that is the scenario tree - """ - - num_scenarios = len(scenlist) - m = scenario_tree.tree_structure_model.CreateAbstractScenarioTreeModel() - m = m.create_instance() - m.Stages.add('Stage1') - m.Stages.add('Stage2') - m.Nodes.add('RootNode') - for i in scenlist: - m.Nodes.add('LeafNode_Experiment' + str(i)) - m.Scenarios.add('Experiment' + str(i)) - m.NodeStage['RootNode'] = 'Stage1' - m.ConditionalProbability['RootNode'] = 1.0 - for node in m.Nodes: - if node != 'RootNode': - m.NodeStage[node] = 'Stage2' - m.Children['RootNode'].add(node) - m.Children[node].clear() - m.ConditionalProbability[node] = 1.0 / num_scenarios - m.ScenarioLeafNode[node.replace('LeafNode_', '')] = node - - return m - - -def group_data(data, groupby_column_name, use_mean=None): - """ - Group data by scenario - - Parameters - ---------- - data: DataFrame - Data - groupby_column_name: strings - Name of data column which contains scenario numbers - use_mean: list of column names or None, optional - Name of data columns which should be reduced to a single value per - scenario by taking the mean - - Returns - ---------- - grouped_data: list of dictionaries - Grouped data - """ - if use_mean is None: - use_mean_list = [] - else: - use_mean_list = use_mean - - grouped_data = [] - for exp_num, group in data.groupby(data[groupby_column_name]): - d = {} - for col in group.columns: - if col in use_mean_list: - d[col] = group[col].mean() - else: - d[col] = list(group[col]) - grouped_data.append(d) - - return grouped_data - +# # ============================================= +# def _treemaker(scenlist): +# """ +# Makes a scenario tree (avoids dependence on daps) + +# Parameters +# ---------- +# scenlist (list of `int`): experiment (i.e. scenario) numbers + +# Returns +# ------- +# a `ConcreteModel` that is the scenario tree +# """ + +# num_scenarios = len(scenlist) +# m = scenario_tree.tree_structure_model.CreateAbstractScenarioTreeModel() +# m = m.create_instance() +# m.Stages.add('Stage1') +# m.Stages.add('Stage2') +# m.Nodes.add('RootNode') +# for i in scenlist: +# m.Nodes.add('LeafNode_Experiment' + str(i)) +# m.Scenarios.add('Experiment' + str(i)) +# m.NodeStage['RootNode'] = 'Stage1' +# m.ConditionalProbability['RootNode'] = 1.0 +# for node in m.Nodes: +# if node != 'RootNode': +# m.NodeStage[node] = 'Stage2' +# m.Children['RootNode'].add(node) +# m.Children[node].clear() +# m.ConditionalProbability[node] = 1.0 / num_scenarios +# m.ScenarioLeafNode[node.replace('LeafNode_', '')] = node + +# return m + + +# def group_data(data, groupby_column_name, use_mean=None): +# """ +# Group data by scenario + +# Parameters +# ---------- +# data: DataFrame +# Data +# groupby_column_name: strings +# Name of data column which contains scenario numbers +# use_mean: list of column names or None, optional +# Name of data columns which should be reduced to a single value per +# scenario by taking the mean + +# Returns +# ---------- +# grouped_data: list of dictionaries +# Grouped data +# """ +# if use_mean is None: +# use_mean_list = [] +# else: +# use_mean_list = use_mean + +# grouped_data = [] +# for exp_num, group in data.groupby(data[groupby_column_name]): +# d = {} +# for col in group.columns: +# if col in use_mean_list: +# d[col] = group[col].mean() +# else: +# d[col] = list(group[col]) +# grouped_data.append(d) + +# return grouped_data + +def SSE(model): + expr = sum((y - yhat)**2 for y, yhat in model.experiment_outputs.items()) + return expr class _SecondStageCostExpr(object): """ Class to pass objective expression into the Pyomo model """ - def __init__(self, ssc_function, data): + def __init__(self, ssc_function): self._ssc_function = ssc_function - self._data = data def __call__(self, model): - return self._ssc_function(model, self._data) + return self._ssc_function(model) class Estimator(object): @@ -316,17 +331,12 @@ class Estimator(object): Parameters ---------- - model_function: function - Function that generates an instance of the Pyomo model using 'data' - as the input argument - data: pd.DataFrame, list of dictionaries, list of dataframes, or list of json file names - Data that is used to build an instance of the Pyomo model and build - the objective function - theta_names: list of strings - List of Var names to estimate - obj_function: function, optional - Function used to formulate parameter estimation objective, generally - sum of squared error between measurements and model variables. + experiement_list: list of Experiments + A list of experiment objects which creates one labeled model for + each expeirment + obj_function: string or function (optional) + Built in objective (currently only "SSE") or custom function used to + formulate parameter estimation objective. If no function is specified, the model is used "as is" and should be defined with a "FirstStageCost" and "SecondStageCost" expression that are used to build an objective. @@ -342,54 +352,49 @@ class Estimator(object): # from parmest_deprecated as well as the new inputs using experiment lists def __init__(self, *args, **kwargs): + # check that we have at least one argument + assert(len(args) > 0) + # use deprecated interface self.pest_deprecated = None - if len(args) > 1: + if callable(args[0]): logger.warning('Using deprecated parmest inputs (model_function, ' + 'data, theta_names), please use experiment lists instead.') self.pest_deprecated = parmest_deprecated.Estimator(*args, **kwargs) return - print("New parmest interface using Experiment lists coming soon!") - exit() - - # def __init__( - # self, - # model_function, - # data, - # theta_names, - # obj_function=None, - # tee=False, - # diagnostic_mode=False, - # solver_options=None, - # ): - - self.model_function = model_function - - assert isinstance( - data, (list, pd.DataFrame) - ), "Data must be a list or DataFrame" - # convert dataframe into a list of dataframes, each row = one scenario - if isinstance(data, pd.DataFrame): - self.callback_data = [ - data.loc[i, :].to_frame().transpose() for i in data.index - ] - else: - self.callback_data = data - assert isinstance( - self.callback_data[0], (dict, pd.DataFrame, str) - ), "The scenarios in data must be a dictionary, DataFrame or filename" - - if len(theta_names) == 0: - self.theta_names = ['parmest_dummy_var'] - else: - self.theta_names = theta_names - - self.obj_function = obj_function - self.tee = tee - self.diagnostic_mode = diagnostic_mode - self.solver_options = solver_options + # check that we have a (non-empty) list of experiments + assert (isinstance(args[0], list)) + assert (len(args[0]) > 0) + self.exp_list = args[0] + # check that an experiment has experiment_outputs and unknown_parameters + model = self.exp_list[0].get_labeled_model() + try: + outputs = [k.name for k,v in model.experiment_outputs.items()] + except: + RuntimeError('Experiment list model does not have suffix ' + + '"experiment_outputs".') + try: + parms = [k.name for k,v in model.unknown_parameters.items()] + except: + RuntimeError('Experiment list model does not have suffix ' + + '"unknown_parameters".') + + # populate keyword argument options + self.obj_function = kwargs.get('obj_function', None) + self.tee = kwargs.get('tee', False) + self.diagnostic_mode = kwargs.get('diagnostic_mode', False) + self.solver_options = kwargs.get('solver_options', None) + + # TODO This might not be needed here. + # We could collect the union (or intersect?) of thetas when the models are built + theta_names = [] + for experiment in self.exp_list: + model = experiment.get_labeled_model() + theta_names.extend([k.name for k,v in model.unknown_parameters.items()]) + self.estimator_theta_names = list(set(theta_names)) + self._second_stage_cost_exp = "SecondStageCost" # boolean to indicate if model is initialized using a square solve self.model_initialized = False @@ -412,17 +417,26 @@ def _return_theta_names(self): ) # default theta_names, created when Estimator object is created else: - return None - def _create_parmest_model(self, data): + # if fitted model parameter names differ from theta_names + # created when Estimator object is created + if hasattr(self, 'theta_names_updated'): + return self.theta_names_updated + + else: + return ( + self.estimator_theta_names + ) # default theta_names, created when Estimator object is created + + def _create_parmest_model(self, experiment_number): """ Modify the Pyomo model for parameter estimation """ - model = self.model_function(data) - if (len(self.theta_names) == 1) and ( - self.theta_names[0] == 'parmest_dummy_var' - ): + model = self.exp_list[experiment_number].get_labeled_model() + self.theta_names = [k.name for k,v in model.unknown_parameters.items()] + + if len(model.unknown_parameters) == 0: model.parmest_dummy_var = pyo.Var(initialize=1.0) # Add objective function (optional) @@ -441,10 +455,17 @@ def _create_parmest_model(self, data): "Parmest will not override the existing model Expression named " + expr.name ) + + # TODO, this needs to be turned a enum class of options that still support custom functions + if self.obj_function == 'SSE': + second_stage_rule=_SecondStageCostExpr(SSE) + else: + # A custom function uses model.experiment_outputs as data + second_stage_rule = _SecondStageCostExpr(self.obj_function) + model.FirstStageCost = pyo.Expression(expr=0) - model.SecondStageCost = pyo.Expression( - rule=_SecondStageCostExpr(self.obj_function, data) - ) + model.SecondStageCost = pyo.Expression(rule=second_stage_rule) + def TotalCost_rule(model): return model.FirstStageCost + model.SecondStageCost @@ -479,20 +500,7 @@ def TotalCost_rule(model): return model def _instance_creation_callback(self, experiment_number=None, cb_data=None): - # cb_data is a list of dictionaries, list of dataframes, OR list of json file names - exp_data = cb_data[experiment_number] - if isinstance(exp_data, (dict, pd.DataFrame)): - pass - elif isinstance(exp_data, str): - try: - with open(exp_data, 'r') as infile: - exp_data = json.load(infile) - except: - raise RuntimeError(f'Could not read {exp_data} as json') - else: - raise RuntimeError(f'Unexpected data format for cb_data={cb_data}') - model = self._create_parmest_model(exp_data) - + model = self._create_parmest_model(experiment_number) return model def _Q_opt( @@ -514,7 +522,7 @@ def _Q_opt( # (Bootstrap scenarios will use indirection through the bootlist) if bootlist is None: - scenario_numbers = list(range(len(self.callback_data))) + scenario_numbers = list(range(len(self.exp_list))) scen_names = ["Scenario{}".format(i) for i in scenario_numbers] else: scen_names = ["Scenario{}".format(i) for i in range(len(bootlist))] @@ -526,8 +534,8 @@ def _Q_opt( outer_cb_data["ThetaVals"] = ThetaVals if bootlist is not None: outer_cb_data["BootList"] = bootlist - outer_cb_data["cb_data"] = self.callback_data # None is OK - outer_cb_data["theta_names"] = self.theta_names + outer_cb_data["cb_data"] = None # None is OK + outer_cb_data["theta_names"] = self.estimator_theta_names options = {"solver": "ipopt"} scenario_creator_options = {"cb_data": outer_cb_data} @@ -702,13 +710,13 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): "callback": self._instance_creation_callback, "ThetaVals": thetavals, "theta_names": self._return_theta_names(), - "cb_data": self.callback_data, + "cb_data": None, } else: dummy_cb = { "callback": self._instance_creation_callback, "theta_names": self._return_theta_names(), - "cb_data": self.callback_data, + "cb_data": None, } if self.diagnostic_mode: @@ -729,7 +737,7 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): WorstStatus = pyo.TerminationCondition.optimal totobj = 0 - scenario_numbers = list(range(len(self.callback_data))) + scenario_numbers = list(range(len(self.exp_list))) if initialize_parmest_model: # create dictionary to store pyomo model instances (scenarios) scen_dict = dict() @@ -737,13 +745,14 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): for snum in scenario_numbers: sname = "scenario_NODE" + str(snum) instance = _experiment_instance_creation_callback(sname, None, dummy_cb) + model_theta_names = [k.name for k,v in instance.unknown_parameters.items()] if initialize_parmest_model: # list to store fitted parameter names that will be unfixed # after initialization theta_init_vals = [] # use appropriate theta_names member - theta_ref = self._return_theta_names() + theta_ref = model_theta_names for i, theta in enumerate(theta_ref): # Use parser in ComponentUID to locate the component @@ -868,7 +877,7 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): def _get_sample_list(self, samplesize, num_samples, replacement=True): samplelist = list() - scenario_numbers = list(range(len(self.callback_data))) + scenario_numbers = list(range(len(self.exp_list))) if num_samples is None: # This could get very large @@ -944,12 +953,12 @@ def theta_est( assert isinstance(return_values, list) assert isinstance(calc_cov, bool) if calc_cov: - assert isinstance( - cov_n, int - ), "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" - assert cov_n > len( - self._return_theta_names() - ), "The number of datapoints must be greater than the number of parameters to estimate" + num_unknowns = max([len(experiment.get_labeled_model().unknown_parameters) + for experiment in self.exp_list]) + assert isinstance(cov_n, int), \ + "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" + assert cov_n > num_unknowns, \ + "The number of datapoints must be greater than the number of parameters to estimate" return self._Q_opt( solver=solver, @@ -1007,7 +1016,7 @@ def theta_est_bootstrap( assert isinstance(return_samples, bool) if samplesize is None: - samplesize = len(self.callback_data) + samplesize = len(self.exp_list) if seed is not None: np.random.seed(seed) @@ -1069,7 +1078,7 @@ def theta_est_leaveNout( assert isinstance(seed, (type(None), int)) assert isinstance(return_samples, bool) - samplesize = len(self.callback_data) - lNo + samplesize = len(self.exp_list) - lNo if seed is not None: np.random.seed(seed) @@ -1082,7 +1091,7 @@ def theta_est_leaveNout( lNo_theta = list() for idx, sample in local_list: objval, thetavals = self._Q_opt(bootlist=list(sample)) - lNo_s = list(set(range(len(self.callback_data))) - set(sample)) + lNo_s = list(set(range(len(self.exp_list))) - set(sample)) thetavals['lNo'] = np.sort(lNo_s) lNo_theta.append(thetavals) @@ -1157,20 +1166,13 @@ def leaveNout_bootstrap_test( if seed is not None: np.random.seed(seed) - data = self.callback_data.copy() - global_list = self._get_sample_list(lNo, lNo_samples, replacement=False) results = [] for idx, sample in global_list: - # Reset callback_data to only include the sample - self.callback_data = [data[i] for i in sample] obj, theta = self.theta_est() - # Reset callback_data to include all scenarios except the sample - self.callback_data = [data[i] for i in range(len(data)) if i not in sample] - bootstrap_theta = self.theta_est_bootstrap(bootstrap_samples) training, test = self.confidence_region_test( @@ -1182,9 +1184,6 @@ def leaveNout_bootstrap_test( results.append((sample, test, training)) - # Reset callback_data (back to full data set) - self.callback_data = data - return results def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): @@ -1214,37 +1213,39 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): theta_values=theta_values, initialize_parmest_model=initialize_parmest_model) - if len(self.theta_names) == 1 and self.theta_names[0] == 'parmest_dummy_var': + if len(self.estimator_theta_names) == 0: pass # skip assertion if model has no fitted parameters else: # create a local instance of the pyomo model to access model variables and parameters - model_temp = self._create_parmest_model(self.callback_data[0]) - model_theta_list = [] # list to store indexed and non-indexed parameters - # iterate over original theta_names - for theta_i in self.theta_names: - var_cuid = ComponentUID(theta_i) - var_validate = var_cuid.find_component_on(model_temp) - # check if theta in theta_names are indexed - try: - # get component UID of Set over which theta is defined - set_cuid = ComponentUID(var_validate.index_set()) - # access and iterate over the Set to generate theta names as they appear - # in the pyomo model - set_validate = set_cuid.find_component_on(model_temp) - for s in set_validate: - self_theta_temp = repr(var_cuid) + "[" + repr(s) + "]" - # generate list of theta names - model_theta_list.append(self_theta_temp) - # if theta is not indexed, copy theta name to list as-is - except AttributeError: - self_theta_temp = repr(var_cuid) - model_theta_list.append(self_theta_temp) - except: - raise + model_temp = self._create_parmest_model(0) + model_theta_list = [k.name for k,v in model_temp.unknown_parameters.items()] + + # # iterate over original theta_names + # for theta_i in self.theta_names: + # var_cuid = ComponentUID(theta_i) + # var_validate = var_cuid.find_component_on(model_temp) + # # check if theta in theta_names are indexed + # try: + # # get component UID of Set over which theta is defined + # set_cuid = ComponentUID(var_validate.index_set()) + # # access and iterate over the Set to generate theta names as they appear + # # in the pyomo model + # set_validate = set_cuid.find_component_on(model_temp) + # for s in set_validate: + # self_theta_temp = repr(var_cuid) + "[" + repr(s) + "]" + # # generate list of theta names + # model_theta_list.append(self_theta_temp) + # # if theta is not indexed, copy theta name to list as-is + # except AttributeError: + # self_theta_temp = repr(var_cuid) + # model_theta_list.append(self_theta_temp) + # except: + # raise + # if self.theta_names is not the same as temp model_theta_list, # create self.theta_names_updated - if set(self.theta_names) == set(model_theta_list) and len( - self.theta_names + if set(self.estimator_theta_names) == set(model_theta_list) and len( + self.estimator_theta_names ) == set(model_theta_list): pass else: @@ -1253,7 +1254,7 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): if theta_values is None: all_thetas = {} # dictionary to store fitted variables # use appropriate theta names member - theta_names = self._return_theta_names() + theta_names = self.estimator_theta_names() else: assert isinstance(theta_values, pd.DataFrame) # for parallel code we need to use lists and dicts in the loop @@ -1343,7 +1344,7 @@ def likelihood_ratio_test( assert isinstance(return_thresholds, bool) LR = obj_at_theta.copy() - S = len(self.callback_data) + S = len(self.exp_list) thresholds = {} for a in alphas: chi2_val = scipy.stats.chi2.ppf(a, 2) From 280cd8027510b89118b13417634d21b411a93597 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 25 Jan 2024 13:25:15 -0700 Subject: [PATCH 0401/3044] Fixed parmest reaction kinetics example to work with new interface. --- .../simple_reaction_parmest_example.py | 78 ++++++++++++++++++- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py index 719a930251c..140fceeb8a2 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py @@ -18,6 +18,7 @@ Code provided by Paul Akula. ''' +import pyomo.environ as pyo from pyomo.environ import ( ConcreteModel, Param, @@ -32,6 +33,7 @@ value, ) import pyomo.contrib.parmest.parmest as parmest +from pyomo.contrib.parmest.experiment import Experiment def simple_reaction_model(data): @@ -72,7 +74,62 @@ def total_cost_rule(m): return model +# For this experiment class, data is dictionary +class SimpleReactionExperiment(Experiment): + + def __init__(self, data): + self.data = data + self.model = None + + def create_model(self): + self.model = simple_reaction_model(self.data) + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.x1, self.data['x1'])]) + m.experiment_outputs.update([(m.x2, self.data['x2'])]) + m.experiment_outputs.update([(m.y, self.data['y'])]) + + return m + + def get_labeled_model(self): + self.create_model() + m = self.label_model() + + return m + +# k[2] fixed +class SimpleReactionExperimentK2Fixed(SimpleReactionExperiment): + + def label_model(self): + + m = super().label_model() + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.k[1]]) + + return m + +# k[2] variable +class SimpleReactionExperimentK2Variable(SimpleReactionExperiment): + + def label_model(self): + + m = super().label_model() + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.k[1], m.k[2]]) + + return m + + def main(): + # Data from Table 5.2 in Y. Bard, "Nonlinear Parameter Estimation", (pg. 124) data = [ {'experiment': 1, 'x1': 0.1, 'x2': 100, 'y': 0.98}, @@ -92,21 +149,34 @@ def main(): {'experiment': 15, 'x1': 0.1, 'x2': 300, 'y': 0.006}, ] + # Create an experiment list with k[2] fixed + exp_list= [] + for i in range(len(data)): + exp_list.append(SimpleReactionExperimentK2Fixed(data[i])) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # ======================================================================= # Parameter estimation without covariance estimate # Only estimate the parameter k[1]. The parameter k[2] will remain fixed # at its initial value - theta_names = ['k[1]'] - pest = parmest.Estimator(simple_reaction_model, data, theta_names) + + pest = parmest.Estimator(exp_list) obj, theta = pest.theta_est() print(obj) print(theta) print() + # Create an experiment list with k[2] variable + exp_list= [] + for i in range(len(data)): + exp_list.append(SimpleReactionExperimentK2Variable(data[i])) + # ======================================================================= # Estimate both k1 and k2 and compute the covariance matrix - theta_names = ['k'] - pest = parmest.Estimator(simple_reaction_model, data, theta_names) + pest = parmest.Estimator(exp_list) n = 15 # total number of data points used in the objective (y in 15 scenarios) obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) print(obj) From b6395abfbc6db5198e04f15c361c745f7441a5af Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 26 Jan 2024 08:27:12 -0700 Subject: [PATCH 0402/3044] new ipopt interface: account for parameters in objective when load_solution=False --- pyomo/contrib/solver/ipopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 1a153422eb1..48c314ffc79 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -420,7 +420,7 @@ def solve(self, model, **kwds): if config.load_solution: results.incumbent_objective = value(nl_info.objectives[0]) else: - results.incumbent_objective = replace_expressions( + results.incumbent_objective = value(replace_expressions( nl_info.objectives[0].expr, substitution_map={ id(v): val @@ -428,7 +428,7 @@ def solve(self, model, **kwds): }, descend_into_named_expressions=True, remove_named_expressions=True, - ) + )) results.solver_configuration = config results.solver_log = ostreams[0].getvalue() From f126bc4520cb6edd76219256e4df87e88a372e6d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 26 Jan 2024 08:52:39 -0700 Subject: [PATCH 0403/3044] Apply black --- pyomo/contrib/solver/ipopt.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 48c314ffc79..534a9173d07 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -420,15 +420,17 @@ def solve(self, model, **kwds): if config.load_solution: results.incumbent_objective = value(nl_info.objectives[0]) else: - results.incumbent_objective = value(replace_expressions( - nl_info.objectives[0].expr, - substitution_map={ - id(v): val - for v, val in results.solution_loader.get_primals().items() - }, - descend_into_named_expressions=True, - remove_named_expressions=True, - )) + results.incumbent_objective = value( + replace_expressions( + nl_info.objectives[0].expr, + substitution_map={ + id(v): val + for v, val in results.solution_loader.get_primals().items() + }, + descend_into_named_expressions=True, + remove_named_expressions=True, + ) + ) results.solver_configuration = config results.solver_log = ostreams[0].getvalue() From c7caf805f93d206f14f3336b3f9f39e2c8862cec Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 29 Jan 2024 09:20:38 -0700 Subject: [PATCH 0404/3044] Added parmest rooney-biegler example wtih new interface. --- .../rooney_biegler/bootstrap_example.py | 22 ++++++---- .../likelihood_ratio_example.py | 22 ++++++---- .../parameter_estimation_example.py | 24 ++++++---- .../examples/rooney_biegler/rooney_biegler.py | 44 ++++++++++++++++++- .../rooney_biegler_with_constraint.py | 42 ++++++++++++++++++ 5 files changed, 128 insertions(+), 26 deletions(-) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py index f686bbd933d..1f15ab95779 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py @@ -12,13 +12,11 @@ import pandas as pd import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, + RooneyBieglerExperiment, ) def main(): - # Vars to estimate - theta_names = ['asymptote', 'rate_constant'] # Data data = pd.DataFrame( @@ -27,14 +25,22 @@ def main(): ) # Sum of squared error function - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index - ) + def SSE(model): + expr = (model.experiment_outputs[model.y] - \ + model.response_function[model.experiment_outputs[model.hour]]) ** 2 return expr + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # Create an instance of the parmest estimator - pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) + pest = parmest.Estimator(exp_list, obj_function=SSE) # Parameter estimation obj, theta = pest.theta_est() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py index 5e54a33abda..869bb39efb9 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py @@ -14,13 +14,11 @@ from itertools import product import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, + RooneyBieglerExperiment, ) def main(): - # Vars to estimate - theta_names = ['asymptote', 'rate_constant'] # Data data = pd.DataFrame( @@ -29,14 +27,22 @@ def main(): ) # Sum of squared error function - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index - ) + def SSE(model): + expr = (model.experiment_outputs[model.y] - \ + model.response_function[model.experiment_outputs[model.hour]]) ** 2 return expr + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # Create an instance of the parmest estimator - pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) + pest = parmest.Estimator(exp_list, obj_function=SSE) # Parameter estimation obj, theta = pest.theta_est() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py index 9af33217fe4..b6ca7af0ab6 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py @@ -12,13 +12,11 @@ import pandas as pd import pyomo.contrib.parmest.parmest as parmest from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, + RooneyBieglerExperiment, ) def main(): - # Vars to estimate - theta_names = ['asymptote', 'rate_constant'] # Data data = pd.DataFrame( @@ -27,15 +25,23 @@ def main(): ) # Sum of squared error function - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index - ) + def SSE(model): + expr = (model.experiment_outputs[model.y] - \ + model.response_function[model.experiment_outputs[model.hour]]) ** 2 return expr - # Create an instance of the parmest estimator - pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # Create an instance of the parmest estimator + pest = parmest.Estimator(exp_list, obj_function=SSE) + # Parameter estimation and covariance n = 6 # total number of data points used in the objective (y in 6 scenarios) obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py index 5a0e1238e85..2ac03504260 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py @@ -17,6 +17,7 @@ import pandas as pd import pyomo.environ as pyo +from pyomo.contrib.parmest.experiment import Experiment def rooney_biegler_model(data): @@ -25,10 +26,13 @@ def rooney_biegler_model(data): model.asymptote = pyo.Var(initialize=15) model.rate_constant = pyo.Var(initialize=0.5) + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + def response_rule(m, h): expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) return expr - + model.response_function = pyo.Expression(data.hour, rule=response_rule) def SSE_rule(m): @@ -41,6 +45,44 @@ def SSE_rule(m): return model +class RooneyBieglerExperiment(Experiment): + + def __init__(self, data): + self.data = data + self.model = None + + def create_model(self): + self.model = rooney_biegler_model(self.data) + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) + m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.asymptote, m.rate_constant]) + + def finalize_model(self): + + m = self.model + + # Experiment output values + m.hour = self.data.iloc[0]['hour'] + m.y = self.data.iloc[0]['y'] + + def get_labeled_model(self): + self.create_model() + self.label_model() + self.finalize_model() + + return self.model + + def main(): # These were taken from Table A1.4 in Bates and Watts (1988). data = pd.DataFrame( diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py index 2582e3fe928..1e213684a01 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py @@ -17,6 +17,7 @@ import pandas as pd import pyomo.environ as pyo +from pyomo.contrib.parmest.experiment import Experiment def rooney_biegler_model_with_constraint(data): @@ -24,6 +25,10 @@ def rooney_biegler_model_with_constraint(data): model.asymptote = pyo.Var(initialize=15) model.rate_constant = pyo.Var(initialize=0.5) + + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.response_function = pyo.Var(data.hour, initialize=0.0) # changed from expression to constraint @@ -43,6 +48,43 @@ def SSE_rule(m): return model +class RooneyBieglerExperiment(Experiment): + + def __init__(self, data): + self.data = data + self.model = None + + def create_model(self): + self.model = rooney_biegler_model_with_constraint(self.data) + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) + m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.asymptote, m.rate_constant]) + + def finalize_model(self): + + m = self.model + + # Experiment output values + m.hour = self.data.iloc[0]['hour'] + m.y = self.data.iloc[0]['y'] + + def get_labeled_model(self): + self.create_model() + self.label_model() + self.finalize_model() + + return self.model + def main(): # These were taken from Table A1.4 in Bates and Watts (1988). From 4260d193bfeb9f2bd6616395824789cbdc637a92 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 29 Jan 2024 15:33:58 -0700 Subject: [PATCH 0405/3044] Adding 'spans' and 'alternative' expressions --- pyomo/contrib/cp/__init__.py | 1 + pyomo/contrib/cp/interval_var.py | 9 +++- pyomo/contrib/cp/repn/docplex_writer.py | 19 ++++++++ .../cp/scheduling_expr/scheduling_logic.py | 48 +++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 pyomo/contrib/cp/scheduling_expr/scheduling_logic.py diff --git a/pyomo/contrib/cp/__init__.py b/pyomo/contrib/cp/__init__.py index 03196537446..bd839f5d578 100644 --- a/pyomo/contrib/cp/__init__.py +++ b/pyomo/contrib/cp/__init__.py @@ -14,6 +14,7 @@ before_in_sequence, predecessor_to, ) +from pyomo.contrib.cp.scheduling_expr.scheduling_logic import alternative, spans from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, Step, diff --git a/pyomo/contrib/cp/interval_var.py b/pyomo/contrib/cp/interval_var.py index 911d9ba50ba..2379a58a1ff 100644 --- a/pyomo/contrib/cp/interval_var.py +++ b/pyomo/contrib/cp/interval_var.py @@ -11,6 +11,7 @@ from pyomo.common.collections import ComponentSet from pyomo.common.pyomo_typing import overload +from pyomo.contrib.cp.scheduling_expr.scheduling_logic import SpanExpression from pyomo.contrib.cp.scheduling_expr.precedence_expressions import ( BeforeExpression, AtExpression, @@ -24,6 +25,7 @@ from pyomo.core.base.indexed_component import IndexedComponent, UnindexedComponent_set from pyomo.core.base.initializer import BoundInitializer, Initializer from pyomo.core.expr import GetItemExpression +from pyomo.core.expr.logical_expr import _flattened class IntervalVarTimePoint(ScalarVar): @@ -80,7 +82,9 @@ class IntervalVarPresence(ScalarBooleanVar): __slots__ = () - def __init__(self): + def __init__(self, *args, **kwd): + # TODO: adding args and kwd above made Reference work, but we + # probably shouldn't just swallow them, right? super().__init__(ctype=IntervalVarPresence) def get_associated_interval_var(self): @@ -122,6 +126,9 @@ def optional(self, val): else: self.is_present.fix(True) + def spans(self, *args): + return SpanExpression([self] + list(_flattened(args))) + @ModelComponentFactory.register("Interval variables for scheduling.") class IntervalVar(Block): diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index de71e4e98dd..1f6bcc347e7 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -36,6 +36,10 @@ IndexedSequenceVar, _SequenceVarData, ) +from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( + AlternativeExpression, + SpanExpression, +) from pyomo.contrib.cp.scheduling_expr.precedence_expressions import ( BeforeExpression, AtExpression, @@ -462,6 +466,7 @@ def _create_docplex_interval_var(visitor, interval_var): nm = interval_var.name if visitor.symbolic_solver_labels else None cpx_interval_var = cp.interval_var(name=nm) visitor.var_map[id(interval_var)] = cpx_interval_var + visitor.pyomo_to_docplex[interval_var] = cpx_interval_var # Figure out if it exists if interval_var.is_present.fixed and not interval_var.is_present.value: @@ -991,6 +996,18 @@ def _handle_predecessor_to_expression_node( return _GENERAL, cp.previous(seq_var[1], before_var[1], after_var[1]) +def _handle_span_expression_node( + visitor, node, *args +): + return _GENERAL, cp.span(args[0][1], [arg[1] for arg in args[1:]]) + + +def _handle_alternative_expression_node( + visitor, node, *args +): + return _GENERAL, cp.alternative(args[0][1], [arg[1] for arg in args[1:]]) + + class LogicalToDoCplex(StreamBasedExpressionVisitor): _operator_handles = { EXPR.GetItemExpression: _handle_getitem, @@ -1037,6 +1054,8 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): LastInSequenceExpression: _handle_last_in_sequence_expression_node, BeforeInSequenceExpression: _handle_before_in_sequence_expression_node, PredecessorToExpression: _handle_predecessor_to_expression_node, + SpanExpression: _handle_span_expression_node, + AlternativeExpression: _handle_alternative_expression_node, } _var_handles = { IntervalVarStartTime: _before_interval_var_start_time, diff --git a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py new file mode 100644 index 00000000000..a1f891a769f --- /dev/null +++ b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py @@ -0,0 +1,48 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +from pyomo.core.expr.logical_expr import NaryBooleanExpression, _flattened + + +class SpanExpression(NaryBooleanExpression): + """ + Expression over IntervalVars representing that the first arg spans all the + following args in the schedule. The first arg is absent if and only if all + the others are absent. + + args: + args (tuple): Child nodes, of type IntervalVar + """ + def _to_string(self, values, verbose, smap): + return "%s.spans(%s)" % (values[0], ", ".join(values[1:])) + + +class AlternativeExpression(NaryBooleanExpression): + """ + TODO/ + """ + def _to_string(self, values, verbose, smap): + return "alternative(%s, [%s])" % (values[0], ", ".join(values[1:])) + + +def spans(*args): + """Creates a new SpanExpression + """ + + return SpanExpression(list(_flattened(args))) + + +def alternative(*args): + """Creates a new AlternativeExpression + """ + + return AlternativeExpression(list(_flattened(args))) From db4cb7dcc9e78471d9e3353ba75b6e79fa3d1651 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 29 Jan 2024 16:21:53 -0700 Subject: [PATCH 0406/3044] Adding tests for span and alternative --- pyomo/contrib/cp/tests/test_docplex_walker.py | 52 ++++++++++++++++++ .../cp/tests/test_sequence_expressions.py | 53 +++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 0b2057217c0..fc475190ade 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -19,6 +19,7 @@ last_in_sequence, before_in_sequence, predecessor_to, + alternative ) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, @@ -1322,6 +1323,57 @@ def param_rule(m, i): self.assertTrue(expr[1].equals(cp.element([2, 4, 6], 0 + 1 * (x - 1) // 2) / a)) +@unittest.skipIf(not docplex_available, "docplex is not available") +class TestCPExpressionWalker_HierarchicalScheduling(CommonTest): + def get_model(self): + m = ConcreteModel() + def start_rule(m, i): + return 2*i + def length_rule(m, i): + return i + m.iv = IntervalVar([1, 2, 3], start=start_rule, length=length_rule, + optional=True) + m.whole_enchilada = IntervalVar() + + return m + + def test_spans(self): + m = self.get_model() + e = m.whole_enchilada.spans(m.iv[i] for i in [1, 2, 3]) + + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + self.assertIn(id(m.whole_enchilada), visitor.var_map) + whole_enchilada = visitor.var_map[id(m.whole_enchilada)] + iv = {} + for i in [1, 2, 3]: + self.assertIn(id(m.iv[i]), visitor.var_map) + iv[i] = visitor.var_map[id(m.iv[i])] + + self.assertTrue(expr[1].equals(cp.span(whole_enchilada, [iv[i] for i in + [1, 2, 3]]))) + + def test_alternative(self): + m = self.get_model() + e = alternative(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) + + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + self.assertIn(id(m.whole_enchilada), visitor.var_map) + whole_enchilada = visitor.var_map[id(m.whole_enchilada)] + iv = {} + for i in [1, 2, 3]: + self.assertIn(id(m.iv[i]), visitor.var_map) + iv[i] = visitor.var_map[id(m.iv[i])] + + self.assertTrue(expr[1].equals(cp.alternative(whole_enchilada, [iv[i] + for i in + [1, 2, + 3]]))) + + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_CumulFuncExpressions(CommonTest): def test_always_in(self): diff --git a/pyomo/contrib/cp/tests/test_sequence_expressions.py b/pyomo/contrib/cp/tests/test_sequence_expressions.py index 0ef2a9e3072..93a283c43d1 100644 --- a/pyomo/contrib/cp/tests/test_sequence_expressions.py +++ b/pyomo/contrib/cp/tests/test_sequence_expressions.py @@ -12,6 +12,12 @@ from io import StringIO import pyomo.common.unittest as unittest from pyomo.contrib.cp.interval_var import IntervalVar +from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( + AlternativeExpression, + SpanExpression, + alternative, + spans +) from pyomo.contrib.cp.scheduling_expr.sequence_expressions import ( NoOverlapExpression, FirstInSequenceExpression, @@ -102,3 +108,50 @@ def test_predecessor_in_sequence(self): self.assertIs(e.args[2], m.seq) self.assertEqual(str(e), "predecessor_to(i[0], i[1], seq)") + + +class TestHierarchicalSchedulingExpressions(unittest.TestCase): + def make_model(self): + m = ConcreteModel() + def start_rule(m, i): + return 2*i + def length_rule(m, i): + return i + m.iv = IntervalVar([1, 2, 3], start=start_rule, length=length_rule, + optional=True) + m.whole_enchilada = IntervalVar() + + return m + + def check_span_expression(self, m, e): + self.assertIsInstance(e, SpanExpression) + self.assertEqual(e.nargs(), 4) + self.assertEqual(len(e.args), 4) + self.assertIs(e.args[0], m.whole_enchilada) + for i in [1, 2, 3]: + self.assertIs(e.args[i], m.iv[i]) + + self.assertEqual(str(e), "whole_enchilada.spans(iv[1], iv[2], iv[3])") + + def test_spans(self): + m = self.make_model() + e = spans(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) + self.check_span_expression(m, e) + + def test_spans_method(self): + m = self.make_model() + e = m.whole_enchilada.spans(m.iv[i] for i in [1, 2, 3]) + self.check_span_expression(m, e) + + def test_alternative(self): + m = self.make_model() + e = alternative(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) + + self.assertIsInstance(e, AlternativeExpression) + self.assertEqual(e.nargs(), 4) + self.assertEqual(len(e.args), 4) + self.assertIs(e.args[0], m.whole_enchilada) + for i in [1, 2, 3]: + self.assertIs(e.args[i], m.iv[i]) + + self.assertEqual(str(e), "alternative(whole_enchilada, [iv[1], iv[2], iv[3]])") From bda572774a229eec120735966f9facb2647001e6 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 29 Jan 2024 16:24:46 -0700 Subject: [PATCH 0407/3044] Making References work for start_time, end_time, length, and is_present --- pyomo/contrib/cp/interval_var.py | 6 +++--- pyomo/contrib/cp/tests/test_interval_var.py | 23 +++++++++++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/cp/interval_var.py b/pyomo/contrib/cp/interval_var.py index 2379a58a1ff..fb88ab14832 100644 --- a/pyomo/contrib/cp/interval_var.py +++ b/pyomo/contrib/cp/interval_var.py @@ -51,7 +51,7 @@ class IntervalVarStartTime(IntervalVarTimePoint): """This class defines a single variable denoting a start time point of an IntervalVar""" - def __init__(self): + def __init__(self, *args, **kwd): super().__init__(domain=Integers, ctype=IntervalVarStartTime) @@ -59,7 +59,7 @@ class IntervalVarEndTime(IntervalVarTimePoint): """This class defines a single variable denoting an end time point of an IntervalVar""" - def __init__(self): + def __init__(self, *args, **kwd): super().__init__(domain=Integers, ctype=IntervalVarEndTime) @@ -69,7 +69,7 @@ class IntervalVarLength(ScalarVar): __slots__ = () - def __init__(self): + def __init__(self, *args, **kwd): super().__init__(domain=Integers, ctype=IntervalVarLength) def get_associated_interval_var(self): diff --git a/pyomo/contrib/cp/tests/test_interval_var.py b/pyomo/contrib/cp/tests/test_interval_var.py index edbf889fcda..1ebb87a67be 100644 --- a/pyomo/contrib/cp/tests/test_interval_var.py +++ b/pyomo/contrib/cp/tests/test_interval_var.py @@ -17,7 +17,7 @@ IntervalVarPresence, ) from pyomo.core.expr import GetItemExpression, GetAttrExpression -from pyomo.environ import ConcreteModel, Integers, Set, value, Var +from pyomo.environ import ConcreteModel, Integers, Reference, Set, value, Var class TestScalarIntervalVar(unittest.TestCase): @@ -217,5 +217,24 @@ def test_index_by_expr(self): self.assertIs(thing2.args[0], thing1) self.assertEqual(thing2.args[1], 'start_time') - # TODO: But this is where it dies. expr1 = m.act[m.i, 2].start_time.before(m.act[m.i**2, 1].end_time) + + def test_reference(self): + m = ConcreteModel() + m.act = IntervalVar([1, 2], end=[0, 10], optional=True) + + thing = Reference(m.act[:].is_present) + self.assertIs(thing[1], m.act[1].is_present) + self.assertIs(thing[2], m.act[2].is_present) + + thing = Reference(m.act[:].start_time) + self.assertIs(thing[1], m.act[1].start_time) + self.assertIs(thing[2], m.act[2].start_time) + + thing = Reference(m.act[:].end_time) + self.assertIs(thing[1], m.act[1].end_time) + self.assertIs(thing[2], m.act[2].end_time) + + thing = Reference(m.act[:].length) + self.assertIs(thing[1], m.act[1].length) + self.assertIs(thing[2], m.act[2].length) From b689a97dcec94fe040c4c7c786ce9f35ba9bee0f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 30 Jan 2024 09:39:16 -0700 Subject: [PATCH 0408/3044] Apply new black --- pyomo/contrib/solver/ipopt.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 534a9173d07..c6c7a6ee17a 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -94,15 +94,15 @@ def __init__( implicit_domain=implicit_domain, visibility=visibility, ) - self.timing_info.no_function_solve_time: Optional[ - float - ] = self.timing_info.declare( - 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) + self.timing_info.no_function_solve_time: Optional[float] = ( + self.timing_info.declare( + 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) + ) ) - self.timing_info.function_solve_time: Optional[ - float - ] = self.timing_info.declare( - 'function_solve_time', ConfigValue(domain=NonNegativeFloat) + self.timing_info.function_solve_time: Optional[float] = ( + self.timing_info.declare( + 'function_solve_time', ConfigValue(domain=NonNegativeFloat) + ) ) From 56a36e97ffeb540964014718219051ea306cc9f8 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 30 Jan 2024 11:55:12 -0700 Subject: [PATCH 0409/3044] Flesh out unit tests for base classes; change to load_solutions for backwards compatibility --- pyomo/contrib/solver/base.py | 34 ++++--- pyomo/contrib/solver/config.py | 4 +- pyomo/contrib/solver/factory.py | 4 +- pyomo/contrib/solver/ipopt.py | 8 +- .../solver/tests/solvers/test_ipopt.py | 2 +- pyomo/contrib/solver/tests/unit/test_base.py | 90 ++++++++++++++++++- .../contrib/solver/tests/unit/test_config.py | 4 +- 7 files changed, 114 insertions(+), 32 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 0b33f8a5648..e0eb58924c1 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -14,8 +14,6 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple import os -from .config import SolverConfig - from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData @@ -29,6 +27,7 @@ from pyomo.core.base import SymbolMap from pyomo.core.base.label import NumericLabeler from pyomo.core.staleflag import StaleFlagManager +from pyomo.contrib.solver.config import SolverConfig from pyomo.contrib.solver.util import get_objective from pyomo.contrib.solver.results import ( Results, @@ -215,7 +214,7 @@ def _get_primals( A map of variables to primals. """ raise NotImplementedError( - '{0} does not support the get_primals method'.format(type(self)) + f'{type(self)} does not support the get_primals method' ) def _get_duals( @@ -235,9 +234,7 @@ def _get_duals( duals: dict Maps constraints to dual values """ - raise NotImplementedError( - '{0} does not support the get_duals method'.format(type(self)) - ) + raise NotImplementedError(f'{type(self)} does not support the get_duals method') def _get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None @@ -255,7 +252,7 @@ def _get_reduced_costs( Maps variable to reduced cost """ raise NotImplementedError( - '{0} does not support the get_reduced_costs method'.format(type(self)) + f'{type(self)} does not support the get_reduced_costs method' ) @abc.abstractmethod @@ -331,15 +328,20 @@ def update_params(self): """ -class LegacySolverInterface: +class LegacySolverWrapper: """ Class to map the new solver interface features into the legacy solver interface. Necessary for backwards compatibility. """ - def set_config(self, config): - # TODO: Make a mapping from new config -> old config - pass + # + # Support "with" statements + # + def __enter__(self): + return self + + def __exit__(self, t, v, traceback): + """Exit statement - enables `with` statements.""" def solve( self, @@ -369,7 +371,7 @@ def solve( original_config = self.config self.config = self.config() self.config.tee = tee - self.config.load_solution = load_solutions + self.config.load_solutions = load_solutions self.config.symbolic_solver_labels = symbolic_solver_labels self.config.time_limit = timelimit self.config.report_timing = report_timing @@ -489,7 +491,7 @@ def options(self): """ Read the options for the dictated solver. - NOTE: Only the set of solvers for which the LegacySolverInterface is compatible + NOTE: Only the set of solvers for which the LegacySolverWrapper is compatible are accounted for within this property. Not all solvers are currently covered by this backwards compatibility class. @@ -511,9 +513,3 @@ def options(self, val): found = True if not found: raise NotImplementedError('Could not find the correct options') - - def __enter__(self): - return self - - def __exit__(self, t, v, traceback): - pass diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 6068269dcae..4c81d31a820 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -58,8 +58,8 @@ def __init__( description="If True, the solver output gets logged.", ), ) - self.load_solution: bool = self.declare( - 'load_solution', + self.load_solutions: bool = self.declare( + 'load_solutions', ConfigValue( domain=bool, default=True, diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index fa3e2611667..e499605afd4 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -12,7 +12,7 @@ from pyomo.opt.base import SolverFactory as LegacySolverFactory from pyomo.common.factory import Factory -from pyomo.contrib.solver.base import LegacySolverInterface +from pyomo.contrib.solver.base import LegacySolverWrapper class SolverFactoryClass(Factory): @@ -21,7 +21,7 @@ def decorator(cls): self._cls[name] = cls self._doc[name] = doc - class LegacySolver(LegacySolverInterface, cls): + class LegacySolver(LegacySolverWrapper, cls): pass LegacySolverFactory.register(name, doc)(LegacySolver) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index c6c7a6ee17a..7c2a3f471e3 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -390,15 +390,15 @@ def solve(self, model, **kwds): results.solver_name = 'ipopt' results.solver_version = self.version() if ( - config.load_solution + config.load_solutions and results.solution_status == SolutionStatus.noSolution ): raise RuntimeError( 'A feasible solution was not found, so no solution can be loaded.' - 'Please set config.load_solution=False to bypass this error.' + 'Please set config.load_solutions=False to bypass this error.' ) - if config.load_solution: + if config.load_solutions: results.solution_loader.load_vars() if ( hasattr(model, 'dual') @@ -417,7 +417,7 @@ def solve(self, model, **kwds): results.solution_status in {SolutionStatus.feasible, SolutionStatus.optimal} and len(nl_info.objectives) > 0 ): - if config.load_solution: + if config.load_solutions: results.incumbent_objective = value(nl_info.objectives[0]) else: results.incumbent_objective = value( diff --git a/pyomo/contrib/solver/tests/solvers/test_ipopt.py b/pyomo/contrib/solver/tests/solvers/test_ipopt.py index 9638d94bdda..627d502629c 100644 --- a/pyomo/contrib/solver/tests/solvers/test_ipopt.py +++ b/pyomo/contrib/solver/tests/solvers/test_ipopt.py @@ -43,7 +43,7 @@ def rosenbrock(m): def test_ipopt_config(self): # Test default initialization config = ipoptConfig() - self.assertTrue(config.load_solution) + self.assertTrue(config.load_solutions) self.assertIsInstance(config.solver_options, ConfigDict) self.assertIsInstance(config.executable, ExecutableData) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index e3a8999d8c5..dd94ef18fc3 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -14,8 +14,27 @@ class TestSolverBase(unittest.TestCase): + def test_abstract_member_list(self): + expected_list = ['solve', 'available', 'version'] + member_list = list(base.SolverBase.__abstractmethods__) + self.assertEqual(sorted(expected_list), sorted(member_list)) + + def test_class_method_list(self): + expected_list = [ + 'Availability', + 'CONFIG', + 'available', + 'is_persistent', + 'solve', + 'version', + ] + method_list = [ + method for method in dir(base.SolverBase) if method.startswith('_') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) - def test_solver_base(self): + def test_init(self): self.instance = base.SolverBase() self.assertFalse(self.instance.is_persistent()) self.assertEqual(self.instance.version(), None) @@ -23,6 +42,20 @@ def test_solver_base(self): self.assertEqual(self.instance.solve(None), None) self.assertEqual(self.instance.available(), None) + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) + def test_context_manager(self): + with base.SolverBase() as self.instance: + self.assertFalse(self.instance.is_persistent()) + self.assertEqual(self.instance.version(), None) + self.assertEqual(self.instance.CONFIG, self.instance.config) + self.assertEqual(self.instance.solve(None), None) + self.assertEqual(self.instance.available(), None) + + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) + def test_config_kwds(self): + self.instance = base.SolverBase(tee=True) + self.assertTrue(self.instance.config.tee) + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) def test_solver_availability(self): self.instance = base.SolverBase() @@ -57,8 +90,41 @@ def test_abstract_member_list(self): member_list = list(base.PersistentSolverBase.__abstractmethods__) self.assertEqual(sorted(expected_list), sorted(member_list)) + def test_class_method_list(self): + expected_list = [ + 'Availability', + 'CONFIG', + '_abc_impl', + '_get_duals', + '_get_primals', + '_get_reduced_costs', + '_load_vars', + 'add_block', + 'add_constraints', + 'add_params', + 'add_variables', + 'available', + 'is_persistent', + 'remove_block', + 'remove_constraints', + 'remove_params', + 'remove_variables', + 'set_instance', + 'set_objective', + 'solve', + 'update_params', + 'update_variables', + 'version', + ] + method_list = [ + method + for method in dir(base.PersistentSolverBase) + if method.startswith('__') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + @unittest.mock.patch.multiple(base.PersistentSolverBase, __abstractmethods__=set()) - def test_persistent_solver_base(self): + def test_init(self): self.instance = base.PersistentSolverBase() self.assertTrue(self.instance.is_persistent()) self.assertEqual(self.instance.set_instance(None), None) @@ -82,3 +148,23 @@ def test_persistent_solver_base(self): with self.assertRaises(NotImplementedError): self.instance._get_reduced_costs() + + @unittest.mock.patch.multiple(base.PersistentSolverBase, __abstractmethods__=set()) + def test_context_manager(self): + with base.PersistentSolverBase() as self.instance: + self.assertTrue(self.instance.is_persistent()) + self.assertEqual(self.instance.set_instance(None), None) + self.assertEqual(self.instance.add_variables(None), None) + self.assertEqual(self.instance.add_params(None), None) + self.assertEqual(self.instance.add_constraints(None), None) + self.assertEqual(self.instance.add_block(None), None) + self.assertEqual(self.instance.remove_variables(None), None) + self.assertEqual(self.instance.remove_params(None), None) + self.assertEqual(self.instance.remove_constraints(None), None) + self.assertEqual(self.instance.remove_block(None), None) + self.assertEqual(self.instance.set_objective(None), None) + self.assertEqual(self.instance.update_variables(None), None) + self.assertEqual(self.instance.update_params(), None) + +class TestLegacySolverWrapper(unittest.TestCase): + pass diff --git a/pyomo/contrib/solver/tests/unit/test_config.py b/pyomo/contrib/solver/tests/unit/test_config.py index 3ad8319343b..4a7cc250623 100644 --- a/pyomo/contrib/solver/tests/unit/test_config.py +++ b/pyomo/contrib/solver/tests/unit/test_config.py @@ -19,7 +19,7 @@ def test_interface_default_instantiation(self): self.assertIsNone(config._description) self.assertEqual(config._visibility, 0) self.assertFalse(config.tee) - self.assertTrue(config.load_solution) + self.assertTrue(config.load_solutions) self.assertTrue(config.raise_exception_on_nonoptimal_result) self.assertFalse(config.symbolic_solver_labels) self.assertIsNone(config.timer) @@ -43,7 +43,7 @@ def test_interface_default_instantiation(self): self.assertIsNone(config._description) self.assertEqual(config._visibility, 0) self.assertFalse(config.tee) - self.assertTrue(config.load_solution) + self.assertTrue(config.load_solutions) self.assertFalse(config.symbolic_solver_labels) self.assertIsNone(config.rel_gap) self.assertIsNone(config.abs_gap) From ed87c162cdb0239171a681344acc2d66b706b0dd Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 30 Jan 2024 12:06:28 -0700 Subject: [PATCH 0410/3044] Fixed parmest tests of new interface. --- .../parmest/deprecated/tests/test_examples.py | 34 +- .../parmest/deprecated/tests/test_parmest.py | 6 +- .../deprecated/tests/test_scenariocreator.py | 4 +- .../parmest/deprecated/tests/test_utils.py | 2 +- .../examples/reactor_design/reactor_design.py | 5 +- .../examples/rooney_biegler/rooney_biegler.py | 1 - .../semibatch/parameter_estimation_example.py | 20 +- .../examples/semibatch/scenario_example.py | 17 +- .../parmest/examples/semibatch/semibatch.py | 32 ++ pyomo/contrib/parmest/graphics.py | 2 +- pyomo/contrib/parmest/parmest.py | 31 +- pyomo/contrib/parmest/scenariocreator.py | 4 +- pyomo/contrib/parmest/tests/test_parmest.py | 348 +++++++++++------- .../parmest/tests/test_scenariocreator.py | 30 +- pyomo/contrib/parmest/tests/test_utils.py | 9 +- 15 files changed, 347 insertions(+), 198 deletions(-) diff --git a/pyomo/contrib/parmest/deprecated/tests/test_examples.py b/pyomo/contrib/parmest/deprecated/tests/test_examples.py index 67e06130384..04aff572529 100644 --- a/pyomo/contrib/parmest/deprecated/tests/test_examples.py +++ b/pyomo/contrib/parmest/deprecated/tests/test_examples.py @@ -32,12 +32,12 @@ def tearDownClass(self): pass def test_model(self): - from pyomo.contrib.parmest.examples.rooney_biegler import rooney_biegler + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import rooney_biegler rooney_biegler.main() def test_model_with_constraint(self): - from pyomo.contrib.parmest.examples.rooney_biegler import ( + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( rooney_biegler_with_constraint, ) @@ -45,7 +45,7 @@ def test_model_with_constraint(self): @unittest.skipUnless(seaborn_available, "test requires seaborn") def test_parameter_estimation_example(self): - from pyomo.contrib.parmest.examples.rooney_biegler import ( + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( parameter_estimation_example, ) @@ -53,13 +53,13 @@ def test_parameter_estimation_example(self): @unittest.skipUnless(seaborn_available, "test requires seaborn") def test_bootstrap_example(self): - from pyomo.contrib.parmest.examples.rooney_biegler import bootstrap_example + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import bootstrap_example bootstrap_example.main() @unittest.skipUnless(seaborn_available, "test requires seaborn") def test_likelihood_ratio_example(self): - from pyomo.contrib.parmest.examples.rooney_biegler import ( + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( likelihood_ratio_example, ) @@ -81,7 +81,7 @@ def tearDownClass(self): pass def test_example(self): - from pyomo.contrib.parmest.examples.reaction_kinetics import ( + from pyomo.contrib.parmest.deprecated.examples.reaction_kinetics import ( simple_reaction_parmest_example, ) @@ -103,19 +103,19 @@ def tearDownClass(self): pass def test_model(self): - from pyomo.contrib.parmest.examples.semibatch import semibatch + from pyomo.contrib.parmest.deprecated.examples.semibatch import semibatch semibatch.main() def test_parameter_estimation_example(self): - from pyomo.contrib.parmest.examples.semibatch import ( + from pyomo.contrib.parmest.deprecated.examples.semibatch import ( parameter_estimation_example, ) parameter_estimation_example.main() def test_scenario_example(self): - from pyomo.contrib.parmest.examples.semibatch import scenario_example + from pyomo.contrib.parmest.deprecated.examples.semibatch import scenario_example scenario_example.main() @@ -136,12 +136,12 @@ def tearDownClass(self): @unittest.pytest.mark.expensive def test_model(self): - from pyomo.contrib.parmest.examples.reactor_design import reactor_design + from pyomo.contrib.parmest.deprecated.examples.reactor_design import reactor_design reactor_design.main() def test_parameter_estimation_example(self): - from pyomo.contrib.parmest.examples.reactor_design import ( + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( parameter_estimation_example, ) @@ -149,13 +149,13 @@ def test_parameter_estimation_example(self): @unittest.skipUnless(seaborn_available, "test requires seaborn") def test_bootstrap_example(self): - from pyomo.contrib.parmest.examples.reactor_design import bootstrap_example + from pyomo.contrib.parmest.deprecated.examples.reactor_design import bootstrap_example bootstrap_example.main() @unittest.pytest.mark.expensive def test_likelihood_ratio_example(self): - from pyomo.contrib.parmest.examples.reactor_design import ( + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( likelihood_ratio_example, ) @@ -163,19 +163,19 @@ def test_likelihood_ratio_example(self): @unittest.pytest.mark.expensive def test_leaveNout_example(self): - from pyomo.contrib.parmest.examples.reactor_design import leaveNout_example + from pyomo.contrib.parmest.deprecated.examples.reactor_design import leaveNout_example leaveNout_example.main() def test_timeseries_data_example(self): - from pyomo.contrib.parmest.examples.reactor_design import ( + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( timeseries_data_example, ) timeseries_data_example.main() def test_multisensor_data_example(self): - from pyomo.contrib.parmest.examples.reactor_design import ( + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( multisensor_data_example, ) @@ -183,7 +183,7 @@ def test_multisensor_data_example(self): @unittest.skipUnless(matplotlib_available, "test requires matplotlib") def test_datarec_example(self): - from pyomo.contrib.parmest.examples.reactor_design import datarec_example + from pyomo.contrib.parmest.deprecated.examples.reactor_design import datarec_example datarec_example.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_parmest.py b/pyomo/contrib/parmest/deprecated/tests/test_parmest.py index 7e692989b0c..40c98dac3af 100644 --- a/pyomo/contrib/parmest/deprecated/tests/test_parmest.py +++ b/pyomo/contrib/parmest/deprecated/tests/test_parmest.py @@ -54,7 +54,7 @@ @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") class TestRooneyBiegler(unittest.TestCase): def setUp(self): - from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler.rooney_biegler import ( rooney_biegler_model, ) @@ -605,7 +605,7 @@ def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") class TestReactorDesign(unittest.TestCase): def setUp(self): - from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) @@ -879,7 +879,7 @@ def test_covariance(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") class TestSquareInitialization_RooneyBiegler(unittest.TestCase): def setUp(self): - from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler_with_constraint import ( + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler.rooney_biegler_with_constraint import ( rooney_biegler_model_with_constraint, ) diff --git a/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py b/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py index 22a851ae32e..54cbe80f73c 100644 --- a/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py +++ b/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py @@ -36,7 +36,7 @@ @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") class TestScenarioReactorDesign(unittest.TestCase): def setUp(self): - from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) @@ -112,7 +112,7 @@ def test_no_csv_if_empty(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") class TestScenarioSemibatch(unittest.TestCase): def setUp(self): - import pyomo.contrib.parmest.examples.semibatch.semibatch as sb + import pyomo.contrib.parmest.deprecated.examples.semibatch.semibatch as sb import json # Vars to estimate in parmest diff --git a/pyomo/contrib/parmest/deprecated/tests/test_utils.py b/pyomo/contrib/parmest/deprecated/tests/test_utils.py index 514c14b1e82..1a8247ddcc9 100644 --- a/pyomo/contrib/parmest/deprecated/tests/test_utils.py +++ b/pyomo/contrib/parmest/deprecated/tests/test_utils.py @@ -35,7 +35,7 @@ def tearDownClass(self): @unittest.pytest.mark.expensive def test_convert_param_to_var(self): - from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( + from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( reactor_design_model, ) diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index 1479009abcc..db3b0e1d380 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -118,7 +118,7 @@ def get_labeled_model(self): return m -if __name__ == "__main__": +def main(): # For a range of sv values, return ca, cb, cc, and cd results = [] @@ -142,4 +142,7 @@ def get_labeled_model(self): results = pd.DataFrame(results, columns=["sv", "caf", "ca", "cb", "cc", "cd"]) print(results) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py index 2ac03504260..6e7d6219a64 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py @@ -62,7 +62,6 @@ def label_model(self): m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) - m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.asymptote, m.rate_constant]) diff --git a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py index fc4c9f5c675..145569f7535 100644 --- a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py @@ -12,12 +12,11 @@ import json from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model - +from pyomo.contrib.parmest.examples.semibatch.semibatch import ( + SemiBatchExperiment, +) def main(): - # Vars to estimate - theta_names = ['k1', 'k2', 'E1', 'E2'] # Data, list of dictionaries data = [] @@ -28,11 +27,20 @@ def main(): d = json.load(infile) data.append(d) + # Create an experiment list + exp_list= [] + for i in range(len(data)): + exp_list.append(SemiBatchExperiment(data[i])) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + # Note, the model already includes a 'SecondStageCost' expression # for sum of squared error that will be used in parameter estimation - pest = parmest.Estimator(generate_model, data, theta_names) - + pest = parmest.Estimator(exp_list) + obj, theta = pest.theta_est() print(obj) print(theta) diff --git a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py index 071e53236c4..a80a82671bc 100644 --- a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py @@ -12,13 +12,13 @@ import json from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model +from pyomo.contrib.parmest.examples.semibatch.semibatch import ( + SemiBatchExperiment, +) import pyomo.contrib.parmest.scenariocreator as sc def main(): - # Vars to estimate in parmest - theta_names = ['k1', 'k2', 'E1', 'E2'] # Data: list of dictionaries data = [] @@ -29,7 +29,16 @@ def main(): d = json.load(infile) data.append(d) - pest = parmest.Estimator(generate_model, data, theta_names) + # Create an experiment list + exp_list= [] + for i in range(len(data)): + exp_list.append(SemiBatchExperiment(data[i])) + + # View one model + # exp0_model = exp_list[0].get_labeled_model() + # print(exp0_model.pprint()) + + pest = parmest.Estimator(exp_list) scenmaker = sc.ScenarioCreator(pest, "ipopt") diff --git a/pyomo/contrib/parmest/examples/semibatch/semibatch.py b/pyomo/contrib/parmest/examples/semibatch/semibatch.py index 6762531a338..3ef7bc01aa9 100644 --- a/pyomo/contrib/parmest/examples/semibatch/semibatch.py +++ b/pyomo/contrib/parmest/examples/semibatch/semibatch.py @@ -29,8 +29,11 @@ SolverFactory, exp, minimize, + Suffix, + ComponentUID, ) from pyomo.dae import ContinuousSet, DerivativeVar +from pyomo.contrib.parmest.experiment import Experiment def generate_model(data): @@ -268,6 +271,35 @@ def total_cost_rule(model): return m +class SemiBatchExperiment(Experiment): + + def __init__(self, data): + self.data = data + self.model = None + + def create_model(self): + self.model = generate_model(self.data) + + def label_model(self): + + m = self.model + + m.unknown_parameters = Suffix(direction=Suffix.LOCAL) + m.unknown_parameters.update((k, ComponentUID(k)) + for k in [m.k1, m.k2, m.E1, m.E2]) + + + def finalize_model(self): + pass + + def get_labeled_model(self): + self.create_model() + self.label_model() + self.finalize_model() + + return self.model + + def main(): # Data loaded from files file_dirname = dirname(abspath(str(__file__))) diff --git a/pyomo/contrib/parmest/graphics.py b/pyomo/contrib/parmest/graphics.py index b8dfa243b9a..65efb5cfd64 100644 --- a/pyomo/contrib/parmest/graphics.py +++ b/pyomo/contrib/parmest/graphics.py @@ -152,7 +152,7 @@ def _add_scipy_dist_CI( data_slice.append(np.array([[theta_star[var]] * ncells] * ncells)) data_slice = np.dstack(tuple(data_slice)) - elif isinstance(dist, stats.kde.gaussian_kde): + elif isinstance(dist, stats.gaussian_kde): for var in theta_star.index: if var == xvar: data_slice.append(X.ravel()) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index a00671c2ea6..e256b0f38d7 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -745,7 +745,7 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): for snum in scenario_numbers: sname = "scenario_NODE" + str(snum) instance = _experiment_instance_creation_callback(sname, None, dummy_cb) - model_theta_names = [k.name for k,v in instance.unknown_parameters.items()] + model_theta_names = self._expand_indexed_unknowns(instance) if initialize_parmest_model: # list to store fitted parameter names that will be unfixed @@ -1186,6 +1186,28 @@ def leaveNout_bootstrap_test( return results + # expand indexed variables to get full list of thetas + def _expand_indexed_unknowns(self, model_temp): + + model_theta_list = [k.name for k,v in model_temp.unknown_parameters.items()] + + # check for indexed theta items + indexed_theta_list = [] + for theta_i in model_theta_list: + var_cuid = ComponentUID(theta_i) + var_validate = var_cuid.find_component_on(model_temp) + for ind in var_validate.index_set(): + if ind is not None: + indexed_theta_list.append(theta_i + '[' + str(ind) + ']') + else: + indexed_theta_list.append(theta_i) + + # if we found indexed thetas, use expanded list + if len(indexed_theta_list) > len(model_theta_list): + model_theta_list = indexed_theta_list + + return model_theta_list + def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): """ Objective value for each theta @@ -1218,8 +1240,8 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): else: # create a local instance of the pyomo model to access model variables and parameters model_temp = self._create_parmest_model(0) - model_theta_list = [k.name for k,v in model_temp.unknown_parameters.items()] - + model_theta_list = self._expand_indexed_unknowns(model_temp) + # # iterate over original theta_names # for theta_i in self.theta_names: # var_cuid = ComponentUID(theta_i) @@ -1254,7 +1276,7 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): if theta_values is None: all_thetas = {} # dictionary to store fitted variables # use appropriate theta names member - theta_names = self.estimator_theta_names() + theta_names = model_theta_list else: assert isinstance(theta_values, pd.DataFrame) # for parallel code we need to use lists and dicts in the loop @@ -1262,7 +1284,6 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): # # check if theta_names are in model for theta in list(theta_names): theta_temp = theta.replace("'", "") # cleaning quotes from theta_names - assert theta_temp in [ t.replace("'", "") for t in model_theta_list ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index b849bfdfd5b..c48ac2bf027 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -151,13 +151,13 @@ def ScenariosFromExperiments(self, addtoSet): assert isinstance(addtoSet, ScenarioSet) - scenario_numbers = list(range(len(self.pest.callback_data))) + scenario_numbers = list(range(len(self.pest.exp_list))) prob = 1.0 / len(scenario_numbers) for exp_num in scenario_numbers: ##print("Experiment number=", exp_num) model = self.pest._instance_creation_callback( - exp_num, self.pest.callback_data + exp_num, ) opt = pyo.SolverFactory(self.solvername) results = opt.solve(model) # solves and updates model diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 2cc8ad36b0a..b88871e0dbc 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -33,6 +33,7 @@ import pyomo.contrib.parmest.parmest as parmest import pyomo.contrib.parmest.graphics as graphics import pyomo.contrib.parmest as parmestbase +from pyomo.contrib.parmest.experiment import Experiment import pyomo.environ as pyo import pyomo.dae as dae @@ -46,7 +47,6 @@ testdir = os.path.dirname(os.path.abspath(__file__)) - @unittest.skipIf( not parmest.parmest_available, "Cannot test parmest: required dependencies are missing", @@ -55,7 +55,7 @@ class TestRooneyBiegler(unittest.TestCase): def setUp(self): from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, + RooneyBieglerExperiment, ) # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) @@ -64,23 +64,26 @@ def setUp(self): columns=["hour", "y"], ) - theta_names = ["asymptote", "rate_constant"] - - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 - for i in data.index - ) + # Sum of squared error function + def SSE(model): + expr = (model.experiment_outputs[model.y] - \ + model.response_function[model.experiment_outputs[model.hour]]) ** 2 return expr + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + + # Create an instance of the parmest estimator + pest = parmest.Estimator(exp_list, obj_function=SSE) + solver_options = {"tol": 1e-8} self.data = data self.pest = parmest.Estimator( - rooney_biegler_model, - data, - theta_names, - SSE, + exp_list, + obj_function=SSE, solver_options=solver_options, tee=True, ) @@ -229,15 +232,17 @@ def test_theta_est_cov(self): # Covariance matrix self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 + cov['asymptote']['asymptote'], 6.30579403, places=2 ) # 6.22864 from paper self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 + cov['asymptote']['rate_constant'], -0.4395341, places=2 ) # -0.4322 from paper self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 + cov['rate_constant']['asymptote'], -0.4395341, places=2 ) # -0.4322 from paper - self.assertAlmostEqual(cov.iloc[1, 1], 0.04124, places=2) # 0.04124 from paper + self.assertAlmostEqual( + cov['rate_constant']['rate_constant'], 0.04124, places=2 + ) # 0.04124 from paper """ Why does the covariance matrix from parmest not match the paper? Parmest is calculating the exact reduced Hessian. The paper (Rooney and Bielger, 2001) likely @@ -348,7 +353,12 @@ def model(t, asymptote, rate_constant): ) @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") class TestModelVariants(unittest.TestCase): + def setUp(self): + from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( + RooneyBieglerExperiment, + ) + self.data = pd.DataFrame( data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], columns=["hour", "y"], @@ -360,6 +370,9 @@ def rooney_biegler_params(data): model.asymptote = pyo.Param(initialize=15, mutable=True) model.rate_constant = pyo.Param(initialize=0.5, mutable=True) + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + def response_rule(m, h): expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) return expr @@ -368,6 +381,17 @@ def response_rule(m, h): return model + class RooneyBieglerExperimentParams(RooneyBieglerExperiment): + + def create_model(self): + self.model = rooney_biegler_params(self.data) + + rooney_biegler_params_exp_list = [] + for i in range(self.data.shape[0]): + rooney_biegler_params_exp_list.append( + RooneyBieglerExperimentParams(self.data.loc[i,:].to_frame().transpose()) + ) + def rooney_biegler_indexed_params(data): model = pyo.ConcreteModel() @@ -378,6 +402,9 @@ def rooney_biegler_indexed_params(data): mutable=True, ) + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + def response_rule(m, h): expr = m.theta["asymptote"] * ( 1 - pyo.exp(-m.theta["rate_constant"] * h) @@ -388,6 +415,29 @@ def response_rule(m, h): return model + class RooneyBieglerExperimentIndexedParams(RooneyBieglerExperiment): + + def create_model(self): + self.model = rooney_biegler_indexed_params(self.data) + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) + m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.theta]) + + rooney_biegler_indexed_params_exp_list = [] + for i in range(self.data.shape[0]): + rooney_biegler_indexed_params_exp_list.append( + RooneyBieglerExperimentIndexedParams(self.data.loc[i,:].to_frame().transpose()) + ) + def rooney_biegler_vars(data): model = pyo.ConcreteModel() @@ -396,6 +446,9 @@ def rooney_biegler_vars(data): model.asymptote.fixed = True # parmest will unfix theta variables model.rate_constant.fixed = True + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + def response_rule(m, h): expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) return expr @@ -404,6 +457,17 @@ def response_rule(m, h): return model + class RooneyBieglerExperimentVars(RooneyBieglerExperiment): + + def create_model(self): + self.model = rooney_biegler_vars(self.data) + + rooney_biegler_vars_exp_list = [] + for i in range(self.data.shape[0]): + rooney_biegler_vars_exp_list.append( + RooneyBieglerExperimentVars(self.data.loc[i,:].to_frame().transpose()) + ) + def rooney_biegler_indexed_vars(data): model = pyo.ConcreteModel() @@ -418,6 +482,9 @@ def rooney_biegler_indexed_vars(data): ) model.theta["rate_constant"].fixed = True + model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) + model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) + def response_rule(m, h): expr = m.theta["asymptote"] * ( 1 - pyo.exp(-m.theta["rate_constant"] * h) @@ -428,11 +495,34 @@ def response_rule(m, h): return model - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 - for i in data.index + class RooneyBieglerExperimentIndexedVars(RooneyBieglerExperiment): + + def create_model(self): + self.model = rooney_biegler_indexed_vars(self.data) + + def label_model(self): + + m = self.model + + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) + m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.theta]) + + + rooney_biegler_indexed_vars_exp_list = [] + for i in range(self.data.shape[0]): + rooney_biegler_indexed_vars_exp_list.append( + RooneyBieglerExperimentIndexedVars(self.data.loc[i,:].to_frame().transpose()) ) + + # Sum of squared error function + def SSE(model): + expr = (model.experiment_outputs[model.y] - \ + model.response_function[model.experiment_outputs[model.hour]]) ** 2 return expr self.objective_function = SSE @@ -444,32 +534,32 @@ def SSE(model, data): self.input = { "param": { - "model": rooney_biegler_params, + "exp_list": rooney_biegler_params_exp_list, "theta_names": ["asymptote", "rate_constant"], "theta_vals": theta_vals, }, "param_index": { - "model": rooney_biegler_indexed_params, + "exp_list": rooney_biegler_indexed_params_exp_list, "theta_names": ["theta"], "theta_vals": theta_vals_index, }, "vars": { - "model": rooney_biegler_vars, + "exp_list": rooney_biegler_vars_exp_list, "theta_names": ["asymptote", "rate_constant"], "theta_vals": theta_vals, }, "vars_index": { - "model": rooney_biegler_indexed_vars, + "exp_list": rooney_biegler_indexed_vars_exp_list, "theta_names": ["theta"], "theta_vals": theta_vals_index, }, "vars_quoted_index": { - "model": rooney_biegler_indexed_vars, + "exp_list": rooney_biegler_indexed_vars_exp_list, "theta_names": ["theta['asymptote']", "theta['rate_constant']"], "theta_vals": theta_vals_index, }, "vars_str_index": { - "model": rooney_biegler_indexed_vars, + "exp_list": rooney_biegler_indexed_vars_exp_list, "theta_names": ["theta[asymptote]", "theta[rate_constant]"], "theta_vals": theta_vals_index, }, @@ -480,58 +570,52 @@ def SSE(model, data): not parmest.inverse_reduced_hessian_available, "Cannot test covariance matrix: required ASL dependency is missing", ) + + def check_rooney_biegler_results(self, objval, cov): + + # get indices in covariance matrix + cov_cols = cov.columns.to_list() + asymptote_index = [idx for idx, s in enumerate(cov_cols) if 'asymptote' in s][0] + rate_constant_index = [idx for idx, s in enumerate(cov_cols) if 'rate_constant' in s][0] + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[asymptote_index, asymptote_index], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[asymptote_index, rate_constant_index], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[rate_constant_index, asymptote_index], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[rate_constant_index, rate_constant_index], 0.04193591, places=2 + ) # 0.04124 from paper + def test_parmest_basics(self): + for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, + parmest_input["exp_list"], + obj_function=self.objective_function, ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper + self.check_rooney_biegler_results(objval, cov) obj_at_theta = pest.objective_at_theta(parmest_input["theta_vals"]) self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) def test_parmest_basics_with_initialize_parmest_model_option(self): - for model_type, parmest_input in self.input.items(): + + for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, + parmest_input["exp_list"], + obj_function=self.objective_function, ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper + self.check_rooney_biegler_results(objval, cov) obj_at_theta = pest.objective_at_theta( parmest_input["theta_vals"], initialize_parmest_model=True @@ -540,12 +624,11 @@ def test_parmest_basics_with_initialize_parmest_model_option(self): self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) def test_parmest_basics_with_square_problem_solve(self): + for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, + parmest_input["exp_list"], + obj_function=self.objective_function, ) obj_at_theta = pest.objective_at_theta( @@ -553,50 +636,23 @@ def test_parmest_basics_with_square_problem_solve(self): ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper + self.check_rooney_biegler_results(objval, cov) self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, + parmest_input["exp_list"], + obj_function=self.objective_function, ) obj_at_theta = pest.objective_at_theta(initialize_parmest_model=True) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper - + self.check_rooney_biegler_results(objval, cov) @unittest.skipIf( not parmest.parmest_available, @@ -606,7 +662,7 @@ def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): class TestReactorDesign(unittest.TestCase): def setUp(self): from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) # Data from the design @@ -635,22 +691,14 @@ def setUp(self): columns=["sv", "caf", "ca", "cb", "cc", "cd"], ) - theta_names = ["k1", "k2", "k3"] - - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) solver_options = {"max_iter": 6000} - self.pest = parmest.Estimator( - reactor_design_model, data, theta_names, SSE, solver_options=solver_options - ) + self.pest = parmest.Estimator(exp_list, obj_function='SSE', solver_options=solver_options) def test_theta_est(self): # used in data reconciliation @@ -759,6 +807,30 @@ def total_cost_rule(model): return m + class ReactorDesignExperimentDAE(Experiment): + + def __init__(self, data): + + self.data = data + self.model = None + + def create_model(self): + self.model = ABC_model(self.data) + + def label_model(self): + + m = self.model + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) + for k in [m.k1, m.k2]) + + def get_labeled_model(self): + self.create_model() + self.label_model() + + return self.model + # This example tests data formatted in 3 ways # Each format holds 1 scenario # 1. dataframe with time index @@ -793,18 +865,21 @@ def total_cost_rule(model): "cc": {k: v for (k, v) in zip(data.t, data.cc)}, } - theta_names = ["k1", "k2"] + # Create an experiment list + exp_list_df = [ReactorDesignExperimentDAE(data_df)] + exp_list_dict = [ReactorDesignExperimentDAE(data_dict)] - self.pest_df = parmest.Estimator(ABC_model, [data_df], theta_names) - self.pest_dict = parmest.Estimator(ABC_model, [data_dict], theta_names) + self.pest_df = parmest.Estimator(exp_list_df) + self.pest_dict = parmest.Estimator(exp_list_dict) # Estimator object with multiple scenarios - self.pest_df_multiple = parmest.Estimator( - ABC_model, [data_df, data_df], theta_names - ) - self.pest_dict_multiple = parmest.Estimator( - ABC_model, [data_dict, data_dict], theta_names - ) + exp_list_df_multiple = [ReactorDesignExperimentDAE(data_df), + ReactorDesignExperimentDAE(data_df)] + exp_list_dict_multiple = [ReactorDesignExperimentDAE(data_dict), + ReactorDesignExperimentDAE(data_dict)] + + self.pest_df_multiple = parmest.Estimator(exp_list_df_multiple) + self.pest_dict_multiple = parmest.Estimator(exp_list_dict_multiple) # Create an instance of the model self.m_df = ABC_model(data_df) @@ -880,7 +955,7 @@ def test_covariance(self): class TestSquareInitialization_RooneyBiegler(unittest.TestCase): def setUp(self): from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler_with_constraint import ( - rooney_biegler_model_with_constraint, + RooneyBieglerExperiment, ) # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) @@ -888,24 +963,25 @@ def setUp(self): data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], columns=["hour", "y"], ) + + # Sum of squared error function + def SSE(model): + expr = (model.experiment_outputs[model.y] - \ + model.response_function[model.experiment_outputs[model.hour]]) ** 2 + return expr - theta_names = ["asymptote", "rate_constant"] - - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 - for i in data.index + exp_list = [] + for i in range(data.shape[0]): + exp_list.append( + RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose()) ) - return expr solver_options = {"tol": 1e-8} self.data = data self.pest = parmest.Estimator( - rooney_biegler_model_with_constraint, - data, - theta_names, - SSE, + exp_list, + obj_function=SSE, solver_options=solver_options, tee=True, ) diff --git a/pyomo/contrib/parmest/tests/test_scenariocreator.py b/pyomo/contrib/parmest/tests/test_scenariocreator.py index 22a851ae32e..bf6fa12b8b1 100644 --- a/pyomo/contrib/parmest/tests/test_scenariocreator.py +++ b/pyomo/contrib/parmest/tests/test_scenariocreator.py @@ -37,7 +37,7 @@ class TestScenarioReactorDesign(unittest.TestCase): def setUp(self): from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) # Data from the design @@ -65,19 +65,13 @@ def setUp(self): ], columns=["sv", "caf", "ca", "cb", "cc", "cd"], ) + + # Create an experiment list + exp_list= [] + for i in range(data.shape[0]): + exp_list.append(ReactorDesignExperiment(data, i)) - theta_names = ["k1", "k2", "k3"] - - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - self.pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + self.pest = parmest.Estimator(exp_list, obj_function='SSE') def test_scen_from_exps(self): scenmaker = sc.ScenarioCreator(self.pest, "ipopt") @@ -115,9 +109,6 @@ def setUp(self): import pyomo.contrib.parmest.examples.semibatch.semibatch as sb import json - # Vars to estimate in parmest - theta_names = ["k1", "k2", "E1", "E2"] - self.fbase = os.path.join(testdir, "..", "examples", "semibatch") # Data, list of dictionaries data = [] @@ -131,7 +122,12 @@ def setUp(self): # Note, the model already includes a 'SecondStageCost' expression # for the sum of squared error that will be used in parameter estimation - self.pest = parmest.Estimator(sb.generate_model, data, theta_names) + # Create an experiment list + exp_list= [] + for i in range(len(data)): + exp_list.append(sb.SemiBatchExperiment(data[i])) + + self.pest = parmest.Estimator(exp_list) def test_semibatch_bootstrap(self): scenmaker = sc.ScenarioCreator(self.pest, "ipopt") diff --git a/pyomo/contrib/parmest/tests/test_utils.py b/pyomo/contrib/parmest/tests/test_utils.py index 514c14b1e82..99ba7b7cd90 100644 --- a/pyomo/contrib/parmest/tests/test_utils.py +++ b/pyomo/contrib/parmest/tests/test_utils.py @@ -48,12 +48,17 @@ def test_convert_param_to_var(self): columns=["sv", "caf", "ca", "cb", "cc", "cd"], ) - theta_names = ["k1", "k2", "k3"] + # make model + instance = reactor_design_model() + + # add caf, sv + instance.caf = data.iloc[0]['caf'] + instance.sv = data.iloc[0]['sv'] - instance = reactor_design_model(data.loc[0]) solver = pyo.SolverFactory("ipopt") solver.solve(instance) + theta_names = ['k1', 'k2', 'k3'] instance_vars = parmest.utils.convert_params_to_vars( instance, theta_names, fix_vars=True ) From 9d7e5c0b15e4758e3fe318995a990b04205cd226 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 30 Jan 2024 12:29:52 -0700 Subject: [PATCH 0411/3044] Save state: making Legacy wrapper more testable --- pyomo/contrib/solver/base.py | 103 +++++++++++++------ pyomo/contrib/solver/tests/unit/test_base.py | 1 + 2 files changed, 73 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index e0eb58924c1..98fa60b722f 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -343,32 +343,21 @@ def __enter__(self): def __exit__(self, t, v, traceback): """Exit statement - enables `with` statements.""" - def solve( + def _map_config( self, - model: _BlockData, - tee: bool = False, - load_solutions: bool = True, - logfile: Optional[str] = None, - solnfile: Optional[str] = None, - timelimit: Optional[float] = None, - report_timing: bool = False, - solver_io: Optional[str] = None, - suffixes: Optional[Sequence] = None, - options: Optional[Dict] = None, - keepfiles: bool = False, - symbolic_solver_labels: bool = False, - raise_exception_on_nonoptimal_result: bool = False, + tee, + load_solutions, + symbolic_solver_labels, + timelimit, + report_timing, + raise_exception_on_nonoptimal_result, + solver_io, + suffixes, + logfile, + keepfiles, + solnfile, ): - """ - Solve method: maps new solve method style to backwards compatible version. - - Returns - ------- - legacy_results - Legacy results object - - """ - original_config = self.config + """Map between legacy and new interface configuration options""" self.config = self.config() self.config.tee = tee self.config.load_solutions = load_solutions @@ -392,12 +381,9 @@ def solve( if 'filename' in self.config: filename = os.path.splitext(solnfile)[0] self.config.filename = filename - original_options = self.options - if options is not None: - self.options = options - - results: Results = super().solve(model) + def _map_results(self, model, results): + """Map between legacy and new Results objects""" legacy_results = LegacySolverResults() legacy_soln = LegacySolution() legacy_results.solver.status = legacy_solver_status_map[ @@ -408,7 +394,6 @@ def solve( ] legacy_soln.status = legacy_solution_status_map[results.solution_status] legacy_results.solver.termination_message = str(results.termination_condition) - obj = get_objective(model) if len(list(obj)) > 0: legacy_results.problem.sense = obj.sense @@ -426,12 +411,16 @@ def solve( legacy_soln.gap = abs(results.incumbent_objective - results.objective_bound) else: legacy_soln.gap = None + return legacy_results, legacy_soln + def _solution_handler( + self, load_solutions, model, results, legacy_results, legacy_soln + ): + """Method to handle the preferred action for the solution""" symbol_map = SymbolMap() symbol_map.default_labeler = NumericLabeler('x') model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) - delete_legacy_soln = True if load_solutions: if hasattr(model, 'dual') and model.dual.import_enabled(): @@ -454,6 +443,58 @@ def solve( legacy_results.solution.insert(legacy_soln) if delete_legacy_soln: legacy_results.solution.delete(0) + return legacy_results + + def solve( + self, + model: _BlockData, + tee: bool = False, + load_solutions: bool = True, + logfile: Optional[str] = None, + solnfile: Optional[str] = None, + timelimit: Optional[float] = None, + report_timing: bool = False, + solver_io: Optional[str] = None, + suffixes: Optional[Sequence] = None, + options: Optional[Dict] = None, + keepfiles: bool = False, + symbolic_solver_labels: bool = False, + raise_exception_on_nonoptimal_result: bool = False, + ): + """ + Solve method: maps new solve method style to backwards compatible version. + + Returns + ------- + legacy_results + Legacy results object + + """ + original_config = self.config + self._map_config( + tee, + load_solutions, + symbolic_solver_labels, + timelimit, + report_timing, + raise_exception_on_nonoptimal_result, + solver_io, + suffixes, + logfile, + keepfiles, + solnfile, + ) + + original_options = self.options + if options is not None: + self.options = options + + results: Results = super().solve(model) + legacy_results, legacy_soln = self._map_results(model, results) + + legacy_results = self._solution_handler( + load_solutions, model, results, legacy_results, legacy_soln + ) self.config = original_config self.options = original_options diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index dd94ef18fc3..2d158025903 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -166,5 +166,6 @@ def test_context_manager(self): self.assertEqual(self.instance.update_variables(None), None) self.assertEqual(self.instance.update_params(), None) + class TestLegacySolverWrapper(unittest.TestCase): pass From 909962426476eb9ee27b08deb4efb67d9bf2508a Mon Sep 17 00:00:00 2001 From: Sakshi <73687517+Sakshi21299@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:32:37 -0500 Subject: [PATCH 0412/3044] add a method to add an edge in the incidence graph interface --- pyomo/contrib/incidence_analysis/interface.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index e922551c6a4..177ca97a6b6 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -932,3 +932,58 @@ def plot(self, variables=None, constraints=None, title=None, show=True): fig.update_layout(title=dict(text=title)) if show: fig.show() + + def add_edge_to_graph(self, node0, node1): + """Adds an edge between node0 and node1 in the incidence graph + + Parameters + --------- + nodes0: VarData/ConstraintData + A node in the graph from the first bipartite set + (``bipartite=0``) + node1: VarData/ConstraintData + A node in the graph from the second bipartite set + (``bipartite=1``) + """ + if self._incidence_graph is None: + raise RuntimeError( + "Attempting to add edge in an incidence graph from cached " + "incidence graph,\nbut no incidence graph has been cached." + ) + + if node0 not in ComponentSet(self._variables) and node0 not in ComponentSet(self._constraints): + raise RuntimeError( + "%s is not a node in the incidence graph" % node0 + ) + + if node1 not in ComponentSet(self._variables) and node1 not in ComponentSet(self._constraints): + raise RuntimeError( + "%s is not a node in the incidence graph" % node1 + ) + + if node0 in ComponentSet(self._variables): + node0_idx = self._var_index_map[node0] + len(self._con_index_map) + if node1 in ComponentSet(self._variables): + raise RuntimeError( + "%s & %s are both variables. Cannot add an edge between two" + "variables.\nThe resulting graph won't be bipartite" + % (node0, node1) + ) + node1_idx = self._con_index_map[node1] + + if node0 in ComponentSet(self._constraints): + node0_idx = self._con_index_map[node0] + if node1 in ComponentSet(self._constraints): + raise RuntimeError( + "%s & %s are both constraints. Cannot add an edge between two" + "constraints.\nThe resulting graph won't be bipartite" + % (node0, node1) + ) + node1_idx = self._var_index_map[node1] + len(self._con_index_map) + + self._incidence_graph.add_edge(node0_idx, node1_idx) + + + + + From fe35b2727db40a2a51a3688168b20ccc1022753f Mon Sep 17 00:00:00 2001 From: Sakshi <73687517+Sakshi21299@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:33:22 -0500 Subject: [PATCH 0413/3044] add tests for the add edge method --- .../tests/test_interface.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 490ea94f63c..63bc74ee6dc 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1790,7 +1790,58 @@ def test_linear_only(self): self.assertEqual(len(matching), 2) self.assertIs(matching[m.eq2], m.x[2]) self.assertIs(matching[m.eq3], m.x[3]) + + def test_add_edge(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3, 4]) + m.eq1 = pyo.Constraint(expr=m.x[1] ** 2 + m.x[2] ** 2 + m.x[3] ** 2 == 1) + m.eq2 = pyo.Constraint(expr=m.x[2] + pyo.sqrt(m.x[1]) + pyo.exp(m.x[3]) == 1) + m.eq3 = pyo.Constraint(expr=m.x[3] + m.x[2] + m.x[4] == 1) + m.eq4 = pyo.Constraint(expr=m.x[1] + m.x[2]**2 == 5) + + # nodes: component + # 0 : eq1 + # 1 : eq2 + # 2 : eq3 + # 3 : eq4 + # 4 : x[1] + # 5 : x[2] + # 6 : x[3] + # 7 : x[4] + + igraph = IncidenceGraphInterface(m, linear_only=False) + n_edges_original = igraph.n_edges + + #Test if there already exists an edge between two nodes, nothing is added + igraph.add_edge_to_graph(m.eq3, m.x[4]) + n_edges_new = igraph.n_edges + self.assertEqual(n_edges_original, n_edges_new) + + igraph.add_edge_to_graph(m.x[1], m.eq3) + n_edges_new = igraph.n_edges + self.assertEqual(set(igraph._incidence_graph[2]), {6, 5, 7, 4}) + self.assertEqual(n_edges_original +1, n_edges_new) + + igraph.add_edge_to_graph(m.eq4, m.x[4]) + n_edges_new = igraph.n_edges + self.assertEqual(set(igraph._incidence_graph[3]), {4, 5, 7}) + self.assertEqual(n_edges_original + 2, n_edges_new) + + def test_add_edge_linear_igraph(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3, 4]) + m.eq1 = pyo.Constraint(expr=m.x[1] + m.x[3] == 1) + m.eq2 = pyo.Constraint(expr=m.x[2] + pyo.sqrt(m.x[1]) + pyo.exp(m.x[3]) == 1) + m.eq3 = pyo.Constraint(expr=m.x[4]**2 + m.x[1] ** 3 + m.x[2] == 1) + + #Make sure error is raised when a variable is not in the igraph + igraph = IncidenceGraphInterface(m, linear_only=True) + n_edges_original = igraph.n_edges + msg = "is not a node in the incidence graph" + with self.assertRaisesRegex(RuntimeError, msg): + igraph.add_edge_to_graph(m.x[4], m.eq2) + @unittest.skipUnless(networkx_available, "networkx is not available.") class TestIndexedBlock(unittest.TestCase): From 8f7f95b2bcacb5e04910a81fe46643f9d5329162 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 30 Jan 2024 15:49:41 -0700 Subject: [PATCH 0414/3044] Adding tests for the pyomo-to-docplex map on the walker --- pyomo/contrib/cp/tests/test_docplex_walker.py | 224 ++++++++++++++++-- 1 file changed, 210 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index fc475190ade..73b4fd8e00c 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -107,6 +107,10 @@ def test_write_addition(self): expr[1].equals(cpx_x + cp.start_of(cpx_i) + cp.length_of(cpx_i2)) ) + self.assertIs(visitor.pyomo_to_docplex[m.x], cpx_x) + self.assertIs(visitor.pyomo_to_docplex[m.i], cpx_i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], cpx_i2) + def test_write_subtraction(self): m = self.get_model() m.a.domain = Binary @@ -122,6 +126,9 @@ def test_write_subtraction(self): self.assertTrue(expr[1].equals(x + (-1 * a1))) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + def test_write_product(self): m = self.get_model() m.a.domain = PositiveIntegers @@ -137,6 +144,9 @@ def test_write_product(self): self.assertTrue(expr[1].equals(x * (a1 + 1))) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + def test_write_floating_point_division(self): m = self.get_model() m.a.domain = NonNegativeIntegers @@ -152,6 +162,9 @@ def test_write_floating_point_division(self): self.assertTrue(expr[1].equals(x / (a1 + 1))) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + def test_write_power_expression(self): m = self.get_model() m.c = Constraint(expr=m.x**2 <= 3) @@ -163,6 +176,8 @@ def test_write_power_expression(self): # .equals checks the equality of two expressions in docplex. self.assertTrue(expr[1].equals(cpx_x**2)) + self.assertIs(visitor.pyomo_to_docplex[m.x], cpx_x) + def test_write_absolute_value_expression(self): m = self.get_model() m.a.domain = NegativeIntegers @@ -176,6 +191,8 @@ def test_write_absolute_value_expression(self): self.assertTrue(expr[1].equals(cp.abs(a1) + 1)) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + def test_write_min_expression(self): m = self.get_model() m.a.domain = NonPositiveIntegers @@ -187,6 +204,7 @@ def test_write_min_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue(expr[1].equals(cp.min(a[i] for i in m.I))) @@ -201,6 +219,7 @@ def test_write_max_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue(expr[1].equals(cp.max(a[i] for i in m.I))) @@ -235,6 +254,14 @@ def test_write_logical_and(self): self.assertTrue(expr[1].equals(cp.logical_and(b, b2b))) + # ESJ: This is ludicrous, but I don't know how to get the args of a CP + # expression, so testing that we were correct in the pyomo to docplex + # map by checking that we can build an expression that is the same as b + # (because b is actually "b == 1" since docplex doesn't believe in + # Booleans) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertTrue(b2b.equals(visitor.pyomo_to_docplex[m.b2['b']] == 1)) + def test_write_logical_or(self): m = self.get_model() m.c = LogicalConstraint(expr=m.b.lor(m.i.is_present)) @@ -248,6 +275,9 @@ def test_write_logical_or(self): self.assertTrue(expr[1].equals(cp.logical_or(b, cp.presence_of(i)))) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + def test_write_xor(self): m = self.get_model() m.c = LogicalConstraint(expr=m.b.xor(m.i2[2].start_time >= 5)) @@ -265,6 +295,9 @@ def test_write_xor(self): expr[1].equals(cp.count([b, cp.less_or_equal(5, cp.start_of(i22))], 1) == 1) ) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + def test_write_logical_not(self): m = self.get_model() m.c = LogicalConstraint(expr=~m.b2['a']) @@ -276,6 +309,8 @@ def test_write_logical_not(self): self.assertTrue(expr[1].equals(cp.logical_not(b2a))) + self.assertTrue(b2a.equals(visitor.pyomo_to_docplex[m.b2['a']] == 1)) + def test_equivalence(self): m = self.get_model() m.c = LogicalConstraint(expr=equivalent(~m.b2['a'], m.b)) @@ -289,18 +324,8 @@ def test_equivalence(self): self.assertTrue(expr[1].equals(cp.equal(cp.logical_not(b2a), b))) - def test_implication(self): - m = self.get_model() - m.c = LogicalConstraint(expr=m.b2['a'].implies(~m.b)) - visitor = self.get_visitor() - expr = visitor.walk_expression((m.c.expr, m.c, 0)) - - self.assertIn(id(m.b), visitor.var_map) - self.assertIn(id(m.b2['a']), visitor.var_map) - b = visitor.var_map[id(m.b)] - b2a = visitor.var_map[id(m.b2['a'])] - - self.assertTrue(expr[1].equals(cp.if_then(b2a, cp.logical_not(b)))) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertTrue(b2a.equals(visitor.pyomo_to_docplex[m.b2['a']] == 1)) def test_equality(self): m = self.get_model() @@ -317,6 +342,9 @@ def test_equality(self): self.assertTrue(expr[1].equals(cp.if_then(b, cp.equal(a3, 4)))) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertIs(visitor.pyomo_to_docplex[m.a[3]], a3) + def test_inequality(self): m = self.get_model() m.a.domain = Integers @@ -334,6 +362,10 @@ def test_inequality(self): self.assertTrue(expr[1].equals(cp.if_then(b, cp.less_or_equal(a4, a3)))) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertIs(visitor.pyomo_to_docplex[m.a[3]], a3) + self.assertIs(visitor.pyomo_to_docplex[m.a[4]], a4) + def test_ranged_inequality(self): m = self.get_model() m.a.domain = Integers @@ -364,6 +396,10 @@ def test_not_equal(self): self.assertTrue(expr[1].equals(cp.if_then(b, a3 != a4))) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + self.assertIs(visitor.pyomo_to_docplex[m.a[3]], a3) + self.assertIs(visitor.pyomo_to_docplex[m.a[4]], a4) + def test_exactly_expression(self): m = self.get_model() m.a.domain = Integers @@ -376,6 +412,7 @@ def test_exactly_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue( expr[1].equals(cp.equal(cp.count([a[i] == 4 for i in m.I], 1), 3)) @@ -393,6 +430,7 @@ def test_atleast_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue( expr[1].equals( @@ -412,6 +450,7 @@ def test_atmost_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue( expr[1].equals(cp.less_or_equal(cp.count([a[i] == 4 for i in m.I], 1), 3)) @@ -430,6 +469,7 @@ def test_all_diff_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue(expr[1].equals(cp.all_diff(a[i] for i in m.I))) @@ -449,6 +489,9 @@ def test_Boolean_args_in_all_diff_expression(self): self.assertTrue(expr[1].equals(cp.all_diff(a0 == 13, b))) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a0) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_count_if_expression(self): m = self.get_model() m.a.domain = Integers @@ -462,6 +505,7 @@ def test_count_if_expression(self): for i in m.I: self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) self.assertTrue(expr[1].equals(cp.count((a[i] == i for i in m.I), 1) == 5)) @@ -480,6 +524,9 @@ def test_interval_var_is_present(self): self.assertTrue(expr[1].equals(cp.if_then(cp.presence_of(i), a1 == 5))) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + def test_interval_var_is_present_indirection(self): m = self.get_model() m.a.domain = Integers @@ -513,6 +560,11 @@ def test_interval_var_is_present_indirection(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a1) + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + def test_is_present_indirection_and_length(self): m = self.get_model() m.y = Var(domain=Integers, bounds=[1, 2]) @@ -547,6 +599,10 @@ def test_is_present_indirection_and_length(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + def test_handle_getattr_lor(self): m = self.get_model() m.y = Var(domain=Integers, bounds=(1, 2)) @@ -578,6 +634,11 @@ def test_handle_getattr_lor(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_handle_getattr_xor(self): m = self.get_model() m.y = Var(domain=Integers, bounds=(1, 2)) @@ -616,6 +677,11 @@ def test_handle_getattr_xor(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_handle_getattr_equivalent_to(self): m = self.get_model() m.y = Var(domain=Integers, bounds=(1, 2)) @@ -647,6 +713,11 @@ def test_handle_getattr_equivalent_to(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_logical_or_on_indirection(self): m = ConcreteModel() m.b = BooleanVar([2, 3, 4, 5]) @@ -676,6 +747,11 @@ def test_logical_or_on_indirection(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertTrue(b3.equals(visitor.pyomo_to_docplex[m.b[3]] == 1)) + self.assertTrue(b4.equals(visitor.pyomo_to_docplex[m.b[4]] == 1)) + self.assertTrue(b5.equals(visitor.pyomo_to_docplex[m.b[5]] == 1)) + def test_logical_xor_on_indirection(self): m = ConcreteModel() m.b = BooleanVar([2, 3, 4, 5]) @@ -710,6 +786,10 @@ def test_logical_xor_on_indirection(self): ) ) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertTrue(b3.equals(visitor.pyomo_to_docplex[m.b[3]] == 1)) + self.assertTrue(b5.equals(visitor.pyomo_to_docplex[m.b[5]] == 1)) + def test_using_precedence_expr_as_boolean_expr(self): m = self.get_model() e = m.b.implies(m.i2[2].start_time.before(m.i2[1].start_time)) @@ -729,6 +809,10 @@ def test_using_precedence_expr_as_boolean_expr(self): expr[1].equals(cp.if_then(b, cp.start_of(i22) + 0 <= cp.start_of(i21))) ) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_using_precedence_expr_as_boolean_expr_positive_delay(self): m = self.get_model() e = m.b.implies(m.i2[2].start_time.before(m.i2[1].start_time, delay=4)) @@ -748,6 +832,10 @@ def test_using_precedence_expr_as_boolean_expr_positive_delay(self): expr[1].equals(cp.if_then(b, cp.start_of(i22) + 4 <= cp.start_of(i21))) ) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + def test_using_precedence_expr_as_boolean_expr_negative_delay(self): m = self.get_model() e = m.b.implies(m.i2[2].start_time.at(m.i2[1].start_time, delay=-3)) @@ -767,6 +855,10 @@ def test_using_precedence_expr_as_boolean_expr_negative_delay(self): expr[1].equals(cp.if_then(b, cp.start_of(i22) + (-3) == cp.start_of(i21))) ) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_IntervalVars(CommonTest): @@ -780,6 +872,7 @@ def test_interval_var_fixed_presences_correct(self): i = visitor.var_map[id(m.i)] # Check that docplex knows it's optional self.assertTrue(i.is_optional()) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) # Now fix it to absent m.i.is_present.fix(False) @@ -790,8 +883,10 @@ def test_interval_var_fixed_presences_correct(self): self.assertIn(id(m.i2[1]), visitor.var_map) i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertIn(id(m.i), visitor.var_map) i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) # Check that we passed on the presence info to docplex self.assertTrue(i.is_absent()) @@ -810,6 +905,7 @@ def test_interval_var_fixed_length(self): self.assertIn(id(m.i), visitor.var_map) i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue(i.is_optional()) self.assertEqual(i.get_length(), (4, 4)) @@ -827,6 +923,7 @@ def test_interval_var_fixed_start_and_end(self): self.assertIn(id(m.i), visitor.var_map) i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertFalse(i.is_optional()) self.assertEqual(i.get_start(), (3, 3)) @@ -844,10 +941,14 @@ def get_model(self): def check_scalar_sequence_var(self, m, visitor): self.assertIn(id(m.seq), visitor.var_map) seq = visitor.var_map[id(m.seq)] + self.assertIs(visitor.pyomo_to_docplex[m.seq], seq) i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) ivs = seq.get_interval_variables() self.assertEqual(len(ivs), 3) @@ -914,6 +1015,8 @@ def test_start_before_start(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.start_before_start(i, i21, 0))) @@ -928,6 +1031,8 @@ def test_start_before_end(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.start_before_end(i, i21, 3))) @@ -942,6 +1047,8 @@ def test_end_before_start(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.end_before_start(i, i21, -2))) @@ -956,6 +1063,8 @@ def test_end_before_end(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.end_before_end(i, i21, 6))) @@ -970,6 +1079,8 @@ def test_start_at_start(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.start_at_start(i, i21, 0))) @@ -984,6 +1095,8 @@ def test_start_at_end(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.start_at_end(i, i21, 3))) @@ -998,6 +1111,8 @@ def test_end_at_start(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.end_at_start(i, i21, -2))) @@ -1012,6 +1127,8 @@ def test_end_at_end(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) self.assertTrue(expr[1].equals(cp.end_at_end(i, i21, 6))) @@ -1036,6 +1153,10 @@ def test_indirection_before_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -1062,6 +1183,10 @@ def test_indirection_after_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -1089,6 +1214,10 @@ def test_indirection_at_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -1116,6 +1245,10 @@ def test_before_indirection_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -1141,6 +1274,10 @@ def test_after_indirection_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -1166,6 +1303,10 @@ def test_at_indirection_constraint(self): i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals( @@ -1200,6 +1341,13 @@ def test_double_indirection_before_constraint(self): i33 = visitor.var_map[id(m.i3[1, 3])] i34 = visitor.var_map[id(m.i3[1, 4])] i35 = visitor.var_map[id(m.i3[1, 5])] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 3]], i33) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 4]], i34) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 5]], i35) self.assertTrue( expr[1].equals( @@ -1237,6 +1385,13 @@ def test_double_indirection_after_constraint(self): i33 = visitor.var_map[id(m.i3[1, 3])] i34 = visitor.var_map[id(m.i3[1, 4])] i35 = visitor.var_map[id(m.i3[1, 5])] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 3]], i33) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 4]], i34) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 5]], i35) self.assertTrue( expr[1].equals( @@ -1272,6 +1427,13 @@ def test_double_indirection_at_constraint(self): i33 = visitor.var_map[id(m.i3[1, 3])] i34 = visitor.var_map[id(m.i3[1, 4])] i35 = visitor.var_map[id(m.i3[1, 5])] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 3]], i33) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 4]], i34) + self.assertIs(visitor.pyomo_to_docplex[m.i3[1, 5]], i35) self.assertTrue( expr[1].equals( @@ -1319,6 +1481,8 @@ def param_rule(m, i): self.assertIn(id(m.a), visitor.var_map) x = visitor.var_map[id(m.x)] a = visitor.var_map[id(m.a)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIs(visitor.pyomo_to_docplex[m.a], a) self.assertTrue(expr[1].equals(cp.element([2, 4, 6], 0 + 1 * (x - 1) // 2) / a)) @@ -1346,6 +1510,8 @@ def test_spans(self): self.assertIn(id(m.whole_enchilada), visitor.var_map) whole_enchilada = visitor.var_map[id(m.whole_enchilada)] + self.assertIs(visitor.pyomo_to_docplex[m.whole_enchilada], whole_enchilada) + iv = {} for i in [1, 2, 3]: self.assertIn(id(m.iv[i]), visitor.var_map) @@ -1363,6 +1529,8 @@ def test_alternative(self): self.assertIn(id(m.whole_enchilada), visitor.var_map) whole_enchilada = visitor.var_map[id(m.whole_enchilada)] + self.assertIs(visitor.pyomo_to_docplex[m.whole_enchilada], whole_enchilada) + iv = {} for i in [1, 2, 3]: self.assertIn(id(m.iv[i]), visitor.var_map) @@ -1395,6 +1563,9 @@ def test_always_in(self): i = visitor.var_map[id(m.i)] i21 = visitor.var_map[id(m.i2[1])] i22 = visitor.var_map[id(m.i2[2])] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) + self.assertIs(visitor.pyomo_to_docplex[m.i2[1]], i21) + self.assertIs(visitor.pyomo_to_docplex[m.i2[2]], i22) self.assertTrue( expr[1].equals( @@ -1422,6 +1593,7 @@ def test_always_in_single_pulse(self): self.assertIn(id(m.i), visitor.var_map) i = visitor.var_map[id(m.i)] + self.assertIs(visitor.pyomo_to_docplex[m.i], i) self.assertTrue( expr[1].equals(cp.always_in(cp.pulse(i, 3), interval=(0, 10), min=0, max=3)) @@ -1440,6 +1612,7 @@ def test_named_expression(self): self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertTrue(expr[1].equals(x**2 + 7)) @@ -1453,6 +1626,7 @@ def test_repeated_named_expression(self): self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertTrue(expr[1].equals(x**2 + 7 + (-1) * (8 * (x**2 + 7)))) @@ -1483,6 +1657,7 @@ def test_fixed_integer_var(self): self.assertIn(id(m.a[2]), visitor.var_map) a2 = visitor.var_map[id(m.a[2])] + self.assertIs(visitor.pyomo_to_docplex[m.a[2]], a2) self.assertTrue(expr[1].equals(3 + a2)) @@ -1497,6 +1672,7 @@ def test_fixed_boolean_var(self): self.assertIn(id(m.b2['b']), visitor.var_map) b2b = visitor.var_map[id(m.b2['b'])] + self.assertTrue(b2b.equals(visitor.pyomo_to_docplex[m.b2['b']] == 1)) self.assertTrue(expr[1].equals(cp.logical_or(False, cp.logical_and(True, b2b)))) @@ -1510,13 +1686,16 @@ def test_indirection_single_index(self): self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) a = [] # only need indices 6, 7, and 8 from a, since that's what x is capable # of selecting. for idx in [6, 7, 8]: v = m.a[idx] self.assertIn(id(v), visitor.var_map) - a.append(visitor.var_map[id(v)]) + cpx_v = visitor.var_map[id(v)] + self.assertIs(visitor.pyomo_to_docplex[v], cpx_v) + a.append(cpx_v) # since x is between 6 and 8, we subtract 6 from it for it to be the # right index self.assertTrue(expr[1].equals(cp.element(a, 0 + 1 * (x - 6) // 1))) @@ -1534,8 +1713,10 @@ def test_indirection_multi_index_second_constant(self): for i in [6, 7, 8]: self.assertIn(id(m.z[i, 3]), visitor.var_map) z[i, 3] = visitor.var_map[id(m.z[i, 3])] + self.assertIs(visitor.pyomo_to_docplex[m.z[i, 3]], z[i, 3]) self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertTrue( expr[1].equals( @@ -1556,8 +1737,11 @@ def test_indirection_multi_index_first_constant(self): for i in [6, 7, 8]: self.assertIn(id(m.z[3, i]), visitor.var_map) z[3, i] = visitor.var_map[id(m.z[3, i])] + self.assertIs(visitor.pyomo_to_docplex[m.z[3, i]], z[3, i]) + self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertTrue( expr[1].equals( @@ -1579,8 +1763,11 @@ def test_indirection_multi_index_neither_constant_same_var(self): for j in [6, 7, 8]: self.assertIn(id(m.z[i, j]), visitor.var_map) z[i, j] = visitor.var_map[id(m.z[i, j])] + self.assertIs(visitor.pyomo_to_docplex[m.z[i, j]], z[i, j]) + self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertTrue( expr[1].equals( @@ -1604,12 +1791,17 @@ def test_indirection_multi_index_neither_constant_diff_vars(self): z = {} for i in [6, 7, 8]: for j in [1, 3, 5]: - self.assertIn(id(m.z[i, 3]), visitor.var_map) + self.assertIn(id(m.z[i, j]), visitor.var_map) z[i, j] = visitor.var_map[id(m.z[i, j])] + self.assertIs(visitor.pyomo_to_docplex[m.z[i, j]], z[i, j]) + self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) + self.assertIn(id(m.y), visitor.var_map) y = visitor.var_map[id(m.y)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) self.assertTrue( expr[1].equals( @@ -1634,10 +1826,14 @@ def test_indirection_expression_index(self): for i in range(1, 8): self.assertIn(id(m.a[i]), visitor.var_map) a[i] = visitor.var_map[id(m.a[i])] + self.assertIs(visitor.pyomo_to_docplex[m.a[i]], a[i]) + self.assertIn(id(m.x), visitor.var_map) x = visitor.var_map[id(m.x)] + self.assertIs(visitor.pyomo_to_docplex[m.x], x) self.assertIn(id(m.y), visitor.var_map) y = visitor.var_map[id(m.y)] + self.assertIs(visitor.pyomo_to_docplex[m.y], y) self.assertTrue( expr[1].equals( From 1e103090bb7724d5d0b7486e7f59a20f44c172d2 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 30 Jan 2024 15:52:00 -0700 Subject: [PATCH 0415/3044] NFC: black --- pyomo/contrib/cp/interval_var.py | 3 +-- pyomo/contrib/cp/repn/docplex_writer.py | 8 ++---- .../cp/scheduling_expr/scheduling_logic.py | 10 +++---- pyomo/contrib/cp/tests/test_docplex_walker.py | 26 +++++++++++-------- .../cp/tests/test_sequence_expressions.py | 14 ++++++---- 5 files changed, 32 insertions(+), 29 deletions(-) diff --git a/pyomo/contrib/cp/interval_var.py b/pyomo/contrib/cp/interval_var.py index fb88ab14832..0e355d1847d 100644 --- a/pyomo/contrib/cp/interval_var.py +++ b/pyomo/contrib/cp/interval_var.py @@ -168,8 +168,7 @@ def __init__( optional=False, name=None, doc=None - ): - ... + ): ... def __init__(self, *args, **kwargs): _start_arg = kwargs.pop('start', None) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 1f6bcc347e7..3440b522e60 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -996,15 +996,11 @@ def _handle_predecessor_to_expression_node( return _GENERAL, cp.previous(seq_var[1], before_var[1], after_var[1]) -def _handle_span_expression_node( - visitor, node, *args -): +def _handle_span_expression_node(visitor, node, *args): return _GENERAL, cp.span(args[0][1], [arg[1] for arg in args[1:]]) -def _handle_alternative_expression_node( - visitor, node, *args -): +def _handle_alternative_expression_node(visitor, node, *args): return _GENERAL, cp.alternative(args[0][1], [arg[1] for arg in args[1:]]) diff --git a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py index a1f891a769f..fc9cefebf4d 100644 --- a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py +++ b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py @@ -22,6 +22,7 @@ class SpanExpression(NaryBooleanExpression): args: args (tuple): Child nodes, of type IntervalVar """ + def _to_string(self, values, verbose, smap): return "%s.spans(%s)" % (values[0], ", ".join(values[1:])) @@ -30,19 +31,18 @@ class AlternativeExpression(NaryBooleanExpression): """ TODO/ """ + def _to_string(self, values, verbose, smap): return "alternative(%s, [%s])" % (values[0], ", ".join(values[1:])) - + def spans(*args): - """Creates a new SpanExpression - """ + """Creates a new SpanExpression""" return SpanExpression(list(_flattened(args))) def alternative(*args): - """Creates a new AlternativeExpression - """ + """Creates a new AlternativeExpression""" return AlternativeExpression(list(_flattened(args))) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 73b4fd8e00c..9d027296654 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -19,7 +19,7 @@ last_in_sequence, before_in_sequence, predecessor_to, - alternative + alternative, ) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, @@ -1491,12 +1491,16 @@ def param_rule(m, i): class TestCPExpressionWalker_HierarchicalScheduling(CommonTest): def get_model(self): m = ConcreteModel() + def start_rule(m, i): - return 2*i + return 2 * i + def length_rule(m, i): return i - m.iv = IntervalVar([1, 2, 3], start=start_rule, length=length_rule, - optional=True) + + m.iv = IntervalVar( + [1, 2, 3], start=start_rule, length=length_rule, optional=True + ) m.whole_enchilada = IntervalVar() return m @@ -1517,8 +1521,9 @@ def test_spans(self): self.assertIn(id(m.iv[i]), visitor.var_map) iv[i] = visitor.var_map[id(m.iv[i])] - self.assertTrue(expr[1].equals(cp.span(whole_enchilada, [iv[i] for i in - [1, 2, 3]]))) + self.assertTrue( + expr[1].equals(cp.span(whole_enchilada, [iv[i] for i in [1, 2, 3]])) + ) def test_alternative(self): m = self.get_model() @@ -1526,7 +1531,7 @@ def test_alternative(self): visitor = self.get_visitor() expr = visitor.walk_expression((e, e, 0)) - + self.assertIn(id(m.whole_enchilada), visitor.var_map) whole_enchilada = visitor.var_map[id(m.whole_enchilada)] self.assertIs(visitor.pyomo_to_docplex[m.whole_enchilada], whole_enchilada) @@ -1536,10 +1541,9 @@ def test_alternative(self): self.assertIn(id(m.iv[i]), visitor.var_map) iv[i] = visitor.var_map[id(m.iv[i])] - self.assertTrue(expr[1].equals(cp.alternative(whole_enchilada, [iv[i] - for i in - [1, 2, - 3]]))) + self.assertTrue( + expr[1].equals(cp.alternative(whole_enchilada, [iv[i] for i in [1, 2, 3]])) + ) @unittest.skipIf(not docplex_available, "docplex is not available") diff --git a/pyomo/contrib/cp/tests/test_sequence_expressions.py b/pyomo/contrib/cp/tests/test_sequence_expressions.py index 93a283c43d1..218a4c0e1a0 100644 --- a/pyomo/contrib/cp/tests/test_sequence_expressions.py +++ b/pyomo/contrib/cp/tests/test_sequence_expressions.py @@ -16,7 +16,7 @@ AlternativeExpression, SpanExpression, alternative, - spans + spans, ) from pyomo.contrib.cp.scheduling_expr.sequence_expressions import ( NoOverlapExpression, @@ -113,12 +113,16 @@ def test_predecessor_in_sequence(self): class TestHierarchicalSchedulingExpressions(unittest.TestCase): def make_model(self): m = ConcreteModel() + def start_rule(m, i): - return 2*i + return 2 * i + def length_rule(m, i): return i - m.iv = IntervalVar([1, 2, 3], start=start_rule, length=length_rule, - optional=True) + + m.iv = IntervalVar( + [1, 2, 3], start=start_rule, length=length_rule, optional=True + ) m.whole_enchilada = IntervalVar() return m @@ -137,7 +141,7 @@ def test_spans(self): m = self.make_model() e = spans(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) self.check_span_expression(m, e) - + def test_spans_method(self): m = self.make_model() e = m.whole_enchilada.spans(m.iv[i] for i in [1, 2, 3]) From bf0a54d5b8e97326caacad7363e35dd7a83ec4ff Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 31 Jan 2024 10:52:46 -0700 Subject: [PATCH 0416/3044] Adding function for debugging infeasible CPs without having to mess with the docplex model --- pyomo/contrib/cp/debugging.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 pyomo/contrib/cp/debugging.py diff --git a/pyomo/contrib/cp/debugging.py b/pyomo/contrib/cp/debugging.py new file mode 100644 index 00000000000..41c4d208de6 --- /dev/null +++ b/pyomo/contrib/cp/debugging.py @@ -0,0 +1,29 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.opt import WriterFactory + + +def write_conflict_set(m, filename): + """ + For debugging infeasible CPs: writes the conflict set found by CP optimizer + to a file with the specified filename. + + Args: + m: Pyomo CP model + filename: string filename + """ + + cpx_mod, var_map = WriterFactory('docplex_model').write( + m, symbolic_solver_labels=True + ) + conflict = cpx_mod.refine_conflict() + conflict.write(filename) From bac8bda15dea59856c2f03e4ffa33fb52afab4b9 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 31 Jan 2024 12:06:33 -0700 Subject: [PATCH 0417/3044] Correcting precedence and some other mistakes John caught --- pyomo/core/expr/logical_expr.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pyomo/core/expr/logical_expr.py b/pyomo/core/expr/logical_expr.py index 31082293a71..aabef99597d 100644 --- a/pyomo/core/expr/logical_expr.py +++ b/pyomo/core/expr/logical_expr.py @@ -539,7 +539,7 @@ class AllDifferentExpression(NaryBooleanExpression): __slots__ = () - PRECEDENCE = 9 # TODO: maybe? + PRECEDENCE = None def getname(self, *arg, **kwd): return 'all_different' @@ -548,9 +548,13 @@ def _to_string(self, values, verbose, smap): return "all_different(%s)" % (", ".join(values)) def _apply_operation(self, result): - for val1, val2 in combinations(result, 2): - if val1 == val2: + last = None + # we know these are integer-valued, so we can just sort them an make + # sure that no adjacent pairs have the same value. + for val in sorted(result): + if last == val: return False + last = val return True @@ -561,13 +565,7 @@ class CountIfExpression(NumericExpression): """ __slots__ = () - PRECEDENCE = 10 # TODO: maybe? - - def __init__(self, args): - # require a list, a la SumExpression - if args.__class__ is not list: - args = list(args) - self._args_ = args + PRECEDENCE = None # NumericExpression assumes binary operator, so we have to override. def nargs(self): @@ -580,7 +578,7 @@ def _to_string(self, values, verbose, smap): return "count_if(%s)" % (", ".join(values)) def _apply_operation(self, result): - return sum(value(r) for r in result) + return sum(r for r in result) special_boolean_atom_types = {ExactlyExpression, AtMostExpression, AtLeastExpression} From 272ea35a03d305ccd4bffd6b5aa428a95b51d2c6 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 31 Jan 2024 12:56:11 -0700 Subject: [PATCH 0418/3044] Checking argument types for logical expressions --- pyomo/core/expr/logical_expr.py | 59 ++++++++++++++++--- .../tests/unit/test_logical_expr_expanded.py | 10 +++- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/pyomo/core/expr/logical_expr.py b/pyomo/core/expr/logical_expr.py index aabef99597d..d345e0f64ae 100644 --- a/pyomo/core/expr/logical_expr.py +++ b/pyomo/core/expr/logical_expr.py @@ -183,12 +183,57 @@ def _flattened(args): yield arg +def _flattened_boolean_args(args): + """Flatten any potentially indexed arguments and check that they are + Boolean-valued.""" + for arg in args: + if arg.__class__ in native_types: + myiter = (arg,) + elif isinstance(arg, (types.GeneratorType, list)): + myiter = arg + elif arg.is_indexed(): + myiter = arg.values() + else: + myiter = (arg,) + for _argdata in myiter: + if _argdata.__class__ in native_logical_types: + yield _argdata + elif hasattr(_argdata, 'is_logical_type') and _argdata.is_logical_type(): + yield _argdata + else: + raise ValueError( + "Non-Boolean-valued argument '%s' encountered when constructing " + "expression of Boolean arguments" % arg) + + +def _flattened_numeric_args(args): + """Flatten any potentially indexed arguments and check that they are + numeric.""" + for arg in args: + if arg.__class__ in native_types: + myiter = (arg,) + elif isinstance(arg, (types.GeneratorType, list)): + myiter = arg + elif arg.is_indexed(): + myiter = arg.values() + else: + myiter = (arg,) + for _argdata in myiter: + if _argdata.__class__ in native_numeric_types: + yield _argdata + elif hasattr(_argdata, 'is_numeric_type') and _argdata.is_numeric_type(): + yield _argdata + else: + raise ValueError( + "Non-numeric argument '%s' encountered when constructing " + "expression with numeric arguments" % arg) + def land(*args): """ Construct an AndExpression between passed arguments. """ result = AndExpression([]) - for argdata in _flattened(args): + for argdata in _flattened_boolean_args(args): result = result.add(argdata) return result @@ -198,7 +243,7 @@ def lor(*args): Construct an OrExpression between passed arguments. """ result = OrExpression([]) - for argdata in _flattened(args): + for argdata in _flattened_boolean_args(args): result = result.add(argdata) return result @@ -211,7 +256,7 @@ def exactly(n, *args): Usage: exactly(2, m.Y1, m.Y2, m.Y3, ...) """ - result = ExactlyExpression([n] + list(_flattened(args))) + result = ExactlyExpression([n] + list(_flattened_boolean_args(args))) return result @@ -223,7 +268,7 @@ def atmost(n, *args): Usage: atmost(2, m.Y1, m.Y2, m.Y3, ...) """ - result = AtMostExpression([n] + list(_flattened(args))) + result = AtMostExpression([n] + list(_flattened_boolean_args(args))) return result @@ -235,7 +280,7 @@ def atleast(n, *args): Usage: atleast(2, m.Y1, m.Y2, m.Y3, ...) """ - result = AtLeastExpression([n] + list(_flattened(args))) + result = AtLeastExpression([n] + list(_flattened_boolean_args(args))) return result @@ -246,7 +291,7 @@ def all_different(*args): Usage: all_different(m.X1, m.X2, ...) """ - return AllDifferentExpression(list(_flattened(args))) + return AllDifferentExpression(list(_flattened_numeric_args(args))) def count_if(*args): @@ -256,7 +301,7 @@ def count_if(*args): Usage: count_if(m.Y1, m.Y2, ...) """ - return CountIfExpression(list(_flattened(args))) + return CountIfExpression(list(_flattened_boolean_args(args))) class UnaryBooleanExpression(BooleanExpression): diff --git a/pyomo/core/tests/unit/test_logical_expr_expanded.py b/pyomo/core/tests/unit/test_logical_expr_expanded.py index ca2b64957ef..0e5bb4da445 100644 --- a/pyomo/core/tests/unit/test_logical_expr_expanded.py +++ b/pyomo/core/tests/unit/test_logical_expr_expanded.py @@ -280,6 +280,8 @@ def test_to_string(self): m.Y2 = BooleanVar() m.Y3 = BooleanVar() m.Y4 = BooleanVar() + m.int1 = Var(domain=Integers) + m.int2 = Var(domain=Integers) self.assertEqual(str(land(m.Y1, m.Y2, m.Y3)), "Y1 ∧ Y2 ∧ Y3") self.assertEqual(str(lor(m.Y1, m.Y2, m.Y3)), "Y1 ∨ Y2 ∨ Y3") @@ -289,7 +291,8 @@ def test_to_string(self): self.assertEqual(str(atleast(1, m.Y1, m.Y2)), "atleast(1: [Y1, Y2])") self.assertEqual(str(atmost(1, m.Y1, m.Y2)), "atmost(1: [Y1, Y2])") self.assertEqual(str(exactly(1, m.Y1, m.Y2)), "exactly(1: [Y1, Y2])") - self.assertEqual(str(all_different(m.Y1, m.Y2)), "all_different(Y1, Y2)") + self.assertEqual(str(all_different(m.int1, m.int2)), + "all_different(int1, int2)") self.assertEqual(str(count_if(m.Y1, m.Y2)), "count_if(Y1, Y2)") # Precedence checks @@ -308,12 +311,15 @@ def test_node_types(self): m.Y1 = BooleanVar() m.Y2 = BooleanVar() m.Y3 = BooleanVar() + m.int1 = Var(domain=Integers) + m.int2 = Var(domain=Integers) + m.int3 = Var(domain=Integers) self.assertFalse(m.Y1.is_expression_type()) self.assertTrue(lnot(m.Y1).is_expression_type()) self.assertTrue(equivalent(m.Y1, m.Y2).is_expression_type()) self.assertTrue(atmost(1, [m.Y1, m.Y2, m.Y3]).is_expression_type()) - self.assertTrue(all_different(m.Y1, m.Y2, m.Y3).is_expression_type()) + self.assertTrue(all_different(m.int1, m.int2, m.int3).is_expression_type()) self.assertTrue(count_if(m.Y1, m.Y2, m.Y3).is_expression_type()) def test_numeric_invalid(self): From e0edbe792caa3d7041abf496793b2c89fcd13e51 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 31 Jan 2024 12:57:24 -0700 Subject: [PATCH 0419/3044] Black disagrees --- pyomo/core/expr/logical_expr.py | 11 +++++++---- pyomo/core/tests/unit/test_logical_expr_expanded.py | 5 +++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pyomo/core/expr/logical_expr.py b/pyomo/core/expr/logical_expr.py index d345e0f64ae..17f4a4dd564 100644 --- a/pyomo/core/expr/logical_expr.py +++ b/pyomo/core/expr/logical_expr.py @@ -184,7 +184,7 @@ def _flattened(args): def _flattened_boolean_args(args): - """Flatten any potentially indexed arguments and check that they are + """Flatten any potentially indexed arguments and check that they are Boolean-valued.""" for arg in args: if arg.__class__ in native_types: @@ -203,11 +203,12 @@ def _flattened_boolean_args(args): else: raise ValueError( "Non-Boolean-valued argument '%s' encountered when constructing " - "expression of Boolean arguments" % arg) + "expression of Boolean arguments" % arg + ) def _flattened_numeric_args(args): - """Flatten any potentially indexed arguments and check that they are + """Flatten any potentially indexed arguments and check that they are numeric.""" for arg in args: if arg.__class__ in native_types: @@ -226,7 +227,9 @@ def _flattened_numeric_args(args): else: raise ValueError( "Non-numeric argument '%s' encountered when constructing " - "expression with numeric arguments" % arg) + "expression with numeric arguments" % arg + ) + def land(*args): """ diff --git a/pyomo/core/tests/unit/test_logical_expr_expanded.py b/pyomo/core/tests/unit/test_logical_expr_expanded.py index 0e5bb4da445..0360e9b4783 100644 --- a/pyomo/core/tests/unit/test_logical_expr_expanded.py +++ b/pyomo/core/tests/unit/test_logical_expr_expanded.py @@ -291,8 +291,9 @@ def test_to_string(self): self.assertEqual(str(atleast(1, m.Y1, m.Y2)), "atleast(1: [Y1, Y2])") self.assertEqual(str(atmost(1, m.Y1, m.Y2)), "atmost(1: [Y1, Y2])") self.assertEqual(str(exactly(1, m.Y1, m.Y2)), "exactly(1: [Y1, Y2])") - self.assertEqual(str(all_different(m.int1, m.int2)), - "all_different(int1, int2)") + self.assertEqual( + str(all_different(m.int1, m.int2)), "all_different(int1, int2)" + ) self.assertEqual(str(count_if(m.Y1, m.Y2)), "count_if(Y1, Y2)") # Precedence checks From cdc21268a71ec906e84dd3c8731d4c296abd6a01 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 31 Jan 2024 15:12:21 -0700 Subject: [PATCH 0420/3044] We do need to evaluate the args when we apply count_if because they can be relational expressions --- pyomo/core/expr/logical_expr.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/core/expr/logical_expr.py b/pyomo/core/expr/logical_expr.py index 17f4a4dd564..875f5107f3a 100644 --- a/pyomo/core/expr/logical_expr.py +++ b/pyomo/core/expr/logical_expr.py @@ -200,6 +200,8 @@ def _flattened_boolean_args(args): yield _argdata elif hasattr(_argdata, 'is_logical_type') and _argdata.is_logical_type(): yield _argdata + elif isinstance(_argdata, BooleanValue): + yield _argdata else: raise ValueError( "Non-Boolean-valued argument '%s' encountered when constructing " @@ -626,7 +628,7 @@ def _to_string(self, values, verbose, smap): return "count_if(%s)" % (", ".join(values)) def _apply_operation(self, result): - return sum(r for r in result) + return sum(value(r) for r in result) special_boolean_atom_types = {ExactlyExpression, AtMostExpression, AtLeastExpression} From 8d2116265326a834680b9cc0bb896747d2749f78 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 31 Jan 2024 15:13:12 -0700 Subject: [PATCH 0421/3044] Removing another test with Boolean args to all diff --- pyomo/contrib/cp/tests/test_docplex_walker.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 0f1c73cd3b1..b897053c93a 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -424,22 +424,6 @@ def test_all_diff_expression(self): self.assertTrue(expr[1].equals(cp.all_diff(a[i] for i in m.I))) - def test_Boolean_args_in_all_diff_expression(self): - m = self.get_model() - m.a.domain = Integers - m.a.bounds = (11, 20) - m.c = LogicalConstraint(expr=all_different(m.a[1] == 13, m.b)) - - visitor = self.get_visitor() - expr = visitor.walk_expression((m.c.body, m.c, 0)) - - self.assertIn(id(m.a[1]), visitor.var_map) - a0 = visitor.var_map[id(m.a[1])] - self.assertIn(id(m.b), visitor.var_map) - b = visitor.var_map[id(m.b)] - - self.assertTrue(expr[1].equals(cp.all_diff(a0 == 13, b))) - def test_count_if_expression(self): m = self.get_model() m.a.domain = Integers From 1b73570e6cceab7127d0dca416dfad2774fedca6 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 31 Jan 2024 18:39:13 -0500 Subject: [PATCH 0422/3044] correct typos --- pyomo/contrib/mindtpy/algorithm_base_class.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index ad462221ec5..b6a223ba24b 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -108,7 +108,7 @@ def __init__(self, **kwds): self.curr_int_sol = [] self.should_terminate = False self.integer_list = [] - # Dictionary {integer solution (list): [cuts begin index, cuts end index] (list)} + # Dictionary {integer solution (tuple): [cuts begin index, cuts end index] (list)} self.integer_solution_to_cuts_index = dict() # Set up iteration counters @@ -2679,9 +2679,9 @@ def initialize_subsolvers(self): if config.mip_regularization_solver == 'gams': self.regularization_mip_opt.options['add_options'] = [] if config.regularization_mip_threads > 0: - self.regularization_mip_opt.options['threads'] = ( - config.regularization_mip_threads - ) + self.regularization_mip_opt.options[ + 'threads' + ] = config.regularization_mip_threads else: self.regularization_mip_opt.options['threads'] = config.threads @@ -2691,9 +2691,9 @@ def initialize_subsolvers(self): 'cplex_persistent', }: if config.solution_limit is not None: - self.regularization_mip_opt.options['mip_limits_solutions'] = ( - config.solution_limit - ) + self.regularization_mip_opt.options[ + 'mip_limits_solutions' + ] = config.solution_limit # We don't need to solve the regularization problem to optimality. # We will choose to perform aggressive node probing during presolve. self.regularization_mip_opt.options['mip_strategy_presolvenode'] = 3 @@ -2706,9 +2706,9 @@ def initialize_subsolvers(self): self.regularization_mip_opt.options['optimalitytarget'] = 3 elif config.mip_regularization_solver == 'gurobi': if config.solution_limit is not None: - self.regularization_mip_opt.options['SolutionLimit'] = ( - config.solution_limit - ) + self.regularization_mip_opt.options[ + 'SolutionLimit' + ] = config.solution_limit # Same reason as mip_strategy_presolvenode. self.regularization_mip_opt.options['Presolve'] = 2 @@ -3055,9 +3055,10 @@ def add_regularization(self): # The main problem might be unbounded, regularization is activated only when a valid bound is provided. if self.dual_bound != self.dual_bound_progress[0]: with time_code(self.timing, 'regularization main'): - (regularization_main_mip, regularization_main_mip_results) = ( - self.solve_regularization_main() - ) + ( + regularization_main_mip, + regularization_main_mip_results, + ) = self.solve_regularization_main() self.handle_regularization_main_tc( regularization_main_mip, regularization_main_mip_results ) From 4ec0e8c69ac7d1992f6879ba9b0e3e52352a9fea Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 31 Jan 2024 18:40:21 -0500 Subject: [PATCH 0423/3044] remove unused log --- pyomo/contrib/mindtpy/algorithm_base_class.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index b6a223ba24b..a4f3075a1e9 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1290,7 +1290,6 @@ def handle_subproblem_infeasible(self, fixed_nlp, cb_opt=None): # elif var.has_lb() and abs(value(var) - var.lb) < config.absolute_bound_tolerance: # fixed_nlp.ipopt_zU_out[var] = -1 - # config.logger.info('Solving feasibility problem') feas_subproblem, feas_subproblem_results = self.solve_feasibility_subproblem() # TODO: do we really need this? if self.should_terminate: From 462457b38b501922f10fcdf26928a78124b31b9c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 31 Jan 2024 16:57:30 -0700 Subject: [PATCH 0424/3044] Fix backwards compatibility --- pyomo/contrib/solver/base.py | 39 +++++------------------------------ pyomo/contrib/solver/ipopt.py | 4 ++-- pyomo/contrib/solver/util.py | 2 +- 3 files changed, 8 insertions(+), 37 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 98fa60b722f..8aca11d2f0a 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -349,6 +349,8 @@ def _map_config( load_solutions, symbolic_solver_labels, timelimit, + # Report timing is no longer a valid option. We now always return a + # timer object that can be inspected. report_timing, raise_exception_on_nonoptimal_result, solver_io, @@ -356,6 +358,7 @@ def _map_config( logfile, keepfiles, solnfile, + options, ): """Map between legacy and new interface configuration options""" self.config = self.config() @@ -363,7 +366,7 @@ def _map_config( self.config.load_solutions = load_solutions self.config.symbolic_solver_labels = symbolic_solver_labels self.config.time_limit = timelimit - self.config.report_timing = report_timing + self.config.solver_options.set_value(options) # This is a new flag in the interface. To preserve backwards compatibility, # its default is set to "False" self.config.raise_exception_on_nonoptimal_result = ( @@ -483,12 +486,9 @@ def solve( logfile, keepfiles, solnfile, + options, ) - original_options = self.options - if options is not None: - self.options = options - results: Results = super().solve(model) legacy_results, legacy_soln = self._map_results(model, results) @@ -497,7 +497,6 @@ def solve( ) self.config = original_config - self.options = original_options return legacy_results @@ -526,31 +525,3 @@ def license_is_valid(self) -> bool: """ return bool(self.available()) - - @property - def options(self): - """ - Read the options for the dictated solver. - - NOTE: Only the set of solvers for which the LegacySolverWrapper is compatible - are accounted for within this property. - Not all solvers are currently covered by this backwards compatibility - class. - """ - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: - if hasattr(self, 'solver_options'): - return getattr(self, 'solver_options') - raise NotImplementedError('Could not find the correct options') - - @options.setter - def options(self, val): - """ - Set the options for the dictated solver. - """ - found = False - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: - if hasattr(self, 'solver_options'): - setattr(self, 'solver_options', val) - found = True - if not found: - raise NotImplementedError('Could not find the correct options') diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 7c2a3f471e3..49cb0430e32 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -17,7 +17,7 @@ from typing import Mapping, Optional, Sequence from pyomo.common import Executable -from pyomo.common.config import ConfigValue, NonNegativeInt, NonNegativeFloat +from pyomo.common.config import ConfigValue, NonNegativeFloat from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer @@ -285,7 +285,7 @@ def solve(self, model, **kwds): f'Solver {self.__class__} is not available ({avail}).' ) # Update configuration options, based on keywords passed to solve - config: ipoptConfig = self.config(value=kwds) + config: ipoptConfig = self.config(value=kwds, preserve_implicit=True) if config.threads: logger.log( logging.WARNING, diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index 727d9c354e2..f8641b06c50 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -41,7 +41,7 @@ def check_optimal_termination(results): # Look at the original version of this function to make that happen. """ This function returns True if the termination condition for the solver - is 'optimal', 'locallyOptimal', or 'globallyOptimal', and the status is 'ok' + is 'optimal'. Parameters ---------- From acb10de438ccc8c7acb5b8ba7d42af7eab3694e9 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 31 Jan 2024 19:32:34 -0500 Subject: [PATCH 0425/3044] add one condition for fix dual bound --- pyomo/contrib/mindtpy/algorithm_base_class.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index a4f3075a1e9..ca428563a68 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1561,7 +1561,7 @@ def fix_dual_bound(self, last_iter_cuts): self.handle_nlp_subproblem_tc(fixed_nlp, fixed_nlp_result) MindtPy = self.mip.MindtPy_utils - # deactivate the integer cuts generated after the best solution was found. + # Deactivate the integer cuts generated after the best solution was found. self.deactivate_no_good_cuts_when_fixing_bound(MindtPy.cuts.no_good_cuts) if ( config.add_regularization is not None @@ -3013,10 +3013,12 @@ def MindtPy_iteration_loop(self): # if add_no_good_cuts is True, the bound obtained in the last iteration is no reliable. # we correct it after the iteration. + # There is no need to fix the dual bound if no feasible solution has been found. if ( (config.add_no_good_cuts or config.use_tabu_list) and not self.should_terminate and config.add_regularization is None + and self.best_solution_found is not None ): self.fix_dual_bound(self.last_iter_cuts) config.logger.info( From a0ad77e40b74dbaf3bfcf445eabe13d99b9d84af Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 31 Jan 2024 17:37:28 -0700 Subject: [PATCH 0426/3044] Add timing information to legacy results wrapper --- pyomo/contrib/solver/base.py | 4 ++++ pyomo/contrib/solver/ipopt.py | 1 + 2 files changed, 5 insertions(+) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 8aca11d2f0a..1948bf8bf1b 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -444,6 +444,10 @@ def _solution_handler( legacy_soln.variable['Rc'] = val legacy_results.solution.insert(legacy_soln) + # Timing info was not originally on the legacy results, but we want + # to make it accessible to folks who are utilizing the backwards + # compatible version. + legacy_results.timing_info = results.timing_info if delete_legacy_soln: legacy_results.solution.delete(0) return legacy_results diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 49cb0430e32..7f62d67d38e 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -441,6 +441,7 @@ def solve(self, model, **kwds): results.timing_info.wall_time = ( end_timestamp - start_timestamp ).total_seconds() + results.timing_info.timer = timer return results def _parse_ipopt_output(self, stream: io.StringIO): From de73340cf82b50d932ebe25a165c9bc3d7c63230 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 31 Jan 2024 19:41:24 -0500 Subject: [PATCH 0427/3044] remove fix_dual_bound for ECP method --- pyomo/contrib/mindtpy/extended_cutting_plane.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyomo/contrib/mindtpy/extended_cutting_plane.py b/pyomo/contrib/mindtpy/extended_cutting_plane.py index ac13e352e35..0a98f88ed3f 100644 --- a/pyomo/contrib/mindtpy/extended_cutting_plane.py +++ b/pyomo/contrib/mindtpy/extended_cutting_plane.py @@ -66,12 +66,6 @@ def MindtPy_iteration_loop(self): add_ecp_cuts(self.mip, self.jacobians, self.config, self.timing) - # if add_no_good_cuts is True, the bound obtained in the last iteration is no reliable. - # we correct it after the iteration. - if ( - self.config.add_no_good_cuts or self.config.use_tabu_list - ) and not self.should_terminate: - self.fix_dual_bound(self.last_iter_cuts) self.config.logger.info( ' ===============================================================================================' ) From 248ffd523a2b7d74464b0a94b48ad311a3279848 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 31 Jan 2024 17:51:47 -0700 Subject: [PATCH 0428/3044] Add more base unit tets --- pyomo/contrib/solver/tests/unit/test_base.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 2d158025903..5531e0530fc 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -168,4 +168,20 @@ def test_context_manager(self): class TestLegacySolverWrapper(unittest.TestCase): - pass + def test_class_method_list(self): + expected_list = [ + 'available', + 'license_is_valid', + 'solve' + ] + method_list = [ + method for method in dir(base.LegacySolverWrapper) if method.startswith('_') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + + def test_context_manager(self): + with base.LegacySolverWrapper() as instance: + with self.assertRaises(AttributeError) as context: + instance.available() + + From 92d9477985e92bcccd2785c4862eb1491dca3488 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 31 Jan 2024 19:52:56 -0500 Subject: [PATCH 0429/3044] black format --- pyomo/contrib/mindtpy/single_tree.py | 7 ++++--- pyomo/contrib/mindtpy/tests/nonconvex3.py | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index c1e52ed72d3..228810a8f90 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -588,9 +588,10 @@ def handle_lazy_subproblem_infeasible(self, fixed_nlp, mindtpy_solver, config, o dual_values = None config.logger.info('Solving feasibility problem') - (feas_subproblem, feas_subproblem_results) = ( - mindtpy_solver.solve_feasibility_subproblem() - ) + ( + feas_subproblem, + feas_subproblem_results, + ) = mindtpy_solver.solve_feasibility_subproblem() # In OA algorithm, OA cuts are generated based on the solution of the subproblem # We need to first copy the value of variables from the subproblem and then add cuts copy_var_list_values( diff --git a/pyomo/contrib/mindtpy/tests/nonconvex3.py b/pyomo/contrib/mindtpy/tests/nonconvex3.py index b08deb67b63..dbb88bb1fad 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex3.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex3.py @@ -40,7 +40,9 @@ def __init__(self, *args, **kwargs): m.objective = Objective(expr=7 * m.x1 + 10 * m.x2, sense=minimize) - m.c1 = Constraint(expr=(m.x1**1.2) * (m.x2**1.7) - 7 * m.x1 - 9 * m.x2 <= -24) + m.c1 = Constraint( + expr=(m.x1**1.2) * (m.x2**1.7) - 7 * m.x1 - 9 * m.x2 <= -24 + ) m.c2 = Constraint(expr=-m.x1 - 2 * m.x2 <= 5) m.c3 = Constraint(expr=-3 * m.x1 + m.x2 <= 1) m.c4 = Constraint(expr=4 * m.x1 - 3 * m.x2 <= 11) From 8d051b09ebaf84ec946b2d2e4f64a865bedde66e Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Thu, 1 Feb 2024 13:06:34 -0500 Subject: [PATCH 0430/3044] black format --- pyomo/contrib/mindtpy/algorithm_base_class.py | 25 +++++++++---------- pyomo/contrib/mindtpy/single_tree.py | 7 +++--- pyomo/contrib/mindtpy/tests/nonconvex3.py | 4 +-- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index ca428563a68..3d5a7ebad03 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -2678,9 +2678,9 @@ def initialize_subsolvers(self): if config.mip_regularization_solver == 'gams': self.regularization_mip_opt.options['add_options'] = [] if config.regularization_mip_threads > 0: - self.regularization_mip_opt.options[ - 'threads' - ] = config.regularization_mip_threads + self.regularization_mip_opt.options['threads'] = ( + config.regularization_mip_threads + ) else: self.regularization_mip_opt.options['threads'] = config.threads @@ -2690,9 +2690,9 @@ def initialize_subsolvers(self): 'cplex_persistent', }: if config.solution_limit is not None: - self.regularization_mip_opt.options[ - 'mip_limits_solutions' - ] = config.solution_limit + self.regularization_mip_opt.options['mip_limits_solutions'] = ( + config.solution_limit + ) # We don't need to solve the regularization problem to optimality. # We will choose to perform aggressive node probing during presolve. self.regularization_mip_opt.options['mip_strategy_presolvenode'] = 3 @@ -2705,9 +2705,9 @@ def initialize_subsolvers(self): self.regularization_mip_opt.options['optimalitytarget'] = 3 elif config.mip_regularization_solver == 'gurobi': if config.solution_limit is not None: - self.regularization_mip_opt.options[ - 'SolutionLimit' - ] = config.solution_limit + self.regularization_mip_opt.options['SolutionLimit'] = ( + config.solution_limit + ) # Same reason as mip_strategy_presolvenode. self.regularization_mip_opt.options['Presolve'] = 2 @@ -3056,10 +3056,9 @@ def add_regularization(self): # The main problem might be unbounded, regularization is activated only when a valid bound is provided. if self.dual_bound != self.dual_bound_progress[0]: with time_code(self.timing, 'regularization main'): - ( - regularization_main_mip, - regularization_main_mip_results, - ) = self.solve_regularization_main() + (regularization_main_mip, regularization_main_mip_results) = ( + self.solve_regularization_main() + ) self.handle_regularization_main_tc( regularization_main_mip, regularization_main_mip_results ) diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index 228810a8f90..c1e52ed72d3 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -588,10 +588,9 @@ def handle_lazy_subproblem_infeasible(self, fixed_nlp, mindtpy_solver, config, o dual_values = None config.logger.info('Solving feasibility problem') - ( - feas_subproblem, - feas_subproblem_results, - ) = mindtpy_solver.solve_feasibility_subproblem() + (feas_subproblem, feas_subproblem_results) = ( + mindtpy_solver.solve_feasibility_subproblem() + ) # In OA algorithm, OA cuts are generated based on the solution of the subproblem # We need to first copy the value of variables from the subproblem and then add cuts copy_var_list_values( diff --git a/pyomo/contrib/mindtpy/tests/nonconvex3.py b/pyomo/contrib/mindtpy/tests/nonconvex3.py index dbb88bb1fad..b08deb67b63 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex3.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex3.py @@ -40,9 +40,7 @@ def __init__(self, *args, **kwargs): m.objective = Objective(expr=7 * m.x1 + 10 * m.x2, sense=minimize) - m.c1 = Constraint( - expr=(m.x1**1.2) * (m.x2**1.7) - 7 * m.x1 - 9 * m.x2 <= -24 - ) + m.c1 = Constraint(expr=(m.x1**1.2) * (m.x2**1.7) - 7 * m.x1 - 9 * m.x2 <= -24) m.c2 = Constraint(expr=-m.x1 - 2 * m.x2 <= 5) m.c3 = Constraint(expr=-3 * m.x1 + m.x2 <= 1) m.c4 = Constraint(expr=4 * m.x1 - 3 * m.x2 <= 11) From a2a5513a4d517e088b3970f83fb27faef0fbe7c7 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 1 Feb 2024 16:00:08 -0700 Subject: [PATCH 0431/3044] Add LegacySolverWrapper tests --- pyomo/contrib/solver/base.py | 14 ++- pyomo/contrib/solver/config.py | 11 ++- pyomo/contrib/solver/ipopt.py | 11 +-- pyomo/contrib/solver/tests/unit/test_base.py | 98 ++++++++++++++++++-- 4 files changed, 116 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 1948bf8bf1b..42524296d74 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -21,6 +21,7 @@ from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.errors import ApplicationError +from pyomo.common.deprecation import deprecation_warning from pyomo.opt.results.results_ import SolverResults as LegacySolverResults from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize @@ -378,8 +379,17 @@ def _map_config( raise NotImplementedError('Still working on this') if logfile is not None: raise NotImplementedError('Still working on this') - if 'keepfiles' in self.config: - self.config.keepfiles = keepfiles + if keepfiles or 'keepfiles' in self.config: + cwd = os.getcwd() + deprecation_warning( + "`keepfiles` has been deprecated in the new solver interface. " + "Use `working_dir` instead to designate a directory in which " + f"files should be generated and saved. Setting `working_dir` to `{cwd}`.", + version='6.7.1.dev0', + ) + self.config.working_dir = cwd + # I believe this currently does nothing; however, it is unclear what + # our desired behavior is for this. if solnfile is not None: if 'filename' in self.config: filename = os.path.splitext(solnfile)[0] diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 4c81d31a820..d5921c526b0 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -58,6 +58,14 @@ def __init__( description="If True, the solver output gets logged.", ), ) + self.working_dir: str = self.declare( + 'working_dir', + ConfigValue( + domain=str, + default=None, + description="The directory in which generated files should be saved. This replaced the `keepfiles` option.", + ), + ) self.load_solutions: bool = self.declare( 'load_solutions', ConfigValue( @@ -79,7 +87,8 @@ def __init__( ConfigValue( domain=bool, default=False, - description="If True, the names given to the solver will reflect the names of the Pyomo components. Cannot be changed after set_instance is called.", + description="If True, the names given to the solver will reflect the names of the Pyomo components." + "Cannot be changed after set_instance is called.", ), ) self.timer: HierarchicalTimer = self.declare( diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 7f62d67d38e..4c4b932381d 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -68,11 +68,6 @@ def __init__( self.executable = self.declare( 'executable', ConfigValue(default=Executable('ipopt')) ) - # TODO: Add in a deprecation here for keepfiles - # M.B.: Is the above TODO still relevant? - self.temp_dir: str = self.declare( - 'temp_dir', ConfigValue(domain=str, default=None) - ) self.writer_config = self.declare( 'writer_config', ConfigValue(default=NLWriter.CONFIG()) ) @@ -262,7 +257,7 @@ def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: boo cmd.append('option_file_name=' + basename + '.opt') if 'option_file_name' in config.solver_options: raise ValueError( - 'Pyomo generates the ipopt options file as part of the solve method. ' + 'Pyomo generates the ipopt options file as part of the `solve` method. ' 'Add all options to ipopt.config.solver_options instead.' ) if ( @@ -298,10 +293,10 @@ def solve(self, model, **kwds): StaleFlagManager.mark_all_as_stale() results = ipoptResults() with TempfileManager.new_context() as tempfile: - if config.temp_dir is None: + if config.working_dir is None: dname = tempfile.mkdtemp() else: - dname = config.temp_dir + dname = config.working_dir if not os.path.exists(dname): os.mkdir(dname) basename = os.path.join(dname, model.name) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 5531e0530fc..00e38d9ac59 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -9,7 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import os + from pyomo.common import unittest +from pyomo.common.config import ConfigDict from pyomo.contrib.solver import base @@ -169,13 +172,11 @@ def test_context_manager(self): class TestLegacySolverWrapper(unittest.TestCase): def test_class_method_list(self): - expected_list = [ - 'available', - 'license_is_valid', - 'solve' - ] + expected_list = ['available', 'license_is_valid', 'solve'] method_list = [ - method for method in dir(base.LegacySolverWrapper) if method.startswith('_') is False + method + for method in dir(base.LegacySolverWrapper) + if method.startswith('_') is False ] self.assertEqual(sorted(expected_list), sorted(method_list)) @@ -184,4 +185,87 @@ def test_context_manager(self): with self.assertRaises(AttributeError) as context: instance.available() - + def test_map_config(self): + # Create a fake/empty config structure that can be added to an empty + # instance of LegacySolverWrapper + self.config = ConfigDict(implicit=True) + self.config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + instance = base.LegacySolverWrapper() + instance.config = self.config + instance._map_config( + True, False, False, 20, True, False, None, None, None, False, None, None + ) + self.assertTrue(instance.config.tee) + self.assertFalse(instance.config.load_solutions) + self.assertEqual(instance.config.time_limit, 20) + # Report timing shouldn't be created because it no longer exists + with self.assertRaises(AttributeError) as context: + print(instance.config.report_timing) + # Keepfiles should not be created because we did not declare keepfiles on + # the original config + with self.assertRaises(AttributeError) as context: + print(instance.config.keepfiles) + # We haven't implemented solver_io, suffixes, or logfile + with self.assertRaises(NotImplementedError) as context: + instance._map_config( + False, + False, + False, + 20, + False, + False, + None, + None, + '/path/to/bogus/file', + False, + None, + None, + ) + with self.assertRaises(NotImplementedError) as context: + instance._map_config( + False, + False, + False, + 20, + False, + False, + None, + '/path/to/bogus/file', + None, + False, + None, + None, + ) + with self.assertRaises(NotImplementedError) as context: + instance._map_config( + False, + False, + False, + 20, + False, + False, + '/path/to/bogus/file', + None, + None, + False, + None, + None, + ) + # If they ask for keepfiles, we redirect them to working_dir + instance._map_config( + False, False, False, 20, False, False, None, None, None, True, None, None + ) + self.assertEqual(instance.config.working_dir, os.getcwd()) + with self.assertRaises(AttributeError) as context: + print(instance.config.keepfiles) + + def test_map_results(self): + # Unclear how to test this + pass + + def test_solution_handler(self): + # Unclear how to test this + pass From 93a04098e02a7c6c705d13a353565fa9ebe77db8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 1 Feb 2024 16:06:46 -0700 Subject: [PATCH 0432/3044] Starting to draft correct mapping for disaggregated vars--this is totally broken --- pyomo/gdp/plugins/hull.py | 45 +++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 3d2be2f9e15..c7a005bb4ea 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -599,6 +599,9 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, bigmConstraint = Constraint(transBlock.lbub) relaxationBlock.add_component(conName, bigmConstraint) + parent_block = var.parent_block() + disaggregated_var_map = self._get_disaggregated_var_map(parent_block) + print("Adding bounds constraints for local var '%s'" % var) # TODO: This gets mapped in a place where we can't find it if we ask # for it from the local var itself. @@ -610,7 +613,7 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, 'lb', 'ub', obj.indicator_var.get_associated_binary(), - transBlock, + disaggregated_var_map, ) var_substitute_map = dict( @@ -647,10 +650,8 @@ def _declare_disaggregated_var_bounds( lb_idx, ub_idx, var_free_indicator, - transBlock=None, + disaggregated_var_map, ): - # If transBlock is None then this is a disaggregated variable for - # multiple Disjuncts and we will handle the mappings separately. lb = original_var.lb ub = original_var.ub if lb is None or ub is None: @@ -669,13 +670,18 @@ def _declare_disaggregated_var_bounds( bigmConstraint.add(ub_idx, disaggregatedVar <= ub * var_free_indicator) # store the mappings from variables to their disaggregated selves on - # the transformation block. - if transBlock is not None: - transBlock._disaggregatedVarMap['disaggregatedVar'][disjunct][ - original_var - ] = disaggregatedVar - transBlock._disaggregatedVarMap['srcVar'][disaggregatedVar] = original_var - transBlock._bigMConstraintMap[disaggregatedVar] = bigmConstraint + # the transformation block + disaggregated_var_map['disaggregatedVar'][disjunct][ + original_var] = disaggregatedVar + disaggregated_var_map['srcVar'][disaggregatedVar] = original_var + bigMConstraintMap[disaggregatedVar] = bigmConstraint + + # if transBlock is not None: + # transBlock._disaggregatedVarMap['disaggregatedVar'][disjunct][ + # original_var + # ] = disaggregatedVar + # transBlock._disaggregatedVarMap['srcVar'][disaggregatedVar] = original_var + # transBlock._bigMConstraintMap[disaggregatedVar] = bigmConstraint def _get_local_var_list(self, parent_disjunct): # Add or retrieve Suffix from parent_disjunct so that, if this is @@ -916,7 +922,7 @@ def get_src_var(self, disaggregated_var): Parameters ---------- - disaggregated_var: a Var which was created by the hull + disaggregated_var: a Var that was created by the hull transformation as a disaggregated variable (and so appears on a transformation block of some Disjunct) @@ -925,17 +931,14 @@ def get_src_var(self, disaggregated_var): "'%s' does not appear to be a " "disaggregated variable" % disaggregated_var.name ) - # There are two possibilities: It is declared on a Disjunct - # transformation Block, or it is declared on the parent of a Disjunct - # transformation block (if it is a single variable for multiple - # Disjuncts the original doesn't appear in) + # We always put a dictionary called '_disaggregatedVarMap' on the parent + # block of the variable. If it's not there, then this probably isn't a + # disaggregated Var (or if it is it's a developer error). Similarly, if + # the var isn't in the dictionary, if we're doing what we should, then + # it's not a disaggregated var. transBlock = disaggregated_var.parent_block() if not hasattr(transBlock, '_disaggregatedVarMap'): - try: - transBlock = transBlock.parent_block().parent_block() - except: - logger.error(msg) - raise + raise GDP_Error(msg) try: return transBlock._disaggregatedVarMap['srcVar'][disaggregated_var] except: From 2c8a4d818c70c7088a4f45d6b5fa23b07f028a75 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 1 Feb 2024 16:48:59 -0700 Subject: [PATCH 0433/3044] Backwards compatibility; add tests --- pyomo/contrib/solver/tests/unit/test_util.py | 43 ++++++++++++++++- pyomo/contrib/solver/util.py | 50 +++++++++++++++----- 2 files changed, 80 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/solver/tests/unit/test_util.py b/pyomo/contrib/solver/tests/unit/test_util.py index 9bf92af72cf..8a8a0221362 100644 --- a/pyomo/contrib/solver/tests/unit/test_util.py +++ b/pyomo/contrib/solver/tests/unit/test_util.py @@ -11,9 +11,18 @@ from pyomo.common import unittest import pyomo.environ as pyo -from pyomo.contrib.solver.util import collect_vars_and_named_exprs, get_objective +from pyomo.contrib.solver.util import ( + collect_vars_and_named_exprs, + get_objective, + check_optimal_termination, + assert_optimal_termination, + SolverStatus, + LegacyTerminationCondition, +) +from pyomo.contrib.solver.results import Results, SolutionStatus, TerminationCondition from typing import Callable from pyomo.common.gsl import find_GSL +from pyomo.opt.results import SolverResults class TestGenericUtils(unittest.TestCase): @@ -73,3 +82,35 @@ def test_get_objective_raise(self): model.OBJ2 = pyo.Objective(expr=model.x[1] - 4 * model.x[2]) with self.assertRaises(ValueError): get_objective(model) + + def test_check_optimal_termination_new_interface(self): + results = Results() + results.solution_status = SolutionStatus.optimal + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) + # Both items satisfied + self.assertTrue(check_optimal_termination(results)) + # Termination condition not satisfied + results.termination_condition = TerminationCondition.iterationLimit + self.assertFalse(check_optimal_termination(results)) + # Both not satisfied + results.solution_status = SolutionStatus.noSolution + self.assertFalse(check_optimal_termination(results)) + + def test_check_optimal_termination_condition_legacy_interface(self): + results = SolverResults() + results.solver.status = SolverStatus.ok + results.solver.termination_condition = LegacyTerminationCondition.optimal + self.assertTrue(check_optimal_termination(results)) + results.solver.termination_condition = LegacyTerminationCondition.unknown + self.assertFalse(check_optimal_termination(results)) + results.solver.termination_condition = SolverStatus.aborted + self.assertFalse(check_optimal_termination(results)) + + # TODO: Left off here; need to make these tests + def test_assert_optimal_termination_new_interface(self): + pass + + def test_assert_optimal_termination_legacy_interface(self): + pass diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index f8641b06c50..807d66f569e 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -22,10 +22,20 @@ from pyomo.common.collections import ComponentMap from pyomo.common.timing import HierarchicalTimer from pyomo.core.expr.numvalue import NumericConstant +from pyomo.opt.results.solver import ( + SolverStatus, + TerminationCondition as LegacyTerminationCondition, +) + + from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus def get_objective(block): + """ + Get current active objective on a block. If there is more than one active, + return an error. + """ obj = None for o in block.component_data_objects( Objective, descend_into=True, active=True, sort=True @@ -37,8 +47,6 @@ def get_objective(block): def check_optimal_termination(results): - # TODO: Make work for legacy and new results objects. - # Look at the original version of this function to make that happen. """ This function returns True if the termination condition for the solver is 'optimal'. @@ -51,11 +59,21 @@ def check_optimal_termination(results): ------- `bool` """ - if results.solution_status == SolutionStatus.optimal and ( - results.termination_condition - == TerminationCondition.convergenceCriteriaSatisfied - ): - return True + if hasattr(results, 'solution_status'): + if results.solution_status == SolutionStatus.optimal and ( + results.termination_condition + == TerminationCondition.convergenceCriteriaSatisfied + ): + return True + else: + if results.solver.status == SolverStatus.ok and ( + results.solver.termination_condition == LegacyTerminationCondition.optimal + or results.solver.termination_condition + == LegacyTerminationCondition.locallyOptimal + or results.solver.termination_condition + == LegacyTerminationCondition.globallyOptimal + ): + return True return False @@ -70,12 +88,20 @@ def assert_optimal_termination(results): results : Pyomo Results object returned from solver.solve """ if not check_optimal_termination(results): - msg = ( - 'Solver failed to return an optimal solution. ' - 'Solution status: {}, Termination condition: {}'.format( - results.solution_status, results.termination_condition + if hasattr(results, 'solution_status'): + msg = ( + 'Solver failed to return an optimal solution. ' + 'Solution status: {}, Termination condition: {}'.format( + results.solution_status, results.termination_condition + ) + ) + else: + msg = ( + 'Solver failed to return an optimal solution. ' + 'Solver status: {}, Termination condition: {}'.format( + results.solver.status, results.solver.termination_condition + ) ) - ) raise RuntimeError(msg) From 6923e3639c8c0b2faac479624bca1f9a3c92d123 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 1 Feb 2024 20:50:12 -0700 Subject: [PATCH 0434/3044] Raising an error for unrecognized active Suffixes, ignoring LocalVars suffix, and logging a message at the debug level about ignoring BigM Suffixes --- pyomo/gdp/plugins/multiple_bigm.py | 19 +++++++++++++++++ pyomo/gdp/tests/test_mbigm.py | 34 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 48ec1177fe5..acd96c488b3 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -202,6 +202,7 @@ def __init__(self): super().__init__(logger) self._arg_list = {} self._set_up_expr_bound_visitor() + self.handlers[Suffix] = self._warn_for_active_suffix def _apply_to(self, instance, **kwds): self.used_args = ComponentMap() @@ -693,6 +694,24 @@ def _calculate_missing_M_values( return arg_Ms + def _warn_for_active_suffix(self, suffix, disjunct, active_disjuncts, Ms): + if suffix.local_name == 'BigM': + logger.debug( + "Found active 'BigM' Suffix on '{0}'. " + "The multiple bigM transformation does not currently " + "support specifying M's with Suffixes and is ignoring " + "this Suffix.".format(disjunct.name) + ) + elif suffix.local_name == 'LocalVars': + # This is fine, but this transformation doesn't need anything from it + pass + else: + raise GDP_Error( + "Found active Suffix '{0}' on Disjunct '{1}'. " + "The multiple bigM transformation does not currently " + "support Suffixes.".format(suffix.name, disjunct.name) + ) + # These are all functions to retrieve transformed components from # original ones and vice versa. diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index 0cdf004a445..33e8781ac63 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -352,6 +352,40 @@ def test_transformed_constraints_correct_Ms_specified(self): self.check_all_untightened_bounds_constraints(m, mbm) self.check_linear_func_constraints(m, mbm) + def test_local_var_suffix_ignored(self): + m = self.make_model() + m.y = Var(bounds=(2, 5)) + m.d1.another_thing = Constraint(expr=m.y == 3) + m.d1.LocalVars = Suffix(direction=Suffix.LOCAL) + m.d1.LocalVars[m.d1] = m.y + + mbigm = TransformationFactory('gdp.mbigm') + mbigm.apply_to(m, reduce_bound_constraints=True, + only_mbigm_bound_constraints=True) + + cons = mbigm.get_transformed_constraints(m.d1.x1_bounds) + self.check_pretty_bound_constraints( + cons[0], m.x1, {m.d1: 0.5, m.d2: 0.65, m.d3: 2}, lb=True + ) + self.check_pretty_bound_constraints( + cons[1], m.x1, {m.d1: 2, m.d2: 3, m.d3: 10}, lb=False + ) + + cons = mbigm.get_transformed_constraints(m.d1.x2_bounds) + self.check_pretty_bound_constraints( + cons[0], m.x2, {m.d1: 0.75, m.d2: 3, m.d3: 0.55}, lb=True + ) + self.check_pretty_bound_constraints( + cons[1], m.x2, {m.d1: 3, m.d2: 10, m.d3: 1}, lb=False + ) + + cons = mbigm.get_transformed_constraints(m.d1.another_thing) + self.assertEqual(len(cons), 2) + self.check_pretty_bound_constraints( + cons[0], m.y, {m.d1: 3, m.d2: 2, m.d3: 2}, lb=True) + self.check_pretty_bound_constraints( + cons[1], m.y, {m.d1: 3, m.d2: 5, m.d3: 5}, lb=False) + def test_pickle_transformed_model(self): m = self.make_model() TransformationFactory('gdp.mbigm').apply_to(m, bigM=self.get_Ms(m)) From 307caa875ad7f52268ed79c7130a9765c23707b2 Mon Sep 17 00:00:00 2001 From: Sakshi <73687517+Sakshi21299@users.noreply.github.com> Date: Fri, 2 Feb 2024 16:47:16 -0500 Subject: [PATCH 0435/3044] applied black --- pyomo/contrib/incidence_analysis/interface.py | 79 +++++++++---------- .../tests/test_interface.py | 42 +++++----- 2 files changed, 58 insertions(+), 63 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 177ca97a6b6..23178bdf14b 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -47,7 +47,7 @@ from pyomo.contrib.pynumero.asl import AmplInterface pyomo_nlp, pyomo_nlp_available = attempt_import( - 'pyomo.contrib.pynumero.interfaces.pyomo_nlp' + "pyomo.contrib.pynumero.interfaces.pyomo_nlp" ) asl_available = pyomo_nlp_available & AmplInterface.available() @@ -886,9 +886,9 @@ def plot(self, variables=None, constraints=None, title=None, show=True): edge_trace = plotly.graph_objects.Scatter( x=edge_x, y=edge_y, - line=dict(width=0.5, color='#888'), - hoverinfo='none', - mode='lines', + line=dict(width=0.5, color="#888"), + hoverinfo="none", + mode="lines", ) node_x = [] @@ -902,28 +902,28 @@ def plot(self, variables=None, constraints=None, title=None, show=True): if node < M: # According to convention, we are a constraint node c = constraints[node] - node_color.append('red') - body_text = '
'.join( + node_color.append("red") + body_text = "
".join( textwrap.wrap(str(c.body), width=120, subsequent_indent=" ") ) node_text.append( - f'{str(c)}
lb: {str(c.lower)}
body: {body_text}
' - f'ub: {str(c.upper)}
active: {str(c.active)}' + f"{str(c)}
lb: {str(c.lower)}
body: {body_text}
" + f"ub: {str(c.upper)}
active: {str(c.active)}" ) else: # According to convention, we are a variable node v = variables[node - M] - node_color.append('blue') + node_color.append("blue") node_text.append( - f'{str(v)}
lb: {str(v.lb)}
ub: {str(v.ub)}
' - f'value: {str(v.value)}
domain: {str(v.domain)}
' - f'fixed: {str(v.is_fixed())}' + f"{str(v)}
lb: {str(v.lb)}
ub: {str(v.ub)}
" + f"value: {str(v.value)}
domain: {str(v.domain)}
" + f"fixed: {str(v.is_fixed())}" ) node_trace = plotly.graph_objects.Scatter( x=node_x, y=node_y, - mode='markers', - hoverinfo='text', + mode="markers", + hoverinfo="text", text=node_text, marker=dict(color=node_color, size=10), ) @@ -932,17 +932,17 @@ def plot(self, variables=None, constraints=None, title=None, show=True): fig.update_layout(title=dict(text=title)) if show: fig.show() - + def add_edge_to_graph(self, node0, node1): """Adds an edge between node0 and node1 in the incidence graph - + Parameters --------- nodes0: VarData/ConstraintData - A node in the graph from the first bipartite set + A node in the graph from the first bipartite set (``bipartite=0``) node1: VarData/ConstraintData - A node in the graph from the second bipartite set + A node in the graph from the second bipartite set (``bipartite=1``) """ if self._incidence_graph is None: @@ -950,40 +950,35 @@ def add_edge_to_graph(self, node0, node1): "Attempting to add edge in an incidence graph from cached " "incidence graph,\nbut no incidence graph has been cached." ) - - if node0 not in ComponentSet(self._variables) and node0 not in ComponentSet(self._constraints): - raise RuntimeError( - "%s is not a node in the incidence graph" % node0 - ) - - if node1 not in ComponentSet(self._variables) and node1 not in ComponentSet(self._constraints): - raise RuntimeError( - "%s is not a node in the incidence graph" % node1 - ) - + + if node0 not in ComponentSet(self._variables) and node0 not in ComponentSet( + self._constraints + ): + raise RuntimeError("%s is not a node in the incidence graph" % node0) + + if node1 not in ComponentSet(self._variables) and node1 not in ComponentSet( + self._constraints + ): + raise RuntimeError("%s is not a node in the incidence graph" % node1) + if node0 in ComponentSet(self._variables): - node0_idx = self._var_index_map[node0] + len(self._con_index_map) + node0_idx = self._var_index_map[node0] + len(self._con_index_map) if node1 in ComponentSet(self._variables): raise RuntimeError( "%s & %s are both variables. Cannot add an edge between two" - "variables.\nThe resulting graph won't be bipartite" + "variables.\nThe resulting graph won't be bipartite" % (node0, node1) - ) + ) node1_idx = self._con_index_map[node1] - + if node0 in ComponentSet(self._constraints): node0_idx = self._con_index_map[node0] if node1 in ComponentSet(self._constraints): raise RuntimeError( "%s & %s are both constraints. Cannot add an edge between two" - "constraints.\nThe resulting graph won't be bipartite" + "constraints.\nThe resulting graph won't be bipartite" % (node0, node1) - ) - node1_idx = self._var_index_map[node1] + len(self._con_index_map) - + ) + node1_idx = self._var_index_map[node1] + len(self._con_index_map) + self._incidence_graph.add_edge(node0_idx, node1_idx) - - - - - diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 63bc74ee6dc..7da563be28c 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -638,13 +638,13 @@ def test_exception(self): variables = [model.P] constraints = [model.ideal_gas] igraph.maximum_matching(variables, constraints) - self.assertIn('must be unindexed', str(exc.exception)) + self.assertIn("must be unindexed", str(exc.exception)) with self.assertRaises(RuntimeError) as exc: variables = [model.P] constraints = [model.ideal_gas] igraph.block_triangularize(variables, constraints) - self.assertIn('must be unindexed', str(exc.exception)) + self.assertIn("must be unindexed", str(exc.exception)) @unittest.skipUnless(networkx_available, "networkx is not available.") @@ -889,13 +889,13 @@ def test_exception(self): variables = [model.P] constraints = [model.ideal_gas] igraph.maximum_matching(variables, constraints) - self.assertIn('must be unindexed', str(exc.exception)) + self.assertIn("must be unindexed", str(exc.exception)) with self.assertRaises(RuntimeError) as exc: variables = [model.P] constraints = [model.ideal_gas] igraph.block_triangularize(variables, constraints) - self.assertIn('must be unindexed', str(exc.exception)) + self.assertIn("must be unindexed", str(exc.exception)) @unittest.skipUnless(scipy_available, "scipy is not available.") def test_remove(self): @@ -1745,7 +1745,7 @@ def test_plot(self): m.c2 = pyo.Constraint(expr=m.z >= m.x) m.y.fix() igraph = IncidenceGraphInterface(m, include_inequality=True, include_fixed=True) - igraph.plot(title='test plot', show=False) + igraph.plot(title="test plot", show=False) def test_zero_coeff(self): m = pyo.ConcreteModel() @@ -1790,15 +1790,15 @@ def test_linear_only(self): self.assertEqual(len(matching), 2) self.assertIs(matching[m.eq2], m.x[2]) self.assertIs(matching[m.eq3], m.x[3]) - + def test_add_edge(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2, 3, 4]) m.eq1 = pyo.Constraint(expr=m.x[1] ** 2 + m.x[2] ** 2 + m.x[3] ** 2 == 1) m.eq2 = pyo.Constraint(expr=m.x[2] + pyo.sqrt(m.x[1]) + pyo.exp(m.x[3]) == 1) m.eq3 = pyo.Constraint(expr=m.x[3] + m.x[2] + m.x[4] == 1) - m.eq4 = pyo.Constraint(expr=m.x[1] + m.x[2]**2 == 5) - + m.eq4 = pyo.Constraint(expr=m.x[1] + m.x[2] ** 2 == 5) + # nodes: component # 0 : eq1 # 1 : eq2 @@ -1807,41 +1807,41 @@ def test_add_edge(self): # 4 : x[1] # 5 : x[2] # 6 : x[3] - # 7 : x[4] - + # 7 : x[4] + igraph = IncidenceGraphInterface(m, linear_only=False) n_edges_original = igraph.n_edges - - #Test if there already exists an edge between two nodes, nothing is added + + # Test if there already exists an edge between two nodes, nothing is added igraph.add_edge_to_graph(m.eq3, m.x[4]) n_edges_new = igraph.n_edges self.assertEqual(n_edges_original, n_edges_new) - + igraph.add_edge_to_graph(m.x[1], m.eq3) n_edges_new = igraph.n_edges self.assertEqual(set(igraph._incidence_graph[2]), {6, 5, 7, 4}) - self.assertEqual(n_edges_original +1, n_edges_new) - + self.assertEqual(n_edges_original + 1, n_edges_new) + igraph.add_edge_to_graph(m.eq4, m.x[4]) n_edges_new = igraph.n_edges self.assertEqual(set(igraph._incidence_graph[3]), {4, 5, 7}) self.assertEqual(n_edges_original + 2, n_edges_new) - + def test_add_edge_linear_igraph(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2, 3, 4]) - m.eq1 = pyo.Constraint(expr=m.x[1] + m.x[3] == 1) + m.eq1 = pyo.Constraint(expr=m.x[1] + m.x[3] == 1) m.eq2 = pyo.Constraint(expr=m.x[2] + pyo.sqrt(m.x[1]) + pyo.exp(m.x[3]) == 1) - m.eq3 = pyo.Constraint(expr=m.x[4]**2 + m.x[1] ** 3 + m.x[2] == 1) - - #Make sure error is raised when a variable is not in the igraph + m.eq3 = pyo.Constraint(expr=m.x[4] ** 2 + m.x[1] ** 3 + m.x[2] == 1) + + # Make sure error is raised when a variable is not in the igraph igraph = IncidenceGraphInterface(m, linear_only=True) n_edges_original = igraph.n_edges msg = "is not a node in the incidence graph" with self.assertRaisesRegex(RuntimeError, msg): igraph.add_edge_to_graph(m.x[4], m.eq2) - + @unittest.skipUnless(networkx_available, "networkx is not available.") class TestIndexedBlock(unittest.TestCase): From 46f3a0455bd7e756084c66431b4dd4c5868b1a60 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Sun, 4 Feb 2024 22:05:41 -0500 Subject: [PATCH 0436/3044] first working version of LDSDA --- pyomo/contrib/gdpopt/config_options.py | 26 ++ pyomo/contrib/gdpopt/ldsda.py | 492 +++++++++++++++++++++++++ pyomo/contrib/gdpopt/plugins.py | 1 + 3 files changed, 519 insertions(+) create mode 100644 pyomo/contrib/gdpopt/ldsda.py diff --git a/pyomo/contrib/gdpopt/config_options.py b/pyomo/contrib/gdpopt/config_options.py index 386826b844c..ff5f17e2278 100644 --- a/pyomo/contrib/gdpopt/config_options.py +++ b/pyomo/contrib/gdpopt/config_options.py @@ -528,3 +528,29 @@ def _add_tolerance_configs(CONFIG): description="Tolerance for bound convergence.", ), ) + + +def _add_ldsda_configs(CONFIG): + CONFIG.declare( + "direction_norm", + ConfigValue( + default='L2', + domain=In(['L2', 'Linf']), + description="The norm to use for the search direction", + ), + ) + CONFIG.declare( + "starting_point", + ConfigValue(default=None, description="The value list of external variables."), + ) + CONFIG.declare( + "logical_constraint_list", + ConfigValue( + default=None, + description=""" + The list of logical constraints to be reformulated into external variables. + The logical constraints should be in the same order of provided starting point. + The provide logical constraints should be ExactlyExpression. + TODO: Maybe we can find a better design for this.""", + ), + ) diff --git a/pyomo/contrib/gdpopt/ldsda.py b/pyomo/contrib/gdpopt/ldsda.py new file mode 100644 index 00000000000..a732ece680e --- /dev/null +++ b/pyomo/contrib/gdpopt/ldsda.py @@ -0,0 +1,492 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from collections import namedtuple +from heapq import heappush, heappop +import traceback + +from pyomo.common.collections import ComponentMap +from pyomo.common.config import document_kwargs_from_configdict +from pyomo.common.errors import InfeasibleConstraintException +from pyomo.contrib.fbbt.fbbt import fbbt +from pyomo.contrib.gdpopt.algorithm_base_class import _GDPoptAlgorithm +from pyomo.contrib.gdpopt.create_oa_subproblems import ( + add_util_block, + add_disjunction_list, + add_disjunct_list, + add_algebraic_variable_list, + add_boolean_variable_lists, + add_transformed_boolean_variable_list, +) +from pyomo.contrib.gdpopt.config_options import ( + _add_nlp_solver_configs, + _add_BB_configs, + _add_ldsda_configs, + _add_mip_solver_configs, + _add_tolerance_configs, + _add_nlp_solve_configs, +) +from pyomo.contrib.gdpopt.nlp_initialization import restore_vars_to_original_values +from pyomo.contrib.gdpopt.util import ( + copy_var_list_values, + SuppressInfeasibleWarning, + get_main_elapsed_time, +) +from pyomo.contrib.satsolver.satsolver import satisfiable +from pyomo.core import ( + minimize, + Suffix, + Constraint, + TransformationFactory, + BooleanVar, + Var, + Objective, +) +from pyomo.opt import SolverFactory, SolverStatus +from pyomo.opt import TerminationCondition as tc +from pyomo.core.expr.logical_expr import ExactlyExpression +import numpy as np # attempt_import +import itertools as it # attempt_import + +_linear_degrees = {1, 0} + +# Data tuple for each node that also functions as the sort key. +# Therefore, ordering of the arguments below matters. +BBNodeData = namedtuple( + 'BBNodeData', + [ + 'obj_lb', # lower bound on objective value, sign corrected to minimize + 'obj_ub', # upper bound on objective value, sign corrected to minimize + 'is_screened', # True if the node has been screened; False if not. + 'is_evaluated', # True if node has been evaluated; False if not. + 'num_unbranched_disjunctions', # number of unbranched disjunctions + 'node_count', # cumulative node counter + 'unbranched_disjunction_indices', # list of unbranched disjunction indices + ], +) + +ExternalVarInfo = namedtuple( + 'ExternalVarInfo', + [ + 'exactly_number', # number of external variables for this type + 'Boolean_vars', # list with names of the ordered Boolean variables to be reformulated + 'Disjuncts', # list of disjuncts that are associated with the external variables + # 'Boolean_vars_ordered_index', # Indexes where the external reformulation is applied + 'LogicExpression', # Logic expression that defines the external variables + 'UB', # upper bound on external variable + 'LB', # lower bound on external variable + ], +) + + +@SolverFactory.register( + 'gdpopt.ldsda', + doc="The LBB (logic-based branch and bound) Generalized Disjunctive " + "Programming (GDP) solver", +) +class GDP_LDSDA_Solver(_GDPoptAlgorithm): + """The GDPopt (Generalized Disjunctive Programming optimizer) logic-based + branch and bound (LBB) solver. + + Accepts models that can include nonlinear, continuous variables and + constraints, as well as logical conditions. + """ + + CONFIG = _GDPoptAlgorithm.CONFIG() + _add_mip_solver_configs(CONFIG) + _add_nlp_solver_configs(CONFIG, default_solver='ipopt') + _add_nlp_solve_configs( + CONFIG, default_nlp_init_method=restore_vars_to_original_values + ) + _add_tolerance_configs(CONFIG) + _add_ldsda_configs(CONFIG) + + algorithm = 'LDSDA' + + # Override solve() to customize the docstring for this solver + @document_kwargs_from_configdict(CONFIG, doc=_GDPoptAlgorithm.solve.__doc__) + def solve(self, model, **kwds): + return super().solve(model, **kwds) + + def _log_citation(self, config): + config.logger.info( + "\n" + + """- LDSDA algorithm: + Bernal DE, Ovalle D, Liñán DA, Ricardez-Sandoval LA, Gómez JM, Grossmann IE. + Process Superstructure Optimization through Discrete Steepest Descent Optimization: a GDP Analysis and Applications in Process Intensification. + Computer Aided Chemical Engineering 2022 Jan 1 (Vol. 49, pp. 1279-1284). Elsevier. + https://doi.org/10.1016/B978-0-323-85159-6.50213-X + """.strip() + ) + + def _solve_gdp(self, model, config): + logger = config.logger + self.explored_nodes = 0 + + # Create utility block on the original model so that we will be able to + # copy solutions between + util_block = self.original_util_block = add_util_block(model) + add_disjunct_list(util_block) + add_algebraic_variable_list(util_block) + add_boolean_variable_lists(util_block) + # TODO: LBB uses logical_to_disjunctive, I am not sure if it is necessary for LDSDA. + # util_block.logical_constraint_list_to_be_tranformed = ( + # config.logical_constraint_list + # ) + + self.working_model = model.clone() + # TODO: I don't like the name way, try something else? + self.working_model_util_block = working_model_util_block = ( + self.working_model.component(util_block.name) + ) + + add_disjunction_list(working_model_util_block) + # TODO: do we need to apply logical_to_disjunctive here? + # This is applied in LBB. + # root_node = TransformationFactory( + # 'contrib.logical_to_disjunctive' + # ).create_using(model) + # Now that logical_to_disjunctive has been called. + add_transformed_boolean_variable_list(working_model_util_block) + + self._log_header(logger) + self.working_model_external_var_info_list = self.get_external_information( + working_model_util_block, config + ) + self.directions = self.get_directions(self.number_of_external_variables, config) + self.best_direction = None + self.current_point = config.starting_point + self.explored_point_set = set() + + # Add the BigM suffix if it does not already exist. Used later during + # nonlinear constraint activation. + if not hasattr(working_model_util_block, 'BigM'): + working_model_util_block.BigM = Suffix() + + locally_optimal = False + # Solve the initial point + self.fix_disjunctions_with_external_var( + self.working_model_util_block, self.current_point + ) + _ = self._solve_rnGDP_subproblem(self.working_model, config, 'Initial point') + + # Main loop + while not locally_optimal: + self.iteration += 1 + if self.any_termination_criterion_met(config): + break + locally_optimal = self.neighbor_search(self.working_model, config) + if not locally_optimal: + self.line_search(self.working_model, config) + + print("Optimal solution", self.current_point) + + def any_termination_criterion_met(self, config): + return self.reached_iteration_limit(config) or self.reached_time_limit(config) + + def _solve_rnGDP_subproblem(self, model, config, search_type): + subproblem = model.clone() + TransformationFactory('core.logical_to_linear').apply_to(subproblem) + TransformationFactory('gdp.bigm').apply_to(subproblem) + + try: + with SuppressInfeasibleWarning(): + # TODO: we can use fbbt or deactivate trivial constraints here. + # try: + # fbbt(subproblem, integer_tol=config.integer_tolerance) + # except InfeasibleConstraintException: + # # copy variable values, even if errored + # copy_var_list_values( + # from_list=subprob_utils.algebraic_variable_list, + # to_list=model_utils.algebraic_variable_list, + # config=config, + # ignore_integrality=True, + # ) + # return float('inf'), float('inf') + minlp_args = dict(config.minlp_solver_args) + if config.time_limit is not None and config.minlp_solver == 'gams': + elapsed = get_main_elapsed_time(self.timing) + remaining = max(config.time_limit - elapsed, 1) + minlp_args['add_options'] = minlp_args.get('add_options', []) + minlp_args['add_options'].append('option reslim=%s;' % remaining) + result = SolverFactory(config.minlp_solver).solve( + subproblem, **minlp_args + ) + primal_improved = self.handle_subproblem_result( + result, subproblem, config, search_type + ) + return primal_improved + except RuntimeError as e: + config.logger.warning( + "Solver encountered RuntimeError. Treating as infeasible. " + "Msg: %s\n%s" % (str(e), traceback.format_exc()) + ) + return False + + def get_external_information(self, util_block, config): + """Function that obtains information from the model to perform the reformulation with external variables. + + Parameters + ---------- + util_block : Block + The GDPOPT utility block of the model. + config : ConfigBlock + GDPopt configuration block + + Raises + ------ + ValueError + exactly_number is greater than 1 + """ + + # self.working_model_util_block = [] + # util_block = self.working_model_util_block + util_block.external_var_info_list = [] + model = util_block.parent_block() + # Identify the variables that can be reformulated by performing a loop over logical constraints + # TODO: we can automatically find all Exactly logical constraints in the model. + # However, we cannot link the starting point and the logical constraint. + # for c in util_block.logical_constraint_list: + # if isinstance(c.body, ExactlyExpression): + for constraint_name in config.logical_constraint_list: + # TODO: in the first version, we don't support more than one exactly constraint. + # TODO: if we use component instead of model.find_component, it will fail. + c = model.find_component(constraint_name) + exactly_number = c.body.args[0] + if exactly_number > 1: + raise ValueError("The function only works for exactly_number = 1") + sorted_boolean_var_list = sorted(c.body.args[1:], key=lambda x: x.index()) + util_block.external_var_info_list.append( + ExternalVarInfo( + exactly_number=1, + Boolean_vars=sorted_boolean_var_list, + Disjuncts=[ + boolean_var.get_associated_binary().parent_block() + for boolean_var in sorted_boolean_var_list + ], + LogicExpression=c.body, + UB=len(sorted_boolean_var_list), + LB=1, + ) + ) + self.number_of_external_variables = sum( + external_var_info.exactly_number + for external_var_info in util_block.external_var_info_list + ) + + def fix_disjunctions_with_external_var(self, util_block, external_var_values_list): + """Function that fixes the disjunctions in the model using the values of the external variables. + + Parameters + ---------- + util_block : Block + The GDPOPT utility block of the model. + external_var_values_list : List + The list of values of the external variables + """ + for external_variable_value, external_var_info in zip( + external_var_values_list, util_block.external_var_info_list + ): + for idx, (boolean_var, disjunct) in enumerate( + zip(external_var_info.Boolean_vars, external_var_info.Disjuncts) + ): + if idx == external_variable_value - 1: + disjunct.activate() + boolean_var.fix(True) + disjunct.indicator_var.fix(True) + disjunct.binary_indicator_var.fix(1) + else: + # TODO: maybe we can simplify this. + boolean_var.fix(False) + disjunct.indicator_var.fix(False) + disjunct.binary_indicator_var.fix(0) + disjunct.deactivate() + self.explored_point_set.add(tuple(external_var_values_list)) + + def get_directions(self, dimension, config): + """Function creates the search directions of the given dimension. + + Parameters + ---------- + dimension : int + Dimension of the neighborhood + config : ConfigBlock + GDPopt configuration block + + Returns + ------- + list + the search directions. + """ + if config.direction_norm == 'L2': + directions = [] + for i in range(dimension): + directions.append(tuple([0] * i + [1] + [0] * (dimension - i - 1))) + directions.append(tuple([0] * i + [-1] + [0] * (dimension - i - 1))) + return directions + elif config.direction_norm == 'Linf': + directions = list(it.product([-1, 0, 1], repeat=dimension)) + directions.remove((0,) * dimension) + return directions + + def check_valid_neighbor(self, neighbor, external_var_info_list): + """Function that checks if a given neighbor is valid. + + Parameters + ---------- + neighbor : list + the neighbor + external_var_info_list : list + the list of the external variable information + + Returns + ------- + bool + True if the neighbor is valid, False otherwise + """ + if neighbor in self.explored_point_set: + return False + if all( + external_var_value >= external_var_info.LB + and external_var_value <= external_var_info.UB + for external_var_value, external_var_info in zip( + neighbor, external_var_info_list + ) + ): + return True + else: + return False + + def neighbor_search(self, model, config): + """Function that evaluates a group of given points and returns the best + + Parameters + ---------- + neighbor_list : list + the list of neighbors + model : ConcreteModel + the subproblem model + config : ConfigBlock + GDPopt configuration block + """ + locally_optimal = True + best_neighbor = None + # reset best direction + self.best_direction = None + for direction in self.directions: + neighbor = tuple(map(sum, zip(self.current_point, direction))) + if self.check_valid_neighbor( + neighbor, self.working_model_util_block.external_var_info_list + ): + self.fix_disjunctions_with_external_var( + self.working_model_util_block, neighbor + ) + primal_improved = self._solve_rnGDP_subproblem( + model, config, 'Neighbor search' + ) + if primal_improved: + locally_optimal = False + best_neighbor = neighbor + self.best_direction = direction + if not locally_optimal: + self.current_point = best_neighbor + return locally_optimal + + def line_search(self, model, config): + """Function that performs a line search in a given direction. + + Parameters + ---------- + model : ConcreteModel + the subproblem model + direction : list + the direction + config : ConfigBlock + GDPopt configuration block + """ + primal_improved = True + while primal_improved: + next_point = tuple(map(sum, zip(self.current_point, self.best_direction))) + if not self.check_valid_neighbor( + next_point, self.working_model_util_block.external_var_info_list + ): + break + self.fix_disjunctions_with_external_var( + self.working_model_util_block, next_point + ) + primal_improved = self._solve_rnGDP_subproblem(model, config, 'Line search') + # line_search_improved = self.handle_subproblem_result( + # subproblem_result, subproblem, config, 'Line search' + # ) + if primal_improved: + self.current_point = next_point + print("Line search finished.") + + def handle_subproblem_result( + self, subproblem_result, subproblem, config, search_type + ): + """Function that handles the result of the subproblem + + Parameters + ---------- + subproblem : ConcreteModel + the subproblem model + subproblem_result : tuple + the result of the subproblem + config : ConfigBlock + GDPopt configuration block + + Returns + ------- + bool + True if the result improved the current point, False otherwise + """ + if subproblem_result is None: + return False + if subproblem_result.solver.termination_condition in { + tc.optimal, + tc.feasible, + tc.globallyOptimal, + tc.locallyOptimal, + tc.maxTimeLimit, + tc.maxIterations, + tc.maxEvaluations, + }: + primal_bound = ( + subproblem_result.problem.upper_bound + if self.objective_sense == minimize + else subproblem_result.problem.lower_bound + ) + primal_improved = self._update_bounds_after_solve( + search_type, primal=primal_bound, logger=config.logger + ) + if primal_improved: + self.update_incumbent( + subproblem.component(self.original_util_block.name) + ) + return primal_improved + return False + + def _log_header(self, logger): + logger.info( + '=================================================================' + '============================' + ) + logger.info( + '{:^9} | {:^15} | {:^11} | {:^11} | {:^8} | {:^7}\n'.format( + 'Iteration', + 'Search Type', + 'Lower Bound', + 'Upper Bound', + ' Gap ', + 'Time(s)', + ) + ) diff --git a/pyomo/contrib/gdpopt/plugins.py b/pyomo/contrib/gdpopt/plugins.py index 9d729c63d9c..3262dd65458 100644 --- a/pyomo/contrib/gdpopt/plugins.py +++ b/pyomo/contrib/gdpopt/plugins.py @@ -17,3 +17,4 @@ def load(): import pyomo.contrib.gdpopt.loa import pyomo.contrib.gdpopt.ric import pyomo.contrib.gdpopt.enumerate + import pyomo.contrib.gdpopt.ldsda From 1c9686ab981ca7d0a5a3605e789c8bfbe6c3c54d Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Sun, 4 Feb 2024 22:21:46 -0500 Subject: [PATCH 0437/3044] remove unused code --- pyomo/contrib/gdpopt/ldsda.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyomo/contrib/gdpopt/ldsda.py b/pyomo/contrib/gdpopt/ldsda.py index a732ece680e..c3f6e2db6c3 100644 --- a/pyomo/contrib/gdpopt/ldsda.py +++ b/pyomo/contrib/gdpopt/ldsda.py @@ -137,10 +137,6 @@ def _solve_gdp(self, model, config): add_disjunct_list(util_block) add_algebraic_variable_list(util_block) add_boolean_variable_lists(util_block) - # TODO: LBB uses logical_to_disjunctive, I am not sure if it is necessary for LDSDA. - # util_block.logical_constraint_list_to_be_tranformed = ( - # config.logical_constraint_list - # ) self.working_model = model.clone() # TODO: I don't like the name way, try something else? From 1ac75d28b6d65fa6ebd86036c772c572b2ca5599 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Sun, 4 Feb 2024 23:03:53 -0500 Subject: [PATCH 0438/3044] use attempt_import for itertools --- pyomo/contrib/gdpopt/ldsda.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/gdpopt/ldsda.py b/pyomo/contrib/gdpopt/ldsda.py index c3f6e2db6c3..ead1f6f3424 100644 --- a/pyomo/contrib/gdpopt/ldsda.py +++ b/pyomo/contrib/gdpopt/ldsda.py @@ -53,8 +53,9 @@ from pyomo.opt import SolverFactory, SolverStatus from pyomo.opt import TerminationCondition as tc from pyomo.core.expr.logical_expr import ExactlyExpression -import numpy as np # attempt_import -import itertools as it # attempt_import +from pyomo.common.dependencies import attempt_import + +it, it_available = attempt_import('itertools') _linear_degrees = {1, 0} From 5f8fb1e242b06247316d9d735e77ee801794f936 Mon Sep 17 00:00:00 2001 From: robbybp Date: Sun, 4 Feb 2024 23:17:32 -0700 Subject: [PATCH 0439/3044] initial implementation of IncidenceGraphInterface.subgraph method --- pyomo/contrib/incidence_analysis/interface.py | 70 +++++++++++++------ 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index b8a6c1275f9..4c970045814 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -138,31 +138,39 @@ def extract_bipartite_subgraph(graph, nodes0, nodes1): in the original graph. """ - subgraph = nx.Graph() - sub_M = len(nodes0) - sub_N = len(nodes1) - subgraph.add_nodes_from(range(sub_M), bipartite=0) - subgraph.add_nodes_from(range(sub_M, sub_M + sub_N), bipartite=1) - + subgraph = graph.subgraph(nodes0 + nodes1) old_new_map = {} for i, node in enumerate(nodes0 + nodes1): if node in old_new_map: raise RuntimeError("Node %s provided more than once.") old_new_map[node] = i - - for node1, node2 in graph.edges(): - if node1 in old_new_map and node2 in old_new_map: - new_node_1 = old_new_map[node1] - new_node_2 = old_new_map[node2] - if ( - subgraph.nodes[new_node_1]["bipartite"] - == subgraph.nodes[new_node_2]["bipartite"] - ): - raise RuntimeError( - "Subgraph is not bipartite. Found an edge between nodes" - " %s and %s (in the original graph)." % (node1, node2) - ) - subgraph.add_edge(new_node_1, new_node_2) + relabeled_subgraph = nx.relabel_nodes(subgraph, old_new_map) + return relabeled_subgraph + #subgraph = nx.Graph() + #sub_M = len(nodes0) + #sub_N = len(nodes1) + #subgraph.add_nodes_from(range(sub_M), bipartite=0) + #subgraph.add_nodes_from(range(sub_M, sub_M + sub_N), bipartite=1) + + #old_new_map = {} + #for i, node in enumerate(nodes0 + nodes1): + # if node in old_new_map: + # raise RuntimeError("Node %s provided more than once.") + # old_new_map[node] = i + + #for node1, node2 in graph.edges(): + # if node1 in old_new_map and node2 in old_new_map: + # new_node_1 = old_new_map[node1] + # new_node_2 = old_new_map[node2] + # if ( + # subgraph.nodes[new_node_1]["bipartite"] + # == subgraph.nodes[new_node_2]["bipartite"] + # ): + # raise RuntimeError( + # "Subgraph is not bipartite. Found an edge between nodes" + # " %s and %s (in the original graph)." % (node1, node2) + # ) + # subgraph.add_edge(new_node_1, new_node_2) return subgraph @@ -334,6 +342,22 @@ def __init__(self, model=None, active=True, include_inequality=True, **kwds): incidence_matrix = nlp.evaluate_jacobian_eq() nxb = nx.algorithms.bipartite self._incidence_graph = nxb.from_biadjacency_matrix(incidence_matrix) + elif isinstance(model, tuple): + # model is a tuple of (nx.Graph, list[pyo.Var], list[pyo.Constraint]) + # We could potentially accept a tuple (variables, constraints). + # TODO: Disallow kwargs if this type of "model" is provided? + nx_graph, variables, constraints = model + self._variables = list(variables) + self._constraints = list(constraints) + self._var_index_map = ComponentMap( + (var, i) for i, var in enumerate(self._variables) + ) + self._con_index_map = ComponentMap( + (con, i) for i, con in enumerate(self._constraints) + ) + # For now, don't check any properties of this graph. We could check + # for a bipartition that matches the variable and constraint lists. + self._incidence_graph = nx_graph else: raise TypeError( "Unsupported type for incidence graph. Expected PyomoNLP" @@ -468,6 +492,12 @@ def _extract_subgraph(self, variables, constraints): ) return subgraph + def subgraph(self, variables, constraints): + # TODO: copy=True argument we can use to optionally modify in-place? + nx_subgraph = self._extract_subgraph(variables, constraints) + subgraph = IncidenceGraphInterface((nx_subgraph, variables, constraints), **self._config) + return subgraph + @property def incidence_matrix(self): """The structural incidence matrix of variables and constraints. From f0c538f1690f01107a9dfc6fbf1a17eb06422bef Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 5 Feb 2024 00:33:11 -0700 Subject: [PATCH 0440/3044] remove unused code from extract_bipartite_subgraph --- pyomo/contrib/incidence_analysis/interface.py | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 4c970045814..6680c32d7c2 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -139,6 +139,7 @@ def extract_bipartite_subgraph(graph, nodes0, nodes1): """ subgraph = graph.subgraph(nodes0 + nodes1) + # TODO: Any error checking that nodes are valid bipartition? old_new_map = {} for i, node in enumerate(nodes0 + nodes1): if node in old_new_map: @@ -146,32 +147,6 @@ def extract_bipartite_subgraph(graph, nodes0, nodes1): old_new_map[node] = i relabeled_subgraph = nx.relabel_nodes(subgraph, old_new_map) return relabeled_subgraph - #subgraph = nx.Graph() - #sub_M = len(nodes0) - #sub_N = len(nodes1) - #subgraph.add_nodes_from(range(sub_M), bipartite=0) - #subgraph.add_nodes_from(range(sub_M, sub_M + sub_N), bipartite=1) - - #old_new_map = {} - #for i, node in enumerate(nodes0 + nodes1): - # if node in old_new_map: - # raise RuntimeError("Node %s provided more than once.") - # old_new_map[node] = i - - #for node1, node2 in graph.edges(): - # if node1 in old_new_map and node2 in old_new_map: - # new_node_1 = old_new_map[node1] - # new_node_2 = old_new_map[node2] - # if ( - # subgraph.nodes[new_node_1]["bipartite"] - # == subgraph.nodes[new_node_2]["bipartite"] - # ): - # raise RuntimeError( - # "Subgraph is not bipartite. Found an edge between nodes" - # " %s and %s (in the original graph)." % (node1, node2) - # ) - # subgraph.add_edge(new_node_1, new_node_2) - return subgraph def _generate_variables_in_constraints(constraints, **kwds): From 19bda95945964588fee8e161ea281f60dc955d62 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 5 Feb 2024 00:33:24 -0700 Subject: [PATCH 0441/3044] test for subgraph method --- .../tests/test_interface.py | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 490ea94f63c..8a0049d4225 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1651,11 +1651,12 @@ def test_extract_exceptions(self): variables = list(m.v.values()) graph = get_bipartite_incidence_graph(variables, constraints) - sg_cons = [0, 2, 5] - sg_vars = [i + len(constraints) for i in [2, 3]] - msg = "Subgraph is not bipartite" - with self.assertRaisesRegex(RuntimeError, msg): - subgraph = extract_bipartite_subgraph(graph, sg_cons, sg_vars) + # TODO: Fix this test + #sg_cons = [0, 2, 5] + #sg_vars = [i + len(constraints) for i in [2, 3]] + #msg = "Subgraph is not bipartite" + #with self.assertRaisesRegex(RuntimeError, msg): + # subgraph = extract_bipartite_subgraph(graph, sg_cons, sg_vars) sg_cons = [0, 2, 5] sg_vars = [i + len(constraints) for i in [2, 0, 3]] @@ -1791,6 +1792,33 @@ def test_linear_only(self): self.assertIs(matching[m.eq2], m.x[2]) self.assertIs(matching[m.eq3], m.x[3]) + def test_subgraph(self): + m = pyo.ConcreteModel() + m.I = pyo.Set(initialize=[1, 2, 3, 4]) + m.v = pyo.Var(m.I, bounds=(0, None)) + m.eq1 = pyo.Constraint(expr=m.v[1] ** 2 + m.v[2] ** 2 == 1.0) + m.eq2 = pyo.Constraint(expr=m.v[1] + 2.0 == m.v[3]) + m.ineq1 = pyo.Constraint(expr=m.v[2] - m.v[3] ** 0.5 + m.v[4] ** 2 <= 1.0) + m.ineq2 = pyo.Constraint(expr=m.v[2] * m.v[4] >= 1.0) + m.ineq3 = pyo.Constraint(expr=m.v[1] >= m.v[4] ** 4) + m.obj = pyo.Objective(expr=-m.v[1] - m.v[2] + m.v[3] ** 2 + m.v[4] ** 2) + igraph = IncidenceGraphInterface(m) + eq_igraph = igraph.subgraph(igraph.variables, [m.eq1, m.eq2]) + for i in range(len(igraph.variables)): + self.assertIs(igraph.variables[i], eq_igraph.variables[i]) + self.assertEqual( + ComponentSet(eq_igraph.constraints), ComponentSet([m.eq1, m.eq2]) + ) + + subgraph = eq_igraph.subgraph([m.v[1], m.v[3]], [m.eq1, m.eq2]) + self.assertEqual( + ComponentSet(subgraph.get_adjacent_to(m.eq2)), + ComponentSet([m.v[1], m.v[3]]), + ) + self.assertEqual( + ComponentSet(subgraph.get_adjacent_to(m.eq1)), ComponentSet([m.v[1]]), + ) + @unittest.skipUnless(networkx_available, "networkx is not available.") class TestIndexedBlock(unittest.TestCase): From adfc543979d655a2a906185b07df509b41bc28bb Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 5 Feb 2024 00:37:32 -0700 Subject: [PATCH 0442/3044] reimplement error checking for bad bipartite sets and update test for new error message --- pyomo/contrib/incidence_analysis/interface.py | 14 ++++++++++++++ .../incidence_analysis/tests/test_interface.py | 13 ++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 6680c32d7c2..73264a0e113 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -140,6 +140,20 @@ def extract_bipartite_subgraph(graph, nodes0, nodes1): """ subgraph = graph.subgraph(nodes0 + nodes1) # TODO: Any error checking that nodes are valid bipartition? + for node in nodes0: + bipartite = graph.nodes[node]["bipartite"] + if bipartite != 0: + raise RuntimeError( + "Invalid bipartite sets. Node {node} in set 0 has" + " bipartite={bipartite}" + ) + for node in nodes1: + bipartite = graph.nodes[node]["bipartite"] + if bipartite != 1: + raise RuntimeError( + "Invalid bipartite sets. Node {node} in set 1 has" + " bipartite={bipartite}" + ) old_new_map = {} for i, node in enumerate(nodes0 + nodes1): if node in old_new_map: diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 8a0049d4225..c7a74ae5784 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1651,14 +1651,13 @@ def test_extract_exceptions(self): variables = list(m.v.values()) graph = get_bipartite_incidence_graph(variables, constraints) - # TODO: Fix this test - #sg_cons = [0, 2, 5] - #sg_vars = [i + len(constraints) for i in [2, 3]] - #msg = "Subgraph is not bipartite" - #with self.assertRaisesRegex(RuntimeError, msg): - # subgraph = extract_bipartite_subgraph(graph, sg_cons, sg_vars) - sg_cons = [0, 2, 5] + sg_vars = [i + len(constraints) for i in [2, 3]] + msg = "Invalid bipartite sets." + with self.assertRaisesRegex(RuntimeError, msg): + subgraph = extract_bipartite_subgraph(graph, sg_cons, sg_vars) + + sg_cons = [0, 2, 0] sg_vars = [i + len(constraints) for i in [2, 0, 3]] msg = "provided more than once" with self.assertRaisesRegex(RuntimeError, msg): From da47a8bed30685e6a4b552e9afd65d50fa032a27 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 5 Feb 2024 00:42:11 -0700 Subject: [PATCH 0443/3044] docstring for subgraph method --- pyomo/contrib/incidence_analysis/interface.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 73264a0e113..d1eb90efbd7 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -482,7 +482,18 @@ def _extract_subgraph(self, variables, constraints): return subgraph def subgraph(self, variables, constraints): - # TODO: copy=True argument we can use to optionally modify in-place? + """Extract a subgraph defined by the provided variables and constraints + + Underlying data structures are copied, and constraints are not reinspected + for incidence variables (the edges from this incidence graph are used). + + Returns + ------- + ``IncidenceGraphInterface`` + A new incidence graph containing only the specified variables and + constraints, and the edges between pairs thereof. + + """ nx_subgraph = self._extract_subgraph(variables, constraints) subgraph = IncidenceGraphInterface((nx_subgraph, variables, constraints), **self._config) return subgraph From f4e9c974289f3991c63b6d122efbedda1e0f009c Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 5 Feb 2024 00:44:25 -0700 Subject: [PATCH 0444/3044] apply black --- pyomo/contrib/incidence_analysis/interface.py | 4 +++- pyomo/contrib/incidence_analysis/tests/test_interface.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index d1eb90efbd7..04b9e3c70f7 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -495,7 +495,9 @@ def subgraph(self, variables, constraints): """ nx_subgraph = self._extract_subgraph(variables, constraints) - subgraph = IncidenceGraphInterface((nx_subgraph, variables, constraints), **self._config) + subgraph = IncidenceGraphInterface( + (nx_subgraph, variables, constraints), **self._config + ) return subgraph @property diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index c7a74ae5784..2769a46a907 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1815,7 +1815,8 @@ def test_subgraph(self): ComponentSet([m.v[1], m.v[3]]), ) self.assertEqual( - ComponentSet(subgraph.get_adjacent_to(m.eq1)), ComponentSet([m.v[1]]), + ComponentSet(subgraph.get_adjacent_to(m.eq1)), + ComponentSet([m.v[1]]), ) From 149680e48dc773cf9caf19434d934d4f7128c1b1 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 5 Feb 2024 00:57:06 -0700 Subject: [PATCH 0445/3044] use the whole line to keep black happy --- pyomo/contrib/incidence_analysis/tests/test_interface.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 2769a46a907..10777a35f78 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1815,8 +1815,7 @@ def test_subgraph(self): ComponentSet([m.v[1], m.v[3]]), ) self.assertEqual( - ComponentSet(subgraph.get_adjacent_to(m.eq1)), - ComponentSet([m.v[1]]), + ComponentSet(subgraph.get_adjacent_to(m.eq1)), ComponentSet([m.v[1]]) ) From cdde7d66f77d7f9a77e9c0b11216f9e629e59b3e Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 5 Feb 2024 10:49:11 -0500 Subject: [PATCH 0446/3044] remove unused code --- pyomo/contrib/gdpopt/ldsda.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/pyomo/contrib/gdpopt/ldsda.py b/pyomo/contrib/gdpopt/ldsda.py index ead1f6f3424..c4aabdfef84 100644 --- a/pyomo/contrib/gdpopt/ldsda.py +++ b/pyomo/contrib/gdpopt/ldsda.py @@ -57,30 +57,13 @@ it, it_available = attempt_import('itertools') -_linear_degrees = {1, 0} - -# Data tuple for each node that also functions as the sort key. -# Therefore, ordering of the arguments below matters. -BBNodeData = namedtuple( - 'BBNodeData', - [ - 'obj_lb', # lower bound on objective value, sign corrected to minimize - 'obj_ub', # upper bound on objective value, sign corrected to minimize - 'is_screened', # True if the node has been screened; False if not. - 'is_evaluated', # True if node has been evaluated; False if not. - 'num_unbranched_disjunctions', # number of unbranched disjunctions - 'node_count', # cumulative node counter - 'unbranched_disjunction_indices', # list of unbranched disjunction indices - ], -) - +# Data tuple for external variables. ExternalVarInfo = namedtuple( 'ExternalVarInfo', [ 'exactly_number', # number of external variables for this type 'Boolean_vars', # list with names of the ordered Boolean variables to be reformulated 'Disjuncts', # list of disjuncts that are associated with the external variables - # 'Boolean_vars_ordered_index', # Indexes where the external reformulation is applied 'LogicExpression', # Logic expression that defines the external variables 'UB', # upper bound on external variable 'LB', # lower bound on external variable From 2aaad10c986cd804bba760645896b17496501220 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 5 Feb 2024 11:04:14 -0500 Subject: [PATCH 0447/3044] remove the unused code --- pyomo/contrib/gdpopt/ldsda.py | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/gdpopt/ldsda.py b/pyomo/contrib/gdpopt/ldsda.py index c4aabdfef84..94fcb0de8e7 100644 --- a/pyomo/contrib/gdpopt/ldsda.py +++ b/pyomo/contrib/gdpopt/ldsda.py @@ -73,12 +73,12 @@ @SolverFactory.register( 'gdpopt.ldsda', - doc="The LBB (logic-based branch and bound) Generalized Disjunctive " - "Programming (GDP) solver", + doc="The LD-SDA (Logic-based Discrete-Steepest Descent Algorithm)" + "Generalized Disjunctive Programming (GDP) solver", ) class GDP_LDSDA_Solver(_GDPoptAlgorithm): - """The GDPopt (Generalized Disjunctive Programming optimizer) logic-based - branch and bound (LBB) solver. + """The GDPopt (Generalized Disjunctive Programming optimizer) + LD-SDA (Logic-based Discrete-Steepest Descent (LD-SDA) solver. Accepts models that can include nonlinear, continuous variables and constraints, as well as logical conditions. @@ -124,22 +124,20 @@ def _solve_gdp(self, model, config): self.working_model = model.clone() # TODO: I don't like the name way, try something else? - self.working_model_util_block = working_model_util_block = ( - self.working_model.component(util_block.name) - ) + self.working_model_util_block = self.working_model.component(util_block.name) - add_disjunction_list(working_model_util_block) + add_disjunction_list(self.working_model_util_block) # TODO: do we need to apply logical_to_disjunctive here? # This is applied in LBB. # root_node = TransformationFactory( # 'contrib.logical_to_disjunctive' # ).create_using(model) # Now that logical_to_disjunctive has been called. - add_transformed_boolean_variable_list(working_model_util_block) + add_transformed_boolean_variable_list(self.working_model_util_block) self._log_header(logger) self.working_model_external_var_info_list = self.get_external_information( - working_model_util_block, config + self.working_model_util_block, config ) self.directions = self.get_directions(self.number_of_external_variables, config) self.best_direction = None @@ -148,8 +146,8 @@ def _solve_gdp(self, model, config): # Add the BigM suffix if it does not already exist. Used later during # nonlinear constraint activation. - if not hasattr(working_model_util_block, 'BigM'): - working_model_util_block.BigM = Suffix() + if not hasattr(self.working_model_util_block, 'BigM'): + self.working_model_util_block.BigM = Suffix() locally_optimal = False # Solve the initial point @@ -226,9 +224,6 @@ def get_external_information(self, util_block, config): ValueError exactly_number is greater than 1 """ - - # self.working_model_util_block = [] - # util_block = self.working_model_util_block util_block.external_var_info_list = [] model = util_block.parent_block() # Identify the variables that can be reformulated by performing a loop over logical constraints @@ -350,8 +345,6 @@ def neighbor_search(self, model, config): Parameters ---------- - neighbor_list : list - the list of neighbors model : ConcreteModel the subproblem model config : ConfigBlock @@ -387,8 +380,6 @@ def line_search(self, model, config): ---------- model : ConcreteModel the subproblem model - direction : list - the direction config : ConfigBlock GDPopt configuration block """ @@ -403,9 +394,6 @@ def line_search(self, model, config): self.working_model_util_block, next_point ) primal_improved = self._solve_rnGDP_subproblem(model, config, 'Line search') - # line_search_improved = self.handle_subproblem_result( - # subproblem_result, subproblem, config, 'Line search' - # ) if primal_improved: self.current_point = next_point print("Line search finished.") From 0b93e7c5c550273b6053b5894a52370943c6c788 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 5 Feb 2024 11:06:14 -0500 Subject: [PATCH 0448/3044] remove unused import --- pyomo/contrib/gdpopt/ldsda.py | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/gdpopt/ldsda.py b/pyomo/contrib/gdpopt/ldsda.py index 94fcb0de8e7..e15e2756afb 100644 --- a/pyomo/contrib/gdpopt/ldsda.py +++ b/pyomo/contrib/gdpopt/ldsda.py @@ -10,10 +10,7 @@ # ___________________________________________________________________________ from collections import namedtuple -from heapq import heappush, heappop import traceback - -from pyomo.common.collections import ComponentMap from pyomo.common.config import document_kwargs_from_configdict from pyomo.common.errors import InfeasibleConstraintException from pyomo.contrib.fbbt.fbbt import fbbt @@ -28,29 +25,16 @@ ) from pyomo.contrib.gdpopt.config_options import ( _add_nlp_solver_configs, - _add_BB_configs, _add_ldsda_configs, _add_mip_solver_configs, _add_tolerance_configs, _add_nlp_solve_configs, ) from pyomo.contrib.gdpopt.nlp_initialization import restore_vars_to_original_values -from pyomo.contrib.gdpopt.util import ( - copy_var_list_values, - SuppressInfeasibleWarning, - get_main_elapsed_time, -) +from pyomo.contrib.gdpopt.util import SuppressInfeasibleWarning, get_main_elapsed_time from pyomo.contrib.satsolver.satsolver import satisfiable -from pyomo.core import ( - minimize, - Suffix, - Constraint, - TransformationFactory, - BooleanVar, - Var, - Objective, -) -from pyomo.opt import SolverFactory, SolverStatus +from pyomo.core import minimize, Suffix, TransformationFactory +from pyomo.opt import SolverFactory from pyomo.opt import TerminationCondition as tc from pyomo.core.expr.logical_expr import ExactlyExpression from pyomo.common.dependencies import attempt_import From 5cc9a4b865ac362d9d3db600875ae2ff38aa2a6b Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 5 Feb 2024 11:16:40 -0500 Subject: [PATCH 0449/3044] improve the LDSDA implementation --- pyomo/contrib/gdpopt/ldsda.py | 37 +++++++++++++++-------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/pyomo/contrib/gdpopt/ldsda.py b/pyomo/contrib/gdpopt/ldsda.py index e15e2756afb..e4466822dcf 100644 --- a/pyomo/contrib/gdpopt/ldsda.py +++ b/pyomo/contrib/gdpopt/ldsda.py @@ -120,9 +120,7 @@ def _solve_gdp(self, model, config): add_transformed_boolean_variable_list(self.working_model_util_block) self._log_header(logger) - self.working_model_external_var_info_list = self.get_external_information( - self.working_model_util_block, config - ) + self.get_external_information(self.working_model_util_block, config) self.directions = self.get_directions(self.number_of_external_variables, config) self.best_direction = None self.current_point = config.starting_point @@ -133,7 +131,6 @@ def _solve_gdp(self, model, config): if not hasattr(self.working_model_util_block, 'BigM'): self.working_model_util_block.BigM = Suffix() - locally_optimal = False # Solve the initial point self.fix_disjunctions_with_external_var( self.working_model_util_block, self.current_point @@ -141,6 +138,7 @@ def _solve_gdp(self, model, config): _ = self._solve_rnGDP_subproblem(self.working_model, config, 'Initial point') # Main loop + locally_optimal = False while not locally_optimal: self.iteration += 1 if self.any_termination_criterion_met(config): @@ -296,15 +294,13 @@ def get_directions(self, dimension, config): directions.remove((0,) * dimension) return directions - def check_valid_neighbor(self, neighbor, external_var_info_list): + def check_valid_neighbor(self, neighbor): """Function that checks if a given neighbor is valid. Parameters ---------- neighbor : list - the neighbor - external_var_info_list : list - the list of the external variable information + the neighbor to be checked Returns ------- @@ -317,7 +313,7 @@ def check_valid_neighbor(self, neighbor, external_var_info_list): external_var_value >= external_var_info.LB and external_var_value <= external_var_info.UB for external_var_value, external_var_info in zip( - neighbor, external_var_info_list + neighbor, self.working_model_util_block.external_var_info_list ) ): return True @@ -340,9 +336,7 @@ def neighbor_search(self, model, config): self.best_direction = None for direction in self.directions: neighbor = tuple(map(sum, zip(self.current_point, direction))) - if self.check_valid_neighbor( - neighbor, self.working_model_util_block.external_var_info_list - ): + if self.check_valid_neighbor(neighbor): self.fix_disjunctions_with_external_var( self.working_model_util_block, neighbor ) @@ -370,16 +364,17 @@ def line_search(self, model, config): primal_improved = True while primal_improved: next_point = tuple(map(sum, zip(self.current_point, self.best_direction))) - if not self.check_valid_neighbor( - next_point, self.working_model_util_block.external_var_info_list - ): + if self.check_valid_neighbor(next_point): + self.fix_disjunctions_with_external_var( + self.working_model_util_block, next_point + ) + primal_improved = self._solve_rnGDP_subproblem( + model, config, 'Line search' + ) + if primal_improved: + self.current_point = next_point + else: break - self.fix_disjunctions_with_external_var( - self.working_model_util_block, next_point - ) - primal_improved = self._solve_rnGDP_subproblem(model, config, 'Line search') - if primal_improved: - self.current_point = next_point print("Line search finished.") def handle_subproblem_result( From 6cf2e6b42413ec6eb8425e1a73cc550a3f937126 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 5 Feb 2024 12:35:01 -0500 Subject: [PATCH 0450/3044] rename _solve_rnGDP_subproblem to _solve_GDP_subproblem --- pyomo/contrib/gdpopt/ldsda.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/gdpopt/ldsda.py b/pyomo/contrib/gdpopt/ldsda.py index e4466822dcf..340c46dae2e 100644 --- a/pyomo/contrib/gdpopt/ldsda.py +++ b/pyomo/contrib/gdpopt/ldsda.py @@ -106,6 +106,9 @@ def _solve_gdp(self, model, config): add_algebraic_variable_list(util_block) add_boolean_variable_lists(util_block) + # We will use the working_model to clone the model and perform + # to transform the ordered boolean variables into external integer variables, + # fix the disjunctions. self.working_model = model.clone() # TODO: I don't like the name way, try something else? self.working_model_util_block = self.working_model.component(util_block.name) @@ -135,7 +138,7 @@ def _solve_gdp(self, model, config): self.fix_disjunctions_with_external_var( self.working_model_util_block, self.current_point ) - _ = self._solve_rnGDP_subproblem(self.working_model, config, 'Initial point') + _ = self._solve_GDP_subproblem(self.working_model, config, 'Initial point') # Main loop locally_optimal = False @@ -152,7 +155,7 @@ def _solve_gdp(self, model, config): def any_termination_criterion_met(self, config): return self.reached_iteration_limit(config) or self.reached_time_limit(config) - def _solve_rnGDP_subproblem(self, model, config, search_type): + def _solve_GDP_subproblem(self, model, config, search_type): subproblem = model.clone() TransformationFactory('core.logical_to_linear').apply_to(subproblem) TransformationFactory('gdp.bigm').apply_to(subproblem) @@ -340,7 +343,7 @@ def neighbor_search(self, model, config): self.fix_disjunctions_with_external_var( self.working_model_util_block, neighbor ) - primal_improved = self._solve_rnGDP_subproblem( + primal_improved = self._solve_GDP_subproblem( model, config, 'Neighbor search' ) if primal_improved: @@ -368,7 +371,7 @@ def line_search(self, model, config): self.fix_disjunctions_with_external_var( self.working_model_util_block, next_point ) - primal_improved = self._solve_rnGDP_subproblem( + primal_improved = self._solve_GDP_subproblem( model, config, 'Line search' ) if primal_improved: From c4721b5a90b1f13e78fd36f4e4b5fb7534ceb495 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 5 Feb 2024 12:41:24 -0500 Subject: [PATCH 0451/3044] add _ for private method --- pyomo/contrib/gdpopt/ldsda.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/gdpopt/ldsda.py b/pyomo/contrib/gdpopt/ldsda.py index 340c46dae2e..e4904c4e2b8 100644 --- a/pyomo/contrib/gdpopt/ldsda.py +++ b/pyomo/contrib/gdpopt/ldsda.py @@ -123,8 +123,10 @@ def _solve_gdp(self, model, config): add_transformed_boolean_variable_list(self.working_model_util_block) self._log_header(logger) - self.get_external_information(self.working_model_util_block, config) - self.directions = self.get_directions(self.number_of_external_variables, config) + self._get_external_information(self.working_model_util_block, config) + self.directions = self._get_directions( + self.number_of_external_variables, config + ) self.best_direction = None self.current_point = config.starting_point self.explored_point_set = set() @@ -183,7 +185,7 @@ def _solve_GDP_subproblem(self, model, config, search_type): result = SolverFactory(config.minlp_solver).solve( subproblem, **minlp_args ) - primal_improved = self.handle_subproblem_result( + primal_improved = self._handle_subproblem_result( result, subproblem, config, search_type ) return primal_improved @@ -194,7 +196,7 @@ def _solve_GDP_subproblem(self, model, config, search_type): ) return False - def get_external_information(self, util_block, config): + def _get_external_information(self, util_block, config): """Function that obtains information from the model to perform the reformulation with external variables. Parameters @@ -271,7 +273,7 @@ def fix_disjunctions_with_external_var(self, util_block, external_var_values_lis disjunct.deactivate() self.explored_point_set.add(tuple(external_var_values_list)) - def get_directions(self, dimension, config): + def _get_directions(self, dimension, config): """Function creates the search directions of the given dimension. Parameters @@ -297,7 +299,7 @@ def get_directions(self, dimension, config): directions.remove((0,) * dimension) return directions - def check_valid_neighbor(self, neighbor): + def _check_valid_neighbor(self, neighbor): """Function that checks if a given neighbor is valid. Parameters @@ -339,7 +341,7 @@ def neighbor_search(self, model, config): self.best_direction = None for direction in self.directions: neighbor = tuple(map(sum, zip(self.current_point, direction))) - if self.check_valid_neighbor(neighbor): + if self._check_valid_neighbor(neighbor): self.fix_disjunctions_with_external_var( self.working_model_util_block, neighbor ) @@ -367,7 +369,7 @@ def line_search(self, model, config): primal_improved = True while primal_improved: next_point = tuple(map(sum, zip(self.current_point, self.best_direction))) - if self.check_valid_neighbor(next_point): + if self._check_valid_neighbor(next_point): self.fix_disjunctions_with_external_var( self.working_model_util_block, next_point ) @@ -380,7 +382,7 @@ def line_search(self, model, config): break print("Line search finished.") - def handle_subproblem_result( + def _handle_subproblem_result( self, subproblem_result, subproblem, config, search_type ): """Function that handles the result of the subproblem From 3760de2e36254457962d06b53d92046958ffa911 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 5 Feb 2024 11:39:45 -0700 Subject: [PATCH 0452/3044] Remove unneeded import --- pyomo/core/expr/logical_expr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/expr/logical_expr.py b/pyomo/core/expr/logical_expr.py index 875f5107f3a..48daa79a5b3 100644 --- a/pyomo/core/expr/logical_expr.py +++ b/pyomo/core/expr/logical_expr.py @@ -11,7 +11,7 @@ # ___________________________________________________________________________ import types -from itertools import combinations, islice +from itertools import islice import logging import traceback From 688a990028edd7b34fedcd9f3efa3efe5b34deaf Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 5 Feb 2024 15:01:56 -0500 Subject: [PATCH 0453/3044] add the information of external variables in the log --- pyomo/contrib/gdpopt/ldsda.py | 141 ++++++++++++++++++++++------------ 1 file changed, 94 insertions(+), 47 deletions(-) diff --git a/pyomo/contrib/gdpopt/ldsda.py b/pyomo/contrib/gdpopt/ldsda.py index e4904c4e2b8..32013533139 100644 --- a/pyomo/contrib/gdpopt/ldsda.py +++ b/pyomo/contrib/gdpopt/ldsda.py @@ -96,8 +96,22 @@ def _log_citation(self, config): ) def _solve_gdp(self, model, config): + """Solve the GDP model. + + Parameters + ---------- + model : ConcreteModel + The GDP model to be solved + config : ConfigBlock + GDPopt configuration block + """ logger = config.logger - self.explored_nodes = 0 + self.log_formatter = ( + '{:>9} {:>15} {:>20} {:>11.5f} {:>11.5f} {:>8.2%} {:>7.2f} {}' + ) + self.best_direction = None + self.current_point = tuple(config.starting_point) + self.explored_point_set = set() # Create utility block on the original model so that we will be able to # copy solutions between @@ -106,9 +120,7 @@ def _solve_gdp(self, model, config): add_algebraic_variable_list(util_block) add_boolean_variable_lists(util_block) - # We will use the working_model to clone the model and perform - # to transform the ordered boolean variables into external integer variables, - # fix the disjunctions. + # We will use the working_model to perform the LDSDA search. self.working_model = model.clone() # TODO: I don't like the name way, try something else? self.working_model_util_block = self.working_model.component(util_block.name) @@ -127,9 +139,6 @@ def _solve_gdp(self, model, config): self.directions = self._get_directions( self.number_of_external_variables, config ) - self.best_direction = None - self.current_point = config.starting_point - self.explored_point_set = set() # Add the BigM suffix if it does not already exist. Used later during # nonlinear constraint activation. @@ -137,10 +146,7 @@ def _solve_gdp(self, model, config): self.working_model_util_block.BigM = Suffix() # Solve the initial point - self.fix_disjunctions_with_external_var( - self.working_model_util_block, self.current_point - ) - _ = self._solve_GDP_subproblem(self.working_model, config, 'Initial point') + _ = self._solve_GDP_subproblem(self.current_point, 'Initial point', config) # Main loop locally_optimal = False @@ -148,17 +154,32 @@ def _solve_gdp(self, model, config): self.iteration += 1 if self.any_termination_criterion_met(config): break - locally_optimal = self.neighbor_search(self.working_model, config) + locally_optimal = self.neighbor_search(config) if not locally_optimal: - self.line_search(self.working_model, config) - - print("Optimal solution", self.current_point) + self.line_search(config) def any_termination_criterion_met(self, config): return self.reached_iteration_limit(config) or self.reached_time_limit(config) - def _solve_GDP_subproblem(self, model, config, search_type): - subproblem = model.clone() + def _solve_GDP_subproblem(self, external_var_value, search_type, config): + """Solve the GDP subproblem with disjunctions fixed according to the external variable. + + Parameters + ---------- + external_var_value : list + The values of the external variables to be evaluated + search_type : str + The type of search, neighbor search or line search + config : ConfigBlock + GDPopt configuration block + + Returns + ------- + bool + weather the primal bound is improved + """ + self.fix_disjunctions_with_external_var(external_var_value) + subproblem = self.working_model.clone() TransformationFactory('core.logical_to_linear').apply_to(subproblem) TransformationFactory('gdp.bigm').apply_to(subproblem) @@ -186,7 +207,7 @@ def _solve_GDP_subproblem(self, model, config, search_type): subproblem, **minlp_args ) primal_improved = self._handle_subproblem_result( - result, subproblem, config, search_type + result, subproblem, external_var_value, config, search_type ) return primal_improved except RuntimeError as e: @@ -244,18 +265,17 @@ def _get_external_information(self, util_block, config): for external_var_info in util_block.external_var_info_list ) - def fix_disjunctions_with_external_var(self, util_block, external_var_values_list): - """Function that fixes the disjunctions in the model using the values of the external variables. + def fix_disjunctions_with_external_var(self, external_var_values_list): + """Function that fixes the disjunctions in the working_model using the values of the external variables. Parameters ---------- - util_block : Block - The GDPOPT utility block of the model. external_var_values_list : List The list of values of the external variables """ for external_variable_value, external_var_info in zip( - external_var_values_list, util_block.external_var_info_list + external_var_values_list, + self.working_model_util_block.external_var_info_list, ): for idx, (boolean_var, disjunct) in enumerate( zip(external_var_info.Boolean_vars, external_var_info.Disjuncts) @@ -298,6 +318,11 @@ def _get_directions(self, dimension, config): directions = list(it.product([-1, 0, 1], repeat=dimension)) directions.remove((0,) * dimension) return directions + else: + raise ValueError( + "The direction_norm option must be 'L2' or 'Linf', " + "but received %s" % config.direction_norm + ) def _check_valid_neighbor(self, neighbor): """Function that checks if a given neighbor is valid. @@ -325,28 +350,22 @@ def _check_valid_neighbor(self, neighbor): else: return False - def neighbor_search(self, model, config): + def neighbor_search(self, config): """Function that evaluates a group of given points and returns the best Parameters ---------- - model : ConcreteModel - the subproblem model config : ConfigBlock GDPopt configuration block """ locally_optimal = True best_neighbor = None - # reset best direction - self.best_direction = None + self.best_direction = None # reset best direction for direction in self.directions: neighbor = tuple(map(sum, zip(self.current_point, direction))) if self._check_valid_neighbor(neighbor): - self.fix_disjunctions_with_external_var( - self.working_model_util_block, neighbor - ) primal_improved = self._solve_GDP_subproblem( - model, config, 'Neighbor search' + neighbor, 'Neighbor search', config ) if primal_improved: locally_optimal = False @@ -356,13 +375,11 @@ def neighbor_search(self, model, config): self.current_point = best_neighbor return locally_optimal - def line_search(self, model, config): - """Function that performs a line search in a given direction. + def line_search(self, config): + """Function that performs a line search in the best direction. Parameters ---------- - model : ConcreteModel - the subproblem model config : ConfigBlock GDPopt configuration block """ @@ -370,31 +387,31 @@ def line_search(self, model, config): while primal_improved: next_point = tuple(map(sum, zip(self.current_point, self.best_direction))) if self._check_valid_neighbor(next_point): - self.fix_disjunctions_with_external_var( - self.working_model_util_block, next_point - ) primal_improved = self._solve_GDP_subproblem( - model, config, 'Line search' + next_point, 'Line search', config ) if primal_improved: self.current_point = next_point else: break - print("Line search finished.") def _handle_subproblem_result( - self, subproblem_result, subproblem, config, search_type + self, subproblem_result, subproblem, external_var_value, config, search_type ): """Function that handles the result of the subproblem Parameters ---------- - subproblem : ConcreteModel - the subproblem model subproblem_result : tuple the result of the subproblem + subproblem : ConcreteModel + the subproblem model + external_var_value : list + the values of the external variables config : ConfigBlock GDPopt configuration block + search_type : str + the type of search, neighbor search or line search Returns ------- @@ -418,7 +435,10 @@ def _handle_subproblem_result( else subproblem_result.problem.lower_bound ) primal_improved = self._update_bounds_after_solve( - search_type, primal=primal_bound, logger=config.logger + search_type, + primal=primal_bound, + logger=config.logger, + current_point=external_var_value, ) if primal_improved: self.update_incumbent( @@ -430,15 +450,42 @@ def _handle_subproblem_result( def _log_header(self, logger): logger.info( '=================================================================' - '============================' + '====================================' ) logger.info( - '{:^9} | {:^15} | {:^11} | {:^11} | {:^8} | {:^7}\n'.format( + '{:^9} | {:^15} | {:^20} | {:^11} | {:^11} | {:^8} | {:^7}\n'.format( 'Iteration', 'Search Type', + 'External Variables', 'Lower Bound', 'Upper Bound', ' Gap ', 'Time(s)', ) ) + + def _log_current_state( + self, logger, search_type, current_point, primal_improved=False + ): + star = "*" if primal_improved else "" + logger.info( + self.log_formatter.format( + self.iteration, + search_type, + str(current_point), + self.LB, + self.UB, + self.relative_gap(), + get_main_elapsed_time(self.timing), + star, + ) + ) + + def _update_bounds_after_solve( + self, search_type, primal=None, dual=None, logger=None, current_point=None + ): + primal_improved = self._update_bounds(primal, dual) + if logger is not None: + self._log_current_state(logger, search_type, current_point, primal_improved) + + return primal_improved From e7115b86dc02d97d6a0bdb05f46c576201b83952 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 5 Feb 2024 15:39:02 -0500 Subject: [PATCH 0454/3044] add Reformulation Summary --- pyomo/contrib/gdpopt/ldsda.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/gdpopt/ldsda.py b/pyomo/contrib/gdpopt/ldsda.py index 32013533139..a5ea48d40a5 100644 --- a/pyomo/contrib/gdpopt/ldsda.py +++ b/pyomo/contrib/gdpopt/ldsda.py @@ -40,6 +40,7 @@ from pyomo.common.dependencies import attempt_import it, it_available = attempt_import('itertools') +tabulate, tabulate_available = attempt_import('tabulate') # Data tuple for external variables. ExternalVarInfo = namedtuple( @@ -133,8 +134,6 @@ def _solve_gdp(self, model, config): # ).create_using(model) # Now that logical_to_disjunctive has been called. add_transformed_boolean_variable_list(self.working_model_util_block) - - self._log_header(logger) self._get_external_information(self.working_model_util_block, config) self.directions = self._get_directions( self.number_of_external_variables, config @@ -144,7 +143,7 @@ def _solve_gdp(self, model, config): # nonlinear constraint activation. if not hasattr(self.working_model_util_block, 'BigM'): self.working_model_util_block.BigM = Suffix() - + self._log_header(logger) # Solve the initial point _ = self._solve_GDP_subproblem(self.current_point, 'Initial point', config) @@ -234,6 +233,7 @@ def _get_external_information(self, util_block, config): """ util_block.external_var_info_list = [] model = util_block.parent_block() + reformulation_summary = [] # Identify the variables that can be reformulated by performing a loop over logical constraints # TODO: we can automatically find all Exactly logical constraints in the model. # However, we cannot link the starting point and the logical constraint. @@ -260,6 +260,22 @@ def _get_external_information(self, util_block, config): LB=1, ) ) + reformulation_summary.append( + [ + 1, + len(sorted_boolean_var_list), + [boolean_var.name for boolean_var in sorted_boolean_var_list], + ] + ) + config.logger.info("Reformulation Summary:") + config.logger.info( + tabulate.tabulate( + reformulation_summary, + headers=["Ext Var Index", "LB", "UB", "Associated Boolean Vars"], + showindex="always", + tablefmt="simple_outline", + ) + ) self.number_of_external_variables = sum( external_var_info.exactly_number for external_var_info in util_block.external_var_info_list From a580f107c499ff4164983fb64c67200310821e1a Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 5 Feb 2024 15:50:17 -0500 Subject: [PATCH 0455/3044] update log_formatter --- pyomo/contrib/gdpopt/ldsda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/gdpopt/ldsda.py b/pyomo/contrib/gdpopt/ldsda.py index a5ea48d40a5..b878c6078b0 100644 --- a/pyomo/contrib/gdpopt/ldsda.py +++ b/pyomo/contrib/gdpopt/ldsda.py @@ -108,7 +108,7 @@ def _solve_gdp(self, model, config): """ logger = config.logger self.log_formatter = ( - '{:>9} {:>15} {:>20} {:>11.5f} {:>11.5f} {:>8.2%} {:>7.2f} {}' + '{:>9} {:>15} {:>20} {:>11.5f} {:>11.5f} {:>8.2%} {:>7.2f} {}' ) self.best_direction = None self.current_point = tuple(config.starting_point) From b27dff5535c8c1241c41e7459bf71c3b082fae76 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 6 Feb 2024 13:10:57 -0700 Subject: [PATCH 0456/3044] revert --- pyomo/gdp/plugins/binary_multiplication.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/gdp/plugins/binary_multiplication.py b/pyomo/gdp/plugins/binary_multiplication.py index 8489fa04808..dfdc87ded19 100644 --- a/pyomo/gdp/plugins/binary_multiplication.py +++ b/pyomo/gdp/plugins/binary_multiplication.py @@ -79,8 +79,7 @@ def _transform_disjunctionData( or_expr += disjunct.binary_indicator_var self._transform_disjunct(disjunct, transBlock) - # rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var - rhs = 1 + rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var if obj.xor: xorConstraint[index] = or_expr == rhs else: From d224bbe4df9f0a94010defce3f266b789590ba87 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Feb 2024 19:48:45 -0500 Subject: [PATCH 0457/3044] Remove custom PyROS `ConfigDict` interfaces --- pyomo/contrib/pyros/pyros.py | 325 +++++------------------------------ 1 file changed, 44 insertions(+), 281 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 829184fc70c..f266b7451e6 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -13,7 +13,13 @@ import logging from textwrap import indent, dedent, wrap from pyomo.common.collections import Bunch, ComponentSet -from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + document_kwargs_from_configdict, + In, + NonNegativeFloat, +) from pyomo.core.base.block import Block from pyomo.core.expr import value from pyomo.core.base.var import Var, _VarData @@ -147,68 +153,6 @@ def __call__(self, obj): return ans -class PyROSConfigValue(ConfigValue): - """ - Subclass of ``common.collections.ConfigValue``, - with a few attributes added to facilitate documentation - of the PyROS solver. - An instance of this class is used for storing and - documenting an argument to the PyROS solver. - - Attributes - ---------- - is_optional : bool - Argument is optional. - document_default : bool, optional - Document the default value of the argument - in any docstring generated from this instance, - or a `ConfigDict` object containing this instance. - dtype_spec_str : None or str, optional - String documenting valid types for this argument. - If `None` is provided, then this string is automatically - determined based on the `domain` argument to the - constructor. - - NOTES - ----- - Cleaner way to access protected attributes - (particularly _doc, _description) inherited from ConfigValue? - - """ - - def __init__( - self, - default=None, - domain=None, - description=None, - doc=None, - visibility=0, - is_optional=True, - document_default=True, - dtype_spec_str=None, - ): - """Initialize self (see class docstring).""" - - # initialize base class attributes - super(self.__class__, self).__init__( - default=default, - domain=domain, - description=description, - doc=doc, - visibility=visibility, - ) - - self.is_optional = is_optional - self.document_default = document_default - - if dtype_spec_str is None: - self.dtype_spec_str = self.domain_name() - # except AttributeError: - # self.dtype_spec_str = repr(self._domain) - else: - self.dtype_spec_str = dtype_spec_str - - def pyros_config(): CONFIG = ConfigDict('PyROS') @@ -217,7 +161,7 @@ def pyros_config(): # ================================================ CONFIG.declare( 'time_limit', - PyROSConfigValue( + ConfigValue( default=None, domain=NonNegativeFloat, doc=( @@ -227,14 +171,11 @@ def pyros_config(): If `None` is provided, then no time limit is enforced. """ ), - is_optional=True, - document_default=False, - dtype_spec_str="None or NonNegativeFloat", ), ) CONFIG.declare( 'keepfiles', - PyROSConfigValue( + ConfigValue( default=False, domain=bool, description=( @@ -245,25 +186,19 @@ def pyros_config(): must also be specified. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( 'tee', - PyROSConfigValue( + ConfigValue( default=False, domain=bool, description="Output subordinate solver logs for all subproblems.", - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( 'load_solution', - PyROSConfigValue( + ConfigValue( default=True, domain=bool, description=( @@ -272,9 +207,6 @@ def pyros_config(): provided. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) @@ -283,27 +215,25 @@ def pyros_config(): # ================================================ CONFIG.declare( "first_stage_variables", - PyROSConfigValue( + ConfigValue( default=[], domain=InputDataStandardizer(Var, _VarData), description="First-stage (or design) variables.", - is_optional=False, - dtype_spec_str="list of Var", + visibility=1, ), ) CONFIG.declare( "second_stage_variables", - PyROSConfigValue( + ConfigValue( default=[], domain=InputDataStandardizer(Var, _VarData), description="Second-stage (or control) variables.", - is_optional=False, - dtype_spec_str="list of Var", + visibility=1, ), ) CONFIG.declare( "uncertain_params", - PyROSConfigValue( + ConfigValue( default=[], domain=InputDataStandardizer(Param, _ParamData), description=( @@ -313,13 +243,12 @@ def pyros_config(): objects should be set to True. """ ), - is_optional=False, - dtype_spec_str="list of Param", + visibility=1, ), ) CONFIG.declare( "uncertainty_set", - PyROSConfigValue( + ConfigValue( default=None, domain=uncertainty_sets, description=( @@ -329,28 +258,25 @@ def pyros_config(): to be robust. """ ), - is_optional=False, - dtype_spec_str="UncertaintySet", + visibility=1, ), ) CONFIG.declare( "local_solver", - PyROSConfigValue( + ConfigValue( default=None, domain=SolverResolvable(), description="Subordinate local NLP solver.", - is_optional=False, - dtype_spec_str="Solver", + visibility=1, ), ) CONFIG.declare( "global_solver", - PyROSConfigValue( + ConfigValue( default=None, domain=SolverResolvable(), description="Subordinate global NLP solver.", - is_optional=False, - dtype_spec_str="Solver", + visibility=1, ), ) # ================================================ @@ -358,7 +284,7 @@ def pyros_config(): # ================================================ CONFIG.declare( "objective_focus", - PyROSConfigValue( + ConfigValue( default=ObjectiveType.nominal, domain=ValidEnum(ObjectiveType), description=( @@ -388,14 +314,11 @@ def pyros_config(): feasibility is guaranteed. """ ), - is_optional=True, - document_default=False, - dtype_spec_str="ObjectiveType", ), ) CONFIG.declare( "nominal_uncertain_param_vals", - PyROSConfigValue( + ConfigValue( default=[], domain=list, doc=( @@ -407,14 +330,11 @@ def pyros_config(): objects specified through `uncertain_params` are chosen. """ ), - is_optional=True, - document_default=True, - dtype_spec_str="list of float", ), ) CONFIG.declare( "decision_rule_order", - PyROSConfigValue( + ConfigValue( default=0, domain=In([0, 1, 2]), description=( @@ -437,14 +357,11 @@ def pyros_config(): - 2: quadratic recourse """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( "solve_master_globally", - PyROSConfigValue( + ConfigValue( default=False, domain=bool, doc=( @@ -460,14 +377,11 @@ def pyros_config(): by PyROS. Otherwise, only robust feasibility is guaranteed. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( "max_iter", - PyROSConfigValue( + ConfigValue( default=-1, domain=PositiveIntOrMinusOne, description=( @@ -476,14 +390,11 @@ def pyros_config(): limit is enforced. """ ), - is_optional=True, - document_default=True, - dtype_spec_str="int", ), ) CONFIG.declare( "robust_feasibility_tolerance", - PyROSConfigValue( + ConfigValue( default=1e-4, domain=NonNegativeFloat, description=( @@ -492,14 +403,11 @@ def pyros_config(): constraint violations during the GRCS separation step. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( "separation_priority_order", - PyROSConfigValue( + ConfigValue( default={}, domain=dict, doc=( @@ -514,14 +422,11 @@ def pyros_config(): priority. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( "progress_logger", - PyROSConfigValue( + ConfigValue( default=default_pyros_solver_logger, domain=a_logger, doc=( @@ -534,14 +439,11 @@ def pyros_config(): object of level ``logging.INFO``. """ ), - is_optional=True, - document_default=True, - dtype_spec_str="str or logging.Logger", ), ) CONFIG.declare( "backup_local_solvers", - PyROSConfigValue( + ConfigValue( default=[], domain=SolverResolvable(), doc=( @@ -551,14 +453,11 @@ def pyros_config(): to solve a subproblem to an acceptable termination condition. """ ), - is_optional=True, - document_default=True, - dtype_spec_str="list of Solver", ), ) CONFIG.declare( "backup_global_solvers", - PyROSConfigValue( + ConfigValue( default=[], domain=SolverResolvable(), doc=( @@ -568,14 +467,11 @@ def pyros_config(): to solve a subproblem to an acceptable termination condition. """ ), - is_optional=True, - document_default=True, - dtype_spec_str="list of Solver", ), ) CONFIG.declare( "subproblem_file_directory", - PyROSConfigValue( + ConfigValue( default=None, domain=str, description=( @@ -587,9 +483,6 @@ def pyros_config(): provided. """ ), - is_optional=True, - document_default=True, - dtype_spec_str="None, str, or path-like", ), ) @@ -598,7 +491,7 @@ def pyros_config(): # ================================================ CONFIG.declare( "bypass_local_separation", - PyROSConfigValue( + ConfigValue( default=False, domain=bool, description=( @@ -611,14 +504,11 @@ def pyros_config(): can quickly solve separation subproblems to global optimality. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( "bypass_global_separation", - PyROSConfigValue( + ConfigValue( default=False, domain=bool, doc=( @@ -635,14 +525,11 @@ def pyros_config(): optimality. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) CONFIG.declare( "p_robustness", - PyROSConfigValue( + ConfigValue( default={}, domain=dict, doc=( @@ -660,9 +547,6 @@ def pyros_config(): the nominal parameter realization. """ ), - is_optional=True, - document_default=True, - dtype_spec_str=None, ), ) @@ -836,6 +720,13 @@ def _log_config(self, logger, config, exclude_options=None, **log_kwargs): logger.log(msg=f" {key}={val!r}", **log_kwargs) logger.log(msg="-" * self._LOG_LINE_LENGTH, **log_kwargs) + @document_kwargs_from_configdict( + config=CONFIG, + section="Keyword Arguments", + indent_spacing=4, + width=72, + visibility=0, + ) def solve( self, model, @@ -1085,131 +976,3 @@ def solve( config.progress_logger.info("=" * self._LOG_LINE_LENGTH) return return_soln - - -def _generate_filtered_docstring(): - """ - Add Numpy-style 'Keyword arguments' section to `PyROS.solve()` - docstring. - """ - cfg = PyROS.CONFIG() - - # mandatory args already documented - exclude_args = [ - "first_stage_variables", - "second_stage_variables", - "uncertain_params", - "uncertainty_set", - "local_solver", - "global_solver", - ] - - indent_by = 8 - width = 72 - before = PyROS.solve.__doc__ - section_name = "Keyword Arguments" - - indent_str = ' ' * indent_by - wrap_width = width - indent_by - cfg = pyros_config() - - arg_docs = [] - - def wrap_doc(doc, indent_by, width): - """ - Wrap a string, accounting for paragraph - breaks ('\n\n') and bullet points (paragraphs - which, when dedented, are such that each line - starts with '- ' or ' '). - """ - paragraphs = doc.split("\n\n") - wrapped_pars = [] - for par in paragraphs: - lines = dedent(par).split("\n") - has_bullets = all( - line.startswith("- ") or line.startswith(" ") - for line in lines - if line != "" - ) - if has_bullets: - # obtain strings of each bullet point - # (dedented, bullet dash and bullet indent removed) - bullet_groups = [] - new_group = False - group = "" - for line in lines: - new_group = line.startswith("- ") - if new_group: - bullet_groups.append(group) - group = "" - new_line = line[2:] - group += f"{new_line}\n" - if group != "": - # ensure last bullet not skipped - bullet_groups.append(group) - - # first entry is just ''; remove - bullet_groups = bullet_groups[1:] - - # wrap each bullet point, then add bullet - # and indents as necessary - wrapped_groups = [] - for group in bullet_groups: - wrapped_groups.append( - "\n".join( - f"{'- ' if idx == 0 else ' '}{line}" - for idx, line in enumerate( - wrap(group, width - 2 - indent_by) - ) - ) - ) - - # now combine bullets into single 'paragraph' - wrapped_pars.append( - indent("\n".join(wrapped_groups), prefix=' ' * indent_by) - ) - else: - wrapped_pars.append( - indent( - "\n".join(wrap(dedent(par), width=width - indent_by)), - prefix=' ' * indent_by, - ) - ) - - return "\n\n".join(wrapped_pars) - - section_header = indent(f"{section_name}\n" + "-" * len(section_name), indent_str) - for key, itm in cfg._data.items(): - if key in exclude_args: - continue - arg_name = key - arg_dtype = itm.dtype_spec_str - - if itm.is_optional: - if itm.document_default: - optional_str = f", default={repr(itm._default)}" - else: - optional_str = ", optional" - else: - optional_str = "" - - arg_header = f"{indent_str}{arg_name} : {arg_dtype}{optional_str}" - - # dedented_doc_str = dedent(itm.doc).replace("\n", ' ').strip() - if itm._doc is not None: - raw_arg_desc = itm._doc - else: - raw_arg_desc = itm._description - - arg_description = wrap_doc( - raw_arg_desc, width=wrap_width, indent_by=indent_by + 4 - ) - - arg_docs.append(f"{arg_header}\n{arg_description}") - - kwargs_section_doc = "\n".join([section_header] + arg_docs) - - return f"{before}\n{kwargs_section_doc}\n" - - -PyROS.solve.__doc__ = _generate_filtered_docstring() From d5fc7cbac76727c4e858aec9b6c360e2e42088bc Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Feb 2024 20:10:53 -0500 Subject: [PATCH 0458/3044] Create new module for config objects --- pyomo/contrib/pyros/config.py | 493 ++++++++++++++++++++++++++++++++++ pyomo/contrib/pyros/pyros.py | 481 +-------------------------------- 2 files changed, 498 insertions(+), 476 deletions(-) create mode 100644 pyomo/contrib/pyros/config.py diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py new file mode 100644 index 00000000000..1dc1608ab16 --- /dev/null +++ b/pyomo/contrib/pyros/config.py @@ -0,0 +1,493 @@ +""" +Interfaces for managing PyROS solver options. +""" + + +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + In, + NonNegativeFloat, +) +from pyomo.core.base import ( + Var, + _VarData, +) +from pyomo.core.base.param import ( + Param, + _ParamData, +) +from pyomo.opt import SolverFactory +from pyomo.contrib.pyros.util import ( + a_logger, + ObjectiveType, + setup_pyros_logger, + ValidEnum, +) +from pyomo.contrib.pyros.uncertainty_sets import uncertainty_sets + + +default_pyros_solver_logger = setup_pyros_logger() + + +def NonNegIntOrMinusOne(obj): + ''' + if obj is a non-negative int, return the non-negative int + if obj is -1, return -1 + else, error + ''' + ans = int(obj) + if ans != float(obj) or (ans < 0 and ans != -1): + raise ValueError("Expected non-negative int, but received %s" % (obj,)) + return ans + + +def PositiveIntOrMinusOne(obj): + ''' + if obj is a positive int, return the int + if obj is -1, return -1 + else, error + ''' + ans = int(obj) + if ans != float(obj) or (ans <= 0 and ans != -1): + raise ValueError("Expected positive int, but received %s" % (obj,)) + return ans + + +class SolverResolvable(object): + def __call__(self, obj): + ''' + if obj is a string, return the Solver object for that solver name + if obj is a Solver object, return a copy of the Solver + if obj is a list, and each element of list is solver resolvable, + return list of solvers + ''' + if isinstance(obj, str): + return SolverFactory(obj.lower()) + elif callable(getattr(obj, "solve", None)): + return obj + elif isinstance(obj, list): + return [self(o) for o in obj] + else: + raise ValueError( + "Expected a Pyomo solver or string object, " + "instead received {0}".format(obj.__class__.__name__) + ) + + +class InputDataStandardizer(object): + def __init__(self, ctype, cdatatype): + self.ctype = ctype + self.cdatatype = cdatatype + + def __call__(self, obj): + if isinstance(obj, self.ctype): + return list(obj.values()) + if isinstance(obj, self.cdatatype): + return [obj] + ans = [] + for item in obj: + ans.extend(self.__call__(item)) + for _ in ans: + assert isinstance(_, self.cdatatype) + return ans + + +def pyros_config(): + CONFIG = ConfigDict('PyROS') + + # ================================================ + # === Options common to all solvers + # ================================================ + CONFIG.declare( + 'time_limit', + ConfigValue( + default=None, + domain=NonNegativeFloat, + doc=( + """ + Wall time limit for the execution of the PyROS solver + in seconds (including time spent by subsolvers). + If `None` is provided, then no time limit is enforced. + """ + ), + ), + ) + CONFIG.declare( + 'keepfiles', + ConfigValue( + default=False, + domain=bool, + description=( + """ + Export subproblems with a non-acceptable termination status + for debugging purposes. + If True is provided, then the argument `subproblem_file_directory` + must also be specified. + """ + ), + ), + ) + CONFIG.declare( + 'tee', + ConfigValue( + default=False, + domain=bool, + description="Output subordinate solver logs for all subproblems.", + ), + ) + CONFIG.declare( + 'load_solution', + ConfigValue( + default=True, + domain=bool, + description=( + """ + Load final solution(s) found by PyROS to the deterministic model + provided. + """ + ), + ), + ) + + # ================================================ + # === Required User Inputs + # ================================================ + CONFIG.declare( + "first_stage_variables", + ConfigValue( + default=[], + domain=InputDataStandardizer(Var, _VarData), + description="First-stage (or design) variables.", + visibility=1, + ), + ) + CONFIG.declare( + "second_stage_variables", + ConfigValue( + default=[], + domain=InputDataStandardizer(Var, _VarData), + description="Second-stage (or control) variables.", + visibility=1, + ), + ) + CONFIG.declare( + "uncertain_params", + ConfigValue( + default=[], + domain=InputDataStandardizer(Param, _ParamData), + description=( + """ + Uncertain model parameters. + The `mutable` attribute for all uncertain parameter + objects should be set to True. + """ + ), + visibility=1, + ), + ) + CONFIG.declare( + "uncertainty_set", + ConfigValue( + default=None, + domain=uncertainty_sets, + description=( + """ + Uncertainty set against which the + final solution(s) returned by PyROS should be certified + to be robust. + """ + ), + visibility=1, + ), + ) + CONFIG.declare( + "local_solver", + ConfigValue( + default=None, + domain=SolverResolvable(), + description="Subordinate local NLP solver.", + visibility=1, + ), + ) + CONFIG.declare( + "global_solver", + ConfigValue( + default=None, + domain=SolverResolvable(), + description="Subordinate global NLP solver.", + visibility=1, + ), + ) + # ================================================ + # === Optional User Inputs + # ================================================ + CONFIG.declare( + "objective_focus", + ConfigValue( + default=ObjectiveType.nominal, + domain=ValidEnum(ObjectiveType), + description=( + """ + Choice of objective focus to optimize in the master problems. + Choices are: `ObjectiveType.worst_case`, + `ObjectiveType.nominal`. + """ + ), + doc=( + """ + Objective focus for the master problems: + + - `ObjectiveType.nominal`: + Optimize the objective function subject to the nominal + uncertain parameter realization. + - `ObjectiveType.worst_case`: + Optimize the objective function subject to the worst-case + uncertain parameter realization. + + By default, `ObjectiveType.nominal` is chosen. + + A worst-case objective focus is required for certification + of robust optimality of the final solution(s) returned + by PyROS. + If a nominal objective focus is chosen, then only robust + feasibility is guaranteed. + """ + ), + ), + ) + CONFIG.declare( + "nominal_uncertain_param_vals", + ConfigValue( + default=[], + domain=list, + doc=( + """ + Nominal uncertain parameter realization. + Entries should be provided in an order consistent with the + entries of the argument `uncertain_params`. + If an empty list is provided, then the values of the `Param` + objects specified through `uncertain_params` are chosen. + """ + ), + ), + ) + CONFIG.declare( + "decision_rule_order", + ConfigValue( + default=0, + domain=In([0, 1, 2]), + description=( + """ + Order (or degree) of the polynomial decision rule functions used + for approximating the adjustability of the second stage + variables with respect to the uncertain parameters. + """ + ), + doc=( + """ + Order (or degree) of the polynomial decision rule functions used + for approximating the adjustability of the second stage + variables with respect to the uncertain parameters. + + Choices are: + + - 0: static recourse + - 1: affine recourse + - 2: quadratic recourse + """ + ), + ), + ) + CONFIG.declare( + "solve_master_globally", + ConfigValue( + default=False, + domain=bool, + doc=( + """ + True to solve all master problems with the subordinate + global solver, False to solve all master problems with + the subordinate local solver. + Along with a worst-case objective focus + (see argument `objective_focus`), + solving the master problems to global optimality is required + for certification + of robust optimality of the final solution(s) returned + by PyROS. Otherwise, only robust feasibility is guaranteed. + """ + ), + ), + ) + CONFIG.declare( + "max_iter", + ConfigValue( + default=-1, + domain=PositiveIntOrMinusOne, + description=( + """ + Iteration limit. If -1 is provided, then no iteration + limit is enforced. + """ + ), + ), + ) + CONFIG.declare( + "robust_feasibility_tolerance", + ConfigValue( + default=1e-4, + domain=NonNegativeFloat, + description=( + """ + Relative tolerance for assessing maximal inequality + constraint violations during the GRCS separation step. + """ + ), + ), + ) + CONFIG.declare( + "separation_priority_order", + ConfigValue( + default={}, + domain=dict, + doc=( + """ + Mapping from model inequality constraint names + to positive integers specifying the priorities + of their corresponding separation subproblems. + A higher integer value indicates a higher priority. + Constraints not referenced in the `dict` assume + a priority of 0. + Separation subproblems are solved in order of decreasing + priority. + """ + ), + ), + ) + CONFIG.declare( + "progress_logger", + ConfigValue( + default=default_pyros_solver_logger, + domain=a_logger, + doc=( + """ + Logger (or name thereof) used for reporting PyROS solver + progress. If a `str` is specified, then ``progress_logger`` + is cast to ``logging.getLogger(progress_logger)``. + In the default case, `progress_logger` is set to + a :class:`pyomo.contrib.pyros.util.PreformattedLogger` + object of level ``logging.INFO``. + """ + ), + ), + ) + CONFIG.declare( + "backup_local_solvers", + ConfigValue( + default=[], + domain=SolverResolvable(), + doc=( + """ + Additional subordinate local NLP optimizers to invoke + in the event the primary local NLP optimizer fails + to solve a subproblem to an acceptable termination condition. + """ + ), + ), + ) + CONFIG.declare( + "backup_global_solvers", + ConfigValue( + default=[], + domain=SolverResolvable(), + doc=( + """ + Additional subordinate global NLP optimizers to invoke + in the event the primary global NLP optimizer fails + to solve a subproblem to an acceptable termination condition. + """ + ), + ), + ) + CONFIG.declare( + "subproblem_file_directory", + ConfigValue( + default=None, + domain=str, + description=( + """ + Directory to which to export subproblems not successfully + solved to an acceptable termination condition. + In the event ``keepfiles=True`` is specified, a str or + path-like referring to an existing directory must be + provided. + """ + ), + ), + ) + + # ================================================ + # === Advanced Options + # ================================================ + CONFIG.declare( + "bypass_local_separation", + ConfigValue( + default=False, + domain=bool, + description=( + """ + This is an advanced option. + Solve all separation subproblems with the subordinate global + solver(s) only. + This option is useful for expediting PyROS + in the event that the subordinate global optimizer(s) provided + can quickly solve separation subproblems to global optimality. + """ + ), + ), + ) + CONFIG.declare( + "bypass_global_separation", + ConfigValue( + default=False, + domain=bool, + doc=( + """ + This is an advanced option. + Solve all separation subproblems with the subordinate local + solver(s) only. + If `True` is chosen, then robustness of the final solution(s) + returned by PyROS is not guaranteed, and a warning will + be issued at termination. + This option is useful for expediting PyROS + in the event that the subordinate global optimizer provided + cannot tractably solve separation subproblems to global + optimality. + """ + ), + ), + ) + CONFIG.declare( + "p_robustness", + ConfigValue( + default={}, + domain=dict, + doc=( + """ + This is an advanced option. + Add p-robustness constraints to all master subproblems. + If an empty dict is provided, then p-robustness constraints + are not added. + Otherwise, the dict must map a `str` of value ``'rho'`` + to a non-negative `float`. PyROS automatically + specifies ``1 + p_robustness['rho']`` + as an upper bound for the ratio of the + objective function value under any PyROS-sampled uncertain + parameter realization to the objective function under + the nominal parameter realization. + """ + ), + ), + ) + + return CONFIG diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index f266b7451e6..962ae79a436 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -11,23 +11,16 @@ # pyros.py: Generalized Robust Cutting-Set Algorithm for Pyomo import logging -from textwrap import indent, dedent, wrap +from pyomo.common.config import document_kwargs_from_configdict from pyomo.common.collections import Bunch, ComponentSet -from pyomo.common.config import ( - ConfigDict, - ConfigValue, - document_kwargs_from_configdict, - In, - NonNegativeFloat, -) from pyomo.core.base.block import Block from pyomo.core.expr import value -from pyomo.core.base.var import Var, _VarData -from pyomo.core.base.param import Param, _ParamData -from pyomo.core.base.objective import Objective, maximize -from pyomo.contrib.pyros.util import a_logger, time_code, get_main_elapsed_time +from pyomo.core.base.var import Var +from pyomo.core.base.objective import Objective +from pyomo.contrib.pyros.util import time_code from pyomo.common.modeling import unique_component_name from pyomo.opt import SolverFactory +from pyomo.contrib.pyros.config import pyros_config from pyomo.contrib.pyros.util import ( model_is_valid, recast_to_min_obj, @@ -35,7 +28,6 @@ add_decision_rule_variables, load_final_solution, pyrosTerminationCondition, - ValidEnum, ObjectiveType, validate_uncertainty_set, identify_objective_functions, @@ -49,7 +41,6 @@ ) from pyomo.contrib.pyros.solve_data import ROSolveResults from pyomo.contrib.pyros.pyros_algorithm_methods import ROSolver_iterative_solve -from pyomo.contrib.pyros.uncertainty_sets import uncertainty_sets from pyomo.core.base import Constraint from datetime import datetime @@ -91,468 +82,6 @@ def _get_pyomo_version_info(): return {"Pyomo version": pyomo_version, "Commit hash": commit_hash} -def NonNegIntOrMinusOne(obj): - ''' - if obj is a non-negative int, return the non-negative int - if obj is -1, return -1 - else, error - ''' - ans = int(obj) - if ans != float(obj) or (ans < 0 and ans != -1): - raise ValueError("Expected non-negative int, but received %s" % (obj,)) - return ans - - -def PositiveIntOrMinusOne(obj): - ''' - if obj is a positive int, return the int - if obj is -1, return -1 - else, error - ''' - ans = int(obj) - if ans != float(obj) or (ans <= 0 and ans != -1): - raise ValueError("Expected positive int, but received %s" % (obj,)) - return ans - - -class SolverResolvable(object): - def __call__(self, obj): - ''' - if obj is a string, return the Solver object for that solver name - if obj is a Solver object, return a copy of the Solver - if obj is a list, and each element of list is solver resolvable, return list of solvers - ''' - if isinstance(obj, str): - return SolverFactory(obj.lower()) - elif callable(getattr(obj, "solve", None)): - return obj - elif isinstance(obj, list): - return [self(o) for o in obj] - else: - raise ValueError( - "Expected a Pyomo solver or string object, " - "instead received {1}".format(obj.__class__.__name__) - ) - - -class InputDataStandardizer(object): - def __init__(self, ctype, cdatatype): - self.ctype = ctype - self.cdatatype = cdatatype - - def __call__(self, obj): - if isinstance(obj, self.ctype): - return list(obj.values()) - if isinstance(obj, self.cdatatype): - return [obj] - ans = [] - for item in obj: - ans.extend(self.__call__(item)) - for _ in ans: - assert isinstance(_, self.cdatatype) - return ans - - -def pyros_config(): - CONFIG = ConfigDict('PyROS') - - # ================================================ - # === Options common to all solvers - # ================================================ - CONFIG.declare( - 'time_limit', - ConfigValue( - default=None, - domain=NonNegativeFloat, - doc=( - """ - Wall time limit for the execution of the PyROS solver - in seconds (including time spent by subsolvers). - If `None` is provided, then no time limit is enforced. - """ - ), - ), - ) - CONFIG.declare( - 'keepfiles', - ConfigValue( - default=False, - domain=bool, - description=( - """ - Export subproblems with a non-acceptable termination status - for debugging purposes. - If True is provided, then the argument `subproblem_file_directory` - must also be specified. - """ - ), - ), - ) - CONFIG.declare( - 'tee', - ConfigValue( - default=False, - domain=bool, - description="Output subordinate solver logs for all subproblems.", - ), - ) - CONFIG.declare( - 'load_solution', - ConfigValue( - default=True, - domain=bool, - description=( - """ - Load final solution(s) found by PyROS to the deterministic model - provided. - """ - ), - ), - ) - - # ================================================ - # === Required User Inputs - # ================================================ - CONFIG.declare( - "first_stage_variables", - ConfigValue( - default=[], - domain=InputDataStandardizer(Var, _VarData), - description="First-stage (or design) variables.", - visibility=1, - ), - ) - CONFIG.declare( - "second_stage_variables", - ConfigValue( - default=[], - domain=InputDataStandardizer(Var, _VarData), - description="Second-stage (or control) variables.", - visibility=1, - ), - ) - CONFIG.declare( - "uncertain_params", - ConfigValue( - default=[], - domain=InputDataStandardizer(Param, _ParamData), - description=( - """ - Uncertain model parameters. - The `mutable` attribute for all uncertain parameter - objects should be set to True. - """ - ), - visibility=1, - ), - ) - CONFIG.declare( - "uncertainty_set", - ConfigValue( - default=None, - domain=uncertainty_sets, - description=( - """ - Uncertainty set against which the - final solution(s) returned by PyROS should be certified - to be robust. - """ - ), - visibility=1, - ), - ) - CONFIG.declare( - "local_solver", - ConfigValue( - default=None, - domain=SolverResolvable(), - description="Subordinate local NLP solver.", - visibility=1, - ), - ) - CONFIG.declare( - "global_solver", - ConfigValue( - default=None, - domain=SolverResolvable(), - description="Subordinate global NLP solver.", - visibility=1, - ), - ) - # ================================================ - # === Optional User Inputs - # ================================================ - CONFIG.declare( - "objective_focus", - ConfigValue( - default=ObjectiveType.nominal, - domain=ValidEnum(ObjectiveType), - description=( - """ - Choice of objective focus to optimize in the master problems. - Choices are: `ObjectiveType.worst_case`, - `ObjectiveType.nominal`. - """ - ), - doc=( - """ - Objective focus for the master problems: - - - `ObjectiveType.nominal`: - Optimize the objective function subject to the nominal - uncertain parameter realization. - - `ObjectiveType.worst_case`: - Optimize the objective function subject to the worst-case - uncertain parameter realization. - - By default, `ObjectiveType.nominal` is chosen. - - A worst-case objective focus is required for certification - of robust optimality of the final solution(s) returned - by PyROS. - If a nominal objective focus is chosen, then only robust - feasibility is guaranteed. - """ - ), - ), - ) - CONFIG.declare( - "nominal_uncertain_param_vals", - ConfigValue( - default=[], - domain=list, - doc=( - """ - Nominal uncertain parameter realization. - Entries should be provided in an order consistent with the - entries of the argument `uncertain_params`. - If an empty list is provided, then the values of the `Param` - objects specified through `uncertain_params` are chosen. - """ - ), - ), - ) - CONFIG.declare( - "decision_rule_order", - ConfigValue( - default=0, - domain=In([0, 1, 2]), - description=( - """ - Order (or degree) of the polynomial decision rule functions used - for approximating the adjustability of the second stage - variables with respect to the uncertain parameters. - """ - ), - doc=( - """ - Order (or degree) of the polynomial decision rule functions used - for approximating the adjustability of the second stage - variables with respect to the uncertain parameters. - - Choices are: - - - 0: static recourse - - 1: affine recourse - - 2: quadratic recourse - """ - ), - ), - ) - CONFIG.declare( - "solve_master_globally", - ConfigValue( - default=False, - domain=bool, - doc=( - """ - True to solve all master problems with the subordinate - global solver, False to solve all master problems with - the subordinate local solver. - Along with a worst-case objective focus - (see argument `objective_focus`), - solving the master problems to global optimality is required - for certification - of robust optimality of the final solution(s) returned - by PyROS. Otherwise, only robust feasibility is guaranteed. - """ - ), - ), - ) - CONFIG.declare( - "max_iter", - ConfigValue( - default=-1, - domain=PositiveIntOrMinusOne, - description=( - """ - Iteration limit. If -1 is provided, then no iteration - limit is enforced. - """ - ), - ), - ) - CONFIG.declare( - "robust_feasibility_tolerance", - ConfigValue( - default=1e-4, - domain=NonNegativeFloat, - description=( - """ - Relative tolerance for assessing maximal inequality - constraint violations during the GRCS separation step. - """ - ), - ), - ) - CONFIG.declare( - "separation_priority_order", - ConfigValue( - default={}, - domain=dict, - doc=( - """ - Mapping from model inequality constraint names - to positive integers specifying the priorities - of their corresponding separation subproblems. - A higher integer value indicates a higher priority. - Constraints not referenced in the `dict` assume - a priority of 0. - Separation subproblems are solved in order of decreasing - priority. - """ - ), - ), - ) - CONFIG.declare( - "progress_logger", - ConfigValue( - default=default_pyros_solver_logger, - domain=a_logger, - doc=( - """ - Logger (or name thereof) used for reporting PyROS solver - progress. If a `str` is specified, then ``progress_logger`` - is cast to ``logging.getLogger(progress_logger)``. - In the default case, `progress_logger` is set to - a :class:`pyomo.contrib.pyros.util.PreformattedLogger` - object of level ``logging.INFO``. - """ - ), - ), - ) - CONFIG.declare( - "backup_local_solvers", - ConfigValue( - default=[], - domain=SolverResolvable(), - doc=( - """ - Additional subordinate local NLP optimizers to invoke - in the event the primary local NLP optimizer fails - to solve a subproblem to an acceptable termination condition. - """ - ), - ), - ) - CONFIG.declare( - "backup_global_solvers", - ConfigValue( - default=[], - domain=SolverResolvable(), - doc=( - """ - Additional subordinate global NLP optimizers to invoke - in the event the primary global NLP optimizer fails - to solve a subproblem to an acceptable termination condition. - """ - ), - ), - ) - CONFIG.declare( - "subproblem_file_directory", - ConfigValue( - default=None, - domain=str, - description=( - """ - Directory to which to export subproblems not successfully - solved to an acceptable termination condition. - In the event ``keepfiles=True`` is specified, a str or - path-like referring to an existing directory must be - provided. - """ - ), - ), - ) - - # ================================================ - # === Advanced Options - # ================================================ - CONFIG.declare( - "bypass_local_separation", - ConfigValue( - default=False, - domain=bool, - description=( - """ - This is an advanced option. - Solve all separation subproblems with the subordinate global - solver(s) only. - This option is useful for expediting PyROS - in the event that the subordinate global optimizer(s) provided - can quickly solve separation subproblems to global optimality. - """ - ), - ), - ) - CONFIG.declare( - "bypass_global_separation", - ConfigValue( - default=False, - domain=bool, - doc=( - """ - This is an advanced option. - Solve all separation subproblems with the subordinate local - solver(s) only. - If `True` is chosen, then robustness of the final solution(s) - returned by PyROS is not guaranteed, and a warning will - be issued at termination. - This option is useful for expediting PyROS - in the event that the subordinate global optimizer provided - cannot tractably solve separation subproblems to global - optimality. - """ - ), - ), - ) - CONFIG.declare( - "p_robustness", - ConfigValue( - default={}, - domain=dict, - doc=( - """ - This is an advanced option. - Add p-robustness constraints to all master subproblems. - If an empty dict is provided, then p-robustness constraints - are not added. - Otherwise, the dict must map a `str` of value ``'rho'`` - to a non-negative `float`. PyROS automatically - specifies ``1 + p_robustness['rho']`` - as an upper bound for the ratio of the - objective function value under any PyROS-sampled uncertain - parameter realization to the objective function under - the nominal parameter realization. - """ - ), - ), - ) - - return CONFIG - - @SolverFactory.register( "pyros", doc="Robust optimization (RO) solver implementing " From 49fa433da7c47646919637a11d9affc0047bd15c Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Feb 2024 20:14:36 -0500 Subject: [PATCH 0459/3044] Apply black, PEP8 code --- pyomo/contrib/pyros/config.py | 37 ++++++++++++----------------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 1dc1608ab16..bd87dc743f9 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -3,20 +3,9 @@ """ -from pyomo.common.config import ( - ConfigDict, - ConfigValue, - In, - NonNegativeFloat, -) -from pyomo.core.base import ( - Var, - _VarData, -) -from pyomo.core.base.param import ( - Param, - _ParamData, -) +from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat +from pyomo.core.base import Var, _VarData +from pyomo.core.base.param import Param, _ParamData from pyomo.opt import SolverFactory from pyomo.contrib.pyros.util import ( a_logger, @@ -122,8 +111,8 @@ def pyros_config(): """ Export subproblems with a non-acceptable termination status for debugging purposes. - If True is provided, then the argument `subproblem_file_directory` - must also be specified. + If True is provided, then the argument + `subproblem_file_directory` must also be specified. """ ), ), @@ -143,8 +132,8 @@ def pyros_config(): domain=bool, description=( """ - Load final solution(s) found by PyROS to the deterministic model - provided. + Load final solution(s) found by PyROS to the deterministic + model provided. """ ), ), @@ -246,7 +235,7 @@ def pyros_config(): uncertain parameter realization. By default, `ObjectiveType.nominal` is chosen. - + A worst-case objective focus is required for certification of robust optimality of the final solution(s) returned by PyROS. @@ -279,19 +268,19 @@ def pyros_config(): domain=In([0, 1, 2]), description=( """ - Order (or degree) of the polynomial decision rule functions used - for approximating the adjustability of the second stage + Order (or degree) of the polynomial decision rule functions + used for approximating the adjustability of the second stage variables with respect to the uncertain parameters. """ ), doc=( """ - Order (or degree) of the polynomial decision rule functions used + Order (or degree) of the polynomial decision rule functions for approximating the adjustability of the second stage variables with respect to the uncertain parameters. - + Choices are: - + - 0: static recourse - 1: affine recourse - 2: quadratic recourse From 1bda0d3c2d38c6423c0c698f87eec265d311794d Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Feb 2024 20:26:07 -0500 Subject: [PATCH 0460/3044] Update documentation of mandatory args --- pyomo/contrib/pyros/pyros.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 962ae79a436..316a5869057 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -273,21 +273,25 @@ def solve( ---------- model: ConcreteModel The deterministic model. - first_stage_variables: list of Var + first_stage_variables: VarData, Var, or iterable of VarData/Var First-stage model variables (or design variables). - second_stage_variables: list of Var + second_stage_variables: VarData, Var, or iterable of VarData/Var Second-stage model variables (or control variables). - uncertain_params: list of Param + uncertain_params: ParamData, Param, or iterable of ParamData/Param Uncertain model parameters. - The `mutable` attribute for every uncertain parameter - objects must be set to True. + The `mutable` attribute for all uncertain parameter objects + must be set to True. uncertainty_set: UncertaintySet Uncertainty set against which the solution(s) returned will be confirmed to be robust. - local_solver: Solver + local_solver: str or solver type Subordinate local NLP solver. - global_solver: Solver + If a `str` is passed, then the `str` is cast to + ``SolverFactory(local_solver)``. + global_solver: str or solver type Subordinate global NLP solver. + If a `str` is passed, then the `str` is cast to + ``SolverFactory(global_solver)``. Returns ------- From ecc2df8d91bc6e98cb612be631ca197e79adecf4 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Feb 2024 22:19:59 -0500 Subject: [PATCH 0461/3044] Add more rigorous `InputDataStandardizer` checks --- pyomo/contrib/pyros/config.py | 177 +++++++++++++++++++++++++++++++--- 1 file changed, 164 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index bd87dc743f9..c118b650208 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -3,6 +3,9 @@ """ +from collections.abc import Iterable + +from pyomo.common.collections import ComponentSet from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat from pyomo.core.base import Var, _VarData from pyomo.core.base.param import Param, _ParamData @@ -64,23 +67,166 @@ def __call__(self, obj): ) +def mutable_param_validator(param_obj): + """ + Check that Param-like object has attribute `mutable=True`. + + Parameters + ---------- + param_obj : Param or _ParamData + Param-like object of interest. + + Raises + ------ + ValueError + If lengths of the param object and the accompanying + index set do not match. This may occur if some entry + of the Param is not initialized. + ValueError + If attribute `mutable` is of value False. + """ + if len(param_obj) != len(param_obj.index_set()): + raise ValueError( + f"Length of Param component object with " + f"name {param_obj.name!r} is {len(param_obj)}, " + "and does not match that of its index set, " + f"which is of length {len(param_obj.index_set())}. " + "Check that all entries of the component object " + "have been initialized." + ) + if not param_obj.mutable: + raise ValueError( + f"Param object with name {param_obj.name!r} is immutable." + ) + + class InputDataStandardizer(object): - def __init__(self, ctype, cdatatype): + """ + Standardizer for objects castable to a list of Pyomo + component types. + + Parameters + ---------- + ctype : type + Pyomo component type, such as Component, Var or Param. + cdatatype : type + Corresponding Pyomo component data type, such as + _ComponentData, _VarData, or _ParamData. + ctype_validator : callable, optional + Validator function for objects of type `ctype`. + cdatatype_validator : callable, optional + Validator function for objects of type `cdatatype`. + allow_repeats : bool, optional + True to allow duplicate component data entries in final + list to which argument is cast, False otherwise. + + Attributes + ---------- + ctype + cdatatype + ctype_validator + cdatatype_validator + allow_repeats + """ + + def __init__( + self, + ctype, + cdatatype, + ctype_validator=None, + cdatatype_validator=None, + allow_repeats=False, + ): + """Initialize self (see class docstring).""" self.ctype = ctype self.cdatatype = cdatatype + self.ctype_validator = ctype_validator + self.cdatatype_validator = cdatatype_validator + self.allow_repeats = allow_repeats + + def standardize_ctype_obj(self, obj): + """ + Standardize object of type ``self.ctype`` to list + of objects of type ``self.cdatatype``. + """ + if self.ctype_validator is not None: + self.ctype_validator(obj) + return list(obj.values()) + + def standardize_cdatatype_obj(self, obj): + """ + Standarize object of type ``self.cdatatype`` to + ``[obj]``. + """ + if self.cdatatype_validator is not None: + self.cdatatype_validator(obj) + return [obj] + + def __call__(self, obj, from_iterable=None, allow_repeats=None): + """ + Cast object to a flat list of Pyomo component data type + entries. + + Parameters + ---------- + obj : object + Object to be cast. + from_iterable : Iterable or None, optional + Iterable from which `obj` obtained, if any. + allow_repeats : bool or None, optional + True if list can contain repeated entries, + False otherwise. + + Raises + ------ + TypeError + If all entries in the resulting list + are not of type ``self.cdatatype``. + ValueError + If the resulting list contains duplicate entries. + """ + if allow_repeats is None: + allow_repeats = self.allow_repeats - def __call__(self, obj): if isinstance(obj, self.ctype): - return list(obj.values()) - if isinstance(obj, self.cdatatype): - return [obj] - ans = [] - for item in obj: - ans.extend(self.__call__(item)) - for _ in ans: - assert isinstance(_, self.cdatatype) + ans = self.standardize_ctype_obj(obj) + elif isinstance(obj, self.cdatatype): + ans = self.standardize_cdatatype_obj(obj) + elif isinstance(obj, Iterable) and not isinstance(obj, str): + ans = [] + for item in obj: + ans.extend(self.__call__(item, from_iterable=obj)) + else: + from_iterable_qual = ( + f" (entry of iterable {from_iterable})" + if from_iterable is not None + else "" + ) + raise TypeError( + f"Input object {obj!r}{from_iterable_qual} " + "is not of valid component type " + f"{self.ctype.__name__} or component data type " + f"{self.cdatatype.__name__}." + ) + + # check for duplicates if desired + if not allow_repeats and len(ans) != len(ComponentSet(ans)): + comp_name_list = [comp.name for comp in ans] + raise ValueError( + f"Standardized component list {comp_name_list} " + f"derived from input {obj} " + "contains duplicate entries." + ) + return ans + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return ( + f"{self.cdatatype.__name__}, {self.ctype.__name__}, " + f"or Iterable of {self.cdatatype.__name__}/{self.ctype.__name__}" + ) + def pyros_config(): CONFIG = ConfigDict('PyROS') @@ -146,7 +292,7 @@ def pyros_config(): "first_stage_variables", ConfigValue( default=[], - domain=InputDataStandardizer(Var, _VarData), + domain=InputDataStandardizer(Var, _VarData, allow_repeats=False), description="First-stage (or design) variables.", visibility=1, ), @@ -155,7 +301,7 @@ def pyros_config(): "second_stage_variables", ConfigValue( default=[], - domain=InputDataStandardizer(Var, _VarData), + domain=InputDataStandardizer(Var, _VarData, allow_repeats=False), description="Second-stage (or control) variables.", visibility=1, ), @@ -164,7 +310,12 @@ def pyros_config(): "uncertain_params", ConfigValue( default=[], - domain=InputDataStandardizer(Param, _ParamData), + domain=InputDataStandardizer( + ctype=Param, + cdatatype=_ParamData, + ctype_validator=mutable_param_validator, + allow_repeats=False, + ), description=( """ Uncertain model parameters. From bcf1730352471390d81fcd53d9e39d4e758d5336 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Feb 2024 22:21:07 -0500 Subject: [PATCH 0462/3044] Add tests for `InputDataStandardizer` --- pyomo/contrib/pyros/tests/test_config.py | 267 +++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 pyomo/contrib/pyros/tests/test_config.py diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py new file mode 100644 index 00000000000..d0c378a52f0 --- /dev/null +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -0,0 +1,267 @@ +""" +Test objects for construction of PyROS ConfigDict. +""" + + +import unittest + +from pyomo.core.base import ( + ConcreteModel, + Var, + _VarData, +) +from pyomo.core.base.param import Param, _ParamData +from pyomo.contrib.pyros.config import ( + InputDataStandardizer, + mutable_param_validator, +) + + +class testInputDataStandardizer(unittest.TestCase): + """ + Test standardizer method for Pyomo component-type inputs. + """ + + def test_single_component_data(self): + """ + Test standardizer works for single component + data-type entry. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + + standardizer_func = InputDataStandardizer(Var, _VarData) + + standardizer_input = mdl.v[0] + standardizer_output = standardizer_func(standardizer_input) + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + 1, + msg="Length of standardizer output is not as expected.", + ) + self.assertIs( + standardizer_output[0], + mdl.v[0], + msg=( + f"Entry {standardizer_output[0]} (id {id(standardizer_output[0])}) " + "is not identical to " + f"input component data object {mdl.v[0]} " + f"(id {id(mdl.v[0])})" + ), + ) + + def test_standardizer_indexed_component(self): + """ + Test component standardizer works on indexed component. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + + standardizer_func = InputDataStandardizer(Var, _VarData) + + standardizer_input = mdl.v + standardizer_output = standardizer_func(standardizer_input) + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + 2, + msg="Length of standardizer output is not as expected.", + ) + enum_zip = enumerate(zip(standardizer_input.values(), standardizer_output)) + for idx, (input, output) in enum_zip: + self.assertIs( + input, + output, + msg=( + f"Entry {input} (id {id(input)}) " + "is not identical to " + f"input component data object {output} " + f"(id {id(output)})" + ), + ) + + def test_standardizer_multiple_components(self): + """ + Test standardizer works on sequence of components. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + mdl.x = Var(["a", "b"]) + + standardizer_func = InputDataStandardizer(Var, _VarData) + + standardizer_input = [mdl.v[0], mdl.x] + standardizer_output = standardizer_func(standardizer_input) + expected_standardizer_output = [mdl.v[0], mdl.x["a"], mdl.x["b"]] + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + len(expected_standardizer_output), + msg="Length of standardizer output is not as expected.", + ) + enum_zip = enumerate(zip(expected_standardizer_output, standardizer_output)) + for idx, (input, output) in enum_zip: + self.assertIs( + input, + output, + msg=( + f"Entry {input} (id {id(input)}) " + "is not identical to " + f"input component data object {output} " + f"(id {id(output)})" + ), + ) + + def test_standardizer_invalid_duplicates(self): + """ + Test standardizer raises exception if input contains duplicates + and duplicates are not allowed. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + mdl.x = Var(["a", "b"]) + + standardizer_func = InputDataStandardizer(Var, _VarData, allow_repeats=False) + + exc_str = r"Standardized.*list.*contains duplicate entries\." + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func([mdl.x, mdl.v, mdl.x]) + + def test_standardizer_invalid_type(self): + """ + Test standardizer raises exception as expected + when input is of invalid type. + """ + standardizer_func = InputDataStandardizer(Var, _VarData) + + exc_str = r"Input object .*is not of valid component type.*" + with self.assertRaisesRegex(TypeError, exc_str): + standardizer_func(2) + + def test_standardizer_iterable_with_invalid_type(self): + """ + Test standardizer raises exception as expected + when input is an iterable with entries of invalid type. + """ + mdl = ConcreteModel() + mdl.v = Var([0, 1]) + standardizer_func = InputDataStandardizer(Var, _VarData) + + exc_str = r"Input object .*entry of iterable.*is not of valid component type.*" + with self.assertRaisesRegex(TypeError, exc_str): + standardizer_func([mdl.v, 2]) + + def test_standardizer_invalid_str_passed(self): + """ + Test standardizer raises exception as expected + when input is of invalid type str. + """ + standardizer_func = InputDataStandardizer(Var, _VarData) + + exc_str = r"Input object .*is not of valid component type.*" + with self.assertRaisesRegex(TypeError, exc_str): + standardizer_func("abcd") + + def test_standardizer_invalid_unintialized_params(self): + """ + Test standardizer raises exception when Param with + uninitialized entries passed. + """ + standardizer_func = InputDataStandardizer( + ctype=Param, cdatatype=_ParamData, ctype_validator=mutable_param_validator + ) + + mdl = ConcreteModel() + mdl.p = Param([0, 1]) + + exc_str = r"Length of .*does not match that of.*index set" + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(mdl.p) + + def test_standardizer_invalid_immutable_params(self): + """ + Test standardizer raises exception when immutable + Param object(s) passed. + """ + standardizer_func = InputDataStandardizer( + ctype=Param, cdatatype=_ParamData, ctype_validator=mutable_param_validator + ) + + mdl = ConcreteModel() + mdl.p = Param([0, 1], initialize=1) + + exc_str = r"Param object with name .*immutable" + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(mdl.p) + + def test_standardizer_valid_mutable_params(self): + """ + Test Param-like standardizer works as expected for sequence + of valid mutable Param objects. + """ + mdl = ConcreteModel() + mdl.p1 = Param([0, 1], initialize=0, mutable=True) + mdl.p2 = Param(["a", "b"], initialize=1, mutable=True) + + standardizer_func = InputDataStandardizer( + ctype=Param, cdatatype=_ParamData, ctype_validator=mutable_param_validator + ) + + standardizer_input = [mdl.p1[0], mdl.p2] + standardizer_output = standardizer_func(standardizer_input) + expected_standardizer_output = [mdl.p1[0], mdl.p2["a"], mdl.p2["b"]] + + self.assertIsInstance( + standardizer_output, + list, + msg=( + "Standardized output should be of type list, " + f"but is of type {standardizer_output.__class__.__name__}." + ), + ) + self.assertEqual( + len(standardizer_output), + len(expected_standardizer_output), + msg="Length of standardizer output is not as expected.", + ) + enum_zip = enumerate(zip(expected_standardizer_output, standardizer_output)) + for idx, (input, output) in enum_zip: + self.assertIs( + input, + output, + msg=( + f"Entry {input} (id {id(input)}) " + "is not identical to " + f"input component data object {output} " + f"(id {id(output)})" + ), + ) + + +if __name__ == "__main__": + unittest.main() From fc497bc6de62734f7d53895062c0fbcc014b944a Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Feb 2024 22:43:43 -0500 Subject: [PATCH 0463/3044] Refactor uncertainty set argument validation --- pyomo/contrib/pyros/config.py | 4 +-- pyomo/contrib/pyros/tests/test_config.py | 28 +++++++++++++++ pyomo/contrib/pyros/uncertainty_sets.py | 45 ++++++++++++++++++++---- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index c118b650208..632a226b47b 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -16,7 +16,7 @@ setup_pyros_logger, ValidEnum, ) -from pyomo.contrib.pyros.uncertainty_sets import uncertainty_sets +from pyomo.contrib.pyros.uncertainty_sets import UncertaintySetDomain default_pyros_solver_logger = setup_pyros_logger() @@ -330,7 +330,7 @@ def pyros_config(): "uncertainty_set", ConfigValue( default=None, - domain=uncertainty_sets, + domain=UncertaintySetDomain(), description=( """ Uncertainty set against which the diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index d0c378a52f0..6d6c3caf76b 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -14,7 +14,9 @@ from pyomo.contrib.pyros.config import ( InputDataStandardizer, mutable_param_validator, + UncertaintySetDomain, ) +from pyomo.contrib.pyros.uncertainty_sets import BoxSet class testInputDataStandardizer(unittest.TestCase): @@ -263,5 +265,31 @@ def test_standardizer_valid_mutable_params(self): ) +class TestUncertaintySetDomain(unittest.TestCase): + """ + Test domain validator for uncertainty set arguments. + """ + def test_uncertainty_set_domain_valid_set(self): + """ + Test validator works for valid argument. + """ + standardizer_func = UncertaintySetDomain() + bset = BoxSet([[0, 1]]) + self.assertIs( + bset, + standardizer_func(bset), + msg="Output of uncertainty set domain not as expected.", + ) + + def test_uncertainty_set_domain_invalid_type(self): + """ + Test validator works for valid argument. + """ + standardizer_func = UncertaintySetDomain() + exc_str = "Expected an .*UncertaintySet object.*received object 2" + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(2) + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 1b51e41fcaf..4a2f198bc17 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -272,12 +272,45 @@ def generate_shape_str(shape, required_shape): ) -def uncertainty_sets(obj): - if not isinstance(obj, UncertaintySet): - raise ValueError( - "Expected an UncertaintySet object, instead received %s" % (obj,) - ) - return obj +class UncertaintySetDomain: + """ + Domain validator for uncertainty set argument. + """ + def __call__(self, obj): + """ + Type validate uncertainty set object. + + Parameters + ---------- + obj : object + Object to validate. + + Returns + ------- + obj : object + Object that was passed, provided type validation successful. + + Raises + ------ + ValueError + If type validation failed. + """ + if not isinstance(obj, UncertaintySet): + raise ValueError( + f"Expected an {UncertaintySet.__name__} object, " + f"instead received object {obj}" + ) + return obj + + def domain_name(self): + """ + Domain name of self. + """ + return UncertaintySet.__name__ + + +# maintain compatibility with prior versions +uncertainty_sets = UncertaintySetDomain() def column(matrix, i): From 497628586141007f142b4a8aa840d57662cc8e94 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 10:13:22 -0500 Subject: [PATCH 0464/3044] Add more rigorous checks for solver-like args --- pyomo/contrib/pyros/config.py | 282 +++++++++++++++++++++-- pyomo/contrib/pyros/tests/test_config.py | 222 ++++++++++++++++++ pyomo/contrib/pyros/tests/test_grcs.py | 89 ++++++- 3 files changed, 567 insertions(+), 26 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 632a226b47b..b31e404f2f6 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -7,6 +7,7 @@ from pyomo.common.collections import ComponentSet from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat +from pyomo.common.errors import ApplicationError from pyomo.core.base import Var, _VarData from pyomo.core.base.param import Param, _ParamData from pyomo.opt import SolverFactory @@ -46,27 +47,6 @@ def PositiveIntOrMinusOne(obj): return ans -class SolverResolvable(object): - def __call__(self, obj): - ''' - if obj is a string, return the Solver object for that solver name - if obj is a Solver object, return a copy of the Solver - if obj is a list, and each element of list is solver resolvable, - return list of solvers - ''' - if isinstance(obj, str): - return SolverFactory(obj.lower()) - elif callable(getattr(obj, "solve", None)): - return obj - elif isinstance(obj, list): - return [self(o) for o in obj] - else: - raise ValueError( - "Expected a Pyomo solver or string object, " - "instead received {0}".format(obj.__class__.__name__) - ) - - def mutable_param_validator(param_obj): """ Check that Param-like object has attribute `mutable=True`. @@ -228,6 +208,247 @@ def domain_name(self): ) +class NotSolverResolvable(Exception): + """ + Exception type for failure to cast an object to a Pyomo solver. + """ + + +class SolverResolvable(object): + """ + Callable for casting an object (such as a str) + to a Pyomo solver. + + Parameters + ---------- + require_available : bool, optional + True if `available()` method of a standardized solver + object obtained through `self` must return `True`, + False otherwise. + solver_desc : str, optional + Descriptor for the solver obtained through `self`, + such as 'local solver' + or 'global solver'. This argument is used + for constructing error/exception messages. + + Attributes + ---------- + require_available + solver_desc + """ + + def __init__(self, require_available=True, solver_desc="solver"): + """Initialize self (see class docstring).""" + self.require_available = require_available + self.solver_desc = solver_desc + + @staticmethod + def is_solver_type(obj): + """ + Return True if object is considered a Pyomo solver, + False otherwise. + + An object is considered a Pyomo solver provided that + it has callable attributes named 'solve' and + 'available'. + """ + return callable(getattr(obj, "solve", None)) and callable( + getattr(obj, "available", None) + ) + + def __call__(self, obj, require_available=None, solver_desc=None): + """ + Cast object to a Pyomo solver. + + If `obj` is a string, then ``SolverFactory(obj.lower())`` + is returned. If `obj` is a Pyomo solver type, then + `obj` is returned. + + Parameters + ---------- + obj : object + Object to be cast to Pyomo solver type. + require_available : bool or None, optional + True if `available()` method of the resolved solver + object must return True, False otherwise. + If `None` is passed, then ``self.require_available`` + is used. + solver_desc : str or None, optional + Brief description of the solver, such as 'local solver' + or 'backup global solver'. This argument is used + for constructing error/exception messages. + If `None` is passed, then ``self.solver_desc`` + is used. + + Returns + ------- + Solver + Pyomo solver. + + Raises + ------ + NotSolverResolvable + If `obj` cannot be cast to a Pyomo solver because + it is neither a str nor a Pyomo solver type. + ApplicationError + In event that solver is not available, the + method `available(exception_flag=True)` of the + solver to which `obj` is cast should raise an + exception of this type. The present method + will also emit a more detailed error message + through the default PyROS logger. + """ + # resort to defaults if necessary + if require_available is None: + require_available = self.require_available + if solver_desc is None: + solver_desc = self.solver_desc + + # perform casting + if isinstance(obj, str): + solver = SolverFactory(obj.lower()) + elif self.is_solver_type(obj): + solver = obj + else: + raise NotSolverResolvable( + f"Cannot cast object `{obj!r}` to a Pyomo optimizer for use as " + f"{solver_desc}, as the object is neither a str nor a " + f"Pyomo Solver type (got type {type(obj).__name__})." + ) + + # availability check, if so desired + if require_available: + try: + solver.available(exception_flag=True) + except ApplicationError: + default_pyros_solver_logger.exception( + f"Output of `available()` method for {solver_desc} " + f"with repr {solver!r} resolved from object {obj} " + "is not `True`. " + "Check solver and any required dependencies " + "have been set up properly." + ) + raise + + return solver + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "str or Solver" + + +class SolverIterable(object): + """ + Callable for casting an iterable (such as a list of strs) + to a list of Pyomo solvers. + + Parameters + ---------- + require_available : bool, optional + True if `available()` method of a standardized solver + object obtained through `self` must return `True`, + False otherwise. + filter_by_availability : bool, optional + True to remove standardized solvers for which `available()` + does not return True, False otherwise. + solver_desc : str, optional + Descriptor for the solver obtained through `self`, + such as 'backup local solver' + or 'backup global solver'. + """ + + def __init__( + self, + require_available=True, + filter_by_availability=True, + solver_desc="solver", + ): + """Initialize self (see class docstring). + + """ + self.require_available = require_available + self.filter_by_availability = filter_by_availability + self.solver_desc = solver_desc + + def __call__( + self, + obj, + require_available=None, + filter_by_availability=None, + solver_desc=None, + ): + """ + Cast iterable object to a list of Pyomo solver objects. + + Parameters + ---------- + obj : str, Solver, or Iterable of str/Solver + Object of interest. + require_available : bool or None, optional + True if `available()` method of each solver + object must return True, False otherwise. + If `None` is passed, then ``self.require_available`` + is used. + solver_desc : str or None, optional + Descriptor for the solver, such as 'backup local solver' + or 'backup global solver'. This argument is used + for constructing error/exception messages. + If `None` is passed, then ``self.solver_desc`` + is used. + + Returns + ------- + solvers : list of solver type + List of solver objects to which obj is cast. + + Raises + ------ + TypeError + If `obj` is a str. + """ + if require_available is None: + require_available = self.require_available + if filter_by_availability is None: + filter_by_availability = self.filter_by_availability + if solver_desc is None: + solver_desc = self.solver_desc + + solver_resolve_func = SolverResolvable() + + if isinstance(obj, str) or solver_resolve_func.is_solver_type(obj): + # single solver resolvable is cast to singleton list. + # perform explicit check for str, otherwise this method + # would attempt to resolve each character. + obj_as_list = [obj] + else: + obj_as_list = list(obj) + + solvers = [] + for idx, val in enumerate(obj_as_list): + solver_desc_str = f"{solver_desc} " f"(index {idx})" + opt = solver_resolve_func( + obj=val, + require_available=require_available, + solver_desc=solver_desc_str, + ) + if filter_by_availability and not opt.available(exception_flag=False): + default_pyros_solver_logger.warning( + f"Output of `available()` method for solver object {opt} " + f"resolved from object {val} of sequence {obj_as_list} " + f"to be used as {self.solver_desc} " + "is not `True`. " + "Removing from list of standardized solvers." + ) + else: + solvers.append(opt) + + return solvers + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "str, solver type, or Iterable of str/solver type" + + def pyros_config(): CONFIG = ConfigDict('PyROS') @@ -345,7 +566,7 @@ def pyros_config(): "local_solver", ConfigValue( default=None, - domain=SolverResolvable(), + domain=SolverResolvable(solver_desc="local solver", require_available=True), description="Subordinate local NLP solver.", visibility=1, ), @@ -354,7 +575,10 @@ def pyros_config(): "global_solver", ConfigValue( default=None, - domain=SolverResolvable(), + domain=SolverResolvable( + solver_desc="global solver", + require_available=True, + ), description="Subordinate global NLP solver.", visibility=1, ), @@ -525,7 +749,11 @@ def pyros_config(): "backup_local_solvers", ConfigValue( default=[], - domain=SolverResolvable(), + domain=SolverIterable( + solver_desc="backup local solver", + require_available=False, + filter_by_availability=True, + ), doc=( """ Additional subordinate local NLP optimizers to invoke @@ -539,7 +767,11 @@ def pyros_config(): "backup_global_solvers", ConfigValue( default=[], - domain=SolverResolvable(), + domain=SolverIterable( + solver_desc="backup global solver", + require_available=False, + filter_by_availability=True, + ), doc=( """ Additional subordinate global NLP optimizers to invoke diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 6d6c3caf76b..05ea35c3dda 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -3,6 +3,7 @@ """ +import logging import unittest from pyomo.core.base import ( @@ -10,12 +11,18 @@ Var, _VarData, ) +from pyomo.common.log import LoggingIntercept +from pyomo.common.errors import ApplicationError from pyomo.core.base.param import Param, _ParamData from pyomo.contrib.pyros.config import ( InputDataStandardizer, mutable_param_validator, + NotSolverResolvable, + SolverIterable, + SolverResolvable, UncertaintySetDomain, ) +from pyomo.opt import SolverFactory, SolverResults from pyomo.contrib.pyros.uncertainty_sets import BoxSet @@ -291,5 +298,220 @@ def test_uncertainty_set_domain_invalid_type(self): standardizer_func(2) +class UnavailableSolver: + def available(self, exception_flag=True): + if exception_flag: + raise ApplicationError(f"Solver {self.__class__} not available") + return False + + def solve(self, model, *args, **kwargs): + return SolverResults() + + +class TestSolverResolvable(unittest.TestCase): + """ + Test PyROS standardizer for solver-type objects. + """ + + def test_solver_resolvable_valid_str(self): + """ + Test solver resolvable class is valid for string + type. + """ + solver_str = "ipopt" + standardizer_func = SolverResolvable() + solver = standardizer_func(solver_str) + expected_solver_type = type(SolverFactory(solver_str)) + + self.assertIsInstance( + solver, + type(SolverFactory(solver_str)), + msg=( + "SolverResolvable object should be of type " + f"{expected_solver_type.__name__}, " + f"but got object of type {solver.__class__.__name__}." + ), + ) + + def test_solver_resolvable_valid_solver_type(self): + """ + Test solver resolvable class is valid for string + type. + """ + solver = SolverFactory("ipopt") + standardizer_func = SolverResolvable() + standardized_solver = standardizer_func(solver) + + self.assertIs( + solver, + standardized_solver, + msg=( + f"Test solver {solver} and standardized solver " + f"{standardized_solver} are not identical." + ), + ) + + def test_solver_resolvable_invalid_type(self): + """ + Test solver resolvable object raises expected + exception when invalid entry is provided. + """ + invalid_object = 2 + standardizer_func = SolverResolvable(solver_desc="local solver") + + exc_str = ( + r"Cannot cast object `2` to a Pyomo optimizer.*" + r"local solver.*got type int.*" + ) + with self.assertRaisesRegex(NotSolverResolvable, exc_str): + standardizer_func(invalid_object) + + def test_solver_resolvable_unavailable_solver(self): + """ + Test solver standardizer fails in event solver is + unavaiable. + """ + unavailable_solver = UnavailableSolver() + standardizer_func = SolverResolvable( + solver_desc="local solver", require_available=True + ) + + exc_str = r"Solver.*UnavailableSolver.*not available" + with self.assertRaisesRegex(ApplicationError, exc_str): + with LoggingIntercept(level=logging.ERROR) as LOG: + standardizer_func(unavailable_solver) + + error_msgs = LOG.getvalue()[:-1] + self.assertRegex( + error_msgs, r"Output of `available\(\)` method.*local solver.*" + ) + + +class TestSolverIterable(unittest.TestCase): + """ + Test standardizer method for iterable of solvers, + used to validate `backup_local_solvers` and `backup_global_solvers` + arguments. + """ + + def test_solver_iterable_valid_list(self): + """ + Test solver type standardizer works for list of valid + objects castable to solver. + """ + solver_list = ["ipopt", SolverFactory("ipopt")] + expected_solver_types = [type(SolverFactory("ipopt"))] * 2 + standardizer_func = SolverIterable() + + standardized_solver_list = standardizer_func(solver_list) + + # check list of solver types returned + for idx, standardized_solver in enumerate(standardized_solver_list): + self.assertIsInstance( + standardized_solver, + expected_solver_types[idx], + msg=( + f"Standardized solver {standardized_solver} " + f"(index {idx}) expected to be of type " + f"{expected_solver_types[idx].__name__}, " + f"but is of type {standardized_solver.__class__.__name__}" + ), + ) + + # second entry of standardized solver list should be the same + # object as that of input list, since the input solver is a Pyomo + # solver type + self.assertIs( + standardized_solver_list[1], + solver_list[1], + msg=( + f"Test solver {solver_list[1]} and standardized solver " + f"{standardized_solver_list[1]} should be identical." + ), + ) + + def test_solver_iterable_valid_str(self): + """ + Test SolverIterable raises exception when str passed. + """ + solver_str = "ipopt" + standardizer_func = SolverIterable() + + solver_list = standardizer_func(solver_str) + self.assertEqual( + len(solver_list), 1, "Standardized solver list is not of expected length" + ) + + def test_solver_iterable_unavailable_solver(self): + """ + Test SolverIterable addresses unavailable solvers appropriately. + """ + solvers = (SolverFactory("ipopt"), UnavailableSolver()) + + standardizer_func = SolverIterable( + require_available=True, + filter_by_availability=True, + solver_desc="example solver list", + ) + exc_str = r"Solver.*UnavailableSolver.* not available" + with self.assertRaisesRegex(ApplicationError, exc_str): + standardizer_func(solvers) + with self.assertRaisesRegex(ApplicationError, exc_str): + standardizer_func(solvers, filter_by_availability=False) + + standardized_solver_list = standardizer_func( + solvers, + filter_by_availability=True, + require_available=False, + ) + self.assertEqual( + len(standardized_solver_list), + 1, + msg=( + "Length of filtered standardized solver list not as " + "expected." + ), + ) + self.assertIs( + standardized_solver_list[0], + solvers[0], + msg="Entry of filtered standardized solver list not as expected.", + ) + + standardized_solver_list = standardizer_func( + solvers, + filter_by_availability=False, + require_available=False, + ) + self.assertEqual( + len(standardized_solver_list), + 2, + msg=( + "Length of filtered standardized solver list not as " + "expected." + ), + ) + self.assertEqual( + standardized_solver_list, + list(solvers), + msg="Entry of filtered standardized solver list not as expected.", + ) + + def test_solver_iterable_invalid_list(self): + """ + Test SolverIterable raises exception if iterable contains + at least one invalid object. + """ + invalid_object = ["ipopt", 2] + standardizer_func = SolverIterable(solver_desc="backup solver") + + exc_str = ( + r"Cannot cast object `2` to a Pyomo optimizer.*" + r"backup solver.*index 1.*got type int.*" + ) + with self.assertRaisesRegex(NotSolverResolvable, exc_str): + standardizer_func(invalid_object) + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 8de1c2666b9..a05e5f06134 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -137,7 +137,7 @@ def __init__(self, calls_to_sleep, max_time, sub_solver): self.num_calls = 0 self.options = Bunch() - def available(self): + def available(self, exception_flag=True): return True def license_is_valid(self): @@ -6302,5 +6302,92 @@ def test_log_disclaimer(self): ) +class UnavailableSolver: + def available(self, exception_flag=True): + if exception_flag: + raise ApplicationError(f"Solver {self.__class__} not available") + return False + + def solve(self, model, *args, **kwargs): + return SolverResults() + + +class TestPyROSUnavailableSubsolvers(unittest.TestCase): + """ + Check that appropriate exceptionsa are raised if + PyROS is invoked with unavailable subsolvers. + """ + + def test_pyros_unavailable_subsolver(self): + """ + Test PyROS raises expected error message when + unavailable subsolver is passed. + """ + m = ConcreteModel() + m.p = Param(range(3), initialize=0, mutable=True) + m.z = Var([0, 1], initialize=0) + m.con = Constraint(expr=m.z[0] + m.z[1] >= m.p[0]) + m.obj = Objective(expr=m.z[0] + m.z[1]) + + pyros_solver = SolverFactory("pyros") + + exc_str = r".*Solver.*UnavailableSolver.*not available" + with self.assertRaisesRegex(ValueError, exc_str): + # note: ConfigDict interface raises ValueError + # once any exception is triggered, + # so we check for that instead of ApplicationError + with LoggingIntercept(level=logging.ERROR) as LOG: + pyros_solver.solve( + model=m, + first_stage_variables=[m.z[0]], + second_stage_variables=[m.z[1]], + uncertain_params=[m.p[0]], + uncertainty_set=BoxSet([[0, 1]]), + local_solver=SolverFactory("ipopt"), + global_solver=UnavailableSolver(), + ) + + error_msgs = LOG.getvalue()[:-1] + self.assertRegex( + error_msgs, r"Output of `available\(\)` method.*global solver.*" + ) + + def test_pyros_unavailable_backup_subsolver(self): + """ + Test PyROS raises expected error message when + unavailable backup subsolver is passed. + """ + m = ConcreteModel() + m.p = Param(range(3), initialize=0, mutable=True) + m.z = Var([0, 1], initialize=0) + m.con = Constraint(expr=m.z[0] + m.z[1] >= m.p[0]) + m.obj = Objective(expr=m.z[0] + m.z[1]) + + pyros_solver = SolverFactory("pyros") + + # note: ConfigDict interface raises ValueError + # once any exception is triggered, + # so we check for that instead of ApplicationError + with LoggingIntercept(level=logging.WARNING) as LOG: + pyros_solver.solve( + model=m, + first_stage_variables=[m.z[0]], + second_stage_variables=[m.z[1]], + uncertain_params=[m.p[0]], + uncertainty_set=BoxSet([[0, 1]]), + local_solver=SolverFactory("ipopt"), + global_solver=SolverFactory("ipopt"), + backup_global_solvers=[UnavailableSolver()], + bypass_global_separation=True, + ) + + error_msgs = LOG.getvalue()[:-1] + self.assertRegex( + error_msgs, + r"Output of `available\(\)` method.*backup global solver.*" + r"Removing from list.*" + ) + + if __name__ == "__main__": unittest.main() From 9e89fee1d9f2984b664fcc68715843410a9feb21 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 10:38:23 -0500 Subject: [PATCH 0465/3044] Extend domain of objective focus argument --- pyomo/contrib/pyros/config.py | 5 ++-- pyomo/contrib/pyros/tests/test_config.py | 37 ++++++++++++++++++++++++ pyomo/contrib/pyros/util.py | 18 ------------ 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index b31e404f2f6..5c51ab546db 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -6,7 +6,7 @@ from collections.abc import Iterable from pyomo.common.collections import ComponentSet -from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat +from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat, InEnum from pyomo.common.errors import ApplicationError from pyomo.core.base import Var, _VarData from pyomo.core.base.param import Param, _ParamData @@ -15,7 +15,6 @@ a_logger, ObjectiveType, setup_pyros_logger, - ValidEnum, ) from pyomo.contrib.pyros.uncertainty_sets import UncertaintySetDomain @@ -590,7 +589,7 @@ def pyros_config(): "objective_focus", ConfigValue( default=ObjectiveType.nominal, - domain=ValidEnum(ObjectiveType), + domain=InEnum(ObjectiveType), description=( """ Choice of objective focus to optimize in the master problems. diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 05ea35c3dda..727c5443315 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -21,7 +21,9 @@ SolverIterable, SolverResolvable, UncertaintySetDomain, + pyros_config, ) +from pyomo.contrib.pyros.util import ObjectiveType from pyomo.opt import SolverFactory, SolverResults from pyomo.contrib.pyros.uncertainty_sets import BoxSet @@ -513,5 +515,40 @@ def test_solver_iterable_invalid_list(self): standardizer_func(invalid_object) +class TestPyROSConfig(unittest.TestCase): + """ + Test PyROS ConfigDict behaves as expected. + """ + + CONFIG = pyros_config() + + def test_config_objective_focus(self): + """ + Test config parses objective focus as expected. + """ + config = self.CONFIG() + + for obj_focus_name in ["nominal", "worst_case"]: + config.objective_focus = obj_focus_name + self.assertEqual( + config.objective_focus, + ObjectiveType[obj_focus_name], + msg="Objective focus not set as expected." + ) + + for obj_focus in ObjectiveType: + config.objective_focus = obj_focus + self.assertEqual( + config.objective_focus, + obj_focus, + msg="Objective focus not set as expected." + ) + + invalid_focus = "test_example" + exc_str = f".*{invalid_focus!r} is not a valid ObjectiveType" + with self.assertRaisesRegex(ValueError, exc_str): + config.objective_focus = invalid_focus + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index e2986ae18c7..e67d55dfb68 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -461,24 +461,6 @@ def a_logger(str_or_logger): return logging.getLogger(str_or_logger) -def ValidEnum(enum_class): - ''' - Python 3 dependent format string - ''' - - def fcn(obj): - if obj not in enum_class: - raise ValueError( - "Expected an {0} object, " - "instead received {1}".format( - enum_class.__name__, obj.__class__.__name__ - ) - ) - return obj - - return fcn - - class pyrosTerminationCondition(Enum): """Enumeration of all possible PyROS termination conditions.""" From 039171f13bcbc6efc913466d67db15c1c251156d Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 10:47:27 -0500 Subject: [PATCH 0466/3044] Extend domain for path-like args --- pyomo/contrib/pyros/config.py | 56 ++++++++++++++- pyomo/contrib/pyros/tests/test_config.py | 92 +++++++++++++++++++++++- 2 files changed, 145 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 5c51ab546db..90d8e8cfb3e 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -4,9 +4,10 @@ from collections.abc import Iterable +import os from pyomo.common.collections import ComponentSet -from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat, InEnum +from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat, InEnum, Path from pyomo.common.errors import ApplicationError from pyomo.core.base import Var, _VarData from pyomo.core.base.param import Param, _ParamData @@ -46,6 +47,57 @@ def PositiveIntOrMinusOne(obj): return ans +class PathLikeOrNone: + """ + Validator for path-like objects. + + This interface is a wrapper around the domain validator + ``common.config.Path``, and extends the domain of interest to + to include: + - None + - objects following the Python ``os.PathLike`` protocol. + + Parameters + ---------- + **config_path_kwargs : dict + Keyword arguments to ``common.config.Path``. + """ + + def __init__(self, **config_path_kwargs): + """Initialize self (see class docstring).""" + self.config_path = Path(**config_path_kwargs) + + def __call__(self, path): + """ + Cast path to expanded string representation. + + Parameters + ---------- + path : None str, bytes, or path-like + Object to be cast. + + Returns + ------- + None + If obj is None. + str + String representation of path-like object. + """ + if path is None: + return path + + # prevent common.config.Path from invoking + # str() on the path-like object + path_str = os.fsdecode(path) + + # standardize path str as necessary + return self.config_path(path_str) + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "path-like or None" + + def mutable_param_validator(param_obj): """ Check that Param-like object has attribute `mutable=True`. @@ -784,7 +836,7 @@ def pyros_config(): "subproblem_file_directory", ConfigValue( default=None, - domain=str, + domain=PathLikeOrNone(), description=( """ Directory to which to export subproblems not successfully diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 727c5443315..2e957cc7df6 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -4,6 +4,7 @@ import logging +import os import unittest from pyomo.core.base import ( @@ -11,6 +12,7 @@ Var, _VarData, ) +from pyomo.common.config import Path from pyomo.common.log import LoggingIntercept from pyomo.common.errors import ApplicationError from pyomo.core.base.param import Param, _ParamData @@ -18,10 +20,11 @@ InputDataStandardizer, mutable_param_validator, NotSolverResolvable, + PathLikeOrNone, + pyros_config, SolverIterable, SolverResolvable, UncertaintySetDomain, - pyros_config, ) from pyomo.contrib.pyros.util import ObjectiveType from pyomo.opt import SolverFactory, SolverResults @@ -550,5 +553,92 @@ def test_config_objective_focus(self): config.objective_focus = invalid_focus +class testPathLikeOrNone(unittest.TestCase): + """ + Test interface for validating path-like arguments. + """ + + def test_none_valid(self): + """ + Test `None` is valid. + """ + standardizer_func = PathLikeOrNone() + + self.assertIs( + standardizer_func(None), + None, + msg="Output of `PathLikeOrNone` standardizer not as expected.", + ) + + def test_str_bytes_path_like_valid(self): + """ + Check path-like validator handles str, bytes, and path-like + inputs correctly. + """ + + class ExamplePathLike(os.PathLike): + """ + Path-like class for testing. Key feature: __fspath__ + and __str__ return different outputs. + """ + + def __init__(self, path_str_or_bytes): + self.path = path_str_or_bytes + + def __fspath__(self): + return self.path + + def __str__(self): + path_str = os.fsdecode(self.path) + return f"{type(self).__name__}({path_str})" + + path_standardization_func = PathLikeOrNone() + + # construct path arguments of different type + path_as_str = "example_output_dir/" + path_as_bytes = os.fsencode(path_as_str) + path_like_from_str = ExamplePathLike(path_as_str) + path_like_from_bytes = ExamplePathLike(path_as_bytes) + + # for all possible arguments, output should be + # the str returned by ``common.config.Path`` when + # string representation of the path is input. + expected_output = Path()(path_as_str) + + # check output is as expected in all cases + self.assertEqual( + path_standardization_func(path_as_str), + expected_output, + msg=( + "Path-like validator output from str input " + "does not match expected value." + ), + ) + self.assertEqual( + path_standardization_func(path_as_bytes), + expected_output, + msg=( + "Path-like validator output from bytes input " + "does not match expected value." + ), + ) + self.assertEqual( + path_standardization_func(path_like_from_str), + expected_output, + msg=( + "Path-like validator output from path-like input " + "derived from str does not match expected value." + ), + ) + self.assertEqual( + path_standardization_func(path_like_from_bytes), + expected_output, + msg=( + "Path-like validator output from path-like input " + "derived from bytes does not match expected value." + ), + ) + + if __name__ == "__main__": unittest.main() From c0f2a41d439e2a45ecc0b6bd853d8216c73f6b02 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 10:56:15 -0500 Subject: [PATCH 0467/3044] Tweak domain name of path-like args --- pyomo/contrib/pyros/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 90d8e8cfb3e..a6e387b62e9 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -95,7 +95,7 @@ def __call__(self, path): def domain_name(self): """Return str briefly describing domain encompassed by self.""" - return "path-like or None" + return "str, bytes, path-like or None" def mutable_param_validator(param_obj): From 154bbba87730f62d4e270f3bd80161f51c258e46 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 10:59:23 -0500 Subject: [PATCH 0468/3044] Apply black --- pyomo/contrib/pyros/config.py | 41 +++++++++--------------- pyomo/contrib/pyros/tests/test_config.py | 29 +++++------------ pyomo/contrib/pyros/tests/test_grcs.py | 8 ++--- 3 files changed, 28 insertions(+), 50 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index a6e387b62e9..663e0252032 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -7,16 +7,19 @@ import os from pyomo.common.collections import ComponentSet -from pyomo.common.config import ConfigDict, ConfigValue, In, NonNegativeFloat, InEnum, Path +from pyomo.common.config import ( + ConfigDict, + ConfigValue, + In, + NonNegativeFloat, + InEnum, + Path, +) from pyomo.common.errors import ApplicationError from pyomo.core.base import Var, _VarData from pyomo.core.base.param import Param, _ParamData from pyomo.opt import SolverFactory -from pyomo.contrib.pyros.util import ( - a_logger, - ObjectiveType, - setup_pyros_logger, -) +from pyomo.contrib.pyros.util import a_logger, ObjectiveType, setup_pyros_logger from pyomo.contrib.pyros.uncertainty_sets import UncertaintySetDomain @@ -126,9 +129,7 @@ def mutable_param_validator(param_obj): "have been initialized." ) if not param_obj.mutable: - raise ValueError( - f"Param object with name {param_obj.name!r} is immutable." - ) + raise ValueError(f"Param object with name {param_obj.name!r} is immutable.") class InputDataStandardizer(object): @@ -409,25 +410,16 @@ class SolverIterable(object): """ def __init__( - self, - require_available=True, - filter_by_availability=True, - solver_desc="solver", - ): - """Initialize self (see class docstring). - - """ + self, require_available=True, filter_by_availability=True, solver_desc="solver" + ): + """Initialize self (see class docstring).""" self.require_available = require_available self.filter_by_availability = filter_by_availability self.solver_desc = solver_desc def __call__( - self, - obj, - require_available=None, - filter_by_availability=None, - solver_desc=None, - ): + self, obj, require_available=None, filter_by_availability=None, solver_desc=None + ): """ Cast iterable object to a list of Pyomo solver objects. @@ -627,8 +619,7 @@ def pyros_config(): ConfigValue( default=None, domain=SolverResolvable( - solver_desc="global solver", - require_available=True, + solver_desc="global solver", require_available=True ), description="Subordinate global NLP solver.", visibility=1, diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 2e957cc7df6..938fbe8b8e1 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -7,11 +7,7 @@ import os import unittest -from pyomo.core.base import ( - ConcreteModel, - Var, - _VarData, -) +from pyomo.core.base import ConcreteModel, Var, _VarData from pyomo.common.config import Path from pyomo.common.log import LoggingIntercept from pyomo.common.errors import ApplicationError @@ -281,6 +277,7 @@ class TestUncertaintySetDomain(unittest.TestCase): """ Test domain validator for uncertainty set arguments. """ + def test_uncertainty_set_domain_valid_set(self): """ Test validator works for valid argument. @@ -465,17 +462,12 @@ def test_solver_iterable_unavailable_solver(self): standardizer_func(solvers, filter_by_availability=False) standardized_solver_list = standardizer_func( - solvers, - filter_by_availability=True, - require_available=False, + solvers, filter_by_availability=True, require_available=False ) self.assertEqual( len(standardized_solver_list), 1, - msg=( - "Length of filtered standardized solver list not as " - "expected." - ), + msg=("Length of filtered standardized solver list not as " "expected."), ) self.assertIs( standardized_solver_list[0], @@ -484,17 +476,12 @@ def test_solver_iterable_unavailable_solver(self): ) standardized_solver_list = standardizer_func( - solvers, - filter_by_availability=False, - require_available=False, + solvers, filter_by_availability=False, require_available=False ) self.assertEqual( len(standardized_solver_list), 2, - msg=( - "Length of filtered standardized solver list not as " - "expected." - ), + msg=("Length of filtered standardized solver list not as " "expected."), ) self.assertEqual( standardized_solver_list, @@ -536,7 +523,7 @@ def test_config_objective_focus(self): self.assertEqual( config.objective_focus, ObjectiveType[obj_focus_name], - msg="Objective focus not set as expected." + msg="Objective focus not set as expected.", ) for obj_focus in ObjectiveType: @@ -544,7 +531,7 @@ def test_config_objective_focus(self): self.assertEqual( config.objective_focus, obj_focus, - msg="Objective focus not set as expected." + msg="Objective focus not set as expected.", ) invalid_focus = "test_example" diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index a05e5f06134..a75aa4dcf41 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3766,9 +3766,9 @@ def test_solve_master(self): master_data.master_model.scenarios[0, 0].second_stage_objective = Expression( expr=master_data.master_model.scenarios[0, 0].x ) - master_data.master_model.scenarios[0, 0].util.dr_var_to_exponent_map = ( - ComponentMap() - ) + master_data.master_model.scenarios[ + 0, 0 + ].util.dr_var_to_exponent_map = ComponentMap() master_data.iteration = 0 master_data.timing = TimingData() @@ -6385,7 +6385,7 @@ def test_pyros_unavailable_backup_subsolver(self): self.assertRegex( error_msgs, r"Output of `available\(\)` method.*backup global solver.*" - r"Removing from list.*" + r"Removing from list.*", ) From d7b41d5351b57da5ae377b271aff78098de964ea Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 12:03:24 -0500 Subject: [PATCH 0469/3044] Refactor checks for int-like args --- pyomo/contrib/pyros/config.py | 60 +++++++++++++++--------- pyomo/contrib/pyros/tests/test_config.py | 35 ++++++++++++++ 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 663e0252032..19fe6c710ef 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -26,28 +26,42 @@ default_pyros_solver_logger = setup_pyros_logger() -def NonNegIntOrMinusOne(obj): - ''' - if obj is a non-negative int, return the non-negative int - if obj is -1, return -1 - else, error - ''' - ans = int(obj) - if ans != float(obj) or (ans < 0 and ans != -1): - raise ValueError("Expected non-negative int, but received %s" % (obj,)) - return ans - - -def PositiveIntOrMinusOne(obj): - ''' - if obj is a positive int, return the int - if obj is -1, return -1 - else, error - ''' - ans = int(obj) - if ans != float(obj) or (ans <= 0 and ans != -1): - raise ValueError("Expected positive int, but received %s" % (obj,)) - return ans +class PositiveIntOrMinusOne: + """ + Domain validator for objects castable to a + strictly positive int or -1. + """ + + def __call__(self, obj): + """ + Cast object to positive int or -1. + + Parameters + ---------- + obj : object + Object of interest. + + Returns + ------- + int + Positive int, or -1. + + Raises + ------ + ValueError + If object not castable to positive int, or -1. + """ + ans = int(obj) + if ans != float(obj) or (ans <= 0 and ans != -1): + raise ValueError( + "Expected positive int or -1, " + f"but received value {obj!r}" + ) + return ans + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "positive int or -1" class PathLikeOrNone: @@ -729,7 +743,7 @@ def pyros_config(): "max_iter", ConfigValue( default=-1, - domain=PositiveIntOrMinusOne, + domain=PositiveIntOrMinusOne(), description=( """ Iteration limit. If -1 is provided, then no iteration diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 938fbe8b8e1..4417966bf72 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -17,6 +17,7 @@ mutable_param_validator, NotSolverResolvable, PathLikeOrNone, + PositiveIntOrMinusOne, pyros_config, SolverIterable, SolverResolvable, @@ -627,5 +628,39 @@ def __str__(self): ) +class TestPositiveIntOrMinusOne(unittest.TestCase): + """ + Test validator for -1 or positive int works as expected. + """ + + def test_positive_int_or_minus_one(self): + """ + Test positive int or -1 validator works as expected. + """ + standardizer_func = PositiveIntOrMinusOne() + self.assertIs( + standardizer_func(1.0), + 1, + msg=( + f"{PositiveIntOrMinusOne.__name__} " + "does not standardize as expected." + ), + ) + self.assertEqual( + standardizer_func(-1.00), + -1, + msg=( + f"{PositiveIntOrMinusOne.__name__} " + "does not standardize as expected." + ), + ) + + exc_str = r"Expected positive int or -1, but received value.*" + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(1.5) + with self.assertRaisesRegex(ValueError, exc_str): + standardizer_func(0) + + if __name__ == "__main__": unittest.main() From 2a0c7907a11972f5ec7e41a879a7ce6e3b8f18d7 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 12:46:33 -0500 Subject: [PATCH 0470/3044] Refactor logger type validator --- pyomo/contrib/pyros/config.py | 40 ++++++++++++++++++++++-- pyomo/contrib/pyros/tests/test_config.py | 28 +++++++++++++++++ pyomo/contrib/pyros/util.py | 27 ---------------- 3 files changed, 65 insertions(+), 30 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 19fe6c710ef..8bafb4ea6dd 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -4,6 +4,7 @@ from collections.abc import Iterable +import logging import os from pyomo.common.collections import ComponentSet @@ -19,13 +20,45 @@ from pyomo.core.base import Var, _VarData from pyomo.core.base.param import Param, _ParamData from pyomo.opt import SolverFactory -from pyomo.contrib.pyros.util import a_logger, ObjectiveType, setup_pyros_logger +from pyomo.contrib.pyros.util import ObjectiveType, setup_pyros_logger from pyomo.contrib.pyros.uncertainty_sets import UncertaintySetDomain default_pyros_solver_logger = setup_pyros_logger() +class LoggerType: + """ + Domain validator for objects castable to logging.Logger. + """ + + def __call__(self, obj): + """ + Cast object to logger. + + Parameters + ---------- + obj : object + Object to be cast. + + Returns + ------- + logging.Logger + If `str_or_logger` is of type `logging.Logger`,then + `str_or_logger` is returned. + Otherwise, ``logging.getLogger(str_or_logger)`` + is returned. + """ + if isinstance(obj, logging.Logger): + return obj + else: + return logging.getLogger(obj) + + def domain_name(self): + """Return str briefly describing domain encompassed by self.""" + return "None, str or logging.Logger" + + class PositiveIntOrMinusOne: """ Domain validator for objects castable to a @@ -788,11 +821,12 @@ def pyros_config(): "progress_logger", ConfigValue( default=default_pyros_solver_logger, - domain=a_logger, + domain=LoggerType(), doc=( """ Logger (or name thereof) used for reporting PyROS solver - progress. If a `str` is specified, then ``progress_logger`` + progress. If `None` or a `str` is provided, then + ``progress_logger`` is cast to ``logging.getLogger(progress_logger)``. In the default case, `progress_logger` is set to a :class:`pyomo.contrib.pyros.util.PreformattedLogger` diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 4417966bf72..73a6678bb9d 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -15,6 +15,7 @@ from pyomo.contrib.pyros.config import ( InputDataStandardizer, mutable_param_validator, + LoggerType, NotSolverResolvable, PathLikeOrNone, PositiveIntOrMinusOne, @@ -662,5 +663,32 @@ def test_positive_int_or_minus_one(self): standardizer_func(0) +class TestLoggerType(unittest.TestCase): + """ + Test logger type validator. + """ + + def test_logger_type(self): + """ + Test logger type validator. + """ + standardizer_func = LoggerType() + mylogger = logging.getLogger("example") + self.assertIs( + standardizer_func(mylogger), + mylogger, + msg=f"{LoggerType.__name__} output not as expected", + ) + self.assertIs( + standardizer_func(mylogger.name), + mylogger, + msg=f"{LoggerType.__name__} output not as expected", + ) + + exc_str = r"A logger name must be a string" + with self.assertRaisesRegex(Exception, exc_str): + standardizer_func(2) + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index e67d55dfb68..30b5d2df427 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -434,33 +434,6 @@ def setup_pyros_logger(name=DEFAULT_LOGGER_NAME): return logger -def a_logger(str_or_logger): - """ - Standardize a string or logger object to a logger object. - - Parameters - ---------- - str_or_logger : str or logging.Logger - String or logger object to normalize. - - Returns - ------- - logging.Logger - If `str_or_logger` is of type `logging.Logger`,then - `str_or_logger` is returned. - Otherwise, ``logging.getLogger(str_or_logger)`` - is returned. In the event `str_or_logger` is - the name of the default PyROS logger, the logger level - is set to `logging.INFO`, and a `PreformattedLogger` - instance is returned in lieu of a standard `Logger` - instance. - """ - if isinstance(str_or_logger, logging.Logger): - return logging.getLogger(str_or_logger.name) - else: - return logging.getLogger(str_or_logger) - - class pyrosTerminationCondition(Enum): """Enumeration of all possible PyROS termination conditions.""" From 14421fa02711efa171f3e1997d358e88c5b5bf70 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 12:58:39 -0500 Subject: [PATCH 0471/3044] Apply black --- pyomo/contrib/pyros/config.py | 5 +---- pyomo/contrib/pyros/tests/test_config.py | 6 ++---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 8bafb4ea6dd..17e4d3804d0 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -86,10 +86,7 @@ def __call__(self, obj): """ ans = int(obj) if ans != float(obj) or (ans <= 0 and ans != -1): - raise ValueError( - "Expected positive int or -1, " - f"but received value {obj!r}" - ) + raise ValueError(f"Expected positive int or -1, but received value {obj!r}") return ans def domain_name(self): diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 73a6678bb9d..821b1fe7d1e 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -643,16 +643,14 @@ def test_positive_int_or_minus_one(self): standardizer_func(1.0), 1, msg=( - f"{PositiveIntOrMinusOne.__name__} " - "does not standardize as expected." + f"{PositiveIntOrMinusOne.__name__} does not standardize as expected." ), ) self.assertEqual( standardizer_func(-1.00), -1, msg=( - f"{PositiveIntOrMinusOne.__name__} " - "does not standardize as expected." + f"{PositiveIntOrMinusOne.__name__} does not standardize as expected." ), ) From be98fa9721c7a7015573671238cc62180f561983 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 14:44:40 -0500 Subject: [PATCH 0472/3044] Restructure PyROS argument resolution and validation --- pyomo/contrib/pyros/config.py | 91 +++++++++++++++ pyomo/contrib/pyros/pyros.py | 71 +++++++++--- pyomo/contrib/pyros/tests/test_config.py | 66 +++++++++++ pyomo/contrib/pyros/tests/test_grcs.py | 140 +++++++++++++++++++++++ 4 files changed, 349 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 17e4d3804d0..c003d699255 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -950,3 +950,94 @@ def pyros_config(): ) return CONFIG + + +def resolve_keyword_arguments(prioritized_kwargs_dicts, func=None): + """ + Resolve the keyword arguments to a callable in the event + the arguments may have been passed in one or more possible + ways. + + A warning-level message is logged (through the default PyROS + logger) in the event an argument is specified in more than one + way. In this case, the value provided through the means with + the highest priority is selected. + + Parameters + ---------- + prioritized_kwargs_dicts : dict + Each entry maps a str to a dict of the keyword arguments + passed via the means described by the str. + Entries of `prioritized_kwargs_dicts` are taken to be + provided in descending order of priority of the means + by which the arguments may have been passed to the callable. + func : callable or None, optional + Callable to which the keyword arguments are/were passed. + Currently, only the `__name__` attribute is used, + for the purpose of logging warning-level messages. + If `None` is passed, then the warning messages + logged are slightly less informative. + + Returns + ------- + resolved_kwargs : dict + Resolved keyword arguments. + """ + # warnings are issued through logger object + default_logger = default_pyros_solver_logger + + # used for warning messages + func_desc = f"passed to {func.__name__}()" if func is not None else "passed" + + # we will loop through the priority dict. initialize: + # - resolved keyword arguments, taking into account the + # priority order and overlap + # - kwarg dicts already processed + # - sequence of kwarg dicts yet to be processed + resolved_kwargs = dict() + prev_prioritized_kwargs_dicts = dict() + remaining_kwargs_dicts = prioritized_kwargs_dicts.copy() + for curr_desc, curr_kwargs in remaining_kwargs_dicts.items(): + overlapping_args = dict() + overlapping_args_set = set() + + for prev_desc, prev_kwargs in prev_prioritized_kwargs_dicts.items(): + # determine overlap between currrent and previous + # set of kwargs, and remove overlap of current + # and higher priority sets from the result + curr_prev_overlapping_args = ( + set(curr_kwargs.keys()) & set(prev_kwargs.keys()) + ) - overlapping_args_set + if curr_prev_overlapping_args: + # if there is overlap, prepare overlapping args + # for when warning is to be issued + overlapping_args[prev_desc] = curr_prev_overlapping_args + + # update set of args overlapping with higher priority dicts + overlapping_args_set |= curr_prev_overlapping_args + + # ensure kwargs specified in higher priority + # dicts are not overwritten in resolved kwargs + resolved_kwargs.update( + { + kw: val + for kw, val in curr_kwargs.items() + if kw not in overlapping_args_set + } + ) + + # if there are overlaps, log warnings accordingly + # per priority level + for overlap_desc, args_set in overlapping_args.items(): + new_overlapping_args_str = ", ".join(f"{arg!r}" for arg in args_set) + default_logger.warning( + f"Arguments [{new_overlapping_args_str}] passed {curr_desc} " + f"already {func_desc} {overlap_desc}, " + "and will not be overwritten. " + "Consider modifying your arguments to remove the overlap." + ) + + # increment sequence of kwarg dicts already processed + prev_prioritized_kwargs_dicts[curr_desc] = curr_kwargs + + return resolved_kwargs diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 316a5869057..5dc0b1a3e39 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -20,7 +20,7 @@ from pyomo.contrib.pyros.util import time_code from pyomo.common.modeling import unique_component_name from pyomo.opt import SolverFactory -from pyomo.contrib.pyros.config import pyros_config +from pyomo.contrib.pyros.config import pyros_config, resolve_keyword_arguments from pyomo.contrib.pyros.util import ( model_is_valid, recast_to_min_obj, @@ -249,6 +249,48 @@ def _log_config(self, logger, config, exclude_options=None, **log_kwargs): logger.log(msg=f" {key}={val!r}", **log_kwargs) logger.log(msg="-" * self._LOG_LINE_LENGTH, **log_kwargs) + def _resolve_and_validate_pyros_args(self, model, **kwds): + """ + Resolve and validate arguments to ``self.solve()``. + + Parameters + ---------- + model : ConcreteModel + Deterministic model object passed to ``self.solve()``. + **kwds : dict + All other arguments to ``self.solve()``. + + Returns + ------- + config : ConfigDict + Standardized arguments. + + Note + ---- + This method can be broken down into three steps: + + 1. Resolve user arguments based on how they were passed + and order of precedence of the various means by which + they could be passed. + 2. Cast resolved arguments to ConfigDict. Argument-wise + validation is performed automatically. + 3. Inter-argument validation. + """ + options_dict = kwds.pop("options", {}) + dev_options_dict = kwds.pop("dev_options", {}) + resolved_kwds = resolve_keyword_arguments( + prioritized_kwargs_dicts={ + "explicitly": kwds, + "implicitly through argument 'options'": options_dict, + "implicitly through argument 'dev_options'": dev_options_dict, + }, + func=self.solve, + ) + config = self.CONFIG(resolved_kwds) + validate_kwarg_inputs(model, config) + + return config + @document_kwargs_from_configdict( config=CONFIG, section="Keyword Arguments", @@ -299,24 +341,15 @@ def solve( Summary of PyROS termination outcome. """ - - # === Add the explicit arguments to the config - config = self.CONFIG(kwds.pop('options', {})) - config.first_stage_variables = first_stage_variables - config.second_stage_variables = second_stage_variables - config.uncertain_params = uncertain_params - config.uncertainty_set = uncertainty_set - config.local_solver = local_solver - config.global_solver = global_solver - - dev_options = kwds.pop('dev_options', {}) - config.set_value(kwds) - config.set_value(dev_options) - - model = model - - # === Validate kwarg inputs - validate_kwarg_inputs(model, config) + kwds.update(dict( + first_stage_variables=first_stage_variables, + second_stage_variables=second_stage_variables, + uncertain_params=uncertain_params, + uncertainty_set=uncertainty_set, + local_solver=local_solver, + global_solver=global_solver, + )) + config = self._resolve_and_validate_pyros_args(model, **kwds) # === Validate ability of grcs RO solver to handle this model if not model_is_valid(model): diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 821b1fe7d1e..8308708e080 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -20,6 +20,7 @@ PathLikeOrNone, PositiveIntOrMinusOne, pyros_config, + resolve_keyword_arguments, SolverIterable, SolverResolvable, UncertaintySetDomain, @@ -688,5 +689,70 @@ def test_logger_type(self): standardizer_func(2) +class TestResolveKeywordArguments(unittest.TestCase): + """ + Test keyword argument resolution function works as expected. + """ + + def test_resolve_kwargs_simple_dict(self): + """ + Test resolve kwargs works, simple example + where there is overlap. + """ + explicit_kwargs = dict(arg1=1) + implicit_kwargs_1 = dict(arg1=2, arg2=3) + implicit_kwargs_2 = dict(arg1=4, arg2=4, arg3=5) + + # expected answer + expected_resolved_kwargs = dict(arg1=1, arg2=3, arg3=5) + + # attempt kwargs resolve + with LoggingIntercept(level=logging.WARNING) as LOG: + resolved_kwargs = resolve_keyword_arguments( + prioritized_kwargs_dicts={ + "explicitly": explicit_kwargs, + "implicitly through set 1": implicit_kwargs_1, + "implicitly through set 2": implicit_kwargs_2, + } + ) + + # check kwargs resolved as expected + self.assertEqual( + resolved_kwargs, + expected_resolved_kwargs, + msg="Resolved kwargs do not match expected value.", + ) + + # extract logger warning messages + warning_msgs = LOG.getvalue().split("\n")[:-1] + + self.assertEqual( + len(warning_msgs), 3, msg="Number of warning messages is not as expected." + ) + + # check contents of warning msgs + self.assertRegex( + warning_msgs[0], + expected_regex=( + r"Arguments \['arg1'\] passed implicitly through set 1 " + r"already passed explicitly.*" + ), + ) + self.assertRegex( + warning_msgs[1], + expected_regex=( + r"Arguments \['arg1'\] passed implicitly through set 2 " + r"already passed explicitly.*" + ), + ) + self.assertRegex( + warning_msgs[2], + expected_regex=( + r"Arguments \['arg2'\] passed implicitly through set 2 " + r"already passed implicitly through set 1.*" + ), + ) + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index a75aa4dcf41..071a579018b 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -6389,5 +6389,145 @@ def test_pyros_unavailable_backup_subsolver(self): ) +class TestPyROSResolveKwargs(unittest.TestCase): + """ + Test PyROS resolves kwargs as expected. + """ + + @unittest.skipUnless( + baron_license_is_valid, "Global NLP solver is not available and licensed." + ) + def test_pyros_kwargs_with_overlap(self): + """ + Test PyROS works as expected when there is overlap between + keyword arguments passed explicitly and implicitly + through `options` or `dev_options`. + """ + # define model + m = ConcreteModel() + m.x1 = Var(initialize=0, bounds=(0, None)) + m.x2 = Var(initialize=0, bounds=(0, None)) + m.x3 = Var(initialize=0, bounds=(None, None)) + m.u1 = Param(initialize=1.125, mutable=True) + m.u2 = Param(initialize=1, mutable=True) + + m.con1 = Constraint(expr=m.x1 * m.u1 ** (0.5) - m.x2 * m.u1 <= 2) + m.con2 = Constraint(expr=m.x1**2 - m.x2**2 * m.u1 == m.x3) + + m.obj = Objective(expr=(m.x1 - 4) ** 2 + (m.x2 - m.u2) ** 2) + + # Define the uncertainty set + # we take the parameter `u2` to be 'fixed' + ellipsoid = AxisAlignedEllipsoidalSet(center=[1.125, 1], half_lengths=[1, 0]) + + # Instantiate the PyROS solver + pyros_solver = SolverFactory("pyros") + + # Define subsolvers utilized in the algorithm + local_subsolver = SolverFactory('ipopt') + global_subsolver = SolverFactory("baron") + + # Call the PyROS solver + with LoggingIntercept(level=logging.WARNING) as LOG: + results = pyros_solver.solve( + model=m, + first_stage_variables=[m.x1, m.x2], + second_stage_variables=[], + uncertain_params=[m.u1, m.u2], + uncertainty_set=ellipsoid, + local_solver=local_subsolver, + global_solver=global_subsolver, + bypass_local_separation=True, + solve_master_globally=True, + options={ + "objective_focus": ObjectiveType.worst_case, + "solve_master_globally": False, + }, + dev_options={ + "objective_focus": ObjectiveType.nominal, + "solve_master_globally": False, + "max_iter": 1, + "time_limit": 1e3, + }, + ) + + # extract warning-level messages. + warning_msgs = LOG.getvalue().split("\n")[:-1] + resolve_kwargs_warning_msgs = [ + msg + for msg in warning_msgs + if msg.startswith("Arguments [") + and "Consider modifying your arguments" in msg + ] + self.assertEqual( + len(resolve_kwargs_warning_msgs), + 3, + msg="Number of warning-level messages not as expected.", + ) + + self.assertRegex( + resolve_kwargs_warning_msgs[0], + expected_regex=( + r"Arguments \['solve_master_globally'\] passed " + r"implicitly through argument 'options' " + r"already passed .*explicitly.*" + ), + ) + self.assertRegex( + resolve_kwargs_warning_msgs[1], + expected_regex=( + r"Arguments \['solve_master_globally'\] passed " + r"implicitly through argument 'dev_options' " + r"already passed .*explicitly.*" + ), + ) + self.assertRegex( + resolve_kwargs_warning_msgs[2], + expected_regex=( + r"Arguments \['objective_focus'\] passed " + r"implicitly through argument 'dev_options' " + r"already passed .*implicitly through argument 'options'.*" + ), + ) + + # check termination status as expected + self.assertEqual( + results.pyros_termination_condition, + pyrosTerminationCondition.max_iter, + msg="Termination condition not as expected", + ) + self.assertEqual( + results.iterations, 1, msg="Number of iterations not as expected" + ) + + # check config resolved as expected + config = results.config + self.assertEqual( + config.bypass_local_separation, + True, + msg="Resolved value of kwarg `bypass_local_separation` not as expected.", + ) + self.assertEqual( + config.solve_master_globally, + True, + msg="Resolved value of kwarg `solve_master_globally` not as expected.", + ) + self.assertEqual( + config.max_iter, + 1, + msg="Resolved value of kwarg `max_iter` not as expected.", + ) + self.assertEqual( + config.objective_focus, + ObjectiveType.worst_case, + msg="Resolved value of kwarg `objective_focus` not as expected.", + ) + self.assertEqual( + config.time_limit, + 1e3, + msg="Resolved value of kwarg `time_limit` not as expected.", + ) + + if __name__ == "__main__": unittest.main() From adc9d68ee4414ae5adf7568a36ad15d2a2f25aeb Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 15:56:24 -0500 Subject: [PATCH 0473/3044] Make advanced validation more rigorous --- pyomo/contrib/pyros/pyros.py | 26 +- pyomo/contrib/pyros/tests/test_grcs.py | 427 +++++++++++++++++++++++-- pyomo/contrib/pyros/util.py | 418 ++++++++++++++++++------ 3 files changed, 723 insertions(+), 148 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 5dc0b1a3e39..0b61798483c 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -22,16 +22,14 @@ from pyomo.opt import SolverFactory from pyomo.contrib.pyros.config import pyros_config, resolve_keyword_arguments from pyomo.contrib.pyros.util import ( - model_is_valid, recast_to_min_obj, add_decision_rule_constraints, add_decision_rule_variables, load_final_solution, pyrosTerminationCondition, ObjectiveType, - validate_uncertainty_set, identify_objective_functions, - validate_kwarg_inputs, + validate_pyros_inputs, transform_to_standard_form, turn_bounds_to_constraints, replace_uncertain_bounds_with_constraints, @@ -287,7 +285,7 @@ def _resolve_and_validate_pyros_args(self, model, **kwds): func=self.solve, ) config = self.CONFIG(resolved_kwds) - validate_kwarg_inputs(model, config) + validate_pyros_inputs(model, config) return config @@ -351,23 +349,6 @@ def solve( )) config = self._resolve_and_validate_pyros_args(model, **kwds) - # === Validate ability of grcs RO solver to handle this model - if not model_is_valid(model): - raise AttributeError( - "This model structure is not currently handled by the ROSolver." - ) - - # === Define nominal point if not specified - if len(config.nominal_uncertain_param_vals) == 0: - config.nominal_uncertain_param_vals = list( - p.value for p in config.uncertain_params - ) - elif len(config.nominal_uncertain_param_vals) != len(config.uncertain_params): - raise AttributeError( - "The nominal_uncertain_param_vals list must be the same length" - "as the uncertain_params list" - ) - # === Create data containers model_data = ROSolveResults() model_data.timing = Bunch() @@ -403,9 +384,6 @@ def solve( model.add_component(model_data.util_block, util) # Note: model.component(model_data.util_block) is util - # === Validate uncertainty set happens here, requires util block for Cardinality and FactorModel sets - validate_uncertainty_set(config=config) - # === Leads to a logger warning here for inactive obj when cloning model_data.original_model = model # === For keeping track of variables after cloning diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 071a579018b..b1546fc62e9 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -18,7 +18,6 @@ selective_clone, add_decision_rule_variables, add_decision_rule_constraints, - model_is_valid, turn_bounds_to_constraints, transform_to_standard_form, ObjectiveType, @@ -610,21 +609,6 @@ def test_dr_eqns_form_correct(self): ) -class testModelIsValid(unittest.TestCase): - def test_model_is_valid_via_possible_inputs(self): - m = ConcreteModel() - m.x = Var() - m.obj1 = Objective(expr=m.x**2) - self.assertTrue(model_is_valid(m)) - m.obj2 = Objective(expr=m.x) - self.assertFalse(model_is_valid(m)) - m.obj2.deactivate() - self.assertTrue(model_is_valid(m)) - m.del_component("obj1") - m.del_component("obj2") - self.assertFalse(model_is_valid(m)) - - class testTurnBoundsToConstraints(unittest.TestCase): def test_bounds_to_constraints(self): m = ConcreteModel() @@ -5406,16 +5390,14 @@ def test_multiple_objs(self): # check validation error raised due to multiple objectives with self.assertRaisesRegex( - AttributeError, - "This model structure is not currently handled by the ROSolver.", + ValueError, r"Expected model with exactly 1 active objective.*has 3" ): pyros_solver.solve(**solve_kwargs) # check validation error raised due to multiple objectives m.b.obj.deactivate() with self.assertRaisesRegex( - AttributeError, - "This model structure is not currently handled by the ROSolver.", + ValueError, r"Expected model with exactly 1 active objective.*has 2" ): pyros_solver.solve(**solve_kwargs) @@ -6529,5 +6511,410 @@ def test_pyros_kwargs_with_overlap(self): ) +class SimpleTestSolver: + """ + Simple test solver class with no actual solve() + functionality. Written to test unrelated aspects + of PyROS functionality. + """ + + def available(self, exception_flag=False): + """ + Check solver available. + """ + return True + + def solve(self, model, **kwds): + """ + Return SolverResults object with 'unknown' termination + condition. Model remains unchanged. + """ + res = SolverResults() + res.solver.termination_condition = TerminationCondition.unknown + + return res + + +class TestPyROSSolverAdvancedValidation(unittest.TestCase): + """ + Test PyROS solver returns expected exception messages + when arguments are invalid. + """ + + def build_simple_test_model(self): + """ + Build simple valid test model. + """ + m = ConcreteModel(name="test_model") + + m.x1 = Var(initialize=0, bounds=(0, None)) + m.x2 = Var(initialize=0, bounds=(0, None)) + m.u = Param(initialize=1.125, mutable=True) + + m.con1 = Constraint(expr=m.x1 * m.u ** (0.5) - m.x2 * m.u <= 2) + + m.obj = Objective(expr=(m.x1 - 4) ** 2 + (m.x2 - 1) ** 2) + + return m + + def test_pyros_invalid_model_type(self): + """ + Test PyROS fails if model is not of correct class. + """ + mdl = self.build_simple_test_model() + + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + pyros = SolverFactory("pyros") + + exc_str = "Model should be of type.*but is of type.*" + with self.assertRaisesRegex(TypeError, exc_str): + pyros.solve( + model=2, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + def test_pyros_multiple_objectives(self): + """ + Test PyROS raises exception if input model has multiple + objectives. + """ + mdl = self.build_simple_test_model() + mdl.obj2 = Objective(expr=(mdl.x1 + mdl.x2)) + + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + pyros = SolverFactory("pyros") + + exc_str = "Expected model with exactly 1 active.*but.*has 2" + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + def test_pyros_empty_dof_vars(self): + """ + Test PyROS solver raises exception raised if there are no + first-stage variables or second-stage variables. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = ( + "Arguments `first_stage_variables` and " + "`second_stage_variables` are both empty lists." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[], + second_stage_variables=[], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + def test_pyros_overlap_dof_vars(self): + """ + Test PyROS solver raises exception raised if there are Vars + passed as both first-stage and second-stage. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = ( + "Arguments `first_stage_variables` and `second_stage_variables` " + "contain at least one common Var object." + ) + with LoggingIntercept(level=logging.ERROR) as LOG: + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x1, mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + # check logger output is as expected + log_msgs = LOG.getvalue().split("\n")[:-1] + self.assertEqual( + len(log_msgs), 3, "Error message does not contain expected number of lines." + ) + self.assertRegex( + text=log_msgs[0], + expected_regex=( + "The following Vars were found in both `first_stage_variables`" + "and `second_stage_variables`.*" + ), + ) + self.assertRegex(text=log_msgs[1], expected_regex=" 'x1'") + self.assertRegex( + text=log_msgs[2], + expected_regex="Ensure no Vars are included in both arguments.", + ) + + def test_pyros_vars_not_in_model(self): + """ + Test PyROS appropriately raises exception if there are + variables not included in active model objective + or constraints which are not descended from model. + """ + # set up model + mdl = self.build_simple_test_model() + mdl.name = "model1" + mdl2 = self.build_simple_test_model() + mdl2.name = "model2" + + # set up solvers + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + pyros = SolverFactory("pyros") + + mdl.bad_con = Constraint(expr=mdl2.x1 + mdl2.x2 >= 1) + + desc_dof_map = [ + ("first-stage", [mdl2.x1], [], 2), + ("second-stage", [], [mdl2.x2], 2), + ("state", [mdl.x1], [], 3), + ] + + # now perform checks + for vardesc, first_stage_vars, second_stage_vars, numlines in desc_dof_map: + with LoggingIntercept(level=logging.ERROR) as LOG: + exc_str = ( + "Found entries of " + f"{vardesc} variables not descended from.*model.*" + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=first_stage_vars, + second_stage_variables=second_stage_vars, + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + log_msgs = LOG.getvalue().split("\n")[:-1] + + # check detailed log message is as expected + self.assertEqual( + len(log_msgs), + numlines, + "Error-level log message does not contain expected number of lines.", + ) + self.assertRegex( + text=log_msgs[0], + expected_regex=( + f"The following {vardesc} variables" + ".*not descended from.*model with name 'model1'" + ), + ) + + def test_pyros_non_continuous_vars(self): + """ + Test PyROS raises exception if model contains + non-continuous variables. + """ + # build model; make one variable discrete + mdl = self.build_simple_test_model() + mdl.x2.domain = NonNegativeIntegers + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = "Model with name 'test_model' contains non-continuous Vars." + with LoggingIntercept(level=logging.ERROR) as LOG: + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + # check logger output is as expected + log_msgs = LOG.getvalue().split("\n")[:-1] + self.assertEqual( + len(log_msgs), 3, "Error message does not contain expected number of lines." + ) + self.assertRegex( + text=log_msgs[0], + expected_regex=( + "The following Vars of model with name 'test_model' " + "are non-continuous:" + ), + ) + self.assertRegex(text=log_msgs[1], expected_regex=" 'x2'") + self.assertRegex( + text=log_msgs[2], + expected_regex=( + "Ensure all model variables passed to " "PyROS solver are continuous." + ), + ) + + def test_pyros_uncertainty_dimension_mismatch(self): + """ + Test PyROS solver raises exception if uncertainty + set dimension does not match the number + of uncertain parameters. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SimpleTestSolver() + global_solver = SimpleTestSolver() + + # perform checks + exc_str = ( + r"Length of argument `uncertain_params` does not match dimension " + r"of argument `uncertainty_set` \(1 != 2\)." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2], [0, 1]]), + local_solver=local_solver, + global_solver=global_solver, + ) + + def test_pyros_nominal_point_not_in_set(self): + """ + Test PyROS raises exception if nominal point is not in the + uncertainty set. + + NOTE: need executable solvers to solve set bounding problems + for validity checks. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SolverFactory("ipopt") + global_solver = SolverFactory("ipopt") + + # perform checks + exc_str = ( + r"Nominal uncertain parameter realization \[0\] " + "is not a point in the uncertainty set.*" + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + nominal_uncertain_param_vals=[0], + ) + + def test_pyros_nominal_point_len_mismatch(self): + """ + Test PyROS raises exception if there is mismatch between length + of nominal uncertain parameter specification and number + of uncertain parameters. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SolverFactory("ipopt") + global_solver = SolverFactory("ipopt") + + # perform checks + exc_str = ( + r"Lengths of arguments `uncertain_params` " + r"and `nominal_uncertain_param_vals` " + r"do not match \(1 != 2\)." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + nominal_uncertain_param_vals=[0, 1], + ) + + def test_pyros_invalid_bypass_separation(self): + """ + Test PyROS raises exception if both local and + global separation are set to be bypassed. + """ + # build model + mdl = self.build_simple_test_model() + + # prepare solvers + pyros = SolverFactory("pyros") + local_solver = SolverFactory("ipopt") + global_solver = SolverFactory("ipopt") + + # perform checks + exc_str = ( + r"Arguments `bypass_local_separation` and `bypass_global_separation` " + r"cannot both be True." + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1], + second_stage_variables=[mdl.x2], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, + bypass_local_separation=True, + bypass_global_separation=True, + ) + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 30b5d2df427..685ff9ca898 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -512,14 +512,6 @@ def recast_to_min_obj(model, obj): obj.sense = minimize -def model_is_valid(model): - """ - Assess whether model is valid on basis of the number of active - Objectives. A valid model must contain exactly one active Objective. - """ - return len(list(model.component_data_objects(Objective, active=True))) == 1 - - def turn_bounds_to_constraints(variable, model, config=None): ''' Turn the variable in question's "bounds" into direct inequality constraints on the model. @@ -603,41 +595,6 @@ def get_time_from_solver(results): return float("nan") if solve_time is None else solve_time -def validate_uncertainty_set(config): - ''' - Confirm expression output from uncertainty set function references all q in q. - Typecheck the uncertainty_set.q is Params referenced inside of m. - Give warning that the nominal point (default value in the model) is not in the specified uncertainty set. - :param config: solver config - ''' - # === Check that q in UncertaintySet object constraint expression is referencing q in model.uncertain_params - uncertain_params = config.uncertain_params - - # === Non-zero number of uncertain parameters - if len(uncertain_params) == 0: - raise AttributeError( - "Must provide uncertain params, uncertain_params list length is 0." - ) - # === No duplicate parameters - if len(uncertain_params) != len(ComponentSet(uncertain_params)): - raise AttributeError("No duplicates allowed for uncertain param objects.") - # === Ensure nominal point is in the set - if not config.uncertainty_set.point_in_set( - point=config.nominal_uncertain_param_vals - ): - raise AttributeError( - "Nominal point for uncertain parameters must be in the uncertainty set." - ) - # === Check set validity via boundedness and non-emptiness - if not config.uncertainty_set.is_valid(config=config): - raise AttributeError( - "Invalid uncertainty set detected. Check the uncertainty set object to " - "ensure non-emptiness and boundedness." - ) - - return - - def add_bounds_for_uncertain_parameters(model, config): ''' This function solves a set of optimization problems to determine bounds on the uncertain parameters @@ -817,98 +774,351 @@ def replace_uncertain_bounds_with_constraints(model, uncertain_params): v.setlb(None) -def validate_kwarg_inputs(model, config): - ''' - Confirm kwarg inputs satisfy PyROS requirements. - :param model: the deterministic model - :param config: the config for this PyROS instance - :return: - ''' - - # === Check if model is ConcreteModel object - if not isinstance(model, ConcreteModel): - raise ValueError("Model passed to PyROS solver must be a ConcreteModel object.") +def check_components_descended_from_model(model, components, components_name, config): + """ + Check all members in a provided sequence of Pyomo component + objects are descended from a given ConcreteModel object. - first_stage_variables = config.first_stage_variables - second_stage_variables = config.second_stage_variables - uncertain_params = config.uncertain_params + Parameters + ---------- + model : ConcreteModel + Model from which components should all be descended. + components : Iterable of Component + Components of interest. + components_name : str + Brief description or name for the sequence of components. + Used for constructing error messages. + config : ConfigDict + PyROS solver options. - if not config.first_stage_variables and not config.second_stage_variables: - # Must have non-zero DOF + Raises + ------ + ValueError + If at least one entry of `components` is not descended + from `model`. + """ + components_not_in_model = [comp for comp in components if comp.model() is not model] + if components_not_in_model: + comp_names_str = "\n ".join( + f"{comp.name!r}, from model with name {comp.model().name!r}" + for comp in components_not_in_model + ) + config.progress_logger.error( + f"The following {components_name} " + "are not descended from the " + f"input deterministic model with name {model.name!r}:\n " + f"{comp_names_str}" + ) raise ValueError( - "first_stage_variables and " - "second_stage_variables cannot both be empty lists." + f"Found entries of {components_name} " + "not descended from input model. " + "Check logger output messages." ) - if ComponentSet(first_stage_variables) != ComponentSet( - config.first_stage_variables - ): + +def get_state_vars(blk, first_stage_variables, second_stage_variables): + """ + Get state variables of a modeling block. + + The state variables with respect to `blk` are the unfixed + `_VarData` objects participating in the active objective + or constraints descended from `blk` which are not + first-stage variables or second-stage variables. + + Parameters + ---------- + blk : ScalarBlock + Block of interest. + first_stage_variables : Iterable of VarData + First-stage variables. + second_stage_variables : Iterable of VarData + Second-stage variables. + + Yields + ------ + _VarData + State variable. + """ + dof_var_set = ( + ComponentSet(first_stage_variables) + | ComponentSet(second_stage_variables) + ) + for var in get_vars_from_component(blk, (Objective, Constraint)): + is_state_var = not var.fixed and var not in dof_var_set + if is_state_var: + yield var + + +def check_variables_continuous(model, vars, config): + """ + Check that all DOF and state variables of the model + are continuous. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Raises + ------ + ValueError + If at least one variable is found to not be continuous. + + Note + ---- + A variable is considered continuous if the `is_continuous()` + method returns True. + """ + non_continuous_vars = [var for var in vars if not var.is_continuous()] + if non_continuous_vars: + non_continuous_vars_str = "\n ".join( + f"{var.name!r}" for var in non_continuous_vars + ) + config.progress_logger.error( + f"The following Vars of model with name {model.name!r} " + f"are non-continuous:\n {non_continuous_vars_str}\n" + "Ensure all model variables passed to PyROS solver are continuous." + ) raise ValueError( - "All elements in first_stage_variables must be Var members of the model object." + f"Model with name {model.name!r} contains non-continuous Vars." ) - if ComponentSet(second_stage_variables) != ComponentSet( - config.second_stage_variables - ): + +def validate_model(model, config): + """ + Validate deterministic model passed to PyROS solver. + + Parameters + ---------- + model : ConcreteModel + Determinstic model. Should have only one active Objective. + config : ConfigDict + PyROS solver options. + + Returns + ------- + ComponentSet + The variables participating in the active Objective + and Constraint expressions of `model`. + + Raises + ------ + TypeError + If model is not of type ConcreteModel. + ValueError + If model does not have exactly one active Objective + component. + """ + # note: only support ConcreteModel. no support for Blocks + if not isinstance(model, ConcreteModel): + raise TypeError( + f"Model should be of type {ConcreteModel.__name__}, " + f"but is of type {type(model).__name__}." + ) + + # active objectives check + active_objs_list = list( + model.component_data_objects(Objective, active=True, descend_into=True) + ) + if len(active_objs_list) != 1: raise ValueError( - "All elements in second_stage_variables must be Var members of the model object." + "Expected model with exactly 1 active objective, but " + f"model provided has {len(active_objs_list)}." ) - if any( - v in ComponentSet(second_stage_variables) - for v in ComponentSet(first_stage_variables) - ): + +def validate_variable_partitioning(model, config): + """ + Check that partitioning of the first-stage variables, + second-stage variables, and uncertain parameters + is valid. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Returns + ------- + list of _VarData + State variables of the model. + + Raises + ------ + ValueError + If first-stage variables and second-stage variables + overlap, or there are no first-stage variables + and no second-stage variables. + """ + # at least one DOF required + if not config.first_stage_variables and not config.second_stage_variables: raise ValueError( - "No common elements allowed between first_stage_variables and second_stage_variables." + "Arguments `first_stage_variables` and " + "`second_stage_variables` are both empty lists." ) - if ComponentSet(uncertain_params) != ComponentSet(config.uncertain_params): + # ensure no overlap between DOF var sets + overlapping_vars = ComponentSet(config.first_stage_variables) & ComponentSet( + config.second_stage_variables + ) + if overlapping_vars: + overlapping_var_list = "\n ".join(f"{var.name!r}" for var in overlapping_vars) + config.progress_logger.error( + "The following Vars were found in both `first_stage_variables`" + f"and `second_stage_variables`:\n {overlapping_var_list}" + "\nEnsure no Vars are included in both arguments." + ) raise ValueError( - "uncertain_params must be mutable Param members of the model object." + "Arguments `first_stage_variables` and `second_stage_variables` " + "contain at least one common Var object." ) - if not config.uncertainty_set: + state_vars = list(get_state_vars( + model, + first_stage_variables=config.first_stage_variables, + second_stage_variables=config.second_stage_variables, + )) + var_type_list_map = { + "first-stage variables": config.first_stage_variables, + "second-stage variables": config.second_stage_variables, + "state variables": state_vars, + } + for desc, vars in var_type_list_map.items(): + check_components_descended_from_model( + model=model, + components=vars, + components_name=desc, + config=config, + ) + + all_vars = ( + config.first_stage_variables + + config.second_stage_variables + + state_vars + ) + check_variables_continuous(model, all_vars, config) + + return state_vars + + +def validate_uncertainty_specification(model, config): + """ + Validate specification of uncertain parameters and uncertainty + set. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Raises + ------ + ValueError + If at least one of the following holds: + + - dimension of uncertainty set does not equal number of + uncertain parameters + - uncertainty set `is_valid()` method does not return + true. + - nominal parameter realization is not in the uncertainty set. + """ + check_components_descended_from_model( + model=model, + components=config.uncertain_params, + components_name="uncertain parameters", + config=config, + ) + + if len(config.uncertain_params) != config.uncertainty_set.dim: raise ValueError( - "An UncertaintySet object must be provided to the PyROS solver." + "Length of argument `uncertain_params` does not match dimension " + "of argument `uncertainty_set` " + f"({len(config.uncertain_params)} != {config.uncertainty_set.dim})." ) - non_mutable_params = [] - for p in config.uncertain_params: - if not ( - not p.is_constant() and p.is_fixed() and not p.is_potentially_variable() - ): - non_mutable_params.append(p) - if non_mutable_params: - raise ValueError( - "Param objects which are uncertain must have attribute mutable=True. " - "Offending Params: %s" % [p.name for p in non_mutable_params] - ) + # validate uncertainty set + if not config.uncertainty_set.is_valid(config=config): + raise ValueError( + f"Uncertainty set {config.uncertainty_set} is invalid, " + "as it is either empty or unbounded." + ) - # === Solvers provided check - if not config.local_solver or not config.global_solver: + # fill-in nominal point as necessary, if not provided. + # otherwise, check length matches uncertainty dimension + if not config.nominal_uncertain_param_vals: + config.nominal_uncertain_param_vals = [ + value(param, exception=True) for param in config.uncertain_params + ] + elif len(config.nominal_uncertain_param_vals) != len(config.uncertain_params): raise ValueError( - "User must designate both a local and global optimization solver via the local_solver" - " and global_solver options." + "Lengths of arguments `uncertain_params` and " + "`nominal_uncertain_param_vals` " + "do not match " + f"({len(config.uncertain_params)} != " + f"{len(config.nominal_uncertain_param_vals)})." ) - if config.bypass_local_separation and config.bypass_global_separation: + # uncertainty set should contain nominal point + nominal_point_in_set = config.uncertainty_set.point_in_set( + point=config.nominal_uncertain_param_vals + ) + if not nominal_point_in_set: raise ValueError( - "User cannot simultaneously enable options " - "'bypass_local_separation' and " - "'bypass_global_separation'." + "Nominal uncertain parameter realization " + f"{config.nominal_uncertain_param_vals} " + "is not a point in the uncertainty set " + f"{config.uncertainty_set!r}." ) - # === Degrees of freedom provided check - if len(config.first_stage_variables) + len(config.second_stage_variables) == 0: + +def validate_separation_problem_options(model, config): + """ + Validate separation problem arguments to the PyROS solver. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + + Raises + ------ + ValueError + If options `bypass_local_separation` and + `bypass_global_separation` are set to False. + """ + if config.bypass_local_separation and config.bypass_global_separation: raise ValueError( - "User must designate at least one first- and/or second-stage variable." + "Arguments `bypass_local_separation` " + "and `bypass_global_separation` " + "cannot both be True." ) - # === Uncertain params provided check - if len(config.uncertain_params) == 0: - raise ValueError("User must designate at least one uncertain parameter.") - return +def validate_pyros_inputs(model, config): + """ + Perform advanced validation of PyROS solver arguments. + + Parameters + ---------- + model : ConcreteModel + Input deterministic model. + config : ConfigDict + PyROS solver options. + """ + validate_model(model, config) + state_vars = validate_variable_partitioning(model, config) + validate_uncertainty_specification(model, config) + validate_separation_problem_options(model, config) + + return state_vars def substitute_ssv_in_dr_constraints(model, constraint): From 4f64c4fa651ffd48cd6a6e5b7436e1002310b539 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 16:01:31 -0500 Subject: [PATCH 0474/3044] Simplify assembly of state variables --- pyomo/contrib/pyros/pyros.py | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 0b61798483c..05512ec777b 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -12,7 +12,7 @@ # pyros.py: Generalized Robust Cutting-Set Algorithm for Pyomo import logging from pyomo.common.config import document_kwargs_from_configdict -from pyomo.common.collections import Bunch, ComponentSet +from pyomo.common.collections import Bunch from pyomo.core.base.block import Block from pyomo.core.expr import value from pyomo.core.base.var import Var @@ -285,9 +285,9 @@ def _resolve_and_validate_pyros_args(self, model, **kwds): func=self.solve, ) config = self.CONFIG(resolved_kwds) - validate_pyros_inputs(model, config) + state_vars = validate_pyros_inputs(model, config) - return config + return config, state_vars @document_kwargs_from_configdict( config=CONFIG, @@ -347,7 +347,7 @@ def solve( local_solver=local_solver, global_solver=global_solver, )) - config = self._resolve_and_validate_pyros_args(model, **kwds) + config, state_vars = self._resolve_and_validate_pyros_args(model, **kwds) # === Create data containers model_data = ROSolveResults() @@ -378,6 +378,7 @@ def solve( util = Block(concrete=True) util.first_stage_variables = config.first_stage_variables util.second_stage_variables = config.second_stage_variables + util.state_vars = state_vars util.uncertain_params = config.uncertain_params model_data.util_block = unique_component_name(model, 'util') @@ -425,22 +426,10 @@ def solve( # === Move bounds on control variables to explicit ineq constraints wm_util = model_data.working_model - # === Every non-fixed variable that is neither first-stage - # nor second-stage is taken to be a state variable - fsv = ComponentSet(model_data.working_model.util.first_stage_variables) - ssv = ComponentSet(model_data.working_model.util.second_stage_variables) - sv = ComponentSet() - model_data.working_model.util.state_vars = [] - for v in model_data.working_model.component_data_objects(Var): - if not v.fixed and v not in fsv | ssv | sv: - model_data.working_model.util.state_vars.append(v) - sv.add(v) - - # Bounds on second stage variables and state variables are separation objectives, - # they are brought in this was as explicit constraints + # cast bounds on second-stage and state variables to + # explicit constraints for separation objectives for c in model_data.working_model.util.second_stage_variables: turn_bounds_to_constraints(c, wm_util, config) - for c in model_data.working_model.util.state_vars: turn_bounds_to_constraints(c, wm_util, config) From 969aac9d0fbe684af4ed74fdb4371a93416bdd34 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 16:45:33 -0500 Subject: [PATCH 0475/3044] Apply black --- pyomo/contrib/pyros/pyros.py | 18 ++++++++------- pyomo/contrib/pyros/tests/test_config.py | 8 ++----- pyomo/contrib/pyros/util.py | 28 ++++++++++-------------- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 05512ec777b..0b37b8e9615 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -339,14 +339,16 @@ def solve( Summary of PyROS termination outcome. """ - kwds.update(dict( - first_stage_variables=first_stage_variables, - second_stage_variables=second_stage_variables, - uncertain_params=uncertain_params, - uncertainty_set=uncertainty_set, - local_solver=local_solver, - global_solver=global_solver, - )) + kwds.update( + dict( + first_stage_variables=first_stage_variables, + second_stage_variables=second_stage_variables, + uncertain_params=uncertain_params, + uncertainty_set=uncertainty_set, + local_solver=local_solver, + global_solver=global_solver, + ) + ) config, state_vars = self._resolve_and_validate_pyros_args(model, **kwds) # === Create data containers diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 8308708e080..37587fcce58 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -643,16 +643,12 @@ def test_positive_int_or_minus_one(self): self.assertIs( standardizer_func(1.0), 1, - msg=( - f"{PositiveIntOrMinusOne.__name__} does not standardize as expected." - ), + msg=(f"{PositiveIntOrMinusOne.__name__} does not standardize as expected."), ) self.assertEqual( standardizer_func(-1.00), -1, - msg=( - f"{PositiveIntOrMinusOne.__name__} does not standardize as expected." - ), + msg=(f"{PositiveIntOrMinusOne.__name__} does not standardize as expected."), ) exc_str = r"Expected positive int or -1, but received value.*" diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 685ff9ca898..bcd2363bc43 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -839,9 +839,8 @@ def get_state_vars(blk, first_stage_variables, second_stage_variables): _VarData State variable. """ - dof_var_set = ( - ComponentSet(first_stage_variables) - | ComponentSet(second_stage_variables) + dof_var_set = ComponentSet(first_stage_variables) | ComponentSet( + second_stage_variables ) for var in get_vars_from_component(blk, (Objective, Constraint)): is_state_var = not var.fixed and var not in dof_var_set @@ -977,11 +976,13 @@ def validate_variable_partitioning(model, config): "contain at least one common Var object." ) - state_vars = list(get_state_vars( - model, - first_stage_variables=config.first_stage_variables, - second_stage_variables=config.second_stage_variables, - )) + state_vars = list( + get_state_vars( + model, + first_stage_variables=config.first_stage_variables, + second_stage_variables=config.second_stage_variables, + ) + ) var_type_list_map = { "first-stage variables": config.first_stage_variables, "second-stage variables": config.second_stage_variables, @@ -989,17 +990,10 @@ def validate_variable_partitioning(model, config): } for desc, vars in var_type_list_map.items(): check_components_descended_from_model( - model=model, - components=vars, - components_name=desc, - config=config, + model=model, components=vars, components_name=desc, config=config ) - all_vars = ( - config.first_stage_variables - + config.second_stage_variables - + state_vars - ) + all_vars = config.first_stage_variables + config.second_stage_variables + state_vars check_variables_continuous(model, all_vars, config) return state_vars From 6f1a0552388f25727563908abde9f1b405b6e4b0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 7 Feb 2024 15:39:39 -0700 Subject: [PATCH 0476/3044] Update ExitNodeDispatcher to be compatible with inherited expression types --- pyomo/repn/util.py | 67 +++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index b65aa9427d5..108bb0ab972 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -387,42 +387,49 @@ def __init__(self, *args, **kwargs): super().__init__(None, *args, **kwargs) def __missing__(self, key): - return functools.partial(self.register_dispatcher, key=key) - - def register_dispatcher(self, visitor, node, *data, key=None): + if type(key) is tuple: + node_class = key[0] + else: + node_class = key + bases = node_class.__mro__ + # Note: if we add an `etype`, then this special-case can be removed if ( - isinstance(node, _named_subexpression_types) - or type(node) is kernel.expression.noclone + issubclass(node_class, _named_subexpression_types) + or node_class is kernel.expression.noclone ): - base_type = Expression - elif not node.is_potentially_variable(): - base_type = node.potentially_variable_base_class() - else: - base_type = node.__class__ - if isinstance(key, tuple): - base_key = (base_type,) + key[1:] - # Only cache handlers for unary, binary and ternary operators - cache = len(key) <= 4 - else: - base_key = base_type - cache = True - if base_key in self: - fcn = self[base_key] - elif base_type in self: - fcn = self[base_type] - elif any((k[0] if k.__class__ is tuple else k) is base_type for k in self): - raise DeveloperError( - f"Base expression key '{base_key}' not found when inserting dispatcher" - f" for node '{type(node).__name__}' while walking expression tree." - ) - else: + bases = [Expression] + fcn = None + for base_type in bases: + if isinstance(key, tuple): + base_key = (base_type,) + key[1:] + # Only cache handlers for unary, binary and ternary operators + cache = len(key) <= 4 + else: + base_key = base_type + cache = True + if base_key in self: + fcn = self[base_key] + elif base_type in self: + fcn = self[base_type] + elif any((k[0] if type(k) is tuple else k) is base_type for k in self): + raise DeveloperError( + f"Base expression key '{base_key}' not found when inserting " + f"dispatcher for node '{node_class.__name__}' while walking " + "expression tree." + ) + if fcn is None: + if type(key) is tuple: + node_class = key[0] + else: + node_class = key raise DeveloperError( - f"Unexpected expression node type '{type(node).__name__}' " - "found while walking expression tree." + f"Unexpected expression node type '{node_class.__name__}' " + f"found while walking expression tree." ) + return self.unexpected_expression_type(key) if cache: self[key] = fcn - return fcn(visitor, node, *data) + return fcn def apply_node_operation(node, args): From 51d23370f198a98481d0260c4b72f93b757c9406 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 7 Feb 2024 15:40:24 -0700 Subject: [PATCH 0477/3044] Refactor ExitNodeDispatcher to provide hook for unknown classes --- pyomo/repn/util.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 108bb0ab972..cb67dd92494 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -418,19 +418,21 @@ def __missing__(self, key): "expression tree." ) if fcn is None: - if type(key) is tuple: - node_class = key[0] - else: - node_class = key - raise DeveloperError( - f"Unexpected expression node type '{node_class.__name__}' " - f"found while walking expression tree." - ) return self.unexpected_expression_type(key) if cache: self[key] = fcn return fcn + def unexpected_expression_type(self, key): + if type(key) is tuple: + node_class = key[0] + else: + node_class = key + raise DeveloperError( + f"Unexpected expression node type '{node_class.__name__}' " + f"found while walking expression tree." + ) + def apply_node_operation(node, args): try: From 8201978d5536c84e465e1470cf72ea4c02868cfc Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 17:40:34 -0500 Subject: [PATCH 0478/3044] Make first char of test class names uppercase --- pyomo/contrib/pyros/tests/test_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 37587fcce58..50152abbacd 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -30,7 +30,7 @@ from pyomo.contrib.pyros.uncertainty_sets import BoxSet -class testInputDataStandardizer(unittest.TestCase): +class TestInputDataStandardizer(unittest.TestCase): """ Test standardizer method for Pyomo component-type inputs. """ @@ -543,7 +543,7 @@ def test_config_objective_focus(self): config.objective_focus = invalid_focus -class testPathLikeOrNone(unittest.TestCase): +class TestPathLikeOrNone(unittest.TestCase): """ Test interface for validating path-like arguments. """ From ce7a6b54256f03c24a1089223d355003869bfa63 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 7 Feb 2024 15:40:39 -0700 Subject: [PATCH 0479/3044] Add tests for inherited classes --- pyomo/repn/tests/test_util.py | 36 +++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/pyomo/repn/tests/test_util.py b/pyomo/repn/tests/test_util.py index 47cc6b1a63a..3f455aad13f 100644 --- a/pyomo/repn/tests/test_util.py +++ b/pyomo/repn/tests/test_util.py @@ -19,6 +19,7 @@ from pyomo.common.errors import DeveloperError, InvalidValueError from pyomo.common.log import LoggingIntercept from pyomo.core.expr import ( + NumericExpression, ProductExpression, NPV_ProductExpression, SumExpression, @@ -671,16 +672,6 @@ def test_ExitNodeDispatcher_registration(self): self.assertEqual(len(end), 4) self.assertIn(NPV_ProductExpression, end) - class NewProductExpression(ProductExpression): - pass - - node = NewProductExpression((6, 7)) - with self.assertRaisesRegex( - DeveloperError, r".*Unexpected expression node type 'NewProductExpression'" - ): - end[node.__class__](None, node, *node.args) - self.assertEqual(len(end), 4) - end[SumExpression, 2] = lambda v, n, *d: 2 * sum(d) self.assertEqual(len(end), 5) @@ -710,6 +701,31 @@ class NewProductExpression(ProductExpression): self.assertEqual(len(end), 7) self.assertNotIn((SumExpression, 3, 4, 5, 6), end) + class NewProductExpression(ProductExpression): + pass + + node = NewProductExpression((6, 7)) + self.assertEqual(end[node.__class__](None, node, *node.args), 42) + self.assertEqual(len(end), 8) + self.assertIn(NewProductExpression, end) + + class UnknownExpression(NumericExpression): + pass + + node = UnknownExpression((6, 7)) + with self.assertRaisesRegex( + DeveloperError, r".*Unexpected expression node type 'UnknownExpression'" + ): + end[node.__class__](None, node, *node.args) + self.assertEqual(len(end), 8) + + node = UnknownExpression((6, 7)) + with self.assertRaisesRegex( + DeveloperError, r".*Unexpected expression node type 'UnknownExpression'" + ): + end[node.__class__, 6, 7](None, node, *node.args) + self.assertEqual(len(end), 8) + def test_BeforeChildDispatcher_registration(self): class BeforeChildDispatcherTester(BeforeChildDispatcher): @staticmethod From 012e319dbbea4554cf44c4fc5cb255ae8a014eee Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 17:47:10 -0500 Subject: [PATCH 0480/3044] Remove support for solver argument `dev_options` --- pyomo/contrib/pyros/pyros.py | 2 -- pyomo/contrib/pyros/tests/test_grcs.py | 24 ++---------------------- 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 0b37b8e9615..69a6ce315da 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -275,12 +275,10 @@ def _resolve_and_validate_pyros_args(self, model, **kwds): 3. Inter-argument validation. """ options_dict = kwds.pop("options", {}) - dev_options_dict = kwds.pop("dev_options", {}) resolved_kwds = resolve_keyword_arguments( prioritized_kwargs_dicts={ "explicitly": kwds, "implicitly through argument 'options'": options_dict, - "implicitly through argument 'dev_options'": dev_options_dict, }, func=self.solve, ) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index b1546fc62e9..5727df70a52 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -6424,12 +6424,8 @@ def test_pyros_kwargs_with_overlap(self): options={ "objective_focus": ObjectiveType.worst_case, "solve_master_globally": False, - }, - dev_options={ - "objective_focus": ObjectiveType.nominal, - "solve_master_globally": False, "max_iter": 1, - "time_limit": 1e3, + "time_limit": 1000, }, ) @@ -6443,7 +6439,7 @@ def test_pyros_kwargs_with_overlap(self): ] self.assertEqual( len(resolve_kwargs_warning_msgs), - 3, + 1, msg="Number of warning-level messages not as expected.", ) @@ -6455,22 +6451,6 @@ def test_pyros_kwargs_with_overlap(self): r"already passed .*explicitly.*" ), ) - self.assertRegex( - resolve_kwargs_warning_msgs[1], - expected_regex=( - r"Arguments \['solve_master_globally'\] passed " - r"implicitly through argument 'dev_options' " - r"already passed .*explicitly.*" - ), - ) - self.assertRegex( - resolve_kwargs_warning_msgs[2], - expected_regex=( - r"Arguments \['objective_focus'\] passed " - r"implicitly through argument 'dev_options' " - r"already passed .*implicitly through argument 'options'.*" - ), - ) # check termination status as expected self.assertEqual( From 2c4b89e1116a79718c2b8d72650b88ca8a09e6bb Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 17:48:04 -0500 Subject: [PATCH 0481/3044] Remove `dev_options` from test docstring --- pyomo/contrib/pyros/tests/test_grcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 5727df70a52..f8c4078f4ee 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -6383,7 +6383,7 @@ def test_pyros_kwargs_with_overlap(self): """ Test PyROS works as expected when there is overlap between keyword arguments passed explicitly and implicitly - through `options` or `dev_options`. + through `options`. """ # define model m = ConcreteModel() From 55884a4ab15b8cc895a5eb3ad0af360f4029bcc0 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 18:48:55 -0500 Subject: [PATCH 0482/3044] Apply black 24.1.1 --- pyomo/contrib/pyros/config.py | 1 - pyomo/contrib/pyros/tests/test_config.py | 1 - pyomo/contrib/pyros/tests/test_grcs.py | 6 +++--- pyomo/contrib/pyros/uncertainty_sets.py | 1 + 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index c003d699255..42b4ddc29a0 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -2,7 +2,6 @@ Interfaces for managing PyROS solver options. """ - from collections.abc import Iterable import logging import os diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 50152abbacd..a7f40ca37e8 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -2,7 +2,6 @@ Test objects for construction of PyROS ConfigDict. """ - import logging import os import unittest diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index f8c4078f4ee..70f8a9dfb60 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3750,9 +3750,9 @@ def test_solve_master(self): master_data.master_model.scenarios[0, 0].second_stage_objective = Expression( expr=master_data.master_model.scenarios[0, 0].x ) - master_data.master_model.scenarios[ - 0, 0 - ].util.dr_var_to_exponent_map = ComponentMap() + master_data.master_model.scenarios[0, 0].util.dr_var_to_exponent_map = ( + ComponentMap() + ) master_data.iteration = 0 master_data.timing = TimingData() diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 4a2f198bc17..963abebb60c 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -276,6 +276,7 @@ class UncertaintySetDomain: """ Domain validator for uncertainty set argument. """ + def __call__(self, obj): """ Type validate uncertainty set object. From cabe4250813473132dd7dc27aab624be584a6194 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 19:03:16 -0500 Subject: [PATCH 0483/3044] Fix typos --- pyomo/contrib/pyros/config.py | 2 +- pyomo/contrib/pyros/tests/test_config.py | 4 ++-- pyomo/contrib/pyros/util.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 42b4ddc29a0..261e4069c03 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -1001,7 +1001,7 @@ def resolve_keyword_arguments(prioritized_kwargs_dicts, func=None): overlapping_args_set = set() for prev_desc, prev_kwargs in prev_prioritized_kwargs_dicts.items(): - # determine overlap between currrent and previous + # determine overlap between current and previous # set of kwargs, and remove overlap of current # and higher priority sets from the result curr_prev_overlapping_args = ( diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index a7f40ca37e8..ec377f96ca6 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -199,7 +199,7 @@ def test_standardizer_invalid_str_passed(self): with self.assertRaisesRegex(TypeError, exc_str): standardizer_func("abcd") - def test_standardizer_invalid_unintialized_params(self): + def test_standardizer_invalid_uninitialized_params(self): """ Test standardizer raises exception when Param with uninitialized entries passed. @@ -373,7 +373,7 @@ def test_solver_resolvable_invalid_type(self): def test_solver_resolvable_unavailable_solver(self): """ Test solver standardizer fails in event solver is - unavaiable. + unavailable. """ unavailable_solver = UnavailableSolver() standardizer_func = SolverResolvable( diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index bcd2363bc43..e0ed552aab4 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -892,7 +892,7 @@ def validate_model(model, config): Parameters ---------- model : ConcreteModel - Determinstic model. Should have only one active Objective. + Deterministic model. Should have only one active Objective. config : ConfigDict PyROS solver options. From f7c8e4af1724f0dd8362467e12444f71110c684c Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 19:05:14 -0500 Subject: [PATCH 0484/3044] Fix another typo --- pyomo/contrib/pyros/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 261e4069c03..f12fb3d0be0 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -230,7 +230,7 @@ def standardize_ctype_obj(self, obj): def standardize_cdatatype_obj(self, obj): """ - Standarize object of type ``self.cdatatype`` to + Standardize object of type ``self.cdatatype`` to ``[obj]``. """ if self.cdatatype_validator is not None: From 7fdcf248c6961aa97de924fc885b437a32270597 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 19:45:54 -0500 Subject: [PATCH 0485/3044] Check IPOPT available before advanced validation tests --- pyomo/contrib/pyros/tests/test_grcs.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 70f8a9dfb60..904c981ed93 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -119,6 +119,9 @@ scip_license_is_valid = False scip_version = (0, 0, 0) +_ipopt = SolverFactory("ipopt") +ipopt_available = _ipopt.available(exception_flag=False) + # @SolverFactory.register("time_delay_solver") class TimeDelaySolver(object): @@ -3533,10 +3536,7 @@ class behaves like a regular Python list. # assigning to slices should work fine all_sets[3:] = [BoxSet([[1, 1.5]]), BoxSet([[1, 3]])] - @unittest.skipUnless( - SolverFactory('ipopt').available(exception_flag=False), - "Local NLP solver is not available.", - ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_uncertainty_set_with_correct_params(self): ''' Case in which the UncertaintySet is constructed using the uncertain_param objects from the model to @@ -3575,10 +3575,7 @@ def test_uncertainty_set_with_correct_params(self): " be the same uncertain param Var objects in the original model.", ) - @unittest.skipUnless( - SolverFactory('ipopt').available(exception_flag=False), - "Local NLP solver is not available.", - ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_uncertainty_set_with_incorrect_params(self): ''' Case in which the set is constructed using uncertain_param objects which are Params instead of @@ -6799,6 +6796,7 @@ def test_pyros_uncertainty_dimension_mismatch(self): global_solver=global_solver, ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_pyros_nominal_point_not_in_set(self): """ Test PyROS raises exception if nominal point is not in the @@ -6832,6 +6830,7 @@ def test_pyros_nominal_point_not_in_set(self): nominal_uncertain_param_vals=[0], ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_pyros_nominal_point_len_mismatch(self): """ Test PyROS raises exception if there is mismatch between length @@ -6864,6 +6863,7 @@ def test_pyros_nominal_point_len_mismatch(self): nominal_uncertain_param_vals=[0, 1], ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_pyros_invalid_bypass_separation(self): """ Test PyROS raises exception if both local and From ec0ad71db2944ec65bfa37f3f64dea94fc942cea Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 20:24:47 -0500 Subject: [PATCH 0486/3044] Remove IPOPT from solver validation tests --- pyomo/contrib/pyros/tests/test_config.py | 40 +++++++++++++++++++----- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index ec377f96ca6..142a14c2122 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -302,6 +302,29 @@ def test_uncertainty_set_domain_invalid_type(self): standardizer_func(2) +AVAILABLE_SOLVER_TYPE_NAME = "available_pyros_test_solver" + + +@SolverFactory.register(name=AVAILABLE_SOLVER_TYPE_NAME) +class AvailableSolver: + """ + Perenially avaiable placeholder solver. + """ + + def available(self, exception_flag=False): + """ + Check solver available. + """ + return True + + def solve(self, model, **kwds): + """ + Return SolverResults object with 'unknown' termination + condition. Model remains unchanged. + """ + return SolverResults() + + class UnavailableSolver: def available(self, exception_flag=True): if exception_flag: @@ -322,7 +345,7 @@ def test_solver_resolvable_valid_str(self): Test solver resolvable class is valid for string type. """ - solver_str = "ipopt" + solver_str = AVAILABLE_SOLVER_TYPE_NAME standardizer_func = SolverResolvable() solver = standardizer_func(solver_str) expected_solver_type = type(SolverFactory(solver_str)) @@ -342,7 +365,7 @@ def test_solver_resolvable_valid_solver_type(self): Test solver resolvable class is valid for string type. """ - solver = SolverFactory("ipopt") + solver = SolverFactory(AVAILABLE_SOLVER_TYPE_NAME) standardizer_func = SolverResolvable() standardized_solver = standardizer_func(solver) @@ -403,8 +426,11 @@ def test_solver_iterable_valid_list(self): Test solver type standardizer works for list of valid objects castable to solver. """ - solver_list = ["ipopt", SolverFactory("ipopt")] - expected_solver_types = [type(SolverFactory("ipopt"))] * 2 + solver_list = [ + AVAILABLE_SOLVER_TYPE_NAME, + SolverFactory(AVAILABLE_SOLVER_TYPE_NAME), + ] + expected_solver_types = [AvailableSolver] * 2 standardizer_func = SolverIterable() standardized_solver_list = standardizer_func(solver_list) @@ -438,7 +464,7 @@ def test_solver_iterable_valid_str(self): """ Test SolverIterable raises exception when str passed. """ - solver_str = "ipopt" + solver_str = AVAILABLE_SOLVER_TYPE_NAME standardizer_func = SolverIterable() solver_list = standardizer_func(solver_str) @@ -450,7 +476,7 @@ def test_solver_iterable_unavailable_solver(self): """ Test SolverIterable addresses unavailable solvers appropriately. """ - solvers = (SolverFactory("ipopt"), UnavailableSolver()) + solvers = (AvailableSolver(), UnavailableSolver()) standardizer_func = SolverIterable( require_available=True, @@ -496,7 +522,7 @@ def test_solver_iterable_invalid_list(self): Test SolverIterable raises exception if iterable contains at least one invalid object. """ - invalid_object = ["ipopt", 2] + invalid_object = [AVAILABLE_SOLVER_TYPE_NAME, 2] standardizer_func = SolverIterable(solver_desc="backup solver") exc_str = ( From 445d4cf7067d08b67d6d73f60a3b8c6a6880b8c7 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 20:49:32 -0500 Subject: [PATCH 0487/3044] Fix typos --- pyomo/contrib/pyros/tests/test_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 142a14c2122..bff098742b6 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -308,7 +308,7 @@ def test_uncertainty_set_domain_invalid_type(self): @SolverFactory.register(name=AVAILABLE_SOLVER_TYPE_NAME) class AvailableSolver: """ - Perenially avaiable placeholder solver. + Perennially available placeholder solver. """ def available(self, exception_flag=False): From 7b26268cb626b6e881849b0f429382e0362f6cce Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 22:02:09 -0500 Subject: [PATCH 0488/3044] Fix test solver registration --- pyomo/contrib/pyros/tests/test_config.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index bff098742b6..adae2dbb1e5 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -305,7 +305,6 @@ def test_uncertainty_set_domain_invalid_type(self): AVAILABLE_SOLVER_TYPE_NAME = "available_pyros_test_solver" -@SolverFactory.register(name=AVAILABLE_SOLVER_TYPE_NAME) class AvailableSolver: """ Perennially available placeholder solver. @@ -340,6 +339,12 @@ class TestSolverResolvable(unittest.TestCase): Test PyROS standardizer for solver-type objects. """ + def setUp(self): + SolverFactory.register(AVAILABLE_SOLVER_TYPE_NAME)(AvailableSolver) + + def tearDown(self): + SolverFactory.unregister(AVAILABLE_SOLVER_TYPE_NAME) + def test_solver_resolvable_valid_str(self): """ Test solver resolvable class is valid for string @@ -421,6 +426,12 @@ class TestSolverIterable(unittest.TestCase): arguments. """ + def setUp(self): + SolverFactory.register(AVAILABLE_SOLVER_TYPE_NAME)(AvailableSolver) + + def tearDown(self): + SolverFactory.unregister(AVAILABLE_SOLVER_TYPE_NAME) + def test_solver_iterable_valid_list(self): """ Test solver type standardizer works for list of valid From b899744ddb4812ee5a76417b4fccf9acc84b3b12 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 22:15:48 -0500 Subject: [PATCH 0489/3044] Check numpy available for uncertainty set test --- pyomo/contrib/pyros/tests/test_config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index adae2dbb1e5..3113afaac89 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -27,6 +27,7 @@ from pyomo.contrib.pyros.util import ObjectiveType from pyomo.opt import SolverFactory, SolverResults from pyomo.contrib.pyros.uncertainty_sets import BoxSet +from pyomo.common.dependencies import numpy_available class TestInputDataStandardizer(unittest.TestCase): @@ -280,6 +281,7 @@ class TestUncertaintySetDomain(unittest.TestCase): Test domain validator for uncertainty set arguments. """ + @unittest.skipUnless(numpy_available, "Numpy is not available.") def test_uncertainty_set_domain_valid_set(self): """ Test validator works for valid argument. From 655c06dc713c3fa7155c501663b74d3f1116ae89 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 22:51:55 -0500 Subject: [PATCH 0490/3044] Add IPOPT availability checks to solve tests --- pyomo/contrib/pyros/tests/test_grcs.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 904c981ed93..ef029d0f352 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -6322,7 +6322,7 @@ def test_pyros_unavailable_subsolver(self): second_stage_variables=[m.z[1]], uncertain_params=[m.p[0]], uncertainty_set=BoxSet([[0, 1]]), - local_solver=SolverFactory("ipopt"), + local_solver=SimpleTestSolver(), global_solver=UnavailableSolver(), ) @@ -6331,6 +6331,7 @@ def test_pyros_unavailable_subsolver(self): error_msgs, r"Output of `available\(\)` method.*global solver.*" ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_pyros_unavailable_backup_subsolver(self): """ Test PyROS raises expected error message when @@ -6373,8 +6374,10 @@ class TestPyROSResolveKwargs(unittest.TestCase): Test PyROS resolves kwargs as expected. """ + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") @unittest.skipUnless( - baron_license_is_valid, "Global NLP solver is not available and licensed." + baron_license_is_valid, + "Global NLP solver is not available and licensed." ) def test_pyros_kwargs_with_overlap(self): """ From 0f727ab36a8135472092d1e0161161936a16e618 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Feb 2024 23:06:31 -0500 Subject: [PATCH 0491/3044] Apply black to tests --- pyomo/contrib/pyros/tests/test_grcs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index ef029d0f352..a94b4d9d408 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -6376,8 +6376,7 @@ class TestPyROSResolveKwargs(unittest.TestCase): @unittest.skipUnless(ipopt_available, "IPOPT is not available.") @unittest.skipUnless( - baron_license_is_valid, - "Global NLP solver is not available and licensed." + baron_license_is_valid, "Global NLP solver is not available and licensed." ) def test_pyros_kwargs_with_overlap(self): """ From db92d2cde0cc796622c15c73774a3b7ed5ea7af8 Mon Sep 17 00:00:00 2001 From: jlgearh Date: Thu, 8 Feb 2024 08:43:19 -0700 Subject: [PATCH 0492/3044] - Updated gitignore file --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 36ef460a4dd..638dc70d13e 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,6 @@ gurobi.log # Jupyterhub/Jupyterlab checkpoints .ipynb_checkpoints cplex.log -/.vs + +# Mac tracking files +*.DS_Store* From 08596e575854b5d7414612c804e8babfbb7e6936 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 8 Feb 2024 13:55:05 -0500 Subject: [PATCH 0493/3044] Update version number, changelog --- pyomo/contrib/pyros/CHANGELOG.txt | 8 ++++++++ pyomo/contrib/pyros/pyros.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/CHANGELOG.txt b/pyomo/contrib/pyros/CHANGELOG.txt index 7d4678f0ba3..94f4848edb2 100644 --- a/pyomo/contrib/pyros/CHANGELOG.txt +++ b/pyomo/contrib/pyros/CHANGELOG.txt @@ -2,6 +2,13 @@ PyROS CHANGELOG =============== +------------------------------------------------------------------------------- +PyROS 1.2.10 07 Feb 2024 +------------------------------------------------------------------------------- +- Update argument resolution and validation routines of `PyROS.solve()` +- Use methods of `common.config` for docstring of `PyROS.solve()` + + ------------------------------------------------------------------------------- PyROS 1.2.9 15 Dec 2023 ------------------------------------------------------------------------------- @@ -14,6 +21,7 @@ PyROS 1.2.9 15 Dec 2023 - Refactor DR polishing routine; initialize auxiliary variables to values they are meant to represent + ------------------------------------------------------------------------------- PyROS 1.2.8 12 Oct 2023 ------------------------------------------------------------------------------- diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 69a6ce315da..0659ab43a64 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -44,7 +44,7 @@ from datetime import datetime -__version__ = "1.2.9" +__version__ = "1.2.10" default_pyros_solver_logger = setup_pyros_logger() From db3b3abc132b6c45545248cca4a4b48cc8d22214 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 8 Feb 2024 15:38:22 -0700 Subject: [PATCH 0494/3044] Reorganizing the dispatcher structure to better support nested types --- .../contrib/fbbt/expression_bounds_walker.py | 37 +++++++++++-------- .../tests/test_expression_bounds_walker.py | 14 +++++-- pyomo/core/expr/__init__.py | 1 + pyomo/repn/tests/test_linear.py | 2 +- pyomo/repn/tests/test_util.py | 4 +- pyomo/repn/util.py | 34 ++++++++++------- 6 files changed, 57 insertions(+), 35 deletions(-) diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index 426d30f0ee6..22ebf28ae81 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -60,6 +60,14 @@ def _before_external_function(visitor, child): # this then this should use them return False, (-inf, inf) + @staticmethod + def _before_native_numeric(visitor, child): + return False, (child, child) + + @staticmethod + def _before_native_logical(visitor, child): + return False, (Bool(child), Bool(child)) + @staticmethod def _before_var(visitor, child): leaf_bounds = visitor.leaf_bounds @@ -67,12 +75,15 @@ def _before_var(visitor, child): pass elif child.is_fixed() and visitor.use_fixed_var_values_as_bounds: val = child.value - if val is None: + try: + ans = visitor._before_child_handlers[val.__class__](visitor, val) + except ValueError: raise ValueError( "Var '%s' is fixed to None. This value cannot be used to " "calculate bounds." % child.name - ) - leaf_bounds[child] = (child.value, child.value) + ) from None + leaf_bounds[child] = ans[1] + return ans else: lb = child.lb ub = child.ub @@ -93,23 +104,20 @@ def _before_named_expression(visitor, child): @staticmethod def _before_param(visitor, child): - return False, (child.value, child.value) - - @staticmethod - def _before_native(visitor, child): - return False, (child, child) + val = child.value + return visitor._before_child_handlers[val.__class__](visitor, val) @staticmethod def _before_string(visitor, child): raise ValueError( - f"{child!r} ({type(child)}) is not a valid numeric type. " + f"{child!r} ({type(child).__name__}) is not a valid numeric type. " f"Cannot compute bounds on expression." ) @staticmethod def _before_invalid(visitor, child): raise ValueError( - f"{child!r} ({type(child)}) is not a valid numeric type. " + f"{child!r} ({type(child).__name__}) is not a valid numeric type. " f"Cannot compute bounds on expression." ) @@ -123,10 +131,7 @@ def _before_complex(visitor, child): @staticmethod def _before_npv(visitor, child): val = value(child) - return False, (val, val) - - -_before_child_handlers = ExpressionBoundsBeforeChildDispatcher() + return visitor._before_child_handlers[val.__class__](visitor, val) def _handle_ProductExpression(visitor, node, arg1, arg2): @@ -277,7 +282,7 @@ def initializeWalker(self, expr): return True, expr def beforeChild(self, node, child, child_idx): - return _before_child_handlers[child.__class__](self, child) + return self._before_child_handlers[child.__class__](self, child) def exitNode(self, node, data): - return _operator_dispatcher[node.__class__](self, node, *data) + return self._operator_dispatcher[node.__class__](self, node, *data) diff --git a/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py index c51230155a7..612a3101ef7 100644 --- a/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py @@ -273,11 +273,19 @@ def test_npv_expression(self): def test_invalid_numeric_type(self): m = self.make_model() - m.p = Param(initialize=True, domain=Any) + m.p = Param(initialize=True, mutable=True, domain=Any) visitor = ExpressionBoundsVisitor() with self.assertRaisesRegex( ValueError, - r"True \(\) is not a valid numeric type. " + r"True \(bool\) is not a valid numeric type. " + r"Cannot compute bounds on expression.", + ): + lb, ub = visitor.walk_expression(m.p + m.y) + + m.p.set_value(None) + with self.assertRaisesRegex( + ValueError, + r"None \(NoneType\) is not a valid numeric type. " r"Cannot compute bounds on expression.", ): lb, ub = visitor.walk_expression(m.p + m.y) @@ -288,7 +296,7 @@ def test_invalid_string(self): visitor = ExpressionBoundsVisitor() with self.assertRaisesRegex( ValueError, - r"'True' \(\) is not a valid numeric type. " + r"'True' \(str\) is not a valid numeric type. " r"Cannot compute bounds on expression.", ): lb, ub = visitor.walk_expression(m.p + m.y) diff --git a/pyomo/core/expr/__init__.py b/pyomo/core/expr/__init__.py index bd6d1b995a1..a03578de957 100644 --- a/pyomo/core/expr/__init__.py +++ b/pyomo/core/expr/__init__.py @@ -56,6 +56,7 @@ # BooleanValue, BooleanConstant, + BooleanExpression, BooleanExpressionBase, # UnaryBooleanExpression, diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index 0eec8a1541c..d4f268ae182 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -1517,7 +1517,7 @@ def test_type_registrations(self): bcd.register_dispatcher(visitor, 5), (False, (linear._CONSTANT, 5)) ) self.assertEqual(len(bcd), 1) - self.assertIs(bcd[int], bcd._before_native) + self.assertIs(bcd[int], bcd._before_native_numeric) # complex type self.assertEqual( bcd.register_dispatcher(visitor, 5j), (False, (linear._CONSTANT, 5j)) diff --git a/pyomo/repn/tests/test_util.py b/pyomo/repn/tests/test_util.py index 3f455aad13f..8ea6bda83b9 100644 --- a/pyomo/repn/tests/test_util.py +++ b/pyomo/repn/tests/test_util.py @@ -750,7 +750,7 @@ def evaluate(self, node): node = 5 self.assertEqual(bcd[node.__class__](None, node), (False, (_CONSTANT, 5))) - self.assertIs(bcd[int], bcd._before_native) + self.assertIs(bcd[int], bcd._before_native_numeric) self.assertEqual(len(bcd), 1) node = 'string' @@ -787,7 +787,7 @@ class new_int(int): node = new_int(5) self.assertEqual(bcd[node.__class__](None, node), (False, (_CONSTANT, 5))) - self.assertIs(bcd[new_int], bcd._before_native) + self.assertIs(bcd[new_int], bcd._before_native_numeric) self.assertEqual(len(bcd), 5) node = [] diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index cb67dd92494..634b4d1d640 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -25,6 +25,7 @@ native_types, native_numeric_types, native_complex_types, + native_logical_types, ) from pyomo.core.pyomoobject import PyomoObject from pyomo.core.base import ( @@ -265,7 +266,9 @@ def __missing__(self, key): def register_dispatcher(self, visitor, child): child_type = type(child) if child_type in native_numeric_types: - self[child_type] = self._before_native + self[child_type] = self._before_native_numeric + elif child_type in native_logical_types: + self[child_type] = self._before_native_logical elif issubclass(child_type, str): self[child_type] = self._before_string elif child_type in native_types: @@ -275,7 +278,7 @@ def register_dispatcher(self, visitor, child): self[child_type] = self._before_invalid elif not hasattr(child, 'is_expression_type'): if check_if_numeric_type(child): - self[child_type] = self._before_native + self[child_type] = self._before_native_numeric else: self[child_type] = self._before_invalid elif not child.is_expression_type(): @@ -306,9 +309,18 @@ def _before_general_expression(visitor, child): return True, None @staticmethod - def _before_native(visitor, child): + def _before_native_numeric(visitor, child): return False, (_CONSTANT, child) + @staticmethod + def _before_native_logical(visitor, child): + return False, ( + _CONSTANT, + InvalidNumber( + child, f"{child!r} ({type(child).__name__}) is not a valid numeric type" + ), + ) + @staticmethod def _before_complex(visitor, child): return False, (_CONSTANT, complex_number_error(child, visitor, child)) @@ -318,7 +330,7 @@ def _before_invalid(visitor, child): return False, ( _CONSTANT, InvalidNumber( - child, f"{child!r} ({type(child)}) is not a valid numeric type" + child, f"{child!r} ({type(child).__name__}) is not a valid numeric type" ), ) @@ -327,7 +339,7 @@ def _before_string(visitor, child): return False, ( _CONSTANT, InvalidNumber( - child, f"{child!r} ({type(child)}) is not a valid numeric type" + child, f"{child!r} ({type(child).__name__}) is not a valid numeric type" ), ) @@ -418,19 +430,15 @@ def __missing__(self, key): "expression tree." ) if fcn is None: - return self.unexpected_expression_type(key) + fcn = self.unexpected_expression_type if cache: self[key] = fcn return fcn - def unexpected_expression_type(self, key): - if type(key) is tuple: - node_class = key[0] - else: - node_class = key + def unexpected_expression_type(self, visitor, node, *arg): raise DeveloperError( - f"Unexpected expression node type '{node_class.__name__}' " - f"found while walking expression tree." + f"Unexpected expression node type '{type(node).__name__}' " + f"found while walking expression tree in {type(visitor).__name__}." ) From 3a9fc9fda5d567a8af40de383a84352050bdc687 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 8 Feb 2024 15:39:43 -0700 Subject: [PATCH 0495/3044] Additional dispatcher restructuring --- .../contrib/fbbt/expression_bounds_walker.py | 59 ++++++++++++++----- .../tests/test_expression_bounds_walker.py | 29 ++++++++- 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index 22ebf28ae81..31e52e0dc79 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import logging from math import pi from pyomo.common.collections import ComponentMap from pyomo.contrib.fbbt.interval import ( @@ -30,6 +31,7 @@ ) from pyomo.core.base.expression import Expression from pyomo.core.expr.numeric_expr import ( + NumericExpression, NegationExpression, ProductExpression, DivisionExpression, @@ -40,12 +42,20 @@ LinearExpression, SumExpression, ExternalFunctionExpression, + Expr_ifExpression, +) +from pyomo.core.expr.logical_expr import BooleanExpression +from pyomo.core.expr.relational_expr import ( + EqualityExpression, + InequalityExpression, + RangedExpression, ) from pyomo.core.expr.numvalue import native_numeric_types, native_types, value from pyomo.core.expr.visitor import StreamBasedExpressionVisitor from pyomo.repn.util import BeforeChildDispatcher, ExitNodeDispatcher inf = float('inf') +logger = logging.getLogger(__name__) class ExpressionBoundsBeforeChildDispatcher(BeforeChildDispatcher): @@ -226,20 +236,20 @@ def _handle_named_expression(visitor, node, arg): } -_operator_dispatcher = ExitNodeDispatcher( - { - ProductExpression: _handle_ProductExpression, - DivisionExpression: _handle_DivisionExpression, - PowExpression: _handle_PowExpression, - AbsExpression: _handle_AbsExpression, - SumExpression: _handle_SumExpression, - MonomialTermExpression: _handle_ProductExpression, - NegationExpression: _handle_NegationExpression, - UnaryFunctionExpression: _handle_UnaryFunctionExpression, - LinearExpression: _handle_SumExpression, - Expression: _handle_named_expression, - } -) +class ExpressionBoundsExitNodeDispatcher(ExitNodeDispatcher): + def unexpected_expression_type(self, visitor, node, *args): + if isinstance(node, NumericExpression): + ans = -inf, inf + elif isinstance(node, BooleanExpression): + ans = Bool(False), Bool(True) + else: + super().unexpected_expression_type(visitor, node, *args) + logger.warning( + f"Unexpected expression node type '{type(node).__name__}' " + f"found while walking expression tree; returning {ans} " + "for the expression bounds." + ) + return ans class ExpressionBoundsVisitor(StreamBasedExpressionVisitor): @@ -264,6 +274,27 @@ class ExpressionBoundsVisitor(StreamBasedExpressionVisitor): the computed bounds should be valid. """ + _before_child_handlers = ExpressionBoundsBeforeChildDispatcher() + _operator_dispatcher = ExpressionBoundsExitNodeDispatcher( + { + ProductExpression: _handle_ProductExpression, + DivisionExpression: _handle_DivisionExpression, + PowExpression: _handle_PowExpression, + AbsExpression: _handle_AbsExpression, + SumExpression: _handle_SumExpression, + MonomialTermExpression: _handle_ProductExpression, + NegationExpression: _handle_NegationExpression, + UnaryFunctionExpression: _handle_UnaryFunctionExpression, + LinearExpression: _handle_SumExpression, + Expression: _handle_named_expression, + ExternalFunctionExpression: _handle_unknowable_bounds, + EqualityExpression: _handle_equality, + InequalityExpression: _handle_inequality, + RangedExpression: _handle_ranged, + Expr_ifExpression: _handle_expr_if, + } + ) + def __init__( self, leaf_bounds=None, diff --git a/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py index 612a3101ef7..8b30ffdef4b 100644 --- a/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py @@ -10,10 +10,33 @@ # ___________________________________________________________________________ import math -from pyomo.environ import exp, log, log10, sin, cos, tan, asin, acos, atan, sqrt import pyomo.common.unittest as unittest -from pyomo.contrib.fbbt.expression_bounds_walker import ExpressionBoundsVisitor -from pyomo.core import Any, ConcreteModel, Expression, Param, Var + +from pyomo.environ import ( + exp, + log, + log10, + sin, + cos, + tan, + asin, + acos, + atan, + sqrt, + inequality, + Expr_if, + Any, + ConcreteModel, + Expression, + Param, + Var, +) + +from pyomo.common.errors import DeveloperError +from pyomo.common.log import LoggingIntercept +from pyomo.contrib.fbbt.expression_bounds_walker import ExpressionBoundsVisitor, inf +from pyomo.contrib.fbbt.interval import _true, _false +from pyomo.core.expr import ExpressionBase, NumericExpression, BooleanExpression class TestExpressionBoundsWalker(unittest.TestCase): From 64d4f473d566b22f27369a1c1b1c5aaa01198467 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 8 Feb 2024 15:40:22 -0700 Subject: [PATCH 0496/3044] Add support for walking logical expressions --- .../contrib/fbbt/expression_bounds_walker.py | 25 ++++++ pyomo/contrib/fbbt/interval.py | 89 +++++++++++++++++++ .../tests/test_expression_bounds_walker.py | 79 ++++++++++++++++ 3 files changed, 193 insertions(+) diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index 31e52e0dc79..a32d138c52b 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -13,6 +13,11 @@ from math import pi from pyomo.common.collections import ComponentMap from pyomo.contrib.fbbt.interval import ( + Bool, + eq, + ineq, + ranged, + if_, add, acos, asin, @@ -222,6 +227,26 @@ def _handle_named_expression(visitor, node, arg): return arg +def _handle_unknowable_bounds(visitor, node, arg): + return -inf, inf + + +def _handle_equality(visitor, node, arg1, arg2): + return eq(*arg1, *arg2) + + +def _handle_inequality(visitor, node, arg1, arg2): + return ineq(*arg1, *arg2) + + +def _handle_ranged(visitor, node, arg1, arg2, arg3): + return ranged(*arg1, *arg2, *arg3) + + +def _handle_expr_if(visitor, node, arg1, arg2, arg3): + return if_(*arg1, *arg2, *arg3) + + _unary_function_dispatcher = { 'exp': _handle_exp, 'log': _handle_log, diff --git a/pyomo/contrib/fbbt/interval.py b/pyomo/contrib/fbbt/interval.py index fd86af4c106..53c236850d9 100644 --- a/pyomo/contrib/fbbt/interval.py +++ b/pyomo/contrib/fbbt/interval.py @@ -17,6 +17,95 @@ inf = float('inf') +class bool_(object): + def __init__(self, val): + self._val = val + + def __bool__(self): + return self._val + + def _op(self, *others): + raise ValueError( + f"{self._val!r} ({type(self._val).__name__}) is not a valid numeric type. " + f"Cannot compute bounds on expression." + ) + + def __repr__(self): + return repr(self._val) + + __float__ = _op + __int__ = _op + __abs__ = _op + __neg__ = _op + __add__ = _op + __sub__ = _op + __mul__ = _op + __div__ = _op + __pow__ = _op + __radd__ = _op + __rsub__ = _op + __rmul__ = _op + __rdiv__ = _op + __rpow__ = _op + + +_true = bool_(True) +_false = bool_(False) + + +def Bool(val): + return _true if val else _false + + +def ineq(xl, xu, yl, yu): + ans = [] + if yl < xu: + ans.append(_false) + if xl <= yu: + ans.append(_true) + assert ans + if len(ans) == 1: + ans.append(ans[0]) + return tuple(ans) + + +def eq(xl, xu, yl, yu): + ans = [] + if xl != xu or yl != yu or xl != yl: + ans.append(_false) + if xl <= yu and yl <= xu: + ans.append(_true) + assert ans + if len(ans) == 1: + ans.append(ans[0]) + return tuple(ans) + + +def ranged(xl, xu, yl, yu, zl, zu): + lb = ineq(xl, xu, yl, yu) + ub = ineq(yl, yu, zl, zu) + ans = [] + if not lb[0] or not ub[0]: + ans.append(_false) + if lb[1] and ub[1]: + ans.append(_true) + if len(ans) == 1: + ans.append(ans[0]) + return tuple(ans) + + +def if_(il, iu, tl, tu, fl, fu): + l = [] + u = [] + if iu: + l.append(tl) + u.append(tu) + if not il: + l.append(fl) + u.append(fu) + return min(l), max(u) + + def add(xl, xu, yl, yu): return xl + yl, xu + yu diff --git a/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py index 8b30ffdef4b..75d273422d1 100644 --- a/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py @@ -334,3 +334,82 @@ def test_invalid_complex(self): r"complex numbers. Encountered when processing \(4\+5j\)", ): lb, ub = visitor.walk_expression(m.p + m.y) + + def test_inequality(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + self.assertEqual(visitor.walk_expression(m.z <= m.y), (_true, _true)) + self.assertEqual(visitor.walk_expression(m.y <= m.z), (_false, _false)) + self.assertEqual(visitor.walk_expression(m.y <= m.x), (_false, _true)) + + def test_equality(self): + m = self.make_model() + m.p = Param(initialize=5) + visitor = ExpressionBoundsVisitor() + self.assertEqual(visitor.walk_expression(m.y == m.z), (_false, _false)) + self.assertEqual(visitor.walk_expression(m.y == m.x), (_false, _true)) + self.assertEqual(visitor.walk_expression(m.p == m.p), (_true, _true)) + + def test_ranged(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + self.assertEqual( + visitor.walk_expression(inequality(m.z, m.y, 5)), (_true, _true) + ) + self.assertEqual( + visitor.walk_expression(inequality(m.y, m.z, m.y)), (_false, _false) + ) + self.assertEqual( + visitor.walk_expression(inequality(m.y, m.x, m.y)), (_false, _true) + ) + + def test_expr_if(self): + m = self.make_model() + visitor = ExpressionBoundsVisitor() + self.assertEqual( + visitor.walk_expression(Expr_if(IF=m.z <= m.y, THEN=m.z, ELSE=m.y)), + m.z.bounds, + ) + self.assertEqual( + visitor.walk_expression(Expr_if(IF=m.z >= m.y, THEN=m.z, ELSE=m.y)), + m.y.bounds, + ) + self.assertEqual( + visitor.walk_expression(Expr_if(IF=m.y <= m.x, THEN=m.y, ELSE=m.x)), (-2, 5) + ) + + def test_unknown_classes(self): + class UnknownNumeric(NumericExpression): + pass + + class UnknownLogic(BooleanExpression): + def nargs(self): + return 0 + + class UnknownOther(ExpressionBase): + @property + def args(self): + return () + + def nargs(self): + return 0 + + visitor = ExpressionBoundsVisitor() + with LoggingIntercept() as LOG: + self.assertEqual(visitor.walk_expression(UnknownNumeric(())), (-inf, inf)) + self.assertEqual( + LOG.getvalue(), + "Unexpected expression node type 'UnknownNumeric' found while walking " + "expression tree; returning (-inf, inf) for the expression bounds.\n", + ) + with LoggingIntercept() as LOG: + self.assertEqual(visitor.walk_expression(UnknownLogic(())), (_false, _true)) + self.assertEqual( + LOG.getvalue(), + "Unexpected expression node type 'UnknownLogic' found while walking " + "expression tree; returning (False, True) for the expression bounds.\n", + ) + with self.assertRaisesRegex( + DeveloperError, "Unexpected expression node type 'UnknownOther' found" + ): + visitor.walk_expression(UnknownOther()) From 45ca395d213dfa74393cd52c094e73992bccfac3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 8 Feb 2024 17:17:51 -0700 Subject: [PATCH 0497/3044] Updating tests to reflect changes in the before child dispatcher --- pyomo/repn/tests/test_util.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/repn/tests/test_util.py b/pyomo/repn/tests/test_util.py index 8ea6bda83b9..48d78c60d6e 100644 --- a/pyomo/repn/tests/test_util.py +++ b/pyomo/repn/tests/test_util.py @@ -717,14 +717,14 @@ class UnknownExpression(NumericExpression): DeveloperError, r".*Unexpected expression node type 'UnknownExpression'" ): end[node.__class__](None, node, *node.args) - self.assertEqual(len(end), 8) + self.assertEqual(len(end), 9) node = UnknownExpression((6, 7)) with self.assertRaisesRegex( DeveloperError, r".*Unexpected expression node type 'UnknownExpression'" ): end[node.__class__, 6, 7](None, node, *node.args) - self.assertEqual(len(end), 8) + self.assertEqual(len(end), 10) def test_BeforeChildDispatcher_registration(self): class BeforeChildDispatcherTester(BeforeChildDispatcher): @@ -758,7 +758,7 @@ def evaluate(self, node): self.assertEqual(ans, (False, (_CONSTANT, InvalidNumber(node)))) self.assertEqual( ''.join(ans[1][1].causes), - "'string' () is not a valid numeric type", + "'string' (str) is not a valid numeric type", ) self.assertIs(bcd[str], bcd._before_string) self.assertEqual(len(bcd), 2) @@ -768,9 +768,9 @@ def evaluate(self, node): self.assertEqual(ans, (False, (_CONSTANT, InvalidNumber(node)))) self.assertEqual( ''.join(ans[1][1].causes), - "True () is not a valid numeric type", + "True (bool) is not a valid numeric type", ) - self.assertIs(bcd[bool], bcd._before_invalid) + self.assertIs(bcd[bool], bcd._before_native_logical) self.assertEqual(len(bcd), 3) node = 1j @@ -794,7 +794,7 @@ class new_int(int): ans = bcd[node.__class__](None, node) self.assertEqual(ans, (False, (_CONSTANT, InvalidNumber([])))) self.assertEqual( - ''.join(ans[1][1].causes), "[] () is not a valid numeric type" + ''.join(ans[1][1].causes), "[] (list) is not a valid numeric type" ) self.assertIs(bcd[list], bcd._before_invalid) self.assertEqual(len(bcd), 6) From 67acc27477914b7e254fa4df5c978281448f50ee Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Feb 2024 09:05:48 -0700 Subject: [PATCH 0498/3044] NFC: apply black --- pyomo/repn/tests/test_util.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/repn/tests/test_util.py b/pyomo/repn/tests/test_util.py index 48d78c60d6e..cce10e58334 100644 --- a/pyomo/repn/tests/test_util.py +++ b/pyomo/repn/tests/test_util.py @@ -757,8 +757,7 @@ def evaluate(self, node): ans = bcd[node.__class__](None, node) self.assertEqual(ans, (False, (_CONSTANT, InvalidNumber(node)))) self.assertEqual( - ''.join(ans[1][1].causes), - "'string' (str) is not a valid numeric type", + ''.join(ans[1][1].causes), "'string' (str) is not a valid numeric type" ) self.assertIs(bcd[str], bcd._before_string) self.assertEqual(len(bcd), 2) @@ -767,8 +766,7 @@ def evaluate(self, node): ans = bcd[node.__class__](None, node) self.assertEqual(ans, (False, (_CONSTANT, InvalidNumber(node)))) self.assertEqual( - ''.join(ans[1][1].causes), - "True (bool) is not a valid numeric type", + ''.join(ans[1][1].causes), "True (bool) is not a valid numeric type" ) self.assertIs(bcd[bool], bcd._before_native_logical) self.assertEqual(len(bcd), 3) From 73e1e2865c82a57d04bd86c662694dc79513b419 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 9 Feb 2024 14:19:46 -0500 Subject: [PATCH 0499/3044] Limit visibility of option `p_robustness` --- pyomo/contrib/pyros/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index f12fb3d0be0..3256a333fdc 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -945,6 +945,7 @@ def pyros_config(): the nominal parameter realization. """ ), + visibility=1, ), ) From 227836df546cef9729f6af5dbb32664e75f3f3ac Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Fri, 9 Feb 2024 13:11:31 -0700 Subject: [PATCH 0500/3044] remove unused imports --- pyomo/contrib/incidence_analysis/config.py | 2 +- pyomo/contrib/incidence_analysis/incidence.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/config.py b/pyomo/contrib/incidence_analysis/config.py index d055be478fe..72d1a41ac74 100644 --- a/pyomo/contrib/incidence_analysis/config.py +++ b/pyomo/contrib/incidence_analysis/config.py @@ -14,7 +14,7 @@ import enum from pyomo.common.config import ConfigDict, ConfigValue, InEnum from pyomo.common.modeling import NOTSET -from pyomo.repn.plugins.nl_writer import AMPLRepnVisitor, AMPLRepn, text_nl_template +from pyomo.repn.plugins.nl_writer import AMPLRepnVisitor, text_nl_template from pyomo.repn.util import FileDeterminism, FileDeterminism_to_SortComponents diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index 636a400def4..13e9997d6c3 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -16,9 +16,8 @@ from pyomo.core.expr.visitor import identify_variables from pyomo.core.expr.numvalue import value as pyo_value from pyomo.repn import generate_standard_repn -from pyomo.repn.plugins.nl_writer import AMPLRepnVisitor, AMPLRepn, text_nl_template -from pyomo.repn.util import FileDeterminism, FileDeterminism_to_SortComponents from pyomo.util.subsystems import TemporarySubsystemManager +from pyomo.repn.plugins.nl_writer import AMPLRepn from pyomo.contrib.incidence_analysis.config import ( IncidenceMethod, get_config_from_kwds, From cbbcceb7a1eae4d215053b77d38cb47bc6bc95b5 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 9 Feb 2024 15:11:49 -0500 Subject: [PATCH 0501/3044] Add note on `options` arg to docs --- doc/OnlineDocs/contributed_packages/pyros.rst | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index 3ff1bfccf0e..d741bb26b5c 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -142,6 +142,7 @@ PyROS Solver Interface Otherwise, the solution returned is certified to only be robust feasible. + PyROS Uncertainty Sets ----------------------------- Uncertainty sets are represented by subclasses of @@ -518,7 +519,7 @@ correspond to first-stage degrees of freedom. >>> # === Designate which variables correspond to first-stage >>> # and second-stage degrees of freedom === - >>> first_stage_variables =[ + >>> first_stage_variables = [ ... m.x1, m.x2, m.x3, m.x4, m.x5, m.x6, ... m.x19, m.x20, m.x21, m.x22, m.x23, m.x24, m.x31, ... ] @@ -657,6 +658,54 @@ For this example, we notice a ~25% decrease in the final objective value when switching from a static decision rule (no second-stage recourse) to an affine decision rule. + +Specifying Arguments Indirectly Through ``options`` +""""""""""""""""""""""""""""""""""""""""""""""""""" +Like other Pyomo solver interface methods, +:meth:`~pyomo.contrib.pyros.PyROS.solve` +provides support for specifying options indirectly by passing +a keyword argument ``options``, whose value must be a :class:`dict` +mapping names of arguments to :meth:`~pyomo.contrib.pyros.PyROS.solve` +to their desired values. +For example, the ``solve()`` statement in the +:ref:`two-stage problem example ` +could have been equivalently written as: + +.. doctest:: + :skipif: not (baron.available() and baron.license_is_valid()) + + >>> results_2 = pyros_solver.solve( + ... model=m, + ... first_stage_variables=first_stage_variables, + ... second_stage_variables=second_stage_variables, + ... uncertain_params=uncertain_parameters, + ... uncertainty_set=box_uncertainty_set, + ... local_solver=local_solver, + ... global_solver=global_solver, + ... options={ + ... "objective_focus": pyros.ObjectiveType.worst_case, + ... "solve_master_globally": True, + ... "decision_rule_order": 1, + ... }, + ... ) + ============================================================================== + PyROS: The Pyomo Robust Optimization Solver. + ... + ------------------------------------------------------------------------------ + Robust optimal solution identified. + ------------------------------------------------------------------------------ + ... + ------------------------------------------------------------------------------ + All done. Exiting PyROS. + ============================================================================== + +In the event an argument is passed directly +by position or keyword, *and* indirectly through ``options``, +an appropriate warning is issued, +and the value passed directly takes precedence over the value +passed through ``options``. + + The Price of Robustness """""""""""""""""""""""" In conjunction with standard Python control flow tools, From d4d8d32c31fbb34c77f7b87946c2e26f1b03b715 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 9 Feb 2024 15:12:59 -0500 Subject: [PATCH 0502/3044] Tweak new note wording --- doc/OnlineDocs/contributed_packages/pyros.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index d741bb26b5c..b5b71020a9c 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -668,7 +668,7 @@ a keyword argument ``options``, whose value must be a :class:`dict` mapping names of arguments to :meth:`~pyomo.contrib.pyros.PyROS.solve` to their desired values. For example, the ``solve()`` statement in the -:ref:`two-stage problem example ` +:ref:`two-stage problem snippet ` could have been equivalently written as: .. doctest:: From 677ebc9e67d363d364fa6f771fa610fe2bda2bbc Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Fri, 9 Feb 2024 13:14:24 -0700 Subject: [PATCH 0503/3044] remove unused imports --- pyomo/contrib/incidence_analysis/interface.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 41f0ece3a75..b6e6583da88 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -45,8 +45,6 @@ ) from pyomo.contrib.incidence_analysis.incidence import get_incident_variables from pyomo.contrib.pynumero.asl import AmplInterface -from pyomo.repn.plugins.nl_writer import AMPLRepnVisitor, AMPLRepn, text_nl_template -from pyomo.repn.util import FileDeterminism, FileDeterminism_to_SortComponents pyomo_nlp, pyomo_nlp_available = attempt_import( 'pyomo.contrib.pynumero.interfaces.pyomo_nlp' From 3ca70d16458afcfe85e9d9cb9dc9936489cdbd22 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 10 Feb 2024 21:31:15 -0700 Subject: [PATCH 0504/3044] porting appsi_gurobi to contrib/solver --- pyomo/contrib/solver/gurobi.py | 1495 +++++++++++++++++ pyomo/contrib/solver/plugins.py | 2 + pyomo/contrib/solver/sol_reader.py | 4 +- .../tests/solvers/test_gurobi_persistent.py | 691 ++++++++ .../solver/tests/solvers/test_solvers.py | 1350 +++++++++++++++ pyomo/contrib/solver/util.py | 62 +- 6 files changed, 3549 insertions(+), 55 deletions(-) create mode 100644 pyomo/contrib/solver/gurobi.py create mode 100644 pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py create mode 100644 pyomo/contrib/solver/tests/solvers/test_solvers.py diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py new file mode 100644 index 00000000000..2dcdacd320d --- /dev/null +++ b/pyomo/contrib/solver/gurobi.py @@ -0,0 +1,1495 @@ +from collections.abc import Iterable +import logging +import math +from typing import List, Dict, Optional +from pyomo.common.collections import ComponentSet, ComponentMap, OrderedSet +from pyomo.common.log import LogStream +from pyomo.common.dependencies import attempt_import +from pyomo.common.errors import PyomoException +from pyomo.common.tee import capture_output, TeeStream +from pyomo.common.timing import HierarchicalTimer +from pyomo.common.shutdown import python_is_shutting_down +from pyomo.common.config import ConfigValue, NonNegativeInt +from pyomo.core.kernel.objective import minimize, maximize +from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler +from pyomo.core.base.var import Var, _GeneralVarData +from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.param import _ParamData +from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types +from pyomo.repn import generate_standard_repn +from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression +from pyomo.contrib.solver.base import PersistentSolverBase +from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus +from pyomo.contrib.solver.config import PersistentBranchAndBoundConfig +from pyomo.contrib.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.solution import PersistentSolutionLoader +from pyomo.core.staleflag import StaleFlagManager +import sys +import datetime +import io +from pyomo.contrib.solver.factory import SolverFactory + +logger = logging.getLogger(__name__) + + +def _import_gurobipy(): + try: + import gurobipy + except ImportError: + Gurobi._available = Gurobi.Availability.NotFound + raise + if gurobipy.GRB.VERSION_MAJOR < 7: + Gurobi._available = Gurobi.Availability.BadVersion + raise ImportError('The APPSI Gurobi interface requires gurobipy>=7.0.0') + return gurobipy + + +gurobipy, gurobipy_available = attempt_import('gurobipy', importer=_import_gurobipy) + + +class DegreeError(PyomoException): + pass + + +class GurobiConfig(PersistentBranchAndBoundConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(GurobiConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + self.use_mipstart: bool = self.declare( + 'use_mipstart', + ConfigValue( + default=False, + domain=bool, + description="If True, the values of the integer variables will be passed to Gurobi.", + ) + ) + + +class GurobiSolutionLoader(PersistentSolutionLoader): + def load_vars(self, vars_to_load=None, solution_number=0): + self._assert_solution_still_valid() + self._solver._load_vars( + vars_to_load=vars_to_load, solution_number=solution_number + ) + + def get_primals(self, vars_to_load=None, solution_number=0): + self._assert_solution_still_valid() + return self._solver._get_primals( + vars_to_load=vars_to_load, solution_number=solution_number + ) + + +class _MutableLowerBound(object): + def __init__(self, expr): + self.var = None + self.expr = expr + + def update(self): + self.var.setAttr('lb', value(self.expr)) + + +class _MutableUpperBound(object): + def __init__(self, expr): + self.var = None + self.expr = expr + + def update(self): + self.var.setAttr('ub', value(self.expr)) + + +class _MutableLinearCoefficient(object): + def __init__(self): + self.expr = None + self.var = None + self.con = None + self.gurobi_model = None + + def update(self): + self.gurobi_model.chgCoeff(self.con, self.var, value(self.expr)) + + +class _MutableRangeConstant(object): + def __init__(self): + self.lhs_expr = None + self.rhs_expr = None + self.con = None + self.slack_name = None + self.gurobi_model = None + + def update(self): + rhs_val = value(self.rhs_expr) + lhs_val = value(self.lhs_expr) + self.con.rhs = rhs_val + slack = self.gurobi_model.getVarByName(self.slack_name) + slack.ub = rhs_val - lhs_val + + +class _MutableConstant(object): + def __init__(self): + self.expr = None + self.con = None + + def update(self): + self.con.rhs = value(self.expr) + + +class _MutableQuadraticConstraint(object): + def __init__( + self, gurobi_model, gurobi_con, constant, linear_coefs, quadratic_coefs + ): + self.con = gurobi_con + self.gurobi_model = gurobi_model + self.constant = constant + self.last_constant_value = value(self.constant.expr) + self.linear_coefs = linear_coefs + self.last_linear_coef_values = [value(i.expr) for i in self.linear_coefs] + self.quadratic_coefs = quadratic_coefs + self.last_quadratic_coef_values = [value(i.expr) for i in self.quadratic_coefs] + + def get_updated_expression(self): + gurobi_expr = self.gurobi_model.getQCRow(self.con) + for ndx, coef in enumerate(self.linear_coefs): + current_coef_value = value(coef.expr) + incremental_coef_value = ( + current_coef_value - self.last_linear_coef_values[ndx] + ) + gurobi_expr += incremental_coef_value * coef.var + self.last_linear_coef_values[ndx] = current_coef_value + for ndx, coef in enumerate(self.quadratic_coefs): + current_coef_value = value(coef.expr) + incremental_coef_value = ( + current_coef_value - self.last_quadratic_coef_values[ndx] + ) + gurobi_expr += incremental_coef_value * coef.var1 * coef.var2 + self.last_quadratic_coef_values[ndx] = current_coef_value + return gurobi_expr + + def get_updated_rhs(self): + return value(self.constant.expr) + + +class _MutableObjective(object): + def __init__(self, gurobi_model, constant, linear_coefs, quadratic_coefs): + self.gurobi_model = gurobi_model + self.constant = constant + self.linear_coefs = linear_coefs + self.quadratic_coefs = quadratic_coefs + self.last_quadratic_coef_values = [value(i.expr) for i in self.quadratic_coefs] + + def get_updated_expression(self): + for ndx, coef in enumerate(self.linear_coefs): + coef.var.obj = value(coef.expr) + self.gurobi_model.ObjCon = value(self.constant.expr) + + gurobi_expr = None + for ndx, coef in enumerate(self.quadratic_coefs): + if value(coef.expr) != self.last_quadratic_coef_values[ndx]: + if gurobi_expr is None: + self.gurobi_model.update() + gurobi_expr = self.gurobi_model.getObjective() + current_coef_value = value(coef.expr) + incremental_coef_value = ( + current_coef_value - self.last_quadratic_coef_values[ndx] + ) + gurobi_expr += incremental_coef_value * coef.var1 * coef.var2 + self.last_quadratic_coef_values[ndx] = current_coef_value + return gurobi_expr + + +class _MutableQuadraticCoefficient(object): + def __init__(self): + self.expr = None + self.var1 = None + self.var2 = None + + +class Gurobi(PersistentSolverUtils, PersistentSolverBase): + """ + Interface to Gurobi + """ + + CONFIG = GurobiConfig() + + _available = None + _num_instances = 0 + + def __init__(self, **kwds): + PersistentSolverUtils.__init__(self) + PersistentSolverBase.__init__(self, **kwds) + self._num_instances += 1 + self._solver_model = None + self._symbol_map = SymbolMap() + self._labeler = None + self._pyomo_var_to_solver_var_map = dict() + self._pyomo_con_to_solver_con_map = dict() + self._solver_con_to_pyomo_con_map = dict() + self._pyomo_sos_to_solver_sos_map = dict() + self._range_constraints = OrderedSet() + self._mutable_helpers = dict() + self._mutable_bounds = dict() + self._mutable_quadratic_helpers = dict() + self._mutable_objective = None + self._needs_updated = True + self._callback = None + self._callback_func = None + self._constraints_added_since_update = OrderedSet() + self._vars_added_since_update = ComponentSet() + self._last_results_object: Optional[Results] = None + self._config: Optional[GurobiConfig] = None + + def available(self): + if not gurobipy_available: # this triggers the deferred import + return self.Availability.NotFound + elif self._available == self.Availability.BadVersion: + return self.Availability.BadVersion + else: + return self._check_license() + + def _check_license(self): + avail = False + try: + # Gurobipy writes out license file information when creating + # the environment + with capture_output(capture_fd=True): + m = gurobipy.Model() + if self._solver_model is None: + self._solver_model = m + avail = True + except gurobipy.GurobiError: + avail = False + + if avail: + if self._available is None: + res = Gurobi._check_full_license() + self._available = res + return res + else: + return self._available + else: + return self.Availability.BadLicense + + @classmethod + def _check_full_license(cls): + m = gurobipy.Model() + m.setParam('OutputFlag', 0) + try: + m.addVars(range(2001)) + m.optimize() + return cls.Availability.FullLicense + except gurobipy.GurobiError: + return cls.Availability.LimitedLicense + + def release_license(self): + self._reinit() + if gurobipy_available: + with capture_output(capture_fd=True): + gurobipy.disposeDefaultEnv() + + def __del__(self): + if not python_is_shutting_down(): + self._num_instances -= 1 + if self._num_instances == 0: + self.release_license() + + def version(self): + version = ( + gurobipy.GRB.VERSION_MAJOR, + gurobipy.GRB.VERSION_MINOR, + gurobipy.GRB.VERSION_TECHNICAL, + ) + return version + + @property + def symbol_map(self): + return self._symbol_map + + def _solve(self): + config = self._config + timer = config.timer + ostreams = [io.StringIO()] + if config.tee: + ostreams.append(sys.stdout) + if config.log_solver_output: + ostreams.append(LogStream(level=logging.INFO, logger=logger)) + + with TeeStream(*ostreams) as t: + with capture_output(output=t.STDOUT, capture_fd=False): + options = config.solver_options + + self._solver_model.setParam('LogToConsole', 1) + + if config.threads is not None: + self._solver_model.setParam('Threads', config.threads) + if config.time_limit is not None: + self._solver_model.setParam('TimeLimit', config.time_limit) + if config.rel_gap is not None: + self._solver_model.setParam('MIPGap', config.rel_gap) + if config.abs_gap is not None: + self._solver_model.setParam('MIPGapAbs', config.abs_gap) + + if config.use_mipstart: + for pyomo_var_id, gurobi_var in self._pyomo_var_to_solver_var_map.items(): + pyomo_var = self._vars[pyomo_var_id][0] + if pyomo_var.is_integer() and pyomo_var.value is not None: + self.set_var_attr(pyomo_var, 'Start', pyomo_var.value) + + for key, option in options.items(): + self._solver_model.setParam(key, option) + + timer.start('optimize') + self._solver_model.optimize(self._callback) + timer.stop('optimize') + + self._needs_updated = False + res = self._postsolve(timer) + res.solver_configuration = config + res.solver_name = 'Gurobi' + res.solver_version = self.version() + res.solver_log = ostreams[0].getvalue() + return res + + def solve(self, model, **kwds) -> Results: + start_timestamp = datetime.datetime.now(datetime.timezone.utc) + self._config = config = self.config(value=kwds, preserve_implicit=True) + StaleFlagManager.mark_all_as_stale() + # Note: solver availability check happens in set_instance(), + # which will be called (either by the user before this call, or + # below) before this method calls self._solve. + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if config.timer is None: + config.timer = HierarchicalTimer() + timer = config.timer + if model is not self._model: + timer.start('set_instance') + self.set_instance(model) + timer.stop('set_instance') + else: + timer.start('update') + self.update(timer=timer) + timer.stop('update') + res = self._solve() + self._last_results_object = res + end_timestamp = datetime.datetime.now(datetime.timezone.utc) + res.timing_info.start_timestamp = start_timestamp + res.timing_info.wall_time = (end_timestamp - start_timestamp).total_seconds() + res.timing_info.timer = timer + return res + + def _process_domain_and_bounds( + self, var, var_id, mutable_lbs, mutable_ubs, ndx, gurobipy_var + ): + _v, _lb, _ub, _fixed, _domain_interval, _value = self._vars[id(var)] + lb, ub, step = _domain_interval + if lb is None: + lb = -gurobipy.GRB.INFINITY + if ub is None: + ub = gurobipy.GRB.INFINITY + if step == 0: + vtype = gurobipy.GRB.CONTINUOUS + elif step == 1: + if lb == 0 and ub == 1: + vtype = gurobipy.GRB.BINARY + else: + vtype = gurobipy.GRB.INTEGER + else: + raise ValueError( + f'Unrecognized domain step: {step} (should be either 0 or 1)' + ) + if _fixed: + lb = _value + ub = _value + else: + if _lb is not None: + if not is_constant(_lb): + mutable_bound = _MutableLowerBound(NPV_MaxExpression((_lb, lb))) + if gurobipy_var is None: + mutable_lbs[ndx] = mutable_bound + else: + mutable_bound.var = gurobipy_var + self._mutable_bounds[var_id, 'lb'] = (var, mutable_bound) + lb = max(value(_lb), lb) + if _ub is not None: + if not is_constant(_ub): + mutable_bound = _MutableUpperBound(NPV_MinExpression((_ub, ub))) + if gurobipy_var is None: + mutable_ubs[ndx] = mutable_bound + else: + mutable_bound.var = gurobipy_var + self._mutable_bounds[var_id, 'ub'] = (var, mutable_bound) + ub = min(value(_ub), ub) + + return lb, ub, vtype + + def _add_variables(self, variables: List[_GeneralVarData]): + var_names = list() + vtypes = list() + lbs = list() + ubs = list() + mutable_lbs = dict() + mutable_ubs = dict() + for ndx, var in enumerate(variables): + varname = self._symbol_map.getSymbol(var, self._labeler) + lb, ub, vtype = self._process_domain_and_bounds( + var, id(var), mutable_lbs, mutable_ubs, ndx, None + ) + var_names.append(varname) + vtypes.append(vtype) + lbs.append(lb) + ubs.append(ub) + + gurobi_vars = self._solver_model.addVars( + len(variables), lb=lbs, ub=ubs, vtype=vtypes, name=var_names + ) + + for ndx, pyomo_var in enumerate(variables): + gurobi_var = gurobi_vars[ndx] + self._pyomo_var_to_solver_var_map[id(pyomo_var)] = gurobi_var + for ndx, mutable_bound in mutable_lbs.items(): + mutable_bound.var = gurobi_vars[ndx] + for ndx, mutable_bound in mutable_ubs.items(): + mutable_bound.var = gurobi_vars[ndx] + self._vars_added_since_update.update(variables) + self._needs_updated = True + + def _add_params(self, params: List[_ParamData]): + pass + + def _reinit(self): + saved_config = self.config + saved_tmp_config = self._config + self.__init__() + self.config = saved_config + self._config = saved_tmp_config + + def set_instance(self, model): + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if not self.available(): + c = self.__class__ + raise PyomoException( + f'Solver {c.__module__}.{c.__qualname__} is not available ' + f'({self.available()}).' + ) + self._reinit() + self._model = model + + if self.config.symbolic_solver_labels: + self._labeler = TextLabeler() + else: + self._labeler = NumericLabeler('x') + + if model.name is not None: + self._solver_model = gurobipy.Model(model.name) + else: + self._solver_model = gurobipy.Model() + + self.add_block(model) + if self._objective is None: + self.set_objective(None) + + def _get_expr_from_pyomo_expr(self, expr): + mutable_linear_coefficients = list() + mutable_quadratic_coefficients = list() + repn = generate_standard_repn(expr, quadratic=True, compute_values=False) + + degree = repn.polynomial_degree() + if (degree is None) or (degree > 2): + raise DegreeError( + 'GurobiAuto does not support expressions of degree {0}.'.format(degree) + ) + + if len(repn.linear_vars) > 0: + linear_coef_vals = list() + for ndx, coef in enumerate(repn.linear_coefs): + if not is_constant(coef): + mutable_linear_coefficient = _MutableLinearCoefficient() + mutable_linear_coefficient.expr = coef + mutable_linear_coefficient.var = self._pyomo_var_to_solver_var_map[ + id(repn.linear_vars[ndx]) + ] + mutable_linear_coefficients.append(mutable_linear_coefficient) + linear_coef_vals.append(value(coef)) + new_expr = gurobipy.LinExpr( + linear_coef_vals, + [self._pyomo_var_to_solver_var_map[id(i)] for i in repn.linear_vars], + ) + else: + new_expr = 0.0 + + for ndx, v in enumerate(repn.quadratic_vars): + x, y = v + gurobi_x = self._pyomo_var_to_solver_var_map[id(x)] + gurobi_y = self._pyomo_var_to_solver_var_map[id(y)] + coef = repn.quadratic_coefs[ndx] + if not is_constant(coef): + mutable_quadratic_coefficient = _MutableQuadraticCoefficient() + mutable_quadratic_coefficient.expr = coef + mutable_quadratic_coefficient.var1 = gurobi_x + mutable_quadratic_coefficient.var2 = gurobi_y + mutable_quadratic_coefficients.append(mutable_quadratic_coefficient) + coef_val = value(coef) + new_expr += coef_val * gurobi_x * gurobi_y + + return ( + new_expr, + repn.constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) + + def _add_constraints(self, cons: List[_GeneralConstraintData]): + for con in cons: + conname = self._symbol_map.getSymbol(con, self._labeler) + ( + gurobi_expr, + repn_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) = self._get_expr_from_pyomo_expr(con.body) + + if ( + gurobi_expr.__class__ in {gurobipy.LinExpr, gurobipy.Var} + or gurobi_expr.__class__ in native_numeric_types + ): + if con.equality: + rhs_expr = con.lower - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addLConstr( + gurobi_expr, gurobipy.GRB.EQUAL, rhs_val, name=conname + ) + if not is_constant(rhs_expr): + mutable_constant = _MutableConstant() + mutable_constant.expr = rhs_expr + mutable_constant.con = gurobipy_con + self._mutable_helpers[con] = [mutable_constant] + elif con.has_lb() and con.has_ub(): + lhs_expr = con.lower - repn_constant + rhs_expr = con.upper - repn_constant + lhs_val = value(lhs_expr) + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addRange( + gurobi_expr, lhs_val, rhs_val, name=conname + ) + self._range_constraints.add(con) + if not is_constant(lhs_expr) or not is_constant(rhs_expr): + mutable_range_constant = _MutableRangeConstant() + mutable_range_constant.lhs_expr = lhs_expr + mutable_range_constant.rhs_expr = rhs_expr + mutable_range_constant.con = gurobipy_con + mutable_range_constant.slack_name = 'Rg' + conname + mutable_range_constant.gurobi_model = self._solver_model + self._mutable_helpers[con] = [mutable_range_constant] + elif con.has_lb(): + rhs_expr = con.lower - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addLConstr( + gurobi_expr, gurobipy.GRB.GREATER_EQUAL, rhs_val, name=conname + ) + if not is_constant(rhs_expr): + mutable_constant = _MutableConstant() + mutable_constant.expr = rhs_expr + mutable_constant.con = gurobipy_con + self._mutable_helpers[con] = [mutable_constant] + elif con.has_ub(): + rhs_expr = con.upper - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addLConstr( + gurobi_expr, gurobipy.GRB.LESS_EQUAL, rhs_val, name=conname + ) + if not is_constant(rhs_expr): + mutable_constant = _MutableConstant() + mutable_constant.expr = rhs_expr + mutable_constant.con = gurobipy_con + self._mutable_helpers[con] = [mutable_constant] + else: + raise ValueError( + "Constraint does not have a lower " + "or an upper bound: {0} \n".format(con) + ) + for tmp in mutable_linear_coefficients: + tmp.con = gurobipy_con + tmp.gurobi_model = self._solver_model + if len(mutable_linear_coefficients) > 0: + if con not in self._mutable_helpers: + self._mutable_helpers[con] = mutable_linear_coefficients + else: + self._mutable_helpers[con].extend(mutable_linear_coefficients) + elif gurobi_expr.__class__ is gurobipy.QuadExpr: + if con.equality: + rhs_expr = con.lower - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addQConstr( + gurobi_expr, gurobipy.GRB.EQUAL, rhs_val, name=conname + ) + elif con.has_lb() and con.has_ub(): + raise NotImplementedError( + 'Quadratic range constraints are not supported' + ) + elif con.has_lb(): + rhs_expr = con.lower - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addQConstr( + gurobi_expr, gurobipy.GRB.GREATER_EQUAL, rhs_val, name=conname + ) + elif con.has_ub(): + rhs_expr = con.upper - repn_constant + rhs_val = value(rhs_expr) + gurobipy_con = self._solver_model.addQConstr( + gurobi_expr, gurobipy.GRB.LESS_EQUAL, rhs_val, name=conname + ) + else: + raise ValueError( + "Constraint does not have a lower " + "or an upper bound: {0} \n".format(con) + ) + if ( + len(mutable_linear_coefficients) > 0 + or len(mutable_quadratic_coefficients) > 0 + or not is_constant(repn_constant) + ): + mutable_constant = _MutableConstant() + mutable_constant.expr = rhs_expr + mutable_quadratic_constraint = _MutableQuadraticConstraint( + self._solver_model, + gurobipy_con, + mutable_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) + self._mutable_quadratic_helpers[con] = mutable_quadratic_constraint + else: + raise ValueError( + 'Unrecognized Gurobi expression type: ' + str(gurobi_expr.__class__) + ) + + self._pyomo_con_to_solver_con_map[con] = gurobipy_con + self._solver_con_to_pyomo_con_map[id(gurobipy_con)] = con + self._constraints_added_since_update.update(cons) + self._needs_updated = True + + def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + for con in cons: + conname = self._symbol_map.getSymbol(con, self._labeler) + level = con.level + if level == 1: + sos_type = gurobipy.GRB.SOS_TYPE1 + elif level == 2: + sos_type = gurobipy.GRB.SOS_TYPE2 + else: + raise ValueError( + "Solver does not support SOS level {0} constraints".format(level) + ) + + gurobi_vars = [] + weights = [] + + for v, w in con.get_items(): + v_id = id(v) + gurobi_vars.append(self._pyomo_var_to_solver_var_map[v_id]) + weights.append(w) + + gurobipy_con = self._solver_model.addSOS(sos_type, gurobi_vars, weights) + self._pyomo_sos_to_solver_sos_map[con] = gurobipy_con + self._constraints_added_since_update.update(cons) + self._needs_updated = True + + def _remove_constraints(self, cons: List[_GeneralConstraintData]): + for con in cons: + if con in self._constraints_added_since_update: + self._update_gurobi_model() + solver_con = self._pyomo_con_to_solver_con_map[con] + self._solver_model.remove(solver_con) + self._symbol_map.removeSymbol(con) + del self._pyomo_con_to_solver_con_map[con] + del self._solver_con_to_pyomo_con_map[id(solver_con)] + self._range_constraints.discard(con) + self._mutable_helpers.pop(con, None) + self._mutable_quadratic_helpers.pop(con, None) + self._needs_updated = True + + def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + for con in cons: + if con in self._constraints_added_since_update: + self._update_gurobi_model() + solver_sos_con = self._pyomo_sos_to_solver_sos_map[con] + self._solver_model.remove(solver_sos_con) + self._symbol_map.removeSymbol(con) + del self._pyomo_sos_to_solver_sos_map[con] + self._needs_updated = True + + def _remove_variables(self, variables: List[_GeneralVarData]): + for var in variables: + v_id = id(var) + if var in self._vars_added_since_update: + self._update_gurobi_model() + solver_var = self._pyomo_var_to_solver_var_map[v_id] + self._solver_model.remove(solver_var) + self._symbol_map.removeSymbol(var) + del self._pyomo_var_to_solver_var_map[v_id] + self._mutable_bounds.pop(v_id, None) + self._needs_updated = True + + def _remove_params(self, params: List[_ParamData]): + pass + + def _update_variables(self, variables: List[_GeneralVarData]): + for var in variables: + var_id = id(var) + if var_id not in self._pyomo_var_to_solver_var_map: + raise ValueError( + 'The Var provided to update_var needs to be added first: {0}'.format( + var + ) + ) + self._mutable_bounds.pop((var_id, 'lb'), None) + self._mutable_bounds.pop((var_id, 'ub'), None) + gurobipy_var = self._pyomo_var_to_solver_var_map[var_id] + lb, ub, vtype = self._process_domain_and_bounds( + var, var_id, None, None, None, gurobipy_var + ) + gurobipy_var.setAttr('lb', lb) + gurobipy_var.setAttr('ub', ub) + gurobipy_var.setAttr('vtype', vtype) + self._needs_updated = True + + def update_params(self): + for con, helpers in self._mutable_helpers.items(): + for helper in helpers: + helper.update() + for k, (v, helper) in self._mutable_bounds.items(): + helper.update() + + for con, helper in self._mutable_quadratic_helpers.items(): + if con in self._constraints_added_since_update: + self._update_gurobi_model() + gurobi_con = helper.con + new_gurobi_expr = helper.get_updated_expression() + new_rhs = helper.get_updated_rhs() + new_sense = gurobi_con.qcsense + pyomo_con = self._solver_con_to_pyomo_con_map[id(gurobi_con)] + name = self._symbol_map.getSymbol(pyomo_con, self._labeler) + self._solver_model.remove(gurobi_con) + new_con = self._solver_model.addQConstr( + new_gurobi_expr, new_sense, new_rhs, name=name + ) + self._pyomo_con_to_solver_con_map[id(pyomo_con)] = new_con + del self._solver_con_to_pyomo_con_map[id(gurobi_con)] + self._solver_con_to_pyomo_con_map[id(new_con)] = pyomo_con + helper.con = new_con + self._constraints_added_since_update.add(con) + + helper = self._mutable_objective + pyomo_obj = self._objective + new_gurobi_expr = helper.get_updated_expression() + if new_gurobi_expr is not None: + if pyomo_obj.sense == minimize: + sense = gurobipy.GRB.MINIMIZE + else: + sense = gurobipy.GRB.MAXIMIZE + self._solver_model.setObjective(new_gurobi_expr, sense=sense) + + def _set_objective(self, obj): + if obj is None: + sense = gurobipy.GRB.MINIMIZE + gurobi_expr = 0 + repn_constant = 0 + mutable_linear_coefficients = list() + mutable_quadratic_coefficients = list() + else: + if obj.sense == minimize: + sense = gurobipy.GRB.MINIMIZE + elif obj.sense == maximize: + sense = gurobipy.GRB.MAXIMIZE + else: + raise ValueError( + 'Objective sense is not recognized: {0}'.format(obj.sense) + ) + + ( + gurobi_expr, + repn_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) = self._get_expr_from_pyomo_expr(obj.expr) + + mutable_constant = _MutableConstant() + mutable_constant.expr = repn_constant + mutable_objective = _MutableObjective( + self._solver_model, + mutable_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) + self._mutable_objective = mutable_objective + + # These two lines are needed as a workaround + # see PR #2454 + self._solver_model.setObjective(0) + self._solver_model.update() + + self._solver_model.setObjective(gurobi_expr + value(repn_constant), sense=sense) + self._needs_updated = True + + def _postsolve(self, timer: HierarchicalTimer): + config = self._config + + gprob = self._solver_model + grb = gurobipy.GRB + status = gprob.Status + + results = Results() + results.solution_loader = GurobiSolutionLoader(self) + results.timing_info.gurobi_time = gprob.Runtime + + if gprob.SolCount > 0: + if status == grb.OPTIMAL: + results.solution_status = SolutionStatus.optimal + else: + results.solution_status = SolutionStatus.feasible + else: + results.solution_status = SolutionStatus.noSolution + + if status == grb.LOADED: # problem is loaded, but no solution + results.termination_condition = TerminationCondition.unknown + elif status == grb.OPTIMAL: # optimal + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + elif status == grb.INFEASIBLE: + results.termination_condition = TerminationCondition.provenInfeasible + elif status == grb.INF_OR_UNBD: + results.termination_condition = TerminationCondition.infeasibleOrUnbounded + elif status == grb.UNBOUNDED: + results.termination_condition = TerminationCondition.unbounded + elif status == grb.CUTOFF: + results.termination_condition = TerminationCondition.objectiveLimit + elif status == grb.ITERATION_LIMIT: + results.termination_condition = TerminationCondition.iterationLimit + elif status == grb.NODE_LIMIT: + results.termination_condition = TerminationCondition.iterationLimit + elif status == grb.TIME_LIMIT: + results.termination_condition = TerminationCondition.maxTimeLimit + elif status == grb.SOLUTION_LIMIT: + results.termination_condition = TerminationCondition.unknown + elif status == grb.INTERRUPTED: + results.termination_condition = TerminationCondition.interrupted + elif status == grb.NUMERIC: + results.termination_condition = TerminationCondition.unknown + elif status == grb.SUBOPTIMAL: + results.termination_condition = TerminationCondition.unknown + elif status == grb.USER_OBJ_LIMIT: + results.termination_condition = TerminationCondition.objectiveLimit + else: + results.termination_condition = TerminationCondition.unknown + + if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied and config.raise_exception_on_nonoptimal_result: + raise RuntimeError( + 'Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' + ) + + results.incumbent_objective = None + results.objective_bound = None + if self._objective is not None: + try: + results.incumbent_objective = gprob.ObjVal + except (gurobipy.GurobiError, AttributeError): + results.incumbent_objective = None + try: + results.objective_bound = gprob.ObjBound + except (gurobipy.GurobiError, AttributeError): + if self._objective.sense == minimize: + results.objective_bound = -math.inf + else: + results.objective_bound = math.inf + + if results.incumbent_objective is not None and not math.isfinite( + results.incumbent_objective + ): + results.incumbent_objective = None + + results.iteration_count = gprob.getAttr('IterCount') + + timer.start('load solution') + if config.load_solutions: + if gprob.SolCount > 0: + self._load_vars() + else: + raise RuntimeError( + 'A feasible solution was not found, so no solution can be loaded.' + 'Please set opt.config.load_solutions=False and check ' + 'results.solution_status and ' + 'results.incumbent_objective before loading a solution.' + ) + timer.stop('load solution') + + return results + + def _load_suboptimal_mip_solution(self, vars_to_load, solution_number): + if ( + self.get_model_attr('NumIntVars') == 0 + and self.get_model_attr('NumBinVars') == 0 + ): + raise ValueError( + 'Cannot obtain suboptimal solutions for a continuous model' + ) + var_map = self._pyomo_var_to_solver_var_map + ref_vars = self._referenced_variables + original_solution_number = self.get_gurobi_param_info('SolutionNumber')[2] + self.set_gurobi_param('SolutionNumber', solution_number) + gurobi_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load] + vals = self._solver_model.getAttr("Xn", gurobi_vars_to_load) + res = ComponentMap() + for var_id, val in zip(vars_to_load, vals): + using_cons, using_sos, using_obj = ref_vars[var_id] + if using_cons or using_sos or (using_obj is not None): + res[self._vars[var_id][0]] = val + self.set_gurobi_param('SolutionNumber', original_solution_number) + return res + + def _load_vars(self, vars_to_load=None, solution_number=0): + for v, val in self._get_primals( + vars_to_load=vars_to_load, solution_number=solution_number + ).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + def _get_primals(self, vars_to_load=None, solution_number=0): + if self._needs_updated: + self._update_gurobi_model() # this is needed to ensure that solutions cannot be loaded after the model has been changed + + if self._solver_model.SolCount == 0: + raise RuntimeError( + 'Solver does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + + var_map = self._pyomo_var_to_solver_var_map + ref_vars = self._referenced_variables + if vars_to_load is None: + vars_to_load = self._pyomo_var_to_solver_var_map.keys() + else: + vars_to_load = [id(v) for v in vars_to_load] + + if solution_number != 0: + return self._load_suboptimal_mip_solution( + vars_to_load=vars_to_load, solution_number=solution_number + ) + else: + gurobi_vars_to_load = [ + var_map[pyomo_var_id] for pyomo_var_id in vars_to_load + ] + vals = self._solver_model.getAttr("X", gurobi_vars_to_load) + + res = ComponentMap() + for var_id, val in zip(vars_to_load, vals): + using_cons, using_sos, using_obj = ref_vars[var_id] + if using_cons or using_sos or (using_obj is not None): + res[self._vars[var_id][0]] = val + return res + + def _get_reduced_costs(self, vars_to_load=None): + if self._needs_updated: + self._update_gurobi_model() + + if self._solver_model.Status != gurobipy.GRB.OPTIMAL: + raise RuntimeError( + 'Solver does not currently have valid reduced costs. Please ' + 'check the termination condition.' + ) + + var_map = self._pyomo_var_to_solver_var_map + ref_vars = self._referenced_variables + res = ComponentMap() + if vars_to_load is None: + vars_to_load = self._pyomo_var_to_solver_var_map.keys() + else: + vars_to_load = [id(v) for v in vars_to_load] + + gurobi_vars_to_load = [var_map[pyomo_var_id] for pyomo_var_id in vars_to_load] + vals = self._solver_model.getAttr("Rc", gurobi_vars_to_load) + + for var_id, val in zip(vars_to_load, vals): + using_cons, using_sos, using_obj = ref_vars[var_id] + if using_cons or using_sos or (using_obj is not None): + res[self._vars[var_id][0]] = val + + return res + + def _get_duals(self, cons_to_load=None): + if self._needs_updated: + self._update_gurobi_model() + + if self._solver_model.Status != gurobipy.GRB.OPTIMAL: + raise RuntimeError( + 'Solver does not currently have valid duals. Please ' + 'check the termination condition.' + ) + + con_map = self._pyomo_con_to_solver_con_map + reverse_con_map = self._solver_con_to_pyomo_con_map + dual = dict() + + if cons_to_load is None: + linear_cons_to_load = self._solver_model.getConstrs() + quadratic_cons_to_load = self._solver_model.getQConstrs() + else: + gurobi_cons_to_load = OrderedSet( + [con_map[pyomo_con] for pyomo_con in cons_to_load] + ) + linear_cons_to_load = list( + gurobi_cons_to_load.intersection( + OrderedSet(self._solver_model.getConstrs()) + ) + ) + quadratic_cons_to_load = list( + gurobi_cons_to_load.intersection( + OrderedSet(self._solver_model.getQConstrs()) + ) + ) + linear_vals = self._solver_model.getAttr("Pi", linear_cons_to_load) + quadratic_vals = self._solver_model.getAttr("QCPi", quadratic_cons_to_load) + + for gurobi_con, val in zip(linear_cons_to_load, linear_vals): + pyomo_con = reverse_con_map[id(gurobi_con)] + dual[pyomo_con] = val + for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals): + pyomo_con = reverse_con_map[id(gurobi_con)] + dual[pyomo_con] = val + + return dual + + def update(self, timer: HierarchicalTimer = None): + if self._needs_updated: + self._update_gurobi_model() + super(Gurobi, self).update(timer=timer) + self._update_gurobi_model() + + def _update_gurobi_model(self): + self._solver_model.update() + self._constraints_added_since_update = OrderedSet() + self._vars_added_since_update = ComponentSet() + self._needs_updated = False + + def get_model_attr(self, attr): + """ + Get the value of an attribute on the Gurobi model. + + Parameters + ---------- + attr: str + The attribute to get. See Gurobi documentation for descriptions of the attributes. + """ + if self._needs_updated: + self._update_gurobi_model() + return self._solver_model.getAttr(attr) + + def write(self, filename): + """ + Write the model to a file (e.g., and lp file). + + Parameters + ---------- + filename: str + Name of the file to which the model should be written. + """ + self._solver_model.write(filename) + self._constraints_added_since_update = OrderedSet() + self._vars_added_since_update = ComponentSet() + self._needs_updated = False + + def set_linear_constraint_attr(self, con, attr, val): + """ + Set the value of an attribute on a gurobi linear constraint. + + Parameters + ---------- + con: pyomo.core.base.constraint._GeneralConstraintData + The pyomo constraint for which the corresponding gurobi constraint attribute + should be modified. + attr: str + The attribute to be modified. Options are: + CBasis + DStart + Lazy + val: any + See gurobi documentation for acceptable values. + """ + if attr in {'Sense', 'RHS', 'ConstrName'}: + raise ValueError( + 'Linear constraint attr {0} cannot be set with' + + ' the set_linear_constraint_attr method. Please use' + + ' the remove_constraint and add_constraint methods.'.format(attr) + ) + self._pyomo_con_to_solver_con_map[con].setAttr(attr, val) + self._needs_updated = True + + def set_var_attr(self, var, attr, val): + """ + Set the value of an attribute on a gurobi variable. + + Parameters + ---------- + var: pyomo.core.base.var._GeneralVarData + The pyomo var for which the corresponding gurobi var attribute + should be modified. + attr: str + The attribute to be modified. Options are: + Start + VarHintVal + VarHintPri + BranchPriority + VBasis + PStart + val: any + See gurobi documentation for acceptable values. + """ + if attr in {'LB', 'UB', 'VType', 'VarName'}: + raise ValueError( + 'Var attr {0} cannot be set with' + + ' the set_var_attr method. Please use' + + ' the update_var method.'.format(attr) + ) + if attr == 'Obj': + raise ValueError( + 'Var attr Obj cannot be set with' + + ' the set_var_attr method. Please use' + + ' the set_objective method.' + ) + self._pyomo_var_to_solver_var_map[id(var)].setAttr(attr, val) + self._needs_updated = True + + def get_var_attr(self, var, attr): + """ + Get the value of an attribute on a gurobi var. + + Parameters + ---------- + var: pyomo.core.base.var._GeneralVarData + The pyomo var for which the corresponding gurobi var attribute + should be retrieved. + attr: str + The attribute to get. See gurobi documentation + """ + if self._needs_updated: + self._update_gurobi_model() + return self._pyomo_var_to_solver_var_map[id(var)].getAttr(attr) + + def get_linear_constraint_attr(self, con, attr): + """ + Get the value of an attribute on a gurobi linear constraint. + + Parameters + ---------- + con: pyomo.core.base.constraint._GeneralConstraintData + The pyomo constraint for which the corresponding gurobi constraint attribute + should be retrieved. + attr: str + The attribute to get. See the Gurobi documentation + """ + if self._needs_updated: + self._update_gurobi_model() + return self._pyomo_con_to_solver_con_map[con].getAttr(attr) + + def get_sos_attr(self, con, attr): + """ + Get the value of an attribute on a gurobi sos constraint. + + Parameters + ---------- + con: pyomo.core.base.sos._SOSConstraintData + The pyomo SOS constraint for which the corresponding gurobi SOS constraint attribute + should be retrieved. + attr: str + The attribute to get. See the Gurobi documentation + """ + if self._needs_updated: + self._update_gurobi_model() + return self._pyomo_sos_to_solver_sos_map[con].getAttr(attr) + + def get_quadratic_constraint_attr(self, con, attr): + """ + Get the value of an attribute on a gurobi quadratic constraint. + + Parameters + ---------- + con: pyomo.core.base.constraint._GeneralConstraintData + The pyomo constraint for which the corresponding gurobi constraint attribute + should be retrieved. + attr: str + The attribute to get. See the Gurobi documentation + """ + if self._needs_updated: + self._update_gurobi_model() + return self._pyomo_con_to_solver_con_map[con].getAttr(attr) + + def set_gurobi_param(self, param, val): + """ + Set a gurobi parameter. + + Parameters + ---------- + param: str + The gurobi parameter to set. Options include any gurobi parameter. + Please see the Gurobi documentation for options. + val: any + The value to set the parameter to. See Gurobi documentation for possible values. + """ + self._solver_model.setParam(param, val) + + def get_gurobi_param_info(self, param): + """ + Get information about a gurobi parameter. + + Parameters + ---------- + param: str + The gurobi parameter to get info for. See Gurobi documentation for possible options. + + Returns + ------- + six-tuple containing the parameter name, type, value, minimum value, maximum value, and default value. + """ + return self._solver_model.getParamInfo(param) + + def _intermediate_callback(self): + def f(gurobi_model, where): + self._callback_func(self._model, self, where) + + return f + + def set_callback(self, func=None): + """ + Specify a callback for gurobi to use. + + Parameters + ---------- + func: function + The function to call. The function should have three arguments. The first will be the pyomo model being + solved. The second will be the GurobiPersistent instance. The third will be an enum member of + gurobipy.GRB.Callback. This will indicate where in the branch and bound algorithm gurobi is at. For + example, suppose we want to solve + + .. math:: + + min 2*x + y + + s.t. + + y >= (x-2)**2 + + 0 <= x <= 4 + + y >= 0 + + y integer + + as an MILP using extended cutting planes in callbacks. + + >>> from gurobipy import GRB # doctest:+SKIP + >>> import pyomo.environ as pe + >>> from pyomo.core.expr.taylor_series import taylor_series_expansion + >>> from pyomo.contrib import appsi + >>> + >>> m = pe.ConcreteModel() + >>> m.x = pe.Var(bounds=(0, 4)) + >>> m.y = pe.Var(within=pe.Integers, bounds=(0, None)) + >>> m.obj = pe.Objective(expr=2*m.x + m.y) + >>> m.cons = pe.ConstraintList() # for the cutting planes + >>> + >>> def _add_cut(xval): + ... # a function to generate the cut + ... m.x.value = xval + ... return m.cons.add(m.y >= taylor_series_expansion((m.x - 2)**2)) + ... + >>> _c = _add_cut(0) # start with 2 cuts at the bounds of x + >>> _c = _add_cut(4) # this is an arbitrary choice + >>> + >>> opt = appsi.solvers.Gurobi() + >>> opt.config.stream_solver = True + >>> opt.set_instance(m) # doctest:+SKIP + >>> opt.gurobi_options['PreCrush'] = 1 + >>> opt.gurobi_options['LazyConstraints'] = 1 + >>> + >>> def my_callback(cb_m, cb_opt, cb_where): + ... if cb_where == GRB.Callback.MIPSOL: + ... cb_opt.cbGetSolution(vars=[m.x, m.y]) + ... if m.y.value < (m.x.value - 2)**2 - 1e-6: + ... cb_opt.cbLazy(_add_cut(m.x.value)) + ... + >>> opt.set_callback(my_callback) + >>> res = opt.solve(m) # doctest:+SKIP + + """ + if func is not None: + self._callback_func = func + self._callback = self._intermediate_callback() + else: + self._callback = None + self._callback_func = None + + def cbCut(self, con): + """ + Add a cut within a callback. + + Parameters + ---------- + con: pyomo.core.base.constraint._GeneralConstraintData + The cut to add + """ + if not con.active: + raise ValueError('cbCut expected an active constraint.') + + if is_fixed(con.body): + raise ValueError('cbCut expected a non-trivial constraint') + + ( + gurobi_expr, + repn_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) = self._get_expr_from_pyomo_expr(con.body) + + if con.has_lb(): + if con.has_ub(): + raise ValueError('Range constraints are not supported in cbCut.') + if not is_fixed(con.lower): + raise ValueError( + 'Lower bound of constraint {0} is not constant.'.format(con) + ) + if con.has_ub(): + if not is_fixed(con.upper): + raise ValueError( + 'Upper bound of constraint {0} is not constant.'.format(con) + ) + + if con.equality: + self._solver_model.cbCut( + lhs=gurobi_expr, + sense=gurobipy.GRB.EQUAL, + rhs=value(con.lower - repn_constant), + ) + elif con.has_lb() and (value(con.lower) > -float('inf')): + self._solver_model.cbCut( + lhs=gurobi_expr, + sense=gurobipy.GRB.GREATER_EQUAL, + rhs=value(con.lower - repn_constant), + ) + elif con.has_ub() and (value(con.upper) < float('inf')): + self._solver_model.cbCut( + lhs=gurobi_expr, + sense=gurobipy.GRB.LESS_EQUAL, + rhs=value(con.upper - repn_constant), + ) + else: + raise ValueError( + 'Constraint does not have a lower or an upper bound {0} \n'.format(con) + ) + + def cbGet(self, what): + return self._solver_model.cbGet(what) + + def cbGetNodeRel(self, vars): + """ + Parameters + ---------- + vars: Var or iterable of Var + """ + if not isinstance(vars, Iterable): + vars = [vars] + gurobi_vars = [self._pyomo_var_to_solver_var_map[id(i)] for i in vars] + var_values = self._solver_model.cbGetNodeRel(gurobi_vars) + for i, v in enumerate(vars): + v.set_value(var_values[i], skip_validation=True) + + def cbGetSolution(self, vars): + """ + Parameters + ---------- + vars: iterable of vars + """ + if not isinstance(vars, Iterable): + vars = [vars] + gurobi_vars = [self._pyomo_var_to_solver_var_map[id(i)] for i in vars] + var_values = self._solver_model.cbGetSolution(gurobi_vars) + for i, v in enumerate(vars): + v.set_value(var_values[i], skip_validation=True) + + def cbLazy(self, con): + """ + Parameters + ---------- + con: pyomo.core.base.constraint._GeneralConstraintData + The lazy constraint to add + """ + if not con.active: + raise ValueError('cbLazy expected an active constraint.') + + if is_fixed(con.body): + raise ValueError('cbLazy expected a non-trivial constraint') + + ( + gurobi_expr, + repn_constant, + mutable_linear_coefficients, + mutable_quadratic_coefficients, + ) = self._get_expr_from_pyomo_expr(con.body) + + if con.has_lb(): + if con.has_ub(): + raise ValueError('Range constraints are not supported in cbLazy.') + if not is_fixed(con.lower): + raise ValueError( + 'Lower bound of constraint {0} is not constant.'.format(con) + ) + if con.has_ub(): + if not is_fixed(con.upper): + raise ValueError( + 'Upper bound of constraint {0} is not constant.'.format(con) + ) + + if con.equality: + self._solver_model.cbLazy( + lhs=gurobi_expr, + sense=gurobipy.GRB.EQUAL, + rhs=value(con.lower - repn_constant), + ) + elif con.has_lb() and (value(con.lower) > -float('inf')): + self._solver_model.cbLazy( + lhs=gurobi_expr, + sense=gurobipy.GRB.GREATER_EQUAL, + rhs=value(con.lower - repn_constant), + ) + elif con.has_ub() and (value(con.upper) < float('inf')): + self._solver_model.cbLazy( + lhs=gurobi_expr, + sense=gurobipy.GRB.LESS_EQUAL, + rhs=value(con.upper - repn_constant), + ) + else: + raise ValueError( + 'Constraint does not have a lower or an upper bound {0} \n'.format(con) + ) + + def cbSetSolution(self, vars, solution): + if not isinstance(vars, Iterable): + vars = [vars] + gurobi_vars = [self._pyomo_var_to_solver_var_map[id(i)] for i in vars] + self._solver_model.cbSetSolution(gurobi_vars, solution) + + def cbUseSolution(self): + return self._solver_model.cbUseSolution() + + def reset(self): + self._solver_model.reset() diff --git a/pyomo/contrib/solver/plugins.py b/pyomo/contrib/solver/plugins.py index 54d03eaf74b..e66818482b4 100644 --- a/pyomo/contrib/solver/plugins.py +++ b/pyomo/contrib/solver/plugins.py @@ -12,9 +12,11 @@ from .factory import SolverFactory from .ipopt import ipopt +from .gurobi import Gurobi def load(): SolverFactory.register(name='ipopt_v2', doc='The IPOPT NLP solver (new interface)')( ipopt ) + SolverFactory.register(name='gurobi_v2', doc='New interface to Gurobi')(Gurobi) diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index 68654a4e9d7..a2e4d90b898 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -122,7 +122,7 @@ def parse_sol_file( # TODO: this is solver dependent # But this was the way in the previous version - and has been fine thus far? result.solution_status = SolutionStatus.infeasible - result.termination_condition = TerminationCondition.iterationLimit + result.termination_condition = TerminationCondition.iterationLimit # this is not always correct elif (exit_code[1] >= 500) and (exit_code[1] <= 599): exit_code_message = ( "FAILURE: the solver stopped by an error condition " @@ -205,4 +205,4 @@ def parse_sol_file( ) line = sol_file.readline() - return result, sol_data + return result, sol_data diff --git a/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py new file mode 100644 index 00000000000..f53088506f9 --- /dev/null +++ b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py @@ -0,0 +1,691 @@ +from pyomo.common.errors import PyomoException +import pyomo.common.unittest as unittest +import pyomo.environ as pe +from pyomo.contrib.solver.gurobi import Gurobi +from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus +from pyomo.core.expr.numeric_expr import LinearExpression +from pyomo.core.expr.taylor_series import taylor_series_expansion + + +opt = Gurobi() +if not opt.available(): + raise unittest.SkipTest +import gurobipy + + +def create_pmedian_model(): + d_dict = { + (1, 1): 1.777356642700564, + (1, 2): 1.6698255595592497, + (1, 3): 1.099139603924817, + (1, 4): 1.3529705111901453, + (1, 5): 1.467907742900842, + (1, 6): 1.5346837414708774, + (2, 1): 1.9783090609123972, + (2, 2): 1.130315350158659, + (2, 3): 1.6712434682302661, + (2, 4): 1.3642294159473756, + (2, 5): 1.4888357071619858, + (2, 6): 1.2030122107340537, + (3, 1): 1.6661983755713592, + (3, 2): 1.227663031206932, + (3, 3): 1.4580640582967632, + (3, 4): 1.0407223975549575, + (3, 5): 1.9742897953778287, + (3, 6): 1.4874760742689066, + (4, 1): 1.4616138636373597, + (4, 2): 1.7141471558082002, + (4, 3): 1.4157281494999725, + (4, 4): 1.888011688001529, + (4, 5): 1.0232934487237717, + (4, 6): 1.8335062677845464, + (5, 1): 1.468494740997508, + (5, 2): 1.8114798126442795, + (5, 3): 1.9455914886158723, + (5, 4): 1.983088378194899, + (5, 5): 1.1761820755785306, + (5, 6): 1.698655759576308, + (6, 1): 1.108855711312383, + (6, 2): 1.1602637342062019, + (6, 3): 1.0928602740245892, + (6, 4): 1.3140620798928404, + (6, 5): 1.0165386843386672, + (6, 6): 1.854049125736362, + (7, 1): 1.2910160386456968, + (7, 2): 1.7800475863350327, + (7, 3): 1.5480965161255695, + (7, 4): 1.1943306766997612, + (7, 5): 1.2920382721805297, + (7, 6): 1.3194527773994338, + (8, 1): 1.6585982235379078, + (8, 2): 1.2315210354122292, + (8, 3): 1.6194303369953538, + (8, 4): 1.8953386098022103, + (8, 5): 1.8694342085696831, + (8, 6): 1.2938069356684523, + (9, 1): 1.4582048085805495, + (9, 2): 1.484979797871119, + (9, 3): 1.2803882693587225, + (9, 4): 1.3289569463506004, + (9, 5): 1.9842424240265042, + (9, 6): 1.0119441379208745, + (10, 1): 1.1429007682932852, + (10, 2): 1.6519772165446711, + (10, 3): 1.0749931799469326, + (10, 4): 1.2920787022811089, + (10, 5): 1.7934429721917704, + (10, 6): 1.9115931008709737, + } + + model = pe.ConcreteModel() + model.N = pe.Param(initialize=10) + model.Locations = pe.RangeSet(1, model.N) + model.P = pe.Param(initialize=3) + model.M = pe.Param(initialize=6) + model.Customers = pe.RangeSet(1, model.M) + model.d = pe.Param( + model.Locations, model.Customers, initialize=d_dict, within=pe.Reals + ) + model.x = pe.Var(model.Locations, model.Customers, bounds=(0.0, 1.0)) + model.y = pe.Var(model.Locations, within=pe.Binary) + + def rule(model): + return sum( + model.d[n, m] * model.x[n, m] + for n in model.Locations + for m in model.Customers + ) + + model.obj = pe.Objective(rule=rule) + + def rule(model, m): + return (sum(model.x[n, m] for n in model.Locations), 1.0) + + model.single_x = pe.Constraint(model.Customers, rule=rule) + + def rule(model, n, m): + return (None, model.x[n, m] - model.y[n], 0.0) + + model.bound_y = pe.Constraint(model.Locations, model.Customers, rule=rule) + + def rule(model): + return (sum(model.y[n] for n in model.Locations) - model.P, 0.0) + + model.num_facilities = pe.Constraint(rule=rule) + + return model + + +class TestGurobiPersistentSimpleLPUpdates(unittest.TestCase): + def setUp(self): + self.m = pe.ConcreteModel() + m = self.m + m.x = pe.Var() + m.y = pe.Var() + m.p1 = pe.Param(mutable=True) + m.p2 = pe.Param(mutable=True) + m.p3 = pe.Param(mutable=True) + m.p4 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.x + m.y) + m.c1 = pe.Constraint(expr=m.y - m.p1 * m.x >= m.p2) + m.c2 = pe.Constraint(expr=m.y - m.p3 * m.x >= m.p4) + + def get_solution(self): + try: + import numpy as np + except: + raise unittest.SkipTest('numpy is not available') + p1 = self.m.p1.value + p2 = self.m.p2.value + p3 = self.m.p3.value + p4 = self.m.p4.value + A = np.array([[1, -p1], [1, -p3]]) + rhs = np.array([p2, p4]) + sol = np.linalg.solve(A, rhs) + x = float(sol[1]) + y = float(sol[0]) + return x, y + + def set_params(self, p1, p2, p3, p4): + self.m.p1.value = p1 + self.m.p2.value = p2 + self.m.p3.value = p3 + self.m.p4.value = p4 + + def test_lp(self): + self.set_params(-1, -2, 0.1, -2) + x, y = self.get_solution() + opt = Gurobi() + res = opt.solve(self.m) + self.assertAlmostEqual(x + y, res.incumbent_objective) + self.assertAlmostEqual(x + y, res.objective_bound) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertTrue(res.incumbent_objective is not None) + self.assertAlmostEqual(x, self.m.x.value) + self.assertAlmostEqual(y, self.m.y.value) + + self.set_params(-1.25, -1, 0.5, -2) + opt.config.load_solutions = False + res = opt.solve(self.m) + self.assertAlmostEqual(x, self.m.x.value) + self.assertAlmostEqual(y, self.m.y.value) + x, y = self.get_solution() + self.assertNotAlmostEqual(x, self.m.x.value) + self.assertNotAlmostEqual(y, self.m.y.value) + res.solution_loader.load_vars() + self.assertAlmostEqual(x, self.m.x.value) + self.assertAlmostEqual(y, self.m.y.value) + + +class TestGurobiPersistent(unittest.TestCase): + def test_nonconvex_qcp_objective_bound_1(self): + # the goal of this test is to ensure we can get an objective bound + # for nonconvex but continuous problems even if a feasible solution + # is not found + # + # This is a fragile test because it could fail if Gurobi's algorithms improve + # (e.g., a heuristic solution is found before an objective bound of -8 is reached + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-5, 5)) + m.y = pe.Var(bounds=(-5, 5)) + m.obj = pe.Objective(expr=-m.x**2 - m.y) + m.c1 = pe.Constraint(expr=m.y <= -2 * m.x + 1) + m.c2 = pe.Constraint(expr=m.y <= m.x - 2) + opt = Gurobi() + opt.config.solver_options['nonconvex'] = 2 + opt.config.solver_options['BestBdStop'] = -8 + opt.config.load_solutions = False + opt.config.raise_exception_on_nonoptimal_result = False + res = opt.solve(m) + self.assertEqual(res.incumbent_objective, None) + self.assertAlmostEqual(res.objective_bound, -8) + + def test_nonconvex_qcp_objective_bound_2(self): + # the goal of this test is to ensure we can objective_bound properly + # for nonconvex but continuous problems when the solver terminates with a nonzero gap + # + # This is a fragile test because it could fail if Gurobi's algorithms change + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-5, 5)) + m.y = pe.Var(bounds=(-5, 5)) + m.obj = pe.Objective(expr=-m.x**2 - m.y) + m.c1 = pe.Constraint(expr=m.y <= -2 * m.x + 1) + m.c2 = pe.Constraint(expr=m.y <= m.x - 2) + opt = Gurobi() + opt.config.solver_options['nonconvex'] = 2 + opt.config.solver_options['MIPGap'] = 0.5 + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -4) + self.assertAlmostEqual(res.objective_bound, -6) + + def test_range_constraints(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.xl = pe.Param(initialize=-1, mutable=True) + m.xu = pe.Param(initialize=1, mutable=True) + m.c = pe.Constraint(expr=pe.inequality(m.xl, m.x, m.xu)) + m.obj = pe.Objective(expr=m.x) + + opt = Gurobi() + opt.set_instance(m) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -1) + + m.xl.value = -3 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -3) + + del m.obj + m.obj = pe.Objective(expr=m.x, sense=pe.maximize) + + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 1) + + m.xu.value = 3 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 3) + + def test_quadratic_constraint_with_params(self): + m = pe.ConcreteModel() + m.a = pe.Param(initialize=1, mutable=True) + m.b = pe.Param(initialize=1, mutable=True) + m.c = pe.Param(initialize=1, mutable=True) + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.con = pe.Constraint(expr=m.y >= m.a * m.x**2 + m.b * m.x + m.c) + + opt = Gurobi() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) + self.assertAlmostEqual( + m.y.value, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value + ) + + m.a.value = 2 + m.b.value = 4 + m.c.value = -1 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) + self.assertAlmostEqual( + m.y.value, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value + ) + + def test_quadratic_objective(self): + m = pe.ConcreteModel() + m.a = pe.Param(initialize=1, mutable=True) + m.b = pe.Param(initialize=1, mutable=True) + m.c = pe.Param(initialize=1, mutable=True) + m.x = pe.Var() + m.obj = pe.Objective(expr=m.a * m.x**2 + m.b * m.x + m.c) + + opt = Gurobi() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) + self.assertAlmostEqual( + res.incumbent_objective, + m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value, + ) + + m.a.value = 2 + m.b.value = 4 + m.c.value = -1 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) + self.assertAlmostEqual( + res.incumbent_objective, + m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value, + ) + + def test_var_bounds(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.obj = pe.Objective(expr=m.x) + + opt = Gurobi() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -1) + + m.x.setlb(-3) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -3) + + del m.obj + m.obj = pe.Objective(expr=m.x, sense=pe.maximize) + + opt = Gurobi() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 1) + + m.x.setub(3) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 3) + + def test_fixed_var(self): + m = pe.ConcreteModel() + m.a = pe.Param(initialize=1, mutable=True) + m.b = pe.Param(initialize=1, mutable=True) + m.c = pe.Param(initialize=1, mutable=True) + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.con = pe.Constraint(expr=m.y >= m.a * m.x**2 + m.b * m.x + m.c) + + m.x.fix(1) + opt = Gurobi() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 3) + + m.x.value = 2 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2) + self.assertAlmostEqual(m.y.value, 7) + + m.x.unfix() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -m.b.value / (2 * m.a.value)) + self.assertAlmostEqual( + m.y.value, m.a.value * m.x.value**2 + m.b.value * m.x.value + m.c.value + ) + + def test_linear_constraint_attr(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.c = pe.Constraint(expr=m.x + m.y == 1) + + opt = Gurobi() + opt.set_instance(m) + opt.set_linear_constraint_attr(m.c, 'Lazy', 1) + self.assertEqual(opt.get_linear_constraint_attr(m.c, 'Lazy'), 1) + + def test_quadratic_constraint_attr(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.c = pe.Constraint(expr=m.y >= m.x**2) + + opt = Gurobi() + opt.set_instance(m) + self.assertEqual(opt.get_quadratic_constraint_attr(m.c, 'QCRHS'), 0) + + def test_var_attr(self): + m = pe.ConcreteModel() + m.x = pe.Var(within=pe.Binary) + m.obj = pe.Objective(expr=m.x) + + opt = Gurobi() + opt.set_instance(m) + opt.set_var_attr(m.x, 'Start', 1) + self.assertEqual(opt.get_var_attr(m.x, 'Start'), 1) + + def test_callback(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(0, 4)) + m.y = pe.Var(within=pe.Integers, bounds=(0, None)) + m.obj = pe.Objective(expr=2 * m.x + m.y) + m.cons = pe.ConstraintList() + + def _add_cut(xval): + m.x.value = xval + return m.cons.add(m.y >= taylor_series_expansion((m.x - 2) ** 2)) + + _add_cut(0) + _add_cut(4) + + opt = Gurobi() + opt.set_instance(m) + opt.set_gurobi_param('PreCrush', 1) + opt.set_gurobi_param('LazyConstraints', 1) + + def _my_callback(cb_m, cb_opt, cb_where): + if cb_where == gurobipy.GRB.Callback.MIPSOL: + cb_opt.cbGetSolution(vars=[m.x, m.y]) + if m.y.value < (m.x.value - 2) ** 2 - 1e-6: + cb_opt.cbLazy(_add_cut(m.x.value)) + + opt.set_callback(_my_callback) + opt.solve(m) + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 1) + + def test_nonconvex(self): + if gurobipy.GRB.VERSION_MAJOR < 9: + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c = pe.Constraint(expr=m.y == (m.x - 1) ** 2 - 2) + opt = Gurobi() + opt.config.solver_options['nonconvex'] = 2 + opt.solve(m) + self.assertAlmostEqual(m.x.value, -0.3660254037844423, 2) + self.assertAlmostEqual(m.y.value, -0.13397459621555508, 2) + + def test_nonconvex2(self): + if gurobipy.GRB.VERSION_MAJOR < 9: + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=0 <= -m.y + (m.x - 1) ** 2 - 2) + m.c2 = pe.Constraint(expr=0 >= -m.y + (m.x - 1) ** 2 - 2) + opt = Gurobi() + opt.config.solver_options['nonconvex'] = 2 + opt.solve(m) + self.assertAlmostEqual(m.x.value, -0.3660254037844423, 2) + self.assertAlmostEqual(m.y.value, -0.13397459621555508, 2) + + def test_solution_number(self): + m = create_pmedian_model() + opt = Gurobi() + opt.config.solver_options['PoolSolutions'] = 3 + opt.config.solver_options['PoolSearchMode'] = 2 + res = opt.solve(m) + num_solutions = opt.get_model_attr('SolCount') + self.assertEqual(num_solutions, 3) + res.solution_loader.load_vars(solution_number=0) + self.assertAlmostEqual(pe.value(m.obj.expr), 6.431184939357673) + res.solution_loader.load_vars(solution_number=1) + self.assertAlmostEqual(pe.value(m.obj.expr), 6.584793218502477) + res.solution_loader.load_vars(solution_number=2) + self.assertAlmostEqual(pe.value(m.obj.expr), 6.592304628123309) + + def test_zero_time_limit(self): + m = create_pmedian_model() + opt = Gurobi() + opt.config.time_limit = 0 + opt.config.load_solutions = False + opt.config.raise_exception_on_nonoptimal_result = False + res = opt.solve(m) + num_solutions = opt.get_model_attr('SolCount') + + # Behavior is different on different platforms, so + # we have to see if there are any solutions + # This means that there is no guarantee we are testing + # what we are trying to test. Unfortunately, I'm + # not sure of a good way to guarantee that + if num_solutions == 0: + self.assertIsNone(res.incumbent_objective) + + +class TestManualModel(unittest.TestCase): + def setUp(self): + opt = Gurobi() + opt.config.auto_updates.check_for_new_or_removed_params = False + opt.config.auto_updates.check_for_new_or_removed_vars = False + opt.config.auto_updates.check_for_new_or_removed_constraints = False + opt.config.auto_updates.update_params = False + opt.config.auto_updates.update_vars = False + opt.config.auto_updates.update_constraints = False + opt.config.auto_updates.update_named_expressions = False + self.opt = opt + + def test_basics(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-10, 10)) + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=m.y >= 2 * m.x + 1) + + opt = self.opt + opt.set_instance(m) + + self.assertEqual(opt.get_model_attr('NumVars'), 2) + self.assertEqual(opt.get_model_attr('NumConstrs'), 1) + self.assertEqual(opt.get_model_attr('NumQConstrs'), 0) + self.assertEqual(opt.get_var_attr(m.x, 'LB'), -10) + self.assertEqual(opt.get_var_attr(m.x, 'UB'), 10) + + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -0.4) + self.assertAlmostEqual(m.y.value, 0.2) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -0.4) + + m.c2 = pe.Constraint(expr=m.y >= -m.x + 1) + opt.add_constraints([m.c2]) + self.assertEqual(opt.get_model_attr('NumVars'), 2) + self.assertEqual(opt.get_model_attr('NumConstrs'), 2) + self.assertEqual(opt.get_model_attr('NumQConstrs'), 0) + + opt.config.load_solutions = False + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -0.4) + self.assertAlmostEqual(m.y.value, 0.2) + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + + opt.remove_constraints([m.c2]) + m.del_component(m.c2) + self.assertEqual(opt.get_model_attr('NumVars'), 2) + self.assertEqual(opt.get_model_attr('NumConstrs'), 1) + self.assertEqual(opt.get_model_attr('NumQConstrs'), 0) + + self.assertEqual(opt.get_gurobi_param_info('FeasibilityTol')[2], 1e-6) + opt.config.solver_options['FeasibilityTol'] = 1e-7 + opt.config.load_solutions = True + res = opt.solve(m) + self.assertEqual(opt.get_gurobi_param_info('FeasibilityTol')[2], 1e-7) + self.assertAlmostEqual(m.x.value, -0.4) + self.assertAlmostEqual(m.y.value, 0.2) + + m.x.setlb(-5) + m.x.setub(5) + opt.update_variables([m.x]) + self.assertEqual(opt.get_var_attr(m.x, 'LB'), -5) + self.assertEqual(opt.get_var_attr(m.x, 'UB'), 5) + + m.x.fix(0) + opt.update_variables([m.x]) + self.assertEqual(opt.get_var_attr(m.x, 'LB'), 0) + self.assertEqual(opt.get_var_attr(m.x, 'UB'), 0) + + m.x.unfix() + opt.update_variables([m.x]) + self.assertEqual(opt.get_var_attr(m.x, 'LB'), -5) + self.assertEqual(opt.get_var_attr(m.x, 'UB'), 5) + + m.c2 = pe.Constraint(expr=m.y >= m.x**2) + opt.add_constraints([m.c2]) + self.assertEqual(opt.get_model_attr('NumVars'), 2) + self.assertEqual(opt.get_model_attr('NumConstrs'), 1) + self.assertEqual(opt.get_model_attr('NumQConstrs'), 1) + + opt.remove_constraints([m.c2]) + m.del_component(m.c2) + self.assertEqual(opt.get_model_attr('NumVars'), 2) + self.assertEqual(opt.get_model_attr('NumConstrs'), 1) + self.assertEqual(opt.get_model_attr('NumQConstrs'), 0) + + m.z = pe.Var() + opt.add_variables([m.z]) + self.assertEqual(opt.get_model_attr('NumVars'), 3) + opt.remove_variables([m.z]) + del m.z + self.assertEqual(opt.get_model_attr('NumVars'), 2) + + def test_update1(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.z) + m.c1 = pe.Constraint(expr=m.z >= m.x**2 + m.y**2) + + opt = self.opt + opt.set_instance(m) + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 1) + + opt.remove_constraints([m.c1]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 0) + + opt.add_constraints([m.c1]) + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 0) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 1) + + def test_update2(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.z) + m.c2 = pe.Constraint(expr=m.x + m.y == 1) + + opt = self.opt + opt.config.symbolic_solver_labels = True + opt.set_instance(m) + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 1) + + opt.remove_constraints([m.c2]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 0) + + opt.add_constraints([m.c2]) + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 0) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 1) + + def test_update3(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.z) + m.c1 = pe.Constraint(expr=m.z >= m.x**2 + m.y**2) + + opt = self.opt + opt.set_instance(m) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 1) + m.c2 = pe.Constraint(expr=m.y >= m.x**2) + opt.add_constraints([m.c2]) + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 1) + opt.remove_constraints([m.c2]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumQConstrs'), 1) + + def test_update4(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.z) + m.c1 = pe.Constraint(expr=m.z >= m.x + m.y) + + opt = self.opt + opt.set_instance(m) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 1) + m.c2 = pe.Constraint(expr=m.y >= m.x) + opt.add_constraints([m.c2]) + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 1) + opt.remove_constraints([m.c2]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumConstrs'), 1) + + def test_update5(self): + m = pe.ConcreteModel() + m.a = pe.Set(initialize=[1, 2, 3], ordered=True) + m.x = pe.Var(m.a, within=pe.Binary) + m.y = pe.Var(within=pe.Binary) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.SOSConstraint(var=m.x, sos=1) + + opt = self.opt + opt.set_instance(m) + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 1) + + opt.remove_sos_constraints([m.c1]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 0) + + opt.add_sos_constraints([m.c1]) + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 0) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 1) + + def test_update6(self): + m = pe.ConcreteModel() + m.a = pe.Set(initialize=[1, 2, 3], ordered=True) + m.x = pe.Var(m.a, within=pe.Binary) + m.y = pe.Var(within=pe.Binary) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.SOSConstraint(var=m.x, sos=1) + + opt = self.opt + opt.set_instance(m) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 1) + m.c2 = pe.SOSConstraint(var=m.x, sos=2) + opt.add_sos_constraints([m.c2]) + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 1) + opt.remove_sos_constraints([m.c2]) + opt.update() + self.assertEqual(opt._solver_model.getAttr('NumSOS'), 1) diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py new file mode 100644 index 00000000000..0499f1abb5d --- /dev/null +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -0,0 +1,1350 @@ +import pyomo.environ as pe +from pyomo.common.dependencies import attempt_import +import pyomo.common.unittest as unittest + +parameterized, param_available = attempt_import('parameterized') +parameterized = parameterized.parameterized +from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus, Results +from pyomo.contrib.solver.base import SolverBase +from pyomo.contrib.solver.ipopt import ipopt +from pyomo.contrib.solver.gurobi import Gurobi +from typing import Type +from pyomo.core.expr.numeric_expr import LinearExpression +import os +import math + +numpy, numpy_available = attempt_import('numpy') +import random +from pyomo import gdp + + +if not param_available: + raise unittest.SkipTest('Parameterized is not available.') + +all_solvers = [ + ('gurobi', Gurobi), + ('ipopt', ipopt), +] +mip_solvers = [('gurobi', Gurobi)] +nlp_solvers = [('ipopt', ipopt)] +qcp_solvers = [('gurobi', Gurobi), ('ipopt', ipopt)] +miqcqp_solvers = [('gurobi', Gurobi)] + + +def _load_tests(solver_list): + res = list() + for solver_name, solver in solver_list: + test_name = f"{solver_name}" + res.append((test_name, solver)) + return res + + +@unittest.skipUnless(numpy_available, 'numpy is not available') +class TestSolvers(unittest.TestCase): + @parameterized.expand(input=_load_tests(all_solvers)) + def test_remove_variable_and_objective( + self, name: str, opt_class: Type[SolverBase], + ): + # this test is for issue #2888 + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(2, None)) + m.obj = pe.Objective(expr=m.x) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, 2) + + del m.x + del m.obj + m.x = pe.Var(bounds=(2, None)) + m.obj = pe.Objective(expr=m.x) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, 2) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_stale_vars( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= m.x) + m.c2 = pe.Constraint(expr=m.y >= -m.x) + m.x.value = 1 + m.y.value = 1 + m.z.value = 1 + self.assertFalse(m.x.stale) + self.assertFalse(m.y.stale) + self.assertFalse(m.z.stale) + + res = opt.solve(m) + self.assertFalse(m.x.stale) + self.assertFalse(m.y.stale) + self.assertTrue(m.z.stale) + + opt.config.load_solutions = False + res = opt.solve(m) + self.assertTrue(m.x.stale) + self.assertTrue(m.y.stale) + self.assertTrue(m.z.stale) + res.solution_loader.load_vars() + self.assertFalse(m.x.stale) + self.assertFalse(m.y.stale) + self.assertTrue(m.z.stale) + + res = opt.solve(m) + self.assertTrue(m.x.stale) + self.assertTrue(m.y.stale) + self.assertTrue(m.z.stale) + res.solution_loader.load_vars([m.y]) + self.assertFalse(m.y.stale) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_range_constraint( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.obj = pe.Objective(expr=m.x) + m.c = pe.Constraint(expr=(-1, m.x, 1)) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, -1) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c], 1) + m.obj.sense = pe.maximize + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, 1) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c], 1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_reduced_costs( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var(bounds=(-2, 2)) + m.obj = pe.Objective(expr=3 * m.x + 4 * m.y) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, -1) + self.assertAlmostEqual(m.y.value, -2) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 3) + self.assertAlmostEqual(rc[m.y], 4) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_reduced_costs2( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.obj = pe.Objective(expr=m.x) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, -1) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) + m.obj.sense = pe.maximize + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, 1) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_param_changes( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(mutable=True) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) + m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) + + params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] + for a1, a2, b1, b2 in params_to_test: + m.a1.value = a1 + m.a2.value = a2 + m.b1.value = b1 + m.b2.value = b2 + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + if res.objective_bound is None: + bound = -math.inf + else: + bound = res.objective_bound + self.assertTrue(bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_immutable_param( + self, name: str, opt_class: Type[SolverBase], + ): + """ + This test is important because component_data_objects returns immutable params as floats. + We want to make sure we process these correctly. + """ + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(initialize=-1) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) + m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) + + params_to_test = [(1, 2, 1), (1, 2, 1), (1, 3, 1)] + for a1, b1, b2 in params_to_test: + a2 = m.a2.value + m.a1.value = a1 + m.b1.value = b1 + m.b2.value = b2 + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + if res.objective_bound is None: + bound = -math.inf + else: + bound = res.objective_bound + self.assertTrue(bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_equality( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + if isinstance(opt, ipopt): + opt.config.writer_config.linear_presolve = False + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(mutable=True) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y == m.a1 * m.x + m.b1) + m.c2 = pe.Constraint(expr=m.y == m.a2 * m.x + m.b2) + + params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] + for a1, a2, b1, b2 in params_to_test: + m.a1.value = a1 + m.a2.value = a2 + m.b1.value = b1 + m.b2.value = b2 + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + if res.objective_bound is None: + bound = -math.inf + else: + bound = res.objective_bound + self.assertTrue(bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_linear_expression( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(mutable=True) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.y) + e = LinearExpression( + constant=m.b1, linear_coefs=[-1, m.a1], linear_vars=[m.y, m.x] + ) + m.c1 = pe.Constraint(expr=e == 0) + e = LinearExpression( + constant=m.b2, linear_coefs=[-1, m.a2], linear_vars=[m.y, m.x] + ) + m.c2 = pe.Constraint(expr=e == 0) + + params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] + for a1, a2, b1, b2 in params_to_test: + m.a1.value = a1 + m.a2.value = a2 + m.b1.value = b1 + m.b2.value = b2 + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + if res.objective_bound is None: + bound = -math.inf + else: + bound = res.objective_bound + self.assertTrue(bound <= m.y.value) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_no_objective( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(mutable=True) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.c1 = pe.Constraint(expr=m.y == m.a1 * m.x + m.b1) + m.c2 = pe.Constraint(expr=m.y == m.a2 * m.x + m.b2) + + params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] + for a1, a2, b1, b2 in params_to_test: + m.a1.value = a1 + m.a2.value = a2 + m.b1.value = b1 + m.b2.value = b2 + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertEqual(res.incumbent_objective, None) + self.assertEqual(res.objective_bound, None) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], 0) + self.assertAlmostEqual(duals[m.c2], 0) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_add_remove_cons( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + a1 = -1 + a2 = 1 + b1 = 1 + b2 = 2 + a3 = 1 + b3 = 3 + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) + m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + if res.objective_bound is None: + bound = -math.inf + else: + bound = res.objective_bound + self.assertTrue(bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + + m.c3 = pe.Constraint(expr=m.y >= a3 * m.x + b3) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b3 - b1) / (a1 - a3)) + self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound is None or res.objective_bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) + self.assertAlmostEqual(duals[m.c2], 0) + self.assertAlmostEqual(duals[m.c3], a1 / (a3 - a1)) + + del m.c3 + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(res.incumbent_objective, m.y.value) + self.assertTrue(res.objective_bound is None or res.objective_bound <= m.y.value) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_results_infeasible( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= m.x) + m.c2 = pe.Constraint(expr=m.y <= m.x - 1) + with self.assertRaises(Exception): + res = opt.solve(m) + opt.config.load_solutions = False + opt.config.raise_exception_on_nonoptimal_result = False + res = opt.solve(m) + self.assertNotEqual(res.solution_status, SolutionStatus.optimal) + if isinstance(opt, ipopt): + acceptable_termination_conditions = { + TerminationCondition.locallyInfeasible, + TerminationCondition.unbounded, + } + else: + acceptable_termination_conditions = { + TerminationCondition.provenInfeasible, + TerminationCondition.infeasibleOrUnbounded, + } + self.assertIn(res.termination_condition, acceptable_termination_conditions) + self.assertAlmostEqual(m.x.value, None) + self.assertAlmostEqual(m.y.value, None) + self.assertTrue(res.incumbent_objective is None) + + if not isinstance(opt, ipopt): + # ipopt can return the values of the variables/duals at the last iterate + # even if it did not converge; raise_exception_on_nonoptimal_result + # is set to False, so we are free to load infeasible solutions + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have a valid solution.*' + ): + res.solution_loader.load_vars() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid duals.*' + ): + res.solution_loader.get_duals() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid reduced costs.*' + ): + res.solution_loader.get_reduced_costs() + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_duals(self, name: str, opt_class: Type[SolverBase],): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y - m.x >= 0) + m.c2 = pe.Constraint(expr=m.y + m.x - 2 >= 0) + + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 1) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], 0.5) + self.assertAlmostEqual(duals[m.c2], 0.5) + + duals = res.solution_loader.get_duals(cons_to_load=[m.c1]) + self.assertAlmostEqual(duals[m.c1], 0.5) + self.assertNotIn(m.c2, duals) + + @parameterized.expand(input=_load_tests(qcp_solvers)) + def test_mutable_quadratic_coefficient( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a = pe.Param(initialize=1, mutable=True) + m.b = pe.Param(initialize=-1, mutable=True) + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c = pe.Constraint(expr=m.y >= (m.a * m.x + m.b) ** 2) + + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0.41024548525899274, 4) + self.assertAlmostEqual(m.y.value, 0.34781038127030117, 4) + m.a.value = 2 + m.b.value = -0.5 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0.10256137418973625, 4) + self.assertAlmostEqual(m.y.value, 0.0869525991355825, 4) + + @parameterized.expand(input=_load_tests(qcp_solvers)) + def test_mutable_quadratic_objective( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a = pe.Param(initialize=1, mutable=True) + m.b = pe.Param(initialize=-1, mutable=True) + m.c = pe.Param(initialize=1, mutable=True) + m.d = pe.Param(initialize=1, mutable=True) + m.obj = pe.Objective(expr=m.x**2 + m.c * m.y**2 + m.d * m.x) + m.ccon = pe.Constraint(expr=m.y >= (m.a * m.x + m.b) ** 2) + + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0.2719178742733325, 4) + self.assertAlmostEqual(m.y.value, 0.5301035741688002, 4) + m.c.value = 3.5 + m.d.value = -1 + res = opt.solve(m) + + self.assertAlmostEqual(m.x.value, 0.6962249634573562, 4) + self.assertAlmostEqual(m.y.value, 0.09227926676152151, 4) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_fixed_vars( + self, name: str, opt_class: Type[SolverBase], + ): + for treat_fixed_vars_as_params in [True, False]: + opt: SolverBase = opt_class() + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = treat_fixed_vars_as_params + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.x.fix(0) + m.y = pe.Var() + a1 = 1 + a2 = -1 + b1 = 1 + b2 = 2 + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) + m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + m.x.unfix() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + m.x.fix(0) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + m.x.value = 2 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2) + self.assertAlmostEqual(m.y.value, 3) + m.x.value = 0 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_fixed_vars_2( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = True + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.x.fix(0) + m.y = pe.Var() + a1 = 1 + a2 = -1 + b1 = 1 + b2 = 2 + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= a1 * m.x + b1) + m.c2 = pe.Constraint(expr=m.y >= a2 * m.x + b2) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + m.x.unfix() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + m.x.fix(0) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + m.x.value = 2 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2) + self.assertAlmostEqual(m.y.value, 3) + m.x.value = 0 + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_fixed_vars_3( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = True + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x + m.y) + m.c1 = pe.Constraint(expr=m.x == 2 / m.y) + m.y.fix(1) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2) + + @parameterized.expand(input=_load_tests(nlp_solvers)) + def test_fixed_vars_4( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = True + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=m.x == 2 / m.y) + m.y.fix(1) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2) + m.y.unfix() + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 2**0.5) + self.assertAlmostEqual(m.y.value, 2**0.5) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_mutable_param_with_range( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + try: + import numpy as np + except: + raise unittest.SkipTest('numpy is not available') + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(initialize=0, mutable=True) + m.a2 = pe.Param(initialize=0, mutable=True) + m.b1 = pe.Param(initialize=0, mutable=True) + m.b2 = pe.Param(initialize=0, mutable=True) + m.c1 = pe.Param(initialize=0, mutable=True) + m.c2 = pe.Param(initialize=0, mutable=True) + m.obj = pe.Objective(expr=m.y) + m.con1 = pe.Constraint(expr=(m.b1, m.y - m.a1 * m.x, m.c1)) + m.con2 = pe.Constraint(expr=(m.b2, m.y - m.a2 * m.x, m.c2)) + + np.random.seed(0) + params_to_test = [ + ( + np.random.uniform(0, 10), + np.random.uniform(-10, 0), + np.random.uniform(-5, 2.5), + np.random.uniform(-5, 2.5), + np.random.uniform(2.5, 10), + np.random.uniform(2.5, 10), + pe.minimize, + ), + ( + np.random.uniform(0, 10), + np.random.uniform(-10, 0), + np.random.uniform(-5, 2.5), + np.random.uniform(-5, 2.5), + np.random.uniform(2.5, 10), + np.random.uniform(2.5, 10), + pe.maximize, + ), + ( + np.random.uniform(0, 10), + np.random.uniform(-10, 0), + np.random.uniform(-5, 2.5), + np.random.uniform(-5, 2.5), + np.random.uniform(2.5, 10), + np.random.uniform(2.5, 10), + pe.minimize, + ), + ( + np.random.uniform(0, 10), + np.random.uniform(-10, 0), + np.random.uniform(-5, 2.5), + np.random.uniform(-5, 2.5), + np.random.uniform(2.5, 10), + np.random.uniform(2.5, 10), + pe.maximize, + ), + ] + for a1, a2, b1, b2, c1, c2, sense in params_to_test: + m.a1.value = float(a1) + m.a2.value = float(a2) + m.b1.value = float(b1) + m.b2.value = float(b2) + m.c1.value = float(c1) + m.c2.value = float(c2) + m.obj.sense = sense + res: Results = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + if sense is pe.minimize: + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2), 6) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) + self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) + self.assertTrue(res.objective_bound is None or res.objective_bound <= m.y.value + 1e-12) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) + self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) + else: + self.assertAlmostEqual(m.x.value, (c2 - c1) / (a1 - a2), 6) + self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) + self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) + self.assertTrue(res.objective_bound is None or res.objective_bound >= m.y.value - 1e-12) + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) + self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_add_and_remove_vars( + self, name: str, opt_class: Type[SolverBase], + ): + opt = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.y = pe.Var(bounds=(-1, None)) + m.obj = pe.Objective(expr=m.y) + if opt.is_persistent(): + opt.config.auto_updates.update_params = False + opt.config.auto_updates.update_vars = False + opt.config.auto_updates.update_constraints = False + opt.config.auto_updates.update_named_expressions = False + opt.config.auto_updates.check_for_new_or_removed_params = False + opt.config.auto_updates.check_for_new_or_removed_constraints = False + opt.config.auto_updates.check_for_new_or_removed_vars = False + opt.config.load_solutions = False + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + res.solution_loader.load_vars() + self.assertAlmostEqual(m.y.value, -1) + m.x = pe.Var() + a1 = 1 + a2 = -1 + b1 = 2 + b2 = 1 + m.c1 = pe.Constraint(expr=(0, m.y - a1 * m.x - b1, None)) + m.c2 = pe.Constraint(expr=(None, -m.y + a2 * m.x + b2, 0)) + if opt.is_persistent(): + opt.add_constraints([m.c1, m.c2]) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + m.c1.deactivate() + m.c2.deactivate() + if opt.is_persistent(): + opt.remove_constraints([m.c1, m.c2]) + m.x.value = None + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + res.solution_loader.load_vars() + self.assertEqual(m.x.value, None) + self.assertAlmostEqual(m.y.value, -1) + + @parameterized.expand(input=_load_tests(nlp_solvers)) + def test_exp(self, name: str, opt_class: Type[SolverBase],): + opt = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=m.y >= pe.exp(m.x)) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, -0.42630274815985264) + self.assertAlmostEqual(m.y.value, 0.6529186341994245) + + @parameterized.expand(input=_load_tests(nlp_solvers)) + def test_log(self, name: str, opt_class: Type[SolverBase],): + opt = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(initialize=1) + m.y = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=m.y <= pe.log(m.x)) + res = opt.solve(m) + self.assertAlmostEqual(m.x.value, 0.6529186341994245) + self.assertAlmostEqual(m.y.value, -0.42630274815985264) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_with_numpy( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + a1 = 1 + b1 = 3 + a2 = -2 + b2 = 1 + m.c1 = pe.Constraint( + expr=(numpy.float64(0), m.y - numpy.int64(1) * m.x - numpy.float32(3), None) + ) + m.c2 = pe.Constraint( + expr=( + None, + -m.y + numpy.int32(-2) * m.x + numpy.float64(1), + numpy.float16(0), + ) + ) + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_bounds_with_params( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.y = pe.Var() + m.p = pe.Param(mutable=True) + m.y.setlb(m.p) + m.p.value = 1 + m.obj = pe.Objective(expr=m.y) + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 1) + m.p.value = -1 + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, -1) + m.y.setlb(None) + m.y.setub(m.p) + m.obj.sense = pe.maximize + m.p.value = 5 + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 5) + m.p.value = 4 + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 4) + m.y.setub(None) + m.y.setlb(m.p) + m.obj.sense = pe.minimize + m.p.value = 3 + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 3) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_solution_loader( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(1, None)) + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=(0, m.y - m.x, None)) + m.c2 = pe.Constraint(expr=(0, m.y - m.x + 1, None)) + opt.config.load_solutions = False + res = opt.solve(m) + self.assertIsNone(m.x.value) + self.assertIsNone(m.y.value) + res.solution_loader.load_vars() + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 1) + m.x.value = None + m.y.value = None + res.solution_loader.load_vars([m.y]) + self.assertAlmostEqual(m.y.value, 1) + primals = res.solution_loader.get_primals() + self.assertIn(m.x, primals) + self.assertIn(m.y, primals) + self.assertAlmostEqual(primals[m.x], 1) + self.assertAlmostEqual(primals[m.y], 1) + primals = res.solution_loader.get_primals([m.y]) + self.assertNotIn(m.x, primals) + self.assertIn(m.y, primals) + self.assertAlmostEqual(primals[m.y], 1) + reduced_costs = res.solution_loader.get_reduced_costs() + self.assertIn(m.x, reduced_costs) + self.assertIn(m.y, reduced_costs) + self.assertAlmostEqual(reduced_costs[m.x], 1) + self.assertAlmostEqual(reduced_costs[m.y], 0) + reduced_costs = res.solution_loader.get_reduced_costs([m.y]) + self.assertNotIn(m.x, reduced_costs) + self.assertIn(m.y, reduced_costs) + self.assertAlmostEqual(reduced_costs[m.y], 0) + duals = res.solution_loader.get_duals() + self.assertIn(m.c1, duals) + self.assertIn(m.c2, duals) + self.assertAlmostEqual(duals[m.c1], 1) + self.assertAlmostEqual(duals[m.c2], 0) + duals = res.solution_loader.get_duals([m.c1]) + self.assertNotIn(m.c2, duals) + self.assertIn(m.c1, duals) + self.assertAlmostEqual(duals[m.c1], 1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_time_limit( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + from sys import platform + + if platform == 'win32': + raise unittest.SkipTest + + N = 30 + m = pe.ConcreteModel() + m.jobs = pe.Set(initialize=list(range(N))) + m.tasks = pe.Set(initialize=list(range(N))) + m.x = pe.Var(m.jobs, m.tasks, bounds=(0, 1)) + + random.seed(0) + coefs = list() + lin_vars = list() + for j in m.jobs: + for t in m.tasks: + coefs.append(random.uniform(0, 10)) + lin_vars.append(m.x[j, t]) + obj_expr = LinearExpression( + linear_coefs=coefs, linear_vars=lin_vars, constant=0 + ) + m.obj = pe.Objective(expr=obj_expr, sense=pe.maximize) + + m.c1 = pe.Constraint(m.jobs) + m.c2 = pe.Constraint(m.tasks) + for j in m.jobs: + expr = LinearExpression( + linear_coefs=[1] * N, + linear_vars=[m.x[j, t] for t in m.tasks], + constant=0, + ) + m.c1[j] = expr == 1 + for t in m.tasks: + expr = LinearExpression( + linear_coefs=[1] * N, + linear_vars=[m.x[j, t] for j in m.jobs], + constant=0, + ) + m.c2[t] = expr == 1 + if isinstance(opt, ipopt): + opt.config.time_limit = 1e-6 + else: + opt.config.time_limit = 0 + opt.config.load_solutions = False + opt.config.raise_exception_on_nonoptimal_result = False + res = opt.solve(m) + self.assertIn( + res.termination_condition, {TerminationCondition.maxTimeLimit, TerminationCondition.iterationLimit} + ) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_objective_changes( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.c1 = pe.Constraint(expr=m.y >= m.x + 1) + m.c2 = pe.Constraint(expr=m.y >= -m.x + 1) + m.obj = pe.Objective(expr=m.y) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + del m.obj + m.obj = pe.Objective(expr=2 * m.y) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2) + m.obj.expr = 3 * m.y + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 3) + m.obj.sense = pe.maximize + opt.config.raise_exception_on_nonoptimal_result = False + opt.config.load_solutions = False + res = opt.solve(m) + self.assertIn( + res.termination_condition, + { + TerminationCondition.unbounded, + TerminationCondition.infeasibleOrUnbounded, + }, + ) + m.obj.sense = pe.minimize + opt.config.load_solutions = True + del m.obj + m.obj = pe.Objective(expr=m.x * m.y) + m.x.fix(2) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 6, 6) + m.x.fix(3) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 12, 6) + m.x.unfix() + m.y.fix(2) + m.x.setlb(-3) + m.x.setub(5) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -2, 6) + m.y.unfix() + m.x.setlb(None) + m.x.setub(None) + m.e = pe.Expression(expr=2) + del m.obj + m.obj = pe.Objective(expr=m.e * m.y) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2) + m.e.expr = 3 + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 3) + if opt.is_persistent(): + opt.config.auto_updates.check_for_new_objective = False + m.e.expr = 4 + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 4) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_domain( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(1, None), domain=pe.NonNegativeReals) + m.obj = pe.Objective(expr=m.x) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + m.x.setlb(-1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + m.x.setlb(1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + m.x.setlb(-1) + m.x.domain = pe.Reals + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -1) + m.x.domain = pe.NonNegativeReals + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + + @parameterized.expand(input=_load_tests(mip_solvers)) + def test_domain_with_integers( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, None), domain=pe.NonNegativeIntegers) + m.obj = pe.Objective(expr=m.x) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + m.x.setlb(0.5) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + m.x.setlb(-5.5) + m.x.domain = pe.Integers + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -5) + m.x.domain = pe.Binary + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + m.x.setlb(0.5) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_fixed_binaries( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var(domain=pe.Binary) + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c = pe.Constraint(expr=m.y >= m.x) + m.x.fix(0) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + m.x.fix(1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + + opt: SolverBase = opt_class() + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = False + m.x.fix(0) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + m.x.fix(1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + + @parameterized.expand(input=_load_tests(mip_solvers)) + def test_with_gdp( + self, name: str, opt_class: Type[SolverBase], + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-10, 10)) + m.y = pe.Var(bounds=(-10, 10)) + m.obj = pe.Objective(expr=m.y) + m.d1 = gdp.Disjunct() + m.d1.c1 = pe.Constraint(expr=m.y >= m.x + 2) + m.d1.c2 = pe.Constraint(expr=m.y >= -m.x + 2) + m.d2 = gdp.Disjunct() + m.d2.c1 = pe.Constraint(expr=m.y >= m.x + 1) + m.d2.c2 = pe.Constraint(expr=m.y >= -m.x + 1) + m.disjunction = gdp.Disjunction(expr=[m.d2, m.d1]) + pe.TransformationFactory("gdp.bigm").apply_to(m) + + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + + opt: SolverBase = opt_class() + opt.use_extensions = True + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + + @parameterized.expand(input=all_solvers) + def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase]): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.b = pe.Block() + m.b.obj = pe.Objective(expr=m.y) + m.b.c1 = pe.Constraint(expr=m.y >= m.x + 2) + m.b.c2 = pe.Constraint(expr=m.y >= -m.x) + + res = opt.solve(m.b) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(m.x.value, -1) + self.assertAlmostEqual(m.y.value, 1) + + m.x.setlb(0) + res = opt.solve(m.b) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 2) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 2) + + @parameterized.expand(input=all_solvers) + def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase]): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= m.x) + m.c2 = pe.Constraint(expr=m.y >= -m.x) + m.c3 = pe.Constraint(expr=m.y >= m.z + 1) + m.c4 = pe.Constraint(expr=m.y >= -m.z + 1) + + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 1) + sol = res.solution_loader.get_primals() + self.assertIn(m.x, sol) + self.assertIn(m.y, sol) + self.assertIn(m.z, sol) + + del m.c3 + del m.c4 + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 0) + sol = res.solution_loader.get_primals() + self.assertIn(m.x, sol) + self.assertIn(m.y, sol) + self.assertNotIn(m.z, sol) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_bug_1(self, name: str, opt_class: Type[SolverBase],): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(3, 7)) + m.y = pe.Var(bounds=(-10, 10)) + m.p = pe.Param(mutable=True, initialize=0) + + m.obj = pe.Objective(expr=m.y) + m.c = pe.Constraint(expr=m.y >= m.p * m.x) + + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 0) + + m.p.value = 1 + res = opt.solve(m) + self.assertEqual(res.solution_status, SolutionStatus.optimal) + self.assertAlmostEqual(res.incumbent_objective, 3) + + @parameterized.expand(input=_load_tests(all_solvers)) + def test_bug_2(self, name: str, opt_class: Type[SolverBase],): + """ + This test is for a bug where an objective containing a fixed variable does + not get updated properly when the variable is unfixed. + """ + for fixed_var_option in [True, False]: + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest + if opt.is_persistent(): + opt.config.auto_updates.treat_fixed_vars_as_params = fixed_var_option + + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-10, 10)) + m.y = pe.Var() + m.obj = pe.Objective(expr=3 * m.y - m.x) + m.c = pe.Constraint(expr=m.y >= m.x) + + m.x.fix(1) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2, 5) + + m.x.unfix() + m.x.setlb(-9) + m.x.setub(9) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, -18, 5) + + +class TestLegacySolverInterface(unittest.TestCase): + @parameterized.expand(input=all_solvers) + def test_param_updates(self, name: str, opt_class: Type[SolverBase]): + opt = pe.SolverFactory(name + '_v2') + if not opt.available(exception_flag=False): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.a1 = pe.Param(mutable=True) + m.a2 = pe.Param(mutable=True) + m.b1 = pe.Param(mutable=True) + m.b2 = pe.Param(mutable=True) + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) + m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) + m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) + + params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] + for a1, a2, b1, b2 in params_to_test: + m.a1.value = a1 + m.a2.value = a2 + m.b1.value = b1 + m.b2.value = b2 + res = opt.solve(m) + pe.assert_optimal_termination(res) + self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) + self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) + self.assertAlmostEqual(m.dual[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) + + @parameterized.expand(input=all_solvers) + def test_load_solutions(self, name: str, opt_class: Type[SolverBase]): + opt = pe.SolverFactory(name + '_v2') + if not opt.available(exception_flag=False): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.x = pe.Var() + m.obj = pe.Objective(expr=m.x) + m.c = pe.Constraint(expr=(-1, m.x, 1)) + m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) + res = opt.solve(m, load_solutions=False) + pe.assert_optimal_termination(res) + self.assertIsNone(m.x.value) + self.assertNotIn(m.c, m.dual) + m.solutions.load_from(res) + self.assertAlmostEqual(m.x.value, -1) + self.assertAlmostEqual(m.dual[m.c], 1) diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index 807d66f569e..c4d13ae31d2 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -166,7 +166,7 @@ class DirectSolverUtils: class PersistentSolverUtils(abc.ABC): - def __init__(self, only_child_vars=False): + def __init__(self): self._model = None self._active_constraints = {} # maps constraint to (lower, body, upper) self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) @@ -185,11 +185,10 @@ def __init__(self, only_child_vars=False): self._vars_referenced_by_con = {} self._vars_referenced_by_obj = [] self._expr_types = None - self._only_child_vars = only_child_vars def set_instance(self, model): saved_config = self.config - self.__init__(only_child_vars=self._only_child_vars) + self.__init__() self.config = saved_config self._model = model self.add_block(model) @@ -257,8 +256,7 @@ def add_constraints(self, cons: List[_GeneralConstraintData]): self._active_constraints[con] = (con.lower, con.body, con.upper) tmp = collect_vars_and_named_exprs(con.body) named_exprs, variables, fixed_vars, external_functions = tmp - if not self._only_child_vars: - self._check_for_new_vars(variables) + self._check_for_new_vars(variables) self._named_expressions[con] = [(e, e.expr) for e in named_exprs] if len(external_functions) > 0: self._external_functions[con] = external_functions @@ -285,8 +283,7 @@ def add_sos_constraints(self, cons: List[_SOSConstraintData]): ) self._active_constraints[con] = tuple() variables = con.get_variables() - if not self._only_child_vars: - self._check_for_new_vars(variables) + self._check_for_new_vars(variables) self._named_expressions[con] = [] self._vars_referenced_by_con[con] = variables for v in variables: @@ -301,8 +298,7 @@ def set_objective(self, obj: _GeneralObjectiveData): if self._objective is not None: for v in self._vars_referenced_by_obj: self._referenced_variables[id(v)][2] = None - if not self._only_child_vars: - self._check_to_remove_vars(self._vars_referenced_by_obj) + self._check_to_remove_vars(self._vars_referenced_by_obj) self._external_functions.pop(self._objective, None) if obj is not None: self._objective = obj @@ -310,8 +306,7 @@ def set_objective(self, obj: _GeneralObjectiveData): self._objective_sense = obj.sense tmp = collect_vars_and_named_exprs(obj.expr) named_exprs, variables, fixed_vars, external_functions = tmp - if not self._only_child_vars: - self._check_for_new_vars(variables) + self._check_for_new_vars(variables) self._obj_named_expressions = [(i, i.expr) for i in named_exprs] if len(external_functions) > 0: self._external_functions[obj] = external_functions @@ -339,15 +334,6 @@ def add_block(self, block): for _p in p.values(): param_dict[id(_p)] = _p self.add_params(list(param_dict.values())) - if self._only_child_vars: - self.add_variables( - list( - dict( - (id(var), var) - for var in block.component_data_objects(Var, descend_into=True) - ).values() - ) - ) self.add_constraints( list( block.component_data_objects(Constraint, descend_into=True, active=True) @@ -379,8 +365,7 @@ def remove_constraints(self, cons: List[_GeneralConstraintData]): ) for v in self._vars_referenced_by_con[con]: self._referenced_variables[id(v)][0].pop(con) - if not self._only_child_vars: - self._check_to_remove_vars(self._vars_referenced_by_con[con]) + self._check_to_remove_vars(self._vars_referenced_by_con[con]) del self._active_constraints[con] del self._named_expressions[con] self._external_functions.pop(con, None) @@ -454,17 +439,6 @@ def remove_block(self, block): ) ) ) - if self._only_child_vars: - self.remove_variables( - list( - dict( - (id(var), var) - for var in block.component_data_objects( - ctype=Var, descend_into=True - ) - ).values() - ) - ) self.remove_params( list( dict( @@ -512,20 +486,7 @@ def update(self, timer: HierarchicalTimer = None): current_cons_dict = {} current_sos_dict = {} timer.start('vars') - if self._only_child_vars and ( - config.check_for_new_or_removed_vars or config.update_vars - ): - current_vars_dict = { - id(v): v - for v in self._model.component_data_objects(Var, descend_into=True) - } - for v_id, v in current_vars_dict.items(): - if v_id not in self._vars: - new_vars.append(v) - for v_id, v_tuple in self._vars.items(): - if v_id not in current_vars_dict: - old_vars.append(v_tuple[0]) - elif config.update_vars: + if config.update_vars: start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} timer.stop('vars') timer.start('params') @@ -636,12 +597,7 @@ def update(self, timer: HierarchicalTimer = None): self.add_sos_constraints(sos_to_update) timer.stop('cons') timer.start('vars') - if self._only_child_vars and config.update_vars: - vars_to_check = [] - for v_id, v in current_vars_dict.items(): - if v_id not in new_vars_set: - vars_to_check.append(v) - elif config.update_vars: + if config.update_vars: end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] if config.update_vars: From 4471b7caab4b120c4a00c627bc015fb9995962b3 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 10 Feb 2024 21:33:18 -0700 Subject: [PATCH 0505/3044] run black --- pyomo/contrib/solver/gurobi.py | 25 ++- pyomo/contrib/solver/ipopt.py | 16 +- pyomo/contrib/solver/sol_reader.py | 4 +- .../solver/tests/solvers/test_solvers.py | 144 ++++++------------ 4 files changed, 76 insertions(+), 113 deletions(-) diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 2dcdacd320d..50d241e1e88 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -69,12 +69,12 @@ def __init__( visibility=visibility, ) self.use_mipstart: bool = self.declare( - 'use_mipstart', + 'use_mipstart', ConfigValue( - default=False, - domain=bool, + default=False, + domain=bool, description="If True, the values of the integer variables will be passed to Gurobi.", - ) + ), ) @@ -339,9 +339,12 @@ def _solve(self): self._solver_model.setParam('MIPGap', config.rel_gap) if config.abs_gap is not None: self._solver_model.setParam('MIPGapAbs', config.abs_gap) - + if config.use_mipstart: - for pyomo_var_id, gurobi_var in self._pyomo_var_to_solver_var_map.items(): + for ( + pyomo_var_id, + gurobi_var, + ) in self._pyomo_var_to_solver_var_map.items(): pyomo_var = self._vars[pyomo_var_id][0] if pyomo_var.is_integer() and pyomo_var.value is not None: self.set_var_attr(pyomo_var, 'Start', pyomo_var.value) @@ -866,7 +869,9 @@ def _postsolve(self, timer: HierarchicalTimer): if status == grb.LOADED: # problem is loaded, but no solution results.termination_condition = TerminationCondition.unknown elif status == grb.OPTIMAL: # optimal - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) elif status == grb.INFEASIBLE: results.termination_condition = TerminationCondition.provenInfeasible elif status == grb.INF_OR_UNBD: @@ -894,7 +899,11 @@ def _postsolve(self, timer: HierarchicalTimer): else: results.termination_condition = TerminationCondition.unknown - if results.termination_condition != TerminationCondition.convergenceCriteriaSatisfied and config.raise_exception_on_nonoptimal_result: + if ( + results.termination_condition + != TerminationCondition.convergenceCriteriaSatisfied + and config.raise_exception_on_nonoptimal_result + ): raise RuntimeError( 'Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' ) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 4c4b932381d..5eb877f0867 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -89,15 +89,15 @@ def __init__( implicit_domain=implicit_domain, visibility=visibility, ) - self.timing_info.no_function_solve_time: Optional[float] = ( - self.timing_info.declare( - 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) - ) + self.timing_info.no_function_solve_time: Optional[ + float + ] = self.timing_info.declare( + 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) ) - self.timing_info.function_solve_time: Optional[float] = ( - self.timing_info.declare( - 'function_solve_time', ConfigValue(domain=NonNegativeFloat) - ) + self.timing_info.function_solve_time: Optional[ + float + ] = self.timing_info.declare( + 'function_solve_time', ConfigValue(domain=NonNegativeFloat) ) diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index a2e4d90b898..04f12feb25e 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -122,7 +122,9 @@ def parse_sol_file( # TODO: this is solver dependent # But this was the way in the previous version - and has been fine thus far? result.solution_status = SolutionStatus.infeasible - result.termination_condition = TerminationCondition.iterationLimit # this is not always correct + result.termination_condition = ( + TerminationCondition.iterationLimit + ) # this is not always correct elif (exit_code[1] >= 500) and (exit_code[1] <= 599): exit_code_message = ( "FAILURE: the solver stopped by an error condition " diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 0499f1abb5d..658aaf41b13 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -21,10 +21,7 @@ if not param_available: raise unittest.SkipTest('Parameterized is not available.') -all_solvers = [ - ('gurobi', Gurobi), - ('ipopt', ipopt), -] +all_solvers = [('gurobi', Gurobi), ('ipopt', ipopt)] mip_solvers = [('gurobi', Gurobi)] nlp_solvers = [('ipopt', ipopt)] qcp_solvers = [('gurobi', Gurobi), ('ipopt', ipopt)] @@ -43,7 +40,7 @@ def _load_tests(solver_list): class TestSolvers(unittest.TestCase): @parameterized.expand(input=_load_tests(all_solvers)) def test_remove_variable_and_objective( - self, name: str, opt_class: Type[SolverBase], + self, name: str, opt_class: Type[SolverBase] ): # this test is for issue #2888 opt: SolverBase = opt_class() @@ -65,9 +62,7 @@ def test_remove_variable_and_objective( self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_stale_vars( - self, name: str, opt_class: Type[SolverBase], - ): + def test_stale_vars(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -108,9 +103,7 @@ def test_stale_vars( self.assertFalse(m.y.stale) @parameterized.expand(input=_load_tests(all_solvers)) - def test_range_constraint( - self, name: str, opt_class: Type[SolverBase], - ): + def test_range_constraint(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -131,9 +124,7 @@ def test_range_constraint( self.assertAlmostEqual(duals[m.c], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_reduced_costs( - self, name: str, opt_class: Type[SolverBase], - ): + def test_reduced_costs(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -150,9 +141,7 @@ def test_reduced_costs( self.assertAlmostEqual(rc[m.y], 4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_reduced_costs2( - self, name: str, opt_class: Type[SolverBase], - ): + def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -172,9 +161,7 @@ def test_reduced_costs2( self.assertAlmostEqual(rc[m.x], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_param_changes( - self, name: str, opt_class: Type[SolverBase], - ): + def test_param_changes(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -210,9 +197,7 @@ def test_param_changes( self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_immutable_param( - self, name: str, opt_class: Type[SolverBase], - ): + def test_immutable_param(self, name: str, opt_class: Type[SolverBase]): """ This test is important because component_data_objects returns immutable params as floats. We want to make sure we process these correctly. @@ -252,9 +237,7 @@ def test_immutable_param( self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_equality( - self, name: str, opt_class: Type[SolverBase], - ): + def test_equality(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -292,9 +275,7 @@ def test_equality( self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_linear_expression( - self, name: str, opt_class: Type[SolverBase], - ): + def test_linear_expression(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -332,9 +313,7 @@ def test_linear_expression( self.assertTrue(bound <= m.y.value) @parameterized.expand(input=_load_tests(all_solvers)) - def test_no_objective( - self, name: str, opt_class: Type[SolverBase], - ): + def test_no_objective(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -365,9 +344,7 @@ def test_no_objective( self.assertAlmostEqual(duals[m.c2], 0) @parameterized.expand(input=_load_tests(all_solvers)) - def test_add_remove_cons( - self, name: str, opt_class: Type[SolverBase], - ): + def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -421,9 +398,7 @@ def test_add_remove_cons( self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_results_infeasible( - self, name: str, opt_class: Type[SolverBase], - ): + def test_results_infeasible(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -472,7 +447,7 @@ def test_results_infeasible( res.solution_loader.get_reduced_costs() @parameterized.expand(input=_load_tests(all_solvers)) - def test_duals(self, name: str, opt_class: Type[SolverBase],): + def test_duals(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -496,7 +471,7 @@ def test_duals(self, name: str, opt_class: Type[SolverBase],): @parameterized.expand(input=_load_tests(qcp_solvers)) def test_mutable_quadratic_coefficient( - self, name: str, opt_class: Type[SolverBase], + self, name: str, opt_class: Type[SolverBase] ): opt: SolverBase = opt_class() if not opt.available(): @@ -519,9 +494,7 @@ def test_mutable_quadratic_coefficient( self.assertAlmostEqual(m.y.value, 0.0869525991355825, 4) @parameterized.expand(input=_load_tests(qcp_solvers)) - def test_mutable_quadratic_objective( - self, name: str, opt_class: Type[SolverBase], - ): + def test_mutable_quadratic_objective(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -546,13 +519,13 @@ def test_mutable_quadratic_objective( self.assertAlmostEqual(m.y.value, 0.09227926676152151, 4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars( - self, name: str, opt_class: Type[SolverBase], - ): + def test_fixed_vars(self, name: str, opt_class: Type[SolverBase]): for treat_fixed_vars_as_params in [True, False]: opt: SolverBase = opt_class() if opt.is_persistent(): - opt.config.auto_updates.treat_fixed_vars_as_params = treat_fixed_vars_as_params + opt.config.auto_updates.treat_fixed_vars_as_params = ( + treat_fixed_vars_as_params + ) if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() @@ -587,9 +560,7 @@ def test_fixed_vars( self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars_2( - self, name: str, opt_class: Type[SolverBase], - ): + def test_fixed_vars_2(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True @@ -627,9 +598,7 @@ def test_fixed_vars_2( self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars_3( - self, name: str, opt_class: Type[SolverBase], - ): + def test_fixed_vars_3(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True @@ -645,9 +614,7 @@ def test_fixed_vars_3( self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(nlp_solvers)) - def test_fixed_vars_4( - self, name: str, opt_class: Type[SolverBase], - ): + def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True @@ -667,9 +634,7 @@ def test_fixed_vars_4( self.assertAlmostEqual(m.y.value, 2**0.5) @parameterized.expand(input=_load_tests(all_solvers)) - def test_mutable_param_with_range( - self, name: str, opt_class: Type[SolverBase], - ): + def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -743,7 +708,10 @@ def test_mutable_param_with_range( self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) - self.assertTrue(res.objective_bound is None or res.objective_bound <= m.y.value + 1e-12) + self.assertTrue( + res.objective_bound is None + or res.objective_bound <= m.y.value + 1e-12 + ) duals = res.solution_loader.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @@ -751,15 +719,16 @@ def test_mutable_param_with_range( self.assertAlmostEqual(m.x.value, (c2 - c1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) self.assertAlmostEqual(res.incumbent_objective, m.y.value, 6) - self.assertTrue(res.objective_bound is None or res.objective_bound >= m.y.value - 1e-12) + self.assertTrue( + res.objective_bound is None + or res.objective_bound >= m.y.value - 1e-12 + ) duals = res.solution_loader.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @parameterized.expand(input=_load_tests(all_solvers)) - def test_add_and_remove_vars( - self, name: str, opt_class: Type[SolverBase], - ): + def test_add_and_remove_vars(self, name: str, opt_class: Type[SolverBase]): opt = opt_class() if not opt.available(): raise unittest.SkipTest @@ -805,7 +774,7 @@ def test_add_and_remove_vars( self.assertAlmostEqual(m.y.value, -1) @parameterized.expand(input=_load_tests(nlp_solvers)) - def test_exp(self, name: str, opt_class: Type[SolverBase],): + def test_exp(self, name: str, opt_class: Type[SolverBase]): opt = opt_class() if not opt.available(): raise unittest.SkipTest @@ -819,7 +788,7 @@ def test_exp(self, name: str, opt_class: Type[SolverBase],): self.assertAlmostEqual(m.y.value, 0.6529186341994245) @parameterized.expand(input=_load_tests(nlp_solvers)) - def test_log(self, name: str, opt_class: Type[SolverBase],): + def test_log(self, name: str, opt_class: Type[SolverBase]): opt = opt_class() if not opt.available(): raise unittest.SkipTest @@ -833,9 +802,7 @@ def test_log(self, name: str, opt_class: Type[SolverBase],): self.assertAlmostEqual(m.y.value, -0.42630274815985264) @parameterized.expand(input=_load_tests(all_solvers)) - def test_with_numpy( - self, name: str, opt_class: Type[SolverBase], - ): + def test_with_numpy(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -863,9 +830,7 @@ def test_with_numpy( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_bounds_with_params( - self, name: str, opt_class: Type[SolverBase], - ): + def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -897,9 +862,7 @@ def test_bounds_with_params( self.assertAlmostEqual(m.y.value, 3) @parameterized.expand(input=_load_tests(all_solvers)) - def test_solution_loader( - self, name: str, opt_class: Type[SolverBase], - ): + def test_solution_loader(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -949,9 +912,7 @@ def test_solution_loader( self.assertAlmostEqual(duals[m.c1], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_time_limit( - self, name: str, opt_class: Type[SolverBase], - ): + def test_time_limit(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -1002,13 +963,12 @@ def test_time_limit( opt.config.raise_exception_on_nonoptimal_result = False res = opt.solve(m) self.assertIn( - res.termination_condition, {TerminationCondition.maxTimeLimit, TerminationCondition.iterationLimit} + res.termination_condition, + {TerminationCondition.maxTimeLimit, TerminationCondition.iterationLimit}, ) @parameterized.expand(input=_load_tests(all_solvers)) - def test_objective_changes( - self, name: str, opt_class: Type[SolverBase], - ): + def test_objective_changes(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -1072,9 +1032,7 @@ def test_objective_changes( self.assertAlmostEqual(res.incumbent_objective, 4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_domain( - self, name: str, opt_class: Type[SolverBase], - ): + def test_domain(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -1098,9 +1056,7 @@ def test_domain( self.assertAlmostEqual(res.incumbent_objective, 0) @parameterized.expand(input=_load_tests(mip_solvers)) - def test_domain_with_integers( - self, name: str, opt_class: Type[SolverBase], - ): + def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -1124,9 +1080,7 @@ def test_domain_with_integers( self.assertAlmostEqual(res.incumbent_objective, 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_binaries( - self, name: str, opt_class: Type[SolverBase], - ): + def test_fixed_binaries(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -1153,9 +1107,7 @@ def test_fixed_binaries( self.assertAlmostEqual(res.incumbent_objective, 1) @parameterized.expand(input=_load_tests(mip_solvers)) - def test_with_gdp( - self, name: str, opt_class: Type[SolverBase], - ): + def test_with_gdp(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -1248,7 +1200,7 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase]): self.assertNotIn(m.z, sol) @parameterized.expand(input=_load_tests(all_solvers)) - def test_bug_1(self, name: str, opt_class: Type[SolverBase],): + def test_bug_1(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest @@ -1271,7 +1223,7 @@ def test_bug_1(self, name: str, opt_class: Type[SolverBase],): self.assertAlmostEqual(res.incumbent_objective, 3) @parameterized.expand(input=_load_tests(all_solvers)) - def test_bug_2(self, name: str, opt_class: Type[SolverBase],): + def test_bug_2(self, name: str, opt_class: Type[SolverBase]): """ This test is for a bug where an objective containing a fixed variable does not get updated properly when the variable is unfixed. From cfc78cb2a532b3239b349d2517e7870636d2d1f1 Mon Sep 17 00:00:00 2001 From: Sakshi <73687517+Sakshi21299@users.noreply.github.com> Date: Sun, 11 Feb 2024 21:08:03 -0500 Subject: [PATCH 0506/3044] Changed add_edge inputs to explicitly be variables and constraints --- pyomo/contrib/incidence_analysis/interface.py | 49 +++++-------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 23178bdf14b..20b3928f979 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -933,17 +933,15 @@ def plot(self, variables=None, constraints=None, title=None, show=True): if show: fig.show() - def add_edge_to_graph(self, node0, node1): + def add_edge(self, variable, constraint): """Adds an edge between node0 and node1 in the incidence graph Parameters --------- - nodes0: VarData/ConstraintData - A node in the graph from the first bipartite set - (``bipartite=0``) - node1: VarData/ConstraintData - A node in the graph from the second bipartite set - (``bipartite=1``) + variable: VarData + A variable in the graph + constraint: ConstraintData + A constraint in the graph """ if self._incidence_graph is None: raise RuntimeError( @@ -951,34 +949,13 @@ def add_edge_to_graph(self, node0, node1): "incidence graph,\nbut no incidence graph has been cached." ) - if node0 not in ComponentSet(self._variables) and node0 not in ComponentSet( - self._constraints - ): - raise RuntimeError("%s is not a node in the incidence graph" % node0) + if variable not in self._var_index_map: + raise RuntimeError("%s is not a variable in the incidence graph" % variable) - if node1 not in ComponentSet(self._variables) and node1 not in ComponentSet( - self._constraints - ): - raise RuntimeError("%s is not a node in the incidence graph" % node1) + if constraint not in self._con_index_map: + raise RuntimeError("%s is not a constraint in the incidence graph" % constraint) - if node0 in ComponentSet(self._variables): - node0_idx = self._var_index_map[node0] + len(self._con_index_map) - if node1 in ComponentSet(self._variables): - raise RuntimeError( - "%s & %s are both variables. Cannot add an edge between two" - "variables.\nThe resulting graph won't be bipartite" - % (node0, node1) - ) - node1_idx = self._con_index_map[node1] - - if node0 in ComponentSet(self._constraints): - node0_idx = self._con_index_map[node0] - if node1 in ComponentSet(self._constraints): - raise RuntimeError( - "%s & %s are both constraints. Cannot add an edge between two" - "constraints.\nThe resulting graph won't be bipartite" - % (node0, node1) - ) - node1_idx = self._var_index_map[node1] + len(self._con_index_map) - - self._incidence_graph.add_edge(node0_idx, node1_idx) + var_id = self._var_index_map[variable] + len(self._con_index_map) + con_id = self._con_index_map[constraint] + + self._incidence_graph.add_edge(var_id, con_id) From 6348ac170f8f5f5a6dace9824a0c46306589b11b Mon Sep 17 00:00:00 2001 From: Sakshi <73687517+Sakshi21299@users.noreply.github.com> Date: Sun, 11 Feb 2024 21:08:56 -0500 Subject: [PATCH 0507/3044] Add a test for variable-constraint elimination --- .../tests/test_interface.py | 63 +++++++++++-------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 7da563be28c..ec518a342c7 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1799,50 +1799,59 @@ def test_add_edge(self): m.eq3 = pyo.Constraint(expr=m.x[3] + m.x[2] + m.x[4] == 1) m.eq4 = pyo.Constraint(expr=m.x[1] + m.x[2] ** 2 == 5) - # nodes: component - # 0 : eq1 - # 1 : eq2 - # 2 : eq3 - # 3 : eq4 - # 4 : x[1] - # 5 : x[2] - # 6 : x[3] - # 7 : x[4] - igraph = IncidenceGraphInterface(m, linear_only=False) n_edges_original = igraph.n_edges - # Test if there already exists an edge between two nodes, nothing is added - igraph.add_edge_to_graph(m.eq3, m.x[4]) - n_edges_new = igraph.n_edges - self.assertEqual(n_edges_original, n_edges_new) - - igraph.add_edge_to_graph(m.x[1], m.eq3) + #Test edge is added between previously unconnectes nodes + igraph.add_edge(m.x[1], m.eq3) n_edges_new = igraph.n_edges - self.assertEqual(set(igraph._incidence_graph[2]), {6, 5, 7, 4}) + assert ComponentSet(igraph.get_adjacent_to(m.eq3)) == ComponentSet(m.x[:]) self.assertEqual(n_edges_original + 1, n_edges_new) - igraph.add_edge_to_graph(m.eq4, m.x[4]) - n_edges_new = igraph.n_edges - self.assertEqual(set(igraph._incidence_graph[3]), {4, 5, 7}) - self.assertEqual(n_edges_original + 2, n_edges_new) + #Test no edge is added if there exists a previous edge between nodes + igraph.add_edge(m.x[2], m.eq3) + n_edges2 = igraph.n_edges + self.assertEqual(n_edges_new, n_edges2) def test_add_edge_linear_igraph(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2, 3, 4]) m.eq1 = pyo.Constraint(expr=m.x[1] + m.x[3] == 1) m.eq2 = pyo.Constraint(expr=m.x[2] + pyo.sqrt(m.x[1]) + pyo.exp(m.x[3]) == 1) - m.eq3 = pyo.Constraint(expr=m.x[4] ** 2 + m.x[1] ** 3 + m.x[2] == 1) + m.eq3 = pyo.Constraint(expr=m.x[4] ** 2 + m.x[1] ** 3 + m.x[2]**2 == 1) # Make sure error is raised when a variable is not in the igraph igraph = IncidenceGraphInterface(m, linear_only=True) - n_edges_original = igraph.n_edges - msg = "is not a node in the incidence graph" + msg = "is not a variable in the incidence graph" with self.assertRaisesRegex(RuntimeError, msg): - igraph.add_edge_to_graph(m.x[4], m.eq2) - - + igraph.add_edge(m.x[4], m.eq2) + + def test_var_elim(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3, 4]) + m.eq1 = pyo.Constraint(expr=m.x[1] ** 2 + m.x[2] ** 2 + m.x[3] ** 2 == 1) + m.eq2 = pyo.Constraint(expr=pyo.sqrt(m.x[1]) + pyo.exp(m.x[3]) == 1) + m.eq3 = pyo.Constraint(expr=m.x[3] + m.x[2] + m.x[4] == 1) + m.eq4 = pyo.Constraint(expr=m.x[1] == 5*m.x[2]) + + igraph = IncidenceGraphInterface(m) + #Eliminate x[1] usinf eq4 + for adj_con in igraph.get_adjacent_to(m.x[1]): + for adj_var in igraph.get_adjacent_to(m.eq4): + igraph.add_edge(adj_var, adj_con) + igraph.remove_nodes([m.x[1], m.eq4]) + + assert ComponentSet(igraph.variables) == ComponentSet([m.x[2], m.x[3], m.x[4]]) + assert ComponentSet(igraph.constraints) == ComponentSet([m.eq1, m.eq2, m.eq3]) + self.assertEqual(7, igraph.n_edges) + + assert m.x[2] in ComponentSet(igraph.get_adjacent_to(m.eq1)) + assert m.x[2] in ComponentSet(igraph.get_adjacent_to(m.eq2)) + + + + @unittest.skipUnless(networkx_available, "networkx is not available.") class TestIndexedBlock(unittest.TestCase): def test_block_data_obj(self): From b8e309da1599ba1eeb6c357e5529d58449ed5cf2 Mon Sep 17 00:00:00 2001 From: Sakshi <73687517+Sakshi21299@users.noreply.github.com> Date: Sun, 11 Feb 2024 21:10:15 -0500 Subject: [PATCH 0508/3044] run black --- pyomo/contrib/incidence_analysis/interface.py | 12 +++++---- .../tests/test_interface.py | 26 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 20b3928f979..5bf7ec71e09 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -939,9 +939,9 @@ def add_edge(self, variable, constraint): Parameters --------- variable: VarData - A variable in the graph + A variable in the graph constraint: ConstraintData - A constraint in the graph + A constraint in the graph """ if self._incidence_graph is None: raise RuntimeError( @@ -953,9 +953,11 @@ def add_edge(self, variable, constraint): raise RuntimeError("%s is not a variable in the incidence graph" % variable) if constraint not in self._con_index_map: - raise RuntimeError("%s is not a constraint in the incidence graph" % constraint) + raise RuntimeError( + "%s is not a constraint in the incidence graph" % constraint + ) - var_id = self._var_index_map[variable] + len(self._con_index_map) + var_id = self._var_index_map[variable] + len(self._con_index_map) con_id = self._con_index_map[constraint] - + self._incidence_graph.add_edge(var_id, con_id) diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index ec518a342c7..01c15c9c84d 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1802,13 +1802,13 @@ def test_add_edge(self): igraph = IncidenceGraphInterface(m, linear_only=False) n_edges_original = igraph.n_edges - #Test edge is added between previously unconnectes nodes + # Test edge is added between previously unconnectes nodes igraph.add_edge(m.x[1], m.eq3) n_edges_new = igraph.n_edges assert ComponentSet(igraph.get_adjacent_to(m.eq3)) == ComponentSet(m.x[:]) self.assertEqual(n_edges_original + 1, n_edges_new) - #Test no edge is added if there exists a previous edge between nodes + # Test no edge is added if there exists a previous edge between nodes igraph.add_edge(m.x[2], m.eq3) n_edges2 = igraph.n_edges self.assertEqual(n_edges_new, n_edges2) @@ -1818,7 +1818,7 @@ def test_add_edge_linear_igraph(self): m.x = pyo.Var([1, 2, 3, 4]) m.eq1 = pyo.Constraint(expr=m.x[1] + m.x[3] == 1) m.eq2 = pyo.Constraint(expr=m.x[2] + pyo.sqrt(m.x[1]) + pyo.exp(m.x[3]) == 1) - m.eq3 = pyo.Constraint(expr=m.x[4] ** 2 + m.x[1] ** 3 + m.x[2]**2 == 1) + m.eq3 = pyo.Constraint(expr=m.x[4] ** 2 + m.x[1] ** 3 + m.x[2] ** 2 == 1) # Make sure error is raised when a variable is not in the igraph igraph = IncidenceGraphInterface(m, linear_only=True) @@ -1826,32 +1826,30 @@ def test_add_edge_linear_igraph(self): msg = "is not a variable in the incidence graph" with self.assertRaisesRegex(RuntimeError, msg): igraph.add_edge(m.x[4], m.eq2) - + def test_var_elim(self): m = pyo.ConcreteModel() m.x = pyo.Var([1, 2, 3, 4]) m.eq1 = pyo.Constraint(expr=m.x[1] ** 2 + m.x[2] ** 2 + m.x[3] ** 2 == 1) m.eq2 = pyo.Constraint(expr=pyo.sqrt(m.x[1]) + pyo.exp(m.x[3]) == 1) m.eq3 = pyo.Constraint(expr=m.x[3] + m.x[2] + m.x[4] == 1) - m.eq4 = pyo.Constraint(expr=m.x[1] == 5*m.x[2]) - - igraph = IncidenceGraphInterface(m) - #Eliminate x[1] usinf eq4 + m.eq4 = pyo.Constraint(expr=m.x[1] == 5 * m.x[2]) + + igraph = IncidenceGraphInterface(m) + # Eliminate x[1] usinf eq4 for adj_con in igraph.get_adjacent_to(m.x[1]): for adj_var in igraph.get_adjacent_to(m.eq4): igraph.add_edge(adj_var, adj_con) igraph.remove_nodes([m.x[1], m.eq4]) - + assert ComponentSet(igraph.variables) == ComponentSet([m.x[2], m.x[3], m.x[4]]) assert ComponentSet(igraph.constraints) == ComponentSet([m.eq1, m.eq2, m.eq3]) self.assertEqual(7, igraph.n_edges) - + assert m.x[2] in ComponentSet(igraph.get_adjacent_to(m.eq1)) assert m.x[2] in ComponentSet(igraph.get_adjacent_to(m.eq2)) - - - - + + @unittest.skipUnless(networkx_available, "networkx is not available.") class TestIndexedBlock(unittest.TestCase): def test_block_data_obj(self): From 90c98c85e254221b44b7b82d66f6ccb674958acb Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 12 Feb 2024 12:12:04 -0700 Subject: [PATCH 0509/3044] Reformatted parmest files using black. --- .../parameter_estimation_example.py | 4 +- pyomo/contrib/parmest/deprecated/parmest.py | 27 ++-- .../parmest/deprecated/tests/test_examples.py | 24 ++- .../parmest/deprecated/tests/test_parmest.py | 4 +- .../simple_reaction_parmest_example.py | 20 +-- .../reactor_design/bootstrap_example.py | 8 +- .../confidence_region_example.py | 8 +- .../reactor_design/datarec_example.py | 23 +-- .../reactor_design/leaveNout_example.py | 7 +- .../likelihood_ratio_example.py | 10 +- .../multisensor_data_example.py | 33 ++-- .../parameter_estimation_example.py | 10 +- .../examples/reactor_design/reactor_design.py | 54 ++++--- .../reactor_design/timeseries_data_example.py | 14 +- .../rooney_biegler/bootstrap_example.py | 10 +- .../likelihood_ratio_example.py | 10 +- .../parameter_estimation_example.py | 12 +- .../examples/rooney_biegler/rooney_biegler.py | 13 +- .../rooney_biegler_with_constraint.py | 13 +- .../semibatch/parameter_estimation_example.py | 9 +- .../examples/semibatch/scenario_example.py | 6 +- .../parmest/examples/semibatch/semibatch.py | 10 +- pyomo/contrib/parmest/experiment.py | 5 +- pyomo/contrib/parmest/parmest.py | 152 +++++++++--------- pyomo/contrib/parmest/scenariocreator.py | 20 +-- pyomo/contrib/parmest/tests/test_parmest.py | 115 +++++++------ .../parmest/tests/test_scenariocreator.py | 6 +- 27 files changed, 337 insertions(+), 290 deletions(-) diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py index 67b69c73555..f5d9364097e 100644 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py @@ -41,9 +41,9 @@ def SSE(model, data): # Parameter estimation obj, theta = pest.theta_est() - print (obj) + print(obj) print(theta) - + # Assert statements compare parameter estimation (theta) to an expected value k1_expected = 5.0 / 6.0 k2_expected = 5.0 / 3.0 diff --git a/pyomo/contrib/parmest/deprecated/parmest.py b/pyomo/contrib/parmest/deprecated/parmest.py index cbdc9179f35..82bf893dd06 100644 --- a/pyomo/contrib/parmest/deprecated/parmest.py +++ b/pyomo/contrib/parmest/deprecated/parmest.py @@ -548,14 +548,13 @@ def _Q_opt( for ndname, Var, solval in ef_nonants(ef): ind_vars.append(Var) # calculate the reduced hessian - ( - solve_result, - inv_red_hes, - ) = inverse_reduced_hessian.inv_reduced_hessian_barrier( - self.ef_instance, - independent_variables=ind_vars, - solver_options=self.solver_options, - tee=self.tee, + (solve_result, inv_red_hes) = ( + inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, + ) ) if self.diagnostic_mode: @@ -745,14 +744,10 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): if self.diagnostic_mode: print(' Experiment = ', snum) print(' First solve with special diagnostics wrapper') - ( - status_obj, - solved, - iters, - time, - regu, - ) = utils.ipopt_solve_with_stats( - instance, optimizer, max_iter=500, max_cpu_time=120 + (status_obj, solved, iters, time, regu) = ( + utils.ipopt_solve_with_stats( + instance, optimizer, max_iter=500, max_cpu_time=120 + ) ) print( " status_obj, solved, iters, time, regularization_stat = ", diff --git a/pyomo/contrib/parmest/deprecated/tests/test_examples.py b/pyomo/contrib/parmest/deprecated/tests/test_examples.py index 04aff572529..6f5d9703f05 100644 --- a/pyomo/contrib/parmest/deprecated/tests/test_examples.py +++ b/pyomo/contrib/parmest/deprecated/tests/test_examples.py @@ -32,7 +32,9 @@ def tearDownClass(self): pass def test_model(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import rooney_biegler + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( + rooney_biegler, + ) rooney_biegler.main() @@ -53,7 +55,9 @@ def test_parameter_estimation_example(self): @unittest.skipUnless(seaborn_available, "test requires seaborn") def test_bootstrap_example(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import bootstrap_example + from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( + bootstrap_example, + ) bootstrap_example.main() @@ -136,7 +140,9 @@ def tearDownClass(self): @unittest.pytest.mark.expensive def test_model(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import reactor_design + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( + reactor_design, + ) reactor_design.main() @@ -149,7 +155,9 @@ def test_parameter_estimation_example(self): @unittest.skipUnless(seaborn_available, "test requires seaborn") def test_bootstrap_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import bootstrap_example + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( + bootstrap_example, + ) bootstrap_example.main() @@ -163,7 +171,9 @@ def test_likelihood_ratio_example(self): @unittest.pytest.mark.expensive def test_leaveNout_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import leaveNout_example + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( + leaveNout_example, + ) leaveNout_example.main() @@ -183,7 +193,9 @@ def test_multisensor_data_example(self): @unittest.skipUnless(matplotlib_available, "test requires matplotlib") def test_datarec_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import datarec_example + from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( + datarec_example, + ) datarec_example.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_parmest.py b/pyomo/contrib/parmest/deprecated/tests/test_parmest.py index 40c98dac3af..27776bdc64c 100644 --- a/pyomo/contrib/parmest/deprecated/tests/test_parmest.py +++ b/pyomo/contrib/parmest/deprecated/tests/test_parmest.py @@ -411,9 +411,7 @@ def rooney_biegler_indexed_vars(data): model.theta = pyo.Var( model.var_names, initialize={"asymptote": 15, "rate_constant": 0.5} ) - model.theta[ - "asymptote" - ].fixed = ( + model.theta["asymptote"].fixed = ( True # parmest will unfix theta variables, even when they are indexed ) model.theta["rate_constant"].fixed = True diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py index 140fceeb8a2..4e9bc6079e7 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py @@ -98,32 +98,32 @@ def label_model(self): def get_labeled_model(self): self.create_model() m = self.label_model() - + return m + # k[2] fixed class SimpleReactionExperimentK2Fixed(SimpleReactionExperiment): def label_model(self): - + m = super().label_model() m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.k[1]]) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.k[1]]) return m + # k[2] variable class SimpleReactionExperimentK2Variable(SimpleReactionExperiment): def label_model(self): - + m = super().label_model() m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.k[1], m.k[2]]) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.k[1], m.k[2]]) return m @@ -150,7 +150,7 @@ def main(): ] # Create an experiment list with k[2] fixed - exp_list= [] + exp_list = [] for i in range(len(data)): exp_list.append(SimpleReactionExperimentK2Fixed(data[i])) @@ -162,7 +162,7 @@ def main(): # Parameter estimation without covariance estimate # Only estimate the parameter k[1]. The parameter k[2] will remain fixed # at its initial value - + pest = parmest.Estimator(exp_list) obj, theta = pest.theta_est() print(obj) @@ -170,7 +170,7 @@ def main(): print() # Create an experiment list with k[2] variable - exp_list= [] + exp_list = [] for i in range(len(data)): exp_list.append(SimpleReactionExperimentK2Variable(data[i])) diff --git a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py index b5cb4196456..0935fbbba41 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py @@ -16,18 +16,19 @@ ReactorDesignExperiment, ) + def main(): # Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - + # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperiment(data, i)) - + # View one model # exp0_model = exp_list[0].get_labeled_model() # print(exp0_model.pprint()) @@ -50,5 +51,6 @@ def main(): title="Bootstrap theta with confidence regions", ) + if __name__ == "__main__": main() diff --git a/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py b/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py index ff84279018d..8aee6e9d67c 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py @@ -16,18 +16,19 @@ ReactorDesignExperiment, ) + def main(): # Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - + # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperiment(data, i)) - + # View one model # exp0_model = exp_list[0].get_labeled_model() # print(exp0_model.pprint()) @@ -45,5 +46,6 @@ def main(): CR = pest.confidence_region_test(bootstrap_theta, "MVN", [0.5, 0.75, 1.0]) print(CR) + if __name__ == "__main__": main() diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index 26185290ea6..2945c284d4a 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -29,11 +29,12 @@ def reactor_design_model_for_datarec(): return model + class ReactorDesignExperimentPreDataRec(ReactorDesignExperiment): def __init__(self, data, data_std, experiment_number): - super().__init__(data, experiment_number) + super().__init__(data, experiment_number) self.data_std = data_std def create_model(self): @@ -43,7 +44,7 @@ def create_model(self): def label_model(self): m = self.model - + # experiment outputs m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.experiment_outputs.update([(m.ca, self.data_i['ca'])]) @@ -63,11 +64,12 @@ def label_model(self): return m + class ReactorDesignExperimentPostDataRec(ReactorDesignExperiment): def __init__(self, data, data_std, experiment_number): - super().__init__(data, experiment_number) + super().__init__(data, experiment_number) self.data_std = data_std def label_model(self): @@ -83,6 +85,7 @@ def label_model(self): return m + def generate_data(): ### Generate data based on real sv, caf, ca, cb, cc, and cd @@ -117,14 +120,16 @@ def main(): data_std = data.std() # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperimentPreDataRec(data, data_std, i)) # Define sum of squared error objective function for data rec def SSE(model): - expr = sum(((y - yhat)/model.experiment_outputs_std[y])**2 - for y, yhat in model.experiment_outputs.items()) + expr = sum( + ((y - yhat) / model.experiment_outputs_std[y]) ** 2 + for y, yhat in model.experiment_outputs.items() + ) return expr # View one model & SSE @@ -138,7 +143,7 @@ def SSE(model): obj, theta, data_rec = pest.theta_est(return_values=["ca", "cb", "cc", "cd", "caf"]) print(obj) print(theta) - + parmest.graphics.grouped_boxplot( data[["ca", "cb", "cc", "cd"]], data_rec[["ca", "cb", "cc", "cd"]], @@ -149,7 +154,7 @@ def SSE(model): data_rec["sv"] = data["sv"] # make a new list of experiments using reconciled data - exp_list= [] + exp_list = [] for i in range(data_rec.shape[0]): exp_list.append(ReactorDesignExperimentPostDataRec(data_rec, data_std, i)) @@ -157,7 +162,7 @@ def SSE(model): obj, theta = pest.theta_est() print(obj) print(theta) - + theta_real = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} print(theta_real) diff --git a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py index 549233d8a84..97aad04c325 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py @@ -24,7 +24,7 @@ def main(): file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - + # Create more data for the example N = 50 df_std = data.std().to_frame().transpose() @@ -33,10 +33,10 @@ def main(): data = df_sample + df_rand.dot(df_std) / 10 # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperiment(data, i)) - + # View one model # exp0_model = exp_list[0].get_labeled_model() # print(exp0_model.pprint()) @@ -89,5 +89,6 @@ def main(): percent_true = sum(r) / len(r) print(percent_true) + if __name__ == "__main__": main() diff --git a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py index 8b6d9fcfecc..7af37b64931 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py @@ -20,17 +20,17 @@ def main(): - -# Read in data + + # Read in data file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - + # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperiment(data, i)) - + # View one model # exp0_model = exp_list[0].get_labeled_model() # print(exp0_model.pprint()) diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index f731032368e..a3802d40360 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -21,34 +21,37 @@ class MultisensorReactorDesignExperiment(ReactorDesignExperiment): def finalize_model(self): - + m = self.model - + # Experiment inputs values m.sv = self.data_i['sv'] m.caf = self.data_i['caf'] - + # Experiment output values - m.ca = (self.data_i['ca1'] + self.data_i['ca2'] + self.data_i['ca3']) * (1/3) + m.ca = (self.data_i['ca1'] + self.data_i['ca2'] + self.data_i['ca3']) * (1 / 3) m.cb = self.data_i['cb'] - m.cc = (self.data_i['cc1'] + self.data_i['cc2']) * (1/2) + m.cc = (self.data_i['cc1'] + self.data_i['cc2']) * (1 / 2) m.cd = self.data_i['cd'] - + return m def label_model(self): m = self.model - + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']])]) + m.experiment_outputs.update( + [(m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']])] + ) m.experiment_outputs.update([(m.cb, [self.data_i['cb']])]) m.experiment_outputs.update([(m.cc, [self.data_i['cc1'], self.data_i['cc2']])]) m.experiment_outputs.update([(m.cd, [self.data_i['cd']])]) - + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.k1, m.k2, m.k3]) + m.unknown_parameters.update( + (k, pyo.ComponentUID(k)) for k in [m.k1, m.k2, m.k3] + ) return m @@ -60,9 +63,9 @@ def main(): file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data_multisensor.csv")) data = pd.read_csv(file_name) - + # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(MultisensorReactorDesignExperiment(data, i)) @@ -72,9 +75,9 @@ def SSE_multisensor(model): for y, yhat in model.experiment_outputs.items(): num_outputs = len(yhat) for i in range(num_outputs): - expr += ((y - yhat[i])**2) * (1 / num_outputs) + expr += ((y - yhat[i]) ** 2) * (1 / num_outputs) return expr - + # View one model # exp0_model = exp_list[0].get_labeled_model() # print(exp0_model.pprint()) diff --git a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py index 76744984cce..4da7dd13023 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py @@ -23,19 +23,19 @@ def main(): file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, "reactor_data.csv")) data = pd.read_csv(file_name) - + # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperiment(data, i)) - + # View one model # exp0_model = exp_list[0].get_labeled_model() # print(exp0_model.pprint()) pest = parmest.Estimator(exp_list, obj_function='SSE') - + # Parameter estimation with covariance obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=17) print(obj) - print(theta) \ No newline at end of file + print(theta) diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index db3b0e1d380..e524a6dd90e 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -21,19 +21,26 @@ from pyomo.contrib.parmest.experiment import Experiment + def reactor_design_model(): # Create the concrete model model = pyo.ConcreteModel() # Rate constants - model.k1 = pyo.Param(initialize=5.0 / 6.0, within=pyo.PositiveReals, mutable=True) # min^-1 - model.k2 = pyo.Param(initialize=5.0 / 3.0, within=pyo.PositiveReals, mutable=True) # min^-1 - model.k3 = pyo.Param(initialize=1.0 / 6000.0, within=pyo.PositiveReals, mutable=True) # m^3/(gmol min) + model.k1 = pyo.Param( + initialize=5.0 / 6.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k2 = pyo.Param( + initialize=5.0 / 3.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k3 = pyo.Param( + initialize=1.0 / 6000.0, within=pyo.PositiveReals, mutable=True + ) # m^3/(gmol min) # Inlet concentration of A, gmol/m^3 model.caf = pyo.Param(initialize=10000, within=pyo.PositiveReals, mutable=True) - + # Space velocity (flowrate/volume) model.sv = pyo.Param(initialize=1.0, within=pyo.PositiveReals, mutable=True) @@ -61,63 +68,68 @@ def reactor_design_model(): expr=(0 == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb) ) - model.cc_bal = pyo.Constraint(expr=(0 == -model.sv * model.cc + model.k2 * model.cb)) + model.cc_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cc + model.k2 * model.cb) + ) model.cd_bal = pyo.Constraint( expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) ) return model - + + class ReactorDesignExperiment(Experiment): - - def __init__(self, data, experiment_number): + + def __init__(self, data, experiment_number): self.data = data self.experiment_number = experiment_number - self.data_i = data.loc[experiment_number,:] + self.data_i = data.loc[experiment_number, :] self.model = None - + def create_model(self): self.model = m = reactor_design_model() return m - + def finalize_model(self): m = self.model - + # Experiment inputs values m.sv = self.data_i['sv'] m.caf = self.data_i['caf'] - + # Experiment output values m.ca = self.data_i['ca'] m.cb = self.data_i['cb'] m.cc = self.data_i['cc'] m.cd = self.data_i['cd'] - + return m def label_model(self): m = self.model - + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.experiment_outputs.update([(m.ca, self.data_i['ca'])]) m.experiment_outputs.update([(m.cb, self.data_i['cb'])]) m.experiment_outputs.update([(m.cc, self.data_i['cc'])]) m.experiment_outputs.update([(m.cd, self.data_i['cd'])]) - + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.k1, m.k2, m.k3]) + m.unknown_parameters.update( + (k, pyo.ComponentUID(k)) for k in [m.k1, m.k2, m.k3] + ) return m - + def get_labeled_model(self): m = self.create_model() m = self.finalize_model() m = self.label_model() - + return m + def main(): # For a range of sv values, return ca, cb, cc, and cd @@ -143,6 +155,6 @@ def main(): results = pd.DataFrame(results, columns=["sv", "caf", "ca", "cb", "cc", "cd"]) print(results) + if __name__ == "__main__": main() - \ No newline at end of file diff --git a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py index a9a5ab20b54..59d6a26ca23 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py @@ -20,25 +20,25 @@ class TimeSeriesReactorDesignExperiment(ReactorDesignExperiment): - def __init__(self, data, experiment_number): + def __init__(self, data, experiment_number): self.data = data self.experiment_number = experiment_number self.data_i = data[experiment_number] self.model = None - + def finalize_model(self): m = self.model - + # Experiment inputs values m.sv = self.data_i['sv'] m.caf = self.data_i['caf'] - + # Experiment output values m.ca = self.data_i['ca'][0] m.cb = self.data_i['cb'][0] m.cc = self.data_i['cc'][0] m.cd = self.data_i['cd'][0] - + return m @@ -92,7 +92,7 @@ def main(): data_ts = group_data(data, 'experiment', ['sv', 'caf']) # Create an experiment list - exp_list= [] + exp_list = [] for i in range(len(data_ts)): exp_list.append(TimeSeriesReactorDesignExperiment(data_ts, i)) @@ -102,7 +102,7 @@ def SSE_timeseries(model): for y, yhat in model.experiment_outputs.items(): num_time_points = len(yhat) for i in range(num_time_points): - expr += ((y - yhat[i])**2) * (1 / num_time_points) + expr += ((y - yhat[i]) ** 2) * (1 / num_time_points) return expr diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py index 1f15ab95779..b79bc8e4c5a 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py @@ -26,14 +26,16 @@ def main(): # Sum of squared error function def SSE(model): - expr = (model.experiment_outputs[model.y] - \ - model.response_function[model.experiment_outputs[model.hour]]) ** 2 + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): - exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose())) # View one model # exp0_model = exp_list[0].get_labeled_model() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py index 869bb39efb9..a8daac79e23 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py @@ -28,14 +28,16 @@ def main(): # Sum of squared error function def SSE(model): - expr = (model.experiment_outputs[model.y] - \ - model.response_function[model.experiment_outputs[model.hour]]) ** 2 + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): - exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose())) # View one model # exp0_model = exp_list[0].get_labeled_model() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py index b6ca7af0ab6..1f73f1cfdb0 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py @@ -26,14 +26,16 @@ def main(): # Sum of squared error function def SSE(model): - expr = (model.experiment_outputs[model.y] - \ - model.response_function[model.experiment_outputs[model.hour]]) ** 2 + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): - exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose())) # View one model # exp0_model = exp_list[0].get_labeled_model() @@ -41,7 +43,7 @@ def SSE(model): # Create an instance of the parmest estimator pest = parmest.Estimator(exp_list, obj_function=SSE) - + # Parameter estimation and covariance n = 6 # total number of data points used in the objective (y in 6 scenarios) obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py index 6e7d6219a64..2b75c8621a7 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py @@ -28,11 +28,11 @@ def rooney_biegler_model(data): model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) - + def response_rule(m, h): expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) return expr - + model.response_function = pyo.Expression(data.hour, rule=response_rule) def SSE_rule(m): @@ -63,13 +63,14 @@ def label_model(self): m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.asymptote, m.rate_constant]) + m.unknown_parameters.update( + (k, pyo.ComponentUID(k)) for k in [m.asymptote, m.rate_constant] + ) def finalize_model(self): m = self.model - + # Experiment output values m.hour = self.data.iloc[0]['hour'] m.y = self.data.iloc[0]['y'] @@ -78,7 +79,7 @@ def get_labeled_model(self): self.create_model() self.label_model() self.finalize_model() - + return self.model diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py index 1e213684a01..11100a8a40f 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py @@ -28,7 +28,7 @@ def rooney_biegler_model_with_constraint(data): model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) - + model.response_function = pyo.Var(data.hour, initialize=0.0) # changed from expression to constraint @@ -48,6 +48,7 @@ def SSE_rule(m): return model + class RooneyBieglerExperiment(Experiment): def __init__(self, data): @@ -65,15 +66,15 @@ def label_model(self): m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) - m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.asymptote, m.rate_constant]) + m.unknown_parameters.update( + (k, pyo.ComponentUID(k)) for k in [m.asymptote, m.rate_constant] + ) def finalize_model(self): m = self.model - + # Experiment output values m.hour = self.data.iloc[0]['hour'] m.y = self.data.iloc[0]['y'] @@ -82,7 +83,7 @@ def get_labeled_model(self): self.create_model() self.label_model() self.finalize_model() - + return self.model diff --git a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py index 145569f7535..d74f094cd4f 100644 --- a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py @@ -12,9 +12,8 @@ import json from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import ( - SemiBatchExperiment, -) +from pyomo.contrib.parmest.examples.semibatch.semibatch import SemiBatchExperiment + def main(): @@ -28,7 +27,7 @@ def main(): data.append(d) # Create an experiment list - exp_list= [] + exp_list = [] for i in range(len(data)): exp_list.append(SemiBatchExperiment(data[i])) @@ -40,7 +39,7 @@ def main(): # for sum of squared error that will be used in parameter estimation pest = parmest.Estimator(exp_list) - + obj, theta = pest.theta_est() print(obj) print(theta) diff --git a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py index a80a82671bc..1270ef49839 100644 --- a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py @@ -12,9 +12,7 @@ import json from os.path import join, abspath, dirname import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import ( - SemiBatchExperiment, -) +from pyomo.contrib.parmest.examples.semibatch.semibatch import SemiBatchExperiment import pyomo.contrib.parmest.scenariocreator as sc @@ -30,7 +28,7 @@ def main(): data.append(d) # Create an experiment list - exp_list= [] + exp_list = [] for i in range(len(data)): exp_list.append(SemiBatchExperiment(data[i])) diff --git a/pyomo/contrib/parmest/examples/semibatch/semibatch.py b/pyomo/contrib/parmest/examples/semibatch/semibatch.py index 3ef7bc01aa9..b882df7a015 100644 --- a/pyomo/contrib/parmest/examples/semibatch/semibatch.py +++ b/pyomo/contrib/parmest/examples/semibatch/semibatch.py @@ -283,11 +283,11 @@ def create_model(self): def label_model(self): m = self.model - - m.unknown_parameters = Suffix(direction=Suffix.LOCAL) - m.unknown_parameters.update((k, ComponentUID(k)) - for k in [m.k1, m.k2, m.E1, m.E2]) + m.unknown_parameters = Suffix(direction=Suffix.LOCAL) + m.unknown_parameters.update( + (k, ComponentUID(k)) for k in [m.k1, m.k2, m.E1, m.E2] + ) def finalize_model(self): pass @@ -296,7 +296,7 @@ def get_labeled_model(self): self.create_model() self.label_model() self.finalize_model() - + return self.model diff --git a/pyomo/contrib/parmest/experiment.py b/pyomo/contrib/parmest/experiment.py index 73b18bb5975..e16ad304e42 100644 --- a/pyomo/contrib/parmest/experiment.py +++ b/pyomo/contrib/parmest/experiment.py @@ -1,15 +1,16 @@ # The experiment class is a template for making experiment lists # to pass to parmest. An experiment is a pyomo model "m" which has # additional suffixes: -# m.experiment_outputs -- which variables are experiment outputs +# m.experiment_outputs -- which variables are experiment outputs # m.unknown_parameters -- which variables are parameters to estimate # The experiment class has only one required method: # get_labeled_model() # which returns the labeled pyomo model. + class Experiment: def __init__(self, model=None): self.model = model def get_labeled_model(self): - return self.model \ No newline at end of file + return self.model diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index e256b0f38d7..7e9e6cf90d4 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -309,10 +309,12 @@ def _experiment_instance_creation_callback( # return grouped_data + def SSE(model): - expr = sum((y - yhat)**2 for y, yhat in model.experiment_outputs.items()) + expr = sum((y - yhat) ** 2 for y, yhat in model.experiment_outputs.items()) return expr + class _SecondStageCostExpr(object): """ Class to pass objective expression into the Pyomo model @@ -332,10 +334,10 @@ class Estimator(object): Parameters ---------- experiement_list: list of Experiments - A list of experiment objects which creates one labeled model for + A list of experiment objects which creates one labeled model for each expeirment obj_function: string or function (optional) - Built in objective (currently only "SSE") or custom function used to + Built in objective (currently only "SSE") or custom function used to formulate parameter estimation objective. If no function is specified, the model is used "as is" and should be defined with a "FirstStageCost" and @@ -351,50 +353,54 @@ class Estimator(object): # backwards compatible constructor will accept the old inputs # from parmest_deprecated as well as the new inputs using experiment lists def __init__(self, *args, **kwargs): - + # check that we have at least one argument - assert(len(args) > 0) + assert len(args) > 0 # use deprecated interface self.pest_deprecated = None if callable(args[0]): - logger.warning('Using deprecated parmest inputs (model_function, ' + - 'data, theta_names), please use experiment lists instead.') + logger.warning( + 'Using deprecated parmest inputs (model_function, ' + + 'data, theta_names), please use experiment lists instead.' + ) self.pest_deprecated = parmest_deprecated.Estimator(*args, **kwargs) return # check that we have a (non-empty) list of experiments - assert (isinstance(args[0], list)) - assert (len(args[0]) > 0) + assert isinstance(args[0], list) + assert len(args[0]) > 0 self.exp_list = args[0] # check that an experiment has experiment_outputs and unknown_parameters model = self.exp_list[0].get_labeled_model() try: - outputs = [k.name for k,v in model.experiment_outputs.items()] + outputs = [k.name for k, v in model.experiment_outputs.items()] except: - RuntimeError('Experiment list model does not have suffix ' + - '"experiment_outputs".') + RuntimeError( + 'Experiment list model does not have suffix ' + '"experiment_outputs".' + ) try: - parms = [k.name for k,v in model.unknown_parameters.items()] + parms = [k.name for k, v in model.unknown_parameters.items()] except: - RuntimeError('Experiment list model does not have suffix ' + - '"unknown_parameters".') - + RuntimeError( + 'Experiment list model does not have suffix ' + '"unknown_parameters".' + ) + # populate keyword argument options self.obj_function = kwargs.get('obj_function', None) self.tee = kwargs.get('tee', False) self.diagnostic_mode = kwargs.get('diagnostic_mode', False) self.solver_options = kwargs.get('solver_options', None) - # TODO This might not be needed here. + # TODO This might not be needed here. # We could collect the union (or intersect?) of thetas when the models are built theta_names = [] for experiment in self.exp_list: model = experiment.get_labeled_model() - theta_names.extend([k.name for k,v in model.unknown_parameters.items()]) + theta_names.extend([k.name for k, v in model.unknown_parameters.items()]) self.estimator_theta_names = list(set(theta_names)) - + self._second_stage_cost_exp = "SecondStageCost" # boolean to indicate if model is initialized using a square solve self.model_initialized = False @@ -406,7 +412,7 @@ def _return_theta_names(self): # check for deprecated inputs if self.pest_deprecated is not None: - # if fitted model parameter names differ from theta_names + # if fitted model parameter names differ from theta_names # created when Estimator object is created if hasattr(self, 'theta_names_updated'): return self.pest_deprecated.theta_names_updated @@ -418,7 +424,7 @@ def _return_theta_names(self): else: - # if fitted model parameter names differ from theta_names + # if fitted model parameter names differ from theta_names # created when Estimator object is created if hasattr(self, 'theta_names_updated'): return self.theta_names_updated @@ -434,7 +440,7 @@ def _create_parmest_model(self, experiment_number): """ model = self.exp_list[experiment_number].get_labeled_model() - self.theta_names = [k.name for k,v in model.unknown_parameters.items()] + self.theta_names = [k.name for k, v in model.unknown_parameters.items()] if len(model.unknown_parameters) == 0: model.parmest_dummy_var = pyo.Var(initialize=1.0) @@ -458,7 +464,7 @@ def _create_parmest_model(self, experiment_number): # TODO, this needs to be turned a enum class of options that still support custom functions if self.obj_function == 'SSE': - second_stage_rule=_SecondStageCostExpr(SSE) + second_stage_rule = _SecondStageCostExpr(SSE) else: # A custom function uses model.experiment_outputs as data second_stage_rule = _SecondStageCostExpr(self.obj_function) @@ -466,7 +472,6 @@ def _create_parmest_model(self, experiment_number): model.FirstStageCost = pyo.Expression(expr=0) model.SecondStageCost = pyo.Expression(rule=second_stage_rule) - def TotalCost_rule(model): return model.FirstStageCost + model.SecondStageCost @@ -534,7 +539,7 @@ def _Q_opt( outer_cb_data["ThetaVals"] = ThetaVals if bootlist is not None: outer_cb_data["BootList"] = bootlist - outer_cb_data["cb_data"] = None # None is OK + outer_cb_data["cb_data"] = None # None is OK outer_cb_data["theta_names"] = self.estimator_theta_names options = {"solver": "ipopt"} @@ -581,14 +586,13 @@ def _Q_opt( for ndname, Var, solval in ef_nonants(ef): ind_vars.append(Var) # calculate the reduced hessian - ( - solve_result, - inv_red_hes, - ) = inverse_reduced_hessian.inv_reduced_hessian_barrier( - self.ef_instance, - independent_variables=ind_vars, - solver_options=self.solver_options, - tee=self.tee, + (solve_result, inv_red_hes) = ( + inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, + ) ) if self.diagnostic_mode: @@ -710,13 +714,13 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): "callback": self._instance_creation_callback, "ThetaVals": thetavals, "theta_names": self._return_theta_names(), - "cb_data": None, + "cb_data": None, } else: dummy_cb = { "callback": self._instance_creation_callback, "theta_names": self._return_theta_names(), - "cb_data": None, + "cb_data": None, } if self.diagnostic_mode: @@ -779,14 +783,10 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): if self.diagnostic_mode: print(' Experiment = ', snum) print(' First solve with special diagnostics wrapper') - ( - status_obj, - solved, - iters, - time, - regu, - ) = utils.ipopt_solve_with_stats( - instance, optimizer, max_iter=500, max_cpu_time=120 + (status_obj, solved, iters, time, regu) = ( + utils.ipopt_solve_with_stats( + instance, optimizer, max_iter=500, max_cpu_time=120 + ) ) print( " status_obj, solved, iters, time, regularization_stat = ", @@ -944,21 +944,28 @@ def theta_est( # check if we are using deprecated parmest if self.pest_deprecated is not None: return self.pest_deprecated.theta_est( - solver=solver, + solver=solver, return_values=return_values, calc_cov=calc_cov, - cov_n=cov_n) - + cov_n=cov_n, + ) + assert isinstance(solver, str) assert isinstance(return_values, list) assert isinstance(calc_cov, bool) if calc_cov: - num_unknowns = max([len(experiment.get_labeled_model().unknown_parameters) - for experiment in self.exp_list]) - assert isinstance(cov_n, int), \ - "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" - assert cov_n > num_unknowns, \ - "The number of datapoints must be greater than the number of parameters to estimate" + num_unknowns = max( + [ + len(experiment.get_labeled_model().unknown_parameters) + for experiment in self.exp_list + ] + ) + assert isinstance( + cov_n, int + ), "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" + assert ( + cov_n > num_unknowns + ), "The number of datapoints must be greater than the number of parameters to estimate" return self._Q_opt( solver=solver, @@ -1007,7 +1014,8 @@ def theta_est_bootstrap( samplesize=samplesize, replacement=replacement, seed=seed, - return_samples=return_samples) + return_samples=return_samples, + ) assert isinstance(bootstrap_samples, int) assert isinstance(samplesize, (type(None), int)) @@ -1068,10 +1076,8 @@ def theta_est_leaveNout( # check if we are using deprecated parmest if self.pest_deprecated is not None: return self.pest_deprecated.theta_est_leaveNout( - lNo, - lNo_samples=lNo_samples, - seed=seed, - return_samples=return_samples) + lNo, lNo_samples=lNo_samples, seed=seed, return_samples=return_samples + ) assert isinstance(lNo, int) assert isinstance(lNo_samples, (type(None), int)) @@ -1150,11 +1156,8 @@ def leaveNout_bootstrap_test( # check if we are using deprecated parmest if self.pest_deprecated is not None: return self.pest_deprecated.leaveNout_bootstrap_test( - lNo, - lNo_samples, - bootstrap_samples, - distribution, alphas, - seed=seed) + lNo, lNo_samples, bootstrap_samples, distribution, alphas, seed=seed + ) assert isinstance(lNo, int) assert isinstance(lNo_samples, (type(None), int)) @@ -1189,8 +1192,8 @@ def leaveNout_bootstrap_test( # expand indexed variables to get full list of thetas def _expand_indexed_unknowns(self, model_temp): - model_theta_list = [k.name for k,v in model_temp.unknown_parameters.items()] - + model_theta_list = [k.name for k, v in model_temp.unknown_parameters.items()] + # check for indexed theta items indexed_theta_list = [] for theta_i in model_theta_list: @@ -1198,7 +1201,7 @@ def _expand_indexed_unknowns(self, model_temp): var_validate = var_cuid.find_component_on(model_temp) for ind in var_validate.index_set(): if ind is not None: - indexed_theta_list.append(theta_i + '[' + str(ind) + ']') + indexed_theta_list.append(theta_i + '[' + str(ind) + ']') else: indexed_theta_list.append(theta_i) @@ -1233,15 +1236,16 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): if self.pest_deprecated is not None: return self.pest_deprecated.objective_at_theta( theta_values=theta_values, - initialize_parmest_model=initialize_parmest_model) + initialize_parmest_model=initialize_parmest_model, + ) - if len(self.estimator_theta_names) == 0: + if len(self.estimator_theta_names) == 0: pass # skip assertion if model has no fitted parameters else: # create a local instance of the pyomo model to access model variables and parameters model_temp = self._create_parmest_model(0) model_theta_list = self._expand_indexed_unknowns(model_temp) - + # # iterate over original theta_names # for theta_i in self.theta_names: # var_cuid = ComponentUID(theta_i) @@ -1354,10 +1358,8 @@ def likelihood_ratio_test( # check if we are using deprecated parmest if self.pest_deprecated is not None: return self.pest_deprecated.likelihood_ratio_test( - obj_at_theta, - obj_value, - alphas, - return_thresholds=return_thresholds) + obj_at_theta, obj_value, alphas, return_thresholds=return_thresholds + ) assert isinstance(obj_at_theta, pd.DataFrame) assert isinstance(obj_value, (int, float)) @@ -1415,10 +1417,8 @@ def confidence_region_test( # check if we are using deprecated parmest if self.pest_deprecated is not None: return self.pest_deprecated.confidence_region_test( - theta_values, - distribution, - alphas, - test_theta_values=test_theta_values) + theta_values, distribution, alphas, test_theta_values=test_theta_values + ) assert isinstance(theta_values, pd.DataFrame) assert distribution in ['Rect', 'MVN', 'KDE'] diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index c48ac2bf027..82d13ce2007 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -17,8 +17,10 @@ import pyomo.contrib.parmest.deprecated.scenariocreator as scen_deprecated import logging + logger = logging.getLogger(__name__) + class ScenarioSet(object): """ Class to hold scenario sets @@ -127,10 +129,13 @@ def __init__(self, pest, solvername): # is this a deprecated pest object? self.scen_deprecated = None if pest.pest_deprecated is not None: - logger.warning("Using a deprecated parmest object for scenario " + - "creator, please recreate object using experiment lists.") + logger.warning( + "Using a deprecated parmest object for scenario " + + "creator, please recreate object using experiment lists." + ) self.scen_deprecated = scen_deprecated.ScenarioCreator( - pest.pest_deprecated, solvername) + pest.pest_deprecated, solvername + ) else: self.pest = pest self.solvername = solvername @@ -148,7 +153,7 @@ def ScenariosFromExperiments(self, addtoSet): if self.scen_deprecated is not None: self.scen_deprecated.ScenariosFromExperiments(addtoSet) return - + assert isinstance(addtoSet, ScenarioSet) scenario_numbers = list(range(len(self.pest.exp_list))) @@ -156,9 +161,7 @@ def ScenariosFromExperiments(self, addtoSet): prob = 1.0 / len(scenario_numbers) for exp_num in scenario_numbers: ##print("Experiment number=", exp_num) - model = self.pest._instance_creation_callback( - exp_num, - ) + model = self.pest._instance_creation_callback(exp_num) opt = pyo.SolverFactory(self.solvername) results = opt.solve(model) # solves and updates model ## pyo.check_termination_optimal(results) @@ -180,8 +183,7 @@ def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): # check if using deprecated pest object if self.scen_deprecated is not None: - self.scen_deprecated.ScenariosFromBootstrap( - addtoSet, numtomake, seed=seed) + self.scen_deprecated.ScenariosFromBootstrap(addtoSet, numtomake, seed=seed) return assert isinstance(addtoSet, ScenarioSet) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index b88871e0dbc..ff8d1663bc9 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -47,6 +47,7 @@ testdir = os.path.dirname(os.path.abspath(__file__)) + @unittest.skipIf( not parmest.parmest_available, "Cannot test parmest: required dependencies are missing", @@ -66,14 +67,18 @@ def setUp(self): # Sum of squared error function def SSE(model): - expr = (model.experiment_outputs[model.y] - \ - model.response_function[model.experiment_outputs[model.hour]]) ** 2 + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): - exp_list.append(RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose())) + exp_list.append( + RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose()) + ) # Create an instance of the parmest estimator pest = parmest.Estimator(exp_list, obj_function=SSE) @@ -82,10 +87,7 @@ def SSE(model): self.data = data self.pest = parmest.Estimator( - exp_list, - obj_function=SSE, - solver_options=solver_options, - tee=True, + exp_list, obj_function=SSE, solver_options=solver_options, tee=True ) def test_theta_est(self): @@ -372,7 +374,7 @@ def rooney_biegler_params(data): model.hour = pyo.Param(within=pyo.PositiveReals, mutable=True) model.y = pyo.Param(within=pyo.PositiveReals, mutable=True) - + def response_rule(m, h): expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) return expr @@ -389,7 +391,9 @@ def create_model(self): rooney_biegler_params_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_params_exp_list.append( - RooneyBieglerExperimentParams(self.data.loc[i,:].to_frame().transpose()) + RooneyBieglerExperimentParams( + self.data.loc[i, :].to_frame().transpose() + ) ) def rooney_biegler_indexed_params(data): @@ -429,13 +433,14 @@ def label_model(self): m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.theta]) + m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) rooney_biegler_indexed_params_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_indexed_params_exp_list.append( - RooneyBieglerExperimentIndexedParams(self.data.loc[i,:].to_frame().transpose()) + RooneyBieglerExperimentIndexedParams( + self.data.loc[i, :].to_frame().transpose() + ) ) def rooney_biegler_vars(data): @@ -465,7 +470,7 @@ def create_model(self): rooney_biegler_vars_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_vars_exp_list.append( - RooneyBieglerExperimentVars(self.data.loc[i,:].to_frame().transpose()) + RooneyBieglerExperimentVars(self.data.loc[i, :].to_frame().transpose()) ) def rooney_biegler_indexed_vars(data): @@ -475,9 +480,7 @@ def rooney_biegler_indexed_vars(data): model.theta = pyo.Var( model.var_names, initialize={"asymptote": 15, "rate_constant": 0.5} ) - model.theta[ - "asymptote" - ].fixed = ( + model.theta["asymptote"].fixed = ( True # parmest will unfix theta variables, even when they are indexed ) model.theta["rate_constant"].fixed = True @@ -509,20 +512,22 @@ def label_model(self): m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.theta]) - + m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) rooney_biegler_indexed_vars_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_indexed_vars_exp_list.append( - RooneyBieglerExperimentIndexedVars(self.data.loc[i,:].to_frame().transpose()) + RooneyBieglerExperimentIndexedVars( + self.data.loc[i, :].to_frame().transpose() + ) ) # Sum of squared error function def SSE(model): - expr = (model.experiment_outputs[model.y] - \ - model.response_function[model.experiment_outputs[model.hour]]) ** 2 + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr self.objective_function = SSE @@ -570,13 +575,14 @@ def SSE(model): not parmest.inverse_reduced_hessian_available, "Cannot test covariance matrix: required ASL dependency is missing", ) - def check_rooney_biegler_results(self, objval, cov): # get indices in covariance matrix cov_cols = cov.columns.to_list() asymptote_index = [idx for idx, s in enumerate(cov_cols) if 'asymptote' in s][0] - rate_constant_index = [idx for idx, s in enumerate(cov_cols) if 'rate_constant' in s][0] + rate_constant_index = [ + idx for idx, s in enumerate(cov_cols) if 'rate_constant' in s + ][0] self.assertAlmostEqual(objval, 4.3317112, places=2) self.assertAlmostEqual( @@ -596,8 +602,7 @@ def test_parmest_basics(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], - obj_function=self.objective_function, + parmest_input["exp_list"], obj_function=self.objective_function ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) @@ -608,10 +613,9 @@ def test_parmest_basics(self): def test_parmest_basics_with_initialize_parmest_model_option(self): - for model_type, parmest_input in self.input.items(): + for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], - obj_function=self.objective_function, + parmest_input["exp_list"], obj_function=self.objective_function ) objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) @@ -627,8 +631,7 @@ def test_parmest_basics_with_square_problem_solve(self): for model_type, parmest_input in self.input.items(): pest = parmest.Estimator( - parmest_input["exp_list"], - obj_function=self.objective_function, + parmest_input["exp_list"], obj_function=self.objective_function ) obj_at_theta = pest.objective_at_theta( @@ -643,10 +646,9 @@ def test_parmest_basics_with_square_problem_solve(self): def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): for model_type, parmest_input in self.input.items(): - + pest = parmest.Estimator( - parmest_input["exp_list"], - obj_function=self.objective_function, + parmest_input["exp_list"], obj_function=self.objective_function ) obj_at_theta = pest.objective_at_theta(initialize_parmest_model=True) @@ -654,6 +656,7 @@ def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) self.check_rooney_biegler_results(objval, cov) + @unittest.skipIf( not parmest.parmest_available, "Cannot test parmest: required dependencies are missing", @@ -692,13 +695,15 @@ def setUp(self): ) # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperiment(data, i)) solver_options = {"max_iter": 6000} - self.pest = parmest.Estimator(exp_list, obj_function='SSE', solver_options=solver_options) + self.pest = parmest.Estimator( + exp_list, obj_function='SSE', solver_options=solver_options + ) def test_theta_est(self): # used in data reconciliation @@ -820,15 +825,16 @@ def create_model(self): def label_model(self): m = self.model - + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.ComponentUID(k)) - for k in [m.k1, m.k2]) + m.unknown_parameters.update( + (k, pyo.ComponentUID(k)) for k in [m.k1, m.k2] + ) def get_labeled_model(self): self.create_model() self.label_model() - + return self.model # This example tests data formatted in 3 ways @@ -873,10 +879,14 @@ def get_labeled_model(self): self.pest_dict = parmest.Estimator(exp_list_dict) # Estimator object with multiple scenarios - exp_list_df_multiple = [ReactorDesignExperimentDAE(data_df), - ReactorDesignExperimentDAE(data_df)] - exp_list_dict_multiple = [ReactorDesignExperimentDAE(data_dict), - ReactorDesignExperimentDAE(data_dict)] + exp_list_df_multiple = [ + ReactorDesignExperimentDAE(data_df), + ReactorDesignExperimentDAE(data_df), + ] + exp_list_dict_multiple = [ + ReactorDesignExperimentDAE(data_dict), + ReactorDesignExperimentDAE(data_dict), + ] self.pest_df_multiple = parmest.Estimator(exp_list_df_multiple) self.pest_dict_multiple = parmest.Estimator(exp_list_dict_multiple) @@ -963,27 +973,26 @@ def setUp(self): data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], columns=["hour", "y"], ) - + # Sum of squared error function def SSE(model): - expr = (model.experiment_outputs[model.y] - \ - model.response_function[model.experiment_outputs[model.hour]]) ** 2 + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr exp_list = [] for i in range(data.shape[0]): exp_list.append( - RooneyBieglerExperiment(data.loc[i,:].to_frame().transpose()) + RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose()) ) solver_options = {"tol": 1e-8} self.data = data self.pest = parmest.Estimator( - exp_list, - obj_function=SSE, - solver_options=solver_options, - tee=True, + exp_list, obj_function=SSE, solver_options=solver_options, tee=True ) def test_theta_est_with_square_initialization(self): diff --git a/pyomo/contrib/parmest/tests/test_scenariocreator.py b/pyomo/contrib/parmest/tests/test_scenariocreator.py index bf6fa12b8b1..0c0976a453f 100644 --- a/pyomo/contrib/parmest/tests/test_scenariocreator.py +++ b/pyomo/contrib/parmest/tests/test_scenariocreator.py @@ -65,9 +65,9 @@ def setUp(self): ], columns=["sv", "caf", "ca", "cb", "cc", "cd"], ) - + # Create an experiment list - exp_list= [] + exp_list = [] for i in range(data.shape[0]): exp_list.append(ReactorDesignExperiment(data, i)) @@ -123,7 +123,7 @@ def setUp(self): # for the sum of squared error that will be used in parameter estimation # Create an experiment list - exp_list= [] + exp_list = [] for i in range(len(data)): exp_list.append(sb.SemiBatchExperiment(data[i])) From 70d66e6d2fba6d6d2ca3b9ab5f347ca38c3176ef Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 12 Feb 2024 12:27:22 -0700 Subject: [PATCH 0510/3044] Fixed typo parmest.py. --- pyomo/contrib/parmest/parmest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 7e9e6cf90d4..ccdec527c06 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -333,7 +333,7 @@ class Estimator(object): Parameters ---------- - experiement_list: list of Experiments + experiment_list: list of Experiments A list of experiment objects which creates one labeled model for each expeirment obj_function: string or function (optional) From 922c715013e62553f5551b338f6d6cd490358bb3 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 12 Feb 2024 12:32:19 -0700 Subject: [PATCH 0511/3044] Another typo in parmest.py. --- pyomo/contrib/parmest/parmest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ccdec527c06..2e44b278423 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -381,7 +381,7 @@ def __init__(self, *args, **kwargs): 'Experiment list model does not have suffix ' + '"experiment_outputs".' ) try: - parms = [k.name for k, v in model.unknown_parameters.items()] + params = [k.name for k, v in model.unknown_parameters.items()] except: RuntimeError( 'Experiment list model does not have suffix ' + '"unknown_parameters".' From 9a208616cc54f844a41effa42bb60f788b8ad4ff Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 12 Feb 2024 14:51:47 -0700 Subject: [PATCH 0512/3044] NFC: fix comment typo --- pyomo/core/base/set.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index f1a24b8fd03..6b850a6366e 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -2958,7 +2958,7 @@ def __init__(self, *args, **kwds): pass def __str__(self): - # Named, components should return their name e.g., Reals + # Named components should return their name e.g., Reals if self._name is not None: return self.name # Unconstructed floating components return their type From b90fb0c1b18ac14d15121c857f344ac1875f32a7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 12 Feb 2024 17:40:35 -0700 Subject: [PATCH 0513/3044] NFC: add docstrings, reformat long lines --- pyomo/contrib/fbbt/interval.py | 197 +++++++++++++++++++++------------ 1 file changed, 129 insertions(+), 68 deletions(-) diff --git a/pyomo/contrib/fbbt/interval.py b/pyomo/contrib/fbbt/interval.py index 53c236850d9..339d547b7d4 100644 --- a/pyomo/contrib/fbbt/interval.py +++ b/pyomo/contrib/fbbt/interval.py @@ -58,6 +58,14 @@ def Bool(val): def ineq(xl, xu, yl, yu): + """Compute the "bounds" on an InequalityExpression + + Note this is *not* performing interval arithmetic: we are + calculating the "bounds" on a RelationalExpression (whose domain is + {True, False}). Therefore we are determining if `x` can be less + than `y`, `x` can not be less than `y`, or both. + + """ ans = [] if yl < xu: ans.append(_false) @@ -70,6 +78,14 @@ def ineq(xl, xu, yl, yu): def eq(xl, xu, yl, yu): + """Compute the "bounds" on an EqualityExpression + + Note this is *not* performing interval arithmetic: we are + calculating the "bounds" on a RelationalExpression (whose domain is + {True, False}). Therefore we are determining if `x` can be equal to + `y`, `x` can not be equal to `y`, or both. + + """ ans = [] if xl != xu or yl != yu or xl != yl: ans.append(_false) @@ -82,6 +98,14 @@ def eq(xl, xu, yl, yu): def ranged(xl, xu, yl, yu, zl, zu): + """Compute the "bounds" on a RangedExpression + + Note this is *not* performing interval arithmetic: we are + calculating the "bounds" on a RelationalExpression (whose domain is + {True, False}). Therefore we are determining if `y` can be between + `z` and `z`, `y` can be outside the range `x` and `z`, or both. + + """ lb = ineq(xl, xu, yl, yu) ub = ineq(yl, yu, zl, zu) ans = [] @@ -128,12 +152,18 @@ def mul(xl, xu, yl, yu): def inv(xl, xu, feasibility_tol): - """ - The case where xl is very slightly positive but should be very slightly negative (or xu is very slightly negative - but should be very slightly positive) should not be an issue. Suppose xu is 2 and xl is 1e-15 but should be -1e-15. - The bounds obtained from this function will be [0.5, 1e15] or [0.5, inf), depending on the value of - feasibility_tol. The true bounds are (-inf, -1e15] U [0.5, inf), where U is union. The exclusion of (-inf, -1e15] - should be acceptable. Additionally, it very important to return a non-negative interval when xl is non-negative. + """Compute the inverse of an interval + + The case where xl is very slightly positive but should be very + slightly negative (or xu is very slightly negative but should be + very slightly positive) should not be an issue. Suppose xu is 2 and + xl is 1e-15 but should be -1e-15. The bounds obtained from this + function will be [0.5, 1e15] or [0.5, inf), depending on the value + of feasibility_tol. The true bounds are (-inf, -1e15] U [0.5, inf), + where U is union. The exclusion of (-inf, -1e15] should be + acceptable. Additionally, it very important to return a non-negative + interval when xl is non-negative. + """ if xu - xl <= -feasibility_tol: raise InfeasibleConstraintException( @@ -178,9 +208,8 @@ def power(xl, xu, yl, yu, feasibility_tol): Compute bounds on x**y. """ if xl > 0: - """ - If x is always positive, things are simple. We only need to worry about the sign of y. - """ + # If x is always positive, things are simple. We only need to + # worry about the sign of y. if yl < 0 < yu: lb = min(xu**yl, xl**yu) ub = max(xl**yl, xu**yu) @@ -270,14 +299,15 @@ def power(xl, xu, yl, yu, feasibility_tol): def _inverse_power1(zl, zu, yl, yu, orig_xl, orig_xu, feasibility_tol): - """ - z = x**y => compute bounds on x. + """z = x**y => compute bounds on x. First, start by computing bounds on x with x = exp(ln(z) / y) - However, if y is an integer, then x can be negative, so there are several special cases. See the docs below. + However, if y is an integer, then x can be negative, so there are + several special cases. See the docs below. + """ xl, xu = log(zl, zu) xl, xu = div(xl, xu, yl, yu, feasibility_tol) @@ -288,22 +318,31 @@ def _inverse_power1(zl, zu, yl, yu, orig_xl, orig_xu, feasibility_tol): y = yl if y == 0: # Anything to the power of 0 is 1, so if y is 0, then x can be anything - # (assuming zl <= 1 <= zu, which is enforced when traversing the tree in the other direction) + # (assuming zl <= 1 <= zu, which is enforced when traversing + # the tree in the other direction) xl = -inf xu = inf elif y % 2 == 0: - """ - if y is even, then there are two primary cases (note that it is much easier to walk through these - while looking at plots): + """if y is even, then there are two primary cases (note that it is much + easier to walk through these while looking at plots): + case 1: y is positive - x**y is convex, positive, and symmetric. The bounds on x depend on the lower bound of z. If zl <= 0, - then xl should simply be -xu. However, if zl > 0, then we may be able to say something better. For - example, if the original lower bound on x is positive, then we can keep xl computed from - x = exp(ln(z) / y). Furthermore, if the original lower bound on x is larger than -xl computed from - x = exp(ln(z) / y), then we can still keep the xl computed from x = exp(ln(z) / y). Similar logic - applies to the upper bound of x. + + x**y is convex, positive, and symmetric. The bounds on x + depend on the lower bound of z. If zl <= 0, then xl + should simply be -xu. However, if zl > 0, then we may be + able to say something better. For example, if the + original lower bound on x is positive, then we can keep + xl computed from x = exp(ln(z) / y). Furthermore, if the + original lower bound on x is larger than -xl computed + from x = exp(ln(z) / y), then we can still keep the xl + computed from x = exp(ln(z) / y). Similar logic applies + to the upper bound of x. + case 2: y is negative + The ideas are similar to case 1. + """ if zu + feasibility_tol < 0: raise InfeasibleConstraintException( @@ -351,16 +390,25 @@ def _inverse_power1(zl, zu, yl, yu, orig_xl, orig_xu, feasibility_tol): xl = _xl xu = _xu else: # y % 2 == 1 - """ - y is odd. + """y is odd. + Case 1: y is positive - x**y is monotonically increasing. If y is positive, then we can can compute the bounds on x using - x = z**(1/y) and the signs on xl and xu depend on the signs of zl and zu. + + x**y is monotonically increasing. If y is positive, then + we can can compute the bounds on x using x = z**(1/y) + and the signs on xl and xu depend on the signs of zl and + zu. + Case 2: y is negative - Again, this is easier to visualize with a plot. x**y approaches zero when x approaches -inf or inf. - Thus, if zl < 0 < zu, then no bounds can be inferred for x. If z is positive (zl >=0 ) then we can - use the bounds computed from x = exp(ln(z) / y). If z is negative (zu <= 0), then we live in the - bottom left quadrant, xl depends on zu, and xu depends on zl. + + Again, this is easier to visualize with a plot. x**y + approaches zero when x approaches -inf or inf. Thus, if + zl < 0 < zu, then no bounds can be inferred for x. If z + is positive (zl >=0 ) then we can use the bounds + computed from x = exp(ln(z) / y). If z is negative (zu + <= 0), then we live in the bottom left quadrant, xl + depends on zu, and xu depends on zl. + """ if y > 0: xl = abs(zl) ** (1.0 / y) @@ -387,12 +435,13 @@ def _inverse_power1(zl, zu, yl, yu, orig_xl, orig_xu, feasibility_tol): def _inverse_power2(zl, zu, xl, xu, feasiblity_tol): - """ - z = x**y => compute bounds on y + """z = x**y => compute bounds on y y = ln(z) / ln(x) - This function assumes the exponent can be fractional, so x must be positive. This method should not be called - if the exponent is an integer. + This function assumes the exponent can be fractional, so x must be + positive. This method should not be called if the exponent is an + integer. + """ if xu <= 0: raise IntervalException( @@ -480,10 +529,12 @@ def sin(xl, xu): ub: float """ - # if there is a minimum between xl and xu, then the lower bound is -1. Minimums occur at 2*pi*n - pi/2 - # find the minimum value of i such that 2*pi*i - pi/2 >= xl. Then round i up. If 2*pi*i - pi/2 is still less - # than or equal to xu, then there is a minimum between xl and xu. Thus the lb is -1. Otherwise, the minimum - # occurs at either xl or xu + # if there is a minimum between xl and xu, then the lower bound is + # -1. Minimums occur at 2*pi*n - pi/2 find the minimum value of i + # such that 2*pi*i - pi/2 >= xl. Then round i up. If 2*pi*i - pi/2 + # is still less than or equal to xu, then there is a minimum between + # xl and xu. Thus the lb is -1. Otherwise, the minimum occurs at + # either xl or xu if xl <= -inf or xu >= inf: return -1, 1 pi = math.pi @@ -495,7 +546,8 @@ def sin(xl, xu): else: lb = min(math.sin(xl), math.sin(xu)) - # if there is a maximum between xl and xu, then the upper bound is 1. Maximums occur at 2*pi*n + pi/2 + # if there is a maximum between xl and xu, then the upper bound is + # 1. Maximums occur at 2*pi*n + pi/2 i = (xu - pi / 2) / (2 * pi) i = math.floor(i) x_at_max = 2 * pi * i + pi / 2 @@ -521,10 +573,12 @@ def cos(xl, xu): ub: float """ - # if there is a minimum between xl and xu, then the lower bound is -1. Minimums occur at 2*pi*n - pi - # find the minimum value of i such that 2*pi*i - pi >= xl. Then round i up. If 2*pi*i - pi/2 is still less - # than or equal to xu, then there is a minimum between xl and xu. Thus the lb is -1. Otherwise, the minimum - # occurs at either xl or xu + # if there is a minimum between xl and xu, then the lower bound is + # -1. Minimums occur at 2*pi*n - pi find the minimum value of i such + # that 2*pi*i - pi >= xl. Then round i up. If 2*pi*i - pi/2 is still + # less than or equal to xu, then there is a minimum between xl and + # xu. Thus the lb is -1. Otherwise, the minimum occurs at either xl + # or xu if xl <= -inf or xu >= inf: return -1, 1 pi = math.pi @@ -536,7 +590,8 @@ def cos(xl, xu): else: lb = min(math.cos(xl), math.cos(xu)) - # if there is a maximum between xl and xu, then the upper bound is 1. Maximums occur at 2*pi*n + # if there is a maximum between xl and xu, then the upper bound is + # 1. Maximums occur at 2*pi*n i = (xu) / (2 * pi) i = math.floor(i) x_at_max = 2 * pi * i @@ -562,10 +617,12 @@ def tan(xl, xu): ub: float """ - # tan goes to -inf and inf at every pi*i + pi/2 (integer i). If one of these values is between xl and xu, then - # the lb is -inf and the ub is inf. Otherwise the minimum occurs at xl and the maximum occurs at xu. - # find the minimum value of i such that pi*i + pi/2 >= xl. Then round i up. If pi*i + pi/2 is still less - # than or equal to xu, then there is an undefined point between xl and xu. + # tan goes to -inf and inf at every pi*i + pi/2 (integer i). If one + # of these values is between xl and xu, then the lb is -inf and the + # ub is inf. Otherwise the minimum occurs at xl and the maximum + # occurs at xu. find the minimum value of i such that pi*i + pi/2 + # >= xl. Then round i up. If pi*i + pi/2 is still less than or equal + # to xu, then there is an undefined point between xl and xu. if xl <= -inf or xu >= inf: return -inf, inf pi = math.pi @@ -609,12 +666,12 @@ def asin(xl, xu, yl, yu, feasibility_tol): if yl <= -inf: lb = yl elif xl <= math.sin(yl) <= xu: - # if sin(yl) >= xl then yl satisfies the bounds on x, and the lower bound of y cannot be improved + # if sin(yl) >= xl then yl satisfies the bounds on x, and the + # lower bound of y cannot be improved lb = yl elif math.sin(yl) < xl: - """ - we can only push yl up from its current value to the next lowest value such that xl = sin(y). In other words, - we need to + """we can only push yl up from its current value to the next lowest + value such that xl = sin(y). In other words, we need to min y s.t. @@ -622,19 +679,21 @@ def asin(xl, xu, yl, yu, feasibility_tol): y >= yl globally. + """ - # first find the next minimum of x = sin(y). Minimums occur at y = 2*pi*n - pi/2 for integer n. + # first find the next minimum of x = sin(y). Minimums occur at y + # = 2*pi*n - pi/2 for integer n. i = (yl + pi / 2) / (2 * pi) i1 = math.floor(i) i2 = math.ceil(i) i1 = 2 * pi * i1 - pi / 2 i2 = 2 * pi * i2 - pi / 2 - # now find the next value of y such that xl = sin(y). This can be computed by a distance from the minimum (i). + # now find the next value of y such that xl = sin(y). This can + # be computed by a distance from the minimum (i). y_tmp = math.asin(xl) # this will give me a value between -pi/2 and pi/2 - dist = y_tmp - ( - -pi / 2 - ) # this is the distance between the minimum of the sin function and a value that - # satisfies xl = sin(y) + dist = y_tmp - (-pi / 2) + # this is the distance between the minimum of the sin function + # and a value that satisfies xl = sin(y) lb1 = i1 + dist lb2 = i2 + dist if lb1 >= yl - feasibility_tol: @@ -722,12 +781,12 @@ def acos(xl, xu, yl, yu, feasibility_tol): if yl <= -inf: lb = yl elif xl <= math.cos(yl) <= xu: - # if xl <= cos(yl) <= xu then yl satisfies the bounds on x, and the lower bound of y cannot be improved + # if xl <= cos(yl) <= xu then yl satisfies the bounds on x, and + # the lower bound of y cannot be improved lb = yl elif math.cos(yl) < xl: - """ - we can only push yl up from its current value to the next lowest value such that xl = cos(y). In other words, - we need to + """we can only push yl up from its current value to the next lowest + value such that xl = cos(y). In other words, we need to min y s.t. @@ -735,19 +794,21 @@ def acos(xl, xu, yl, yu, feasibility_tol): y >= yl globally. + """ - # first find the next minimum of x = cos(y). Minimums occur at y = 2*pi*n - pi for integer n. + # first find the next minimum of x = cos(y). Minimums occur at y + # = 2*pi*n - pi for integer n. i = (yl + pi) / (2 * pi) i1 = math.floor(i) i2 = math.ceil(i) i1 = 2 * pi * i1 - pi i2 = 2 * pi * i2 - pi - # now find the next value of y such that xl = cos(y). This can be computed by a distance from the minimum (i). + # now find the next value of y such that xl = cos(y). This can + # be computed by a distance from the minimum (i). y_tmp = math.acos(xl) # this will give me a value between 0 and pi - dist = ( - pi - y_tmp - ) # this is the distance between the minimum of the sin function and a value that - # satisfies xl = sin(y) + dist = pi - y_tmp + # this is the distance between the minimum of the sin function + # and a value that satisfies xl = sin(y) lb1 = i1 + dist lb2 = i2 + dist if lb1 >= yl - feasibility_tol: From 3d14132f02340a928c63a78c2c23d2358968f140 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 12 Feb 2024 17:42:41 -0700 Subject: [PATCH 0514/3044] Make bool_ private; rename Bool -> BoolFlag so usage doesn't look like a bool --- pyomo/contrib/fbbt/expression_bounds_walker.py | 6 +++--- pyomo/contrib/fbbt/interval.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index a32d138c52b..340af94c83e 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -13,7 +13,7 @@ from math import pi from pyomo.common.collections import ComponentMap from pyomo.contrib.fbbt.interval import ( - Bool, + BoolFlag, eq, ineq, ranged, @@ -81,7 +81,7 @@ def _before_native_numeric(visitor, child): @staticmethod def _before_native_logical(visitor, child): - return False, (Bool(child), Bool(child)) + return False, (BoolFlag(child), BoolFlag(child)) @staticmethod def _before_var(visitor, child): @@ -266,7 +266,7 @@ def unexpected_expression_type(self, visitor, node, *args): if isinstance(node, NumericExpression): ans = -inf, inf elif isinstance(node, BooleanExpression): - ans = Bool(False), Bool(True) + ans = BoolFlag(False), BoolFlag(True) else: super().unexpected_expression_type(visitor, node, *args) logger.warning( diff --git a/pyomo/contrib/fbbt/interval.py b/pyomo/contrib/fbbt/interval.py index 339d547b7d4..8bebe128988 100644 --- a/pyomo/contrib/fbbt/interval.py +++ b/pyomo/contrib/fbbt/interval.py @@ -17,7 +17,7 @@ inf = float('inf') -class bool_(object): +class _bool_flag(object): def __init__(self, val): self._val = val @@ -49,11 +49,11 @@ def __repr__(self): __rpow__ = _op -_true = bool_(True) -_false = bool_(False) +_true = _bool_flag(True) +_false = _bool_flag(False) -def Bool(val): +def BoolFlag(val): return _true if val else _false From dac2f31c38d8be12f629f4fb5e322d564579696b Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 13 Feb 2024 09:32:54 -0500 Subject: [PATCH 0515/3044] fix lbb solve_data bug --- pyomo/contrib/gdpopt/branch_and_bound.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/gdpopt/branch_and_bound.py b/pyomo/contrib/gdpopt/branch_and_bound.py index 26dc2b5f2eb..645e3564a88 100644 --- a/pyomo/contrib/gdpopt/branch_and_bound.py +++ b/pyomo/contrib/gdpopt/branch_and_bound.py @@ -230,12 +230,12 @@ def _solve_gdp(self, model, config): no_feasible_soln = float('inf') self.LB = ( node_data.obj_lb - if solve_data.objective_sense == minimize + if self.objective_sense == minimize else -no_feasible_soln ) self.UB = ( no_feasible_soln - if solve_data.objective_sense == minimize + if self.objective_sense == minimize else -node_data.obj_lb ) config.logger.info( From e893381c55a29c6b1c4a2deaece4808651638417 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Feb 2024 08:36:48 -0700 Subject: [PATCH 0516/3044] Updating OnlineDocs examples to reflext anonymous sets --- doc/OnlineDocs/src/data/table2.txt | 9 ++-- doc/OnlineDocs/src/data/table3.txt | 9 ++-- doc/OnlineDocs/src/data/table3.ul.txt | 9 ++-- .../src/dataportal/param_initialization.txt | 14 ++---- .../src/dataportal/set_initialization.txt | 9 ++-- doc/OnlineDocs/src/kernel/examples.txt | 45 +++++-------------- 6 files changed, 26 insertions(+), 69 deletions(-) diff --git a/doc/OnlineDocs/src/data/table2.txt b/doc/OnlineDocs/src/data/table2.txt index 60eb55aab4a..a710b6b6042 100644 --- a/doc/OnlineDocs/src/data/table2.txt +++ b/doc/OnlineDocs/src/data/table2.txt @@ -1,13 +1,10 @@ -3 Set Declarations +2 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'A1', 'A2', 'A3'} B : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'B1', 'B2', 'B3'} - N_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 'B1'), ('A1', 'B2'), ('A1', 'B3'), ('A2', 'B1'), ('A2', 'B2'), ('A2', 'B3'), ('A3', 'B1'), ('A3', 'B2'), ('A3', 'B3')} 2 Param Declarations M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False @@ -15,10 +12,10 @@ A1 : 4.3 A2 : 4.4 A3 : 4.5 - N : Size=3, Index=N_index, Domain=Any, Default=None, Mutable=False + N : Size=3, Index=A*B, Domain=Any, Default=None, Mutable=False Key : Value ('A1', 'B1') : 5.3 ('A2', 'B2') : 5.4 ('A3', 'B3') : 5.5 -5 Declarations: A B M N_index N +4 Declarations: A B M N diff --git a/doc/OnlineDocs/src/data/table3.txt b/doc/OnlineDocs/src/data/table3.txt index cb5e63b30d4..c0c61cd5a5b 100644 --- a/doc/OnlineDocs/src/data/table3.txt +++ b/doc/OnlineDocs/src/data/table3.txt @@ -1,13 +1,10 @@ -4 Set Declarations +3 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'A1', 'A2', 'A3'} B : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'B1', 'B2', 'B3'} - N_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 'B1'), ('A1', 'B2'), ('A1', 'B3'), ('A2', 'B1'), ('A2', 'B2'), ('A2', 'B3'), ('A3', 'B1'), ('A3', 'B2'), ('A3', 'B3')} Z : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} @@ -18,10 +15,10 @@ A1 : 4.3 A2 : 4.4 A3 : 4.5 - N : Size=3, Index=N_index, Domain=Any, Default=None, Mutable=False + N : Size=3, Index=A*B, Domain=Any, Default=None, Mutable=False Key : Value ('A1', 'B1') : 5.3 ('A2', 'B2') : 5.4 ('A3', 'B3') : 5.5 -6 Declarations: A B Z M N_index N +5 Declarations: A B Z M N diff --git a/doc/OnlineDocs/src/data/table3.ul.txt b/doc/OnlineDocs/src/data/table3.ul.txt index cb5e63b30d4..c0c61cd5a5b 100644 --- a/doc/OnlineDocs/src/data/table3.ul.txt +++ b/doc/OnlineDocs/src/data/table3.ul.txt @@ -1,13 +1,10 @@ -4 Set Declarations +3 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'A1', 'A2', 'A3'} B : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'B1', 'B2', 'B3'} - N_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*B : 9 : {('A1', 'B1'), ('A1', 'B2'), ('A1', 'B3'), ('A2', 'B1'), ('A2', 'B2'), ('A2', 'B3'), ('A3', 'B1'), ('A3', 'B2'), ('A3', 'B3')} Z : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} @@ -18,10 +15,10 @@ A1 : 4.3 A2 : 4.4 A3 : 4.5 - N : Size=3, Index=N_index, Domain=Any, Default=None, Mutable=False + N : Size=3, Index=A*B, Domain=Any, Default=None, Mutable=False Key : Value ('A1', 'B1') : 5.3 ('A2', 'B2') : 5.4 ('A3', 'B3') : 5.5 -6 Declarations: A B Z M N_index N +5 Declarations: A B Z M N diff --git a/doc/OnlineDocs/src/dataportal/param_initialization.txt b/doc/OnlineDocs/src/dataportal/param_initialization.txt index fec8a06a84a..49ea105f120 100644 --- a/doc/OnlineDocs/src/dataportal/param_initialization.txt +++ b/doc/OnlineDocs/src/dataportal/param_initialization.txt @@ -1,24 +1,16 @@ -2 Set Declarations - b_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - c_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - 3 Param Declarations a : Size=1, Index=None, Domain=Any, Default=None, Mutable=False Key : Value None : 1.1 - b : Size=3, Index=b_index, Domain=Any, Default=None, Mutable=False + b : Size=3, Index={1, 2, 3}, Domain=Any, Default=None, Mutable=False Key : Value 1 : 1 2 : 2 3 : 3 - c : Size=3, Index=c_index, Domain=Any, Default=None, Mutable=False + c : Size=3, Index={1, 2, 3}, Domain=Any, Default=None, Mutable=False Key : Value 1 : 1 2 : 2 3 : 3 -5 Declarations: a b_index b c_index c +3 Declarations: a b c diff --git a/doc/OnlineDocs/src/dataportal/set_initialization.txt b/doc/OnlineDocs/src/dataportal/set_initialization.txt index c6be448eba9..3c2960ce4ef 100644 --- a/doc/OnlineDocs/src/dataportal/set_initialization.txt +++ b/doc/OnlineDocs/src/dataportal/set_initialization.txt @@ -1,6 +1,6 @@ WARNING: Initializing ordered Set B with a fundamentally unordered data source (type: set). This WILL potentially lead to nondeterministic behavior in Pyomo -9 Set Declarations +8 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {2, 3, 5} @@ -22,13 +22,10 @@ WARNING: Initializing ordered Set B with a fundamentally unordered data source G : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {2, 3, 5} - H : Size=3, Index=H_index, Ordered=Insertion + H : Size=3, Index={2, 3, 4}, Ordered=Insertion Key : Dimen : Domain : Size : Members 2 : 1 : Any : 3 : {1, 3, 5} 3 : 1 : Any : 3 : {2, 4, 6} 4 : 1 : Any : 3 : {3, 5, 7} - H_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {2, 3, 4} -9 Declarations: A B C D E F G H_index H +8 Declarations: A B C D E F G H diff --git a/doc/OnlineDocs/src/kernel/examples.txt b/doc/OnlineDocs/src/kernel/examples.txt index e85c64efd86..8ba072d28b1 100644 --- a/doc/OnlineDocs/src/kernel/examples.txt +++ b/doc/OnlineDocs/src/kernel/examples.txt @@ -1,22 +1,7 @@ -6 Set Declarations - cd_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : s*q : 6 : {(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3)} - cl_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - ol_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} +1 Set Declarations s : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 2 : {1, 2} - sd_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - vl_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} 1 RangeSet Declarations q : Dimen=1, Size=3, Bounds=(1, 3) @@ -43,7 +28,7 @@ Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : None : None : 9 : False : True : Reals 2 : None : None : 9 : False : True : Reals - vl : Size=3, Index=vl_index + vl : Size=3, Index={1, 2, 3} Key : Lower : Value : Upper : Fixed : Stale : Domain 1 : 1 : None : None : False : True : Reals 2 : 2 : None : None : False : True : Reals @@ -66,7 +51,7 @@ Key : Active : Sense : Expression 1 : True : minimize : - vd[1] 2 : True : minimize : - vd[2] - ol : Size=3, Index=ol_index, Active=True + ol : Size=3, Index={1, 2, 3}, Active=True Key : Active : Sense : Expression 1 : True : minimize : - vl[1] 2 : True : minimize : - vl[2] @@ -76,7 +61,7 @@ c : Size=1, Index=None, Active=True Key : Lower : Body : Upper : Active None : -Inf : vd[1] + vd[2] : 9.0 : True - cd : Size=6, Index=cd_index, Active=True + cd : Size=6, Index=s*q, Active=True Key : Lower : Body : Upper : Active (1, 1) : 1.0 : vd[1] : 1.0 : True (1, 2) : 2.0 : vd[1] : 2.0 : True @@ -84,14 +69,14 @@ (2, 1) : 1.0 : vd[2] : 1.0 : True (2, 2) : 2.0 : vd[2] : 2.0 : True (2, 3) : 3.0 : vd[2] : 3.0 : True - cl : Size=3, Index=cl_index, Active=True + cl : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : -5.0 : vl[1] - v : 5.0 : True 2 : -5.0 : vl[2] - v : 5.0 : True 3 : -5.0 : vl[3] - v : 5.0 : True 3 SOSConstraint Declarations - sd : Size=2 Index= sd_index + sd : Size=2 Index= OrderedScalarSet 1 Type=1 Weight : Variable @@ -119,16 +104,8 @@ b : Size=1, Index=None, Active=True 0 Declarations: pw : Size=1, Index=None, Active=True - 2 Set Declarations - SOS2_constraint_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {1, 2, 3} - SOS2_y_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 4 : {0, 1, 2, 3} - 1 Var Declarations - SOS2_y : Size=4, Index=pw.SOS2_y_index + SOS2_y : Size=4, Index={0, 1, 2, 3} Key : Lower : Value : Upper : Fixed : Stale : Domain 0 : 0 : None : None : False : True : NonNegativeReals 1 : 0 : None : None : False : True : NonNegativeReals @@ -136,7 +113,7 @@ 3 : 0 : None : None : False : True : NonNegativeReals 1 Constraint Declarations - SOS2_constraint : Size=3, Index=pw.SOS2_constraint_index, Active=True + SOS2_constraint : Size=3, Index={1, 2, 3}, Active=True Key : Lower : Body : Upper : Active 1 : 0.0 : v - (pw.SOS2_y[0] + 2*pw.SOS2_y[1] + 3*pw.SOS2_y[2] + 4*pw.SOS2_y[3]) : 0.0 : True 2 : 0.0 : f - (pw.SOS2_y[0] + 2*pw.SOS2_y[1] + pw.SOS2_y[2] + 2*pw.SOS2_y[3]) : 0.0 : True @@ -151,13 +128,13 @@ 3 : pw.SOS2_y[2] 4 : pw.SOS2_y[3] - 5 Declarations: SOS2_y_index SOS2_y SOS2_constraint_index SOS2_constraint SOS2_sosconstraint + 3 Declarations: SOS2_y SOS2_constraint SOS2_sosconstraint 1 Suffix Declarations dual : Direction=IMPORT, Datatype=FLOAT Key : Value -27 Declarations: b s q p pd v vd vl_index vl c cd_index cd cl_index cl e ed o od ol_index ol sos1 sos2 sd_index sd dual f pw +22 Declarations: b s q p pd v vd vl c cd cl e ed o od ol sos1 sos2 sd dual f pw : block(active=True, ctype=IBlock) - b: block(active=True, ctype=IBlock) - p: parameter(active=True, value=0) @@ -231,4 +208,4 @@ - pw.c[2]: linear_constraint(active=True, expr=pw.v[0] + pw.v[1] + pw.v[2] + pw.v[3] == 1) - pw.s: sos(active=True, level=2, entries=['(pw.v[0],1)', '(pw.v[1],2)', '(pw.v[2],3)', '(pw.v[3],4)']) Memory: 1.9 KB -Memory: 9.5 KB +Memory: 9.7 KB From e8ba13e43dc16ca829b8ecda15f1dc7ce8635b19 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Feb 2024 09:12:24 -0700 Subject: [PATCH 0517/3044] Minor update to tests --- pyomo/repn/tests/test_util.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/repn/tests/test_util.py b/pyomo/repn/tests/test_util.py index cce10e58334..ac3f7e62791 100644 --- a/pyomo/repn/tests/test_util.py +++ b/pyomo/repn/tests/test_util.py @@ -699,6 +699,7 @@ def test_ExitNodeDispatcher_registration(self): self.assertEqual(end[node.__class__, 3, 4, 5, 6](None, node, *node.args), 6) self.assertEqual(len(end), 7) + # We don't cache etypes with more than 3 arguments self.assertNotIn((SumExpression, 3, 4, 5, 6), end) class NewProductExpression(ProductExpression): @@ -718,6 +719,7 @@ class UnknownExpression(NumericExpression): ): end[node.__class__](None, node, *node.args) self.assertEqual(len(end), 9) + self.assertIn(UnknownExpression, end) node = UnknownExpression((6, 7)) with self.assertRaisesRegex( @@ -725,6 +727,8 @@ class UnknownExpression(NumericExpression): ): end[node.__class__, 6, 7](None, node, *node.args) self.assertEqual(len(end), 10) + self.assertIn((UnknownExpression, 6, 7), end) + def test_BeforeChildDispatcher_registration(self): class BeforeChildDispatcherTester(BeforeChildDispatcher): From 94d131fca8aba4ec45271f51dcd12722025e97a5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Feb 2024 09:16:15 -0700 Subject: [PATCH 0518/3044] NFC: apply black --- pyomo/repn/tests/test_util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/repn/tests/test_util.py b/pyomo/repn/tests/test_util.py index ac3f7e62791..c4902a7064d 100644 --- a/pyomo/repn/tests/test_util.py +++ b/pyomo/repn/tests/test_util.py @@ -729,7 +729,6 @@ class UnknownExpression(NumericExpression): self.assertEqual(len(end), 10) self.assertIn((UnknownExpression, 6, 7), end) - def test_BeforeChildDispatcher_registration(self): class BeforeChildDispatcherTester(BeforeChildDispatcher): @staticmethod From 6408d44daa677dc2a62b6216b4efecd162341655 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Feb 2024 09:27:04 -0700 Subject: [PATCH 0519/3044] Updating an additional baseline (it is only tested with pyutilib) --- .../src/dataportal/dataportal_tab.txt | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/doc/OnlineDocs/src/dataportal/dataportal_tab.txt b/doc/OnlineDocs/src/dataportal/dataportal_tab.txt index 2e507971157..a23c63d90c9 100644 --- a/doc/OnlineDocs/src/dataportal/dataportal_tab.txt +++ b/doc/OnlineDocs/src/dataportal/dataportal_tab.txt @@ -85,19 +85,16 @@ A3 : 4.5 2 Declarations: A w -3 Set Declarations +2 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'A1', 'A2', 'A3'} I : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 4 : {'I1', 'I2', 'I3', 'I4'} - u_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : I*A : 12 : {('I1', 'A1'), ('I1', 'A2'), ('I1', 'A3'), ('I2', 'A1'), ('I2', 'A2'), ('I2', 'A3'), ('I3', 'A1'), ('I3', 'A2'), ('I3', 'A3'), ('I4', 'A1'), ('I4', 'A2'), ('I4', 'A3')} 1 Param Declarations - u : Size=12, Index=u_index, Domain=Any, Default=None, Mutable=False + u : Size=12, Index=I*A, Domain=Any, Default=None, Mutable=False Key : Value ('I1', 'A1') : 1.3 ('I1', 'A2') : 2.3 @@ -112,20 +109,17 @@ ('I4', 'A2') : 2.6 ('I4', 'A3') : 3.6 -4 Declarations: A I u_index u -3 Set Declarations +3 Declarations: A I u +2 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {'A1', 'A2', 'A3'} I : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 4 : {'I1', 'I2', 'I3', 'I4'} - t_index : Size=1, Index=None, Ordered=True - Key : Dimen : Domain : Size : Members - None : 2 : A*I : 12 : {('A1', 'I1'), ('A1', 'I2'), ('A1', 'I3'), ('A1', 'I4'), ('A2', 'I1'), ('A2', 'I2'), ('A2', 'I3'), ('A2', 'I4'), ('A3', 'I1'), ('A3', 'I2'), ('A3', 'I3'), ('A3', 'I4')} 1 Param Declarations - t : Size=12, Index=t_index, Domain=Any, Default=None, Mutable=False + t : Size=12, Index=A*I, Domain=Any, Default=None, Mutable=False Key : Value ('A1', 'I1') : 1.3 ('A1', 'I2') : 1.4 @@ -140,7 +134,7 @@ ('A3', 'I3') : 3.5 ('A3', 'I4') : 3.6 -4 Declarations: A I t_index t +3 Declarations: A I t 1 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members @@ -185,13 +179,9 @@ None : 1 : Any : 3 : {'A1', 'A2', 'A3'} 1 Declarations: A -1 Set Declarations - y_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} 2 Param Declarations - y : Size=3, Index=y_index, Domain=Any, Default=None, Mutable=False + y : Size=3, Index={A1, A2, A3}, Domain=Any, Default=None, Mutable=False Key : Value A1 : 3.3 A2 : 3.4 @@ -200,7 +190,7 @@ Key : Value None : 1.1 -3 Declarations: z y_index y +2 Declarations: z y ['A1', 'A2', 'A3'] 1.1 A1 3.3 From 5cc5e621694b05360fd93fbe98835a4c963e08f0 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 13 Feb 2024 10:44:57 -0700 Subject: [PATCH 0520/3044] Black --- pyomo/gdp/tests/test_mbigm.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index 33e8781ac63..51230bab075 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -360,8 +360,9 @@ def test_local_var_suffix_ignored(self): m.d1.LocalVars[m.d1] = m.y mbigm = TransformationFactory('gdp.mbigm') - mbigm.apply_to(m, reduce_bound_constraints=True, - only_mbigm_bound_constraints=True) + mbigm.apply_to( + m, reduce_bound_constraints=True, only_mbigm_bound_constraints=True + ) cons = mbigm.get_transformed_constraints(m.d1.x1_bounds) self.check_pretty_bound_constraints( @@ -382,9 +383,11 @@ def test_local_var_suffix_ignored(self): cons = mbigm.get_transformed_constraints(m.d1.another_thing) self.assertEqual(len(cons), 2) self.check_pretty_bound_constraints( - cons[0], m.y, {m.d1: 3, m.d2: 2, m.d3: 2}, lb=True) + cons[0], m.y, {m.d1: 3, m.d2: 2, m.d3: 2}, lb=True + ) self.check_pretty_bound_constraints( - cons[1], m.y, {m.d1: 3, m.d2: 5, m.d3: 5}, lb=False) + cons[1], m.y, {m.d1: 3, m.d2: 5, m.d3: 5}, lb=False + ) def test_pickle_transformed_model(self): m = self.make_model() From a77a19b5302544aad90b457307f1a5f650583402 Mon Sep 17 00:00:00 2001 From: Zedong Date: Tue, 13 Feb 2024 13:24:13 -0500 Subject: [PATCH 0521/3044] Update pyomo/contrib/mindtpy/util.py Co-authored-by: Bethany Nicholson --- pyomo/contrib/mindtpy/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 6061da0f0d9..575544eed5c 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -1024,6 +1024,6 @@ def set_var_valid_value( var.set_value(0) else: raise ValueError( - "copy_var_list_values failed with variable {}, value = {} and rounded value = {}" + "set_var_valid_value failed with variable {}, value = {} and rounded value = {}" "".format(var.name, var_val, rounded_val) ) From a0997d824d9079cc3621b4cb77e2d5933f16804c Mon Sep 17 00:00:00 2001 From: Zedong Date: Tue, 13 Feb 2024 13:24:28 -0500 Subject: [PATCH 0522/3044] Update pyomo/contrib/mindtpy/util.py Co-authored-by: Bethany Nicholson --- pyomo/contrib/mindtpy/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 575544eed5c..fa6aec7f08f 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -944,7 +944,7 @@ def copy_var_list_values( Sets to zero for NonNegativeReals if necessary from_list : list - The variables that provides the values to copy from. + The variables that provide the values to copy from. to_list : list The variables that need to set value. config : ConfigBlock From a5aa8273eb78273d22591eae6b4b8111ee9908ca Mon Sep 17 00:00:00 2001 From: Sakshi <73687517+Sakshi21299@users.noreply.github.com> Date: Tue, 13 Feb 2024 13:28:24 -0500 Subject: [PATCH 0523/3044] change function doc --- pyomo/contrib/incidence_analysis/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 5bf7ec71e09..595bb42dc41 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -934,10 +934,10 @@ def plot(self, variables=None, constraints=None, title=None, show=True): fig.show() def add_edge(self, variable, constraint): - """Adds an edge between node0 and node1 in the incidence graph + """Adds an edge between variable and constraint in the incidence graph Parameters - --------- + ---------- variable: VarData A variable in the graph constraint: ConstraintData From 6c9fa3ee64bbaeb9d8bce565a857b584b886a401 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 13 Feb 2024 13:34:35 -0500 Subject: [PATCH 0524/3044] update the differentiate.Modes --- pyomo/contrib/mindtpy/util.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index fa6aec7f08f..69c7ca5030a 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -57,10 +57,7 @@ def calc_jacobians(constraint_list, differentiate_mode): # Map nonlinear_constraint --> Map( # variable --> jacobian of constraint w.r.t. variable) jacobians = ComponentMap() - if differentiate_mode == 'reverse_symbolic': - mode = EXPR.differentiate.Modes.reverse_symbolic - elif differentiate_mode == 'sympy': - mode = EXPR.differentiate.Modes.sympy + mode = EXPR.differentiate.Modes(differentiate_mode) for c in constraint_list: vars_in_constr = list(EXPR.identify_variables(c.body)) jac_list = EXPR.differentiate(c.body, wrt_list=vars_in_constr, mode=mode) From 0d39b5d0f20e35c88755dd6557dca63eb1fbf473 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Feb 2024 13:43:35 -0700 Subject: [PATCH 0525/3044] Update NLv2 to only raise exception on empty models in the legacy API --- pyomo/repn/plugins/nl_writer.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 2d5eae151b0..cd570a8a0e1 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -346,6 +346,17 @@ def __call__(self, model, filename, solver_capability, io_options): row_fname ) as ROWFILE, _open(col_fname) as COLFILE: info = self.write(model, FILE, ROWFILE, COLFILE, config=config) + if not info.variables: + # This exception is included for compatibility with the + # original NL writer v1. + os.remove(filename) + os.remove(row_filename) + os.remove(col_filename) + raise ValueError( + "No variables appear in the Pyomo model constraints or" + " objective. This is not supported by the NL file interface" + ) + # Historically, the NL writer communicated the external function # libraries back to the ASL interface through the PYOMO_AMPLFUNC # environment variable. @@ -854,13 +865,6 @@ def write(self, model): con_vars = con_vars_linear | con_vars_nonlinear all_vars = con_vars | obj_vars n_vars = len(all_vars) - if n_vars < 1: - # TODO: Remove this. This exception is included for - # compatibility with the original NL writer v1. - raise ValueError( - "No variables appear in the Pyomo model constraints or" - " objective. This is not supported by the NL file interface" - ) continuous_vars = set() binary_vars = set() From 9764b84541859c797f7790e3b8475a9e7e7abe24 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Feb 2024 15:35:19 -0700 Subject: [PATCH 0526/3044] bugfix: fix symbol name --- pyomo/repn/plugins/nl_writer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index cd570a8a0e1..e12c1f47eb1 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -350,8 +350,8 @@ def __call__(self, model, filename, solver_capability, io_options): # This exception is included for compatibility with the # original NL writer v1. os.remove(filename) - os.remove(row_filename) - os.remove(col_filename) + os.remove(row_fname) + os.remove(col_fname) raise ValueError( "No variables appear in the Pyomo model constraints or" " objective. This is not supported by the NL file interface" From 1462403273125e3cda69720460a0e36240d4c73c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Feb 2024 15:37:49 -0700 Subject: [PATCH 0527/3044] add guard for symbolic_solver_labels=False --- pyomo/repn/plugins/nl_writer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index e12c1f47eb1..cda4ee011d3 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -350,8 +350,9 @@ def __call__(self, model, filename, solver_capability, io_options): # This exception is included for compatibility with the # original NL writer v1. os.remove(filename) - os.remove(row_fname) - os.remove(col_fname) + if config.symbolic_solver_labels: + os.remove(row_fname) + os.remove(col_fname) raise ValueError( "No variables appear in the Pyomo model constraints or" " objective. This is not supported by the NL file interface" From 4e8ef42c2534de210adb77105df831b82609c483 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 14 Feb 2024 10:55:21 -0700 Subject: [PATCH 0528/3044] First draft of private data on Blocks --- pyomo/core/base/block.py | 12 ++++++++++++ pyomo/core/tests/unit/test_block.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 89e872ebbe5..d10754082bd 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -550,6 +550,7 @@ def __init__(self, component): super(_BlockData, self).__setattr__('_ctypes', {}) super(_BlockData, self).__setattr__('_decl', {}) super(_BlockData, self).__setattr__('_decl_order', []) + self._private_data_dict = None def __getattr__(self, val): if val in ModelComponentFactory: @@ -2241,6 +2242,17 @@ def display(self, filename=None, ostream=None, prefix=""): for key in sorted(self): _BlockData.display(self[key], filename, ostream, prefix) + @property + def _private_data(self): + if self._private_data_dict is None: + self._private_data_dict = {} + return self._private_data_dict + + def private_data(self, scope): + if scope not in self._private_data: + self._private_data[scope] = {} + return self._private_data[scope] + class ScalarBlock(_BlockData, Block): def __init__(self, *args, **kwds): diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index f68850d9421..803237b1588 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -3407,6 +3407,21 @@ def test_deduplicate_component_data_iterindex(self): ], ) + def test_private_data(self): + m = ConcreteModel() + m.b = Block() + m.b.b = Block([1, 2]) + + mfe = m.private_data('my_scope') + self.assertIsInstance(mfe, dict) + mfe2 = m.private_data('another_scope') + self.assertIsInstance(mfe2, dict) + self.assertEqual(len(m._private_data), 2) + + mfe = m.b.private_data('my_scope') + self.assertIsInstance(mfe, dict) + + if __name__ == "__main__": unittest.main() From 5aa9670c1af9f2e7eee517a0f48cacf840b31822 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 14 Feb 2024 11:01:28 -0700 Subject: [PATCH 0529/3044] Adding copyright statement --- pyomo/gdp/plugins/binary_multiplication.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pyomo/gdp/plugins/binary_multiplication.py b/pyomo/gdp/plugins/binary_multiplication.py index dfdc87ded19..4089ee13a32 100644 --- a/pyomo/gdp/plugins/binary_multiplication.py +++ b/pyomo/gdp/plugins/binary_multiplication.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .gdp_to_mip_transformation import GDP_to_MIP_Transformation from pyomo.common.config import ConfigDict, ConfigValue from pyomo.core.base import TransformationFactory From 2c8a0a950839ed69c7ee5cd6ba46d22e57a4dab2 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 14 Feb 2024 11:02:34 -0700 Subject: [PATCH 0530/3044] Employing the enter key --- pyomo/gdp/plugins/binary_multiplication.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/gdp/plugins/binary_multiplication.py b/pyomo/gdp/plugins/binary_multiplication.py index 4089ee13a32..f919ee34434 100644 --- a/pyomo/gdp/plugins/binary_multiplication.py +++ b/pyomo/gdp/plugins/binary_multiplication.py @@ -23,7 +23,9 @@ @TransformationFactory.register( 'gdp.binary_multiplication', - doc="Reformulate the GDP as an MINLP by multiplying f(x) <= 0 by y to get f(x) * y <= 0 where y is the binary corresponding to the Boolean indicator var of the Disjunct containing f(x) <= 0.", + doc="Reformulate the GDP as an MINLP by multiplying f(x) <= 0 by y to get " + "f(x) * y <= 0 where y is the binary corresponding to the Boolean indicator " + "var of the Disjunct containing f(x) <= 0.", ) class GDPBinaryMultiplicationTransformation(GDP_to_MIP_Transformation): CONFIG = ConfigDict("gdp.binary_multiplication") From 42067948a923f20bdde323e24e71bf001d1807b5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 14 Feb 2024 11:16:04 -0700 Subject: [PATCH 0531/3044] Simplifying logic with mapping transformed constraints --- pyomo/gdp/plugins/binary_multiplication.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/pyomo/gdp/plugins/binary_multiplication.py b/pyomo/gdp/plugins/binary_multiplication.py index f919ee34434..5afb661aaa8 100644 --- a/pyomo/gdp/plugins/binary_multiplication.py +++ b/pyomo/gdp/plugins/binary_multiplication.py @@ -157,30 +157,21 @@ def _add_constraint_expressions( # over the constraint indices, but I don't think it matters a lot.) unique = len(newConstraint) name = c.local_name + "_%s" % unique + transformed = constraintMap['transformedConstraints'][c] = [] lb, ub = c.lower, c.upper if (c.equality or lb is ub) and lb is not None: # equality newConstraint.add((name, i, 'eq'), (c.body - lb) * indicator_var == 0) - constraintMap['transformedConstraints'][c] = [newConstraint[name, i, 'eq']] + transformed.append(newConstraint[name, i, 'eq']) constraintMap['srcConstraints'][newConstraint[name, i, 'eq']] = c else: # inequality if lb is not None: newConstraint.add((name, i, 'lb'), 0 <= (c.body - lb) * indicator_var) - constraintMap['transformedConstraints'][c] = [ - newConstraint[name, i, 'lb'] - ] + transformed.append(newConstraint[name, i, 'lb']) constraintMap['srcConstraints'][newConstraint[name, i, 'lb']] = c if ub is not None: newConstraint.add((name, i, 'ub'), (c.body - ub) * indicator_var <= 0) - transformed = constraintMap['transformedConstraints'].get(c) - if transformed is not None: - constraintMap['transformedConstraints'][c].append( - newConstraint[name, i, 'ub'] - ) - else: - constraintMap['transformedConstraints'][c] = [ - newConstraint[name, i, 'ub'] - ] + transformed.append(newConstraint[name, i, 'ub']) constraintMap['srcConstraints'][newConstraint[name, i, 'ub']] = c From 2a3b5731b6b73f8c1ba3b7831c1cdfc9c0a988f2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 12:34:58 -0700 Subject: [PATCH 0532/3044] Unit tests for config, results, and util complete --- pyomo/contrib/solver/config.py | 6 +- pyomo/contrib/solver/results.py | 103 ++++++++++++------ pyomo/contrib/solver/sol_reader.py | 8 +- pyomo/contrib/solver/solution.py | 6 +- .../contrib/solver/tests/unit/test_config.py | 61 ++++++++++- .../contrib/solver/tests/unit/test_results.py | 6 +- .../solver/tests/unit/test_solution.py | 18 ++- pyomo/contrib/solver/tests/unit/test_util.py | 32 +++++- 8 files changed, 187 insertions(+), 53 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index d5921c526b0..2a1a129d1ac 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -63,7 +63,8 @@ def __init__( ConfigValue( domain=str, default=None, - description="The directory in which generated files should be saved. This replaced the `keepfiles` option.", + description="The directory in which generated files should be saved. " + "This replaced the `keepfiles` option.", ), ) self.load_solutions: bool = self.declare( @@ -79,7 +80,8 @@ def __init__( ConfigValue( domain=bool, default=True, - description="If False, the `solve` method will continue processing even if the returned result is nonoptimal.", + description="If False, the `solve` method will continue processing " + "even if the returned result is nonoptimal.", ), ) self.symbolic_solver_labels: bool = self.declare( diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index 1fa9d653d01..5ed6de44430 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -22,18 +22,12 @@ NonNegativeFloat, ADVANCED_OPTION, ) -from pyomo.common.errors import PyomoException from pyomo.opt.results.solution import SolutionStatus as LegacySolutionStatus from pyomo.opt.results.solver import ( TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus, ) - - -class SolverResultsError(PyomoException): - """ - General exception to catch solver system errors - """ +from pyomo.common.timing import HierarchicalTimer class TerminationCondition(enum.Enum): @@ -167,12 +161,16 @@ class Results(ConfigDict): iteration_count: int The total number of iterations. timing_info: ConfigDict - A ConfigDict containing three pieces of information: - start_time: UTC timestamp of when run was initiated + A ConfigDict containing two pieces of information: + start_timestamp: UTC timestamp of when run was initiated wall_time: elapsed wall clock time for entire process - solver_wall_time: elapsed wall clock time for solve call + timer: a HierarchicalTimer object containing timing data about the solve extra_info: ConfigDict A ConfigDict to store extra information such as solver messages. + solver_configuration: ConfigDict + A copy of the SolverConfig ConfigDict, for later inspection/reproducibility. + solver_log: str + (ADVANCED OPTION) Any solver log messages. """ def __init__( @@ -191,41 +189,85 @@ def __init__( visibility=visibility, ) - self.solution_loader = self.declare('solution_loader', ConfigValue()) + self.solution_loader = self.declare( + 'solution_loader', + ConfigValue( + description="Object for loading the solution back into the model." + ), + ) self.termination_condition: TerminationCondition = self.declare( 'termination_condition', ConfigValue( - domain=In(TerminationCondition), default=TerminationCondition.unknown + domain=In(TerminationCondition), + default=TerminationCondition.unknown, + description="The reason the solver exited. This is a member of the " + "TerminationCondition enum.", ), ) self.solution_status: SolutionStatus = self.declare( 'solution_status', - ConfigValue(domain=In(SolutionStatus), default=SolutionStatus.noSolution), + ConfigValue( + domain=In(SolutionStatus), + default=SolutionStatus.noSolution, + description="The result of the solve call. This is a member of " + "the SolutionStatus enum.", + ), ) self.incumbent_objective: Optional[float] = self.declare( - 'incumbent_objective', ConfigValue(domain=float, default=None) + 'incumbent_objective', + ConfigValue( + domain=float, + default=None, + description="If a feasible solution was found, this is the objective " + "value of the best solution found. If no feasible solution was found, this is None.", + ), ) self.objective_bound: Optional[float] = self.declare( - 'objective_bound', ConfigValue(domain=float, default=None) + 'objective_bound', + ConfigValue( + domain=float, + default=None, + description="The best objective bound found. For minimization problems, " + "this is the lower bound. For maximization problems, this is the " + "upper bound. For solvers that do not provide an objective bound, " + "this should be -inf (minimization) or inf (maximization)", + ), ) self.solver_name: Optional[str] = self.declare( - 'solver_name', ConfigValue(domain=str) + 'solver_name', + ConfigValue(domain=str, description="The name of the solver in use."), ) self.solver_version: Optional[Tuple[int, ...]] = self.declare( - 'solver_version', ConfigValue(domain=tuple) + 'solver_version', + ConfigValue( + domain=tuple, + description="A tuple representing the version of the solver in use.", + ), ) self.iteration_count: Optional[int] = self.declare( - 'iteration_count', ConfigValue(domain=NonNegativeInt, default=None) + 'iteration_count', + ConfigValue( + domain=NonNegativeInt, + default=None, + description="The total number of iterations.", + ), ) self.timing_info: ConfigDict = self.declare( 'timing_info', ConfigDict(implicit=True) ) self.timing_info.start_timestamp: datetime = self.timing_info.declare( - 'start_timestamp', ConfigValue(domain=Datetime) + 'start_timestamp', + ConfigValue( + domain=Datetime, description="UTC timestamp of when run was initiated." + ), ) self.timing_info.wall_time: Optional[float] = self.timing_info.declare( - 'wall_time', ConfigValue(domain=NonNegativeFloat) + 'wall_time', + ConfigValue( + domain=NonNegativeFloat, + description="Elapsed wall clock time for entire process.", + ), ) self.extra_info: ConfigDict = self.declare( 'extra_info', ConfigDict(implicit=True) @@ -233,13 +275,18 @@ def __init__( self.solver_configuration: ConfigDict = self.declare( 'solver_configuration', ConfigValue( - description="A copy of the config object used in the solve", + description="A copy of the config object used in the solve call.", visibility=ADVANCED_OPTION, ), ) self.solver_log: str = self.declare( 'solver_log', - ConfigValue(domain=str, default=None, visibility=ADVANCED_OPTION), + ConfigValue( + domain=str, + default=None, + visibility=ADVANCED_OPTION, + description="Any solver log messages.", + ), ) def display( @@ -248,18 +295,6 @@ def display( return super().display(content_filter, indent_spacing, ostream, visibility) -class ResultsReader: - pass - - -def parse_yaml(): - pass - - -def parse_json(): - pass - - # Everything below here preserves backwards compatibility legacy_termination_condition_map = { diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index 68654a4e9d7..3af30e1826b 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -15,7 +15,7 @@ from pyomo.common.errors import DeveloperError from pyomo.repn.plugins.nl_writer import NLWriterInfo -from .results import Results, SolverResultsError, SolutionStatus, TerminationCondition +from .results import Results, SolutionStatus, TerminationCondition class SolFileData: @@ -69,7 +69,7 @@ def parse_sol_file( line = sol_file.readline() model_objects.append(int(line)) else: - raise SolverResultsError("ERROR READING `sol` FILE. No 'Options' line found.") + raise Exception("ERROR READING `sol` FILE. No 'Options' line found.") # Identify the total number of variables and constraints number_of_cons = model_objects[number_of_options + 1] number_of_vars = model_objects[number_of_options + 3] @@ -85,12 +85,12 @@ def parse_sol_file( if line and ('objno' in line): exit_code_line = line.split() if len(exit_code_line) != 3: - raise SolverResultsError( + raise Exception( f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}." ) exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] else: - raise SolverResultsError( + raise Exception( f"ERROR READING `sol` FILE. Expected `objno`; received {line}." ) result.extra_info.solver_message = message.strip().replace('\n', '; ') diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 33a3b1c939c..beb53cf979a 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -23,6 +23,11 @@ class SolutionLoaderBase(abc.ABC): + """ + Base class for all future SolutionLoader classes. + + Intent of this class and its children is to load the solution back into the model. + """ def load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> NoReturn: @@ -58,7 +63,6 @@ def get_primals( primals: ComponentMap Maps variables to solution values """ - pass def get_duals( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None diff --git a/pyomo/contrib/solver/tests/unit/test_config.py b/pyomo/contrib/solver/tests/unit/test_config.py index 4a7cc250623..f28dd5fcedf 100644 --- a/pyomo/contrib/solver/tests/unit/test_config.py +++ b/pyomo/contrib/solver/tests/unit/test_config.py @@ -10,7 +10,12 @@ # ___________________________________________________________________________ from pyomo.common import unittest -from pyomo.contrib.solver.config import SolverConfig, BranchAndBoundConfig +from pyomo.contrib.solver.config import ( + SolverConfig, + BranchAndBoundConfig, + AutoUpdateConfig, + PersistentSolverConfig, +) class TestSolverConfig(unittest.TestCase): @@ -59,3 +64,57 @@ def test_interface_custom_instantiation(self): self.assertIsInstance(config.time_limit, float) config.rel_gap = 2.5 self.assertEqual(config.rel_gap, 2.5) + + +class TestAutoUpdateConfig(unittest.TestCase): + def test_interface_default_instantiation(self): + config = AutoUpdateConfig() + self.assertTrue(config.check_for_new_or_removed_constraints) + self.assertTrue(config.check_for_new_or_removed_vars) + self.assertTrue(config.check_for_new_or_removed_params) + self.assertTrue(config.check_for_new_objective) + self.assertTrue(config.update_constraints) + self.assertTrue(config.update_vars) + self.assertTrue(config.update_named_expressions) + self.assertTrue(config.update_objective) + self.assertTrue(config.update_objective) + self.assertTrue(config.treat_fixed_vars_as_params) + + def test_interface_custom_instantiation(self): + config = AutoUpdateConfig(description="A description") + config.check_for_new_objective = False + self.assertEqual(config._description, "A description") + self.assertTrue(config.check_for_new_or_removed_constraints) + self.assertFalse(config.check_for_new_objective) + + +class TestPersistentSolverConfig(unittest.TestCase): + def test_interface_default_instantiation(self): + config = PersistentSolverConfig() + self.assertIsNone(config._description) + self.assertEqual(config._visibility, 0) + self.assertFalse(config.tee) + self.assertTrue(config.load_solutions) + self.assertTrue(config.raise_exception_on_nonoptimal_result) + self.assertFalse(config.symbolic_solver_labels) + self.assertIsNone(config.timer) + self.assertIsNone(config.threads) + self.assertIsNone(config.time_limit) + self.assertTrue(config.auto_updates.check_for_new_or_removed_constraints) + self.assertTrue(config.auto_updates.check_for_new_or_removed_vars) + self.assertTrue(config.auto_updates.check_for_new_or_removed_params) + self.assertTrue(config.auto_updates.check_for_new_objective) + self.assertTrue(config.auto_updates.update_constraints) + self.assertTrue(config.auto_updates.update_vars) + self.assertTrue(config.auto_updates.update_named_expressions) + self.assertTrue(config.auto_updates.update_objective) + self.assertTrue(config.auto_updates.update_objective) + self.assertTrue(config.auto_updates.treat_fixed_vars_as_params) + + def test_interface_custom_instantiation(self): + config = PersistentSolverConfig(description="A description") + config.tee = True + config.auto_updates.check_for_new_objective = False + self.assertTrue(config.tee) + self.assertEqual(config._description, "A description") + self.assertFalse(config.auto_updates.check_for_new_objective) diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 23c2c32f819..caef82129ec 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -69,7 +69,7 @@ def test_codes(self): class TestResults(unittest.TestCase): - def test_declared_items(self): + def test_member_list(self): res = results.Results() expected_declared = { 'extra_info', @@ -88,7 +88,7 @@ def test_declared_items(self): actual_declared = res._declared self.assertEqual(expected_declared, actual_declared) - def test_uninitialized(self): + def test_default_initialization(self): res = results.Results() self.assertIsNone(res.incumbent_objective) self.assertIsNone(res.objective_bound) @@ -118,7 +118,7 @@ def test_uninitialized(self): ): res.solution_loader.get_reduced_costs() - def test_results(self): + def test_generated_results(self): m = pyo.ConcreteModel() m.x = ScalarVar() m.y = ScalarVar() diff --git a/pyomo/contrib/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py index 1ecba45b32a..67ce2556317 100644 --- a/pyomo/contrib/solver/tests/unit/test_solution.py +++ b/pyomo/contrib/solver/tests/unit/test_solution.py @@ -10,22 +10,30 @@ # ___________________________________________________________________________ from pyomo.common import unittest -from pyomo.contrib.solver import solution +from pyomo.contrib.solver.solution import SolutionLoaderBase, PersistentSolutionLoader -class TestPersistentSolverBase(unittest.TestCase): +class TestSolutionLoaderBase(unittest.TestCase): def test_abstract_member_list(self): expected_list = ['get_primals'] - member_list = list(solution.SolutionLoaderBase.__abstractmethods__) + member_list = list(SolutionLoaderBase.__abstractmethods__) self.assertEqual(sorted(expected_list), sorted(member_list)) @unittest.mock.patch.multiple( - solution.SolutionLoaderBase, __abstractmethods__=set() + SolutionLoaderBase, __abstractmethods__=set() ) def test_solution_loader_base(self): - self.instance = solution.SolutionLoaderBase() + self.instance = SolutionLoaderBase() self.assertEqual(self.instance.get_primals(), None) with self.assertRaises(NotImplementedError): self.instance.get_duals() with self.assertRaises(NotImplementedError): self.instance.get_reduced_costs() + + +class TestPersistentSolutionLoader(unittest.TestCase): + def test_abstract_member_list(self): + # We expect no abstract members at this point because it's a real-life + # instantiation of SolutionLoaderBase + member_list = list(PersistentSolutionLoader('ipopt').__abstractmethods__) + self.assertEqual(member_list, []) diff --git a/pyomo/contrib/solver/tests/unit/test_util.py b/pyomo/contrib/solver/tests/unit/test_util.py index 8a8a0221362..ab8a778067f 100644 --- a/pyomo/contrib/solver/tests/unit/test_util.py +++ b/pyomo/contrib/solver/tests/unit/test_util.py @@ -102,15 +102,41 @@ def test_check_optimal_termination_condition_legacy_interface(self): results = SolverResults() results.solver.status = SolverStatus.ok results.solver.termination_condition = LegacyTerminationCondition.optimal + # Both items satisfied self.assertTrue(check_optimal_termination(results)) + # Termination condition not satisfied results.solver.termination_condition = LegacyTerminationCondition.unknown self.assertFalse(check_optimal_termination(results)) + # Both not satisfied results.solver.termination_condition = SolverStatus.aborted self.assertFalse(check_optimal_termination(results)) - # TODO: Left off here; need to make these tests def test_assert_optimal_termination_new_interface(self): - pass + results = Results() + results.solution_status = SolutionStatus.optimal + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) + assert_optimal_termination(results) + # Termination condition not satisfied + results.termination_condition = TerminationCondition.iterationLimit + with self.assertRaises(RuntimeError): + assert_optimal_termination(results) + # Both not satisfied + results.solution_status = SolutionStatus.noSolution + with self.assertRaises(RuntimeError): + assert_optimal_termination(results) def test_assert_optimal_termination_legacy_interface(self): - pass + results = SolverResults() + results.solver.status = SolverStatus.ok + results.solver.termination_condition = LegacyTerminationCondition.optimal + assert_optimal_termination(results) + # Termination condition not satisfied + results.solver.termination_condition = LegacyTerminationCondition.unknown + with self.assertRaises(RuntimeError): + assert_optimal_termination(results) + # Both not satisfied + results.solver.termination_condition = SolverStatus.aborted + with self.assertRaises(RuntimeError): + assert_optimal_termination(results) From d4e56e81cb5d8ca354aad72b2da4cc2261f2ca05 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 12:38:41 -0700 Subject: [PATCH 0533/3044] Apply new version of black --- pyomo/contrib/solver/ipopt.py | 16 ++++++++-------- pyomo/contrib/solver/sol_reader.py | 4 +--- pyomo/contrib/solver/solution.py | 1 + pyomo/contrib/solver/tests/unit/test_solution.py | 4 +--- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 5eb877f0867..4c4b932381d 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -89,15 +89,15 @@ def __init__( implicit_domain=implicit_domain, visibility=visibility, ) - self.timing_info.no_function_solve_time: Optional[ - float - ] = self.timing_info.declare( - 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) + self.timing_info.no_function_solve_time: Optional[float] = ( + self.timing_info.declare( + 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) + ) ) - self.timing_info.function_solve_time: Optional[ - float - ] = self.timing_info.declare( - 'function_solve_time', ConfigValue(domain=NonNegativeFloat) + self.timing_info.function_solve_time: Optional[float] = ( + self.timing_info.declare( + 'function_solve_time', ConfigValue(domain=NonNegativeFloat) + ) ) diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index f78d9fb3115..b9b33272fd6 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -90,9 +90,7 @@ def parse_sol_file( ) exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] else: - raise Exception( - f"ERROR READING `sol` FILE. Expected `objno`; received {line}." - ) + raise Exception(f"ERROR READING `sol` FILE. Expected `objno`; received {line}.") result.extra_info.solver_message = message.strip().replace('\n', '; ') exit_code_message = '' if (exit_code[1] >= 0) and (exit_code[1] <= 99): diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index beb53cf979a..ca19e4df0e9 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -28,6 +28,7 @@ class SolutionLoaderBase(abc.ABC): Intent of this class and its children is to load the solution back into the model. """ + def load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> NoReturn: diff --git a/pyomo/contrib/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py index 67ce2556317..877be34d29b 100644 --- a/pyomo/contrib/solver/tests/unit/test_solution.py +++ b/pyomo/contrib/solver/tests/unit/test_solution.py @@ -19,9 +19,7 @@ def test_abstract_member_list(self): member_list = list(SolutionLoaderBase.__abstractmethods__) self.assertEqual(sorted(expected_list), sorted(member_list)) - @unittest.mock.patch.multiple( - SolutionLoaderBase, __abstractmethods__=set() - ) + @unittest.mock.patch.multiple(SolutionLoaderBase, __abstractmethods__=set()) def test_solution_loader_base(self): self.instance = SolutionLoaderBase() self.assertEqual(self.instance.get_primals(), None) From 51dccea8e8835b5150abc68ef29429413a733554 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 13:28:36 -0700 Subject: [PATCH 0534/3044] Add unit tests for solution module --- pyomo/contrib/solver/sol_reader.py | 53 +++++++++---------- .../solver/tests/unit/test_solution.py | 45 ++++++++++++++++ 2 files changed, 71 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index b9b33272fd6..ed4fe4865c2 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -13,7 +13,7 @@ from typing import Tuple, Dict, Any, List import io -from pyomo.common.errors import DeveloperError +from pyomo.common.errors import DeveloperError, PyomoException from pyomo.repn.plugins.nl_writer import NLWriterInfo from .results import Results, SolutionStatus, TerminationCondition @@ -26,6 +26,7 @@ def __init__(self) -> None: self.con_suffixes: Dict[str, Dict[Any]] = dict() self.obj_suffixes: Dict[str, Dict[int, Any]] = dict() self.problem_suffixes: Dict[str, List[Any]] = dict() + self.other: List(str) = list() def parse_sol_file( @@ -69,7 +70,7 @@ def parse_sol_file( line = sol_file.readline() model_objects.append(int(line)) else: - raise Exception("ERROR READING `sol` FILE. No 'Options' line found.") + raise PyomoException("ERROR READING `sol` FILE. No 'Options' line found.") # Identify the total number of variables and constraints number_of_cons = model_objects[number_of_options + 1] number_of_vars = model_objects[number_of_options + 3] @@ -85,12 +86,14 @@ def parse_sol_file( if line and ('objno' in line): exit_code_line = line.split() if len(exit_code_line) != 3: - raise Exception( + raise PyomoException( f"ERROR READING `sol` FILE. Expected two numbers in `objno` line; received {line}." ) exit_code = [int(exit_code_line[1]), int(exit_code_line[2])] else: - raise Exception(f"ERROR READING `sol` FILE. Expected `objno`; received {line}.") + raise PyomoException( + f"ERROR READING `sol` FILE. Expected `objno`; received {line}." + ) result.extra_info.solver_message = message.strip().replace('\n', '; ') exit_code_message = '' if (exit_code[1] >= 0) and (exit_code[1] <= 99): @@ -103,8 +106,6 @@ def parse_sol_file( elif (exit_code[1] >= 200) and (exit_code[1] <= 299): exit_code_message = "INFEASIBLE SOLUTION: constraints cannot be satisfied!" result.solution_status = SolutionStatus.infeasible - # TODO: this is solver dependent - # But this was the way in the previous version - and has been fine thus far? result.termination_condition = TerminationCondition.locallyInfeasible elif (exit_code[1] >= 300) and (exit_code[1] <= 399): exit_code_message = ( @@ -117,8 +118,6 @@ def parse_sol_file( "EXCEEDED MAXIMUM NUMBER OF ITERATIONS: the solver " "was stopped by a limit that you set!" ) - # TODO: this is solver dependent - # But this was the way in the previous version - and has been fine thus far? result.solution_status = SolutionStatus.infeasible result.termination_condition = ( TerminationCondition.iterationLimit @@ -158,47 +157,47 @@ def parse_sol_file( line = sol_file.readline() result.extra_info.solver_message += remaining break - unmasked_kind = int(line[1]) - kind = unmasked_kind & 3 # 0-var, 1-con, 2-obj, 3-prob + read_data_type = int(line[1]) + data_type = read_data_type & 3 # 0-var, 1-con, 2-obj, 3-prob convert_function = int - if (unmasked_kind & 4) == 4: + if (read_data_type & 4) == 4: convert_function = float - nvalues = int(line[2]) - # namelen = int(line[3]) - # tablen = int(line[4]) - tabline = int(line[5]) + number_of_entries = int(line[2]) + # The third entry is name length, and it is length+1. This is unnecessary + # except for data validation. + # The fourth entry is table "length", e.g., memory size. + number_of_string_lines = int(line[5]) suffix_name = sol_file.readline().strip() - # ignore translation of the table number to string value for now, - # this information can be obtained from the solver documentation - for n in range(tabline): - sol_file.readline() - if kind == 0: # Var + # Add any of arbitrary string lines to the "other" list + for line in range(number_of_string_lines): + sol_data.other.append(sol_file.readline()) + if data_type == 0: # Var sol_data.var_suffixes[suffix_name] = dict() - for cnt in range(nvalues): + for cnt in range(number_of_entries): suf_line = sol_file.readline().split() var_ndx = int(suf_line[0]) sol_data.var_suffixes[suffix_name][var_ndx] = convert_function( suf_line[1] ) - elif kind == 1: # Con + elif data_type == 1: # Con sol_data.con_suffixes[suffix_name] = dict() - for cnt in range(nvalues): + for cnt in range(number_of_entries): suf_line = sol_file.readline().split() con_ndx = int(suf_line[0]) sol_data.con_suffixes[suffix_name][con_ndx] = convert_function( suf_line[1] ) - elif kind == 2: # Obj + elif data_type == 2: # Obj sol_data.obj_suffixes[suffix_name] = dict() - for cnt in range(nvalues): + for cnt in range(number_of_entries): suf_line = sol_file.readline().split() obj_ndx = int(suf_line[0]) sol_data.obj_suffixes[suffix_name][obj_ndx] = convert_function( suf_line[1] ) - elif kind == 3: # Prob + elif data_type == 3: # Prob sol_data.problem_suffixes[suffix_name] = list() - for cnt in range(nvalues): + for cnt in range(number_of_entries): suf_line = sol_file.readline().split() sol_data.problem_suffixes[suffix_name].append( convert_function(suf_line[1]) diff --git a/pyomo/contrib/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py index 877be34d29b..bbcc85bdac8 100644 --- a/pyomo/contrib/solver/tests/unit/test_solution.py +++ b/pyomo/contrib/solver/tests/unit/test_solution.py @@ -19,6 +19,15 @@ def test_abstract_member_list(self): member_list = list(SolutionLoaderBase.__abstractmethods__) self.assertEqual(sorted(expected_list), sorted(member_list)) + def test_member_list(self): + expected_list = ['load_vars', 'get_primals', 'get_duals', 'get_reduced_costs'] + method_list = [ + method + for method in dir(SolutionLoaderBase) + if method.startswith('_') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + @unittest.mock.patch.multiple(SolutionLoaderBase, __abstractmethods__=set()) def test_solution_loader_base(self): self.instance = SolutionLoaderBase() @@ -29,9 +38,45 @@ def test_solution_loader_base(self): self.instance.get_reduced_costs() +class TestSolSolutionLoader(unittest.TestCase): + # I am currently unsure how to test this further because it relies heavily on + # SolFileData and NLWriterInfo + def test_member_list(self): + expected_list = ['load_vars', 'get_primals', 'get_duals', 'get_reduced_costs'] + method_list = [ + method + for method in dir(SolutionLoaderBase) + if method.startswith('_') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + + class TestPersistentSolutionLoader(unittest.TestCase): def test_abstract_member_list(self): # We expect no abstract members at this point because it's a real-life # instantiation of SolutionLoaderBase member_list = list(PersistentSolutionLoader('ipopt').__abstractmethods__) self.assertEqual(member_list, []) + + def test_member_list(self): + expected_list = [ + 'load_vars', + 'get_primals', + 'get_duals', + 'get_reduced_costs', + 'invalidate', + ] + method_list = [ + method + for method in dir(PersistentSolutionLoader) + if method.startswith('_') is False + ] + self.assertEqual(sorted(expected_list), sorted(method_list)) + + def test_default_initialization(self): + # Realistically, a solver object should be passed into this. + # However, it works with a string. It'll just error loudly if you + # try to run get_primals, etc. + self.instance = PersistentSolutionLoader('ipopt') + self.assertTrue(self.instance._valid) + self.assertEqual(self.instance._solver, 'ipopt') From 571cc0860be6460363d92f08bca690fbbee4989d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 13:56:41 -0700 Subject: [PATCH 0535/3044] Add 'name' to SolverBase --- pyomo/contrib/solver/base.py | 7 +++++++ pyomo/contrib/solver/tests/unit/test_base.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 42524296d74..046a83fb7ec 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -54,6 +54,13 @@ class SolverBase(abc.ABC): CONFIG = SolverConfig() def __init__(self, **kwds) -> None: + # We allow the user and/or developer to name the solver something else, + # if they really desire. Otherwise it defaults to the class name (all lowercase) + if "name" in kwds: + self.name = kwds["name"] + kwds.pop('name') + else: + self.name = type(self).__name__.lower() self.config = self.CONFIG(value=kwds) # diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 00e38d9ac59..cda1631d921 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -41,6 +41,7 @@ def test_init(self): self.instance = base.SolverBase() self.assertFalse(self.instance.is_persistent()) self.assertEqual(self.instance.version(), None) + self.assertEqual(self.instance.name, 'solverbase') self.assertEqual(self.instance.CONFIG, self.instance.config) self.assertEqual(self.instance.solve(None), None) self.assertEqual(self.instance.available(), None) @@ -50,6 +51,7 @@ def test_context_manager(self): with base.SolverBase() as self.instance: self.assertFalse(self.instance.is_persistent()) self.assertEqual(self.instance.version(), None) + self.assertEqual(self.instance.name, 'solverbase') self.assertEqual(self.instance.CONFIG, self.instance.config) self.assertEqual(self.instance.solve(None), None) self.assertEqual(self.instance.available(), None) @@ -69,6 +71,11 @@ def test_solver_availability(self): self.instance.Availability.__bool__(self.instance.Availability) ) + @unittest.mock.patch.multiple(base.SolverBase, __abstractmethods__=set()) + def test_custom_solver_name(self): + self.instance = base.SolverBase(name='my_unique_name') + self.assertEqual(self.instance.name, 'my_unique_name') + class TestPersistentSolverBase(unittest.TestCase): def test_abstract_member_list(self): From b19c75aa02ba2249cc92f17612087a3df5c6e3dc Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 14:08:21 -0700 Subject: [PATCH 0536/3044] Update documentation (which is still slim but is a reasonable start) --- .../developer_reference/solvers.rst | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 10e7e829463..fa24d69a211 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -9,8 +9,19 @@ Pyomo offers interfaces into multiple solvers, both commercial and open source. Interface Implementation ------------------------ -TBD: How to add a new interface; the pieces. +All new interfaces should be built upon one of two classes (currently): +``pyomo.contrib.solver.base.SolverBase`` or ``pyomo.contrib.solver.base.PersistentSolverBase``. +All solvers should have the following: + +.. autoclass:: pyomo.contrib.solver.base.SolverBase + :members: + +Persistent solvers should also include: + +.. autoclass:: pyomo.contrib.solver.base.PersistentSolverBase + :show-inheritance: + :members: Results ------- @@ -56,4 +67,10 @@ returned solver messages or logs for more information. Solution -------- -TBD: How to load/parse a solution. +Solutions can be loaded back into a model using a ``SolutionLoader``. A specific +loader should be written for each unique case. Several have already been +implemented. For example, for ``ipopt``: + +.. autoclass:: pyomo.contrib.solver.solution.SolSolutionLoader + :show-inheritance: + :members: From 9ed93fe11fb2f8ee060614fb3b1fc639d746052b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 14:10:45 -0700 Subject: [PATCH 0537/3044] Update docs to point to new gurobi interface --- pyomo/contrib/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 046a83fb7ec..96b87924bf6 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -176,7 +176,7 @@ class PersistentSolverBase(SolverBase): methods from the direct solver base and adds those methods that are necessary for persistent solvers. - Example usage can be seen in solvers within APPSI. + Example usage can be seen in the GUROBI solver. """ def is_persistent(self): From 398493c32e2b193f76842a30fc58b52a352c7df7 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 14 Feb 2024 14:24:21 -0700 Subject: [PATCH 0538/3044] solver refactor: update tests --- pyomo/contrib/solver/tests/solvers/test_solvers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 658aaf41b13..6b798f9bafd 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -139,6 +139,11 @@ def test_reduced_costs(self, name: str, opt_class: Type[SolverBase]): rc = res.solution_loader.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 3) self.assertAlmostEqual(rc[m.y], 4) + m.obj.expr *= -1 + res = opt.solve(m) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], -3) + self.assertAlmostEqual(rc[m.y], -4) @parameterized.expand(input=_load_tests(all_solvers)) def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase]): From 9fcb2366e82e515ea718057e4aa6645308943a88 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 15:23:02 -0700 Subject: [PATCH 0539/3044] Set up structure for sol_reader tests --- pyomo/contrib/solver/ipopt.py | 2 +- pyomo/contrib/solver/sol_reader.py | 2 +- pyomo/contrib/solver/solution.py | 2 +- .../solver/tests/unit/sol_files/bad_objno.sol | 22 + .../tests/unit/sol_files/bad_objnoline.sol | 22 + .../tests/unit/sol_files/bad_options.sol | 22 + .../tests/unit/sol_files/conopt_optimal.sol | 22 + .../tests/unit/sol_files/depr_solver.sol | 67 +++ .../unit/sol_files/iis_no_variable_values.sol | 34 ++ .../tests/unit/sol_files/infeasible1.sol | 491 ++++++++++++++++++ .../tests/unit/sol_files/infeasible2.sol | 13 + pyomo/contrib/solver/tests/unit/test_base.py | 14 +- .../solver/tests/unit/test_sol_reader.py | 51 ++ 13 files changed, 754 insertions(+), 10 deletions(-) create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/bad_objno.sol create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/bad_objnoline.sol create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/bad_options.sol create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/conopt_optimal.sol create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/depr_solver.sol create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/iis_no_variable_values.sol create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/infeasible1.sol create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/infeasible2.sol create mode 100644 pyomo/contrib/solver/tests/unit/test_sol_reader.py diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 4c4b932381d..f70cbb5f194 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -28,7 +28,7 @@ from pyomo.contrib.solver.config import SolverConfig from pyomo.contrib.solver.factory import SolverFactory from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus -from .sol_reader import parse_sol_file +from pyomo.contrib.solver.sol_reader import parse_sol_file from pyomo.contrib.solver.solution import SolSolutionLoader, SolutionLoader from pyomo.common.tee import TeeStream from pyomo.common.log import LogStream diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index ed4fe4865c2..c4497516de2 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -15,7 +15,7 @@ from pyomo.common.errors import DeveloperError, PyomoException from pyomo.repn.plugins.nl_writer import NLWriterInfo -from .results import Results, SolutionStatus, TerminationCondition +from pyomo.contrib.solver.results import Results, SolutionStatus, TerminationCondition class SolFileData: diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index ca19e4df0e9..d4069b5b5a1 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -16,7 +16,7 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.common.collections import ComponentMap from pyomo.core.staleflag import StaleFlagManager -from .sol_reader import SolFileData +from pyomo.contrib.solver.sol_reader import SolFileData from pyomo.repn.plugins.nl_writer import NLWriterInfo from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions diff --git a/pyomo/contrib/solver/tests/unit/sol_files/bad_objno.sol b/pyomo/contrib/solver/tests/unit/sol_files/bad_objno.sol new file mode 100644 index 00000000000..a7eccfca388 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/bad_objno.sol @@ -0,0 +1,22 @@ +CONOPT 3.17A: Optimal; objective 1 +4 iterations; evals: nf = 2, ng = 0, nc = 2, nJ = 0, nH = 0, nHv = 0 + +Options +3 +1 +1 +0 +1 +1 +1 +1 +1 +1 +Xobjno 0 0 +suffix 0 1 8 0 0 +sstatus +0 1 +suffix 1 1 8 0 0 +sstatus +0 3 + diff --git a/pyomo/contrib/solver/tests/unit/sol_files/bad_objnoline.sol b/pyomo/contrib/solver/tests/unit/sol_files/bad_objnoline.sol new file mode 100644 index 00000000000..6abcacbb3c4 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/bad_objnoline.sol @@ -0,0 +1,22 @@ +CONOPT 3.17A: Optimal; objective 1 +4 iterations; evals: nf = 2, ng = 0, nc = 2, nJ = 0, nH = 0, nHv = 0 + +Options +3 +1 +1 +0 +1 +1 +1 +1 +1 +1 +objno 0 0 1 +suffix 0 1 8 0 0 +sstatus +0 1 +suffix 1 1 8 0 0 +sstatus +0 3 + diff --git a/pyomo/contrib/solver/tests/unit/sol_files/bad_options.sol b/pyomo/contrib/solver/tests/unit/sol_files/bad_options.sol new file mode 100644 index 00000000000..f59a2ffd3b4 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/bad_options.sol @@ -0,0 +1,22 @@ +CONOPT 3.17A: Optimal; objective 1 +4 iterations; evals: nf = 2, ng = 0, nc = 2, nJ = 0, nH = 0, nHv = 0 + +OXptions +3 +1 +1 +0 +1 +1 +1 +1 +1 +1 +objno 0 0 +suffix 0 1 8 0 0 +sstatus +0 1 +suffix 1 1 8 0 0 +sstatus +0 3 + diff --git a/pyomo/contrib/solver/tests/unit/sol_files/conopt_optimal.sol b/pyomo/contrib/solver/tests/unit/sol_files/conopt_optimal.sol new file mode 100644 index 00000000000..4ff14b50bc7 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/conopt_optimal.sol @@ -0,0 +1,22 @@ +CONOPT 3.17A: Optimal; objective 1 +4 iterations; evals: nf = 2, ng = 0, nc = 2, nJ = 0, nH = 0, nHv = 0 + +Options +3 +1 +1 +0 +1 +1 +1 +1 +1 +1 +objno 0 0 +suffix 0 1 8 0 0 +sstatus +0 1 +suffix 1 1 8 0 0 +sstatus +0 3 + diff --git a/pyomo/contrib/solver/tests/unit/sol_files/depr_solver.sol b/pyomo/contrib/solver/tests/unit/sol_files/depr_solver.sol new file mode 100644 index 00000000000..01ceb566334 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/depr_solver.sol @@ -0,0 +1,67 @@ +PICO Solver: final f = 88.200000 + +Options +3 +0 +0 +0 +24 +24 +32 +32 +0 +0 +0.12599999999999997 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +46.666666666666664 +0 +0 +0 +0 +0 +0 +933.3333333333336 +10000 +10000 +10000 +10000 +0 +100 +0 +100 +0 +100 +0 +100 +46.666666666666664 +53.333333333333336 +0 +100 +0 +100 +0 +100 diff --git a/pyomo/contrib/solver/tests/unit/sol_files/iis_no_variable_values.sol b/pyomo/contrib/solver/tests/unit/sol_files/iis_no_variable_values.sol new file mode 100644 index 00000000000..641a3162a8f --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/iis_no_variable_values.sol @@ -0,0 +1,34 @@ +CPLEX 12.8.0.0: integer infeasible. +0 MIP simplex iterations +0 branch-and-bound nodes +Returning an IIS of 2 variables and 1 constraints. +No basis. + +Options +3 +1 +1 +0 +1 +0 +2 +0 +objno 0 220 +suffix 0 2 4 181 11 +iis + +0 non not in the iis +1 low at lower bound +2 fix fixed +3 upp at upper bound +4 mem member +5 pmem possible member +6 plow possibly at lower bound +7 pupp possibly at upper bound +8 bug + +0 1 +1 1 +suffix 1 1 4 0 0 +iis +0 4 diff --git a/pyomo/contrib/solver/tests/unit/sol_files/infeasible1.sol b/pyomo/contrib/solver/tests/unit/sol_files/infeasible1.sol new file mode 100644 index 00000000000..9e7c47f2091 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/infeasible1.sol @@ -0,0 +1,491 @@ + +Ipopt 3.12: Converged to a locally infeasible point. Problem may be infeasible. + +Options +3 +1 +1 +0 +242 +242 +86 +86 +-3.5031247438024307e-14 +-3.5234584915901186e-14 +-3.5172095867741636e-14 +-3.530546013164763e-14 +-3.5172095867741636e-14 +-3.5305460131648396e-14 +-2.366093398247632e-13 +-2.3660933995816667e-13 +-2.366093403160036e-13 +-2.366093402111279e-13 +-2.366093403160036e-13 +-2.366093402111279e-13 +-3.230618014133495e-14 +-3.229008861611988e-14 +-3.2372291959738883e-14 +-3.233107904711923e-14 +-3.2372291959738883e-14 +-3.233107904711986e-14 +-2.366093402825742e-13 +-2.3660934046399004e-13 +-2.366093408240676e-13 +-2.3660934074259244e-13 +-2.366093408240676e-13 +-2.3660934074259244e-13 +-3.5337260190603076e-15 +-3.5384985959538063e-15 +-3.5360752870197467e-15 +-3.5401103667524204e-15 +-3.5360752870197475e-15 +-3.540110366752954e-15 +-1.1241014244910024e-13 +-7.229408362081387e-14 +-1.1241014257725814e-13 +-7.229408365067014e-14 +-1.1241014257725814e-13 +-7.229408365067014e-14 +-0.045045044618550245 +-2.2503048100082865e-13 +-0.04504504461894986 +-2.3019280438209537e-13 +-2.4246742873024166e-13 +-2.3089017630512727e-13 +-2.303517676239642e-13 +-2.3258460904987257e-13 +-2.2657149778091163e-13 +-2.3561210481068387e-13 +-2.260257681221233e-13 +-2.4196851090379605e-13 +-2.2609595226592818e-13 +-0.04504504461900244 +-2.249595193064585e-13 +-0.04504504461913233 +-2.2215413967954347e-13 +-0.045045044619133334 +1.4720100770836167e-13 +0.5405405354313707 +-1.1746366725687393e-13 +-8.181817954545458e-14 +3.3628105937413004e-10 +2.5420446367682183e-10 +-4.068865957494519e-10 +-3.3083656247909664e-10 +2.0162505532975142e-10 +1.3899803000287233e-10 +1.9264257030343367e-10 +1.5784707460270425e-10 +4.0453655296452274e-10 +1.8623815108786813e-10 +4.023012427968502e-10 +2.2427204843237042e-10 +4.285852894154949e-10 +2.7438151967949997e-10 +4.990725722952413e-10 +3.24233733037425e-10 +6.365790489375267e-10 +1.8786461752037693e-10 +9.36934851555115e-10 +1.9328729420874646e-10 +2.1302900967163764e-09 +1.9184434624295806e-10 +1.839058810801874e-10 +3.1045038304739125e-08 +2.033627397720737e-10 +1.965179362792721e-09 +3.9014568630621037e-10 +9.629991995490913e-10 +3.8529492862465446e-10 +6.543016210883198e-10 +3.1023232285992586e-10 +5.203524431666233e-10 +2.443053484937026e-10 +4.814394103716646e-10 +1.9839047821553417e-10 +2.29157081595439e-10 +1.6697733108860693e-10 +2.2885043298472609e-10 +1.4439699240241691e-10 +2.231817349184844e-10 +7.996844380007978e-07 +7.95878555840714e-07 +-6.161782990947841e-09 +-6.174783045271923e-09 +-6.180473110458713e-09 +-6.1838001759594465e-09 +-6.180473110458713e-09 +-6.183800175957144e-09 +-1.3264604647361279e-14 +-1.3437580361963064e-14 +-1.381614108205247e-14 +-1.3724139850276759e-14 +-1.381614108205247e-14 +-1.3724139850276584e-14 +-1.3264604647361279e-14 +-1.3437580361963064e-14 +-1.381614108205247e-14 +-1.3724139850276759e-14 +-1.381614108205247e-14 +-1.3724139850276584e-14 +-1.3264604647357383e-14 +-1.3264604647357383e-14 +-1.258629585661237e-14 +-1.2586303131773045e-14 +-1.2586307639008801e-14 +-1.2586311120145482e-14 +-1.2586314285443517e-14 +-1.258631748040718e-14 +-1.2586321221671653e-14 +-1.2741959563395428e-14 +-1.2741955464025058e-14 +-1.2741952925774324e-14 +-1.2741950138083889e-14 +-1.2741945491635486e-14 +-1.274193825746462e-14 +-1.3437580361959015e-14 +-1.3437580361959015e-14 +-1.3437580361959015e-14 +-1.3816141082048241e-14 +-1.3816141082048241e-14 +-1.3081851406508949e-14 +-1.308185926540242e-14 +-1.3081864134282786e-14 +-1.3081867894733614e-14 +-1.308187131400409e-14 +-1.308187476532053e-14 +-1.3081878806771144e-14 +-1.2999353684840647e-14 +-1.299934941829921e-14 +-1.2999346776539415e-14 +-1.2999343875167873e-14 +-1.2999339039238868e-14 +-1.2999331510061096e-14 +-1.3724139850272537e-14 +-1.3724139850272537e-14 +-1.3724139850272537e-14 +-1.3816141082048243e-14 +-1.3816141082048243e-14 +-1.3081851406508949e-14 +-1.3081859265402422e-14 +-1.3081864134282784e-14 +-1.3081867894733614e-14 +-1.308187131400409e-14 +-1.308187476532053e-14 +-1.3081878806771145e-14 +-1.299935368484049e-14 +-1.2999349418299049e-14 +-1.2999346776539257e-14 +-1.2999343875167712e-14 +-1.299933903923871e-14 +-1.2999331510060935e-14 +-1.3724139850272359e-14 +-1.3724139850272359e-14 +-1.3724139850272359e-14 +-0.39647376852165084 +-0.4455844823264693 +-0.3964737698727394 +-0.4455844904349083 +-0.04058112126213324 +-2.37392784926522e-13 +-0.04058112126182639 +-2.3739125313713354e-13 +-2.3738581599973924e-13 +-2.3739030469186293e-13 +-2.373886019673396e-13 +-2.3738926304868226e-13 +-2.3739032800906814e-13 +-2.373875268840388e-13 +-2.3739166112281285e-13 +-2.373848238523691e-13 +-2.3739287329689576e-13 +-0.04058112126709927 +-2.3739409684312144e-13 +-0.04058112126734901 +-2.3739552961585984e-13 +-0.040581121263560345 +-7.976233462779415e-11 +-8.149038165921345e-11 +-8.149038165921345e-11 +-8.022671984428942e-11 +-8.112229180405433e-11 +-8.112229180405698e-11 +-1.1362727144888948e-10 +-4.545363318183219e-10 +-1.5766054471383136e-10 +-999.9999999987843 +2.0239864420785628e-10 +3.6952311802810024e-10 +2.123373938372435e-10 +2.804864327332228e-10 +1.346149969721881e-10 +2.2070281853153174e-10 +1.3486437441647496e-10 +1.837701666832909e-10 +1.3214731344936636e-10 +1.59848684557641e-10 +1.2663217798563007e-10 +1.4670685236091518e-10 +1.2005152713943525e-10 +2.1846147211317584e-10 +1.1320656639453056e-10 +2.1155957764572616e-10 +1.0602947953081767e-10 +2.1331568061293854e-10 +2.2406981587244565e-10 +1.0144323269437438e-10 +2.0067712609010725e-10 +1.0647572138657723e-10 +1.3628795523686926e-10 +1.1283736217061156e-10 +1.3689006597815967e-10 +1.1944117806753888e-10 +1.4976540231691364e-10 +1.2533138246033542e-10 +1.7219937613078787e-10 +1.2782000199367948e-10 +2.0576625901474408e-10 +1.8061506448741275e-10 +2.5564782647515365e-10 +1.8080595589290967e-10 +3.3611540082361537e-10 +1.8450853640157845e-10 +-999.9999999992634 +500.00000267889834 +3700.000036997707 +3700.00003699796 +3700.000036997707 +3700.00003699796 +3700.000036977598 +3700.000036977598 +11.65620349374497 +11.697892989049905 +11.723721175743378 +11.743669409189184 +11.761807757832353 +11.780116092441125 +11.801554922843986 +11.760485435103986 +11.737564481489017 +11.723372263570411 +11.70778533743834 +11.68180544764916 +11.64135667458445 +3700.000036977598 +3700.000036977598 +3700.000036977598 +0.3151184672323908 +0.32392866804605874 +0.34244076638380455 +0.33803566597697493 +0.34244076638380455 +0.3380356659769663 +0.27110063090377123 +0.2699297687440479 +0.2929786728909554 +0.29344480424126584 +0.28838393432428394 +0.2893992806145764 +0.2710728789062779 +0.26993404119945896 +0.2934152392453943 +0.29361001971947676 +0.2884212793214469 +0.28944447549328195 +0.2710728789062779 +0.2699340411994531 +0.29341523924539437 +0.29361001971947087 +0.28842127932144684 +0.2894444754932388 +0.5508615869879336 +0.15398873818985254 +0.6718832432569866 +0.17589826345513584 +0.5247189958883286 +0.18810973351399282 +0.6259675738420305 +0.20533542867213556 +0.7121098490801165 +0.23131269225729922 +0.7821527320463884 +0.28037348913556315 +0.8428067559035302 +0.5838840489481971 +0.8970272395501521 +0.6703093152878702 +0.94267886174376 +0.7738465562949745 +0.8177198430399907 +0.9786900926762641 +0.6704296542151029 +0.9210489338249574 +0.3564282839324347 +0.8691777702202935 +0.2593618184144545 +0.8137154539828636 +0.21644752420062746 +0.7494805564573437 +0.1955192721716388 +0.6636009115148781 +0.1816326651938952 +0.7714724374833359 +0.16783059150769936 +0.6720038647474075 +0.15295832306009652 +0.5820927246947017 +0 +5.999999940000606 +3.2342062150876796 +9.747775650827162 +objno 0 200 +suffix 4 60 13 0 0 +ipopt_zU_out +22 -1.327369555645263e-09 +23 -1.3446671271054377e-09 +24 -1.382523199114386e-09 +25 -1.373323075936809e-09 +26 -1.382523199114386e-09 +27 -1.3733230759367915e-09 +28 -1.2472104315043693e-09 +29 -1.2452101972496192e-09 +30 -1.2858040647227637e-09 +31 -1.2866523403876923e-09 +32 -1.2775019286011434e-09 +33 -1.2793272952136163e-09 +34 -1.2471629472231613e-09 +35 -1.2452174844060395e-09 +36 -1.2865985041388369e-09 +37 -1.2869532717202986e-09 +38 -1.2775689743171436e-09 +39 -1.2794086668147935e-09 +40 -1.2471629472231613e-09 +41 -1.2452174844060298e-09 +42 -1.2865985041388369e-09 +43 -1.2869532717202878e-09 +44 -1.2775689743171434e-09 +45 -1.2794086668147155e-09 +46 -2.0240773556752306e-09 +47 -1.0745612255836558e-09 +48 -2.770632290509263e-09 +49 -1.103129453565228e-09 +50 -1.9127440056903688e-09 +51 -1.1197213910483093e-09 +52 -2.430513566198766e-09 +53 -1.1439932412498466e-09 +54 -3.1577699873109563e-09 +55 -1.182653712929702e-09 +56 -4.173065268467735e-09 +57 -1.2632815552706913e-09 +58 -5.783269227344645e-09 +59 -2.1847056932251413e-09 +60 -8.828459262787896e-09 +61 -2.7574054223382863e-09 +62 -1.5860201572267072e-08 +63 -4.019796745114287e-09 +64 -4.987327799213503e-09 +65 -4.128677327837785e-08 +66 -2.7584122571707027e-09 +67 -1.1514963264478648e-08 +68 -1.4125712376227499e-09 +69 -6.9490543282105264e-09 +70 -1.2274426584743552e-09 +71 -4.880119585077116e-09 +72 -1.160216995366489e-09 +73 -3.628823630675873e-09 +74 -1.13003440308759e-09 +75 -2.7024178093492304e-09 +76 -1.1108592195439713e-09 +77 -3.978035995523888e-09 +78 -1.0924348929579286e-09 +79 -2.7716511991201962e-09 +80 -1.073254036073809e-09 +81 -2.175341139896496e-09 +suffix 4 86 13 0 0 +ipopt_zL_out +0 2.457002432427315e-13 +1 2.457002432427147e-13 +2 2.457002432427315e-13 +3 2.457002432427147e-13 +4 2.457002432440668e-13 +5 2.457002432440668e-13 +6 7.799202448711829e-11 +7 7.771407288173584e-11 +8 7.754286328443318e-11 +9 7.741114609420585e-11 +10 7.72917673061454e-11 +11 7.717164255304123e-11 +12 7.703145172513595e-11 +13 7.730045781990877e-11 +14 7.7451409084917e-11 +15 7.754517112285163e-11 +16 7.76484093372809e-11 +17 7.782109643810629e-11 +18 7.809149171545744e-11 +19 2.457002432440668e-13 +20 2.457002432440668e-13 +21 2.457002432440668e-13 +22 2.88491781594494e-09 +23 2.806453922602062e-09 +24 2.6547390725285084e-09 +25 2.6893342144319893e-09 +26 2.6547390725285084e-09 +27 2.6893342144320575e-09 +28 3.3533336782625715e-09 +29 3.367879281546927e-09 +30 3.1029251008167857e-09 +31 3.0979961649984553e-09 +32 3.152363115331538e-09 +33 3.1413031705213295e-09 +34 3.353676987058653e-09 +35 3.3678259755079893e-09 +36 3.0983083240635833e-09 +37 3.096252910785026e-09 +38 3.1519549450665203e-09 +39 3.1408126764021113e-09 +40 3.353676987058653e-09 +41 3.367825975508062e-09 +42 3.0983083240635824e-09 +43 3.0962529107850877e-09 +44 3.151954945066521e-09 +45 3.140812676402579e-09 +46 1.6503072927322882e-09 +47 5.903619062223097e-09 +48 1.3530489183372102e-09 +49 5.168276510428202e-09 +50 1.7325290303934247e-09 +51 4.8327689212818915e-09 +52 1.4522971044995076e-09 +53 4.4273454737645e-09 +54 1.276616097383978e-09 +55 3.930138360770138e-09 +56 1.1622933223262232e-09 +57 3.242428123819113e-09 +58 1.0786469044524248e-09 +59 1.556971619947646e-09 +60 1.0134484872637181e-09 +61 1.356225961423535e-09 +62 9.643698375125132e-10 +63 1.174768939146355e-09 +64 1.1117388275802617e-09 +65 9.288986889801197e-10 +66 1.3559825252250914e-09 +67 9.870172368223874e-10 +68 2.55055764727633e-09 +69 1.0459205566343963e-09 +70 3.5051068618760334e-09 +71 1.1172098225860037e-09 +72 4.2000521577056155e-09 +73 1.212961283078632e-09 +74 4.649622902405193e-09 +75 1.3699361786951016e-09 +76 5.005106744564875e-09 +77 1.1783841562800436e-09 +78 5.416717299785639e-09 +79 1.3528060526165563e-09 +80 5.943389257560972e-09 +81 1.561763024323873e-09 +82 500.00000026951534 +83 1.515151527777625e-10 +84 2.8108595681091103e-10 +85 9.326135918021712e-11 diff --git a/pyomo/contrib/solver/tests/unit/sol_files/infeasible2.sol b/pyomo/contrib/solver/tests/unit/sol_files/infeasible2.sol new file mode 100644 index 00000000000..6fddb053745 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/infeasible2.sol @@ -0,0 +1,13 @@ + + Couenne (C:\Users\SASCHA~1\AppData\Local\Temp\tmpvcmknhw0.pyomo.nl May 18 2015): Infeasible + +Options +3 +0 +1 +0 +242 +0 +86 +0 +objno 0 220 diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index cda1631d921..59a80ba270b 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -189,7 +189,7 @@ def test_class_method_list(self): def test_context_manager(self): with base.LegacySolverWrapper() as instance: - with self.assertRaises(AttributeError) as context: + with self.assertRaises(AttributeError): instance.available() def test_map_config(self): @@ -209,14 +209,14 @@ def test_map_config(self): self.assertFalse(instance.config.load_solutions) self.assertEqual(instance.config.time_limit, 20) # Report timing shouldn't be created because it no longer exists - with self.assertRaises(AttributeError) as context: + with self.assertRaises(AttributeError): print(instance.config.report_timing) # Keepfiles should not be created because we did not declare keepfiles on # the original config - with self.assertRaises(AttributeError) as context: + with self.assertRaises(AttributeError): print(instance.config.keepfiles) # We haven't implemented solver_io, suffixes, or logfile - with self.assertRaises(NotImplementedError) as context: + with self.assertRaises(NotImplementedError): instance._map_config( False, False, @@ -231,7 +231,7 @@ def test_map_config(self): None, None, ) - with self.assertRaises(NotImplementedError) as context: + with self.assertRaises(NotImplementedError): instance._map_config( False, False, @@ -246,7 +246,7 @@ def test_map_config(self): None, None, ) - with self.assertRaises(NotImplementedError) as context: + with self.assertRaises(NotImplementedError): instance._map_config( False, False, @@ -266,7 +266,7 @@ def test_map_config(self): False, False, False, 20, False, False, None, None, None, True, None, None ) self.assertEqual(instance.config.working_dir, os.getcwd()) - with self.assertRaises(AttributeError) as context: + with self.assertRaises(AttributeError): print(instance.config.keepfiles) def test_map_results(self): diff --git a/pyomo/contrib/solver/tests/unit/test_sol_reader.py b/pyomo/contrib/solver/tests/unit/test_sol_reader.py new file mode 100644 index 00000000000..0ab94dfc4ac --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/test_sol_reader.py @@ -0,0 +1,51 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.common.fileutils import this_file_dir +from pyomo.common.tempfiles import TempfileManager +from pyomo.contrib.solver.sol_reader import parse_sol_file, SolFileData + +currdir = this_file_dir() + + +class TestSolFileData(unittest.TestCase): + def test_default_instantiation(self): + instance = SolFileData() + self.assertIsInstance(instance.primals, list) + self.assertIsInstance(instance.duals, list) + self.assertIsInstance(instance.var_suffixes, dict) + self.assertIsInstance(instance.con_suffixes, dict) + self.assertIsInstance(instance.obj_suffixes, dict) + self.assertIsInstance(instance.problem_suffixes, dict) + self.assertIsInstance(instance.other, list) + + +class TestSolParser(unittest.TestCase): + # I am not sure how to write these tests best since the sol parser requires + # not only a file but also the nl_info and results objects. + def setUp(self): + TempfileManager.push() + + def tearDown(self): + TempfileManager.pop(remove=True) + + def test_default_behavior(self): + pass + + def test_custom_behavior(self): + pass + + def test_infeasible1(self): + pass + + def test_infeasible2(self): + pass From f4355927cfe9592e1a88ce351fd5b77e8c35d080 Mon Sep 17 00:00:00 2001 From: Sakshi <73687517+Sakshi21299@users.noreply.github.com> Date: Wed, 14 Feb 2024 17:38:24 -0500 Subject: [PATCH 0540/3044] Update pyomo/contrib/incidence_analysis/tests/test_interface.py Co-authored-by: Bethany Nicholson --- pyomo/contrib/incidence_analysis/tests/test_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 01c15c9c84d..2908a4dd04a 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1802,7 +1802,7 @@ def test_add_edge(self): igraph = IncidenceGraphInterface(m, linear_only=False) n_edges_original = igraph.n_edges - # Test edge is added between previously unconnectes nodes + # Test edge is added between previously unconnected nodes igraph.add_edge(m.x[1], m.eq3) n_edges_new = igraph.n_edges assert ComponentSet(igraph.get_adjacent_to(m.eq3)) == ComponentSet(m.x[:]) From dbb92306b29882ee94f481349ffb128ab7eee37c Mon Sep 17 00:00:00 2001 From: Sakshi <73687517+Sakshi21299@users.noreply.github.com> Date: Wed, 14 Feb 2024 17:40:44 -0500 Subject: [PATCH 0541/3044] fix typo --- pyomo/contrib/incidence_analysis/tests/test_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 2908a4dd04a..3ec37d5531a 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1836,7 +1836,7 @@ def test_var_elim(self): m.eq4 = pyo.Constraint(expr=m.x[1] == 5 * m.x[2]) igraph = IncidenceGraphInterface(m) - # Eliminate x[1] usinf eq4 + # Eliminate x[1] using eq4 for adj_con in igraph.get_adjacent_to(m.x[1]): for adj_var in igraph.get_adjacent_to(m.eq4): igraph.add_edge(adj_var, adj_con) From 273fd72d1587093b67eca832d9606f78b3a88b1b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 14 Feb 2024 15:50:17 -0700 Subject: [PATCH 0542/3044] Add init file --- pyomo/contrib/solver/tests/unit/sol_files/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 pyomo/contrib/solver/tests/unit/sol_files/__init__.py diff --git a/pyomo/contrib/solver/tests/unit/sol_files/__init__.py b/pyomo/contrib/solver/tests/unit/sol_files/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/sol_files/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ From b0ecba2421219e46679c47578d82ce67bd15db04 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 14 Feb 2024 20:10:29 -0500 Subject: [PATCH 0543/3044] Update name and base class of solver arg exception --- pyomo/contrib/pyros/config.py | 8 ++++---- pyomo/contrib/pyros/tests/test_config.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 3256a333fdc..798e68b157f 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -15,7 +15,7 @@ InEnum, Path, ) -from pyomo.common.errors import ApplicationError +from pyomo.common.errors import ApplicationError, PyomoException from pyomo.core.base import Var, _VarData from pyomo.core.base.param import Param, _ParamData from pyomo.opt import SolverFactory @@ -303,7 +303,7 @@ def domain_name(self): ) -class NotSolverResolvable(Exception): +class SolverNotResolvable(PyomoException): """ Exception type for failure to cast an object to a Pyomo solver. """ @@ -382,7 +382,7 @@ def __call__(self, obj, require_available=None, solver_desc=None): Raises ------ - NotSolverResolvable + SolverNotResolvable If `obj` cannot be cast to a Pyomo solver because it is neither a str nor a Pyomo solver type. ApplicationError @@ -405,7 +405,7 @@ def __call__(self, obj, require_available=None, solver_desc=None): elif self.is_solver_type(obj): solver = obj else: - raise NotSolverResolvable( + raise SolverNotResolvable( f"Cannot cast object `{obj!r}` to a Pyomo optimizer for use as " f"{solver_desc}, as the object is neither a str nor a " f"Pyomo Solver type (got type {type(obj).__name__})." diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 3113afaac89..eaed462a9b3 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -15,7 +15,7 @@ InputDataStandardizer, mutable_param_validator, LoggerType, - NotSolverResolvable, + SolverNotResolvable, PathLikeOrNone, PositiveIntOrMinusOne, pyros_config, @@ -397,7 +397,7 @@ def test_solver_resolvable_invalid_type(self): r"Cannot cast object `2` to a Pyomo optimizer.*" r"local solver.*got type int.*" ) - with self.assertRaisesRegex(NotSolverResolvable, exc_str): + with self.assertRaisesRegex(SolverNotResolvable, exc_str): standardizer_func(invalid_object) def test_solver_resolvable_unavailable_solver(self): @@ -542,7 +542,7 @@ def test_solver_iterable_invalid_list(self): r"Cannot cast object `2` to a Pyomo optimizer.*" r"backup solver.*index 1.*got type int.*" ) - with self.assertRaisesRegex(NotSolverResolvable, exc_str): + with self.assertRaisesRegex(SolverNotResolvable, exc_str): standardizer_func(invalid_object) From f52db9d2efe9fdc1317183ad8c9e9f347d706bab Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 07:47:40 -0700 Subject: [PATCH 0544/3044] Update base member unit test - better checking logic --- pyomo/contrib/solver/tests/unit/test_base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 59a80ba270b..b8d5c79fc0f 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -104,7 +104,6 @@ def test_class_method_list(self): expected_list = [ 'Availability', 'CONFIG', - '_abc_impl', '_get_duals', '_get_primals', '_get_reduced_costs', @@ -129,7 +128,7 @@ def test_class_method_list(self): method_list = [ method for method in dir(base.PersistentSolverBase) - if method.startswith('__') is False + if (method.startswith('__') or method.startswith('_abc')) is False ] self.assertEqual(sorted(expected_list), sorted(method_list)) From b99221cfaf58d1bada29f3e8d059c1ca4a6a1585 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 08:32:43 -0700 Subject: [PATCH 0545/3044] Address missing coverage for several modules --- pyomo/common/config.py | 2 ++ pyomo/common/tests/test_config.py | 18 +++++++++++++++ .../contrib/solver/tests/unit/test_results.py | 22 +++++++++++++++++++ .../solver/tests/unit/test_solution.py | 6 +++++ 4 files changed, 48 insertions(+) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 657765fdc02..4adb0299f0e 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -211,6 +211,8 @@ def Datetime(val): This domain will return the original object, assuming it is of the right type. """ + if val is None: + return val if not isinstance(val, datetime.datetime): raise ValueError(f"Expected datetime object, but received {type(val)}.") return val diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index e2a8c0fb591..ac23e4c54d3 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -25,6 +25,7 @@ # ___________________________________________________________________________ import argparse +import datetime import enum import os import os.path @@ -47,6 +48,7 @@ def yaml_load(arg): ConfigDict, ConfigValue, ConfigList, + Datetime, MarkImmutable, ImmutableConfigValue, Bool, @@ -738,6 +740,22 @@ def _rule(key, val): } ) + def test_Datetime(self): + c = ConfigDict() + c.declare('a', ConfigValue(domain=Datetime, default=None)) + self.assertEqual(c.get('a').domain_name(), 'Datetime') + + self.assertEqual(c.a, None) + c.a = datetime.datetime(2022, 1, 1) + self.assertEqual(c.a, datetime.datetime(2022, 1, 1)) + + with self.assertRaises(ValueError): + c.a = 5 + with self.assertRaises(ValueError): + c.a = 'Hello' + with self.assertRaises(ValueError): + c.a = False + class TestImmutableConfigValue(unittest.TestCase): def test_immutable_config_value(self): diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index caef82129ec..4672903cb43 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -9,6 +9,9 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import sys +from io import StringIO + from pyomo.common import unittest from pyomo.common.config import ConfigDict from pyomo.contrib.solver import results @@ -118,6 +121,25 @@ def test_default_initialization(self): ): res.solution_loader.get_reduced_costs() + def test_display(self): + res = results.Results() + stream = StringIO() + res.display(ostream=stream) + expected_print = """solution_loader: None +termination_condition: TerminationCondition.unknown +solution_status: SolutionStatus.noSolution +incumbent_objective: None +objective_bound: None +solver_name: None +solver_version: None +iteration_count: None +timing_info: + start_timestamp: None + wall_time: None +extra_info: +""" + self.assertEqual(expected_print, stream.getvalue()) + def test_generated_results(self): m = pyo.ConcreteModel() m.x = ScalarVar() diff --git a/pyomo/contrib/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py index bbcc85bdac8..7a18344d4cb 100644 --- a/pyomo/contrib/solver/tests/unit/test_solution.py +++ b/pyomo/contrib/solver/tests/unit/test_solution.py @@ -80,3 +80,9 @@ def test_default_initialization(self): self.instance = PersistentSolutionLoader('ipopt') self.assertTrue(self.instance._valid) self.assertEqual(self.instance._solver, 'ipopt') + + def test_invalid(self): + self.instance = PersistentSolutionLoader('ipopt') + self.instance.invalidate() + with self.assertRaises(RuntimeError): + self.instance.get_primals() From a45e0854746ab4d54b69a8160bab8386ff3b07e0 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 08:38:43 -0700 Subject: [PATCH 0546/3044] Add copyright statements; clean up unused imports --- pyomo/contrib/solver/gurobi.py | 26 +++++++++++++------ .../tests/solvers/test_gurobi_persistent.py | 15 ++++++++--- .../solver/tests/solvers/test_solvers.py | 12 ++++++++- .../contrib/solver/tests/unit/test_results.py | 1 - 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 50d241e1e88..919e7ae3995 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -1,7 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from collections.abc import Iterable import logging import math -from typing import List, Dict, Optional +from typing import List, Optional from pyomo.common.collections import ComponentSet, ComponentMap, OrderedSet from pyomo.common.log import LogStream from pyomo.common.dependencies import attempt_import @@ -9,10 +20,10 @@ from pyomo.common.tee import capture_output, TeeStream from pyomo.common.timing import HierarchicalTimer from pyomo.common.shutdown import python_is_shutting_down -from pyomo.common.config import ConfigValue, NonNegativeInt +from pyomo.common.config import ConfigValue from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.base.var import Var, _GeneralVarData +from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData @@ -28,7 +39,6 @@ import sys import datetime import io -from pyomo.contrib.solver.factory import SolverFactory logger = logging.getLogger(__name__) @@ -1137,9 +1147,9 @@ def set_linear_constraint_attr(self, con, attr, val): """ if attr in {'Sense', 'RHS', 'ConstrName'}: raise ValueError( - 'Linear constraint attr {0} cannot be set with' + 'Linear constraint attr {0} cannot be set with'.format(attr) + ' the set_linear_constraint_attr method. Please use' - + ' the remove_constraint and add_constraint methods.'.format(attr) + + ' the remove_constraint and add_constraint methods.' ) self._pyomo_con_to_solver_con_map[con].setAttr(attr, val) self._needs_updated = True @@ -1166,9 +1176,9 @@ def set_var_attr(self, var, attr, val): """ if attr in {'LB', 'UB', 'VType', 'VarName'}: raise ValueError( - 'Var attr {0} cannot be set with' + 'Var attr {0} cannot be set with'.format(attr) + ' the set_var_attr method. Please use' - + ' the update_var method.'.format(attr) + + ' the update_var method.' ) if attr == 'Obj': raise ValueError( diff --git a/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py index f53088506f9..d4c0078a0df 100644 --- a/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py +++ b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py @@ -1,9 +1,18 @@ -from pyomo.common.errors import PyomoException +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest import pyomo.environ as pe from pyomo.contrib.solver.gurobi import Gurobi -from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus -from pyomo.core.expr.numeric_expr import LinearExpression +from pyomo.contrib.solver.results import SolutionStatus from pyomo.core.expr.taylor_series import taylor_series_expansion diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 6b798f9bafd..36f3596e890 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pe from pyomo.common.dependencies import attempt_import import pyomo.common.unittest as unittest @@ -10,7 +21,6 @@ from pyomo.contrib.solver.gurobi import Gurobi from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression -import os import math numpy, numpy_available = attempt_import('numpy') diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 4672903cb43..8e16a1384ee 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -9,7 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import sys from io import StringIO from pyomo.common import unittest From 6d21b71c6c95b448ff4cb16ff4d8b646611becc7 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 15 Feb 2024 08:52:17 -0700 Subject: [PATCH 0547/3044] updating docs --- doc/OnlineDocs/conf.py | 1 + .../developer_reference/solvers.rst | 35 +++++++++++-------- pyomo/contrib/solver/base.py | 29 +++++++-------- pyomo/contrib/solver/results.py | 4 +-- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index ef6510daedf..88f84ec8b37 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -72,6 +72,7 @@ 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx_copybutton', + 'enum_tools.autoenum', #'sphinx.ext.githubpages', ] diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index fa24d69a211..45d8e55daf9 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -10,7 +10,8 @@ Interface Implementation ------------------------ All new interfaces should be built upon one of two classes (currently): -``pyomo.contrib.solver.base.SolverBase`` or ``pyomo.contrib.solver.base.PersistentSolverBase``. +:class:`SolverBase` or +:class:`PersistentSolverBase`. All solvers should have the following: @@ -26,9 +27,11 @@ Persistent solvers should also include: Results ------- -Every solver, at the end of a ``solve`` call, will return a ``Results`` object. -This object is a :py:class:`pyomo.common.config.ConfigDict`, which can be manipulated similar -to a standard ``dict`` in Python. +Every solver, at the end of a +:meth:`solve` call, will +return a :class:`Results` +object. This object is a :py:class:`pyomo.common.config.ConfigDict`, +which can be manipulated similar to a standard ``dict`` in Python. .. autoclass:: pyomo.contrib.solver.results.Results :show-inheritance: @@ -40,28 +43,29 @@ Termination Conditions ^^^^^^^^^^^^^^^^^^^^^^ Pyomo offers a standard set of termination conditions to map to solver -returns. The intent of ``TerminationCondition`` is to notify the user of why -the solver exited. The user is expected to inspect the ``Results`` object or any -returned solver messages or logs for more information. - - +returns. The intent of +:class:`TerminationCondition` +is to notify the user of why the solver exited. The user is expected +to inspect the :class:`Results` +object or any returned solver messages or logs for more information. .. autoclass:: pyomo.contrib.solver.results.TerminationCondition :show-inheritance: - :noindex: Solution Status ^^^^^^^^^^^^^^^ -Pyomo offers a standard set of solution statuses to map to solver output. The -intent of ``SolutionStatus`` is to notify the user of what the solver returned -at a high level. The user is expected to inspect the ``Results`` object or any +Pyomo offers a standard set of solution statuses to map to solver +output. The intent of +:class:`SolutionStatus` +is to notify the user of what the solver returned at a high level. The +user is expected to inspect the +:class:`Results` object or any returned solver messages or logs for more information. .. autoclass:: pyomo.contrib.solver.results.SolutionStatus :show-inheritance: - :noindex: Solution @@ -71,6 +75,7 @@ Solutions can be loaded back into a model using a ``SolutionLoader``. A specific loader should be written for each unique case. Several have already been implemented. For example, for ``ipopt``: -.. autoclass:: pyomo.contrib.solver.solution.SolSolutionLoader +.. autoclass:: pyomo.contrib.solver.ipopt.ipoptSolutionLoader :show-inheritance: :members: + :inherited-members: diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 96b87924bf6..aad8d10ec63 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -40,15 +40,18 @@ class SolverBase(abc.ABC): """ - Base class upon which direct solver interfaces can be built. - - This base class contains the required methods for all direct solvers: - - available: Determines whether the solver is able to be run, combining - both whether it can be found on the system and if the license is valid. - - config: The configuration method for solver objects. + This base class defines the methods required for all solvers: + - available: Determines whether the solver is able to be run, + combining both whether it can be found on the system and if the license is valid. - solve: The main method of every solver - version: The version of the solver - - is_persistent: Set to false for all direct solvers. + - is_persistent: Set to false for all non-persistent solvers. + + Additionally, solvers should have a :attr:`config` attribute that + inherits from one of :class:`SolverConfig`, + :class:`BranchAndBoundConfig`, + :class:`PersistentSolverConfig`, or + :class:`PersistentBranchAndBoundConfig`. """ CONFIG = SolverConfig() @@ -104,7 +107,7 @@ def __str__(self): @abc.abstractmethod def solve( - self, model: _BlockData, timer: HierarchicalTimer = None, **kwargs + self, model: _BlockData, **kwargs ) -> Results: """ Solve a Pyomo model. @@ -113,15 +116,13 @@ def solve( ---------- model: _BlockData The Pyomo model to be solved - timer: HierarchicalTimer - An option timer for reporting timing **kwargs Additional keyword arguments (including solver_options - passthrough options; delivered directly to the solver (with no validation)) Returns ------- - results: Results + results: :class:`Results` A results object """ @@ -144,7 +145,7 @@ def available(self): Returns ------- - available: Solver.Availability + available: SolverBase.Availability An enum that indicates "how available" the solver is. Note that the enum can be cast to bool, which will be True if the solver is runable at all and False @@ -173,10 +174,10 @@ def is_persistent(self): class PersistentSolverBase(SolverBase): """ Base class upon which persistent solvers can be built. This inherits the - methods from the direct solver base and adds those methods that are necessary + methods from the solver base class and adds those methods that are necessary for persistent solvers. - Example usage can be seen in the GUROBI solver. + Example usage can be seen in the Gurobi interface. """ def is_persistent(self): diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index 5ed6de44430..e80bad126a1 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -139,10 +139,10 @@ class Results(ConfigDict): ---------- solution_loader: SolutionLoaderBase Object for loading the solution back into the model. - termination_condition: TerminationCondition + termination_condition: :class:`TerminationCondition` The reason the solver exited. This is a member of the TerminationCondition enum. - solution_status: SolutionStatus + solution_status: :class:`SolutionStatus` The result of the solve call. This is a member of the SolutionStatus enum. incumbent_objective: float From 69905e5ddfa4a02f2ce19ec0537a06c3a4711ade Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 15 Feb 2024 09:47:56 -0700 Subject: [PATCH 0548/3044] Only using infeasible termination condition from trusted solvers, adding tests --- pyomo/gdp/plugins/multiple_bigm.py | 94 +++++++++++++----------------- pyomo/gdp/tests/test_mbigm.py | 39 ++++++++++++- 2 files changed, 79 insertions(+), 54 deletions(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 1cd9af2911b..bdab6363c3a 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -59,6 +59,8 @@ logger = logging.getLogger('pyomo.gdp.mbigm') +_trusted_solvers = {'gurobi', 'cplex', 'cbc', 'glpk', 'scip', 'xpress_direct', + 'mosek_direct', 'baron', 'apsi_highs'} @TransformationFactory.register( 'gdp.mbigm', @@ -623,68 +625,25 @@ def _calculate_missing_M_values( self.used_args[constraint, other_disjunct] = (lower_M, upper_M) else: (lower_M, upper_M) = (None, None) + unsuccessful_solve_msg = ( + "Unsuccessful solve to calculate M value to " + "relax constraint '%s' on Disjunct '%s' when " + "Disjunct '%s' is selected." + % (constraint.name, disjunct.name, other_disjunct.name)) if constraint.lower is not None and lower_M is None: # last resort: calculate if lower_M is None: scratch.obj.expr = constraint.body - constraint.lower scratch.obj.sense = minimize - results = self._config.solver.solve( - other_disjunct, load_solutions=False - ) - if ( - results.solver.termination_condition - is TerminationCondition.infeasible - ): - logger.debug( - "Disjunct '%s' is infeasible, deactivating." - % other_disjunct.name - ) - other_disjunct.deactivate() - lower_M = 0 - elif ( - results.solver.termination_condition - is not TerminationCondition.optimal - ): - raise GDP_Error( - "Unsuccessful solve to calculate M value to " - "relax constraint '%s' on Disjunct '%s' when " - "Disjunct '%s' is selected." - % (constraint.name, disjunct.name, other_disjunct.name) - ) - else: - other_disjunct.solutions.load_from(results) - lower_M = value(scratch.obj.expr) + lower_M = self._solve_disjunct_for_M(other_disjunct, scratch, + unsuccessful_solve_msg) if constraint.upper is not None and upper_M is None: # last resort: calculate if upper_M is None: scratch.obj.expr = constraint.body - constraint.upper scratch.obj.sense = maximize - results = self._config.solver.solve( - other_disjunct, load_solutions=False - ) - if ( - results.solver.termination_condition - is TerminationCondition.infeasible - ): - logger.debug( - "Disjunct '%s' is infeasible, deactivating." - % other_disjunct.name - ) - other_disjunct.deactivate() - upper_M = 0 - elif ( - results.solver.termination_condition - is not TerminationCondition.optimal - ): - raise GDP_Error( - "Unsuccessful solve to calculate M value to " - "relax constraint '%s' on Disjunct '%s' when " - "Disjunct '%s' is selected." - % (constraint.name, disjunct.name, other_disjunct.name) - ) - else: - other_disjunct.solutions.load_from(results) - upper_M = value(scratch.obj.expr) + upper_M = self._solve_disjunct_for_M(other_disjunct, scratch, + unsuccessful_solve_msg) arg_Ms[constraint, other_disjunct] = (lower_M, upper_M) transBlock._mbm_values[constraint, other_disjunct] = (lower_M, upper_M) @@ -694,6 +653,37 @@ def _calculate_missing_M_values( return arg_Ms + def _solve_disjunct_for_M(self, other_disjunct, scratch_block, + unsuccessful_solve_msg): + solver = self._config.solver + solver_trusted = solver.name in _trusted_solvers + results = solver.solve(other_disjunct, load_solutions=False) + if (results.solver.termination_condition is + TerminationCondition.infeasible ): + if solver_trusted: + logger.debug( + "Disjunct '%s' is infeasible, deactivating." + % other_disjunct.name + ) + other_disjunct.deactivate() + M = 0 + else: + # This is a solver that might report + # 'infeasible' for local infeasibility, so we + # can't deactivate with confidence. To be + # conservative, we'll just complain about + # it. Post-solver-rewrite we will want to change + # this so that we check for 'proven_infeasible' + # and then we can abandon this hack + raise GDP_Error(unsuccessful_solve_msg) + elif (results.solver.termination_condition is not + TerminationCondition.optimal): + raise GDP_Error(unsuccessful_solve_msg) + else: + other_disjunct.solutions.load_from(results) + M = value(scratch_block.obj.expr) + return M + def _warn_for_active_suffix(self, suffix, disjunct, active_disjuncts, Ms): if suffix.local_name == 'BigM': logger.debug( diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index 51230bab075..d65f6e350dd 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -992,8 +992,7 @@ def test_two_term_indexed_disjunction(self): class EdgeCases(unittest.TestCase): - @unittest.skipUnless(gurobi_available, "Gurobi is not available") - def test_calculate_Ms_infeasible_Disjunct(self): + def make_infeasible_disjunct_model(self): m = ConcreteModel() m.x = Var(bounds=(1, 12)) m.y = Var(bounds=(19, 22)) @@ -1004,7 +1003,11 @@ def test_calculate_Ms_infeasible_Disjunct(self): [m.x == m.y - 9], # x in interval [10, 12] ] ) + return m + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_calculate_Ms_infeasible_Disjunct(self): + m = self.make_infeasible_disjunct_model() out = StringIO() mbm = TransformationFactory('gdp.mbigm') with LoggingIntercept(out, 'pyomo.gdp.mbigm', logging.DEBUG): @@ -1050,3 +1053,35 @@ def test_calculate_Ms_infeasible_Disjunct(self): <= 0.0 * m.disjunction_disjuncts[0].binary_indicator_var - 12.0 * m.disjunction_disjuncts[1].binary_indicator_var, ) + + @unittest.skipUnless(SolverFactory('ipopt').available(exception_flag=False), + "Ipopt is not available") + def test_calculate_Ms_infeasible_Disjunct_local_solver(self): + m = self.make_infeasible_disjunct_model() + with self.assertRaisesRegex( + GDP_Error, + r"Unsuccessful solve to calculate M value to " + r"relax constraint 'disjunction_disjuncts\[1\].constraint\[1\]' " + r"on Disjunct 'disjunction_disjuncts\[1\]' when " + r"Disjunct 'disjunction_disjuncts\[0\]' is selected."): + TransformationFactory('gdp.mbigm').apply_to( + m, solver=SolverFactory('ipopt'), + reduce_bound_constraints=False) + + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_politely_ignore_BigM_Suffix(self): + m = self.make_infeasible_disjunct_model() + m.disjunction.disjuncts[0].deactivate() + m.disjunction.disjuncts[1].BigM = Suffix(direction=Suffix.LOCAL) + out = StringIO() + with LoggingIntercept(out, 'pyomo.gdp.mbigm', logging.DEBUG): + TransformationFactory('gdp.mbigm').apply_to( + m, reduce_bound_constraints=False) + warnings = out.getvalue() + self.assertIn( + r"Found active 'BigM' Suffix on 'disjunction_disjuncts[1]'. " + r"The multiple bigM transformation does not currently " + r"support specifying M's with Suffixes and is ignoring " + r"this Suffix.", + warnings, + ) From 877c0aa57bcfc02e559a1736bfda97fb60ba08e7 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 15 Feb 2024 09:48:16 -0700 Subject: [PATCH 0549/3044] black --- pyomo/gdp/plugins/multiple_bigm.py | 41 +++++++++++++++++++----------- pyomo/gdp/tests/test_mbigm.py | 29 +++++++++++---------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index bdab6363c3a..803d2e80807 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -59,8 +59,18 @@ logger = logging.getLogger('pyomo.gdp.mbigm') -_trusted_solvers = {'gurobi', 'cplex', 'cbc', 'glpk', 'scip', 'xpress_direct', - 'mosek_direct', 'baron', 'apsi_highs'} +_trusted_solvers = { + 'gurobi', + 'cplex', + 'cbc', + 'glpk', + 'scip', + 'xpress_direct', + 'mosek_direct', + 'baron', + 'apsi_highs', +} + @TransformationFactory.register( 'gdp.mbigm', @@ -629,21 +639,24 @@ def _calculate_missing_M_values( "Unsuccessful solve to calculate M value to " "relax constraint '%s' on Disjunct '%s' when " "Disjunct '%s' is selected." - % (constraint.name, disjunct.name, other_disjunct.name)) + % (constraint.name, disjunct.name, other_disjunct.name) + ) if constraint.lower is not None and lower_M is None: # last resort: calculate if lower_M is None: scratch.obj.expr = constraint.body - constraint.lower scratch.obj.sense = minimize - lower_M = self._solve_disjunct_for_M(other_disjunct, scratch, - unsuccessful_solve_msg) + lower_M = self._solve_disjunct_for_M( + other_disjunct, scratch, unsuccessful_solve_msg + ) if constraint.upper is not None and upper_M is None: # last resort: calculate if upper_M is None: scratch.obj.expr = constraint.body - constraint.upper scratch.obj.sense = maximize - upper_M = self._solve_disjunct_for_M(other_disjunct, scratch, - unsuccessful_solve_msg) + upper_M = self._solve_disjunct_for_M( + other_disjunct, scratch, unsuccessful_solve_msg + ) arg_Ms[constraint, other_disjunct] = (lower_M, upper_M) transBlock._mbm_values[constraint, other_disjunct] = (lower_M, upper_M) @@ -653,17 +666,16 @@ def _calculate_missing_M_values( return arg_Ms - def _solve_disjunct_for_M(self, other_disjunct, scratch_block, - unsuccessful_solve_msg): + def _solve_disjunct_for_M( + self, other_disjunct, scratch_block, unsuccessful_solve_msg + ): solver = self._config.solver solver_trusted = solver.name in _trusted_solvers results = solver.solve(other_disjunct, load_solutions=False) - if (results.solver.termination_condition is - TerminationCondition.infeasible ): + if results.solver.termination_condition is TerminationCondition.infeasible: if solver_trusted: logger.debug( - "Disjunct '%s' is infeasible, deactivating." - % other_disjunct.name + "Disjunct '%s' is infeasible, deactivating." % other_disjunct.name ) other_disjunct.deactivate() M = 0 @@ -676,8 +688,7 @@ def _solve_disjunct_for_M(self, other_disjunct, scratch_block, # this so that we check for 'proven_infeasible' # and then we can abandon this hack raise GDP_Error(unsuccessful_solve_msg) - elif (results.solver.termination_condition is not - TerminationCondition.optimal): + elif results.solver.termination_condition is not TerminationCondition.optimal: raise GDP_Error(unsuccessful_solve_msg) else: other_disjunct.solutions.load_from(results) diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index d65f6e350dd..dc395c87576 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -1054,19 +1054,21 @@ def test_calculate_Ms_infeasible_Disjunct(self): - 12.0 * m.disjunction_disjuncts[1].binary_indicator_var, ) - @unittest.skipUnless(SolverFactory('ipopt').available(exception_flag=False), - "Ipopt is not available") + @unittest.skipUnless( + SolverFactory('ipopt').available(exception_flag=False), "Ipopt is not available" + ) def test_calculate_Ms_infeasible_Disjunct_local_solver(self): m = self.make_infeasible_disjunct_model() with self.assertRaisesRegex( - GDP_Error, - r"Unsuccessful solve to calculate M value to " - r"relax constraint 'disjunction_disjuncts\[1\].constraint\[1\]' " - r"on Disjunct 'disjunction_disjuncts\[1\]' when " - r"Disjunct 'disjunction_disjuncts\[0\]' is selected."): + GDP_Error, + r"Unsuccessful solve to calculate M value to " + r"relax constraint 'disjunction_disjuncts\[1\].constraint\[1\]' " + r"on Disjunct 'disjunction_disjuncts\[1\]' when " + r"Disjunct 'disjunction_disjuncts\[0\]' is selected.", + ): TransformationFactory('gdp.mbigm').apply_to( - m, solver=SolverFactory('ipopt'), - reduce_bound_constraints=False) + m, solver=SolverFactory('ipopt'), reduce_bound_constraints=False + ) @unittest.skipUnless(gurobi_available, "Gurobi is not available") def test_politely_ignore_BigM_Suffix(self): @@ -1076,12 +1078,13 @@ def test_politely_ignore_BigM_Suffix(self): out = StringIO() with LoggingIntercept(out, 'pyomo.gdp.mbigm', logging.DEBUG): TransformationFactory('gdp.mbigm').apply_to( - m, reduce_bound_constraints=False) + m, reduce_bound_constraints=False + ) warnings = out.getvalue() self.assertIn( - r"Found active 'BigM' Suffix on 'disjunction_disjuncts[1]'. " - r"The multiple bigM transformation does not currently " - r"support specifying M's with Suffixes and is ignoring " + r"Found active 'BigM' Suffix on 'disjunction_disjuncts[1]'. " + r"The multiple bigM transformation does not currently " + r"support specifying M's with Suffixes and is ignoring " r"this Suffix.", warnings, ) From 6c3739e6bd2c52a6ed3daaf3c14c92346ed31c5e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 09:53:56 -0700 Subject: [PATCH 0550/3044] Update docstrings; fix one test to account for pypy differences --- pyomo/contrib/solver/config.py | 33 ++++++++++++++----- .../contrib/solver/tests/unit/test_results.py | 5 ++- pyomo/contrib/solver/util.py | 2 +- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 2a1a129d1ac..e38f903e1ac 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -23,7 +23,7 @@ class SolverConfig(ConfigDict): """ - Base config values for all solver interfaces + Base config for all direct solver interfaces """ def __init__( @@ -118,13 +118,14 @@ def __init__( class BranchAndBoundConfig(SolverConfig): """ + Base config for all direct MIP solver interfaces + Attributes ---------- - mip_gap: float - Solver will terminate if the mip gap is less than mip_gap - relax_integrality: bool - If True, all integer variables will be relaxed to continuous - variables before solving + rel_gap: float + The relative value of the gap in relation to the best bound + abs_gap: float + The absolute value of the difference between the incumbent and best bound """ def __init__( @@ -144,10 +145,20 @@ def __init__( ) self.rel_gap: Optional[float] = self.declare( - 'rel_gap', ConfigValue(domain=NonNegativeFloat) + 'rel_gap', + ConfigValue( + domain=NonNegativeFloat, + description="Optional termination condition; the relative value of the " + "gap in relation to the best bound", + ), ) self.abs_gap: Optional[float] = self.declare( - 'abs_gap', ConfigValue(domain=NonNegativeFloat) + 'abs_gap', + ConfigValue( + domain=NonNegativeFloat, + description="Optional termination condition; the absolute value of the " + "difference between the incumbent and best bound", + ), ) @@ -315,6 +326,9 @@ def __init__( class PersistentSolverConfig(SolverConfig): + """ + Base config for all persistent solver interfaces + """ def __init__( self, description=None, @@ -337,6 +351,9 @@ def __init__( class PersistentBranchAndBoundConfig(BranchAndBoundConfig): + """ + Base config for all persistent MIP solver interfaces + """ def __init__( self, description=None, diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 8e16a1384ee..7b9de32bc00 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -137,7 +137,10 @@ def test_display(self): wall_time: None extra_info: """ - self.assertEqual(expected_print, stream.getvalue()) + out = stream.getvalue() + if 'null' in out: + out = out.replace('null', 'None') + self.assertEqual(expected_print, out) def test_generated_results(self): m = pyo.ConcreteModel() diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index c4d13ae31d2..af856eab7e2 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -16,7 +16,7 @@ import pyomo.core.expr as EXPR from pyomo.core.base.constraint import _GeneralConstraintData, Constraint from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint -from pyomo.core.base.var import _GeneralVarData, Var +from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData, Param from pyomo.core.base.objective import Objective, _GeneralObjectiveData from pyomo.common.collections import ComponentMap From 6da161c6d558dbe05180b7787edf39ea808a5b9c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 15 Feb 2024 09:58:40 -0700 Subject: [PATCH 0551/3044] Adding a test for unrecognized suffixes --- pyomo/gdp/plugins/multiple_bigm.py | 4 ++-- pyomo/gdp/tests/test_mbigm.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 803d2e80807..c3964dd84f3 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -709,8 +709,8 @@ def _warn_for_active_suffix(self, suffix, disjunct, active_disjuncts, Ms): else: raise GDP_Error( "Found active Suffix '{0}' on Disjunct '{1}'. " - "The multiple bigM transformation does not currently " - "support Suffixes.".format(suffix.name, disjunct.name) + "The multiple bigM transformation does not " + "support this Suffix.".format(suffix.name, disjunct.name) ) # These are all functions to retrieve transformed components from diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index dc395c87576..521d975652b 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -1088,3 +1088,19 @@ def test_politely_ignore_BigM_Suffix(self): r"this Suffix.", warnings, ) + + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_complain_for_unrecognized_Suffix(self): + m = self.make_infeasible_disjunct_model() + m.disjunction.disjuncts[0].deactivate() + m.disjunction.disjuncts[1].HiThere = Suffix(direction=Suffix.LOCAL) + out = StringIO() + with self.assertRaisesRegex( + GDP_Error, + r"Found active Suffix 'disjunction_disjuncts\[1\].HiThere' " + r"on Disjunct 'disjunction_disjuncts\[1\]'. The multiple bigM " + r"transformation does not support this Suffix.", + ): + TransformationFactory('gdp.mbigm').apply_to( + m, reduce_bound_constraints=False + ) From 220dd134dfdaf232561ea37efd45a64ef95e11fc Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 10:02:37 -0700 Subject: [PATCH 0552/3044] Black and its empty lines --- pyomo/contrib/solver/config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index e38f903e1ac..a1133f93ae4 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -329,6 +329,7 @@ class PersistentSolverConfig(SolverConfig): """ Base config for all persistent solver interfaces """ + def __init__( self, description=None, @@ -354,6 +355,7 @@ class PersistentBranchAndBoundConfig(BranchAndBoundConfig): """ Base config for all persistent MIP solver interfaces """ + def __init__( self, description=None, From e4fe3a5e37283039f8e49cff2e8e1c8940f71678 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 15 Feb 2024 10:03:12 -0700 Subject: [PATCH 0553/3044] Fixing a typo --- pyomo/gdp/plugins/multiple_bigm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index c3964dd84f3..5e23a706361 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -68,7 +68,7 @@ 'xpress_direct', 'mosek_direct', 'baron', - 'apsi_highs', + 'appsi_highs', } From 95f005ab9b6bfa006120401d7b9f53f599a5c292 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 15 Feb 2024 10:27:03 -0700 Subject: [PATCH 0554/3044] Actually putting private_data on _BlockData (whoops), adding a kind of silly test --- pyomo/core/base/block.py | 22 +++++++++++----------- pyomo/core/tests/unit/test_block.py | 5 ++++- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index d10754082bd..73ac2ebf397 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2029,6 +2029,17 @@ def _create_objects_for_deepcopy(self, memo, component_list): comp._create_objects_for_deepcopy(memo, component_list) return _ans + @property + def _private_data(self): + if self._private_data_dict is None: + self._private_data_dict = {} + return self._private_data_dict + + def private_data(self, scope): + if scope not in self._private_data: + self._private_data[scope] = {} + return self._private_data[scope] + @ModelComponentFactory.register( "A component that contains one or more model components." @@ -2242,17 +2253,6 @@ def display(self, filename=None, ostream=None, prefix=""): for key in sorted(self): _BlockData.display(self[key], filename, ostream, prefix) - @property - def _private_data(self): - if self._private_data_dict is None: - self._private_data_dict = {} - return self._private_data_dict - - def private_data(self, scope): - if scope not in self._private_data: - self._private_data[scope] = {} - return self._private_data[scope] - class ScalarBlock(_BlockData, Block): def __init__(self, *args, **kwds): diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index 803237b1588..93333d9f764 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -3420,7 +3420,10 @@ def test_private_data(self): mfe = m.b.private_data('my_scope') self.assertIsInstance(mfe, dict) - + mfe1 = m.b.b[1].private_data('no mice here') + self.assertIsInstance(mfe1, dict) + mfe2 = m.b.b[2].private_data('no mice here') + self.assertIsInstance(mfe2, dict) if __name__ == "__main__": From cc54ae506818d2fe3dbeb728b2ff3bba2ffa973f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 15 Feb 2024 11:50:37 -0700 Subject: [PATCH 0555/3044] Making _private_data the actual dict and doing away with the property --- pyomo/core/base/block.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 73ac2ebf397..f0e0e60350f 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -550,7 +550,7 @@ def __init__(self, component): super(_BlockData, self).__setattr__('_ctypes', {}) super(_BlockData, self).__setattr__('_decl', {}) super(_BlockData, self).__setattr__('_decl_order', []) - self._private_data_dict = None + self._private_data = None def __getattr__(self, val): if val in ModelComponentFactory: @@ -2029,13 +2029,9 @@ def _create_objects_for_deepcopy(self, memo, component_list): comp._create_objects_for_deepcopy(memo, component_list) return _ans - @property - def _private_data(self): - if self._private_data_dict is None: - self._private_data_dict = {} - return self._private_data_dict - def private_data(self, scope): + if self._private_data is None: + self._private_data = {} if scope not in self._private_data: self._private_data[scope] = {} return self._private_data[scope] From 73977aa5b8f3dd501644a928d60d0a7c352491af Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 15 Feb 2024 12:18:03 -0700 Subject: [PATCH 0556/3044] Ensuring private_data really is private by limiting the possible keys to substrings of the caller's scope --- pyomo/core/base/block.py | 14 +++++++-- pyomo/core/tests/unit/test_block.py | 46 ++++++++++++++++++++++------- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index f0e0e60350f..ea12295b7bd 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -29,7 +29,7 @@ import textwrap from contextlib import contextmanager -from inspect import isclass +from inspect import isclass, currentframe from itertools import filterfalse, chain from operator import itemgetter, attrgetter from io import StringIO @@ -2029,7 +2029,17 @@ def _create_objects_for_deepcopy(self, memo, component_list): comp._create_objects_for_deepcopy(memo, component_list) return _ans - def private_data(self, scope): + def private_data(self, scope=None): + mod = currentframe().f_back.f_globals['__name__'] + if scope is None: + scope = mod + elif not mod.startswith(scope): + raise ValueError( + "All keys in the 'private_data' dictionary must " + "be substrings of the caller's module name. " + "Received '%s' when calling private_data on Block " + "'%s'." % (scope, self.name) + ) if self._private_data is None: self._private_data = {} if scope not in self._private_data: diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index 93333d9f764..eb9c449af21 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -3412,18 +3412,44 @@ def test_private_data(self): m.b = Block() m.b.b = Block([1, 2]) - mfe = m.private_data('my_scope') + mfe = m.private_data() self.assertIsInstance(mfe, dict) - mfe2 = m.private_data('another_scope') - self.assertIsInstance(mfe2, dict) - self.assertEqual(len(m._private_data), 2) + self.assertEqual(len(mfe), 0) + self.assertEqual(len(m._private_data), 1) + self.assertIn('pyomo.core.tests.unit.test_block', m._private_data) + self.assertIs(mfe, m._private_data['pyomo.core.tests.unit.test_block']) - mfe = m.b.private_data('my_scope') - self.assertIsInstance(mfe, dict) - mfe1 = m.b.b[1].private_data('no mice here') - self.assertIsInstance(mfe1, dict) - mfe2 = m.b.b[2].private_data('no mice here') - self.assertIsInstance(mfe2, dict) + with self.assertRaisesRegex( + ValueError, + "All keys in the 'private_data' dictionary must " + "be substrings of the caller's module name. " + "Received 'no mice here' when calling private_data on Block " + "'b'.", + ): + mfe2 = m.b.private_data('no mice here') + + mfe3 = m.b.b[1].private_data('pyomo.core.tests') + self.assertIsInstance(mfe3, dict) + self.assertEqual(len(mfe3), 0) + self.assertIsInstance(m.b.b[1]._private_data, dict) + self.assertEqual(len(m.b.b[1]._private_data), 1) + self.assertIn('pyomo.core.tests', m.b.b[1]._private_data) + self.assertIs(mfe3, m.b.b[1]._private_data['pyomo.core.tests']) + mfe3['there are cookies'] = 'but no mice' + + mfe4 = m.b.b[1].private_data('pyomo.core.tests') + self.assertIs(mfe4, mfe3) + + # mfe2 = m.private_data('another_scope') + # self.assertIsInstance(mfe2, dict) + # self.assertEqual(len(m._private_data), 2) + + # mfe = m.b.private_data('my_scope') + # self.assertIsInstance(mfe, dict) + # mfe1 = m.b.b[1].private_data('no mice here') + # self.assertIsInstance(mfe1, dict) + # mfe2 = m.b.b[2].private_data('no mice here') + # self.assertIsInstance(mfe2, dict) if __name__ == "__main__": From 170acb83c80e6f452217ab39c590ab656d1753fc Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 15 Feb 2024 12:22:20 -0700 Subject: [PATCH 0557/3044] Whoops, removing comments --- pyomo/core/tests/unit/test_block.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index eb9c449af21..0ffdb537ac1 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -3440,17 +3440,6 @@ def test_private_data(self): mfe4 = m.b.b[1].private_data('pyomo.core.tests') self.assertIs(mfe4, mfe3) - # mfe2 = m.private_data('another_scope') - # self.assertIsInstance(mfe2, dict) - # self.assertEqual(len(m._private_data), 2) - - # mfe = m.b.private_data('my_scope') - # self.assertIsInstance(mfe, dict) - # mfe1 = m.b.b[1].private_data('no mice here') - # self.assertIsInstance(mfe1, dict) - # mfe2 = m.b.b[2].private_data('no mice here') - # self.assertIsInstance(mfe2, dict) - if __name__ == "__main__": unittest.main() From 7edd9db575bf8e89e8b537fb34baf4e551276e09 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 12:48:51 -0700 Subject: [PATCH 0558/3044] Update documentation to include examples for usage --- .../developer_reference/solvers.rst | 74 ++++++++++++++++++- pyomo/contrib/solver/base.py | 8 -- 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index fa24d69a211..ad0ade94f41 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -1,11 +1,81 @@ -Solver Interfaces -================= +Future Solver Interface Changes +=============================== Pyomo offers interfaces into multiple solvers, both commercial and open source. +To support better capabilities for solver interfaces, the Pyomo team is actively +redesigning the existing interfaces to make them more maintainable and intuitive +for use. Redesigned interfaces can be found in ``pyomo.contrib.solver``. .. currentmodule:: pyomo.contrib.solver +New Interface Usage +------------------- + +The new interfaces have two modes: backwards compatible and future capability. +To use the backwards compatible version, simply use the ``SolverFactory`` +as usual and replace the solver name with the new version. Currently, the new +versions available are: + +.. list-table:: Available Redesigned Solvers + :widths: 25 25 + :header-rows: 1 + + * - Solver + - ``SolverFactory`` Name + * - ipopt + - ``ipopt_v2`` + * - GUROBI + - ``gurobi_v2`` + +Backwards Compatible Mode +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + status = pyo.SolverFactory('ipopt_v2').solve(model) + assert_optimal_termination(status) + model.pprint() + +Future Capability Mode +^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + from pyomo.contrib.solver.ipopt import ipopt + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + opt = ipopt() + status = opt.solve(model) + assert_optimal_termination(status) + # Displays important results information; only available in future capability mode + status.display() + model.pprint() + + + Interface Implementation ------------------------ diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 96b87924bf6..d69fecc5837 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -41,14 +41,6 @@ class SolverBase(abc.ABC): """ Base class upon which direct solver interfaces can be built. - - This base class contains the required methods for all direct solvers: - - available: Determines whether the solver is able to be run, combining - both whether it can be found on the system and if the license is valid. - - config: The configuration method for solver objects. - - solve: The main method of every solver - - version: The version of the solver - - is_persistent: Set to false for all direct solvers. """ CONFIG = SolverConfig() From 843c0f416b591cfc33a92a290428b3ab7522fe00 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 13:17:50 -0700 Subject: [PATCH 0559/3044] Add copyright statement to all source files --- doc/OnlineDocs/conf.py | 11 + .../kernel/examples/aml_example.py | 11 + .../kernel/examples/conic.py | 11 + .../kernel/examples/kernel_containers.py | 11 + .../kernel/examples/kernel_example.py | 11 + .../kernel/examples/kernel_solving.py | 11 + .../kernel/examples/kernel_subclassing.py | 11 + .../kernel/examples/transformer.py | 11 + .../modeling_extensions/__init__.py | 10 + doc/OnlineDocs/src/data/ABCD1.py | 11 + doc/OnlineDocs/src/data/ABCD2.py | 11 + doc/OnlineDocs/src/data/ABCD3.py | 11 + doc/OnlineDocs/src/data/ABCD4.py | 11 + doc/OnlineDocs/src/data/ABCD5.py | 11 + doc/OnlineDocs/src/data/ABCD6.py | 11 + doc/OnlineDocs/src/data/ABCD7.py | 11 + doc/OnlineDocs/src/data/ABCD8.py | 11 + doc/OnlineDocs/src/data/ABCD9.py | 11 + doc/OnlineDocs/src/data/diet1.py | 11 + doc/OnlineDocs/src/data/ex.py | 11 + doc/OnlineDocs/src/data/import1.tab.py | 11 + doc/OnlineDocs/src/data/import2.tab.py | 11 + doc/OnlineDocs/src/data/import3.tab.py | 11 + doc/OnlineDocs/src/data/import4.tab.py | 11 + doc/OnlineDocs/src/data/import5.tab.py | 11 + doc/OnlineDocs/src/data/import6.tab.py | 11 + doc/OnlineDocs/src/data/import7.tab.py | 11 + doc/OnlineDocs/src/data/import8.tab.py | 11 + doc/OnlineDocs/src/data/param1.py | 11 + doc/OnlineDocs/src/data/param2.py | 11 + doc/OnlineDocs/src/data/param2a.py | 11 + doc/OnlineDocs/src/data/param3.py | 11 + doc/OnlineDocs/src/data/param3a.py | 11 + doc/OnlineDocs/src/data/param3b.py | 11 + doc/OnlineDocs/src/data/param3c.py | 11 + doc/OnlineDocs/src/data/param4.py | 11 + doc/OnlineDocs/src/data/param5.py | 11 + doc/OnlineDocs/src/data/param5a.py | 11 + doc/OnlineDocs/src/data/param6.py | 11 + doc/OnlineDocs/src/data/param6a.py | 11 + doc/OnlineDocs/src/data/param7a.py | 11 + doc/OnlineDocs/src/data/param7b.py | 11 + doc/OnlineDocs/src/data/param8a.py | 11 + doc/OnlineDocs/src/data/set1.py | 11 + doc/OnlineDocs/src/data/set2.py | 11 + doc/OnlineDocs/src/data/set2a.py | 11 + doc/OnlineDocs/src/data/set3.py | 11 + doc/OnlineDocs/src/data/set4.py | 11 + doc/OnlineDocs/src/data/set5.py | 11 + doc/OnlineDocs/src/data/table0.py | 11 + doc/OnlineDocs/src/data/table0.ul.py | 11 + doc/OnlineDocs/src/data/table1.py | 11 + doc/OnlineDocs/src/data/table2.py | 11 + doc/OnlineDocs/src/data/table3.py | 11 + doc/OnlineDocs/src/data/table3.ul.py | 11 + doc/OnlineDocs/src/data/table4.py | 11 + doc/OnlineDocs/src/data/table4.ul.py | 11 + doc/OnlineDocs/src/data/table5.py | 11 + doc/OnlineDocs/src/data/table6.py | 11 + doc/OnlineDocs/src/data/table7.py | 11 + .../src/dataportal/dataportal_tab.py | 11 + .../src/dataportal/param_initialization.py | 11 + .../src/dataportal/set_initialization.py | 11 + doc/OnlineDocs/src/expr/design.py | 11 + doc/OnlineDocs/src/expr/index.py | 11 + doc/OnlineDocs/src/expr/managing.py | 11 + doc/OnlineDocs/src/expr/overview.py | 11 + doc/OnlineDocs/src/expr/performance.py | 11 + doc/OnlineDocs/src/expr/quicksum.py | 11 + .../src/scripting/AbstractSuffixes.py | 11 + doc/OnlineDocs/src/scripting/Isinglebuild.py | 11 + doc/OnlineDocs/src/scripting/NodesIn_init.py | 12 + doc/OnlineDocs/src/scripting/Z_init.py | 12 + doc/OnlineDocs/src/scripting/abstract2.py | 11 + .../src/scripting/abstract2piece.py | 11 + .../src/scripting/abstract2piecebuild.py | 11 + .../src/scripting/block_iter_example.py | 11 + doc/OnlineDocs/src/scripting/concrete1.py | 11 + doc/OnlineDocs/src/scripting/doubleA.py | 12 + doc/OnlineDocs/src/scripting/driveabs2.py | 11 + doc/OnlineDocs/src/scripting/driveconc1.py | 11 + doc/OnlineDocs/src/scripting/iterative1.py | 11 + doc/OnlineDocs/src/scripting/iterative2.py | 11 + doc/OnlineDocs/src/scripting/noiteration1.py | 11 + doc/OnlineDocs/src/scripting/parallel.py | 11 + .../src/scripting/spy4Constraints.py | 11 + .../src/scripting/spy4Expressions.py | 11 + .../src/scripting/spy4PyomoCommand.py | 11 + doc/OnlineDocs/src/scripting/spy4Variables.py | 11 + doc/OnlineDocs/src/scripting/spy4scripts.py | 11 + doc/OnlineDocs/src/strip_examples.py | 11 + examples/dae/car_example.py | 11 + examples/dae/disease_DAE.py | 11 + examples/dae/run_disease.py | 11 + examples/dae/run_stochpdegas_automatic.py | 11 + examples/dae/simulator_dae_example.py | 11 + .../dae/simulator_dae_multindex_example.py | 11 + examples/dae/simulator_ode_example.py | 11 + .../dae/simulator_ode_multindex_example.py | 11 + examples/dae/stochpdegas_automatic.py | 11 + examples/doc/samples/__init__.py | 11 + .../samples/case_studies/deer/DeerProblem.py | 11 + .../samples/case_studies/diet/DietProblem.py | 11 + .../disease_est/DiseaseEstimation.py | 11 + .../samples/case_studies/max_flow/MaxFlow.py | 11 + .../case_studies/network_flow/networkFlow1.py | 11 + .../samples/case_studies/rosen/Rosenbrock.py | 11 + .../transportation/transportation.py | 11 + .../comparisons/cutstock/cutstock_cplex.py | 11 + .../comparisons/cutstock/cutstock_grb.py | 11 + .../comparisons/cutstock/cutstock_lpsolve.py | 11 + .../comparisons/cutstock/cutstock_pulpor.py | 11 + .../comparisons/cutstock/cutstock_pyomo.py | 11 + .../comparisons/cutstock/cutstock_util.py | 12 + .../samples/comparisons/sched/pyomo/sched.py | 11 + examples/doc/samples/scripts/__init__.py | 11 + examples/doc/samples/scripts/s1/knapsack.py | 11 + examples/doc/samples/scripts/s1/script.py | 11 + examples/doc/samples/scripts/s2/knapsack.py | 11 + examples/doc/samples/scripts/s2/script.py | 11 + examples/doc/samples/update.py | 11 + examples/gdp/batchProcessing.py | 11 + examples/gdp/circles/circles.py | 11 + .../constrained_layout/cons_layout_model.py | 11 + .../gdp/eight_process/eight_proc_logical.py | 11 + .../gdp/eight_process/eight_proc_model.py | 11 + .../eight_process/eight_proc_verbose_model.py | 11 + examples/gdp/farm_layout/farm_layout.py | 11 + examples/gdp/medTermPurchasing_Literal.py | 11 + examples/gdp/nine_process/small_process.py | 11 + examples/gdp/simple1.py | 11 + examples/gdp/simple2.py | 11 + examples/gdp/simple3.py | 11 + examples/gdp/small_lit/basic_step.py | 11 + examples/gdp/small_lit/contracts_problem.py | 11 + examples/gdp/small_lit/ex1_Lee.py | 11 + examples/gdp/small_lit/ex_633_trespalacios.py | 11 + examples/gdp/small_lit/nonconvex_HEN.py | 11 + examples/gdp/stickies.py | 11 + examples/gdp/strip_packing/stripPacking.py | 11 + .../gdp/strip_packing/strip_packing_8rect.py | 11 + .../strip_packing/strip_packing_concrete.py | 11 + examples/gdp/two_rxn_lee/two_rxn_model.py | 11 + examples/kernel/blocks.py | 11 + examples/kernel/conic.py | 11 + examples/kernel/constraints.py | 11 + examples/kernel/containers.py | 11 + examples/kernel/expressions.py | 11 + examples/kernel/mosek/geometric1.py | 11 + examples/kernel/mosek/geometric2.py | 11 + .../kernel/mosek/maximum_volume_cuboid.py | 11 + examples/kernel/mosek/power1.py | 11 + examples/kernel/mosek/semidefinite.py | 11 + examples/kernel/objectives.py | 11 + examples/kernel/parameters.py | 11 + examples/kernel/piecewise_functions.py | 11 + examples/kernel/piecewise_nd_functions.py | 11 + examples/kernel/special_ordered_sets.py | 11 + examples/kernel/suffixes.py | 11 + examples/kernel/variables.py | 11 + examples/mpec/bard1.py | 11 + examples/mpec/scholtes4.py | 11 + .../dae/run_stochpdegas1_automatic.py | 11 + .../performance/dae/stochpdegas1_automatic.py | 11 + examples/performance/jump/clnlbeam.py | 11 + examples/performance/jump/facility.py | 11 + examples/performance/jump/lqcp.py | 11 + examples/performance/jump/opf_66200bus.py | 11 + examples/performance/jump/opf_6620bus.py | 11 + examples/performance/jump/opf_662bus.py | 11 + examples/performance/misc/bilinear1_100.py | 11 + examples/performance/misc/bilinear1_100000.py | 11 + examples/performance/misc/bilinear2_100.py | 11 + examples/performance/misc/bilinear2_100000.py | 11 + examples/performance/misc/diag1_100.py | 11 + examples/performance/misc/diag1_100000.py | 11 + examples/performance/misc/diag2_100.py | 11 + examples/performance/misc/diag2_100000.py | 11 + examples/performance/misc/set1.py | 11 + examples/performance/misc/sparse1.py | 11 + examples/pyomo/concrete/rosen.py | 11 + examples/pyomo/concrete/sodacan.py | 11 + examples/pyomo/concrete/sodacan_fig.py | 11 + examples/pyomo/concrete/sp.py | 11 + examples/pyomo/concrete/sp_data.py | 11 + examples/pyomo/p-median/decorated_pmedian.py | 11 + examples/pyomobook/__init__.py | 10 + .../pyomobook/abstract-ch/AbstHLinScript.py | 11 + examples/pyomobook/abstract-ch/AbstractH.py | 11 + .../pyomobook/abstract-ch/AbstractHLinear.py | 11 + examples/pyomobook/abstract-ch/abstract5.py | 11 + examples/pyomobook/abstract-ch/abstract6.py | 11 + examples/pyomobook/abstract-ch/abstract7.py | 11 + .../pyomobook/abstract-ch/buildactions.py | 11 + examples/pyomobook/abstract-ch/concrete1.py | 11 + examples/pyomobook/abstract-ch/concrete2.py | 11 + examples/pyomobook/abstract-ch/diet1.py | 11 + examples/pyomobook/abstract-ch/ex.py | 11 + examples/pyomobook/abstract-ch/param1.py | 11 + examples/pyomobook/abstract-ch/param2.py | 11 + examples/pyomobook/abstract-ch/param2a.py | 11 + examples/pyomobook/abstract-ch/param3.py | 11 + examples/pyomobook/abstract-ch/param3a.py | 11 + examples/pyomobook/abstract-ch/param3b.py | 11 + examples/pyomobook/abstract-ch/param3c.py | 11 + examples/pyomobook/abstract-ch/param4.py | 11 + examples/pyomobook/abstract-ch/param5.py | 11 + examples/pyomobook/abstract-ch/param5a.py | 11 + examples/pyomobook/abstract-ch/param6.py | 11 + examples/pyomobook/abstract-ch/param6a.py | 11 + examples/pyomobook/abstract-ch/param7a.py | 11 + examples/pyomobook/abstract-ch/param7b.py | 11 + examples/pyomobook/abstract-ch/param8a.py | 11 + .../pyomobook/abstract-ch/postprocess_fn.py | 11 + examples/pyomobook/abstract-ch/set1.py | 11 + examples/pyomobook/abstract-ch/set2.py | 11 + examples/pyomobook/abstract-ch/set2a.py | 11 + examples/pyomobook/abstract-ch/set3.py | 11 + examples/pyomobook/abstract-ch/set4.py | 11 + examples/pyomobook/abstract-ch/set5.py | 11 + examples/pyomobook/abstract-ch/wl_abstract.py | 11 + .../abstract-ch/wl_abstract_script.py | 11 + examples/pyomobook/blocks-ch/blocks_gen.py | 11 + examples/pyomobook/blocks-ch/blocks_intro.py | 11 + .../pyomobook/blocks-ch/blocks_lotsizing.py | 11 + examples/pyomobook/blocks-ch/lotsizing.py | 11 + .../pyomobook/blocks-ch/lotsizing_no_time.py | 11 + .../blocks-ch/lotsizing_uncertain.py | 11 + examples/pyomobook/dae-ch/dae_tester_model.py | 11 + .../pyomobook/dae-ch/plot_path_constraint.py | 12 + .../pyomobook/dae-ch/run_path_constraint.py | 11 + .../dae-ch/run_path_constraint_tester.py | 11 + examples/pyomobook/gdp-ch/gdp_uc.py | 11 + examples/pyomobook/gdp-ch/scont.py | 11 + examples/pyomobook/gdp-ch/scont2.py | 11 + examples/pyomobook/gdp-ch/scont_script.py | 11 + examples/pyomobook/gdp-ch/verify_scont.py | 11 + examples/pyomobook/intro-ch/abstract5.py | 11 + .../pyomobook/intro-ch/coloring_concrete.py | 11 + examples/pyomobook/intro-ch/concrete1.py | 11 + .../pyomobook/intro-ch/concrete1_generic.py | 11 + examples/pyomobook/intro-ch/mydata.py | 11 + examples/pyomobook/mpec-ch/ex1a.py | 11 + examples/pyomobook/mpec-ch/ex1b.py | 11 + examples/pyomobook/mpec-ch/ex1c.py | 11 + examples/pyomobook/mpec-ch/ex1d.py | 11 + examples/pyomobook/mpec-ch/ex1e.py | 11 + examples/pyomobook/mpec-ch/ex2.py | 11 + examples/pyomobook/mpec-ch/munson1.py | 11 + examples/pyomobook/mpec-ch/ralph1.py | 11 + .../nonlinear-ch/deer/DeerProblem.py | 11 + .../disease_est/disease_estimation.py | 11 + .../multimodal/multimodal_init1.py | 11 + .../multimodal/multimodal_init2.py | 11 + .../react_design/ReactorDesign.py | 11 + .../react_design/ReactorDesignTable.py | 11 + .../nonlinear-ch/rosen/rosenbrock.py | 11 + .../optimization-ch/ConcHLinScript.py | 11 + .../pyomobook/optimization-ch/ConcreteH.py | 11 + .../optimization-ch/ConcreteHLinear.py | 11 + .../optimization-ch/IC_model_dict.py | 11 + .../overview-ch/var_obj_con_snippet.py | 11 + examples/pyomobook/overview-ch/wl_abstract.py | 11 + .../overview-ch/wl_abstract_script.py | 11 + examples/pyomobook/overview-ch/wl_concrete.py | 11 + .../overview-ch/wl_concrete_script.py | 11 + examples/pyomobook/overview-ch/wl_excel.py | 11 + examples/pyomobook/overview-ch/wl_list.py | 11 + examples/pyomobook/overview-ch/wl_mutable.py | 11 + .../pyomobook/overview-ch/wl_mutable_excel.py | 11 + examples/pyomobook/overview-ch/wl_scalar.py | 11 + .../pyomobook/performance-ch/SparseSets.py | 11 + examples/pyomobook/performance-ch/lin_expr.py | 11 + .../pyomobook/performance-ch/persistent.py | 11 + examples/pyomobook/performance-ch/wl.py | 11 + .../pyomo-components-ch/con_declaration.py | 11 + .../pyomobook/pyomo-components-ch/examples.py | 11 + .../pyomo-components-ch/expr_declaration.py | 11 + .../pyomo-components-ch/obj_declaration.py | 11 + .../pyomo-components-ch/param_declaration.py | 11 + .../param_initialization.py | 11 + .../pyomo-components-ch/param_misc.py | 11 + .../pyomo-components-ch/param_validation.py | 11 + .../pyomobook/pyomo-components-ch/rangeset.py | 11 + .../pyomo-components-ch/set_declaration.py | 11 + .../pyomo-components-ch/set_initialization.py | 11 + .../pyomobook/pyomo-components-ch/set_misc.py | 11 + .../pyomo-components-ch/set_options.py | 11 + .../pyomo-components-ch/set_validation.py | 11 + .../pyomo-components-ch/suffix_declaration.py | 11 + .../pyomo-components-ch/var_declaration.py | 11 + examples/pyomobook/python-ch/BadIndent.py | 11 + examples/pyomobook/python-ch/LineExample.py | 11 + examples/pyomobook/python-ch/class.py | 61 +- examples/pyomobook/python-ch/ctob.py | 11 + examples/pyomobook/python-ch/example.py | 11 + examples/pyomobook/python-ch/example2.py | 11 + examples/pyomobook/python-ch/functions.py | 59 +- examples/pyomobook/python-ch/iterate.py | 47 +- .../pyomobook/python-ch/pythonconditional.py | 11 + examples/pyomobook/scripts-ch/attributes.py | 11 + examples/pyomobook/scripts-ch/prob_mod_ex.py | 11 + .../pyomobook/scripts-ch/sudoku/sudoku.py | 11 + .../pyomobook/scripts-ch/sudoku/sudoku_run.py | 11 + .../pyomobook/scripts-ch/value_expression.py | 11 + .../pyomobook/scripts-ch/warehouse_cuts.py | 11 + .../scripts-ch/warehouse_load_solutions.py | 11 + .../pyomobook/scripts-ch/warehouse_model.py | 11 + .../pyomobook/scripts-ch/warehouse_print.py | 11 + .../pyomobook/scripts-ch/warehouse_script.py | 11 + .../scripts-ch/warehouse_solver_options.py | 11 + examples/pyomobook/strip_examples.py | 11 + pyomo/common/multithread.py | 11 + pyomo/common/shutdown.py | 11 + pyomo/common/tests/import_ex.py | 12 + pyomo/common/tests/test_multithread.py | 11 + pyomo/contrib/__init__.py | 10 + pyomo/contrib/ampl_function_demo/__init__.py | 10 + .../ampl_function_demo/tests/__init__.py | 10 + pyomo/contrib/appsi/__init__.py | 11 + pyomo/contrib/appsi/base.py | 11 + pyomo/contrib/appsi/cmodel/src/common.cpp | 12 + pyomo/contrib/appsi/cmodel/src/common.hpp | 12 + pyomo/contrib/appsi/cmodel/src/expression.cpp | 3952 +++++++++-------- pyomo/contrib/appsi/cmodel/src/expression.hpp | 1588 +++---- pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp | 12 + pyomo/contrib/appsi/cmodel/src/fbbt_model.hpp | 12 + pyomo/contrib/appsi/cmodel/src/interval.cpp | 12 + pyomo/contrib/appsi/cmodel/src/interval.hpp | 12 + pyomo/contrib/appsi/cmodel/src/lp_writer.cpp | 12 + pyomo/contrib/appsi/cmodel/src/lp_writer.hpp | 12 + pyomo/contrib/appsi/cmodel/src/model_base.cpp | 12 + pyomo/contrib/appsi/cmodel/src/model_base.hpp | 12 + pyomo/contrib/appsi/cmodel/src/nl_writer.cpp | 12 + pyomo/contrib/appsi/cmodel/src/nl_writer.hpp | 12 + pyomo/contrib/appsi/cmodel/tests/__init__.py | 10 + .../contrib/appsi/cmodel/tests/test_import.py | 11 + pyomo/contrib/appsi/examples/__init__.py | 10 + .../contrib/appsi/examples/getting_started.py | 11 + .../contrib/appsi/examples/tests/__init__.py | 10 + .../appsi/examples/tests/test_examples.py | 11 + pyomo/contrib/appsi/fbbt.py | 11 + pyomo/contrib/appsi/plugins.py | 11 + pyomo/contrib/appsi/solvers/__init__.py | 11 + pyomo/contrib/appsi/solvers/cbc.py | 11 + pyomo/contrib/appsi/solvers/cplex.py | 11 + pyomo/contrib/appsi/solvers/gurobi.py | 11 + pyomo/contrib/appsi/solvers/highs.py | 11 + pyomo/contrib/appsi/solvers/ipopt.py | 11 + pyomo/contrib/appsi/solvers/tests/__init__.py | 10 + .../solvers/tests/test_gurobi_persistent.py | 11 + .../solvers/tests/test_highs_persistent.py | 11 + .../solvers/tests/test_ipopt_persistent.py | 11 + .../solvers/tests/test_persistent_solvers.py | 11 + .../solvers/tests/test_wntr_persistent.py | 11 + pyomo/contrib/appsi/solvers/wntr.py | 11 + pyomo/contrib/appsi/tests/__init__.py | 10 + pyomo/contrib/appsi/tests/test_base.py | 11 + pyomo/contrib/appsi/tests/test_fbbt.py | 11 + pyomo/contrib/appsi/tests/test_interval.py | 11 + pyomo/contrib/appsi/utils/__init__.py | 11 + .../utils/collect_vars_and_named_exprs.py | 11 + pyomo/contrib/appsi/utils/get_objective.py | 11 + pyomo/contrib/appsi/utils/tests/__init__.py | 10 + .../test_collect_vars_and_named_exprs.py | 11 + pyomo/contrib/appsi/writers/__init__.py | 11 + pyomo/contrib/appsi/writers/config.py | 12 + pyomo/contrib/appsi/writers/lp_writer.py | 11 + pyomo/contrib/appsi/writers/nl_writer.py | 11 + pyomo/contrib/appsi/writers/tests/__init__.py | 10 + .../appsi/writers/tests/test_nl_writer.py | 11 + pyomo/contrib/benders/__init__.py | 10 + pyomo/contrib/benders/examples/__init__.py | 10 + pyomo/contrib/benders/tests/__init__.py | 10 + pyomo/contrib/community_detection/__init__.py | 10 + .../community_detection/community_graph.py | 11 + .../contrib/community_detection/detection.py | 11 + .../contrib/community_detection/event_log.py | 11 + pyomo/contrib/community_detection/plugins.py | 12 + .../community_detection/tests/__init__.py | 10 + pyomo/contrib/cp/__init__.py | 11 + pyomo/contrib/cp/repn/__init__.py | 10 + pyomo/contrib/cp/scheduling_expr/__init__.py | 11 +- pyomo/contrib/cp/tests/__init__.py | 10 + pyomo/contrib/cp/transform/__init__.py | 10 + pyomo/contrib/doe/examples/__init__.py | 10 + pyomo/contrib/doe/tests/__init__.py | 10 + pyomo/contrib/example/__init__.py | 11 + pyomo/contrib/example/bar.py | 11 + pyomo/contrib/example/foo.py | 11 + pyomo/contrib/example/plugins/__init__.py | 11 + pyomo/contrib/example/plugins/ex_plugin.py | 11 + pyomo/contrib/example/tests/__init__.py | 11 + pyomo/contrib/fbbt/__init__.py | 10 + pyomo/contrib/fbbt/tests/__init__.py | 10 + pyomo/contrib/fbbt/tests/test_interval.py | 11 + pyomo/contrib/fme/__init__.py | 10 + pyomo/contrib/fme/tests/__init__.py | 10 + pyomo/contrib/gdp_bounds/__init__.py | 11 + pyomo/contrib/gdp_bounds/info.py | 11 + pyomo/contrib/gdp_bounds/tests/__init__.py | 10 + .../gdp_bounds/tests/test_gdp_bounds.py | 11 + pyomo/contrib/gdpopt/__init__.py | 11 + pyomo/contrib/gdpopt/tests/__init__.py | 10 + pyomo/contrib/iis/__init__.py | 11 + pyomo/contrib/iis/iis.py | 11 + pyomo/contrib/iis/tests/__init__.py | 10 + pyomo/contrib/iis/tests/test_iis.py | 11 + pyomo/contrib/incidence_analysis/__init__.py | 11 + .../incidence_analysis/common/__init__.py | 10 + .../common/tests/__init__.py | 10 + .../incidence_analysis/tests/__init__.py | 10 + pyomo/contrib/interior_point/__init__.py | 10 + .../interior_point/examples/__init__.py | 10 + .../contrib/interior_point/linalg/__init__.py | 10 + .../linalg/base_linear_solver_interface.py | 11 + .../interior_point/linalg/ma27_interface.py | 11 + .../interior_point/linalg/scipy_interface.py | 11 + .../interior_point/linalg/tests/__init__.py | 10 + .../linalg/tests/test_linear_solvers.py | 11 + .../linalg/tests/test_realloc.py | 11 + .../contrib/interior_point/tests/__init__.py | 10 + .../interior_point/tests/test_realloc.py | 11 + pyomo/contrib/latex_printer/__init__.py | 11 + pyomo/contrib/latex_printer/latex_printer.py | 11 + pyomo/contrib/latex_printer/tests/__init__.py | 11 +- .../latex_printer/tests/test_latex_printer.py | 11 + .../tests/test_latex_printer_vartypes.py | 11 + pyomo/contrib/mindtpy/__init__.py | 11 + pyomo/contrib/mindtpy/config_options.py | 11 + pyomo/contrib/mindtpy/tests/MINLP4_simple.py | 11 + pyomo/contrib/mindtpy/tests/MINLP5_simple.py | 11 + .../mindtpy/tests/MINLP_simple_grey_box.py | 11 + pyomo/contrib/mindtpy/tests/__init__.py | 10 + .../tests/constraint_qualification_example.py | 11 + .../mindtpy/tests/eight_process_problem.py | 11 + .../mindtpy/tests/feasibility_pump1.py | 11 + .../mindtpy/tests/feasibility_pump2.py | 11 + pyomo/contrib/mindtpy/tests/from_proposal.py | 11 + pyomo/contrib/mindtpy/tests/nonconvex1.py | 11 + pyomo/contrib/mindtpy/tests/nonconvex2.py | 11 + pyomo/contrib/mindtpy/tests/nonconvex3.py | 11 + pyomo/contrib/mindtpy/tests/nonconvex4.py | 11 + .../contrib/mindtpy/tests/test_mindtpy_ECP.py | 11 + .../mindtpy/tests/test_mindtpy_feas_pump.py | 11 + .../mindtpy/tests/test_mindtpy_global.py | 11 + .../tests/test_mindtpy_global_lp_nlp.py | 11 + .../tests/test_mindtpy_regularization.py | 11 + .../tests/test_mindtpy_solution_pool.py | 11 + pyomo/contrib/mpc/data/tests/__init__.py | 10 + pyomo/contrib/mpc/examples/__init__.py | 10 + pyomo/contrib/mpc/examples/cstr/__init__.py | 10 + .../mpc/examples/cstr/tests/__init__.py | 10 + .../contrib/mpc/interfaces/tests/__init__.py | 10 + pyomo/contrib/mpc/modeling/tests/__init__.py | 10 + pyomo/contrib/multistart/__init__.py | 10 + pyomo/contrib/multistart/high_conf_stop.py | 11 + pyomo/contrib/multistart/plugins.py | 12 + pyomo/contrib/multistart/reinit.py | 11 + pyomo/contrib/multistart/test_multi.py | 11 + pyomo/contrib/parmest/utils/create_ef.py | 11 + pyomo/contrib/parmest/utils/scenario_tree.py | 11 + pyomo/contrib/piecewise/__init__.py | 11 + pyomo/contrib/piecewise/tests/__init__.py | 10 + pyomo/contrib/piecewise/transform/__init__.py | 10 + pyomo/contrib/preprocessing/__init__.py | 11 + .../contrib/preprocessing/plugins/__init__.py | 12 + .../plugins/constraint_tightener.py | 11 + .../preprocessing/plugins/int_to_binary.py | 11 + pyomo/contrib/preprocessing/tests/__init__.py | 10 + .../tests/test_bounds_to_vars_xfrm.py | 11 + .../tests/test_constraint_tightener.py | 11 + .../test_deactivate_trivial_constraints.py | 11 + .../tests/test_detect_fixed_vars.py | 11 + .../tests/test_equality_propagate.py | 11 + .../preprocessing/tests/test_init_vars.py | 11 + .../preprocessing/tests/test_strip_bounds.py | 11 + .../tests/test_var_aggregator.py | 11 + .../tests/test_zero_sum_propagate.py | 11 + .../tests/test_zero_term_removal.py | 11 + pyomo/contrib/preprocessing/util.py | 11 + .../pynumero/algorithms/solvers/__init__.py | 10 + .../algorithms/solvers/tests/__init__.py | 10 + .../pynumero/examples/callback/__init__.py | 10 + .../examples/callback/cyipopt_callback.py | 11 + .../callback/cyipopt_callback_halt.py | 11 + .../callback/cyipopt_functor_callback.py | 11 + .../examples/callback/reactor_design.py | 11 + .../examples/external_grey_box/__init__.py | 10 + .../external_grey_box/param_est/__init__.py | 10 + .../param_est/generate_data.py | 11 + .../external_grey_box/param_est/models.py | 11 + .../param_est/perform_estimation.py | 11 + .../react_example/__init__.py | 10 + .../pynumero/examples/mumps_example.py | 11 + .../pynumero/examples/parallel_matvec.py | 11 + .../pynumero/examples/parallel_vector_ops.py | 11 + pyomo/contrib/pynumero/examples/sqp.py | 11 + .../pynumero/examples/tests/__init__.py | 10 + .../pynumero/examples/tests/test_examples.py | 11 + .../examples/tests/test_mpi_examples.py | 11 + .../pynumero/interfaces/nlp_projections.py | 11 + .../tests/external_grey_box_models.py | 11 + pyomo/contrib/pynumero/linalg/base.py | 11 + .../contrib/pynumero/linalg/ma27_interface.py | 11 + .../contrib/pynumero/linalg/ma57_interface.py | 11 + .../pynumero/linalg/scipy_interface.py | 11 + .../contrib/pynumero/linalg/tests/__init__.py | 10 + .../linalg/tests/test_linear_solvers.py | 11 + pyomo/contrib/pynumero/src/ma27Interface.cpp | 12 + pyomo/contrib/pynumero/src/ma57Interface.cpp | 12 + .../pynumero/src/tests/simple_test.cpp | 12 + pyomo/contrib/pynumero/tests/__init__.py | 10 + pyomo/contrib/pyros/__init__.py | 11 + pyomo/contrib/pyros/master_problem_methods.py | 11 + .../contrib/pyros/pyros_algorithm_methods.py | 11 + .../pyros/separation_problem_methods.py | 11 + pyomo/contrib/pyros/solve_data.py | 11 + pyomo/contrib/pyros/tests/__init__.py | 10 + pyomo/contrib/pyros/tests/test_grcs.py | 11 + pyomo/contrib/pyros/uncertainty_sets.py | 11 + pyomo/contrib/pyros/util.py | 11 + pyomo/contrib/satsolver/__init__.py | 10 + pyomo/contrib/satsolver/satsolver.py | 11 + pyomo/contrib/sensitivity_toolbox/__init__.py | 11 + .../sensitivity_toolbox/examples/__init__.py | 11 + .../examples/rooney_biegler.py | 11 + pyomo/contrib/sensitivity_toolbox/k_aug.py | 11 + pyomo/contrib/sensitivity_toolbox/sens.py | 11 + .../sensitivity_toolbox/tests/__init__.py | 11 + .../tests/test_k_aug_interface.py | 11 + .../sensitivity_toolbox/tests/test_sens.py | 11 + .../tests/test_sens_unit.py | 11 + pyomo/contrib/viewer/__init__.py | 11 +- pyomo/contrib/viewer/tests/__init__.py | 10 + pyomo/core/expr/calculus/__init__.py | 10 + pyomo/core/expr/taylor_series.py | 11 + .../plugins/transform/logical_to_linear.py | 11 + .../tests/unit/test_logical_constraint.py | 11 + pyomo/core/tests/unit/test_sos_v2.py | 11 + pyomo/dae/simulator.py | 11 + pyomo/gdp/tests/models.py | 11 + pyomo/gdp/tests/test_reclassify.py | 11 + pyomo/repn/tests/ampl/__init__.py | 10 + pyomo/solvers/plugins/solvers/XPRESS.py | 11 + .../tests/checks/test_MOSEKPersistent.py | 11 + pyomo/solvers/tests/checks/test_gurobi.py | 11 + .../tests/checks/test_gurobi_direct.py | 11 + .../tests/checks/test_xpress_persistent.py | 11 + pyomo/solvers/tests/mip/test_scip_log_data.py | 11 + pyomo/util/__init__.py | 10 + pyomo/util/diagnostics.py | 11 + pyomo/util/tests/__init__.py | 10 + scripts/performance/compare_components.py | 11 + scripts/performance/expr_perf.py | 11 + scripts/performance/simple.py | 11 + 556 files changed, 8901 insertions(+), 2828 deletions(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index ef6510daedf..24f8d26c9e8 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + #!/usr/bin/env python3 # -*- coding: utf-8 -*- # diff --git a/doc/OnlineDocs/library_reference/kernel/examples/aml_example.py b/doc/OnlineDocs/library_reference/kernel/examples/aml_example.py index 146048a6046..564764071b7 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/aml_example.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/aml_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @Import_Syntax import pyomo.environ as aml diff --git a/doc/OnlineDocs/library_reference/kernel/examples/conic.py b/doc/OnlineDocs/library_reference/kernel/examples/conic.py index 9282bc67f9a..866377ed641 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/conic.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/conic.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @Class import pyomo.kernel as pmo diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_containers.py b/doc/OnlineDocs/library_reference/kernel/examples/kernel_containers.py index f2a4ec25ac5..9b33ed71e1d 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/kernel_containers.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/kernel_containers.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel # @all diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_example.py b/doc/OnlineDocs/library_reference/kernel/examples/kernel_example.py index 1caf064bb2a..6ee766e3055 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/kernel_example.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/kernel_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @Import_Syntax import pyomo.kernel as pmo diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_solving.py b/doc/OnlineDocs/library_reference/kernel/examples/kernel_solving.py index 5a8eed9fd89..30b588f89b8 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/kernel_solving.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/kernel_solving.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo model = pmo.block() diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_subclassing.py b/doc/OnlineDocs/library_reference/kernel/examples/kernel_subclassing.py index c21c6dc890b..a603050e828 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/kernel_subclassing.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/kernel_subclassing.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel diff --git a/doc/OnlineDocs/library_reference/kernel/examples/transformer.py b/doc/OnlineDocs/library_reference/kernel/examples/transformer.py index 66893008cf9..0df239c61ad 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/transformer.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/transformer.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ import pyomo.kernel diff --git a/doc/OnlineDocs/modeling_extensions/__init__.py b/doc/OnlineDocs/modeling_extensions/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/doc/OnlineDocs/modeling_extensions/__init__.py +++ b/doc/OnlineDocs/modeling_extensions/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/doc/OnlineDocs/src/data/ABCD1.py b/doc/OnlineDocs/src/data/ABCD1.py index 32600b226e1..6d34bec756e 100644 --- a/doc/OnlineDocs/src/data/ABCD1.py +++ b/doc/OnlineDocs/src/data/ABCD1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/ABCD2.py b/doc/OnlineDocs/src/data/ABCD2.py index 65a46415368..beadd71916d 100644 --- a/doc/OnlineDocs/src/data/ABCD2.py +++ b/doc/OnlineDocs/src/data/ABCD2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/ABCD3.py b/doc/OnlineDocs/src/data/ABCD3.py index 48797ced5bb..1a3e826c6a9 100644 --- a/doc/OnlineDocs/src/data/ABCD3.py +++ b/doc/OnlineDocs/src/data/ABCD3.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/ABCD4.py b/doc/OnlineDocs/src/data/ABCD4.py index 20f6a21c011..59055cadf71 100644 --- a/doc/OnlineDocs/src/data/ABCD4.py +++ b/doc/OnlineDocs/src/data/ABCD4.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/ABCD5.py b/doc/OnlineDocs/src/data/ABCD5.py index 58461af056b..051e2c9ba9e 100644 --- a/doc/OnlineDocs/src/data/ABCD5.py +++ b/doc/OnlineDocs/src/data/ABCD5.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/ABCD6.py b/doc/OnlineDocs/src/data/ABCD6.py index 961408dbc7e..8c239eb5332 100644 --- a/doc/OnlineDocs/src/data/ABCD6.py +++ b/doc/OnlineDocs/src/data/ABCD6.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/ABCD7.py b/doc/OnlineDocs/src/data/ABCD7.py index a97e764fa5a..a52c27af234 100644 --- a/doc/OnlineDocs/src/data/ABCD7.py +++ b/doc/OnlineDocs/src/data/ABCD7.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * import pyomo.common import sys diff --git a/doc/OnlineDocs/src/data/ABCD8.py b/doc/OnlineDocs/src/data/ABCD8.py index 9bcd950c681..f979f373a18 100644 --- a/doc/OnlineDocs/src/data/ABCD8.py +++ b/doc/OnlineDocs/src/data/ABCD8.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * import pyomo.common import sys diff --git a/doc/OnlineDocs/src/data/ABCD9.py b/doc/OnlineDocs/src/data/ABCD9.py index 29fcb6426db..aaa1e7c908d 100644 --- a/doc/OnlineDocs/src/data/ABCD9.py +++ b/doc/OnlineDocs/src/data/ABCD9.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * import pyomo.common import sys diff --git a/doc/OnlineDocs/src/data/diet1.py b/doc/OnlineDocs/src/data/diet1.py index ef0d8096350..9201edb8c4c 100644 --- a/doc/OnlineDocs/src/data/diet1.py +++ b/doc/OnlineDocs/src/data/diet1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # diet1.py from pyomo.environ import * diff --git a/doc/OnlineDocs/src/data/ex.py b/doc/OnlineDocs/src/data/ex.py index 8c9473f2852..3fd91b623b2 100644 --- a/doc/OnlineDocs/src/data/ex.py +++ b/doc/OnlineDocs/src/data/ex.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/import1.tab.py b/doc/OnlineDocs/src/data/import1.tab.py index c9164ab73ec..ade8edcc2a3 100644 --- a/doc/OnlineDocs/src/data/import1.tab.py +++ b/doc/OnlineDocs/src/data/import1.tab.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/import2.tab.py b/doc/OnlineDocs/src/data/import2.tab.py index d03f053d090..6491c1ec30e 100644 --- a/doc/OnlineDocs/src/data/import2.tab.py +++ b/doc/OnlineDocs/src/data/import2.tab.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/import3.tab.py b/doc/OnlineDocs/src/data/import3.tab.py index e86557677ee..ec57c018b00 100644 --- a/doc/OnlineDocs/src/data/import3.tab.py +++ b/doc/OnlineDocs/src/data/import3.tab.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/import4.tab.py b/doc/OnlineDocs/src/data/import4.tab.py index 93df9c761ab..b48278bd28d 100644 --- a/doc/OnlineDocs/src/data/import4.tab.py +++ b/doc/OnlineDocs/src/data/import4.tab.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/import5.tab.py b/doc/OnlineDocs/src/data/import5.tab.py index 1d20476a16f..9604c328c64 100644 --- a/doc/OnlineDocs/src/data/import5.tab.py +++ b/doc/OnlineDocs/src/data/import5.tab.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/import6.tab.py b/doc/OnlineDocs/src/data/import6.tab.py index 8a1ab232f86..a1c269a0abf 100644 --- a/doc/OnlineDocs/src/data/import6.tab.py +++ b/doc/OnlineDocs/src/data/import6.tab.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/import7.tab.py b/doc/OnlineDocs/src/data/import7.tab.py index 747d884be31..f4b60cb42d9 100644 --- a/doc/OnlineDocs/src/data/import7.tab.py +++ b/doc/OnlineDocs/src/data/import7.tab.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/import8.tab.py b/doc/OnlineDocs/src/data/import8.tab.py index b7866d7a3e5..d1d2e6f8160 100644 --- a/doc/OnlineDocs/src/data/import8.tab.py +++ b/doc/OnlineDocs/src/data/import8.tab.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/param1.py b/doc/OnlineDocs/src/data/param1.py index c4bc8de5acc..e606b7f6b4f 100644 --- a/doc/OnlineDocs/src/data/param1.py +++ b/doc/OnlineDocs/src/data/param1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/param2.py b/doc/OnlineDocs/src/data/param2.py index f46f05ceebc..725a6002ede 100644 --- a/doc/OnlineDocs/src/data/param2.py +++ b/doc/OnlineDocs/src/data/param2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/param2a.py b/doc/OnlineDocs/src/data/param2a.py index 4557f63d841..29416e2dcbc 100644 --- a/doc/OnlineDocs/src/data/param2a.py +++ b/doc/OnlineDocs/src/data/param2a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/param3.py b/doc/OnlineDocs/src/data/param3.py index 149155ce67d..0cc4df57511 100644 --- a/doc/OnlineDocs/src/data/param3.py +++ b/doc/OnlineDocs/src/data/param3.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/param3a.py b/doc/OnlineDocs/src/data/param3a.py index 0e99cad0c7a..42204de468f 100644 --- a/doc/OnlineDocs/src/data/param3a.py +++ b/doc/OnlineDocs/src/data/param3a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/param3b.py b/doc/OnlineDocs/src/data/param3b.py index deda175ea12..9f0375d7b87 100644 --- a/doc/OnlineDocs/src/data/param3b.py +++ b/doc/OnlineDocs/src/data/param3b.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/param3c.py b/doc/OnlineDocs/src/data/param3c.py index 4056dc8107d..9efac11553e 100644 --- a/doc/OnlineDocs/src/data/param3c.py +++ b/doc/OnlineDocs/src/data/param3c.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/param4.py b/doc/OnlineDocs/src/data/param4.py index 1190dae8dec..ab184e65ed3 100644 --- a/doc/OnlineDocs/src/data/param4.py +++ b/doc/OnlineDocs/src/data/param4.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/param5.py b/doc/OnlineDocs/src/data/param5.py index 69f6cc46552..f842e48995a 100644 --- a/doc/OnlineDocs/src/data/param5.py +++ b/doc/OnlineDocs/src/data/param5.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/param5a.py b/doc/OnlineDocs/src/data/param5a.py index 303b92f9f2e..f65de59ca78 100644 --- a/doc/OnlineDocs/src/data/param5a.py +++ b/doc/OnlineDocs/src/data/param5a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/param6.py b/doc/OnlineDocs/src/data/param6.py index c3e4b25d144..54cb350298b 100644 --- a/doc/OnlineDocs/src/data/param6.py +++ b/doc/OnlineDocs/src/data/param6.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/param6a.py b/doc/OnlineDocs/src/data/param6a.py index 07e8280cc18..7aabe7ec929 100644 --- a/doc/OnlineDocs/src/data/param6a.py +++ b/doc/OnlineDocs/src/data/param6a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/param7a.py b/doc/OnlineDocs/src/data/param7a.py index 3bb68b3f3b7..8d0c49210b5 100644 --- a/doc/OnlineDocs/src/data/param7a.py +++ b/doc/OnlineDocs/src/data/param7a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/param7b.py b/doc/OnlineDocs/src/data/param7b.py index 6e5c857851f..8481083c31c 100644 --- a/doc/OnlineDocs/src/data/param7b.py +++ b/doc/OnlineDocs/src/data/param7b.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/param8a.py b/doc/OnlineDocs/src/data/param8a.py index 57c9b08ca43..59ddba34091 100644 --- a/doc/OnlineDocs/src/data/param8a.py +++ b/doc/OnlineDocs/src/data/param8a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/set1.py b/doc/OnlineDocs/src/data/set1.py index 5248e9d5dc9..e1d8a09c394 100644 --- a/doc/OnlineDocs/src/data/set1.py +++ b/doc/OnlineDocs/src/data/set1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/set2.py b/doc/OnlineDocs/src/data/set2.py index 82772f48e46..8e2f4b756d9 100644 --- a/doc/OnlineDocs/src/data/set2.py +++ b/doc/OnlineDocs/src/data/set2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/set2a.py b/doc/OnlineDocs/src/data/set2a.py index edf28757f96..6358178d109 100644 --- a/doc/OnlineDocs/src/data/set2a.py +++ b/doc/OnlineDocs/src/data/set2a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/set3.py b/doc/OnlineDocs/src/data/set3.py index d58e0c0dd43..dced69c2375 100644 --- a/doc/OnlineDocs/src/data/set3.py +++ b/doc/OnlineDocs/src/data/set3.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/set4.py b/doc/OnlineDocs/src/data/set4.py index 29548519571..887d12ebf05 100644 --- a/doc/OnlineDocs/src/data/set4.py +++ b/doc/OnlineDocs/src/data/set4.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/set5.py b/doc/OnlineDocs/src/data/set5.py index 35acd4e4317..8fb5ced0a3f 100644 --- a/doc/OnlineDocs/src/data/set5.py +++ b/doc/OnlineDocs/src/data/set5.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/table0.py b/doc/OnlineDocs/src/data/table0.py index af7f634bd34..10ace6ea37b 100644 --- a/doc/OnlineDocs/src/data/table0.py +++ b/doc/OnlineDocs/src/data/table0.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/table0.ul.py b/doc/OnlineDocs/src/data/table0.ul.py index 213407b071c..79dc2933667 100644 --- a/doc/OnlineDocs/src/data/table0.ul.py +++ b/doc/OnlineDocs/src/data/table0.ul.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/table1.py b/doc/OnlineDocs/src/data/table1.py index 1f86508c60a..d69c35b4860 100644 --- a/doc/OnlineDocs/src/data/table1.py +++ b/doc/OnlineDocs/src/data/table1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/table2.py b/doc/OnlineDocs/src/data/table2.py index d7708b9277f..5b4718f60ad 100644 --- a/doc/OnlineDocs/src/data/table2.py +++ b/doc/OnlineDocs/src/data/table2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/table3.py b/doc/OnlineDocs/src/data/table3.py index fa871a4f79c..efa438eddd0 100644 --- a/doc/OnlineDocs/src/data/table3.py +++ b/doc/OnlineDocs/src/data/table3.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/table3.ul.py b/doc/OnlineDocs/src/data/table3.ul.py index 713d36b9f3a..ace661ac91c 100644 --- a/doc/OnlineDocs/src/data/table3.ul.py +++ b/doc/OnlineDocs/src/data/table3.ul.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/table4.py b/doc/OnlineDocs/src/data/table4.py index 1af9fe47a44..b571d8ec1e4 100644 --- a/doc/OnlineDocs/src/data/table4.py +++ b/doc/OnlineDocs/src/data/table4.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/table4.ul.py b/doc/OnlineDocs/src/data/table4.ul.py index 2acf8e21ca8..1b4f192130b 100644 --- a/doc/OnlineDocs/src/data/table4.ul.py +++ b/doc/OnlineDocs/src/data/table4.ul.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/table5.py b/doc/OnlineDocs/src/data/table5.py index 2fe3d08fe91..25269bb0bde 100644 --- a/doc/OnlineDocs/src/data/table5.py +++ b/doc/OnlineDocs/src/data/table5.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/table6.py b/doc/OnlineDocs/src/data/table6.py index fcbc2f10860..1e2201cb6d6 100644 --- a/doc/OnlineDocs/src/data/table6.py +++ b/doc/OnlineDocs/src/data/table6.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/data/table7.py b/doc/OnlineDocs/src/data/table7.py index f8f8e769b2e..e5507c546ea 100644 --- a/doc/OnlineDocs/src/data/table7.py +++ b/doc/OnlineDocs/src/data/table7.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/dataportal/dataportal_tab.py b/doc/OnlineDocs/src/dataportal/dataportal_tab.py index d1a75196c99..d6b679078e6 100644 --- a/doc/OnlineDocs/src/dataportal/dataportal_tab.py +++ b/doc/OnlineDocs/src/dataportal/dataportal_tab.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * # -------------------------------------------------- diff --git a/doc/OnlineDocs/src/dataportal/param_initialization.py b/doc/OnlineDocs/src/dataportal/param_initialization.py index 5567b01f284..71c54b4a9d9 100644 --- a/doc/OnlineDocs/src/dataportal/param_initialization.py +++ b/doc/OnlineDocs/src/dataportal/param_initialization.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * import numpy diff --git a/doc/OnlineDocs/src/dataportal/set_initialization.py b/doc/OnlineDocs/src/dataportal/set_initialization.py index aa7b426fa82..a086473fb1c 100644 --- a/doc/OnlineDocs/src/dataportal/set_initialization.py +++ b/doc/OnlineDocs/src/dataportal/set_initialization.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * import numpy diff --git a/doc/OnlineDocs/src/expr/design.py b/doc/OnlineDocs/src/expr/design.py index b122a5f2bf3..a5401a3c554 100644 --- a/doc/OnlineDocs/src/expr/design.py +++ b/doc/OnlineDocs/src/expr/design.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * # --------------------------------------------- diff --git a/doc/OnlineDocs/src/expr/index.py b/doc/OnlineDocs/src/expr/index.py index 9c9c79bf7be..65291c0ff6f 100644 --- a/doc/OnlineDocs/src/expr/index.py +++ b/doc/OnlineDocs/src/expr/index.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * # --------------------------------------------- diff --git a/doc/OnlineDocs/src/expr/managing.py b/doc/OnlineDocs/src/expr/managing.py index 0a59c13bc1b..48bb005943e 100644 --- a/doc/OnlineDocs/src/expr/managing.py +++ b/doc/OnlineDocs/src/expr/managing.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * from math import isclose import math diff --git a/doc/OnlineDocs/src/expr/overview.py b/doc/OnlineDocs/src/expr/overview.py index 6207a4c4288..32a5f569115 100644 --- a/doc/OnlineDocs/src/expr/overview.py +++ b/doc/OnlineDocs/src/expr/overview.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * # --------------------------------------------- diff --git a/doc/OnlineDocs/src/expr/performance.py b/doc/OnlineDocs/src/expr/performance.py index 53ac5bb4f9e..59514718cb4 100644 --- a/doc/OnlineDocs/src/expr/performance.py +++ b/doc/OnlineDocs/src/expr/performance.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * # --------------------------------------------- diff --git a/doc/OnlineDocs/src/expr/quicksum.py b/doc/OnlineDocs/src/expr/quicksum.py index a1ad9660664..6b4d70bd961 100644 --- a/doc/OnlineDocs/src/expr/quicksum.py +++ b/doc/OnlineDocs/src/expr/quicksum.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * from pyomo.repn import generate_standard_repn import time diff --git a/doc/OnlineDocs/src/scripting/AbstractSuffixes.py b/doc/OnlineDocs/src/scripting/AbstractSuffixes.py index 20a4cc20581..24162d7bc8f 100644 --- a/doc/OnlineDocs/src/scripting/AbstractSuffixes.py +++ b/doc/OnlineDocs/src/scripting/AbstractSuffixes.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/doc/OnlineDocs/src/scripting/Isinglebuild.py b/doc/OnlineDocs/src/scripting/Isinglebuild.py index 00f79c9a750..b31e692d198 100644 --- a/doc/OnlineDocs/src/scripting/Isinglebuild.py +++ b/doc/OnlineDocs/src/scripting/Isinglebuild.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Isinglebuild.py # NodesIn and NodesOut are created by a build action using the Arcs from pyomo.environ import * diff --git a/doc/OnlineDocs/src/scripting/NodesIn_init.py b/doc/OnlineDocs/src/scripting/NodesIn_init.py index 4a90029baa3..60268ef3183 100644 --- a/doc/OnlineDocs/src/scripting/NodesIn_init.py +++ b/doc/OnlineDocs/src/scripting/NodesIn_init.py @@ -1,3 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + def NodesIn_init(model, node): retval = [] for i, j in model.Arcs: diff --git a/doc/OnlineDocs/src/scripting/Z_init.py b/doc/OnlineDocs/src/scripting/Z_init.py index 426de6f7d08..ab441eef101 100644 --- a/doc/OnlineDocs/src/scripting/Z_init.py +++ b/doc/OnlineDocs/src/scripting/Z_init.py @@ -1,3 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + def Z_init(model, i): if i > 10: return Set.End diff --git a/doc/OnlineDocs/src/scripting/abstract2.py b/doc/OnlineDocs/src/scripting/abstract2.py index 1e14d1d1898..1f7c35508db 100644 --- a/doc/OnlineDocs/src/scripting/abstract2.py +++ b/doc/OnlineDocs/src/scripting/abstract2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # abstract2.py diff --git a/doc/OnlineDocs/src/scripting/abstract2piece.py b/doc/OnlineDocs/src/scripting/abstract2piece.py index 225ec0d1a64..8b58184e5e4 100644 --- a/doc/OnlineDocs/src/scripting/abstract2piece.py +++ b/doc/OnlineDocs/src/scripting/abstract2piece.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # abstract2piece.py # Similar to abstract2.py, but the objective is now c times x to the fourth power diff --git a/doc/OnlineDocs/src/scripting/abstract2piecebuild.py b/doc/OnlineDocs/src/scripting/abstract2piecebuild.py index 1f00cdb0265..694ee2b0336 100644 --- a/doc/OnlineDocs/src/scripting/abstract2piecebuild.py +++ b/doc/OnlineDocs/src/scripting/abstract2piecebuild.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # abstract2piecebuild.py # Similar to abstract2piece.py, but the breakpoints are created using a build action diff --git a/doc/OnlineDocs/src/scripting/block_iter_example.py b/doc/OnlineDocs/src/scripting/block_iter_example.py index 680e0d1728b..27d5a0e1819 100644 --- a/doc/OnlineDocs/src/scripting/block_iter_example.py +++ b/doc/OnlineDocs/src/scripting/block_iter_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # written by jds, adapted for doc by dlw from pyomo.environ import * diff --git a/doc/OnlineDocs/src/scripting/concrete1.py b/doc/OnlineDocs/src/scripting/concrete1.py index 2cd1a1f722c..31986c21ded 100644 --- a/doc/OnlineDocs/src/scripting/concrete1.py +++ b/doc/OnlineDocs/src/scripting/concrete1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = ConcreteModel() diff --git a/doc/OnlineDocs/src/scripting/doubleA.py b/doc/OnlineDocs/src/scripting/doubleA.py index 12a07944db3..2f65266d685 100644 --- a/doc/OnlineDocs/src/scripting/doubleA.py +++ b/doc/OnlineDocs/src/scripting/doubleA.py @@ -1,3 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + def doubleA_init(model): return (i * 2 for i in model.A) diff --git a/doc/OnlineDocs/src/scripting/driveabs2.py b/doc/OnlineDocs/src/scripting/driveabs2.py index 45862195a57..87056f0fcb6 100644 --- a/doc/OnlineDocs/src/scripting/driveabs2.py +++ b/doc/OnlineDocs/src/scripting/driveabs2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # driveabs2.py import pyomo.environ as pyo diff --git a/doc/OnlineDocs/src/scripting/driveconc1.py b/doc/OnlineDocs/src/scripting/driveconc1.py index 95b0f42806d..2f7ece65a30 100644 --- a/doc/OnlineDocs/src/scripting/driveconc1.py +++ b/doc/OnlineDocs/src/scripting/driveconc1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # driveconc1.py import pyomo.environ as pyo diff --git a/doc/OnlineDocs/src/scripting/iterative1.py b/doc/OnlineDocs/src/scripting/iterative1.py index 61b0fd3828e..8e91ea0d516 100644 --- a/doc/OnlineDocs/src/scripting/iterative1.py +++ b/doc/OnlineDocs/src/scripting/iterative1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @Import_symbols_for_pyomo # iterative1.py import pyomo.environ as pyo diff --git a/doc/OnlineDocs/src/scripting/iterative2.py b/doc/OnlineDocs/src/scripting/iterative2.py index e559a2c8400..558e7427441 100644 --- a/doc/OnlineDocs/src/scripting/iterative2.py +++ b/doc/OnlineDocs/src/scripting/iterative2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # iterative2.py import pyomo.environ as pyo diff --git a/doc/OnlineDocs/src/scripting/noiteration1.py b/doc/OnlineDocs/src/scripting/noiteration1.py index be9fb529855..079b99365da 100644 --- a/doc/OnlineDocs/src/scripting/noiteration1.py +++ b/doc/OnlineDocs/src/scripting/noiteration1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # noiteration1.py import pyomo.environ as pyo diff --git a/doc/OnlineDocs/src/scripting/parallel.py b/doc/OnlineDocs/src/scripting/parallel.py index cf9b55d9605..ead3e1d674b 100644 --- a/doc/OnlineDocs/src/scripting/parallel.py +++ b/doc/OnlineDocs/src/scripting/parallel.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # parallel.py # run with mpirun -np 2 python -m mpi4py parallel.py import pyomo.environ as pyo diff --git a/doc/OnlineDocs/src/scripting/spy4Constraints.py b/doc/OnlineDocs/src/scripting/spy4Constraints.py index f0033bbc33e..f0f43672602 100644 --- a/doc/OnlineDocs/src/scripting/spy4Constraints.py +++ b/doc/OnlineDocs/src/scripting/spy4Constraints.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ David L. Woodruff and Mingye Yang, Spring 2018 Code snippets for Constraints.rst in testable form diff --git a/doc/OnlineDocs/src/scripting/spy4Expressions.py b/doc/OnlineDocs/src/scripting/spy4Expressions.py index 0e8a50c78b3..415481203a5 100644 --- a/doc/OnlineDocs/src/scripting/spy4Expressions.py +++ b/doc/OnlineDocs/src/scripting/spy4Expressions.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ David L. Woodruff and Mingye Yang, Spring 2018 Code snippets for Expressions.rst in testable form diff --git a/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py b/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py index f655b812076..66dcb5e36b4 100644 --- a/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py +++ b/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ David L. Woodruff and Mingye Yang, Spring 2018 Code snippets for PyomoCommand.rst in testable form diff --git a/doc/OnlineDocs/src/scripting/spy4Variables.py b/doc/OnlineDocs/src/scripting/spy4Variables.py index c4e2ff612f1..1dcdfc58a10 100644 --- a/doc/OnlineDocs/src/scripting/spy4Variables.py +++ b/doc/OnlineDocs/src/scripting/spy4Variables.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ David L. Woodruff and Mingye Yang, Spring 2018 Code snippets for Variables.rst in testable form diff --git a/doc/OnlineDocs/src/scripting/spy4scripts.py b/doc/OnlineDocs/src/scripting/spy4scripts.py index 48ba923d09c..d35030241fc 100644 --- a/doc/OnlineDocs/src/scripting/spy4scripts.py +++ b/doc/OnlineDocs/src/scripting/spy4scripts.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + ###NOTE: as of May 16, this will not even come close to running. DLW ### and it is "wrong" in a lot of places. ### Someone should edit this file, then delete these comment lines. DLW may 16 diff --git a/doc/OnlineDocs/src/strip_examples.py b/doc/OnlineDocs/src/strip_examples.py index 045af6b87cc..dc742c1f53b 100644 --- a/doc/OnlineDocs/src/strip_examples.py +++ b/doc/OnlineDocs/src/strip_examples.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # This script finds all *.py files in the current and subdirectories. # It processes these files to find blocks that start/end with "# @" diff --git a/examples/dae/car_example.py b/examples/dae/car_example.py index a157159cf6c..f632e83f62a 100644 --- a/examples/dae/car_example.py +++ b/examples/dae/car_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Ampl Car Example # # Shows how to convert a minimize final time optimal control problem diff --git a/examples/dae/disease_DAE.py b/examples/dae/disease_DAE.py index 59e598aa504..319e7276d83 100644 --- a/examples/dae/disease_DAE.py +++ b/examples/dae/disease_DAE.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + ### # SIR disease model using radau collocation ### diff --git a/examples/dae/run_disease.py b/examples/dae/run_disease.py index 139046d434e..04457dfc890 100644 --- a/examples/dae/run_disease.py +++ b/examples/dae/run_disease.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * from pyomo.dae import * from disease_DAE import model diff --git a/examples/dae/run_stochpdegas_automatic.py b/examples/dae/run_stochpdegas_automatic.py index dd710588406..92f95b9d828 100644 --- a/examples/dae/run_stochpdegas_automatic.py +++ b/examples/dae/run_stochpdegas_automatic.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import time from pyomo.environ import * diff --git a/examples/dae/simulator_dae_example.py b/examples/dae/simulator_dae_example.py index ef6484be6c6..81fd3af816d 100644 --- a/examples/dae/simulator_dae_example.py +++ b/examples/dae/simulator_dae_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # Batch reactor example from Biegler book on Nonlinear Programming Chapter 9 # diff --git a/examples/dae/simulator_dae_multindex_example.py b/examples/dae/simulator_dae_multindex_example.py index d1a97fec79f..bc17fd41643 100644 --- a/examples/dae/simulator_dae_multindex_example.py +++ b/examples/dae/simulator_dae_multindex_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # Batch reactor example from Biegler book on Nonlinear Programming Chapter 9 # diff --git a/examples/dae/simulator_ode_example.py b/examples/dae/simulator_ode_example.py index bf600cf163e..ae30071f4ef 100644 --- a/examples/dae/simulator_ode_example.py +++ b/examples/dae/simulator_ode_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # Example from Scipy odeint examples # diff --git a/examples/dae/simulator_ode_multindex_example.py b/examples/dae/simulator_ode_multindex_example.py index fa2623f4cc2..e02eabef076 100644 --- a/examples/dae/simulator_ode_multindex_example.py +++ b/examples/dae/simulator_ode_multindex_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # Example from Scipy odeint examples # diff --git a/examples/dae/stochpdegas_automatic.py b/examples/dae/stochpdegas_automatic.py index fdde099a396..e846d045ddc 100644 --- a/examples/dae/stochpdegas_automatic.py +++ b/examples/dae/stochpdegas_automatic.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # stochastic pde model for natural gas network # victor m. zavala / 2013 diff --git a/examples/doc/samples/__init__.py b/examples/doc/samples/__init__.py index 3115f06ef53..c967348cb68 100644 --- a/examples/doc/samples/__init__.py +++ b/examples/doc/samples/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Dummy file for pytest diff --git a/examples/doc/samples/case_studies/deer/DeerProblem.py b/examples/doc/samples/case_studies/deer/DeerProblem.py index 0b6b7252aaa..dfdc987ade4 100644 --- a/examples/doc/samples/case_studies/deer/DeerProblem.py +++ b/examples/doc/samples/case_studies/deer/DeerProblem.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * # diff --git a/examples/doc/samples/case_studies/diet/DietProblem.py b/examples/doc/samples/case_studies/diet/DietProblem.py index f070201c28e..462deee03a5 100644 --- a/examples/doc/samples/case_studies/diet/DietProblem.py +++ b/examples/doc/samples/case_studies/diet/DietProblem.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = AbstractModel() diff --git a/examples/doc/samples/case_studies/disease_est/DiseaseEstimation.py b/examples/doc/samples/case_studies/disease_est/DiseaseEstimation.py index c685a6ee67f..d8f3f94bcc5 100644 --- a/examples/doc/samples/case_studies/disease_est/DiseaseEstimation.py +++ b/examples/doc/samples/case_studies/disease_est/DiseaseEstimation.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = AbstractModel() diff --git a/examples/doc/samples/case_studies/max_flow/MaxFlow.py b/examples/doc/samples/case_studies/max_flow/MaxFlow.py index c6eb42ccf7d..36d42dfd3e3 100644 --- a/examples/doc/samples/case_studies/max_flow/MaxFlow.py +++ b/examples/doc/samples/case_studies/max_flow/MaxFlow.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = AbstractModel() diff --git a/examples/doc/samples/case_studies/network_flow/networkFlow1.py b/examples/doc/samples/case_studies/network_flow/networkFlow1.py index adfaab4476b..a1d05ccbd20 100644 --- a/examples/doc/samples/case_studies/network_flow/networkFlow1.py +++ b/examples/doc/samples/case_studies/network_flow/networkFlow1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = AbstractModel() diff --git a/examples/doc/samples/case_studies/rosen/Rosenbrock.py b/examples/doc/samples/case_studies/rosen/Rosenbrock.py index 9677cea95dd..f70fa30199c 100644 --- a/examples/doc/samples/case_studies/rosen/Rosenbrock.py +++ b/examples/doc/samples/case_studies/rosen/Rosenbrock.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @intro: from pyomo.core import * diff --git a/examples/doc/samples/case_studies/transportation/transportation.py b/examples/doc/samples/case_studies/transportation/transportation.py index 26fcb5f0b66..620e6c3fa1d 100644 --- a/examples/doc/samples/case_studies/transportation/transportation.py +++ b/examples/doc/samples/case_studies/transportation/transportation.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = AbstractModel() diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_cplex.py b/examples/doc/samples/comparisons/cutstock/cutstock_cplex.py index 796c39810f8..e61f82b388d 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_cplex.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_cplex.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import cplex from cutstock_util import * from cplex.exceptions import CplexSolverError diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_grb.py b/examples/doc/samples/comparisons/cutstock/cutstock_grb.py index 4fa4556fc96..9940c729b6e 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_grb.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_grb.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from gurobipy import * from cutstock_util import * diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_lpsolve.py b/examples/doc/samples/comparisons/cutstock/cutstock_lpsolve.py index 658ee006c30..5bc7aea2116 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_lpsolve.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_lpsolve.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from lpsolve55 import * from cutstock_util import * diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_pulpor.py b/examples/doc/samples/comparisons/cutstock/cutstock_pulpor.py index 2f2506ba3d6..3f366e9b3e2 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_pulpor.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_pulpor.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pulp import * from cutstock_util import * diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_pyomo.py b/examples/doc/samples/comparisons/cutstock/cutstock_pyomo.py index a67ebdd0675..c87b93c37a5 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_pyomo.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_pyomo.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * import pyomo.opt from cutstock_util import * diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_util.py b/examples/doc/samples/comparisons/cutstock/cutstock_util.py index 1cd8c61922f..3fde234a0f9 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_util.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_util.py @@ -1,3 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + def getCutCount(): cutCount = 0 fout1 = open('WidthDemand.csv', 'r') diff --git a/examples/doc/samples/comparisons/sched/pyomo/sched.py b/examples/doc/samples/comparisons/sched/pyomo/sched.py index 627bc083fbe..2c03bebb421 100644 --- a/examples/doc/samples/comparisons/sched/pyomo/sched.py +++ b/examples/doc/samples/comparisons/sched/pyomo/sched.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = AbstractModel() diff --git a/examples/doc/samples/scripts/__init__.py b/examples/doc/samples/scripts/__init__.py index 3115f06ef53..c967348cb68 100644 --- a/examples/doc/samples/scripts/__init__.py +++ b/examples/doc/samples/scripts/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Dummy file for pytest diff --git a/examples/doc/samples/scripts/s1/knapsack.py b/examples/doc/samples/scripts/s1/knapsack.py index 642e0faaaed..2965d76650c 100644 --- a/examples/doc/samples/scripts/s1/knapsack.py +++ b/examples/doc/samples/scripts/s1/knapsack.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * diff --git a/examples/doc/samples/scripts/s1/script.py b/examples/doc/samples/scripts/s1/script.py index 02b6b406922..b5d60af6182 100644 --- a/examples/doc/samples/scripts/s1/script.py +++ b/examples/doc/samples/scripts/s1/script.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * import pyomo.opt import pyomo.environ diff --git a/examples/doc/samples/scripts/s2/knapsack.py b/examples/doc/samples/scripts/s2/knapsack.py index a7d693f5d35..66e55188871 100644 --- a/examples/doc/samples/scripts/s2/knapsack.py +++ b/examples/doc/samples/scripts/s2/knapsack.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * diff --git a/examples/doc/samples/scripts/s2/script.py b/examples/doc/samples/scripts/s2/script.py index 88de1dec680..481ae7b26bb 100644 --- a/examples/doc/samples/scripts/s2/script.py +++ b/examples/doc/samples/scripts/s2/script.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * import pyomo.opt import pyomo.environ diff --git a/examples/doc/samples/update.py b/examples/doc/samples/update.py index 9eae2f4b694..ab2195d1f32 100644 --- a/examples/doc/samples/update.py +++ b/examples/doc/samples/update.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + #!/usr/bin/env python # # This is a Python script that regenerates the top-level TRAC.txt file, which diff --git a/examples/gdp/batchProcessing.py b/examples/gdp/batchProcessing.py index f0980dd5034..4f3fb02df25 100644 --- a/examples/gdp/batchProcessing.py +++ b/examples/gdp/batchProcessing.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * from pyomo.gdp import * diff --git a/examples/gdp/circles/circles.py b/examples/gdp/circles/circles.py index ae905998403..3a7846f1441 100644 --- a/examples/gdp/circles/circles.py +++ b/examples/gdp/circles/circles.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ The "circles" GDP example problem originating in Lee and Grossman (2000). The goal is to choose a point to minimize a convex quadratic function over a set of diff --git a/examples/gdp/constrained_layout/cons_layout_model.py b/examples/gdp/constrained_layout/cons_layout_model.py index 245aa2df58e..9f9169ede22 100644 --- a/examples/gdp/constrained_layout/cons_layout_model.py +++ b/examples/gdp/constrained_layout/cons_layout_model.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """2-D constrained layout example. Example based on: https://www.minlp.org/library/problem/index.php?i=107&lib=GDP diff --git a/examples/gdp/eight_process/eight_proc_logical.py b/examples/gdp/eight_process/eight_proc_logical.py index 60f7acee876..23827a52d71 100644 --- a/examples/gdp/eight_process/eight_proc_logical.py +++ b/examples/gdp/eight_process/eight_proc_logical.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Disjunctive re-implementation of eight-process problem. Re-implementation of Duran example 3 superstructure synthesis problem in Pyomo diff --git a/examples/gdp/eight_process/eight_proc_model.py b/examples/gdp/eight_process/eight_proc_model.py index 840b6911d83..d333405e469 100644 --- a/examples/gdp/eight_process/eight_proc_model.py +++ b/examples/gdp/eight_process/eight_proc_model.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Disjunctive re-implementation of eight-process problem. Re-implementation of Duran example 3 superstructure synthesis problem in Pyomo diff --git a/examples/gdp/eight_process/eight_proc_verbose_model.py b/examples/gdp/eight_process/eight_proc_verbose_model.py index cae584d4127..fc748cce20f 100644 --- a/examples/gdp/eight_process/eight_proc_verbose_model.py +++ b/examples/gdp/eight_process/eight_proc_verbose_model.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Disjunctive re-implementation of eight-process problem. This is the more verbose formulation of the same problem given in diff --git a/examples/gdp/farm_layout/farm_layout.py b/examples/gdp/farm_layout/farm_layout.py index 411e2de3242..87043bc4ff5 100644 --- a/examples/gdp/farm_layout/farm_layout.py +++ b/examples/gdp/farm_layout/farm_layout.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Farm layout example from Sawaya (2006). The goal is to determine optimal placements and dimensions for farm plots of specified areas to minimize the perimeter of a minimal enclosing fence. This is a GDP problem with diff --git a/examples/gdp/medTermPurchasing_Literal.py b/examples/gdp/medTermPurchasing_Literal.py index c9b27920396..14ec25d750c 100755 --- a/examples/gdp/medTermPurchasing_Literal.py +++ b/examples/gdp/medTermPurchasing_Literal.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * from pyomo.gdp import * diff --git a/examples/gdp/nine_process/small_process.py b/examples/gdp/nine_process/small_process.py index 2758069f316..adc7098d991 100644 --- a/examples/gdp/nine_process/small_process.py +++ b/examples/gdp/nine_process/small_process.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Small process synthesis-inspired toy GDP example. """ diff --git a/examples/gdp/simple1.py b/examples/gdp/simple1.py index f7c77b111f0..323943cd552 100644 --- a/examples/gdp/simple1.py +++ b/examples/gdp/simple1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Example: modeling a complementarity condition as a # disjunction # diff --git a/examples/gdp/simple2.py b/examples/gdp/simple2.py index 6bcc7bbf747..9c13872100c 100644 --- a/examples/gdp/simple2.py +++ b/examples/gdp/simple2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Example: modeling a complementarity condition as a # disjunction # diff --git a/examples/gdp/simple3.py b/examples/gdp/simple3.py index 6b3d6ec46c4..bbe9745d193 100644 --- a/examples/gdp/simple3.py +++ b/examples/gdp/simple3.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Example: modeling a complementarity condition as a # disjunction # diff --git a/examples/gdp/small_lit/basic_step.py b/examples/gdp/small_lit/basic_step.py index 48ef52d9ba0..fd466dfc1f4 100644 --- a/examples/gdp/small_lit/basic_step.py +++ b/examples/gdp/small_lit/basic_step.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Example from Section 3.2 in paper of Pseudo Basic Steps Ref: diff --git a/examples/gdp/small_lit/contracts_problem.py b/examples/gdp/small_lit/contracts_problem.py index 500fe15cb2a..9d1254688b2 100644 --- a/examples/gdp/small_lit/contracts_problem.py +++ b/examples/gdp/small_lit/contracts_problem.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Example from 'Lagrangean Relaxation of the Hull-Reformulation of Linear \ Generalized Disjunctive Programs and its use in Disjunctive Branch \ and Bound' Page 25 f. diff --git a/examples/gdp/small_lit/ex1_Lee.py b/examples/gdp/small_lit/ex1_Lee.py index ddd2e1c3d2f..05bd1bd1bc0 100644 --- a/examples/gdp/small_lit/ex1_Lee.py +++ b/examples/gdp/small_lit/ex1_Lee.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Simple example of nonlinear problem modeled with GDP framework. Taken from Example 1 of the paper "New Algorithms for Nonlinear Generalized Disjunctive Programming" by Lee and Grossmann diff --git a/examples/gdp/small_lit/ex_633_trespalacios.py b/examples/gdp/small_lit/ex_633_trespalacios.py index 61b7294e3ba..499294be2ae 100644 --- a/examples/gdp/small_lit/ex_633_trespalacios.py +++ b/examples/gdp/small_lit/ex_633_trespalacios.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Analytical example from Section 6.3.3 of F. Trespalacions Ph.D. Thesis (2015) Analytical example for a nonconvex GDP with 2 disjunctions, each with 2 disjuncts. diff --git a/examples/gdp/small_lit/nonconvex_HEN.py b/examples/gdp/small_lit/nonconvex_HEN.py index 99e2c4f15e2..bdec0e2823a 100644 --- a/examples/gdp/small_lit/nonconvex_HEN.py +++ b/examples/gdp/small_lit/nonconvex_HEN.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Example from 'Systematic Modeling of Discrete-Continuous Optimization \ Models through Generalized Disjunctive Programming' Ignacio E. Grossmann and Francisco Trespalacios, 2013 diff --git a/examples/gdp/stickies.py b/examples/gdp/stickies.py index 75beb911415..154a9cbc0cd 100644 --- a/examples/gdp/stickies.py +++ b/examples/gdp/stickies.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import os from pyomo.common.fileutils import this_file_dir diff --git a/examples/gdp/strip_packing/stripPacking.py b/examples/gdp/strip_packing/stripPacking.py index 0e8902c5ee4..fb2ed3f91fd 100644 --- a/examples/gdp/strip_packing/stripPacking.py +++ b/examples/gdp/strip_packing/stripPacking.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * from pyomo.gdp import * diff --git a/examples/gdp/strip_packing/strip_packing_8rect.py b/examples/gdp/strip_packing/strip_packing_8rect.py index e1350dbc39e..f03b3f798f9 100644 --- a/examples/gdp/strip_packing/strip_packing_8rect.py +++ b/examples/gdp/strip_packing/strip_packing_8rect.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Strip packing example from MINLP.org library. Strip-packing example from http://minlp.org/library/lib.php?lib=GDP This model packs a set of rectangles without rotation or overlap within a diff --git a/examples/gdp/strip_packing/strip_packing_concrete.py b/examples/gdp/strip_packing/strip_packing_concrete.py index 1313d75561c..d5ace9632fd 100644 --- a/examples/gdp/strip_packing/strip_packing_concrete.py +++ b/examples/gdp/strip_packing/strip_packing_concrete.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Strip packing example from MINLP.org library. Strip-packing example from http://minlp.org/library/lib.php?lib=GDP diff --git a/examples/gdp/two_rxn_lee/two_rxn_model.py b/examples/gdp/two_rxn_lee/two_rxn_model.py index 2e5f1734130..4f9471b583a 100644 --- a/examples/gdp/two_rxn_lee/two_rxn_model.py +++ b/examples/gdp/two_rxn_lee/two_rxn_model.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Two reactor model from literature. See README.md.""" from pyomo.core import ConcreteModel, Constraint, Objective, Param, Var, maximize diff --git a/examples/kernel/blocks.py b/examples/kernel/blocks.py index 7036981dcc8..b19108ffb44 100644 --- a/examples/kernel/blocks.py +++ b/examples/kernel/blocks.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo # diff --git a/examples/kernel/conic.py b/examples/kernel/conic.py index a2a787794a4..86e5a95580c 100644 --- a/examples/kernel/conic.py +++ b/examples/kernel/conic.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo # diff --git a/examples/kernel/constraints.py b/examples/kernel/constraints.py index 6495ad12f63..e5bf9797987 100644 --- a/examples/kernel/constraints.py +++ b/examples/kernel/constraints.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo v = pmo.variable() diff --git a/examples/kernel/containers.py b/examples/kernel/containers.py index 9b525e87af6..44c65bfbda8 100644 --- a/examples/kernel/containers.py +++ b/examples/kernel/containers.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo # diff --git a/examples/kernel/expressions.py b/examples/kernel/expressions.py index 1756e5d3fd4..2f27239f26e 100644 --- a/examples/kernel/expressions.py +++ b/examples/kernel/expressions.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo v = pmo.variable(value=2) diff --git a/examples/kernel/mosek/geometric1.py b/examples/kernel/mosek/geometric1.py index b5ec59541c4..9cd492f36cf 100644 --- a/examples/kernel/mosek/geometric1.py +++ b/examples/kernel/mosek/geometric1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Source: https://docs.mosek.com/9.0/pythonapi/tutorial-gp-shared.html import pyomo.kernel as pmo diff --git a/examples/kernel/mosek/geometric2.py b/examples/kernel/mosek/geometric2.py index 84825c0a39b..2f75f721dc4 100644 --- a/examples/kernel/mosek/geometric2.py +++ b/examples/kernel/mosek/geometric2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Source: https://docs.mosek.com/modeling-cookbook/expo.html # (first example in Section 5.3.1) diff --git a/examples/kernel/mosek/maximum_volume_cuboid.py b/examples/kernel/mosek/maximum_volume_cuboid.py index 92e210cf400..661adc3bf5a 100644 --- a/examples/kernel/mosek/maximum_volume_cuboid.py +++ b/examples/kernel/mosek/maximum_volume_cuboid.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from scipy.spatial import ConvexHull from mpl_toolkits.mplot3d import Axes3D from mpl_toolkits.mplot3d.art3d import Poly3DCollection diff --git a/examples/kernel/mosek/power1.py b/examples/kernel/mosek/power1.py index d7a12c1ce54..b8a306e7cdf 100644 --- a/examples/kernel/mosek/power1.py +++ b/examples/kernel/mosek/power1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Source: https://docs.mosek.com/9.0/pythonapi/tutorial-pow-shared.html import pyomo.kernel as pmo diff --git a/examples/kernel/mosek/semidefinite.py b/examples/kernel/mosek/semidefinite.py index 44ab7c95a68..177662205f8 100644 --- a/examples/kernel/mosek/semidefinite.py +++ b/examples/kernel/mosek/semidefinite.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Source: https://docs.mosek.com/latest/pythonfusion/tutorial-sdo-shared.html#doc-tutorial-sdo # This examples illustrates SDP formulations in Pyomo using diff --git a/examples/kernel/objectives.py b/examples/kernel/objectives.py index 7d87671ef8d..bbb0c704211 100644 --- a/examples/kernel/objectives.py +++ b/examples/kernel/objectives.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo v = pmo.variable(value=2) diff --git a/examples/kernel/parameters.py b/examples/kernel/parameters.py index 55b230add6b..a8bce6ca6af 100644 --- a/examples/kernel/parameters.py +++ b/examples/kernel/parameters.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo # diff --git a/examples/kernel/piecewise_functions.py b/examples/kernel/piecewise_functions.py index 528d4c16791..f372227fcb4 100644 --- a/examples/kernel/piecewise_functions.py +++ b/examples/kernel/piecewise_functions.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo # diff --git a/examples/kernel/piecewise_nd_functions.py b/examples/kernel/piecewise_nd_functions.py index 847bb5f4a84..78739e3825c 100644 --- a/examples/kernel/piecewise_nd_functions.py +++ b/examples/kernel/piecewise_nd_functions.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import random import sys diff --git a/examples/kernel/special_ordered_sets.py b/examples/kernel/special_ordered_sets.py index 9526a551c12..53328923a60 100644 --- a/examples/kernel/special_ordered_sets.py +++ b/examples/kernel/special_ordered_sets.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo v1 = pmo.variable() diff --git a/examples/kernel/suffixes.py b/examples/kernel/suffixes.py index 39caa5b8652..029dd046f26 100644 --- a/examples/kernel/suffixes.py +++ b/examples/kernel/suffixes.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo # diff --git a/examples/kernel/variables.py b/examples/kernel/variables.py index 7ab571245a1..b2dd0ae8dff 100644 --- a/examples/kernel/variables.py +++ b/examples/kernel/variables.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.kernel as pmo # diff --git a/examples/mpec/bard1.py b/examples/mpec/bard1.py index dbe666a7004..4a6f7ab6642 100644 --- a/examples/mpec/bard1.py +++ b/examples/mpec/bard1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # bard1.py QQR2-MN-8-5 # Original Pyomo coding by William Hart # Adapted from AMPL coding by Sven Leyffer diff --git a/examples/mpec/scholtes4.py b/examples/mpec/scholtes4.py index 904729780cf..93cdb8fa6fe 100644 --- a/examples/mpec/scholtes4.py +++ b/examples/mpec/scholtes4.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # scholtes4.py LQR2-MN-3-2 # Original Pyomo coding by William Hart # Adapted from AMPL coding by Sven Leyffer diff --git a/examples/performance/dae/run_stochpdegas1_automatic.py b/examples/performance/dae/run_stochpdegas1_automatic.py index 993e22c7c86..5eacc8992d1 100644 --- a/examples/performance/dae/run_stochpdegas1_automatic.py +++ b/examples/performance/dae/run_stochpdegas1_automatic.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import time from pyomo.environ import * diff --git a/examples/performance/dae/stochpdegas1_automatic.py b/examples/performance/dae/stochpdegas1_automatic.py index 905ec9a5330..962ed266148 100644 --- a/examples/performance/dae/stochpdegas1_automatic.py +++ b/examples/performance/dae/stochpdegas1_automatic.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # stochastic pde model for natural gas network # victor m. zavala / 2013 diff --git a/examples/performance/jump/clnlbeam.py b/examples/performance/jump/clnlbeam.py index d2ceda790ec..18948c1b549 100644 --- a/examples/performance/jump/clnlbeam.py +++ b/examples/performance/jump/clnlbeam.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = AbstractModel() diff --git a/examples/performance/jump/facility.py b/examples/performance/jump/facility.py index 6832e8d32ac..b67f21bd048 100644 --- a/examples/performance/jump/facility.py +++ b/examples/performance/jump/facility.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = AbstractModel() diff --git a/examples/performance/jump/lqcp.py b/examples/performance/jump/lqcp.py index bb3e66b36f5..b4da6b62a5e 100644 --- a/examples/performance/jump/lqcp.py +++ b/examples/performance/jump/lqcp.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import * model = ConcreteModel() diff --git a/examples/performance/jump/opf_66200bus.py b/examples/performance/jump/opf_66200bus.py index f3e1822fbfb..022eb938fb0 100644 --- a/examples/performance/jump/opf_66200bus.py +++ b/examples/performance/jump/opf_66200bus.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/jump/opf_6620bus.py b/examples/performance/jump/opf_6620bus.py index 64348ae931e..0e139cd8c5f 100644 --- a/examples/performance/jump/opf_6620bus.py +++ b/examples/performance/jump/opf_6620bus.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/jump/opf_662bus.py b/examples/performance/jump/opf_662bus.py index 6ff97c577e3..5270c573236 100644 --- a/examples/performance/jump/opf_662bus.py +++ b/examples/performance/jump/opf_662bus.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/bilinear1_100.py b/examples/performance/misc/bilinear1_100.py index e68fbba6283..527cf5d7ccc 100644 --- a/examples/performance/misc/bilinear1_100.py +++ b/examples/performance/misc/bilinear1_100.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/bilinear1_100000.py b/examples/performance/misc/bilinear1_100000.py index 924d7233d24..9fdef98c059 100644 --- a/examples/performance/misc/bilinear1_100000.py +++ b/examples/performance/misc/bilinear1_100000.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/bilinear2_100.py b/examples/performance/misc/bilinear2_100.py index 4dd9f9ead57..77b8737339d 100644 --- a/examples/performance/misc/bilinear2_100.py +++ b/examples/performance/misc/bilinear2_100.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/bilinear2_100000.py b/examples/performance/misc/bilinear2_100000.py index 90eeaf82271..7bf224f8b47 100644 --- a/examples/performance/misc/bilinear2_100000.py +++ b/examples/performance/misc/bilinear2_100000.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/diag1_100.py b/examples/performance/misc/diag1_100.py index e47a9179974..e92fc50201f 100644 --- a/examples/performance/misc/diag1_100.py +++ b/examples/performance/misc/diag1_100.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/diag1_100000.py b/examples/performance/misc/diag1_100000.py index a110c0d9d67..2bdfe99e749 100644 --- a/examples/performance/misc/diag1_100000.py +++ b/examples/performance/misc/diag1_100000.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/diag2_100.py b/examples/performance/misc/diag2_100.py index fe820e8590b..fe005eb74f1 100644 --- a/examples/performance/misc/diag2_100.py +++ b/examples/performance/misc/diag2_100.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/diag2_100000.py b/examples/performance/misc/diag2_100000.py index 38563de57b9..eca192b9679 100644 --- a/examples/performance/misc/diag2_100000.py +++ b/examples/performance/misc/diag2_100000.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * diff --git a/examples/performance/misc/set1.py b/examples/performance/misc/set1.py index 53227a3ee73..abf656ee350 100644 --- a/examples/performance/misc/set1.py +++ b/examples/performance/misc/set1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * model = ConcreteModel() diff --git a/examples/performance/misc/sparse1.py b/examples/performance/misc/sparse1.py index 264862760f9..0858f374248 100644 --- a/examples/performance/misc/sparse1.py +++ b/examples/performance/misc/sparse1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # This is a performance test that we cannot easily execute right now # diff --git a/examples/pyomo/concrete/rosen.py b/examples/pyomo/concrete/rosen.py index a8e8a175127..a8eb89081e8 100644 --- a/examples/pyomo/concrete/rosen.py +++ b/examples/pyomo/concrete/rosen.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # rosen.py from pyomo.environ import * diff --git a/examples/pyomo/concrete/sodacan.py b/examples/pyomo/concrete/sodacan.py index 3c0cfd3aab2..fddc8d5aa95 100644 --- a/examples/pyomo/concrete/sodacan.py +++ b/examples/pyomo/concrete/sodacan.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # sodacan.py from pyomo.environ import * from math import pi diff --git a/examples/pyomo/concrete/sodacan_fig.py b/examples/pyomo/concrete/sodacan_fig.py index bf9ae476b4c..ab33f522dfe 100644 --- a/examples/pyomo/concrete/sodacan_fig.py +++ b/examples/pyomo/concrete/sodacan_fig.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from mpl_toolkits.mplot3d import Axes3D from matplotlib import cm from matplotlib.ticker import LinearLocator, FormatStrFormatter diff --git a/examples/pyomo/concrete/sp.py b/examples/pyomo/concrete/sp.py index edc2d68b170..3a1b8aeef5a 100644 --- a/examples/pyomo/concrete/sp.py +++ b/examples/pyomo/concrete/sp.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # sp.py from pyomo.environ import * from sp_data import * # define c, b, h, and d diff --git a/examples/pyomo/concrete/sp_data.py b/examples/pyomo/concrete/sp_data.py index 58210126819..d65ae5a1d83 100644 --- a/examples/pyomo/concrete/sp_data.py +++ b/examples/pyomo/concrete/sp_data.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + c = 1.0 b = 1.5 h = 0.1 diff --git a/examples/pyomo/p-median/decorated_pmedian.py b/examples/pyomo/p-median/decorated_pmedian.py index 90345daf78d..be4cc5994be 100644 --- a/examples/pyomo/p-median/decorated_pmedian.py +++ b/examples/pyomo/p-median/decorated_pmedian.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * import random diff --git a/examples/pyomobook/__init__.py b/examples/pyomobook/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/examples/pyomobook/__init__.py +++ b/examples/pyomobook/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/examples/pyomobook/abstract-ch/AbstHLinScript.py b/examples/pyomobook/abstract-ch/AbstHLinScript.py index adf700bfd5c..48946e0fb3d 100644 --- a/examples/pyomobook/abstract-ch/AbstHLinScript.py +++ b/examples/pyomobook/abstract-ch/AbstHLinScript.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # AbstHLinScript.py - Script for a simple linear version of (H) import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/AbstractH.py b/examples/pyomobook/abstract-ch/AbstractH.py index da9f0a4931c..cda8b489f28 100644 --- a/examples/pyomobook/abstract-ch/AbstractH.py +++ b/examples/pyomobook/abstract-ch/AbstractH.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # AbstractH.py - Implement model (H) import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/AbstractHLinear.py b/examples/pyomobook/abstract-ch/AbstractHLinear.py index 575487d3e95..78ac4813709 100644 --- a/examples/pyomobook/abstract-ch/AbstractHLinear.py +++ b/examples/pyomobook/abstract-ch/AbstractHLinear.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # AbstractHLinear.py - A simple linear version of (H) import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/abstract5.py b/examples/pyomobook/abstract-ch/abstract5.py index 3a06256dff8..20abd31dbd6 100644 --- a/examples/pyomobook/abstract-ch/abstract5.py +++ b/examples/pyomobook/abstract-ch/abstract5.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # abstract5.py import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/abstract6.py b/examples/pyomobook/abstract-ch/abstract6.py index d11a4652f64..fdbbed88d25 100644 --- a/examples/pyomobook/abstract-ch/abstract6.py +++ b/examples/pyomobook/abstract-ch/abstract6.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # abstract6.py import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/abstract7.py b/examples/pyomobook/abstract-ch/abstract7.py index 2fd5d467d3e..21d264d53e1 100644 --- a/examples/pyomobook/abstract-ch/abstract7.py +++ b/examples/pyomobook/abstract-ch/abstract7.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # abstract7.py import pyomo.environ as pyo import pickle diff --git a/examples/pyomobook/abstract-ch/buildactions.py b/examples/pyomobook/abstract-ch/buildactions.py index ad918e2b5f2..a64de052176 100644 --- a/examples/pyomobook/abstract-ch/buildactions.py +++ b/examples/pyomobook/abstract-ch/buildactions.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # buildactions.py: Warehouse location problem showing build actions import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/concrete1.py b/examples/pyomobook/abstract-ch/concrete1.py index 0ad41c79ea3..d2d6d09ac4f 100644 --- a/examples/pyomobook/abstract-ch/concrete1.py +++ b/examples/pyomobook/abstract-ch/concrete1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/abstract-ch/concrete2.py b/examples/pyomobook/abstract-ch/concrete2.py index 6aee434d556..d0500df53fa 100644 --- a/examples/pyomobook/abstract-ch/concrete2.py +++ b/examples/pyomobook/abstract-ch/concrete2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/abstract-ch/diet1.py b/examples/pyomobook/abstract-ch/diet1.py index eb8b071cdb5..319bdec5144 100644 --- a/examples/pyomobook/abstract-ch/diet1.py +++ b/examples/pyomobook/abstract-ch/diet1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # diet1.py import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/ex.py b/examples/pyomobook/abstract-ch/ex.py index 88005b7dc0c..2309f3330a0 100644 --- a/examples/pyomobook/abstract-ch/ex.py +++ b/examples/pyomobook/abstract-ch/ex.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param1.py b/examples/pyomobook/abstract-ch/param1.py index fc9fac99ff4..f5f34838215 100644 --- a/examples/pyomobook/abstract-ch/param1.py +++ b/examples/pyomobook/abstract-ch/param1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param2.py b/examples/pyomobook/abstract-ch/param2.py index d51cbeffe84..ac3b5b8bd27 100644 --- a/examples/pyomobook/abstract-ch/param2.py +++ b/examples/pyomobook/abstract-ch/param2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param2a.py b/examples/pyomobook/abstract-ch/param2a.py index fe928eb4197..59f455bc290 100644 --- a/examples/pyomobook/abstract-ch/param2a.py +++ b/examples/pyomobook/abstract-ch/param2a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param3.py b/examples/pyomobook/abstract-ch/param3.py index 64efba5c5ad..5c3462f2e64 100644 --- a/examples/pyomobook/abstract-ch/param3.py +++ b/examples/pyomobook/abstract-ch/param3.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param3a.py b/examples/pyomobook/abstract-ch/param3a.py index 857d96f8318..25b575e3266 100644 --- a/examples/pyomobook/abstract-ch/param3a.py +++ b/examples/pyomobook/abstract-ch/param3a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param3b.py b/examples/pyomobook/abstract-ch/param3b.py index 655694c33dd..a4ad2d4ffc5 100644 --- a/examples/pyomobook/abstract-ch/param3b.py +++ b/examples/pyomobook/abstract-ch/param3b.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param3c.py b/examples/pyomobook/abstract-ch/param3c.py index 7d58b8b6a39..96e2f4e88a4 100644 --- a/examples/pyomobook/abstract-ch/param3c.py +++ b/examples/pyomobook/abstract-ch/param3c.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param4.py b/examples/pyomobook/abstract-ch/param4.py index c902b9034ad..4f427f44ee6 100644 --- a/examples/pyomobook/abstract-ch/param4.py +++ b/examples/pyomobook/abstract-ch/param4.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param5.py b/examples/pyomobook/abstract-ch/param5.py index 488e1debda8..6cdb46db30d 100644 --- a/examples/pyomobook/abstract-ch/param5.py +++ b/examples/pyomobook/abstract-ch/param5.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param5a.py b/examples/pyomobook/abstract-ch/param5a.py index 7e814b917cc..cd0187dabb1 100644 --- a/examples/pyomobook/abstract-ch/param5a.py +++ b/examples/pyomobook/abstract-ch/param5a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param6.py b/examples/pyomobook/abstract-ch/param6.py index d9c49a548b2..e4cbb40f984 100644 --- a/examples/pyomobook/abstract-ch/param6.py +++ b/examples/pyomobook/abstract-ch/param6.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param6a.py b/examples/pyomobook/abstract-ch/param6a.py index e9aca384ee6..c2995ee864c 100644 --- a/examples/pyomobook/abstract-ch/param6a.py +++ b/examples/pyomobook/abstract-ch/param6a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param7a.py b/examples/pyomobook/abstract-ch/param7a.py index 2a18cceabf6..3ed9163daec 100644 --- a/examples/pyomobook/abstract-ch/param7a.py +++ b/examples/pyomobook/abstract-ch/param7a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param7b.py b/examples/pyomobook/abstract-ch/param7b.py index acf02ddd62f..59f5e28f979 100644 --- a/examples/pyomobook/abstract-ch/param7b.py +++ b/examples/pyomobook/abstract-ch/param7b.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/param8a.py b/examples/pyomobook/abstract-ch/param8a.py index e68378961ed..2e57f9c3bb7 100644 --- a/examples/pyomobook/abstract-ch/param8a.py +++ b/examples/pyomobook/abstract-ch/param8a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/postprocess_fn.py b/examples/pyomobook/abstract-ch/postprocess_fn.py index f96a5b4dac1..b54c11b5a0e 100644 --- a/examples/pyomobook/abstract-ch/postprocess_fn.py +++ b/examples/pyomobook/abstract-ch/postprocess_fn.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import csv diff --git a/examples/pyomobook/abstract-ch/set1.py b/examples/pyomobook/abstract-ch/set1.py index ee281bd10bd..6c549f61c49 100644 --- a/examples/pyomobook/abstract-ch/set1.py +++ b/examples/pyomobook/abstract-ch/set1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/set2.py b/examples/pyomobook/abstract-ch/set2.py index 27af609cead..bd7f98d5174 100644 --- a/examples/pyomobook/abstract-ch/set2.py +++ b/examples/pyomobook/abstract-ch/set2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/set2a.py b/examples/pyomobook/abstract-ch/set2a.py index bf8f06dd7a8..e6960396dd7 100644 --- a/examples/pyomobook/abstract-ch/set2a.py +++ b/examples/pyomobook/abstract-ch/set2a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/set3.py b/examples/pyomobook/abstract-ch/set3.py index 7661963d19d..4a3a27aa342 100644 --- a/examples/pyomobook/abstract-ch/set3.py +++ b/examples/pyomobook/abstract-ch/set3.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/set4.py b/examples/pyomobook/abstract-ch/set4.py index c9125dad657..7d782cb268e 100644 --- a/examples/pyomobook/abstract-ch/set4.py +++ b/examples/pyomobook/abstract-ch/set4.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/set5.py b/examples/pyomobook/abstract-ch/set5.py index 9f79870d3ff..7478316897a 100644 --- a/examples/pyomobook/abstract-ch/set5.py +++ b/examples/pyomobook/abstract-ch/set5.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/abstract-ch/wl_abstract.py b/examples/pyomobook/abstract-ch/wl_abstract.py index f35a5327bfb..361729a1eff 100644 --- a/examples/pyomobook/abstract-ch/wl_abstract.py +++ b/examples/pyomobook/abstract-ch/wl_abstract.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_abstract.py: AbstractModel version of warehouse location determination problem import pyomo.environ as pyo diff --git a/examples/pyomobook/abstract-ch/wl_abstract_script.py b/examples/pyomobook/abstract-ch/wl_abstract_script.py index 0b042405714..b70c6dbb8d2 100644 --- a/examples/pyomobook/abstract-ch/wl_abstract_script.py +++ b/examples/pyomobook/abstract-ch/wl_abstract_script.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_abstract_script.py: Scripting using an AbstractModel import pyomo.environ as pyo diff --git a/examples/pyomobook/blocks-ch/blocks_gen.py b/examples/pyomobook/blocks-ch/blocks_gen.py index 109e881cad5..7a74986ed81 100644 --- a/examples/pyomobook/blocks-ch/blocks_gen.py +++ b/examples/pyomobook/blocks-ch/blocks_gen.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo time = range(5) diff --git a/examples/pyomobook/blocks-ch/blocks_intro.py b/examples/pyomobook/blocks-ch/blocks_intro.py index ad3ceaa4349..3160c29b385 100644 --- a/examples/pyomobook/blocks-ch/blocks_intro.py +++ b/examples/pyomobook/blocks-ch/blocks_intro.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo # @hierarchy: diff --git a/examples/pyomobook/blocks-ch/blocks_lotsizing.py b/examples/pyomobook/blocks-ch/blocks_lotsizing.py index fe0717d8c7c..897ba9a4e5c 100644 --- a/examples/pyomobook/blocks-ch/blocks_lotsizing.py +++ b/examples/pyomobook/blocks-ch/blocks_lotsizing.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/blocks-ch/lotsizing.py b/examples/pyomobook/blocks-ch/lotsizing.py index 47ea265246e..766c1892111 100644 --- a/examples/pyomobook/blocks-ch/lotsizing.py +++ b/examples/pyomobook/blocks-ch/lotsizing.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/blocks-ch/lotsizing_no_time.py b/examples/pyomobook/blocks-ch/lotsizing_no_time.py index 901467a0cbb..e0fa69922c1 100644 --- a/examples/pyomobook/blocks-ch/lotsizing_no_time.py +++ b/examples/pyomobook/blocks-ch/lotsizing_no_time.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/blocks-ch/lotsizing_uncertain.py b/examples/pyomobook/blocks-ch/lotsizing_uncertain.py index 6d16de7e3a7..9870d195841 100644 --- a/examples/pyomobook/blocks-ch/lotsizing_uncertain.py +++ b/examples/pyomobook/blocks-ch/lotsizing_uncertain.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/dae-ch/dae_tester_model.py b/examples/pyomobook/dae-ch/dae_tester_model.py index 9e0da9f4a62..00d51e8e05d 100644 --- a/examples/pyomobook/dae-ch/dae_tester_model.py +++ b/examples/pyomobook/dae-ch/dae_tester_model.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # This is a file for testing miscellaneous code snippets from the DAE chapter import pyomo.environ as pyo import pyomo.dae as dae diff --git a/examples/pyomobook/dae-ch/plot_path_constraint.py b/examples/pyomobook/dae-ch/plot_path_constraint.py index 4c04bc1b6b6..d1af5c617ff 100644 --- a/examples/pyomobook/dae-ch/plot_path_constraint.py +++ b/examples/pyomobook/dae-ch/plot_path_constraint.py @@ -1,3 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + # @plot_path: def plotter(subplot, x, *y, **kwds): plt.subplot(subplot) diff --git a/examples/pyomobook/dae-ch/run_path_constraint.py b/examples/pyomobook/dae-ch/run_path_constraint.py index b819d6a7127..d4345e9e424 100644 --- a/examples/pyomobook/dae-ch/run_path_constraint.py +++ b/examples/pyomobook/dae-ch/run_path_constraint.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from pyomo.dae import * from path_constraint import m diff --git a/examples/pyomobook/dae-ch/run_path_constraint_tester.py b/examples/pyomobook/dae-ch/run_path_constraint_tester.py index bbcd83f5da5..d71c5126609 100644 --- a/examples/pyomobook/dae-ch/run_path_constraint_tester.py +++ b/examples/pyomobook/dae-ch/run_path_constraint_tester.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.tee import capture_output from six import StringIO diff --git a/examples/pyomobook/gdp-ch/gdp_uc.py b/examples/pyomobook/gdp-ch/gdp_uc.py index 2495ed9bef1..9f2562efad0 100644 --- a/examples/pyomobook/gdp-ch/gdp_uc.py +++ b/examples/pyomobook/gdp-ch/gdp_uc.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # gdp_uc.py import pyomo.environ as pyo from pyomo.gdp import * diff --git a/examples/pyomobook/gdp-ch/scont.py b/examples/pyomobook/gdp-ch/scont.py index 76597326700..99beb042728 100644 --- a/examples/pyomobook/gdp-ch/scont.py +++ b/examples/pyomobook/gdp-ch/scont.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # scont.py import pyomo.environ as pyo from pyomo.gdp import Disjunct, Disjunction diff --git a/examples/pyomobook/gdp-ch/scont2.py b/examples/pyomobook/gdp-ch/scont2.py index 94e510b358a..cf392441487 100644 --- a/examples/pyomobook/gdp-ch/scont2.py +++ b/examples/pyomobook/gdp-ch/scont2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo import scont diff --git a/examples/pyomobook/gdp-ch/scont_script.py b/examples/pyomobook/gdp-ch/scont_script.py index 22c9b88ad0c..fee14bedaac 100644 --- a/examples/pyomobook/gdp-ch/scont_script.py +++ b/examples/pyomobook/gdp-ch/scont_script.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo import scont diff --git a/examples/pyomobook/gdp-ch/verify_scont.py b/examples/pyomobook/gdp-ch/verify_scont.py index db44024fe66..a0acd3cf376 100644 --- a/examples/pyomobook/gdp-ch/verify_scont.py +++ b/examples/pyomobook/gdp-ch/verify_scont.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import os diff --git a/examples/pyomobook/intro-ch/abstract5.py b/examples/pyomobook/intro-ch/abstract5.py index 2184ed7b3aa..b273d49b2ea 100644 --- a/examples/pyomobook/intro-ch/abstract5.py +++ b/examples/pyomobook/intro-ch/abstract5.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/intro-ch/coloring_concrete.py b/examples/pyomobook/intro-ch/coloring_concrete.py index 107a31668c4..5b4baca99af 100644 --- a/examples/pyomobook/intro-ch/coloring_concrete.py +++ b/examples/pyomobook/intro-ch/coloring_concrete.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # Graph coloring example adapted from # diff --git a/examples/pyomobook/intro-ch/concrete1.py b/examples/pyomobook/intro-ch/concrete1.py index a39ca1d41cd..c7aea6ff0b6 100644 --- a/examples/pyomobook/intro-ch/concrete1.py +++ b/examples/pyomobook/intro-ch/concrete1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/intro-ch/concrete1_generic.py b/examples/pyomobook/intro-ch/concrete1_generic.py index de648470469..183eb480fa1 100644 --- a/examples/pyomobook/intro-ch/concrete1_generic.py +++ b/examples/pyomobook/intro-ch/concrete1_generic.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo import mydata diff --git a/examples/pyomobook/intro-ch/mydata.py b/examples/pyomobook/intro-ch/mydata.py index 83aa26bacd9..aaf8ec3d8be 100644 --- a/examples/pyomobook/intro-ch/mydata.py +++ b/examples/pyomobook/intro-ch/mydata.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + N = [1, 2] M = [1, 2] c = {1: 1, 2: 2} diff --git a/examples/pyomobook/mpec-ch/ex1a.py b/examples/pyomobook/mpec-ch/ex1a.py index 30cd2842556..a57e714cd1c 100644 --- a/examples/pyomobook/mpec-ch/ex1a.py +++ b/examples/pyomobook/mpec-ch/ex1a.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ex1a.py import pyomo.environ as pyo from pyomo.mpec import Complementarity, complements diff --git a/examples/pyomobook/mpec-ch/ex1b.py b/examples/pyomobook/mpec-ch/ex1b.py index 9592c81c4f6..37a658f5294 100644 --- a/examples/pyomobook/mpec-ch/ex1b.py +++ b/examples/pyomobook/mpec-ch/ex1b.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ex1b.py import pyomo.environ as pyo from pyomo.mpec import ComplementarityList, complements diff --git a/examples/pyomobook/mpec-ch/ex1c.py b/examples/pyomobook/mpec-ch/ex1c.py index aad9c9b0d47..35c0be9345d 100644 --- a/examples/pyomobook/mpec-ch/ex1c.py +++ b/examples/pyomobook/mpec-ch/ex1c.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ex1c.py import pyomo.environ as pyo from pyomo.mpec import ComplementarityList, complements diff --git a/examples/pyomobook/mpec-ch/ex1d.py b/examples/pyomobook/mpec-ch/ex1d.py index fa5247ff831..05105df265c 100644 --- a/examples/pyomobook/mpec-ch/ex1d.py +++ b/examples/pyomobook/mpec-ch/ex1d.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ex1d.py import pyomo.environ as pyo from pyomo.mpec import Complementarity, complements diff --git a/examples/pyomobook/mpec-ch/ex1e.py b/examples/pyomobook/mpec-ch/ex1e.py index bf714411396..66831a58255 100644 --- a/examples/pyomobook/mpec-ch/ex1e.py +++ b/examples/pyomobook/mpec-ch/ex1e.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ex1e.py import pyomo.environ as pyo from pyomo.mpec import ComplementarityList, complements diff --git a/examples/pyomobook/mpec-ch/ex2.py b/examples/pyomobook/mpec-ch/ex2.py index c192ccc7a34..69d3813432d 100644 --- a/examples/pyomobook/mpec-ch/ex2.py +++ b/examples/pyomobook/mpec-ch/ex2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ex2.py import pyomo.environ as pyo from pyomo.mpec import * diff --git a/examples/pyomobook/mpec-ch/munson1.py b/examples/pyomobook/mpec-ch/munson1.py index c7d171eb416..1c73c6279af 100644 --- a/examples/pyomobook/mpec-ch/munson1.py +++ b/examples/pyomobook/mpec-ch/munson1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # munson1.py import pyomo.environ as pyo from pyomo.mpec import Complementarity, complements diff --git a/examples/pyomobook/mpec-ch/ralph1.py b/examples/pyomobook/mpec-ch/ralph1.py index 1d44a303b84..38ee803b1f1 100644 --- a/examples/pyomobook/mpec-ch/ralph1.py +++ b/examples/pyomobook/mpec-ch/ralph1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ralph1.py import pyomo.environ as pyo from pyomo.mpec import Complementarity, complements diff --git a/examples/pyomobook/nonlinear-ch/deer/DeerProblem.py b/examples/pyomobook/nonlinear-ch/deer/DeerProblem.py index c076a7f4687..574a92ed0a2 100644 --- a/examples/pyomobook/nonlinear-ch/deer/DeerProblem.py +++ b/examples/pyomobook/nonlinear-ch/deer/DeerProblem.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # DeerProblem.py import pyomo.environ as pyo diff --git a/examples/pyomobook/nonlinear-ch/disease_est/disease_estimation.py b/examples/pyomobook/nonlinear-ch/disease_est/disease_estimation.py index 4eb859dc349..4b805b9cf7f 100644 --- a/examples/pyomobook/nonlinear-ch/disease_est/disease_estimation.py +++ b/examples/pyomobook/nonlinear-ch/disease_est/disease_estimation.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # disease_estimation.py import pyomo.environ as pyo diff --git a/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init1.py b/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init1.py index c435cafc3d5..6cebe59a612 100644 --- a/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init1.py +++ b/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # multimodal_init1.py import pyomo.environ as pyo from math import pi diff --git a/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init2.py b/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init2.py index aa0dbae1e66..a2c9d9c5a60 100644 --- a/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init2.py +++ b/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from math import pi diff --git a/examples/pyomobook/nonlinear-ch/react_design/ReactorDesign.py b/examples/pyomobook/nonlinear-ch/react_design/ReactorDesign.py index 90822c153a5..c3115f396ce 100644 --- a/examples/pyomobook/nonlinear-ch/react_design/ReactorDesign.py +++ b/examples/pyomobook/nonlinear-ch/react_design/ReactorDesign.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ import pyomo.environ as pyo diff --git a/examples/pyomobook/nonlinear-ch/react_design/ReactorDesignTable.py b/examples/pyomobook/nonlinear-ch/react_design/ReactorDesignTable.py index a242c85fbc2..c748cd7d41e 100644 --- a/examples/pyomobook/nonlinear-ch/react_design/ReactorDesignTable.py +++ b/examples/pyomobook/nonlinear-ch/react_design/ReactorDesignTable.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from ReactorDesign import create_model diff --git a/examples/pyomobook/nonlinear-ch/rosen/rosenbrock.py b/examples/pyomobook/nonlinear-ch/rosen/rosenbrock.py index e1633e2df69..3d14d15aa93 100644 --- a/examples/pyomobook/nonlinear-ch/rosen/rosenbrock.py +++ b/examples/pyomobook/nonlinear-ch/rosen/rosenbrock.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # rosenbrock.py # A Pyomo model for the Rosenbrock problem import pyomo.environ as pyo diff --git a/examples/pyomobook/optimization-ch/ConcHLinScript.py b/examples/pyomobook/optimization-ch/ConcHLinScript.py index 8481a83afbf..f4f5fac6b6c 100644 --- a/examples/pyomobook/optimization-ch/ConcHLinScript.py +++ b/examples/pyomobook/optimization-ch/ConcHLinScript.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ConcHLinScript.py - Linear (H) as a script import pyomo.environ as pyo diff --git a/examples/pyomobook/optimization-ch/ConcreteH.py b/examples/pyomobook/optimization-ch/ConcreteH.py index 1bf2a9446c1..6cb3f7c5052 100644 --- a/examples/pyomobook/optimization-ch/ConcreteH.py +++ b/examples/pyomobook/optimization-ch/ConcreteH.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ConcreteH.py - Implement a particular instance of (H) # @fct: diff --git a/examples/pyomobook/optimization-ch/ConcreteHLinear.py b/examples/pyomobook/optimization-ch/ConcreteHLinear.py index 0b42d5e2187..3cc7478f1c9 100644 --- a/examples/pyomobook/optimization-ch/ConcreteHLinear.py +++ b/examples/pyomobook/optimization-ch/ConcreteHLinear.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ConcreteHLinear.py - Linear (H) import pyomo.environ as pyo diff --git a/examples/pyomobook/optimization-ch/IC_model_dict.py b/examples/pyomobook/optimization-ch/IC_model_dict.py index 4c54ef83701..a76f19797af 100644 --- a/examples/pyomobook/optimization-ch/IC_model_dict.py +++ b/examples/pyomobook/optimization-ch/IC_model_dict.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # IC_model_dict.py - Implement a particular instance of (H) # @fct: diff --git a/examples/pyomobook/overview-ch/var_obj_con_snippet.py b/examples/pyomobook/overview-ch/var_obj_con_snippet.py index 49bb7c1276b..e979e4b18de 100644 --- a/examples/pyomobook/overview-ch/var_obj_con_snippet.py +++ b/examples/pyomobook/overview-ch/var_obj_con_snippet.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/overview-ch/wl_abstract.py b/examples/pyomobook/overview-ch/wl_abstract.py index f35a5327bfb..361729a1eff 100644 --- a/examples/pyomobook/overview-ch/wl_abstract.py +++ b/examples/pyomobook/overview-ch/wl_abstract.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_abstract.py: AbstractModel version of warehouse location determination problem import pyomo.environ as pyo diff --git a/examples/pyomobook/overview-ch/wl_abstract_script.py b/examples/pyomobook/overview-ch/wl_abstract_script.py index 0b042405714..b70c6dbb8d2 100644 --- a/examples/pyomobook/overview-ch/wl_abstract_script.py +++ b/examples/pyomobook/overview-ch/wl_abstract_script.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_abstract_script.py: Scripting using an AbstractModel import pyomo.environ as pyo diff --git a/examples/pyomobook/overview-ch/wl_concrete.py b/examples/pyomobook/overview-ch/wl_concrete.py index 29316304f0a..da32c7ba5bf 100644 --- a/examples/pyomobook/overview-ch/wl_concrete.py +++ b/examples/pyomobook/overview-ch/wl_concrete.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_concrete.py # ConcreteModel version of warehouse location problem import pyomo.environ as pyo diff --git a/examples/pyomobook/overview-ch/wl_concrete_script.py b/examples/pyomobook/overview-ch/wl_concrete_script.py index 278937f5aed..59baa241718 100644 --- a/examples/pyomobook/overview-ch/wl_concrete_script.py +++ b/examples/pyomobook/overview-ch/wl_concrete_script.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_concrete_script.py # Solve an instance of the warehouse location problem diff --git a/examples/pyomobook/overview-ch/wl_excel.py b/examples/pyomobook/overview-ch/wl_excel.py index 1c4ad997225..777412abb23 100644 --- a/examples/pyomobook/overview-ch/wl_excel.py +++ b/examples/pyomobook/overview-ch/wl_excel.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_excel.py: Loading Excel data using Pandas import pandas import pyomo.environ as pyo diff --git a/examples/pyomobook/overview-ch/wl_list.py b/examples/pyomobook/overview-ch/wl_list.py index 64db76be548..375a1c7400e 100644 --- a/examples/pyomobook/overview-ch/wl_list.py +++ b/examples/pyomobook/overview-ch/wl_list.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_list.py: Warehouse location problem using constraint lists import pyomo.environ as pyo diff --git a/examples/pyomobook/overview-ch/wl_mutable.py b/examples/pyomobook/overview-ch/wl_mutable.py index e5c4f5e9dbb..1b65dcc84a1 100644 --- a/examples/pyomobook/overview-ch/wl_mutable.py +++ b/examples/pyomobook/overview-ch/wl_mutable.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_mutable.py: warehouse location problem with mutable param import pyomo.environ as pyo diff --git a/examples/pyomobook/overview-ch/wl_mutable_excel.py b/examples/pyomobook/overview-ch/wl_mutable_excel.py index 0906fbb25b3..52cac31f5f6 100644 --- a/examples/pyomobook/overview-ch/wl_mutable_excel.py +++ b/examples/pyomobook/overview-ch/wl_mutable_excel.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_mutable_excel.py: solve problem with different values for P import pandas import pyomo.environ as pyo diff --git a/examples/pyomobook/overview-ch/wl_scalar.py b/examples/pyomobook/overview-ch/wl_scalar.py index ac10fbe8265..b524f22c82d 100644 --- a/examples/pyomobook/overview-ch/wl_scalar.py +++ b/examples/pyomobook/overview-ch/wl_scalar.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl_scalar.py: snippets that show the warehouse location problem implemented as scalar quantities import pyomo.environ as pyo diff --git a/examples/pyomobook/performance-ch/SparseSets.py b/examples/pyomobook/performance-ch/SparseSets.py index 90d097b53aa..913b7587368 100644 --- a/examples/pyomobook/performance-ch/SparseSets.py +++ b/examples/pyomobook/performance-ch/SparseSets.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/performance-ch/lin_expr.py b/examples/pyomobook/performance-ch/lin_expr.py index 75f4e70ec2a..20585d4719b 100644 --- a/examples/pyomobook/performance-ch/lin_expr.py +++ b/examples/pyomobook/performance-ch/lin_expr.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from pyomo.common.timing import TicTocTimer from pyomo.core.expr.numeric_expr import LinearExpression diff --git a/examples/pyomobook/performance-ch/persistent.py b/examples/pyomobook/performance-ch/persistent.py index 98207909cb6..e468b281579 100644 --- a/examples/pyomobook/performance-ch/persistent.py +++ b/examples/pyomobook/performance-ch/persistent.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @model: import pyomo.environ as pyo diff --git a/examples/pyomobook/performance-ch/wl.py b/examples/pyomobook/performance-ch/wl.py index 34c8a73f36e..614ffc0fd66 100644 --- a/examples/pyomobook/performance-ch/wl.py +++ b/examples/pyomobook/performance-ch/wl.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # wl.py # define a script to demonstrate performance profiling and improvements # @imports: import pyomo.environ as pyo # import pyomo environment diff --git a/examples/pyomobook/pyomo-components-ch/con_declaration.py b/examples/pyomobook/pyomo-components-ch/con_declaration.py index 7775c1b26a0..b014697fd62 100644 --- a/examples/pyomobook/pyomo-components-ch/con_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/con_declaration.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/pyomo-components-ch/examples.py b/examples/pyomobook/pyomo-components-ch/examples.py index 6ba96792e28..5f154c0ecc9 100644 --- a/examples/pyomobook/pyomo-components-ch/examples.py +++ b/examples/pyomobook/pyomo-components-ch/examples.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo print("indexed1") diff --git a/examples/pyomobook/pyomo-components-ch/expr_declaration.py b/examples/pyomobook/pyomo-components-ch/expr_declaration.py index 8974a4d406a..9baff1e4dba 100644 --- a/examples/pyomobook/pyomo-components-ch/expr_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/expr_declaration.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/pyomo-components-ch/obj_declaration.py b/examples/pyomobook/pyomo-components-ch/obj_declaration.py index 2c26c2b3363..ac8b56a3a03 100644 --- a/examples/pyomobook/pyomo-components-ch/obj_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/obj_declaration.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/pyomo-components-ch/param_declaration.py b/examples/pyomobook/pyomo-components-ch/param_declaration.py index a9d3256abfe..ded0adfcb22 100644 --- a/examples/pyomobook/pyomo-components-ch/param_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/param_declaration.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/pyomo-components-ch/param_initialization.py b/examples/pyomobook/pyomo-components-ch/param_initialization.py index 11c257d2c31..e9a90210df5 100644 --- a/examples/pyomobook/pyomo-components-ch/param_initialization.py +++ b/examples/pyomobook/pyomo-components-ch/param_initialization.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/pyomo-components-ch/param_misc.py b/examples/pyomobook/pyomo-components-ch/param_misc.py index baf76cc7c03..cc3be7a6ac5 100644 --- a/examples/pyomobook/pyomo-components-ch/param_misc.py +++ b/examples/pyomobook/pyomo-components-ch/param_misc.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo # @mutable1: diff --git a/examples/pyomobook/pyomo-components-ch/param_validation.py b/examples/pyomobook/pyomo-components-ch/param_validation.py index c82657c8d0f..cf540ac8a70 100644 --- a/examples/pyomobook/pyomo-components-ch/param_validation.py +++ b/examples/pyomobook/pyomo-components-ch/param_validation.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/pyomo-components-ch/rangeset.py b/examples/pyomobook/pyomo-components-ch/rangeset.py index d5e1015064c..a5ef4a85017 100644 --- a/examples/pyomobook/pyomo-components-ch/rangeset.py +++ b/examples/pyomobook/pyomo-components-ch/rangeset.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/pyomo-components-ch/set_declaration.py b/examples/pyomobook/pyomo-components-ch/set_declaration.py index 1a507d4f588..a60904ff510 100644 --- a/examples/pyomobook/pyomo-components-ch/set_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/set_declaration.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/pyomo-components-ch/set_initialization.py b/examples/pyomobook/pyomo-components-ch/set_initialization.py index 89dbaa713db..972d65e0499 100644 --- a/examples/pyomobook/pyomo-components-ch/set_initialization.py +++ b/examples/pyomobook/pyomo-components-ch/set_initialization.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/pyomo-components-ch/set_misc.py b/examples/pyomobook/pyomo-components-ch/set_misc.py index 9a795b196b8..2bd8297cc80 100644 --- a/examples/pyomobook/pyomo-components-ch/set_misc.py +++ b/examples/pyomobook/pyomo-components-ch/set_misc.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/pyomo-components-ch/set_options.py b/examples/pyomobook/pyomo-components-ch/set_options.py index 8d49882de2f..27c47ee95c7 100644 --- a/examples/pyomobook/pyomo-components-ch/set_options.py +++ b/examples/pyomobook/pyomo-components-ch/set_options.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/pyomo-components-ch/set_validation.py b/examples/pyomobook/pyomo-components-ch/set_validation.py index a55dfc9ab7c..3b6b8bee25b 100644 --- a/examples/pyomobook/pyomo-components-ch/set_validation.py +++ b/examples/pyomobook/pyomo-components-ch/set_validation.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.AbstractModel() diff --git a/examples/pyomobook/pyomo-components-ch/suffix_declaration.py b/examples/pyomobook/pyomo-components-ch/suffix_declaration.py index 650669ef5a6..a5c0bc988bb 100644 --- a/examples/pyomobook/pyomo-components-ch/suffix_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/suffix_declaration.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo print('') diff --git a/examples/pyomobook/pyomo-components-ch/var_declaration.py b/examples/pyomobook/pyomo-components-ch/var_declaration.py index 60d3b00756a..b3180f25381 100644 --- a/examples/pyomobook/pyomo-components-ch/var_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/var_declaration.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/python-ch/BadIndent.py b/examples/pyomobook/python-ch/BadIndent.py index 6ab545a6f46..63013067468 100644 --- a/examples/pyomobook/python-ch/BadIndent.py +++ b/examples/pyomobook/python-ch/BadIndent.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # This comment is the first line of BadIndent.py, # which will cause Python to give an error message # concerning indentation. diff --git a/examples/pyomobook/python-ch/LineExample.py b/examples/pyomobook/python-ch/LineExample.py index 0109a64167e..320289a2a79 100644 --- a/examples/pyomobook/python-ch/LineExample.py +++ b/examples/pyomobook/python-ch/LineExample.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # This comment is the first line of LineExample.py # all characters on a line after the #-character are # ignored by Python diff --git a/examples/pyomobook/python-ch/class.py b/examples/pyomobook/python-ch/class.py index 562cef07ea7..12eafe23a44 100644 --- a/examples/pyomobook/python-ch/class.py +++ b/examples/pyomobook/python-ch/class.py @@ -1,25 +1,36 @@ -# class.py - - -# @all: -class IntLocker: - sint = None - - def __init__(self, i): - self.set_value(i) - - def set_value(self, i): - if type(i) is not int: - print("Error: %d is not integer." % i) - else: - self.sint = i - - def pprint(self): - print("The Int Locker has " + str(self.sint)) - - -a = IntLocker(3) -a.pprint() # prints: The Int Locker has 3 -a.set_value(5) -a.pprint() # prints: The Int Locker has 5 -# @:all +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# class.py + + +# @all: +class IntLocker: + sint = None + + def __init__(self, i): + self.set_value(i) + + def set_value(self, i): + if type(i) is not int: + print("Error: %d is not integer." % i) + else: + self.sint = i + + def pprint(self): + print("The Int Locker has " + str(self.sint)) + + +a = IntLocker(3) +a.pprint() # prints: The Int Locker has 3 +a.set_value(5) +a.pprint() # prints: The Int Locker has 5 +# @:all diff --git a/examples/pyomobook/python-ch/ctob.py b/examples/pyomobook/python-ch/ctob.py index e418d27f103..fe2c474de4d 100644 --- a/examples/pyomobook/python-ch/ctob.py +++ b/examples/pyomobook/python-ch/ctob.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # An example of a silly decorator to change 'c' to 'b' # in the return value of a function. diff --git a/examples/pyomobook/python-ch/example.py b/examples/pyomobook/python-ch/example.py index 0a404add58d..2bab6d4b9fe 100644 --- a/examples/pyomobook/python-ch/example.py +++ b/examples/pyomobook/python-ch/example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # This is a comment line, which is ignored by Python print("Hello World") diff --git a/examples/pyomobook/python-ch/example2.py b/examples/pyomobook/python-ch/example2.py index da7d14e24ae..0c282eccacd 100644 --- a/examples/pyomobook/python-ch/example2.py +++ b/examples/pyomobook/python-ch/example2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # A modified example.py program print("Hello World") diff --git a/examples/pyomobook/python-ch/functions.py b/examples/pyomobook/python-ch/functions.py index 7948c5e55df..b23b6dc6bee 100644 --- a/examples/pyomobook/python-ch/functions.py +++ b/examples/pyomobook/python-ch/functions.py @@ -1,24 +1,35 @@ -# functions.py - - -# @all: -def Apply(f, a): - r = [] - for i in range(len(a)): - r.append(f(a[i])) - return r - - -def SqifOdd(x): - # if x is odd, 2*int(x/2) is not x - # due to integer divide of x/2 - if 2 * int(x / 2) == x: - return x - else: - return x * x - - -ShortList = range(4) -B = Apply(SqifOdd, ShortList) -print(B) -# @:all +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# functions.py + + +# @all: +def Apply(f, a): + r = [] + for i in range(len(a)): + r.append(f(a[i])) + return r + + +def SqifOdd(x): + # if x is odd, 2*int(x/2) is not x + # due to integer divide of x/2 + if 2 * int(x / 2) == x: + return x + else: + return x * x + + +ShortList = range(4) +B = Apply(SqifOdd, ShortList) +print(B) +# @:all diff --git a/examples/pyomobook/python-ch/iterate.py b/examples/pyomobook/python-ch/iterate.py index 3a3422b2a09..cd8fe697afb 100644 --- a/examples/pyomobook/python-ch/iterate.py +++ b/examples/pyomobook/python-ch/iterate.py @@ -1,18 +1,29 @@ -# iterate.py - -# @all: -D = {'Mary': 231} -D['Bob'] = 123 -D['Alice'] = 331 -D['Ted'] = 987 - -for i in sorted(D): - if i == 'Alice': - continue - if i == 'John': - print("Loop ends. Cleese alert!") - break - print(i + " " + str(D[i])) -else: - print("Cleese is not in the list.") -# @:all +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# iterate.py + +# @all: +D = {'Mary': 231} +D['Bob'] = 123 +D['Alice'] = 331 +D['Ted'] = 987 + +for i in sorted(D): + if i == 'Alice': + continue + if i == 'John': + print("Loop ends. Cleese alert!") + break + print(i + " " + str(D[i])) +else: + print("Cleese is not in the list.") +# @:all diff --git a/examples/pyomobook/python-ch/pythonconditional.py b/examples/pyomobook/python-ch/pythonconditional.py index 205428e5ad1..2c48a2db6f4 100644 --- a/examples/pyomobook/python-ch/pythonconditional.py +++ b/examples/pyomobook/python-ch/pythonconditional.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # pythonconditional.py # @all: diff --git a/examples/pyomobook/scripts-ch/attributes.py b/examples/pyomobook/scripts-ch/attributes.py index 643162082b6..fccdb6932da 100644 --- a/examples/pyomobook/scripts-ch/attributes.py +++ b/examples/pyomobook/scripts-ch/attributes.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import json import pyomo.environ as pyo from warehouse_model import create_wl_model diff --git a/examples/pyomobook/scripts-ch/prob_mod_ex.py b/examples/pyomobook/scripts-ch/prob_mod_ex.py index 6d610e9b44a..f94fec5eb8a 100644 --- a/examples/pyomobook/scripts-ch/prob_mod_ex.py +++ b/examples/pyomobook/scripts-ch/prob_mod_ex.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/scripts-ch/sudoku/sudoku.py b/examples/pyomobook/scripts-ch/sudoku/sudoku.py index ea0c0044e1d..ac6d1eabf14 100644 --- a/examples/pyomobook/scripts-ch/sudoku/sudoku.py +++ b/examples/pyomobook/scripts-ch/sudoku/sudoku.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo # create a standard python dict for mapping subsquares to diff --git a/examples/pyomobook/scripts-ch/sudoku/sudoku_run.py b/examples/pyomobook/scripts-ch/sudoku/sudoku_run.py index 266362308fa..948c5a59ee8 100644 --- a/examples/pyomobook/scripts-ch/sudoku/sudoku_run.py +++ b/examples/pyomobook/scripts-ch/sudoku/sudoku_run.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.opt import SolverFactory, TerminationCondition from sudoku import create_sudoku_model, print_solution, add_integer_cut diff --git a/examples/pyomobook/scripts-ch/value_expression.py b/examples/pyomobook/scripts-ch/value_expression.py index 51c07500ea8..ca154341b43 100644 --- a/examples/pyomobook/scripts-ch/value_expression.py +++ b/examples/pyomobook/scripts-ch/value_expression.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo model = pyo.ConcreteModel() diff --git a/examples/pyomobook/scripts-ch/warehouse_cuts.py b/examples/pyomobook/scripts-ch/warehouse_cuts.py index c6516e796af..82dabfcb6f8 100644 --- a/examples/pyomobook/scripts-ch/warehouse_cuts.py +++ b/examples/pyomobook/scripts-ch/warehouse_cuts.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import warnings warnings.filterwarnings("ignore") diff --git a/examples/pyomobook/scripts-ch/warehouse_load_solutions.py b/examples/pyomobook/scripts-ch/warehouse_load_solutions.py index 790333a0e64..4d47a8ab916 100644 --- a/examples/pyomobook/scripts-ch/warehouse_load_solutions.py +++ b/examples/pyomobook/scripts-ch/warehouse_load_solutions.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import json import pyomo.environ as pyo from warehouse_model import create_wl_model diff --git a/examples/pyomobook/scripts-ch/warehouse_model.py b/examples/pyomobook/scripts-ch/warehouse_model.py index f5983d3cd89..cb9a43563fb 100644 --- a/examples/pyomobook/scripts-ch/warehouse_model.py +++ b/examples/pyomobook/scripts-ch/warehouse_model.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo diff --git a/examples/pyomobook/scripts-ch/warehouse_print.py b/examples/pyomobook/scripts-ch/warehouse_print.py index e0e2f961345..2353a8d6b44 100644 --- a/examples/pyomobook/scripts-ch/warehouse_print.py +++ b/examples/pyomobook/scripts-ch/warehouse_print.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import json import pyomo.environ as pyo from warehouse_model import create_wl_model diff --git a/examples/pyomobook/scripts-ch/warehouse_script.py b/examples/pyomobook/scripts-ch/warehouse_script.py index f2635a45d3d..37d71b466d2 100644 --- a/examples/pyomobook/scripts-ch/warehouse_script.py +++ b/examples/pyomobook/scripts-ch/warehouse_script.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @script: import json import pyomo.environ as pyo diff --git a/examples/pyomobook/scripts-ch/warehouse_solver_options.py b/examples/pyomobook/scripts-ch/warehouse_solver_options.py index c8eaf11a0f3..5a482bf3216 100644 --- a/examples/pyomobook/scripts-ch/warehouse_solver_options.py +++ b/examples/pyomobook/scripts-ch/warehouse_solver_options.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # @script: import json import pyomo.environ as pyo diff --git a/examples/pyomobook/strip_examples.py b/examples/pyomobook/strip_examples.py index 0a65eef7c04..68d9e0d99a5 100644 --- a/examples/pyomobook/strip_examples.py +++ b/examples/pyomobook/strip_examples.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import glob import sys import os diff --git a/pyomo/common/multithread.py b/pyomo/common/multithread.py index 415d8aaba7e..f90e7f7c89e 100644 --- a/pyomo/common/multithread.py +++ b/pyomo/common/multithread.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from collections import defaultdict from threading import get_ident, main_thread diff --git a/pyomo/common/shutdown.py b/pyomo/common/shutdown.py index 5054fd21279..984fa8e8a52 100644 --- a/pyomo/common/shutdown.py +++ b/pyomo/common/shutdown.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import atexit diff --git a/pyomo/common/tests/import_ex.py b/pyomo/common/tests/import_ex.py index e19ad956044..d1bf02752eb 100644 --- a/pyomo/common/tests/import_ex.py +++ b/pyomo/common/tests/import_ex.py @@ -1,3 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + def a(): pass diff --git a/pyomo/common/tests/test_multithread.py b/pyomo/common/tests/test_multithread.py index ae1bc48be44..a6c0cac32c7 100644 --- a/pyomo/common/tests/test_multithread.py +++ b/pyomo/common/tests/test_multithread.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import threading import pyomo.common.unittest as unittest from pyomo.common.multithread import * diff --git a/pyomo/contrib/__init__.py b/pyomo/contrib/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/__init__.py +++ b/pyomo/contrib/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/ampl_function_demo/__init__.py b/pyomo/contrib/ampl_function_demo/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/ampl_function_demo/__init__.py +++ b/pyomo/contrib/ampl_function_demo/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/ampl_function_demo/tests/__init__.py b/pyomo/contrib/ampl_function_demo/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/ampl_function_demo/tests/__init__.py +++ b/pyomo/contrib/ampl_function_demo/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/appsi/__init__.py b/pyomo/contrib/appsi/__init__.py index df3ba212448..305231001c4 100644 --- a/pyomo/contrib/appsi/__init__.py +++ b/pyomo/contrib/appsi/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from . import base from . import solvers from . import writers diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index e6186eeedd2..a34bbdb5e1f 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import abc import enum from typing import ( diff --git a/pyomo/contrib/appsi/cmodel/src/common.cpp b/pyomo/contrib/appsi/cmodel/src/common.cpp index 255a0a3a70f..e9f1398fa2f 100644 --- a/pyomo/contrib/appsi/cmodel/src/common.cpp +++ b/pyomo/contrib/appsi/cmodel/src/common.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "common.hpp" double inf; diff --git a/pyomo/contrib/appsi/cmodel/src/common.hpp b/pyomo/contrib/appsi/cmodel/src/common.hpp index 36afd549116..9a025e031ae 100644 --- a/pyomo/contrib/appsi/cmodel/src/common.hpp +++ b/pyomo/contrib/appsi/cmodel/src/common.hpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include #include diff --git a/pyomo/contrib/appsi/cmodel/src/expression.cpp b/pyomo/contrib/appsi/cmodel/src/expression.cpp index 1923d3a1894..f9e6b5c326a 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.cpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.cpp @@ -1,1970 +1,1982 @@ -#include "expression.hpp" - -bool Leaf::is_leaf() { return true; } - -bool Var::is_variable_type() { return true; } - -bool Param::is_param_type() { return true; } - -bool Constant::is_constant_type() { return true; } - -bool Expression::is_expression_type() { return true; } - -double Leaf::evaluate() { return value; } - -double Var::get_lb() { - if (fixed) - return value; - else - return std::max(lb->evaluate(), domain_lb); -} - -double Var::get_ub() { - if (fixed) - return value; - else - return std::min(ub->evaluate(), domain_ub); -} - -Domain Var::get_domain() { return domain; } - -bool Operator::is_operator_type() { return true; } - -std::vector> Expression::get_operators() { - std::vector> res(n_operators); - for (unsigned int i = 0; i < n_operators; ++i) { - res[i] = operators[i]; - } - return res; -} - -double Leaf::get_value_from_array(double *val_array) { return value; } - -double Expression::get_value_from_array(double *val_array) { - return val_array[n_operators - 1]; -} - -double Operator::get_value_from_array(double *val_array) { - return val_array[index]; -} - -void MultiplyOperator::evaluate(double *values) { - values[index] = operand1->get_value_from_array(values) * - operand2->get_value_from_array(values); -} - -void ExternalOperator::evaluate(double *values) { - // It would be nice to implement this, but it will take some more work. - // This would require dynamic linking to the external function. - throw std::runtime_error("cannot evaluate ExternalOperator yet"); -} - -void LinearOperator::evaluate(double *values) { - values[index] = constant->evaluate(); - for (unsigned int i = 0; i < nterms; ++i) { - values[index] += coefficients[i]->evaluate() * variables[i]->evaluate(); - } -} - -void SumOperator::evaluate(double *values) { - values[index] = 0.0; - for (unsigned int i = 0; i < nargs; ++i) { - values[index] += operands[i]->get_value_from_array(values); - } -} - -void DivideOperator::evaluate(double *values) { - values[index] = operand1->get_value_from_array(values) / - operand2->get_value_from_array(values); -} - -void PowerOperator::evaluate(double *values) { - values[index] = std::pow(operand1->get_value_from_array(values), - operand2->get_value_from_array(values)); -} - -void NegationOperator::evaluate(double *values) { - values[index] = -operand->get_value_from_array(values); -} - -void ExpOperator::evaluate(double *values) { - values[index] = std::exp(operand->get_value_from_array(values)); -} - -void LogOperator::evaluate(double *values) { - values[index] = std::log(operand->get_value_from_array(values)); -} - -void AbsOperator::evaluate(double *values) { - values[index] = std::fabs(operand->get_value_from_array(values)); -} - -void SqrtOperator::evaluate(double *values) { - values[index] = std::pow(operand->get_value_from_array(values), 0.5); -} - -void Log10Operator::evaluate(double *values) { - values[index] = std::log10(operand->get_value_from_array(values)); -} - -void SinOperator::evaluate(double *values) { - values[index] = std::sin(operand->get_value_from_array(values)); -} - -void CosOperator::evaluate(double *values) { - values[index] = std::cos(operand->get_value_from_array(values)); -} - -void TanOperator::evaluate(double *values) { - values[index] = std::tan(operand->get_value_from_array(values)); -} - -void AsinOperator::evaluate(double *values) { - values[index] = std::asin(operand->get_value_from_array(values)); -} - -void AcosOperator::evaluate(double *values) { - values[index] = std::acos(operand->get_value_from_array(values)); -} - -void AtanOperator::evaluate(double *values) { - values[index] = std::atan(operand->get_value_from_array(values)); -} - -double Expression::evaluate() { - double *values = new double[n_operators]; - for (unsigned int i = 0; i < n_operators; ++i) { - operators[i]->index = i; - operators[i]->evaluate(values); - } - double res = get_value_from_array(values); - delete[] values; - return res; -} - -void UnaryOperator::identify_variables( - std::set> &var_set, - std::shared_ptr>> var_vec) { - if (operand->is_variable_type()) { - if (var_set.count(operand) == 0) { - var_vec->push_back(std::dynamic_pointer_cast(operand)); - var_set.insert(operand); - } - } -} - -void BinaryOperator::identify_variables( - std::set> &var_set, - std::shared_ptr>> var_vec) { - if (operand1->is_variable_type()) { - if (var_set.count(operand1) == 0) { - var_vec->push_back(std::dynamic_pointer_cast(operand1)); - var_set.insert(operand1); - } - } - if (operand2->is_variable_type()) { - if (var_set.count(operand2) == 0) { - var_vec->push_back(std::dynamic_pointer_cast(operand2)); - var_set.insert(operand2); - } - } -} - -void ExternalOperator::identify_variables( - std::set> &var_set, - std::shared_ptr>> var_vec) { - for (unsigned int i = 0; i < nargs; ++i) { - if (operands[i]->is_variable_type()) { - if (var_set.count(operands[i]) == 0) { - var_vec->push_back(std::dynamic_pointer_cast(operands[i])); - var_set.insert(operands[i]); - } - } - } -} - -void LinearOperator::identify_variables( - std::set> &var_set, - std::shared_ptr>> var_vec) { - for (unsigned int i = 0; i < nterms; ++i) { - if (var_set.count(variables[i]) == 0) { - var_vec->push_back(std::dynamic_pointer_cast(variables[i])); - var_set.insert(variables[i]); - } - } -} - -void SumOperator::identify_variables( - std::set> &var_set, - std::shared_ptr>> var_vec) { - for (unsigned int i = 0; i < nargs; ++i) { - if (operands[i]->is_variable_type()) { - if (var_set.count(operands[i]) == 0) { - var_vec->push_back(std::dynamic_pointer_cast(operands[i])); - var_set.insert(operands[i]); - } - } - } -} - -std::shared_ptr>> -Expression::identify_variables() { - std::set> var_set; - std::shared_ptr>> res = - std::make_shared>>(var_set.size()); - for (unsigned int i = 0; i < n_operators; ++i) { - operators[i]->identify_variables(var_set, res); - } - return res; -} - -std::shared_ptr>> Var::identify_variables() { - std::shared_ptr>> res = - std::make_shared>>(); - res->push_back(shared_from_this()); - return res; -} - -std::shared_ptr>> -Constant::identify_variables() { - std::shared_ptr>> res = - std::make_shared>>(); - return res; -} - -std::shared_ptr>> Param::identify_variables() { - std::shared_ptr>> res = - std::make_shared>>(); - return res; -} - -std::shared_ptr>> -Expression::identify_external_operators() { - std::set> external_set; - for (unsigned int i = 0; i < n_operators; ++i) { - if (operators[i]->is_external_operator()) { - external_set.insert(operators[i]); - } - } - std::shared_ptr>> res = - std::make_shared>>( - external_set.size()); - int ndx = 0; - for (std::shared_ptr n : external_set) { - (*res)[ndx] = std::dynamic_pointer_cast(n); - ndx += 1; - } - return res; -} - -std::shared_ptr>> -Var::identify_external_operators() { - std::shared_ptr>> res = - std::make_shared>>(); - return res; -} - -std::shared_ptr>> -Constant::identify_external_operators() { - std::shared_ptr>> res = - std::make_shared>>(); - return res; -} - -std::shared_ptr>> -Param::identify_external_operators() { - std::shared_ptr>> res = - std::make_shared>>(); - return res; -} - -int Var::get_degree_from_array(int *degree_array) { return 1; } - -int Param::get_degree_from_array(int *degree_array) { return 0; } - -int Constant::get_degree_from_array(int *degree_array) { return 0; } - -int Expression::get_degree_from_array(int *degree_array) { - return degree_array[n_operators - 1]; -} - -int Operator::get_degree_from_array(int *degree_array) { - return degree_array[index]; -} - -void LinearOperator::propagate_degree_forward(int *degrees, double *values) { - degrees[index] = 1; -} - -void SumOperator::propagate_degree_forward(int *degrees, double *values) { - int deg = 0; - int _deg; - for (unsigned int i = 0; i < nargs; ++i) { - _deg = operands[i]->get_degree_from_array(degrees); - if (_deg > deg) { - deg = _deg; - } - } - degrees[index] = deg; -} - -void MultiplyOperator::propagate_degree_forward(int *degrees, double *values) { - degrees[index] = operand1->get_degree_from_array(degrees) + - operand2->get_degree_from_array(degrees); -} - -void ExternalOperator::propagate_degree_forward(int *degrees, double *values) { - // External functions are always considered nonlinear - // Anything larger than 2 is nonlinear - degrees[index] = 3; -} - -void DivideOperator::propagate_degree_forward(int *degrees, double *values) { - // anything larger than 2 is nonlinear - degrees[index] = std::max(operand1->get_degree_from_array(degrees), - 3 * (operand2->get_degree_from_array(degrees))); -} - -void PowerOperator::propagate_degree_forward(int *degrees, double *values) { - if (operand2->get_degree_from_array(degrees) != 0) { - degrees[index] = 3; - } else { - double val2 = operand2->get_value_from_array(values); - double intpart; - if (std::modf(val2, &intpart) == 0.0) { - degrees[index] = operand1->get_degree_from_array(degrees) * (int)val2; - } else { - degrees[index] = 3; - } - } -} - -void NegationOperator::propagate_degree_forward(int *degrees, double *values) { - degrees[index] = operand->get_degree_from_array(degrees); -} - -void UnaryOperator::propagate_degree_forward(int *degrees, double *values) { - if (operand->get_degree_from_array(degrees) == 0) { - degrees[index] = 0; - } else { - degrees[index] = 3; - } -} - -std::string Var::__str__() { return name; } - -std::string Param::__str__() { return name; } - -std::string Constant::__str__() { return std::to_string(value); } - -std::string Expression::__str__() { - std::string *string_array = new std::string[n_operators]; - std::shared_ptr oper; - for (unsigned int i = 0; i < n_operators; ++i) { - oper = operators[i]; - oper->index = i; - oper->print(string_array); - } - std::string res = string_array[n_operators - 1]; - delete[] string_array; - return res; -} - -std::string Leaf::get_string_from_array(std::string *string_array) { - return __str__(); -} - -std::string Expression::get_string_from_array(std::string *string_array) { - return string_array[n_operators - 1]; -} - -std::string Operator::get_string_from_array(std::string *string_array) { - return string_array[index]; -} - -void MultiplyOperator::print(std::string *string_array) { - string_array[index] = - ("(" + operand1->get_string_from_array(string_array) + "*" + - operand2->get_string_from_array(string_array) + ")"); -} - -void ExternalOperator::print(std::string *string_array) { - std::string res = function_name + "("; - for (unsigned int i = 0; i < (nargs - 1); ++i) { - res += operands[i]->get_string_from_array(string_array); - res += ", "; - } - res += operands[nargs - 1]->get_string_from_array(string_array); - res += ")"; - string_array[index] = res; -} - -void DivideOperator::print(std::string *string_array) { - string_array[index] = - ("(" + operand1->get_string_from_array(string_array) + "/" + - operand2->get_string_from_array(string_array) + ")"); -} - -void PowerOperator::print(std::string *string_array) { - string_array[index] = - ("(" + operand1->get_string_from_array(string_array) + "**" + - operand2->get_string_from_array(string_array) + ")"); -} - -void NegationOperator::print(std::string *string_array) { - string_array[index] = - ("(-" + operand->get_string_from_array(string_array) + ")"); -} - -void ExpOperator::print(std::string *string_array) { - string_array[index] = - ("exp(" + operand->get_string_from_array(string_array) + ")"); -} - -void LogOperator::print(std::string *string_array) { - string_array[index] = - ("log(" + operand->get_string_from_array(string_array) + ")"); -} - -void AbsOperator::print(std::string *string_array) { - string_array[index] = - ("abs(" + operand->get_string_from_array(string_array) + ")"); -} - -void SqrtOperator::print(std::string *string_array) { - string_array[index] = - ("sqrt(" + operand->get_string_from_array(string_array) + ")"); -} - -void Log10Operator::print(std::string *string_array) { - string_array[index] = - ("log10(" + operand->get_string_from_array(string_array) + ")"); -} - -void SinOperator::print(std::string *string_array) { - string_array[index] = - ("sin(" + operand->get_string_from_array(string_array) + ")"); -} - -void CosOperator::print(std::string *string_array) { - string_array[index] = - ("cos(" + operand->get_string_from_array(string_array) + ")"); -} - -void TanOperator::print(std::string *string_array) { - string_array[index] = - ("tan(" + operand->get_string_from_array(string_array) + ")"); -} - -void AsinOperator::print(std::string *string_array) { - string_array[index] = - ("asin(" + operand->get_string_from_array(string_array) + ")"); -} - -void AcosOperator::print(std::string *string_array) { - string_array[index] = - ("acos(" + operand->get_string_from_array(string_array) + ")"); -} - -void AtanOperator::print(std::string *string_array) { - string_array[index] = - ("atan(" + operand->get_string_from_array(string_array) + ")"); -} - -void LinearOperator::print(std::string *string_array) { - std::string res = "(" + constant->__str__(); - for (unsigned int i = 0; i < nterms; ++i) { - res += " + " + coefficients[i]->__str__() + "*" + variables[i]->__str__(); - } - res += ")"; - string_array[index] = res; -} - -void SumOperator::print(std::string *string_array) { - std::string res = "(" + operands[0]->get_string_from_array(string_array); - for (unsigned int i = 1; i < nargs; ++i) { - res += " + " + operands[i]->get_string_from_array(string_array); - } - res += ")"; - string_array[index] = res; -} - -std::shared_ptr>> -Leaf::get_prefix_notation() { - std::shared_ptr>> res = - std::make_shared>>(); - res->push_back(shared_from_this()); - return res; -} - -std::shared_ptr>> -Expression::get_prefix_notation() { - std::shared_ptr>> res = - std::make_shared>>(); - std::shared_ptr>> stack = - std::make_shared>>(); - std::shared_ptr node; - stack->push_back(operators[n_operators - 1]); - while (stack->size() > 0) { - node = stack->back(); - stack->pop_back(); - res->push_back(node); - node->fill_prefix_notation_stack(stack); - } - - return res; -} - -void BinaryOperator::fill_prefix_notation_stack( - std::shared_ptr>> stack) { - stack->push_back(operand2); - stack->push_back(operand1); -} - -void UnaryOperator::fill_prefix_notation_stack( - std::shared_ptr>> stack) { - stack->push_back(operand); -} - -void SumOperator::fill_prefix_notation_stack( - std::shared_ptr>> stack) { - int ndx = nargs - 1; - while (ndx >= 0) { - stack->push_back(operands[ndx]); - ndx -= 1; - } -} - -void LinearOperator::fill_prefix_notation_stack( - std::shared_ptr>> stack) { - ; // This is treated as a leaf in this context; write_nl_string will take care - // of it -} - -void ExternalOperator::fill_prefix_notation_stack( - std::shared_ptr>> stack) { - int i = nargs - 1; - while (i >= 0) { - stack->push_back(operands[i]); - i -= 1; - } -} - -void Var::write_nl_string(std::ofstream &f) { f << "v" << index << "\n"; } - -void Param::write_nl_string(std::ofstream &f) { f << "n" << value << "\n"; } - -void Constant::write_nl_string(std::ofstream &f) { f << "n" << value << "\n"; } - -void Expression::write_nl_string(std::ofstream &f) { - std::shared_ptr>> prefix_notation = - get_prefix_notation(); - for (std::shared_ptr &node : *(prefix_notation)) { - node->write_nl_string(f); - } -} - -void MultiplyOperator::write_nl_string(std::ofstream &f) { f << "o2\n"; } - -void ExternalOperator::write_nl_string(std::ofstream &f) { - f << "f" << external_function_index << " " << nargs << "\n"; -} - -void SumOperator::write_nl_string(std::ofstream &f) { - if (nargs == 2) { - f << "o0\n"; - } else { - f << "o54\n"; - f << nargs << "\n"; - } -} - -void LinearOperator::write_nl_string(std::ofstream &f) { - bool has_const = - (!constant->is_constant_type()) || (constant->evaluate() != 0); - unsigned int n_sum_args = nterms + (has_const ? 1 : 0); - if (n_sum_args == 2) { - f << "o0\n"; - } else { - f << "o54\n"; - f << n_sum_args << "\n"; - } - if (has_const) - f << "n" << constant->evaluate() << "\n"; - for (unsigned int ndx = 0; ndx < nterms; ++ndx) { - f << "o2\n"; - f << "n" << coefficients[ndx]->evaluate() << "\n"; - variables[ndx]->write_nl_string(f); - } -} - -void DivideOperator::write_nl_string(std::ofstream &f) { f << "o3\n"; } - -void PowerOperator::write_nl_string(std::ofstream &f) { f << "o5\n"; } - -void NegationOperator::write_nl_string(std::ofstream &f) { f << "o16\n"; } - -void ExpOperator::write_nl_string(std::ofstream &f) { f << "o44\n"; } - -void LogOperator::write_nl_string(std::ofstream &f) { f << "o43\n"; } - -void AbsOperator::write_nl_string(std::ofstream &f) { f << "o15\n"; } - -void SqrtOperator::write_nl_string(std::ofstream &f) { f << "o39\n"; } - -void Log10Operator::write_nl_string(std::ofstream &f) { f << "o42\n"; } - -void SinOperator::write_nl_string(std::ofstream &f) { f << "o41\n"; } - -void CosOperator::write_nl_string(std::ofstream &f) { f << "o46\n"; } - -void TanOperator::write_nl_string(std::ofstream &f) { f << "o38\n"; } - -void AsinOperator::write_nl_string(std::ofstream &f) { f << "o51\n"; } - -void AcosOperator::write_nl_string(std::ofstream &f) { f << "o53\n"; } - -void AtanOperator::write_nl_string(std::ofstream &f) { f << "o49\n"; } - -bool BinaryOperator::is_binary_operator() { return true; } - -bool UnaryOperator::is_unary_operator() { return true; } - -bool LinearOperator::is_linear_operator() { return true; } - -bool SumOperator::is_sum_operator() { return true; } - -bool MultiplyOperator::is_multiply_operator() { return true; } - -bool DivideOperator::is_divide_operator() { return true; } - -bool PowerOperator::is_power_operator() { return true; } - -bool NegationOperator::is_negation_operator() { return true; } - -bool ExpOperator::is_exp_operator() { return true; } - -bool LogOperator::is_log_operator() { return true; } - -bool AbsOperator::is_abs_operator() { return true; } - -bool SqrtOperator::is_sqrt_operator() { return true; } - -bool ExternalOperator::is_external_operator() { return true; } - -void Leaf::fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) { - ; -} - -void Expression::fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) { - throw std::runtime_error("This should not happen"); -} - -void BinaryOperator::fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) { - oper_ndx -= 1; - oper_array[oper_ndx] = shared_from_this(); - // The order does not actually matter here. It - // will just be easier to debug this way. - operand2->fill_expression(oper_array, oper_ndx); - operand1->fill_expression(oper_array, oper_ndx); -} - -void UnaryOperator::fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) { - oper_ndx -= 1; - oper_array[oper_ndx] = shared_from_this(); - operand->fill_expression(oper_array, oper_ndx); -} - -void LinearOperator::fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) { - oper_ndx -= 1; - oper_array[oper_ndx] = shared_from_this(); -} - -void SumOperator::fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) { - oper_ndx -= 1; - oper_array[oper_ndx] = shared_from_this(); - // The order does not actually matter here. It - // will just be easier to debug this way. - int arg_ndx = nargs - 1; - while (arg_ndx >= 0) { - operands[arg_ndx]->fill_expression(oper_array, oper_ndx); - arg_ndx -= 1; - } -} - -void ExternalOperator::fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) { - oper_ndx -= 1; - oper_array[oper_ndx] = shared_from_this(); - // The order does not actually matter here. It - // will just be easier to debug this way. - int arg_ndx = nargs - 1; - while (arg_ndx >= 0) { - operands[arg_ndx]->fill_expression(oper_array, oper_ndx); - arg_ndx -= 1; - } -} - -double Leaf::get_lb_from_array(double *lbs) { return value; } - -double Leaf::get_ub_from_array(double *ubs) { return value; } - -double Var::get_lb_from_array(double *lbs) { return get_lb(); } - -double Var::get_ub_from_array(double *ubs) { return get_ub(); } - -double Expression::get_lb_from_array(double *lbs) { - return lbs[n_operators - 1]; -} - -double Expression::get_ub_from_array(double *ubs) { - return ubs[n_operators - 1]; -} - -double Operator::get_lb_from_array(double *lbs) { return lbs[index]; } - -double Operator::get_ub_from_array(double *ubs) { return ubs[index]; } - -void Leaf::set_bounds_in_array(double new_lb, double new_ub, double *lbs, - double *ubs, double feasibility_tol, - double integer_tol, double improvement_tol, - std::set> &improved_vars) { - if (new_lb < value - feasibility_tol || new_lb > value + feasibility_tol) { - throw InfeasibleConstraintException( - "Infeasible constraint; bounds computed on parameter or constant " - "disagree with the value of the parameter or constant\n value: " + - std::to_string(value) + "\n computed LB: " + std::to_string(new_lb) + - "\n computed UB: " + std::to_string(new_ub)); - } - - if (new_ub < value - feasibility_tol || new_ub > value + feasibility_tol) { - throw InfeasibleConstraintException( - "Infeasible constraint; bounds computed on parameter or constant " - "disagree with the value of the parameter or constant\n value: " + - std::to_string(value) + "\n computed LB: " + std::to_string(new_lb) + - "\n computed UB: " + std::to_string(new_ub)); - } -} - -void Var::set_bounds_in_array(double new_lb, double new_ub, double *lbs, - double *ubs, double feasibility_tol, - double integer_tol, double improvement_tol, - std::set> &improved_vars) { - if (new_lb > new_ub) { - if (new_lb - feasibility_tol > new_ub) - throw InfeasibleConstraintException( - "Infeasible constraint; The computed lower bound for a variable is " - "larger than the computed upper bound.\n computed LB: " + - std::to_string(new_lb) + - "\n computed UB: " + std::to_string(new_ub)); - else { - new_lb -= feasibility_tol; - new_ub += feasibility_tol; - } - } - if (new_lb >= inf) - throw InfeasibleConstraintException( - "Infeasible constraint; The compute lower bound for " + name + - " is inf"); - if (new_ub <= -inf) - throw InfeasibleConstraintException( - "Infeasible constraint; The computed upper bound for " + name + - " is -inf"); - - if (domain == integers || domain == binary) { - if (new_lb > -inf) { - double lb_floor = floor(new_lb); - double lb_ceil = ceil(new_lb - integer_tol); - if (lb_floor > lb_ceil) - new_lb = lb_floor; - else - new_lb = lb_ceil; - } - if (new_ub < inf) { - double ub_ceil = ceil(new_ub); - double ub_floor = floor(new_ub + integer_tol); - if (ub_ceil < ub_floor) - new_ub = ub_ceil; - else - new_ub = ub_floor; - } - } - - double current_lb = get_lb(); - double current_ub = get_ub(); - - if (new_lb > current_lb + improvement_tol || - new_ub < current_ub - improvement_tol) - improved_vars.insert(shared_from_this()); - - if (new_lb > current_lb) { - if (lb->is_leaf()) - std::dynamic_pointer_cast(lb)->value = new_lb; - else - throw py::value_error( - "variable bounds cannot be expressions when performing FBBT"); - } - - if (new_ub < current_ub) { - if (ub->is_leaf()) - std::dynamic_pointer_cast(ub)->value = new_ub; - else - throw py::value_error( - "variable bounds cannot be expressions when performing FBBT"); - } -} - -void Expression::set_bounds_in_array( - double new_lb, double new_ub, double *lbs, double *ubs, - double feasibility_tol, double integer_tol, double improvement_tol, - std::set> &improved_vars) { - lbs[n_operators - 1] = new_lb; - ubs[n_operators - 1] = new_ub; -} - -void Operator::set_bounds_in_array( - double new_lb, double new_ub, double *lbs, double *ubs, - double feasibility_tol, double integer_tol, double improvement_tol, - std::set> &improved_vars) { - lbs[index] = new_lb; - ubs[index] = new_ub; -} - -void Expression::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - for (unsigned int ndx = 0; ndx < n_operators; ++ndx) { - operators[ndx]->index = ndx; - operators[ndx]->propagate_bounds_forward(lbs, ubs, feasibility_tol, - integer_tol); - } -} - -void Expression::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - int ndx = n_operators - 1; - while (ndx >= 0) { - operators[ndx]->propagate_bounds_backward( - lbs, ubs, feasibility_tol, integer_tol, improvement_tol, improved_vars); - ndx -= 1; - } -} - -void Operator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - lbs[index] = -inf; - ubs[index] = inf; -} - -void Operator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - ; -} - -void MultiplyOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - if (operand1 == operand2) { - interval_power(operand1->get_lb_from_array(lbs), - operand1->get_ub_from_array(ubs), 2, 2, &lbs[index], - &ubs[index], feasibility_tol); - } else { - interval_mul(operand1->get_lb_from_array(lbs), - operand1->get_ub_from_array(ubs), - operand2->get_lb_from_array(lbs), - operand2->get_ub_from_array(ubs), &lbs[index], &ubs[index]); - } -} - -void MultiplyOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand1->get_lb_from_array(lbs); - double xu = operand1->get_ub_from_array(ubs); - double yl = operand2->get_lb_from_array(lbs); - double yu = operand2->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu, new_yl, new_yu; - - if (operand1 == operand2) { - _inverse_power1(lb, ub, 2, 2, xl, xu, &new_xl, &new_xu, feasibility_tol); - new_yl = new_xl; - new_yu = new_xu; - } else { - interval_div(lb, ub, yl, yu, &new_xl, &new_xu, feasibility_tol); - interval_div(lb, ub, xl, xu, &new_yl, &new_yu, feasibility_tol); - } - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand1->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); - - if (new_yl > yl) - yl = new_yl; - if (new_yu < yu) - yu = new_yu; - operand2->set_bounds_in_array(yl, yu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void SumOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - double lb = operands[0]->get_lb_from_array(lbs); - double ub = operands[0]->get_ub_from_array(ubs); - double tmp_lb; - double tmp_ub; - - for (unsigned int ndx = 1; ndx < nargs; ++ndx) { - interval_add(lb, ub, operands[ndx]->get_lb_from_array(lbs), - operands[ndx]->get_ub_from_array(ubs), &tmp_lb, &tmp_ub); - lb = tmp_lb; - ub = tmp_ub; - } - - lbs[index] = lb; - ubs[index] = ub; -} - -void SumOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double *accumulated_lbs = new double[nargs]; - double *accumulated_ubs = new double[nargs]; - - accumulated_lbs[0] = operands[0]->get_lb_from_array(lbs); - accumulated_ubs[0] = operands[0]->get_ub_from_array(ubs); - for (unsigned int ndx = 1; ndx < nargs; ++ndx) { - interval_add(accumulated_lbs[ndx - 1], accumulated_ubs[ndx - 1], - operands[ndx]->get_lb_from_array(lbs), - operands[ndx]->get_ub_from_array(ubs), &accumulated_lbs[ndx], - &accumulated_ubs[ndx]); - } - - double new_sum_lb = get_lb_from_array(lbs); - double new_sum_ub = get_ub_from_array(ubs); - - if (new_sum_lb > accumulated_lbs[nargs - 1]) - accumulated_lbs[nargs - 1] = new_sum_lb; - if (new_sum_ub < accumulated_ubs[nargs - 1]) - accumulated_ubs[nargs - 1] = new_sum_ub; - - double lb0, ub0, lb1, ub1, lb2, ub2, _lb1, _ub1, _lb2, _ub2; - - int ndx = nargs - 1; - while (ndx >= 1) { - lb0 = accumulated_lbs[ndx]; - ub0 = accumulated_ubs[ndx]; - lb1 = accumulated_lbs[ndx - 1]; - ub1 = accumulated_ubs[ndx - 1]; - lb2 = operands[ndx]->get_lb_from_array(lbs); - ub2 = operands[ndx]->get_ub_from_array(ubs); - interval_sub(lb0, ub0, lb2, ub2, &_lb1, &_ub1); - interval_sub(lb0, ub0, lb1, ub1, &_lb2, &_ub2); - if (_lb1 > lb1) - lb1 = _lb1; - if (_ub1 < ub1) - ub1 = _ub1; - if (_lb2 > lb2) - lb2 = _lb2; - if (_ub2 < ub2) - ub2 = _ub2; - accumulated_lbs[ndx - 1] = lb1; - accumulated_ubs[ndx - 1] = ub1; - operands[ndx]->set_bounds_in_array(lb2, ub2, lbs, ubs, feasibility_tol, - integer_tol, improvement_tol, - improved_vars); - ndx -= 1; - } - - // take care of ndx = 0 - lb1 = operands[0]->get_lb_from_array(lbs); - ub1 = operands[0]->get_ub_from_array(ubs); - _lb1 = accumulated_lbs[0]; - _ub1 = accumulated_ubs[0]; - if (_lb1 > lb1) - lb1 = _lb1; - if (_ub1 < ub1) - ub1 = _ub1; - operands[0]->set_bounds_in_array(lb1, ub1, lbs, ubs, feasibility_tol, - integer_tol, improvement_tol, improved_vars); - - delete[] accumulated_lbs; - delete[] accumulated_ubs; -} - -void LinearOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - double lb = constant->evaluate(); - double ub = lb; - double tmp_lb; - double tmp_ub; - double coef; - - for (unsigned int ndx = 0; ndx < nterms; ++ndx) { - coef = coefficients[ndx]->evaluate(); - interval_mul(coef, coef, variables[ndx]->get_lb(), variables[ndx]->get_ub(), - &tmp_lb, &tmp_ub); - interval_add(lb, ub, tmp_lb, tmp_ub, &lb, &ub); - } - - lbs[index] = lb; - ubs[index] = ub; -} - -void LinearOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double *accumulated_lbs = new double[nterms + 1]; - double *accumulated_ubs = new double[nterms + 1]; - - double coef; - - accumulated_lbs[0] = constant->evaluate(); - accumulated_ubs[0] = constant->evaluate(); - for (unsigned int ndx = 0; ndx < nterms; ++ndx) { - coef = coefficients[ndx]->evaluate(); - interval_mul(coef, coef, variables[ndx]->get_lb(), variables[ndx]->get_ub(), - &accumulated_lbs[ndx + 1], &accumulated_ubs[ndx + 1]); - interval_add(accumulated_lbs[ndx], accumulated_ubs[ndx], - accumulated_lbs[ndx + 1], accumulated_ubs[ndx + 1], - &accumulated_lbs[ndx + 1], &accumulated_ubs[ndx + 1]); - } - - double new_sum_lb = get_lb_from_array(lbs); - double new_sum_ub = get_ub_from_array(ubs); - - if (new_sum_lb > accumulated_lbs[nterms]) - accumulated_lbs[nterms] = new_sum_lb; - if (new_sum_ub < accumulated_ubs[nterms]) - accumulated_ubs[nterms] = new_sum_ub; - - double lb0, ub0, lb1, ub1, lb2, ub2, _lb1, _ub1, _lb2, _ub2, new_v_lb, - new_v_ub; - - int ndx = nterms - 1; - while (ndx >= 0) { - lb0 = accumulated_lbs[ndx + 1]; - ub0 = accumulated_ubs[ndx + 1]; - lb1 = accumulated_lbs[ndx]; - ub1 = accumulated_ubs[ndx]; - coef = coefficients[ndx]->evaluate(); - interval_mul(coef, coef, variables[ndx]->get_lb(), variables[ndx]->get_ub(), - &lb2, &ub2); - interval_sub(lb0, ub0, lb2, ub2, &_lb1, &_ub1); - interval_sub(lb0, ub0, lb1, ub1, &_lb2, &_ub2); - if (_lb1 > lb1) - lb1 = _lb1; - if (_ub1 < ub1) - ub1 = _ub1; - if (_lb2 > lb2) - lb2 = _lb2; - if (_ub2 < ub2) - ub2 = _ub2; - accumulated_lbs[ndx] = lb1; - accumulated_ubs[ndx] = ub1; - interval_div(lb2, ub2, coef, coef, &new_v_lb, &new_v_ub, feasibility_tol); - variables[ndx]->set_bounds_in_array(new_v_lb, new_v_ub, lbs, ubs, - feasibility_tol, integer_tol, - improvement_tol, improved_vars); - ndx -= 1; - } - - delete[] accumulated_lbs; - delete[] accumulated_ubs; -} - -void DivideOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_div( - operand1->get_lb_from_array(lbs), operand1->get_ub_from_array(ubs), - operand2->get_lb_from_array(lbs), operand2->get_ub_from_array(ubs), - &lbs[index], &ubs[index], feasibility_tol); -} - -void DivideOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand1->get_lb_from_array(lbs); - double xu = operand1->get_ub_from_array(ubs); - double yl = operand2->get_lb_from_array(lbs); - double yu = operand2->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl; - double new_xu; - double new_yl; - double new_yu; - - interval_mul(lb, ub, yl, yu, &new_xl, &new_xu); - interval_div(xl, xu, lb, ub, &new_yl, &new_yu, feasibility_tol); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand1->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); - - if (new_yl > yl) - yl = new_yl; - if (new_yu < yu) - yu = new_yu; - operand2->set_bounds_in_array(yl, yu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void NegationOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_sub(0, 0, operand->get_lb_from_array(lbs), - operand->get_ub_from_array(ubs), &lbs[index], &ubs[index]); -} - -void NegationOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl; - double new_xu; - - interval_sub(0, 0, lb, ub, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void PowerOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_power( - operand1->get_lb_from_array(lbs), operand1->get_ub_from_array(ubs), - operand2->get_lb_from_array(lbs), operand2->get_ub_from_array(ubs), - &lbs[index], &ubs[index], feasibility_tol); -} - -void PowerOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand1->get_lb_from_array(lbs); - double xu = operand1->get_ub_from_array(ubs); - double yl = operand2->get_lb_from_array(lbs); - double yu = operand2->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu, new_yl, new_yu; - _inverse_power1(lb, ub, yl, yu, xl, xu, &new_xl, &new_xu, feasibility_tol); - if (yl != yu) - _inverse_power2(lb, ub, xl, xu, &new_yl, &new_yu, feasibility_tol); - else { - new_yl = yl; - new_yu = yu; - } - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand1->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); - - if (new_yl > yl) - yl = new_yl; - if (new_yu < yu) - yu = new_yu; - operand2->set_bounds_in_array(yl, yu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void SqrtOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_power(operand->get_lb_from_array(lbs), - operand->get_ub_from_array(ubs), 0.5, 0.5, &lbs[index], - &ubs[index], feasibility_tol); -} - -void SqrtOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double yl = 0.5; - double yu = 0.5; - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - _inverse_power1(lb, ub, yl, yu, xl, xu, &new_xl, &new_xu, feasibility_tol); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void ExpOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_exp(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), - &lbs[index], &ubs[index]); -} - -void ExpOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_log(lb, ub, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void LogOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_log(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), - &lbs[index], &ubs[index]); -} - -void LogOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_exp(lb, ub, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void AbsOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_abs(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), - &lbs[index], &ubs[index]); -} - -void AbsOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - _inverse_abs(lb, ub, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void Log10Operator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_log10(operand->get_lb_from_array(lbs), - operand->get_ub_from_array(ubs), &lbs[index], &ubs[index]); -} - -void Log10Operator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_power(10, 10, lb, ub, &new_xl, &new_xu, feasibility_tol); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void SinOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_sin(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), - &lbs[index], &ubs[index]); -} - -void SinOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_asin(lb, ub, xl, xu, &new_xl, &new_xu, feasibility_tol); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void CosOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_cos(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), - &lbs[index], &ubs[index]); -} - -void CosOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_acos(lb, ub, xl, xu, &new_xl, &new_xu, feasibility_tol); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void TanOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_tan(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), - &lbs[index], &ubs[index]); -} - -void TanOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_atan(lb, ub, xl, xu, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void AsinOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_asin(operand->get_lb_from_array(lbs), - operand->get_ub_from_array(ubs), -inf, inf, &lbs[index], - &ubs[index], feasibility_tol); -} - -void AsinOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_sin(lb, ub, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void AcosOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_acos(operand->get_lb_from_array(lbs), - operand->get_ub_from_array(ubs), -inf, inf, &lbs[index], - &ubs[index], feasibility_tol); -} - -void AcosOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_cos(lb, ub, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -void AtanOperator::propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) { - interval_atan(operand->get_lb_from_array(lbs), - operand->get_ub_from_array(ubs), -inf, inf, &lbs[index], - &ubs[index]); -} - -void AtanOperator::propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, std::set> &improved_vars) { - double xl = operand->get_lb_from_array(lbs); - double xu = operand->get_ub_from_array(ubs); - double lb = get_lb_from_array(lbs); - double ub = get_ub_from_array(ubs); - - double new_xl, new_xu; - interval_tan(lb, ub, &new_xl, &new_xu); - - if (new_xl > xl) - xl = new_xl; - if (new_xu < xu) - xu = new_xu; - operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, - improvement_tol, improved_vars); -} - -std::vector> create_vars(int n_vars) { - std::vector> res; - for (int i = 0; i < n_vars; ++i) { - res.push_back(std::make_shared()); - } - return res; -} - -std::vector> create_params(int n_params) { - std::vector> res; - for (int i = 0; i < n_params; ++i) { - res.push_back(std::make_shared()); - } - return res; -} - -std::vector> create_constants(int n_constants) { - std::vector> res; - for (int i = 0; i < n_constants; ++i) { - res.push_back(std::make_shared()); - } - return res; -} - -std::shared_ptr -appsi_operator_from_pyomo_expr(py::handle expr, py::handle var_map, - py::handle param_map, - PyomoExprTypes &expr_types) { - std::shared_ptr res; - ExprType tmp_type = - expr_types.expr_type_map[py::type::of(expr)].cast(); - - switch (tmp_type) { - case py_float: { - res = std::make_shared(expr.cast()); - break; - } - case var: { - res = var_map[expr_types.id(expr)].cast>(); - break; - } - case param: { - res = param_map[expr_types.id(expr)].cast>(); - break; - } - case product: { - res = std::make_shared(); - break; - } - case sum: { - res = std::make_shared(expr.attr("nargs")().cast()); - break; - } - case negation: { - res = std::make_shared(); - break; - } - case external_func: { - res = std::make_shared(expr.attr("nargs")().cast()); - std::shared_ptr oper = - std::dynamic_pointer_cast(res); - oper->function_name = - expr.attr("_fcn").attr("_function").cast(); - break; - } - case power: { - res = std::make_shared(); - break; - } - case division: { - res = std::make_shared(); - break; - } - case unary_func: { - std::string function_name = expr.attr("getname")().cast(); - if (function_name == "exp") - res = std::make_shared(); - else if (function_name == "log") - res = std::make_shared(); - else if (function_name == "log10") - res = std::make_shared(); - else if (function_name == "sin") - res = std::make_shared(); - else if (function_name == "cos") - res = std::make_shared(); - else if (function_name == "tan") - res = std::make_shared(); - else if (function_name == "asin") - res = std::make_shared(); - else if (function_name == "acos") - res = std::make_shared(); - else if (function_name == "atan") - res = std::make_shared(); - else if (function_name == "sqrt") - res = std::make_shared(); - else - throw py::value_error("Unrecognized expression type: " + function_name); - break; - } - case linear: { - res = std::make_shared( - expr_types.len(expr.attr("linear_vars")).cast()); - break; - } - case named_expr: { - res = appsi_operator_from_pyomo_expr(expr.attr("expr"), var_map, param_map, - expr_types); - break; - } - case numeric_constant: { - res = std::make_shared(expr.attr("value").cast()); - break; - } - case pyomo_unit: { - res = std::make_shared(1.0); - break; - } - case unary_abs: { - res = std::make_shared(); - break; - } - default: { - throw py::value_error("Unrecognized expression type: " + - expr_types.builtins.attr("str")(py::type::of(expr)) - .cast()); - break; - } - } - return res; -} - -void prep_for_repn_helper(py::handle expr, py::handle named_exprs, - py::handle variables, py::handle fixed_vars, - py::handle external_funcs, - PyomoExprTypes &expr_types) { - ExprType tmp_type = - expr_types.expr_type_map[py::type::of(expr)].cast(); - - switch (tmp_type) { - case py_float: { - break; - } - case var: { - variables[expr_types.id(expr)] = expr; - if (expr.attr("fixed").cast()) { - fixed_vars[expr_types.id(expr)] = expr; - } - break; - } - case param: { - break; - } - case product: { - py::tuple args = expr.attr("_args_"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - case sum: { - py::tuple args = expr.attr("args"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - case negation: { - py::tuple args = expr.attr("_args_"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - case external_func: { - external_funcs[expr_types.id(expr)] = expr; - py::tuple args = expr.attr("args"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - case power: { - py::tuple args = expr.attr("_args_"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - case division: { - py::tuple args = expr.attr("_args_"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - case unary_func: { - py::tuple args = expr.attr("_args_"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - case linear: { - py::list linear_vars = expr.attr("linear_vars"); - py::list linear_coefs = expr.attr("linear_coefs"); - for (py::handle arg : linear_vars) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - for (py::handle arg : linear_coefs) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - prep_for_repn_helper(expr.attr("constant"), named_exprs, variables, - fixed_vars, external_funcs, expr_types); - break; - } - case named_expr: { - named_exprs[expr_types.id(expr)] = expr; - prep_for_repn_helper(expr.attr("expr"), named_exprs, variables, fixed_vars, - external_funcs, expr_types); - break; - } - case numeric_constant: { - break; - } - case pyomo_unit: { - break; - } - case unary_abs: { - py::tuple args = expr.attr("_args_"); - for (py::handle arg : args) { - prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, - external_funcs, expr_types); - } - break; - } - default: { - if (expr_types.builtins.attr("hasattr")(expr, "is_constant").cast()) { - if (expr.attr("is_constant")().cast()) - break; - } - throw py::value_error("Unrecognized expression type: " + - expr_types.builtins.attr("str")(py::type::of(expr)) - .cast()); - break; - } - } -} - -py::tuple prep_for_repn(py::handle expr, PyomoExprTypes &expr_types) { - py::dict named_exprs; - py::dict variables; - py::dict fixed_vars; - py::dict external_funcs; - - prep_for_repn_helper(expr, named_exprs, variables, fixed_vars, external_funcs, - expr_types); - - py::list named_expr_list = named_exprs.attr("values")(); - py::list variable_list = variables.attr("values")(); - py::list fixed_var_list = fixed_vars.attr("values")(); - py::list external_func_list = external_funcs.attr("values")(); - - py::tuple res = py::make_tuple(named_expr_list, variable_list, fixed_var_list, - external_func_list); - return res; -} - -int build_expression_tree(py::handle pyomo_expr, - std::shared_ptr appsi_expr, py::handle var_map, - py::handle param_map, PyomoExprTypes &expr_types) { - int num_nodes = 0; - - if (expr_types.expr_type_map[py::type::of(pyomo_expr)].cast() == - named_expr) - pyomo_expr = pyomo_expr.attr("expr"); - - if (appsi_expr->is_leaf()) { - ; - } else if (appsi_expr->is_binary_operator()) { - num_nodes += 1; - std::shared_ptr oper = - std::dynamic_pointer_cast(appsi_expr); - py::list pyomo_args = pyomo_expr.attr("args"); - oper->operand1 = appsi_operator_from_pyomo_expr(pyomo_args[0], var_map, - param_map, expr_types); - oper->operand2 = appsi_operator_from_pyomo_expr(pyomo_args[1], var_map, - param_map, expr_types); - num_nodes += build_expression_tree(pyomo_args[0], oper->operand1, var_map, - param_map, expr_types); - num_nodes += build_expression_tree(pyomo_args[1], oper->operand2, var_map, - param_map, expr_types); - } else if (appsi_expr->is_unary_operator()) { - num_nodes += 1; - std::shared_ptr oper = - std::dynamic_pointer_cast(appsi_expr); - py::list pyomo_args = pyomo_expr.attr("args"); - oper->operand = appsi_operator_from_pyomo_expr(pyomo_args[0], var_map, - param_map, expr_types); - num_nodes += build_expression_tree(pyomo_args[0], oper->operand, var_map, - param_map, expr_types); - } else if (appsi_expr->is_sum_operator()) { - num_nodes += 1; - std::shared_ptr oper = - std::dynamic_pointer_cast(appsi_expr); - py::list pyomo_args = pyomo_expr.attr("args"); - for (unsigned int arg_ndx = 0; arg_ndx < oper->nargs; ++arg_ndx) { - oper->operands[arg_ndx] = appsi_operator_from_pyomo_expr( - pyomo_args[arg_ndx], var_map, param_map, expr_types); - num_nodes += - build_expression_tree(pyomo_args[arg_ndx], oper->operands[arg_ndx], - var_map, param_map, expr_types); - } - } else if (appsi_expr->is_linear_operator()) { - num_nodes += 1; - std::shared_ptr oper = - std::dynamic_pointer_cast(appsi_expr); - oper->constant = appsi_expr_from_pyomo_expr(pyomo_expr.attr("constant"), - var_map, param_map, expr_types); - py::list pyomo_vars = pyomo_expr.attr("linear_vars"); - py::list pyomo_coefs = pyomo_expr.attr("linear_coefs"); - for (unsigned int arg_ndx = 0; arg_ndx < oper->nterms; ++arg_ndx) { - oper->variables[arg_ndx] = var_map[expr_types.id(pyomo_vars[arg_ndx])] - .cast>(); - oper->coefficients[arg_ndx] = appsi_expr_from_pyomo_expr( - pyomo_coefs[arg_ndx], var_map, param_map, expr_types); - } - } else if (appsi_expr->is_external_operator()) { - num_nodes += 1; - std::shared_ptr oper = - std::dynamic_pointer_cast(appsi_expr); - py::list pyomo_args = pyomo_expr.attr("args"); - for (unsigned int arg_ndx = 0; arg_ndx < oper->nargs; ++arg_ndx) { - oper->operands[arg_ndx] = appsi_operator_from_pyomo_expr( - pyomo_args[arg_ndx], var_map, param_map, expr_types); - num_nodes += - build_expression_tree(pyomo_args[arg_ndx], oper->operands[arg_ndx], - var_map, param_map, expr_types); - } - } else { - throw py::value_error( - "Unrecognized expression type: " + - expr_types.builtins.attr("str")(py::type::of(pyomo_expr)) - .cast()); - } - return num_nodes; -} - -std::shared_ptr -appsi_expr_from_pyomo_expr(py::handle expr, py::handle var_map, - py::handle param_map, PyomoExprTypes &expr_types) { - std::shared_ptr node = - appsi_operator_from_pyomo_expr(expr, var_map, param_map, expr_types); - int num_nodes = - build_expression_tree(expr, node, var_map, param_map, expr_types); - if (num_nodes == 0) { - return std::dynamic_pointer_cast(node); - } else { - std::shared_ptr res = std::make_shared(num_nodes); - node->fill_expression(res->operators, num_nodes); - return res; - } -} - -std::vector> -appsi_exprs_from_pyomo_exprs(py::list expr_list, py::dict var_map, - py::dict param_map) { - PyomoExprTypes expr_types = PyomoExprTypes(); - int num_exprs = expr_types.builtins.attr("len")(expr_list).cast(); - std::vector> res(num_exprs); - - int ndx = 0; - for (py::handle expr : expr_list) { - res[ndx] = appsi_expr_from_pyomo_expr(expr, var_map, param_map, expr_types); - ndx += 1; - } - return res; -} - -void process_pyomo_vars(PyomoExprTypes &expr_types, py::list pyomo_vars, - py::dict var_map, py::dict param_map, - py::dict var_attrs, py::dict rev_var_map, - py::bool_ _set_name, py::handle symbol_map, - py::handle labeler, py::bool_ _update) { - py::tuple v_attrs; - std::shared_ptr cv; - py::handle v_lb; - py::handle v_ub; - py::handle v_val; - py::tuple domain_interval; - py::handle interval_lb; - py::handle interval_ub; - py::handle interval_step; - bool v_fixed; - bool set_name = _set_name.cast(); - bool update = _update.cast(); - double domain_step; - - for (py::handle v : pyomo_vars) { - v_attrs = var_attrs[expr_types.id(v)]; - v_lb = v_attrs[1]; - v_ub = v_attrs[2]; - v_fixed = v_attrs[3].cast(); - domain_interval = v_attrs[4]; - v_val = v_attrs[5]; - - interval_lb = domain_interval[0]; - interval_ub = domain_interval[1]; - interval_step = domain_interval[2]; - domain_step = interval_step.cast(); - - if (update) { - cv = var_map[expr_types.id(v)].cast>(); - } else { - cv = std::make_shared(); - } - - if (!(v_lb.is(py::none()))) { - cv->lb = appsi_expr_from_pyomo_expr(v_lb, var_map, param_map, expr_types); - } else { - cv->lb = std::make_shared(-inf); - } - if (!(v_ub.is(py::none()))) { - cv->ub = appsi_expr_from_pyomo_expr(v_ub, var_map, param_map, expr_types); - } else { - cv->ub = std::make_shared(inf); - } - - if (!(v_val.is(py::none()))) { - cv->value = v_val.cast(); - } - - if (v_fixed) { - cv->fixed = true; - } else { - cv->fixed = false; - } - - if (set_name && !update) { - cv->name = symbol_map.attr("getSymbol")(v, labeler).cast(); - } - - if (interval_lb.is(py::none())) - cv->domain_lb = -inf; - else - cv->domain_lb = interval_lb.cast(); - if (interval_ub.is(py::none())) - cv->domain_ub = inf; - else - cv->domain_ub = interval_ub.cast(); - if (domain_step == 0) - cv->domain = continuous; - else if (domain_step == 1) { - if ((cv->domain_lb == 0) && (cv->domain_ub == 1)) - cv->domain = binary; - else - cv->domain = integers; - } else - throw py::value_error("Unrecognized domain step"); - - if (!update) { - var_map[expr_types.id(v)] = py::cast(cv); - rev_var_map[py::cast(cv)] = v; - } - } -} +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + +#include "expression.hpp" + +bool Leaf::is_leaf() { return true; } + +bool Var::is_variable_type() { return true; } + +bool Param::is_param_type() { return true; } + +bool Constant::is_constant_type() { return true; } + +bool Expression::is_expression_type() { return true; } + +double Leaf::evaluate() { return value; } + +double Var::get_lb() { + if (fixed) + return value; + else + return std::max(lb->evaluate(), domain_lb); +} + +double Var::get_ub() { + if (fixed) + return value; + else + return std::min(ub->evaluate(), domain_ub); +} + +Domain Var::get_domain() { return domain; } + +bool Operator::is_operator_type() { return true; } + +std::vector> Expression::get_operators() { + std::vector> res(n_operators); + for (unsigned int i = 0; i < n_operators; ++i) { + res[i] = operators[i]; + } + return res; +} + +double Leaf::get_value_from_array(double *val_array) { return value; } + +double Expression::get_value_from_array(double *val_array) { + return val_array[n_operators - 1]; +} + +double Operator::get_value_from_array(double *val_array) { + return val_array[index]; +} + +void MultiplyOperator::evaluate(double *values) { + values[index] = operand1->get_value_from_array(values) * + operand2->get_value_from_array(values); +} + +void ExternalOperator::evaluate(double *values) { + // It would be nice to implement this, but it will take some more work. + // This would require dynamic linking to the external function. + throw std::runtime_error("cannot evaluate ExternalOperator yet"); +} + +void LinearOperator::evaluate(double *values) { + values[index] = constant->evaluate(); + for (unsigned int i = 0; i < nterms; ++i) { + values[index] += coefficients[i]->evaluate() * variables[i]->evaluate(); + } +} + +void SumOperator::evaluate(double *values) { + values[index] = 0.0; + for (unsigned int i = 0; i < nargs; ++i) { + values[index] += operands[i]->get_value_from_array(values); + } +} + +void DivideOperator::evaluate(double *values) { + values[index] = operand1->get_value_from_array(values) / + operand2->get_value_from_array(values); +} + +void PowerOperator::evaluate(double *values) { + values[index] = std::pow(operand1->get_value_from_array(values), + operand2->get_value_from_array(values)); +} + +void NegationOperator::evaluate(double *values) { + values[index] = -operand->get_value_from_array(values); +} + +void ExpOperator::evaluate(double *values) { + values[index] = std::exp(operand->get_value_from_array(values)); +} + +void LogOperator::evaluate(double *values) { + values[index] = std::log(operand->get_value_from_array(values)); +} + +void AbsOperator::evaluate(double *values) { + values[index] = std::fabs(operand->get_value_from_array(values)); +} + +void SqrtOperator::evaluate(double *values) { + values[index] = std::pow(operand->get_value_from_array(values), 0.5); +} + +void Log10Operator::evaluate(double *values) { + values[index] = std::log10(operand->get_value_from_array(values)); +} + +void SinOperator::evaluate(double *values) { + values[index] = std::sin(operand->get_value_from_array(values)); +} + +void CosOperator::evaluate(double *values) { + values[index] = std::cos(operand->get_value_from_array(values)); +} + +void TanOperator::evaluate(double *values) { + values[index] = std::tan(operand->get_value_from_array(values)); +} + +void AsinOperator::evaluate(double *values) { + values[index] = std::asin(operand->get_value_from_array(values)); +} + +void AcosOperator::evaluate(double *values) { + values[index] = std::acos(operand->get_value_from_array(values)); +} + +void AtanOperator::evaluate(double *values) { + values[index] = std::atan(operand->get_value_from_array(values)); +} + +double Expression::evaluate() { + double *values = new double[n_operators]; + for (unsigned int i = 0; i < n_operators; ++i) { + operators[i]->index = i; + operators[i]->evaluate(values); + } + double res = get_value_from_array(values); + delete[] values; + return res; +} + +void UnaryOperator::identify_variables( + std::set> &var_set, + std::shared_ptr>> var_vec) { + if (operand->is_variable_type()) { + if (var_set.count(operand) == 0) { + var_vec->push_back(std::dynamic_pointer_cast(operand)); + var_set.insert(operand); + } + } +} + +void BinaryOperator::identify_variables( + std::set> &var_set, + std::shared_ptr>> var_vec) { + if (operand1->is_variable_type()) { + if (var_set.count(operand1) == 0) { + var_vec->push_back(std::dynamic_pointer_cast(operand1)); + var_set.insert(operand1); + } + } + if (operand2->is_variable_type()) { + if (var_set.count(operand2) == 0) { + var_vec->push_back(std::dynamic_pointer_cast(operand2)); + var_set.insert(operand2); + } + } +} + +void ExternalOperator::identify_variables( + std::set> &var_set, + std::shared_ptr>> var_vec) { + for (unsigned int i = 0; i < nargs; ++i) { + if (operands[i]->is_variable_type()) { + if (var_set.count(operands[i]) == 0) { + var_vec->push_back(std::dynamic_pointer_cast(operands[i])); + var_set.insert(operands[i]); + } + } + } +} + +void LinearOperator::identify_variables( + std::set> &var_set, + std::shared_ptr>> var_vec) { + for (unsigned int i = 0; i < nterms; ++i) { + if (var_set.count(variables[i]) == 0) { + var_vec->push_back(std::dynamic_pointer_cast(variables[i])); + var_set.insert(variables[i]); + } + } +} + +void SumOperator::identify_variables( + std::set> &var_set, + std::shared_ptr>> var_vec) { + for (unsigned int i = 0; i < nargs; ++i) { + if (operands[i]->is_variable_type()) { + if (var_set.count(operands[i]) == 0) { + var_vec->push_back(std::dynamic_pointer_cast(operands[i])); + var_set.insert(operands[i]); + } + } + } +} + +std::shared_ptr>> +Expression::identify_variables() { + std::set> var_set; + std::shared_ptr>> res = + std::make_shared>>(var_set.size()); + for (unsigned int i = 0; i < n_operators; ++i) { + operators[i]->identify_variables(var_set, res); + } + return res; +} + +std::shared_ptr>> Var::identify_variables() { + std::shared_ptr>> res = + std::make_shared>>(); + res->push_back(shared_from_this()); + return res; +} + +std::shared_ptr>> +Constant::identify_variables() { + std::shared_ptr>> res = + std::make_shared>>(); + return res; +} + +std::shared_ptr>> Param::identify_variables() { + std::shared_ptr>> res = + std::make_shared>>(); + return res; +} + +std::shared_ptr>> +Expression::identify_external_operators() { + std::set> external_set; + for (unsigned int i = 0; i < n_operators; ++i) { + if (operators[i]->is_external_operator()) { + external_set.insert(operators[i]); + } + } + std::shared_ptr>> res = + std::make_shared>>( + external_set.size()); + int ndx = 0; + for (std::shared_ptr n : external_set) { + (*res)[ndx] = std::dynamic_pointer_cast(n); + ndx += 1; + } + return res; +} + +std::shared_ptr>> +Var::identify_external_operators() { + std::shared_ptr>> res = + std::make_shared>>(); + return res; +} + +std::shared_ptr>> +Constant::identify_external_operators() { + std::shared_ptr>> res = + std::make_shared>>(); + return res; +} + +std::shared_ptr>> +Param::identify_external_operators() { + std::shared_ptr>> res = + std::make_shared>>(); + return res; +} + +int Var::get_degree_from_array(int *degree_array) { return 1; } + +int Param::get_degree_from_array(int *degree_array) { return 0; } + +int Constant::get_degree_from_array(int *degree_array) { return 0; } + +int Expression::get_degree_from_array(int *degree_array) { + return degree_array[n_operators - 1]; +} + +int Operator::get_degree_from_array(int *degree_array) { + return degree_array[index]; +} + +void LinearOperator::propagate_degree_forward(int *degrees, double *values) { + degrees[index] = 1; +} + +void SumOperator::propagate_degree_forward(int *degrees, double *values) { + int deg = 0; + int _deg; + for (unsigned int i = 0; i < nargs; ++i) { + _deg = operands[i]->get_degree_from_array(degrees); + if (_deg > deg) { + deg = _deg; + } + } + degrees[index] = deg; +} + +void MultiplyOperator::propagate_degree_forward(int *degrees, double *values) { + degrees[index] = operand1->get_degree_from_array(degrees) + + operand2->get_degree_from_array(degrees); +} + +void ExternalOperator::propagate_degree_forward(int *degrees, double *values) { + // External functions are always considered nonlinear + // Anything larger than 2 is nonlinear + degrees[index] = 3; +} + +void DivideOperator::propagate_degree_forward(int *degrees, double *values) { + // anything larger than 2 is nonlinear + degrees[index] = std::max(operand1->get_degree_from_array(degrees), + 3 * (operand2->get_degree_from_array(degrees))); +} + +void PowerOperator::propagate_degree_forward(int *degrees, double *values) { + if (operand2->get_degree_from_array(degrees) != 0) { + degrees[index] = 3; + } else { + double val2 = operand2->get_value_from_array(values); + double intpart; + if (std::modf(val2, &intpart) == 0.0) { + degrees[index] = operand1->get_degree_from_array(degrees) * (int)val2; + } else { + degrees[index] = 3; + } + } +} + +void NegationOperator::propagate_degree_forward(int *degrees, double *values) { + degrees[index] = operand->get_degree_from_array(degrees); +} + +void UnaryOperator::propagate_degree_forward(int *degrees, double *values) { + if (operand->get_degree_from_array(degrees) == 0) { + degrees[index] = 0; + } else { + degrees[index] = 3; + } +} + +std::string Var::__str__() { return name; } + +std::string Param::__str__() { return name; } + +std::string Constant::__str__() { return std::to_string(value); } + +std::string Expression::__str__() { + std::string *string_array = new std::string[n_operators]; + std::shared_ptr oper; + for (unsigned int i = 0; i < n_operators; ++i) { + oper = operators[i]; + oper->index = i; + oper->print(string_array); + } + std::string res = string_array[n_operators - 1]; + delete[] string_array; + return res; +} + +std::string Leaf::get_string_from_array(std::string *string_array) { + return __str__(); +} + +std::string Expression::get_string_from_array(std::string *string_array) { + return string_array[n_operators - 1]; +} + +std::string Operator::get_string_from_array(std::string *string_array) { + return string_array[index]; +} + +void MultiplyOperator::print(std::string *string_array) { + string_array[index] = + ("(" + operand1->get_string_from_array(string_array) + "*" + + operand2->get_string_from_array(string_array) + ")"); +} + +void ExternalOperator::print(std::string *string_array) { + std::string res = function_name + "("; + for (unsigned int i = 0; i < (nargs - 1); ++i) { + res += operands[i]->get_string_from_array(string_array); + res += ", "; + } + res += operands[nargs - 1]->get_string_from_array(string_array); + res += ")"; + string_array[index] = res; +} + +void DivideOperator::print(std::string *string_array) { + string_array[index] = + ("(" + operand1->get_string_from_array(string_array) + "/" + + operand2->get_string_from_array(string_array) + ")"); +} + +void PowerOperator::print(std::string *string_array) { + string_array[index] = + ("(" + operand1->get_string_from_array(string_array) + "**" + + operand2->get_string_from_array(string_array) + ")"); +} + +void NegationOperator::print(std::string *string_array) { + string_array[index] = + ("(-" + operand->get_string_from_array(string_array) + ")"); +} + +void ExpOperator::print(std::string *string_array) { + string_array[index] = + ("exp(" + operand->get_string_from_array(string_array) + ")"); +} + +void LogOperator::print(std::string *string_array) { + string_array[index] = + ("log(" + operand->get_string_from_array(string_array) + ")"); +} + +void AbsOperator::print(std::string *string_array) { + string_array[index] = + ("abs(" + operand->get_string_from_array(string_array) + ")"); +} + +void SqrtOperator::print(std::string *string_array) { + string_array[index] = + ("sqrt(" + operand->get_string_from_array(string_array) + ")"); +} + +void Log10Operator::print(std::string *string_array) { + string_array[index] = + ("log10(" + operand->get_string_from_array(string_array) + ")"); +} + +void SinOperator::print(std::string *string_array) { + string_array[index] = + ("sin(" + operand->get_string_from_array(string_array) + ")"); +} + +void CosOperator::print(std::string *string_array) { + string_array[index] = + ("cos(" + operand->get_string_from_array(string_array) + ")"); +} + +void TanOperator::print(std::string *string_array) { + string_array[index] = + ("tan(" + operand->get_string_from_array(string_array) + ")"); +} + +void AsinOperator::print(std::string *string_array) { + string_array[index] = + ("asin(" + operand->get_string_from_array(string_array) + ")"); +} + +void AcosOperator::print(std::string *string_array) { + string_array[index] = + ("acos(" + operand->get_string_from_array(string_array) + ")"); +} + +void AtanOperator::print(std::string *string_array) { + string_array[index] = + ("atan(" + operand->get_string_from_array(string_array) + ")"); +} + +void LinearOperator::print(std::string *string_array) { + std::string res = "(" + constant->__str__(); + for (unsigned int i = 0; i < nterms; ++i) { + res += " + " + coefficients[i]->__str__() + "*" + variables[i]->__str__(); + } + res += ")"; + string_array[index] = res; +} + +void SumOperator::print(std::string *string_array) { + std::string res = "(" + operands[0]->get_string_from_array(string_array); + for (unsigned int i = 1; i < nargs; ++i) { + res += " + " + operands[i]->get_string_from_array(string_array); + } + res += ")"; + string_array[index] = res; +} + +std::shared_ptr>> +Leaf::get_prefix_notation() { + std::shared_ptr>> res = + std::make_shared>>(); + res->push_back(shared_from_this()); + return res; +} + +std::shared_ptr>> +Expression::get_prefix_notation() { + std::shared_ptr>> res = + std::make_shared>>(); + std::shared_ptr>> stack = + std::make_shared>>(); + std::shared_ptr node; + stack->push_back(operators[n_operators - 1]); + while (stack->size() > 0) { + node = stack->back(); + stack->pop_back(); + res->push_back(node); + node->fill_prefix_notation_stack(stack); + } + + return res; +} + +void BinaryOperator::fill_prefix_notation_stack( + std::shared_ptr>> stack) { + stack->push_back(operand2); + stack->push_back(operand1); +} + +void UnaryOperator::fill_prefix_notation_stack( + std::shared_ptr>> stack) { + stack->push_back(operand); +} + +void SumOperator::fill_prefix_notation_stack( + std::shared_ptr>> stack) { + int ndx = nargs - 1; + while (ndx >= 0) { + stack->push_back(operands[ndx]); + ndx -= 1; + } +} + +void LinearOperator::fill_prefix_notation_stack( + std::shared_ptr>> stack) { + ; // This is treated as a leaf in this context; write_nl_string will take care + // of it +} + +void ExternalOperator::fill_prefix_notation_stack( + std::shared_ptr>> stack) { + int i = nargs - 1; + while (i >= 0) { + stack->push_back(operands[i]); + i -= 1; + } +} + +void Var::write_nl_string(std::ofstream &f) { f << "v" << index << "\n"; } + +void Param::write_nl_string(std::ofstream &f) { f << "n" << value << "\n"; } + +void Constant::write_nl_string(std::ofstream &f) { f << "n" << value << "\n"; } + +void Expression::write_nl_string(std::ofstream &f) { + std::shared_ptr>> prefix_notation = + get_prefix_notation(); + for (std::shared_ptr &node : *(prefix_notation)) { + node->write_nl_string(f); + } +} + +void MultiplyOperator::write_nl_string(std::ofstream &f) { f << "o2\n"; } + +void ExternalOperator::write_nl_string(std::ofstream &f) { + f << "f" << external_function_index << " " << nargs << "\n"; +} + +void SumOperator::write_nl_string(std::ofstream &f) { + if (nargs == 2) { + f << "o0\n"; + } else { + f << "o54\n"; + f << nargs << "\n"; + } +} + +void LinearOperator::write_nl_string(std::ofstream &f) { + bool has_const = + (!constant->is_constant_type()) || (constant->evaluate() != 0); + unsigned int n_sum_args = nterms + (has_const ? 1 : 0); + if (n_sum_args == 2) { + f << "o0\n"; + } else { + f << "o54\n"; + f << n_sum_args << "\n"; + } + if (has_const) + f << "n" << constant->evaluate() << "\n"; + for (unsigned int ndx = 0; ndx < nterms; ++ndx) { + f << "o2\n"; + f << "n" << coefficients[ndx]->evaluate() << "\n"; + variables[ndx]->write_nl_string(f); + } +} + +void DivideOperator::write_nl_string(std::ofstream &f) { f << "o3\n"; } + +void PowerOperator::write_nl_string(std::ofstream &f) { f << "o5\n"; } + +void NegationOperator::write_nl_string(std::ofstream &f) { f << "o16\n"; } + +void ExpOperator::write_nl_string(std::ofstream &f) { f << "o44\n"; } + +void LogOperator::write_nl_string(std::ofstream &f) { f << "o43\n"; } + +void AbsOperator::write_nl_string(std::ofstream &f) { f << "o15\n"; } + +void SqrtOperator::write_nl_string(std::ofstream &f) { f << "o39\n"; } + +void Log10Operator::write_nl_string(std::ofstream &f) { f << "o42\n"; } + +void SinOperator::write_nl_string(std::ofstream &f) { f << "o41\n"; } + +void CosOperator::write_nl_string(std::ofstream &f) { f << "o46\n"; } + +void TanOperator::write_nl_string(std::ofstream &f) { f << "o38\n"; } + +void AsinOperator::write_nl_string(std::ofstream &f) { f << "o51\n"; } + +void AcosOperator::write_nl_string(std::ofstream &f) { f << "o53\n"; } + +void AtanOperator::write_nl_string(std::ofstream &f) { f << "o49\n"; } + +bool BinaryOperator::is_binary_operator() { return true; } + +bool UnaryOperator::is_unary_operator() { return true; } + +bool LinearOperator::is_linear_operator() { return true; } + +bool SumOperator::is_sum_operator() { return true; } + +bool MultiplyOperator::is_multiply_operator() { return true; } + +bool DivideOperator::is_divide_operator() { return true; } + +bool PowerOperator::is_power_operator() { return true; } + +bool NegationOperator::is_negation_operator() { return true; } + +bool ExpOperator::is_exp_operator() { return true; } + +bool LogOperator::is_log_operator() { return true; } + +bool AbsOperator::is_abs_operator() { return true; } + +bool SqrtOperator::is_sqrt_operator() { return true; } + +bool ExternalOperator::is_external_operator() { return true; } + +void Leaf::fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) { + ; +} + +void Expression::fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) { + throw std::runtime_error("This should not happen"); +} + +void BinaryOperator::fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) { + oper_ndx -= 1; + oper_array[oper_ndx] = shared_from_this(); + // The order does not actually matter here. It + // will just be easier to debug this way. + operand2->fill_expression(oper_array, oper_ndx); + operand1->fill_expression(oper_array, oper_ndx); +} + +void UnaryOperator::fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) { + oper_ndx -= 1; + oper_array[oper_ndx] = shared_from_this(); + operand->fill_expression(oper_array, oper_ndx); +} + +void LinearOperator::fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) { + oper_ndx -= 1; + oper_array[oper_ndx] = shared_from_this(); +} + +void SumOperator::fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) { + oper_ndx -= 1; + oper_array[oper_ndx] = shared_from_this(); + // The order does not actually matter here. It + // will just be easier to debug this way. + int arg_ndx = nargs - 1; + while (arg_ndx >= 0) { + operands[arg_ndx]->fill_expression(oper_array, oper_ndx); + arg_ndx -= 1; + } +} + +void ExternalOperator::fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) { + oper_ndx -= 1; + oper_array[oper_ndx] = shared_from_this(); + // The order does not actually matter here. It + // will just be easier to debug this way. + int arg_ndx = nargs - 1; + while (arg_ndx >= 0) { + operands[arg_ndx]->fill_expression(oper_array, oper_ndx); + arg_ndx -= 1; + } +} + +double Leaf::get_lb_from_array(double *lbs) { return value; } + +double Leaf::get_ub_from_array(double *ubs) { return value; } + +double Var::get_lb_from_array(double *lbs) { return get_lb(); } + +double Var::get_ub_from_array(double *ubs) { return get_ub(); } + +double Expression::get_lb_from_array(double *lbs) { + return lbs[n_operators - 1]; +} + +double Expression::get_ub_from_array(double *ubs) { + return ubs[n_operators - 1]; +} + +double Operator::get_lb_from_array(double *lbs) { return lbs[index]; } + +double Operator::get_ub_from_array(double *ubs) { return ubs[index]; } + +void Leaf::set_bounds_in_array(double new_lb, double new_ub, double *lbs, + double *ubs, double feasibility_tol, + double integer_tol, double improvement_tol, + std::set> &improved_vars) { + if (new_lb < value - feasibility_tol || new_lb > value + feasibility_tol) { + throw InfeasibleConstraintException( + "Infeasible constraint; bounds computed on parameter or constant " + "disagree with the value of the parameter or constant\n value: " + + std::to_string(value) + "\n computed LB: " + std::to_string(new_lb) + + "\n computed UB: " + std::to_string(new_ub)); + } + + if (new_ub < value - feasibility_tol || new_ub > value + feasibility_tol) { + throw InfeasibleConstraintException( + "Infeasible constraint; bounds computed on parameter or constant " + "disagree with the value of the parameter or constant\n value: " + + std::to_string(value) + "\n computed LB: " + std::to_string(new_lb) + + "\n computed UB: " + std::to_string(new_ub)); + } +} + +void Var::set_bounds_in_array(double new_lb, double new_ub, double *lbs, + double *ubs, double feasibility_tol, + double integer_tol, double improvement_tol, + std::set> &improved_vars) { + if (new_lb > new_ub) { + if (new_lb - feasibility_tol > new_ub) + throw InfeasibleConstraintException( + "Infeasible constraint; The computed lower bound for a variable is " + "larger than the computed upper bound.\n computed LB: " + + std::to_string(new_lb) + + "\n computed UB: " + std::to_string(new_ub)); + else { + new_lb -= feasibility_tol; + new_ub += feasibility_tol; + } + } + if (new_lb >= inf) + throw InfeasibleConstraintException( + "Infeasible constraint; The compute lower bound for " + name + + " is inf"); + if (new_ub <= -inf) + throw InfeasibleConstraintException( + "Infeasible constraint; The computed upper bound for " + name + + " is -inf"); + + if (domain == integers || domain == binary) { + if (new_lb > -inf) { + double lb_floor = floor(new_lb); + double lb_ceil = ceil(new_lb - integer_tol); + if (lb_floor > lb_ceil) + new_lb = lb_floor; + else + new_lb = lb_ceil; + } + if (new_ub < inf) { + double ub_ceil = ceil(new_ub); + double ub_floor = floor(new_ub + integer_tol); + if (ub_ceil < ub_floor) + new_ub = ub_ceil; + else + new_ub = ub_floor; + } + } + + double current_lb = get_lb(); + double current_ub = get_ub(); + + if (new_lb > current_lb + improvement_tol || + new_ub < current_ub - improvement_tol) + improved_vars.insert(shared_from_this()); + + if (new_lb > current_lb) { + if (lb->is_leaf()) + std::dynamic_pointer_cast(lb)->value = new_lb; + else + throw py::value_error( + "variable bounds cannot be expressions when performing FBBT"); + } + + if (new_ub < current_ub) { + if (ub->is_leaf()) + std::dynamic_pointer_cast(ub)->value = new_ub; + else + throw py::value_error( + "variable bounds cannot be expressions when performing FBBT"); + } +} + +void Expression::set_bounds_in_array( + double new_lb, double new_ub, double *lbs, double *ubs, + double feasibility_tol, double integer_tol, double improvement_tol, + std::set> &improved_vars) { + lbs[n_operators - 1] = new_lb; + ubs[n_operators - 1] = new_ub; +} + +void Operator::set_bounds_in_array( + double new_lb, double new_ub, double *lbs, double *ubs, + double feasibility_tol, double integer_tol, double improvement_tol, + std::set> &improved_vars) { + lbs[index] = new_lb; + ubs[index] = new_ub; +} + +void Expression::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + for (unsigned int ndx = 0; ndx < n_operators; ++ndx) { + operators[ndx]->index = ndx; + operators[ndx]->propagate_bounds_forward(lbs, ubs, feasibility_tol, + integer_tol); + } +} + +void Expression::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + int ndx = n_operators - 1; + while (ndx >= 0) { + operators[ndx]->propagate_bounds_backward( + lbs, ubs, feasibility_tol, integer_tol, improvement_tol, improved_vars); + ndx -= 1; + } +} + +void Operator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + lbs[index] = -inf; + ubs[index] = inf; +} + +void Operator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + ; +} + +void MultiplyOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + if (operand1 == operand2) { + interval_power(operand1->get_lb_from_array(lbs), + operand1->get_ub_from_array(ubs), 2, 2, &lbs[index], + &ubs[index], feasibility_tol); + } else { + interval_mul(operand1->get_lb_from_array(lbs), + operand1->get_ub_from_array(ubs), + operand2->get_lb_from_array(lbs), + operand2->get_ub_from_array(ubs), &lbs[index], &ubs[index]); + } +} + +void MultiplyOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand1->get_lb_from_array(lbs); + double xu = operand1->get_ub_from_array(ubs); + double yl = operand2->get_lb_from_array(lbs); + double yu = operand2->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu, new_yl, new_yu; + + if (operand1 == operand2) { + _inverse_power1(lb, ub, 2, 2, xl, xu, &new_xl, &new_xu, feasibility_tol); + new_yl = new_xl; + new_yu = new_xu; + } else { + interval_div(lb, ub, yl, yu, &new_xl, &new_xu, feasibility_tol); + interval_div(lb, ub, xl, xu, &new_yl, &new_yu, feasibility_tol); + } + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand1->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); + + if (new_yl > yl) + yl = new_yl; + if (new_yu < yu) + yu = new_yu; + operand2->set_bounds_in_array(yl, yu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void SumOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + double lb = operands[0]->get_lb_from_array(lbs); + double ub = operands[0]->get_ub_from_array(ubs); + double tmp_lb; + double tmp_ub; + + for (unsigned int ndx = 1; ndx < nargs; ++ndx) { + interval_add(lb, ub, operands[ndx]->get_lb_from_array(lbs), + operands[ndx]->get_ub_from_array(ubs), &tmp_lb, &tmp_ub); + lb = tmp_lb; + ub = tmp_ub; + } + + lbs[index] = lb; + ubs[index] = ub; +} + +void SumOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double *accumulated_lbs = new double[nargs]; + double *accumulated_ubs = new double[nargs]; + + accumulated_lbs[0] = operands[0]->get_lb_from_array(lbs); + accumulated_ubs[0] = operands[0]->get_ub_from_array(ubs); + for (unsigned int ndx = 1; ndx < nargs; ++ndx) { + interval_add(accumulated_lbs[ndx - 1], accumulated_ubs[ndx - 1], + operands[ndx]->get_lb_from_array(lbs), + operands[ndx]->get_ub_from_array(ubs), &accumulated_lbs[ndx], + &accumulated_ubs[ndx]); + } + + double new_sum_lb = get_lb_from_array(lbs); + double new_sum_ub = get_ub_from_array(ubs); + + if (new_sum_lb > accumulated_lbs[nargs - 1]) + accumulated_lbs[nargs - 1] = new_sum_lb; + if (new_sum_ub < accumulated_ubs[nargs - 1]) + accumulated_ubs[nargs - 1] = new_sum_ub; + + double lb0, ub0, lb1, ub1, lb2, ub2, _lb1, _ub1, _lb2, _ub2; + + int ndx = nargs - 1; + while (ndx >= 1) { + lb0 = accumulated_lbs[ndx]; + ub0 = accumulated_ubs[ndx]; + lb1 = accumulated_lbs[ndx - 1]; + ub1 = accumulated_ubs[ndx - 1]; + lb2 = operands[ndx]->get_lb_from_array(lbs); + ub2 = operands[ndx]->get_ub_from_array(ubs); + interval_sub(lb0, ub0, lb2, ub2, &_lb1, &_ub1); + interval_sub(lb0, ub0, lb1, ub1, &_lb2, &_ub2); + if (_lb1 > lb1) + lb1 = _lb1; + if (_ub1 < ub1) + ub1 = _ub1; + if (_lb2 > lb2) + lb2 = _lb2; + if (_ub2 < ub2) + ub2 = _ub2; + accumulated_lbs[ndx - 1] = lb1; + accumulated_ubs[ndx - 1] = ub1; + operands[ndx]->set_bounds_in_array(lb2, ub2, lbs, ubs, feasibility_tol, + integer_tol, improvement_tol, + improved_vars); + ndx -= 1; + } + + // take care of ndx = 0 + lb1 = operands[0]->get_lb_from_array(lbs); + ub1 = operands[0]->get_ub_from_array(ubs); + _lb1 = accumulated_lbs[0]; + _ub1 = accumulated_ubs[0]; + if (_lb1 > lb1) + lb1 = _lb1; + if (_ub1 < ub1) + ub1 = _ub1; + operands[0]->set_bounds_in_array(lb1, ub1, lbs, ubs, feasibility_tol, + integer_tol, improvement_tol, improved_vars); + + delete[] accumulated_lbs; + delete[] accumulated_ubs; +} + +void LinearOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + double lb = constant->evaluate(); + double ub = lb; + double tmp_lb; + double tmp_ub; + double coef; + + for (unsigned int ndx = 0; ndx < nterms; ++ndx) { + coef = coefficients[ndx]->evaluate(); + interval_mul(coef, coef, variables[ndx]->get_lb(), variables[ndx]->get_ub(), + &tmp_lb, &tmp_ub); + interval_add(lb, ub, tmp_lb, tmp_ub, &lb, &ub); + } + + lbs[index] = lb; + ubs[index] = ub; +} + +void LinearOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double *accumulated_lbs = new double[nterms + 1]; + double *accumulated_ubs = new double[nterms + 1]; + + double coef; + + accumulated_lbs[0] = constant->evaluate(); + accumulated_ubs[0] = constant->evaluate(); + for (unsigned int ndx = 0; ndx < nterms; ++ndx) { + coef = coefficients[ndx]->evaluate(); + interval_mul(coef, coef, variables[ndx]->get_lb(), variables[ndx]->get_ub(), + &accumulated_lbs[ndx + 1], &accumulated_ubs[ndx + 1]); + interval_add(accumulated_lbs[ndx], accumulated_ubs[ndx], + accumulated_lbs[ndx + 1], accumulated_ubs[ndx + 1], + &accumulated_lbs[ndx + 1], &accumulated_ubs[ndx + 1]); + } + + double new_sum_lb = get_lb_from_array(lbs); + double new_sum_ub = get_ub_from_array(ubs); + + if (new_sum_lb > accumulated_lbs[nterms]) + accumulated_lbs[nterms] = new_sum_lb; + if (new_sum_ub < accumulated_ubs[nterms]) + accumulated_ubs[nterms] = new_sum_ub; + + double lb0, ub0, lb1, ub1, lb2, ub2, _lb1, _ub1, _lb2, _ub2, new_v_lb, + new_v_ub; + + int ndx = nterms - 1; + while (ndx >= 0) { + lb0 = accumulated_lbs[ndx + 1]; + ub0 = accumulated_ubs[ndx + 1]; + lb1 = accumulated_lbs[ndx]; + ub1 = accumulated_ubs[ndx]; + coef = coefficients[ndx]->evaluate(); + interval_mul(coef, coef, variables[ndx]->get_lb(), variables[ndx]->get_ub(), + &lb2, &ub2); + interval_sub(lb0, ub0, lb2, ub2, &_lb1, &_ub1); + interval_sub(lb0, ub0, lb1, ub1, &_lb2, &_ub2); + if (_lb1 > lb1) + lb1 = _lb1; + if (_ub1 < ub1) + ub1 = _ub1; + if (_lb2 > lb2) + lb2 = _lb2; + if (_ub2 < ub2) + ub2 = _ub2; + accumulated_lbs[ndx] = lb1; + accumulated_ubs[ndx] = ub1; + interval_div(lb2, ub2, coef, coef, &new_v_lb, &new_v_ub, feasibility_tol); + variables[ndx]->set_bounds_in_array(new_v_lb, new_v_ub, lbs, ubs, + feasibility_tol, integer_tol, + improvement_tol, improved_vars); + ndx -= 1; + } + + delete[] accumulated_lbs; + delete[] accumulated_ubs; +} + +void DivideOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_div( + operand1->get_lb_from_array(lbs), operand1->get_ub_from_array(ubs), + operand2->get_lb_from_array(lbs), operand2->get_ub_from_array(ubs), + &lbs[index], &ubs[index], feasibility_tol); +} + +void DivideOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand1->get_lb_from_array(lbs); + double xu = operand1->get_ub_from_array(ubs); + double yl = operand2->get_lb_from_array(lbs); + double yu = operand2->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl; + double new_xu; + double new_yl; + double new_yu; + + interval_mul(lb, ub, yl, yu, &new_xl, &new_xu); + interval_div(xl, xu, lb, ub, &new_yl, &new_yu, feasibility_tol); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand1->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); + + if (new_yl > yl) + yl = new_yl; + if (new_yu < yu) + yu = new_yu; + operand2->set_bounds_in_array(yl, yu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void NegationOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_sub(0, 0, operand->get_lb_from_array(lbs), + operand->get_ub_from_array(ubs), &lbs[index], &ubs[index]); +} + +void NegationOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl; + double new_xu; + + interval_sub(0, 0, lb, ub, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void PowerOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_power( + operand1->get_lb_from_array(lbs), operand1->get_ub_from_array(ubs), + operand2->get_lb_from_array(lbs), operand2->get_ub_from_array(ubs), + &lbs[index], &ubs[index], feasibility_tol); +} + +void PowerOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand1->get_lb_from_array(lbs); + double xu = operand1->get_ub_from_array(ubs); + double yl = operand2->get_lb_from_array(lbs); + double yu = operand2->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu, new_yl, new_yu; + _inverse_power1(lb, ub, yl, yu, xl, xu, &new_xl, &new_xu, feasibility_tol); + if (yl != yu) + _inverse_power2(lb, ub, xl, xu, &new_yl, &new_yu, feasibility_tol); + else { + new_yl = yl; + new_yu = yu; + } + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand1->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); + + if (new_yl > yl) + yl = new_yl; + if (new_yu < yu) + yu = new_yu; + operand2->set_bounds_in_array(yl, yu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void SqrtOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_power(operand->get_lb_from_array(lbs), + operand->get_ub_from_array(ubs), 0.5, 0.5, &lbs[index], + &ubs[index], feasibility_tol); +} + +void SqrtOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double yl = 0.5; + double yu = 0.5; + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + _inverse_power1(lb, ub, yl, yu, xl, xu, &new_xl, &new_xu, feasibility_tol); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void ExpOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_exp(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), + &lbs[index], &ubs[index]); +} + +void ExpOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_log(lb, ub, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void LogOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_log(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), + &lbs[index], &ubs[index]); +} + +void LogOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_exp(lb, ub, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void AbsOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_abs(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), + &lbs[index], &ubs[index]); +} + +void AbsOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + _inverse_abs(lb, ub, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void Log10Operator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_log10(operand->get_lb_from_array(lbs), + operand->get_ub_from_array(ubs), &lbs[index], &ubs[index]); +} + +void Log10Operator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_power(10, 10, lb, ub, &new_xl, &new_xu, feasibility_tol); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void SinOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_sin(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), + &lbs[index], &ubs[index]); +} + +void SinOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_asin(lb, ub, xl, xu, &new_xl, &new_xu, feasibility_tol); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void CosOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_cos(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), + &lbs[index], &ubs[index]); +} + +void CosOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_acos(lb, ub, xl, xu, &new_xl, &new_xu, feasibility_tol); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void TanOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_tan(operand->get_lb_from_array(lbs), operand->get_ub_from_array(ubs), + &lbs[index], &ubs[index]); +} + +void TanOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_atan(lb, ub, xl, xu, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void AsinOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_asin(operand->get_lb_from_array(lbs), + operand->get_ub_from_array(ubs), -inf, inf, &lbs[index], + &ubs[index], feasibility_tol); +} + +void AsinOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_sin(lb, ub, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void AcosOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_acos(operand->get_lb_from_array(lbs), + operand->get_ub_from_array(ubs), -inf, inf, &lbs[index], + &ubs[index], feasibility_tol); +} + +void AcosOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_cos(lb, ub, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +void AtanOperator::propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) { + interval_atan(operand->get_lb_from_array(lbs), + operand->get_ub_from_array(ubs), -inf, inf, &lbs[index], + &ubs[index]); +} + +void AtanOperator::propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, std::set> &improved_vars) { + double xl = operand->get_lb_from_array(lbs); + double xu = operand->get_ub_from_array(ubs); + double lb = get_lb_from_array(lbs); + double ub = get_ub_from_array(ubs); + + double new_xl, new_xu; + interval_tan(lb, ub, &new_xl, &new_xu); + + if (new_xl > xl) + xl = new_xl; + if (new_xu < xu) + xu = new_xu; + operand->set_bounds_in_array(xl, xu, lbs, ubs, feasibility_tol, integer_tol, + improvement_tol, improved_vars); +} + +std::vector> create_vars(int n_vars) { + std::vector> res; + for (int i = 0; i < n_vars; ++i) { + res.push_back(std::make_shared()); + } + return res; +} + +std::vector> create_params(int n_params) { + std::vector> res; + for (int i = 0; i < n_params; ++i) { + res.push_back(std::make_shared()); + } + return res; +} + +std::vector> create_constants(int n_constants) { + std::vector> res; + for (int i = 0; i < n_constants; ++i) { + res.push_back(std::make_shared()); + } + return res; +} + +std::shared_ptr +appsi_operator_from_pyomo_expr(py::handle expr, py::handle var_map, + py::handle param_map, + PyomoExprTypes &expr_types) { + std::shared_ptr res; + ExprType tmp_type = + expr_types.expr_type_map[py::type::of(expr)].cast(); + + switch (tmp_type) { + case py_float: { + res = std::make_shared(expr.cast()); + break; + } + case var: { + res = var_map[expr_types.id(expr)].cast>(); + break; + } + case param: { + res = param_map[expr_types.id(expr)].cast>(); + break; + } + case product: { + res = std::make_shared(); + break; + } + case sum: { + res = std::make_shared(expr.attr("nargs")().cast()); + break; + } + case negation: { + res = std::make_shared(); + break; + } + case external_func: { + res = std::make_shared(expr.attr("nargs")().cast()); + std::shared_ptr oper = + std::dynamic_pointer_cast(res); + oper->function_name = + expr.attr("_fcn").attr("_function").cast(); + break; + } + case power: { + res = std::make_shared(); + break; + } + case division: { + res = std::make_shared(); + break; + } + case unary_func: { + std::string function_name = expr.attr("getname")().cast(); + if (function_name == "exp") + res = std::make_shared(); + else if (function_name == "log") + res = std::make_shared(); + else if (function_name == "log10") + res = std::make_shared(); + else if (function_name == "sin") + res = std::make_shared(); + else if (function_name == "cos") + res = std::make_shared(); + else if (function_name == "tan") + res = std::make_shared(); + else if (function_name == "asin") + res = std::make_shared(); + else if (function_name == "acos") + res = std::make_shared(); + else if (function_name == "atan") + res = std::make_shared(); + else if (function_name == "sqrt") + res = std::make_shared(); + else + throw py::value_error("Unrecognized expression type: " + function_name); + break; + } + case linear: { + res = std::make_shared( + expr_types.len(expr.attr("linear_vars")).cast()); + break; + } + case named_expr: { + res = appsi_operator_from_pyomo_expr(expr.attr("expr"), var_map, param_map, + expr_types); + break; + } + case numeric_constant: { + res = std::make_shared(expr.attr("value").cast()); + break; + } + case pyomo_unit: { + res = std::make_shared(1.0); + break; + } + case unary_abs: { + res = std::make_shared(); + break; + } + default: { + throw py::value_error("Unrecognized expression type: " + + expr_types.builtins.attr("str")(py::type::of(expr)) + .cast()); + break; + } + } + return res; +} + +void prep_for_repn_helper(py::handle expr, py::handle named_exprs, + py::handle variables, py::handle fixed_vars, + py::handle external_funcs, + PyomoExprTypes &expr_types) { + ExprType tmp_type = + expr_types.expr_type_map[py::type::of(expr)].cast(); + + switch (tmp_type) { + case py_float: { + break; + } + case var: { + variables[expr_types.id(expr)] = expr; + if (expr.attr("fixed").cast()) { + fixed_vars[expr_types.id(expr)] = expr; + } + break; + } + case param: { + break; + } + case product: { + py::tuple args = expr.attr("_args_"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + case sum: { + py::tuple args = expr.attr("args"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + case negation: { + py::tuple args = expr.attr("_args_"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + case external_func: { + external_funcs[expr_types.id(expr)] = expr; + py::tuple args = expr.attr("args"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + case power: { + py::tuple args = expr.attr("_args_"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + case division: { + py::tuple args = expr.attr("_args_"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + case unary_func: { + py::tuple args = expr.attr("_args_"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + case linear: { + py::list linear_vars = expr.attr("linear_vars"); + py::list linear_coefs = expr.attr("linear_coefs"); + for (py::handle arg : linear_vars) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + for (py::handle arg : linear_coefs) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + prep_for_repn_helper(expr.attr("constant"), named_exprs, variables, + fixed_vars, external_funcs, expr_types); + break; + } + case named_expr: { + named_exprs[expr_types.id(expr)] = expr; + prep_for_repn_helper(expr.attr("expr"), named_exprs, variables, fixed_vars, + external_funcs, expr_types); + break; + } + case numeric_constant: { + break; + } + case pyomo_unit: { + break; + } + case unary_abs: { + py::tuple args = expr.attr("_args_"); + for (py::handle arg : args) { + prep_for_repn_helper(arg, named_exprs, variables, fixed_vars, + external_funcs, expr_types); + } + break; + } + default: { + if (expr_types.builtins.attr("hasattr")(expr, "is_constant").cast()) { + if (expr.attr("is_constant")().cast()) + break; + } + throw py::value_error("Unrecognized expression type: " + + expr_types.builtins.attr("str")(py::type::of(expr)) + .cast()); + break; + } + } +} + +py::tuple prep_for_repn(py::handle expr, PyomoExprTypes &expr_types) { + py::dict named_exprs; + py::dict variables; + py::dict fixed_vars; + py::dict external_funcs; + + prep_for_repn_helper(expr, named_exprs, variables, fixed_vars, external_funcs, + expr_types); + + py::list named_expr_list = named_exprs.attr("values")(); + py::list variable_list = variables.attr("values")(); + py::list fixed_var_list = fixed_vars.attr("values")(); + py::list external_func_list = external_funcs.attr("values")(); + + py::tuple res = py::make_tuple(named_expr_list, variable_list, fixed_var_list, + external_func_list); + return res; +} + +int build_expression_tree(py::handle pyomo_expr, + std::shared_ptr appsi_expr, py::handle var_map, + py::handle param_map, PyomoExprTypes &expr_types) { + int num_nodes = 0; + + if (expr_types.expr_type_map[py::type::of(pyomo_expr)].cast() == + named_expr) + pyomo_expr = pyomo_expr.attr("expr"); + + if (appsi_expr->is_leaf()) { + ; + } else if (appsi_expr->is_binary_operator()) { + num_nodes += 1; + std::shared_ptr oper = + std::dynamic_pointer_cast(appsi_expr); + py::list pyomo_args = pyomo_expr.attr("args"); + oper->operand1 = appsi_operator_from_pyomo_expr(pyomo_args[0], var_map, + param_map, expr_types); + oper->operand2 = appsi_operator_from_pyomo_expr(pyomo_args[1], var_map, + param_map, expr_types); + num_nodes += build_expression_tree(pyomo_args[0], oper->operand1, var_map, + param_map, expr_types); + num_nodes += build_expression_tree(pyomo_args[1], oper->operand2, var_map, + param_map, expr_types); + } else if (appsi_expr->is_unary_operator()) { + num_nodes += 1; + std::shared_ptr oper = + std::dynamic_pointer_cast(appsi_expr); + py::list pyomo_args = pyomo_expr.attr("args"); + oper->operand = appsi_operator_from_pyomo_expr(pyomo_args[0], var_map, + param_map, expr_types); + num_nodes += build_expression_tree(pyomo_args[0], oper->operand, var_map, + param_map, expr_types); + } else if (appsi_expr->is_sum_operator()) { + num_nodes += 1; + std::shared_ptr oper = + std::dynamic_pointer_cast(appsi_expr); + py::list pyomo_args = pyomo_expr.attr("args"); + for (unsigned int arg_ndx = 0; arg_ndx < oper->nargs; ++arg_ndx) { + oper->operands[arg_ndx] = appsi_operator_from_pyomo_expr( + pyomo_args[arg_ndx], var_map, param_map, expr_types); + num_nodes += + build_expression_tree(pyomo_args[arg_ndx], oper->operands[arg_ndx], + var_map, param_map, expr_types); + } + } else if (appsi_expr->is_linear_operator()) { + num_nodes += 1; + std::shared_ptr oper = + std::dynamic_pointer_cast(appsi_expr); + oper->constant = appsi_expr_from_pyomo_expr(pyomo_expr.attr("constant"), + var_map, param_map, expr_types); + py::list pyomo_vars = pyomo_expr.attr("linear_vars"); + py::list pyomo_coefs = pyomo_expr.attr("linear_coefs"); + for (unsigned int arg_ndx = 0; arg_ndx < oper->nterms; ++arg_ndx) { + oper->variables[arg_ndx] = var_map[expr_types.id(pyomo_vars[arg_ndx])] + .cast>(); + oper->coefficients[arg_ndx] = appsi_expr_from_pyomo_expr( + pyomo_coefs[arg_ndx], var_map, param_map, expr_types); + } + } else if (appsi_expr->is_external_operator()) { + num_nodes += 1; + std::shared_ptr oper = + std::dynamic_pointer_cast(appsi_expr); + py::list pyomo_args = pyomo_expr.attr("args"); + for (unsigned int arg_ndx = 0; arg_ndx < oper->nargs; ++arg_ndx) { + oper->operands[arg_ndx] = appsi_operator_from_pyomo_expr( + pyomo_args[arg_ndx], var_map, param_map, expr_types); + num_nodes += + build_expression_tree(pyomo_args[arg_ndx], oper->operands[arg_ndx], + var_map, param_map, expr_types); + } + } else { + throw py::value_error( + "Unrecognized expression type: " + + expr_types.builtins.attr("str")(py::type::of(pyomo_expr)) + .cast()); + } + return num_nodes; +} + +std::shared_ptr +appsi_expr_from_pyomo_expr(py::handle expr, py::handle var_map, + py::handle param_map, PyomoExprTypes &expr_types) { + std::shared_ptr node = + appsi_operator_from_pyomo_expr(expr, var_map, param_map, expr_types); + int num_nodes = + build_expression_tree(expr, node, var_map, param_map, expr_types); + if (num_nodes == 0) { + return std::dynamic_pointer_cast(node); + } else { + std::shared_ptr res = std::make_shared(num_nodes); + node->fill_expression(res->operators, num_nodes); + return res; + } +} + +std::vector> +appsi_exprs_from_pyomo_exprs(py::list expr_list, py::dict var_map, + py::dict param_map) { + PyomoExprTypes expr_types = PyomoExprTypes(); + int num_exprs = expr_types.builtins.attr("len")(expr_list).cast(); + std::vector> res(num_exprs); + + int ndx = 0; + for (py::handle expr : expr_list) { + res[ndx] = appsi_expr_from_pyomo_expr(expr, var_map, param_map, expr_types); + ndx += 1; + } + return res; +} + +void process_pyomo_vars(PyomoExprTypes &expr_types, py::list pyomo_vars, + py::dict var_map, py::dict param_map, + py::dict var_attrs, py::dict rev_var_map, + py::bool_ _set_name, py::handle symbol_map, + py::handle labeler, py::bool_ _update) { + py::tuple v_attrs; + std::shared_ptr cv; + py::handle v_lb; + py::handle v_ub; + py::handle v_val; + py::tuple domain_interval; + py::handle interval_lb; + py::handle interval_ub; + py::handle interval_step; + bool v_fixed; + bool set_name = _set_name.cast(); + bool update = _update.cast(); + double domain_step; + + for (py::handle v : pyomo_vars) { + v_attrs = var_attrs[expr_types.id(v)]; + v_lb = v_attrs[1]; + v_ub = v_attrs[2]; + v_fixed = v_attrs[3].cast(); + domain_interval = v_attrs[4]; + v_val = v_attrs[5]; + + interval_lb = domain_interval[0]; + interval_ub = domain_interval[1]; + interval_step = domain_interval[2]; + domain_step = interval_step.cast(); + + if (update) { + cv = var_map[expr_types.id(v)].cast>(); + } else { + cv = std::make_shared(); + } + + if (!(v_lb.is(py::none()))) { + cv->lb = appsi_expr_from_pyomo_expr(v_lb, var_map, param_map, expr_types); + } else { + cv->lb = std::make_shared(-inf); + } + if (!(v_ub.is(py::none()))) { + cv->ub = appsi_expr_from_pyomo_expr(v_ub, var_map, param_map, expr_types); + } else { + cv->ub = std::make_shared(inf); + } + + if (!(v_val.is(py::none()))) { + cv->value = v_val.cast(); + } + + if (v_fixed) { + cv->fixed = true; + } else { + cv->fixed = false; + } + + if (set_name && !update) { + cv->name = symbol_map.attr("getSymbol")(v, labeler).cast(); + } + + if (interval_lb.is(py::none())) + cv->domain_lb = -inf; + else + cv->domain_lb = interval_lb.cast(); + if (interval_ub.is(py::none())) + cv->domain_ub = inf; + else + cv->domain_ub = interval_ub.cast(); + if (domain_step == 0) + cv->domain = continuous; + else if (domain_step == 1) { + if ((cv->domain_lb == 0) && (cv->domain_ub == 1)) + cv->domain = binary; + else + cv->domain = integers; + } else + throw py::value_error("Unrecognized domain step"); + + if (!update) { + var_map[expr_types.id(v)] = py::cast(cv); + rev_var_map[py::cast(cv)] = v; + } + } +} diff --git a/pyomo/contrib/appsi/cmodel/src/expression.hpp b/pyomo/contrib/appsi/cmodel/src/expression.hpp index 9a991102a90..220f5f22b0d 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.hpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.hpp @@ -1,788 +1,800 @@ -#ifndef EXPRESSION_HEADER -#define EXPRESSION_HEADER - -#include "interval.hpp" -#include - -class Node; -class ExpressionBase; -class Leaf; -class Var; -class Constant; -class Param; -class Expression; -class Operator; -class BinaryOperator; -class UnaryOperator; -class LinearOperator; -class SumOperator; -class MultiplyOperator; -class DivideOperator; -class PowerOperator; -class NegationOperator; -class ExpOperator; -class LogOperator; -class AbsOperator; -class ExternalOperator; -class PyomoExprTypes; - -extern double inf; - -class Node : public std::enable_shared_from_this { -public: - Node() = default; - virtual ~Node() = default; - virtual bool is_variable_type() { return false; } - virtual bool is_param_type() { return false; } - virtual bool is_expression_type() { return false; } - virtual bool is_operator_type() { return false; } - virtual bool is_constant_type() { return false; } - virtual bool is_leaf() { return false; } - virtual bool is_binary_operator() { return false; } - virtual bool is_unary_operator() { return false; } - virtual bool is_linear_operator() { return false; } - virtual bool is_sum_operator() { return false; } - virtual bool is_multiply_operator() { return false; } - virtual bool is_divide_operator() { return false; } - virtual bool is_power_operator() { return false; } - virtual bool is_negation_operator() { return false; } - virtual bool is_exp_operator() { return false; } - virtual bool is_log_operator() { return false; } - virtual bool is_abs_operator() { return false; } - virtual bool is_sqrt_operator() { return false; } - virtual bool is_external_operator() { return false; } - virtual double get_value_from_array(double *) = 0; - virtual int get_degree_from_array(int *) = 0; - virtual std::string get_string_from_array(std::string *) = 0; - virtual void fill_prefix_notation_stack( - std::shared_ptr>> stack) = 0; - virtual void write_nl_string(std::ofstream &) = 0; - virtual void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) = 0; - virtual double get_lb_from_array(double *lbs) = 0; - virtual double get_ub_from_array(double *ubs) = 0; - virtual void - set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, - double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) = 0; -}; - -class ExpressionBase : public Node { -public: - ExpressionBase() = default; - virtual double evaluate() = 0; - virtual std::string __str__() = 0; - virtual std::shared_ptr>> - identify_variables() = 0; - virtual std::shared_ptr>> - identify_external_operators() = 0; - virtual std::shared_ptr>> - get_prefix_notation() = 0; - std::shared_ptr shared_from_this() { - return std::static_pointer_cast(Node::shared_from_this()); - } - void fill_prefix_notation_stack( - std::shared_ptr>> stack) override { - ; - } -}; - -class Leaf : public ExpressionBase { -public: - Leaf() = default; - Leaf(double value) : value(value) {} - virtual ~Leaf() = default; - double value = 0.0; - bool is_leaf() override; - double evaluate() override; - double get_value_from_array(double *) override; - std::string get_string_from_array(std::string *) override; - std::shared_ptr>> - get_prefix_notation() override; - void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) override; - double get_lb_from_array(double *lbs) override; - double get_ub_from_array(double *ubs) override; - void - set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, - double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class Constant : public Leaf { -public: - Constant() = default; - Constant(double value) : Leaf(value) {} - bool is_constant_type() override; - std::string __str__() override; - int get_degree_from_array(int *) override; - std::shared_ptr>> - identify_variables() override; - std::shared_ptr>> - identify_external_operators() override; - void write_nl_string(std::ofstream &) override; -}; - -enum Domain { continuous, binary, integers }; - -class Var : public Leaf { -public: - Var() = default; - Var(double val) : Leaf(val) {} - Var(std::string _name) : name(_name) {} - Var(std::string _name, double val) : Leaf(val), name(_name) {} - std::string name = "v"; - std::string __str__() override; - std::shared_ptr lb; - std::shared_ptr ub; - int index = -1; - bool fixed = false; - double domain_lb = -inf; - double domain_ub = inf; - Domain domain = continuous; - bool is_variable_type() override; - int get_degree_from_array(int *) override; - std::shared_ptr>> - identify_variables() override; - std::shared_ptr>> - identify_external_operators() override; - void write_nl_string(std::ofstream &) override; - std::shared_ptr shared_from_this() { - return std::static_pointer_cast(Node::shared_from_this()); - } - double get_lb(); - double get_ub(); - Domain get_domain(); - double get_lb_from_array(double *lbs) override; - double get_ub_from_array(double *ubs) override; - void - set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, - double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class Param : public Leaf { -public: - Param() = default; - Param(double val) : Leaf(val) {} - Param(std::string _name) : name(_name) {} - Param(std::string _name, double val) : Leaf(val), name(_name) {} - std::string name = "p"; - std::string __str__() override; - bool is_param_type() override; - int get_degree_from_array(int *) override; - std::shared_ptr>> - identify_variables() override; - std::shared_ptr>> - identify_external_operators() override; - void write_nl_string(std::ofstream &) override; -}; - -class Expression : public ExpressionBase { -public: - Expression(int _n_operators) : ExpressionBase() { - operators = new std::shared_ptr[_n_operators]; - n_operators = _n_operators; - } - ~Expression() { delete[] operators; } - std::string __str__() override; - bool is_expression_type() override; - double evaluate() override; - double get_value_from_array(double *) override; - int get_degree_from_array(int *) override; - std::shared_ptr>> - identify_variables() override; - std::shared_ptr>> - identify_external_operators() override; - std::string get_string_from_array(std::string *) override; - std::shared_ptr>> - get_prefix_notation() override; - void write_nl_string(std::ofstream &) override; - std::vector> get_operators(); - std::shared_ptr *operators; - unsigned int n_operators; - void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, double integer_tol); - void propagate_bounds_backward(double *lbs, double *ubs, - double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars); - double get_lb_from_array(double *lbs) override; - double get_ub_from_array(double *ubs) override; - void - set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, - double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class Operator : public Node { -public: - Operator() = default; - int index = 0; - virtual void evaluate(double *values) = 0; - virtual void propagate_degree_forward(int *degrees, double *values) = 0; - virtual void - identify_variables(std::set> &, - std::shared_ptr>>) = 0; - std::shared_ptr shared_from_this() { - return std::static_pointer_cast(Node::shared_from_this()); - } - bool is_operator_type() override; - double get_value_from_array(double *) override; - int get_degree_from_array(int *) override; - std::string get_string_from_array(std::string *) override; - virtual void print(std::string *) = 0; - virtual std::string name() = 0; - virtual void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol); - virtual void - propagate_bounds_backward(double *lbs, double *ubs, double feasibility_tol, - double integer_tol, double improvement_tol, - std::set> &improved_vars); - double get_lb_from_array(double *lbs) override; - double get_ub_from_array(double *ubs) override; - void - set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, - double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class BinaryOperator : public Operator { -public: - BinaryOperator() = default; - virtual ~BinaryOperator() = default; - void identify_variables( - std::set> &, - std::shared_ptr>>) override; - std::shared_ptr operand1; - std::shared_ptr operand2; - void fill_prefix_notation_stack( - std::shared_ptr>> stack) override; - bool is_binary_operator() override; - void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) override; -}; - -class UnaryOperator : public Operator { -public: - UnaryOperator() = default; - virtual ~UnaryOperator() = default; - void identify_variables( - std::set> &, - std::shared_ptr>>) override; - std::shared_ptr operand; - void fill_prefix_notation_stack( - std::shared_ptr>> stack) override; - bool is_unary_operator() override; - void propagate_degree_forward(int *degrees, double *values) override; - void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) override; -}; - -class LinearOperator : public Operator { -public: - LinearOperator(int _nterms) { - variables = new std::shared_ptr[_nterms]; - coefficients = new std::shared_ptr[_nterms]; - nterms = _nterms; - } - ~LinearOperator() { - delete[] variables; - delete[] coefficients; - } - void identify_variables( - std::set> &, - std::shared_ptr>>) override; - std::shared_ptr *variables; - std::shared_ptr *coefficients; - std::shared_ptr constant = std::make_shared(0); - void evaluate(double *values) override; - void propagate_degree_forward(int *degrees, double *values) override; - void print(std::string *) override; - std::string name() override { return "LinearOperator"; }; - void write_nl_string(std::ofstream &) override; - void fill_prefix_notation_stack( - std::shared_ptr>> stack) override; - bool is_linear_operator() override; - unsigned int nterms; - void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class SumOperator : public Operator { -public: - SumOperator(int _nargs) { - operands = new std::shared_ptr[_nargs]; - nargs = _nargs; - } - ~SumOperator() { delete[] operands; } - void identify_variables( - std::set> &, - std::shared_ptr>>) override; - void evaluate(double *values) override; - void propagate_degree_forward(int *degrees, double *values) override; - void print(std::string *) override; - std::string name() override { return "SumOperator"; }; - void write_nl_string(std::ofstream &) override; - void fill_prefix_notation_stack( - std::shared_ptr>> stack) override; - bool is_sum_operator() override; - std::shared_ptr *operands; - unsigned int nargs; - void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class MultiplyOperator : public BinaryOperator { -public: - MultiplyOperator() = default; - void evaluate(double *values) override; - void propagate_degree_forward(int *degrees, double *values) override; - void print(std::string *) override; - std::string name() override { return "MultiplyOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_multiply_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class ExternalOperator : public Operator { -public: - ExternalOperator(int _nargs) { - operands = new std::shared_ptr[_nargs]; - nargs = _nargs; - } - ~ExternalOperator() { delete[] operands; } - void evaluate(double *values) override; - void propagate_degree_forward(int *degrees, double *values) override; - void print(std::string *) override; - std::string name() override { return "ExternalOperator"; }; - void write_nl_string(std::ofstream &) override; - void fill_prefix_notation_stack( - std::shared_ptr>> stack) override; - void identify_variables( - std::set> &, - std::shared_ptr>>) override; - bool is_external_operator() override; - std::string function_name; - int external_function_index = -1; - std::shared_ptr *operands; - unsigned int nargs; - void fill_expression(std::shared_ptr *oper_array, - int &oper_ndx) override; -}; - -class DivideOperator : public BinaryOperator { -public: - DivideOperator() = default; - void evaluate(double *values) override; - void propagate_degree_forward(int *degrees, double *values) override; - void print(std::string *) override; - std::string name() override { return "DivideOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_divide_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class PowerOperator : public BinaryOperator { -public: - PowerOperator() = default; - void evaluate(double *values) override; - void propagate_degree_forward(int *degrees, double *values) override; - void print(std::string *) override; - std::string name() override { return "PowerOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_power_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class NegationOperator : public UnaryOperator { -public: - NegationOperator() = default; - void evaluate(double *values) override; - void propagate_degree_forward(int *degrees, double *values) override; - void print(std::string *) override; - std::string name() override { return "NegationOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_negation_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class ExpOperator : public UnaryOperator { -public: - ExpOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "ExpOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_exp_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class LogOperator : public UnaryOperator { -public: - LogOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "LogOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_log_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class AbsOperator : public UnaryOperator { -public: - AbsOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "AbsOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_abs_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class SqrtOperator : public UnaryOperator { -public: - SqrtOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "SqrtOperator"; }; - void write_nl_string(std::ofstream &) override; - bool is_sqrt_operator() override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class Log10Operator : public UnaryOperator { -public: - Log10Operator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "Log10Operator"; }; - void write_nl_string(std::ofstream &) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class SinOperator : public UnaryOperator { -public: - SinOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "SinOperator"; }; - void write_nl_string(std::ofstream &) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class CosOperator : public UnaryOperator { -public: - CosOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "CosOperator"; }; - void write_nl_string(std::ofstream &) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class TanOperator : public UnaryOperator { -public: - TanOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "TanOperator"; }; - void write_nl_string(std::ofstream &) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class AsinOperator : public UnaryOperator { -public: - AsinOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "AsinOperator"; }; - void write_nl_string(std::ofstream &) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class AcosOperator : public UnaryOperator { -public: - AcosOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "AcosOperator"; }; - void write_nl_string(std::ofstream &) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -class AtanOperator : public UnaryOperator { -public: - AtanOperator() = default; - void evaluate(double *values) override; - void print(std::string *) override; - std::string name() override { return "AtanOperator"; }; - void write_nl_string(std::ofstream &) override; - void propagate_bounds_forward(double *lbs, double *ubs, - double feasibility_tol, - double integer_tol) override; - void propagate_bounds_backward( - double *lbs, double *ubs, double feasibility_tol, double integer_tol, - double improvement_tol, - std::set> &improved_vars) override; -}; - -enum ExprType { - py_float = 0, - var = 1, - param = 2, - product = 3, - sum = 4, - negation = 5, - external_func = 6, - power = 7, - division = 8, - unary_func = 9, - linear = 10, - named_expr = 11, - numeric_constant = 12, - pyomo_unit = 13, - unary_abs = 14 -}; - -class PyomoExprTypes { -public: - PyomoExprTypes() { - expr_type_map[int_] = py_float; - expr_type_map[float_] = py_float; - expr_type_map[np_int16] = py_float; - expr_type_map[np_int32] = py_float; - expr_type_map[np_int64] = py_float; - expr_type_map[np_longlong] = py_float; - expr_type_map[np_uint16] = py_float; - expr_type_map[np_uint32] = py_float; - expr_type_map[np_uint64] = py_float; - expr_type_map[np_ulonglong] = py_float; - expr_type_map[np_float16] = py_float; - expr_type_map[np_float32] = py_float; - expr_type_map[np_float64] = py_float; - expr_type_map[ScalarVar] = var; - expr_type_map[_GeneralVarData] = var; - expr_type_map[AutoLinkedBinaryVar] = var; - expr_type_map[ScalarParam] = param; - expr_type_map[_ParamData] = param; - expr_type_map[MonomialTermExpression] = product; - expr_type_map[ProductExpression] = product; - expr_type_map[NPV_ProductExpression] = product; - expr_type_map[SumExpression] = sum; - expr_type_map[NPV_SumExpression] = sum; - expr_type_map[NegationExpression] = negation; - expr_type_map[NPV_NegationExpression] = negation; - expr_type_map[ExternalFunctionExpression] = external_func; - expr_type_map[NPV_ExternalFunctionExpression] = external_func; - expr_type_map[PowExpression] = power; - expr_type_map[NPV_PowExpression] = power; - expr_type_map[DivisionExpression] = division; - expr_type_map[NPV_DivisionExpression] = division; - expr_type_map[UnaryFunctionExpression] = unary_func; - expr_type_map[NPV_UnaryFunctionExpression] = unary_func; - expr_type_map[LinearExpression] = linear; - expr_type_map[_GeneralExpressionData] = named_expr; - expr_type_map[ScalarExpression] = named_expr; - expr_type_map[Integral] = named_expr; - expr_type_map[ScalarIntegral] = named_expr; - expr_type_map[NumericConstant] = numeric_constant; - expr_type_map[_PyomoUnit] = pyomo_unit; - expr_type_map[AbsExpression] = unary_abs; - expr_type_map[NPV_AbsExpression] = unary_abs; - } - ~PyomoExprTypes() = default; - py::int_ ione = 1; - py::float_ fone = 1.0; - py::type int_ = py::type::of(ione); - py::type float_ = py::type::of(fone); - py::object np = py::module_::import("numpy"); - py::type np_int16 = np.attr("int16"); - py::type np_int32 = np.attr("int32"); - py::type np_int64 = np.attr("int64"); - py::type np_longlong = np.attr("longlong"); - py::type np_uint16 = np.attr("uint16"); - py::type np_uint32 = np.attr("uint32"); - py::type np_uint64 = np.attr("uint64"); - py::type np_ulonglong = np.attr("ulonglong"); - py::type np_float16 = np.attr("float16"); - py::type np_float32 = np.attr("float32"); - py::type np_float64 = np.attr("float64"); - py::object ScalarParam = - py::module_::import("pyomo.core.base.param").attr("ScalarParam"); - py::object _ParamData = - py::module_::import("pyomo.core.base.param").attr("_ParamData"); - py::object ScalarVar = - py::module_::import("pyomo.core.base.var").attr("ScalarVar"); - py::object _GeneralVarData = - py::module_::import("pyomo.core.base.var").attr("_GeneralVarData"); - py::object AutoLinkedBinaryVar = - py::module_::import("pyomo.gdp.disjunct").attr("AutoLinkedBinaryVar"); - py::object numeric_expr = py::module_::import("pyomo.core.expr.numeric_expr"); - py::object NegationExpression = numeric_expr.attr("NegationExpression"); - py::object NPV_NegationExpression = - numeric_expr.attr("NPV_NegationExpression"); - py::object ExternalFunctionExpression = - numeric_expr.attr("ExternalFunctionExpression"); - py::object NPV_ExternalFunctionExpression = - numeric_expr.attr("NPV_ExternalFunctionExpression"); - py::object PowExpression = numeric_expr.attr("PowExpression"); - py::object NPV_PowExpression = numeric_expr.attr("NPV_PowExpression"); - py::object ProductExpression = numeric_expr.attr("ProductExpression"); - py::object NPV_ProductExpression = numeric_expr.attr("NPV_ProductExpression"); - py::object MonomialTermExpression = - numeric_expr.attr("MonomialTermExpression"); - py::object DivisionExpression = numeric_expr.attr("DivisionExpression"); - py::object NPV_DivisionExpression = - numeric_expr.attr("NPV_DivisionExpression"); - py::object SumExpression = numeric_expr.attr("SumExpression"); - py::object NPV_SumExpression = numeric_expr.attr("NPV_SumExpression"); - py::object UnaryFunctionExpression = - numeric_expr.attr("UnaryFunctionExpression"); - py::object AbsExpression = numeric_expr.attr("AbsExpression"); - py::object NPV_AbsExpression = numeric_expr.attr("NPV_AbsExpression"); - py::object NPV_UnaryFunctionExpression = - numeric_expr.attr("NPV_UnaryFunctionExpression"); - py::object LinearExpression = numeric_expr.attr("LinearExpression"); - py::object NumericConstant = - py::module_::import("pyomo.core.expr.numvalue").attr("NumericConstant"); - py::object expr_module = py::module_::import("pyomo.core.base.expression"); - py::object _GeneralExpressionData = - expr_module.attr("_GeneralExpressionData"); - py::object ScalarExpression = expr_module.attr("ScalarExpression"); - py::object ScalarIntegral = - py::module_::import("pyomo.dae.integral").attr("ScalarIntegral"); - py::object Integral = - py::module_::import("pyomo.dae.integral").attr("Integral"); - py::object _PyomoUnit = - py::module_::import("pyomo.core.base.units_container").attr("_PyomoUnit"); - py::object builtins = py::module_::import("builtins"); - py::object id = builtins.attr("id"); - py::object len = builtins.attr("len"); - py::dict expr_type_map; -}; - -std::vector> create_vars(int n_vars); -std::vector> create_params(int n_params); -std::vector> create_constants(int n_constants); -std::shared_ptr -appsi_expr_from_pyomo_expr(py::handle expr, py::handle var_map, - py::handle param_map, PyomoExprTypes &expr_types); -std::vector> -appsi_exprs_from_pyomo_exprs(py::list expr_list, py::dict var_map, - py::dict param_map); -py::tuple prep_for_repn(py::handle expr, PyomoExprTypes &expr_types); - -void process_pyomo_vars(PyomoExprTypes &expr_types, py::list pyomo_vars, - py::dict var_map, py::dict param_map, - py::dict var_attrs, py::dict rev_var_map, - py::bool_ _set_name, py::handle symbol_map, - py::handle labeler, py::bool_ _update); - -#endif +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + +#ifndef EXPRESSION_HEADER +#define EXPRESSION_HEADER + +#include "interval.hpp" +#include + +class Node; +class ExpressionBase; +class Leaf; +class Var; +class Constant; +class Param; +class Expression; +class Operator; +class BinaryOperator; +class UnaryOperator; +class LinearOperator; +class SumOperator; +class MultiplyOperator; +class DivideOperator; +class PowerOperator; +class NegationOperator; +class ExpOperator; +class LogOperator; +class AbsOperator; +class ExternalOperator; +class PyomoExprTypes; + +extern double inf; + +class Node : public std::enable_shared_from_this { +public: + Node() = default; + virtual ~Node() = default; + virtual bool is_variable_type() { return false; } + virtual bool is_param_type() { return false; } + virtual bool is_expression_type() { return false; } + virtual bool is_operator_type() { return false; } + virtual bool is_constant_type() { return false; } + virtual bool is_leaf() { return false; } + virtual bool is_binary_operator() { return false; } + virtual bool is_unary_operator() { return false; } + virtual bool is_linear_operator() { return false; } + virtual bool is_sum_operator() { return false; } + virtual bool is_multiply_operator() { return false; } + virtual bool is_divide_operator() { return false; } + virtual bool is_power_operator() { return false; } + virtual bool is_negation_operator() { return false; } + virtual bool is_exp_operator() { return false; } + virtual bool is_log_operator() { return false; } + virtual bool is_abs_operator() { return false; } + virtual bool is_sqrt_operator() { return false; } + virtual bool is_external_operator() { return false; } + virtual double get_value_from_array(double *) = 0; + virtual int get_degree_from_array(int *) = 0; + virtual std::string get_string_from_array(std::string *) = 0; + virtual void fill_prefix_notation_stack( + std::shared_ptr>> stack) = 0; + virtual void write_nl_string(std::ofstream &) = 0; + virtual void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) = 0; + virtual double get_lb_from_array(double *lbs) = 0; + virtual double get_ub_from_array(double *ubs) = 0; + virtual void + set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, + double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) = 0; +}; + +class ExpressionBase : public Node { +public: + ExpressionBase() = default; + virtual double evaluate() = 0; + virtual std::string __str__() = 0; + virtual std::shared_ptr>> + identify_variables() = 0; + virtual std::shared_ptr>> + identify_external_operators() = 0; + virtual std::shared_ptr>> + get_prefix_notation() = 0; + std::shared_ptr shared_from_this() { + return std::static_pointer_cast(Node::shared_from_this()); + } + void fill_prefix_notation_stack( + std::shared_ptr>> stack) override { + ; + } +}; + +class Leaf : public ExpressionBase { +public: + Leaf() = default; + Leaf(double value) : value(value) {} + virtual ~Leaf() = default; + double value = 0.0; + bool is_leaf() override; + double evaluate() override; + double get_value_from_array(double *) override; + std::string get_string_from_array(std::string *) override; + std::shared_ptr>> + get_prefix_notation() override; + void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) override; + double get_lb_from_array(double *lbs) override; + double get_ub_from_array(double *ubs) override; + void + set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, + double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class Constant : public Leaf { +public: + Constant() = default; + Constant(double value) : Leaf(value) {} + bool is_constant_type() override; + std::string __str__() override; + int get_degree_from_array(int *) override; + std::shared_ptr>> + identify_variables() override; + std::shared_ptr>> + identify_external_operators() override; + void write_nl_string(std::ofstream &) override; +}; + +enum Domain { continuous, binary, integers }; + +class Var : public Leaf { +public: + Var() = default; + Var(double val) : Leaf(val) {} + Var(std::string _name) : name(_name) {} + Var(std::string _name, double val) : Leaf(val), name(_name) {} + std::string name = "v"; + std::string __str__() override; + std::shared_ptr lb; + std::shared_ptr ub; + int index = -1; + bool fixed = false; + double domain_lb = -inf; + double domain_ub = inf; + Domain domain = continuous; + bool is_variable_type() override; + int get_degree_from_array(int *) override; + std::shared_ptr>> + identify_variables() override; + std::shared_ptr>> + identify_external_operators() override; + void write_nl_string(std::ofstream &) override; + std::shared_ptr shared_from_this() { + return std::static_pointer_cast(Node::shared_from_this()); + } + double get_lb(); + double get_ub(); + Domain get_domain(); + double get_lb_from_array(double *lbs) override; + double get_ub_from_array(double *ubs) override; + void + set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, + double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class Param : public Leaf { +public: + Param() = default; + Param(double val) : Leaf(val) {} + Param(std::string _name) : name(_name) {} + Param(std::string _name, double val) : Leaf(val), name(_name) {} + std::string name = "p"; + std::string __str__() override; + bool is_param_type() override; + int get_degree_from_array(int *) override; + std::shared_ptr>> + identify_variables() override; + std::shared_ptr>> + identify_external_operators() override; + void write_nl_string(std::ofstream &) override; +}; + +class Expression : public ExpressionBase { +public: + Expression(int _n_operators) : ExpressionBase() { + operators = new std::shared_ptr[_n_operators]; + n_operators = _n_operators; + } + ~Expression() { delete[] operators; } + std::string __str__() override; + bool is_expression_type() override; + double evaluate() override; + double get_value_from_array(double *) override; + int get_degree_from_array(int *) override; + std::shared_ptr>> + identify_variables() override; + std::shared_ptr>> + identify_external_operators() override; + std::string get_string_from_array(std::string *) override; + std::shared_ptr>> + get_prefix_notation() override; + void write_nl_string(std::ofstream &) override; + std::vector> get_operators(); + std::shared_ptr *operators; + unsigned int n_operators; + void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, double integer_tol); + void propagate_bounds_backward(double *lbs, double *ubs, + double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars); + double get_lb_from_array(double *lbs) override; + double get_ub_from_array(double *ubs) override; + void + set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, + double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class Operator : public Node { +public: + Operator() = default; + int index = 0; + virtual void evaluate(double *values) = 0; + virtual void propagate_degree_forward(int *degrees, double *values) = 0; + virtual void + identify_variables(std::set> &, + std::shared_ptr>>) = 0; + std::shared_ptr shared_from_this() { + return std::static_pointer_cast(Node::shared_from_this()); + } + bool is_operator_type() override; + double get_value_from_array(double *) override; + int get_degree_from_array(int *) override; + std::string get_string_from_array(std::string *) override; + virtual void print(std::string *) = 0; + virtual std::string name() = 0; + virtual void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol); + virtual void + propagate_bounds_backward(double *lbs, double *ubs, double feasibility_tol, + double integer_tol, double improvement_tol, + std::set> &improved_vars); + double get_lb_from_array(double *lbs) override; + double get_ub_from_array(double *ubs) override; + void + set_bounds_in_array(double new_lb, double new_ub, double *lbs, double *ubs, + double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class BinaryOperator : public Operator { +public: + BinaryOperator() = default; + virtual ~BinaryOperator() = default; + void identify_variables( + std::set> &, + std::shared_ptr>>) override; + std::shared_ptr operand1; + std::shared_ptr operand2; + void fill_prefix_notation_stack( + std::shared_ptr>> stack) override; + bool is_binary_operator() override; + void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) override; +}; + +class UnaryOperator : public Operator { +public: + UnaryOperator() = default; + virtual ~UnaryOperator() = default; + void identify_variables( + std::set> &, + std::shared_ptr>>) override; + std::shared_ptr operand; + void fill_prefix_notation_stack( + std::shared_ptr>> stack) override; + bool is_unary_operator() override; + void propagate_degree_forward(int *degrees, double *values) override; + void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) override; +}; + +class LinearOperator : public Operator { +public: + LinearOperator(int _nterms) { + variables = new std::shared_ptr[_nterms]; + coefficients = new std::shared_ptr[_nterms]; + nterms = _nterms; + } + ~LinearOperator() { + delete[] variables; + delete[] coefficients; + } + void identify_variables( + std::set> &, + std::shared_ptr>>) override; + std::shared_ptr *variables; + std::shared_ptr *coefficients; + std::shared_ptr constant = std::make_shared(0); + void evaluate(double *values) override; + void propagate_degree_forward(int *degrees, double *values) override; + void print(std::string *) override; + std::string name() override { return "LinearOperator"; }; + void write_nl_string(std::ofstream &) override; + void fill_prefix_notation_stack( + std::shared_ptr>> stack) override; + bool is_linear_operator() override; + unsigned int nterms; + void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class SumOperator : public Operator { +public: + SumOperator(int _nargs) { + operands = new std::shared_ptr[_nargs]; + nargs = _nargs; + } + ~SumOperator() { delete[] operands; } + void identify_variables( + std::set> &, + std::shared_ptr>>) override; + void evaluate(double *values) override; + void propagate_degree_forward(int *degrees, double *values) override; + void print(std::string *) override; + std::string name() override { return "SumOperator"; }; + void write_nl_string(std::ofstream &) override; + void fill_prefix_notation_stack( + std::shared_ptr>> stack) override; + bool is_sum_operator() override; + std::shared_ptr *operands; + unsigned int nargs; + void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class MultiplyOperator : public BinaryOperator { +public: + MultiplyOperator() = default; + void evaluate(double *values) override; + void propagate_degree_forward(int *degrees, double *values) override; + void print(std::string *) override; + std::string name() override { return "MultiplyOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_multiply_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class ExternalOperator : public Operator { +public: + ExternalOperator(int _nargs) { + operands = new std::shared_ptr[_nargs]; + nargs = _nargs; + } + ~ExternalOperator() { delete[] operands; } + void evaluate(double *values) override; + void propagate_degree_forward(int *degrees, double *values) override; + void print(std::string *) override; + std::string name() override { return "ExternalOperator"; }; + void write_nl_string(std::ofstream &) override; + void fill_prefix_notation_stack( + std::shared_ptr>> stack) override; + void identify_variables( + std::set> &, + std::shared_ptr>>) override; + bool is_external_operator() override; + std::string function_name; + int external_function_index = -1; + std::shared_ptr *operands; + unsigned int nargs; + void fill_expression(std::shared_ptr *oper_array, + int &oper_ndx) override; +}; + +class DivideOperator : public BinaryOperator { +public: + DivideOperator() = default; + void evaluate(double *values) override; + void propagate_degree_forward(int *degrees, double *values) override; + void print(std::string *) override; + std::string name() override { return "DivideOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_divide_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class PowerOperator : public BinaryOperator { +public: + PowerOperator() = default; + void evaluate(double *values) override; + void propagate_degree_forward(int *degrees, double *values) override; + void print(std::string *) override; + std::string name() override { return "PowerOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_power_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class NegationOperator : public UnaryOperator { +public: + NegationOperator() = default; + void evaluate(double *values) override; + void propagate_degree_forward(int *degrees, double *values) override; + void print(std::string *) override; + std::string name() override { return "NegationOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_negation_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class ExpOperator : public UnaryOperator { +public: + ExpOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "ExpOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_exp_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class LogOperator : public UnaryOperator { +public: + LogOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "LogOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_log_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class AbsOperator : public UnaryOperator { +public: + AbsOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "AbsOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_abs_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class SqrtOperator : public UnaryOperator { +public: + SqrtOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "SqrtOperator"; }; + void write_nl_string(std::ofstream &) override; + bool is_sqrt_operator() override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class Log10Operator : public UnaryOperator { +public: + Log10Operator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "Log10Operator"; }; + void write_nl_string(std::ofstream &) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class SinOperator : public UnaryOperator { +public: + SinOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "SinOperator"; }; + void write_nl_string(std::ofstream &) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class CosOperator : public UnaryOperator { +public: + CosOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "CosOperator"; }; + void write_nl_string(std::ofstream &) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class TanOperator : public UnaryOperator { +public: + TanOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "TanOperator"; }; + void write_nl_string(std::ofstream &) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class AsinOperator : public UnaryOperator { +public: + AsinOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "AsinOperator"; }; + void write_nl_string(std::ofstream &) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class AcosOperator : public UnaryOperator { +public: + AcosOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "AcosOperator"; }; + void write_nl_string(std::ofstream &) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +class AtanOperator : public UnaryOperator { +public: + AtanOperator() = default; + void evaluate(double *values) override; + void print(std::string *) override; + std::string name() override { return "AtanOperator"; }; + void write_nl_string(std::ofstream &) override; + void propagate_bounds_forward(double *lbs, double *ubs, + double feasibility_tol, + double integer_tol) override; + void propagate_bounds_backward( + double *lbs, double *ubs, double feasibility_tol, double integer_tol, + double improvement_tol, + std::set> &improved_vars) override; +}; + +enum ExprType { + py_float = 0, + var = 1, + param = 2, + product = 3, + sum = 4, + negation = 5, + external_func = 6, + power = 7, + division = 8, + unary_func = 9, + linear = 10, + named_expr = 11, + numeric_constant = 12, + pyomo_unit = 13, + unary_abs = 14 +}; + +class PyomoExprTypes { +public: + PyomoExprTypes() { + expr_type_map[int_] = py_float; + expr_type_map[float_] = py_float; + expr_type_map[np_int16] = py_float; + expr_type_map[np_int32] = py_float; + expr_type_map[np_int64] = py_float; + expr_type_map[np_longlong] = py_float; + expr_type_map[np_uint16] = py_float; + expr_type_map[np_uint32] = py_float; + expr_type_map[np_uint64] = py_float; + expr_type_map[np_ulonglong] = py_float; + expr_type_map[np_float16] = py_float; + expr_type_map[np_float32] = py_float; + expr_type_map[np_float64] = py_float; + expr_type_map[ScalarVar] = var; + expr_type_map[_GeneralVarData] = var; + expr_type_map[AutoLinkedBinaryVar] = var; + expr_type_map[ScalarParam] = param; + expr_type_map[_ParamData] = param; + expr_type_map[MonomialTermExpression] = product; + expr_type_map[ProductExpression] = product; + expr_type_map[NPV_ProductExpression] = product; + expr_type_map[SumExpression] = sum; + expr_type_map[NPV_SumExpression] = sum; + expr_type_map[NegationExpression] = negation; + expr_type_map[NPV_NegationExpression] = negation; + expr_type_map[ExternalFunctionExpression] = external_func; + expr_type_map[NPV_ExternalFunctionExpression] = external_func; + expr_type_map[PowExpression] = power; + expr_type_map[NPV_PowExpression] = power; + expr_type_map[DivisionExpression] = division; + expr_type_map[NPV_DivisionExpression] = division; + expr_type_map[UnaryFunctionExpression] = unary_func; + expr_type_map[NPV_UnaryFunctionExpression] = unary_func; + expr_type_map[LinearExpression] = linear; + expr_type_map[_GeneralExpressionData] = named_expr; + expr_type_map[ScalarExpression] = named_expr; + expr_type_map[Integral] = named_expr; + expr_type_map[ScalarIntegral] = named_expr; + expr_type_map[NumericConstant] = numeric_constant; + expr_type_map[_PyomoUnit] = pyomo_unit; + expr_type_map[AbsExpression] = unary_abs; + expr_type_map[NPV_AbsExpression] = unary_abs; + } + ~PyomoExprTypes() = default; + py::int_ ione = 1; + py::float_ fone = 1.0; + py::type int_ = py::type::of(ione); + py::type float_ = py::type::of(fone); + py::object np = py::module_::import("numpy"); + py::type np_int16 = np.attr("int16"); + py::type np_int32 = np.attr("int32"); + py::type np_int64 = np.attr("int64"); + py::type np_longlong = np.attr("longlong"); + py::type np_uint16 = np.attr("uint16"); + py::type np_uint32 = np.attr("uint32"); + py::type np_uint64 = np.attr("uint64"); + py::type np_ulonglong = np.attr("ulonglong"); + py::type np_float16 = np.attr("float16"); + py::type np_float32 = np.attr("float32"); + py::type np_float64 = np.attr("float64"); + py::object ScalarParam = + py::module_::import("pyomo.core.base.param").attr("ScalarParam"); + py::object _ParamData = + py::module_::import("pyomo.core.base.param").attr("_ParamData"); + py::object ScalarVar = + py::module_::import("pyomo.core.base.var").attr("ScalarVar"); + py::object _GeneralVarData = + py::module_::import("pyomo.core.base.var").attr("_GeneralVarData"); + py::object AutoLinkedBinaryVar = + py::module_::import("pyomo.gdp.disjunct").attr("AutoLinkedBinaryVar"); + py::object numeric_expr = py::module_::import("pyomo.core.expr.numeric_expr"); + py::object NegationExpression = numeric_expr.attr("NegationExpression"); + py::object NPV_NegationExpression = + numeric_expr.attr("NPV_NegationExpression"); + py::object ExternalFunctionExpression = + numeric_expr.attr("ExternalFunctionExpression"); + py::object NPV_ExternalFunctionExpression = + numeric_expr.attr("NPV_ExternalFunctionExpression"); + py::object PowExpression = numeric_expr.attr("PowExpression"); + py::object NPV_PowExpression = numeric_expr.attr("NPV_PowExpression"); + py::object ProductExpression = numeric_expr.attr("ProductExpression"); + py::object NPV_ProductExpression = numeric_expr.attr("NPV_ProductExpression"); + py::object MonomialTermExpression = + numeric_expr.attr("MonomialTermExpression"); + py::object DivisionExpression = numeric_expr.attr("DivisionExpression"); + py::object NPV_DivisionExpression = + numeric_expr.attr("NPV_DivisionExpression"); + py::object SumExpression = numeric_expr.attr("SumExpression"); + py::object NPV_SumExpression = numeric_expr.attr("NPV_SumExpression"); + py::object UnaryFunctionExpression = + numeric_expr.attr("UnaryFunctionExpression"); + py::object AbsExpression = numeric_expr.attr("AbsExpression"); + py::object NPV_AbsExpression = numeric_expr.attr("NPV_AbsExpression"); + py::object NPV_UnaryFunctionExpression = + numeric_expr.attr("NPV_UnaryFunctionExpression"); + py::object LinearExpression = numeric_expr.attr("LinearExpression"); + py::object NumericConstant = + py::module_::import("pyomo.core.expr.numvalue").attr("NumericConstant"); + py::object expr_module = py::module_::import("pyomo.core.base.expression"); + py::object _GeneralExpressionData = + expr_module.attr("_GeneralExpressionData"); + py::object ScalarExpression = expr_module.attr("ScalarExpression"); + py::object ScalarIntegral = + py::module_::import("pyomo.dae.integral").attr("ScalarIntegral"); + py::object Integral = + py::module_::import("pyomo.dae.integral").attr("Integral"); + py::object _PyomoUnit = + py::module_::import("pyomo.core.base.units_container").attr("_PyomoUnit"); + py::object builtins = py::module_::import("builtins"); + py::object id = builtins.attr("id"); + py::object len = builtins.attr("len"); + py::dict expr_type_map; +}; + +std::vector> create_vars(int n_vars); +std::vector> create_params(int n_params); +std::vector> create_constants(int n_constants); +std::shared_ptr +appsi_expr_from_pyomo_expr(py::handle expr, py::handle var_map, + py::handle param_map, PyomoExprTypes &expr_types); +std::vector> +appsi_exprs_from_pyomo_exprs(py::list expr_list, py::dict var_map, + py::dict param_map); +py::tuple prep_for_repn(py::handle expr, PyomoExprTypes &expr_types); + +void process_pyomo_vars(PyomoExprTypes &expr_types, py::list pyomo_vars, + py::dict var_map, py::dict param_map, + py::dict var_attrs, py::dict rev_var_map, + py::bool_ _set_name, py::handle symbol_map, + py::handle labeler, py::bool_ _update); + +#endif diff --git a/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp b/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp index 2e490659fab..68efa7d9c26 100644 --- a/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp +++ b/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "fbbt_model.hpp" FBBTObjective::FBBTObjective(std::shared_ptr _expr) diff --git a/pyomo/contrib/appsi/cmodel/src/fbbt_model.hpp b/pyomo/contrib/appsi/cmodel/src/fbbt_model.hpp index 3d1c3a76caa..032ff8c2616 100644 --- a/pyomo/contrib/appsi/cmodel/src/fbbt_model.hpp +++ b/pyomo/contrib/appsi/cmodel/src/fbbt_model.hpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "model_base.hpp" class FBBTConstraint; diff --git a/pyomo/contrib/appsi/cmodel/src/interval.cpp b/pyomo/contrib/appsi/cmodel/src/interval.cpp index f0a1aa2c2bb..a9f26704825 100644 --- a/pyomo/contrib/appsi/cmodel/src/interval.cpp +++ b/pyomo/contrib/appsi/cmodel/src/interval.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "interval.hpp" bool _is_inf(double x) { diff --git a/pyomo/contrib/appsi/cmodel/src/interval.hpp b/pyomo/contrib/appsi/cmodel/src/interval.hpp index c35438887dd..0f3a2a9a816 100644 --- a/pyomo/contrib/appsi/cmodel/src/interval.hpp +++ b/pyomo/contrib/appsi/cmodel/src/interval.hpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #ifndef INTERVAL_HEADER #define INTERVAL_HEADER diff --git a/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp b/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp index 1ce421b7c97..be7ff6d9ac9 100644 --- a/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp +++ b/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "lp_writer.hpp" void write_expr(std::ofstream &f, std::shared_ptr obj, diff --git a/pyomo/contrib/appsi/cmodel/src/lp_writer.hpp b/pyomo/contrib/appsi/cmodel/src/lp_writer.hpp index ee4ad77500a..1cb6adb462b 100644 --- a/pyomo/contrib/appsi/cmodel/src/lp_writer.hpp +++ b/pyomo/contrib/appsi/cmodel/src/lp_writer.hpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "model_base.hpp" class LPBase; diff --git a/pyomo/contrib/appsi/cmodel/src/model_base.cpp b/pyomo/contrib/appsi/cmodel/src/model_base.cpp index ab0b25d8e0d..4503138bf1b 100644 --- a/pyomo/contrib/appsi/cmodel/src/model_base.cpp +++ b/pyomo/contrib/appsi/cmodel/src/model_base.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "model_base.hpp" bool constraint_sorter(std::shared_ptr c1, diff --git a/pyomo/contrib/appsi/cmodel/src/model_base.hpp b/pyomo/contrib/appsi/cmodel/src/model_base.hpp index bc61bc053de..b797976aa2f 100644 --- a/pyomo/contrib/appsi/cmodel/src/model_base.hpp +++ b/pyomo/contrib/appsi/cmodel/src/model_base.hpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #ifndef MODEL_HEADER #define MODEL_HEADER diff --git a/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp b/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp index dc7004abc16..a1b699e6355 100644 --- a/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp +++ b/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "nl_writer.hpp" NLBase::NLBase( diff --git a/pyomo/contrib/appsi/cmodel/src/nl_writer.hpp b/pyomo/contrib/appsi/cmodel/src/nl_writer.hpp index 40e4c9b1222..557d0645e4a 100644 --- a/pyomo/contrib/appsi/cmodel/src/nl_writer.hpp +++ b/pyomo/contrib/appsi/cmodel/src/nl_writer.hpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include "model_base.hpp" class NLBase; diff --git a/pyomo/contrib/appsi/cmodel/tests/__init__.py b/pyomo/contrib/appsi/cmodel/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/appsi/cmodel/tests/__init__.py +++ b/pyomo/contrib/appsi/cmodel/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/appsi/cmodel/tests/test_import.py b/pyomo/contrib/appsi/cmodel/tests/test_import.py index f4647c216ba..9fce3559aff 100644 --- a/pyomo/contrib/appsi/cmodel/tests/test_import.py +++ b/pyomo/contrib/appsi/cmodel/tests/test_import.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common import unittest from pyomo.common.fileutils import find_library, this_file_dir import os diff --git a/pyomo/contrib/appsi/examples/__init__.py b/pyomo/contrib/appsi/examples/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/appsi/examples/__init__.py +++ b/pyomo/contrib/appsi/examples/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index de22d28e0a4..c1500c482d9 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pe from pyomo.contrib import appsi from pyomo.common.timing import HierarchicalTimer diff --git a/pyomo/contrib/appsi/examples/tests/__init__.py b/pyomo/contrib/appsi/examples/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/appsi/examples/tests/__init__.py +++ b/pyomo/contrib/appsi/examples/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/appsi/examples/tests/test_examples.py b/pyomo/contrib/appsi/examples/tests/test_examples.py index d2c88224a7d..7c04271f6d3 100644 --- a/pyomo/contrib/appsi/examples/tests/test_examples.py +++ b/pyomo/contrib/appsi/examples/tests/test_examples.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.appsi.examples import getting_started import pyomo.common.unittest as unittest import pyomo.environ as pe diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 92a0e0c8cbc..957fdc593d4 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.appsi.base import PersistentBase from pyomo.common.config import ( ConfigDict, diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index 5333158239e..aea9edb3faf 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.extensions import ExtensionBuilderFactory from .base import SolverFactory from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs diff --git a/pyomo/contrib/appsi/solvers/__init__.py b/pyomo/contrib/appsi/solvers/__init__.py index 20755d1eb07..359e3f80742 100644 --- a/pyomo/contrib/appsi/solvers/__init__.py +++ b/pyomo/contrib/appsi/solvers/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .gurobi import Gurobi, GurobiResults from .ipopt import Ipopt from .cbc import Cbc diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index a3aae2a9213..c0e1f15c01e 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.tempfiles import TempfileManager from pyomo.common.fileutils import Executable from pyomo.contrib.appsi.base import ( diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index f03bee6ecc5..e8ee204ad63 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.tempfiles import TempfileManager from pyomo.contrib.appsi.base import ( PersistentSolver, diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index a173c69abc6..842cbbf175d 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from collections.abc import Iterable import logging import math diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 3d498f9388e..2619aa2f0c7 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import logging from typing import List, Dict, Optional from pyomo.common.collections import ComponentMap diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index d38a836a2ac..13cda3e3a19 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.tempfiles import TempfileManager from pyomo.common.fileutils import Executable from pyomo.contrib.appsi.base import ( diff --git a/pyomo/contrib/appsi/solvers/tests/__init__.py b/pyomo/contrib/appsi/solvers/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/appsi/solvers/tests/__init__.py +++ b/pyomo/contrib/appsi/solvers/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index b032f5c827e..ed2859fef36 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.errors import PyomoException import pyomo.common.unittest as unittest import pyomo.environ as pe diff --git a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py index 6451db18087..02de50542f3 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import subprocess import sys diff --git a/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py index 6b86deaa535..dc82d04b900 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pe import pyomo.common.unittest as unittest from pyomo.contrib.appsi.cmodel import cmodel_available diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 33f6877aaf8..7b0cbeaf284 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pe from pyomo.common.dependencies import attempt_import import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py index d250923f104..df1d36442b9 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pe import pyomo.common.unittest as unittest from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 0a358c6aedf..2937a5f1b7c 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.appsi.base import ( PersistentBase, PersistentSolver, diff --git a/pyomo/contrib/appsi/tests/__init__.py b/pyomo/contrib/appsi/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/appsi/tests/__init__.py +++ b/pyomo/contrib/appsi/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/appsi/tests/test_base.py b/pyomo/contrib/appsi/tests/test_base.py index 0d67ca4d01a..7700d4f5534 100644 --- a/pyomo/contrib/appsi/tests/test_base.py +++ b/pyomo/contrib/appsi/tests/test_base.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common import unittest from pyomo.contrib import appsi import pyomo.environ as pe diff --git a/pyomo/contrib/appsi/tests/test_fbbt.py b/pyomo/contrib/appsi/tests/test_fbbt.py index f92960769cf..b739367b989 100644 --- a/pyomo/contrib/appsi/tests/test_fbbt.py +++ b/pyomo/contrib/appsi/tests/test_fbbt.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common import unittest import pyomo.environ as pyo from pyomo.contrib import appsi diff --git a/pyomo/contrib/appsi/tests/test_interval.py b/pyomo/contrib/appsi/tests/test_interval.py index 7963cc31665..7c66d63a543 100644 --- a/pyomo/contrib/appsi/tests/test_interval.py +++ b/pyomo/contrib/appsi/tests/test_interval.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available import pyomo.common.unittest as unittest import math diff --git a/pyomo/contrib/appsi/utils/__init__.py b/pyomo/contrib/appsi/utils/__init__.py index f665736fd4a..147d82a923a 100644 --- a/pyomo/contrib/appsi/utils/__init__.py +++ b/pyomo/contrib/appsi/utils/__init__.py @@ -1,2 +1,13 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .get_objective import get_objective from .collect_vars_and_named_exprs import collect_vars_and_named_exprs diff --git a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py index 9027080f08c..7bf273dbf87 100644 --- a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py +++ b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core.expr.visitor import ExpressionValueVisitor, nonpyomo_leaf_types import pyomo.core.expr as EXPR diff --git a/pyomo/contrib/appsi/utils/get_objective.py b/pyomo/contrib/appsi/utils/get_objective.py index 30dd911f9c8..7b43a981622 100644 --- a/pyomo/contrib/appsi/utils/get_objective.py +++ b/pyomo/contrib/appsi/utils/get_objective.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core.base.objective import Objective diff --git a/pyomo/contrib/appsi/utils/tests/__init__.py b/pyomo/contrib/appsi/utils/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/appsi/utils/tests/__init__.py +++ b/pyomo/contrib/appsi/utils/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py index 4c2a167a017..9a5e08385f3 100644 --- a/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py +++ b/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common import unittest import pyomo.environ as pe from pyomo.contrib.appsi.utils import collect_vars_and_named_exprs diff --git a/pyomo/contrib/appsi/writers/__init__.py b/pyomo/contrib/appsi/writers/__init__.py index eeadfa73d03..0d5191e8b97 100644 --- a/pyomo/contrib/appsi/writers/__init__.py +++ b/pyomo/contrib/appsi/writers/__init__.py @@ -1,2 +1,13 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .nl_writer import NLWriter from .lp_writer import LPWriter diff --git a/pyomo/contrib/appsi/writers/config.py b/pyomo/contrib/appsi/writers/config.py index 7a7faadaabe..9d66aba2037 100644 --- a/pyomo/contrib/appsi/writers/config.py +++ b/pyomo/contrib/appsi/writers/config.py @@ -1,3 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + class WriterConfig(object): def __init__(self): self.symbolic_solver_labels = False diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 8a76fa5f9eb..09470202be3 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from typing import List from pyomo.core.base.param import _ParamData from pyomo.core.base.var import _GeneralVarData diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 9c739fd6ebb..c2c93992140 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from typing import List from pyomo.core.base.param import _ParamData from pyomo.core.base.var import _GeneralVarData diff --git a/pyomo/contrib/appsi/writers/tests/__init__.py b/pyomo/contrib/appsi/writers/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/appsi/writers/tests/__init__.py +++ b/pyomo/contrib/appsi/writers/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/appsi/writers/tests/test_nl_writer.py b/pyomo/contrib/appsi/writers/tests/test_nl_writer.py index 3b61a5901c3..d0844263d5a 100644 --- a/pyomo/contrib/appsi/writers/tests/test_nl_writer.py +++ b/pyomo/contrib/appsi/writers/tests/test_nl_writer.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest from pyomo.common.tempfiles import TempfileManager import pyomo.environ as pe diff --git a/pyomo/contrib/benders/__init__.py b/pyomo/contrib/benders/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/benders/__init__.py +++ b/pyomo/contrib/benders/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/benders/examples/__init__.py b/pyomo/contrib/benders/examples/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/benders/examples/__init__.py +++ b/pyomo/contrib/benders/examples/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/benders/tests/__init__.py b/pyomo/contrib/benders/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/benders/tests/__init__.py +++ b/pyomo/contrib/benders/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/community_detection/__init__.py b/pyomo/contrib/community_detection/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/community_detection/__init__.py +++ b/pyomo/contrib/community_detection/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/community_detection/community_graph.py b/pyomo/contrib/community_detection/community_graph.py index f0a1f9149bd..d1bd49df20c 100644 --- a/pyomo/contrib/community_detection/community_graph.py +++ b/pyomo/contrib/community_detection/community_graph.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Model Graph Generator Code""" from pyomo.common.dependencies import networkx as nx diff --git a/pyomo/contrib/community_detection/detection.py b/pyomo/contrib/community_detection/detection.py index c5366394530..9fe7005f1f2 100644 --- a/pyomo/contrib/community_detection/detection.py +++ b/pyomo/contrib/community_detection/detection.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Main module for community detection integration with Pyomo models. diff --git a/pyomo/contrib/community_detection/event_log.py b/pyomo/contrib/community_detection/event_log.py index 30e28257de8..09b1039a8f7 100644 --- a/pyomo/contrib/community_detection/event_log.py +++ b/pyomo/contrib/community_detection/event_log.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Logger function for community_graph.py """ from logging import getLogger diff --git a/pyomo/contrib/community_detection/plugins.py b/pyomo/contrib/community_detection/plugins.py index 0cdc95ad02a..578da835d5e 100644 --- a/pyomo/contrib/community_detection/plugins.py +++ b/pyomo/contrib/community_detection/plugins.py @@ -1,2 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + def load(): import pyomo.contrib.community_detection.detection diff --git a/pyomo/contrib/community_detection/tests/__init__.py b/pyomo/contrib/community_detection/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/community_detection/tests/__init__.py +++ b/pyomo/contrib/community_detection/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/cp/__init__.py b/pyomo/contrib/cp/__init__.py index c51160bf931..71ba479523f 100644 --- a/pyomo/contrib/cp/__init__.py +++ b/pyomo/contrib/cp/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.cp.interval_var import ( IntervalVar, IntervalVarStartTime, diff --git a/pyomo/contrib/cp/repn/__init__.py b/pyomo/contrib/cp/repn/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/cp/repn/__init__.py +++ b/pyomo/contrib/cp/repn/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/cp/scheduling_expr/__init__.py b/pyomo/contrib/cp/scheduling_expr/__init__.py index 8b137891791..d93cfd77b3c 100644 --- a/pyomo/contrib/cp/scheduling_expr/__init__.py +++ b/pyomo/contrib/cp/scheduling_expr/__init__.py @@ -1 +1,10 @@ - +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/cp/tests/__init__.py b/pyomo/contrib/cp/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/cp/tests/__init__.py +++ b/pyomo/contrib/cp/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/cp/transform/__init__.py b/pyomo/contrib/cp/transform/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/cp/transform/__init__.py +++ b/pyomo/contrib/cp/transform/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/doe/examples/__init__.py b/pyomo/contrib/doe/examples/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/doe/examples/__init__.py +++ b/pyomo/contrib/doe/examples/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/doe/tests/__init__.py b/pyomo/contrib/doe/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/doe/tests/__init__.py +++ b/pyomo/contrib/doe/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/example/__init__.py b/pyomo/contrib/example/__init__.py index 7f2d08a0292..7a9e6e76de4 100644 --- a/pyomo/contrib/example/__init__.py +++ b/pyomo/contrib/example/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # import symbols and sub-packages # diff --git a/pyomo/contrib/example/bar.py b/pyomo/contrib/example/bar.py index 295540d3318..eb39c5f8748 100644 --- a/pyomo/contrib/example/bar.py +++ b/pyomo/contrib/example/bar.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + b = "1" diff --git a/pyomo/contrib/example/foo.py b/pyomo/contrib/example/foo.py index 1337a530cbc..a1a10b1dd62 100644 --- a/pyomo/contrib/example/foo.py +++ b/pyomo/contrib/example/foo.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + a = 1 diff --git a/pyomo/contrib/example/plugins/__init__.py b/pyomo/contrib/example/plugins/__init__.py index dc71adec9dc..0c6c248c122 100644 --- a/pyomo/contrib/example/plugins/__init__.py +++ b/pyomo/contrib/example/plugins/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Define a 'load()' function, which simply imports # sub-packages that define plugin classes. diff --git a/pyomo/contrib/example/plugins/ex_plugin.py b/pyomo/contrib/example/plugins/ex_plugin.py index 504605205f4..0a23afc0158 100644 --- a/pyomo/contrib/example/plugins/ex_plugin.py +++ b/pyomo/contrib/example/plugins/ex_plugin.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core.base import Transformation, TransformationFactory diff --git a/pyomo/contrib/example/tests/__init__.py b/pyomo/contrib/example/tests/__init__.py index 5a1047f74ae..3ecae26215c 100644 --- a/pyomo/contrib/example/tests/__init__.py +++ b/pyomo/contrib/example/tests/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # Tests for pyomo.contrib.example diff --git a/pyomo/contrib/fbbt/__init__.py b/pyomo/contrib/fbbt/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/fbbt/__init__.py +++ b/pyomo/contrib/fbbt/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/fbbt/tests/__init__.py b/pyomo/contrib/fbbt/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/fbbt/tests/__init__.py +++ b/pyomo/contrib/fbbt/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/fbbt/tests/test_interval.py b/pyomo/contrib/fbbt/tests/test_interval.py index 59c62be4e84..d5dc7b54ff5 100644 --- a/pyomo/contrib/fbbt/tests/test_interval.py +++ b/pyomo/contrib/fbbt/tests/test_interval.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import math import pyomo.common.unittest as unittest from pyomo.common.dependencies import numpy as np, numpy_available diff --git a/pyomo/contrib/fme/__init__.py b/pyomo/contrib/fme/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/fme/__init__.py +++ b/pyomo/contrib/fme/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/fme/tests/__init__.py b/pyomo/contrib/fme/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/fme/tests/__init__.py +++ b/pyomo/contrib/fme/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/gdp_bounds/__init__.py b/pyomo/contrib/gdp_bounds/__init__.py index 3a02f9e5f8e..4918f6dfa0e 100644 --- a/pyomo/contrib/gdp_bounds/__init__.py +++ b/pyomo/contrib/gdp_bounds/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.contrib.gdp_bounds.plugins diff --git a/pyomo/contrib/gdp_bounds/info.py b/pyomo/contrib/gdp_bounds/info.py index 3ee87041d25..f7e83ee62c9 100644 --- a/pyomo/contrib/gdp_bounds/info.py +++ b/pyomo/contrib/gdp_bounds/info.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Provides functions for retrieving disjunctive variable bound information stored on a model.""" from pyomo.common.collections import ComponentMap diff --git a/pyomo/contrib/gdp_bounds/tests/__init__.py b/pyomo/contrib/gdp_bounds/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/gdp_bounds/tests/__init__.py +++ b/pyomo/contrib/gdp_bounds/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/gdp_bounds/tests/test_gdp_bounds.py b/pyomo/contrib/gdp_bounds/tests/test_gdp_bounds.py index e856ae247f3..551236c7d97 100644 --- a/pyomo/contrib/gdp_bounds/tests/test_gdp_bounds.py +++ b/pyomo/contrib/gdp_bounds/tests/test_gdp_bounds.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests explicit bound to variable bound transformation module.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/gdpopt/__init__.py b/pyomo/contrib/gdpopt/__init__.py index 307fbc1594c..f74855f0206 100644 --- a/pyomo/contrib/gdpopt/__init__.py +++ b/pyomo/contrib/gdpopt/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + __version__ = (22, 5, 13) # Note: date-based version number diff --git a/pyomo/contrib/gdpopt/tests/__init__.py b/pyomo/contrib/gdpopt/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/gdpopt/tests/__init__.py +++ b/pyomo/contrib/gdpopt/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/iis/__init__.py b/pyomo/contrib/iis/__init__.py index eb9f60b8928..29f5d4f3d40 100644 --- a/pyomo/contrib/iis/__init__.py +++ b/pyomo/contrib/iis/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.iis.iis import write_iis diff --git a/pyomo/contrib/iis/iis.py b/pyomo/contrib/iis/iis.py index bd192d04eb3..a279ce0aac3 100644 --- a/pyomo/contrib/iis/iis.py +++ b/pyomo/contrib/iis/iis.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ This module contains functions for computing an irreducible infeasible set for a Pyomo MILP or LP using a specified commercial solver, one of CPLEX, diff --git a/pyomo/contrib/iis/tests/__init__.py b/pyomo/contrib/iis/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/iis/tests/__init__.py +++ b/pyomo/contrib/iis/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/iis/tests/test_iis.py b/pyomo/contrib/iis/tests/test_iis.py index b1b675d5081..8343798741a 100644 --- a/pyomo/contrib/iis/tests/test_iis.py +++ b/pyomo/contrib/iis/tests/test_iis.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest import pyomo.environ as pyo from pyomo.contrib.iis import write_iis diff --git a/pyomo/contrib/incidence_analysis/__init__.py b/pyomo/contrib/incidence_analysis/__init__.py index ee078690f2f..612b4fe7d02 100644 --- a/pyomo/contrib/incidence_analysis/__init__.py +++ b/pyomo/contrib/incidence_analysis/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .triangularize import block_triangularize from .matching import maximum_matching from .interface import IncidenceGraphInterface, get_bipartite_incidence_graph diff --git a/pyomo/contrib/incidence_analysis/common/__init__.py b/pyomo/contrib/incidence_analysis/common/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/incidence_analysis/common/__init__.py +++ b/pyomo/contrib/incidence_analysis/common/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/incidence_analysis/common/tests/__init__.py b/pyomo/contrib/incidence_analysis/common/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/incidence_analysis/common/tests/__init__.py +++ b/pyomo/contrib/incidence_analysis/common/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/incidence_analysis/tests/__init__.py b/pyomo/contrib/incidence_analysis/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/incidence_analysis/tests/__init__.py +++ b/pyomo/contrib/incidence_analysis/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/interior_point/__init__.py b/pyomo/contrib/interior_point/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/interior_point/__init__.py +++ b/pyomo/contrib/interior_point/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/interior_point/examples/__init__.py b/pyomo/contrib/interior_point/examples/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/interior_point/examples/__init__.py +++ b/pyomo/contrib/interior_point/examples/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/interior_point/linalg/__init__.py b/pyomo/contrib/interior_point/linalg/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/interior_point/linalg/__init__.py +++ b/pyomo/contrib/interior_point/linalg/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/interior_point/linalg/base_linear_solver_interface.py b/pyomo/contrib/interior_point/linalg/base_linear_solver_interface.py index 722a5c55e8d..2bc7fe2eee5 100644 --- a/pyomo/contrib/interior_point/linalg/base_linear_solver_interface.py +++ b/pyomo/contrib/interior_point/linalg/base_linear_solver_interface.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.pynumero.linalg.base import DirectLinearSolverInterface from abc import ABCMeta, abstractmethod import logging diff --git a/pyomo/contrib/interior_point/linalg/ma27_interface.py b/pyomo/contrib/interior_point/linalg/ma27_interface.py index 7bb98b0b6fd..0a28e50578d 100644 --- a/pyomo/contrib/interior_point/linalg/ma27_interface.py +++ b/pyomo/contrib/interior_point/linalg/ma27_interface.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .base_linear_solver_interface import IPLinearSolverInterface from pyomo.contrib.pynumero.linalg.base import LinearSolverStatus, LinearSolverResults from pyomo.contrib.pynumero.linalg.ma27_interface import MA27 diff --git a/pyomo/contrib/interior_point/linalg/scipy_interface.py b/pyomo/contrib/interior_point/linalg/scipy_interface.py index b7b7923bad4..87b0cad8ea0 100644 --- a/pyomo/contrib/interior_point/linalg/scipy_interface.py +++ b/pyomo/contrib/interior_point/linalg/scipy_interface.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .base_linear_solver_interface import IPLinearSolverInterface from pyomo.contrib.pynumero.linalg.base import LinearSolverResults from scipy.linalg import eigvals diff --git a/pyomo/contrib/interior_point/linalg/tests/__init__.py b/pyomo/contrib/interior_point/linalg/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/interior_point/linalg/tests/__init__.py +++ b/pyomo/contrib/interior_point/linalg/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/interior_point/linalg/tests/test_linear_solvers.py b/pyomo/contrib/interior_point/linalg/tests/test_linear_solvers.py index 35863aa7cf7..c13aad215cc 100644 --- a/pyomo/contrib/interior_point/linalg/tests/test_linear_solvers.py +++ b/pyomo/contrib/interior_point/linalg/tests/test_linear_solvers.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest from pyomo.common.dependencies import attempt_import diff --git a/pyomo/contrib/interior_point/linalg/tests/test_realloc.py b/pyomo/contrib/interior_point/linalg/tests/test_realloc.py index bfe089dc602..7dce4755261 100644 --- a/pyomo/contrib/interior_point/linalg/tests/test_realloc.py +++ b/pyomo/contrib/interior_point/linalg/tests/test_realloc.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest from pyomo.common.dependencies import attempt_import diff --git a/pyomo/contrib/interior_point/tests/__init__.py b/pyomo/contrib/interior_point/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/interior_point/tests/__init__.py +++ b/pyomo/contrib/interior_point/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/interior_point/tests/test_realloc.py b/pyomo/contrib/interior_point/tests/test_realloc.py index b3758c946d4..dcf94eb6da7 100644 --- a/pyomo/contrib/interior_point/tests/test_realloc.py +++ b/pyomo/contrib/interior_point/tests/test_realloc.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest import pyomo.environ as pe from pyomo.core.base import ConcreteModel, Var, Constraint, Objective diff --git a/pyomo/contrib/latex_printer/__init__.py b/pyomo/contrib/latex_printer/__init__.py index 27c1552017a..7208b1e7d64 100644 --- a/pyomo/contrib/latex_printer/__init__.py +++ b/pyomo/contrib/latex_printer/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index b84f9a420fc..f9150f700a3 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects diff --git a/pyomo/contrib/latex_printer/tests/__init__.py b/pyomo/contrib/latex_printer/tests/__init__.py index 8b137891791..d93cfd77b3c 100644 --- a/pyomo/contrib/latex_printer/tests/__init__.py +++ b/pyomo/contrib/latex_printer/tests/__init__.py @@ -1 +1,10 @@ - +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/latex_printer/tests/test_latex_printer.py b/pyomo/contrib/latex_printer/tests/test_latex_printer.py index e9de4e4ad05..1797e0a39a0 100644 --- a/pyomo/contrib/latex_printer/tests/test_latex_printer.py +++ b/pyomo/contrib/latex_printer/tests/test_latex_printer.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects diff --git a/pyomo/contrib/latex_printer/tests/test_latex_printer_vartypes.py b/pyomo/contrib/latex_printer/tests/test_latex_printer_vartypes.py index 14e9ebbe0e6..dc571030fde 100644 --- a/pyomo/contrib/latex_printer/tests/test_latex_printer_vartypes.py +++ b/pyomo/contrib/latex_printer/tests/test_latex_printer_vartypes.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects diff --git a/pyomo/contrib/mindtpy/__init__.py b/pyomo/contrib/mindtpy/__init__.py index 8dcd085211f..94a91238819 100644 --- a/pyomo/contrib/mindtpy/__init__.py +++ b/pyomo/contrib/mindtpy/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + __version__ = (1, 0, 0) diff --git a/pyomo/contrib/mindtpy/config_options.py b/pyomo/contrib/mindtpy/config_options.py index ed0c86baae9..f1c4a23d46e 100644 --- a/pyomo/contrib/mindtpy/config_options.py +++ b/pyomo/contrib/mindtpy/config_options.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- import logging from pyomo.common.config import ( diff --git a/pyomo/contrib/mindtpy/tests/MINLP4_simple.py b/pyomo/contrib/mindtpy/tests/MINLP4_simple.py index 7b57c6b8f0d..684fdf4a932 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP4_simple.py +++ b/pyomo/contrib/mindtpy/tests/MINLP4_simple.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """ Example 1 in Paper 'Using regularization and second order information in outer approximation for convex MINLP' diff --git a/pyomo/contrib/mindtpy/tests/MINLP5_simple.py b/pyomo/contrib/mindtpy/tests/MINLP5_simple.py index 5ab5f98b894..cb78f6e0804 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP5_simple.py +++ b/pyomo/contrib/mindtpy/tests/MINLP5_simple.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Example in paper 'Using regularization and second order information in outer approximation for convex MINLP' diff --git a/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py b/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py index 9c1f33e80cc..789dfea6191 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py +++ b/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.dependencies import numpy as np import pyomo.common.dependencies.scipy.sparse as scipy_sparse from pyomo.common.dependencies import attempt_import diff --git a/pyomo/contrib/mindtpy/tests/__init__.py b/pyomo/contrib/mindtpy/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/mindtpy/tests/__init__.py +++ b/pyomo/contrib/mindtpy/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/mindtpy/tests/constraint_qualification_example.py b/pyomo/contrib/mindtpy/tests/constraint_qualification_example.py index 6038f9a74eb..75ec56df738 100644 --- a/pyomo/contrib/mindtpy/tests/constraint_qualification_example.py +++ b/pyomo/contrib/mindtpy/tests/constraint_qualification_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """ Example of constraint qualification. diff --git a/pyomo/contrib/mindtpy/tests/eight_process_problem.py b/pyomo/contrib/mindtpy/tests/eight_process_problem.py index d3876a9dc44..8233fc52c53 100644 --- a/pyomo/contrib/mindtpy/tests/eight_process_problem.py +++ b/pyomo/contrib/mindtpy/tests/eight_process_problem.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Re-implementation of eight-process problem. diff --git a/pyomo/contrib/mindtpy/tests/feasibility_pump1.py b/pyomo/contrib/mindtpy/tests/feasibility_pump1.py index e0a611c1ed2..3149ccef6e4 100644 --- a/pyomo/contrib/mindtpy/tests/feasibility_pump1.py +++ b/pyomo/contrib/mindtpy/tests/feasibility_pump1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Example 1 in paper 'A Feasibility Pump for mixed integer nonlinear programs' diff --git a/pyomo/contrib/mindtpy/tests/feasibility_pump2.py b/pyomo/contrib/mindtpy/tests/feasibility_pump2.py index 48b98dc5800..9fed8238fe1 100644 --- a/pyomo/contrib/mindtpy/tests/feasibility_pump2.py +++ b/pyomo/contrib/mindtpy/tests/feasibility_pump2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Example 2 in paper 'A Feasibility Pump for mixed integer nonlinear programs' diff --git a/pyomo/contrib/mindtpy/tests/from_proposal.py b/pyomo/contrib/mindtpy/tests/from_proposal.py index 6ddab15ee53..e34fddedcd3 100644 --- a/pyomo/contrib/mindtpy/tests/from_proposal.py +++ b/pyomo/contrib/mindtpy/tests/from_proposal.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """ See David Bernal PhD proposal example. diff --git a/pyomo/contrib/mindtpy/tests/nonconvex1.py b/pyomo/contrib/mindtpy/tests/nonconvex1.py index 94a4de29405..60115a52c32 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex1.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex1.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Problem A in paper 'Outer approximation algorithms for separable nonconvex mixed-integer nonlinear programs' diff --git a/pyomo/contrib/mindtpy/tests/nonconvex2.py b/pyomo/contrib/mindtpy/tests/nonconvex2.py index 525db1292c1..ac48167b350 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex2.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Problem B in paper 'Outer approximation algorithms for separable nonconvex mixed-integer nonlinear programs' diff --git a/pyomo/contrib/mindtpy/tests/nonconvex3.py b/pyomo/contrib/mindtpy/tests/nonconvex3.py index b08deb67b63..8337beb8d68 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex3.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex3.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Problem C in paper 'Outer approximation algorithms for separable nonconvex mixed-integer nonlinear programs'. The problem in the paper has two optimal solution. Variable y4 and y6 are symmetric. Therefore, we remove variable y6 for simplification. diff --git a/pyomo/contrib/mindtpy/tests/nonconvex4.py b/pyomo/contrib/mindtpy/tests/nonconvex4.py index c30fb9922a0..79e6465239f 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex4.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex4.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Problem D in paper 'Outer approximation algorithms for separable nonconvex mixed-integer nonlinear programs' diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py index b5bfbe62553..07f2b1aaff5 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Tests for the MindtPy solver.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py index dcb5c4bce75..c7b47b7fde2 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Tests for the MindtPy solver.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_global.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_global.py index 0fa19b30d9c..dbe9270c363 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_global.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_global.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Tests for the MindtPy solver.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_global_lp_nlp.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_global_lp_nlp.py index 259cfe9dd7c..08bfb8df2de 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_global_lp_nlp.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_global_lp_nlp.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Tests for global LP/NLP in the MindtPy solver.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_regularization.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_regularization.py index 4c2ae4d1220..33f296083ed 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_regularization.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_regularization.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Tests for the MindtPy solver.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_solution_pool.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_solution_pool.py index 7a9898d3c7b..775d1a4e117 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_solution_pool.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_solution_pool.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests for solution pool in the MindtPy solver.""" from pyomo.core.expr.calculus.diff_with_sympy import differentiate_available diff --git a/pyomo/contrib/mpc/data/tests/__init__.py b/pyomo/contrib/mpc/data/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/mpc/data/tests/__init__.py +++ b/pyomo/contrib/mpc/data/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/mpc/examples/__init__.py b/pyomo/contrib/mpc/examples/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/mpc/examples/__init__.py +++ b/pyomo/contrib/mpc/examples/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/mpc/examples/cstr/__init__.py b/pyomo/contrib/mpc/examples/cstr/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/mpc/examples/cstr/__init__.py +++ b/pyomo/contrib/mpc/examples/cstr/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/mpc/examples/cstr/tests/__init__.py b/pyomo/contrib/mpc/examples/cstr/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/mpc/examples/cstr/tests/__init__.py +++ b/pyomo/contrib/mpc/examples/cstr/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/mpc/interfaces/tests/__init__.py b/pyomo/contrib/mpc/interfaces/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/mpc/interfaces/tests/__init__.py +++ b/pyomo/contrib/mpc/interfaces/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/mpc/modeling/tests/__init__.py b/pyomo/contrib/mpc/modeling/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/mpc/modeling/tests/__init__.py +++ b/pyomo/contrib/mpc/modeling/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/multistart/__init__.py b/pyomo/contrib/multistart/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/multistart/__init__.py +++ b/pyomo/contrib/multistart/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/multistart/high_conf_stop.py b/pyomo/contrib/multistart/high_conf_stop.py index 96b350557ae..153d22e9edd 100644 --- a/pyomo/contrib/multistart/high_conf_stop.py +++ b/pyomo/contrib/multistart/high_conf_stop.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Utility functions for the high confidence stopping rule. This stopping criterion operates by estimating the amount of missing optima, diff --git a/pyomo/contrib/multistart/plugins.py b/pyomo/contrib/multistart/plugins.py index 297b2f059cc..acfd2f06274 100644 --- a/pyomo/contrib/multistart/plugins.py +++ b/pyomo/contrib/multistart/plugins.py @@ -1,2 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + def load(): import pyomo.contrib.multistart.multi diff --git a/pyomo/contrib/multistart/reinit.py b/pyomo/contrib/multistart/reinit.py index de10fe3ba8b..14dce0352cc 100644 --- a/pyomo/contrib/multistart/reinit.py +++ b/pyomo/contrib/multistart/reinit.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Helper functions for variable reinitialization.""" import logging diff --git a/pyomo/contrib/multistart/test_multi.py b/pyomo/contrib/multistart/test_multi.py index 16c8563ae9e..a8e3d420266 100644 --- a/pyomo/contrib/multistart/test_multi.py +++ b/pyomo/contrib/multistart/test_multi.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import logging from itertools import product diff --git a/pyomo/contrib/parmest/utils/create_ef.py b/pyomo/contrib/parmest/utils/create_ef.py index 2e6c8541fa1..7a7dd72f7da 100644 --- a/pyomo/contrib/parmest/utils/create_ef.py +++ b/pyomo/contrib/parmest/utils/create_ef.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # This software is distributed under the 3-clause BSD License. # Copied with minor modifications from create_EF in mpisppy/utils/sputils.py # from the mpi-sppy library (https://github.com/Pyomo/mpi-sppy). diff --git a/pyomo/contrib/parmest/utils/scenario_tree.py b/pyomo/contrib/parmest/utils/scenario_tree.py index d46a8f2c5f0..46b02b8ddc1 100644 --- a/pyomo/contrib/parmest/utils/scenario_tree.py +++ b/pyomo/contrib/parmest/utils/scenario_tree.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # This software is distributed under the 3-clause BSD License. # Copied with minor modifications from mpisppy/scenario_tree.py # from the mpi-sppy library (https://github.com/Pyomo/mpi-sppy). diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 33cfc6f1606..9e15cfd6670 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.piecewise.piecewise_linear_expression import ( PiecewiseLinearExpression, ) diff --git a/pyomo/contrib/piecewise/tests/__init__.py b/pyomo/contrib/piecewise/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/piecewise/tests/__init__.py +++ b/pyomo/contrib/piecewise/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/piecewise/transform/__init__.py b/pyomo/contrib/piecewise/transform/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/piecewise/transform/__init__.py +++ b/pyomo/contrib/piecewise/transform/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/preprocessing/__init__.py b/pyomo/contrib/preprocessing/__init__.py index dcd444ad312..40d38e74d23 100644 --- a/pyomo/contrib/preprocessing/__init__.py +++ b/pyomo/contrib/preprocessing/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.contrib.preprocessing.plugins diff --git a/pyomo/contrib/preprocessing/plugins/__init__.py b/pyomo/contrib/preprocessing/plugins/__init__.py index 12eee351308..ae5dfe31682 100644 --- a/pyomo/contrib/preprocessing/plugins/__init__.py +++ b/pyomo/contrib/preprocessing/plugins/__init__.py @@ -1,3 +1,15 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + def load(): import pyomo.contrib.preprocessing.plugins.deactivate_trivial_constraints import pyomo.contrib.preprocessing.plugins.detect_fixed_vars diff --git a/pyomo/contrib/preprocessing/plugins/constraint_tightener.py b/pyomo/contrib/preprocessing/plugins/constraint_tightener.py index 4c8b28e0319..7c5495e72b8 100644 --- a/pyomo/contrib/preprocessing/plugins/constraint_tightener.py +++ b/pyomo/contrib/preprocessing/plugins/constraint_tightener.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import logging from pyomo.common import deprecated diff --git a/pyomo/contrib/preprocessing/plugins/int_to_binary.py b/pyomo/contrib/preprocessing/plugins/int_to_binary.py index 6ed6c3a9cfa..6a08dab9645 100644 --- a/pyomo/contrib/preprocessing/plugins/int_to_binary.py +++ b/pyomo/contrib/preprocessing/plugins/int_to_binary.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Transformation to reformulate integer variables into binary.""" from math import floor, log diff --git a/pyomo/contrib/preprocessing/tests/__init__.py b/pyomo/contrib/preprocessing/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/preprocessing/tests/__init__.py +++ b/pyomo/contrib/preprocessing/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/preprocessing/tests/test_bounds_to_vars_xfrm.py b/pyomo/contrib/preprocessing/tests/test_bounds_to_vars_xfrm.py index c2b8acd3e49..534ba11d22f 100644 --- a/pyomo/contrib/preprocessing/tests/test_bounds_to_vars_xfrm.py +++ b/pyomo/contrib/preprocessing/tests/test_bounds_to_vars_xfrm.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests explicit bound to variable bound transformation module.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/preprocessing/tests/test_constraint_tightener.py b/pyomo/contrib/preprocessing/tests/test_constraint_tightener.py index 8f36bee15a1..808eb688087 100644 --- a/pyomo/contrib/preprocessing/tests/test_constraint_tightener.py +++ b/pyomo/contrib/preprocessing/tests/test_constraint_tightener.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests the Bounds Tightening module.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/preprocessing/tests/test_deactivate_trivial_constraints.py b/pyomo/contrib/preprocessing/tests/test_deactivate_trivial_constraints.py index fa0ca6cfa9a..0c89b0d7d86 100644 --- a/pyomo/contrib/preprocessing/tests/test_deactivate_trivial_constraints.py +++ b/pyomo/contrib/preprocessing/tests/test_deactivate_trivial_constraints.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: utf-8 -*- """Tests deactivation of trivial constraints.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/preprocessing/tests/test_detect_fixed_vars.py b/pyomo/contrib/preprocessing/tests/test_detect_fixed_vars.py index b3c72531f77..f18ac5c3b8a 100644 --- a/pyomo/contrib/preprocessing/tests/test_detect_fixed_vars.py +++ b/pyomo/contrib/preprocessing/tests/test_detect_fixed_vars.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests detection of fixed variables.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/preprocessing/tests/test_equality_propagate.py b/pyomo/contrib/preprocessing/tests/test_equality_propagate.py index b77f5c5f3f5..a2c00a15e72 100644 --- a/pyomo/contrib/preprocessing/tests/test_equality_propagate.py +++ b/pyomo/contrib/preprocessing/tests/test_equality_propagate.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests the equality set propagation module.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/preprocessing/tests/test_init_vars.py b/pyomo/contrib/preprocessing/tests/test_init_vars.py index e52c9fd5cc8..6ddf859e930 100644 --- a/pyomo/contrib/preprocessing/tests/test_init_vars.py +++ b/pyomo/contrib/preprocessing/tests/test_init_vars.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests initialization of uninitialized variables.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/preprocessing/tests/test_strip_bounds.py b/pyomo/contrib/preprocessing/tests/test_strip_bounds.py index a8526c613c4..4eec0cf7434 100644 --- a/pyomo/contrib/preprocessing/tests/test_strip_bounds.py +++ b/pyomo/contrib/preprocessing/tests/test_strip_bounds.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests stripping of variable bounds.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/preprocessing/tests/test_var_aggregator.py b/pyomo/contrib/preprocessing/tests/test_var_aggregator.py index 1f2c06dd0d1..b3630225402 100644 --- a/pyomo/contrib/preprocessing/tests/test_var_aggregator.py +++ b/pyomo/contrib/preprocessing/tests/test_var_aggregator.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests the variable aggregation module.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/preprocessing/tests/test_zero_sum_propagate.py b/pyomo/contrib/preprocessing/tests/test_zero_sum_propagate.py index bec889c7635..ce88b8ca86e 100644 --- a/pyomo/contrib/preprocessing/tests/test_zero_sum_propagate.py +++ b/pyomo/contrib/preprocessing/tests/test_zero_sum_propagate.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests the zero sum propagation module.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/preprocessing/tests/test_zero_term_removal.py b/pyomo/contrib/preprocessing/tests/test_zero_term_removal.py index d1b74822747..abe79034ec2 100644 --- a/pyomo/contrib/preprocessing/tests/test_zero_term_removal.py +++ b/pyomo/contrib/preprocessing/tests/test_zero_term_removal.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Tests detection of zero terms.""" import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/preprocessing/util.py b/pyomo/contrib/preprocessing/util.py index 69182f56656..ffc72f46902 100644 --- a/pyomo/contrib/preprocessing/util.py +++ b/pyomo/contrib/preprocessing/util.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import logging from io import StringIO diff --git a/pyomo/contrib/pynumero/algorithms/solvers/__init__.py b/pyomo/contrib/pynumero/algorithms/solvers/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/__init__.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/__init__.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/__init__.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/examples/callback/__init__.py b/pyomo/contrib/pynumero/examples/callback/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/pynumero/examples/callback/__init__.py +++ b/pyomo/contrib/pynumero/examples/callback/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/examples/callback/cyipopt_callback.py b/pyomo/contrib/pynumero/examples/callback/cyipopt_callback.py index 6bd86c006a1..58367e0bc5a 100644 --- a/pyomo/contrib/pynumero/examples/callback/cyipopt_callback.py +++ b/pyomo/contrib/pynumero/examples/callback/cyipopt_callback.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from pyomo.contrib.pynumero.examples.callback.reactor_design import model as m import logging diff --git a/pyomo/contrib/pynumero/examples/callback/cyipopt_callback_halt.py b/pyomo/contrib/pynumero/examples/callback/cyipopt_callback_halt.py index 18fad2bbcd8..55138c99318 100644 --- a/pyomo/contrib/pynumero/examples/callback/cyipopt_callback_halt.py +++ b/pyomo/contrib/pynumero/examples/callback/cyipopt_callback_halt.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from pyomo.contrib.pynumero.examples.callback.reactor_design import model as m diff --git a/pyomo/contrib/pynumero/examples/callback/cyipopt_functor_callback.py b/pyomo/contrib/pynumero/examples/callback/cyipopt_functor_callback.py index ca452f33c90..b52897d58b1 100644 --- a/pyomo/contrib/pynumero/examples/callback/cyipopt_functor_callback.py +++ b/pyomo/contrib/pynumero/examples/callback/cyipopt_functor_callback.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from pyomo.contrib.pynumero.examples.callback.reactor_design import model as m from pyomo.common.dependencies import pandas as pd diff --git a/pyomo/contrib/pynumero/examples/callback/reactor_design.py b/pyomo/contrib/pynumero/examples/callback/reactor_design.py index 927b25f9bc9..98fbc93ee58 100644 --- a/pyomo/contrib/pynumero/examples/callback/reactor_design.py +++ b/pyomo/contrib/pynumero/examples/callback/reactor_design.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ from pyomo.core import * diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/__init__.py b/pyomo/contrib/pynumero/examples/external_grey_box/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/__init__.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/__init__.py b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/__init__.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/generate_data.py b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/generate_data.py index 5bf0defbb8d..3af10a465b7 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/generate_data.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/generate_data.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo import numpy.random as rnd import pyomo.contrib.pynumero.examples.external_grey_box.param_est.models as pm diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/models.py b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/models.py index a8b9befb188..a7962f5634d 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/models.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/models.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pyo from pyomo.contrib.pynumero.interfaces.external_grey_box import ( ExternalGreyBoxModel, diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/perform_estimation.py b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/perform_estimation.py index f27192f9281..9a18c7fb54b 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/perform_estimation.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/perform_estimation.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import sys import pyomo.environ as pyo import numpy.random as rnd diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/__init__.py b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/__init__.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/examples/mumps_example.py b/pyomo/contrib/pynumero/examples/mumps_example.py index 938fab99279..7f96bfce4ae 100644 --- a/pyomo/contrib/pynumero/examples/mumps_example.py +++ b/pyomo/contrib/pynumero/examples/mumps_example.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import numpy as np import scipy.sparse as sp from scipy.linalg import hilbert diff --git a/pyomo/contrib/pynumero/examples/parallel_matvec.py b/pyomo/contrib/pynumero/examples/parallel_matvec.py index 26a2ec9a632..78095fe1acd 100644 --- a/pyomo/contrib/pynumero/examples/parallel_matvec.py +++ b/pyomo/contrib/pynumero/examples/parallel_matvec.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import numpy as np from pyomo.common.dependencies import mpi4py from pyomo.contrib.pynumero.sparse.mpi_block_vector import MPIBlockVector diff --git a/pyomo/contrib/pynumero/examples/parallel_vector_ops.py b/pyomo/contrib/pynumero/examples/parallel_vector_ops.py index 4b155ce7493..83e40a342db 100644 --- a/pyomo/contrib/pynumero/examples/parallel_vector_ops.py +++ b/pyomo/contrib/pynumero/examples/parallel_vector_ops.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import numpy as np from pyomo.common.dependencies import mpi4py from pyomo.contrib.pynumero.sparse.mpi_block_vector import MPIBlockVector diff --git a/pyomo/contrib/pynumero/examples/sqp.py b/pyomo/contrib/pynumero/examples/sqp.py index 7d321676817..15ad62670f2 100644 --- a/pyomo/contrib/pynumero/examples/sqp.py +++ b/pyomo/contrib/pynumero/examples/sqp.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.pynumero.interfaces.nlp import NLP from pyomo.contrib.pynumero.sparse import BlockVector, BlockMatrix from pyomo.contrib.pynumero.linalg.ma27_interface import MA27 diff --git a/pyomo/contrib/pynumero/examples/tests/__init__.py b/pyomo/contrib/pynumero/examples/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/pynumero/examples/tests/__init__.py +++ b/pyomo/contrib/pynumero/examples/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/examples/tests/test_examples.py b/pyomo/contrib/pynumero/examples/tests/test_examples.py index 5c7993ebbb6..d4a5313908c 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_examples.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.pynumero.dependencies import numpy_available, scipy_available import pyomo.common.unittest as unittest diff --git a/pyomo/contrib/pynumero/examples/tests/test_mpi_examples.py b/pyomo/contrib/pynumero/examples/tests/test_mpi_examples.py index 68fe907a8ef..554305f23c9 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_mpi_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_mpi_examples.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest from pyomo.contrib.pynumero.dependencies import ( diff --git a/pyomo/contrib/pynumero/interfaces/nlp_projections.py b/pyomo/contrib/pynumero/interfaces/nlp_projections.py index 68cb0eef15f..3f4e8a88c60 100644 --- a/pyomo/contrib/pynumero/interfaces/nlp_projections.py +++ b/pyomo/contrib/pynumero/interfaces/nlp_projections.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.pynumero.interfaces.nlp import NLP, ExtendedNLP import numpy as np import scipy.sparse as sp diff --git a/pyomo/contrib/pynumero/interfaces/tests/external_grey_box_models.py b/pyomo/contrib/pynumero/interfaces/tests/external_grey_box_models.py index e65e9a7eb5c..d7ec499eaf9 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/external_grey_box_models.py +++ b/pyomo/contrib/pynumero/interfaces/tests/external_grey_box_models.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.pynumero.dependencies import ( numpy as np, numpy_available, diff --git a/pyomo/contrib/pynumero/linalg/base.py b/pyomo/contrib/pynumero/linalg/base.py index 2b4eeaef451..7f3d1ffa115 100644 --- a/pyomo/contrib/pynumero/linalg/base.py +++ b/pyomo/contrib/pynumero/linalg/base.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from abc import ABCMeta, abstractmethod import enum from typing import Optional, Union, Tuple diff --git a/pyomo/contrib/pynumero/linalg/ma27_interface.py b/pyomo/contrib/pynumero/linalg/ma27_interface.py index 1ae02fe3290..d974cfc1263 100644 --- a/pyomo/contrib/pynumero/linalg/ma27_interface.py +++ b/pyomo/contrib/pynumero/linalg/ma27_interface.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .base import DirectLinearSolverInterface, LinearSolverStatus, LinearSolverResults from .ma27 import MA27Interface from scipy.sparse import isspmatrix_coo, tril, spmatrix diff --git a/pyomo/contrib/pynumero/linalg/ma57_interface.py b/pyomo/contrib/pynumero/linalg/ma57_interface.py index ef80ac653cf..dcd47795256 100644 --- a/pyomo/contrib/pynumero/linalg/ma57_interface.py +++ b/pyomo/contrib/pynumero/linalg/ma57_interface.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .base import DirectLinearSolverInterface, LinearSolverStatus, LinearSolverResults from .ma57 import MA57Interface from scipy.sparse import isspmatrix_coo, tril, spmatrix diff --git a/pyomo/contrib/pynumero/linalg/scipy_interface.py b/pyomo/contrib/pynumero/linalg/scipy_interface.py index 819e22ff1aa..a5a53690eb0 100644 --- a/pyomo/contrib/pynumero/linalg/scipy_interface.py +++ b/pyomo/contrib/pynumero/linalg/scipy_interface.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .base import ( DirectLinearSolverInterface, LinearSolverStatus, diff --git a/pyomo/contrib/pynumero/linalg/tests/__init__.py b/pyomo/contrib/pynumero/linalg/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/pynumero/linalg/tests/__init__.py +++ b/pyomo/contrib/pynumero/linalg/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pynumero/linalg/tests/test_linear_solvers.py b/pyomo/contrib/pynumero/linalg/tests/test_linear_solvers.py index 8d19127dde6..d5025042d95 100644 --- a/pyomo/contrib/pynumero/linalg/tests/test_linear_solvers.py +++ b/pyomo/contrib/pynumero/linalg/tests/test_linear_solvers.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common import unittest from pyomo.contrib.pynumero.dependencies import numpy_available, scipy_available diff --git a/pyomo/contrib/pynumero/src/ma27Interface.cpp b/pyomo/contrib/pynumero/src/ma27Interface.cpp index 624c7edd6f3..29ffef73938 100644 --- a/pyomo/contrib/pynumero/src/ma27Interface.cpp +++ b/pyomo/contrib/pynumero/src/ma27Interface.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include #include #include diff --git a/pyomo/contrib/pynumero/src/ma57Interface.cpp b/pyomo/contrib/pynumero/src/ma57Interface.cpp index 99b98ef6215..a0fb60edcc8 100644 --- a/pyomo/contrib/pynumero/src/ma57Interface.cpp +++ b/pyomo/contrib/pynumero/src/ma57Interface.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include #include #include diff --git a/pyomo/contrib/pynumero/src/tests/simple_test.cpp b/pyomo/contrib/pynumero/src/tests/simple_test.cpp index 4edbbb67a35..30255912c1f 100644 --- a/pyomo/contrib/pynumero/src/tests/simple_test.cpp +++ b/pyomo/contrib/pynumero/src/tests/simple_test.cpp @@ -1,3 +1,15 @@ +/**___________________________________________________________________________ + * + * Pyomo: Python Optimization Modeling Objects + * Copyright (c) 2008-2022 + * National Technology and Engineering Solutions of Sandia, LLC + * Under the terms of Contract DE-NA0003525 with National Technology and + * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain + * rights in this software. + * This software is distributed under the 3-clause BSD License. + * ___________________________________________________________________________ +**/ + #include #include "AmplInterface.hpp" diff --git a/pyomo/contrib/pynumero/tests/__init__.py b/pyomo/contrib/pynumero/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/pynumero/tests/__init__.py +++ b/pyomo/contrib/pynumero/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pyros/__init__.py b/pyomo/contrib/pyros/__init__.py index aeb92eb13fd..8ecd8ee7478 100644 --- a/pyomo/contrib/pyros/__init__.py +++ b/pyomo/contrib/pyros/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.pyros.pyros import PyROS from pyomo.contrib.pyros.pyros import ObjectiveType, pyrosTerminationCondition from pyomo.contrib.pyros.uncertainty_sets import ( diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index e2ce74a493e..a4b1785a987 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Functions for handling the construction and solving of the GRCS master problem via ROSolver """ diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 4ae033b9498..61615652a01 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + ''' Methods for the execution of the grcs algorithm ''' diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index b9659f044f4..e37d3325a57 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Functions for the construction and solving of the GRCS separation problem via ROsolver """ diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index 40a52757bae..3ee22af9749 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Objects to contain all model data and solve results for the ROSolver """ diff --git a/pyomo/contrib/pyros/tests/__init__.py b/pyomo/contrib/pyros/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/pyros/tests/__init__.py +++ b/pyomo/contrib/pyros/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 8de1c2666b9..c49c131fdf8 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + ''' Unit tests for the grcs API One class per function being tested, minimum one test per class diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 1b51e41fcaf..7e13026b1e9 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Abstract and pre-defined classes for representing uncertainty sets (or uncertain parameter spaces) of two-stage nonlinear robust optimization diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index e2986ae18c7..c5a80d27102 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + ''' Utility functions for the PyROS solver ''' diff --git a/pyomo/contrib/satsolver/__init__.py b/pyomo/contrib/satsolver/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/satsolver/__init__.py +++ b/pyomo/contrib/satsolver/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/satsolver/satsolver.py b/pyomo/contrib/satsolver/satsolver.py index 139b5218169..50e471253e2 100644 --- a/pyomo/contrib/satsolver/satsolver.py +++ b/pyomo/contrib/satsolver/satsolver.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import math from pyomo.common.dependencies import attempt_import diff --git a/pyomo/contrib/sensitivity_toolbox/__init__.py b/pyomo/contrib/sensitivity_toolbox/__init__.py index cac6562157e..feb094b6b76 100644 --- a/pyomo/contrib/sensitivity_toolbox/__init__.py +++ b/pyomo/contrib/sensitivity_toolbox/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects diff --git a/pyomo/contrib/sensitivity_toolbox/examples/__init__.py b/pyomo/contrib/sensitivity_toolbox/examples/__init__.py index 5223f39bbc1..d67dc03be6c 100644 --- a/pyomo/contrib/sensitivity_toolbox/examples/__init__.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects diff --git a/pyomo/contrib/sensitivity_toolbox/examples/rooney_biegler.py b/pyomo/contrib/sensitivity_toolbox/examples/rooney_biegler.py index f058e8189dc..350860a0b50 100644 --- a/pyomo/contrib/sensitivity_toolbox/examples/rooney_biegler.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/rooney_biegler.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + ############################################################################## # Institute for the Design of Advanced Energy Systems Process Systems # Engineering Framework (IDAES PSE Framework) Copyright (c) 2018-2019, by the diff --git a/pyomo/contrib/sensitivity_toolbox/k_aug.py b/pyomo/contrib/sensitivity_toolbox/k_aug.py index 8d739506492..e7ccb4960a5 100644 --- a/pyomo/contrib/sensitivity_toolbox/k_aug.py +++ b/pyomo/contrib/sensitivity_toolbox/k_aug.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ______________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects diff --git a/pyomo/contrib/sensitivity_toolbox/sens.py b/pyomo/contrib/sensitivity_toolbox/sens.py index e1c69d75974..43279c7bc5e 100644 --- a/pyomo/contrib/sensitivity_toolbox/sens.py +++ b/pyomo/contrib/sensitivity_toolbox/sens.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ______________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects diff --git a/pyomo/contrib/sensitivity_toolbox/tests/__init__.py b/pyomo/contrib/sensitivity_toolbox/tests/__init__.py index 557846ee521..5aecf9868db 100644 --- a/pyomo/contrib/sensitivity_toolbox/tests/__init__.py +++ b/pyomo/contrib/sensitivity_toolbox/tests/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects diff --git a/pyomo/contrib/sensitivity_toolbox/tests/test_k_aug_interface.py b/pyomo/contrib/sensitivity_toolbox/tests/test_k_aug_interface.py index 8c14cfc91d0..cb219f4f403 100644 --- a/pyomo/contrib/sensitivity_toolbox/tests/test_k_aug_interface.py +++ b/pyomo/contrib/sensitivity_toolbox/tests/test_k_aug_interface.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ____________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects diff --git a/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py b/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py index f4b3fb5548c..76d180ae422 100644 --- a/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py +++ b/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ____________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects diff --git a/pyomo/contrib/sensitivity_toolbox/tests/test_sens_unit.py b/pyomo/contrib/sensitivity_toolbox/tests/test_sens_unit.py index 05faada3007..d6da0d814e8 100644 --- a/pyomo/contrib/sensitivity_toolbox/tests/test_sens_unit.py +++ b/pyomo/contrib/sensitivity_toolbox/tests/test_sens_unit.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ____________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects diff --git a/pyomo/contrib/viewer/__init__.py b/pyomo/contrib/viewer/__init__.py index 8b137891791..d93cfd77b3c 100644 --- a/pyomo/contrib/viewer/__init__.py +++ b/pyomo/contrib/viewer/__init__.py @@ -1 +1,10 @@ - +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/viewer/tests/__init__.py b/pyomo/contrib/viewer/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/contrib/viewer/tests/__init__.py +++ b/pyomo/contrib/viewer/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/core/expr/calculus/__init__.py b/pyomo/core/expr/calculus/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/core/expr/calculus/__init__.py +++ b/pyomo/core/expr/calculus/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/core/expr/taylor_series.py b/pyomo/core/expr/taylor_series.py index 2c72f8bcfbc..467b1faa679 100644 --- a/pyomo/core/expr/taylor_series.py +++ b/pyomo/core/expr/taylor_series.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core.expr import identify_variables, value, differentiate import logging import math diff --git a/pyomo/core/plugins/transform/logical_to_linear.py b/pyomo/core/plugins/transform/logical_to_linear.py index f4107b8a32c..f2c609348e5 100644 --- a/pyomo/core/plugins/transform/logical_to_linear.py +++ b/pyomo/core/plugins/transform/logical_to_linear.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """Transformation from BooleanVar and LogicalConstraint to Binary and Constraints.""" diff --git a/pyomo/core/tests/unit/test_logical_constraint.py b/pyomo/core/tests/unit/test_logical_constraint.py index ed8120da935..e38a67a39d0 100644 --- a/pyomo/core/tests/unit/test_logical_constraint.py +++ b/pyomo/core/tests/unit/test_logical_constraint.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest from pyomo.core.expr.sympy_tools import sympy_available diff --git a/pyomo/core/tests/unit/test_sos_v2.py b/pyomo/core/tests/unit/test_sos_v2.py index 8b6fab549a2..4f4599056b5 100644 --- a/pyomo/core/tests/unit/test_sos_v2.py +++ b/pyomo/core/tests/unit/test_sos_v2.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # ***************************************************************************** # ***************************************************************************** diff --git a/pyomo/dae/simulator.py b/pyomo/dae/simulator.py index b869592553a..149c42ca6b4 100644 --- a/pyomo/dae/simulator.py +++ b/pyomo/dae/simulator.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # _________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects diff --git a/pyomo/gdp/tests/models.py b/pyomo/gdp/tests/models.py index a52f08b790e..f03a847162b 100644 --- a/pyomo/gdp/tests/models.py +++ b/pyomo/gdp/tests/models.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core import ( Block, ConcreteModel, diff --git a/pyomo/gdp/tests/test_reclassify.py b/pyomo/gdp/tests/test_reclassify.py index fd98f8f0954..dcf3470a211 100644 --- a/pyomo/gdp/tests/test_reclassify.py +++ b/pyomo/gdp/tests/test_reclassify.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: UTF-8 -*- """Tests disjunct reclassifier transformation.""" import pyomo.common.unittest as unittest diff --git a/pyomo/repn/tests/ampl/__init__.py b/pyomo/repn/tests/ampl/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/repn/tests/ampl/__init__.py +++ b/pyomo/repn/tests/ampl/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/solvers/plugins/solvers/XPRESS.py b/pyomo/solvers/plugins/solvers/XPRESS.py index 6ab51cfbbf3..7b85aea1266 100644 --- a/pyomo/solvers/plugins/solvers/XPRESS.py +++ b/pyomo/solvers/plugins/solvers/XPRESS.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.opt.base import OptSolver from pyomo.opt.base.solvers import SolverFactory import logging diff --git a/pyomo/solvers/tests/checks/test_MOSEKPersistent.py b/pyomo/solvers/tests/checks/test_MOSEKPersistent.py index 59ea930c4f0..6db99919177 100644 --- a/pyomo/solvers/tests/checks/test_MOSEKPersistent.py +++ b/pyomo/solvers/tests/checks/test_MOSEKPersistent.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest from pyomo.opt import ( diff --git a/pyomo/solvers/tests/checks/test_gurobi.py b/pyomo/solvers/tests/checks/test_gurobi.py index f33a00ce8a2..cfd0f077eab 100644 --- a/pyomo/solvers/tests/checks/test_gurobi.py +++ b/pyomo/solvers/tests/checks/test_gurobi.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest from unittest.mock import patch, MagicMock diff --git a/pyomo/solvers/tests/checks/test_gurobi_direct.py b/pyomo/solvers/tests/checks/test_gurobi_direct.py index 7c60b207a9f..d9802894c47 100644 --- a/pyomo/solvers/tests/checks/test_gurobi_direct.py +++ b/pyomo/solvers/tests/checks/test_gurobi_direct.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Tests for working with Gurobi environments. Some require a single-use license and are skipped if this isn't the case. diff --git a/pyomo/solvers/tests/checks/test_xpress_persistent.py b/pyomo/solvers/tests/checks/test_xpress_persistent.py index cd9c30fc73b..abfcf9c0afc 100644 --- a/pyomo/solvers/tests/checks/test_xpress_persistent.py +++ b/pyomo/solvers/tests/checks/test_xpress_persistent.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.common.unittest as unittest import pyomo.environ as pe from pyomo.core.expr.taylor_series import taylor_series_expansion diff --git a/pyomo/solvers/tests/mip/test_scip_log_data.py b/pyomo/solvers/tests/mip/test_scip_log_data.py index 8f756de220a..0dc0825afb3 100644 --- a/pyomo/solvers/tests/mip/test_scip_log_data.py +++ b/pyomo/solvers/tests/mip/test_scip_log_data.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ diff --git a/pyomo/util/__init__.py b/pyomo/util/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/util/__init__.py +++ b/pyomo/util/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/util/diagnostics.py b/pyomo/util/diagnostics.py index d4b7974b9da..8bad078ad64 100644 --- a/pyomo/util/diagnostics.py +++ b/pyomo/util/diagnostics.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # -*- coding: UTF-8 -*- """Module with miscellaneous diagnostic tools""" from pyomo.core.base.block import TraversalStrategy, Block diff --git a/pyomo/util/tests/__init__.py b/pyomo/util/tests/__init__.py index e69de29bb2d..d93cfd77b3c 100644 --- a/pyomo/util/tests/__init__.py +++ b/pyomo/util/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/scripts/performance/compare_components.py b/scripts/performance/compare_components.py index f390fad8454..1edaa73003b 100644 --- a/scripts/performance/compare_components.py +++ b/scripts/performance/compare_components.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # This script compares build time and memory usage for # various modeling objects. The output is organized into diff --git a/scripts/performance/expr_perf.py b/scripts/performance/expr_perf.py index 6566431b9f3..6f0d246e1f3 100644 --- a/scripts/performance/expr_perf.py +++ b/scripts/performance/expr_perf.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + # # This script runs performance tests on expressions # diff --git a/scripts/performance/simple.py b/scripts/performance/simple.py index 2990f13f413..bd5ffd99368 100644 --- a/scripts/performance/simple.py +++ b/scripts/performance/simple.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.environ import * import pyomo.core.expr.current as EXPR import timeit From 0b7857475de8741196191e34147760175649957e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 13:21:48 -0700 Subject: [PATCH 0560/3044] Apply black to doc changes --- pyomo/contrib/solver/base.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 98663d85501..1cd9db2baa9 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -46,11 +46,11 @@ class SolverBase(abc.ABC): - version: The version of the solver - is_persistent: Set to false for all non-persistent solvers. - Additionally, solvers should have a :attr:`config` attribute that - inherits from one of :class:`SolverConfig`, - :class:`BranchAndBoundConfig`, - :class:`PersistentSolverConfig`, or - :class:`PersistentBranchAndBoundConfig`. + Additionally, solvers should have a :attr:`config` attribute that + inherits from one of :class:`SolverConfig`, + :class:`BranchAndBoundConfig`, + :class:`PersistentSolverConfig`, or + :class:`PersistentBranchAndBoundConfig`. """ CONFIG = SolverConfig() @@ -105,9 +105,7 @@ def __str__(self): return self.name @abc.abstractmethod - def solve( - self, model: _BlockData, **kwargs - ) -> Results: + def solve(self, model: _BlockData, **kwargs) -> Results: """ Solve a Pyomo model. From 928018003a010a31a36afe62c822361d27569d32 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 13:44:09 -0700 Subject: [PATCH 0561/3044] Switch APPSI/contrib.solver registrations to use "local" (unqualified) names for solvers --- pyomo/contrib/appsi/base.py | 2 +- pyomo/contrib/appsi/plugins.py | 10 +++++----- pyomo/contrib/solver/factory.py | 7 +++++-- pyomo/contrib/solver/plugins.py | 10 ++++++---- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index e6186eeedd2..941883ab997 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -1685,7 +1685,7 @@ def decorator(cls): class LegacySolver(LegacySolverInterface, cls): pass - LegacySolverFactory.register(name, doc)(LegacySolver) + LegacySolverFactory.register('appsi_' + name, doc)(LegacySolver) return cls diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index 5333158239e..cec95337a9b 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -7,17 +7,17 @@ def load(): ExtensionBuilderFactory.register('appsi')(AppsiBuilder) SolverFactory.register( - name='appsi_gurobi', doc='Automated persistent interface to Gurobi' + name='gurobi', doc='Automated persistent interface to Gurobi' )(Gurobi) SolverFactory.register( - name='appsi_cplex', doc='Automated persistent interface to Cplex' + name='cplex', doc='Automated persistent interface to Cplex' )(Cplex) SolverFactory.register( - name='appsi_ipopt', doc='Automated persistent interface to Ipopt' + name='ipopt', doc='Automated persistent interface to Ipopt' )(Ipopt) SolverFactory.register( - name='appsi_cbc', doc='Automated persistent interface to Cbc' + name='cbc', doc='Automated persistent interface to Cbc' )(Cbc) SolverFactory.register( - name='appsi_highs', doc='Automated persistent interface to Highs' + name='highs', doc='Automated persistent interface to Highs' )(Highs) diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index e499605afd4..cdd042f9e78 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -16,7 +16,10 @@ class SolverFactoryClass(Factory): - def register(self, name, doc=None): + def register(self, name, legacy_name=None, doc=None): + if legacy_name is None: + legacy_name = name + def decorator(cls): self._cls[name] = cls self._doc[name] = doc @@ -24,7 +27,7 @@ def decorator(cls): class LegacySolver(LegacySolverWrapper, cls): pass - LegacySolverFactory.register(name, doc)(LegacySolver) + LegacySolverFactory.register(legacy_name, doc)(LegacySolver) return cls diff --git a/pyomo/contrib/solver/plugins.py b/pyomo/contrib/solver/plugins.py index e66818482b4..7d984d10eaa 100644 --- a/pyomo/contrib/solver/plugins.py +++ b/pyomo/contrib/solver/plugins.py @@ -16,7 +16,9 @@ def load(): - SolverFactory.register(name='ipopt_v2', doc='The IPOPT NLP solver (new interface)')( - ipopt - ) - SolverFactory.register(name='gurobi_v2', doc='New interface to Gurobi')(Gurobi) + SolverFactory.register( + name='ipopt', legacy_name='ipopt_v2', doc='The IPOPT NLP solver (new interface)' + )(ipopt) + SolverFactory.register( + name='gurobi', legacy_name='gurobi_v2', doc='New interface to Gurobi' + )(Gurobi) From 316fb3faa4c0faa37a095eceddd60b36957e9ee4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 13:45:37 -0700 Subject: [PATCH 0562/3044] Add __future__ mechanism for switching solver factories --- pyomo/__future__.py | 69 +++++++++++++++++++++++++++++++++ pyomo/contrib/solver/factory.py | 2 +- pyomo/opt/base/solvers.py | 4 ++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 pyomo/__future__.py diff --git a/pyomo/__future__.py b/pyomo/__future__.py new file mode 100644 index 00000000000..7028265b2ad --- /dev/null +++ b/pyomo/__future__.py @@ -0,0 +1,69 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.environ as _environ + + +def __getattr__(name): + if name in ('solver_factory_v1', 'solver_factory_v2', 'solver_factory_v3'): + return solver_factory(int(name[-1])) + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + + +def solver_factory(version=None): + """Get (or set) the active implementation of the SolverFactory + + This allows users to query / set the current implementation of the + SolverFactory that should be used throughout Pyomo. Valid options are: + + 1: the original Pyomo SolverFactor + 2: the SolverFactory from APPSI + 3: the SolverFactory from pyomo.contrib.solver + + """ + import pyomo.opt.base.solvers as _solvers + import pyomo.contrib.solver.factory as _contrib + import pyomo.contrib.appsi.base as _appsi + versions = { + 1: _solvers.LegacySolverFactory, + 2: _appsi.SolverFactory, + 3: _contrib.SolverFactory, + } + + current = getattr(solver_factory, '_active_version', None) + # First time through, _active_version is not defined. Go look and + # see what it was initialized to in pyomo.environ + if current is None: + for ver, cls in versions.items(): + if cls._cls is _environ.SolverFactory._cls: + solver_factory._active_version = ver + break + return solver_factory._active_version + # + # The user is just asking what the current SolverFactory is; tell them. + if version is None: + return solver_factory._active_version + # + # Update the current SolverFactory to be a shim around (shallow copy + # of) the new active factory + src = versions.get(version, None) + if version is not None: + solver_factory._active_version = version + for attr in ('_description', '_cls', '_doc'): + setattr(_environ.SolverFactory, attr, getattr(src, attr)) + else: + raise ValueError( + "Invalid value for target solver factory version; expected {1, 2, 3}, " + f"received {version}" + ) + return src + +solver_factory._active_version = solver_factory() diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index cdd042f9e78..73666ff57e4 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ -from pyomo.opt.base import SolverFactory as LegacySolverFactory +from pyomo.opt.base import LegacySolverFactory from pyomo.common.factory import Factory from pyomo.contrib.solver.base import LegacySolverWrapper diff --git a/pyomo/opt/base/solvers.py b/pyomo/opt/base/solvers.py index b11e6393b02..439dda55b57 100644 --- a/pyomo/opt/base/solvers.py +++ b/pyomo/opt/base/solvers.py @@ -181,7 +181,11 @@ def __call__(self, _name=None, **kwds): return opt +LegacySolverFactory = SolverFactoryClass('solver type') + SolverFactory = SolverFactoryClass('solver type') +SolverFactory._cls = LegacySolverFactory._cls +SolverFactory._doc = LegacySolverFactory._doc # From 0e3e7df49080bc115f34cc6c7682f82b86def763 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 13:55:53 -0700 Subject: [PATCH 0563/3044] Update years on allyear on all copyright statements --- LICENSE.md | 2 +- conftest.py | 2 +- doc/OnlineDocs/conf.py | 2 +- .../library_reference/kernel/examples/aml_example.py | 2 +- doc/OnlineDocs/library_reference/kernel/examples/conic.py | 2 +- .../library_reference/kernel/examples/kernel_containers.py | 2 +- .../library_reference/kernel/examples/kernel_example.py | 2 +- .../library_reference/kernel/examples/kernel_solving.py | 2 +- .../library_reference/kernel/examples/kernel_subclassing.py | 2 +- .../library_reference/kernel/examples/transformer.py | 2 +- doc/OnlineDocs/modeling_extensions/__init__.py | 2 +- doc/OnlineDocs/src/data/ABCD1.py | 2 +- doc/OnlineDocs/src/data/ABCD2.py | 2 +- doc/OnlineDocs/src/data/ABCD3.py | 2 +- doc/OnlineDocs/src/data/ABCD4.py | 2 +- doc/OnlineDocs/src/data/ABCD5.py | 2 +- doc/OnlineDocs/src/data/ABCD6.py | 2 +- doc/OnlineDocs/src/data/ABCD7.py | 2 +- doc/OnlineDocs/src/data/ABCD8.py | 2 +- doc/OnlineDocs/src/data/ABCD9.py | 2 +- doc/OnlineDocs/src/data/diet1.py | 2 +- doc/OnlineDocs/src/data/ex.py | 2 +- doc/OnlineDocs/src/data/import1.tab.py | 2 +- doc/OnlineDocs/src/data/import2.tab.py | 2 +- doc/OnlineDocs/src/data/import3.tab.py | 2 +- doc/OnlineDocs/src/data/import4.tab.py | 2 +- doc/OnlineDocs/src/data/import5.tab.py | 2 +- doc/OnlineDocs/src/data/import6.tab.py | 2 +- doc/OnlineDocs/src/data/import7.tab.py | 2 +- doc/OnlineDocs/src/data/import8.tab.py | 2 +- doc/OnlineDocs/src/data/param1.py | 2 +- doc/OnlineDocs/src/data/param2.py | 2 +- doc/OnlineDocs/src/data/param2a.py | 2 +- doc/OnlineDocs/src/data/param3.py | 2 +- doc/OnlineDocs/src/data/param3a.py | 2 +- doc/OnlineDocs/src/data/param3b.py | 2 +- doc/OnlineDocs/src/data/param3c.py | 2 +- doc/OnlineDocs/src/data/param4.py | 2 +- doc/OnlineDocs/src/data/param5.py | 2 +- doc/OnlineDocs/src/data/param5a.py | 2 +- doc/OnlineDocs/src/data/param6.py | 2 +- doc/OnlineDocs/src/data/param6a.py | 2 +- doc/OnlineDocs/src/data/param7a.py | 2 +- doc/OnlineDocs/src/data/param7b.py | 2 +- doc/OnlineDocs/src/data/param8a.py | 2 +- doc/OnlineDocs/src/data/set1.py | 2 +- doc/OnlineDocs/src/data/set2.py | 2 +- doc/OnlineDocs/src/data/set2a.py | 2 +- doc/OnlineDocs/src/data/set3.py | 2 +- doc/OnlineDocs/src/data/set4.py | 2 +- doc/OnlineDocs/src/data/set5.py | 2 +- doc/OnlineDocs/src/data/table0.py | 2 +- doc/OnlineDocs/src/data/table0.ul.py | 2 +- doc/OnlineDocs/src/data/table1.py | 2 +- doc/OnlineDocs/src/data/table2.py | 2 +- doc/OnlineDocs/src/data/table3.py | 2 +- doc/OnlineDocs/src/data/table3.ul.py | 2 +- doc/OnlineDocs/src/data/table4.py | 2 +- doc/OnlineDocs/src/data/table4.ul.py | 2 +- doc/OnlineDocs/src/data/table5.py | 2 +- doc/OnlineDocs/src/data/table6.py | 2 +- doc/OnlineDocs/src/data/table7.py | 2 +- doc/OnlineDocs/src/dataportal/PP_sqlite.py | 2 +- doc/OnlineDocs/src/dataportal/dataportal_tab.py | 2 +- doc/OnlineDocs/src/dataportal/param_initialization.py | 2 +- doc/OnlineDocs/src/dataportal/set_initialization.py | 2 +- doc/OnlineDocs/src/expr/design.py | 2 +- doc/OnlineDocs/src/expr/index.py | 2 +- doc/OnlineDocs/src/expr/managing.py | 2 +- doc/OnlineDocs/src/expr/overview.py | 2 +- doc/OnlineDocs/src/expr/performance.py | 2 +- doc/OnlineDocs/src/expr/quicksum.py | 2 +- doc/OnlineDocs/src/scripting/AbstractSuffixes.py | 2 +- doc/OnlineDocs/src/scripting/Isinglebuild.py | 2 +- doc/OnlineDocs/src/scripting/NodesIn_init.py | 2 +- doc/OnlineDocs/src/scripting/Z_init.py | 2 +- doc/OnlineDocs/src/scripting/abstract2.py | 2 +- doc/OnlineDocs/src/scripting/abstract2piece.py | 2 +- doc/OnlineDocs/src/scripting/abstract2piecebuild.py | 2 +- doc/OnlineDocs/src/scripting/block_iter_example.py | 2 +- doc/OnlineDocs/src/scripting/concrete1.py | 2 +- doc/OnlineDocs/src/scripting/doubleA.py | 2 +- doc/OnlineDocs/src/scripting/driveabs2.py | 2 +- doc/OnlineDocs/src/scripting/driveconc1.py | 2 +- doc/OnlineDocs/src/scripting/iterative1.py | 2 +- doc/OnlineDocs/src/scripting/iterative2.py | 2 +- doc/OnlineDocs/src/scripting/noiteration1.py | 2 +- doc/OnlineDocs/src/scripting/parallel.py | 2 +- doc/OnlineDocs/src/scripting/spy4Constraints.py | 2 +- doc/OnlineDocs/src/scripting/spy4Expressions.py | 2 +- doc/OnlineDocs/src/scripting/spy4PyomoCommand.py | 2 +- doc/OnlineDocs/src/scripting/spy4Variables.py | 2 +- doc/OnlineDocs/src/scripting/spy4scripts.py | 2 +- doc/OnlineDocs/src/strip_examples.py | 2 +- doc/OnlineDocs/src/test_examples.py | 2 +- examples/dae/Heat_Conduction.py | 2 +- examples/dae/Optimal_Control.py | 2 +- examples/dae/PDE_example.py | 2 +- examples/dae/Parameter_Estimation.py | 2 +- examples/dae/Path_Constraint.py | 2 +- examples/dae/ReactionKinetics.py | 2 +- examples/dae/car_example.py | 2 +- examples/dae/disease_DAE.py | 2 +- examples/dae/distill_DAE.py | 2 +- examples/dae/dynamic_scheduling.py | 2 +- examples/dae/laplace_BVP.py | 2 +- examples/dae/run_Optimal_Control.py | 2 +- examples/dae/run_Parameter_Estimation.py | 2 +- examples/dae/run_Path_Constraint.py | 2 +- examples/dae/run_disease.py | 2 +- examples/dae/run_distill.py | 2 +- examples/dae/run_stochpdegas_automatic.py | 2 +- examples/dae/simulator_dae_example.py | 2 +- examples/dae/simulator_dae_multindex_example.py | 2 +- examples/dae/simulator_ode_example.py | 2 +- examples/dae/simulator_ode_multindex_example.py | 2 +- examples/dae/stochpdegas_automatic.py | 2 +- examples/doc/samples/__init__.py | 2 +- examples/doc/samples/case_studies/deer/DeerProblem.py | 2 +- examples/doc/samples/case_studies/diet/DietProblem.py | 2 +- .../doc/samples/case_studies/disease_est/DiseaseEstimation.py | 2 +- examples/doc/samples/case_studies/max_flow/MaxFlow.py | 2 +- .../doc/samples/case_studies/network_flow/networkFlow1.py | 2 +- examples/doc/samples/case_studies/rosen/Rosenbrock.py | 2 +- .../doc/samples/case_studies/transportation/transportation.py | 2 +- examples/doc/samples/comparisons/cutstock/cutstock_cplex.py | 2 +- examples/doc/samples/comparisons/cutstock/cutstock_grb.py | 2 +- examples/doc/samples/comparisons/cutstock/cutstock_lpsolve.py | 2 +- examples/doc/samples/comparisons/cutstock/cutstock_pulpor.py | 2 +- examples/doc/samples/comparisons/cutstock/cutstock_pyomo.py | 2 +- examples/doc/samples/comparisons/cutstock/cutstock_util.py | 2 +- examples/doc/samples/comparisons/sched/pyomo/sched.py | 2 +- examples/doc/samples/scripts/__init__.py | 2 +- examples/doc/samples/scripts/s1/knapsack.py | 2 +- examples/doc/samples/scripts/s1/script.py | 2 +- examples/doc/samples/scripts/s2/knapsack.py | 2 +- examples/doc/samples/scripts/s2/script.py | 2 +- examples/doc/samples/scripts/test_scripts.py | 2 +- examples/doc/samples/update.py | 2 +- examples/gdp/batchProcessing.py | 2 +- examples/gdp/circles/circles.py | 2 +- examples/gdp/constrained_layout/cons_layout_model.py | 2 +- examples/gdp/disease_model.py | 2 +- examples/gdp/eight_process/eight_proc_logical.py | 2 +- examples/gdp/eight_process/eight_proc_model.py | 2 +- examples/gdp/eight_process/eight_proc_verbose_model.py | 2 +- examples/gdp/farm_layout/farm_layout.py | 2 +- examples/gdp/jobshop-nodisjuncts.py | 2 +- examples/gdp/jobshop.py | 2 +- examples/gdp/medTermPurchasing_Literal.py | 2 +- examples/gdp/nine_process/small_process.py | 2 +- examples/gdp/simple1.py | 2 +- examples/gdp/simple2.py | 2 +- examples/gdp/simple3.py | 2 +- examples/gdp/small_lit/basic_step.py | 2 +- examples/gdp/small_lit/contracts_problem.py | 2 +- examples/gdp/small_lit/ex1_Lee.py | 2 +- examples/gdp/small_lit/ex_633_trespalacios.py | 2 +- examples/gdp/small_lit/nonconvex_HEN.py | 2 +- examples/gdp/stickies.py | 2 +- examples/gdp/strip_packing/stripPacking.py | 2 +- examples/gdp/strip_packing/strip_packing_8rect.py | 2 +- examples/gdp/strip_packing/strip_packing_concrete.py | 2 +- examples/gdp/two_rxn_lee/two_rxn_model.py | 2 +- examples/kernel/blocks.py | 2 +- examples/kernel/conic.py | 2 +- examples/kernel/constraints.py | 2 +- examples/kernel/containers.py | 2 +- examples/kernel/expressions.py | 2 +- examples/kernel/mosek/geometric1.py | 2 +- examples/kernel/mosek/geometric2.py | 2 +- examples/kernel/mosek/maximum_volume_cuboid.py | 2 +- examples/kernel/mosek/power1.py | 2 +- examples/kernel/mosek/semidefinite.py | 2 +- examples/kernel/objectives.py | 2 +- examples/kernel/parameters.py | 2 +- examples/kernel/piecewise_functions.py | 2 +- examples/kernel/piecewise_nd_functions.py | 2 +- examples/kernel/special_ordered_sets.py | 2 +- examples/kernel/suffixes.py | 2 +- examples/kernel/variables.py | 2 +- examples/mpec/bard1.py | 2 +- examples/mpec/df.py | 2 +- examples/mpec/indexed.py | 2 +- examples/mpec/linear1.py | 2 +- examples/mpec/munson1.py | 2 +- examples/mpec/munson1a.py | 2 +- examples/mpec/munson1b.py | 2 +- examples/mpec/munson1c.py | 2 +- examples/mpec/munson1d.py | 2 +- examples/mpec/scholtes4.py | 2 +- examples/performance/dae/run_stochpdegas1_automatic.py | 2 +- examples/performance/dae/stochpdegas1_automatic.py | 2 +- examples/performance/jump/clnlbeam.py | 2 +- examples/performance/jump/facility.py | 2 +- examples/performance/jump/lqcp.py | 2 +- examples/performance/jump/opf_66200bus.py | 2 +- examples/performance/jump/opf_6620bus.py | 2 +- examples/performance/jump/opf_662bus.py | 2 +- examples/performance/misc/bilinear1_100.py | 2 +- examples/performance/misc/bilinear1_100000.py | 2 +- examples/performance/misc/bilinear2_100.py | 2 +- examples/performance/misc/bilinear2_100000.py | 2 +- examples/performance/misc/diag1_100.py | 2 +- examples/performance/misc/diag1_100000.py | 2 +- examples/performance/misc/diag2_100.py | 2 +- examples/performance/misc/diag2_100000.py | 2 +- examples/performance/misc/set1.py | 2 +- examples/performance/misc/sparse1.py | 2 +- examples/performance/pmedian/pmedian1.py | 2 +- examples/performance/pmedian/pmedian2.py | 2 +- examples/pyomo/amplbook2/diet.py | 2 +- examples/pyomo/amplbook2/dieti.py | 2 +- examples/pyomo/amplbook2/econ2min.py | 2 +- examples/pyomo/amplbook2/econmin.py | 2 +- examples/pyomo/amplbook2/prod.py | 2 +- examples/pyomo/amplbook2/steel.py | 2 +- examples/pyomo/amplbook2/steel3.py | 2 +- examples/pyomo/amplbook2/steel4.py | 2 +- examples/pyomo/benders/master.py | 2 +- examples/pyomo/benders/subproblem.py | 2 +- examples/pyomo/callbacks/sc.py | 2 +- examples/pyomo/callbacks/sc_callback.py | 2 +- examples/pyomo/callbacks/sc_script.py | 2 +- examples/pyomo/callbacks/scalability/run.py | 2 +- examples/pyomo/callbacks/tsp.py | 2 +- examples/pyomo/columngeneration/cutting_stock.py | 2 +- examples/pyomo/concrete/Whiskas.py | 2 +- examples/pyomo/concrete/knapsack-abstract.py | 2 +- examples/pyomo/concrete/knapsack-concrete.py | 2 +- examples/pyomo/concrete/rosen.py | 2 +- examples/pyomo/concrete/sodacan.py | 2 +- examples/pyomo/concrete/sodacan_fig.py | 2 +- examples/pyomo/concrete/sp.py | 2 +- examples/pyomo/concrete/sp_data.py | 2 +- examples/pyomo/connectors/network_flow.py | 2 +- examples/pyomo/connectors/network_flow_proposed.py | 2 +- examples/pyomo/core/block1.py | 2 +- examples/pyomo/core/integrality1.py | 2 +- examples/pyomo/core/integrality2.py | 2 +- examples/pyomo/core/simple.py | 2 +- examples/pyomo/core/t1.py | 2 +- examples/pyomo/core/t2.py | 2 +- examples/pyomo/core/t5.py | 2 +- examples/pyomo/diet/diet-sqlite.py | 2 +- examples/pyomo/diet/diet1.py | 2 +- examples/pyomo/diet/diet2.py | 2 +- examples/pyomo/draft/api.py | 2 +- examples/pyomo/draft/bpack.py | 2 +- examples/pyomo/draft/diet2.py | 2 +- examples/pyomo/p-median/decorated_pmedian.py | 2 +- examples/pyomo/p-median/pmedian.py | 2 +- examples/pyomo/p-median/solver1.py | 2 +- examples/pyomo/p-median/solver2.py | 2 +- examples/pyomo/piecewise/convex.py | 2 +- examples/pyomo/piecewise/indexed.py | 2 +- examples/pyomo/piecewise/indexed_nonlinear.py | 2 +- examples/pyomo/piecewise/indexed_points.py | 2 +- examples/pyomo/piecewise/nonconvex.py | 2 +- examples/pyomo/piecewise/points.py | 2 +- examples/pyomo/piecewise/step.py | 2 +- examples/pyomo/quadratic/example1.py | 2 +- examples/pyomo/quadratic/example2.py | 2 +- examples/pyomo/quadratic/example3.py | 2 +- examples/pyomo/quadratic/example4.py | 2 +- examples/pyomo/radertext/Ex2_1.py | 2 +- examples/pyomo/radertext/Ex2_2.py | 2 +- examples/pyomo/radertext/Ex2_3.py | 2 +- examples/pyomo/radertext/Ex2_5.py | 2 +- examples/pyomo/radertext/Ex2_6a.py | 2 +- examples/pyomo/radertext/Ex2_6b.py | 2 +- examples/pyomo/sos/DepotSiting.py | 2 +- examples/pyomo/sos/basic_sos2_example.py | 2 +- examples/pyomo/sos/sos2_piecewise.py | 2 +- examples/pyomo/suffixes/duals_pyomo.py | 2 +- examples/pyomo/suffixes/duals_script.py | 2 +- examples/pyomo/suffixes/gurobi_ampl_basis.py | 2 +- examples/pyomo/suffixes/gurobi_ampl_example.py | 2 +- examples/pyomo/suffixes/gurobi_ampl_iis.py | 2 +- examples/pyomo/suffixes/ipopt_scaling.py | 2 +- examples/pyomo/suffixes/ipopt_warmstart.py | 2 +- examples/pyomo/suffixes/sipopt_hicks.py | 2 +- examples/pyomo/suffixes/sipopt_parametric.py | 2 +- examples/pyomo/transform/scaling_ex.py | 2 +- examples/pyomo/tutorials/data.py | 2 +- examples/pyomo/tutorials/excel.py | 2 +- examples/pyomo/tutorials/param.py | 2 +- examples/pyomo/tutorials/set.py | 2 +- examples/pyomo/tutorials/table.py | 2 +- examples/pyomobook/__init__.py | 2 +- examples/pyomobook/abstract-ch/AbstHLinScript.py | 2 +- examples/pyomobook/abstract-ch/AbstractH.py | 2 +- examples/pyomobook/abstract-ch/AbstractHLinear.py | 2 +- examples/pyomobook/abstract-ch/abstract5.py | 2 +- examples/pyomobook/abstract-ch/abstract6.py | 2 +- examples/pyomobook/abstract-ch/abstract7.py | 2 +- examples/pyomobook/abstract-ch/buildactions.py | 2 +- examples/pyomobook/abstract-ch/concrete1.py | 2 +- examples/pyomobook/abstract-ch/concrete2.py | 2 +- examples/pyomobook/abstract-ch/diet1.py | 2 +- examples/pyomobook/abstract-ch/ex.py | 2 +- examples/pyomobook/abstract-ch/param1.py | 2 +- examples/pyomobook/abstract-ch/param2.py | 2 +- examples/pyomobook/abstract-ch/param2a.py | 2 +- examples/pyomobook/abstract-ch/param3.py | 2 +- examples/pyomobook/abstract-ch/param3a.py | 2 +- examples/pyomobook/abstract-ch/param3b.py | 2 +- examples/pyomobook/abstract-ch/param3c.py | 2 +- examples/pyomobook/abstract-ch/param4.py | 2 +- examples/pyomobook/abstract-ch/param5.py | 2 +- examples/pyomobook/abstract-ch/param5a.py | 2 +- examples/pyomobook/abstract-ch/param6.py | 2 +- examples/pyomobook/abstract-ch/param6a.py | 2 +- examples/pyomobook/abstract-ch/param7a.py | 2 +- examples/pyomobook/abstract-ch/param7b.py | 2 +- examples/pyomobook/abstract-ch/param8a.py | 2 +- examples/pyomobook/abstract-ch/postprocess_fn.py | 2 +- examples/pyomobook/abstract-ch/set1.py | 2 +- examples/pyomobook/abstract-ch/set2.py | 2 +- examples/pyomobook/abstract-ch/set2a.py | 2 +- examples/pyomobook/abstract-ch/set3.py | 2 +- examples/pyomobook/abstract-ch/set4.py | 2 +- examples/pyomobook/abstract-ch/set5.py | 2 +- examples/pyomobook/abstract-ch/wl_abstract.py | 2 +- examples/pyomobook/abstract-ch/wl_abstract_script.py | 2 +- examples/pyomobook/blocks-ch/blocks_gen.py | 2 +- examples/pyomobook/blocks-ch/blocks_intro.py | 2 +- examples/pyomobook/blocks-ch/blocks_lotsizing.py | 2 +- examples/pyomobook/blocks-ch/lotsizing.py | 2 +- examples/pyomobook/blocks-ch/lotsizing_no_time.py | 2 +- examples/pyomobook/blocks-ch/lotsizing_uncertain.py | 2 +- examples/pyomobook/dae-ch/dae_tester_model.py | 2 +- examples/pyomobook/dae-ch/path_constraint.py | 2 +- examples/pyomobook/dae-ch/plot_path_constraint.py | 2 +- examples/pyomobook/dae-ch/run_path_constraint.py | 2 +- examples/pyomobook/dae-ch/run_path_constraint_tester.py | 2 +- examples/pyomobook/gdp-ch/gdp_uc.py | 2 +- examples/pyomobook/gdp-ch/scont.py | 2 +- examples/pyomobook/gdp-ch/scont2.py | 2 +- examples/pyomobook/gdp-ch/scont_script.py | 2 +- examples/pyomobook/gdp-ch/verify_scont.py | 2 +- examples/pyomobook/intro-ch/abstract5.py | 2 +- examples/pyomobook/intro-ch/coloring_concrete.py | 2 +- examples/pyomobook/intro-ch/concrete1.py | 2 +- examples/pyomobook/intro-ch/concrete1_generic.py | 2 +- examples/pyomobook/intro-ch/mydata.py | 2 +- examples/pyomobook/mpec-ch/ex1a.py | 2 +- examples/pyomobook/mpec-ch/ex1b.py | 2 +- examples/pyomobook/mpec-ch/ex1c.py | 2 +- examples/pyomobook/mpec-ch/ex1d.py | 2 +- examples/pyomobook/mpec-ch/ex1e.py | 2 +- examples/pyomobook/mpec-ch/ex2.py | 2 +- examples/pyomobook/mpec-ch/munson1.py | 2 +- examples/pyomobook/mpec-ch/ralph1.py | 2 +- examples/pyomobook/nonlinear-ch/deer/DeerProblem.py | 2 +- .../pyomobook/nonlinear-ch/disease_est/disease_estimation.py | 2 +- .../pyomobook/nonlinear-ch/multimodal/multimodal_init1.py | 2 +- .../pyomobook/nonlinear-ch/multimodal/multimodal_init2.py | 2 +- examples/pyomobook/nonlinear-ch/react_design/ReactorDesign.py | 2 +- .../pyomobook/nonlinear-ch/react_design/ReactorDesignTable.py | 2 +- examples/pyomobook/nonlinear-ch/rosen/rosenbrock.py | 2 +- examples/pyomobook/optimization-ch/ConcHLinScript.py | 2 +- examples/pyomobook/optimization-ch/ConcreteH.py | 2 +- examples/pyomobook/optimization-ch/ConcreteHLinear.py | 2 +- examples/pyomobook/optimization-ch/IC_model_dict.py | 2 +- examples/pyomobook/overview-ch/var_obj_con_snippet.py | 2 +- examples/pyomobook/overview-ch/wl_abstract.py | 2 +- examples/pyomobook/overview-ch/wl_abstract_script.py | 2 +- examples/pyomobook/overview-ch/wl_concrete.py | 2 +- examples/pyomobook/overview-ch/wl_concrete_script.py | 2 +- examples/pyomobook/overview-ch/wl_excel.py | 2 +- examples/pyomobook/overview-ch/wl_list.py | 2 +- examples/pyomobook/overview-ch/wl_mutable.py | 2 +- examples/pyomobook/overview-ch/wl_mutable_excel.py | 2 +- examples/pyomobook/overview-ch/wl_scalar.py | 2 +- examples/pyomobook/performance-ch/SparseSets.py | 2 +- examples/pyomobook/performance-ch/lin_expr.py | 2 +- examples/pyomobook/performance-ch/persistent.py | 2 +- examples/pyomobook/performance-ch/wl.py | 2 +- examples/pyomobook/pyomo-components-ch/con_declaration.py | 2 +- examples/pyomobook/pyomo-components-ch/examples.py | 2 +- examples/pyomobook/pyomo-components-ch/expr_declaration.py | 2 +- examples/pyomobook/pyomo-components-ch/obj_declaration.py | 2 +- examples/pyomobook/pyomo-components-ch/param_declaration.py | 2 +- .../pyomobook/pyomo-components-ch/param_initialization.py | 2 +- examples/pyomobook/pyomo-components-ch/param_misc.py | 2 +- examples/pyomobook/pyomo-components-ch/param_validation.py | 2 +- examples/pyomobook/pyomo-components-ch/rangeset.py | 2 +- examples/pyomobook/pyomo-components-ch/set_declaration.py | 2 +- examples/pyomobook/pyomo-components-ch/set_initialization.py | 2 +- examples/pyomobook/pyomo-components-ch/set_misc.py | 2 +- examples/pyomobook/pyomo-components-ch/set_options.py | 2 +- examples/pyomobook/pyomo-components-ch/set_validation.py | 2 +- examples/pyomobook/pyomo-components-ch/suffix_declaration.py | 2 +- examples/pyomobook/pyomo-components-ch/var_declaration.py | 2 +- examples/pyomobook/python-ch/BadIndent.py | 2 +- examples/pyomobook/python-ch/LineExample.py | 2 +- examples/pyomobook/python-ch/class.py | 2 +- examples/pyomobook/python-ch/ctob.py | 2 +- examples/pyomobook/python-ch/example.py | 2 +- examples/pyomobook/python-ch/example2.py | 2 +- examples/pyomobook/python-ch/functions.py | 2 +- examples/pyomobook/python-ch/iterate.py | 2 +- examples/pyomobook/python-ch/pythonconditional.py | 2 +- examples/pyomobook/scripts-ch/attributes.py | 2 +- examples/pyomobook/scripts-ch/prob_mod_ex.py | 2 +- examples/pyomobook/scripts-ch/sudoku/sudoku.py | 2 +- examples/pyomobook/scripts-ch/sudoku/sudoku_run.py | 2 +- examples/pyomobook/scripts-ch/value_expression.py | 2 +- examples/pyomobook/scripts-ch/warehouse_cuts.py | 2 +- examples/pyomobook/scripts-ch/warehouse_load_solutions.py | 2 +- examples/pyomobook/scripts-ch/warehouse_model.py | 2 +- examples/pyomobook/scripts-ch/warehouse_print.py | 2 +- examples/pyomobook/scripts-ch/warehouse_script.py | 2 +- examples/pyomobook/scripts-ch/warehouse_solver_options.py | 2 +- examples/pyomobook/strip_examples.py | 2 +- examples/pyomobook/test_book_examples.py | 2 +- pyomo/__init__.py | 2 +- pyomo/common/__init__.py | 2 +- pyomo/common/_command.py | 2 +- pyomo/common/_common.py | 2 +- pyomo/common/autoslots.py | 2 +- pyomo/common/backports.py | 2 +- pyomo/common/cmake_builder.py | 2 +- pyomo/common/collections/__init__.py | 2 +- pyomo/common/collections/bunch.py | 2 +- pyomo/common/collections/component_map.py | 2 +- pyomo/common/collections/component_set.py | 2 +- pyomo/common/collections/orderedset.py | 2 +- pyomo/common/config.py | 2 +- pyomo/common/dependencies.py | 2 +- pyomo/common/deprecation.py | 2 +- pyomo/common/download.py | 2 +- pyomo/common/env.py | 2 +- pyomo/common/envvar.py | 2 +- pyomo/common/errors.py | 2 +- pyomo/common/extensions.py | 2 +- pyomo/common/factory.py | 2 +- pyomo/common/fileutils.py | 2 +- pyomo/common/formatting.py | 2 +- pyomo/common/gc_manager.py | 2 +- pyomo/common/getGSL.py | 2 +- pyomo/common/gsl.py | 2 +- pyomo/common/log.py | 2 +- pyomo/common/modeling.py | 2 +- pyomo/common/multithread.py | 2 +- pyomo/common/numeric_types.py | 2 +- pyomo/common/plugin.py | 2 +- pyomo/common/plugin_base.py | 2 +- pyomo/common/plugins.py | 2 +- pyomo/common/pyomo_typing.py | 2 +- pyomo/common/shutdown.py | 2 +- pyomo/common/sorting.py | 2 +- pyomo/common/tee.py | 2 +- pyomo/common/tempfiles.py | 2 +- pyomo/common/tests/__init__.py | 2 +- pyomo/common/tests/config_plugin.py | 2 +- pyomo/common/tests/dep_mod.py | 2 +- pyomo/common/tests/dep_mod_except.py | 2 +- pyomo/common/tests/deps.py | 2 +- pyomo/common/tests/import_ex.py | 2 +- pyomo/common/tests/relo_mod.py | 2 +- pyomo/common/tests/relo_mod_new.py | 2 +- pyomo/common/tests/relocated.py | 2 +- pyomo/common/tests/test_bunch.py | 2 +- pyomo/common/tests/test_config.py | 2 +- pyomo/common/tests/test_dependencies.py | 2 +- pyomo/common/tests/test_deprecated.py | 2 +- pyomo/common/tests/test_download.py | 2 +- pyomo/common/tests/test_env.py | 2 +- pyomo/common/tests/test_errors.py | 2 +- pyomo/common/tests/test_fileutils.py | 2 +- pyomo/common/tests/test_formatting.py | 2 +- pyomo/common/tests/test_gc.py | 2 +- pyomo/common/tests/test_log.py | 2 +- pyomo/common/tests/test_modeling.py | 2 +- pyomo/common/tests/test_multithread.py | 2 +- pyomo/common/tests/test_orderedset.py | 2 +- pyomo/common/tests/test_plugin.py | 2 +- pyomo/common/tests/test_sorting.py | 2 +- pyomo/common/tests/test_tee.py | 2 +- pyomo/common/tests/test_tempfile.py | 2 +- pyomo/common/tests/test_timing.py | 2 +- pyomo/common/tests/test_typing.py | 2 +- pyomo/common/tests/test_unittest.py | 2 +- pyomo/common/timing.py | 2 +- pyomo/common/unittest.py | 2 +- pyomo/contrib/__init__.py | 2 +- pyomo/contrib/ampl_function_demo/__init__.py | 2 +- pyomo/contrib/ampl_function_demo/build.py | 2 +- pyomo/contrib/ampl_function_demo/plugins.py | 2 +- pyomo/contrib/ampl_function_demo/src/CMakeLists.txt | 2 +- pyomo/contrib/ampl_function_demo/src/FindASL.cmake | 2 +- pyomo/contrib/ampl_function_demo/src/functions.c | 2 +- pyomo/contrib/ampl_function_demo/tests/__init__.py | 2 +- .../ampl_function_demo/tests/test_ampl_function_demo.py | 2 +- pyomo/contrib/appsi/__init__.py | 2 +- pyomo/contrib/appsi/base.py | 2 +- pyomo/contrib/appsi/build.py | 2 +- pyomo/contrib/appsi/cmodel/__init__.py | 2 +- pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp | 2 +- pyomo/contrib/appsi/cmodel/src/common.cpp | 2 +- pyomo/contrib/appsi/cmodel/src/common.hpp | 2 +- pyomo/contrib/appsi/cmodel/src/expression.cpp | 2 +- pyomo/contrib/appsi/cmodel/src/expression.hpp | 2 +- pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp | 2 +- pyomo/contrib/appsi/cmodel/src/fbbt_model.hpp | 2 +- pyomo/contrib/appsi/cmodel/src/interval.cpp | 2 +- pyomo/contrib/appsi/cmodel/src/interval.hpp | 2 +- pyomo/contrib/appsi/cmodel/src/lp_writer.cpp | 2 +- pyomo/contrib/appsi/cmodel/src/lp_writer.hpp | 2 +- pyomo/contrib/appsi/cmodel/src/model_base.cpp | 2 +- pyomo/contrib/appsi/cmodel/src/model_base.hpp | 2 +- pyomo/contrib/appsi/cmodel/src/nl_writer.cpp | 2 +- pyomo/contrib/appsi/cmodel/src/nl_writer.hpp | 2 +- pyomo/contrib/appsi/cmodel/tests/__init__.py | 2 +- pyomo/contrib/appsi/cmodel/tests/test_import.py | 2 +- pyomo/contrib/appsi/examples/__init__.py | 2 +- pyomo/contrib/appsi/examples/getting_started.py | 2 +- pyomo/contrib/appsi/examples/tests/__init__.py | 2 +- pyomo/contrib/appsi/examples/tests/test_examples.py | 2 +- pyomo/contrib/appsi/fbbt.py | 2 +- pyomo/contrib/appsi/plugins.py | 2 +- pyomo/contrib/appsi/solvers/__init__.py | 2 +- pyomo/contrib/appsi/solvers/cbc.py | 2 +- pyomo/contrib/appsi/solvers/cplex.py | 2 +- pyomo/contrib/appsi/solvers/gurobi.py | 2 +- pyomo/contrib/appsi/solvers/highs.py | 2 +- pyomo/contrib/appsi/solvers/ipopt.py | 2 +- pyomo/contrib/appsi/solvers/tests/__init__.py | 2 +- pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py | 2 +- pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py | 2 +- pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py | 2 +- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 2 +- pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py | 2 +- pyomo/contrib/appsi/solvers/wntr.py | 2 +- pyomo/contrib/appsi/tests/__init__.py | 2 +- pyomo/contrib/appsi/tests/test_base.py | 2 +- pyomo/contrib/appsi/tests/test_fbbt.py | 2 +- pyomo/contrib/appsi/tests/test_interval.py | 2 +- pyomo/contrib/appsi/utils/__init__.py | 2 +- pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py | 2 +- pyomo/contrib/appsi/utils/get_objective.py | 2 +- pyomo/contrib/appsi/utils/tests/__init__.py | 2 +- .../appsi/utils/tests/test_collect_vars_and_named_exprs.py | 2 +- pyomo/contrib/appsi/writers/__init__.py | 2 +- pyomo/contrib/appsi/writers/config.py | 2 +- pyomo/contrib/appsi/writers/lp_writer.py | 2 +- pyomo/contrib/appsi/writers/nl_writer.py | 2 +- pyomo/contrib/appsi/writers/tests/__init__.py | 2 +- pyomo/contrib/appsi/writers/tests/test_nl_writer.py | 2 +- pyomo/contrib/benders/__init__.py | 2 +- pyomo/contrib/benders/benders_cuts.py | 2 +- pyomo/contrib/benders/examples/__init__.py | 2 +- pyomo/contrib/benders/examples/farmer.py | 2 +- pyomo/contrib/benders/examples/grothey_ex.py | 2 +- pyomo/contrib/benders/tests/__init__.py | 2 +- pyomo/contrib/benders/tests/test_benders.py | 2 +- pyomo/contrib/community_detection/__init__.py | 2 +- pyomo/contrib/community_detection/community_graph.py | 2 +- pyomo/contrib/community_detection/detection.py | 2 +- pyomo/contrib/community_detection/event_log.py | 2 +- pyomo/contrib/community_detection/plugins.py | 2 +- pyomo/contrib/community_detection/tests/__init__.py | 2 +- pyomo/contrib/community_detection/tests/test_detection.py | 2 +- pyomo/contrib/cp/__init__.py | 2 +- pyomo/contrib/cp/interval_var.py | 2 +- pyomo/contrib/cp/plugins.py | 2 +- pyomo/contrib/cp/repn/__init__.py | 2 +- pyomo/contrib/cp/repn/docplex_writer.py | 2 +- pyomo/contrib/cp/scheduling_expr/__init__.py | 2 +- pyomo/contrib/cp/scheduling_expr/precedence_expressions.py | 2 +- pyomo/contrib/cp/scheduling_expr/step_function_expressions.py | 2 +- pyomo/contrib/cp/tests/__init__.py | 2 +- pyomo/contrib/cp/tests/test_docplex_walker.py | 2 +- pyomo/contrib/cp/tests/test_docplex_writer.py | 2 +- pyomo/contrib/cp/tests/test_interval_var.py | 2 +- pyomo/contrib/cp/tests/test_logical_to_disjunctive.py | 2 +- pyomo/contrib/cp/tests/test_precedence_constraints.py | 2 +- pyomo/contrib/cp/tests/test_step_function_expressions.py | 2 +- pyomo/contrib/cp/transform/__init__.py | 2 +- pyomo/contrib/cp/transform/logical_to_disjunctive_program.py | 2 +- pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py | 2 +- pyomo/contrib/doe/__init__.py | 2 +- pyomo/contrib/doe/doe.py | 2 +- pyomo/contrib/doe/examples/__init__.py | 2 +- pyomo/contrib/doe/examples/reactor_compute_FIM.py | 2 +- pyomo/contrib/doe/examples/reactor_grid_search.py | 2 +- pyomo/contrib/doe/examples/reactor_kinetics.py | 2 +- pyomo/contrib/doe/examples/reactor_optimize_doe.py | 2 +- pyomo/contrib/doe/measurements.py | 2 +- pyomo/contrib/doe/result.py | 2 +- pyomo/contrib/doe/scenario.py | 2 +- pyomo/contrib/doe/tests/__init__.py | 2 +- pyomo/contrib/doe/tests/test_example.py | 2 +- pyomo/contrib/doe/tests/test_fim_doe.py | 2 +- pyomo/contrib/doe/tests/test_reactor_example.py | 2 +- pyomo/contrib/example/__init__.py | 2 +- pyomo/contrib/example/bar.py | 2 +- pyomo/contrib/example/foo.py | 2 +- pyomo/contrib/example/plugins/__init__.py | 2 +- pyomo/contrib/example/plugins/ex_plugin.py | 2 +- pyomo/contrib/example/tests/__init__.py | 2 +- pyomo/contrib/example/tests/test_example.py | 2 +- pyomo/contrib/fbbt/__init__.py | 2 +- pyomo/contrib/fbbt/expression_bounds_walker.py | 2 +- pyomo/contrib/fbbt/fbbt.py | 2 +- pyomo/contrib/fbbt/interval.py | 2 +- pyomo/contrib/fbbt/tests/__init__.py | 2 +- pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py | 2 +- pyomo/contrib/fbbt/tests/test_fbbt.py | 2 +- pyomo/contrib/fbbt/tests/test_interval.py | 2 +- pyomo/contrib/fme/__init__.py | 2 +- pyomo/contrib/fme/fourier_motzkin_elimination.py | 2 +- pyomo/contrib/fme/plugins.py | 2 +- pyomo/contrib/fme/tests/__init__.py | 2 +- pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py | 2 +- pyomo/contrib/gdp_bounds/__init__.py | 2 +- pyomo/contrib/gdp_bounds/compute_bounds.py | 2 +- pyomo/contrib/gdp_bounds/info.py | 2 +- pyomo/contrib/gdp_bounds/plugins.py | 2 +- pyomo/contrib/gdp_bounds/tests/__init__.py | 2 +- pyomo/contrib/gdp_bounds/tests/test_gdp_bounds.py | 2 +- pyomo/contrib/gdpopt/GDPopt.py | 2 +- pyomo/contrib/gdpopt/__init__.py | 2 +- pyomo/contrib/gdpopt/algorithm_base_class.py | 2 +- pyomo/contrib/gdpopt/branch_and_bound.py | 2 +- pyomo/contrib/gdpopt/config_options.py | 2 +- pyomo/contrib/gdpopt/create_oa_subproblems.py | 2 +- pyomo/contrib/gdpopt/cut_generation.py | 2 +- pyomo/contrib/gdpopt/discrete_problem_initialize.py | 2 +- pyomo/contrib/gdpopt/enumerate.py | 2 +- pyomo/contrib/gdpopt/gloa.py | 2 +- pyomo/contrib/gdpopt/loa.py | 2 +- pyomo/contrib/gdpopt/nlp_initialization.py | 2 +- pyomo/contrib/gdpopt/oa_algorithm_utils.py | 2 +- pyomo/contrib/gdpopt/plugins.py | 2 +- pyomo/contrib/gdpopt/ric.py | 2 +- pyomo/contrib/gdpopt/solve_discrete_problem.py | 2 +- pyomo/contrib/gdpopt/solve_subproblem.py | 2 +- pyomo/contrib/gdpopt/tests/__init__.py | 2 +- pyomo/contrib/gdpopt/tests/common_tests.py | 2 +- pyomo/contrib/gdpopt/tests/test_LBB.py | 2 +- pyomo/contrib/gdpopt/tests/test_enumerate.py | 2 +- pyomo/contrib/gdpopt/tests/test_gdpopt.py | 2 +- pyomo/contrib/gdpopt/util.py | 2 +- pyomo/contrib/gjh/GJH.py | 2 +- pyomo/contrib/gjh/__init__.py | 2 +- pyomo/contrib/gjh/getGJH.py | 2 +- pyomo/contrib/gjh/plugins.py | 2 +- pyomo/contrib/iis/__init__.py | 2 +- pyomo/contrib/iis/iis.py | 2 +- pyomo/contrib/iis/tests/__init__.py | 2 +- pyomo/contrib/iis/tests/test_iis.py | 2 +- pyomo/contrib/incidence_analysis/__init__.py | 2 +- pyomo/contrib/incidence_analysis/common/__init__.py | 2 +- pyomo/contrib/incidence_analysis/common/dulmage_mendelsohn.py | 2 +- pyomo/contrib/incidence_analysis/common/tests/__init__.py | 2 +- .../common/tests/test_dulmage_mendelsohn.py | 2 +- pyomo/contrib/incidence_analysis/config.py | 2 +- pyomo/contrib/incidence_analysis/connected.py | 2 +- pyomo/contrib/incidence_analysis/dulmage_mendelsohn.py | 2 +- pyomo/contrib/incidence_analysis/incidence.py | 2 +- pyomo/contrib/incidence_analysis/interface.py | 2 +- pyomo/contrib/incidence_analysis/matching.py | 2 +- pyomo/contrib/incidence_analysis/scc_solver.py | 2 +- pyomo/contrib/incidence_analysis/tests/__init__.py | 2 +- pyomo/contrib/incidence_analysis/tests/models_for_testing.py | 2 +- pyomo/contrib/incidence_analysis/tests/test_connected.py | 2 +- .../incidence_analysis/tests/test_dulmage_mendelsohn.py | 2 +- pyomo/contrib/incidence_analysis/tests/test_incidence.py | 2 +- pyomo/contrib/incidence_analysis/tests/test_interface.py | 2 +- pyomo/contrib/incidence_analysis/tests/test_matching.py | 2 +- pyomo/contrib/incidence_analysis/tests/test_scc_solver.py | 2 +- pyomo/contrib/incidence_analysis/tests/test_triangularize.py | 2 +- pyomo/contrib/incidence_analysis/triangularize.py | 2 +- pyomo/contrib/incidence_analysis/util.py | 2 +- pyomo/contrib/interior_point/__init__.py | 2 +- pyomo/contrib/interior_point/examples/__init__.py | 2 +- pyomo/contrib/interior_point/examples/ex1.py | 2 +- pyomo/contrib/interior_point/interface.py | 2 +- pyomo/contrib/interior_point/interior_point.py | 2 +- pyomo/contrib/interior_point/inverse_reduced_hessian.py | 2 +- pyomo/contrib/interior_point/linalg/__init__.py | 2 +- .../interior_point/linalg/base_linear_solver_interface.py | 2 +- pyomo/contrib/interior_point/linalg/ma27_interface.py | 2 +- pyomo/contrib/interior_point/linalg/mumps_interface.py | 2 +- pyomo/contrib/interior_point/linalg/scipy_interface.py | 2 +- pyomo/contrib/interior_point/linalg/tests/__init__.py | 2 +- .../interior_point/linalg/tests/test_linear_solvers.py | 2 +- pyomo/contrib/interior_point/linalg/tests/test_realloc.py | 2 +- pyomo/contrib/interior_point/tests/__init__.py | 2 +- pyomo/contrib/interior_point/tests/test_interior_point.py | 2 +- .../interior_point/tests/test_inverse_reduced_hessian.py | 2 +- pyomo/contrib/interior_point/tests/test_realloc.py | 2 +- pyomo/contrib/interior_point/tests/test_reg.py | 2 +- pyomo/contrib/latex_printer/__init__.py | 2 +- pyomo/contrib/latex_printer/latex_printer.py | 2 +- pyomo/contrib/latex_printer/tests/__init__.py | 2 +- pyomo/contrib/latex_printer/tests/test_latex_printer.py | 2 +- .../latex_printer/tests/test_latex_printer_vartypes.py | 2 +- pyomo/contrib/mcpp/__init__.py | 2 +- pyomo/contrib/mcpp/build.py | 2 +- pyomo/contrib/mcpp/getMCPP.py | 2 +- pyomo/contrib/mcpp/mcppInterface.cpp | 2 +- pyomo/contrib/mcpp/plugins.py | 2 +- pyomo/contrib/mcpp/pyomo_mcpp.py | 2 +- pyomo/contrib/mcpp/test_mcpp.py | 2 +- pyomo/contrib/mindtpy/MindtPy.py | 2 +- pyomo/contrib/mindtpy/__init__.py | 2 +- pyomo/contrib/mindtpy/algorithm_base_class.py | 2 +- pyomo/contrib/mindtpy/config_options.py | 2 +- pyomo/contrib/mindtpy/cut_generation.py | 2 +- pyomo/contrib/mindtpy/extended_cutting_plane.py | 2 +- pyomo/contrib/mindtpy/feasibility_pump.py | 2 +- pyomo/contrib/mindtpy/global_outer_approximation.py | 2 +- pyomo/contrib/mindtpy/outer_approximation.py | 2 +- pyomo/contrib/mindtpy/plugins.py | 2 +- pyomo/contrib/mindtpy/single_tree.py | 2 +- pyomo/contrib/mindtpy/tabu_list.py | 2 +- pyomo/contrib/mindtpy/tests/MINLP2_simple.py | 2 +- pyomo/contrib/mindtpy/tests/MINLP3_simple.py | 2 +- pyomo/contrib/mindtpy/tests/MINLP4_simple.py | 2 +- pyomo/contrib/mindtpy/tests/MINLP5_simple.py | 2 +- pyomo/contrib/mindtpy/tests/MINLP_simple.py | 2 +- pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py | 2 +- pyomo/contrib/mindtpy/tests/__init__.py | 2 +- .../contrib/mindtpy/tests/constraint_qualification_example.py | 2 +- pyomo/contrib/mindtpy/tests/eight_process_problem.py | 2 +- pyomo/contrib/mindtpy/tests/feasibility_pump1.py | 2 +- pyomo/contrib/mindtpy/tests/feasibility_pump2.py | 2 +- pyomo/contrib/mindtpy/tests/from_proposal.py | 2 +- pyomo/contrib/mindtpy/tests/nonconvex1.py | 2 +- pyomo/contrib/mindtpy/tests/nonconvex2.py | 2 +- pyomo/contrib/mindtpy/tests/nonconvex3.py | 2 +- pyomo/contrib/mindtpy/tests/nonconvex4.py | 2 +- pyomo/contrib/mindtpy/tests/online_doc_example.py | 2 +- pyomo/contrib/mindtpy/tests/test_mindtpy.py | 2 +- pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py | 2 +- pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py | 2 +- pyomo/contrib/mindtpy/tests/test_mindtpy_global.py | 2 +- pyomo/contrib/mindtpy/tests/test_mindtpy_global_lp_nlp.py | 2 +- pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py | 2 +- pyomo/contrib/mindtpy/tests/test_mindtpy_lp_nlp.py | 2 +- pyomo/contrib/mindtpy/tests/test_mindtpy_regularization.py | 2 +- pyomo/contrib/mindtpy/tests/test_mindtpy_solution_pool.py | 2 +- pyomo/contrib/mindtpy/tests/unit_test.py | 2 +- pyomo/contrib/mindtpy/util.py | 2 +- pyomo/contrib/mpc/__init__.py | 2 +- pyomo/contrib/mpc/data/__init__.py | 2 +- pyomo/contrib/mpc/data/convert.py | 2 +- pyomo/contrib/mpc/data/dynamic_data_base.py | 2 +- pyomo/contrib/mpc/data/find_nearest_index.py | 2 +- pyomo/contrib/mpc/data/get_cuid.py | 2 +- pyomo/contrib/mpc/data/interval_data.py | 2 +- pyomo/contrib/mpc/data/scalar_data.py | 2 +- pyomo/contrib/mpc/data/series_data.py | 2 +- pyomo/contrib/mpc/data/tests/__init__.py | 2 +- pyomo/contrib/mpc/data/tests/test_convert.py | 2 +- pyomo/contrib/mpc/data/tests/test_find_nearest_index.py | 2 +- pyomo/contrib/mpc/data/tests/test_get_cuid.py | 2 +- pyomo/contrib/mpc/data/tests/test_interval_data.py | 2 +- pyomo/contrib/mpc/data/tests/test_scalar_data.py | 2 +- pyomo/contrib/mpc/data/tests/test_series_data.py | 2 +- pyomo/contrib/mpc/examples/__init__.py | 2 +- pyomo/contrib/mpc/examples/cstr/__init__.py | 2 +- pyomo/contrib/mpc/examples/cstr/model.py | 2 +- pyomo/contrib/mpc/examples/cstr/run_mpc.py | 2 +- pyomo/contrib/mpc/examples/cstr/run_openloop.py | 2 +- pyomo/contrib/mpc/examples/cstr/tests/__init__.py | 2 +- pyomo/contrib/mpc/examples/cstr/tests/test_mpc.py | 2 +- pyomo/contrib/mpc/examples/cstr/tests/test_openloop.py | 2 +- pyomo/contrib/mpc/interfaces/__init__.py | 2 +- pyomo/contrib/mpc/interfaces/copy_values.py | 2 +- pyomo/contrib/mpc/interfaces/load_data.py | 2 +- pyomo/contrib/mpc/interfaces/model_interface.py | 2 +- pyomo/contrib/mpc/interfaces/tests/__init__.py | 2 +- pyomo/contrib/mpc/interfaces/tests/test_interface.py | 2 +- pyomo/contrib/mpc/interfaces/tests/test_var_linker.py | 2 +- pyomo/contrib/mpc/interfaces/var_linker.py | 2 +- pyomo/contrib/mpc/modeling/__init__.py | 2 +- pyomo/contrib/mpc/modeling/constraints.py | 2 +- pyomo/contrib/mpc/modeling/cost_expressions.py | 2 +- pyomo/contrib/mpc/modeling/terminal.py | 2 +- pyomo/contrib/mpc/modeling/tests/__init__.py | 2 +- pyomo/contrib/mpc/modeling/tests/test_cost_expressions.py | 2 +- pyomo/contrib/mpc/modeling/tests/test_input_constraints.py | 2 +- pyomo/contrib/mpc/modeling/tests/test_terminal.py | 2 +- pyomo/contrib/multistart/__init__.py | 2 +- pyomo/contrib/multistart/high_conf_stop.py | 2 +- pyomo/contrib/multistart/multi.py | 2 +- pyomo/contrib/multistart/plugins.py | 2 +- pyomo/contrib/multistart/reinit.py | 2 +- pyomo/contrib/multistart/test_multi.py | 2 +- pyomo/contrib/parmest/__init__.py | 2 +- pyomo/contrib/parmest/examples/__init__.py | 2 +- pyomo/contrib/parmest/examples/reaction_kinetics/__init__.py | 2 +- .../reaction_kinetics/simple_reaction_parmest_example.py | 2 +- pyomo/contrib/parmest/examples/reactor_design/__init__.py | 2 +- .../parmest/examples/reactor_design/bootstrap_example.py | 2 +- .../parmest/examples/reactor_design/datarec_example.py | 2 +- .../parmest/examples/reactor_design/leaveNout_example.py | 2 +- .../examples/reactor_design/likelihood_ratio_example.py | 2 +- .../examples/reactor_design/multisensor_data_example.py | 2 +- .../examples/reactor_design/parameter_estimation_example.py | 2 +- .../contrib/parmest/examples/reactor_design/reactor_design.py | 2 +- .../examples/reactor_design/timeseries_data_example.py | 2 +- pyomo/contrib/parmest/examples/rooney_biegler/__init__.py | 2 +- .../parmest/examples/rooney_biegler/bootstrap_example.py | 2 +- .../examples/rooney_biegler/likelihood_ratio_example.py | 2 +- .../examples/rooney_biegler/parameter_estimation_example.py | 2 +- .../contrib/parmest/examples/rooney_biegler/rooney_biegler.py | 2 +- .../examples/rooney_biegler/rooney_biegler_with_constraint.py | 2 +- pyomo/contrib/parmest/examples/semibatch/__init__.py | 2 +- pyomo/contrib/parmest/examples/semibatch/parallel_example.py | 2 +- .../examples/semibatch/parameter_estimation_example.py | 2 +- pyomo/contrib/parmest/examples/semibatch/scenario_example.py | 2 +- pyomo/contrib/parmest/examples/semibatch/semibatch.py | 2 +- pyomo/contrib/parmest/graphics.py | 2 +- pyomo/contrib/parmest/ipopt_solver_wrapper.py | 2 +- pyomo/contrib/parmest/parmest.py | 2 +- pyomo/contrib/parmest/scenariocreator.py | 2 +- pyomo/contrib/parmest/tests/__init__.py | 2 +- pyomo/contrib/parmest/tests/test_examples.py | 2 +- pyomo/contrib/parmest/tests/test_graphics.py | 2 +- pyomo/contrib/parmest/tests/test_parmest.py | 2 +- pyomo/contrib/parmest/tests/test_scenariocreator.py | 2 +- pyomo/contrib/parmest/tests/test_solver.py | 2 +- pyomo/contrib/parmest/tests/test_utils.py | 2 +- pyomo/contrib/parmest/utils/__init__.py | 2 +- pyomo/contrib/parmest/utils/create_ef.py | 2 +- pyomo/contrib/parmest/utils/ipopt_solver_wrapper.py | 2 +- pyomo/contrib/parmest/utils/model_utils.py | 2 +- pyomo/contrib/parmest/utils/mpi_utils.py | 2 +- pyomo/contrib/parmest/utils/scenario_tree.py | 2 +- pyomo/contrib/piecewise/__init__.py | 2 +- pyomo/contrib/piecewise/piecewise_linear_expression.py | 2 +- pyomo/contrib/piecewise/piecewise_linear_function.py | 2 +- pyomo/contrib/piecewise/tests/__init__.py | 2 +- pyomo/contrib/piecewise/tests/common_tests.py | 2 +- pyomo/contrib/piecewise/tests/models.py | 2 +- pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py | 2 +- pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py | 2 +- .../contrib/piecewise/tests/test_piecewise_linear_function.py | 2 +- pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py | 2 +- pyomo/contrib/piecewise/transform/__init__.py | 2 +- pyomo/contrib/piecewise/transform/convex_combination.py | 2 +- .../piecewise/transform/disaggregated_convex_combination.py | 2 +- pyomo/contrib/piecewise/transform/inner_representation_gdp.py | 2 +- pyomo/contrib/piecewise/transform/multiple_choice.py | 2 +- pyomo/contrib/piecewise/transform/outer_representation_gdp.py | 2 +- .../piecewise/transform/piecewise_to_gdp_transformation.py | 2 +- pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py | 2 +- .../piecewise/transform/reduced_inner_representation_gdp.py | 2 +- pyomo/contrib/preprocessing/__init__.py | 2 +- pyomo/contrib/preprocessing/plugins/__init__.py | 2 +- pyomo/contrib/preprocessing/plugins/bounds_to_vars.py | 2 +- pyomo/contrib/preprocessing/plugins/constraint_tightener.py | 2 +- .../preprocessing/plugins/deactivate_trivial_constraints.py | 2 +- pyomo/contrib/preprocessing/plugins/detect_fixed_vars.py | 2 +- pyomo/contrib/preprocessing/plugins/equality_propagate.py | 2 +- pyomo/contrib/preprocessing/plugins/induced_linearity.py | 2 +- pyomo/contrib/preprocessing/plugins/init_vars.py | 2 +- pyomo/contrib/preprocessing/plugins/int_to_binary.py | 2 +- pyomo/contrib/preprocessing/plugins/remove_zero_terms.py | 2 +- pyomo/contrib/preprocessing/plugins/strip_bounds.py | 2 +- pyomo/contrib/preprocessing/plugins/var_aggregator.py | 2 +- pyomo/contrib/preprocessing/plugins/zero_sum_propagator.py | 2 +- pyomo/contrib/preprocessing/tests/__init__.py | 2 +- pyomo/contrib/preprocessing/tests/test_bounds_to_vars_xfrm.py | 2 +- .../contrib/preprocessing/tests/test_constraint_tightener.py | 2 +- .../tests/test_deactivate_trivial_constraints.py | 2 +- pyomo/contrib/preprocessing/tests/test_detect_fixed_vars.py | 2 +- pyomo/contrib/preprocessing/tests/test_equality_propagate.py | 2 +- pyomo/contrib/preprocessing/tests/test_induced_linearity.py | 2 +- pyomo/contrib/preprocessing/tests/test_init_vars.py | 2 +- pyomo/contrib/preprocessing/tests/test_int_to_binary.py | 2 +- pyomo/contrib/preprocessing/tests/test_strip_bounds.py | 2 +- pyomo/contrib/preprocessing/tests/test_var_aggregator.py | 2 +- pyomo/contrib/preprocessing/tests/test_zero_sum_propagate.py | 2 +- pyomo/contrib/preprocessing/tests/test_zero_term_removal.py | 2 +- pyomo/contrib/preprocessing/util.py | 2 +- pyomo/contrib/pynumero/__init__.py | 2 +- pyomo/contrib/pynumero/algorithms/__init__.py | 2 +- pyomo/contrib/pynumero/algorithms/solvers/__init__.py | 2 +- pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py | 2 +- .../contrib/pynumero/algorithms/solvers/implicit_functions.py | 2 +- .../contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py | 2 +- pyomo/contrib/pynumero/algorithms/solvers/scipy_solvers.py | 2 +- .../contrib/pynumero/algorithms/solvers/square_solver_base.py | 2 +- pyomo/contrib/pynumero/algorithms/solvers/tests/__init__.py | 2 +- .../algorithms/solvers/tests/test_cyipopt_interfaces.py | 2 +- .../pynumero/algorithms/solvers/tests/test_cyipopt_solver.py | 2 +- .../algorithms/solvers/tests/test_implicit_functions.py | 2 +- .../algorithms/solvers/tests/test_pyomo_ext_cyipopt.py | 2 +- .../pynumero/algorithms/solvers/tests/test_scipy_solvers.py | 2 +- pyomo/contrib/pynumero/asl.py | 2 +- pyomo/contrib/pynumero/build.py | 2 +- pyomo/contrib/pynumero/dependencies.py | 2 +- pyomo/contrib/pynumero/examples/__init__.py | 2 +- pyomo/contrib/pynumero/examples/callback/__init__.py | 2 +- pyomo/contrib/pynumero/examples/callback/cyipopt_callback.py | 2 +- .../pynumero/examples/callback/cyipopt_callback_halt.py | 2 +- .../pynumero/examples/callback/cyipopt_functor_callback.py | 2 +- pyomo/contrib/pynumero/examples/callback/reactor_design.py | 2 +- pyomo/contrib/pynumero/examples/external_grey_box/__init__.py | 2 +- .../pynumero/examples/external_grey_box/param_est/__init__.py | 2 +- .../examples/external_grey_box/param_est/generate_data.py | 2 +- .../pynumero/examples/external_grey_box/param_est/models.py | 2 +- .../external_grey_box/param_est/perform_estimation.py | 2 +- .../examples/external_grey_box/react_example/__init__.py | 2 +- .../external_grey_box/react_example/maximize_cb_outputs.py | 2 +- .../react_example/maximize_cb_ratio_residuals.py | 2 +- .../external_grey_box/react_example/reactor_model_outputs.py | 2 +- .../react_example/reactor_model_residuals.py | 2 +- pyomo/contrib/pynumero/examples/feasibility.py | 2 +- pyomo/contrib/pynumero/examples/mumps_example.py | 2 +- pyomo/contrib/pynumero/examples/nlp_interface.py | 2 +- pyomo/contrib/pynumero/examples/nlp_interface_2.py | 2 +- pyomo/contrib/pynumero/examples/parallel_matvec.py | 2 +- pyomo/contrib/pynumero/examples/parallel_vector_ops.py | 2 +- pyomo/contrib/pynumero/examples/sensitivity.py | 2 +- pyomo/contrib/pynumero/examples/sqp.py | 2 +- pyomo/contrib/pynumero/examples/tests/__init__.py | 2 +- .../contrib/pynumero/examples/tests/test_cyipopt_examples.py | 2 +- pyomo/contrib/pynumero/examples/tests/test_examples.py | 2 +- pyomo/contrib/pynumero/examples/tests/test_mpi_examples.py | 2 +- pyomo/contrib/pynumero/exceptions.py | 2 +- pyomo/contrib/pynumero/interfaces/__init__.py | 2 +- pyomo/contrib/pynumero/interfaces/ampl_nlp.py | 2 +- pyomo/contrib/pynumero/interfaces/cyipopt_interface.py | 2 +- pyomo/contrib/pynumero/interfaces/external_grey_box.py | 2 +- pyomo/contrib/pynumero/interfaces/external_pyomo_model.py | 2 +- pyomo/contrib/pynumero/interfaces/nlp.py | 2 +- pyomo/contrib/pynumero/interfaces/nlp_projections.py | 2 +- pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py | 2 +- pyomo/contrib/pynumero/interfaces/pyomo_nlp.py | 2 +- pyomo/contrib/pynumero/interfaces/tests/__init__.py | 2 +- pyomo/contrib/pynumero/interfaces/tests/compare_utils.py | 2 +- .../pynumero/interfaces/tests/external_grey_box_models.py | 2 +- .../pynumero/interfaces/tests/test_cyipopt_interface.py | 2 +- pyomo/contrib/pynumero/interfaces/tests/test_dynamic_model.py | 2 +- .../pynumero/interfaces/tests/test_external_asl_function.py | 2 +- .../pynumero/interfaces/tests/test_external_grey_box_model.py | 2 +- .../pynumero/interfaces/tests/test_external_pyomo_block.py | 2 +- .../pynumero/interfaces/tests/test_external_pyomo_model.py | 2 +- pyomo/contrib/pynumero/interfaces/tests/test_nlp.py | 2 +- .../contrib/pynumero/interfaces/tests/test_nlp_projections.py | 2 +- .../pynumero/interfaces/tests/test_pyomo_grey_box_nlp.py | 2 +- pyomo/contrib/pynumero/interfaces/tests/test_utils.py | 2 +- pyomo/contrib/pynumero/interfaces/utils.py | 2 +- pyomo/contrib/pynumero/intrinsic.py | 2 +- pyomo/contrib/pynumero/linalg/__init__.py | 2 +- pyomo/contrib/pynumero/linalg/base.py | 2 +- pyomo/contrib/pynumero/linalg/ma27.py | 2 +- pyomo/contrib/pynumero/linalg/ma27_interface.py | 2 +- pyomo/contrib/pynumero/linalg/ma57.py | 2 +- pyomo/contrib/pynumero/linalg/ma57_interface.py | 2 +- pyomo/contrib/pynumero/linalg/mumps_interface.py | 2 +- pyomo/contrib/pynumero/linalg/scipy_interface.py | 2 +- pyomo/contrib/pynumero/linalg/tests/__init__.py | 2 +- pyomo/contrib/pynumero/linalg/tests/test_linear_solvers.py | 2 +- pyomo/contrib/pynumero/linalg/tests/test_ma27.py | 2 +- pyomo/contrib/pynumero/linalg/tests/test_ma57.py | 2 +- pyomo/contrib/pynumero/linalg/tests/test_mumps_interface.py | 2 +- pyomo/contrib/pynumero/linalg/utils.py | 2 +- pyomo/contrib/pynumero/plugins.py | 2 +- pyomo/contrib/pynumero/sparse/__init__.py | 2 +- pyomo/contrib/pynumero/sparse/base_block.py | 2 +- pyomo/contrib/pynumero/sparse/block_matrix.py | 2 +- pyomo/contrib/pynumero/sparse/block_vector.py | 2 +- pyomo/contrib/pynumero/sparse/mpi_block_matrix.py | 2 +- pyomo/contrib/pynumero/sparse/mpi_block_vector.py | 2 +- pyomo/contrib/pynumero/sparse/tests/__init__.py | 2 +- pyomo/contrib/pynumero/sparse/tests/test_block_matrix.py | 2 +- pyomo/contrib/pynumero/sparse/tests/test_block_vector.py | 2 +- pyomo/contrib/pynumero/sparse/tests/test_intrinsics.py | 2 +- pyomo/contrib/pynumero/sparse/tests/test_mpi_block_matrix.py | 2 +- pyomo/contrib/pynumero/sparse/tests/test_mpi_block_vector.py | 2 +- pyomo/contrib/pynumero/src/AmplInterface.cpp | 2 +- pyomo/contrib/pynumero/src/AmplInterface.hpp | 2 +- pyomo/contrib/pynumero/src/AssertUtils.hpp | 2 +- pyomo/contrib/pynumero/src/ma27Interface.cpp | 2 +- pyomo/contrib/pynumero/src/ma57Interface.cpp | 2 +- pyomo/contrib/pynumero/src/tests/simple_test.cpp | 2 +- pyomo/contrib/pynumero/tests/__init__.py | 2 +- pyomo/contrib/pyros/__init__.py | 2 +- pyomo/contrib/pyros/master_problem_methods.py | 2 +- pyomo/contrib/pyros/pyros.py | 2 +- pyomo/contrib/pyros/pyros_algorithm_methods.py | 2 +- pyomo/contrib/pyros/separation_problem_methods.py | 2 +- pyomo/contrib/pyros/solve_data.py | 2 +- pyomo/contrib/pyros/tests/__init__.py | 2 +- pyomo/contrib/pyros/tests/test_grcs.py | 2 +- pyomo/contrib/pyros/uncertainty_sets.py | 2 +- pyomo/contrib/pyros/util.py | 2 +- pyomo/contrib/satsolver/__init__.py | 2 +- pyomo/contrib/satsolver/satsolver.py | 2 +- pyomo/contrib/satsolver/test_satsolver.py | 2 +- pyomo/contrib/sensitivity_toolbox/__init__.py | 4 ++-- .../contrib/sensitivity_toolbox/examples/HIV_Transmission.py | 2 +- pyomo/contrib/sensitivity_toolbox/examples/__init__.py | 4 ++-- .../sensitivity_toolbox/examples/feedbackController.py | 2 +- pyomo/contrib/sensitivity_toolbox/examples/parameter.py | 2 +- pyomo/contrib/sensitivity_toolbox/examples/parameter_kaug.py | 2 +- pyomo/contrib/sensitivity_toolbox/examples/rangeInequality.py | 2 +- pyomo/contrib/sensitivity_toolbox/examples/rooney_biegler.py | 2 +- pyomo/contrib/sensitivity_toolbox/k_aug.py | 4 ++-- pyomo/contrib/sensitivity_toolbox/sens.py | 4 ++-- pyomo/contrib/sensitivity_toolbox/tests/__init__.py | 4 ++-- .../contrib/sensitivity_toolbox/tests/test_k_aug_interface.py | 4 ++-- pyomo/contrib/sensitivity_toolbox/tests/test_sens.py | 4 ++-- pyomo/contrib/sensitivity_toolbox/tests/test_sens_unit.py | 4 ++-- pyomo/contrib/simplemodel/__init__.py | 2 +- pyomo/contrib/trustregion/TRF.py | 2 +- pyomo/contrib/trustregion/__init__.py | 2 +- pyomo/contrib/trustregion/examples/__init__.py | 2 +- pyomo/contrib/trustregion/examples/example1.py | 2 +- pyomo/contrib/trustregion/examples/example2.py | 2 +- pyomo/contrib/trustregion/filter.py | 2 +- pyomo/contrib/trustregion/interface.py | 2 +- pyomo/contrib/trustregion/plugins.py | 2 +- pyomo/contrib/trustregion/tests/__init__.py | 2 +- pyomo/contrib/trustregion/tests/test_TRF.py | 2 +- pyomo/contrib/trustregion/tests/test_examples.py | 2 +- pyomo/contrib/trustregion/tests/test_filter.py | 2 +- pyomo/contrib/trustregion/tests/test_interface.py | 2 +- pyomo/contrib/trustregion/tests/test_util.py | 2 +- pyomo/contrib/trustregion/util.py | 2 +- pyomo/contrib/viewer/__init__.py | 2 +- pyomo/contrib/viewer/model_browser.py | 2 +- pyomo/contrib/viewer/model_select.py | 2 +- pyomo/contrib/viewer/pyomo_viewer.py | 2 +- pyomo/contrib/viewer/qt.py | 2 +- pyomo/contrib/viewer/report.py | 2 +- pyomo/contrib/viewer/residual_table.py | 2 +- pyomo/contrib/viewer/tests/__init__.py | 2 +- pyomo/contrib/viewer/tests/test_data_model_item.py | 2 +- pyomo/contrib/viewer/tests/test_data_model_tree.py | 2 +- pyomo/contrib/viewer/tests/test_qt.py | 2 +- pyomo/contrib/viewer/tests/test_report.py | 2 +- pyomo/contrib/viewer/ui.py | 2 +- pyomo/contrib/viewer/ui_data.py | 2 +- pyomo/core/__init__.py | 2 +- pyomo/core/base/PyomoModel.py | 2 +- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/action.py | 2 +- pyomo/core/base/block.py | 2 +- pyomo/core/base/blockutil.py | 2 +- pyomo/core/base/boolean_var.py | 2 +- pyomo/core/base/check.py | 2 +- pyomo/core/base/component.py | 2 +- pyomo/core/base/component_namer.py | 2 +- pyomo/core/base/component_order.py | 2 +- pyomo/core/base/componentuid.py | 2 +- pyomo/core/base/config.py | 2 +- pyomo/core/base/connector.py | 2 +- pyomo/core/base/constraint.py | 2 +- pyomo/core/base/disable_methods.py | 2 +- pyomo/core/base/enums.py | 2 +- pyomo/core/base/expression.py | 2 +- pyomo/core/base/external.py | 2 +- pyomo/core/base/global_set.py | 2 +- pyomo/core/base/indexed_component.py | 2 +- pyomo/core/base/indexed_component_slice.py | 2 +- pyomo/core/base/initializer.py | 2 +- pyomo/core/base/instance2dat.py | 2 +- pyomo/core/base/label.py | 2 +- pyomo/core/base/logical_constraint.py | 2 +- pyomo/core/base/matrix_constraint.py | 2 +- pyomo/core/base/misc.py | 2 +- pyomo/core/base/numvalue.py | 2 +- pyomo/core/base/objective.py | 2 +- pyomo/core/base/param.py | 2 +- pyomo/core/base/piecewise.py | 2 +- pyomo/core/base/plugin.py | 2 +- pyomo/core/base/range.py | 2 +- pyomo/core/base/rangeset.py | 2 +- pyomo/core/base/reference.py | 2 +- pyomo/core/base/set.py | 2 +- pyomo/core/base/set_types.py | 2 +- pyomo/core/base/sets.py | 2 +- pyomo/core/base/sos.py | 2 +- pyomo/core/base/suffix.py | 2 +- pyomo/core/base/symbol_map.py | 2 +- pyomo/core/base/symbolic.py | 2 +- pyomo/core/base/template_expr.py | 2 +- pyomo/core/base/transformation.py | 2 +- pyomo/core/base/units_container.py | 2 +- pyomo/core/base/util.py | 2 +- pyomo/core/base/var.py | 2 +- pyomo/core/beta/__init__.py | 2 +- pyomo/core/beta/dict_objects.py | 2 +- pyomo/core/beta/list_objects.py | 2 +- pyomo/core/expr/__init__.py | 2 +- pyomo/core/expr/base.py | 2 +- pyomo/core/expr/boolean_value.py | 2 +- pyomo/core/expr/calculus/__init__.py | 2 +- pyomo/core/expr/calculus/derivatives.py | 2 +- pyomo/core/expr/calculus/diff_with_pyomo.py | 2 +- pyomo/core/expr/calculus/diff_with_sympy.py | 2 +- pyomo/core/expr/cnf_walker.py | 2 +- pyomo/core/expr/compare.py | 2 +- pyomo/core/expr/current.py | 2 +- pyomo/core/expr/expr_common.py | 2 +- pyomo/core/expr/expr_errors.py | 2 +- pyomo/core/expr/logical_expr.py | 2 +- pyomo/core/expr/ndarray.py | 2 +- pyomo/core/expr/numeric_expr.py | 2 +- pyomo/core/expr/numvalue.py | 2 +- pyomo/core/expr/relational_expr.py | 2 +- pyomo/core/expr/symbol_map.py | 2 +- pyomo/core/expr/sympy_tools.py | 2 +- pyomo/core/expr/taylor_series.py | 2 +- pyomo/core/expr/template_expr.py | 2 +- pyomo/core/expr/visitor.py | 2 +- pyomo/core/kernel/__init__.py | 2 +- pyomo/core/kernel/base.py | 2 +- pyomo/core/kernel/block.py | 2 +- pyomo/core/kernel/component_map.py | 2 +- pyomo/core/kernel/component_set.py | 2 +- pyomo/core/kernel/conic.py | 2 +- pyomo/core/kernel/constraint.py | 2 +- pyomo/core/kernel/container_utils.py | 2 +- pyomo/core/kernel/dict_container.py | 2 +- pyomo/core/kernel/expression.py | 2 +- pyomo/core/kernel/heterogeneous_container.py | 2 +- pyomo/core/kernel/homogeneous_container.py | 2 +- pyomo/core/kernel/list_container.py | 2 +- pyomo/core/kernel/matrix_constraint.py | 2 +- pyomo/core/kernel/objective.py | 2 +- pyomo/core/kernel/parameter.py | 2 +- pyomo/core/kernel/piecewise_library/__init__.py | 2 +- pyomo/core/kernel/piecewise_library/transforms.py | 2 +- pyomo/core/kernel/piecewise_library/transforms_nd.py | 2 +- pyomo/core/kernel/piecewise_library/util.py | 2 +- pyomo/core/kernel/register_numpy_types.py | 2 +- pyomo/core/kernel/set_types.py | 2 +- pyomo/core/kernel/sos.py | 2 +- pyomo/core/kernel/suffix.py | 2 +- pyomo/core/kernel/tuple_container.py | 2 +- pyomo/core/kernel/variable.py | 2 +- pyomo/core/plugins/__init__.py | 2 +- pyomo/core/plugins/transform/__init__.py | 2 +- pyomo/core/plugins/transform/add_slack_vars.py | 2 +- pyomo/core/plugins/transform/discrete_vars.py | 2 +- pyomo/core/plugins/transform/eliminate_fixed_vars.py | 2 +- pyomo/core/plugins/transform/equality_transform.py | 2 +- pyomo/core/plugins/transform/expand_connectors.py | 2 +- pyomo/core/plugins/transform/hierarchy.py | 2 +- pyomo/core/plugins/transform/logical_to_linear.py | 2 +- pyomo/core/plugins/transform/model.py | 2 +- pyomo/core/plugins/transform/nonnegative_transform.py | 2 +- pyomo/core/plugins/transform/radix_linearization.py | 2 +- pyomo/core/plugins/transform/relax_integrality.py | 2 +- pyomo/core/plugins/transform/scaling.py | 2 +- pyomo/core/plugins/transform/standard_form.py | 2 +- pyomo/core/plugins/transform/util.py | 2 +- pyomo/core/pyomoobject.py | 2 +- pyomo/core/staleflag.py | 2 +- pyomo/core/tests/__init__.py | 2 +- pyomo/core/tests/data/__init__.py | 2 +- pyomo/core/tests/data/test_odbc_ini.py | 2 +- pyomo/core/tests/diet/__init__.py | 2 +- pyomo/core/tests/diet/test_diet.py | 2 +- pyomo/core/tests/examples/__init__.py | 2 +- pyomo/core/tests/examples/pmedian.py | 2 +- pyomo/core/tests/examples/pmedian1.py | 2 +- pyomo/core/tests/examples/pmedian2.py | 2 +- pyomo/core/tests/examples/pmedian4.py | 2 +- pyomo/core/tests/examples/test_amplbook2.py | 2 +- pyomo/core/tests/examples/test_kernel_examples.py | 2 +- pyomo/core/tests/examples/test_pyomo.py | 2 +- pyomo/core/tests/examples/test_tutorials.py | 2 +- pyomo/core/tests/transform/__init__.py | 2 +- pyomo/core/tests/transform/test_add_slacks.py | 2 +- pyomo/core/tests/transform/test_scaling.py | 2 +- pyomo/core/tests/transform/test_transform.py | 2 +- pyomo/core/tests/unit/__init__.py | 2 +- pyomo/core/tests/unit/kernel/__init__.py | 2 +- pyomo/core/tests/unit/kernel/test_block.py | 2 +- pyomo/core/tests/unit/kernel/test_component_map.py | 2 +- pyomo/core/tests/unit/kernel/test_component_set.py | 2 +- pyomo/core/tests/unit/kernel/test_conic.py | 2 +- pyomo/core/tests/unit/kernel/test_constraint.py | 2 +- pyomo/core/tests/unit/kernel/test_dict_container.py | 2 +- pyomo/core/tests/unit/kernel/test_expression.py | 2 +- pyomo/core/tests/unit/kernel/test_kernel.py | 2 +- pyomo/core/tests/unit/kernel/test_list_container.py | 2 +- pyomo/core/tests/unit/kernel/test_matrix_constraint.py | 2 +- pyomo/core/tests/unit/kernel/test_objective.py | 2 +- pyomo/core/tests/unit/kernel/test_parameter.py | 2 +- pyomo/core/tests/unit/kernel/test_piecewise.py | 2 +- pyomo/core/tests/unit/kernel/test_sos.py | 2 +- pyomo/core/tests/unit/kernel/test_suffix.py | 2 +- pyomo/core/tests/unit/kernel/test_tuple_container.py | 2 +- pyomo/core/tests/unit/kernel/test_variable.py | 2 +- pyomo/core/tests/unit/test_action.py | 2 +- pyomo/core/tests/unit/test_block.py | 2 +- pyomo/core/tests/unit/test_block_model.py | 2 +- pyomo/core/tests/unit/test_bounds.py | 2 +- pyomo/core/tests/unit/test_check.py | 2 +- pyomo/core/tests/unit/test_compare.py | 2 +- pyomo/core/tests/unit/test_component.py | 2 +- pyomo/core/tests/unit/test_componentuid.py | 2 +- pyomo/core/tests/unit/test_con.py | 2 +- pyomo/core/tests/unit/test_concrete.py | 2 +- pyomo/core/tests/unit/test_connector.py | 2 +- pyomo/core/tests/unit/test_deprecation.py | 2 +- pyomo/core/tests/unit/test_derivs.py | 2 +- pyomo/core/tests/unit/test_dict_objects.py | 2 +- pyomo/core/tests/unit/test_disable_methods.py | 2 +- pyomo/core/tests/unit/test_enums.py | 2 +- pyomo/core/tests/unit/test_expr_misc.py | 2 +- pyomo/core/tests/unit/test_expression.py | 2 +- pyomo/core/tests/unit/test_external.py | 2 +- pyomo/core/tests/unit/test_indexed.py | 2 +- pyomo/core/tests/unit/test_indexed_slice.py | 2 +- pyomo/core/tests/unit/test_initializer.py | 2 +- pyomo/core/tests/unit/test_kernel_register_numpy_types.py | 2 +- pyomo/core/tests/unit/test_labelers.py | 2 +- pyomo/core/tests/unit/test_list_objects.py | 2 +- pyomo/core/tests/unit/test_logical_constraint.py | 2 +- pyomo/core/tests/unit/test_logical_expr_expanded.py | 2 +- pyomo/core/tests/unit/test_logical_to_linear.py | 2 +- pyomo/core/tests/unit/test_matrix_constraint.py | 2 +- pyomo/core/tests/unit/test_misc.py | 2 +- pyomo/core/tests/unit/test_model.py | 2 +- pyomo/core/tests/unit/test_mutable.py | 2 +- pyomo/core/tests/unit/test_numeric_expr.py | 2 +- pyomo/core/tests/unit/test_numeric_expr_api.py | 2 +- pyomo/core/tests/unit/test_numeric_expr_dispatcher.py | 2 +- pyomo/core/tests/unit/test_numeric_expr_zerofilter.py | 2 +- pyomo/core/tests/unit/test_numpy_expr.py | 2 +- pyomo/core/tests/unit/test_numvalue.py | 2 +- pyomo/core/tests/unit/test_obj.py | 2 +- pyomo/core/tests/unit/test_param.py | 2 +- pyomo/core/tests/unit/test_pickle.py | 2 +- pyomo/core/tests/unit/test_piecewise.py | 2 +- pyomo/core/tests/unit/test_preprocess.py | 2 +- pyomo/core/tests/unit/test_range.py | 2 +- pyomo/core/tests/unit/test_reference.py | 2 +- pyomo/core/tests/unit/test_relational_expr.py | 2 +- pyomo/core/tests/unit/test_set.py | 2 +- pyomo/core/tests/unit/test_sets.py | 2 +- pyomo/core/tests/unit/test_smap.py | 2 +- pyomo/core/tests/unit/test_sos.py | 2 +- pyomo/core/tests/unit/test_sos_v2.py | 2 +- pyomo/core/tests/unit/test_suffix.py | 2 +- pyomo/core/tests/unit/test_symbol_map.py | 2 +- pyomo/core/tests/unit/test_symbolic.py | 2 +- pyomo/core/tests/unit/test_taylor_series.py | 2 +- pyomo/core/tests/unit/test_template_expr.py | 2 +- pyomo/core/tests/unit/test_units.py | 2 +- pyomo/core/tests/unit/test_var.py | 2 +- pyomo/core/tests/unit/test_var_set_bounds.py | 2 +- pyomo/core/tests/unit/test_visitor.py | 2 +- pyomo/core/tests/unit/test_xfrm_discrete_vars.py | 2 +- pyomo/core/tests/unit/uninstantiated_model_linear.py | 2 +- pyomo/core/tests/unit/uninstantiated_model_quadratic.py | 2 +- pyomo/core/util.py | 2 +- pyomo/dae/__init__.py | 2 +- pyomo/dae/contset.py | 2 +- pyomo/dae/diffvar.py | 2 +- pyomo/dae/flatten.py | 2 +- pyomo/dae/initialization.py | 2 +- pyomo/dae/integral.py | 2 +- pyomo/dae/misc.py | 2 +- pyomo/dae/plugins/__init__.py | 2 +- pyomo/dae/plugins/colloc.py | 2 +- pyomo/dae/plugins/finitedifference.py | 2 +- pyomo/dae/set_utils.py | 2 +- pyomo/dae/simulator.py | 2 +- pyomo/dae/tests/__init__.py | 2 +- pyomo/dae/tests/test_colloc.py | 2 +- pyomo/dae/tests/test_contset.py | 2 +- pyomo/dae/tests/test_diffvar.py | 2 +- pyomo/dae/tests/test_finite_diff.py | 2 +- pyomo/dae/tests/test_flatten.py | 2 +- pyomo/dae/tests/test_initialization.py | 2 +- pyomo/dae/tests/test_integral.py | 2 +- pyomo/dae/tests/test_misc.py | 2 +- pyomo/dae/tests/test_set_utils.py | 2 +- pyomo/dae/tests/test_simulator.py | 2 +- pyomo/dae/utilities.py | 2 +- pyomo/dataportal/DataPortal.py | 2 +- pyomo/dataportal/TableData.py | 2 +- pyomo/dataportal/__init__.py | 2 +- pyomo/dataportal/factory.py | 2 +- pyomo/dataportal/parse_datacmds.py | 2 +- pyomo/dataportal/plugins/__init__.py | 2 +- pyomo/dataportal/plugins/csv_table.py | 2 +- pyomo/dataportal/plugins/datacommands.py | 2 +- pyomo/dataportal/plugins/db_table.py | 2 +- pyomo/dataportal/plugins/json_dict.py | 2 +- pyomo/dataportal/plugins/sheet.py | 2 +- pyomo/dataportal/plugins/text.py | 2 +- pyomo/dataportal/plugins/xml_table.py | 2 +- pyomo/dataportal/process_data.py | 2 +- pyomo/dataportal/tests/__init__.py | 2 +- pyomo/dataportal/tests/test_dat_parser.py | 2 +- pyomo/dataportal/tests/test_dataportal.py | 2 +- pyomo/duality/__init__.py | 2 +- pyomo/duality/collect.py | 2 +- pyomo/duality/lagrangian_dual.py | 2 +- pyomo/duality/plugins.py | 2 +- pyomo/duality/tests/__init__.py | 2 +- pyomo/duality/tests/test_linear_dual.py | 2 +- pyomo/environ/__init__.py | 2 +- pyomo/environ/tests/__init__.py | 2 +- pyomo/environ/tests/standalone_minimal_pyomo_driver.py | 2 +- pyomo/environ/tests/test_environ.py | 2 +- pyomo/environ/tests/test_package_layout.py | 2 +- pyomo/gdp/__init__.py | 2 +- pyomo/gdp/basic_step.py | 2 +- pyomo/gdp/disjunct.py | 2 +- pyomo/gdp/plugins/__init__.py | 2 +- pyomo/gdp/plugins/between_steps.py | 2 +- pyomo/gdp/plugins/bigm.py | 2 +- pyomo/gdp/plugins/bigm_mixin.py | 2 +- pyomo/gdp/plugins/bilinear.py | 2 +- pyomo/gdp/plugins/binary_multiplication.py | 2 +- pyomo/gdp/plugins/bound_pretransformation.py | 2 +- pyomo/gdp/plugins/chull.py | 2 +- pyomo/gdp/plugins/cuttingplane.py | 2 +- pyomo/gdp/plugins/fix_disjuncts.py | 2 +- pyomo/gdp/plugins/gdp_to_mip_transformation.py | 2 +- pyomo/gdp/plugins/gdp_var_mover.py | 2 +- pyomo/gdp/plugins/hull.py | 2 +- pyomo/gdp/plugins/multiple_bigm.py | 2 +- pyomo/gdp/plugins/partition_disjuncts.py | 2 +- pyomo/gdp/plugins/transform_current_disjunctive_state.py | 2 +- pyomo/gdp/tests/__init__.py | 2 +- pyomo/gdp/tests/common_tests.py | 2 +- pyomo/gdp/tests/models.py | 2 +- pyomo/gdp/tests/test_basic_step.py | 2 +- pyomo/gdp/tests/test_bigm.py | 2 +- pyomo/gdp/tests/test_binary_multiplication.py | 2 +- pyomo/gdp/tests/test_bound_pretransformation.py | 2 +- pyomo/gdp/tests/test_cuttingplane.py | 2 +- pyomo/gdp/tests/test_disjunct.py | 2 +- pyomo/gdp/tests/test_fix_disjuncts.py | 2 +- pyomo/gdp/tests/test_gdp.py | 2 +- pyomo/gdp/tests/test_gdp_reclassification_error.py | 2 +- pyomo/gdp/tests/test_hull.py | 2 +- pyomo/gdp/tests/test_mbigm.py | 2 +- pyomo/gdp/tests/test_partition_disjuncts.py | 2 +- pyomo/gdp/tests/test_reclassify.py | 2 +- pyomo/gdp/tests/test_transform_current_disjunctive_state.py | 2 +- pyomo/gdp/tests/test_util.py | 2 +- pyomo/gdp/transformed_disjunct.py | 2 +- pyomo/gdp/util.py | 2 +- pyomo/kernel/__init__.py | 2 +- pyomo/kernel/util.py | 2 +- pyomo/mpec/__init__.py | 2 +- pyomo/mpec/complementarity.py | 2 +- pyomo/mpec/plugins/__init__.py | 2 +- pyomo/mpec/plugins/mpec1.py | 2 +- pyomo/mpec/plugins/mpec2.py | 2 +- pyomo/mpec/plugins/mpec3.py | 2 +- pyomo/mpec/plugins/mpec4.py | 2 +- pyomo/mpec/plugins/pathampl.py | 2 +- pyomo/mpec/plugins/solver1.py | 2 +- pyomo/mpec/plugins/solver2.py | 2 +- pyomo/mpec/tests/__init__.py | 2 +- pyomo/mpec/tests/test_complementarity.py | 2 +- pyomo/mpec/tests/test_minlp.py | 2 +- pyomo/mpec/tests/test_nlp.py | 2 +- pyomo/mpec/tests/test_path.py | 2 +- pyomo/neos/__init__.py | 2 +- pyomo/neos/kestrel.py | 2 +- pyomo/neos/plugins/NEOS.py | 2 +- pyomo/neos/plugins/__init__.py | 2 +- pyomo/neos/plugins/kestrel_plugin.py | 2 +- pyomo/neos/tests/__init__.py | 2 +- pyomo/neos/tests/model_min_lp.py | 2 +- pyomo/neos/tests/test_neos.py | 2 +- pyomo/network/__init__.py | 2 +- pyomo/network/arc.py | 2 +- pyomo/network/decomposition.py | 2 +- pyomo/network/foqus_graph.py | 2 +- pyomo/network/plugins/__init__.py | 2 +- pyomo/network/plugins/expand_arcs.py | 2 +- pyomo/network/port.py | 2 +- pyomo/network/tests/__init__.py | 2 +- pyomo/network/tests/test_arc.py | 2 +- pyomo/network/tests/test_decomposition.py | 2 +- pyomo/network/tests/test_port.py | 2 +- pyomo/network/util.py | 2 +- pyomo/opt/__init__.py | 2 +- pyomo/opt/base/__init__.py | 2 +- pyomo/opt/base/convert.py | 2 +- pyomo/opt/base/error.py | 2 +- pyomo/opt/base/formats.py | 2 +- pyomo/opt/base/opt_config.py | 2 +- pyomo/opt/base/problem.py | 2 +- pyomo/opt/base/results.py | 2 +- pyomo/opt/base/solvers.py | 2 +- pyomo/opt/parallel/__init__.py | 2 +- pyomo/opt/parallel/async_solver.py | 2 +- pyomo/opt/parallel/local.py | 2 +- pyomo/opt/parallel/manager.py | 2 +- pyomo/opt/plugins/__init__.py | 2 +- pyomo/opt/plugins/driver.py | 2 +- pyomo/opt/plugins/res.py | 2 +- pyomo/opt/plugins/sol.py | 2 +- pyomo/opt/problem/__init__.py | 2 +- pyomo/opt/problem/ampl.py | 2 +- pyomo/opt/results/__init__.py | 2 +- pyomo/opt/results/container.py | 2 +- pyomo/opt/results/problem.py | 2 +- pyomo/opt/results/results_.py | 2 +- pyomo/opt/results/solution.py | 2 +- pyomo/opt/results/solver.py | 2 +- pyomo/opt/solver/__init__.py | 2 +- pyomo/opt/solver/ilmcmd.py | 2 +- pyomo/opt/solver/shellcmd.py | 2 +- pyomo/opt/testing/__init__.py | 2 +- pyomo/opt/testing/pyunit.py | 2 +- pyomo/opt/tests/__init__.py | 2 +- pyomo/opt/tests/base/__init__.py | 2 +- pyomo/opt/tests/base/test_ampl.py | 2 +- pyomo/opt/tests/base/test_convert.py | 2 +- pyomo/opt/tests/base/test_factory.py | 2 +- pyomo/opt/tests/base/test_sol.py | 2 +- pyomo/opt/tests/base/test_soln.py | 2 +- pyomo/opt/tests/base/test_solver.py | 2 +- pyomo/opt/tests/solver/__init__.py | 2 +- pyomo/opt/tests/solver/test_shellcmd.py | 2 +- pyomo/pysp/__init__.py | 2 +- pyomo/repn/__init__.py | 2 +- pyomo/repn/beta/__init__.py | 2 +- pyomo/repn/beta/matrix.py | 2 +- pyomo/repn/linear.py | 2 +- pyomo/repn/plugins/__init__.py | 2 +- pyomo/repn/plugins/ampl/__init__.py | 2 +- pyomo/repn/plugins/ampl/ampl_.py | 2 +- pyomo/repn/plugins/baron_writer.py | 2 +- pyomo/repn/plugins/cpxlp.py | 2 +- pyomo/repn/plugins/gams_writer.py | 2 +- pyomo/repn/plugins/lp_writer.py | 2 +- pyomo/repn/plugins/mps.py | 2 +- pyomo/repn/plugins/nl_writer.py | 2 +- pyomo/repn/plugins/standard_form.py | 2 +- pyomo/repn/quadratic.py | 2 +- pyomo/repn/standard_aux.py | 2 +- pyomo/repn/standard_repn.py | 2 +- pyomo/repn/tests/__init__.py | 2 +- pyomo/repn/tests/ampl/__init__.py | 2 +- pyomo/repn/tests/ampl/helper.py | 2 +- pyomo/repn/tests/ampl/nl_diff.py | 2 +- pyomo/repn/tests/ampl/small10_testCase.py | 2 +- pyomo/repn/tests/ampl/small11_testCase.py | 2 +- pyomo/repn/tests/ampl/small12_testCase.py | 2 +- pyomo/repn/tests/ampl/small13_testCase.py | 2 +- pyomo/repn/tests/ampl/small14_testCase.py | 2 +- pyomo/repn/tests/ampl/small15_testCase.py | 2 +- pyomo/repn/tests/ampl/small1_testCase.py | 2 +- pyomo/repn/tests/ampl/small2_testCase.py | 2 +- pyomo/repn/tests/ampl/small3_testCase.py | 2 +- pyomo/repn/tests/ampl/small4_testCase.py | 2 +- pyomo/repn/tests/ampl/small5_testCase.py | 2 +- pyomo/repn/tests/ampl/small6_testCase.py | 2 +- pyomo/repn/tests/ampl/small7_testCase.py | 2 +- pyomo/repn/tests/ampl/small8_testCase.py | 2 +- pyomo/repn/tests/ampl/small9_testCase.py | 2 +- pyomo/repn/tests/ampl/test_ampl_comparison.py | 2 +- pyomo/repn/tests/ampl/test_ampl_nl.py | 2 +- pyomo/repn/tests/ampl/test_ampl_repn.py | 2 +- pyomo/repn/tests/ampl/test_nlv2.py | 2 +- pyomo/repn/tests/ampl/test_suffixes.py | 2 +- pyomo/repn/tests/baron/__init__.py | 2 +- pyomo/repn/tests/baron/small14a_testCase.py | 2 +- pyomo/repn/tests/baron/test_baron.py | 2 +- pyomo/repn/tests/baron/test_baron_comparison.py | 2 +- pyomo/repn/tests/cpxlp/__init__.py | 2 +- pyomo/repn/tests/cpxlp/test_cpxlp.py | 2 +- pyomo/repn/tests/cpxlp/test_lpv2.py | 2 +- pyomo/repn/tests/diffutils.py | 2 +- pyomo/repn/tests/gams/__init__.py | 2 +- pyomo/repn/tests/gams/small14a_testCase.py | 2 +- pyomo/repn/tests/gams/test_gams.py | 2 +- pyomo/repn/tests/gams/test_gams_comparison.py | 2 +- pyomo/repn/tests/lp_diff.py | 2 +- pyomo/repn/tests/mps/__init__.py | 2 +- pyomo/repn/tests/mps/test_mps.py | 2 +- pyomo/repn/tests/nl_diff.py | 2 +- pyomo/repn/tests/test_linear.py | 2 +- pyomo/repn/tests/test_quadratic.py | 2 +- pyomo/repn/tests/test_standard.py | 2 +- pyomo/repn/tests/test_standard_form.py | 2 +- pyomo/repn/tests/test_util.py | 2 +- pyomo/repn/util.py | 2 +- pyomo/scripting/__init__.py | 2 +- pyomo/scripting/commands.py | 2 +- pyomo/scripting/convert.py | 2 +- pyomo/scripting/driver_help.py | 2 +- pyomo/scripting/interface.py | 2 +- pyomo/scripting/plugins/__init__.py | 2 +- pyomo/scripting/plugins/build_ext.py | 2 +- pyomo/scripting/plugins/convert.py | 2 +- pyomo/scripting/plugins/download.py | 2 +- pyomo/scripting/plugins/extras.py | 2 +- pyomo/scripting/plugins/solve.py | 2 +- pyomo/scripting/pyomo_command.py | 2 +- pyomo/scripting/pyomo_main.py | 2 +- pyomo/scripting/pyomo_parser.py | 2 +- pyomo/scripting/solve_config.py | 2 +- pyomo/scripting/tests/__init__.py | 2 +- pyomo/scripting/tests/test_cmds.py | 2 +- pyomo/scripting/util.py | 2 +- pyomo/solvers/__init__.py | 2 +- pyomo/solvers/mockmip.py | 2 +- pyomo/solvers/plugins/__init__.py | 2 +- pyomo/solvers/plugins/converter/__init__.py | 2 +- pyomo/solvers/plugins/converter/ampl.py | 2 +- pyomo/solvers/plugins/converter/glpsol.py | 2 +- pyomo/solvers/plugins/converter/model.py | 2 +- pyomo/solvers/plugins/converter/pico.py | 2 +- pyomo/solvers/plugins/solvers/ASL.py | 2 +- pyomo/solvers/plugins/solvers/BARON.py | 2 +- pyomo/solvers/plugins/solvers/CBCplugin.py | 2 +- pyomo/solvers/plugins/solvers/CONOPT.py | 2 +- pyomo/solvers/plugins/solvers/CPLEX.py | 2 +- pyomo/solvers/plugins/solvers/GAMS.py | 2 +- pyomo/solvers/plugins/solvers/GLPK.py | 2 +- pyomo/solvers/plugins/solvers/GUROBI.py | 2 +- pyomo/solvers/plugins/solvers/GUROBI_RUN.py | 2 +- pyomo/solvers/plugins/solvers/IPOPT.py | 2 +- pyomo/solvers/plugins/solvers/SCIPAMPL.py | 2 +- pyomo/solvers/plugins/solvers/XPRESS.py | 2 +- pyomo/solvers/plugins/solvers/__init__.py | 2 +- pyomo/solvers/plugins/solvers/cplex_direct.py | 2 +- pyomo/solvers/plugins/solvers/cplex_persistent.py | 2 +- pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py | 2 +- pyomo/solvers/plugins/solvers/direct_solver.py | 2 +- pyomo/solvers/plugins/solvers/gurobi_direct.py | 2 +- pyomo/solvers/plugins/solvers/gurobi_persistent.py | 2 +- pyomo/solvers/plugins/solvers/mosek_direct.py | 2 +- pyomo/solvers/plugins/solvers/mosek_persistent.py | 2 +- pyomo/solvers/plugins/solvers/persistent_solver.py | 2 +- pyomo/solvers/plugins/solvers/pywrapper.py | 2 +- pyomo/solvers/plugins/solvers/xpress_direct.py | 2 +- pyomo/solvers/plugins/solvers/xpress_persistent.py | 2 +- pyomo/solvers/tests/__init__.py | 2 +- pyomo/solvers/tests/checks/__init__.py | 2 +- pyomo/solvers/tests/checks/test_BARON.py | 2 +- pyomo/solvers/tests/checks/test_CBCplugin.py | 2 +- pyomo/solvers/tests/checks/test_CPLEXDirect.py | 2 +- pyomo/solvers/tests/checks/test_CPLEXPersistent.py | 2 +- pyomo/solvers/tests/checks/test_GAMS.py | 2 +- pyomo/solvers/tests/checks/test_MOSEKDirect.py | 2 +- pyomo/solvers/tests/checks/test_MOSEKPersistent.py | 2 +- pyomo/solvers/tests/checks/test_cbc.py | 2 +- pyomo/solvers/tests/checks/test_cplex.py | 2 +- pyomo/solvers/tests/checks/test_gurobi.py | 2 +- pyomo/solvers/tests/checks/test_gurobi_direct.py | 2 +- pyomo/solvers/tests/checks/test_gurobi_persistent.py | 2 +- pyomo/solvers/tests/checks/test_no_solution_behavior.py | 2 +- pyomo/solvers/tests/checks/test_pickle.py | 2 +- pyomo/solvers/tests/checks/test_writers.py | 2 +- pyomo/solvers/tests/checks/test_xpress_persistent.py | 2 +- pyomo/solvers/tests/mip/__init__.py | 2 +- pyomo/solvers/tests/mip/model.py | 2 +- pyomo/solvers/tests/mip/test_asl.py | 2 +- pyomo/solvers/tests/mip/test_convert.py | 2 +- pyomo/solvers/tests/mip/test_factory.py | 2 +- pyomo/solvers/tests/mip/test_ipopt.py | 2 +- pyomo/solvers/tests/mip/test_mip.py | 2 +- pyomo/solvers/tests/mip/test_qp.py | 2 +- pyomo/solvers/tests/mip/test_scip.py | 2 +- pyomo/solvers/tests/mip/test_scip_log_data.py | 2 +- pyomo/solvers/tests/mip/test_scip_version.py | 2 +- pyomo/solvers/tests/mip/test_solver.py | 2 +- pyomo/solvers/tests/models/LP_block.py | 2 +- pyomo/solvers/tests/models/LP_compiled.py | 2 +- pyomo/solvers/tests/models/LP_constant_objective1.py | 2 +- pyomo/solvers/tests/models/LP_constant_objective2.py | 2 +- pyomo/solvers/tests/models/LP_duals_maximize.py | 2 +- pyomo/solvers/tests/models/LP_duals_minimize.py | 2 +- pyomo/solvers/tests/models/LP_inactive_index.py | 2 +- pyomo/solvers/tests/models/LP_infeasible1.py | 2 +- pyomo/solvers/tests/models/LP_infeasible2.py | 2 +- pyomo/solvers/tests/models/LP_piecewise.py | 2 +- pyomo/solvers/tests/models/LP_simple.py | 2 +- pyomo/solvers/tests/models/LP_trivial_constraints.py | 2 +- pyomo/solvers/tests/models/LP_unbounded.py | 2 +- pyomo/solvers/tests/models/LP_unique_duals.py | 2 +- pyomo/solvers/tests/models/LP_unused_vars.py | 2 +- pyomo/solvers/tests/models/MILP_discrete_var_bounds.py | 2 +- pyomo/solvers/tests/models/MILP_infeasible1.py | 2 +- pyomo/solvers/tests/models/MILP_simple.py | 2 +- pyomo/solvers/tests/models/MILP_unbounded.py | 2 +- pyomo/solvers/tests/models/MILP_unused_vars.py | 2 +- pyomo/solvers/tests/models/MIQCP_simple.py | 2 +- pyomo/solvers/tests/models/MIQP_simple.py | 2 +- pyomo/solvers/tests/models/QCP_simple.py | 2 +- pyomo/solvers/tests/models/QP_constant_objective.py | 2 +- pyomo/solvers/tests/models/QP_simple.py | 2 +- pyomo/solvers/tests/models/SOS1_simple.py | 2 +- pyomo/solvers/tests/models/SOS2_simple.py | 2 +- pyomo/solvers/tests/models/__init__.py | 2 +- pyomo/solvers/tests/models/base.py | 2 +- pyomo/solvers/tests/piecewise_linear/__init__.py | 2 +- .../tests/piecewise_linear/kernel_problems/concave_var.py | 2 +- .../tests/piecewise_linear/kernel_problems/convex_var.py | 2 +- .../tests/piecewise_linear/kernel_problems/piecewise_var.py | 2 +- .../tests/piecewise_linear/kernel_problems/step_var.py | 2 +- .../piecewise_linear/problems/concave_multi_vararray1.py | 2 +- .../piecewise_linear/problems/concave_multi_vararray2.py | 2 +- pyomo/solvers/tests/piecewise_linear/problems/concave_var.py | 2 +- .../tests/piecewise_linear/problems/concave_vararray.py | 2 +- .../tests/piecewise_linear/problems/convex_multi_vararray1.py | 2 +- .../tests/piecewise_linear/problems/convex_multi_vararray2.py | 2 +- pyomo/solvers/tests/piecewise_linear/problems/convex_var.py | 2 +- .../tests/piecewise_linear/problems/convex_vararray.py | 2 +- .../piecewise_linear/problems/piecewise_multi_vararray.py | 2 +- .../solvers/tests/piecewise_linear/problems/piecewise_var.py | 2 +- .../tests/piecewise_linear/problems/piecewise_vararray.py | 2 +- pyomo/solvers/tests/piecewise_linear/problems/step_var.py | 2 +- .../solvers/tests/piecewise_linear/problems/step_vararray.py | 2 +- pyomo/solvers/tests/piecewise_linear/problems/tester.py | 2 +- pyomo/solvers/tests/piecewise_linear/test_examples.py | 2 +- pyomo/solvers/tests/piecewise_linear/test_piecewise_linear.py | 2 +- .../tests/piecewise_linear/test_piecewise_linear_kernel.py | 2 +- pyomo/solvers/tests/solvers.py | 2 +- pyomo/solvers/tests/testcases.py | 2 +- pyomo/solvers/wrappers.py | 2 +- pyomo/util/__init__.py | 2 +- pyomo/util/blockutil.py | 2 +- pyomo/util/calc_var_value.py | 2 +- pyomo/util/check_units.py | 2 +- pyomo/util/components.py | 2 +- pyomo/util/diagnostics.py | 2 +- pyomo/util/infeasible.py | 2 +- pyomo/util/model_size.py | 2 +- pyomo/util/report_scaling.py | 2 +- pyomo/util/slices.py | 2 +- pyomo/util/subsystems.py | 2 +- pyomo/util/tests/__init__.py | 2 +- pyomo/util/tests/test_blockutil.py | 2 +- pyomo/util/tests/test_calc_var_value.py | 2 +- pyomo/util/tests/test_check_units.py | 2 +- pyomo/util/tests/test_components.py | 2 +- pyomo/util/tests/test_infeasible.py | 2 +- pyomo/util/tests/test_model_size.py | 2 +- pyomo/util/tests/test_report_scaling.py | 2 +- pyomo/util/tests/test_slices.py | 2 +- pyomo/util/tests/test_subsystems.py | 2 +- pyomo/util/vars_from_expressions.py | 2 +- pyomo/version/__init__.py | 2 +- pyomo/version/info.py | 2 +- pyomo/version/tests/__init__.py | 2 +- pyomo/version/tests/check.py | 2 +- pyomo/version/tests/test_version.py | 2 +- scripts/admin/contributors.py | 2 +- scripts/get_pyomo.py | 2 +- scripts/get_pyomo_extras.py | 2 +- scripts/performance/compare.py | 2 +- scripts/performance/compare_components.py | 2 +- scripts/performance/expr_perf.py | 2 +- scripts/performance/main.py | 2 +- scripts/performance/simple.py | 2 +- setup.py | 2 +- 1664 files changed, 1672 insertions(+), 1672 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 192d315e4b5..9fd5d9b810c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,7 +1,7 @@ LICENSE ======= -Copyright (c) 2008-2022 National Technology and Engineering Solutions of +Copyright (c) 2008-2024 National Technology and Engineering Solutions of Sandia, LLC . Under the terms of Contract DE-NA0003525 with National Technology and Engineering Solutions of Sandia, LLC , the U.S. Government retains certain rights in this software. diff --git a/conftest.py b/conftest.py index df5b0f31e59..7faad6fc89b 100644 --- a/conftest.py +++ b/conftest.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 24f8d26c9e8..89c346f5abc 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/library_reference/kernel/examples/aml_example.py b/doc/OnlineDocs/library_reference/kernel/examples/aml_example.py index 564764071b7..a640b94cc76 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/aml_example.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/aml_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/library_reference/kernel/examples/conic.py b/doc/OnlineDocs/library_reference/kernel/examples/conic.py index 866377ed641..0418d188722 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/conic.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/conic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_containers.py b/doc/OnlineDocs/library_reference/kernel/examples/kernel_containers.py index 9b33ed71e1d..1931c6d9b56 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/kernel_containers.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/kernel_containers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_example.py b/doc/OnlineDocs/library_reference/kernel/examples/kernel_example.py index 6ee766e3055..1f80bce9788 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/kernel_example.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/kernel_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_solving.py b/doc/OnlineDocs/library_reference/kernel/examples/kernel_solving.py index 30b588f89b8..13d7efc052a 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/kernel_solving.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/kernel_solving.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_subclassing.py b/doc/OnlineDocs/library_reference/kernel/examples/kernel_subclassing.py index a603050e828..d6e38f6b0e0 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/kernel_subclassing.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/kernel_subclassing.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/library_reference/kernel/examples/transformer.py b/doc/OnlineDocs/library_reference/kernel/examples/transformer.py index 0df239c61ad..43a1d0675bf 100644 --- a/doc/OnlineDocs/library_reference/kernel/examples/transformer.py +++ b/doc/OnlineDocs/library_reference/kernel/examples/transformer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/modeling_extensions/__init__.py b/doc/OnlineDocs/modeling_extensions/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/doc/OnlineDocs/modeling_extensions/__init__.py +++ b/doc/OnlineDocs/modeling_extensions/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/ABCD1.py b/doc/OnlineDocs/src/data/ABCD1.py index 6d34bec756e..aa2f46e71fa 100644 --- a/doc/OnlineDocs/src/data/ABCD1.py +++ b/doc/OnlineDocs/src/data/ABCD1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/ABCD2.py b/doc/OnlineDocs/src/data/ABCD2.py index beadd71916d..ec0e7ccb15c 100644 --- a/doc/OnlineDocs/src/data/ABCD2.py +++ b/doc/OnlineDocs/src/data/ABCD2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/ABCD3.py b/doc/OnlineDocs/src/data/ABCD3.py index 1a3e826c6a9..ba55fd970cc 100644 --- a/doc/OnlineDocs/src/data/ABCD3.py +++ b/doc/OnlineDocs/src/data/ABCD3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/ABCD4.py b/doc/OnlineDocs/src/data/ABCD4.py index 59055cadf71..2fb397aa3b0 100644 --- a/doc/OnlineDocs/src/data/ABCD4.py +++ b/doc/OnlineDocs/src/data/ABCD4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/ABCD5.py b/doc/OnlineDocs/src/data/ABCD5.py index 051e2c9ba9e..abc03505e96 100644 --- a/doc/OnlineDocs/src/data/ABCD5.py +++ b/doc/OnlineDocs/src/data/ABCD5.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/ABCD6.py b/doc/OnlineDocs/src/data/ABCD6.py index 8c239eb5332..59e0e8e98ae 100644 --- a/doc/OnlineDocs/src/data/ABCD6.py +++ b/doc/OnlineDocs/src/data/ABCD6.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/ABCD7.py b/doc/OnlineDocs/src/data/ABCD7.py index a52c27af234..1bfb4d1e3fb 100644 --- a/doc/OnlineDocs/src/data/ABCD7.py +++ b/doc/OnlineDocs/src/data/ABCD7.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/ABCD8.py b/doc/OnlineDocs/src/data/ABCD8.py index f979f373a18..aa1ba0b4cf5 100644 --- a/doc/OnlineDocs/src/data/ABCD8.py +++ b/doc/OnlineDocs/src/data/ABCD8.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/ABCD9.py b/doc/OnlineDocs/src/data/ABCD9.py index aaa1e7c908d..194c71486d9 100644 --- a/doc/OnlineDocs/src/data/ABCD9.py +++ b/doc/OnlineDocs/src/data/ABCD9.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/diet1.py b/doc/OnlineDocs/src/data/diet1.py index 9201edb8c4c..40582e16ba0 100644 --- a/doc/OnlineDocs/src/data/diet1.py +++ b/doc/OnlineDocs/src/data/diet1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/ex.py b/doc/OnlineDocs/src/data/ex.py index 3fd91b623b2..a66ee30b494 100644 --- a/doc/OnlineDocs/src/data/ex.py +++ b/doc/OnlineDocs/src/data/ex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/import1.tab.py b/doc/OnlineDocs/src/data/import1.tab.py index ade8edcc2a3..e160e4fdcde 100644 --- a/doc/OnlineDocs/src/data/import1.tab.py +++ b/doc/OnlineDocs/src/data/import1.tab.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/import2.tab.py b/doc/OnlineDocs/src/data/import2.tab.py index 6491c1ec30e..54339551279 100644 --- a/doc/OnlineDocs/src/data/import2.tab.py +++ b/doc/OnlineDocs/src/data/import2.tab.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/import3.tab.py b/doc/OnlineDocs/src/data/import3.tab.py index ec57c018b00..664151d1438 100644 --- a/doc/OnlineDocs/src/data/import3.tab.py +++ b/doc/OnlineDocs/src/data/import3.tab.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/import4.tab.py b/doc/OnlineDocs/src/data/import4.tab.py index b48278bd28d..91dd3f26a42 100644 --- a/doc/OnlineDocs/src/data/import4.tab.py +++ b/doc/OnlineDocs/src/data/import4.tab.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/import5.tab.py b/doc/OnlineDocs/src/data/import5.tab.py index 9604c328c64..263677c308c 100644 --- a/doc/OnlineDocs/src/data/import5.tab.py +++ b/doc/OnlineDocs/src/data/import5.tab.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/import6.tab.py b/doc/OnlineDocs/src/data/import6.tab.py index a1c269a0abf..8f4824ad3fe 100644 --- a/doc/OnlineDocs/src/data/import6.tab.py +++ b/doc/OnlineDocs/src/data/import6.tab.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/import7.tab.py b/doc/OnlineDocs/src/data/import7.tab.py index f4b60cb42d9..503f9224323 100644 --- a/doc/OnlineDocs/src/data/import7.tab.py +++ b/doc/OnlineDocs/src/data/import7.tab.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/import8.tab.py b/doc/OnlineDocs/src/data/import8.tab.py index d1d2e6f8160..02b8724fe45 100644 --- a/doc/OnlineDocs/src/data/import8.tab.py +++ b/doc/OnlineDocs/src/data/import8.tab.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/param1.py b/doc/OnlineDocs/src/data/param1.py index e606b7f6b4f..336a04287b9 100644 --- a/doc/OnlineDocs/src/data/param1.py +++ b/doc/OnlineDocs/src/data/param1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/param2.py b/doc/OnlineDocs/src/data/param2.py index 725a6002ede..a7d0feafff9 100644 --- a/doc/OnlineDocs/src/data/param2.py +++ b/doc/OnlineDocs/src/data/param2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/param2a.py b/doc/OnlineDocs/src/data/param2a.py index 29416e2dcbc..42056793ffd 100644 --- a/doc/OnlineDocs/src/data/param2a.py +++ b/doc/OnlineDocs/src/data/param2a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/param3.py b/doc/OnlineDocs/src/data/param3.py index 0cc4df57511..952f9a9b707 100644 --- a/doc/OnlineDocs/src/data/param3.py +++ b/doc/OnlineDocs/src/data/param3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/param3a.py b/doc/OnlineDocs/src/data/param3a.py index 42204de468f..028e1d07296 100644 --- a/doc/OnlineDocs/src/data/param3a.py +++ b/doc/OnlineDocs/src/data/param3a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/param3b.py b/doc/OnlineDocs/src/data/param3b.py index 9f0375d7b87..97f8598610a 100644 --- a/doc/OnlineDocs/src/data/param3b.py +++ b/doc/OnlineDocs/src/data/param3b.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/param3c.py b/doc/OnlineDocs/src/data/param3c.py index 9efac11553e..582b0f7db75 100644 --- a/doc/OnlineDocs/src/data/param3c.py +++ b/doc/OnlineDocs/src/data/param3c.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/param4.py b/doc/OnlineDocs/src/data/param4.py index ab184e65ed3..010c46fc9c5 100644 --- a/doc/OnlineDocs/src/data/param4.py +++ b/doc/OnlineDocs/src/data/param4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/param5.py b/doc/OnlineDocs/src/data/param5.py index f842e48995a..2db07f3f990 100644 --- a/doc/OnlineDocs/src/data/param5.py +++ b/doc/OnlineDocs/src/data/param5.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/param5a.py b/doc/OnlineDocs/src/data/param5a.py index f65de59ca78..32a53d24e9b 100644 --- a/doc/OnlineDocs/src/data/param5a.py +++ b/doc/OnlineDocs/src/data/param5a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/param6.py b/doc/OnlineDocs/src/data/param6.py index 54cb350298b..e3364a933cf 100644 --- a/doc/OnlineDocs/src/data/param6.py +++ b/doc/OnlineDocs/src/data/param6.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/param6a.py b/doc/OnlineDocs/src/data/param6a.py index 7aabe7ec929..3d2fa645411 100644 --- a/doc/OnlineDocs/src/data/param6a.py +++ b/doc/OnlineDocs/src/data/param6a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/param7a.py b/doc/OnlineDocs/src/data/param7a.py index 8d0c49210b5..b3aba9ec23d 100644 --- a/doc/OnlineDocs/src/data/param7a.py +++ b/doc/OnlineDocs/src/data/param7a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/param7b.py b/doc/OnlineDocs/src/data/param7b.py index 8481083c31c..8b022f399a8 100644 --- a/doc/OnlineDocs/src/data/param7b.py +++ b/doc/OnlineDocs/src/data/param7b.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/param8a.py b/doc/OnlineDocs/src/data/param8a.py index 59ddba34091..abfa885ded4 100644 --- a/doc/OnlineDocs/src/data/param8a.py +++ b/doc/OnlineDocs/src/data/param8a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/set1.py b/doc/OnlineDocs/src/data/set1.py index e1d8a09c394..c84c1ef0819 100644 --- a/doc/OnlineDocs/src/data/set1.py +++ b/doc/OnlineDocs/src/data/set1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/set2.py b/doc/OnlineDocs/src/data/set2.py index 8e2f4b756d9..9048a49fecb 100644 --- a/doc/OnlineDocs/src/data/set2.py +++ b/doc/OnlineDocs/src/data/set2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/set2a.py b/doc/OnlineDocs/src/data/set2a.py index 6358178d109..f2fa4d71916 100644 --- a/doc/OnlineDocs/src/data/set2a.py +++ b/doc/OnlineDocs/src/data/set2a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/set3.py b/doc/OnlineDocs/src/data/set3.py index dced69c2375..9cdacbe39e0 100644 --- a/doc/OnlineDocs/src/data/set3.py +++ b/doc/OnlineDocs/src/data/set3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/set4.py b/doc/OnlineDocs/src/data/set4.py index 887d12ebf05..b3485638c6f 100644 --- a/doc/OnlineDocs/src/data/set4.py +++ b/doc/OnlineDocs/src/data/set4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/set5.py b/doc/OnlineDocs/src/data/set5.py index 8fb5ced0a3f..d745d8408d0 100644 --- a/doc/OnlineDocs/src/data/set5.py +++ b/doc/OnlineDocs/src/data/set5.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/table0.py b/doc/OnlineDocs/src/data/table0.py index 10ace6ea37b..de0fae0c861 100644 --- a/doc/OnlineDocs/src/data/table0.py +++ b/doc/OnlineDocs/src/data/table0.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/table0.ul.py b/doc/OnlineDocs/src/data/table0.ul.py index 79dc2933667..524c3756782 100644 --- a/doc/OnlineDocs/src/data/table0.ul.py +++ b/doc/OnlineDocs/src/data/table0.ul.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/table1.py b/doc/OnlineDocs/src/data/table1.py index d69c35b4860..f36714b8f1f 100644 --- a/doc/OnlineDocs/src/data/table1.py +++ b/doc/OnlineDocs/src/data/table1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/table2.py b/doc/OnlineDocs/src/data/table2.py index 5b4718f60ad..03648a00f8c 100644 --- a/doc/OnlineDocs/src/data/table2.py +++ b/doc/OnlineDocs/src/data/table2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/table3.py b/doc/OnlineDocs/src/data/table3.py index efa438eddd0..2c598f112df 100644 --- a/doc/OnlineDocs/src/data/table3.py +++ b/doc/OnlineDocs/src/data/table3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/table3.ul.py b/doc/OnlineDocs/src/data/table3.ul.py index ace661ac91c..18ced12b388 100644 --- a/doc/OnlineDocs/src/data/table3.ul.py +++ b/doc/OnlineDocs/src/data/table3.ul.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/table4.py b/doc/OnlineDocs/src/data/table4.py index b571d8ec1e4..bd20682b5a9 100644 --- a/doc/OnlineDocs/src/data/table4.py +++ b/doc/OnlineDocs/src/data/table4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/table4.ul.py b/doc/OnlineDocs/src/data/table4.ul.py index 1b4f192130b..9f16f21fe19 100644 --- a/doc/OnlineDocs/src/data/table4.ul.py +++ b/doc/OnlineDocs/src/data/table4.ul.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/table5.py b/doc/OnlineDocs/src/data/table5.py index 25269bb0bde..a3cb01209a2 100644 --- a/doc/OnlineDocs/src/data/table5.py +++ b/doc/OnlineDocs/src/data/table5.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/table6.py b/doc/OnlineDocs/src/data/table6.py index 1e2201cb6d6..1db0a764a23 100644 --- a/doc/OnlineDocs/src/data/table6.py +++ b/doc/OnlineDocs/src/data/table6.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/data/table7.py b/doc/OnlineDocs/src/data/table7.py index e5507c546ea..84a841aca86 100644 --- a/doc/OnlineDocs/src/data/table7.py +++ b/doc/OnlineDocs/src/data/table7.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/dataportal/PP_sqlite.py b/doc/OnlineDocs/src/dataportal/PP_sqlite.py index 9c6fc5ddc0b..1592e820900 100644 --- a/doc/OnlineDocs/src/dataportal/PP_sqlite.py +++ b/doc/OnlineDocs/src/dataportal/PP_sqlite.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/dataportal/dataportal_tab.py b/doc/OnlineDocs/src/dataportal/dataportal_tab.py index d6b679078e6..655329d31de 100644 --- a/doc/OnlineDocs/src/dataportal/dataportal_tab.py +++ b/doc/OnlineDocs/src/dataportal/dataportal_tab.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/dataportal/param_initialization.py b/doc/OnlineDocs/src/dataportal/param_initialization.py index 71c54b4a9d9..7f9270b5fda 100644 --- a/doc/OnlineDocs/src/dataportal/param_initialization.py +++ b/doc/OnlineDocs/src/dataportal/param_initialization.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/dataportal/set_initialization.py b/doc/OnlineDocs/src/dataportal/set_initialization.py index a086473fb1c..a5ab03894e3 100644 --- a/doc/OnlineDocs/src/dataportal/set_initialization.py +++ b/doc/OnlineDocs/src/dataportal/set_initialization.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/expr/design.py b/doc/OnlineDocs/src/expr/design.py index a5401a3c554..647a4537ca4 100644 --- a/doc/OnlineDocs/src/expr/design.py +++ b/doc/OnlineDocs/src/expr/design.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/expr/index.py b/doc/OnlineDocs/src/expr/index.py index 65291c0ff6f..fe5b03461c0 100644 --- a/doc/OnlineDocs/src/expr/index.py +++ b/doc/OnlineDocs/src/expr/index.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/expr/managing.py b/doc/OnlineDocs/src/expr/managing.py index 48bb005943e..00d521d16ab 100644 --- a/doc/OnlineDocs/src/expr/managing.py +++ b/doc/OnlineDocs/src/expr/managing.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/expr/overview.py b/doc/OnlineDocs/src/expr/overview.py index 32a5f569115..d33725edb88 100644 --- a/doc/OnlineDocs/src/expr/overview.py +++ b/doc/OnlineDocs/src/expr/overview.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/expr/performance.py b/doc/OnlineDocs/src/expr/performance.py index 59514718cb4..8936bd2ed8c 100644 --- a/doc/OnlineDocs/src/expr/performance.py +++ b/doc/OnlineDocs/src/expr/performance.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/expr/quicksum.py b/doc/OnlineDocs/src/expr/quicksum.py index 6b4d70bd961..1b6cd3f9909 100644 --- a/doc/OnlineDocs/src/expr/quicksum.py +++ b/doc/OnlineDocs/src/expr/quicksum.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/AbstractSuffixes.py b/doc/OnlineDocs/src/scripting/AbstractSuffixes.py index 24162d7bc8f..1c064042c6b 100644 --- a/doc/OnlineDocs/src/scripting/AbstractSuffixes.py +++ b/doc/OnlineDocs/src/scripting/AbstractSuffixes.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/Isinglebuild.py b/doc/OnlineDocs/src/scripting/Isinglebuild.py index b31e692d198..344f8905a4a 100644 --- a/doc/OnlineDocs/src/scripting/Isinglebuild.py +++ b/doc/OnlineDocs/src/scripting/Isinglebuild.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/NodesIn_init.py b/doc/OnlineDocs/src/scripting/NodesIn_init.py index 60268ef3183..c17b70150bc 100644 --- a/doc/OnlineDocs/src/scripting/NodesIn_init.py +++ b/doc/OnlineDocs/src/scripting/NodesIn_init.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/Z_init.py b/doc/OnlineDocs/src/scripting/Z_init.py index ab441eef101..1dd2843f4f0 100644 --- a/doc/OnlineDocs/src/scripting/Z_init.py +++ b/doc/OnlineDocs/src/scripting/Z_init.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/abstract2.py b/doc/OnlineDocs/src/scripting/abstract2.py index 1f7c35508db..544399a8a42 100644 --- a/doc/OnlineDocs/src/scripting/abstract2.py +++ b/doc/OnlineDocs/src/scripting/abstract2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/abstract2piece.py b/doc/OnlineDocs/src/scripting/abstract2piece.py index 8b58184e5e4..03c5139004e 100644 --- a/doc/OnlineDocs/src/scripting/abstract2piece.py +++ b/doc/OnlineDocs/src/scripting/abstract2piece.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/abstract2piecebuild.py b/doc/OnlineDocs/src/scripting/abstract2piecebuild.py index 694ee2b0336..d454d7fbc79 100644 --- a/doc/OnlineDocs/src/scripting/abstract2piecebuild.py +++ b/doc/OnlineDocs/src/scripting/abstract2piecebuild.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/block_iter_example.py b/doc/OnlineDocs/src/scripting/block_iter_example.py index 27d5a0e1819..10c8a4ea43d 100644 --- a/doc/OnlineDocs/src/scripting/block_iter_example.py +++ b/doc/OnlineDocs/src/scripting/block_iter_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/concrete1.py b/doc/OnlineDocs/src/scripting/concrete1.py index 31986c21ded..399715efde6 100644 --- a/doc/OnlineDocs/src/scripting/concrete1.py +++ b/doc/OnlineDocs/src/scripting/concrete1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/doubleA.py b/doc/OnlineDocs/src/scripting/doubleA.py index 2f65266d685..abf35979a05 100644 --- a/doc/OnlineDocs/src/scripting/doubleA.py +++ b/doc/OnlineDocs/src/scripting/doubleA.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/driveabs2.py b/doc/OnlineDocs/src/scripting/driveabs2.py index 87056f0fcb6..f8f972460b1 100644 --- a/doc/OnlineDocs/src/scripting/driveabs2.py +++ b/doc/OnlineDocs/src/scripting/driveabs2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/driveconc1.py b/doc/OnlineDocs/src/scripting/driveconc1.py index 2f7ece65a30..49b92f32d09 100644 --- a/doc/OnlineDocs/src/scripting/driveconc1.py +++ b/doc/OnlineDocs/src/scripting/driveconc1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/iterative1.py b/doc/OnlineDocs/src/scripting/iterative1.py index 8e91ea0d516..939120e834f 100644 --- a/doc/OnlineDocs/src/scripting/iterative1.py +++ b/doc/OnlineDocs/src/scripting/iterative1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/iterative2.py b/doc/OnlineDocs/src/scripting/iterative2.py index 558e7427441..7506337a491 100644 --- a/doc/OnlineDocs/src/scripting/iterative2.py +++ b/doc/OnlineDocs/src/scripting/iterative2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/noiteration1.py b/doc/OnlineDocs/src/scripting/noiteration1.py index 079b99365da..c7a86e9d1e9 100644 --- a/doc/OnlineDocs/src/scripting/noiteration1.py +++ b/doc/OnlineDocs/src/scripting/noiteration1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/parallel.py b/doc/OnlineDocs/src/scripting/parallel.py index ead3e1d674b..e6cfa002780 100644 --- a/doc/OnlineDocs/src/scripting/parallel.py +++ b/doc/OnlineDocs/src/scripting/parallel.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/spy4Constraints.py b/doc/OnlineDocs/src/scripting/spy4Constraints.py index f0f43672602..66f82802402 100644 --- a/doc/OnlineDocs/src/scripting/spy4Constraints.py +++ b/doc/OnlineDocs/src/scripting/spy4Constraints.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/spy4Expressions.py b/doc/OnlineDocs/src/scripting/spy4Expressions.py index 415481203a5..cf7ed1f112f 100644 --- a/doc/OnlineDocs/src/scripting/spy4Expressions.py +++ b/doc/OnlineDocs/src/scripting/spy4Expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py b/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py index 66dcb5e36b4..9f6698d63c9 100644 --- a/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py +++ b/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/spy4Variables.py b/doc/OnlineDocs/src/scripting/spy4Variables.py index 1dcdfc58a10..1bc2dc9f1ef 100644 --- a/doc/OnlineDocs/src/scripting/spy4Variables.py +++ b/doc/OnlineDocs/src/scripting/spy4Variables.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/scripting/spy4scripts.py b/doc/OnlineDocs/src/scripting/spy4scripts.py index d35030241fc..f71a1b67b11 100644 --- a/doc/OnlineDocs/src/scripting/spy4scripts.py +++ b/doc/OnlineDocs/src/scripting/spy4scripts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/strip_examples.py b/doc/OnlineDocs/src/strip_examples.py index dc742c1f53b..2fd03256499 100644 --- a/doc/OnlineDocs/src/strip_examples.py +++ b/doc/OnlineDocs/src/strip_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/doc/OnlineDocs/src/test_examples.py b/doc/OnlineDocs/src/test_examples.py index a7991eadf19..c5c9a135ee9 100644 --- a/doc/OnlineDocs/src/test_examples.py +++ b/doc/OnlineDocs/src/test_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/Heat_Conduction.py b/examples/dae/Heat_Conduction.py index 11f35fddd13..7e11ec59263 100644 --- a/examples/dae/Heat_Conduction.py +++ b/examples/dae/Heat_Conduction.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/Optimal_Control.py b/examples/dae/Optimal_Control.py index ed44d5eeb59..676c95271f2 100644 --- a/examples/dae/Optimal_Control.py +++ b/examples/dae/Optimal_Control.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/PDE_example.py b/examples/dae/PDE_example.py index 6cb7eb4a7fe..0aea173415b 100644 --- a/examples/dae/PDE_example.py +++ b/examples/dae/PDE_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/Parameter_Estimation.py b/examples/dae/Parameter_Estimation.py index 7ee2f112b94..332a21d93dc 100644 --- a/examples/dae/Parameter_Estimation.py +++ b/examples/dae/Parameter_Estimation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/Path_Constraint.py b/examples/dae/Path_Constraint.py index 866b4b3b90a..69f31980c63 100644 --- a/examples/dae/Path_Constraint.py +++ b/examples/dae/Path_Constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/ReactionKinetics.py b/examples/dae/ReactionKinetics.py index ef760820c4b..fa747cf8b21 100644 --- a/examples/dae/ReactionKinetics.py +++ b/examples/dae/ReactionKinetics.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/car_example.py b/examples/dae/car_example.py index f632e83f62a..b6ca2203860 100644 --- a/examples/dae/car_example.py +++ b/examples/dae/car_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/disease_DAE.py b/examples/dae/disease_DAE.py index 319e7276d83..bfeb2530fc9 100644 --- a/examples/dae/disease_DAE.py +++ b/examples/dae/disease_DAE.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/distill_DAE.py b/examples/dae/distill_DAE.py index cdfd543f9a8..e822cfb1752 100644 --- a/examples/dae/distill_DAE.py +++ b/examples/dae/distill_DAE.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/dynamic_scheduling.py b/examples/dae/dynamic_scheduling.py index 13cabeb5bcf..137307e31a9 100644 --- a/examples/dae/dynamic_scheduling.py +++ b/examples/dae/dynamic_scheduling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/laplace_BVP.py b/examples/dae/laplace_BVP.py index 6b2e2841575..61f911b3826 100644 --- a/examples/dae/laplace_BVP.py +++ b/examples/dae/laplace_BVP.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/run_Optimal_Control.py b/examples/dae/run_Optimal_Control.py index 2523bd8c607..2e7bc79dff4 100644 --- a/examples/dae/run_Optimal_Control.py +++ b/examples/dae/run_Optimal_Control.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/run_Parameter_Estimation.py b/examples/dae/run_Parameter_Estimation.py index a319000cb59..c9b649df8dd 100644 --- a/examples/dae/run_Parameter_Estimation.py +++ b/examples/dae/run_Parameter_Estimation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/run_Path_Constraint.py b/examples/dae/run_Path_Constraint.py index 17a576a57d8..996b432a555 100644 --- a/examples/dae/run_Path_Constraint.py +++ b/examples/dae/run_Path_Constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/run_disease.py b/examples/dae/run_disease.py index 04457dfc890..5d9595a89d5 100644 --- a/examples/dae/run_disease.py +++ b/examples/dae/run_disease.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/run_distill.py b/examples/dae/run_distill.py index d9ececf34fc..9b09850f90a 100644 --- a/examples/dae/run_distill.py +++ b/examples/dae/run_distill.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/run_stochpdegas_automatic.py b/examples/dae/run_stochpdegas_automatic.py index 92f95b9d828..6fc9f6d594c 100644 --- a/examples/dae/run_stochpdegas_automatic.py +++ b/examples/dae/run_stochpdegas_automatic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/simulator_dae_example.py b/examples/dae/simulator_dae_example.py index 81fd3af816d..4ea1f9fd5f0 100644 --- a/examples/dae/simulator_dae_example.py +++ b/examples/dae/simulator_dae_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/simulator_dae_multindex_example.py b/examples/dae/simulator_dae_multindex_example.py index bc17fd41643..775eb4f8c79 100644 --- a/examples/dae/simulator_dae_multindex_example.py +++ b/examples/dae/simulator_dae_multindex_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/simulator_ode_example.py b/examples/dae/simulator_ode_example.py index ae30071f4ef..f6f28b87d07 100644 --- a/examples/dae/simulator_ode_example.py +++ b/examples/dae/simulator_ode_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/simulator_ode_multindex_example.py b/examples/dae/simulator_ode_multindex_example.py index e02eabef076..b1b9111084b 100644 --- a/examples/dae/simulator_ode_multindex_example.py +++ b/examples/dae/simulator_ode_multindex_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/dae/stochpdegas_automatic.py b/examples/dae/stochpdegas_automatic.py index e846d045ddc..397b4a18100 100644 --- a/examples/dae/stochpdegas_automatic.py +++ b/examples/dae/stochpdegas_automatic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/__init__.py b/examples/doc/samples/__init__.py index c967348cb68..0110902b288 100644 --- a/examples/doc/samples/__init__.py +++ b/examples/doc/samples/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/case_studies/deer/DeerProblem.py b/examples/doc/samples/case_studies/deer/DeerProblem.py index dfdc987ade4..d09c9b53887 100644 --- a/examples/doc/samples/case_studies/deer/DeerProblem.py +++ b/examples/doc/samples/case_studies/deer/DeerProblem.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/case_studies/diet/DietProblem.py b/examples/doc/samples/case_studies/diet/DietProblem.py index 462deee03a5..64624310943 100644 --- a/examples/doc/samples/case_studies/diet/DietProblem.py +++ b/examples/doc/samples/case_studies/diet/DietProblem.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/case_studies/disease_est/DiseaseEstimation.py b/examples/doc/samples/case_studies/disease_est/DiseaseEstimation.py index d8f3f94bcc5..6a0edb38350 100644 --- a/examples/doc/samples/case_studies/disease_est/DiseaseEstimation.py +++ b/examples/doc/samples/case_studies/disease_est/DiseaseEstimation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/case_studies/max_flow/MaxFlow.py b/examples/doc/samples/case_studies/max_flow/MaxFlow.py index 36d42dfd3e3..1e75fa4e79d 100644 --- a/examples/doc/samples/case_studies/max_flow/MaxFlow.py +++ b/examples/doc/samples/case_studies/max_flow/MaxFlow.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/case_studies/network_flow/networkFlow1.py b/examples/doc/samples/case_studies/network_flow/networkFlow1.py index a1d05ccbd20..eb8c8e48a1a 100644 --- a/examples/doc/samples/case_studies/network_flow/networkFlow1.py +++ b/examples/doc/samples/case_studies/network_flow/networkFlow1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/case_studies/rosen/Rosenbrock.py b/examples/doc/samples/case_studies/rosen/Rosenbrock.py index f70fa30199c..51e7d51b57d 100644 --- a/examples/doc/samples/case_studies/rosen/Rosenbrock.py +++ b/examples/doc/samples/case_studies/rosen/Rosenbrock.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/case_studies/transportation/transportation.py b/examples/doc/samples/case_studies/transportation/transportation.py index 620e6c3fa1d..588ae764953 100644 --- a/examples/doc/samples/case_studies/transportation/transportation.py +++ b/examples/doc/samples/case_studies/transportation/transportation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_cplex.py b/examples/doc/samples/comparisons/cutstock/cutstock_cplex.py index e61f82b388d..f49c5b591ae 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_cplex.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_cplex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_grb.py b/examples/doc/samples/comparisons/cutstock/cutstock_grb.py index 9940c729b6e..483d84b02e6 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_grb.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_grb.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_lpsolve.py b/examples/doc/samples/comparisons/cutstock/cutstock_lpsolve.py index 5bc7aea2116..9a6c8301e8f 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_lpsolve.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_lpsolve.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_pulpor.py b/examples/doc/samples/comparisons/cutstock/cutstock_pulpor.py index 3f366e9b3e2..d14b0fe46c1 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_pulpor.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_pulpor.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_pyomo.py b/examples/doc/samples/comparisons/cutstock/cutstock_pyomo.py index c87b93c37a5..48d7e6b26fd 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_pyomo.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_pyomo.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/comparisons/cutstock/cutstock_util.py b/examples/doc/samples/comparisons/cutstock/cutstock_util.py index 3fde234a0f9..da5349ec06c 100644 --- a/examples/doc/samples/comparisons/cutstock/cutstock_util.py +++ b/examples/doc/samples/comparisons/cutstock/cutstock_util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/comparisons/sched/pyomo/sched.py b/examples/doc/samples/comparisons/sched/pyomo/sched.py index 2c03bebb421..cf781713641 100644 --- a/examples/doc/samples/comparisons/sched/pyomo/sched.py +++ b/examples/doc/samples/comparisons/sched/pyomo/sched.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/scripts/__init__.py b/examples/doc/samples/scripts/__init__.py index c967348cb68..0110902b288 100644 --- a/examples/doc/samples/scripts/__init__.py +++ b/examples/doc/samples/scripts/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/scripts/s1/knapsack.py b/examples/doc/samples/scripts/s1/knapsack.py index 2965d76650c..cee3937b668 100644 --- a/examples/doc/samples/scripts/s1/knapsack.py +++ b/examples/doc/samples/scripts/s1/knapsack.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/scripts/s1/script.py b/examples/doc/samples/scripts/s1/script.py index b5d60af6182..4ddaea45e19 100644 --- a/examples/doc/samples/scripts/s1/script.py +++ b/examples/doc/samples/scripts/s1/script.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/scripts/s2/knapsack.py b/examples/doc/samples/scripts/s2/knapsack.py index 66e55188871..3131cee7bc5 100644 --- a/examples/doc/samples/scripts/s2/knapsack.py +++ b/examples/doc/samples/scripts/s2/knapsack.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/scripts/s2/script.py b/examples/doc/samples/scripts/s2/script.py index 481ae7b26bb..fe97d6ab8fd 100644 --- a/examples/doc/samples/scripts/s2/script.py +++ b/examples/doc/samples/scripts/s2/script.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/scripts/test_scripts.py b/examples/doc/samples/scripts/test_scripts.py index ca0c8a7cc4e..691a44aea2d 100644 --- a/examples/doc/samples/scripts/test_scripts.py +++ b/examples/doc/samples/scripts/test_scripts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/doc/samples/update.py b/examples/doc/samples/update.py index ab2195d1f32..8789413303c 100644 --- a/examples/doc/samples/update.py +++ b/examples/doc/samples/update.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/batchProcessing.py b/examples/gdp/batchProcessing.py index 4f3fb02df25..9810f5d63f1 100644 --- a/examples/gdp/batchProcessing.py +++ b/examples/gdp/batchProcessing.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/circles/circles.py b/examples/gdp/circles/circles.py index 3a7846f1441..a8b7a156fad 100644 --- a/examples/gdp/circles/circles.py +++ b/examples/gdp/circles/circles.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/constrained_layout/cons_layout_model.py b/examples/gdp/constrained_layout/cons_layout_model.py index 9f9169ede22..d38fd0cc66b 100644 --- a/examples/gdp/constrained_layout/cons_layout_model.py +++ b/examples/gdp/constrained_layout/cons_layout_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/disease_model.py b/examples/gdp/disease_model.py index bc3e69600ec..498337e35e6 100644 --- a/examples/gdp/disease_model.py +++ b/examples/gdp/disease_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/eight_process/eight_proc_logical.py b/examples/gdp/eight_process/eight_proc_logical.py index 23827a52d71..4496427d421 100644 --- a/examples/gdp/eight_process/eight_proc_logical.py +++ b/examples/gdp/eight_process/eight_proc_logical.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/eight_process/eight_proc_model.py b/examples/gdp/eight_process/eight_proc_model.py index d333405e469..41bb6d462f1 100644 --- a/examples/gdp/eight_process/eight_proc_model.py +++ b/examples/gdp/eight_process/eight_proc_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/eight_process/eight_proc_verbose_model.py b/examples/gdp/eight_process/eight_proc_verbose_model.py index fc748cce20f..1fd68909146 100644 --- a/examples/gdp/eight_process/eight_proc_verbose_model.py +++ b/examples/gdp/eight_process/eight_proc_verbose_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/farm_layout/farm_layout.py b/examples/gdp/farm_layout/farm_layout.py index 87043bc4ff5..1b232b9cfa6 100644 --- a/examples/gdp/farm_layout/farm_layout.py +++ b/examples/gdp/farm_layout/farm_layout.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/jobshop-nodisjuncts.py b/examples/gdp/jobshop-nodisjuncts.py index bc656dc4717..0cd5b5ab274 100644 --- a/examples/gdp/jobshop-nodisjuncts.py +++ b/examples/gdp/jobshop-nodisjuncts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/jobshop.py b/examples/gdp/jobshop.py index 619ece47e72..7119ee7655c 100644 --- a/examples/gdp/jobshop.py +++ b/examples/gdp/jobshop.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/medTermPurchasing_Literal.py b/examples/gdp/medTermPurchasing_Literal.py index 14ec25d750c..b6d16c216fe 100755 --- a/examples/gdp/medTermPurchasing_Literal.py +++ b/examples/gdp/medTermPurchasing_Literal.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/nine_process/small_process.py b/examples/gdp/nine_process/small_process.py index adc7098d991..2abffef1af6 100644 --- a/examples/gdp/nine_process/small_process.py +++ b/examples/gdp/nine_process/small_process.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/simple1.py b/examples/gdp/simple1.py index 323943cd552..de41c0bfd00 100644 --- a/examples/gdp/simple1.py +++ b/examples/gdp/simple1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/simple2.py b/examples/gdp/simple2.py index 9c13872100c..b066d705036 100644 --- a/examples/gdp/simple2.py +++ b/examples/gdp/simple2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/simple3.py b/examples/gdp/simple3.py index bbe9745d193..890daf8882b 100644 --- a/examples/gdp/simple3.py +++ b/examples/gdp/simple3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/small_lit/basic_step.py b/examples/gdp/small_lit/basic_step.py index fd466dfc1f4..2d9da97167c 100644 --- a/examples/gdp/small_lit/basic_step.py +++ b/examples/gdp/small_lit/basic_step.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/small_lit/contracts_problem.py b/examples/gdp/small_lit/contracts_problem.py index 9d1254688b2..0c59d2264ee 100644 --- a/examples/gdp/small_lit/contracts_problem.py +++ b/examples/gdp/small_lit/contracts_problem.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/small_lit/ex1_Lee.py b/examples/gdp/small_lit/ex1_Lee.py index 05bd1bd1bc0..abbf470a1c3 100644 --- a/examples/gdp/small_lit/ex1_Lee.py +++ b/examples/gdp/small_lit/ex1_Lee.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/small_lit/ex_633_trespalacios.py b/examples/gdp/small_lit/ex_633_trespalacios.py index 499294be2ae..b0c5fbd85ac 100644 --- a/examples/gdp/small_lit/ex_633_trespalacios.py +++ b/examples/gdp/small_lit/ex_633_trespalacios.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/small_lit/nonconvex_HEN.py b/examples/gdp/small_lit/nonconvex_HEN.py index bdec0e2823a..05fad970b84 100644 --- a/examples/gdp/small_lit/nonconvex_HEN.py +++ b/examples/gdp/small_lit/nonconvex_HEN.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/stickies.py b/examples/gdp/stickies.py index 154a9cbc0cd..73b537ff13d 100644 --- a/examples/gdp/stickies.py +++ b/examples/gdp/stickies.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/strip_packing/stripPacking.py b/examples/gdp/strip_packing/stripPacking.py index fb2ed3f91fd..39f7208b838 100644 --- a/examples/gdp/strip_packing/stripPacking.py +++ b/examples/gdp/strip_packing/stripPacking.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/strip_packing/strip_packing_8rect.py b/examples/gdp/strip_packing/strip_packing_8rect.py index f03b3f798f9..2bd7c4840ca 100644 --- a/examples/gdp/strip_packing/strip_packing_8rect.py +++ b/examples/gdp/strip_packing/strip_packing_8rect.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/strip_packing/strip_packing_concrete.py b/examples/gdp/strip_packing/strip_packing_concrete.py index d5ace9632fd..b0907cdea61 100644 --- a/examples/gdp/strip_packing/strip_packing_concrete.py +++ b/examples/gdp/strip_packing/strip_packing_concrete.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/gdp/two_rxn_lee/two_rxn_model.py b/examples/gdp/two_rxn_lee/two_rxn_model.py index 4f9471b583a..98e4cc2e878 100644 --- a/examples/gdp/two_rxn_lee/two_rxn_model.py +++ b/examples/gdp/two_rxn_lee/two_rxn_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/blocks.py b/examples/kernel/blocks.py index b19108ffb44..db1cb6655c2 100644 --- a/examples/kernel/blocks.py +++ b/examples/kernel/blocks.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/conic.py b/examples/kernel/conic.py index 86e5a95580c..5ee66a00ee9 100644 --- a/examples/kernel/conic.py +++ b/examples/kernel/conic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/constraints.py b/examples/kernel/constraints.py index e5bf9797987..69823a6ebbe 100644 --- a/examples/kernel/constraints.py +++ b/examples/kernel/constraints.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/containers.py b/examples/kernel/containers.py index 44c65bfbda8..9ec749b8c3e 100644 --- a/examples/kernel/containers.py +++ b/examples/kernel/containers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/expressions.py b/examples/kernel/expressions.py index 2f27239f26e..faef8d1d4ad 100644 --- a/examples/kernel/expressions.py +++ b/examples/kernel/expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/mosek/geometric1.py b/examples/kernel/mosek/geometric1.py index 9cd492f36cf..8148e707819 100644 --- a/examples/kernel/mosek/geometric1.py +++ b/examples/kernel/mosek/geometric1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/mosek/geometric2.py b/examples/kernel/mosek/geometric2.py index 2f75f721dc4..3fb62c86312 100644 --- a/examples/kernel/mosek/geometric2.py +++ b/examples/kernel/mosek/geometric2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/mosek/maximum_volume_cuboid.py b/examples/kernel/mosek/maximum_volume_cuboid.py index 661adc3bf5a..df200cc801c 100644 --- a/examples/kernel/mosek/maximum_volume_cuboid.py +++ b/examples/kernel/mosek/maximum_volume_cuboid.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/mosek/power1.py b/examples/kernel/mosek/power1.py index b8a306e7cdf..a6d6ebbe47d 100644 --- a/examples/kernel/mosek/power1.py +++ b/examples/kernel/mosek/power1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/mosek/semidefinite.py b/examples/kernel/mosek/semidefinite.py index 177662205f8..6be47d85451 100644 --- a/examples/kernel/mosek/semidefinite.py +++ b/examples/kernel/mosek/semidefinite.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/objectives.py b/examples/kernel/objectives.py index bbb0c704211..27a41f4edb5 100644 --- a/examples/kernel/objectives.py +++ b/examples/kernel/objectives.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/parameters.py b/examples/kernel/parameters.py index a8bce6ca6af..e9e412525bb 100644 --- a/examples/kernel/parameters.py +++ b/examples/kernel/parameters.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/piecewise_functions.py b/examples/kernel/piecewise_functions.py index f372227fcb4..73a7f680725 100644 --- a/examples/kernel/piecewise_functions.py +++ b/examples/kernel/piecewise_functions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/piecewise_nd_functions.py b/examples/kernel/piecewise_nd_functions.py index 78739e3825c..7de37fcbfc6 100644 --- a/examples/kernel/piecewise_nd_functions.py +++ b/examples/kernel/piecewise_nd_functions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/special_ordered_sets.py b/examples/kernel/special_ordered_sets.py index 53328923a60..abacc3d4205 100644 --- a/examples/kernel/special_ordered_sets.py +++ b/examples/kernel/special_ordered_sets.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/suffixes.py b/examples/kernel/suffixes.py index 029dd046f26..ae95fbbdd09 100644 --- a/examples/kernel/suffixes.py +++ b/examples/kernel/suffixes.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/kernel/variables.py b/examples/kernel/variables.py index b2dd0ae8dff..36865b58183 100644 --- a/examples/kernel/variables.py +++ b/examples/kernel/variables.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/bard1.py b/examples/mpec/bard1.py index 4a6f7ab6642..59955eefb8e 100644 --- a/examples/mpec/bard1.py +++ b/examples/mpec/bard1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/df.py b/examples/mpec/df.py index 41984992bdd..7bb25b11e07 100644 --- a/examples/mpec/df.py +++ b/examples/mpec/df.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/indexed.py b/examples/mpec/indexed.py index b69d5093477..0aff5de5b20 100644 --- a/examples/mpec/indexed.py +++ b/examples/mpec/indexed.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/linear1.py b/examples/mpec/linear1.py index eba04759ae3..f24fd357e62 100644 --- a/examples/mpec/linear1.py +++ b/examples/mpec/linear1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/munson1.py b/examples/mpec/munson1.py index debdf709db9..99c240b5c06 100644 --- a/examples/mpec/munson1.py +++ b/examples/mpec/munson1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/munson1a.py b/examples/mpec/munson1a.py index 519db4e6ec2..67f8f318531 100644 --- a/examples/mpec/munson1a.py +++ b/examples/mpec/munson1a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/munson1b.py b/examples/mpec/munson1b.py index ff2b7b51294..46fff90a785 100644 --- a/examples/mpec/munson1b.py +++ b/examples/mpec/munson1b.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/munson1c.py b/examples/mpec/munson1c.py index 2592b25c515..dee5b224e75 100644 --- a/examples/mpec/munson1c.py +++ b/examples/mpec/munson1c.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/munson1d.py b/examples/mpec/munson1d.py index 0fb08ce73fb..157177f2eb0 100644 --- a/examples/mpec/munson1d.py +++ b/examples/mpec/munson1d.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/mpec/scholtes4.py b/examples/mpec/scholtes4.py index 93cdb8fa6fe..8d574dd1916 100644 --- a/examples/mpec/scholtes4.py +++ b/examples/mpec/scholtes4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/dae/run_stochpdegas1_automatic.py b/examples/performance/dae/run_stochpdegas1_automatic.py index 5eacc8992d1..fffa1a71ae1 100644 --- a/examples/performance/dae/run_stochpdegas1_automatic.py +++ b/examples/performance/dae/run_stochpdegas1_automatic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/dae/stochpdegas1_automatic.py b/examples/performance/dae/stochpdegas1_automatic.py index 962ed266148..ce6132e6cf5 100644 --- a/examples/performance/dae/stochpdegas1_automatic.py +++ b/examples/performance/dae/stochpdegas1_automatic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/jump/clnlbeam.py b/examples/performance/jump/clnlbeam.py index 18948c1b549..410068a6753 100644 --- a/examples/performance/jump/clnlbeam.py +++ b/examples/performance/jump/clnlbeam.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/jump/facility.py b/examples/performance/jump/facility.py index b67f21bd048..fa0c306d6e5 100644 --- a/examples/performance/jump/facility.py +++ b/examples/performance/jump/facility.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/jump/lqcp.py b/examples/performance/jump/lqcp.py index b4da6b62a5e..b8ef096d7be 100644 --- a/examples/performance/jump/lqcp.py +++ b/examples/performance/jump/lqcp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/jump/opf_66200bus.py b/examples/performance/jump/opf_66200bus.py index 022eb938fb0..702ff59a61c 100644 --- a/examples/performance/jump/opf_66200bus.py +++ b/examples/performance/jump/opf_66200bus.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/jump/opf_6620bus.py b/examples/performance/jump/opf_6620bus.py index 0e139cd8c5f..34b910f43c0 100644 --- a/examples/performance/jump/opf_6620bus.py +++ b/examples/performance/jump/opf_6620bus.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/jump/opf_662bus.py b/examples/performance/jump/opf_662bus.py index 5270c573236..8a768ca16e0 100644 --- a/examples/performance/jump/opf_662bus.py +++ b/examples/performance/jump/opf_662bus.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/misc/bilinear1_100.py b/examples/performance/misc/bilinear1_100.py index 527cf5d7ccc..d86091c4c76 100644 --- a/examples/performance/misc/bilinear1_100.py +++ b/examples/performance/misc/bilinear1_100.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/misc/bilinear1_100000.py b/examples/performance/misc/bilinear1_100000.py index 9fdef98c059..0fa2eafedc6 100644 --- a/examples/performance/misc/bilinear1_100000.py +++ b/examples/performance/misc/bilinear1_100000.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/misc/bilinear2_100.py b/examples/performance/misc/bilinear2_100.py index 77b8737339d..227bfe000e0 100644 --- a/examples/performance/misc/bilinear2_100.py +++ b/examples/performance/misc/bilinear2_100.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/misc/bilinear2_100000.py b/examples/performance/misc/bilinear2_100000.py index 7bf224f8b47..9d2a4d6fb7c 100644 --- a/examples/performance/misc/bilinear2_100000.py +++ b/examples/performance/misc/bilinear2_100000.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/misc/diag1_100.py b/examples/performance/misc/diag1_100.py index e92fc50201f..369d81982f0 100644 --- a/examples/performance/misc/diag1_100.py +++ b/examples/performance/misc/diag1_100.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/misc/diag1_100000.py b/examples/performance/misc/diag1_100000.py index 2bdfe99e749..536758fda5d 100644 --- a/examples/performance/misc/diag1_100000.py +++ b/examples/performance/misc/diag1_100000.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/misc/diag2_100.py b/examples/performance/misc/diag2_100.py index fe005eb74f1..6ad47528ff2 100644 --- a/examples/performance/misc/diag2_100.py +++ b/examples/performance/misc/diag2_100.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/misc/diag2_100000.py b/examples/performance/misc/diag2_100000.py index eca192b9679..b95e2dd1d6f 100644 --- a/examples/performance/misc/diag2_100000.py +++ b/examples/performance/misc/diag2_100000.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/misc/set1.py b/examples/performance/misc/set1.py index abf656ee350..8a8b84fdcc3 100644 --- a/examples/performance/misc/set1.py +++ b/examples/performance/misc/set1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/misc/sparse1.py b/examples/performance/misc/sparse1.py index 0858f374248..b4883d379bc 100644 --- a/examples/performance/misc/sparse1.py +++ b/examples/performance/misc/sparse1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/pmedian/pmedian1.py b/examples/performance/pmedian/pmedian1.py index 3d3f6c5407f..a22540efdd5 100644 --- a/examples/performance/pmedian/pmedian1.py +++ b/examples/performance/pmedian/pmedian1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/performance/pmedian/pmedian2.py b/examples/performance/pmedian/pmedian2.py index 434ded6dcbc..ff25a6c15eb 100644 --- a/examples/performance/pmedian/pmedian2.py +++ b/examples/performance/pmedian/pmedian2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/diet.py b/examples/pyomo/amplbook2/diet.py index 8cdffefa20f..cc52eacae20 100644 --- a/examples/pyomo/amplbook2/diet.py +++ b/examples/pyomo/amplbook2/diet.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/dieti.py b/examples/pyomo/amplbook2/dieti.py index 0934dcf83c6..45d403dd810 100644 --- a/examples/pyomo/amplbook2/dieti.py +++ b/examples/pyomo/amplbook2/dieti.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/econ2min.py b/examples/pyomo/amplbook2/econ2min.py index 0d27df780bb..fb870e02364 100644 --- a/examples/pyomo/amplbook2/econ2min.py +++ b/examples/pyomo/amplbook2/econ2min.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/econmin.py b/examples/pyomo/amplbook2/econmin.py index 84e41107ff2..d9c95758d4d 100644 --- a/examples/pyomo/amplbook2/econmin.py +++ b/examples/pyomo/amplbook2/econmin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/prod.py b/examples/pyomo/amplbook2/prod.py index 74e456e013f..236f7254b29 100644 --- a/examples/pyomo/amplbook2/prod.py +++ b/examples/pyomo/amplbook2/prod.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/steel.py b/examples/pyomo/amplbook2/steel.py index 43bea775526..8c5c9b2a1d3 100644 --- a/examples/pyomo/amplbook2/steel.py +++ b/examples/pyomo/amplbook2/steel.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/steel3.py b/examples/pyomo/amplbook2/steel3.py index e9e494b6a1a..dd3b3ac202f 100644 --- a/examples/pyomo/amplbook2/steel3.py +++ b/examples/pyomo/amplbook2/steel3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/amplbook2/steel4.py b/examples/pyomo/amplbook2/steel4.py index b6709e478e9..10cb0979d24 100644 --- a/examples/pyomo/amplbook2/steel4.py +++ b/examples/pyomo/amplbook2/steel4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/benders/master.py b/examples/pyomo/benders/master.py index a457bf28b06..372810dc024 100644 --- a/examples/pyomo/benders/master.py +++ b/examples/pyomo/benders/master.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/benders/subproblem.py b/examples/pyomo/benders/subproblem.py index 886f71ff321..ae46dad2d41 100644 --- a/examples/pyomo/benders/subproblem.py +++ b/examples/pyomo/benders/subproblem.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/callbacks/sc.py b/examples/pyomo/callbacks/sc.py index ce32b0a1074..0882815c6b7 100644 --- a/examples/pyomo/callbacks/sc.py +++ b/examples/pyomo/callbacks/sc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/callbacks/sc_callback.py b/examples/pyomo/callbacks/sc_callback.py index 0dae9e1befc..cacc438b380 100644 --- a/examples/pyomo/callbacks/sc_callback.py +++ b/examples/pyomo/callbacks/sc_callback.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/callbacks/sc_script.py b/examples/pyomo/callbacks/sc_script.py index 8e4ade21b51..d3044e4d667 100644 --- a/examples/pyomo/callbacks/sc_script.py +++ b/examples/pyomo/callbacks/sc_script.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/callbacks/scalability/run.py b/examples/pyomo/callbacks/scalability/run.py index 8465e3f5019..cf95076fcc3 100644 --- a/examples/pyomo/callbacks/scalability/run.py +++ b/examples/pyomo/callbacks/scalability/run.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/callbacks/tsp.py b/examples/pyomo/callbacks/tsp.py index d3e28a98d3f..8526a540b66 100644 --- a/examples/pyomo/callbacks/tsp.py +++ b/examples/pyomo/callbacks/tsp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/columngeneration/cutting_stock.py b/examples/pyomo/columngeneration/cutting_stock.py index 58df6a5ad16..2d9399c7db4 100644 --- a/examples/pyomo/columngeneration/cutting_stock.py +++ b/examples/pyomo/columngeneration/cutting_stock.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/concrete/Whiskas.py b/examples/pyomo/concrete/Whiskas.py index 9bc8dd87e9d..3d3c19e94ac 100644 --- a/examples/pyomo/concrete/Whiskas.py +++ b/examples/pyomo/concrete/Whiskas.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/concrete/knapsack-abstract.py b/examples/pyomo/concrete/knapsack-abstract.py index bbef95f7810..9766d902722 100644 --- a/examples/pyomo/concrete/knapsack-abstract.py +++ b/examples/pyomo/concrete/knapsack-abstract.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/concrete/knapsack-concrete.py b/examples/pyomo/concrete/knapsack-concrete.py index cd115ab40a3..8966d0b8498 100644 --- a/examples/pyomo/concrete/knapsack-concrete.py +++ b/examples/pyomo/concrete/knapsack-concrete.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/concrete/rosen.py b/examples/pyomo/concrete/rosen.py index a8eb89081e8..ae51ae50ac0 100644 --- a/examples/pyomo/concrete/rosen.py +++ b/examples/pyomo/concrete/rosen.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/concrete/sodacan.py b/examples/pyomo/concrete/sodacan.py index fddc8d5aa95..5429b27a9d5 100644 --- a/examples/pyomo/concrete/sodacan.py +++ b/examples/pyomo/concrete/sodacan.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/concrete/sodacan_fig.py b/examples/pyomo/concrete/sodacan_fig.py index ab33f522dfe..b263eaf558d 100644 --- a/examples/pyomo/concrete/sodacan_fig.py +++ b/examples/pyomo/concrete/sodacan_fig.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/concrete/sp.py b/examples/pyomo/concrete/sp.py index 3a1b8aeef5a..e82a4bca0a9 100644 --- a/examples/pyomo/concrete/sp.py +++ b/examples/pyomo/concrete/sp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/concrete/sp_data.py b/examples/pyomo/concrete/sp_data.py index d65ae5a1d83..4453a10cead 100644 --- a/examples/pyomo/concrete/sp_data.py +++ b/examples/pyomo/concrete/sp_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/connectors/network_flow.py b/examples/pyomo/connectors/network_flow.py index cb75ca7ecf2..d5587fdf4c8 100644 --- a/examples/pyomo/connectors/network_flow.py +++ b/examples/pyomo/connectors/network_flow.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/connectors/network_flow_proposed.py b/examples/pyomo/connectors/network_flow_proposed.py index ed603ff6626..f234f2decf4 100644 --- a/examples/pyomo/connectors/network_flow_proposed.py +++ b/examples/pyomo/connectors/network_flow_proposed.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/core/block1.py b/examples/pyomo/core/block1.py index 96f8114f19c..161fc2ca2f7 100644 --- a/examples/pyomo/core/block1.py +++ b/examples/pyomo/core/block1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/core/integrality1.py b/examples/pyomo/core/integrality1.py index db81805555f..0ab3a433dac 100644 --- a/examples/pyomo/core/integrality1.py +++ b/examples/pyomo/core/integrality1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/core/integrality2.py b/examples/pyomo/core/integrality2.py index 2d85c9f2455..6461d36f923 100644 --- a/examples/pyomo/core/integrality2.py +++ b/examples/pyomo/core/integrality2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/core/simple.py b/examples/pyomo/core/simple.py index d0359c143bf..6976f3d25ad 100644 --- a/examples/pyomo/core/simple.py +++ b/examples/pyomo/core/simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/core/t1.py b/examples/pyomo/core/t1.py index 4135049d4be..5d5416985a9 100644 --- a/examples/pyomo/core/t1.py +++ b/examples/pyomo/core/t1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/core/t2.py b/examples/pyomo/core/t2.py index 5d687917fba..4d3f1934cbe 100644 --- a/examples/pyomo/core/t2.py +++ b/examples/pyomo/core/t2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/core/t5.py b/examples/pyomo/core/t5.py index 38605751015..6b9d94e0ff1 100644 --- a/examples/pyomo/core/t5.py +++ b/examples/pyomo/core/t5.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/diet/diet-sqlite.py b/examples/pyomo/diet/diet-sqlite.py index e8963485294..dccd3c338d0 100644 --- a/examples/pyomo/diet/diet-sqlite.py +++ b/examples/pyomo/diet/diet-sqlite.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/diet/diet1.py b/examples/pyomo/diet/diet1.py index 1fd61ca268c..217f80b9c25 100644 --- a/examples/pyomo/diet/diet1.py +++ b/examples/pyomo/diet/diet1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/diet/diet2.py b/examples/pyomo/diet/diet2.py index 526dbcef484..291261b0901 100644 --- a/examples/pyomo/diet/diet2.py +++ b/examples/pyomo/diet/diet2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/draft/api.py b/examples/pyomo/draft/api.py index 5b506882d9b..d785f41935e 100644 --- a/examples/pyomo/draft/api.py +++ b/examples/pyomo/draft/api.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/draft/bpack.py b/examples/pyomo/draft/bpack.py index 697ce531013..7b076f7737b 100644 --- a/examples/pyomo/draft/bpack.py +++ b/examples/pyomo/draft/bpack.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/draft/diet2.py b/examples/pyomo/draft/diet2.py index 9e4d2c5d9c4..d23fa3cf5db 100644 --- a/examples/pyomo/draft/diet2.py +++ b/examples/pyomo/draft/diet2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/p-median/decorated_pmedian.py b/examples/pyomo/p-median/decorated_pmedian.py index be4cc5994be..c66971945f3 100644 --- a/examples/pyomo/p-median/decorated_pmedian.py +++ b/examples/pyomo/p-median/decorated_pmedian.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/p-median/pmedian.py b/examples/pyomo/p-median/pmedian.py index 88731f287d8..865aa7cb61f 100644 --- a/examples/pyomo/p-median/pmedian.py +++ b/examples/pyomo/p-median/pmedian.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/p-median/solver1.py b/examples/pyomo/p-median/solver1.py index 113bf9fdd29..2652ab13943 100644 --- a/examples/pyomo/p-median/solver1.py +++ b/examples/pyomo/p-median/solver1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/p-median/solver2.py b/examples/pyomo/p-median/solver2.py index c62f161fd24..50ec5388811 100644 --- a/examples/pyomo/p-median/solver2.py +++ b/examples/pyomo/p-median/solver2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/piecewise/convex.py b/examples/pyomo/piecewise/convex.py index a3233ae5c3e..fb8095f80e3 100644 --- a/examples/pyomo/piecewise/convex.py +++ b/examples/pyomo/piecewise/convex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/piecewise/indexed.py b/examples/pyomo/piecewise/indexed.py index dea56df3911..cde21ec847e 100644 --- a/examples/pyomo/piecewise/indexed.py +++ b/examples/pyomo/piecewise/indexed.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/piecewise/indexed_nonlinear.py b/examples/pyomo/piecewise/indexed_nonlinear.py index e871508d1be..d72fbc8a899 100644 --- a/examples/pyomo/piecewise/indexed_nonlinear.py +++ b/examples/pyomo/piecewise/indexed_nonlinear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/piecewise/indexed_points.py b/examples/pyomo/piecewise/indexed_points.py index 15b1c33a7ec..66110bea342 100644 --- a/examples/pyomo/piecewise/indexed_points.py +++ b/examples/pyomo/piecewise/indexed_points.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/piecewise/nonconvex.py b/examples/pyomo/piecewise/nonconvex.py index 004748ab2eb..5300278d5b9 100644 --- a/examples/pyomo/piecewise/nonconvex.py +++ b/examples/pyomo/piecewise/nonconvex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/piecewise/points.py b/examples/pyomo/piecewise/points.py index c822ceb5860..91d45684c4f 100644 --- a/examples/pyomo/piecewise/points.py +++ b/examples/pyomo/piecewise/points.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/piecewise/step.py b/examples/pyomo/piecewise/step.py index c3fbb4762ab..95aac74d7f7 100644 --- a/examples/pyomo/piecewise/step.py +++ b/examples/pyomo/piecewise/step.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/quadratic/example1.py b/examples/pyomo/quadratic/example1.py index dff911a0f0c..ab77c5a1733 100644 --- a/examples/pyomo/quadratic/example1.py +++ b/examples/pyomo/quadratic/example1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/quadratic/example2.py b/examples/pyomo/quadratic/example2.py index 981f2ef0bfb..ce02c6f70c8 100644 --- a/examples/pyomo/quadratic/example2.py +++ b/examples/pyomo/quadratic/example2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/quadratic/example3.py b/examples/pyomo/quadratic/example3.py index 4d96afe3328..bdba936f694 100644 --- a/examples/pyomo/quadratic/example3.py +++ b/examples/pyomo/quadratic/example3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/quadratic/example4.py b/examples/pyomo/quadratic/example4.py index 256fc862a16..ecfc9981162 100644 --- a/examples/pyomo/quadratic/example4.py +++ b/examples/pyomo/quadratic/example4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/radertext/Ex2_1.py b/examples/pyomo/radertext/Ex2_1.py index d352325798a..981388d4c72 100644 --- a/examples/pyomo/radertext/Ex2_1.py +++ b/examples/pyomo/radertext/Ex2_1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/radertext/Ex2_2.py b/examples/pyomo/radertext/Ex2_2.py index 13c23dd1816..41b56e52669 100644 --- a/examples/pyomo/radertext/Ex2_2.py +++ b/examples/pyomo/radertext/Ex2_2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/radertext/Ex2_3.py b/examples/pyomo/radertext/Ex2_3.py index d4dc3109ea1..7dc39afa773 100644 --- a/examples/pyomo/radertext/Ex2_3.py +++ b/examples/pyomo/radertext/Ex2_3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/radertext/Ex2_5.py b/examples/pyomo/radertext/Ex2_5.py index da90b473b1f..fee49b46cb0 100644 --- a/examples/pyomo/radertext/Ex2_5.py +++ b/examples/pyomo/radertext/Ex2_5.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/radertext/Ex2_6a.py b/examples/pyomo/radertext/Ex2_6a.py index dc33a9b64e2..24bb866ec51 100644 --- a/examples/pyomo/radertext/Ex2_6a.py +++ b/examples/pyomo/radertext/Ex2_6a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/radertext/Ex2_6b.py b/examples/pyomo/radertext/Ex2_6b.py index 8049d4ebb05..1be55461b9e 100644 --- a/examples/pyomo/radertext/Ex2_6b.py +++ b/examples/pyomo/radertext/Ex2_6b.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/sos/DepotSiting.py b/examples/pyomo/sos/DepotSiting.py index 98697681f44..40826e989b7 100644 --- a/examples/pyomo/sos/DepotSiting.py +++ b/examples/pyomo/sos/DepotSiting.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/sos/basic_sos2_example.py b/examples/pyomo/sos/basic_sos2_example.py index 655169ffe54..3aa0887356c 100644 --- a/examples/pyomo/sos/basic_sos2_example.py +++ b/examples/pyomo/sos/basic_sos2_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/sos/sos2_piecewise.py b/examples/pyomo/sos/sos2_piecewise.py index 4e79ce2ee62..79195761f3d 100644 --- a/examples/pyomo/sos/sos2_piecewise.py +++ b/examples/pyomo/sos/sos2_piecewise.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/duals_pyomo.py b/examples/pyomo/suffixes/duals_pyomo.py index 9743add3ddd..6ce88fde429 100644 --- a/examples/pyomo/suffixes/duals_pyomo.py +++ b/examples/pyomo/suffixes/duals_pyomo.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/duals_script.py b/examples/pyomo/suffixes/duals_script.py index a9db615cad3..e8ef9aef1bc 100644 --- a/examples/pyomo/suffixes/duals_script.py +++ b/examples/pyomo/suffixes/duals_script.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/gurobi_ampl_basis.py b/examples/pyomo/suffixes/gurobi_ampl_basis.py index cd8e4e8f129..eab86f8aa47 100644 --- a/examples/pyomo/suffixes/gurobi_ampl_basis.py +++ b/examples/pyomo/suffixes/gurobi_ampl_basis.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/gurobi_ampl_example.py b/examples/pyomo/suffixes/gurobi_ampl_example.py index d133fa422dc..4f3364c09dc 100644 --- a/examples/pyomo/suffixes/gurobi_ampl_example.py +++ b/examples/pyomo/suffixes/gurobi_ampl_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/gurobi_ampl_iis.py b/examples/pyomo/suffixes/gurobi_ampl_iis.py index ccba226db78..da5bad073e7 100644 --- a/examples/pyomo/suffixes/gurobi_ampl_iis.py +++ b/examples/pyomo/suffixes/gurobi_ampl_iis.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/ipopt_scaling.py b/examples/pyomo/suffixes/ipopt_scaling.py index c192a98dd98..7113128c21d 100644 --- a/examples/pyomo/suffixes/ipopt_scaling.py +++ b/examples/pyomo/suffixes/ipopt_scaling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/ipopt_warmstart.py b/examples/pyomo/suffixes/ipopt_warmstart.py index 6975bbaaa62..4882c48c8c8 100644 --- a/examples/pyomo/suffixes/ipopt_warmstart.py +++ b/examples/pyomo/suffixes/ipopt_warmstart.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/sipopt_hicks.py b/examples/pyomo/suffixes/sipopt_hicks.py index dbf4e07b8f7..c7e058d5907 100644 --- a/examples/pyomo/suffixes/sipopt_hicks.py +++ b/examples/pyomo/suffixes/sipopt_hicks.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/suffixes/sipopt_parametric.py b/examples/pyomo/suffixes/sipopt_parametric.py index 29bba934bd8..0cb1c35f441 100644 --- a/examples/pyomo/suffixes/sipopt_parametric.py +++ b/examples/pyomo/suffixes/sipopt_parametric.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/transform/scaling_ex.py b/examples/pyomo/transform/scaling_ex.py index a5960393e75..34f937cbb45 100644 --- a/examples/pyomo/transform/scaling_ex.py +++ b/examples/pyomo/transform/scaling_ex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/tutorials/data.py b/examples/pyomo/tutorials/data.py index d065c9ff9bc..ea2569af934 100644 --- a/examples/pyomo/tutorials/data.py +++ b/examples/pyomo/tutorials/data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/tutorials/excel.py b/examples/pyomo/tutorials/excel.py index 127db722c07..f9a5f66826b 100644 --- a/examples/pyomo/tutorials/excel.py +++ b/examples/pyomo/tutorials/excel.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/tutorials/param.py b/examples/pyomo/tutorials/param.py index ba31975ab4b..5a94bafaa5e 100644 --- a/examples/pyomo/tutorials/param.py +++ b/examples/pyomo/tutorials/param.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/tutorials/set.py b/examples/pyomo/tutorials/set.py index 78f2656d739..a14301484c9 100644 --- a/examples/pyomo/tutorials/set.py +++ b/examples/pyomo/tutorials/set.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomo/tutorials/table.py b/examples/pyomo/tutorials/table.py index 16951352ee1..7d9fceda14a 100644 --- a/examples/pyomo/tutorials/table.py +++ b/examples/pyomo/tutorials/table.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/__init__.py b/examples/pyomobook/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/examples/pyomobook/__init__.py +++ b/examples/pyomobook/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/AbstHLinScript.py b/examples/pyomobook/abstract-ch/AbstHLinScript.py index 48946e0fb3d..687d3fc4e6b 100644 --- a/examples/pyomobook/abstract-ch/AbstHLinScript.py +++ b/examples/pyomobook/abstract-ch/AbstHLinScript.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/AbstractH.py b/examples/pyomobook/abstract-ch/AbstractH.py index cda8b489f28..7595cbc4933 100644 --- a/examples/pyomobook/abstract-ch/AbstractH.py +++ b/examples/pyomobook/abstract-ch/AbstractH.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/AbstractHLinear.py b/examples/pyomobook/abstract-ch/AbstractHLinear.py index 78ac4813709..f312020a9d5 100644 --- a/examples/pyomobook/abstract-ch/AbstractHLinear.py +++ b/examples/pyomobook/abstract-ch/AbstractHLinear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/abstract5.py b/examples/pyomobook/abstract-ch/abstract5.py index 20abd31dbd6..8849d2dfe7f 100644 --- a/examples/pyomobook/abstract-ch/abstract5.py +++ b/examples/pyomobook/abstract-ch/abstract5.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/abstract6.py b/examples/pyomobook/abstract-ch/abstract6.py index fdbbed88d25..121b12a51fa 100644 --- a/examples/pyomobook/abstract-ch/abstract6.py +++ b/examples/pyomobook/abstract-ch/abstract6.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/abstract7.py b/examples/pyomobook/abstract-ch/abstract7.py index 21d264d53e1..3e8131bf42b 100644 --- a/examples/pyomobook/abstract-ch/abstract7.py +++ b/examples/pyomobook/abstract-ch/abstract7.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/buildactions.py b/examples/pyomobook/abstract-ch/buildactions.py index a64de052176..6963f285c4c 100644 --- a/examples/pyomobook/abstract-ch/buildactions.py +++ b/examples/pyomobook/abstract-ch/buildactions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/concrete1.py b/examples/pyomobook/abstract-ch/concrete1.py index d2d6d09ac4f..2c89fbafaad 100644 --- a/examples/pyomobook/abstract-ch/concrete1.py +++ b/examples/pyomobook/abstract-ch/concrete1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/concrete2.py b/examples/pyomobook/abstract-ch/concrete2.py index d0500df53fa..f68c4d6e242 100644 --- a/examples/pyomobook/abstract-ch/concrete2.py +++ b/examples/pyomobook/abstract-ch/concrete2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/diet1.py b/examples/pyomobook/abstract-ch/diet1.py index 319bdec5144..fa8bf5f549f 100644 --- a/examples/pyomobook/abstract-ch/diet1.py +++ b/examples/pyomobook/abstract-ch/diet1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/ex.py b/examples/pyomobook/abstract-ch/ex.py index 2309f3330a0..83cfd445e01 100644 --- a/examples/pyomobook/abstract-ch/ex.py +++ b/examples/pyomobook/abstract-ch/ex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/param1.py b/examples/pyomobook/abstract-ch/param1.py index f5f34838215..3ff8b648661 100644 --- a/examples/pyomobook/abstract-ch/param1.py +++ b/examples/pyomobook/abstract-ch/param1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/param2.py b/examples/pyomobook/abstract-ch/param2.py index ac3b5b8bd27..aca8fac0baf 100644 --- a/examples/pyomobook/abstract-ch/param2.py +++ b/examples/pyomobook/abstract-ch/param2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/param2a.py b/examples/pyomobook/abstract-ch/param2a.py index 59f455bc290..6b6f77f2a8f 100644 --- a/examples/pyomobook/abstract-ch/param2a.py +++ b/examples/pyomobook/abstract-ch/param2a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/param3.py b/examples/pyomobook/abstract-ch/param3.py index 5c3462f2e64..7545b47dadc 100644 --- a/examples/pyomobook/abstract-ch/param3.py +++ b/examples/pyomobook/abstract-ch/param3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/param3a.py b/examples/pyomobook/abstract-ch/param3a.py index 25b575e3266..4c52b6432fb 100644 --- a/examples/pyomobook/abstract-ch/param3a.py +++ b/examples/pyomobook/abstract-ch/param3a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/param3b.py b/examples/pyomobook/abstract-ch/param3b.py index a4ad2d4ffc5..786d6b58a16 100644 --- a/examples/pyomobook/abstract-ch/param3b.py +++ b/examples/pyomobook/abstract-ch/param3b.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/param3c.py b/examples/pyomobook/abstract-ch/param3c.py index 96e2f4e88a4..3f5da5f837e 100644 --- a/examples/pyomobook/abstract-ch/param3c.py +++ b/examples/pyomobook/abstract-ch/param3c.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/param4.py b/examples/pyomobook/abstract-ch/param4.py index 4f427f44ee6..c1926ddea74 100644 --- a/examples/pyomobook/abstract-ch/param4.py +++ b/examples/pyomobook/abstract-ch/param4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/param5.py b/examples/pyomobook/abstract-ch/param5.py index 6cdb46db30d..7e0020f70b7 100644 --- a/examples/pyomobook/abstract-ch/param5.py +++ b/examples/pyomobook/abstract-ch/param5.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/param5a.py b/examples/pyomobook/abstract-ch/param5a.py index cd0187dabb1..efdd1855f3f 100644 --- a/examples/pyomobook/abstract-ch/param5a.py +++ b/examples/pyomobook/abstract-ch/param5a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/param6.py b/examples/pyomobook/abstract-ch/param6.py index e4cbb40f984..f6d60f11e4b 100644 --- a/examples/pyomobook/abstract-ch/param6.py +++ b/examples/pyomobook/abstract-ch/param6.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/param6a.py b/examples/pyomobook/abstract-ch/param6a.py index c2995ee864c..280e942d01d 100644 --- a/examples/pyomobook/abstract-ch/param6a.py +++ b/examples/pyomobook/abstract-ch/param6a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/param7a.py b/examples/pyomobook/abstract-ch/param7a.py index 3ed9163daec..21839bf3b64 100644 --- a/examples/pyomobook/abstract-ch/param7a.py +++ b/examples/pyomobook/abstract-ch/param7a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/param7b.py b/examples/pyomobook/abstract-ch/param7b.py index 59f5e28f979..a4d79b6dee9 100644 --- a/examples/pyomobook/abstract-ch/param7b.py +++ b/examples/pyomobook/abstract-ch/param7b.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/param8a.py b/examples/pyomobook/abstract-ch/param8a.py index 2e57f9c3bb7..f00ed649c30 100644 --- a/examples/pyomobook/abstract-ch/param8a.py +++ b/examples/pyomobook/abstract-ch/param8a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/postprocess_fn.py b/examples/pyomobook/abstract-ch/postprocess_fn.py index b54c11b5a0e..2f2d114c216 100644 --- a/examples/pyomobook/abstract-ch/postprocess_fn.py +++ b/examples/pyomobook/abstract-ch/postprocess_fn.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/set1.py b/examples/pyomobook/abstract-ch/set1.py index 6c549f61c49..5a23fe683e0 100644 --- a/examples/pyomobook/abstract-ch/set1.py +++ b/examples/pyomobook/abstract-ch/set1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/set2.py b/examples/pyomobook/abstract-ch/set2.py index bd7f98d5174..5ecc0914bee 100644 --- a/examples/pyomobook/abstract-ch/set2.py +++ b/examples/pyomobook/abstract-ch/set2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/set2a.py b/examples/pyomobook/abstract-ch/set2a.py index e6960396dd7..7252ec0ad69 100644 --- a/examples/pyomobook/abstract-ch/set2a.py +++ b/examples/pyomobook/abstract-ch/set2a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/set3.py b/examples/pyomobook/abstract-ch/set3.py index 4a3a27aa342..f3e3efc33c7 100644 --- a/examples/pyomobook/abstract-ch/set3.py +++ b/examples/pyomobook/abstract-ch/set3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/set4.py b/examples/pyomobook/abstract-ch/set4.py index 7d782cb268e..0c29798b816 100644 --- a/examples/pyomobook/abstract-ch/set4.py +++ b/examples/pyomobook/abstract-ch/set4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/set5.py b/examples/pyomobook/abstract-ch/set5.py index 7478316897a..781b956404e 100644 --- a/examples/pyomobook/abstract-ch/set5.py +++ b/examples/pyomobook/abstract-ch/set5.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/wl_abstract.py b/examples/pyomobook/abstract-ch/wl_abstract.py index 361729a1eff..61eeed6b506 100644 --- a/examples/pyomobook/abstract-ch/wl_abstract.py +++ b/examples/pyomobook/abstract-ch/wl_abstract.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/abstract-ch/wl_abstract_script.py b/examples/pyomobook/abstract-ch/wl_abstract_script.py index b70c6dbb8d2..7f0871350fc 100644 --- a/examples/pyomobook/abstract-ch/wl_abstract_script.py +++ b/examples/pyomobook/abstract-ch/wl_abstract_script.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/blocks-ch/blocks_gen.py b/examples/pyomobook/blocks-ch/blocks_gen.py index 7a74986ed81..31a4462f7d6 100644 --- a/examples/pyomobook/blocks-ch/blocks_gen.py +++ b/examples/pyomobook/blocks-ch/blocks_gen.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/blocks-ch/blocks_intro.py b/examples/pyomobook/blocks-ch/blocks_intro.py index 3160c29b385..ba2bd9d3a97 100644 --- a/examples/pyomobook/blocks-ch/blocks_intro.py +++ b/examples/pyomobook/blocks-ch/blocks_intro.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/blocks-ch/blocks_lotsizing.py b/examples/pyomobook/blocks-ch/blocks_lotsizing.py index 897ba9a4e5c..758ad964dc5 100644 --- a/examples/pyomobook/blocks-ch/blocks_lotsizing.py +++ b/examples/pyomobook/blocks-ch/blocks_lotsizing.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/blocks-ch/lotsizing.py b/examples/pyomobook/blocks-ch/lotsizing.py index 766c1892111..ece4d6b541c 100644 --- a/examples/pyomobook/blocks-ch/lotsizing.py +++ b/examples/pyomobook/blocks-ch/lotsizing.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/blocks-ch/lotsizing_no_time.py b/examples/pyomobook/blocks-ch/lotsizing_no_time.py index e0fa69922c1..60e8ba44424 100644 --- a/examples/pyomobook/blocks-ch/lotsizing_no_time.py +++ b/examples/pyomobook/blocks-ch/lotsizing_no_time.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/blocks-ch/lotsizing_uncertain.py b/examples/pyomobook/blocks-ch/lotsizing_uncertain.py index 9870d195841..f72161db5c6 100644 --- a/examples/pyomobook/blocks-ch/lotsizing_uncertain.py +++ b/examples/pyomobook/blocks-ch/lotsizing_uncertain.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/dae-ch/dae_tester_model.py b/examples/pyomobook/dae-ch/dae_tester_model.py index 00d51e8e05d..396b8a53db1 100644 --- a/examples/pyomobook/dae-ch/dae_tester_model.py +++ b/examples/pyomobook/dae-ch/dae_tester_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/dae-ch/path_constraint.py b/examples/pyomobook/dae-ch/path_constraint.py index 5fe41dd132d..5e252d1b99f 100644 --- a/examples/pyomobook/dae-ch/path_constraint.py +++ b/examples/pyomobook/dae-ch/path_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/dae-ch/plot_path_constraint.py b/examples/pyomobook/dae-ch/plot_path_constraint.py index d1af5c617ff..be86f13cbc0 100644 --- a/examples/pyomobook/dae-ch/plot_path_constraint.py +++ b/examples/pyomobook/dae-ch/plot_path_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/dae-ch/run_path_constraint.py b/examples/pyomobook/dae-ch/run_path_constraint.py index d4345e9e424..fc115f5649c 100644 --- a/examples/pyomobook/dae-ch/run_path_constraint.py +++ b/examples/pyomobook/dae-ch/run_path_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/dae-ch/run_path_constraint_tester.py b/examples/pyomobook/dae-ch/run_path_constraint_tester.py index d71c5126609..22d887e9b11 100644 --- a/examples/pyomobook/dae-ch/run_path_constraint_tester.py +++ b/examples/pyomobook/dae-ch/run_path_constraint_tester.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/gdp-ch/gdp_uc.py b/examples/pyomobook/gdp-ch/gdp_uc.py index 9f2562efad0..6268bcce068 100644 --- a/examples/pyomobook/gdp-ch/gdp_uc.py +++ b/examples/pyomobook/gdp-ch/gdp_uc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/gdp-ch/scont.py b/examples/pyomobook/gdp-ch/scont.py index 99beb042728..d1cf4b172bd 100644 --- a/examples/pyomobook/gdp-ch/scont.py +++ b/examples/pyomobook/gdp-ch/scont.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/gdp-ch/scont2.py b/examples/pyomobook/gdp-ch/scont2.py index cf392441487..2c77fe670d5 100644 --- a/examples/pyomobook/gdp-ch/scont2.py +++ b/examples/pyomobook/gdp-ch/scont2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/gdp-ch/scont_script.py b/examples/pyomobook/gdp-ch/scont_script.py index fee14bedaac..fe0702dc262 100644 --- a/examples/pyomobook/gdp-ch/scont_script.py +++ b/examples/pyomobook/gdp-ch/scont_script.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/gdp-ch/verify_scont.py b/examples/pyomobook/gdp-ch/verify_scont.py index a0acd3cf376..222453560b6 100644 --- a/examples/pyomobook/gdp-ch/verify_scont.py +++ b/examples/pyomobook/gdp-ch/verify_scont.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/intro-ch/abstract5.py b/examples/pyomobook/intro-ch/abstract5.py index b273d49b2ea..2caad5f9351 100644 --- a/examples/pyomobook/intro-ch/abstract5.py +++ b/examples/pyomobook/intro-ch/abstract5.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/intro-ch/coloring_concrete.py b/examples/pyomobook/intro-ch/coloring_concrete.py index 5b4baca99af..9931b5d80de 100644 --- a/examples/pyomobook/intro-ch/coloring_concrete.py +++ b/examples/pyomobook/intro-ch/coloring_concrete.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/intro-ch/concrete1.py b/examples/pyomobook/intro-ch/concrete1.py index c7aea6ff0b6..169fbeb281c 100644 --- a/examples/pyomobook/intro-ch/concrete1.py +++ b/examples/pyomobook/intro-ch/concrete1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/intro-ch/concrete1_generic.py b/examples/pyomobook/intro-ch/concrete1_generic.py index 183eb480fa1..9a2d26bded8 100644 --- a/examples/pyomobook/intro-ch/concrete1_generic.py +++ b/examples/pyomobook/intro-ch/concrete1_generic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/intro-ch/mydata.py b/examples/pyomobook/intro-ch/mydata.py index aaf8ec3d8be..209546ebeaf 100644 --- a/examples/pyomobook/intro-ch/mydata.py +++ b/examples/pyomobook/intro-ch/mydata.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/mpec-ch/ex1a.py b/examples/pyomobook/mpec-ch/ex1a.py index a57e714cd1c..e6f1c33fbbc 100644 --- a/examples/pyomobook/mpec-ch/ex1a.py +++ b/examples/pyomobook/mpec-ch/ex1a.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/mpec-ch/ex1b.py b/examples/pyomobook/mpec-ch/ex1b.py index 37a658f5294..2b0ac2ce1b7 100644 --- a/examples/pyomobook/mpec-ch/ex1b.py +++ b/examples/pyomobook/mpec-ch/ex1b.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/mpec-ch/ex1c.py b/examples/pyomobook/mpec-ch/ex1c.py index 35c0be9345d..eaf0292b50d 100644 --- a/examples/pyomobook/mpec-ch/ex1c.py +++ b/examples/pyomobook/mpec-ch/ex1c.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/mpec-ch/ex1d.py b/examples/pyomobook/mpec-ch/ex1d.py index 05105df265c..4c0e0d9fd0f 100644 --- a/examples/pyomobook/mpec-ch/ex1d.py +++ b/examples/pyomobook/mpec-ch/ex1d.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/mpec-ch/ex1e.py b/examples/pyomobook/mpec-ch/ex1e.py index 66831a58255..c552847fcfb 100644 --- a/examples/pyomobook/mpec-ch/ex1e.py +++ b/examples/pyomobook/mpec-ch/ex1e.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/mpec-ch/ex2.py b/examples/pyomobook/mpec-ch/ex2.py index 69d3813432d..6981af33376 100644 --- a/examples/pyomobook/mpec-ch/ex2.py +++ b/examples/pyomobook/mpec-ch/ex2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/mpec-ch/munson1.py b/examples/pyomobook/mpec-ch/munson1.py index 1c73c6279af..e85d9359768 100644 --- a/examples/pyomobook/mpec-ch/munson1.py +++ b/examples/pyomobook/mpec-ch/munson1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/mpec-ch/ralph1.py b/examples/pyomobook/mpec-ch/ralph1.py index 38ee803b1f1..b6a8b45e8df 100644 --- a/examples/pyomobook/mpec-ch/ralph1.py +++ b/examples/pyomobook/mpec-ch/ralph1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/nonlinear-ch/deer/DeerProblem.py b/examples/pyomobook/nonlinear-ch/deer/DeerProblem.py index 574a92ed0a2..dc3ca179a58 100644 --- a/examples/pyomobook/nonlinear-ch/deer/DeerProblem.py +++ b/examples/pyomobook/nonlinear-ch/deer/DeerProblem.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/nonlinear-ch/disease_est/disease_estimation.py b/examples/pyomobook/nonlinear-ch/disease_est/disease_estimation.py index 4b805b9cf7f..5675d7a715b 100644 --- a/examples/pyomobook/nonlinear-ch/disease_est/disease_estimation.py +++ b/examples/pyomobook/nonlinear-ch/disease_est/disease_estimation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init1.py b/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init1.py index 6cebe59a612..a50bf3321d6 100644 --- a/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init1.py +++ b/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init2.py b/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init2.py index a2c9d9c5a60..6a209334521 100644 --- a/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init2.py +++ b/examples/pyomobook/nonlinear-ch/multimodal/multimodal_init2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/nonlinear-ch/react_design/ReactorDesign.py b/examples/pyomobook/nonlinear-ch/react_design/ReactorDesign.py index c3115f396ce..1cfe3b7193f 100644 --- a/examples/pyomobook/nonlinear-ch/react_design/ReactorDesign.py +++ b/examples/pyomobook/nonlinear-ch/react_design/ReactorDesign.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/nonlinear-ch/react_design/ReactorDesignTable.py b/examples/pyomobook/nonlinear-ch/react_design/ReactorDesignTable.py index c748cd7d41e..2bd9574b427 100644 --- a/examples/pyomobook/nonlinear-ch/react_design/ReactorDesignTable.py +++ b/examples/pyomobook/nonlinear-ch/react_design/ReactorDesignTable.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/nonlinear-ch/rosen/rosenbrock.py b/examples/pyomobook/nonlinear-ch/rosen/rosenbrock.py index 3d14d15aa93..bec1d04c12c 100644 --- a/examples/pyomobook/nonlinear-ch/rosen/rosenbrock.py +++ b/examples/pyomobook/nonlinear-ch/rosen/rosenbrock.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/optimization-ch/ConcHLinScript.py b/examples/pyomobook/optimization-ch/ConcHLinScript.py index f4f5fac6b6c..b94903585dc 100644 --- a/examples/pyomobook/optimization-ch/ConcHLinScript.py +++ b/examples/pyomobook/optimization-ch/ConcHLinScript.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/optimization-ch/ConcreteH.py b/examples/pyomobook/optimization-ch/ConcreteH.py index 6cb3f7c5052..d7474291d0d 100644 --- a/examples/pyomobook/optimization-ch/ConcreteH.py +++ b/examples/pyomobook/optimization-ch/ConcreteH.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/optimization-ch/ConcreteHLinear.py b/examples/pyomobook/optimization-ch/ConcreteHLinear.py index 3cc7478f1c9..772c18cb6d5 100644 --- a/examples/pyomobook/optimization-ch/ConcreteHLinear.py +++ b/examples/pyomobook/optimization-ch/ConcreteHLinear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/optimization-ch/IC_model_dict.py b/examples/pyomobook/optimization-ch/IC_model_dict.py index a76f19797af..b7e359777c7 100644 --- a/examples/pyomobook/optimization-ch/IC_model_dict.py +++ b/examples/pyomobook/optimization-ch/IC_model_dict.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/overview-ch/var_obj_con_snippet.py b/examples/pyomobook/overview-ch/var_obj_con_snippet.py index e979e4b18de..22524b5815a 100644 --- a/examples/pyomobook/overview-ch/var_obj_con_snippet.py +++ b/examples/pyomobook/overview-ch/var_obj_con_snippet.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/overview-ch/wl_abstract.py b/examples/pyomobook/overview-ch/wl_abstract.py index 361729a1eff..61eeed6b506 100644 --- a/examples/pyomobook/overview-ch/wl_abstract.py +++ b/examples/pyomobook/overview-ch/wl_abstract.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/overview-ch/wl_abstract_script.py b/examples/pyomobook/overview-ch/wl_abstract_script.py index b70c6dbb8d2..7f0871350fc 100644 --- a/examples/pyomobook/overview-ch/wl_abstract_script.py +++ b/examples/pyomobook/overview-ch/wl_abstract_script.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/overview-ch/wl_concrete.py b/examples/pyomobook/overview-ch/wl_concrete.py index da32c7ba5bf..c1bf70b07f1 100644 --- a/examples/pyomobook/overview-ch/wl_concrete.py +++ b/examples/pyomobook/overview-ch/wl_concrete.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/overview-ch/wl_concrete_script.py b/examples/pyomobook/overview-ch/wl_concrete_script.py index 59baa241718..b369521994c 100644 --- a/examples/pyomobook/overview-ch/wl_concrete_script.py +++ b/examples/pyomobook/overview-ch/wl_concrete_script.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/overview-ch/wl_excel.py b/examples/pyomobook/overview-ch/wl_excel.py index 777412abb23..180e36422fe 100644 --- a/examples/pyomobook/overview-ch/wl_excel.py +++ b/examples/pyomobook/overview-ch/wl_excel.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/overview-ch/wl_list.py b/examples/pyomobook/overview-ch/wl_list.py index 375a1c7400e..37cba5a9595 100644 --- a/examples/pyomobook/overview-ch/wl_list.py +++ b/examples/pyomobook/overview-ch/wl_list.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/overview-ch/wl_mutable.py b/examples/pyomobook/overview-ch/wl_mutable.py index 1b65dcc84a1..8e129dd3c49 100644 --- a/examples/pyomobook/overview-ch/wl_mutable.py +++ b/examples/pyomobook/overview-ch/wl_mutable.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/overview-ch/wl_mutable_excel.py b/examples/pyomobook/overview-ch/wl_mutable_excel.py index 52cac31f5f6..935fa4963e5 100644 --- a/examples/pyomobook/overview-ch/wl_mutable_excel.py +++ b/examples/pyomobook/overview-ch/wl_mutable_excel.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/overview-ch/wl_scalar.py b/examples/pyomobook/overview-ch/wl_scalar.py index b524f22c82d..6f538baedb8 100644 --- a/examples/pyomobook/overview-ch/wl_scalar.py +++ b/examples/pyomobook/overview-ch/wl_scalar.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/performance-ch/SparseSets.py b/examples/pyomobook/performance-ch/SparseSets.py index 913b7587368..519808306de 100644 --- a/examples/pyomobook/performance-ch/SparseSets.py +++ b/examples/pyomobook/performance-ch/SparseSets.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/performance-ch/lin_expr.py b/examples/pyomobook/performance-ch/lin_expr.py index 20585d4719b..af50ddd6228 100644 --- a/examples/pyomobook/performance-ch/lin_expr.py +++ b/examples/pyomobook/performance-ch/lin_expr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/performance-ch/persistent.py b/examples/pyomobook/performance-ch/persistent.py index e468b281579..67f8c656cfe 100644 --- a/examples/pyomobook/performance-ch/persistent.py +++ b/examples/pyomobook/performance-ch/persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/performance-ch/wl.py b/examples/pyomobook/performance-ch/wl.py index 614ffc0fd66..000f81272a1 100644 --- a/examples/pyomobook/performance-ch/wl.py +++ b/examples/pyomobook/performance-ch/wl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/con_declaration.py b/examples/pyomobook/pyomo-components-ch/con_declaration.py index b014697fd62..0890ba4771b 100644 --- a/examples/pyomobook/pyomo-components-ch/con_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/con_declaration.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/examples.py b/examples/pyomobook/pyomo-components-ch/examples.py index 5f154c0ecc9..1a59e9e308e 100644 --- a/examples/pyomobook/pyomo-components-ch/examples.py +++ b/examples/pyomobook/pyomo-components-ch/examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/expr_declaration.py b/examples/pyomobook/pyomo-components-ch/expr_declaration.py index 9baff1e4dba..da0d854e513 100644 --- a/examples/pyomobook/pyomo-components-ch/expr_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/expr_declaration.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/obj_declaration.py b/examples/pyomobook/pyomo-components-ch/obj_declaration.py index ac8b56a3a03..a63fc441206 100644 --- a/examples/pyomobook/pyomo-components-ch/obj_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/obj_declaration.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/param_declaration.py b/examples/pyomobook/pyomo-components-ch/param_declaration.py index ded0adfcb22..98b16548c28 100644 --- a/examples/pyomobook/pyomo-components-ch/param_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/param_declaration.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/param_initialization.py b/examples/pyomobook/pyomo-components-ch/param_initialization.py index e9a90210df5..88da8a68354 100644 --- a/examples/pyomobook/pyomo-components-ch/param_initialization.py +++ b/examples/pyomobook/pyomo-components-ch/param_initialization.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/param_misc.py b/examples/pyomobook/pyomo-components-ch/param_misc.py index cc3be7a6ac5..72fca60f787 100644 --- a/examples/pyomobook/pyomo-components-ch/param_misc.py +++ b/examples/pyomobook/pyomo-components-ch/param_misc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/param_validation.py b/examples/pyomobook/pyomo-components-ch/param_validation.py index cf540ac8a70..baf5f0ac1e2 100644 --- a/examples/pyomobook/pyomo-components-ch/param_validation.py +++ b/examples/pyomobook/pyomo-components-ch/param_validation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/rangeset.py b/examples/pyomobook/pyomo-components-ch/rangeset.py index a5ef4a85017..169060e9ab2 100644 --- a/examples/pyomobook/pyomo-components-ch/rangeset.py +++ b/examples/pyomobook/pyomo-components-ch/rangeset.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/set_declaration.py b/examples/pyomobook/pyomo-components-ch/set_declaration.py index a60904ff510..bf3cfa1be15 100644 --- a/examples/pyomobook/pyomo-components-ch/set_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/set_declaration.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/set_initialization.py b/examples/pyomobook/pyomo-components-ch/set_initialization.py index 972d65e0499..bdfd662c985 100644 --- a/examples/pyomobook/pyomo-components-ch/set_initialization.py +++ b/examples/pyomobook/pyomo-components-ch/set_initialization.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/set_misc.py b/examples/pyomobook/pyomo-components-ch/set_misc.py index 2bd8297cc80..20ed9518f52 100644 --- a/examples/pyomobook/pyomo-components-ch/set_misc.py +++ b/examples/pyomobook/pyomo-components-ch/set_misc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/set_options.py b/examples/pyomobook/pyomo-components-ch/set_options.py index 27c47ee95c7..30c0b49706d 100644 --- a/examples/pyomobook/pyomo-components-ch/set_options.py +++ b/examples/pyomobook/pyomo-components-ch/set_options.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/set_validation.py b/examples/pyomobook/pyomo-components-ch/set_validation.py index 3b6b8bee25b..2300c0be693 100644 --- a/examples/pyomobook/pyomo-components-ch/set_validation.py +++ b/examples/pyomobook/pyomo-components-ch/set_validation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/suffix_declaration.py b/examples/pyomobook/pyomo-components-ch/suffix_declaration.py index a5c0bc988bb..619093712f1 100644 --- a/examples/pyomobook/pyomo-components-ch/suffix_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/suffix_declaration.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/pyomo-components-ch/var_declaration.py b/examples/pyomobook/pyomo-components-ch/var_declaration.py index b3180f25381..2ee5d7fb749 100644 --- a/examples/pyomobook/pyomo-components-ch/var_declaration.py +++ b/examples/pyomobook/pyomo-components-ch/var_declaration.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/python-ch/BadIndent.py b/examples/pyomobook/python-ch/BadIndent.py index 63013067468..4a00cae12ef 100644 --- a/examples/pyomobook/python-ch/BadIndent.py +++ b/examples/pyomobook/python-ch/BadIndent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/python-ch/LineExample.py b/examples/pyomobook/python-ch/LineExample.py index 320289a2a79..31cface5760 100644 --- a/examples/pyomobook/python-ch/LineExample.py +++ b/examples/pyomobook/python-ch/LineExample.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/python-ch/class.py b/examples/pyomobook/python-ch/class.py index 12eafe23a44..a09f991d37b 100644 --- a/examples/pyomobook/python-ch/class.py +++ b/examples/pyomobook/python-ch/class.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/python-ch/ctob.py b/examples/pyomobook/python-ch/ctob.py index fe2c474de4d..8945e4863de 100644 --- a/examples/pyomobook/python-ch/ctob.py +++ b/examples/pyomobook/python-ch/ctob.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/python-ch/example.py b/examples/pyomobook/python-ch/example.py index 2bab6d4b9fe..184153545a3 100644 --- a/examples/pyomobook/python-ch/example.py +++ b/examples/pyomobook/python-ch/example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/python-ch/example2.py b/examples/pyomobook/python-ch/example2.py index 0c282eccacd..9a6a28bedbd 100644 --- a/examples/pyomobook/python-ch/example2.py +++ b/examples/pyomobook/python-ch/example2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/python-ch/functions.py b/examples/pyomobook/python-ch/functions.py index b23b6dc6bee..97fb77edbe4 100644 --- a/examples/pyomobook/python-ch/functions.py +++ b/examples/pyomobook/python-ch/functions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/python-ch/iterate.py b/examples/pyomobook/python-ch/iterate.py index cd8fe697afb..50d74f93da7 100644 --- a/examples/pyomobook/python-ch/iterate.py +++ b/examples/pyomobook/python-ch/iterate.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/python-ch/pythonconditional.py b/examples/pyomobook/python-ch/pythonconditional.py index 2c48a2db6f4..a39e148622b 100644 --- a/examples/pyomobook/python-ch/pythonconditional.py +++ b/examples/pyomobook/python-ch/pythonconditional.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/scripts-ch/attributes.py b/examples/pyomobook/scripts-ch/attributes.py index fccdb6932da..c406bbf3e1c 100644 --- a/examples/pyomobook/scripts-ch/attributes.py +++ b/examples/pyomobook/scripts-ch/attributes.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/scripts-ch/prob_mod_ex.py b/examples/pyomobook/scripts-ch/prob_mod_ex.py index f94fec5eb8a..dceafe9d4f0 100644 --- a/examples/pyomobook/scripts-ch/prob_mod_ex.py +++ b/examples/pyomobook/scripts-ch/prob_mod_ex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/scripts-ch/sudoku/sudoku.py b/examples/pyomobook/scripts-ch/sudoku/sudoku.py index ac6d1eabf14..8aa39f91203 100644 --- a/examples/pyomobook/scripts-ch/sudoku/sudoku.py +++ b/examples/pyomobook/scripts-ch/sudoku/sudoku.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/scripts-ch/sudoku/sudoku_run.py b/examples/pyomobook/scripts-ch/sudoku/sudoku_run.py index 948c5a59ee8..b3f861f86b5 100644 --- a/examples/pyomobook/scripts-ch/sudoku/sudoku_run.py +++ b/examples/pyomobook/scripts-ch/sudoku/sudoku_run.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/scripts-ch/value_expression.py b/examples/pyomobook/scripts-ch/value_expression.py index ca154341b43..00c79fec501 100644 --- a/examples/pyomobook/scripts-ch/value_expression.py +++ b/examples/pyomobook/scripts-ch/value_expression.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/scripts-ch/warehouse_cuts.py b/examples/pyomobook/scripts-ch/warehouse_cuts.py index 82dabfcb6f8..345dc5540cb 100644 --- a/examples/pyomobook/scripts-ch/warehouse_cuts.py +++ b/examples/pyomobook/scripts-ch/warehouse_cuts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/scripts-ch/warehouse_load_solutions.py b/examples/pyomobook/scripts-ch/warehouse_load_solutions.py index 4d47a8ab916..d38412f84df 100644 --- a/examples/pyomobook/scripts-ch/warehouse_load_solutions.py +++ b/examples/pyomobook/scripts-ch/warehouse_load_solutions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/scripts-ch/warehouse_model.py b/examples/pyomobook/scripts-ch/warehouse_model.py index cb9a43563fb..149eb212759 100644 --- a/examples/pyomobook/scripts-ch/warehouse_model.py +++ b/examples/pyomobook/scripts-ch/warehouse_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/scripts-ch/warehouse_print.py b/examples/pyomobook/scripts-ch/warehouse_print.py index 2353a8d6b44..8c862506bf0 100644 --- a/examples/pyomobook/scripts-ch/warehouse_print.py +++ b/examples/pyomobook/scripts-ch/warehouse_print.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/scripts-ch/warehouse_script.py b/examples/pyomobook/scripts-ch/warehouse_script.py index 37d71b466d2..617b8036abf 100644 --- a/examples/pyomobook/scripts-ch/warehouse_script.py +++ b/examples/pyomobook/scripts-ch/warehouse_script.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/scripts-ch/warehouse_solver_options.py b/examples/pyomobook/scripts-ch/warehouse_solver_options.py index 5a482bf3216..4e79e158d50 100644 --- a/examples/pyomobook/scripts-ch/warehouse_solver_options.py +++ b/examples/pyomobook/scripts-ch/warehouse_solver_options.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/strip_examples.py b/examples/pyomobook/strip_examples.py index 68d9e0d99a5..84017299fb6 100644 --- a/examples/pyomobook/strip_examples.py +++ b/examples/pyomobook/strip_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/examples/pyomobook/test_book_examples.py b/examples/pyomobook/test_book_examples.py index e946864c1aa..192330dc1bf 100644 --- a/examples/pyomobook/test_book_examples.py +++ b/examples/pyomobook/test_book_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/__init__.py b/pyomo/__init__.py index 20ee59d48b2..14cc42b626e 100644 --- a/pyomo/__init__.py +++ b/pyomo/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/__init__.py b/pyomo/common/__init__.py index 563974b5617..d7297c067c9 100644 --- a/pyomo/common/__init__.py +++ b/pyomo/common/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/_command.py b/pyomo/common/_command.py index ae633648ace..0777155a557 100644 --- a/pyomo/common/_command.py +++ b/pyomo/common/_command.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/_common.py b/pyomo/common/_common.py index 21a5ddcc7bc..0d50f74537a 100644 --- a/pyomo/common/_common.py +++ b/pyomo/common/_common.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/autoslots.py b/pyomo/common/autoslots.py index 1b55a818b83..cb79d4a0338 100644 --- a/pyomo/common/autoslots.py +++ b/pyomo/common/autoslots.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/backports.py b/pyomo/common/backports.py index 36f2dac87ab..e70b0f6d267 100644 --- a/pyomo/common/backports.py +++ b/pyomo/common/backports.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/cmake_builder.py b/pyomo/common/cmake_builder.py index bb612b43b72..523dbf64c91 100644 --- a/pyomo/common/cmake_builder.py +++ b/pyomo/common/cmake_builder.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/collections/__init__.py b/pyomo/common/collections/__init__.py index 9ffd1e931f6..93785124e3c 100644 --- a/pyomo/common/collections/__init__.py +++ b/pyomo/common/collections/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/collections/bunch.py b/pyomo/common/collections/bunch.py index f19e4ad64e3..2ae9cf8c517 100644 --- a/pyomo/common/collections/bunch.py +++ b/pyomo/common/collections/bunch.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index 41796876d7c..80ba5fe0d1c 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index e205773220f..dfeac5cbfa5 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/collections/orderedset.py b/pyomo/common/collections/orderedset.py index 448939c8822..f29245b75fe 100644 --- a/pyomo/common/collections/orderedset.py +++ b/pyomo/common/collections/orderedset.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 15f15872fc6..2e14359d1af 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 0a179b5c2de..9e96fdd5860 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/deprecation.py b/pyomo/common/deprecation.py index 2e39083770d..5a6ca456079 100644 --- a/pyomo/common/deprecation.py +++ b/pyomo/common/deprecation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/download.py b/pyomo/common/download.py index 79d5302a58e..5332287cfc7 100644 --- a/pyomo/common/download.py +++ b/pyomo/common/download.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/env.py b/pyomo/common/env.py index 2ce0f368b9e..ee07cdc1e6a 100644 --- a/pyomo/common/env.py +++ b/pyomo/common/env.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/envvar.py b/pyomo/common/envvar.py index d74cb764641..1f933d4b08c 100644 --- a/pyomo/common/envvar.py +++ b/pyomo/common/envvar.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/errors.py b/pyomo/common/errors.py index 17013ce4dca..3c82f2b07c1 100644 --- a/pyomo/common/errors.py +++ b/pyomo/common/errors.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/extensions.py b/pyomo/common/extensions.py index e4f7b047bb3..0ac27f125a7 100644 --- a/pyomo/common/extensions.py +++ b/pyomo/common/extensions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/factory.py b/pyomo/common/factory.py index 6a97759c714..c449cf826b4 100644 --- a/pyomo/common/factory.py +++ b/pyomo/common/factory.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/fileutils.py b/pyomo/common/fileutils.py index 557901c401e..2cade36154d 100644 --- a/pyomo/common/fileutils.py +++ b/pyomo/common/fileutils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index 5c2b329ce21..430ec96ca09 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/gc_manager.py b/pyomo/common/gc_manager.py index 54fbca32736..751eb95cf18 100644 --- a/pyomo/common/gc_manager.py +++ b/pyomo/common/gc_manager.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/getGSL.py b/pyomo/common/getGSL.py index e8b2507ab81..66b75b45665 100644 --- a/pyomo/common/getGSL.py +++ b/pyomo/common/getGSL.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/gsl.py b/pyomo/common/gsl.py index 5243758a0de..1c14b64bd70 100644 --- a/pyomo/common/gsl.py +++ b/pyomo/common/gsl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/log.py b/pyomo/common/log.py index 3097fe1c6de..d61ed62f373 100644 --- a/pyomo/common/log.py +++ b/pyomo/common/log.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/modeling.py b/pyomo/common/modeling.py index 5ecc56cce9b..4c07048d77a 100644 --- a/pyomo/common/modeling.py +++ b/pyomo/common/modeling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/multithread.py b/pyomo/common/multithread.py index f90e7f7c89e..a2dace2be0f 100644 --- a/pyomo/common/multithread.py +++ b/pyomo/common/multithread.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index 19718b308b6..ba104203667 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/plugin.py b/pyomo/common/plugin.py index b48fa96a483..ac88388ebc0 100644 --- a/pyomo/common/plugin.py +++ b/pyomo/common/plugin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/plugin_base.py b/pyomo/common/plugin_base.py index 67960ebbb12..75b8657d1a9 100644 --- a/pyomo/common/plugin_base.py +++ b/pyomo/common/plugin_base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/plugins.py b/pyomo/common/plugins.py index 7db8077855a..ed44f8bf776 100644 --- a/pyomo/common/plugins.py +++ b/pyomo/common/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/pyomo_typing.py b/pyomo/common/pyomo_typing.py index 64ab2ddafc9..22ec3480842 100644 --- a/pyomo/common/pyomo_typing.py +++ b/pyomo/common/pyomo_typing.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/shutdown.py b/pyomo/common/shutdown.py index 984fa8e8a52..a96a6bc04fc 100644 --- a/pyomo/common/shutdown.py +++ b/pyomo/common/shutdown.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/sorting.py b/pyomo/common/sorting.py index 31e796c6a9e..4f78a7892b8 100644 --- a/pyomo/common/sorting.py +++ b/pyomo/common/sorting.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tee.py b/pyomo/common/tee.py index 029d66f5767..500f7b6f58d 100644 --- a/pyomo/common/tee.py +++ b/pyomo/common/tee.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tempfiles.py b/pyomo/common/tempfiles.py index f51fad3f3ac..b9dface71b2 100644 --- a/pyomo/common/tempfiles.py +++ b/pyomo/common/tempfiles.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/__init__.py b/pyomo/common/tests/__init__.py index bc8dfa27c9c..d8d8856e52f 100644 --- a/pyomo/common/tests/__init__.py +++ b/pyomo/common/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/config_plugin.py b/pyomo/common/tests/config_plugin.py index ada788fd7d4..6aebc40806a 100644 --- a/pyomo/common/tests/config_plugin.py +++ b/pyomo/common/tests/config_plugin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/dep_mod.py b/pyomo/common/tests/dep_mod.py index 54530393783..f6add596ed4 100644 --- a/pyomo/common/tests/dep_mod.py +++ b/pyomo/common/tests/dep_mod.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/dep_mod_except.py b/pyomo/common/tests/dep_mod_except.py index 8132e8a08ac..16936996eeb 100644 --- a/pyomo/common/tests/dep_mod_except.py +++ b/pyomo/common/tests/dep_mod_except.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/deps.py b/pyomo/common/tests/deps.py index e5236d0f7ec..d00281553f4 100644 --- a/pyomo/common/tests/deps.py +++ b/pyomo/common/tests/deps.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/import_ex.py b/pyomo/common/tests/import_ex.py index d1bf02752eb..73375bdc819 100644 --- a/pyomo/common/tests/import_ex.py +++ b/pyomo/common/tests/import_ex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/relo_mod.py b/pyomo/common/tests/relo_mod.py index 20b0712e09b..4881caba671 100644 --- a/pyomo/common/tests/relo_mod.py +++ b/pyomo/common/tests/relo_mod.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/relo_mod_new.py b/pyomo/common/tests/relo_mod_new.py index 1ef27681b66..0f59f3beebc 100644 --- a/pyomo/common/tests/relo_mod_new.py +++ b/pyomo/common/tests/relo_mod_new.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/relocated.py b/pyomo/common/tests/relocated.py index 9de63e0cec9..90cb28c23ba 100644 --- a/pyomo/common/tests/relocated.py +++ b/pyomo/common/tests/relocated.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_bunch.py b/pyomo/common/tests/test_bunch.py index a8daf5a0071..8c10df83005 100644 --- a/pyomo/common/tests/test_bunch.py +++ b/pyomo/common/tests/test_bunch.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 1b732d86c0a..0cc71169a34 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_dependencies.py b/pyomo/common/tests/test_dependencies.py index 65058e01812..30822a4f81f 100644 --- a/pyomo/common/tests/test_dependencies.py +++ b/pyomo/common/tests/test_dependencies.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_deprecated.py b/pyomo/common/tests/test_deprecated.py index 1fb4a471740..377e229c775 100644 --- a/pyomo/common/tests/test_deprecated.py +++ b/pyomo/common/tests/test_deprecated.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_download.py b/pyomo/common/tests/test_download.py index 8c41edc1512..87108be1c59 100644 --- a/pyomo/common/tests/test_download.py +++ b/pyomo/common/tests/test_download.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_env.py b/pyomo/common/tests/test_env.py index d14326ddc19..93802fc40bb 100644 --- a/pyomo/common/tests/test_env.py +++ b/pyomo/common/tests/test_env.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_errors.py b/pyomo/common/tests/test_errors.py index ec77643f722..67a200e84e3 100644 --- a/pyomo/common/tests/test_errors.py +++ b/pyomo/common/tests/test_errors.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_fileutils.py b/pyomo/common/tests/test_fileutils.py index 63570774e5b..068360b55cb 100644 --- a/pyomo/common/tests/test_fileutils.py +++ b/pyomo/common/tests/test_fileutils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_formatting.py b/pyomo/common/tests/test_formatting.py index d502c81da5a..29db26676ab 100644 --- a/pyomo/common/tests/test_formatting.py +++ b/pyomo/common/tests/test_formatting.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_gc.py b/pyomo/common/tests/test_gc.py index b2f23102a0e..176010b8d0d 100644 --- a/pyomo/common/tests/test_gc.py +++ b/pyomo/common/tests/test_gc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_log.py b/pyomo/common/tests/test_log.py index 39fab153e98..64691c0015a 100644 --- a/pyomo/common/tests/test_log.py +++ b/pyomo/common/tests/test_log.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_modeling.py b/pyomo/common/tests/test_modeling.py index 0684d77b2e9..97bef76c2c0 100644 --- a/pyomo/common/tests/test_modeling.py +++ b/pyomo/common/tests/test_modeling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_multithread.py b/pyomo/common/tests/test_multithread.py index a6c0cac32c7..fa1a46fa25f 100644 --- a/pyomo/common/tests/test_multithread.py +++ b/pyomo/common/tests/test_multithread.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_orderedset.py b/pyomo/common/tests/test_orderedset.py index d87bebc1e4a..8f944e66bd7 100644 --- a/pyomo/common/tests/test_orderedset.py +++ b/pyomo/common/tests/test_orderedset.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_plugin.py b/pyomo/common/tests/test_plugin.py index 86d136dd9d1..54431334d5b 100644 --- a/pyomo/common/tests/test_plugin.py +++ b/pyomo/common/tests/test_plugin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_sorting.py b/pyomo/common/tests/test_sorting.py index 7a9fe5ac923..7fbefda6a19 100644 --- a/pyomo/common/tests/test_sorting.py +++ b/pyomo/common/tests/test_sorting.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_tee.py b/pyomo/common/tests/test_tee.py index 666a431631f..a5c6ee894b2 100644 --- a/pyomo/common/tests/test_tee.py +++ b/pyomo/common/tests/test_tee.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_tempfile.py b/pyomo/common/tests/test_tempfile.py index 5e75c55305a..c49aa8c6771 100644 --- a/pyomo/common/tests/test_tempfile.py +++ b/pyomo/common/tests/test_tempfile.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_timing.py b/pyomo/common/tests/test_timing.py index d885359e6c6..0a4224c5476 100644 --- a/pyomo/common/tests/test_timing.py +++ b/pyomo/common/tests/test_timing.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_typing.py b/pyomo/common/tests/test_typing.py index 982462f8a8d..e65effe7f29 100644 --- a/pyomo/common/tests/test_typing.py +++ b/pyomo/common/tests/test_typing.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/tests/test_unittest.py b/pyomo/common/tests/test_unittest.py index e3779e6f86e..9344853b737 100644 --- a/pyomo/common/tests/test_unittest.py +++ b/pyomo/common/tests/test_unittest.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/timing.py b/pyomo/common/timing.py index b37570fa666..d502b38d12d 100644 --- a/pyomo/common/timing.py +++ b/pyomo/common/timing.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/common/unittest.py b/pyomo/common/unittest.py index 1ed26f72320..9a21b35faa8 100644 --- a/pyomo/common/unittest.py +++ b/pyomo/common/unittest.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/__init__.py b/pyomo/contrib/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/__init__.py +++ b/pyomo/contrib/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/ampl_function_demo/__init__.py b/pyomo/contrib/ampl_function_demo/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/ampl_function_demo/__init__.py +++ b/pyomo/contrib/ampl_function_demo/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/ampl_function_demo/build.py b/pyomo/contrib/ampl_function_demo/build.py index cd35064ea4e..764a613b3d7 100644 --- a/pyomo/contrib/ampl_function_demo/build.py +++ b/pyomo/contrib/ampl_function_demo/build.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/ampl_function_demo/plugins.py b/pyomo/contrib/ampl_function_demo/plugins.py index 230d9c4b667..5a200174c43 100644 --- a/pyomo/contrib/ampl_function_demo/plugins.py +++ b/pyomo/contrib/ampl_function_demo/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/ampl_function_demo/src/CMakeLists.txt b/pyomo/contrib/ampl_function_demo/src/CMakeLists.txt index ce2c1a60f82..67efc13d3c8 100644 --- a/pyomo/contrib/ampl_function_demo/src/CMakeLists.txt +++ b/pyomo/contrib/ampl_function_demo/src/CMakeLists.txt @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/ampl_function_demo/src/FindASL.cmake b/pyomo/contrib/ampl_function_demo/src/FindASL.cmake index f413176f1cc..8bbc048fa6e 100644 --- a/pyomo/contrib/ampl_function_demo/src/FindASL.cmake +++ b/pyomo/contrib/ampl_function_demo/src/FindASL.cmake @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/ampl_function_demo/src/functions.c b/pyomo/contrib/ampl_function_demo/src/functions.c index f62148c995a..e87af745aea 100644 --- a/pyomo/contrib/ampl_function_demo/src/functions.c +++ b/pyomo/contrib/ampl_function_demo/src/functions.c @@ -1,6 +1,6 @@ /* ___________________________________________________________________________ * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/ampl_function_demo/tests/__init__.py b/pyomo/contrib/ampl_function_demo/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/ampl_function_demo/tests/__init__.py +++ b/pyomo/contrib/ampl_function_demo/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/ampl_function_demo/tests/test_ampl_function_demo.py b/pyomo/contrib/ampl_function_demo/tests/test_ampl_function_demo.py index af52c2def9f..39890494d55 100644 --- a/pyomo/contrib/ampl_function_demo/tests/test_ampl_function_demo.py +++ b/pyomo/contrib/ampl_function_demo/tests/test_ampl_function_demo.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/__init__.py b/pyomo/contrib/appsi/__init__.py index 305231001c4..2f06fc89e70 100644 --- a/pyomo/contrib/appsi/__init__.py +++ b/pyomo/contrib/appsi/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index a34bbdb5e1f..80e3cecec6d 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/build.py b/pyomo/contrib/appsi/build.py index 2c8d02dd3ac..b3d78467f01 100644 --- a/pyomo/contrib/appsi/build.py +++ b/pyomo/contrib/appsi/build.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/__init__.py b/pyomo/contrib/appsi/cmodel/__init__.py index 9c276b518de..cc2aec28241 100644 --- a/pyomo/contrib/appsi/cmodel/__init__.py +++ b/pyomo/contrib/appsi/cmodel/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp b/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp index db9d3112069..6acc1d79845 100644 --- a/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp +++ b/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/common.cpp b/pyomo/contrib/appsi/cmodel/src/common.cpp index e9f1398fa2f..6f8002cb50e 100644 --- a/pyomo/contrib/appsi/cmodel/src/common.cpp +++ b/pyomo/contrib/appsi/cmodel/src/common.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/common.hpp b/pyomo/contrib/appsi/cmodel/src/common.hpp index 9a025e031ae..9edc9571a4d 100644 --- a/pyomo/contrib/appsi/cmodel/src/common.hpp +++ b/pyomo/contrib/appsi/cmodel/src/common.hpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/expression.cpp b/pyomo/contrib/appsi/cmodel/src/expression.cpp index f9e6b5c326a..234ef47e86f 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.cpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/expression.hpp b/pyomo/contrib/appsi/cmodel/src/expression.hpp index 220f5f22b0d..0c0777ef468 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.hpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.hpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp b/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp index 68efa7d9c26..bd8d7dbf854 100644 --- a/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp +++ b/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/fbbt_model.hpp b/pyomo/contrib/appsi/cmodel/src/fbbt_model.hpp index 032ff8c2616..ca1980a797b 100644 --- a/pyomo/contrib/appsi/cmodel/src/fbbt_model.hpp +++ b/pyomo/contrib/appsi/cmodel/src/fbbt_model.hpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/interval.cpp b/pyomo/contrib/appsi/cmodel/src/interval.cpp index a9f26704825..1d9b3a6f82e 100644 --- a/pyomo/contrib/appsi/cmodel/src/interval.cpp +++ b/pyomo/contrib/appsi/cmodel/src/interval.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/interval.hpp b/pyomo/contrib/appsi/cmodel/src/interval.hpp index 0f3a2a9a816..a57f107f8db 100644 --- a/pyomo/contrib/appsi/cmodel/src/interval.hpp +++ b/pyomo/contrib/appsi/cmodel/src/interval.hpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp b/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp index be7ff6d9ac9..68baf2b8ae8 100644 --- a/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp +++ b/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/lp_writer.hpp b/pyomo/contrib/appsi/cmodel/src/lp_writer.hpp index 1cb6adb462b..0b2e2882510 100644 --- a/pyomo/contrib/appsi/cmodel/src/lp_writer.hpp +++ b/pyomo/contrib/appsi/cmodel/src/lp_writer.hpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/model_base.cpp b/pyomo/contrib/appsi/cmodel/src/model_base.cpp index 4503138bf1b..b0ae4013b32 100644 --- a/pyomo/contrib/appsi/cmodel/src/model_base.cpp +++ b/pyomo/contrib/appsi/cmodel/src/model_base.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/model_base.hpp b/pyomo/contrib/appsi/cmodel/src/model_base.hpp index b797976aa2f..a47f1d14a0b 100644 --- a/pyomo/contrib/appsi/cmodel/src/model_base.hpp +++ b/pyomo/contrib/appsi/cmodel/src/model_base.hpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp b/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp index a1b699e6355..8de6cc74ab4 100644 --- a/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp +++ b/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/src/nl_writer.hpp b/pyomo/contrib/appsi/cmodel/src/nl_writer.hpp index 557d0645e4a..b7439875301 100644 --- a/pyomo/contrib/appsi/cmodel/src/nl_writer.hpp +++ b/pyomo/contrib/appsi/cmodel/src/nl_writer.hpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/tests/__init__.py b/pyomo/contrib/appsi/cmodel/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/appsi/cmodel/tests/__init__.py +++ b/pyomo/contrib/appsi/cmodel/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/cmodel/tests/test_import.py b/pyomo/contrib/appsi/cmodel/tests/test_import.py index 9fce3559aff..76eda902ac0 100644 --- a/pyomo/contrib/appsi/cmodel/tests/test_import.py +++ b/pyomo/contrib/appsi/cmodel/tests/test_import.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/examples/__init__.py b/pyomo/contrib/appsi/examples/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/appsi/examples/__init__.py +++ b/pyomo/contrib/appsi/examples/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/examples/getting_started.py b/pyomo/contrib/appsi/examples/getting_started.py index c1500c482d9..6bc42d1d377 100644 --- a/pyomo/contrib/appsi/examples/getting_started.py +++ b/pyomo/contrib/appsi/examples/getting_started.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/examples/tests/__init__.py b/pyomo/contrib/appsi/examples/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/appsi/examples/tests/__init__.py +++ b/pyomo/contrib/appsi/examples/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/examples/tests/test_examples.py b/pyomo/contrib/appsi/examples/tests/test_examples.py index 7c04271f6d3..a7608d36b98 100644 --- a/pyomo/contrib/appsi/examples/tests/test_examples.py +++ b/pyomo/contrib/appsi/examples/tests/test_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 957fdc593d4..8b6cc52d2aa 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index aea9edb3faf..b5cfd080b32 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/solvers/__init__.py b/pyomo/contrib/appsi/solvers/__init__.py index 359e3f80742..c03523a69d4 100644 --- a/pyomo/contrib/appsi/solvers/__init__.py +++ b/pyomo/contrib/appsi/solvers/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index c0e1f15c01e..2c522af864d 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index e8ee204ad63..1d7147f16e8 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 842cbbf175d..aa233ef77d6 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 2619aa2f0c7..a9a23682355 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 13cda3e3a19..d7a786e6c2c 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/solvers/tests/__init__.py b/pyomo/contrib/appsi/solvers/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/appsi/solvers/tests/__init__.py +++ b/pyomo/contrib/appsi/solvers/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py index ed2859fef36..2f674a2eb6a 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_gurobi_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py index 02de50542f3..b26f45ff2cc 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_highs_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py index dc82d04b900..8e6473a6b01 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_ipopt_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 7b0cbeaf284..af615d1ed8b 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py index df1d36442b9..6fb25bfb529 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py +++ b/pyomo/contrib/appsi/solvers/tests/test_wntr_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 2937a5f1b7c..e1835b810b0 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/tests/__init__.py b/pyomo/contrib/appsi/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/appsi/tests/__init__.py +++ b/pyomo/contrib/appsi/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/tests/test_base.py b/pyomo/contrib/appsi/tests/test_base.py index 7700d4f5534..e537cc0f219 100644 --- a/pyomo/contrib/appsi/tests/test_base.py +++ b/pyomo/contrib/appsi/tests/test_base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/tests/test_fbbt.py b/pyomo/contrib/appsi/tests/test_fbbt.py index b739367b989..a3f520e7bd6 100644 --- a/pyomo/contrib/appsi/tests/test_fbbt.py +++ b/pyomo/contrib/appsi/tests/test_fbbt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/tests/test_interval.py b/pyomo/contrib/appsi/tests/test_interval.py index 7c66d63a543..2184f69621a 100644 --- a/pyomo/contrib/appsi/tests/test_interval.py +++ b/pyomo/contrib/appsi/tests/test_interval.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/utils/__init__.py b/pyomo/contrib/appsi/utils/__init__.py index 147d82a923a..e1278431835 100644 --- a/pyomo/contrib/appsi/utils/__init__.py +++ b/pyomo/contrib/appsi/utils/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py index 7bf273dbf87..4e117b04094 100644 --- a/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py +++ b/pyomo/contrib/appsi/utils/collect_vars_and_named_exprs.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/utils/get_objective.py b/pyomo/contrib/appsi/utils/get_objective.py index 7b43a981622..110c0188d16 100644 --- a/pyomo/contrib/appsi/utils/get_objective.py +++ b/pyomo/contrib/appsi/utils/get_objective.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/utils/tests/__init__.py b/pyomo/contrib/appsi/utils/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/appsi/utils/tests/__init__.py +++ b/pyomo/contrib/appsi/utils/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py b/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py index 9a5e08385f3..62f98728850 100644 --- a/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py +++ b/pyomo/contrib/appsi/utils/tests/test_collect_vars_and_named_exprs.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/writers/__init__.py b/pyomo/contrib/appsi/writers/__init__.py index 0d5191e8b97..18f90e8aa96 100644 --- a/pyomo/contrib/appsi/writers/__init__.py +++ b/pyomo/contrib/appsi/writers/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/writers/config.py b/pyomo/contrib/appsi/writers/config.py index 9d66aba2037..32d45325e96 100644 --- a/pyomo/contrib/appsi/writers/config.py +++ b/pyomo/contrib/appsi/writers/config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 09470202be3..9984cb7465d 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index c2c93992140..bd24a86216a 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/writers/tests/__init__.py b/pyomo/contrib/appsi/writers/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/appsi/writers/tests/__init__.py +++ b/pyomo/contrib/appsi/writers/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/appsi/writers/tests/test_nl_writer.py b/pyomo/contrib/appsi/writers/tests/test_nl_writer.py index d0844263d5a..c6005afceb2 100644 --- a/pyomo/contrib/appsi/writers/tests/test_nl_writer.py +++ b/pyomo/contrib/appsi/writers/tests/test_nl_writer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/benders/__init__.py b/pyomo/contrib/benders/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/benders/__init__.py +++ b/pyomo/contrib/benders/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/benders/benders_cuts.py b/pyomo/contrib/benders/benders_cuts.py index 5eb2e91cc82..01734993552 100644 --- a/pyomo/contrib/benders/benders_cuts.py +++ b/pyomo/contrib/benders/benders_cuts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/benders/examples/__init__.py b/pyomo/contrib/benders/examples/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/benders/examples/__init__.py +++ b/pyomo/contrib/benders/examples/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/benders/examples/farmer.py b/pyomo/contrib/benders/examples/farmer.py index bf5d40e112c..47cdb3511a3 100644 --- a/pyomo/contrib/benders/examples/farmer.py +++ b/pyomo/contrib/benders/examples/farmer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/benders/examples/grothey_ex.py b/pyomo/contrib/benders/examples/grothey_ex.py index 66457fa7293..27d37cac124 100644 --- a/pyomo/contrib/benders/examples/grothey_ex.py +++ b/pyomo/contrib/benders/examples/grothey_ex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/benders/tests/__init__.py b/pyomo/contrib/benders/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/benders/tests/__init__.py +++ b/pyomo/contrib/benders/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/benders/tests/test_benders.py b/pyomo/contrib/benders/tests/test_benders.py index 26a2a0b7910..52ae9e56db8 100644 --- a/pyomo/contrib/benders/tests/test_benders.py +++ b/pyomo/contrib/benders/tests/test_benders.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/community_detection/__init__.py b/pyomo/contrib/community_detection/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/community_detection/__init__.py +++ b/pyomo/contrib/community_detection/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/community_detection/community_graph.py b/pyomo/contrib/community_detection/community_graph.py index d1bd49df20c..889940b5996 100644 --- a/pyomo/contrib/community_detection/community_graph.py +++ b/pyomo/contrib/community_detection/community_graph.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/community_detection/detection.py b/pyomo/contrib/community_detection/detection.py index 9fe7005f1f2..5bf8187a243 100644 --- a/pyomo/contrib/community_detection/detection.py +++ b/pyomo/contrib/community_detection/detection.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/community_detection/event_log.py b/pyomo/contrib/community_detection/event_log.py index 09b1039a8f7..767ff0f50f5 100644 --- a/pyomo/contrib/community_detection/event_log.py +++ b/pyomo/contrib/community_detection/event_log.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/community_detection/plugins.py b/pyomo/contrib/community_detection/plugins.py index 578da835d5e..229b7255a27 100644 --- a/pyomo/contrib/community_detection/plugins.py +++ b/pyomo/contrib/community_detection/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/community_detection/tests/__init__.py b/pyomo/contrib/community_detection/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/community_detection/tests/__init__.py +++ b/pyomo/contrib/community_detection/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/community_detection/tests/test_detection.py b/pyomo/contrib/community_detection/tests/test_detection.py index acfd441005f..6a43ea1b61a 100644 --- a/pyomo/contrib/community_detection/tests/test_detection.py +++ b/pyomo/contrib/community_detection/tests/test_detection.py @@ -4,7 +4,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/__init__.py b/pyomo/contrib/cp/__init__.py index 71ba479523f..ed45344fb95 100644 --- a/pyomo/contrib/cp/__init__.py +++ b/pyomo/contrib/cp/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/interval_var.py b/pyomo/contrib/cp/interval_var.py index 4e22c2b2d3d..953b859ea20 100644 --- a/pyomo/contrib/cp/interval_var.py +++ b/pyomo/contrib/cp/interval_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/plugins.py b/pyomo/contrib/cp/plugins.py index 445599daab0..b0f7c84eb65 100644 --- a/pyomo/contrib/cp/plugins.py +++ b/pyomo/contrib/cp/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/repn/__init__.py b/pyomo/contrib/cp/repn/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/cp/repn/__init__.py +++ b/pyomo/contrib/cp/repn/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index c2687662fe8..8356a1e752f 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/scheduling_expr/__init__.py b/pyomo/contrib/cp/scheduling_expr/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/cp/scheduling_expr/__init__.py +++ b/pyomo/contrib/cp/scheduling_expr/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py b/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py index 5340583a216..1dec02bba23 100644 --- a/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/precedence_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py b/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py index b4f8fbb4977..b75306f72c9 100644 --- a/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/tests/__init__.py b/pyomo/contrib/cp/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/cp/tests/__init__.py +++ b/pyomo/contrib/cp/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index b897053c93a..8e0e8c6955e 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/tests/test_docplex_writer.py b/pyomo/contrib/cp/tests/test_docplex_writer.py index b563052ef3a..b5f30f24440 100644 --- a/pyomo/contrib/cp/tests/test_docplex_writer.py +++ b/pyomo/contrib/cp/tests/test_docplex_writer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/tests/test_interval_var.py b/pyomo/contrib/cp/tests/test_interval_var.py index edbf889fcda..1645258d98a 100644 --- a/pyomo/contrib/cp/tests/test_interval_var.py +++ b/pyomo/contrib/cp/tests/test_interval_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py b/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py index c6733f34f83..3f66aa57726 100755 --- a/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py +++ b/pyomo/contrib/cp/tests/test_logical_to_disjunctive.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/tests/test_precedence_constraints.py b/pyomo/contrib/cp/tests/test_precedence_constraints.py index 461dabf564c..0a84a4d1960 100644 --- a/pyomo/contrib/cp/tests/test_precedence_constraints.py +++ b/pyomo/contrib/cp/tests/test_precedence_constraints.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/tests/test_step_function_expressions.py b/pyomo/contrib/cp/tests/test_step_function_expressions.py index 7212cc870d5..a7b30c1d4e6 100644 --- a/pyomo/contrib/cp/tests/test_step_function_expressions.py +++ b/pyomo/contrib/cp/tests/test_step_function_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/transform/__init__.py b/pyomo/contrib/cp/transform/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/cp/transform/__init__.py +++ b/pyomo/contrib/cp/transform/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py index cd7681d4d87..e318e621e88 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index 624629d326d..d5f13e91535 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index e38b5dce1d9..e45aa3b44a3 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index b451c431f21..d2ba2f277d6 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/examples/__init__.py b/pyomo/contrib/doe/examples/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/doe/examples/__init__.py +++ b/pyomo/contrib/doe/examples/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/examples/reactor_compute_FIM.py b/pyomo/contrib/doe/examples/reactor_compute_FIM.py index c004ad36f00..108f5bd16a0 100644 --- a/pyomo/contrib/doe/examples/reactor_compute_FIM.py +++ b/pyomo/contrib/doe/examples/reactor_compute_FIM.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/examples/reactor_grid_search.py b/pyomo/contrib/doe/examples/reactor_grid_search.py index a4516c36451..1f5aae77f85 100644 --- a/pyomo/contrib/doe/examples/reactor_grid_search.py +++ b/pyomo/contrib/doe/examples/reactor_grid_search.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/examples/reactor_kinetics.py b/pyomo/contrib/doe/examples/reactor_kinetics.py index 57d06e146c5..ed2175085f2 100644 --- a/pyomo/contrib/doe/examples/reactor_kinetics.py +++ b/pyomo/contrib/doe/examples/reactor_kinetics.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/examples/reactor_optimize_doe.py b/pyomo/contrib/doe/examples/reactor_optimize_doe.py index 56ea1ffeac3..f7b4a74c891 100644 --- a/pyomo/contrib/doe/examples/reactor_optimize_doe.py +++ b/pyomo/contrib/doe/examples/reactor_optimize_doe.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index 75fd4f7c485..5a3c44a76e4 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/result.py b/pyomo/contrib/doe/result.py index 65ded38a63b..1593214c30a 100644 --- a/pyomo/contrib/doe/result.py +++ b/pyomo/contrib/doe/result.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/scenario.py b/pyomo/contrib/doe/scenario.py index eff9c883e0b..6c6f5ef7d1b 100644 --- a/pyomo/contrib/doe/scenario.py +++ b/pyomo/contrib/doe/scenario.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/tests/__init__.py b/pyomo/contrib/doe/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/doe/tests/__init__.py +++ b/pyomo/contrib/doe/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/tests/test_example.py b/pyomo/contrib/doe/tests/test_example.py index 0f143e03677..b59014a8110 100644 --- a/pyomo/contrib/doe/tests/test_example.py +++ b/pyomo/contrib/doe/tests/test_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py index 42b463162b2..31d250f0d10 100644 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ b/pyomo/contrib/doe/tests/test_fim_doe.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/doe/tests/test_reactor_example.py b/pyomo/contrib/doe/tests/test_reactor_example.py index 86c914ec4e0..daf2ee89194 100644 --- a/pyomo/contrib/doe/tests/test_reactor_example.py +++ b/pyomo/contrib/doe/tests/test_reactor_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/example/__init__.py b/pyomo/contrib/example/__init__.py index 7a9e6e76de4..c70b50e84de 100644 --- a/pyomo/contrib/example/__init__.py +++ b/pyomo/contrib/example/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/example/bar.py b/pyomo/contrib/example/bar.py index eb39c5f8748..22e5c3997e9 100644 --- a/pyomo/contrib/example/bar.py +++ b/pyomo/contrib/example/bar.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/example/foo.py b/pyomo/contrib/example/foo.py index a1a10b1dd62..f879bc70722 100644 --- a/pyomo/contrib/example/foo.py +++ b/pyomo/contrib/example/foo.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/example/plugins/__init__.py b/pyomo/contrib/example/plugins/__init__.py index 0c6c248c122..179098bc18e 100644 --- a/pyomo/contrib/example/plugins/__init__.py +++ b/pyomo/contrib/example/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/example/plugins/ex_plugin.py b/pyomo/contrib/example/plugins/ex_plugin.py index 0a23afc0158..7ee4c414ccf 100644 --- a/pyomo/contrib/example/plugins/ex_plugin.py +++ b/pyomo/contrib/example/plugins/ex_plugin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/example/tests/__init__.py b/pyomo/contrib/example/tests/__init__.py index 3ecae26215c..9c45a6ef8b6 100644 --- a/pyomo/contrib/example/tests/__init__.py +++ b/pyomo/contrib/example/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/example/tests/test_example.py b/pyomo/contrib/example/tests/test_example.py index c38de1b914f..55394f5d0c1 100644 --- a/pyomo/contrib/example/tests/test_example.py +++ b/pyomo/contrib/example/tests/test_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fbbt/__init__.py b/pyomo/contrib/fbbt/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/fbbt/__init__.py +++ b/pyomo/contrib/fbbt/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fbbt/expression_bounds_walker.py b/pyomo/contrib/fbbt/expression_bounds_walker.py index 340af94c83e..cb287d54df5 100644 --- a/pyomo/contrib/fbbt/expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/expression_bounds_walker.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index bf42cbe7f33..bde33b3caa0 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fbbt/interval.py b/pyomo/contrib/fbbt/interval.py index 8bebe128988..a12d1a4529f 100644 --- a/pyomo/contrib/fbbt/interval.py +++ b/pyomo/contrib/fbbt/interval.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fbbt/tests/__init__.py b/pyomo/contrib/fbbt/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/fbbt/tests/__init__.py +++ b/pyomo/contrib/fbbt/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py index 75d273422d1..5d27a2e4087 100644 --- a/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py +++ b/pyomo/contrib/fbbt/tests/test_expression_bounds_walker.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fbbt/tests/test_fbbt.py b/pyomo/contrib/fbbt/tests/test_fbbt.py index 5e8d656eeab..f7d08d11215 100644 --- a/pyomo/contrib/fbbt/tests/test_fbbt.py +++ b/pyomo/contrib/fbbt/tests/test_fbbt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fbbt/tests/test_interval.py b/pyomo/contrib/fbbt/tests/test_interval.py index d5dc7b54ff5..1e42162a35e 100644 --- a/pyomo/contrib/fbbt/tests/test_interval.py +++ b/pyomo/contrib/fbbt/tests/test_interval.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fme/__init__.py b/pyomo/contrib/fme/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/fme/__init__.py +++ b/pyomo/contrib/fme/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fme/fourier_motzkin_elimination.py b/pyomo/contrib/fme/fourier_motzkin_elimination.py index 18aa157545e..a1b5d744cf4 100644 --- a/pyomo/contrib/fme/fourier_motzkin_elimination.py +++ b/pyomo/contrib/fme/fourier_motzkin_elimination.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fme/plugins.py b/pyomo/contrib/fme/plugins.py index 324dd583d0f..b8278ccbb27 100644 --- a/pyomo/contrib/fme/plugins.py +++ b/pyomo/contrib/fme/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fme/tests/__init__.py b/pyomo/contrib/fme/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/fme/tests/__init__.py +++ b/pyomo/contrib/fme/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py index 11c008acf82..3c01acab531 100644 --- a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py +++ b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdp_bounds/__init__.py b/pyomo/contrib/gdp_bounds/__init__.py index 4918f6dfa0e..ac71890cf7c 100644 --- a/pyomo/contrib/gdp_bounds/__init__.py +++ b/pyomo/contrib/gdp_bounds/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdp_bounds/compute_bounds.py b/pyomo/contrib/gdp_bounds/compute_bounds.py index f4f046e79df..3c04e4e1af7 100644 --- a/pyomo/contrib/gdp_bounds/compute_bounds.py +++ b/pyomo/contrib/gdp_bounds/compute_bounds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdp_bounds/info.py b/pyomo/contrib/gdp_bounds/info.py index f7e83ee62c9..6f39af5908d 100644 --- a/pyomo/contrib/gdp_bounds/info.py +++ b/pyomo/contrib/gdp_bounds/info.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdp_bounds/plugins.py b/pyomo/contrib/gdp_bounds/plugins.py index 1ebe44378f0..016a1fc7b13 100644 --- a/pyomo/contrib/gdp_bounds/plugins.py +++ b/pyomo/contrib/gdp_bounds/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdp_bounds/tests/__init__.py b/pyomo/contrib/gdp_bounds/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/gdp_bounds/tests/__init__.py +++ b/pyomo/contrib/gdp_bounds/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdp_bounds/tests/test_gdp_bounds.py b/pyomo/contrib/gdp_bounds/tests/test_gdp_bounds.py index 551236c7d97..0c8eae2c43b 100644 --- a/pyomo/contrib/gdp_bounds/tests/test_gdp_bounds.py +++ b/pyomo/contrib/gdp_bounds/tests/test_gdp_bounds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/GDPopt.py b/pyomo/contrib/gdpopt/GDPopt.py index 3d45fa504cb..f0ff6d690d6 100644 --- a/pyomo/contrib/gdpopt/GDPopt.py +++ b/pyomo/contrib/gdpopt/GDPopt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/__init__.py b/pyomo/contrib/gdpopt/__init__.py index f74855f0206..a84b8385ad3 100644 --- a/pyomo/contrib/gdpopt/__init__.py +++ b/pyomo/contrib/gdpopt/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/algorithm_base_class.py b/pyomo/contrib/gdpopt/algorithm_base_class.py index 5bf41148700..c5929ad4a88 100644 --- a/pyomo/contrib/gdpopt/algorithm_base_class.py +++ b/pyomo/contrib/gdpopt/algorithm_base_class.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/branch_and_bound.py b/pyomo/contrib/gdpopt/branch_and_bound.py index 26dc2b5f2eb..918f3d459a0 100644 --- a/pyomo/contrib/gdpopt/branch_and_bound.py +++ b/pyomo/contrib/gdpopt/branch_and_bound.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/config_options.py b/pyomo/contrib/gdpopt/config_options.py index 386826b844c..467c4a6ec32 100644 --- a/pyomo/contrib/gdpopt/config_options.py +++ b/pyomo/contrib/gdpopt/config_options.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/create_oa_subproblems.py b/pyomo/contrib/gdpopt/create_oa_subproblems.py index 12266866dbc..690fe1f15f1 100644 --- a/pyomo/contrib/gdpopt/create_oa_subproblems.py +++ b/pyomo/contrib/gdpopt/create_oa_subproblems.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/cut_generation.py b/pyomo/contrib/gdpopt/cut_generation.py index 36a826a4f83..742a2cde395 100644 --- a/pyomo/contrib/gdpopt/cut_generation.py +++ b/pyomo/contrib/gdpopt/cut_generation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/discrete_problem_initialize.py b/pyomo/contrib/gdpopt/discrete_problem_initialize.py index 3dc18132c5b..81c339b94a2 100644 --- a/pyomo/contrib/gdpopt/discrete_problem_initialize.py +++ b/pyomo/contrib/gdpopt/discrete_problem_initialize.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/enumerate.py b/pyomo/contrib/gdpopt/enumerate.py index 45ecc8864f9..6c25d0088f4 100644 --- a/pyomo/contrib/gdpopt/enumerate.py +++ b/pyomo/contrib/gdpopt/enumerate.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/gloa.py b/pyomo/contrib/gdpopt/gloa.py index 68bd692f967..212da057e05 100644 --- a/pyomo/contrib/gdpopt/gloa.py +++ b/pyomo/contrib/gdpopt/gloa.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/loa.py b/pyomo/contrib/gdpopt/loa.py index 44c1f8609e8..354b61ae940 100644 --- a/pyomo/contrib/gdpopt/loa.py +++ b/pyomo/contrib/gdpopt/loa.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/nlp_initialization.py b/pyomo/contrib/gdpopt/nlp_initialization.py index fc083c095da..dbc33eb20be 100644 --- a/pyomo/contrib/gdpopt/nlp_initialization.py +++ b/pyomo/contrib/gdpopt/nlp_initialization.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/oa_algorithm_utils.py b/pyomo/contrib/gdpopt/oa_algorithm_utils.py index 9aba59e4527..ce4012d8800 100644 --- a/pyomo/contrib/gdpopt/oa_algorithm_utils.py +++ b/pyomo/contrib/gdpopt/oa_algorithm_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/plugins.py b/pyomo/contrib/gdpopt/plugins.py index 9d729c63d9c..d0068d25993 100644 --- a/pyomo/contrib/gdpopt/plugins.py +++ b/pyomo/contrib/gdpopt/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/ric.py b/pyomo/contrib/gdpopt/ric.py index 586a27362a1..2aa1aaf8c67 100644 --- a/pyomo/contrib/gdpopt/ric.py +++ b/pyomo/contrib/gdpopt/ric.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/solve_discrete_problem.py b/pyomo/contrib/gdpopt/solve_discrete_problem.py index 3de66fbaca0..54218edc50a 100644 --- a/pyomo/contrib/gdpopt/solve_discrete_problem.py +++ b/pyomo/contrib/gdpopt/solve_discrete_problem.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/solve_subproblem.py b/pyomo/contrib/gdpopt/solve_subproblem.py index bd9b85c0cef..e3980c3c784 100644 --- a/pyomo/contrib/gdpopt/solve_subproblem.py +++ b/pyomo/contrib/gdpopt/solve_subproblem.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/tests/__init__.py b/pyomo/contrib/gdpopt/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/gdpopt/tests/__init__.py +++ b/pyomo/contrib/gdpopt/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/tests/common_tests.py b/pyomo/contrib/gdpopt/tests/common_tests.py index 5a363430381..88a2642704a 100644 --- a/pyomo/contrib/gdpopt/tests/common_tests.py +++ b/pyomo/contrib/gdpopt/tests/common_tests.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/tests/test_LBB.py b/pyomo/contrib/gdpopt/tests/test_LBB.py index 7d25767020e..273327b02a4 100644 --- a/pyomo/contrib/gdpopt/tests/test_LBB.py +++ b/pyomo/contrib/gdpopt/tests/test_LBB.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/tests/test_enumerate.py b/pyomo/contrib/gdpopt/tests/test_enumerate.py index 606dd172064..8798557ddc9 100644 --- a/pyomo/contrib/gdpopt/tests/test_enumerate.py +++ b/pyomo/contrib/gdpopt/tests/test_enumerate.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index 1d5559a9b33..005df56ced5 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gdpopt/util.py b/pyomo/contrib/gdpopt/util.py index f288f9e2647..2cb70f0ea60 100644 --- a/pyomo/contrib/gdpopt/util.py +++ b/pyomo/contrib/gdpopt/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gjh/GJH.py b/pyomo/contrib/gjh/GJH.py index df9dfebf477..dc7c8de89c1 100644 --- a/pyomo/contrib/gjh/GJH.py +++ b/pyomo/contrib/gjh/GJH.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gjh/__init__.py b/pyomo/contrib/gjh/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/gjh/__init__.py +++ b/pyomo/contrib/gjh/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gjh/getGJH.py b/pyomo/contrib/gjh/getGJH.py index 112de054745..2d503c71438 100644 --- a/pyomo/contrib/gjh/getGJH.py +++ b/pyomo/contrib/gjh/getGJH.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/gjh/plugins.py b/pyomo/contrib/gjh/plugins.py index 4af2f38becd..f072f7b2c38 100644 --- a/pyomo/contrib/gjh/plugins.py +++ b/pyomo/contrib/gjh/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/iis/__init__.py b/pyomo/contrib/iis/__init__.py index 29f5d4f3d40..e8d6a7ac2c3 100644 --- a/pyomo/contrib/iis/__init__.py +++ b/pyomo/contrib/iis/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/iis/iis.py b/pyomo/contrib/iis/iis.py index a279ce0aac3..1ffd6cb0bd3 100644 --- a/pyomo/contrib/iis/iis.py +++ b/pyomo/contrib/iis/iis.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/iis/tests/__init__.py b/pyomo/contrib/iis/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/iis/tests/__init__.py +++ b/pyomo/contrib/iis/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/iis/tests/test_iis.py b/pyomo/contrib/iis/tests/test_iis.py index 8343798741a..cf7b5613a3a 100644 --- a/pyomo/contrib/iis/tests/test_iis.py +++ b/pyomo/contrib/iis/tests/test_iis.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/__init__.py b/pyomo/contrib/incidence_analysis/__init__.py index 612b4fe7d02..8942d09b6b9 100644 --- a/pyomo/contrib/incidence_analysis/__init__.py +++ b/pyomo/contrib/incidence_analysis/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/common/__init__.py b/pyomo/contrib/incidence_analysis/common/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/incidence_analysis/common/__init__.py +++ b/pyomo/contrib/incidence_analysis/common/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/common/dulmage_mendelsohn.py b/pyomo/contrib/incidence_analysis/common/dulmage_mendelsohn.py index 09a926cdec2..5bc724fafc1 100644 --- a/pyomo/contrib/incidence_analysis/common/dulmage_mendelsohn.py +++ b/pyomo/contrib/incidence_analysis/common/dulmage_mendelsohn.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/common/tests/__init__.py b/pyomo/contrib/incidence_analysis/common/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/incidence_analysis/common/tests/__init__.py +++ b/pyomo/contrib/incidence_analysis/common/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/common/tests/test_dulmage_mendelsohn.py b/pyomo/contrib/incidence_analysis/common/tests/test_dulmage_mendelsohn.py index 1675fc7420a..b17ae9b1dfc 100644 --- a/pyomo/contrib/incidence_analysis/common/tests/test_dulmage_mendelsohn.py +++ b/pyomo/contrib/incidence_analysis/common/tests/test_dulmage_mendelsohn.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/config.py b/pyomo/contrib/incidence_analysis/config.py index 72d1a41ac74..128273b4dec 100644 --- a/pyomo/contrib/incidence_analysis/config.py +++ b/pyomo/contrib/incidence_analysis/config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/connected.py b/pyomo/contrib/incidence_analysis/connected.py index 2dcf31c0fe0..28d4bdee73f 100644 --- a/pyomo/contrib/incidence_analysis/connected.py +++ b/pyomo/contrib/incidence_analysis/connected.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/dulmage_mendelsohn.py b/pyomo/contrib/incidence_analysis/dulmage_mendelsohn.py index eb24b0559fc..3a6d06a809c 100644 --- a/pyomo/contrib/incidence_analysis/dulmage_mendelsohn.py +++ b/pyomo/contrib/incidence_analysis/dulmage_mendelsohn.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index 13e9997d6c3..96cbf77c47d 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index acf2d318578..8361c32a43c 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/matching.py b/pyomo/contrib/incidence_analysis/matching.py index 14b3cd5b18d..e37b35cd973 100644 --- a/pyomo/contrib/incidence_analysis/matching.py +++ b/pyomo/contrib/incidence_analysis/matching.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index d7620278fd3..835e07c7c02 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/tests/__init__.py b/pyomo/contrib/incidence_analysis/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/incidence_analysis/tests/__init__.py +++ b/pyomo/contrib/incidence_analysis/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/tests/models_for_testing.py b/pyomo/contrib/incidence_analysis/tests/models_for_testing.py index 98d61201619..6040e80e068 100644 --- a/pyomo/contrib/incidence_analysis/tests/models_for_testing.py +++ b/pyomo/contrib/incidence_analysis/tests/models_for_testing.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/tests/test_connected.py b/pyomo/contrib/incidence_analysis/tests/test_connected.py index a937a5029a1..421231d3dd0 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_connected.py +++ b/pyomo/contrib/incidence_analysis/tests/test_connected.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/tests/test_dulmage_mendelsohn.py b/pyomo/contrib/incidence_analysis/tests/test_dulmage_mendelsohn.py index 98fefea2d80..6195d6afca7 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_dulmage_mendelsohn.py +++ b/pyomo/contrib/incidence_analysis/tests/test_dulmage_mendelsohn.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/tests/test_incidence.py b/pyomo/contrib/incidence_analysis/tests/test_incidence.py index 2d178c62119..832fbbfb10c 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_incidence.py +++ b/pyomo/contrib/incidence_analysis/tests/test_incidence.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 10777a35f78..e6c3f341e81 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/tests/test_matching.py b/pyomo/contrib/incidence_analysis/tests/test_matching.py index b5550b3b84c..2327439f0a2 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_matching.py +++ b/pyomo/contrib/incidence_analysis/tests/test_matching.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/tests/test_scc_solver.py b/pyomo/contrib/incidence_analysis/tests/test_scc_solver.py index 6efe52a7d80..b75f93e4a12 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_scc_solver.py +++ b/pyomo/contrib/incidence_analysis/tests/test_scc_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/tests/test_triangularize.py b/pyomo/contrib/incidence_analysis/tests/test_triangularize.py index 76ba4403310..22548a15998 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_triangularize.py +++ b/pyomo/contrib/incidence_analysis/tests/test_triangularize.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/triangularize.py b/pyomo/contrib/incidence_analysis/triangularize.py index ac6680a367e..6af251b1ec6 100644 --- a/pyomo/contrib/incidence_analysis/triangularize.py +++ b/pyomo/contrib/incidence_analysis/triangularize.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/incidence_analysis/util.py b/pyomo/contrib/incidence_analysis/util.py index a127161d33d..8b6572eb900 100644 --- a/pyomo/contrib/incidence_analysis/util.py +++ b/pyomo/contrib/incidence_analysis/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/__init__.py b/pyomo/contrib/interior_point/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/interior_point/__init__.py +++ b/pyomo/contrib/interior_point/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/examples/__init__.py b/pyomo/contrib/interior_point/examples/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/interior_point/examples/__init__.py +++ b/pyomo/contrib/interior_point/examples/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/examples/ex1.py b/pyomo/contrib/interior_point/examples/ex1.py index d9931e1daa8..f6d8f14ac0a 100644 --- a/pyomo/contrib/interior_point/examples/ex1.py +++ b/pyomo/contrib/interior_point/examples/ex1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/interface.py b/pyomo/contrib/interior_point/interface.py index 7d04f578238..93b83f385ba 100644 --- a/pyomo/contrib/interior_point/interface.py +++ b/pyomo/contrib/interior_point/interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/interior_point.py b/pyomo/contrib/interior_point/interior_point.py index 00d26ddef03..502de338fdc 100644 --- a/pyomo/contrib/interior_point/interior_point.py +++ b/pyomo/contrib/interior_point/interior_point.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/inverse_reduced_hessian.py b/pyomo/contrib/interior_point/inverse_reduced_hessian.py index 6144a4afeb8..ac3c6a98463 100644 --- a/pyomo/contrib/interior_point/inverse_reduced_hessian.py +++ b/pyomo/contrib/interior_point/inverse_reduced_hessian.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/linalg/__init__.py b/pyomo/contrib/interior_point/linalg/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/interior_point/linalg/__init__.py +++ b/pyomo/contrib/interior_point/linalg/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/linalg/base_linear_solver_interface.py b/pyomo/contrib/interior_point/linalg/base_linear_solver_interface.py index 2bc7fe2eee5..c3304fd1395 100644 --- a/pyomo/contrib/interior_point/linalg/base_linear_solver_interface.py +++ b/pyomo/contrib/interior_point/linalg/base_linear_solver_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/linalg/ma27_interface.py b/pyomo/contrib/interior_point/linalg/ma27_interface.py index 0a28e50578d..7604bd432bb 100644 --- a/pyomo/contrib/interior_point/linalg/ma27_interface.py +++ b/pyomo/contrib/interior_point/linalg/ma27_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/linalg/mumps_interface.py b/pyomo/contrib/interior_point/linalg/mumps_interface.py index 98f0ef03210..c7480e2b6d0 100644 --- a/pyomo/contrib/interior_point/linalg/mumps_interface.py +++ b/pyomo/contrib/interior_point/linalg/mumps_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/linalg/scipy_interface.py b/pyomo/contrib/interior_point/linalg/scipy_interface.py index 87b0cad8ea0..d0f773fcb81 100644 --- a/pyomo/contrib/interior_point/linalg/scipy_interface.py +++ b/pyomo/contrib/interior_point/linalg/scipy_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/linalg/tests/__init__.py b/pyomo/contrib/interior_point/linalg/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/interior_point/linalg/tests/__init__.py +++ b/pyomo/contrib/interior_point/linalg/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/linalg/tests/test_linear_solvers.py b/pyomo/contrib/interior_point/linalg/tests/test_linear_solvers.py index c13aad215cc..93071a5f215 100644 --- a/pyomo/contrib/interior_point/linalg/tests/test_linear_solvers.py +++ b/pyomo/contrib/interior_point/linalg/tests/test_linear_solvers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/linalg/tests/test_realloc.py b/pyomo/contrib/interior_point/linalg/tests/test_realloc.py index 7dce4755261..3a53d0e7db9 100644 --- a/pyomo/contrib/interior_point/linalg/tests/test_realloc.py +++ b/pyomo/contrib/interior_point/linalg/tests/test_realloc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/tests/__init__.py b/pyomo/contrib/interior_point/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/interior_point/tests/__init__.py +++ b/pyomo/contrib/interior_point/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/tests/test_interior_point.py b/pyomo/contrib/interior_point/tests/test_interior_point.py index bff80934d20..a05408abe1e 100644 --- a/pyomo/contrib/interior_point/tests/test_interior_point.py +++ b/pyomo/contrib/interior_point/tests/test_interior_point.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/tests/test_inverse_reduced_hessian.py b/pyomo/contrib/interior_point/tests/test_inverse_reduced_hessian.py index 67657dfce47..61f5e90e3cf 100644 --- a/pyomo/contrib/interior_point/tests/test_inverse_reduced_hessian.py +++ b/pyomo/contrib/interior_point/tests/test_inverse_reduced_hessian.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/tests/test_realloc.py b/pyomo/contrib/interior_point/tests/test_realloc.py index dcf94eb6da7..b7a5d00e488 100644 --- a/pyomo/contrib/interior_point/tests/test_realloc.py +++ b/pyomo/contrib/interior_point/tests/test_realloc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/interior_point/tests/test_reg.py b/pyomo/contrib/interior_point/tests/test_reg.py index b37d9532428..a7fc686545b 100644 --- a/pyomo/contrib/interior_point/tests/test_reg.py +++ b/pyomo/contrib/interior_point/tests/test_reg.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/latex_printer/__init__.py b/pyomo/contrib/latex_printer/__init__.py index 7208b1e7d64..c434b53dfe1 100644 --- a/pyomo/contrib/latex_printer/__init__.py +++ b/pyomo/contrib/latex_printer/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index f9150f700a3..110df7cd5ca 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/latex_printer/tests/__init__.py b/pyomo/contrib/latex_printer/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/latex_printer/tests/__init__.py +++ b/pyomo/contrib/latex_printer/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/latex_printer/tests/test_latex_printer.py b/pyomo/contrib/latex_printer/tests/test_latex_printer.py index 1797e0a39a0..2d7dd69dba8 100644 --- a/pyomo/contrib/latex_printer/tests/test_latex_printer.py +++ b/pyomo/contrib/latex_printer/tests/test_latex_printer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/latex_printer/tests/test_latex_printer_vartypes.py b/pyomo/contrib/latex_printer/tests/test_latex_printer_vartypes.py index dc571030fde..dc3a415618b 100644 --- a/pyomo/contrib/latex_printer/tests/test_latex_printer_vartypes.py +++ b/pyomo/contrib/latex_printer/tests/test_latex_printer_vartypes.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mcpp/__init__.py b/pyomo/contrib/mcpp/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/mcpp/__init__.py +++ b/pyomo/contrib/mcpp/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mcpp/build.py b/pyomo/contrib/mcpp/build.py index 55c893335d2..7e119caec9f 100644 --- a/pyomo/contrib/mcpp/build.py +++ b/pyomo/contrib/mcpp/build.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mcpp/getMCPP.py b/pyomo/contrib/mcpp/getMCPP.py index caf9566df64..dbce611d1a0 100644 --- a/pyomo/contrib/mcpp/getMCPP.py +++ b/pyomo/contrib/mcpp/getMCPP.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mcpp/mcppInterface.cpp b/pyomo/contrib/mcpp/mcppInterface.cpp index 30491fde1b1..a1e74567896 100644 --- a/pyomo/contrib/mcpp/mcppInterface.cpp +++ b/pyomo/contrib/mcpp/mcppInterface.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mcpp/plugins.py b/pyomo/contrib/mcpp/plugins.py index eed8874b1e7..577feec7fe3 100644 --- a/pyomo/contrib/mcpp/plugins.py +++ b/pyomo/contrib/mcpp/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mcpp/pyomo_mcpp.py b/pyomo/contrib/mcpp/pyomo_mcpp.py index 25a4237ff16..35e883f98da 100644 --- a/pyomo/contrib/mcpp/pyomo_mcpp.py +++ b/pyomo/contrib/mcpp/pyomo_mcpp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mcpp/test_mcpp.py b/pyomo/contrib/mcpp/test_mcpp.py index 23b963e11bf..1cfb46ce328 100644 --- a/pyomo/contrib/mcpp/test_mcpp.py +++ b/pyomo/contrib/mcpp/test_mcpp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/MindtPy.py b/pyomo/contrib/mindtpy/MindtPy.py index bd873d950fd..7b41e0078a3 100644 --- a/pyomo/contrib/mindtpy/MindtPy.py +++ b/pyomo/contrib/mindtpy/MindtPy.py @@ -3,7 +3,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/__init__.py b/pyomo/contrib/mindtpy/__init__.py index 94a91238819..652493b03a6 100644 --- a/pyomo/contrib/mindtpy/__init__.py +++ b/pyomo/contrib/mindtpy/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 3d5a7ebad03..785a89d8982 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/config_options.py b/pyomo/contrib/mindtpy/config_options.py index f1c4a23d46e..ba2b74cdfe0 100644 --- a/pyomo/contrib/mindtpy/config_options.py +++ b/pyomo/contrib/mindtpy/config_options.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/cut_generation.py b/pyomo/contrib/mindtpy/cut_generation.py index 4ee7a6ff07b..e932755e9fd 100644 --- a/pyomo/contrib/mindtpy/cut_generation.py +++ b/pyomo/contrib/mindtpy/cut_generation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/extended_cutting_plane.py b/pyomo/contrib/mindtpy/extended_cutting_plane.py index 0a98f88ed3f..7bb3ff783c9 100644 --- a/pyomo/contrib/mindtpy/extended_cutting_plane.py +++ b/pyomo/contrib/mindtpy/extended_cutting_plane.py @@ -3,7 +3,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/feasibility_pump.py b/pyomo/contrib/mindtpy/feasibility_pump.py index a34cceb014c..5ee1260dd42 100644 --- a/pyomo/contrib/mindtpy/feasibility_pump.py +++ b/pyomo/contrib/mindtpy/feasibility_pump.py @@ -3,7 +3,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/global_outer_approximation.py b/pyomo/contrib/mindtpy/global_outer_approximation.py index 70fc4cffb90..c43409a8493 100644 --- a/pyomo/contrib/mindtpy/global_outer_approximation.py +++ b/pyomo/contrib/mindtpy/global_outer_approximation.py @@ -3,7 +3,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/outer_approximation.py b/pyomo/contrib/mindtpy/outer_approximation.py index f6e6147724e..ead5cadfeac 100644 --- a/pyomo/contrib/mindtpy/outer_approximation.py +++ b/pyomo/contrib/mindtpy/outer_approximation.py @@ -3,7 +3,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/plugins.py b/pyomo/contrib/mindtpy/plugins.py index f25706d086a..bf0ab0d1581 100644 --- a/pyomo/contrib/mindtpy/plugins.py +++ b/pyomo/contrib/mindtpy/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index c1e52ed72d3..05ba6bbee86 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tabu_list.py b/pyomo/contrib/mindtpy/tabu_list.py index 313bd6f6271..15c1d3b3a2b 100644 --- a/pyomo/contrib/mindtpy/tabu_list.py +++ b/pyomo/contrib/mindtpy/tabu_list.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/MINLP2_simple.py b/pyomo/contrib/mindtpy/tests/MINLP2_simple.py index 10da243d332..f3fd51af79a 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP2_simple.py +++ b/pyomo/contrib/mindtpy/tests/MINLP2_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/MINLP3_simple.py b/pyomo/contrib/mindtpy/tests/MINLP3_simple.py index f387b0e26a1..a17659e0c51 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP3_simple.py +++ b/pyomo/contrib/mindtpy/tests/MINLP3_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/MINLP4_simple.py b/pyomo/contrib/mindtpy/tests/MINLP4_simple.py index 684fdf4a932..44b6c7df543 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP4_simple.py +++ b/pyomo/contrib/mindtpy/tests/MINLP4_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/MINLP5_simple.py b/pyomo/contrib/mindtpy/tests/MINLP5_simple.py index cb78f6e0804..d5b04d0915c 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP5_simple.py +++ b/pyomo/contrib/mindtpy/tests/MINLP5_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/MINLP_simple.py b/pyomo/contrib/mindtpy/tests/MINLP_simple.py index 7454b595986..cde65536f43 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP_simple.py +++ b/pyomo/contrib/mindtpy/tests/MINLP_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py b/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py index 789dfea6191..412067de0b5 100644 --- a/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py +++ b/pyomo/contrib/mindtpy/tests/MINLP_simple_grey_box.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/__init__.py b/pyomo/contrib/mindtpy/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/mindtpy/tests/__init__.py +++ b/pyomo/contrib/mindtpy/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/constraint_qualification_example.py b/pyomo/contrib/mindtpy/tests/constraint_qualification_example.py index 75ec56df738..c0849094300 100644 --- a/pyomo/contrib/mindtpy/tests/constraint_qualification_example.py +++ b/pyomo/contrib/mindtpy/tests/constraint_qualification_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/eight_process_problem.py b/pyomo/contrib/mindtpy/tests/eight_process_problem.py index 8233fc52c53..ed9059ae4ae 100644 --- a/pyomo/contrib/mindtpy/tests/eight_process_problem.py +++ b/pyomo/contrib/mindtpy/tests/eight_process_problem.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/feasibility_pump1.py b/pyomo/contrib/mindtpy/tests/feasibility_pump1.py index 3149ccef6e4..fec750f9f12 100644 --- a/pyomo/contrib/mindtpy/tests/feasibility_pump1.py +++ b/pyomo/contrib/mindtpy/tests/feasibility_pump1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/feasibility_pump2.py b/pyomo/contrib/mindtpy/tests/feasibility_pump2.py index 9fed8238fe1..d739e4efbbe 100644 --- a/pyomo/contrib/mindtpy/tests/feasibility_pump2.py +++ b/pyomo/contrib/mindtpy/tests/feasibility_pump2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/from_proposal.py b/pyomo/contrib/mindtpy/tests/from_proposal.py index e34fddedcd3..f29fbcd2cf7 100644 --- a/pyomo/contrib/mindtpy/tests/from_proposal.py +++ b/pyomo/contrib/mindtpy/tests/from_proposal.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/nonconvex1.py b/pyomo/contrib/mindtpy/tests/nonconvex1.py index 60115a52c32..71b7e22af96 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex1.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/nonconvex2.py b/pyomo/contrib/mindtpy/tests/nonconvex2.py index ac48167b350..94c519ab0e1 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex2.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/nonconvex3.py b/pyomo/contrib/mindtpy/tests/nonconvex3.py index 8337beb8d68..5b6a1de8d7d 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex3.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/nonconvex4.py b/pyomo/contrib/mindtpy/tests/nonconvex4.py index 79e6465239f..3b7f6660ddf 100644 --- a/pyomo/contrib/mindtpy/tests/nonconvex4.py +++ b/pyomo/contrib/mindtpy/tests/nonconvex4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/online_doc_example.py b/pyomo/contrib/mindtpy/tests/online_doc_example.py index d741455e7f7..17a758552c0 100644 --- a/pyomo/contrib/mindtpy/tests/online_doc_example.py +++ b/pyomo/contrib/mindtpy/tests/online_doc_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy.py b/pyomo/contrib/mindtpy/tests/test_mindtpy.py index ae531f9bd84..37969276d55 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py index 07f2b1aaff5..24679047793 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py index c7b47b7fde2..cbc906851bf 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_global.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_global.py index dbe9270c363..07774805364 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_global.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_global.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_global_lp_nlp.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_global_lp_nlp.py index 08bfb8df2de..792bdb8d993 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_global_lp_nlp.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_global_lp_nlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py index f84136ca6bf..d50a41ad000 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_lp_nlp.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_lp_nlp.py index 2662a0e6f56..97f73ece525 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_lp_nlp.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_lp_nlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_regularization.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_regularization.py index 33f296083ed..2e864a49578 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_regularization.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_regularization.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_solution_pool.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_solution_pool.py index 775d1a4e117..a41f41d4d65 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_solution_pool.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_solution_pool.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/tests/unit_test.py b/pyomo/contrib/mindtpy/tests/unit_test.py index a1ceadda41e..af6ffad282d 100644 --- a/pyomo/contrib/mindtpy/tests/unit_test.py +++ b/pyomo/contrib/mindtpy/tests/unit_test.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 69c7ca5030a..1543497838f 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/__init__.py b/pyomo/contrib/mpc/__init__.py index da977f365d2..2e1c51e154f 100644 --- a/pyomo/contrib/mpc/__init__.py +++ b/pyomo/contrib/mpc/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/__init__.py b/pyomo/contrib/mpc/data/__init__.py index 9061fda4bfd..6051f4ba3a2 100644 --- a/pyomo/contrib/mpc/data/__init__.py +++ b/pyomo/contrib/mpc/data/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/convert.py b/pyomo/contrib/mpc/data/convert.py index f1d35592a9f..10885370032 100644 --- a/pyomo/contrib/mpc/data/convert.py +++ b/pyomo/contrib/mpc/data/convert.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/dynamic_data_base.py b/pyomo/contrib/mpc/data/dynamic_data_base.py index c0223d2dcbe..5e567f060cf 100644 --- a/pyomo/contrib/mpc/data/dynamic_data_base.py +++ b/pyomo/contrib/mpc/data/dynamic_data_base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/find_nearest_index.py b/pyomo/contrib/mpc/data/find_nearest_index.py index 0875bde63e9..c53a7a79841 100644 --- a/pyomo/contrib/mpc/data/find_nearest_index.py +++ b/pyomo/contrib/mpc/data/find_nearest_index.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/get_cuid.py b/pyomo/contrib/mpc/data/get_cuid.py index 1f229b35645..03659d6153f 100644 --- a/pyomo/contrib/mpc/data/get_cuid.py +++ b/pyomo/contrib/mpc/data/get_cuid.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/interval_data.py b/pyomo/contrib/mpc/data/interval_data.py index cdd3b0e37dc..54b7ca7e906 100644 --- a/pyomo/contrib/mpc/data/interval_data.py +++ b/pyomo/contrib/mpc/data/interval_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/scalar_data.py b/pyomo/contrib/mpc/data/scalar_data.py index 5426921ef06..b67384c8159 100644 --- a/pyomo/contrib/mpc/data/scalar_data.py +++ b/pyomo/contrib/mpc/data/scalar_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/series_data.py b/pyomo/contrib/mpc/data/series_data.py index d09ab8cae24..c812e76c9fc 100644 --- a/pyomo/contrib/mpc/data/series_data.py +++ b/pyomo/contrib/mpc/data/series_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/tests/__init__.py b/pyomo/contrib/mpc/data/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/mpc/data/tests/__init__.py +++ b/pyomo/contrib/mpc/data/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/tests/test_convert.py b/pyomo/contrib/mpc/data/tests/test_convert.py index 0f8a4623e20..dda3583cb00 100644 --- a/pyomo/contrib/mpc/data/tests/test_convert.py +++ b/pyomo/contrib/mpc/data/tests/test_convert.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/tests/test_find_nearest_index.py b/pyomo/contrib/mpc/data/tests/test_find_nearest_index.py index e90024ef108..8fb92e17534 100644 --- a/pyomo/contrib/mpc/data/tests/test_find_nearest_index.py +++ b/pyomo/contrib/mpc/data/tests/test_find_nearest_index.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/tests/test_get_cuid.py b/pyomo/contrib/mpc/data/tests/test_get_cuid.py index 30ba2b58b1b..66bfb613bcb 100644 --- a/pyomo/contrib/mpc/data/tests/test_get_cuid.py +++ b/pyomo/contrib/mpc/data/tests/test_get_cuid.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/tests/test_interval_data.py b/pyomo/contrib/mpc/data/tests/test_interval_data.py index 8afe3eb3021..b208c9066f9 100644 --- a/pyomo/contrib/mpc/data/tests/test_interval_data.py +++ b/pyomo/contrib/mpc/data/tests/test_interval_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/tests/test_scalar_data.py b/pyomo/contrib/mpc/data/tests/test_scalar_data.py index 110ed749bda..6522242e267 100644 --- a/pyomo/contrib/mpc/data/tests/test_scalar_data.py +++ b/pyomo/contrib/mpc/data/tests/test_scalar_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/data/tests/test_series_data.py b/pyomo/contrib/mpc/data/tests/test_series_data.py index e32559ac074..88b672279f2 100644 --- a/pyomo/contrib/mpc/data/tests/test_series_data.py +++ b/pyomo/contrib/mpc/data/tests/test_series_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/examples/__init__.py b/pyomo/contrib/mpc/examples/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/mpc/examples/__init__.py +++ b/pyomo/contrib/mpc/examples/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/examples/cstr/__init__.py b/pyomo/contrib/mpc/examples/cstr/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/mpc/examples/cstr/__init__.py +++ b/pyomo/contrib/mpc/examples/cstr/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/examples/cstr/model.py b/pyomo/contrib/mpc/examples/cstr/model.py index d794084f122..376e77186dd 100644 --- a/pyomo/contrib/mpc/examples/cstr/model.py +++ b/pyomo/contrib/mpc/examples/cstr/model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/examples/cstr/run_mpc.py b/pyomo/contrib/mpc/examples/cstr/run_mpc.py index 86ae7e4e47b..588ed7d49fe 100644 --- a/pyomo/contrib/mpc/examples/cstr/run_mpc.py +++ b/pyomo/contrib/mpc/examples/cstr/run_mpc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/examples/cstr/run_openloop.py b/pyomo/contrib/mpc/examples/cstr/run_openloop.py index 36ddb990545..66fd0680a01 100644 --- a/pyomo/contrib/mpc/examples/cstr/run_openloop.py +++ b/pyomo/contrib/mpc/examples/cstr/run_openloop.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/examples/cstr/tests/__init__.py b/pyomo/contrib/mpc/examples/cstr/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/mpc/examples/cstr/tests/__init__.py +++ b/pyomo/contrib/mpc/examples/cstr/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/examples/cstr/tests/test_mpc.py b/pyomo/contrib/mpc/examples/cstr/tests/test_mpc.py index 741a1533da3..e808b8fc414 100644 --- a/pyomo/contrib/mpc/examples/cstr/tests/test_mpc.py +++ b/pyomo/contrib/mpc/examples/cstr/tests/test_mpc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/examples/cstr/tests/test_openloop.py b/pyomo/contrib/mpc/examples/cstr/tests/test_openloop.py index 218865ceabb..c21cb55233e 100644 --- a/pyomo/contrib/mpc/examples/cstr/tests/test_openloop.py +++ b/pyomo/contrib/mpc/examples/cstr/tests/test_openloop.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/interfaces/__init__.py b/pyomo/contrib/mpc/interfaces/__init__.py index 8e02003f99e..9b70a983e24 100644 --- a/pyomo/contrib/mpc/interfaces/__init__.py +++ b/pyomo/contrib/mpc/interfaces/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/interfaces/copy_values.py b/pyomo/contrib/mpc/interfaces/copy_values.py index 896656b230d..faf1594f114 100644 --- a/pyomo/contrib/mpc/interfaces/copy_values.py +++ b/pyomo/contrib/mpc/interfaces/copy_values.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/interfaces/load_data.py b/pyomo/contrib/mpc/interfaces/load_data.py index efa9515901e..b1851c3aa51 100644 --- a/pyomo/contrib/mpc/interfaces/load_data.py +++ b/pyomo/contrib/mpc/interfaces/load_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/interfaces/model_interface.py b/pyomo/contrib/mpc/interfaces/model_interface.py index 35f81af4a7a..9a30878c921 100644 --- a/pyomo/contrib/mpc/interfaces/model_interface.py +++ b/pyomo/contrib/mpc/interfaces/model_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/interfaces/tests/__init__.py b/pyomo/contrib/mpc/interfaces/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/mpc/interfaces/tests/__init__.py +++ b/pyomo/contrib/mpc/interfaces/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/interfaces/tests/test_interface.py b/pyomo/contrib/mpc/interfaces/tests/test_interface.py index 65ffc7bb40a..e67e58bf900 100644 --- a/pyomo/contrib/mpc/interfaces/tests/test_interface.py +++ b/pyomo/contrib/mpc/interfaces/tests/test_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/interfaces/tests/test_var_linker.py b/pyomo/contrib/mpc/interfaces/tests/test_var_linker.py index ceec9fada36..e169af686f3 100644 --- a/pyomo/contrib/mpc/interfaces/tests/test_var_linker.py +++ b/pyomo/contrib/mpc/interfaces/tests/test_var_linker.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/interfaces/var_linker.py b/pyomo/contrib/mpc/interfaces/var_linker.py index fd831c9a2c1..87831379204 100644 --- a/pyomo/contrib/mpc/interfaces/var_linker.py +++ b/pyomo/contrib/mpc/interfaces/var_linker.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/modeling/__init__.py b/pyomo/contrib/mpc/modeling/__init__.py index 0eb255a9f56..a174bafc944 100644 --- a/pyomo/contrib/mpc/modeling/__init__.py +++ b/pyomo/contrib/mpc/modeling/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/modeling/constraints.py b/pyomo/contrib/mpc/modeling/constraints.py index 6fb6a311afb..e6a1edf648b 100644 --- a/pyomo/contrib/mpc/modeling/constraints.py +++ b/pyomo/contrib/mpc/modeling/constraints.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/modeling/cost_expressions.py b/pyomo/contrib/mpc/modeling/cost_expressions.py index 65a376e42d2..aeb26705a38 100644 --- a/pyomo/contrib/mpc/modeling/cost_expressions.py +++ b/pyomo/contrib/mpc/modeling/cost_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/modeling/terminal.py b/pyomo/contrib/mpc/modeling/terminal.py index c25efca280a..d2118c7d92e 100644 --- a/pyomo/contrib/mpc/modeling/terminal.py +++ b/pyomo/contrib/mpc/modeling/terminal.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/modeling/tests/__init__.py b/pyomo/contrib/mpc/modeling/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/mpc/modeling/tests/__init__.py +++ b/pyomo/contrib/mpc/modeling/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/modeling/tests/test_cost_expressions.py b/pyomo/contrib/mpc/modeling/tests/test_cost_expressions.py index 5db390ffa47..67c474f7722 100644 --- a/pyomo/contrib/mpc/modeling/tests/test_cost_expressions.py +++ b/pyomo/contrib/mpc/modeling/tests/test_cost_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/modeling/tests/test_input_constraints.py b/pyomo/contrib/mpc/modeling/tests/test_input_constraints.py index e3ba3bf3760..be9edad37b9 100644 --- a/pyomo/contrib/mpc/modeling/tests/test_input_constraints.py +++ b/pyomo/contrib/mpc/modeling/tests/test_input_constraints.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/mpc/modeling/tests/test_terminal.py b/pyomo/contrib/mpc/modeling/tests/test_terminal.py index b835f0b1087..ef89fe24b57 100644 --- a/pyomo/contrib/mpc/modeling/tests/test_terminal.py +++ b/pyomo/contrib/mpc/modeling/tests/test_terminal.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/multistart/__init__.py b/pyomo/contrib/multistart/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/multistart/__init__.py +++ b/pyomo/contrib/multistart/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/multistart/high_conf_stop.py b/pyomo/contrib/multistart/high_conf_stop.py index 153d22e9edd..ce24d2dc1fc 100644 --- a/pyomo/contrib/multistart/high_conf_stop.py +++ b/pyomo/contrib/multistart/high_conf_stop.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/multistart/multi.py b/pyomo/contrib/multistart/multi.py index 867d47d4951..377ac8182e2 100644 --- a/pyomo/contrib/multistart/multi.py +++ b/pyomo/contrib/multistart/multi.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/multistart/plugins.py b/pyomo/contrib/multistart/plugins.py index acfd2f06274..f094e2f58cc 100644 --- a/pyomo/contrib/multistart/plugins.py +++ b/pyomo/contrib/multistart/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/multistart/reinit.py b/pyomo/contrib/multistart/reinit.py index 14dce0352cc..2b097bbc898 100644 --- a/pyomo/contrib/multistart/reinit.py +++ b/pyomo/contrib/multistart/reinit.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/multistart/test_multi.py b/pyomo/contrib/multistart/test_multi.py index a8e3d420266..f8103eed3b8 100644 --- a/pyomo/contrib/multistart/test_multi.py +++ b/pyomo/contrib/multistart/test_multi.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/__init__.py b/pyomo/contrib/parmest/__init__.py index d340885b3fd..e7d513dd95c 100644 --- a/pyomo/contrib/parmest/__init__.py +++ b/pyomo/contrib/parmest/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/__init__.py b/pyomo/contrib/parmest/examples/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/parmest/examples/__init__.py +++ b/pyomo/contrib/parmest/examples/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/__init__.py b/pyomo/contrib/parmest/examples/reaction_kinetics/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/__init__.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py index 719a930251c..7dfe0829262 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/reactor_design/__init__.py b/pyomo/contrib/parmest/examples/reactor_design/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/__init__.py +++ b/pyomo/contrib/parmest/examples/reactor_design/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py index 16ae9343dfd..1a4dc75e083 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index 507a3ee7582..e995502d4ae 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py index cda50ef3efd..8cac82e3879 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py index 448354f600a..0d5665123b4 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index 10d56c8e457..cf77f46f08a 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py index 43af4fbcb94..c53d9ef36dc 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index e86446febd7..65046d76a05 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py index ff6c167f68d..b0b213752cb 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/__init__.py b/pyomo/contrib/parmest/examples/rooney_biegler/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/__init__.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py index 1c82adb909a..49fed17c5b2 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py index 7cd77166a4b..a87beeb4d39 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py index 9aa59be6a17..d11f0738ab4 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py index 7a48dcf190d..724578b419f 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py index 0ad65b1eb7a..0c0de4dc6d8 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/semibatch/__init__.py b/pyomo/contrib/parmest/examples/semibatch/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/parmest/examples/semibatch/__init__.py +++ b/pyomo/contrib/parmest/examples/semibatch/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/semibatch/parallel_example.py b/pyomo/contrib/parmest/examples/semibatch/parallel_example.py index ba69b9f2d06..d7cc497803e 100644 --- a/pyomo/contrib/parmest/examples/semibatch/parallel_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/parallel_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py index fc4c9f5c675..c95d9084dc5 100644 --- a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py index 071e53236c4..853a3770bb7 100644 --- a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/examples/semibatch/semibatch.py b/pyomo/contrib/parmest/examples/semibatch/semibatch.py index 6762531a338..462e5554142 100644 --- a/pyomo/contrib/parmest/examples/semibatch/semibatch.py +++ b/pyomo/contrib/parmest/examples/semibatch/semibatch.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/graphics.py b/pyomo/contrib/parmest/graphics.py index 65efb5cfd64..c57bfb19696 100644 --- a/pyomo/contrib/parmest/graphics.py +++ b/pyomo/contrib/parmest/graphics.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/ipopt_solver_wrapper.py b/pyomo/contrib/parmest/ipopt_solver_wrapper.py index a6d5e0506fb..75c470a4b81 100644 --- a/pyomo/contrib/parmest/ipopt_solver_wrapper.py +++ b/pyomo/contrib/parmest/ipopt_solver_wrapper.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 82bf893dd06..44c256f2019 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 58d2d4da722..b599e5952d2 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/tests/__init__.py b/pyomo/contrib/parmest/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/parmest/tests/__init__.py +++ b/pyomo/contrib/parmest/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index 67e06130384..59a3e0adde2 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/tests/test_graphics.py b/pyomo/contrib/parmest/tests/test_graphics.py index c18659e9948..3b4d0224ebe 100644 --- a/pyomo/contrib/parmest/tests/test_graphics.py +++ b/pyomo/contrib/parmest/tests/test_graphics.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index b5c1fe1bfac..31e083a5f33 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/tests/test_scenariocreator.py b/pyomo/contrib/parmest/tests/test_scenariocreator.py index 22a851ae32e..7db7d0ed5db 100644 --- a/pyomo/contrib/parmest/tests/test_scenariocreator.py +++ b/pyomo/contrib/parmest/tests/test_scenariocreator.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/tests/test_solver.py b/pyomo/contrib/parmest/tests/test_solver.py index eb655023b9b..77eca3a13b6 100644 --- a/pyomo/contrib/parmest/tests/test_solver.py +++ b/pyomo/contrib/parmest/tests/test_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/tests/test_utils.py b/pyomo/contrib/parmest/tests/test_utils.py index 514c14b1e82..e75cc9d3bcd 100644 --- a/pyomo/contrib/parmest/tests/test_utils.py +++ b/pyomo/contrib/parmest/tests/test_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/utils/__init__.py b/pyomo/contrib/parmest/utils/__init__.py index 1615ab206f7..3c6900aa5d9 100644 --- a/pyomo/contrib/parmest/utils/__init__.py +++ b/pyomo/contrib/parmest/utils/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/utils/create_ef.py b/pyomo/contrib/parmest/utils/create_ef.py index 7a7dd72f7da..aaadc7f98b9 100644 --- a/pyomo/contrib/parmest/utils/create_ef.py +++ b/pyomo/contrib/parmest/utils/create_ef.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/utils/ipopt_solver_wrapper.py b/pyomo/contrib/parmest/utils/ipopt_solver_wrapper.py index 7d8289cd181..08388dc5ec1 100644 --- a/pyomo/contrib/parmest/utils/ipopt_solver_wrapper.py +++ b/pyomo/contrib/parmest/utils/ipopt_solver_wrapper.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/utils/model_utils.py b/pyomo/contrib/parmest/utils/model_utils.py index c3c71dc2d6c..77491f74b02 100644 --- a/pyomo/contrib/parmest/utils/model_utils.py +++ b/pyomo/contrib/parmest/utils/model_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/utils/mpi_utils.py b/pyomo/contrib/parmest/utils/mpi_utils.py index 35c4bf137bc..45e3260117d 100644 --- a/pyomo/contrib/parmest/utils/mpi_utils.py +++ b/pyomo/contrib/parmest/utils/mpi_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/parmest/utils/scenario_tree.py b/pyomo/contrib/parmest/utils/scenario_tree.py index 46b02b8ddc1..e71f51877b5 100644 --- a/pyomo/contrib/parmest/utils/scenario_tree.py +++ b/pyomo/contrib/parmest/utils/scenario_tree.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 9e15cfd6670..37873c83b3b 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/piecewise_linear_expression.py b/pyomo/contrib/piecewise/piecewise_linear_expression.py index ea1d95b0f51..ddcb7c6a42f 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_expression.py +++ b/pyomo/contrib/piecewise/piecewise_linear_expression.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 6d4fa658f88..66ca02ad125 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/tests/__init__.py b/pyomo/contrib/piecewise/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/piecewise/tests/__init__.py +++ b/pyomo/contrib/piecewise/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/tests/common_tests.py b/pyomo/contrib/piecewise/tests/common_tests.py index c77d7064544..23e67474934 100644 --- a/pyomo/contrib/piecewise/tests/common_tests.py +++ b/pyomo/contrib/piecewise/tests/common_tests.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/tests/models.py b/pyomo/contrib/piecewise/tests/models.py index be2811a70a4..1a8bef04ad7 100644 --- a/pyomo/contrib/piecewise/tests/models.py +++ b/pyomo/contrib/piecewise/tests/models.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py index a0dbd1cca19..27fe43e54d5 100644 --- a/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py index edc5d9d3d95..5ee18875cb9 100644 --- a/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_outer_repn_gdp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py index e740e5e3384..571601fefbc 100644 --- a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py b/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py index a2d41c04016..b70281c83ed 100644 --- a/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py +++ b/pyomo/contrib/piecewise/tests/test_reduced_inner_repn.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/__init__.py b/pyomo/contrib/piecewise/transform/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/piecewise/transform/__init__.py +++ b/pyomo/contrib/piecewise/transform/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/convex_combination.py b/pyomo/contrib/piecewise/transform/convex_combination.py index abfeac27129..21b72bd9e5d 100644 --- a/pyomo/contrib/piecewise/transform/convex_combination.py +++ b/pyomo/contrib/piecewise/transform/convex_combination.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/disaggregated_convex_combination.py b/pyomo/contrib/piecewise/transform/disaggregated_convex_combination.py index 44059935e09..0117bf1d045 100644 --- a/pyomo/contrib/piecewise/transform/disaggregated_convex_combination.py +++ b/pyomo/contrib/piecewise/transform/disaggregated_convex_combination.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/inner_representation_gdp.py b/pyomo/contrib/piecewise/transform/inner_representation_gdp.py index 627e41aeae9..f0be2d98825 100644 --- a/pyomo/contrib/piecewise/transform/inner_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/inner_representation_gdp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/multiple_choice.py b/pyomo/contrib/piecewise/transform/multiple_choice.py index 97dc8e9d2b3..9291afa8862 100644 --- a/pyomo/contrib/piecewise/transform/multiple_choice.py +++ b/pyomo/contrib/piecewise/transform/multiple_choice.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/outer_representation_gdp.py b/pyomo/contrib/piecewise/transform/outer_representation_gdp.py index 04cd01e1246..7c81619430a 100644 --- a/pyomo/contrib/piecewise/transform/outer_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/outer_representation_gdp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py index ed4902ae6d5..2e056c47a15 100644 --- a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py +++ b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py b/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py index e3347cf206a..fae95a564bf 100644 --- a/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py +++ b/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py b/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py index b89852530d9..5c7dfa895ab 100644 --- a/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/__init__.py b/pyomo/contrib/preprocessing/__init__.py index 40d38e74d23..6458b7a6e71 100644 --- a/pyomo/contrib/preprocessing/__init__.py +++ b/pyomo/contrib/preprocessing/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/__init__.py b/pyomo/contrib/preprocessing/plugins/__init__.py index ae5dfe31682..62f5a40c6a9 100644 --- a/pyomo/contrib/preprocessing/plugins/__init__.py +++ b/pyomo/contrib/preprocessing/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/bounds_to_vars.py b/pyomo/contrib/preprocessing/plugins/bounds_to_vars.py index 33eaa731816..8cc17296ac3 100644 --- a/pyomo/contrib/preprocessing/plugins/bounds_to_vars.py +++ b/pyomo/contrib/preprocessing/plugins/bounds_to_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/constraint_tightener.py b/pyomo/contrib/preprocessing/plugins/constraint_tightener.py index 7c5495e72b8..73851bce618 100644 --- a/pyomo/contrib/preprocessing/plugins/constraint_tightener.py +++ b/pyomo/contrib/preprocessing/plugins/constraint_tightener.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/deactivate_trivial_constraints.py b/pyomo/contrib/preprocessing/plugins/deactivate_trivial_constraints.py index a91e0a292f2..59e475e9ba1 100644 --- a/pyomo/contrib/preprocessing/plugins/deactivate_trivial_constraints.py +++ b/pyomo/contrib/preprocessing/plugins/deactivate_trivial_constraints.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/detect_fixed_vars.py b/pyomo/contrib/preprocessing/plugins/detect_fixed_vars.py index bafbec7b8bd..e48914e0a91 100644 --- a/pyomo/contrib/preprocessing/plugins/detect_fixed_vars.py +++ b/pyomo/contrib/preprocessing/plugins/detect_fixed_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/equality_propagate.py b/pyomo/contrib/preprocessing/plugins/equality_propagate.py index 03e2e11dadb..357a556fcb2 100644 --- a/pyomo/contrib/preprocessing/plugins/equality_propagate.py +++ b/pyomo/contrib/preprocessing/plugins/equality_propagate.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/induced_linearity.py b/pyomo/contrib/preprocessing/plugins/induced_linearity.py index 6378c94e44e..ba291070644 100644 --- a/pyomo/contrib/preprocessing/plugins/induced_linearity.py +++ b/pyomo/contrib/preprocessing/plugins/induced_linearity.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/init_vars.py b/pyomo/contrib/preprocessing/plugins/init_vars.py index 7469722cf23..a81d898d52c 100644 --- a/pyomo/contrib/preprocessing/plugins/init_vars.py +++ b/pyomo/contrib/preprocessing/plugins/init_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/int_to_binary.py b/pyomo/contrib/preprocessing/plugins/int_to_binary.py index 6a08dab9645..e1f7f98a81b 100644 --- a/pyomo/contrib/preprocessing/plugins/int_to_binary.py +++ b/pyomo/contrib/preprocessing/plugins/int_to_binary.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/remove_zero_terms.py b/pyomo/contrib/preprocessing/plugins/remove_zero_terms.py index 256c94d4b7a..ca2052fa471 100644 --- a/pyomo/contrib/preprocessing/plugins/remove_zero_terms.py +++ b/pyomo/contrib/preprocessing/plugins/remove_zero_terms.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/strip_bounds.py b/pyomo/contrib/preprocessing/plugins/strip_bounds.py index 51704bc9d58..196de64e405 100644 --- a/pyomo/contrib/preprocessing/plugins/strip_bounds.py +++ b/pyomo/contrib/preprocessing/plugins/strip_bounds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/var_aggregator.py b/pyomo/contrib/preprocessing/plugins/var_aggregator.py index 651c0ecf7e0..d862f167fd7 100644 --- a/pyomo/contrib/preprocessing/plugins/var_aggregator.py +++ b/pyomo/contrib/preprocessing/plugins/var_aggregator.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/plugins/zero_sum_propagator.py b/pyomo/contrib/preprocessing/plugins/zero_sum_propagator.py index 16c6614cb3b..df6867719d2 100644 --- a/pyomo/contrib/preprocessing/plugins/zero_sum_propagator.py +++ b/pyomo/contrib/preprocessing/plugins/zero_sum_propagator.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/__init__.py b/pyomo/contrib/preprocessing/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/preprocessing/tests/__init__.py +++ b/pyomo/contrib/preprocessing/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/test_bounds_to_vars_xfrm.py b/pyomo/contrib/preprocessing/tests/test_bounds_to_vars_xfrm.py index 534ba11d22f..0df9dd2462d 100644 --- a/pyomo/contrib/preprocessing/tests/test_bounds_to_vars_xfrm.py +++ b/pyomo/contrib/preprocessing/tests/test_bounds_to_vars_xfrm.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/test_constraint_tightener.py b/pyomo/contrib/preprocessing/tests/test_constraint_tightener.py index 808eb688087..acb939552f8 100644 --- a/pyomo/contrib/preprocessing/tests/test_constraint_tightener.py +++ b/pyomo/contrib/preprocessing/tests/test_constraint_tightener.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/test_deactivate_trivial_constraints.py b/pyomo/contrib/preprocessing/tests/test_deactivate_trivial_constraints.py index 0c89b0d7d86..9e26aab8b77 100644 --- a/pyomo/contrib/preprocessing/tests/test_deactivate_trivial_constraints.py +++ b/pyomo/contrib/preprocessing/tests/test_deactivate_trivial_constraints.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/test_detect_fixed_vars.py b/pyomo/contrib/preprocessing/tests/test_detect_fixed_vars.py index f18ac5c3b8a..a67291dc69f 100644 --- a/pyomo/contrib/preprocessing/tests/test_detect_fixed_vars.py +++ b/pyomo/contrib/preprocessing/tests/test_detect_fixed_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/test_equality_propagate.py b/pyomo/contrib/preprocessing/tests/test_equality_propagate.py index a2c00a15e72..6b12f464710 100644 --- a/pyomo/contrib/preprocessing/tests/test_equality_propagate.py +++ b/pyomo/contrib/preprocessing/tests/test_equality_propagate.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/test_induced_linearity.py b/pyomo/contrib/preprocessing/tests/test_induced_linearity.py index c2c24c33f14..4853cb838df 100644 --- a/pyomo/contrib/preprocessing/tests/test_induced_linearity.py +++ b/pyomo/contrib/preprocessing/tests/test_induced_linearity.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/test_init_vars.py b/pyomo/contrib/preprocessing/tests/test_init_vars.py index 6ddf859e930..a90d39af91c 100644 --- a/pyomo/contrib/preprocessing/tests/test_init_vars.py +++ b/pyomo/contrib/preprocessing/tests/test_init_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/test_int_to_binary.py b/pyomo/contrib/preprocessing/tests/test_int_to_binary.py index bb75a075592..8aa244212ed 100644 --- a/pyomo/contrib/preprocessing/tests/test_int_to_binary.py +++ b/pyomo/contrib/preprocessing/tests/test_int_to_binary.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/test_strip_bounds.py b/pyomo/contrib/preprocessing/tests/test_strip_bounds.py index 4eec0cf7434..f36ff4e9f52 100644 --- a/pyomo/contrib/preprocessing/tests/test_strip_bounds.py +++ b/pyomo/contrib/preprocessing/tests/test_strip_bounds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/test_var_aggregator.py b/pyomo/contrib/preprocessing/tests/test_var_aggregator.py index b3630225402..6f6d02f2180 100644 --- a/pyomo/contrib/preprocessing/tests/test_var_aggregator.py +++ b/pyomo/contrib/preprocessing/tests/test_var_aggregator.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/test_zero_sum_propagate.py b/pyomo/contrib/preprocessing/tests/test_zero_sum_propagate.py index ce88b8ca86e..41ece8e804f 100644 --- a/pyomo/contrib/preprocessing/tests/test_zero_sum_propagate.py +++ b/pyomo/contrib/preprocessing/tests/test_zero_sum_propagate.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/tests/test_zero_term_removal.py b/pyomo/contrib/preprocessing/tests/test_zero_term_removal.py index abe79034ec2..c5b7477c8f6 100644 --- a/pyomo/contrib/preprocessing/tests/test_zero_term_removal.py +++ b/pyomo/contrib/preprocessing/tests/test_zero_term_removal.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/preprocessing/util.py b/pyomo/contrib/preprocessing/util.py index ffc72f46902..13f3e5dd18c 100644 --- a/pyomo/contrib/preprocessing/util.py +++ b/pyomo/contrib/preprocessing/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/__init__.py b/pyomo/contrib/pynumero/__init__.py index 9364a552999..39ee2197cbf 100644 --- a/pyomo/contrib/pynumero/__init__.py +++ b/pyomo/contrib/pynumero/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/__init__.py b/pyomo/contrib/pynumero/algorithms/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/algorithms/__init__.py +++ b/pyomo/contrib/pynumero/algorithms/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/__init__.py b/pyomo/contrib/pynumero/algorithms/solvers/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/__init__.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py index cedbf430a12..9d24c0dd562 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py b/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py index e0bc0170d33..e40580c1161 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py b/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py index b234d2f0890..16c5a19a5c6 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/scipy_solvers.py b/pyomo/contrib/pynumero/algorithms/solvers/scipy_solvers.py index 53f657c984f..ec1f106b73c 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/scipy_solvers.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/scipy_solvers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/square_solver_base.py b/pyomo/contrib/pynumero/algorithms/solvers/square_solver_base.py index c4a33d97611..1be3032c358 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/square_solver_base.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/square_solver_base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/__init__.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/__init__.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_interfaces.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_interfaces.py index 119c4604f19..88d4df1e17d 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_interfaces.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_interfaces.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py index 7ead30117cb..e9da31097a0 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_implicit_functions.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_implicit_functions.py index 04d4ed321f1..3a13c1a7598 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_implicit_functions.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_implicit_functions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_pyomo_ext_cyipopt.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_pyomo_ext_cyipopt.py index 82a37873d5f..0036a6b3623 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_pyomo_ext_cyipopt.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_pyomo_ext_cyipopt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_scipy_solvers.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_scipy_solvers.py index 6636dc3d6e2..33b58f17887 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_scipy_solvers.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_scipy_solvers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/asl.py b/pyomo/contrib/pynumero/asl.py index a28741fb230..55ecc7fd0ee 100644 --- a/pyomo/contrib/pynumero/asl.py +++ b/pyomo/contrib/pynumero/asl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/build.py b/pyomo/contrib/pynumero/build.py index 08b5c512ab7..bb8443640d5 100644 --- a/pyomo/contrib/pynumero/build.py +++ b/pyomo/contrib/pynumero/build.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/dependencies.py b/pyomo/contrib/pynumero/dependencies.py index d386bbc3dda..9e2088ffa0a 100644 --- a/pyomo/contrib/pynumero/dependencies.py +++ b/pyomo/contrib/pynumero/dependencies.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/__init__.py b/pyomo/contrib/pynumero/examples/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/examples/__init__.py +++ b/pyomo/contrib/pynumero/examples/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/callback/__init__.py b/pyomo/contrib/pynumero/examples/callback/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/examples/callback/__init__.py +++ b/pyomo/contrib/pynumero/examples/callback/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/callback/cyipopt_callback.py b/pyomo/contrib/pynumero/examples/callback/cyipopt_callback.py index 58367e0bc5a..f66374f6213 100644 --- a/pyomo/contrib/pynumero/examples/callback/cyipopt_callback.py +++ b/pyomo/contrib/pynumero/examples/callback/cyipopt_callback.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/callback/cyipopt_callback_halt.py b/pyomo/contrib/pynumero/examples/callback/cyipopt_callback_halt.py index 55138c99318..9e88f8d4964 100644 --- a/pyomo/contrib/pynumero/examples/callback/cyipopt_callback_halt.py +++ b/pyomo/contrib/pynumero/examples/callback/cyipopt_callback_halt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/callback/cyipopt_functor_callback.py b/pyomo/contrib/pynumero/examples/callback/cyipopt_functor_callback.py index b52897d58b1..4befc816e1b 100644 --- a/pyomo/contrib/pynumero/examples/callback/cyipopt_functor_callback.py +++ b/pyomo/contrib/pynumero/examples/callback/cyipopt_functor_callback.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/callback/reactor_design.py b/pyomo/contrib/pynumero/examples/callback/reactor_design.py index 98fbc93ee58..3d9e19a446e 100644 --- a/pyomo/contrib/pynumero/examples/callback/reactor_design.py +++ b/pyomo/contrib/pynumero/examples/callback/reactor_design.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/__init__.py b/pyomo/contrib/pynumero/examples/external_grey_box/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/__init__.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/__init__.py b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/__init__.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/generate_data.py b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/generate_data.py index 3af10a465b7..65bb2c82de8 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/generate_data.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/generate_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/models.py b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/models.py index a7962f5634d..c6560b4f9c5 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/models.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/models.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/perform_estimation.py b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/perform_estimation.py index 9a18c7fb54b..142b47f8172 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/param_est/perform_estimation.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/param_est/perform_estimation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/__init__.py b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/__init__.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_outputs.py b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_outputs.py index 9f683b146fe..e6afd8995a2 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_outputs.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_outputs.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_ratio_residuals.py b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_ratio_residuals.py index 26d70c7921e..415b58bee54 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_ratio_residuals.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/maximize_cb_ratio_residuals.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_outputs.py b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_outputs.py index 6e6c997880b..ef8b2783237 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_outputs.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_outputs.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_residuals.py b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_residuals.py index 69a79425750..bc5a2ca4ce4 100644 --- a/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_residuals.py +++ b/pyomo/contrib/pynumero/examples/external_grey_box/react_example/reactor_model_residuals.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/feasibility.py b/pyomo/contrib/pynumero/examples/feasibility.py index 94baabb7bec..59e4edcc9ec 100644 --- a/pyomo/contrib/pynumero/examples/feasibility.py +++ b/pyomo/contrib/pynumero/examples/feasibility.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/mumps_example.py b/pyomo/contrib/pynumero/examples/mumps_example.py index 7f96bfce4ae..588ce58bc12 100644 --- a/pyomo/contrib/pynumero/examples/mumps_example.py +++ b/pyomo/contrib/pynumero/examples/mumps_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/nlp_interface.py b/pyomo/contrib/pynumero/examples/nlp_interface.py index 730e0fbda47..556b8ec0713 100644 --- a/pyomo/contrib/pynumero/examples/nlp_interface.py +++ b/pyomo/contrib/pynumero/examples/nlp_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/nlp_interface_2.py b/pyomo/contrib/pynumero/examples/nlp_interface_2.py index ecd63d28c49..4a288a178b1 100644 --- a/pyomo/contrib/pynumero/examples/nlp_interface_2.py +++ b/pyomo/contrib/pynumero/examples/nlp_interface_2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/parallel_matvec.py b/pyomo/contrib/pynumero/examples/parallel_matvec.py index 78095fe1acd..cd77bcfabc9 100644 --- a/pyomo/contrib/pynumero/examples/parallel_matvec.py +++ b/pyomo/contrib/pynumero/examples/parallel_matvec.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/parallel_vector_ops.py b/pyomo/contrib/pynumero/examples/parallel_vector_ops.py index 83e40a342db..fe49ff29e59 100644 --- a/pyomo/contrib/pynumero/examples/parallel_vector_ops.py +++ b/pyomo/contrib/pynumero/examples/parallel_vector_ops.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/sensitivity.py b/pyomo/contrib/pynumero/examples/sensitivity.py index a3927d637b3..0bb0fb3a740 100644 --- a/pyomo/contrib/pynumero/examples/sensitivity.py +++ b/pyomo/contrib/pynumero/examples/sensitivity.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/sqp.py b/pyomo/contrib/pynumero/examples/sqp.py index 15ad62670f2..925cab4c20b 100644 --- a/pyomo/contrib/pynumero/examples/sqp.py +++ b/pyomo/contrib/pynumero/examples/sqp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/tests/__init__.py b/pyomo/contrib/pynumero/examples/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/examples/tests/__init__.py +++ b/pyomo/contrib/pynumero/examples/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py index 167b0601f7a..1f45f26d43b 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/tests/test_examples.py b/pyomo/contrib/pynumero/examples/tests/test_examples.py index d4a5313908c..d1494bab557 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/examples/tests/test_mpi_examples.py b/pyomo/contrib/pynumero/examples/tests/test_mpi_examples.py index 554305f23c9..1ee02bb70ca 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_mpi_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_mpi_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/exceptions.py b/pyomo/contrib/pynumero/exceptions.py index dc2167d75d2..6b46dd2d9a7 100644 --- a/pyomo/contrib/pynumero/exceptions.py +++ b/pyomo/contrib/pynumero/exceptions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/__init__.py b/pyomo/contrib/pynumero/interfaces/__init__.py index debe453e175..e2de0dd25cc 100644 --- a/pyomo/contrib/pynumero/interfaces/__init__.py +++ b/pyomo/contrib/pynumero/interfaces/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/ampl_nlp.py b/pyomo/contrib/pynumero/interfaces/ampl_nlp.py index f5bd56696cf..c19d252667d 100644 --- a/pyomo/contrib/pynumero/interfaces/ampl_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/ampl_nlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py index fc9c45c6d1a..7845a4c189e 100644 --- a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py +++ b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/external_grey_box.py b/pyomo/contrib/pynumero/interfaces/external_grey_box.py index 642fd3bf310..7e42f161bee 100644 --- a/pyomo/contrib/pynumero/interfaces/external_grey_box.py +++ b/pyomo/contrib/pynumero/interfaces/external_grey_box.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/external_pyomo_model.py b/pyomo/contrib/pynumero/interfaces/external_pyomo_model.py index d0e6c21fa64..bae3e0b8159 100644 --- a/pyomo/contrib/pynumero/interfaces/external_pyomo_model.py +++ b/pyomo/contrib/pynumero/interfaces/external_pyomo_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/nlp.py b/pyomo/contrib/pynumero/interfaces/nlp.py index 95c05f06a61..20b3a5e4938 100644 --- a/pyomo/contrib/pynumero/interfaces/nlp.py +++ b/pyomo/contrib/pynumero/interfaces/nlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/nlp_projections.py b/pyomo/contrib/pynumero/interfaces/nlp_projections.py index 3f4e8a88c60..4be3cd28dd5 100644 --- a/pyomo/contrib/pynumero/interfaces/nlp_projections.py +++ b/pyomo/contrib/pynumero/interfaces/nlp_projections.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py index 945e9a05f51..e6ed40e9974 100644 --- a/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py index 8017c642854..f9014ab29c0 100644 --- a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py @@ -1,6 +1,6 @@ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/__init__.py b/pyomo/contrib/pynumero/interfaces/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/__init__.py +++ b/pyomo/contrib/pynumero/interfaces/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/compare_utils.py b/pyomo/contrib/pynumero/interfaces/tests/compare_utils.py index d30cfb8f56a..8296ea2d1af 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/compare_utils.py +++ b/pyomo/contrib/pynumero/interfaces/tests/compare_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/external_grey_box_models.py b/pyomo/contrib/pynumero/interfaces/tests/external_grey_box_models.py index d7ec499eaf9..b81731b209e 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/external_grey_box_models.py +++ b/pyomo/contrib/pynumero/interfaces/tests/external_grey_box_models.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_cyipopt_interface.py b/pyomo/contrib/pynumero/interfaces/tests/test_cyipopt_interface.py index f28b7b9b549..bbcd6d4f26d 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_cyipopt_interface.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_cyipopt_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_dynamic_model.py b/pyomo/contrib/pynumero/interfaces/tests/test_dynamic_model.py index ddd56afb5b4..5b8a8d688dd 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_dynamic_model.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_dynamic_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_external_asl_function.py b/pyomo/contrib/pynumero/interfaces/tests/test_external_asl_function.py index 88a4024aeeb..9ca0aef4187 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_external_asl_function.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_external_asl_function.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_external_grey_box_model.py b/pyomo/contrib/pynumero/interfaces/tests/test_external_grey_box_model.py index 58e08a409f0..0fc342c4e40 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_external_grey_box_model.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_external_grey_box_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_block.py b/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_block.py index 7e250b9194e..913d3055c9c 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_block.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_block.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_model.py b/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_model.py index 390d0b6fe63..9773fa7e4a8 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_model.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_external_pyomo_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py b/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py index 38d44473a67..4f735e06de7 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_nlp_projections.py b/pyomo/contrib/pynumero/interfaces/tests/test_nlp_projections.py index 7bf693b1eb6..2fada5f679a 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_nlp_projections.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_nlp_projections.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_pyomo_grey_box_nlp.py b/pyomo/contrib/pynumero/interfaces/tests/test_pyomo_grey_box_nlp.py index 52536dd9c06..ecadf40e5cf 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_pyomo_grey_box_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_pyomo_grey_box_nlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_utils.py b/pyomo/contrib/pynumero/interfaces/tests/test_utils.py index dafe89ca2c7..474d26836b9 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_utils.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/interfaces/utils.py b/pyomo/contrib/pynumero/interfaces/utils.py index c7bd04eb002..2aa30fc5946 100644 --- a/pyomo/contrib/pynumero/interfaces/utils.py +++ b/pyomo/contrib/pynumero/interfaces/utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/intrinsic.py b/pyomo/contrib/pynumero/intrinsic.py index 5a2dccb64e7..84675cc4c02 100644 --- a/pyomo/contrib/pynumero/intrinsic.py +++ b/pyomo/contrib/pynumero/intrinsic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/__init__.py b/pyomo/contrib/pynumero/linalg/__init__.py index 09bccd7449b..c1d9ff38825 100644 --- a/pyomo/contrib/pynumero/linalg/__init__.py +++ b/pyomo/contrib/pynumero/linalg/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/base.py b/pyomo/contrib/pynumero/linalg/base.py index 7f3d1ffa115..21565b052a5 100644 --- a/pyomo/contrib/pynumero/linalg/base.py +++ b/pyomo/contrib/pynumero/linalg/base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/ma27.py b/pyomo/contrib/pynumero/linalg/ma27.py index 21c137e837b..40a7d0e1064 100644 --- a/pyomo/contrib/pynumero/linalg/ma27.py +++ b/pyomo/contrib/pynumero/linalg/ma27.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/ma27_interface.py b/pyomo/contrib/pynumero/linalg/ma27_interface.py index d974cfc1263..42ac6e73154 100644 --- a/pyomo/contrib/pynumero/linalg/ma27_interface.py +++ b/pyomo/contrib/pynumero/linalg/ma27_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/ma57.py b/pyomo/contrib/pynumero/linalg/ma57.py index 1be6c8abcf7..baaa3f34100 100644 --- a/pyomo/contrib/pynumero/linalg/ma57.py +++ b/pyomo/contrib/pynumero/linalg/ma57.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/ma57_interface.py b/pyomo/contrib/pynumero/linalg/ma57_interface.py index dcd47795256..93004406612 100644 --- a/pyomo/contrib/pynumero/linalg/ma57_interface.py +++ b/pyomo/contrib/pynumero/linalg/ma57_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/mumps_interface.py b/pyomo/contrib/pynumero/linalg/mumps_interface.py index baab5562716..8735994f16c 100644 --- a/pyomo/contrib/pynumero/linalg/mumps_interface.py +++ b/pyomo/contrib/pynumero/linalg/mumps_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/scipy_interface.py b/pyomo/contrib/pynumero/linalg/scipy_interface.py index a5a53690eb0..025cc539245 100644 --- a/pyomo/contrib/pynumero/linalg/scipy_interface.py +++ b/pyomo/contrib/pynumero/linalg/scipy_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/tests/__init__.py b/pyomo/contrib/pynumero/linalg/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/linalg/tests/__init__.py +++ b/pyomo/contrib/pynumero/linalg/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/tests/test_linear_solvers.py b/pyomo/contrib/pynumero/linalg/tests/test_linear_solvers.py index d5025042d95..d2fa955434c 100644 --- a/pyomo/contrib/pynumero/linalg/tests/test_linear_solvers.py +++ b/pyomo/contrib/pynumero/linalg/tests/test_linear_solvers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/tests/test_ma27.py b/pyomo/contrib/pynumero/linalg/tests/test_ma27.py index 5a02871306a..979be6f747a 100644 --- a/pyomo/contrib/pynumero/linalg/tests/test_ma27.py +++ b/pyomo/contrib/pynumero/linalg/tests/test_ma27.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/tests/test_ma57.py b/pyomo/contrib/pynumero/linalg/tests/test_ma57.py index 86dbbd3ca50..de245172f96 100644 --- a/pyomo/contrib/pynumero/linalg/tests/test_ma57.py +++ b/pyomo/contrib/pynumero/linalg/tests/test_ma57.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/tests/test_mumps_interface.py b/pyomo/contrib/pynumero/linalg/tests/test_mumps_interface.py index 9b0aba96be1..8e5b924fb65 100644 --- a/pyomo/contrib/pynumero/linalg/tests/test_mumps_interface.py +++ b/pyomo/contrib/pynumero/linalg/tests/test_mumps_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/linalg/utils.py b/pyomo/contrib/pynumero/linalg/utils.py index 2b7a9e99142..adec9ae5f35 100644 --- a/pyomo/contrib/pynumero/linalg/utils.py +++ b/pyomo/contrib/pynumero/linalg/utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/plugins.py b/pyomo/contrib/pynumero/plugins.py index 06bb0a5a059..c6890cbbb4d 100644 --- a/pyomo/contrib/pynumero/plugins.py +++ b/pyomo/contrib/pynumero/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/__init__.py b/pyomo/contrib/pynumero/sparse/__init__.py index e72d1cd7b2d..ee8196566db 100644 --- a/pyomo/contrib/pynumero/sparse/__init__.py +++ b/pyomo/contrib/pynumero/sparse/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/base_block.py b/pyomo/contrib/pynumero/sparse/base_block.py index 4f2ae385a7e..0b923ce6efb 100644 --- a/pyomo/contrib/pynumero/sparse/base_block.py +++ b/pyomo/contrib/pynumero/sparse/base_block.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/block_matrix.py b/pyomo/contrib/pynumero/sparse/block_matrix.py index 97e090fec4c..ba7ed4f085b 100644 --- a/pyomo/contrib/pynumero/sparse/block_matrix.py +++ b/pyomo/contrib/pynumero/sparse/block_matrix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/block_vector.py b/pyomo/contrib/pynumero/sparse/block_vector.py index 00733a71752..2b529736935 100644 --- a/pyomo/contrib/pynumero/sparse/block_vector.py +++ b/pyomo/contrib/pynumero/sparse/block_vector.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/mpi_block_matrix.py b/pyomo/contrib/pynumero/sparse/mpi_block_matrix.py index ee045464dec..28a39b4e2eb 100644 --- a/pyomo/contrib/pynumero/sparse/mpi_block_matrix.py +++ b/pyomo/contrib/pynumero/sparse/mpi_block_matrix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/mpi_block_vector.py b/pyomo/contrib/pynumero/sparse/mpi_block_vector.py index 0f57f0eb41e..be8091c9597 100644 --- a/pyomo/contrib/pynumero/sparse/mpi_block_vector.py +++ b/pyomo/contrib/pynumero/sparse/mpi_block_vector.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/tests/__init__.py b/pyomo/contrib/pynumero/sparse/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/sparse/tests/__init__.py +++ b/pyomo/contrib/pynumero/sparse/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/tests/test_block_matrix.py b/pyomo/contrib/pynumero/sparse/tests/test_block_matrix.py index 7402881a285..48c1d3dc77e 100644 --- a/pyomo/contrib/pynumero/sparse/tests/test_block_matrix.py +++ b/pyomo/contrib/pynumero/sparse/tests/test_block_matrix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/tests/test_block_vector.py b/pyomo/contrib/pynumero/sparse/tests/test_block_vector.py index 780a8bc2609..610d41a09a4 100644 --- a/pyomo/contrib/pynumero/sparse/tests/test_block_vector.py +++ b/pyomo/contrib/pynumero/sparse/tests/test_block_vector.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/tests/test_intrinsics.py b/pyomo/contrib/pynumero/sparse/tests/test_intrinsics.py index 0768442c2c4..ef0a5142849 100644 --- a/pyomo/contrib/pynumero/sparse/tests/test_intrinsics.py +++ b/pyomo/contrib/pynumero/sparse/tests/test_intrinsics.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_matrix.py b/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_matrix.py index 1415636c50d..917b4433120 100644 --- a/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_matrix.py +++ b/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_matrix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_vector.py b/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_vector.py index cd37b7543a2..c28c524823a 100644 --- a/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_vector.py +++ b/pyomo/contrib/pynumero/sparse/tests/test_mpi_block_vector.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/src/AmplInterface.cpp b/pyomo/contrib/pynumero/src/AmplInterface.cpp index 26053a9611b..805955f7671 100644 --- a/pyomo/contrib/pynumero/src/AmplInterface.cpp +++ b/pyomo/contrib/pynumero/src/AmplInterface.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/src/AmplInterface.hpp b/pyomo/contrib/pynumero/src/AmplInterface.hpp index 259cf88d895..bedc6d4f669 100644 --- a/pyomo/contrib/pynumero/src/AmplInterface.hpp +++ b/pyomo/contrib/pynumero/src/AmplInterface.hpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/src/AssertUtils.hpp b/pyomo/contrib/pynumero/src/AssertUtils.hpp index ba2e5dc887f..061442eb6e9 100644 --- a/pyomo/contrib/pynumero/src/AssertUtils.hpp +++ b/pyomo/contrib/pynumero/src/AssertUtils.hpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/src/ma27Interface.cpp b/pyomo/contrib/pynumero/src/ma27Interface.cpp index 29ffef73938..4816e1274e3 100644 --- a/pyomo/contrib/pynumero/src/ma27Interface.cpp +++ b/pyomo/contrib/pynumero/src/ma27Interface.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/src/ma57Interface.cpp b/pyomo/contrib/pynumero/src/ma57Interface.cpp index a0fb60edcc8..fa9cf4e6811 100644 --- a/pyomo/contrib/pynumero/src/ma57Interface.cpp +++ b/pyomo/contrib/pynumero/src/ma57Interface.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/src/tests/simple_test.cpp b/pyomo/contrib/pynumero/src/tests/simple_test.cpp index 30255912c1f..9f39fbbd8ff 100644 --- a/pyomo/contrib/pynumero/src/tests/simple_test.cpp +++ b/pyomo/contrib/pynumero/src/tests/simple_test.cpp @@ -1,7 +1,7 @@ /**___________________________________________________________________________ * * Pyomo: Python Optimization Modeling Objects - * Copyright (c) 2008-2022 + * Copyright (c) 2008-2024 * National Technology and Engineering Solutions of Sandia, LLC * Under the terms of Contract DE-NA0003525 with National Technology and * Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pynumero/tests/__init__.py b/pyomo/contrib/pynumero/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pynumero/tests/__init__.py +++ b/pyomo/contrib/pynumero/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pyros/__init__.py b/pyomo/contrib/pyros/__init__.py index 8ecd8ee7478..4e134ef1166 100644 --- a/pyomo/contrib/pyros/__init__.py +++ b/pyomo/contrib/pyros/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index a4b1785a987..abf02809396 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 829184fc70c..475eb424c0b 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 61615652a01..45b652447ff 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index e37d3325a57..084b0442ae6 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index 3ee22af9749..bc6c071c9a3 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pyros/tests/__init__.py b/pyomo/contrib/pyros/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/pyros/tests/__init__.py +++ b/pyomo/contrib/pyros/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index c49c131fdf8..fc215f86a7c 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 7e13026b1e9..17b51be709b 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index c5a80d27102..97fa42c32e9 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/satsolver/__init__.py b/pyomo/contrib/satsolver/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/satsolver/__init__.py +++ b/pyomo/contrib/satsolver/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/satsolver/satsolver.py b/pyomo/contrib/satsolver/satsolver.py index 50e471253e2..b5004d6a611 100644 --- a/pyomo/contrib/satsolver/satsolver.py +++ b/pyomo/contrib/satsolver/satsolver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/satsolver/test_satsolver.py b/pyomo/contrib/satsolver/test_satsolver.py index 7ac7aaff03f..f19f172f7b2 100644 --- a/pyomo/contrib/satsolver/test_satsolver.py +++ b/pyomo/contrib/satsolver/test_satsolver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/__init__.py b/pyomo/contrib/sensitivity_toolbox/__init__.py index feb094b6b76..a20cbc389d7 100644 --- a/pyomo/contrib/sensitivity_toolbox/__init__.py +++ b/pyomo/contrib/sensitivity_toolbox/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,7 +12,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/examples/HIV_Transmission.py b/pyomo/contrib/sensitivity_toolbox/examples/HIV_Transmission.py index 2c8996c95ca..8d43dea26b2 100755 --- a/pyomo/contrib/sensitivity_toolbox/examples/HIV_Transmission.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/HIV_Transmission.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/examples/__init__.py b/pyomo/contrib/sensitivity_toolbox/examples/__init__.py index d67dc03be6c..a408b878891 100644 --- a/pyomo/contrib/sensitivity_toolbox/examples/__init__.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,7 +12,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/examples/feedbackController.py b/pyomo/contrib/sensitivity_toolbox/examples/feedbackController.py index 1112a0c82b3..d973bedf5ba 100644 --- a/pyomo/contrib/sensitivity_toolbox/examples/feedbackController.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/feedbackController.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/examples/parameter.py b/pyomo/contrib/sensitivity_toolbox/examples/parameter.py index 3ed1628f2c2..85d31d3303e 100644 --- a/pyomo/contrib/sensitivity_toolbox/examples/parameter.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/parameter.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/examples/parameter_kaug.py b/pyomo/contrib/sensitivity_toolbox/examples/parameter_kaug.py index f54e7903442..c5e61307046 100644 --- a/pyomo/contrib/sensitivity_toolbox/examples/parameter_kaug.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/parameter_kaug.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/examples/rangeInequality.py b/pyomo/contrib/sensitivity_toolbox/examples/rangeInequality.py index 39e4d26f695..b06cc8390d2 100644 --- a/pyomo/contrib/sensitivity_toolbox/examples/rangeInequality.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/rangeInequality.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/examples/rooney_biegler.py b/pyomo/contrib/sensitivity_toolbox/examples/rooney_biegler.py index 350860a0b50..3efb20bd44b 100644 --- a/pyomo/contrib/sensitivity_toolbox/examples/rooney_biegler.py +++ b/pyomo/contrib/sensitivity_toolbox/examples/rooney_biegler.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/k_aug.py b/pyomo/contrib/sensitivity_toolbox/k_aug.py index e7ccb4960a5..a7fc10569fe 100644 --- a/pyomo/contrib/sensitivity_toolbox/k_aug.py +++ b/pyomo/contrib/sensitivity_toolbox/k_aug.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,7 +12,7 @@ # ______________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/sens.py b/pyomo/contrib/sensitivity_toolbox/sens.py index 43279c7bc5e..a3d69b2c7b1 100644 --- a/pyomo/contrib/sensitivity_toolbox/sens.py +++ b/pyomo/contrib/sensitivity_toolbox/sens.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,7 +12,7 @@ # ______________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/tests/__init__.py b/pyomo/contrib/sensitivity_toolbox/tests/__init__.py index 5aecf9868db..53f447ece43 100644 --- a/pyomo/contrib/sensitivity_toolbox/tests/__init__.py +++ b/pyomo/contrib/sensitivity_toolbox/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,7 +12,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/tests/test_k_aug_interface.py b/pyomo/contrib/sensitivity_toolbox/tests/test_k_aug_interface.py index cb219f4f403..e941656a392 100644 --- a/pyomo/contrib/sensitivity_toolbox/tests/test_k_aug_interface.py +++ b/pyomo/contrib/sensitivity_toolbox/tests/test_k_aug_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,7 +12,7 @@ # ____________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py b/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py index 76d180ae422..69cf0303987 100644 --- a/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py +++ b/pyomo/contrib/sensitivity_toolbox/tests/test_sens.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,7 +12,7 @@ # ____________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/sensitivity_toolbox/tests/test_sens_unit.py b/pyomo/contrib/sensitivity_toolbox/tests/test_sens_unit.py index d6da0d814e8..9f4bcb2b497 100644 --- a/pyomo/contrib/sensitivity_toolbox/tests/test_sens_unit.py +++ b/pyomo/contrib/sensitivity_toolbox/tests/test_sens_unit.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -12,7 +12,7 @@ # ____________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/simplemodel/__init__.py b/pyomo/contrib/simplemodel/__init__.py index 4fa4fa2dd16..f2f4922223e 100644 --- a/pyomo/contrib/simplemodel/__init__.py +++ b/pyomo/contrib/simplemodel/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/TRF.py b/pyomo/contrib/trustregion/TRF.py index 45e60df7658..6d2cf863d69 100644 --- a/pyomo/contrib/trustregion/TRF.py +++ b/pyomo/contrib/trustregion/TRF.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/__init__.py b/pyomo/contrib/trustregion/__init__.py index 62ba0892686..38b30839be3 100644 --- a/pyomo/contrib/trustregion/__init__.py +++ b/pyomo/contrib/trustregion/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/examples/__init__.py b/pyomo/contrib/trustregion/examples/__init__.py index 62ba0892686..38b30839be3 100644 --- a/pyomo/contrib/trustregion/examples/__init__.py +++ b/pyomo/contrib/trustregion/examples/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/examples/example1.py b/pyomo/contrib/trustregion/examples/example1.py index 19965ff1cb2..66df26d143f 100755 --- a/pyomo/contrib/trustregion/examples/example1.py +++ b/pyomo/contrib/trustregion/examples/example1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/examples/example2.py b/pyomo/contrib/trustregion/examples/example2.py index 0c506eb6891..ad648855410 100644 --- a/pyomo/contrib/trustregion/examples/example2.py +++ b/pyomo/contrib/trustregion/examples/example2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/filter.py b/pyomo/contrib/trustregion/filter.py index 2f0b20ee8f8..7e647a7f0c5 100644 --- a/pyomo/contrib/trustregion/filter.py +++ b/pyomo/contrib/trustregion/filter.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/interface.py b/pyomo/contrib/trustregion/interface.py index f68f2fdb308..b459e7cfa17 100644 --- a/pyomo/contrib/trustregion/interface.py +++ b/pyomo/contrib/trustregion/interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/plugins.py b/pyomo/contrib/trustregion/plugins.py index 59a11986f3c..d4ed22b9d2f 100644 --- a/pyomo/contrib/trustregion/plugins.py +++ b/pyomo/contrib/trustregion/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/tests/__init__.py b/pyomo/contrib/trustregion/tests/__init__.py index 62ba0892686..38b30839be3 100644 --- a/pyomo/contrib/trustregion/tests/__init__.py +++ b/pyomo/contrib/trustregion/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/tests/test_TRF.py b/pyomo/contrib/trustregion/tests/test_TRF.py index e14a784b4af..e2b2b2b64ad 100644 --- a/pyomo/contrib/trustregion/tests/test_TRF.py +++ b/pyomo/contrib/trustregion/tests/test_TRF.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/tests/test_examples.py b/pyomo/contrib/trustregion/tests/test_examples.py index a954b0851c7..5451cca5961 100644 --- a/pyomo/contrib/trustregion/tests/test_examples.py +++ b/pyomo/contrib/trustregion/tests/test_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/tests/test_filter.py b/pyomo/contrib/trustregion/tests/test_filter.py index 1b89d8d5cd1..18e833685f8 100644 --- a/pyomo/contrib/trustregion/tests/test_filter.py +++ b/pyomo/contrib/trustregion/tests/test_filter.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/tests/test_interface.py b/pyomo/contrib/trustregion/tests/test_interface.py index a7e6457a5ca..148caceddd1 100644 --- a/pyomo/contrib/trustregion/tests/test_interface.py +++ b/pyomo/contrib/trustregion/tests/test_interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/tests/test_util.py b/pyomo/contrib/trustregion/tests/test_util.py index 3054c2c2bd5..bdc91744e61 100644 --- a/pyomo/contrib/trustregion/tests/test_util.py +++ b/pyomo/contrib/trustregion/tests/test_util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/trustregion/util.py b/pyomo/contrib/trustregion/util.py index f27420a2bee..ff3f218fc27 100644 --- a/pyomo/contrib/trustregion/util.py +++ b/pyomo/contrib/trustregion/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/__init__.py b/pyomo/contrib/viewer/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/viewer/__init__.py +++ b/pyomo/contrib/viewer/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/model_browser.py b/pyomo/contrib/viewer/model_browser.py index 8379518a4cf..5887a577ba0 100644 --- a/pyomo/contrib/viewer/model_browser.py +++ b/pyomo/contrib/viewer/model_browser.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/model_select.py b/pyomo/contrib/viewer/model_select.py index 3c6c4ccdf17..e9c82740708 100644 --- a/pyomo/contrib/viewer/model_select.py +++ b/pyomo/contrib/viewer/model_select.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/pyomo_viewer.py b/pyomo/contrib/viewer/pyomo_viewer.py index a8fec745af4..6a24e12aa61 100644 --- a/pyomo/contrib/viewer/pyomo_viewer.py +++ b/pyomo/contrib/viewer/pyomo_viewer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/qt.py b/pyomo/contrib/viewer/qt.py index 150fa3560f6..2715d275758 100644 --- a/pyomo/contrib/viewer/qt.py +++ b/pyomo/contrib/viewer/qt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/report.py b/pyomo/contrib/viewer/report.py index 6f212b2fbc3..f83a53c608d 100644 --- a/pyomo/contrib/viewer/report.py +++ b/pyomo/contrib/viewer/report.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/residual_table.py b/pyomo/contrib/viewer/residual_table.py index 73cf73847e5..94e8902848f 100644 --- a/pyomo/contrib/viewer/residual_table.py +++ b/pyomo/contrib/viewer/residual_table.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/tests/__init__.py b/pyomo/contrib/viewer/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/viewer/tests/__init__.py +++ b/pyomo/contrib/viewer/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/tests/test_data_model_item.py b/pyomo/contrib/viewer/tests/test_data_model_item.py index f3e7aaf9513..781ca25508a 100644 --- a/pyomo/contrib/viewer/tests/test_data_model_item.py +++ b/pyomo/contrib/viewer/tests/test_data_model_item.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/tests/test_data_model_tree.py b/pyomo/contrib/viewer/tests/test_data_model_tree.py index db745aee9ca..d517c91b353 100644 --- a/pyomo/contrib/viewer/tests/test_data_model_tree.py +++ b/pyomo/contrib/viewer/tests/test_data_model_tree.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/tests/test_qt.py b/pyomo/contrib/viewer/tests/test_qt.py index 38a022b6668..e71921500f9 100644 --- a/pyomo/contrib/viewer/tests/test_qt.py +++ b/pyomo/contrib/viewer/tests/test_qt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/tests/test_report.py b/pyomo/contrib/viewer/tests/test_report.py index b496e2294ff..88044490a77 100644 --- a/pyomo/contrib/viewer/tests/test_report.py +++ b/pyomo/contrib/viewer/tests/test_report.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/ui.py b/pyomo/contrib/viewer/ui.py index 8a621534b31..374af8a26f0 100644 --- a/pyomo/contrib/viewer/ui.py +++ b/pyomo/contrib/viewer/ui.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/viewer/ui_data.py b/pyomo/contrib/viewer/ui_data.py index 8bbaac14e13..c716cfeedf6 100644 --- a/pyomo/contrib/viewer/ui_data.py +++ b/pyomo/contrib/viewer/ui_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/__init__.py b/pyomo/core/__init__.py index b119c6357d0..bce79faacc5 100644 --- a/pyomo/core/__init__.py +++ b/pyomo/core/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/PyomoModel.py b/pyomo/core/base/PyomoModel.py index 055f6f8450a..759b17c9a79 100644 --- a/pyomo/core/base/PyomoModel.py +++ b/pyomo/core/base/PyomoModel.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index f7815f1676b..4bbd0c9dc44 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/action.py b/pyomo/core/base/action.py index b54beab8584..f929c4b38ff 100644 --- a/pyomo/core/base/action.py +++ b/pyomo/core/base/action.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 89e872ebbe5..6d4da04de06 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/blockutil.py b/pyomo/core/base/blockutil.py index 21e6ac4db90..fc763da8b98 100644 --- a/pyomo/core/base/blockutil.py +++ b/pyomo/core/base/blockutil.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index 1945045abdd..238540ed125 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/check.py b/pyomo/core/base/check.py index 0e9d8e889b2..cbf2a99e7a3 100644 --- a/pyomo/core/base/check.py +++ b/pyomo/core/base/check.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index bb855bd6f8d..d9ef6911b6f 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/component_namer.py b/pyomo/core/base/component_namer.py index 17d46c12fae..c2fa01f6ad5 100644 --- a/pyomo/core/base/component_namer.py +++ b/pyomo/core/base/component_namer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/component_order.py b/pyomo/core/base/component_order.py index 0685571ccb0..8e69baa0972 100644 --- a/pyomo/core/base/component_order.py +++ b/pyomo/core/base/component_order.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/componentuid.py b/pyomo/core/base/componentuid.py index 89f7e5f8320..2075aa197dc 100644 --- a/pyomo/core/base/componentuid.py +++ b/pyomo/core/base/componentuid.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/config.py b/pyomo/core/base/config.py index 4c6cc06f90c..14c00522673 100644 --- a/pyomo/core/base/config.py +++ b/pyomo/core/base/config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/connector.py b/pyomo/core/base/connector.py index f3d4833b837..8dfee45236e 100644 --- a/pyomo/core/base/connector.py +++ b/pyomo/core/base/connector.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index e391b4a5605..f675a80333a 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/disable_methods.py b/pyomo/core/base/disable_methods.py index 61d63d0a385..ff8eb98487a 100644 --- a/pyomo/core/base/disable_methods.py +++ b/pyomo/core/base/disable_methods.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/enums.py b/pyomo/core/base/enums.py index ddcc66fdc4e..31f2212a661 100644 --- a/pyomo/core/base/enums.py +++ b/pyomo/core/base/enums.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index 780bc17c8a3..c8e4136be88 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/external.py b/pyomo/core/base/external.py index 93fb69e8cf7..c1f7ef32a80 100644 --- a/pyomo/core/base/external.py +++ b/pyomo/core/base/external.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/global_set.py b/pyomo/core/base/global_set.py index f4d97403308..6defb426a74 100644 --- a/pyomo/core/base/global_set.py +++ b/pyomo/core/base/global_set.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index 86b210331bb..ac9773c630b 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/indexed_component_slice.py b/pyomo/core/base/indexed_component_slice.py index 9779711a19b..208aee142e4 100644 --- a/pyomo/core/base/indexed_component_slice.py +++ b/pyomo/core/base/indexed_component_slice.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/initializer.py b/pyomo/core/base/initializer.py index 991feb0450d..c87a4236abe 100644 --- a/pyomo/core/base/initializer.py +++ b/pyomo/core/base/initializer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/instance2dat.py b/pyomo/core/base/instance2dat.py index b11c0c18e11..4dab6435187 100644 --- a/pyomo/core/base/instance2dat.py +++ b/pyomo/core/base/instance2dat.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/label.py b/pyomo/core/base/label.py index b642b834146..4ed61773a7e 100644 --- a/pyomo/core/base/label.py +++ b/pyomo/core/base/label.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index 6d553c66fed..6c2d1b036f9 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/matrix_constraint.py b/pyomo/core/base/matrix_constraint.py index 0c55dbc15d3..adc9742302e 100644 --- a/pyomo/core/base/matrix_constraint.py +++ b/pyomo/core/base/matrix_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/misc.py b/pyomo/core/base/misc.py index cf37ad48fea..926d4e576f4 100644 --- a/pyomo/core/base/misc.py +++ b/pyomo/core/base/misc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/numvalue.py b/pyomo/core/base/numvalue.py index 11d45228bf5..75bceef7ebb 100644 --- a/pyomo/core/base/numvalue.py +++ b/pyomo/core/base/numvalue.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index 7fb495f3e5b..3021a35525d 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index ea4290d880d..734df5fb24e 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index ef2fb9eefae..b6ae66ac093 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/plugin.py b/pyomo/core/base/plugin.py index 4ecb12d86a6..8c44af2dd61 100644 --- a/pyomo/core/base/plugin.py +++ b/pyomo/core/base/plugin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/range.py b/pyomo/core/base/range.py index 9df4828f550..acb004e3b10 100644 --- a/pyomo/core/base/range.py +++ b/pyomo/core/base/range.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/rangeset.py b/pyomo/core/base/rangeset.py index 18dedb84c34..27693548c1d 100644 --- a/pyomo/core/base/rangeset.py +++ b/pyomo/core/base/rangeset.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/reference.py b/pyomo/core/base/reference.py index 79ae83b97be..e2166e41e80 100644 --- a/pyomo/core/base/reference.py +++ b/pyomo/core/base/reference.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index d820ae8d933..89099fb54e8 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/set_types.py b/pyomo/core/base/set_types.py index db9fe0f796c..80c8a41ff2e 100644 --- a/pyomo/core/base/set_types.py +++ b/pyomo/core/base/set_types.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/sets.py b/pyomo/core/base/sets.py index cbaad33c0b8..f2ae44be459 100644 --- a/pyomo/core/base/sets.py +++ b/pyomo/core/base/sets.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/sos.py b/pyomo/core/base/sos.py index 98cc9d28c8f..32265df6686 100644 --- a/pyomo/core/base/sos.py +++ b/pyomo/core/base/sos.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/suffix.py b/pyomo/core/base/suffix.py index 160ae20f116..67ab0b74215 100644 --- a/pyomo/core/base/suffix.py +++ b/pyomo/core/base/suffix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/symbol_map.py b/pyomo/core/base/symbol_map.py index e4e7f9d781c..189cce7646a 100644 --- a/pyomo/core/base/symbol_map.py +++ b/pyomo/core/base/symbol_map.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/symbolic.py b/pyomo/core/base/symbolic.py index 3fa5c168207..c1ee08dd584 100644 --- a/pyomo/core/base/symbolic.py +++ b/pyomo/core/base/symbolic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/template_expr.py b/pyomo/core/base/template_expr.py index f8ff345a1e5..c3697be7eb0 100644 --- a/pyomo/core/base/template_expr.py +++ b/pyomo/core/base/template_expr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/transformation.py b/pyomo/core/base/transformation.py index 70d89af3798..31f5a251553 100644 --- a/pyomo/core/base/transformation.py +++ b/pyomo/core/base/transformation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/units_container.py b/pyomo/core/base/units_container.py index dd6bb75aec9..1bf25ffdead 100644 --- a/pyomo/core/base/units_container.py +++ b/pyomo/core/base/units_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/util.py b/pyomo/core/base/util.py index 867a303395b..6a3885cedfb 100644 --- a/pyomo/core/base/util.py +++ b/pyomo/core/base/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index f54cea98a9e..d84eabcc76b 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/beta/__init__.py b/pyomo/core/beta/__init__.py index d07668534c8..a2d51d0b23e 100644 --- a/pyomo/core/beta/__init__.py +++ b/pyomo/core/beta/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/beta/dict_objects.py b/pyomo/core/beta/dict_objects.py index c987d0946a3..a698fcbb717 100644 --- a/pyomo/core/beta/dict_objects.py +++ b/pyomo/core/beta/dict_objects.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/beta/list_objects.py b/pyomo/core/beta/list_objects.py index 2c42dfa57c8..f2ccf0d37aa 100644 --- a/pyomo/core/beta/list_objects.py +++ b/pyomo/core/beta/list_objects.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/__init__.py b/pyomo/core/expr/__init__.py index a03578de957..5efb5026c65 100644 --- a/pyomo/core/expr/__init__.py +++ b/pyomo/core/expr/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/base.py b/pyomo/core/expr/base.py index b74bbff4e3c..f506956e478 100644 --- a/pyomo/core/expr/base.py +++ b/pyomo/core/expr/base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/boolean_value.py b/pyomo/core/expr/boolean_value.py index b9c8ece29c8..002ec91be9d 100644 --- a/pyomo/core/expr/boolean_value.py +++ b/pyomo/core/expr/boolean_value.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/calculus/__init__.py b/pyomo/core/expr/calculus/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/core/expr/calculus/__init__.py +++ b/pyomo/core/expr/calculus/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/calculus/derivatives.py b/pyomo/core/expr/calculus/derivatives.py index c9787b0e309..ecfdce02fd4 100644 --- a/pyomo/core/expr/calculus/derivatives.py +++ b/pyomo/core/expr/calculus/derivatives.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/calculus/diff_with_pyomo.py b/pyomo/core/expr/calculus/diff_with_pyomo.py index 0e3ba3cc2b2..fe3eddf1490 100644 --- a/pyomo/core/expr/calculus/diff_with_pyomo.py +++ b/pyomo/core/expr/calculus/diff_with_pyomo.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/calculus/diff_with_sympy.py b/pyomo/core/expr/calculus/diff_with_sympy.py index 32cf60547ec..ab62fa3c307 100644 --- a/pyomo/core/expr/calculus/diff_with_sympy.py +++ b/pyomo/core/expr/calculus/diff_with_sympy.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/cnf_walker.py b/pyomo/core/expr/cnf_walker.py index a7bf61bef5a..7b2081e5d36 100644 --- a/pyomo/core/expr/cnf_walker.py +++ b/pyomo/core/expr/cnf_walker.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py index ec8d56896b8..790bc30aaee 100644 --- a/pyomo/core/expr/compare.py +++ b/pyomo/core/expr/compare.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/current.py b/pyomo/core/expr/current.py index 0a2ff01c82a..1209dac0310 100644 --- a/pyomo/core/expr/current.py +++ b/pyomo/core/expr/current.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/expr_common.py b/pyomo/core/expr/expr_common.py index daf86c7afc8..88065e37a2c 100644 --- a/pyomo/core/expr/expr_common.py +++ b/pyomo/core/expr/expr_common.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/expr_errors.py b/pyomo/core/expr/expr_errors.py index e33a6cbbbd7..b0ad816d725 100644 --- a/pyomo/core/expr/expr_errors.py +++ b/pyomo/core/expr/expr_errors.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/logical_expr.py b/pyomo/core/expr/logical_expr.py index 48daa79a5b3..9519b02a43b 100644 --- a/pyomo/core/expr/logical_expr.py +++ b/pyomo/core/expr/logical_expr.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/ndarray.py b/pyomo/core/expr/ndarray.py index fcbe5477a08..41514c91153 100644 --- a/pyomo/core/expr/ndarray.py +++ b/pyomo/core/expr/ndarray.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index 0a300474790..c1199ffdcad 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index 6c605b080a3..8cc20648eb4 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/relational_expr.py b/pyomo/core/expr/relational_expr.py index 6e4831d5c0c..c80fdd4930a 100644 --- a/pyomo/core/expr/relational_expr.py +++ b/pyomo/core/expr/relational_expr.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/symbol_map.py b/pyomo/core/expr/symbol_map.py index ab497c217a8..ebcf9b2953e 100644 --- a/pyomo/core/expr/symbol_map.py +++ b/pyomo/core/expr/symbol_map.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/sympy_tools.py b/pyomo/core/expr/sympy_tools.py index 7b494a610cd..48bd542be0f 100644 --- a/pyomo/core/expr/sympy_tools.py +++ b/pyomo/core/expr/sympy_tools.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/taylor_series.py b/pyomo/core/expr/taylor_series.py index 467b1faa679..2658dd36ff5 100644 --- a/pyomo/core/expr/taylor_series.py +++ b/pyomo/core/expr/taylor_series.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index fd6294f2289..f65a1f2b9b0 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index f1cd3b7bde6..6a9b7955281 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/__init__.py b/pyomo/core/kernel/__init__.py index 28a329109fc..ffe0beee080 100644 --- a/pyomo/core/kernel/__init__.py +++ b/pyomo/core/kernel/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/base.py b/pyomo/core/kernel/base.py index 2c0af56bc10..d599c76f6a1 100644 --- a/pyomo/core/kernel/base.py +++ b/pyomo/core/kernel/base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/block.py b/pyomo/core/kernel/block.py index fd779578fc4..8ba332e5545 100644 --- a/pyomo/core/kernel/block.py +++ b/pyomo/core/kernel/block.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/component_map.py b/pyomo/core/kernel/component_map.py index 501854ad972..5b5b6e9a6f2 100644 --- a/pyomo/core/kernel/component_map.py +++ b/pyomo/core/kernel/component_map.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/component_set.py b/pyomo/core/kernel/component_set.py index b0eb3507347..969b8b86372 100644 --- a/pyomo/core/kernel/component_set.py +++ b/pyomo/core/kernel/component_set.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/conic.py b/pyomo/core/kernel/conic.py index 730c072d1b7..1bb5f1b6ce8 100644 --- a/pyomo/core/kernel/conic.py +++ b/pyomo/core/kernel/conic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/constraint.py b/pyomo/core/kernel/constraint.py index 7c7969cb025..6aa4abc4bfe 100644 --- a/pyomo/core/kernel/constraint.py +++ b/pyomo/core/kernel/constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/container_utils.py b/pyomo/core/kernel/container_utils.py index 7f3329aadb3..e197d0162b5 100644 --- a/pyomo/core/kernel/container_utils.py +++ b/pyomo/core/kernel/container_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/dict_container.py b/pyomo/core/kernel/dict_container.py index b86d9c5b8f2..ae23044f8ed 100644 --- a/pyomo/core/kernel/dict_container.py +++ b/pyomo/core/kernel/dict_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/expression.py b/pyomo/core/kernel/expression.py index b375a6a89fc..a477ff9d0e3 100644 --- a/pyomo/core/kernel/expression.py +++ b/pyomo/core/kernel/expression.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/heterogeneous_container.py b/pyomo/core/kernel/heterogeneous_container.py index 43846673838..4783a2d3ec6 100644 --- a/pyomo/core/kernel/heterogeneous_container.py +++ b/pyomo/core/kernel/heterogeneous_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/homogeneous_container.py b/pyomo/core/kernel/homogeneous_container.py index 22a70e1edff..edec98e9736 100644 --- a/pyomo/core/kernel/homogeneous_container.py +++ b/pyomo/core/kernel/homogeneous_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/list_container.py b/pyomo/core/kernel/list_container.py index 05116797f3a..d60b0c7678d 100644 --- a/pyomo/core/kernel/list_container.py +++ b/pyomo/core/kernel/list_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/matrix_constraint.py b/pyomo/core/kernel/matrix_constraint.py index 1dc0fa7ddc3..ac0ec8e832d 100644 --- a/pyomo/core/kernel/matrix_constraint.py +++ b/pyomo/core/kernel/matrix_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/objective.py b/pyomo/core/kernel/objective.py index c25c86d3c09..9aa8e3315ef 100644 --- a/pyomo/core/kernel/objective.py +++ b/pyomo/core/kernel/objective.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/parameter.py b/pyomo/core/kernel/parameter.py index 1d22072435d..d4dd6336c69 100644 --- a/pyomo/core/kernel/parameter.py +++ b/pyomo/core/kernel/parameter.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/piecewise_library/__init__.py b/pyomo/core/kernel/piecewise_library/__init__.py index d275b52367e..c4d2a751632 100644 --- a/pyomo/core/kernel/piecewise_library/__init__.py +++ b/pyomo/core/kernel/piecewise_library/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/piecewise_library/transforms.py b/pyomo/core/kernel/piecewise_library/transforms.py index f00e57c199d..bc6cb0f51ad 100644 --- a/pyomo/core/kernel/piecewise_library/transforms.py +++ b/pyomo/core/kernel/piecewise_library/transforms.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/piecewise_library/transforms_nd.py b/pyomo/core/kernel/piecewise_library/transforms_nd.py index f1ea67e8d4b..2c4c8a1f1f2 100644 --- a/pyomo/core/kernel/piecewise_library/transforms_nd.py +++ b/pyomo/core/kernel/piecewise_library/transforms_nd.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/piecewise_library/util.py b/pyomo/core/kernel/piecewise_library/util.py index e65502b1a12..23975d87596 100644 --- a/pyomo/core/kernel/piecewise_library/util.py +++ b/pyomo/core/kernel/piecewise_library/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/register_numpy_types.py b/pyomo/core/kernel/register_numpy_types.py index 5f7812354d9..86877be2230 100644 --- a/pyomo/core/kernel/register_numpy_types.py +++ b/pyomo/core/kernel/register_numpy_types.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/set_types.py b/pyomo/core/kernel/set_types.py index efe5965946a..5915f0d64b3 100644 --- a/pyomo/core/kernel/set_types.py +++ b/pyomo/core/kernel/set_types.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/sos.py b/pyomo/core/kernel/sos.py index cb8d8ea4930..1845343f526 100644 --- a/pyomo/core/kernel/sos.py +++ b/pyomo/core/kernel/sos.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/suffix.py b/pyomo/core/kernel/suffix.py index 77079364703..56e13a371a3 100644 --- a/pyomo/core/kernel/suffix.py +++ b/pyomo/core/kernel/suffix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/tuple_container.py b/pyomo/core/kernel/tuple_container.py index f717fe0350a..83aab49e5db 100644 --- a/pyomo/core/kernel/tuple_container.py +++ b/pyomo/core/kernel/tuple_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/kernel/variable.py b/pyomo/core/kernel/variable.py index ff54bcb2fca..61324b3dc0f 100644 --- a/pyomo/core/kernel/variable.py +++ b/pyomo/core/kernel/variable.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/__init__.py b/pyomo/core/plugins/__init__.py index f763881c50c..23407cd77ef 100644 --- a/pyomo/core/plugins/__init__.py +++ b/pyomo/core/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/__init__.py b/pyomo/core/plugins/transform/__init__.py index 7d37c706542..21e762047ca 100644 --- a/pyomo/core/plugins/transform/__init__.py +++ b/pyomo/core/plugins/transform/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/add_slack_vars.py b/pyomo/core/plugins/transform/add_slack_vars.py index 6906b033aab..6b5096d315c 100644 --- a/pyomo/core/plugins/transform/add_slack_vars.py +++ b/pyomo/core/plugins/transform/add_slack_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/discrete_vars.py b/pyomo/core/plugins/transform/discrete_vars.py index cfb1c5e144f..35729e76517 100644 --- a/pyomo/core/plugins/transform/discrete_vars.py +++ b/pyomo/core/plugins/transform/discrete_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/eliminate_fixed_vars.py b/pyomo/core/plugins/transform/eliminate_fixed_vars.py index 1048b957e08..9312035b8c8 100644 --- a/pyomo/core/plugins/transform/eliminate_fixed_vars.py +++ b/pyomo/core/plugins/transform/eliminate_fixed_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/equality_transform.py b/pyomo/core/plugins/transform/equality_transform.py index e0cc463e238..a1a1b72f146 100644 --- a/pyomo/core/plugins/transform/equality_transform.py +++ b/pyomo/core/plugins/transform/equality_transform.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/expand_connectors.py b/pyomo/core/plugins/transform/expand_connectors.py index 8fe14318669..8c02f3e5698 100644 --- a/pyomo/core/plugins/transform/expand_connectors.py +++ b/pyomo/core/plugins/transform/expand_connectors.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/hierarchy.py b/pyomo/core/plugins/transform/hierarchy.py index a7667fc028a..86338d17f88 100644 --- a/pyomo/core/plugins/transform/hierarchy.py +++ b/pyomo/core/plugins/transform/hierarchy.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/logical_to_linear.py b/pyomo/core/plugins/transform/logical_to_linear.py index f2c609348e5..7aa541a5fdd 100644 --- a/pyomo/core/plugins/transform/logical_to_linear.py +++ b/pyomo/core/plugins/transform/logical_to_linear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/model.py b/pyomo/core/plugins/transform/model.py index 99c1d21c9a0..db8376afd29 100644 --- a/pyomo/core/plugins/transform/model.py +++ b/pyomo/core/plugins/transform/model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/nonnegative_transform.py b/pyomo/core/plugins/transform/nonnegative_transform.py index b32b7b1efc0..d123e68cb2e 100644 --- a/pyomo/core/plugins/transform/nonnegative_transform.py +++ b/pyomo/core/plugins/transform/nonnegative_transform.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/radix_linearization.py b/pyomo/core/plugins/transform/radix_linearization.py index b7ff3375a76..c67e556d60c 100644 --- a/pyomo/core/plugins/transform/radix_linearization.py +++ b/pyomo/core/plugins/transform/radix_linearization.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/relax_integrality.py b/pyomo/core/plugins/transform/relax_integrality.py index 06dd2faba77..40cf74ddbcc 100644 --- a/pyomo/core/plugins/transform/relax_integrality.py +++ b/pyomo/core/plugins/transform/relax_integrality.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index 0883455f9de..ad894b31fde 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/standard_form.py b/pyomo/core/plugins/transform/standard_form.py index 54df13fc49d..ffc382a2cf7 100644 --- a/pyomo/core/plugins/transform/standard_form.py +++ b/pyomo/core/plugins/transform/standard_form.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/plugins/transform/util.py b/pyomo/core/plugins/transform/util.py index bba8adfbc0f..9719b1f38d9 100644 --- a/pyomo/core/plugins/transform/util.py +++ b/pyomo/core/plugins/transform/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/pyomoobject.py b/pyomo/core/pyomoobject.py index 692db444f84..3bf6de37489 100644 --- a/pyomo/core/pyomoobject.py +++ b/pyomo/core/pyomoobject.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/staleflag.py b/pyomo/core/staleflag.py index 7d0dddef0dd..da90032a03c 100644 --- a/pyomo/core/staleflag.py +++ b/pyomo/core/staleflag.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/__init__.py b/pyomo/core/tests/__init__.py index 0dc08cc5aea..761a6e6c44c 100644 --- a/pyomo/core/tests/__init__.py +++ b/pyomo/core/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/data/__init__.py b/pyomo/core/tests/data/__init__.py index 21b3abf0760..a73865ee112 100644 --- a/pyomo/core/tests/data/__init__.py +++ b/pyomo/core/tests/data/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/data/test_odbc_ini.py b/pyomo/core/tests/data/test_odbc_ini.py index e7152181645..43584fe3ca9 100644 --- a/pyomo/core/tests/data/test_odbc_ini.py +++ b/pyomo/core/tests/data/test_odbc_ini.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/diet/__init__.py b/pyomo/core/tests/diet/__init__.py index 3e98344ba07..717247051c4 100644 --- a/pyomo/core/tests/diet/__init__.py +++ b/pyomo/core/tests/diet/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/diet/test_diet.py b/pyomo/core/tests/diet/test_diet.py index d92f0a024ba..9e11907179e 100644 --- a/pyomo/core/tests/diet/test_diet.py +++ b/pyomo/core/tests/diet/test_diet.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/__init__.py b/pyomo/core/tests/examples/__init__.py index 602516fcb56..c5ecc4ee437 100644 --- a/pyomo/core/tests/examples/__init__.py +++ b/pyomo/core/tests/examples/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/pmedian.py b/pyomo/core/tests/examples/pmedian.py index 5176f8bad18..c476f01bd17 100644 --- a/pyomo/core/tests/examples/pmedian.py +++ b/pyomo/core/tests/examples/pmedian.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/pmedian1.py b/pyomo/core/tests/examples/pmedian1.py index 5aeec502f7c..8e11383116b 100644 --- a/pyomo/core/tests/examples/pmedian1.py +++ b/pyomo/core/tests/examples/pmedian1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/pmedian2.py b/pyomo/core/tests/examples/pmedian2.py index 8a908f7d661..88a9666fe41 100644 --- a/pyomo/core/tests/examples/pmedian2.py +++ b/pyomo/core/tests/examples/pmedian2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/pmedian4.py b/pyomo/core/tests/examples/pmedian4.py index 98dd90f3e8f..101ee3e7c46 100644 --- a/pyomo/core/tests/examples/pmedian4.py +++ b/pyomo/core/tests/examples/pmedian4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/test_amplbook2.py b/pyomo/core/tests/examples/test_amplbook2.py index fdb9cc571bf..72e3d2b4599 100644 --- a/pyomo/core/tests/examples/test_amplbook2.py +++ b/pyomo/core/tests/examples/test_amplbook2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/test_kernel_examples.py b/pyomo/core/tests/examples/test_kernel_examples.py index 0434d9127a3..61d0fa2527d 100644 --- a/pyomo/core/tests/examples/test_kernel_examples.py +++ b/pyomo/core/tests/examples/test_kernel_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/test_pyomo.py b/pyomo/core/tests/examples/test_pyomo.py index 64c195c0ab4..2d3a39ebdda 100644 --- a/pyomo/core/tests/examples/test_pyomo.py +++ b/pyomo/core/tests/examples/test_pyomo.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/examples/test_tutorials.py b/pyomo/core/tests/examples/test_tutorials.py index 3a74c1ca142..c8de003007e 100644 --- a/pyomo/core/tests/examples/test_tutorials.py +++ b/pyomo/core/tests/examples/test_tutorials.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/transform/__init__.py b/pyomo/core/tests/transform/__init__.py index df59aa21988..f34c7624e25 100644 --- a/pyomo/core/tests/transform/__init__.py +++ b/pyomo/core/tests/transform/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/transform/test_add_slacks.py b/pyomo/core/tests/transform/test_add_slacks.py index a3698b7d529..7896cab7e88 100644 --- a/pyomo/core/tests/transform/test_add_slacks.py +++ b/pyomo/core/tests/transform/test_add_slacks.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/transform/test_scaling.py b/pyomo/core/tests/transform/test_scaling.py index 1cb4e886956..d0fbfab61bd 100644 --- a/pyomo/core/tests/transform/test_scaling.py +++ b/pyomo/core/tests/transform/test_scaling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/transform/test_transform.py b/pyomo/core/tests/transform/test_transform.py index 7c3f17fcfec..cd1f26417a7 100644 --- a/pyomo/core/tests/transform/test_transform.py +++ b/pyomo/core/tests/transform/test_transform.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/__init__.py b/pyomo/core/tests/unit/__init__.py index 65e82b81c0c..85ece8d8cd5 100644 --- a/pyomo/core/tests/unit/__init__.py +++ b/pyomo/core/tests/unit/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/__init__.py b/pyomo/core/tests/unit/kernel/__init__.py index ff387efbd03..e5231e0f859 100644 --- a/pyomo/core/tests/unit/kernel/__init__.py +++ b/pyomo/core/tests/unit/kernel/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_block.py b/pyomo/core/tests/unit/kernel/test_block.py index a22ed4fb4b5..b21771653bb 100644 --- a/pyomo/core/tests/unit/kernel/test_block.py +++ b/pyomo/core/tests/unit/kernel/test_block.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_component_map.py b/pyomo/core/tests/unit/kernel/test_component_map.py index 6d19743c3fe..3fb8b99a9a3 100644 --- a/pyomo/core/tests/unit/kernel/test_component_map.py +++ b/pyomo/core/tests/unit/kernel/test_component_map.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_component_set.py b/pyomo/core/tests/unit/kernel/test_component_set.py index 30f2cf72716..38f17a702c1 100644 --- a/pyomo/core/tests/unit/kernel/test_component_set.py +++ b/pyomo/core/tests/unit/kernel/test_component_set.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_conic.py b/pyomo/core/tests/unit/kernel/test_conic.py index 352976a2410..ccfbcca7e1f 100644 --- a/pyomo/core/tests/unit/kernel/test_conic.py +++ b/pyomo/core/tests/unit/kernel/test_conic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_constraint.py b/pyomo/core/tests/unit/kernel/test_constraint.py index f2f219cc66f..97832dd8bca 100644 --- a/pyomo/core/tests/unit/kernel/test_constraint.py +++ b/pyomo/core/tests/unit/kernel/test_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_dict_container.py b/pyomo/core/tests/unit/kernel/test_dict_container.py index e6b6f8d7aab..6ae25362bb2 100644 --- a/pyomo/core/tests/unit/kernel/test_dict_container.py +++ b/pyomo/core/tests/unit/kernel/test_dict_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_expression.py b/pyomo/core/tests/unit/kernel/test_expression.py index 85f8c331a46..39d3eaa463c 100644 --- a/pyomo/core/tests/unit/kernel/test_expression.py +++ b/pyomo/core/tests/unit/kernel/test_expression.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_kernel.py b/pyomo/core/tests/unit/kernel/test_kernel.py index fbff295881a..b34bcdeaadb 100644 --- a/pyomo/core/tests/unit/kernel/test_kernel.py +++ b/pyomo/core/tests/unit/kernel/test_kernel.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_list_container.py b/pyomo/core/tests/unit/kernel/test_list_container.py index 9e3ada739b2..a4641f83295 100644 --- a/pyomo/core/tests/unit/kernel/test_list_container.py +++ b/pyomo/core/tests/unit/kernel/test_list_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_matrix_constraint.py b/pyomo/core/tests/unit/kernel/test_matrix_constraint.py index c986e5eda96..24a2915f224 100644 --- a/pyomo/core/tests/unit/kernel/test_matrix_constraint.py +++ b/pyomo/core/tests/unit/kernel/test_matrix_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_objective.py b/pyomo/core/tests/unit/kernel/test_objective.py index f60ff9bdb49..810218f1dc2 100644 --- a/pyomo/core/tests/unit/kernel/test_objective.py +++ b/pyomo/core/tests/unit/kernel/test_objective.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_parameter.py b/pyomo/core/tests/unit/kernel/test_parameter.py index 04dc08f095f..469ed9fbe8c 100644 --- a/pyomo/core/tests/unit/kernel/test_parameter.py +++ b/pyomo/core/tests/unit/kernel/test_parameter.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_piecewise.py b/pyomo/core/tests/unit/kernel/test_piecewise.py index 2c236c0dd12..3d9cf66e39c 100644 --- a/pyomo/core/tests/unit/kernel/test_piecewise.py +++ b/pyomo/core/tests/unit/kernel/test_piecewise.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_sos.py b/pyomo/core/tests/unit/kernel/test_sos.py index 9410425d405..b1cb67a96f8 100644 --- a/pyomo/core/tests/unit/kernel/test_sos.py +++ b/pyomo/core/tests/unit/kernel/test_sos.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_suffix.py b/pyomo/core/tests/unit/kernel/test_suffix.py index c4c75278d50..2a73888c2d3 100644 --- a/pyomo/core/tests/unit/kernel/test_suffix.py +++ b/pyomo/core/tests/unit/kernel/test_suffix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_tuple_container.py b/pyomo/core/tests/unit/kernel/test_tuple_container.py index 0b45c36b299..c016c5fc789 100644 --- a/pyomo/core/tests/unit/kernel/test_tuple_container.py +++ b/pyomo/core/tests/unit/kernel/test_tuple_container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/kernel/test_variable.py b/pyomo/core/tests/unit/kernel/test_variable.py index e360240f3b2..181eb15c972 100644 --- a/pyomo/core/tests/unit/kernel/test_variable.py +++ b/pyomo/core/tests/unit/kernel/test_variable.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_action.py b/pyomo/core/tests/unit/test_action.py index 5db6f165854..3481c90a021 100644 --- a/pyomo/core/tests/unit/test_action.py +++ b/pyomo/core/tests/unit/test_action.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index f68850d9421..f60a758eb62 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_block_model.py b/pyomo/core/tests/unit/test_block_model.py index ed751e96fc5..b4cf34e7516 100644 --- a/pyomo/core/tests/unit/test_block_model.py +++ b/pyomo/core/tests/unit/test_block_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_bounds.py b/pyomo/core/tests/unit/test_bounds.py index c2c6a69bdd2..23554f555c9 100644 --- a/pyomo/core/tests/unit/test_bounds.py +++ b/pyomo/core/tests/unit/test_bounds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_check.py b/pyomo/core/tests/unit/test_check.py index 5b2d5408fd5..e61e3998fb7 100644 --- a/pyomo/core/tests/unit/test_check.py +++ b/pyomo/core/tests/unit/test_check.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_compare.py b/pyomo/core/tests/unit/test_compare.py index 8b8538a8656..f80753bdb61 100644 --- a/pyomo/core/tests/unit/test_compare.py +++ b/pyomo/core/tests/unit/test_compare.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_component.py b/pyomo/core/tests/unit/test_component.py index b4408fe8c54..175c4c47d46 100644 --- a/pyomo/core/tests/unit/test_component.py +++ b/pyomo/core/tests/unit/test_component.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_componentuid.py b/pyomo/core/tests/unit/test_componentuid.py index 1c9b3c444bf..2893e737136 100644 --- a/pyomo/core/tests/unit/test_componentuid.py +++ b/pyomo/core/tests/unit/test_componentuid.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_con.py b/pyomo/core/tests/unit/test_con.py index bd90972fee2..6ed19c1bcfd 100644 --- a/pyomo/core/tests/unit/test_con.py +++ b/pyomo/core/tests/unit/test_con.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_concrete.py b/pyomo/core/tests/unit/test_concrete.py index a9bd75f05c7..9083c5cf7f9 100644 --- a/pyomo/core/tests/unit/test_concrete.py +++ b/pyomo/core/tests/unit/test_concrete.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_connector.py b/pyomo/core/tests/unit/test_connector.py index 1dde9f3af24..78799d13d0b 100644 --- a/pyomo/core/tests/unit/test_connector.py +++ b/pyomo/core/tests/unit/test_connector.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_deprecation.py b/pyomo/core/tests/unit/test_deprecation.py index 9adf2de26cd..7d718a4bd2a 100644 --- a/pyomo/core/tests/unit/test_deprecation.py +++ b/pyomo/core/tests/unit/test_deprecation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_derivs.py b/pyomo/core/tests/unit/test_derivs.py index 7db284cb29a..6a4fc6814b3 100644 --- a/pyomo/core/tests/unit/test_derivs.py +++ b/pyomo/core/tests/unit/test_derivs.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index 7d3244f4d86..8260f1ae320 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_disable_methods.py b/pyomo/core/tests/unit/test_disable_methods.py index 4d6595e5fe8..618752aee85 100644 --- a/pyomo/core/tests/unit/test_disable_methods.py +++ b/pyomo/core/tests/unit/test_disable_methods.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_enums.py b/pyomo/core/tests/unit/test_enums.py index 8f342e55188..cce908a87de 100644 --- a/pyomo/core/tests/unit/test_enums.py +++ b/pyomo/core/tests/unit/test_enums.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_expr_misc.py b/pyomo/core/tests/unit/test_expr_misc.py index 4ec53521d6b..f4fd7556117 100644 --- a/pyomo/core/tests/unit/test_expr_misc.py +++ b/pyomo/core/tests/unit/test_expr_misc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_expression.py b/pyomo/core/tests/unit/test_expression.py index 8dca0062dd0..acf5abb2626 100644 --- a/pyomo/core/tests/unit/test_expression.py +++ b/pyomo/core/tests/unit/test_expression.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_external.py b/pyomo/core/tests/unit/test_external.py index 96c05b6b0b8..1d4a59647c1 100644 --- a/pyomo/core/tests/unit/test_external.py +++ b/pyomo/core/tests/unit/test_external.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_indexed.py b/pyomo/core/tests/unit/test_indexed.py index 29bf22ceeb1..3480b653ea5 100644 --- a/pyomo/core/tests/unit/test_indexed.py +++ b/pyomo/core/tests/unit/test_indexed.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_indexed_slice.py b/pyomo/core/tests/unit/test_indexed_slice.py index e89c48a6061..babd3f3c46a 100644 --- a/pyomo/core/tests/unit/test_indexed_slice.py +++ b/pyomo/core/tests/unit/test_indexed_slice.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_initializer.py b/pyomo/core/tests/unit/test_initializer.py index b334a6b857b..c0f9ddc9565 100644 --- a/pyomo/core/tests/unit/test_initializer.py +++ b/pyomo/core/tests/unit/test_initializer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_kernel_register_numpy_types.py b/pyomo/core/tests/unit/test_kernel_register_numpy_types.py index 117de5c5f4c..91a0f571881 100644 --- a/pyomo/core/tests/unit/test_kernel_register_numpy_types.py +++ b/pyomo/core/tests/unit/test_kernel_register_numpy_types.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_labelers.py b/pyomo/core/tests/unit/test_labelers.py index 15c56b5390d..579abfd8b52 100644 --- a/pyomo/core/tests/unit/test_labelers.py +++ b/pyomo/core/tests/unit/test_labelers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index 442fa97b6d1..3eb2e279964 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_logical_constraint.py b/pyomo/core/tests/unit/test_logical_constraint.py index e38a67a39d0..b1f37996018 100644 --- a/pyomo/core/tests/unit/test_logical_constraint.py +++ b/pyomo/core/tests/unit/test_logical_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_logical_expr_expanded.py b/pyomo/core/tests/unit/test_logical_expr_expanded.py index 0360e9b4783..6468a21e336 100644 --- a/pyomo/core/tests/unit/test_logical_expr_expanded.py +++ b/pyomo/core/tests/unit/test_logical_expr_expanded.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_logical_to_linear.py b/pyomo/core/tests/unit/test_logical_to_linear.py index 22133f22ba2..e777259f8ce 100644 --- a/pyomo/core/tests/unit/test_logical_to_linear.py +++ b/pyomo/core/tests/unit/test_logical_to_linear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_matrix_constraint.py b/pyomo/core/tests/unit/test_matrix_constraint.py index d9b51de7bf6..993e2a18eb3 100644 --- a/pyomo/core/tests/unit/test_matrix_constraint.py +++ b/pyomo/core/tests/unit/test_matrix_constraint.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_misc.py b/pyomo/core/tests/unit/test_misc.py index 261c94d96bd..440c8807358 100644 --- a/pyomo/core/tests/unit/test_misc.py +++ b/pyomo/core/tests/unit/test_misc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_model.py b/pyomo/core/tests/unit/test_model.py index 95ad17e97f4..9016f9937c0 100644 --- a/pyomo/core/tests/unit/test_model.py +++ b/pyomo/core/tests/unit/test_model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_mutable.py b/pyomo/core/tests/unit/test_mutable.py index 933ef1fe3dc..d10622d84c0 100644 --- a/pyomo/core/tests/unit/test_mutable.py +++ b/pyomo/core/tests/unit/test_mutable.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index a4f3295441e..c073ee0f726 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_numeric_expr_api.py b/pyomo/core/tests/unit/test_numeric_expr_api.py index 69cb43f3ad5..4e0af126315 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_api.py +++ b/pyomo/core/tests/unit/test_numeric_expr_api.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py index 3e9e160b1b1..3787f00de47 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py +++ b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py index 3000f644e80..162d664e0f8 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py +++ b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_numpy_expr.py b/pyomo/core/tests/unit/test_numpy_expr.py index 8f58eb29e56..fb81dfe809f 100644 --- a/pyomo/core/tests/unit/test_numpy_expr.py +++ b/pyomo/core/tests/unit/test_numpy_expr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_numvalue.py b/pyomo/core/tests/unit/test_numvalue.py index 74df1d29522..eceab3a42d9 100644 --- a/pyomo/core/tests/unit/test_numvalue.py +++ b/pyomo/core/tests/unit/test_numvalue.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_obj.py b/pyomo/core/tests/unit/test_obj.py index d73bf7d6dfd..3c8a05f7058 100644 --- a/pyomo/core/tests/unit/test_obj.py +++ b/pyomo/core/tests/unit/test_obj.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_param.py b/pyomo/core/tests/unit/test_param.py index 6ba1163e3c3..9bc0c4b2ad2 100644 --- a/pyomo/core/tests/unit/test_param.py +++ b/pyomo/core/tests/unit/test_param.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_pickle.py b/pyomo/core/tests/unit/test_pickle.py index 861704a2f9c..fccc92bbfa2 100644 --- a/pyomo/core/tests/unit/test_pickle.py +++ b/pyomo/core/tests/unit/test_pickle.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_piecewise.py b/pyomo/core/tests/unit/test_piecewise.py index aeb02b82624..af82ef7c06d 100644 --- a/pyomo/core/tests/unit/test_piecewise.py +++ b/pyomo/core/tests/unit/test_piecewise.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_preprocess.py b/pyomo/core/tests/unit/test_preprocess.py index d4c5ae75bb0..ce7924f3ac5 100644 --- a/pyomo/core/tests/unit/test_preprocess.py +++ b/pyomo/core/tests/unit/test_preprocess.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_range.py b/pyomo/core/tests/unit/test_range.py index 8cd1e7ce46c..4b489f50d44 100644 --- a/pyomo/core/tests/unit/test_range.py +++ b/pyomo/core/tests/unit/test_range.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_reference.py b/pyomo/core/tests/unit/test_reference.py index a7a470b1a3b..9865e04985f 100644 --- a/pyomo/core/tests/unit/test_reference.py +++ b/pyomo/core/tests/unit/test_reference.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_relational_expr.py b/pyomo/core/tests/unit/test_relational_expr.py index f55bfff108c..d361bfcc83c 100644 --- a/pyomo/core/tests/unit/test_relational_expr.py +++ b/pyomo/core/tests/unit/test_relational_expr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 72231bb08d7..35779691a31 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index 90668a28e72..48869397aae 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_smap.py b/pyomo/core/tests/unit/test_smap.py index 2b9d2f192c0..69448916a04 100644 --- a/pyomo/core/tests/unit/test_smap.py +++ b/pyomo/core/tests/unit/test_smap.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_sos.py b/pyomo/core/tests/unit/test_sos.py index 92a8a5eabaa..cacfcdf5d42 100644 --- a/pyomo/core/tests/unit/test_sos.py +++ b/pyomo/core/tests/unit/test_sos.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_sos_v2.py b/pyomo/core/tests/unit/test_sos_v2.py index 4f4599056b5..996dd10829d 100644 --- a/pyomo/core/tests/unit/test_sos_v2.py +++ b/pyomo/core/tests/unit/test_sos_v2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_suffix.py b/pyomo/core/tests/unit/test_suffix.py index 1ec1af9d919..d2e861cceb5 100644 --- a/pyomo/core/tests/unit/test_suffix.py +++ b/pyomo/core/tests/unit/test_suffix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_symbol_map.py b/pyomo/core/tests/unit/test_symbol_map.py index 5f6416e2c8d..773e6d335f1 100644 --- a/pyomo/core/tests/unit/test_symbol_map.py +++ b/pyomo/core/tests/unit/test_symbol_map.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_symbolic.py b/pyomo/core/tests/unit/test_symbolic.py index bbac4599363..91887f27bb7 100644 --- a/pyomo/core/tests/unit/test_symbolic.py +++ b/pyomo/core/tests/unit/test_symbolic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_taylor_series.py b/pyomo/core/tests/unit/test_taylor_series.py index d4fe5291b2d..4b36451d222 100644 --- a/pyomo/core/tests/unit/test_taylor_series.py +++ b/pyomo/core/tests/unit/test_taylor_series.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_template_expr.py b/pyomo/core/tests/unit/test_template_expr.py index 4b4ea494b0e..4f255e3567a 100644 --- a/pyomo/core/tests/unit/test_template_expr.py +++ b/pyomo/core/tests/unit/test_template_expr.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_units.py b/pyomo/core/tests/unit/test_units.py index 809db733cde..bda62835711 100644 --- a/pyomo/core/tests/unit/test_units.py +++ b/pyomo/core/tests/unit/test_units.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_var.py b/pyomo/core/tests/unit/test_var.py index 33e46a79e9b..6b2e92be832 100644 --- a/pyomo/core/tests/unit/test_var.py +++ b/pyomo/core/tests/unit/test_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_var_set_bounds.py b/pyomo/core/tests/unit/test_var_set_bounds.py index eb969c2ca73..bae89556ce3 100644 --- a/pyomo/core/tests/unit/test_var_set_bounds.py +++ b/pyomo/core/tests/unit/test_var_set_bounds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_visitor.py b/pyomo/core/tests/unit/test_visitor.py index 086c57aa560..c968287ff1b 100644 --- a/pyomo/core/tests/unit/test_visitor.py +++ b/pyomo/core/tests/unit/test_visitor.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/test_xfrm_discrete_vars.py b/pyomo/core/tests/unit/test_xfrm_discrete_vars.py index ae630586480..d0e74c8cae3 100644 --- a/pyomo/core/tests/unit/test_xfrm_discrete_vars.py +++ b/pyomo/core/tests/unit/test_xfrm_discrete_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/uninstantiated_model_linear.py b/pyomo/core/tests/unit/uninstantiated_model_linear.py index 387444b7bc5..417f7763d87 100644 --- a/pyomo/core/tests/unit/uninstantiated_model_linear.py +++ b/pyomo/core/tests/unit/uninstantiated_model_linear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/tests/unit/uninstantiated_model_quadratic.py b/pyomo/core/tests/unit/uninstantiated_model_quadratic.py index 572c6a43a14..350d96a85bb 100644 --- a/pyomo/core/tests/unit/uninstantiated_model_quadratic.py +++ b/pyomo/core/tests/unit/uninstantiated_model_quadratic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/core/util.py b/pyomo/core/util.py index 3f8a136e07d..4e076c7505b 100644 --- a/pyomo/core/util.py +++ b/pyomo/core/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/__init__.py b/pyomo/dae/__init__.py index 8d07b184336..5860a129aa2 100644 --- a/pyomo/dae/__init__.py +++ b/pyomo/dae/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/contset.py b/pyomo/dae/contset.py index ee4c9f79e89..94d20723770 100644 --- a/pyomo/dae/contset.py +++ b/pyomo/dae/contset.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/diffvar.py b/pyomo/dae/diffvar.py index 8d75b9ae148..6bb3a8b06f0 100644 --- a/pyomo/dae/diffvar.py +++ b/pyomo/dae/diffvar.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/flatten.py b/pyomo/dae/flatten.py index 595f90b3dc7..927b92c8f7d 100644 --- a/pyomo/dae/flatten.py +++ b/pyomo/dae/flatten.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/initialization.py b/pyomo/dae/initialization.py index c10ccb023d1..97928026de2 100644 --- a/pyomo/dae/initialization.py +++ b/pyomo/dae/initialization.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/integral.py b/pyomo/dae/integral.py index 302e50a007d..34a34fdcd9c 100644 --- a/pyomo/dae/integral.py +++ b/pyomo/dae/integral.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/misc.py b/pyomo/dae/misc.py index 9b867bcfff4..3e09a055577 100644 --- a/pyomo/dae/misc.py +++ b/pyomo/dae/misc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/plugins/__init__.py b/pyomo/dae/plugins/__init__.py index 96ab91b0ac0..681112dd970 100644 --- a/pyomo/dae/plugins/__init__.py +++ b/pyomo/dae/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/plugins/colloc.py b/pyomo/dae/plugins/colloc.py index 7f86e8bc2e2..81f1e4dd7ea 100644 --- a/pyomo/dae/plugins/colloc.py +++ b/pyomo/dae/plugins/colloc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/plugins/finitedifference.py b/pyomo/dae/plugins/finitedifference.py index 71bb2ffc9b6..6557a14e562 100644 --- a/pyomo/dae/plugins/finitedifference.py +++ b/pyomo/dae/plugins/finitedifference.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/set_utils.py b/pyomo/dae/set_utils.py index 981954189b3..d7a1d9517d9 100644 --- a/pyomo/dae/set_utils.py +++ b/pyomo/dae/set_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/simulator.py b/pyomo/dae/simulator.py index 149c42ca6b4..f9121dbc0cc 100644 --- a/pyomo/dae/simulator.py +++ b/pyomo/dae/simulator.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/__init__.py b/pyomo/dae/tests/__init__.py index 12bdccd0ef4..4638923595a 100644 --- a/pyomo/dae/tests/__init__.py +++ b/pyomo/dae/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_colloc.py b/pyomo/dae/tests/test_colloc.py index 0786903f12e..e7e6b20d660 100644 --- a/pyomo/dae/tests/test_colloc.py +++ b/pyomo/dae/tests/test_colloc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_contset.py b/pyomo/dae/tests/test_contset.py index ce13d53dfd5..e5f11b90e27 100644 --- a/pyomo/dae/tests/test_contset.py +++ b/pyomo/dae/tests/test_contset.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_diffvar.py b/pyomo/dae/tests/test_diffvar.py index 718781d5916..279a7e02680 100644 --- a/pyomo/dae/tests/test_diffvar.py +++ b/pyomo/dae/tests/test_diffvar.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_finite_diff.py b/pyomo/dae/tests/test_finite_diff.py index adca8bf6a15..a1b842feccf 100644 --- a/pyomo/dae/tests/test_finite_diff.py +++ b/pyomo/dae/tests/test_finite_diff.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_flatten.py b/pyomo/dae/tests/test_flatten.py index a6ea824c3ef..7037cd79c96 100644 --- a/pyomo/dae/tests/test_flatten.py +++ b/pyomo/dae/tests/test_flatten.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_initialization.py b/pyomo/dae/tests/test_initialization.py index 390b6ecc59e..8407ad2b2a4 100644 --- a/pyomo/dae/tests/test_initialization.py +++ b/pyomo/dae/tests/test_initialization.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_integral.py b/pyomo/dae/tests/test_integral.py index 77d6d4dd8a9..933bd97d7b4 100644 --- a/pyomo/dae/tests/test_integral.py +++ b/pyomo/dae/tests/test_integral.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_misc.py b/pyomo/dae/tests/test_misc.py index 11c4e44b7b0..48c1e48418d 100644 --- a/pyomo/dae/tests/test_misc.py +++ b/pyomo/dae/tests/test_misc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_set_utils.py b/pyomo/dae/tests/test_set_utils.py index fa592e05181..8877dadf798 100644 --- a/pyomo/dae/tests/test_set_utils.py +++ b/pyomo/dae/tests/test_set_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/tests/test_simulator.py b/pyomo/dae/tests/test_simulator.py index e79bc7b23b6..76316b5571e 100644 --- a/pyomo/dae/tests/test_simulator.py +++ b/pyomo/dae/tests/test_simulator.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dae/utilities.py b/pyomo/dae/utilities.py index e48c66e003d..ae4018a122e 100644 --- a/pyomo/dae/utilities.py +++ b/pyomo/dae/utilities.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/DataPortal.py b/pyomo/dataportal/DataPortal.py index 8eb577af013..24a9c847d48 100644 --- a/pyomo/dataportal/DataPortal.py +++ b/pyomo/dataportal/DataPortal.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/TableData.py b/pyomo/dataportal/TableData.py index 1d428967449..b7fb98d596a 100644 --- a/pyomo/dataportal/TableData.py +++ b/pyomo/dataportal/TableData.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/__init__.py b/pyomo/dataportal/__init__.py index ca82614ef2a..ece0ac039f6 100644 --- a/pyomo/dataportal/__init__.py +++ b/pyomo/dataportal/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/factory.py b/pyomo/dataportal/factory.py index f1c18dc05c9..e6424be25c4 100644 --- a/pyomo/dataportal/factory.py +++ b/pyomo/dataportal/factory.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/parse_datacmds.py b/pyomo/dataportal/parse_datacmds.py index be363fdb64b..d9f44405577 100644 --- a/pyomo/dataportal/parse_datacmds.py +++ b/pyomo/dataportal/parse_datacmds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/plugins/__init__.py b/pyomo/dataportal/plugins/__init__.py index c3387af9d1e..3a356ee9da8 100644 --- a/pyomo/dataportal/plugins/__init__.py +++ b/pyomo/dataportal/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/plugins/csv_table.py b/pyomo/dataportal/plugins/csv_table.py index 6563a89df10..a52c8227695 100644 --- a/pyomo/dataportal/plugins/csv_table.py +++ b/pyomo/dataportal/plugins/csv_table.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/plugins/datacommands.py b/pyomo/dataportal/plugins/datacommands.py index 068a551d8d2..2da0d44f048 100644 --- a/pyomo/dataportal/plugins/datacommands.py +++ b/pyomo/dataportal/plugins/datacommands.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/plugins/db_table.py b/pyomo/dataportal/plugins/db_table.py index 682b87ab13e..a39705a6058 100644 --- a/pyomo/dataportal/plugins/db_table.py +++ b/pyomo/dataportal/plugins/db_table.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/plugins/json_dict.py b/pyomo/dataportal/plugins/json_dict.py index e42c040ad0b..8b41e9a1c7b 100644 --- a/pyomo/dataportal/plugins/json_dict.py +++ b/pyomo/dataportal/plugins/json_dict.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/plugins/sheet.py b/pyomo/dataportal/plugins/sheet.py index 8672b9917da..773cce81116 100644 --- a/pyomo/dataportal/plugins/sheet.py +++ b/pyomo/dataportal/plugins/sheet.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/plugins/text.py b/pyomo/dataportal/plugins/text.py index a9b169e27bd..9a86fd4481b 100644 --- a/pyomo/dataportal/plugins/text.py +++ b/pyomo/dataportal/plugins/text.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/plugins/xml_table.py b/pyomo/dataportal/plugins/xml_table.py index 79245c6d24a..7e10b96312e 100644 --- a/pyomo/dataportal/plugins/xml_table.py +++ b/pyomo/dataportal/plugins/xml_table.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/process_data.py b/pyomo/dataportal/process_data.py index 5eb15269e0c..f6f20d69f67 100644 --- a/pyomo/dataportal/process_data.py +++ b/pyomo/dataportal/process_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/tests/__init__.py b/pyomo/dataportal/tests/__init__.py index 65e82b81c0c..85ece8d8cd5 100644 --- a/pyomo/dataportal/tests/__init__.py +++ b/pyomo/dataportal/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/tests/test_dat_parser.py b/pyomo/dataportal/tests/test_dat_parser.py index 0663279875d..43bf216525c 100644 --- a/pyomo/dataportal/tests/test_dat_parser.py +++ b/pyomo/dataportal/tests/test_dat_parser.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/dataportal/tests/test_dataportal.py b/pyomo/dataportal/tests/test_dataportal.py index 3171a118118..8496a8fa3f8 100644 --- a/pyomo/dataportal/tests/test_dataportal.py +++ b/pyomo/dataportal/tests/test_dataportal.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/duality/__init__.py b/pyomo/duality/__init__.py index 7f1c869670d..a08ca813ff8 100644 --- a/pyomo/duality/__init__.py +++ b/pyomo/duality/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/duality/collect.py b/pyomo/duality/collect.py index a8b62cb8dfe..350ca058f82 100644 --- a/pyomo/duality/collect.py +++ b/pyomo/duality/collect.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/duality/lagrangian_dual.py b/pyomo/duality/lagrangian_dual.py index 1b27a3f93d4..96bc3f4a95e 100644 --- a/pyomo/duality/lagrangian_dual.py +++ b/pyomo/duality/lagrangian_dual.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/duality/plugins.py b/pyomo/duality/plugins.py index c8c84153975..0e89857ded1 100644 --- a/pyomo/duality/plugins.py +++ b/pyomo/duality/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/duality/tests/__init__.py b/pyomo/duality/tests/__init__.py index 0dc08cc5aea..761a6e6c44c 100644 --- a/pyomo/duality/tests/__init__.py +++ b/pyomo/duality/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/duality/tests/test_linear_dual.py b/pyomo/duality/tests/test_linear_dual.py index ba3554bdc50..da8ba7a370c 100644 --- a/pyomo/duality/tests/test_linear_dual.py +++ b/pyomo/duality/tests/test_linear_dual.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/environ/__init__.py b/pyomo/environ/__init__.py index c3fb3ec4a85..ec0cc5878e4 100644 --- a/pyomo/environ/__init__.py +++ b/pyomo/environ/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/environ/tests/__init__.py b/pyomo/environ/tests/__init__.py index b1d721839c7..61e159c169b 100644 --- a/pyomo/environ/tests/__init__.py +++ b/pyomo/environ/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/environ/tests/standalone_minimal_pyomo_driver.py b/pyomo/environ/tests/standalone_minimal_pyomo_driver.py index 88f8e9f8651..80fb5d15121 100644 --- a/pyomo/environ/tests/standalone_minimal_pyomo_driver.py +++ b/pyomo/environ/tests/standalone_minimal_pyomo_driver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/environ/tests/test_environ.py b/pyomo/environ/tests/test_environ.py index b223ba0e916..9c89fd135d5 100644 --- a/pyomo/environ/tests/test_environ.py +++ b/pyomo/environ/tests/test_environ.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/environ/tests/test_package_layout.py b/pyomo/environ/tests/test_package_layout.py index 0bc8c55113a..4e1574ab158 100644 --- a/pyomo/environ/tests/test_package_layout.py +++ b/pyomo/environ/tests/test_package_layout.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/__init__.py b/pyomo/gdp/__init__.py index 6fc2d4b7351..a18bc03084a 100644 --- a/pyomo/gdp/__init__.py +++ b/pyomo/gdp/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/basic_step.py b/pyomo/gdp/basic_step.py index 69313ac2b1b..56a19e2a0f2 100644 --- a/pyomo/gdp/basic_step.py +++ b/pyomo/gdp/basic_step.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/disjunct.py b/pyomo/gdp/disjunct.py index eca6d93d732..b575ab65c99 100644 --- a/pyomo/gdp/disjunct.py +++ b/pyomo/gdp/disjunct.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/__init__.py b/pyomo/gdp/plugins/__init__.py index 2edb99bbe1b..875e47e6cc1 100644 --- a/pyomo/gdp/plugins/__init__.py +++ b/pyomo/gdp/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/between_steps.py b/pyomo/gdp/plugins/between_steps.py index fad783d595d..8f57164334e 100644 --- a/pyomo/gdp/plugins/between_steps.py +++ b/pyomo/gdp/plugins/between_steps.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index e554d5593ab..bdd353a6136 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index a4df641c8c6..ad6e6dcad86 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/bilinear.py b/pyomo/gdp/plugins/bilinear.py index feacaaddefc..67390801348 100644 --- a/pyomo/gdp/plugins/bilinear.py +++ b/pyomo/gdp/plugins/bilinear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/binary_multiplication.py b/pyomo/gdp/plugins/binary_multiplication.py index 5afb661aaa8..ef4239e09dc 100644 --- a/pyomo/gdp/plugins/binary_multiplication.py +++ b/pyomo/gdp/plugins/binary_multiplication.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/bound_pretransformation.py b/pyomo/gdp/plugins/bound_pretransformation.py index 56a39115f34..7c90c24d869 100644 --- a/pyomo/gdp/plugins/bound_pretransformation.py +++ b/pyomo/gdp/plugins/bound_pretransformation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/chull.py b/pyomo/gdp/plugins/chull.py index d226c57aae7..c11d8ea0729 100644 --- a/pyomo/gdp/plugins/chull.py +++ b/pyomo/gdp/plugins/chull.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/cuttingplane.py b/pyomo/gdp/plugins/cuttingplane.py index 7a6a927a316..6c77a582987 100644 --- a/pyomo/gdp/plugins/cuttingplane.py +++ b/pyomo/gdp/plugins/cuttingplane.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/fix_disjuncts.py b/pyomo/gdp/plugins/fix_disjuncts.py index d0f59ce87ce..44a9d91d513 100644 --- a/pyomo/gdp/plugins/fix_disjuncts.py +++ b/pyomo/gdp/plugins/fix_disjuncts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 0aa5ec163b6..96d97206c97 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/gdp_var_mover.py b/pyomo/gdp/plugins/gdp_var_mover.py index df659670bf4..5402b576368 100644 --- a/pyomo/gdp/plugins/gdp_var_mover.py +++ b/pyomo/gdp/plugins/gdp_var_mover.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index a600ef76bc7..630560b57f0 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 85fb1e4aa6b..4220caa12c1 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/partition_disjuncts.py b/pyomo/gdp/plugins/partition_disjuncts.py index fbe25ed3ae1..1a76900047c 100644 --- a/pyomo/gdp/plugins/partition_disjuncts.py +++ b/pyomo/gdp/plugins/partition_disjuncts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/plugins/transform_current_disjunctive_state.py b/pyomo/gdp/plugins/transform_current_disjunctive_state.py index 338f42c68da..3e20224ec3d 100644 --- a/pyomo/gdp/plugins/transform_current_disjunctive_state.py +++ b/pyomo/gdp/plugins/transform_current_disjunctive_state.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/__init__.py b/pyomo/gdp/tests/__init__.py index c5e495e5aa3..a2a2c61779a 100644 --- a/pyomo/gdp/tests/__init__.py +++ b/pyomo/gdp/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index 4a772a7ae56..e6d38ef1502 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/models.py b/pyomo/gdp/tests/models.py index f03a847162b..273bdec7261 100644 --- a/pyomo/gdp/tests/models.py +++ b/pyomo/gdp/tests/models.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_basic_step.py b/pyomo/gdp/tests/test_basic_step.py index 631611a2651..7e21c46da92 100644 --- a/pyomo/gdp/tests/test_basic_step.py +++ b/pyomo/gdp/tests/test_basic_step.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index 13ffe30f9f0..d518219eabd 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_binary_multiplication.py b/pyomo/gdp/tests/test_binary_multiplication.py index 6b3ba87fa21..5f4c4f90ab6 100644 --- a/pyomo/gdp/tests/test_binary_multiplication.py +++ b/pyomo/gdp/tests/test_binary_multiplication.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_bound_pretransformation.py b/pyomo/gdp/tests/test_bound_pretransformation.py index 30ce76b7e31..68db64ce93b 100644 --- a/pyomo/gdp/tests/test_bound_pretransformation.py +++ b/pyomo/gdp/tests/test_bound_pretransformation.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_cuttingplane.py b/pyomo/gdp/tests/test_cuttingplane.py index 827eac9aa6a..153e236942d 100644 --- a/pyomo/gdp/tests/test_cuttingplane.py +++ b/pyomo/gdp/tests/test_cuttingplane.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_disjunct.py b/pyomo/gdp/tests/test_disjunct.py index 676b49a80cd..d969b245ee7 100644 --- a/pyomo/gdp/tests/test_disjunct.py +++ b/pyomo/gdp/tests/test_disjunct.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_fix_disjuncts.py b/pyomo/gdp/tests/test_fix_disjuncts.py index 1b741f7a840..6f01e096e9d 100644 --- a/pyomo/gdp/tests/test_fix_disjuncts.py +++ b/pyomo/gdp/tests/test_fix_disjuncts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_gdp.py b/pyomo/gdp/tests/test_gdp.py index 5c810dcce18..b22a60bc04a 100644 --- a/pyomo/gdp/tests/test_gdp.py +++ b/pyomo/gdp/tests/test_gdp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_gdp_reclassification_error.py b/pyomo/gdp/tests/test_gdp_reclassification_error.py index a65ccac2d8f..556dc44eead 100644 --- a/pyomo/gdp/tests/test_gdp_reclassification_error.py +++ b/pyomo/gdp/tests/test_gdp_reclassification_error.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 09f65765fe6..cf0ce3234af 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_mbigm.py b/pyomo/gdp/tests/test_mbigm.py index f067e1da5af..6310cb319e3 100644 --- a/pyomo/gdp/tests/test_mbigm.py +++ b/pyomo/gdp/tests/test_mbigm.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_partition_disjuncts.py b/pyomo/gdp/tests/test_partition_disjuncts.py index b050bc5e653..dc5ae9f70ce 100644 --- a/pyomo/gdp/tests/test_partition_disjuncts.py +++ b/pyomo/gdp/tests/test_partition_disjuncts.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_reclassify.py b/pyomo/gdp/tests/test_reclassify.py index dcf3470a211..223c28c5c7a 100644 --- a/pyomo/gdp/tests/test_reclassify.py +++ b/pyomo/gdp/tests/test_reclassify.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_transform_current_disjunctive_state.py b/pyomo/gdp/tests/test_transform_current_disjunctive_state.py index d257c3db8fb..54d80c910e5 100644 --- a/pyomo/gdp/tests/test_transform_current_disjunctive_state.py +++ b/pyomo/gdp/tests/test_transform_current_disjunctive_state.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/tests/test_util.py b/pyomo/gdp/tests/test_util.py index 90c63717b81..fd555fc2f59 100644 --- a/pyomo/gdp/tests/test_util.py +++ b/pyomo/gdp/tests/test_util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/transformed_disjunct.py b/pyomo/gdp/transformed_disjunct.py index 400f77a31f6..6cf60abf414 100644 --- a/pyomo/gdp/transformed_disjunct.py +++ b/pyomo/gdp/transformed_disjunct.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index b460a3d691c..343b2fd4f42 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/kernel/__init__.py b/pyomo/kernel/__init__.py index 6ecea6343cd..289fe83f0e4 100644 --- a/pyomo/kernel/__init__.py +++ b/pyomo/kernel/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/kernel/util.py b/pyomo/kernel/util.py index 5fba6a2c2d9..bdfd0939537 100644 --- a/pyomo/kernel/util.py +++ b/pyomo/kernel/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/__init__.py b/pyomo/mpec/__init__.py index 3989fe07b8e..a98ab94dc87 100644 --- a/pyomo/mpec/__init__.py +++ b/pyomo/mpec/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/complementarity.py b/pyomo/mpec/complementarity.py index df991ce9686..38da643cf38 100644 --- a/pyomo/mpec/complementarity.py +++ b/pyomo/mpec/complementarity.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/plugins/__init__.py b/pyomo/mpec/plugins/__init__.py index 3317e1ce829..1ff8c316e9b 100644 --- a/pyomo/mpec/plugins/__init__.py +++ b/pyomo/mpec/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/plugins/mpec1.py b/pyomo/mpec/plugins/mpec1.py index ad6905158c7..5935569d370 100644 --- a/pyomo/mpec/plugins/mpec1.py +++ b/pyomo/mpec/plugins/mpec1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/plugins/mpec2.py b/pyomo/mpec/plugins/mpec2.py index d019424ea4b..89d6c0814b2 100644 --- a/pyomo/mpec/plugins/mpec2.py +++ b/pyomo/mpec/plugins/mpec2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/plugins/mpec3.py b/pyomo/mpec/plugins/mpec3.py index d681c305a2d..1b7eb58b021 100644 --- a/pyomo/mpec/plugins/mpec3.py +++ b/pyomo/mpec/plugins/mpec3.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/plugins/mpec4.py b/pyomo/mpec/plugins/mpec4.py index 5b32886711a..fa3e37b16fe 100644 --- a/pyomo/mpec/plugins/mpec4.py +++ b/pyomo/mpec/plugins/mpec4.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/plugins/pathampl.py b/pyomo/mpec/plugins/pathampl.py index 7875251c04b..23b1b393ef3 100644 --- a/pyomo/mpec/plugins/pathampl.py +++ b/pyomo/mpec/plugins/pathampl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/plugins/solver1.py b/pyomo/mpec/plugins/solver1.py index 0ac1af85522..02659844f1c 100644 --- a/pyomo/mpec/plugins/solver1.py +++ b/pyomo/mpec/plugins/solver1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/plugins/solver2.py b/pyomo/mpec/plugins/solver2.py index 491c8122d2e..5f5b6922e6f 100644 --- a/pyomo/mpec/plugins/solver2.py +++ b/pyomo/mpec/plugins/solver2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/tests/__init__.py b/pyomo/mpec/tests/__init__.py index c5e495e5aa3..a2a2c61779a 100644 --- a/pyomo/mpec/tests/__init__.py +++ b/pyomo/mpec/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/tests/test_complementarity.py b/pyomo/mpec/tests/test_complementarity.py index 1eb0385c3e5..545104364cf 100644 --- a/pyomo/mpec/tests/test_complementarity.py +++ b/pyomo/mpec/tests/test_complementarity.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/tests/test_minlp.py b/pyomo/mpec/tests/test_minlp.py index 367a57b817e..965906f4235 100644 --- a/pyomo/mpec/tests/test_minlp.py +++ b/pyomo/mpec/tests/test_minlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/tests/test_nlp.py b/pyomo/mpec/tests/test_nlp.py index be5234136a1..a87d4ad2b09 100644 --- a/pyomo/mpec/tests/test_nlp.py +++ b/pyomo/mpec/tests/test_nlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/mpec/tests/test_path.py b/pyomo/mpec/tests/test_path.py index 5dd7178acf5..0501d19d2ac 100644 --- a/pyomo/mpec/tests/test_path.py +++ b/pyomo/mpec/tests/test_path.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/neos/__init__.py b/pyomo/neos/__init__.py index 73ac0c51216..7d18535e753 100644 --- a/pyomo/neos/__init__.py +++ b/pyomo/neos/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/neos/kestrel.py b/pyomo/neos/kestrel.py index 44734294eb4..8959a81bd0f 100644 --- a/pyomo/neos/kestrel.py +++ b/pyomo/neos/kestrel.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/neos/plugins/NEOS.py b/pyomo/neos/plugins/NEOS.py index 85fad42d4b2..84bc51645c0 100644 --- a/pyomo/neos/plugins/NEOS.py +++ b/pyomo/neos/plugins/NEOS.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/neos/plugins/__init__.py b/pyomo/neos/plugins/__init__.py index 323f96e9bdc..75105e87088 100644 --- a/pyomo/neos/plugins/__init__.py +++ b/pyomo/neos/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/neos/plugins/kestrel_plugin.py b/pyomo/neos/plugins/kestrel_plugin.py index 49fb3809622..fecb98e0084 100644 --- a/pyomo/neos/plugins/kestrel_plugin.py +++ b/pyomo/neos/plugins/kestrel_plugin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/neos/tests/__init__.py b/pyomo/neos/tests/__init__.py index 1cf642c0eac..83603e3d8ba 100644 --- a/pyomo/neos/tests/__init__.py +++ b/pyomo/neos/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/neos/tests/model_min_lp.py b/pyomo/neos/tests/model_min_lp.py index 56e1b124cd4..eacf0451c94 100644 --- a/pyomo/neos/tests/model_min_lp.py +++ b/pyomo/neos/tests/model_min_lp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/neos/tests/test_neos.py b/pyomo/neos/tests/test_neos.py index c43869e65cc..a4c4e9e6367 100644 --- a/pyomo/neos/tests/test_neos.py +++ b/pyomo/neos/tests/test_neos.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/__init__.py b/pyomo/network/__init__.py index 097471102be..6ccfb64f79c 100644 --- a/pyomo/network/__init__.py +++ b/pyomo/network/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/arc.py b/pyomo/network/arc.py index ff1874b0274..24efd0b25bf 100644 --- a/pyomo/network/arc.py +++ b/pyomo/network/arc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/decomposition.py b/pyomo/network/decomposition.py index ae306766ae0..da7e8950395 100644 --- a/pyomo/network/decomposition.py +++ b/pyomo/network/decomposition.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/foqus_graph.py b/pyomo/network/foqus_graph.py index e6fc34aaf62..e4cf3b92014 100644 --- a/pyomo/network/foqus_graph.py +++ b/pyomo/network/foqus_graph.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/plugins/__init__.py b/pyomo/network/plugins/__init__.py index 5e9677d2bc4..ab3cde23daa 100644 --- a/pyomo/network/plugins/__init__.py +++ b/pyomo/network/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/plugins/expand_arcs.py b/pyomo/network/plugins/expand_arcs.py index 4f6185d3173..b1f915214eb 100644 --- a/pyomo/network/plugins/expand_arcs.py +++ b/pyomo/network/plugins/expand_arcs.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/port.py b/pyomo/network/port.py index 4afb0e23ed0..1472a07224e 100644 --- a/pyomo/network/port.py +++ b/pyomo/network/port.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/tests/__init__.py b/pyomo/network/tests/__init__.py index 1eb6d95e148..173fdc4e727 100644 --- a/pyomo/network/tests/__init__.py +++ b/pyomo/network/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/tests/test_arc.py b/pyomo/network/tests/test_arc.py index cd340cace7a..f77dff07f2f 100644 --- a/pyomo/network/tests/test_arc.py +++ b/pyomo/network/tests/test_arc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/tests/test_decomposition.py b/pyomo/network/tests/test_decomposition.py index 4e4d0231d00..2db310217d0 100644 --- a/pyomo/network/tests/test_decomposition.py +++ b/pyomo/network/tests/test_decomposition.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/tests/test_port.py b/pyomo/network/tests/test_port.py index bc9a6fc527f..a417a832015 100644 --- a/pyomo/network/tests/test_port.py +++ b/pyomo/network/tests/test_port.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/network/util.py b/pyomo/network/util.py index be0fa2c84d1..4865218aca8 100644 --- a/pyomo/network/util.py +++ b/pyomo/network/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/__init__.py b/pyomo/opt/__init__.py index 8c12d3fa201..c78dd0384d2 100644 --- a/pyomo/opt/__init__.py +++ b/pyomo/opt/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/base/__init__.py b/pyomo/opt/base/__init__.py index 9d29efc859d..8d11114dd09 100644 --- a/pyomo/opt/base/__init__.py +++ b/pyomo/opt/base/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/base/convert.py b/pyomo/opt/base/convert.py index 8d8bd78e2ee..a17d1914801 100644 --- a/pyomo/opt/base/convert.py +++ b/pyomo/opt/base/convert.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/base/error.py b/pyomo/opt/base/error.py index aa97469f6d0..b03fafd7037 100644 --- a/pyomo/opt/base/error.py +++ b/pyomo/opt/base/error.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/base/formats.py b/pyomo/opt/base/formats.py index 2acd77b80e4..72c4f5306a7 100644 --- a/pyomo/opt/base/formats.py +++ b/pyomo/opt/base/formats.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/base/opt_config.py b/pyomo/opt/base/opt_config.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/opt/base/opt_config.py +++ b/pyomo/opt/base/opt_config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/base/problem.py b/pyomo/opt/base/problem.py index 6be1d4d6db6..02748e08b70 100644 --- a/pyomo/opt/base/problem.py +++ b/pyomo/opt/base/problem.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/base/results.py b/pyomo/opt/base/results.py index 68999fae6e4..8b00ec3e14e 100644 --- a/pyomo/opt/base/results.py +++ b/pyomo/opt/base/results.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/base/solvers.py b/pyomo/opt/base/solvers.py index b11e6393b02..cc49349142e 100644 --- a/pyomo/opt/base/solvers.py +++ b/pyomo/opt/base/solvers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/parallel/__init__.py b/pyomo/opt/parallel/__init__.py index 9820f39afd4..dbfdf2302ca 100644 --- a/pyomo/opt/parallel/__init__.py +++ b/pyomo/opt/parallel/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/parallel/async_solver.py b/pyomo/opt/parallel/async_solver.py index e9806b7125a..d74206e4790 100644 --- a/pyomo/opt/parallel/async_solver.py +++ b/pyomo/opt/parallel/async_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/parallel/local.py b/pyomo/opt/parallel/local.py index a7a80a7d33c..211adf92e5c 100644 --- a/pyomo/opt/parallel/local.py +++ b/pyomo/opt/parallel/local.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/parallel/manager.py b/pyomo/opt/parallel/manager.py index a97f6ae1d27..faa34d5190f 100644 --- a/pyomo/opt/parallel/manager.py +++ b/pyomo/opt/parallel/manager.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/plugins/__init__.py b/pyomo/opt/plugins/__init__.py index 797147f5f69..5ea2490b534 100644 --- a/pyomo/opt/plugins/__init__.py +++ b/pyomo/opt/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/plugins/driver.py b/pyomo/opt/plugins/driver.py index 23757053beb..c7c7103835c 100644 --- a/pyomo/opt/plugins/driver.py +++ b/pyomo/opt/plugins/driver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/plugins/res.py b/pyomo/opt/plugins/res.py index 25d25d5feb0..31971ee7d25 100644 --- a/pyomo/opt/plugins/res.py +++ b/pyomo/opt/plugins/res.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/plugins/sol.py b/pyomo/opt/plugins/sol.py index 297b1c87d06..10da469f186 100644 --- a/pyomo/opt/plugins/sol.py +++ b/pyomo/opt/plugins/sol.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/problem/__init__.py b/pyomo/opt/problem/__init__.py index 1b1a5328beb..8199553247d 100644 --- a/pyomo/opt/problem/__init__.py +++ b/pyomo/opt/problem/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/problem/ampl.py b/pyomo/opt/problem/ampl.py index 625c342f005..d128ec94930 100644 --- a/pyomo/opt/problem/ampl.py +++ b/pyomo/opt/problem/ampl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/results/__init__.py b/pyomo/opt/results/__init__.py index 8b2933adfe0..64a1b42ac86 100644 --- a/pyomo/opt/results/__init__.py +++ b/pyomo/opt/results/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/results/container.py b/pyomo/opt/results/container.py index 98a68048b45..1cdf6fe77ce 100644 --- a/pyomo/opt/results/container.py +++ b/pyomo/opt/results/container.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/results/problem.py b/pyomo/opt/results/problem.py index 71fd748dd81..d39ba204aaf 100644 --- a/pyomo/opt/results/problem.py +++ b/pyomo/opt/results/problem.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/results/results_.py b/pyomo/opt/results/results_.py index 2852bb72e8a..a9b802e2adb 100644 --- a/pyomo/opt/results/results_.py +++ b/pyomo/opt/results/results_.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/results/solution.py b/pyomo/opt/results/solution.py index 0cb8e92e730..2862087cf43 100644 --- a/pyomo/opt/results/solution.py +++ b/pyomo/opt/results/solution.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/results/solver.py b/pyomo/opt/results/solver.py index 5f9ceb3b68e..e2d0cfff605 100644 --- a/pyomo/opt/results/solver.py +++ b/pyomo/opt/results/solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/solver/__init__.py b/pyomo/opt/solver/__init__.py index 961d7e0edbd..6da73d408fa 100644 --- a/pyomo/opt/solver/__init__.py +++ b/pyomo/opt/solver/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/solver/ilmcmd.py b/pyomo/opt/solver/ilmcmd.py index d08feab7d9a..efd1096c20f 100644 --- a/pyomo/opt/solver/ilmcmd.py +++ b/pyomo/opt/solver/ilmcmd.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/solver/shellcmd.py b/pyomo/opt/solver/shellcmd.py index 20892000066..58274b572d3 100644 --- a/pyomo/opt/solver/shellcmd.py +++ b/pyomo/opt/solver/shellcmd.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/testing/__init__.py b/pyomo/opt/testing/__init__.py index 5d0d8ebd8d7..37ed419fbe3 100644 --- a/pyomo/opt/testing/__init__.py +++ b/pyomo/opt/testing/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/testing/pyunit.py b/pyomo/opt/testing/pyunit.py index 527b72cec7a..9143714f4e3 100644 --- a/pyomo/opt/testing/pyunit.py +++ b/pyomo/opt/testing/pyunit.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/__init__.py b/pyomo/opt/tests/__init__.py index 65dc8785c9b..b333eb78878 100644 --- a/pyomo/opt/tests/__init__.py +++ b/pyomo/opt/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/base/__init__.py b/pyomo/opt/tests/base/__init__.py index dbebb21e4f1..cde23945b56 100644 --- a/pyomo/opt/tests/base/__init__.py +++ b/pyomo/opt/tests/base/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/base/test_ampl.py b/pyomo/opt/tests/base/test_ampl.py index 1baffcbb0af..d37befcac57 100644 --- a/pyomo/opt/tests/base/test_ampl.py +++ b/pyomo/opt/tests/base/test_ampl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/base/test_convert.py b/pyomo/opt/tests/base/test_convert.py index f8f0bef0fe4..30a8fb0d1fc 100644 --- a/pyomo/opt/tests/base/test_convert.py +++ b/pyomo/opt/tests/base/test_convert.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/base/test_factory.py b/pyomo/opt/tests/base/test_factory.py index ab2a64a6330..441ba245c5e 100644 --- a/pyomo/opt/tests/base/test_factory.py +++ b/pyomo/opt/tests/base/test_factory.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/base/test_sol.py b/pyomo/opt/tests/base/test_sol.py index ff233b42a43..fada795b925 100644 --- a/pyomo/opt/tests/base/test_sol.py +++ b/pyomo/opt/tests/base/test_sol.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/base/test_soln.py b/pyomo/opt/tests/base/test_soln.py index 0511b3ceb9c..d39baeab15f 100644 --- a/pyomo/opt/tests/base/test_soln.py +++ b/pyomo/opt/tests/base/test_soln.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/base/test_solver.py b/pyomo/opt/tests/base/test_solver.py index 73d6067efe4..8ffc647804d 100644 --- a/pyomo/opt/tests/base/test_solver.py +++ b/pyomo/opt/tests/base/test_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/solver/__init__.py b/pyomo/opt/tests/solver/__init__.py index 4c145a1b507..d27a8ab41d6 100644 --- a/pyomo/opt/tests/solver/__init__.py +++ b/pyomo/opt/tests/solver/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/opt/tests/solver/test_shellcmd.py b/pyomo/opt/tests/solver/test_shellcmd.py index f71fcf07c6d..b6cc264b8f7 100644 --- a/pyomo/opt/tests/solver/test_shellcmd.py +++ b/pyomo/opt/tests/solver/test_shellcmd.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/pysp/__init__.py b/pyomo/pysp/__init__.py index 3fb4abbbd42..bb8a401e45e 100644 --- a/pyomo/pysp/__init__.py +++ b/pyomo/pysp/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/__init__.py b/pyomo/repn/__init__.py index 1b27071c404..842f4750127 100644 --- a/pyomo/repn/__init__.py +++ b/pyomo/repn/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/beta/__init__.py b/pyomo/repn/beta/__init__.py index fd7fac1125a..a75a75ec760 100644 --- a/pyomo/repn/beta/__init__.py +++ b/pyomo/repn/beta/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/beta/matrix.py b/pyomo/repn/beta/matrix.py index ff2d6857bd6..741e54d380c 100644 --- a/pyomo/repn/beta/matrix.py +++ b/pyomo/repn/beta/matrix.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 59bc0b58d99..6ab4abfdaf5 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/plugins/__init__.py b/pyomo/repn/plugins/__init__.py index 56b221d3129..d3804c55106 100644 --- a/pyomo/repn/plugins/__init__.py +++ b/pyomo/repn/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/plugins/ampl/__init__.py b/pyomo/repn/plugins/ampl/__init__.py index 493bc06d9c4..d935056c90b 100644 --- a/pyomo/repn/plugins/ampl/__init__.py +++ b/pyomo/repn/plugins/ampl/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/plugins/ampl/ampl_.py b/pyomo/repn/plugins/ampl/ampl_.py index d1a11bf2f38..4cc55cabd51 100644 --- a/pyomo/repn/plugins/ampl/ampl_.py +++ b/pyomo/repn/plugins/ampl/ampl_.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/plugins/baron_writer.py b/pyomo/repn/plugins/baron_writer.py index 0d684fcd1d2..de19b5aad73 100644 --- a/pyomo/repn/plugins/baron_writer.py +++ b/pyomo/repn/plugins/baron_writer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/plugins/cpxlp.py b/pyomo/repn/plugins/cpxlp.py index cdcb4b42c3b..46e6b6d5265 100644 --- a/pyomo/repn/plugins/cpxlp.py +++ b/pyomo/repn/plugins/cpxlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/plugins/gams_writer.py b/pyomo/repn/plugins/gams_writer.py index 719839fc8dd..5f94f176762 100644 --- a/pyomo/repn/plugins/gams_writer.py +++ b/pyomo/repn/plugins/gams_writer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/plugins/lp_writer.py b/pyomo/repn/plugins/lp_writer.py index be718ee696e..627a54e3f68 100644 --- a/pyomo/repn/plugins/lp_writer.py +++ b/pyomo/repn/plugins/lp_writer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/plugins/mps.py b/pyomo/repn/plugins/mps.py index f40c7666278..ba26783eea1 100644 --- a/pyomo/repn/plugins/mps.py +++ b/pyomo/repn/plugins/mps.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index cda4ee011d3..5c0b505a2be 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index c72661daaf0..239cd845930 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/quadratic.py b/pyomo/repn/quadratic.py index 2d11261de5d..c538d1efc7f 100644 --- a/pyomo/repn/quadratic.py +++ b/pyomo/repn/quadratic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/standard_aux.py b/pyomo/repn/standard_aux.py index 8704253eca3..628914780a6 100644 --- a/pyomo/repn/standard_aux.py +++ b/pyomo/repn/standard_aux.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 53618d3eb50..c1cca42afe4 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/__init__.py b/pyomo/repn/tests/__init__.py index 5e413c0132c..a9e1a5bea47 100644 --- a/pyomo/repn/tests/__init__.py +++ b/pyomo/repn/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/__init__.py b/pyomo/repn/tests/ampl/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/repn/tests/ampl/__init__.py +++ b/pyomo/repn/tests/ampl/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/helper.py b/pyomo/repn/tests/ampl/helper.py index eb09afc37cc..2bf2198d20f 100644 --- a/pyomo/repn/tests/ampl/helper.py +++ b/pyomo/repn/tests/ampl/helper.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/nl_diff.py b/pyomo/repn/tests/ampl/nl_diff.py index ecac3967dfe..9fe352ee503 100644 --- a/pyomo/repn/tests/ampl/nl_diff.py +++ b/pyomo/repn/tests/ampl/nl_diff.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small10_testCase.py b/pyomo/repn/tests/ampl/small10_testCase.py index f51aea76d3e..deb56f92a88 100644 --- a/pyomo/repn/tests/ampl/small10_testCase.py +++ b/pyomo/repn/tests/ampl/small10_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small11_testCase.py b/pyomo/repn/tests/ampl/small11_testCase.py index 5874007e13c..11b61805d5e 100644 --- a/pyomo/repn/tests/ampl/small11_testCase.py +++ b/pyomo/repn/tests/ampl/small11_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small12_testCase.py b/pyomo/repn/tests/ampl/small12_testCase.py index 63d4ba29cf6..b73a8f528f2 100644 --- a/pyomo/repn/tests/ampl/small12_testCase.py +++ b/pyomo/repn/tests/ampl/small12_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small13_testCase.py b/pyomo/repn/tests/ampl/small13_testCase.py index 9814c979cc7..c24185bf8d7 100644 --- a/pyomo/repn/tests/ampl/small13_testCase.py +++ b/pyomo/repn/tests/ampl/small13_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small14_testCase.py b/pyomo/repn/tests/ampl/small14_testCase.py index 3d896242243..fb2c2bc6c5e 100644 --- a/pyomo/repn/tests/ampl/small14_testCase.py +++ b/pyomo/repn/tests/ampl/small14_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small15_testCase.py b/pyomo/repn/tests/ampl/small15_testCase.py index 8345621cecd..d4d5796aaa5 100644 --- a/pyomo/repn/tests/ampl/small15_testCase.py +++ b/pyomo/repn/tests/ampl/small15_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small1_testCase.py b/pyomo/repn/tests/ampl/small1_testCase.py index 00e6dd322ed..06f5ad122d9 100644 --- a/pyomo/repn/tests/ampl/small1_testCase.py +++ b/pyomo/repn/tests/ampl/small1_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small2_testCase.py b/pyomo/repn/tests/ampl/small2_testCase.py index 2df3aebb139..8a65779f55e 100644 --- a/pyomo/repn/tests/ampl/small2_testCase.py +++ b/pyomo/repn/tests/ampl/small2_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small3_testCase.py b/pyomo/repn/tests/ampl/small3_testCase.py index f11137979b4..999143d9a0c 100644 --- a/pyomo/repn/tests/ampl/small3_testCase.py +++ b/pyomo/repn/tests/ampl/small3_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small4_testCase.py b/pyomo/repn/tests/ampl/small4_testCase.py index 08d68c21f50..9736dd9bf3b 100644 --- a/pyomo/repn/tests/ampl/small4_testCase.py +++ b/pyomo/repn/tests/ampl/small4_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small5_testCase.py b/pyomo/repn/tests/ampl/small5_testCase.py index 1e976820f9b..1f254b7f04d 100644 --- a/pyomo/repn/tests/ampl/small5_testCase.py +++ b/pyomo/repn/tests/ampl/small5_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small6_testCase.py b/pyomo/repn/tests/ampl/small6_testCase.py index da9f1d58f9b..9d309c09fef 100644 --- a/pyomo/repn/tests/ampl/small6_testCase.py +++ b/pyomo/repn/tests/ampl/small6_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small7_testCase.py b/pyomo/repn/tests/ampl/small7_testCase.py index 22a75a33394..485962dd211 100644 --- a/pyomo/repn/tests/ampl/small7_testCase.py +++ b/pyomo/repn/tests/ampl/small7_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small8_testCase.py b/pyomo/repn/tests/ampl/small8_testCase.py index 554e27c0924..61a3e3ccce7 100644 --- a/pyomo/repn/tests/ampl/small8_testCase.py +++ b/pyomo/repn/tests/ampl/small8_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/small9_testCase.py b/pyomo/repn/tests/ampl/small9_testCase.py index 3d7af602a88..7cb0913a762 100644 --- a/pyomo/repn/tests/ampl/small9_testCase.py +++ b/pyomo/repn/tests/ampl/small9_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/test_ampl_comparison.py b/pyomo/repn/tests/ampl/test_ampl_comparison.py index eb5aff329e1..8210bbdd173 100644 --- a/pyomo/repn/tests/ampl/test_ampl_comparison.py +++ b/pyomo/repn/tests/ampl/test_ampl_comparison.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/test_ampl_nl.py b/pyomo/repn/tests/ampl/test_ampl_nl.py index bd58c254bfd..53a2d3cda82 100644 --- a/pyomo/repn/tests/ampl/test_ampl_nl.py +++ b/pyomo/repn/tests/ampl/test_ampl_nl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/test_ampl_repn.py b/pyomo/repn/tests/ampl/test_ampl_repn.py index cf1a889006e..9c911540eb0 100644 --- a/pyomo/repn/tests/ampl/test_ampl_repn.py +++ b/pyomo/repn/tests/ampl/test_ampl_repn.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 6422a2b0020..8b95fc03bdb 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/ampl/test_suffixes.py b/pyomo/repn/tests/ampl/test_suffixes.py index e73060e7e8c..1372da68bdc 100644 --- a/pyomo/repn/tests/ampl/test_suffixes.py +++ b/pyomo/repn/tests/ampl/test_suffixes.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/baron/__init__.py b/pyomo/repn/tests/baron/__init__.py index 030f46eaca8..c693bb8accd 100644 --- a/pyomo/repn/tests/baron/__init__.py +++ b/pyomo/repn/tests/baron/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/baron/small14a_testCase.py b/pyomo/repn/tests/baron/small14a_testCase.py index 72190756dc7..b2cf5afcb72 100644 --- a/pyomo/repn/tests/baron/small14a_testCase.py +++ b/pyomo/repn/tests/baron/small14a_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/baron/test_baron.py b/pyomo/repn/tests/baron/test_baron.py index 348ad6036fb..6f22f26cd38 100644 --- a/pyomo/repn/tests/baron/test_baron.py +++ b/pyomo/repn/tests/baron/test_baron.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/baron/test_baron_comparison.py b/pyomo/repn/tests/baron/test_baron_comparison.py index 7c480321624..1b394f6a5b1 100644 --- a/pyomo/repn/tests/baron/test_baron_comparison.py +++ b/pyomo/repn/tests/baron/test_baron_comparison.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/cpxlp/__init__.py b/pyomo/repn/tests/cpxlp/__init__.py index 8ffbfd52054..f216a76f48b 100644 --- a/pyomo/repn/tests/cpxlp/__init__.py +++ b/pyomo/repn/tests/cpxlp/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/cpxlp/test_cpxlp.py b/pyomo/repn/tests/cpxlp/test_cpxlp.py index 28c9043a8de..567c5184517 100644 --- a/pyomo/repn/tests/cpxlp/test_cpxlp.py +++ b/pyomo/repn/tests/cpxlp/test_cpxlp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/cpxlp/test_lpv2.py b/pyomo/repn/tests/cpxlp/test_lpv2.py index 336939a4d7d..fbef24c77c3 100644 --- a/pyomo/repn/tests/cpxlp/test_lpv2.py +++ b/pyomo/repn/tests/cpxlp/test_lpv2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/diffutils.py b/pyomo/repn/tests/diffutils.py index 24188d46c86..c346f8c48b2 100644 --- a/pyomo/repn/tests/diffutils.py +++ b/pyomo/repn/tests/diffutils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/gams/__init__.py b/pyomo/repn/tests/gams/__init__.py index 8d13c4ffb99..e548666fd72 100644 --- a/pyomo/repn/tests/gams/__init__.py +++ b/pyomo/repn/tests/gams/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/gams/small14a_testCase.py b/pyomo/repn/tests/gams/small14a_testCase.py index c7e3e0805ea..1efdd1baa25 100644 --- a/pyomo/repn/tests/gams/small14a_testCase.py +++ b/pyomo/repn/tests/gams/small14a_testCase.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/gams/test_gams.py b/pyomo/repn/tests/gams/test_gams.py index e6b729e5dfc..e3304e18491 100644 --- a/pyomo/repn/tests/gams/test_gams.py +++ b/pyomo/repn/tests/gams/test_gams.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/gams/test_gams_comparison.py b/pyomo/repn/tests/gams/test_gams_comparison.py index 4e530b10d43..42fa9f71dda 100644 --- a/pyomo/repn/tests/gams/test_gams_comparison.py +++ b/pyomo/repn/tests/gams/test_gams_comparison.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/lp_diff.py b/pyomo/repn/tests/lp_diff.py index 23b24f8b51b..2c119d72c6f 100644 --- a/pyomo/repn/tests/lp_diff.py +++ b/pyomo/repn/tests/lp_diff.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/mps/__init__.py b/pyomo/repn/tests/mps/__init__.py index 1a8a69a1409..effc182aa1c 100644 --- a/pyomo/repn/tests/mps/__init__.py +++ b/pyomo/repn/tests/mps/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/mps/test_mps.py b/pyomo/repn/tests/mps/test_mps.py index 9be45a17870..ff7981b391c 100644 --- a/pyomo/repn/tests/mps/test_mps.py +++ b/pyomo/repn/tests/mps/test_mps.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/nl_diff.py b/pyomo/repn/tests/nl_diff.py index e96d6f6357b..aa2b4519db3 100644 --- a/pyomo/repn/tests/nl_diff.py +++ b/pyomo/repn/tests/nl_diff.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index d4f268ae182..6843650d0c2 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/test_quadratic.py b/pyomo/repn/tests/test_quadratic.py index 605c859464a..2d2e4022037 100644 --- a/pyomo/repn/tests/test_quadratic.py +++ b/pyomo/repn/tests/test_quadratic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/test_standard.py b/pyomo/repn/tests/test_standard.py index b62d18e6eff..6c5a6e3e033 100644 --- a/pyomo/repn/tests/test_standard.py +++ b/pyomo/repn/tests/test_standard.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/test_standard_form.py b/pyomo/repn/tests/test_standard_form.py index d186f28dab8..e24195edfde 100644 --- a/pyomo/repn/tests/test_standard_form.py +++ b/pyomo/repn/tests/test_standard_form.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/tests/test_util.py b/pyomo/repn/tests/test_util.py index c4902a7064d..b5e4cc4facf 100644 --- a/pyomo/repn/tests/test_util.py +++ b/pyomo/repn/tests/test_util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 634b4d1d640..49cca32eaf9 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/__init__.py b/pyomo/scripting/__init__.py index a3c2c1bb7ce..7cb5ac652fc 100644 --- a/pyomo/scripting/__init__.py +++ b/pyomo/scripting/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/commands.py b/pyomo/scripting/commands.py index 7782962c2c1..ef59d64b542 100644 --- a/pyomo/scripting/commands.py +++ b/pyomo/scripting/commands.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/convert.py b/pyomo/scripting/convert.py index 2f0c0e5b400..997e69ac7c9 100644 --- a/pyomo/scripting/convert.py +++ b/pyomo/scripting/convert.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/driver_help.py b/pyomo/scripting/driver_help.py index 81970a6b5cc..38d1a4c16bf 100644 --- a/pyomo/scripting/driver_help.py +++ b/pyomo/scripting/driver_help.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/interface.py b/pyomo/scripting/interface.py index efb97470e43..fca485b279b 100644 --- a/pyomo/scripting/interface.py +++ b/pyomo/scripting/interface.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/plugins/__init__.py b/pyomo/scripting/plugins/__init__.py index 44e3956f314..86a3100e077 100644 --- a/pyomo/scripting/plugins/__init__.py +++ b/pyomo/scripting/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/plugins/build_ext.py b/pyomo/scripting/plugins/build_ext.py index 9ae63cbb8a1..5b4ac836a00 100644 --- a/pyomo/scripting/plugins/build_ext.py +++ b/pyomo/scripting/plugins/build_ext.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/plugins/convert.py b/pyomo/scripting/plugins/convert.py index 55290ed90ce..ea6742cec56 100644 --- a/pyomo/scripting/plugins/convert.py +++ b/pyomo/scripting/plugins/convert.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/plugins/download.py b/pyomo/scripting/plugins/download.py index 73a164ee708..eea858a737f 100644 --- a/pyomo/scripting/plugins/download.py +++ b/pyomo/scripting/plugins/download.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/plugins/extras.py b/pyomo/scripting/plugins/extras.py index 4cf9e623212..2bd1c4a0803 100644 --- a/pyomo/scripting/plugins/extras.py +++ b/pyomo/scripting/plugins/extras.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/plugins/solve.py b/pyomo/scripting/plugins/solve.py index 69451a04e3c..b2a849e995b 100644 --- a/pyomo/scripting/plugins/solve.py +++ b/pyomo/scripting/plugins/solve.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/pyomo_command.py b/pyomo/scripting/pyomo_command.py index b652e95372a..8beec41a8b1 100644 --- a/pyomo/scripting/pyomo_command.py +++ b/pyomo/scripting/pyomo_command.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/pyomo_main.py b/pyomo/scripting/pyomo_main.py index 9acafea0471..6497206fdda 100644 --- a/pyomo/scripting/pyomo_main.py +++ b/pyomo/scripting/pyomo_main.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/pyomo_parser.py b/pyomo/scripting/pyomo_parser.py index 345d400a1aa..09998085576 100644 --- a/pyomo/scripting/pyomo_parser.py +++ b/pyomo/scripting/pyomo_parser.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/solve_config.py b/pyomo/scripting/solve_config.py index 3048431d443..7ce3505d045 100644 --- a/pyomo/scripting/solve_config.py +++ b/pyomo/scripting/solve_config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/tests/__init__.py b/pyomo/scripting/tests/__init__.py index 88e18b19035..d9146f7eee4 100644 --- a/pyomo/scripting/tests/__init__.py +++ b/pyomo/scripting/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/tests/test_cmds.py b/pyomo/scripting/tests/test_cmds.py index 960e0d4ada1..9a120c8c175 100644 --- a/pyomo/scripting/tests/test_cmds.py +++ b/pyomo/scripting/tests/test_cmds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/scripting/util.py b/pyomo/scripting/util.py index 5bc65eb35ae..b2a30ebaecd 100644 --- a/pyomo/scripting/util.py +++ b/pyomo/scripting/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/__init__.py b/pyomo/solvers/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/solvers/__init__.py +++ b/pyomo/solvers/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/mockmip.py b/pyomo/solvers/mockmip.py index 9497a6dff9d..2c28b7a9be0 100644 --- a/pyomo/solvers/mockmip.py +++ b/pyomo/solvers/mockmip.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/__init__.py b/pyomo/solvers/plugins/__init__.py index 797ed5036bd..2a7bf2fea04 100644 --- a/pyomo/solvers/plugins/__init__.py +++ b/pyomo/solvers/plugins/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/converter/__init__.py b/pyomo/solvers/plugins/converter/__init__.py index b6baf4f6682..56c32f1c8c1 100644 --- a/pyomo/solvers/plugins/converter/__init__.py +++ b/pyomo/solvers/plugins/converter/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/converter/ampl.py b/pyomo/solvers/plugins/converter/ampl.py index b718faf2d21..0798115a448 100644 --- a/pyomo/solvers/plugins/converter/ampl.py +++ b/pyomo/solvers/plugins/converter/ampl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/converter/glpsol.py b/pyomo/solvers/plugins/converter/glpsol.py index a38892e3cf5..9b404567c4d 100644 --- a/pyomo/solvers/plugins/converter/glpsol.py +++ b/pyomo/solvers/plugins/converter/glpsol.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/converter/model.py b/pyomo/solvers/plugins/converter/model.py index 89a521d1521..817df157bf5 100644 --- a/pyomo/solvers/plugins/converter/model.py +++ b/pyomo/solvers/plugins/converter/model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/converter/pico.py b/pyomo/solvers/plugins/converter/pico.py index 7fd0d11222b..e5d008da347 100644 --- a/pyomo/solvers/plugins/converter/pico.py +++ b/pyomo/solvers/plugins/converter/pico.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/ASL.py b/pyomo/solvers/plugins/solvers/ASL.py index debcd27f75e..ae7ad82c870 100644 --- a/pyomo/solvers/plugins/solvers/ASL.py +++ b/pyomo/solvers/plugins/solvers/ASL.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/BARON.py b/pyomo/solvers/plugins/solvers/BARON.py index eb5ac0830c5..044cab27b86 100644 --- a/pyomo/solvers/plugins/solvers/BARON.py +++ b/pyomo/solvers/plugins/solvers/BARON.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/CBCplugin.py b/pyomo/solvers/plugins/solvers/CBCplugin.py index 86871dbc1ac..108b142a9e0 100644 --- a/pyomo/solvers/plugins/solvers/CBCplugin.py +++ b/pyomo/solvers/plugins/solvers/CBCplugin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/CONOPT.py b/pyomo/solvers/plugins/solvers/CONOPT.py index 30e8ada11a1..89ee3848805 100644 --- a/pyomo/solvers/plugins/solvers/CONOPT.py +++ b/pyomo/solvers/plugins/solvers/CONOPT.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/CPLEX.py b/pyomo/solvers/plugins/solvers/CPLEX.py index 9755bc58614..b2b8c5e988d 100644 --- a/pyomo/solvers/plugins/solvers/CPLEX.py +++ b/pyomo/solvers/plugins/solvers/CPLEX.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/GAMS.py b/pyomo/solvers/plugins/solvers/GAMS.py index d0365d49078..e84cbdb441d 100644 --- a/pyomo/solvers/plugins/solvers/GAMS.py +++ b/pyomo/solvers/plugins/solvers/GAMS.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/GLPK.py b/pyomo/solvers/plugins/solvers/GLPK.py index a5b8ad9c019..39948d465f4 100644 --- a/pyomo/solvers/plugins/solvers/GLPK.py +++ b/pyomo/solvers/plugins/solvers/GLPK.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/GUROBI.py b/pyomo/solvers/plugins/solvers/GUROBI.py index e0eddf008af..c8b0912970e 100644 --- a/pyomo/solvers/plugins/solvers/GUROBI.py +++ b/pyomo/solvers/plugins/solvers/GUROBI.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/GUROBI_RUN.py b/pyomo/solvers/plugins/solvers/GUROBI_RUN.py index 2b505adf49c..88f953e18ae 100644 --- a/pyomo/solvers/plugins/solvers/GUROBI_RUN.py +++ b/pyomo/solvers/plugins/solvers/GUROBI_RUN.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index 611180113c8..deda4314a52 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/SCIPAMPL.py b/pyomo/solvers/plugins/solvers/SCIPAMPL.py index 9898b9cdd90..be7415a19ef 100644 --- a/pyomo/solvers/plugins/solvers/SCIPAMPL.py +++ b/pyomo/solvers/plugins/solvers/SCIPAMPL.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/XPRESS.py b/pyomo/solvers/plugins/solvers/XPRESS.py index 7b85aea1266..2c16d971144 100644 --- a/pyomo/solvers/plugins/solvers/XPRESS.py +++ b/pyomo/solvers/plugins/solvers/XPRESS.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/__init__.py b/pyomo/solvers/plugins/solvers/__init__.py index c5fbfa97e42..9b2507d876c 100644 --- a/pyomo/solvers/plugins/solvers/__init__.py +++ b/pyomo/solvers/plugins/solvers/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/cplex_direct.py b/pyomo/solvers/plugins/solvers/cplex_direct.py index 308d3438329..93d8015514e 100644 --- a/pyomo/solvers/plugins/solvers/cplex_direct.py +++ b/pyomo/solvers/plugins/solvers/cplex_direct.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/cplex_persistent.py b/pyomo/solvers/plugins/solvers/cplex_persistent.py index a7fdcc45ade..fd396a8c87f 100644 --- a/pyomo/solvers/plugins/solvers/cplex_persistent.py +++ b/pyomo/solvers/plugins/solvers/cplex_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py b/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py index 09bbfbda70f..c131b8ad10a 100644 --- a/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py +++ b/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/direct_solver.py b/pyomo/solvers/plugins/solvers/direct_solver.py index 4f90a753fe6..3eab658391c 100644 --- a/pyomo/solvers/plugins/solvers/direct_solver.py +++ b/pyomo/solvers/plugins/solvers/direct_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/gurobi_direct.py b/pyomo/solvers/plugins/solvers/gurobi_direct.py index 54ea9111508..1d88eced629 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_direct.py +++ b/pyomo/solvers/plugins/solvers/gurobi_direct.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/gurobi_persistent.py b/pyomo/solvers/plugins/solvers/gurobi_persistent.py index 382cb7c4e6d..4522a2151c3 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_persistent.py +++ b/pyomo/solvers/plugins/solvers/gurobi_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/mosek_direct.py b/pyomo/solvers/plugins/solvers/mosek_direct.py index 4c0718bfe74..5000a2f35c4 100644 --- a/pyomo/solvers/plugins/solvers/mosek_direct.py +++ b/pyomo/solvers/plugins/solvers/mosek_direct.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/mosek_persistent.py b/pyomo/solvers/plugins/solvers/mosek_persistent.py index 6eaad564781..97f88e0cb9a 100644 --- a/pyomo/solvers/plugins/solvers/mosek_persistent.py +++ b/pyomo/solvers/plugins/solvers/mosek_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/persistent_solver.py b/pyomo/solvers/plugins/solvers/persistent_solver.py index 141621d0a31..29aa3f2bbf5 100644 --- a/pyomo/solvers/plugins/solvers/persistent_solver.py +++ b/pyomo/solvers/plugins/solvers/persistent_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/pywrapper.py b/pyomo/solvers/plugins/solvers/pywrapper.py index 8f72e630a3d..c3ec2eaf709 100644 --- a/pyomo/solvers/plugins/solvers/pywrapper.py +++ b/pyomo/solvers/plugins/solvers/pywrapper.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/xpress_direct.py b/pyomo/solvers/plugins/solvers/xpress_direct.py index aa5a4ba1b4e..75cf8f921df 100644 --- a/pyomo/solvers/plugins/solvers/xpress_direct.py +++ b/pyomo/solvers/plugins/solvers/xpress_direct.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/plugins/solvers/xpress_persistent.py b/pyomo/solvers/plugins/solvers/xpress_persistent.py index 56024bc0540..513a7fbc257 100644 --- a/pyomo/solvers/plugins/solvers/xpress_persistent.py +++ b/pyomo/solvers/plugins/solvers/xpress_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/__init__.py b/pyomo/solvers/tests/__init__.py index 42c694b0170..4d8d45da724 100644 --- a/pyomo/solvers/tests/__init__.py +++ b/pyomo/solvers/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/__init__.py b/pyomo/solvers/tests/checks/__init__.py index 03a34303759..ccd3a0f98a4 100644 --- a/pyomo/solvers/tests/checks/__init__.py +++ b/pyomo/solvers/tests/checks/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_BARON.py b/pyomo/solvers/tests/checks/test_BARON.py index 897f1e88a42..29c7ffb0148 100644 --- a/pyomo/solvers/tests/checks/test_BARON.py +++ b/pyomo/solvers/tests/checks/test_BARON.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_CBCplugin.py b/pyomo/solvers/tests/checks/test_CBCplugin.py index fe01a89bb53..2ea0e55c5f4 100644 --- a/pyomo/solvers/tests/checks/test_CBCplugin.py +++ b/pyomo/solvers/tests/checks/test_CBCplugin.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_CPLEXDirect.py b/pyomo/solvers/tests/checks/test_CPLEXDirect.py index 86e03d1024f..400d7ee5f75 100644 --- a/pyomo/solvers/tests/checks/test_CPLEXDirect.py +++ b/pyomo/solvers/tests/checks/test_CPLEXDirect.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_CPLEXPersistent.py b/pyomo/solvers/tests/checks/test_CPLEXPersistent.py index d7f00d0f486..91a60eee9dd 100644 --- a/pyomo/solvers/tests/checks/test_CPLEXPersistent.py +++ b/pyomo/solvers/tests/checks/test_CPLEXPersistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_GAMS.py b/pyomo/solvers/tests/checks/test_GAMS.py index 7aa952a6c69..1eef09819f7 100644 --- a/pyomo/solvers/tests/checks/test_GAMS.py +++ b/pyomo/solvers/tests/checks/test_GAMS.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_MOSEKDirect.py b/pyomo/solvers/tests/checks/test_MOSEKDirect.py index 369cc08161a..2cf7034b80a 100644 --- a/pyomo/solvers/tests/checks/test_MOSEKDirect.py +++ b/pyomo/solvers/tests/checks/test_MOSEKDirect.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_MOSEKPersistent.py b/pyomo/solvers/tests/checks/test_MOSEKPersistent.py index 6db99919177..a4c0aa21666 100644 --- a/pyomo/solvers/tests/checks/test_MOSEKPersistent.py +++ b/pyomo/solvers/tests/checks/test_MOSEKPersistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_cbc.py b/pyomo/solvers/tests/checks/test_cbc.py index 0fd6e9f49a1..420de7cc61d 100644 --- a/pyomo/solvers/tests/checks/test_cbc.py +++ b/pyomo/solvers/tests/checks/test_cbc.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_cplex.py b/pyomo/solvers/tests/checks/test_cplex.py index 44b82d2ad77..ff5ac5f17e1 100644 --- a/pyomo/solvers/tests/checks/test_cplex.py +++ b/pyomo/solvers/tests/checks/test_cplex.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_gurobi.py b/pyomo/solvers/tests/checks/test_gurobi.py index cfd0f077eab..e87685a046c 100644 --- a/pyomo/solvers/tests/checks/test_gurobi.py +++ b/pyomo/solvers/tests/checks/test_gurobi.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_gurobi_direct.py b/pyomo/solvers/tests/checks/test_gurobi_direct.py index d9802894c47..1e3a366a37a 100644 --- a/pyomo/solvers/tests/checks/test_gurobi_direct.py +++ b/pyomo/solvers/tests/checks/test_gurobi_direct.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_gurobi_persistent.py b/pyomo/solvers/tests/checks/test_gurobi_persistent.py index 9d69c1dd920..a2c089207e5 100644 --- a/pyomo/solvers/tests/checks/test_gurobi_persistent.py +++ b/pyomo/solvers/tests/checks/test_gurobi_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_no_solution_behavior.py b/pyomo/solvers/tests/checks/test_no_solution_behavior.py index 9ba8e86a013..81a2d2bf297 100644 --- a/pyomo/solvers/tests/checks/test_no_solution_behavior.py +++ b/pyomo/solvers/tests/checks/test_no_solution_behavior.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_pickle.py b/pyomo/solvers/tests/checks/test_pickle.py index d8551b34740..745320cb4eb 100644 --- a/pyomo/solvers/tests/checks/test_pickle.py +++ b/pyomo/solvers/tests/checks/test_pickle.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_writers.py b/pyomo/solvers/tests/checks/test_writers.py index e406e07a4d6..55002c71357 100644 --- a/pyomo/solvers/tests/checks/test_writers.py +++ b/pyomo/solvers/tests/checks/test_writers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/checks/test_xpress_persistent.py b/pyomo/solvers/tests/checks/test_xpress_persistent.py index abfcf9c0afc..ddae860cd92 100644 --- a/pyomo/solvers/tests/checks/test_xpress_persistent.py +++ b/pyomo/solvers/tests/checks/test_xpress_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/__init__.py b/pyomo/solvers/tests/mip/__init__.py index c95d27d9497..707a8c4b7e5 100644 --- a/pyomo/solvers/tests/mip/__init__.py +++ b/pyomo/solvers/tests/mip/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/model.py b/pyomo/solvers/tests/mip/model.py index 389151160b8..83c1411fe6c 100644 --- a/pyomo/solvers/tests/mip/model.py +++ b/pyomo/solvers/tests/mip/model.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/test_asl.py b/pyomo/solvers/tests/mip/test_asl.py index 42b77df7d87..6f23a06eff2 100644 --- a/pyomo/solvers/tests/mip/test_asl.py +++ b/pyomo/solvers/tests/mip/test_asl.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/test_convert.py b/pyomo/solvers/tests/mip/test_convert.py index cd916da29f2..962b021c4ae 100644 --- a/pyomo/solvers/tests/mip/test_convert.py +++ b/pyomo/solvers/tests/mip/test_convert.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/test_factory.py b/pyomo/solvers/tests/mip/test_factory.py index 6960a0f8ced..31d47486aa4 100644 --- a/pyomo/solvers/tests/mip/test_factory.py +++ b/pyomo/solvers/tests/mip/test_factory.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/test_ipopt.py b/pyomo/solvers/tests/mip/test_ipopt.py index bccb4f2a27c..38c3b35d8a1 100644 --- a/pyomo/solvers/tests/mip/test_ipopt.py +++ b/pyomo/solvers/tests/mip/test_ipopt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/test_mip.py b/pyomo/solvers/tests/mip/test_mip.py index 0257e65de20..58cdfe9f7de 100644 --- a/pyomo/solvers/tests/mip/test_mip.py +++ b/pyomo/solvers/tests/mip/test_mip.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/test_qp.py b/pyomo/solvers/tests/mip/test_qp.py index 5d920b9085d..9c5cb5ffbc4 100644 --- a/pyomo/solvers/tests/mip/test_qp.py +++ b/pyomo/solvers/tests/mip/test_qp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/test_scip.py b/pyomo/solvers/tests/mip/test_scip.py index 7fffdc53c13..01de0d16826 100644 --- a/pyomo/solvers/tests/mip/test_scip.py +++ b/pyomo/solvers/tests/mip/test_scip.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/test_scip_log_data.py b/pyomo/solvers/tests/mip/test_scip_log_data.py index 0dc0825afb3..a0006d69eb7 100644 --- a/pyomo/solvers/tests/mip/test_scip_log_data.py +++ b/pyomo/solvers/tests/mip/test_scip_log_data.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/test_scip_version.py b/pyomo/solvers/tests/mip/test_scip_version.py index c0cc80c0316..f83bed2da32 100644 --- a/pyomo/solvers/tests/mip/test_scip_version.py +++ b/pyomo/solvers/tests/mip/test_scip_version.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/mip/test_solver.py b/pyomo/solvers/tests/mip/test_solver.py index 90a7076cbca..bf3550a001d 100644 --- a/pyomo/solvers/tests/mip/test_solver.py +++ b/pyomo/solvers/tests/mip/test_solver.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_block.py b/pyomo/solvers/tests/models/LP_block.py index 64c866faa9e..37b01dc1c2d 100644 --- a/pyomo/solvers/tests/models/LP_block.py +++ b/pyomo/solvers/tests/models/LP_block.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_compiled.py b/pyomo/solvers/tests/models/LP_compiled.py index 686406e7ec6..960b8730e0c 100644 --- a/pyomo/solvers/tests/models/LP_compiled.py +++ b/pyomo/solvers/tests/models/LP_compiled.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_constant_objective1.py b/pyomo/solvers/tests/models/LP_constant_objective1.py index 306a7a867a2..0c01cd7085f 100644 --- a/pyomo/solvers/tests/models/LP_constant_objective1.py +++ b/pyomo/solvers/tests/models/LP_constant_objective1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_constant_objective2.py b/pyomo/solvers/tests/models/LP_constant_objective2.py index 17da01bf209..07739c1f708 100644 --- a/pyomo/solvers/tests/models/LP_constant_objective2.py +++ b/pyomo/solvers/tests/models/LP_constant_objective2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_duals_maximize.py b/pyomo/solvers/tests/models/LP_duals_maximize.py index 61d827daa62..ed45e4eee29 100644 --- a/pyomo/solvers/tests/models/LP_duals_maximize.py +++ b/pyomo/solvers/tests/models/LP_duals_maximize.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_duals_minimize.py b/pyomo/solvers/tests/models/LP_duals_minimize.py index 77471d0182c..3f97276a61e 100644 --- a/pyomo/solvers/tests/models/LP_duals_minimize.py +++ b/pyomo/solvers/tests/models/LP_duals_minimize.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_inactive_index.py b/pyomo/solvers/tests/models/LP_inactive_index.py index d3fdd5b32ca..5e2b570a1e8 100644 --- a/pyomo/solvers/tests/models/LP_inactive_index.py +++ b/pyomo/solvers/tests/models/LP_inactive_index.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_infeasible1.py b/pyomo/solvers/tests/models/LP_infeasible1.py index 28243574a37..8cba441a6c3 100644 --- a/pyomo/solvers/tests/models/LP_infeasible1.py +++ b/pyomo/solvers/tests/models/LP_infeasible1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_infeasible2.py b/pyomo/solvers/tests/models/LP_infeasible2.py index 383267c0e3c..7f417d9145c 100644 --- a/pyomo/solvers/tests/models/LP_infeasible2.py +++ b/pyomo/solvers/tests/models/LP_infeasible2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_piecewise.py b/pyomo/solvers/tests/models/LP_piecewise.py index f6350b38591..22ee9d08694 100644 --- a/pyomo/solvers/tests/models/LP_piecewise.py +++ b/pyomo/solvers/tests/models/LP_piecewise.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_simple.py b/pyomo/solvers/tests/models/LP_simple.py index 3449a657f79..4f1e6dcbc7e 100644 --- a/pyomo/solvers/tests/models/LP_simple.py +++ b/pyomo/solvers/tests/models/LP_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_trivial_constraints.py b/pyomo/solvers/tests/models/LP_trivial_constraints.py index 096c9e71712..3958f2b4493 100644 --- a/pyomo/solvers/tests/models/LP_trivial_constraints.py +++ b/pyomo/solvers/tests/models/LP_trivial_constraints.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_unbounded.py b/pyomo/solvers/tests/models/LP_unbounded.py index e3173e2ff07..e75977c40ba 100644 --- a/pyomo/solvers/tests/models/LP_unbounded.py +++ b/pyomo/solvers/tests/models/LP_unbounded.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_unique_duals.py b/pyomo/solvers/tests/models/LP_unique_duals.py index 624181eb27d..f5a4df6338d 100644 --- a/pyomo/solvers/tests/models/LP_unique_duals.py +++ b/pyomo/solvers/tests/models/LP_unique_duals.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/LP_unused_vars.py b/pyomo/solvers/tests/models/LP_unused_vars.py index 5e6b40fa4bf..0062fc58463 100644 --- a/pyomo/solvers/tests/models/LP_unused_vars.py +++ b/pyomo/solvers/tests/models/LP_unused_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/MILP_discrete_var_bounds.py b/pyomo/solvers/tests/models/MILP_discrete_var_bounds.py index 8fef69ef76a..22876a7a291 100644 --- a/pyomo/solvers/tests/models/MILP_discrete_var_bounds.py +++ b/pyomo/solvers/tests/models/MILP_discrete_var_bounds.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/MILP_infeasible1.py b/pyomo/solvers/tests/models/MILP_infeasible1.py index 2a0bf1bd188..e95fef92744 100644 --- a/pyomo/solvers/tests/models/MILP_infeasible1.py +++ b/pyomo/solvers/tests/models/MILP_infeasible1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/MILP_simple.py b/pyomo/solvers/tests/models/MILP_simple.py index fb157ea6555..488c7841024 100644 --- a/pyomo/solvers/tests/models/MILP_simple.py +++ b/pyomo/solvers/tests/models/MILP_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/MILP_unbounded.py b/pyomo/solvers/tests/models/MILP_unbounded.py index 364f3ffeb86..c5a166a6141 100644 --- a/pyomo/solvers/tests/models/MILP_unbounded.py +++ b/pyomo/solvers/tests/models/MILP_unbounded.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/MILP_unused_vars.py b/pyomo/solvers/tests/models/MILP_unused_vars.py index 742d0f951a8..b6e06c8db0c 100644 --- a/pyomo/solvers/tests/models/MILP_unused_vars.py +++ b/pyomo/solvers/tests/models/MILP_unused_vars.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/MIQCP_simple.py b/pyomo/solvers/tests/models/MIQCP_simple.py index 46c1293b23c..5946e83fadb 100644 --- a/pyomo/solvers/tests/models/MIQCP_simple.py +++ b/pyomo/solvers/tests/models/MIQCP_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/MIQP_simple.py b/pyomo/solvers/tests/models/MIQP_simple.py index 1d43d96ab8b..6922d6be97d 100644 --- a/pyomo/solvers/tests/models/MIQP_simple.py +++ b/pyomo/solvers/tests/models/MIQP_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/QCP_simple.py b/pyomo/solvers/tests/models/QCP_simple.py index 5f8405f1f00..5f4311a3ab9 100644 --- a/pyomo/solvers/tests/models/QCP_simple.py +++ b/pyomo/solvers/tests/models/QCP_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/QP_constant_objective.py b/pyomo/solvers/tests/models/QP_constant_objective.py index 2769fe07556..6ea34b69f51 100644 --- a/pyomo/solvers/tests/models/QP_constant_objective.py +++ b/pyomo/solvers/tests/models/QP_constant_objective.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/QP_simple.py b/pyomo/solvers/tests/models/QP_simple.py index 5959cf1d8b1..c5f4f40c576 100644 --- a/pyomo/solvers/tests/models/QP_simple.py +++ b/pyomo/solvers/tests/models/QP_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/SOS1_simple.py b/pyomo/solvers/tests/models/SOS1_simple.py index e6156ad5c32..ba3c89e680b 100644 --- a/pyomo/solvers/tests/models/SOS1_simple.py +++ b/pyomo/solvers/tests/models/SOS1_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/SOS2_simple.py b/pyomo/solvers/tests/models/SOS2_simple.py index 4f192773ca4..2062611f8cf 100644 --- a/pyomo/solvers/tests/models/SOS2_simple.py +++ b/pyomo/solvers/tests/models/SOS2_simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/__init__.py b/pyomo/solvers/tests/models/__init__.py index c6a550397d5..46a1c96936d 100644 --- a/pyomo/solvers/tests/models/__init__.py +++ b/pyomo/solvers/tests/models/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/models/base.py b/pyomo/solvers/tests/models/base.py index 106e8860145..25442611806 100644 --- a/pyomo/solvers/tests/models/base.py +++ b/pyomo/solvers/tests/models/base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/__init__.py b/pyomo/solvers/tests/piecewise_linear/__init__.py index bcaa157f6f4..79b33f0d427 100644 --- a/pyomo/solvers/tests/piecewise_linear/__init__.py +++ b/pyomo/solvers/tests/piecewise_linear/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/kernel_problems/concave_var.py b/pyomo/solvers/tests/piecewise_linear/kernel_problems/concave_var.py index 38c840f9ed9..45270d7dc34 100644 --- a/pyomo/solvers/tests/piecewise_linear/kernel_problems/concave_var.py +++ b/pyomo/solvers/tests/piecewise_linear/kernel_problems/concave_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/kernel_problems/convex_var.py b/pyomo/solvers/tests/piecewise_linear/kernel_problems/convex_var.py index 3aef735965e..cf28dc044eb 100644 --- a/pyomo/solvers/tests/piecewise_linear/kernel_problems/convex_var.py +++ b/pyomo/solvers/tests/piecewise_linear/kernel_problems/convex_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/kernel_problems/piecewise_var.py b/pyomo/solvers/tests/piecewise_linear/kernel_problems/piecewise_var.py index b77566e9d2d..cadbff305e8 100644 --- a/pyomo/solvers/tests/piecewise_linear/kernel_problems/piecewise_var.py +++ b/pyomo/solvers/tests/piecewise_linear/kernel_problems/piecewise_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/kernel_problems/step_var.py b/pyomo/solvers/tests/piecewise_linear/kernel_problems/step_var.py index 642181deb7d..1e6e418acf0 100644 --- a/pyomo/solvers/tests/piecewise_linear/kernel_problems/step_var.py +++ b/pyomo/solvers/tests/piecewise_linear/kernel_problems/step_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray1.py b/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray1.py index b24f7e1bd72..473b3328660 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray1.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray2.py b/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray2.py index 24c8beeba34..e6b57a4b652 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray2.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/concave_multi_vararray2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/concave_var.py b/pyomo/solvers/tests/piecewise_linear/problems/concave_var.py index 4eedf7bdeb9..b225cee4f87 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/concave_var.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/concave_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/concave_vararray.py b/pyomo/solvers/tests/piecewise_linear/problems/concave_vararray.py index be013b62309..727fc33ed80 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/concave_vararray.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/concave_vararray.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray1.py b/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray1.py index 8d00a99d49d..98f369b8c45 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray1.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray1.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray2.py b/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray2.py index 2892b759a65..c877bb6b72b 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray2.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/convex_multi_vararray2.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/convex_var.py b/pyomo/solvers/tests/piecewise_linear/problems/convex_var.py index bb4609be7c9..842ef50515b 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/convex_var.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/convex_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/convex_vararray.py b/pyomo/solvers/tests/piecewise_linear/problems/convex_vararray.py index 140d69dcb1a..087d0977ee0 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/convex_vararray.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/convex_vararray.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/piecewise_multi_vararray.py b/pyomo/solvers/tests/piecewise_linear/problems/piecewise_multi_vararray.py index 3c587d694e1..56452e0cd19 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/piecewise_multi_vararray.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/piecewise_multi_vararray.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/piecewise_var.py b/pyomo/solvers/tests/piecewise_linear/problems/piecewise_var.py index 5b18842f81d..60c45a69e80 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/piecewise_var.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/piecewise_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/piecewise_vararray.py b/pyomo/solvers/tests/piecewise_linear/problems/piecewise_vararray.py index d35c308e172..9e53edb0c93 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/piecewise_vararray.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/piecewise_vararray.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/step_var.py b/pyomo/solvers/tests/piecewise_linear/problems/step_var.py index a0c1062c9d6..59cefdd39c9 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/step_var.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/step_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/step_vararray.py b/pyomo/solvers/tests/piecewise_linear/problems/step_vararray.py index 749df3b6d7f..e4853e666d6 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/step_vararray.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/step_vararray.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/problems/tester.py b/pyomo/solvers/tests/piecewise_linear/problems/tester.py index 02e04f5052e..56261f7cc38 100644 --- a/pyomo/solvers/tests/piecewise_linear/problems/tester.py +++ b/pyomo/solvers/tests/piecewise_linear/problems/tester.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/test_examples.py b/pyomo/solvers/tests/piecewise_linear/test_examples.py index b151ffd2c0e..3454f62d56b 100644 --- a/pyomo/solvers/tests/piecewise_linear/test_examples.py +++ b/pyomo/solvers/tests/piecewise_linear/test_examples.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear.py b/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear.py index bfa206a987b..48472c2dabf 100644 --- a/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear.py +++ b/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear_kernel.py b/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear_kernel.py index 4137d9d3eed..20addb2b1eb 100644 --- a/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear_kernel.py +++ b/pyomo/solvers/tests/piecewise_linear/test_piecewise_linear_kernel.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/solvers.py b/pyomo/solvers/tests/solvers.py index 6bbfe08c7c7..e67df47a0b0 100644 --- a/pyomo/solvers/tests/solvers.py +++ b/pyomo/solvers/tests/solvers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/tests/testcases.py b/pyomo/solvers/tests/testcases.py index f5920ed6814..6bef40818d9 100644 --- a/pyomo/solvers/tests/testcases.py +++ b/pyomo/solvers/tests/testcases.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/solvers/wrappers.py b/pyomo/solvers/wrappers.py index 3b083f7a14f..ee167ce1cb0 100644 --- a/pyomo/solvers/wrappers.py +++ b/pyomo/solvers/wrappers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/__init__.py b/pyomo/util/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/util/__init__.py +++ b/pyomo/util/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/blockutil.py b/pyomo/util/blockutil.py index 52befea6ed5..56cc4266017 100644 --- a/pyomo/util/blockutil.py +++ b/pyomo/util/blockutil.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/calc_var_value.py b/pyomo/util/calc_var_value.py index 42d38f2f874..b5e620fea07 100644 --- a/pyomo/util/calc_var_value.py +++ b/pyomo/util/calc_var_value.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/check_units.py b/pyomo/util/check_units.py index be72493af3f..6f95486c8cd 100644 --- a/pyomo/util/check_units.py +++ b/pyomo/util/check_units.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/components.py b/pyomo/util/components.py index 02ef8a30f64..2f1d85a4934 100644 --- a/pyomo/util/components.py +++ b/pyomo/util/components.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/diagnostics.py b/pyomo/util/diagnostics.py index 8bad078ad64..709a483f2ff 100644 --- a/pyomo/util/diagnostics.py +++ b/pyomo/util/diagnostics.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/infeasible.py b/pyomo/util/infeasible.py index 9c8196d1ff4..961d5b35036 100644 --- a/pyomo/util/infeasible.py +++ b/pyomo/util/infeasible.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/model_size.py b/pyomo/util/model_size.py index 9575e327a74..1fdac357368 100644 --- a/pyomo/util/model_size.py +++ b/pyomo/util/model_size.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/report_scaling.py b/pyomo/util/report_scaling.py index 5b4a4df7c84..201319ea92a 100644 --- a/pyomo/util/report_scaling.py +++ b/pyomo/util/report_scaling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/slices.py b/pyomo/util/slices.py index 0449acb3f2f..53f6d364219 100644 --- a/pyomo/util/slices.py +++ b/pyomo/util/slices.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 673781def17..70a0af1b2a7 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/__init__.py b/pyomo/util/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/util/tests/__init__.py +++ b/pyomo/util/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_blockutil.py b/pyomo/util/tests/test_blockutil.py index 06b75bd6b68..dfe4f482fb2 100644 --- a/pyomo/util/tests/test_blockutil.py +++ b/pyomo/util/tests/test_blockutil.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_calc_var_value.py b/pyomo/util/tests/test_calc_var_value.py index 91f23dd5a5d..a02d7a7d838 100644 --- a/pyomo/util/tests/test_calc_var_value.py +++ b/pyomo/util/tests/test_calc_var_value.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_check_units.py b/pyomo/util/tests/test_check_units.py index d2fb35c4f3b..9cde8d8dbae 100644 --- a/pyomo/util/tests/test_check_units.py +++ b/pyomo/util/tests/test_check_units.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_components.py b/pyomo/util/tests/test_components.py index 92eb7dd5ef1..1027815ca6b 100644 --- a/pyomo/util/tests/test_components.py +++ b/pyomo/util/tests/test_components.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_infeasible.py b/pyomo/util/tests/test_infeasible.py index cefc129b41e..687a578e5c8 100644 --- a/pyomo/util/tests/test_infeasible.py +++ b/pyomo/util/tests/test_infeasible.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_model_size.py b/pyomo/util/tests/test_model_size.py index 417ff7526e8..2380d272a24 100644 --- a/pyomo/util/tests/test_model_size.py +++ b/pyomo/util/tests/test_model_size.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_report_scaling.py b/pyomo/util/tests/test_report_scaling.py index b010065d697..2eaed2d0ade 100644 --- a/pyomo/util/tests/test_report_scaling.py +++ b/pyomo/util/tests/test_report_scaling.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_slices.py b/pyomo/util/tests/test_slices.py index db66a74b468..992bdc0a332 100644 --- a/pyomo/util/tests/test_slices.py +++ b/pyomo/util/tests/test_slices.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/tests/test_subsystems.py b/pyomo/util/tests/test_subsystems.py index a081b51cee9..87a4fb3cf28 100644 --- a/pyomo/util/tests/test_subsystems.py +++ b/pyomo/util/tests/test_subsystems.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/util/vars_from_expressions.py b/pyomo/util/vars_from_expressions.py index 8866ba980bd..f9b3f1ab8ae 100644 --- a/pyomo/util/vars_from_expressions.py +++ b/pyomo/util/vars_from_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/version/__init__.py b/pyomo/version/__init__.py index 08bcde304a6..acc92ff6b37 100644 --- a/pyomo/version/__init__.py +++ b/pyomo/version/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/version/info.py b/pyomo/version/info.py index cedb30c2dd4..0db00ac240f 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/version/tests/__init__.py b/pyomo/version/tests/__init__.py index 9fb4f531a5b..f013ccd3fa3 100644 --- a/pyomo/version/tests/__init__.py +++ b/pyomo/version/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/version/tests/check.py b/pyomo/version/tests/check.py index ab3b45ffc6c..0fca9badb2f 100644 --- a/pyomo/version/tests/check.py +++ b/pyomo/version/tests/check.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/version/tests/test_version.py b/pyomo/version/tests/test_version.py index 253ee53137c..3b39bd71cb1 100644 --- a/pyomo/version/tests/test_version.py +++ b/pyomo/version/tests/test_version.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/scripts/admin/contributors.py b/scripts/admin/contributors.py index fe5d483f16d..ffc02059d6f 100644 --- a/scripts/admin/contributors.py +++ b/scripts/admin/contributors.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/scripts/get_pyomo.py b/scripts/get_pyomo.py index a97c0ba3a00..d90773f2315 100644 --- a/scripts/get_pyomo.py +++ b/scripts/get_pyomo.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/scripts/get_pyomo_extras.py b/scripts/get_pyomo_extras.py index d2aa097154a..6688f3c6dc4 100644 --- a/scripts/get_pyomo_extras.py +++ b/scripts/get_pyomo_extras.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/scripts/performance/compare.py b/scripts/performance/compare.py index 5edef9bfadd..e62440fd6d9 100755 --- a/scripts/performance/compare.py +++ b/scripts/performance/compare.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/scripts/performance/compare_components.py b/scripts/performance/compare_components.py index 1edaa73003b..764b50217ef 100644 --- a/scripts/performance/compare_components.py +++ b/scripts/performance/compare_components.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/scripts/performance/expr_perf.py b/scripts/performance/expr_perf.py index 6f0d246e1f3..9abdd560887 100644 --- a/scripts/performance/expr_perf.py +++ b/scripts/performance/expr_perf.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/scripts/performance/main.py b/scripts/performance/main.py index 10349c0eb73..07dc38a11a7 100755 --- a/scripts/performance/main.py +++ b/scripts/performance/main.py @@ -2,7 +2,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/scripts/performance/simple.py b/scripts/performance/simple.py index bd5ffd99368..c5fb836b64b 100644 --- a/scripts/performance/simple.py +++ b/scripts/performance/simple.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/setup.py b/setup.py index e2d702db010..0bbcb6a8390 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain From 6cfbd21615ce8b82f15a0545e187d8a4d2c5e89a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 14:12:31 -0700 Subject: [PATCH 0564/3044] Add documentation (and fix an import) --- doc/OnlineDocs/developer_reference/index.rst | 1 + pyomo/__future__.py | 46 +++++++++++++++++++- pyomo/contrib/solver/factory.py | 2 +- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/index.rst b/doc/OnlineDocs/developer_reference/index.rst index 0f0f636abee..0feb33cdab9 100644 --- a/doc/OnlineDocs/developer_reference/index.rst +++ b/doc/OnlineDocs/developer_reference/index.rst @@ -12,4 +12,5 @@ scripts using Pyomo. config.rst deprecation.rst expressions/index.rst + future.rst solvers.rst diff --git a/pyomo/__future__.py b/pyomo/__future__.py index 7028265b2ad..c614bf6cc04 100644 --- a/pyomo/__future__.py +++ b/pyomo/__future__.py @@ -11,6 +11,25 @@ import pyomo.environ as _environ +__doc__ = """ +Preview capabilities through `pyomo.__future__` +=============================================== + +This module provides a uniform interface for gaining access to future +("preview") capabilities that are either slightly incompatible with the +current official offering, or are still under development with the +intent to replace the current offering. + +Currently supported `__future__` offerings include: + +.. autosummary:: + + solver_factory + +.. autofunction:: solver_factory + +""" + def __getattr__(name): if name in ('solver_factory_v1', 'solver_factory_v2', 'solver_factory_v3'): @@ -23,15 +42,39 @@ def solver_factory(version=None): This allows users to query / set the current implementation of the SolverFactory that should be used throughout Pyomo. Valid options are: - + 1: the original Pyomo SolverFactor 2: the SolverFactory from APPSI 3: the SolverFactory from pyomo.contrib.solver + The current active version can be obtained by calling the method + with no arguments + + .. doctest:: + + >>> from pyomo.__future__ import solver_factory + >>> solver_factory() + 1 + + The active factory can be set either by passing the appropriate + version to this function: + + .. doctest:: + + >>> solver_factory(3) + + + or by importing the "special" name: + + .. doctest:: + + >>> from pyomo.__future__ import solver_factory_v3 + """ import pyomo.opt.base.solvers as _solvers import pyomo.contrib.solver.factory as _contrib import pyomo.contrib.appsi.base as _appsi + versions = { 1: _solvers.LegacySolverFactory, 2: _appsi.SolverFactory, @@ -66,4 +109,5 @@ def solver_factory(version=None): ) return src + solver_factory._active_version = solver_factory() diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index 73666ff57e4..52fd9e51236 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ -from pyomo.opt.base import LegacySolverFactory +from pyomo.opt.base.solvers import LegacySolverFactory from pyomo.common.factory import Factory from pyomo.contrib.solver.base import LegacySolverWrapper From 2c471e43e4e2194a4cdeb9c1b7f7870ee7c19a59 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 15 Feb 2024 16:39:22 -0500 Subject: [PATCH 0565/3044] Simplify argument resolution --- pyomo/contrib/pyros/config.py | 91 ------------------------ pyomo/contrib/pyros/pyros.py | 23 +++--- pyomo/contrib/pyros/tests/test_config.py | 66 ----------------- pyomo/contrib/pyros/tests/test_grcs.py | 56 +++++---------- 4 files changed, 24 insertions(+), 212 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 798e68b157f..749152f234c 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -950,94 +950,3 @@ def pyros_config(): ) return CONFIG - - -def resolve_keyword_arguments(prioritized_kwargs_dicts, func=None): - """ - Resolve the keyword arguments to a callable in the event - the arguments may have been passed in one or more possible - ways. - - A warning-level message is logged (through the default PyROS - logger) in the event an argument is specified in more than one - way. In this case, the value provided through the means with - the highest priority is selected. - - Parameters - ---------- - prioritized_kwargs_dicts : dict - Each entry maps a str to a dict of the keyword arguments - passed via the means described by the str. - Entries of `prioritized_kwargs_dicts` are taken to be - provided in descending order of priority of the means - by which the arguments may have been passed to the callable. - func : callable or None, optional - Callable to which the keyword arguments are/were passed. - Currently, only the `__name__` attribute is used, - for the purpose of logging warning-level messages. - If `None` is passed, then the warning messages - logged are slightly less informative. - - Returns - ------- - resolved_kwargs : dict - Resolved keyword arguments. - """ - # warnings are issued through logger object - default_logger = default_pyros_solver_logger - - # used for warning messages - func_desc = f"passed to {func.__name__}()" if func is not None else "passed" - - # we will loop through the priority dict. initialize: - # - resolved keyword arguments, taking into account the - # priority order and overlap - # - kwarg dicts already processed - # - sequence of kwarg dicts yet to be processed - resolved_kwargs = dict() - prev_prioritized_kwargs_dicts = dict() - remaining_kwargs_dicts = prioritized_kwargs_dicts.copy() - for curr_desc, curr_kwargs in remaining_kwargs_dicts.items(): - overlapping_args = dict() - overlapping_args_set = set() - - for prev_desc, prev_kwargs in prev_prioritized_kwargs_dicts.items(): - # determine overlap between current and previous - # set of kwargs, and remove overlap of current - # and higher priority sets from the result - curr_prev_overlapping_args = ( - set(curr_kwargs.keys()) & set(prev_kwargs.keys()) - ) - overlapping_args_set - if curr_prev_overlapping_args: - # if there is overlap, prepare overlapping args - # for when warning is to be issued - overlapping_args[prev_desc] = curr_prev_overlapping_args - - # update set of args overlapping with higher priority dicts - overlapping_args_set |= curr_prev_overlapping_args - - # ensure kwargs specified in higher priority - # dicts are not overwritten in resolved kwargs - resolved_kwargs.update( - { - kw: val - for kw, val in curr_kwargs.items() - if kw not in overlapping_args_set - } - ) - - # if there are overlaps, log warnings accordingly - # per priority level - for overlap_desc, args_set in overlapping_args.items(): - new_overlapping_args_str = ", ".join(f"{arg!r}" for arg in args_set) - default_logger.warning( - f"Arguments [{new_overlapping_args_str}] passed {curr_desc} " - f"already {func_desc} {overlap_desc}, " - "and will not be overwritten. " - "Consider modifying your arguments to remove the overlap." - ) - - # increment sequence of kwarg dicts already processed - prev_prioritized_kwargs_dicts[curr_desc] = curr_kwargs - - return resolved_kwargs diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 0659ab43a64..314b0c3eac4 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -20,7 +20,7 @@ from pyomo.contrib.pyros.util import time_code from pyomo.common.modeling import unique_component_name from pyomo.opt import SolverFactory -from pyomo.contrib.pyros.config import pyros_config, resolve_keyword_arguments +from pyomo.contrib.pyros.config import pyros_config from pyomo.contrib.pyros.util import ( recast_to_min_obj, add_decision_rule_constraints, @@ -267,22 +267,15 @@ def _resolve_and_validate_pyros_args(self, model, **kwds): ---- This method can be broken down into three steps: - 1. Resolve user arguments based on how they were passed - and order of precedence of the various means by which - they could be passed. - 2. Cast resolved arguments to ConfigDict. Argument-wise + 1. Cast arguments to ConfigDict. Argument-wise validation is performed automatically. - 3. Inter-argument validation. + Note that arguments specified directly take + precedence over arguments specified indirectly + through direct argument 'options'. + 2. Inter-argument validation. """ - options_dict = kwds.pop("options", {}) - resolved_kwds = resolve_keyword_arguments( - prioritized_kwargs_dicts={ - "explicitly": kwds, - "implicitly through argument 'options'": options_dict, - }, - func=self.solve, - ) - config = self.CONFIG(resolved_kwds) + config = self.CONFIG(kwds.pop("options", {})) + config = config(kwds) state_vars = validate_pyros_inputs(model, config) return config, state_vars diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index eaed462a9b3..cc6fde225f3 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -19,7 +19,6 @@ PathLikeOrNone, PositiveIntOrMinusOne, pyros_config, - resolve_keyword_arguments, SolverIterable, SolverResolvable, UncertaintySetDomain, @@ -723,70 +722,5 @@ def test_logger_type(self): standardizer_func(2) -class TestResolveKeywordArguments(unittest.TestCase): - """ - Test keyword argument resolution function works as expected. - """ - - def test_resolve_kwargs_simple_dict(self): - """ - Test resolve kwargs works, simple example - where there is overlap. - """ - explicit_kwargs = dict(arg1=1) - implicit_kwargs_1 = dict(arg1=2, arg2=3) - implicit_kwargs_2 = dict(arg1=4, arg2=4, arg3=5) - - # expected answer - expected_resolved_kwargs = dict(arg1=1, arg2=3, arg3=5) - - # attempt kwargs resolve - with LoggingIntercept(level=logging.WARNING) as LOG: - resolved_kwargs = resolve_keyword_arguments( - prioritized_kwargs_dicts={ - "explicitly": explicit_kwargs, - "implicitly through set 1": implicit_kwargs_1, - "implicitly through set 2": implicit_kwargs_2, - } - ) - - # check kwargs resolved as expected - self.assertEqual( - resolved_kwargs, - expected_resolved_kwargs, - msg="Resolved kwargs do not match expected value.", - ) - - # extract logger warning messages - warning_msgs = LOG.getvalue().split("\n")[:-1] - - self.assertEqual( - len(warning_msgs), 3, msg="Number of warning messages is not as expected." - ) - - # check contents of warning msgs - self.assertRegex( - warning_msgs[0], - expected_regex=( - r"Arguments \['arg1'\] passed implicitly through set 1 " - r"already passed explicitly.*" - ), - ) - self.assertRegex( - warning_msgs[1], - expected_regex=( - r"Arguments \['arg1'\] passed implicitly through set 2 " - r"already passed explicitly.*" - ), - ) - self.assertRegex( - warning_msgs[2], - expected_regex=( - r"Arguments \['arg2'\] passed implicitly through set 2 " - r"already passed implicitly through set 1.*" - ), - ) - - if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index a94b4d9d408..59045f3c6b7 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -6409,46 +6409,22 @@ def test_pyros_kwargs_with_overlap(self): global_subsolver = SolverFactory("baron") # Call the PyROS solver - with LoggingIntercept(level=logging.WARNING) as LOG: - results = pyros_solver.solve( - model=m, - first_stage_variables=[m.x1, m.x2], - second_stage_variables=[], - uncertain_params=[m.u1, m.u2], - uncertainty_set=ellipsoid, - local_solver=local_subsolver, - global_solver=global_subsolver, - bypass_local_separation=True, - solve_master_globally=True, - options={ - "objective_focus": ObjectiveType.worst_case, - "solve_master_globally": False, - "max_iter": 1, - "time_limit": 1000, - }, - ) - - # extract warning-level messages. - warning_msgs = LOG.getvalue().split("\n")[:-1] - resolve_kwargs_warning_msgs = [ - msg - for msg in warning_msgs - if msg.startswith("Arguments [") - and "Consider modifying your arguments" in msg - ] - self.assertEqual( - len(resolve_kwargs_warning_msgs), - 1, - msg="Number of warning-level messages not as expected.", - ) - - self.assertRegex( - resolve_kwargs_warning_msgs[0], - expected_regex=( - r"Arguments \['solve_master_globally'\] passed " - r"implicitly through argument 'options' " - r"already passed .*explicitly.*" - ), + results = pyros_solver.solve( + model=m, + first_stage_variables=[m.x1, m.x2], + second_stage_variables=[], + uncertain_params=[m.u1, m.u2], + uncertainty_set=ellipsoid, + local_solver=local_subsolver, + global_solver=global_subsolver, + bypass_local_separation=True, + solve_master_globally=True, + options={ + "objective_focus": ObjectiveType.worst_case, + "solve_master_globally": False, + "max_iter": 1, + "time_limit": 1000, + }, ) # check termination status as expected From d2a3ff14e9d70d118a18a5dca4c728185e8e0a24 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 15:06:02 -0700 Subject: [PATCH 0566/3044] Update documentation; include package needed for sphinx enum tools --- doc/OnlineDocs/developer_reference/solvers.rst | 3 +-- pyomo/contrib/solver/base.py | 13 +++++++++++-- pyomo/contrib/solver/config.py | 2 +- setup.py | 1 + 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 78344293e39..581d899af50 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -75,7 +75,6 @@ Future Capability Mode model.pprint() - Interface Implementation ------------------------ @@ -88,7 +87,7 @@ All solvers should have the following: .. autoclass:: pyomo.contrib.solver.base.SolverBase :members: -Persistent solvers should also include: +Persistent solvers include additional members as well as other configuration options: .. autoclass:: pyomo.contrib.solver.base.PersistentSolverBase :show-inheritance: diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 1cd9db2baa9..327ad2e01ca 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -19,7 +19,7 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.common.timing import HierarchicalTimer +from pyomo.common.config import document_kwargs_from_configdict from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning from pyomo.opt.results.results_ import SolverResults as LegacySolverResults @@ -28,7 +28,7 @@ from pyomo.core.base import SymbolMap from pyomo.core.base.label import NumericLabeler from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.solver.config import SolverConfig +from pyomo.contrib.solver.config import SolverConfig, PersistentSolverConfig from pyomo.contrib.solver.util import get_objective from pyomo.contrib.solver.results import ( Results, @@ -104,6 +104,7 @@ def __str__(self): # preserve the previous behavior return self.name + @document_kwargs_from_configdict(CONFIG) @abc.abstractmethod def solve(self, model: _BlockData, **kwargs) -> Results: """ @@ -176,6 +177,14 @@ class PersistentSolverBase(SolverBase): Example usage can be seen in the Gurobi interface. """ + CONFIG = PersistentSolverConfig() + + def __init__(self, kwds): + super().__init__(kwds) + + @document_kwargs_from_configdict(CONFIG) + def solve(self, model: _BlockData, **kwargs) -> Results: + super().solve(model, kwargs) def is_persistent(self): """ diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index a1133f93ae4..335307c1bbf 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -64,7 +64,7 @@ def __init__( domain=str, default=None, description="The directory in which generated files should be saved. " - "This replaced the `keepfiles` option.", + "This replaces the `keepfiles` option.", ), ) self.load_solutions: bool = self.declare( diff --git a/setup.py b/setup.py index e2d702db010..27d169af746 100644 --- a/setup.py +++ b/setup.py @@ -253,6 +253,7 @@ def __ne__(self, other): 'sphinx_rtd_theme>0.5', 'sphinxcontrib-jsmath', 'sphinxcontrib-napoleon', + 'enum-tools[sphinx]', 'numpy', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero ], From 75da70e77d245866cf865ae7e8fd997769441566 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 15:16:13 -0700 Subject: [PATCH 0567/3044] Fix init; add more descriptive skip messages --- pyomo/contrib/solver/base.py | 5 +- .../solver/tests/solvers/test_solvers.py | 76 +++++++++---------- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 327ad2e01ca..bc4ab725a81 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -179,10 +179,11 @@ class PersistentSolverBase(SolverBase): """ CONFIG = PersistentSolverConfig() - def __init__(self, kwds): - super().__init__(kwds) + def __init__(self, **kwds): + super().__init__(**kwds) @document_kwargs_from_configdict(CONFIG) + @abc.abstractmethod def solve(self, model: _BlockData, **kwargs) -> Results: super().solve(model, kwargs) diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 36f3596e890..0393d1adb2e 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -55,7 +55,7 @@ def test_remove_variable_and_objective( # this test is for issue #2888 opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) @@ -75,7 +75,7 @@ def test_remove_variable_and_objective( def test_stale_vars(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -116,7 +116,7 @@ def test_stale_vars(self, name: str, opt_class: Type[SolverBase]): def test_range_constraint(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.obj = pe.Objective(expr=m.x) @@ -137,7 +137,7 @@ def test_range_constraint(self, name: str, opt_class: Type[SolverBase]): def test_reduced_costs(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, 1)) m.y = pe.Var(bounds=(-2, 2)) @@ -159,7 +159,7 @@ def test_reduced_costs(self, name: str, opt_class: Type[SolverBase]): def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, 1)) m.obj = pe.Objective(expr=m.x) @@ -179,7 +179,7 @@ def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase]): def test_param_changes(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -219,7 +219,7 @@ def test_immutable_param(self, name: str, opt_class: Type[SolverBase]): """ opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -255,7 +255,7 @@ def test_immutable_param(self, name: str, opt_class: Type[SolverBase]): def test_equality(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') if isinstance(opt, ipopt): opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() @@ -293,7 +293,7 @@ def test_equality(self, name: str, opt_class: Type[SolverBase]): def test_linear_expression(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -331,7 +331,7 @@ def test_linear_expression(self, name: str, opt_class: Type[SolverBase]): def test_no_objective(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -362,7 +362,7 @@ def test_no_objective(self, name: str, opt_class: Type[SolverBase]): def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -416,7 +416,7 @@ def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase]): def test_results_infeasible(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -465,7 +465,7 @@ def test_results_infeasible(self, name: str, opt_class: Type[SolverBase]): def test_duals(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -490,7 +490,7 @@ def test_mutable_quadratic_coefficient( ): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -512,7 +512,7 @@ def test_mutable_quadratic_coefficient( def test_mutable_quadratic_objective(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -542,7 +542,7 @@ def test_fixed_vars(self, name: str, opt_class: Type[SolverBase]): treat_fixed_vars_as_params ) if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.x.fix(0) @@ -580,7 +580,7 @@ def test_fixed_vars_2(self, name: str, opt_class: Type[SolverBase]): if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.x.fix(0) @@ -618,7 +618,7 @@ def test_fixed_vars_3(self, name: str, opt_class: Type[SolverBase]): if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -634,7 +634,7 @@ def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase]): if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -652,7 +652,7 @@ def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase]): def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') try: import numpy as np except: @@ -746,7 +746,7 @@ def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase]): def test_add_and_remove_vars(self, name: str, opt_class: Type[SolverBase]): opt = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.y = pe.Var(bounds=(-1, None)) m.obj = pe.Objective(expr=m.y) @@ -792,7 +792,7 @@ def test_add_and_remove_vars(self, name: str, opt_class: Type[SolverBase]): def test_exp(self, name: str, opt_class: Type[SolverBase]): opt = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -806,7 +806,7 @@ def test_exp(self, name: str, opt_class: Type[SolverBase]): def test_log(self, name: str, opt_class: Type[SolverBase]): opt = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(initialize=1) m.y = pe.Var() @@ -820,7 +820,7 @@ def test_log(self, name: str, opt_class: Type[SolverBase]): def test_with_numpy(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -848,7 +848,7 @@ def test_with_numpy(self, name: str, opt_class: Type[SolverBase]): def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.y = pe.Var() m.p = pe.Param(mutable=True) @@ -880,7 +880,7 @@ def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase]): def test_solution_loader(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(1, None)) m.y = pe.Var() @@ -930,7 +930,7 @@ def test_solution_loader(self, name: str, opt_class: Type[SolverBase]): def test_time_limit(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') from sys import platform if platform == 'win32': @@ -986,7 +986,7 @@ def test_time_limit(self, name: str, opt_class: Type[SolverBase]): def test_objective_changes(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -1050,7 +1050,7 @@ def test_objective_changes(self, name: str, opt_class: Type[SolverBase]): def test_domain(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(1, None), domain=pe.NonNegativeReals) m.obj = pe.Objective(expr=m.x) @@ -1074,7 +1074,7 @@ def test_domain(self, name: str, opt_class: Type[SolverBase]): def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, None), domain=pe.NonNegativeIntegers) m.obj = pe.Objective(expr=m.x) @@ -1098,7 +1098,7 @@ def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase]): def test_fixed_binaries(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(domain=pe.Binary) m.y = pe.Var() @@ -1125,7 +1125,7 @@ def test_fixed_binaries(self, name: str, opt_class: Type[SolverBase]): def test_with_gdp(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(-10, 10)) @@ -1156,7 +1156,7 @@ def test_with_gdp(self, name: str, opt_class: Type[SolverBase]): def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() @@ -1183,7 +1183,7 @@ def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase]): def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() @@ -1218,7 +1218,7 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase]): def test_bug_1(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var(bounds=(3, 7)) @@ -1246,7 +1246,7 @@ def test_bug_2(self, name: str, opt_class: Type[SolverBase]): for fixed_var_option in [True, False]: opt: SolverBase = opt_class() if not opt.available(): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = fixed_var_option @@ -1272,7 +1272,7 @@ class TestLegacySolverInterface(unittest.TestCase): def test_param_updates(self, name: str, opt_class: Type[SolverBase]): opt = pe.SolverFactory(name + '_v2') if not opt.available(exception_flag=False): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -1302,7 +1302,7 @@ def test_param_updates(self, name: str, opt_class: Type[SolverBase]): def test_load_solutions(self, name: str, opt_class: Type[SolverBase]): opt = pe.SolverFactory(name + '_v2') if not opt.available(exception_flag=False): - raise unittest.SkipTest + raise unittest.SkipTest(f'Solver {opt.name} not available.') m = pe.ConcreteModel() m.x = pe.Var() m.obj = pe.Objective(expr=m.x) From e7eb1423272e53e984d7ae3ea8541eded92b56e7 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 15:16:36 -0700 Subject: [PATCH 0568/3044] Apply blacl --- pyomo/contrib/solver/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index bc4ab725a81..09c73ab3a9b 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -177,6 +177,7 @@ class PersistentSolverBase(SolverBase): Example usage can be seen in the Gurobi interface. """ + CONFIG = PersistentSolverConfig() def __init__(self, **kwds): From bf4f27018d4948cefc0f44f4e3c39d5ff3848eca Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 15:35:36 -0700 Subject: [PATCH 0569/3044] Small typo; changes enum-tools line --- pyomo/contrib/solver/config.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 335307c1bbf..d36c7102620 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -89,7 +89,7 @@ def __init__( ConfigValue( domain=bool, default=False, - description="If True, the names given to the solver will reflect the names of the Pyomo components." + description="If True, the names given to the solver will reflect the names of the Pyomo components. " "Cannot be changed after set_instance is called.", ), ) diff --git a/setup.py b/setup.py index 27d169af746..1572910ad89 100644 --- a/setup.py +++ b/setup.py @@ -253,7 +253,7 @@ def __ne__(self, other): 'sphinx_rtd_theme>0.5', 'sphinxcontrib-jsmath', 'sphinxcontrib-napoleon', - 'enum-tools[sphinx]', + 'enum-tools', 'numpy', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero ], From eb9b2532cd4045ca0b674a6c5b73503258731b9a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 15:49:19 -0700 Subject: [PATCH 0570/3044] Underscore instead of dash --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1572910ad89..1f0d56c10a7 100644 --- a/setup.py +++ b/setup.py @@ -253,7 +253,7 @@ def __ne__(self, other): 'sphinx_rtd_theme>0.5', 'sphinxcontrib-jsmath', 'sphinxcontrib-napoleon', - 'enum-tools', + 'enum_tools', 'numpy', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero ], From f313b0a49c1d8bbb92244029487c8ce57d5e3977 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 15:51:09 -0700 Subject: [PATCH 0571/3044] NFC: apply black --- pyomo/contrib/appsi/plugins.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index cec95337a9b..a765f9a45de 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -9,15 +9,13 @@ def load(): SolverFactory.register( name='gurobi', doc='Automated persistent interface to Gurobi' )(Gurobi) - SolverFactory.register( - name='cplex', doc='Automated persistent interface to Cplex' - )(Cplex) - SolverFactory.register( - name='ipopt', doc='Automated persistent interface to Ipopt' - )(Ipopt) - SolverFactory.register( - name='cbc', doc='Automated persistent interface to Cbc' - )(Cbc) - SolverFactory.register( - name='highs', doc='Automated persistent interface to Highs' - )(Highs) + SolverFactory.register(name='cplex', doc='Automated persistent interface to Cplex')( + Cplex + ) + SolverFactory.register(name='ipopt', doc='Automated persistent interface to Ipopt')( + Ipopt + ) + SolverFactory.register(name='cbc', doc='Automated persistent interface to Cbc')(Cbc) + SolverFactory.register(name='highs', doc='Automated persistent interface to Highs')( + Highs + ) From 74971722ca3bd9a8e1aa3d2d8549373a77a0ba6f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 15:58:03 -0700 Subject: [PATCH 0572/3044] NFC: update copyright on new file (missed by #3139) --- pyomo/__future__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/__future__.py b/pyomo/__future__.py index c614bf6cc04..235143592f1 100644 --- a/pyomo/__future__.py +++ b/pyomo/__future__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain From cfbab706bd472a37f70830d3a3f8371f1be1ada0 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 15:59:22 -0700 Subject: [PATCH 0573/3044] Add in two more deps --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 1f0d56c10a7..dbb5a68ceb2 100644 --- a/setup.py +++ b/setup.py @@ -253,6 +253,8 @@ def __ne__(self, other): 'sphinx_rtd_theme>0.5', 'sphinxcontrib-jsmath', 'sphinxcontrib-napoleon', + 'sphinx-toolbox>=2.16.0', + 'sphinx-jinja2-compat>=0.1.1', 'enum_tools', 'numpy', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero From 0330e095f681d368a0bb50213bf4347575248b6b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 16:06:03 -0700 Subject: [PATCH 0574/3044] NFC: fix doc formatting --- pyomo/__future__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/__future__.py b/pyomo/__future__.py index 235143592f1..0dc22cca0a7 100644 --- a/pyomo/__future__.py +++ b/pyomo/__future__.py @@ -43,9 +43,9 @@ def solver_factory(version=None): This allows users to query / set the current implementation of the SolverFactory that should be used throughout Pyomo. Valid options are: - 1: the original Pyomo SolverFactor - 2: the SolverFactory from APPSI - 3: the SolverFactory from pyomo.contrib.solver + - ``1``: the original Pyomo SolverFactor + - ``2``: the SolverFactory from APPSI + - ``3``: the SolverFactory from pyomo.contrib.solver The current active version can be obtained by calling the method with no arguments From b3c4b66bf0b78aa8a69d7bb30f9c7991dee1f147 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 16:07:55 -0700 Subject: [PATCH 0575/3044] Add missing doc file --- doc/OnlineDocs/developer_reference/future.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 doc/OnlineDocs/developer_reference/future.rst diff --git a/doc/OnlineDocs/developer_reference/future.rst b/doc/OnlineDocs/developer_reference/future.rst new file mode 100644 index 00000000000..531c0fdb5c6 --- /dev/null +++ b/doc/OnlineDocs/developer_reference/future.rst @@ -0,0 +1,3 @@ + +.. automodule:: pyomo.__future__ + :noindex: From f45201a3209a52979f43168c2af5ddaa36bf3ab6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 15 Feb 2024 16:15:41 -0700 Subject: [PATCH 0576/3044] NFC: additional doc formatting --- pyomo/__future__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/__future__.py b/pyomo/__future__.py index 0dc22cca0a7..a2e08ccf291 100644 --- a/pyomo/__future__.py +++ b/pyomo/__future__.py @@ -12,15 +12,15 @@ import pyomo.environ as _environ __doc__ = """ -Preview capabilities through `pyomo.__future__` -=============================================== +Preview capabilities through ``pyomo.__future__`` +================================================= This module provides a uniform interface for gaining access to future ("preview") capabilities that are either slightly incompatible with the current official offering, or are still under development with the intent to replace the current offering. -Currently supported `__future__` offerings include: +Currently supported ``__future__`` offerings include: .. autosummary:: From 7965ac52b8b88bf14a0f8bac32b2f6c20d6eabce Mon Sep 17 00:00:00 2001 From: kaklise Date: Thu, 15 Feb 2024 15:27:31 -0800 Subject: [PATCH 0577/3044] removed group_data function --- pyomo/contrib/parmest/parmest.py | 39 -------------------------------- 1 file changed, 39 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 2e44b278423..83b24e39327 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -16,7 +16,6 @@ # TODO: move use_mpisppy to a Pyomo configuration option # Redesign TODOS -# TODO: remove group_data,this is only used in 1 example and should be handled by the user in Experiment # TODO: _treemaker is not used in parmest, the code could be moved to scenario tree if needed # TODO: Create additional built in objective expressions in an Enum class which includes SSE (see SSE function below) # TODO: Clean up the use of theta_names through out the code. The Experiment returns the CUID of each theta and this can be used directly (instead of the name) @@ -272,44 +271,6 @@ def _experiment_instance_creation_callback( # return m - -# def group_data(data, groupby_column_name, use_mean=None): -# """ -# Group data by scenario - -# Parameters -# ---------- -# data: DataFrame -# Data -# groupby_column_name: strings -# Name of data column which contains scenario numbers -# use_mean: list of column names or None, optional -# Name of data columns which should be reduced to a single value per -# scenario by taking the mean - -# Returns -# ---------- -# grouped_data: list of dictionaries -# Grouped data -# """ -# if use_mean is None: -# use_mean_list = [] -# else: -# use_mean_list = use_mean - -# grouped_data = [] -# for exp_num, group in data.groupby(data[groupby_column_name]): -# d = {} -# for col in group.columns: -# if col in use_mean_list: -# d[col] = group[col].mean() -# else: -# d[col] = list(group[col]) -# grouped_data.append(d) - -# return grouped_data - - def SSE(model): expr = sum((y - yhat) ** 2 for y, yhat in model.experiment_outputs.items()) return expr From 9157b8c048a26aef8b3637f979a16d01c3768cf1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 16:29:59 -0700 Subject: [PATCH 0578/3044] Update documentation to reflect the new preview page --- .../developer_reference/solvers.rst | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 581d899af50..db9fe307b18 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -13,20 +13,23 @@ New Interface Usage ------------------- The new interfaces have two modes: backwards compatible and future capability. -To use the backwards compatible version, simply use the ``SolverFactory`` -as usual and replace the solver name with the new version. Currently, the new -versions available are: +The future capability mode can be accessed directly or by switching the default +``SolverFactory`` version (see :doc:`future`). Currently, the new versions +available are: .. list-table:: Available Redesigned Solvers - :widths: 25 25 + :widths: 25 25 25 :header-rows: 1 * - Solver - - ``SolverFactory`` Name + - ``SolverFactory``([1]) Name + - ``SolverFactory``([3]) Name * - ipopt - ``ipopt_v2`` - * - GUROBI + - ``ipopt`` + * - Gurobi - ``gurobi_v2`` + - ``gurobi`` Backwards Compatible Mode ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -52,8 +55,12 @@ Backwards Compatible Mode Future Capability Mode ^^^^^^^^^^^^^^^^^^^^^^ +There are multiple ways to utilize the future compatibility mode: direct import +or changed ``SolverFactory`` version. + .. code-block:: python + # Direct import import pyomo.environ as pyo from pyomo.contrib.solver.util import assert_optimal_termination from pyomo.contrib.solver.ipopt import ipopt @@ -74,6 +81,29 @@ Future Capability Mode status.display() model.pprint() +Changing the ``SolverFactory`` version: + +.. code-block:: python + + # Change SolverFactory version + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + from pyomo.__future__ import solver_factory_v3 + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + status = pyo.SolverFactory('ipopt').solve(model) + assert_optimal_termination(status) + # Displays important results information; only available in future capability mode + status.display() + model.pprint() Interface Implementation ------------------------ From 8e56d4e0acdb36821bd96f39624f88e21f01fd93 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 15 Feb 2024 16:41:52 -0700 Subject: [PATCH 0579/3044] Doc formatting fix --- doc/OnlineDocs/developer_reference/solvers.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index db9fe307b18..f0f2a574331 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -22,8 +22,8 @@ available are: :header-rows: 1 * - Solver - - ``SolverFactory``([1]) Name - - ``SolverFactory``([3]) Name + - ``SolverFactory`` ([1]) Name + - ``SolverFactory`` ([3]) Name * - ipopt - ``ipopt_v2`` - ``ipopt`` From edfc620772a17274afc08cfea78031ec578319ef Mon Sep 17 00:00:00 2001 From: kaklise Date: Thu, 15 Feb 2024 15:57:46 -0800 Subject: [PATCH 0580/3044] Removed group_data from example, data is now grouped using data_i, and mean of sv and caf --- .../reactor_design/timeseries_data_example.py | 54 +++---------------- 1 file changed, 7 insertions(+), 47 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py index 7ffca3696eb..cde7febf6cc 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py @@ -23,15 +23,16 @@ class TimeSeriesReactorDesignExperiment(ReactorDesignExperiment): def __init__(self, data, experiment_number): self.data = data self.experiment_number = experiment_number - self.data_i = data[experiment_number] + data_i = data.loc[data['experiment'] == experiment_number,:] + self.data_i = data_i.reset_index() self.model = None def finalize_model(self): m = self.model # Experiment inputs values - m.sv = self.data_i['sv'] - m.caf = self.data_i['caf'] + m.sv = self.data_i['sv'].mean() + m.caf = self.data_i['caf'].mean() # Experiment output values m.ca = self.data_i['ca'][0] @@ -42,59 +43,18 @@ def finalize_model(self): return m -def group_data(data, groupby_column_name, use_mean=None): - """ - Group data by scenario - - Parameters - ---------- - data: DataFrame - Data - groupby_column_name: strings - Name of data column which contains scenario numbers - use_mean: list of column names or None, optional - Name of data columns which should be reduced to a single value per - scenario by taking the mean - - Returns - ---------- - grouped_data: list of dictionaries - Grouped data - """ - if use_mean is None: - use_mean_list = [] - else: - use_mean_list = use_mean - - grouped_data = [] - for exp_num, group in data.groupby(data[groupby_column_name]): - d = {} - for col in group.columns: - if col in use_mean_list: - d[col] = group[col].mean() - else: - d[col] = list(group[col]) - grouped_data.append(d) - - return grouped_data - - def main(): - # Parameter estimation using timeseries data + # Parameter estimation using timeseries data, grouped by experiment number # Data, includes multiple sensors for ca and cc file_dirname = dirname(abspath(str(__file__))) file_name = abspath(join(file_dirname, 'reactor_data_timeseries.csv')) data = pd.read_csv(file_name) - # Group time series data into experiments, return the mean value for sv and caf - # Returns a list of dictionaries - data_ts = group_data(data, 'experiment', ['sv', 'caf']) - # Create an experiment list exp_list = [] - for i in range(len(data_ts)): - exp_list.append(TimeSeriesReactorDesignExperiment(data_ts, i)) + for i in data['experiment'].unique(): + exp_list.append(TimeSeriesReactorDesignExperiment(data, i)) def SSE_timeseries(model): From 2bcb0e074dce33d2aa965e63e1d647fa4aa7403c Mon Sep 17 00:00:00 2001 From: kaklise Date: Thu, 15 Feb 2024 16:04:57 -0800 Subject: [PATCH 0581/3044] Data formatting moved to create_model --- .../rooney_biegler/bootstrap_example.py | 2 +- .../likelihood_ratio_example.py | 2 +- .../parameter_estimation_example.py | 2 +- .../examples/rooney_biegler/rooney_biegler.py | 12 ++++--- .../rooney_biegler_with_constraint.py | 12 ++++--- pyomo/contrib/parmest/tests/test_parmest.py | 32 +++++++++++-------- 6 files changed, 35 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py index 953de98a48e..b9ef114c2b3 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py @@ -35,7 +35,7 @@ def SSE(model): # Create an experiment list exp_list = [] for i in range(data.shape[0]): - exp_list.append(RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose())) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) # View one model # exp0_model = exp_list[0].get_labeled_model() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py index b08d5456982..7799148389a 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py @@ -36,7 +36,7 @@ def SSE(model): # Create an experiment list exp_list = [] for i in range(data.shape[0]): - exp_list.append(RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose())) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) # View one model # exp0_model = exp_list[0].get_labeled_model() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py index 9c851ecd9c8..aa810453883 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py @@ -35,7 +35,7 @@ def SSE(model): # Create an experiment list exp_list = [] for i in range(data.shape[0]): - exp_list.append(RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose())) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) # View one model # exp0_model = exp_list[0].get_labeled_model() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py index 8fa0fd70ec6..920bd2987e0 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py @@ -52,15 +52,17 @@ def __init__(self, data): self.model = None def create_model(self): - self.model = rooney_biegler_model(self.data) + # rooney_biegler_model expects a dataframe + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_model(data_df) def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) - m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + m.experiment_outputs.update([(m.hour, self.data['hour'])]) + m.experiment_outputs.update([(m.y, self.data['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( @@ -72,8 +74,8 @@ def finalize_model(self): m = self.model # Experiment output values - m.hour = self.data.iloc[0]['hour'] - m.y = self.data.iloc[0]['y'] + m.hour = self.data['hour'] + m.y = self.data['y'] def get_labeled_model(self): self.create_model() diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py index 463c876dc43..499f6eb505b 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py @@ -56,15 +56,17 @@ def __init__(self, data): self.model = None def create_model(self): - self.model = rooney_biegler_model_with_constraint(self.data) + # rooney_biegler_model_with_constraint expects a dataframe + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_model_with_constraint(data_df) def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) - m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + m.experiment_outputs.update([(m.hour, self.data['hour'])]) + m.experiment_outputs.update([(m.y, self.data['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( @@ -76,8 +78,8 @@ def finalize_model(self): m = self.model # Experiment output values - m.hour = self.data.iloc[0]['hour'] - m.y = self.data.iloc[0]['y'] + m.hour = self.data['hour'] + m.y = self.data['y'] def get_labeled_model(self): self.create_model() diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index ff8d1663bc9..bbffa982bcf 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -77,7 +77,7 @@ def SSE(model): exp_list = [] for i in range(data.shape[0]): exp_list.append( - RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose()) + RooneyBieglerExperiment(data.loc[i, :]) ) # Create an instance of the parmest estimator @@ -386,13 +386,14 @@ def response_rule(m, h): class RooneyBieglerExperimentParams(RooneyBieglerExperiment): def create_model(self): - self.model = rooney_biegler_params(self.data) + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_params(data_df) rooney_biegler_params_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_params_exp_list.append( RooneyBieglerExperimentParams( - self.data.loc[i, :].to_frame().transpose() + self.data.loc[i, :] ) ) @@ -422,15 +423,16 @@ def response_rule(m, h): class RooneyBieglerExperimentIndexedParams(RooneyBieglerExperiment): def create_model(self): - self.model = rooney_biegler_indexed_params(self.data) + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_indexed_params(data_df) def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) - m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + m.experiment_outputs.update([(m.hour, self.data['hour'])]) + m.experiment_outputs.update([(m.y, self.data['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) @@ -439,7 +441,7 @@ def label_model(self): for i in range(self.data.shape[0]): rooney_biegler_indexed_params_exp_list.append( RooneyBieglerExperimentIndexedParams( - self.data.loc[i, :].to_frame().transpose() + self.data.loc[i, :] ) ) @@ -465,12 +467,13 @@ def response_rule(m, h): class RooneyBieglerExperimentVars(RooneyBieglerExperiment): def create_model(self): - self.model = rooney_biegler_vars(self.data) + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_vars(data_df) rooney_biegler_vars_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_vars_exp_list.append( - RooneyBieglerExperimentVars(self.data.loc[i, :].to_frame().transpose()) + RooneyBieglerExperimentVars(self.data.loc[i, :]) ) def rooney_biegler_indexed_vars(data): @@ -501,15 +504,16 @@ def response_rule(m, h): class RooneyBieglerExperimentIndexedVars(RooneyBieglerExperiment): def create_model(self): - self.model = rooney_biegler_indexed_vars(self.data) + data_df = self.data.to_frame().transpose() + self.model = rooney_biegler_indexed_vars(data_df) def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data.iloc[0]['hour'])]) - m.experiment_outputs.update([(m.y, self.data.iloc[0]['y'])]) + m.experiment_outputs.update([(m.hour, self.data['hour'])]) + m.experiment_outputs.update([(m.y, self.data['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) @@ -518,7 +522,7 @@ def label_model(self): for i in range(self.data.shape[0]): rooney_biegler_indexed_vars_exp_list.append( RooneyBieglerExperimentIndexedVars( - self.data.loc[i, :].to_frame().transpose() + self.data.loc[i, :] ) ) @@ -985,7 +989,7 @@ def SSE(model): exp_list = [] for i in range(data.shape[0]): exp_list.append( - RooneyBieglerExperiment(data.loc[i, :].to_frame().transpose()) + RooneyBieglerExperiment(data.loc[i, :]) ) solver_options = {"tol": 1e-8} From ae28ceea6f4b255242d58847e0c92bba5536a87d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 16 Feb 2024 07:29:27 -0700 Subject: [PATCH 0582/3044] Update copyright year on all solver files --- pyomo/contrib/solver/__init__.py | 2 +- pyomo/contrib/solver/base.py | 2 +- pyomo/contrib/solver/config.py | 2 +- pyomo/contrib/solver/factory.py | 2 +- pyomo/contrib/solver/gurobi.py | 2 +- pyomo/contrib/solver/ipopt.py | 2 +- pyomo/contrib/solver/plugins.py | 2 +- pyomo/contrib/solver/results.py | 2 +- pyomo/contrib/solver/sol_reader.py | 2 +- pyomo/contrib/solver/solution.py | 2 +- pyomo/contrib/solver/tests/__init__.py | 2 +- pyomo/contrib/solver/tests/solvers/__init__.py | 2 +- pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py | 2 +- pyomo/contrib/solver/tests/solvers/test_ipopt.py | 2 +- pyomo/contrib/solver/tests/solvers/test_solvers.py | 2 +- pyomo/contrib/solver/tests/unit/__init__.py | 2 +- pyomo/contrib/solver/tests/unit/sol_files/__init__.py | 2 +- pyomo/contrib/solver/tests/unit/test_base.py | 2 +- pyomo/contrib/solver/tests/unit/test_config.py | 2 +- pyomo/contrib/solver/tests/unit/test_results.py | 2 +- pyomo/contrib/solver/tests/unit/test_sol_reader.py | 2 +- pyomo/contrib/solver/tests/unit/test_solution.py | 2 +- pyomo/contrib/solver/tests/unit/test_util.py | 2 +- pyomo/contrib/solver/util.py | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/solver/__init__.py b/pyomo/contrib/solver/__init__.py index e3eafa991cc..2dc73091ea2 100644 --- a/pyomo/contrib/solver/__init__.py +++ b/pyomo/contrib/solver/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 09c73ab3a9b..a60e770e660 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index d36c7102620..d13e1caf81d 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index 52fd9e51236..91ce92a9dee 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 919e7ae3995..c1b02c08ef9 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index f70cbb5f194..ff809a146c1 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/plugins.py b/pyomo/contrib/solver/plugins.py index 7d984d10eaa..cb089200100 100644 --- a/pyomo/contrib/solver/plugins.py +++ b/pyomo/contrib/solver/plugins.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index e80bad126a1..b330773e4f3 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index c4497516de2..2817dab4516 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index d4069b5b5a1..31792a76dfe 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/__init__.py b/pyomo/contrib/solver/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/solver/tests/__init__.py +++ b/pyomo/contrib/solver/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/solvers/__init__.py b/pyomo/contrib/solver/tests/solvers/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/solver/tests/solvers/__init__.py +++ b/pyomo/contrib/solver/tests/solvers/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py index d4c0078a0df..f2dd79619b4 100644 --- a/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py +++ b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/solvers/test_ipopt.py b/pyomo/contrib/solver/tests/solvers/test_ipopt.py index 627d502629c..2886045055c 100644 --- a/pyomo/contrib/solver/tests/solvers/test_ipopt.py +++ b/pyomo/contrib/solver/tests/solvers/test_ipopt.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 0393d1adb2e..e5af2ada170 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/__init__.py b/pyomo/contrib/solver/tests/unit/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/solver/tests/unit/__init__.py +++ b/pyomo/contrib/solver/tests/unit/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/sol_files/__init__.py b/pyomo/contrib/solver/tests/unit/sol_files/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/solver/tests/unit/sol_files/__init__.py +++ b/pyomo/contrib/solver/tests/unit/sol_files/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index b8d5c79fc0f..5fecd012cda 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/test_config.py b/pyomo/contrib/solver/tests/unit/test_config.py index f28dd5fcedf..354cfd8a37a 100644 --- a/pyomo/contrib/solver/tests/unit/test_config.py +++ b/pyomo/contrib/solver/tests/unit/test_config.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 7b9de32bc00..2d8f6460448 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/test_sol_reader.py b/pyomo/contrib/solver/tests/unit/test_sol_reader.py index 0ab94dfc4ac..d5602945e07 100644 --- a/pyomo/contrib/solver/tests/unit/test_sol_reader.py +++ b/pyomo/contrib/solver/tests/unit/test_sol_reader.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/test_solution.py b/pyomo/contrib/solver/tests/unit/test_solution.py index 7a18344d4cb..a5ee8a9e391 100644 --- a/pyomo/contrib/solver/tests/unit/test_solution.py +++ b/pyomo/contrib/solver/tests/unit/test_solution.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/tests/unit/test_util.py b/pyomo/contrib/solver/tests/unit/test_util.py index ab8a778067f..f2e8ee707f4 100644 --- a/pyomo/contrib/solver/tests/unit/test_util.py +++ b/pyomo/contrib/solver/tests/unit/test_util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index af856eab7e2..d104022692e 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain From 23bdbf7b76f674b5d437a3136387e28a84844086 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 16 Feb 2024 07:37:30 -0700 Subject: [PATCH 0583/3044] Add hidden doctest to reset solver factory --- pyomo/__future__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyomo/__future__.py b/pyomo/__future__.py index a2e08ccf291..87b1d4e77b3 100644 --- a/pyomo/__future__.py +++ b/pyomo/__future__.py @@ -70,6 +70,11 @@ def solver_factory(version=None): >>> from pyomo.__future__ import solver_factory_v3 + .. doctest:: + :hide: + + >>> from pyomo.__future__ import solver_factory_v1 + """ import pyomo.opt.base.solvers as _solvers import pyomo.contrib.solver.factory as _contrib From d405bcb4ed379cd0f61c4dd0f7901019ff742810 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 16 Feb 2024 09:01:05 -0700 Subject: [PATCH 0584/3044] Add missing dep for windows/conda enum_tools --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- doc/OnlineDocs/developer_reference/solvers.rst | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index e5513d25975..77f47b505ff 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -75,7 +75,7 @@ jobs: python: 3.9 TARGET: win PYENV: conda - PACKAGES: glpk pytest-qt + PACKAGES: glpk pytest-qt filelock - os: ubuntu-latest python: '3.11' diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index c5028606c17..87d6aa4d7a8 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -76,7 +76,7 @@ jobs: - os: windows-latest TARGET: win PYENV: conda - PACKAGES: glpk pytest-qt + PACKAGES: glpk pytest-qt filelock - os: ubuntu-latest python: '3.11' diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index f0f2a574331..5f6f3fc547b 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -22,8 +22,8 @@ available are: :header-rows: 1 * - Solver - - ``SolverFactory`` ([1]) Name - - ``SolverFactory`` ([3]) Name + - ``SolverFactory`` (v1) Name + - ``SolverFactory`` (v3) Name * - ipopt - ``ipopt_v2`` - ``ipopt`` From a3f1f826bbf186975751a0f28cd1e46152a4ee9c Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 16 Feb 2024 12:04:43 -0500 Subject: [PATCH 0585/3044] Extend range of support of `common.config.Path` --- pyomo/common/config.py | 2 +- pyomo/common/tests/test_config.py | 125 ++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 15f15872fc6..000cd76de80 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -454,7 +454,7 @@ def __init__(self, basePath=None, expandPath=None): self.expandPath = expandPath def __call__(self, path): - path = str(path) + path = os.fsdecode(path) _expand = self.expandPath if _expand is None: _expand = not Path.SuppressPathExpansion diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 1b732d86c0a..fd70e36397e 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -454,6 +454,17 @@ def norm(x): x = cwd[:2] + x return x.replace('/', os.path.sep) + class ExamplePathLike: + def __init__(self, path_str_or_bytes): + self.path = path_str_or_bytes + + def __fspath__(self): + return self.path + + def __str__(self): + path_str = str(self.path) + return f"{type(self).__name__}({path_str})" + cwd = os.getcwd() + os.path.sep c = ConfigDict() @@ -462,12 +473,30 @@ def norm(x): c.a = "/a/b/c" self.assertTrue(os.path.sep in c.a) self.assertEqual(c.a, norm('/a/b/c')) + c.a = b"/a/b/c" + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm('/a/b/c')) + c.a = ExamplePathLike("/a/b/c") + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm('/a/b/c')) c.a = "a/b/c" self.assertTrue(os.path.sep in c.a) self.assertEqual(c.a, norm(cwd + 'a/b/c')) + c.a = b'a/b/c' + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm(cwd + 'a/b/c')) + c.a = ExamplePathLike('a/b/c') + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm(cwd + 'a/b/c')) c.a = "${CWD}/a/b/c" self.assertTrue(os.path.sep in c.a) self.assertEqual(c.a, norm(cwd + 'a/b/c')) + c.a = b'${CWD}/a/b/c' + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm(cwd + 'a/b/c')) + c.a = ExamplePathLike('${CWD}/a/b/c') + self.assertTrue(os.path.sep in c.a) + self.assertEqual(c.a, norm(cwd + 'a/b/c')) c.a = None self.assertIs(c.a, None) @@ -476,12 +505,30 @@ def norm(x): c.b = "/a/b/c" self.assertTrue(os.path.sep in c.b) self.assertEqual(c.b, norm('/a/b/c')) + c.b = b"/a/b/c" + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm('/a/b/c')) + c.b = ExamplePathLike("/a/b/c") + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm('/a/b/c')) c.b = "a/b/c" self.assertTrue(os.path.sep in c.b) self.assertEqual(c.b, norm(cwd + 'rel/path/a/b/c')) + c.b = b"a/b/c" + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm(cwd + 'rel/path/a/b/c')) + c.b = ExamplePathLike("a/b/c") + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm(cwd + "rel/path/a/b/c")) c.b = "${CWD}/a/b/c" self.assertTrue(os.path.sep in c.b) self.assertEqual(c.b, norm(cwd + 'a/b/c')) + c.b = b"${CWD}/a/b/c" + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm(cwd + 'a/b/c')) + c.b = ExamplePathLike("${CWD}/a/b/c") + self.assertTrue(os.path.sep in c.b) + self.assertEqual(c.b, norm(cwd + 'a/b/c')) c.b = None self.assertIs(c.b, None) @@ -490,12 +537,30 @@ def norm(x): c.c = "/a/b/c" self.assertTrue(os.path.sep in c.c) self.assertEqual(c.c, norm('/a/b/c')) + c.c = b"/a/b/c" + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm('/a/b/c')) + c.c = ExamplePathLike("/a/b/c") + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm('/a/b/c')) c.c = "a/b/c" self.assertTrue(os.path.sep in c.c) self.assertEqual(c.c, norm('/my/dir/a/b/c')) + c.c = b"a/b/c" + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm('/my/dir/a/b/c')) + c.c = ExamplePathLike("a/b/c") + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm("/my/dir/a/b/c")) c.c = "${CWD}/a/b/c" self.assertTrue(os.path.sep in c.c) self.assertEqual(c.c, norm(cwd + 'a/b/c')) + c.c = b"${CWD}/a/b/c" + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm(cwd + 'a/b/c')) + c.c = ExamplePathLike("${CWD}/a/b/c") + self.assertTrue(os.path.sep in c.c) + self.assertEqual(c.c, norm(cwd + 'a/b/c')) c.c = None self.assertIs(c.c, None) @@ -505,12 +570,30 @@ def norm(x): c.d = "/a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm('/a/b/c')) + c.d = b"/a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm('/a/b/c')) + c.d = ExamplePathLike("/a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm('/a/b/c')) c.d = "a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = b"a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = ExamplePathLike("a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) c.d = "${CWD}/a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = b"${CWD}/a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = ExamplePathLike("${CWD}/a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) c.d_base = '/my/dir' c.d = "/a/b/c" @@ -527,12 +610,30 @@ def norm(x): c.d = "/a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm('/a/b/c')) + c.d = b"/a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm('/a/b/c')) + c.d = ExamplePathLike("/a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm('/a/b/c')) c.d = "a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm(cwd + 'rel/path/a/b/c')) + c.d = b"a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'rel/path/a/b/c')) + c.d = ExamplePathLike("a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'rel/path/a/b/c')) c.d = "${CWD}/a/b/c" self.assertTrue(os.path.sep in c.d) self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = b"${CWD}/a/b/c" + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) + c.d = ExamplePathLike("${CWD}/a/b/c") + self.assertTrue(os.path.sep in c.d) + self.assertEqual(c.d, norm(cwd + 'a/b/c')) try: Path.SuppressPathExpansion = True @@ -540,14 +641,38 @@ def norm(x): self.assertTrue('/' in c.d) self.assertTrue('\\' not in c.d) self.assertEqual(c.d, '/a/b/c') + c.d = b"/a/b/c" + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, '/a/b/c') + c.d = ExamplePathLike("/a/b/c") + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, '/a/b/c') c.d = "a/b/c" self.assertTrue('/' in c.d) self.assertTrue('\\' not in c.d) self.assertEqual(c.d, 'a/b/c') + c.d = b"a/b/c" + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, 'a/b/c') + c.d = ExamplePathLike("a/b/c") + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, 'a/b/c') c.d = "${CWD}/a/b/c" self.assertTrue('/' in c.d) self.assertTrue('\\' not in c.d) self.assertEqual(c.d, "${CWD}/a/b/c") + c.d = b"${CWD}/a/b/c" + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, "${CWD}/a/b/c") + c.d = ExamplePathLike("${CWD}/a/b/c") + self.assertTrue('/' in c.d) + self.assertTrue('\\' not in c.d) + self.assertEqual(c.d, "${CWD}/a/b/c") finally: Path.SuppressPathExpansion = False From e2965167b214c01f199065f15688b35dcd642eb2 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 16 Feb 2024 13:05:43 -0500 Subject: [PATCH 0586/3044] Add `IsInstance` domain validator to `common.config` --- pyomo/common/config.py | 40 +++++++++++++++++++++++++++++++ pyomo/common/tests/test_config.py | 37 ++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 000cd76de80..baf07b39fdf 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -302,6 +302,46 @@ def domain_name(self): return f'InEnum[{self._domain.__name__}]' +class IsInstance(object): + def __init__(self, *bases): + assert bases + self.baseClasses = bases + + @staticmethod + def _fullname(klass): + """ + Get full name of class, including appropriate module qualifier. + """ + module_name = klass.__module__ + module_qual = "" if module_name == "builtins" else f"{module_name}." + return f"{module_qual}{klass.__name__}" + + def __call__(self, obj): + if isinstance(obj, self.baseClasses): + return obj + if len(self.baseClasses) > 1: + class_names = ", ".join( + f"{self._fullname(kls)!r}" for kls in self.baseClasses + ) + msg = ( + "Expected an instance of one of these types: " + f"{class_names}, but received value {obj!r} of type " + f"{self._fullname(type(obj))!r}" + ) + else: + msg = ( + f"Expected an instance of " + f"{self._fullname(self.baseClasses[0])!r}, " + f"but received value {obj!r} of type {self._fullname(type(obj))!r}" + ) + raise ValueError(msg) + + def domain_name(self): + return ( + f"IsInstance({', '.join(self._fullname(kls) for kls in self.baseClasses)})" + ) + + class ListOf(object): """Domain validator for lists of a specified type diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index fd70e36397e..19d4bfbe7e8 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -60,6 +60,7 @@ def yaml_load(arg): NonPositiveFloat, NonNegativeFloat, In, + IsInstance, ListOf, Module, Path, @@ -448,6 +449,42 @@ class TestEnum(enum.Enum): with self.assertRaisesRegex(ValueError, '.*invalid value'): cfg.enum = 'ITEM_THREE' + def test_IsInstance(self): + c = ConfigDict() + c.declare("val", ConfigValue(None, IsInstance(int))) + c.val = 1 + self.assertEqual(c.val, 1) + exc_str = ( + "Expected an instance of 'int', but received value 2.4 of type 'float'" + ) + with self.assertRaisesRegex(ValueError, exc_str): + c.val = 2.4 + + class TestClass: + def __repr__(self): + return f"{TestClass.__name__}()" + + c.declare("val2", ConfigValue(None, IsInstance(TestClass))) + testinst = TestClass() + c.val2 = testinst + self.assertEqual(c.val2, testinst) + exc_str = ( + r"Expected an instance of '.*\.TestClass', " + "but received value 2.4 of type 'float'" + ) + with self.assertRaisesRegex(ValueError, exc_str): + c.val2 = 2.4 + + c.declare("val3", ConfigValue(None, IsInstance(int, str))) + c.val3 = 2 + self.assertEqual(c.val3, 2) + exc_str = ( + r"Expected an instance of one of these types: 'int', 'str'" + r", but received value TestClass\(\) of type '.*\.TestClass'" + ) + with self.assertRaisesRegex(ValueError, exc_str): + c.val3 = TestClass() + def test_Path(self): def norm(x): if cwd[1] == ':' and x[0] == '/': From 02796dd0cebd1c6ec6ae9d120cb7b85f296b4139 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 16 Feb 2024 13:21:13 -0500 Subject: [PATCH 0587/3044] Add `IsInstance` domain validator for type checking --- pyomo/common/config.py | 10 ++++++++++ pyomo/common/tests/test_config.py | 11 +++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index baf07b39fdf..d812363bdec 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -303,6 +303,15 @@ def domain_name(self): class IsInstance(object): + """ + Domain validator for type checking. + + Parameters + ---------- + *bases : tuple of type + Valid types. + """ + def __init__(self, *bases): assert bases self.baseClasses = bases @@ -749,6 +758,7 @@ def from_enum_or_string(cls, arg): NonNegativeFloat In InEnum + IsInstance ListOf Module Path diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 19d4bfbe7e8..b5acf51ba46 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -475,15 +475,18 @@ def __repr__(self): with self.assertRaisesRegex(ValueError, exc_str): c.val2 = 2.4 - c.declare("val3", ConfigValue(None, IsInstance(int, str))) + c.declare("val3", ConfigValue(None, IsInstance(int, TestClass))) + self.assertRegex( + c.get("val3").domain_name(), r"IsInstance\(int, .*\.TestClass\)" + ) c.val3 = 2 self.assertEqual(c.val3, 2) exc_str = ( - r"Expected an instance of one of these types: 'int', 'str'" - r", but received value TestClass\(\) of type '.*\.TestClass'" + r"Expected an instance of one of these types: 'int', '.*\.TestClass'" + r", but received value 2.4 of type 'float'" ) with self.assertRaisesRegex(ValueError, exc_str): - c.val3 = TestClass() + c.val3 = 2.4 def test_Path(self): def norm(x): From 832a789cd0c8858d7c0c6616419288bacf794643 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 16 Feb 2024 11:29:36 -0700 Subject: [PATCH 0588/3044] Add minimal example for presolve and scaling to docs --- .../developer_reference/solvers.rst | 39 +++++++++++++++++++ pyomo/contrib/solver/ipopt.py | 25 +++++++++--- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 5f6f3fc547b..8c8c9e5b8ee 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -105,6 +105,45 @@ Changing the ``SolverFactory`` version: status.display() model.pprint() +Linear Presolve and Scaling +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The new interface will allow for direct manipulation of linear presolve and scaling +options for certain solvers. Currently, these options are only available for +``ipopt``. + +.. autoclass:: pyomo.contrib.solver.ipopt.ipopt + :members: solve + +The ``writer_config`` configuration option can be used to manipulate presolve +and scaling options: + +.. code-block:: python + + >>> from pyomo.contrib.solver.ipopt import ipopt + >>> opt = ipopt() + >>> opt.config.writer_config.display() + + show_section_timing: false + skip_trivial_constraints: true + file_determinism: FileDeterminism.ORDERED + symbolic_solver_labels: false + scale_model: true + export_nonlinear_variables: None + row_order: None + column_order: None + export_defined_variables: true + linear_presolve: true + +Note that, by default, both ``linear_presolve`` and ``scale_model`` are enabled. +Users can manipulate ``linear_presolve`` and ``scale_model`` to their preferred +states by changing their values. + +.. code-block:: python + + >>> opt.config.writer_config.linear_presolve = False + + Interface Implementation ------------------------ diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index ff809a146c1..edea4e693b4 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -17,7 +17,12 @@ from typing import Mapping, Optional, Sequence from pyomo.common import Executable -from pyomo.common.config import ConfigValue, NonNegativeFloat +from pyomo.common.config import ( + ConfigValue, + NonNegativeFloat, + document_kwargs_from_configdict, + ConfigDict, +) from pyomo.common.errors import PyomoException from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer @@ -65,11 +70,20 @@ def __init__( visibility=visibility, ) - self.executable = self.declare( - 'executable', ConfigValue(default=Executable('ipopt')) + self.executable: Executable = self.declare( + 'executable', + ConfigValue( + default=Executable('ipopt'), + description="Preferred executable for ipopt. Defaults to searching the " + "``PATH`` for the first available ``ipopt``.", + ), ) - self.writer_config = self.declare( - 'writer_config', ConfigValue(default=NLWriter.CONFIG()) + self.writer_config: ConfigDict = self.declare( + 'writer_config', + ConfigValue( + default=NLWriter.CONFIG(), + description="For the manipulation of NL writer options.", + ), ) @@ -270,6 +284,7 @@ def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: boo cmd.append(str(k) + '=' + str(val)) return cmd + @document_kwargs_from_configdict(CONFIG) def solve(self, model, **kwds): # Begin time tracking start_timestamp = datetime.datetime.now(datetime.timezone.utc) From f43a49e105a14d04756e1d510e1fbf8aa5a4e138 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 16 Feb 2024 14:51:59 -0700 Subject: [PATCH 0589/3044] Remove __all__ from modules --- pyomo/common/_command.py | 2 -- pyomo/contrib/pynumero/interfaces/ampl_nlp.py | 4 +--- pyomo/contrib/pynumero/interfaces/nlp.py | 3 +-- .../contrib/pynumero/interfaces/pyomo_nlp.py | 3 --- pyomo/contrib/pynumero/sparse/block_matrix.py | 2 -- pyomo/contrib/pynumero/sparse/block_vector.py | 2 -- .../pynumero/sparse/mpi_block_matrix.py | 2 -- .../pynumero/sparse/mpi_block_vector.py | 2 -- pyomo/core/base/PyomoModel.py | 7 ++---- pyomo/core/base/action.py | 5 ++-- pyomo/core/base/block.py | 14 ----------- pyomo/core/base/blockutil.py | 2 -- pyomo/core/base/check.py | 2 -- pyomo/core/base/component_order.py | 3 --- pyomo/core/base/connector.py | 3 --- pyomo/core/base/constraint.py | 10 -------- pyomo/core/base/expression.py | 5 +--- pyomo/core/base/external.py | 2 -- pyomo/core/base/indexed_component.py | 8 +------ pyomo/core/base/instance2dat.py | 2 -- pyomo/core/base/label.py | 11 --------- pyomo/core/base/logical_constraint.py | 3 --- pyomo/core/base/misc.py | 4 ---- pyomo/core/base/objective.py | 10 -------- pyomo/core/base/param.py | 2 -- pyomo/core/base/piecewise.py | 3 --- pyomo/core/base/plugin.py | 24 ------------------- pyomo/core/base/rangeset.py | 2 -- pyomo/core/base/sets.py | 2 -- pyomo/core/base/sos.py | 2 -- pyomo/core/base/suffix.py | 2 -- pyomo/core/base/var.py | 5 ---- pyomo/core/beta/dict_objects.py | 2 -- pyomo/core/beta/list_objects.py | 2 -- pyomo/core/expr/__init__.py | 8 ------- pyomo/core/expr/numvalue.py | 17 ------------- pyomo/core/util.py | 12 ---------- pyomo/dae/contset.py | 1 - pyomo/dae/diffvar.py | 2 -- pyomo/dae/integral.py | 2 -- pyomo/dae/simulator.py | 12 ++++------ pyomo/dataportal/DataPortal.py | 2 -- pyomo/dataportal/TableData.py | 2 -- pyomo/dataportal/factory.py | 2 -- pyomo/dataportal/parse_datacmds.py | 2 -- pyomo/network/arc.py | 2 -- pyomo/network/decomposition.py | 2 -- pyomo/network/port.py | 2 -- pyomo/opt/base/convert.py | 2 -- pyomo/opt/base/formats.py | 5 ---- pyomo/opt/base/problem.py | 2 -- pyomo/opt/base/results.py | 2 -- pyomo/opt/base/solvers.py | 5 +--- pyomo/opt/parallel/async_solver.py | 3 --- pyomo/opt/parallel/local.py | 3 --- pyomo/opt/parallel/manager.py | 10 -------- pyomo/opt/problem/ampl.py | 2 -- pyomo/opt/results/container.py | 17 ++----------- pyomo/opt/results/problem.py | 2 -- pyomo/opt/results/results_.py | 4 +--- pyomo/opt/results/solution.py | 2 -- pyomo/opt/results/solver.py | 8 ------- pyomo/opt/solver/ilmcmd.py | 2 -- pyomo/opt/solver/shellcmd.py | 2 -- pyomo/opt/testing/pyunit.py | 3 --- pyomo/repn/beta/matrix.py | 6 ----- pyomo/repn/plugins/ampl/ampl_.py | 2 -- pyomo/repn/standard_aux.py | 3 --- pyomo/repn/standard_repn.py | 3 --- pyomo/scripting/convert.py | 2 -- pyomo/scripting/pyomo_parser.py | 2 -- pyomo/solvers/plugins/solvers/CBCplugin.py | 2 -- pyomo/solvers/tests/solvers.py | 2 -- pyomo/util/blockutil.py | 2 -- 74 files changed, 16 insertions(+), 307 deletions(-) diff --git a/pyomo/common/_command.py b/pyomo/common/_command.py index 0777155a557..ad521659aa7 100644 --- a/pyomo/common/_command.py +++ b/pyomo/common/_command.py @@ -13,8 +13,6 @@ Management of Pyomo commands """ -__all__ = ['pyomo_command', 'get_pyomo_commands'] - import logging logger = logging.getLogger('pyomo.common') diff --git a/pyomo/contrib/pynumero/interfaces/ampl_nlp.py b/pyomo/contrib/pynumero/interfaces/ampl_nlp.py index c19d252667d..30258b3e685 100644 --- a/pyomo/contrib/pynumero/interfaces/ampl_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/ampl_nlp.py @@ -27,10 +27,8 @@ from pyomo.common.deprecation import deprecated from pyomo.contrib.pynumero.interfaces.nlp import ExtendedNLP -__all__ = ['AslNLP', 'AmplNLP'] - -# ToDo: need to add support for modifying bounds. +# TODO: need to add support for modifying bounds. # support for changing variable bounds seems possible. # support for changing inequality bounds would require more work. (this is less frequent?) # TODO: check performance impacts of caching - memory and computational time. diff --git a/pyomo/contrib/pynumero/interfaces/nlp.py b/pyomo/contrib/pynumero/interfaces/nlp.py index 20b3a5e4938..d6571086429 100644 --- a/pyomo/contrib/pynumero/interfaces/nlp.py +++ b/pyomo/contrib/pynumero/interfaces/nlp.py @@ -50,9 +50,8 @@ .. rubric:: Contents """ -import abc -__all__ = ['NLP'] +import abc class NLP(object, metaclass=abc.ABCMeta): diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py index f9014ab29c0..51edd09311a 100644 --- a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py @@ -28,9 +28,6 @@ from .external_grey_box import ExternalGreyBoxBlock -__all__ = ['PyomoNLP'] - - # TODO: There are todos in the code below class PyomoNLP(AslNLP): def __init__(self, pyomo_model, nl_file_options=None): diff --git a/pyomo/contrib/pynumero/sparse/block_matrix.py b/pyomo/contrib/pynumero/sparse/block_matrix.py index ba7ed4f085b..02ad584928b 100644 --- a/pyomo/contrib/pynumero/sparse/block_matrix.py +++ b/pyomo/contrib/pynumero/sparse/block_matrix.py @@ -31,8 +31,6 @@ import logging import warnings -__all__ = ['BlockMatrix', 'NotFullyDefinedBlockMatrixError'] - logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/pynumero/sparse/block_vector.py b/pyomo/contrib/pynumero/sparse/block_vector.py index 2b529736935..b636dd74203 100644 --- a/pyomo/contrib/pynumero/sparse/block_vector.py +++ b/pyomo/contrib/pynumero/sparse/block_vector.py @@ -27,8 +27,6 @@ from ..dependencies import numpy as np from .base_block import BaseBlockVector -__all__ = ['BlockVector', 'NotFullyDefinedBlockVectorError'] - class NotFullyDefinedBlockVectorError(Exception): pass diff --git a/pyomo/contrib/pynumero/sparse/mpi_block_matrix.py b/pyomo/contrib/pynumero/sparse/mpi_block_matrix.py index 28a39b4e2eb..d32adebce0e 100644 --- a/pyomo/contrib/pynumero/sparse/mpi_block_matrix.py +++ b/pyomo/contrib/pynumero/sparse/mpi_block_matrix.py @@ -32,8 +32,6 @@ from scipy.sparse import coo_matrix import operator -__all__ = ['MPIBlockMatrix'] - def assert_block_structure(mat: MPIBlockMatrix): if mat.has_undefined_row_sizes() or mat.has_undefined_col_sizes(): diff --git a/pyomo/contrib/pynumero/sparse/mpi_block_vector.py b/pyomo/contrib/pynumero/sparse/mpi_block_vector.py index be8091c9597..89cf136a5f7 100644 --- a/pyomo/contrib/pynumero/sparse/mpi_block_vector.py +++ b/pyomo/contrib/pynumero/sparse/mpi_block_vector.py @@ -17,8 +17,6 @@ import numpy as np import operator -__all__ = ['MPIBlockVector'] - def assert_block_structure(vec): if vec.has_none: diff --git a/pyomo/core/base/PyomoModel.py b/pyomo/core/base/PyomoModel.py index 759b17c9a79..ba7823c642a 100644 --- a/pyomo/core/base/PyomoModel.py +++ b/pyomo/core/base/PyomoModel.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['Model', 'ConcreteModel', 'AbstractModel', 'global_option'] - import logging import sys from weakref import ref as weakref_ref @@ -20,7 +18,7 @@ from pyomo.common import timing from pyomo.common.collections import Bunch from pyomo.common.dependencies import pympler, pympler_available -from pyomo.common.deprecation import deprecated, deprecation_warning +from pyomo.common.deprecation import deprecated from pyomo.common.gc_manager import PauseGC from pyomo.common.log import is_debug_set from pyomo.common.numeric_types import value @@ -34,11 +32,10 @@ from pyomo.core.base.block import ScalarBlock from pyomo.core.base.set import Set from pyomo.core.base.componentuid import ComponentUID -from pyomo.core.base.transformation import TransformationFactory from pyomo.core.base.label import CNameLabeler, CuidLabeler from pyomo.dataportal.DataPortal import DataPortal -from pyomo.opt.results import SolverResults, Solution, SolverStatus, UndefinedData +from pyomo.opt.results import Solution, SolverStatus, UndefinedData from contextlib import nullcontext from io import StringIO diff --git a/pyomo/core/base/action.py b/pyomo/core/base/action.py index f929c4b38ff..d24d94fe05a 100644 --- a/pyomo/core/base/action.py +++ b/pyomo/core/base/action.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['BuildAction'] - import logging import types @@ -24,7 +22,8 @@ @ModelComponentFactory.register( - "A component that performs arbitrary actions during model construction. The action rule is applied to every index value." + "A component that performs arbitrary actions during model construction. " + "The action rule is applied to every index value." ) class BuildAction(IndexedComponent): """A build action, which executes a rule for all valid indices. diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 190a820fbfe..48353078fca 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -9,20 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = [ - 'Block', - 'TraversalStrategy', - 'SortComponents', - 'active_components', - 'components', - 'active_components_data', - 'components_data', - 'SimpleBlock', - 'ScalarBlock', -] - import copy -import enum import logging import sys import weakref @@ -41,7 +28,6 @@ from pyomo.common.formatting import StreamIndenter from pyomo.common.gc_manager import PauseGC from pyomo.common.log import is_debug_set -from pyomo.common.sorting import sorted_robust from pyomo.common.timing import ConstructionTimer from pyomo.core.base.component import ( Component, diff --git a/pyomo/core/base/blockutil.py b/pyomo/core/base/blockutil.py index fc763da8b98..d91a5c85ac2 100644 --- a/pyomo/core/base/blockutil.py +++ b/pyomo/core/base/blockutil.py @@ -12,8 +12,6 @@ # the purpose of this file is to collect all utility methods that compute # attributes of blocks, based on their contents. -__all__ = ['has_discrete_variables'] - from pyomo.common import deprecated from pyomo.core.base import Var diff --git a/pyomo/core/base/check.py b/pyomo/core/base/check.py index cbf2a99e7a3..485d1a73b6b 100644 --- a/pyomo/core/base/check.py +++ b/pyomo/core/base/check.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['BuildCheck'] - import logging import types diff --git a/pyomo/core/base/component_order.py b/pyomo/core/base/component_order.py index 8e69baa0972..9244828cbe5 100644 --- a/pyomo/core/base/component_order.py +++ b/pyomo/core/base/component_order.py @@ -9,9 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -__all__ = ['items', 'display_items', 'display_name'] - from pyomo.core.base.set import Set, RangeSet from pyomo.core.base.param import Param from pyomo.core.base.var import Var diff --git a/pyomo/core/base/connector.py b/pyomo/core/base/connector.py index 8dfee45236e..435a2c2fccb 100644 --- a/pyomo/core/base/connector.py +++ b/pyomo/core/base/connector.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['Connector'] - import logging import sys from weakref import ref as weakref_ref @@ -26,7 +24,6 @@ from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import IndexedComponent from pyomo.core.base.misc import apply_indexed_rule -from pyomo.core.base.transformation import TransformationFactory logger = logging.getLogger('pyomo.core') diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 21da457edf6..c67236656be 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -9,18 +9,8 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = [ - 'Constraint', - '_ConstraintData', - 'ConstraintList', - 'simple_constraint_rule', - 'simple_constraintlist_rule', -] - -import io import sys import logging -import math from weakref import ref as weakref_ref from pyomo.common.pyomo_typing import overload diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index 3695f95b0be..3ce998b62a4 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -9,15 +9,13 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['Expression', '_ExpressionData'] - import sys import logging from weakref import ref as weakref_ref from pyomo.common.pyomo_typing import overload from pyomo.common.log import is_debug_set -from pyomo.common.deprecation import deprecated, RenamedClass +from pyomo.common.deprecation import RenamedClass from pyomo.common.modeling import NOTSET from pyomo.common.formatting import tabular_writer from pyomo.common.timing import ConstructionTimer @@ -32,7 +30,6 @@ from pyomo.core.base.component import ComponentData, ModelComponentFactory from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import IndexedComponent, UnindexedComponent_set -from pyomo.core.base.misc import apply_indexed_rule from pyomo.core.expr.numvalue import as_numeric from pyomo.core.base.initializer import Initializer diff --git a/pyomo/core/base/external.py b/pyomo/core/base/external.py index c1f7ef32a80..3c0038d745d 100644 --- a/pyomo/core/base/external.py +++ b/pyomo/core/base/external.py @@ -43,8 +43,6 @@ from pyomo.core.base.component import Component from pyomo.core.base.units_container import units -__all__ = ('ExternalFunction',) - logger = logging.getLogger('pyomo.core') nan = float('nan') diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index e87cafa0606..abb29580960 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -9,16 +9,11 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['IndexedComponent', 'ActiveIndexedComponent'] - -import enum import inspect import logging import sys import textwrap -from copy import deepcopy - import pyomo.core.expr as EXPR import pyomo.core.base as BASE from pyomo.core.base.indexed_component_slice import IndexedComponent_slice @@ -32,9 +27,8 @@ from pyomo.common import DeveloperError from pyomo.common.autoslots import fast_deepcopy from pyomo.common.collections import ComponentSet -from pyomo.common.dependencies import numpy as np, numpy_available from pyomo.common.deprecation import deprecated, deprecation_warning -from pyomo.common.errors import DeveloperError, TemplateExpressionError +from pyomo.common.errors import TemplateExpressionError from pyomo.common.modeling import NOTSET from pyomo.common.numeric_types import native_types from pyomo.common.sorting import sorted_robust diff --git a/pyomo/core/base/instance2dat.py b/pyomo/core/base/instance2dat.py index 4dab6435187..5cd690b7ece 100644 --- a/pyomo/core/base/instance2dat.py +++ b/pyomo/core/base/instance2dat.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['instance2dat'] - import types from pyomo.core.base import Set, Param, value diff --git a/pyomo/core/base/label.py b/pyomo/core/base/label.py index 4ed61773a7e..e22c1283138 100644 --- a/pyomo/core/base/label.py +++ b/pyomo/core/base/label.py @@ -9,17 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = [ - 'CounterLabeler', - 'NumericLabeler', - 'CNameLabeler', - 'TextLabeler', - 'AlphaNumericTextLabeler', - 'NameLabeler', - 'CuidLabeler', - 'ShortNameLabeler', -] - import re from pyomo.common.deprecation import deprecated diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index 9a6e9d552d0..f32d727931a 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['LogicalConstraint', '_LogicalConstraintData', 'LogicalConstraintList'] - import inspect import sys import logging @@ -22,7 +20,6 @@ from pyomo.common.modeling import NOTSET from pyomo.common.timing import ConstructionTimer -from pyomo.core.base.constraint import Constraint from pyomo.core.expr.boolean_value import as_boolean, BooleanConstant from pyomo.core.expr.numvalue import native_types, native_logical_types from pyomo.core.base.component import ActiveComponentData, ModelComponentFactory diff --git a/pyomo/core/base/misc.py b/pyomo/core/base/misc.py index 926d4e576f4..456a4531e30 100644 --- a/pyomo/core/base/misc.py +++ b/pyomo/core/base/misc.py @@ -9,14 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['display'] - import logging import sys -import types from pyomo.common.deprecation import relocated_module_attribute -from pyomo.core.expr import native_numeric_types logger = logging.getLogger('pyomo.core') diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index f0e60a00e85..fcc63755f2b 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -9,16 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ( - 'Objective', - 'simple_objective_rule', - '_ObjectiveData', - 'minimize', - 'maximize', - 'simple_objectivelist_rule', - 'ObjectiveList', -) - import sys import logging from weakref import ref as weakref_ref diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 88e7ca98de7..03d700140e8 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['Param'] - import sys import types import logging diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index b6ae66ac093..7817a61b2f2 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -32,9 +32,6 @@ *) piecewise for functions of the form y = f(x1,x2,...) """ - -__all__ = ['Piecewise'] - import logging import math import itertools diff --git a/pyomo/core/base/plugin.py b/pyomo/core/base/plugin.py index 8c44af2dd61..062e9f9fb85 100644 --- a/pyomo/core/base/plugin.py +++ b/pyomo/core/base/plugin.py @@ -21,30 +21,6 @@ calling_frame=inspect.currentframe().f_back, ) -__all__ = [ - 'pyomo_callback', - 'IPyomoExpression', - 'ExpressionFactory', - 'ExpressionRegistration', - 'IPyomoPresolver', - 'IPyomoPresolveAction', - 'IParamRepresentation', - 'ParamRepresentationFactory', - 'IPyomoScriptPreprocess', - 'IPyomoScriptCreateModel', - 'IPyomoScriptCreateDataPortal', - 'IPyomoScriptModifyInstance', - 'IPyomoScriptPrintModel', - 'IPyomoScriptPrintInstance', - 'IPyomoScriptSaveInstance', - 'IPyomoScriptPrintResults', - 'IPyomoScriptSaveResults', - 'IPyomoScriptPostprocess', - 'ModelComponentFactory', - 'Transformation', - 'TransformationFactory', -] - from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.transformation import ( Transformation, diff --git a/pyomo/core/base/rangeset.py b/pyomo/core/base/rangeset.py index 27693548c1d..32e41698aab 100644 --- a/pyomo/core/base/rangeset.py +++ b/pyomo/core/base/rangeset.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['RangeSet'] - from .set import RangeSet from pyomo.common.deprecation import deprecation_warning diff --git a/pyomo/core/base/sets.py b/pyomo/core/base/sets.py index f2ae44be459..ca693cf7d8b 100644 --- a/pyomo/core/base/sets.py +++ b/pyomo/core/base/sets.py @@ -13,8 +13,6 @@ # . rename 'filter' to something else # . confirm that filtering is efficient -__all__ = ['Set', 'set_options', 'simple_set_rule', 'SetOf'] - from .set import ( process_setarg, set_options, diff --git a/pyomo/core/base/sos.py b/pyomo/core/base/sos.py index 32265df6686..6b8586c9b49 100644 --- a/pyomo/core/base/sos.py +++ b/pyomo/core/base/sos.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['SOSConstraint'] - import sys import logging diff --git a/pyomo/core/base/suffix.py b/pyomo/core/base/suffix.py index 67ab0b74215..0c27eee060f 100644 --- a/pyomo/core/base/suffix.py +++ b/pyomo/core/base/suffix.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ('Suffix', 'active_export_suffix_generator', 'active_import_suffix_generator') - import enum import logging diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index 67b6e1a28d7..d03fd0b677f 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['Var', '_VarData', '_GeneralVarData', 'VarList', 'SimpleVar', 'ScalarVar'] - import logging import sys from pyomo.common.pyomo_typing import overload @@ -29,7 +27,6 @@ value, is_potentially_variable, native_numeric_types, - native_types, ) from pyomo.core.base.component import ComponentData, ModelComponentFactory from pyomo.core.base.global_set import UnindexedComponent_index @@ -44,7 +41,6 @@ DefaultInitializer, BoundInitializer, ) -from pyomo.core.base.misc import apply_indexed_rule from pyomo.core.base.set import ( Reals, Binary, @@ -54,7 +50,6 @@ integer_global_set_ids, ) from pyomo.core.base.units_container import units -from pyomo.core.base.util import is_functor logger = logging.getLogger('pyomo.core') diff --git a/pyomo/core/beta/dict_objects.py b/pyomo/core/beta/dict_objects.py index a698fcbb717..a8298b08e63 100644 --- a/pyomo/core/beta/dict_objects.py +++ b/pyomo/core/beta/dict_objects.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = () - import logging from weakref import ref as weakref_ref diff --git a/pyomo/core/beta/list_objects.py b/pyomo/core/beta/list_objects.py index f2ccf0d37aa..f53997fed17 100644 --- a/pyomo/core/beta/list_objects.py +++ b/pyomo/core/beta/list_objects.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = () - import logging from weakref import ref as weakref_ref diff --git a/pyomo/core/expr/__init__.py b/pyomo/core/expr/__init__.py index 5efb5026c65..b0ad2ac4892 100644 --- a/pyomo/core/expr/__init__.py +++ b/pyomo/core/expr/__init__.py @@ -9,14 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -# -# The definition of __all__ is a bit funky here, because we want to -# expose symbols in pyomo.core.expr.current that are not included in -# pyomo.core.expr. The idea is that pyomo.core.expr provides symbols -# that are used by general users, but pyomo.core.expr.current provides -# symbols that are used by developers. -# - from . import ( numvalue, visitor, diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index 8cc20648eb4..f3ea76c305c 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -9,21 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ( - 'value', - 'is_constant', - 'is_fixed', - 'is_variable_type', - 'is_potentially_variable', - 'NumericValue', - 'ZeroConstant', - 'native_numeric_types', - 'native_types', - 'nonpyomo_leaf_types', - 'polynomial_degree', -) - -import collections import sys import logging @@ -34,7 +19,6 @@ ) from pyomo.core.expr.expr_common import ExpressionType from pyomo.core.expr.numeric_expr import NumericValue -import pyomo.common.numeric_types as _numeric_types # TODO: update Pyomo to import these objects from common.numeric_types # (and not from here) @@ -48,7 +32,6 @@ check_if_numeric_type, value, ) -from pyomo.core.pyomoobject import PyomoObject relocated_module_attribute( 'native_boolean_types', diff --git a/pyomo/core/util.py b/pyomo/core/util.py index 4e076c7505b..e4a70aea05a 100644 --- a/pyomo/core/util.py +++ b/pyomo/core/util.py @@ -13,24 +13,12 @@ # Utility functions # -__all__ = [ - 'sum_product', - 'summation', - 'dot_product', - 'sequence', - 'prod', - 'quicksum', - 'target_list', -] - from pyomo.common.deprecation import deprecation_warning from pyomo.core.expr.numvalue import native_numeric_types from pyomo.core.expr.numeric_expr import ( mutable_expression, - nonlinear_expression, NPV_SumExpression, ) -import pyomo.core.expr as EXPR from pyomo.core.base.var import Var from pyomo.core.base.expression import Expression from pyomo.core.base.component import _ComponentBase diff --git a/pyomo/dae/contset.py b/pyomo/dae/contset.py index 94d20723770..9b4f11714df 100644 --- a/pyomo/dae/contset.py +++ b/pyomo/dae/contset.py @@ -17,7 +17,6 @@ from pyomo.core.base.component import ModelComponentFactory logger = logging.getLogger('pyomo.dae') -__all__ = ['ContinuousSet'] @ModelComponentFactory.register( diff --git a/pyomo/dae/diffvar.py b/pyomo/dae/diffvar.py index 6bb3a8b06f0..b921107957f 100644 --- a/pyomo/dae/diffvar.py +++ b/pyomo/dae/diffvar.py @@ -16,8 +16,6 @@ from pyomo.core.base.var import Var from pyomo.dae.contset import ContinuousSet -__all__ = ('DerivativeVar', 'DAE_Error') - def create_access_function(var): """ diff --git a/pyomo/dae/integral.py b/pyomo/dae/integral.py index 34a34fdcd9c..41114296a93 100644 --- a/pyomo/dae/integral.py +++ b/pyomo/dae/integral.py @@ -21,8 +21,6 @@ from pyomo.dae.contset import ContinuousSet from pyomo.dae.diffvar import DAE_Error -__all__ = ('Integral',) - @ModelComponentFactory.register("Integral Expression in a DAE model.") class Integral(Expression): diff --git a/pyomo/dae/simulator.py b/pyomo/dae/simulator.py index f9121dbc0cc..72ba0c7331d 100644 --- a/pyomo/dae/simulator.py +++ b/pyomo/dae/simulator.py @@ -17,20 +17,14 @@ # the U.S. Government retains certain rights in this software. # This software is distributed under the BSD License. # _________________________________________________________________________ -from pyomo.core.base import Constraint, Param, value, Suffix, Block +import logging +from pyomo.core.base import Constraint, Param, value, Suffix, Block from pyomo.dae import ContinuousSet, DerivativeVar from pyomo.dae.diffvar import DAE_Error - import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import native_numeric_types from pyomo.core.expr.template_expr import IndexTemplate, _GetItemIndexer - -import logging - -__all__ = ('Simulator',) -logger = logging.getLogger('pyomo.core') - from pyomo.common.dependencies import ( numpy as np, numpy_available, @@ -39,6 +33,8 @@ attempt_import, ) +logger = logging.getLogger('pyomo.core') + casadi_intrinsic = {} diff --git a/pyomo/dataportal/DataPortal.py b/pyomo/dataportal/DataPortal.py index 24a9c847d48..457bb1aacee 100644 --- a/pyomo/dataportal/DataPortal.py +++ b/pyomo/dataportal/DataPortal.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['DataPortal'] - import logging from pyomo.common.log import is_debug_set from pyomo.dataportal.factory import DataManagerFactory, UnknownDataManager diff --git a/pyomo/dataportal/TableData.py b/pyomo/dataportal/TableData.py index b7fb98d596a..f1500d09f9b 100644 --- a/pyomo/dataportal/TableData.py +++ b/pyomo/dataportal/TableData.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['TableData'] - from pyomo.common.collections import Bunch from pyomo.dataportal.process_data import _process_data diff --git a/pyomo/dataportal/factory.py b/pyomo/dataportal/factory.py index e6424be25c4..479769137e2 100644 --- a/pyomo/dataportal/factory.py +++ b/pyomo/dataportal/factory.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['DataManagerFactory', 'UnknownDataManager'] - import logging from pyomo.common import Factory from pyomo.common.plugin_base import PluginError diff --git a/pyomo/dataportal/parse_datacmds.py b/pyomo/dataportal/parse_datacmds.py index d9f44405577..60e2f2c0acb 100644 --- a/pyomo/dataportal/parse_datacmds.py +++ b/pyomo/dataportal/parse_datacmds.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['parse_data_commands'] - import bisect import sys import logging diff --git a/pyomo/network/arc.py b/pyomo/network/arc.py index 1aa2b88edb6..42b7c6ea075 100644 --- a/pyomo/network/arc.py +++ b/pyomo/network/arc.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['Arc'] - from pyomo.network.port import Port from pyomo.core.base.component import ActiveComponentData, ModelComponentFactory from pyomo.core.base.indexed_component import ( diff --git a/pyomo/network/decomposition.py b/pyomo/network/decomposition.py index da7e8950395..1ffb6a710ff 100644 --- a/pyomo/network/decomposition.py +++ b/pyomo/network/decomposition.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['SequentialDecomposition'] - from pyomo.network import Port, Arc from pyomo.network.foqus_graph import FOQUSGraph from pyomo.core import ( diff --git a/pyomo/network/port.py b/pyomo/network/port.py index 63ed8b097b1..26822d4fee9 100644 --- a/pyomo/network/port.py +++ b/pyomo/network/port.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['Port'] - import logging, sys from weakref import ref as weakref_ref diff --git a/pyomo/opt/base/convert.py b/pyomo/opt/base/convert.py index a17d1914801..28ad6727d3e 100644 --- a/pyomo/opt/base/convert.py +++ b/pyomo/opt/base/convert.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['convert_problem'] - import copy import os diff --git a/pyomo/opt/base/formats.py b/pyomo/opt/base/formats.py index 72c4f5306a7..6e9d3958f48 100644 --- a/pyomo/opt/base/formats.py +++ b/pyomo/opt/base/formats.py @@ -9,11 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -# -# The formats that are supported by Pyomo -# -__all__ = ['ProblemFormat', 'ResultsFormat', 'guess_format'] - import enum diff --git a/pyomo/opt/base/problem.py b/pyomo/opt/base/problem.py index 02748e08b70..804a97e2e4c 100644 --- a/pyomo/opt/base/problem.py +++ b/pyomo/opt/base/problem.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ["AbstractProblemWriter", "WriterFactory", "BranchDirection"] - from pyomo.common import Factory diff --git a/pyomo/opt/base/results.py b/pyomo/opt/base/results.py index 8b00ec3e14e..ea295a66315 100644 --- a/pyomo/opt/base/results.py +++ b/pyomo/opt/base/results.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['AbstractResultsReader', 'ReaderFactory'] - from pyomo.common import Factory diff --git a/pyomo/opt/base/solvers.py b/pyomo/opt/base/solvers.py index cc49349142e..68e719e3862 100644 --- a/pyomo/opt/base/solvers.py +++ b/pyomo/opt/base/solvers.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ('OptSolver', 'SolverFactory', 'UnknownSolver', 'check_available_solvers') - import re import sys import time @@ -18,12 +16,11 @@ import shlex from pyomo.common import Factory -from pyomo.common.config import ConfigDict from pyomo.common.errors import ApplicationError from pyomo.common.collections import Bunch from pyomo.opt.base.convert import convert_problem -from pyomo.opt.base.formats import ResultsFormat, ProblemFormat +from pyomo.opt.base.formats import ResultsFormat import pyomo.opt.base.results logger = logging.getLogger('pyomo.opt') diff --git a/pyomo/opt/parallel/async_solver.py b/pyomo/opt/parallel/async_solver.py index d74206e4790..74e222e2241 100644 --- a/pyomo/opt/parallel/async_solver.py +++ b/pyomo/opt/parallel/async_solver.py @@ -9,9 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -__all__ = ['AsynchronousSolverManager', 'SolverManagerFactory'] - from pyomo.common import Factory from pyomo.opt.parallel.manager import AsynchronousActionManager diff --git a/pyomo/opt/parallel/local.py b/pyomo/opt/parallel/local.py index 211adf92e5c..e130ea0407f 100644 --- a/pyomo/opt/parallel/local.py +++ b/pyomo/opt/parallel/local.py @@ -9,9 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -__all__ = () - import time from pyomo.common.collections import OrderedDict diff --git a/pyomo/opt/parallel/manager.py b/pyomo/opt/parallel/manager.py index faa34d5190f..203c348e119 100644 --- a/pyomo/opt/parallel/manager.py +++ b/pyomo/opt/parallel/manager.py @@ -9,16 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -__all__ = [ - 'ActionManagerError', - 'ActionHandle', - 'AsynchronousActionManager', - 'ActionStatus', - 'FailedActionHandle', - 'solve_all_instances', -] - import enum diff --git a/pyomo/opt/problem/ampl.py b/pyomo/opt/problem/ampl.py index d128ec94930..ed107cace60 100644 --- a/pyomo/opt/problem/ampl.py +++ b/pyomo/opt/problem/ampl.py @@ -14,8 +14,6 @@ can be optimized with the Acro COLIN optimizers. """ -__all__ = ['AmplModel'] - import os from pyomo.opt.base import ProblemFormat, convert_problem, guess_format diff --git a/pyomo/opt/results/container.py b/pyomo/opt/results/container.py index 1cdf6fe77ce..ec4cb3c1c53 100644 --- a/pyomo/opt/results/container.py +++ b/pyomo/opt/results/container.py @@ -9,25 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = [ - 'UndefinedData', - 'undefined', - 'ignore', - 'ScalarData', - 'ListContainer', - 'MapContainer', - 'default_print_options', - 'ScalarType', -] - import copy - -from math import inf -from pyomo.common.collections import Bunch - import enum from io import StringIO +from math import inf +from pyomo.common.collections import Bunch class ScalarType(str, enum.Enum): int = 'int' diff --git a/pyomo/opt/results/problem.py b/pyomo/opt/results/problem.py index d39ba204aaf..98f749f3aeb 100644 --- a/pyomo/opt/results/problem.py +++ b/pyomo/opt/results/problem.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['ProblemInformation', 'ProblemSense'] - import enum from pyomo.opt.results.container import MapContainer diff --git a/pyomo/opt/results/results_.py b/pyomo/opt/results/results_.py index a9b802e2adb..0a045550517 100644 --- a/pyomo/opt/results/results_.py +++ b/pyomo/opt/results/results_.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['SolverResults'] - import math import sys import copy @@ -18,7 +16,7 @@ import logging import os.path -from pyomo.common.dependencies import yaml, yaml_load_args, yaml_available +from pyomo.common.dependencies import yaml, yaml_load_args import pyomo.opt from pyomo.opt.results.container import undefined, ignore, ListContainer, MapContainer import pyomo.opt.results.solution diff --git a/pyomo/opt/results/solution.py b/pyomo/opt/results/solution.py index 2862087cf43..6dcd348ea72 100644 --- a/pyomo/opt/results/solution.py +++ b/pyomo/opt/results/solution.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['SolutionStatus', 'Solution'] - import math import enum from pyomo.opt.results.container import MapContainer, ListContainer, ignore diff --git a/pyomo/opt/results/solver.py b/pyomo/opt/results/solver.py index e2d0cfff605..d4cf46c38a9 100644 --- a/pyomo/opt/results/solver.py +++ b/pyomo/opt/results/solver.py @@ -9,14 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = [ - 'SolverInformation', - 'SolverStatus', - 'TerminationCondition', - 'check_optimal_termination', - 'assert_optimal_termination', -] - import enum from pyomo.opt.results.container import MapContainer, ScalarType diff --git a/pyomo/opt/solver/ilmcmd.py b/pyomo/opt/solver/ilmcmd.py index efd1096c20f..c956b2ed42f 100644 --- a/pyomo/opt/solver/ilmcmd.py +++ b/pyomo/opt/solver/ilmcmd.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['ILMLicensedSystemCallSolver'] - import re import sys import os diff --git a/pyomo/opt/solver/shellcmd.py b/pyomo/opt/solver/shellcmd.py index 58274b572d3..94117779237 100644 --- a/pyomo/opt/solver/shellcmd.py +++ b/pyomo/opt/solver/shellcmd.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['SystemCallSolver'] - import os import sys import time diff --git a/pyomo/opt/testing/pyunit.py b/pyomo/opt/testing/pyunit.py index 9143714f4e3..bb96806d520 100644 --- a/pyomo/opt/testing/pyunit.py +++ b/pyomo/opt/testing/pyunit.py @@ -9,9 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -__all__ = ['TestCase'] - import sys import os import re diff --git a/pyomo/repn/beta/matrix.py b/pyomo/repn/beta/matrix.py index 741e54d380c..916b0daf755 100644 --- a/pyomo/repn/beta/matrix.py +++ b/pyomo/repn/beta/matrix.py @@ -9,12 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ( - "_LinearConstraintData", - "MatrixConstraint", - "compile_block_linear_constraints", -) - import time import logging import array diff --git a/pyomo/repn/plugins/ampl/ampl_.py b/pyomo/repn/plugins/ampl/ampl_.py index 4cc55cabd51..f422a085a3c 100644 --- a/pyomo/repn/plugins/ampl/ampl_.py +++ b/pyomo/repn/plugins/ampl/ampl_.py @@ -13,8 +13,6 @@ # AMPL Problem Writer Plugin # -__all__ = ['ProblemWriter_nl'] - import itertools import logging import operator diff --git a/pyomo/repn/standard_aux.py b/pyomo/repn/standard_aux.py index 628914780a6..403320c462c 100644 --- a/pyomo/repn/standard_aux.py +++ b/pyomo/repn/standard_aux.py @@ -10,9 +10,6 @@ # ___________________________________________________________________________ -__all__ = ['compute_standard_repn'] - - from pyomo.repn.standard_repn import ( preprocess_block_constraints, preprocess_block_objectives, diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index c1cca42afe4..8700872f04f 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -10,9 +10,6 @@ # ___________________________________________________________________________ -__all__ = ['StandardRepn', 'generate_standard_repn'] - - import sys import logging import itertools diff --git a/pyomo/scripting/convert.py b/pyomo/scripting/convert.py index 997e69ac7c9..20f9ef6d382 100644 --- a/pyomo/scripting/convert.py +++ b/pyomo/scripting/convert.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['pyomo2lp', 'pyomo2nl', 'pyomo2dakota'] - import os import sys diff --git a/pyomo/scripting/pyomo_parser.py b/pyomo/scripting/pyomo_parser.py index 09998085576..9294d46f85e 100644 --- a/pyomo/scripting/pyomo_parser.py +++ b/pyomo/scripting/pyomo_parser.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['add_subparser', 'get_parser', 'subparsers'] - import argparse import sys diff --git a/pyomo/solvers/plugins/solvers/CBCplugin.py b/pyomo/solvers/plugins/solvers/CBCplugin.py index 108b142a9e0..eb6c2c2e1bd 100644 --- a/pyomo/solvers/plugins/solvers/CBCplugin.py +++ b/pyomo/solvers/plugins/solvers/CBCplugin.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['CBC', 'MockCBC'] - import os import re import time diff --git a/pyomo/solvers/tests/solvers.py b/pyomo/solvers/tests/solvers.py index e67df47a0b0..918a801ae37 100644 --- a/pyomo/solvers/tests/solvers.py +++ b/pyomo/solvers/tests/solvers.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -__all__ = ['test_solver_cases'] - import logging from pyomo.common.collections import Bunch diff --git a/pyomo/util/blockutil.py b/pyomo/util/blockutil.py index 56cc4266017..9f043e64ab7 100644 --- a/pyomo/util/blockutil.py +++ b/pyomo/util/blockutil.py @@ -12,8 +12,6 @@ # the purpose of this file is to collect all utility methods that compute # attributes of blocks, based on their contents. -__all__ = ['has_discrete_variables'] - import logging from pyomo.core import Var, Constraint, TraversalStrategy From db5fdf02d7e876ae3bd690ab85c032ebf9b46d26 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 16 Feb 2024 14:53:59 -0700 Subject: [PATCH 0590/3044] Fix import; apply black --- pyomo/core/expr/numvalue.py | 1 + pyomo/core/util.py | 5 +---- pyomo/opt/results/container.py | 1 + 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index f3ea76c305c..3a4359af2f9 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -32,6 +32,7 @@ check_if_numeric_type, value, ) +from pyomo.core.pyomoobject import PyomoObject relocated_module_attribute( 'native_boolean_types', diff --git a/pyomo/core/util.py b/pyomo/core/util.py index e4a70aea05a..f337b487cef 100644 --- a/pyomo/core/util.py +++ b/pyomo/core/util.py @@ -15,10 +15,7 @@ from pyomo.common.deprecation import deprecation_warning from pyomo.core.expr.numvalue import native_numeric_types -from pyomo.core.expr.numeric_expr import ( - mutable_expression, - NPV_SumExpression, -) +from pyomo.core.expr.numeric_expr import mutable_expression, NPV_SumExpression from pyomo.core.base.var import Var from pyomo.core.base.expression import Expression from pyomo.core.base.component import _ComponentBase diff --git a/pyomo/opt/results/container.py b/pyomo/opt/results/container.py index ec4cb3c1c53..4bbaf44edf7 100644 --- a/pyomo/opt/results/container.py +++ b/pyomo/opt/results/container.py @@ -16,6 +16,7 @@ from pyomo.common.collections import Bunch + class ScalarType(str, enum.Enum): int = 'int' time = 'time' From ec7c8e6a3b417b42880d36528c0d75462594497b Mon Sep 17 00:00:00 2001 From: lukasbiton Date: Fri, 16 Feb 2024 22:12:45 +0000 Subject: [PATCH 0591/3044] error msg more explicit wrt different interfaces --- pyomo/contrib/appsi/solvers/highs.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index a9a23682355..3612b9d5014 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -680,9 +680,11 @@ def _postsolve(self, timer: HierarchicalTimer): self.load_vars() else: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Highs interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) timer.stop('load solution') From 5b74de426559b5c39876e1de38e2721a9e997d92 Mon Sep 17 00:00:00 2001 From: lukasbiton Date: Fri, 16 Feb 2024 22:21:40 +0000 Subject: [PATCH 0592/3044] align new error message for all appsi solvers --- pyomo/contrib/appsi/solvers/cbc.py | 8 +++++--- pyomo/contrib/appsi/solvers/cplex.py | 8 +++++--- pyomo/contrib/appsi/solvers/gurobi.py | 8 +++++--- pyomo/contrib/appsi/solvers/ipopt.py | 8 +++++--- pyomo/contrib/appsi/solvers/wntr.py | 8 +++++--- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 2c522af864d..7f04ffbfce7 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -411,9 +411,11 @@ def _check_and_escape_options(): if cp.returncode != 0: if self.config.load_solution: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Cbc interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) results = Results() diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 1d7147f16e8..1b7ab5000d2 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -341,9 +341,11 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): if config.load_solution: if cpxprob.solution.get_solution_type() == cpxprob.solution.type.none: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loades. ' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Cplex interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) else: diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index aa233ef77d6..1e18862e3bd 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -946,9 +946,11 @@ def _postsolve(self, timer: HierarchicalTimer): self.load_vars() else: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Gurobi interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) timer.stop('load solution') diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index d7a786e6c2c..29e74f81c98 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -421,9 +421,11 @@ def _parse_sol(self): results.best_feasible_objective = value(obj_expr_evaluated) elif self.config.load_solution: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Ipopt interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index e1835b810b0..00c0598c687 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -169,9 +169,11 @@ def _solve(self, timer: HierarchicalTimer): timer.stop('load solution') else: raise RuntimeError( - 'A feasible solution was not found, so no solution can be loaded.' - 'Please set opt.config.load_solution=False and check ' - 'results.termination_condition and ' + 'A feasible solution was not found, so no solution can be loaded. ' + 'If using the appsi.solvers.Wntr interface, you can ' + 'set opt.config.load_solution=False. If using the environ.SolverFactory ' + 'interface, you can set opt.solve(model, load_solutions = False). ' + 'Then you can check results.termination_condition and ' 'results.best_feasible_objective before loading a solution.' ) return results From 120c9a4c8fea118e6fbf5a1a9ace5b93f6ce127f Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 16 Feb 2024 17:22:03 -0500 Subject: [PATCH 0593/3044] Incorporate updated interfaces of `common.config` --- pyomo/contrib/pyros/config.py | 59 +---------- pyomo/contrib/pyros/tests/test_config.py | 119 ----------------------- pyomo/contrib/pyros/uncertainty_sets.py | 42 -------- 3 files changed, 4 insertions(+), 216 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 749152f234c..a7ca41d095f 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -4,13 +4,13 @@ from collections.abc import Iterable import logging -import os from pyomo.common.collections import ComponentSet from pyomo.common.config import ( ConfigDict, ConfigValue, In, + IsInstance, NonNegativeFloat, InEnum, Path, @@ -20,7 +20,7 @@ from pyomo.core.base.param import Param, _ParamData from pyomo.opt import SolverFactory from pyomo.contrib.pyros.util import ObjectiveType, setup_pyros_logger -from pyomo.contrib.pyros.uncertainty_sets import UncertaintySetDomain +from pyomo.contrib.pyros.uncertainty_sets import UncertaintySet default_pyros_solver_logger = setup_pyros_logger() @@ -93,57 +93,6 @@ def domain_name(self): return "positive int or -1" -class PathLikeOrNone: - """ - Validator for path-like objects. - - This interface is a wrapper around the domain validator - ``common.config.Path``, and extends the domain of interest to - to include: - - None - - objects following the Python ``os.PathLike`` protocol. - - Parameters - ---------- - **config_path_kwargs : dict - Keyword arguments to ``common.config.Path``. - """ - - def __init__(self, **config_path_kwargs): - """Initialize self (see class docstring).""" - self.config_path = Path(**config_path_kwargs) - - def __call__(self, path): - """ - Cast path to expanded string representation. - - Parameters - ---------- - path : None str, bytes, or path-like - Object to be cast. - - Returns - ------- - None - If obj is None. - str - String representation of path-like object. - """ - if path is None: - return path - - # prevent common.config.Path from invoking - # str() on the path-like object - path_str = os.fsdecode(path) - - # standardize path str as necessary - return self.config_path(path_str) - - def domain_name(self): - """Return str briefly describing domain encompassed by self.""" - return "str, bytes, path-like or None" - - def mutable_param_validator(param_obj): """ Check that Param-like object has attribute `mutable=True`. @@ -637,7 +586,7 @@ def pyros_config(): "uncertainty_set", ConfigValue( default=None, - domain=UncertaintySetDomain(), + domain=IsInstance(UncertaintySet), description=( """ Uncertainty set against which the @@ -871,7 +820,7 @@ def pyros_config(): "subproblem_file_directory", ConfigValue( default=None, - domain=PathLikeOrNone(), + domain=Path(), description=( """ Directory to which to export subproblems not successfully diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index cc6fde225f3..76b9114b9e6 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -3,11 +3,9 @@ """ import logging -import os import unittest from pyomo.core.base import ConcreteModel, Var, _VarData -from pyomo.common.config import Path from pyomo.common.log import LoggingIntercept from pyomo.common.errors import ApplicationError from pyomo.core.base.param import Param, _ParamData @@ -16,12 +14,10 @@ mutable_param_validator, LoggerType, SolverNotResolvable, - PathLikeOrNone, PositiveIntOrMinusOne, pyros_config, SolverIterable, SolverResolvable, - UncertaintySetDomain, ) from pyomo.contrib.pyros.util import ObjectiveType from pyomo.opt import SolverFactory, SolverResults @@ -275,34 +271,6 @@ def test_standardizer_valid_mutable_params(self): ) -class TestUncertaintySetDomain(unittest.TestCase): - """ - Test domain validator for uncertainty set arguments. - """ - - @unittest.skipUnless(numpy_available, "Numpy is not available.") - def test_uncertainty_set_domain_valid_set(self): - """ - Test validator works for valid argument. - """ - standardizer_func = UncertaintySetDomain() - bset = BoxSet([[0, 1]]) - self.assertIs( - bset, - standardizer_func(bset), - msg="Output of uncertainty set domain not as expected.", - ) - - def test_uncertainty_set_domain_invalid_type(self): - """ - Test validator works for valid argument. - """ - standardizer_func = UncertaintySetDomain() - exc_str = "Expected an .*UncertaintySet object.*received object 2" - with self.assertRaisesRegex(ValueError, exc_str): - standardizer_func(2) - - AVAILABLE_SOLVER_TYPE_NAME = "available_pyros_test_solver" @@ -580,93 +548,6 @@ def test_config_objective_focus(self): config.objective_focus = invalid_focus -class TestPathLikeOrNone(unittest.TestCase): - """ - Test interface for validating path-like arguments. - """ - - def test_none_valid(self): - """ - Test `None` is valid. - """ - standardizer_func = PathLikeOrNone() - - self.assertIs( - standardizer_func(None), - None, - msg="Output of `PathLikeOrNone` standardizer not as expected.", - ) - - def test_str_bytes_path_like_valid(self): - """ - Check path-like validator handles str, bytes, and path-like - inputs correctly. - """ - - class ExamplePathLike(os.PathLike): - """ - Path-like class for testing. Key feature: __fspath__ - and __str__ return different outputs. - """ - - def __init__(self, path_str_or_bytes): - self.path = path_str_or_bytes - - def __fspath__(self): - return self.path - - def __str__(self): - path_str = os.fsdecode(self.path) - return f"{type(self).__name__}({path_str})" - - path_standardization_func = PathLikeOrNone() - - # construct path arguments of different type - path_as_str = "example_output_dir/" - path_as_bytes = os.fsencode(path_as_str) - path_like_from_str = ExamplePathLike(path_as_str) - path_like_from_bytes = ExamplePathLike(path_as_bytes) - - # for all possible arguments, output should be - # the str returned by ``common.config.Path`` when - # string representation of the path is input. - expected_output = Path()(path_as_str) - - # check output is as expected in all cases - self.assertEqual( - path_standardization_func(path_as_str), - expected_output, - msg=( - "Path-like validator output from str input " - "does not match expected value." - ), - ) - self.assertEqual( - path_standardization_func(path_as_bytes), - expected_output, - msg=( - "Path-like validator output from bytes input " - "does not match expected value." - ), - ) - self.assertEqual( - path_standardization_func(path_like_from_str), - expected_output, - msg=( - "Path-like validator output from path-like input " - "derived from str does not match expected value." - ), - ) - self.assertEqual( - path_standardization_func(path_like_from_bytes), - expected_output, - msg=( - "Path-like validator output from path-like input " - "derived from bytes does not match expected value." - ), - ) - - class TestPositiveIntOrMinusOne(unittest.TestCase): """ Test validator for -1 or positive int works as expected. diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 179f986fdac..028a9f38da1 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -283,48 +283,6 @@ def generate_shape_str(shape, required_shape): ) -class UncertaintySetDomain: - """ - Domain validator for uncertainty set argument. - """ - - def __call__(self, obj): - """ - Type validate uncertainty set object. - - Parameters - ---------- - obj : object - Object to validate. - - Returns - ------- - obj : object - Object that was passed, provided type validation successful. - - Raises - ------ - ValueError - If type validation failed. - """ - if not isinstance(obj, UncertaintySet): - raise ValueError( - f"Expected an {UncertaintySet.__name__} object, " - f"instead received object {obj}" - ) - return obj - - def domain_name(self): - """ - Domain name of self. - """ - return UncertaintySet.__name__ - - -# maintain compatibility with prior versions -uncertainty_sets = UncertaintySetDomain() - - def column(matrix, i): # Get column i of a given multi-dimensional list return [row[i] for row in matrix] From d8b0ba354cb14d3b538dbcdbc41860b694d3afba Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 16 Feb 2024 15:42:23 -0700 Subject: [PATCH 0594/3044] bug --- pyomo/contrib/solver/gurobi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index c1b02c08ef9..c55565e20fb 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -239,7 +239,7 @@ class Gurobi(PersistentSolverUtils, PersistentSolverBase): def __init__(self, **kwds): PersistentSolverUtils.__init__(self) PersistentSolverBase.__init__(self, **kwds) - self._num_instances += 1 + Gurobi._num_instances += 1 self._solver_model = None self._symbol_map = SymbolMap() self._labeler = None @@ -310,8 +310,8 @@ def release_license(self): def __del__(self): if not python_is_shutting_down(): - self._num_instances -= 1 - if self._num_instances == 0: + Gurobi._num_instances -= 1 + if Gurobi._num_instances == 0: self.release_license() def version(self): From 350c3c37fa61691f250199ed8574bacd34fb569c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sat, 17 Feb 2024 13:19:21 -0700 Subject: [PATCH 0595/3044] Working rewrite of hull that handles nested correctly, many changes to tests because of this --- pyomo/gdp/plugins/hull.py | 386 ++++++++++++++++---------------- pyomo/gdp/tests/common_tests.py | 69 ++++-- pyomo/gdp/tests/models.py | 2 +- pyomo/gdp/tests/test_hull.py | 212 ++++++++++-------- 4 files changed, 366 insertions(+), 303 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index c7a005bb4ea..e880d599366 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -50,6 +50,7 @@ _warn_for_active_disjunct, ) from pyomo.core.util import target_list +from pyomo.util.vars_from_expressions import get_vars_from_components from weakref import ref as weakref_ref logger = logging.getLogger('pyomo.gdp.hull') @@ -224,10 +225,11 @@ def _get_user_defined_local_vars(self, targets): if t.ctype is Disjunct or isinstance(t, _DisjunctData): # first look beneath where we are (there could be Blocks on this # disjunct) - for b in t.component_data_objects(Block, descend_into=Block, - active=True, - sort=SortComponents.deterministic - ): + for b in t.component_data_objects( + Block, descend_into=Block, + active=True, + sort=SortComponents.deterministic + ): if b not in seen_blocks: self._collect_local_vars_from_block(b, user_defined_local_vars) seen_blocks.add(b) @@ -282,6 +284,7 @@ def _apply_to_impl(self, instance, **kwds): # nested GDPs, we will introduce variables that need disaggregating into # parent Disjuncts as we transform their child Disjunctions. preprocessed_targets = gdp_tree.reverse_topological_sort() + # Get all LocalVars from Suffixes ahead of time local_vars_by_disjunct = self._get_user_defined_local_vars( preprocessed_targets) @@ -303,15 +306,7 @@ def _add_transformation_block(self, to_block): return transBlock, new_block transBlock.lbub = Set(initialize=['lb', 'ub', 'eq']) - # Map between disaggregated variables and their - # originals - transBlock._disaggregatedVarMap = { - 'srcVar': ComponentMap(), - 'disaggregatedVar': ComponentMap(), - } - # Map between disaggregated variables and their lb*indicator <= var <= - # ub*indicator constraints - transBlock._bigMConstraintMap = ComponentMap() + # We will store all of the disaggregation constraints for any # Disjunctions we transform onto this block here. transBlock.disaggregationConstraints = Constraint(NonNegativeIntegers) @@ -329,6 +324,7 @@ def _add_transformation_block(self, to_block): def _transform_disjunctionData(self, obj, index, parent_disjunct, local_vars_by_disjunct): + print("Transforming Disjunction %s" % obj) # Hull reformulation doesn't work if this is an OR constraint. So if # xor is false, give up if not obj.xor: @@ -338,8 +334,8 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, "Must be an XOR!" % obj.name ) # collect the Disjuncts we are going to transform now because we will - # change their active status when we transform them, but still need this - # list after the fact. + # change their active status when we transform them, but we still need + # this list after the fact. active_disjuncts = [disj for disj in obj.disjuncts if disj.active] # We put *all* transformed things on the parent Block of this @@ -357,19 +353,23 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # We first go through and collect all the variables that we are going to # disaggregate. We do this in its own pass because we want to know all - # the Disjuncts that each Var appears in. + # the Disjuncts that each Var appears in since that will tell us exactly + # which diaggregated variables we need. var_order = ComponentSet() disjuncts_var_appears_in = ComponentMap() + # For each disjunct in the disjunction, we will store a list of Vars + # that need a disaggregated counterpart in that disjunct. + disjunct_disaggregated_var_map = {} for disjunct in active_disjuncts: # create the key for each disjunct now - transBlock._disaggregatedVarMap['disaggregatedVar'][ - disjunct - ] = ComponentMap() - for cons in disjunct.component_data_objects( - Constraint, - active=True, - sort=SortComponents.deterministic, - descend_into=Block, + disjunct_disaggregated_var_map[disjunct] = ComponentMap() + for var in get_vars_from_components( + disjunct, + Constraint, + include_fixed=not self._config.assume_fixed_vars_permanent, + active=True, + sort=SortComponents.deterministic, + descend_into=Block ): # [ESJ 02/14/2020] By default, we disaggregate fixed variables # on the philosophy that fixing is not a promise for the future @@ -378,21 +378,20 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # with their transformed model. However, the user may have set # assume_fixed_vars_permanent to True in which case we will skip # them - for var in EXPR.identify_variables( - cons.body, include_fixed=not - self._config.assume_fixed_vars_permanent): - # Note that, because ComponentSets are ordered, we will - # eventually disaggregate the vars in a deterministic order - # (the order that we found them) - if var not in var_order: - var_order.add(var) - disjuncts_var_appears_in[var] = ComponentSet([disjunct]) - else: - disjuncts_var_appears_in[var].add(disjunct) + + # Note that, because ComponentSets are ordered, we will + # eventually disaggregate the vars in a deterministic order + # (the order that we found them) + if var not in var_order: + var_order.add(var) + disjuncts_var_appears_in[var] = ComponentSet([disjunct]) + else: + disjuncts_var_appears_in[var].add(disjunct) - # We will disaggregate all variables that are not explicitly declared as - # being local. We have marked our own disaggregated variables as local, - # so they will not be re-disaggregated. + # Now, we will disaggregate all variables that are not explicitly + # declared as being local. If we are moving up in a nested tree, we have + # marked our own disaggregated variables as local, so they will not be + # re-disaggregated. vars_to_disaggregate = {disj: ComponentSet() for disj in obj.disjuncts} all_vars_to_disaggregate = ComponentSet() # We will ignore variables declared as local in a Disjunct that don't @@ -407,27 +406,22 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, if self._generate_debug_messages: logger.debug( "Assuming '%s' is not a local var since it is" - "used in multiple disjuncts." - % var.getname(fully_qualified=True) + "used in multiple disjuncts." % var.name ) for disj in disjuncts: vars_to_disaggregate[disj].add(var) all_vars_to_disaggregate.add(var) - else: # disjuncts is a set of length 1 + else: # var only appears in one disjunct disjunct = next(iter(disjuncts)) + # We check if the user declared it as local if disjunct in local_vars_by_disjunct: if var in local_vars_by_disjunct[disjunct]: local_vars[disjunct].add(var) - else: - # It's not declared local to this Disjunct, so we - # disaggregate - vars_to_disaggregate[disjunct].add(var) - all_vars_to_disaggregate.add(var) - else: - # The user didn't declare any local vars for this - # Disjunct, so we know we're disaggregating it - vars_to_disaggregate[disjunct].add(var) - all_vars_to_disaggregate.add(var) + continue + # It's not declared local to this Disjunct, so we + # disaggregate + vars_to_disaggregate[disjunct].add(var) + all_vars_to_disaggregate.add(var) # Now that we know who we need to disaggregate, we will do it # while we also transform the disjuncts. @@ -438,22 +432,22 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, or_expr = 0 for disjunct in obj.disjuncts: or_expr += disjunct.indicator_var.get_associated_binary() - if obj.active: + if disjunct.active: self._transform_disjunct( - disjunct, - transBlock, - vars_to_disaggregate[disjunct], - local_vars[disjunct], - parent_local_var_list, - local_vars_by_disjunct[parent_disjunct] + obj=disjunct, + transBlock=transBlock, + vars_to_disaggregate=vars_to_disaggregate[disjunct], + local_vars=local_vars[disjunct], + parent_local_var_suffix=parent_local_var_list, + parent_disjunct_local_vars=local_vars_by_disjunct[parent_disjunct], + disjunct_disaggregated_var_map=disjunct_disaggregated_var_map ) xorConstraint.add(index, (or_expr, 1)) # map the DisjunctionData to its XOR constraint to mark it as # transformed obj._algebraic_constraint = weakref_ref(xorConstraint[index]) - # add the reaggregation constraints - i = 0 + # Now add the reaggregation constraints for var in all_vars_to_disaggregate: # There are two cases here: Either the var appeared in every # disjunct in the disjunction, or it didn't. If it did, there's @@ -465,8 +459,9 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # create one more disaggregated var idx = len(disaggregatedVars) disaggregated_var = disaggregatedVars[idx] - # mark this as local because we won't re-disaggregate if this is - # a nested disjunction + print("Creating extra disaggregated var: '%s'" % disaggregated_var) + # mark this as local because we won't re-disaggregate it if this + # is a nested disjunction if parent_local_var_list is not None: parent_local_var_list.append(disaggregated_var) local_vars_by_disjunct[parent_disjunct].add(disaggregated_var) @@ -475,14 +470,34 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, for disj in disjuncts_var_appears_in[var] ) self._declare_disaggregated_var_bounds( - var, - disaggregated_var, - obj, - disaggregated_var_bounds, - (idx, 'lb'), - (idx, 'ub'), - var_free, + original_var=var, + disaggregatedVar=disaggregated_var, + disjunct=obj, + bigmConstraint=disaggregated_var_bounds, + lb_idx=(idx, 'lb'), + ub_idx=(idx, 'ub'), + var_free_indicator=var_free + ) + # Update mappings: + var_info = var.parent_block().private_data() + if 'disaggregated_var_map' not in var_info: + var_info['disaggregated_var_map'] = ComponentMap() + disaggregated_var_map = var_info['disaggregated_var_map'] + dis_var_info = disaggregated_var.parent_block().private_data() + if 'original_var_map' not in dis_var_info: + dis_var_info['original_var_map'] = ComponentMap() + original_var_map = dis_var_info['original_var_map'] + if 'bigm_constraint_map' not in dis_var_info: + dis_var_info['bigm_constraint_map'] = ComponentMap() + bigm_constraint_map = dis_var_info['bigm_constraint_map'] + + if disaggregated_var not in bigm_constraint_map: + bigm_constraint_map[disaggregated_var] = {} + bigm_constraint_map[disaggregated_var][obj] = ( + Reference(disaggregated_var_bounds[idx, :]) ) + original_var_map[disaggregated_var] = var + # For every Disjunct the Var does not appear in, we want to map # that this new variable is its disaggreggated variable. for disj in active_disjuncts: @@ -493,38 +508,28 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, disj._transformation_block is not None and disj not in disjuncts_var_appears_in[var] ): - relaxationBlock = disj._transformation_block().parent_block() - relaxationBlock._bigMConstraintMap[disaggregated_var] = ( - Reference(disaggregated_var_bounds[idx, :]) - ) - relaxationBlock._disaggregatedVarMap['srcVar'][ - disaggregated_var - ] = var - relaxationBlock._disaggregatedVarMap[ - 'disaggregatedVar'][disj][ - var - ] = disaggregated_var + if not disj in disaggregated_var_map: + disaggregated_var_map[disj] = ComponentMap() + disaggregated_var_map[disj][var] = disaggregated_var + # start the expression for the reaggregation constraint with + # this var disaggregatedExpr = disaggregated_var else: disaggregatedExpr = 0 for disjunct in disjuncts_var_appears_in[var]: - # We know this Disjunct was active, so it has been transformed now. - disaggregatedVar = ( - disjunct._transformation_block() - .parent_block() - ._disaggregatedVarMap['disaggregatedVar'][disjunct][var] - ) - disaggregatedExpr += disaggregatedVar + disaggregatedExpr += disjunct_disaggregated_var_map[disjunct][var] cons_idx = len(disaggregationConstraint) # We always aggregate to the original var. If this is nested, this - # constraint will be transformed again. + # constraint will be transformed again. (And if it turns out + # everything in it is local, then that transformation won't actually + # change the mathematical expression, so it's okay. disaggregationConstraint.add(cons_idx, var == disaggregatedExpr) # and update the map so that we can find this later. We index by # variable and the particular disjunction because there is a # different one for each disjunction - if disaggregationConstraintMap.get(var) is not None: + if var in disaggregationConstraintMap: disaggregationConstraintMap[var][obj] = disaggregationConstraint[ cons_idx ] @@ -532,14 +537,13 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, thismap = disaggregationConstraintMap[var] = ComponentMap() thismap[obj] = disaggregationConstraint[cons_idx] - i += 1 - # deactivate for the writers obj.deactivate() def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, - parent_local_var_suffix, parent_disjunct_local_vars): - print("\nTransforming '%s'" % obj.name) + parent_local_var_suffix, parent_disjunct_local_vars, + disjunct_disaggregated_var_map): + print("\nTransforming Disjunct '%s'" % obj.name) relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) # Put the disaggregated variables all on their own block so that we can @@ -565,7 +569,7 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, if parent_local_var_suffix is not None: parent_local_var_suffix.append(disaggregatedVar) # Record that it's local for our own bookkeeping in case we're in a - # nested situation in *this* transformation + # nested tree in *this* transformation parent_disjunct_local_vars.add(disaggregatedVar) # add the bigm constraint @@ -574,17 +578,26 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, disaggregatedVarName + "_bounds", bigmConstraint ) - print("Adding bounds constraints for '%s'" % var) + print("Adding bounds constraints for '%s', the disaggregated var " + "corresponding to Var '%s' on Disjunct '%s'" % + (disaggregatedVar, var, obj)) self._declare_disaggregated_var_bounds( - var, - disaggregatedVar, - obj, - bigmConstraint, - 'lb', - 'ub', - obj.indicator_var.get_associated_binary(), - transBlock, + original_var=var, + disaggregatedVar=disaggregatedVar, + disjunct=obj, + bigmConstraint=bigmConstraint, + lb_idx='lb', + ub_idx='ub', + var_free_indicator=obj.indicator_var.get_associated_binary(), ) + # update the bigm constraint mappings + data_dict = disaggregatedVar.parent_block().private_data() + if 'bigm_constraint_map' not in data_dict: + data_dict['bigm_constraint_map'] = ComponentMap() + if disaggregatedVar not in data_dict['bigm_constraint_map']: + data_dict['bigm_constraint_map'][disaggregatedVar] = {} + data_dict['bigm_constraint_map'][disaggregatedVar][obj] = bigmConstraint + disjunct_disaggregated_var_map[obj][var] = disaggregatedVar for var in local_vars: # we don't need to disaggregate, i.e., we can use this Var, but we @@ -600,35 +613,30 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, relaxationBlock.add_component(conName, bigmConstraint) parent_block = var.parent_block() - disaggregated_var_map = self._get_disaggregated_var_map(parent_block) print("Adding bounds constraints for local var '%s'" % var) - # TODO: This gets mapped in a place where we can't find it if we ask - # for it from the local var itself. self._declare_disaggregated_var_bounds( - var, - var, - obj, - bigmConstraint, - 'lb', - 'ub', - obj.indicator_var.get_associated_binary(), - disaggregated_var_map, + original_var=var, + disaggregatedVar=var, + disjunct=obj, + bigmConstraint=bigmConstraint, + lb_idx='lb', + ub_idx='ub', + var_free_indicator=obj.indicator_var.get_associated_binary(), ) - - var_substitute_map = dict( - (id(v), newV) - for v, newV in transBlock._disaggregatedVarMap['disaggregatedVar'][ - obj - ].items() - ) - zero_substitute_map = dict( - (id(v), ZeroConstant) - for v, newV in transBlock._disaggregatedVarMap['disaggregatedVar'][ - obj - ].items() - ) - zero_substitute_map.update((id(v), ZeroConstant) for v in local_vars) + # update the bigm constraint mappings + data_dict = var.parent_block().private_data() + if 'bigm_constraint_map' not in data_dict: + data_dict['bigm_constraint_map'] = ComponentMap() + if var not in data_dict['bigm_constraint_map']: + data_dict['bigm_constraint_map'][var] = {} + data_dict['bigm_constraint_map'][var][obj] = bigmConstraint + disjunct_disaggregated_var_map[obj][var] = var + + var_substitute_map = dict((id(v), newV) for v, newV in + disjunct_disaggregated_var_map[obj].items() ) + zero_substitute_map = dict((id(v), ZeroConstant) for v, newV in + disjunct_disaggregated_var_map[obj].items() ) # Transform each component within this disjunct self._transform_block_components( @@ -650,7 +658,6 @@ def _declare_disaggregated_var_bounds( lb_idx, ub_idx, var_free_indicator, - disaggregated_var_map, ): lb = original_var.lb ub = original_var.ub @@ -669,19 +676,24 @@ def _declare_disaggregated_var_bounds( if ub: bigmConstraint.add(ub_idx, disaggregatedVar <= ub * var_free_indicator) + original_var_info = original_var.parent_block().private_data() + if 'disaggregated_var_map' not in original_var_info: + original_var_info['disaggregated_var_map'] = ComponentMap() + disaggregated_var_map = original_var_info['disaggregated_var_map'] + + disaggregated_var_info = disaggregatedVar.parent_block().private_data() + if 'original_var_map' not in disaggregated_var_info: + disaggregated_var_info['original_var_map'] = ComponentMap() + original_var_map = disaggregated_var_info['original_var_map'] + # store the mappings from variables to their disaggregated selves on # the transformation block - disaggregated_var_map['disaggregatedVar'][disjunct][ - original_var] = disaggregatedVar - disaggregated_var_map['srcVar'][disaggregatedVar] = original_var - bigMConstraintMap[disaggregatedVar] = bigmConstraint - - # if transBlock is not None: - # transBlock._disaggregatedVarMap['disaggregatedVar'][disjunct][ - # original_var - # ] = disaggregatedVar - # transBlock._disaggregatedVarMap['srcVar'][disaggregatedVar] = original_var - # transBlock._bigMConstraintMap[disaggregatedVar] = bigmConstraint + if disjunct not in disaggregated_var_map: + disaggregated_var_map[disjunct] = ComponentMap() + print("DISAGGREGATED VAR MAP (%s, %s) : %s" % (disjunct, original_var, + disaggregatedVar)) + disaggregated_var_map[disjunct][original_var] = disaggregatedVar + original_var_map[disaggregatedVar] = original_var def _get_local_var_list(self, parent_disjunct): # Add or retrieve Suffix from parent_disjunct so that, if this is @@ -903,17 +915,19 @@ def get_disaggregated_var(self, v, disjunct, raise_exception=True): """ if disjunct._transformation_block is None: raise GDP_Error("Disjunct '%s' has not been transformed" % disjunct.name) - transBlock = disjunct._transformation_block().parent_block() - try: - return transBlock._disaggregatedVarMap['disaggregatedVar'][disjunct][v] - except: - if raise_exception: - logger.error( - "It does not appear '%s' is a " - "variable that appears in disjunct '%s'" % (v.name, disjunct.name) - ) - raise - return none + msg = ("It does not appear '%s' is a " + "variable that appears in disjunct '%s'" % (v.name, disjunct.name)) + var_map = v.parent_block().private_data() + if 'disaggregated_var_map' in var_map: + try: + return var_map['disaggregated_var_map'][disjunct][v] + except: + if raise_exception: + logger.error(msg) + raise + elif raise_exception: + raise GDP_Error(msg) + return None def get_src_var(self, disaggregated_var): """ @@ -927,23 +941,13 @@ def get_src_var(self, disaggregated_var): (and so appears on a transformation block of some Disjunct) """ - msg = ( + var_map = disaggregated_var.parent_block().private_data() + if 'original_var_map' in var_map: + if disaggregated_var in var_map['original_var_map']: + return var_map['original_var_map'][disaggregated_var] + raise GDP_Error( "'%s' does not appear to be a " - "disaggregated variable" % disaggregated_var.name - ) - # We always put a dictionary called '_disaggregatedVarMap' on the parent - # block of the variable. If it's not there, then this probably isn't a - # disaggregated Var (or if it is it's a developer error). Similarly, if - # the var isn't in the dictionary, if we're doing what we should, then - # it's not a disaggregated var. - transBlock = disaggregated_var.parent_block() - if not hasattr(transBlock, '_disaggregatedVarMap'): - raise GDP_Error(msg) - try: - return transBlock._disaggregatedVarMap['srcVar'][disaggregated_var] - except: - logger.error(msg) - raise + "disaggregated variable" % disaggregated_var.name) # retrieves the disaggregation constraint for original_var resulting from # transforming disjunction @@ -990,7 +994,7 @@ def get_disaggregation_constraint(self, original_var, disjunction, cons = self.get_transformed_constraints(cons)[0] return cons - def get_var_bounds_constraint(self, v): + def get_var_bounds_constraint(self, v, disjunct=None): """ Returns the IndexedConstraint which sets a disaggregated variable to be within its bounds when its Disjunct is active and to @@ -1002,36 +1006,32 @@ def get_var_bounds_constraint(self, v): v: a Var that was created by the hull transformation as a disaggregated variable (and so appears on a transformation block of some Disjunct) + disjunct: (For nested Disjunctions) Which Disjunct in the + hierarchy the bounds Constraint should correspond to. + Optional since for non-nested models this can be inferred. """ - msg = ( + info = v.parent_block().private_data() + if 'bigm_constraint_map' in info: + if v in info['bigm_constraint_map']: + if len(info['bigm_constraint_map'][v]) == 1: + # Not nested, or it's at the top layer, so we're fine. + return list(info['bigm_constraint_map'][v].values())[0] + elif disjunct is not None: + # This is nested, so we need to walk up to find the active ones + return info['bigm_constraint_map'][v][disjunct] + else: + raise ValueError( + "It appears that the variable '%s' appears " + "within a nested GDP hierarchy, and no " + "'disjunct' argument was specified. Please " + "specify for which Disjunct the bounds " + "constraint for '%s' should be returned." + % (v, v)) + raise GDP_Error( "Either '%s' is not a disaggregated variable, or " "the disjunction that disaggregates it has not " "been properly transformed." % v.name ) - # This can only go well if v is a disaggregated var - transBlock = v.parent_block() - if not hasattr(transBlock, '_bigMConstraintMap'): - try: - transBlock = transBlock.parent_block().parent_block() - except: - logger.error(msg) - raise - try: - cons = transBlock._bigMConstraintMap[v] - except: - logger.error(msg) - raise - transformed_cons = {key: con for key, con in cons.items()} - def is_active(cons): - return all(c.active for c in cons.values()) - while not is_active(transformed_cons): - if 'lb' in transformed_cons: - transformed_cons['lb'] = self.get_transformed_constraints( - transformed_cons['lb'])[0] - if 'ub' in transformed_cons: - transformed_cons['ub'] = self.get_transformed_constraints( - transformed_cons['ub'])[0] - return transformed_cons def get_transformed_constraints(self, cons): cons = super().get_transformed_constraints(cons) diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index c5e750e4f08..bef05a78cf6 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -697,32 +697,29 @@ def check_indexedDisj_only_targets_transformed(self, transformation): trans.get_transformed_constraints(m.disjunct1[1, 0].c)[0] .parent_block() .parent_block(), - disjBlock[2], + disjBlock[0], ) self.assertIs( trans.get_transformed_constraints(m.disjunct1[1, 1].c)[0].parent_block(), - disjBlock[3], + disjBlock[1], ) # In the disaggregated var bounds self.assertIs( trans.get_transformed_constraints(m.disjunct1[2, 0].c)[0] .parent_block() .parent_block(), - disjBlock[0], + disjBlock[2], ) self.assertIs( trans.get_transformed_constraints(m.disjunct1[2, 1].c)[0].parent_block(), - disjBlock[1], + disjBlock[3], ) # This relies on the disjunctions being transformed in the same order # every time. These are the mappings between the indices of the original # disjuncts and the indices on the indexed block on the transformation # block. - if transformation == 'bigm': - pairs = [((1, 0), 0), ((1, 1), 1), ((2, 0), 2), ((2, 1), 3)] - elif transformation == 'hull': - pairs = [((2, 0), 0), ((2, 1), 1), ((1, 0), 2), ((1, 1), 3)] + pairs = [((1, 0), 0), ((1, 1), 1), ((2, 0), 2), ((2, 1), 3)] for i, j in pairs: self.assertIs(trans.get_src_disjunct(disjBlock[j]), m.disjunct1[i]) @@ -1731,35 +1728,69 @@ def check_transformation_blocks_nestedDisjunctions(self, m, transformation): # This is a much more comprehensive test that doesn't depend on # transformation Block structure, so just reuse it: hull = TransformationFactory('gdp.hull') - d3 = hull.get_disaggregated_var(m.d1.d3.indicator_var, m.d1) - d4 = hull.get_disaggregated_var(m.d1.d4.indicator_var, m.d1) + d3 = hull.get_disaggregated_var(m.d1.d3.binary_indicator_var, m.d1) + d4 = hull.get_disaggregated_var(m.d1.d4.binary_indicator_var, m.d1) self.check_transformed_model_nestedDisjuncts(m, d3, d4) - # check the disaggregated indicator var bound constraints too - cons = hull.get_var_bounds_constraint(d3) + # Check the 4 constraints that are unique to the case where we didn't + # declare d1.d3 and d1.d4 as local + d32 = hull.get_disaggregated_var(m.d1.d3.binary_indicator_var, m.d2) + d42 = hull.get_disaggregated_var(m.d1.d4.binary_indicator_var, m.d2) + # check the additional disaggregated indicator var bound constraints + cons = hull.get_var_bounds_constraint(d32) self.assertEqual(len(cons), 1) check_obj_in_active_tree(self, cons['ub']) cons_expr = self.simplify_leq_cons(cons['ub']) + # Note that this comes out as d32 <= 1 - d1.ind_var because it's the + # "extra" disaggregated var that gets created when it need to be + # disaggregated for d1, but it's not used in d2 assertExpressionsEqual( self, cons_expr, - d3 - m.d1.binary_indicator_var <= 0.0 + d32 + m.d1.binary_indicator_var - 1 <= 0.0 ) - cons = hull.get_var_bounds_constraint(d4) + cons = hull.get_var_bounds_constraint(d42) self.assertEqual(len(cons), 1) check_obj_in_active_tree(self, cons['ub']) cons_expr = self.simplify_leq_cons(cons['ub']) + # Note that this comes out as d42 <= 1 - d1.ind_var because it's the + # "extra" disaggregated var that gets created when it need to be + # disaggregated for d1, but it's not used in d2 + assertExpressionsEqual( + self, + cons_expr, + d42 + m.d1.binary_indicator_var - 1 <= 0.0 + ) + # check the aggregation constraints for the disaggregated indicator vars + cons = hull.get_disaggregation_constraint(m.d1.d3.binary_indicator_var, + m.disj) + check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual( + self, + cons_expr, + m.d1.d3.binary_indicator_var - d32 - d3 == 0.0 + ) + cons = hull.get_disaggregation_constraint(m.d1.d4.binary_indicator_var, + m.disj) + check_obj_in_active_tree(self, cons) + cons_expr = self.simplify_cons(cons) assertExpressionsEqual( self, cons_expr, - d4 - m.d1.binary_indicator_var <= 0.0 + m.d1.d4.binary_indicator_var - d42 - d4 == 0.0 ) - num_cons = len(m.component_data_objects(Constraint, - active=True, - descend_into=Block)) - self.assertEqual(num_cons, 10) + num_cons = len(list(m.component_data_objects(Constraint, + active=True, + descend_into=Block))) + # 30 total constraints in transformed model minus 10 trivial bounds + # (lower bounds of 0) gives us 20 constraints total: + self.assertEqual(num_cons, 20) + # (And this is 4 more than we test in + # self.check_transformed_model_nestedDisjuncts, so that's comforting + # too.) def check_nested_disjunction_target(self, transformation): diff --git a/pyomo/gdp/tests/models.py b/pyomo/gdp/tests/models.py index a52f08b790e..3477d182241 100644 --- a/pyomo/gdp/tests/models.py +++ b/pyomo/gdp/tests/models.py @@ -463,7 +463,7 @@ def makeNestedDisjunctions(): (makeNestedDisjunctions_NestedDisjuncts is a much simpler model. All this adds is that it has a nested disjunction on a DisjunctData as well - as on a SimpleDisjunct. So mostly it exists for historical reasons.) + as on a ScalarDisjunct. So mostly it exists for historical reasons.) """ m = ConcreteModel() m.x = Var(bounds=(-9, 9)) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 436367b3a89..b3bfbaaf8da 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -412,13 +412,11 @@ def test_error_for_or(self): ) def check_disaggregation_constraint(self, cons, var, disvar1, disvar2): - repn = generate_standard_repn(cons.body) - self.assertEqual(cons.lower, 0) - self.assertEqual(cons.upper, 0) - self.assertEqual(len(repn.linear_vars), 3) - ct.check_linear_coef(self, repn, var, 1) - ct.check_linear_coef(self, repn, disvar1, -1) - ct.check_linear_coef(self, repn, disvar2, -1) + assertExpressionsEqual( + self, + cons.expr, + var == disvar1 + disvar2 + ) def test_disaggregation_constraint(self): m = models.makeTwoTermDisj_Nonlinear() @@ -430,8 +428,8 @@ def test_disaggregation_constraint(self): self.check_disaggregation_constraint( hull.get_disaggregation_constraint(m.w, m.disjunction), m.w, - disjBlock[1].disaggregatedVars.w, transBlock._disaggregatedVars[1], + disjBlock[1].disaggregatedVars.w, ) self.check_disaggregation_constraint( hull.get_disaggregation_constraint(m.x, m.disjunction), @@ -442,8 +440,8 @@ def test_disaggregation_constraint(self): self.check_disaggregation_constraint( hull.get_disaggregation_constraint(m.y, m.disjunction), m.y, - disjBlock[0].disaggregatedVars.y, transBlock._disaggregatedVars[0], + disjBlock[0].disaggregatedVars.y, ) def test_xor_constraint_mapping(self): @@ -672,17 +670,38 @@ def test_global_vars_local_to_a_disjunction_disaggregated(self): self.assertIs(hull.get_src_var(x), m.disj1.x) # there is a spare x on disjunction1's block - x2 = m.disjunction1.algebraic_constraint.parent_block()._disaggregatedVars[2] + x2 = m.disjunction1.algebraic_constraint.parent_block()._disaggregatedVars[0] self.assertIs(hull.get_disaggregated_var(m.disj1.x, m.disj2), x2) self.assertIs(hull.get_src_var(x2), m.disj1.x) + # What really matters is that the above matches this: + agg_cons = hull.get_disaggregation_constraint(m.disj1.x, m.disjunction1) + assertExpressionsEqual( + self, + agg_cons.expr, + m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj1) + ) # and both a spare x and y on disjunction2's block - x2 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[0] - y1 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[1] + x2 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[1] + y1 = m.disjunction2.algebraic_constraint.parent_block()._disaggregatedVars[2] self.assertIs(hull.get_disaggregated_var(m.disj1.x, m.disj4), x2) self.assertIs(hull.get_src_var(x2), m.disj1.x) self.assertIs(hull.get_disaggregated_var(m.disj1.y, m.disj3), y1) self.assertIs(hull.get_src_var(y1), m.disj1.y) + # and again what really matters is that these align with the + # disaggregation constraints: + agg_cons = hull.get_disaggregation_constraint(m.disj1.x, m.disjunction2) + assertExpressionsEqual( + self, + agg_cons.expr, + m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj3) + ) + agg_cons = hull.get_disaggregation_constraint(m.disj1.y, m.disjunction2) + assertExpressionsEqual( + self, + agg_cons.expr, + m.disj1.y == y1 + hull.get_disaggregated_var(m.disj1.y, m.disj4) + ) def check_name_collision_disaggregated_vars(self, m, disj): hull = TransformationFactory('gdp.hull') @@ -1105,7 +1124,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertEqual(len(transBlock1.relaxedDisjuncts), 4) hull = TransformationFactory('gdp.hull') - firstTerm2 = transBlock1.relaxedDisjuncts[0] + firstTerm2 = transBlock1.relaxedDisjuncts[2] self.assertIs(firstTerm2, m.firstTerm[2].transformation_block) self.assertIsInstance(firstTerm2.disaggregatedVars.component("x"), Var) constraints = hull.get_transformed_constraints(m.firstTerm[2].cons) @@ -1119,7 +1138,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertIs(cons.parent_block(), firstTerm2) self.assertEqual(len(cons), 2) - secondTerm2 = transBlock1.relaxedDisjuncts[1] + secondTerm2 = transBlock1.relaxedDisjuncts[3] self.assertIs(secondTerm2, m.secondTerm[2].transformation_block) self.assertIsInstance(secondTerm2.disaggregatedVars.component("x"), Var) constraints = hull.get_transformed_constraints(m.secondTerm[2].cons) @@ -1133,7 +1152,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertIs(cons.parent_block(), secondTerm2) self.assertEqual(len(cons), 2) - firstTerm1 = transBlock1.relaxedDisjuncts[2] + firstTerm1 = transBlock1.relaxedDisjuncts[0] self.assertIs(firstTerm1, m.firstTerm[1].transformation_block) self.assertIsInstance(firstTerm1.disaggregatedVars.component("x"), Var) self.assertTrue(firstTerm1.disaggregatedVars.x.is_fixed()) @@ -1151,7 +1170,7 @@ def check_trans_block_disjunctions_of_disjunct_datas(self, m): self.assertIs(cons.parent_block(), firstTerm1) self.assertEqual(len(cons), 2) - secondTerm1 = transBlock1.relaxedDisjuncts[3] + secondTerm1 = transBlock1.relaxedDisjuncts[1] self.assertIs(secondTerm1, m.secondTerm[1].transformation_block) self.assertIsInstance(secondTerm1.disaggregatedVars.component("x"), Var) constraints = hull.get_transformed_constraints(m.secondTerm[1].cons) @@ -1379,9 +1398,8 @@ def test_deactivated_disjunct_leaves_nested_disjuncts_active(self): ct.check_deactivated_disjunct_leaves_nested_disjunct_active(self, 'hull') def test_mappings_between_disjunctions_and_xors(self): - # This test is nearly identical to the one in bigm, but because of - # different transformation orders, the name conflict gets resolved in - # the opposite way. + # Tests that the XOR constraints are put on the parent block of the + # disjunction, and checks the mappings. m = models.makeNestedDisjunctions() transform = TransformationFactory('gdp.hull') transform.apply_to(m) @@ -1390,8 +1408,10 @@ def test_mappings_between_disjunctions_and_xors(self): disjunctionPairs = [ (m.disjunction, transBlock.disjunction_xor), - (m.disjunct[1].innerdisjunction[0], transBlock.innerdisjunction_xor[0]), - (m.simpledisjunct.innerdisjunction, transBlock.innerdisjunction_xor_4), + (m.disjunct[1].innerdisjunction[0], + m.disjunct[1].innerdisjunction[0].algebraic_constraint.parent_block().innerdisjunction_xor[0]), + (m.simpledisjunct.innerdisjunction, + m.simpledisjunct.innerdisjunction.algebraic_constraint.parent_block().innerdisjunction_xor), ] # check disjunction mappings @@ -1568,24 +1588,23 @@ def test_transformed_model_nestedDisjuncts(self): m.d1.d4.binary_indicator_var) # Last, check that there aren't things we weren't expecting - all_cons = list(m.component_data_objects(Constraint, active=True, descend_into=Block)) - num_cons = len(all_cons) - - for idx, cons in enumerate(all_cons): - print(idx) - print(cons.name) - print(cons.expr) - print("") # 2 disaggregation constraints for x 0,3 - # + 6 bounds constraints for x 6,8,9,13,14,16 These are dumb: 10,14,16 + # + 6 bounds constraints for x 6,8,9,13,14,16 # + 2 bounds constraints for inner indicator vars 11, 12 # + 2 exactly-one constraints 1,4 # + 4 transformed constraints 2,5,7,15 - self.assertEqual(num_cons, 16) + self.assertEqual(len(all_cons), 16) def check_transformed_model_nestedDisjuncts(self, m, d3, d4): + # This function checks all of the 16 constraint expressions from + # transforming models.makeNestedDisjunction_NestedDisjuncts when + # declaring the inner indicator vars (d3 and d4) as local. Note that it + # also is a correct test for the case where the inner indicator vars are + # *not* declared as local, but not a complete one, since there are + # additional constraints in that case (see + # check_transformation_blocks_nestedDisjunctions in common_tests.py). hull = TransformationFactory('gdp.hull') transBlock = m._pyomo_gdp_hull_reformulation self.assertTrue(transBlock.active) @@ -1654,7 +1673,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): assertExpressionsEqual( self, cons_expr, - 1.2*m.d1.d3.binary_indicator_var - x_d3 <= 0.0 + 1.2*d3 - x_d3 <= 0.0 ) cons = hull.get_transformed_constraints(m.d1.d4.c) @@ -1665,7 +1684,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): assertExpressionsEqual( self, cons_expr, - 1.3*m.d1.d4.binary_indicator_var - x_d4 <= 0.0 + 1.3*d4 - x_d4 <= 0.0 ) cons = hull.get_transformed_constraints(m.d1.c) @@ -1711,41 +1730,69 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): cons_expr, x_d2 - 2*m.d2.binary_indicator_var <= 0.0 ) - cons = hull.get_var_bounds_constraint(x_d3) + cons = hull.get_var_bounds_constraint(x_d3, m.d1.d3) # the lb is trivial in this case, so we just have 1 self.assertEqual(len(cons), 1) - ct.check_obj_in_active_tree(self, cons['ub']) - cons_expr = self.simplify_leq_cons(cons['ub']) + # And we know it has actually been transformed again, so get that one + cons = hull.get_transformed_constraints(cons['ub']) + self.assertEqual(len(cons), 1) + ub = cons[0] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) assertExpressionsEqual( self, cons_expr, x_d3 - 2*d3 <= 0.0 ) - cons = hull.get_var_bounds_constraint(x_d4) + cons = hull.get_var_bounds_constraint(x_d4, m.d1.d4) # the lb is trivial in this case, so we just have 1 self.assertEqual(len(cons), 1) - ct.check_obj_in_active_tree(self, cons['ub']) - cons_expr = self.simplify_leq_cons(cons['ub']) + # And we know it has actually been transformed again, so get that one + cons = hull.get_transformed_constraints(cons['ub']) + self.assertEqual(len(cons), 1) + ub = cons[0] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) assertExpressionsEqual( self, cons_expr, x_d4 - 2*d4 <= 0.0 ) + cons = hull.get_var_bounds_constraint(x_d3, m.d1) + self.assertEqual(len(cons), 1) + ub = cons['ub'] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) + assertExpressionsEqual( + self, + cons_expr, + x_d3 - 2*m.d1.binary_indicator_var <= 0.0 + ) + cons = hull.get_var_bounds_constraint(x_d4, m.d1) + self.assertEqual(len(cons), 1) + ub = cons['ub'] + ct.check_obj_in_active_tree(self, ub) + cons_expr = self.simplify_leq_cons(ub) + assertExpressionsEqual( + self, + cons_expr, + x_d4 - 2*m.d1.binary_indicator_var <= 0.0 + ) # Bounds constraints for local vars - cons = hull.get_var_bounds_constraint(m.d1.d3.binary_indicator_var) + cons = hull.get_var_bounds_constraint(d3) ct.check_obj_in_active_tree(self, cons['ub']) assertExpressionsEqual( self, cons['ub'].expr, - m.d1.d3.binary_indicator_var <= m.d1.binary_indicator_var + d3 <= m.d1.binary_indicator_var ) - cons = hull.get_var_bounds_constraint(m.d1.d4.binary_indicator_var) + cons = hull.get_var_bounds_constraint(d4) ct.check_obj_in_active_tree(self, cons['ub']) assertExpressionsEqual( self, cons['ub'].expr, - m.d1.d4.binary_indicator_var <= m.d1.binary_indicator_var + d4 <= m.d1.binary_indicator_var ) @unittest.skipIf(not linear_solvers, "No linear solver available") @@ -1853,6 +1900,9 @@ def d_r(e): e.c1 = Constraint(expr=e.lambdas[1] + e.lambdas[2] == 1) e.c2 = Constraint(expr=m.x == 2 * e.lambdas[1] + 3 * e.lambdas[2]) + d.LocalVars = Suffix(direction=Suffix.LOCAL) + d.LocalVars[d] = [d.d_l.indicator_var.get_associated_binary(), + d.d_r.indicator_var.get_associated_binary()] d.inner_disj = Disjunction(expr=[d.d_l, d.d_r]) m.disj = Disjunction(expr=[m.d_l, m.d_r]) @@ -1875,28 +1925,29 @@ def d_r(e): cons = hull.get_transformed_constraints(d.c1) self.assertEqual(len(cons), 1) convex_combo = cons[0] + convex_combo_expr = self.simplify_cons(convex_combo) assertExpressionsEqual( self, - convex_combo.expr, - lambda1 + lambda2 - (1 - d.indicator_var.get_associated_binary()) * 0.0 - == d.indicator_var.get_associated_binary(), + convex_combo_expr, + lambda1 + lambda2 - d.indicator_var.get_associated_binary() + == 0.0, ) cons = hull.get_transformed_constraints(d.c2) self.assertEqual(len(cons), 1) get_x = cons[0] + get_x_expr = self.simplify_cons(get_x) assertExpressionsEqual( self, - get_x.expr, - x - - (2 * lambda1 + 3 * lambda2) - - (1 - d.indicator_var.get_associated_binary()) * 0.0 - == 0.0 * d.indicator_var.get_associated_binary(), + get_x_expr, + x - 2 * lambda1 - 3 * lambda2 + == 0.0, ) cons = hull.get_disaggregation_constraint(m.x, m.disj) assertExpressionsEqual(self, cons.expr, m.x == x1 + x2) cons = hull.get_disaggregation_constraint(m.x, m.d_r.inner_disj) - assertExpressionsEqual(self, cons.expr, x2 == x3 + x4) + cons_expr = self.simplify_cons(cons) + assertExpressionsEqual(self, cons_expr, x2 - x3 - x4 == 0.0) def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): m = ConcreteModel() @@ -1949,7 +2000,7 @@ def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): x_c3 == 0.0) def simplify_cons(self, cons): - visitor = LinearRepnVisitor({}, {}, {}) + visitor = LinearRepnVisitor({}, {}, {}, None) lb = cons.lower ub = cons.upper self.assertEqual(cons.lb, cons.ub) @@ -1958,7 +2009,7 @@ def simplify_cons(self, cons): return repn.to_expression(visitor) == lb def simplify_leq_cons(self, cons): - visitor = LinearRepnVisitor({}, {}, {}) + visitor = LinearRepnVisitor({}, {}, {}, None) self.assertIsNone(cons.lower) ub = cons.upper repn = visitor.walk_expression(cons.body) @@ -2294,20 +2345,13 @@ def test_mapping_method_errors(self): hull = TransformationFactory('gdp.hull') hull.apply_to(m) - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): - self.assertRaisesRegex( - AttributeError, - "'NoneType' object has no attribute 'parent_block'", - hull.get_var_bounds_constraint, - m.w, - ) - self.assertRegex( - log.getvalue(), + with self.assertRaisesRegex( + GDP_Error, ".*Either 'w' is not a disaggregated variable, " "or the disjunction that disaggregates it has " "not been properly transformed.", - ) + ): + hull.get_var_bounds_constraint(m.w) log = StringIO() with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): @@ -2328,36 +2372,24 @@ def test_mapping_method_errors(self): r"Disjunction 'disjunction'", ) - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): - self.assertRaisesRegex( - AttributeError, - "'NoneType' object has no attribute 'parent_block'", - hull.get_src_var, - m.w, - ) - self.assertRegex( - log.getvalue(), ".*'w' does not appear to be a disaggregated variable" - ) + with self.assertRaisesRegex( + GDP_Error, + ".*'w' does not appear to be a disaggregated variable" + ): + hull.get_src_var(m.w,) - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): - self.assertRaisesRegex( - KeyError, - r".*_pyomo_gdp_hull_reformulation.relaxedDisjuncts\[1\]." - r"disaggregatedVars.w", - hull.get_disaggregated_var, - m.d[1].transformation_block.disaggregatedVars.w, - m.d[1], - ) - self.assertRegex( - log.getvalue(), + with self.assertRaisesRegex( + GDP_Error, r".*It does not appear " r"'_pyomo_gdp_hull_reformulation." r"relaxedDisjuncts\[1\].disaggregatedVars.w' " r"is a variable that appears in disjunct " - r"'d\[1\]'", - ) + r"'d\[1\]'" + ): + hull.get_disaggregated_var( + m.d[1].transformation_block.disaggregatedVars.w, + m.d[1], + ) m.random_disjunction = Disjunction(expr=[m.w == 2, m.w >= 7]) self.assertRaisesRegex( From 0ff74b63eed54196534b04ee57c607c041aa5b3f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sat, 17 Feb 2024 13:21:04 -0700 Subject: [PATCH 0596/3044] Black has opinions --- pyomo/gdp/plugins/hull.py | 116 +++++++++++++---------- pyomo/gdp/tests/common_tests.py | 28 ++---- pyomo/gdp/tests/test_hull.py | 163 ++++++++++++-------------------- 3 files changed, 133 insertions(+), 174 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index e880d599366..9fcac6a8f4e 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -57,6 +57,7 @@ from pytest import set_trace + @TransformationFactory.register( 'gdp.hull', doc="Relax disjunctive model by forming the hull reformulation." ) @@ -226,9 +227,10 @@ def _get_user_defined_local_vars(self, targets): # first look beneath where we are (there could be Blocks on this # disjunct) for b in t.component_data_objects( - Block, descend_into=Block, - active=True, - sort=SortComponents.deterministic + Block, + descend_into=Block, + active=True, + sort=SortComponents.deterministic, ): if b not in seen_blocks: self._collect_local_vars_from_block(b, user_defined_local_vars) @@ -237,8 +239,9 @@ def _get_user_defined_local_vars(self, targets): blk = t while blk is not None: if blk not in seen_blocks: - self._collect_local_vars_from_block(blk, - user_defined_local_vars) + self._collect_local_vars_from_block( + blk, user_defined_local_vars + ) seen_blocks.add(blk) blk = blk.parent_block() return user_defined_local_vars @@ -285,16 +288,12 @@ def _apply_to_impl(self, instance, **kwds): # parent Disjuncts as we transform their child Disjunctions. preprocessed_targets = gdp_tree.reverse_topological_sort() # Get all LocalVars from Suffixes ahead of time - local_vars_by_disjunct = self._get_user_defined_local_vars( - preprocessed_targets) + local_vars_by_disjunct = self._get_user_defined_local_vars(preprocessed_targets) for t in preprocessed_targets: if t.ctype is Disjunction: self._transform_disjunctionData( - t, - t.index(), - gdp_tree.parent(t), - local_vars_by_disjunct + t, t.index(), gdp_tree.parent(t), local_vars_by_disjunct ) # We skip disjuncts now, because we need information from the # disjunctions to transform them (which variables to disaggregate), @@ -322,8 +321,9 @@ def _add_transformation_block(self, to_block): return transBlock, True - def _transform_disjunctionData(self, obj, index, parent_disjunct, - local_vars_by_disjunct): + def _transform_disjunctionData( + self, obj, index, parent_disjunct, local_vars_by_disjunct + ): print("Transforming Disjunction %s" % obj) # Hull reformulation doesn't work if this is an OR constraint. So if # xor is false, give up @@ -364,12 +364,12 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # create the key for each disjunct now disjunct_disaggregated_var_map[disjunct] = ComponentMap() for var in get_vars_from_components( - disjunct, - Constraint, - include_fixed=not self._config.assume_fixed_vars_permanent, - active=True, - sort=SortComponents.deterministic, - descend_into=Block + disjunct, + Constraint, + include_fixed=not self._config.assume_fixed_vars_permanent, + active=True, + sort=SortComponents.deterministic, + descend_into=Block, ): # [ESJ 02/14/2020] By default, we disaggregate fixed variables # on the philosophy that fixing is not a promise for the future @@ -378,7 +378,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # with their transformed model. However, the user may have set # assume_fixed_vars_permanent to True in which case we will skip # them - + # Note that, because ComponentSets are ordered, we will # eventually disaggregate the vars in a deterministic order # (the order that we found them) @@ -411,7 +411,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, for disj in disjuncts: vars_to_disaggregate[disj].add(var) all_vars_to_disaggregate.add(var) - else: # var only appears in one disjunct + else: # var only appears in one disjunct disjunct = next(iter(disjuncts)) # We check if the user declared it as local if disjunct in local_vars_by_disjunct: @@ -440,7 +440,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, local_vars=local_vars[disjunct], parent_local_var_suffix=parent_local_var_list, parent_disjunct_local_vars=local_vars_by_disjunct[parent_disjunct], - disjunct_disaggregated_var_map=disjunct_disaggregated_var_map + disjunct_disaggregated_var_map=disjunct_disaggregated_var_map, ) xorConstraint.add(index, (or_expr, 1)) # map the DisjunctionData to its XOR constraint to mark it as @@ -476,7 +476,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, bigmConstraint=disaggregated_var_bounds, lb_idx=(idx, 'lb'), ub_idx=(idx, 'ub'), - var_free_indicator=var_free + var_free_indicator=var_free, ) # Update mappings: var_info = var.parent_block().private_data() @@ -493,8 +493,8 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, if disaggregated_var not in bigm_constraint_map: bigm_constraint_map[disaggregated_var] = {} - bigm_constraint_map[disaggregated_var][obj] = ( - Reference(disaggregated_var_bounds[idx, :]) + bigm_constraint_map[disaggregated_var][obj] = Reference( + disaggregated_var_bounds[idx, :] ) original_var_map[disaggregated_var] = var @@ -540,9 +540,16 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, # deactivate for the writers obj.deactivate() - def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, - parent_local_var_suffix, parent_disjunct_local_vars, - disjunct_disaggregated_var_map): + def _transform_disjunct( + self, + obj, + transBlock, + vars_to_disaggregate, + local_vars, + parent_local_var_suffix, + parent_disjunct_local_vars, + disjunct_disaggregated_var_map, + ): print("\nTransforming Disjunct '%s'" % obj.name) relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) @@ -578,9 +585,11 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, disaggregatedVarName + "_bounds", bigmConstraint ) - print("Adding bounds constraints for '%s', the disaggregated var " - "corresponding to Var '%s' on Disjunct '%s'" % - (disaggregatedVar, var, obj)) + print( + "Adding bounds constraints for '%s', the disaggregated var " + "corresponding to Var '%s' on Disjunct '%s'" + % (disaggregatedVar, var, obj) + ) self._declare_disaggregated_var_bounds( original_var=var, disaggregatedVar=disaggregatedVar, @@ -633,10 +642,13 @@ def _transform_disjunct(self, obj, transBlock, vars_to_disaggregate, local_vars, data_dict['bigm_constraint_map'][var][obj] = bigmConstraint disjunct_disaggregated_var_map[obj][var] = var - var_substitute_map = dict((id(v), newV) for v, newV in - disjunct_disaggregated_var_map[obj].items() ) - zero_substitute_map = dict((id(v), ZeroConstant) for v, newV in - disjunct_disaggregated_var_map[obj].items() ) + var_substitute_map = dict( + (id(v), newV) for v, newV in disjunct_disaggregated_var_map[obj].items() + ) + zero_substitute_map = dict( + (id(v), ZeroConstant) + for v, newV in disjunct_disaggregated_var_map[obj].items() + ) # Transform each component within this disjunct self._transform_block_components( @@ -690,8 +702,10 @@ def _declare_disaggregated_var_bounds( # the transformation block if disjunct not in disaggregated_var_map: disaggregated_var_map[disjunct] = ComponentMap() - print("DISAGGREGATED VAR MAP (%s, %s) : %s" % (disjunct, original_var, - disaggregatedVar)) + print( + "DISAGGREGATED VAR MAP (%s, %s) : %s" + % (disjunct, original_var, disaggregatedVar) + ) disaggregated_var_map[disjunct][original_var] = disaggregatedVar original_var_map[disaggregatedVar] = original_var @@ -915,8 +929,10 @@ def get_disaggregated_var(self, v, disjunct, raise_exception=True): """ if disjunct._transformation_block is None: raise GDP_Error("Disjunct '%s' has not been transformed" % disjunct.name) - msg = ("It does not appear '%s' is a " - "variable that appears in disjunct '%s'" % (v.name, disjunct.name)) + msg = ( + "It does not appear '%s' is a " + "variable that appears in disjunct '%s'" % (v.name, disjunct.name) + ) var_map = v.parent_block().private_data() if 'disaggregated_var_map' in var_map: try: @@ -947,12 +963,14 @@ def get_src_var(self, disaggregated_var): return var_map['original_var_map'][disaggregated_var] raise GDP_Error( "'%s' does not appear to be a " - "disaggregated variable" % disaggregated_var.name) + "disaggregated variable" % disaggregated_var.name + ) # retrieves the disaggregation constraint for original_var resulting from # transforming disjunction - def get_disaggregation_constraint(self, original_var, disjunction, - raise_exception=True): + def get_disaggregation_constraint( + self, original_var, disjunction, raise_exception=True + ): """ Returns the disaggregation (re-aggregation?) constraint (which links the disaggregated variables to their original) @@ -976,11 +994,9 @@ def get_disaggregation_constraint(self, original_var, disjunction, ) try: - cons = ( - transBlock - .parent_block() - ._disaggregationConstraintMap[original_var][disjunction] - ) + cons = transBlock.parent_block()._disaggregationConstraintMap[original_var][ + disjunction + ] except: if raise_exception: logger.error( @@ -1006,7 +1022,7 @@ def get_var_bounds_constraint(self, v, disjunct=None): v: a Var that was created by the hull transformation as a disaggregated variable (and so appears on a transformation block of some Disjunct) - disjunct: (For nested Disjunctions) Which Disjunct in the + disjunct: (For nested Disjunctions) Which Disjunct in the hierarchy the bounds Constraint should correspond to. Optional since for non-nested models this can be inferred. """ @@ -1025,8 +1041,8 @@ def get_var_bounds_constraint(self, v, disjunct=None): "within a nested GDP hierarchy, and no " "'disjunct' argument was specified. Please " "specify for which Disjunct the bounds " - "constraint for '%s' should be returned." - % (v, v)) + "constraint for '%s' should be returned." % (v, v) + ) raise GDP_Error( "Either '%s' is not a disaggregated variable, or " "the disjunction that disaggregates it has not " diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index bef05a78cf6..e63742bb1a8 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -1745,9 +1745,7 @@ def check_transformation_blocks_nestedDisjunctions(self, m, transformation): # "extra" disaggregated var that gets created when it need to be # disaggregated for d1, but it's not used in d2 assertExpressionsEqual( - self, - cons_expr, - d32 + m.d1.binary_indicator_var - 1 <= 0.0 + self, cons_expr, d32 + m.d1.binary_indicator_var - 1 <= 0.0 ) cons = hull.get_var_bounds_constraint(d42) @@ -1758,33 +1756,25 @@ def check_transformation_blocks_nestedDisjunctions(self, m, transformation): # "extra" disaggregated var that gets created when it need to be # disaggregated for d1, but it's not used in d2 assertExpressionsEqual( - self, - cons_expr, - d42 + m.d1.binary_indicator_var - 1 <= 0.0 + self, cons_expr, d42 + m.d1.binary_indicator_var - 1 <= 0.0 ) # check the aggregation constraints for the disaggregated indicator vars - cons = hull.get_disaggregation_constraint(m.d1.d3.binary_indicator_var, - m.disj) + cons = hull.get_disaggregation_constraint(m.d1.d3.binary_indicator_var, m.disj) check_obj_in_active_tree(self, cons) cons_expr = self.simplify_cons(cons) assertExpressionsEqual( - self, - cons_expr, - m.d1.d3.binary_indicator_var - d32 - d3 == 0.0 + self, cons_expr, m.d1.d3.binary_indicator_var - d32 - d3 == 0.0 ) - cons = hull.get_disaggregation_constraint(m.d1.d4.binary_indicator_var, - m.disj) + cons = hull.get_disaggregation_constraint(m.d1.d4.binary_indicator_var, m.disj) check_obj_in_active_tree(self, cons) cons_expr = self.simplify_cons(cons) assertExpressionsEqual( - self, - cons_expr, - m.d1.d4.binary_indicator_var - d42 - d4 == 0.0 + self, cons_expr, m.d1.d4.binary_indicator_var - d42 - d4 == 0.0 ) - num_cons = len(list(m.component_data_objects(Constraint, - active=True, - descend_into=Block))) + num_cons = len( + list(m.component_data_objects(Constraint, active=True, descend_into=Block)) + ) # 30 total constraints in transformed model minus 10 trivial bounds # (lower bounds of 0) gives us 20 constraints total: self.assertEqual(num_cons, 20) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index b3bfbaaf8da..aef119c0f1e 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -412,11 +412,7 @@ def test_error_for_or(self): ) def check_disaggregation_constraint(self, cons, var, disvar1, disvar2): - assertExpressionsEqual( - self, - cons.expr, - var == disvar1 + disvar2 - ) + assertExpressionsEqual(self, cons.expr, var == disvar1 + disvar2) def test_disaggregation_constraint(self): m = models.makeTwoTermDisj_Nonlinear() @@ -678,7 +674,7 @@ def test_global_vars_local_to_a_disjunction_disaggregated(self): assertExpressionsEqual( self, agg_cons.expr, - m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj1) + m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj1), ) # and both a spare x and y on disjunction2's block @@ -694,13 +690,13 @@ def test_global_vars_local_to_a_disjunction_disaggregated(self): assertExpressionsEqual( self, agg_cons.expr, - m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj3) + m.disj1.x == x2 + hull.get_disaggregated_var(m.disj1.x, m.disj3), ) agg_cons = hull.get_disaggregation_constraint(m.disj1.y, m.disjunction2) assertExpressionsEqual( self, agg_cons.expr, - m.disj1.y == y1 + hull.get_disaggregated_var(m.disj1.y, m.disj4) + m.disj1.y == y1 + hull.get_disaggregated_var(m.disj1.y, m.disj4), ) def check_name_collision_disaggregated_vars(self, m, disj): @@ -1408,10 +1404,17 @@ def test_mappings_between_disjunctions_and_xors(self): disjunctionPairs = [ (m.disjunction, transBlock.disjunction_xor), - (m.disjunct[1].innerdisjunction[0], - m.disjunct[1].innerdisjunction[0].algebraic_constraint.parent_block().innerdisjunction_xor[0]), - (m.simpledisjunct.innerdisjunction, - m.simpledisjunct.innerdisjunction.algebraic_constraint.parent_block().innerdisjunction_xor), + ( + m.disjunct[1].innerdisjunction[0], + m.disjunct[1] + .innerdisjunction[0] + .algebraic_constraint.parent_block() + .innerdisjunction_xor[0], + ), + ( + m.simpledisjunct.innerdisjunction, + m.simpledisjunct.innerdisjunction.algebraic_constraint.parent_block().innerdisjunction_xor, + ), ] # check disjunction mappings @@ -1578,18 +1581,20 @@ def test_transformed_model_nestedDisjuncts(self): m.LocalVars[m.d1] = [ m.d1.binary_indicator_var, m.d1.d3.binary_indicator_var, - m.d1.d4.binary_indicator_var + m.d1.d4.binary_indicator_var, ] - + hull = TransformationFactory('gdp.hull') hull.apply_to(m) - self.check_transformed_model_nestedDisjuncts(m, m.d1.d3.binary_indicator_var, - m.d1.d4.binary_indicator_var) + self.check_transformed_model_nestedDisjuncts( + m, m.d1.d3.binary_indicator_var, m.d1.d4.binary_indicator_var + ) # Last, check that there aren't things we weren't expecting - all_cons = list(m.component_data_objects(Constraint, active=True, - descend_into=Block)) + all_cons = list( + m.component_data_objects(Constraint, active=True, descend_into=Block) + ) # 2 disaggregation constraints for x 0,3 # + 6 bounds constraints for x 6,8,9,13,14,16 # + 2 bounds constraints for inner indicator vars 11, 12 @@ -1614,9 +1619,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): self.assertIsInstance(xor, Constraint) ct.check_obj_in_active_tree(self, xor) assertExpressionsEqual( - self, - xor.expr, - m.d1.binary_indicator_var + m.d2.binary_indicator_var == 1 + self, xor.expr, m.d1.binary_indicator_var + m.d2.binary_indicator_var == 1 ) self.assertIs(xor, m.disj.algebraic_constraint) self.assertIs(m.disj, hull.get_src_disjunction(xor)) @@ -1630,11 +1633,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): ct.check_obj_in_active_tree(self, xor) xor_expr = self.simplify_cons(xor) assertExpressionsEqual( - self, - xor_expr, - d3 + - d4 - - m.d1.binary_indicator_var == 0.0 + self, xor_expr, d3 + d4 - m.d1.binary_indicator_var == 0.0 ) # check disaggregation constraints @@ -1649,20 +1648,12 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): cons = hull.get_disaggregation_constraint(m.x, m.d1.disj2) ct.check_obj_in_active_tree(self, cons) cons_expr = self.simplify_cons(cons) - assertExpressionsEqual( - self, - cons_expr, - x_d1 - x_d3 - x_d4 == 0.0 - ) + assertExpressionsEqual(self, cons_expr, x_d1 - x_d3 - x_d4 == 0.0) # Outer disjunction cons = hull.get_disaggregation_constraint(m.x, m.disj) ct.check_obj_in_active_tree(self, cons) cons_expr = self.simplify_cons(cons) - assertExpressionsEqual( - self, - cons_expr, - m.x - x_d1 - x_d2 == 0.0 - ) + assertExpressionsEqual(self, cons_expr, m.x - x_d1 - x_d2 == 0.0) ## Transformed constraints cons = hull.get_transformed_constraints(m.d1.d3.c) @@ -1670,32 +1661,22 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): cons = cons[0] ct.check_obj_in_active_tree(self, cons) cons_expr = self.simplify_leq_cons(cons) - assertExpressionsEqual( - self, - cons_expr, - 1.2*d3 - x_d3 <= 0.0 - ) + assertExpressionsEqual(self, cons_expr, 1.2 * d3 - x_d3 <= 0.0) cons = hull.get_transformed_constraints(m.d1.d4.c) self.assertEqual(len(cons), 1) cons = cons[0] ct.check_obj_in_active_tree(self, cons) cons_expr = self.simplify_leq_cons(cons) - assertExpressionsEqual( - self, - cons_expr, - 1.3*d4 - x_d4 <= 0.0 - ) - + assertExpressionsEqual(self, cons_expr, 1.3 * d4 - x_d4 <= 0.0) + cons = hull.get_transformed_constraints(m.d1.c) self.assertEqual(len(cons), 1) cons = cons[0] ct.check_obj_in_active_tree(self, cons) cons_expr = self.simplify_leq_cons(cons) assertExpressionsEqual( - self, - cons_expr, - 1.0*m.d1.binary_indicator_var - x_d1 <= 0.0 + self, cons_expr, 1.0 * m.d1.binary_indicator_var - x_d1 <= 0.0 ) cons = hull.get_transformed_constraints(m.d2.c) @@ -1704,9 +1685,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): ct.check_obj_in_active_tree(self, cons) cons_expr = self.simplify_leq_cons(cons) assertExpressionsEqual( - self, - cons_expr, - 1.1*m.d2.binary_indicator_var - x_d2 <= 0.0 + self, cons_expr, 1.1 * m.d2.binary_indicator_var - x_d2 <= 0.0 ) ## Bounds constraints @@ -1716,9 +1695,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): ct.check_obj_in_active_tree(self, cons['ub']) cons_expr = self.simplify_leq_cons(cons['ub']) assertExpressionsEqual( - self, - cons_expr, - x_d1 - 2*m.d1.binary_indicator_var <= 0.0 + self, cons_expr, x_d1 - 2 * m.d1.binary_indicator_var <= 0.0 ) cons = hull.get_var_bounds_constraint(x_d2) # the lb is trivial in this case, so we just have 1 @@ -1726,9 +1703,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): ct.check_obj_in_active_tree(self, cons['ub']) cons_expr = self.simplify_leq_cons(cons['ub']) assertExpressionsEqual( - self, - cons_expr, - x_d2 - 2*m.d2.binary_indicator_var <= 0.0 + self, cons_expr, x_d2 - 2 * m.d2.binary_indicator_var <= 0.0 ) cons = hull.get_var_bounds_constraint(x_d3, m.d1.d3) # the lb is trivial in this case, so we just have 1 @@ -1739,11 +1714,7 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): ub = cons[0] ct.check_obj_in_active_tree(self, ub) cons_expr = self.simplify_leq_cons(ub) - assertExpressionsEqual( - self, - cons_expr, - x_d3 - 2*d3 <= 0.0 - ) + assertExpressionsEqual(self, cons_expr, x_d3 - 2 * d3 <= 0.0) cons = hull.get_var_bounds_constraint(x_d4, m.d1.d4) # the lb is trivial in this case, so we just have 1 self.assertEqual(len(cons), 1) @@ -1753,20 +1724,14 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): ub = cons[0] ct.check_obj_in_active_tree(self, ub) cons_expr = self.simplify_leq_cons(ub) - assertExpressionsEqual( - self, - cons_expr, - x_d4 - 2*d4 <= 0.0 - ) + assertExpressionsEqual(self, cons_expr, x_d4 - 2 * d4 <= 0.0) cons = hull.get_var_bounds_constraint(x_d3, m.d1) self.assertEqual(len(cons), 1) ub = cons['ub'] ct.check_obj_in_active_tree(self, ub) cons_expr = self.simplify_leq_cons(ub) assertExpressionsEqual( - self, - cons_expr, - x_d3 - 2*m.d1.binary_indicator_var <= 0.0 + self, cons_expr, x_d3 - 2 * m.d1.binary_indicator_var <= 0.0 ) cons = hull.get_var_bounds_constraint(x_d4, m.d1) self.assertEqual(len(cons), 1) @@ -1774,26 +1739,16 @@ def check_transformed_model_nestedDisjuncts(self, m, d3, d4): ct.check_obj_in_active_tree(self, ub) cons_expr = self.simplify_leq_cons(ub) assertExpressionsEqual( - self, - cons_expr, - x_d4 - 2*m.d1.binary_indicator_var <= 0.0 + self, cons_expr, x_d4 - 2 * m.d1.binary_indicator_var <= 0.0 ) # Bounds constraints for local vars cons = hull.get_var_bounds_constraint(d3) ct.check_obj_in_active_tree(self, cons['ub']) - assertExpressionsEqual( - self, - cons['ub'].expr, - d3 <= m.d1.binary_indicator_var - ) + assertExpressionsEqual(self, cons['ub'].expr, d3 <= m.d1.binary_indicator_var) cons = hull.get_var_bounds_constraint(d4) ct.check_obj_in_active_tree(self, cons['ub']) - assertExpressionsEqual( - self, - cons['ub'].expr, - d4 <= m.d1.binary_indicator_var - ) + assertExpressionsEqual(self, cons['ub'].expr, d4 <= m.d1.binary_indicator_var) @unittest.skipIf(not linear_solvers, "No linear solver available") def test_solve_nested_model(self): @@ -1804,8 +1759,8 @@ def test_solve_nested_model(self): m.LocalVars[m.d1] = [ m.d1.binary_indicator_var, m.d1.d3.binary_indicator_var, - m.d1.d4.binary_indicator_var - ] + m.d1.d4.binary_indicator_var, + ] hull = TransformationFactory('gdp.hull') m_hull = hull.create_using(m) @@ -1901,8 +1856,10 @@ def d_r(e): e.c2 = Constraint(expr=m.x == 2 * e.lambdas[1] + 3 * e.lambdas[2]) d.LocalVars = Suffix(direction=Suffix.LOCAL) - d.LocalVars[d] = [d.d_l.indicator_var.get_associated_binary(), - d.d_r.indicator_var.get_associated_binary()] + d.LocalVars[d] = [ + d.d_l.indicator_var.get_associated_binary(), + d.d_r.indicator_var.get_associated_binary(), + ] d.inner_disj = Disjunction(expr=[d.d_l, d.d_r]) m.disj = Disjunction(expr=[m.d_l, m.d_r]) @@ -1929,18 +1886,14 @@ def d_r(e): assertExpressionsEqual( self, convex_combo_expr, - lambda1 + lambda2 - d.indicator_var.get_associated_binary() - == 0.0, + lambda1 + lambda2 - d.indicator_var.get_associated_binary() == 0.0, ) cons = hull.get_transformed_constraints(d.c2) self.assertEqual(len(cons), 1) get_x = cons[0] get_x_expr = self.simplify_cons(get_x) assertExpressionsEqual( - self, - get_x_expr, - x - 2 * lambda1 - 3 * lambda2 - == 0.0, + self, get_x_expr, x - 2 * lambda1 - 3 * lambda2 == 0.0 ) cons = hull.get_disaggregation_constraint(m.x, m.disj) @@ -1996,8 +1949,9 @@ def test_nested_with_var_that_does_not_appear_in_every_disjunct(self): assertExpressionsEqual(self, x_cons_parent.expr, m.x == x_p1 + x_p2) x_cons_child = hull.get_disaggregation_constraint(m.x, m.parent1.disjunction) x_cons_child_expr = self.simplify_cons(x_cons_child) - assertExpressionsEqual(self, x_cons_child_expr, x_p1 - x_c1 - x_c2 - - x_c3 == 0.0) + assertExpressionsEqual( + self, x_cons_child_expr, x_p1 - x_c1 - x_c2 - x_c3 == 0.0 + ) def simplify_cons(self, cons): visitor = LinearRepnVisitor({}, {}, {}, None) @@ -2065,8 +2019,9 @@ def test_nested_with_var_that_skips_a_level(self): self.assertTrue(cons.active) cons_expr = self.simplify_cons(cons) assertExpressionsEqual(self, cons_expr, m.x - x_y1 - x_y2 == 0.0) - cons = hull.get_disaggregation_constraint(m.y, m.y1.z1.disjunction, - raise_exception=False) + cons = hull.get_disaggregation_constraint( + m.y, m.y1.z1.disjunction, raise_exception=False + ) self.assertIsNone(cons) cons = hull.get_disaggregation_constraint(m.y, m.y1.disjunction) self.assertTrue(cons.active) @@ -2373,10 +2328,9 @@ def test_mapping_method_errors(self): ) with self.assertRaisesRegex( - GDP_Error, - ".*'w' does not appear to be a disaggregated variable" + GDP_Error, ".*'w' does not appear to be a disaggregated variable" ): - hull.get_src_var(m.w,) + hull.get_src_var(m.w) with self.assertRaisesRegex( GDP_Error, @@ -2384,11 +2338,10 @@ def test_mapping_method_errors(self): r"'_pyomo_gdp_hull_reformulation." r"relaxedDisjuncts\[1\].disaggregatedVars.w' " r"is a variable that appears in disjunct " - r"'d\[1\]'" + r"'d\[1\]'", ): hull.get_disaggregated_var( - m.d[1].transformation_block.disaggregatedVars.w, - m.d[1], + m.d[1].transformation_block.disaggregatedVars.w, m.d[1] ) m.random_disjunction = Disjunction(expr=[m.w == 2, m.w >= 7]) From c9eca76fae1b319b456aa014e7a49b36e213ce8c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sat, 17 Feb 2024 13:22:21 -0700 Subject: [PATCH 0597/3044] Removing debugging --- pyomo/gdp/plugins/hull.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 9fcac6a8f4e..53dffc7a18e 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -324,7 +324,6 @@ def _add_transformation_block(self, to_block): def _transform_disjunctionData( self, obj, index, parent_disjunct, local_vars_by_disjunct ): - print("Transforming Disjunction %s" % obj) # Hull reformulation doesn't work if this is an OR constraint. So if # xor is false, give up if not obj.xor: @@ -459,7 +458,6 @@ def _transform_disjunctionData( # create one more disaggregated var idx = len(disaggregatedVars) disaggregated_var = disaggregatedVars[idx] - print("Creating extra disaggregated var: '%s'" % disaggregated_var) # mark this as local because we won't re-disaggregate it if this # is a nested disjunction if parent_local_var_list is not None: @@ -550,7 +548,6 @@ def _transform_disjunct( parent_disjunct_local_vars, disjunct_disaggregated_var_map, ): - print("\nTransforming Disjunct '%s'" % obj.name) relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) # Put the disaggregated variables all on their own block so that we can @@ -585,11 +582,6 @@ def _transform_disjunct( disaggregatedVarName + "_bounds", bigmConstraint ) - print( - "Adding bounds constraints for '%s', the disaggregated var " - "corresponding to Var '%s' on Disjunct '%s'" - % (disaggregatedVar, var, obj) - ) self._declare_disaggregated_var_bounds( original_var=var, disaggregatedVar=disaggregatedVar, @@ -623,7 +615,6 @@ def _transform_disjunct( parent_block = var.parent_block() - print("Adding bounds constraints for local var '%s'" % var) self._declare_disaggregated_var_bounds( original_var=var, disaggregatedVar=var, @@ -702,10 +693,6 @@ def _declare_disaggregated_var_bounds( # the transformation block if disjunct not in disaggregated_var_map: disaggregated_var_map[disjunct] = ComponentMap() - print( - "DISAGGREGATED VAR MAP (%s, %s) : %s" - % (disjunct, original_var, disaggregatedVar) - ) disaggregated_var_map[disjunct][original_var] = disaggregatedVar original_var_map[disaggregatedVar] = original_var From f12b76290f74ea257a210ae3deeb5af7fd91fe08 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sat, 17 Feb 2024 13:24:23 -0700 Subject: [PATCH 0598/3044] Removing more debugging --- pyomo/gdp/plugins/hull.py | 2 -- pyomo/gdp/tests/test_hull.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 53dffc7a18e..12665eef340 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -55,8 +55,6 @@ logger = logging.getLogger('pyomo.gdp.hull') -from pytest import set_trace - @TransformationFactory.register( 'gdp.hull', doc="Relax disjunctive model by forming the hull reformulation." diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index aef119c0f1e..e45a7543e25 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -52,8 +52,6 @@ import os from os.path import abspath, dirname, join -##DEBUG -from pytest import set_trace currdir = dirname(abspath(__file__)) from filecmp import cmp From fed34aedb900160a94261fe448d3e562b8a21fc0 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sat, 17 Feb 2024 13:32:03 -0700 Subject: [PATCH 0599/3044] NFC: updating docstring and removing comments --- pyomo/gdp/plugins/hull.py | 36 +++++------------------------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 12665eef340..8813cc25137 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -80,19 +80,11 @@ class Hull_Reformulation(GDP_to_MIP_Transformation): list of blocks and Disjunctions [default: the instance] The transformation will create a new Block with a unique - name beginning "_pyomo_gdp_hull_reformulation". - The block will have a dictionary "_disaggregatedVarMap: - 'srcVar': ComponentMap(:), - 'disaggregatedVar': ComponentMap(:) - - It will also have a ComponentMap "_bigMConstraintMap": - - : - - Last, it will contain an indexed Block named "relaxedDisjuncts", - which will hold the relaxed disjuncts. This block is indexed by - an integer indicating the order in which the disjuncts were relaxed. - Each block has a dictionary "_constraintMap": + name beginning "_pyomo_gdp_hull_reformulation". It will contain an + indexed Block named "relaxedDisjuncts" that will hold the relaxed + disjuncts. This block is indexed by an integer indicating the order + in which the disjuncts were relaxed. Each block has a dictionary + "_constraintMap": 'srcConstraints': ComponentMap(: ), @@ -108,7 +100,6 @@ class Hull_Reformulation(GDP_to_MIP_Transformation): The _pyomo_gdp_hull_reformulation block will have a ComponentMap "_disaggregationConstraintMap": :ComponentMap(: ) - """ CONFIG = cfg.ConfigDict('gdp.hull') @@ -244,23 +235,6 @@ def _get_user_defined_local_vars(self, targets): blk = blk.parent_block() return user_defined_local_vars - # def _get_local_vars_from_suffixes(self, block, local_var_dict): - # # You can specify suffixes on any block (disjuncts included). This - # # method starts from a Disjunct (presumably) and checks for a LocalVar - # # suffixes going both up and down the tree, adding them into the - # # dictionary that is the second argument. - - # # first look beneath where we are (there could be Blocks on this - # # disjunct) - # for b in block.component_data_objects( - # Block, descend_into=Block, active=True, sort=SortComponents.deterministic - # ): - # self._collect_local_vars_from_block(b, local_var_dict) - # # now traverse upwards and get what's above - # while block is not None: - # self._collect_local_vars_from_block(block, local_var_dict) - # block = block.parent_block() - def _apply_to(self, instance, **kwds): try: self._apply_to_impl(instance, **kwds) From 9caba527f53b101429edd0471fb70647e5df94fb Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sat, 17 Feb 2024 20:31:58 -0700 Subject: [PATCH 0600/3044] Updating baselines because I changed the transformation order --- pyomo/gdp/tests/jobshop_large_hull.lp | 1778 ++++++++++++------------- pyomo/gdp/tests/jobshop_small_hull.lp | 122 +- 2 files changed, 950 insertions(+), 950 deletions(-) diff --git a/pyomo/gdp/tests/jobshop_large_hull.lp b/pyomo/gdp/tests/jobshop_large_hull.lp index df3833bdee3..ee8ee0a73d2 100644 --- a/pyomo/gdp/tests/jobshop_large_hull.lp +++ b/pyomo/gdp/tests/jobshop_large_hull.lp @@ -42,87 +42,87 @@ c_u_Feas(G)_: <= -17 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(0)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(1)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(2)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(6)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(7)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(8)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(9)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(10)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(11)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(12)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(13)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(14)_: @@ -132,81 +132,81 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(14)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(15)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(16)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(17)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(18)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(19)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(20)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(21)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(22)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(23)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(24)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(25)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(26)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(27)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(28)_: @@ -216,33 +216,33 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(28)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(29)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(30)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(31)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(32)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(33)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(34)_: @@ -258,27 +258,27 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(35)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(36)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(37)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(38)_: -+1 t(F) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(39)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(40)_: @@ -288,81 +288,81 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(40)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(41)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(42)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(43)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(44)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(45)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(46)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(47)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(48)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(49)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(50)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(51)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(52)_: -+1 t(G) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(53)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(54)_: @@ -372,9 +372,9 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(54)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(55)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(56)_: @@ -384,81 +384,81 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(56)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(57)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(58)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(59)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(60)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(61)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(62)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(63)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(64)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(65)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(66)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(67)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(68)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ ++1 t(G) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(69)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ ++1 t(F) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ = 0 c_e__pyomo_gdp_hull_reformulation_disj_xor(A_B_3)_: @@ -637,546 +637,544 @@ c_e__pyomo_gdp_hull_reformulation_disj_xor(F_G_4)_: = 1 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ -+6.0 NoClash(F_G_4_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_B_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ --92 NoClash(F_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ --92 NoClash(F_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ -+6.0 NoClash(F_G_4_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ ++5.0 NoClash(A_B_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ --92 NoClash(F_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ --92 NoClash(F_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ -+7.0 NoClash(E_G_5_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ ++2.0 NoClash(A_B_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ --92 NoClash(E_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ --92 NoClash(E_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ --1 NoClash(E_G_5_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ ++3.0 NoClash(A_B_5_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ --92 NoClash(E_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ +-92 NoClash(A_B_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ --92 NoClash(E_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ +-92 NoClash(A_B_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ -+8.0 NoClash(E_G_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ ++6.0 NoClash(A_C_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ --92 NoClash(E_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +-92 NoClash(A_C_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ --92 NoClash(E_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-92 NoClash(A_C_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ -+4.0 NoClash(E_G_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ ++3.0 NoClash(A_C_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ --92 NoClash(E_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ +-92 NoClash(A_C_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ --92 NoClash(E_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ +-92 NoClash(A_C_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ -+3.0 NoClash(E_F_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ ++10.0 NoClash(A_D_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ --92 NoClash(E_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ +-92 NoClash(A_D_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ --92 NoClash(E_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ +-92 NoClash(A_D_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ -+8.0 NoClash(E_F_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ --92 NoClash(E_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ +-92 NoClash(A_D_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ --92 NoClash(E_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ +-92 NoClash(A_D_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ ++7.0 NoClash(A_E_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ --92 NoClash(D_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ --92 NoClash(D_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ -+6.0 NoClash(D_G_4_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_E_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ --92 NoClash(D_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ --92 NoClash(D_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ -+8.0 NoClash(D_G_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_E_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ --92 NoClash(D_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ --92 NoClash(D_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ -+8.0 NoClash(D_G_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ --92 NoClash(D_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ +-92 NoClash(A_E_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ --92 NoClash(D_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ +-92 NoClash(A_E_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_transformedConstraints(c_0_ub)_: +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ -+1 NoClash(D_F_4_0)_binary_indicator_var ++2.0 NoClash(A_F_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ --92 NoClash(D_F_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ --92 NoClash(D_F_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ +-92 NoClash(A_F_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_transformedConstraints(c_0_ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ -+7.0 NoClash(D_F_4_1)_binary_indicator_var ++3.0 NoClash(A_F_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ --92 NoClash(D_F_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ --92 NoClash(D_F_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ +-92 NoClash(A_F_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ --1 NoClash(D_F_3_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ ++4.0 NoClash(A_F_3_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ --92 NoClash(D_F_3_0)_binary_indicator_var +-92 NoClash(A_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ --92 NoClash(D_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ -+11.0 NoClash(D_F_3_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ ++6.0 NoClash(A_F_3_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(F)_ --92 NoClash(D_F_3_1)_binary_indicator_var +-92 NoClash(A_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ --92 NoClash(D_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ +-92 NoClash(A_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ -+2.0 NoClash(D_E_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ ++9.0 NoClash(A_G_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ --92 NoClash(D_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ +-92 NoClash(A_G_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ --92 NoClash(D_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ +-92 NoClash(A_G_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ -+9.0 NoClash(D_E_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ +-3.0 NoClash(A_G_5_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ --92 NoClash(D_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ +-92 NoClash(A_G_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ --92 NoClash(D_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ +-92 NoClash(A_G_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ -+4.0 NoClash(D_E_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ ++9.0 NoClash(B_C_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ --92 NoClash(D_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +-92 NoClash(B_C_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ --92 NoClash(D_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ +-92 NoClash(B_C_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ -+8.0 NoClash(D_E_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ +-3.0 NoClash(B_C_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ --92 NoClash(D_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ +-92 NoClash(B_C_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ --92 NoClash(D_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ +-92 NoClash(B_C_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ -+4.0 NoClash(C_G_4_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ ++8.0 NoClash(B_D_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ --92 NoClash(C_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ --92 NoClash(C_G_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ -+7.0 NoClash(C_G_4_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ ++3.0 NoClash(B_D_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ --92 NoClash(C_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ --92 NoClash(C_G_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_G_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ ++10.0 NoClash(B_D_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ --92 NoClash(C_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ --92 NoClash(C_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ -+9.0 NoClash(C_G_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ +-1 NoClash(B_D_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ --92 NoClash(C_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ +-92 NoClash(B_D_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ --92 NoClash(C_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ +-92 NoClash(B_D_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ -+5.0 NoClash(C_F_4_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ ++4.0 NoClash(B_E_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ --92 NoClash(C_F_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ --92 NoClash(C_F_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ -+8.0 NoClash(C_F_4_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ ++3.0 NoClash(B_E_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ --92 NoClash(C_F_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ --92 NoClash(C_F_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_F_1_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ ++7.0 NoClash(B_E_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ --92 NoClash(C_F_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ --92 NoClash(C_F_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ -+6.0 NoClash(C_F_1_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ ++3.0 NoClash(B_E_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ --92 NoClash(C_F_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ +-92 NoClash(B_E_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ --92 NoClash(C_F_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ --2.0 NoClash(C_E_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ ++5.0 NoClash(B_E_5_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(E)_ --92 NoClash(C_E_2_0)_binary_indicator_var +-92 NoClash(B_E_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ --92 NoClash(C_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ -+9.0 NoClash(C_E_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(E)_ --92 NoClash(C_E_2_1)_binary_indicator_var +-92 NoClash(B_E_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ --92 NoClash(C_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ +-92 NoClash(B_E_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ -+5.0 NoClash(C_D_4_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ ++4.0 NoClash(B_F_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ --92 NoClash(C_D_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ +-92 NoClash(B_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ --92 NoClash(C_D_4_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ +-92 NoClash(B_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_D_4_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ ++5.0 NoClash(B_F_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ --92 NoClash(C_D_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ +-92 NoClash(B_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ --92 NoClash(C_D_4_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ +-92 NoClash(B_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ -+2.0 NoClash(C_D_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ ++8.0 NoClash(B_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ --92 NoClash(C_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +-92 NoClash(B_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ --92 NoClash(C_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ +-92 NoClash(B_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ -+9.0 NoClash(C_D_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ ++3.0 NoClash(B_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ --92 NoClash(C_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ +-92 NoClash(B_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ --92 NoClash(C_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ +-92 NoClash(B_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_transformedConstraints(c_0_ub)_: @@ -1212,544 +1210,546 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(35)__t(B)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ -+8.0 NoClash(B_G_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ ++2.0 NoClash(C_D_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ --92 NoClash(B_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ --92 NoClash(B_G_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_G_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ ++9.0 NoClash(C_D_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ --92 NoClash(B_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ --92 NoClash(B_G_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ -+4.0 NoClash(B_F_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ ++5.0 NoClash(C_D_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ --92 NoClash(B_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ --92 NoClash(B_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ -+5.0 NoClash(B_F_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ ++2.0 NoClash(C_D_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ --92 NoClash(B_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ +-92 NoClash(C_D_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ --92 NoClash(B_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ +-92 NoClash(C_D_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ -+5.0 NoClash(B_E_5_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ +-2.0 NoClash(C_E_2_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(E)_ --92 NoClash(B_E_5_0)_binary_indicator_var +-92 NoClash(C_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ --92 NoClash(B_E_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ +-92 NoClash(C_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ ++9.0 NoClash(C_E_2_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)__t(E)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(E)_ --92 NoClash(B_E_5_1)_binary_indicator_var +-92 NoClash(C_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ --92 NoClash(B_E_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ +-92 NoClash(C_E_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ -+7.0 NoClash(B_E_3_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ ++2.0 NoClash(C_F_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ --92 NoClash(B_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ --92 NoClash(B_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_E_3_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ ++6.0 NoClash(C_F_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ --92 NoClash(B_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ --92 NoClash(B_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ -+4.0 NoClash(B_E_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ ++5.0 NoClash(C_F_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ --92 NoClash(B_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ --92 NoClash(B_E_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_E_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ ++8.0 NoClash(C_F_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ --92 NoClash(B_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ +-92 NoClash(C_F_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ --92 NoClash(B_E_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ +-92 NoClash(C_F_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ -+10.0 NoClash(B_D_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ ++2.0 NoClash(C_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ --92 NoClash(B_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ --92 NoClash(B_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ --1 NoClash(B_D_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ ++9.0 NoClash(C_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ --92 NoClash(B_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ --92 NoClash(B_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ -+8.0 NoClash(B_D_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ ++4.0 NoClash(C_G_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ --92 NoClash(B_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ --92 NoClash(B_D_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ -+3.0 NoClash(B_D_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ ++7.0 NoClash(C_G_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ --92 NoClash(B_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ +-92 NoClash(C_G_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ --92 NoClash(B_D_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ +-92 NoClash(C_G_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ -+9.0 NoClash(B_C_2_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ ++4.0 NoClash(D_E_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ --92 NoClash(B_C_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ --92 NoClash(B_C_2_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ --3.0 NoClash(B_C_2_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ ++8.0 NoClash(D_E_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ --92 NoClash(B_C_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ --92 NoClash(B_C_2_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ -+9.0 NoClash(A_G_5_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ ++2.0 NoClash(D_E_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ --92 NoClash(A_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ --92 NoClash(A_G_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ --3.0 NoClash(A_G_5_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ ++9.0 NoClash(D_E_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ --92 NoClash(A_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ +-92 NoClash(D_E_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ --92 NoClash(A_G_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ +-92 NoClash(D_E_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_F_3_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ +-1 NoClash(D_F_3_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(F)_ --92 NoClash(A_F_3_0)_binary_indicator_var +-92 NoClash(D_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ --92 NoClash(A_F_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ -+6.0 NoClash(A_F_3_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ ++11.0 NoClash(D_F_3_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(F)_ --92 NoClash(A_F_3_1)_binary_indicator_var +-92 NoClash(D_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ --92 NoClash(A_F_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_transformedConstraints(c_0_ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(F)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ -+2.0 NoClash(A_F_1_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ ++1 NoClash(D_F_4_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(F)_ --92 NoClash(A_F_1_0)_binary_indicator_var +-92 NoClash(D_F_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ --92 NoClash(A_F_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_transformedConstraints(c_0_ub)_: -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(F)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ -+3.0 NoClash(A_F_1_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ ++7.0 NoClash(D_F_4_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)__t(F)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(F)_ --92 NoClash(A_F_1_1)_binary_indicator_var +-92 NoClash(D_F_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ --92 NoClash(A_F_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ +-92 NoClash(D_F_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_E_5_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ ++8.0 NoClash(D_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ --92 NoClash(A_E_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ --92 NoClash(A_E_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ ++8.0 NoClash(D_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ --92 NoClash(A_E_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ --92 NoClash(A_E_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ -+7.0 NoClash(A_E_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ --92 NoClash(A_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ --92 NoClash(A_E_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_E_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ ++6.0 NoClash(D_G_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ --92 NoClash(A_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ +-92 NoClash(D_G_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ --92 NoClash(A_E_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ +-92 NoClash(D_G_4_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ -+10.0 NoClash(A_D_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ ++3.0 NoClash(E_F_3_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ --92 NoClash(A_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ +-92 NoClash(E_F_3_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ --92 NoClash(A_D_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ +-92 NoClash(E_F_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ ++8.0 NoClash(E_F_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ --92 NoClash(A_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ +-92 NoClash(E_F_3_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ --92 NoClash(A_D_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ +-92 NoClash(E_F_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ -+6.0 NoClash(A_C_1_0)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ ++8.0 NoClash(E_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ --92 NoClash(A_C_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ --92 NoClash(A_C_1_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ -+3.0 NoClash(A_C_1_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ ++4.0 NoClash(E_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ --92 NoClash(A_C_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ --92 NoClash(A_C_1_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ -+2.0 NoClash(A_B_5_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ ++7.0 NoClash(E_G_5_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ --92 NoClash(A_B_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_5_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ --92 NoClash(A_B_5_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_5_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ -+3.0 NoClash(A_B_5_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ +-1 NoClash(E_G_5_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ --92 NoClash(A_B_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ +-92 NoClash(E_G_5_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ --92 NoClash(A_B_5_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ +-92 NoClash(E_G_5_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ -+4.0 NoClash(A_B_3_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ ++6.0 NoClash(F_G_4_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ --92 NoClash(A_B_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ +-92 NoClash(F_G_4_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ --92 NoClash(A_B_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ +-92 NoClash(F_G_4_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ -+5.0 NoClash(A_B_3_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ ++6.0 NoClash(F_G_4_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ --92 NoClash(A_B_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ +-92 NoClash(F_G_4_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ --92 NoClash(A_B_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ +-92 NoClash(F_G_4_1)_binary_indicator_var <= 0 bounds @@ -1761,146 +1761,146 @@ bounds 0 <= t(E) <= 92 0 <= t(F) <= 92 0 <= t(G) <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(8)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(9)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(28)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(29)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(35)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(35)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(36)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(37)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(38)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(39)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(G)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(46)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(47)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(54)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(55)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(F)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(68)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(69)_disaggregatedVars__t(F)_ <= 92 0 <= NoClash(A_B_3_0)_binary_indicator_var <= 1 0 <= NoClash(A_B_3_1)_binary_indicator_var <= 1 0 <= NoClash(A_B_5_0)_binary_indicator_var <= 1 diff --git a/pyomo/gdp/tests/jobshop_small_hull.lp b/pyomo/gdp/tests/jobshop_small_hull.lp index c07b9cd048e..ae2d738d29c 100644 --- a/pyomo/gdp/tests/jobshop_small_hull.lp +++ b/pyomo/gdp/tests/jobshop_small_hull.lp @@ -22,29 +22,29 @@ c_u_Feas(C)_: <= -6 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(0)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(1)_: +1 t(B) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(2)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(1)_: ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(2)_: +1 t(A) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: +1 t(B) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ @@ -52,9 +52,9 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ = 0 c_e__pyomo_gdp_hull_reformulation_disj_xor(A_B_3)_: @@ -73,98 +73,98 @@ c_e__pyomo_gdp_hull_reformulation_disj_xor(B_C_2)_: = 1 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ -+6.0 NoClash(B_C_2_0)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ --19 NoClash(B_C_2_0)_binary_indicator_var -<= 0 - c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(B)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ --19 NoClash(B_C_2_0)_binary_indicator_var +-19 NoClash(A_B_3_0)_binary_indicator_var +<= 0 + +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ +-19 NoClash(A_B_3_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ -+1 NoClash(B_C_2_1)_binary_indicator_var +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ ++5.0 NoClash(A_B_3_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ --19 NoClash(B_C_2_1)_binary_indicator_var -<= 0 - c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(B)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ --19 NoClash(B_C_2_1)_binary_indicator_var +-19 NoClash(A_B_3_1)_binary_indicator_var +<= 0 + +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ +-19 NoClash(A_B_3_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ +2.0 NoClash(A_C_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ -19 NoClash(A_C_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ -19 NoClash(A_C_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +5.0 NoClash(A_C_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ -19 NoClash(A_C_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ -19 NoClash(A_C_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ ++6.0 NoClash(B_C_2_0)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(B)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ --19 NoClash(A_B_3_0)_binary_indicator_var +-19 NoClash(B_C_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ --19 NoClash(A_B_3_0)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-19 NoClash(B_C_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ -+5.0 NoClash(A_B_3_1)_binary_indicator_var ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ ++1 NoClash(B_C_2_1)_binary_indicator_var <= 0.0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(B)_bounds_(ub)_: +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ --19 NoClash(A_B_3_1)_binary_indicator_var +-19 NoClash(B_C_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ --19 NoClash(A_B_3_1)_binary_indicator_var +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ +-19 NoClash(B_C_2_1)_binary_indicator_var <= 0 bounds @@ -172,18 +172,18 @@ bounds 0 <= t(A) <= 19 0 <= t(B) <= 19 0 <= t(C) <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(C)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(C)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(B)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ <= 19 0 <= NoClash(A_B_3_0)_binary_indicator_var <= 1 0 <= NoClash(A_B_3_1)_binary_indicator_var <= 1 0 <= NoClash(A_C_1_0)_binary_indicator_var <= 1 From 8ce0ce9657aac63f3013e9c419eec2a5870e3248 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sat, 17 Feb 2024 20:56:12 -0700 Subject: [PATCH 0601/3044] Changing FME tests that use hull, because I changed the order of transformation --- .../contrib/fme/tests/test_fourier_motzkin_elimination.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py index 11c008acf82..e997e138724 100644 --- a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py +++ b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py @@ -435,7 +435,7 @@ def check_hull_projected_constraints(self, m, constraints, indices): self.assertIs(body.linear_vars[2], m.startup.binary_indicator_var) self.assertEqual(body.linear_coefs[2], 2) - # 1 <= time1_disjuncts[0].ind_var + time_1.disjuncts[1].ind_var + # 1 <= time1_disjuncts[0].ind_var + time1_disjuncts[1].ind_var cons = constraints[indices[7]] self.assertEqual(cons.lower, 1) self.assertIsNone(cons.upper) @@ -548,12 +548,12 @@ def test_project_disaggregated_vars(self): # we of course get tremendous amounts of garbage, but we make sure that # what should be here is: self.check_hull_projected_constraints( - m, constraints, [23, 19, 8, 10, 54, 67, 35, 3, 4, 1, 2] + m, constraints, [16, 12, 69, 71, 47, 60, 28, 1, 2, 3, 4] ) # and when we filter, it's still there. constraints = filtered._pyomo_contrib_fme_transformation.projected_constraints self.check_hull_projected_constraints( - filtered, constraints, [10, 8, 5, 6, 15, 19, 11, 3, 4, 1, 2] + filtered, constraints, [8, 6, 20, 21, 13, 17, 9, 1, 2, 3, 4] ) @unittest.skipIf(not 'glpk' in solvers, 'glpk not available') @@ -570,7 +570,7 @@ def test_post_processing(self): # They should be the same as the above, but now these are *all* the # constraints self.check_hull_projected_constraints( - m, constraints, [10, 8, 5, 6, 15, 19, 11, 3, 4, 1, 2] + m, constraints, [8, 6, 20, 21, 13, 17, 9, 1, 2, 3, 4] ) # and check that we didn't change the model From 1491523e3eeca485fe3885605397a93437c76b28 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 18 Feb 2024 14:07:29 -0700 Subject: [PATCH 0602/3044] Changing so that we trust any solver with a name that contains a trusted solver name for now, and adding a TODO about how the future will be better. --- pyomo/gdp/plugins/multiple_bigm.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 5e23a706361..0dc6d76eb6e 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -65,10 +65,10 @@ 'cbc', 'glpk', 'scip', - 'xpress_direct', - 'mosek_direct', + 'xpress', + 'mosek', 'baron', - 'appsi_highs', + 'highs', } @@ -670,10 +670,17 @@ def _solve_disjunct_for_M( self, other_disjunct, scratch_block, unsuccessful_solve_msg ): solver = self._config.solver - solver_trusted = solver.name in _trusted_solvers results = solver.solve(other_disjunct, load_solutions=False) if results.solver.termination_condition is TerminationCondition.infeasible: - if solver_trusted: + # [2/18/24]: TODO: After the solver rewrite is complete, we will not + # need this check since we can actually determine from the + # termination condition whether or not the solver proved + # infeasibility or just terminated at local infeasiblity. For now, + # while this is not complete, it catches most of the solvers we + # trust, and, unless someone is so pathological as to *rename* an + # untrusted solver using a trusted solver name, it will never do the + # *wrong* thing. + if any(s in solver.name for s in _trusted_solvers): logger.debug( "Disjunct '%s' is infeasible, deactivating." % other_disjunct.name ) From 353939782eb0896527598253b35fd8a23a7d948f Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 18 Feb 2024 16:29:55 -0500 Subject: [PATCH 0603/3044] Make `IsInstance` module qualifiers optional --- pyomo/common/config.py | 29 +++++++++++++++++++++++------ pyomo/common/tests/test_config.py | 25 +++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 92613266885..4207392389a 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -310,11 +310,16 @@ class IsInstance(object): ---------- *bases : tuple of type Valid types. + document_full_base_names : bool, optional + True to prepend full module qualifier to the name of each + member of `bases` in ``self.domain_name()`` and/or any + error messages generated by this object, False otherwise. """ - def __init__(self, *bases): + def __init__(self, *bases, document_full_base_names=False): assert bases self.baseClasses = bases + self.document_full_base_names = document_full_base_names @staticmethod def _fullname(klass): @@ -325,29 +330,41 @@ def _fullname(klass): module_qual = "" if module_name == "builtins" else f"{module_name}." return f"{module_qual}{klass.__name__}" + def _get_class_name(self, klass): + """ + Get name of class. Module qualifier may be included, + depending on value of `self.document_full_base_names`. + """ + if self.document_full_base_names: + return self._fullname(klass) + else: + return klass.__name__ + def __call__(self, obj): if isinstance(obj, self.baseClasses): return obj if len(self.baseClasses) > 1: class_names = ", ".join( - f"{self._fullname(kls)!r}" for kls in self.baseClasses + f"{self._get_class_name(kls)!r}" for kls in self.baseClasses ) msg = ( "Expected an instance of one of these types: " f"{class_names}, but received value {obj!r} of type " - f"{self._fullname(type(obj))!r}" + f"{self._get_class_name(type(obj))!r}" ) else: msg = ( f"Expected an instance of " - f"{self._fullname(self.baseClasses[0])!r}, " - f"but received value {obj!r} of type {self._fullname(type(obj))!r}" + f"{self._get_class_name(self.baseClasses[0])!r}, " + f"but received value {obj!r} of type " + f"{self._get_class_name(type(obj))!r}" ) raise ValueError(msg) def domain_name(self): + class_names = (self._get_class_name(kls) for kls in self.baseClasses) return ( - f"IsInstance({', '.join(self._fullname(kls) for kls in self.baseClasses)})" + f"IsInstance({', '.join(class_names)})" ) diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 068017d836f..f3f5cbedad6 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -469,13 +469,18 @@ def __repr__(self): c.val2 = testinst self.assertEqual(c.val2, testinst) exc_str = ( - r"Expected an instance of '.*\.TestClass', " + r"Expected an instance of 'TestClass', " "but received value 2.4 of type 'float'" ) with self.assertRaisesRegex(ValueError, exc_str): c.val2 = 2.4 - c.declare("val3", ConfigValue(None, IsInstance(int, TestClass))) + c.declare( + "val3", + ConfigValue( + None, IsInstance(int, TestClass, document_full_base_names=True) + ), + ) self.assertRegex( c.get("val3").domain_name(), r"IsInstance\(int, .*\.TestClass\)" ) @@ -488,6 +493,22 @@ def __repr__(self): with self.assertRaisesRegex(ValueError, exc_str): c.val3 = 2.4 + c.declare( + "val4", + ConfigValue( + None, IsInstance(int, TestClass, document_full_base_names=False) + ), + ) + self.assertEqual(c.get("val4").domain_name(), "IsInstance(int, TestClass)") + c.val4 = 2 + self.assertEqual(c.val4, 2) + exc_str = ( + r"Expected an instance of one of these types: 'int', 'TestClass'" + r", but received value 2.4 of type 'float'" + ) + with self.assertRaisesRegex(ValueError, exc_str): + c.val4 = 2.4 + def test_Path(self): def norm(x): if cwd[1] == ':' and x[0] == '/': From bbba7629703ec3461fd8287a2c4bc6e26e29a558 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 18 Feb 2024 16:35:35 -0500 Subject: [PATCH 0604/3044] Add `IsInstance` to config library reference docs --- doc/OnlineDocs/library_reference/common/config.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/OnlineDocs/library_reference/common/config.rst b/doc/OnlineDocs/library_reference/common/config.rst index 7a400b26ce3..c5dc607977a 100644 --- a/doc/OnlineDocs/library_reference/common/config.rst +++ b/doc/OnlineDocs/library_reference/common/config.rst @@ -36,6 +36,7 @@ Domain validators NonPositiveFloat NonNegativeFloat In + IsInstance InEnum ListOf Module @@ -75,6 +76,7 @@ Domain validators .. autofunction:: NonPositiveFloat .. autofunction:: NonNegativeFloat .. autoclass:: In +.. autoclass:: IsInstance .. autoclass:: InEnum .. autoclass:: ListOf .. autoclass:: Module From 0fc42f1923a68259f362c40227fd8da7c78b5b94 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 18 Feb 2024 16:40:18 -0500 Subject: [PATCH 0605/3044] Implement `Path.domain_name()` --- pyomo/common/config.py | 3 +++ pyomo/common/tests/test_config.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 4207392389a..8ffb162ac41 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -555,6 +555,9 @@ def __call__(self, path): ) return ans + def domain_name(self): + return type(self).__name__ + class PathList(Path): """Domain validator for a list of path-like objects. diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index f3f5cbedad6..6c657e8d04b 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -526,6 +526,8 @@ def __str__(self): path_str = str(self.path) return f"{type(self).__name__}({path_str})" + self.assertEqual(Path().domain_name(), "Path") + cwd = os.getcwd() + os.path.sep c = ConfigDict() From 7df175f4d1fd29ce65d37a3d3bed0dabb563d458 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 18 Feb 2024 16:09:58 -0700 Subject: [PATCH 0606/3044] Fixing BigM to not assume nested indicator vars are local, editing its tests accordingly --- pyomo/gdp/plugins/bigm.py | 5 +- .../gdp/plugins/gdp_to_mip_transformation.py | 32 ++--- pyomo/gdp/tests/test_bigm.py | 117 ++++++++---------- 3 files changed, 75 insertions(+), 79 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index bdd353a6136..1f9f561b192 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -224,11 +224,10 @@ def _transform_disjunctionData( or_expr += disjunct.binary_indicator_var self._transform_disjunct(disjunct, bigM, transBlock) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var if obj.xor: - xorConstraint[index] = or_expr == rhs + xorConstraint[index] = or_expr == 1 else: - xorConstraint[index] = or_expr >= rhs + xorConstraint[index] = or_expr >= 1 # Mark the DisjunctionData as transformed by mapping it to its XOR # constraint. obj._algebraic_constraint = weakref_ref(xorConstraint[index]) diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 96d97206c97..5603259a278 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -213,21 +213,25 @@ def _setup_transform_disjunctionData(self, obj, root_disjunct): "likely indicative of a modeling error." % obj.name ) - # Create or fetch the transformation block + # We always need to create or fetch a transformation block on the parent block. + trans_block, new_block = self._add_transformation_block( + obj.parent_block()) + # This is where we put exactly_one/or constraint + algebraic_constraint = self._add_xor_constraint(obj.parent_component(), + trans_block) + + # If requested, create or fetch the transformation block above the + # nested hierarchy if root_disjunct is not None: - # We want to put all the transformed things on the root - # Disjunct's parent's block so that they do not get - # re-transformed - transBlock, new_block = self._add_transformation_block( - root_disjunct.parent_block() - ) - else: - # This isn't nested--just put it on the parent block. - transBlock, new_block = self._add_transformation_block(obj.parent_block()) - - xorConstraint = self._add_xor_constraint(obj.parent_component(), transBlock) - - return transBlock, xorConstraint + # We want to put some transformed things on the root Disjunct's + # parent's block so that they do not get re-transformed. (Note this + # is never true for hull, but it calls this method with + # root_disjunct=None. BigM can't put the exactly-one constraint up + # here, but it can put everything else.) + trans_block, new_block = self._add_transformation_block( + root_disjunct.parent_block() ) + + return trans_block, algebraic_constraint def _get_disjunct_transformation_block(self, disjunct, transBlock): if disjunct.transformation_block is not None: diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index d518219eabd..f210f3cd660 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -33,6 +33,7 @@ assertExpressionsStructurallyEqual, ) from pyomo.repn import generate_standard_repn +from pyomo.repn.linear import LinearRepnVisitor from pyomo.common.log import LoggingIntercept import logging @@ -1764,22 +1765,19 @@ def test_transformation_block_structure(self): # we have the XOR constraints for both the outer and inner disjunctions self.assertIsInstance(transBlock.component("disjunction_xor"), Constraint) - def test_transformation_block_on_inner_disjunct_empty(self): - m = models.makeNestedDisjunctions() - TransformationFactory('gdp.bigm').apply_to(m) - self.assertIsNone(m.disjunct[1].component("_pyomo_gdp_bigm_reformulation")) - def test_mappings_between_disjunctions_and_xors(self): m = models.makeNestedDisjunctions() transform = TransformationFactory('gdp.bigm') transform.apply_to(m) transBlock1 = m.component("_pyomo_gdp_bigm_reformulation") + transBlock2 = m.disjunct[1].component("_pyomo_gdp_bigm_reformulation") + transBlock3 = m.simpledisjunct.component("_pyomo_gdp_bigm_reformulation") disjunctionPairs = [ (m.disjunction, transBlock1.disjunction_xor), - (m.disjunct[1].innerdisjunction[0], transBlock1.innerdisjunction_xor_4[0]), - (m.simpledisjunct.innerdisjunction, transBlock1.innerdisjunction_xor), + (m.disjunct[1].innerdisjunction[0], transBlock2.innerdisjunction_xor[0]), + (m.simpledisjunct.innerdisjunction, transBlock3.innerdisjunction_xor), ] # check disjunction mappings @@ -1900,18 +1898,39 @@ def check_bigM_constraint(self, cons, variable, M, indicator_var): ct.check_linear_coef(self, repn, indicator_var, M) def check_inner_xor_constraint( - self, inner_disjunction, outer_disjunct, inner_disjuncts + self, inner_disjunction, outer_disjunct, bigm ): - self.assertIsNotNone(inner_disjunction.algebraic_constraint) - cons = inner_disjunction.algebraic_constraint - self.assertEqual(cons.lower, 0) - self.assertEqual(cons.upper, 0) - repn = generate_standard_repn(cons.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - for disj in inner_disjuncts: - ct.check_linear_coef(self, repn, disj.binary_indicator_var, 1) - ct.check_linear_coef(self, repn, outer_disjunct.binary_indicator_var, -1) + inner_xor = inner_disjunction.algebraic_constraint + sum_indicators = sum(d.binary_indicator_var for d in + inner_disjunction.disjuncts) + assertExpressionsEqual( + self, + inner_xor.expr, + sum_indicators == 1 + ) + # this guy has been transformed + self.assertFalse(inner_xor.active) + cons = bigm.get_transformed_constraints(inner_xor) + self.assertEqual(len(cons), 2) + lb = cons[0] + ct.check_obj_in_active_tree(self, lb) + lb_expr = self.simplify_cons(lb, leq=False) + assertExpressionsEqual( + self, + lb_expr, + 1.0 <= + sum_indicators + - outer_disjunct.binary_indicator_var + 1.0 + ) + ub = cons[1] + ct.check_obj_in_active_tree(self, ub) + ub_expr = self.simplify_cons(ub, leq=True) + assertExpressionsEqual( + self, + ub_expr, + sum_indicators + + outer_disjunct.binary_indicator_var - 1 <= 1.0 + ) def test_transformed_constraints(self): # We'll check all the transformed constraints to make sure @@ -1993,26 +2012,8 @@ def test_transformed_constraints(self): # Here we check that the xor constraint from # simpledisjunct.innerdisjunction is transformed. - cons5 = m.simpledisjunct.innerdisjunction.algebraic_constraint - self.assertIsNotNone(cons5) - self.check_inner_xor_constraint( - m.simpledisjunct.innerdisjunction, - m.simpledisjunct, - [m.simpledisjunct.innerdisjunct0, m.simpledisjunct.innerdisjunct1], - ) - self.assertIsInstance(cons5, Constraint) - self.assertEqual(cons5.lower, 0) - self.assertEqual(cons5.upper, 0) - repn = generate_standard_repn(cons5.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef( - self, repn, m.simpledisjunct.innerdisjunct0.binary_indicator_var, 1 - ) - ct.check_linear_coef( - self, repn, m.simpledisjunct.innerdisjunct1.binary_indicator_var, 1 - ) - ct.check_linear_coef(self, repn, m.simpledisjunct.binary_indicator_var, -1) + self.check_inner_xor_constraint(m.simpledisjunct.innerdisjunction, + m.simpledisjunct, bigm) cons6 = bigm.get_transformed_constraints(m.disjunct[0].c) self.assertEqual(len(cons6), 2) @@ -2029,8 +2030,7 @@ def test_transformed_constraints(self): # is correct. self.check_inner_xor_constraint( m.disjunct[1].innerdisjunction[0], - m.disjunct[1], - [m.disjunct[1].innerdisjunct[0], m.disjunct[1].innerdisjunct[1]], + m.disjunct[1], bigm ) cons8 = bigm.get_transformed_constraints(m.disjunct[1].c) @@ -2136,34 +2136,27 @@ def check_second_disjunct_constraint(self, disj2c, x, ind_var): ct.check_squared_term_coef(self, repn, x[i], 1) ct.check_linear_coef(self, repn, x[i], -6) + def simplify_cons(self, cons, leq): + visitor = LinearRepnVisitor({}, {}, {}, None) + repn = visitor.walk_expression(cons.body) + self.assertIsNone(repn.nonlinear) + if leq: + self.assertIsNone(cons.lower) + ub = cons.upper + return ub >= repn.to_expression(visitor) + else: + self.assertIsNone(cons.upper) + lb = cons.lower + return lb <= repn.to_expression(visitor) + def check_hierarchical_nested_model(self, m, bigm): outer_xor = m.disjunction_block.disjunction.algebraic_constraint ct.check_two_term_disjunction_xor( self, outer_xor, m.disj1, m.disjunct_block.disj2 ) - inner_xor = m.disjunct_block.disj2.disjunction.algebraic_constraint - self.assertEqual(inner_xor.lower, 0) - self.assertEqual(inner_xor.upper, 0) - repn = generate_standard_repn(inner_xor.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(len(repn.linear_vars), 3) - self.assertEqual(repn.constant, 0) - ct.check_linear_coef( - self, - repn, - m.disjunct_block.disj2.disjunction_disjuncts[0].binary_indicator_var, - 1, - ) - ct.check_linear_coef( - self, - repn, - m.disjunct_block.disj2.disjunction_disjuncts[1].binary_indicator_var, - 1, - ) - ct.check_linear_coef( - self, repn, m.disjunct_block.disj2.binary_indicator_var, -1 - ) + self.check_inner_xor_constraint(m.disjunct_block.disj2.disjunction, + m.disjunct_block.disj2, bigm) # outer disjunction constraints disj1c = bigm.get_transformed_constraints(m.disj1.c) From 1e8f359ad5cf8c4c14f2f7b3a49584ca9cbc0a56 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 18 Feb 2024 16:10:45 -0700 Subject: [PATCH 0607/3044] Black --- .../gdp/plugins/gdp_to_mip_transformation.py | 11 ++++--- pyomo/gdp/tests/test_bigm.py | 33 ++++++++----------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 5603259a278..59cb221321a 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -214,11 +214,11 @@ def _setup_transform_disjunctionData(self, obj, root_disjunct): ) # We always need to create or fetch a transformation block on the parent block. - trans_block, new_block = self._add_transformation_block( - obj.parent_block()) + trans_block, new_block = self._add_transformation_block(obj.parent_block()) # This is where we put exactly_one/or constraint - algebraic_constraint = self._add_xor_constraint(obj.parent_component(), - trans_block) + algebraic_constraint = self._add_xor_constraint( + obj.parent_component(), trans_block + ) # If requested, create or fetch the transformation block above the # nested hierarchy @@ -229,7 +229,8 @@ def _setup_transform_disjunctionData(self, obj, root_disjunct): # root_disjunct=None. BigM can't put the exactly-one constraint up # here, but it can put everything else.) trans_block, new_block = self._add_transformation_block( - root_disjunct.parent_block() ) + root_disjunct.parent_block() + ) return trans_block, algebraic_constraint diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index f210f3cd660..daec9a20c93 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1897,17 +1897,12 @@ def check_bigM_constraint(self, cons, variable, M, indicator_var): ct.check_linear_coef(self, repn, variable, 1) ct.check_linear_coef(self, repn, indicator_var, M) - def check_inner_xor_constraint( - self, inner_disjunction, outer_disjunct, bigm - ): + def check_inner_xor_constraint(self, inner_disjunction, outer_disjunct, bigm): inner_xor = inner_disjunction.algebraic_constraint - sum_indicators = sum(d.binary_indicator_var for d in - inner_disjunction.disjuncts) - assertExpressionsEqual( - self, - inner_xor.expr, - sum_indicators == 1 + sum_indicators = sum( + d.binary_indicator_var for d in inner_disjunction.disjuncts ) + assertExpressionsEqual(self, inner_xor.expr, sum_indicators == 1) # this guy has been transformed self.assertFalse(inner_xor.active) cons = bigm.get_transformed_constraints(inner_xor) @@ -1918,9 +1913,7 @@ def check_inner_xor_constraint( assertExpressionsEqual( self, lb_expr, - 1.0 <= - sum_indicators - - outer_disjunct.binary_indicator_var + 1.0 + 1.0 <= sum_indicators - outer_disjunct.binary_indicator_var + 1.0, ) ub = cons[1] ct.check_obj_in_active_tree(self, ub) @@ -1928,8 +1921,7 @@ def check_inner_xor_constraint( assertExpressionsEqual( self, ub_expr, - sum_indicators - + outer_disjunct.binary_indicator_var - 1 <= 1.0 + sum_indicators + outer_disjunct.binary_indicator_var - 1 <= 1.0, ) def test_transformed_constraints(self): @@ -2012,8 +2004,9 @@ def test_transformed_constraints(self): # Here we check that the xor constraint from # simpledisjunct.innerdisjunction is transformed. - self.check_inner_xor_constraint(m.simpledisjunct.innerdisjunction, - m.simpledisjunct, bigm) + self.check_inner_xor_constraint( + m.simpledisjunct.innerdisjunction, m.simpledisjunct, bigm + ) cons6 = bigm.get_transformed_constraints(m.disjunct[0].c) self.assertEqual(len(cons6), 2) @@ -2029,8 +2022,7 @@ def test_transformed_constraints(self): # now we check that the xor constraint from disjunct[1].innerdisjunction # is correct. self.check_inner_xor_constraint( - m.disjunct[1].innerdisjunction[0], - m.disjunct[1], bigm + m.disjunct[1].innerdisjunction[0], m.disjunct[1], bigm ) cons8 = bigm.get_transformed_constraints(m.disjunct[1].c) @@ -2155,8 +2147,9 @@ def check_hierarchical_nested_model(self, m, bigm): self, outer_xor, m.disj1, m.disjunct_block.disj2 ) - self.check_inner_xor_constraint(m.disjunct_block.disj2.disjunction, - m.disjunct_block.disj2, bigm) + self.check_inner_xor_constraint( + m.disjunct_block.disj2.disjunction, m.disjunct_block.disj2, bigm + ) # outer disjunction constraints disj1c = bigm.get_transformed_constraints(m.disj1.c) From 263d873c195d9a469b79818cf7d1fcc422c6e492 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 18 Feb 2024 16:18:29 -0700 Subject: [PATCH 0608/3044] Fixing the algebraic constraint for mbigm to be correct for nested GDPs which is ironic because mbigm doesn't currently support nested GDPs. --- pyomo/gdp/plugins/multiple_bigm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index a2e7d5beeec..6177de3c037 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -336,8 +336,7 @@ def _transform_disjunctionData(self, obj, index, parent_disjunct, root_disjunct) for disjunct in active_disjuncts: or_expr += disjunct.indicator_var.get_associated_binary() self._transform_disjunct(disjunct, transBlock, active_disjuncts, Ms) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var - algebraic_constraint.add(index, (or_expr, rhs)) + algebraic_constraint.add(index, or_expr == 1) # map the DisjunctionData to its XOR constraint to mark it as # transformed obj._algebraic_constraint = weakref_ref(algebraic_constraint[index]) From 664f3026181ac51f01e5d65abff03fc89b424cf0 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 18 Feb 2024 16:21:24 -0700 Subject: [PATCH 0609/3044] Correcting binary multiplication transformation's handling of nested GDP --- pyomo/gdp/plugins/binary_multiplication.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyomo/gdp/plugins/binary_multiplication.py b/pyomo/gdp/plugins/binary_multiplication.py index ef4239e09dc..d68f7efe76f 100644 --- a/pyomo/gdp/plugins/binary_multiplication.py +++ b/pyomo/gdp/plugins/binary_multiplication.py @@ -92,11 +92,10 @@ def _transform_disjunctionData( or_expr += disjunct.binary_indicator_var self._transform_disjunct(disjunct, transBlock) - rhs = 1 if parent_disjunct is None else parent_disjunct.binary_indicator_var if obj.xor: - xorConstraint[index] = or_expr == rhs + xorConstraint[index] = or_expr == 1 else: - xorConstraint[index] = or_expr >= rhs + xorConstraint[index] = or_expr >= 1 # Mark the DisjunctionData as transformed by mapping it to its XOR # constraint. obj._algebraic_constraint = weakref_ref(xorConstraint[index]) From 019818456cea31153366f8b11f5e69465e226770 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 18 Feb 2024 18:37:14 -0500 Subject: [PATCH 0610/3044] Update documentation of `Path` --- pyomo/common/config.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 8ffb162ac41..8f5ac513ee4 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -490,9 +490,14 @@ def __call__(self, module_id): class Path(object): - """Domain validator for path-like options. + """ + Domain validator for a + :py:term:`path-like object `. - This will admit any object and convert it to a string. It will then + This will admit a path-like object + and get the object's file system representation + through :py:obj:`os.fsdecode`. + It will then expand any environment variables and leading usernames (e.g., "~myuser" or "~/") appearing in either the value or the base path before concatenating the base path and value, expanding the path to From e4d9d796b45701982c4643b0e5e4ee064893f48a Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 18 Feb 2024 16:42:38 -0700 Subject: [PATCH 0611/3044] Adding an integration test for correct handling of nested with non-local indicator vars --- pyomo/gdp/tests/common_tests.py | 13 ++++++++ pyomo/gdp/tests/models.py | 32 +++++++++++++++++++ pyomo/gdp/tests/test_bigm.py | 4 +++ pyomo/gdp/tests/test_binary_multiplication.py | 11 +++++++ pyomo/gdp/tests/test_hull.py | 4 +++ 5 files changed, 64 insertions(+) diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index b76de61887f..585aafc967d 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -1944,3 +1944,16 @@ def check_nested_disjuncts_in_flat_gdp(self, transformation): for t in m.T: self.assertTrue(value(m.disj1[t].indicator_var)) self.assertTrue(value(m.disj1[t].sub1.indicator_var)) + +def check_do_not_assume_nested_indicators_local(self, transformation): + m = models.why_indicator_vars_are_not_always_local() + TransformationFactory(transformation).apply_to(m) + + results = SolverFactory('gurobi').solve(m) + self.assertEqual(results.solver.termination_condition, TerminationCondition.optimal) + self.assertAlmostEqual(value(m.obj), 9) + self.assertAlmostEqual(value(m.x), 9) + self.assertTrue(value(m.Y2.indicator_var)) + self.assertFalse(value(m.Y1.indicator_var)) + self.assertTrue(value(m.Z1.indicator_var)) + self.assertTrue(value(m.Z1.indicator_var)) diff --git a/pyomo/gdp/tests/models.py b/pyomo/gdp/tests/models.py index 94bf5d0e592..fc5e6327c7e 100644 --- a/pyomo/gdp/tests/models.py +++ b/pyomo/gdp/tests/models.py @@ -563,6 +563,38 @@ def makeNestedDisjunctions_NestedDisjuncts(): return m +def why_indicator_vars_are_not_always_local(): + m = ConcreteModel() + m.x = Var(bounds=(1, 10)) + @m.Disjunct() + def Z1(d): + m = d.model() + d.c = Constraint(expr=m.x >= 1.1) + @m.Disjunct() + def Z2(d): + m = d.model() + d.c = Constraint(expr=m.x >= 1.2) + @m.Disjunct() + def Y1(d): + m = d.model() + d.c = Constraint(expr=(1.15, m.x, 8)) + d.disjunction = Disjunction(expr=[m.Z1, m.Z2]) + @m.Disjunct() + def Y2(d): + m = d.model() + d.c = Constraint(expr=m.x==9) + m.disjunction = Disjunction(expr=[m.Y1, m.Y2]) + + m.logical_cons = LogicalConstraint(expr=m.Y2.indicator_var.implies( + m.Z1.indicator_var.land(m.Z2.indicator_var))) + + # optimal value is 9, but it will be 8 if we wrongly assume that the nested + # indicator_vars are local. + m.obj = Objective(expr=m.x, sense=maximize) + + return m + + def makeTwoSimpleDisjunctions(): """Two SimpleDisjunctions on the same model.""" m = ConcreteModel() diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index daec9a20c93..00efcb46485 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -2200,6 +2200,10 @@ def test_decl_order_opposite_instantiation_order(self): # the same check to make sure everything is transformed correctly. self.check_hierarchical_nested_model(m, bigm) + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_do_not_assume_nested_indicators_local(self): + ct.check_do_not_assume_nested_indicators_local(self, 'gdp.bigm') + class IndexedDisjunction(unittest.TestCase): # this tests that if the targets are a subset of the diff --git a/pyomo/gdp/tests/test_binary_multiplication.py b/pyomo/gdp/tests/test_binary_multiplication.py index 5f4c4f90ab6..fbe6f86fd46 100644 --- a/pyomo/gdp/tests/test_binary_multiplication.py +++ b/pyomo/gdp/tests/test_binary_multiplication.py @@ -18,6 +18,7 @@ ConcreteModel, Var, Any, + SolverFactory, ) from pyomo.gdp import Disjunct, Disjunction from pyomo.core.expr.compare import assertExpressionsEqual @@ -30,6 +31,11 @@ import random +gurobi_available = ( + SolverFactory('gurobi').available(exception_flag=False) + and SolverFactory('gurobi').license_is_valid() +) + class CommonTests: def diff_apply_to_and_create_using(self, model): @@ -297,5 +303,10 @@ def test_local_var(self): self.assertEqual(eq.ub, 0) +class TestNestedGDP(unittest.TestCase): + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_do_not_assume_nested_indicators_local(self): + ct.check_do_not_assume_nested_indicators_local(self, 'gdp.binary_multiplication') + if __name__ == '__main__': unittest.main() diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 694178ee96f..858764759ee 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -2030,6 +2030,10 @@ def test_nested_with_var_that_skips_a_level(self): cons_expr = self.simplify_cons(cons) assertExpressionsEqual(self, cons_expr, m.y - y_y2 - y_y1 == 0.0) + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_do_not_assume_nested_indicators_local(self): + ct.check_do_not_assume_nested_indicators_local(self, 'gdp.hull') + class TestSpecialCases(unittest.TestCase): def test_local_vars(self): From d97f4ec7d2eeaa441643195aa6a665d1bc53b9da Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 18 Feb 2024 16:43:08 -0700 Subject: [PATCH 0612/3044] weighing in with black --- pyomo/gdp/tests/common_tests.py | 1 + pyomo/gdp/tests/models.py | 12 +++++++++--- pyomo/gdp/tests/test_binary_multiplication.py | 5 ++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index 585aafc967d..28025816262 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -1945,6 +1945,7 @@ def check_nested_disjuncts_in_flat_gdp(self, transformation): self.assertTrue(value(m.disj1[t].indicator_var)) self.assertTrue(value(m.disj1[t].sub1.indicator_var)) + def check_do_not_assume_nested_indicators_local(self, transformation): m = models.why_indicator_vars_are_not_always_local() TransformationFactory(transformation).apply_to(m) diff --git a/pyomo/gdp/tests/models.py b/pyomo/gdp/tests/models.py index fc5e6327c7e..0b84641899c 100644 --- a/pyomo/gdp/tests/models.py +++ b/pyomo/gdp/tests/models.py @@ -566,27 +566,33 @@ def makeNestedDisjunctions_NestedDisjuncts(): def why_indicator_vars_are_not_always_local(): m = ConcreteModel() m.x = Var(bounds=(1, 10)) + @m.Disjunct() def Z1(d): m = d.model() d.c = Constraint(expr=m.x >= 1.1) + @m.Disjunct() def Z2(d): m = d.model() d.c = Constraint(expr=m.x >= 1.2) + @m.Disjunct() def Y1(d): m = d.model() d.c = Constraint(expr=(1.15, m.x, 8)) d.disjunction = Disjunction(expr=[m.Z1, m.Z2]) + @m.Disjunct() def Y2(d): m = d.model() - d.c = Constraint(expr=m.x==9) + d.c = Constraint(expr=m.x == 9) + m.disjunction = Disjunction(expr=[m.Y1, m.Y2]) - m.logical_cons = LogicalConstraint(expr=m.Y2.indicator_var.implies( - m.Z1.indicator_var.land(m.Z2.indicator_var))) + m.logical_cons = LogicalConstraint( + expr=m.Y2.indicator_var.implies(m.Z1.indicator_var.land(m.Z2.indicator_var)) + ) # optimal value is 9, but it will be 8 if we wrongly assume that the nested # indicator_vars are local. diff --git a/pyomo/gdp/tests/test_binary_multiplication.py b/pyomo/gdp/tests/test_binary_multiplication.py index fbe6f86fd46..aa846c4710a 100644 --- a/pyomo/gdp/tests/test_binary_multiplication.py +++ b/pyomo/gdp/tests/test_binary_multiplication.py @@ -306,7 +306,10 @@ def test_local_var(self): class TestNestedGDP(unittest.TestCase): @unittest.skipUnless(gurobi_available, "Gurobi is not available") def test_do_not_assume_nested_indicators_local(self): - ct.check_do_not_assume_nested_indicators_local(self, 'gdp.binary_multiplication') + ct.check_do_not_assume_nested_indicators_local( + self, 'gdp.binary_multiplication' + ) + if __name__ == '__main__': unittest.main() From 4236f63d2afa62bed822b2f49171e13c7bc04650 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 18 Feb 2024 19:17:01 -0500 Subject: [PATCH 0613/3044] Make `PathList` more consistent with `Path` --- pyomo/common/config.py | 27 ++++++++++++++++++--------- pyomo/common/tests/test_config.py | 9 +++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 8f5ac513ee4..1da2e603c7b 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -565,12 +565,16 @@ def domain_name(self): class PathList(Path): - """Domain validator for a list of path-like objects. + """ + Domain validator for a list of + :py:term:`path-like objects `. - This will admit any iterable or object convertible to a string. - Iterable objects (other than strings) will have each member - normalized using :py:class:`Path`. Other types will be passed to - :py:class:`Path`, returning a list with the single resulting path. + This admits a path-like object or iterable of such. + If a path-like object is passed, then + a singleton list containing the object normalized through + :py:class:`Path` is returned. + An iterable of path-like objects is cast to a list, each + entry of which is normalized through :py:class:`Path`. Parameters ---------- @@ -587,10 +591,15 @@ class PathList(Path): """ def __call__(self, data): - if hasattr(data, "__iter__") and not isinstance(data, str): - return [super(PathList, self).__call__(i) for i in data] - else: - return [super(PathList, self).__call__(data)] + try: + pathlist = [super(PathList, self).__call__(data)] + except TypeError as err: + is_not_path_like = ("expected str, bytes or os.PathLike" in str(err)) + if is_not_path_like and hasattr(data, "__iter__"): + pathlist = [super(PathList, self).__call__(i) for i in data] + else: + raise + return pathlist class DynamicImplicitDomain(object): diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 6c657e8d04b..912a9ab1d7c 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -748,6 +748,8 @@ def norm(x): cwd = os.getcwd() + os.path.sep c = ConfigDict() + self.assertEqual(PathList().domain_name(), "PathList") + c.declare('a', ConfigValue(None, PathList())) self.assertEqual(c.a, None) c.a = "/a/b/c" @@ -770,6 +772,13 @@ def norm(x): self.assertEqual(len(c.a), 0) self.assertIs(type(c.a), list) + exc_str = r".*expected str, bytes or os.PathLike.*int" + + with self.assertRaisesRegex(ValueError, exc_str): + c.a = 2 + with self.assertRaisesRegex(ValueError, exc_str): + c.a = ["/a/b/c", 2] + def test_ListOf(self): c = ConfigDict() c.declare('a', ConfigValue(domain=ListOf(int), default=None)) From a07d696447d92fa58085c391809cb6762c94a44f Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 18 Feb 2024 19:44:37 -0500 Subject: [PATCH 0614/3044] Apply black --- pyomo/common/config.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 1da2e603c7b..f156bee79a9 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -363,9 +363,7 @@ def __call__(self, obj): def domain_name(self): class_names = (self._get_class_name(kls) for kls in self.baseClasses) - return ( - f"IsInstance({', '.join(class_names)})" - ) + return f"IsInstance({', '.join(class_names)})" class ListOf(object): @@ -594,7 +592,7 @@ def __call__(self, data): try: pathlist = [super(PathList, self).__call__(data)] except TypeError as err: - is_not_path_like = ("expected str, bytes or os.PathLike" in str(err)) + is_not_path_like = "expected str, bytes or os.PathLike" in str(err) if is_not_path_like and hasattr(data, "__iter__"): pathlist = [super(PathList, self).__call__(i) for i in data] else: From 9c1727bbb7e99a3018a9c8afa56384e1e7d27e80 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 08:07:19 -0700 Subject: [PATCH 0615/3044] Save state: config changes --- pyomo/common/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index a796c34340b..d9b495ff1bd 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -211,8 +211,6 @@ def Datetime(val): This domain will return the original object, assuming it is of the right type. """ - if val is None: - return val if not isinstance(val, datetime.datetime): raise ValueError(f"Expected datetime object, but received {type(val)}.") return val From fb6f97e80e507af5418bbacc00c603f48f898920 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 08:53:37 -0700 Subject: [PATCH 0616/3044] Address @jsiirola's comments --- pyomo/common/config.py | 2 +- pyomo/common/formatting.py | 4 +- pyomo/contrib/solver/base.py | 3 - pyomo/contrib/solver/config.py | 6 +- pyomo/contrib/solver/gurobi.py | 7 +- pyomo/contrib/solver/ipopt.py | 40 +++--- pyomo/contrib/solver/results.py | 1 - pyomo/contrib/solver/solution.py | 130 ++++-------------- .../contrib/solver/tests/unit/test_results.py | 99 +++++++++++-- pyomo/opt/plugins/sol.py | 1 - 10 files changed, 141 insertions(+), 152 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 7ece0e6a48c..3e9b580c7bc 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -40,7 +40,6 @@ deprecation_warning, relocated_module_attribute, ) -from pyomo.common.errors import DeveloperError from pyomo.common.fileutils import import_file from pyomo.common.formatting import wrap_reStructuredText from pyomo.common.modeling import NOTSET @@ -767,6 +766,7 @@ def from_enum_or_string(cls, arg): NegativeFloat NonPositiveFloat NonNegativeFloat + Datetime In InEnum IsInstance diff --git a/pyomo/common/formatting.py b/pyomo/common/formatting.py index 6194f928844..430ec96ca09 100644 --- a/pyomo/common/formatting.py +++ b/pyomo/common/formatting.py @@ -257,8 +257,8 @@ def writelines(self, sequence): r'|(?:\[\s*[A-Za-z0-9\.]+\s*\] +)' # [PASS]|[FAIL]|[ OK ] ) _verbatim_line_start = re.compile( - r'(\| )' - r'|(\+((-{3,})|(={3,}))\+)' # line blocks # grid table + r'(\| )' # line blocks + r'|(\+((-{3,})|(={3,}))\+)' # grid table ) _verbatim_line = re.compile( r'(={3,}[ =]+)' # simple tables, ======== sections diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index a60e770e660..cb13809c438 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -180,9 +180,6 @@ class PersistentSolverBase(SolverBase): CONFIG = PersistentSolverConfig() - def __init__(self, **kwds): - super().__init__(**kwds) - @document_kwargs_from_configdict(CONFIG) @abc.abstractmethod def solve(self, model: _BlockData, **kwargs) -> Results: diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index d13e1caf81d..21f6e233d78 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -94,7 +94,11 @@ def __init__( ), ) self.timer: HierarchicalTimer = self.declare( - 'timer', ConfigValue(default=None, description="A HierarchicalTimer.") + 'timer', + ConfigValue( + default=None, + description="A timer object for recording relevant process timing data.", + ), ) self.threads: Optional[int] = self.declare( 'threads', diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index c1b02c08ef9..2b4986edaf8 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -283,11 +283,8 @@ def _check_license(self): if avail: if self._available is None: - res = Gurobi._check_full_license() - self._available = res - return res - else: - return self._available + self._available = Gurobi._check_full_license() + return self._available else: return self.Availability.BadLicense diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index edea4e693b4..0d0d89f837a 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -34,7 +34,7 @@ from pyomo.contrib.solver.factory import SolverFactory from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus from pyomo.contrib.solver.sol_reader import parse_sol_file -from pyomo.contrib.solver.solution import SolSolutionLoader, SolutionLoader +from pyomo.contrib.solver.solution import SolSolutionLoader from pyomo.common.tee import TeeStream from pyomo.common.log import LogStream from pyomo.core.expr.visitor import replace_expressions @@ -103,14 +103,14 @@ def __init__( implicit_domain=implicit_domain, visibility=visibility, ) - self.timing_info.no_function_solve_time: Optional[float] = ( + self.timing_info.ipopt_excluding_nlp_functions: Optional[float] = ( self.timing_info.declare( - 'no_function_solve_time', ConfigValue(domain=NonNegativeFloat) + 'ipopt_excluding_nlp_functions', ConfigValue(domain=NonNegativeFloat) ) ) - self.timing_info.function_solve_time: Optional[float] = ( + self.timing_info.nlp_function_evaluations: Optional[float] = ( self.timing_info.declare( - 'function_solve_time', ConfigValue(domain=NonNegativeFloat) + 'nlp_function_evaluations', ConfigValue(domain=NonNegativeFloat) ) ) @@ -225,10 +225,11 @@ def __init__(self, **kwds): self._writer = NLWriter() self._available_cache = None self._version_cache = None + self.executable = self.config.executable def available(self): if self._available_cache is None: - if self.config.executable.path() is None: + if self.executable.path() is None: self._available_cache = self.Availability.NotFound else: self._available_cache = self.Availability.FullLicense @@ -237,7 +238,7 @@ def available(self): def version(self): if self._version_cache is None: results = subprocess.run( - [str(self.config.executable), '--version'], + [str(self.executable), '--version'], timeout=1, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -266,7 +267,7 @@ def _write_options_file(self, filename: str, options: Mapping): return opt_file_exists def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: bool): - cmd = [str(config.executable), basename + '.nl', '-AMPL'] + cmd = [str(self.executable), basename + '.nl', '-AMPL'] if opt_file: cmd.append('option_file_name=' + basename + '.opt') if 'option_file_name' in config.solver_options: @@ -296,6 +297,7 @@ def solve(self, model, **kwds): ) # Update configuration options, based on keywords passed to solve config: ipoptConfig = self.config(value=kwds, preserve_implicit=True) + self.executable = config.executable if config.threads: logger.log( logging.WARNING, @@ -306,7 +308,6 @@ def solve(self, model, **kwds): else: timer = config.timer StaleFlagManager.mark_all_as_stale() - results = ipoptResults() with TempfileManager.new_context() as tempfile: if config.working_dir is None: dname = tempfile.mkdtemp() @@ -379,16 +380,18 @@ def solve(self, model, **kwds): ) if process.returncode != 0: + results = ipoptResults() + results.extra_info.return_code = process.returncode results.termination_condition = TerminationCondition.error - results.solution_loader = SolutionLoader(None, None, None) + results.solution_loader = SolSolutionLoader(None, None) else: with open(basename + '.sol', 'r') as sol_file: timer.start('parse_sol') - results = self._parse_solution(sol_file, nl_info, results) + results = self._parse_solution(sol_file, nl_info) timer.stop('parse_sol') results.iteration_count = iters - results.timing_info.no_function_solve_time = ipopt_time_nofunc - results.timing_info.function_solve_time = ipopt_time_func + results.timing_info.ipopt_excluding_nlp_functions = ipopt_time_nofunc + results.timing_info.nlp_function_evaluations = ipopt_time_func if ( config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal @@ -397,7 +400,7 @@ def solve(self, model, **kwds): 'Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' ) - results.solver_name = 'ipopt' + results.solver_name = self.name results.solver_version = self.version() if ( config.load_solutions @@ -484,15 +487,14 @@ def _parse_ipopt_output(self, stream: io.StringIO): return iters, nofunc_time, func_time - def _parse_solution( - self, instream: io.TextIOBase, nl_info: NLWriterInfo, result: ipoptResults - ): + def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): + results = ipoptResults() res, sol_data = parse_sol_file( - sol_file=instream, nl_info=nl_info, result=result + sol_file=instream, nl_info=nl_info, result=results ) if res.solution_status == SolutionStatus.noSolution: - res.solution_loader = SolutionLoader(None, None, None) + res.solution_loader = SolSolutionLoader(None, None) else: res.solution_loader = ipoptSolutionLoader( sol_data=sol_data, nl_info=nl_info diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index b330773e4f3..f2c9cde64fe 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -27,7 +27,6 @@ TerminationCondition as LegacyTerminationCondition, SolverStatus as LegacySolverStatus, ) -from pyomo.common.timing import HierarchicalTimer class TerminationCondition(enum.Enum): diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 31792a76dfe..1812e21a596 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ import abc -from typing import Sequence, Dict, Optional, Mapping, MutableMapping, NoReturn +from typing import Sequence, Dict, Optional, Mapping, NoReturn from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData @@ -18,7 +18,6 @@ from pyomo.core.staleflag import StaleFlagManager from pyomo.contrib.solver.sol_reader import SolFileData from pyomo.repn.plugins.nl_writer import NLWriterInfo -from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions @@ -106,78 +105,33 @@ def get_reduced_costs( ) -# TODO: This is for development uses only; not to be released to the wild -# May turn into documentation someday -class SolutionLoader(SolutionLoaderBase): - def __init__( - self, - primals: Optional[MutableMapping], - duals: Optional[MutableMapping], - reduced_costs: Optional[MutableMapping], - ): - """ - Parameters - ---------- - primals: dict - maps id(Var) to (var, value) - duals: dict - maps Constraint to dual value - reduced_costs: dict - maps id(Var) to (var, reduced_cost) - """ - self._primals = primals - self._duals = duals - self._reduced_costs = reduced_costs +class PersistentSolutionLoader(SolutionLoaderBase): + def __init__(self, solver): + self._solver = solver + self._valid = True - def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - if self._primals is None: - raise RuntimeError( - 'Solution loader does not currently have a valid solution. Please ' - 'check the termination condition.' - ) - if vars_to_load is None: - return ComponentMap(self._primals.values()) - else: - primals = ComponentMap() - for v in vars_to_load: - primals[v] = self._primals[id(v)][1] - return primals + def _assert_solution_still_valid(self): + if not self._valid: + raise RuntimeError('The results in the solver are no longer valid.') + + def get_primals(self, vars_to_load=None): + self._assert_solution_still_valid() + return self._solver._get_primals(vars_to_load=vars_to_load) def get_duals( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None ) -> Dict[_GeneralConstraintData, float]: - if self._duals is None: - raise RuntimeError( - 'Solution loader does not currently have valid duals. Please ' - 'check the termination condition and ensure the solver returns duals ' - 'for the given problem type.' - ) - if cons_to_load is None: - duals = dict(self._duals) - else: - duals = {} - for c in cons_to_load: - duals[c] = self._duals[c] - return duals + self._assert_solution_still_valid() + return self._solver._get_duals(cons_to_load=cons_to_load) def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: - if self._reduced_costs is None: - raise RuntimeError( - 'Solution loader does not currently have valid reduced costs. Please ' - 'check the termination condition and ensure the solver returns reduced ' - 'costs for the given problem type.' - ) - if vars_to_load is None: - rc = ComponentMap(self._reduced_costs.values()) - else: - rc = ComponentMap() - for v in vars_to_load: - rc[v] = self._reduced_costs[id(v)][1] - return rc + self._assert_solution_still_valid() + return self._solver._get_reduced_costs(vars_to_load=vars_to_load) + + def invalidate(self): + self._valid = False class SolSolutionLoader(SolutionLoaderBase): @@ -188,17 +142,14 @@ def __init__(self, sol_data: SolFileData, nl_info: NLWriterInfo) -> None: def load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> NoReturn: - if self._nl_info.scaling is None: - scale_list = [1] * len(self._nl_info.variables) + if self._nl_info.scaling: + for v, val, scale in zip( + self._nl_info.variables, self._sol_data.primals, self._nl_info.scaling + ): + v.set_value(val / scale, skip_validation=True) else: - scale_list = self._nl_info.scaling.variables - for v, val, scale in zip( - self._nl_info.variables, self._sol_data.primals, scale_list - ): - v.set_value(val / scale, skip_validation=True) - - for v, v_expr in self._nl_info.eliminated_vars: - v.set_value(value(v_expr), skip_validation=True) + for v, val in zip(self._nl_info.variables, self._sol_data.primals): + v.set_value(val, skip_validation=True) StaleFlagManager.mark_all_as_stale(delayed=True) @@ -248,32 +199,3 @@ def get_duals( if c in cons_to_load: res[c] = val * scale return res - - -class PersistentSolutionLoader(SolutionLoaderBase): - def __init__(self, solver): - self._solver = solver - self._valid = True - - def _assert_solution_still_valid(self): - if not self._valid: - raise RuntimeError('The results in the solver are no longer valid.') - - def get_primals(self, vars_to_load=None): - self._assert_solution_still_valid() - return self._solver._get_primals(vars_to_load=vars_to_load) - - def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: - self._assert_solution_still_valid() - return self._solver._get_duals(cons_to_load=cons_to_load) - - def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: - self._assert_solution_still_valid() - return self._solver._get_reduced_costs(vars_to_load=vars_to_load) - - def invalidate(self): - self._valid = False diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 2d8f6460448..4856b737295 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -10,15 +10,97 @@ # ___________________________________________________________________________ from io import StringIO +from typing import Sequence, Dict, Optional, Mapping, MutableMapping + from pyomo.common import unittest from pyomo.common.config import ConfigDict +from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.var import _GeneralVarData +from pyomo.common.collections import ComponentMap from pyomo.contrib.solver import results from pyomo.contrib.solver import solution import pyomo.environ as pyo from pyomo.core.base.var import ScalarVar +class SolutionLoaderExample(solution.SolutionLoaderBase): + """ + This is an example instantiation of a SolutionLoader that is used for + testing generated results. + """ + + def __init__( + self, + primals: Optional[MutableMapping], + duals: Optional[MutableMapping], + reduced_costs: Optional[MutableMapping], + ): + """ + Parameters + ---------- + primals: dict + maps id(Var) to (var, value) + duals: dict + maps Constraint to dual value + reduced_costs: dict + maps id(Var) to (var, reduced_cost) + """ + self._primals = primals + self._duals = duals + self._reduced_costs = reduced_costs + + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + if self._primals is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + if vars_to_load is None: + return ComponentMap(self._primals.values()) + else: + primals = ComponentMap() + for v in vars_to_load: + primals[v] = self._primals[id(v)][1] + return primals + + def get_duals( + self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None + ) -> Dict[_GeneralConstraintData, float]: + if self._duals is None: + raise RuntimeError( + 'Solution loader does not currently have valid duals. Please ' + 'check the termination condition and ensure the solver returns duals ' + 'for the given problem type.' + ) + if cons_to_load is None: + duals = dict(self._duals) + else: + duals = {} + for c in cons_to_load: + duals[c] = self._duals[c] + return duals + + def get_reduced_costs( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + if self._reduced_costs is None: + raise RuntimeError( + 'Solution loader does not currently have valid reduced costs. Please ' + 'check the termination condition and ensure the solver returns reduced ' + 'costs for the given problem type.' + ) + if vars_to_load is None: + rc = ComponentMap(self._reduced_costs.values()) + else: + rc = ComponentMap() + for v in vars_to_load: + rc[v] = self._reduced_costs[id(v)][1] + return rc + + class TestTerminationCondition(unittest.TestCase): def test_member_list(self): member_list = results.TerminationCondition._member_names_ @@ -92,6 +174,7 @@ def test_member_list(self): def test_default_initialization(self): res = results.Results() + self.assertIsNone(res.solution_loader) self.assertIsNone(res.incumbent_objective) self.assertIsNone(res.objective_bound) self.assertEqual( @@ -105,20 +188,6 @@ def test_default_initialization(self): self.assertIsInstance(res.extra_info, ConfigDict) self.assertIsNone(res.timing_info.start_timestamp) self.assertIsNone(res.timing_info.wall_time) - res.solution_loader = solution.SolutionLoader(None, None, None) - - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have a valid solution.*' - ): - res.solution_loader.load_vars() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid duals.*' - ): - res.solution_loader.get_duals() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid reduced costs.*' - ): - res.solution_loader.get_reduced_costs() def test_display(self): res = results.Results() @@ -160,7 +229,7 @@ def test_generated_results(self): rc[id(m.y)] = (m.y, 6) res = results.Results() - res.solution_loader = solution.SolutionLoader( + res.solution_loader = SolutionLoaderExample( primals=primals, duals=duals, reduced_costs=rc ) diff --git a/pyomo/opt/plugins/sol.py b/pyomo/opt/plugins/sol.py index a6088cb25af..10da469f186 100644 --- a/pyomo/opt/plugins/sol.py +++ b/pyomo/opt/plugins/sol.py @@ -189,7 +189,6 @@ def _load(self, fin, res, soln, suffixes): if line == "": continue line = line.split() - # Some sort of garbage we tag onto the solver message, assuming we are past the suffixes if line[0] != 'suffix': # We assume this is the start of a # section like kestrel_option, which From 544aa459ed9cebc890dd92bfa1601ada19aadb4d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 08:54:22 -0700 Subject: [PATCH 0617/3044] Missed one file --- pyomo/contrib/solver/util.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index d104022692e..ca499748adf 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -153,18 +153,6 @@ def collect_vars_and_named_exprs(expr): ) -class SolverUtils: - pass - - -class SubprocessSolverUtils: - pass - - -class DirectSolverUtils: - pass - - class PersistentSolverUtils(abc.ABC): def __init__(self): self._model = None From 9bf1970ffc2d19c7978a7a5312deb6a68c6f03cc Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 09:09:53 -0700 Subject: [PATCH 0618/3044] Remove because it's not implemented and we have a different idea --- pyomo/contrib/solver/config.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 21f6e233d78..9b8de245bd6 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -50,14 +50,6 @@ def __init__( description="If True, the solver log prints to stdout.", ), ) - self.log_solver_output: bool = self.declare( - 'log_solver_output', - ConfigValue( - domain=bool, - default=False, - description="If True, the solver output gets logged.", - ), - ) self.working_dir: str = self.declare( 'working_dir', ConfigValue( From 95cd64ecbbbb51363c92a6e2cdac5a0835c2ce90 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 09:14:46 -0700 Subject: [PATCH 0619/3044] Remove from __init__ --- pyomo/contrib/solver/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyomo/contrib/solver/__init__.py b/pyomo/contrib/solver/__init__.py index 2dc73091ea2..a4a626013c4 100644 --- a/pyomo/contrib/solver/__init__.py +++ b/pyomo/contrib/solver/__init__.py @@ -8,9 +8,3 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -from . import base -from . import config -from . import results -from . import solution -from . import util From c17d2b9da8639641e5ba910014c4a8a653fcd505 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 09:37:05 -0700 Subject: [PATCH 0620/3044] Reverting: log_solver_output does do something --- pyomo/contrib/solver/config.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 9b8de245bd6..21f6e233d78 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -50,6 +50,14 @@ def __init__( description="If True, the solver log prints to stdout.", ), ) + self.log_solver_output: bool = self.declare( + 'log_solver_output', + ConfigValue( + domain=bool, + default=False, + description="If True, the solver output gets logged.", + ), + ) self.working_dir: str = self.declare( 'working_dir', ConfigValue( From 10e08f922463f915c5566d973dba8d9440335fe9 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 10:00:45 -0700 Subject: [PATCH 0621/3044] Update domain validator for bools --- pyomo/contrib/solver/config.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 21f6e233d78..1e9f0b1fbe9 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -17,6 +17,7 @@ NonNegativeFloat, NonNegativeInt, ADVANCED_OPTION, + Bool, ) from pyomo.common.timing import HierarchicalTimer @@ -45,7 +46,7 @@ def __init__( self.tee: bool = self.declare( 'tee', ConfigValue( - domain=bool, + domain=Bool, default=False, description="If True, the solver log prints to stdout.", ), @@ -53,7 +54,7 @@ def __init__( self.log_solver_output: bool = self.declare( 'log_solver_output', ConfigValue( - domain=bool, + domain=Bool, default=False, description="If True, the solver output gets logged.", ), @@ -70,7 +71,7 @@ def __init__( self.load_solutions: bool = self.declare( 'load_solutions', ConfigValue( - domain=bool, + domain=Bool, default=True, description="If True, the values of the primal variables will be loaded into the model.", ), @@ -78,7 +79,7 @@ def __init__( self.raise_exception_on_nonoptimal_result: bool = self.declare( 'raise_exception_on_nonoptimal_result', ConfigValue( - domain=bool, + domain=Bool, default=True, description="If False, the `solve` method will continue processing " "even if the returned result is nonoptimal.", @@ -87,7 +88,7 @@ def __init__( self.symbolic_solver_labels: bool = self.declare( 'symbolic_solver_labels', ConfigValue( - domain=bool, + domain=Bool, default=False, description="If True, the names given to the solver will reflect the names of the Pyomo components. " "Cannot be changed after set_instance is called.", From ca0280c5b50aa09fb0083c753f3b2972ab5c9c50 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 19 Feb 2024 10:03:50 -0700 Subject: [PATCH 0622/3044] update type hints in configs --- pyomo/contrib/solver/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 1e9f0b1fbe9..ca9557d0002 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -59,7 +59,7 @@ def __init__( description="If True, the solver output gets logged.", ), ) - self.working_dir: str = self.declare( + self.working_dir: Optional[str] = self.declare( 'working_dir', ConfigValue( domain=str, @@ -94,7 +94,7 @@ def __init__( "Cannot be changed after set_instance is called.", ), ) - self.timer: HierarchicalTimer = self.declare( + self.timer: Optional[HierarchicalTimer] = self.declare( 'timer', ConfigValue( default=None, From df339688e125abed7996c68df71cc9fcc19117f6 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 19 Feb 2024 10:30:19 -0700 Subject: [PATCH 0623/3044] properly copy the nl writer config --- pyomo/contrib/solver/ipopt.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 0d0d89f837a..6fb369d4785 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -80,10 +80,7 @@ def __init__( ) self.writer_config: ConfigDict = self.declare( 'writer_config', - ConfigValue( - default=NLWriter.CONFIG(), - description="For the manipulation of NL writer options.", - ), + NLWriter.CONFIG(), ) From 086d53cd197168665cf6f63fcfc57bcd3b97ea14 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 11:07:55 -0700 Subject: [PATCH 0624/3044] Apply black; change to ccapital I --- pyomo/contrib/solver/ipopt.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 6fb369d4785..1aa00c0c8e2 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -53,7 +53,7 @@ class ipoptSolverError(PyomoException): """ -class ipoptConfig(SolverConfig): +class IpoptConfig(SolverConfig): def __init__( self, description=None, @@ -79,12 +79,11 @@ def __init__( ), ) self.writer_config: ConfigDict = self.declare( - 'writer_config', - NLWriter.CONFIG(), + 'writer_config', NLWriter.CONFIG() ) -class ipoptResults(Results): +class IpoptResults(Results): def __init__( self, description=None, @@ -112,7 +111,7 @@ def __init__( ) -class ipoptSolutionLoader(SolSolutionLoader): +class IpoptSolutionLoader(SolSolutionLoader): def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: @@ -214,8 +213,8 @@ def get_reduced_costs( @SolverFactory.register('ipopt_v2', doc='The ipopt NLP solver (new interface)') -class ipopt(SolverBase): - CONFIG = ipoptConfig() +class Ipopt(SolverBase): + CONFIG = IpoptConfig() def __init__(self, **kwds): super().__init__(**kwds) @@ -263,7 +262,7 @@ def _write_options_file(self, filename: str, options: Mapping): opt_file.write(str(k) + ' ' + str(val) + '\n') return opt_file_exists - def _create_command_line(self, basename: str, config: ipoptConfig, opt_file: bool): + def _create_command_line(self, basename: str, config: IpoptConfig, opt_file: bool): cmd = [str(self.executable), basename + '.nl', '-AMPL'] if opt_file: cmd.append('option_file_name=' + basename + '.opt') @@ -293,7 +292,7 @@ def solve(self, model, **kwds): f'Solver {self.__class__} is not available ({avail}).' ) # Update configuration options, based on keywords passed to solve - config: ipoptConfig = self.config(value=kwds, preserve_implicit=True) + config: IpoptConfig = self.config(value=kwds, preserve_implicit=True) self.executable = config.executable if config.threads: logger.log( @@ -377,7 +376,7 @@ def solve(self, model, **kwds): ) if process.returncode != 0: - results = ipoptResults() + results = IpoptResults() results.extra_info.return_code = process.returncode results.termination_condition = TerminationCondition.error results.solution_loader = SolSolutionLoader(None, None) @@ -485,7 +484,7 @@ def _parse_ipopt_output(self, stream: io.StringIO): return iters, nofunc_time, func_time def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): - results = ipoptResults() + results = IpoptResults() res, sol_data = parse_sol_file( sol_file=instream, nl_info=nl_info, result=results ) @@ -493,7 +492,7 @@ def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): if res.solution_status == SolutionStatus.noSolution: res.solution_loader = SolSolutionLoader(None, None) else: - res.solution_loader = ipoptSolutionLoader( + res.solution_loader = IpoptSolutionLoader( sol_data=sol_data, nl_info=nl_info ) From 69fd8d03a5b2349c060a0b6d7a861d045681b4ec Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 11:23:16 -0700 Subject: [PATCH 0625/3044] Move PersistentUtils into own file --- pyomo/contrib/solver/persistent.py | 523 +++++++++++++++++++++++++++++ pyomo/contrib/solver/util.py | 512 +--------------------------- 2 files changed, 524 insertions(+), 511 deletions(-) create mode 100644 pyomo/contrib/solver/persistent.py diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py new file mode 100644 index 00000000000..0994aa53093 --- /dev/null +++ b/pyomo/contrib/solver/persistent.py @@ -0,0 +1,523 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# __________________________________________________________________________ + +import abc +from typing import List + +from pyomo.core.base.constraint import _GeneralConstraintData, Constraint +from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.param import _ParamData, Param +from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.common.collections import ComponentMap +from pyomo.common.timing import HierarchicalTimer +from pyomo.core.expr.numvalue import NumericConstant +from pyomo.contrib.solver.util import collect_vars_and_named_exprs, get_objective + + +class PersistentSolverUtils(abc.ABC): + def __init__(self): + self._model = None + self._active_constraints = {} # maps constraint to (lower, body, upper) + self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) + self._params = {} # maps param id to param + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._named_expressions = ( + {} + ) # maps constraint to list of tuples (named_expr, named_expr.expr) + self._external_functions = ComponentMap() + self._obj_named_expressions = [] + self._referenced_variables = ( + {} + ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] + self._vars_referenced_by_con = {} + self._vars_referenced_by_obj = [] + self._expr_types = None + + def set_instance(self, model): + saved_config = self.config + self.__init__() + self.config = saved_config + self._model = model + self.add_block(model) + if self._objective is None: + self.set_objective(None) + + @abc.abstractmethod + def _add_variables(self, variables: List[_GeneralVarData]): + pass + + def add_variables(self, variables: List[_GeneralVarData]): + for v in variables: + if id(v) in self._referenced_variables: + raise ValueError( + 'variable {name} has already been added'.format(name=v.name) + ) + self._referenced_variables[id(v)] = [{}, {}, None] + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._add_variables(variables) + + @abc.abstractmethod + def _add_params(self, params: List[_ParamData]): + pass + + def add_params(self, params: List[_ParamData]): + for p in params: + self._params[id(p)] = p + self._add_params(params) + + @abc.abstractmethod + def _add_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def _check_for_new_vars(self, variables: List[_GeneralVarData]): + new_vars = {} + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + new_vars[v_id] = v + self.add_variables(list(new_vars.values())) + + def _check_to_remove_vars(self, variables: List[_GeneralVarData]): + vars_to_remove = {} + for v in variables: + v_id = id(v) + ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] + if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: + vars_to_remove[v_id] = v + self.remove_variables(list(vars_to_remove.values())) + + def add_constraints(self, cons: List[_GeneralConstraintData]): + all_fixed_vars = {} + for con in cons: + if con in self._named_expressions: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = (con.lower, con.body, con.upper) + tmp = collect_vars_and_named_exprs(con.body) + named_exprs, variables, fixed_vars, external_functions = tmp + self._check_for_new_vars(variables) + self._named_expressions[con] = [(e, e.expr) for e in named_exprs] + if len(external_functions) > 0: + self._external_functions[con] = external_functions + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][0][con] = None + if not self.config.auto_updates.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + all_fixed_vars[id(v)] = v + self._add_constraints(cons) + for v in all_fixed_vars.values(): + v.fix() + + @abc.abstractmethod + def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def add_sos_constraints(self, cons: List[_SOSConstraintData]): + for con in cons: + if con in self._vars_referenced_by_con: + raise ValueError( + 'constraint {name} has already been added'.format(name=con.name) + ) + self._active_constraints[con] = tuple() + variables = con.get_variables() + self._check_for_new_vars(variables) + self._named_expressions[con] = [] + self._vars_referenced_by_con[con] = variables + for v in variables: + self._referenced_variables[id(v)][1][con] = None + self._add_sos_constraints(cons) + + @abc.abstractmethod + def _set_objective(self, obj: _GeneralObjectiveData): + pass + + def set_objective(self, obj: _GeneralObjectiveData): + if self._objective is not None: + for v in self._vars_referenced_by_obj: + self._referenced_variables[id(v)][2] = None + self._check_to_remove_vars(self._vars_referenced_by_obj) + self._external_functions.pop(self._objective, None) + if obj is not None: + self._objective = obj + self._objective_expr = obj.expr + self._objective_sense = obj.sense + tmp = collect_vars_and_named_exprs(obj.expr) + named_exprs, variables, fixed_vars, external_functions = tmp + self._check_for_new_vars(variables) + self._obj_named_expressions = [(i, i.expr) for i in named_exprs] + if len(external_functions) > 0: + self._external_functions[obj] = external_functions + self._vars_referenced_by_obj = variables + for v in variables: + self._referenced_variables[id(v)][2] = obj + if not self.config.auto_updates.treat_fixed_vars_as_params: + for v in fixed_vars: + v.unfix() + self._set_objective(obj) + for v in fixed_vars: + v.fix() + else: + self._vars_referenced_by_obj = [] + self._objective = None + self._objective_expr = None + self._objective_sense = None + self._obj_named_expressions = [] + self._set_objective(obj) + + def add_block(self, block): + param_dict = {} + for p in block.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + param_dict[id(_p)] = _p + self.add_params(list(param_dict.values())) + self.add_constraints( + list( + block.component_data_objects(Constraint, descend_into=True, active=True) + ) + ) + self.add_sos_constraints( + list( + block.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + ) + ) + obj = get_objective(block) + if obj is not None: + self.set_objective(obj) + + @abc.abstractmethod + def _remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def remove_constraints(self, cons: List[_GeneralConstraintData]): + self._remove_constraints(cons) + for con in cons: + if con not in self._named_expressions: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][0].pop(con) + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + self._external_functions.pop(con, None) + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def remove_sos_constraints(self, cons: List[_SOSConstraintData]): + self._remove_sos_constraints(cons) + for con in cons: + if con not in self._vars_referenced_by_con: + raise ValueError( + 'cannot remove constraint {name} - it was not added'.format( + name=con.name + ) + ) + for v in self._vars_referenced_by_con[con]: + self._referenced_variables[id(v)][1].pop(con) + self._check_to_remove_vars(self._vars_referenced_by_con[con]) + del self._active_constraints[con] + del self._named_expressions[con] + del self._vars_referenced_by_con[con] + + @abc.abstractmethod + def _remove_variables(self, variables: List[_GeneralVarData]): + pass + + def remove_variables(self, variables: List[_GeneralVarData]): + self._remove_variables(variables) + for v in variables: + v_id = id(v) + if v_id not in self._referenced_variables: + raise ValueError( + 'cannot remove variable {name} - it has not been added'.format( + name=v.name + ) + ) + cons_using, sos_using, obj_using = self._referenced_variables[v_id] + if cons_using or sos_using or (obj_using is not None): + raise ValueError( + 'cannot remove variable {name} - it is still being used by constraints or the objective'.format( + name=v.name + ) + ) + del self._referenced_variables[v_id] + del self._vars[v_id] + + @abc.abstractmethod + def _remove_params(self, params: List[_ParamData]): + pass + + def remove_params(self, params: List[_ParamData]): + self._remove_params(params) + for p in params: + del self._params[id(p)] + + def remove_block(self, block): + self.remove_constraints( + list( + block.component_data_objects( + ctype=Constraint, descend_into=True, active=True + ) + ) + ) + self.remove_sos_constraints( + list( + block.component_data_objects( + ctype=SOSConstraint, descend_into=True, active=True + ) + ) + ) + self.remove_params( + list( + dict( + (id(p), p) + for p in block.component_data_objects( + ctype=Param, descend_into=True + ) + ).values() + ) + ) + + @abc.abstractmethod + def _update_variables(self, variables: List[_GeneralVarData]): + pass + + def update_variables(self, variables: List[_GeneralVarData]): + for v in variables: + self._vars[id(v)] = ( + v, + v._lb, + v._ub, + v.fixed, + v.domain.get_interval(), + v.value, + ) + self._update_variables(variables) + + @abc.abstractmethod + def update_params(self): + pass + + def update(self, timer: HierarchicalTimer = None): + if timer is None: + timer = HierarchicalTimer() + config = self.config.auto_updates + new_vars = [] + old_vars = [] + new_params = [] + old_params = [] + new_cons = [] + old_cons = [] + old_sos = [] + new_sos = [] + current_vars_dict = {} + current_cons_dict = {} + current_sos_dict = {} + timer.start('vars') + if config.update_vars: + start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + timer.stop('vars') + timer.start('params') + if config.check_for_new_or_removed_params: + current_params_dict = {} + for p in self._model.component_objects(Param, descend_into=True): + if p.mutable: + for _p in p.values(): + current_params_dict[id(_p)] = _p + for p_id, p in current_params_dict.items(): + if p_id not in self._params: + new_params.append(p) + for p_id, p in self._params.items(): + if p_id not in current_params_dict: + old_params.append(p) + timer.stop('params') + timer.start('cons') + if config.check_for_new_or_removed_constraints or config.update_constraints: + current_cons_dict = { + c: None + for c in self._model.component_data_objects( + Constraint, descend_into=True, active=True + ) + } + current_sos_dict = { + c: None + for c in self._model.component_data_objects( + SOSConstraint, descend_into=True, active=True + ) + } + for c in current_cons_dict.keys(): + if c not in self._vars_referenced_by_con: + new_cons.append(c) + for c in current_sos_dict.keys(): + if c not in self._vars_referenced_by_con: + new_sos.append(c) + for c in self._vars_referenced_by_con.keys(): + if c not in current_cons_dict and c not in current_sos_dict: + if (c.ctype is Constraint) or ( + c.ctype is None and isinstance(c, _GeneralConstraintData) + ): + old_cons.append(c) + else: + assert (c.ctype is SOSConstraint) or ( + c.ctype is None and isinstance(c, _SOSConstraintData) + ) + old_sos.append(c) + self.remove_constraints(old_cons) + self.remove_sos_constraints(old_sos) + timer.stop('cons') + timer.start('params') + self.remove_params(old_params) + + # sticking this between removal and addition + # is important so that we don't do unnecessary work + if config.update_params: + self.update_params() + + self.add_params(new_params) + timer.stop('params') + timer.start('vars') + self.add_variables(new_vars) + timer.stop('vars') + timer.start('cons') + self.add_constraints(new_cons) + self.add_sos_constraints(new_sos) + new_cons_set = set(new_cons) + new_sos_set = set(new_sos) + new_vars_set = set(id(v) for v in new_vars) + cons_to_remove_and_add = {} + need_to_set_objective = False + if config.update_constraints: + cons_to_update = [] + sos_to_update = [] + for c in current_cons_dict.keys(): + if c not in new_cons_set: + cons_to_update.append(c) + for c in current_sos_dict.keys(): + if c not in new_sos_set: + sos_to_update.append(c) + for c in cons_to_update: + lower, body, upper = self._active_constraints[c] + new_lower, new_body, new_upper = c.lower, c.body, c.upper + if new_body is not body: + cons_to_remove_and_add[c] = None + continue + if new_lower is not lower: + if ( + type(new_lower) is NumericConstant + and type(lower) is NumericConstant + and new_lower.value == lower.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + if new_upper is not upper: + if ( + type(new_upper) is NumericConstant + and type(upper) is NumericConstant + and new_upper.value == upper.value + ): + pass + else: + cons_to_remove_and_add[c] = None + continue + self.remove_sos_constraints(sos_to_update) + self.add_sos_constraints(sos_to_update) + timer.stop('cons') + timer.start('vars') + if config.update_vars: + end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} + vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] + if config.update_vars: + vars_to_update = [] + for v in vars_to_check: + _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] + if (fixed != v.fixed) or (fixed and (value != v.value)): + vars_to_update.append(v) + if self.config.auto_updates.treat_fixed_vars_as_params: + for c in self._referenced_variables[id(v)][0]: + cons_to_remove_and_add[c] = None + if self._referenced_variables[id(v)][2] is not None: + need_to_set_objective = True + elif lb is not v._lb: + vars_to_update.append(v) + elif ub is not v._ub: + vars_to_update.append(v) + elif domain_interval != v.domain.get_interval(): + vars_to_update.append(v) + self.update_variables(vars_to_update) + timer.stop('vars') + timer.start('cons') + cons_to_remove_and_add = list(cons_to_remove_and_add.keys()) + self.remove_constraints(cons_to_remove_and_add) + self.add_constraints(cons_to_remove_and_add) + timer.stop('cons') + timer.start('named expressions') + if config.update_named_expressions: + cons_to_update = [] + for c, expr_list in self._named_expressions.items(): + if c in new_cons_set: + continue + for named_expr, old_expr in expr_list: + if named_expr.expr is not old_expr: + cons_to_update.append(c) + break + self.remove_constraints(cons_to_update) + self.add_constraints(cons_to_update) + for named_expr, old_expr in self._obj_named_expressions: + if named_expr.expr is not old_expr: + need_to_set_objective = True + break + timer.stop('named expressions') + timer.start('objective') + if self.config.auto_updates.check_for_new_objective: + pyomo_obj = get_objective(self._model) + if pyomo_obj is not self._objective: + need_to_set_objective = True + else: + pyomo_obj = self._objective + if self.config.auto_updates.update_objective: + if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: + need_to_set_objective = True + elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: + # we can definitely do something faster here than resetting the whole objective + need_to_set_objective = True + if need_to_set_objective: + self.set_objective(pyomo_obj) + timer.stop('objective') + + # this has to be done after the objective and constraints in case the + # old objective/constraints use old variables + timer.start('vars') + self.remove_variables(old_vars) + timer.stop('vars') diff --git a/pyomo/contrib/solver/util.py b/pyomo/contrib/solver/util.py index ca499748adf..c6bbfbd22ad 100644 --- a/pyomo/contrib/solver/util.py +++ b/pyomo/contrib/solver/util.py @@ -9,19 +9,9 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import abc -from typing import List - from pyomo.core.expr.visitor import ExpressionValueVisitor, nonpyomo_leaf_types import pyomo.core.expr as EXPR -from pyomo.core.base.constraint import _GeneralConstraintData, Constraint -from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint -from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.param import _ParamData, Param -from pyomo.core.base.objective import Objective, _GeneralObjectiveData -from pyomo.common.collections import ComponentMap -from pyomo.common.timing import HierarchicalTimer -from pyomo.core.expr.numvalue import NumericConstant +from pyomo.core.base.objective import Objective from pyomo.opt.results.solver import ( SolverStatus, TerminationCondition as LegacyTerminationCondition, @@ -151,503 +141,3 @@ def collect_vars_and_named_exprs(expr): list(_visitor.fixed_vars.values()), list(_visitor._external_functions.values()), ) - - -class PersistentSolverUtils(abc.ABC): - def __init__(self): - self._model = None - self._active_constraints = {} # maps constraint to (lower, body, upper) - self._vars = {} # maps var id to (var, lb, ub, fixed, domain, value) - self._params = {} # maps param id to param - self._objective = None - self._objective_expr = None - self._objective_sense = None - self._named_expressions = ( - {} - ) # maps constraint to list of tuples (named_expr, named_expr.expr) - self._external_functions = ComponentMap() - self._obj_named_expressions = [] - self._referenced_variables = ( - {} - ) # var_id: [dict[constraints, None], dict[sos constraints, None], None or objective] - self._vars_referenced_by_con = {} - self._vars_referenced_by_obj = [] - self._expr_types = None - - def set_instance(self, model): - saved_config = self.config - self.__init__() - self.config = saved_config - self._model = model - self.add_block(model) - if self._objective is None: - self.set_objective(None) - - @abc.abstractmethod - def _add_variables(self, variables: List[_GeneralVarData]): - pass - - def add_variables(self, variables: List[_GeneralVarData]): - for v in variables: - if id(v) in self._referenced_variables: - raise ValueError( - 'variable {name} has already been added'.format(name=v.name) - ) - self._referenced_variables[id(v)] = [{}, {}, None] - self._vars[id(v)] = ( - v, - v._lb, - v._ub, - v.fixed, - v.domain.get_interval(), - v.value, - ) - self._add_variables(variables) - - @abc.abstractmethod - def _add_params(self, params: List[_ParamData]): - pass - - def add_params(self, params: List[_ParamData]): - for p in params: - self._params[id(p)] = p - self._add_params(params) - - @abc.abstractmethod - def _add_constraints(self, cons: List[_GeneralConstraintData]): - pass - - def _check_for_new_vars(self, variables: List[_GeneralVarData]): - new_vars = {} - for v in variables: - v_id = id(v) - if v_id not in self._referenced_variables: - new_vars[v_id] = v - self.add_variables(list(new_vars.values())) - - def _check_to_remove_vars(self, variables: List[_GeneralVarData]): - vars_to_remove = {} - for v in variables: - v_id = id(v) - ref_cons, ref_sos, ref_obj = self._referenced_variables[v_id] - if len(ref_cons) == 0 and len(ref_sos) == 0 and ref_obj is None: - vars_to_remove[v_id] = v - self.remove_variables(list(vars_to_remove.values())) - - def add_constraints(self, cons: List[_GeneralConstraintData]): - all_fixed_vars = {} - for con in cons: - if con in self._named_expressions: - raise ValueError( - 'constraint {name} has already been added'.format(name=con.name) - ) - self._active_constraints[con] = (con.lower, con.body, con.upper) - tmp = collect_vars_and_named_exprs(con.body) - named_exprs, variables, fixed_vars, external_functions = tmp - self._check_for_new_vars(variables) - self._named_expressions[con] = [(e, e.expr) for e in named_exprs] - if len(external_functions) > 0: - self._external_functions[con] = external_functions - self._vars_referenced_by_con[con] = variables - for v in variables: - self._referenced_variables[id(v)][0][con] = None - if not self.config.auto_updates.treat_fixed_vars_as_params: - for v in fixed_vars: - v.unfix() - all_fixed_vars[id(v)] = v - self._add_constraints(cons) - for v in all_fixed_vars.values(): - v.fix() - - @abc.abstractmethod - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): - pass - - def add_sos_constraints(self, cons: List[_SOSConstraintData]): - for con in cons: - if con in self._vars_referenced_by_con: - raise ValueError( - 'constraint {name} has already been added'.format(name=con.name) - ) - self._active_constraints[con] = tuple() - variables = con.get_variables() - self._check_for_new_vars(variables) - self._named_expressions[con] = [] - self._vars_referenced_by_con[con] = variables - for v in variables: - self._referenced_variables[id(v)][1][con] = None - self._add_sos_constraints(cons) - - @abc.abstractmethod - def _set_objective(self, obj: _GeneralObjectiveData): - pass - - def set_objective(self, obj: _GeneralObjectiveData): - if self._objective is not None: - for v in self._vars_referenced_by_obj: - self._referenced_variables[id(v)][2] = None - self._check_to_remove_vars(self._vars_referenced_by_obj) - self._external_functions.pop(self._objective, None) - if obj is not None: - self._objective = obj - self._objective_expr = obj.expr - self._objective_sense = obj.sense - tmp = collect_vars_and_named_exprs(obj.expr) - named_exprs, variables, fixed_vars, external_functions = tmp - self._check_for_new_vars(variables) - self._obj_named_expressions = [(i, i.expr) for i in named_exprs] - if len(external_functions) > 0: - self._external_functions[obj] = external_functions - self._vars_referenced_by_obj = variables - for v in variables: - self._referenced_variables[id(v)][2] = obj - if not self.config.auto_updates.treat_fixed_vars_as_params: - for v in fixed_vars: - v.unfix() - self._set_objective(obj) - for v in fixed_vars: - v.fix() - else: - self._vars_referenced_by_obj = [] - self._objective = None - self._objective_expr = None - self._objective_sense = None - self._obj_named_expressions = [] - self._set_objective(obj) - - def add_block(self, block): - param_dict = {} - for p in block.component_objects(Param, descend_into=True): - if p.mutable: - for _p in p.values(): - param_dict[id(_p)] = _p - self.add_params(list(param_dict.values())) - self.add_constraints( - list( - block.component_data_objects(Constraint, descend_into=True, active=True) - ) - ) - self.add_sos_constraints( - list( - block.component_data_objects( - SOSConstraint, descend_into=True, active=True - ) - ) - ) - obj = get_objective(block) - if obj is not None: - self.set_objective(obj) - - @abc.abstractmethod - def _remove_constraints(self, cons: List[_GeneralConstraintData]): - pass - - def remove_constraints(self, cons: List[_GeneralConstraintData]): - self._remove_constraints(cons) - for con in cons: - if con not in self._named_expressions: - raise ValueError( - 'cannot remove constraint {name} - it was not added'.format( - name=con.name - ) - ) - for v in self._vars_referenced_by_con[con]: - self._referenced_variables[id(v)][0].pop(con) - self._check_to_remove_vars(self._vars_referenced_by_con[con]) - del self._active_constraints[con] - del self._named_expressions[con] - self._external_functions.pop(con, None) - del self._vars_referenced_by_con[con] - - @abc.abstractmethod - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): - pass - - def remove_sos_constraints(self, cons: List[_SOSConstraintData]): - self._remove_sos_constraints(cons) - for con in cons: - if con not in self._vars_referenced_by_con: - raise ValueError( - 'cannot remove constraint {name} - it was not added'.format( - name=con.name - ) - ) - for v in self._vars_referenced_by_con[con]: - self._referenced_variables[id(v)][1].pop(con) - self._check_to_remove_vars(self._vars_referenced_by_con[con]) - del self._active_constraints[con] - del self._named_expressions[con] - del self._vars_referenced_by_con[con] - - @abc.abstractmethod - def _remove_variables(self, variables: List[_GeneralVarData]): - pass - - def remove_variables(self, variables: List[_GeneralVarData]): - self._remove_variables(variables) - for v in variables: - v_id = id(v) - if v_id not in self._referenced_variables: - raise ValueError( - 'cannot remove variable {name} - it has not been added'.format( - name=v.name - ) - ) - cons_using, sos_using, obj_using = self._referenced_variables[v_id] - if cons_using or sos_using or (obj_using is not None): - raise ValueError( - 'cannot remove variable {name} - it is still being used by constraints or the objective'.format( - name=v.name - ) - ) - del self._referenced_variables[v_id] - del self._vars[v_id] - - @abc.abstractmethod - def _remove_params(self, params: List[_ParamData]): - pass - - def remove_params(self, params: List[_ParamData]): - self._remove_params(params) - for p in params: - del self._params[id(p)] - - def remove_block(self, block): - self.remove_constraints( - list( - block.component_data_objects( - ctype=Constraint, descend_into=True, active=True - ) - ) - ) - self.remove_sos_constraints( - list( - block.component_data_objects( - ctype=SOSConstraint, descend_into=True, active=True - ) - ) - ) - self.remove_params( - list( - dict( - (id(p), p) - for p in block.component_data_objects( - ctype=Param, descend_into=True - ) - ).values() - ) - ) - - @abc.abstractmethod - def _update_variables(self, variables: List[_GeneralVarData]): - pass - - def update_variables(self, variables: List[_GeneralVarData]): - for v in variables: - self._vars[id(v)] = ( - v, - v._lb, - v._ub, - v.fixed, - v.domain.get_interval(), - v.value, - ) - self._update_variables(variables) - - @abc.abstractmethod - def update_params(self): - pass - - def update(self, timer: HierarchicalTimer = None): - if timer is None: - timer = HierarchicalTimer() - config = self.config.auto_updates - new_vars = [] - old_vars = [] - new_params = [] - old_params = [] - new_cons = [] - old_cons = [] - old_sos = [] - new_sos = [] - current_vars_dict = {} - current_cons_dict = {} - current_sos_dict = {} - timer.start('vars') - if config.update_vars: - start_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} - timer.stop('vars') - timer.start('params') - if config.check_for_new_or_removed_params: - current_params_dict = {} - for p in self._model.component_objects(Param, descend_into=True): - if p.mutable: - for _p in p.values(): - current_params_dict[id(_p)] = _p - for p_id, p in current_params_dict.items(): - if p_id not in self._params: - new_params.append(p) - for p_id, p in self._params.items(): - if p_id not in current_params_dict: - old_params.append(p) - timer.stop('params') - timer.start('cons') - if config.check_for_new_or_removed_constraints or config.update_constraints: - current_cons_dict = { - c: None - for c in self._model.component_data_objects( - Constraint, descend_into=True, active=True - ) - } - current_sos_dict = { - c: None - for c in self._model.component_data_objects( - SOSConstraint, descend_into=True, active=True - ) - } - for c in current_cons_dict.keys(): - if c not in self._vars_referenced_by_con: - new_cons.append(c) - for c in current_sos_dict.keys(): - if c not in self._vars_referenced_by_con: - new_sos.append(c) - for c in self._vars_referenced_by_con.keys(): - if c not in current_cons_dict and c not in current_sos_dict: - if (c.ctype is Constraint) or ( - c.ctype is None and isinstance(c, _GeneralConstraintData) - ): - old_cons.append(c) - else: - assert (c.ctype is SOSConstraint) or ( - c.ctype is None and isinstance(c, _SOSConstraintData) - ) - old_sos.append(c) - self.remove_constraints(old_cons) - self.remove_sos_constraints(old_sos) - timer.stop('cons') - timer.start('params') - self.remove_params(old_params) - - # sticking this between removal and addition - # is important so that we don't do unnecessary work - if config.update_params: - self.update_params() - - self.add_params(new_params) - timer.stop('params') - timer.start('vars') - self.add_variables(new_vars) - timer.stop('vars') - timer.start('cons') - self.add_constraints(new_cons) - self.add_sos_constraints(new_sos) - new_cons_set = set(new_cons) - new_sos_set = set(new_sos) - new_vars_set = set(id(v) for v in new_vars) - cons_to_remove_and_add = {} - need_to_set_objective = False - if config.update_constraints: - cons_to_update = [] - sos_to_update = [] - for c in current_cons_dict.keys(): - if c not in new_cons_set: - cons_to_update.append(c) - for c in current_sos_dict.keys(): - if c not in new_sos_set: - sos_to_update.append(c) - for c in cons_to_update: - lower, body, upper = self._active_constraints[c] - new_lower, new_body, new_upper = c.lower, c.body, c.upper - if new_body is not body: - cons_to_remove_and_add[c] = None - continue - if new_lower is not lower: - if ( - type(new_lower) is NumericConstant - and type(lower) is NumericConstant - and new_lower.value == lower.value - ): - pass - else: - cons_to_remove_and_add[c] = None - continue - if new_upper is not upper: - if ( - type(new_upper) is NumericConstant - and type(upper) is NumericConstant - and new_upper.value == upper.value - ): - pass - else: - cons_to_remove_and_add[c] = None - continue - self.remove_sos_constraints(sos_to_update) - self.add_sos_constraints(sos_to_update) - timer.stop('cons') - timer.start('vars') - if config.update_vars: - end_vars = {v_id: v_tuple[0] for v_id, v_tuple in self._vars.items()} - vars_to_check = [v for v_id, v in end_vars.items() if v_id in start_vars] - if config.update_vars: - vars_to_update = [] - for v in vars_to_check: - _v, lb, ub, fixed, domain_interval, value = self._vars[id(v)] - if (fixed != v.fixed) or (fixed and (value != v.value)): - vars_to_update.append(v) - if self.config.auto_updates.treat_fixed_vars_as_params: - for c in self._referenced_variables[id(v)][0]: - cons_to_remove_and_add[c] = None - if self._referenced_variables[id(v)][2] is not None: - need_to_set_objective = True - elif lb is not v._lb: - vars_to_update.append(v) - elif ub is not v._ub: - vars_to_update.append(v) - elif domain_interval != v.domain.get_interval(): - vars_to_update.append(v) - self.update_variables(vars_to_update) - timer.stop('vars') - timer.start('cons') - cons_to_remove_and_add = list(cons_to_remove_and_add.keys()) - self.remove_constraints(cons_to_remove_and_add) - self.add_constraints(cons_to_remove_and_add) - timer.stop('cons') - timer.start('named expressions') - if config.update_named_expressions: - cons_to_update = [] - for c, expr_list in self._named_expressions.items(): - if c in new_cons_set: - continue - for named_expr, old_expr in expr_list: - if named_expr.expr is not old_expr: - cons_to_update.append(c) - break - self.remove_constraints(cons_to_update) - self.add_constraints(cons_to_update) - for named_expr, old_expr in self._obj_named_expressions: - if named_expr.expr is not old_expr: - need_to_set_objective = True - break - timer.stop('named expressions') - timer.start('objective') - if self.config.auto_updates.check_for_new_objective: - pyomo_obj = get_objective(self._model) - if pyomo_obj is not self._objective: - need_to_set_objective = True - else: - pyomo_obj = self._objective - if self.config.auto_updates.update_objective: - if pyomo_obj is not None and pyomo_obj.expr is not self._objective_expr: - need_to_set_objective = True - elif pyomo_obj is not None and pyomo_obj.sense is not self._objective_sense: - # we can definitely do something faster here than resetting the whole objective - need_to_set_objective = True - if need_to_set_objective: - self.set_objective(pyomo_obj) - timer.stop('objective') - - # this has to be done after the objective and constraints in case the - # old objective/constraints use old variables - timer.start('vars') - self.remove_variables(old_vars) - timer.stop('vars') From 92c0262090702505ea0b35437713760fb4f78f57 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 11:23:59 -0700 Subject: [PATCH 0626/3044] Change import statement --- pyomo/contrib/solver/gurobi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 66e03a2a0f4..85131ba73bd 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -33,7 +33,7 @@ from pyomo.contrib.solver.base import PersistentSolverBase from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus from pyomo.contrib.solver.config import PersistentBranchAndBoundConfig -from pyomo.contrib.solver.util import PersistentSolverUtils +from pyomo.contrib.solver.persistent import PersistentSolverUtils from pyomo.contrib.solver.solution import PersistentSolutionLoader from pyomo.core.staleflag import StaleFlagManager import sys From 3447f0792f767590e806c5bb9929984c6ecb063f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 11:54:12 -0700 Subject: [PATCH 0627/3044] Update ipopt imports; change available and version --- .../developer_reference/solvers.rst | 12 ++--- pyomo/contrib/solver/ipopt.py | 45 ++++++++++--------- pyomo/contrib/solver/plugins.py | 4 +- .../solver/tests/solvers/test_ipopt.py | 4 +- .../solver/tests/solvers/test_solvers.py | 16 +++---- 5 files changed, 43 insertions(+), 38 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 8c8c9e5b8ee..921e452004d 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -63,7 +63,7 @@ or changed ``SolverFactory`` version. # Direct import import pyomo.environ as pyo from pyomo.contrib.solver.util import assert_optimal_termination - from pyomo.contrib.solver.ipopt import ipopt + from pyomo.contrib.solver.ipopt import Ipopt model = pyo.ConcreteModel() model.x = pyo.Var(initialize=1.5) @@ -74,7 +74,7 @@ or changed ``SolverFactory`` version. model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) - opt = ipopt() + opt = Ipopt() status = opt.solve(model) assert_optimal_termination(status) # Displays important results information; only available in future capability mode @@ -112,7 +112,7 @@ The new interface will allow for direct manipulation of linear presolve and scal options for certain solvers. Currently, these options are only available for ``ipopt``. -.. autoclass:: pyomo.contrib.solver.ipopt.ipopt +.. autoclass:: pyomo.contrib.solver.ipopt.Ipopt :members: solve The ``writer_config`` configuration option can be used to manipulate presolve @@ -120,8 +120,8 @@ and scaling options: .. code-block:: python - >>> from pyomo.contrib.solver.ipopt import ipopt - >>> opt = ipopt() + >>> from pyomo.contrib.solver.ipopt import Ipopt + >>> opt = Ipopt() >>> opt.config.writer_config.display() show_section_timing: false @@ -213,7 +213,7 @@ Solutions can be loaded back into a model using a ``SolutionLoader``. A specific loader should be written for each unique case. Several have already been implemented. For example, for ``ipopt``: -.. autoclass:: pyomo.contrib.solver.ipopt.ipoptSolutionLoader +.. autoclass:: pyomo.contrib.solver.ipopt.IpoptSolutionLoader :show-inheritance: :members: :inherited-members: diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 1aa00c0c8e2..3b1e95da42c 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -221,20 +221,26 @@ def __init__(self, **kwds): self._writer = NLWriter() self._available_cache = None self._version_cache = None - self.executable = self.config.executable - - def available(self): - if self._available_cache is None: - if self.executable.path() is None: - self._available_cache = self.Availability.NotFound + self._executable = self.config.executable + + def available(self, config=None): + if config is None: + config = self.config + pth = config.executable.path() + if self._available_cache is None or self._available_cache[0] != pth: + if pth is None: + self._available_cache = (None, self.Availability.NotFound) else: - self._available_cache = self.Availability.FullLicense - return self._available_cache - - def version(self): - if self._version_cache is None: + self._available_cache = (pth, self.Availability.FullLicense) + return self._available_cache[1] + + def version(self, config=None): + if config is None: + config = self.config + pth = config.executable.path() + if self._version_cache is None or self._version_cache[0] != pth: results = subprocess.run( - [str(self.executable), '--version'], + [str(pth), '--version'], timeout=1, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -243,8 +249,8 @@ def version(self): version = results.stdout.splitlines()[0] version = version.split(' ')[1].strip() version = tuple(int(i) for i in version.split('.')) - self._version_cache = version - return self._version_cache + self._version_cache = (pth, version) + return self._version_cache[1] def _write_options_file(self, filename: str, options: Mapping): # First we need to determine if we even need to create a file. @@ -263,7 +269,7 @@ def _write_options_file(self, filename: str, options: Mapping): return opt_file_exists def _create_command_line(self, basename: str, config: IpoptConfig, opt_file: bool): - cmd = [str(self.executable), basename + '.nl', '-AMPL'] + cmd = [str(self._executable), basename + '.nl', '-AMPL'] if opt_file: cmd.append('option_file_name=' + basename + '.opt') if 'option_file_name' in config.solver_options: @@ -285,15 +291,14 @@ def _create_command_line(self, basename: str, config: IpoptConfig, opt_file: boo def solve(self, model, **kwds): # Begin time tracking start_timestamp = datetime.datetime.now(datetime.timezone.utc) + # Update configuration options, based on keywords passed to solve + config: IpoptConfig = self.config(value=kwds, preserve_implicit=True) # Check if solver is available - avail = self.available() + avail = self.available(config) if not avail: raise ipoptSolverError( f'Solver {self.__class__} is not available ({avail}).' ) - # Update configuration options, based on keywords passed to solve - config: IpoptConfig = self.config(value=kwds, preserve_implicit=True) - self.executable = config.executable if config.threads: logger.log( logging.WARNING, @@ -397,7 +402,7 @@ def solve(self, model, **kwds): ) results.solver_name = self.name - results.solver_version = self.version() + results.solver_version = self.version(config) if ( config.load_solutions and results.solution_status == SolutionStatus.noSolution diff --git a/pyomo/contrib/solver/plugins.py b/pyomo/contrib/solver/plugins.py index cb089200100..c7da41463a2 100644 --- a/pyomo/contrib/solver/plugins.py +++ b/pyomo/contrib/solver/plugins.py @@ -11,14 +11,14 @@ from .factory import SolverFactory -from .ipopt import ipopt +from .ipopt import Ipopt from .gurobi import Gurobi def load(): SolverFactory.register( name='ipopt', legacy_name='ipopt_v2', doc='The IPOPT NLP solver (new interface)' - )(ipopt) + )(Ipopt) SolverFactory.register( name='gurobi', legacy_name='gurobi_v2', doc='New interface to Gurobi' )(Gurobi) diff --git a/pyomo/contrib/solver/tests/solvers/test_ipopt.py b/pyomo/contrib/solver/tests/solvers/test_ipopt.py index 2886045055c..dc6bcf24855 100644 --- a/pyomo/contrib/solver/tests/solvers/test_ipopt.py +++ b/pyomo/contrib/solver/tests/solvers/test_ipopt.py @@ -13,7 +13,7 @@ import pyomo.environ as pyo from pyomo.common.fileutils import ExecutableData from pyomo.common.config import ConfigDict -from pyomo.contrib.solver.ipopt import ipoptConfig +from pyomo.contrib.solver.ipopt import IpoptConfig from pyomo.contrib.solver.factory import SolverFactory from pyomo.common import unittest @@ -42,7 +42,7 @@ def rosenbrock(m): def test_ipopt_config(self): # Test default initialization - config = ipoptConfig() + config = IpoptConfig() self.assertTrue(config.load_solutions) self.assertIsInstance(config.solver_options, ConfigDict) self.assertIsInstance(config.executable, ExecutableData) diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index e5af2ada170..2b9e783ad16 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -17,7 +17,7 @@ parameterized = parameterized.parameterized from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus, Results from pyomo.contrib.solver.base import SolverBase -from pyomo.contrib.solver.ipopt import ipopt +from pyomo.contrib.solver.ipopt import Ipopt from pyomo.contrib.solver.gurobi import Gurobi from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression @@ -31,10 +31,10 @@ if not param_available: raise unittest.SkipTest('Parameterized is not available.') -all_solvers = [('gurobi', Gurobi), ('ipopt', ipopt)] +all_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] mip_solvers = [('gurobi', Gurobi)] -nlp_solvers = [('ipopt', ipopt)] -qcp_solvers = [('gurobi', Gurobi), ('ipopt', ipopt)] +nlp_solvers = [('ipopt', Ipopt)] +qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] miqcqp_solvers = [('gurobi', Gurobi)] @@ -256,7 +256,7 @@ def test_equality(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') - if isinstance(opt, ipopt): + if isinstance(opt, Ipopt): opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() @@ -429,7 +429,7 @@ def test_results_infeasible(self, name: str, opt_class: Type[SolverBase]): opt.config.raise_exception_on_nonoptimal_result = False res = opt.solve(m) self.assertNotEqual(res.solution_status, SolutionStatus.optimal) - if isinstance(opt, ipopt): + if isinstance(opt, Ipopt): acceptable_termination_conditions = { TerminationCondition.locallyInfeasible, TerminationCondition.unbounded, @@ -444,7 +444,7 @@ def test_results_infeasible(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, None) self.assertTrue(res.incumbent_objective is None) - if not isinstance(opt, ipopt): + if not isinstance(opt, Ipopt): # ipopt can return the values of the variables/duals at the last iterate # even if it did not converge; raise_exception_on_nonoptimal_result # is set to False, so we are free to load infeasible solutions @@ -970,7 +970,7 @@ def test_time_limit(self, name: str, opt_class: Type[SolverBase]): constant=0, ) m.c2[t] = expr == 1 - if isinstance(opt, ipopt): + if isinstance(opt, Ipopt): opt.config.time_limit = 1e-6 else: opt.config.time_limit = 0 From 74a9c7b32a44c9242d8f1abdb56045e8ae99bcb8 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 19 Feb 2024 11:57:03 -0700 Subject: [PATCH 0628/3044] add mpc readme --- pyomo/contrib/mpc/README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 pyomo/contrib/mpc/README.md diff --git a/pyomo/contrib/mpc/README.md b/pyomo/contrib/mpc/README.md new file mode 100644 index 00000000000..21fe39c5f50 --- /dev/null +++ b/pyomo/contrib/mpc/README.md @@ -0,0 +1,34 @@ +# Pyomo MPC + +Pyomo MPC is an extension for developing model predictive control simulations +using Pyomo models. Please see the +[documentation](https://pyomo.readthedocs.io/en/stable/contributed_packages/mpc/index.html) +for more detailed information. + +Pyomo MPC helps with, among other things, the following use cases: +- Transfering values between different points in time in a dynamic model +(e.g. to initialize a dynamic model to its initial conditions) +- Extracting or loading disturbances and inputs from or to models, and storing +these in model-agnostic, easily JSON-serializable data structures +- Constructing common modeling components, such as weighted-least-squares +tracking objective functions, piecewise-constant input constraints, or +terminal region constraints. + +## Citation + +If you use Pyomo MPC in your research, please cite the following paper, which +discusses the motivation for the Pyomo MPC data structures and the underlying +Pyomo features that make them possible. +```bibtex +@article{parker2023mpc, +title = {Model predictive control simulations with block-hierarchical differential-algebraic process models}, +journal = {Journal of Process Control}, +volume = {132}, +pages = {103113}, +year = {2023}, +issn = {0959-1524}, +doi = {https://doi.org/10.1016/j.jprocont.2023.103113}, +url = {https://www.sciencedirect.com/science/article/pii/S0959152423002007}, +author = {Robert B. Parker and Bethany L. Nicholson and John D. Siirola and Lorenz T. Biegler}, +} +``` From 321a18f8085d5b0c77ed6295606bd0b4d221e0ae Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 19 Feb 2024 12:04:13 -0700 Subject: [PATCH 0629/3044] add citation to mpc/index --- .../contributed_packages/mpc/index.rst | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/mpc/index.rst b/doc/OnlineDocs/contributed_packages/mpc/index.rst index b93abf223e2..c9ac929a71a 100644 --- a/doc/OnlineDocs/contributed_packages/mpc/index.rst +++ b/doc/OnlineDocs/contributed_packages/mpc/index.rst @@ -1,7 +1,7 @@ MPC === -This package contains data structures and utilities for dynamic optimization +Pyomo MPC contains data structures and utilities for dynamic optimization and rolling horizon applications, e.g. model predictive control. .. toctree:: @@ -10,3 +10,22 @@ and rolling horizon applications, e.g. model predictive control. overview.rst examples.rst faq.rst + +Citation +-------- + +If you use Pyomo MPC in your research, please cite the following paper: + +.. code-block:: bibtex + + @article{parker2023mpc, + title = {Model predictive control simulations with block-hierarchical differential-algebraic process models}, + journal = {Journal of Process Control}, + volume = {132}, + pages = {103113}, + year = {2023}, + issn = {0959-1524}, + doi = {https://doi.org/10.1016/j.jprocont.2023.103113}, + url = {https://www.sciencedirect.com/science/article/pii/S0959152423002007}, + author = {Robert B. Parker and Bethany L. Nicholson and John D. Siirola and Lorenz T. Biegler}, + } From b902a30ec8dd0f259aaef32516b3d94009ddb448 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 12:16:42 -0700 Subject: [PATCH 0630/3044] If sol file exists, parse it --- pyomo/contrib/solver/ipopt.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 3b1e95da42c..580f350dff3 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -380,16 +380,18 @@ def solve(self, model, **kwds): ostreams[0] ) - if process.returncode != 0: + if os.path.isfile(basename + '.sol'): + with open(basename + '.sol', 'r') as sol_file: + timer.start('parse_sol') + results = self._parse_solution(sol_file, nl_info) + timer.stop('parse_sol') + else: results = IpoptResults() + if process.returncode != 0: results.extra_info.return_code = process.returncode results.termination_condition = TerminationCondition.error results.solution_loader = SolSolutionLoader(None, None) else: - with open(basename + '.sol', 'r') as sol_file: - timer.start('parse_sol') - results = self._parse_solution(sol_file, nl_info) - timer.stop('parse_sol') results.iteration_count = iters results.timing_info.ipopt_excluding_nlp_functions = ipopt_time_nofunc results.timing_info.nlp_function_evaluations = ipopt_time_func From 15eddd3a21560cd46d306cee15fba3a61863dedc Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 12:37:13 -0700 Subject: [PATCH 0631/3044] Update _parse_ipopt_output to address newer versions of IPOPT --- pyomo/contrib/solver/ipopt.py | 46 ++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 580f350dff3..fc009c77522 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -101,14 +101,33 @@ def __init__( ) self.timing_info.ipopt_excluding_nlp_functions: Optional[float] = ( self.timing_info.declare( - 'ipopt_excluding_nlp_functions', ConfigValue(domain=NonNegativeFloat) + 'ipopt_excluding_nlp_functions', + ConfigValue( + domain=NonNegativeFloat, + default=None, + description="Total CPU seconds in IPOPT without function evaluations.", + ), ) ) self.timing_info.nlp_function_evaluations: Optional[float] = ( self.timing_info.declare( - 'nlp_function_evaluations', ConfigValue(domain=NonNegativeFloat) + 'nlp_function_evaluations', + ConfigValue( + domain=NonNegativeFloat, + default=None, + description="Total CPU seconds in NLP function evaluations.", + ), ) ) + self.timing_info.total_seconds: Optional[float] = self.timing_info.declare( + 'total_seconds', + ConfigValue( + domain=NonNegativeFloat, + default=None, + description="Total seconds in IPOPT. NOTE: Newer versions of IPOPT (3.14+) " + "no longer separate timing information.", + ), + ) class IpoptSolutionLoader(SolSolutionLoader): @@ -376,8 +395,8 @@ def solve(self, model, **kwds): timer.stop('subprocess') # This is the stuff we need to parse to get the iterations # and time - iters, ipopt_time_nofunc, ipopt_time_func = self._parse_ipopt_output( - ostreams[0] + iters, ipopt_time_nofunc, ipopt_time_func, ipopt_total_time = ( + self._parse_ipopt_output(ostreams[0]) ) if os.path.isfile(basename + '.sol'): @@ -395,12 +414,14 @@ def solve(self, model, **kwds): results.iteration_count = iters results.timing_info.ipopt_excluding_nlp_functions = ipopt_time_nofunc results.timing_info.nlp_function_evaluations = ipopt_time_func + results.timing_info.total_seconds = ipopt_total_time if ( config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal ): raise RuntimeError( - 'Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' + 'Solver did not find the optimal solution. Set ' + 'opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' ) results.solver_name = self.name @@ -411,7 +432,7 @@ def solve(self, model, **kwds): ): raise RuntimeError( 'A feasible solution was not found, so no solution can be loaded.' - 'Please set config.load_solutions=False to bypass this error.' + 'Please set opt.config.load_solutions=False to bypass this error.' ) if config.load_solutions: @@ -472,23 +493,30 @@ def _parse_ipopt_output(self, stream: io.StringIO): iters = None nofunc_time = None func_time = None + total_time = None # parse the output stream to get the iteration count and solver time for line in stream.getvalue().splitlines(): if line.startswith("Number of Iterations....:"): tokens = line.split() iters = int(tokens[3]) + elif line.startswith( + "Total seconds in IPOPT =" + ): + # Newer versions of IPOPT no longer separate the + tokens = line.split() + total_time = float(tokens[-1]) elif line.startswith( "Total CPU secs in IPOPT (w/o function evaluations) =" ): tokens = line.split() - nofunc_time = float(tokens[9]) + nofunc_time = float(tokens[-1]) elif line.startswith( "Total CPU secs in NLP function evaluations =" ): tokens = line.split() - func_time = float(tokens[8]) + func_time = float(tokens[-1]) - return iters, nofunc_time, func_time + return iters, nofunc_time, func_time, total_time def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): results = IpoptResults() From d8536b6cd1751c594d81c7c88ca0bcaa0d380589 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 19 Feb 2024 12:37:26 -0700 Subject: [PATCH 0632/3044] add api docs to mpi --- doc/OnlineDocs/contributed_packages/mpc/api.rst | 10 ++++++++++ .../contributed_packages/mpc/conversion.rst | 5 +++++ .../contributed_packages/mpc/data.rst | 17 +++++++++++++++++ .../contributed_packages/mpc/index.rst | 1 + .../contributed_packages/mpc/interface.rst | 8 ++++++++ .../contributed_packages/mpc/modeling.rst | 11 +++++++++++ 6 files changed, 52 insertions(+) create mode 100644 doc/OnlineDocs/contributed_packages/mpc/api.rst create mode 100644 doc/OnlineDocs/contributed_packages/mpc/conversion.rst create mode 100644 doc/OnlineDocs/contributed_packages/mpc/data.rst create mode 100644 doc/OnlineDocs/contributed_packages/mpc/interface.rst create mode 100644 doc/OnlineDocs/contributed_packages/mpc/modeling.rst diff --git a/doc/OnlineDocs/contributed_packages/mpc/api.rst b/doc/OnlineDocs/contributed_packages/mpc/api.rst new file mode 100644 index 00000000000..2752fea8af6 --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mpc/api.rst @@ -0,0 +1,10 @@ +.. _mpc_api: + +API Reference +============= + +.. toctree:: + data.rst + conversion.rst + interface.rst + modeling.rst diff --git a/doc/OnlineDocs/contributed_packages/mpc/conversion.rst b/doc/OnlineDocs/contributed_packages/mpc/conversion.rst new file mode 100644 index 00000000000..9d9406edb75 --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mpc/conversion.rst @@ -0,0 +1,5 @@ +Data Conversion +=============== + +.. automodule:: pyomo.contrib.mpc.data.convert + :members: diff --git a/doc/OnlineDocs/contributed_packages/mpc/data.rst b/doc/OnlineDocs/contributed_packages/mpc/data.rst new file mode 100644 index 00000000000..73cb6543b1e --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mpc/data.rst @@ -0,0 +1,17 @@ +Data Structures +=============== + +.. automodule:: pyomo.contrib.mpc.data.get_cuid + :members: + +.. automodule:: pyomo.contrib.mpc.data.dynamic_data_base + :members: + +.. automodule:: pyomo.contrib.mpc.data.scalar_data + :members: + +.. automodule:: pyomo.contrib.mpc.data.series_data + :members: + +.. automodule:: pyomo.contrib.mpc.data.interval_data + :members: diff --git a/doc/OnlineDocs/contributed_packages/mpc/index.rst b/doc/OnlineDocs/contributed_packages/mpc/index.rst index c9ac929a71a..e512d1a6ef5 100644 --- a/doc/OnlineDocs/contributed_packages/mpc/index.rst +++ b/doc/OnlineDocs/contributed_packages/mpc/index.rst @@ -10,6 +10,7 @@ and rolling horizon applications, e.g. model predictive control. overview.rst examples.rst faq.rst + api.rst Citation -------- diff --git a/doc/OnlineDocs/contributed_packages/mpc/interface.rst b/doc/OnlineDocs/contributed_packages/mpc/interface.rst new file mode 100644 index 00000000000..eb5bac548fd --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mpc/interface.rst @@ -0,0 +1,8 @@ +Interfaces +========== + +.. automodule:: pyomo.contrib.mpc.interfaces.model_interface + :members: + +.. automodule:: pyomo.contrib.mpc.interfaces.var_linker + :members: diff --git a/doc/OnlineDocs/contributed_packages/mpc/modeling.rst b/doc/OnlineDocs/contributed_packages/mpc/modeling.rst new file mode 100644 index 00000000000..cbae03161b1 --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/mpc/modeling.rst @@ -0,0 +1,11 @@ +Modeling Components +=================== + +.. automodule:: pyomo.contrib.mpc.modeling.constraints + :members: + +.. automodule:: pyomo.contrib.mpc.modeling.cost_expressions + :members: + +.. automodule:: pyomo.contrib.mpc.modeling.terminal + :members: From 7ae3ed8d737f82857ab8e5654bb1c7f6973933c9 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 19 Feb 2024 12:53:13 -0700 Subject: [PATCH 0633/3044] clarify definition of "flatten" and add citation --- .../advanced_topics/flattener/index.rst | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/advanced_topics/flattener/index.rst b/doc/OnlineDocs/advanced_topics/flattener/index.rst index 377de5233ec..982a8931d36 100644 --- a/doc/OnlineDocs/advanced_topics/flattener/index.rst +++ b/doc/OnlineDocs/advanced_topics/flattener/index.rst @@ -30,8 +30,9 @@ The ``pyomo.dae.flatten`` module aims to address this use case by providing utilities to generate all components indexed, explicitly or implicitly, by user-provided sets. -**When we say "flatten a model," we mean "generate all components in the model, -preserving all user-specified indexing sets."** +**When we say "flatten a model," we mean "recursively generate all components in +the model, where a component can be indexed only by user-specified indexing +sets (or is not indexed at all)**. Data structures --------------- @@ -42,3 +43,23 @@ Slices are necessary as they can encode "implicit indexing" -- where a component is contained in an indexed block. It is natural to return references to these slices, so they may be accessed and manipulated like any other component. + +Citation +-------- +If you use the ``pyomo.dae.flatten`` module in your research, we would appreciate +you citing the following paper, which gives more detail about the motivation for +and examples of using this functinoality. + +.. code-block:: bibtex + + @article{parker2023mpc, + title = {Model predictive control simulations with block-hierarchical differential-algebraic process models}, + journal = {Journal of Process Control}, + volume = {132}, + pages = {103113}, + year = {2023}, + issn = {0959-1524}, + doi = {https://doi.org/10.1016/j.jprocont.2023.103113}, + url = {https://www.sciencedirect.com/science/article/pii/S0959152423002007}, + author = {Robert B. Parker and Bethany L. Nicholson and John D. Siirola and Lorenz T. Biegler}, + } From 90d03c15b3e732b2f00f5f49ab6f63d6e1f3f744 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 19 Feb 2024 13:03:36 -0700 Subject: [PATCH 0634/3044] improve docstring and fix typo --- pyomo/contrib/mpc/data/get_cuid.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/mpc/data/get_cuid.py b/pyomo/contrib/mpc/data/get_cuid.py index 03659d6153f..ef0df7ea679 100644 --- a/pyomo/contrib/mpc/data/get_cuid.py +++ b/pyomo/contrib/mpc/data/get_cuid.py @@ -16,14 +16,13 @@ def get_indexed_cuid(var, sets=None, dereference=None, context=None): - """ - Attempts to convert the provided "var" object into a CUID with - with wildcards. + """Attempt to convert the provided "var" object into a CUID with wildcards Arguments --------- var: - Object to process + Object to process. May be a VarData, IndexedVar (reference or otherwise), + ComponentUID, slice, or string. sets: Tuple of sets Sets to use if slicing a vardata object dereference: None or int @@ -32,6 +31,11 @@ def get_indexed_cuid(var, sets=None, dereference=None, context=None): context: Block Block with respect to which slices and CUIDs will be generated + Returns + ------- + ``ComponentUID`` + ComponentUID corresponding to the provided ``var`` and sets + """ # TODO: Does this function have a good name? # Should this function be generalized beyond a single indexing set? From bc4c71bf3469ac1fa68f888290bfc7f42458f7a7 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 19 Feb 2024 13:07:08 -0700 Subject: [PATCH 0635/3044] add end quote --- doc/OnlineDocs/advanced_topics/flattener/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/advanced_topics/flattener/index.rst b/doc/OnlineDocs/advanced_topics/flattener/index.rst index 982a8931d36..f9dd8ea6abb 100644 --- a/doc/OnlineDocs/advanced_topics/flattener/index.rst +++ b/doc/OnlineDocs/advanced_topics/flattener/index.rst @@ -31,7 +31,7 @@ utilities to generate all components indexed, explicitly or implicitly, by user-provided sets. **When we say "flatten a model," we mean "recursively generate all components in -the model, where a component can be indexed only by user-specified indexing +the model," where a component can be indexed only by user-specified indexing sets (or is not indexed at all)**. Data structures From 76c970f35f9cedfa6890b64fddd30570f53961a4 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 19 Feb 2024 13:08:03 -0700 Subject: [PATCH 0636/3044] fix typo --- pyomo/contrib/mpc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/mpc/README.md b/pyomo/contrib/mpc/README.md index 21fe39c5f50..7e03163f703 100644 --- a/pyomo/contrib/mpc/README.md +++ b/pyomo/contrib/mpc/README.md @@ -6,7 +6,7 @@ using Pyomo models. Please see the for more detailed information. Pyomo MPC helps with, among other things, the following use cases: -- Transfering values between different points in time in a dynamic model +- Transferring values between different points in time in a dynamic model (e.g. to initialize a dynamic model to its initial conditions) - Extracting or loading disturbances and inputs from or to models, and storing these in model-agnostic, easily JSON-serializable data structures From afcedb15449f76d308d73a96fed18dbb0eda5638 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 13:27:08 -0700 Subject: [PATCH 0637/3044] Incomplete comment; stronger parsing --- pyomo/contrib/solver/ipopt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index fc009c77522..074e5b19c5c 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -498,11 +498,13 @@ def _parse_ipopt_output(self, stream: io.StringIO): for line in stream.getvalue().splitlines(): if line.startswith("Number of Iterations....:"): tokens = line.split() - iters = int(tokens[3]) + iters = int(tokens[-1]) elif line.startswith( "Total seconds in IPOPT =" ): - # Newer versions of IPOPT no longer separate the + # Newer versions of IPOPT no longer separate timing into + # two different values. This is so we have compatibility with + # both new and old versions tokens = line.split() total_time = float(tokens[-1]) elif line.startswith( From dfea3ecca321cef955e31d9a5d48412014f2fad6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 13:29:51 -0700 Subject: [PATCH 0638/3044] Update Component{Map,Set} to support tuple keys --- pyomo/common/collections/component_map.py | 56 +++++++++++++----- pyomo/common/collections/component_set.py | 70 +++++++++++------------ 2 files changed, 77 insertions(+), 49 deletions(-) diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index 80ba5fe0d1c..0851ffad301 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -9,21 +9,49 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from collections.abc import MutableMapping as collections_MutableMapping +import collections from collections.abc import Mapping as collections_Mapping from pyomo.common.autoslots import AutoSlots -def _rebuild_ids(encode, val): +def _rehash_keys(encode, val): if encode: - return val + return list(val.values()) else: # object id() may have changed after unpickling, # so we rebuild the dictionary keys - return {id(obj): (obj, v) for obj, v in val.values()} + return {_hasher[obj.__class__](obj): (obj, v) for obj, v in val} + + +class _Hasher(collections.defaultdict): + def __init__(self, *args, **kwargs): + super().__init__(lambda: self._missing_impl, *args, **kwargs) + self[tuple] = self._tuple + + def _missing_impl(self, val): + try: + hash(val) + self[val.__class__] = self._hashable + except: + self[val.__class__] = self._unhashable + return self[val.__class__](val) + + @staticmethod + def _hashable(val): + return val + + @staticmethod + def _unhashable(val): + return id(val) + + def _tuple(self, val): + return tuple(self[i.__class__](i) for i in val) + + +_hasher = _Hasher() -class ComponentMap(AutoSlots.Mixin, collections_MutableMapping): +class ComponentMap(AutoSlots.Mixin, collections.abc.MutableMapping): """ This class is a replacement for dict that allows Pyomo modeling components to be used as entry keys. The @@ -49,7 +77,7 @@ class ComponentMap(AutoSlots.Mixin, collections_MutableMapping): """ __slots__ = ("_dict",) - __autoslot_mappers__ = {'_dict': _rebuild_ids} + __autoslot_mappers__ = {'_dict': _rehash_keys} def __init__(self, *args, **kwds): # maps id(obj) -> (obj,val) @@ -68,18 +96,20 @@ def __str__(self): def __getitem__(self, obj): try: - return self._dict[id(obj)][1] + return self._dict[_hasher[obj.__class__](obj)][1] except KeyError: - raise KeyError("Component with id '%s': %s" % (id(obj), str(obj))) + _id = _hasher[obj.__class__](obj) + raise KeyError("Component with id '%s': %s" % (_id, obj)) def __setitem__(self, obj, val): - self._dict[id(obj)] = (obj, val) + self._dict[_hasher[obj.__class__](obj)] = (obj, val) def __delitem__(self, obj): try: - del self._dict[id(obj)] + del self._dict[_hasher[obj.__class__](obj)] except KeyError: - raise KeyError("Component with id '%s': %s" % (id(obj), str(obj))) + _id = _hasher[obj.__class__](obj) + raise KeyError("Component with id '%s': %s" % (_id, obj)) def __iter__(self): return (obj for obj, val in self._dict.values()) @@ -107,7 +137,7 @@ def __eq__(self, other): return False # Note we have already verified the dicts are the same size for key, val in other.items(): - other_id = id(key) + other_id = _hasher[key.__class__](key) if other_id not in self._dict: return False self_val = self._dict[other_id][1] @@ -130,7 +160,7 @@ def __ne__(self, other): # def __contains__(self, obj): - return id(obj) in self._dict + return _hasher[obj.__class__](obj) in self._dict def clear(self): 'D.clear() -> None. Remove all items from D.' diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index dfeac5cbfa5..f1fe7bc8cd6 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -12,8 +12,20 @@ from collections.abc import MutableSet as collections_MutableSet from collections.abc import Set as collections_Set +from pyomo.common.autoslots import AutoSlots +from pyomo.common.collections.component_map import _hasher -class ComponentSet(collections_MutableSet): + +def _rehash_keys(encode, val): + if encode: + return list(val.values()) + else: + # object id() may have changed after unpickling, + # so we rebuild the dictionary keys + return {_hasher[obj.__class__](obj): obj for obj in val} + + +class ComponentSet(AutoSlots.Mixin, collections_MutableSet): """ This class is a replacement for set that allows Pyomo modeling components to be used as entries. The @@ -38,16 +50,12 @@ class ComponentSet(collections_MutableSet): """ __slots__ = ("_data",) + __autoslot_mappers__ = {'_data': _rehash_keys} - def __init__(self, *args): - self._data = dict() - if len(args) > 0: - if len(args) > 1: - raise TypeError( - "%s expected at most 1 arguments, " - "got %s" % (self.__class__.__name__, len(args)) - ) - self.update(args[0]) + def __init__(self, iterable=None): + self._data = {} + if iterable is not None: + self.update(iterable) def __str__(self): """String representation of the mapping.""" @@ -56,29 +64,19 @@ def __str__(self): tmp.append(str(obj) + " (id=" + str(objid) + ")") return "ComponentSet(" + str(tmp) + ")" - def update(self, args): + def update(self, iterable): """Update a set with the union of itself and others.""" - self._data.update((id(obj), obj) for obj in args) - - # - # This method must be defined for deepcopy/pickling - # because this class relies on Python ids. - # - def __setstate__(self, state): - # object id() may have changed after unpickling, - # so we rebuild the dictionary keys - assert len(state) == 1 - self._data = {id(obj): obj for obj in state['_data']} - - def __getstate__(self): - return {'_data': tuple(self._data.values())} + if isinstance(iterable, ComponentSet): + self._data.update(iterable._data) + else: + self._data.update((_hasher[val.__class__](val), val) for val in iterable) # # Implement MutableSet abstract methods # def __contains__(self, val): - return self._data.__contains__(id(val)) + return _hasher[val.__class__](val) in self._data def __iter__(self): return iter(self._data.values()) @@ -88,27 +86,26 @@ def __len__(self): def add(self, val): """Add an element.""" - self._data[id(val)] = val + self._data[_hasher[val.__class__](val)] = val def discard(self, val): """Remove an element. Do not raise an exception if absent.""" - if id(val) in self._data: - del self._data[id(val)] + _id = _hasher[val.__class__](val) + if _id in self._data: + del self._data[_id] # # Overload MutableSet default implementations # - # We want to avoid generating Pyomo expressions due to - # comparison of values, so we convert both objects to a - # plain dictionary mapping key->(type(val), id(val)) and - # compare that instead. def __eq__(self, other): if self is other: return True if not isinstance(other, collections_Set): return False - return len(self) == len(other) and all(id(key) in self._data for key in other) + return len(self) == len(other) and all( + _hasher[val.__class__](val) in self._data for val in other + ) def __ne__(self, other): return not (self == other) @@ -125,6 +122,7 @@ def clear(self): def remove(self, val): """Remove an element. If not a member, raise a KeyError.""" try: - del self._data[id(val)] + del self._data[_hasher[val.__class__](val)] except KeyError: - raise KeyError("Component with id '%s': %s" % (id(val), str(val))) + _id = _hasher[val.__class__](val) + raise KeyError("Component with id '%s': %s" % (_id, val)) From b817cfcb1362e59cef492329aa835a5e4cdad2d7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 13:30:18 -0700 Subject: [PATCH 0639/3044] Add test if using tuples in ComponentMap --- pyomo/common/tests/test_component_map.py | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 pyomo/common/tests/test_component_map.py diff --git a/pyomo/common/tests/test_component_map.py b/pyomo/common/tests/test_component_map.py new file mode 100644 index 00000000000..cc746642f28 --- /dev/null +++ b/pyomo/common/tests/test_component_map.py @@ -0,0 +1,50 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest + +from pyomo.common.collections import ComponentMap +from pyomo.environ import ConcreteModel, Var, Constraint + + +class TestComponentMap(unittest.TestCase): + def test_tuple(self): + m = ConcreteModel() + m.v = Var() + m.c = Constraint(expr=m.v >= 0) + m.cm = cm = ComponentMap() + + cm[(1,2)] = 5 + self.assertEqual(len(cm), 1) + self.assertIn((1,2), cm) + self.assertEqual(cm[1,2], 5) + + cm[(1,2)] = 50 + self.assertEqual(len(cm), 1) + self.assertIn((1,2), cm) + self.assertEqual(cm[1,2], 50) + + cm[(1, (2, m.v))] = 10 + self.assertEqual(len(cm), 2) + self.assertIn((1,(2, m.v)), cm) + self.assertEqual(cm[1, (2, m.v)], 10) + + cm[(1, (2, m.v))] = 100 + self.assertEqual(len(cm), 2) + self.assertIn((1,(2, m.v)), cm) + self.assertEqual(cm[1, (2, m.v)], 100) + + i = m.clone() + self.assertIn((1, 2), i.cm) + self.assertIn((1, (2, i.v)), i.cm) + self.assertNotIn((1, (2, i.v)), m.cm) + self.assertIn((1, (2, m.v)), m.cm) + self.assertNotIn((1, (2, m.v)), i.cm) From eb672d5e0190ea9c27d40e66e252b07f37b06353 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 13:32:42 -0700 Subject: [PATCH 0640/3044] Clean up / update OrderedSet (for post-Python 3.7) --- pyomo/common/collections/orderedset.py | 30 ++++++++------------------ 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/pyomo/common/collections/orderedset.py b/pyomo/common/collections/orderedset.py index f29245b75fe..6bcf0c2fafb 100644 --- a/pyomo/common/collections/orderedset.py +++ b/pyomo/common/collections/orderedset.py @@ -9,42 +9,30 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from collections.abc import MutableSet from collections import OrderedDict +from collections.abc import MutableSet +from pyomo.common.autoslots import AutoSlots -class OrderedSet(MutableSet): +class OrderedSet(AutoSlots.Mixin, MutableSet): __slots__ = ('_dict',) def __init__(self, iterable=None): # TODO: Starting in Python 3.7, dict is ordered (and is faster # than OrderedDict). dict began supporting reversed() in 3.8. - # We should consider changing the underlying data type here from - # OrderedDict to dict. - self._dict = OrderedDict() + self._dict = {} if iterable is not None: - if iterable.__class__ is OrderedSet: - self._dict.update(iterable._dict) - else: - self.update(iterable) + self.update(iterable) def __str__(self): """String representation of the mapping.""" return "OrderedSet(%s)" % (', '.join(repr(x) for x in self)) def update(self, iterable): - for val in iterable: - self.add(val) - - # - # This method must be defined for deepcopy/pickling - # because this class is slotized. - # - def __setstate__(self, state): - self._dict = state - - def __getstate__(self): - return self._dict + if isinstance(iterable, OrderedSet): + self._dict.update(iterable._dict) + else: + self._dict.update((val, None) for val in iterable) # # Implement MutableSet abstract methods From d91ced077a8c3a7beb2f8cf7785e0e8e0f532a22 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 19 Feb 2024 15:38:40 -0500 Subject: [PATCH 0641/3044] Simplify `PathList.__call__` --- pyomo/common/config.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index f156bee79a9..09a1706ee5a 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -589,15 +589,11 @@ class PathList(Path): """ def __call__(self, data): - try: - pathlist = [super(PathList, self).__call__(data)] - except TypeError as err: - is_not_path_like = "expected str, bytes or os.PathLike" in str(err) - if is_not_path_like and hasattr(data, "__iter__"): - pathlist = [super(PathList, self).__call__(i) for i in data] - else: - raise - return pathlist + is_path_like = isinstance(data, (str, bytes)) or hasattr(data, "__fspath__") + if hasattr(data, "__iter__") and not is_path_like: + return [super(PathList, self).__call__(i) for i in data] + else: + return [super(PathList, self).__call__(data)] class DynamicImplicitDomain(object): From 4c0effdbf74263f9cb9cf6cba6708d537194775f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 14:00:23 -0700 Subject: [PATCH 0642/3044] Add overwrite flag --- .github/workflows/release_wheel_creation.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index ef44806d6d4..f978415e99e 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -45,6 +45,7 @@ jobs: with: name: native_wheels path: dist/*.whl + overwrite: true alternative_wheels: name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for aarch64 @@ -76,6 +77,7 @@ jobs: with: name: alt_wheels path: dist/*.whl + overwrite: true generictarball: name: ${{ matrix.TARGET }} @@ -106,4 +108,5 @@ jobs: with: name: generictarball path: dist + overwrite: true From 153e24920cfa77ffe418dfba3a77178c900dc8b3 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 14:07:05 -0700 Subject: [PATCH 0643/3044] Update action version --- .github/workflows/release_wheel_creation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index f978415e99e..19c2a6c50a9 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Build wheels - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v2 with: output-dir: dist env: @@ -63,7 +63,7 @@ jobs: with: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v2.16.2 + uses: pypa/cibuildwheel@v2 with: output-dir: dist env: From fcb7cef0f64fe3457f3b2e955bb41ba5c8831189 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 14:11:56 -0700 Subject: [PATCH 0644/3044] Update action version --- .github/workflows/release_wheel_creation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 19c2a6c50a9..2dd44652489 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Build wheels - uses: pypa/cibuildwheel@v2 + uses: pypa/cibuildwheel@v2.16.5 with: output-dir: dist env: @@ -63,7 +63,7 @@ jobs: with: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v2 + uses: pypa/cibuildwheel@v2.16.5 with: output-dir: dist env: From 217adeaec63873ec6ce945a240cd70d087587d98 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 14:15:35 -0700 Subject: [PATCH 0645/3044] NFC: apply black --- pyomo/common/tests/test_component_map.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/common/tests/test_component_map.py b/pyomo/common/tests/test_component_map.py index cc746642f28..9e771175d42 100644 --- a/pyomo/common/tests/test_component_map.py +++ b/pyomo/common/tests/test_component_map.py @@ -22,24 +22,24 @@ def test_tuple(self): m.c = Constraint(expr=m.v >= 0) m.cm = cm = ComponentMap() - cm[(1,2)] = 5 + cm[(1, 2)] = 5 self.assertEqual(len(cm), 1) - self.assertIn((1,2), cm) - self.assertEqual(cm[1,2], 5) + self.assertIn((1, 2), cm) + self.assertEqual(cm[1, 2], 5) - cm[(1,2)] = 50 + cm[(1, 2)] = 50 self.assertEqual(len(cm), 1) - self.assertIn((1,2), cm) - self.assertEqual(cm[1,2], 50) + self.assertIn((1, 2), cm) + self.assertEqual(cm[1, 2], 50) cm[(1, (2, m.v))] = 10 self.assertEqual(len(cm), 2) - self.assertIn((1,(2, m.v)), cm) + self.assertIn((1, (2, m.v)), cm) self.assertEqual(cm[1, (2, m.v)], 10) cm[(1, (2, m.v))] = 100 self.assertEqual(len(cm), 2) - self.assertIn((1,(2, m.v)), cm) + self.assertIn((1, (2, m.v)), cm) self.assertEqual(cm[1, (2, m.v)], 100) i = m.clone() From a80c20605e61f764ec2371683676ace2739f0a96 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 14:18:19 -0700 Subject: [PATCH 0646/3044] Change from overwrite to merge --- .github/workflows/release_wheel_creation.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 2dd44652489..203b4f391c7 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -45,7 +45,7 @@ jobs: with: name: native_wheels path: dist/*.whl - overwrite: true + merge-multiple: true alternative_wheels: name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for aarch64 @@ -77,7 +77,7 @@ jobs: with: name: alt_wheels path: dist/*.whl - overwrite: true + merge-multiple: true generictarball: name: ${{ matrix.TARGET }} @@ -108,5 +108,5 @@ jobs: with: name: generictarball path: dist - overwrite: true + merge-multiple: true From 88aab73d33d8cfaecef8337a3121f986285f6a2a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 14:24:49 -0700 Subject: [PATCH 0647/3044] Have to give unique names now. Yay. --- .github/workflows/release_wheel_creation.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 203b4f391c7..d847b0f2cff 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -43,9 +43,9 @@ jobs: CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' - uses: actions/upload-artifact@v4 with: - name: native_wheels + name: alt_wheels-${{ matrix.os }}-${{ matrix.wheel-version }} path: dist/*.whl - merge-multiple: true + overwrite: true alternative_wheels: name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for aarch64 @@ -75,9 +75,9 @@ jobs: CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' - uses: actions/upload-artifact@v4 with: - name: alt_wheels + name: alt_wheels-${{ matrix.os }}-${{ matrix.wheel-version }} path: dist/*.whl - merge-multiple: true + overwrite: true generictarball: name: ${{ matrix.TARGET }} @@ -108,5 +108,5 @@ jobs: with: name: generictarball path: dist - merge-multiple: true + overwrite: true From dd9cf09b290302e854eb032644cf9e0ed8ee9509 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 14:35:52 -0700 Subject: [PATCH 0648/3044] Add fail fast; target names --- .github/workflows/release_wheel_creation.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index d847b0f2cff..6b65938706c 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -22,10 +22,23 @@ jobs: name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for native and cross-compiled architecture runs-on: ${{ matrix.os }} strategy: + fail-fast: true matrix: os: [ubuntu-22.04, windows-latest, macos-latest] arch: [all] wheel-version: ['cp38*', 'cp39*', 'cp310*', 'cp311*', 'cp312*'] + + include: + - wheel-version: 'cp38*' + TARGET: 'py38' + - wheel-version: 'cp39*' + TARGET: 'py39' + - wheel-version: 'cp310*' + TARGET: 'py310' + - wheel-version: 'cp311*' + TARGET: 'py311' + - wheel-version: 'cp312*' + TARGET: 'py312' steps: - uses: actions/checkout@v4 - name: Build wheels @@ -43,7 +56,7 @@ jobs: CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' - uses: actions/upload-artifact@v4 with: - name: alt_wheels-${{ matrix.os }}-${{ matrix.wheel-version }} + name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} path: dist/*.whl overwrite: true @@ -75,7 +88,7 @@ jobs: CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' - uses: actions/upload-artifact@v4 with: - name: alt_wheels-${{ matrix.os }}-${{ matrix.wheel-version }} + name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} path: dist/*.whl overwrite: true From 8c643ca08867b646a7dea8964ff20061737ee1fb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 14:57:34 -0700 Subject: [PATCH 0649/3044] Copy-pasta failure --- .github/workflows/release_wheel_creation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 6b65938706c..17152dc3d1e 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -56,7 +56,7 @@ jobs: CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' - uses: actions/upload-artifact@v4 with: - name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} + name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} path: dist/*.whl overwrite: true From 5ad721cc20d8e2db73df7beb19d029cf1d11e46b Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 19 Feb 2024 15:27:24 -0700 Subject: [PATCH 0650/3044] updating results processing and tests --- pyomo/contrib/solver/ipopt.py | 147 +++++--- pyomo/contrib/solver/results.py | 4 + pyomo/contrib/solver/solution.py | 64 +++- .../solver/tests/solvers/test_solvers.py | 350 +++++++++++++++--- 4 files changed, 439 insertions(+), 126 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index fc009c77522..82d145d2e93 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -134,10 +134,20 @@ class IpoptSolutionLoader(SolSolutionLoader): def get_reduced_costs( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: + if self._nl_info is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + if len(self._nl_info.eliminated_vars) > 0: + raise NotImplementedError('For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get reduced costs.') + assert self._sol_data is not None if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.variables) + obj_scale = 1 else: scale_list = self._nl_info.scaling.variables + obj_scale = self._nl_info.scaling.objectives[0] sol_data = self._sol_data nl_info = self._nl_info zl_map = sol_data.var_suffixes['ipopt_zL_out'] @@ -148,11 +158,11 @@ def get_reduced_costs( v_id = id(v) rc[v_id] = (v, 0) if ndx in zl_map: - zl = zl_map[ndx] * scale + zl = zl_map[ndx] * scale / obj_scale if abs(zl) > abs(rc[v_id][1]): rc[v_id] = (v, zl) if ndx in zu_map: - zu = zu_map[ndx] * scale + zu = zu_map[ndx] * scale / obj_scale if abs(zu) > abs(rc[v_id][1]): rc[v_id] = (v, zu) @@ -353,68 +363,82 @@ def solve(self, model, **kwds): symbolic_solver_labels=config.symbolic_solver_labels, ) timer.stop('write_nl_file') - # Get a copy of the environment to pass to the subprocess - env = os.environ.copy() - if nl_info.external_function_libraries: - if env.get('AMPLFUNC'): - nl_info.external_function_libraries.append(env.get('AMPLFUNC')) - env['AMPLFUNC'] = "\n".join(nl_info.external_function_libraries) - # Write the opt_file, if there should be one; return a bool to say - # whether or not we have one (so we can correctly build the command line) - opt_file = self._write_options_file( - filename=basename, options=config.solver_options - ) - # Call ipopt - passing the files via the subprocess - cmd = self._create_command_line( - basename=basename, config=config, opt_file=opt_file - ) - # this seems silly, but we have to give the subprocess slightly longer to finish than - # ipopt - if config.time_limit is not None: - timeout = config.time_limit + min( - max(1.0, 0.01 * config.time_limit), 100 + if len(nl_info.variables) > 0: + # Get a copy of the environment to pass to the subprocess + env = os.environ.copy() + if nl_info.external_function_libraries: + if env.get('AMPLFUNC'): + nl_info.external_function_libraries.append(env.get('AMPLFUNC')) + env['AMPLFUNC'] = "\n".join(nl_info.external_function_libraries) + # Write the opt_file, if there should be one; return a bool to say + # whether or not we have one (so we can correctly build the command line) + opt_file = self._write_options_file( + filename=basename, options=config.solver_options ) - else: - timeout = None - - ostreams = [io.StringIO()] - if config.tee: - ostreams.append(sys.stdout) - if config.log_solver_output: - ostreams.append(LogStream(level=logging.INFO, logger=logger)) - with TeeStream(*ostreams) as t: - timer.start('subprocess') - process = subprocess.run( - cmd, - timeout=timeout, - env=env, - universal_newlines=True, - stdout=t.STDOUT, - stderr=t.STDERR, - ) - timer.stop('subprocess') - # This is the stuff we need to parse to get the iterations - # and time - iters, ipopt_time_nofunc, ipopt_time_func, ipopt_total_time = ( - self._parse_ipopt_output(ostreams[0]) + # Call ipopt - passing the files via the subprocess + cmd = self._create_command_line( + basename=basename, config=config, opt_file=opt_file ) + # this seems silly, but we have to give the subprocess slightly longer to finish than + # ipopt + if config.time_limit is not None: + timeout = config.time_limit + min( + max(1.0, 0.01 * config.time_limit), 100 + ) + else: + timeout = None + + ostreams = [io.StringIO()] + if config.tee: + ostreams.append(sys.stdout) + if config.log_solver_output: + ostreams.append(LogStream(level=logging.INFO, logger=logger)) + with TeeStream(*ostreams) as t: + timer.start('subprocess') + process = subprocess.run( + cmd, + timeout=timeout, + env=env, + universal_newlines=True, + stdout=t.STDOUT, + stderr=t.STDERR, + ) + timer.stop('subprocess') + # This is the stuff we need to parse to get the iterations + # and time + iters, ipopt_time_nofunc, ipopt_time_func, ipopt_total_time = ( + self._parse_ipopt_output(ostreams[0]) + ) - if os.path.isfile(basename + '.sol'): - with open(basename + '.sol', 'r') as sol_file: - timer.start('parse_sol') - results = self._parse_solution(sol_file, nl_info) - timer.stop('parse_sol') - else: - results = IpoptResults() - if process.returncode != 0: - results.extra_info.return_code = process.returncode - results.termination_condition = TerminationCondition.error - results.solution_loader = SolSolutionLoader(None, None) + if len(nl_info.variables) == 0: + if len(nl_info.eliminated_vars) == 0: + results = IpoptResults() + results.termination_condition = TerminationCondition.emptyModel + results.solution_loader = SolSolutionLoader(None, None) + else: + results = IpoptResults() + results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.solution_status = SolutionStatus.optimal + results.solution_loader = SolSolutionLoader(None, nl_info=nl_info) + results.iteration_count = 0 + results.timing_info.total_seconds = 0 else: - results.iteration_count = iters - results.timing_info.ipopt_excluding_nlp_functions = ipopt_time_nofunc - results.timing_info.nlp_function_evaluations = ipopt_time_func - results.timing_info.total_seconds = ipopt_total_time + if os.path.isfile(basename + '.sol'): + with open(basename + '.sol', 'r') as sol_file: + timer.start('parse_sol') + results = self._parse_solution(sol_file, nl_info) + timer.stop('parse_sol') + else: + results = IpoptResults() + if process.returncode != 0: + results.extra_info.return_code = process.returncode + results.termination_condition = TerminationCondition.error + results.solution_loader = SolSolutionLoader(None, None) + else: + results.iteration_count = iters + results.timing_info.ipopt_excluding_nlp_functions = ipopt_time_nofunc + results.timing_info.nlp_function_evaluations = ipopt_time_func + results.timing_info.total_seconds = ipopt_total_time if ( config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal @@ -470,7 +494,8 @@ def solve(self, model, **kwds): ) results.solver_configuration = config - results.solver_log = ostreams[0].getvalue() + if len(nl_info.variables) > 0: + results.solver_log = ostreams[0].getvalue() # Capture/record end-time / wall-time end_timestamp = datetime.datetime.now(datetime.timezone.utc) diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index f2c9cde64fe..88de0624629 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -73,6 +73,8 @@ class TerminationCondition(enum.Enum): license was found, the license is of the wrong type for the problem (e.g., problem is too big for type of license), or there was an issue contacting a licensing server. + emptyModel: 12 + The model being solved did not have any variables unknown: 42 All other unrecognized exit statuses fall in this category. """ @@ -101,6 +103,8 @@ class TerminationCondition(enum.Enum): licensingProblems = 11 + emptyModel = 12 + unknown = 42 diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 1812e21a596..5c971597789 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -14,6 +14,7 @@ from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData +from pyomo.core.expr import value from pyomo.common.collections import ComponentMap from pyomo.core.staleflag import StaleFlagManager from pyomo.contrib.solver.sol_reader import SolFileData @@ -142,29 +143,48 @@ def __init__(self, sol_data: SolFileData, nl_info: NLWriterInfo) -> None: def load_vars( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> NoReturn: - if self._nl_info.scaling: - for v, val, scale in zip( - self._nl_info.variables, self._sol_data.primals, self._nl_info.scaling - ): - v.set_value(val / scale, skip_validation=True) + if self._nl_info is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + if self._sol_data is None: + assert len(self._nl_info.variables) == 0 else: - for v, val in zip(self._nl_info.variables, self._sol_data.primals): - v.set_value(val, skip_validation=True) + if self._nl_info.scaling: + for v, val, scale in zip( + self._nl_info.variables, self._sol_data.primals, self._nl_info.scaling.variables + ): + v.set_value(val / scale, skip_validation=True) + else: + for v, val in zip(self._nl_info.variables, self._sol_data.primals): + v.set_value(val, skip_validation=True) + + for v, v_expr in self._nl_info.eliminated_vars: + v.value = value(v_expr) StaleFlagManager.mark_all_as_stale(delayed=True) def get_primals( self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None ) -> Mapping[_GeneralVarData, float]: - if self._nl_info.scaling is None: - scale_list = [1] * len(self._nl_info.variables) - else: - scale_list = self._nl_info.scaling.variables + if self._nl_info is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check the termination condition.' + ) val_map = dict() - for v, val, scale in zip( - self._nl_info.variables, self._sol_data.primals, scale_list - ): - val_map[id(v)] = val / scale + if self._sol_data is None: + assert len(self._nl_info.variables) == 0 + else: + if self._nl_info.scaling is None: + scale_list = [1] * len(self._nl_info.variables) + else: + scale_list = self._nl_info.scaling.variables + for v, val, scale in zip( + self._nl_info.variables, self._sol_data.primals, scale_list + ): + val_map[id(v)] = val / scale for v, v_expr in self._nl_info.eliminated_vars: val = replace_expressions(v_expr, substitution_map=val_map) @@ -184,18 +204,28 @@ def get_primals( def get_duals( self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None ) -> Dict[_GeneralConstraintData, float]: + if self._nl_info is None: + raise RuntimeError( + 'Solution loader does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + if len(self._nl_info.eliminated_vars) > 0: + raise NotImplementedError('For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get dual variable values.') + assert self._sol_data is not None + res = dict() if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.constraints) + obj_scale = 1 else: scale_list = self._nl_info.scaling.constraints + obj_scale = self._nl_info.scaling.objectives[0] if cons_to_load is None: cons_to_load = set(self._nl_info.constraints) else: cons_to_load = set(cons_to_load) - res = dict() for c, val, scale in zip( self._nl_info.constraints, self._sol_data.duals, scale_list ): if c in cons_to_load: - res[c] = val * scale + res[c] = val * scale / obj_scale return res diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 2b9e783ad16..7f916c21dd7 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -36,26 +36,43 @@ nlp_solvers = [('ipopt', Ipopt)] qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] miqcqp_solvers = [('gurobi', Gurobi)] +nl_solvers = [('ipopt', Ipopt)] +nl_solvers_set = {i[0] for i in nl_solvers} def _load_tests(solver_list): res = list() for solver_name, solver in solver_list: - test_name = f"{solver_name}" - res.append((test_name, solver)) + if solver_name in nl_solvers_set: + test_name = f"{solver_name}_presolve" + res.append((test_name, solver, True)) + test_name = f"{solver_name}" + res.append((test_name, solver, False)) + else: + test_name = f"{solver_name}" + res.append((test_name, solver, None)) return res @unittest.skipUnless(numpy_available, 'numpy is not available') class TestSolvers(unittest.TestCase): + @parameterized.expand(input=all_solvers) + def test_config_overwrite(self, name: str, opt_class: Type[SolverBase]): + self.assertIsNot(SolverBase.CONFIG, opt_class.CONFIG) + @parameterized.expand(input=_load_tests(all_solvers)) def test_remove_variable_and_objective( - self, name: str, opt_class: Type[SolverBase] + self, name: str, opt_class: Type[SolverBase], use_presolve ): # this test is for issue #2888 opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(2, None)) m.obj = pe.Objective(expr=m.x) @@ -72,10 +89,15 @@ def test_remove_variable_and_objective( self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_stale_vars(self, name: str, opt_class: Type[SolverBase]): + def test_stale_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -113,10 +135,15 @@ def test_stale_vars(self, name: str, opt_class: Type[SolverBase]): self.assertFalse(m.y.stale) @parameterized.expand(input=_load_tests(all_solvers)) - def test_range_constraint(self, name: str, opt_class: Type[SolverBase]): + def test_range_constraint(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.obj = pe.Objective(expr=m.x) @@ -134,10 +161,15 @@ def test_range_constraint(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(duals[m.c], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_reduced_costs(self, name: str, opt_class: Type[SolverBase]): + def test_reduced_costs(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, 1)) m.y = pe.Var(bounds=(-2, 2)) @@ -156,10 +188,15 @@ def test_reduced_costs(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(rc[m.y], -4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase]): + def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, 1)) m.obj = pe.Objective(expr=m.x) @@ -176,10 +213,15 @@ def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(rc[m.x], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_param_changes(self, name: str, opt_class: Type[SolverBase]): + def test_param_changes(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -212,7 +254,7 @@ def test_param_changes(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_immutable_param(self, name: str, opt_class: Type[SolverBase]): + def test_immutable_param(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): """ This test is important because component_data_objects returns immutable params as floats. We want to make sure we process these correctly. @@ -220,6 +262,11 @@ def test_immutable_param(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -252,12 +299,17 @@ def test_immutable_param(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_equality(self, name: str, opt_class: Type[SolverBase]): + def test_equality(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') - if isinstance(opt, Ipopt): - opt.config.writer_config.linear_presolve = False + check_duals = True + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + check_duals = False + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -285,15 +337,21 @@ def test_equality(self, name: str, opt_class: Type[SolverBase]): else: bound = res.objective_bound self.assertTrue(bound <= m.y.value) - duals = res.solution_loader.get_duals() - self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) + if check_duals: + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_linear_expression(self, name: str, opt_class: Type[SolverBase]): + def test_linear_expression(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -328,10 +386,17 @@ def test_linear_expression(self, name: str, opt_class: Type[SolverBase]): self.assertTrue(bound <= m.y.value) @parameterized.expand(input=_load_tests(all_solvers)) - def test_no_objective(self, name: str, opt_class: Type[SolverBase]): + def test_no_objective(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + check_duals = True + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + check_duals = False + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -354,15 +419,21 @@ def test_no_objective(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertEqual(res.incumbent_objective, None) self.assertEqual(res.objective_bound, None) - duals = res.solution_loader.get_duals() - self.assertAlmostEqual(duals[m.c1], 0) - self.assertAlmostEqual(duals[m.c2], 0) + if check_duals: + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], 0) + self.assertAlmostEqual(duals[m.c2], 0) @parameterized.expand(input=_load_tests(all_solvers)) - def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase]): + def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -413,10 +484,15 @@ def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_results_infeasible(self, name: str, opt_class: Type[SolverBase]): + def test_results_infeasible(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -462,10 +538,15 @@ def test_results_infeasible(self, name: str, opt_class: Type[SolverBase]): res.solution_loader.get_reduced_costs() @parameterized.expand(input=_load_tests(all_solvers)) - def test_duals(self, name: str, opt_class: Type[SolverBase]): + def test_duals(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -486,11 +567,16 @@ def test_duals(self, name: str, opt_class: Type[SolverBase]): @parameterized.expand(input=_load_tests(qcp_solvers)) def test_mutable_quadratic_coefficient( - self, name: str, opt_class: Type[SolverBase] + self, name: str, opt_class: Type[SolverBase], use_presolve: bool ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -509,10 +595,15 @@ def test_mutable_quadratic_coefficient( self.assertAlmostEqual(m.y.value, 0.0869525991355825, 4) @parameterized.expand(input=_load_tests(qcp_solvers)) - def test_mutable_quadratic_objective(self, name: str, opt_class: Type[SolverBase]): + def test_mutable_quadratic_objective(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -534,7 +625,7 @@ def test_mutable_quadratic_objective(self, name: str, opt_class: Type[SolverBase self.assertAlmostEqual(m.y.value, 0.09227926676152151, 4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars(self, name: str, opt_class: Type[SolverBase]): + def test_fixed_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): for treat_fixed_vars_as_params in [True, False]: opt: SolverBase = opt_class() if opt.is_persistent(): @@ -543,6 +634,11 @@ def test_fixed_vars(self, name: str, opt_class: Type[SolverBase]): ) if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.x.fix(0) @@ -575,12 +671,17 @@ def test_fixed_vars(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars_2(self, name: str, opt_class: Type[SolverBase]): + def test_fixed_vars_2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.x.fix(0) @@ -613,12 +714,17 @@ def test_fixed_vars_2(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars_3(self, name: str, opt_class: Type[SolverBase]): + def test_fixed_vars_3(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -626,15 +732,21 @@ def test_fixed_vars_3(self, name: str, opt_class: Type[SolverBase]): m.c1 = pe.Constraint(expr=m.x == 2 / m.y) m.y.fix(1) res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 3) self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(nlp_solvers)) - def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase]): + def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -649,10 +761,15 @@ def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, 2**0.5) @parameterized.expand(input=_load_tests(all_solvers)) - def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase]): + def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False try: import numpy as np except: @@ -743,10 +860,15 @@ def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @parameterized.expand(input=_load_tests(all_solvers)) - def test_add_and_remove_vars(self, name: str, opt_class: Type[SolverBase]): + def test_add_and_remove_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.y = pe.Var(bounds=(-1, None)) m.obj = pe.Objective(expr=m.y) @@ -789,10 +911,15 @@ def test_add_and_remove_vars(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, -1) @parameterized.expand(input=_load_tests(nlp_solvers)) - def test_exp(self, name: str, opt_class: Type[SolverBase]): + def test_exp(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -803,10 +930,15 @@ def test_exp(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, 0.6529186341994245) @parameterized.expand(input=_load_tests(nlp_solvers)) - def test_log(self, name: str, opt_class: Type[SolverBase]): + def test_log(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(initialize=1) m.y = pe.Var() @@ -817,10 +949,15 @@ def test_log(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, -0.42630274815985264) @parameterized.expand(input=_load_tests(all_solvers)) - def test_with_numpy(self, name: str, opt_class: Type[SolverBase]): + def test_with_numpy(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -845,10 +982,15 @@ def test_with_numpy(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase]): + def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.y = pe.Var() m.p = pe.Param(mutable=True) @@ -877,10 +1019,15 @@ def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.y.value, 3) @parameterized.expand(input=_load_tests(all_solvers)) - def test_solution_loader(self, name: str, opt_class: Type[SolverBase]): + def test_solution_loader(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(1, None)) m.y = pe.Var() @@ -927,10 +1074,15 @@ def test_solution_loader(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(duals[m.c1], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_time_limit(self, name: str, opt_class: Type[SolverBase]): + def test_time_limit(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False from sys import platform if platform == 'win32': @@ -983,10 +1135,15 @@ def test_time_limit(self, name: str, opt_class: Type[SolverBase]): ) @parameterized.expand(input=_load_tests(all_solvers)) - def test_objective_changes(self, name: str, opt_class: Type[SolverBase]): + def test_objective_changes(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -1047,10 +1204,15 @@ def test_objective_changes(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(res.incumbent_objective, 4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_domain(self, name: str, opt_class: Type[SolverBase]): + def test_domain(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(1, None), domain=pe.NonNegativeReals) m.obj = pe.Objective(expr=m.x) @@ -1071,10 +1233,15 @@ def test_domain(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(res.incumbent_objective, 0) @parameterized.expand(input=_load_tests(mip_solvers)) - def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase]): + def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(-1, None), domain=pe.NonNegativeIntegers) m.obj = pe.Objective(expr=m.x) @@ -1095,10 +1262,15 @@ def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(res.incumbent_objective, 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_binaries(self, name: str, opt_class: Type[SolverBase]): + def test_fixed_binaries(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(domain=pe.Binary) m.y = pe.Var() @@ -1122,10 +1294,15 @@ def test_fixed_binaries(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(res.incumbent_objective, 1) @parameterized.expand(input=_load_tests(mip_solvers)) - def test_with_gdp(self, name: str, opt_class: Type[SolverBase]): + def test_with_gdp(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(-10, 10)) @@ -1152,11 +1329,16 @@ def test_with_gdp(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) - @parameterized.expand(input=all_solvers) - def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase]): + @parameterized.expand(input=_load_tests(all_solvers)) + def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() @@ -1179,11 +1361,16 @@ def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 2) - @parameterized.expand(input=all_solvers) - def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase]): + @parameterized.expand(input=_load_tests(all_solvers)) + def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var() @@ -1215,10 +1402,15 @@ def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase]): self.assertNotIn(m.z, sol) @parameterized.expand(input=_load_tests(all_solvers)) - def test_bug_1(self, name: str, opt_class: Type[SolverBase]): + def test_bug_1(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False m = pe.ConcreteModel() m.x = pe.Var(bounds=(3, 7)) @@ -1238,7 +1430,7 @@ def test_bug_1(self, name: str, opt_class: Type[SolverBase]): self.assertAlmostEqual(res.incumbent_objective, 3) @parameterized.expand(input=_load_tests(all_solvers)) - def test_bug_2(self, name: str, opt_class: Type[SolverBase]): + def test_bug_2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): """ This test is for a bug where an objective containing a fixed variable does not get updated properly when the variable is unfixed. @@ -1247,6 +1439,11 @@ def test_bug_2(self, name: str, opt_class: Type[SolverBase]): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = fixed_var_option @@ -1266,6 +1463,63 @@ def test_bug_2(self, name: str, opt_class: Type[SolverBase]): res = opt.solve(m) self.assertAlmostEqual(res.incumbent_objective, -18, 5) + @parameterized.expand(input=_load_tests(all_solvers)) + def test_scaling(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + check_duals = True + if any(name.startswith(i) for i in nl_solvers_set): + if use_presolve: + check_duals = False + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= (m.x - 1) + 1) + m.c2 = pe.Constraint(expr=m.y >= -(m.x - 1) + 1) + m.scaling_factor = pe.Suffix(direction=pe.Suffix.EXPORT) + m.scaling_factor[m.x] = 0.5 + m.scaling_factor[m.y] = 2 + m.scaling_factor[m.c1] = 0.5 + m.scaling_factor[m.c2] = 2 + m.scaling_factor[m.obj] = 2 + + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 1) + self.assertAlmostEqual(m.x.value, 1) + self.assertAlmostEqual(m.y.value, 1) + primals = res.solution_loader.get_primals() + self.assertAlmostEqual(primals[m.x], 1) + self.assertAlmostEqual(primals[m.y], 1) + if check_duals: + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -0.5) + self.assertAlmostEqual(duals[m.c2], -0.5) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 0) + self.assertAlmostEqual(rc[m.y], 0) + + m.x.setlb(2) + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2) + self.assertAlmostEqual(m.x.value, 2) + self.assertAlmostEqual(m.y.value, 2) + primals = res.solution_loader.get_primals() + self.assertAlmostEqual(primals[m.x], 2) + self.assertAlmostEqual(primals[m.y], 2) + if check_duals: + duals = res.solution_loader.get_duals() + self.assertAlmostEqual(duals[m.c1], -1) + self.assertAlmostEqual(duals[m.c2], 0) + rc = res.solution_loader.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) + self.assertAlmostEqual(rc[m.y], 0) + class TestLegacySolverInterface(unittest.TestCase): @parameterized.expand(input=all_solvers) From 2368ab94fc12d38eff15277ce4acd97c471cbd81 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 15:37:41 -0700 Subject: [PATCH 0651/3044] Work around strange deepcopy bug --- pyomo/common/collections/component_map.py | 2 +- pyomo/common/collections/component_set.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index 0851ffad301..90d985990d1 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -16,7 +16,7 @@ def _rehash_keys(encode, val): if encode: - return list(val.values()) + return tuple(val.values()) else: # object id() may have changed after unpickling, # so we rebuild the dictionary keys diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index f1fe7bc8cd6..bad40e90195 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -18,7 +18,17 @@ def _rehash_keys(encode, val): if encode: - return list(val.values()) + # TBD [JDS 2/2024]: if we + # + # return list(val.values()) + # + # here, then we get a strange failure when deepcopying + # ComponentSets containing an _ImplicitAny domain. We could + # track it down to teh implementation of + # autoslots.fast_deepcopy, but couldn't find an obvious bug. + # There is no error if we just return the original dict, or if + # we return a tuple(val.values) + return tuple(val.values()) else: # object id() may have changed after unpickling, # so we rebuild the dictionary keys From 0b6fd076de3a4b7bb48d924d3bce23f6913d689a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 15:41:04 -0700 Subject: [PATCH 0652/3044] NFC: fix spelling --- pyomo/common/autoslots.py | 2 +- pyomo/common/collections/component_set.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/common/autoslots.py b/pyomo/common/autoslots.py index cb79d4a0338..89fefaf4f21 100644 --- a/pyomo/common/autoslots.py +++ b/pyomo/common/autoslots.py @@ -29,7 +29,7 @@ def _deepcopy_tuple(obj, memo, _id): unchanged = False if unchanged: # Python does not duplicate "unchanged" tuples (i.e. allows the - # original objecct to be returned from deepcopy()). We will + # original object to be returned from deepcopy()). We will # preserve that behavior here. # # It also appears to be faster *not* to cache the fact that this diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index bad40e90195..d99dd694b64 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -24,7 +24,7 @@ def _rehash_keys(encode, val): # # here, then we get a strange failure when deepcopying # ComponentSets containing an _ImplicitAny domain. We could - # track it down to teh implementation of + # track it down to the implementation of # autoslots.fast_deepcopy, but couldn't find an obvious bug. # There is no error if we just return the original dict, or if # we return a tuple(val.values) From b9a6e8341da751c3b1ff6f834cfb110d8c5049d8 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 19 Feb 2024 15:46:45 -0700 Subject: [PATCH 0653/3044] error message --- pyomo/contrib/solver/solution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 5c971597789..e4734bd8a46 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -211,7 +211,7 @@ def get_duals( ) if len(self._nl_info.eliminated_vars) > 0: raise NotImplementedError('For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get dual variable values.') - assert self._sol_data is not None + assert self._sol_data is not None, "report this to the Pyomo developers" res = dict() if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.constraints) From 52391fc198bc4adc80b13ac8676f2cda9b1755d9 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 15:52:23 -0700 Subject: [PATCH 0654/3044] Missed capitalization --- pyomo/contrib/solver/ipopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 074e5b19c5c..3ec69879675 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -47,7 +47,7 @@ logger = logging.getLogger(__name__) -class ipoptSolverError(PyomoException): +class IpoptSolverError(PyomoException): """ General exception to catch solver system errors """ @@ -315,7 +315,7 @@ def solve(self, model, **kwds): # Check if solver is available avail = self.available(config) if not avail: - raise ipoptSolverError( + raise IpoptSolverError( f'Solver {self.__class__} is not available ({avail}).' ) if config.threads: From bc1b3e9cec2266b7383627601511ac541212ffd4 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 15:54:37 -0700 Subject: [PATCH 0655/3044] Apply black to updates --- pyomo/contrib/solver/ipopt.py | 12 +- pyomo/contrib/solver/solution.py | 8 +- .../solver/tests/solvers/test_solvers.py | 104 +++++++++++++----- 3 files changed, 93 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 876ca749921..537e6f85968 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -140,7 +140,9 @@ def get_reduced_costs( 'check the termination condition.' ) if len(self._nl_info.eliminated_vars) > 0: - raise NotImplementedError('For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get reduced costs.') + raise NotImplementedError( + 'For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get reduced costs.' + ) assert self._sol_data is not None if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.variables) @@ -417,7 +419,9 @@ def solve(self, model, **kwds): results.solution_loader = SolSolutionLoader(None, None) else: results = IpoptResults() - results.termination_condition = TerminationCondition.convergenceCriteriaSatisfied + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) results.solution_status = SolutionStatus.optimal results.solution_loader = SolSolutionLoader(None, nl_info=nl_info) results.iteration_count = 0 @@ -436,7 +440,9 @@ def solve(self, model, **kwds): results.solution_loader = SolSolutionLoader(None, None) else: results.iteration_count = iters - results.timing_info.ipopt_excluding_nlp_functions = ipopt_time_nofunc + results.timing_info.ipopt_excluding_nlp_functions = ( + ipopt_time_nofunc + ) results.timing_info.nlp_function_evaluations = ipopt_time_func results.timing_info.total_seconds = ipopt_total_time if ( diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index e4734bd8a46..7cef86a4e8f 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -153,7 +153,9 @@ def load_vars( else: if self._nl_info.scaling: for v, val, scale in zip( - self._nl_info.variables, self._sol_data.primals, self._nl_info.scaling.variables + self._nl_info.variables, + self._sol_data.primals, + self._nl_info.scaling.variables, ): v.set_value(val / scale, skip_validation=True) else: @@ -210,7 +212,9 @@ def get_duals( 'check the termination condition.' ) if len(self._nl_info.eliminated_vars) > 0: - raise NotImplementedError('For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get dual variable values.') + raise NotImplementedError( + 'For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get dual variable values.' + ) assert self._sol_data is not None, "report this to the Pyomo developers" res = dict() if self._nl_info.scaling is None: diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 7f916c21dd7..c6c73ea2dc7 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -89,7 +89,9 @@ def test_remove_variable_and_objective( self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_stale_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_stale_vars( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -135,7 +137,9 @@ def test_stale_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: self.assertFalse(m.y.stale) @parameterized.expand(input=_load_tests(all_solvers)) - def test_range_constraint(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_range_constraint( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -161,7 +165,9 @@ def test_range_constraint(self, name: str, opt_class: Type[SolverBase], use_pres self.assertAlmostEqual(duals[m.c], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_reduced_costs(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_reduced_costs( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -188,7 +194,9 @@ def test_reduced_costs(self, name: str, opt_class: Type[SolverBase], use_presolv self.assertAlmostEqual(rc[m.y], -4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_reduced_costs2( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -213,7 +221,9 @@ def test_reduced_costs2(self, name: str, opt_class: Type[SolverBase], use_presol self.assertAlmostEqual(rc[m.x], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_param_changes(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_param_changes( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -254,7 +264,9 @@ def test_param_changes(self, name: str, opt_class: Type[SolverBase], use_presolv self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_immutable_param(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_immutable_param( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): """ This test is important because component_data_objects returns immutable params as floats. We want to make sure we process these correctly. @@ -343,7 +355,9 @@ def test_equality(self, name: str, opt_class: Type[SolverBase], use_presolve: bo self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_linear_expression(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_linear_expression( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -386,7 +400,9 @@ def test_linear_expression(self, name: str, opt_class: Type[SolverBase], use_pre self.assertTrue(bound <= m.y.value) @parameterized.expand(input=_load_tests(all_solvers)) - def test_no_objective(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_no_objective( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -425,7 +441,9 @@ def test_no_objective(self, name: str, opt_class: Type[SolverBase], use_presolve self.assertAlmostEqual(duals[m.c2], 0) @parameterized.expand(input=_load_tests(all_solvers)) - def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_add_remove_cons( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -484,7 +502,9 @@ def test_add_remove_cons(self, name: str, opt_class: Type[SolverBase], use_preso self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers)) - def test_results_infeasible(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_results_infeasible( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -595,7 +615,9 @@ def test_mutable_quadratic_coefficient( self.assertAlmostEqual(m.y.value, 0.0869525991355825, 4) @parameterized.expand(input=_load_tests(qcp_solvers)) - def test_mutable_quadratic_objective(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_mutable_quadratic_objective( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -625,7 +647,9 @@ def test_mutable_quadratic_objective(self, name: str, opt_class: Type[SolverBase self.assertAlmostEqual(m.y.value, 0.09227926676152151, 4) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_fixed_vars( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): for treat_fixed_vars_as_params in [True, False]: opt: SolverBase = opt_class() if opt.is_persistent(): @@ -671,7 +695,9 @@ def test_fixed_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars_2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_fixed_vars_2( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True @@ -714,7 +740,9 @@ def test_fixed_vars_2(self, name: str, opt_class: Type[SolverBase], use_presolve self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_vars_3(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_fixed_vars_3( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True @@ -736,7 +764,9 @@ def test_fixed_vars_3(self, name: str, opt_class: Type[SolverBase], use_presolve self.assertAlmostEqual(m.x.value, 2) @parameterized.expand(input=_load_tests(nlp_solvers)) - def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_fixed_vars_4( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if opt.is_persistent(): opt.config.auto_updates.treat_fixed_vars_as_params = True @@ -761,7 +791,9 @@ def test_fixed_vars_4(self, name: str, opt_class: Type[SolverBase], use_presolve self.assertAlmostEqual(m.y.value, 2**0.5) @parameterized.expand(input=_load_tests(all_solvers)) - def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_mutable_param_with_range( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -860,7 +892,9 @@ def test_mutable_param_with_range(self, name: str, opt_class: Type[SolverBase], self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @parameterized.expand(input=_load_tests(all_solvers)) - def test_add_and_remove_vars(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_add_and_remove_vars( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -949,7 +983,9 @@ def test_log(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): self.assertAlmostEqual(m.y.value, -0.42630274815985264) @parameterized.expand(input=_load_tests(all_solvers)) - def test_with_numpy(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_with_numpy( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -982,7 +1018,9 @@ def test_with_numpy(self, name: str, opt_class: Type[SolverBase], use_presolve: self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_bounds_with_params( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -1019,7 +1057,9 @@ def test_bounds_with_params(self, name: str, opt_class: Type[SolverBase], use_pr self.assertAlmostEqual(m.y.value, 3) @parameterized.expand(input=_load_tests(all_solvers)) - def test_solution_loader(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_solution_loader( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -1074,7 +1114,9 @@ def test_solution_loader(self, name: str, opt_class: Type[SolverBase], use_preso self.assertAlmostEqual(duals[m.c1], 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_time_limit(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_time_limit( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -1135,7 +1177,9 @@ def test_time_limit(self, name: str, opt_class: Type[SolverBase], use_presolve: ) @parameterized.expand(input=_load_tests(all_solvers)) - def test_objective_changes(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_objective_changes( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -1233,7 +1277,9 @@ def test_domain(self, name: str, opt_class: Type[SolverBase], use_presolve: bool self.assertAlmostEqual(res.incumbent_objective, 0) @parameterized.expand(input=_load_tests(mip_solvers)) - def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_domain_with_integers( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -1262,7 +1308,9 @@ def test_domain_with_integers(self, name: str, opt_class: Type[SolverBase], use_ self.assertAlmostEqual(res.incumbent_objective, 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_fixed_binaries(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_fixed_binaries( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -1330,7 +1378,9 @@ def test_with_gdp(self, name: str, opt_class: Type[SolverBase], use_presolve: bo self.assertAlmostEqual(m.y.value, 1) @parameterized.expand(input=_load_tests(all_solvers)) - def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_variables_elsewhere( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') @@ -1362,7 +1412,9 @@ def test_variables_elsewhere(self, name: str, opt_class: Type[SolverBase], use_p self.assertAlmostEqual(m.y.value, 2) @parameterized.expand(input=_load_tests(all_solvers)) - def test_variables_elsewhere2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_variables_elsewhere2( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') From ac7ce8b2bfbd06a1631f3f25a96ac0a48a4e7571 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 16:01:18 -0700 Subject: [PATCH 0656/3044] Update error messages --- pyomo/contrib/solver/solution.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 7cef86a4e8f..32e84d2abca 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -16,6 +16,7 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.core.expr import value from pyomo.common.collections import ComponentMap +from pyomo.common.errors import DeveloperError from pyomo.core.staleflag import StaleFlagManager from pyomo.contrib.solver.sol_reader import SolFileData from pyomo.repn.plugins.nl_writer import NLWriterInfo @@ -146,7 +147,7 @@ def load_vars( if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' - 'check the termination condition.' + 'check results.TerminationCondition and/or results.SolutionStatus.' ) if self._sol_data is None: assert len(self._nl_info.variables) == 0 @@ -173,7 +174,7 @@ def get_primals( if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' - 'check the termination condition.' + 'check results.TerminationCondition and/or results.SolutionStatus.' ) val_map = dict() if self._sol_data is None: @@ -209,13 +210,18 @@ def get_duals( if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' - 'check the termination condition.' + 'check results.TerminationCondition and/or results.SolutionStatus.' ) if len(self._nl_info.eliminated_vars) > 0: raise NotImplementedError( - 'For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get dual variable values.' + 'For now, turn presolve off (opt.config.writer_config.linear_presolve=False) ' + 'to get dual variable values.' + ) + if self._sol_data is None: + raise DeveloperError( + "Solution data is empty. This should not " + "have happened. Report this error to the Pyomo Developers." ) - assert self._sol_data is not None, "report this to the Pyomo developers" res = dict() if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.constraints) From 8f581df56f057101bf7fa41eb76c1ed87dd6b5e2 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 19 Feb 2024 16:17:42 -0700 Subject: [PATCH 0657/3044] Added __init__.py to parmest deprecated folder. --- pyomo/contrib/parmest/deprecated/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 pyomo/contrib/parmest/deprecated/__init__.py diff --git a/pyomo/contrib/parmest/deprecated/__init__.py b/pyomo/contrib/parmest/deprecated/__init__.py new file mode 100644 index 00000000000..d93cfd77b3c --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ From 48533dbc566525deb1c6f20acb93bdd394d8e162 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 16:18:10 -0700 Subject: [PATCH 0658/3044] NFC: update copyright --- pyomo/common/tests/test_component_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/tests/test_component_map.py b/pyomo/common/tests/test_component_map.py index 9e771175d42..b9e2a953047 100644 --- a/pyomo/common/tests/test_component_map.py +++ b/pyomo/common/tests/test_component_map.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain From 030aa576776959a0b52ad40ef3f04cad38b73b55 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 19 Feb 2024 16:32:11 -0700 Subject: [PATCH 0659/3044] More parmest black formatting. --- .../reactor_design/timeseries_data_example.py | 2 +- pyomo/contrib/parmest/parmest.py | 1 + pyomo/contrib/parmest/tests/test_parmest.py | 20 +++++-------------- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py index 85095fb94de..1e457bf1e89 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py @@ -23,7 +23,7 @@ class TimeSeriesReactorDesignExperiment(ReactorDesignExperiment): def __init__(self, data, experiment_number): self.data = data self.experiment_number = experiment_number - data_i = data.loc[data['experiment'] == experiment_number,:] + data_i = data.loc[data['experiment'] == experiment_number, :] self.data_i = data_i.reset_index() self.model = None diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ed9bda232b4..90d42e68910 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -271,6 +271,7 @@ def _experiment_instance_creation_callback( # return m + def SSE(model): expr = sum((y - yhat) ** 2 for y, yhat in model.experiment_outputs.items()) return expr diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 0893c9b4fde..15264a18989 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -76,9 +76,7 @@ def SSE(model): # Create an experiment list exp_list = [] for i in range(data.shape[0]): - exp_list.append( - RooneyBieglerExperiment(data.loc[i, :]) - ) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) # Create an instance of the parmest estimator pest = parmest.Estimator(exp_list, obj_function=SSE) @@ -392,9 +390,7 @@ def create_model(self): rooney_biegler_params_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_params_exp_list.append( - RooneyBieglerExperimentParams( - self.data.loc[i, :] - ) + RooneyBieglerExperimentParams(self.data.loc[i, :]) ) def rooney_biegler_indexed_params(data): @@ -440,9 +436,7 @@ def label_model(self): rooney_biegler_indexed_params_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_indexed_params_exp_list.append( - RooneyBieglerExperimentIndexedParams( - self.data.loc[i, :] - ) + RooneyBieglerExperimentIndexedParams(self.data.loc[i, :]) ) def rooney_biegler_vars(data): @@ -521,9 +515,7 @@ def label_model(self): rooney_biegler_indexed_vars_exp_list = [] for i in range(self.data.shape[0]): rooney_biegler_indexed_vars_exp_list.append( - RooneyBieglerExperimentIndexedVars( - self.data.loc[i, :] - ) + RooneyBieglerExperimentIndexedVars(self.data.loc[i, :]) ) # Sum of squared error function @@ -988,9 +980,7 @@ def SSE(model): exp_list = [] for i in range(data.shape[0]): - exp_list.append( - RooneyBieglerExperiment(data.loc[i, :]) - ) + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) solver_options = {"tol": 1e-8} From 3828841bf5e327f69c5369acd9ce089e28a81751 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 19 Feb 2024 16:33:21 -0700 Subject: [PATCH 0660/3044] Forgot targets for alt_wheels --- .github/workflows/release_wheel_creation.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 17152dc3d1e..932b0d8eea6 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -68,6 +68,18 @@ jobs: os: [ubuntu-22.04] arch: [all] wheel-version: ['cp38*', 'cp39*', 'cp310*', 'cp311*', 'cp312*'] + + include: + - wheel-version: 'cp38*' + TARGET: 'py38' + - wheel-version: 'cp39*' + TARGET: 'py39' + - wheel-version: 'cp310*' + TARGET: 'py310' + - wheel-version: 'cp311*' + TARGET: 'py311' + - wheel-version: 'cp312*' + TARGET: 'py312' steps: - uses: actions/checkout@v4 - name: Set up QEMU From e9a99499d1b5b4184a2ada6accd160f67b4c7cd5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 16:40:52 -0700 Subject: [PATCH 0661/3044] Add DefaultComponentMap --- pyomo/common/collections/__init__.py | 2 +- pyomo/common/collections/component_map.py | 29 +++++++++++++++ pyomo/common/tests/test_component_map.py | 44 +++++++++++++++++++++-- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/pyomo/common/collections/__init__.py b/pyomo/common/collections/__init__.py index 93785124e3c..717caf87b2c 100644 --- a/pyomo/common/collections/__init__.py +++ b/pyomo/common/collections/__init__.py @@ -14,6 +14,6 @@ from collections import UserDict from .orderedset import OrderedDict, OrderedSet -from .component_map import ComponentMap +from .component_map import ComponentMap, DefaultComponentMap from .component_set import ComponentSet from .bunch import Bunch diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index 90d985990d1..be44bd6ff68 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -179,3 +179,32 @@ def setdefault(self, key, default=None): else: self[key] = default return default + + +class DefaultComponentMap(ComponentMap): + """A :py:class:`defaultdict` admitting Pyomo Components as keys + + This class is a replacement for defaultdict that allows Pyomo + modeling components to be used as entry keys. The base + implementation builds on :py:class:`ComponentMap`. + + """ + + __slots__ = ('default_factory',) + + def __init__(self, default_factory=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.default_factory = default_factory + + def __missing__(self, key): + if self.default_factory is None: + raise KeyError(key) + self[key] = ans = self.default_factory() + return ans + + def __getitem__(self, obj): + _key = _hasher[obj.__class__](obj) + if _key in self._dict: + return self._dict[_key][1] + else: + return self.__missing__(obj) diff --git a/pyomo/common/tests/test_component_map.py b/pyomo/common/tests/test_component_map.py index b9e2a953047..7cd4ec2c458 100644 --- a/pyomo/common/tests/test_component_map.py +++ b/pyomo/common/tests/test_component_map.py @@ -11,8 +11,8 @@ import pyomo.common.unittest as unittest -from pyomo.common.collections import ComponentMap -from pyomo.environ import ConcreteModel, Var, Constraint +from pyomo.common.collections import ComponentMap, ComponentSet, DefaultComponentMap +from pyomo.environ import ConcreteModel, Block, Var, Constraint class TestComponentMap(unittest.TestCase): @@ -48,3 +48,43 @@ def test_tuple(self): self.assertNotIn((1, (2, i.v)), m.cm) self.assertIn((1, (2, m.v)), m.cm) self.assertNotIn((1, (2, m.v)), i.cm) + + +class TestDefaultComponentMap(unittest.TestCase): + def test_default_component_map(self): + dcm = DefaultComponentMap(ComponentSet) + + m = ConcreteModel() + m.x = Var() + m.b = Block() + m.b.y = Var() + + self.assertEqual(len(dcm), 0) + + dcm[m.x].add(m) + self.assertEqual(len(dcm), 1) + self.assertIn(m.x, dcm) + self.assertIn(m, dcm[m.x]) + + dcm[m.b.y].add(m.b) + self.assertEqual(len(dcm), 2) + self.assertIn(m.b.y, dcm) + self.assertNotIn(m, dcm[m.b.y]) + self.assertIn(m.b, dcm[m.b.y]) + + dcm[m.b.y].add(m) + self.assertEqual(len(dcm), 2) + self.assertIn(m.b.y, dcm) + self.assertIn(m, dcm[m.b.y]) + self.assertIn(m.b, dcm[m.b.y]) + + def test_no_default_factory(self): + dcm = DefaultComponentMap() + + dcm['found'] = 5 + self.assertEqual(len(dcm), 1) + self.assertIn('found', dcm) + self.assertEqual(dcm['found'], 5) + + with self.assertRaisesRegex(KeyError, "'missing'"): + dcm["missing"] From cf6364cc870aa2050a49a9baba7687b1ceb45742 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Feb 2024 17:42:55 -0700 Subject: [PATCH 0662/3044] Additional attempt to resolve ComponentMap deepcopy --- pyomo/common/collections/component_map.py | 4 ++-- pyomo/common/collections/component_set.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index be44bd6ff68..c110e9f390b 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -16,11 +16,11 @@ def _rehash_keys(encode, val): if encode: - return tuple(val.values()) + return val else: # object id() may have changed after unpickling, # so we rebuild the dictionary keys - return {_hasher[obj.__class__](obj): (obj, v) for obj, v in val} + return {_hasher[obj.__class__](obj): (obj, v) for obj, v in val.values()} class _Hasher(collections.defaultdict): diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index d99dd694b64..19d2ef2f7f9 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -28,11 +28,11 @@ def _rehash_keys(encode, val): # autoslots.fast_deepcopy, but couldn't find an obvious bug. # There is no error if we just return the original dict, or if # we return a tuple(val.values) - return tuple(val.values()) + return val else: # object id() may have changed after unpickling, # so we rebuild the dictionary keys - return {_hasher[obj.__class__](obj): obj for obj in val} + return {_hasher[obj.__class__](obj): obj for obj in val.values()} class ComponentSet(AutoSlots.Mixin, collections_MutableSet): From 3f787a3f5a2fb772caf11468f267fa2968190ca7 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 20 Feb 2024 06:52:47 -0700 Subject: [PATCH 0663/3044] Added exp1.out to exp14.out by force for deprecated semibatch examples in parmest. --- pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out | 1 + pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out | 1 + 14 files changed, 14 insertions(+) create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out create mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out new file mode 100644 index 00000000000..f1d826085bf --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out @@ -0,0 +1 @@ +{"experiment": 1, "Ca0": 0, "Ca_meas": {"17280.0": 1.0206137429621787, "0.0": 0.3179205033819153, "7560.0": 6.1190611079452015, "9720.0": 5.143125125521654, "19440.0": 0.6280402056097951, "16200.0": 0.8579867036528984, "11880.0": 3.7282923165210042, "2160.0": 4.63887641485678, "8640.0": 5.343033989137008, "14040.0": 2.053029378587664, "4320.0": 6.161603379127277, "6480.0": 6.175522427215327, "1080.0": 2.5735849991352358, "18360.0": 0.6351040530590654, "10800.0": 5.068206847862049, "21600.0": 0.40727295182614354, "20520.0": 0.6621175064161002, "5400.0": 6.567824349703669, "3240.0": 5.655458079751501, "12960.0": 2.654764659666162, "15120.0": 1.6757350275784135}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"17280.0": 5.987636915771096, "0.0": 0.09558280565339891, "7560.0": 1.238130321602845, "9720.0": 1.9945247030805529, "19440.0": 6.695864385679773, "16200.0": 5.41645220805244, "11880.0": 2.719892366798277, "2160.0": 0.10805070272409367, "8640.0": 1.800229763433655, "14040.0": 4.156268601598023, "4320.0": -0.044818714779864405, "6480.0": 0.8106022415380871, "1080.0": -0.07327388848369068, "18360.0": 5.96868114596425, "10800.0": 2.0726982059573835, "21600.0": 7.269213818513372, "20520.0": 6.725777234409265, "5400.0": 0.18749831830326769, "3240.0": -0.10164819461093579, "12960.0": 3.745361461163259, "15120.0": 4.92464438752146}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 12.527811727243744, "0.0": 0.006198551839384427, "7560.0": 8.198459268980448, "9720.0": 10.884163155983586, "19440.0": 12.150552321810109, "16200.0": 13.247640677577017, "11880.0": 12.921639059281906, "2160.0": 1.2393091651113075, "8640.0": 9.76716833273541, "14040.0": 13.211149989298647, "4320.0": 3.803104433804622, "6480.0": 6.5810650565269375, "1080.0": 0.3042714459761661, "18360.0": 12.544400522361945, "10800.0": 11.737104197836604, "21600.0": 11.886358606219954, "20520.0": 11.832544691029744, "5400.0": 5.213810980890077, "3240.0": 2.1926632109587216, "12960.0": 13.113839789286594, "15120.0": 13.27982750652159}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 325.16059207223026, "0.0": 297.66636064503314, "7560.0": 331.3122493868531, "9720.0": 332.5912691607457, "19440.0": 325.80525903012233, "16200.0": 323.2692288871708, "11880.0": 334.0549081870754, "2160.0": 323.2373236557714, "8640.0": 333.4576764024497, "14040.0": 328.42212335544315, "4320.0": 327.6704558317418, "6480.0": 331.06042780025075, "1080.0": 316.2567029216892, "18360.0": 326.6647586865489, "10800.0": 334.23136746878185, "21600.0": 324.0057232633869, "20520.0": 324.4555288383823, "5400.0": 331.9568676813546, "3240.0": 326.12583828081813, "12960.0": 329.2382904744002, "15120.0": 327.3959354386782}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out new file mode 100644 index 00000000000..7eb7980a7be --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out @@ -0,0 +1 @@ +{"experiment": 10, "Ca0": 0, "Ca_meas": {"0.0": 0.17585381115505311, "4320.0": 6.0315379232850992, "7560.0": 5.4904391073929908, "21600.0": 0.28272237229599306, "9720.0": 5.2677178238115365, "16200.0": 1.1265897978788217, "20520.0": 0.057584376823091032, "3240.0": 5.6315955838815661, "11880.0": 3.328489578835276, "14040.0": 2.0226562017072607, "17280.0": 1.1268539727440208, "2160.0": 4.3030132489621638, "5400.0": 6.1094034780709618, "18360.0": 0.50886621390394615, "8640.0": 5.3773941013828752, "6480.0": 5.9760510402178078, "1080.0": 2.9280667525762598, "15120.0": 1.375026987701359, "12960.0": 2.5451999496997635, "19440.0": 0.79349685535634917, "10800.0": 4.3653401523141229}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": -0.038024312530073517, "4320.0": 0.35646997778490641, "7560.0": 1.0986283962244756, "21600.0": 7.160087143091391, "9720.0": 2.129148884681515, "16200.0": 5.1383561968992435, "20520.0": 6.8451793517536901, "3240.0": 0.098714783055484312, "11880.0": 3.0269500169512602, "14040.0": 3.9370558676788283, "17280.0": 5.9641262824404357, "2160.0": -0.12281730158248855, "5400.0": 0.59307341448224149, "18360.0": 6.2121451052794248, "8640.0": 1.7607685730069123, "6480.0": 0.53516134735284115, "1080.0": 0.021830284057365701, "15120.0": 4.7082270119144871, "12960.0": 4.0629501433813449, "19440.0": 6.333023537154518, "10800.0": 2.2805891192921983}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": -0.063620932422370907, "4320.0": 3.5433262677921662, "7560.0": 8.6893371808300444, "21600.0": 11.870505989648386, "9720.0": 11.309193344250055, "16200.0": 12.739396897287321, "20520.0": 11.791007739959538, "3240.0": 2.1799284210951009, "11880.0": 12.669418985545658, "14040.0": 13.141014574935076, "17280.0": 12.645153211711902, "2160.0": 1.4830589148905116, "5400.0": 5.2739635750232985, "18360.0": 12.761210138866151, "8640.0": 9.9142303856203373, "6480.0": 7.0761548290524603, "1080.0": 0.43050918133895111, "15120.0": 12.66028651303237, "12960.0": 12.766057551719733, "19440.0": 12.385465894826957, "10800.0": 11.96758252080965}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 301.47604965958681, "4320.0": 327.40308974239883, "7560.0": 332.84954650085029, "21600.0": 322.5157157706418, "9720.0": 333.3451281132692, "16200.0": 325.64418049630734, "20520.0": 321.94339225425767, "3240.0": 327.19623764314332, "11880.0": 330.24784399520615, "14040.0": 328.20666366981681, "17280.0": 324.73232197103431, "2160.0": 324.1280979971192, "5400.0": 330.87716394833132, "18360.0": 323.47482388751507, "8640.0": 332.49299626233665, "6480.0": 332.1275559564059, "1080.0": 319.29329353123859, "15120.0": 327.29837672678497, "12960.0": 329.24537484541742, "19440.0": 324.02559861322572, "10800.0": 335.57159429203386}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out new file mode 100644 index 00000000000..d97442f031b --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out @@ -0,0 +1 @@ +{"experiment": 11, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 2.9214639882977118, "7560.0": 3.782748575728669, "21600.0": 1.0934559688831289, "9720.0": 3.9988602724163731, "16200.0": 1.3975369118671503, "20520.0": 1.1243254110346068, "3240.0": 2.4246081576650433, "11880.0": 3.4755989173839743, "14040.0": 1.9594736461287787, "17280.0": 1.2827751626299488, "2160.0": 1.7884659182526941, "5400.0": 3.3009088212806073, "18360.0": 1.2111862780556402, "8640.0": 3.9172974313558302, "6480.0": 3.5822886585117493, "1080.0": 0.9878678077907459, "15120.0": 1.5972116122332332, "12960.0": 2.5920858659103394, "19440.0": 1.1618336860102094, "10800.0": 4.0378190270354528}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": 0.0, "4320.0": 0.023982318123109209, "7560.0": 0.12357850466238102, "21600.0": 6.5347744286121001, "9720.0": 0.24908724520575926, "16200.0": 3.2484340438243127, "20520.0": 5.9039946620569568, "3240.0": 0.0099959742916886744, "11880.0": 0.58037814751790051, "14040.0": 1.8149241834100336, "17280.0": 3.9360275733370447, "2160.0": 0.0028162554877217529, "5400.0": 0.046614709277751604, "18360.0": 4.6059327483829717, "8640.0": 0.17995429953061945, "6480.0": 0.079417046516631534, "1080.0": 0.00029995464126276485, "15120.0": 2.5396785773426069, "12960.0": 1.1193190878564474, "19440.0": 5.2613272888405183, "10800.0": 0.33137635542084787}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.0, "4320.0": 0.96588936445839646, "7560.0": 2.5063363714379272, "21600.0": 7.0191274767686718, "9720.0": 3.6738114082888234, "16200.0": 7.2205659498627206, "20520.0": 7.0929480434376666, "3240.0": 0.56824903663740756, "11880.0": 5.2689545424581627, "14040.0": 6.8616827734843717, "17280.0": 7.2356915893004699, "2160.0": 0.25984959276122965, "5400.0": 1.4328144788513235, "18360.0": 7.2085405027857679, "8640.0": 3.0841896398822519, "6480.0": 1.9514434452676097, "1080.0": 0.063681239951285565, "15120.0": 7.1238797146821753, "12960.0": 6.279843675978368, "19440.0": 7.1578041487575703, "10800.0": 4.2664529460540459}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 312.23882460110946, "7560.0": 313.65588947948061, "21600.0": 347.47264008527429, "9720.0": 314.23459379740643, "16200.0": 349.53974420970837, "20520.0": 347.6739307915775, "3240.0": 311.5507050941913, "11880.0": 343.84565806826117, "14040.0": 351.77400891772504, "17280.0": 348.76203275606656, "2160.0": 310.56857709679934, "5400.0": 312.79850843986321, "18360.0": 348.2596349485566, "8640.0": 313.9757607933924, "6480.0": 313.26649244154709, "1080.0": 308.41052123547064, "15120.0": 350.65940715239384, "12960.0": 351.11078111562267, "19440.0": 347.92214750818005, "10800.0": 317.60632266285268}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out new file mode 100644 index 00000000000..cbed2d89634 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out @@ -0,0 +1 @@ +{"experiment": 12, "Ca0": 0, "Ca_meas": {"0.0": -0.11670041274142862, "4320.0": 2.5843604031503551, "7560.0": 3.9909358380362421, "21600.0": 0.85803342268297711, "9720.0": 3.8011636431252702, "16200.0": 1.1002575863333801, "20520.0": 1.5248654901477563, "3240.0": 2.8626873137030655, "11880.0": 3.0876088469734766, "14040.0": 2.1233520630854348, "17280.0": 1.3118009790549121, "2160.0": 1.9396713478350336, "5400.0": 3.4898817799323192, "18360.0": 1.3214498335778544, "8640.0": 4.3166520687122008, "6480.0": 3.7430014080130212, "1080.0": 0.71502131244112932, "15120.0": 1.6869248126824714, "12960.0": 2.6199132141077999, "19440.0": 1.1934026369102775, "10800.0": 4.1662760005238244}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": -0.14625373198142194, "4320.0": 0.50521240190855021, "7560.0": 0.072658369151157948, "21600.0": 6.5820277701997947, "9720.0": 0.14656574869775074, "16200.0": 3.2344935292984842, "20520.0": 6.1934992398225113, "3240.0": 0.073187422926812754, "11880.0": 0.68271223751792398, "14040.0": 1.5495094265280573, "17280.0": 3.5758298262936474, "2160.0": -0.052665808995189155, "5400.0": -0.067101579134353218, "18360.0": 4.6809081069906799, "8640.0": 0.58099071333349073, "6480.0": -0.34905530826754594, "1080.0": -0.13981627097780677, "15120.0": 2.7577781806493582, "12960.0": 0.9824219783559841, "19440.0": 5.2002977724609245, "10800.0": 0.0089889440138224419}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": -0.34864609349591541, "4320.0": 0.99561164186682227, "7560.0": 2.4688230226633143, "21600.0": 7.0564292657160461, "9720.0": 3.9961025255558669, "16200.0": 7.161739218807984, "20520.0": 6.8044634236188921, "3240.0": 0.21017912447011355, "11880.0": 5.1168427571991435, "14040.0": 7.0032016822280907, "17280.0": 7.11845364876228, "2160.0": 0.22241873726625405, "5400.0": 1.3174801426799267, "18360.0": 6.9581816529657257, "8640.0": 3.0438444011178785, "6480.0": 2.2558063612162291, "1080.0": 0.38969247867806489, "15120.0": 7.2125210995495488, "12960.0": 6.4014182164755429, "19440.0": 6.8128450220608308, "10800.0": 4.2649851849420299}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.53274252012955, "4320.0": 311.15339067777529, "7560.0": 312.18867239090878, "21600.0": 346.00646319366473, "9720.0": 313.61710665630221, "16200.0": 352.81825567952984, "20520.0": 346.66248325950249, "3240.0": 311.37928873928001, "11880.0": 343.17457873193757, "14040.0": 352.88609842940627, "17280.0": 348.48519596719899, "2160.0": 312.70676562686674, "5400.0": 314.29841143358993, "18360.0": 347.81967845794014, "8640.0": 313.26528616120316, "6480.0": 314.33539425367189, "1080.0": 308.35955588183077, "15120.0": 350.62179387183005, "12960.0": 348.72513776404708, "19440.0": 346.65341312375318, "10800.0": 318.79995964600641}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out new file mode 100644 index 00000000000..6ef514c951a --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out @@ -0,0 +1 @@ +{"experiment": 13, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 2.9015294551493156, "7560.0": 3.7456240714596114, "21600.0": 1.0893473942847287, "9720.0": 3.955661759695563, "16200.0": 1.3896514099314119, "20520.0": 1.1200835239496501, "3240.0": 2.4116751003091821, "11880.0": 3.4212519616089123, "14040.0": 1.9331934810250495, "17280.0": 1.2772682671606199, "2160.0": 1.7821342925311514, "5400.0": 3.2743367747379883, "18360.0": 1.2065189956942144, "8640.0": 3.8765800278544793, "6480.0": 3.5499054339456388, "1080.0": 0.98643229677676525, "15120.0": 1.583391359829623, "12960.0": 2.5471212725882397, "19440.0": 1.1574522594063252, "10800.0": 3.9931029485836738}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": 0.0, "4320.0": 0.025294213033754839, "7560.0": 0.12897041691694733, "21600.0": 6.5640983670402253, "9720.0": 0.2583989163781088, "16200.0": 3.2753828012649455, "20520.0": 5.9325635171128175, "3240.0": 0.01057908838025531, "11880.0": 0.60077017176350533, "14040.0": 1.8465767045073862, "17280.0": 3.9624456898675042, "2160.0": 0.0029862658207987017, "5400.0": 0.048985517519306479, "18360.0": 4.6327886545170172, "8640.0": 0.1872203610374778, "6480.0": 0.083160507067847278, "1080.0": 0.0003160950335645278, "15120.0": 2.5686484578016535, "12960.0": 1.149697840345306, "19440.0": 5.2890172904133799, "10800.0": 0.3428648031290083}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.0, "4320.0": 0.98451200269614558, "7560.0": 2.5380689634524152, "21600.0": 6.993912112938939, "9720.0": 3.7076982498372795, "16200.0": 7.2015026943578233, "20520.0": 7.0686210754667576, "3240.0": 0.58059897990470211, "11880.0": 5.3029094739876195, "14040.0": 6.8563104174907483, "17280.0": 7.2147803682393352, "2160.0": 0.26601120814969509, "5400.0": 1.4570157171523839, "18360.0": 7.1863518790131433, "8640.0": 3.1176409818767401, "6480.0": 1.9800832092825005, "1080.0": 0.06510061057296454, "15120.0": 7.1087300866267373, "12960.0": 6.294429516811606, "19440.0": 7.1344955737885876, "10800.0": 4.2996805767976616}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 312.83405362164399, "7560.0": 314.10857941130064, "21600.0": 347.58975942376372, "9720.0": 314.61233912755387, "16200.0": 349.56203914424219, "20520.0": 347.79317623855678, "3240.0": 312.2016171592112, "11880.0": 344.43453363173575, "14040.0": 351.72789167129031, "17280.0": 348.83759825667926, "2160.0": 311.27622091507072, "5400.0": 313.34232236658977, "18360.0": 348.36453268948424, "8640.0": 314.38892058203726, "6480.0": 313.76278431242508, "1080.0": 309.11491437259622, "15120.0": 350.61679816631096, "12960.0": 351.27269989378158, "19440.0": 348.03901542922108, "10800.0": 318.0555419842903}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out new file mode 100644 index 00000000000..cc3f95da860 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out @@ -0,0 +1 @@ +{"experiment": 14, "Ca0": 0, "Ca_meas": {"0.0": 0.081520066870384211, "4320.0": 2.8337428652268013, "7560.0": 3.9530051428610888, "21600.0": 1.1807185641508762, "9720.0": 4.0236480913821637, "16200.0": 1.4376034521825525, "20520.0": 1.5413859123725682, "3240.0": 2.7181227248994633, "11880.0": 3.3903617547506242, "14040.0": 2.0159168723544196, "17280.0": 1.0638207528750085, "2160.0": 1.8959521419461316, "5400.0": 2.9705863021885124, "18360.0": 1.0674705545585839, "8640.0": 4.144391992823846, "6480.0": 3.5918471023904477, "1080.0": 1.0113708479421195, "15120.0": 1.6541373379275581, "12960.0": 2.6476139688086877, "19440.0": 1.2312633238777129, "10800.0": 3.8743606114727154}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": -0.089503442052066576, "4320.0": 0.18208896954493076, "7560.0": 0.17697219964055824, "21600.0": 6.8780014679710426, "9720.0": 0.35693081777790492, "16200.0": 3.1977597178279002, "20520.0": 6.1361982627818481, "3240.0": 0.10972700122863582, "11880.0": 0.76070080263615369, "14040.0": 1.5671921543009262, "17280.0": 4.0972632572434122, "2160.0": -0.28322371444225319, "5400.0": -0.079382269802482266, "18360.0": 4.5706426776745905, "8640.0": 0.26888423813019369, "6480.0": 0.1979519809989283, "1080.0": 0.13979150229071807, "15120.0": 2.6867100129129424, "12960.0": 0.99472739483139283, "19440.0": 5.6554142424694902, "10800.0": 0.46709816548507599}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.1139198104669558, "4320.0": 0.99036932502879282, "7560.0": 2.4258194495364482, "21600.0": 7.0446273994379434, "9720.0": 3.7506516413639113, "16200.0": 6.9192013751011503, "20520.0": 7.1555299371654808, "3240.0": 0.66230857796133746, "11880.0": 5.0716652323781499, "14040.0": 7.0971130388695007, "17280.0": 7.4091470358534082, "2160.0": -0.039078609338807413, "5400.0": 1.480378464133409, "18360.0": 7.1741052031883399, "8640.0": 3.4996110541636019, "6480.0": 2.0450173775271647, "1080.0": 0.13728827557251419, "15120.0": 6.7382150794212539, "12960.0": 6.3936782268937753, "19440.0": 7.3298178049407321, "10800.0": 4.6925893428035463}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 301.43741888919669, "4320.0": 313.81215912108536, "7560.0": 313.22398065446896, "21600.0": 347.95979639817102, "9720.0": 316.38480427739029, "16200.0": 350.46267406552954, "20520.0": 349.96840569755773, "3240.0": 313.32903259260996, "11880.0": 345.61089172333249, "14040.0": 352.83446396320579, "17280.0": 347.23320123776085, "2160.0": 310.02897702317114, "5400.0": 314.70707924235739, "18360.0": 349.8524737596988, "8640.0": 314.13917383134913, "6480.0": 314.15959183349207, "1080.0": 307.97982604287193, "15120.0": 349.00176197155969, "12960.0": 350.41651244789142, "19440.0": 346.1591726550746, "10800.0": 317.90588794308121}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out new file mode 100644 index 00000000000..e5245dff3f0 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out @@ -0,0 +1 @@ +{"experiment": 2, "Ca0": 0, "Ca_meas": {"12960.0": 2.6833775561963225, "21600.0": 1.1452663167259416, "17280.0": 1.2021595324165524, "19440.0": 1.0621392247792012, "0.0": -0.1316904398446574, "7560.0": 3.544605362880795, "11880.0": 3.1817551426501267, "14040.0": 2.066815570405579, "4320.0": 3.0488618589043432, "15120.0": 1.5896211475539537, "1080.0": 0.8608182979507091, "18360.0": 1.1317484585922248, "8640.0": 3.454602822099547, "2160.0": 1.7479951078246254, "20520.0": 1.2966191801491087, "9720.0": 4.0596636929917285, "6480.0": 3.9085446597134283, "3240.0": 2.5050366860794875, "5400.0": 3.2668528110981576, "10800.0": 4.004828727345138, "16200.0": 1.2860720212507326}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"12960.0": 1.3318254182147888, "21600.0": 6.431089735352747, "17280.0": 3.9711701719678825, "19440.0": 5.321278981143728, "0.0": 0.10325037628575211, "7560.0": 0.10827803632010198, "11880.0": 0.5184920846420157, "14040.0": 1.7974496302186054, "4320.0": 0.03112694971654564, "15120.0": 2.5245584142423207, "1080.0": 0.27315169217241275, "18360.0": 4.587420104936772, "8640.0": 0.1841080751926184, "2160.0": -0.3018405210283593, "20520.0": 6.0086124912796794, "9720.0": 0.08100716578409362, "6480.0": 0.027897809479352567, "3240.0": 0.01973928836607919, "5400.0": 0.02694434057766582, "10800.0": 0.3021977838614568, "16200.0": 3.561159267372319}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"12960.0": 6.358162874770003, "21600.0": 7.190121324797683, "17280.0": 7.302887809357959, "19440.0": 7.249179120248633, "0.0": -0.2300456054526237, "7560.0": 2.5654112157969364, "11880.0": 5.3061363321784025, "14040.0": 6.84009925060659, "4320.0": 0.9644475554073758, "15120.0": 7.261439274435592, "1080.0": 0.2644823572584467, "18360.0": 7.3015136544611305, "8640.0": 3.065189548359326, "2160.0": 0.27122113581760515, "20520.0": 7.162520282520871, "9720.0": 3.701445814227159, "6480.0": 2.0255650533089735, "3240.0": 0.48891328422133334, "5400.0": 1.326135697891304, "10800.0": 4.031252283597449, "16200.0": 7.38249778574694}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"12960.0": 350.10381529626676, "21600.0": 345.04247547236935, "17280.0": 348.24859807524, "19440.0": 346.7076647696431, "0.0": 301.30135373385, "7560.0": 314.7140350191631, "11880.0": 343.608488145403, "14040.0": 352.2902404657956, "4320.0": 312.2349716351422, "15120.0": 351.28330527232634, "1080.0": 307.67178366332996, "18360.0": 347.3517733033304, "8640.0": 313.62143853358026, "2160.0": 310.2674222024707, "20520.0": 348.1662021459614, "9720.0": 314.8081875940964, "6480.0": 312.7396303616638, "3240.0": 310.9258419605079, "5400.0": 312.8448509580561, "10800.0": 317.33866255037736, "16200.0": 349.5494112095972}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out new file mode 100644 index 00000000000..a9b013d476f --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out @@ -0,0 +1 @@ +{"experiment": 3, "Ca0": 0, "Ca_meas": {"17280.0": 0.39426558381033217, "0.0": -0.25761950998771116, "7560.0": 7.638659217862309, "9720.0": 7.741721088143662, "19440.0": 0.12706787288438182, "16200.0": 0.15060928317089423, "11880.0": 4.534965302380243, "2160.0": 4.47101661859487, "8640.0": 7.562734803826617, "14040.0": 0.8456407304976143, "4320.0": 7.395023123698018, "6480.0": 7.952409415603349, "1080.0": 3.0448009666821947, "18360.0": 0.0754742404427045, "10800.0": 7.223420802364689, "21600.0": 0.181946676125186, "20520.0": 0.195256504023462, "5400.0": 7.844394030136843, "3240.0": 6.031994466757849, "12960.0": 1.813573590958129, "15120.0": 0.30408071219857113}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"17280.0": 10.587758291638856, "0.0": -0.21796531802425348, "7560.0": 0.8680716299725404, "9720.0": 0.9085250272598128, "19440.0": 11.914154788848554, "16200.0": 9.737618534040658, "11880.0": 2.0705302921286064, "2160.0": -0.022903632391514165, "8640.0": 0.5195918959805059, "14040.0": 6.395605356788582, "4320.0": 0.010107836996695638, "6480.0": 0.09956884355869228, "1080.0": -0.21178603534213003, "18360.0": 10.990013628196317, "10800.0": 1.345982231414325, "21600.0": 12.771296192955955, "20520.0": 12.196513048345082, "5400.0": 0.04790148745481311, "3240.0": 0.318546588876358, "12960.0": 4.693433882970767, "15120.0": 8.491695125145553}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 8.731932027503914, "0.0": -0.1375816812387338, "7560.0": 6.8484762842626985, "9720.0": 9.333087689786101, "19440.0": 7.381577268709603, "16200.0": 9.502048821225666, "11880.0": 12.406853134672218, "2160.0": 0.8107944776900446, "8640.0": 8.158484571318013, "14040.0": 11.54445651179274, "4320.0": 2.8119825114954082, "6480.0": 5.520857819630275, "1080.0": 0.18414413253133835, "18360.0": 8.145712620219781, "10800.0": 10.75765409121092, "21600.0": 6.1356948865706356, "20520.0": 6.576247039355788, "5400.0": 3.92591568907661, "3240.0": 2.015632242014947, "12960.0": 12.71320139030468, "15120.0": 10.314039809497785}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 345.7556825478521, "0.0": 300.1125978749429, "7560.0": 319.7301093967675, "9720.0": 321.4474947517745, "19440.0": 345.1982881134514, "16200.0": 348.50691612993586, "11880.0": 357.2800598685144, "2160.0": 311.7652063627056, "8640.0": 323.6663990115871, "14040.0": 365.2497105829804, "4320.0": 317.1702037696461, "6480.0": 319.9056594806601, "1080.0": 309.6187369359568, "18360.0": 345.1427626681205, "10800.0": 323.9154289387584, "21600.0": 345.45022165546357, "20520.0": 344.5991879566869, "5400.0": 316.21958050333416, "3240.0": 314.95895736530616, "12960.0": 371.5669986462856, "15120.0": 355.3612054360245}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out new file mode 100644 index 00000000000..e702db7d05b --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out @@ -0,0 +1 @@ +{"experiment": 4, "Ca0": 0, "Ca_meas": {"17280.0": 1.1816674202534045, "0.0": -0.25414468591124584, "7560.0": 5.914582094251343, "9720.0": 5.185067133371561, "19440.0": 0.5307435290768995, "16200.0": 1.2011215994628408, "11880.0": 3.5778183914967925, "2160.0": 4.254440534150475, "8640.0": 5.473440610227645, "14040.0": 2.1475894664278354, "4320.0": 5.8795707148110266, "6480.0": 6.089523479429854, "1080.0": 2.4339586418303543, "18360.0": 0.545228377126232, "10800.0": 4.946406396626746, "21600.0": 0.3169450590438124, "20520.0": 0.5859997070045333, "5400.0": 6.15901928205937, "3240.0": 5.5559094344993225, "12960.0": 2.476561130612629, "15120.0": 1.35232620260846}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"17280.0": 6.529641678005382, "0.0": 0.23057916607631007, "7560.0": 1.4209973582182187, "9720.0": 2.662002861314475, "19440.0": 7.612677904908142, "16200.0": 6.236740105453746, "11880.0": 3.6132373498432813, "2160.0": 0.41778377045750303, "8640.0": 2.3031169482702336, "14040.0": 4.857027337132376, "4320.0": 0.21846387467958883, "6480.0": 1.304912741118713, "1080.0": -0.002497120213976349, "18360.0": 7.207560113722179, "10800.0": 3.072350404943197, "21600.0": 8.437070128901182, "20520.0": 7.985790844633096, "5400.0": 0.5552902218354748, "3240.0": 0.4207298617922146, "12960.0": 4.791797355546968, "15120.0": 5.544868662346418}, "alphaj": 0.8, "Cb0": 2, "Vr0": 1, "Cb_meas": {"17280.0": 13.559468310792148, "0.0": 2.1040937779048963, "7560.0": 9.809117250733637, "9720.0": 12.404875593478181, "19440.0": 12.616477055699818, "16200.0": 13.94157106495499, "11880.0": 13.828382570964388, "2160.0": 3.1373485614417618, "8640.0": 11.053587506450443, "14040.0": 14.267859106799012, "4320.0": 5.6037726942190424, "6480.0": 8.499893646580981, "1080.0": 2.2930856900001535, "18360.0": 13.566545045105496, "10800.0": 13.397445458860116, "21600.0": 12.347983697813623, "20520.0": 12.422291796055749, "5400.0": 7.367727360896684, "3240.0": 3.8542518870239824, "12960.0": 14.038139660530316, "15120.0": 14.114308276615096}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 324.4465785902298, "0.0": 299.6733512720839, "7560.0": 333.04897592093835, "9720.0": 334.48916083151335, "19440.0": 323.5321646227951, "16200.0": 326.84528144052564, "11880.0": 332.77528830002086, "2160.0": 322.2453586799791, "8640.0": 334.2512423463752, "14040.0": 329.05431569837447, "4320.0": 327.99896899102527, "6480.0": 334.3262547857335, "1080.0": 317.32350372398014, "18360.0": 324.97573570445866, "10800.0": 334.23107235994195, "21600.0": 323.6424061352077, "20520.0": 324.00045995355015, "5400.0": 331.45112696032044, "3240.0": 326.33322784448785, "12960.0": 330.4778004752374, "15120.0": 326.8776604963411}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out new file mode 100644 index 00000000000..6c4b1b1d9e0 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out @@ -0,0 +1 @@ +{"experiment": 5, "Ca0": 0, "Ca_meas": {"17280.0": 0.5384747196579402, "0.0": -0.14396911833443257, "7560.0": 6.038385982663388, "9720.0": 5.0792329539506, "19440.0": 0.3782801126758533, "16200.0": 1.0619887309834395, "11880.0": 3.6494330494296436, "2160.0": 4.775401751873804, "8640.0": 5.629577532845656, "14040.0": 2.0037718871692265, "4320.0": 5.889802624055117, "6480.0": 6.09724817816528, "1080.0": 2.875851853145854, "18360.0": 0.6780066197547887, "10800.0": 4.859469684381779, "21600.0": 0.3889400954173796, "20520.0": 0.3351378562274788, "5400.0": 6.127222815180268, "3240.0": 5.289726682847115, "12960.0": 2.830845316709853, "15120.0": 1.5312992911111707}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 2, "Tc1": 320, "Cc_meas": {"17280.0": 7.596710556194337, "0.0": 1.6504080367065743, "7560.0": 2.809410585275859, "9720.0": 3.8419183958835132, "19440.0": 8.137782633931637, "16200.0": 6.938967086259325, "11880.0": 5.022162589071362, "2160.0": 2.0515545922033964, "8640.0": 3.506455726732785, "14040.0": 6.010539749263416, "4320.0": 2.2056993474658584, "6480.0": 2.5775763528099858, "1080.0": 2.03522693402577, "18360.0": 8.083917616781594, "10800.0": 4.662851778068136, "21600.0": 9.279674687903626, "20520.0": 8.963676424956157, "5400.0": 2.293408505844697, "3240.0": 1.9216270432789067, "12960.0": 5.637375563057352, "15120.0": 6.3296720972633045}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 12.831256801432378, "0.0": 0.13194621122245662, "7560.0": 7.965534934436229, "9720.0": 10.908595985103954, "19440.0": 12.408596390398941, "16200.0": 12.975405069340143, "11880.0": 12.710800046234393, "2160.0": 0.9223242691530996, "8640.0": 9.454601468197033, "14040.0": 13.19437793062601, "4320.0": 3.713168763161746, "6480.0": 6.515936097446724, "1080.0": 0.031354105110323494, "18360.0": 12.821094923672087, "10800.0": 11.90520078370877, "21600.0": 11.81429953673305, "20520.0": 12.099271866573613, "5400.0": 4.982941965055916, "3240.0": 2.766378581935415, "12960.0": 13.074621618364043, "15120.0": 12.957819319226212}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 326.8759279818479, "0.0": 300.7736745288814, "7560.0": 333.6674031533901, "9720.0": 333.59854139744135, "19440.0": 324.5264772316974, "16200.0": 325.2454701315101, "11880.0": 332.9849253092768, "2160.0": 322.1940607068012, "8640.0": 331.78378240085084, "14040.0": 328.48981010099453, "4320.0": 327.3883510651506, "6480.0": 330.15101610436426, "1080.0": 318.24994073025096, "18360.0": 323.9527212120804, "10800.0": 333.3006916263996, "21600.0": 322.07065855783964, "20520.0": 324.3518907083261, "5400.0": 331.5429008148077, "3240.0": 324.52116111644654, "12960.0": 329.21899337854876, "15120.0": 328.26179934031467}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out new file mode 100644 index 00000000000..c1630902e1a --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out @@ -0,0 +1 @@ +{"experiment": 6, "Ca0": 2, "Ca_meas": {"17280.0": 1.1595382970987758, "0.0": 1.9187666718004224, "7560.0": 5.977461039170304, "9720.0": 5.164472215594892, "19440.0": 0.6528636977624275, "16200.0": 1.2106046606700225, "11880.0": 3.3229659243191296, "2160.0": 5.923887627906124, "8640.0": 5.225477003110976, "14040.0": 1.7878931129582107, "4320.0": 6.782806717544953, "6480.0": 6.27323507174512, "1080.0": 4.481633914987097, "18360.0": 0.8866911582721309, "10800.0": 4.481150474336123, "21600.0": 0.2170007972283953, "20520.0": 0.3199825651255196, "5400.0": 6.795886093698936, "3240.0": 6.288606047308427, "12960.0": 2.4509424000990685, "15120.0": 1.506568611506372}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"17280.0": 6.588306395438309, "0.0": 0.24402401145820712, "7560.0": 1.4036889138374646, "9720.0": 2.4218855673847455, "19440.0": 7.764492558630308, "16200.0": 6.205315919403138, "11880.0": 3.593219427441702, "2160.0": -0.10553376629311664, "8640.0": 1.8628128392103824, "14040.0": 5.027532358914124, "4320.0": 0.2172961549286831, "6480.0": 0.875637414228913, "1080.0": 0.2688503672636328, "18360.0": 7.240866507350995, "10800.0": 3.26514503365032, "21600.0": 8.251445433411781, "20520.0": 7.987953548408583, "5400.0": 0.6458428001299884, "3240.0": 0.3201498579399834, "12960.0": 4.263491165240245, "15120.0": 5.885223860529403}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 13.604962664333257, "0.0": -0.20747860874385013, "7560.0": 10.013416230907982, "9720.0": 12.055665770517061, "19440.0": 13.289064414490856, "16200.0": 13.82240671329648, "11880.0": 13.783622629658064, "2160.0": 1.8287780310400052, "8640.0": 11.17018006067254, "14040.0": 14.064985893141849, "4320.0": 5.072613174963567, "6480.0": 8.597613514631933, "1080.0": 0.3932885074677299, "18360.0": 13.473844971871975, "10800.0": 13.343451941795923, "21600.0": 12.209822751574375, "20520.0": 12.483108442400093, "5400.0": 6.7290370118557545, "3240.0": 3.4163314305947527, "12960.0": 14.296996898861073, "15120.0": 14.543019390786785}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 324.3952638382081, "0.0": 300.9417777707309, "7560.0": 334.57634369064954, "9720.0": 336.13718804612154, "19440.0": 324.10564173791454, "16200.0": 324.97714743647435, "11880.0": 332.29384802281055, "2160.0": 324.243456129639, "8640.0": 335.85007440436317, "14040.0": 329.01187645109906, "4320.0": 331.1961476255781, "6480.0": 333.16262386818596, "1080.0": 319.0632107387995, "18360.0": 322.11836267923206, "10800.0": 332.8894634515628, "21600.0": 323.92451205164855, "20520.0": 323.319714630304, "5400.0": 334.21206737651613, "3240.0": 326.78695915581983, "12960.0": 329.6184998003745, "15120.0": 327.5414299857002}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out new file mode 100644 index 00000000000..6ef879f3a17 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out @@ -0,0 +1 @@ +{"experiment": 7, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 5.9967662103693256, "7560.0": 5.6550872327951165, "21600.0": 0.35323003565444244, "9720.0": 5.0380179697260221, "16200.0": 1.1574854069611118, "20520.0": 0.44526599556762764, "3240.0": 5.5465851989526014, "11880.0": 3.4091089779405226, "14040.0": 1.9309662288658835, "17280.0": 0.90614590071077117, "2160.0": 4.5361731979596218, "5400.0": 6.071094394377246, "18360.0": 0.71270371205373184, "8640.0": 5.3475100856285955, "6480.0": 5.9202343949662177, "1080.0": 2.7532264453848811, "15120.0": 1.4880794860689583, "12960.0": 2.5406275798785134, "19440.0": 0.56254756816886675, "10800.0": 4.6684283481238458}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": 9.1835496157991212e-40, "4320.0": 0.2042152487619561, "7560.0": 1.0742319748668527, "21600.0": 7.2209534629193319, "9720.0": 2.0351470130435847, "16200.0": 5.2015141957639486, "20520.0": 6.8469618370195322, "3240.0": 0.080409363629683248, "11880.0": 3.1846111102658314, "14040.0": 4.26052274570326, "17280.0": 5.6380747523602874, "2160.0": 0.020484309410223334, "5400.0": 0.4082391087574655, "18360.0": 6.0566679041163232, "8640.0": 1.5236474138282798, "6480.0": 0.69922443474303053, "1080.0": 0.0017732304596560309, "15120.0": 4.7439891962421239, "12960.0": 3.7433194976361364, "19440.0": 6.4591935065135972, "10800.0": 2.5966732389812686}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 9.1835496157991212e-41, "4320.0": 3.7902671698338786, "7560.0": 8.430643849127673, "21600.0": 11.611715650093123, "9720.0": 10.913795239302978, "16200.0": 12.826899545942249, "20520.0": 11.893671316079821, "3240.0": 2.2947643625752363, "11880.0": 12.592179060461286, "14040.0": 12.994410174098332, "17280.0": 12.641678495596169, "2160.0": 1.0564916422363797, "5400.0": 5.3872034016274126, "18360.0": 12.416527532497087, "8640.0": 9.7522120688862319, "6480.0": 6.9615062931011256, "1080.0": 0.24785349222969222, "15120.0": 12.953830466356314, "12960.0": 12.901952071152909, "19440.0": 12.164158073984597, "10800.0": 11.920797561562614}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 329.97137612867306, "7560.0": 333.16333833254259, "21600.0": 323.31193212521009, "9720.0": 333.2600929343854, "16200.0": 325.39648330671537, "20520.0": 323.56691057280642, "3240.0": 327.66452370998064, "11880.0": 331.65035718435655, "14040.0": 327.56747174535786, "17280.0": 324.74920150215411, "2160.0": 324.4585751379426, "5400.0": 331.60125794337375, "18360.0": 324.25971411725646, "8640.0": 333.33010300559897, "6480.0": 332.63089408099353, "1080.0": 318.87878296714581, "15120.0": 326.2893083756407, "12960.0": 329.38909200530219, "19440.0": 323.87612174726837, "10800.0": 333.08705569007822}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out new file mode 100644 index 00000000000..6aa9fea17b3 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out @@ -0,0 +1 @@ +{"experiment": 8, "Ca0": 0, "Ca_meas": {"0.0": 0.30862088766711671, "4320.0": 6.0491110491659228, "7560.0": 5.7909310485601502, "21600.0": 0.232444399226299, "9720.0": 4.9320449060797475, "16200.0": 0.97134242753331668, "20520.0": 0.42847724332841963, "3240.0": 5.6988320807198498, "11880.0": 3.3235733576868514, "14040.0": 1.9846460628194049, "17280.0": 0.87715206210722585, "2160.0": 4.615346351863904, "5400.0": 6.384056703029386, "18360.0": 0.41688144324552118, "8640.0": 5.4121173109702099, "6480.0": 6.0660731346226324, "1080.0": 2.8379509025410488, "15120.0": 0.98831570466285279, "12960.0": 2.2167483934357417, "19440.0": 0.46284950985984985, "10800.0": 4.7377220491627412}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": -0.050833820035522316, "4320.0": 0.21055066883154505, "7560.0": 1.3851246436045646, "21600.0": 7.3672985895890362, "9720.0": 2.0100502709379842, "16200.0": 5.1793087406376159, "20520.0": 6.840381847429823, "3240.0": 0.13411276227648503, "11880.0": 3.3052152545385454, "14040.0": 3.9431305823279708, "17280.0": 5.7290141848801586, "2160.0": 0.16719633749951002, "5400.0": 0.49872603502453117, "18360.0": 6.1508540969551078, "8640.0": 1.4737312345987361, "6480.0": 0.69437977126769512, "1080.0": -0.0093978134715377304, "15120.0": 4.9151661032041298, "12960.0": 4.0623539766149843, "19440.0": 6.3058400571478561, "10800.0": 2.820347355873587}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.037166728983599892, "4320.0": 3.5183932586092856, "7560.0": 8.916682434428397, "21600.0": 11.534104657089006, "9720.0": 11.087958791247285, "16200.0": 13.02597376112109, "20520.0": 11.639999923137731, "3240.0": 2.346958004261503, "11880.0": 12.049613604010537, "14040.0": 12.906738918465997, "17280.0": 12.867691165140879, "2160.0": 1.3313958783841602, "5400.0": 5.3650409213472221, "18360.0": 12.405763004965722, "8640.0": 9.5635832344445717, "6480.0": 7.0954049721214671, "1080.0": 0.40883709280782765, "15120.0": 12.971506554625082, "12960.0": 12.829158718434032, "19440.0": 11.946615137583075, "10800.0": 11.373799750334223}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.55141264078583, "4320.0": 329.42497918063066, "7560.0": 333.82602046942475, "21600.0": 323.68642879192487, "9720.0": 332.94208820576767, "16200.0": 325.82299128298814, "20520.0": 325.19753703643721, "3240.0": 329.66504941755875, "11880.0": 332.29546982118751, "14040.0": 326.51837436850099, "17280.0": 326.51851506890586, "2160.0": 323.70134945698589, "5400.0": 328.6805843225718, "18360.0": 324.79832692054578, "8640.0": 331.94068007914785, "6480.0": 332.75141896044545, "1080.0": 318.90722718736015, "15120.0": 325.85289150843209, "12960.0": 327.72250161440121, "19440.0": 325.17198606848808, "10800.0": 334.1255807822717}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out new file mode 100644 index 00000000000..627f92b1f83 --- /dev/null +++ b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out @@ -0,0 +1 @@ +{"experiment": 9, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 5.9967662103693256, "7560.0": 5.6550872327951165, "21600.0": 0.35323003565444244, "9720.0": 5.0380179697260221, "16200.0": 1.1574854069611118, "20520.0": 0.44526599556762764, "3240.0": 5.5465851989526014, "11880.0": 3.4091089779405226, "14040.0": 1.9309662288658835, "17280.0": 0.90614590071077117, "2160.0": 4.5361731979596218, "5400.0": 6.071094394377246, "18360.0": 0.71270371205373184, "8640.0": 5.3475100856285955, "6480.0": 5.9202343949662177, "1080.0": 2.7532264453848811, "15120.0": 1.4880794860689583, "12960.0": 2.5406275798785134, "19440.0": 0.56254756816886675, "10800.0": 4.6684283481238458}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": 9.1835496157991212e-40, "4320.0": 0.2042152487619561, "7560.0": 1.0742319748668527, "21600.0": 7.2209534629193319, "9720.0": 2.0351470130435847, "16200.0": 5.2015141957639486, "20520.0": 6.8469618370195322, "3240.0": 0.080409363629683248, "11880.0": 3.1846111102658314, "14040.0": 4.26052274570326, "17280.0": 5.6380747523602874, "2160.0": 0.020484309410223334, "5400.0": 0.4082391087574655, "18360.0": 6.0566679041163232, "8640.0": 1.5236474138282798, "6480.0": 0.69922443474303053, "1080.0": 0.0017732304596560309, "15120.0": 4.7439891962421239, "12960.0": 3.7433194976361364, "19440.0": 6.4591935065135972, "10800.0": 2.5966732389812686}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 9.1835496157991212e-41, "4320.0": 3.7902671698338786, "7560.0": 8.430643849127673, "21600.0": 11.611715650093123, "9720.0": 10.913795239302978, "16200.0": 12.826899545942249, "20520.0": 11.893671316079821, "3240.0": 2.2947643625752363, "11880.0": 12.592179060461286, "14040.0": 12.994410174098332, "17280.0": 12.641678495596169, "2160.0": 1.0564916422363797, "5400.0": 5.3872034016274126, "18360.0": 12.416527532497087, "8640.0": 9.7522120688862319, "6480.0": 6.9615062931011256, "1080.0": 0.24785349222969222, "15120.0": 12.953830466356314, "12960.0": 12.901952071152909, "19440.0": 12.164158073984597, "10800.0": 11.920797561562614}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 329.97137612867306, "7560.0": 333.16333833254259, "21600.0": 323.31193212521009, "9720.0": 333.2600929343854, "16200.0": 325.39648330671537, "20520.0": 323.56691057280642, "3240.0": 327.66452370998064, "11880.0": 331.65035718435655, "14040.0": 327.56747174535786, "17280.0": 324.74920150215411, "2160.0": 324.4585751379426, "5400.0": 331.60125794337375, "18360.0": 324.25971411725646, "8640.0": 333.33010300559897, "6480.0": 332.63089408099353, "1080.0": 318.87878296714581, "15120.0": 326.2893083756407, "12960.0": 329.38909200530219, "19440.0": 323.87612174726837, "10800.0": 333.08705569007822}} \ No newline at end of file From 07b612a1ef1396b831a35ad18d86faacc67a16d0 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 20 Feb 2024 07:13:16 -0700 Subject: [PATCH 0664/3044] Removed group_data from parmest documentation. --- doc/OnlineDocs/contributed_packages/parmest/driver.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/parmest/driver.rst b/doc/OnlineDocs/contributed_packages/parmest/driver.rst index 28238928b83..e8d2fcd44e5 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/driver.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/driver.rst @@ -42,7 +42,6 @@ results, and fit distributions to theta values. .. autosummary:: :nosignatures: - ~pyomo.contrib.parmest.parmest.group_data ~pyomo.contrib.parmest.graphics.pairwise_plot ~pyomo.contrib.parmest.graphics.grouped_boxplot ~pyomo.contrib.parmest.graphics.grouped_violinplot From ff5077b202bcdccf8d4175d8e0afdcce6812aabd Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 20 Feb 2024 08:34:36 -0700 Subject: [PATCH 0665/3044] Fixed parmest documentation to use examples with new UI. --- .../contributed_packages/parmest/datarec.rst | 13 ++++---- .../contributed_packages/parmest/driver.rst | 30 +++++++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/parmest/datarec.rst b/doc/OnlineDocs/contributed_packages/parmest/datarec.rst index 6b721377e46..a3ece17190c 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/datarec.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/datarec.rst @@ -38,9 +38,7 @@ is the response function that is defined in the model file): >>> import pandas as pd >>> import pyomo.contrib.parmest.parmest as parmest - >>> from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import rooney_biegler_model - - >>> theta_names = ['asymptote', 'rate_constant'] + >>> from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import RooneyBieglerExperiment >>> data = pd.DataFrame(data=[[1,8.3],[2,10.3],[3,19.0], ... [4,16.0],[5,15.6],[7,19.8]], @@ -51,8 +49,13 @@ is the response function that is defined in the model file): ... - model.response_function[data.hour[i]])**2 for i in data.index) ... return expr - >>> pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE, - ... solver_options=None) + >>> def SSE(model): + ... expr = (model.experiment_outputs[model.y] + ... - model.response_function[model.experiment_outputs[model.hour]] + ... ) ** 2 + return expr + + >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=None) >>> obj, theta, var_values = pest.theta_est(return_values=['response_function']) >>> #print(var_values) diff --git a/doc/OnlineDocs/contributed_packages/parmest/driver.rst b/doc/OnlineDocs/contributed_packages/parmest/driver.rst index e8d2fcd44e5..45533e9520c 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/driver.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/driver.rst @@ -57,21 +57,33 @@ Section. .. testsetup:: * :skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available + # Data import pandas as pd - from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import rooney_biegler_model as model_function - data = pd.DataFrame(data=[[1,8.3],[2,10.3],[3,19.0], - [4,16.0],[5,15.6],[6,19.8]], - columns=['hour', 'y']) - theta_names = ['asymptote', 'rate_constant'] - def objective_function(model, data): - expr = sum((data.y[i] - model.response_function[data.hour[i]])**2 for i in data.index) + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], + [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + # Sum of squared error function + def SSE(model): + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 return expr + # Create an experiment list + from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import RooneyBieglerExperiment + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) + .. doctest:: :skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available >>> import pyomo.contrib.parmest.parmest as parmest - >>> pest = parmest.Estimator(model_function, data, theta_names, objective_function) + >>> pest = parmest.Estimator(exp_list, obj_function=SSE) Optionally, solver options can be supplied, e.g., @@ -79,7 +91,7 @@ Optionally, solver options can be supplied, e.g., :skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available >>> solver_options = {"max_iter": 6000} - >>> pest = parmest.Estimator(model_function, data, theta_names, objective_function, solver_options) + >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=solver_options) From abbda5d91cf895e9342fd6891bc8508d1daa9921 Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 20 Feb 2024 09:03:10 -0700 Subject: [PATCH 0666/3044] Fixed parmest datarec documentation for new UI. --- .../contributed_packages/parmest/datarec.rst | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/parmest/datarec.rst b/doc/OnlineDocs/contributed_packages/parmest/datarec.rst index a3ece17190c..6e9be904286 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/datarec.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/datarec.rst @@ -40,20 +40,22 @@ is the response function that is defined in the model file): >>> import pyomo.contrib.parmest.parmest as parmest >>> from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import RooneyBieglerExperiment + >>> # Generate data >>> data = pd.DataFrame(data=[[1,8.3],[2,10.3],[3,19.0], ... [4,16.0],[5,15.6],[7,19.8]], ... columns=['hour', 'y']) - >>> def SSE(model, data): - ... expr = sum((data.y[i]\ - ... - model.response_function[data.hour[i]])**2 for i in data.index) - ... return expr + >>> # Create an experiment list + >>> exp_list = [] + >>> for i in range(data.shape[0]): + ... exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) + >>> # Define objective >>> def SSE(model): ... expr = (model.experiment_outputs[model.y] ... - model.response_function[model.experiment_outputs[model.hour]] ... ) ** 2 - return expr + ... return expr >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=None) >>> obj, theta, var_values = pest.theta_est(return_values=['response_function']) From 54c3ab197a4aee657f5ea161991fd8277b04b033 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 08:49:07 -0700 Subject: [PATCH 0667/3044] Catch an edge case assigning new numeric types to Var/Param with units --- pyomo/core/base/param.py | 25 ++++++++++++++++++++----- pyomo/core/base/var.py | 15 ++++++++++----- pyomo/core/tests/unit/test_numvalue.py | 8 +++++++- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 03d700140e8..fc77c7b6f8f 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -162,16 +162,31 @@ def set_value(self, value, idx=NOTSET): # required to be mutable. # _comp = self.parent_component() - if type(value) in native_types: + if value.__class__ in native_types: # TODO: warn/error: check if this Param has units: assigning # a dimensionless value to a united param should be an error pass elif _comp._units is not None: _src_magnitude = expr_value(value) - _src_units = units.get_units(value) - value = units.convert_value( - num_value=_src_magnitude, from_units=_src_units, to_units=_comp._units - ) + # Note: expr_value() could have just registered a new numeric type + if value.__class__ in native_types: + value = _src_magnitude + else: + _src_units = units.get_units(value) + value = units.convert_value( + num_value=_src_magnitude, + from_units=_src_units, + to_units=_comp._units, + ) + # FIXME: we should call value() here [to ensure types get + # registered], but doing so breks non-numeric Params (which we + # allow). The real fix will be to follow the precedent from + # GetItemExpressiona and have separate types based on which + # expression "system" the Param should participate in (numeric, + # logical, or structural). + # + # else: + # value = expr_value(value) old_value, self._value = self._value, value try: diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index d03fd0b677f..f426c9c4f55 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -384,17 +384,22 @@ def set_value(self, val, skip_validation=False): # # Check if this Var has units: assigning dimensionless # values to a variable with units should be an error - if type(val) not in native_numeric_types: - if self.parent_component()._units is not None: - _src_magnitude = value(val) + if val.__class__ in native_numeric_types: + pass + elif self.parent_component()._units is not None: + _src_magnitude = value(val) + # Note: value() could have just registered a new numeric type + if val.__class__ in native_numeric_types: + val = _src_magnitude + else: _src_units = units.get_units(val) val = units.convert_value( num_value=_src_magnitude, from_units=_src_units, to_units=self.parent_component()._units, ) - else: - val = value(val) + else: + val = value(val) if not skip_validation: if val not in self.domain: diff --git a/pyomo/core/tests/unit/test_numvalue.py b/pyomo/core/tests/unit/test_numvalue.py index eceab3a42d9..bd784d655e8 100644 --- a/pyomo/core/tests/unit/test_numvalue.py +++ b/pyomo/core/tests/unit/test_numvalue.py @@ -562,7 +562,8 @@ def test_numpy_basic_bool_registration(self): @unittest.skipUnless(numpy_available, "This test requires NumPy") def test_automatic_numpy_registration(self): cmd = ( - 'import pyomo; from pyomo.core.base import Var, Param; import numpy as np; ' + 'import pyomo; from pyomo.core.base import Var, Param; ' + 'from pyomo.core.base.units_container import units; import numpy as np; ' 'print(np.float64 in pyomo.common.numeric_types.native_numeric_types); ' '%s; print(np.float64 in pyomo.common.numeric_types.native_numeric_types)' ) @@ -582,6 +583,11 @@ def _tester(expr): _tester('Var() + np.float64(5)') _tester('v = Var(); v.construct(); v.value = np.float64(5)') _tester('p = Param(mutable=True); p.construct(); p.value = np.float64(5)') + _tester('v = Var(units=units.m); v.construct(); v.value = np.float64(5)') + _tester( + 'p = Param(mutable=True, units=units.m); p.construct(); ' + 'p.value = np.float64(5)' + ) if __name__ == "__main__": From 0ad34438220112d6ac9213bd789fe679d50770f0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 10:12:47 -0700 Subject: [PATCH 0668/3044] Catch numpy.bool_ in the simple_constraint_rule decorator --- pyomo/core/base/constraint.py | 33 ++++++++++++++-------------- pyomo/core/base/indexed_component.py | 11 ++++++---- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index c67236656be..f3f0681d0fe 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -27,6 +27,7 @@ as_numeric, is_fixed, native_numeric_types, + native_logical_types, native_types, ) from pyomo.core.expr import ( @@ -84,14 +85,14 @@ def C_rule(model, i, j): model.c = Constraint(rule=simple_constraint_rule(...)) """ - return rule_wrapper( - rule, - { - None: Constraint.Skip, - True: Constraint.Feasible, - False: Constraint.Infeasible, - }, - ) + result_map = {None: Constraint.Skip} + for l_type in native_logical_types: + result_map[l_type(True)] = Constraint.Feasible + result_map[l_type(False)] = Constraint.Infeasible + # Note: some logical types has the same as bool (e.g., np.bool_), so + # we will pass the set of all logical types in addition to the + # result_map + return rule_wrapper(rule, result_map, map_types=native_logical_types) def simple_constraintlist_rule(rule): @@ -109,14 +110,14 @@ def C_rule(model, i, j): model.c = ConstraintList(expr=simple_constraintlist_rule(...)) """ - return rule_wrapper( - rule, - { - None: ConstraintList.End, - True: Constraint.Feasible, - False: Constraint.Infeasible, - }, - ) + result_map = {None: ConstraintList.End} + for l_type in native_logical_types: + result_map[l_type(True)] = Constraint.Feasible + result_map[l_type(False)] = Constraint.Infeasible + # Note: some logical types has the same as bool (e.g., np.bool_), so + # we will pass the set of all logical types in addition to the + # result_map + return rule_wrapper(rule, result_map, map_types=native_logical_types) # diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index abb29580960..0d498da091d 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -160,9 +160,12 @@ def _get_indexed_component_data_name(component, index): """ -def rule_result_substituter(result_map): +def rule_result_substituter(result_map, map_types): _map = result_map - _map_types = set(type(key) for key in result_map) + if map_types is None: + _map_types = set(type(key) for key in result_map) + else: + _map_types = map_types def rule_result_substituter_impl(rule, *args, **kwargs): if rule.__class__ in _map_types: @@ -203,7 +206,7 @@ def rule_result_substituter_impl(rule, *args, **kwargs): """ -def rule_wrapper(rule, wrapping_fcn, positional_arg_map=None): +def rule_wrapper(rule, wrapping_fcn, positional_arg_map=None, map_types=None): """Wrap a rule with another function This utility method provides a way to wrap a function (rule) with @@ -230,7 +233,7 @@ def rule_wrapper(rule, wrapping_fcn, positional_arg_map=None): """ if isinstance(wrapping_fcn, dict): - wrapping_fcn = rule_result_substituter(wrapping_fcn) + wrapping_fcn = rule_result_substituter(wrapping_fcn, map_types) if not inspect.isfunction(rule): return wrapping_fcn(rule) # Because some of our processing of initializer functions relies on From 044f8476abfad0c4c3e1f3bc9d12235b22d80e62 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Feb 2024 10:21:51 -0700 Subject: [PATCH 0669/3044] Apply @blnicho 's csuggestions --- .../developer_reference/solvers.rst | 44 +++++++++++++++---- pyomo/__future__.py | 2 +- pyomo/contrib/solver/base.py | 2 +- pyomo/contrib/solver/config.py | 2 +- pyomo/contrib/solver/gurobi.py | 2 +- pyomo/contrib/solver/persistent.py | 10 ++--- pyomo/contrib/solver/sol_reader.py | 4 +- pyomo/contrib/solver/tests/unit/test_base.py | 8 ++-- 8 files changed, 51 insertions(+), 23 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 921e452004d..237bc7e523b 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -34,7 +34,7 @@ available are: Backwards Compatible Mode ^^^^^^^^^^^^^^^^^^^^^^^^^ -.. code-block:: python +.. testcode:: import pyomo.environ as pyo from pyomo.contrib.solver.util import assert_optimal_termination @@ -52,13 +52,20 @@ Backwards Compatible Mode assert_optimal_termination(status) model.pprint() +.. testoutput:: + :hide: + + 2 Var Declarations + ... + 3 Declarations: x y obj + Future Capability Mode ^^^^^^^^^^^^^^^^^^^^^^ -There are multiple ways to utilize the future compatibility mode: direct import +There are multiple ways to utilize the future capability mode: direct import or changed ``SolverFactory`` version. -.. code-block:: python +.. testcode:: # Direct import import pyomo.environ as pyo @@ -81,9 +88,16 @@ or changed ``SolverFactory`` version. status.display() model.pprint() +.. testoutput:: + :hide: + + solution_loader: ... + ... + 3 Declarations: x y obj + Changing the ``SolverFactory`` version: -.. code-block:: python +.. testcode:: # Change SolverFactory version import pyomo.environ as pyo @@ -105,6 +119,18 @@ Changing the ``SolverFactory`` version: status.display() model.pprint() +.. testoutput:: + :hide: + + solution_loader: ... + ... + 3 Declarations: x y obj + +.. testcode:: + :hide: + + from pyomo.__future__ import solver_factory_v1 + Linear Presolve and Scaling ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -118,11 +144,13 @@ options for certain solvers. Currently, these options are only available for The ``writer_config`` configuration option can be used to manipulate presolve and scaling options: -.. code-block:: python +.. testcode:: + + from pyomo.contrib.solver.ipopt import Ipopt + opt = Ipopt() + opt.config.writer_config.display() - >>> from pyomo.contrib.solver.ipopt import Ipopt - >>> opt = Ipopt() - >>> opt.config.writer_config.display() +.. testoutput:: show_section_timing: false skip_trivial_constraints: true diff --git a/pyomo/__future__.py b/pyomo/__future__.py index 87b1d4e77b3..d298e12cab6 100644 --- a/pyomo/__future__.py +++ b/pyomo/__future__.py @@ -43,7 +43,7 @@ def solver_factory(version=None): This allows users to query / set the current implementation of the SolverFactory that should be used throughout Pyomo. Valid options are: - - ``1``: the original Pyomo SolverFactor + - ``1``: the original Pyomo SolverFactory - ``2``: the SolverFactory from APPSI - ``3``: the SolverFactory from pyomo.contrib.solver diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index cb13809c438..3bfa83050ad 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -288,7 +288,7 @@ def add_variables(self, variables: List[_GeneralVarData]): """ @abc.abstractmethod - def add_params(self, params: List[_ParamData]): + def add_parameters(self, params: List[_ParamData]): """ Add parameters to the model """ diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index ca9557d0002..0c86f7646d3 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -234,7 +234,7 @@ def __init__( default=True, description=""" If False, new/old parameters will not be automatically detected on subsequent - solves. Use False only when manually updating the solver with opt.add_params() and + solves. Use False only when manually updating the solver with opt.add_parameters() and opt.remove_params() or when you are certain parameters are not being added to / removed from the model.""", ), diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 85131ba73bd..ad476b9261e 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -475,7 +475,7 @@ def _add_variables(self, variables: List[_GeneralVarData]): self._vars_added_since_update.update(variables) self._needs_updated = True - def _add_params(self, params: List[_ParamData]): + def _add_parameters(self, params: List[_ParamData]): pass def _reinit(self): diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index 0994aa53093..e389e5d4019 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -75,13 +75,13 @@ def add_variables(self, variables: List[_GeneralVarData]): self._add_variables(variables) @abc.abstractmethod - def _add_params(self, params: List[_ParamData]): + def _add_parameters(self, params: List[_ParamData]): pass - def add_params(self, params: List[_ParamData]): + def add_parameters(self, params: List[_ParamData]): for p in params: self._params[id(p)] = p - self._add_params(params) + self._add_parameters(params) @abc.abstractmethod def _add_constraints(self, cons: List[_GeneralConstraintData]): @@ -191,7 +191,7 @@ def add_block(self, block): if p.mutable: for _p in p.values(): param_dict[id(_p)] = _p - self.add_params(list(param_dict.values())) + self.add_parameters(list(param_dict.values())) self.add_constraints( list( block.component_data_objects(Constraint, descend_into=True, active=True) @@ -403,7 +403,7 @@ def update(self, timer: HierarchicalTimer = None): if config.update_params: self.update_params() - self.add_params(new_params) + self.add_parameters(new_params) timer.stop('params') timer.start('vars') self.add_variables(new_vars) diff --git a/pyomo/contrib/solver/sol_reader.py b/pyomo/contrib/solver/sol_reader.py index 2817dab4516..41d840f8d07 100644 --- a/pyomo/contrib/solver/sol_reader.py +++ b/pyomo/contrib/solver/sol_reader.py @@ -36,7 +36,7 @@ def parse_sol_file( # # Some solvers (minto) do not write a message. We will assume - # all non-blank lines up the 'Options' line is the message. + # all non-blank lines up to the 'Options' line is the message. # For backwards compatibility and general safety, we will parse all # lines until "Options" appears. Anything before "Options" we will # consider to be the solver message. @@ -168,7 +168,7 @@ def parse_sol_file( # The fourth entry is table "length", e.g., memory size. number_of_string_lines = int(line[5]) suffix_name = sol_file.readline().strip() - # Add any of arbitrary string lines to the "other" list + # Add any arbitrary string lines to the "other" list for line in range(number_of_string_lines): sol_data.other.append(sol_file.readline()) if data_type == 0: # Var diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 5fecd012cda..a9b3e4f4711 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -92,7 +92,7 @@ def test_abstract_member_list(self): 'remove_block', 'add_block', 'available', - 'add_params', + 'add_parameters', 'remove_constraints', 'add_variables', 'solve', @@ -110,7 +110,7 @@ def test_class_method_list(self): '_load_vars', 'add_block', 'add_constraints', - 'add_params', + 'add_parameters', 'add_variables', 'available', 'is_persistent', @@ -138,7 +138,7 @@ def test_init(self): self.assertTrue(self.instance.is_persistent()) self.assertEqual(self.instance.set_instance(None), None) self.assertEqual(self.instance.add_variables(None), None) - self.assertEqual(self.instance.add_params(None), None) + self.assertEqual(self.instance.add_parameters(None), None) self.assertEqual(self.instance.add_constraints(None), None) self.assertEqual(self.instance.add_block(None), None) self.assertEqual(self.instance.remove_variables(None), None) @@ -164,7 +164,7 @@ def test_context_manager(self): self.assertTrue(self.instance.is_persistent()) self.assertEqual(self.instance.set_instance(None), None) self.assertEqual(self.instance.add_variables(None), None) - self.assertEqual(self.instance.add_params(None), None) + self.assertEqual(self.instance.add_parameters(None), None) self.assertEqual(self.instance.add_constraints(None), None) self.assertEqual(self.instance.add_block(None), None) self.assertEqual(self.instance.remove_variables(None), None) From 6cb1180ed79aefbbf3dffb5623621cda05934785 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 10:42:25 -0700 Subject: [PATCH 0670/3044] NFC: updating comments --- pyomo/common/collections/component_map.py | 2 +- pyomo/common/collections/component_set.py | 1 + pyomo/common/collections/orderedset.py | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index c110e9f390b..caeabb1e650 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -80,7 +80,7 @@ class ComponentMap(AutoSlots.Mixin, collections.abc.MutableMapping): __autoslot_mappers__ = {'_dict': _rehash_keys} def __init__(self, *args, **kwds): - # maps id(obj) -> (obj,val) + # maps id_hash(obj) -> (obj,val) self._dict = {} # handle the dict-style initialization scenarios self.update(*args, **kwds) diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index 19d2ef2f7f9..5e9d794ff8e 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -63,6 +63,7 @@ class ComponentSet(AutoSlots.Mixin, collections_MutableSet): __autoslot_mappers__ = {'_data': _rehash_keys} def __init__(self, iterable=None): + # maps id_hash(obj) -> obj self._data = {} if iterable is not None: self.update(iterable) diff --git a/pyomo/common/collections/orderedset.py b/pyomo/common/collections/orderedset.py index 6bcf0c2fafb..834101e3896 100644 --- a/pyomo/common/collections/orderedset.py +++ b/pyomo/common/collections/orderedset.py @@ -18,8 +18,8 @@ class OrderedSet(AutoSlots.Mixin, MutableSet): __slots__ = ('_dict',) def __init__(self, iterable=None): - # TODO: Starting in Python 3.7, dict is ordered (and is faster - # than OrderedDict). dict began supporting reversed() in 3.8. + # Starting in Python 3.7, dict is ordered (and is faster than + # OrderedDict). dict began supporting reversed() in 3.8. self._dict = {} if iterable is not None: self.update(iterable) From f3ded2787e18757a2ec5ff0330c64acd338b45fd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 10:42:49 -0700 Subject: [PATCH 0671/3044] Clean up exception messages / string representation --- pyomo/common/collections/component_map.py | 8 ++++---- pyomo/common/collections/component_set.py | 8 +++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pyomo/common/collections/component_map.py b/pyomo/common/collections/component_map.py index caeabb1e650..8dcfdb6c837 100644 --- a/pyomo/common/collections/component_map.py +++ b/pyomo/common/collections/component_map.py @@ -87,8 +87,8 @@ def __init__(self, *args, **kwds): def __str__(self): """String representation of the mapping.""" - tmp = {str(c) + " (id=" + str(id(c)) + ")": v for c, v in self.items()} - return "ComponentMap(" + str(tmp) + ")" + tmp = {f"{v[0]} (key={k})": v[1] for k, v in self._dict.items()} + return f"ComponentMap({tmp})" # # Implement MutableMapping abstract methods @@ -99,7 +99,7 @@ def __getitem__(self, obj): return self._dict[_hasher[obj.__class__](obj)][1] except KeyError: _id = _hasher[obj.__class__](obj) - raise KeyError("Component with id '%s': %s" % (_id, obj)) + raise KeyError(f"{obj} (key={_id})") from None def __setitem__(self, obj, val): self._dict[_hasher[obj.__class__](obj)] = (obj, val) @@ -109,7 +109,7 @@ def __delitem__(self, obj): del self._dict[_hasher[obj.__class__](obj)] except KeyError: _id = _hasher[obj.__class__](obj) - raise KeyError("Component with id '%s': %s" % (_id, obj)) + raise KeyError(f"{obj} (key={_id})") from None def __iter__(self): return (obj for obj, val in self._dict.values()) diff --git a/pyomo/common/collections/component_set.py b/pyomo/common/collections/component_set.py index 5e9d794ff8e..6e12bad7277 100644 --- a/pyomo/common/collections/component_set.py +++ b/pyomo/common/collections/component_set.py @@ -70,10 +70,8 @@ def __init__(self, iterable=None): def __str__(self): """String representation of the mapping.""" - tmp = [] - for objid, obj in self._data.items(): - tmp.append(str(obj) + " (id=" + str(objid) + ")") - return "ComponentSet(" + str(tmp) + ")" + tmp = [f"{v} (key={k})" for k, v in self._data.items()] + return f"ComponentSet({tmp})" def update(self, iterable): """Update a set with the union of itself and others.""" @@ -136,4 +134,4 @@ def remove(self, val): del self._data[_hasher[val.__class__](val)] except KeyError: _id = _hasher[val.__class__](val) - raise KeyError("Component with id '%s': %s" % (_id, val)) + raise KeyError(f"{val} (key={_id})") from None From c9a9f0d5132da90cecb43278dd2697d0389c2189 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 10:55:27 -0700 Subject: [PATCH 0672/3044] Ensure NoneType is in the map_types --- pyomo/core/base/constraint.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index f3f0681d0fe..108ff7383c3 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -85,6 +85,7 @@ def C_rule(model, i, j): model.c = Constraint(rule=simple_constraint_rule(...)) """ + map_types = set([type(None)]) | native_logical_types result_map = {None: Constraint.Skip} for l_type in native_logical_types: result_map[l_type(True)] = Constraint.Feasible @@ -92,7 +93,7 @@ def C_rule(model, i, j): # Note: some logical types has the same as bool (e.g., np.bool_), so # we will pass the set of all logical types in addition to the # result_map - return rule_wrapper(rule, result_map, map_types=native_logical_types) + return rule_wrapper(rule, result_map, map_types=map_types) def simple_constraintlist_rule(rule): @@ -110,6 +111,7 @@ def C_rule(model, i, j): model.c = ConstraintList(expr=simple_constraintlist_rule(...)) """ + map_types = set([type(None)]) | native_logical_types result_map = {None: ConstraintList.End} for l_type in native_logical_types: result_map[l_type(True)] = Constraint.Feasible @@ -117,7 +119,7 @@ def C_rule(model, i, j): # Note: some logical types has the same as bool (e.g., np.bool_), so # we will pass the set of all logical types in addition to the # result_map - return rule_wrapper(rule, result_map, map_types=native_logical_types) + return rule_wrapper(rule, result_map, map_types=map_types) # From 7ecdcc930c9f4d89777412da5915eadf78d70330 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 10:55:50 -0700 Subject: [PATCH 0673/3044] NFC: fix comment --- pyomo/core/base/constraint.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 108ff7383c3..8cf3c48ad0a 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -90,7 +90,7 @@ def C_rule(model, i, j): for l_type in native_logical_types: result_map[l_type(True)] = Constraint.Feasible result_map[l_type(False)] = Constraint.Infeasible - # Note: some logical types has the same as bool (e.g., np.bool_), so + # Note: some logical types hash the same as bool (e.g., np.bool_), so # we will pass the set of all logical types in addition to the # result_map return rule_wrapper(rule, result_map, map_types=map_types) @@ -116,7 +116,7 @@ def C_rule(model, i, j): for l_type in native_logical_types: result_map[l_type(True)] = Constraint.Feasible result_map[l_type(False)] = Constraint.Infeasible - # Note: some logical types has the same as bool (e.g., np.bool_), so + # Note: some logical types hash the same as bool (e.g., np.bool_), so # we will pass the set of all logical types in addition to the # result_map return rule_wrapper(rule, result_map, map_types=map_types) From 423b1412fb4e2696b1a976a06793957505320dee Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Feb 2024 11:34:35 -0700 Subject: [PATCH 0674/3044] Add skip statement to doctests; start ipopt unit tests --- .../developer_reference/solvers.rst | 7 ++ pyomo/contrib/solver/tests/unit/test_ipopt.py | 73 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 pyomo/contrib/solver/tests/unit/test_ipopt.py diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 237bc7e523b..45945c18b12 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -35,6 +35,7 @@ Backwards Compatible Mode ^^^^^^^^^^^^^^^^^^^^^^^^^ .. testcode:: + :skipif: not ipopt_available import pyomo.environ as pyo from pyomo.contrib.solver.util import assert_optimal_termination @@ -53,6 +54,7 @@ Backwards Compatible Mode model.pprint() .. testoutput:: + :skipif: not ipopt_available :hide: 2 Var Declarations @@ -66,6 +68,7 @@ There are multiple ways to utilize the future capability mode: direct import or changed ``SolverFactory`` version. .. testcode:: + :skipif: not ipopt_available # Direct import import pyomo.environ as pyo @@ -89,6 +92,7 @@ or changed ``SolverFactory`` version. model.pprint() .. testoutput:: + :skipif: not ipopt_available :hide: solution_loader: ... @@ -98,6 +102,7 @@ or changed ``SolverFactory`` version. Changing the ``SolverFactory`` version: .. testcode:: + :skipif: not ipopt_available # Change SolverFactory version import pyomo.environ as pyo @@ -120,6 +125,7 @@ Changing the ``SolverFactory`` version: model.pprint() .. testoutput:: + :skipif: not ipopt_available :hide: solution_loader: ... @@ -127,6 +133,7 @@ Changing the ``SolverFactory`` version: 3 Declarations: x y obj .. testcode:: + :skipif: not ipopt_available :hide: from pyomo.__future__ import solver_factory_v1 diff --git a/pyomo/contrib/solver/tests/unit/test_ipopt.py b/pyomo/contrib/solver/tests/unit/test_ipopt.py new file mode 100644 index 00000000000..2ddcce2e456 --- /dev/null +++ b/pyomo/contrib/solver/tests/unit/test_ipopt.py @@ -0,0 +1,73 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest, Executable +from pyomo.repn.plugins.nl_writer import NLWriter +from pyomo.contrib.solver import ipopt + + +ipopt_available = ipopt.Ipopt().available() + + +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestIpoptSolverConfig(unittest.TestCase): + def test_default_instantiation(self): + config = ipopt.IpoptConfig() + # Should be inherited + self.assertIsNone(config._description) + self.assertEqual(config._visibility, 0) + self.assertFalse(config.tee) + self.assertTrue(config.load_solutions) + self.assertTrue(config.raise_exception_on_nonoptimal_result) + self.assertFalse(config.symbolic_solver_labels) + self.assertIsNone(config.timer) + self.assertIsNone(config.threads) + self.assertIsNone(config.time_limit) + # Unique to this object + self.assertIsInstance(config.executable, type(Executable('path'))) + self.assertIsInstance(config.writer_config, type(NLWriter.CONFIG())) + + def test_custom_instantiation(self): + config = ipopt.IpoptConfig(description="A description") + config.tee = True + self.assertTrue(config.tee) + self.assertEqual(config._description, "A description") + self.assertFalse(config.time_limit) + # Default should be `ipopt` + self.assertIsNotNone(str(config.executable)) + self.assertIn('ipopt', str(config.executable)) + # Set to a totally bogus path + config.executable = Executable('/bogus/path') + self.assertIsNone(config.executable.executable) + self.assertFalse(config.executable.available()) + + +class TestIpoptResults(unittest.TestCase): + def test_default_instantiation(self): + res = ipopt.IpoptResults() + # Inherited methods/attributes + self.assertIsNone(res.solution_loader) + self.assertIsNone(res.incumbent_objective) + self.assertIsNone(res.objective_bound) + self.assertIsNone(res.solver_name) + self.assertIsNone(res.solver_version) + self.assertIsNone(res.iteration_count) + self.assertIsNone(res.timing_info.start_timestamp) + self.assertIsNone(res.timing_info.wall_time) + # Unique to this object + self.assertIsNone(res.timing_info.ipopt_excluding_nlp_functions) + self.assertIsNone(res.timing_info.nlp_function_evaluations) + self.assertIsNone(res.timing_info.total_seconds) + + +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestIpoptInterface(unittest.TestCase): + pass From 924c38aebb82d2a72ba340df39f2948116fdc861 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 20 Feb 2024 11:43:00 -0700 Subject: [PATCH 0675/3044] NFC: Fixing typos in comments in param.py --- pyomo/core/base/param.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index fc77c7b6f8f..3ef33b9ee45 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -179,9 +179,9 @@ def set_value(self, value, idx=NOTSET): to_units=_comp._units, ) # FIXME: we should call value() here [to ensure types get - # registered], but doing so breks non-numeric Params (which we + # registered], but doing so breaks non-numeric Params (which we # allow). The real fix will be to follow the precedent from - # GetItemExpressiona and have separate types based on which + # GetItemExpression and have separate types based on which # expression "system" the Param should participate in (numeric, # logical, or structural). # From d0cdeaab066b35f164a9a05de01c580284559405 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 12:11:34 -0700 Subject: [PATCH 0676/3044] Consolidate log_solevr_output into tee --- pyomo/contrib/solver/config.py | 43 +++++++++++++++------ pyomo/contrib/solver/gurobi.py | 68 ++++++++++++++++------------------ pyomo/contrib/solver/ipopt.py | 7 +--- 3 files changed, 64 insertions(+), 54 deletions(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 0c86f7646d3..7ca9ac104ae 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -9,6 +9,11 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import io +import logging +import sys + +from collections.abc import Sequence from typing import Optional from pyomo.common.config import ( @@ -19,9 +24,29 @@ ADVANCED_OPTION, Bool, ) +from pyomo.common.log import LogStream +from pyomo.common.numeric_types import native_logical_types from pyomo.common.timing import HierarchicalTimer +def TextIO_or_Logger(val): + ans = [] + if not isinstance(val, Sequence): + val = [val] + for v in val: + if v.__class__ in native_logical_types: + if v: + ans.append(sys.stdout) + elif isinstance(v, io.TextIOBase): + ans.append(v) + elif isinstance(v, logging.Logger): + ans.append(LogStream(level=logging.INFO, logger=v)) + else: + raise ValueError( + "Expected bool, TextIOBase, or Logger, but received {v.__class__}" + ) + return ans + class SolverConfig(ConfigDict): """ Base config for all direct solver interfaces @@ -43,20 +68,16 @@ def __init__( visibility=visibility, ) - self.tee: bool = self.declare( + self.tee: List[TextIO] = self.declare( 'tee', ConfigValue( - domain=Bool, - default=False, - description="If True, the solver log prints to stdout.", - ), - ) - self.log_solver_output: bool = self.declare( - 'log_solver_output', - ConfigValue( - domain=Bool, + domain=TextIO_or_Logger, default=False, - description="If True, the solver output gets logged.", + description="""`tee` accepts :py:class:`bool`, + :py:class:`io.TextIOBase`, or :py:class:`logging.Logger` + (or a list of these types). ``True`` is mapped to + ``sys.stdout``. The solver log will be printed to each of + these streams / destinations. """, ), ) self.working_dir: Optional[str] = self.declare( diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index ad476b9261e..63387730c45 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -14,7 +14,6 @@ import math from typing import List, Optional from pyomo.common.collections import ComponentSet, ComponentMap, OrderedSet -from pyomo.common.log import LogStream from pyomo.common.dependencies import attempt_import from pyomo.common.errors import PyomoException from pyomo.common.tee import capture_output, TeeStream @@ -326,42 +325,37 @@ def symbol_map(self): def _solve(self): config = self._config timer = config.timer - ostreams = [io.StringIO()] - if config.tee: - ostreams.append(sys.stdout) - if config.log_solver_output: - ostreams.append(LogStream(level=logging.INFO, logger=logger)) - - with TeeStream(*ostreams) as t: - with capture_output(output=t.STDOUT, capture_fd=False): - options = config.solver_options - - self._solver_model.setParam('LogToConsole', 1) - - if config.threads is not None: - self._solver_model.setParam('Threads', config.threads) - if config.time_limit is not None: - self._solver_model.setParam('TimeLimit', config.time_limit) - if config.rel_gap is not None: - self._solver_model.setParam('MIPGap', config.rel_gap) - if config.abs_gap is not None: - self._solver_model.setParam('MIPGapAbs', config.abs_gap) - - if config.use_mipstart: - for ( - pyomo_var_id, - gurobi_var, - ) in self._pyomo_var_to_solver_var_map.items(): - pyomo_var = self._vars[pyomo_var_id][0] - if pyomo_var.is_integer() and pyomo_var.value is not None: - self.set_var_attr(pyomo_var, 'Start', pyomo_var.value) - - for key, option in options.items(): - self._solver_model.setParam(key, option) - - timer.start('optimize') - self._solver_model.optimize(self._callback) - timer.stop('optimize') + ostreams = [io.StringIO()] + config.tee + + with TeeStream(*ostreams) as t, capture_output(t.STDOUT, capture_fd=False): + options = config.solver_options + + self._solver_model.setParam('LogToConsole', 1) + + if config.threads is not None: + self._solver_model.setParam('Threads', config.threads) + if config.time_limit is not None: + self._solver_model.setParam('TimeLimit', config.time_limit) + if config.rel_gap is not None: + self._solver_model.setParam('MIPGap', config.rel_gap) + if config.abs_gap is not None: + self._solver_model.setParam('MIPGapAbs', config.abs_gap) + + if config.use_mipstart: + for ( + pyomo_var_id, + gurobi_var, + ) in self._pyomo_var_to_solver_var_map.items(): + pyomo_var = self._vars[pyomo_var_id][0] + if pyomo_var.is_integer() and pyomo_var.value is not None: + self.set_var_attr(pyomo_var, 'Start', pyomo_var.value) + + for key, option in options.items(): + self._solver_model.setParam(key, option) + + timer.start('optimize') + self._solver_model.optimize(self._callback) + timer.stop('optimize') self._needs_updated = False res = self._postsolve(timer) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 537e6f85968..38272d58fa1 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -36,7 +36,6 @@ from pyomo.contrib.solver.sol_reader import parse_sol_file from pyomo.contrib.solver.solution import SolSolutionLoader from pyomo.common.tee import TeeStream -from pyomo.common.log import LogStream from pyomo.core.expr.visitor import replace_expressions from pyomo.core.expr.numvalue import value from pyomo.core.base.suffix import Suffix @@ -390,11 +389,7 @@ def solve(self, model, **kwds): else: timeout = None - ostreams = [io.StringIO()] - if config.tee: - ostreams.append(sys.stdout) - if config.log_solver_output: - ostreams.append(LogStream(level=logging.INFO, logger=logger)) + ostreams = [io.StringIO()] + config.tee with TeeStream(*ostreams) as t: timer.start('subprocess') process = subprocess.run( From ff92c62b89cc376ef87849adb35e5b0c741eed3a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Feb 2024 12:55:00 -0700 Subject: [PATCH 0677/3044] More unit tests for ipopt; fix some bugs as well --- pyomo/contrib/solver/ipopt.py | 49 ++--- pyomo/contrib/solver/tests/unit/test_ipopt.py | 172 +++++++++++++++++- 2 files changed, 199 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 537e6f85968..42ac24ec352 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -23,7 +23,7 @@ document_kwargs_from_configdict, ConfigDict, ) -from pyomo.common.errors import PyomoException +from pyomo.common.errors import PyomoException, DeveloperError from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer from pyomo.core.base.var import _GeneralVarData @@ -137,13 +137,18 @@ def get_reduced_costs( if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' - 'check the termination condition.' + 'check results.TerminationCondition and/or results.SolutionStatus.' ) if len(self._nl_info.eliminated_vars) > 0: raise NotImplementedError( - 'For now, turn presolve off (opt.config.writer_config.linear_presolve=False) to get reduced costs.' + 'For now, turn presolve off (opt.config.writer_config.linear_presolve=False) ' + 'to get dual variable values.' + ) + if self._sol_data is None: + raise DeveloperError( + "Solution data is empty. This should not " + "have happened. Report this error to the Pyomo Developers." ) - assert self._sol_data is not None if self._nl_info.scaling is None: scale_list = [1] * len(self._nl_info.variables) obj_scale = 1 @@ -252,7 +257,6 @@ def __init__(self, **kwds): self._writer = NLWriter() self._available_cache = None self._version_cache = None - self._executable = self.config.executable def available(self, config=None): if config is None: @@ -270,17 +274,20 @@ def version(self, config=None): config = self.config pth = config.executable.path() if self._version_cache is None or self._version_cache[0] != pth: - results = subprocess.run( - [str(pth), '--version'], - timeout=1, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=True, - ) - version = results.stdout.splitlines()[0] - version = version.split(' ')[1].strip() - version = tuple(int(i) for i in version.split('.')) - self._version_cache = (pth, version) + if pth is None: + self._version_cache = (None, None) + else: + results = subprocess.run( + [str(pth), '--version'], + timeout=1, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + ) + version = results.stdout.splitlines()[0] + version = version.split(' ')[1].strip() + version = tuple(int(i) for i in version.split('.')) + self._version_cache = (pth, version) return self._version_cache[1] def _write_options_file(self, filename: str, options: Mapping): @@ -292,15 +299,15 @@ def _write_options_file(self, filename: str, options: Mapping): # If it has options in it, parse them and write them to a file. # If they are command line options, ignore them; they will be # parsed during _create_command_line - with open(filename + '.opt', 'w') as opt_file: - for k, val in options.items(): - if k not in ipopt_command_line_options: - opt_file_exists = True + for k, val in options.items(): + if k not in ipopt_command_line_options: + opt_file_exists = True + with open(filename + '.opt', 'a+') as opt_file: opt_file.write(str(k) + ' ' + str(val) + '\n') return opt_file_exists def _create_command_line(self, basename: str, config: IpoptConfig, opt_file: bool): - cmd = [str(self._executable), basename + '.nl', '-AMPL'] + cmd = [str(config.executable), basename + '.nl', '-AMPL'] if opt_file: cmd.append('option_file_name=' + basename + '.opt') if 'option_file_name' in config.solver_options: diff --git a/pyomo/contrib/solver/tests/unit/test_ipopt.py b/pyomo/contrib/solver/tests/unit/test_ipopt.py index 2ddcce2e456..ae07bd37f86 100644 --- a/pyomo/contrib/solver/tests/unit/test_ipopt.py +++ b/pyomo/contrib/solver/tests/unit/test_ipopt.py @@ -9,7 +9,11 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import os + from pyomo.common import unittest, Executable +from pyomo.common.errors import DeveloperError +from pyomo.common.tempfiles import TempfileManager from pyomo.repn.plugins.nl_writer import NLWriter from pyomo.contrib.solver import ipopt @@ -68,6 +72,172 @@ def test_default_instantiation(self): self.assertIsNone(res.timing_info.total_seconds) +class TestIpoptSolutionLoader(unittest.TestCase): + def test_get_reduced_costs_error(self): + loader = ipopt.IpoptSolutionLoader(None, None) + with self.assertRaises(RuntimeError): + loader.get_reduced_costs() + + # Set _nl_info to something completely bogus but is not None + class NLInfo: + pass + + loader._nl_info = NLInfo() + loader._nl_info.eliminated_vars = [1, 2, 3] + with self.assertRaises(NotImplementedError): + loader.get_reduced_costs() + # Reset _nl_info so we can ensure we get an error + # when _sol_data is None + loader._nl_info.eliminated_vars = [] + with self.assertRaises(DeveloperError): + loader.get_reduced_costs() + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") class TestIpoptInterface(unittest.TestCase): - pass + def test_class_member_list(self): + opt = ipopt.Ipopt() + expected_list = [ + 'Availability', + 'CONFIG', + 'config', + 'available', + 'is_persistent', + 'solve', + 'version', + 'name', + ] + method_list = [method for method in dir(opt) if method.startswith('_') is False] + self.assertEqual(sorted(expected_list), sorted(method_list)) + + def test_default_instantiation(self): + opt = ipopt.Ipopt() + self.assertFalse(opt.is_persistent()) + self.assertIsNotNone(opt.version()) + self.assertEqual(opt.name, 'ipopt') + self.assertEqual(opt.CONFIG, opt.config) + self.assertTrue(opt.available()) + + def test_context_manager(self): + with ipopt.Ipopt() as opt: + self.assertFalse(opt.is_persistent()) + self.assertIsNotNone(opt.version()) + self.assertEqual(opt.name, 'ipopt') + self.assertEqual(opt.CONFIG, opt.config) + self.assertTrue(opt.available()) + + def test_available_cache(self): + opt = ipopt.Ipopt() + opt.available() + self.assertTrue(opt._available_cache[1]) + self.assertIsNotNone(opt._available_cache[0]) + # Now we will try with a custom config that has a fake path + config = ipopt.IpoptConfig() + config.executable = Executable('/a/bogus/path') + opt.available(config=config) + self.assertFalse(opt._available_cache[1]) + self.assertIsNone(opt._available_cache[0]) + + def test_version_cache(self): + opt = ipopt.Ipopt() + opt.version() + self.assertIsNotNone(opt._version_cache[0]) + self.assertIsNotNone(opt._version_cache[1]) + # Now we will try with a custom config that has a fake path + config = ipopt.IpoptConfig() + config.executable = Executable('/a/bogus/path') + opt.version(config=config) + self.assertIsNone(opt._version_cache[0]) + self.assertIsNone(opt._version_cache[1]) + + def test_write_options_file(self): + # If we have no options, we should get false back + opt = ipopt.Ipopt() + result = opt._write_options_file('fakename', None) + self.assertFalse(result) + # Pass it some options that ARE on the command line + opt = ipopt.Ipopt(solver_options={'max_iter': 4}) + result = opt._write_options_file('myfile', opt.config.solver_options) + self.assertFalse(result) + self.assertFalse(os.path.isfile('myfile.opt')) + # Now we are going to actually pass it some options that are NOT on + # the command line + opt = ipopt.Ipopt(solver_options={'custom_option': 4}) + with TempfileManager.new_context() as temp: + dname = temp.mkdtemp() + if not os.path.exists(dname): + os.mkdir(dname) + filename = os.path.join(dname, 'myfile') + result = opt._write_options_file(filename, opt.config.solver_options) + self.assertTrue(result) + self.assertTrue(os.path.isfile(filename + '.opt')) + # Make sure all options are writing to the file + opt = ipopt.Ipopt(solver_options={'custom_option_1': 4, 'custom_option_2': 3}) + with TempfileManager.new_context() as temp: + dname = temp.mkdtemp() + if not os.path.exists(dname): + os.mkdir(dname) + filename = os.path.join(dname, 'myfile') + result = opt._write_options_file(filename, opt.config.solver_options) + self.assertTrue(result) + self.assertTrue(os.path.isfile(filename + '.opt')) + with open(filename + '.opt', 'r') as f: + data = f.readlines() + self.assertEqual(len(data), len(list(opt.config.solver_options.keys()))) + + def test_create_command_line(self): + opt = ipopt.Ipopt() + # No custom options, no file created. Plain and simple. + result = opt._create_command_line('myfile', opt.config, False) + self.assertEqual(result, [str(opt.config.executable), 'myfile.nl', '-AMPL']) + # Custom command line options + opt = ipopt.Ipopt(solver_options={'max_iter': 4}) + result = opt._create_command_line('myfile', opt.config, False) + self.assertEqual( + result, [str(opt.config.executable), 'myfile.nl', '-AMPL', 'max_iter=4'] + ) + # Let's see if we correctly parse config.time_limit + opt = ipopt.Ipopt(solver_options={'max_iter': 4}, time_limit=10) + result = opt._create_command_line('myfile', opt.config, False) + self.assertEqual( + result, + [ + str(opt.config.executable), + 'myfile.nl', + '-AMPL', + 'max_iter=4', + 'max_cpu_time=10.0', + ], + ) + # Now let's do multiple command line options + opt = ipopt.Ipopt(solver_options={'max_iter': 4, 'max_cpu_time': 10}) + result = opt._create_command_line('myfile', opt.config, False) + self.assertEqual( + result, + [ + str(opt.config.executable), + 'myfile.nl', + '-AMPL', + 'max_cpu_time=10', + 'max_iter=4', + ], + ) + # Let's now include if we "have" an options file + result = opt._create_command_line('myfile', opt.config, True) + self.assertEqual( + result, + [ + '/Users/mmundt/Documents/idaes/venv-pyomo/bin/ipopt', + 'myfile.nl', + '-AMPL', + 'option_file_name=myfile.opt', + 'max_cpu_time=10', + 'max_iter=4', + ], + ) + # Finally, let's make sure it errors if someone tries to pass option_file_name + opt = ipopt.Ipopt( + solver_options={'max_iter': 4, 'option_file_name': 'myfile.opt'} + ) + with self.assertRaises(ValueError): + result = opt._create_command_line('myfile', opt.config, False) From c2472b3cb09cf367fb711845fb9780908c028f89 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Feb 2024 13:10:27 -0700 Subject: [PATCH 0678/3044] Remove Datetime validator; replace with IsInstance --- pyomo/common/config.py | 12 ------------ pyomo/common/tests/test_config.py | 17 ----------------- pyomo/contrib/solver/config.py | 1 + pyomo/contrib/solver/results.py | 5 +++-- 4 files changed, 4 insertions(+), 31 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 46a05494094..238bdd78e9d 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -18,7 +18,6 @@ import argparse import builtins -import datetime import enum import importlib import inspect @@ -205,16 +204,6 @@ def NonNegativeFloat(val): return ans -def Datetime(val): - """Domain validation function to check for datetime.datetime type. - - This domain will return the original object, assuming it is of the right type. - """ - if not isinstance(val, datetime.datetime): - raise ValueError(f"Expected datetime object, but received {type(val)}.") - return val - - class In(object): """In(domain, cast=None) Domain validation class admitting a Container of possible values @@ -794,7 +783,6 @@ def from_enum_or_string(cls, arg): NegativeFloat NonPositiveFloat NonNegativeFloat - Datetime In InEnum IsInstance diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index e2f64a3a9d5..02f4fc88251 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -48,7 +48,6 @@ def yaml_load(arg): ConfigDict, ConfigValue, ConfigList, - Datetime, MarkImmutable, ImmutableConfigValue, Bool, @@ -937,22 +936,6 @@ def _rule(key, val): } ) - def test_Datetime(self): - c = ConfigDict() - c.declare('a', ConfigValue(domain=Datetime, default=None)) - self.assertEqual(c.get('a').domain_name(), 'Datetime') - - self.assertEqual(c.a, None) - c.a = datetime.datetime(2022, 1, 1) - self.assertEqual(c.a, datetime.datetime(2022, 1, 1)) - - with self.assertRaises(ValueError): - c.a = 5 - with self.assertRaises(ValueError): - c.a = 'Hello' - with self.assertRaises(ValueError): - c.a = False - class TestImmutableConfigValue(unittest.TestCase): def test_immutable_config_value(self): diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 7ca9ac104ae..8f715ac7250 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -47,6 +47,7 @@ def TextIO_or_Logger(val): ) return ans + class SolverConfig(ConfigDict): """ Base config for all direct solver interfaces diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index 88de0624629..699137d2fc9 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -16,7 +16,7 @@ from pyomo.common.config import ( ConfigDict, ConfigValue, - Datetime, + IsInstance, NonNegativeInt, In, NonNegativeFloat, @@ -262,7 +262,8 @@ def __init__( self.timing_info.start_timestamp: datetime = self.timing_info.declare( 'start_timestamp', ConfigValue( - domain=Datetime, description="UTC timestamp of when run was initiated." + domain=IsInstance(datetime), + description="UTC timestamp of when run was initiated.", ), ) self.timing_info.wall_time: Optional[float] = self.timing_info.declare( From d1549d69c21c754b9ffe2c0a2f60d5aafc2e70a6 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 13:21:50 -0700 Subject: [PATCH 0679/3044] Not checking isinstance when we check ctype in util --- pyomo/gdp/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index 0e4e5f5e9ff..57eef29eded 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -169,7 +169,7 @@ def parent_disjunct(self, u): Arg: u : A node in the forest """ - if isinstance(u, _DisjunctData) or u.ctype is Disjunct: + if u.ctype is Disjunct: return self.parent(self.parent(u)) else: return self.parent(u) @@ -186,7 +186,7 @@ def root_disjunct(self, u): while True: if parent is None: return rootmost_disjunct - if isinstance(parent, _DisjunctData) or parent.ctype is Disjunct: + if parent.ctype is Disjunct: rootmost_disjunct = parent parent = self.parent(parent) @@ -246,7 +246,7 @@ def leaves(self): @property def disjunct_nodes(self): for v in self._vertices: - if isinstance(v, _DisjunctData) or v.ctype is Disjunct: + if v.ctype is Disjunct: yield v From 06621a75a2151076d90ee7b9a91b9794fdbac29b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 13:22:45 -0700 Subject: [PATCH 0680/3044] Not checking isinstance when we check ctype in hull --- pyomo/gdp/plugins/hull.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index a70aa2760eb..b2e1ffd76fd 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -212,7 +212,7 @@ def _get_user_defined_local_vars(self, targets): # we cache what Blocks/Disjuncts we've already looked on so that we # don't duplicate effort. for t in targets: - if t.ctype is Disjunct or isinstance(t, _DisjunctData): + if t.ctype is Disjunct: # first look beneath where we are (there could be Blocks on this # disjunct) for b in t.component_data_objects( From 3435aa1a82fcfec68227faba0918f0629680e699 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 13:24:16 -0700 Subject: [PATCH 0681/3044] Prettier defaultdicts --- pyomo/gdp/plugins/hull.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index b2e1ffd76fd..5a3349a8b34 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -206,7 +206,7 @@ def _collect_local_vars_from_block(self, block, local_var_dict): local_var_dict[disj].update(var_list) def _get_user_defined_local_vars(self, targets): - user_defined_local_vars = defaultdict(lambda: ComponentSet()) + user_defined_local_vars = defaultdict(ComponentSet) seen_blocks = set() # we go through the targets looking both up and down the hierarchy, but # we cache what Blocks/Disjuncts we've already looked on so that we @@ -369,7 +369,7 @@ def _transform_disjunctionData( # actually appear in any Constraints on that Disjunct, but in order to # do this, we will explicitly collect the set of local_vars in this # loop. - local_vars = defaultdict(lambda: ComponentSet()) + local_vars = defaultdict(ComponentSet) for var in var_order: disjuncts = disjuncts_var_appears_in[var] # clearly not local if used in more than one disjunct From b673bf74c0664e33679a8998ffc2a2ce72cb3d3e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 13:32:03 -0700 Subject: [PATCH 0682/3044] stopping the Suffix search going up once we hit a seen block --- pyomo/gdp/plugins/hull.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 5a3349a8b34..911233a0b2b 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -227,11 +227,12 @@ def _get_user_defined_local_vars(self, targets): # now look up in the tree blk = t while blk is not None: - if blk not in seen_blocks: - self._collect_local_vars_from_block( - blk, user_defined_local_vars - ) - seen_blocks.add(blk) + if blk in seen_blocks: + break + self._collect_local_vars_from_block( + blk, user_defined_local_vars + ) + seen_blocks.add(blk) blk = blk.parent_block() return user_defined_local_vars From 044316c796f11a8e2bf77b8ca797d95a7cb4fcf4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 13:59:13 -0700 Subject: [PATCH 0683/3044] Black --- pyomo/gdp/plugins/hull.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 911233a0b2b..6ee329cbff7 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -229,9 +229,7 @@ def _get_user_defined_local_vars(self, targets): while blk is not None: if blk in seen_blocks: break - self._collect_local_vars_from_block( - blk, user_defined_local_vars - ) + self._collect_local_vars_from_block(blk, user_defined_local_vars) seen_blocks.add(blk) blk = blk.parent_block() return user_defined_local_vars From 074ea7807b226c8854c74d15a6d433955dd32da5 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Feb 2024 14:01:10 -0700 Subject: [PATCH 0684/3044] Generalize the tests --- pyomo/contrib/solver/tests/unit/test_ipopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/tests/unit/test_ipopt.py b/pyomo/contrib/solver/tests/unit/test_ipopt.py index ae07bd37f86..eff8787592e 100644 --- a/pyomo/contrib/solver/tests/unit/test_ipopt.py +++ b/pyomo/contrib/solver/tests/unit/test_ipopt.py @@ -227,7 +227,7 @@ def test_create_command_line(self): self.assertEqual( result, [ - '/Users/mmundt/Documents/idaes/venv-pyomo/bin/ipopt', + str(opt.config.executable), 'myfile.nl', '-AMPL', 'option_file_name=myfile.opt', From 66285d9d9ff544a03ad3c176f6018b0858d9abad Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 14:35:25 -0700 Subject: [PATCH 0685/3044] Switching the bigm constraint map to a DefaultComponentMap :) --- pyomo/gdp/plugins/hull.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 6ee329cbff7..f1ed574907c 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -15,7 +15,7 @@ import pyomo.common.config as cfg from pyomo.common import deprecated -from pyomo.common.collections import ComponentMap, ComponentSet +from pyomo.common.collections import ComponentMap, ComponentSet, DefaultComponentMap from pyomo.common.modeling import unique_component_name from pyomo.core.expr.numvalue import ZeroConstant import pyomo.core.expr as EXPR @@ -457,11 +457,9 @@ def _transform_disjunctionData( dis_var_info['original_var_map'] = ComponentMap() original_var_map = dis_var_info['original_var_map'] if 'bigm_constraint_map' not in dis_var_info: - dis_var_info['bigm_constraint_map'] = ComponentMap() + dis_var_info['bigm_constraint_map'] = DefaultComponentMap(dict) bigm_constraint_map = dis_var_info['bigm_constraint_map'] - if disaggregated_var not in bigm_constraint_map: - bigm_constraint_map[disaggregated_var] = {} bigm_constraint_map[disaggregated_var][obj] = Reference( disaggregated_var_bounds[idx, :] ) @@ -565,9 +563,7 @@ def _transform_disjunct( # update the bigm constraint mappings data_dict = disaggregatedVar.parent_block().private_data() if 'bigm_constraint_map' not in data_dict: - data_dict['bigm_constraint_map'] = ComponentMap() - if disaggregatedVar not in data_dict['bigm_constraint_map']: - data_dict['bigm_constraint_map'][disaggregatedVar] = {} + data_dict['bigm_constraint_map'] = DefaultComponentMap(dict) data_dict['bigm_constraint_map'][disaggregatedVar][obj] = bigmConstraint disjunct_disaggregated_var_map[obj][var] = disaggregatedVar @@ -598,9 +594,7 @@ def _transform_disjunct( # update the bigm constraint mappings data_dict = var.parent_block().private_data() if 'bigm_constraint_map' not in data_dict: - data_dict['bigm_constraint_map'] = ComponentMap() - if var not in data_dict['bigm_constraint_map']: - data_dict['bigm_constraint_map'][var] = {} + data_dict['bigm_constraint_map'] = DefaultComponentMap(dict) data_dict['bigm_constraint_map'][var][obj] = bigmConstraint disjunct_disaggregated_var_map[obj][var] = var From b96bd2a583f894d38a963d5f133404e551aea1e7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 15:05:17 -0700 Subject: [PATCH 0686/3044] Add Block.register_private_data_initializer() --- pyomo/core/base/block.py | 27 ++++++++++++-- pyomo/core/tests/unit/test_block.py | 58 +++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 48353078fca..a0948c693d7 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -14,13 +14,13 @@ import sys import weakref import textwrap -from contextlib import contextmanager +from collections import defaultdict +from contextlib import contextmanager from inspect import isclass, currentframe +from io import StringIO from itertools import filterfalse, chain from operator import itemgetter, attrgetter -from io import StringIO -from pyomo.common.pyomo_typing import overload from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import Mapping @@ -28,6 +28,7 @@ from pyomo.common.formatting import StreamIndenter from pyomo.common.gc_manager import PauseGC from pyomo.common.log import is_debug_set +from pyomo.common.pyomo_typing import overload from pyomo.common.timing import ConstructionTimer from pyomo.core.base.component import ( Component, @@ -1986,7 +1987,7 @@ def private_data(self, scope=None): if self._private_data is None: self._private_data = {} if scope not in self._private_data: - self._private_data[scope] = {} + self._private_data[scope] = Block._private_data_initializers[scope]() return self._private_data[scope] @@ -2004,6 +2005,7 @@ class Block(ActiveIndexedComponent): """ _ComponentDataClass = _BlockData + _private_data_initializers = defaultdict(lambda: dict) def __new__(cls, *args, **kwds): if cls != Block: @@ -2207,6 +2209,23 @@ def display(self, filename=None, ostream=None, prefix=""): for key in sorted(self): _BlockData.display(self[key], filename, ostream, prefix) + @staticmethod + def register_private_data_initializer(initializer, scope=None): + mod = currentframe().f_back.f_globals['__name__'] + if scope is None: + scope = mod + elif not mod.startswith(scope): + raise ValueError( + "'private_data' scope must be substrings of the caller's module name. " + f"Received '{scope}' when calling register_private_data_initializer()." + ) + if scope in Block._private_data_initializers: + raise RuntimeError( + "Duplicate initializer registration for 'private_data' dictionary " + f"(scope={scope})" + ) + Block._private_data_initializers[scope] = initializer + class ScalarBlock(_BlockData, Block): def __init__(self, *args, **kwds): diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index c9c68a820f7..88646643703 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -3437,6 +3437,64 @@ def test_private_data(self): mfe4 = m.b.b[1].private_data('pyomo.core.tests') self.assertIs(mfe4, mfe3) + def test_register_private_data(self): + _save = Block._private_data_initializers + + Block._private_data_initializers = pdi = _save.copy() + pdi.clear() + try: + self.assertEqual(len(pdi), 0) + b = Block(concrete=True) + ps = b.private_data() + self.assertEqual(ps, {}) + self.assertEqual(len(pdi), 1) + finally: + Block._private_data_initializers = _save + + def init(): + return {'a': None, 'b': 1} + + Block._private_data_initializers = pdi = _save.copy() + pdi.clear() + try: + self.assertEqual(len(pdi), 0) + Block.register_private_data_initializer(init) + self.assertEqual(len(pdi), 1) + + b = Block(concrete=True) + ps = b.private_data() + self.assertEqual(ps, {'a': None, 'b': 1}) + self.assertEqual(len(pdi), 1) + finally: + Block._private_data_initializers = _save + + Block._private_data_initializers = pdi = _save.copy() + pdi.clear() + try: + Block.register_private_data_initializer(init) + self.assertEqual(len(pdi), 1) + Block.register_private_data_initializer(init, 'pyomo') + self.assertEqual(len(pdi), 2) + + with self.assertRaisesRegex( + RuntimeError, + r"Duplicate initializer registration for 'private_data' " + r"dictionary \(scope=pyomo.core.tests.unit.test_block\)", + ): + Block.register_private_data_initializer(init) + + with self.assertRaisesRegex( + ValueError, + r"'private_data' scope must be substrings of the caller's " + r"module name. Received 'invalid' when calling " + r"register_private_data_initializer\(\).", + ): + Block.register_private_data_initializer(init, 'invalid') + + self.assertEqual(len(pdi), 2) + finally: + Block._private_data_initializers = _save + if __name__ == "__main__": unittest.main() From 05e0b470731611dc50725c1128613c1db3d84f58 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Feb 2024 16:08:45 -0700 Subject: [PATCH 0687/3044] Bug fix: missing imports --- pyomo/contrib/solver/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index 8f715ac7250..c91eb603b32 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -14,7 +14,7 @@ import sys from collections.abc import Sequence -from typing import Optional +from typing import Optional, List, TextIO from pyomo.common.config import ( ConfigDict, From a462f362a0dd4214ee28a7f9a4d829a6e90d8419 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 16:34:09 -0700 Subject: [PATCH 0688/3044] Registering a data class with the private data for the hull scope--this is very pretty --- pyomo/gdp/plugins/hull.py | 103 ++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 60 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index f1ed574907c..78d4e917fca 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -13,6 +13,7 @@ from collections import defaultdict +from pyomo.common.autoslots import AutoSlots import pyomo.common.config as cfg from pyomo.common import deprecated from pyomo.common.collections import ComponentMap, ComponentSet, DefaultComponentMap @@ -55,6 +56,17 @@ logger = logging.getLogger('pyomo.gdp.hull') +class _HullTransformationData(AutoSlots.Mixin): + __slots__ = ('disaggregated_var_map', + 'original_var_map', + 'bigm_constraint_map') + + def __init__(self): + self.disaggregated_var_map = DefaultComponentMap(ComponentMap) + self.original_var_map = ComponentMap() + self.bigm_constraint_map = DefaultComponentMap(ComponentMap) + +Block.register_private_data_initializer(_HullTransformationData) @TransformationFactory.register( 'gdp.hull', doc="Relax disjunctive model by forming the hull reformulation." @@ -449,21 +461,13 @@ def _transform_disjunctionData( ) # Update mappings: var_info = var.parent_block().private_data() - if 'disaggregated_var_map' not in var_info: - var_info['disaggregated_var_map'] = ComponentMap() - disaggregated_var_map = var_info['disaggregated_var_map'] + disaggregated_var_map = var_info.disaggregated_var_map dis_var_info = disaggregated_var.parent_block().private_data() - if 'original_var_map' not in dis_var_info: - dis_var_info['original_var_map'] = ComponentMap() - original_var_map = dis_var_info['original_var_map'] - if 'bigm_constraint_map' not in dis_var_info: - dis_var_info['bigm_constraint_map'] = DefaultComponentMap(dict) - bigm_constraint_map = dis_var_info['bigm_constraint_map'] - - bigm_constraint_map[disaggregated_var][obj] = Reference( + + dis_var_info.bigm_constraint_map[disaggregated_var][obj] = Reference( disaggregated_var_bounds[idx, :] ) - original_var_map[disaggregated_var] = var + dis_var_info.original_var_map[disaggregated_var] = var # For every Disjunct the Var does not appear in, we want to map # that this new variable is its disaggreggated variable. @@ -475,8 +479,6 @@ def _transform_disjunctionData( disj._transformation_block is not None and disj not in disjuncts_var_appears_in[var] ): - if not disj in disaggregated_var_map: - disaggregated_var_map[disj] = ComponentMap() disaggregated_var_map[disj][var] = disaggregated_var # start the expression for the reaggregation constraint with @@ -562,9 +564,7 @@ def _transform_disjunct( ) # update the bigm constraint mappings data_dict = disaggregatedVar.parent_block().private_data() - if 'bigm_constraint_map' not in data_dict: - data_dict['bigm_constraint_map'] = DefaultComponentMap(dict) - data_dict['bigm_constraint_map'][disaggregatedVar][obj] = bigmConstraint + data_dict.bigm_constraint_map[disaggregatedVar][obj] = bigmConstraint disjunct_disaggregated_var_map[obj][var] = disaggregatedVar for var in local_vars: @@ -593,9 +593,7 @@ def _transform_disjunct( ) # update the bigm constraint mappings data_dict = var.parent_block().private_data() - if 'bigm_constraint_map' not in data_dict: - data_dict['bigm_constraint_map'] = DefaultComponentMap(dict) - data_dict['bigm_constraint_map'][var][obj] = bigmConstraint + data_dict.bigm_constraint_map[var][obj] = bigmConstraint disjunct_disaggregated_var_map[obj][var] = var var_substitute_map = dict( @@ -645,21 +643,13 @@ def _declare_disaggregated_var_bounds( bigmConstraint.add(ub_idx, disaggregatedVar <= ub * var_free_indicator) original_var_info = original_var.parent_block().private_data() - if 'disaggregated_var_map' not in original_var_info: - original_var_info['disaggregated_var_map'] = ComponentMap() - disaggregated_var_map = original_var_info['disaggregated_var_map'] - + disaggregated_var_map = original_var_info.disaggregated_var_map disaggregated_var_info = disaggregatedVar.parent_block().private_data() - if 'original_var_map' not in disaggregated_var_info: - disaggregated_var_info['original_var_map'] = ComponentMap() - original_var_map = disaggregated_var_info['original_var_map'] # store the mappings from variables to their disaggregated selves on # the transformation block - if disjunct not in disaggregated_var_map: - disaggregated_var_map[disjunct] = ComponentMap() disaggregated_var_map[disjunct][original_var] = disaggregatedVar - original_var_map[disaggregatedVar] = original_var + disaggregated_var_info.original_var_map[disaggregatedVar] = original_var def _get_local_var_list(self, parent_disjunct): # Add or retrieve Suffix from parent_disjunct so that, if this is @@ -885,17 +875,12 @@ def get_disaggregated_var(self, v, disjunct, raise_exception=True): "It does not appear '%s' is a " "variable that appears in disjunct '%s'" % (v.name, disjunct.name) ) - var_map = v.parent_block().private_data() - if 'disaggregated_var_map' in var_map: - try: - return var_map['disaggregated_var_map'][disjunct][v] - except: - if raise_exception: - logger.error(msg) - raise - elif raise_exception: - raise GDP_Error(msg) - return None + disaggregated_var_map = v.parent_block().private_data().disaggregated_var_map + if v in disaggregated_var_map[disjunct]: + return disaggregated_var_map[disjunct][v] + else: + if raise_exception: + raise GDP_Error(msg) def get_src_var(self, disaggregated_var): """ @@ -910,9 +895,8 @@ def get_src_var(self, disaggregated_var): of some Disjunct) """ var_map = disaggregated_var.parent_block().private_data() - if 'original_var_map' in var_map: - if disaggregated_var in var_map['original_var_map']: - return var_map['original_var_map'][disaggregated_var] + if disaggregated_var in var_map.original_var_map: + return var_map.original_var_map[disaggregated_var] raise GDP_Error( "'%s' does not appear to be a " "disaggregated variable" % disaggregated_var.name @@ -979,22 +963,21 @@ def get_var_bounds_constraint(self, v, disjunct=None): Optional since for non-nested models this can be inferred. """ info = v.parent_block().private_data() - if 'bigm_constraint_map' in info: - if v in info['bigm_constraint_map']: - if len(info['bigm_constraint_map'][v]) == 1: - # Not nested, or it's at the top layer, so we're fine. - return list(info['bigm_constraint_map'][v].values())[0] - elif disjunct is not None: - # This is nested, so we need to walk up to find the active ones - return info['bigm_constraint_map'][v][disjunct] - else: - raise ValueError( - "It appears that the variable '%s' appears " - "within a nested GDP hierarchy, and no " - "'disjunct' argument was specified. Please " - "specify for which Disjunct the bounds " - "constraint for '%s' should be returned." % (v, v) - ) + if v in info.bigm_constraint_map: + if len(info.bigm_constraint_map[v]) == 1: + # Not nested, or it's at the top layer, so we're fine. + return list(info.bigm_constraint_map[v].values())[0] + elif disjunct is not None: + # This is nested, so we need to walk up to find the active ones + return info.bigm_constraint_map[v][disjunct] + else: + raise ValueError( + "It appears that the variable '%s' appears " + "within a nested GDP hierarchy, and no " + "'disjunct' argument was specified. Please " + "specify for which Disjunct the bounds " + "constraint for '%s' should be returned." % (v, v) + ) raise GDP_Error( "Either '%s' is not a disaggregated variable, or " "the disjunction that disaggregates it has not " From 5a71219cb49d236719b8a9c725ac37ac6a7c020f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 20 Feb 2024 16:35:22 -0700 Subject: [PATCH 0689/3044] Black is relatively tame --- pyomo/gdp/plugins/hull.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 78d4e917fca..d1c38bde039 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -56,18 +56,19 @@ logger = logging.getLogger('pyomo.gdp.hull') + class _HullTransformationData(AutoSlots.Mixin): - __slots__ = ('disaggregated_var_map', - 'original_var_map', - 'bigm_constraint_map') + __slots__ = ('disaggregated_var_map', 'original_var_map', 'bigm_constraint_map') def __init__(self): self.disaggregated_var_map = DefaultComponentMap(ComponentMap) self.original_var_map = ComponentMap() self.bigm_constraint_map = DefaultComponentMap(ComponentMap) + Block.register_private_data_initializer(_HullTransformationData) + @TransformationFactory.register( 'gdp.hull', doc="Relax disjunctive model by forming the hull reformulation." ) From 33a05453c2251d0a51821ea1b8733fb11f57a7c1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Feb 2024 23:42:21 -0700 Subject: [PATCH 0690/3044] Update intersphinx links, remove documentation of nonfunctional code --- doc/OnlineDocs/conf.py | 4 ++-- .../library_reference/expressions/context_managers.rst | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 89c346f5abc..7196606b7d6 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -57,8 +57,8 @@ 'numpy': ('https://numpy.org/doc/stable/', None), 'pandas': ('https://pandas.pydata.org/docs/', None), 'scikit-learn': ('https://scikit-learn.org/stable/', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), - 'Sphinx': ('https://www.sphinx-doc.org/en/stable/', None), + 'scipy': ('https://docs.scipy.org/doc/scipy/', None), + 'Sphinx': ('https://www.sphinx-doc.org/en/master/', None), } # -- General configuration ------------------------------------------------ diff --git a/doc/OnlineDocs/library_reference/expressions/context_managers.rst b/doc/OnlineDocs/library_reference/expressions/context_managers.rst index 0e92f583c73..ae6884d684f 100644 --- a/doc/OnlineDocs/library_reference/expressions/context_managers.rst +++ b/doc/OnlineDocs/library_reference/expressions/context_managers.rst @@ -8,6 +8,3 @@ Context Managers .. autoclass:: pyomo.core.expr.linear_expression :members: -.. autoclass:: pyomo.core.expr.current.clone_counter - :members: - From 90f6901c51ca5272d5ccfd1ce6787bfdce1ad499 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 01:30:42 -0700 Subject: [PATCH 0691/3044] Updating CHANGELOG in preparation for the release --- CHANGELOG.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 553a4f1c3bd..747025a8bdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,81 @@ Pyomo CHANGELOG =============== +------------------------------------------------------------------------------- +Pyomo 6.7.1 (21 Feb 2024) +------------------------------------------------------------------------------- + +- General + - Add support for tuples in `ComponentMap`; add `DefaultComponentMap` (#3150) + - Update `Path`, `PathList`, and `IsInstance` Domain Validators (#3144) + - Remove usage of `__all__` (#3142) + - Extend Path and Type Checking Validators of `common.config` (#3140) + - Update Copyright Statements (#3139) + - Update `ExitNodeDispatcher` to better support extensibility (#3125) + - Create contributors data gathering script (#3117) + - Prevent duplicate entries in ConfigDict declaration order (#3116) + - Remove unnecessary `__future__` imports (#3109) + - Import pandas through pyomo.common.dependencies (#3102) + - Update links to workshop slides (#3079) + - Remove incorrect use of identity (is) comparisons (#3061) +- Core + - Add `Block.register_private_data_initializer()` (#3153) + - Generalize the simple_constraint_rule decorator (#3152) + - Fix edge case assigning new numeric types to Var/Param with units (#3151) + - Add private_data to `_BlockData` (#3138) + - Convert implicit sets created by `IndexedComponent`s to "anonymous" sets (#3075) + - Add `all_different` and `count_if` to the logical expression system (#3058) + - Fix RangeSet.__len__ when defined by floats (#3119) + - Overhaul the `Suffix` component (#3072) + - Enforce expression immutability in `expr.args` (#3099) + - Improve NumPy registration when assigning numpy to Param (#3093) + - Track changes in PyPy behavior introduced in 7.3.14 (#3087) + - Remove automatic numpy import (#3077) + - Fix `range_difference` for Sets with nonzero anchor points (#3063) + - Clarify errors raised by accessing Sets by positional index (#3062) +- Documentation + - Update MPC documentation and citation (#3148) + - Fix an error in the documentation for LinearExpression (#3090) + - Fix bugs in the documentation of Pyomo.DoE (#3070) + - Fix a latex_printer vestige in the documentation (#3066) +- Solver Interfaces + - Make error msg more explicit wrt different interfaces (#3141) + - NLv2: only raise exception for empty models in the legacy API (#3135) + - Add `to_expr()` to AMPLRepn, fix NLWriterInfo return type (#3095) +- Testing + - Update Release Wheel Builder Action (#3149) + - Actions Version Update: Address node.js deprecations (#3118) + - New Black Major Release (24.1.0) (#3108) + - Use scip for PyROS tests (#3104) + - Add missing solver dependency flags for OnlineDocs tests (#3094) + - Re-enable `contrib.viewer.tests.test_qt.py` (#3085) + - Add automated testing of OnlineDocs examples (#3080) + - Silence deprecation warnings emitted by Pyomo tests (#3076) + - Fix Python 3.12 tests (manage `pyutilib`, `distutils` dependencies) (#3065) +- DAE + - Replace deprecated `numpy.math` alias with standard `math` module (#3074) +- GDP + - Handle nested GDPs correctly in all the transformations (#3145) + - Fix bugs in nested models in gdp.hull transformation (#3143) + - Various bug fixes in gdp.mbigm transformation (#3073) + - Add GDP => MINLP Transformation (#3082) +- Contributed Packages + - GDPopt: Fix lbb solve_data bug (#3133) + - GDPopt: Adding missing import for gdpopt.enumerate (#3105) + - FBBT: Extend `fbbt.ExpressionBoundsVisitor` to handle relational + expressions and Expr_if (#3129) + - incidence_analysis: Method to add an edge in IncidenceGraphInterface (#3120) + - incidence_analysis: Add subgraph method to IncidencegraphInterface (#3122) + - incidence_analysis: Add `ampl_repn` option (#3069) + - incidence_analysis: Fix config documentation of `linear_only` argument in + `get_incident_variables` (#3067) + - interior_point: Workaround for improvement in Mumps memory prediction + algorithm (#3114) + - MindtPy: Various bug fixes (#3034) + - PyROS: Update Solver Argument Resolution and Validation Routines (#3126) + - PyROS: Update Subproblem Initialization Routines (#3071) + - PyROS: Fix DR polishing under nominal objective focus (#3060) + ------------------------------------------------------------------------------- Pyomo 6.7.0 (29 Nov 2023) ------------------------------------------------------------------------------- From 1bccb1699bd504af1e851eb8ffdb61e152af6578 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 21 Feb 2024 08:03:57 -0700 Subject: [PATCH 0692/3044] Address comments from @jsiirola, @blnicho --- pyomo/common/tests/test_config.py | 1 - pyomo/contrib/appsi/base.py | 1 + pyomo/contrib/solver/base.py | 12 ++- pyomo/contrib/solver/config.py | 22 +++--- pyomo/contrib/solver/gurobi.py | 4 +- pyomo/contrib/solver/ipopt.py | 79 ++++--------------- pyomo/contrib/solver/persistent.py | 16 ++-- .../tests/solvers/test_gurobi_persistent.py | 2 +- .../solver/tests/solvers/test_solvers.py | 31 +++----- pyomo/contrib/solver/tests/unit/test_base.py | 16 ++-- pyomo/contrib/solver/tests/unit/test_ipopt.py | 20 +---- .../contrib/solver/tests/unit/test_results.py | 6 +- 12 files changed, 68 insertions(+), 142 deletions(-) diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 02f4fc88251..0bbed43423d 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -25,7 +25,6 @@ # ___________________________________________________________________________ import argparse -import datetime import enum import os import os.path diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 201e5975ac9..d028bdc4fde 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -597,6 +597,7 @@ def __init__( class Solver(abc.ABC): class Availability(enum.IntEnum): + """Docstring""" NotFound = 0 BadVersion = -1 BadLicense = -2 diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 3bfa83050ad..f3d60bef03d 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -125,7 +125,7 @@ def solve(self, model: _BlockData, **kwargs) -> Results: """ @abc.abstractmethod - def available(self): + def available(self) -> bool: """Test if the solver is available on this system. Nominally, this will return True if the solver interface is @@ -159,7 +159,7 @@ def version(self) -> Tuple: A tuple representing the version """ - def is_persistent(self): + def is_persistent(self) -> bool: """ Returns ------- @@ -178,9 +178,7 @@ class PersistentSolverBase(SolverBase): Example usage can be seen in the Gurobi interface. """ - CONFIG = PersistentSolverConfig() - - @document_kwargs_from_configdict(CONFIG) + @document_kwargs_from_configdict(PersistentSolverConfig()) @abc.abstractmethod def solve(self, model: _BlockData, **kwargs) -> Results: super().solve(model, kwargs) @@ -312,7 +310,7 @@ def remove_variables(self, variables: List[_GeneralVarData]): """ @abc.abstractmethod - def remove_params(self, params: List[_ParamData]): + def remove_parameters(self, params: List[_ParamData]): """ Remove parameters from the model """ @@ -336,7 +334,7 @@ def update_variables(self, variables: List[_GeneralVarData]): """ @abc.abstractmethod - def update_params(self): + def update_parameters(self): """ Update parameters on the model """ diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index c91eb603b32..e60219a74b5 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -23,6 +23,7 @@ NonNegativeInt, ADVANCED_OPTION, Bool, + Path, ) from pyomo.common.log import LogStream from pyomo.common.numeric_types import native_logical_types @@ -74,17 +75,17 @@ def __init__( ConfigValue( domain=TextIO_or_Logger, default=False, - description="""`tee` accepts :py:class:`bool`, + description="""``tee`` accepts :py:class:`bool`, :py:class:`io.TextIOBase`, or :py:class:`logging.Logger` (or a list of these types). ``True`` is mapped to ``sys.stdout``. The solver log will be printed to each of - these streams / destinations. """, + these streams / destinations.""", ), ) - self.working_dir: Optional[str] = self.declare( + self.working_dir: Optional[Path] = self.declare( 'working_dir', ConfigValue( - domain=str, + domain=Path(), default=None, description="The directory in which generated files should be saved. " "This replaces the `keepfiles` option.", @@ -134,7 +135,8 @@ def __init__( self.time_limit: Optional[float] = self.declare( 'time_limit', ConfigValue( - domain=NonNegativeFloat, description="Time limit applied to the solver." + domain=NonNegativeFloat, + description="Time limit applied to the solver (in seconds).", ), ) self.solver_options: ConfigDict = self.declare( @@ -201,7 +203,7 @@ class AutoUpdateConfig(ConfigDict): check_for_new_objective: bool update_constraints: bool update_vars: bool - update_params: bool + update_parameters: bool update_named_expressions: bool update_objective: bool treat_fixed_vars_as_params: bool @@ -257,7 +259,7 @@ def __init__( description=""" If False, new/old parameters will not be automatically detected on subsequent solves. Use False only when manually updating the solver with opt.add_parameters() and - opt.remove_params() or when you are certain parameters are not being added to / + opt.remove_parameters() or when you are certain parameters are not being added to / removed from the model.""", ), ) @@ -297,15 +299,15 @@ def __init__( opt.update_variables() or when you are certain variables are not being modified.""", ), ) - self.update_params: bool = self.declare( - 'update_params', + self.update_parameters: bool = self.declare( + 'update_parameters', ConfigValue( domain=bool, default=True, description=""" If False, changes to parameter values will not be automatically detected on subsequent solves. Use False only when manually updating the solver with - opt.update_params() or when you are certain parameters are not being modified.""", + opt.update_parameters() or when you are certain parameters are not being modified.""", ), ) self.update_named_expressions: bool = self.declare( diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 63387730c45..d0ac0d80f45 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -747,7 +747,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): self._mutable_bounds.pop(v_id, None) self._needs_updated = True - def _remove_params(self, params: List[_ParamData]): + def _remove_parameters(self, params: List[_ParamData]): pass def _update_variables(self, variables: List[_GeneralVarData]): @@ -770,7 +770,7 @@ def _update_variables(self, variables: List[_GeneralVarData]): gurobipy_var.setAttr('vtype', vtype) self._needs_updated = True - def update_params(self): + def update_parameters(self): for con, helpers in self._mutable_helpers.items(): for helper in helpers: helper.update() diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 3e911aea036..ad12e26ee92 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -13,16 +13,10 @@ import subprocess import datetime import io -import sys from typing import Mapping, Optional, Sequence from pyomo.common import Executable -from pyomo.common.config import ( - ConfigValue, - NonNegativeFloat, - document_kwargs_from_configdict, - ConfigDict, -) +from pyomo.common.config import ConfigValue, document_kwargs_from_configdict, ConfigDict from pyomo.common.errors import PyomoException, DeveloperError from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer @@ -78,54 +72,7 @@ def __init__( ), ) self.writer_config: ConfigDict = self.declare( - 'writer_config', NLWriter.CONFIG() - ) - - -class IpoptResults(Results): - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__( - description=description, - doc=doc, - implicit=implicit, - implicit_domain=implicit_domain, - visibility=visibility, - ) - self.timing_info.ipopt_excluding_nlp_functions: Optional[float] = ( - self.timing_info.declare( - 'ipopt_excluding_nlp_functions', - ConfigValue( - domain=NonNegativeFloat, - default=None, - description="Total CPU seconds in IPOPT without function evaluations.", - ), - ) - ) - self.timing_info.nlp_function_evaluations: Optional[float] = ( - self.timing_info.declare( - 'nlp_function_evaluations', - ConfigValue( - domain=NonNegativeFloat, - default=None, - description="Total CPU seconds in NLP function evaluations.", - ), - ) - ) - self.timing_info.total_seconds: Optional[float] = self.timing_info.declare( - 'total_seconds', - ConfigValue( - domain=NonNegativeFloat, - default=None, - description="Total seconds in IPOPT. NOTE: Newer versions of IPOPT (3.14+) " - "no longer separate timing information.", - ), + 'writer_config', ConfigValue(default=NLWriter.CONFIG(), description="Configuration that controls options in the NL writer.") ) @@ -416,11 +363,11 @@ def solve(self, model, **kwds): if len(nl_info.variables) == 0: if len(nl_info.eliminated_vars) == 0: - results = IpoptResults() + results = Results() results.termination_condition = TerminationCondition.emptyModel results.solution_loader = SolSolutionLoader(None, None) else: - results = IpoptResults() + results = Results() results.termination_condition = ( TerminationCondition.convergenceCriteriaSatisfied ) @@ -435,18 +382,22 @@ def solve(self, model, **kwds): results = self._parse_solution(sol_file, nl_info) timer.stop('parse_sol') else: - results = IpoptResults() + results = Results() if process.returncode != 0: results.extra_info.return_code = process.returncode results.termination_condition = TerminationCondition.error results.solution_loader = SolSolutionLoader(None, None) else: results.iteration_count = iters - results.timing_info.ipopt_excluding_nlp_functions = ( - ipopt_time_nofunc - ) - results.timing_info.nlp_function_evaluations = ipopt_time_func - results.timing_info.total_seconds = ipopt_total_time + if ipopt_time_nofunc is not None: + results.timing_info.ipopt_excluding_nlp_functions = ( + ipopt_time_nofunc + ) + + if ipopt_time_func is not None: + results.timing_info.nlp_function_evaluations = ipopt_time_func + if ipopt_total_time is not None: + results.timing_info.total_seconds = ipopt_total_time if ( config.raise_exception_on_nonoptimal_result and results.solution_status != SolutionStatus.optimal @@ -554,7 +505,7 @@ def _parse_ipopt_output(self, stream: io.StringIO): return iters, nofunc_time, func_time, total_time def _parse_solution(self, instream: io.TextIOBase, nl_info: NLWriterInfo): - results = IpoptResults() + results = Results() res, sol_data = parse_sol_file( sol_file=instream, nl_info=nl_info, result=results ) diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index e389e5d4019..4b1a7c58dcd 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -274,11 +274,11 @@ def remove_variables(self, variables: List[_GeneralVarData]): del self._vars[v_id] @abc.abstractmethod - def _remove_params(self, params: List[_ParamData]): + def _remove_parameters(self, params: List[_ParamData]): pass - def remove_params(self, params: List[_ParamData]): - self._remove_params(params) + def remove_parameters(self, params: List[_ParamData]): + self._remove_parameters(params) for p in params: del self._params[id(p)] @@ -297,7 +297,7 @@ def remove_block(self, block): ) ) ) - self.remove_params( + self.remove_parameters( list( dict( (id(p), p) @@ -325,7 +325,7 @@ def update_variables(self, variables: List[_GeneralVarData]): self._update_variables(variables) @abc.abstractmethod - def update_params(self): + def update_parameters(self): pass def update(self, timer: HierarchicalTimer = None): @@ -396,12 +396,12 @@ def update(self, timer: HierarchicalTimer = None): self.remove_sos_constraints(old_sos) timer.stop('cons') timer.start('params') - self.remove_params(old_params) + self.remove_parameters(old_params) # sticking this between removal and addition # is important so that we don't do unnecessary work - if config.update_params: - self.update_params() + if config.update_parameters: + self.update_parameters() self.add_parameters(new_params) timer.stop('params') diff --git a/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py index f2dd79619b4..2f281e2abf0 100644 --- a/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py +++ b/pyomo/contrib/solver/tests/solvers/test_gurobi_persistent.py @@ -487,7 +487,7 @@ def setUp(self): opt.config.auto_updates.check_for_new_or_removed_params = False opt.config.auto_updates.check_for_new_or_removed_vars = False opt.config.auto_updates.check_for_new_or_removed_constraints = False - opt.config.auto_updates.update_params = False + opt.config.auto_updates.update_parameters = False opt.config.auto_updates.update_vars = False opt.config.auto_updates.update_constraints = False opt.config.auto_updates.update_named_expressions = False diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index c6c73ea2dc7..cf5f6cf5c57 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -9,23 +9,24 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import random +import math +from typing import Type + import pyomo.environ as pe +from pyomo import gdp from pyomo.common.dependencies import attempt_import import pyomo.common.unittest as unittest - -parameterized, param_available = attempt_import('parameterized') -parameterized = parameterized.parameterized from pyomo.contrib.solver.results import TerminationCondition, SolutionStatus, Results from pyomo.contrib.solver.base import SolverBase from pyomo.contrib.solver.ipopt import Ipopt from pyomo.contrib.solver.gurobi import Gurobi -from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression -import math -numpy, numpy_available = attempt_import('numpy') -import random -from pyomo import gdp + +np, numpy_available = attempt_import('numpy') +parameterized, param_available = attempt_import('parameterized') +parameterized = parameterized.parameterized if not param_available: @@ -802,10 +803,6 @@ def test_mutable_param_with_range( opt.config.writer_config.linear_presolve = True else: opt.config.writer_config.linear_presolve = False - try: - import numpy as np - except: - raise unittest.SkipTest('numpy is not available') m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() @@ -907,7 +904,7 @@ def test_add_and_remove_vars( m.y = pe.Var(bounds=(-1, None)) m.obj = pe.Objective(expr=m.y) if opt.is_persistent(): - opt.config.auto_updates.update_params = False + opt.config.auto_updates.update_parameters = False opt.config.auto_updates.update_vars = False opt.config.auto_updates.update_constraints = False opt.config.auto_updates.update_named_expressions = False @@ -1003,14 +1000,10 @@ def test_with_numpy( a2 = -2 b2 = 1 m.c1 = pe.Constraint( - expr=(numpy.float64(0), m.y - numpy.int64(1) * m.x - numpy.float32(3), None) + expr=(np.float64(0), m.y - np.int64(1) * m.x - np.float32(3), None) ) m.c2 = pe.Constraint( - expr=( - None, - -m.y + numpy.int32(-2) * m.x + numpy.float64(1), - numpy.float16(0), - ) + expr=(None, -m.y + np.int32(-2) * m.x + np.float64(1), np.float16(0)) ) res = opt.solve(m) self.assertEqual(res.solution_status, SolutionStatus.optimal) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index a9b3e4f4711..74c495b86cc 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -80,7 +80,7 @@ def test_custom_solver_name(self): class TestPersistentSolverBase(unittest.TestCase): def test_abstract_member_list(self): expected_list = [ - 'remove_params', + 'remove_parameters', 'version', 'update_variables', 'remove_variables', @@ -88,7 +88,7 @@ def test_abstract_member_list(self): '_get_primals', 'set_instance', 'set_objective', - 'update_params', + 'update_parameters', 'remove_block', 'add_block', 'available', @@ -116,12 +116,12 @@ def test_class_method_list(self): 'is_persistent', 'remove_block', 'remove_constraints', - 'remove_params', + 'remove_parameters', 'remove_variables', 'set_instance', 'set_objective', 'solve', - 'update_params', + 'update_parameters', 'update_variables', 'version', ] @@ -142,12 +142,12 @@ def test_init(self): self.assertEqual(self.instance.add_constraints(None), None) self.assertEqual(self.instance.add_block(None), None) self.assertEqual(self.instance.remove_variables(None), None) - self.assertEqual(self.instance.remove_params(None), None) + self.assertEqual(self.instance.remove_parameters(None), None) self.assertEqual(self.instance.remove_constraints(None), None) self.assertEqual(self.instance.remove_block(None), None) self.assertEqual(self.instance.set_objective(None), None) self.assertEqual(self.instance.update_variables(None), None) - self.assertEqual(self.instance.update_params(), None) + self.assertEqual(self.instance.update_parameters(), None) with self.assertRaises(NotImplementedError): self.instance._get_primals() @@ -168,12 +168,12 @@ def test_context_manager(self): self.assertEqual(self.instance.add_constraints(None), None) self.assertEqual(self.instance.add_block(None), None) self.assertEqual(self.instance.remove_variables(None), None) - self.assertEqual(self.instance.remove_params(None), None) + self.assertEqual(self.instance.remove_parameters(None), None) self.assertEqual(self.instance.remove_constraints(None), None) self.assertEqual(self.instance.remove_block(None), None) self.assertEqual(self.instance.set_objective(None), None) self.assertEqual(self.instance.update_variables(None), None) - self.assertEqual(self.instance.update_params(), None) + self.assertEqual(self.instance.update_parameters(), None) class TestLegacySolverWrapper(unittest.TestCase): diff --git a/pyomo/contrib/solver/tests/unit/test_ipopt.py b/pyomo/contrib/solver/tests/unit/test_ipopt.py index eff8787592e..cc459245506 100644 --- a/pyomo/contrib/solver/tests/unit/test_ipopt.py +++ b/pyomo/contrib/solver/tests/unit/test_ipopt.py @@ -44,7 +44,7 @@ def test_custom_instantiation(self): config.tee = True self.assertTrue(config.tee) self.assertEqual(config._description, "A description") - self.assertFalse(config.time_limit) + self.assertIsNone(config.time_limit) # Default should be `ipopt` self.assertIsNotNone(str(config.executable)) self.assertIn('ipopt', str(config.executable)) @@ -54,24 +54,6 @@ def test_custom_instantiation(self): self.assertFalse(config.executable.available()) -class TestIpoptResults(unittest.TestCase): - def test_default_instantiation(self): - res = ipopt.IpoptResults() - # Inherited methods/attributes - self.assertIsNone(res.solution_loader) - self.assertIsNone(res.incumbent_objective) - self.assertIsNone(res.objective_bound) - self.assertIsNone(res.solver_name) - self.assertIsNone(res.solver_version) - self.assertIsNone(res.iteration_count) - self.assertIsNone(res.timing_info.start_timestamp) - self.assertIsNone(res.timing_info.wall_time) - # Unique to this object - self.assertIsNone(res.timing_info.ipopt_excluding_nlp_functions) - self.assertIsNone(res.timing_info.nlp_function_evaluations) - self.assertIsNone(res.timing_info.total_seconds) - - class TestIpoptSolutionLoader(unittest.TestCase): def test_get_reduced_costs_error(self): loader = ipopt.IpoptSolutionLoader(None, None) diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 4856b737295..74404aaba4c 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -21,7 +21,7 @@ from pyomo.contrib.solver import results from pyomo.contrib.solver import solution import pyomo.environ as pyo -from pyomo.core.base.var import ScalarVar +from pyomo.core.base.var import Var class SolutionLoaderExample(solution.SolutionLoaderBase): @@ -213,8 +213,8 @@ def test_display(self): def test_generated_results(self): m = pyo.ConcreteModel() - m.x = ScalarVar() - m.y = ScalarVar() + m.x = Var() + m.y = Var() m.c1 = pyo.Constraint(expr=m.x == 1) m.c2 = pyo.Constraint(expr=m.y == 2) From 9b21273f92d4b62d3666f9ee1dbcb61741376b65 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 21 Feb 2024 08:08:41 -0700 Subject: [PATCH 0693/3044] Apply black --- pyomo/contrib/appsi/base.py | 1 - pyomo/contrib/solver/ipopt.py | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index d028bdc4fde..201e5975ac9 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -597,7 +597,6 @@ def __init__( class Solver(abc.ABC): class Availability(enum.IntEnum): - """Docstring""" NotFound = 0 BadVersion = -1 BadLicense = -2 diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index ad12e26ee92..3ac1a5ac4a2 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -72,7 +72,11 @@ def __init__( ), ) self.writer_config: ConfigDict = self.declare( - 'writer_config', ConfigValue(default=NLWriter.CONFIG(), description="Configuration that controls options in the NL writer.") + 'writer_config', + ConfigValue( + default=NLWriter.CONFIG(), + description="Configuration that controls options in the NL writer.", + ), ) From 434c1dadeeed7531a6b372661891dbf17b7d647b Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 09:11:46 -0700 Subject: [PATCH 0694/3044] More updates to the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 747025a8bdf..c548a8c830c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Pyomo 6.7.1 (21 Feb 2024) - Fix `range_difference` for Sets with nonzero anchor points (#3063) - Clarify errors raised by accessing Sets by positional index (#3062) - Documentation + - Update intersphinx links, remove docs for nonfunctional code (#3155) - Update MPC documentation and citation (#3148) - Fix an error in the documentation for LinearExpression (#3090) - Fix bugs in the documentation of Pyomo.DoE (#3070) From 92163f2989dc99d4e14f63c658c0cc77e3d9dc7a Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 21 Feb 2024 09:15:41 -0700 Subject: [PATCH 0695/3044] cleanup --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- pyomo/contrib/simplification/__init__.py | 11 +++++++++++ pyomo/contrib/simplification/build.py | 11 +++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 83e652cbef8..57c24d99090 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -157,7 +157,7 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - - name: install ginac + - name: install GiNaC if: matrix.other == '/singletest' run: | cd .. diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 6df28fbadc9..a55d100a18f 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -179,7 +179,7 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - - name: install ginac + - name: install GiNaC if: matrix.other == '/singletest' run: | cd .. diff --git a/pyomo/contrib/simplification/__init__.py b/pyomo/contrib/simplification/__init__.py index 3abe5a25ba0..b4fa68eb386 100644 --- a/pyomo/contrib/simplification/__init__.py +++ b/pyomo/contrib/simplification/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from .simplify import Simplifier diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 39742e1e351..67ae1d37335 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pybind11.setup_helpers import Pybind11Extension, build_ext from pyomo.common.fileutils import this_file_dir, find_library import os From 0dff80fb37b9052805d744f42236711067d69bcc Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 21 Feb 2024 09:16:59 -0700 Subject: [PATCH 0696/3044] cleanup --- pyomo/contrib/simplification/build.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 67ae1d37335..4bf28a0fa33 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -9,15 +9,16 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pybind11.setup_helpers import Pybind11Extension, build_ext -from pyomo.common.fileutils import this_file_dir, find_library +import glob import os -from distutils.dist import Distribution -import sys import shutil -import glob +import sys import tempfile +from distutils.dist import Distribution + +from pybind11.setup_helpers import Pybind11Extension, build_ext from pyomo.common.envvar import PYOMO_CONFIG_DIR +from pyomo.common.fileutils import find_library, this_file_dir def build_ginac_interface(args=[]): From c49c2df810775f1c64702a26e3cf75c36f717c99 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 21 Feb 2024 09:23:10 -0700 Subject: [PATCH 0697/3044] cleanup --- pyomo/contrib/simplification/build.py | 11 ++++++----- pyomo/contrib/simplification/ginac_interface.cpp | 11 +++++++++++ pyomo/contrib/simplification/simplify.py | 11 +++++++++++ pyomo/contrib/simplification/tests/__init__.py | 11 +++++++++++ .../simplification/tests/test_simplification.py | 11 +++++++++++ 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 4bf28a0fa33..d9d1e701290 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -21,7 +21,9 @@ from pyomo.common.fileutils import find_library, this_file_dir -def build_ginac_interface(args=[]): +def build_ginac_interface(args=None): + if args is None: + args = list() dname = this_file_dir() _sources = ['ginac_interface.cpp'] sources = list() @@ -29,7 +31,6 @@ def build_ginac_interface(args=[]): sources.append(os.path.join(dname, fname)) ginac_lib = find_library('ginac') - print(ginac_lib) if ginac_lib is None: raise RuntimeError( 'could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable' @@ -62,7 +63,7 @@ def build_ginac_interface(args=[]): extra_compile_args=extra_args, ) - class ginac_build_ext(build_ext): + class ginacBuildExt(build_ext): def run(self): basedir = os.path.abspath(os.path.curdir) if self.inplace: @@ -72,7 +73,7 @@ def run(self): print("Building in '%s'" % tmpdir) os.chdir(tmpdir) try: - super(ginac_build_ext, self).run() + super(ginacBuildExt, self).run() if not self.inplace: library = glob.glob("build/*/ginac_interface.*")[0] target = os.path.join( @@ -94,7 +95,7 @@ def run(self): 'name': 'ginac_interface', 'packages': [], 'ext_modules': [ext], - 'cmdclass': {"build_ext": ginac_build_ext}, + 'cmdclass': {"build_ext": ginacBuildExt}, } dist = Distribution(package_config) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index 32bea8dadd0..489f281bc2c 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -1,3 +1,14 @@ +// ___________________________________________________________________________ +// +// Pyomo: Python Optimization Modeling Objects +// Copyright (c) 2008-2022 +// National Technology and Engineering Solutions of Sandia, LLC +// Under the terms of Contract DE-NA0003525 with National Technology and +// Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +// rights in this software. +// This software is distributed under the 3-clause BSD License. +// ___________________________________________________________________________ + #include "ginac_interface.hpp" diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 4002f1a233f..b8cc4995f91 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression from pyomo.core.expr.numeric_expr import NumericExpression from pyomo.core.expr.numvalue import is_fixed, value diff --git a/pyomo/contrib/simplification/tests/__init__.py b/pyomo/contrib/simplification/tests/__init__.py index e69de29bb2d..9320e403e95 100644 --- a/pyomo/contrib/simplification/tests/__init__.py +++ b/pyomo/contrib/simplification/tests/__init__.py @@ -0,0 +1,11 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index e3c60cb02ca..95402f98318 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.common.unittest import TestCase from pyomo.common import unittest from pyomo.contrib.simplification import Simplifier From 29d6a19d0f1704a294d62d5a89370d6110a4fbe7 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 21 Feb 2024 09:29:35 -0700 Subject: [PATCH 0698/3044] cleanup --- pyomo/contrib/simplification/tests/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/simplification/tests/__init__.py b/pyomo/contrib/simplification/tests/__init__.py index 9320e403e95..d93cfd77b3c 100644 --- a/pyomo/contrib/simplification/tests/__init__.py +++ b/pyomo/contrib/simplification/tests/__init__.py @@ -8,4 +8,3 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - From 3e5cca27025a04d09ecf938433a9afb3534d501b Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 09:30:29 -0700 Subject: [PATCH 0699/3044] More updates to the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c548a8c830c..daba7cac96c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ Pyomo 6.7.1 (21 Feb 2024) - PyROS: Update Solver Argument Resolution and Validation Routines (#3126) - PyROS: Update Subproblem Initialization Routines (#3071) - PyROS: Fix DR polishing under nominal objective focus (#3060) + - solver: Solver Refactor Part 1: Introducing the new solver interface (#3137) ------------------------------------------------------------------------------- Pyomo 6.7.0 (29 Nov 2023) From d027b190e1331db30b40bf25c2ff4b4a0fccd621 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 09:35:49 -0700 Subject: [PATCH 0700/3044] Updating deprecation version --- pyomo/contrib/solver/base.py | 2 +- pyomo/core/base/suffix.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index f3d60bef03d..13bd5ddb212 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -396,7 +396,7 @@ def _map_config( "`keepfiles` has been deprecated in the new solver interface. " "Use `working_dir` instead to designate a directory in which " f"files should be generated and saved. Setting `working_dir` to `{cwd}`.", - version='6.7.1.dev0', + version='6.7.1', ) self.config.working_dir = cwd # I believe this currently does nothing; however, it is unclear what diff --git a/pyomo/core/base/suffix.py b/pyomo/core/base/suffix.py index 0c27eee060f..be2f732650d 100644 --- a/pyomo/core/base/suffix.py +++ b/pyomo/core/base/suffix.py @@ -341,7 +341,7 @@ def clear_all_values(self): @deprecated( 'Suffix.set_datatype is replaced with the Suffix.datatype property', - version='6.7.1.dev0', + version='6.7.1', ) def set_datatype(self, datatype): """ @@ -351,7 +351,7 @@ def set_datatype(self, datatype): @deprecated( 'Suffix.get_datatype is replaced with the Suffix.datatype property', - version='6.7.1.dev0', + version='6.7.1', ) def get_datatype(self): """ @@ -361,7 +361,7 @@ def get_datatype(self): @deprecated( 'Suffix.set_direction is replaced with the Suffix.direction property', - version='6.7.1.dev0', + version='6.7.1', ) def set_direction(self, direction): """ @@ -371,7 +371,7 @@ def set_direction(self, direction): @deprecated( 'Suffix.get_direction is replaced with the Suffix.direction property', - version='6.7.1.dev0', + version='6.7.1', ) def get_direction(self): """ From 156bf168ce817ad3a9c942b94535bf2876baae64 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 09:36:26 -0700 Subject: [PATCH 0701/3044] Updating RELEASE.md --- RELEASE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE.md b/RELEASE.md index 03baa803ac9..8313c969f25 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,6 +11,7 @@ The following are highlights of the 6.7 release series: - New writer for converting linear models to matrix form - New packages: - latex_printer (print Pyomo models to a LaTeX compatible format) + - contrib.solve: Part 1 of refactoring Pyomo's solver interfaces - ...and of course numerous minor bug fixes and performance enhancements A full list of updates and changes is available in the From a4d109425a4e7f1f271aeddf0fd536c909f6c9fc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 21 Feb 2024 09:36:46 -0700 Subject: [PATCH 0702/3044] Clarify some solver documentation --- .../developer_reference/solvers.rst | 113 +++++++++++++----- 1 file changed, 83 insertions(+), 30 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 45945c18b12..9f18119e373 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -1,10 +1,11 @@ Future Solver Interface Changes =============================== -Pyomo offers interfaces into multiple solvers, both commercial and open source. -To support better capabilities for solver interfaces, the Pyomo team is actively -redesigning the existing interfaces to make them more maintainable and intuitive -for use. Redesigned interfaces can be found in ``pyomo.contrib.solver``. +Pyomo offers interfaces into multiple solvers, both commercial and open +source. To support better capabilities for solver interfaces, the Pyomo +team is actively redesigning the existing interfaces to make them more +maintainable and intuitive for use. A preview of the redesigned +interfaces can be found in ``pyomo.contrib.solver``. .. currentmodule:: pyomo.contrib.solver @@ -12,27 +13,39 @@ for use. Redesigned interfaces can be found in ``pyomo.contrib.solver``. New Interface Usage ------------------- -The new interfaces have two modes: backwards compatible and future capability. -The future capability mode can be accessed directly or by switching the default -``SolverFactory`` version (see :doc:`future`). Currently, the new versions -available are: +The new interfaces are not completely backwards compatible with the +existing Pyomo solver interfaces. However, to aid in testing and +evaluation, we are distributing versions of the new solver interfaces +that are compatible with the existing ("legacy") solver interface. +These "legacy" interfaces are registered with the current +``SolverFactory`` using slightly different names (to avoid conflicts +with existing interfaces). -.. list-table:: Available Redesigned Solvers - :widths: 25 25 25 +.. |br| raw:: html + +
+ +.. list-table:: Available Redesigned Solvers and Names Registered + in the SolverFactories :header-rows: 1 * - Solver - - ``SolverFactory`` (v1) Name - - ``SolverFactory`` (v3) Name - * - ipopt - - ``ipopt_v2`` + - Name registered in the |br| ``pyomo.contrib.solver.factory.SolverFactory`` + - Name registered in the |br| ``pyomo.opt.base.solvers.LegacySolverFactory`` + * - Ipopt - ``ipopt`` + - ``ipopt_v2`` * - Gurobi - - ``gurobi_v2`` - ``gurobi`` + - ``gurobi_v2`` -Backwards Compatible Mode -^^^^^^^^^^^^^^^^^^^^^^^^^ +Using the new interfaces through the legacy interface +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here we use the new interface as exposed through the existing (legacy) +solver factory and solver interface wrapper. This provides an API that +is compatible with the existing (legacy) Pyomo solver interface and can +be used with other Pyomo tools / capabilities. .. testcode:: :skipif: not ipopt_available @@ -61,11 +74,10 @@ Backwards Compatible Mode ... 3 Declarations: x y obj -Future Capability Mode -^^^^^^^^^^^^^^^^^^^^^^ +Using the new interfaces directly +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -There are multiple ways to utilize the future capability mode: direct import -or changed ``SolverFactory`` version. +Here we use the new interface by importing it directly: .. testcode:: :skipif: not ipopt_available @@ -87,7 +99,7 @@ or changed ``SolverFactory`` version. opt = Ipopt() status = opt.solve(model) assert_optimal_termination(status) - # Displays important results information; only available in future capability mode + # Displays important results information; only available through the new interfaces status.display() model.pprint() @@ -99,7 +111,49 @@ or changed ``SolverFactory`` version. ... 3 Declarations: x y obj -Changing the ``SolverFactory`` version: +Using the new interfaces through the "new" SolverFactory +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here we use the new interface by retrieving it from the new ``SolverFactory``: + +.. testcode:: + :skipif: not ipopt_available + + # Direct import + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + from pyomo.contrib.solver.factory import SolverFactory + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + opt = SolverFactory('ipopt') + status = opt.solve(model) + assert_optimal_termination(status) + # Displays important results information; only available through the new interfaces + status.display() + model.pprint() + +.. testoutput:: + :skipif: not ipopt_available + :hide: + + solution_loader: ... + ... + 3 Declarations: x y obj + +Switching all of Pyomo to use the new interfaces +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We also provide a mechansim to get a "preview" of the future where we +replace the existing (legacy) SolverFactory and utilities with the new +(development) version: .. testcode:: :skipif: not ipopt_available @@ -120,7 +174,7 @@ Changing the ``SolverFactory`` version: status = pyo.SolverFactory('ipopt').solve(model) assert_optimal_termination(status) - # Displays important results information; only available in future capability mode + # Displays important results information; only available through the new interfaces status.display() model.pprint() @@ -141,16 +195,15 @@ Changing the ``SolverFactory`` version: Linear Presolve and Scaling ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The new interface will allow for direct manipulation of linear presolve and scaling -options for certain solvers. Currently, these options are only available for -``ipopt``. +The new interface allows access to new capabilities in the various +problem writers, including the linear presolve and scaling options +recently incorporated into the redesigned NL writer. For example, you +can control the NL writer in the new ``ipopt`` interface through the +solver's ``writer_config`` configuration option: .. autoclass:: pyomo.contrib.solver.ipopt.Ipopt :members: solve -The ``writer_config`` configuration option can be used to manipulate presolve -and scaling options: - .. testcode:: from pyomo.contrib.solver.ipopt import Ipopt From 286e99de1e076c19f74df6705d1b8493cfb82992 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 21 Feb 2024 09:42:55 -0700 Subject: [PATCH 0703/3044] Fix typos --- doc/OnlineDocs/developer_reference/solvers.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 9f18119e373..cdf36b74397 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -82,7 +82,7 @@ Here we use the new interface by importing it directly: .. testcode:: :skipif: not ipopt_available - # Direct import + # Direct import import pyomo.environ as pyo from pyomo.contrib.solver.util import assert_optimal_termination from pyomo.contrib.solver.ipopt import Ipopt @@ -119,7 +119,7 @@ Here we use the new interface by retrieving it from the new ``SolverFactory``: .. testcode:: :skipif: not ipopt_available - # Direct import + # Import through new SolverFactory import pyomo.environ as pyo from pyomo.contrib.solver.util import assert_optimal_termination from pyomo.contrib.solver.factory import SolverFactory @@ -151,14 +151,14 @@ Here we use the new interface by retrieving it from the new ``SolverFactory``: Switching all of Pyomo to use the new interfaces ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -We also provide a mechansim to get a "preview" of the future where we +We also provide a mechanism to get a "preview" of the future where we replace the existing (legacy) SolverFactory and utilities with the new (development) version: .. testcode:: :skipif: not ipopt_available - # Change SolverFactory version + # Change default SolverFactory version import pyomo.environ as pyo from pyomo.contrib.solver.util import assert_optimal_termination from pyomo.__future__ import solver_factory_v3 From 93fff175609a32262728a33a16e27866398b5f62 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 21 Feb 2024 09:45:34 -0700 Subject: [PATCH 0704/3044] Restore link to future docs --- doc/OnlineDocs/developer_reference/solvers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index cdf36b74397..7b17c4b40f0 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -153,7 +153,7 @@ Switching all of Pyomo to use the new interfaces We also provide a mechanism to get a "preview" of the future where we replace the existing (legacy) SolverFactory and utilities with the new -(development) version: +(development) version (see :doc:`future`): .. testcode:: :skipif: not ipopt_available From a906f9fb760d64bf96d60aa0093ddafacefd14d7 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Wed, 21 Feb 2024 09:52:29 -0700 Subject: [PATCH 0705/3044] Fix incorrect docstring --- pyomo/contrib/solver/results.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index 699137d2fc9..cbc04681235 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -164,10 +164,12 @@ class Results(ConfigDict): iteration_count: int The total number of iterations. timing_info: ConfigDict - A ConfigDict containing two pieces of information: - start_timestamp: UTC timestamp of when run was initiated - wall_time: elapsed wall clock time for entire process - timer: a HierarchicalTimer object containing timing data about the solve + A ConfigDict containing three pieces of information: + - ``start_timestamp``: UTC timestamp of when run was initiated + - ``wall_time``: elapsed wall clock time for entire process + - ``timer``: a HierarchicalTimer object containing timing data about the solve + + Specific solvers may add other relevant timing information, as appropriate. extra_info: ConfigDict A ConfigDict to store extra information such as solver messages. solver_configuration: ConfigDict From cf5dc9c954700d106152ff6afc47a766a4062001 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 21 Feb 2024 09:55:39 -0700 Subject: [PATCH 0706/3044] Add warning / link to #1030 --- doc/OnlineDocs/developer_reference/solvers.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 7b17c4b40f0..6168da3480e 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -1,6 +1,16 @@ Future Solver Interface Changes =============================== +.. note:: + + The new solver interfaces are still under active development. They + are included in the releases as development previews. Please be + aware that APIs and functionality may change with no notice. + + We welcome any feedback and ideas as we develop this capability. + Please post feedback on + `Issue 1030 `_. + Pyomo offers interfaces into multiple solvers, both commercial and open source. To support better capabilities for solver interfaces, the Pyomo team is actively redesigning the existing interfaces to make them more From 63cc14d28a4b15552bb2e8d82eae5dcc75bbac2b Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 10:02:09 -0700 Subject: [PATCH 0707/3044] More edits to the CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daba7cac96c..faa2fa094f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,7 +77,7 @@ Pyomo 6.7.1 (21 Feb 2024) - PyROS: Update Solver Argument Resolution and Validation Routines (#3126) - PyROS: Update Subproblem Initialization Routines (#3071) - PyROS: Fix DR polishing under nominal objective focus (#3060) - - solver: Solver Refactor Part 1: Introducing the new solver interface (#3137) + - solver: Solver Refactor Part 1: Introducing the new solver interface (#3137, #3156) ------------------------------------------------------------------------------- Pyomo 6.7.0 (29 Nov 2023) From 83040cdfd08a26aab94966a50b1e5e4d16cc62fa Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 10:10:41 -0700 Subject: [PATCH 0708/3044] More updates to the CHANGELOG --- CHANGELOG.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index faa2fa094f8..c06e0f71378 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ Pyomo 6.7.1 (21 Feb 2024) - Generalize the simple_constraint_rule decorator (#3152) - Fix edge case assigning new numeric types to Var/Param with units (#3151) - Add private_data to `_BlockData` (#3138) - - Convert implicit sets created by `IndexedComponent`s to "anonymous" sets (#3075) + - IndexComponent create implicit sets as "anonymous" sets (#3075) - Add `all_different` and `count_if` to the logical expression system (#3058) - Fix RangeSet.__len__ when defined by floats (#3119) - Overhaul the `Suffix` component (#3072) @@ -38,9 +38,11 @@ Pyomo 6.7.1 (21 Feb 2024) - Update intersphinx links, remove docs for nonfunctional code (#3155) - Update MPC documentation and citation (#3148) - Fix an error in the documentation for LinearExpression (#3090) - - Fix bugs in the documentation of Pyomo.DoE (#3070) - - Fix a latex_printer vestige in the documentation (#3066) + - Fix Pyomo.DoE documentation (#3070) + - Fix latex_printer documentation (#3066) - Solver Interfaces + - Preview release of new solver interfaces as pyomo.contrib.solver + (#3137, #3156) - Make error msg more explicit wrt different interfaces (#3141) - NLv2: only raise exception for empty models in the legacy API (#3135) - Add `to_expr()` to AMPLRepn, fix NLWriterInfo return type (#3095) @@ -69,15 +71,12 @@ Pyomo 6.7.1 (21 Feb 2024) - incidence_analysis: Method to add an edge in IncidenceGraphInterface (#3120) - incidence_analysis: Add subgraph method to IncidencegraphInterface (#3122) - incidence_analysis: Add `ampl_repn` option (#3069) - - incidence_analysis: Fix config documentation of `linear_only` argument in - `get_incident_variables` (#3067) - - interior_point: Workaround for improvement in Mumps memory prediction - algorithm (#3114) + - incidence_analysis: Update documentation (#3067) + - interior_point: Resolve test failure due to Mumps update (#3114) - MindtPy: Various bug fixes (#3034) - PyROS: Update Solver Argument Resolution and Validation Routines (#3126) - PyROS: Update Subproblem Initialization Routines (#3071) - PyROS: Fix DR polishing under nominal objective focus (#3060) - - solver: Solver Refactor Part 1: Introducing the new solver interface (#3137, #3156) ------------------------------------------------------------------------------- Pyomo 6.7.0 (29 Nov 2023) From 7b7f3881103a0333453417b268f87a259d1eeeec Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Wed, 21 Feb 2024 10:21:32 -0700 Subject: [PATCH 0709/3044] Update for 6.7.1 release --- .coin-or/projDesc.xml | 4 ++-- README.md | 2 +- RELEASE.md | 5 +++-- pyomo/version/info.py | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.coin-or/projDesc.xml b/.coin-or/projDesc.xml index 1ee247e100f..da977677d1f 100644 --- a/.coin-or/projDesc.xml +++ b/.coin-or/projDesc.xml @@ -227,8 +227,8 @@ Carl D. Laird, Chair, Pyomo Management Committee, claird at andrew dot cmu dot e Use explicit overrides to disable use of automated version reporting. --> - 6.7.0 - 6.7.0 + 6.7.1 + 6.7.1 diff --git a/README.md b/README.md index 2f8a25403c2..95558e52a42 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ To get help from the Pyomo community ask a question on one of the following: ### Developers -Pyomo development moved to this repository in June, 2016 from +Pyomo development moved to this repository in June 2016 from Sandia National Laboratories. Developer discussions are hosted by [Google Groups](https://groups.google.com/forum/#!forum/pyomo-developers). diff --git a/RELEASE.md b/RELEASE.md index 8313c969f25..9b101e0999a 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,4 +1,4 @@ -We are pleased to announce the release of Pyomo 6.7.0. +We are pleased to announce the release of Pyomo 6.7.1. Pyomo is a collection of Python software packages that supports a diverse set of optimization capabilities for formulating and analyzing @@ -9,9 +9,10 @@ The following are highlights of the 6.7 release series: - Added support for Python 3.12 - Removed support for Python 3.7 - New writer for converting linear models to matrix form + - Improved handling of nested GDPs - New packages: - latex_printer (print Pyomo models to a LaTeX compatible format) - - contrib.solve: Part 1 of refactoring Pyomo's solver interfaces + - contrib.solver: preview of redesigned solver interfaces - ...and of course numerous minor bug fixes and performance enhancements A full list of updates and changes is available in the diff --git a/pyomo/version/info.py b/pyomo/version/info.py index 0db00ac240f..dae1b6b6c7f 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -27,8 +27,8 @@ major = 6 minor = 7 micro = 1 -releaselevel = 'invalid' -# releaselevel = 'final' +# releaselevel = 'invalid' +releaselevel = 'final' serial = 0 if releaselevel == 'final': From e7ec104640433f9e507dc7690664974075b4b9d9 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 21 Feb 2024 10:39:18 -0700 Subject: [PATCH 0710/3044] Resetting main for development (6.7.2.dev0) --- pyomo/version/info.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/version/info.py b/pyomo/version/info.py index dae1b6b6c7f..de2efe83fb6 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -26,9 +26,9 @@ # main and needs a hard reference to "suitably new" development. major = 6 minor = 7 -micro = 1 -# releaselevel = 'invalid' -releaselevel = 'final' +micro = 2 +releaselevel = 'invalid' +# releaselevel = 'final' serial = 0 if releaselevel == 'final': From 04f68dcefedde1cab5b502778504e4189a647308 Mon Sep 17 00:00:00 2001 From: Shawn Martin Date: Wed, 21 Feb 2024 13:56:25 -0700 Subject: [PATCH 0711/3044] Integrated deprecated parmest UI into new UI. Removed deprecated folder. --- pyomo/contrib/parmest/deprecated/__init__.py | 10 - .../parmest/deprecated/examples/__init__.py | 10 - .../examples/reaction_kinetics/__init__.py | 10 - .../simple_reaction_parmest_example.py | 118 -- .../examples/reactor_design/__init__.py | 10 - .../reactor_design/bootstrap_example.py | 60 - .../reactor_design/datarec_example.py | 100 -- .../reactor_design/leaveNout_example.py | 98 -- .../likelihood_ratio_example.py | 64 - .../multisensor_data_example.py | 51 - .../parameter_estimation_example.py | 60 - .../examples/reactor_design/reactor_data.csv | 20 - .../reactor_data_multisensor.csv | 20 - .../reactor_data_timeseries.csv | 20 - .../examples/reactor_design/reactor_design.py | 104 -- .../reactor_design/timeseries_data_example.py | 56 - .../examples/rooney_biegler/__init__.py | 10 - .../rooney_biegler/bootstrap_example.py | 57 - .../likelihood_ratio_example.py | 62 - .../parameter_estimation_example.py | 60 - .../examples/rooney_biegler/rooney_biegler.py | 60 - .../rooney_biegler_with_constraint.py | 63 - .../deprecated/examples/semibatch/__init__.py | 10 - .../examples/semibatch/bootstrap_theta.csv | 101 -- .../deprecated/examples/semibatch/exp1.out | 1 - .../deprecated/examples/semibatch/exp10.out | 1 - .../deprecated/examples/semibatch/exp11.out | 1 - .../deprecated/examples/semibatch/exp12.out | 1 - .../deprecated/examples/semibatch/exp13.out | 1 - .../deprecated/examples/semibatch/exp14.out | 1 - .../deprecated/examples/semibatch/exp2.out | 1 - .../deprecated/examples/semibatch/exp3.out | 1 - .../deprecated/examples/semibatch/exp4.out | 1 - .../deprecated/examples/semibatch/exp5.out | 1 - .../deprecated/examples/semibatch/exp6.out | 1 - .../deprecated/examples/semibatch/exp7.out | 1 - .../deprecated/examples/semibatch/exp8.out | 1 - .../deprecated/examples/semibatch/exp9.out | 1 - .../examples/semibatch/obj_at_theta.csv | 1009 ------------ .../examples/semibatch/parallel_example.py | 57 - .../semibatch/parameter_estimation_example.py | 42 - .../examples/semibatch/scenario_example.py | 52 - .../examples/semibatch/scenarios.csv | 11 - .../examples/semibatch/semibatch.py | 287 ---- pyomo/contrib/parmest/deprecated/parmest.py | 1361 ----------------- .../parmest/deprecated/scenariocreator.py | 166 -- .../parmest/deprecated/tests/__init__.py | 10 - .../parmest/deprecated/tests/scenarios.csv | 11 - .../parmest/deprecated/tests/test_examples.py | 204 --- .../parmest/deprecated/tests/test_graphics.py | 68 - .../parmest/deprecated/tests/test_parmest.py | 956 ------------ .../deprecated/tests/test_scenariocreator.py | 146 -- .../parmest/deprecated/tests/test_solver.py | 75 - .../parmest/deprecated/tests/test_utils.py | 68 - pyomo/contrib/parmest/parmest.py | 1125 +++++++++++++- pyomo/contrib/parmest/scenariocreator.py | 74 +- pyomo/contrib/parmest/tests/test_parmest.py | 1048 ++++++++++++- .../parmest/tests/test_scenariocreator.py | 448 ++++++ 58 files changed, 2674 insertions(+), 5792 deletions(-) delete mode 100644 pyomo/contrib/parmest/deprecated/__init__.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/__init__.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/__init__.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/simple_reaction_parmest_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/__init__.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data.csv delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_multisensor.csv delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_timeseries.csv delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_design.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/rooney_biegler/__init__.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/rooney_biegler/bootstrap_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/rooney_biegler/likelihood_ratio_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/rooney_biegler/parameter_estimation_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler_with_constraint.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/__init__.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/bootstrap_theta.csv delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/obj_at_theta.csv delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/parallel_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/parameter_estimation_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/scenario_example.py delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/scenarios.csv delete mode 100644 pyomo/contrib/parmest/deprecated/examples/semibatch/semibatch.py delete mode 100644 pyomo/contrib/parmest/deprecated/parmest.py delete mode 100644 pyomo/contrib/parmest/deprecated/scenariocreator.py delete mode 100644 pyomo/contrib/parmest/deprecated/tests/__init__.py delete mode 100644 pyomo/contrib/parmest/deprecated/tests/scenarios.csv delete mode 100644 pyomo/contrib/parmest/deprecated/tests/test_examples.py delete mode 100644 pyomo/contrib/parmest/deprecated/tests/test_graphics.py delete mode 100644 pyomo/contrib/parmest/deprecated/tests/test_parmest.py delete mode 100644 pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py delete mode 100644 pyomo/contrib/parmest/deprecated/tests/test_solver.py delete mode 100644 pyomo/contrib/parmest/deprecated/tests/test_utils.py diff --git a/pyomo/contrib/parmest/deprecated/__init__.py b/pyomo/contrib/parmest/deprecated/__init__.py deleted file mode 100644 index d93cfd77b3c..00000000000 --- a/pyomo/contrib/parmest/deprecated/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/examples/__init__.py b/pyomo/contrib/parmest/deprecated/examples/__init__.py deleted file mode 100644 index d93cfd77b3c..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/__init__.py b/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/__init__.py deleted file mode 100644 index d93cfd77b3c..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/simple_reaction_parmest_example.py deleted file mode 100644 index 719a930251c..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ /dev/null @@ -1,118 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ -''' -Example from Y. Bard, "Nonlinear Parameter Estimation", (pg. 124) - -This example shows: -1. How to define the unknown (to be regressed parameters) with an index -2. How to call parmest to only estimate some of the parameters (and fix the rest) - -Code provided by Paul Akula. -''' - -from pyomo.environ import ( - ConcreteModel, - Param, - Var, - PositiveReals, - Objective, - Constraint, - RangeSet, - Expression, - minimize, - exp, - value, -) -import pyomo.contrib.parmest.parmest as parmest - - -def simple_reaction_model(data): - # Create the concrete model - model = ConcreteModel() - - model.x1 = Param(initialize=float(data['x1'])) - model.x2 = Param(initialize=float(data['x2'])) - - # Rate constants - model.rxn = RangeSet(2) - initial_guess = {1: 750, 2: 1200} - model.k = Var(model.rxn, initialize=initial_guess, within=PositiveReals) - - # reaction product - model.y = Expression(expr=exp(-model.k[1] * model.x1 * exp(-model.k[2] / model.x2))) - - # fix all of the regressed parameters - model.k.fix() - - # =================================================================== - # Stage-specific cost computations - def ComputeFirstStageCost_rule(model): - return 0 - - model.FirstStageCost = Expression(rule=ComputeFirstStageCost_rule) - - def AllMeasurements(m): - return (float(data['y']) - m.y) ** 2 - - model.SecondStageCost = Expression(rule=AllMeasurements) - - def total_cost_rule(m): - return m.FirstStageCost + m.SecondStageCost - - model.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) - - return model - - -def main(): - # Data from Table 5.2 in Y. Bard, "Nonlinear Parameter Estimation", (pg. 124) - data = [ - {'experiment': 1, 'x1': 0.1, 'x2': 100, 'y': 0.98}, - {'experiment': 2, 'x1': 0.2, 'x2': 100, 'y': 0.983}, - {'experiment': 3, 'x1': 0.3, 'x2': 100, 'y': 0.955}, - {'experiment': 4, 'x1': 0.4, 'x2': 100, 'y': 0.979}, - {'experiment': 5, 'x1': 0.5, 'x2': 100, 'y': 0.993}, - {'experiment': 6, 'x1': 0.05, 'x2': 200, 'y': 0.626}, - {'experiment': 7, 'x1': 0.1, 'x2': 200, 'y': 0.544}, - {'experiment': 8, 'x1': 0.15, 'x2': 200, 'y': 0.455}, - {'experiment': 9, 'x1': 0.2, 'x2': 200, 'y': 0.225}, - {'experiment': 10, 'x1': 0.25, 'x2': 200, 'y': 0.167}, - {'experiment': 11, 'x1': 0.02, 'x2': 300, 'y': 0.566}, - {'experiment': 12, 'x1': 0.04, 'x2': 300, 'y': 0.317}, - {'experiment': 13, 'x1': 0.06, 'x2': 300, 'y': 0.034}, - {'experiment': 14, 'x1': 0.08, 'x2': 300, 'y': 0.016}, - {'experiment': 15, 'x1': 0.1, 'x2': 300, 'y': 0.006}, - ] - - # ======================================================================= - # Parameter estimation without covariance estimate - # Only estimate the parameter k[1]. The parameter k[2] will remain fixed - # at its initial value - theta_names = ['k[1]'] - pest = parmest.Estimator(simple_reaction_model, data, theta_names) - obj, theta = pest.theta_est() - print(obj) - print(theta) - print() - - # ======================================================================= - # Estimate both k1 and k2 and compute the covariance matrix - theta_names = ['k'] - pest = parmest.Estimator(simple_reaction_model, data, theta_names) - n = 15 # total number of data points used in the objective (y in 15 scenarios) - obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) - print(obj) - print(theta) - print(cov) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/__init__.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/__init__.py deleted file mode 100644 index d93cfd77b3c..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py deleted file mode 100644 index 3820b78c9b1..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/bootstrap_example.py +++ /dev/null @@ -1,60 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pandas as pd -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, -) - - -def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - - # Data - file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, "reactor_data.csv")) - data = pd.read_csv(file_name) - - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) - - # Parameter estimation - obj, theta = pest.theta_est() - - # Parameter estimation with bootstrap resampling - bootstrap_theta = pest.theta_est_bootstrap(50) - - # Plot results - parmest.graphics.pairwise_plot(bootstrap_theta, title="Bootstrap theta") - parmest.graphics.pairwise_plot( - bootstrap_theta, - theta, - 0.8, - ["MVN", "KDE", "Rect"], - title="Bootstrap theta with confidence regions", - ) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py deleted file mode 100644 index bae538f364c..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/datarec_example.py +++ /dev/null @@ -1,100 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import numpy as np -import pandas as pd -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, -) - -np.random.seed(1234) - - -def reactor_design_model_for_datarec(data): - # Unfix inlet concentration for data rec - model = reactor_design_model(data) - model.caf.fixed = False - - return model - - -def generate_data(): - ### Generate data based on real sv, caf, ca, cb, cc, and cd - sv_real = 1.05 - caf_real = 10000 - ca_real = 3458.4 - cb_real = 1060.8 - cc_real = 1683.9 - cd_real = 1898.5 - - data = pd.DataFrame() - ndata = 200 - # Normal distribution, mean = 3400, std = 500 - data["ca"] = 500 * np.random.randn(ndata) + 3400 - # Random distribution between 500 and 1500 - data["cb"] = np.random.rand(ndata) * 1000 + 500 - # Lognormal distribution - data["cc"] = np.random.lognormal(np.log(1600), 0.25, ndata) - # Triangular distribution between 1000 and 2000 - data["cd"] = np.random.triangular(1000, 1800, 3000, size=ndata) - - data["sv"] = sv_real - data["caf"] = caf_real - - return data - - -def main(): - # Generate data - data = generate_data() - data_std = data.std() - - # Define sum of squared error objective function for data rec - def SSE(model, data): - expr = ( - ((float(data.iloc[0]["ca"]) - model.ca) / float(data_std["ca"])) ** 2 - + ((float(data.iloc[0]["cb"]) - model.cb) / float(data_std["cb"])) ** 2 - + ((float(data.iloc[0]["cc"]) - model.cc) / float(data_std["cc"])) ** 2 - + ((float(data.iloc[0]["cd"]) - model.cd) / float(data_std["cd"])) ** 2 - ) - return expr - - ### Data reconciliation - theta_names = [] # no variables to estimate, use initialized values - - pest = parmest.Estimator(reactor_design_model_for_datarec, data, theta_names, SSE) - - obj, theta, data_rec = pest.theta_est(return_values=["ca", "cb", "cc", "cd", "caf"]) - print(obj) - print(theta) - - parmest.graphics.grouped_boxplot( - data[["ca", "cb", "cc", "cd"]], - data_rec[["ca", "cb", "cc", "cd"]], - group_names=["Data", "Data Rec"], - ) - - ### Parameter estimation using reconciled data - theta_names = ["k1", "k2", "k3"] - data_rec["sv"] = data["sv"] - - pest = parmest.Estimator(reactor_design_model, data_rec, theta_names, SSE) - obj, theta = pest.theta_est() - print(obj) - print(theta) - - theta_real = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} - print(theta_real) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py deleted file mode 100644 index d4ca9651753..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/leaveNout_example.py +++ /dev/null @@ -1,98 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import numpy as np -import pandas as pd -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, -) - - -def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - - # Data - file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, "reactor_data.csv")) - data = pd.read_csv(file_name) - - # Create more data for the example - N = 50 - df_std = data.std().to_frame().transpose() - df_rand = pd.DataFrame(np.random.normal(size=N)) - df_sample = data.sample(N, replace=True).reset_index(drop=True) - data = df_sample + df_rand.dot(df_std) / 10 - - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) - - # Parameter estimation - obj, theta = pest.theta_est() - print(obj) - print(theta) - - ### Parameter estimation with 'leave-N-out' - # Example use case: For each combination of data where one data point is left - # out, estimate theta - lNo_theta = pest.theta_est_leaveNout(1) - print(lNo_theta.head()) - - parmest.graphics.pairwise_plot(lNo_theta, theta) - - ### Leave one out/boostrap analysis - # Example use case: leave 25 data points out, run 20 bootstrap samples with the - # remaining points, determine if the theta estimate using the points left out - # is inside or outside an alpha region based on the bootstrap samples, repeat - # 5 times. Results are stored as a list of tuples, see API docs for information. - lNo = 25 - lNo_samples = 5 - bootstrap_samples = 20 - dist = "MVN" - alphas = [0.7, 0.8, 0.9] - - results = pest.leaveNout_bootstrap_test( - lNo, lNo_samples, bootstrap_samples, dist, alphas, seed=524 - ) - - # Plot results for a single value of alpha - alpha = 0.8 - for i in range(lNo_samples): - theta_est_N = results[i][1] - bootstrap_results = results[i][2] - parmest.graphics.pairwise_plot( - bootstrap_results, - theta_est_N, - alpha, - ["MVN"], - title="Alpha: " + str(alpha) + ", " + str(theta_est_N.loc[0, alpha]), - ) - - # Extract the percent of points that are within the alpha region - r = [results[i][1].loc[0, alpha] for i in range(lNo_samples)] - percent_true = sum(r) / len(r) - print(percent_true) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py deleted file mode 100644 index c47acf7f932..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/likelihood_ratio_example.py +++ /dev/null @@ -1,64 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import numpy as np -import pandas as pd -from itertools import product -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, -) - - -def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - - # Data - file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, "reactor_data.csv")) - data = pd.read_csv(file_name) - - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) - - # Parameter estimation - obj, theta = pest.theta_est() - - # Find the objective value at each theta estimate - k1 = [0.8, 0.85, 0.9] - k2 = [1.6, 1.65, 1.7] - k3 = [0.00016, 0.000165, 0.00017] - theta_vals = pd.DataFrame(list(product(k1, k2, k3)), columns=["k1", "k2", "k3"]) - obj_at_theta = pest.objective_at_theta(theta_vals) - - # Run the likelihood ratio test - LR = pest.likelihood_ratio_test(obj_at_theta, obj, [0.8, 0.85, 0.9, 0.95]) - - # Plot results - parmest.graphics.pairwise_plot( - LR, theta, 0.9, title="LR results within 90% confidence region" - ) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py deleted file mode 100644 index 84c4abdf92a..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/multisensor_data_example.py +++ /dev/null @@ -1,51 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pandas as pd -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, -) - - -def main(): - # Parameter estimation using multisensor data - - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - - # Data, includes multiple sensors for ca and cc - file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, "reactor_data_multisensor.csv")) - data = pd.read_csv(file_name) - - # Sum of squared error function - def SSE_multisensor(model, data): - expr = ( - ((float(data.iloc[0]["ca1"]) - model.ca) ** 2) * (1 / 3) - + ((float(data.iloc[0]["ca2"]) - model.ca) ** 2) * (1 / 3) - + ((float(data.iloc[0]["ca3"]) - model.ca) ** 2) * (1 / 3) - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + ((float(data.iloc[0]["cc1"]) - model.cc) ** 2) * (1 / 2) - + ((float(data.iloc[0]["cc2"]) - model.cc) ** 2) * (1 / 2) - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE_multisensor) - obj, theta = pest.theta_est() - print(obj) - print(theta) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py deleted file mode 100644 index f5d9364097e..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/parameter_estimation_example.py +++ /dev/null @@ -1,60 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pandas as pd -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, -) - - -def main(): - # Vars to estimate - theta_names = ["k1", "k2", "k3"] - - # Data - file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, "reactor_data.csv")) - data = pd.read_csv(file_name) - - # Sum of squared error function - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) - - # Parameter estimation - obj, theta = pest.theta_est() - print(obj) - print(theta) - - # Assert statements compare parameter estimation (theta) to an expected value - k1_expected = 5.0 / 6.0 - k2_expected = 5.0 / 3.0 - k3_expected = 1.0 / 6000.0 - relative_error = abs(theta["k1"] - k1_expected) / k1_expected - assert relative_error < 0.05 - relative_error = abs(theta["k2"] - k2_expected) / k2_expected - assert relative_error < 0.05 - relative_error = abs(theta["k3"] - k3_expected) / k3_expected - assert relative_error < 0.05 - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data.csv b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data.csv deleted file mode 100644 index c0695c049c4..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data.csv +++ /dev/null @@ -1,20 +0,0 @@ -sv,caf,ca,cb,cc,cd -1.06,10010,3407.4,945.4,1717.1,1931.9 -1.11,10010,3631.6,1247.2,1694.1,1960.6 -1.16,10010,3645.3,971.4,1552.3,1898.8 -1.21,10002,3536.2,1225.9,1351.1,1757.0 -1.26,10002,3755.6,1263.8,1562.3,1952.2 -1.30,10007,3598.3,1153.4,1413.4,1903.3 -1.35,10007,3939.0,971.4,1416.9,1794.9 -1.41,10009,4227.9,986.3,1188.7,1821.5 -1.45,10001,4163.1,972.5,1085.6,1908.7 -1.50,10002,3896.3,977.3,1132.9,2080.5 -1.56,10004,3801.6,1040.6,1157.7,1780.0 -1.60,10008,4128.4,1198.6,1150.0,1581.9 -1.66,10002,4385.4,1158.7,970.0,1629.8 -1.70,10007,3960.8,1194.9,1091.2,1835.5 -1.76,10007,4180.8,1244.2,1034.8,1739.5 -1.80,10001,4212.3,1240.7,1010.3,1739.6 -1.85,10004,4200.2,1164.0,931.5,1783.7 -1.90,10009,4748.6,1037.9,1065.9,1685.6 -1.96,10009,4941.3,1038.5,996.0,1855.7 diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_multisensor.csv b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_multisensor.csv deleted file mode 100644 index 9df745a8422..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_multisensor.csv +++ /dev/null @@ -1,20 +0,0 @@ -sv,caf,ca1,ca2,ca3,cb,cc1,cc2,cd -1.06,10010,3407.4,3363.1,3759.1,945.4,1717.1,1695.1,1931.9 -1.11,10010,3631.6,3345.2,3906.0,1247.2,1694.1,1536.7,1960.6 -1.16,10010,3645.3,3784.9,3301.3,971.4,1552.3,1496.2,1898.8 -1.21,10002,3536.2,3718.3,3678.5,1225.9,1351.1,1549.7,1757.0 -1.26,10002,3755.6,3731.8,3854.7,1263.8,1562.3,1410.1,1952.2 -1.30,10007,3598.3,3751.6,3722.5,1153.4,1413.4,1291.6,1903.3 -1.35,10007,3939.0,3969.5,3827.2,971.4,1416.9,1276.8,1794.9 -1.41,10009,4227.9,3721.3,4046.7,986.3,1188.7,1221.0,1821.5 -1.45,10001,4163.1,4142.7,4512.1,972.5,1085.6,1212.1,1908.7 -1.50,10002,3896.3,3953.7,4028.0,977.3,1132.9,1167.7,2080.5 -1.56,10004,3801.6,4263.3,4015.3,1040.6,1157.7,1236.5,1780.0 -1.60,10008,4128.4,4061.1,3914.8,1198.6,1150.0,1032.2,1581.9 -1.66,10002,4385.4,4344.7,4006.8,1158.7,970.0,1155.1,1629.8 -1.70,10007,3960.8,4259.1,4274.7,1194.9,1091.2,958.6,1835.5 -1.76,10007,4180.8,4071.1,4598.7,1244.2,1034.8,1086.8,1739.5 -1.80,10001,4212.3,4541.8,4440.0,1240.7,1010.3,920.8,1739.6 -1.85,10004,4200.2,4444.9,4667.2,1164.0,931.5,850.7,1783.7 -1.90,10009,4748.6,4813.4,4753.2,1037.9,1065.9,898.5,1685.6 -1.96,10009,4941.3,4511.8,4405.4,1038.5,996.0,921.9,1855.7 diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_timeseries.csv b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_timeseries.csv deleted file mode 100644 index 1421cfef6a0..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_data_timeseries.csv +++ /dev/null @@ -1,20 +0,0 @@ -experiment,time,sv,caf,ca,cb,cc,cd -0,18000,1.075,10008,3537.5,1077.2,1591.2,1938.7 -0,18060,1.121,10002,3547.7,1186.2,1766.3,1946.9 -0,18120,1.095,10005,3614.4,1009.9,1702.9,1841.8 -0,18180,1.102,10007,3443.7,863.1,1666.2,1918.7 -0,18240,1.105,10002,3687.1,1052.1,1501.7,1905.0 -0,18300,1.084,10008,3452.7,1000.5,1512.0,2043.4 -1,18360,1.159,10009,3427.8,1133.1,1481.1,1837.1 -1,18420,1.432,10010,4029.8,1058.8,1213.0,1911.1 -1,18480,1.413,10005,3953.1,960.1,1304.8,1754.3 -1,18540,1.475,10008,4034.8,1121.2,1351.0,1992.0 -1,18600,1.433,10002,4029.8,1100.6,1199.5,1713.9 -1,18660,1.488,10006,3972.8,1148.0,1380.7,1992.1 -1,18720,1.456,10003,4031.2,1145.2,1133.1,1812.6 -2,18780,1.821,10008,4499.1,980.8,924.7,1840.9 -2,18840,1.856,10005,4370.9,1000.7,833.4,1848.4 -2,18900,1.846,10002,4438.6,1038.6,1042.8,1703.3 -2,18960,1.852,10002,4468.4,1151.8,1119.1,1564.8 -2,19020,1.865,10009,4341.6,1060.5,844.2,1974.8 -2,19080,1.872,10002,4427.0,964.6,840.2,1928.5 diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_design.py deleted file mode 100644 index 16f65e236eb..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/reactor_design.py +++ /dev/null @@ -1,104 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ -""" -Continuously stirred tank reactor model, based on -pyomo/examples/doc/pyomobook/nonlinear-ch/react_design/ReactorDesign.py -""" -import pandas as pd -from pyomo.environ import ( - ConcreteModel, - Param, - Var, - PositiveReals, - Objective, - Constraint, - maximize, - SolverFactory, -) - - -def reactor_design_model(data): - # Create the concrete model - model = ConcreteModel() - - # Rate constants - model.k1 = Param(initialize=5.0 / 6.0, within=PositiveReals, mutable=True) # min^-1 - model.k2 = Param(initialize=5.0 / 3.0, within=PositiveReals, mutable=True) # min^-1 - model.k3 = Param( - initialize=1.0 / 6000.0, within=PositiveReals, mutable=True - ) # m^3/(gmol min) - - # Inlet concentration of A, gmol/m^3 - if isinstance(data, dict) or isinstance(data, pd.Series): - model.caf = Param(initialize=float(data["caf"]), within=PositiveReals) - elif isinstance(data, pd.DataFrame): - model.caf = Param(initialize=float(data.iloc[0]["caf"]), within=PositiveReals) - else: - raise ValueError("Unrecognized data type.") - - # Space velocity (flowrate/volume) - if isinstance(data, dict) or isinstance(data, pd.Series): - model.sv = Param(initialize=float(data["sv"]), within=PositiveReals) - elif isinstance(data, pd.DataFrame): - model.sv = Param(initialize=float(data.iloc[0]["sv"]), within=PositiveReals) - else: - raise ValueError("Unrecognized data type.") - - # Outlet concentration of each component - model.ca = Var(initialize=5000.0, within=PositiveReals) - model.cb = Var(initialize=2000.0, within=PositiveReals) - model.cc = Var(initialize=2000.0, within=PositiveReals) - model.cd = Var(initialize=1000.0, within=PositiveReals) - - # Objective - model.obj = Objective(expr=model.cb, sense=maximize) - - # Constraints - model.ca_bal = Constraint( - expr=( - 0 - == model.sv * model.caf - - model.sv * model.ca - - model.k1 * model.ca - - 2.0 * model.k3 * model.ca**2.0 - ) - ) - - model.cb_bal = Constraint( - expr=(0 == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb) - ) - - model.cc_bal = Constraint(expr=(0 == -model.sv * model.cc + model.k2 * model.cb)) - - model.cd_bal = Constraint( - expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) - ) - - return model - - -def main(): - # For a range of sv values, return ca, cb, cc, and cd - results = [] - sv_values = [1.0 + v * 0.05 for v in range(1, 20)] - caf = 10000 - for sv in sv_values: - model = reactor_design_model(pd.DataFrame(data={"caf": [caf], "sv": [sv]})) - solver = SolverFactory("ipopt") - solver.solve(model) - results.append([sv, caf, model.ca(), model.cb(), model.cc(), model.cd()]) - - results = pd.DataFrame(results, columns=["sv", "caf", "ca", "cb", "cc", "cd"]) - print(results) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py deleted file mode 100644 index e7acefc2224..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/reactor_design/timeseries_data_example.py +++ /dev/null @@ -1,56 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pandas as pd -from os.path import join, abspath, dirname - -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, -) -from pyomo.contrib.parmest.deprecated.parmest import group_data - - -def main(): - # Parameter estimation using timeseries data - - # Vars to estimate - theta_names = ['k1', 'k2', 'k3'] - - # Data, includes multiple sensors for ca and cc - file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, 'reactor_data_timeseries.csv')) - data = pd.read_csv(file_name) - - # Group time series data into experiments, return the mean value for sv and caf - # Returns a list of dictionaries - data_ts = group_data(data, 'experiment', ['sv', 'caf']) - - def SSE_timeseries(model, data): - expr = 0 - for val in data['ca']: - expr = expr + ((float(val) - model.ca) ** 2) * (1 / len(data['ca'])) - for val in data['cb']: - expr = expr + ((float(val) - model.cb) ** 2) * (1 / len(data['cb'])) - for val in data['cc']: - expr = expr + ((float(val) - model.cc) ** 2) * (1 / len(data['cc'])) - for val in data['cd']: - expr = expr + ((float(val) - model.cd) ** 2) * (1 / len(data['cd'])) - return expr - - pest = parmest.Estimator(reactor_design_model, data_ts, theta_names, SSE_timeseries) - obj, theta = pest.theta_est() - print(obj) - print(theta) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/__init__.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/__init__.py deleted file mode 100644 index d93cfd77b3c..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/bootstrap_example.py deleted file mode 100644 index f686bbd933d..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/bootstrap_example.py +++ /dev/null @@ -1,57 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pandas as pd -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, -) - - -def main(): - # Vars to estimate - theta_names = ['asymptote', 'rate_constant'] - - # Data - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], - ) - - # Sum of squared error function - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) - - # Parameter estimation - obj, theta = pest.theta_est() - - # Parameter estimation with bootstrap resampling - bootstrap_theta = pest.theta_est_bootstrap(50, seed=4581) - - # Plot results - parmest.graphics.pairwise_plot(bootstrap_theta, title='Bootstrap theta') - parmest.graphics.pairwise_plot( - bootstrap_theta, - theta, - 0.8, - ['MVN', 'KDE', 'Rect'], - title='Bootstrap theta with confidence regions', - ) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/likelihood_ratio_example.py deleted file mode 100644 index 5e54a33abda..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/likelihood_ratio_example.py +++ /dev/null @@ -1,62 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import numpy as np -import pandas as pd -from itertools import product -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, -) - - -def main(): - # Vars to estimate - theta_names = ['asymptote', 'rate_constant'] - - # Data - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], - ) - - # Sum of squared error function - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) - - # Parameter estimation - obj, theta = pest.theta_est() - - # Find the objective value at each theta estimate - asym = np.arange(10, 30, 2) - rate = np.arange(0, 1.5, 0.1) - theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=['asymptote', 'rate_constant'] - ) - obj_at_theta = pest.objective_at_theta(theta_vals) - - # Run the likelihood ratio test - LR = pest.likelihood_ratio_test(obj_at_theta, obj, [0.8, 0.85, 0.9, 0.95]) - - # Plot results - parmest.graphics.pairwise_plot( - LR, theta, 0.8, title='LR results within 80% confidence region' - ) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/parameter_estimation_example.py deleted file mode 100644 index 9af33217fe4..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/parameter_estimation_example.py +++ /dev/null @@ -1,60 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pandas as pd -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, -) - - -def main(): - # Vars to estimate - theta_names = ['asymptote', 'rate_constant'] - - # Data - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], - ) - - # Sum of squared error function - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 for i in data.index - ) - return expr - - # Create an instance of the parmest estimator - pest = parmest.Estimator(rooney_biegler_model, data, theta_names, SSE) - - # Parameter estimation and covariance - n = 6 # total number of data points used in the objective (y in 6 scenarios) - obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=n) - - # Plot theta estimates using a multivariate Gaussian distribution - parmest.graphics.pairwise_plot( - (theta, cov, 100), - theta_star=theta, - alpha=0.8, - distributions=['MVN'], - title='Theta estimates within 80% confidence region', - ) - - # Assert statements compare parameter estimation (theta) to an expected value - relative_error = abs(theta['asymptote'] - 19.1426) / 19.1426 - assert relative_error < 0.01 - relative_error = abs(theta['rate_constant'] - 0.5311) / 0.5311 - assert relative_error < 0.01 - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler.py deleted file mode 100644 index 5a0e1238e85..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler.py +++ /dev/null @@ -1,60 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -""" -Rooney Biegler model, based on Rooney, W. C. and Biegler, L. T. (2001). Design for -model parameter uncertainty using nonlinear confidence regions. AIChE Journal, -47(8), 1794-1804. -""" - -import pandas as pd -import pyomo.environ as pyo - - -def rooney_biegler_model(data): - model = pyo.ConcreteModel() - - model.asymptote = pyo.Var(initialize=15) - model.rate_constant = pyo.Var(initialize=0.5) - - def response_rule(m, h): - expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) - return expr - - model.response_function = pyo.Expression(data.hour, rule=response_rule) - - def SSE_rule(m): - return sum( - (data.y[i] - m.response_function[data.hour[i]]) ** 2 for i in data.index - ) - - model.SSE = pyo.Objective(rule=SSE_rule, sense=pyo.minimize) - - return model - - -def main(): - # These were taken from Table A1.4 in Bates and Watts (1988). - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], - ) - - model = rooney_biegler_model(data) - solver = pyo.SolverFactory('ipopt') - solver.solve(model) - - print('asymptote = ', model.asymptote()) - print('rate constant = ', model.rate_constant()) - - -if __name__ == '__main__': - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler_with_constraint.py deleted file mode 100644 index 2582e3fe928..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/rooney_biegler/rooney_biegler_with_constraint.py +++ /dev/null @@ -1,63 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -""" -Rooney Biegler model, based on Rooney, W. C. and Biegler, L. T. (2001). Design for -model parameter uncertainty using nonlinear confidence regions. AIChE Journal, -47(8), 1794-1804. -""" - -import pandas as pd -import pyomo.environ as pyo - - -def rooney_biegler_model_with_constraint(data): - model = pyo.ConcreteModel() - - model.asymptote = pyo.Var(initialize=15) - model.rate_constant = pyo.Var(initialize=0.5) - model.response_function = pyo.Var(data.hour, initialize=0.0) - - # changed from expression to constraint - def response_rule(m, h): - return m.response_function[h] == m.asymptote * ( - 1 - pyo.exp(-m.rate_constant * h) - ) - - model.response_function_constraint = pyo.Constraint(data.hour, rule=response_rule) - - def SSE_rule(m): - return sum( - (data.y[i] - m.response_function[data.hour[i]]) ** 2 for i in data.index - ) - - model.SSE = pyo.Objective(rule=SSE_rule, sense=pyo.minimize) - - return model - - -def main(): - # These were taken from Table A1.4 in Bates and Watts (1988). - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], - ) - - model = rooney_biegler_model_with_constraint(data) - solver = pyo.SolverFactory('ipopt') - solver.solve(model) - - print('asymptote = ', model.asymptote()) - print('rate constant = ', model.rate_constant()) - - -if __name__ == '__main__': - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/__init__.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/__init__.py deleted file mode 100644 index d93cfd77b3c..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/bootstrap_theta.csv b/pyomo/contrib/parmest/deprecated/examples/semibatch/bootstrap_theta.csv deleted file mode 100644 index 29923a782c5..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/bootstrap_theta.csv +++ /dev/null @@ -1,101 +0,0 @@ -,k1,k2,E1,E2 -0,23.8359813557911,149.99999125263844,31164.260824269295,41489.69422529956 -1,19.251987486659512,105.3374117880675,30505.86059307485,40516.897897740404 -2,19.31940450911214,105.78105886426505,30509.636888745794,40539.53548872927 -3,8.754357429283429,149.99988037658665,28334.500331107014,41482.01554893696 -4,23.016722464092286,80.03743984792878,31091.61503716734,39770.08415278276 -5,6.612337410520649,140.0259411600077,27521.46259880474,41302.159495413296 -6,14.29348509961158,147.0817016641302,29605.749859593245,41443.12009807534 -7,14.152069480386153,149.9914759675382,29676.633227079245,41483.41029455195 -8,19.081046896092914,125.55106106390114,30586.57857977985,41005.60243351924 -9,3.063566173952205,149.9999684548014,25473.079370305273,41483.426370389796 -10,17.79494440791066,108.52425726918327,30316.710830136202,40618.63715404914 -11,97.307579412204,149.99998972597675,35084.37589956093,41485.835559276136 -12,20.793042577945116,91.124365144131,30782.17494940993,40138.215713547994 -13,12.740540794730641,89.86327635412908,29396.65520336387,40086.14665722912 -14,6.930810780299319,149.99999327266906,27667.0240033497,41480.188987754496 -15,20.29404799567638,101.07539817765885,30697.087258737916,40443.316578889426 -16,85.77501788788223,149.99996482096984,34755.77375009206,41499.23448818336 -17,24.13150325243255,77.06876294766496,31222.03914354306,39658.418332258894 -18,16.026645517712712,149.99993094015056,30015.46332620076,41490.69892111652 -19,31.020018442708537,62.11558789585982,31971.311996398897,39089.828285017575 -20,20.815008037484656,87.35968459422139,30788.643843293,40007.78137819648 -21,19.007148519616447,96.44320176694993,30516.36933261116,40284.23312198372 -22,22.232021812057308,89.71692873746096,30956.252845068626,40095.009765519 -23,16.830765834427297,120.65209863104229,30139.92208332896,40912.673450399234 -24,15.274799190396566,129.82767733073857,29780.055282261117,41078.04749417758 -25,22.37343657709118,82.32861355430458,31013.57952062852,39853.06284595207 -26,9.055694749134819,149.99987339406314,28422.482259116612,41504.97564187301 -27,19.909770949417275,86.5634026379812,30705.60369894775,39996.134938503914 -28,20.604557306290886,87.96473948102359,30786.467003867263,40051.28176004557 -29,21.94101237923462,88.18216423767153,30942.372558158557,40051.20357069738 -30,3.200663718121338,149.99997712051055,25472.46099917771,41450.884180452646 -31,20.5812467558026,86.36098672832426,30802.74421085271,40010.76777825347 -32,18.776139793586893,108.99943042186453,30432.474809193136,40641.48011315501 -33,17.14246930769276,112.29370332257908,30164.332101438307,40684.867629869856 -34,20.52146255576043,99.7078140453859,30727.90573864389,40401.20730725967 -35,17.05073306185531,66.00385439687035,30257.075479935145,39247.26647870223 -36,7.1238843213074015,51.05163218895348,27811.250260416655,38521.11199236329 -37,10.54291332571747,76.74902426944477,28763.52244085013,39639.92644514267 -38,16.329028964122656,107.60037882134996,30073.5111433796,40592.825374177235 -39,18.0923131790489,107.75659679748213,30355.62290415686,40593.10521263782 -40,15.477264179087811,149.99995828085014,29948.62617372307,41490.770726165414 -41,23.190670255199933,76.5654091811839,31107.96477489951,39635.650879492074 -42,20.34720227734719,90.07051780196629,30716.131795936217,40096.932765428995 -43,23.60627359054596,80.0847207027996,31130.449736501876,39756.06693747353 -44,22.54968153535252,83.72995448206636,31038.51932262643,39906.60181934743 -45,24.951320839961582,67.97010976959977,31356.00147390564,39307.75709154711 -46,61.216667588824386,149.9999967830529,33730.22100500659,41474.80665231048 -47,9.797300324197744,136.33054557076974,28588.83540859912,41222.22413163186 -48,21.75078861615545,139.82641444329093,30894.847060525986,41290.16131583715 -49,21.76324066920255,99.57885291658233,30860.292260186063,40386.00605205238 -50,20.244262248110417,86.2553098058883,30742.054735645124,39981.83946305757 -51,21.859217291379004,72.89837327878459,30999.703939831277,39514.23768439393 -52,20.902111153308944,88.36862895882298,30782.76240691508,40033.44884393017 -53,59.58504995089654,149.9999677447201,33771.647879014425,41496.69202917452 -54,21.63994234351529,80.9641923004028,30933.578583737795,39809.523930207484 -55,9.804873383156298,149.9995892138235,28729.93818644509,41500.94496844104 -56,9.517359502437172,149.99308840029815,28505.329315103318,41470.65218792529 -57,19.923610217578116,88.23847592895486,30636.024864041487,40020.79650218989 -58,20.366495228182394,85.1991151089578,30752.560133063143,39947.719888972904 -59,12.242715793208157,149.99998097746882,29308.42752633667,41512.25071862387 -60,19.677765799324447,97.30674967097808,30618.37668428642,40323.0499230797 -61,19.03651315222424,109.20775378637025,30455.39615515442,40614.722801684395 -62,21.37660531151217,149.99999616215425,30806.121697474813,41479.3976433347 -63,21.896838392882998,86.86206456282005,30918.823491874144,39986.262281131254 -64,5.030122322262226,149.99991736085678,26792.302062236955,41480.579525893794 -65,17.851755694421776,53.33521102556455,30419.017295420916,38644.47349861614 -66,20.963796542255896,90.72302887846234,30795.751244616677,40114.19163802526 -67,23.082992539267945,77.24345020180209,31107.07485019312,39665.22410226011 -68,18.953050386839383,90.80802949182345,30529.280393040182,40113.73467038244 -69,20.710937910951355,83.16996057131982,30805.892332796295,39876.270184728084 -70,18.18549080794899,65.72657652078952,30416.294615296756,39223.21339606898 -71,12.147892028456324,45.12945045196771,29302.888575028635,38194.144730342545 -72,4.929663537166405,133.89086200105797,26635.8524254091,41163.82082194103 -73,20.512731504598662,106.98199797354127,30660.67479570742,40560.70063653076 -74,21.006700520199008,93.35471748418676,30761.272887418058,40178.10564855804 -75,19.73635577733317,98.75362910260881,30599.64039254174,40346.31274388047 -76,3.6393630101175565,149.99998305638113,25806.925407145678,41446.42489819377 -77,14.430958212981363,149.9999928114441,29710.277666486683,41478.96029884101 -78,21.138173237661093,90.73414659450283,30833.36092609432,40128.61898313504 -79,19.294823672883208,104.69324605284973,30510.371654343133,40510.84889949937 -80,2.607050470695225,69.22680095813037,25000.001468502505,39333.142090801295 -81,16.949842823156228,118.76691429120146,30074.04126731665,40824.66852388976 -82,21.029588811317897,95.27115352081795,30770.753828753943,40243.47156167542 -83,18.862418349044077,111.08370690591005,30421.17882623639,40670.941374189555 -84,24.708015660945147,76.24225941680999,31286.7038829574,39632.545034540664 -85,21.58937721477476,92.6329553952883,30871.989108388123,40181.7478528116 -86,21.091322126816706,96.07721666941696,30765.91144819689,40265.321194575095 -87,19.337815749868728,96.50567420686403,30604.551156564357,40318.12321325275 -88,17.77732130279279,108.5062535737451,30287.456682982094,40602.76307166587 -89,15.259532609396405,134.79914728383426,29793.69015375863,41199.11159557717 -90,21.616910309091583,90.65235108674251,30848.137718134392,40096.0776408459 -91,3.3372937891220475,149.99991062247588,25630.388452101062,41483.30064805118 -92,20.652437906744403,97.86062128528714,30747.864718937744,40330.11871286893 -93,22.134113060054425,73.68464943802763,31013.225174702933,39535.65213713519 -94,20.297310066178802,93.79207093658654,30684.309981457223,40191.747572763874 -95,6.007958386675472,149.99997175883215,27126.707007542,41465.75099589974 -96,16.572749402536758,40.75746000309888,30154.396795028595,37923.85448825053 -97,21.235697111801056,98.97798760165126,30807.097617165928,40373.550932032136 -98,20.10350615639414,96.19608053749371,30632.029399836003,40258.3813340696 -99,18.274272179970747,96.49060573948069,30456.872524151822,40305.258325587834 diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out deleted file mode 100644 index f1d826085bf..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp1.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 1, "Ca0": 0, "Ca_meas": {"17280.0": 1.0206137429621787, "0.0": 0.3179205033819153, "7560.0": 6.1190611079452015, "9720.0": 5.143125125521654, "19440.0": 0.6280402056097951, "16200.0": 0.8579867036528984, "11880.0": 3.7282923165210042, "2160.0": 4.63887641485678, "8640.0": 5.343033989137008, "14040.0": 2.053029378587664, "4320.0": 6.161603379127277, "6480.0": 6.175522427215327, "1080.0": 2.5735849991352358, "18360.0": 0.6351040530590654, "10800.0": 5.068206847862049, "21600.0": 0.40727295182614354, "20520.0": 0.6621175064161002, "5400.0": 6.567824349703669, "3240.0": 5.655458079751501, "12960.0": 2.654764659666162, "15120.0": 1.6757350275784135}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"17280.0": 5.987636915771096, "0.0": 0.09558280565339891, "7560.0": 1.238130321602845, "9720.0": 1.9945247030805529, "19440.0": 6.695864385679773, "16200.0": 5.41645220805244, "11880.0": 2.719892366798277, "2160.0": 0.10805070272409367, "8640.0": 1.800229763433655, "14040.0": 4.156268601598023, "4320.0": -0.044818714779864405, "6480.0": 0.8106022415380871, "1080.0": -0.07327388848369068, "18360.0": 5.96868114596425, "10800.0": 2.0726982059573835, "21600.0": 7.269213818513372, "20520.0": 6.725777234409265, "5400.0": 0.18749831830326769, "3240.0": -0.10164819461093579, "12960.0": 3.745361461163259, "15120.0": 4.92464438752146}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 12.527811727243744, "0.0": 0.006198551839384427, "7560.0": 8.198459268980448, "9720.0": 10.884163155983586, "19440.0": 12.150552321810109, "16200.0": 13.247640677577017, "11880.0": 12.921639059281906, "2160.0": 1.2393091651113075, "8640.0": 9.76716833273541, "14040.0": 13.211149989298647, "4320.0": 3.803104433804622, "6480.0": 6.5810650565269375, "1080.0": 0.3042714459761661, "18360.0": 12.544400522361945, "10800.0": 11.737104197836604, "21600.0": 11.886358606219954, "20520.0": 11.832544691029744, "5400.0": 5.213810980890077, "3240.0": 2.1926632109587216, "12960.0": 13.113839789286594, "15120.0": 13.27982750652159}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 325.16059207223026, "0.0": 297.66636064503314, "7560.0": 331.3122493868531, "9720.0": 332.5912691607457, "19440.0": 325.80525903012233, "16200.0": 323.2692288871708, "11880.0": 334.0549081870754, "2160.0": 323.2373236557714, "8640.0": 333.4576764024497, "14040.0": 328.42212335544315, "4320.0": 327.6704558317418, "6480.0": 331.06042780025075, "1080.0": 316.2567029216892, "18360.0": 326.6647586865489, "10800.0": 334.23136746878185, "21600.0": 324.0057232633869, "20520.0": 324.4555288383823, "5400.0": 331.9568676813546, "3240.0": 326.12583828081813, "12960.0": 329.2382904744002, "15120.0": 327.3959354386782}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out deleted file mode 100644 index 7eb7980a7be..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp10.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 10, "Ca0": 0, "Ca_meas": {"0.0": 0.17585381115505311, "4320.0": 6.0315379232850992, "7560.0": 5.4904391073929908, "21600.0": 0.28272237229599306, "9720.0": 5.2677178238115365, "16200.0": 1.1265897978788217, "20520.0": 0.057584376823091032, "3240.0": 5.6315955838815661, "11880.0": 3.328489578835276, "14040.0": 2.0226562017072607, "17280.0": 1.1268539727440208, "2160.0": 4.3030132489621638, "5400.0": 6.1094034780709618, "18360.0": 0.50886621390394615, "8640.0": 5.3773941013828752, "6480.0": 5.9760510402178078, "1080.0": 2.9280667525762598, "15120.0": 1.375026987701359, "12960.0": 2.5451999496997635, "19440.0": 0.79349685535634917, "10800.0": 4.3653401523141229}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": -0.038024312530073517, "4320.0": 0.35646997778490641, "7560.0": 1.0986283962244756, "21600.0": 7.160087143091391, "9720.0": 2.129148884681515, "16200.0": 5.1383561968992435, "20520.0": 6.8451793517536901, "3240.0": 0.098714783055484312, "11880.0": 3.0269500169512602, "14040.0": 3.9370558676788283, "17280.0": 5.9641262824404357, "2160.0": -0.12281730158248855, "5400.0": 0.59307341448224149, "18360.0": 6.2121451052794248, "8640.0": 1.7607685730069123, "6480.0": 0.53516134735284115, "1080.0": 0.021830284057365701, "15120.0": 4.7082270119144871, "12960.0": 4.0629501433813449, "19440.0": 6.333023537154518, "10800.0": 2.2805891192921983}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": -0.063620932422370907, "4320.0": 3.5433262677921662, "7560.0": 8.6893371808300444, "21600.0": 11.870505989648386, "9720.0": 11.309193344250055, "16200.0": 12.739396897287321, "20520.0": 11.791007739959538, "3240.0": 2.1799284210951009, "11880.0": 12.669418985545658, "14040.0": 13.141014574935076, "17280.0": 12.645153211711902, "2160.0": 1.4830589148905116, "5400.0": 5.2739635750232985, "18360.0": 12.761210138866151, "8640.0": 9.9142303856203373, "6480.0": 7.0761548290524603, "1080.0": 0.43050918133895111, "15120.0": 12.66028651303237, "12960.0": 12.766057551719733, "19440.0": 12.385465894826957, "10800.0": 11.96758252080965}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 301.47604965958681, "4320.0": 327.40308974239883, "7560.0": 332.84954650085029, "21600.0": 322.5157157706418, "9720.0": 333.3451281132692, "16200.0": 325.64418049630734, "20520.0": 321.94339225425767, "3240.0": 327.19623764314332, "11880.0": 330.24784399520615, "14040.0": 328.20666366981681, "17280.0": 324.73232197103431, "2160.0": 324.1280979971192, "5400.0": 330.87716394833132, "18360.0": 323.47482388751507, "8640.0": 332.49299626233665, "6480.0": 332.1275559564059, "1080.0": 319.29329353123859, "15120.0": 327.29837672678497, "12960.0": 329.24537484541742, "19440.0": 324.02559861322572, "10800.0": 335.57159429203386}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out deleted file mode 100644 index d97442f031b..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp11.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 11, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 2.9214639882977118, "7560.0": 3.782748575728669, "21600.0": 1.0934559688831289, "9720.0": 3.9988602724163731, "16200.0": 1.3975369118671503, "20520.0": 1.1243254110346068, "3240.0": 2.4246081576650433, "11880.0": 3.4755989173839743, "14040.0": 1.9594736461287787, "17280.0": 1.2827751626299488, "2160.0": 1.7884659182526941, "5400.0": 3.3009088212806073, "18360.0": 1.2111862780556402, "8640.0": 3.9172974313558302, "6480.0": 3.5822886585117493, "1080.0": 0.9878678077907459, "15120.0": 1.5972116122332332, "12960.0": 2.5920858659103394, "19440.0": 1.1618336860102094, "10800.0": 4.0378190270354528}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": 0.0, "4320.0": 0.023982318123109209, "7560.0": 0.12357850466238102, "21600.0": 6.5347744286121001, "9720.0": 0.24908724520575926, "16200.0": 3.2484340438243127, "20520.0": 5.9039946620569568, "3240.0": 0.0099959742916886744, "11880.0": 0.58037814751790051, "14040.0": 1.8149241834100336, "17280.0": 3.9360275733370447, "2160.0": 0.0028162554877217529, "5400.0": 0.046614709277751604, "18360.0": 4.6059327483829717, "8640.0": 0.17995429953061945, "6480.0": 0.079417046516631534, "1080.0": 0.00029995464126276485, "15120.0": 2.5396785773426069, "12960.0": 1.1193190878564474, "19440.0": 5.2613272888405183, "10800.0": 0.33137635542084787}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.0, "4320.0": 0.96588936445839646, "7560.0": 2.5063363714379272, "21600.0": 7.0191274767686718, "9720.0": 3.6738114082888234, "16200.0": 7.2205659498627206, "20520.0": 7.0929480434376666, "3240.0": 0.56824903663740756, "11880.0": 5.2689545424581627, "14040.0": 6.8616827734843717, "17280.0": 7.2356915893004699, "2160.0": 0.25984959276122965, "5400.0": 1.4328144788513235, "18360.0": 7.2085405027857679, "8640.0": 3.0841896398822519, "6480.0": 1.9514434452676097, "1080.0": 0.063681239951285565, "15120.0": 7.1238797146821753, "12960.0": 6.279843675978368, "19440.0": 7.1578041487575703, "10800.0": 4.2664529460540459}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 312.23882460110946, "7560.0": 313.65588947948061, "21600.0": 347.47264008527429, "9720.0": 314.23459379740643, "16200.0": 349.53974420970837, "20520.0": 347.6739307915775, "3240.0": 311.5507050941913, "11880.0": 343.84565806826117, "14040.0": 351.77400891772504, "17280.0": 348.76203275606656, "2160.0": 310.56857709679934, "5400.0": 312.79850843986321, "18360.0": 348.2596349485566, "8640.0": 313.9757607933924, "6480.0": 313.26649244154709, "1080.0": 308.41052123547064, "15120.0": 350.65940715239384, "12960.0": 351.11078111562267, "19440.0": 347.92214750818005, "10800.0": 317.60632266285268}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out deleted file mode 100644 index cbed2d89634..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp12.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 12, "Ca0": 0, "Ca_meas": {"0.0": -0.11670041274142862, "4320.0": 2.5843604031503551, "7560.0": 3.9909358380362421, "21600.0": 0.85803342268297711, "9720.0": 3.8011636431252702, "16200.0": 1.1002575863333801, "20520.0": 1.5248654901477563, "3240.0": 2.8626873137030655, "11880.0": 3.0876088469734766, "14040.0": 2.1233520630854348, "17280.0": 1.3118009790549121, "2160.0": 1.9396713478350336, "5400.0": 3.4898817799323192, "18360.0": 1.3214498335778544, "8640.0": 4.3166520687122008, "6480.0": 3.7430014080130212, "1080.0": 0.71502131244112932, "15120.0": 1.6869248126824714, "12960.0": 2.6199132141077999, "19440.0": 1.1934026369102775, "10800.0": 4.1662760005238244}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": -0.14625373198142194, "4320.0": 0.50521240190855021, "7560.0": 0.072658369151157948, "21600.0": 6.5820277701997947, "9720.0": 0.14656574869775074, "16200.0": 3.2344935292984842, "20520.0": 6.1934992398225113, "3240.0": 0.073187422926812754, "11880.0": 0.68271223751792398, "14040.0": 1.5495094265280573, "17280.0": 3.5758298262936474, "2160.0": -0.052665808995189155, "5400.0": -0.067101579134353218, "18360.0": 4.6809081069906799, "8640.0": 0.58099071333349073, "6480.0": -0.34905530826754594, "1080.0": -0.13981627097780677, "15120.0": 2.7577781806493582, "12960.0": 0.9824219783559841, "19440.0": 5.2002977724609245, "10800.0": 0.0089889440138224419}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": -0.34864609349591541, "4320.0": 0.99561164186682227, "7560.0": 2.4688230226633143, "21600.0": 7.0564292657160461, "9720.0": 3.9961025255558669, "16200.0": 7.161739218807984, "20520.0": 6.8044634236188921, "3240.0": 0.21017912447011355, "11880.0": 5.1168427571991435, "14040.0": 7.0032016822280907, "17280.0": 7.11845364876228, "2160.0": 0.22241873726625405, "5400.0": 1.3174801426799267, "18360.0": 6.9581816529657257, "8640.0": 3.0438444011178785, "6480.0": 2.2558063612162291, "1080.0": 0.38969247867806489, "15120.0": 7.2125210995495488, "12960.0": 6.4014182164755429, "19440.0": 6.8128450220608308, "10800.0": 4.2649851849420299}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.53274252012955, "4320.0": 311.15339067777529, "7560.0": 312.18867239090878, "21600.0": 346.00646319366473, "9720.0": 313.61710665630221, "16200.0": 352.81825567952984, "20520.0": 346.66248325950249, "3240.0": 311.37928873928001, "11880.0": 343.17457873193757, "14040.0": 352.88609842940627, "17280.0": 348.48519596719899, "2160.0": 312.70676562686674, "5400.0": 314.29841143358993, "18360.0": 347.81967845794014, "8640.0": 313.26528616120316, "6480.0": 314.33539425367189, "1080.0": 308.35955588183077, "15120.0": 350.62179387183005, "12960.0": 348.72513776404708, "19440.0": 346.65341312375318, "10800.0": 318.79995964600641}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out deleted file mode 100644 index 6ef514c951a..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp13.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 13, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 2.9015294551493156, "7560.0": 3.7456240714596114, "21600.0": 1.0893473942847287, "9720.0": 3.955661759695563, "16200.0": 1.3896514099314119, "20520.0": 1.1200835239496501, "3240.0": 2.4116751003091821, "11880.0": 3.4212519616089123, "14040.0": 1.9331934810250495, "17280.0": 1.2772682671606199, "2160.0": 1.7821342925311514, "5400.0": 3.2743367747379883, "18360.0": 1.2065189956942144, "8640.0": 3.8765800278544793, "6480.0": 3.5499054339456388, "1080.0": 0.98643229677676525, "15120.0": 1.583391359829623, "12960.0": 2.5471212725882397, "19440.0": 1.1574522594063252, "10800.0": 3.9931029485836738}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": 0.0, "4320.0": 0.025294213033754839, "7560.0": 0.12897041691694733, "21600.0": 6.5640983670402253, "9720.0": 0.2583989163781088, "16200.0": 3.2753828012649455, "20520.0": 5.9325635171128175, "3240.0": 0.01057908838025531, "11880.0": 0.60077017176350533, "14040.0": 1.8465767045073862, "17280.0": 3.9624456898675042, "2160.0": 0.0029862658207987017, "5400.0": 0.048985517519306479, "18360.0": 4.6327886545170172, "8640.0": 0.1872203610374778, "6480.0": 0.083160507067847278, "1080.0": 0.0003160950335645278, "15120.0": 2.5686484578016535, "12960.0": 1.149697840345306, "19440.0": 5.2890172904133799, "10800.0": 0.3428648031290083}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.0, "4320.0": 0.98451200269614558, "7560.0": 2.5380689634524152, "21600.0": 6.993912112938939, "9720.0": 3.7076982498372795, "16200.0": 7.2015026943578233, "20520.0": 7.0686210754667576, "3240.0": 0.58059897990470211, "11880.0": 5.3029094739876195, "14040.0": 6.8563104174907483, "17280.0": 7.2147803682393352, "2160.0": 0.26601120814969509, "5400.0": 1.4570157171523839, "18360.0": 7.1863518790131433, "8640.0": 3.1176409818767401, "6480.0": 1.9800832092825005, "1080.0": 0.06510061057296454, "15120.0": 7.1087300866267373, "12960.0": 6.294429516811606, "19440.0": 7.1344955737885876, "10800.0": 4.2996805767976616}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 312.83405362164399, "7560.0": 314.10857941130064, "21600.0": 347.58975942376372, "9720.0": 314.61233912755387, "16200.0": 349.56203914424219, "20520.0": 347.79317623855678, "3240.0": 312.2016171592112, "11880.0": 344.43453363173575, "14040.0": 351.72789167129031, "17280.0": 348.83759825667926, "2160.0": 311.27622091507072, "5400.0": 313.34232236658977, "18360.0": 348.36453268948424, "8640.0": 314.38892058203726, "6480.0": 313.76278431242508, "1080.0": 309.11491437259622, "15120.0": 350.61679816631096, "12960.0": 351.27269989378158, "19440.0": 348.03901542922108, "10800.0": 318.0555419842903}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out deleted file mode 100644 index cc3f95da860..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp14.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 14, "Ca0": 0, "Ca_meas": {"0.0": 0.081520066870384211, "4320.0": 2.8337428652268013, "7560.0": 3.9530051428610888, "21600.0": 1.1807185641508762, "9720.0": 4.0236480913821637, "16200.0": 1.4376034521825525, "20520.0": 1.5413859123725682, "3240.0": 2.7181227248994633, "11880.0": 3.3903617547506242, "14040.0": 2.0159168723544196, "17280.0": 1.0638207528750085, "2160.0": 1.8959521419461316, "5400.0": 2.9705863021885124, "18360.0": 1.0674705545585839, "8640.0": 4.144391992823846, "6480.0": 3.5918471023904477, "1080.0": 1.0113708479421195, "15120.0": 1.6541373379275581, "12960.0": 2.6476139688086877, "19440.0": 1.2312633238777129, "10800.0": 3.8743606114727154}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"0.0": -0.089503442052066576, "4320.0": 0.18208896954493076, "7560.0": 0.17697219964055824, "21600.0": 6.8780014679710426, "9720.0": 0.35693081777790492, "16200.0": 3.1977597178279002, "20520.0": 6.1361982627818481, "3240.0": 0.10972700122863582, "11880.0": 0.76070080263615369, "14040.0": 1.5671921543009262, "17280.0": 4.0972632572434122, "2160.0": -0.28322371444225319, "5400.0": -0.079382269802482266, "18360.0": 4.5706426776745905, "8640.0": 0.26888423813019369, "6480.0": 0.1979519809989283, "1080.0": 0.13979150229071807, "15120.0": 2.6867100129129424, "12960.0": 0.99472739483139283, "19440.0": 5.6554142424694902, "10800.0": 0.46709816548507599}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.1139198104669558, "4320.0": 0.99036932502879282, "7560.0": 2.4258194495364482, "21600.0": 7.0446273994379434, "9720.0": 3.7506516413639113, "16200.0": 6.9192013751011503, "20520.0": 7.1555299371654808, "3240.0": 0.66230857796133746, "11880.0": 5.0716652323781499, "14040.0": 7.0971130388695007, "17280.0": 7.4091470358534082, "2160.0": -0.039078609338807413, "5400.0": 1.480378464133409, "18360.0": 7.1741052031883399, "8640.0": 3.4996110541636019, "6480.0": 2.0450173775271647, "1080.0": 0.13728827557251419, "15120.0": 6.7382150794212539, "12960.0": 6.3936782268937753, "19440.0": 7.3298178049407321, "10800.0": 4.6925893428035463}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 301.43741888919669, "4320.0": 313.81215912108536, "7560.0": 313.22398065446896, "21600.0": 347.95979639817102, "9720.0": 316.38480427739029, "16200.0": 350.46267406552954, "20520.0": 349.96840569755773, "3240.0": 313.32903259260996, "11880.0": 345.61089172333249, "14040.0": 352.83446396320579, "17280.0": 347.23320123776085, "2160.0": 310.02897702317114, "5400.0": 314.70707924235739, "18360.0": 349.8524737596988, "8640.0": 314.13917383134913, "6480.0": 314.15959183349207, "1080.0": 307.97982604287193, "15120.0": 349.00176197155969, "12960.0": 350.41651244789142, "19440.0": 346.1591726550746, "10800.0": 317.90588794308121}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out deleted file mode 100644 index e5245dff3f0..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp2.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 2, "Ca0": 0, "Ca_meas": {"12960.0": 2.6833775561963225, "21600.0": 1.1452663167259416, "17280.0": 1.2021595324165524, "19440.0": 1.0621392247792012, "0.0": -0.1316904398446574, "7560.0": 3.544605362880795, "11880.0": 3.1817551426501267, "14040.0": 2.066815570405579, "4320.0": 3.0488618589043432, "15120.0": 1.5896211475539537, "1080.0": 0.8608182979507091, "18360.0": 1.1317484585922248, "8640.0": 3.454602822099547, "2160.0": 1.7479951078246254, "20520.0": 1.2966191801491087, "9720.0": 4.0596636929917285, "6480.0": 3.9085446597134283, "3240.0": 2.5050366860794875, "5400.0": 3.2668528110981576, "10800.0": 4.004828727345138, "16200.0": 1.2860720212507326}, "alphac": 0.7, "Fa2": 0.001, "deltaH1": -40000, "Fa1": 0.001, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"12960.0": 1.3318254182147888, "21600.0": 6.431089735352747, "17280.0": 3.9711701719678825, "19440.0": 5.321278981143728, "0.0": 0.10325037628575211, "7560.0": 0.10827803632010198, "11880.0": 0.5184920846420157, "14040.0": 1.7974496302186054, "4320.0": 0.03112694971654564, "15120.0": 2.5245584142423207, "1080.0": 0.27315169217241275, "18360.0": 4.587420104936772, "8640.0": 0.1841080751926184, "2160.0": -0.3018405210283593, "20520.0": 6.0086124912796794, "9720.0": 0.08100716578409362, "6480.0": 0.027897809479352567, "3240.0": 0.01973928836607919, "5400.0": 0.02694434057766582, "10800.0": 0.3021977838614568, "16200.0": 3.561159267372319}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"12960.0": 6.358162874770003, "21600.0": 7.190121324797683, "17280.0": 7.302887809357959, "19440.0": 7.249179120248633, "0.0": -0.2300456054526237, "7560.0": 2.5654112157969364, "11880.0": 5.3061363321784025, "14040.0": 6.84009925060659, "4320.0": 0.9644475554073758, "15120.0": 7.261439274435592, "1080.0": 0.2644823572584467, "18360.0": 7.3015136544611305, "8640.0": 3.065189548359326, "2160.0": 0.27122113581760515, "20520.0": 7.162520282520871, "9720.0": 3.701445814227159, "6480.0": 2.0255650533089735, "3240.0": 0.48891328422133334, "5400.0": 1.326135697891304, "10800.0": 4.031252283597449, "16200.0": 7.38249778574694}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"12960.0": 350.10381529626676, "21600.0": 345.04247547236935, "17280.0": 348.24859807524, "19440.0": 346.7076647696431, "0.0": 301.30135373385, "7560.0": 314.7140350191631, "11880.0": 343.608488145403, "14040.0": 352.2902404657956, "4320.0": 312.2349716351422, "15120.0": 351.28330527232634, "1080.0": 307.67178366332996, "18360.0": 347.3517733033304, "8640.0": 313.62143853358026, "2160.0": 310.2674222024707, "20520.0": 348.1662021459614, "9720.0": 314.8081875940964, "6480.0": 312.7396303616638, "3240.0": 310.9258419605079, "5400.0": 312.8448509580561, "10800.0": 317.33866255037736, "16200.0": 349.5494112095972}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out deleted file mode 100644 index a9b013d476f..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp3.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 3, "Ca0": 0, "Ca_meas": {"17280.0": 0.39426558381033217, "0.0": -0.25761950998771116, "7560.0": 7.638659217862309, "9720.0": 7.741721088143662, "19440.0": 0.12706787288438182, "16200.0": 0.15060928317089423, "11880.0": 4.534965302380243, "2160.0": 4.47101661859487, "8640.0": 7.562734803826617, "14040.0": 0.8456407304976143, "4320.0": 7.395023123698018, "6480.0": 7.952409415603349, "1080.0": 3.0448009666821947, "18360.0": 0.0754742404427045, "10800.0": 7.223420802364689, "21600.0": 0.181946676125186, "20520.0": 0.195256504023462, "5400.0": 7.844394030136843, "3240.0": 6.031994466757849, "12960.0": 1.813573590958129, "15120.0": 0.30408071219857113}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 340, "Cc0": 0, "Tc1": 310, "Cc_meas": {"17280.0": 10.587758291638856, "0.0": -0.21796531802425348, "7560.0": 0.8680716299725404, "9720.0": 0.9085250272598128, "19440.0": 11.914154788848554, "16200.0": 9.737618534040658, "11880.0": 2.0705302921286064, "2160.0": -0.022903632391514165, "8640.0": 0.5195918959805059, "14040.0": 6.395605356788582, "4320.0": 0.010107836996695638, "6480.0": 0.09956884355869228, "1080.0": -0.21178603534213003, "18360.0": 10.990013628196317, "10800.0": 1.345982231414325, "21600.0": 12.771296192955955, "20520.0": 12.196513048345082, "5400.0": 0.04790148745481311, "3240.0": 0.318546588876358, "12960.0": 4.693433882970767, "15120.0": 8.491695125145553}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 8.731932027503914, "0.0": -0.1375816812387338, "7560.0": 6.8484762842626985, "9720.0": 9.333087689786101, "19440.0": 7.381577268709603, "16200.0": 9.502048821225666, "11880.0": 12.406853134672218, "2160.0": 0.8107944776900446, "8640.0": 8.158484571318013, "14040.0": 11.54445651179274, "4320.0": 2.8119825114954082, "6480.0": 5.520857819630275, "1080.0": 0.18414413253133835, "18360.0": 8.145712620219781, "10800.0": 10.75765409121092, "21600.0": 6.1356948865706356, "20520.0": 6.576247039355788, "5400.0": 3.92591568907661, "3240.0": 2.015632242014947, "12960.0": 12.71320139030468, "15120.0": 10.314039809497785}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 345.7556825478521, "0.0": 300.1125978749429, "7560.0": 319.7301093967675, "9720.0": 321.4474947517745, "19440.0": 345.1982881134514, "16200.0": 348.50691612993586, "11880.0": 357.2800598685144, "2160.0": 311.7652063627056, "8640.0": 323.6663990115871, "14040.0": 365.2497105829804, "4320.0": 317.1702037696461, "6480.0": 319.9056594806601, "1080.0": 309.6187369359568, "18360.0": 345.1427626681205, "10800.0": 323.9154289387584, "21600.0": 345.45022165546357, "20520.0": 344.5991879566869, "5400.0": 316.21958050333416, "3240.0": 314.95895736530616, "12960.0": 371.5669986462856, "15120.0": 355.3612054360245}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out deleted file mode 100644 index e702db7d05b..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp4.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 4, "Ca0": 0, "Ca_meas": {"17280.0": 1.1816674202534045, "0.0": -0.25414468591124584, "7560.0": 5.914582094251343, "9720.0": 5.185067133371561, "19440.0": 0.5307435290768995, "16200.0": 1.2011215994628408, "11880.0": 3.5778183914967925, "2160.0": 4.254440534150475, "8640.0": 5.473440610227645, "14040.0": 2.1475894664278354, "4320.0": 5.8795707148110266, "6480.0": 6.089523479429854, "1080.0": 2.4339586418303543, "18360.0": 0.545228377126232, "10800.0": 4.946406396626746, "21600.0": 0.3169450590438124, "20520.0": 0.5859997070045333, "5400.0": 6.15901928205937, "3240.0": 5.5559094344993225, "12960.0": 2.476561130612629, "15120.0": 1.35232620260846}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"17280.0": 6.529641678005382, "0.0": 0.23057916607631007, "7560.0": 1.4209973582182187, "9720.0": 2.662002861314475, "19440.0": 7.612677904908142, "16200.0": 6.236740105453746, "11880.0": 3.6132373498432813, "2160.0": 0.41778377045750303, "8640.0": 2.3031169482702336, "14040.0": 4.857027337132376, "4320.0": 0.21846387467958883, "6480.0": 1.304912741118713, "1080.0": -0.002497120213976349, "18360.0": 7.207560113722179, "10800.0": 3.072350404943197, "21600.0": 8.437070128901182, "20520.0": 7.985790844633096, "5400.0": 0.5552902218354748, "3240.0": 0.4207298617922146, "12960.0": 4.791797355546968, "15120.0": 5.544868662346418}, "alphaj": 0.8, "Cb0": 2, "Vr0": 1, "Cb_meas": {"17280.0": 13.559468310792148, "0.0": 2.1040937779048963, "7560.0": 9.809117250733637, "9720.0": 12.404875593478181, "19440.0": 12.616477055699818, "16200.0": 13.94157106495499, "11880.0": 13.828382570964388, "2160.0": 3.1373485614417618, "8640.0": 11.053587506450443, "14040.0": 14.267859106799012, "4320.0": 5.6037726942190424, "6480.0": 8.499893646580981, "1080.0": 2.2930856900001535, "18360.0": 13.566545045105496, "10800.0": 13.397445458860116, "21600.0": 12.347983697813623, "20520.0": 12.422291796055749, "5400.0": 7.367727360896684, "3240.0": 3.8542518870239824, "12960.0": 14.038139660530316, "15120.0": 14.114308276615096}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 324.4465785902298, "0.0": 299.6733512720839, "7560.0": 333.04897592093835, "9720.0": 334.48916083151335, "19440.0": 323.5321646227951, "16200.0": 326.84528144052564, "11880.0": 332.77528830002086, "2160.0": 322.2453586799791, "8640.0": 334.2512423463752, "14040.0": 329.05431569837447, "4320.0": 327.99896899102527, "6480.0": 334.3262547857335, "1080.0": 317.32350372398014, "18360.0": 324.97573570445866, "10800.0": 334.23107235994195, "21600.0": 323.6424061352077, "20520.0": 324.00045995355015, "5400.0": 331.45112696032044, "3240.0": 326.33322784448785, "12960.0": 330.4778004752374, "15120.0": 326.8776604963411}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out deleted file mode 100644 index 6c4b1b1d9e0..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp5.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 5, "Ca0": 0, "Ca_meas": {"17280.0": 0.5384747196579402, "0.0": -0.14396911833443257, "7560.0": 6.038385982663388, "9720.0": 5.0792329539506, "19440.0": 0.3782801126758533, "16200.0": 1.0619887309834395, "11880.0": 3.6494330494296436, "2160.0": 4.775401751873804, "8640.0": 5.629577532845656, "14040.0": 2.0037718871692265, "4320.0": 5.889802624055117, "6480.0": 6.09724817816528, "1080.0": 2.875851853145854, "18360.0": 0.6780066197547887, "10800.0": 4.859469684381779, "21600.0": 0.3889400954173796, "20520.0": 0.3351378562274788, "5400.0": 6.127222815180268, "3240.0": 5.289726682847115, "12960.0": 2.830845316709853, "15120.0": 1.5312992911111707}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 2, "Tc1": 320, "Cc_meas": {"17280.0": 7.596710556194337, "0.0": 1.6504080367065743, "7560.0": 2.809410585275859, "9720.0": 3.8419183958835132, "19440.0": 8.137782633931637, "16200.0": 6.938967086259325, "11880.0": 5.022162589071362, "2160.0": 2.0515545922033964, "8640.0": 3.506455726732785, "14040.0": 6.010539749263416, "4320.0": 2.2056993474658584, "6480.0": 2.5775763528099858, "1080.0": 2.03522693402577, "18360.0": 8.083917616781594, "10800.0": 4.662851778068136, "21600.0": 9.279674687903626, "20520.0": 8.963676424956157, "5400.0": 2.293408505844697, "3240.0": 1.9216270432789067, "12960.0": 5.637375563057352, "15120.0": 6.3296720972633045}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 12.831256801432378, "0.0": 0.13194621122245662, "7560.0": 7.965534934436229, "9720.0": 10.908595985103954, "19440.0": 12.408596390398941, "16200.0": 12.975405069340143, "11880.0": 12.710800046234393, "2160.0": 0.9223242691530996, "8640.0": 9.454601468197033, "14040.0": 13.19437793062601, "4320.0": 3.713168763161746, "6480.0": 6.515936097446724, "1080.0": 0.031354105110323494, "18360.0": 12.821094923672087, "10800.0": 11.90520078370877, "21600.0": 11.81429953673305, "20520.0": 12.099271866573613, "5400.0": 4.982941965055916, "3240.0": 2.766378581935415, "12960.0": 13.074621618364043, "15120.0": 12.957819319226212}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 326.8759279818479, "0.0": 300.7736745288814, "7560.0": 333.6674031533901, "9720.0": 333.59854139744135, "19440.0": 324.5264772316974, "16200.0": 325.2454701315101, "11880.0": 332.9849253092768, "2160.0": 322.1940607068012, "8640.0": 331.78378240085084, "14040.0": 328.48981010099453, "4320.0": 327.3883510651506, "6480.0": 330.15101610436426, "1080.0": 318.24994073025096, "18360.0": 323.9527212120804, "10800.0": 333.3006916263996, "21600.0": 322.07065855783964, "20520.0": 324.3518907083261, "5400.0": 331.5429008148077, "3240.0": 324.52116111644654, "12960.0": 329.21899337854876, "15120.0": 328.26179934031467}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out deleted file mode 100644 index c1630902e1a..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp6.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 6, "Ca0": 2, "Ca_meas": {"17280.0": 1.1595382970987758, "0.0": 1.9187666718004224, "7560.0": 5.977461039170304, "9720.0": 5.164472215594892, "19440.0": 0.6528636977624275, "16200.0": 1.2106046606700225, "11880.0": 3.3229659243191296, "2160.0": 5.923887627906124, "8640.0": 5.225477003110976, "14040.0": 1.7878931129582107, "4320.0": 6.782806717544953, "6480.0": 6.27323507174512, "1080.0": 4.481633914987097, "18360.0": 0.8866911582721309, "10800.0": 4.481150474336123, "21600.0": 0.2170007972283953, "20520.0": 0.3199825651255196, "5400.0": 6.795886093698936, "3240.0": 6.288606047308427, "12960.0": 2.4509424000990685, "15120.0": 1.506568611506372}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"17280.0": 6.588306395438309, "0.0": 0.24402401145820712, "7560.0": 1.4036889138374646, "9720.0": 2.4218855673847455, "19440.0": 7.764492558630308, "16200.0": 6.205315919403138, "11880.0": 3.593219427441702, "2160.0": -0.10553376629311664, "8640.0": 1.8628128392103824, "14040.0": 5.027532358914124, "4320.0": 0.2172961549286831, "6480.0": 0.875637414228913, "1080.0": 0.2688503672636328, "18360.0": 7.240866507350995, "10800.0": 3.26514503365032, "21600.0": 8.251445433411781, "20520.0": 7.987953548408583, "5400.0": 0.6458428001299884, "3240.0": 0.3201498579399834, "12960.0": 4.263491165240245, "15120.0": 5.885223860529403}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"17280.0": 13.604962664333257, "0.0": -0.20747860874385013, "7560.0": 10.013416230907982, "9720.0": 12.055665770517061, "19440.0": 13.289064414490856, "16200.0": 13.82240671329648, "11880.0": 13.783622629658064, "2160.0": 1.8287780310400052, "8640.0": 11.17018006067254, "14040.0": 14.064985893141849, "4320.0": 5.072613174963567, "6480.0": 8.597613514631933, "1080.0": 0.3932885074677299, "18360.0": 13.473844971871975, "10800.0": 13.343451941795923, "21600.0": 12.209822751574375, "20520.0": 12.483108442400093, "5400.0": 6.7290370118557545, "3240.0": 3.4163314305947527, "12960.0": 14.296996898861073, "15120.0": 14.543019390786785}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"17280.0": 324.3952638382081, "0.0": 300.9417777707309, "7560.0": 334.57634369064954, "9720.0": 336.13718804612154, "19440.0": 324.10564173791454, "16200.0": 324.97714743647435, "11880.0": 332.29384802281055, "2160.0": 324.243456129639, "8640.0": 335.85007440436317, "14040.0": 329.01187645109906, "4320.0": 331.1961476255781, "6480.0": 333.16262386818596, "1080.0": 319.0632107387995, "18360.0": 322.11836267923206, "10800.0": 332.8894634515628, "21600.0": 323.92451205164855, "20520.0": 323.319714630304, "5400.0": 334.21206737651613, "3240.0": 326.78695915581983, "12960.0": 329.6184998003745, "15120.0": 327.5414299857002}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out deleted file mode 100644 index 6ef879f3a17..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp7.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 7, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 5.9967662103693256, "7560.0": 5.6550872327951165, "21600.0": 0.35323003565444244, "9720.0": 5.0380179697260221, "16200.0": 1.1574854069611118, "20520.0": 0.44526599556762764, "3240.0": 5.5465851989526014, "11880.0": 3.4091089779405226, "14040.0": 1.9309662288658835, "17280.0": 0.90614590071077117, "2160.0": 4.5361731979596218, "5400.0": 6.071094394377246, "18360.0": 0.71270371205373184, "8640.0": 5.3475100856285955, "6480.0": 5.9202343949662177, "1080.0": 2.7532264453848811, "15120.0": 1.4880794860689583, "12960.0": 2.5406275798785134, "19440.0": 0.56254756816886675, "10800.0": 4.6684283481238458}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": 9.1835496157991212e-40, "4320.0": 0.2042152487619561, "7560.0": 1.0742319748668527, "21600.0": 7.2209534629193319, "9720.0": 2.0351470130435847, "16200.0": 5.2015141957639486, "20520.0": 6.8469618370195322, "3240.0": 0.080409363629683248, "11880.0": 3.1846111102658314, "14040.0": 4.26052274570326, "17280.0": 5.6380747523602874, "2160.0": 0.020484309410223334, "5400.0": 0.4082391087574655, "18360.0": 6.0566679041163232, "8640.0": 1.5236474138282798, "6480.0": 0.69922443474303053, "1080.0": 0.0017732304596560309, "15120.0": 4.7439891962421239, "12960.0": 3.7433194976361364, "19440.0": 6.4591935065135972, "10800.0": 2.5966732389812686}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 9.1835496157991212e-41, "4320.0": 3.7902671698338786, "7560.0": 8.430643849127673, "21600.0": 11.611715650093123, "9720.0": 10.913795239302978, "16200.0": 12.826899545942249, "20520.0": 11.893671316079821, "3240.0": 2.2947643625752363, "11880.0": 12.592179060461286, "14040.0": 12.994410174098332, "17280.0": 12.641678495596169, "2160.0": 1.0564916422363797, "5400.0": 5.3872034016274126, "18360.0": 12.416527532497087, "8640.0": 9.7522120688862319, "6480.0": 6.9615062931011256, "1080.0": 0.24785349222969222, "15120.0": 12.953830466356314, "12960.0": 12.901952071152909, "19440.0": 12.164158073984597, "10800.0": 11.920797561562614}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 329.97137612867306, "7560.0": 333.16333833254259, "21600.0": 323.31193212521009, "9720.0": 333.2600929343854, "16200.0": 325.39648330671537, "20520.0": 323.56691057280642, "3240.0": 327.66452370998064, "11880.0": 331.65035718435655, "14040.0": 327.56747174535786, "17280.0": 324.74920150215411, "2160.0": 324.4585751379426, "5400.0": 331.60125794337375, "18360.0": 324.25971411725646, "8640.0": 333.33010300559897, "6480.0": 332.63089408099353, "1080.0": 318.87878296714581, "15120.0": 326.2893083756407, "12960.0": 329.38909200530219, "19440.0": 323.87612174726837, "10800.0": 333.08705569007822}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out deleted file mode 100644 index 6aa9fea17b3..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp8.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 8, "Ca0": 0, "Ca_meas": {"0.0": 0.30862088766711671, "4320.0": 6.0491110491659228, "7560.0": 5.7909310485601502, "21600.0": 0.232444399226299, "9720.0": 4.9320449060797475, "16200.0": 0.97134242753331668, "20520.0": 0.42847724332841963, "3240.0": 5.6988320807198498, "11880.0": 3.3235733576868514, "14040.0": 1.9846460628194049, "17280.0": 0.87715206210722585, "2160.0": 4.615346351863904, "5400.0": 6.384056703029386, "18360.0": 0.41688144324552118, "8640.0": 5.4121173109702099, "6480.0": 6.0660731346226324, "1080.0": 2.8379509025410488, "15120.0": 0.98831570466285279, "12960.0": 2.2167483934357417, "19440.0": 0.46284950985984985, "10800.0": 4.7377220491627412}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": -0.050833820035522316, "4320.0": 0.21055066883154505, "7560.0": 1.3851246436045646, "21600.0": 7.3672985895890362, "9720.0": 2.0100502709379842, "16200.0": 5.1793087406376159, "20520.0": 6.840381847429823, "3240.0": 0.13411276227648503, "11880.0": 3.3052152545385454, "14040.0": 3.9431305823279708, "17280.0": 5.7290141848801586, "2160.0": 0.16719633749951002, "5400.0": 0.49872603502453117, "18360.0": 6.1508540969551078, "8640.0": 1.4737312345987361, "6480.0": 0.69437977126769512, "1080.0": -0.0093978134715377304, "15120.0": 4.9151661032041298, "12960.0": 4.0623539766149843, "19440.0": 6.3058400571478561, "10800.0": 2.820347355873587}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 0.037166728983599892, "4320.0": 3.5183932586092856, "7560.0": 8.916682434428397, "21600.0": 11.534104657089006, "9720.0": 11.087958791247285, "16200.0": 13.02597376112109, "20520.0": 11.639999923137731, "3240.0": 2.346958004261503, "11880.0": 12.049613604010537, "14040.0": 12.906738918465997, "17280.0": 12.867691165140879, "2160.0": 1.3313958783841602, "5400.0": 5.3650409213472221, "18360.0": 12.405763004965722, "8640.0": 9.5635832344445717, "6480.0": 7.0954049721214671, "1080.0": 0.40883709280782765, "15120.0": 12.971506554625082, "12960.0": 12.829158718434032, "19440.0": 11.946615137583075, "10800.0": 11.373799750334223}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.55141264078583, "4320.0": 329.42497918063066, "7560.0": 333.82602046942475, "21600.0": 323.68642879192487, "9720.0": 332.94208820576767, "16200.0": 325.82299128298814, "20520.0": 325.19753703643721, "3240.0": 329.66504941755875, "11880.0": 332.29546982118751, "14040.0": 326.51837436850099, "17280.0": 326.51851506890586, "2160.0": 323.70134945698589, "5400.0": 328.6805843225718, "18360.0": 324.79832692054578, "8640.0": 331.94068007914785, "6480.0": 332.75141896044545, "1080.0": 318.90722718736015, "15120.0": 325.85289150843209, "12960.0": 327.72250161440121, "19440.0": 325.17198606848808, "10800.0": 334.1255807822717}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out b/pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out deleted file mode 100644 index 627f92b1f83..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/exp9.out +++ /dev/null @@ -1 +0,0 @@ -{"experiment": 9, "Ca0": 0, "Ca_meas": {"0.0": 0.0, "4320.0": 5.9967662103693256, "7560.0": 5.6550872327951165, "21600.0": 0.35323003565444244, "9720.0": 5.0380179697260221, "16200.0": 1.1574854069611118, "20520.0": 0.44526599556762764, "3240.0": 5.5465851989526014, "11880.0": 3.4091089779405226, "14040.0": 1.9309662288658835, "17280.0": 0.90614590071077117, "2160.0": 4.5361731979596218, "5400.0": 6.071094394377246, "18360.0": 0.71270371205373184, "8640.0": 5.3475100856285955, "6480.0": 5.9202343949662177, "1080.0": 2.7532264453848811, "15120.0": 1.4880794860689583, "12960.0": 2.5406275798785134, "19440.0": 0.56254756816886675, "10800.0": 4.6684283481238458}, "alphac": 0.7, "Fa2": 0.0, "deltaH1": -40000, "Fa1": 0.003, "Tc2": 320, "Cc0": 0, "Tc1": 320, "Cc_meas": {"0.0": 9.1835496157991212e-40, "4320.0": 0.2042152487619561, "7560.0": 1.0742319748668527, "21600.0": 7.2209534629193319, "9720.0": 2.0351470130435847, "16200.0": 5.2015141957639486, "20520.0": 6.8469618370195322, "3240.0": 0.080409363629683248, "11880.0": 3.1846111102658314, "14040.0": 4.26052274570326, "17280.0": 5.6380747523602874, "2160.0": 0.020484309410223334, "5400.0": 0.4082391087574655, "18360.0": 6.0566679041163232, "8640.0": 1.5236474138282798, "6480.0": 0.69922443474303053, "1080.0": 0.0017732304596560309, "15120.0": 4.7439891962421239, "12960.0": 3.7433194976361364, "19440.0": 6.4591935065135972, "10800.0": 2.5966732389812686}, "alphaj": 0.8, "Cb0": 0, "Vr0": 1, "Cb_meas": {"0.0": 9.1835496157991212e-41, "4320.0": 3.7902671698338786, "7560.0": 8.430643849127673, "21600.0": 11.611715650093123, "9720.0": 10.913795239302978, "16200.0": 12.826899545942249, "20520.0": 11.893671316079821, "3240.0": 2.2947643625752363, "11880.0": 12.592179060461286, "14040.0": 12.994410174098332, "17280.0": 12.641678495596169, "2160.0": 1.0564916422363797, "5400.0": 5.3872034016274126, "18360.0": 12.416527532497087, "8640.0": 9.7522120688862319, "6480.0": 6.9615062931011256, "1080.0": 0.24785349222969222, "15120.0": 12.953830466356314, "12960.0": 12.901952071152909, "19440.0": 12.164158073984597, "10800.0": 11.920797561562614}, "Tf": 300, "Tr0": 300, "deltaH2": -50000, "Tr_meas": {"0.0": 300.0, "4320.0": 329.97137612867306, "7560.0": 333.16333833254259, "21600.0": 323.31193212521009, "9720.0": 333.2600929343854, "16200.0": 325.39648330671537, "20520.0": 323.56691057280642, "3240.0": 327.66452370998064, "11880.0": 331.65035718435655, "14040.0": 327.56747174535786, "17280.0": 324.74920150215411, "2160.0": 324.4585751379426, "5400.0": 331.60125794337375, "18360.0": 324.25971411725646, "8640.0": 333.33010300559897, "6480.0": 332.63089408099353, "1080.0": 318.87878296714581, "15120.0": 326.2893083756407, "12960.0": 329.38909200530219, "19440.0": 323.87612174726837, "10800.0": 333.08705569007822}} \ No newline at end of file diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/obj_at_theta.csv b/pyomo/contrib/parmest/deprecated/examples/semibatch/obj_at_theta.csv deleted file mode 100644 index 79f03e07dcd..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/obj_at_theta.csv +++ /dev/null @@ -1,1009 +0,0 @@ -,k1,k2,E1,E2,obj -0,4,40,29000,38000,667.4023645794207 -1,4,40,29000,38500,665.8312183437167 -2,4,40,29000,39000,672.7539769993407 -3,4,40,29000,39500,684.9503752463216 -4,4,40,29000,40000,699.985589093255 -5,4,40,29000,40500,716.1241770970677 -6,4,40,29000,41000,732.2023201586336 -7,4,40,29000,41500,747.4931745925483 -8,4,40,29500,38000,907.4405527163311 -9,4,40,29500,38500,904.2229271927299 -10,4,40,29500,39000,907.6942345285257 -11,4,40,29500,39500,915.4570013614677 -12,4,40,29500,40000,925.65401444575 -13,4,40,29500,40500,936.9348578520337 -14,4,40,29500,41000,948.3759339765711 -15,4,40,29500,41500,959.386491783636 -16,4,40,30000,38000,1169.8685711377334 -17,4,40,30000,38500,1166.2211505723928 -18,4,40,30000,39000,1167.702295374574 -19,4,40,30000,39500,1172.5517020611685 -20,4,40,30000,40000,1179.3820406408263 -21,4,40,30000,40500,1187.1698633839655 -22,4,40,30000,41000,1195.2047840919602 -23,4,40,30000,41500,1203.0241101248102 -24,4,40,30500,38000,1445.9591944684807 -25,4,40,30500,38500,1442.6632745483 -26,4,40,30500,39000,1443.1982444457385 -27,4,40,30500,39500,1446.2833842279929 -28,4,40,30500,40000,1450.9012120934779 -29,4,40,30500,40500,1456.295140290636 -30,4,40,30500,41000,1461.9350767569827 -31,4,40,30500,41500,1467.4715014446226 -32,4,40,31000,38000,1726.8744994061449 -33,4,40,31000,38500,1724.2679845375048 -34,4,40,31000,39000,1724.4550886870552 -35,4,40,31000,39500,1726.5124587129135 -36,4,40,31000,40000,1729.7061680616455 -37,4,40,31000,40500,1733.48893482641 -38,4,40,31000,41000,1737.4753558920438 -39,4,40,31000,41500,1741.4093763605517 -40,4,40,31500,38000,2004.1978135112938 -41,4,40,31500,38500,2002.2807839860222 -42,4,40,31500,39000,2002.3676405166086 -43,4,40,31500,39500,2003.797808439923 -44,4,40,31500,40000,2006.048051591001 -45,4,40,31500,40500,2008.7281679153625 -46,4,40,31500,41000,2011.5626384878237 -47,4,40,31500,41500,2014.3675286347284 -48,4,80,29000,38000,845.8197358579285 -49,4,80,29000,38500,763.5039795545781 -50,4,80,29000,39000,709.8529964173656 -51,4,80,29000,39500,679.4215539491266 -52,4,80,29000,40000,666.4876088521157 -53,4,80,29000,40500,665.978271760966 -54,4,80,29000,41000,673.7240200504901 -55,4,80,29000,41500,686.4763909417914 -56,4,80,29500,38000,1042.519415429413 -57,4,80,29500,38500,982.8097210678039 -58,4,80,29500,39000,942.2990207573541 -59,4,80,29500,39500,917.9550916645245 -60,4,80,29500,40000,906.3116029967189 -61,4,80,29500,40500,904.0326666308792 -62,4,80,29500,41000,908.1964630052729 -63,4,80,29500,41500,916.4222043837499 -64,4,80,30000,38000,1271.1030403496538 -65,4,80,30000,38500,1227.7527550544085 -66,4,80,30000,39000,1197.433957624904 -67,4,80,30000,39500,1178.447676126182 -68,4,80,30000,40000,1168.645219243497 -69,4,80,30000,40500,1165.7995210546096 -70,4,80,30000,41000,1167.8586496250396 -71,4,80,30000,41500,1173.0949214020527 -72,4,80,30500,38000,1520.8220402652044 -73,4,80,30500,38500,1489.2563260709424 -74,4,80,30500,39000,1466.8099189128857 -75,4,80,30500,39500,1452.4352624958806 -76,4,80,30500,40000,1444.7074679423818 -77,4,80,30500,40500,1442.0820578624343 -78,4,80,30500,41000,1443.099006489627 -79,4,80,30500,41500,1446.5106517200784 -80,4,80,31000,38000,1781.149136032395 -81,4,80,31000,38500,1758.2414369536502 -82,4,80,31000,39000,1741.891639711003 -83,4,80,31000,39500,1731.358661496594 -84,4,80,31000,40000,1725.6231647999593 -85,4,80,31000,40500,1723.5757174297378 -86,4,80,31000,41000,1724.1680229486278 -87,4,80,31000,41500,1726.5050840601884 -88,4,80,31500,38000,2042.8335948845602 -89,4,80,31500,38500,2026.3067503042414 -90,4,80,31500,39000,2014.5720701940838 -91,4,80,31500,39500,2007.0463766643977 -92,4,80,31500,40000,2002.9647983728314 -93,4,80,31500,40500,2001.5163951989875 -94,4,80,31500,41000,2001.9474217001339 -95,4,80,31500,41500,2003.6204088755821 -96,4,120,29000,38000,1176.0713512305115 -97,4,120,29000,38500,1016.8213383282462 -98,4,120,29000,39000,886.0136231565133 -99,4,120,29000,39500,789.0101180066036 -100,4,120,29000,40000,724.5420056133441 -101,4,120,29000,40500,686.6877602625062 -102,4,120,29000,41000,668.8129085873959 -103,4,120,29000,41500,665.1167761036883 -104,4,120,29500,38000,1263.887274509128 -105,4,120,29500,38500,1155.6528408872423 -106,4,120,29500,39000,1066.393539894248 -107,4,120,29500,39500,998.9931006471243 -108,4,120,29500,40000,952.36314487701 -109,4,120,29500,40500,923.4000293372077 -110,4,120,29500,41000,908.407361383214 -111,4,120,29500,41500,903.8136176328255 -112,4,120,30000,38000,1421.1418235449091 -113,4,120,30000,38500,1347.114022652679 -114,4,120,30000,39000,1285.686103704643 -115,4,120,30000,39500,1238.2456448658272 -116,4,120,30000,40000,1204.3526810790904 -117,4,120,30000,40500,1182.4272879027071 -118,4,120,30000,41000,1170.3447810121902 -119,4,120,30000,41500,1165.8422968073423 -120,4,120,30500,38000,1625.5588911535713 -121,4,120,30500,38500,1573.5546642859429 -122,4,120,30500,39000,1530.1592840718379 -123,4,120,30500,39500,1496.2087139473604 -124,4,120,30500,40000,1471.525855239756 -125,4,120,30500,40500,1455.2084749904016 -126,4,120,30500,41000,1445.9160840082027 -127,4,120,30500,41500,1442.1255377330835 -128,4,120,31000,38000,1855.8467211183756 -129,4,120,31000,38500,1818.4368412235558 -130,4,120,31000,39000,1787.25956706785 -131,4,120,31000,39500,1762.8169908546402 -132,4,120,31000,40000,1744.9825741661596 -133,4,120,31000,40500,1733.136625016882 -134,4,120,31000,41000,1726.3352245899828 -135,4,120,31000,41500,1723.492199933745 -136,4,120,31500,38000,2096.6479813687533 -137,4,120,31500,38500,2069.3606691038876 -138,4,120,31500,39000,2046.792043575205 -139,4,120,31500,39500,2029.2128703900223 -140,4,120,31500,40000,2016.4664599897606 -141,4,120,31500,40500,2008.054814885348 -142,4,120,31500,41000,2003.2622557140814 -143,4,120,31500,41500,2001.289784483679 -144,7,40,29000,38000,149.32898706737052 -145,7,40,29000,38500,161.04814413969586 -146,7,40,29000,39000,187.87801343005242 -147,7,40,29000,39500,223.00789161520424 -148,7,40,29000,40000,261.66779887964003 -149,7,40,29000,40500,300.676316191238 -150,7,40,29000,41000,338.04021206995765 -151,7,40,29000,41500,372.6191631389286 -152,7,40,29500,38000,276.6495061185777 -153,7,40,29500,38500,282.1304583501965 -154,7,40,29500,39000,300.91417483065254 -155,7,40,29500,39500,327.24304394350395 -156,7,40,29500,40000,357.0561976596432 -157,7,40,29500,40500,387.61662064170207 -158,7,40,29500,41000,417.1836349752378 -159,7,40,29500,41500,444.73705844573243 -160,7,40,30000,38000,448.0380830353589 -161,7,40,30000,38500,448.8094536459122 -162,7,40,30000,39000,460.77530593327293 -163,7,40,30000,39500,479.342874472736 -164,7,40,30000,40000,501.20694459059405 -165,7,40,30000,40500,524.0971649678811 -166,7,40,30000,41000,546.539334134893 -167,7,40,30000,41500,567.6447156158981 -168,7,40,30500,38000,657.9909416906933 -169,7,40,30500,38500,655.7465129488842 -170,7,40,30500,39000,662.5420970804985 -171,7,40,30500,39500,674.8914651553109 -172,7,40,30500,40000,690.2111920703564 -173,7,40,30500,40500,706.6833639709198 -174,7,40,30500,41000,723.0994507096715 -175,7,40,30500,41500,738.7096013891406 -176,7,40,31000,38000,899.1769906655776 -177,7,40,31000,38500,895.4391505892945 -178,7,40,31000,39000,898.7695629120826 -179,7,40,31000,39500,906.603316771593 -180,7,40,31000,40000,916.9811481373996 -181,7,40,31000,40500,928.4913367709245 -182,7,40,31000,41000,940.1744934710283 -183,7,40,31000,41500,951.4199286075984 -184,7,40,31500,38000,1163.093373675207 -185,7,40,31500,38500,1159.0457727559028 -186,7,40,31500,39000,1160.3831770028223 -187,7,40,31500,39500,1165.2451698296604 -188,7,40,31500,40000,1172.1768190340001 -189,7,40,31500,40500,1180.1105659428963 -190,7,40,31500,41000,1188.3083929833688 -191,7,40,31500,41500,1196.29112579565 -192,7,80,29000,38000,514.0332369183081 -193,7,80,29000,38500,329.3645784712966 -194,7,80,29000,39000,215.73000998706416 -195,7,80,29000,39500,162.37338399591852 -196,7,80,29000,40000,149.8401793263549 -197,7,80,29000,40500,162.96125998112578 -198,7,80,29000,41000,191.173279165834 -199,7,80,29000,41500,227.2781971491003 -200,7,80,29500,38000,623.559246695578 -201,7,80,29500,38500,448.60620511421484 -202,7,80,29500,39000,344.21940687907573 -203,7,80,29500,39500,292.9758707105001 -204,7,80,29500,40000,277.07670134364804 -205,7,80,29500,40500,283.5158840045542 -206,7,80,29500,41000,303.33951582820265 -207,7,80,29500,41500,330.43357046741954 -208,7,80,30000,38000,732.5907387079073 -209,7,80,30000,38500,593.1926567994672 -210,7,80,30000,39000,508.5638538704666 -211,7,80,30000,39500,464.47881763522037 -212,7,80,30000,40000,448.0394620671692 -213,7,80,30000,40500,449.64309860415494 -214,7,80,30000,41000,462.4490598612332 -215,7,80,30000,41500,481.6323506247537 -216,7,80,30500,38000,871.1163930229344 -217,7,80,30500,38500,771.1320563649375 -218,7,80,30500,39000,707.8872660015606 -219,7,80,30500,39500,672.6612145133173 -220,7,80,30500,40000,657.4974157809264 -221,7,80,30500,40500,656.0835852491216 -222,7,80,30500,41000,663.6006958125331 -223,7,80,30500,41500,676.460675405631 -224,7,80,31000,38000,1053.1852617390061 -225,7,80,31000,38500,984.3647109805877 -226,7,80,31000,39000,938.6158531749268 -227,7,80,31000,39500,911.4268280093535 -228,7,80,31000,40000,898.333365348419 -229,7,80,31000,40500,895.3996527486954 -230,7,80,31000,41000,899.3556288533885 -231,7,80,31000,41500,907.6180684887955 -232,7,80,31500,38000,1274.2255948763498 -233,7,80,31500,38500,1226.5236809533717 -234,7,80,31500,39000,1193.4538731398666 -235,7,80,31500,39500,1172.8105398345213 -236,7,80,31500,40000,1162.0692230240734 -237,7,80,31500,40500,1158.7461521476607 -238,7,80,31500,41000,1160.6173577210805 -239,7,80,31500,41500,1165.840315694716 -240,7,120,29000,38000,1325.2409732290193 -241,7,120,29000,38500,900.8063148840154 -242,7,120,29000,39000,629.9300352098937 -243,7,120,29000,39500,413.81648033893424 -244,7,120,29000,40000,257.3116751690404 -245,7,120,29000,40500,177.89217179438947 -246,7,120,29000,41000,151.58366848473491 -247,7,120,29000,41500,157.56967437251706 -248,7,120,29500,38000,1211.2807882170853 -249,7,120,29500,38500,956.936161969002 -250,7,120,29500,39000,753.3050086992201 -251,7,120,29500,39500,528.2452647799327 -252,7,120,29500,40000,382.62610532894917 -253,7,120,29500,40500,308.44199089882375 -254,7,120,29500,41000,280.3893024671524 -255,7,120,29500,41500,280.4028092582749 -256,7,120,30000,38000,1266.5740351143413 -257,7,120,30000,38500,1084.3028700477778 -258,7,120,30000,39000,834.2392498526193 -259,7,120,30000,39500,650.7560171314304 -260,7,120,30000,40000,537.7846910878052 -261,7,120,30000,40500,477.3001078155485 -262,7,120,30000,41000,451.6865380286754 -263,7,120,30000,41500,448.14911508024613 -264,7,120,30500,38000,1319.6603196780936 -265,7,120,30500,38500,1102.3027489012372 -266,7,120,30500,39000,931.2523583659847 -267,7,120,30500,39500,807.0833484596384 -268,7,120,30500,40000,727.4852710400268 -269,7,120,30500,40500,682.1437030344305 -270,7,120,30500,41000,660.7859329989657 -271,7,120,30500,41500,655.6001132492668 -272,7,120,31000,38000,1330.5306924865326 -273,7,120,31000,38500,1195.9190861202942 -274,7,120,31000,39000,1086.0328080422887 -275,7,120,31000,39500,1005.4160637517409 -276,7,120,31000,40000,951.2021706290612 -277,7,120,31000,40500,918.1457644271304 -278,7,120,31000,41000,901.0511005554887 -279,7,120,31000,41500,895.4599964465793 -280,7,120,31500,38000,1447.8365822059013 -281,7,120,31500,38500,1362.3417347939844 -282,7,120,31500,39000,1292.382727215108 -283,7,120,31500,39500,1239.1826828976662 -284,7,120,31500,40000,1201.6474412465277 -285,7,120,31500,40500,1177.5235955796813 -286,7,120,31500,41000,1164.1761722345295 -287,7,120,31500,41500,1158.9997785002718 -288,10,40,29000,38000,33.437068437082054 -289,10,40,29000,38500,58.471249815534996 -290,10,40,29000,39000,101.41937628542912 -291,10,40,29000,39500,153.80690200519626 -292,10,40,29000,40000,209.66451461551316 -293,10,40,29000,40500,265.03070792175197 -294,10,40,29000,41000,317.46079310177566 -295,10,40,29000,41500,365.59950388342645 -296,10,40,29500,38000,70.26818405688635 -297,10,40,29500,38500,87.96463718548947 -298,10,40,29500,39000,122.58188233160993 -299,10,40,29500,39500,166.2478945807132 -300,10,40,29500,40000,213.48669617414316 -301,10,40,29500,40500,260.67953961944477 -302,10,40,29500,41000,305.5877041218316 -303,10,40,29500,41500,346.95612213021155 -304,10,40,30000,38000,153.67588703371362 -305,10,40,30000,38500,164.07504103479005 -306,10,40,30000,39000,190.0800160661499 -307,10,40,30000,39500,224.61382980242837 -308,10,40,30000,40000,262.79232847382445 -309,10,40,30000,40500,301.38687703450415 -310,10,40,30000,41000,338.38536686093164 -311,10,40,30000,41500,372.6399011703545 -312,10,40,30500,38000,284.2936286531718 -313,10,40,30500,38500,288.4690608277705 -314,10,40,30500,39000,306.44667517621144 -315,10,40,30500,39500,332.20122250191986 -316,10,40,30500,40000,361.5566690083291 -317,10,40,30500,40500,391.72755224929614 -318,10,40,30500,41000,420.95317535960476 -319,10,40,30500,41500,448.2049230608669 -320,10,40,31000,38000,459.03140021766137 -321,10,40,31000,38500,458.71477027519967 -322,10,40,31000,39000,469.9910751800656 -323,10,40,31000,39500,488.05850105225426 -324,10,40,31000,40000,509.5204701455629 -325,10,40,31000,40500,532.0674969691778 -326,10,40,31000,41000,554.2088430693509 -327,10,40,31000,41500,575.0485839499048 -328,10,40,31500,38000,672.2476845983564 -329,10,40,31500,38500,669.2240508488649 -330,10,40,31500,39000,675.4956226836405 -331,10,40,31500,39500,687.447764319295 -332,10,40,31500,40000,702.4395430742891 -333,10,40,31500,40500,718.6279487347668 -334,10,40,31500,41000,734.793684592168 -335,10,40,31500,41500,750.1821072409286 -336,10,80,29000,38000,387.7617282731497 -337,10,80,29000,38500,195.33642612593002 -338,10,80,29000,39000,82.7306931465102 -339,10,80,29000,39500,35.13436471793541 -340,10,80,29000,40000,33.521138659248706 -341,10,80,29000,40500,61.47395975053128 -342,10,80,29000,41000,106.71403229340167 -343,10,80,29000,41500,160.56068704487473 -344,10,80,29500,38000,459.63404601804103 -345,10,80,29500,38500,258.7453720995899 -346,10,80,29500,39000,135.96435731320256 -347,10,80,29500,39500,80.2685095017944 -348,10,80,29500,40000,70.86302366453106 -349,10,80,29500,40500,90.43203026480438 -350,10,80,29500,41000,126.7844695901737 -351,10,80,29500,41500,171.63682876805044 -352,10,80,30000,38000,564.1463320344325 -353,10,80,30000,38500,360.75718124523866 -354,10,80,30000,39000,231.70119191254307 -355,10,80,30000,39500,170.74752201483128 -356,10,80,30000,40000,154.7149036950422 -357,10,80,30000,40500,166.10596450541493 -358,10,80,30000,41000,193.3351721194443 -359,10,80,30000,41500,228.78394172417038 -360,10,80,30500,38000,689.6797223218513 -361,10,80,30500,38500,484.8023695265838 -362,10,80,30500,39000,363.5979340028588 -363,10,80,30500,39500,304.67857102688225 -364,10,80,30500,40000,285.29210000833734 -365,10,80,30500,40500,290.0135917456113 -366,10,80,30500,41000,308.8672169492536 -367,10,80,30500,41500,335.3210332569182 -368,10,80,31000,38000,789.946106942773 -369,10,80,31000,38500,625.7722360026959 -370,10,80,31000,39000,528.6063264942235 -371,10,80,31000,39500,478.6863763478618 -372,10,80,31000,40000,459.5026243189753 -373,10,80,31000,40500,459.6982093164963 -374,10,80,31000,41000,471.6790024321937 -375,10,80,31000,41500,490.3034492109124 -376,10,80,31500,38000,912.3540488244158 -377,10,80,31500,38500,798.2135101409633 -378,10,80,31500,39000,727.746684419146 -379,10,80,31500,39500,689.0119464356724 -380,10,80,31500,40000,672.0757202772029 -381,10,80,31500,40500,669.678339553036 -382,10,80,31500,41000,676.5761221409929 -383,10,80,31500,41500,688.9934449650118 -384,10,120,29000,38000,1155.1165164624408 -385,10,120,29000,38500,840.2641727088946 -386,10,120,29000,39000,506.9102636732852 -387,10,120,29000,39500,265.5278912452038 -388,10,120,29000,40000,116.39516513179322 -389,10,120,29000,40500,45.2088092745619 -390,10,120,29000,41000,30.22267557153353 -391,10,120,29000,41500,51.06063746392809 -392,10,120,29500,38000,1343.7868459826054 -393,10,120,29500,38500,977.9852373227346 -394,10,120,29500,39000,594.632756549817 -395,10,120,29500,39500,346.2478773329187 -396,10,120,29500,40000,180.23082247413407 -397,10,120,29500,40500,95.81649989178923 -398,10,120,29500,41000,71.0837801649128 -399,10,120,29500,41500,82.84289818279714 -400,10,120,30000,38000,1532.9333545384934 -401,10,120,30000,38500,1012.2223350568845 -402,10,120,30000,39000,688.4884716222766 -403,10,120,30000,39500,464.6206903113392 -404,10,120,30000,40000,283.5644748300334 -405,10,120,30000,40500,190.27593217865416 -406,10,120,30000,41000,158.0192279691727 -407,10,120,30000,41500,161.3611926772337 -408,10,120,30500,38000,1349.3785399811063 -409,10,120,30500,38500,1014.785480110738 -410,10,120,30500,39000,843.0316833766408 -411,10,120,30500,39500,589.4543896730125 -412,10,120,30500,40000,412.3358512291996 -413,10,120,30500,40500,324.11715620464133 -414,10,120,30500,41000,290.17588242984766 -415,10,120,30500,41500,287.56857384673356 -416,10,120,31000,38000,1328.0973931040146 -417,10,120,31000,38500,1216.5659656437845 -418,10,120,31000,39000,928.4831767181619 -419,10,120,31000,39500,700.3115484040329 -420,10,120,31000,40000,565.0876352458171 -421,10,120,31000,40500,494.44016026435037 -422,10,120,31000,41000,464.38005437182983 -423,10,120,31000,41500,458.7614573733091 -424,10,120,31500,38000,1473.1154650008834 -425,10,120,31500,38500,1195.943614951571 -426,10,120,31500,39000,990.2486604382486 -427,10,120,31500,39500,843.1390407497395 -428,10,120,31500,40000,751.2746391170706 -429,10,120,31500,40500,700.215375503209 -430,10,120,31500,41000,676.1585052687219 -431,10,120,31500,41500,669.5907920932743 -432,13,40,29000,38000,49.96352152045025 -433,13,40,29000,38500,83.75104994958261 -434,13,40,29000,39000,136.8176091795391 -435,13,40,29000,39500,199.91486685466407 -436,13,40,29000,40000,266.4367154860076 -437,13,40,29000,40500,331.97224579940524 -438,13,40,29000,41000,393.8001583706036 -439,13,40,29000,41500,450.42425363084493 -440,13,40,29500,38000,29.775721038786923 -441,13,40,29500,38500,57.37673742631121 -442,13,40,29500,39000,103.49161398239501 -443,13,40,29500,39500,159.3058253852367 -444,13,40,29500,40000,218.60083223764073 -445,13,40,29500,40500,277.2507278183831 -446,13,40,29500,41000,332.7141278886951 -447,13,40,29500,41500,383.58832292300576 -448,13,40,30000,38000,47.72263852005472 -449,13,40,30000,38500,68.07581028940402 -450,13,40,30000,39000,106.13974628945516 -451,13,40,30000,39500,153.58449949683063 -452,13,40,30000,40000,204.62393623358633 -453,13,40,30000,40500,255.44513025602419 -454,13,40,30000,41000,303.69954914051766 -455,13,40,30000,41500,348.0803709720354 -456,13,40,30500,38000,110.9331168284094 -457,13,40,30500,38500,123.63361262704746 -458,13,40,30500,39000,153.02654433825705 -459,13,40,30500,39500,191.40769947472756 -460,13,40,30500,40000,233.503841403055 -461,13,40,30500,40500,275.8557790922913 -462,13,40,30500,41000,316.32529882763697 -463,13,40,30500,41500,353.7060432094809 -464,13,40,31000,38000,221.90608823073939 -465,13,40,31000,38500,227.67026441593657 -466,13,40,31000,39000,248.62107049869064 -467,13,40,31000,39500,277.9507605389158 -468,13,40,31000,40000,311.0267471957685 -469,13,40,31000,40500,344.8024031161673 -470,13,40,31000,41000,377.3761144228052 -471,13,40,31000,41500,407.6529635071056 -472,13,40,31500,38000,378.8738382757093 -473,13,40,31500,38500,379.39748335944216 -474,13,40,31500,39000,393.01223361732553 -475,13,40,31500,39500,414.10238059122855 -476,13,40,31500,40000,438.8024282436204 -477,13,40,31500,40500,464.5348067190265 -478,13,40,31500,41000,489.6621039898805 -479,13,40,31500,41500,513.2163939332803 -480,13,80,29000,38000,364.387588581215 -481,13,80,29000,38500,184.2902007673634 -482,13,80,29000,39000,81.57192155036655 -483,13,80,29000,39500,42.54811210095659 -484,13,80,29000,40000,49.897338772663076 -485,13,80,29000,40500,87.84229516509882 -486,13,80,29000,41000,143.85451969447664 -487,13,80,29000,41500,208.71467984917848 -488,13,80,29500,38000,382.5794635435733 -489,13,80,29500,38500,188.38619353711718 -490,13,80,29500,39000,75.75749359688277 -491,13,80,29500,39500,29.27891251986562 -492,13,80,29500,40000,29.794874961934568 -493,13,80,29500,40500,60.654888662698205 -494,13,80,29500,41000,109.25801388824325 -495,13,80,29500,41500,166.6311093454692 -496,13,80,30000,38000,448.97795526074816 -497,13,80,30000,38500,238.44530107604737 -498,13,80,30000,39000,112.34545890264337 -499,13,80,30000,39500,56.125871791222835 -500,13,80,30000,40000,48.29987461781518 -501,13,80,30000,40500,70.7900626637678 -502,13,80,30000,41000,110.76865376691964 -503,13,80,30000,41500,159.50197316936024 -504,13,80,30500,38000,547.7818730461195 -505,13,80,30500,38500,332.92604070423494 -506,13,80,30500,39000,193.80760050280742 -507,13,80,30500,39500,128.3457644087917 -508,13,80,30500,40000,112.23915895822442 -509,13,80,30500,40500,125.96369396512564 -510,13,80,30500,41000,156.67918617660013 -511,13,80,30500,41500,196.05195109523765 -512,13,80,31000,38000,682.8591931963246 -513,13,80,31000,38500,457.56562267948556 -514,13,80,31000,39000,313.6380169123524 -515,13,80,31000,39500,245.13531819580908 -516,13,80,31000,40000,223.54473391202873 -517,13,80,31000,40500,229.60752111202834 -518,13,80,31000,41000,251.42377424735136 -519,13,80,31000,41500,281.48720903016886 -520,13,80,31500,38000,807.925638050234 -521,13,80,31500,38500,588.686585641994 -522,13,80,31500,39000,464.0488586698228 -523,13,80,31500,39500,402.69214492641095 -524,13,80,31500,40000,380.13626165363934 -525,13,80,31500,40500,380.8064948609387 -526,13,80,31500,41000,395.05186915919086 -527,13,80,31500,41500,416.70193045600774 -528,13,120,29000,38000,1068.8279454397398 -529,13,120,29000,38500,743.0012805963486 -530,13,120,29000,39000,451.2538301167544 -531,13,120,29000,39500,235.4154251166075 -532,13,120,29000,40000,104.73720814447498 -533,13,120,29000,40500,46.91983990671749 -534,13,120,29000,41000,42.81092192562316 -535,13,120,29000,41500,74.33530639171506 -536,13,120,29500,38000,1133.1178848710972 -537,13,120,29500,38500,824.0745323788527 -538,13,120,29500,39000,499.10867111401996 -539,13,120,29500,39500,256.1626809904186 -540,13,120,29500,40000,107.68599585294751 -541,13,120,29500,40500,38.18533662516749 -542,13,120,29500,41000,25.499608203619154 -543,13,120,29500,41500,49.283537699300375 -544,13,120,30000,38000,1292.409871290162 -545,13,120,30000,38500,994.669572829704 -546,13,120,30000,39000,598.9783697712826 -547,13,120,30000,39500,327.47348408537925 -548,13,120,30000,40000,156.82634841081907 -549,13,120,30000,40500,71.30833688875883 -550,13,120,30000,41000,47.72389750130817 -551,13,120,30000,41500,62.1982461882982 -552,13,120,30500,38000,1585.8797221278146 -553,13,120,30500,38500,1144.66688416451 -554,13,120,30500,39000,692.6651441690645 -555,13,120,30500,39500,441.98837639874046 -556,13,120,30500,40000,251.56311435857728 -557,13,120,30500,40500,149.79670413140468 -558,13,120,30500,41000,115.52645596043719 -559,13,120,30500,41500,120.44019473389324 -560,13,120,31000,38000,1702.7625866892163 -561,13,120,31000,38500,1071.7854750250656 -562,13,120,31000,39000,807.8943299034604 -563,13,120,31000,39500,588.672223513561 -564,13,120,31000,40000,376.44658358671404 -565,13,120,31000,40500,269.2159719426485 -566,13,120,31000,41000,229.41660529009877 -567,13,120,31000,41500,226.78274707181976 -568,13,120,31500,38000,1331.3523701291767 -569,13,120,31500,38500,1151.2055268669133 -570,13,120,31500,39000,1006.811285091974 -571,13,120,31500,39500,702.0053094629535 -572,13,120,31500,40000,515.9081891614829 -573,13,120,31500,40500,423.8652275555525 -574,13,120,31500,41000,386.4939696097151 -575,13,120,31500,41500,379.8118453367429 -576,16,40,29000,38000,106.1025746852808 -577,16,40,29000,38500,145.32590128581407 -578,16,40,29000,39000,204.74804378224422 -579,16,40,29000,39500,274.6339266648551 -580,16,40,29000,40000,347.9667393938497 -581,16,40,29000,40500,420.03753452490974 -582,16,40,29000,41000,487.9353932879741 -583,16,40,29000,41500,550.0623063219693 -584,16,40,29500,38000,54.65040870471303 -585,16,40,29500,38500,88.94089091627293 -586,16,40,29500,39000,142.72223808288405 -587,16,40,29500,39500,206.63598763907422 -588,16,40,29500,40000,273.99851593521134 -589,16,40,29500,40500,340.34861536649436 -590,16,40,29500,41000,402.935270882596 -591,16,40,29500,41500,460.2471155081633 -592,16,40,30000,38000,29.788548081995298 -593,16,40,30000,38500,57.96323252610644 -594,16,40,30000,39000,104.92815906834525 -595,16,40,30000,39500,161.71867032726158 -596,16,40,30000,40000,222.01677586338877 -597,16,40,30000,40500,281.6349465235367 -598,16,40,30000,41000,337.99683241119567 -599,16,40,30000,41500,389.68271710858414 -600,16,40,30500,38000,42.06569536892785 -601,16,40,30500,38500,62.95145274276575 -602,16,40,30500,39000,101.93860830594608 -603,16,40,30500,39500,150.47910837525734 -604,16,40,30500,40000,202.65388851823258 -605,16,40,30500,40500,254.5724108541227 -606,16,40,30500,41000,303.84403622726694 -607,16,40,30500,41500,349.1422884543064 -608,16,40,31000,38000,99.21707896667829 -609,16,40,31000,38500,112.24153596941301 -610,16,40,31000,39000,142.5186177618655 -611,16,40,31000,39500,182.02836955332134 -612,16,40,31000,40000,225.3201896575212 -613,16,40,31000,40500,268.83705389232614 -614,16,40,31000,41000,310.3895932135811 -615,16,40,31000,41500,348.7480165565453 -616,16,40,31500,38000,204.30418825821732 -617,16,40,31500,38500,210.0759235359138 -618,16,40,31500,39000,231.7643258544752 -619,16,40,31500,39500,262.1512494310348 -620,16,40,31500,40000,296.3864127264238 -621,16,40,31500,40500,331.30743171999035 -622,16,40,31500,41000,364.95322314895554 -623,16,40,31500,41500,396.20142191205844 -624,16,80,29000,38000,399.5975649320935 -625,16,80,29000,38500,225.6318269911425 -626,16,80,29000,39000,127.97354075513151 -627,16,80,29000,39500,93.73584101549991 -628,16,80,29000,40000,106.43084032022394 -629,16,80,29000,40500,150.51245762256931 -630,16,80,29000,41000,213.24213500046466 -631,16,80,29000,41500,285.0426423013882 -632,16,80,29500,38000,371.37706087096393 -633,16,80,29500,38500,189.77150413822454 -634,16,80,29500,39000,86.22375488959844 -635,16,80,29500,39500,46.98714814001572 -636,16,80,29500,40000,54.596900621760675 -637,16,80,29500,40500,93.12033833747024 -638,16,80,29500,41000,149.89341227947025 -639,16,80,29500,41500,215.5937000584367 -640,16,80,30000,38000,388.43657991253195 -641,16,80,30000,38500,190.77121362008674 -642,16,80,30000,39000,76.28535232335287 -643,16,80,30000,39500,29.152860363695716 -644,16,80,30000,40000,29.820972887404942 -645,16,80,30000,40500,61.320203047752464 -646,16,80,30000,41000,110.82086782062603 -647,16,80,30000,41500,169.197767615573 -648,16,80,30500,38000,458.8964339917103 -649,16,80,30500,38500,239.547928886725 -650,16,80,30500,39000,109.02338779317503 -651,16,80,30500,39500,50.888746196140914 -652,16,80,30500,40000,42.73606982375976 -653,16,80,30500,40500,65.75935122724029 -654,16,80,30500,41000,106.68884313872147 -655,16,80,30500,41500,156.54100549486617 -656,16,80,31000,38000,561.7385153195615 -657,16,80,31000,38500,335.5692026144635 -658,16,80,31000,39000,188.0383015831574 -659,16,80,31000,39500,118.2318539104416 -660,16,80,31000,40000,100.81000168801492 -661,16,80,31000,40500,114.72014539486217 -662,16,80,31000,41000,146.2992492326178 -663,16,80,31000,41500,186.8074429488408 -664,16,80,31500,38000,697.9937997454152 -665,16,80,31500,38500,466.42234442578484 -666,16,80,31500,39000,306.52125608515166 -667,16,80,31500,39500,230.54692639209762 -668,16,80,31500,40000,206.461121102699 -669,16,80,31500,40500,212.23429887269359 -670,16,80,31500,41000,234.70913795495554 -671,16,80,31500,41500,265.8143069252357 -672,16,120,29000,38000,1085.688903883652 -673,16,120,29000,38500,750.2887000017752 -674,16,120,29000,39000,469.92662852990964 -675,16,120,29000,39500,267.1560282754928 -676,16,120,29000,40000,146.06299930062625 -677,16,120,29000,40500,95.28836772053619 -678,16,120,29000,41000,97.41466545178946 -679,16,120,29000,41500,135.3804131941845 -680,16,120,29500,38000,1079.5576154477903 -681,16,120,29500,38500,751.2932384998761 -682,16,120,29500,39000,458.27083477307207 -683,16,120,29500,39500,240.9658024131812 -684,16,120,29500,40000,109.3801465044384 -685,16,120,29500,40500,51.274139057659724 -686,16,120,29500,41000,47.36446629605638 -687,16,120,29500,41500,79.42944320845996 -688,16,120,30000,38000,1139.3792936518537 -689,16,120,30000,38500,833.7979589668842 -690,16,120,30000,39000,507.805443202025 -691,16,120,30000,39500,259.93892964607977 -692,16,120,30000,40000,108.7341499557062 -693,16,120,30000,40500,38.152937143498605 -694,16,120,30000,41000,25.403985123518716 -695,16,120,30000,41500,49.72822589160786 -696,16,120,30500,38000,1285.0396277304772 -697,16,120,30500,38500,1025.254169031627 -698,16,120,30500,39000,622.5890550779666 -699,16,120,30500,39500,333.3353043756717 -700,16,120,30500,40000,155.70268128051293 -701,16,120,30500,40500,66.84125446522368 -702,16,120,30500,41000,42.25187049753978 -703,16,120,30500,41500,56.98314898830595 -704,16,120,31000,38000,1595.7993459811262 -705,16,120,31000,38500,1252.8886556470425 -706,16,120,31000,39000,731.4408383874198 -707,16,120,31000,39500,451.0090473423308 -708,16,120,31000,40000,251.5086563526081 -709,16,120,31000,40500,141.8915050063955 -710,16,120,31000,41000,104.67474675582574 -711,16,120,31000,41500,109.1609567535697 -712,16,120,31500,38000,1942.3896021770768 -713,16,120,31500,38500,1197.207050908449 -714,16,120,31500,39000,812.6818768064074 -715,16,120,31500,39500,611.45532452889 -716,16,120,31500,40000,380.63642711770643 -717,16,120,31500,40500,258.5514125337487 -718,16,120,31500,41000,213.48518421250665 -719,16,120,31500,41500,209.58134396574906 -720,19,40,29000,38000,169.3907733115706 -721,19,40,29000,38500,212.23331960093145 -722,19,40,29000,39000,275.9376503672959 -723,19,40,29000,39500,350.4301397081139 -724,19,40,29000,40000,428.40863665493924 -725,19,40,29000,40500,504.955113902399 -726,19,40,29000,41000,577.023450987656 -727,19,40,29000,41500,642.9410032211753 -728,19,40,29500,38000,102.40889356493292 -729,19,40,29500,38500,141.19036226103668 -730,19,40,29500,39000,200.19333708701748 -731,19,40,29500,39500,269.6750686488757 -732,19,40,29500,40000,342.6217886299377 -733,19,40,29500,40500,414.33044375626207 -734,19,40,29500,41000,481.89521316730713 -735,19,40,29500,41500,543.7211700546151 -736,19,40,30000,38000,51.95330426445395 -737,19,40,30000,38500,85.69656829127965 -738,19,40,30000,39000,138.98376466247876 -739,19,40,30000,39500,202.43251598105033 -740,19,40,30000,40000,269.3557903452929 -741,19,40,30000,40500,335.2960133312316 -742,19,40,30000,41000,397.50658847538665 -743,19,40,30000,41500,454.47903112410967 -744,19,40,30500,38000,28.864802790801026 -745,19,40,30500,38500,56.32899754732796 -746,19,40,30500,39000,102.69825523352162 -747,19,40,30500,39500,158.95118263535466 -748,19,40,30500,40000,218.75241957992617 -749,19,40,30500,40500,277.9122290233915 -750,19,40,30500,41000,333.8561815041273 -751,19,40,30500,41500,385.1662652901447 -752,19,40,31000,38000,43.72359701781447 -753,19,40,31000,38500,63.683967347844224 -754,19,40,31000,39000,101.95579433282329 -755,19,40,31000,39500,149.8826019475827 -756,19,40,31000,40000,201.50605279789198 -757,19,40,31000,40500,252.92391570754876 -758,19,40,31000,41000,301.7431453727685 -759,19,40,31000,41500,346.6368192781496 -760,19,40,31500,38000,104.05710998615942 -761,19,40,31500,38500,115.95783594434451 -762,19,40,31500,39000,145.42181873662554 -763,19,40,31500,39500,184.26373455825217 -764,19,40,31500,40000,226.97066340897095 -765,19,40,31500,40500,269.96403356902357 -766,19,40,31500,41000,311.04753558871505 -767,19,40,31500,41500,348.98866332680115 -768,19,80,29000,38000,453.1314944429312 -769,19,80,29000,38500,281.24067760117225 -770,19,80,29000,39000,185.83730378881882 -771,19,80,29000,39500,154.25726305915472 -772,19,80,29000,40000,170.2912737797755 -773,19,80,29000,40500,218.38979299191152 -774,19,80,29000,41000,285.604024444273 -775,19,80,29000,41500,362.0858325427657 -776,19,80,29500,38000,400.06299682217264 -777,19,80,29500,38500,224.41725666435008 -778,19,80,29500,39000,125.58476107530382 -779,19,80,29500,39500,90.55733834394478 -780,19,80,29500,40000,102.67519971027264 -781,19,80,29500,40500,146.27807815967392 -782,19,80,29500,41000,208.57372904155937 -783,19,80,29500,41500,279.9669583078214 -784,19,80,30000,38000,376.1594584816549 -785,19,80,30000,38500,191.30452808298463 -786,19,80,30000,39000,85.63116084217559 -787,19,80,30000,39500,45.10487847849711 -788,19,80,30000,40000,51.88389644342952 -789,19,80,30000,40500,89.78942817703852 -790,19,80,30000,41000,146.0393555385696 -791,19,80,30000,41500,211.26567367707352 -792,19,80,30500,38000,401.874315275947 -793,19,80,30500,38500,197.55305366608133 -794,19,80,30500,39000,79.00348967857379 -795,19,80,30500,39500,29.602719961568614 -796,19,80,30500,40000,28.980451378502487 -797,19,80,30500,40500,59.63541802023186 -798,19,80,30500,41000,108.48607655362268 -799,19,80,30500,41500,166.30589286399507 -800,19,80,31000,38000,484.930958445979 -801,19,80,31000,38500,254.27552635537404 -802,19,80,31000,39000,116.75543721560439 -803,19,80,31000,39500,54.77547840250418 -804,19,80,31000,40000,44.637472658824976 -805,19,80,31000,40500,66.50466903927668 -806,19,80,31000,41000,106.62737262508298 -807,19,80,31000,41500,155.8310688191254 -808,19,80,31500,38000,595.6094306603337 -809,19,80,31500,38500,359.60040819463063 -810,19,80,31500,39000,201.85328967228585 -811,19,80,31500,39500,126.24442464793601 -812,19,80,31500,40000,106.07388975142673 -813,19,80,31500,40500,118.52358345403363 -814,19,80,31500,41000,149.1597537162607 -815,19,80,31500,41500,188.94964975523197 -816,19,120,29000,38000,1133.9213841599772 -817,19,120,29000,38500,793.9759807804692 -818,19,120,29000,39000,516.5580425563733 -819,19,120,29000,39500,318.60172051726147 -820,19,120,29000,40000,201.662212274693 -821,19,120,29000,40500,154.47522945829064 -822,19,120,29000,41000,160.28049502033574 -823,19,120,29000,41500,202.35345983501588 -824,19,120,29500,38000,1091.6343400395158 -825,19,120,29500,38500,754.9332443184217 -826,19,120,29500,39000,472.1777992591152 -827,19,120,29500,39500,267.03951846894995 -828,19,120,29500,40000,144.25558152688114 -829,19,120,29500,40500,92.40384156679512 -830,19,120,29500,41000,93.81833253459942 -831,19,120,29500,41500,131.24753560710644 -832,19,120,30000,38000,1092.719296892266 -833,19,120,30000,38500,764.7065490850255 -834,19,120,30000,39000,467.2268758064373 -835,19,120,30000,39500,244.9367732985332 -836,19,120,30000,40000,110.00996333393202 -837,19,120,30000,40500,49.96381544207811 -838,19,120,30000,41000,44.9298739569088 -839,19,120,30000,41500,76.25447129089613 -840,19,120,30500,38000,1160.6160120981158 -841,19,120,30500,38500,865.5953188304933 -842,19,120,30500,39000,531.1657093741892 -843,19,120,30500,39500,271.98520008106277 -844,19,120,30500,40000,114.03616090967407 -845,19,120,30500,40500,39.74252227099571 -846,19,120,30500,41000,25.07176465285551 -847,19,120,30500,41500,48.298794094852724 -848,19,120,31000,38000,1304.8870694342509 -849,19,120,31000,38500,1089.6854636757826 -850,19,120,31000,39000,668.6632735260521 -851,19,120,31000,39500,356.7751012890747 -852,19,120,31000,40000,168.32491564142487 -853,19,120,31000,40500,72.82648063377391 -854,19,120,31000,41000,45.02326687759286 -855,19,120,31000,41500,58.13111530831655 -856,19,120,31500,38000,1645.2697164013964 -857,19,120,31500,38500,1373.859712069864 -858,19,120,31500,39000,787.3948673670299 -859,19,120,31500,39500,483.60546305948367 -860,19,120,31500,40000,273.4285373433001 -861,19,120,31500,40500,153.21079535396908 -862,19,120,31500,41000,111.21299419905313 -863,19,120,31500,41500,113.52006337929113 -864,22,40,29000,38000,229.2032513971666 -865,22,40,29000,38500,274.65023153674116 -866,22,40,29000,39000,341.4424739822062 -867,22,40,29000,39500,419.2624324130753 -868,22,40,29000,40000,500.6022690006133 -869,22,40,29000,40500,580.3923016374031 -870,22,40,29000,41000,655.4874207991389 -871,22,40,29000,41500,724.1595537770351 -872,22,40,29500,38000,155.45206306046595 -873,22,40,29500,38500,197.41588482427002 -874,22,40,29500,39000,260.1641484982308 -875,22,40,29500,39500,333.666918810689 -876,22,40,29500,40000,410.66541588422854 -877,22,40,29500,40500,486.276072112155 -878,22,40,29500,41000,557.4760464927683 -879,22,40,29500,41500,622.6057687448293 -880,22,40,30000,38000,90.70026588811803 -881,22,40,30000,38500,128.41239603755494 -882,22,40,30000,39000,186.27261386900233 -883,22,40,30000,39500,254.5802373859711 -884,22,40,30000,40000,326.3686182341553 -885,22,40,30000,40500,396.9735001502319 -886,22,40,30000,41000,463.5155278718613 -887,22,40,30000,41500,524.414569320113 -888,22,40,30500,38000,44.551475763397946 -889,22,40,30500,38500,76.95264448905411 -890,22,40,30500,39000,128.85898727872572 -891,22,40,30500,39500,190.91422001003792 -892,22,40,30500,40000,256.4755613806196 -893,22,40,30500,40500,321.125224208803 -894,22,40,30500,41000,382.14434919800453 -895,22,40,30500,41500,438.03974322333033 -896,22,40,31000,38000,28.101321546315717 -897,22,40,31000,38500,53.867829756398805 -898,22,40,31000,39000,98.57619184859544 -899,22,40,31000,39500,153.19473192134507 -900,22,40,31000,40000,211.4202434313414 -901,22,40,31000,40500,269.09905982026265 -902,22,40,31000,41000,323.68306330754416 -903,22,40,31000,41500,373.76836451736045 -904,22,40,31500,38000,51.648288279447364 -905,22,40,31500,38500,69.56074881661863 -906,22,40,31500,39000,105.91402675097291 -907,22,40,31500,39500,151.99456204656389 -908,22,40,31500,40000,201.85995274525234 -909,22,40,31500,40500,251.63807959916412 -910,22,40,31500,41000,298.9593498669657 -911,22,40,31500,41500,342.50888994628025 -912,22,80,29000,38000,507.5440336860194 -913,22,80,29000,38500,336.42019672232965 -914,22,80,29000,39000,242.21016116765423 -915,22,80,29000,39500,212.33396533224905 -916,22,80,29000,40000,230.67632355958136 -917,22,80,29000,40500,281.6224662955561 -918,22,80,29000,41000,352.0457411487133 -919,22,80,29000,41500,431.89288175778637 -920,22,80,29500,38000,443.2889283037078 -921,22,80,29500,38500,270.0648237630224 -922,22,80,29500,39000,173.57666711629645 -923,22,80,29500,39500,141.06258420240613 -924,22,80,29500,40000,156.18412870159142 -925,22,80,29500,40500,203.33105261575707 -926,22,80,29500,41000,269.5552387411201 -927,22,80,29500,41500,345.03801326123767 -928,22,80,30000,38000,395.34177505602497 -929,22,80,30000,38500,217.11094192826982 -930,22,80,30000,39000,116.38535634181476 -931,22,80,30000,39500,79.94742924888467 -932,22,80,30000,40000,90.84706550421288 -933,22,80,30000,40500,133.26308067939766 -934,22,80,30000,41000,194.36064414396228 -935,22,80,30000,41500,264.56059537656466 -936,22,80,30500,38000,382.0341866812038 -937,22,80,30500,38500,191.65621311671836 -938,22,80,30500,39000,82.3318677587146 -939,22,80,30500,39500,39.44606931321677 -940,22,80,30500,40000,44.476166488763134 -941,22,80,30500,40500,80.84561981845566 -942,22,80,30500,41000,135.62459431793735 -943,22,80,30500,41500,199.42208168600175 -944,22,80,31000,38000,425.5181957619983 -945,22,80,31000,38500,210.2667219741389 -946,22,80,31000,39000,84.97041062888985 -947,22,80,31000,39500,31.593073529038755 -948,22,80,31000,40000,28.407154164211214 -949,22,80,31000,40500,57.05446633976857 -950,22,80,31000,41000,104.10423883907688 -951,22,80,31000,41500,160.23135976433713 -952,22,80,31500,38000,527.5015417150911 -953,22,80,31500,38500,282.29650611769665 -954,22,80,31500,39000,134.62881845323489 -955,22,80,31500,39500,66.62736532046851 -956,22,80,31500,40000,52.9918858786988 -957,22,80,31500,40500,72.36913743145999 -958,22,80,31500,41000,110.38003828747726 -959,22,80,31500,41500,157.65470091455973 -960,22,120,29000,38000,1186.823326813257 -961,22,120,29000,38500,844.3317816964005 -962,22,120,29000,39000,567.7367986440256 -963,22,120,29000,39500,371.79782508970567 -964,22,120,29000,40000,256.9261857702517 -965,22,120,29000,40500,211.85466060592006 -966,22,120,29000,41000,220.09534855737033 -967,22,120,29000,41500,265.02731793490034 -968,22,120,29500,38000,1128.4568915685559 -969,22,120,29500,38500,787.7709648712951 -970,22,120,29500,39000,508.4832626962424 -971,22,120,29500,39500,308.52654841064975 -972,22,120,29500,40000,190.01030358402707 -973,22,120,29500,40500,141.62663282114926 -974,22,120,29500,41000,146.40704203984612 -975,22,120,29500,41500,187.48734389188584 -976,22,120,30000,38000,1094.7007205604846 -977,22,120,30000,38500,757.7313528729464 -978,22,120,30000,39000,471.282561364766 -979,22,120,30000,39500,262.0412520036699 -980,22,120,30000,40000,136.26956239282435 -981,22,120,30000,40500,82.4268827471484 -982,22,120,30000,41000,82.3695177584498 -983,22,120,30000,41500,118.51210034475737 -984,22,120,30500,38000,1111.0872182758205 -985,22,120,30500,38500,787.2204655558988 -986,22,120,30500,39000,481.85960605002055 -987,22,120,30500,39500,250.28740868446397 -988,22,120,30500,40000,109.21968920710272 -989,22,120,30500,40500,45.51600269221681 -990,22,120,30500,41000,38.172157811051115 -991,22,120,30500,41500,67.73748641348168 -992,22,120,31000,38000,1193.3958874354898 -993,22,120,31000,38500,923.0731791194576 -994,22,120,31000,39000,573.4457650536078 -995,22,120,31000,39500,294.2980811757103 -996,22,120,31000,40000,124.86249624679849 -997,22,120,31000,40500,43.948524347749846 -998,22,120,31000,41000,25.582084045731808 -999,22,120,31000,41500,46.36268252714472 -1000,22,120,31500,38000,1336.0993444856913 -1001,22,120,31500,38500,1194.893001664831 -1002,22,120,31500,39000,740.6584250286721 -1003,22,120,31500,39500,397.18127104230757 -1004,22,120,31500,40000,194.20390582893873 -1005,22,120,31500,40500,88.22588964369922 -1006,22,120,31500,41000,54.97797247760634 -1007,22,120,31500,41500,64.88195101638016 diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/parallel_example.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/parallel_example.py deleted file mode 100644 index ff1287811cf..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/parallel_example.py +++ /dev/null @@ -1,57 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -""" -The following script can be used to run semibatch parameter estimation in -parallel and save results to files for later analysis and graphics. -Example command: mpiexec -n 4 python parallel_example.py -""" -import numpy as np -import pandas as pd -from itertools import product -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model - - -def main(): - # Vars to estimate - theta_names = ['k1', 'k2', 'E1', 'E2'] - - # Data, list of json file names - data = [] - file_dirname = dirname(abspath(str(__file__))) - for exp_num in range(10): - file_name = abspath(join(file_dirname, 'exp' + str(exp_num + 1) + '.out')) - data.append(file_name) - - # Note, the model already includes a 'SecondStageCost' expression - # for sum of squared error that will be used in parameter estimation - - pest = parmest.Estimator(generate_model, data, theta_names) - - ### Parameter estimation with bootstrap resampling - bootstrap_theta = pest.theta_est_bootstrap(100) - bootstrap_theta.to_csv('bootstrap_theta.csv') - - ### Compute objective at theta for likelihood ratio test - k1 = np.arange(4, 24, 3) - k2 = np.arange(40, 160, 40) - E1 = np.arange(29000, 32000, 500) - E2 = np.arange(38000, 42000, 500) - theta_vals = pd.DataFrame(list(product(k1, k2, E1, E2)), columns=theta_names) - - obj_at_theta = pest.objective_at_theta(theta_vals) - obj_at_theta.to_csv('obj_at_theta.csv') - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/parameter_estimation_example.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/parameter_estimation_example.py deleted file mode 100644 index fc4c9f5c675..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/parameter_estimation_example.py +++ /dev/null @@ -1,42 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import json -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model - - -def main(): - # Vars to estimate - theta_names = ['k1', 'k2', 'E1', 'E2'] - - # Data, list of dictionaries - data = [] - file_dirname = dirname(abspath(str(__file__))) - for exp_num in range(10): - file_name = abspath(join(file_dirname, 'exp' + str(exp_num + 1) + '.out')) - with open(file_name, 'r') as infile: - d = json.load(infile) - data.append(d) - - # Note, the model already includes a 'SecondStageCost' expression - # for sum of squared error that will be used in parameter estimation - - pest = parmest.Estimator(generate_model, data, theta_names) - - obj, theta = pest.theta_est() - print(obj) - print(theta) - - -if __name__ == '__main__': - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/scenario_example.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/scenario_example.py deleted file mode 100644 index 071e53236c4..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/scenario_example.py +++ /dev/null @@ -1,52 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import json -from os.path import join, abspath, dirname -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.examples.semibatch.semibatch import generate_model -import pyomo.contrib.parmest.scenariocreator as sc - - -def main(): - # Vars to estimate in parmest - theta_names = ['k1', 'k2', 'E1', 'E2'] - - # Data: list of dictionaries - data = [] - file_dirname = dirname(abspath(str(__file__))) - for exp_num in range(10): - fname = join(file_dirname, 'exp' + str(exp_num + 1) + '.out') - with open(fname, 'r') as infile: - d = json.load(infile) - data.append(d) - - pest = parmest.Estimator(generate_model, data, theta_names) - - scenmaker = sc.ScenarioCreator(pest, "ipopt") - - # Make one scenario per experiment and write to a csv file - output_file = "scenarios.csv" - experimentscens = sc.ScenarioSet("Experiments") - scenmaker.ScenariosFromExperiments(experimentscens) - experimentscens.write_csv(output_file) - - # Use the bootstrap to make 3 scenarios and print - bootscens = sc.ScenarioSet("Bootstrap") - scenmaker.ScenariosFromBootstrap(bootscens, 3) - for s in bootscens.ScensIterator(): - print("{}, {}".format(s.name, s.probability)) - for n, v in s.ThetaVals.items(): - print(" {}={}".format(n, v)) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/scenarios.csv b/pyomo/contrib/parmest/deprecated/examples/semibatch/scenarios.csv deleted file mode 100644 index 22f9a651bc3..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/scenarios.csv +++ /dev/null @@ -1,11 +0,0 @@ -Name,Probability,k1,k2,E1,E2 -ExpScen0,0.1,25.800350800448314,14.14421520525348,31505.74905064048,35000.0 -ExpScen1,0.1,25.128373083865036,149.99999951481198,31452.336651974012,41938.781301641866 -ExpScen2,0.1,22.225574065344002,130.92739780265404,30948.669111672247,41260.15420929141 -ExpScen3,0.1,100.0,149.99999970011854,35182.73130744844,41444.52600373733 -ExpScen4,0.1,82.99114366189944,45.95424665995078,34810.857217141674,38300.633349887314 -ExpScen5,0.1,100.0,150.0,35142.20219150486,41495.41105795494 -ExpScen6,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 -ExpScen7,0.1,2.754580914035567,14.381786096822475,25000.0,35000.0 -ExpScen8,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 -ExpScen9,0.1,2.669780822294865,150.0,25000.0,41514.7476113499 diff --git a/pyomo/contrib/parmest/deprecated/examples/semibatch/semibatch.py b/pyomo/contrib/parmest/deprecated/examples/semibatch/semibatch.py deleted file mode 100644 index 6762531a338..00000000000 --- a/pyomo/contrib/parmest/deprecated/examples/semibatch/semibatch.py +++ /dev/null @@ -1,287 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ -""" -Semibatch model, based on Nicholson et al. (2018). pyomo.dae: A modeling and -automatic discretization framework for optimization with di -erential and -algebraic equations. Mathematical Programming Computation, 10(2), 187-223. -""" -import json -from os.path import join, abspath, dirname -from pyomo.environ import ( - ConcreteModel, - Set, - Param, - Var, - Constraint, - ConstraintList, - Expression, - Objective, - TransformationFactory, - SolverFactory, - exp, - minimize, -) -from pyomo.dae import ContinuousSet, DerivativeVar - - -def generate_model(data): - # if data is a file name, then load file first - if isinstance(data, str): - file_name = data - try: - with open(file_name, "r") as infile: - data = json.load(infile) - except: - raise RuntimeError(f"Could not read {file_name} as json") - - # unpack and fix the data - cameastemp = data["Ca_meas"] - cbmeastemp = data["Cb_meas"] - ccmeastemp = data["Cc_meas"] - trmeastemp = data["Tr_meas"] - - cameas = {} - cbmeas = {} - ccmeas = {} - trmeas = {} - for i in cameastemp.keys(): - cameas[float(i)] = cameastemp[i] - cbmeas[float(i)] = cbmeastemp[i] - ccmeas[float(i)] = ccmeastemp[i] - trmeas[float(i)] = trmeastemp[i] - - m = ConcreteModel() - - # - # Measurement Data - # - m.measT = Set(initialize=sorted(cameas.keys())) - m.Ca_meas = Param(m.measT, initialize=cameas) - m.Cb_meas = Param(m.measT, initialize=cbmeas) - m.Cc_meas = Param(m.measT, initialize=ccmeas) - m.Tr_meas = Param(m.measT, initialize=trmeas) - - # - # Parameters for semi-batch reactor model - # - m.R = Param(initialize=8.314) # kJ/kmol/K - m.Mwa = Param(initialize=50.0) # kg/kmol - m.rhor = Param(initialize=1000.0) # kg/m^3 - m.cpr = Param(initialize=3.9) # kJ/kg/K - m.Tf = Param(initialize=300) # K - m.deltaH1 = Param(initialize=-40000.0) # kJ/kmol - m.deltaH2 = Param(initialize=-50000.0) # kJ/kmol - m.alphaj = Param(initialize=0.8) # kJ/s/m^2/K - m.alphac = Param(initialize=0.7) # kJ/s/m^2/K - m.Aj = Param(initialize=5.0) # m^2 - m.Ac = Param(initialize=3.0) # m^2 - m.Vj = Param(initialize=0.9) # m^3 - m.Vc = Param(initialize=0.07) # m^3 - m.rhow = Param(initialize=700.0) # kg/m^3 - m.cpw = Param(initialize=3.1) # kJ/kg/K - m.Ca0 = Param(initialize=data["Ca0"]) # kmol/m^3) - m.Cb0 = Param(initialize=data["Cb0"]) # kmol/m^3) - m.Cc0 = Param(initialize=data["Cc0"]) # kmol/m^3) - m.Tr0 = Param(initialize=300.0) # K - m.Vr0 = Param(initialize=1.0) # m^3 - - m.time = ContinuousSet(bounds=(0, 21600), initialize=m.measT) # Time in seconds - - # - # Control Inputs - # - def _initTc(m, t): - if t < 10800: - return data["Tc1"] - else: - return data["Tc2"] - - m.Tc = Param( - m.time, initialize=_initTc, default=_initTc - ) # bounds= (288,432) Cooling coil temp, control input - - def _initFa(m, t): - if t < 10800: - return data["Fa1"] - else: - return data["Fa2"] - - m.Fa = Param( - m.time, initialize=_initFa, default=_initFa - ) # bounds=(0,0.05) Inlet flow rate, control input - - # - # Parameters being estimated - # - m.k1 = Var(initialize=14, bounds=(2, 100)) # 1/s Actual: 15.01 - m.k2 = Var(initialize=90, bounds=(2, 150)) # 1/s Actual: 85.01 - m.E1 = Var(initialize=27000.0, bounds=(25000, 40000)) # kJ/kmol Actual: 30000 - m.E2 = Var(initialize=45000.0, bounds=(35000, 50000)) # kJ/kmol Actual: 40000 - # m.E1.fix(30000) - # m.E2.fix(40000) - - # - # Time dependent variables - # - m.Ca = Var(m.time, initialize=m.Ca0, bounds=(0, 25)) - m.Cb = Var(m.time, initialize=m.Cb0, bounds=(0, 25)) - m.Cc = Var(m.time, initialize=m.Cc0, bounds=(0, 25)) - m.Vr = Var(m.time, initialize=m.Vr0) - m.Tr = Var(m.time, initialize=m.Tr0) - m.Tj = Var( - m.time, initialize=310.0, bounds=(288, None) - ) # Cooling jacket temp, follows coil temp until failure - - # - # Derivatives in the model - # - m.dCa = DerivativeVar(m.Ca) - m.dCb = DerivativeVar(m.Cb) - m.dCc = DerivativeVar(m.Cc) - m.dVr = DerivativeVar(m.Vr) - m.dTr = DerivativeVar(m.Tr) - - # - # Differential Equations in the model - # - - def _dCacon(m, t): - if t == 0: - return Constraint.Skip - return ( - m.dCa[t] - == m.Fa[t] / m.Vr[t] - m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] - ) - - m.dCacon = Constraint(m.time, rule=_dCacon) - - def _dCbcon(m, t): - if t == 0: - return Constraint.Skip - return ( - m.dCb[t] - == m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] - - m.k2 * exp(-m.E2 / (m.R * m.Tr[t])) * m.Cb[t] - ) - - m.dCbcon = Constraint(m.time, rule=_dCbcon) - - def _dCccon(m, t): - if t == 0: - return Constraint.Skip - return m.dCc[t] == m.k2 * exp(-m.E2 / (m.R * m.Tr[t])) * m.Cb[t] - - m.dCccon = Constraint(m.time, rule=_dCccon) - - def _dVrcon(m, t): - if t == 0: - return Constraint.Skip - return m.dVr[t] == m.Fa[t] * m.Mwa / m.rhor - - m.dVrcon = Constraint(m.time, rule=_dVrcon) - - def _dTrcon(m, t): - if t == 0: - return Constraint.Skip - return m.rhor * m.cpr * m.dTr[t] == m.Fa[t] * m.Mwa * m.cpr / m.Vr[t] * ( - m.Tf - m.Tr[t] - ) - m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] * m.deltaH1 - m.k2 * exp( - -m.E2 / (m.R * m.Tr[t]) - ) * m.Cb[ - t - ] * m.deltaH2 + m.alphaj * m.Aj / m.Vr0 * ( - m.Tj[t] - m.Tr[t] - ) + m.alphac * m.Ac / m.Vr0 * ( - m.Tc[t] - m.Tr[t] - ) - - m.dTrcon = Constraint(m.time, rule=_dTrcon) - - def _singlecooling(m, t): - return m.Tc[t] == m.Tj[t] - - m.singlecooling = Constraint(m.time, rule=_singlecooling) - - # Initial Conditions - def _initcon(m): - yield m.Ca[m.time.first()] == m.Ca0 - yield m.Cb[m.time.first()] == m.Cb0 - yield m.Cc[m.time.first()] == m.Cc0 - yield m.Vr[m.time.first()] == m.Vr0 - yield m.Tr[m.time.first()] == m.Tr0 - - m.initcon = ConstraintList(rule=_initcon) - - # - # Stage-specific cost computations - # - def ComputeFirstStageCost_rule(model): - return 0 - - m.FirstStageCost = Expression(rule=ComputeFirstStageCost_rule) - - def AllMeasurements(m): - return sum( - (m.Ca[t] - m.Ca_meas[t]) ** 2 - + (m.Cb[t] - m.Cb_meas[t]) ** 2 - + (m.Cc[t] - m.Cc_meas[t]) ** 2 - + 0.01 * (m.Tr[t] - m.Tr_meas[t]) ** 2 - for t in m.measT - ) - - def MissingMeasurements(m): - if data["experiment"] == 1: - return sum( - (m.Ca[t] - m.Ca_meas[t]) ** 2 - + (m.Cb[t] - m.Cb_meas[t]) ** 2 - + (m.Cc[t] - m.Cc_meas[t]) ** 2 - + (m.Tr[t] - m.Tr_meas[t]) ** 2 - for t in m.measT - ) - elif data["experiment"] == 2: - return sum((m.Tr[t] - m.Tr_meas[t]) ** 2 for t in m.measT) - else: - return sum( - (m.Cb[t] - m.Cb_meas[t]) ** 2 + (m.Tr[t] - m.Tr_meas[t]) ** 2 - for t in m.measT - ) - - m.SecondStageCost = Expression(rule=MissingMeasurements) - - def total_cost_rule(model): - return model.FirstStageCost + model.SecondStageCost - - m.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) - - # Discretize model - disc = TransformationFactory("dae.collocation") - disc.apply_to(m, nfe=20, ncp=4) - return m - - -def main(): - # Data loaded from files - file_dirname = dirname(abspath(str(__file__))) - file_name = abspath(join(file_dirname, "exp2.out")) - with open(file_name, "r") as infile: - data = json.load(infile) - data["experiment"] = 2 - - model = generate_model(data) - solver = SolverFactory("ipopt") - solver.solve(model) - print("k1 = ", model.k1()) - print("E1 = ", model.E1()) - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/parmest/deprecated/parmest.py b/pyomo/contrib/parmest/deprecated/parmest.py deleted file mode 100644 index 82bf893dd06..00000000000 --- a/pyomo/contrib/parmest/deprecated/parmest.py +++ /dev/null @@ -1,1361 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ -#### Using mpi-sppy instead of PySP; May 2020 -#### Adding option for "local" EF starting Sept 2020 -#### Wrapping mpi-sppy functionality and local option Jan 2021, Feb 2021 - -# TODO: move use_mpisppy to a Pyomo configuration option -# -# False implies always use the EF that is local to parmest -use_mpisppy = True # Use it if we can but use local if not. -if use_mpisppy: - try: - # MPI-SPPY has an unfortunate side effect of outputting - # "[ 0.00] Initializing mpi-sppy" when it is imported. This can - # cause things like doctests to fail. We will suppress that - # information here. - from pyomo.common.tee import capture_output - - with capture_output(): - import mpisppy.utils.sputils as sputils - except ImportError: - use_mpisppy = False # we can't use it -if use_mpisppy: - # These things should be outside the try block. - sputils.disable_tictoc_output() - import mpisppy.opt.ef as st - import mpisppy.scenario_tree as scenario_tree -else: - import pyomo.contrib.parmest.utils.create_ef as local_ef - import pyomo.contrib.parmest.utils.scenario_tree as scenario_tree - -import re -import importlib as im -import logging -import types -import json -from itertools import combinations - -from pyomo.common.dependencies import ( - attempt_import, - numpy as np, - numpy_available, - pandas as pd, - pandas_available, - scipy, - scipy_available, -) - -import pyomo.environ as pyo - -from pyomo.opt import SolverFactory -from pyomo.environ import Block, ComponentUID - -import pyomo.contrib.parmest.utils as utils -import pyomo.contrib.parmest.graphics as graphics -from pyomo.dae import ContinuousSet - -parmest_available = numpy_available & pandas_available & scipy_available - -inverse_reduced_hessian, inverse_reduced_hessian_available = attempt_import( - 'pyomo.contrib.interior_point.inverse_reduced_hessian' -) - -logger = logging.getLogger(__name__) - - -def ef_nonants(ef): - # Wrapper to call someone's ef_nonants - # (the function being called is very short, but it might be changed) - if use_mpisppy: - return sputils.ef_nonants(ef) - else: - return local_ef.ef_nonants(ef) - - -def _experiment_instance_creation_callback( - scenario_name, node_names=None, cb_data=None -): - """ - This is going to be called by mpi-sppy or the local EF and it will call into - the user's model's callback. - - Parameters: - ----------- - scenario_name: `str` Scenario name should end with a number - node_names: `None` ( Not used here ) - cb_data : dict with ["callback"], ["BootList"], - ["theta_names"], ["cb_data"], etc. - "cb_data" is passed through to user's callback function - that is the "callback" value. - "BootList" is None or bootstrap experiment number list. - (called cb_data by mpisppy) - - - Returns: - -------- - instance: `ConcreteModel` - instantiated scenario - - Note: - ---- - There is flexibility both in how the function is passed and its signature. - """ - assert cb_data is not None - outer_cb_data = cb_data - scen_num_str = re.compile(r'(\d+)$').search(scenario_name).group(1) - scen_num = int(scen_num_str) - basename = scenario_name[: -len(scen_num_str)] # to reconstruct name - - CallbackFunction = outer_cb_data["callback"] - - if callable(CallbackFunction): - callback = CallbackFunction - else: - cb_name = CallbackFunction - - if "CallbackModule" not in outer_cb_data: - raise RuntimeError( - "Internal Error: need CallbackModule in parmest callback" - ) - else: - modname = outer_cb_data["CallbackModule"] - - if isinstance(modname, str): - cb_module = im.import_module(modname, package=None) - elif isinstance(modname, types.ModuleType): - cb_module = modname - else: - print("Internal Error: bad CallbackModule") - raise - - try: - callback = getattr(cb_module, cb_name) - except: - print("Error getting function=" + cb_name + " from module=" + str(modname)) - raise - - if "BootList" in outer_cb_data: - bootlist = outer_cb_data["BootList"] - # print("debug in callback: using bootlist=",str(bootlist)) - # assuming bootlist itself is zero based - exp_num = bootlist[scen_num] - else: - exp_num = scen_num - - scen_name = basename + str(exp_num) - - cb_data = outer_cb_data["cb_data"] # cb_data might be None. - - # at least three signatures are supported. The first is preferred - try: - instance = callback(experiment_number=exp_num, cb_data=cb_data) - except TypeError: - raise RuntimeError( - "Only one callback signature is supported: " - "callback(experiment_number, cb_data) " - ) - """ - try: - instance = callback(scenario_tree_model, scen_name, node_names) - except TypeError: # deprecated signature? - try: - instance = callback(scen_name, node_names) - except: - print("Failed to create instance using callback; TypeError+") - raise - except: - print("Failed to create instance using callback.") - raise - """ - if hasattr(instance, "_mpisppy_node_list"): - raise RuntimeError(f"scenario for experiment {exp_num} has _mpisppy_node_list") - nonant_list = [ - instance.find_component(vstr) for vstr in outer_cb_data["theta_names"] - ] - if use_mpisppy: - instance._mpisppy_node_list = [ - scenario_tree.ScenarioNode( - name="ROOT", - cond_prob=1.0, - stage=1, - cost_expression=instance.FirstStageCost, - nonant_list=nonant_list, - scen_model=instance, - ) - ] - else: - instance._mpisppy_node_list = [ - scenario_tree.ScenarioNode( - name="ROOT", - cond_prob=1.0, - stage=1, - cost_expression=instance.FirstStageCost, - scen_name_list=None, - nonant_list=nonant_list, - scen_model=instance, - ) - ] - - if "ThetaVals" in outer_cb_data: - thetavals = outer_cb_data["ThetaVals"] - - # dlw august 2018: see mea code for more general theta - for vstr in thetavals: - theta_cuid = ComponentUID(vstr) - theta_object = theta_cuid.find_component_on(instance) - if thetavals[vstr] is not None: - # print("Fixing",vstr,"at",str(thetavals[vstr])) - theta_object.fix(thetavals[vstr]) - else: - # print("Freeing",vstr) - theta_object.unfix() - - return instance - - -# ============================================= -def _treemaker(scenlist): - """ - Makes a scenario tree (avoids dependence on daps) - - Parameters - ---------- - scenlist (list of `int`): experiment (i.e. scenario) numbers - - Returns - ------- - a `ConcreteModel` that is the scenario tree - """ - - num_scenarios = len(scenlist) - m = scenario_tree.tree_structure_model.CreateAbstractScenarioTreeModel() - m = m.create_instance() - m.Stages.add('Stage1') - m.Stages.add('Stage2') - m.Nodes.add('RootNode') - for i in scenlist: - m.Nodes.add('LeafNode_Experiment' + str(i)) - m.Scenarios.add('Experiment' + str(i)) - m.NodeStage['RootNode'] = 'Stage1' - m.ConditionalProbability['RootNode'] = 1.0 - for node in m.Nodes: - if node != 'RootNode': - m.NodeStage[node] = 'Stage2' - m.Children['RootNode'].add(node) - m.Children[node].clear() - m.ConditionalProbability[node] = 1.0 / num_scenarios - m.ScenarioLeafNode[node.replace('LeafNode_', '')] = node - - return m - - -def group_data(data, groupby_column_name, use_mean=None): - """ - Group data by scenario - - Parameters - ---------- - data: DataFrame - Data - groupby_column_name: strings - Name of data column which contains scenario numbers - use_mean: list of column names or None, optional - Name of data columns which should be reduced to a single value per - scenario by taking the mean - - Returns - ---------- - grouped_data: list of dictionaries - Grouped data - """ - if use_mean is None: - use_mean_list = [] - else: - use_mean_list = use_mean - - grouped_data = [] - for exp_num, group in data.groupby(data[groupby_column_name]): - d = {} - for col in group.columns: - if col in use_mean_list: - d[col] = group[col].mean() - else: - d[col] = list(group[col]) - grouped_data.append(d) - - return grouped_data - - -class _SecondStageCostExpr(object): - """ - Class to pass objective expression into the Pyomo model - """ - - def __init__(self, ssc_function, data): - self._ssc_function = ssc_function - self._data = data - - def __call__(self, model): - return self._ssc_function(model, self._data) - - -class Estimator(object): - """ - Parameter estimation class - - Parameters - ---------- - model_function: function - Function that generates an instance of the Pyomo model using 'data' - as the input argument - data: pd.DataFrame, list of dictionaries, list of dataframes, or list of json file names - Data that is used to build an instance of the Pyomo model and build - the objective function - theta_names: list of strings - List of Var names to estimate - obj_function: function, optional - Function used to formulate parameter estimation objective, generally - sum of squared error between measurements and model variables. - If no function is specified, the model is used - "as is" and should be defined with a "FirstStageCost" and - "SecondStageCost" expression that are used to build an objective. - tee: bool, optional - Indicates that ef solver output should be teed - diagnostic_mode: bool, optional - If True, print diagnostics from the solver - solver_options: dict, optional - Provides options to the solver (also the name of an attribute) - """ - - def __init__( - self, - model_function, - data, - theta_names, - obj_function=None, - tee=False, - diagnostic_mode=False, - solver_options=None, - ): - self.model_function = model_function - - assert isinstance( - data, (list, pd.DataFrame) - ), "Data must be a list or DataFrame" - # convert dataframe into a list of dataframes, each row = one scenario - if isinstance(data, pd.DataFrame): - self.callback_data = [ - data.loc[i, :].to_frame().transpose() for i in data.index - ] - else: - self.callback_data = data - assert isinstance( - self.callback_data[0], (dict, pd.DataFrame, str) - ), "The scenarios in data must be a dictionary, DataFrame or filename" - - if len(theta_names) == 0: - self.theta_names = ['parmest_dummy_var'] - else: - self.theta_names = theta_names - - self.obj_function = obj_function - self.tee = tee - self.diagnostic_mode = diagnostic_mode - self.solver_options = solver_options - - self._second_stage_cost_exp = "SecondStageCost" - # boolean to indicate if model is initialized using a square solve - self.model_initialized = False - - def _return_theta_names(self): - """ - Return list of fitted model parameter names - """ - # if fitted model parameter names differ from theta_names created when Estimator object is created - if hasattr(self, 'theta_names_updated'): - return self.theta_names_updated - - else: - return ( - self.theta_names - ) # default theta_names, created when Estimator object is created - - def _create_parmest_model(self, data): - """ - Modify the Pyomo model for parameter estimation - """ - model = self.model_function(data) - - if (len(self.theta_names) == 1) and ( - self.theta_names[0] == 'parmest_dummy_var' - ): - model.parmest_dummy_var = pyo.Var(initialize=1.0) - - # Add objective function (optional) - if self.obj_function: - for obj in model.component_objects(pyo.Objective): - if obj.name in ["Total_Cost_Objective"]: - raise RuntimeError( - "Parmest will not override the existing model Objective named " - + obj.name - ) - obj.deactivate() - - for expr in model.component_data_objects(pyo.Expression): - if expr.name in ["FirstStageCost", "SecondStageCost"]: - raise RuntimeError( - "Parmest will not override the existing model Expression named " - + expr.name - ) - model.FirstStageCost = pyo.Expression(expr=0) - model.SecondStageCost = pyo.Expression( - rule=_SecondStageCostExpr(self.obj_function, data) - ) - - def TotalCost_rule(model): - return model.FirstStageCost + model.SecondStageCost - - model.Total_Cost_Objective = pyo.Objective( - rule=TotalCost_rule, sense=pyo.minimize - ) - - # Convert theta Params to Vars, and unfix theta Vars - model = utils.convert_params_to_vars(model, self.theta_names) - - # Update theta names list to use CUID string representation - for i, theta in enumerate(self.theta_names): - var_cuid = ComponentUID(theta) - var_validate = var_cuid.find_component_on(model) - if var_validate is None: - logger.warning( - "theta_name[%s] (%s) was not found on the model", (i, theta) - ) - else: - try: - # If the component is not a variable, - # this will generate an exception (and the warning - # in the 'except') - var_validate.unfix() - self.theta_names[i] = repr(var_cuid) - except: - logger.warning(theta + ' is not a variable') - - self.parmest_model = model - - return model - - def _instance_creation_callback(self, experiment_number=None, cb_data=None): - # cb_data is a list of dictionaries, list of dataframes, OR list of json file names - exp_data = cb_data[experiment_number] - if isinstance(exp_data, (dict, pd.DataFrame)): - pass - elif isinstance(exp_data, str): - try: - with open(exp_data, 'r') as infile: - exp_data = json.load(infile) - except: - raise RuntimeError(f'Could not read {exp_data} as json') - else: - raise RuntimeError(f'Unexpected data format for cb_data={cb_data}') - model = self._create_parmest_model(exp_data) - - return model - - def _Q_opt( - self, - ThetaVals=None, - solver="ef_ipopt", - return_values=[], - bootlist=None, - calc_cov=False, - cov_n=None, - ): - """ - Set up all thetas as first stage Vars, return resulting theta - values as well as the objective function value. - - """ - if solver == "k_aug": - raise RuntimeError("k_aug no longer supported.") - - # (Bootstrap scenarios will use indirection through the bootlist) - if bootlist is None: - scenario_numbers = list(range(len(self.callback_data))) - scen_names = ["Scenario{}".format(i) for i in scenario_numbers] - else: - scen_names = ["Scenario{}".format(i) for i in range(len(bootlist))] - - # tree_model.CallbackModule = None - outer_cb_data = dict() - outer_cb_data["callback"] = self._instance_creation_callback - if ThetaVals is not None: - outer_cb_data["ThetaVals"] = ThetaVals - if bootlist is not None: - outer_cb_data["BootList"] = bootlist - outer_cb_data["cb_data"] = self.callback_data # None is OK - outer_cb_data["theta_names"] = self.theta_names - - options = {"solver": "ipopt"} - scenario_creator_options = {"cb_data": outer_cb_data} - if use_mpisppy: - ef = sputils.create_EF( - scen_names, - _experiment_instance_creation_callback, - EF_name="_Q_opt", - suppress_warnings=True, - scenario_creator_kwargs=scenario_creator_options, - ) - else: - ef = local_ef.create_EF( - scen_names, - _experiment_instance_creation_callback, - EF_name="_Q_opt", - suppress_warnings=True, - scenario_creator_kwargs=scenario_creator_options, - ) - self.ef_instance = ef - - # Solve the extensive form with ipopt - if solver == "ef_ipopt": - if not calc_cov: - # Do not calculate the reduced hessian - - solver = SolverFactory('ipopt') - if self.solver_options is not None: - for key in self.solver_options: - solver.options[key] = self.solver_options[key] - - solve_result = solver.solve(self.ef_instance, tee=self.tee) - - # The import error will be raised when we attempt to use - # inv_reduced_hessian_barrier below. - # - # elif not asl_available: - # raise ImportError("parmest requires ASL to calculate the " - # "covariance matrix with solver 'ipopt'") - else: - # parmest makes the fitted parameters stage 1 variables - ind_vars = [] - for ndname, Var, solval in ef_nonants(ef): - ind_vars.append(Var) - # calculate the reduced hessian - (solve_result, inv_red_hes) = ( - inverse_reduced_hessian.inv_reduced_hessian_barrier( - self.ef_instance, - independent_variables=ind_vars, - solver_options=self.solver_options, - tee=self.tee, - ) - ) - - if self.diagnostic_mode: - print( - ' Solver termination condition = ', - str(solve_result.solver.termination_condition), - ) - - # assume all first stage are thetas... - thetavals = {} - for ndname, Var, solval in ef_nonants(ef): - # process the name - # the scenarios are blocks, so strip the scenario name - vname = Var.name[Var.name.find(".") + 1 :] - thetavals[vname] = solval - - objval = pyo.value(ef.EF_Obj) - - if calc_cov: - # Calculate the covariance matrix - - # Number of data points considered - n = cov_n - - # Extract number of fitted parameters - l = len(thetavals) - - # Assumption: Objective value is sum of squared errors - sse = objval - - '''Calculate covariance assuming experimental observation errors are - independent and follow a Gaussian - distribution with constant variance. - - The formula used in parmest was verified against equations (7-5-15) and - (7-5-16) in "Nonlinear Parameter Estimation", Y. Bard, 1974. - - This formula is also applicable if the objective is scaled by a constant; - the constant cancels out. (was scaled by 1/n because it computes an - expected value.) - ''' - cov = 2 * sse / (n - l) * inv_red_hes - cov = pd.DataFrame( - cov, index=thetavals.keys(), columns=thetavals.keys() - ) - - thetavals = pd.Series(thetavals) - - if len(return_values) > 0: - var_values = [] - if len(scen_names) > 1: # multiple scenarios - block_objects = self.ef_instance.component_objects( - Block, descend_into=False - ) - else: # single scenario - block_objects = [self.ef_instance] - for exp_i in block_objects: - vals = {} - for var in return_values: - exp_i_var = exp_i.find_component(str(var)) - if ( - exp_i_var is None - ): # we might have a block such as _mpisppy_data - continue - # if value to return is ContinuousSet - if type(exp_i_var) == ContinuousSet: - temp = list(exp_i_var) - else: - temp = [pyo.value(_) for _ in exp_i_var.values()] - if len(temp) == 1: - vals[var] = temp[0] - else: - vals[var] = temp - if len(vals) > 0: - var_values.append(vals) - var_values = pd.DataFrame(var_values) - if calc_cov: - return objval, thetavals, var_values, cov - else: - return objval, thetavals, var_values - - if calc_cov: - return objval, thetavals, cov - else: - return objval, thetavals - - else: - raise RuntimeError("Unknown solver in Q_Opt=" + solver) - - def _Q_at_theta(self, thetavals, initialize_parmest_model=False): - """ - Return the objective function value with fixed theta values. - - Parameters - ---------- - thetavals: dict - A dictionary of theta values. - - initialize_parmest_model: boolean - If True: Solve square problem instance, build extensive form of the model for - parameter estimation, and set flag model_initialized to True - - Returns - ------- - objectiveval: float - The objective function value. - thetavals: dict - A dictionary of all values for theta that were input. - solvertermination: Pyomo TerminationCondition - Tries to return the "worst" solver status across the scenarios. - pyo.TerminationCondition.optimal is the best and - pyo.TerminationCondition.infeasible is the worst. - """ - - optimizer = pyo.SolverFactory('ipopt') - - if len(thetavals) > 0: - dummy_cb = { - "callback": self._instance_creation_callback, - "ThetaVals": thetavals, - "theta_names": self._return_theta_names(), - "cb_data": self.callback_data, - } - else: - dummy_cb = { - "callback": self._instance_creation_callback, - "theta_names": self._return_theta_names(), - "cb_data": self.callback_data, - } - - if self.diagnostic_mode: - if len(thetavals) > 0: - print(' Compute objective at theta = ', str(thetavals)) - else: - print(' Compute objective at initial theta') - - # start block of code to deal with models with no constraints - # (ipopt will crash or complain on such problems without special care) - instance = _experiment_instance_creation_callback("FOO0", None, dummy_cb) - try: # deal with special problems so Ipopt will not crash - first = next(instance.component_objects(pyo.Constraint, active=True)) - active_constraints = True - except: - active_constraints = False - # end block of code to deal with models with no constraints - - WorstStatus = pyo.TerminationCondition.optimal - totobj = 0 - scenario_numbers = list(range(len(self.callback_data))) - if initialize_parmest_model: - # create dictionary to store pyomo model instances (scenarios) - scen_dict = dict() - - for snum in scenario_numbers: - sname = "scenario_NODE" + str(snum) - instance = _experiment_instance_creation_callback(sname, None, dummy_cb) - - if initialize_parmest_model: - # list to store fitted parameter names that will be unfixed - # after initialization - theta_init_vals = [] - # use appropriate theta_names member - theta_ref = self._return_theta_names() - - for i, theta in enumerate(theta_ref): - # Use parser in ComponentUID to locate the component - var_cuid = ComponentUID(theta) - var_validate = var_cuid.find_component_on(instance) - if var_validate is None: - logger.warning( - "theta_name %s was not found on the model", (theta) - ) - else: - try: - if len(thetavals) == 0: - var_validate.fix() - else: - var_validate.fix(thetavals[theta]) - theta_init_vals.append(var_validate) - except: - logger.warning( - 'Unable to fix model parameter value for %s (not a Pyomo model Var)', - (theta), - ) - - if active_constraints: - if self.diagnostic_mode: - print(' Experiment = ', snum) - print(' First solve with special diagnostics wrapper') - (status_obj, solved, iters, time, regu) = ( - utils.ipopt_solve_with_stats( - instance, optimizer, max_iter=500, max_cpu_time=120 - ) - ) - print( - " status_obj, solved, iters, time, regularization_stat = ", - str(status_obj), - str(solved), - str(iters), - str(time), - str(regu), - ) - - results = optimizer.solve(instance) - if self.diagnostic_mode: - print( - 'standard solve solver termination condition=', - str(results.solver.termination_condition), - ) - - if ( - results.solver.termination_condition - != pyo.TerminationCondition.optimal - ): - # DLW: Aug2018: not distinguishing "middlish" conditions - if WorstStatus != pyo.TerminationCondition.infeasible: - WorstStatus = results.solver.termination_condition - if initialize_parmest_model: - if self.diagnostic_mode: - print( - "Scenario {:d} infeasible with initialized parameter values".format( - snum - ) - ) - else: - if initialize_parmest_model: - if self.diagnostic_mode: - print( - "Scenario {:d} initialization successful with initial parameter values".format( - snum - ) - ) - if initialize_parmest_model: - # unfix parameters after initialization - for theta in theta_init_vals: - theta.unfix() - scen_dict[sname] = instance - else: - if initialize_parmest_model: - # unfix parameters after initialization - for theta in theta_init_vals: - theta.unfix() - scen_dict[sname] = instance - - objobject = getattr(instance, self._second_stage_cost_exp) - objval = pyo.value(objobject) - totobj += objval - - retval = totobj / len(scenario_numbers) # -1?? - if initialize_parmest_model and not hasattr(self, 'ef_instance'): - # create extensive form of the model using scenario dictionary - if len(scen_dict) > 0: - for scen in scen_dict.values(): - scen._mpisppy_probability = 1 / len(scen_dict) - - if use_mpisppy: - EF_instance = sputils._create_EF_from_scen_dict( - scen_dict, - EF_name="_Q_at_theta", - # suppress_warnings=True - ) - else: - EF_instance = local_ef._create_EF_from_scen_dict( - scen_dict, EF_name="_Q_at_theta", nonant_for_fixed_vars=True - ) - - self.ef_instance = EF_instance - # set self.model_initialized flag to True to skip extensive form model - # creation using theta_est() - self.model_initialized = True - - # return initialized theta values - if len(thetavals) == 0: - # use appropriate theta_names member - theta_ref = self._return_theta_names() - for i, theta in enumerate(theta_ref): - thetavals[theta] = theta_init_vals[i]() - - return retval, thetavals, WorstStatus - - def _get_sample_list(self, samplesize, num_samples, replacement=True): - samplelist = list() - - scenario_numbers = list(range(len(self.callback_data))) - - if num_samples is None: - # This could get very large - for i, l in enumerate(combinations(scenario_numbers, samplesize)): - samplelist.append((i, np.sort(l))) - else: - for i in range(num_samples): - attempts = 0 - unique_samples = 0 # check for duplicates in each sample - duplicate = False # check for duplicates between samples - while (unique_samples <= len(self._return_theta_names())) and ( - not duplicate - ): - sample = np.random.choice( - scenario_numbers, samplesize, replace=replacement - ) - sample = np.sort(sample).tolist() - unique_samples = len(np.unique(sample)) - if sample in samplelist: - duplicate = True - - attempts += 1 - if attempts > num_samples: # arbitrary timeout limit - raise RuntimeError( - """Internal error: timeout constructing - a sample, the dim of theta may be too - close to the samplesize""" - ) - - samplelist.append((i, sample)) - - return samplelist - - def theta_est( - self, solver="ef_ipopt", return_values=[], calc_cov=False, cov_n=None - ): - """ - Parameter estimation using all scenarios in the data - - Parameters - ---------- - solver: string, optional - Currently only "ef_ipopt" is supported. Default is "ef_ipopt". - return_values: list, optional - List of Variable names, used to return values from the model for data reconciliation - calc_cov: boolean, optional - If True, calculate and return the covariance matrix (only for "ef_ipopt" solver) - cov_n: int, optional - If calc_cov=True, then the user needs to supply the number of datapoints - that are used in the objective function - - Returns - ------- - objectiveval: float - The objective function value - thetavals: pd.Series - Estimated values for theta - variable values: pd.DataFrame - Variable values for each variable name in return_values (only for solver='ef_ipopt') - cov: pd.DataFrame - Covariance matrix of the fitted parameters (only for solver='ef_ipopt') - """ - assert isinstance(solver, str) - assert isinstance(return_values, list) - assert isinstance(calc_cov, bool) - if calc_cov: - assert isinstance( - cov_n, int - ), "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" - assert cov_n > len( - self._return_theta_names() - ), "The number of datapoints must be greater than the number of parameters to estimate" - - return self._Q_opt( - solver=solver, - return_values=return_values, - bootlist=None, - calc_cov=calc_cov, - cov_n=cov_n, - ) - - def theta_est_bootstrap( - self, - bootstrap_samples, - samplesize=None, - replacement=True, - seed=None, - return_samples=False, - ): - """ - Parameter estimation using bootstrap resampling of the data - - Parameters - ---------- - bootstrap_samples: int - Number of bootstrap samples to draw from the data - samplesize: int or None, optional - Size of each bootstrap sample. If samplesize=None, samplesize will be - set to the number of samples in the data - replacement: bool, optional - Sample with or without replacement - seed: int or None, optional - Random seed - return_samples: bool, optional - Return a list of sample numbers used in each bootstrap estimation - - Returns - ------- - bootstrap_theta: pd.DataFrame - Theta values for each sample and (if return_samples = True) - the sample numbers used in each estimation - """ - assert isinstance(bootstrap_samples, int) - assert isinstance(samplesize, (type(None), int)) - assert isinstance(replacement, bool) - assert isinstance(seed, (type(None), int)) - assert isinstance(return_samples, bool) - - if samplesize is None: - samplesize = len(self.callback_data) - - if seed is not None: - np.random.seed(seed) - - global_list = self._get_sample_list(samplesize, bootstrap_samples, replacement) - - task_mgr = utils.ParallelTaskManager(bootstrap_samples) - local_list = task_mgr.global_to_local_data(global_list) - - bootstrap_theta = list() - for idx, sample in local_list: - objval, thetavals = self._Q_opt(bootlist=list(sample)) - thetavals['samples'] = sample - bootstrap_theta.append(thetavals) - - global_bootstrap_theta = task_mgr.allgather_global_data(bootstrap_theta) - bootstrap_theta = pd.DataFrame(global_bootstrap_theta) - - if not return_samples: - del bootstrap_theta['samples'] - - return bootstrap_theta - - def theta_est_leaveNout( - self, lNo, lNo_samples=None, seed=None, return_samples=False - ): - """ - Parameter estimation where N data points are left out of each sample - - Parameters - ---------- - lNo: int - Number of data points to leave out for parameter estimation - lNo_samples: int - Number of leave-N-out samples. If lNo_samples=None, the maximum - number of combinations will be used - seed: int or None, optional - Random seed - return_samples: bool, optional - Return a list of sample numbers that were left out - - Returns - ------- - lNo_theta: pd.DataFrame - Theta values for each sample and (if return_samples = True) - the sample numbers left out of each estimation - """ - assert isinstance(lNo, int) - assert isinstance(lNo_samples, (type(None), int)) - assert isinstance(seed, (type(None), int)) - assert isinstance(return_samples, bool) - - samplesize = len(self.callback_data) - lNo - - if seed is not None: - np.random.seed(seed) - - global_list = self._get_sample_list(samplesize, lNo_samples, replacement=False) - - task_mgr = utils.ParallelTaskManager(len(global_list)) - local_list = task_mgr.global_to_local_data(global_list) - - lNo_theta = list() - for idx, sample in local_list: - objval, thetavals = self._Q_opt(bootlist=list(sample)) - lNo_s = list(set(range(len(self.callback_data))) - set(sample)) - thetavals['lNo'] = np.sort(lNo_s) - lNo_theta.append(thetavals) - - global_bootstrap_theta = task_mgr.allgather_global_data(lNo_theta) - lNo_theta = pd.DataFrame(global_bootstrap_theta) - - if not return_samples: - del lNo_theta['lNo'] - - return lNo_theta - - def leaveNout_bootstrap_test( - self, lNo, lNo_samples, bootstrap_samples, distribution, alphas, seed=None - ): - """ - Leave-N-out bootstrap test to compare theta values where N data points are - left out to a bootstrap analysis using the remaining data, - results indicate if theta is within a confidence region - determined by the bootstrap analysis - - Parameters - ---------- - lNo: int - Number of data points to leave out for parameter estimation - lNo_samples: int - Leave-N-out sample size. If lNo_samples=None, the maximum number - of combinations will be used - bootstrap_samples: int: - Bootstrap sample size - distribution: string - Statistical distribution used to define a confidence region, - options = 'MVN' for multivariate_normal, 'KDE' for gaussian_kde, - and 'Rect' for rectangular. - alphas: list - List of alpha values used to determine if theta values are inside - or outside the region. - seed: int or None, optional - Random seed - - Returns - ---------- - List of tuples with one entry per lNo_sample: - - * The first item in each tuple is the list of N samples that are left - out. - * The second item in each tuple is a DataFrame of theta estimated using - the N samples. - * The third item in each tuple is a DataFrame containing results from - the bootstrap analysis using the remaining samples. - - For each DataFrame a column is added for each value of alpha which - indicates if the theta estimate is in (True) or out (False) of the - alpha region for a given distribution (based on the bootstrap results) - """ - assert isinstance(lNo, int) - assert isinstance(lNo_samples, (type(None), int)) - assert isinstance(bootstrap_samples, int) - assert distribution in ['Rect', 'MVN', 'KDE'] - assert isinstance(alphas, list) - assert isinstance(seed, (type(None), int)) - - if seed is not None: - np.random.seed(seed) - - data = self.callback_data.copy() - - global_list = self._get_sample_list(lNo, lNo_samples, replacement=False) - - results = [] - for idx, sample in global_list: - # Reset callback_data to only include the sample - self.callback_data = [data[i] for i in sample] - - obj, theta = self.theta_est() - - # Reset callback_data to include all scenarios except the sample - self.callback_data = [data[i] for i in range(len(data)) if i not in sample] - - bootstrap_theta = self.theta_est_bootstrap(bootstrap_samples) - - training, test = self.confidence_region_test( - bootstrap_theta, - distribution=distribution, - alphas=alphas, - test_theta_values=theta, - ) - - results.append((sample, test, training)) - - # Reset callback_data (back to full data set) - self.callback_data = data - - return results - - def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): - """ - Objective value for each theta - - Parameters - ---------- - theta_values: pd.DataFrame, columns=theta_names - Values of theta used to compute the objective - - initialize_parmest_model: boolean - If True: Solve square problem instance, build extensive form of the model for - parameter estimation, and set flag model_initialized to True - - - Returns - ------- - obj_at_theta: pd.DataFrame - Objective value for each theta (infeasible solutions are - omitted). - """ - if len(self.theta_names) == 1 and self.theta_names[0] == 'parmest_dummy_var': - pass # skip assertion if model has no fitted parameters - else: - # create a local instance of the pyomo model to access model variables and parameters - model_temp = self._create_parmest_model(self.callback_data[0]) - model_theta_list = [] # list to store indexed and non-indexed parameters - # iterate over original theta_names - for theta_i in self.theta_names: - var_cuid = ComponentUID(theta_i) - var_validate = var_cuid.find_component_on(model_temp) - # check if theta in theta_names are indexed - try: - # get component UID of Set over which theta is defined - set_cuid = ComponentUID(var_validate.index_set()) - # access and iterate over the Set to generate theta names as they appear - # in the pyomo model - set_validate = set_cuid.find_component_on(model_temp) - for s in set_validate: - self_theta_temp = repr(var_cuid) + "[" + repr(s) + "]" - # generate list of theta names - model_theta_list.append(self_theta_temp) - # if theta is not indexed, copy theta name to list as-is - except AttributeError: - self_theta_temp = repr(var_cuid) - model_theta_list.append(self_theta_temp) - except: - raise - # if self.theta_names is not the same as temp model_theta_list, - # create self.theta_names_updated - if set(self.theta_names) == set(model_theta_list) and len( - self.theta_names - ) == set(model_theta_list): - pass - else: - self.theta_names_updated = model_theta_list - - if theta_values is None: - all_thetas = {} # dictionary to store fitted variables - # use appropriate theta names member - theta_names = self._return_theta_names() - else: - assert isinstance(theta_values, pd.DataFrame) - # for parallel code we need to use lists and dicts in the loop - theta_names = theta_values.columns - # # check if theta_names are in model - for theta in list(theta_names): - theta_temp = theta.replace("'", "") # cleaning quotes from theta_names - - assert theta_temp in [ - t.replace("'", "") for t in model_theta_list - ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( - theta_temp, model_theta_list - ) - assert len(list(theta_names)) == len(model_theta_list) - - all_thetas = theta_values.to_dict('records') - - if all_thetas: - task_mgr = utils.ParallelTaskManager(len(all_thetas)) - local_thetas = task_mgr.global_to_local_data(all_thetas) - else: - if initialize_parmest_model: - task_mgr = utils.ParallelTaskManager( - 1 - ) # initialization performed using just 1 set of theta values - # walk over the mesh, return objective function - all_obj = list() - if len(all_thetas) > 0: - for Theta in local_thetas: - obj, thetvals, worststatus = self._Q_at_theta( - Theta, initialize_parmest_model=initialize_parmest_model - ) - if worststatus != pyo.TerminationCondition.infeasible: - all_obj.append(list(Theta.values()) + [obj]) - # DLW, Aug2018: should we also store the worst solver status? - else: - obj, thetvals, worststatus = self._Q_at_theta( - thetavals={}, initialize_parmest_model=initialize_parmest_model - ) - if worststatus != pyo.TerminationCondition.infeasible: - all_obj.append(list(thetvals.values()) + [obj]) - - global_all_obj = task_mgr.allgather_global_data(all_obj) - dfcols = list(theta_names) + ['obj'] - obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) - return obj_at_theta - - def likelihood_ratio_test( - self, obj_at_theta, obj_value, alphas, return_thresholds=False - ): - r""" - Likelihood ratio test to identify theta values within a confidence - region using the :math:`\chi^2` distribution - - Parameters - ---------- - obj_at_theta: pd.DataFrame, columns = theta_names + 'obj' - Objective values for each theta value (returned by - objective_at_theta) - obj_value: int or float - Objective value from parameter estimation using all data - alphas: list - List of alpha values to use in the chi2 test - return_thresholds: bool, optional - Return the threshold value for each alpha - - Returns - ------- - LR: pd.DataFrame - Objective values for each theta value along with True or False for - each alpha - thresholds: pd.Series - If return_threshold = True, the thresholds are also returned. - """ - assert isinstance(obj_at_theta, pd.DataFrame) - assert isinstance(obj_value, (int, float)) - assert isinstance(alphas, list) - assert isinstance(return_thresholds, bool) - - LR = obj_at_theta.copy() - S = len(self.callback_data) - thresholds = {} - for a in alphas: - chi2_val = scipy.stats.chi2.ppf(a, 2) - thresholds[a] = obj_value * ((chi2_val / (S - 2)) + 1) - LR[a] = LR['obj'] < thresholds[a] - - thresholds = pd.Series(thresholds) - - if return_thresholds: - return LR, thresholds - else: - return LR - - def confidence_region_test( - self, theta_values, distribution, alphas, test_theta_values=None - ): - """ - Confidence region test to determine if theta values are within a - rectangular, multivariate normal, or Gaussian kernel density distribution - for a range of alpha values - - Parameters - ---------- - theta_values: pd.DataFrame, columns = theta_names - Theta values used to generate a confidence region - (generally returned by theta_est_bootstrap) - distribution: string - Statistical distribution used to define a confidence region, - options = 'MVN' for multivariate_normal, 'KDE' for gaussian_kde, - and 'Rect' for rectangular. - alphas: list - List of alpha values used to determine if theta values are inside - or outside the region. - test_theta_values: pd.Series or pd.DataFrame, keys/columns = theta_names, optional - Additional theta values that are compared to the confidence region - to determine if they are inside or outside. - - Returns - training_results: pd.DataFrame - Theta value used to generate the confidence region along with True - (inside) or False (outside) for each alpha - test_results: pd.DataFrame - If test_theta_values is not None, returns test theta value along - with True (inside) or False (outside) for each alpha - """ - assert isinstance(theta_values, pd.DataFrame) - assert distribution in ['Rect', 'MVN', 'KDE'] - assert isinstance(alphas, list) - assert isinstance( - test_theta_values, (type(None), dict, pd.Series, pd.DataFrame) - ) - - if isinstance(test_theta_values, (dict, pd.Series)): - test_theta_values = pd.Series(test_theta_values).to_frame().transpose() - - training_results = theta_values.copy() - - if test_theta_values is not None: - test_result = test_theta_values.copy() - - for a in alphas: - if distribution == 'Rect': - lb, ub = graphics.fit_rect_dist(theta_values, a) - training_results[a] = (theta_values > lb).all(axis=1) & ( - theta_values < ub - ).all(axis=1) - - if test_theta_values is not None: - # use upper and lower bound from the training set - test_result[a] = (test_theta_values > lb).all(axis=1) & ( - test_theta_values < ub - ).all(axis=1) - - elif distribution == 'MVN': - dist = graphics.fit_mvn_dist(theta_values) - Z = dist.pdf(theta_values) - score = scipy.stats.scoreatpercentile(Z, (1 - a) * 100) - training_results[a] = Z >= score - - if test_theta_values is not None: - # use score from the training set - Z = dist.pdf(test_theta_values) - test_result[a] = Z >= score - - elif distribution == 'KDE': - dist = graphics.fit_kde_dist(theta_values) - Z = dist.pdf(theta_values.transpose()) - score = scipy.stats.scoreatpercentile(Z, (1 - a) * 100) - training_results[a] = Z >= score - - if test_theta_values is not None: - # use score from the training set - Z = dist.pdf(test_theta_values.transpose()) - test_result[a] = Z >= score - - if test_theta_values is not None: - return training_results, test_result - else: - return training_results diff --git a/pyomo/contrib/parmest/deprecated/scenariocreator.py b/pyomo/contrib/parmest/deprecated/scenariocreator.py deleted file mode 100644 index af084d0712c..00000000000 --- a/pyomo/contrib/parmest/deprecated/scenariocreator.py +++ /dev/null @@ -1,166 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# ScenariosCreator.py - Class to create and deliver scenarios using parmest -# DLW March 2020 - -import pyomo.environ as pyo - - -class ScenarioSet(object): - """ - Class to hold scenario sets - - Args: - name (str): name of the set (might be "") - - """ - - def __init__(self, name): - # Note: If there was a use-case, the list could be a dataframe. - self._scens = list() # use a df instead? - self.name = name # might be "" - - def _firstscen(self): - # Return the first scenario for testing and to get Theta names. - assert len(self._scens) > 0 - return self._scens[0] - - def ScensIterator(self): - """Usage: for scenario in ScensIterator()""" - return iter(self._scens) - - def ScenarioNumber(self, scennum): - """Returns the scenario with the given, zero-based number""" - return self._scens[scennum] - - def addone(self, scen): - """Add a scenario to the set - - Args: - scen (ParmestScen): the scenario to add - """ - assert isinstance(self._scens, list) - self._scens.append(scen) - - def append_bootstrap(self, bootstrap_theta): - """Append a bootstrap theta df to the scenario set; equally likely - - Args: - bootstrap_theta (dataframe): created by the bootstrap - Note: this can be cleaned up a lot with the list becomes a df, - which is why I put it in the ScenarioSet class. - """ - assert len(bootstrap_theta) > 0 - prob = 1.0 / len(bootstrap_theta) - - # dict of ThetaVal dicts - dfdict = bootstrap_theta.to_dict(orient='index') - - for index, ThetaVals in dfdict.items(): - name = "Bootstrap" + str(index) - self.addone(ParmestScen(name, ThetaVals, prob)) - - def write_csv(self, filename): - """write a csv file with the scenarios in the set - - Args: - filename (str): full path and full name of file - """ - if len(self._scens) == 0: - print("Empty scenario set, not writing file={}".format(filename)) - return - with open(filename, "w") as f: - f.write("Name,Probability") - for n in self._firstscen().ThetaVals.keys(): - f.write(",{}".format(n)) - f.write('\n') - for s in self.ScensIterator(): - f.write("{},{}".format(s.name, s.probability)) - for v in s.ThetaVals.values(): - f.write(",{}".format(v)) - f.write('\n') - - -class ParmestScen(object): - """A little container for scenarios; the Args are the attributes. - - Args: - name (str): name for reporting; might be "" - ThetaVals (dict): ThetaVals[name]=val - probability (float): probability of occurrence "near" these ThetaVals - """ - - def __init__(self, name, ThetaVals, probability): - self.name = name - assert isinstance(ThetaVals, dict) - self.ThetaVals = ThetaVals - self.probability = probability - - -############################################################ - - -class ScenarioCreator(object): - """Create scenarios from parmest. - - Args: - pest (Estimator): the parmest object - solvername (str): name of the solver (e.g. "ipopt") - - """ - - def __init__(self, pest, solvername): - self.pest = pest - self.solvername = solvername - - def ScenariosFromExperiments(self, addtoSet): - """Creates new self.Scenarios list using the experiments only. - - Args: - addtoSet (ScenarioSet): the scenarios will be added to this set - Returns: - a ScenarioSet - """ - - # assert isinstance(addtoSet, ScenarioSet) - - scenario_numbers = list(range(len(self.pest.callback_data))) - - prob = 1.0 / len(scenario_numbers) - for exp_num in scenario_numbers: - ##print("Experiment number=", exp_num) - model = self.pest._instance_creation_callback( - exp_num, self.pest.callback_data - ) - opt = pyo.SolverFactory(self.solvername) - results = opt.solve(model) # solves and updates model - ## pyo.check_termination_optimal(results) - ThetaVals = dict() - for theta in self.pest.theta_names: - tvar = eval('model.' + theta) - tval = pyo.value(tvar) - ##print(" theta, tval=", tvar, tval) - ThetaVals[theta] = tval - addtoSet.addone(ParmestScen("ExpScen" + str(exp_num), ThetaVals, prob)) - - def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): - """Creates new self.Scenarios list using the experiments only. - - Args: - addtoSet (ScenarioSet): the scenarios will be added to this set - numtomake (int) : number of scenarios to create - """ - - # assert isinstance(addtoSet, ScenarioSet) - - bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) - addtoSet.append_bootstrap(bootstrap_thetas) diff --git a/pyomo/contrib/parmest/deprecated/tests/__init__.py b/pyomo/contrib/parmest/deprecated/tests/__init__.py deleted file mode 100644 index d93cfd77b3c..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/pyomo/contrib/parmest/deprecated/tests/scenarios.csv b/pyomo/contrib/parmest/deprecated/tests/scenarios.csv deleted file mode 100644 index 22f9a651bc3..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/scenarios.csv +++ /dev/null @@ -1,11 +0,0 @@ -Name,Probability,k1,k2,E1,E2 -ExpScen0,0.1,25.800350800448314,14.14421520525348,31505.74905064048,35000.0 -ExpScen1,0.1,25.128373083865036,149.99999951481198,31452.336651974012,41938.781301641866 -ExpScen2,0.1,22.225574065344002,130.92739780265404,30948.669111672247,41260.15420929141 -ExpScen3,0.1,100.0,149.99999970011854,35182.73130744844,41444.52600373733 -ExpScen4,0.1,82.99114366189944,45.95424665995078,34810.857217141674,38300.633349887314 -ExpScen5,0.1,100.0,150.0,35142.20219150486,41495.41105795494 -ExpScen6,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 -ExpScen7,0.1,2.754580914035567,14.381786096822475,25000.0,35000.0 -ExpScen8,0.1,2.8743643265301118,149.99999477176598,25000.0,41431.61195969211 -ExpScen9,0.1,2.669780822294865,150.0,25000.0,41514.7476113499 diff --git a/pyomo/contrib/parmest/deprecated/tests/test_examples.py b/pyomo/contrib/parmest/deprecated/tests/test_examples.py deleted file mode 100644 index 6f5d9703f05..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/test_examples.py +++ /dev/null @@ -1,204 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pyomo.common.unittest as unittest -import pyomo.contrib.parmest.parmest as parmest -from pyomo.contrib.parmest.graphics import matplotlib_available, seaborn_available -from pyomo.opt import SolverFactory - -ipopt_available = SolverFactory("ipopt").available() - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestRooneyBieglerExamples(unittest.TestCase): - @classmethod - def setUpClass(self): - pass - - @classmethod - def tearDownClass(self): - pass - - def test_model(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( - rooney_biegler, - ) - - rooney_biegler.main() - - def test_model_with_constraint(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( - rooney_biegler_with_constraint, - ) - - rooney_biegler_with_constraint.main() - - @unittest.skipUnless(seaborn_available, "test requires seaborn") - def test_parameter_estimation_example(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( - parameter_estimation_example, - ) - - parameter_estimation_example.main() - - @unittest.skipUnless(seaborn_available, "test requires seaborn") - def test_bootstrap_example(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( - bootstrap_example, - ) - - bootstrap_example.main() - - @unittest.skipUnless(seaborn_available, "test requires seaborn") - def test_likelihood_ratio_example(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler import ( - likelihood_ratio_example, - ) - - likelihood_ratio_example.main() - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestReactionKineticsExamples(unittest.TestCase): - @classmethod - def setUpClass(self): - pass - - @classmethod - def tearDownClass(self): - pass - - def test_example(self): - from pyomo.contrib.parmest.deprecated.examples.reaction_kinetics import ( - simple_reaction_parmest_example, - ) - - simple_reaction_parmest_example.main() - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestSemibatchExamples(unittest.TestCase): - @classmethod - def setUpClass(self): - pass - - @classmethod - def tearDownClass(self): - pass - - def test_model(self): - from pyomo.contrib.parmest.deprecated.examples.semibatch import semibatch - - semibatch.main() - - def test_parameter_estimation_example(self): - from pyomo.contrib.parmest.deprecated.examples.semibatch import ( - parameter_estimation_example, - ) - - parameter_estimation_example.main() - - def test_scenario_example(self): - from pyomo.contrib.parmest.deprecated.examples.semibatch import scenario_example - - scenario_example.main() - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestReactorDesignExamples(unittest.TestCase): - @classmethod - def setUpClass(self): - pass - - @classmethod - def tearDownClass(self): - pass - - @unittest.pytest.mark.expensive - def test_model(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - reactor_design, - ) - - reactor_design.main() - - def test_parameter_estimation_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - parameter_estimation_example, - ) - - parameter_estimation_example.main() - - @unittest.skipUnless(seaborn_available, "test requires seaborn") - def test_bootstrap_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - bootstrap_example, - ) - - bootstrap_example.main() - - @unittest.pytest.mark.expensive - def test_likelihood_ratio_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - likelihood_ratio_example, - ) - - likelihood_ratio_example.main() - - @unittest.pytest.mark.expensive - def test_leaveNout_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - leaveNout_example, - ) - - leaveNout_example.main() - - def test_timeseries_data_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - timeseries_data_example, - ) - - timeseries_data_example.main() - - def test_multisensor_data_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - multisensor_data_example, - ) - - multisensor_data_example.main() - - @unittest.skipUnless(matplotlib_available, "test requires matplotlib") - def test_datarec_example(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design import ( - datarec_example, - ) - - datarec_example.main() - - -if __name__ == "__main__": - unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_graphics.py b/pyomo/contrib/parmest/deprecated/tests/test_graphics.py deleted file mode 100644 index c18659e9948..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/test_graphics.py +++ /dev/null @@ -1,68 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.common.dependencies import ( - numpy as np, - numpy_available, - pandas as pd, - pandas_available, - scipy, - scipy_available, - matplotlib, - matplotlib_available, -) - -import platform - -is_osx = platform.mac_ver()[0] != '' - -import pyomo.common.unittest as unittest -import sys -import os - -import pyomo.contrib.parmest.parmest as parmest -import pyomo.contrib.parmest.graphics as graphics - -testdir = os.path.dirname(os.path.abspath(__file__)) - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf( - not graphics.imports_available, "parmest.graphics imports are unavailable" -) -@unittest.skipIf( - is_osx, - "Disabling graphics tests on OSX due to issue in Matplotlib, see Pyomo PR #1337", -) -class TestGraphics(unittest.TestCase): - def setUp(self): - self.A = pd.DataFrame( - np.random.randint(0, 100, size=(100, 4)), columns=list('ABCD') - ) - self.B = pd.DataFrame( - np.random.randint(0, 100, size=(100, 4)), columns=list('ABCD') - ) - - def test_pairwise_plot(self): - graphics.pairwise_plot(self.A, alpha=0.8, distributions=['Rect', 'MVN', 'KDE']) - - def test_grouped_boxplot(self): - graphics.grouped_boxplot(self.A, self.B, normalize=True, group_names=['A', 'B']) - - def test_grouped_violinplot(self): - graphics.grouped_violinplot(self.A, self.B) - - -if __name__ == '__main__': - unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_parmest.py b/pyomo/contrib/parmest/deprecated/tests/test_parmest.py deleted file mode 100644 index 27776bdc64c..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/test_parmest.py +++ /dev/null @@ -1,956 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.common.dependencies import ( - numpy as np, - numpy_available, - pandas as pd, - pandas_available, - scipy, - scipy_available, - matplotlib, - matplotlib_available, -) - -import platform - -is_osx = platform.mac_ver()[0] != "" - -import pyomo.common.unittest as unittest -import sys -import os -import subprocess -from itertools import product - -import pyomo.contrib.parmest.parmest as parmest -import pyomo.contrib.parmest.graphics as graphics -import pyomo.contrib.parmest as parmestbase -import pyomo.environ as pyo -import pyomo.dae as dae - -from pyomo.opt import SolverFactory - -ipopt_available = SolverFactory("ipopt").available() - -from pyomo.common.fileutils import find_library - -pynumero_ASL_available = False if find_library("pynumero_ASL") is None else True - -testdir = os.path.dirname(os.path.abspath(__file__)) - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") -class TestRooneyBiegler(unittest.TestCase): - def setUp(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, - ) - - # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=["hour", "y"], - ) - - theta_names = ["asymptote", "rate_constant"] - - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 - for i in data.index - ) - return expr - - solver_options = {"tol": 1e-8} - - self.data = data - self.pest = parmest.Estimator( - rooney_biegler_model, - data, - theta_names, - SSE, - solver_options=solver_options, - tee=True, - ) - - def test_theta_est(self): - objval, thetavals = self.pest.theta_est() - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - thetavals["asymptote"], 19.1426, places=2 - ) # 19.1426 from the paper - self.assertAlmostEqual( - thetavals["rate_constant"], 0.5311, places=2 - ) # 0.5311 from the paper - - @unittest.skipIf( - not graphics.imports_available, "parmest.graphics imports are unavailable" - ) - def test_bootstrap(self): - objval, thetavals = self.pest.theta_est() - - num_bootstraps = 10 - theta_est = self.pest.theta_est_bootstrap(num_bootstraps, return_samples=True) - - num_samples = theta_est["samples"].apply(len) - self.assertTrue(len(theta_est.index), 10) - self.assertTrue(num_samples.equals(pd.Series([6] * 10))) - - del theta_est["samples"] - - # apply confidence region test - CR = self.pest.confidence_region_test(theta_est, "MVN", [0.5, 0.75, 1.0]) - - self.assertTrue(set(CR.columns) >= set([0.5, 0.75, 1.0])) - self.assertTrue(CR[0.5].sum() == 5) - self.assertTrue(CR[0.75].sum() == 7) - self.assertTrue(CR[1.0].sum() == 10) # all true - - graphics.pairwise_plot(theta_est) - graphics.pairwise_plot(theta_est, thetavals) - graphics.pairwise_plot(theta_est, thetavals, 0.8, ["MVN", "KDE", "Rect"]) - - @unittest.skipIf( - not graphics.imports_available, "parmest.graphics imports are unavailable" - ) - def test_likelihood_ratio(self): - objval, thetavals = self.pest.theta_est() - - asym = np.arange(10, 30, 2) - rate = np.arange(0, 1.5, 0.25) - theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=self.pest._return_theta_names() - ) - - obj_at_theta = self.pest.objective_at_theta(theta_vals) - - LR = self.pest.likelihood_ratio_test(obj_at_theta, objval, [0.8, 0.9, 1.0]) - - self.assertTrue(set(LR.columns) >= set([0.8, 0.9, 1.0])) - self.assertTrue(LR[0.8].sum() == 6) - self.assertTrue(LR[0.9].sum() == 10) - self.assertTrue(LR[1.0].sum() == 60) # all true - - graphics.pairwise_plot(LR, thetavals, 0.8) - - def test_leaveNout(self): - lNo_theta = self.pest.theta_est_leaveNout(1) - self.assertTrue(lNo_theta.shape == (6, 2)) - - results = self.pest.leaveNout_bootstrap_test( - 1, None, 3, "Rect", [0.5, 1.0], seed=5436 - ) - self.assertTrue(len(results) == 6) # 6 lNo samples - i = 1 - samples = results[i][0] # list of N samples that are left out - lno_theta = results[i][1] - bootstrap_theta = results[i][2] - self.assertTrue(samples == [1]) # sample 1 was left out - self.assertTrue(lno_theta.shape[0] == 1) # lno estimate for sample 1 - self.assertTrue(set(lno_theta.columns) >= set([0.5, 1.0])) - self.assertTrue(lno_theta[1.0].sum() == 1) # all true - self.assertTrue(bootstrap_theta.shape[0] == 3) # bootstrap for sample 1 - self.assertTrue(bootstrap_theta[1.0].sum() == 3) # all true - - def test_diagnostic_mode(self): - self.pest.diagnostic_mode = True - - objval, thetavals = self.pest.theta_est() - - asym = np.arange(10, 30, 2) - rate = np.arange(0, 1.5, 0.25) - theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=self.pest._return_theta_names() - ) - - obj_at_theta = self.pest.objective_at_theta(theta_vals) - - self.pest.diagnostic_mode = False - - @unittest.skip("Presently having trouble with mpiexec on appveyor") - def test_parallel_parmest(self): - """use mpiexec and mpi4py""" - p = str(parmestbase.__path__) - l = p.find("'") - r = p.find("'", l + 1) - parmestpath = p[l + 1 : r] - rbpath = ( - parmestpath - + os.sep - + "examples" - + os.sep - + "rooney_biegler" - + os.sep - + "rooney_biegler_parmest.py" - ) - rbpath = os.path.abspath(rbpath) # paranoia strikes deep... - rlist = ["mpiexec", "--allow-run-as-root", "-n", "2", sys.executable, rbpath] - if sys.version_info >= (3, 5): - ret = subprocess.run(rlist) - retcode = ret.returncode - else: - retcode = subprocess.call(rlist) - assert retcode == 0 - - @unittest.skip("Most folks don't have k_aug installed") - def test_theta_k_aug_for_Hessian(self): - # this will fail if k_aug is not installed - objval, thetavals, Hessian = self.pest.theta_est(solver="k_aug") - self.assertAlmostEqual(objval, 4.4675, places=2) - - @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") - @unittest.skipIf( - not parmest.inverse_reduced_hessian_available, - "Cannot test covariance matrix: required ASL dependency is missing", - ) - def test_theta_est_cov(self): - objval, thetavals, cov = self.pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - thetavals["asymptote"], 19.1426, places=2 - ) # 19.1426 from the paper - self.assertAlmostEqual( - thetavals["rate_constant"], 0.5311, places=2 - ) # 0.5311 from the paper - - # Covariance matrix - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual(cov.iloc[1, 1], 0.04124, places=2) # 0.04124 from paper - - """ Why does the covariance matrix from parmest not match the paper? Parmest is - calculating the exact reduced Hessian. The paper (Rooney and Bielger, 2001) likely - employed the first order approximation common for nonlinear regression. The paper - values were verified with Scipy, which uses the same first order approximation. - The formula used in parmest was verified against equations (7-5-15) and (7-5-16) in - "Nonlinear Parameter Estimation", Y. Bard, 1974. - """ - - def test_cov_scipy_least_squares_comparison(self): - """ - Scipy results differ in the 3rd decimal place from the paper. It is possible - the paper used an alternative finite difference approximation for the Jacobian. - """ - - def model(theta, t): - """ - Model to be fitted y = model(theta, t) - Arguments: - theta: vector of fitted parameters - t: independent variable [hours] - - Returns: - y: model predictions [need to check paper for units] - """ - asymptote = theta[0] - rate_constant = theta[1] - - return asymptote * (1 - np.exp(-rate_constant * t)) - - def residual(theta, t, y): - """ - Calculate residuals - Arguments: - theta: vector of fitted parameters - t: independent variable [hours] - y: dependent variable [?] - """ - return y - model(theta, t) - - # define data - t = self.data["hour"].to_numpy() - y = self.data["y"].to_numpy() - - # define initial guess - theta_guess = np.array([15, 0.5]) - - ## solve with optimize.least_squares - sol = scipy.optimize.least_squares( - residual, theta_guess, method="trf", args=(t, y), verbose=2 - ) - theta_hat = sol.x - - self.assertAlmostEqual( - theta_hat[0], 19.1426, places=2 - ) # 19.1426 from the paper - self.assertAlmostEqual(theta_hat[1], 0.5311, places=2) # 0.5311 from the paper - - # calculate residuals - r = residual(theta_hat, t, y) - - # calculate variance of the residuals - # -2 because there are 2 fitted parameters - sigre = np.matmul(r.T, r / (len(y) - 2)) - - # approximate covariance - # Need to divide by 2 because optimize.least_squares scaled the objective by 1/2 - cov = sigre * np.linalg.inv(np.matmul(sol.jac.T, sol.jac)) - - self.assertAlmostEqual(cov[0, 0], 6.22864, places=2) # 6.22864 from paper - self.assertAlmostEqual(cov[0, 1], -0.4322, places=2) # -0.4322 from paper - self.assertAlmostEqual(cov[1, 0], -0.4322, places=2) # -0.4322 from paper - self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper - - def test_cov_scipy_curve_fit_comparison(self): - """ - Scipy results differ in the 3rd decimal place from the paper. It is possible - the paper used an alternative finite difference approximation for the Jacobian. - """ - - ## solve with optimize.curve_fit - def model(t, asymptote, rate_constant): - return asymptote * (1 - np.exp(-rate_constant * t)) - - # define data - t = self.data["hour"].to_numpy() - y = self.data["y"].to_numpy() - - # define initial guess - theta_guess = np.array([15, 0.5]) - - theta_hat, cov = scipy.optimize.curve_fit(model, t, y, p0=theta_guess) - - self.assertAlmostEqual( - theta_hat[0], 19.1426, places=2 - ) # 19.1426 from the paper - self.assertAlmostEqual(theta_hat[1], 0.5311, places=2) # 0.5311 from the paper - - self.assertAlmostEqual(cov[0, 0], 6.22864, places=2) # 6.22864 from paper - self.assertAlmostEqual(cov[0, 1], -0.4322, places=2) # -0.4322 from paper - self.assertAlmostEqual(cov[1, 0], -0.4322, places=2) # -0.4322 from paper - self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") -class TestModelVariants(unittest.TestCase): - def setUp(self): - self.data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=["hour", "y"], - ) - - def rooney_biegler_params(data): - model = pyo.ConcreteModel() - - model.asymptote = pyo.Param(initialize=15, mutable=True) - model.rate_constant = pyo.Param(initialize=0.5, mutable=True) - - def response_rule(m, h): - expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) - return expr - - model.response_function = pyo.Expression(data.hour, rule=response_rule) - - return model - - def rooney_biegler_indexed_params(data): - model = pyo.ConcreteModel() - - model.param_names = pyo.Set(initialize=["asymptote", "rate_constant"]) - model.theta = pyo.Param( - model.param_names, - initialize={"asymptote": 15, "rate_constant": 0.5}, - mutable=True, - ) - - def response_rule(m, h): - expr = m.theta["asymptote"] * ( - 1 - pyo.exp(-m.theta["rate_constant"] * h) - ) - return expr - - model.response_function = pyo.Expression(data.hour, rule=response_rule) - - return model - - def rooney_biegler_vars(data): - model = pyo.ConcreteModel() - - model.asymptote = pyo.Var(initialize=15) - model.rate_constant = pyo.Var(initialize=0.5) - model.asymptote.fixed = True # parmest will unfix theta variables - model.rate_constant.fixed = True - - def response_rule(m, h): - expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) - return expr - - model.response_function = pyo.Expression(data.hour, rule=response_rule) - - return model - - def rooney_biegler_indexed_vars(data): - model = pyo.ConcreteModel() - - model.var_names = pyo.Set(initialize=["asymptote", "rate_constant"]) - model.theta = pyo.Var( - model.var_names, initialize={"asymptote": 15, "rate_constant": 0.5} - ) - model.theta["asymptote"].fixed = ( - True # parmest will unfix theta variables, even when they are indexed - ) - model.theta["rate_constant"].fixed = True - - def response_rule(m, h): - expr = m.theta["asymptote"] * ( - 1 - pyo.exp(-m.theta["rate_constant"] * h) - ) - return expr - - model.response_function = pyo.Expression(data.hour, rule=response_rule) - - return model - - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 - for i in data.index - ) - return expr - - self.objective_function = SSE - - theta_vals = pd.DataFrame([20, 1], index=["asymptote", "rate_constant"]).T - theta_vals_index = pd.DataFrame( - [20, 1], index=["theta['asymptote']", "theta['rate_constant']"] - ).T - - self.input = { - "param": { - "model": rooney_biegler_params, - "theta_names": ["asymptote", "rate_constant"], - "theta_vals": theta_vals, - }, - "param_index": { - "model": rooney_biegler_indexed_params, - "theta_names": ["theta"], - "theta_vals": theta_vals_index, - }, - "vars": { - "model": rooney_biegler_vars, - "theta_names": ["asymptote", "rate_constant"], - "theta_vals": theta_vals, - }, - "vars_index": { - "model": rooney_biegler_indexed_vars, - "theta_names": ["theta"], - "theta_vals": theta_vals_index, - }, - "vars_quoted_index": { - "model": rooney_biegler_indexed_vars, - "theta_names": ["theta['asymptote']", "theta['rate_constant']"], - "theta_vals": theta_vals_index, - }, - "vars_str_index": { - "model": rooney_biegler_indexed_vars, - "theta_names": ["theta[asymptote]", "theta[rate_constant]"], - "theta_vals": theta_vals_index, - }, - } - - @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") - @unittest.skipIf( - not parmest.inverse_reduced_hessian_available, - "Cannot test covariance matrix: required ASL dependency is missing", - ) - def test_parmest_basics(self): - for model_type, parmest_input in self.input.items(): - pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, - ) - - objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper - - obj_at_theta = pest.objective_at_theta(parmest_input["theta_vals"]) - self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) - - def test_parmest_basics_with_initialize_parmest_model_option(self): - for model_type, parmest_input in self.input.items(): - pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, - ) - - objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper - - obj_at_theta = pest.objective_at_theta( - parmest_input["theta_vals"], initialize_parmest_model=True - ) - - self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) - - def test_parmest_basics_with_square_problem_solve(self): - for model_type, parmest_input in self.input.items(): - pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, - ) - - obj_at_theta = pest.objective_at_theta( - parmest_input["theta_vals"], initialize_parmest_model=True - ) - - objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper - - self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) - - def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): - for model_type, parmest_input in self.input.items(): - pest = parmest.Estimator( - parmest_input["model"], - self.data, - parmest_input["theta_names"], - self.objective_function, - ) - - obj_at_theta = pest.objective_at_theta(initialize_parmest_model=True) - - objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - cov.iloc[0, 0], 6.30579403, places=2 - ) # 6.22864 from paper - self.assertAlmostEqual( - cov.iloc[0, 1], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 0], -0.4395341, places=2 - ) # -0.4322 from paper - self.assertAlmostEqual( - cov.iloc[1, 1], 0.04193591, places=2 - ) # 0.04124 from paper - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestReactorDesign(unittest.TestCase): - def setUp(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, - ) - - # Data from the design - data = pd.DataFrame( - data=[ - [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], - [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], - [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], - [1.20, 10000, 3680.7, 1070.0, 1486.1, 1881.6], - [1.25, 10000, 3750.0, 1071.4, 1428.6, 1875.0], - [1.30, 10000, 3817.1, 1072.2, 1374.6, 1868.0], - [1.35, 10000, 3882.2, 1072.4, 1324.0, 1860.7], - [1.40, 10000, 3945.4, 1072.1, 1276.3, 1853.1], - [1.45, 10000, 4006.7, 1071.3, 1231.4, 1845.3], - [1.50, 10000, 4066.4, 1070.1, 1189.0, 1837.3], - [1.55, 10000, 4124.4, 1068.5, 1148.9, 1829.1], - [1.60, 10000, 4180.9, 1066.5, 1111.0, 1820.8], - [1.65, 10000, 4235.9, 1064.3, 1075.0, 1812.4], - [1.70, 10000, 4289.5, 1061.8, 1040.9, 1803.9], - [1.75, 10000, 4341.8, 1059.0, 1008.5, 1795.3], - [1.80, 10000, 4392.8, 1056.0, 977.7, 1786.7], - [1.85, 10000, 4442.6, 1052.8, 948.4, 1778.1], - [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], - [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], - ], - columns=["sv", "caf", "ca", "cb", "cc", "cd"], - ) - - theta_names = ["k1", "k2", "k3"] - - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - solver_options = {"max_iter": 6000} - - self.pest = parmest.Estimator( - reactor_design_model, data, theta_names, SSE, solver_options=solver_options - ) - - def test_theta_est(self): - # used in data reconciliation - objval, thetavals = self.pest.theta_est() - - self.assertAlmostEqual(thetavals["k1"], 5.0 / 6.0, places=4) - self.assertAlmostEqual(thetavals["k2"], 5.0 / 3.0, places=4) - self.assertAlmostEqual(thetavals["k3"], 1.0 / 6000.0, places=7) - - def test_return_values(self): - objval, thetavals, data_rec = self.pest.theta_est( - return_values=["ca", "cb", "cc", "cd", "caf"] - ) - self.assertAlmostEqual(data_rec["cc"].loc[18], 893.84924, places=3) - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestReactorDesign_DAE(unittest.TestCase): - # Based on a reactor example in `Chemical Reactor Analysis and Design Fundamentals`, - # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/ - # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/fig-html/appendix/fig-A-10.html - - def setUp(self): - def ABC_model(data): - ca_meas = data["ca"] - cb_meas = data["cb"] - cc_meas = data["cc"] - - if isinstance(data, pd.DataFrame): - meas_t = data.index # time index - else: # dictionary - meas_t = list(ca_meas.keys()) # nested dictionary - - ca0 = 1.0 - cb0 = 0.0 - cc0 = 0.0 - - m = pyo.ConcreteModel() - - m.k1 = pyo.Var(initialize=0.5, bounds=(1e-4, 10)) - m.k2 = pyo.Var(initialize=3.0, bounds=(1e-4, 10)) - - m.time = dae.ContinuousSet(bounds=(0.0, 5.0), initialize=meas_t) - - # initialization and bounds - m.ca = pyo.Var(m.time, initialize=ca0, bounds=(-1e-3, ca0 + 1e-3)) - m.cb = pyo.Var(m.time, initialize=cb0, bounds=(-1e-3, ca0 + 1e-3)) - m.cc = pyo.Var(m.time, initialize=cc0, bounds=(-1e-3, ca0 + 1e-3)) - - m.dca = dae.DerivativeVar(m.ca, wrt=m.time) - m.dcb = dae.DerivativeVar(m.cb, wrt=m.time) - m.dcc = dae.DerivativeVar(m.cc, wrt=m.time) - - def _dcarate(m, t): - if t == 0: - return pyo.Constraint.Skip - else: - return m.dca[t] == -m.k1 * m.ca[t] - - m.dcarate = pyo.Constraint(m.time, rule=_dcarate) - - def _dcbrate(m, t): - if t == 0: - return pyo.Constraint.Skip - else: - return m.dcb[t] == m.k1 * m.ca[t] - m.k2 * m.cb[t] - - m.dcbrate = pyo.Constraint(m.time, rule=_dcbrate) - - def _dccrate(m, t): - if t == 0: - return pyo.Constraint.Skip - else: - return m.dcc[t] == m.k2 * m.cb[t] - - m.dccrate = pyo.Constraint(m.time, rule=_dccrate) - - def ComputeFirstStageCost_rule(m): - return 0 - - m.FirstStageCost = pyo.Expression(rule=ComputeFirstStageCost_rule) - - def ComputeSecondStageCost_rule(m): - return sum( - (m.ca[t] - ca_meas[t]) ** 2 - + (m.cb[t] - cb_meas[t]) ** 2 - + (m.cc[t] - cc_meas[t]) ** 2 - for t in meas_t - ) - - m.SecondStageCost = pyo.Expression(rule=ComputeSecondStageCost_rule) - - def total_cost_rule(model): - return model.FirstStageCost + model.SecondStageCost - - m.Total_Cost_Objective = pyo.Objective( - rule=total_cost_rule, sense=pyo.minimize - ) - - disc = pyo.TransformationFactory("dae.collocation") - disc.apply_to(m, nfe=20, ncp=2) - - return m - - # This example tests data formatted in 3 ways - # Each format holds 1 scenario - # 1. dataframe with time index - # 2. nested dictionary {ca: {t, val pairs}, ... } - data = [ - [0.000, 0.957, -0.031, -0.015], - [0.263, 0.557, 0.330, 0.044], - [0.526, 0.342, 0.512, 0.156], - [0.789, 0.224, 0.499, 0.310], - [1.053, 0.123, 0.428, 0.454], - [1.316, 0.079, 0.396, 0.556], - [1.579, 0.035, 0.303, 0.651], - [1.842, 0.029, 0.287, 0.658], - [2.105, 0.025, 0.221, 0.750], - [2.368, 0.017, 0.148, 0.854], - [2.632, -0.002, 0.182, 0.845], - [2.895, 0.009, 0.116, 0.893], - [3.158, -0.023, 0.079, 0.942], - [3.421, 0.006, 0.078, 0.899], - [3.684, 0.016, 0.059, 0.942], - [3.947, 0.014, 0.036, 0.991], - [4.211, -0.009, 0.014, 0.988], - [4.474, -0.030, 0.036, 0.941], - [4.737, 0.004, 0.036, 0.971], - [5.000, -0.024, 0.028, 0.985], - ] - data = pd.DataFrame(data, columns=["t", "ca", "cb", "cc"]) - data_df = data.set_index("t") - data_dict = { - "ca": {k: v for (k, v) in zip(data.t, data.ca)}, - "cb": {k: v for (k, v) in zip(data.t, data.cb)}, - "cc": {k: v for (k, v) in zip(data.t, data.cc)}, - } - - theta_names = ["k1", "k2"] - - self.pest_df = parmest.Estimator(ABC_model, [data_df], theta_names) - self.pest_dict = parmest.Estimator(ABC_model, [data_dict], theta_names) - - # Estimator object with multiple scenarios - self.pest_df_multiple = parmest.Estimator( - ABC_model, [data_df, data_df], theta_names - ) - self.pest_dict_multiple = parmest.Estimator( - ABC_model, [data_dict, data_dict], theta_names - ) - - # Create an instance of the model - self.m_df = ABC_model(data_df) - self.m_dict = ABC_model(data_dict) - - def test_dataformats(self): - obj1, theta1 = self.pest_df.theta_est() - obj2, theta2 = self.pest_dict.theta_est() - - self.assertAlmostEqual(obj1, obj2, places=6) - self.assertAlmostEqual(theta1["k1"], theta2["k1"], places=6) - self.assertAlmostEqual(theta1["k2"], theta2["k2"], places=6) - - def test_return_continuous_set(self): - """ - test if ContinuousSet elements are returned correctly from theta_est() - """ - obj1, theta1, return_vals1 = self.pest_df.theta_est(return_values=["time"]) - obj2, theta2, return_vals2 = self.pest_dict.theta_est(return_values=["time"]) - self.assertAlmostEqual(return_vals1["time"].loc[0][18], 2.368, places=3) - self.assertAlmostEqual(return_vals2["time"].loc[0][18], 2.368, places=3) - - def test_return_continuous_set_multiple_datasets(self): - """ - test if ContinuousSet elements are returned correctly from theta_est() - """ - obj1, theta1, return_vals1 = self.pest_df_multiple.theta_est( - return_values=["time"] - ) - obj2, theta2, return_vals2 = self.pest_dict_multiple.theta_est( - return_values=["time"] - ) - self.assertAlmostEqual(return_vals1["time"].loc[1][18], 2.368, places=3) - self.assertAlmostEqual(return_vals2["time"].loc[1][18], 2.368, places=3) - - def test_covariance(self): - from pyomo.contrib.interior_point.inverse_reduced_hessian import ( - inv_reduced_hessian_barrier, - ) - - # Number of datapoints. - # 3 data components (ca, cb, cc), 20 timesteps, 1 scenario = 60 - # In this example, this is the number of data points in data_df, but that's - # only because the data is indexed by time and contains no additional information. - n = 60 - - # Compute covariance using parmest - obj, theta, cov = self.pest_df.theta_est(calc_cov=True, cov_n=n) - - # Compute covariance using interior_point - vars_list = [self.m_df.k1, self.m_df.k2] - solve_result, inv_red_hes = inv_reduced_hessian_barrier( - self.m_df, independent_variables=vars_list, tee=True - ) - l = len(vars_list) - cov_interior_point = 2 * obj / (n - l) * inv_red_hes - cov_interior_point = pd.DataFrame( - cov_interior_point, ["k1", "k2"], ["k1", "k2"] - ) - - cov_diff = (cov - cov_interior_point).abs().sum().sum() - - self.assertTrue(cov.loc["k1", "k1"] > 0) - self.assertTrue(cov.loc["k2", "k2"] > 0) - self.assertAlmostEqual(cov_diff, 0, places=6) - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") -class TestSquareInitialization_RooneyBiegler(unittest.TestCase): - def setUp(self): - from pyomo.contrib.parmest.deprecated.examples.rooney_biegler.rooney_biegler_with_constraint import ( - rooney_biegler_model_with_constraint, - ) - - # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=["hour", "y"], - ) - - theta_names = ["asymptote", "rate_constant"] - - def SSE(model, data): - expr = sum( - (data.y[i] - model.response_function[data.hour[i]]) ** 2 - for i in data.index - ) - return expr - - solver_options = {"tol": 1e-8} - - self.data = data - self.pest = parmest.Estimator( - rooney_biegler_model_with_constraint, - data, - theta_names, - SSE, - solver_options=solver_options, - tee=True, - ) - - def test_theta_est_with_square_initialization(self): - obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) - objval, thetavals = self.pest.theta_est() - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - thetavals["asymptote"], 19.1426, places=2 - ) # 19.1426 from the paper - self.assertAlmostEqual( - thetavals["rate_constant"], 0.5311, places=2 - ) # 0.5311 from the paper - - def test_theta_est_with_square_initialization_and_custom_init_theta(self): - theta_vals_init = pd.DataFrame( - data=[[19.0, 0.5]], columns=["asymptote", "rate_constant"] - ) - obj_init = self.pest.objective_at_theta( - theta_values=theta_vals_init, initialize_parmest_model=True - ) - objval, thetavals = self.pest.theta_est() - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - thetavals["asymptote"], 19.1426, places=2 - ) # 19.1426 from the paper - self.assertAlmostEqual( - thetavals["rate_constant"], 0.5311, places=2 - ) # 0.5311 from the paper - - def test_theta_est_with_square_initialization_diagnostic_mode_true(self): - self.pest.diagnostic_mode = True - obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) - objval, thetavals = self.pest.theta_est() - - self.assertAlmostEqual(objval, 4.3317112, places=2) - self.assertAlmostEqual( - thetavals["asymptote"], 19.1426, places=2 - ) # 19.1426 from the paper - self.assertAlmostEqual( - thetavals["rate_constant"], 0.5311, places=2 - ) # 0.5311 from the paper - - self.pest.diagnostic_mode = False - - -if __name__ == "__main__": - unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py b/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py deleted file mode 100644 index 54cbe80f73c..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/test_scenariocreator.py +++ /dev/null @@ -1,146 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.common.dependencies import pandas as pd, pandas_available - -uuid_available = True -try: - import uuid -except: - uuid_available = False - -import pyomo.common.unittest as unittest -import os -import pyomo.contrib.parmest.parmest as parmest -import pyomo.contrib.parmest.scenariocreator as sc -import pyomo.environ as pyo -from pyomo.environ import SolverFactory - -ipopt_available = SolverFactory("ipopt").available() - -testdir = os.path.dirname(os.path.abspath(__file__)) - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") -class TestScenarioReactorDesign(unittest.TestCase): - def setUp(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, - ) - - # Data from the design - data = pd.DataFrame( - data=[ - [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], - [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], - [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], - [1.20, 10000, 3680.7, 1070.0, 1486.1, 1881.6], - [1.25, 10000, 3750.0, 1071.4, 1428.6, 1875.0], - [1.30, 10000, 3817.1, 1072.2, 1374.6, 1868.0], - [1.35, 10000, 3882.2, 1072.4, 1324.0, 1860.7], - [1.40, 10000, 3945.4, 1072.1, 1276.3, 1853.1], - [1.45, 10000, 4006.7, 1071.3, 1231.4, 1845.3], - [1.50, 10000, 4066.4, 1070.1, 1189.0, 1837.3], - [1.55, 10000, 4124.4, 1068.5, 1148.9, 1829.1], - [1.60, 10000, 4180.9, 1066.5, 1111.0, 1820.8], - [1.65, 10000, 4235.9, 1064.3, 1075.0, 1812.4], - [1.70, 10000, 4289.5, 1061.8, 1040.9, 1803.9], - [1.75, 10000, 4341.8, 1059.0, 1008.5, 1795.3], - [1.80, 10000, 4392.8, 1056.0, 977.7, 1786.7], - [1.85, 10000, 4442.6, 1052.8, 948.4, 1778.1], - [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], - [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], - ], - columns=["sv", "caf", "ca", "cb", "cc", "cd"], - ) - - theta_names = ["k1", "k2", "k3"] - - def SSE(model, data): - expr = ( - (float(data.iloc[0]["ca"]) - model.ca) ** 2 - + (float(data.iloc[0]["cb"]) - model.cb) ** 2 - + (float(data.iloc[0]["cc"]) - model.cc) ** 2 - + (float(data.iloc[0]["cd"]) - model.cd) ** 2 - ) - return expr - - self.pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) - - def test_scen_from_exps(self): - scenmaker = sc.ScenarioCreator(self.pest, "ipopt") - experimentscens = sc.ScenarioSet("Experiments") - scenmaker.ScenariosFromExperiments(experimentscens) - experimentscens.write_csv("delme_exp_csv.csv") - df = pd.read_csv("delme_exp_csv.csv") - os.remove("delme_exp_csv.csv") - # March '20: all reactor_design experiments have the same theta values! - k1val = df.loc[5].at["k1"] - self.assertAlmostEqual(k1val, 5.0 / 6.0, places=2) - tval = experimentscens.ScenarioNumber(0).ThetaVals["k1"] - self.assertAlmostEqual(tval, 5.0 / 6.0, places=2) - - @unittest.skipIf(not uuid_available, "The uuid module is not available") - def test_no_csv_if_empty(self): - # low level test of scenario sets - # verify that nothing is written, but no errors with empty set - - emptyset = sc.ScenarioSet("empty") - tfile = uuid.uuid4().hex + ".csv" - emptyset.write_csv(tfile) - self.assertFalse( - os.path.exists(tfile), "ScenarioSet wrote csv in spite of empty set" - ) - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") -class TestScenarioSemibatch(unittest.TestCase): - def setUp(self): - import pyomo.contrib.parmest.deprecated.examples.semibatch.semibatch as sb - import json - - # Vars to estimate in parmest - theta_names = ["k1", "k2", "E1", "E2"] - - self.fbase = os.path.join(testdir, "..", "examples", "semibatch") - # Data, list of dictionaries - data = [] - for exp_num in range(10): - fname = "exp" + str(exp_num + 1) + ".out" - fullname = os.path.join(self.fbase, fname) - with open(fullname, "r") as infile: - d = json.load(infile) - data.append(d) - - # Note, the model already includes a 'SecondStageCost' expression - # for the sum of squared error that will be used in parameter estimation - - self.pest = parmest.Estimator(sb.generate_model, data, theta_names) - - def test_semibatch_bootstrap(self): - scenmaker = sc.ScenarioCreator(self.pest, "ipopt") - bootscens = sc.ScenarioSet("Bootstrap") - numtomake = 2 - scenmaker.ScenariosFromBootstrap(bootscens, numtomake, seed=1134) - tval = bootscens.ScenarioNumber(0).ThetaVals["k1"] - self.assertAlmostEqual(tval, 20.64, places=1) - - -if __name__ == "__main__": - unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_solver.py b/pyomo/contrib/parmest/deprecated/tests/test_solver.py deleted file mode 100644 index eb655023b9b..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/test_solver.py +++ /dev/null @@ -1,75 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.common.dependencies import ( - numpy as np, - numpy_available, - pandas as pd, - pandas_available, - scipy, - scipy_available, - matplotlib, - matplotlib_available, -) - -import platform - -is_osx = platform.mac_ver()[0] != '' - -import pyomo.common.unittest as unittest -import os - -import pyomo.contrib.parmest.parmest as parmest -import pyomo.contrib.parmest as parmestbase -import pyomo.environ as pyo - -from pyomo.opt import SolverFactory - -ipopt_available = SolverFactory('ipopt').available() - -from pyomo.common.fileutils import find_library - -pynumero_ASL_available = False if find_library('pynumero_ASL') is None else True - -testdir = os.path.dirname(os.path.abspath(__file__)) - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") -class TestSolver(unittest.TestCase): - def setUp(self): - pass - - def test_ipopt_solve_with_stats(self): - from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import ( - rooney_biegler_model, - ) - from pyomo.contrib.parmest.utils import ipopt_solve_with_stats - - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], - ) - - model = rooney_biegler_model(data) - solver = pyo.SolverFactory('ipopt') - solver.solve(model) - - status_obj, solved, iters, time, regu = ipopt_solve_with_stats(model, solver) - - self.assertEqual(solved, True) - - -if __name__ == '__main__': - unittest.main() diff --git a/pyomo/contrib/parmest/deprecated/tests/test_utils.py b/pyomo/contrib/parmest/deprecated/tests/test_utils.py deleted file mode 100644 index 1a8247ddcc9..00000000000 --- a/pyomo/contrib/parmest/deprecated/tests/test_utils.py +++ /dev/null @@ -1,68 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.common.dependencies import pandas as pd, pandas_available - -import pyomo.environ as pyo -import pyomo.common.unittest as unittest -import pyomo.contrib.parmest.parmest as parmest -from pyomo.opt import SolverFactory - -ipopt_available = SolverFactory("ipopt").available() - - -@unittest.skipIf( - not parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", -) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") -class TestUtils(unittest.TestCase): - @classmethod - def setUpClass(self): - pass - - @classmethod - def tearDownClass(self): - pass - - @unittest.pytest.mark.expensive - def test_convert_param_to_var(self): - from pyomo.contrib.parmest.deprecated.examples.reactor_design.reactor_design import ( - reactor_design_model, - ) - - data = pd.DataFrame( - data=[ - [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], - [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], - [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], - ], - columns=["sv", "caf", "ca", "cb", "cc", "cd"], - ) - - theta_names = ["k1", "k2", "k3"] - - instance = reactor_design_model(data.loc[0]) - solver = pyo.SolverFactory("ipopt") - solver.solve(instance) - - instance_vars = parmest.utils.convert_params_to_vars( - instance, theta_names, fix_vars=True - ) - solver.solve(instance_vars) - - assert instance.k1() == instance_vars.k1() - assert instance.k2() == instance_vars.k2() - assert instance.k3() == instance_vars.k3() - - -if __name__ == "__main__": - unittest.main() diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 90d42e68910..9e5b480332d 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -75,7 +75,10 @@ import pyomo.contrib.parmest.graphics as graphics from pyomo.dae import ContinuousSet -import pyomo.contrib.parmest.deprecated.parmest as parmest_deprecated +from pyomo.common.deprecation import deprecated +from pyomo.common.deprecation import deprecation_warning + +DEPRECATION_VERSION = '6.7.0' parmest_available = numpy_available & pandas_available & scipy_available @@ -312,8 +315,8 @@ class Estimator(object): Provides options to the solver (also the name of an attribute) """ - # backwards compatible constructor will accept the old inputs - # from parmest_deprecated as well as the new inputs using experiment lists + # backwards compatible constructor will accept the old deprecated inputs + # as well as the new inputs using experiment lists def __init__(self, *args, **kwargs): # check that we have at least one argument @@ -322,11 +325,12 @@ def __init__(self, *args, **kwargs): # use deprecated interface self.pest_deprecated = None if callable(args[0]): - logger.warning( + deprecation_warning( 'Using deprecated parmest inputs (model_function, ' - + 'data, theta_names), please use experiment lists instead.' + + 'data, theta_names), please use experiment lists instead.', + version=DEPRECATION_VERSION, ) - self.pest_deprecated = parmest_deprecated.Estimator(*args, **kwargs) + self.pest_deprecated = DeprecatedEstimator(*args, **kwargs) return # check that we have a (non-empty) list of experiments @@ -1436,3 +1440,1112 @@ def confidence_region_test( return training_results, test_result else: return training_results + + +################################ +# deprecated functions/classes # +################################ + + +@deprecated(version=DEPRECATION_VERSION) +def group_data(data, groupby_column_name, use_mean=None): + """ + Group data by scenario + + Parameters + ---------- + data: DataFrame + Data + groupby_column_name: strings + Name of data column which contains scenario numbers + use_mean: list of column names or None, optional + Name of data columns which should be reduced to a single value per + scenario by taking the mean + + Returns + ---------- + grouped_data: list of dictionaries + Grouped data + """ + if use_mean is None: + use_mean_list = [] + else: + use_mean_list = use_mean + + grouped_data = [] + for exp_num, group in data.groupby(data[groupby_column_name]): + d = {} + for col in group.columns: + if col in use_mean_list: + d[col] = group[col].mean() + else: + d[col] = list(group[col]) + grouped_data.append(d) + + return grouped_data + + +class _DeprecatedSecondStageCostExpr(object): + """ + Class to pass objective expression into the Pyomo model + """ + + def __init__(self, ssc_function, data): + self._ssc_function = ssc_function + self._data = data + + def __call__(self, model): + return self._ssc_function(model, self._data) + + +class DeprecatedEstimator(object): + """ + Parameter estimation class + + Parameters + ---------- + model_function: function + Function that generates an instance of the Pyomo model using 'data' + as the input argument + data: pd.DataFrame, list of dictionaries, list of dataframes, or list of json file names + Data that is used to build an instance of the Pyomo model and build + the objective function + theta_names: list of strings + List of Var names to estimate + obj_function: function, optional + Function used to formulate parameter estimation objective, generally + sum of squared error between measurements and model variables. + If no function is specified, the model is used + "as is" and should be defined with a "FirstStageCost" and + "SecondStageCost" expression that are used to build an objective. + tee: bool, optional + Indicates that ef solver output should be teed + diagnostic_mode: bool, optional + If True, print diagnostics from the solver + solver_options: dict, optional + Provides options to the solver (also the name of an attribute) + """ + + def __init__( + self, + model_function, + data, + theta_names, + obj_function=None, + tee=False, + diagnostic_mode=False, + solver_options=None, + ): + self.model_function = model_function + + assert isinstance( + data, (list, pd.DataFrame) + ), "Data must be a list or DataFrame" + # convert dataframe into a list of dataframes, each row = one scenario + if isinstance(data, pd.DataFrame): + self.callback_data = [ + data.loc[i, :].to_frame().transpose() for i in data.index + ] + else: + self.callback_data = data + assert isinstance( + self.callback_data[0], (dict, pd.DataFrame, str) + ), "The scenarios in data must be a dictionary, DataFrame or filename" + + if len(theta_names) == 0: + self.theta_names = ['parmest_dummy_var'] + else: + self.theta_names = theta_names + + self.obj_function = obj_function + self.tee = tee + self.diagnostic_mode = diagnostic_mode + self.solver_options = solver_options + + self._second_stage_cost_exp = "SecondStageCost" + # boolean to indicate if model is initialized using a square solve + self.model_initialized = False + + def _return_theta_names(self): + """ + Return list of fitted model parameter names + """ + # if fitted model parameter names differ from theta_names created when Estimator object is created + if hasattr(self, 'theta_names_updated'): + return self.theta_names_updated + + else: + return ( + self.theta_names + ) # default theta_names, created when Estimator object is created + + def _create_parmest_model(self, data): + """ + Modify the Pyomo model for parameter estimation + """ + model = self.model_function(data) + + if (len(self.theta_names) == 1) and ( + self.theta_names[0] == 'parmest_dummy_var' + ): + model.parmest_dummy_var = pyo.Var(initialize=1.0) + + # Add objective function (optional) + if self.obj_function: + for obj in model.component_objects(pyo.Objective): + if obj.name in ["Total_Cost_Objective"]: + raise RuntimeError( + "Parmest will not override the existing model Objective named " + + obj.name + ) + obj.deactivate() + + for expr in model.component_data_objects(pyo.Expression): + if expr.name in ["FirstStageCost", "SecondStageCost"]: + raise RuntimeError( + "Parmest will not override the existing model Expression named " + + expr.name + ) + model.FirstStageCost = pyo.Expression(expr=0) + model.SecondStageCost = pyo.Expression( + rule=_DeprecatedSecondStageCostExpr(self.obj_function, data) + ) + + def TotalCost_rule(model): + return model.FirstStageCost + model.SecondStageCost + + model.Total_Cost_Objective = pyo.Objective( + rule=TotalCost_rule, sense=pyo.minimize + ) + + # Convert theta Params to Vars, and unfix theta Vars + model = utils.convert_params_to_vars(model, self.theta_names) + + # Update theta names list to use CUID string representation + for i, theta in enumerate(self.theta_names): + var_cuid = ComponentUID(theta) + var_validate = var_cuid.find_component_on(model) + if var_validate is None: + logger.warning( + "theta_name[%s] (%s) was not found on the model", (i, theta) + ) + else: + try: + # If the component is not a variable, + # this will generate an exception (and the warning + # in the 'except') + var_validate.unfix() + self.theta_names[i] = repr(var_cuid) + except: + logger.warning(theta + ' is not a variable') + + self.parmest_model = model + + return model + + def _instance_creation_callback(self, experiment_number=None, cb_data=None): + # cb_data is a list of dictionaries, list of dataframes, OR list of json file names + exp_data = cb_data[experiment_number] + if isinstance(exp_data, (dict, pd.DataFrame)): + pass + elif isinstance(exp_data, str): + try: + with open(exp_data, 'r') as infile: + exp_data = json.load(infile) + except: + raise RuntimeError(f'Could not read {exp_data} as json') + else: + raise RuntimeError(f'Unexpected data format for cb_data={cb_data}') + model = self._create_parmest_model(exp_data) + + return model + + def _Q_opt( + self, + ThetaVals=None, + solver="ef_ipopt", + return_values=[], + bootlist=None, + calc_cov=False, + cov_n=None, + ): + """ + Set up all thetas as first stage Vars, return resulting theta + values as well as the objective function value. + + """ + if solver == "k_aug": + raise RuntimeError("k_aug no longer supported.") + + # (Bootstrap scenarios will use indirection through the bootlist) + if bootlist is None: + scenario_numbers = list(range(len(self.callback_data))) + scen_names = ["Scenario{}".format(i) for i in scenario_numbers] + else: + scen_names = ["Scenario{}".format(i) for i in range(len(bootlist))] + + # tree_model.CallbackModule = None + outer_cb_data = dict() + outer_cb_data["callback"] = self._instance_creation_callback + if ThetaVals is not None: + outer_cb_data["ThetaVals"] = ThetaVals + if bootlist is not None: + outer_cb_data["BootList"] = bootlist + outer_cb_data["cb_data"] = self.callback_data # None is OK + outer_cb_data["theta_names"] = self.theta_names + + options = {"solver": "ipopt"} + scenario_creator_options = {"cb_data": outer_cb_data} + if use_mpisppy: + ef = sputils.create_EF( + scen_names, + _experiment_instance_creation_callback, + EF_name="_Q_opt", + suppress_warnings=True, + scenario_creator_kwargs=scenario_creator_options, + ) + else: + ef = local_ef.create_EF( + scen_names, + _experiment_instance_creation_callback, + EF_name="_Q_opt", + suppress_warnings=True, + scenario_creator_kwargs=scenario_creator_options, + ) + self.ef_instance = ef + + # Solve the extensive form with ipopt + if solver == "ef_ipopt": + if not calc_cov: + # Do not calculate the reduced hessian + + solver = SolverFactory('ipopt') + if self.solver_options is not None: + for key in self.solver_options: + solver.options[key] = self.solver_options[key] + + solve_result = solver.solve(self.ef_instance, tee=self.tee) + + # The import error will be raised when we attempt to use + # inv_reduced_hessian_barrier below. + # + # elif not asl_available: + # raise ImportError("parmest requires ASL to calculate the " + # "covariance matrix with solver 'ipopt'") + else: + # parmest makes the fitted parameters stage 1 variables + ind_vars = [] + for ndname, Var, solval in ef_nonants(ef): + ind_vars.append(Var) + # calculate the reduced hessian + (solve_result, inv_red_hes) = ( + inverse_reduced_hessian.inv_reduced_hessian_barrier( + self.ef_instance, + independent_variables=ind_vars, + solver_options=self.solver_options, + tee=self.tee, + ) + ) + + if self.diagnostic_mode: + print( + ' Solver termination condition = ', + str(solve_result.solver.termination_condition), + ) + + # assume all first stage are thetas... + thetavals = {} + for ndname, Var, solval in ef_nonants(ef): + # process the name + # the scenarios are blocks, so strip the scenario name + vname = Var.name[Var.name.find(".") + 1 :] + thetavals[vname] = solval + + objval = pyo.value(ef.EF_Obj) + + if calc_cov: + # Calculate the covariance matrix + + # Number of data points considered + n = cov_n + + # Extract number of fitted parameters + l = len(thetavals) + + # Assumption: Objective value is sum of squared errors + sse = objval + + '''Calculate covariance assuming experimental observation errors are + independent and follow a Gaussian + distribution with constant variance. + + The formula used in parmest was verified against equations (7-5-15) and + (7-5-16) in "Nonlinear Parameter Estimation", Y. Bard, 1974. + + This formula is also applicable if the objective is scaled by a constant; + the constant cancels out. (was scaled by 1/n because it computes an + expected value.) + ''' + cov = 2 * sse / (n - l) * inv_red_hes + cov = pd.DataFrame( + cov, index=thetavals.keys(), columns=thetavals.keys() + ) + + thetavals = pd.Series(thetavals) + + if len(return_values) > 0: + var_values = [] + if len(scen_names) > 1: # multiple scenarios + block_objects = self.ef_instance.component_objects( + Block, descend_into=False + ) + else: # single scenario + block_objects = [self.ef_instance] + for exp_i in block_objects: + vals = {} + for var in return_values: + exp_i_var = exp_i.find_component(str(var)) + if ( + exp_i_var is None + ): # we might have a block such as _mpisppy_data + continue + # if value to return is ContinuousSet + if type(exp_i_var) == ContinuousSet: + temp = list(exp_i_var) + else: + temp = [pyo.value(_) for _ in exp_i_var.values()] + if len(temp) == 1: + vals[var] = temp[0] + else: + vals[var] = temp + if len(vals) > 0: + var_values.append(vals) + var_values = pd.DataFrame(var_values) + if calc_cov: + return objval, thetavals, var_values, cov + else: + return objval, thetavals, var_values + + if calc_cov: + return objval, thetavals, cov + else: + return objval, thetavals + + else: + raise RuntimeError("Unknown solver in Q_Opt=" + solver) + + def _Q_at_theta(self, thetavals, initialize_parmest_model=False): + """ + Return the objective function value with fixed theta values. + + Parameters + ---------- + thetavals: dict + A dictionary of theta values. + + initialize_parmest_model: boolean + If True: Solve square problem instance, build extensive form of the model for + parameter estimation, and set flag model_initialized to True + + Returns + ------- + objectiveval: float + The objective function value. + thetavals: dict + A dictionary of all values for theta that were input. + solvertermination: Pyomo TerminationCondition + Tries to return the "worst" solver status across the scenarios. + pyo.TerminationCondition.optimal is the best and + pyo.TerminationCondition.infeasible is the worst. + """ + + optimizer = pyo.SolverFactory('ipopt') + + if len(thetavals) > 0: + dummy_cb = { + "callback": self._instance_creation_callback, + "ThetaVals": thetavals, + "theta_names": self._return_theta_names(), + "cb_data": self.callback_data, + } + else: + dummy_cb = { + "callback": self._instance_creation_callback, + "theta_names": self._return_theta_names(), + "cb_data": self.callback_data, + } + + if self.diagnostic_mode: + if len(thetavals) > 0: + print(' Compute objective at theta = ', str(thetavals)) + else: + print(' Compute objective at initial theta') + + # start block of code to deal with models with no constraints + # (ipopt will crash or complain on such problems without special care) + instance = _experiment_instance_creation_callback("FOO0", None, dummy_cb) + try: # deal with special problems so Ipopt will not crash + first = next(instance.component_objects(pyo.Constraint, active=True)) + active_constraints = True + except: + active_constraints = False + # end block of code to deal with models with no constraints + + WorstStatus = pyo.TerminationCondition.optimal + totobj = 0 + scenario_numbers = list(range(len(self.callback_data))) + if initialize_parmest_model: + # create dictionary to store pyomo model instances (scenarios) + scen_dict = dict() + + for snum in scenario_numbers: + sname = "scenario_NODE" + str(snum) + instance = _experiment_instance_creation_callback(sname, None, dummy_cb) + + if initialize_parmest_model: + # list to store fitted parameter names that will be unfixed + # after initialization + theta_init_vals = [] + # use appropriate theta_names member + theta_ref = self._return_theta_names() + + for i, theta in enumerate(theta_ref): + # Use parser in ComponentUID to locate the component + var_cuid = ComponentUID(theta) + var_validate = var_cuid.find_component_on(instance) + if var_validate is None: + logger.warning( + "theta_name %s was not found on the model", (theta) + ) + else: + try: + if len(thetavals) == 0: + var_validate.fix() + else: + var_validate.fix(thetavals[theta]) + theta_init_vals.append(var_validate) + except: + logger.warning( + 'Unable to fix model parameter value for %s (not a Pyomo model Var)', + (theta), + ) + + if active_constraints: + if self.diagnostic_mode: + print(' Experiment = ', snum) + print(' First solve with special diagnostics wrapper') + (status_obj, solved, iters, time, regu) = ( + utils.ipopt_solve_with_stats( + instance, optimizer, max_iter=500, max_cpu_time=120 + ) + ) + print( + " status_obj, solved, iters, time, regularization_stat = ", + str(status_obj), + str(solved), + str(iters), + str(time), + str(regu), + ) + + results = optimizer.solve(instance) + if self.diagnostic_mode: + print( + 'standard solve solver termination condition=', + str(results.solver.termination_condition), + ) + + if ( + results.solver.termination_condition + != pyo.TerminationCondition.optimal + ): + # DLW: Aug2018: not distinguishing "middlish" conditions + if WorstStatus != pyo.TerminationCondition.infeasible: + WorstStatus = results.solver.termination_condition + if initialize_parmest_model: + if self.diagnostic_mode: + print( + "Scenario {:d} infeasible with initialized parameter values".format( + snum + ) + ) + else: + if initialize_parmest_model: + if self.diagnostic_mode: + print( + "Scenario {:d} initialization successful with initial parameter values".format( + snum + ) + ) + if initialize_parmest_model: + # unfix parameters after initialization + for theta in theta_init_vals: + theta.unfix() + scen_dict[sname] = instance + else: + if initialize_parmest_model: + # unfix parameters after initialization + for theta in theta_init_vals: + theta.unfix() + scen_dict[sname] = instance + + objobject = getattr(instance, self._second_stage_cost_exp) + objval = pyo.value(objobject) + totobj += objval + + retval = totobj / len(scenario_numbers) # -1?? + if initialize_parmest_model and not hasattr(self, 'ef_instance'): + # create extensive form of the model using scenario dictionary + if len(scen_dict) > 0: + for scen in scen_dict.values(): + scen._mpisppy_probability = 1 / len(scen_dict) + + if use_mpisppy: + EF_instance = sputils._create_EF_from_scen_dict( + scen_dict, + EF_name="_Q_at_theta", + # suppress_warnings=True + ) + else: + EF_instance = local_ef._create_EF_from_scen_dict( + scen_dict, EF_name="_Q_at_theta", nonant_for_fixed_vars=True + ) + + self.ef_instance = EF_instance + # set self.model_initialized flag to True to skip extensive form model + # creation using theta_est() + self.model_initialized = True + + # return initialized theta values + if len(thetavals) == 0: + # use appropriate theta_names member + theta_ref = self._return_theta_names() + for i, theta in enumerate(theta_ref): + thetavals[theta] = theta_init_vals[i]() + + return retval, thetavals, WorstStatus + + def _get_sample_list(self, samplesize, num_samples, replacement=True): + samplelist = list() + + scenario_numbers = list(range(len(self.callback_data))) + + if num_samples is None: + # This could get very large + for i, l in enumerate(combinations(scenario_numbers, samplesize)): + samplelist.append((i, np.sort(l))) + else: + for i in range(num_samples): + attempts = 0 + unique_samples = 0 # check for duplicates in each sample + duplicate = False # check for duplicates between samples + while (unique_samples <= len(self._return_theta_names())) and ( + not duplicate + ): + sample = np.random.choice( + scenario_numbers, samplesize, replace=replacement + ) + sample = np.sort(sample).tolist() + unique_samples = len(np.unique(sample)) + if sample in samplelist: + duplicate = True + + attempts += 1 + if attempts > num_samples: # arbitrary timeout limit + raise RuntimeError( + """Internal error: timeout constructing + a sample, the dim of theta may be too + close to the samplesize""" + ) + + samplelist.append((i, sample)) + + return samplelist + + def theta_est( + self, solver="ef_ipopt", return_values=[], calc_cov=False, cov_n=None + ): + """ + Parameter estimation using all scenarios in the data + + Parameters + ---------- + solver: string, optional + Currently only "ef_ipopt" is supported. Default is "ef_ipopt". + return_values: list, optional + List of Variable names, used to return values from the model for data reconciliation + calc_cov: boolean, optional + If True, calculate and return the covariance matrix (only for "ef_ipopt" solver) + cov_n: int, optional + If calc_cov=True, then the user needs to supply the number of datapoints + that are used in the objective function + + Returns + ------- + objectiveval: float + The objective function value + thetavals: pd.Series + Estimated values for theta + variable values: pd.DataFrame + Variable values for each variable name in return_values (only for solver='ef_ipopt') + cov: pd.DataFrame + Covariance matrix of the fitted parameters (only for solver='ef_ipopt') + """ + assert isinstance(solver, str) + assert isinstance(return_values, list) + assert isinstance(calc_cov, bool) + if calc_cov: + assert isinstance( + cov_n, int + ), "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" + assert cov_n > len( + self._return_theta_names() + ), "The number of datapoints must be greater than the number of parameters to estimate" + + return self._Q_opt( + solver=solver, + return_values=return_values, + bootlist=None, + calc_cov=calc_cov, + cov_n=cov_n, + ) + + def theta_est_bootstrap( + self, + bootstrap_samples, + samplesize=None, + replacement=True, + seed=None, + return_samples=False, + ): + """ + Parameter estimation using bootstrap resampling of the data + + Parameters + ---------- + bootstrap_samples: int + Number of bootstrap samples to draw from the data + samplesize: int or None, optional + Size of each bootstrap sample. If samplesize=None, samplesize will be + set to the number of samples in the data + replacement: bool, optional + Sample with or without replacement + seed: int or None, optional + Random seed + return_samples: bool, optional + Return a list of sample numbers used in each bootstrap estimation + + Returns + ------- + bootstrap_theta: pd.DataFrame + Theta values for each sample and (if return_samples = True) + the sample numbers used in each estimation + """ + assert isinstance(bootstrap_samples, int) + assert isinstance(samplesize, (type(None), int)) + assert isinstance(replacement, bool) + assert isinstance(seed, (type(None), int)) + assert isinstance(return_samples, bool) + + if samplesize is None: + samplesize = len(self.callback_data) + + if seed is not None: + np.random.seed(seed) + + global_list = self._get_sample_list(samplesize, bootstrap_samples, replacement) + + task_mgr = utils.ParallelTaskManager(bootstrap_samples) + local_list = task_mgr.global_to_local_data(global_list) + + bootstrap_theta = list() + for idx, sample in local_list: + objval, thetavals = self._Q_opt(bootlist=list(sample)) + thetavals['samples'] = sample + bootstrap_theta.append(thetavals) + + global_bootstrap_theta = task_mgr.allgather_global_data(bootstrap_theta) + bootstrap_theta = pd.DataFrame(global_bootstrap_theta) + + if not return_samples: + del bootstrap_theta['samples'] + + return bootstrap_theta + + def theta_est_leaveNout( + self, lNo, lNo_samples=None, seed=None, return_samples=False + ): + """ + Parameter estimation where N data points are left out of each sample + + Parameters + ---------- + lNo: int + Number of data points to leave out for parameter estimation + lNo_samples: int + Number of leave-N-out samples. If lNo_samples=None, the maximum + number of combinations will be used + seed: int or None, optional + Random seed + return_samples: bool, optional + Return a list of sample numbers that were left out + + Returns + ------- + lNo_theta: pd.DataFrame + Theta values for each sample and (if return_samples = True) + the sample numbers left out of each estimation + """ + assert isinstance(lNo, int) + assert isinstance(lNo_samples, (type(None), int)) + assert isinstance(seed, (type(None), int)) + assert isinstance(return_samples, bool) + + samplesize = len(self.callback_data) - lNo + + if seed is not None: + np.random.seed(seed) + + global_list = self._get_sample_list(samplesize, lNo_samples, replacement=False) + + task_mgr = utils.ParallelTaskManager(len(global_list)) + local_list = task_mgr.global_to_local_data(global_list) + + lNo_theta = list() + for idx, sample in local_list: + objval, thetavals = self._Q_opt(bootlist=list(sample)) + lNo_s = list(set(range(len(self.callback_data))) - set(sample)) + thetavals['lNo'] = np.sort(lNo_s) + lNo_theta.append(thetavals) + + global_bootstrap_theta = task_mgr.allgather_global_data(lNo_theta) + lNo_theta = pd.DataFrame(global_bootstrap_theta) + + if not return_samples: + del lNo_theta['lNo'] + + return lNo_theta + + def leaveNout_bootstrap_test( + self, lNo, lNo_samples, bootstrap_samples, distribution, alphas, seed=None + ): + """ + Leave-N-out bootstrap test to compare theta values where N data points are + left out to a bootstrap analysis using the remaining data, + results indicate if theta is within a confidence region + determined by the bootstrap analysis + + Parameters + ---------- + lNo: int + Number of data points to leave out for parameter estimation + lNo_samples: int + Leave-N-out sample size. If lNo_samples=None, the maximum number + of combinations will be used + bootstrap_samples: int: + Bootstrap sample size + distribution: string + Statistical distribution used to define a confidence region, + options = 'MVN' for multivariate_normal, 'KDE' for gaussian_kde, + and 'Rect' for rectangular. + alphas: list + List of alpha values used to determine if theta values are inside + or outside the region. + seed: int or None, optional + Random seed + + Returns + ---------- + List of tuples with one entry per lNo_sample: + + * The first item in each tuple is the list of N samples that are left + out. + * The second item in each tuple is a DataFrame of theta estimated using + the N samples. + * The third item in each tuple is a DataFrame containing results from + the bootstrap analysis using the remaining samples. + + For each DataFrame a column is added for each value of alpha which + indicates if the theta estimate is in (True) or out (False) of the + alpha region for a given distribution (based on the bootstrap results) + """ + assert isinstance(lNo, int) + assert isinstance(lNo_samples, (type(None), int)) + assert isinstance(bootstrap_samples, int) + assert distribution in ['Rect', 'MVN', 'KDE'] + assert isinstance(alphas, list) + assert isinstance(seed, (type(None), int)) + + if seed is not None: + np.random.seed(seed) + + data = self.callback_data.copy() + + global_list = self._get_sample_list(lNo, lNo_samples, replacement=False) + + results = [] + for idx, sample in global_list: + # Reset callback_data to only include the sample + self.callback_data = [data[i] for i in sample] + + obj, theta = self.theta_est() + + # Reset callback_data to include all scenarios except the sample + self.callback_data = [data[i] for i in range(len(data)) if i not in sample] + + bootstrap_theta = self.theta_est_bootstrap(bootstrap_samples) + + training, test = self.confidence_region_test( + bootstrap_theta, + distribution=distribution, + alphas=alphas, + test_theta_values=theta, + ) + + results.append((sample, test, training)) + + # Reset callback_data (back to full data set) + self.callback_data = data + + return results + + def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): + """ + Objective value for each theta + + Parameters + ---------- + theta_values: pd.DataFrame, columns=theta_names + Values of theta used to compute the objective + + initialize_parmest_model: boolean + If True: Solve square problem instance, build extensive form of the model for + parameter estimation, and set flag model_initialized to True + + + Returns + ------- + obj_at_theta: pd.DataFrame + Objective value for each theta (infeasible solutions are + omitted). + """ + if len(self.theta_names) == 1 and self.theta_names[0] == 'parmest_dummy_var': + pass # skip assertion if model has no fitted parameters + else: + # create a local instance of the pyomo model to access model variables and parameters + model_temp = self._create_parmest_model(self.callback_data[0]) + model_theta_list = [] # list to store indexed and non-indexed parameters + # iterate over original theta_names + for theta_i in self.theta_names: + var_cuid = ComponentUID(theta_i) + var_validate = var_cuid.find_component_on(model_temp) + # check if theta in theta_names are indexed + try: + # get component UID of Set over which theta is defined + set_cuid = ComponentUID(var_validate.index_set()) + # access and iterate over the Set to generate theta names as they appear + # in the pyomo model + set_validate = set_cuid.find_component_on(model_temp) + for s in set_validate: + self_theta_temp = repr(var_cuid) + "[" + repr(s) + "]" + # generate list of theta names + model_theta_list.append(self_theta_temp) + # if theta is not indexed, copy theta name to list as-is + except AttributeError: + self_theta_temp = repr(var_cuid) + model_theta_list.append(self_theta_temp) + except: + raise + # if self.theta_names is not the same as temp model_theta_list, + # create self.theta_names_updated + if set(self.theta_names) == set(model_theta_list) and len( + self.theta_names + ) == set(model_theta_list): + pass + else: + self.theta_names_updated = model_theta_list + + if theta_values is None: + all_thetas = {} # dictionary to store fitted variables + # use appropriate theta names member + theta_names = self._return_theta_names() + else: + assert isinstance(theta_values, pd.DataFrame) + # for parallel code we need to use lists and dicts in the loop + theta_names = theta_values.columns + # # check if theta_names are in model + for theta in list(theta_names): + theta_temp = theta.replace("'", "") # cleaning quotes from theta_names + + assert theta_temp in [ + t.replace("'", "") for t in model_theta_list + ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( + theta_temp, model_theta_list + ) + assert len(list(theta_names)) == len(model_theta_list) + + all_thetas = theta_values.to_dict('records') + + if all_thetas: + task_mgr = utils.ParallelTaskManager(len(all_thetas)) + local_thetas = task_mgr.global_to_local_data(all_thetas) + else: + if initialize_parmest_model: + task_mgr = utils.ParallelTaskManager( + 1 + ) # initialization performed using just 1 set of theta values + # walk over the mesh, return objective function + all_obj = list() + if len(all_thetas) > 0: + for Theta in local_thetas: + obj, thetvals, worststatus = self._Q_at_theta( + Theta, initialize_parmest_model=initialize_parmest_model + ) + if worststatus != pyo.TerminationCondition.infeasible: + all_obj.append(list(Theta.values()) + [obj]) + # DLW, Aug2018: should we also store the worst solver status? + else: + obj, thetvals, worststatus = self._Q_at_theta( + thetavals={}, initialize_parmest_model=initialize_parmest_model + ) + if worststatus != pyo.TerminationCondition.infeasible: + all_obj.append(list(thetvals.values()) + [obj]) + + global_all_obj = task_mgr.allgather_global_data(all_obj) + dfcols = list(theta_names) + ['obj'] + obj_at_theta = pd.DataFrame(data=global_all_obj, columns=dfcols) + return obj_at_theta + + def likelihood_ratio_test( + self, obj_at_theta, obj_value, alphas, return_thresholds=False + ): + r""" + Likelihood ratio test to identify theta values within a confidence + region using the :math:`\chi^2` distribution + + Parameters + ---------- + obj_at_theta: pd.DataFrame, columns = theta_names + 'obj' + Objective values for each theta value (returned by + objective_at_theta) + obj_value: int or float + Objective value from parameter estimation using all data + alphas: list + List of alpha values to use in the chi2 test + return_thresholds: bool, optional + Return the threshold value for each alpha + + Returns + ------- + LR: pd.DataFrame + Objective values for each theta value along with True or False for + each alpha + thresholds: pd.Series + If return_threshold = True, the thresholds are also returned. + """ + assert isinstance(obj_at_theta, pd.DataFrame) + assert isinstance(obj_value, (int, float)) + assert isinstance(alphas, list) + assert isinstance(return_thresholds, bool) + + LR = obj_at_theta.copy() + S = len(self.callback_data) + thresholds = {} + for a in alphas: + chi2_val = scipy.stats.chi2.ppf(a, 2) + thresholds[a] = obj_value * ((chi2_val / (S - 2)) + 1) + LR[a] = LR['obj'] < thresholds[a] + + thresholds = pd.Series(thresholds) + + if return_thresholds: + return LR, thresholds + else: + return LR + + def confidence_region_test( + self, theta_values, distribution, alphas, test_theta_values=None + ): + """ + Confidence region test to determine if theta values are within a + rectangular, multivariate normal, or Gaussian kernel density distribution + for a range of alpha values + + Parameters + ---------- + theta_values: pd.DataFrame, columns = theta_names + Theta values used to generate a confidence region + (generally returned by theta_est_bootstrap) + distribution: string + Statistical distribution used to define a confidence region, + options = 'MVN' for multivariate_normal, 'KDE' for gaussian_kde, + and 'Rect' for rectangular. + alphas: list + List of alpha values used to determine if theta values are inside + or outside the region. + test_theta_values: pd.Series or pd.DataFrame, keys/columns = theta_names, optional + Additional theta values that are compared to the confidence region + to determine if they are inside or outside. + + Returns + training_results: pd.DataFrame + Theta value used to generate the confidence region along with True + (inside) or False (outside) for each alpha + test_results: pd.DataFrame + If test_theta_values is not None, returns test theta value along + with True (inside) or False (outside) for each alpha + """ + assert isinstance(theta_values, pd.DataFrame) + assert distribution in ['Rect', 'MVN', 'KDE'] + assert isinstance(alphas, list) + assert isinstance( + test_theta_values, (type(None), dict, pd.Series, pd.DataFrame) + ) + + if isinstance(test_theta_values, (dict, pd.Series)): + test_theta_values = pd.Series(test_theta_values).to_frame().transpose() + + training_results = theta_values.copy() + + if test_theta_values is not None: + test_result = test_theta_values.copy() + + for a in alphas: + if distribution == 'Rect': + lb, ub = graphics.fit_rect_dist(theta_values, a) + training_results[a] = (theta_values > lb).all(axis=1) & ( + theta_values < ub + ).all(axis=1) + + if test_theta_values is not None: + # use upper and lower bound from the training set + test_result[a] = (test_theta_values > lb).all(axis=1) & ( + test_theta_values < ub + ).all(axis=1) + + elif distribution == 'MVN': + dist = graphics.fit_mvn_dist(theta_values) + Z = dist.pdf(theta_values) + score = scipy.stats.scoreatpercentile(Z, (1 - a) * 100) + training_results[a] = Z >= score + + if test_theta_values is not None: + # use score from the training set + Z = dist.pdf(test_theta_values) + test_result[a] = Z >= score + + elif distribution == 'KDE': + dist = graphics.fit_kde_dist(theta_values) + Z = dist.pdf(theta_values.transpose()) + score = scipy.stats.scoreatpercentile(Z, (1 - a) * 100) + training_results[a] = Z >= score + + if test_theta_values is not None: + # use score from the training set + Z = dist.pdf(test_theta_values.transpose()) + test_result[a] = Z >= score + + if test_theta_values is not None: + return training_results, test_result + else: + return training_results diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 76ea2f2ab81..434e15e7f31 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -14,7 +14,10 @@ import pyomo.environ as pyo -import pyomo.contrib.parmest.deprecated.scenariocreator as scen_deprecated +from pyomo.common.deprecation import deprecated +from pyomo.common.deprecation import deprecation_warning + +DEPRECATION_VERSION = '6.7.0' import logging @@ -129,11 +132,12 @@ def __init__(self, pest, solvername): # is this a deprecated pest object? self.scen_deprecated = None if pest.pest_deprecated is not None: - logger.warning( + deprecation_warning( "Using a deprecated parmest object for scenario " - + "creator, please recreate object using experiment lists." + + "creator, please recreate object using experiment lists.", + version=DEPRECATION_VERSION, ) - self.scen_deprecated = scen_deprecated.ScenarioCreator( + self.scen_deprecated = ScenarioCreatorDeprecated( pest.pest_deprecated, solvername ) else: @@ -190,3 +194,65 @@ def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) addtoSet.append_bootstrap(bootstrap_thetas) + + +################################ +# deprecated functions/classes # +################################ + + +class ScenarioCreatorDeprecated(object): + """Create scenarios from parmest. + + Args: + pest (Estimator): the parmest object + solvername (str): name of the solver (e.g. "ipopt") + + """ + + def __init__(self, pest, solvername): + self.pest = pest + self.solvername = solvername + + def ScenariosFromExperiments(self, addtoSet): + """Creates new self.Scenarios list using the experiments only. + + Args: + addtoSet (ScenarioSet): the scenarios will be added to this set + Returns: + a ScenarioSet + """ + + # assert isinstance(addtoSet, ScenarioSet) + + scenario_numbers = list(range(len(self.pest.callback_data))) + + prob = 1.0 / len(scenario_numbers) + for exp_num in scenario_numbers: + ##print("Experiment number=", exp_num) + model = self.pest._instance_creation_callback( + exp_num, self.pest.callback_data + ) + opt = pyo.SolverFactory(self.solvername) + results = opt.solve(model) # solves and updates model + ## pyo.check_termination_optimal(results) + ThetaVals = dict() + for theta in self.pest.theta_names: + tvar = eval('model.' + theta) + tval = pyo.value(tvar) + ##print(" theta, tval=", tvar, tval) + ThetaVals[theta] = tval + addtoSet.addone(ParmestScen("ExpScen" + str(exp_num), ThetaVals, prob)) + + def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): + """Creates new self.Scenarios list using the experiments only. + + Args: + addtoSet (ScenarioSet): the scenarios will be added to this set + numtomake (int) : number of scenarios to create + """ + + # assert isinstance(addtoSet, ScenarioSet) + + bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) + addtoSet.append_bootstrap(bootstrap_thetas) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 15264a18989..9c65a31352f 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -232,16 +232,16 @@ def test_theta_est_cov(self): # Covariance matrix self.assertAlmostEqual( - cov['asymptote']['asymptote'], 6.30579403, places=2 + cov["asymptote"]["asymptote"], 6.30579403, places=2 ) # 6.22864 from paper self.assertAlmostEqual( - cov['asymptote']['rate_constant'], -0.4395341, places=2 + cov["asymptote"]["rate_constant"], -0.4395341, places=2 ) # -0.4322 from paper self.assertAlmostEqual( - cov['rate_constant']['asymptote'], -0.4395341, places=2 + cov["rate_constant"]["asymptote"], -0.4395341, places=2 ) # -0.4322 from paper self.assertAlmostEqual( - cov['rate_constant']['rate_constant'], 0.04124, places=2 + cov["rate_constant"]["rate_constant"], 0.04124, places=2 ) # 0.04124 from paper """ Why does the covariance matrix from parmest not match the paper? Parmest is @@ -427,8 +427,8 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data['hour'])]) - m.experiment_outputs.update([(m.y, self.data['y'])]) + m.experiment_outputs.update([(m.hour, self.data["hour"])]) + m.experiment_outputs.update([(m.y, self.data["y"])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) @@ -506,8 +506,8 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data['hour'])]) - m.experiment_outputs.update([(m.y, self.data['y'])]) + m.experiment_outputs.update([(m.hour, self.data["hour"])]) + m.experiment_outputs.update([(m.y, self.data["y"])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) @@ -575,9 +575,9 @@ def check_rooney_biegler_results(self, objval, cov): # get indices in covariance matrix cov_cols = cov.columns.to_list() - asymptote_index = [idx for idx, s in enumerate(cov_cols) if 'asymptote' in s][0] + asymptote_index = [idx for idx, s in enumerate(cov_cols) if "asymptote" in s][0] rate_constant_index = [ - idx for idx, s in enumerate(cov_cols) if 'rate_constant' in s + idx for idx, s in enumerate(cov_cols) if "rate_constant" in s ][0] self.assertAlmostEqual(objval, 4.3317112, places=2) @@ -698,7 +698,7 @@ def setUp(self): solver_options = {"max_iter": 6000} self.pest = parmest.Estimator( - exp_list, obj_function='SSE', solver_options=solver_options + exp_list, obj_function="SSE", solver_options=solver_options ) def test_theta_est(self): @@ -1033,5 +1033,1031 @@ def test_theta_est_with_square_initialization_diagnostic_mode_true(self): self.pest.diagnostic_mode = False +########################### +# tests for deprecated UI # +########################### + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestRooneyBieglerDeprecated(unittest.TestCase): + def setUp(self): + + def rooney_biegler_model(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Var(initialize=15) + model.rate_constant = pyo.Var(initialize=0.5) + + def response_rule(m, h): + expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + def SSE_rule(m): + return sum( + (data.y[i] - m.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + + model.SSE = pyo.Objective(rule=SSE_rule, sense=pyo.minimize) + + return model + + # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=["hour", "y"], + ) + + theta_names = ["asymptote", "rate_constant"] + + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + return expr + + solver_options = {"tol": 1e-8} + + self.data = data + self.pest = parmest.Estimator( + rooney_biegler_model, + data, + theta_names, + SSE, + solver_options=solver_options, + tee=True, + ) + + def test_theta_est(self): + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + @unittest.skipIf( + not graphics.imports_available, "parmest.graphics imports are unavailable" + ) + def test_bootstrap(self): + objval, thetavals = self.pest.theta_est() + + num_bootstraps = 10 + theta_est = self.pest.theta_est_bootstrap(num_bootstraps, return_samples=True) + + num_samples = theta_est["samples"].apply(len) + self.assertTrue(len(theta_est.index), 10) + self.assertTrue(num_samples.equals(pd.Series([6] * 10))) + + del theta_est["samples"] + + # apply confidence region test + CR = self.pest.confidence_region_test(theta_est, "MVN", [0.5, 0.75, 1.0]) + + self.assertTrue(set(CR.columns) >= set([0.5, 0.75, 1.0])) + self.assertTrue(CR[0.5].sum() == 5) + self.assertTrue(CR[0.75].sum() == 7) + self.assertTrue(CR[1.0].sum() == 10) # all true + + graphics.pairwise_plot(theta_est) + graphics.pairwise_plot(theta_est, thetavals) + graphics.pairwise_plot(theta_est, thetavals, 0.8, ["MVN", "KDE", "Rect"]) + + @unittest.skipIf( + not graphics.imports_available, "parmest.graphics imports are unavailable" + ) + def test_likelihood_ratio(self): + objval, thetavals = self.pest.theta_est() + + asym = np.arange(10, 30, 2) + rate = np.arange(0, 1.5, 0.25) + theta_vals = pd.DataFrame( + list(product(asym, rate)), columns=self.pest._return_theta_names() + ) + + obj_at_theta = self.pest.objective_at_theta(theta_vals) + + LR = self.pest.likelihood_ratio_test(obj_at_theta, objval, [0.8, 0.9, 1.0]) + + self.assertTrue(set(LR.columns) >= set([0.8, 0.9, 1.0])) + self.assertTrue(LR[0.8].sum() == 6) + self.assertTrue(LR[0.9].sum() == 10) + self.assertTrue(LR[1.0].sum() == 60) # all true + + graphics.pairwise_plot(LR, thetavals, 0.8) + + def test_leaveNout(self): + lNo_theta = self.pest.theta_est_leaveNout(1) + self.assertTrue(lNo_theta.shape == (6, 2)) + + results = self.pest.leaveNout_bootstrap_test( + 1, None, 3, "Rect", [0.5, 1.0], seed=5436 + ) + self.assertTrue(len(results) == 6) # 6 lNo samples + i = 1 + samples = results[i][0] # list of N samples that are left out + lno_theta = results[i][1] + bootstrap_theta = results[i][2] + self.assertTrue(samples == [1]) # sample 1 was left out + self.assertTrue(lno_theta.shape[0] == 1) # lno estimate for sample 1 + self.assertTrue(set(lno_theta.columns) >= set([0.5, 1.0])) + self.assertTrue(lno_theta[1.0].sum() == 1) # all true + self.assertTrue(bootstrap_theta.shape[0] == 3) # bootstrap for sample 1 + self.assertTrue(bootstrap_theta[1.0].sum() == 3) # all true + + def test_diagnostic_mode(self): + self.pest.diagnostic_mode = True + + objval, thetavals = self.pest.theta_est() + + asym = np.arange(10, 30, 2) + rate = np.arange(0, 1.5, 0.25) + theta_vals = pd.DataFrame( + list(product(asym, rate)), columns=self.pest._return_theta_names() + ) + + obj_at_theta = self.pest.objective_at_theta(theta_vals) + + self.pest.diagnostic_mode = False + + @unittest.skip("Presently having trouble with mpiexec on appveyor") + def test_parallel_parmest(self): + """use mpiexec and mpi4py""" + p = str(parmestbase.__path__) + l = p.find("'") + r = p.find("'", l + 1) + parmestpath = p[l + 1 : r] + rbpath = ( + parmestpath + + os.sep + + "examples" + + os.sep + + "rooney_biegler" + + os.sep + + "rooney_biegler_parmest.py" + ) + rbpath = os.path.abspath(rbpath) # paranoia strikes deep... + rlist = ["mpiexec", "--allow-run-as-root", "-n", "2", sys.executable, rbpath] + if sys.version_info >= (3, 5): + ret = subprocess.run(rlist) + retcode = ret.returncode + else: + retcode = subprocess.call(rlist) + assert retcode == 0 + + @unittest.skip("Most folks don't have k_aug installed") + def test_theta_k_aug_for_Hessian(self): + # this will fail if k_aug is not installed + objval, thetavals, Hessian = self.pest.theta_est(solver="k_aug") + self.assertAlmostEqual(objval, 4.4675, places=2) + + @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") + @unittest.skipIf( + not parmest.inverse_reduced_hessian_available, + "Cannot test covariance matrix: required ASL dependency is missing", + ) + def test_theta_est_cov(self): + objval, thetavals, cov = self.pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + # Covariance matrix + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual(cov.iloc[1, 1], 0.04124, places=2) # 0.04124 from paper + + """ Why does the covariance matrix from parmest not match the paper? Parmest is + calculating the exact reduced Hessian. The paper (Rooney and Bielger, 2001) likely + employed the first order approximation common for nonlinear regression. The paper + values were verified with Scipy, which uses the same first order approximation. + The formula used in parmest was verified against equations (7-5-15) and (7-5-16) in + "Nonlinear Parameter Estimation", Y. Bard, 1974. + """ + + def test_cov_scipy_least_squares_comparison(self): + """ + Scipy results differ in the 3rd decimal place from the paper. It is possible + the paper used an alternative finite difference approximation for the Jacobian. + """ + + def model(theta, t): + """ + Model to be fitted y = model(theta, t) + Arguments: + theta: vector of fitted parameters + t: independent variable [hours] + + Returns: + y: model predictions [need to check paper for units] + """ + asymptote = theta[0] + rate_constant = theta[1] + + return asymptote * (1 - np.exp(-rate_constant * t)) + + def residual(theta, t, y): + """ + Calculate residuals + Arguments: + theta: vector of fitted parameters + t: independent variable [hours] + y: dependent variable [?] + """ + return y - model(theta, t) + + # define data + t = self.data["hour"].to_numpy() + y = self.data["y"].to_numpy() + + # define initial guess + theta_guess = np.array([15, 0.5]) + + ## solve with optimize.least_squares + sol = scipy.optimize.least_squares( + residual, theta_guess, method="trf", args=(t, y), verbose=2 + ) + theta_hat = sol.x + + self.assertAlmostEqual( + theta_hat[0], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual(theta_hat[1], 0.5311, places=2) # 0.5311 from the paper + + # calculate residuals + r = residual(theta_hat, t, y) + + # calculate variance of the residuals + # -2 because there are 2 fitted parameters + sigre = np.matmul(r.T, r / (len(y) - 2)) + + # approximate covariance + # Need to divide by 2 because optimize.least_squares scaled the objective by 1/2 + cov = sigre * np.linalg.inv(np.matmul(sol.jac.T, sol.jac)) + + self.assertAlmostEqual(cov[0, 0], 6.22864, places=2) # 6.22864 from paper + self.assertAlmostEqual(cov[0, 1], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 0], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper + + def test_cov_scipy_curve_fit_comparison(self): + """ + Scipy results differ in the 3rd decimal place from the paper. It is possible + the paper used an alternative finite difference approximation for the Jacobian. + """ + + ## solve with optimize.curve_fit + def model(t, asymptote, rate_constant): + return asymptote * (1 - np.exp(-rate_constant * t)) + + # define data + t = self.data["hour"].to_numpy() + y = self.data["y"].to_numpy() + + # define initial guess + theta_guess = np.array([15, 0.5]) + + theta_hat, cov = scipy.optimize.curve_fit(model, t, y, p0=theta_guess) + + self.assertAlmostEqual( + theta_hat[0], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual(theta_hat[1], 0.5311, places=2) # 0.5311 from the paper + + self.assertAlmostEqual(cov[0, 0], 6.22864, places=2) # 6.22864 from paper + self.assertAlmostEqual(cov[0, 1], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 0], -0.4322, places=2) # -0.4322 from paper + self.assertAlmostEqual(cov[1, 1], 0.04124, places=2) # 0.04124 from paper + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestModelVariantsDeprecated(unittest.TestCase): + def setUp(self): + self.data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=["hour", "y"], + ) + + def rooney_biegler_params(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Param(initialize=15, mutable=True) + model.rate_constant = pyo.Param(initialize=0.5, mutable=True) + + def response_rule(m, h): + expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def rooney_biegler_indexed_params(data): + model = pyo.ConcreteModel() + + model.param_names = pyo.Set(initialize=["asymptote", "rate_constant"]) + model.theta = pyo.Param( + model.param_names, + initialize={"asymptote": 15, "rate_constant": 0.5}, + mutable=True, + ) + + def response_rule(m, h): + expr = m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * h) + ) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def rooney_biegler_vars(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Var(initialize=15) + model.rate_constant = pyo.Var(initialize=0.5) + model.asymptote.fixed = True # parmest will unfix theta variables + model.rate_constant.fixed = True + + def response_rule(m, h): + expr = m.asymptote * (1 - pyo.exp(-m.rate_constant * h)) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def rooney_biegler_indexed_vars(data): + model = pyo.ConcreteModel() + + model.var_names = pyo.Set(initialize=["asymptote", "rate_constant"]) + model.theta = pyo.Var( + model.var_names, initialize={"asymptote": 15, "rate_constant": 0.5} + ) + model.theta["asymptote"].fixed = ( + True # parmest will unfix theta variables, even when they are indexed + ) + model.theta["rate_constant"].fixed = True + + def response_rule(m, h): + expr = m.theta["asymptote"] * ( + 1 - pyo.exp(-m.theta["rate_constant"] * h) + ) + return expr + + model.response_function = pyo.Expression(data.hour, rule=response_rule) + + return model + + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + return expr + + self.objective_function = SSE + + theta_vals = pd.DataFrame([20, 1], index=["asymptote", "rate_constant"]).T + theta_vals_index = pd.DataFrame( + [20, 1], index=["theta['asymptote']", "theta['rate_constant']"] + ).T + + self.input = { + "param": { + "model": rooney_biegler_params, + "theta_names": ["asymptote", "rate_constant"], + "theta_vals": theta_vals, + }, + "param_index": { + "model": rooney_biegler_indexed_params, + "theta_names": ["theta"], + "theta_vals": theta_vals_index, + }, + "vars": { + "model": rooney_biegler_vars, + "theta_names": ["asymptote", "rate_constant"], + "theta_vals": theta_vals, + }, + "vars_index": { + "model": rooney_biegler_indexed_vars, + "theta_names": ["theta"], + "theta_vals": theta_vals_index, + }, + "vars_quoted_index": { + "model": rooney_biegler_indexed_vars, + "theta_names": ["theta['asymptote']", "theta['rate_constant']"], + "theta_vals": theta_vals_index, + }, + "vars_str_index": { + "model": rooney_biegler_indexed_vars, + "theta_names": ["theta[asymptote]", "theta[rate_constant]"], + "theta_vals": theta_vals_index, + }, + } + + @unittest.skipIf(not pynumero_ASL_available, "pynumero ASL is not available") + @unittest.skipIf( + not parmest.inverse_reduced_hessian_available, + "Cannot test covariance matrix: required ASL dependency is missing", + ) + def test_parmest_basics(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + obj_at_theta = pest.objective_at_theta(parmest_input["theta_vals"]) + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + + def test_parmest_basics_with_initialize_parmest_model_option(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + obj_at_theta = pest.objective_at_theta( + parmest_input["theta_vals"], initialize_parmest_model=True + ) + + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + + def test_parmest_basics_with_square_problem_solve(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + obj_at_theta = pest.objective_at_theta( + parmest_input["theta_vals"], initialize_parmest_model=True + ) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + self.assertAlmostEqual(obj_at_theta["obj"][0], 16.531953, places=2) + + def test_parmest_basics_with_square_problem_solve_no_theta_vals(self): + for model_type, parmest_input in self.input.items(): + pest = parmest.Estimator( + parmest_input["model"], + self.data, + parmest_input["theta_names"], + self.objective_function, + ) + + obj_at_theta = pest.objective_at_theta(initialize_parmest_model=True) + + objval, thetavals, cov = pest.theta_est(calc_cov=True, cov_n=6) + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + cov.iloc[0, 0], 6.30579403, places=2 + ) # 6.22864 from paper + self.assertAlmostEqual( + cov.iloc[0, 1], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 0], -0.4395341, places=2 + ) # -0.4322 from paper + self.assertAlmostEqual( + cov.iloc[1, 1], 0.04193591, places=2 + ) # 0.04124 from paper + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestReactorDesignDeprecated(unittest.TestCase): + def setUp(self): + + def reactor_design_model(data): + # Create the concrete model + model = pyo.ConcreteModel() + + # Rate constants + model.k1 = pyo.Param( + initialize=5.0 / 6.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k2 = pyo.Param( + initialize=5.0 / 3.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k3 = pyo.Param( + initialize=1.0 / 6000.0, within=pyo.PositiveReals, mutable=True + ) # m^3/(gmol min) + + # Inlet concentration of A, gmol/m^3 + if isinstance(data, dict) or isinstance(data, pd.Series): + model.caf = pyo.Param( + initialize=float(data["caf"]), within=pyo.PositiveReals + ) + elif isinstance(data, pd.DataFrame): + model.caf = pyo.Param( + initialize=float(data.iloc[0]["caf"]), within=pyo.PositiveReals + ) + else: + raise ValueError("Unrecognized data type.") + + # Space velocity (flowrate/volume) + if isinstance(data, dict) or isinstance(data, pd.Series): + model.sv = pyo.Param( + initialize=float(data["sv"]), within=pyo.PositiveReals + ) + elif isinstance(data, pd.DataFrame): + model.sv = pyo.Param( + initialize=float(data.iloc[0]["sv"]), within=pyo.PositiveReals + ) + else: + raise ValueError("Unrecognized data type.") + + # Outlet concentration of each component + model.ca = pyo.Var(initialize=5000.0, within=pyo.PositiveReals) + model.cb = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cc = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cd = pyo.Var(initialize=1000.0, within=pyo.PositiveReals) + + # Objective + model.obj = pyo.Objective(expr=model.cb, sense=pyo.maximize) + + # Constraints + model.ca_bal = pyo.Constraint( + expr=( + 0 + == model.sv * model.caf + - model.sv * model.ca + - model.k1 * model.ca + - 2.0 * model.k3 * model.ca**2.0 + ) + ) + + model.cb_bal = pyo.Constraint( + expr=( + 0 + == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb + ) + ) + + model.cc_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cc + model.k2 * model.cb) + ) + + model.cd_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) + ) + + return model + + # Data from the design + data = pd.DataFrame( + data=[ + [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], + [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], + [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], + [1.20, 10000, 3680.7, 1070.0, 1486.1, 1881.6], + [1.25, 10000, 3750.0, 1071.4, 1428.6, 1875.0], + [1.30, 10000, 3817.1, 1072.2, 1374.6, 1868.0], + [1.35, 10000, 3882.2, 1072.4, 1324.0, 1860.7], + [1.40, 10000, 3945.4, 1072.1, 1276.3, 1853.1], + [1.45, 10000, 4006.7, 1071.3, 1231.4, 1845.3], + [1.50, 10000, 4066.4, 1070.1, 1189.0, 1837.3], + [1.55, 10000, 4124.4, 1068.5, 1148.9, 1829.1], + [1.60, 10000, 4180.9, 1066.5, 1111.0, 1820.8], + [1.65, 10000, 4235.9, 1064.3, 1075.0, 1812.4], + [1.70, 10000, 4289.5, 1061.8, 1040.9, 1803.9], + [1.75, 10000, 4341.8, 1059.0, 1008.5, 1795.3], + [1.80, 10000, 4392.8, 1056.0, 977.7, 1786.7], + [1.85, 10000, 4442.6, 1052.8, 948.4, 1778.1], + [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], + [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], + ], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], + ) + + theta_names = ["k1", "k2", "k3"] + + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + solver_options = {"max_iter": 6000} + + self.pest = parmest.Estimator( + reactor_design_model, data, theta_names, SSE, solver_options=solver_options + ) + + def test_theta_est(self): + # used in data reconciliation + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(thetavals["k1"], 5.0 / 6.0, places=4) + self.assertAlmostEqual(thetavals["k2"], 5.0 / 3.0, places=4) + self.assertAlmostEqual(thetavals["k3"], 1.0 / 6000.0, places=7) + + def test_return_values(self): + objval, thetavals, data_rec = self.pest.theta_est( + return_values=["ca", "cb", "cc", "cd", "caf"] + ) + self.assertAlmostEqual(data_rec["cc"].loc[18], 893.84924, places=3) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") +class TestReactorDesign_DAE_Deprecated(unittest.TestCase): + # Based on a reactor example in `Chemical Reactor Analysis and Design Fundamentals`, + # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/ + # https://sites.engineering.ucsb.edu/~jbraw/chemreacfun/fig-html/appendix/fig-A-10.html + + def setUp(self): + def ABC_model(data): + ca_meas = data["ca"] + cb_meas = data["cb"] + cc_meas = data["cc"] + + if isinstance(data, pd.DataFrame): + meas_t = data.index # time index + else: # dictionary + meas_t = list(ca_meas.keys()) # nested dictionary + + ca0 = 1.0 + cb0 = 0.0 + cc0 = 0.0 + + m = pyo.ConcreteModel() + + m.k1 = pyo.Var(initialize=0.5, bounds=(1e-4, 10)) + m.k2 = pyo.Var(initialize=3.0, bounds=(1e-4, 10)) + + m.time = dae.ContinuousSet(bounds=(0.0, 5.0), initialize=meas_t) + + # initialization and bounds + m.ca = pyo.Var(m.time, initialize=ca0, bounds=(-1e-3, ca0 + 1e-3)) + m.cb = pyo.Var(m.time, initialize=cb0, bounds=(-1e-3, ca0 + 1e-3)) + m.cc = pyo.Var(m.time, initialize=cc0, bounds=(-1e-3, ca0 + 1e-3)) + + m.dca = dae.DerivativeVar(m.ca, wrt=m.time) + m.dcb = dae.DerivativeVar(m.cb, wrt=m.time) + m.dcc = dae.DerivativeVar(m.cc, wrt=m.time) + + def _dcarate(m, t): + if t == 0: + return pyo.Constraint.Skip + else: + return m.dca[t] == -m.k1 * m.ca[t] + + m.dcarate = pyo.Constraint(m.time, rule=_dcarate) + + def _dcbrate(m, t): + if t == 0: + return pyo.Constraint.Skip + else: + return m.dcb[t] == m.k1 * m.ca[t] - m.k2 * m.cb[t] + + m.dcbrate = pyo.Constraint(m.time, rule=_dcbrate) + + def _dccrate(m, t): + if t == 0: + return pyo.Constraint.Skip + else: + return m.dcc[t] == m.k2 * m.cb[t] + + m.dccrate = pyo.Constraint(m.time, rule=_dccrate) + + def ComputeFirstStageCost_rule(m): + return 0 + + m.FirstStageCost = pyo.Expression(rule=ComputeFirstStageCost_rule) + + def ComputeSecondStageCost_rule(m): + return sum( + (m.ca[t] - ca_meas[t]) ** 2 + + (m.cb[t] - cb_meas[t]) ** 2 + + (m.cc[t] - cc_meas[t]) ** 2 + for t in meas_t + ) + + m.SecondStageCost = pyo.Expression(rule=ComputeSecondStageCost_rule) + + def total_cost_rule(model): + return model.FirstStageCost + model.SecondStageCost + + m.Total_Cost_Objective = pyo.Objective( + rule=total_cost_rule, sense=pyo.minimize + ) + + disc = pyo.TransformationFactory("dae.collocation") + disc.apply_to(m, nfe=20, ncp=2) + + return m + + # This example tests data formatted in 3 ways + # Each format holds 1 scenario + # 1. dataframe with time index + # 2. nested dictionary {ca: {t, val pairs}, ... } + data = [ + [0.000, 0.957, -0.031, -0.015], + [0.263, 0.557, 0.330, 0.044], + [0.526, 0.342, 0.512, 0.156], + [0.789, 0.224, 0.499, 0.310], + [1.053, 0.123, 0.428, 0.454], + [1.316, 0.079, 0.396, 0.556], + [1.579, 0.035, 0.303, 0.651], + [1.842, 0.029, 0.287, 0.658], + [2.105, 0.025, 0.221, 0.750], + [2.368, 0.017, 0.148, 0.854], + [2.632, -0.002, 0.182, 0.845], + [2.895, 0.009, 0.116, 0.893], + [3.158, -0.023, 0.079, 0.942], + [3.421, 0.006, 0.078, 0.899], + [3.684, 0.016, 0.059, 0.942], + [3.947, 0.014, 0.036, 0.991], + [4.211, -0.009, 0.014, 0.988], + [4.474, -0.030, 0.036, 0.941], + [4.737, 0.004, 0.036, 0.971], + [5.000, -0.024, 0.028, 0.985], + ] + data = pd.DataFrame(data, columns=["t", "ca", "cb", "cc"]) + data_df = data.set_index("t") + data_dict = { + "ca": {k: v for (k, v) in zip(data.t, data.ca)}, + "cb": {k: v for (k, v) in zip(data.t, data.cb)}, + "cc": {k: v for (k, v) in zip(data.t, data.cc)}, + } + + theta_names = ["k1", "k2"] + + self.pest_df = parmest.Estimator(ABC_model, [data_df], theta_names) + self.pest_dict = parmest.Estimator(ABC_model, [data_dict], theta_names) + + # Estimator object with multiple scenarios + self.pest_df_multiple = parmest.Estimator( + ABC_model, [data_df, data_df], theta_names + ) + self.pest_dict_multiple = parmest.Estimator( + ABC_model, [data_dict, data_dict], theta_names + ) + + # Create an instance of the model + self.m_df = ABC_model(data_df) + self.m_dict = ABC_model(data_dict) + + def test_dataformats(self): + obj1, theta1 = self.pest_df.theta_est() + obj2, theta2 = self.pest_dict.theta_est() + + self.assertAlmostEqual(obj1, obj2, places=6) + self.assertAlmostEqual(theta1["k1"], theta2["k1"], places=6) + self.assertAlmostEqual(theta1["k2"], theta2["k2"], places=6) + + def test_return_continuous_set(self): + """ + test if ContinuousSet elements are returned correctly from theta_est() + """ + obj1, theta1, return_vals1 = self.pest_df.theta_est(return_values=["time"]) + obj2, theta2, return_vals2 = self.pest_dict.theta_est(return_values=["time"]) + self.assertAlmostEqual(return_vals1["time"].loc[0][18], 2.368, places=3) + self.assertAlmostEqual(return_vals2["time"].loc[0][18], 2.368, places=3) + + def test_return_continuous_set_multiple_datasets(self): + """ + test if ContinuousSet elements are returned correctly from theta_est() + """ + obj1, theta1, return_vals1 = self.pest_df_multiple.theta_est( + return_values=["time"] + ) + obj2, theta2, return_vals2 = self.pest_dict_multiple.theta_est( + return_values=["time"] + ) + self.assertAlmostEqual(return_vals1["time"].loc[1][18], 2.368, places=3) + self.assertAlmostEqual(return_vals2["time"].loc[1][18], 2.368, places=3) + + def test_covariance(self): + from pyomo.contrib.interior_point.inverse_reduced_hessian import ( + inv_reduced_hessian_barrier, + ) + + # Number of datapoints. + # 3 data components (ca, cb, cc), 20 timesteps, 1 scenario = 60 + # In this example, this is the number of data points in data_df, but that's + # only because the data is indexed by time and contains no additional information. + n = 60 + + # Compute covariance using parmest + obj, theta, cov = self.pest_df.theta_est(calc_cov=True, cov_n=n) + + # Compute covariance using interior_point + vars_list = [self.m_df.k1, self.m_df.k2] + solve_result, inv_red_hes = inv_reduced_hessian_barrier( + self.m_df, independent_variables=vars_list, tee=True + ) + l = len(vars_list) + cov_interior_point = 2 * obj / (n - l) * inv_red_hes + cov_interior_point = pd.DataFrame( + cov_interior_point, ["k1", "k2"], ["k1", "k2"] + ) + + cov_diff = (cov - cov_interior_point).abs().sum().sum() + + self.assertTrue(cov.loc["k1", "k1"] > 0) + self.assertTrue(cov.loc["k2", "k2"] > 0) + self.assertAlmostEqual(cov_diff, 0, places=6) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestSquareInitialization_RooneyBiegler_Deprecated(unittest.TestCase): + def setUp(self): + + def rooney_biegler_model_with_constraint(data): + model = pyo.ConcreteModel() + + model.asymptote = pyo.Var(initialize=15) + model.rate_constant = pyo.Var(initialize=0.5) + model.response_function = pyo.Var(data.hour, initialize=0.0) + + # changed from expression to constraint + def response_rule(m, h): + return m.response_function[h] == m.asymptote * ( + 1 - pyo.exp(-m.rate_constant * h) + ) + + model.response_function_constraint = pyo.Constraint( + data.hour, rule=response_rule + ) + + def SSE_rule(m): + return sum( + (data.y[i] - m.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + + model.SSE = pyo.Objective(rule=SSE_rule, sense=pyo.minimize) + + return model + + # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], + columns=["hour", "y"], + ) + + theta_names = ["asymptote", "rate_constant"] + + def SSE(model, data): + expr = sum( + (data.y[i] - model.response_function[data.hour[i]]) ** 2 + for i in data.index + ) + return expr + + solver_options = {"tol": 1e-8} + + self.data = data + self.pest = parmest.Estimator( + rooney_biegler_model_with_constraint, + data, + theta_names, + SSE, + solver_options=solver_options, + tee=True, + ) + + def test_theta_est_with_square_initialization(self): + obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + def test_theta_est_with_square_initialization_and_custom_init_theta(self): + theta_vals_init = pd.DataFrame( + data=[[19.0, 0.5]], columns=["asymptote", "rate_constant"] + ) + obj_init = self.pest.objective_at_theta( + theta_values=theta_vals_init, initialize_parmest_model=True + ) + objval, thetavals = self.pest.theta_est() + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + def test_theta_est_with_square_initialization_diagnostic_mode_true(self): + self.pest.diagnostic_mode = True + obj_init = self.pest.objective_at_theta(initialize_parmest_model=True) + objval, thetavals = self.pest.theta_est() + + self.assertAlmostEqual(objval, 4.3317112, places=2) + self.assertAlmostEqual( + thetavals["asymptote"], 19.1426, places=2 + ) # 19.1426 from the paper + self.assertAlmostEqual( + thetavals["rate_constant"], 0.5311, places=2 + ) # 0.5311 from the paper + + self.pest.diagnostic_mode = False + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/parmest/tests/test_scenariocreator.py b/pyomo/contrib/parmest/tests/test_scenariocreator.py index 1f8ccdb20fe..af755e34b67 100644 --- a/pyomo/contrib/parmest/tests/test_scenariocreator.py +++ b/pyomo/contrib/parmest/tests/test_scenariocreator.py @@ -138,5 +138,453 @@ def test_semibatch_bootstrap(self): self.assertAlmostEqual(tval, 20.64, places=1) +########################### +# tests for deprecated UI # +########################### + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestScenarioReactorDesignDeprecated(unittest.TestCase): + def setUp(self): + + def reactor_design_model(data): + # Create the concrete model + model = pyo.ConcreteModel() + + # Rate constants + model.k1 = pyo.Param( + initialize=5.0 / 6.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k2 = pyo.Param( + initialize=5.0 / 3.0, within=pyo.PositiveReals, mutable=True + ) # min^-1 + model.k3 = pyo.Param( + initialize=1.0 / 6000.0, within=pyo.PositiveReals, mutable=True + ) # m^3/(gmol min) + + # Inlet concentration of A, gmol/m^3 + if isinstance(data, dict) or isinstance(data, pd.Series): + model.caf = pyo.Param( + initialize=float(data["caf"]), within=pyo.PositiveReals + ) + elif isinstance(data, pd.DataFrame): + model.caf = pyo.Param( + initialize=float(data.iloc[0]["caf"]), within=pyo.PositiveReals + ) + else: + raise ValueError("Unrecognized data type.") + + # Space velocity (flowrate/volume) + if isinstance(data, dict) or isinstance(data, pd.Series): + model.sv = pyo.Param( + initialize=float(data["sv"]), within=pyo.PositiveReals + ) + elif isinstance(data, pd.DataFrame): + model.sv = pyo.Param( + initialize=float(data.iloc[0]["sv"]), within=pyo.PositiveReals + ) + else: + raise ValueError("Unrecognized data type.") + + # Outlet concentration of each component + model.ca = pyo.Var(initialize=5000.0, within=pyo.PositiveReals) + model.cb = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cc = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cd = pyo.Var(initialize=1000.0, within=pyo.PositiveReals) + + # Objective + model.obj = pyo.Objective(expr=model.cb, sense=pyo.maximize) + + # Constraints + model.ca_bal = pyo.Constraint( + expr=( + 0 + == model.sv * model.caf + - model.sv * model.ca + - model.k1 * model.ca + - 2.0 * model.k3 * model.ca**2.0 + ) + ) + + model.cb_bal = pyo.Constraint( + expr=( + 0 + == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb + ) + ) + + model.cc_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cc + model.k2 * model.cb) + ) + + model.cd_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) + ) + + return model + + # Data from the design + data = pd.DataFrame( + data=[ + [1.05, 10000, 3458.4, 1060.8, 1683.9, 1898.5], + [1.10, 10000, 3535.1, 1064.8, 1613.3, 1893.4], + [1.15, 10000, 3609.1, 1067.8, 1547.5, 1887.8], + [1.20, 10000, 3680.7, 1070.0, 1486.1, 1881.6], + [1.25, 10000, 3750.0, 1071.4, 1428.6, 1875.0], + [1.30, 10000, 3817.1, 1072.2, 1374.6, 1868.0], + [1.35, 10000, 3882.2, 1072.4, 1324.0, 1860.7], + [1.40, 10000, 3945.4, 1072.1, 1276.3, 1853.1], + [1.45, 10000, 4006.7, 1071.3, 1231.4, 1845.3], + [1.50, 10000, 4066.4, 1070.1, 1189.0, 1837.3], + [1.55, 10000, 4124.4, 1068.5, 1148.9, 1829.1], + [1.60, 10000, 4180.9, 1066.5, 1111.0, 1820.8], + [1.65, 10000, 4235.9, 1064.3, 1075.0, 1812.4], + [1.70, 10000, 4289.5, 1061.8, 1040.9, 1803.9], + [1.75, 10000, 4341.8, 1059.0, 1008.5, 1795.3], + [1.80, 10000, 4392.8, 1056.0, 977.7, 1786.7], + [1.85, 10000, 4442.6, 1052.8, 948.4, 1778.1], + [1.90, 10000, 4491.3, 1049.4, 920.5, 1769.4], + [1.95, 10000, 4538.8, 1045.8, 893.9, 1760.8], + ], + columns=["sv", "caf", "ca", "cb", "cc", "cd"], + ) + + theta_names = ["k1", "k2", "k3"] + + def SSE(model, data): + expr = ( + (float(data.iloc[0]["ca"]) - model.ca) ** 2 + + (float(data.iloc[0]["cb"]) - model.cb) ** 2 + + (float(data.iloc[0]["cc"]) - model.cc) ** 2 + + (float(data.iloc[0]["cd"]) - model.cd) ** 2 + ) + return expr + + self.pest = parmest.Estimator(reactor_design_model, data, theta_names, SSE) + + def test_scen_from_exps(self): + scenmaker = sc.ScenarioCreator(self.pest, "ipopt") + experimentscens = sc.ScenarioSet("Experiments") + scenmaker.ScenariosFromExperiments(experimentscens) + experimentscens.write_csv("delme_exp_csv.csv") + df = pd.read_csv("delme_exp_csv.csv") + os.remove("delme_exp_csv.csv") + # March '20: all reactor_design experiments have the same theta values! + k1val = df.loc[5].at["k1"] + self.assertAlmostEqual(k1val, 5.0 / 6.0, places=2) + tval = experimentscens.ScenarioNumber(0).ThetaVals["k1"] + self.assertAlmostEqual(tval, 5.0 / 6.0, places=2) + + @unittest.skipIf(not uuid_available, "The uuid module is not available") + def test_no_csv_if_empty(self): + # low level test of scenario sets + # verify that nothing is written, but no errors with empty set + + emptyset = sc.ScenarioSet("empty") + tfile = uuid.uuid4().hex + ".csv" + emptyset.write_csv(tfile) + self.assertFalse( + os.path.exists(tfile), "ScenarioSet wrote csv in spite of empty set" + ) + + +@unittest.skipIf( + not parmest.parmest_available, + "Cannot test parmest: required dependencies are missing", +) +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestScenarioSemibatchDeprecated(unittest.TestCase): + def setUp(self): + + import json + from pyomo.environ import ( + ConcreteModel, + Set, + Param, + Var, + Constraint, + ConstraintList, + Expression, + Objective, + TransformationFactory, + SolverFactory, + exp, + minimize, + ) + from pyomo.dae import ContinuousSet, DerivativeVar + + def generate_model(data): + # if data is a file name, then load file first + if isinstance(data, str): + file_name = data + try: + with open(file_name, "r") as infile: + data = json.load(infile) + except: + raise RuntimeError(f"Could not read {file_name} as json") + + # unpack and fix the data + cameastemp = data["Ca_meas"] + cbmeastemp = data["Cb_meas"] + ccmeastemp = data["Cc_meas"] + trmeastemp = data["Tr_meas"] + + cameas = {} + cbmeas = {} + ccmeas = {} + trmeas = {} + for i in cameastemp.keys(): + cameas[float(i)] = cameastemp[i] + cbmeas[float(i)] = cbmeastemp[i] + ccmeas[float(i)] = ccmeastemp[i] + trmeas[float(i)] = trmeastemp[i] + + m = ConcreteModel() + + # + # Measurement Data + # + m.measT = Set(initialize=sorted(cameas.keys())) + m.Ca_meas = Param(m.measT, initialize=cameas) + m.Cb_meas = Param(m.measT, initialize=cbmeas) + m.Cc_meas = Param(m.measT, initialize=ccmeas) + m.Tr_meas = Param(m.measT, initialize=trmeas) + + # + # Parameters for semi-batch reactor model + # + m.R = Param(initialize=8.314) # kJ/kmol/K + m.Mwa = Param(initialize=50.0) # kg/kmol + m.rhor = Param(initialize=1000.0) # kg/m^3 + m.cpr = Param(initialize=3.9) # kJ/kg/K + m.Tf = Param(initialize=300) # K + m.deltaH1 = Param(initialize=-40000.0) # kJ/kmol + m.deltaH2 = Param(initialize=-50000.0) # kJ/kmol + m.alphaj = Param(initialize=0.8) # kJ/s/m^2/K + m.alphac = Param(initialize=0.7) # kJ/s/m^2/K + m.Aj = Param(initialize=5.0) # m^2 + m.Ac = Param(initialize=3.0) # m^2 + m.Vj = Param(initialize=0.9) # m^3 + m.Vc = Param(initialize=0.07) # m^3 + m.rhow = Param(initialize=700.0) # kg/m^3 + m.cpw = Param(initialize=3.1) # kJ/kg/K + m.Ca0 = Param(initialize=data["Ca0"]) # kmol/m^3) + m.Cb0 = Param(initialize=data["Cb0"]) # kmol/m^3) + m.Cc0 = Param(initialize=data["Cc0"]) # kmol/m^3) + m.Tr0 = Param(initialize=300.0) # K + m.Vr0 = Param(initialize=1.0) # m^3 + + m.time = ContinuousSet( + bounds=(0, 21600), initialize=m.measT + ) # Time in seconds + + # + # Control Inputs + # + def _initTc(m, t): + if t < 10800: + return data["Tc1"] + else: + return data["Tc2"] + + m.Tc = Param( + m.time, initialize=_initTc, default=_initTc + ) # bounds= (288,432) Cooling coil temp, control input + + def _initFa(m, t): + if t < 10800: + return data["Fa1"] + else: + return data["Fa2"] + + m.Fa = Param( + m.time, initialize=_initFa, default=_initFa + ) # bounds=(0,0.05) Inlet flow rate, control input + + # + # Parameters being estimated + # + m.k1 = Var(initialize=14, bounds=(2, 100)) # 1/s Actual: 15.01 + m.k2 = Var(initialize=90, bounds=(2, 150)) # 1/s Actual: 85.01 + m.E1 = Var( + initialize=27000.0, bounds=(25000, 40000) + ) # kJ/kmol Actual: 30000 + m.E2 = Var( + initialize=45000.0, bounds=(35000, 50000) + ) # kJ/kmol Actual: 40000 + # m.E1.fix(30000) + # m.E2.fix(40000) + + # + # Time dependent variables + # + m.Ca = Var(m.time, initialize=m.Ca0, bounds=(0, 25)) + m.Cb = Var(m.time, initialize=m.Cb0, bounds=(0, 25)) + m.Cc = Var(m.time, initialize=m.Cc0, bounds=(0, 25)) + m.Vr = Var(m.time, initialize=m.Vr0) + m.Tr = Var(m.time, initialize=m.Tr0) + m.Tj = Var( + m.time, initialize=310.0, bounds=(288, None) + ) # Cooling jacket temp, follows coil temp until failure + + # + # Derivatives in the model + # + m.dCa = DerivativeVar(m.Ca) + m.dCb = DerivativeVar(m.Cb) + m.dCc = DerivativeVar(m.Cc) + m.dVr = DerivativeVar(m.Vr) + m.dTr = DerivativeVar(m.Tr) + + # + # Differential Equations in the model + # + + def _dCacon(m, t): + if t == 0: + return Constraint.Skip + return ( + m.dCa[t] + == m.Fa[t] / m.Vr[t] - m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] + ) + + m.dCacon = Constraint(m.time, rule=_dCacon) + + def _dCbcon(m, t): + if t == 0: + return Constraint.Skip + return ( + m.dCb[t] + == m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[t] + - m.k2 * exp(-m.E2 / (m.R * m.Tr[t])) * m.Cb[t] + ) + + m.dCbcon = Constraint(m.time, rule=_dCbcon) + + def _dCccon(m, t): + if t == 0: + return Constraint.Skip + return m.dCc[t] == m.k2 * exp(-m.E2 / (m.R * m.Tr[t])) * m.Cb[t] + + m.dCccon = Constraint(m.time, rule=_dCccon) + + def _dVrcon(m, t): + if t == 0: + return Constraint.Skip + return m.dVr[t] == m.Fa[t] * m.Mwa / m.rhor + + m.dVrcon = Constraint(m.time, rule=_dVrcon) + + def _dTrcon(m, t): + if t == 0: + return Constraint.Skip + return m.rhor * m.cpr * m.dTr[t] == m.Fa[t] * m.Mwa * m.cpr / m.Vr[ + t + ] * (m.Tf - m.Tr[t]) - m.k1 * exp(-m.E1 / (m.R * m.Tr[t])) * m.Ca[ + t + ] * m.deltaH1 - m.k2 * exp( + -m.E2 / (m.R * m.Tr[t]) + ) * m.Cb[ + t + ] * m.deltaH2 + m.alphaj * m.Aj / m.Vr0 * ( + m.Tj[t] - m.Tr[t] + ) + m.alphac * m.Ac / m.Vr0 * ( + m.Tc[t] - m.Tr[t] + ) + + m.dTrcon = Constraint(m.time, rule=_dTrcon) + + def _singlecooling(m, t): + return m.Tc[t] == m.Tj[t] + + m.singlecooling = Constraint(m.time, rule=_singlecooling) + + # Initial Conditions + def _initcon(m): + yield m.Ca[m.time.first()] == m.Ca0 + yield m.Cb[m.time.first()] == m.Cb0 + yield m.Cc[m.time.first()] == m.Cc0 + yield m.Vr[m.time.first()] == m.Vr0 + yield m.Tr[m.time.first()] == m.Tr0 + + m.initcon = ConstraintList(rule=_initcon) + + # + # Stage-specific cost computations + # + def ComputeFirstStageCost_rule(model): + return 0 + + m.FirstStageCost = Expression(rule=ComputeFirstStageCost_rule) + + def AllMeasurements(m): + return sum( + (m.Ca[t] - m.Ca_meas[t]) ** 2 + + (m.Cb[t] - m.Cb_meas[t]) ** 2 + + (m.Cc[t] - m.Cc_meas[t]) ** 2 + + 0.01 * (m.Tr[t] - m.Tr_meas[t]) ** 2 + for t in m.measT + ) + + def MissingMeasurements(m): + if data["experiment"] == 1: + return sum( + (m.Ca[t] - m.Ca_meas[t]) ** 2 + + (m.Cb[t] - m.Cb_meas[t]) ** 2 + + (m.Cc[t] - m.Cc_meas[t]) ** 2 + + (m.Tr[t] - m.Tr_meas[t]) ** 2 + for t in m.measT + ) + elif data["experiment"] == 2: + return sum((m.Tr[t] - m.Tr_meas[t]) ** 2 for t in m.measT) + else: + return sum( + (m.Cb[t] - m.Cb_meas[t]) ** 2 + (m.Tr[t] - m.Tr_meas[t]) ** 2 + for t in m.measT + ) + + m.SecondStageCost = Expression(rule=MissingMeasurements) + + def total_cost_rule(model): + return model.FirstStageCost + model.SecondStageCost + + m.Total_Cost_Objective = Objective(rule=total_cost_rule, sense=minimize) + + # Discretize model + disc = TransformationFactory("dae.collocation") + disc.apply_to(m, nfe=20, ncp=4) + return m + + # Vars to estimate in parmest + theta_names = ["k1", "k2", "E1", "E2"] + + self.fbase = os.path.join(testdir, "..", "examples", "semibatch") + # Data, list of dictionaries + data = [] + for exp_num in range(10): + fname = "exp" + str(exp_num + 1) + ".out" + fullname = os.path.join(self.fbase, fname) + with open(fullname, "r") as infile: + d = json.load(infile) + data.append(d) + + # Note, the model already includes a 'SecondStageCost' expression + # for the sum of squared error that will be used in parameter estimation + + self.pest = parmest.Estimator(generate_model, data, theta_names) + + def test_semibatch_bootstrap(self): + scenmaker = sc.ScenarioCreator(self.pest, "ipopt") + bootscens = sc.ScenarioSet("Bootstrap") + numtomake = 2 + scenmaker.ScenariosFromBootstrap(bootscens, numtomake, seed=1134) + tval = bootscens.ScenarioNumber(0).ThetaVals["k1"] + self.assertAlmostEqual(tval, 20.64, places=1) + + if __name__ == "__main__": unittest.main() From 41d8197bbc2627aa6742d0358e16ade0a93a4747 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 21 Feb 2024 16:23:41 -0700 Subject: [PATCH 0712/3044] Support config domains with either method or attribute domain_name --- pyomo/common/config.py | 6 +++++- pyomo/common/tests/test_config.py | 35 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 238bdd78e9d..f9c3a725bb8 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -1134,7 +1134,11 @@ def _domain_name(domain): if domain is None: return "" elif hasattr(domain, 'domain_name'): - return domain.domain_name() + dn = domain.domain_name + if hasattr(dn, '__call__'): + return dn() + else: + return dn elif domain.__class__ is type: return domain.__name__ elif inspect.isfunction(domain): diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 0bbed43423d..12657481764 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -3265,6 +3265,41 @@ def __init__( OUT.getvalue().replace('null', 'None'), ) + def test_domain_name(self): + cfg = ConfigDict() + + cfg.declare('none', ConfigValue()) + self.assertEqual(cfg.get('none').domain_name(), '') + + def fcn(val): + return val + + cfg.declare('fcn', ConfigValue(domain=fcn)) + self.assertEqual(cfg.get('fcn').domain_name(), 'fcn') + + fcn.domain_name = 'custom fcn' + self.assertEqual(cfg.get('fcn').domain_name(), 'custom fcn') + + class functor: + def __call__(self, val): + return val + + cfg.declare('functor', ConfigValue(domain=functor())) + self.assertEqual(cfg.get('functor').domain_name(), 'functor') + + class cfunctor: + def __call__(self, val): + return val + + def domain_name(self): + return 'custom functor' + + cfg.declare('cfunctor', ConfigValue(domain=cfunctor())) + self.assertEqual(cfg.get('cfunctor').domain_name(), 'custom functor') + + cfg.declare('type', ConfigValue(domain=int)) + self.assertEqual(cfg.get('type').domain_name(), 'int') + if __name__ == "__main__": unittest.main() From df19b6bf869f94b792aa30733edfdff68b3da7ad Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Mon, 17 Jul 2023 11:36:32 -0600 Subject: [PATCH 0713/3044] add initial work on nested inner repn pw to gdp transformation. identify variables mode does not work, gives infeasible models --- .../tests/test_nested_inner_repn_gdp.py | 36 ++++ .../piecewise/transform/nested_inner_repn.py | 171 ++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py create mode 100644 pyomo/contrib/piecewise/transform/nested_inner_repn.py diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py new file mode 100644 index 00000000000..48357c828df --- /dev/null +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -0,0 +1,36 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.piecewise.tests import models +import pyomo.contrib.piecewise.tests.common_tests as ct +from pyomo.core.base import TransformationFactory +from pyomo.core.expr.compare import ( + assertExpressionsEqual, + assertExpressionsStructurallyEqual, +) +from pyomo.gdp import Disjunct, Disjunction +from pyomo.environ import Constraint, SolverFactory, Var + +from pyomo.contrib.piecewise.transform.nested_inner_repn import NestedInnerRepresentationGDPTransformation + +class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): + + def test_solve_log_model(self): + m = models.make_log_x_model() + TransformationFactory( + 'contrib.piecewise.nested_inner_repn_gdp' + ).apply_to(m) + TransformationFactory( + 'gdp.bigm' + ).apply_to(m) + SolverFactory('gurobi').solve(m) + ct.check_log_x_model_soln(self, m) \ No newline at end of file diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py new file mode 100644 index 00000000000..b25ca3981a8 --- /dev/null +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -0,0 +1,171 @@ +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( + PiecewiseLinearToGDP, +) +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core.base import TransformationFactory +from pyomo.gdp import Disjunct, Disjunction +from pyomo.common.errors import DeveloperError +from pyomo.core.expr.visitor import SimpleExpressionVisitor +from pyomo.core.expr.current import identify_components + +@TransformationFactory.register( + 'contrib.piecewise.nested_inner_repn_gdp', + doc="TODO document", +) +class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): + """ + Represent a piecewise linear function "logarithmically" by using a nested + GDP to determine which polytope a point is in, then representing it as a + convex combination of extreme points, with multipliers "local" to that + particular polytope, i.e., not shared with neighbors. This method of + logarithmically formulating the piecewise linear function imposes no + restrictions on the family of polytopes. We rely on the identification of + variables to make this logarithmic in the number of binaries. This method + is due to Vielma et al., 2010. + """ + CONFIG = PiecewiseLinearToGDP.CONFIG() + _transformation_name = 'pw_linear_nested_inner_repn' + + # Implement to use PiecewiseLinearToGDP. This function returns the Var + # that replaces the transformed piecewise linear expr + def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): + self.DEBUG = True + identify_vars = True + # Get a new Block() in transformation_block.transformed_functions, which + # is a Block(Any) + transBlock = transformation_block.transformed_functions[ + len(transformation_block.transformed_functions) + ] + + # these copy-pasted lines (from inner_representation_gdp) seem useful + # adding some of this stuff to self so I don't have to pass it around + self.pw_linear_func = pw_linear_func + # map number -> list of Disjuncts which contain Disjunctions at that level + self.disjunct_levels = {} + self.dimension = pw_expr.nargs() + substitute_var = transBlock.substitute_var = Var() + pw_linear_func.map_transformation_var(pw_expr, substitute_var) + self.substitute_var_lb = float('inf') + self.substitute_var_ub = -float('inf') + + choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) + + if self.DEBUG: + print(f"dimension is {self.dimension}") + + # Add the disjunction + transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) + + # Widen bounds as determined when setting up the disjunction + if self.substitute_var_lb < float('inf'): + transBlock.substitute_var.setlb(self.substitute_var_lb) + if self.substitute_var_ub > -float('inf'): + transBlock.substitute_var.setub(self.substitute_var_ub) + + if self.DEBUG: + print(f"lb is {self.substitute_var_lb}, ub is {self.substitute_var_ub}") + + if identify_vars: + if self.DEBUG: + print("Now identifying variables") + for i in self.disjunct_levels.keys(): + print(f"level {i}: {len(self.disjunct_levels[i])} disjuncts") + transBlock.var_identifications_l = Constraint(NonNegativeIntegers, NonNegativeIntegers) + transBlock.var_identifications_r = Constraint(NonNegativeIntegers, NonNegativeIntegers) + for k in self.disjunct_levels.keys(): + disj_0 = self.disjunct_levels[k][0] + for i, disj in enumerate(self.disjunct_levels[k][1:]): + transBlock.var_identifications_l[k, i] = disj.d_l.binary_indicator_var == disj_0.d_l.binary_indicator_var + transBlock.var_identifications_r[k, i] = disj.d_r.binary_indicator_var == disj_0.d_r.binary_indicator_var + return substitute_var + + # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up + # the stack, since the whole point is that we'll only go logarithmically + # many calls deep. + def _get_disjunction(self, choices, parent_block, pw_expr, root_block, level): + size = len(choices) + if self.DEBUG: + print(f"calling _get_disjunction with size={size}") + # Our base cases will be 3 and 2, since it would be silly to construct + # a Disjunction containing only one Disjunct. We can ensure that size + # is never 1 unless it was only passsed a single choice from the start, + # which we can handle before calling. + if size > 3: + half = size // 2 # (integer divide) + # This tree will be slightly heavier on the right side + choices_l = choices[:half] + choices_r = choices[half:] + # Is this valid Pyomo? + @parent_block.Disjunct() + def d_l(b): + b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block, level + 1) + @parent_block.Disjunct() + def d_r(b): + b.inner_disjunction_r = self._get_disjunction(choices_r, b, pw_expr, root_block, level + 1) + if level not in self.disjunct_levels.keys(): + self.disjunct_levels[level] = [] + self.disjunct_levels[level].append(parent_block.d_l) + self.disjunct_levels[level].append(parent_block.d_r) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) + elif size == 3: + # Let's stay heavier on the right side for consistency. So the left + # Disjunct will be the one to contain constraints, rather than a + # Disjunction + @parent_block.Disjunct() + def d_l(b): + simplex, linear_func = choices[0] + self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + @parent_block.Disjunct() + def d_r(b): + b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block, level + 1) + if level not in self.disjunct_levels.keys(): + self.disjunct_levels[level] = [] + self.disjunct_levels[level].append(parent_block.d_r) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) + elif size == 2: + # In this case both sides are regular Disjuncts + @parent_block.Disjunct() + def d_l(b): + simplex, linear_func = choices[0] + self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + @parent_block.Disjunct() + def d_r(b): + simplex, linear_func = choices[1] + self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) + else: + raise DeveloperError("Unreachable: 1 or 0 choices were passed to " + "_get_disjunction in nested_inner_repn.py.") + + def _set_disjunct_block_constraints(self, b, simplex, linear_func, pw_expr, root_block): + # Define the lambdas sparsely like in the version I'm copying, + # only the first few will participate in constraints + b.lambdas = Var(NonNegativeIntegers, dense=False, bounds=(0, 1)) + # Get the extreme points to add up + extreme_pts = [] + for idx in simplex: + extreme_pts.append(self.pw_linear_func._points[idx]) + # Constrain sum(lambda_i) = 1 + b.convex_combo = Constraint( + expr=sum(b.lambdas[i] for i in range(len(extreme_pts))) == 1 + ) + linear_func_expr = linear_func(*pw_expr.args) + # Make the substitute Var equal the PWLE + b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) + # Widen the variable bounds to those of this linear func expression + (lb, ub) = compute_bounds_on_expr(linear_func_expr) + if lb is not None and lb < self.substitute_var_lb: + self.substitute_var_lb = lb + if ub is not None and ub > self.substitute_var_ub: + self.substitute_var_ub = ub + # Constrain x = \sum \lambda_i v_i + @b.Constraint(range(self.dimension)) + def linear_combo(d, i): + return pw_expr.args[i] == sum( + d.lambdas[j] * pt[i] for j, pt in enumerate(extreme_pts) + ) + # Mark the lambdas as local in order to prevent disagreggating multiple + # times in the hull transformation + b.LocalVars = Suffix(direction=Suffix.LOCAL) + b.LocalVars[b] = [v for v in b.lambdas.values()] From 3e600ce5d32262053d46f1328a9008c6719cfe5b Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 1 Aug 2023 12:48:46 -0600 Subject: [PATCH 0714/3044] wip: working on some other pw linear representations --- .../transform/disagreggated_logarithmic.py | 102 ++++++++++++++++++ .../piecewise/transform/nested_inner_repn.py | 7 +- 2 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py new file mode 100644 index 00000000000..fceb02d4d8c --- /dev/null +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -0,0 +1,102 @@ +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( + PiecewiseLinearToGDP, +) +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core.base import TransformationFactory +from pyomo.gdp import Disjunct, Disjunction +from pyomo.common.errors import DeveloperError +from pyomo.core.expr.visitor import SimpleExpressionVisitor +from pyomo.core.expr.current import identify_components +from math import ceil, log2 + +@TransformationFactory.register( + 'contrib.piecewise.disaggregated_logarithmic', + doc="TODO document", +) +class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): + """ + Represent a piecewise linear function "logarithmically" by using a MIP with + log_2(|P|) binary decision variables. This method of logarithmically + formulating the piecewise linear function imposes no restrictions on the + family of polytopes. This method is due to Vielma et al., 2010. + """ + CONFIG = PiecewiseLinearToGDP.CONFIG() + _transformation_name = 'pw_linear_disaggregated_log' + + # Implement to use PiecewiseLinearToGDP. This function returns the Var + # that replaces the transformed piecewise linear expr + def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): + self.DEBUG = False + # Get a new Block() in transformation_block.transformed_functions, which + # is a Block(Any) + transBlock = transformation_block.transformed_functions[ + len(transformation_block.transformed_functions) + ] + + dimension = pw_expr.nargs() + substitute_var = transBlock.substitute_var = Var() + pw_linear_func.map_transformation_var(pw_expr, substitute_var) + self.substitute_var_lb = float('inf') + self.substitute_var_ub = -float('inf') + + simplices = pw_linear_func._simplices + num_simplices = len(simplices) + simplex_indices = range(num_simplices) + # Assumption: the simplices are really simplices and all have the same number of points + simplex_point_indices = range(len(simplices[0])) + + choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) + + log_dimension = ceil(log2(num_simplices)) + binaries = transBlock.binaries = Var(range(log_dimension), domain=Binary) + + # injective function \mathcal{P} -> ceil(log_2(|P|)) used to identify simplices + # (really just polytopes are required) with binary vectors + B = {} + for i, p in enumerate(simplices): + B[id(p)] = self._get_binary_vector(i, log_dimension) + + # The lambdas \lambda_{P,v} + lambdas = transBlock.lambdas = Var(simplex_indices, simplex_point_indices, bounds=(0, 1)) + transBlock.convex_combo = Constraint(sum(lambdas[P, v] for P in simplex_indices for v in simplex_point_indices) == 1) + + # The branching rules, establishing using the binaries that only one simplex's lambdas + # may be nonzero + @transBlock.Constraint(range(log_dimension)) + def simplex_choice_1(b, l): + return ( + sum(lambdas[P, v] for P in self._P_plus(B, l) for v in simplex_point_indices) <= binaries[l] + ) + @transBlock.Constraint(range(log_dimension)) + def simplex_choice_2(b, l): + return ( + sum(lambdas[P, v] for P in self._P_0(B, l) for v in simplex_point_indices) <= 1 - binaries[l] + ) + + #for i, (simplex, pwlf) in enumerate(choices): + # x_i = sum(lambda_P,v v_i) + @transBlock.Constraint(range(dimension)) + def x_constraint(b, i): + return sum([stuff] for ) + + + #linear_func_expr = linear_func(*pw_expr.args) + ## Make the substitute Var equal the PWLE + #b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) + + # Not a gray code, just a regular binary representation + # TODO this is probably not optimal, test the gray codes too + def _get_binary_vector(self, num, length): + if ceil(log2(num)) > length: + raise DeveloperError("Invalid input in _get_binary_vector") + # Use python's string formatting instead of bothering with modular + # arithmetic. May be slow. + return (int(x) for x in format(num, f'0{length}b')) + + # Return {P \in \mathcal{P} | B(P)_l = 0} + def _P_0(B, l, simplices): + return [p for p in simplices if B[id(p)][l] == 0] + # Return {P \in \mathcal{P} | B(P)_l = 1} + def _P_plus(B, l, simplices): + return [p for p in simplices if B[id(p)][l] == 1] \ No newline at end of file diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index b25ca3981a8..fc5761de434 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -30,8 +30,8 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - self.DEBUG = True - identify_vars = True + self.DEBUG = False + identify_vars = False # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ @@ -66,6 +66,9 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc if self.DEBUG: print(f"lb is {self.substitute_var_lb}, ub is {self.substitute_var_ub}") + # NOTE - This functionality does not work. Even when we can choose the indicator + # variables, it seems that infeasibilities will always be generated. We may need + # to just directly transform to mip :( if identify_vars: if self.DEBUG: print("Now identifying variables") From c86220450129db5ac1f12020aae3d5e620b90014 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 23 Aug 2023 16:34:57 -0400 Subject: [PATCH 0715/3044] properly handle one-simplex case instead of ignoring --- .../piecewise/transform/nested_inner_repn.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index fc5761de434..1e86a1406b4 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -54,8 +54,17 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc if self.DEBUG: print(f"dimension is {self.dimension}") - # Add the disjunction - transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) + # If there was only one choice, don't bother making a disjunction, just + # use the linear function directly (but still use the substitute_var for + # consistency). + if len(choices) == 1: + (_, linear_func) = choices[0] # simplex isn't important in this case + linear_func_expr = linear_func(*pw_expr.args) + transBlock.set_substitute = Constraint(expr=substitute_var == linear_func_expr) + (self.substitute_var_lb, self.substitute_var_ub) = compute_bounds_on_expr(linear_func_expr) + else: + # Add the disjunction + transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) # Widen bounds as determined when setting up the disjunction if self.substitute_var_lb < float('inf'): From 8b1997fd1a3604581937c50eb643d915b5b74bbb Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 23 Aug 2023 17:03:43 -0400 Subject: [PATCH 0716/3044] nested inner repn: remove non-working variable identification code --- .../piecewise/transform/nested_inner_repn.py | 55 +++++-------------- 1 file changed, 14 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 1e86a1406b4..aaa0e03c79b 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -2,27 +2,25 @@ from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( PiecewiseLinearToGDP, ) -from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var from pyomo.core.base import TransformationFactory -from pyomo.gdp import Disjunct, Disjunction +from pyomo.gdp import Disjunction from pyomo.common.errors import DeveloperError -from pyomo.core.expr.visitor import SimpleExpressionVisitor -from pyomo.core.expr.current import identify_components @TransformationFactory.register( 'contrib.piecewise.nested_inner_repn_gdp', - doc="TODO document", + doc="TODO document", # TODO ) class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): """ - Represent a piecewise linear function "logarithmically" by using a nested - GDP to determine which polytope a point is in, then representing it as a - convex combination of extreme points, with multipliers "local" to that - particular polytope, i.e., not shared with neighbors. This method of - logarithmically formulating the piecewise linear function imposes no - restrictions on the family of polytopes. We rely on the identification of - variables to make this logarithmic in the number of binaries. This method - is due to Vielma et al., 2010. + Represent a piecewise linear function by using a nested GDP to determine + which polytope a point is in, then representing it as a convex combination + of extreme points, with multipliers "local" to that particular polytope, + i.e., not shared with neighbors. This method of formulating the piecewise + linear function imposes no restrictions on the family of polytopes. Note + that this is NOT a logarithmic formulation - it has linearly many binaries. + This method was, however, inspired by the disagreggated logarithmic + formulation of Vielma et al., 2010. """ CONFIG = PiecewiseLinearToGDP.CONFIG() _transformation_name = 'pw_linear_nested_inner_repn' @@ -31,7 +29,6 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): self.DEBUG = False - identify_vars = False # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ @@ -41,8 +38,6 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # these copy-pasted lines (from inner_representation_gdp) seem useful # adding some of this stuff to self so I don't have to pass it around self.pw_linear_func = pw_linear_func - # map number -> list of Disjuncts which contain Disjunctions at that level - self.disjunct_levels = {} self.dimension = pw_expr.nargs() substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) @@ -64,7 +59,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc (self.substitute_var_lb, self.substitute_var_ub) = compute_bounds_on_expr(linear_func_expr) else: # Add the disjunction - transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) + transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock) # Widen bounds as determined when setting up the disjunction if self.substitute_var_lb < float('inf'): @@ -75,21 +70,6 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc if self.DEBUG: print(f"lb is {self.substitute_var_lb}, ub is {self.substitute_var_ub}") - # NOTE - This functionality does not work. Even when we can choose the indicator - # variables, it seems that infeasibilities will always be generated. We may need - # to just directly transform to mip :( - if identify_vars: - if self.DEBUG: - print("Now identifying variables") - for i in self.disjunct_levels.keys(): - print(f"level {i}: {len(self.disjunct_levels[i])} disjuncts") - transBlock.var_identifications_l = Constraint(NonNegativeIntegers, NonNegativeIntegers) - transBlock.var_identifications_r = Constraint(NonNegativeIntegers, NonNegativeIntegers) - for k in self.disjunct_levels.keys(): - disj_0 = self.disjunct_levels[k][0] - for i, disj in enumerate(self.disjunct_levels[k][1:]): - transBlock.var_identifications_l[k, i] = disj.d_l.binary_indicator_var == disj_0.d_l.binary_indicator_var - transBlock.var_identifications_r[k, i] = disj.d_r.binary_indicator_var == disj_0.d_r.binary_indicator_var return substitute_var # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up @@ -111,14 +91,10 @@ def _get_disjunction(self, choices, parent_block, pw_expr, root_block, level): # Is this valid Pyomo? @parent_block.Disjunct() def d_l(b): - b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block, level + 1) + b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block) @parent_block.Disjunct() def d_r(b): - b.inner_disjunction_r = self._get_disjunction(choices_r, b, pw_expr, root_block, level + 1) - if level not in self.disjunct_levels.keys(): - self.disjunct_levels[level] = [] - self.disjunct_levels[level].append(parent_block.d_l) - self.disjunct_levels[level].append(parent_block.d_r) + b.inner_disjunction_r = self._get_disjunction(choices_r, b, pw_expr, root_block) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 3: # Let's stay heavier on the right side for consistency. So the left @@ -131,9 +107,6 @@ def d_l(b): @parent_block.Disjunct() def d_r(b): b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block, level + 1) - if level not in self.disjunct_levels.keys(): - self.disjunct_levels[level] = [] - self.disjunct_levels[level].append(parent_block.d_r) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 2: # In this case both sides are regular Disjuncts From b1cc43403dfe50beeca3023809cfcd0e1b23d3fe Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 23 Aug 2023 17:13:15 -0400 Subject: [PATCH 0717/3044] fix errors --- pyomo/contrib/piecewise/transform/nested_inner_repn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index aaa0e03c79b..6c551818c84 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -75,7 +75,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up # the stack, since the whole point is that we'll only go logarithmically # many calls deep. - def _get_disjunction(self, choices, parent_block, pw_expr, root_block, level): + def _get_disjunction(self, choices, parent_block, pw_expr, root_block): size = len(choices) if self.DEBUG: print(f"calling _get_disjunction with size={size}") @@ -106,7 +106,7 @@ def d_l(b): self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) @parent_block.Disjunct() def d_r(b): - b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block, level + 1) + b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 2: # In this case both sides are regular Disjuncts From 601abcbaf7b4d7db5cdf66c43a1f5199a3226a5d Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 12 Oct 2023 00:38:54 -0400 Subject: [PATCH 0718/3044] disaggregated logarithmic reworking --- .../transform/disagreggated_logarithmic.py | 186 +++++++++++++----- 1 file changed, 142 insertions(+), 44 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index fceb02d4d8c..e0b6d75d0e4 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -2,7 +2,7 @@ from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( PiecewiseLinearToGDP, ) -from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet from pyomo.core.base import TransformationFactory from pyomo.gdp import Disjunct, Disjunction from pyomo.common.errors import DeveloperError @@ -10,93 +10,191 @@ from pyomo.core.expr.current import identify_components from math import ceil, log2 + @TransformationFactory.register( - 'contrib.piecewise.disaggregated_logarithmic', - doc="TODO document", -) -class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): - """ + "contrib.piecewise.disaggregated_logarithmic", + doc=""" Represent a piecewise linear function "logarithmically" by using a MIP with log_2(|P|) binary decision variables. This method of logarithmically formulating the piecewise linear function imposes no restrictions on the family of polytopes. This method is due to Vielma et al., 2010. + """, +) +class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): + """ + Represent a piecewise linear function "logarithmically" by using a MIP with + log_2(|P|) binary decision variables. This method of logarithmically + formulating the piecewise linear function imposes no restrictions on the + family of polytopes. This method is due to Vielma et al., 2010. """ + CONFIG = PiecewiseLinearToGDP.CONFIG() - _transformation_name = 'pw_linear_disaggregated_log' - + _transformation_name = "pw_linear_disaggregated_log" + # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): self.DEBUG = False + # Get a new Block() in transformation_block.transformed_functions, which - # is a Block(Any) + # is a Block(Any). This is where we will put our new components. transBlock = transformation_block.transformed_functions[ len(transformation_block.transformed_functions) ] + # Dimensionality of the PWLF dimension = pw_expr.nargs() + print(f"DIMENSIOn={dimension}") + transBlock.dimension_indices = RangeSet(0, dimension - 1) + + # Substitute Var that will hold the value of the PWLE substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) - self.substitute_var_lb = float('inf') - self.substitute_var_ub = -float('inf') + # Bounds for the substitute_var that we will tighten + self.substitute_var_lb = float("inf") + self.substitute_var_ub = -float("inf") + + # Simplices are tuples of indices of points. Give them their own indices, too simplices = pw_linear_func._simplices num_simplices = len(simplices) - simplex_indices = range(num_simplices) - # Assumption: the simplices are really simplices and all have the same number of points - simplex_point_indices = range(len(simplices[0])) + transBlock.simplex_indices = RangeSet(0, num_simplices - 1) + # Assumption: the simplices are really simplices and all have the same number of points, + # which is dimension + 1 + transBlock.simplex_point_indices = RangeSet(0, dimension) + + # Enumeration of simplices, map from simplex number to simplex object + self.idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} + # Inverse of previous enumeration + self.simplex_to_idx = {v: k for k, v in self.idx_to_simplex.items()} + + # List of tuples of simplices with their linear function + simplices_and_lin_funcs = list(zip(simplices, pw_linear_func._linear_functions)) - choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) + print("a") + print(f"Num_simplices: {num_simplices}") log_dimension = ceil(log2(num_simplices)) - binaries = transBlock.binaries = Var(range(log_dimension), domain=Binary) + transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) + binaries = transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) - # injective function \mathcal{P} -> ceil(log_2(|P|)) used to identify simplices - # (really just polytopes are required) with binary vectors + # Injective function \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices + # (really just polytopes are required) with binary vectors. Any injective function + # is valid. B = {} - for i, p in enumerate(simplices): - B[id(p)] = self._get_binary_vector(i, log_dimension) - - # The lambdas \lambda_{P,v} - lambdas = transBlock.lambdas = Var(simplex_indices, simplex_point_indices, bounds=(0, 1)) - transBlock.convex_combo = Constraint(sum(lambdas[P, v] for P in simplex_indices for v in simplex_point_indices) == 1) + for i in transBlock.simplex_indices: + # map index(P) -> corresponding vector in {0, 1}^n + B[i] = self._get_binary_vector(i, log_dimension) + print(f"after construction, B = {B}") + + print("b") + # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it + transBlock.lambdas = Var(transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1)) + print("b1") + + # Sum of all lambdas is one (6b) + transBlock.convex_combo = Constraint( + expr=sum( + transBlock.lambdas[P, v] + for P in transBlock.simplex_indices + for v in transBlock.simplex_point_indices + ) + == 1 + ) + + print("c") # The branching rules, establishing using the binaries that only one simplex's lambdas # may be nonzero - @transBlock.Constraint(range(log_dimension)) + @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) def simplex_choice_1(b, l): + print("entering constraint generator") + print(f"thing={self._P_plus(B, l, simplices)}") + print("returning") return ( - sum(lambdas[P, v] for P in self._P_plus(B, l) for v in simplex_point_indices) <= binaries[l] + sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + for P in self._P_plus(B, l, simplices) + for v in transBlock.simplex_point_indices + ) + <= binaries[l] ) - @transBlock.Constraint(range(log_dimension)) + + print("c1") + + @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) def simplex_choice_2(b, l): return ( - sum(lambdas[P, v] for P in self._P_0(B, l) for v in simplex_point_indices) <= 1 - binaries[l] + sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + for P in self._P_0(B, l, simplices) + for v in transBlock.simplex_point_indices + ) + <= 1 - binaries[l] ) - - #for i, (simplex, pwlf) in enumerate(choices): - # x_i = sum(lambda_P,v v_i) - @transBlock.Constraint(range(dimension)) + + print("d") + + # for i, (simplex, pwlf) in enumerate(choices): + # x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P)) + @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) def x_constraint(b, i): - return sum([stuff] for ) + print(f"simplices are {[P for P in simplices]}") + print(f"points are {pw_linear_func._points}") + print(f"simplex_point_indices is {list(transBlock.simplex_point_indices)}") + print(f"i={i}") + + return pw_expr.args[i] == sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + * pw_linear_func._points[P[v]][i] + for P in simplices + for v in transBlock.simplex_point_indices + ) + + # Make the substitute Var equal the PWLE (6a.2) + for P, linear_func in simplices_and_lin_funcs: + print(f"P, linear_func = {P}, {linear_func}") + for v in transBlock.simplex_point_indices: + print(f" v={v}") + print(f" pt={pw_linear_func._points[P[v]]}") + print( + f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" + ) + transBlock.set_substitute = Constraint( + expr=substitute_var + == sum( + sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + * linear_func(*pw_linear_func._points[P[v]]) + for v in transBlock.simplex_point_indices + ) + for (P, linear_func) in simplices_and_lin_funcs + ) + ) + + print("f") + return substitute_var - #linear_func_expr = linear_func(*pw_expr.args) - ## Make the substitute Var equal the PWLE - #b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) - # Not a gray code, just a regular binary representation # TODO this is probably not optimal, test the gray codes too def _get_binary_vector(self, num, length): - if ceil(log2(num)) > length: + if num != 0 and ceil(log2(num)) > length: raise DeveloperError("Invalid input in _get_binary_vector") - # Use python's string formatting instead of bothering with modular + # Hack: use python's string formatting instead of bothering with modular # arithmetic. May be slow. - return (int(x) for x in format(num, f'0{length}b')) + return tuple(int(x) for x in format(num, f"0{length}b")) # Return {P \in \mathcal{P} | B(P)_l = 0} - def _P_0(B, l, simplices): - return [p for p in simplices if B[id(p)][l] == 0] + def _P_0(self, B, l, simplices): + return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 0] + # Return {P \in \mathcal{P} | B(P)_l = 1} - def _P_plus(B, l, simplices): - return [p for p in simplices if B[id(p)][l] == 1] \ No newline at end of file + def _P_plus(self, B, l, simplices): + print(f"p plus: B={B}, l={l}, simplices={simplices}") + for p in simplices: + print(f"for p={p}, simplex_to_idx[p]={self.simplex_to_idx[p]}") + print( + f"returning {[p for p in simplices if B[self.simplex_to_idx[p]][l] == 1]}" + ) + return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 1] From ee616bf226d4d23b7b4ad7dfbacfcc94bfb4a715 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 12 Oct 2023 01:34:14 -0400 Subject: [PATCH 0719/3044] remove printf debugging --- .../transform/disagreggated_logarithmic.py | 67 +++++++------------ 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index e0b6d75d0e4..00fb1546412 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -34,7 +34,6 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - self.DEBUG = False # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any). This is where we will put our new components. @@ -44,14 +43,13 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Dimensionality of the PWLF dimension = pw_expr.nargs() - print(f"DIMENSIOn={dimension}") transBlock.dimension_indices = RangeSet(0, dimension - 1) # Substitute Var that will hold the value of the PWLE substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) - # Bounds for the substitute_var that we will tighten + # Bounds for the substitute_var that we will widen self.substitute_var_lb = float("inf") self.substitute_var_ub = -float("inf") @@ -71,26 +69,35 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # List of tuples of simplices with their linear function simplices_and_lin_funcs = list(zip(simplices, pw_linear_func._linear_functions)) - print("a") - print(f"Num_simplices: {num_simplices}") + # We don't seem to get a convenient opportunity later, so let's just widen + # the bounds here. All we need to do is go through the corners of each simplex. + for P, linear_func in simplices_and_lin_funcs: + for v in transBlock.simplex_point_indices: + val = linear_func(*pw_linear_func._points[P[v]]) + if val < self.substitute_var_lb: + self.substitute_var_lb = val + if val > self.substitute_var_ub: + self.substitute_var_ub = val + # Now set those bounds + if self.substitute_var_lb < float('inf'): + transBlock.substitute_var.setlb(self.substitute_var_lb) + if self.substitute_var_ub > -float('inf'): + transBlock.substitute_var.setub(self.substitute_var_ub) log_dimension = ceil(log2(num_simplices)) transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) binaries = transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) - # Injective function \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices + # Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices # (really just polytopes are required) with binary vectors. Any injective function - # is valid. + # is enough here. B = {} for i in transBlock.simplex_indices: # map index(P) -> corresponding vector in {0, 1}^n B[i] = self._get_binary_vector(i, log_dimension) - print(f"after construction, B = {B}") - print("b") # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it transBlock.lambdas = Var(transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1)) - print("b1") # Sum of all lambdas is one (6b) transBlock.convex_combo = Constraint( @@ -102,15 +109,10 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc == 1 ) - print("c") - # The branching rules, establishing using the binaries that only one simplex's lambdas # may be nonzero @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) def simplex_choice_1(b, l): - print("entering constraint generator") - print(f"thing={self._P_plus(B, l, simplices)}") - print("returning") return ( sum( transBlock.lambdas[self.simplex_to_idx[P], v] @@ -120,8 +122,6 @@ def simplex_choice_1(b, l): <= binaries[l] ) - print("c1") - @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) def simplex_choice_2(b, l): return ( @@ -133,18 +133,10 @@ def simplex_choice_2(b, l): <= 1 - binaries[l] ) - print("d") - # for i, (simplex, pwlf) in enumerate(choices): # x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P)) @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) def x_constraint(b, i): - - print(f"simplices are {[P for P in simplices]}") - print(f"points are {pw_linear_func._points}") - print(f"simplex_point_indices is {list(transBlock.simplex_point_indices)}") - print(f"i={i}") - return pw_expr.args[i] == sum( transBlock.lambdas[self.simplex_to_idx[P], v] * pw_linear_func._points[P[v]][i] @@ -153,14 +145,14 @@ def x_constraint(b, i): ) # Make the substitute Var equal the PWLE (6a.2) - for P, linear_func in simplices_and_lin_funcs: - print(f"P, linear_func = {P}, {linear_func}") - for v in transBlock.simplex_point_indices: - print(f" v={v}") - print(f" pt={pw_linear_func._points[P[v]]}") - print( - f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" - ) + #for P, linear_func in simplices_and_lin_funcs: + # print(f"P, linear_func = {P}, {linear_func}") + # for v in transBlock.simplex_point_indices: + # print(f" v={v}") + # print(f" pt={pw_linear_func._points[P[v]]}") + # print( + # f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" + # ) transBlock.set_substitute = Constraint( expr=substitute_var == sum( @@ -173,11 +165,10 @@ def x_constraint(b, i): ) ) - print("f") return substitute_var # Not a gray code, just a regular binary representation - # TODO this is probably not optimal, test the gray codes too + # TODO this may not be optimal, test the gray codes too def _get_binary_vector(self, num, length): if num != 0 and ceil(log2(num)) > length: raise DeveloperError("Invalid input in _get_binary_vector") @@ -191,10 +182,4 @@ def _P_0(self, B, l, simplices): # Return {P \in \mathcal{P} | B(P)_l = 1} def _P_plus(self, B, l, simplices): - print(f"p plus: B={B}, l={l}, simplices={simplices}") - for p in simplices: - print(f"for p={p}, simplex_to_idx[p]={self.simplex_to_idx[p]}") - print( - f"returning {[p for p in simplices if B[self.simplex_to_idx[p]][l] == 1]}" - ) return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 1] From dbecedd3a1644fd1b940cf1cf9ab95e994e307e6 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 12 Oct 2023 02:23:00 -0400 Subject: [PATCH 0720/3044] fix strange reverse indexing --- .../transform/disagreggated_logarithmic.py | 70 +++++++++---------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index 00fb1546412..e86d5539367 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -15,17 +15,19 @@ "contrib.piecewise.disaggregated_logarithmic", doc=""" Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This method of logarithmically - formulating the piecewise linear function imposes no restrictions on the - family of polytopes. This method is due to Vielma et al., 2010. + log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; + GDP is not used. This method of logarithmically formulating the piecewise + linear function imposes no restrictions on the family of polytopes, but we + assume we have simplces in this code. This method is due to Vielma et al., 2010. """, ) class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): """ Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This method of logarithmically - formulating the piecewise linear function imposes no restrictions on the - family of polytopes. This method is due to Vielma et al., 2010. + log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; + GDP is not used. This method of logarithmically formulating the piecewise + linear function imposes no restrictions on the family of polytopes, but we + assume we have simplces in this code. This method is due to Vielma et al., 2010. """ CONFIG = PiecewiseLinearToGDP.CONFIG() @@ -35,8 +37,8 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - # Get a new Block() in transformation_block.transformed_functions, which - # is a Block(Any). This is where we will put our new components. + # Get a new Block for our transformationin transformation_block.transformed_functions, + # which is a Block(Any). This is where we will put our new components. transBlock = transformation_block.transformed_functions[ len(transformation_block.transformed_functions) ] @@ -61,32 +63,28 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # which is dimension + 1 transBlock.simplex_point_indices = RangeSet(0, dimension) - # Enumeration of simplices, map from simplex number to simplex object + # Enumeration of simplices: map from simplex number to simplex object self.idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} - # Inverse of previous enumeration - self.simplex_to_idx = {v: k for k, v in self.idx_to_simplex.items()} - # List of tuples of simplices with their linear function - simplices_and_lin_funcs = list(zip(simplices, pw_linear_func._linear_functions)) + # List of tuples of simplex indices with their linear function + simplex_indices_and_lin_funcs = list(zip(transBlock.simplex_indices, pw_linear_func._linear_functions)) # We don't seem to get a convenient opportunity later, so let's just widen # the bounds here. All we need to do is go through the corners of each simplex. - for P, linear_func in simplices_and_lin_funcs: + for P, linear_func in simplex_indices_and_lin_funcs: for v in transBlock.simplex_point_indices: - val = linear_func(*pw_linear_func._points[P[v]]) + val = linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) if val < self.substitute_var_lb: self.substitute_var_lb = val if val > self.substitute_var_ub: self.substitute_var_ub = val # Now set those bounds - if self.substitute_var_lb < float('inf'): - transBlock.substitute_var.setlb(self.substitute_var_lb) - if self.substitute_var_ub > -float('inf'): - transBlock.substitute_var.setub(self.substitute_var_ub) + transBlock.substitute_var.setlb(self.substitute_var_lb) + transBlock.substitute_var.setub(self.substitute_var_ub) log_dimension = ceil(log2(num_simplices)) transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) - binaries = transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) + transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) # Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices # (really just polytopes are required) with binary vectors. Any injective function @@ -115,22 +113,22 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc def simplex_choice_1(b, l): return ( sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - for P in self._P_plus(B, l, simplices) + transBlock.lambdas[P, v] + for P in self._P_plus(B, l, transBlock.simplex_indices) for v in transBlock.simplex_point_indices ) - <= binaries[l] + <= transBlock.binaries[l] ) @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) def simplex_choice_2(b, l): return ( sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - for P in self._P_0(B, l, simplices) + transBlock.lambdas[P, v] + for P in self._P_0(B, l, transBlock.simplex_indices) for v in transBlock.simplex_point_indices ) - <= 1 - binaries[l] + <= 1 - transBlock.binaries[l] ) # for i, (simplex, pwlf) in enumerate(choices): @@ -138,9 +136,9 @@ def simplex_choice_2(b, l): @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) def x_constraint(b, i): return pw_expr.args[i] == sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - * pw_linear_func._points[P[v]][i] - for P in simplices + transBlock.lambdas[P, v] + * pw_linear_func._points[self.idx_to_simplex[P][v]][i] + for P in transBlock.simplex_indices for v in transBlock.simplex_point_indices ) @@ -157,11 +155,11 @@ def x_constraint(b, i): expr=substitute_var == sum( sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - * linear_func(*pw_linear_func._points[P[v]]) + transBlock.lambdas[P, v] + * linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) for v in transBlock.simplex_point_indices ) - for (P, linear_func) in simplices_and_lin_funcs + for (P, linear_func) in simplex_indices_and_lin_funcs ) ) @@ -177,9 +175,9 @@ def _get_binary_vector(self, num, length): return tuple(int(x) for x in format(num, f"0{length}b")) # Return {P \in \mathcal{P} | B(P)_l = 0} - def _P_0(self, B, l, simplices): - return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 0] + def _P_0(self, B, l, simplex_indices): + return [p for p in simplex_indices if B[p][l] == 0] # Return {P \in \mathcal{P} | B(P)_l = 1} - def _P_plus(self, B, l, simplices): - return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 1] + def _P_plus(self, B, l, simplex_indices): + return [p for p in simplex_indices if B[p][l] == 1] From 8ff5d5ccd59679ce0deb8a254cfdb85e76e2dab3 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 9 Nov 2023 11:34:02 -0500 Subject: [PATCH 0721/3044] minor changes and add basic test for disaggregated log --- .../test_disaggregated_logarithmic_gdp.py | 36 +++++++++++++++++++ .../transform/disagreggated_logarithmic.py | 28 +++++---------- .../piecewise/transform/nested_inner_repn.py | 22 ++++++------ 3 files changed, 55 insertions(+), 31 deletions(-) create mode 100644 pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py new file mode 100644 index 00000000000..b3dc871882b --- /dev/null +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py @@ -0,0 +1,36 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.piecewise.tests import models +import pyomo.contrib.piecewise.tests.common_tests as ct +from pyomo.core.base import TransformationFactory +from pyomo.core.expr.compare import ( + assertExpressionsEqual, + assertExpressionsStructurallyEqual, +) +from pyomo.gdp import Disjunct, Disjunction +from pyomo.environ import Constraint, SolverFactory, Var + +from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import DisaggregatedLogarithmicInnerGDPTransformation + +class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): + + def test_solve_log_model(self): + m = models.make_log_x_model() + TransformationFactory( + 'contrib.piecewise.disaggregated_logarithmic' + ).apply_to(m) + TransformationFactory( + 'gdp.bigm' + ).apply_to(m) + SolverFactory('gurobi').solve(m) + ct.check_log_x_model_soln(self, m) \ No newline at end of file diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index e86d5539367..a793ad77e38 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -1,13 +1,9 @@ -from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( PiecewiseLinearToGDP, ) -from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet +from pyomo.core import Constraint, Binary, Var, RangeSet from pyomo.core.base import TransformationFactory -from pyomo.gdp import Disjunct, Disjunction from pyomo.common.errors import DeveloperError -from pyomo.core.expr.visitor import SimpleExpressionVisitor -from pyomo.core.expr.current import identify_components from math import ceil, log2 @@ -37,7 +33,7 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - # Get a new Block for our transformationin transformation_block.transformed_functions, + # Get a new Block for our transformation in transformation_block.transformed_functions, # which is a Block(Any). This is where we will put our new components. transBlock = transformation_block.transformed_functions[ len(transformation_block.transformed_functions) @@ -78,7 +74,6 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc self.substitute_var_lb = val if val > self.substitute_var_ub: self.substitute_var_ub = val - # Now set those bounds transBlock.substitute_var.setlb(self.substitute_var_lb) transBlock.substitute_var.setub(self.substitute_var_ub) @@ -97,6 +92,9 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it transBlock.lambdas = Var(transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1)) + # Numbered citations are from Vielma et al 2010, Mixed-Integer Models + # for Nonseparable Piecewise-Linear Optimization + # Sum of all lambdas is one (6b) transBlock.convex_combo = Constraint( expr=sum( @@ -143,14 +141,6 @@ def x_constraint(b, i): ) # Make the substitute Var equal the PWLE (6a.2) - #for P, linear_func in simplices_and_lin_funcs: - # print(f"P, linear_func = {P}, {linear_func}") - # for v in transBlock.simplex_point_indices: - # print(f" v={v}") - # print(f" pt={pw_linear_func._points[P[v]]}") - # print( - # f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" - # ) transBlock.set_substitute = Constraint( expr=substitute_var == sum( @@ -165,13 +155,13 @@ def x_constraint(b, i): return substitute_var - # Not a gray code, just a regular binary representation - # TODO this may not be optimal, test the gray codes too + # Not a Gray code, just a regular binary representation + # TODO test the Gray codes too def _get_binary_vector(self, num, length): if num != 0 and ceil(log2(num)) > length: raise DeveloperError("Invalid input in _get_binary_vector") - # Hack: use python's string formatting instead of bothering with modular - # arithmetic. May be slow. + # Use python's string formatting instead of bothering with modular + # arithmetic. Hopefully not slow. return tuple(int(x) for x in format(num, f"0{length}b")) # Return {P \in \mathcal{P} | B(P)_l = 0} diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 6c551818c84..a5c9b5015d3 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -28,7 +28,7 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - self.DEBUG = False + # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ @@ -46,9 +46,6 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) - if self.DEBUG: - print(f"dimension is {self.dimension}") - # If there was only one choice, don't bother making a disjunction, just # use the linear function directly (but still use the substitute_var for # consistency). @@ -61,15 +58,12 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Add the disjunction transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock) - # Widen bounds as determined when setting up the disjunction + # Set bounds as determined when setting up the disjunction if self.substitute_var_lb < float('inf'): transBlock.substitute_var.setlb(self.substitute_var_lb) if self.substitute_var_ub > -float('inf'): transBlock.substitute_var.setub(self.substitute_var_ub) - if self.DEBUG: - print(f"lb is {self.substitute_var_lb}, ub is {self.substitute_var_ub}") - return substitute_var # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up @@ -77,8 +71,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # many calls deep. def _get_disjunction(self, choices, parent_block, pw_expr, root_block): size = len(choices) - if self.DEBUG: - print(f"calling _get_disjunction with size={size}") + # Our base cases will be 3 and 2, since it would be silly to construct # a Disjunction containing only one Disjunct. We can ensure that size # is never 1 unless it was only passsed a single choice from the start, @@ -88,7 +81,6 @@ def _get_disjunction(self, choices, parent_block, pw_expr, root_block): # This tree will be slightly heavier on the right side choices_l = choices[:half] choices_r = choices[half:] - # Is this valid Pyomo? @parent_block.Disjunct() def d_l(b): b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block) @@ -124,32 +116,38 @@ def d_r(b): "_get_disjunction in nested_inner_repn.py.") def _set_disjunct_block_constraints(self, b, simplex, linear_func, pw_expr, root_block): - # Define the lambdas sparsely like in the version I'm copying, + # Define the lambdas sparsely like in the normal inner repn, # only the first few will participate in constraints b.lambdas = Var(NonNegativeIntegers, dense=False, bounds=(0, 1)) + # Get the extreme points to add up extreme_pts = [] for idx in simplex: extreme_pts.append(self.pw_linear_func._points[idx]) + # Constrain sum(lambda_i) = 1 b.convex_combo = Constraint( expr=sum(b.lambdas[i] for i in range(len(extreme_pts))) == 1 ) linear_func_expr = linear_func(*pw_expr.args) + # Make the substitute Var equal the PWLE b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) + # Widen the variable bounds to those of this linear func expression (lb, ub) = compute_bounds_on_expr(linear_func_expr) if lb is not None and lb < self.substitute_var_lb: self.substitute_var_lb = lb if ub is not None and ub > self.substitute_var_ub: self.substitute_var_ub = ub + # Constrain x = \sum \lambda_i v_i @b.Constraint(range(self.dimension)) def linear_combo(d, i): return pw_expr.args[i] == sum( d.lambdas[j] * pt[i] for j, pt in enumerate(extreme_pts) ) + # Mark the lambdas as local in order to prevent disagreggating multiple # times in the hull transformation b.LocalVars = Suffix(direction=Suffix.LOCAL) From 6f4de26da622a35f446516d57e29e6acf5ded9e8 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 9 Nov 2023 11:38:21 -0500 Subject: [PATCH 0722/3044] apply black --- .../test_disaggregated_logarithmic_gdp.py | 18 ++- .../tests/test_nested_inner_repn_gdp.py | 18 ++- .../transform/disagreggated_logarithmic.py | 25 +++-- .../piecewise/transform/nested_inner_repn.py | 104 ++++++++++++------ 4 files changed, 99 insertions(+), 66 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py index b3dc871882b..d3b58f401f2 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py @@ -20,17 +20,15 @@ from pyomo.gdp import Disjunct, Disjunction from pyomo.environ import Constraint, SolverFactory, Var -from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import DisaggregatedLogarithmicInnerGDPTransformation +from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import ( + DisaggregatedLogarithmicInnerGDPTransformation, +) -class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): +class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): def test_solve_log_model(self): m = models.make_log_x_model() - TransformationFactory( - 'contrib.piecewise.disaggregated_logarithmic' - ).apply_to(m) - TransformationFactory( - 'gdp.bigm' - ).apply_to(m) - SolverFactory('gurobi').solve(m) - ct.check_log_x_model_soln(self, m) \ No newline at end of file + TransformationFactory("contrib.piecewise.disaggregated_logarithmic").apply_to(m) + TransformationFactory("gdp.bigm").apply_to(m) + SolverFactory("gurobi").solve(m) + ct.check_log_x_model_soln(self, m) diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py index 48357c828df..f41233435d4 100644 --- a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -20,17 +20,15 @@ from pyomo.gdp import Disjunct, Disjunction from pyomo.environ import Constraint, SolverFactory, Var -from pyomo.contrib.piecewise.transform.nested_inner_repn import NestedInnerRepresentationGDPTransformation +from pyomo.contrib.piecewise.transform.nested_inner_repn import ( + NestedInnerRepresentationGDPTransformation, +) -class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): +class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): def test_solve_log_model(self): m = models.make_log_x_model() - TransformationFactory( - 'contrib.piecewise.nested_inner_repn_gdp' - ).apply_to(m) - TransformationFactory( - 'gdp.bigm' - ).apply_to(m) - SolverFactory('gurobi').solve(m) - ct.check_log_x_model_soln(self, m) \ No newline at end of file + TransformationFactory("contrib.piecewise.nested_inner_repn_gdp").apply_to(m) + TransformationFactory("gdp.bigm").apply_to(m) + SolverFactory("gurobi").solve(m) + ct.check_log_x_model_soln(self, m) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index a793ad77e38..8a9b493bdfe 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -20,9 +20,9 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): """ Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; - GDP is not used. This method of logarithmically formulating the piecewise - linear function imposes no restrictions on the family of polytopes, but we + log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; + GDP is not used. This method of logarithmically formulating the piecewise + linear function imposes no restrictions on the family of polytopes, but we assume we have simplces in this code. This method is due to Vielma et al., 2010. """ @@ -32,8 +32,7 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - - # Get a new Block for our transformation in transformation_block.transformed_functions, + # Get a new Block for our transformation in transformation_block.transformed_functions, # which is a Block(Any). This is where we will put our new components. transBlock = transformation_block.transformed_functions[ len(transformation_block.transformed_functions) @@ -60,12 +59,16 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc transBlock.simplex_point_indices = RangeSet(0, dimension) # Enumeration of simplices: map from simplex number to simplex object - self.idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} + self.idx_to_simplex = { + k: v for k, v in zip(transBlock.simplex_indices, simplices) + } # List of tuples of simplex indices with their linear function - simplex_indices_and_lin_funcs = list(zip(transBlock.simplex_indices, pw_linear_func._linear_functions)) + simplex_indices_and_lin_funcs = list( + zip(transBlock.simplex_indices, pw_linear_func._linear_functions) + ) - # We don't seem to get a convenient opportunity later, so let's just widen + # We don't seem to get a convenient opportunity later, so let's just widen # the bounds here. All we need to do is go through the corners of each simplex. for P, linear_func in simplex_indices_and_lin_funcs: for v in transBlock.simplex_point_indices: @@ -90,9 +93,11 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc B[i] = self._get_binary_vector(i, log_dimension) # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it - transBlock.lambdas = Var(transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1)) + transBlock.lambdas = Var( + transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1) + ) - # Numbered citations are from Vielma et al 2010, Mixed-Integer Models + # Numbered citations are from Vielma et al 2010, Mixed-Integer Models # for Nonseparable Piecewise-Linear Optimization # Sum of all lambdas is one (6b) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index a5c9b5015d3..af8728c0605 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -7,28 +7,29 @@ from pyomo.gdp import Disjunction from pyomo.common.errors import DeveloperError + @TransformationFactory.register( - 'contrib.piecewise.nested_inner_repn_gdp', - doc="TODO document", # TODO + "contrib.piecewise.nested_inner_repn_gdp", + doc="TODO document", # TODO ) class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): """ - Represent a piecewise linear function by using a nested GDP to determine - which polytope a point is in, then representing it as a convex combination - of extreme points, with multipliers "local" to that particular polytope, - i.e., not shared with neighbors. This method of formulating the piecewise - linear function imposes no restrictions on the family of polytopes. Note - that this is NOT a logarithmic formulation - it has linearly many binaries. - This method was, however, inspired by the disagreggated logarithmic + Represent a piecewise linear function by using a nested GDP to determine + which polytope a point is in, then representing it as a convex combination + of extreme points, with multipliers "local" to that particular polytope, + i.e., not shared with neighbors. This method of formulating the piecewise + linear function imposes no restrictions on the family of polytopes. Note + that this is NOT a logarithmic formulation - it has linearly many binaries. + This method was, however, inspired by the disagreggated logarithmic formulation of Vielma et al., 2010. """ + CONFIG = PiecewiseLinearToGDP.CONFIG() - _transformation_name = 'pw_linear_nested_inner_repn' - + _transformation_name = "pw_linear_nested_inner_repn" + # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ @@ -41,29 +42,35 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc self.dimension = pw_expr.nargs() substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) - self.substitute_var_lb = float('inf') - self.substitute_var_ub = -float('inf') - + self.substitute_var_lb = float("inf") + self.substitute_var_ub = -float("inf") + choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) # If there was only one choice, don't bother making a disjunction, just - # use the linear function directly (but still use the substitute_var for + # use the linear function directly (but still use the substitute_var for # consistency). if len(choices) == 1: - (_, linear_func) = choices[0] # simplex isn't important in this case + (_, linear_func) = choices[0] # simplex isn't important in this case linear_func_expr = linear_func(*pw_expr.args) - transBlock.set_substitute = Constraint(expr=substitute_var == linear_func_expr) - (self.substitute_var_lb, self.substitute_var_ub) = compute_bounds_on_expr(linear_func_expr) + transBlock.set_substitute = Constraint( + expr=substitute_var == linear_func_expr + ) + (self.substitute_var_lb, self.substitute_var_ub) = compute_bounds_on_expr( + linear_func_expr + ) else: # Add the disjunction - transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock) + transBlock.disj = self._get_disjunction( + choices, transBlock, pw_expr, transBlock + ) # Set bounds as determined when setting up the disjunction - if self.substitute_var_lb < float('inf'): + if self.substitute_var_lb < float("inf"): transBlock.substitute_var.setlb(self.substitute_var_lb) - if self.substitute_var_ub > -float('inf'): + if self.substitute_var_ub > -float("inf"): transBlock.substitute_var.setub(self.substitute_var_ub) - + return substitute_var # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up @@ -76,17 +83,24 @@ def _get_disjunction(self, choices, parent_block, pw_expr, root_block): # a Disjunction containing only one Disjunct. We can ensure that size # is never 1 unless it was only passsed a single choice from the start, # which we can handle before calling. - if size > 3: - half = size // 2 # (integer divide) + if size > 3: + half = size // 2 # (integer divide) # This tree will be slightly heavier on the right side choices_l = choices[:half] choices_r = choices[half:] + @parent_block.Disjunct() def d_l(b): - b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block) + b.inner_disjunction_l = self._get_disjunction( + choices_l, b, pw_expr, root_block + ) + @parent_block.Disjunct() def d_r(b): - b.inner_disjunction_r = self._get_disjunction(choices_r, b, pw_expr, root_block) + b.inner_disjunction_r = self._get_disjunction( + choices_r, b, pw_expr, root_block + ) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 3: # Let's stay heavier on the right side for consistency. So the left @@ -95,27 +109,43 @@ def d_r(b): @parent_block.Disjunct() def d_l(b): simplex, linear_func = choices[0] - self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + self._set_disjunct_block_constraints( + b, simplex, linear_func, pw_expr, root_block + ) + @parent_block.Disjunct() def d_r(b): - b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block) + b.inner_disjunction_r = self._get_disjunction( + choices[1:], b, pw_expr, root_block + ) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 2: # In this case both sides are regular Disjuncts @parent_block.Disjunct() def d_l(b): simplex, linear_func = choices[0] - self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + self._set_disjunct_block_constraints( + b, simplex, linear_func, pw_expr, root_block + ) + @parent_block.Disjunct() def d_r(b): simplex, linear_func = choices[1] - self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + self._set_disjunct_block_constraints( + b, simplex, linear_func, pw_expr, root_block + ) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) else: - raise DeveloperError("Unreachable: 1 or 0 choices were passed to " - "_get_disjunction in nested_inner_repn.py.") + raise DeveloperError( + "Unreachable: 1 or 0 choices were passed to " + "_get_disjunction in nested_inner_repn.py." + ) - def _set_disjunct_block_constraints(self, b, simplex, linear_func, pw_expr, root_block): + def _set_disjunct_block_constraints( + self, b, simplex, linear_func, pw_expr, root_block + ): # Define the lambdas sparsely like in the normal inner repn, # only the first few will participate in constraints b.lambdas = Var(NonNegativeIntegers, dense=False, bounds=(0, 1)) @@ -125,14 +155,16 @@ def _set_disjunct_block_constraints(self, b, simplex, linear_func, pw_expr, root for idx in simplex: extreme_pts.append(self.pw_linear_func._points[idx]) - # Constrain sum(lambda_i) = 1 + # Constrain sum(lambda_i) = 1 b.convex_combo = Constraint( expr=sum(b.lambdas[i] for i in range(len(extreme_pts))) == 1 ) linear_func_expr = linear_func(*pw_expr.args) # Make the substitute Var equal the PWLE - b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) + b.set_substitute = Constraint( + expr=root_block.substitute_var == linear_func_expr + ) # Widen the variable bounds to those of this linear func expression (lb, ub) = compute_bounds_on_expr(linear_func_expr) From c11fa709e5577fbff10b28d6b66aee0b71668304 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 14 Nov 2023 13:16:51 -0500 Subject: [PATCH 0723/3044] rename: PiecewiseLinearToGDP->PiecewiseLinearTransformationBase, since it isn't GDP-specific --- .../transform/disagreggated_logarithmic.py | 6 +++--- .../transform/inner_representation_gdp.py | 6 +++--- .../piecewise/transform/nested_inner_repn.py | 17 +++++++++++++---- .../transform/outer_representation_gdp.py | 6 +++--- .../piecewise_to_gdp_transformation.py | 2 +- .../reduced_inner_representation_gdp.py | 6 +++--- 6 files changed, 26 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index 8a9b493bdfe..e10c1ee8091 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -1,5 +1,5 @@ from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, + PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, Binary, Var, RangeSet from pyomo.core.base import TransformationFactory @@ -17,7 +17,7 @@ assume we have simplces in this code. This method is due to Vielma et al., 2010. """, ) -class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): +class DisaggregatedLogarithmicInnerMIPTransformation(PiecewiseLinearTransformationBase): """ Represent a piecewise linear function "logarithmically" by using a MIP with log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; @@ -26,7 +26,7 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): assume we have simplces in this code. This method is due to Vielma et al., 2010. """ - CONFIG = PiecewiseLinearToGDP.CONFIG() + CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = "pw_linear_disaggregated_log" # Implement to use PiecewiseLinearToGDP. This function returns the Var diff --git a/pyomo/contrib/piecewise/transform/inner_representation_gdp.py b/pyomo/contrib/piecewise/transform/inner_representation_gdp.py index f0be2d98825..25b8664ccf2 100644 --- a/pyomo/contrib/piecewise/transform/inner_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/inner_representation_gdp.py @@ -11,7 +11,7 @@ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, + PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var from pyomo.core.base import TransformationFactory @@ -25,7 +25,7 @@ "simplices that are the domains of the linear " "functions.", ) -class InnerRepresentationGDPTransformation(PiecewiseLinearToGDP): +class InnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase): """ Convert a model involving piecewise linear expressions into a GDP by representing the piecewise linear functions as Disjunctions where the @@ -49,7 +49,7 @@ class InnerRepresentationGDPTransformation(PiecewiseLinearToGDP): this mode, targets must be Blocks, Constraints, and/or Objectives. """ - CONFIG = PiecewiseLinearToGDP.CONFIG() + CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = 'pw_linear_inner_repn' def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index af8728c0605..6900d1f3322 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -1,6 +1,6 @@ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, + PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var from pyomo.core.base import TransformationFactory @@ -10,9 +10,18 @@ @TransformationFactory.register( "contrib.piecewise.nested_inner_repn_gdp", - doc="TODO document", # TODO + doc=""" + Represent a piecewise linear function by using a nested GDP to determine + which polytope a point is in, then representing it as a convex combination + of extreme points, with multipliers "local" to that particular polytope, + i.e., not shared with neighbors. This method of formulating the piecewise + linear function imposes no restrictions on the family of polytopes. Note + that this is NOT a logarithmic formulation - it has linearly many binaries. + This method was, however, inspired by the disagreggated logarithmic + formulation of Vielma et al., 2010. + """ ) -class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): +class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase): """ Represent a piecewise linear function by using a nested GDP to determine which polytope a point is in, then representing it as a convex combination @@ -24,7 +33,7 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): formulation of Vielma et al., 2010. """ - CONFIG = PiecewiseLinearToGDP.CONFIG() + CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = "pw_linear_nested_inner_repn" # Implement to use PiecewiseLinearToGDP. This function returns the Var diff --git a/pyomo/contrib/piecewise/transform/outer_representation_gdp.py b/pyomo/contrib/piecewise/transform/outer_representation_gdp.py index 7c81619430a..bd50b9c708f 100644 --- a/pyomo/contrib/piecewise/transform/outer_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/outer_representation_gdp.py @@ -13,7 +13,7 @@ from pyomo.common.dependencies.scipy import spatial from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, + PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var from pyomo.core.base import TransformationFactory @@ -27,7 +27,7 @@ "the simplices that are the domains of the " "linear functions.", ) -class OuterRepresentationGDPTransformation(PiecewiseLinearToGDP): +class OuterRepresentationGDPTransformation(PiecewiseLinearTransformationBase): """ Convert a model involving piecewise linear expressions into a GDP by representing the piecewise linear functions as Disjunctions where the @@ -49,7 +49,7 @@ class OuterRepresentationGDPTransformation(PiecewiseLinearToGDP): this mode, targets must be Blocks, Constraints, and/or Objectives. """ - CONFIG = PiecewiseLinearToGDP.CONFIG() + CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = 'pw_linear_outer_repn' def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py index 2e056c47a15..d8e46ad7311 100644 --- a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py +++ b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py @@ -40,7 +40,7 @@ from pyomo.network import Port -class PiecewiseLinearToGDP(Transformation): +class PiecewiseLinearTransformationBase(Transformation): """ Base class for transformations of piecewise-linear models to GDPs """ diff --git a/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py b/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py index 5c7dfa895ab..86c33e40623 100644 --- a/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py @@ -11,7 +11,7 @@ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, + PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Var from pyomo.core.base import TransformationFactory @@ -25,7 +25,7 @@ "simplices that are the domains of the linear " "functions.", ) -class ReducedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): +class ReducedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase): """ Convert a model involving piecewise linear expressions into a GDP by representing the piecewise linear functions as Disjunctions where the @@ -51,7 +51,7 @@ class ReducedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): this mode, targets must be Blocks, Constraints, and/or Objectives. """ - CONFIG = PiecewiseLinearToGDP.CONFIG() + CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = 'pw_linear_reduced_inner_repn' def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): From f34bf30c73d3297cc88f33a21be0938379feafa3 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 14 Nov 2023 13:21:37 -0500 Subject: [PATCH 0724/3044] apply black --- pyomo/contrib/piecewise/transform/nested_inner_repn.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 6900d1f3322..17b85604337 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -19,7 +19,7 @@ that this is NOT a logarithmic formulation - it has linearly many binaries. This method was, however, inspired by the disagreggated logarithmic formulation of Vielma et al., 2010. - """ + """, ) class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase): """ From 7acb187a0737c885d1fb4e78cf5ca13c18908ea3 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 14 Nov 2023 14:15:13 -0500 Subject: [PATCH 0725/3044] fix typos and add title for reference --- .../transform/disagreggated_logarithmic.py | 14 +++++++++----- .../piecewise/transform/nested_inner_repn.py | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index e10c1ee8091..c6c492c70a3 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -11,10 +11,12 @@ "contrib.piecewise.disaggregated_logarithmic", doc=""" Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; - GDP is not used. This method of logarithmically formulating the piecewise - linear function imposes no restrictions on the family of polytopes, but we - assume we have simplces in this code. This method is due to Vielma et al., 2010. + log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; + GDP is not used. This method of logarithmically formulating the piecewise + linear function imposes no restrictions on the family of polytopes, but we + assume we have simplices in this code. This method is due to Vielma, Ahmed, + and Nemhauser 2010, Mixed-Integer Models for Nonseparable Piecewise-Linear + Optimization. """, ) class DisaggregatedLogarithmicInnerMIPTransformation(PiecewiseLinearTransformationBase): @@ -23,7 +25,9 @@ class DisaggregatedLogarithmicInnerMIPTransformation(PiecewiseLinearTransformati log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; GDP is not used. This method of logarithmically formulating the piecewise linear function imposes no restrictions on the family of polytopes, but we - assume we have simplces in this code. This method is due to Vielma et al., 2010. + assume we have simplices in this code. This method is due to Vielma, Ahmed, + and Nemhauser 2010, Mixed-Integer Models for Nonseparable Piecewise-Linear + Optimization. """ CONFIG = PiecewiseLinearTransformationBase.CONFIG() diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 17b85604337..f6939a8a288 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -90,7 +90,7 @@ def _get_disjunction(self, choices, parent_block, pw_expr, root_block): # Our base cases will be 3 and 2, since it would be silly to construct # a Disjunction containing only one Disjunct. We can ensure that size - # is never 1 unless it was only passsed a single choice from the start, + # is never 1 unless it was only passed a single choice from the start, # which we can handle before calling. if size > 3: half = size // 2 # (integer divide) From 6548b6207217bd7ef48356275505242b8845fa15 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 14 Nov 2023 14:18:31 -0500 Subject: [PATCH 0726/3044] fix incorrect imports and comments --- ...d_logarithmic_gdp.py => test_disaggregated_logarithmic.py} | 4 ++-- .../contrib/piecewise/transform/disagreggated_logarithmic.py | 2 +- pyomo/contrib/piecewise/transform/nested_inner_repn.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename pyomo/contrib/piecewise/tests/{test_disaggregated_logarithmic_gdp.py => test_disaggregated_logarithmic.py} (92%) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py similarity index 92% rename from pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py rename to pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py index d3b58f401f2..8a225985d82 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -21,11 +21,11 @@ from pyomo.environ import Constraint, SolverFactory, Var from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import ( - DisaggregatedLogarithmicInnerGDPTransformation, + DisaggregatedLogarithmicInnerMIPTransformation ) -class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): +class TestTransformPiecewiseModelToNestedInnerRepnMIP(unittest.TestCase): def test_solve_log_model(self): m = models.make_log_x_model() TransformationFactory("contrib.piecewise.disaggregated_logarithmic").apply_to(m) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index c6c492c70a3..1104b1c265b 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -33,7 +33,7 @@ class DisaggregatedLogarithmicInnerMIPTransformation(PiecewiseLinearTransformati CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = "pw_linear_disaggregated_log" - # Implement to use PiecewiseLinearToGDP. This function returns the Var + # Implement to use PiecewiseLinearTransformationBase. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): # Get a new Block for our transformation in transformation_block.transformed_functions, diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index f6939a8a288..7f5c54dd9a2 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -36,7 +36,7 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBa CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = "pw_linear_nested_inner_repn" - # Implement to use PiecewiseLinearToGDP. This function returns the Var + # Implement to use PiecewiseLinearTransformationBase. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): # Get a new Block() in transformation_block.transformed_functions, which From ae953e894f4e3be1e9ae07dcf487978e9dbba25a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 14 Nov 2023 14:22:00 -0500 Subject: [PATCH 0727/3044] register transformations in __init__.py so they don't need to be imported --- pyomo/contrib/piecewise/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 37873c83b3b..de18e559a93 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -33,3 +33,9 @@ from pyomo.contrib.piecewise.transform.convex_combination import ( ConvexCombinationTransformation, ) +from pyomo.contrib.piecewise.transform.nested_inner_repn import ( + NestedInnerRepresentationGDPTransformation, +) +from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import ( + DisaggregatedLogarithmicInnerMIPTransformation, +) From 63af6266ab73d74547d837ee33b260bdadc71183 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 14 Nov 2023 14:26:32 -0500 Subject: [PATCH 0728/3044] remove unused (for now) imports --- .../piecewise/tests/test_disaggregated_logarithmic.py | 11 +---------- .../piecewise/tests/test_nested_inner_repn_gdp.py | 11 +---------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py index 8a225985d82..8fd4bebfc37 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -13,16 +13,7 @@ from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct from pyomo.core.base import TransformationFactory -from pyomo.core.expr.compare import ( - assertExpressionsEqual, - assertExpressionsStructurallyEqual, -) -from pyomo.gdp import Disjunct, Disjunction -from pyomo.environ import Constraint, SolverFactory, Var - -from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import ( - DisaggregatedLogarithmicInnerMIPTransformation -) +from pyomo.environ import SolverFactory class TestTransformPiecewiseModelToNestedInnerRepnMIP(unittest.TestCase): diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py index f41233435d4..fd8e7ab201c 100644 --- a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -13,16 +13,7 @@ from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct from pyomo.core.base import TransformationFactory -from pyomo.core.expr.compare import ( - assertExpressionsEqual, - assertExpressionsStructurallyEqual, -) -from pyomo.gdp import Disjunct, Disjunction -from pyomo.environ import Constraint, SolverFactory, Var - -from pyomo.contrib.piecewise.transform.nested_inner_repn import ( - NestedInnerRepresentationGDPTransformation, -) +from pyomo.environ import SolverFactory class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): From acfa476b9deb3ed3f2f141dd925fb2a54df3db3a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 14 Nov 2023 14:34:26 -0500 Subject: [PATCH 0729/3044] do the reference properly --- .../transform/disagreggated_logarithmic.py | 24 ++++++++++--------- .../piecewise/transform/nested_inner_repn.py | 16 +++++++------ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index 1104b1c265b..4fa7a0eeeb1 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -12,22 +12,24 @@ doc=""" Represent a piecewise linear function "logarithmically" by using a MIP with log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; - GDP is not used. This method of logarithmically formulating the piecewise - linear function imposes no restrictions on the family of polytopes, but we - assume we have simplices in this code. This method is due to Vielma, Ahmed, - and Nemhauser 2010, Mixed-Integer Models for Nonseparable Piecewise-Linear - Optimization. + GDP is not used. """, ) class DisaggregatedLogarithmicInnerMIPTransformation(PiecewiseLinearTransformationBase): """ Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; - GDP is not used. This method of logarithmically formulating the piecewise - linear function imposes no restrictions on the family of polytopes, but we - assume we have simplices in this code. This method is due to Vielma, Ahmed, - and Nemhauser 2010, Mixed-Integer Models for Nonseparable Piecewise-Linear - Optimization. + log_2(|P|) binary decision variables, following the "disaggregated logarithmic" + method from [1]. This is a direct-to-MIP transformation; GDP is not used. + This method of logarithmically formulating the piecewise linear function + imposes no restrictions on the family of polytopes, but we assume we have + simplices in this code. + + References + ---------- + [1] J.P. Vielma, S. Ahmed, and G. Nemhauser, "Mixed-integer models + for nonseparable piecewise-linear optimization: unifying framework + and extensions," Operations Research, vol. 58, no. 2, pp. 305-315, + 2010. """ CONFIG = PiecewiseLinearTransformationBase.CONFIG() diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 7f5c54dd9a2..67abd815c7f 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -14,11 +14,7 @@ Represent a piecewise linear function by using a nested GDP to determine which polytope a point is in, then representing it as a convex combination of extreme points, with multipliers "local" to that particular polytope, - i.e., not shared with neighbors. This method of formulating the piecewise - linear function imposes no restrictions on the family of polytopes. Note - that this is NOT a logarithmic formulation - it has linearly many binaries. - This method was, however, inspired by the disagreggated logarithmic - formulation of Vielma et al., 2010. + i.e., not shared with neighbors. This formulation has linearly many binaries. """, ) class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase): @@ -29,8 +25,14 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBa i.e., not shared with neighbors. This method of formulating the piecewise linear function imposes no restrictions on the family of polytopes. Note that this is NOT a logarithmic formulation - it has linearly many binaries. - This method was, however, inspired by the disagreggated logarithmic - formulation of Vielma et al., 2010. + However, it is inspired by the disaggregated logarithmic formulation of [1]. + + References + ---------- + [1] J.P. Vielma, S. Ahmed, and G. Nemhauser, "Mixed-integer models + for nonseparable piecewise-linear optimization: unifying framework + and extensions," Operations Research, vol. 58, no. 2, pp. 305-315, + 2010. """ CONFIG = PiecewiseLinearTransformationBase.CONFIG() From 81960f146d22f54e00ab8db6a11e758545eca41f Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 14 Dec 2023 18:48:01 -0500 Subject: [PATCH 0730/3044] stop using `self` unnecessarily --- .../transform/disagreggated_logarithmic.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index 4fa7a0eeeb1..4b388d9df1e 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -53,19 +53,19 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc pw_linear_func.map_transformation_var(pw_expr, substitute_var) # Bounds for the substitute_var that we will widen - self.substitute_var_lb = float("inf") - self.substitute_var_ub = -float("inf") + substitute_var_lb = float("inf") + substitute_var_ub = -float("inf") # Simplices are tuples of indices of points. Give them their own indices, too simplices = pw_linear_func._simplices num_simplices = len(simplices) transBlock.simplex_indices = RangeSet(0, num_simplices - 1) - # Assumption: the simplices are really simplices and all have the same number of points, - # which is dimension + 1 + # Assumption: the simplices are really full-dimensional simplices and all have the + # same number of points, which is dimension + 1 transBlock.simplex_point_indices = RangeSet(0, dimension) # Enumeration of simplices: map from simplex number to simplex object - self.idx_to_simplex = { + idx_to_simplex = { k: v for k, v in zip(transBlock.simplex_indices, simplices) } @@ -78,13 +78,13 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # the bounds here. All we need to do is go through the corners of each simplex. for P, linear_func in simplex_indices_and_lin_funcs: for v in transBlock.simplex_point_indices: - val = linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) - if val < self.substitute_var_lb: - self.substitute_var_lb = val - if val > self.substitute_var_ub: - self.substitute_var_ub = val - transBlock.substitute_var.setlb(self.substitute_var_lb) - transBlock.substitute_var.setub(self.substitute_var_ub) + val = linear_func(*pw_linear_func._points[idx_to_simplex[P][v]]) + if val < substitute_var_lb: + substitute_var_lb = val + if val > substitute_var_ub: + substitute_var_ub = val + transBlock.substitute_var.setlb(substitute_var_lb) + transBlock.substitute_var.setub(substitute_var_ub) log_dimension = ceil(log2(num_simplices)) transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) @@ -146,7 +146,7 @@ def simplex_choice_2(b, l): def x_constraint(b, i): return pw_expr.args[i] == sum( transBlock.lambdas[P, v] - * pw_linear_func._points[self.idx_to_simplex[P][v]][i] + * pw_linear_func._points[idx_to_simplex[P][v]][i] for P in transBlock.simplex_indices for v in transBlock.simplex_point_indices ) @@ -157,7 +157,7 @@ def x_constraint(b, i): == sum( sum( transBlock.lambdas[P, v] - * linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) + * linear_func(*pw_linear_func._points[idx_to_simplex[P][v]]) for v in transBlock.simplex_point_indices ) for (P, linear_func) in simplex_indices_and_lin_funcs From c7975cdd48c1b058e15e81ec7e06bfa34c8bbe3a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 14 Dec 2023 18:55:07 -0500 Subject: [PATCH 0731/3044] rename file to match refactored class name --- pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py | 2 +- pyomo/contrib/piecewise/transform/inner_representation_gdp.py | 2 +- pyomo/contrib/piecewise/transform/nested_inner_repn.py | 2 +- pyomo/contrib/piecewise/transform/outer_representation_gdp.py | 2 +- ...ransformation.py => piecewise_linear_transformation_base.py} | 0 .../piecewise/transform/reduced_inner_representation_gdp.py | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename pyomo/contrib/piecewise/transform/{piecewise_to_gdp_transformation.py => piecewise_linear_transformation_base.py} (100%) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index 4b388d9df1e..4e399d714f7 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -1,4 +1,4 @@ -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, Binary, Var, RangeSet diff --git a/pyomo/contrib/piecewise/transform/inner_representation_gdp.py b/pyomo/contrib/piecewise/transform/inner_representation_gdp.py index 25b8664ccf2..e4818c1cbb9 100644 --- a/pyomo/contrib/piecewise/transform/inner_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/inner_representation_gdp.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 67abd815c7f..e7e76dc3778 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -1,5 +1,5 @@ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var diff --git a/pyomo/contrib/piecewise/transform/outer_representation_gdp.py b/pyomo/contrib/piecewise/transform/outer_representation_gdp.py index bd50b9c708f..6c26772fe6a 100644 --- a/pyomo/contrib/piecewise/transform/outer_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/outer_representation_gdp.py @@ -12,7 +12,7 @@ import pyomo.common.dependencies.numpy as np from pyomo.common.dependencies.scipy import spatial from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py b/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py similarity index 100% rename from pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py rename to pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py diff --git a/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py b/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py index 86c33e40623..a19507a93fd 100644 --- a/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py +++ b/pyomo/contrib/piecewise/transform/reduced_inner_representation_gdp.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) from pyomo.core import Constraint, NonNegativeIntegers, Var From 526305c6c3c87c918bed3f1be4e1f43bc325dea9 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 14 Dec 2023 22:06:24 -0500 Subject: [PATCH 0732/3044] minor refactors --- .../piecewise/transform/nested_inner_repn.py | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index e7e76dc3778..6ee0e6c9e80 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -14,7 +14,8 @@ Represent a piecewise linear function by using a nested GDP to determine which polytope a point is in, then representing it as a convex combination of extreme points, with multipliers "local" to that particular polytope, - i.e., not shared with neighbors. This formulation has linearly many binaries. + i.e., not shared with neighbors. This formulation has linearly many Boolean + variables, though up to variable substitution, it has logarithmically many. """, ) class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBase): @@ -24,8 +25,10 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBa of extreme points, with multipliers "local" to that particular polytope, i.e., not shared with neighbors. This method of formulating the piecewise linear function imposes no restrictions on the family of polytopes. Note - that this is NOT a logarithmic formulation - it has linearly many binaries. - However, it is inspired by the disaggregated logarithmic formulation of [1]. + that this is NOT a logarithmic formulation - it has linearly many Boolean + variables. However, it is inspired by the disaggregated logarithmic + formulation of [1]. Up to variable substitution, the amount of Boolean + variables is logarithmic, as in [1]. References ---------- @@ -47,14 +50,10 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc len(transformation_block.transformed_functions) ] - # these copy-pasted lines (from inner_representation_gdp) seem useful - # adding some of this stuff to self so I don't have to pass it around - self.pw_linear_func = pw_linear_func - self.dimension = pw_expr.nargs() substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) - self.substitute_var_lb = float("inf") - self.substitute_var_ub = -float("inf") + substitute_var_lb = float("inf") + substitute_var_ub = -float("inf") choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) @@ -67,27 +66,27 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc transBlock.set_substitute = Constraint( expr=substitute_var == linear_func_expr ) - (self.substitute_var_lb, self.substitute_var_ub) = compute_bounds_on_expr( + (substitute_var_lb, substitute_var_ub) = compute_bounds_on_expr( linear_func_expr ) else: # Add the disjunction transBlock.disj = self._get_disjunction( - choices, transBlock, pw_expr, transBlock + choices, transBlock, pw_expr, pw_linear_func, transBlock ) # Set bounds as determined when setting up the disjunction - if self.substitute_var_lb < float("inf"): - transBlock.substitute_var.setlb(self.substitute_var_lb) - if self.substitute_var_ub > -float("inf"): - transBlock.substitute_var.setub(self.substitute_var_ub) + if substitute_var_lb < float("inf"): + transBlock.substitute_var.setlb(substitute_var_lb) + if substitute_var_ub > -float("inf"): + transBlock.substitute_var.setub(substitute_var_ub) return substitute_var # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up # the stack, since the whole point is that we'll only go logarithmically # many calls deep. - def _get_disjunction(self, choices, parent_block, pw_expr, root_block): + def _get_disjunction(self, choices, parent_block, pw_expr, pw_linear_func, root_block): size = len(choices) # Our base cases will be 3 and 2, since it would be silly to construct @@ -103,13 +102,13 @@ def _get_disjunction(self, choices, parent_block, pw_expr, root_block): @parent_block.Disjunct() def d_l(b): b.inner_disjunction_l = self._get_disjunction( - choices_l, b, pw_expr, root_block + choices_l, b, pw_expr, pw_linear_func, root_block ) @parent_block.Disjunct() def d_r(b): b.inner_disjunction_r = self._get_disjunction( - choices_r, b, pw_expr, root_block + choices_r, b, pw_expr, pw_linear_func, root_block ) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) @@ -121,13 +120,13 @@ def d_r(b): def d_l(b): simplex, linear_func = choices[0] self._set_disjunct_block_constraints( - b, simplex, linear_func, pw_expr, root_block + b, simplex, linear_func, pw_expr, pw_linear_func, root_block ) @parent_block.Disjunct() def d_r(b): b.inner_disjunction_r = self._get_disjunction( - choices[1:], b, pw_expr, root_block + choices[1:], b, pw_expr, pw_linear_func, root_block ) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) @@ -137,14 +136,14 @@ def d_r(b): def d_l(b): simplex, linear_func = choices[0] self._set_disjunct_block_constraints( - b, simplex, linear_func, pw_expr, root_block + b, simplex, linear_func, pw_expr, pw_linear_func, root_block ) @parent_block.Disjunct() def d_r(b): simplex, linear_func = choices[1] self._set_disjunct_block_constraints( - b, simplex, linear_func, pw_expr, root_block + b, simplex, linear_func, pw_expr, pw_linear_func, root_block ) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) @@ -155,7 +154,7 @@ def d_r(b): ) def _set_disjunct_block_constraints( - self, b, simplex, linear_func, pw_expr, root_block + self, b, simplex, linear_func, pw_expr, pw_linear_func, root_block ): # Define the lambdas sparsely like in the normal inner repn, # only the first few will participate in constraints @@ -164,7 +163,7 @@ def _set_disjunct_block_constraints( # Get the extreme points to add up extreme_pts = [] for idx in simplex: - extreme_pts.append(self.pw_linear_func._points[idx]) + extreme_pts.append(pw_linear_func._points[idx]) # Constrain sum(lambda_i) = 1 b.convex_combo = Constraint( @@ -179,13 +178,13 @@ def _set_disjunct_block_constraints( # Widen the variable bounds to those of this linear func expression (lb, ub) = compute_bounds_on_expr(linear_func_expr) - if lb is not None and lb < self.substitute_var_lb: - self.substitute_var_lb = lb - if ub is not None and ub > self.substitute_var_ub: - self.substitute_var_ub = ub + if lb is not None and lb < substitute_var_lb: + substitute_var_lb = lb + if ub is not None and ub > substitute_var_ub: + substitute_var_ub = ub # Constrain x = \sum \lambda_i v_i - @b.Constraint(range(self.dimension)) + @b.Constraint(range(pw_expr.nargs())) # dimension def linear_combo(d, i): return pw_expr.args[i] == sum( d.lambdas[j] * pt[i] for j, pt in enumerate(extreme_pts) From d6b9f8829dfe1d436a754955d29d1030269d791a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 21 Dec 2023 19:26:43 -0500 Subject: [PATCH 0733/3044] Fix needless quadratic loops I think this also could've been achieved by reordering the iterators, but using indexed Sets should be more clear --- .../transform/disagreggated_logarithmic.py | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index 4e399d714f7..e22bba0215f 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -98,6 +98,18 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # map index(P) -> corresponding vector in {0, 1}^n B[i] = self._get_binary_vector(i, log_dimension) + # Build up P_0 and P_plus ahead of time. + + # {P \in \mathcal{P} | B(P)_l = 0} + @transBlock.Set(transBlock.simplex_indices) + def P_0(l): + return [p for p in transBlock.simplex_indices if B[p][l] == 0] + + # {P \in \mathcal{P} | B(P)_l = 1} + @transBlock.Set(transBlock.simplex_indices) + def P_0(l): + return [p for p in transBlock.simplex_indices if B[p][l] == 1] + # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it transBlock.lambdas = Var( transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1) @@ -116,14 +128,14 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc == 1 ) - # The branching rules, establishing using the binaries that only one simplex's lambdas - # may be nonzero + # The branching rules, establishing using the binaries that only one simplex's lambda + # coefficients may be nonzero @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) def simplex_choice_1(b, l): return ( sum( transBlock.lambdas[P, v] - for P in self._P_plus(B, l, transBlock.simplex_indices) + for P in transBlock.P_plus[l] for v in transBlock.simplex_point_indices ) <= transBlock.binaries[l] @@ -134,7 +146,7 @@ def simplex_choice_2(b, l): return ( sum( transBlock.lambdas[P, v] - for P in self._P_0(B, l, transBlock.simplex_indices) + for P in transBlock.P_0[l] for v in transBlock.simplex_point_indices ) <= 1 - transBlock.binaries[l] @@ -174,11 +186,3 @@ def _get_binary_vector(self, num, length): # Use python's string formatting instead of bothering with modular # arithmetic. Hopefully not slow. return tuple(int(x) for x in format(num, f"0{length}b")) - - # Return {P \in \mathcal{P} | B(P)_l = 0} - def _P_0(self, B, l, simplex_indices): - return [p for p in simplex_indices if B[p][l] == 0] - - # Return {P \in \mathcal{P} | B(P)_l = 1} - def _P_plus(self, B, l, simplex_indices): - return [p for p in simplex_indices if B[p][l] == 1] From 1248806195707f1a27e49ecc3dd9b1b1e2e9c794 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 21 Dec 2023 20:41:43 -0500 Subject: [PATCH 0734/3044] fix scoping error --- .../piecewise/transform/nested_inner_repn.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 6ee0e6c9e80..143bd827c58 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -52,8 +52,8 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) - substitute_var_lb = float("inf") - substitute_var_ub = -float("inf") + transBlock.substitute_var_lb = float("inf") + transBlock.substitute_var_ub = -float("inf") choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) @@ -66,7 +66,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc transBlock.set_substitute = Constraint( expr=substitute_var == linear_func_expr ) - (substitute_var_lb, substitute_var_ub) = compute_bounds_on_expr( + (transBlock.substitute_var_lb, transBlock.substitute_var_ub) = compute_bounds_on_expr( linear_func_expr ) else: @@ -76,10 +76,10 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc ) # Set bounds as determined when setting up the disjunction - if substitute_var_lb < float("inf"): - transBlock.substitute_var.setlb(substitute_var_lb) - if substitute_var_ub > -float("inf"): - transBlock.substitute_var.setub(substitute_var_ub) + if transBlock.substitute_var_lb < float("inf"): + transBlock.substitute_var.setlb(transBlock.substitute_var_lb) + if transBlock.substitute_var_ub > -float("inf"): + transBlock.substitute_var.setub(transBlock.substitute_var_ub) return substitute_var @@ -178,10 +178,10 @@ def _set_disjunct_block_constraints( # Widen the variable bounds to those of this linear func expression (lb, ub) = compute_bounds_on_expr(linear_func_expr) - if lb is not None and lb < substitute_var_lb: - substitute_var_lb = lb - if ub is not None and ub > substitute_var_ub: - substitute_var_ub = ub + if lb is not None and lb < root_block.substitute_var_lb: + root_block.substitute_var_lb = lb + if ub is not None and ub > root_block.substitute_var_ub: + root_block.substitute_var_ub = ub # Constrain x = \sum \lambda_i v_i @b.Constraint(range(pw_expr.nargs())) # dimension From 18821c7a4db6934705f6d7dc4ba463880e583137 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 22 Dec 2023 13:55:48 -0500 Subject: [PATCH 0735/3044] proper testing for nested_inner_repn_gdp --- .../tests/test_nested_inner_repn_gdp.py | 169 +++++++++++++++++- 1 file changed, 167 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py index fd8e7ab201c..4deeb9abe78 100644 --- a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -13,10 +13,175 @@ from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct from pyomo.core.base import TransformationFactory -from pyomo.environ import SolverFactory - +from pyomo.environ import SolverFactory, Var, Constraint +from pyomo.gdp import Disjunction, Disjunct +from pyomo.core.expr.compare import assertExpressionsEqual +# Test the nested inner repn gdp model using the common_tests code class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): + + # Check one disjunct for proper contents. Disjunct structure should be + # identical to the version for the inner representation gdp + def check_log_disjunct(self, d, pts, f, substitute_var, x): + self.assertEqual(len(d.component_map(Constraint)), 3) + # lambdas and indicator_var + self.assertEqual(len(d.component_map(Var)), 2) + self.assertIsInstance(d.lambdas, Var) + self.assertEqual(len(d.lambdas), 2) + for lamb in d.lambdas.values(): + self.assertEqual(lamb.lb, 0) + self.assertEqual(lamb.ub, 1) + self.assertIsInstance(d.convex_combo, Constraint) + assertExpressionsEqual( + self, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] == 1 + ) + self.assertIsInstance(d.set_substitute, Constraint) + assertExpressionsEqual( + self, d.set_substitute.expr, substitute_var == f(x), places=7 + ) + self.assertIsInstance(d.linear_combo, Constraint) + self.assertEqual(len(d.linear_combo), 1) + assertExpressionsEqual( + self, + d.linear_combo[0].expr, + x == pts[0] * d.lambdas[0] + pts[1] * d.lambdas[1], + ) + + # Check one disjunct from the paraboloid block for proper contents. This should + # be identical to the inner_representation_gdp one + def check_paraboloid_disjunct(self, d, pts, f, substitute_var, x1, x2): + self.assertEqual(len(d.component_map(Constraint)), 3) + # lambdas and indicator_var + self.assertEqual(len(d.component_map(Var)), 2) + self.assertIsInstance(d.lambdas, Var) + self.assertEqual(len(d.lambdas), 3) + for lamb in d.lambdas.values(): + self.assertEqual(lamb.lb, 0) + self.assertEqual(lamb.ub, 1) + self.assertIsInstance(d.convex_combo, Constraint) + assertExpressionsEqual( + self, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] + d.lambdas[2] == 1 + ) + self.assertIsInstance(d.set_substitute, Constraint) + assertExpressionsEqual( + self, d.set_substitute.expr, substitute_var == f(x1, x2), places=7 + ) + self.assertIsInstance(d.linear_combo, Constraint) + self.assertEqual(len(d.linear_combo), 2) + assertExpressionsEqual( + self, + d.linear_combo[0].expr, + x1 + == pts[0][0] * d.lambdas[0] + + pts[1][0] * d.lambdas[1] + + pts[2][0] * d.lambdas[2], + ) + assertExpressionsEqual( + self, + d.linear_combo[1].expr, + x2 + == pts[0][1] * d.lambdas[0] + + pts[1][1] * d.lambdas[1] + + pts[2][1] * d.lambdas[2], + ) + + + # Check the structure of the log PWLF Block + def check_pw_log(self, m): + z = m.pw_log.get_transformation_var(m.log_expr) + self.assertIsInstance(z, Var) + # Now we can use those Vars to check on what the transformation created + log_block = z.parent_block() + + # Not using ct.check_trans_block_structure() because these are slightly + # different + # Two top-level disjuncts + self.assertEqual(len(log_block.component_map(Disjunct)), 2) + # One disjunction + self.assertEqual(len(log_block.component_map(Disjunction)), 1) + # The 'z' var (that we will substitute in for the function being + # approximated) is here: + self.assertEqual(len(log_block.component_map(Var)), 1) + self.assertIsInstance(log_block.substitute_var, Var) + + # Check the tree structure, which should be heavier on the right + # Parent disjunction + self.assertIsInstance(log_block.disj, Disjunction) + self.assertEqual(len(log_block.disj.disjuncts), 2) + + # Left disjunct with constraints + self.assertIsInstance(log_block.d_l, Disjunct) + self.check_log_disjunct(log_block.d_l, (1, 3), m.f1, log_block.substitute_var, m.x) + + # Right disjunct with disjunction + self.assertIsInstance(log_block.d_r, Disjunct) + self.assertIsInstance(log_block.d_r.inner_disjunction_r, Disjunction) + self.assertEqual(len(log_block.d_r.inner_disjunction_r.disjuncts), 2) + + # Left and right child disjuncts with constraints + self.assertIsInstance(log_block.d_r.d_l, Disjunct) + self.check_log_disjunct(log_block.d_r.d_l, (3, 6), m.f2, log_block.substitute_var, m.x) + self.assertIsInstance(log_block.d_r.d_r, Disjunct) + self.check_log_disjunct(log_block.d_r.d_r, (6, 10), m.f3, log_block.substitute_var, m.x) + + # Check that this also became the objective + self.assertIs(m.obj.expr.expr, log_block.substitute_var) + + # Check the structure of the paraboloid PWLF block + def check_pw_paraboloid(self, m): + z = m.pw_paraboloid.get_transformation_var(m.paraboloid_expr) + self.assertIsInstance(z, Var) + paraboloid_block = z.parent_block() + + # Two top-level disjuncts + self.assertEqual(len(paraboloid_block.component_map(Disjunct)), 2) + # One disjunction + self.assertEqual(len(paraboloid_block.component_map(Disjunction)), 1) + # The 'z' var (that we will substitute in for the function being + # approximated) is here: + self.assertEqual(len(paraboloid_block.component_map(Var)), 1) + self.assertIsInstance(paraboloid_block.substitute_var, Var) + + # This one should have an even tree with four leaf disjuncts + disjuncts_dict = { + paraboloid_block.d_l.d_l: ([(0, 1), (0, 4), (3, 4)], m.g1), + paraboloid_block.d_l.d_r: ([(0, 1), (3, 4), (3, 1)], m.g1), + paraboloid_block.d_r.d_l: ([(3, 4), (3, 7), (0, 7)], m.g2), + paraboloid_block.d_r.d_r: ([(0, 7), (0, 4), (3, 4)], m.g2), + } + for d, (pts, f) in disjuncts_dict.items(): + self.check_paraboloid_disjunct( + d, pts, f, paraboloid_block.substitute_var, m.x1, m.x2 + ) + + # And check the substitute Var is in the objective now. + self.assertIs(m.indexed_c[0].body.args[0].expr, paraboloid_block.substitute_var) + + # Test methods using the common_tests.py code. Copied in from test_inner_repn_gdp.py. + def test_transformation_do_not_descend(self): + ct.check_transformation_do_not_descend(self, 'contrib.piecewise.nested_inner_repn_gdp') + + def test_transformation_PiecewiseLinearFunction_targets(self): + ct.check_transformation_PiecewiseLinearFunction_targets( + self, 'contrib.piecewise.nested_inner_repn_gdp' + ) + + def test_descend_into_expressions(self): + ct.check_descend_into_expressions(self, 'contrib.piecewise.nested_inner_repn_gdp') + + def test_descend_into_expressions_constraint_target(self): + ct.check_descend_into_expressions_constraint_target( + self, 'contrib.piecewise.nested_inner_repn_gdp' + ) + + def test_descend_into_expressions_objective_target(self): + ct.check_descend_into_expressions_objective_target( + self, 'contrib.piecewise.nested_inner_repn_gdp' + ) + + # Check the solution of the log(x) model + @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available') + @unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license') def test_solve_log_model(self): m = models.make_log_x_model() TransformationFactory("contrib.piecewise.nested_inner_repn_gdp").apply_to(m) From d1a92e0789ec6c9bf217ddbae41b428b782aeb6f Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 22 Dec 2023 13:57:42 -0500 Subject: [PATCH 0736/3044] apply black --- .../tests/test_nested_inner_repn_gdp.py | 27 ++++++++++++------- .../transform/disagreggated_logarithmic.py | 6 ++--- .../piecewise/transform/nested_inner_repn.py | 17 +++++++----- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py index 4deeb9abe78..8e8e4530d2c 100644 --- a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -17,10 +17,10 @@ from pyomo.gdp import Disjunction, Disjunct from pyomo.core.expr.compare import assertExpressionsEqual + # Test the nested inner repn gdp model using the common_tests code class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): - - # Check one disjunct for proper contents. Disjunct structure should be + # Check one disjunct for proper contents. Disjunct structure should be # identical to the version for the inner representation gdp def check_log_disjunct(self, d, pts, f, substitute_var, x): self.assertEqual(len(d.component_map(Constraint)), 3) @@ -85,7 +85,6 @@ def check_paraboloid_disjunct(self, d, pts, f, substitute_var, x1, x2): + pts[2][1] * d.lambdas[2], ) - # Check the structure of the log PWLF Block def check_pw_log(self, m): z = m.pw_log.get_transformation_var(m.log_expr) @@ -111,7 +110,9 @@ def check_pw_log(self, m): # Left disjunct with constraints self.assertIsInstance(log_block.d_l, Disjunct) - self.check_log_disjunct(log_block.d_l, (1, 3), m.f1, log_block.substitute_var, m.x) + self.check_log_disjunct( + log_block.d_l, (1, 3), m.f1, log_block.substitute_var, m.x + ) # Right disjunct with disjunction self.assertIsInstance(log_block.d_r, Disjunct) @@ -120,9 +121,13 @@ def check_pw_log(self, m): # Left and right child disjuncts with constraints self.assertIsInstance(log_block.d_r.d_l, Disjunct) - self.check_log_disjunct(log_block.d_r.d_l, (3, 6), m.f2, log_block.substitute_var, m.x) + self.check_log_disjunct( + log_block.d_r.d_l, (3, 6), m.f2, log_block.substitute_var, m.x + ) self.assertIsInstance(log_block.d_r.d_r, Disjunct) - self.check_log_disjunct(log_block.d_r.d_r, (6, 10), m.f3, log_block.substitute_var, m.x) + self.check_log_disjunct( + log_block.d_r.d_r, (6, 10), m.f3, log_block.substitute_var, m.x + ) # Check that this also became the objective self.assertIs(m.obj.expr.expr, log_block.substitute_var) @@ -156,10 +161,12 @@ def check_pw_paraboloid(self, m): # And check the substitute Var is in the objective now. self.assertIs(m.indexed_c[0].body.args[0].expr, paraboloid_block.substitute_var) - + # Test methods using the common_tests.py code. Copied in from test_inner_repn_gdp.py. def test_transformation_do_not_descend(self): - ct.check_transformation_do_not_descend(self, 'contrib.piecewise.nested_inner_repn_gdp') + ct.check_transformation_do_not_descend( + self, 'contrib.piecewise.nested_inner_repn_gdp' + ) def test_transformation_PiecewiseLinearFunction_targets(self): ct.check_transformation_PiecewiseLinearFunction_targets( @@ -167,7 +174,9 @@ def test_transformation_PiecewiseLinearFunction_targets(self): ) def test_descend_into_expressions(self): - ct.check_descend_into_expressions(self, 'contrib.piecewise.nested_inner_repn_gdp') + ct.check_descend_into_expressions( + self, 'contrib.piecewise.nested_inner_repn_gdp' + ) def test_descend_into_expressions_constraint_target(self): ct.check_descend_into_expressions_constraint_target( diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index e22bba0215f..b54be3d0b75 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -60,14 +60,12 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc simplices = pw_linear_func._simplices num_simplices = len(simplices) transBlock.simplex_indices = RangeSet(0, num_simplices - 1) - # Assumption: the simplices are really full-dimensional simplices and all have the + # Assumption: the simplices are really full-dimensional simplices and all have the # same number of points, which is dimension + 1 transBlock.simplex_point_indices = RangeSet(0, dimension) # Enumeration of simplices: map from simplex number to simplex object - idx_to_simplex = { - k: v for k, v in zip(transBlock.simplex_indices, simplices) - } + idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} # List of tuples of simplex indices with their linear function simplex_indices_and_lin_funcs = list( diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 143bd827c58..97bfd9316f4 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -25,8 +25,8 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearTransformationBa of extreme points, with multipliers "local" to that particular polytope, i.e., not shared with neighbors. This method of formulating the piecewise linear function imposes no restrictions on the family of polytopes. Note - that this is NOT a logarithmic formulation - it has linearly many Boolean - variables. However, it is inspired by the disaggregated logarithmic + that this is NOT a logarithmic formulation - it has linearly many Boolean + variables. However, it is inspired by the disaggregated logarithmic formulation of [1]. Up to variable substitution, the amount of Boolean variables is logarithmic, as in [1]. @@ -66,9 +66,10 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc transBlock.set_substitute = Constraint( expr=substitute_var == linear_func_expr ) - (transBlock.substitute_var_lb, transBlock.substitute_var_ub) = compute_bounds_on_expr( - linear_func_expr - ) + ( + transBlock.substitute_var_lb, + transBlock.substitute_var_ub, + ) = compute_bounds_on_expr(linear_func_expr) else: # Add the disjunction transBlock.disj = self._get_disjunction( @@ -86,7 +87,9 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up # the stack, since the whole point is that we'll only go logarithmically # many calls deep. - def _get_disjunction(self, choices, parent_block, pw_expr, pw_linear_func, root_block): + def _get_disjunction( + self, choices, parent_block, pw_expr, pw_linear_func, root_block + ): size = len(choices) # Our base cases will be 3 and 2, since it would be silly to construct @@ -184,7 +187,7 @@ def _set_disjunct_block_constraints( root_block.substitute_var_ub = ub # Constrain x = \sum \lambda_i v_i - @b.Constraint(range(pw_expr.nargs())) # dimension + @b.Constraint(range(pw_expr.nargs())) # dimension def linear_combo(d, i): return pw_expr.args[i] == sum( d.lambdas[j] * pt[i] for j, pt in enumerate(extreme_pts) From 14aa6fcb6a065db500bcd47360cecb7e0659649a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 22 Dec 2023 15:02:10 -0500 Subject: [PATCH 0737/3044] fix initialization bug --- .../piecewise/transform/disagreggated_logarithmic.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index b54be3d0b75..b04f66584d2 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -1,7 +1,7 @@ from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) -from pyomo.core import Constraint, Binary, Var, RangeSet +from pyomo.core import Constraint, Binary, Var, RangeSet, Set from pyomo.core.base import TransformationFactory from pyomo.common.errors import DeveloperError from math import ceil, log2 @@ -99,14 +99,14 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Build up P_0 and P_plus ahead of time. # {P \in \mathcal{P} | B(P)_l = 0} - @transBlock.Set(transBlock.simplex_indices) - def P_0(l): + def P_0_init(m, l): return [p for p in transBlock.simplex_indices if B[p][l] == 0] + transBlock.P_0 = Set(transBlock.log_simplex_indices, initialize=P_0_init) # {P \in \mathcal{P} | B(P)_l = 1} - @transBlock.Set(transBlock.simplex_indices) - def P_0(l): + def P_plus_init(m, l): return [p for p in transBlock.simplex_indices if B[p][l] == 1] + transBlock.P_plus = Set(transBlock.log_simplex_indices, initialize=P_plus_init) # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it transBlock.lambdas = Var( From 98ed11940aff5f7b0b82474712544d15d2f8a72f Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 15 Feb 2024 15:00:42 -0500 Subject: [PATCH 0738/3044] Fix up tests for disaggreggated logarithmic --- .../tests/test_disaggregated_logarithmic.py | 267 +++++++++++++++++- .../transform/disagreggated_logarithmic.py | 4 + 2 files changed, 269 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py index 8fd4bebfc37..cb77ec844fe 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -13,13 +13,276 @@ from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct from pyomo.core.base import TransformationFactory -from pyomo.environ import SolverFactory +from pyomo.environ import SolverFactory, Var, Constraint +from pyomo.core.expr.compare import assertExpressionsEqual class TestTransformPiecewiseModelToNestedInnerRepnMIP(unittest.TestCase): + def check_pw_log(self, m): + z = m.pw_log.get_transformation_var(m.log_expr) + self.assertIsInstance(z, Var) + # Now we can use those Vars to check on what the transformation created + log_block = z.parent_block() + + # We should have three Vars, two of which are indexed, and five + # Constraints, three of which are indexed + + self.assertEqual(len(log_block.component_map(Var)), 3) + self.assertEqual(len(log_block.component_map(Constraint)), 5) + + # Constants + simplex_count = 3 + log_simplex_count = 2 + simplex_point_count = 2 + + # Substitute var + self.assertIsInstance(log_block.substitute_var, Var) + self.assertIs(m.obj.expr.expr, log_block.substitute_var) + # Binaries + self.assertIsInstance(log_block.binaries, Var) + self.assertEqual(len(log_block.binaries), log_simplex_count) + # Lambdas + self.assertIsInstance(log_block.lambdas, Var) + self.assertEqual(len(log_block.lambdas), simplex_count * simplex_point_count) + for l in log_block.lambdas.values(): + self.assertEqual(l.lb, 0) + self.assertEqual(l.ub, 1) + + # Convex combo constraint + self.assertIsInstance(log_block.convex_combo, Constraint) + assertExpressionsEqual( + self, + log_block.convex_combo.expr, + log_block.lambdas[0, 0] + + log_block.lambdas[0, 1] + + log_block.lambdas[1, 0] + + log_block.lambdas[1, 1] + + log_block.lambdas[2, 0] + + log_block.lambdas[2, 1] + == 1, + ) + + # Set substitute constraint + self.assertIsInstance(log_block.set_substitute, Constraint) + assertExpressionsEqual( + self, + log_block.set_substitute.expr, + log_block.substitute_var + == log_block.lambdas[0, 0] * m.f1(1) + + log_block.lambdas[0, 1] * m.f1(3) + + log_block.lambdas[1, 0] * m.f2(3) + + log_block.lambdas[1, 1] * m.f2(6) + + log_block.lambdas[2, 0] * m.f3(6) + + log_block.lambdas[2, 1] * m.f3(10), + places=7, + ) + + # x constraint + self.assertIsInstance(log_block.x_constraint, Constraint) + # one-dimensional case, so there is only one x variable here + self.assertEqual(len(log_block.x_constraint), 1) + assertExpressionsEqual( + self, + log_block.x_constraint[0].expr, + m.x + == 1 * log_block.lambdas[0, 0] + + 3 * log_block.lambdas[0, 1] + + 3 * log_block.lambdas[1, 0] + + 6 * log_block.lambdas[1, 1] + + 6 * log_block.lambdas[2, 0] + + 10 * log_block.lambdas[2, 1], + ) + + # simplex choice 1 constraint enables lambdas when binaries are on + self.assertEqual(len(log_block.simplex_choice_1), log_simplex_count) + assertExpressionsEqual( + self, + log_block.simplex_choice_1[0].expr, + log_block.lambdas[2, 0] + log_block.lambdas[2, 1] <= log_block.binaries[0], + ) + assertExpressionsEqual( + self, + log_block.simplex_choice_1[1].expr, + log_block.lambdas[1, 0] + log_block.lambdas[1, 1] <= log_block.binaries[1], + ) + # simplex choice 2 constraint enables lambdas when binaries are off + self.assertEqual(len(log_block.simplex_choice_2), log_simplex_count) + assertExpressionsEqual( + self, + log_block.simplex_choice_2[0].expr, + log_block.lambdas[0, 0] + + log_block.lambdas[0, 1] + + log_block.lambdas[1, 0] + + log_block.lambdas[1, 1] + <= 1 - log_block.binaries[0], + ) + assertExpressionsEqual( + self, + log_block.simplex_choice_2[1].expr, + log_block.lambdas[0, 0] + + log_block.lambdas[0, 1] + + log_block.lambdas[2, 0] + + log_block.lambdas[2, 1] + <= 1 - log_block.binaries[1], + ) + + def check_pw_paraboloid(self, m): + # This is a little larger, but at least test that the right numbers of + # everything are created + z = m.pw_paraboloid.get_transformation_var(m.paraboloid_expr) + self.assertIsInstance(z, Var) + paraboloid_block = z.parent_block() + + self.assertEqual(len(paraboloid_block.component_map(Var)), 3) + self.assertEqual(len(paraboloid_block.component_map(Constraint)), 5) + + # Constants + simplex_count = 4 + log_simplex_count = 2 + simplex_point_count = 3 + + # Substitute var + self.assertIsInstance(paraboloid_block.substitute_var, Var) + # assertExpressionsEqual( + # self, + # m.indexed_c[0].expr, + # m.x >= paraboloid_block.substitute_var + # ) + # Binaries + self.assertIsInstance(paraboloid_block.binaries, Var) + self.assertEqual(len(paraboloid_block.binaries), log_simplex_count) + # Lambdas + self.assertIsInstance(paraboloid_block.lambdas, Var) + # print(f"the lambdas are: {paraboloid_block.lambdas.pprint()}") + self.assertEqual( + len(paraboloid_block.lambdas), simplex_count * simplex_point_count + ) + for l in paraboloid_block.lambdas.values(): + self.assertEqual(l.lb, 0) + self.assertEqual(l.ub, 1) + + # Convex combo constraint + self.assertIsInstance(paraboloid_block.convex_combo, Constraint) + assertExpressionsEqual( + self, + paraboloid_block.convex_combo.expr, + paraboloid_block.lambdas[0, 0] + + paraboloid_block.lambdas[0, 1] + + paraboloid_block.lambdas[0, 2] + + paraboloid_block.lambdas[1, 0] + + paraboloid_block.lambdas[1, 1] + + paraboloid_block.lambdas[1, 2] + + paraboloid_block.lambdas[2, 0] + + paraboloid_block.lambdas[2, 1] + + paraboloid_block.lambdas[2, 2] + + paraboloid_block.lambdas[3, 0] + + paraboloid_block.lambdas[3, 1] + + paraboloid_block.lambdas[3, 2] + == 1, + ) + + # Set substitute constraint + self.assertIsInstance(paraboloid_block.set_substitute, Constraint) + assertExpressionsEqual( + self, + paraboloid_block.set_substitute.expr, + paraboloid_block.substitute_var + == paraboloid_block.lambdas[0, 0] * m.g1(0, 1) + + paraboloid_block.lambdas[0, 1] * m.g1(0, 4) + + paraboloid_block.lambdas[0, 2] * m.g1(3, 4) + + paraboloid_block.lambdas[1, 0] * m.g1(0, 1) + + paraboloid_block.lambdas[1, 1] * m.g1(3, 4) + + paraboloid_block.lambdas[1, 2] * m.g1(3, 1) + + paraboloid_block.lambdas[2, 0] * m.g2(3, 4) + + paraboloid_block.lambdas[2, 1] * m.g2(3, 7) + + paraboloid_block.lambdas[2, 2] * m.g2(0, 7) + + paraboloid_block.lambdas[3, 0] * m.g2(0, 7) + + paraboloid_block.lambdas[3, 1] * m.g2(0, 4) + + paraboloid_block.lambdas[3, 2] * m.g2(3, 4), + places=7, + ) + + # x constraint + self.assertIsInstance(paraboloid_block.x_constraint, Constraint) + # Here we have two x variables + self.assertEqual(len(paraboloid_block.x_constraint), 2) + assertExpressionsEqual( + self, + paraboloid_block.x_constraint[0].expr, + m.x1 + == 0 * paraboloid_block.lambdas[0, 0] + + 0 * paraboloid_block.lambdas[0, 1] + + 3 * paraboloid_block.lambdas[0, 2] + + 0 * paraboloid_block.lambdas[1, 0] + + 3 * paraboloid_block.lambdas[1, 1] + + 3 * paraboloid_block.lambdas[1, 2] + + 3 * paraboloid_block.lambdas[2, 0] + + 3 * paraboloid_block.lambdas[2, 1] + + 0 * paraboloid_block.lambdas[2, 2] + + 0 * paraboloid_block.lambdas[3, 0] + + 0 * paraboloid_block.lambdas[3, 1] + + 3 * paraboloid_block.lambdas[3, 2], + ) + assertExpressionsEqual( + self, + paraboloid_block.x_constraint[1].expr, + m.x2 + == 1 * paraboloid_block.lambdas[0, 0] + + 4 * paraboloid_block.lambdas[0, 1] + + 4 * paraboloid_block.lambdas[0, 2] + + 1 * paraboloid_block.lambdas[1, 0] + + 4 * paraboloid_block.lambdas[1, 1] + + 1 * paraboloid_block.lambdas[1, 2] + + 4 * paraboloid_block.lambdas[2, 0] + + 7 * paraboloid_block.lambdas[2, 1] + + 7 * paraboloid_block.lambdas[2, 2] + + 7 * paraboloid_block.lambdas[3, 0] + + 4 * paraboloid_block.lambdas[3, 1] + + 4 * paraboloid_block.lambdas[3, 2], + ) + + # The choices will get long, so let's just assert we have enough + self.assertEqual(len(paraboloid_block.simplex_choice_1), log_simplex_count) + self.assertEqual(len(paraboloid_block.simplex_choice_2), log_simplex_count) + + # Test methods using the common_tests.py code. + def test_transformation_do_not_descend(self): + ct.check_transformation_do_not_descend( + self, 'contrib.piecewise.disaggregated_logarithmic' + ) + + def test_transformation_PiecewiseLinearFunction_targets(self): + ct.check_transformation_PiecewiseLinearFunction_targets( + self, 'contrib.piecewise.disaggregated_logarithmic' + ) + + def test_descend_into_expressions(self): + ct.check_descend_into_expressions( + self, 'contrib.piecewise.disaggregated_logarithmic' + ) + + def test_descend_into_expressions_constraint_target(self): + ct.check_descend_into_expressions_constraint_target( + self, 'contrib.piecewise.disaggregated_logarithmic' + ) + + def test_descend_into_expressions_objective_target(self): + ct.check_descend_into_expressions_objective_target( + self, 'contrib.piecewise.disaggregated_logarithmic' + ) + + # Check solution of the log(x) model + @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available') + @unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license') def test_solve_log_model(self): m = models.make_log_x_model() TransformationFactory("contrib.piecewise.disaggregated_logarithmic").apply_to(m) - TransformationFactory("gdp.bigm").apply_to(m) SolverFactory("gurobi").solve(m) ct.check_log_x_model_soln(self, m) + + @unittest.skipIf(True, reason="because") + def test_test(self): + m = models.make_log_x_model() + TransformationFactory("contrib.piecewise.disaggregated_logarithmic").apply_to(m) + m.pprint() + assert False diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index b04f66584d2..009282aa310 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -101,11 +101,13 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # {P \in \mathcal{P} | B(P)_l = 0} def P_0_init(m, l): return [p for p in transBlock.simplex_indices if B[p][l] == 0] + transBlock.P_0 = Set(transBlock.log_simplex_indices, initialize=P_0_init) # {P \in \mathcal{P} | B(P)_l = 1} def P_plus_init(m, l): return [p for p in transBlock.simplex_indices if B[p][l] == 1] + transBlock.P_plus = Set(transBlock.log_simplex_indices, initialize=P_plus_init) # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it @@ -128,6 +130,7 @@ def P_plus_init(m, l): # The branching rules, establishing using the binaries that only one simplex's lambda # coefficients may be nonzero + # Enabling lambdas when binaries are on @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) def simplex_choice_1(b, l): return ( @@ -139,6 +142,7 @@ def simplex_choice_1(b, l): <= transBlock.binaries[l] ) + # Disabling lambdas when binaries are on @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) def simplex_choice_2(b, l): return ( From 1312af5dd0fb1c97b69dd9345999ddc134f2391f Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 15 Feb 2024 15:30:51 -0500 Subject: [PATCH 0739/3044] fix name typo --- pyomo/contrib/piecewise/__init__.py | 2 +- .../piecewise/tests/test_disaggregated_logarithmic.py | 7 ------- ...ggated_logarithmic.py => disaggreggated_logarithmic.py} | 0 3 files changed, 1 insertion(+), 8 deletions(-) rename pyomo/contrib/piecewise/transform/{disagreggated_logarithmic.py => disaggreggated_logarithmic.py} (100%) diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index de18e559a93..8a66af89bad 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -36,6 +36,6 @@ from pyomo.contrib.piecewise.transform.nested_inner_repn import ( NestedInnerRepresentationGDPTransformation, ) -from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import ( +from pyomo.contrib.piecewise.transform.disaggreggated_logarithmic import ( DisaggregatedLogarithmicInnerMIPTransformation, ) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py index cb77ec844fe..49a30d0996c 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -279,10 +279,3 @@ def test_solve_log_model(self): TransformationFactory("contrib.piecewise.disaggregated_logarithmic").apply_to(m) SolverFactory("gurobi").solve(m) ct.check_log_x_model_soln(self, m) - - @unittest.skipIf(True, reason="because") - def test_test(self): - m = models.make_log_x_model() - TransformationFactory("contrib.piecewise.disaggregated_logarithmic").apply_to(m) - m.pprint() - assert False diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py similarity index 100% rename from pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py rename to pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py From b9a3e4426a101ce6483b549fe26e9f8dc32d00ac Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 15 Feb 2024 15:52:12 -0500 Subject: [PATCH 0740/3044] satisfy black? --- pyomo/contrib/piecewise/transform/nested_inner_repn.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 97bfd9316f4..b273c9776a2 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -66,10 +66,9 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc transBlock.set_substitute = Constraint( expr=substitute_var == linear_func_expr ) - ( - transBlock.substitute_var_lb, - transBlock.substitute_var_ub, - ) = compute_bounds_on_expr(linear_func_expr) + (transBlock.substitute_var_lb, transBlock.substitute_var_ub) = ( + compute_bounds_on_expr(linear_func_expr) + ) else: # Add the disjunction transBlock.disj = self._get_disjunction( From 133d0ff3e7871d9cae5209c7577c947ba94b773e Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 20 Feb 2024 18:30:20 -0500 Subject: [PATCH 0741/3044] update and add copyright notices --- .../piecewise/tests/test_disaggregated_logarithmic.py | 2 +- .../piecewise/tests/test_nested_inner_repn_gdp.py | 2 +- .../piecewise/transform/disaggreggated_logarithmic.py | 11 +++++++++++ .../contrib/piecewise/transform/nested_inner_repn.py | 11 +++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py index 49a30d0996c..ab71679aac9 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py index 8e8e4530d2c..e888db7eb72 100644 --- a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py index 009282aa310..368b92d6424 100644 --- a/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index b273c9776a2..dbbd8c73bad 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, From 423dbf8b4f8ef70e9bbec5af1c2cde7fd6f207aa Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 21 Feb 2024 20:12:14 -0500 Subject: [PATCH 0742/3044] rename and cleanup, try 2 --- pyomo/contrib/piecewise/__init__.py | 4 +- .../tests/test_disaggregated_logarithmic.py | 16 +- .../transform/disaggregated_logarithmic.py | 199 ++++++++++++++++++ 3 files changed, 209 insertions(+), 10 deletions(-) create mode 100644 pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 8a66af89bad..b23200b3f7d 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -36,6 +36,6 @@ from pyomo.contrib.piecewise.transform.nested_inner_repn import ( NestedInnerRepresentationGDPTransformation, ) -from pyomo.contrib.piecewise.transform.disaggreggated_logarithmic import ( - DisaggregatedLogarithmicInnerMIPTransformation, +from pyomo.contrib.piecewise.transform.disaggregated_logarithmic import ( + DisaggregatedLogarithmicMIPTransformation, ) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py index ab71679aac9..2e0fa886361 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -69,10 +69,10 @@ def check_pw_log(self, m): log_block.set_substitute.expr, log_block.substitute_var == log_block.lambdas[0, 0] * m.f1(1) - + log_block.lambdas[0, 1] * m.f1(3) + log_block.lambdas[1, 0] * m.f2(3) - + log_block.lambdas[1, 1] * m.f2(6) + log_block.lambdas[2, 0] * m.f3(6) + + log_block.lambdas[0, 1] * m.f1(3) + + log_block.lambdas[1, 1] * m.f2(6) + log_block.lambdas[2, 1] * m.f3(10), places=7, ) @@ -188,16 +188,16 @@ def check_pw_paraboloid(self, m): paraboloid_block.set_substitute.expr, paraboloid_block.substitute_var == paraboloid_block.lambdas[0, 0] * m.g1(0, 1) - + paraboloid_block.lambdas[0, 1] * m.g1(0, 4) - + paraboloid_block.lambdas[0, 2] * m.g1(3, 4) + paraboloid_block.lambdas[1, 0] * m.g1(0, 1) - + paraboloid_block.lambdas[1, 1] * m.g1(3, 4) - + paraboloid_block.lambdas[1, 2] * m.g1(3, 1) + paraboloid_block.lambdas[2, 0] * m.g2(3, 4) - + paraboloid_block.lambdas[2, 1] * m.g2(3, 7) - + paraboloid_block.lambdas[2, 2] * m.g2(0, 7) + paraboloid_block.lambdas[3, 0] * m.g2(0, 7) + + paraboloid_block.lambdas[0, 1] * m.g1(0, 4) + + paraboloid_block.lambdas[1, 1] * m.g1(3, 4) + + paraboloid_block.lambdas[2, 1] * m.g2(3, 7) + paraboloid_block.lambdas[3, 1] * m.g2(0, 4) + + paraboloid_block.lambdas[0, 2] * m.g1(3, 4) + + paraboloid_block.lambdas[1, 2] * m.g1(3, 1) + + paraboloid_block.lambdas[2, 2] * m.g2(0, 7) + paraboloid_block.lambdas[3, 2] * m.g2(3, 4), places=7, ) diff --git a/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py new file mode 100644 index 00000000000..edb1a03afe6 --- /dev/null +++ b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py @@ -0,0 +1,199 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( + PiecewiseLinearTransformationBase, +) +from pyomo.core import Constraint, Binary, Var, RangeSet, Set +from pyomo.core.base import TransformationFactory +from pyomo.common.errors import DeveloperError +from math import ceil, log2 + + +@TransformationFactory.register( + "contrib.piecewise.disaggregated_logarithmic", + doc=""" + Represent a piecewise linear function "logarithmically" by using a MIP with + log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; + GDP is not used. + """, +) +class DisaggregatedLogarithmicMIPTransformation(PiecewiseLinearTransformationBase): + """ + Represent a piecewise linear function "logarithmically" by using a MIP with + log_2(|P|) binary decision variables, following the "disaggregated logarithmic" + method from [1]. This is a direct-to-MIP transformation; GDP is not used. + This method of logarithmically formulating the piecewise linear function + imposes no restrictions on the family of polytopes, but we assume we have + simplices in this code. + + References + ---------- + [1] J.P. Vielma, S. Ahmed, and G. Nemhauser, "Mixed-integer models + for nonseparable piecewise-linear optimization: unifying framework + and extensions," Operations Research, vol. 58, no. 2, pp. 305-315, + 2010. + """ + + CONFIG = PiecewiseLinearTransformationBase.CONFIG() + _transformation_name = "pw_linear_disaggregated_log" + + # Implement to use PiecewiseLinearTransformationBase. This function returns the Var + # that replaces the transformed piecewise linear expr + def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): + # Get a new Block for our transformation in transformation_block.transformed_functions, + # which is a Block(Any). This is where we will put our new components. + transBlock = transformation_block.transformed_functions[ + len(transformation_block.transformed_functions) + ] + + # Dimensionality of the PWLF + dimension = pw_expr.nargs() + transBlock.dimension_indices = RangeSet(0, dimension - 1) + + # Substitute Var that will hold the value of the PWLE + substitute_var = transBlock.substitute_var = Var() + pw_linear_func.map_transformation_var(pw_expr, substitute_var) + + # Bounds for the substitute_var that we will widen + substitute_var_lb = float("inf") + substitute_var_ub = -float("inf") + + # Simplices are tuples of indices of points. Give them their own indices, too + simplices = pw_linear_func._simplices + num_simplices = len(simplices) + transBlock.simplex_indices = RangeSet(0, num_simplices - 1) + # Assumption: the simplices are really full-dimensional simplices and all have the + # same number of points, which is dimension + 1 + transBlock.simplex_point_indices = RangeSet(0, dimension) + + # Enumeration of simplices: map from simplex number to simplex object + idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} + + # List of tuples of simplex indices with their linear function + simplex_indices_and_lin_funcs = list( + zip(transBlock.simplex_indices, pw_linear_func._linear_functions) + ) + + # We don't seem to get a convenient opportunity later, so let's just widen + # the bounds here. All we need to do is go through the corners of each simplex. + for P, linear_func in simplex_indices_and_lin_funcs: + for v in transBlock.simplex_point_indices: + val = linear_func(*pw_linear_func._points[idx_to_simplex[P][v]]) + if val < substitute_var_lb: + substitute_var_lb = val + if val > substitute_var_ub: + substitute_var_ub = val + transBlock.substitute_var.setlb(substitute_var_lb) + transBlock.substitute_var.setub(substitute_var_ub) + + log_dimension = ceil(log2(num_simplices)) + transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) + transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) + + # Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices + # (really just polytopes are required) with binary vectors. Any injective function + # is enough here. + B = {} + for i in transBlock.simplex_indices: + # map index(P) -> corresponding vector in {0, 1}^n + B[i] = self._get_binary_vector(i, log_dimension) + + # Build up P_0 and P_plus ahead of time. + + # {P \in \mathcal{P} | B(P)_l = 0} + def P_0_init(m, l): + return [p for p in transBlock.simplex_indices if B[p][l] == 0] + + transBlock.P_0 = Set(transBlock.log_simplex_indices, initialize=P_0_init) + + # {P \in \mathcal{P} | B(P)_l = 1} + def P_plus_init(m, l): + return [p for p in transBlock.simplex_indices if B[p][l] == 1] + + transBlock.P_plus = Set(transBlock.log_simplex_indices, initialize=P_plus_init) + + # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it + transBlock.lambdas = Var( + transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1) + ) + + # Numbered citations are from Vielma et al 2010, Mixed-Integer Models + # for Nonseparable Piecewise-Linear Optimization + + # Sum of all lambdas is one (6b) + transBlock.convex_combo = Constraint( + expr=sum( + transBlock.lambdas[P, v] + for P in transBlock.simplex_indices + for v in transBlock.simplex_point_indices + ) + == 1 + ) + + # The branching rules, establishing using the binaries that only one simplex's lambda + # coefficients may be nonzero + # Enabling lambdas when binaries are on + @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) + def simplex_choice_1(b, l): + return ( + sum( + transBlock.lambdas[P, v] + for P in transBlock.P_plus[l] + for v in transBlock.simplex_point_indices + ) + <= transBlock.binaries[l] + ) + + # Disabling lambdas when binaries are on + @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) + def simplex_choice_2(b, l): + return ( + sum( + transBlock.lambdas[P, v] + for P in transBlock.P_0[l] + for v in transBlock.simplex_point_indices + ) + <= 1 - transBlock.binaries[l] + ) + + # for i, (simplex, pwlf) in enumerate(choices): + # x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P)) + @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) + def x_constraint(b, i): + return pw_expr.args[i] == sum( + transBlock.lambdas[P, v] + * pw_linear_func._points[idx_to_simplex[P][v]][i] + for P in transBlock.simplex_indices + for v in transBlock.simplex_point_indices + ) + + # Make the substitute Var equal the PWLE (6a.2) + transBlock.set_substitute = Constraint( + expr=substitute_var + == sum( + transBlock.lambdas[P, v] + * linear_func(*pw_linear_func._points[idx_to_simplex[P][v]]) + for v in transBlock.simplex_point_indices + for (P, linear_func) in simplex_indices_and_lin_funcs + ) + ) + + return substitute_var + + # Not a Gray code, just a regular binary representation + # TODO test the Gray codes too + def _get_binary_vector(self, num, length): + if num != 0 and ceil(log2(num)) > length: + raise DeveloperError("Invalid input in _get_binary_vector") + # Use python's string formatting instead of bothering with modular + # arithmetic. Hopefully not slow. + return tuple(int(x) for x in format(num, f"0{length}b")) From 290be256363f2b1d98f382cfa79a8f542666f446 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 21 Feb 2024 20:18:43 -0500 Subject: [PATCH 0743/3044] redo cleanup I managed to lose --- .../piecewise/tests/test_disaggregated_logarithmic.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py index 2e0fa886361..f848c610e9d 100644 --- a/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/tests/test_disaggregated_logarithmic.py @@ -143,17 +143,11 @@ def check_pw_paraboloid(self, m): # Substitute var self.assertIsInstance(paraboloid_block.substitute_var, Var) - # assertExpressionsEqual( - # self, - # m.indexed_c[0].expr, - # m.x >= paraboloid_block.substitute_var - # ) # Binaries self.assertIsInstance(paraboloid_block.binaries, Var) self.assertEqual(len(paraboloid_block.binaries), log_simplex_count) # Lambdas self.assertIsInstance(paraboloid_block.lambdas, Var) - # print(f"the lambdas are: {paraboloid_block.lambdas.pprint()}") self.assertEqual( len(paraboloid_block.lambdas), simplex_count * simplex_point_count ) From dd6ed3e8a94939f68644f582c1c7e1d3bb53fd18 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 21 Feb 2024 20:21:06 -0500 Subject: [PATCH 0744/3044] fix again something strange I did --- .../transform/disaggreggated_logarithmic.py | 201 ------------------ 1 file changed, 201 deletions(-) delete mode 100644 pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py diff --git a/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py deleted file mode 100644 index 368b92d6424..00000000000 --- a/pyomo/contrib/piecewise/transform/disaggreggated_logarithmic.py +++ /dev/null @@ -1,201 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( - PiecewiseLinearTransformationBase, -) -from pyomo.core import Constraint, Binary, Var, RangeSet, Set -from pyomo.core.base import TransformationFactory -from pyomo.common.errors import DeveloperError -from math import ceil, log2 - - -@TransformationFactory.register( - "contrib.piecewise.disaggregated_logarithmic", - doc=""" - Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; - GDP is not used. - """, -) -class DisaggregatedLogarithmicInnerMIPTransformation(PiecewiseLinearTransformationBase): - """ - Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables, following the "disaggregated logarithmic" - method from [1]. This is a direct-to-MIP transformation; GDP is not used. - This method of logarithmically formulating the piecewise linear function - imposes no restrictions on the family of polytopes, but we assume we have - simplices in this code. - - References - ---------- - [1] J.P. Vielma, S. Ahmed, and G. Nemhauser, "Mixed-integer models - for nonseparable piecewise-linear optimization: unifying framework - and extensions," Operations Research, vol. 58, no. 2, pp. 305-315, - 2010. - """ - - CONFIG = PiecewiseLinearTransformationBase.CONFIG() - _transformation_name = "pw_linear_disaggregated_log" - - # Implement to use PiecewiseLinearTransformationBase. This function returns the Var - # that replaces the transformed piecewise linear expr - def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - # Get a new Block for our transformation in transformation_block.transformed_functions, - # which is a Block(Any). This is where we will put our new components. - transBlock = transformation_block.transformed_functions[ - len(transformation_block.transformed_functions) - ] - - # Dimensionality of the PWLF - dimension = pw_expr.nargs() - transBlock.dimension_indices = RangeSet(0, dimension - 1) - - # Substitute Var that will hold the value of the PWLE - substitute_var = transBlock.substitute_var = Var() - pw_linear_func.map_transformation_var(pw_expr, substitute_var) - - # Bounds for the substitute_var that we will widen - substitute_var_lb = float("inf") - substitute_var_ub = -float("inf") - - # Simplices are tuples of indices of points. Give them their own indices, too - simplices = pw_linear_func._simplices - num_simplices = len(simplices) - transBlock.simplex_indices = RangeSet(0, num_simplices - 1) - # Assumption: the simplices are really full-dimensional simplices and all have the - # same number of points, which is dimension + 1 - transBlock.simplex_point_indices = RangeSet(0, dimension) - - # Enumeration of simplices: map from simplex number to simplex object - idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} - - # List of tuples of simplex indices with their linear function - simplex_indices_and_lin_funcs = list( - zip(transBlock.simplex_indices, pw_linear_func._linear_functions) - ) - - # We don't seem to get a convenient opportunity later, so let's just widen - # the bounds here. All we need to do is go through the corners of each simplex. - for P, linear_func in simplex_indices_and_lin_funcs: - for v in transBlock.simplex_point_indices: - val = linear_func(*pw_linear_func._points[idx_to_simplex[P][v]]) - if val < substitute_var_lb: - substitute_var_lb = val - if val > substitute_var_ub: - substitute_var_ub = val - transBlock.substitute_var.setlb(substitute_var_lb) - transBlock.substitute_var.setub(substitute_var_ub) - - log_dimension = ceil(log2(num_simplices)) - transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) - transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) - - # Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices - # (really just polytopes are required) with binary vectors. Any injective function - # is enough here. - B = {} - for i in transBlock.simplex_indices: - # map index(P) -> corresponding vector in {0, 1}^n - B[i] = self._get_binary_vector(i, log_dimension) - - # Build up P_0 and P_plus ahead of time. - - # {P \in \mathcal{P} | B(P)_l = 0} - def P_0_init(m, l): - return [p for p in transBlock.simplex_indices if B[p][l] == 0] - - transBlock.P_0 = Set(transBlock.log_simplex_indices, initialize=P_0_init) - - # {P \in \mathcal{P} | B(P)_l = 1} - def P_plus_init(m, l): - return [p for p in transBlock.simplex_indices if B[p][l] == 1] - - transBlock.P_plus = Set(transBlock.log_simplex_indices, initialize=P_plus_init) - - # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it - transBlock.lambdas = Var( - transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1) - ) - - # Numbered citations are from Vielma et al 2010, Mixed-Integer Models - # for Nonseparable Piecewise-Linear Optimization - - # Sum of all lambdas is one (6b) - transBlock.convex_combo = Constraint( - expr=sum( - transBlock.lambdas[P, v] - for P in transBlock.simplex_indices - for v in transBlock.simplex_point_indices - ) - == 1 - ) - - # The branching rules, establishing using the binaries that only one simplex's lambda - # coefficients may be nonzero - # Enabling lambdas when binaries are on - @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) - def simplex_choice_1(b, l): - return ( - sum( - transBlock.lambdas[P, v] - for P in transBlock.P_plus[l] - for v in transBlock.simplex_point_indices - ) - <= transBlock.binaries[l] - ) - - # Disabling lambdas when binaries are on - @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) - def simplex_choice_2(b, l): - return ( - sum( - transBlock.lambdas[P, v] - for P in transBlock.P_0[l] - for v in transBlock.simplex_point_indices - ) - <= 1 - transBlock.binaries[l] - ) - - # for i, (simplex, pwlf) in enumerate(choices): - # x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P)) - @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) - def x_constraint(b, i): - return pw_expr.args[i] == sum( - transBlock.lambdas[P, v] - * pw_linear_func._points[idx_to_simplex[P][v]][i] - for P in transBlock.simplex_indices - for v in transBlock.simplex_point_indices - ) - - # Make the substitute Var equal the PWLE (6a.2) - transBlock.set_substitute = Constraint( - expr=substitute_var - == sum( - sum( - transBlock.lambdas[P, v] - * linear_func(*pw_linear_func._points[idx_to_simplex[P][v]]) - for v in transBlock.simplex_point_indices - ) - for (P, linear_func) in simplex_indices_and_lin_funcs - ) - ) - - return substitute_var - - # Not a Gray code, just a regular binary representation - # TODO test the Gray codes too - def _get_binary_vector(self, num, length): - if num != 0 and ceil(log2(num)) > length: - raise DeveloperError("Invalid input in _get_binary_vector") - # Use python's string formatting instead of bothering with modular - # arithmetic. Hopefully not slow. - return tuple(int(x) for x in format(num, f"0{length}b")) From 289915dcc11371a3f423b27adfb055824029b52b Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 22 Feb 2024 14:40:11 -0500 Subject: [PATCH 0745/3044] move duplicated methods into common file for inner-repn-GDP based transforms --- .../tests/common_inner_repn_tests.py | 80 ++++++++++++++++++ .../piecewise/tests/test_inner_repn_gdp.py | 70 ++-------------- .../tests/test_nested_inner_repn_gdp.py | 82 ++----------------- 3 files changed, 95 insertions(+), 137 deletions(-) create mode 100644 pyomo/contrib/piecewise/tests/common_inner_repn_tests.py diff --git a/pyomo/contrib/piecewise/tests/common_inner_repn_tests.py b/pyomo/contrib/piecewise/tests/common_inner_repn_tests.py new file mode 100644 index 00000000000..e0b8e878be3 --- /dev/null +++ b/pyomo/contrib/piecewise/tests/common_inner_repn_tests.py @@ -0,0 +1,80 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.core import Var +from pyomo.core.base import Constraint +from pyomo.core.expr.compare import assertExpressionsEqual + +# This file contains check methods shared between GDP inner representation-based +# transformations. Currently, those are the inner_representation_gdp and +# nested_inner_repn_gdp transformations, since each have disjuncts with the +# same structure. + + +# Check one disjunct from the log model for proper contents +def check_log_disjunct(test, d, pts, f, substitute_var, x): + test.assertEqual(len(d.component_map(Constraint)), 3) + # lambdas and indicator_var + test.assertEqual(len(d.component_map(Var)), 2) + test.assertIsInstance(d.lambdas, Var) + test.assertEqual(len(d.lambdas), 2) + for lamb in d.lambdas.values(): + test.assertEqual(lamb.lb, 0) + test.assertEqual(lamb.ub, 1) + test.assertIsInstance(d.convex_combo, Constraint) + assertExpressionsEqual(test, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] == 1) + test.assertIsInstance(d.set_substitute, Constraint) + assertExpressionsEqual( + test, d.set_substitute.expr, substitute_var == f(x), places=7 + ) + test.assertIsInstance(d.linear_combo, Constraint) + test.assertEqual(len(d.linear_combo), 1) + assertExpressionsEqual( + test, d.linear_combo[0].expr, x == pts[0] * d.lambdas[0] + pts[1] * d.lambdas[1] + ) + + +# Check one disjunct from the paraboloid model for proper contents. +def check_paraboloid_disjunct(test, d, pts, f, substitute_var, x1, x2): + test.assertEqual(len(d.component_map(Constraint)), 3) + # lambdas and indicator_var + test.assertEqual(len(d.component_map(Var)), 2) + test.assertIsInstance(d.lambdas, Var) + test.assertEqual(len(d.lambdas), 3) + for lamb in d.lambdas.values(): + test.assertEqual(lamb.lb, 0) + test.assertEqual(lamb.ub, 1) + test.assertIsInstance(d.convex_combo, Constraint) + assertExpressionsEqual( + test, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] + d.lambdas[2] == 1 + ) + test.assertIsInstance(d.set_substitute, Constraint) + assertExpressionsEqual( + test, d.set_substitute.expr, substitute_var == f(x1, x2), places=7 + ) + test.assertIsInstance(d.linear_combo, Constraint) + test.assertEqual(len(d.linear_combo), 2) + assertExpressionsEqual( + test, + d.linear_combo[0].expr, + x1 + == pts[0][0] * d.lambdas[0] + + pts[1][0] * d.lambdas[1] + + pts[2][0] * d.lambdas[2], + ) + assertExpressionsEqual( + test, + d.linear_combo[1].expr, + x2 + == pts[0][1] * d.lambdas[0] + + pts[1][1] * d.lambdas[1] + + pts[2][1] * d.lambdas[2], + ) diff --git a/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py index 27fe43e54d5..e7505bb92d3 100644 --- a/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_inner_repn_gdp.py @@ -12,6 +12,7 @@ import pyomo.common.unittest as unittest from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct +import pyomo.contrib.piecewise.tests.common_inner_repn_tests as inner_repn_tests from pyomo.core.base import TransformationFactory from pyomo.core.expr.compare import ( assertExpressionsEqual, @@ -22,67 +23,6 @@ class TestTransformPiecewiseModelToInnerRepnGDP(unittest.TestCase): - def check_log_disjunct(self, d, pts, f, substitute_var, x): - self.assertEqual(len(d.component_map(Constraint)), 3) - # lambdas and indicator_var - self.assertEqual(len(d.component_map(Var)), 2) - self.assertIsInstance(d.lambdas, Var) - self.assertEqual(len(d.lambdas), 2) - for lamb in d.lambdas.values(): - self.assertEqual(lamb.lb, 0) - self.assertEqual(lamb.ub, 1) - self.assertIsInstance(d.convex_combo, Constraint) - assertExpressionsEqual( - self, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] == 1 - ) - self.assertIsInstance(d.set_substitute, Constraint) - assertExpressionsEqual( - self, d.set_substitute.expr, substitute_var == f(x), places=7 - ) - self.assertIsInstance(d.linear_combo, Constraint) - self.assertEqual(len(d.linear_combo), 1) - assertExpressionsEqual( - self, - d.linear_combo[0].expr, - x == pts[0] * d.lambdas[0] + pts[1] * d.lambdas[1], - ) - - def check_paraboloid_disjunct(self, d, pts, f, substitute_var, x1, x2): - self.assertEqual(len(d.component_map(Constraint)), 3) - # lambdas and indicator_var - self.assertEqual(len(d.component_map(Var)), 2) - self.assertIsInstance(d.lambdas, Var) - self.assertEqual(len(d.lambdas), 3) - for lamb in d.lambdas.values(): - self.assertEqual(lamb.lb, 0) - self.assertEqual(lamb.ub, 1) - self.assertIsInstance(d.convex_combo, Constraint) - assertExpressionsEqual( - self, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] + d.lambdas[2] == 1 - ) - self.assertIsInstance(d.set_substitute, Constraint) - assertExpressionsEqual( - self, d.set_substitute.expr, substitute_var == f(x1, x2), places=7 - ) - self.assertIsInstance(d.linear_combo, Constraint) - self.assertEqual(len(d.linear_combo), 2) - assertExpressionsEqual( - self, - d.linear_combo[0].expr, - x1 - == pts[0][0] * d.lambdas[0] - + pts[1][0] * d.lambdas[1] - + pts[2][0] * d.lambdas[2], - ) - assertExpressionsEqual( - self, - d.linear_combo[1].expr, - x2 - == pts[0][1] * d.lambdas[0] - + pts[1][1] * d.lambdas[1] - + pts[2][1] * d.lambdas[2], - ) - def check_pw_log(self, m): ## # Check the transformation of the approximation of log(x) @@ -101,7 +41,9 @@ def check_pw_log(self, m): log_block.disjuncts[2]: ((6, 10), m.f3), } for d, (pts, f) in disjuncts_dict.items(): - self.check_log_disjunct(d, pts, f, log_block.substitute_var, m.x) + inner_repn_tests.check_log_disjunct( + self, d, pts, f, log_block.substitute_var, m.x + ) # Check the Disjunction self.assertIsInstance(log_block.pick_a_piece, Disjunction) @@ -129,8 +71,8 @@ def check_pw_paraboloid(self, m): paraboloid_block.disjuncts[3]: ([(0, 7), (0, 4), (3, 4)], m.g2), } for d, (pts, f) in disjuncts_dict.items(): - self.check_paraboloid_disjunct( - d, pts, f, paraboloid_block.substitute_var, m.x1, m.x2 + inner_repn_tests.check_paraboloid_disjunct( + self, d, pts, f, paraboloid_block.substitute_var, m.x1, m.x2 ) # Check the Disjunction diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py index e888db7eb72..2024f014f55 100644 --- a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -12,6 +12,7 @@ import pyomo.common.unittest as unittest from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct +import pyomo.contrib.piecewise.tests.common_inner_repn_tests as inner_repn_tests from pyomo.core.base import TransformationFactory from pyomo.environ import SolverFactory, Var, Constraint from pyomo.gdp import Disjunction, Disjunct @@ -20,71 +21,6 @@ # Test the nested inner repn gdp model using the common_tests code class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): - # Check one disjunct for proper contents. Disjunct structure should be - # identical to the version for the inner representation gdp - def check_log_disjunct(self, d, pts, f, substitute_var, x): - self.assertEqual(len(d.component_map(Constraint)), 3) - # lambdas and indicator_var - self.assertEqual(len(d.component_map(Var)), 2) - self.assertIsInstance(d.lambdas, Var) - self.assertEqual(len(d.lambdas), 2) - for lamb in d.lambdas.values(): - self.assertEqual(lamb.lb, 0) - self.assertEqual(lamb.ub, 1) - self.assertIsInstance(d.convex_combo, Constraint) - assertExpressionsEqual( - self, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] == 1 - ) - self.assertIsInstance(d.set_substitute, Constraint) - assertExpressionsEqual( - self, d.set_substitute.expr, substitute_var == f(x), places=7 - ) - self.assertIsInstance(d.linear_combo, Constraint) - self.assertEqual(len(d.linear_combo), 1) - assertExpressionsEqual( - self, - d.linear_combo[0].expr, - x == pts[0] * d.lambdas[0] + pts[1] * d.lambdas[1], - ) - - # Check one disjunct from the paraboloid block for proper contents. This should - # be identical to the inner_representation_gdp one - def check_paraboloid_disjunct(self, d, pts, f, substitute_var, x1, x2): - self.assertEqual(len(d.component_map(Constraint)), 3) - # lambdas and indicator_var - self.assertEqual(len(d.component_map(Var)), 2) - self.assertIsInstance(d.lambdas, Var) - self.assertEqual(len(d.lambdas), 3) - for lamb in d.lambdas.values(): - self.assertEqual(lamb.lb, 0) - self.assertEqual(lamb.ub, 1) - self.assertIsInstance(d.convex_combo, Constraint) - assertExpressionsEqual( - self, d.convex_combo.expr, d.lambdas[0] + d.lambdas[1] + d.lambdas[2] == 1 - ) - self.assertIsInstance(d.set_substitute, Constraint) - assertExpressionsEqual( - self, d.set_substitute.expr, substitute_var == f(x1, x2), places=7 - ) - self.assertIsInstance(d.linear_combo, Constraint) - self.assertEqual(len(d.linear_combo), 2) - assertExpressionsEqual( - self, - d.linear_combo[0].expr, - x1 - == pts[0][0] * d.lambdas[0] - + pts[1][0] * d.lambdas[1] - + pts[2][0] * d.lambdas[2], - ) - assertExpressionsEqual( - self, - d.linear_combo[1].expr, - x2 - == pts[0][1] * d.lambdas[0] - + pts[1][1] * d.lambdas[1] - + pts[2][1] * d.lambdas[2], - ) - # Check the structure of the log PWLF Block def check_pw_log(self, m): z = m.pw_log.get_transformation_var(m.log_expr) @@ -110,8 +46,8 @@ def check_pw_log(self, m): # Left disjunct with constraints self.assertIsInstance(log_block.d_l, Disjunct) - self.check_log_disjunct( - log_block.d_l, (1, 3), m.f1, log_block.substitute_var, m.x + inner_repn_tests.check_log_disjunct( + self, log_block.d_l, (1, 3), m.f1, log_block.substitute_var, m.x ) # Right disjunct with disjunction @@ -121,12 +57,12 @@ def check_pw_log(self, m): # Left and right child disjuncts with constraints self.assertIsInstance(log_block.d_r.d_l, Disjunct) - self.check_log_disjunct( - log_block.d_r.d_l, (3, 6), m.f2, log_block.substitute_var, m.x + inner_repn_tests.check_log_disjunct( + self, log_block.d_r.d_l, (3, 6), m.f2, log_block.substitute_var, m.x ) self.assertIsInstance(log_block.d_r.d_r, Disjunct) - self.check_log_disjunct( - log_block.d_r.d_r, (6, 10), m.f3, log_block.substitute_var, m.x + inner_repn_tests.check_log_disjunct( + self, log_block.d_r.d_r, (6, 10), m.f3, log_block.substitute_var, m.x ) # Check that this also became the objective @@ -155,8 +91,8 @@ def check_pw_paraboloid(self, m): paraboloid_block.d_r.d_r: ([(0, 7), (0, 4), (3, 4)], m.g2), } for d, (pts, f) in disjuncts_dict.items(): - self.check_paraboloid_disjunct( - d, pts, f, paraboloid_block.substitute_var, m.x1, m.x2 + inner_repn_tests.check_paraboloid_disjunct( + self, d, pts, f, paraboloid_block.substitute_var, m.x1, m.x2 ) # And check the substitute Var is in the objective now. From 85f865d0128fc0019684c579ef81e7b4b353212a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 22 Feb 2024 14:58:23 -0500 Subject: [PATCH 0746/3044] add j1 triangulate script --- .../piecewise/transform/incremental.py | 17 +++- .../piecewise/union_jack_triangulate.py | 81 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 pyomo/contrib/piecewise/union_jack_triangulate.py diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 4287a2c5230..510b591ea82 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( PiecewiseLinearToGDP, @@ -72,7 +83,11 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Ordering of simplices to follow Vielma - # TODO: this enumeration must satisfy O1 (Vielma): each T_i \cap T_{i-1} is nonempty + # TODO: assumption: this enumeration must satisfy O1 (Vielma): each T_i \cap T_{i-1} + # is nonempty + # TODO: One way to make this true will be to use the union_jack__triangulate.py + # script to generate the triangulation, but it is also possible for other + # triangulations to be correct. This should be checkable using a MIP. self.simplex_ordering = { n: n for n in transBlock.simplex_indices } diff --git a/pyomo/contrib/piecewise/union_jack_triangulate.py b/pyomo/contrib/piecewise/union_jack_triangulate.py new file mode 100644 index 00000000000..2853add8161 --- /dev/null +++ b/pyomo/contrib/piecewise/union_jack_triangulate.py @@ -0,0 +1,81 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import itertools +from math import factorial +import time + +# This implements the J1 "Union Jack" triangulation (Todd 77) as explained by +# Vielma 2010. + +# Triangulate {0, ..., K}^n for even K using the J1 triangulation. +def triangulate(K, n): + if K % 2 != 0: + raise ValueError("K must be even") + # 1, 3, ..., K - 1 + axis_odds = range(1, K, 2) + V_0 = itertools.product(axis_odds, repeat=n) + big_iterator = itertools.product(V_0, + itertools.permutations(range(0, n), n), + itertools.product((-1, 1), repeat=n)) + J1 = [] + for v_0, pi, s in big_iterator: + simplex = [] + current = list(v_0) + simplex.append(current) + for i in range(0, n): + current = current.copy() + current[pi[i]] += s[pi[i]] + simplex.append(current) + J1.append(simplex) + return J1 + +if __name__ == '__main__': + # do some tests. TODO move to real test file + start0 = time.time() + small_2d = triangulate(2, 2) + elapsed0 = time.time() - start0 + print(f"triangulated small_2d in {elapsed0} sec.") + assert len(small_2d) == 8 + assert small_2d == [[[1, 1], [0, 1], [0, 0]], + [[1, 1], [0, 1], [0, 2]], + [[1, 1], [2, 1], [2, 0]], + [[1, 1], [2, 1], [2, 2]], + [[1, 1], [1, 0], [0, 0]], + [[1, 1], [1, 2], [0, 2]], + [[1, 1], [1, 0], [2, 0]], + [[1, 1], [1, 2], [2, 2]]] + start1 = time.time() + bigger_2d = triangulate(4, 2) + elapsed1 = time.time() - start1 + print(f"triangulated bigger_2d in {elapsed1} sec.") + assert len(bigger_2d) == 32 + + start2 = time.time() + medium_3d = triangulate(12, 3) + elapsed2 = time.time() - start2 + print(f"triangulated medium_3d in {elapsed2} sec.") + # A J1 triangulation of {0, ..., K}^n has K^n * n! simplices + assert len(medium_3d) == 12**3 * factorial(3) + + start3 = time.time() + big_4d = triangulate(20, 4) + elapsed3 = time.time() - start3 + print(f"triangulated big_4d in {elapsed3} sec.") + assert len(big_4d) == 20**4 * factorial(4) + + print("starting huge_5d") + start4 = time.time() + huge_5d = triangulate(10, 5) + elapsed4 = time.time() - start4 + print(f"triangulated huge_5d in {elapsed4} sec.") + + print("Success") From fcddaf5a0a10503a73ecd1c30009eaefca6cee86 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:02:57 -0700 Subject: [PATCH 0747/3044] Creating a GDP-wide private data class and converting hull to use it for constraint mappings --- .../gdp/plugins/gdp_to_mip_transformation.py | 11 ++++ pyomo/gdp/plugins/hull.py | 53 ++++++++----------- pyomo/gdp/util.py | 8 +-- 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 59cb221321a..7cc55b80f78 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -11,6 +11,7 @@ from functools import wraps +from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import ComponentMap from pyomo.common.log import is_debug_set from pyomo.common.modeling import unique_component_name @@ -48,6 +49,16 @@ from weakref import ref as weakref_ref +class _GDPTransformationData(AutoSlots.Mixin): + __slots__ = ('src_constraint', 'transformed_constraint') + def __init__(self): + self.src_constraint = ComponentMap() + self.transformed_constraint = ComponentMap() + + +Block.register_private_data_initializer(_GDPTransformationData, scope='pyomo.gdp') + + class GDP_to_MIP_Transformation(Transformation): """ Base class for transformations from GDP to MIP diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index d1c38bde039..b4e9fccc089 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -95,20 +95,11 @@ class Hull_Reformulation(GDP_to_MIP_Transformation): The transformation will create a new Block with a unique name beginning "_pyomo_gdp_hull_reformulation". It will contain an indexed Block named "relaxedDisjuncts" that will hold the relaxed - disjuncts. This block is indexed by an integer indicating the order - in which the disjuncts were relaxed. Each block has a dictionary - "_constraintMap": - - 'srcConstraints': ComponentMap(: - ), - 'transformedConstraints': - ComponentMap( : - , - : []) - - All transformed Disjuncts will have a pointer to the block their transformed - constraints are on, and all transformed Disjunctions will have a - pointer to the corresponding OR or XOR constraint. + disjuncts. This block is indexed by an integer indicating the order + in which the disjuncts were relaxed. All transformed Disjuncts will + have a pointer to the block their transformed constraints are on, + and all transformed Disjunctions will have a pointer to the + corresponding OR or XOR constraint. The _pyomo_gdp_hull_reformulation block will have a ComponentMap "_disaggregationConstraintMap": @@ -675,7 +666,7 @@ def _transform_constraint( ): # we will put a new transformed constraint on the relaxation block. relaxationBlock = disjunct._transformation_block() - constraintMap = relaxationBlock._constraintMap + constraint_map = relaxationBlock.private_data('pyomo.gdp') # We will make indexes from ({obj.local_name} x obj.index_set() x ['lb', # 'ub']), but don't bother construct that set here, as taking Cartesian @@ -757,19 +748,19 @@ def _transform_constraint( # this variable, so I'm going to return # it. Alternatively we could return an empty list, but I # think I like this better. - constraintMap['transformedConstraints'][c] = [v[0]] + constraint_map.transformed_constraint[c] = [v[0]] # Reverse map also (this is strange) - constraintMap['srcConstraints'][v[0]] = c + constraint_map.src_constraint[v[0]] = c continue newConsExpr = expr - (1 - y) * h_0 == c.lower * y if obj.is_indexed(): newConstraint.add((name, i, 'eq'), newConsExpr) # map the _ConstraintDatas (we mapped the container above) - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraint[c] = [ newConstraint[name, i, 'eq'] ] - constraintMap['srcConstraints'][newConstraint[name, i, 'eq']] = c + constraint_map.src_constraint[newConstraint[name, i, 'eq']] = c else: newConstraint.add((name, 'eq'), newConsExpr) # map to the _ConstraintData (And yes, for @@ -779,10 +770,10 @@ def _transform_constraint( # IndexedConstraints, we can map the container to the # container, but more importantly, we are mapping the # _ConstraintDatas to each other above) - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraint[c] = [ newConstraint[name, 'eq'] ] - constraintMap['srcConstraints'][newConstraint[name, 'eq']] = c + constraint_map.src_constraint[newConstraint[name, 'eq']] = c continue @@ -797,16 +788,16 @@ def _transform_constraint( if obj.is_indexed(): newConstraint.add((name, i, 'lb'), newConsExpr) - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraint[c] = [ newConstraint[name, i, 'lb'] ] - constraintMap['srcConstraints'][newConstraint[name, i, 'lb']] = c + constraint_map.src_constraint[newConstraint[name, i, 'lb']] = c else: newConstraint.add((name, 'lb'), newConsExpr) - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraint[c] = [ newConstraint[name, 'lb'] ] - constraintMap['srcConstraints'][newConstraint[name, 'lb']] = c + constraint_map.src_constraint[newConstraint[name, 'lb']] = c if c.upper is not None: if self._generate_debug_messages: @@ -821,24 +812,24 @@ def _transform_constraint( newConstraint.add((name, i, 'ub'), newConsExpr) # map (have to account for fact we might have created list # above - transformed = constraintMap['transformedConstraints'].get(c) + transformed = constraint_map.transformed_constraint.get(c) if transformed is not None: transformed.append(newConstraint[name, i, 'ub']) else: - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraint[c] = [ newConstraint[name, i, 'ub'] ] - constraintMap['srcConstraints'][newConstraint[name, i, 'ub']] = c + constraint_map.src_constraint[newConstraint[name, i, 'ub']] = c else: newConstraint.add((name, 'ub'), newConsExpr) - transformed = constraintMap['transformedConstraints'].get(c) + transformed = constraint_map.transformed_constraint.get(c) if transformed is not None: transformed.append(newConstraint[name, 'ub']) else: - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraint[c] = [ newConstraint[name, 'ub'] ] - constraintMap['srcConstraints'][newConstraint[name, 'ub']] = c + constraint_map.src_constraint[newConstraint[name, 'ub']] = c # deactivate now that we have transformed obj.deactivate() diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index 57eef29eded..9f929fbc621 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -477,16 +477,17 @@ def get_src_constraint(transformedConstraint): a transformation block """ transBlock = transformedConstraint.parent_block() + src_constraints = transBlock.private_data('pyomo.gdp').src_constraint # This should be our block, so if it's not, the user messed up and gave # us the wrong thing. If they happen to also have a _constraintMap then # the world is really against us. - if not hasattr(transBlock, "_constraintMap"): + if transformedConstraint not in src_constraints: raise GDP_Error( "Constraint '%s' is not a transformed constraint" % transformedConstraint.name ) # if something goes wrong here, it's a bug in the mappings. - return transBlock._constraintMap['srcConstraints'][transformedConstraint] + return src_constraints[transformedConstraint] def _find_parent_disjunct(constraint): @@ -538,7 +539,8 @@ def get_transformed_constraints(srcConstraint): ) transBlock = _get_constraint_transBlock(srcConstraint) try: - return transBlock._constraintMap['transformedConstraints'][srcConstraint] + return transBlock.private_data('pyomo.gdp').transformed_constraint[ + srcConstraint] except: logger.error("Constraint '%s' has not been transformed." % srcConstraint.name) raise From 735056b9a8e90b5d0efd4b981e84dcdaa022267e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:18:43 -0700 Subject: [PATCH 0748/3044] Moving bigm transformations onto private data for mapping constraints --- pyomo/gdp/plugins/bigm.py | 15 ++----- pyomo/gdp/plugins/bigm_mixin.py | 14 +++---- .../gdp/plugins/gdp_to_mip_transformation.py | 7 ---- pyomo/gdp/plugins/multiple_bigm.py | 40 +++++++++---------- 4 files changed, 30 insertions(+), 46 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index 1f9f561b192..ce98180e9d0 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -94,15 +94,8 @@ class BigM_Transformation(GDP_to_MIP_Transformation, _BigM_MixIn): name beginning "_pyomo_gdp_bigm_reformulation". That Block will contain an indexed Block named "relaxedDisjuncts", which will hold the relaxed disjuncts. This block is indexed by an integer - indicating the order in which the disjuncts were relaxed. - Each block has a dictionary "_constraintMap": - - 'srcConstraints': ComponentMap(: - ) - 'transformedConstraints': ComponentMap(: - ) - - All transformed Disjuncts will have a pointer to the block their transformed + indicating the order in which the disjuncts were relaxed. All + transformed Disjuncts will have a pointer to the block their transformed constraints are on, and all transformed Disjunctions will have a pointer to the corresponding 'Or' or 'ExactlyOne' constraint. @@ -279,7 +272,7 @@ def _transform_constraint( # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() bigm_src = transBlock.bigm_src - constraintMap = transBlock._constraintMap + constraint_map = transBlock.private_data('pyomo.gdp') disjunctionRelaxationBlock = transBlock.parent_block() @@ -346,7 +339,7 @@ def _transform_constraint( bigm_src[c] = (lower, upper) self._add_constraint_expressions( - c, i, M, disjunct.binary_indicator_var, newConstraint, constraintMap + c, i, M, disjunct.binary_indicator_var, newConstraint, constraint_map ) # deactivate because we relaxed diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index ad6e6dcad86..59d79331a34 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -232,7 +232,7 @@ def _estimate_M(self, expr, constraint): return tuple(M) def _add_constraint_expressions( - self, c, i, M, indicator_var, newConstraint, constraintMap + self, c, i, M, indicator_var, newConstraint, constraint_map ): # Since we are both combining components from multiple blocks and using # local names, we need to make sure that the first index for @@ -253,8 +253,8 @@ def _add_constraint_expressions( ) M_expr = M[0] * (1 - indicator_var) newConstraint.add((name, i, 'lb'), c.lower <= c.body - M_expr) - constraintMap['transformedConstraints'][c] = [newConstraint[name, i, 'lb']] - constraintMap['srcConstraints'][newConstraint[name, i, 'lb']] = c + constraint_map.transformed_constraint[c] = [newConstraint[name, i, 'lb']] + constraint_map.src_constraint[newConstraint[name, i, 'lb']] = c if c.upper is not None: if M[1] is None: raise GDP_Error( @@ -263,13 +263,13 @@ def _add_constraint_expressions( ) M_expr = M[1] * (1 - indicator_var) newConstraint.add((name, i, 'ub'), c.body - M_expr <= c.upper) - transformed = constraintMap['transformedConstraints'].get(c) + transformed = constraint_map.transformed_constraint.get(c) if transformed is not None: - constraintMap['transformedConstraints'][c].append( + constraint_map.transformed_constraint[c].append( newConstraint[name, i, 'ub'] ) else: - constraintMap['transformedConstraints'][c] = [ + constraint_map.transformed_constraint[c] = [ newConstraint[name, i, 'ub'] ] - constraintMap['srcConstraints'][newConstraint[name, i, 'ub']] = c + constraint_map.src_constraint[newConstraint[name, i, 'ub']] = c diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 7cc55b80f78..9547d80bab3 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -254,14 +254,7 @@ def _get_disjunct_transformation_block(self, disjunct, transBlock): relaxationBlock = relaxedDisjuncts[len(relaxedDisjuncts)] relaxationBlock.transformedConstraints = Constraint(Any) - relaxationBlock.localVarReferences = Block() - # add the map that will link back and forth between transformed - # constraints and their originals. - relaxationBlock._constraintMap = { - 'srcConstraints': ComponentMap(), - 'transformedConstraints': ComponentMap(), - } # add mappings to source disjunct (so we'll know we've relaxed) disjunct._transformation_block = weakref_ref(relaxationBlock) diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 6177de3c037..3d867798161 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -359,7 +359,7 @@ def _transform_disjunct(self, obj, transBlock, active_disjuncts, Ms): def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): # we will put a new transformed constraint on the relaxation block. relaxationBlock = disjunct._transformation_block() - constraintMap = relaxationBlock._constraintMap + constraint_map = relaxationBlock.private_data('pyomo.gdp') transBlock = relaxationBlock.parent_block() # Though rare, it is possible to get naming conflicts here @@ -397,8 +397,8 @@ def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): newConstraint.add((i, 'ub'), c.body - c.upper <= rhs) transformed.append(newConstraint[i, 'ub']) for c_new in transformed: - constraintMap['srcConstraints'][c_new] = [c] - constraintMap['transformedConstraints'][c] = transformed + constraint_map.src_constraint[c_new] = [c] + constraint_map.transformed_constraint[c] = transformed else: lower = (None, None, None) upper = (None, None, None) @@ -427,7 +427,7 @@ def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): M, disjunct.indicator_var.get_associated_binary(), newConstraint, - constraintMap, + constraint_map, ) # deactivate now that we have transformed @@ -496,6 +496,7 @@ def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): relaxationBlock = self._get_disjunct_transformation_block( disj, transBlock ) + constraint_map = relaxationBlock.private_data('pyomo.gdp') if len(lower_dict) > 0: M = lower_dict.get(disj, None) if M is None: @@ -527,39 +528,36 @@ def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): idx = i + offset if len(lower_dict) > 0: transformed.add((idx, 'lb'), v >= lower_rhs) - relaxationBlock._constraintMap['srcConstraints'][ + constraint_map.src_constraint[ transformed[idx, 'lb'] ] = [] for c, disj in lower_bound_constraints_by_var[v]: - relaxationBlock._constraintMap['srcConstraints'][ + constraint_map.src_constraint[ transformed[idx, 'lb'] ].append(c) - disj.transformation_block._constraintMap['transformedConstraints'][ + disj.transformation_block.private_data( + 'pyomo.gdp').transformed_constraint[ c ] = [transformed[idx, 'lb']] if len(upper_dict) > 0: transformed.add((idx, 'ub'), v <= upper_rhs) - relaxationBlock._constraintMap['srcConstraints'][ + constraint_map.src_constraint[ transformed[idx, 'ub'] ] = [] for c, disj in upper_bound_constraints_by_var[v]: - relaxationBlock._constraintMap['srcConstraints'][ + constraint_map.src_constraint[ transformed[idx, 'ub'] ].append(c) # might already be here if it had an upper bound - if ( - c - in disj.transformation_block._constraintMap[ - 'transformedConstraints' - ] - ): - disj.transformation_block._constraintMap[ - 'transformedConstraints' - ][c].append(transformed[idx, 'ub']) + disj_constraint_map = disj.transformation_block.private_data( + 'pyomo.gdp') + if c in disj_constraint_map.transformed_constraint: + disj_constraint_map.transformed_constraint[c].append( + transformed[idx, 'ub']) else: - disj.transformation_block._constraintMap[ - 'transformedConstraints' - ][c] = [transformed[idx, 'ub']] + disj_constraint_map.transformed_constraint[c] = [ + transformed[idx, 'ub'] + ] return transformed_constraints From 763586e44b6081f800094d954bdf5fe6a0f4105c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:21:44 -0700 Subject: [PATCH 0749/3044] Moving binary multiplication onto private data mappings --- pyomo/gdp/plugins/binary_multiplication.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyomo/gdp/plugins/binary_multiplication.py b/pyomo/gdp/plugins/binary_multiplication.py index d68f7efe76f..6d0955c95a7 100644 --- a/pyomo/gdp/plugins/binary_multiplication.py +++ b/pyomo/gdp/plugins/binary_multiplication.py @@ -121,7 +121,7 @@ def _transform_disjunct(self, obj, transBlock): def _transform_constraint(self, obj, disjunct): # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() - constraintMap = transBlock._constraintMap + constraint_map = transBlock.private_data('pyomo.gdp') disjunctionRelaxationBlock = transBlock.parent_block() @@ -137,14 +137,14 @@ def _transform_constraint(self, obj, disjunct): continue self._add_constraint_expressions( - c, i, disjunct.binary_indicator_var, newConstraint, constraintMap + c, i, disjunct.binary_indicator_var, newConstraint, constraint_map ) # deactivate because we relaxed c.deactivate() def _add_constraint_expressions( - self, c, i, indicator_var, newConstraint, constraintMap + self, c, i, indicator_var, newConstraint, constraint_map ): # Since we are both combining components from multiple blocks and using # local names, we need to make sure that the first index for @@ -156,21 +156,21 @@ def _add_constraint_expressions( # over the constraint indices, but I don't think it matters a lot.) unique = len(newConstraint) name = c.local_name + "_%s" % unique - transformed = constraintMap['transformedConstraints'][c] = [] + transformed = constraint_map.transformed_constraint[c] = [] lb, ub = c.lower, c.upper if (c.equality or lb is ub) and lb is not None: # equality newConstraint.add((name, i, 'eq'), (c.body - lb) * indicator_var == 0) transformed.append(newConstraint[name, i, 'eq']) - constraintMap['srcConstraints'][newConstraint[name, i, 'eq']] = c + constraint_map.src_constraint[newConstraint[name, i, 'eq']] = c else: # inequality if lb is not None: newConstraint.add((name, i, 'lb'), 0 <= (c.body - lb) * indicator_var) transformed.append(newConstraint[name, i, 'lb']) - constraintMap['srcConstraints'][newConstraint[name, i, 'lb']] = c + constraint_map.src_constraint[newConstraint[name, i, 'lb']] = c if ub is not None: newConstraint.add((name, i, 'ub'), (c.body - ub) * indicator_var <= 0) transformed.append(newConstraint[name, i, 'ub']) - constraintMap['srcConstraints'][newConstraint[name, i, 'ub']] = c + constraint_map.src_constraint[newConstraint[name, i, 'ub']] = c From b0da69ce9a6ae69aae3cb07ef38fcd890ecefcae Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:22:59 -0700 Subject: [PATCH 0750/3044] Even black thinks this is prettier --- .../gdp/plugins/gdp_to_mip_transformation.py | 1 + pyomo/gdp/plugins/multiple_bigm.py | 27 +++++++------------ pyomo/gdp/util.py | 3 ++- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 9547d80bab3..94dde433a15 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -51,6 +51,7 @@ class _GDPTransformationData(AutoSlots.Mixin): __slots__ = ('src_constraint', 'transformed_constraint') + def __init__(self): self.src_constraint = ComponentMap() self.transformed_constraint = ComponentMap() diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index 3d867798161..fccb0514dfb 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -528,32 +528,25 @@ def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): idx = i + offset if len(lower_dict) > 0: transformed.add((idx, 'lb'), v >= lower_rhs) - constraint_map.src_constraint[ - transformed[idx, 'lb'] - ] = [] + constraint_map.src_constraint[transformed[idx, 'lb']] = [] for c, disj in lower_bound_constraints_by_var[v]: - constraint_map.src_constraint[ - transformed[idx, 'lb'] - ].append(c) + constraint_map.src_constraint[transformed[idx, 'lb']].append(c) disj.transformation_block.private_data( - 'pyomo.gdp').transformed_constraint[ - c - ] = [transformed[idx, 'lb']] + 'pyomo.gdp' + ).transformed_constraint[c] = [transformed[idx, 'lb']] if len(upper_dict) > 0: transformed.add((idx, 'ub'), v <= upper_rhs) - constraint_map.src_constraint[ - transformed[idx, 'ub'] - ] = [] + constraint_map.src_constraint[transformed[idx, 'ub']] = [] for c, disj in upper_bound_constraints_by_var[v]: - constraint_map.src_constraint[ - transformed[idx, 'ub'] - ].append(c) + constraint_map.src_constraint[transformed[idx, 'ub']].append(c) # might already be here if it had an upper bound disj_constraint_map = disj.transformation_block.private_data( - 'pyomo.gdp') + 'pyomo.gdp' + ) if c in disj_constraint_map.transformed_constraint: disj_constraint_map.transformed_constraint[c].append( - transformed[idx, 'ub']) + transformed[idx, 'ub'] + ) else: disj_constraint_map.transformed_constraint[c] = [ transformed[idx, 'ub'] diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index 9f929fbc621..b3e7de8a7cd 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -540,7 +540,8 @@ def get_transformed_constraints(srcConstraint): transBlock = _get_constraint_transBlock(srcConstraint) try: return transBlock.private_data('pyomo.gdp').transformed_constraint[ - srcConstraint] + srcConstraint + ] except: logger.error("Constraint '%s' has not been transformed." % srcConstraint.name) raise From e7acc12dbefa4a2d53305cd2b517bfeaac5222a0 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 23 Feb 2024 08:30:46 -0700 Subject: [PATCH 0751/3044] fix division by zero error in linear presolve --- pyomo/contrib/solver/ipopt.py | 44 ++++++++----- .../solver/tests/solvers/test_solvers.py | 65 +++++++++++++++++++ 2 files changed, 92 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 3ac1a5ac4a2..dc632adb184 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -17,7 +17,11 @@ from pyomo.common import Executable from pyomo.common.config import ConfigValue, document_kwargs_from_configdict, ConfigDict -from pyomo.common.errors import PyomoException, DeveloperError +from pyomo.common.errors import ( + PyomoException, + DeveloperError, + InfeasibleConstraintException, +) from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer from pyomo.core.base.var import _GeneralVarData @@ -72,11 +76,7 @@ def __init__( ), ) self.writer_config: ConfigDict = self.declare( - 'writer_config', - ConfigValue( - default=NLWriter.CONFIG(), - description="Configuration that controls options in the NL writer.", - ), + 'writer_config', NLWriter.CONFIG() ) @@ -314,15 +314,19 @@ def solve(self, model, **kwds): ) as row_file, open(basename + '.col', 'w') as col_file: timer.start('write_nl_file') self._writer.config.set_value(config.writer_config) - nl_info = self._writer.write( - model, - nl_file, - row_file, - col_file, - symbolic_solver_labels=config.symbolic_solver_labels, - ) + try: + nl_info = self._writer.write( + model, + nl_file, + row_file, + col_file, + symbolic_solver_labels=config.symbolic_solver_labels, + ) + proven_infeasible = False + except InfeasibleConstraintException: + proven_infeasible = True timer.stop('write_nl_file') - if len(nl_info.variables) > 0: + if not proven_infeasible and len(nl_info.variables) > 0: # Get a copy of the environment to pass to the subprocess env = os.environ.copy() if nl_info.external_function_libraries: @@ -361,11 +365,17 @@ def solve(self, model, **kwds): timer.stop('subprocess') # This is the stuff we need to parse to get the iterations # and time - iters, ipopt_time_nofunc, ipopt_time_func, ipopt_total_time = ( + (iters, ipopt_time_nofunc, ipopt_time_func, ipopt_total_time) = ( self._parse_ipopt_output(ostreams[0]) ) - if len(nl_info.variables) == 0: + if proven_infeasible: + results = Results() + results.termination_condition = TerminationCondition.provenInfeasible + results.solution_loader = SolSolutionLoader(None, None) + results.iteration_count = 0 + results.timing_info.total_seconds = 0 + elif len(nl_info.variables) == 0: if len(nl_info.eliminated_vars) == 0: results = Results() results.termination_condition = TerminationCondition.emptyModel @@ -457,7 +467,7 @@ def solve(self, model, **kwds): ) results.solver_configuration = config - if len(nl_info.variables) > 0: + if not proven_infeasible and len(nl_info.variables) > 0: results.solver_log = ostreams[0].getvalue() # Capture/record end-time / wall-time diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index cf5f6cf5c57..a4f4a3bc389 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -1508,6 +1508,71 @@ def test_bug_2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool) res = opt.solve(m) self.assertAlmostEqual(res.incumbent_objective, -18, 5) + @parameterized.expand(input=_load_tests(nl_solvers)) + def test_presolve_with_zero_coef( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False + + """ + when c2 gets presolved out, c1 becomes + x - y + y = 0 which becomes + x - 0*y == 0 which is the zero we are testing for + """ + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2 + m.z**2) + m.c1 = pe.Constraint(expr=m.x == m.y + m.z + 1.5) + m.c2 = pe.Constraint(expr=m.z == -m.y) + + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2.25) + self.assertAlmostEqual(m.x.value, 1.5) + self.assertAlmostEqual(m.y.value, 0) + self.assertAlmostEqual(m.z.value, 0) + + m.x.setlb(2) + res = opt.solve( + m, load_solutions=False, raise_exception_on_nonoptimal_result=False + ) + if use_presolve: + exp = TerminationCondition.provenInfeasible + else: + exp = TerminationCondition.locallyInfeasible + self.assertEqual(res.termination_condition, exp) + + m = pe.ConcreteModel() + m.w = pe.Var() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2 + m.z**2 + m.w**2) + m.c1 = pe.Constraint(expr=m.x + m.w == m.y + m.z) + m.c2 = pe.Constraint(expr=m.z == -m.y) + m.c3 = pe.Constraint(expr=m.x == -m.w) + + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(m.w.value, 0) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 0) + self.assertAlmostEqual(m.z.value, 0) + + del m.c1 + m.c1 = pe.Constraint(expr=m.x + m.w == m.y + m.z + 1.5) + res = opt.solve( + m, load_solutions=False, raise_exception_on_nonoptimal_result=False + ) + self.assertEqual(res.termination_condition, exp) + @parameterized.expand(input=_load_tests(all_solvers)) def test_scaling(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() From 0dabe3fe9d0636e9122544dbdcec16d61907bd84 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 23 Feb 2024 08:35:17 -0700 Subject: [PATCH 0752/3044] fix division by zero error in linear presolve --- pyomo/repn/plugins/nl_writer.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index f3ff94ea8c9..66c695dafa3 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1820,10 +1820,24 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): # appropriately (that expr_info is persisting in the # eliminated_vars dict - and we will use that to # update other linear expressions later.) + old_nnz = len(expr_info.linear) c = expr_info.linear.pop(_id, 0) + nnz = old_nnz - 1 expr_info.const += c * b if x in expr_info.linear: expr_info.linear[x] += c * a + if expr_info.linear[x] == 0: + nnz -= 1 + coef = expr_info.linear.pop(x) + if not nnz: + if abs(expr_info.const) > TOL: + # constraint is trivially infeasible + raise InfeasibleConstraintException( + "model contains a trivially infeasible constrint " + f"{expr_info.const} == {coef}*{var_map[x]}" + ) + # constraint is trivially feasible + eliminated_cons.add(con_id) elif a: expr_info.linear[x] = c * a # replacing _id with x... NNZ is not changing, @@ -1831,9 +1845,7 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): # this constraint comp_by_linear_var[x].append((con_id, expr_info)) continue - # NNZ has been reduced by 1 - nnz = len(expr_info.linear) - _old = lcon_by_linear_nnz[nnz + 1] + _old = lcon_by_linear_nnz[old_nnz] if con_id in _old: lcon_by_linear_nnz[nnz][con_id] = _old.pop(con_id) # If variables were replaced by the variable that From b59978e8b8fba623e4affa00ba94713b006af32f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:35:25 -0700 Subject: [PATCH 0753/3044] Moving hull disaggregation constraint mappings to private_data --- pyomo/gdp/plugins/hull.py | 27 +++++++-------------------- pyomo/gdp/tests/test_hull.py | 3 +-- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index b4e9fccc089..4a7445283a9 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -58,12 +58,14 @@ class _HullTransformationData(AutoSlots.Mixin): - __slots__ = ('disaggregated_var_map', 'original_var_map', 'bigm_constraint_map') + __slots__ = ('disaggregated_var_map', 'original_var_map', 'bigm_constraint_map', + 'disaggregation_constraint_map') def __init__(self): self.disaggregated_var_map = DefaultComponentMap(ComponentMap) self.original_var_map = ComponentMap() self.bigm_constraint_map = DefaultComponentMap(ComponentMap) + self.disaggregation_constraint_map = DefaultComponentMap(ComponentMap) Block.register_private_data_initializer(_HullTransformationData) @@ -100,10 +102,6 @@ class Hull_Reformulation(GDP_to_MIP_Transformation): have a pointer to the block their transformed constraints are on, and all transformed Disjunctions will have a pointer to the corresponding OR or XOR constraint. - - The _pyomo_gdp_hull_reformulation block will have a ComponentMap - "_disaggregationConstraintMap": - :ComponentMap(: ) """ CONFIG = cfg.ConfigDict('gdp.hull') @@ -285,10 +283,6 @@ def _add_transformation_block(self, to_block): # Disjunctions we transform onto this block here. transBlock.disaggregationConstraints = Constraint(NonNegativeIntegers) - # This will map from srcVar to a map of srcDisjunction to the - # disaggregation constraint corresponding to srcDisjunction - transBlock._disaggregationConstraintMap = ComponentMap() - # we are going to store some of the disaggregated vars directly here # when we have vars that don't appear in every disjunct transBlock._disaggregatedVars = Var(NonNegativeIntegers, dense=False) @@ -321,7 +315,7 @@ def _transform_disjunctionData( ) disaggregationConstraint = transBlock.disaggregationConstraints - disaggregationConstraintMap = transBlock._disaggregationConstraintMap + disaggregationConstraintMap = transBlock.private_data().disaggregation_constraint_map disaggregatedVars = transBlock._disaggregatedVars disaggregated_var_bounds = transBlock._boundsConstraints @@ -490,13 +484,7 @@ def _transform_disjunctionData( # and update the map so that we can find this later. We index by # variable and the particular disjunction because there is a # different one for each disjunction - if var in disaggregationConstraintMap: - disaggregationConstraintMap[var][obj] = disaggregationConstraint[ - cons_idx - ] - else: - thismap = disaggregationConstraintMap[var] = ComponentMap() - thismap[obj] = disaggregationConstraint[cons_idx] + disaggregationConstraintMap[var][obj] = disaggregationConstraint[cons_idx] # deactivate for the writers obj.deactivate() @@ -922,9 +910,8 @@ def get_disaggregation_constraint( ) try: - cons = transBlock.parent_block()._disaggregationConstraintMap[original_var][ - disjunction - ] + cons = transBlock.parent_block().private_data().disaggregation_constraint_map[ + original_var][disjunction] except: if raise_exception: logger.error( diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 858764759ee..98322d4888d 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -2314,8 +2314,7 @@ def test_mapping_method_errors(self): with LoggingIntercept(log, 'pyomo.gdp.hull', logging.ERROR): self.assertRaisesRegex( KeyError, - r".*_pyomo_gdp_hull_reformulation.relaxedDisjuncts\[1\]." - r"disaggregatedVars.w", + r".*disjunction", hull.get_disaggregation_constraint, m.d[1].transformation_block.disaggregatedVars.w, m.disjunction, From 9f3d6980eaff3b996bf434da2f862d07ce4fd53e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:36:01 -0700 Subject: [PATCH 0754/3044] black adding newlines --- pyomo/gdp/plugins/hull.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 4a7445283a9..a2b050298e2 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -58,8 +58,12 @@ class _HullTransformationData(AutoSlots.Mixin): - __slots__ = ('disaggregated_var_map', 'original_var_map', 'bigm_constraint_map', - 'disaggregation_constraint_map') + __slots__ = ( + 'disaggregated_var_map', + 'original_var_map', + 'bigm_constraint_map', + 'disaggregation_constraint_map', + ) def __init__(self): self.disaggregated_var_map = DefaultComponentMap(ComponentMap) @@ -315,7 +319,9 @@ def _transform_disjunctionData( ) disaggregationConstraint = transBlock.disaggregationConstraints - disaggregationConstraintMap = transBlock.private_data().disaggregation_constraint_map + disaggregationConstraintMap = ( + transBlock.private_data().disaggregation_constraint_map + ) disaggregatedVars = transBlock._disaggregatedVars disaggregated_var_bounds = transBlock._boundsConstraints @@ -910,8 +916,11 @@ def get_disaggregation_constraint( ) try: - cons = transBlock.parent_block().private_data().disaggregation_constraint_map[ - original_var][disjunction] + cons = ( + transBlock.parent_block() + .private_data() + .disaggregation_constraint_map[original_var][disjunction] + ) except: if raise_exception: logger.error( From 1acf6188789f63d558d521e3d8f1c8e7a99d55d8 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 23 Feb 2024 08:37:03 -0700 Subject: [PATCH 0755/3044] fix typo --- pyomo/repn/plugins/nl_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 66c695dafa3..bd7ade34923 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1760,7 +1760,7 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): id2_isdiscrete = var_map[id2].domain.isdiscrete() if var_map[_id].domain.isdiscrete() ^ id2_isdiscrete: # if only one variable is discrete, then we need to - # substiitute out the other + # substitute out the other if id2_isdiscrete: _id, id2 = id2, _id coef, coef2 = coef2, coef From 4cceaa99653c0bdcdec759b75e249f73a2f9723c Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 23 Feb 2024 08:42:41 -0700 Subject: [PATCH 0756/3044] fix typo --- pyomo/repn/plugins/nl_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index bd7ade34923..3fd97ac06d0 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1833,7 +1833,7 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): if abs(expr_info.const) > TOL: # constraint is trivially infeasible raise InfeasibleConstraintException( - "model contains a trivially infeasible constrint " + "model contains a trivially infeasible constraint " f"{expr_info.const} == {coef}*{var_map[x]}" ) # constraint is trivially feasible From 6811943281374a5732d35660e1cda0c1c9ed1ca5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:45:33 -0700 Subject: [PATCH 0757/3044] Moving bigM src mapping to private data --- pyomo/gdp/plugins/bigm.py | 43 ++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index ce98180e9d0..bcf9606877b 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -13,6 +13,7 @@ import logging +from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import ComponentMap from pyomo.common.config import ConfigDict, ConfigValue from pyomo.common.gc_manager import PauseGC @@ -58,6 +59,23 @@ logger = logging.getLogger('pyomo.gdp.bigm') +class _BigMData(AutoSlots.Mixin): + __slots__ = ('bigm_src',) + def __init__(self): + # we will keep a map of constraints (hashable, ha!) to a tuple to + # indicate what their M value is and where it came from, of the form: + # ((lower_value, lower_source, lower_key), (upper_value, upper_source, + # upper_key)), where the first tuple is the information for the lower M, + # the second tuple is the info for the upper M, source is the Suffix or + # argument dictionary and None if the value was calculated, and key is + # the key in the Suffix or argument dictionary, and None if it was + # calculated. (Note that it is possible the lower or upper is + # user-specified and the other is not, hence the need to store + # information for both.) + self.bigm_src = {} + +Block.register_private_data_initializer(_BigMData) + @TransformationFactory.register( 'gdp.bigm', doc="Relax disjunctive model using big-M terms." ) @@ -240,18 +258,6 @@ def _transform_disjunct(self, obj, bigM, transBlock): relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) - # we will keep a map of constraints (hashable, ha!) to a tuple to - # indicate what their M value is and where it came from, of the form: - # ((lower_value, lower_source, lower_key), (upper_value, upper_source, - # upper_key)), where the first tuple is the information for the lower M, - # the second tuple is the info for the upper M, source is the Suffix or - # argument dictionary and None if the value was calculated, and key is - # the key in the Suffix or argument dictionary, and None if it was - # calculated. (Note that it is possible the lower or upper is - # user-specified and the other is not, hence the need to store - # information for both.) - relaxationBlock.bigm_src = {} - # This is crazy, but if the disjunction has been previously # relaxed, the disjunct *could* be deactivated. This is a big # deal for Hull, as it uses the component_objects / @@ -271,7 +277,7 @@ def _transform_constraint( ): # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() - bigm_src = transBlock.bigm_src + bigm_src = transBlock.private_data().bigm_src constraint_map = transBlock.private_data('pyomo.gdp') disjunctionRelaxationBlock = transBlock.parent_block() @@ -402,7 +408,7 @@ def _update_M_from_suffixes(self, constraint, suffix_list, lower, upper): def get_m_value_src(self, constraint): transBlock = _get_constraint_transBlock(constraint) ((lower_val, lower_source, lower_key), (upper_val, upper_source, upper_key)) = ( - transBlock.bigm_src[constraint] + transBlock.private_data().bigm_src[constraint] ) if ( @@ -457,7 +463,7 @@ def get_M_value_src(self, constraint): transBlock = _get_constraint_transBlock(constraint) # This is a KeyError if it fails, but it is also my fault if it # fails... (That is, it's a bug in the mapping.) - return transBlock.bigm_src[constraint] + return transBlock.private_data().bigm_src[constraint] def get_M_value(self, constraint): """Returns the M values used to transform constraint. Return is a tuple: @@ -472,7 +478,7 @@ def get_M_value(self, constraint): transBlock = _get_constraint_transBlock(constraint) # This is a KeyError if it fails, but it is also my fault if it # fails... (That is, it's a bug in the mapping.) - lower, upper = transBlock.bigm_src[constraint] + lower, upper = transBlock.private_data().bigm_src[constraint] return (lower[0], upper[0]) def get_all_M_values_by_constraint(self, model): @@ -492,9 +498,8 @@ def get_all_M_values_by_constraint(self, model): # First check if it was transformed at all. if transBlock is not None: # If it was transformed with BigM, we get the M values. - if hasattr(transBlock, 'bigm_src'): - for cons in transBlock.bigm_src: - m_values[cons] = self.get_M_value(cons) + for cons in transBlock.private_data().bigm_src: + m_values[cons] = self.get_M_value(cons) return m_values def get_largest_M_value(self, model): From c9232e9439507919a714cfd3d7dd31b724ada14c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 08:45:57 -0700 Subject: [PATCH 0758/3044] black --- pyomo/gdp/plugins/bigm.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index bcf9606877b..3f450dbbd4f 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -61,6 +61,7 @@ class _BigMData(AutoSlots.Mixin): __slots__ = ('bigm_src',) + def __init__(self): # we will keep a map of constraints (hashable, ha!) to a tuple to # indicate what their M value is and where it came from, of the form: @@ -74,8 +75,10 @@ def __init__(self): # information for both.) self.bigm_src = {} + Block.register_private_data_initializer(_BigMData) + @TransformationFactory.register( 'gdp.bigm', doc="Relax disjunctive model using big-M terms." ) From 1221996f3a940ddc0e0de7240158f4c49551d386 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 13:20:11 -0700 Subject: [PATCH 0759/3044] Making transformed_constraints a DefaultComponentMap --- pyomo/gdp/plugins/bigm_mixin.py | 15 +++---- pyomo/gdp/plugins/binary_multiplication.py | 2 +- .../gdp/plugins/gdp_to_mip_transformation.py | 6 +-- pyomo/gdp/plugins/hull.py | 39 +++++++------------ pyomo/gdp/plugins/multiple_bigm.py | 16 +++----- pyomo/gdp/tests/test_bigm.py | 36 ++++++----------- pyomo/gdp/tests/test_hull.py | 17 +++----- pyomo/gdp/util.py | 14 +++---- 8 files changed, 53 insertions(+), 92 deletions(-) diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index 59d79331a34..b76c8d43279 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -253,7 +253,8 @@ def _add_constraint_expressions( ) M_expr = M[0] * (1 - indicator_var) newConstraint.add((name, i, 'lb'), c.lower <= c.body - M_expr) - constraint_map.transformed_constraint[c] = [newConstraint[name, i, 'lb']] + constraint_map.transformed_constraints[c].append( + newConstraint[name, i, 'lb']) constraint_map.src_constraint[newConstraint[name, i, 'lb']] = c if c.upper is not None: if M[1] is None: @@ -263,13 +264,7 @@ def _add_constraint_expressions( ) M_expr = M[1] * (1 - indicator_var) newConstraint.add((name, i, 'ub'), c.body - M_expr <= c.upper) - transformed = constraint_map.transformed_constraint.get(c) - if transformed is not None: - constraint_map.transformed_constraint[c].append( - newConstraint[name, i, 'ub'] - ) - else: - constraint_map.transformed_constraint[c] = [ - newConstraint[name, i, 'ub'] - ] + constraint_map.transformed_constraints[c].append( + newConstraint[name, i, 'ub'] + ) constraint_map.src_constraint[newConstraint[name, i, 'ub']] = c diff --git a/pyomo/gdp/plugins/binary_multiplication.py b/pyomo/gdp/plugins/binary_multiplication.py index 6d0955c95a7..bea33580ed6 100644 --- a/pyomo/gdp/plugins/binary_multiplication.py +++ b/pyomo/gdp/plugins/binary_multiplication.py @@ -156,7 +156,7 @@ def _add_constraint_expressions( # over the constraint indices, but I don't think it matters a lot.) unique = len(newConstraint) name = c.local_name + "_%s" % unique - transformed = constraint_map.transformed_constraint[c] = [] + transformed = constraint_map.transformed_constraints[c] lb, ub = c.lower, c.upper if (c.equality or lb is ub) and lb is not None: diff --git a/pyomo/gdp/plugins/gdp_to_mip_transformation.py b/pyomo/gdp/plugins/gdp_to_mip_transformation.py index 94dde433a15..8dcd22b292a 100644 --- a/pyomo/gdp/plugins/gdp_to_mip_transformation.py +++ b/pyomo/gdp/plugins/gdp_to_mip_transformation.py @@ -12,7 +12,7 @@ from functools import wraps from pyomo.common.autoslots import AutoSlots -from pyomo.common.collections import ComponentMap +from pyomo.common.collections import ComponentMap, DefaultComponentMap from pyomo.common.log import is_debug_set from pyomo.common.modeling import unique_component_name @@ -50,11 +50,11 @@ class _GDPTransformationData(AutoSlots.Mixin): - __slots__ = ('src_constraint', 'transformed_constraint') + __slots__ = ('src_constraint', 'transformed_constraints') def __init__(self): self.src_constraint = ComponentMap() - self.transformed_constraint = ComponentMap() + self.transformed_constraints = DefaultComponentMap(list) Block.register_private_data_initializer(_GDPTransformationData, scope='pyomo.gdp') diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index a2b050298e2..1dc6b76e6a6 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -742,7 +742,7 @@ def _transform_constraint( # this variable, so I'm going to return # it. Alternatively we could return an empty list, but I # think I like this better. - constraint_map.transformed_constraint[c] = [v[0]] + constraint_map.transformed_constraints[c].append(v[0]) # Reverse map also (this is strange) constraint_map.src_constraint[v[0]] = c continue @@ -751,9 +751,8 @@ def _transform_constraint( if obj.is_indexed(): newConstraint.add((name, i, 'eq'), newConsExpr) # map the _ConstraintDatas (we mapped the container above) - constraint_map.transformed_constraint[c] = [ - newConstraint[name, i, 'eq'] - ] + constraint_map.transformed_constraints[c].append( + newConstraint[name, i, 'eq']) constraint_map.src_constraint[newConstraint[name, i, 'eq']] = c else: newConstraint.add((name, 'eq'), newConsExpr) @@ -764,9 +763,9 @@ def _transform_constraint( # IndexedConstraints, we can map the container to the # container, but more importantly, we are mapping the # _ConstraintDatas to each other above) - constraint_map.transformed_constraint[c] = [ + constraint_map.transformed_constraints[c].append( newConstraint[name, 'eq'] - ] + ) constraint_map.src_constraint[newConstraint[name, 'eq']] = c continue @@ -782,15 +781,15 @@ def _transform_constraint( if obj.is_indexed(): newConstraint.add((name, i, 'lb'), newConsExpr) - constraint_map.transformed_constraint[c] = [ + constraint_map.transformed_constraints[c].append( newConstraint[name, i, 'lb'] - ] + ) constraint_map.src_constraint[newConstraint[name, i, 'lb']] = c else: newConstraint.add((name, 'lb'), newConsExpr) - constraint_map.transformed_constraint[c] = [ + constraint_map.transformed_constraints[c].append( newConstraint[name, 'lb'] - ] + ) constraint_map.src_constraint[newConstraint[name, 'lb']] = c if c.upper is not None: @@ -806,23 +805,15 @@ def _transform_constraint( newConstraint.add((name, i, 'ub'), newConsExpr) # map (have to account for fact we might have created list # above - transformed = constraint_map.transformed_constraint.get(c) - if transformed is not None: - transformed.append(newConstraint[name, i, 'ub']) - else: - constraint_map.transformed_constraint[c] = [ - newConstraint[name, i, 'ub'] - ] + constraint_map.transformed_constraints[c].append( + newConstraint[name, i, 'ub'] + ) constraint_map.src_constraint[newConstraint[name, i, 'ub']] = c else: newConstraint.add((name, 'ub'), newConsExpr) - transformed = constraint_map.transformed_constraint.get(c) - if transformed is not None: - transformed.append(newConstraint[name, 'ub']) - else: - constraint_map.transformed_constraint[c] = [ - newConstraint[name, 'ub'] - ] + constraint_map.transformed_constraints[c].append( + newConstraint[name, 'ub'] + ) constraint_map.src_constraint[newConstraint[name, 'ub']] = c # deactivate now that we have transformed diff --git a/pyomo/gdp/plugins/multiple_bigm.py b/pyomo/gdp/plugins/multiple_bigm.py index fccb0514dfb..4dffd4e9f9a 100644 --- a/pyomo/gdp/plugins/multiple_bigm.py +++ b/pyomo/gdp/plugins/multiple_bigm.py @@ -378,7 +378,7 @@ def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): continue if not self._config.only_mbigm_bound_constraints: - transformed = [] + transformed = constraint_map.transformed_constraints[c] if c.lower is not None: rhs = sum( Ms[c, disj][0] * disj.indicator_var.get_associated_binary() @@ -398,7 +398,6 @@ def _transform_constraint(self, obj, disjunct, active_disjuncts, Ms): transformed.append(newConstraint[i, 'ub']) for c_new in transformed: constraint_map.src_constraint[c_new] = [c] - constraint_map.transformed_constraint[c] = transformed else: lower = (None, None, None) upper = (None, None, None) @@ -533,7 +532,7 @@ def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): constraint_map.src_constraint[transformed[idx, 'lb']].append(c) disj.transformation_block.private_data( 'pyomo.gdp' - ).transformed_constraint[c] = [transformed[idx, 'lb']] + ).transformed_constraints[c].append(transformed[idx, 'lb']) if len(upper_dict) > 0: transformed.add((idx, 'ub'), v <= upper_rhs) constraint_map.src_constraint[transformed[idx, 'ub']] = [] @@ -543,14 +542,9 @@ def _transform_bound_constraints(self, active_disjuncts, transBlock, Ms): disj_constraint_map = disj.transformation_block.private_data( 'pyomo.gdp' ) - if c in disj_constraint_map.transformed_constraint: - disj_constraint_map.transformed_constraint[c].append( - transformed[idx, 'ub'] - ) - else: - disj_constraint_map.transformed_constraint[c] = [ - transformed[idx, 'ub'] - ] + disj_constraint_map.transformed_constraints[c].append( + transformed[idx, 'ub'] + ) return transformed_constraints diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index 00efcb46485..ec281218786 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1316,18 +1316,11 @@ def test_do_not_transform_deactivated_constraintDatas(self): bigm.apply_to(m) # the real test: This wasn't transformed - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp', logging.ERROR): - self.assertRaisesRegex( - KeyError, - r".*b.simpledisj1.c\[1\]", - bigm.get_transformed_constraints, - m.b.simpledisj1.c[1], - ) - self.assertRegex( - log.getvalue(), - r".*Constraint 'b.simpledisj1.c\[1\]' has not been transformed.", - ) + with self.assertRaisesRegex( + GDP_Error, + r"Constraint 'b.simpledisj1.c\[1\]' has not been transformed." + ): + bigm.get_transformed_constraints(m.b.simpledisj1.c[1]) # and the rest of the container was transformed cons_list = bigm.get_transformed_constraints(m.b.simpledisj1.c[2]) @@ -2272,18 +2265,13 @@ def check_all_but_evil1_b_anotherblock_constraint_transformed(self, m): self.assertEqual(len(evil1), 2) self.assertIs(evil1[0].parent_block(), disjBlock[1]) self.assertIs(evil1[1].parent_block(), disjBlock[1]) - out = StringIO() - with LoggingIntercept(out, 'pyomo.gdp', logging.ERROR): - self.assertRaisesRegex( - KeyError, - r".*.evil\[1\].b.anotherblock.c", - bigm.get_transformed_constraints, - m.evil[1].b.anotherblock.c, - ) - self.assertRegex( - out.getvalue(), - r".*Constraint 'evil\[1\].b.anotherblock.c' has not been transformed.", - ) + with self.assertRaisesRegex( + GDP_Error, + r"Constraint 'evil\[1\].b.anotherblock.c' has not been " + r"transformed.", + ): + bigm.get_transformed_constraints(m.evil[1].b.anotherblock.c) + evil1 = bigm.get_transformed_constraints(m.evil[1].bb[1].c) self.assertEqual(len(evil1), 2) self.assertIs(evil1[0].parent_block(), disjBlock[1]) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 98322d4888d..02b3e0152b4 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -897,18 +897,11 @@ def test_do_not_transform_deactivated_constraintDatas(self): hull = TransformationFactory('gdp.hull') hull.apply_to(m) # can't ask for simpledisj1.c[1]: it wasn't transformed - log = StringIO() - with LoggingIntercept(log, 'pyomo.gdp', logging.ERROR): - self.assertRaisesRegex( - KeyError, - r".*b.simpledisj1.c\[1\]", - hull.get_transformed_constraints, - m.b.simpledisj1.c[1], - ) - self.assertRegex( - log.getvalue(), - r".*Constraint 'b.simpledisj1.c\[1\]' has not been transformed.", - ) + with self.assertRaisesRegex( + GDP_Error, + r"Constraint 'b.simpledisj1.c\[1\]' has not been transformed." + ): + hull.get_transformed_constraints(m.b.simpledisj1.c[1]) # this fixes a[2] to 0, so we should get the disggregated var transformed = hull.get_transformed_constraints(m.b.simpledisj1.c[2]) diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index b3e7de8a7cd..e7da03c7f41 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -538,13 +538,13 @@ def get_transformed_constraints(srcConstraint): "from any of its _ComponentDatas.)" ) transBlock = _get_constraint_transBlock(srcConstraint) - try: - return transBlock.private_data('pyomo.gdp').transformed_constraint[ - srcConstraint - ] - except: - logger.error("Constraint '%s' has not been transformed." % srcConstraint.name) - raise + transformed_constraints = transBlock.private_data( + 'pyomo.gdp').transformed_constraints + if srcConstraint in transformed_constraints: + return transformed_constraints[srcConstraint] + else: + raise GDP_Error("Constraint '%s' has not been transformed." % + srcConstraint.name) def _warn_for_active_disjunct(innerdisjunct, outerdisjunct): From 1405731cfa5547d478c3cc9a52e1a60dd61bfa27 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Feb 2024 13:21:50 -0700 Subject: [PATCH 0760/3044] black --- pyomo/gdp/plugins/bigm_mixin.py | 3 ++- pyomo/gdp/plugins/hull.py | 3 ++- pyomo/gdp/tests/test_bigm.py | 6 ++---- pyomo/gdp/tests/test_hull.py | 3 +-- pyomo/gdp/util.py | 8 +++++--- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index b76c8d43279..510b36b5102 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -254,7 +254,8 @@ def _add_constraint_expressions( M_expr = M[0] * (1 - indicator_var) newConstraint.add((name, i, 'lb'), c.lower <= c.body - M_expr) constraint_map.transformed_constraints[c].append( - newConstraint[name, i, 'lb']) + newConstraint[name, i, 'lb'] + ) constraint_map.src_constraint[newConstraint[name, i, 'lb']] = c if c.upper is not None: if M[1] is None: diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 1dc6b76e6a6..5b9d2ad08a9 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -752,7 +752,8 @@ def _transform_constraint( newConstraint.add((name, i, 'eq'), newConsExpr) # map the _ConstraintDatas (we mapped the container above) constraint_map.transformed_constraints[c].append( - newConstraint[name, i, 'eq']) + newConstraint[name, i, 'eq'] + ) constraint_map.src_constraint[newConstraint[name, i, 'eq']] = c else: newConstraint.add((name, 'eq'), newConsExpr) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index ec281218786..2383d4587f5 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1317,8 +1317,7 @@ def test_do_not_transform_deactivated_constraintDatas(self): # the real test: This wasn't transformed with self.assertRaisesRegex( - GDP_Error, - r"Constraint 'b.simpledisj1.c\[1\]' has not been transformed." + GDP_Error, r"Constraint 'b.simpledisj1.c\[1\]' has not been transformed." ): bigm.get_transformed_constraints(m.b.simpledisj1.c[1]) @@ -2267,8 +2266,7 @@ def check_all_but_evil1_b_anotherblock_constraint_transformed(self, m): self.assertIs(evil1[1].parent_block(), disjBlock[1]) with self.assertRaisesRegex( GDP_Error, - r"Constraint 'evil\[1\].b.anotherblock.c' has not been " - r"transformed.", + r"Constraint 'evil\[1\].b.anotherblock.c' has not been transformed.", ): bigm.get_transformed_constraints(m.evil[1].b.anotherblock.c) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 02b3e0152b4..55edf244731 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -898,8 +898,7 @@ def test_do_not_transform_deactivated_constraintDatas(self): hull.apply_to(m) # can't ask for simpledisj1.c[1]: it wasn't transformed with self.assertRaisesRegex( - GDP_Error, - r"Constraint 'b.simpledisj1.c\[1\]' has not been transformed." + GDP_Error, r"Constraint 'b.simpledisj1.c\[1\]' has not been transformed." ): hull.get_transformed_constraints(m.b.simpledisj1.c[1]) diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index e7da03c7f41..fe11975954d 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -539,12 +539,14 @@ def get_transformed_constraints(srcConstraint): ) transBlock = _get_constraint_transBlock(srcConstraint) transformed_constraints = transBlock.private_data( - 'pyomo.gdp').transformed_constraints + 'pyomo.gdp' + ).transformed_constraints if srcConstraint in transformed_constraints: return transformed_constraints[srcConstraint] else: - raise GDP_Error("Constraint '%s' has not been transformed." % - srcConstraint.name) + raise GDP_Error( + "Constraint '%s' has not been transformed." % srcConstraint.name + ) def _warn_for_active_disjunct(innerdisjunct, outerdisjunct): From 58996fa1e4da2df12a5e0d6290ff95ed178d2603 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 24 Feb 2024 13:12:58 -0700 Subject: [PATCH 0761/3044] fix division by zero error in linear presolve --- pyomo/repn/plugins/nl_writer.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 3fd97ac06d0..a256cd1b900 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1829,15 +1829,6 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): if expr_info.linear[x] == 0: nnz -= 1 coef = expr_info.linear.pop(x) - if not nnz: - if abs(expr_info.const) > TOL: - # constraint is trivially infeasible - raise InfeasibleConstraintException( - "model contains a trivially infeasible constraint " - f"{expr_info.const} == {coef}*{var_map[x]}" - ) - # constraint is trivially feasible - eliminated_cons.add(con_id) elif a: expr_info.linear[x] = c * a # replacing _id with x... NNZ is not changing, @@ -1847,6 +1838,15 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): continue _old = lcon_by_linear_nnz[old_nnz] if con_id in _old: + if not nnz: + if abs(expr_info.const) > TOL: + # constraint is trivially infeasible + raise InfeasibleConstraintException( + "model contains a trivially infeasible constraint " + f"{expr_info.const} == {coef}*{var_map[x]}" + ) + # constraint is trivially feasible + eliminated_cons.add(con_id) lcon_by_linear_nnz[nnz][con_id] = _old.pop(con_id) # If variables were replaced by the variable that # we are currently eliminating, then we need to update From 9bf36c7f81367449ba0441a0840487bb122320bd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 25 Feb 2024 13:26:51 -0700 Subject: [PATCH 0762/3044] Adding tests to NLv2 motivated by 58996fa --- pyomo/repn/tests/ampl/test_nlv2.py | 125 +++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 8b95fc03bdb..215715dba10 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -1683,6 +1683,131 @@ def test_presolve_named_expressions(self): G0 2 #obj 0 0 1 0 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_zero_coef(self): + m = ConcreteModel() + m.x = Var() + m.y = Var() + m.z = Var() + m.obj = Objective(expr=m.x**2 + m.y**2 + m.z**2) + m.c1 = Constraint(expr=m.x == m.y + m.z + 1.5) + m.c2 = Constraint(expr=m.z == -m.y) + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual(LOG.getvalue(), "") + + self.assertEqual(nlinfo.eliminated_vars[0], (m.x, 1.5)) + self.assertIs(nlinfo.eliminated_vars[1][0], m.y) + self.assertExpressionsEqual( + nlinfo.eliminated_vars[1][1], LinearExpression([-1.0 * m.z]) + ) + + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 0 1 0 0 #vars, constraints, objectives, ranges, eqns + 0 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 1 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 0 1 #nonzeros in Jacobian, obj. gradient + 3 1 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +O0 0 #obj +o54 #sumlist +3 #(n) +o5 #^ +n1.5 +n2 +o5 #^ +o16 #- +v0 #z +n2 +o5 #^ +v0 #z +n2 +x0 #initial guess +r #0 ranges (rhs's) +b #1 bounds (on variables) +3 #z +k0 #intermediate Jacobian column lengths +G0 1 #obj +0 0 +""", + OUT.getvalue(), + ) + ) + + m.c3 = Constraint(expr=m.x == 2) + OUT = io.StringIO() + with LoggingIntercept() as LOG: + with self.assertRaisesRegex( + nl_writer.InfeasibleConstraintException, + r"model contains a trivially infeasible constraint 0.5 == 0.0\*y", + ): + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual(LOG.getvalue(), "") + + m.c1.set_value(m.x >= m.y + m.z + 1.5) + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual(LOG.getvalue(), "") + + self.assertIs(nlinfo.eliminated_vars[0][0], m.y) + self.assertExpressionsEqual( + nlinfo.eliminated_vars[0][1], LinearExpression([-1.0 * m.z]) + ) + self.assertEqual(nlinfo.eliminated_vars[1], (m.x, 2)) + + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 1 1 0 0 #vars, constraints, objectives, ranges, eqns + 0 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 1 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 0 1 #nonzeros in Jacobian, obj. gradient + 3 1 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +C0 #c1 +n0 +O0 0 #obj +o54 #sumlist +3 #(n) +o5 #^ +n2 +n2 +o5 #^ +o16 #- +v0 #z +n2 +o5 #^ +v0 #z +n2 +x0 #initial guess +r #1 ranges (rhs's) +1 0.5 #c1 +b #1 bounds (on variables) +3 #z +k0 #intermediate Jacobian column lengths +G0 1 #obj +0 0 """, OUT.getvalue(), ) From fca1035351fec7f189c07557705f340a00d71dae Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sun, 25 Feb 2024 16:01:49 -0700 Subject: [PATCH 0763/3044] allow cyipopt to solve problems without objectives --- .../algorithms/solvers/cyipopt_solver.py | 23 +++++++++++++++---- .../solvers/tests/test_cyipopt_solver.py | 10 ++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py index 9d24c0dd562..cdea542295b 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py @@ -24,6 +24,8 @@ from pyomo.common.deprecation import relocated_module_attribute from pyomo.common.dependencies import attempt_import, numpy as np, numpy_available from pyomo.common.tee import redirect_fd, TeeStream +from pyomo.common.modeling import unique_component_name +from pyomo.core.base.objective import Objective # Because pynumero.interfaces requires numpy, we will leverage deferred # imports here so that the solver can be registered even when numpy is @@ -332,11 +334,22 @@ def solve(self, model, **kwds): grey_box_blocks = list( model.component_data_objects(egb.ExternalGreyBoxBlock, active=True) ) - if grey_box_blocks: - # nlp = pyomo_nlp.PyomoGreyBoxNLP(model) - nlp = pyomo_grey_box.PyomoNLPWithGreyBoxBlocks(model) - else: - nlp = pyomo_nlp.PyomoNLP(model) + # if there is no objective, add one temporarily so we can construct an NLP + objectives = list(model.component_data_objects(Objective, active=True)) + if not objectives: + objname = unique_component_name(model, "_obj") + objective = model.add_component(objname, Objective(expr=0.0)) + try: + if grey_box_blocks: + # nlp = pyomo_nlp.PyomoGreyBoxNLP(model) + nlp = pyomo_grey_box.PyomoNLPWithGreyBoxBlocks(model) + else: + nlp = pyomo_nlp.PyomoNLP(model) + finally: + # We only need the objective to construct the NLP, so we delete + # it from the model ASAP + if not objectives: + model.del_component(objective) problem = cyipopt_interface.CyIpoptNLP( nlp, diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py index e9da31097a0..0af5a772c98 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py @@ -316,3 +316,13 @@ def test_hs071_evalerror_old_cyipopt(self): msg = "Error in AMPL evaluation" with self.assertRaisesRegex(PyNumeroEvaluationError, msg): res = solver.solve(m, tee=True) + + def test_solve_without_objective(self): + m = create_model1() + m.o.deactivate() + m.x[2].fix(0.0) + m.x[3].fix(4.0) + solver = pyo.SolverFactory("cyipopt") + res = solver.solve(m, tee=True) + pyo.assert_optimal_termination(res) + self.assertAlmostEqual(m.x[1].value, 9.0) From 242ba7f424eb351ad250c48963627b30e43d2a3c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 25 Feb 2024 17:15:39 -0700 Subject: [PATCH 0764/3044] Updating the TPL package list due to contrib.solver --- pyomo/environ/tests/test_environ.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/environ/tests/test_environ.py b/pyomo/environ/tests/test_environ.py index 9c89fd135d5..6121a310024 100644 --- a/pyomo/environ/tests/test_environ.py +++ b/pyomo/environ/tests/test_environ.py @@ -140,6 +140,7 @@ def test_tpl_import_time(self): 'cPickle', 'csv', 'ctypes', # mandatory import in core/base/external.py; TODO: fix this + 'datetime', # imported by contrib.solver 'decimal', 'gc', # Imported on MacOS, Windows; Linux in 3.10 'glob', From 2edbf24a6cde120889cc7a1d32bda814f3d9379a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 25 Feb 2024 17:21:30 -0700 Subject: [PATCH 0765/3044] Apply black --- pyomo/environ/tests/test_environ.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/environ/tests/test_environ.py b/pyomo/environ/tests/test_environ.py index 6121a310024..9811b412af7 100644 --- a/pyomo/environ/tests/test_environ.py +++ b/pyomo/environ/tests/test_environ.py @@ -140,7 +140,7 @@ def test_tpl_import_time(self): 'cPickle', 'csv', 'ctypes', # mandatory import in core/base/external.py; TODO: fix this - 'datetime', # imported by contrib.solver + 'datetime', # imported by contrib.solver 'decimal', 'gc', # Imported on MacOS, Windows; Linux in 3.10 'glob', From 7d88fe4aee8a97e0e199e95c3f7e29064bccd3fa Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Mon, 26 Feb 2024 17:05:40 +0100 Subject: [PATCH 0766/3044] Added MAiNGO appsi-interface --- pyomo/contrib/appsi/solvers/__init__.py | 1 + pyomo/contrib/appsi/solvers/maingo.py | 653 ++++++++++++++++++++++++ 2 files changed, 654 insertions(+) create mode 100644 pyomo/contrib/appsi/solvers/maingo.py diff --git a/pyomo/contrib/appsi/solvers/__init__.py b/pyomo/contrib/appsi/solvers/__init__.py index c03523a69d4..c9e0a2a003d 100644 --- a/pyomo/contrib/appsi/solvers/__init__.py +++ b/pyomo/contrib/appsi/solvers/__init__.py @@ -15,3 +15,4 @@ from .cplex import Cplex from .highs import Highs from .wntr import Wntr, WntrResults +from .maingo import MAiNGO \ No newline at end of file diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py new file mode 100644 index 00000000000..dcb8040eabe --- /dev/null +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -0,0 +1,653 @@ +from collections import namedtuple +import logging +import math +import sys +from typing import Optional, List, Dict + +from pyomo.contrib.appsi.base import ( + PersistentSolver, + Results, + TerminationCondition, + MIPSolverConfig, + PersistentBase, + PersistentSolutionLoader, +) +from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available +from pyomo.common.collections import ComponentMap +from pyomo.common.config import ConfigValue, NonNegativeInt +from pyomo.common.dependencies import attempt_import +from pyomo.common.errors import PyomoException +from pyomo.common.log import LogStream +from pyomo.common.tee import capture_output, TeeStream +from pyomo.common.timing import HierarchicalTimer +from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler +from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.expression import ScalarExpression +from pyomo.core.base.param import _ParamData +from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.var import Var, _GeneralVarData +import pyomo.core.expr.expr_common as common +import pyomo.core.expr as EXPR +from pyomo.core.expr.numvalue import ( + value, + is_constant, + is_fixed, + native_numeric_types, + native_types, + nonpyomo_leaf_types, +) +from pyomo.core.kernel.objective import minimize, maximize +from pyomo.core.staleflag import StaleFlagManager +from pyomo.repn.util import valid_expr_ctypes_minlp + +_plusMinusOne = {-1, 1} + +MaingoVar = namedtuple("MaingoVar", "type name lb ub init") + +logger = logging.getLogger(__name__) + + +def _import_maingopy(): + try: + import maingopy + except ImportError: + MAiNGO._available = MAiNGO.Availability.NotFound + raise + return maingopy + + +maingopy, maingopy_available = attempt_import("maingopy", importer=_import_maingopy) + + +class MAiNGOConfig(MIPSolverConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(MAiNGOConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare("logfile", ConfigValue(domain=str)) + self.declare("solver_output_logger", ConfigValue()) + self.declare("log_level", ConfigValue(domain=NonNegativeInt)) + + self.logfile = "" + self.solver_output_logger = logger + self.log_level = logging.INFO + + +class MAiNGOSolutionLoader(PersistentSolutionLoader): + def load_vars(self, vars_to_load=None): + self._assert_solution_still_valid() + self._solver.load_vars(vars_to_load=vars_to_load) + + def get_primals(self, vars_to_load=None): + self._assert_solution_still_valid() + return self._solver.get_primals(vars_to_load=vars_to_load) + + +class MAiNGOResults(Results): + def __init__(self, solver): + super(MAiNGOResults, self).__init__() + self.wallclock_time = None + self.cpu_time = None + self.solution_loader = MAiNGOSolutionLoader(solver=solver) + + +class SolverModel(maingopy.MAiNGOmodel): + def __init__(self, var_list, objective, con_list, idmap): + maingopy.MAiNGOmodel.__init__(self) + self._var_list = var_list + self._con_list = con_list + self._objective = objective + self._idmap = idmap + + def build_maingo_objective(self, obj, visitor): + maingo_obj = visitor.dfs_postorder_stack(obj.expr) + if obj.sense == maximize: + maingo_obj *= -1 + return maingo_obj + + def build_maingo_constraints(self, cons, visitor): + eqs = [] + ineqs = [] + for con in cons: + if con.equality: + eqs += [visitor.dfs_postorder_stack(con.body - con.lower)] + elif con.has_ub() and con.has_lb(): + ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] + ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] + elif con.has_ub(): + ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] + elif con.has_ub(): + ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] + else: + raise ValueError( + "Constraint does not have a lower " + "or an upper bound: {0} \n".format(con) + ) + return eqs, ineqs + + def get_variables(self): + return [ + maingopy.OptimizationVariable( + maingopy.Bounds(var.lb, var.ub), var.type, var.name + ) + for var in self._var_list + ] + + def get_initial_point(self): + return [var.init if not var.init is None else var.lb for var in self._var_list] + + def evaluate(self, maingo_vars): + visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) + result = maingopy.EvaluationContainer() + result.objective = self.build_maingo_objective(self._objective, visitor) + eqs, ineqs = self.build_maingo_constraints(self._con_list, visitor) + result.eq = eqs + result.ineq = ineqs + return result + + +LEFT_TO_RIGHT = common.OperatorAssociativity.LEFT_TO_RIGHT +RIGHT_TO_LEFT = common.OperatorAssociativity.RIGHT_TO_LEFT + + +class ToMAiNGOVisitor(EXPR.ExpressionValueVisitor): + def __init__(self, variables, idmap): + super(ToMAiNGOVisitor, self).__init__() + self.variables = variables + self.idmap = idmap + self._pyomo_func_to_maingo_func = { + "log": maingopy.log, + "log10": ToMAiNGOVisitor.maingo_log10, + "sin": maingopy.sin, + "cos": maingopy.cos, + "tan": maingopy.tan, + "cosh": maingopy.cosh, + "sinh": maingopy.sinh, + "tanh": maingopy.tanh, + "asin": maingopy.asin, + "acos": maingopy.acos, + "atan": maingopy.atan, + "exp": maingopy.exp, + "sqrt": maingopy.sqrt, + "asinh": ToMAiNGOVisitor.maingo_asinh, + "acosh": ToMAiNGOVisitor.maingo_acosh, + "atanh": ToMAiNGOVisitor.maingo_atanh, + } + + @classmethod + def maingo_log10(cls, x): + return maingopy.log(x) / math.log(10) + + @classmethod + def maingo_asinh(cls, x): + return maingopy.inv(maingopy.sinh(x)) + + @classmethod + def maingo_acosh(cls, x): + return maingopy.inv(maingopy.cosh(x)) + + @classmethod + def maingo_atanh(cls, x): + return maingopy.inv(maingopy.tanh(x)) + + def visit(self, node, values): + """Visit nodes that have been expanded""" + for i, val in enumerate(values): + arg = node._args_[i] + + if arg is None: + values[i] = "Undefined" + elif arg.__class__ in native_numeric_types: + pass + elif arg.__class__ in nonpyomo_leaf_types: + values[i] = val + else: + parens = False + if arg.is_expression_type() and node.PRECEDENCE is not None: + if arg.PRECEDENCE is None: + pass + elif node.PRECEDENCE < arg.PRECEDENCE: + parens = True + elif node.PRECEDENCE == arg.PRECEDENCE: + if i == 0: + parens = node.ASSOCIATIVITY != LEFT_TO_RIGHT + elif i == len(node._args_) - 1: + parens = node.ASSOCIATIVITY != RIGHT_TO_LEFT + else: + parens = True + if parens: + values[i] = val + + if node.__class__ in EXPR.NPV_expression_types: + return value(node) + + if node.__class__ in {EXPR.ProductExpression, EXPR.MonomialTermExpression}: + return values[0] * values[1] + + if node.__class__ in {EXPR.SumExpression}: + return sum(values) + + if node.__class__ in {EXPR.PowExpression}: + return maingopy.pow(values[0], values[1]) + + if node.__class__ in {EXPR.DivisionExpression}: + return values[0] / values[1] + + if node.__class__ in {EXPR.NegationExpression}: + return -values[0] + + if node.__class__ in {EXPR.AbsExpression}: + return maingopy.abs(values[0]) + + if node.__class__ in {EXPR.UnaryFunctionExpression}: + pyomo_func = node.getname() + maingo_func = self._pyomo_func_to_maingo_func[pyomo_func] + return maingo_func(values[0]) + + if node.__class__ in {ScalarExpression}: + return values[0] + + raise ValueError(f"Unknown function expression encountered: {node.getname()}") + + def visiting_potential_leaf(self, node): + """ + Visiting a potential leaf. + + Return True if the node is not expanded. + """ + if node.__class__ in native_types: + return True, node + + if node.is_expression_type(): + if node.__class__ is EXPR.MonomialTermExpression: + return True, self._monomial_to_maingo(node) + if node.__class__ is EXPR.LinearExpression: + return True, self._linear_to_maingo(node) + return False, None + + if node.is_component_type(): + if node.ctype not in valid_expr_ctypes_minlp: + # Make sure all components in active constraints + # are basic ctypes we know how to deal with. + raise RuntimeError( + "Unallowable component '%s' of type %s found in an active " + "constraint or objective.\nMAiNGO cannot export " + "expressions with this component type." + % (node.name, node.ctype.__name__) + ) + + if node.is_fixed(): + return True, node() + else: + assert node.is_variable_type() + maingo_var_id = self.idmap[id(node)] + maingo_var = self.variables[maingo_var_id] + return True, maingo_var + + def _monomial_to_maingo(self, node): + const, var = node.args + maingo_var_id = self.idmap[id(var)] + maingo_var = self.variables[maingo_var_id] + if const.__class__ not in native_types: + const = value(const) + if var.is_fixed(): + return const * var.value + if not const: + return 0 + if const in _plusMinusOne: + if const < 0: + return -maingo_var + else: + return maingo_var + return const * maingo_var + + def _linear_to_maingo(self, node): + values = [ + self._monomial_to_maingo(arg) + if ( + arg.__class__ is EXPR.MonomialTermExpression + and not arg.arg(1).is_fixed() + ) + else value(arg) + for arg in node.args + ] + return sum(values) + + +class MAiNGO(PersistentBase, PersistentSolver): + """ + Interface to MAiNGO + """ + + _available = None + + def __init__(self, only_child_vars=False): + super(MAiNGO, self).__init__(only_child_vars=only_child_vars) + self._config = MAiNGOConfig() + self._solver_options = dict() + self._solver_model = None + self._mymaingo = None + self._symbol_map = SymbolMap() + self._labeler = None + self._maingo_vars = [] + self._objective = None + self._cons = [] + self._pyomo_var_to_solver_var_id_map = dict() + self._last_results_object: Optional[MAiNGOResults] = None + + def available(self): + if not maingopy_available: + return self.Availability.NotFound + self._available = True + return self._available + + def version(self): + pass + + @property + def config(self) -> MAiNGOConfig: + return self._config + + @config.setter + def config(self, val: MAiNGOConfig): + self._config = val + + @property + def maingo_options(self): + """ + A dictionary mapping solver options to values for those options. These + are solver specific. + + Returns + ------- + dict + A dictionary mapping solver options to values for those options + """ + return self._solver_options + + @maingo_options.setter + def maingo_options(self, val: Dict): + self._solver_options = val + + @property + def symbol_map(self): + return self._symbol_map + + def _solve(self, timer: HierarchicalTimer): + ostreams = [ + LogStream( + level=self.config.log_level, logger=self.config.solver_output_logger + ) + ] + if self.config.stream_solver: + ostreams.append(sys.stdout) + + with TeeStream(*ostreams) as t: + with capture_output(output=t.STDOUT, capture_fd=False): + config = self.config + options = self.maingo_options + + self._mymaingo = maingopy.MAiNGO(self._solver_model) + + self._mymaingo.set_option("loggingDestination", 2) + self._mymaingo.set_log_file_name(config.logfile) + + if config.time_limit is not None: + self._mymaingo.set_option("maxTime", config.time_limit) + if config.mip_gap is not None: + self._mymaingo.set_option("epsilonA", config.mip_gap) + for key, option in options.items(): + self._mymaingo.set_option(key, option) + + timer.start("MAiNGO solve") + self._mymaingo.solve() + timer.stop("MAiNGO solve") + + return self._postsolve(timer) + + def solve(self, model, timer: HierarchicalTimer = None): + StaleFlagManager.mark_all_as_stale() + + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if timer is None: + timer = HierarchicalTimer() + timer.start("set_instance") + self.set_instance(model) + timer.stop("set_instance") + res = self._solve(timer) + self._last_results_object = res + if self.config.report_timing: + logger.info("\n" + str(timer)) + return res + + def _process_domain_and_bounds(self, var): + _v, _lb, _ub, _fixed, _domain_interval, _value = self._vars[id(var)] + lb, ub, step = _domain_interval + if lb is None: + lb = -1e10 + if ub is None: + ub = 1e10 + if step == 0: + vtype = maingopy.VT_CONTINUOUS + elif step == 1: + if lb == 0 and ub == 1: + vtype = maingopy.VT_BINARY + else: + vtype = maingopy.VT_INTEGER + else: + raise ValueError( + f"Unrecognized domain step: {step} (should be either 0 or 1)" + ) + if _fixed: + lb = _value + ub = _value + else: + if _lb is not None: + lb = max(value(_lb), lb) + if _ub is not None: + ub = min(value(_ub), ub) + + return lb, ub, vtype + + def _add_variables(self, variables: List[_GeneralVarData]): + for ndx, var in enumerate(variables): + varname = self._symbol_map.getSymbol(var, self._labeler) + lb, ub, vtype = self._process_domain_and_bounds(var) + self._maingo_vars.append( + MaingoVar(name=varname, type=vtype, lb=lb, ub=ub, init=var.value) + ) + self._pyomo_var_to_solver_var_id_map[id(var)] = len(self._maingo_vars) - 1 + + def _add_params(self, params: List[_ParamData]): + pass + + def _reinit(self): + saved_config = self.config + saved_options = self.maingo_options + saved_update_config = self.update_config + self.__init__(only_child_vars=self._only_child_vars) + self.config = saved_config + self.maingo_options = saved_options + self.update_config = saved_update_config + + def set_instance(self, model): + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if not self.available(): + c = self.__class__ + raise PyomoException( + f"Solver {c.__module__}.{c.__qualname__} is not available " + f"({self.available()})." + ) + self._reinit() + self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() + + if self.config.symbolic_solver_labels: + self._labeler = TextLabeler() + else: + self._labeler = NumericLabeler("x") + + self.add_block(model) + self._solver_model = SolverModel( + var_list=self._maingo_vars, + con_list=self._cons, + objective=self._objective, + idmap=self._pyomo_var_to_solver_var_id_map, + ) + + def _add_constraints(self, cons: List[_GeneralConstraintData]): + self._cons = cons + + def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def _remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def _remove_variables(self, variables: List[_GeneralVarData]): + pass + + def _remove_params(self, params: List[_ParamData]): + pass + + def _update_variables(self, variables: List[_GeneralVarData]): + pass + + def update_params(self): + pass + + def _set_objective(self, obj): + if obj is None: + raise NotImplementedError( + "MAiNGO needs a objective. Please set a dummy objective." + ) + else: + if not obj.sense in {minimize, maximize}: + raise ValueError( + "Objective sense is not recognized: {0}".format(obj.sense) + ) + self._objective = obj + + def _postsolve(self, timer: HierarchicalTimer): + config = self.config + + mprob = self._mymaingo + status = mprob.get_status() + results = MAiNGOResults(solver=self) + results.wallclock_time = mprob.get_wallclock_solution_time() + results.cpu_time = mprob.get_cpu_solution_time() + + if status == maingopy.GLOBALLY_OPTIMAL: + results.termination_condition = TerminationCondition.optimal + elif status == maingopy.INFEASIBLE: + results.termination_condition = TerminationCondition.infeasible + else: + results.termination_condition = TerminationCondition.unknown + + results.best_feasible_objective = None + results.best_objective_bound = None + if self._objective is not None: + try: + if self._objective.sense == maximize: + results.best_feasible_objective = -mprob.get_objective_value() + else: + results.best_feasible_objective = mprob.get_objective_value() + except: + results.best_feasible_objective = None + try: + if self._objective.sense == maximize: + results.best_objective_bound = -mprob.get_final_LBD() + else: + results.best_objective_bound = mprob.get_final_LBD() + except: + if self._objective.sense == maximize: + results.best_objective_bound = math.inf + else: + results.best_objective_bound = -math.inf + + if results.best_feasible_objective is not None and not math.isfinite( + results.best_feasible_objective + ): + results.best_feasible_objective = None + + timer.start("load solution") + if config.load_solution: + if not results.best_feasible_objective is None: + if results.termination_condition != TerminationCondition.optimal: + logger.warning( + "Loading a feasible but suboptimal solution. " + "Please set load_solution=False and check " + "results.termination_condition and " + "results.found_feasible_solution() before loading a solution." + ) + self.load_vars() + else: + raise RuntimeError( + "A feasible solution was not found, so no solution can be loaded." + "Please set opt.config.load_solution=False and check " + "results.termination_condition and " + "results.best_feasible_objective before loading a solution." + ) + timer.stop("load solution") + + return results + + def load_vars(self, vars_to_load=None): + for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + def get_primals(self, vars_to_load=None): + if not self._mymaingo.get_status() in { + maingopy.GLOBALLY_OPTIMAL, + maingopy.FEASIBLE_POINT, + }: + raise RuntimeError( + "Solver does not currently have a valid solution." + "Please check the termination condition." + ) + + var_id_map = self._pyomo_var_to_solver_var_id_map + ref_vars = self._referenced_variables + if vars_to_load is None: + vars_to_load = var_id_map.keys() + else: + vars_to_load = [id(v) for v in vars_to_load] + + maingo_var_ids_to_load = [ + var_id_map[pyomo_var_id] for pyomo_var_id in vars_to_load + ] + + solution_point = self._mymaingo.get_solution_point() + vals = [solution_point[var_id] for var_id in maingo_var_ids_to_load] + + res = ComponentMap() + for var_id, val in zip(vars_to_load, vals): + using_cons, using_sos, using_obj = ref_vars[var_id] + if using_cons or using_sos or (using_obj is not None): + res[self._vars[var_id][0]] = val + return res + + def get_reduced_costs(self, vars_to_load=None): + raise ValueError("MAiNGO does not support returning Reduced Costs") + + def get_duals(self, cons_to_load=None): + raise ValueError("MAiNGO does not support returning Duals") From 0ea6a293df516acb7a02ba4ec56a51b51009cbc6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 10:25:06 -0700 Subject: [PATCH 0767/3044] Improve registration of new native types encountered by ExternalFunction --- pyomo/common/numeric_types.py | 108 +++++++++++++++++++++++++++------- pyomo/core/base/external.py | 14 +++-- 2 files changed, 95 insertions(+), 27 deletions(-) diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index ba104203667..f24b007b096 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -194,6 +194,53 @@ def RegisterLogicalType(new_type: type): nonpyomo_leaf_types.add(new_type) +def check_if_native_type(obj): + if isinstance(obj, (str, bytes)): + native_types.add(obj.__class__) + return True + if check_if_logical_type(obj): + return True + if check_if_numeric_type(obj): + return True + return False + + +def check_if_logical_type(obj): + """Test if the argument behaves like a logical type. + + We check for "numeric types" by checking if we can add zero to it + without changing the object's type, and that the object compares to + 0 in a meaningful way. If that works, then we register the type in + :py:attr:`native_numeric_types`. + + """ + obj_class = obj.__class__ + # Do not re-evaluate known native types + if obj_class in native_types: + return obj_class in native_logical_types + + if 'numpy' in obj_class.__module__: + # trigger the resolution of numpy_available and check if this + # type was automatically registered + bool(numpy_available) + if obj_class in native_types: + return obj_class in native_logical_types + + try: + if all(( + obj_class(1) == obj_class(2), + obj_class(False) != obj_class(True), + obj_class(False) ^ obj_class(True) == obj_class(True), + obj_class(False) | obj_class(True) == obj_class(True), + obj_class(False) & obj_class(True) == obj_class(False), + )): + RegisterLogicalType(obj_class) + return True + except: + pass + return False + + def check_if_numeric_type(obj): """Test if the argument behaves like a numeric type. @@ -218,36 +265,53 @@ def check_if_numeric_type(obj): try: obj_plus_0 = obj + 0 obj_p0_class = obj_plus_0.__class__ - # ensure that the object is comparable to 0 in a meaningful way - # (among other things, this prevents numpy.ndarray objects from - # being added to native_numeric_types) + # Native numeric types *must* be hashable + hash(obj) + except: + return False + if obj_p0_class is not obj_class and obj_p0_class not in native_numeric_types: + return False + # + # Check if the numeric type behaves like a complex type + # + try: + if 1.41 < abs(obj_class(1j+1)) < 1.42: + RegisterComplexType(obj_class) + return False + except: + pass + # + # ensure that the object is comparable to 0 in a meaningful way + # (among other things, this prevents numpy.ndarray objects from + # being added to native_numeric_types) + try: if not ((obj < 0) ^ (obj >= 0)): return False - # Native types *must* be hashable - hash(obj) except: return False - if obj_p0_class is obj_class or obj_p0_class in native_numeric_types: - # - # If we get here, this is a reasonably well-behaving - # numeric type: add it to the native numeric types - # so that future lookups will be faster. - # - RegisterNumericType(obj_class) - # - # Generate a warning, since Pyomo's management of third-party - # numeric types is more robust when registering explicitly. - # - logger.warning( - f"""Dynamically registering the following numeric type: + # + # If we get here, this is a reasonably well-behaving + # numeric type: add it to the native numeric types + # so that future lookups will be faster. + # + RegisterNumericType(obj_class) + try: + if obj_class(0.4) == obj_class(0): + RegisterIntegerType(obj_class) + except: + pass + # + # Generate a warning, since Pyomo's management of third-party + # numeric types is more robust when registering explicitly. + # + logger.warning( + f"""Dynamically registering the following numeric type: {obj_class.__module__}.{obj_class.__name__} Dynamic registration is supported for convenience, but there are known limitations to this approach. We recommend explicitly registering numeric types using RegisterNumericType() or RegisterIntegerType().""" - ) - return True - else: - return False + ) + return True def value(obj, exception=True): diff --git a/pyomo/core/base/external.py b/pyomo/core/base/external.py index 3c0038d745d..cae62d31941 100644 --- a/pyomo/core/base/external.py +++ b/pyomo/core/base/external.py @@ -31,13 +31,16 @@ from pyomo.common.autoslots import AutoSlots from pyomo.common.fileutils import find_library -from pyomo.core.expr.numvalue import ( +from pyomo.common.numeric_types import ( + check_if_native_type, native_types, native_numeric_types, pyomo_constant_types, + value, +) +from pyomo.core.expr.numvalue import ( NonNumericValue, NumericConstant, - value, ) import pyomo.core.expr as EXPR from pyomo.core.base.component import Component @@ -197,14 +200,15 @@ def __call__(self, *args): pv = False for i, arg in enumerate(args_): try: - # Q: Is there a better way to test if a value is an object - # not in native_types and not a standard expression type? if arg.__class__ in native_types: continue if arg.is_potentially_variable(): pv = True + continue except AttributeError: - args_[i] = NonNumericValue(arg) + if check_if_native_type(arg): + continue + args_[i] = NonNumericValue(arg) # if pv: return EXPR.ExternalFunctionExpression(args_, self) From bf15d23e3c46c1735d9ff4b3050e85947a9760a0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 10:26:06 -0700 Subject: [PATCH 0768/3044] Deprecate the pyomo_constant_types set --- pyomo/common/numeric_types.py | 21 ++++++++++----------- pyomo/core/base/external.py | 4 ++-- pyomo/core/base/units_container.py | 5 ++--- pyomo/core/expr/numvalue.py | 4 ++-- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index f24b007b096..0cfdd347484 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -50,7 +50,6 @@ native_integer_types = {int} native_logical_types = {bool} native_complex_types = {complex} -pyomo_constant_types = set() # includes NumericConstant _native_boolean_types = {int, bool, str, bytes} relocated_module_attribute( @@ -62,6 +61,16 @@ "be treated as if they were bool (as was the case for the other " "native_*_types sets). Users likely should use native_logical_types.", ) +_pyomo_constant_types = set() # includes NumericConstant, _PythonCallbackFunctionID +relocated_module_attribute( + 'pyomo_constant_types', + 'pyomo.common.numeric_types._pyomo_constant_types', + version='6.7.2.dev0', + msg="The pyomo_constant_types set will be removed in the future: the set " + "contained only NumericConstant and _PythonCallbackFunctionID, and provided " + "no meaningful value to clients or walkers. Users should likely handle " + "these types in the same manner as immutable Params.", +) #: Python set used to identify numeric constants and related native @@ -338,16 +347,6 @@ def value(obj, exception=True): """ if obj.__class__ in native_types: return obj - if obj.__class__ in pyomo_constant_types: - # - # I'm commenting this out for now, but I think we should never expect - # to see a numeric constant with value None. - # - # if exception and obj.value is None: - # raise ValueError( - # "No value for uninitialized NumericConstant object %s" - # % (obj.name,)) - return obj.value # # Test if we have a duck typed Pyomo expression # diff --git a/pyomo/core/base/external.py b/pyomo/core/base/external.py index cae62d31941..92e7286f3d4 100644 --- a/pyomo/core/base/external.py +++ b/pyomo/core/base/external.py @@ -35,8 +35,8 @@ check_if_native_type, native_types, native_numeric_types, - pyomo_constant_types, value, + _pyomo_constant_types, ) from pyomo.core.expr.numvalue import ( NonNumericValue, @@ -495,7 +495,7 @@ def is_constant(self): return False -pyomo_constant_types.add(_PythonCallbackFunctionID) +_pyomo_constant_types.add(_PythonCallbackFunctionID) class PythonCallbackFunction(ExternalFunction): diff --git a/pyomo/core/base/units_container.py b/pyomo/core/base/units_container.py index 1bf25ffdead..af8c77b25aa 100644 --- a/pyomo/core/base/units_container.py +++ b/pyomo/core/base/units_container.py @@ -119,7 +119,6 @@ value, native_types, native_numeric_types, - pyomo_constant_types, ) from pyomo.core.expr.template_expr import IndexTemplate from pyomo.core.expr.visitor import ExpressionValueVisitor @@ -902,7 +901,7 @@ def initializeWalker(self, expr): def beforeChild(self, node, child, child_idx): ctype = child.__class__ - if ctype in native_types or ctype in pyomo_constant_types: + if ctype in native_types: return False, self._pint_dimensionless if child.is_expression_type(): @@ -917,7 +916,7 @@ def beforeChild(self, node, child, child_idx): pint_unit = self._pyomo_units_container._get_pint_units(pyomo_unit) return False, pint_unit - return True, None + return False, self._pint_dimensionless def exitNode(self, node, data): """Visitor callback when moving up the expression tree. diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index 3a4359af2f9..95914248bc7 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -28,7 +28,7 @@ native_numeric_types, native_integer_types, native_logical_types, - pyomo_constant_types, + _pyomo_constant_types, check_if_numeric_type, value, ) @@ -410,7 +410,7 @@ def pprint(self, ostream=None, verbose=False): ostream.write(str(self)) -pyomo_constant_types.add(NumericConstant) +_pyomo_constant_types.add(NumericConstant) # We use as_numeric() so that the constant is also in the cache ZeroConstant = as_numeric(0) From 5373c333746e219c0fdc3b202d73bac55f449da3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 10:27:31 -0700 Subject: [PATCH 0769/3044] Improve efficiency of value() --- pyomo/common/numeric_types.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index 0cfdd347484..a234bb4df05 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -350,9 +350,7 @@ def value(obj, exception=True): # # Test if we have a duck typed Pyomo expression # - try: - obj.is_numeric_type() - except AttributeError: + if not hasattr(obj, 'is_numeric_type'): # # TODO: Historically we checked for new *numeric* types and # raised exceptions for anything else. That is inconsistent @@ -367,7 +365,7 @@ def value(obj, exception=True): return None raise TypeError( "Cannot evaluate object with unknown type: %s" % obj.__class__.__name__ - ) from None + ) # # Evaluate the expression object # From 6830e2787aa49fd48b29a0cb7316a4f7f2aa1841 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 10:28:00 -0700 Subject: [PATCH 0770/3044] Add NonNumericValue to teh PyomoObject hierarchy, define __call__ --- pyomo/core/expr/numvalue.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index 95914248bc7..64ef0a6cca2 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -85,7 +85,7 @@ ##------------------------------------------------------------------------ -class NonNumericValue(object): +class NonNumericValue(PyomoObject): """An object that contains a non-numeric value Constructor Arguments: @@ -100,6 +100,8 @@ def __init__(self, value): def __str__(self): return str(self.value) + def __call__(self, exception=None): + return self.value nonpyomo_leaf_types.add(NonNumericValue) From 10281708b1efef82751d1f53b8a6ae645321a0ac Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 10:28:48 -0700 Subject: [PATCH 0771/3044] NFC: apply black --- pyomo/common/numeric_types.py | 18 ++++++++++-------- pyomo/core/base/external.py | 5 +---- pyomo/core/expr/numvalue.py | 1 + 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index a234bb4df05..412a1bbeade 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -236,13 +236,15 @@ def check_if_logical_type(obj): return obj_class in native_logical_types try: - if all(( - obj_class(1) == obj_class(2), - obj_class(False) != obj_class(True), - obj_class(False) ^ obj_class(True) == obj_class(True), - obj_class(False) | obj_class(True) == obj_class(True), - obj_class(False) & obj_class(True) == obj_class(False), - )): + if all( + ( + obj_class(1) == obj_class(2), + obj_class(False) != obj_class(True), + obj_class(False) ^ obj_class(True) == obj_class(True), + obj_class(False) | obj_class(True) == obj_class(True), + obj_class(False) & obj_class(True) == obj_class(False), + ) + ): RegisterLogicalType(obj_class) return True except: @@ -284,7 +286,7 @@ def check_if_numeric_type(obj): # Check if the numeric type behaves like a complex type # try: - if 1.41 < abs(obj_class(1j+1)) < 1.42: + if 1.41 < abs(obj_class(1j + 1)) < 1.42: RegisterComplexType(obj_class) return False except: diff --git a/pyomo/core/base/external.py b/pyomo/core/base/external.py index 92e7286f3d4..0fda004b664 100644 --- a/pyomo/core/base/external.py +++ b/pyomo/core/base/external.py @@ -38,10 +38,7 @@ value, _pyomo_constant_types, ) -from pyomo.core.expr.numvalue import ( - NonNumericValue, - NumericConstant, -) +from pyomo.core.expr.numvalue import NonNumericValue, NumericConstant import pyomo.core.expr as EXPR from pyomo.core.base.component import Component from pyomo.core.base.units_container import units diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index 64ef0a6cca2..b656eea1bcd 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -103,6 +103,7 @@ def __str__(self): def __call__(self, exception=None): return self.value + nonpyomo_leaf_types.add(NonNumericValue) From 28cf0695f7fc9140f20dcb846c9deff8897cffe1 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 26 Feb 2024 12:32:01 -0700 Subject: [PATCH 0772/3044] timing calls and some performance improvements --- .../contrib/incidence_analysis/scc_solver.py | 97 ++++++++++++++++--- 1 file changed, 85 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 835e07c7c02..f6697b11567 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -13,18 +13,27 @@ from pyomo.core.base.constraint import Constraint from pyomo.util.calc_var_value import calculate_variable_from_constraint -from pyomo.util.subsystems import TemporarySubsystemManager, generate_subsystem_blocks +from pyomo.util.subsystems import ( + TemporarySubsystemManager, + generate_subsystem_blocks, + create_subsystem_block, +) from pyomo.contrib.incidence_analysis.interface import ( IncidenceGraphInterface, _generate_variables_in_constraints, ) +from pyomo.contrib.incidence_analysis.config import IncidenceMethod _log = logging.getLogger(__name__) +from pyomo.common.timing import HierarchicalTimer def generate_strongly_connected_components( - constraints, variables=None, include_fixed=False + constraints, + variables=None, + include_fixed=False, + timer=None, ): """Yield in order ``_BlockData`` that each contain the variables and constraints of a single diagonal block in a block lower triangularization @@ -53,27 +62,52 @@ def generate_strongly_connected_components( "input variables" for that block. """ - if variables is None: - variables = list( - _generate_variables_in_constraints(constraints, include_fixed=include_fixed) - ) + if timer is None: + timer = HierarchicalTimer() + + if isinstance(constraints, IncidenceGraphInterface): + igraph = constraints + variables = igraph.variables + constraints = igraph.constraints + else: + if variables is None: + timer.start("generate-variables") + variables = list( + _generate_variables_in_constraints(constraints, include_fixed=include_fixed) + ) + timer.stop("generate-variables") + timer.start("igraph") + igraph = IncidenceGraphInterface() + timer.stop("igraph") assert len(variables) == len(constraints) - igraph = IncidenceGraphInterface() + + timer.start("block-triang") var_blocks, con_blocks = igraph.block_triangularize( variables=variables, constraints=constraints ) + timer.stop("block-triang") subsets = [(cblock, vblock) for vblock, cblock in zip(var_blocks, con_blocks)] + timer.start("generate-block") for block, inputs in generate_subsystem_blocks( subsets, include_fixed=include_fixed ): + timer.stop("generate-block") # TODO: How does len scale for reference-to-list? assert len(block.vars) == len(block.cons) yield (block, inputs) + # Note that this code, after the last yield, I believe is only called + # at time of GC. + timer.start("generate-block") + timer.stop("generate-block") def solve_strongly_connected_components( - block, solver=None, solve_kwds=None, calc_var_kwds=None + block, + solver=None, + solve_kwds=None, + calc_var_kwds=None, + timer=None, ): """Solve a square system of variables and equality constraints by solving strongly connected components individually. @@ -110,24 +144,59 @@ def solve_strongly_connected_components( solve_kwds = {} if calc_var_kwds is None: calc_var_kwds = {} + if timer is None: + timer = HierarchicalTimer() + timer.start("igraph") igraph = IncidenceGraphInterface( - block, active=True, include_fixed=False, include_inequality=False + block, + active=True, + include_fixed=False, + include_inequality=False, + method=IncidenceMethod.ampl_repn, ) + timer.stop("igraph") + # Use IncidenceGraphInterface to get the constraints and variables constraints = igraph.constraints variables = igraph.variables + timer.start("block-triang") + var_blocks, con_blocks = igraph.block_triangularize() + timer.stop("block-triang") + timer.start("subsystem-blocks") + subsystem_blocks = [ + create_subsystem_block(conbl, varbl, timer=timer) if len(varbl) > 1 else None + for varbl, conbl in zip(var_blocks, con_blocks) + ] + timer.stop("subsystem-blocks") + res_list = [] log_blocks = _log.isEnabledFor(logging.DEBUG) - for scc, inputs in generate_strongly_connected_components(constraints, variables): + + #timer.start("generate-scc") + #for scc, inputs in generate_strongly_connected_components(igraph, timer=timer): + # timer.stop("generate-scc") + for i, scc in enumerate(subsystem_blocks): + if scc is None: + # Since a block is not necessary for 1x1 solve, we use the convention + # that None indicates a 1x1 SCC. + inputs = [] + var = var_blocks[i][0] + con = con_blocks[i][0] + else: + inputs = list(scc.input_vars.values()) + with TemporarySubsystemManager(to_fix=inputs): - N = len(scc.vars) + N = len(var_blocks[i]) if N == 1: if log_blocks: _log.debug(f"Solving 1x1 block: {scc.cons[0].name}.") + timer.start("calc-var") results = calculate_variable_from_constraint( - scc.vars[0], scc.cons[0], **calc_var_kwds + #scc.vars[0], scc.cons[0], **calc_var_kwds + var, con, **calc_var_kwds ) + timer.stop("calc-var") res_list.append(results) else: if solver is None: @@ -141,6 +210,10 @@ def solve_strongly_connected_components( ) if log_blocks: _log.debug(f"Solving {N}x{N} block.") + timer.start("solve") results = solver.solve(scc, **solve_kwds) + timer.stop("solve") res_list.append(results) + # timer.start("generate-scc") + #timer.stop("generate-scc") return res_list From 2b47212ab8c5db29033e4ffd8b7cd2edb86272cb Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 26 Feb 2024 12:32:23 -0700 Subject: [PATCH 0773/3044] dont repeat work for the same named expression in ExternalFunctionVisitor --- pyomo/util/subsystems.py | 78 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 70a0af1b2a7..43246da37a4 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -20,15 +20,32 @@ from pyomo.core.base.external import ExternalFunction from pyomo.core.expr.visitor import StreamBasedExpressionVisitor from pyomo.core.expr.numeric_expr import ExternalFunctionExpression -from pyomo.core.expr.numvalue import native_types +from pyomo.core.expr.numvalue import native_types, NumericValue class _ExternalFunctionVisitor(StreamBasedExpressionVisitor): + + def __init__(self, descend_into_named_expressions=True): + super().__init__() + self._descend_into_named_expressions = descend_into_named_expressions + self.named_expressions = [] + def initializeWalker(self, expr): self._functions = [] self._seen = set() return True, None + def beforeChild(self, parent, child, index): + if ( + not self._descend_into_named_expressions + and isinstance(child, NumericValue) + and child.is_named_expression_type() + ): + self.named_expressions.append(child) + return False, None + else: + return True, None + def exitNode(self, node, data): if type(node) is ExternalFunctionExpression: if id(node) not in self._seen: @@ -50,14 +67,47 @@ def acceptChildResult(self, node, data, child_result, child_idx): return child_result.is_expression_type(), None -def identify_external_functions(expr): - yield from _ExternalFunctionVisitor().walk_expression(expr) +def identify_external_functions( + expr, + descend_into_named_expressions=True, + named_expressions=None, +): + visitor = _ExternalFunctionVisitor( + descend_into_named_expressions=descend_into_named_expressions + ) + efs = list(visitor.walk_expression(expr)) + if not descend_into_named_expressions and named_expressions is not None: + named_expressions.extend(visitor.named_expressions) + return efs + #yield from _ExternalFunctionVisitor().walk_expression(expr) def add_local_external_functions(block): ef_exprs = [] + named_expressions = [] for comp in block.component_data_objects((Constraint, Expression), active=True): - ef_exprs.extend(identify_external_functions(comp.expr)) + ef_exprs.extend(identify_external_functions( + comp.expr, + descend_into_named_expressions=False, + named_expressions=named_expressions, + )) + named_expr_set = ComponentSet(named_expressions) + named_expressions = list(named_expr_set) + while named_expressions: + expr = named_expressions.pop() + local_named_exprs = [] + ef_exprs.extend(identify_external_functions( + expr, + descend_into_named_expressions=False, + named_expressions=local_named_exprs, + )) + # Only add to the stack named expressions that we have + # not encountered yet. + for local_expr in local_named_exprs: + if local_expr not in named_expr_set: + named_expressions.append(local_expr) + named_expr_set.add(local_expr) + unique_functions = [] fcn_set = set() for expr in ef_exprs: @@ -75,7 +125,13 @@ def add_local_external_functions(block): return fcn_comp_map -def create_subsystem_block(constraints, variables=None, include_fixed=False): +from pyomo.common.timing import HierarchicalTimer +def create_subsystem_block( + constraints, + variables=None, + include_fixed=False, + timer=None, +): """This function creates a block to serve as a subsystem with the specified variables and constraints. To satisfy certain writers, other variables that appear in the constraints must be added to the block as @@ -99,20 +155,32 @@ def create_subsystem_block(constraints, variables=None, include_fixed=False): as well as other variables present in the constraints """ + if timer is None: + timer = HierarchicalTimer() if variables is None: variables = [] + timer.start("block") block = Block(concrete=True) + timer.stop("block") + timer.start("reference") block.vars = Reference(variables) block.cons = Reference(constraints) + timer.stop("reference") var_set = ComponentSet(variables) input_vars = [] + timer.start("identify-vars") for con in constraints: for var in identify_variables(con.expr, include_fixed=include_fixed): if var not in var_set: input_vars.append(var) var_set.add(var) + timer.stop("identify-vars") + timer.start("reference") block.input_vars = Reference(input_vars) + timer.stop("reference") + timer.start("external-fcns") add_local_external_functions(block) + timer.stop("external-fcns") return block From 8643a00674ab125b432e0d1f40c6f60e3d7ef520 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Mon, 26 Feb 2024 14:38:01 -0500 Subject: [PATCH 0774/3044] add more flexible external variable definition in LDSDA --- pyomo/contrib/gdpopt/config_options.py | 10 ++ pyomo/contrib/gdpopt/ldsda.py | 125 ++++++++++-------- .../plugins/detect_fixed_vars.py | 6 +- 3 files changed, 84 insertions(+), 57 deletions(-) diff --git a/pyomo/contrib/gdpopt/config_options.py b/pyomo/contrib/gdpopt/config_options.py index ff5f17e2278..136baaa8e9c 100644 --- a/pyomo/contrib/gdpopt/config_options.py +++ b/pyomo/contrib/gdpopt/config_options.py @@ -554,3 +554,13 @@ def _add_ldsda_configs(CONFIG): TODO: Maybe we can find a better design for this.""", ), ) + CONFIG.declare( + "disjunction_list", + ConfigValue( + default=None, + description=""" + The list of disjunctions to be reformulated into external variables. + The disjunctions should be in the same order of provided starting point. + """, + ), + ) diff --git a/pyomo/contrib/gdpopt/ldsda.py b/pyomo/contrib/gdpopt/ldsda.py index b878c6078b0..c5987dde574 100644 --- a/pyomo/contrib/gdpopt/ldsda.py +++ b/pyomo/contrib/gdpopt/ldsda.py @@ -48,8 +48,6 @@ [ 'exactly_number', # number of external variables for this type 'Boolean_vars', # list with names of the ordered Boolean variables to be reformulated - 'Disjuncts', # list of disjuncts that are associated with the external variables - 'LogicExpression', # Logic expression that defines the external variables 'UB', # upper bound on external variable 'LB', # lower bound on external variable ], @@ -127,11 +125,7 @@ def _solve_gdp(self, model, config): self.working_model_util_block = self.working_model.component(util_block.name) add_disjunction_list(self.working_model_util_block) - # TODO: do we need to apply logical_to_disjunctive here? - # This is applied in LBB. - # root_node = TransformationFactory( - # 'contrib.logical_to_disjunctive' - # ).create_using(model) + TransformationFactory('core.logical_to_linear').apply_to(self.working_model) # Now that logical_to_disjunctive has been called. add_transformed_boolean_variable_list(self.working_model_util_block) self._get_external_information(self.working_model_util_block, config) @@ -180,22 +174,23 @@ def _solve_GDP_subproblem(self, external_var_value, search_type, config): self.fix_disjunctions_with_external_var(external_var_value) subproblem = self.working_model.clone() TransformationFactory('core.logical_to_linear').apply_to(subproblem) - TransformationFactory('gdp.bigm').apply_to(subproblem) try: with SuppressInfeasibleWarning(): - # TODO: we can use fbbt or deactivate trivial constraints here. - # try: - # fbbt(subproblem, integer_tol=config.integer_tolerance) - # except InfeasibleConstraintException: - # # copy variable values, even if errored - # copy_var_list_values( - # from_list=subprob_utils.algebraic_variable_list, - # to_list=model_utils.algebraic_variable_list, - # config=config, - # ignore_integrality=True, - # ) - # return float('inf'), float('inf') + try: + fbbt(subproblem, integer_tol=config.integer_tolerance) + TransformationFactory('contrib.detect_fixed_vars').apply_to( + subproblem + ) + TransformationFactory('contrib.propagate_fixed_vars').apply_to( + subproblem + ) + TransformationFactory( + 'contrib.deactivate_trivial_constraints' + ).apply_to(subproblem, tmp=False, ignore_infeasible=False) + TransformationFactory('gdp.bigm').apply_to(subproblem) + except InfeasibleConstraintException: + return False minlp_args = dict(config.minlp_solver_args) if config.time_limit is not None and config.minlp_solver == 'gams': elapsed = get_main_elapsed_time(self.timing) @@ -239,34 +234,53 @@ def _get_external_information(self, util_block, config): # However, we cannot link the starting point and the logical constraint. # for c in util_block.logical_constraint_list: # if isinstance(c.body, ExactlyExpression): - for constraint_name in config.logical_constraint_list: - # TODO: in the first version, we don't support more than one exactly constraint. - # TODO: if we use component instead of model.find_component, it will fail. - c = model.find_component(constraint_name) - exactly_number = c.body.args[0] - if exactly_number > 1: - raise ValueError("The function only works for exactly_number = 1") - sorted_boolean_var_list = sorted(c.body.args[1:], key=lambda x: x.index()) - util_block.external_var_info_list.append( - ExternalVarInfo( - exactly_number=1, - Boolean_vars=sorted_boolean_var_list, - Disjuncts=[ - boolean_var.get_associated_binary().parent_block() - for boolean_var in sorted_boolean_var_list - ], - LogicExpression=c.body, - UB=len(sorted_boolean_var_list), - LB=1, + if config.logical_constraint_list is not None: + for constraint_name in config.logical_constraint_list: + # TODO: in the first version, we don't support more than one exactly constraint. + # TODO: if we use component instead of model.find_component, it will fail. + c = model.find_component(constraint_name) + exactly_number = c.body.args[0] + if exactly_number > 1: + raise ValueError("The function only works for exactly_number = 1") + sorted_boolean_var_list = c.body.args[1:] + util_block.external_var_info_list.append( + ExternalVarInfo( + exactly_number=1, + Boolean_vars=sorted_boolean_var_list, + UB=len(sorted_boolean_var_list), + LB=1, + ) ) - ) - reformulation_summary.append( - [ - 1, - len(sorted_boolean_var_list), - [boolean_var.name for boolean_var in sorted_boolean_var_list], + reformulation_summary.append( + [ + 1, + len(sorted_boolean_var_list), + [boolean_var.name for boolean_var in sorted_boolean_var_list], + ] + ) + if config.disjunction_list is not None: + for disjunction_name in config.disjunction_list: + # TODO: in the first version, we don't support more than one exactly constraint. + # TODO: if we use component instead of model.find_component, it will fail. + disjunction = model.find_component(disjunction_name) + sorted_boolean_var_list = [ + disjunct.indicator_var for disjunct in disjunction.disjuncts ] - ) + util_block.external_var_info_list.append( + ExternalVarInfo( + exactly_number=1, + Boolean_vars=sorted_boolean_var_list, + UB=len(sorted_boolean_var_list), + LB=1, + ) + ) + reformulation_summary.append( + [ + 1, + len(sorted_boolean_var_list), + [boolean_var.name for boolean_var in sorted_boolean_var_list], + ] + ) config.logger.info("Reformulation Summary:") config.logger.info( tabulate.tabulate( @@ -280,6 +294,10 @@ def _get_external_information(self, util_block, config): external_var_info.exactly_number for external_var_info in util_block.external_var_info_list ) + if self.number_of_external_variables != len(config.starting_point): + raise ValueError( + "The length of the provided starting point doesn't equal to the number of disjunctions." + ) def fix_disjunctions_with_external_var(self, external_var_values_list): """Function that fixes the disjunctions in the working_model using the values of the external variables. @@ -293,20 +311,15 @@ def fix_disjunctions_with_external_var(self, external_var_values_list): external_var_values_list, self.working_model_util_block.external_var_info_list, ): - for idx, (boolean_var, disjunct) in enumerate( - zip(external_var_info.Boolean_vars, external_var_info.Disjuncts) - ): + for idx, boolean_var in enumerate(external_var_info.Boolean_vars): if idx == external_variable_value - 1: - disjunct.activate() boolean_var.fix(True) - disjunct.indicator_var.fix(True) - disjunct.binary_indicator_var.fix(1) + if boolean_var.get_associated_binary() is not None: + boolean_var.get_associated_binary().fix(1) else: - # TODO: maybe we can simplify this. boolean_var.fix(False) - disjunct.indicator_var.fix(False) - disjunct.binary_indicator_var.fix(0) - disjunct.deactivate() + if boolean_var.get_associated_binary() is not None: + boolean_var.get_associated_binary().fix(0) self.explored_point_set.add(tuple(external_var_values_list)) def _get_directions(self, dimension, config): diff --git a/pyomo/contrib/preprocessing/plugins/detect_fixed_vars.py b/pyomo/contrib/preprocessing/plugins/detect_fixed_vars.py index bafbec7b8bd..0832b4b1515 100644 --- a/pyomo/contrib/preprocessing/plugins/detect_fixed_vars.py +++ b/pyomo/contrib/preprocessing/plugins/detect_fixed_vars.py @@ -23,6 +23,8 @@ from pyomo.core.base.var import Var from pyomo.core.expr.numvalue import value from pyomo.core.plugins.transform.hierarchy import IsomorphicTransformation +from pyomo.core.base.block import Block +from pyomo.gdp import Disjunct @TransformationFactory.register( @@ -67,7 +69,9 @@ def _apply_to(self, instance, **kwargs): if config.tmp: instance._xfrm_detect_fixed_vars_old_values = ComponentMap() - for var in instance.component_data_objects(ctype=Var, descend_into=True): + for var in instance.component_data_objects( + ctype=Var, descend_into=[Block, Disjunct] + ): if var.fixed or var.lb is None or var.ub is None: # if the variable is already fixed, or if it is missing a # bound, we skip it. From 7aaaeed7f6d051a79a06435a4e0c4a0ad3f8a8bc Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 26 Feb 2024 14:19:19 -0700 Subject: [PATCH 0775/3044] scc_solver implementation using SccImplicitFunctionSolver --- .../contrib/incidence_analysis/scc_solver.py | 167 +++++++++++------- 1 file changed, 100 insertions(+), 67 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index f6697b11567..5d1e2e23f2d 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -147,73 +147,106 @@ def solve_strongly_connected_components( if timer is None: timer = HierarchicalTimer() - timer.start("igraph") - igraph = IncidenceGraphInterface( - block, - active=True, - include_fixed=False, - include_inequality=False, - method=IncidenceMethod.ampl_repn, - ) - timer.stop("igraph") - # Use IncidenceGraphInterface to get the constraints and variables - constraints = igraph.constraints - variables = igraph.variables + USE_IMPLICIT = True + if not USE_IMPLICIT: + timer.start("igraph") + igraph = IncidenceGraphInterface( + block, + active=True, + include_fixed=False, + include_inequality=False, + method=IncidenceMethod.ampl_repn, + ) + timer.stop("igraph") + # Use IncidenceGraphInterface to get the constraints and variables + constraints = igraph.constraints + variables = igraph.variables - timer.start("block-triang") - var_blocks, con_blocks = igraph.block_triangularize() - timer.stop("block-triang") - timer.start("subsystem-blocks") - subsystem_blocks = [ - create_subsystem_block(conbl, varbl, timer=timer) if len(varbl) > 1 else None - for varbl, conbl in zip(var_blocks, con_blocks) - ] - timer.stop("subsystem-blocks") - - res_list = [] - log_blocks = _log.isEnabledFor(logging.DEBUG) - - #timer.start("generate-scc") - #for scc, inputs in generate_strongly_connected_components(igraph, timer=timer): - # timer.stop("generate-scc") - for i, scc in enumerate(subsystem_blocks): - if scc is None: - # Since a block is not necessary for 1x1 solve, we use the convention - # that None indicates a 1x1 SCC. - inputs = [] - var = var_blocks[i][0] - con = con_blocks[i][0] - else: - inputs = list(scc.input_vars.values()) - - with TemporarySubsystemManager(to_fix=inputs): - N = len(var_blocks[i]) - if N == 1: - if log_blocks: - _log.debug(f"Solving 1x1 block: {scc.cons[0].name}.") - timer.start("calc-var") - results = calculate_variable_from_constraint( - #scc.vars[0], scc.cons[0], **calc_var_kwds - var, con, **calc_var_kwds - ) - timer.stop("calc-var") - res_list.append(results) + timer.start("block-triang") + var_blocks, con_blocks = igraph.block_triangularize() + timer.stop("block-triang") + timer.start("subsystem-blocks") + subsystem_blocks = [ + create_subsystem_block(conbl, varbl, timer=timer) if len(varbl) > 1 else None + for varbl, conbl in zip(var_blocks, con_blocks) + ] + timer.stop("subsystem-blocks") + + res_list = [] + log_blocks = _log.isEnabledFor(logging.DEBUG) + + #timer.start("generate-scc") + #for scc, inputs in generate_strongly_connected_components(igraph, timer=timer): + # timer.stop("generate-scc") + for i, scc in enumerate(subsystem_blocks): + if scc is None: + # Since a block is not necessary for 1x1 solve, we use the convention + # that None indicates a 1x1 SCC. + inputs = [] + var = var_blocks[i][0] + con = con_blocks[i][0] else: - if solver is None: - var_names = [var.name for var in scc.vars.values()][:10] - con_names = [con.name for con in scc.cons.values()][:10] - 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 of size %sx%s including" - " components:\n%s\n%s" % (N, N, var_names, con_names) + inputs = list(scc.input_vars.values()) + + with TemporarySubsystemManager(to_fix=inputs): + N = len(var_blocks[i]) + if N == 1: + if log_blocks: + _log.debug(f"Solving 1x1 block: {scc.cons[0].name}.") + timer.start("calc-var") + results = calculate_variable_from_constraint( + #scc.vars[0], scc.cons[0], **calc_var_kwds + var, con, **calc_var_kwds ) - if log_blocks: - _log.debug(f"Solving {N}x{N} block.") - timer.start("solve") - results = solver.solve(scc, **solve_kwds) - timer.stop("solve") - res_list.append(results) - # timer.start("generate-scc") - #timer.stop("generate-scc") - return res_list + timer.stop("calc-var") + res_list.append(results) + else: + if solver is None: + var_names = [var.name for var in scc.vars.values()][:10] + con_names = [con.name for con in scc.cons.values()][:10] + 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 of size %sx%s including" + " components:\n%s\n%s" % (N, N, var_names, con_names) + ) + if log_blocks: + _log.debug(f"Solving {N}x{N} block.") + timer.start("solve") + results = solver.solve(scc, **solve_kwds) + timer.stop("solve") + res_list.append(results) + # timer.start("generate-scc") + #timer.stop("generate-scc") + return res_list + else: + from pyomo.contrib.pynumero.algorithms.solvers.implicit_functions import ( + SccImplicitFunctionSolver, + ScipySolverWrapper, + ) + timer.start("igraph") + igraph = IncidenceGraphInterface( + block, + active=True, + include_fixed=False, + include_inequality=False, + method=IncidenceMethod.ampl_repn, + ) + timer.stop("igraph") + # Use IncidenceGraphInterface to get the constraints and variables + constraints = igraph.constraints + variables = igraph.variables + + # Construct an implicit function solver with no parameters. (This is just + # a square system solver.) + scc_solver = SccImplicitFunctionSolver( + variables, + constraints, + [], + solver_class=ScipySolverWrapper, + timer=timer, + use_calc_var=False, + ) + # set_parameters triggers the Newton solve. + scc_solver.set_parameters([]) + scc_solver.update_pyomo_model() From aaecb45022782bfcb00e60f29fb8895139c126fc Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 26 Feb 2024 14:19:34 -0700 Subject: [PATCH 0776/3044] additional timing calls in SccImplicitFunctionSolver --- .../algorithms/solvers/implicit_functions.py | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py b/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py index e40580c1161..17b895507f2 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/implicit_functions.py @@ -32,10 +32,8 @@ NewtonNlpSolver, SecantNewtonNlpSolver, ) +from pyomo.contrib.incidence_analysis.config import IncidenceMethod from pyomo.contrib.incidence_analysis import IncidenceGraphInterface -from pyomo.contrib.incidence_analysis.scc_solver import ( - generate_strongly_connected_components, -) class NlpSolverBase(object): @@ -133,7 +131,7 @@ class PyomoImplicitFunctionBase(object): """ - def __init__(self, variables, constraints, parameters): + def __init__(self, variables, constraints, parameters, timer=None): """ Arguments --------- @@ -145,11 +143,13 @@ def __init__(self, variables, constraints, parameters): Variables to be treated as inputs to the implicit function """ + if timer is None: + timer = HierarchicalTimer() self._variables = variables self._constraints = constraints self._parameters = parameters self._block_variables = variables + parameters - self._block = create_subsystem_block(constraints, self._block_variables) + self._block = create_subsystem_block(constraints, self._block_variables, timer=timer) def get_variables(self): return self._variables @@ -361,7 +361,9 @@ def __init__( self._solver_options = solver_options self._calc_var_cutoff = 1 if use_calc_var else 0 # NOTE: This super call is only necessary so the get_* methods work - super().__init__(variables, constraints, parameters) + timer.start("super.__init__") + super().__init__(variables, constraints, parameters, timer=timer) + timer.stop("super.__init__") subsystem_list = [ # Switch order in list for compatibility with generate_subsystem_blocks @@ -376,6 +378,7 @@ def __init__( # an equality constraint. constants = [] constant_set = ComponentSet() + timer.start("identify-vars") for con in constraints: for var in identify_variables(con.expr, include_fixed=False): if var not in constant_set and var not in var_param_set: @@ -383,6 +386,7 @@ def __init__( # a var nor param, treat it as a "constant" constant_set.add(var) constants.append(var) + timer.stop("identify-vars") with TemporarySubsystemManager(to_fix=constants): # Temporarily fix "constant" variables so (a) they don't show @@ -390,6 +394,7 @@ def __init__( # they don't appear as additional columns in the NLPs and # ProjectedNLPs. + timer.start("subsystem-blocks") self._subsystem_list = list(generate_subsystem_blocks(subsystem_list)) # These are subsystems that need an external solver, rather than # calculate_variable_from_constraint. _calc_var_cutoff should be either @@ -399,6 +404,7 @@ def __init__( for block, inputs in self._subsystem_list if len(block.vars) > self._calc_var_cutoff ] + timer.stop("subsystem-blocks") # Need a dummy objective to create an NLP for block, inputs in self._solver_subsystem_list: @@ -421,16 +427,20 @@ def __init__( # "Output variable" names are required to construct ProjectedNLPs. # Ideally, we can eventually replace these with variable indices. + timer.start("names") self._solver_subsystem_var_names = [ [var.name for var in block.vars.values()] for block, inputs in self._solver_subsystem_list ] + timer.stop("names") + timer.start("proj-ext-nlp") self._solver_proj_nlps = [ nlp_proj.ProjectedExtendedNLP(nlp, names) for nlp, names in zip( self._solver_subsystem_nlps, self._solver_subsystem_var_names ) ] + timer.stop("proj-ext-nlp") # We will solve the ProjectedNLPs rather than the original NLPs self._timer.start("NlpSolver") @@ -439,6 +449,7 @@ def __init__( for nlp in self._solver_proj_nlps ] self._timer.stop("NlpSolver") + timer.start("input-indices") self._solver_subsystem_input_coords = [ # Coordinates in the NLP, not ProjectedNLP nlp.get_primal_indices(inputs) @@ -446,6 +457,7 @@ def __init__( self._solver_subsystem_nlps, self._solver_subsystem_list ) ] + timer.stop("input-indices") self._n_variables = len(variables) self._n_constraints = len(constraints) @@ -465,6 +477,7 @@ def __init__( ) # Cache the global array-coordinates of each subset of "input" # variables. These are used for updating before each solve. + timer.start("coord-maps") self._local_input_global_coords = [ # If I do not fix "constants" above, I get errors here # that only show up in the CLC models. @@ -481,6 +494,7 @@ def __init__( ) for (block, _) in self._solver_subsystem_list ] + timer.stop("coord-maps") self._timer.stop("__init__") @@ -612,7 +626,7 @@ def update_pyomo_model(self): class SccImplicitFunctionSolver(DecomposedImplicitFunctionBase): def partition_system(self, variables, constraints): self._timer.start("partition") - igraph = IncidenceGraphInterface() + igraph = IncidenceGraphInterface(method=IncidenceMethod.ampl_repn) var_blocks, con_blocks = igraph.block_triangularize(variables, constraints) self._timer.stop("partition") return zip(var_blocks, con_blocks) From e0312525ed13dfd1e437bf65d9892f2975f3bf03 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 15:22:42 -0700 Subject: [PATCH 0777/3044] Add hooks for automatic registration on external TPL module import --- pyomo/common/dependencies.py | 87 ++++++++++++++++++++++++++++++++++- pyomo/common/numeric_types.py | 8 ---- 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 9e96fdd5860..9aa6c4c4f7a 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -9,13 +9,16 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from collections.abc import Mapping import inspect import importlib import logging import sys import warnings +from collections.abc import Mapping +from types import ModuleType +from typing import List + from .deprecation import deprecated, deprecation_warning, in_testing_environment from .errors import DeferredImportError @@ -312,6 +315,12 @@ def __init__( self._module = None self._available = None self._deferred_submodules = deferred_submodules + # If this import has a callback, then record this deferred + # import so that any direct imports of this module also trigger + # the resolution of this DeferredImportIndicator (and the + # corresponding callback) + if callback is not None: + DeferredImportCallbackFinder._callbacks.setdefault(name, []).append(self) def __bool__(self): self.resolve() @@ -433,6 +442,82 @@ def check_min_version(module, min_version): check_min_version._parser = None +# +# Note that we are duck-typing the Loader and MetaPathFinder base +# classes from importlib.abc. This avoids a (surprisingly costly) +# import of importlib.abc +# +class DeferredImportCallbackLoader: + """Custom Loader to resolve registered :py:class:`DeferredImportIndicator` objects + + This :py:class:`importlib.abc.Loader` loader wraps a regular loader + and automatically resolves the registered + :py:class:`DeferredImportIndicator` objects after the module is + loaded. + + """ + + def __init__(self, loader, deferred_indicators: List[DeferredImportIndicator]): + self._loader = loader + self._deferred_indicators = deferred_indicators + + def module_repr(self, module: ModuleType) -> str: + return self._loader.module_repr(module) + + def create_module(self, spec) -> ModuleType: + return self._loader.create_module(spec) + + def exec_module(self, module: ModuleType) -> None: + self._loader.exec_module(module) + # Now that the module has been loaded, trigger the resolution of + # the deferred indicators (and their associated callbacks) + for deferred in self._deferred_indicators: + deferred.resolve() + + def load_module(self, fullname) -> ModuleType: + return self._loader.load_module(fullname) + + +class DeferredImportCallbackFinder: + """Custom Finder that will wrap the normal loader to trigger callbacks + + This :py:class:`importlib.abc.MetaPathFinder` finder will wrap the + normal loader returned by ``PathFinder`` with a loader that will + trigger custom callbacks after the module is loaded. We use this to + trigger the post import callbacks registered through + :py:fcn:`attempt_import` even when a user imports the target library + directly (and not through attribute access on the + :py:class:`DeferredImportModule`. + + """ + _callbacks = {} + + def find_spec(self, fullname, path, target=None): + if fullname not in self._callbacks: + return None + + spec = importlib.machinery.PathFinder.find_spec(fullname, path, target) + if spec is None: + # Module not found. Returning None will proceed to the next + # finder (which is likely to raise a ModuleNotFoundError) + return None + spec.loader = DeferredImportCallbackLoader( + spec.loader, self._callbacks[fullname] + ) + return spec + + def invalidate_caches(self): + pass + + +_DeferredImportCallbackFinder = DeferredImportCallbackFinder() +# Insert the DeferredImportCallbackFinder at the beginning of the +# mata_path to that it is found before the standard finders (so that we +# can correctly inject the resolution of the DeferredImportIndicators -- +# which triggers the needed callbacks) +sys.meta_path.insert(0, _DeferredImportCallbackFinder) + + def attempt_import( name, error_message=None, diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index ba104203667..ca2ce0f9c6c 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -12,7 +12,6 @@ import logging import sys -from pyomo.common.dependencies import numpy_available from pyomo.common.deprecation import deprecated, relocated_module_attribute from pyomo.common.errors import TemplateExpressionError @@ -208,13 +207,6 @@ def check_if_numeric_type(obj): if obj_class in native_types: return obj_class in native_numeric_types - if 'numpy' in obj_class.__module__: - # trigger the resolution of numpy_available and check if this - # type was automatically registered - bool(numpy_available) - if obj_class in native_types: - return obj_class in native_numeric_types - try: obj_plus_0 = obj + 0 obj_p0_class = obj_plus_0.__class__ From 9205b81d6a3f39e2d1fc5c730deac932716b6a3a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 15:34:28 -0700 Subject: [PATCH 0778/3044] rename 'defer_check' to 'defer_import' --- doc/OnlineDocs/conf.py | 2 +- pyomo/common/dependencies.py | 31 ++++++++---- pyomo/common/tests/dep_mod.py | 4 +- pyomo/common/tests/deps.py | 5 +- pyomo/common/tests/test_dependencies.py | 48 +++++++++---------- pyomo/common/unittest.py | 2 +- pyomo/contrib/pynumero/dependencies.py | 2 +- .../examples/tests/test_cyipopt_examples.py | 2 +- pyomo/contrib/pynumero/intrinsic.py | 4 +- pyomo/core/base/units_container.py | 1 - pyomo/solvers/plugins/solvers/GAMS.py | 2 +- 11 files changed, 58 insertions(+), 45 deletions(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 04fe458407b..1aab4cd76c2 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -271,7 +271,7 @@ def check_output(self, want, got, optionflags): yaml_available, networkx_available, matplotlib_available, pympler_available, dill_available, ) -pint_available = attempt_import('pint', defer_check=False)[1] +pint_available = attempt_import('pint', defer_import=False)[1] from pyomo.contrib.parmest.parmest import parmest_available import pyomo.environ as _pe # (trigger all plugin registrations) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 9aa6c4c4f7a..12ca6bd4ce3 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -130,7 +130,7 @@ class DeferredImportModule(object): This object is returned by :py:func:`attempt_import()` in lieu of the module when :py:func:`attempt_import()` is called with - ``defer_check=True``. Any attempts to access attributes on this + ``defer_import=True``. Any attempts to access attributes on this object will trigger the actual module import and return either the appropriate module attribute or else if the module import fails, raise a :py:class:`.DeferredImportError` exception. @@ -526,7 +526,8 @@ def attempt_import( alt_names=None, callback=None, importer=None, - defer_check=True, + defer_check=None, + defer_import=None, deferred_submodules=None, catch_exceptions=None, ): @@ -607,10 +608,16 @@ def attempt_import( want to import/return the first one that is available. defer_check: bool, optional - If True (the default), then the attempted import is deferred - until the first use of either the module or the availability - flag. The method will return instances of :py:class:`DeferredImportModule` - and :py:class:`DeferredImportIndicator`. + DEPRECATED: renamed to ``defer_import`` + + defer_import: bool, optional + If True, then the attempted import is deferred until the first + use of either the module or the availability flag. The method + will return instances of :py:class:`DeferredImportModule` and + :py:class:`DeferredImportIndicator`. If False, the import will + be attempted immediately. If not set, then the import will be + deferred unless the ``name`` is already present in + ``sys.modules``. deferred_submodules: Iterable[str], optional If provided, an iterable of submodule names within this module @@ -661,9 +668,17 @@ def attempt_import( if catch_exceptions is None: catch_exceptions = (ImportError,) + if defer_check is not None: + deprecation_warning( + 'defer_check=%s is deprecated. Please use defer_import' % (defer_check,), + version='6.7.2.dev0', + ) + assert defer_import is None + defer_import = defer_check + # If we are going to defer the check until later, return the # deferred import module object - if defer_check: + if defer_import: if deferred_submodules: if isinstance(deferred_submodules, Mapping): deprecation_warning( @@ -706,7 +721,7 @@ def attempt_import( return DeferredImportModule(indicator, deferred, None), indicator if deferred_submodules: - raise ValueError("deferred_submodules is only valid if defer_check==True") + raise ValueError("deferred_submodules is only valid if defer_import==True") return _perform_import( name=name, diff --git a/pyomo/common/tests/dep_mod.py b/pyomo/common/tests/dep_mod.py index f6add596ed4..34c7219c6eb 100644 --- a/pyomo/common/tests/dep_mod.py +++ b/pyomo/common/tests/dep_mod.py @@ -13,8 +13,8 @@ __version__ = '1.5' -numpy, numpy_available = attempt_import('numpy', defer_check=True) +numpy, numpy_available = attempt_import('numpy', defer_import=True) bogus_nonexisting_module, bogus_nonexisting_module_available = attempt_import( - 'bogus_nonexisting_module', alt_names=['bogus_nem'], defer_check=True + 'bogus_nonexisting_module', alt_names=['bogus_nem'], defer_import=True ) diff --git a/pyomo/common/tests/deps.py b/pyomo/common/tests/deps.py index d00281553f4..5f8c1fffdf8 100644 --- a/pyomo/common/tests/deps.py +++ b/pyomo/common/tests/deps.py @@ -23,15 +23,16 @@ bogus_nonexisting_module_available as has_bogus_nem, ) -bogus, bogus_available = attempt_import('nonexisting.module.bogus', defer_check=True) +bogus, bogus_available = attempt_import('nonexisting.module.bogus', defer_import=True) pkl_test, pkl_available = attempt_import( - 'nonexisting.module.pickle_test', deferred_submodules=['submod'], defer_check=True + 'nonexisting.module.pickle_test', deferred_submodules=['submod'], defer_import=True ) pyo, pyo_available = attempt_import( 'pyomo', alt_names=['pyo'], + defer_import=True, deferred_submodules={'version': None, 'common.tests.dep_mod': ['dm']}, ) diff --git a/pyomo/common/tests/test_dependencies.py b/pyomo/common/tests/test_dependencies.py index 30822a4f81f..31f9520b613 100644 --- a/pyomo/common/tests/test_dependencies.py +++ b/pyomo/common/tests/test_dependencies.py @@ -45,7 +45,7 @@ def test_import_error(self): module_obj, module_available = attempt_import( '__there_is_no_module_named_this__', 'Testing import of a non-existent module', - defer_check=False, + defer_import=False, ) self.assertFalse(module_available) with self.assertRaisesRegex( @@ -85,7 +85,7 @@ def test_pickle(self): def test_import_success(self): module_obj, module_available = attempt_import( - 'ply', 'Testing import of ply', defer_check=False + 'ply', 'Testing import of ply', defer_import=False ) self.assertTrue(module_available) import ply @@ -123,7 +123,7 @@ def test_imported_deferred_import(self): def test_min_version(self): mod, avail = attempt_import( - 'pyomo.common.tests.dep_mod', minimum_version='1.0', defer_check=False + 'pyomo.common.tests.dep_mod', minimum_version='1.0', defer_import=False ) self.assertTrue(avail) self.assertTrue(inspect.ismodule(mod)) @@ -131,7 +131,7 @@ def test_min_version(self): self.assertFalse(check_min_version(mod, '2.0')) mod, avail = attempt_import( - 'pyomo.common.tests.dep_mod', minimum_version='2.0', defer_check=False + 'pyomo.common.tests.dep_mod', minimum_version='2.0', defer_import=False ) self.assertFalse(avail) self.assertIs(type(mod), ModuleUnavailable) @@ -146,7 +146,7 @@ def test_min_version(self): 'pyomo.common.tests.dep_mod', error_message="Failed import", minimum_version='2.0', - defer_check=False, + defer_import=False, ) self.assertFalse(avail) self.assertIs(type(mod), ModuleUnavailable) @@ -159,10 +159,10 @@ def test_min_version(self): # Verify check_min_version works with deferred imports - mod, avail = attempt_import('pyomo.common.tests.dep_mod', defer_check=True) + mod, avail = attempt_import('pyomo.common.tests.dep_mod', defer_import=True) self.assertTrue(check_min_version(mod, '1.0')) - mod, avail = attempt_import('pyomo.common.tests.dep_mod', defer_check=True) + mod, avail = attempt_import('pyomo.common.tests.dep_mod', defer_import=True) self.assertFalse(check_min_version(mod, '2.0')) # Verify check_min_version works when called directly @@ -174,10 +174,10 @@ def test_min_version(self): self.assertFalse(check_min_version(mod, '1.0')) def test_and_or(self): - mod0, avail0 = attempt_import('ply', defer_check=True) - mod1, avail1 = attempt_import('pyomo.common.tests.dep_mod', defer_check=True) + mod0, avail0 = attempt_import('ply', defer_import=True) + mod1, avail1 = attempt_import('pyomo.common.tests.dep_mod', defer_import=True) mod2, avail2 = attempt_import( - 'pyomo.common.tests.dep_mod', minimum_version='2.0', defer_check=True + 'pyomo.common.tests.dep_mod', minimum_version='2.0', defer_import=True ) _and = avail0 & avail1 @@ -233,11 +233,11 @@ def test_callbacks(self): def _record_avail(module, avail): ans.append(avail) - mod0, avail0 = attempt_import('ply', defer_check=True, callback=_record_avail) + mod0, avail0 = attempt_import('ply', defer_import=True, callback=_record_avail) mod1, avail1 = attempt_import( 'pyomo.common.tests.dep_mod', minimum_version='2.0', - defer_check=True, + defer_import=True, callback=_record_avail, ) @@ -250,7 +250,7 @@ def _record_avail(module, avail): def test_import_exceptions(self): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, only_catch_importerror=True, ) with self.assertRaisesRegex(ValueError, "cannot import module"): @@ -260,7 +260,7 @@ def test_import_exceptions(self): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, only_catch_importerror=False, ) self.assertFalse(avail) @@ -268,7 +268,7 @@ def test_import_exceptions(self): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, catch_exceptions=(ImportError, ValueError), ) self.assertFalse(avail) @@ -280,7 +280,7 @@ def test_import_exceptions(self): ): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, only_catch_importerror=True, catch_exceptions=(ImportError,), ) @@ -288,7 +288,7 @@ def test_import_exceptions(self): def test_generate_warning(self): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, only_catch_importerror=False, ) @@ -324,7 +324,7 @@ def test_generate_warning(self): def test_log_warning(self): mod, avail = attempt_import( 'pyomo.common.tests.dep_mod_except', - defer_check=True, + defer_import=True, only_catch_importerror=False, ) log = StringIO() @@ -366,9 +366,9 @@ def test_importer(self): def _importer(): attempted_import.append(True) - return attempt_import('pyomo.common.tests.dep_mod', defer_check=False)[0] + return attempt_import('pyomo.common.tests.dep_mod', defer_import=False)[0] - mod, avail = attempt_import('foo', importer=_importer, defer_check=True) + mod, avail = attempt_import('foo', importer=_importer, defer_import=True) self.assertEqual(attempted_import, []) self.assertIsInstance(mod, DeferredImportModule) @@ -401,17 +401,17 @@ def test_deferred_submodules(self): self.assertTrue(inspect.ismodule(deps.dm)) with self.assertRaisesRegex( - ValueError, "deferred_submodules is only valid if defer_check==True" + ValueError, "deferred_submodules is only valid if defer_import==True" ): mod, mod_available = attempt_import( 'nonexisting.module', - defer_check=False, + defer_import=False, deferred_submodules={'submod': None}, ) mod, mod_available = attempt_import( 'nonexisting.module', - defer_check=True, + defer_import=True, deferred_submodules={'submod.subsubmod': None}, ) self.assertIs(type(mod), DeferredImportModule) @@ -427,7 +427,7 @@ def test_UnavailableClass(self): module_obj, module_available = attempt_import( '__there_is_no_module_named_this__', 'Testing import of a non-existent module', - defer_check=False, + defer_import=False, ) class A_Class(UnavailableClass(module_obj)): diff --git a/pyomo/common/unittest.py b/pyomo/common/unittest.py index 9a21b35faa8..84b44775c1b 100644 --- a/pyomo/common/unittest.py +++ b/pyomo/common/unittest.py @@ -631,7 +631,7 @@ def initialize_dependencies(self): cls.package_modules = {} packages_used = set(sum(list(cls.package_dependencies.values()), [])) for package_ in packages_used: - pack, pack_avail = attempt_import(package_, defer_check=False) + pack, pack_avail = attempt_import(package_, defer_import=False) cls.package_available[package_] = pack_avail cls.package_modules[package_] = pack diff --git a/pyomo/contrib/pynumero/dependencies.py b/pyomo/contrib/pynumero/dependencies.py index 9e2088ffa0a..d323bd43e84 100644 --- a/pyomo/contrib/pynumero/dependencies.py +++ b/pyomo/contrib/pynumero/dependencies.py @@ -17,7 +17,7 @@ 'numpy', 'Pynumero requires the optional Pyomo dependency "numpy"', minimum_version='1.13.0', - defer_check=False, + defer_import=False, ) if not numpy_available: diff --git a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py index 1f45f26d43b..408a0197382 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py @@ -35,7 +35,7 @@ 'One of the tests below requires a recent version of pandas for' ' comparing with a tolerance.', minimum_version='1.1.0', - defer_check=False, + defer_import=False, ) from pyomo.contrib.pynumero.asl import AmplInterface diff --git a/pyomo/contrib/pynumero/intrinsic.py b/pyomo/contrib/pynumero/intrinsic.py index 84675cc4c02..34054e7ffa2 100644 --- a/pyomo/contrib/pynumero/intrinsic.py +++ b/pyomo/contrib/pynumero/intrinsic.py @@ -11,9 +11,7 @@ from pyomo.common.dependencies import numpy as np, attempt_import -block_vector = attempt_import( - 'pyomo.contrib.pynumero.sparse.block_vector', defer_check=True -)[0] +block_vector = attempt_import('pyomo.contrib.pynumero.sparse.block_vector')[0] def norm(x, ord=None): diff --git a/pyomo/core/base/units_container.py b/pyomo/core/base/units_container.py index 1bf25ffdead..fb3d28385d0 100644 --- a/pyomo/core/base/units_container.py +++ b/pyomo/core/base/units_container.py @@ -127,7 +127,6 @@ pint_module, pint_available = attempt_import( 'pint', - defer_check=True, error_message=( 'The "pint" package failed to import. ' 'This package is necessary to use Pyomo units.' diff --git a/pyomo/solvers/plugins/solvers/GAMS.py b/pyomo/solvers/plugins/solvers/GAMS.py index e84cbdb441d..be3499a2f6b 100644 --- a/pyomo/solvers/plugins/solvers/GAMS.py +++ b/pyomo/solvers/plugins/solvers/GAMS.py @@ -41,7 +41,7 @@ from pyomo.common.dependencies import attempt_import -gdxcc, gdxcc_available = attempt_import('gdxcc', defer_check=True) +gdxcc, gdxcc_available = attempt_import('gdxcc') logger = logging.getLogger('pyomo.solvers') From 4f1b93c0f08b5819b2f54c565b2ebc8c96061887 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 15:34:44 -0700 Subject: [PATCH 0779/3044] NFC: update comment --- pyomo/common/dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 12ca6bd4ce3..8246cbb0776 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -597,7 +597,7 @@ def attempt_import( module in the ``globals()`` namespaces. For example, the alt_names for NumPy would be ``['np']``. (deprecated in version 6.0) - callback: function, optional + callback: Callable[[ModuleType, bool], None], optional A function with the signature "``fcn(module, available)``" that will be called after the import is first attempted. From d80cde1728883cb11e193be4dbb1109e68833665 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 15:35:22 -0700 Subject: [PATCH 0780/3044] Do not defer import if module is already imported --- pyomo/common/dependencies.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 8246cbb0776..9b2f5ab5767 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -676,6 +676,15 @@ def attempt_import( assert defer_import is None defer_import = defer_check + # If the module has already been imported, there is no reason to + # further defer things: just import it. + if defer_import is None: + if name in sys.modules: + defer_import = False + deferred_submodules = None + else: + defer_import = True + # If we are going to defer the check until later, return the # deferred import module object if defer_import: From 6e2db622df1e3e556be9aee6523d269dd3bf95df Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 15:37:48 -0700 Subject: [PATCH 0781/3044] NFC: apply black --- pyomo/common/dependencies.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 9b2f5ab5767..5bc752deb53 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -490,6 +490,7 @@ class DeferredImportCallbackFinder: :py:class:`DeferredImportModule`. """ + _callbacks = {} def find_spec(self, fullname, path, target=None): From a10d36405c28cabb180dae9c852335f7dd967e95 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 26 Feb 2024 15:41:11 -0700 Subject: [PATCH 0782/3044] NFC: fix typo --- pyomo/common/dependencies.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 5bc752deb53..9034342b5a1 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -513,9 +513,9 @@ def invalidate_caches(self): _DeferredImportCallbackFinder = DeferredImportCallbackFinder() # Insert the DeferredImportCallbackFinder at the beginning of the -# mata_path to that it is found before the standard finders (so that we -# can correctly inject the resolution of the DeferredImportIndicators -- -# which triggers the needed callbacks) +# sys.meta_path to that it is found before the standard finders (so that +# we can correctly inject the resolution of the DeferredImportIndicators +# -- which triggers the needed callbacks) sys.meta_path.insert(0, _DeferredImportCallbackFinder) From 648aae7392ec98bc33debe94aaa530b7f872417a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 00:00:10 -0700 Subject: [PATCH 0783/3044] Update type registration tests to reflect automatic numpy callback --- pyomo/core/tests/unit/test_numvalue.py | 90 ++++++++++++++++++++------ 1 file changed, 72 insertions(+), 18 deletions(-) diff --git a/pyomo/core/tests/unit/test_numvalue.py b/pyomo/core/tests/unit/test_numvalue.py index bd784d655e8..2dca2df56a6 100644 --- a/pyomo/core/tests/unit/test_numvalue.py +++ b/pyomo/core/tests/unit/test_numvalue.py @@ -50,7 +50,16 @@ def __init__(self, val=0): class MyBogusNumericType(MyBogusType): def __add__(self, other): - return MyBogusNumericType(self.val + float(other)) + if other.__class__ in native_numeric_types: + return MyBogusNumericType(self.val + float(other)) + else: + return NotImplemented + + def __le__(self, other): + if other.__class__ in native_numeric_types: + return self.val <= float(other) + else: + return NotImplemented def __lt__(self, other): return self.val < float(other) @@ -534,6 +543,8 @@ def test_unknownNumericType(self): try: val = as_numeric(ref) self.assertEqual(val().val, 42.0) + self.assertIn(MyBogusNumericType, native_numeric_types) + self.assertIn(MyBogusNumericType, native_types) finally: native_numeric_types.remove(MyBogusNumericType) native_types.remove(MyBogusNumericType) @@ -562,10 +573,43 @@ def test_numpy_basic_bool_registration(self): @unittest.skipUnless(numpy_available, "This test requires NumPy") def test_automatic_numpy_registration(self): cmd = ( - 'import pyomo; from pyomo.core.base import Var, Param; ' - 'from pyomo.core.base.units_container import units; import numpy as np; ' - 'print(np.float64 in pyomo.common.numeric_types.native_numeric_types); ' - '%s; print(np.float64 in pyomo.common.numeric_types.native_numeric_types)' + 'from pyomo.common.numeric_types import native_numeric_types as nnt; ' + 'print("float64" in [_.__name__ for _ in nnt]); ' + 'import numpy; ' + 'print("float64" in [_.__name__ for _ in nnt])' + ) + + rc = subprocess.run( + [sys.executable, '-c', cmd], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + self.assertEqual((rc.returncode, rc.stdout), (0, "False\nTrue\n")) + + cmd = ( + 'import numpy; ' + 'from pyomo.common.numeric_types import native_numeric_types as nnt; ' + 'print("float64" in [_.__name__ for _ in nnt])' + ) + + rc = subprocess.run( + [sys.executable, '-c', cmd], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + self.assertEqual((rc.returncode, rc.stdout), (0, "True\n")) + + def test_unknownNumericType_expr_registration(self): + cmd = ( + 'import pyomo; ' + 'from pyomo.core.base import Var, Param; ' + 'from pyomo.core.base.units_container import units; ' + 'from pyomo.common.numeric_types import native_numeric_types as nnt; ' + f'from {__name__} import MyBogusNumericType; ' + 'ref = MyBogusNumericType(42); ' + 'print(MyBogusNumericType in nnt); %s; print(MyBogusNumericType in nnt); ' ) def _tester(expr): @@ -575,19 +619,29 @@ def _tester(expr): stderr=subprocess.STDOUT, text=True, ) - self.assertEqual((rc.returncode, rc.stdout), (0, "False\nTrue\n")) - - _tester('Var() <= np.float64(5)') - _tester('np.float64(5) <= Var()') - _tester('np.float64(5) + Var()') - _tester('Var() + np.float64(5)') - _tester('v = Var(); v.construct(); v.value = np.float64(5)') - _tester('p = Param(mutable=True); p.construct(); p.value = np.float64(5)') - _tester('v = Var(units=units.m); v.construct(); v.value = np.float64(5)') - _tester( - 'p = Param(mutable=True, units=units.m); p.construct(); ' - 'p.value = np.float64(5)' - ) + self.assertEqual( + (rc.returncode, rc.stdout), + ( + 0, + '''False +WARNING: Dynamically registering the following numeric type: + pyomo.core.tests.unit.test_numvalue.MyBogusNumericType + Dynamic registration is supported for convenience, but there are known + limitations to this approach. We recommend explicitly registering numeric + types using RegisterNumericType() or RegisterIntegerType(). +True +''', + ), + ) + + _tester('Var() <= ref') + _tester('ref <= Var()') + _tester('ref + Var()') + _tester('Var() + ref') + _tester('v = Var(); v.construct(); v.value = ref') + _tester('p = Param(mutable=True); p.construct(); p.value = ref') + _tester('v = Var(units=units.m); v.construct(); v.value = ref') + _tester('p = Param(mutable=True, units=units.m); p.construct(); p.value = ref') if __name__ == "__main__": From adbf1de2e3b6ca8c16eb6b81a9ae4966ea30fd94 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 00:09:19 -0700 Subject: [PATCH 0784/3044] Remove numpy reference/check --- pyomo/common/numeric_types.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index 412a1bbeade..616d4c4bae4 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -228,13 +228,6 @@ def check_if_logical_type(obj): if obj_class in native_types: return obj_class in native_logical_types - if 'numpy' in obj_class.__module__: - # trigger the resolution of numpy_available and check if this - # type was automatically registered - bool(numpy_available) - if obj_class in native_types: - return obj_class in native_logical_types - try: if all( ( From ea77dca9ecf302e3757fbc916ce515562e16473e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 12:36:36 -0700 Subject: [PATCH 0785/3044] Resolve registration for modules that were already inmported --- pyomo/common/dependencies.py | 117 +++++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 47 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 9034342b5a1..505211aeb56 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -822,20 +822,36 @@ def declare_deferred_modules_as_importable(globals_dict): :py:class:`ModuleUnavailable` instance. """ - _global_name = globals_dict['__name__'] + '.' - deferred = list( - (k, v) for k, v in globals_dict.items() if type(v) is DeferredImportModule - ) - while deferred: - name, mod = deferred.pop(0) - mod.__path__ = None - mod.__spec__ = None - sys.modules[_global_name + name] = mod - deferred.extend( - (name + '.' + k, v) - for k, v in mod.__dict__.items() - if type(v) is DeferredImportModule - ) + return declare_modules_as_importable(globals_dict).__exit__(None, None, None) + + +class declare_modules_as_importable(object): + def __init__(self, globals_dict): + self.globals_dict = globals_dict + self.init_dict = {} + + def __enter__(self): + self.init_dict.update(self.globals_dict) + + def __exit__(self, exc_type, exc_value, traceback): + _global_name = self.globals_dict['__name__'] + '.' + deferred = [ + (k, v) + for k, v in self.globals_dict.items() + if k not in self.init_dict + and isinstance(v, (ModuleType, DeferredImportModule)) + ] + while deferred: + name, mod = deferred.pop(0) + mod.__path__ = None + mod.__spec__ = None + sys.modules[_global_name + name] = mod + if isinstance(mod, DeferredImportModule): + deferred.extend( + (name + '.' + k, v) + for k, v in mod.__dict__.items() + if type(v) is DeferredImportModule + ) # @@ -952,41 +968,48 @@ def _pyutilib_importer(): return importlib.import_module('pyutilib') -# Standard libraries that are slower to import and not strictly required -# on all platforms / situations. -ctypes, _ = attempt_import( - 'ctypes', deferred_submodules=['util'], callback=_finalize_ctypes -) -random, _ = attempt_import('random') - -# Commonly-used optional dependencies -dill, dill_available = attempt_import('dill') -mpi4py, mpi4py_available = attempt_import('mpi4py') -networkx, networkx_available = attempt_import('networkx') -numpy, numpy_available = attempt_import('numpy', callback=_finalize_numpy) -pandas, pandas_available = attempt_import('pandas') -plotly, plotly_available = attempt_import('plotly') -pympler, pympler_available = attempt_import('pympler', callback=_finalize_pympler) -pyutilib, pyutilib_available = attempt_import('pyutilib', importer=_pyutilib_importer) -scipy, scipy_available = attempt_import( - 'scipy', - callback=_finalize_scipy, - deferred_submodules=['stats', 'sparse', 'spatial', 'integrate'], -) -yaml, yaml_available = attempt_import('yaml', callback=_finalize_yaml) - -# Note that matplotlib.pyplot can generate a runtime error on OSX when -# not installed as a Framework (as is the case in the CI systems) -matplotlib, matplotlib_available = attempt_import( - 'matplotlib', - callback=_finalize_matplotlib, - deferred_submodules=['pyplot', 'pylab'], - catch_exceptions=(ImportError, RuntimeError), -) +# +# Note: because we will be calling +# declare_deferred_modules_as_importable, it is important that the +# following declarations explicitly defer_import (even if the target +# module has already been imported) +# +with declare_modules_as_importable(globals()): + # Standard libraries that are slower to import and not strictly required + # on all platforms / situations. + ctypes, _ = attempt_import( + 'ctypes', deferred_submodules=['util'], callback=_finalize_ctypes + ) + random, _ = attempt_import('random') + + # Commonly-used optional dependencies + dill, dill_available = attempt_import('dill') + mpi4py, mpi4py_available = attempt_import('mpi4py') + networkx, networkx_available = attempt_import('networkx') + numpy, numpy_available = attempt_import('numpy', callback=_finalize_numpy) + pandas, pandas_available = attempt_import('pandas') + plotly, plotly_available = attempt_import('plotly') + pympler, pympler_available = attempt_import('pympler', callback=_finalize_pympler) + pyutilib, pyutilib_available = attempt_import( + 'pyutilib', importer=_pyutilib_importer + ) + scipy, scipy_available = attempt_import( + 'scipy', + callback=_finalize_scipy, + deferred_submodules=['stats', 'sparse', 'spatial', 'integrate'], + ) + yaml, yaml_available = attempt_import('yaml', callback=_finalize_yaml) + + # Note that matplotlib.pyplot can generate a runtime error on OSX when + # not installed as a Framework (as is the case in the CI systems) + matplotlib, matplotlib_available = attempt_import( + 'matplotlib', + callback=_finalize_matplotlib, + deferred_submodules=['pyplot', 'pylab'], + catch_exceptions=(ImportError, RuntimeError), + ) try: import cPickle as pickle except ImportError: import pickle - -declare_deferred_modules_as_importable(globals()) From 24f649334eb0d2291551c8b09923e251e7ef486c Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 27 Feb 2024 12:48:51 -0800 Subject: [PATCH 0786/3044] clean up, moved _expand_indexed_unknowns --- pyomo/contrib/parmest/parmest.py | 75 ++++++++++++-------------------- 1 file changed, 27 insertions(+), 48 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 9e5b480332d..ffe9afc059e 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -226,12 +226,12 @@ def _experiment_instance_creation_callback( thetavals = outer_cb_data["ThetaVals"] # dlw august 2018: see mea code for more general theta - for vstr in thetavals: - theta_cuid = ComponentUID(vstr) + for name, val in thetavals.items(): + theta_cuid = ComponentUID(name) theta_object = theta_cuid.find_component_on(instance) - if thetavals[vstr] is not None: + if val is not None: # print("Fixing",vstr,"at",str(thetavals[vstr])) - theta_object.fix(thetavals[vstr]) + theta_object.fix(val) else: # print("Freeing",vstr) theta_object.unfix() @@ -400,6 +400,29 @@ def _return_theta_names(self): self.estimator_theta_names ) # default theta_names, created when Estimator object is created + def _expand_indexed_unknowns(self, model_temp): + """ + Expand indexed variables to get full list of thetas + """ + model_theta_list = [k.name for k, v in model_temp.unknown_parameters.items()] + + # check for indexed theta items + indexed_theta_list = [] + for theta_i in model_theta_list: + var_cuid = ComponentUID(theta_i) + var_validate = var_cuid.find_component_on(model_temp) + for ind in var_validate.index_set(): + if ind is not None: + indexed_theta_list.append(theta_i + '[' + str(ind) + ']') + else: + indexed_theta_list.append(theta_i) + + # if we found indexed thetas, use expanded list + if len(indexed_theta_list) > len(model_theta_list): + model_theta_list = indexed_theta_list + + return model_theta_list + def _create_parmest_model(self, experiment_number): """ Modify the Pyomo model for parameter estimation @@ -1155,28 +1178,6 @@ def leaveNout_bootstrap_test( return results - # expand indexed variables to get full list of thetas - def _expand_indexed_unknowns(self, model_temp): - - model_theta_list = [k.name for k, v in model_temp.unknown_parameters.items()] - - # check for indexed theta items - indexed_theta_list = [] - for theta_i in model_theta_list: - var_cuid = ComponentUID(theta_i) - var_validate = var_cuid.find_component_on(model_temp) - for ind in var_validate.index_set(): - if ind is not None: - indexed_theta_list.append(theta_i + '[' + str(ind) + ']') - else: - indexed_theta_list.append(theta_i) - - # if we found indexed thetas, use expanded list - if len(indexed_theta_list) > len(model_theta_list): - model_theta_list = indexed_theta_list - - return model_theta_list - def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): """ Objective value for each theta @@ -1212,28 +1213,6 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): model_temp = self._create_parmest_model(0) model_theta_list = self._expand_indexed_unknowns(model_temp) - # # iterate over original theta_names - # for theta_i in self.theta_names: - # var_cuid = ComponentUID(theta_i) - # var_validate = var_cuid.find_component_on(model_temp) - # # check if theta in theta_names are indexed - # try: - # # get component UID of Set over which theta is defined - # set_cuid = ComponentUID(var_validate.index_set()) - # # access and iterate over the Set to generate theta names as they appear - # # in the pyomo model - # set_validate = set_cuid.find_component_on(model_temp) - # for s in set_validate: - # self_theta_temp = repr(var_cuid) + "[" + repr(s) + "]" - # # generate list of theta names - # model_theta_list.append(self_theta_temp) - # # if theta is not indexed, copy theta name to list as-is - # except AttributeError: - # self_theta_temp = repr(var_cuid) - # model_theta_list.append(self_theta_temp) - # except: - # raise - # if self.theta_names is not the same as temp model_theta_list, # create self.theta_names_updated if set(self.estimator_theta_names) == set(model_theta_list) and len( From 6699fde39928e0dc9eba0482bc018a8d1a540101 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 16:07:43 -0700 Subject: [PATCH 0787/3044] declare_modules_as_importable will also detect imported submodules --- pyomo/common/dependencies.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 505211aeb56..2954a8bff83 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -829,25 +829,31 @@ class declare_modules_as_importable(object): def __init__(self, globals_dict): self.globals_dict = globals_dict self.init_dict = {} + self.init_modules = None def __enter__(self): self.init_dict.update(self.globals_dict) + self.init_modules = set(sys.modules) def __exit__(self, exc_type, exc_value, traceback): _global_name = self.globals_dict['__name__'] + '.' - deferred = [ - (k, v) + deferred = { + k: v for k, v in self.globals_dict.items() if k not in self.init_dict and isinstance(v, (ModuleType, DeferredImportModule)) - ] + } + if self.init_modules: + for name in set(sys.modules) - self.init_modules: + if '.' in name and name.split('.', 1)[0] in deferred: + sys.modules[_global_name + name] = sys.modules[name] while deferred: - name, mod = deferred.pop(0) + name, mod = deferred.popitem() mod.__path__ = None mod.__spec__ = None sys.modules[_global_name + name] = mod if isinstance(mod, DeferredImportModule): - deferred.extend( + deferred.update( (name + '.' + k, v) for k, v in mod.__dict__.items() if type(v) is DeferredImportModule From a95af93eb62be5ea0d4137457f904d109de00363 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 16:08:05 -0700 Subject: [PATCH 0788/3044] Update docs, deprecate declare_deferred_modules_as_importable --- pyomo/common/dependencies.py | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 2954a8bff83..7d5437f6da9 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -782,6 +782,11 @@ def _perform_import( return module, False +@deprecated( + "declare_deferred_modules_as_importable() is dperecated. " + "Use the declare_modules_as_importable() context manager." + version='6.7.2.dev0' +) def declare_deferred_modules_as_importable(globals_dict): """Make all :py:class:`DeferredImportModules` in ``globals_dict`` importable @@ -826,6 +831,50 @@ def declare_deferred_modules_as_importable(globals_dict): class declare_modules_as_importable(object): + """Make all :py:class:`ModuleType` and :py:class:`DeferredImportModules` + importable through the ``globals_dict`` context. + + This context manager will detect all modules imported into the + specified ``globals_dict`` environment (either directly or through + :py:fcn:`attempt_import`) and will make those modules importable + from the specified ``globals_dict`` context. It works by detecting + changes in the specified ``globals_dict`` dictionary and adding any new + modules or instances of :py:class:`DeferredImportModule` that it + finds (and any of their deferred submodules) to ``sys.modules`` so + that the modules can be imported through the ``globals_dict`` + namespace. + + For example, ``pyomo/common/dependencies.py`` declares: + + .. doctest:: + :hide: + + >>> from pyomo.common.dependencies import ( + ... attempt_import, _finalize_scipy, __dict__ as dep_globals, + ... declare_deferred_modules_as_importable, ) + >>> # Sphinx does not provide a proper globals() + >>> def globals(): return dep_globals + + .. doctest:: + + >>> with declare_modules_as_importable(globals()): + ... scipy, scipy_available = attempt_import( + ... 'scipy', callback=_finalize_scipy, + ... deferred_submodules=['stats', 'sparse', 'spatial', 'integrate']) + + Which enables users to use: + + .. doctest:: + + >>> import pyomo.common.dependencies.scipy.sparse as spa + + If the deferred import has not yet been triggered, then the + :py:class:`DeferredImportModule` is returned and named ``spa``. + However, if the import has already been triggered, then ``spa`` will + either be the ``scipy.sparse`` module, or a + :py:class:`ModuleUnavailable` instance. + + """ def __init__(self, globals_dict): self.globals_dict = globals_dict self.init_dict = {} From 076dabe721876e931cb488e84f9b546e4a9baa2f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 16:08:27 -0700 Subject: [PATCH 0789/3044] Add deep import for numpy to resolve scipy import error --- pyomo/common/dependencies.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 7d5437f6da9..aab0d55d9b4 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -964,6 +964,11 @@ def _finalize_matplotlib(module, available): def _finalize_numpy(np, available): if not available: return + # scipy has a dependence on numpy.testing, and if we don't import it + # as part of resolving numpy, then certain deferred scipy imports + # fail when run under pytest. + import numpy.testing + from . import numeric_types # Register ndarray as a native type to prevent 1-element ndarrays From dc19d4e333f6c714deab75a06e9602c90ea97b5a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 16:09:49 -0700 Subject: [PATCH 0790/3044] Fix typo --- pyomo/common/dependencies.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index aab0d55d9b4..900618b696a 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -784,8 +784,8 @@ def _perform_import( @deprecated( "declare_deferred_modules_as_importable() is dperecated. " - "Use the declare_modules_as_importable() context manager." - version='6.7.2.dev0' + "Use the declare_modules_as_importable() context manager.", + version='6.7.2.dev0', ) def declare_deferred_modules_as_importable(globals_dict): """Make all :py:class:`DeferredImportModules` in ``globals_dict`` importable @@ -875,6 +875,7 @@ class declare_modules_as_importable(object): :py:class:`ModuleUnavailable` instance. """ + def __init__(self, globals_dict): self.globals_dict = globals_dict self.init_dict = {} From 6efece0f3385b5e782525d0d7a1aeee45f824039 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 16:30:21 -0700 Subject: [PATCH 0791/3044] Resolve doctest failures --- doc/OnlineDocs/contributed_packages/pyros.rst | 2 +- pyomo/common/dependencies.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index aad37a9685a..76a751dd994 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -689,7 +689,7 @@ could have been equivalently written as: ... }, ... ) ============================================================================== - PyROS: The Pyomo Robust Optimization Solver. + PyROS: The Pyomo Robust Optimization Solver... ... ------------------------------------------------------------------------------ Robust optimal solution identified. diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 900618b696a..c09594b6e12 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -813,6 +813,7 @@ def declare_deferred_modules_as_importable(globals_dict): ... 'scipy', callback=_finalize_scipy, ... deferred_submodules=['stats', 'sparse', 'spatial', 'integrate']) >>> declare_deferred_modules_as_importable(globals()) + WARNING: DEPRECATED: ... Which enables users to use: @@ -851,7 +852,7 @@ class declare_modules_as_importable(object): >>> from pyomo.common.dependencies import ( ... attempt_import, _finalize_scipy, __dict__ as dep_globals, - ... declare_deferred_modules_as_importable, ) + ... declare_modules_as_importable, ) >>> # Sphinx does not provide a proper globals() >>> def globals(): return dep_globals From 754de4114ac58381089c21fdffa0ee3345b8d21a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 16:30:48 -0700 Subject: [PATCH 0792/3044] NFC: doc updates --- pyomo/common/dependencies.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index c09594b6e12..f0713a53cbf 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -485,7 +485,7 @@ class DeferredImportCallbackFinder: normal loader returned by ``PathFinder`` with a loader that will trigger custom callbacks after the module is loaded. We use this to trigger the post import callbacks registered through - :py:fcn:`attempt_import` even when a user imports the target library + :py:func:`attempt_import` even when a user imports the target library directly (and not through attribute access on the :py:class:`DeferredImportModule`. @@ -582,7 +582,8 @@ def attempt_import( The message for the exception raised by :py:class:`ModuleUnavailable` only_catch_importerror: bool, optional - DEPRECATED: use catch_exceptions instead or only_catch_importerror. + DEPRECATED: use ``catch_exceptions`` instead of ``only_catch_importerror``. + If True (the default), exceptions other than ``ImportError`` raised during module import will be reraised. If False, any exception will result in returning a :py:class:`ModuleUnavailable` object. @@ -593,13 +594,14 @@ def attempt_import( ``module.__version__``) alt_names: list, optional - DEPRECATED: alt_names no longer needs to be specified and is ignored. + DEPRECATED: ``alt_names`` no longer needs to be specified and is ignored. + A list of common alternate names by which to look for this module in the ``globals()`` namespaces. For example, the alt_names for NumPy would be ``['np']``. (deprecated in version 6.0) callback: Callable[[ModuleType, bool], None], optional - A function with the signature "``fcn(module, available)``" that + A function with the signature ``fcn(module, available)`` that will be called after the import is first attempted. importer: function, optional @@ -609,7 +611,7 @@ def attempt_import( want to import/return the first one that is available. defer_check: bool, optional - DEPRECATED: renamed to ``defer_import`` + DEPRECATED: renamed to ``defer_import`` (deprecated in version 6.7.2.dev0) defer_import: bool, optional If True, then the attempted import is deferred until the first @@ -783,8 +785,8 @@ def _perform_import( @deprecated( - "declare_deferred_modules_as_importable() is dperecated. " - "Use the declare_modules_as_importable() context manager.", + "``declare_deferred_modules_as_importable()`` is deprecated. " + "Use the :py:class:`declare_modules_as_importable` context manager.", version='6.7.2.dev0', ) def declare_deferred_modules_as_importable(globals_dict): @@ -837,7 +839,7 @@ class declare_modules_as_importable(object): This context manager will detect all modules imported into the specified ``globals_dict`` environment (either directly or through - :py:fcn:`attempt_import`) and will make those modules importable + :py:func:`attempt_import`) and will make those modules importable from the specified ``globals_dict`` context. It works by detecting changes in the specified ``globals_dict`` dictionary and adding any new modules or instances of :py:class:`DeferredImportModule` that it From 96658623579fb767c4a6347c01c6392e17ebf774 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 16:31:07 -0700 Subject: [PATCH 0793/3044] Add backends tot eh matplotlib deferred imports --- pyomo/common/dependencies.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index f0713a53cbf..edf32baa6d6 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -963,6 +963,8 @@ def _finalize_matplotlib(module, available): if in_testing_environment(): module.use('Agg') import matplotlib.pyplot + import matplotlib.pylab + import matplotlib.backends def _finalize_numpy(np, available): @@ -1069,7 +1071,7 @@ def _pyutilib_importer(): matplotlib, matplotlib_available = attempt_import( 'matplotlib', callback=_finalize_matplotlib, - deferred_submodules=['pyplot', 'pylab'], + deferred_submodules=['pyplot', 'pylab', 'backends'], catch_exceptions=(ImportError, RuntimeError), ) From 0938052a92a147e0f450e8744fb400eacae89f5e Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 27 Feb 2024 19:14:46 -0500 Subject: [PATCH 0794/3044] Simplify a few config domain validators --- pyomo/contrib/pyros/config.py | 77 ++++++------------------ pyomo/contrib/pyros/tests/test_config.py | 26 +++++--- 2 files changed, 36 insertions(+), 67 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index a7ca41d095f..bc2bfd591e6 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -26,71 +26,34 @@ default_pyros_solver_logger = setup_pyros_logger() -class LoggerType: +def logger_domain(obj): """ - Domain validator for objects castable to logging.Logger. - """ - - def __call__(self, obj): - """ - Cast object to logger. + Domain validator for logger-type arguments. - Parameters - ---------- - obj : object - Object to be cast. + This admits any object of type ``logging.Logger``, + or which can be cast to ``logging.Logger``. + """ + if isinstance(obj, logging.Logger): + return obj + else: + return logging.getLogger(obj) - Returns - ------- - logging.Logger - If `str_or_logger` is of type `logging.Logger`,then - `str_or_logger` is returned. - Otherwise, ``logging.getLogger(str_or_logger)`` - is returned. - """ - if isinstance(obj, logging.Logger): - return obj - else: - return logging.getLogger(obj) - def domain_name(self): - """Return str briefly describing domain encompassed by self.""" - return "None, str or logging.Logger" +logger_domain.domain_name = "None, str or logging.Logger" -class PositiveIntOrMinusOne: +def positive_int_or_minus_one(obj): """ - Domain validator for objects castable to a - strictly positive int or -1. + Domain validator for objects castable to a strictly + positive int or -1. """ + ans = int(obj) + if ans != float(obj) or (ans <= 0 and ans != -1): + raise ValueError(f"Expected positive int or -1, but received value {obj!r}") + return ans - def __call__(self, obj): - """ - Cast object to positive int or -1. - Parameters - ---------- - obj : object - Object of interest. - - Returns - ------- - int - Positive int, or -1. - - Raises - ------ - ValueError - If object not castable to positive int, or -1. - """ - ans = int(obj) - if ans != float(obj) or (ans <= 0 and ans != -1): - raise ValueError(f"Expected positive int or -1, but received value {obj!r}") - return ans - - def domain_name(self): - """Return str briefly describing domain encompassed by self.""" - return "positive int or -1" +positive_int_or_minus_one.domain_name = "positive int or -1" def mutable_param_validator(param_obj): @@ -721,7 +684,7 @@ def pyros_config(): "max_iter", ConfigValue( default=-1, - domain=PositiveIntOrMinusOne(), + domain=positive_int_or_minus_one, description=( """ Iteration limit. If -1 is provided, then no iteration @@ -766,7 +729,7 @@ def pyros_config(): "progress_logger", ConfigValue( default=default_pyros_solver_logger, - domain=LoggerType(), + domain=logger_domain, doc=( """ Logger (or name thereof) used for reporting PyROS solver diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 76b9114b9e6..3555391fd95 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -12,9 +12,9 @@ from pyomo.contrib.pyros.config import ( InputDataStandardizer, mutable_param_validator, - LoggerType, + logger_domain, SolverNotResolvable, - PositiveIntOrMinusOne, + positive_int_or_minus_one, pyros_config, SolverIterable, SolverResolvable, @@ -557,16 +557,22 @@ def test_positive_int_or_minus_one(self): """ Test positive int or -1 validator works as expected. """ - standardizer_func = PositiveIntOrMinusOne() + standardizer_func = positive_int_or_minus_one self.assertIs( standardizer_func(1.0), 1, - msg=(f"{PositiveIntOrMinusOne.__name__} does not standardize as expected."), + msg=( + f"{positive_int_or_minus_one.__name__} " + "does not standardize as expected." + ), ) self.assertEqual( standardizer_func(-1.00), -1, - msg=(f"{PositiveIntOrMinusOne.__name__} does not standardize as expected."), + msg=( + f"{positive_int_or_minus_one.__name__} " + "does not standardize as expected." + ), ) exc_str = r"Expected positive int or -1, but received value.*" @@ -576,26 +582,26 @@ def test_positive_int_or_minus_one(self): standardizer_func(0) -class TestLoggerType(unittest.TestCase): +class TestLoggerDomain(unittest.TestCase): """ - Test logger type validator. + Test logger type domain validator. """ def test_logger_type(self): """ Test logger type validator. """ - standardizer_func = LoggerType() + standardizer_func = logger_domain mylogger = logging.getLogger("example") self.assertIs( standardizer_func(mylogger), mylogger, - msg=f"{LoggerType.__name__} output not as expected", + msg=f"{standardizer_func.__name__} output not as expected", ) self.assertIs( standardizer_func(mylogger.name), mylogger, - msg=f"{LoggerType.__name__} output not as expected", + msg=f"{standardizer_func.__name__} output not as expected", ) exc_str = r"A logger name must be a string" From 3d5cb6c8fc7eae46f91d85bc4bf2cc71aaac9dc9 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 27 Feb 2024 20:22:14 -0500 Subject: [PATCH 0795/3044] Fix PyROS discrete separation iteration log --- .../contrib/pyros/pyros_algorithm_methods.py | 2 +- .../pyros/separation_problem_methods.py | 1 + pyomo/contrib/pyros/solve_data.py | 29 +++++++++++++++++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 45b652447ff..f0e32a284bb 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -805,7 +805,7 @@ def ROSolver_iterative_solve(model_data, config): len(scaled_violations) == len(separation_model.util.performance_constraints) and not separation_results.subsolver_error and not separation_results.time_out - ) + ) or separation_results.all_discrete_scenarios_exhausted iter_log_record = IterationLogRecord( iteration=k, diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index 084b0442ae6..b5939ff5b19 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -649,6 +649,7 @@ def perform_separation_loop(model_data, config, solve_globally): solver_call_results=ComponentMap(), solved_globally=solve_globally, worst_case_perf_con=None, + all_discrete_scenarios_exhausted=True, ) perf_con_to_maximize = sorted_priority_groups[ diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index bc6c071c9a3..c31eb8e5d3f 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -347,16 +347,23 @@ class SeparationLoopResults: solver_call_results : ComponentMap Mapping from performance constraints to corresponding ``SeparationSolveCallResults`` objects. - worst_case_perf_con : None or int, optional + worst_case_perf_con : None or Constraint Performance constraint mapped to ``SeparationSolveCallResults`` object in `self` corresponding to maximally violating separation problem solution. + all_discrete_scenarios_exhausted : bool, optional + For problems with discrete uncertainty sets, + True if all scenarios were explicitly accounted for in master + (which occurs if there have been + as many PyROS iterations as there are scenarios in the set) + False otherwise. Attributes ---------- solver_call_results solved_globally worst_case_perf_con + all_discrete_scenarios_exhausted found_violation violating_param_realization scaled_violations @@ -365,11 +372,18 @@ class SeparationLoopResults: time_out """ - def __init__(self, solved_globally, solver_call_results, worst_case_perf_con): + def __init__( + self, + solved_globally, + solver_call_results, + worst_case_perf_con, + all_discrete_scenarios_exhausted=False, + ): """Initialize self (see class docstring).""" self.solver_call_results = solver_call_results self.solved_globally = solved_globally self.worst_case_perf_con = worst_case_perf_con + self.all_discrete_scenarios_exhausted = all_discrete_scenarios_exhausted @property def found_violation(self): @@ -599,6 +613,17 @@ def get_violating_attr(self, attr_name): """ return getattr(self.main_loop_results, attr_name, None) + @property + def all_discrete_scenarios_exhausted(self): + """ + bool : For problems where the uncertainty set is of type + DiscreteScenarioSet, + True if last master problem solved explicitly + accounts for all scenarios in the uncertainty set, + False otherwise. + """ + return self.get_violating_attr("all_discrete_scenarios_exhausted") + @property def worst_case_perf_con(self): """ From 782c4ec093e95b14cdf4ca4aaa2a95ca5a0302b5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 23:45:04 -0700 Subject: [PATCH 0796/3044] Add test guards for pint availability --- pyomo/core/tests/unit/test_numvalue.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/core/tests/unit/test_numvalue.py b/pyomo/core/tests/unit/test_numvalue.py index 2dca2df56a6..442d5bc1a6c 100644 --- a/pyomo/core/tests/unit/test_numvalue.py +++ b/pyomo/core/tests/unit/test_numvalue.py @@ -18,6 +18,7 @@ import pyomo.common.unittest as unittest from pyomo.common.dependencies import numpy, numpy_available +from pyomo.core.base.units_container import pint_available from pyomo.environ import ( value, @@ -640,8 +641,9 @@ def _tester(expr): _tester('Var() + ref') _tester('v = Var(); v.construct(); v.value = ref') _tester('p = Param(mutable=True); p.construct(); p.value = ref') - _tester('v = Var(units=units.m); v.construct(); v.value = ref') - _tester('p = Param(mutable=True, units=units.m); p.construct(); p.value = ref') + if pint_available: + _tester('v = Var(units=units.m); v.construct(); v.value = ref') + _tester('p = Param(mutable=True, units=units.m); p.construct(); p.value = ref') if __name__ == "__main__": From e46d2b193a25c321d118ae1474788f5119a155e1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Feb 2024 23:56:19 -0700 Subject: [PATCH 0797/3044] Set maxDiff=None on the base TestCase class --- pyomo/common/tests/test_config.py | 6 ------ pyomo/common/tests/test_log.py | 1 - pyomo/common/tests/test_timing.py | 4 ---- pyomo/common/unittest.py | 4 ++++ pyomo/core/tests/unit/test_block.py | 1 - pyomo/core/tests/unit/test_numeric_expr.py | 1 - pyomo/core/tests/unit/test_reference.py | 2 -- pyomo/core/tests/unit/test_set.py | 1 - pyomo/repn/tests/ampl/test_nlv2.py | 1 - 9 files changed, 4 insertions(+), 17 deletions(-) diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 12657481764..a47f5e0d8af 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -2098,7 +2098,6 @@ def test_generate_custom_documentation(self): "generate_documentation is deprecated.", LOG, ) - self.maxDiff = None # print(test) self.assertEqual(test, reference) @@ -2113,7 +2112,6 @@ def test_generate_custom_documentation(self): ) ) self.assertEqual(LOG.getvalue(), "") - self.maxDiff = None # print(test) self.assertEqual(test, reference) @@ -2159,7 +2157,6 @@ def test_generate_custom_documentation(self): "generate_documentation is deprecated.", LOG, ) - self.maxDiff = None # print(test) self.assertEqual(test, reference) @@ -2577,7 +2574,6 @@ def test_argparse_help_implicit_disable(self): parser = argparse.ArgumentParser(prog='tester') self.config.initialize_argparse(parser) help = parser.format_help() - self.maxDiff = None self.assertIn( """ -h, --help show this help message and exit @@ -3106,8 +3102,6 @@ def test_declare_from(self): cfg2.declare_from({}) def test_docstring_decorator(self): - self.maxDiff = None - @document_kwargs_from_configdict('CONFIG') class ExampleClass(object): CONFIG = ExampleConfig() diff --git a/pyomo/common/tests/test_log.py b/pyomo/common/tests/test_log.py index 64691c0015a..166e1e44cdb 100644 --- a/pyomo/common/tests/test_log.py +++ b/pyomo/common/tests/test_log.py @@ -511,7 +511,6 @@ def test_verbatim(self): "\n" " quote block\n" ) - self.maxDiff = None self.assertEqual(self.stream.getvalue(), ans) diff --git a/pyomo/common/tests/test_timing.py b/pyomo/common/tests/test_timing.py index 0a4224c5476..48288746882 100644 --- a/pyomo/common/tests/test_timing.py +++ b/pyomo/common/tests/test_timing.py @@ -107,7 +107,6 @@ def test_report_timing(self): m.y = Var(Any, dense=False) xfrm.apply_to(m) result = out.getvalue().strip() - self.maxDiff = None for l, r in zip(result.splitlines(), ref.splitlines()): self.assertRegex(str(l.strip()), str(r.strip())) finally: @@ -122,7 +121,6 @@ def test_report_timing(self): m.y = Var(Any, dense=False) xfrm.apply_to(m) result = os.getvalue().strip() - self.maxDiff = None for l, r in zip(result.splitlines(), ref.splitlines()): self.assertRegex(str(l.strip()), str(r.strip())) finally: @@ -135,7 +133,6 @@ def test_report_timing(self): m.y = Var(Any, dense=False) xfrm.apply_to(m) result = os.getvalue().strip() - self.maxDiff = None for l, r in zip(result.splitlines(), ref.splitlines()): self.assertRegex(str(l.strip()), str(r.strip())) self.assertEqual(buf.getvalue().strip(), "") @@ -172,7 +169,6 @@ def test_report_timing_context_manager(self): xfrm.apply_to(m) self.assertEqual(OUT.getvalue(), "") result = OS.getvalue().strip() - self.maxDiff = None for l, r in zip_longest(result.splitlines(), ref.splitlines()): self.assertRegex(str(l.strip()), str(r.strip())) # Active reporting is False: the previous log should not have changed diff --git a/pyomo/common/unittest.py b/pyomo/common/unittest.py index 9a21b35faa8..9ee7731bda4 100644 --- a/pyomo/common/unittest.py +++ b/pyomo/common/unittest.py @@ -498,6 +498,10 @@ class TestCase(_unittest.TestCase): __doc__ += _unittest.TestCase.__doc__ + # By default, we always want to spend the time to create the full + # diff of the test reault and the baseline + maxDiff = None + def assertStructuredAlmostEqual( self, first, diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index 88646643703..71e80d90a73 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -2667,7 +2667,6 @@ def test_pprint(self): 5 Declarations: a1_IDX a3_IDX c a b """ - self.maxDiff = None self.assertEqual(ref, buf.getvalue()) @unittest.skipIf(not 'glpk' in solvers, "glpk solver is not available") diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index c073ee0f726..c1066c292d7 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -1424,7 +1424,6 @@ def test_sumOf_nestedTrivialProduct2(self): e1 = m.a * m.p e2 = m.b - m.c e = e2 - e1 - self.maxDiff = None self.assertExpressionsEqual( e, LinearExpression( diff --git a/pyomo/core/tests/unit/test_reference.py b/pyomo/core/tests/unit/test_reference.py index 287ff204f9e..cfd9b99f945 100644 --- a/pyomo/core/tests/unit/test_reference.py +++ b/pyomo/core/tests/unit/test_reference.py @@ -1280,7 +1280,6 @@ def test_contains_with_nonflattened(self): normalize_index.flatten = _old_flatten def test_pprint_nonfinite_sets(self): - self.maxDiff = None m = ConcreteModel() m.v = Var(NonNegativeIntegers, dense=False) m.ref = Reference(m.v) @@ -1322,7 +1321,6 @@ def test_pprint_nonfinite_sets(self): def test_pprint_nonfinite_sets_ctypeNone(self): # test issue #2039 - self.maxDiff = None m = ConcreteModel() m.v = Var(NonNegativeIntegers, dense=False) m.ref = Reference(m.v, ctype=None) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 1ad08ba025c..4bbac6ecaa0 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -6267,7 +6267,6 @@ def test_issue_835(self): @unittest.skipIf(NamedTuple is None, "typing module not available") def test_issue_938(self): - self.maxDiff = None NodeKey = NamedTuple('NodeKey', [('id', int)]) ArcKey = NamedTuple('ArcKey', [('node_from', NodeKey), ('node_to', NodeKey)]) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 215715dba10..86eb43d9a37 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -1096,7 +1096,6 @@ def test_log_timing(self): m.c1 = Constraint([1, 2], rule=lambda m, i: sum(m.x.values()) == 1) m.c2 = Constraint(expr=m.p * m.x[1] ** 2 + m.x[2] ** 3 <= 100) - self.maxDiff = None OUT = io.StringIO() with capture_output() as LOG: with report_timing(level=logging.DEBUG): From 66696b33dd17ae61b02b729af24da7ee0cc0164a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 28 Feb 2024 00:40:15 -0700 Subject: [PATCH 0798/3044] Add tests for native type set registration --- pyomo/common/tests/test_numeric_types.py | 219 +++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 pyomo/common/tests/test_numeric_types.py diff --git a/pyomo/common/tests/test_numeric_types.py b/pyomo/common/tests/test_numeric_types.py new file mode 100644 index 00000000000..a6570b7440e --- /dev/null +++ b/pyomo/common/tests/test_numeric_types.py @@ -0,0 +1,219 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.numeric_types as nt +import pyomo.common.unittest as unittest + +from pyomo.common.dependencies import numpy, numpy_available +from pyomo.core.expr import LinearExpression +from pyomo.environ import Var + +_type_sets = ( + 'native_types', + 'native_numeric_types', + 'native_logical_types', + 'native_integer_types', + 'native_complex_types', +) + + +class TestNativeTypes(unittest.TestCase): + def setUp(self): + bool(numpy_available) + for s in _type_sets: + setattr(self, s, set(getattr(nt, s))) + getattr(nt, s).clear() + + def tearDown(self): + for s in _type_sets: + getattr(nt, s).clear() + getattr(nt, s).update(getattr(nt, s)) + + def test_check_if_native_type(self): + self.assertEqual(nt.native_types, set()) + self.assertEqual(nt.native_logical_types, set()) + self.assertEqual(nt.native_numeric_types, set()) + self.assertEqual(nt.native_integer_types, set()) + self.assertEqual(nt.native_complex_types, set()) + + self.assertTrue(nt.check_if_native_type("a")) + self.assertIn(str, nt.native_types) + self.assertNotIn(str, nt.native_logical_types) + self.assertNotIn(str, nt.native_numeric_types) + self.assertNotIn(str, nt.native_integer_types) + self.assertNotIn(str, nt.native_complex_types) + + self.assertTrue(nt.check_if_native_type(1)) + self.assertIn(int, nt.native_types) + self.assertNotIn(int, nt.native_logical_types) + self.assertIn(int, nt.native_numeric_types) + self.assertIn(int, nt.native_integer_types) + self.assertNotIn(int, nt.native_complex_types) + + self.assertTrue(nt.check_if_native_type(1.5)) + self.assertIn(float, nt.native_types) + self.assertNotIn(float, nt.native_logical_types) + self.assertIn(float, nt.native_numeric_types) + self.assertNotIn(float, nt.native_integer_types) + self.assertNotIn(float, nt.native_complex_types) + + self.assertTrue(nt.check_if_native_type(True)) + self.assertIn(bool, nt.native_types) + self.assertIn(bool, nt.native_logical_types) + self.assertNotIn(bool, nt.native_numeric_types) + self.assertNotIn(bool, nt.native_integer_types) + self.assertNotIn(bool, nt.native_complex_types) + + self.assertFalse(nt.check_if_native_type(slice(None, None, None))) + self.assertNotIn(slice, nt.native_types) + self.assertNotIn(slice, nt.native_logical_types) + self.assertNotIn(slice, nt.native_numeric_types) + self.assertNotIn(slice, nt.native_integer_types) + self.assertNotIn(slice, nt.native_complex_types) + + def test_check_if_logical_type(self): + self.assertEqual(nt.native_types, set()) + self.assertEqual(nt.native_logical_types, set()) + self.assertEqual(nt.native_numeric_types, set()) + self.assertEqual(nt.native_integer_types, set()) + self.assertEqual(nt.native_complex_types, set()) + + self.assertFalse(nt.check_if_logical_type("a")) + self.assertNotIn(str, nt.native_types) + self.assertNotIn(str, nt.native_logical_types) + self.assertNotIn(str, nt.native_numeric_types) + self.assertNotIn(str, nt.native_integer_types) + self.assertNotIn(str, nt.native_complex_types) + + self.assertFalse(nt.check_if_logical_type("a")) + + self.assertTrue(nt.check_if_logical_type(True)) + self.assertIn(bool, nt.native_types) + self.assertIn(bool, nt.native_logical_types) + self.assertNotIn(bool, nt.native_numeric_types) + self.assertNotIn(bool, nt.native_integer_types) + self.assertNotIn(bool, nt.native_complex_types) + + self.assertTrue(nt.check_if_logical_type(True)) + + self.assertFalse(nt.check_if_logical_type(1)) + self.assertNotIn(int, nt.native_types) + self.assertNotIn(int, nt.native_logical_types) + self.assertNotIn(int, nt.native_numeric_types) + self.assertNotIn(int, nt.native_integer_types) + self.assertNotIn(int, nt.native_complex_types) + + if numpy_available: + self.assertTrue(nt.check_if_logical_type(numpy.bool_(1))) + self.assertIn(numpy.bool_, nt.native_types) + self.assertIn(numpy.bool_, nt.native_logical_types) + self.assertNotIn(numpy.bool_, nt.native_numeric_types) + self.assertNotIn(numpy.bool_, nt.native_integer_types) + self.assertNotIn(numpy.bool_, nt.native_complex_types) + + def test_check_if_numeric_type(self): + self.assertEqual(nt.native_types, set()) + self.assertEqual(nt.native_logical_types, set()) + self.assertEqual(nt.native_numeric_types, set()) + self.assertEqual(nt.native_integer_types, set()) + self.assertEqual(nt.native_complex_types, set()) + + self.assertFalse(nt.check_if_numeric_type("a")) + self.assertFalse(nt.check_if_numeric_type("a")) + self.assertNotIn(str, nt.native_types) + self.assertNotIn(str, nt.native_logical_types) + self.assertNotIn(str, nt.native_numeric_types) + self.assertNotIn(str, nt.native_integer_types) + self.assertNotIn(str, nt.native_complex_types) + + self.assertFalse(nt.check_if_numeric_type(True)) + self.assertFalse(nt.check_if_numeric_type(True)) + self.assertNotIn(bool, nt.native_types) + self.assertNotIn(bool, nt.native_logical_types) + self.assertNotIn(bool, nt.native_numeric_types) + self.assertNotIn(bool, nt.native_integer_types) + self.assertNotIn(bool, nt.native_complex_types) + + self.assertTrue(nt.check_if_numeric_type(1)) + self.assertTrue(nt.check_if_numeric_type(1)) + self.assertIn(int, nt.native_types) + self.assertNotIn(int, nt.native_logical_types) + self.assertIn(int, nt.native_numeric_types) + self.assertIn(int, nt.native_integer_types) + self.assertNotIn(int, nt.native_complex_types) + + self.assertTrue(nt.check_if_numeric_type(1.5)) + self.assertTrue(nt.check_if_numeric_type(1.5)) + self.assertIn(float, nt.native_types) + self.assertNotIn(float, nt.native_logical_types) + self.assertIn(float, nt.native_numeric_types) + self.assertNotIn(float, nt.native_integer_types) + self.assertNotIn(float, nt.native_complex_types) + + self.assertFalse(nt.check_if_numeric_type(1j)) + self.assertIn(complex, nt.native_types) + self.assertNotIn(complex, nt.native_logical_types) + self.assertNotIn(complex, nt.native_numeric_types) + self.assertNotIn(complex, nt.native_integer_types) + self.assertIn(complex, nt.native_complex_types) + + v = Var() + v.construct() + self.assertFalse(nt.check_if_numeric_type(v)) + self.assertNotIn(type(v), nt.native_types) + self.assertNotIn(type(v), nt.native_logical_types) + self.assertNotIn(type(v), nt.native_numeric_types) + self.assertNotIn(type(v), nt.native_integer_types) + self.assertNotIn(type(v), nt.native_complex_types) + + e = LinearExpression([1]) + self.assertFalse(nt.check_if_numeric_type(e)) + self.assertNotIn(type(e), nt.native_types) + self.assertNotIn(type(e), nt.native_logical_types) + self.assertNotIn(type(e), nt.native_numeric_types) + self.assertNotIn(type(e), nt.native_integer_types) + self.assertNotIn(type(e), nt.native_complex_types) + + if numpy_available: + self.assertFalse(nt.check_if_numeric_type(numpy.bool_(1))) + self.assertNotIn(numpy.bool_, nt.native_types) + self.assertNotIn(numpy.bool_, nt.native_logical_types) + self.assertNotIn(numpy.bool_, nt.native_numeric_types) + self.assertNotIn(numpy.bool_, nt.native_integer_types) + self.assertNotIn(numpy.bool_, nt.native_complex_types) + + self.assertFalse(nt.check_if_numeric_type(numpy.array([1]))) + self.assertNotIn(numpy.ndarray, nt.native_types) + self.assertNotIn(numpy.ndarray, nt.native_logical_types) + self.assertNotIn(numpy.ndarray, nt.native_numeric_types) + self.assertNotIn(numpy.ndarray, nt.native_integer_types) + self.assertNotIn(numpy.ndarray, nt.native_complex_types) + + self.assertTrue(nt.check_if_numeric_type(numpy.float64(1))) + self.assertIn(numpy.float64, nt.native_types) + self.assertNotIn(numpy.float64, nt.native_logical_types) + self.assertIn(numpy.float64, nt.native_numeric_types) + self.assertNotIn(numpy.float64, nt.native_integer_types) + self.assertNotIn(numpy.float64, nt.native_complex_types) + + self.assertTrue(nt.check_if_numeric_type(numpy.int64(1))) + self.assertIn(numpy.int64, nt.native_types) + self.assertNotIn(numpy.int64, nt.native_logical_types) + self.assertIn(numpy.int64, nt.native_numeric_types) + self.assertIn(numpy.int64, nt.native_integer_types) + self.assertNotIn(numpy.int64, nt.native_complex_types) + + self.assertFalse(nt.check_if_numeric_type(numpy.complex128(1))) + self.assertIn(numpy.complex128, nt.native_types) + self.assertNotIn(numpy.complex128, nt.native_logical_types) + self.assertNotIn(numpy.complex128, nt.native_numeric_types) + self.assertNotIn(numpy.complex128, nt.native_integer_types) + self.assertIn(numpy.complex128, nt.native_complex_types) From 5b6cf69c862e8a97605eb272561e60b73ae640f1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 28 Feb 2024 00:43:22 -0700 Subject: [PATCH 0799/3044] NFC: apply black --- pyomo/core/tests/unit/test_numvalue.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/core/tests/unit/test_numvalue.py b/pyomo/core/tests/unit/test_numvalue.py index 442d5bc1a6c..1cccd3863ea 100644 --- a/pyomo/core/tests/unit/test_numvalue.py +++ b/pyomo/core/tests/unit/test_numvalue.py @@ -643,7 +643,9 @@ def _tester(expr): _tester('p = Param(mutable=True); p.construct(); p.value = ref') if pint_available: _tester('v = Var(units=units.m); v.construct(); v.value = ref') - _tester('p = Param(mutable=True, units=units.m); p.construct(); p.value = ref') + _tester( + 'p = Param(mutable=True, units=units.m); p.construct(); p.value = ref' + ) if __name__ == "__main__": From 1a347bf7fea5430cd1041e408f4b66cdcc874e68 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 28 Feb 2024 00:52:46 -0700 Subject: [PATCH 0800/3044] Fix typo restoring state after test --- pyomo/common/tests/test_numeric_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/tests/test_numeric_types.py b/pyomo/common/tests/test_numeric_types.py index a6570b7440e..b7ffb5fb255 100644 --- a/pyomo/common/tests/test_numeric_types.py +++ b/pyomo/common/tests/test_numeric_types.py @@ -35,7 +35,7 @@ def setUp(self): def tearDown(self): for s in _type_sets: getattr(nt, s).clear() - getattr(nt, s).update(getattr(nt, s)) + getattr(nt, s).update(getattr(self, s)) def test_check_if_native_type(self): self.assertEqual(nt.native_types, set()) From caa688ed390e609b78ff3af305f87223dd3a69e6 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:12:00 +0100 Subject: [PATCH 0801/3044] Initial point calculation consistent with ALE syntax --- pyomo/contrib/appsi/solvers/maingo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index dcb8040eabe..530521f6b83 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -146,7 +146,7 @@ def get_variables(self): ] def get_initial_point(self): - return [var.init if not var.init is None else var.lb for var in self._var_list] + return [var.init if not var.init is None else (var.lb + var.ub)/2.0 for var in self._var_list] def evaluate(self, maingo_vars): visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) From 365370a4220ff2fe0df818878f89b5f6fe36d7a3 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:24:17 +0100 Subject: [PATCH 0802/3044] Added warning for missing variable bounds --- pyomo/contrib/appsi/solvers/maingo.py | 35 ++++++++++++++++++--------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 530521f6b83..52b12d434ef 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -436,10 +436,28 @@ def solve(self, model, timer: HierarchicalTimer = None): def _process_domain_and_bounds(self, var): _v, _lb, _ub, _fixed, _domain_interval, _value = self._vars[id(var)] lb, ub, step = _domain_interval - if lb is None: - lb = -1e10 - if ub is None: - ub = 1e10 + + if _fixed: + lb = _value + ub = _value + else: + if lb is None and _lb is None: + logger.warning("No lower bound for variable " + var.getname() + " set. Using -1e10 instead. Please consider setting a valid lower bound.") + if ub is None and _ub is None: + logger.warning("No upper bound for variable " + var.getname() + " set. Using +1e10 instead. Please consider setting a valid upper bound.") + + if _lb is None: + _lb = -1e10 + if _ub is None: + _ub = 1e10 + if lb is None: + lb = -1e10 + if ub is None: + ub = 1e10 + + lb = max(value(_lb), lb) + ub = min(value(_ub), ub) + if step == 0: vtype = maingopy.VT_CONTINUOUS elif step == 1: @@ -451,14 +469,7 @@ def _process_domain_and_bounds(self, var): raise ValueError( f"Unrecognized domain step: {step} (should be either 0 or 1)" ) - if _fixed: - lb = _value - ub = _value - else: - if _lb is not None: - lb = max(value(_lb), lb) - if _ub is not None: - ub = min(value(_ub), ub) + return lb, ub, vtype From c440037d95146caaa896404235ebd2b60f8d8926 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:32:55 +0100 Subject: [PATCH 0803/3044] Added: NotImplementedError for SOS constraints --- pyomo/contrib/appsi/solvers/maingo.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 52b12d434ef..99cff4b7aa9 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -525,12 +525,16 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): self._cons = cons def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + if len(cons) >= 1: + raise NotImplementedError("MAiNGO does not currently support SOS constraints.") pass def _remove_constraints(self, cons: List[_GeneralConstraintData]): pass def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + if len(cons) >= 1: + raise NotImplementedError("MAiNGO does not currently support SOS constraints.") pass def _remove_variables(self, variables: List[_GeneralVarData]): From 52a4cd9e59baf27bdab4df32b3a850d511ce894b Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:39:28 +0100 Subject: [PATCH 0804/3044] Changed: Formulation of asinh, acosh, atanh --- pyomo/contrib/appsi/solvers/maingo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 99cff4b7aa9..099865f5a84 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -192,15 +192,15 @@ def maingo_log10(cls, x): @classmethod def maingo_asinh(cls, x): - return maingopy.inv(maingopy.sinh(x)) + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x,2) + 1)) @classmethod def maingo_acosh(cls, x): - return maingopy.inv(maingopy.cosh(x)) + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x,2) - 1)) @classmethod def maingo_atanh(cls, x): - return maingopy.inv(maingopy.tanh(x)) + return 0.5 * maingopy.log(x+1) - 0.5 * maingopy.log(1-x) def visit(self, node, values): """Visit nodes that have been expanded""" From 44311203d1b426a0894e8f9c7efbdbfa3c0eec85 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:49:15 +0100 Subject: [PATCH 0805/3044] Added: Warning for non-global solutions --- pyomo/contrib/appsi/solvers/maingo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 099865f5a84..7a153f938b7 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -570,8 +570,10 @@ def _postsolve(self, timer: HierarchicalTimer): results.wallclock_time = mprob.get_wallclock_solution_time() results.cpu_time = mprob.get_cpu_solution_time() - if status == maingopy.GLOBALLY_OPTIMAL: + if status in {maingopy.GLOBALLY_OPTIMAL, maingopy.FEASIBLE_POINT}: results.termination_condition = TerminationCondition.optimal + if status == maingopy.FEASIBLE_POINT: + logger.warning("MAiNGO did only find a feasible solution but did not prove its global optimality.") elif status == maingopy.INFEASIBLE: results.termination_condition = TerminationCondition.infeasible else: From ff2c9bd86eefe7b2693f60a165383b525a764984 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:57:01 +0100 Subject: [PATCH 0806/3044] Changed: absolute to relative MIP gap --- pyomo/contrib/appsi/solvers/maingo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 7a153f938b7..cf09b42fbda 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -407,7 +407,7 @@ def _solve(self, timer: HierarchicalTimer): if config.time_limit is not None: self._mymaingo.set_option("maxTime", config.time_limit) if config.mip_gap is not None: - self._mymaingo.set_option("epsilonA", config.mip_gap) + self._mymaingo.set_option("epsilonR", config.mip_gap) for key, option in options.items(): self._mymaingo.set_option(key, option) From b833ac729aa7741331811c133d54d809f27e17be Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 12:11:48 +0100 Subject: [PATCH 0807/3044] Added: Maingopy version --- pyomo/contrib/appsi/solvers/maingo.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index cf09b42fbda..05c5f50a295 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -354,7 +354,15 @@ def available(self): return self._available def version(self): - pass + # Check if Python >= 3.8 + if sys.version_info.major >= 3 and sys.version_info.minor >= 8: + from importlib.metadata import version + version = version('maingopy') + else: + import pkg_resources + version = pkg_resources.get_distribution('maingopy').version + + return tuple(int(k) for k in version.split('.')) @property def config(self) -> MAiNGOConfig: From c937b63e2e6e699efe24ef0e46cd36fd56da8134 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 12:21:12 +0100 Subject: [PATCH 0808/3044] Black Formatting --- pyomo/contrib/appsi/solvers/__init__.py | 2 +- pyomo/contrib/appsi/solvers/maingo.py | 42 ++++++++++++++++++------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/__init__.py b/pyomo/contrib/appsi/solvers/__init__.py index c9e0a2a003d..352571b98f8 100644 --- a/pyomo/contrib/appsi/solvers/__init__.py +++ b/pyomo/contrib/appsi/solvers/__init__.py @@ -15,4 +15,4 @@ from .cplex import Cplex from .highs import Highs from .wntr import Wntr, WntrResults -from .maingo import MAiNGO \ No newline at end of file +from .maingo import MAiNGO diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 05c5f50a295..d98b33af998 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -146,7 +146,10 @@ def get_variables(self): ] def get_initial_point(self): - return [var.init if not var.init is None else (var.lb + var.ub)/2.0 for var in self._var_list] + return [ + var.init if not var.init is None else (var.lb + var.ub) / 2.0 + for var in self._var_list + ] def evaluate(self, maingo_vars): visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) @@ -192,15 +195,15 @@ def maingo_log10(cls, x): @classmethod def maingo_asinh(cls, x): - return maingopy.log(x + maingopy.sqrt(maingopy.pow(x,2) + 1)) + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) + 1)) @classmethod def maingo_acosh(cls, x): - return maingopy.log(x + maingopy.sqrt(maingopy.pow(x,2) - 1)) + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) - 1)) @classmethod def maingo_atanh(cls, x): - return 0.5 * maingopy.log(x+1) - 0.5 * maingopy.log(1-x) + return 0.5 * maingopy.log(x + 1) - 0.5 * maingopy.log(1 - x) def visit(self, node, values): """Visit nodes that have been expanded""" @@ -357,11 +360,13 @@ def version(self): # Check if Python >= 3.8 if sys.version_info.major >= 3 and sys.version_info.minor >= 8: from importlib.metadata import version + version = version('maingopy') else: import pkg_resources + version = pkg_resources.get_distribution('maingopy').version - + return tuple(int(k) for k in version.split('.')) @property @@ -450,10 +455,18 @@ def _process_domain_and_bounds(self, var): ub = _value else: if lb is None and _lb is None: - logger.warning("No lower bound for variable " + var.getname() + " set. Using -1e10 instead. Please consider setting a valid lower bound.") + logger.warning( + "No lower bound for variable " + + var.getname() + + " set. Using -1e10 instead. Please consider setting a valid lower bound." + ) if ub is None and _ub is None: - logger.warning("No upper bound for variable " + var.getname() + " set. Using +1e10 instead. Please consider setting a valid upper bound.") - + logger.warning( + "No upper bound for variable " + + var.getname() + + " set. Using +1e10 instead. Please consider setting a valid upper bound." + ) + if _lb is None: _lb = -1e10 if _ub is None: @@ -478,7 +491,6 @@ def _process_domain_and_bounds(self, var): f"Unrecognized domain step: {step} (should be either 0 or 1)" ) - return lb, ub, vtype def _add_variables(self, variables: List[_GeneralVarData]): @@ -534,7 +546,9 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): def _add_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) >= 1: - raise NotImplementedError("MAiNGO does not currently support SOS constraints.") + raise NotImplementedError( + "MAiNGO does not currently support SOS constraints." + ) pass def _remove_constraints(self, cons: List[_GeneralConstraintData]): @@ -542,7 +556,9 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) >= 1: - raise NotImplementedError("MAiNGO does not currently support SOS constraints.") + raise NotImplementedError( + "MAiNGO does not currently support SOS constraints." + ) pass def _remove_variables(self, variables: List[_GeneralVarData]): @@ -581,7 +597,9 @@ def _postsolve(self, timer: HierarchicalTimer): if status in {maingopy.GLOBALLY_OPTIMAL, maingopy.FEASIBLE_POINT}: results.termination_condition = TerminationCondition.optimal if status == maingopy.FEASIBLE_POINT: - logger.warning("MAiNGO did only find a feasible solution but did not prove its global optimality.") + logger.warning( + "MAiNGO did only find a feasible solution but did not prove its global optimality." + ) elif status == maingopy.INFEASIBLE: results.termination_condition = TerminationCondition.infeasible else: From ccb7723466f0bb06eb3abf552c44c084932704b3 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 12:44:02 +0100 Subject: [PATCH 0809/3044] Fixed: Black Formatting --- pyomo/contrib/appsi/solvers/maingo.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index d98b33af998..614e12d227b 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -318,12 +318,14 @@ def _monomial_to_maingo(self, node): def _linear_to_maingo(self, node): values = [ - self._monomial_to_maingo(arg) - if ( - arg.__class__ is EXPR.MonomialTermExpression - and not arg.arg(1).is_fixed() + ( + self._monomial_to_maingo(arg) + if ( + arg.__class__ is EXPR.MonomialTermExpression + and not arg.arg(1).is_fixed() + ) + else value(arg) ) - else value(arg) for arg in node.args ] return sum(values) From e28e14db71b7ee09a54af2882d80837289f5c448 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 14:33:06 +0100 Subject: [PATCH 0810/3044] Added: pip install maingopy to test_branches.yml --- .github/workflows/test_branches.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 77f47b505ff..1441cd53623 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -268,6 +268,8 @@ jobs: || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" + python -m pip install --cache-dir cache/pip maingopy \ + || echo "WARNING: MAiNGO is not available" if [[ ${{matrix.python}} == pypy* ]]; then echo "skipping wntr for pypy" else From c52bbc7f3c565d5d50ff98187efdb8d25de32e4b Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 14:35:41 +0100 Subject: [PATCH 0811/3044] Added: pip install maingopy to test_pr_and_main.yml --- .github/workflows/test_pr_and_main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 87d6aa4d7a8..0214442d4e5 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -298,6 +298,8 @@ jobs: || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" + python -m pip install --cache-dir cache/pip maingopy \ + || echo "WARNING: MAiNGO is not available" if [[ ${{matrix.python}} == pypy* ]]; then echo "skipping wntr for pypy" else From 84ca52464286faeb95fd5edda052b4d3459f021c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 28 Feb 2024 08:35:37 -0700 Subject: [PATCH 0812/3044] NFC: fix comment typo --- pyomo/common/dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index edf32baa6d6..895759a8a2c 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -513,7 +513,7 @@ def invalidate_caches(self): _DeferredImportCallbackFinder = DeferredImportCallbackFinder() # Insert the DeferredImportCallbackFinder at the beginning of the -# sys.meta_path to that it is found before the standard finders (so that +# sys.meta_path so that it is found before the standard finders (so that # we can correctly inject the resolution of the DeferredImportIndicators # -- which triggers the needed callbacks) sys.meta_path.insert(0, _DeferredImportCallbackFinder) From de86a6aa93374baf6e0f6147a13421c9ef45c49e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 28 Feb 2024 09:03:49 -0700 Subject: [PATCH 0813/3044] check_if_logical_type(): expand Boolean tests, relax cast-from-int requirement --- pyomo/common/numeric_types.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index 616d4c4bae4..9d4adc12e22 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -229,13 +229,32 @@ def check_if_logical_type(obj): return obj_class in native_logical_types try: + # It is not an error if you can't initialize the type from an + # int, but if you can, it should map !0 to True + if obj_class(1) != obj_class(2): + return False + except: + pass + + try: + # Native logical types *must* be hashable + hash(obj) + # Native logical types must honor standard Boolean operators if all( ( - obj_class(1) == obj_class(2), obj_class(False) != obj_class(True), + obj_class(False) ^ obj_class(False) == obj_class(False), obj_class(False) ^ obj_class(True) == obj_class(True), + obj_class(True) ^ obj_class(False) == obj_class(True), + obj_class(True) ^ obj_class(True) == obj_class(False), + obj_class(False) | obj_class(False) == obj_class(False), obj_class(False) | obj_class(True) == obj_class(True), + obj_class(True) | obj_class(False) == obj_class(True), + obj_class(True) | obj_class(True) == obj_class(True), + obj_class(False) & obj_class(False) == obj_class(False), obj_class(False) & obj_class(True) == obj_class(False), + obj_class(True) & obj_class(False) == obj_class(False), + obj_class(True) & obj_class(True) == obj_class(True), ) ): RegisterLogicalType(obj_class) From e617a6e773c16ff840a4f22cd9751eaadf1ed132 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 28 Feb 2024 09:04:16 -0700 Subject: [PATCH 0814/3044] NFC: update comments/docstrings --- pyomo/common/numeric_types.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index 9d4adc12e22..a1fe1e7514e 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -217,10 +217,10 @@ def check_if_native_type(obj): def check_if_logical_type(obj): """Test if the argument behaves like a logical type. - We check for "numeric types" by checking if we can add zero to it - without changing the object's type, and that the object compares to - 0 in a meaningful way. If that works, then we register the type in - :py:attr:`native_numeric_types`. + We check for "logical types" by checking if the type returns sane + results for Boolean operators (``^``, ``|``, ``&``) and if it maps + ``1`` and ``2`` both to the same equivalent instance. If that + works, then we register the type in :py:attr:`native_logical_types`. """ obj_class = obj.__class__ @@ -304,9 +304,8 @@ def check_if_numeric_type(obj): except: pass # - # ensure that the object is comparable to 0 in a meaningful way - # (among other things, this prevents numpy.ndarray objects from - # being added to native_numeric_types) + # Ensure that the object is comparable to 0 in a meaningful way + # try: if not ((obj < 0) ^ (obj >= 0)): return False From a931ace7b198e4db00970ab0c009d3c4e5805959 Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 09:33:43 -0800 Subject: [PATCH 0815/3044] minor updates to datarec example --- .../reactor_design/datarec_example.py | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index 00287730c63..45f826f880d 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -20,15 +20,6 @@ np.random.seed(1234) -def reactor_design_model_for_datarec(): - - # Unfix inlet concentration for data rec - model = reactor_design_model() - model.caf.fixed = False - - return model - - class ReactorDesignExperimentPreDataRec(ReactorDesignExperiment): def __init__(self, data, data_std, experiment_number): @@ -37,7 +28,10 @@ def __init__(self, data, data_std, experiment_number): self.data_std = data_std def create_model(self): - self.model = m = reactor_design_model_for_datarec() + + self.model = m = reactor_design_model() + m.caf.fixed = False + return m def label_model(self): @@ -124,20 +118,15 @@ def main(): exp_list.append(ReactorDesignExperimentPreDataRec(data, data_std, i)) # Define sum of squared error objective function for data rec - def SSE(model): + def SSE_with_std(model): expr = sum( ((y - yhat) / model.experiment_outputs_std[y]) ** 2 for y, yhat in model.experiment_outputs.items() ) return expr - # View one model & SSE - # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) - # print(SSE(exp0_model)) - ### Data reconciliation - pest = parmest.Estimator(exp_list, obj_function=SSE) + pest = parmest.Estimator(exp_list, obj_function=SSE_with_std) obj, theta, data_rec = pest.theta_est(return_values=["ca", "cb", "cc", "cd", "caf"]) print(obj) @@ -157,7 +146,7 @@ def SSE(model): for i in range(data_rec.shape[0]): exp_list.append(ReactorDesignExperimentPostDataRec(data_rec, data_std, i)) - pest = parmest.Estimator(exp_list, obj_function=SSE) + pest = parmest.Estimator(exp_list, obj_function=SSE_with_std) obj, theta = pest.theta_est() print(obj) print(theta) From f5350a4f45982fb7661dd468d94df5bfc8880164 Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 09:34:28 -0800 Subject: [PATCH 0816/3044] Added API docs and made deprecated class private --- pyomo/contrib/parmest/parmest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ffe9afc059e..8f98ab5cd5a 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -276,6 +276,9 @@ def _experiment_instance_creation_callback( def SSE(model): + """ + Sum of squared error between `experiment_output` model and data values + """ expr = sum((y - yhat) ** 2 for y, yhat in model.experiment_outputs.items()) return expr @@ -330,7 +333,7 @@ def __init__(self, *args, **kwargs): + 'data, theta_names), please use experiment lists instead.', version=DEPRECATION_VERSION, ) - self.pest_deprecated = DeprecatedEstimator(*args, **kwargs) + self.pest_deprecated = _DeprecatedEstimator(*args, **kwargs) return # check that we have a (non-empty) list of experiments @@ -1477,7 +1480,7 @@ def __call__(self, model): return self._ssc_function(model, self._data) -class DeprecatedEstimator(object): +class _DeprecatedEstimator(object): """ Parameter estimation class From 1761879c34b93c8c6d573cdbdbd1527555d14817 Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 09:36:19 -0800 Subject: [PATCH 0817/3044] renamed class in datarec example --- .../parmest/examples/reactor_design/datarec_example.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index 45f826f880d..e05b69aa4cc 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -20,7 +20,7 @@ np.random.seed(1234) -class ReactorDesignExperimentPreDataRec(ReactorDesignExperiment): +class ReactorDesignExperimentDataRec(ReactorDesignExperiment): def __init__(self, data, data_std, experiment_number): @@ -115,7 +115,7 @@ def main(): # Create an experiment list exp_list = [] for i in range(data.shape[0]): - exp_list.append(ReactorDesignExperimentPreDataRec(data, data_std, i)) + exp_list.append(ReactorDesignExperimentDataRec(data, data_std, i)) # Define sum of squared error objective function for data rec def SSE_with_std(model): From 97469b24ed63d4b635952b36008656162486f11e Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 09:43:25 -0800 Subject: [PATCH 0818/3044] parmest doc updates --- .../contributed_packages/parmest/datarec.rst | 35 +++--- .../contributed_packages/parmest/driver.rst | 105 ++++++++---------- .../contributed_packages/parmest/examples.rst | 2 +- .../parmest/scencreate.rst | 2 +- 4 files changed, 60 insertions(+), 84 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/parmest/datarec.rst b/doc/OnlineDocs/contributed_packages/parmest/datarec.rst index 6e9be904286..2260450192c 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/datarec.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/datarec.rst @@ -3,35 +3,27 @@ Data Reconciliation ==================== -The method :class:`~pyomo.contrib.parmest.parmest.Estimator.theta_est` -can optionally return model values. This feature can be used to return -reconciled data using a user specified objective. In this case, the list -of variable names the user wants to estimate (theta_names) is set to an -empty list and the objective function is defined to minimize +The optional argument ``return_values`` in :class:`~pyomo.contrib.parmest.parmest.Estimator.theta_est` +can be used for data reconciliation or to return model values based on the specified objective. + +For data reconciliation, the ``m.unknown_parameters`` is empty +and the objective function is defined to minimize measurement to model error. Note that the model used for data reconciliation may differ from the model used for parameter estimation. -The following example illustrates the use of parmest for data -reconciliation. The functions +The functions :class:`~pyomo.contrib.parmest.graphics.grouped_boxplot` or :class:`~pyomo.contrib.parmest.graphics.grouped_violinplot` can be used to visually compare the original and reconciled data. -Here's a stylized code snippet showing how box plots might be created: - -.. doctest:: - :skipif: True - - >>> import pyomo.contrib.parmest.parmest as parmest - >>> pest = parmest.Estimator(model_function, data, [], objective_function) - >>> obj, theta, data_rec = pest.theta_est(return_values=['A', 'B']) - >>> parmest.graphics.grouped_boxplot(data, data_rec) +The following example from the reactor design subdirectory returns reconciled values for experiment outputs +(`ca`, `cb`, `cc`, and `cd`) and then uses those values in +parameter estimation (`k1`, `k2`, and `k3`). -Returned Values -^^^^^^^^^^^^^^^ - -Here's a full program that can be run to see returned values (in this case it -is the response function that is defined in the model file): +.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/reactor_design/datarec_example.py + :language: python + +The following example returns model values from a Pyomo Expression. .. doctest:: :skipif: not ipopt_available or not parmest_available @@ -60,4 +52,3 @@ is the response function that is defined in the model file): >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=None) >>> obj, theta, var_values = pest.theta_est(return_values=['response_function']) >>> #print(var_values) - diff --git a/doc/OnlineDocs/contributed_packages/parmest/driver.rst b/doc/OnlineDocs/contributed_packages/parmest/driver.rst index 45533e9520c..695cab36e93 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/driver.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/driver.rst @@ -4,7 +4,7 @@ Parameter Estimation ================================== Parameter Estimation using parmest requires a Pyomo model, experimental -data which defines multiple scenarios, and a list of parameter names +data which defines multiple scenarios, and parameters (thetas) to estimate. parmest uses Pyomo [PyomoBookII]_ and (optionally) mpi-sppy [mpisppy]_ to solve a two-stage stochastic programming problem, where the experimental data is @@ -36,8 +36,8 @@ which includes the following methods: ~pyomo.contrib.parmest.parmest.Estimator.likelihood_ratio_test ~pyomo.contrib.parmest.parmest.Estimator.leaveNout_bootstrap_test -Additional functions are available in parmest to group data, plot -results, and fit distributions to theta values. +Additional functions are available in parmest to plot +results and fit distributions to theta values. .. autosummary:: :nosignatures: @@ -92,65 +92,43 @@ Optionally, solver options can be supplied, e.g., >>> solver_options = {"max_iter": 6000} >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=solver_options) - - - -Model function --------------- - -The first argument is a function which uses data for a single scenario -to return a populated and initialized Pyomo model for that scenario. - -Parameters that the user would like to estimate can be defined as -**mutable parameters (Pyomo `Param`) or variables (Pyomo `Var`)**. -Within parmest, any parameters that are to be estimated are converted to unfixed variables. -Variables that are to be estimated are also unfixed. - -The model does not have to be specifically written as a -two-stage stochastic programming problem for parmest. -That is, parmest can modify the -objective, see :ref:`ObjFunction` below. - -Data ----- - -The second argument is the data which will be used to populate the Pyomo -model. Supported data formats include: - -* **Pandas Dataframe** where each row is a separate scenario and column - names refer to observed quantities. Pandas DataFrames are easily - stored and read in from csv, excel, or databases, or created directly - in Python. -* **List of Pandas Dataframe** where each entry in the list is a separate scenario. - Dataframes store observed quantities, referenced by index and column. -* **List of dictionaries** where each entry in the list is a separate - scenario and the keys (or nested keys) refer to observed quantities. - Dictionaries are often preferred over DataFrames when using static and - time series data. Dictionaries are easily stored and read in from - json or yaml files, or created directly in Python. -* **List of json file names** where each entry in the list contains a - json file name for a separate scenario. This format is recommended - when using large datasets in parallel computing. - -The data must be compatible with the model function that returns a -populated and initialized Pyomo model for a single scenario. Data can -include multiple entries per variable (time series and/or duplicate -sensors). This information can be included in custom objective -functions, see :ref:`ObjFunction` below. - -Theta names ------------ - -The third argument is a list of parameters or variable names that the user wants to -estimate. The list contains strings with `Param` and/or `Var` names from the Pyomo -model. + + +List of experiment objects +-------------------------- + +The first argument is a list of experiment objects which is used to +create one labeled model for each expeirment. +The template :class:`~pyomo.contrib.parmest.experiment.Experiment` +can be used to generate a list of experiment objects. + +A labeled Pyomo model ``m`` has the following additional suffixes (Pyomo `Suffix`): + +* ``m.experiment_outputs`` which defines experiment output (Pyomo `Param`, `Var`, or `Expression`) + and their associated data values (float, int). +* ``m.unknown_parameters`` which defines the mutable parameters or variables (Pyomo `Parm` or `Var`) + to estimate along with their component unique identifier (Pyomo `ComponentUID`). + Within parmest, any parameters that are to be estimated are converted to unfixed variables. + Variables that are to be estimated are also unfixed. + +The experiment class has one required method: + +* :class:`~pyomo.contrib.parmest.experiment.Experiment.get_labeled_model` which returns the labeled Pyomo model. + Note that the model does not have to be specifically written as a + two-stage stochastic programming problem for parmest. + That is, parmest can modify the + objective, see :ref:`ObjFunction` below. + +Parmest comes with several :ref:`examplesection` that illustrates how to set up the list of experiment objects. +The examples commonly include additional :class:`~pyomo.contrib.parmest.experiment.Experiment` class methods to +create the model, finalize the model, and label the model. The user can customize methods to suit their needs. .. _ObjFunction: Objective function ------------------ -The fourth argument is an optional argument which defines the +The second argument is an optional argument which defines the optimization objective function to use in parameter estimation. If no objective function is specified, the Pyomo model is used "as is" and @@ -161,20 +139,27 @@ stochastic programming problem. If the Pyomo model is not written as a two-stage stochastic programming problem in this format, and/or if the user wants to use an objective that is different than the original model, a custom objective function can be -defined for parameter estimation. The objective function arguments -include `model` and `data` and the objective function returns a Pyomo +defined for parameter estimation. The objective function has a single argument, +which is the model from a single experiment. +The objective function returns a Pyomo expression which is used to define "SecondStageCost". The objective function can be used to customize data points and weights that are used in parameter estimation. +Parmest includes one built in objective function to compute the sum of squared errors ("SSE") between the +``m.experiment_outputs`` model values and data values. + Suggested initialization procedure for parameter estimation problems -------------------------------------------------------------------- To check the quality of initial guess values provided for the fitted parameters, we suggest solving a square instance of the problem prior to solving the parameter estimation problem using the following steps: -1. Create :class:`~pyomo.contrib.parmest.parmest.Estimator` object. To initialize the parameter estimation solve from the square problem solution, set optional argument ``solver_options = {bound_push: 1e-8}``. +1. Create :class:`~pyomo.contrib.parmest.parmest.Estimator` object. To initialize the parameter +estimation solve from the square problem solution, set optional argument ``solver_options = {bound_push: 1e-8}``. -2. Call :class:`~pyomo.contrib.parmest.parmest.Estimator.objective_at_theta` with optional argument ``(initialize_parmest_model=True)``. Different initial guess values for the fitted parameters can be provided using optional argument `theta_values` (**Pandas Dataframe**) +2. Call :class:`~pyomo.contrib.parmest.parmest.Estimator.objective_at_theta` with optional +argument ``(initialize_parmest_model=True)``. Different initial guess values for the fitted +parameters can be provided using optional argument `theta_values` (**Pandas Dataframe**) 3. Solve parameter estimation problem by calling :class:`~pyomo.contrib.parmest.parmest.Estimator.theta_est` diff --git a/doc/OnlineDocs/contributed_packages/parmest/examples.rst b/doc/OnlineDocs/contributed_packages/parmest/examples.rst index 793ff3d0c8d..a59d79dfa2b 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/examples.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/examples.rst @@ -20,7 +20,7 @@ Additional use cases include: * Parameter estimation using mpi4py, the example saves results to a file for later analysis/graphics (semibatch example) -The description below uses the reactor design example. The file +The example below uses the reactor design example. The file **reactor_design.py** includes a function which returns an populated instance of the Pyomo model. Note that the model is defined to maximize `cb` and that `k1`, `k2`, and `k3` are fixed. The _main_ program is diff --git a/doc/OnlineDocs/contributed_packages/parmest/scencreate.rst b/doc/OnlineDocs/contributed_packages/parmest/scencreate.rst index 66d41d4c606..b63ac5893c2 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/scencreate.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/scencreate.rst @@ -18,5 +18,5 @@ scenarios to the screen, accessing them via the ``ScensItator`` a ``print`` :language: python .. note:: - This example may produce an error message your version of Ipopt is not based + This example may produce an error message if your version of Ipopt is not based on a good linear solver. From ba840aabf0c1281b68524cf11ed67ddbc940b89e Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 09:55:17 -0800 Subject: [PATCH 0819/3044] added header and updated API docs for Experiment --- pyomo/contrib/parmest/experiment.py | 30 +++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/parmest/experiment.py b/pyomo/contrib/parmest/experiment.py index e16ad304e42..69474a32bb0 100644 --- a/pyomo/contrib/parmest/experiment.py +++ b/pyomo/contrib/parmest/experiment.py @@ -1,14 +1,28 @@ -# The experiment class is a template for making experiment lists -# to pass to parmest. An experiment is a pyomo model "m" which has -# additional suffixes: -# m.experiment_outputs -- which variables are experiment outputs -# m.unknown_parameters -- which variables are parameters to estimate -# The experiment class has only one required method: -# get_labeled_model() -# which returns the labeled pyomo model. +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ class Experiment: + """ + The experiment class is a template for making experiment lists + to pass to parmest. + + An experiment is a Pyomo model "m" which is labeled + with additional suffixes: + * m.experiment_outputs which defines experiment outputs + * m.unknown_parameters which defines parameters to estimate + + The experiment class has one required method: + * get_labeled_model() which returns the labeled Pyomo model + """ def __init__(self, model=None): self.model = model From be878d18b608b3bd1e0b67c70caefc5d83b86029 Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 09:55:32 -0800 Subject: [PATCH 0820/3044] made ScenarioCreatorDeprecated class private --- pyomo/contrib/parmest/scenariocreator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 434e15e7f31..f2798ad2e94 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -137,7 +137,7 @@ def __init__(self, pest, solvername): + "creator, please recreate object using experiment lists.", version=DEPRECATION_VERSION, ) - self.scen_deprecated = ScenarioCreatorDeprecated( + self.scen_deprecated = _ScenarioCreatorDeprecated( pest.pest_deprecated, solvername ) else: @@ -201,7 +201,7 @@ def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): ################################ -class ScenarioCreatorDeprecated(object): +class _ScenarioCreatorDeprecated(object): """Create scenarios from parmest. Args: From 46bfee38dbdcc11c76e8279207b7ac2d0dfeafb7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 28 Feb 2024 11:02:12 -0700 Subject: [PATCH 0821/3044] NFC: removing a comment that is no longer relevant --- pyomo/common/dependencies.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 895759a8a2c..472b0011edb 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -1034,12 +1034,6 @@ def _pyutilib_importer(): return importlib.import_module('pyutilib') -# -# Note: because we will be calling -# declare_deferred_modules_as_importable, it is important that the -# following declarations explicitly defer_import (even if the target -# module has already been imported) -# with declare_modules_as_importable(globals()): # Standard libraries that are slower to import and not strictly required # on all platforms / situations. From 9cb26c7629c49b055ec699e505e4c5ab32d07d62 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 28 Feb 2024 11:16:41 -0700 Subject: [PATCH 0822/3044] NFC: Fix year in copyright assertion comment Co-authored-by: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> --- pyomo/contrib/simplification/__init__.py | 2 +- pyomo/contrib/simplification/build.py | 2 +- pyomo/contrib/simplification/ginac_interface.cpp | 2 +- pyomo/contrib/simplification/simplify.py | 2 +- pyomo/contrib/simplification/tests/__init__.py | 2 +- pyomo/contrib/simplification/tests/test_simplification.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/simplification/__init__.py b/pyomo/contrib/simplification/__init__.py index b4fa68eb386..c6111ddcb89 100644 --- a/pyomo/contrib/simplification/__init__.py +++ b/pyomo/contrib/simplification/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index d9d1e701290..2c7b1830ff6 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index 489f281bc2c..1060f87161c 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -1,7 +1,7 @@ // ___________________________________________________________________________ // // Pyomo: Python Optimization Modeling Objects -// Copyright (c) 2008-2022 +// Copyright (c) 2008-2024 // National Technology and Engineering Solutions of Sandia, LLC // Under the terms of Contract DE-NA0003525 with National Technology and // Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index b8cc4995f91..00c5dde348e 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/simplification/tests/__init__.py b/pyomo/contrib/simplification/tests/__init__.py index d93cfd77b3c..a4a626013c4 100644 --- a/pyomo/contrib/simplification/tests/__init__.py +++ b/pyomo/contrib/simplification/tests/__init__.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 95402f98318..1a5ae1e0036 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain From 8b2568804f83f89d24eb53330ffae61dbc95b3c7 Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 10:48:49 -0800 Subject: [PATCH 0823/3044] formatting updates --- pyomo/contrib/parmest/experiment.py | 9 +++++---- pyomo/contrib/parmest/parmest.py | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/parmest/experiment.py b/pyomo/contrib/parmest/experiment.py index 69474a32bb0..4f797d6c89c 100644 --- a/pyomo/contrib/parmest/experiment.py +++ b/pyomo/contrib/parmest/experiment.py @@ -13,16 +13,17 @@ class Experiment: """ The experiment class is a template for making experiment lists - to pass to parmest. - - An experiment is a Pyomo model "m" which is labeled + to pass to parmest. + + An experiment is a Pyomo model "m" which is labeled with additional suffixes: * m.experiment_outputs which defines experiment outputs * m.unknown_parameters which defines parameters to estimate - + The experiment class has one required method: * get_labeled_model() which returns the labeled Pyomo model """ + def __init__(self, model=None): self.model = model diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 8f98ab5cd5a..aecc9d5ebc2 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -408,7 +408,7 @@ def _expand_indexed_unknowns(self, model_temp): Expand indexed variables to get full list of thetas """ model_theta_list = [k.name for k, v in model_temp.unknown_parameters.items()] - + # check for indexed theta items indexed_theta_list = [] for theta_i in model_theta_list: @@ -419,11 +419,11 @@ def _expand_indexed_unknowns(self, model_temp): indexed_theta_list.append(theta_i + '[' + str(ind) + ']') else: indexed_theta_list.append(theta_i) - + # if we found indexed thetas, use expanded list if len(indexed_theta_list) > len(model_theta_list): model_theta_list = indexed_theta_list - + return model_theta_list def _create_parmest_model(self, experiment_number): From c5bdb0ba866bb0be1dd956fca35332ef84b88d5b Mon Sep 17 00:00:00 2001 From: kaklise Date: Wed, 28 Feb 2024 12:01:02 -0800 Subject: [PATCH 0824/3044] fixed typo --- doc/OnlineDocs/contributed_packages/parmest/driver.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/parmest/driver.rst b/doc/OnlineDocs/contributed_packages/parmest/driver.rst index 695cab36e93..5881d2748f9 100644 --- a/doc/OnlineDocs/contributed_packages/parmest/driver.rst +++ b/doc/OnlineDocs/contributed_packages/parmest/driver.rst @@ -106,7 +106,7 @@ A labeled Pyomo model ``m`` has the following additional suffixes (Pyomo `Suffix * ``m.experiment_outputs`` which defines experiment output (Pyomo `Param`, `Var`, or `Expression`) and their associated data values (float, int). -* ``m.unknown_parameters`` which defines the mutable parameters or variables (Pyomo `Parm` or `Var`) +* ``m.unknown_parameters`` which defines the mutable parameters or variables (Pyomo `Param` or `Var`) to estimate along with their component unique identifier (Pyomo `ComponentUID`). Within parmest, any parameters that are to be estimated are converted to unfixed variables. Variables that are to be estimated are also unfixed. From 5b5f0046ab59accedee601deb51cfe14939298ec Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 28 Feb 2024 15:24:45 -0500 Subject: [PATCH 0825/3044] Fix indentation typo --- pyomo/contrib/pyros/solve_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index c31eb8e5d3f..73eee5202aa 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -347,7 +347,7 @@ class SeparationLoopResults: solver_call_results : ComponentMap Mapping from performance constraints to corresponding ``SeparationSolveCallResults`` objects. - worst_case_perf_con : None or Constraint + worst_case_perf_con : None or Constraint Performance constraint mapped to ``SeparationSolveCallResults`` object in `self` corresponding to maximally violating separation problem solution. From 20a63602692ee8c9e8ee2bfa9efa95af392f5a6e Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 28 Feb 2024 15:52:14 -0500 Subject: [PATCH 0826/3044] Update `positive_int_or_minus_1` tests --- pyomo/contrib/pyros/tests/test_config.py | 29 +++++++++++++++--------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 3555391fd95..0f52d04135d 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -558,21 +558,28 @@ def test_positive_int_or_minus_one(self): Test positive int or -1 validator works as expected. """ standardizer_func = positive_int_or_minus_one - self.assertIs( - standardizer_func(1.0), + ans = standardizer_func(1.0) + self.assertEqual( + ans, 1, - msg=( - f"{positive_int_or_minus_one.__name__} " - "does not standardize as expected." - ), + msg=f"{positive_int_or_minus_one.__name__} output value not as expected.", + ) + self.assertIs( + type(ans), + int, + msg=f"{positive_int_or_minus_one.__name__} output type not as expected.", ) + + ans = standardizer_func(-1.0) self.assertEqual( - standardizer_func(-1.00), + ans, -1, - msg=( - f"{positive_int_or_minus_one.__name__} " - "does not standardize as expected." - ), + msg=f"{positive_int_or_minus_one.__name__} output value not as expected.", + ) + self.assertIs( + type(ans), + int, + msg=f"{positive_int_or_minus_one.__name__} output type not as expected.", ) exc_str = r"Expected positive int or -1, but received value.*" From 4e5bbbf2f073911ed89d39002ea96b652e807e16 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 22:58:51 -0700 Subject: [PATCH 0827/3044] NFC: fix copyright header --- pyomo/contrib/latex_printer/latex_printer.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 110df7cd5ca..a986f5d6b81 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -9,17 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2023 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - import math import copy import re From 0e673b663ad339e00ca93a65cf414bd0f79ca012 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 23:01:38 -0700 Subject: [PATCH 0828/3044] performance: avoid duplication, linear searches --- pyomo/contrib/latex_printer/latex_printer.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index a986f5d6b81..41cff29ad80 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -275,11 +275,11 @@ def handle_functionID_node(visitor, node, *args): def handle_indexTemplate_node(visitor, node, *args): - if node._set in ComponentSet(visitor.setMap.keys()): + if node._set in visitor.setMap: # already detected set, do nothing pass else: - visitor.setMap[node._set] = 'SET%d' % (len(visitor.setMap.keys()) + 1) + visitor.setMap[node._set] = 'SET%d' % (len(visitor.setMap) + 1) return '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' % ( node._group, @@ -616,15 +616,15 @@ def latex_printer( # Cody's backdoor because he got outvoted if latex_component_map is not None: - if 'use_short_descriptors' in list(latex_component_map.keys()): + if 'use_short_descriptors' in latex_component_map: if latex_component_map['use_short_descriptors'] == False: use_short_descriptors = False if latex_component_map is None: latex_component_map = ComponentMap() - existing_components = ComponentSet([]) + existing_components = ComponentSet() else: - existing_components = ComponentSet(list(latex_component_map.keys())) + existing_components = ComponentSet(latex_component_map) isSingle = False @@ -1225,14 +1225,14 @@ def latex_printer( ) for ky, vl in new_variableMap.items(): - if ky not in ComponentSet(latex_component_map.keys()): + if ky not in latex_component_map: latex_component_map[ky] = vl for ky, vl in new_parameterMap.items(): - if ky not in ComponentSet(latex_component_map.keys()): + if ky not in latex_component_map: latex_component_map[ky] = vl rep_dict = {} - for ky in ComponentSet(list(reversed(list(latex_component_map.keys())))): + for ky in reversed(list(latex_component_map)): if isinstance(ky, (pyo.Var, _GeneralVarData)): overwrite_value = latex_component_map[ky] if ky not in existing_components: From ff111df42ac8aa4ff87377ab3fd16612a91b0eda Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 23:04:18 -0700 Subject: [PATCH 0829/3044] resolve indextemplate naming for multidimensional sets --- pyomo/contrib/latex_printer/latex_printer.py | 153 +++++++++++-------- 1 file changed, 88 insertions(+), 65 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 41cff29ad80..90a5da0d9c1 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -281,8 +281,9 @@ def handle_indexTemplate_node(visitor, node, *args): else: visitor.setMap[node._set] = 'SET%d' % (len(visitor.setMap) + 1) - return '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' % ( + return '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % ( node._group, + node._id, visitor.setMap[node._set], ) @@ -304,8 +305,9 @@ def handle_numericGetItemExpression_node(visitor, node, *args): def handle_templateSumExpression_node(visitor, node, *args): pstr = '' for i in range(0, len(node._iters)): - pstr += '\\sum_{__S_PLACEHOLDER_8675309_GROUP_%s_%s__} ' % ( + pstr += '\\sum_{__S_PLACEHOLDER_8675309_GROUP_%s_%s_%s__} ' % ( node._iters[i][0]._group, + ','.join(str(it._id) for it in node._iters[i]), visitor.setMap[node._iters[i][0]._set], ) @@ -904,24 +906,33 @@ def latex_printer( # setMap = visitor.setMap # Multiple constraints are generated using a set if len(indices) > 0: - if indices[0]._set in ComponentSet(visitor.setMap.keys()): - # already detected set, do nothing - pass - else: - visitor.setMap[indices[0]._set] = 'SET%d' % ( - len(visitor.setMap.keys()) + 1 + conLine += ' \\qquad \\forall' + + _bygroups = {} + for idx in indices: + _bygroups.setdefault(idx._group, []).append(idx) + for _group, idxs in _bygroups.items(): + if idxs[0]._set in visitor.setMap: + # already detected set, do nothing + pass + else: + visitor.setMap[idxs[0]._set] = 'SET%d' % ( + len(visitor.setMap) + 1 + ) + + idxTag = ','.join( + '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' + % (idx._group, idx._id, visitor.setMap[idx._set]) + for idx in idxs ) - idxTag = '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' % ( - indices[0]._group, - visitor.setMap[indices[0]._set], - ) - setTag = '__S_PLACEHOLDER_8675309_GROUP_%s_%s__' % ( - indices[0]._group, - visitor.setMap[indices[0]._set], - ) + setTag = '__S_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % ( + indices[0]._group, + ','.join(str(it._id) for it in idxs), + visitor.setMap[indices[0]._set], + ) - conLine += ' \\qquad \\forall %s \\in %s ' % (idxTag, setTag) + conLine += ' %s \\in %s ' % (idxTag, setTag) pstr += conLine # Add labels as needed @@ -1070,8 +1081,8 @@ def latex_printer( for word in splitLatex: if "PLACEHOLDER_8675309_GROUP_" in word: ifo = word.split("PLACEHOLDER_8675309_GROUP_")[1] - gpNum, stName = ifo.split('_') - if gpNum not in groupMap.keys(): + gpNum, idNum, stName = ifo.split('_') + if gpNum not in groupMap: groupMap[gpNum] = [stName] if stName not in ComponentSet(uniqueSets): uniqueSets.append(stName) @@ -1088,10 +1099,7 @@ def latex_printer( ix = int(ky[3:]) - 1 setInfo[ky]['setObject'] = setMap_inverse[ky] # setList[ix] setInfo[ky]['setRegEx'] = ( - r'__S_PLACEHOLDER_8675309_GROUP_([0-9*])_%s__' % (ky) - ) - setInfo[ky]['sumSetRegEx'] = ( - r'sum_{__S_PLACEHOLDER_8675309_GROUP_([0-9*])_%s__}' % (ky) + r'__S_PLACEHOLDER_8675309_GROUP_([0-9]+)_([0-9,]+)_%s__' % (ky,) ) # setInfo[ky]['idxRegEx'] = r'__I_PLACEHOLDER_8675309_GROUP_[0-9*]_%s__'%(ky) @@ -1116,27 +1124,41 @@ def latex_printer( ed = stData[-1] replacement = ( - r'sum_{ __I_PLACEHOLDER_8675309_GROUP_\1_%s__ = %d }^{%d}' + r'sum_{ __I_PLACEHOLDER_8675309_GROUP_\1_\2_%s__ = %d }^{%d}' % (ky, bgn, ed) ) - ln = re.sub(setInfo[ky]['sumSetRegEx'], replacement, ln) + ln = re.sub( + 'sum_{' + setInfo[ky]['setRegEx'] + '}', replacement, ln + ) else: # if the set is not continuous or the flag has not been set - replacement = ( - r'sum_{ __I_PLACEHOLDER_8675309_GROUP_\1_%s__ \\in __S_PLACEHOLDER_8675309_GROUP_\1_%s__ }' - % (ky, ky) - ) - ln = re.sub(setInfo[ky]['sumSetRegEx'], replacement, ln) + for _grp, _id in re.findall( + 'sum_{' + setInfo[ky]['setRegEx'] + '}', ln + ): + set_placeholder = '__S_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % ( + _grp, + _id, + ky, + ) + i_placeholder = ','.join( + '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' % (_grp, _, ky) + for _ in _id.split(',') + ) + replacement = r'sum_{ %s \in %s }' % ( + i_placeholder, + set_placeholder, + ) + ln = ln.replace('sum_{' + set_placeholder + '}', replacement) replacement = repr(defaultSetLatexNames[setInfo[ky]['setObject']])[1:-1] ln = re.sub(setInfo[ky]['setRegEx'], replacement, ln) # groupNumbers = re.findall(r'__I_PLACEHOLDER_8675309_GROUP_([0-9*])_SET[0-9]*__',ln) setNumbers = re.findall( - r'__I_PLACEHOLDER_8675309_GROUP_[0-9*]_SET([0-9]*)__', ln + r'__I_PLACEHOLDER_8675309_GROUP_[0-9]+_[0-9]+_SET([0-9]+)__', ln ) - groupSetPairs = re.findall( - r'__I_PLACEHOLDER_8675309_GROUP_([0-9*])_SET([0-9]*)__', ln + groupIdSetTuples = re.findall( + r'__I_PLACEHOLDER_8675309_GROUP_([0-9]+)_([0-9]+)_SET([0-9]+)__', ln ) groupInfo = {} @@ -1146,43 +1168,44 @@ def latex_printer( 'indices': [], } - for gp in groupSetPairs: - if gp[0] not in groupInfo['SET' + gp[1]]['indices']: - groupInfo['SET' + gp[1]]['indices'].append(gp[0]) + for _gp, _id, _set in groupIdSetTuples: + if (_gp, _id) not in groupInfo['SET' + _set]['indices']: + groupInfo['SET' + _set]['indices'].append((_gp, _id)) + + def get_index_names(st, lcm): + if st in lcm: + return lcm[st][1] + elif isinstance(st, SetOperator): + return sum( + (get_index_names(s, lcm) for s in st.subsets(False)), start=[] + ) + elif st.dimen is not None: + return [None] * st.dimen + else: + return [Ellipsis] indexCounter = 0 for ky, vl in groupInfo.items(): - if vl['setObject'] in ComponentSet(latex_component_map.keys()): - indexNames = latex_component_map[vl['setObject']][1] - if len(indexNames) != 0: - if len(indexNames) < len(vl['indices']): - raise ValueError( - 'Insufficient number of indices provided to the overwrite dictionary for set %s' - % (vl['setObject'].name) - ) - for i in range(0, len(vl['indices'])): - ln = ln.replace( - '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' - % (vl['indices'][i], ky), - indexNames[i], - ) - else: - for i in range(0, len(vl['indices'])): - ln = ln.replace( - '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' - % (vl['indices'][i], ky), - alphabetStringGenerator(indexCounter), - ) - indexCounter += 1 - else: - for i in range(0, len(vl['indices'])): - ln = ln.replace( - '__I_PLACEHOLDER_8675309_GROUP_%s_%s__' - % (vl['indices'][i], ky), - alphabetStringGenerator(indexCounter), + indexNames = get_index_names(vl['setObject'], latex_component_map) + nonNone = list(filter(None, indexNames)) + if nonNone: + if len(nonNone) < len(vl['indices']): + raise ValueError( + 'Insufficient number of indices provided to the ' + 'overwrite dictionary for set %s (expected %s, but got %s)' + % (vl['setObject'].name, len(vl['indices']), indexNames) ) + else: + indexNames = [] + for i in vl['indices']: + indexNames.append(alphabetStringGenerator(indexCounter)) indexCounter += 1 - + for i in range(0, len(vl['indices'])): + ln = ln.replace( + '__I_PLACEHOLDER_8675309_GROUP_%s_%s_%s__' + % (*vl['indices'][i], ky), + indexNames[i], + ) latexLines[jj] = ln pstr = '\n'.join(latexLines) From e2e8165b731f24564a689a0f035797b621e78942 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 23:04:53 -0700 Subject: [PATCH 0830/3044] make it easier to switch mathds/mathbb --- pyomo/contrib/latex_printer/latex_printer.py | 27 +++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 90a5da0d9c1..f3ffe2e5982 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -406,25 +406,28 @@ def exitNode(self, node, data): ) +mathbb = r'\mathbb' + + def analyze_variable(vr): domainMap = { - 'Reals': '\\mathds{R}', - 'PositiveReals': '\\mathds{R}_{> 0}', - 'NonPositiveReals': '\\mathds{R}_{\\leq 0}', - 'NegativeReals': '\\mathds{R}_{< 0}', - 'NonNegativeReals': '\\mathds{R}_{\\geq 0}', - 'Integers': '\\mathds{Z}', - 'PositiveIntegers': '\\mathds{Z}_{> 0}', - 'NonPositiveIntegers': '\\mathds{Z}_{\\leq 0}', - 'NegativeIntegers': '\\mathds{Z}_{< 0}', - 'NonNegativeIntegers': '\\mathds{Z}_{\\geq 0}', + 'Reals': mathbb + '{R}', + 'PositiveReals': mathbb + '{R}_{> 0}', + 'NonPositiveReals': mathbb + '{R}_{\\leq 0}', + 'NegativeReals': mathbb + '{R}_{< 0}', + 'NonNegativeReals': mathbb + '{R}_{\\geq 0}', + 'Integers': mathbb + '{Z}', + 'PositiveIntegers': mathbb + '{Z}_{> 0}', + 'NonPositiveIntegers': mathbb + '{Z}_{\\leq 0}', + 'NegativeIntegers': mathbb + '{Z}_{< 0}', + 'NonNegativeIntegers': mathbb + '{Z}_{\\geq 0}', 'Boolean': '\\left\\{ \\text{True} , \\text{False} \\right \\}', 'Binary': '\\left\\{ 0 , 1 \\right \\}', # 'Any': None, # 'AnyWithNone': None, 'EmptySet': '\\varnothing', - 'UnitInterval': '\\mathds{R}', - 'PercentFraction': '\\mathds{R}', + 'UnitInterval': mathbb + '{R}', + 'PercentFraction': mathbb + '{R}', # 'RealInterval' : None , # 'IntegerInterval' : None , } From 99c1bc319f281215fe42260af3da0296719dc650 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 23:05:59 -0700 Subject: [PATCH 0831/3044] Resolve issue with ambiguous field codes (when >10 vars or params) --- pyomo/contrib/latex_printer/latex_printer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index f3ffe2e5982..77afeb8f849 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -776,12 +776,12 @@ def latex_printer( for vr in variableList: vrIdx += 1 if isinstance(vr, ScalarVar): - variableMap[vr] = 'x_' + str(vrIdx) + variableMap[vr] = 'x_' + str(vrIdx) + '_' elif isinstance(vr, IndexedVar): - variableMap[vr] = 'x_' + str(vrIdx) + variableMap[vr] = 'x_' + str(vrIdx) + '_' for sd in vr.index_set().data(): vrIdx += 1 - variableMap[vr[sd]] = 'x_' + str(vrIdx) + variableMap[vr[sd]] = 'x_' + str(vrIdx) + '_' else: raise DeveloperError( 'Variable is not a variable. Should not happen. Contact developers' @@ -793,12 +793,12 @@ def latex_printer( for vr in parameterList: pmIdx += 1 if isinstance(vr, ScalarParam): - parameterMap[vr] = 'p_' + str(pmIdx) + parameterMap[vr] = 'p_' + str(pmIdx) + '_' elif isinstance(vr, IndexedParam): - parameterMap[vr] = 'p_' + str(pmIdx) + parameterMap[vr] = 'p_' + str(pmIdx) + '_' for sd in vr.index_set().data(): pmIdx += 1 - parameterMap[vr[sd]] = 'p_' + str(pmIdx) + parameterMap[vr[sd]] = 'p_' + str(pmIdx) + '_' else: raise DeveloperError( 'Parameter is not a parameter. Should not happen. Contact developers' From 28db0387d398c96af331741820d1715f210d0cdc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 2 Mar 2024 23:25:07 -0700 Subject: [PATCH 0832/3044] Support name generation for set expressions --- pyomo/contrib/latex_printer/latex_printer.py | 81 ++++++++++++-------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 77afeb8f849..e41cbeac51e 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -49,7 +49,7 @@ ) from pyomo.core.base.var import ScalarVar, _GeneralVarData, IndexedVar from pyomo.core.base.param import _ParamData, ScalarParam, IndexedParam -from pyomo.core.base.set import _SetData +from pyomo.core.base.set import _SetData, SetOperator from pyomo.core.base.constraint import ScalarConstraint, IndexedConstraint from pyomo.common.collections.component_map import ComponentMap from pyomo.common.collections.component_set import ComponentSet @@ -79,6 +79,39 @@ from pyomo.common.dependencies import numpy as np, numpy_available +set_operator_map = { + '|': r' \cup ', + '&': r' \cap ', + '*': r' \times ', + '-': r' \setminus ', + '^': r' \triangle ', +} + +latex_reals = r'\mathds{R}' +latex_integers = r'\mathds{Z}' + +domainMap = { + 'Reals': latex_reals, + 'PositiveReals': latex_reals + '_{> 0}', + 'NonPositiveReals': latex_reals + '_{\\leq 0}', + 'NegativeReals': latex_reals + '_{< 0}', + 'NonNegativeReals': latex_reals + '_{\\geq 0}', + 'Integers': latex_integers, + 'PositiveIntegers': latex_integers + '_{> 0}', + 'NonPositiveIntegers': latex_integers + '_{\\leq 0}', + 'NegativeIntegers': latex_integers + '_{< 0}', + 'NonNegativeIntegers': latex_integers + '_{\\geq 0}', + 'Boolean': '\\left\\{ \\text{True} , \\text{False} \\right \\}', + 'Binary': '\\left\\{ 0 , 1 \\right \\}', + # 'Any': None, + # 'AnyWithNone': None, + 'EmptySet': '\\varnothing', + 'UnitInterval': latex_reals, + 'PercentFraction': latex_reals, + # 'RealInterval' : None , + # 'IntegerInterval' : None , +} + def decoder(num, base): if int(num) != abs(num): # Requiring an integer is nice, but not strictly necessary; @@ -406,32 +439,7 @@ def exitNode(self, node, data): ) -mathbb = r'\mathbb' - - def analyze_variable(vr): - domainMap = { - 'Reals': mathbb + '{R}', - 'PositiveReals': mathbb + '{R}_{> 0}', - 'NonPositiveReals': mathbb + '{R}_{\\leq 0}', - 'NegativeReals': mathbb + '{R}_{< 0}', - 'NonNegativeReals': mathbb + '{R}_{\\geq 0}', - 'Integers': mathbb + '{Z}', - 'PositiveIntegers': mathbb + '{Z}_{> 0}', - 'NonPositiveIntegers': mathbb + '{Z}_{\\leq 0}', - 'NegativeIntegers': mathbb + '{Z}_{< 0}', - 'NonNegativeIntegers': mathbb + '{Z}_{\\geq 0}', - 'Boolean': '\\left\\{ \\text{True} , \\text{False} \\right \\}', - 'Binary': '\\left\\{ 0 , 1 \\right \\}', - # 'Any': None, - # 'AnyWithNone': None, - 'EmptySet': '\\varnothing', - 'UnitInterval': mathbb + '{R}', - 'PercentFraction': mathbb + '{R}', - # 'RealInterval' : None , - # 'IntegerInterval' : None , - } - domainName = vr.domain.name varBounds = vr.bounds lowerBoundValue = varBounds[0] @@ -1062,15 +1070,22 @@ def latex_printer( setMap = visitor.setMap setMap_inverse = {vl: ky for ky, vl in setMap.items()} + def generate_set_name(st, lcm): + if st in lcm: + return lcm[st][0] + if st.parent_block().component(st.name) is st: + return st.name.replace('_', r'\_') + if isinstance(st, SetOperator): + return _set_op_map[st._operator.strip()].join( + generate_set_name(s, lcm) for s in st.subsets(False) + ) + else: + return str(st).replace('_', r'\_').replace('{', '\{').replace('}', '\}') + # Handling the iterator indices defaultSetLatexNames = ComponentMap() - for ky, vl in setMap.items(): - st = ky - defaultSetLatexNames[st] = st.name.replace('_', '\\_') - if st in ComponentSet(latex_component_map.keys()): - defaultSetLatexNames[st] = latex_component_map[st][ - 0 - ] # .replace('_', '\\_') + for ky in setMap: + defaultSetLatexNames[ky] = generate_set_name(ky, latex_component_map) latexLines = pstr.split('\n') for jj in range(0, len(latexLines)): From 0d3400ffeeb93ed1764b4ba5f4eb487a309ecb35 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sun, 3 Mar 2024 05:09:56 -0700 Subject: [PATCH 0833/3044] add type hints to components --- pyomo/core/base/block.py | 22 ++++++++++++++++++++-- pyomo/core/base/constraint.py | 17 +++++++++++++++++ pyomo/core/base/indexed_component.py | 4 ++-- pyomo/core/base/param.py | 16 +++++++++++++++- pyomo/core/base/set.py | 16 +++++++++++++++- pyomo/core/base/var.py | 16 +++++++++++++++- 6 files changed, 84 insertions(+), 7 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index a0948c693d7..9ca2112d498 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from __future__ import annotations import copy import logging import sys @@ -21,6 +22,7 @@ from io import StringIO from itertools import filterfalse, chain from operator import itemgetter, attrgetter +from typing import Union, Any, Type from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import Mapping @@ -44,6 +46,7 @@ from pyomo.core.base.indexed_component import ( ActiveIndexedComponent, UnindexedComponent_set, + IndexedComponent, ) from pyomo.opt.base import ProblemFormat, guess_format @@ -539,7 +542,7 @@ def __init__(self, component): super(_BlockData, self).__setattr__('_decl_order', []) self._private_data = None - def __getattr__(self, val): + def __getattr__(self, val) -> Union[Component, IndexedComponent, Any]: if val in ModelComponentFactory: return _component_decorator(self, ModelComponentFactory.get_class(val)) # Since the base classes don't support getattr, we can just @@ -548,7 +551,7 @@ def __getattr__(self, val): "'%s' object has no attribute '%s'" % (self.__class__.__name__, val) ) - def __setattr__(self, name, val): + def __setattr__(self, name: str, val: Union[Component, IndexedComponent, Any]): """ Set an attribute of a block data object. """ @@ -2007,6 +2010,18 @@ class Block(ActiveIndexedComponent): _ComponentDataClass = _BlockData _private_data_initializers = defaultdict(lambda: dict) + @overload + def __new__(cls: Type[Block], *args, **kwds) -> Union[ScalarBlock, IndexedBlock]: + ... + + @overload + def __new__(cls: Type[ScalarBlock], *args, **kwds) -> ScalarBlock: + ... + + @overload + def __new__(cls: Type[IndexedBlock], *args, **kwds) -> IndexedBlock: + ... + def __new__(cls, *args, **kwds): if cls != Block: return super(Block, cls).__new__(cls) @@ -2251,6 +2266,9 @@ class IndexedBlock(Block): def __init__(self, *args, **kwds): Block.__init__(self, *args, **kwds) + def __getitem__(self, index) -> _BlockData: + return super().__getitem__(index) + # # Deprecated functions. diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 8cf3c48ad0a..dcc90fd6280 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -9,10 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from __future__ import annotations import sys import logging from weakref import ref as weakref_ref from pyomo.common.pyomo_typing import overload +from typing import Union, Type from pyomo.common.deprecation import RenamedClass from pyomo.common.errors import DeveloperError @@ -728,6 +730,18 @@ class Infeasible(object): Violated = Infeasible Satisfied = Feasible + @overload + def __new__(cls: Type[Constraint], *args, **kwds) -> Union[ScalarConstraint, IndexedConstraint]: + ... + + @overload + def __new__(cls: Type[ScalarConstraint], *args, **kwds) -> ScalarConstraint: + ... + + @overload + def __new__(cls: Type[IndexedConstraint], *args, **kwds) -> IndexedConstraint: + ... + def __new__(cls, *args, **kwds): if cls != Constraint: return super(Constraint, cls).__new__(cls) @@ -1019,6 +1033,9 @@ class IndexedConstraint(Constraint): def add(self, index, expr): """Add a constraint with a given index.""" return self.__setitem__(index, expr) + + def __getitem__(self, index) -> _GeneralConstraintData: + return super().__getitem__(index) @ModelComponentFactory.register("A list of constraint expressions.") diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index 0d498da091d..e1be613d666 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -18,7 +18,7 @@ import pyomo.core.base as BASE from pyomo.core.base.indexed_component_slice import IndexedComponent_slice from pyomo.core.base.initializer import Initializer -from pyomo.core.base.component import Component, ActiveComponent +from pyomo.core.base.component import Component, ActiveComponent, ComponentData from pyomo.core.base.config import PyomoOptions from pyomo.core.base.enums import SortComponents from pyomo.core.base.global_set import UnindexedComponent_set @@ -606,7 +606,7 @@ def iteritems(self): """Return a list (index,data) tuples from the dictionary""" return self.items() - def __getitem__(self, index): + def __getitem__(self, index) -> ComponentData: """ This method returns the data corresponding to the given index. """ diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 3ef33b9ee45..dde390661ab 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -9,11 +9,13 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from __future__ import annotations import sys import types import logging from weakref import ref as weakref_ref from pyomo.common.pyomo_typing import overload +from typing import Union, Type from pyomo.common.autoslots import AutoSlots from pyomo.common.deprecation import deprecation_warning, RenamedClass @@ -291,6 +293,18 @@ class NoValue(object): pass + @overload + def __new__(cls: Type[Param], *args, **kwds) -> Union[ScalarParam, IndexedParam]: + ... + + @overload + def __new__(cls: Type[ScalarParam], *args, **kwds) -> ScalarParam: + ... + + @overload + def __new__(cls: Type[IndexedParam], *args, **kwds) -> IndexedParam: + ... + def __new__(cls, *args, **kwds): if cls != Param: return super(Param, cls).__new__(cls) @@ -983,7 +997,7 @@ def _create_objects_for_deepcopy(self, memo, component_list): # between potentially variable GetItemExpression objects and # "constant" GetItemExpression objects. That will need to wait for # the expression rework [JDS; Nov 22]. - def __getitem__(self, args): + def __getitem__(self, args) -> _ParamData: try: return super().__getitem__(args) except: diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 2dc14460911..c52945dfd30 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from __future__ import annotations import inspect import itertools import logging @@ -16,6 +17,8 @@ import sys import weakref from pyomo.common.pyomo_typing import overload +from typing import Union, Type, Any +from collections.abc import Iterator from pyomo.common.collections import ComponentSet from pyomo.common.deprecation import deprecated, deprecation_warning, RenamedClass @@ -569,7 +572,7 @@ def isordered(self): def subsets(self, expand_all_set_operators=None): return iter((self,)) - def __iter__(self): + def __iter__(self) -> Iterator[Any]: """Iterate over the set members Raises AttributeError for non-finite sets. This must be @@ -1967,6 +1970,14 @@ class SortedOrder(object): _ValidOrderedAuguments = {True, False, InsertionOrder, SortedOrder} _UnorderedInitializers = {set} + @overload + def __new__(cls: Type[Set], *args, **kwds) -> Union[_SetData, IndexedSet]: + ... + + @overload + def __new__(cls: Type[OrderedScalarSet], *args, **kwds) -> OrderedScalarSet: + ... + def __new__(cls, *args, **kwds): if cls is not Set: return super(Set, cls).__new__(cls) @@ -2373,6 +2384,9 @@ def data(self): "Return a dict containing the data() of each Set in this IndexedSet" return {k: v.data() for k, v in self.items()} + def __getitem__(self, index) -> _SetData: + return super().__getitem__(index) + class FiniteScalarSet(_FiniteSetData, Set): def __init__(self, **kwds): diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index f426c9c4f55..c92a4056667 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -9,10 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from __future__ import annotations import logging import sys from pyomo.common.pyomo_typing import overload from weakref import ref as weakref_ref +from typing import Union, Type from pyomo.common.deprecation import RenamedClass from pyomo.common.log import is_debug_set @@ -668,6 +670,18 @@ class Var(IndexedComponent, IndexedComponent_NDArrayMixin): _ComponentDataClass = _GeneralVarData + @overload + def __new__(cls: Type[Var], *args, **kwargs) -> Union[ScalarVar, IndexedVar]: + ... + + @overload + def __new__(cls: Type[ScalarVar], *args, **kwargs) -> ScalarVar: + ... + + @overload + def __new__(cls: Type[IndexedVar], *args, **kwargs) -> IndexedVar: + ... + def __new__(cls, *args, **kwargs): if cls is not Var: return super(Var, cls).__new__(cls) @@ -1046,7 +1060,7 @@ def domain(self, domain): # between potentially variable GetItemExpression objects and # "constant" GetItemExpression objects. That will need to wait for # the expression rework [JDS; Nov 22]. - def __getitem__(self, args): + def __getitem__(self, args) -> _GeneralVarData: try: return super().__getitem__(args) except RuntimeError: From ae5ddeb36428d0a77adeccbd15f9daa6fcce7c4e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sun, 3 Mar 2024 05:13:01 -0700 Subject: [PATCH 0834/3044] run black --- pyomo/core/base/block.py | 11 +++++------ pyomo/core/base/constraint.py | 13 ++++++------- pyomo/core/base/param.py | 11 +++++------ pyomo/core/base/set.py | 6 ++---- pyomo/core/base/var.py | 11 ++++------- 5 files changed, 22 insertions(+), 30 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 9ca2112d498..908e0ef1abd 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2011,16 +2011,15 @@ class Block(ActiveIndexedComponent): _private_data_initializers = defaultdict(lambda: dict) @overload - def __new__(cls: Type[Block], *args, **kwds) -> Union[ScalarBlock, IndexedBlock]: - ... + def __new__( + cls: Type[Block], *args, **kwds + ) -> Union[ScalarBlock, IndexedBlock]: ... @overload - def __new__(cls: Type[ScalarBlock], *args, **kwds) -> ScalarBlock: - ... + def __new__(cls: Type[ScalarBlock], *args, **kwds) -> ScalarBlock: ... @overload - def __new__(cls: Type[IndexedBlock], *args, **kwds) -> IndexedBlock: - ... + def __new__(cls: Type[IndexedBlock], *args, **kwds) -> IndexedBlock: ... def __new__(cls, *args, **kwds): if cls != Block: diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index dcc90fd6280..a36bc679e49 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -731,16 +731,15 @@ class Infeasible(object): Satisfied = Feasible @overload - def __new__(cls: Type[Constraint], *args, **kwds) -> Union[ScalarConstraint, IndexedConstraint]: - ... + def __new__( + cls: Type[Constraint], *args, **kwds + ) -> Union[ScalarConstraint, IndexedConstraint]: ... @overload - def __new__(cls: Type[ScalarConstraint], *args, **kwds) -> ScalarConstraint: - ... + def __new__(cls: Type[ScalarConstraint], *args, **kwds) -> ScalarConstraint: ... @overload - def __new__(cls: Type[IndexedConstraint], *args, **kwds) -> IndexedConstraint: - ... + def __new__(cls: Type[IndexedConstraint], *args, **kwds) -> IndexedConstraint: ... def __new__(cls, *args, **kwds): if cls != Constraint: @@ -1033,7 +1032,7 @@ class IndexedConstraint(Constraint): def add(self, index, expr): """Add a constraint with a given index.""" return self.__setitem__(index, expr) - + def __getitem__(self, index) -> _GeneralConstraintData: return super().__getitem__(index) diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index dde390661ab..5fcaf92b25a 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -294,16 +294,15 @@ class NoValue(object): pass @overload - def __new__(cls: Type[Param], *args, **kwds) -> Union[ScalarParam, IndexedParam]: - ... + def __new__( + cls: Type[Param], *args, **kwds + ) -> Union[ScalarParam, IndexedParam]: ... @overload - def __new__(cls: Type[ScalarParam], *args, **kwds) -> ScalarParam: - ... + def __new__(cls: Type[ScalarParam], *args, **kwds) -> ScalarParam: ... @overload - def __new__(cls: Type[IndexedParam], *args, **kwds) -> IndexedParam: - ... + def __new__(cls: Type[IndexedParam], *args, **kwds) -> IndexedParam: ... def __new__(cls, *args, **kwds): if cls != Param: diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index c52945dfd30..6373af97683 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1971,12 +1971,10 @@ class SortedOrder(object): _UnorderedInitializers = {set} @overload - def __new__(cls: Type[Set], *args, **kwds) -> Union[_SetData, IndexedSet]: - ... + def __new__(cls: Type[Set], *args, **kwds) -> Union[_SetData, IndexedSet]: ... @overload - def __new__(cls: Type[OrderedScalarSet], *args, **kwds) -> OrderedScalarSet: - ... + def __new__(cls: Type[OrderedScalarSet], *args, **kwds) -> OrderedScalarSet: ... def __new__(cls, *args, **kwds): if cls is not Set: diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index c92a4056667..856a2dc0237 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -671,16 +671,13 @@ class Var(IndexedComponent, IndexedComponent_NDArrayMixin): _ComponentDataClass = _GeneralVarData @overload - def __new__(cls: Type[Var], *args, **kwargs) -> Union[ScalarVar, IndexedVar]: - ... + def __new__(cls: Type[Var], *args, **kwargs) -> Union[ScalarVar, IndexedVar]: ... @overload - def __new__(cls: Type[ScalarVar], *args, **kwargs) -> ScalarVar: - ... + def __new__(cls: Type[ScalarVar], *args, **kwargs) -> ScalarVar: ... @overload - def __new__(cls: Type[IndexedVar], *args, **kwargs) -> IndexedVar: - ... + def __new__(cls: Type[IndexedVar], *args, **kwargs) -> IndexedVar: ... def __new__(cls, *args, **kwargs): if cls is not Var: @@ -702,7 +699,7 @@ def __init__( dense=True, units=None, name=None, - doc=None + doc=None, ): ... def __init__(self, *args, **kwargs): From b28f4bb7179a614f2532b591e1ff38eb1df4ad6f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sun, 3 Mar 2024 05:20:32 -0700 Subject: [PATCH 0835/3044] name conflict --- pyomo/core/base/set.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 6373af97683..b8ddae14e9f 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -17,7 +17,7 @@ import sys import weakref from pyomo.common.pyomo_typing import overload -from typing import Union, Type, Any +from typing import Union, Type, Any as typingAny from collections.abc import Iterator from pyomo.common.collections import ComponentSet @@ -572,7 +572,7 @@ def isordered(self): def subsets(self, expand_all_set_operators=None): return iter((self,)) - def __iter__(self) -> Iterator[Any]: + def __iter__(self) -> Iterator[typingAny]: """Iterate over the set members Raises AttributeError for non-finite sets. This must be From b1444017c3fd536b0630dda91f9290b6e3043798 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 3 Mar 2024 09:07:15 -0700 Subject: [PATCH 0836/3044] NFC: apply black --- pyomo/contrib/latex_printer/latex_printer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index e41cbeac51e..c2cbfd6b2e1 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -112,6 +112,7 @@ # 'IntegerInterval' : None , } + def decoder(num, base): if int(num) != abs(num): # Requiring an integer is nice, but not strictly necessary; From 2273282c76d8cba84767ece983153445fa795ade Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Mar 2024 08:17:07 -0700 Subject: [PATCH 0837/3044] Try some different stuff to get more printouts --- .github/workflows/test_branches.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 77f47b505ff..661d86ef890 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -86,7 +86,7 @@ jobs: PACKAGES: pytest-qt - os: ubuntu-latest - python: 3.9 + python: '3.10' other: /mpi mpi: 3 skip_doctest: 1 @@ -333,10 +333,11 @@ jobs: CONDA_DEPENDENCIES="$CONDA_DEPENDENCIES $PKG" fi done + echo "" echo "*** Install Pyomo dependencies ***" # Note: this will fail the build if any installation fails (or # possibly if it outputs messages to stderr) - conda install --update-deps -q -y $CONDA_DEPENDENCIES + conda install --update-deps -y $CONDA_DEPENDENCIES if test -z "${{matrix.slim}}"; then PYVER=$(echo "py${{matrix.python}}" | sed 's/\.//g') echo "Installing for $PYVER" From a32c0040b5c66ed524966a072d2ab22d5f6d9745 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Mar 2024 09:00:06 -0700 Subject: [PATCH 0838/3044] Revert to 3.9 --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 661d86ef890..5dca79f294e 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -86,7 +86,7 @@ jobs: PACKAGES: pytest-qt - os: ubuntu-latest - python: '3.10' + python: 3.9 other: /mpi mpi: 3 skip_doctest: 1 From ffa594cdf1e4bc7f621e73ae5a8bb94595cf6399 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Mar 2024 09:08:04 -0700 Subject: [PATCH 0839/3044] Back to 3.10 --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 5dca79f294e..661d86ef890 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -86,7 +86,7 @@ jobs: PACKAGES: pytest-qt - os: ubuntu-latest - python: 3.9 + python: '3.10' other: /mpi mpi: 3 skip_doctest: 1 From e446941c9d2d965b6b2cb4744eb01f03de93f67c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Mar 2024 09:11:00 -0700 Subject: [PATCH 0840/3044] Upgrade to macos-13 --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 661d86ef890..89c1fbeb7e4 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -66,7 +66,7 @@ jobs: TARGET: linux PYENV: pip - - os: macos-latest + - os: macos-13 python: '3.10' TARGET: osx PYENV: pip From a4a1fd41a70708eb7179b421e67d9c739a016793 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Mar 2024 09:23:16 -0700 Subject: [PATCH 0841/3044] Lower to 2 --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 89c1fbeb7e4..6867043f67e 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -88,7 +88,7 @@ jobs: - os: ubuntu-latest python: '3.10' other: /mpi - mpi: 3 + mpi: 2 skip_doctest: 1 TARGET: linux PYENV: conda From 4ae6a70a72b07b0f811ea8e72035937b7a111efb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Mar 2024 12:15:57 -0700 Subject: [PATCH 0842/3044] Add oversubscribe --- .github/workflows/test_branches.yml | 4 ++-- .github/workflows/test_pr_and_main.yml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 6867043f67e..1cb6fd0926d 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -88,7 +88,7 @@ jobs: - os: ubuntu-latest python: '3.10' other: /mpi - mpi: 2 + mpi: 3 skip_doctest: 1 TARGET: linux PYENV: conda @@ -632,7 +632,7 @@ jobs: $PYTHON_EXE -c "from pyomo.dataportal.parse_datacmds import \ parse_data_commands; parse_data_commands(data='')" # Note: if we are testing with openmpi, add '--oversubscribe' - mpirun -np ${{matrix.mpi}} pytest -v \ + mpirun -np ${{matrix.mpi}} -oversubscribe pytest -v \ --junit-xml=TEST-pyomo-mpi.xml \ -m "mpi" -W ignore::Warning \ pyomo `pwd`/pyomo-model-libraries diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 87d6aa4d7a8..e3e08847aa9 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -59,7 +59,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-13, windows-latest] python: [ 3.8, 3.9, '3.10', '3.11', '3.12' ] other: [""] category: [""] @@ -69,7 +69,7 @@ jobs: TARGET: linux PYENV: pip - - os: macos-latest + - os: macos-13 TARGET: osx PYENV: pip @@ -87,7 +87,7 @@ jobs: PACKAGES: pytest-qt - os: ubuntu-latest - python: 3.9 + python: '3.10' other: /mpi mpi: 3 skip_doctest: 1 @@ -661,7 +661,7 @@ jobs: $PYTHON_EXE -c "from pyomo.dataportal.parse_datacmds import \ parse_data_commands; parse_data_commands(data='')" # Note: if we are testing with openmpi, add '--oversubscribe' - mpirun -np ${{matrix.mpi}} pytest -v \ + mpirun -np ${{matrix.mpi}} -oversubscribe pytest -v \ --junit-xml=TEST-pyomo-mpi.xml \ -m "mpi" -W ignore::Warning \ pyomo `pwd`/pyomo-model-libraries From 1e609c067dc01e7cd6ea8c65a347e540f0534d02 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 13:54:23 -0700 Subject: [PATCH 0843/3044] run black --- pyomo/contrib/simplification/simplify.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index b8cc4995f91..80e7c42549c 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -55,7 +55,11 @@ def simplify(self, expr: NumericExpression): return simplify_with_ginac(expr, self.gi) else: if not self.suppress_no_ginac_warnings: - msg = f"GiNaC does not seem to be available. Using SymPy. Note that the GiNac interface is significantly faster." + msg = ( + "GiNaC does not seem to be available. Using SymPy. " + + "Note that the GiNac interface is significantly faster." + ) logger.warning(msg) warnings.warn(msg) + self.suppress_no_ginac_warnings = True return simplify_with_sympy(expr) From 42fd802267b5f4e25606f844a966cceb569cbdd7 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 4 Mar 2024 14:20:57 -0700 Subject: [PATCH 0844/3044] Change macos for coverage upload as well --- .github/workflows/test_branches.yml | 4 ++-- .github/workflows/test_pr_and_main.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 1cb6fd0926d..55f903a37f9 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -709,12 +709,12 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-13, windows-latest] include: - os: ubuntu-latest TARGET: linux - - os: macos-latest + - os: macos-13 TARGET: osx - os: windows-latest TARGET: win diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index e3e08847aa9..76ec6de951a 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -739,12 +739,12 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-13, windows-latest] include: - os: ubuntu-latest TARGET: linux - - os: macos-latest + - os: macos-13 TARGET: osx - os: windows-latest TARGET: win From ada10bd444d93aad724d665cbaf890a8badbd6cd Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 16:31:20 -0700 Subject: [PATCH 0845/3044] only modify module __path__ and __spec__ for deferred import modules --- pyomo/common/dependencies.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 472b0011edb..22d15749879 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -902,10 +902,10 @@ def __exit__(self, exc_type, exc_value, traceback): sys.modules[_global_name + name] = sys.modules[name] while deferred: name, mod = deferred.popitem() - mod.__path__ = None - mod.__spec__ = None - sys.modules[_global_name + name] = mod if isinstance(mod, DeferredImportModule): + mod.__path__ = None + mod.__spec__ = None + sys.modules[_global_name + name] = mod deferred.update( (name + '.' + k, v) for k, v in mod.__dict__.items() From df94ec7786894a38e528d4802b5ff6ecf23ca1dc Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 17:06:08 -0700 Subject: [PATCH 0846/3044] deferred import fix --- pyomo/common/dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 22d15749879..ea9efe370f7 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -902,10 +902,10 @@ def __exit__(self, exc_type, exc_value, traceback): sys.modules[_global_name + name] = sys.modules[name] while deferred: name, mod = deferred.popitem() + sys.modules[_global_name + name] = mod if isinstance(mod, DeferredImportModule): mod.__path__ = None mod.__spec__ = None - sys.modules[_global_name + name] = mod deferred.update( (name + '.' + k, v) for k, v in mod.__dict__.items() From 8ee01941a433eab987e6fa11ffa74a6b7ea5f2bb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 12:38:32 -0700 Subject: [PATCH 0847/3044] Add tests for set products --- pyomo/contrib/latex_printer/latex_printer.py | 2 +- .../latex_printer/tests/test_latex_printer.py | 63 +++++++++++++---- pyomo/core/tests/examples/pmedian_concrete.py | 70 +++++++++++++++++++ 3 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 pyomo/core/tests/examples/pmedian_concrete.py diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index c2cbfd6b2e1..1d5279e984a 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -1077,7 +1077,7 @@ def generate_set_name(st, lcm): if st.parent_block().component(st.name) is st: return st.name.replace('_', r'\_') if isinstance(st, SetOperator): - return _set_op_map[st._operator.strip()].join( + return set_operator_map[st._operator.strip()].join( generate_set_name(s, lcm) for s in st.subsets(False) ) else: diff --git a/pyomo/contrib/latex_printer/tests/test_latex_printer.py b/pyomo/contrib/latex_printer/tests/test_latex_printer.py index 2d7dd69dba8..f09a14b8b00 100644 --- a/pyomo/contrib/latex_printer/tests/test_latex_printer.py +++ b/pyomo/contrib/latex_printer/tests/test_latex_printer.py @@ -9,25 +9,16 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2023 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - import io +from textwrap import dedent + import pyomo.common.unittest as unittest -from pyomo.contrib.latex_printer import latex_printer +import pyomo.core.tests.examples.pmedian_concrete as pmedian_concrete import pyomo.environ as pyo -from textwrap import dedent + +from pyomo.contrib.latex_printer import latex_printer from pyomo.common.tempfiles import TempfileManager from pyomo.common.collections.component_map import ComponentMap - from pyomo.environ import ( Reals, PositiveReals, @@ -797,6 +788,50 @@ def ruleMaker_2(m, i): self.assertEqual('\n' + pstr + '\n', bstr) + def test_latexPrinter_pmedian_verbose(self): + m = pmedian_concrete.create_model() + self.assertEqual( + latex_printer(m).strip(), + r""" +\begin{align} + & \min + & & \sum_{ i \in Locations } \sum_{ j \in Customers } cost_{i,j} serve\_customer\_from\_location_{i,j} & \label{obj:M1_obj} \\ + & \text{s.t.} + & & \sum_{ i \in Locations } serve\_customer\_from\_location_{i,j} = 1 & \qquad \forall j \in Customers \label{con:M1_single_x} \\ + &&& serve\_customer\_from\_location_{i,j} \leq select\_location_{i} & \qquad \forall i,j \in Locations \times Customers \label{con:M1_bound_y} \\ + &&& \sum_{ i \in Locations } select\_location_{i} = P & \label{con:M1_num_facilities} \\ + & \text{w.b.} + & & 0.0 \leq serve\_customer\_from\_location \leq 1.0 & \qquad \in \mathds{R} \label{con:M1_serve_customer_from_location_bound} \\ + &&& select\_location & \qquad \in \left\{ 0 , 1 \right \} \label{con:M1_select_location_bound} +\end{align} + """.strip() + ) + + def test_latexPrinter_pmedian_concise(self): + m = pmedian_concrete.create_model() + lcm = ComponentMap() + lcm[m.Locations] = ['L', ['n']] + lcm[m.Customers] = ['C', ['m']] + lcm[m.cost] = 'd' + lcm[m.serve_customer_from_location] = 'x' + lcm[m.select_location] = 'y' + self.assertEqual( + latex_printer(m, latex_component_map=lcm).strip(), + r""" +\begin{align} + & \min + & & \sum_{ n \in L } \sum_{ m \in C } d_{n,m} x_{n,m} & \label{obj:M1_obj} \\ + & \text{s.t.} + & & \sum_{ n \in L } x_{n,m} = 1 & \qquad \forall m \in C \label{con:M1_single_x} \\ + &&& x_{n,m} \leq y_{n} & \qquad \forall n,m \in L \times C \label{con:M1_bound_y} \\ + &&& \sum_{ n \in L } y_{n} = P & \label{con:M1_num_facilities} \\ + & \text{w.b.} + & & 0.0 \leq x \leq 1.0 & \qquad \in \mathds{R} \label{con:M1_x_bound} \\ + &&& y & \qquad \in \left\{ 0 , 1 \right \} \label{con:M1_y_bound} +\end{align} + """.strip() + ) + if __name__ == '__main__': unittest.main() diff --git a/pyomo/core/tests/examples/pmedian_concrete.py b/pyomo/core/tests/examples/pmedian_concrete.py new file mode 100644 index 00000000000..a6a1859df23 --- /dev/null +++ b/pyomo/core/tests/examples/pmedian_concrete.py @@ -0,0 +1,70 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import math +from pyomo.environ import ( + ConcreteModel, + Param, + RangeSet, + Var, + Reals, + Binary, + PositiveIntegers, +) + + +def _cost_rule(model, n, m): + # We will assume costs are an arbitrary function of the indices + return math.sin(n * 2.33333 + m * 7.99999) + + +def create_model(n=3, m=3, p=2): + model = ConcreteModel(name="M1") + + model.N = Param(initialize=n, within=PositiveIntegers) + model.M = Param(initialize=m, within=PositiveIntegers) + model.P = Param(initialize=p, within=RangeSet(1, model.N), mutable=True) + + model.Locations = RangeSet(1, model.N) + model.Customers = RangeSet(1, model.M) + + model.cost = Param( + model.Locations, model.Customers, initialize=_cost_rule, within=Reals + ) + model.serve_customer_from_location = Var( + model.Locations, model.Customers, bounds=(0.0, 1.0) + ) + model.select_location = Var(model.Locations, within=Binary) + + @model.Objective() + def obj(model): + return sum( + model.cost[n, m] * model.serve_customer_from_location[n, m] + for n in model.Locations + for m in model.Customers + ) + + @model.Constraint(model.Customers) + def single_x(model, m): + return ( + sum(model.serve_customer_from_location[n, m] for n in model.Locations) + == 1.0 + ) + + @model.Constraint(model.Locations, model.Customers) + def bound_y(model, n, m): + return model.serve_customer_from_location[n, m] <= model.select_location[n] + + @model.Constraint() + def num_facilities(model): + return sum(model.select_location[n] for n in model.Locations) == model.P + + return model From 30cb7e4b6daa82430eab415ae5cb603cd849ccf1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 12:49:29 -0700 Subject: [PATCH 0848/3044] NFC: apply black --- pyomo/contrib/latex_printer/tests/test_latex_printer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/latex_printer/tests/test_latex_printer.py b/pyomo/contrib/latex_printer/tests/test_latex_printer.py index f09a14b8b00..b0ada97a5fe 100644 --- a/pyomo/contrib/latex_printer/tests/test_latex_printer.py +++ b/pyomo/contrib/latex_printer/tests/test_latex_printer.py @@ -13,7 +13,7 @@ from textwrap import dedent import pyomo.common.unittest as unittest -import pyomo.core.tests.examples.pmedian_concrete as pmedian_concrete +import pyomo.core.tests.examples.pmedian_concrete as pmedian_concrete import pyomo.environ as pyo from pyomo.contrib.latex_printer import latex_printer @@ -804,7 +804,7 @@ def test_latexPrinter_pmedian_verbose(self): & & 0.0 \leq serve\_customer\_from\_location \leq 1.0 & \qquad \in \mathds{R} \label{con:M1_serve_customer_from_location_bound} \\ &&& select\_location & \qquad \in \left\{ 0 , 1 \right \} \label{con:M1_select_location_bound} \end{align} - """.strip() + """.strip(), ) def test_latexPrinter_pmedian_concise(self): @@ -829,7 +829,7 @@ def test_latexPrinter_pmedian_concise(self): & & 0.0 \leq x \leq 1.0 & \qquad \in \mathds{R} \label{con:M1_x_bound} \\ &&& y & \qquad \in \left\{ 0 , 1 \right \} \label{con:M1_y_bound} \end{align} - """.strip() + """.strip(), ) From 9d9c98ad02699535f0a446fb90683f8a3356f3b4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 20:05:43 -0700 Subject: [PATCH 0849/3044] simplify initialization of exit node dispatchers --- pyomo/repn/linear.py | 63 ++++++++--------------------- pyomo/repn/quadratic.py | 89 ++++++++--------------------------------- 2 files changed, 33 insertions(+), 119 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 6ab4abfdaf5..68dda60d3c0 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -208,9 +208,8 @@ def _handle_negation_ANY(visitor, node, arg): _exit_node_handlers[NegationExpression] = { + None: _handle_negation_ANY, (_CONSTANT,): _handle_negation_constant, - (_LINEAR,): _handle_negation_ANY, - (_GENERAL,): _handle_negation_ANY, } # @@ -284,15 +283,12 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): _exit_node_handlers[ProductExpression] = { + None: _handle_product_nonlinear, (_CONSTANT, _CONSTANT): _handle_product_constant_constant, (_CONSTANT, _LINEAR): _handle_product_constant_ANY, (_CONSTANT, _GENERAL): _handle_product_constant_ANY, (_LINEAR, _CONSTANT): _handle_product_ANY_constant, - (_LINEAR, _LINEAR): _handle_product_nonlinear, - (_LINEAR, _GENERAL): _handle_product_nonlinear, (_GENERAL, _CONSTANT): _handle_product_ANY_constant, - (_GENERAL, _LINEAR): _handle_product_nonlinear, - (_GENERAL, _GENERAL): _handle_product_nonlinear, } _exit_node_handlers[MonomialTermExpression] = _exit_node_handlers[ProductExpression] @@ -317,15 +313,10 @@ def _handle_division_nonlinear(visitor, node, arg1, arg2): _exit_node_handlers[DivisionExpression] = { + None: _handle_division_nonlinear, (_CONSTANT, _CONSTANT): _handle_division_constant_constant, - (_CONSTANT, _LINEAR): _handle_division_nonlinear, - (_CONSTANT, _GENERAL): _handle_division_nonlinear, (_LINEAR, _CONSTANT): _handle_division_ANY_constant, - (_LINEAR, _LINEAR): _handle_division_nonlinear, - (_LINEAR, _GENERAL): _handle_division_nonlinear, (_GENERAL, _CONSTANT): _handle_division_ANY_constant, - (_GENERAL, _LINEAR): _handle_division_nonlinear, - (_GENERAL, _GENERAL): _handle_division_nonlinear, } # @@ -366,15 +357,10 @@ def _handle_pow_nonlinear(visitor, node, arg1, arg2): _exit_node_handlers[PowExpression] = { + None: _handle_pow_nonlinear, (_CONSTANT, _CONSTANT): _handle_pow_constant_constant, - (_CONSTANT, _LINEAR): _handle_pow_nonlinear, - (_CONSTANT, _GENERAL): _handle_pow_nonlinear, (_LINEAR, _CONSTANT): _handle_pow_ANY_constant, - (_LINEAR, _LINEAR): _handle_pow_nonlinear, - (_LINEAR, _GENERAL): _handle_pow_nonlinear, (_GENERAL, _CONSTANT): _handle_pow_ANY_constant, - (_GENERAL, _LINEAR): _handle_pow_nonlinear, - (_GENERAL, _GENERAL): _handle_pow_nonlinear, } # @@ -397,9 +383,8 @@ def _handle_unary_nonlinear(visitor, node, arg): _exit_node_handlers[UnaryFunctionExpression] = { + None: _handle_unary_nonlinear, (_CONSTANT,): _handle_unary_constant, - (_LINEAR,): _handle_unary_nonlinear, - (_GENERAL,): _handle_unary_nonlinear, } _exit_node_handlers[AbsExpression] = _exit_node_handlers[UnaryFunctionExpression] @@ -422,9 +407,8 @@ def _handle_named_ANY(visitor, node, arg1): _exit_node_handlers[Expression] = { + None: _handle_named_ANY, (_CONSTANT,): _handle_named_constant, - (_LINEAR,): _handle_named_ANY, - (_GENERAL,): _handle_named_ANY, } # @@ -457,12 +441,7 @@ def _handle_expr_if_nonlinear(visitor, node, arg1, arg2, arg3): return _GENERAL, ans -_exit_node_handlers[Expr_ifExpression] = { - (i, j, k): _handle_expr_if_nonlinear - for i in (_LINEAR, _GENERAL) - for j in (_CONSTANT, _LINEAR, _GENERAL) - for k in (_CONSTANT, _LINEAR, _GENERAL) -} +_exit_node_handlers[Expr_ifExpression] = {None: _handle_expr_if_nonlinear} for j in (_CONSTANT, _LINEAR, _GENERAL): for k in (_CONSTANT, _LINEAR, _GENERAL): _exit_node_handlers[Expr_ifExpression][_CONSTANT, j, k] = _handle_expr_if_const @@ -495,11 +474,9 @@ def _handle_equality_general(visitor, node, arg1, arg2): _exit_node_handlers[EqualityExpression] = { - (i, j): _handle_equality_general - for i in (_CONSTANT, _LINEAR, _GENERAL) - for j in (_CONSTANT, _LINEAR, _GENERAL) + None: _handle_equality_general, + (_CONSTANT, _CONSTANT): _handle_equality_const, } -_exit_node_handlers[EqualityExpression][_CONSTANT, _CONSTANT] = _handle_equality_const def _handle_inequality_const(visitor, node, arg1, arg2): @@ -525,13 +502,9 @@ def _handle_inequality_general(visitor, node, arg1, arg2): _exit_node_handlers[InequalityExpression] = { - (i, j): _handle_inequality_general - for i in (_CONSTANT, _LINEAR, _GENERAL) - for j in (_CONSTANT, _LINEAR, _GENERAL) + None: _handle_inequality_general, + (_CONSTANT, _CONSTANT): _handle_inequality_const, } -_exit_node_handlers[InequalityExpression][ - _CONSTANT, _CONSTANT -] = _handle_inequality_const def _handle_ranged_const(visitor, node, arg1, arg2, arg3): @@ -562,14 +535,9 @@ def _handle_ranged_general(visitor, node, arg1, arg2, arg3): _exit_node_handlers[RangedExpression] = { - (i, j, k): _handle_ranged_general - for i in (_CONSTANT, _LINEAR, _GENERAL) - for j in (_CONSTANT, _LINEAR, _GENERAL) - for k in (_CONSTANT, _LINEAR, _GENERAL) + None: _handle_ranged_general, + (_CONSTANT, _CONSTANT, _CONSTANT): _handle_ranged_const, } -_exit_node_handlers[RangedExpression][ - _CONSTANT, _CONSTANT, _CONSTANT -] = _handle_ranged_const class LinearBeforeChildDispatcher(BeforeChildDispatcher): @@ -750,7 +718,10 @@ def _initialize_exit_node_dispatcher(exit_handlers): exit_dispatcher = {} for cls, handlers in exit_handlers.items(): for args, fcn in handlers.items(): - exit_dispatcher[(cls, *args)] = fcn + if args is None: + exit_dispatcher[cls] = fcn + else: + exit_dispatcher[(cls, *args)] = fcn return exit_dispatcher diff --git a/pyomo/repn/quadratic.py b/pyomo/repn/quadratic.py index c538d1efc7f..2ff3276f7f5 100644 --- a/pyomo/repn/quadratic.py +++ b/pyomo/repn/quadratic.py @@ -284,18 +284,11 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): _exit_node_handlers[ProductExpression].update( { + None: _handle_product_nonlinear, (_CONSTANT, _QUADRATIC): linear._handle_product_constant_ANY, - (_LINEAR, _QUADRATIC): _handle_product_nonlinear, - (_QUADRATIC, _QUADRATIC): _handle_product_nonlinear, - (_GENERAL, _QUADRATIC): _handle_product_nonlinear, (_QUADRATIC, _CONSTANT): linear._handle_product_ANY_constant, - (_QUADRATIC, _LINEAR): _handle_product_nonlinear, - (_QUADRATIC, _GENERAL): _handle_product_nonlinear, # Replace handler from the linear walker (_LINEAR, _LINEAR): _handle_product_linear_linear, - (_GENERAL, _GENERAL): _handle_product_nonlinear, - (_GENERAL, _LINEAR): _handle_product_nonlinear, - (_LINEAR, _GENERAL): _handle_product_nonlinear, } ) @@ -303,15 +296,7 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): # DIVISION # _exit_node_handlers[DivisionExpression].update( - { - (_CONSTANT, _QUADRATIC): linear._handle_division_nonlinear, - (_LINEAR, _QUADRATIC): linear._handle_division_nonlinear, - (_QUADRATIC, _QUADRATIC): linear._handle_division_nonlinear, - (_GENERAL, _QUADRATIC): linear._handle_division_nonlinear, - (_QUADRATIC, _CONSTANT): linear._handle_division_ANY_constant, - (_QUADRATIC, _LINEAR): linear._handle_division_nonlinear, - (_QUADRATIC, _GENERAL): linear._handle_division_nonlinear, - } + {(_QUADRATIC, _CONSTANT): linear._handle_division_ANY_constant} ) @@ -319,84 +304,42 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): # EXPONENTIATION # _exit_node_handlers[PowExpression].update( - { - (_CONSTANT, _QUADRATIC): linear._handle_pow_nonlinear, - (_LINEAR, _QUADRATIC): linear._handle_pow_nonlinear, - (_QUADRATIC, _QUADRATIC): linear._handle_pow_nonlinear, - (_GENERAL, _QUADRATIC): linear._handle_pow_nonlinear, - (_QUADRATIC, _CONSTANT): linear._handle_pow_ANY_constant, - (_QUADRATIC, _LINEAR): linear._handle_pow_nonlinear, - (_QUADRATIC, _GENERAL): linear._handle_pow_nonlinear, - } + {(_QUADRATIC, _CONSTANT): linear._handle_pow_ANY_constant} ) # # ABS and UNARY handlers # -_exit_node_handlers[AbsExpression][(_QUADRATIC,)] = linear._handle_unary_nonlinear -_exit_node_handlers[UnaryFunctionExpression][ - (_QUADRATIC,) -] = linear._handle_unary_nonlinear +# (no changes needed) # # NAMED EXPRESSION handlers # -_exit_node_handlers[Expression][(_QUADRATIC,)] = linear._handle_named_ANY +# (no changes needed) # # EXPR_IF handlers # # Note: it is easier to just recreate the entire data structure, rather # than update it -_exit_node_handlers[Expr_ifExpression] = { - (i, j, k): linear._handle_expr_if_nonlinear - for i in (_LINEAR, _QUADRATIC, _GENERAL) - for j in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL) - for k in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL) -} -for j in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL): - for k in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL): - _exit_node_handlers[Expr_ifExpression][ - _CONSTANT, j, k - ] = linear._handle_expr_if_const - -# -# RELATIONAL handlers -# -_exit_node_handlers[EqualityExpression].update( +_exit_node_handlers[Expr_ifExpression].update( { - (_CONSTANT, _QUADRATIC): linear._handle_equality_general, - (_LINEAR, _QUADRATIC): linear._handle_equality_general, - (_QUADRATIC, _QUADRATIC): linear._handle_equality_general, - (_GENERAL, _QUADRATIC): linear._handle_equality_general, - (_QUADRATIC, _CONSTANT): linear._handle_equality_general, - (_QUADRATIC, _LINEAR): linear._handle_equality_general, - (_QUADRATIC, _GENERAL): linear._handle_equality_general, + (_CONSTANT, i, _QUADRATIC): linear._handle_expr_if_const + for i in (_CONSTANT, _LINEAR, _QUADRATIC, _GENERAL) } ) -_exit_node_handlers[InequalityExpression].update( +_exit_node_handlers[Expr_ifExpression].update( { - (_CONSTANT, _QUADRATIC): linear._handle_inequality_general, - (_LINEAR, _QUADRATIC): linear._handle_inequality_general, - (_QUADRATIC, _QUADRATIC): linear._handle_inequality_general, - (_GENERAL, _QUADRATIC): linear._handle_inequality_general, - (_QUADRATIC, _CONSTANT): linear._handle_inequality_general, - (_QUADRATIC, _LINEAR): linear._handle_inequality_general, - (_QUADRATIC, _GENERAL): linear._handle_inequality_general, - } -) -_exit_node_handlers[RangedExpression].update( - { - (_CONSTANT, _QUADRATIC): linear._handle_ranged_general, - (_LINEAR, _QUADRATIC): linear._handle_ranged_general, - (_QUADRATIC, _QUADRATIC): linear._handle_ranged_general, - (_GENERAL, _QUADRATIC): linear._handle_ranged_general, - (_QUADRATIC, _CONSTANT): linear._handle_ranged_general, - (_QUADRATIC, _LINEAR): linear._handle_ranged_general, - (_QUADRATIC, _GENERAL): linear._handle_ranged_general, + (_CONSTANT, _QUADRATIC, i): linear._handle_expr_if_const + for i in (_CONSTANT, _LINEAR, _GENERAL) } ) +# +# RELATIONAL handlers +# +# (no changes needed) + class QuadraticRepnVisitor(linear.LinearRepnVisitor): Result = QuadraticRepn From 81f6e273d5585bc7ca29d5b3531e0bdb91e2e8eb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 20:06:55 -0700 Subject: [PATCH 0850/3044] rework ExitNodeDispatcher.__missing__ to make default fallback cleaner --- pyomo/repn/util.py | 55 +++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 49cca32eaf9..7bba4041c70 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -401,6 +401,13 @@ def __init__(self, *args, **kwargs): def __missing__(self, key): if type(key) is tuple: node_class = key[0] + node_args = key[1:] + # Only lookup/cache argument-specific handlers for unary, + # binary and ternary operators + if len(key) > 3: + key = node_class + if key in self: + return self[key] else: node_class = key bases = node_class.__mro__ @@ -412,35 +419,33 @@ def __missing__(self, key): bases = [Expression] fcn = None for base_type in bases: - if isinstance(key, tuple): - base_key = (base_type,) + key[1:] - # Only cache handlers for unary, binary and ternary operators - cache = len(key) <= 4 - else: - base_key = base_type - cache = True - if base_key in self: - fcn = self[base_key] - elif base_type in self: + if key is not node_class: + if (base_type,) + node_args in self: + fcn = self[(base_type,) + node_args] + break + if base_type in self: fcn = self[base_type] - elif any((k[0] if type(k) is tuple else k) is base_type for k in self): - raise DeveloperError( - f"Base expression key '{base_key}' not found when inserting " - f"dispatcher for node '{node_class.__name__}' while walking " - "expression tree." - ) + break if fcn is None: - fcn = self.unexpected_expression_type - if cache: - self[key] = fcn + partial_matches = set( + k[0] for k in self if type(k) is tuple and issubclass(node_class, k[0]) + ) + for base_type in node_class.__mro__: + if node_class is not key: + key = (base_type,) + node_args + if base_type in partial_matches: + raise DeveloperError( + f"Base expression key '{key}' not found when inserting " + f"dispatcher for node '{node_class.__name__}' while walking " + "expression tree." + ) + raise DeveloperError( + f"Unexpected expression node type '{node_class.__name__}' " + f"found while walking expression tree in {type(self).__name__}." + ) + self[key] = fcn return fcn - def unexpected_expression_type(self, visitor, node, *arg): - raise DeveloperError( - f"Unexpected expression node type '{type(node).__name__}' " - f"found while walking expression tree in {type(visitor).__name__}." - ) - def apply_node_operation(node, args): try: From 4c9d44b46966372a4031ca66e9110f907fbc55e6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 20:07:27 -0700 Subject: [PATCH 0851/3044] Minor dispatcher performance improvement --- pyomo/repn/linear.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 68dda60d3c0..ed1d8f6b32f 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -218,20 +218,18 @@ def _handle_negation_ANY(visitor, node, arg): def _handle_product_constant_constant(visitor, node, arg1, arg2): - _, arg1 = arg1 - _, arg2 = arg2 - ans = arg1 * arg2 + ans = arg1[1] * arg2[1] if ans != ans: - if not arg1 or not arg2: + if not arg1[1] or not arg2[1]: deprecation_warning( - f"Encountered {str(arg1)}*{str(arg2)} in expression tree. " + f"Encountered {str(arg1[1])}*{str(arg2[1])} in expression tree. " "Mapping the NaN result to 0 for compatibility " "with the lp_v1 writer. In the future, this NaN " "will be preserved/emitted to comply with IEEE-754.", version='6.6.0', ) - return _, 0 - return _, arg1 * arg2 + return _CONSTANT, 0 + return _CONSTANT, ans def _handle_product_constant_ANY(visitor, node, arg1, arg2): @@ -324,8 +322,7 @@ def _handle_division_nonlinear(visitor, node, arg1, arg2): # -def _handle_pow_constant_constant(visitor, node, *args): - arg1, arg2 = args +def _handle_pow_constant_constant(visitor, node, arg1, arg2): ans = apply_node_operation(node, (arg1[1], arg2[1])) if ans.__class__ in native_complex_types: ans = complex_number_error(ans, visitor, node) From 795bb26fda37024bef96467b484ef138ebbfaa17 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 20:08:17 -0700 Subject: [PATCH 0852/3044] update tests: we no longer cache the unknown error handler --- pyomo/repn/tests/test_util.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/repn/tests/test_util.py b/pyomo/repn/tests/test_util.py index b5e4cc4facf..e0fea0fb45c 100644 --- a/pyomo/repn/tests/test_util.py +++ b/pyomo/repn/tests/test_util.py @@ -718,16 +718,14 @@ class UnknownExpression(NumericExpression): DeveloperError, r".*Unexpected expression node type 'UnknownExpression'" ): end[node.__class__](None, node, *node.args) - self.assertEqual(len(end), 9) - self.assertIn(UnknownExpression, end) + self.assertEqual(len(end), 8) node = UnknownExpression((6, 7)) with self.assertRaisesRegex( DeveloperError, r".*Unexpected expression node type 'UnknownExpression'" ): end[node.__class__, 6, 7](None, node, *node.args) - self.assertEqual(len(end), 10) - self.assertIn((UnknownExpression, 6, 7), end) + self.assertEqual(len(end), 8) def test_BeforeChildDispatcher_registration(self): class BeforeChildDispatcherTester(BeforeChildDispatcher): From 44b7ef2fca90843d807ff818ce36efea78a09713 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 21:32:05 -0700 Subject: [PATCH 0853/3044] Fix raw string escaping --- pyomo/contrib/latex_printer/latex_printer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 1d5279e984a..0a595dd8e1b 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -1081,7 +1081,7 @@ def generate_set_name(st, lcm): generate_set_name(s, lcm) for s in st.subsets(False) ) else: - return str(st).replace('_', r'\_').replace('{', '\{').replace('}', '\}') + return str(st).replace('_', r'\_').replace('{', r'\{').replace('}', r'\}') # Handling the iterator indices defaultSetLatexNames = ComponentMap() From 8730e17a67541469c84a8d955ac5868c34da17a9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 21:56:42 -0700 Subject: [PATCH 0854/3044] restore unexpected_expression_type hook --- pyomo/repn/util.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 7bba4041c70..a51ee1c6d64 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -439,13 +439,16 @@ def __missing__(self, key): f"dispatcher for node '{node_class.__name__}' while walking " "expression tree." ) - raise DeveloperError( - f"Unexpected expression node type '{node_class.__name__}' " - f"found while walking expression tree in {type(self).__name__}." - ) + return self.unexpected_expression_type self[key] = fcn return fcn + def unexpected_expression_type(self, visitor, node, *args): + raise DeveloperError( + f"Unexpected expression node type '{type(node).__name__}' " + f"found while walking expression tree in {type(self).__name__}." + ) + def apply_node_operation(node, args): try: From 53fe3455429b8ee004a05d6d643cc2955f17602b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 22:22:09 -0700 Subject: [PATCH 0855/3044] Improve automatic flattening of LinearExpression args --- pyomo/core/expr/numeric_expr.py | 33 +++++++------ pyomo/core/expr/template_expr.py | 6 +-- .../unit/test_numeric_expr_dispatcher.py | 8 ++-- .../unit/test_numeric_expr_zerofilter.py | 8 ++-- pyomo/core/tests/unit/test_template_expr.py | 48 +++++++++---------- 5 files changed, 53 insertions(+), 50 deletions(-) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index c1199ffdcad..e8f7227208c 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -2283,8 +2283,11 @@ def _iadd_mutablenpvsum_mutable(a, b): def _iadd_mutablenpvsum_native(a, b): if not b: return a - a._args_.append(b) - a._nargs += 1 + if a._args_ and a._args_[-1].__class__ in native_numeric_types: + a._args_[-1] += b + else: + a._args_.append(b) + a._nargs += 1 return a @@ -2296,9 +2299,7 @@ def _iadd_mutablenpvsum_npv(a, b): def _iadd_mutablenpvsum_param(a, b): if b.is_constant(): - b = b.value - if not b: - return a + return _iadd_mutablesum_native(a, b.value) a._args_.append(b) a._nargs += 1 return a @@ -2379,8 +2380,11 @@ def _iadd_mutablelinear_mutable(a, b): def _iadd_mutablelinear_native(a, b): if not b: return a - a._args_.append(b) - a._nargs += 1 + if a._args_ and a._args_[-1].__class__ in native_numeric_types: + a._args_[-1] += b + else: + a._args_.append(b) + a._nargs += 1 return a @@ -2392,9 +2396,7 @@ def _iadd_mutablelinear_npv(a, b): def _iadd_mutablelinear_param(a, b): if b.is_constant(): - b = b.value - if not b: - return a + return _iadd_mutablesum_native(a, b.value) a._args_.append(b) a._nargs += 1 return a @@ -2478,8 +2480,11 @@ def _iadd_mutablesum_mutable(a, b): def _iadd_mutablesum_native(a, b): if not b: return a - a._args_.append(b) - a._nargs += 1 + if a._args_ and a._args_[-1].__class__ in native_numeric_types: + a._args_[-1] += b + else: + a._args_.append(b) + a._nargs += 1 return a @@ -2491,9 +2496,7 @@ def _iadd_mutablesum_npv(a, b): def _iadd_mutablesum_param(a, b): if b.is_constant(): - b = b.value - if not b: - return a + return _iadd_mutablesum_native(a, b.value) a._args_.append(b) a._nargs += 1 return a diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index f65a1f2b9b0..f982ef38d1d 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -116,7 +116,7 @@ def _to_string(self, values, verbose, smap): return "%s[%s]" % (values[0], ','.join(values[1:])) def _resolve_template(self, args): - return args[0].__getitem__(tuple(args[1:])) + return args[0][*args[1:]] def _apply_operation(self, result): args = tuple( @@ -127,7 +127,7 @@ def _apply_operation(self, result): ) for arg in result[1:] ) - return result[0].__getitem__(tuple(result[1:])) + return result[0][*result[1:]] class Numeric_GetItemExpression(GetItemExpression, NumericExpression): @@ -273,7 +273,7 @@ def _to_string(self, values, verbose, smap): return "%s.%s" % (values[0], attr) def _resolve_template(self, args): - return getattr(*tuple(args)) + return getattr(*args) class Numeric_GetAttrExpression(GetAttrExpression, NumericExpression): diff --git a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py index 3787f00de47..7c6e2af9974 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py +++ b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py @@ -6548,11 +6548,11 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.invalid, NotImplemented), (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), - (mutable_npv, self.one, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.one, _MutableNPVSumExpression([11])), # 4: - (mutable_npv, self.native, _MutableNPVSumExpression([10, 5])), + (mutable_npv, self.native, _MutableNPVSumExpression([15])), (mutable_npv, self.npv, _MutableNPVSumExpression([10, self.npv])), - (mutable_npv, self.param, _MutableNPVSumExpression([10, 6])), + (mutable_npv, self.param, _MutableNPVSumExpression([16])), ( mutable_npv, self.param_mut, @@ -6592,7 +6592,7 @@ def test_mutable_nvp_iadd(self): _MutableSumExpression([10] + self.mutable_l2.args), ), (mutable_npv, self.param0, _MutableNPVSumExpression([10])), - (mutable_npv, self.param1, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.param1, _MutableNPVSumExpression([11])), # 20: (mutable_npv, self.mutable_l3, _MutableNPVSumExpression([10, self.npv])), ] diff --git a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py index 162d664e0f8..34d2e1cc2c2 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py +++ b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py @@ -6076,11 +6076,11 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.invalid, NotImplemented), (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), - (mutable_npv, self.one, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.one, _MutableNPVSumExpression([11])), # 4: - (mutable_npv, self.native, _MutableNPVSumExpression([10, 5])), + (mutable_npv, self.native, _MutableNPVSumExpression([15])), (mutable_npv, self.npv, _MutableNPVSumExpression([10, self.npv])), - (mutable_npv, self.param, _MutableNPVSumExpression([10, 6])), + (mutable_npv, self.param, _MutableNPVSumExpression([16])), ( mutable_npv, self.param_mut, @@ -6120,7 +6120,7 @@ def test_mutable_nvp_iadd(self): _MutableSumExpression([10] + self.mutable_l2.args), ), (mutable_npv, self.param0, _MutableNPVSumExpression([10])), - (mutable_npv, self.param1, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.param1, _MutableNPVSumExpression([11])), # 20: (mutable_npv, self.mutable_l3, _MutableNPVSumExpression([10, self.npv])), ] diff --git a/pyomo/core/tests/unit/test_template_expr.py b/pyomo/core/tests/unit/test_template_expr.py index 4f255e3567a..4c872e1e11d 100644 --- a/pyomo/core/tests/unit/test_template_expr.py +++ b/pyomo/core/tests/unit/test_template_expr.py @@ -490,14 +490,14 @@ def c(m): self.assertEqual( str(resolve_template(template)), 'x[1,1,10] + ' - '(x[2,1,10] + x[2,1,20]) + ' - '(x[3,1,10] + x[3,1,20] + x[3,1,30]) + ' - '(x[1,2,10]) + ' - '(x[2,2,10] + x[2,2,20]) + ' - '(x[3,2,10] + x[3,2,20] + x[3,2,30]) + ' - '(x[1,3,10]) + ' - '(x[2,3,10] + x[2,3,20]) + ' - '(x[3,3,10] + x[3,3,20] + x[3,3,30]) <= 0', + 'x[2,1,10] + x[2,1,20] + ' + 'x[3,1,10] + x[3,1,20] + x[3,1,30] + ' + 'x[1,2,10] + ' + 'x[2,2,10] + x[2,2,20] + ' + 'x[3,2,10] + x[3,2,20] + x[3,2,30] + ' + 'x[1,3,10] + ' + 'x[2,3,10] + x[2,3,20] + ' + 'x[3,3,10] + x[3,3,20] + x[3,3,30] <= 0', ) def test_multidim_nested_sum_rule(self): @@ -566,14 +566,14 @@ def c(m): self.assertEqual( str(resolve_template(template)), 'x[1,1,10] + ' - '(x[2,1,10] + x[2,1,20]) + ' - '(x[3,1,10] + x[3,1,20] + x[3,1,30]) + ' - '(x[1,2,10]) + ' - '(x[2,2,10] + x[2,2,20]) + ' - '(x[3,2,10] + x[3,2,20] + x[3,2,30]) + ' - '(x[1,3,10]) + ' - '(x[2,3,10] + x[2,3,20]) + ' - '(x[3,3,10] + x[3,3,20] + x[3,3,30]) <= 0', + 'x[2,1,10] + x[2,1,20] + ' + 'x[3,1,10] + x[3,1,20] + x[3,1,30] + ' + 'x[1,2,10] + ' + 'x[2,2,10] + x[2,2,20] + ' + 'x[3,2,10] + x[3,2,20] + x[3,2,30] + ' + 'x[1,3,10] + ' + 'x[2,3,10] + x[2,3,20] + ' + 'x[3,3,10] + x[3,3,20] + x[3,3,30] <= 0', ) def test_multidim_nested_getattr_sum_rule(self): @@ -609,14 +609,14 @@ def c(m): self.assertEqual( str(resolve_template(template)), 'x[1,1,10] + ' - '(x[2,1,10] + x[2,1,20]) + ' - '(x[3,1,10] + x[3,1,20] + x[3,1,30]) + ' - '(x[1,2,10]) + ' - '(x[2,2,10] + x[2,2,20]) + ' - '(x[3,2,10] + x[3,2,20] + x[3,2,30]) + ' - '(x[1,3,10]) + ' - '(x[2,3,10] + x[2,3,20]) + ' - '(x[3,3,10] + x[3,3,20] + x[3,3,30]) <= 0', + 'x[2,1,10] + x[2,1,20] + ' + 'x[3,1,10] + x[3,1,20] + x[3,1,30] + ' + 'x[1,2,10] + ' + 'x[2,2,10] + x[2,2,20] + ' + 'x[3,2,10] + x[3,2,20] + x[3,2,30] + ' + 'x[1,3,10] + ' + 'x[2,3,10] + x[2,3,20] + ' + 'x[3,3,10] + x[3,3,20] + x[3,3,30] <= 0', ) def test_eval_getattr(self): From ac6949244d4fea5ae7e4b50750a1fb3d783c3fbd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 22:23:03 -0700 Subject: [PATCH 0856/3044] bugfix: resolution of TemplateSumExpression --- pyomo/core/expr/template_expr.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index f982ef38d1d..6ac4c8c041f 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -19,11 +19,12 @@ from pyomo.core.expr.base import ExpressionBase, ExpressionArgs_Mixin, NPV_Mixin from pyomo.core.expr.logical_expr import BooleanExpression from pyomo.core.expr.numeric_expr import ( + ARG_TYPE, NumericExpression, - SumExpression, Numeric_NPV_Mixin, + SumExpression, + mutable_expression, register_arg_type, - ARG_TYPE, _balanced_parens, ) from pyomo.core.expr.numvalue import ( @@ -521,7 +522,15 @@ def _to_string(self, values, verbose, smap): return 'SUM(%s %s)' % (val, iterStr) def _resolve_template(self, args): - return SumExpression(args) + with mutable_expression() as e: + for arg in args: + e += arg + if e.nargs() > 1: + return e + elif not e.nargs(): + return 0 + else: + return e.arg(0) class IndexTemplate(NumericValue): From d46c90df00347927d2d56403db9c7016c0336ab3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 22:23:23 -0700 Subject: [PATCH 0857/3044] NFC: remove coverage pragma --- pyomo/core/expr/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/expr/base.py b/pyomo/core/expr/base.py index f506956e478..6e2066afcc5 100644 --- a/pyomo/core/expr/base.py +++ b/pyomo/core/expr/base.py @@ -360,7 +360,7 @@ def size(self): """ return visitor.sizeof_expression(self) - def _apply_operation(self, result): # pragma: no cover + def _apply_operation(self, result): """ Compute the values of this node given the values of its children. From dd27f662ecfe56f80343736a4839cb58faeaf22a Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 6 Mar 2024 17:30:17 -0700 Subject: [PATCH 0858/3044] add option to remove bounds from fixed variables to TemporarySubsystemManager --- pyomo/util/subsystems.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 70a0af1b2a7..3e4be46fca0 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -148,7 +148,14 @@ class TemporarySubsystemManager(object): """ - def __init__(self, to_fix=None, to_deactivate=None, to_reset=None, to_unfix=None): + def __init__( + self, + to_fix=None, + to_deactivate=None, + to_reset=None, + to_unfix=None, + remove_bounds_on_fix=False, + ): """ Arguments --------- @@ -168,6 +175,8 @@ def __init__(self, to_fix=None, to_deactivate=None, to_reset=None, to_unfix=None List of var data objects to be temporarily unfixed. These are restored to their original status on exit from this object's context manager. + remove_bounds_on_fix: Bool + Whether bounds should be removed temporarily for fixed variables """ if to_fix is None: @@ -194,6 +203,8 @@ def __init__(self, to_fix=None, to_deactivate=None, to_reset=None, to_unfix=None self._con_was_active = None self._comp_original_value = None self._var_was_unfixed = None + self._remove_bounds_on_fix = remove_bounds_on_fix + self._fixed_var_bounds = None def __enter__(self): to_fix = self._vars_to_fix @@ -203,8 +214,13 @@ def __enter__(self): self._var_was_fixed = [(var, var.fixed) for var in to_fix + to_unfix] self._con_was_active = [(con, con.active) for con in to_deactivate] self._comp_original_value = [(comp, comp.value) for comp in to_set] + self._fixed_var_bounds = [(var.lb, var.ub) for var in to_fix] for var in self._vars_to_fix: + if self._remove_bounds_on_fix: + # TODO: Potentially override var.domain as well? + var.setlb(None) + var.setub(None) var.fix() for con in self._cons_to_deactivate: @@ -223,6 +239,11 @@ def __exit__(self, ex_type, ex_val, ex_bt): var.fix() else: var.unfix() + if self._remove_bounds_on_fix: + for var, (lb, ub) in zip(self._vars_to_fix, self._fixed_var_bounds): + var.setlb(lb) + var.setub(ub) + for con, was_active in self._con_was_active: if was_active: con.activate() From 1a1c1a88966ebd2066dc6b0418f8c9b36eb0ced6 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 6 Mar 2024 17:30:39 -0700 Subject: [PATCH 0859/3044] timing calls and option to not use calculate_variable_from_constraint in solve_strongly_connected_components --- .../contrib/incidence_analysis/scc_solver.py | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 835e07c7c02..d965fb38203 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -11,6 +11,7 @@ import logging +from pyomo.common.timing import HierarchicalTimer from pyomo.core.base.constraint import Constraint from pyomo.util.calc_var_value import calculate_variable_from_constraint from pyomo.util.subsystems import TemporarySubsystemManager, generate_subsystem_blocks @@ -18,6 +19,7 @@ IncidenceGraphInterface, _generate_variables_in_constraints, ) +from pyomo.contrib.incidence_analysis.config import IncidenceMethod _log = logging.getLogger(__name__) @@ -73,7 +75,13 @@ def generate_strongly_connected_components( def solve_strongly_connected_components( - block, solver=None, solve_kwds=None, calc_var_kwds=None + block, + *, + solver=None, + solve_kwds=None, + use_calc_var=False, + calc_var_kwds=None, + timer=None, ): """Solve a square system of variables and equality constraints by solving strongly connected components individually. @@ -98,6 +106,9 @@ def solve_strongly_connected_components( a solve method. solve_kwds: Dictionary Keyword arguments for the solver's solve method + use_calc_var: Bool + Whether to use ``calculate_variable_from_constraint`` for one-by-one + square system solves calc_var_kwds: Dictionary Keyword arguments for calculate_variable_from_constraint @@ -110,25 +121,36 @@ def solve_strongly_connected_components( solve_kwds = {} if calc_var_kwds is None: calc_var_kwds = {} + if timer is None: + timer = HierarchicalTimer() + timer.start("igraph") igraph = IncidenceGraphInterface( - block, active=True, include_fixed=False, include_inequality=False + block, + active=True, + include_fixed=False, + include_inequality=False, + method=IncidenceMethod.ampl_repn, ) + timer.stop("igraph") constraints = igraph.constraints variables = igraph.variables res_list = [] log_blocks = _log.isEnabledFor(logging.DEBUG) + timer.start("generate-scc") for scc, inputs in generate_strongly_connected_components(constraints, variables): - with TemporarySubsystemManager(to_fix=inputs): + timer.stop("generate-scc") + with TemporarySubsystemManager(to_fix=inputs, remove_bounds_on_fix=True): N = len(scc.vars) - if N == 1: + if N == 1 and use_calc_var: if log_blocks: _log.debug(f"Solving 1x1 block: {scc.cons[0].name}.") + timer.start("calc-var-from-con") results = calculate_variable_from_constraint( scc.vars[0], scc.cons[0], **calc_var_kwds ) - res_list.append(results) + timer.stop("calc-var-from-con") else: if solver is None: var_names = [var.name for var in scc.vars.values()][:10] @@ -141,6 +163,10 @@ def solve_strongly_connected_components( ) if log_blocks: _log.debug(f"Solving {N}x{N} block.") + timer.start("scc-subsolver") results = solver.solve(scc, **solve_kwds) - res_list.append(results) + timer.stop("scc-subsolver") + res_list.append(results) + timer.start("generate-scc") + timer.stop("generate-scc") return res_list From 3d7f2c30da36f12b3f49f5cef0f2f887fe19fec2 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 6 Mar 2024 17:50:17 -0700 Subject: [PATCH 0860/3044] timing calls in generate-scc and option to reuse an incidence graph if one already exists --- .../contrib/incidence_analysis/scc_solver.py | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index d965fb38203..1d59be0bc71 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -26,7 +26,11 @@ def generate_strongly_connected_components( - constraints, variables=None, include_fixed=False + constraints, + variables=None, + include_fixed=False, + igraph=None, + timer=None, ): """Yield in order ``_BlockData`` that each contain the variables and constraints of a single diagonal block in a block lower triangularization @@ -55,23 +59,38 @@ def generate_strongly_connected_components( "input variables" for that block. """ + if timer is None: + timer = HierarchicalTimer() if variables is None: + timer.start("generate-vars") variables = list( - _generate_variables_in_constraints(constraints, include_fixed=include_fixed) + _generate_variables_in_constraints( + constraints, + include_fixed=include_fixed, + #method=IncidenceMethod.ampl_repn + ) ) + timer.stop("generate-vars") assert len(variables) == len(constraints) - igraph = IncidenceGraphInterface() + if igraph is None: + igraph = IncidenceGraphInterface() + timer.start("block-triang") var_blocks, con_blocks = igraph.block_triangularize( variables=variables, constraints=constraints ) + timer.stop("block-triang") subsets = [(cblock, vblock) for vblock, cblock in zip(var_blocks, con_blocks)] + timer.start("subsystem-blocks") for block, inputs in generate_subsystem_blocks( subsets, include_fixed=include_fixed ): + timer.stop("subsystem-blocks") # TODO: How does len scale for reference-to-list? assert len(block.vars) == len(block.cons) yield (block, inputs) + timer.start("subsystem-blocks") + timer.stop("subsystem-blocks") def solve_strongly_connected_components( @@ -139,7 +158,9 @@ def solve_strongly_connected_components( res_list = [] log_blocks = _log.isEnabledFor(logging.DEBUG) timer.start("generate-scc") - for scc, inputs in generate_strongly_connected_components(constraints, variables): + for scc, inputs in generate_strongly_connected_components( + constraints, variables, timer=timer, igraph=igraph + ): timer.stop("generate-scc") with TemporarySubsystemManager(to_fix=inputs, remove_bounds_on_fix=True): N = len(scc.vars) From 3b5aaa66d0abd83064f48762d54f759a52a3a92d Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 09:06:56 -0700 Subject: [PATCH 0861/3044] remove unnecessary import --- pyomo/contrib/incidence_analysis/scc_solver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index e7ec2bfc75b..b95a9bb66a5 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -29,7 +29,6 @@ _log = logging.getLogger(__name__) -from pyomo.common.timing import HierarchicalTimer def generate_strongly_connected_components( constraints, variables=None, From 565a29ecdcbae605f381b3e7cd13ae112843babb Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 09:08:11 -0700 Subject: [PATCH 0862/3044] remove unnecessary imports --- pyomo/contrib/incidence_analysis/scc_solver.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index b95a9bb66a5..5e21631f2ef 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -14,11 +14,7 @@ from pyomo.common.timing import HierarchicalTimer from pyomo.core.base.constraint import Constraint from pyomo.util.calc_var_value import calculate_variable_from_constraint -from pyomo.util.subsystems import ( - TemporarySubsystemManager, - generate_subsystem_blocks, - create_subsystem_block, -) +from pyomo.util.subsystems import TemporarySubsystemManager, generate_subsystem_blocks from pyomo.contrib.incidence_analysis.interface import ( IncidenceGraphInterface, _generate_variables_in_constraints, From 1902e8e1f9ce04153a22d324fae5015342eb23ce Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 09:11:35 -0700 Subject: [PATCH 0863/3044] Allow bare variables in LinearExpression nodes --- pyomo/core/expr/numeric_expr.py | 39 ++++++++++++++++-------------- pyomo/repn/linear.py | 38 ++++++++++++++++------------- pyomo/repn/plugins/baron_writer.py | 19 ++++++++++++--- pyomo/repn/plugins/gams_writer.py | 10 +++++++- pyomo/repn/plugins/nl_writer.py | 14 +++++++++++ pyomo/repn/quadratic.py | 25 +++++++------------ pyomo/repn/standard_repn.py | 22 +++++++++++++++++ pyomo/repn/tests/test_linear.py | 2 +- 8 files changed, 112 insertions(+), 57 deletions(-) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index e8f7227208c..2cf4073b49f 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -1298,8 +1298,14 @@ def _build_cache(self): if arg.__class__ is MonomialTermExpression: coef.append(arg._args_[0]) var.append(arg._args_[1]) - else: + elif arg.__class__ in native_numeric_types: const += arg + elif not arg.is_potentially_variable(): + const += arg + else: + assert arg.is_potentially_variable() + coef.append(1) + var.append(arg) LinearExpression._cache = (self, const, coef, var) @property @@ -1325,7 +1331,7 @@ def create_node_with_local_data(self, args, classtype=None): classtype = self.__class__ if type(args) is not list: args = list(args) - for i, arg in enumerate(args): + for arg in args: if arg.__class__ in self._allowable_linear_expr_arg_types: # 99% of the time, the arg type hasn't changed continue @@ -1336,8 +1342,7 @@ def create_node_with_local_data(self, args, classtype=None): # NPV expressions are OK pass elif arg.is_variable_type(): - # vars are OK, but need to be mapped to monomial terms - args[i] = MonomialTermExpression((1, arg)) + # vars are OK continue else: # For anything else, convert this to a general sum @@ -1820,7 +1825,7 @@ def _add_native_param(a, b): def _add_native_var(a, b): if not a: return b - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_native_monomial(a, b): @@ -1871,7 +1876,7 @@ def _add_npv_param(a, b): def _add_npv_var(a, b): - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_npv_monomial(a, b): @@ -1929,7 +1934,7 @@ def _add_param_var(a, b): a = a.value if not a: return b - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_param_monomial(a, b): @@ -1972,11 +1977,11 @@ def _add_param_other(a, b): def _add_var_native(a, b): if not b: return a - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_npv(a, b): - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_param(a, b): @@ -1984,21 +1989,19 @@ def _add_var_param(a, b): b = b.value if not b: return a - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_var(a, b): - return LinearExpression( - [MonomialTermExpression((1, a)), MonomialTermExpression((1, b))] - ) + return LinearExpression([a, b]) def _add_var_monomial(a, b): - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_linear(a, b): - return b._trunc_append(MonomialTermExpression((1, a))) + return b._trunc_append(a) def _add_var_sum(a, b): @@ -2033,7 +2036,7 @@ def _add_monomial_param(a, b): def _add_monomial_var(a, b): - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_monomial_monomial(a, b): @@ -2076,7 +2079,7 @@ def _add_linear_param(a, b): def _add_linear_var(a, b): - return a._trunc_append(MonomialTermExpression((1, b))) + return a._trunc_append(b) def _add_linear_monomial(a, b): @@ -2403,7 +2406,7 @@ def _iadd_mutablelinear_param(a, b): def _iadd_mutablelinear_var(a, b): - a._args_.append(MonomialTermExpression((1, b))) + a._args_.append(b) a._nargs += 1 return a diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 6ab4abfdaf5..d601ccbcd7c 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -31,8 +31,8 @@ MonomialTermExpression, LinearExpression, SumExpression, - NPV_SumExpression, ExternalFunctionExpression, + mutable_expression, ) from pyomo.core.expr.relational_expr import ( EqualityExpression, @@ -120,22 +120,14 @@ def to_expression(self, visitor): ans = 0 if self.linear: var_map = visitor.var_map - if len(self.linear) == 1: - vid, coef = next(iter(self.linear.items())) - if coef == 1: - ans += var_map[vid] - elif coef: - ans += MonomialTermExpression((coef, var_map[vid])) - else: - pass - else: - ans += LinearExpression( - [ - MonomialTermExpression((coef, var_map[vid])) - for vid, coef in self.linear.items() - if coef - ] - ) + with mutable_expression() as e: + for vid, coef in self.linear.items(): + if coef: + e += coef * var_map[vid] + if e.nargs() > 1: + ans += e + elif e.nargs() == 1: + ans += e.arg(0) if self.constant: ans += self.constant if self.multiplier != 1: @@ -704,6 +696,18 @@ def _before_linear(visitor, child): linear[_id] = arg1 elif arg.__class__ in native_numeric_types: const += arg + elif arg.is_variable_type(): + _id = id(arg) + if _id not in var_map: + if arg.fixed: + const += visitor.check_constant(arg.value, arg) + continue + LinearBeforeChildDispatcher._record_var(visitor, arg) + linear[_id] = 1 + elif _id in linear: + linear[_id] += 1 + else: + linear[_id] = 1 else: try: const += visitor.check_constant(visitor.evaluate(arg), arg) diff --git a/pyomo/repn/plugins/baron_writer.py b/pyomo/repn/plugins/baron_writer.py index de19b5aad73..ab673b0c1c3 100644 --- a/pyomo/repn/plugins/baron_writer.py +++ b/pyomo/repn/plugins/baron_writer.py @@ -174,15 +174,26 @@ def _monomial_to_string(self, node): return self.smap.getSymbol(var) return ftoa(const, True) + '*' + self.smap.getSymbol(var) + def _var_to_string(self, node): + if node.is_fixed(): + return ftoa(node.value, True) + self.variables.add(id(node)) + return self.smap.getSymbol(node) + def _linear_to_string(self, node): values = [ ( self._monomial_to_string(arg) - if ( - arg.__class__ is EXPR.MonomialTermExpression - and not arg.arg(1).is_fixed() + if arg.__class__ is EXPR.MonomialTermExpression + else ( + ftoa(arg) + if arg.__class__ in native_numeric_types + else ( + self._var_to_string(arg) + if arg.is_variable_type() + else ftoa(value(arg), True) + ) ) - else ftoa(value(arg)) ) for arg in node.args ] diff --git a/pyomo/repn/plugins/gams_writer.py b/pyomo/repn/plugins/gams_writer.py index 5f94f176762..0756cb64920 100644 --- a/pyomo/repn/plugins/gams_writer.py +++ b/pyomo/repn/plugins/gams_writer.py @@ -183,7 +183,15 @@ def _linear_to_string(self, node): ( self._monomial_to_string(arg) if arg.__class__ is EXPR.MonomialTermExpression - else ftoa(arg, True) + else ( + ftoa(arg, True) + if arg.__class__ in native_numeric_types + else ( + self.smap.getSymbol(arg) + if arg.is_variable_type() and (not arg.fixed or self.output_fixed_variables) + else ftoa(value(arg), True) + ) + ) ) for arg in node.args ] diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index a256cd1b900..b82d4df77e2 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2780,6 +2780,20 @@ def _before_linear(visitor, child): linear[_id] = arg1 elif arg.__class__ in native_types: const += arg + elif arg.is_variable_type(): + _id = id(arg) + if _id not in var_map: + if arg.fixed: + if _id not in visitor.fixed_vars: + visitor.cache_fixed_var(_id, arg) + const += visitor.fixed_vars[_id] + continue + _before_child_handlers._record_var(visitor, arg) + linear[_id] = 1 + elif _id in linear: + linear[_id] += 1 + else: + linear[_id] = 1 else: try: const += visitor.check_constant(visitor.evaluate(arg), arg) diff --git a/pyomo/repn/quadratic.py b/pyomo/repn/quadratic.py index c538d1efc7f..0ddfda829ed 100644 --- a/pyomo/repn/quadratic.py +++ b/pyomo/repn/quadratic.py @@ -98,22 +98,15 @@ def to_expression(self, visitor): e += coef * (var_map[x1] * var_map[x2]) ans += e if self.linear: - if len(self.linear) == 1: - vid, coef = next(iter(self.linear.items())) - if coef == 1: - ans += var_map[vid] - elif coef: - ans += MonomialTermExpression((coef, var_map[vid])) - else: - pass - else: - ans += LinearExpression( - [ - MonomialTermExpression((coef, var_map[vid])) - for vid, coef in self.linear.items() - if coef - ] - ) + var_map = visitor.var_map + with mutable_expression() as e: + for vid, coef in self.linear.items(): + if coef: + e += coef * var_map[vid] + if e.nargs() > 1: + ans += e + elif e.nargs() == 1: + ans += e.arg(0) if self.constant: ans += self.constant if self.multiplier != 1: diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 8700872f04f..8600a8a50f6 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -321,6 +321,16 @@ def generate_standard_repn( linear_vars[id_] = v elif arg.__class__ in native_numeric_types: C_ += arg + elif arg.is_variable_type(): + if arg.fixed: + C_ += arg.value + continue + id_ = id(arg) + if id_ in linear_coefs: + linear_coefs[id_] += 1 + else: + linear_coefs[id_] = 1 + linear_vars[id_] = arg else: C_ += EXPR.evaluate_expression(arg) else: # compute_values == False @@ -336,6 +346,18 @@ def generate_standard_repn( else: linear_coefs[id_] = c linear_vars[id_] = v + elif arg.__class__ in native_numeric_types: + C_ += arg + elif arg.is_variable_type(): + if arg.fixed: + C_ += arg + continue + id_ = id(arg) + if id_ in linear_coefs: + linear_coefs[id_] += 1 + else: + linear_coefs[id_] = 1 + linear_vars[id_] = arg else: C_ += arg diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index 6843650d0c2..0fd428fd8ee 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -1589,7 +1589,7 @@ def test_to_expression(self): expr.constant = 0 expr.linear[id(m.x)] = 0 expr.linear[id(m.y)] = 0 - assertExpressionsEqual(self, expr.to_expression(visitor), LinearExpression()) + assertExpressionsEqual(self, expr.to_expression(visitor), 0) @unittest.skipUnless(numpy_available, "Test requires numpy") def test_nonnumeric(self): From f36e31109b878178eea70c88d496ece1fd864316 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 09:34:52 -0700 Subject: [PATCH 0864/3044] NFC: apply black --- pyomo/repn/plugins/gams_writer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/gams_writer.py b/pyomo/repn/plugins/gams_writer.py index 0756cb64920..a0f407d7952 100644 --- a/pyomo/repn/plugins/gams_writer.py +++ b/pyomo/repn/plugins/gams_writer.py @@ -188,7 +188,8 @@ def _linear_to_string(self, node): if arg.__class__ in native_numeric_types else ( self.smap.getSymbol(arg) - if arg.is_variable_type() and (not arg.fixed or self.output_fixed_variables) + if arg.is_variable_type() + and (not arg.fixed or self.output_fixed_variables) else ftoa(value(arg), True) ) ) From df4f7af6d75e1a29f3a23c8144bb0945c8908f32 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 10:03:45 -0700 Subject: [PATCH 0865/3044] pass timer to generate_subsystem_blocks --- pyomo/contrib/incidence_analysis/scc_solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 5e21631f2ef..117554c52de 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -84,7 +84,7 @@ def generate_strongly_connected_components( subsets = [(cblock, vblock) for vblock, cblock in zip(var_blocks, con_blocks)] timer.start("generate-block") for block, inputs in generate_subsystem_blocks( - subsets, include_fixed=include_fixed + subsets, include_fixed=include_fixed, timer=timer ): timer.stop("generate-block") # TODO: How does len scale for reference-to-list? From f47345cdaea937cd962a1db6f2ea6f5257437026 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 10:04:45 -0700 Subject: [PATCH 0866/3044] accept timer argument in generate_subsystem_blocks, revert implementation of identify_external_functions to be a generator --- pyomo/util/subsystems.py | 53 +++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index ba1d56a787b..9a2a8b6635d 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -17,6 +17,7 @@ from pyomo.core.base.constraint import Constraint from pyomo.core.base.expression import Expression +from pyomo.core.base.objective import Objective from pyomo.core.base.external import ExternalFunction from pyomo.core.expr.visitor import StreamBasedExpressionVisitor from pyomo.core.expr.numeric_expr import ExternalFunctionExpression @@ -67,47 +68,37 @@ def acceptChildResult(self, node, data, child_result, child_idx): return child_result.is_expression_type(), None -def identify_external_functions( - expr, - descend_into_named_expressions=True, - named_expressions=None, -): - visitor = _ExternalFunctionVisitor( - descend_into_named_expressions=descend_into_named_expressions - ) - efs = list(visitor.walk_expression(expr)) - if not descend_into_named_expressions and named_expressions is not None: - named_expressions.extend(visitor.named_expressions) - return efs - #yield from _ExternalFunctionVisitor().walk_expression(expr) +def identify_external_functions(expr): + # TODO: Potentially support descend_into_named_expressions argument here. + # This will likely require converting from a generator to a function. + yield from _ExternalFunctionVisitor().walk_expression(expr) def add_local_external_functions(block): ef_exprs = [] named_expressions = [] - for comp in block.component_data_objects((Constraint, Expression), active=True): - ef_exprs.extend(identify_external_functions( - comp.expr, - descend_into_named_expressions=False, - named_expressions=named_expressions, - )) - named_expr_set = ComponentSet(named_expressions) + visitor = _ExternalFunctionVisitor(descend_into_named_expressions=False) + for comp in block.component_data_objects( + (Constraint, Expression, Objective), + active=True, + ): + ef_exprs.extend(visitor.walk_expression(comp.expr)) + named_expr_set = ComponentSet(visitor.named_expressions) + # List of unique named expressions named_expressions = list(named_expr_set) while named_expressions: expr = named_expressions.pop() - local_named_exprs = [] - ef_exprs.extend(identify_external_functions( - expr, - descend_into_named_expressions=False, - named_expressions=local_named_exprs, - )) + # Clear named expression cache so we don't re-check named expressions + # we've seen before. + visitor.named_expressions.clear() + ef_exprs.extend(visitor.walk_expression(expr)) # Only add to the stack named expressions that we have # not encountered yet. - for local_expr in local_named_exprs: + for local_expr in visitor.named_expressions: if local_expr not in named_expr_set: named_expressions.append(local_expr) named_expr_set.add(local_expr) - + unique_functions = [] fcn_set = set() for expr in ef_exprs: @@ -184,7 +175,7 @@ def create_subsystem_block( return block -def generate_subsystem_blocks(subsystems, include_fixed=False): +def generate_subsystem_blocks(subsystems, include_fixed=False, timer=None): """Generates blocks that contain subsystems of variables and constraints. Arguments @@ -203,8 +194,10 @@ def generate_subsystem_blocks(subsystems, include_fixed=False): not specified are contained in the input_vars component. """ + if timer is None: + timer = HierarchicalTimer() for cons, vars in subsystems: - block = create_subsystem_block(cons, vars, include_fixed) + block = create_subsystem_block(cons, vars, include_fixed, timer=timer) yield block, list(block.input_vars.values()) From 9eebe2297961854783dd5feb57595b46d8aa3866 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 10:41:49 -0700 Subject: [PATCH 0867/3044] This is a small bug fix to address when there is no objective --- pyomo/contrib/solver/base.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 13bd5ddb212..a2174fea237 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -22,6 +22,7 @@ from pyomo.common.config import document_kwargs_from_configdict from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning +from pyomo.opt import ProblemSense from pyomo.opt.results.results_ import SolverResults as LegacySolverResults from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize @@ -418,9 +419,15 @@ def _map_results(self, model, results): ] legacy_soln.status = legacy_solution_status_map[results.solution_status] legacy_results.solver.termination_message = str(results.termination_condition) + legacy_results.problem.number_of_constraints = model.nconstraints() + legacy_results.problem.number_of_variables = model.nvariables() obj = get_objective(model) - if len(list(obj)) > 0: + if not obj: + legacy_results.problem.sense = ProblemSense.unknown + legacy_results.problem.number_of_objectives = 0 + else: legacy_results.problem.sense = obj.sense + legacy_results.problem.number_of_objectives = len(obj) if obj.sense == minimize: legacy_results.problem.lower_bound = results.objective_bound From 28ecd960b60a2691a43c7eea688252046253e84a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 10:45:42 -0700 Subject: [PATCH 0868/3044] Cannot convert non-constant Pyomo to bool --- pyomo/contrib/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index a2174fea237..035d25bf97d 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -422,7 +422,7 @@ def _map_results(self, model, results): legacy_results.problem.number_of_constraints = model.nconstraints() legacy_results.problem.number_of_variables = model.nvariables() obj = get_objective(model) - if not obj: + if len(obj) == 0: legacy_results.problem.sense = ProblemSense.unknown legacy_results.problem.number_of_objectives = 0 else: From 191fadf94dd9ff1ea5e41ad3d15e193f0346d861 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 10:55:55 -0700 Subject: [PATCH 0869/3044] Change way of checking number of objectives --- pyomo/contrib/solver/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 035d25bf97d..91a581c5998 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -421,11 +421,11 @@ def _map_results(self, model, results): legacy_results.solver.termination_message = str(results.termination_condition) legacy_results.problem.number_of_constraints = model.nconstraints() legacy_results.problem.number_of_variables = model.nvariables() - obj = get_objective(model) - if len(obj) == 0: + if model.nobjectives() == 0: legacy_results.problem.sense = ProblemSense.unknown legacy_results.problem.number_of_objectives = 0 else: + obj = get_objective(model) legacy_results.problem.sense = obj.sense legacy_results.problem.number_of_objectives = len(obj) From 2167deaa97de640e872d4004c979c08d21020ba1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 10:57:44 -0700 Subject: [PATCH 0870/3044] Problem sense is already unknown by default --- pyomo/contrib/solver/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 91a581c5998..f9cd213bf73 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -422,7 +422,6 @@ def _map_results(self, model, results): legacy_results.problem.number_of_constraints = model.nconstraints() legacy_results.problem.number_of_variables = model.nvariables() if model.nobjectives() == 0: - legacy_results.problem.sense = ProblemSense.unknown legacy_results.problem.number_of_objectives = 0 else: obj = get_objective(model) From ff2ddad1f91713071221c9eaf003869b939d379f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 10:58:22 -0700 Subject: [PATCH 0871/3044] Remove import --- pyomo/contrib/solver/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index f9cd213bf73..54871e90c2f 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -22,7 +22,6 @@ from pyomo.common.config import document_kwargs_from_configdict from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning -from pyomo.opt import ProblemSense from pyomo.opt.results.results_ import SolverResults as LegacySolverResults from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize From db4062419b56d810f30c05a96f72cca5eefdfd1a Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Thu, 7 Mar 2024 11:07:40 -0700 Subject: [PATCH 0872/3044] add failing test --- .../solvers/tests/test_persistent_solvers.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index af615d1ed8b..ae189aca701 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -918,6 +918,27 @@ def test_bounds_with_params( res = opt.solve(m) self.assertAlmostEqual(m.y.value, 3) + @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) + def test_bounds_with_immutable_params( + self, name: str, opt_class: Type[PersistentSolver], only_child_vars + ): + # this test is for issue #2574 + opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) + if not opt.available(): + raise unittest.SkipTest + m = pe.ConcreteModel() + m.p = pe.Param(mutable=False, initialize=1) + m.q = pe.Param([1, 2], mutable=False, initialize=10) + m.y = pe.Var() + m.y.setlb(m.p) + m.y.setub(m.q[1]) + m.obj = pe.Objective(expr=m.y) + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 1) + m.y.setlb(m.q[2]) + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 10) + @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_solution_loader( self, name: str, opt_class: Type[PersistentSolver], only_child_vars From b09b3077c10be1431452c877e86593476f267a1b Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Thu, 7 Mar 2024 11:10:05 -0700 Subject: [PATCH 0873/3044] apply patch --- pyomo/contrib/appsi/cmodel/src/expression.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/cmodel/src/expression.cpp b/pyomo/contrib/appsi/cmodel/src/expression.cpp index 234ef47e86f..8079de42b21 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.cpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.cpp @@ -1548,7 +1548,10 @@ appsi_operator_from_pyomo_expr(py::handle expr, py::handle var_map, break; } case param: { - res = param_map[expr_types.id(expr)].cast>(); + if (expr.attr("parent_component")().attr("mutable").cast()) + res = param_map[expr_types.id(expr)].cast>(); + else + res = std::make_shared(expr.attr("value").cast()); break; } case product: { From 9bbb8871d7407574bf22d25e52f4553d3f9da53e Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 11:23:25 -0700 Subject: [PATCH 0874/3044] Standardize subprocess_timeout import to 2; move to a central location --- pyomo/contrib/appsi/solvers/ipopt.py | 3 ++- pyomo/contrib/solver/ipopt.py | 6 +++--- pyomo/opt/base/__init__.py | 2 ++ pyomo/solvers/plugins/solvers/CONOPT.py | 4 ++-- pyomo/solvers/plugins/solvers/CPLEX.py | 10 ++++++++-- pyomo/solvers/plugins/solvers/GLPK.py | 3 ++- pyomo/solvers/plugins/solvers/IPOPT.py | 4 ++-- pyomo/solvers/plugins/solvers/SCIPAMPL.py | 4 ++-- 8 files changed, 23 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 29e74f81c98..82f851ce02c 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -42,6 +42,7 @@ import os from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager +from pyomo.opt.base import subprocess_timeout logger = logging.getLogger(__name__) @@ -158,7 +159,7 @@ def available(self): def version(self): results = subprocess.run( [str(self.config.executable), '--version'], - timeout=1, + timeout=subprocess_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index dc632adb184..8c5e13a534e 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import logging import os import subprocess import datetime @@ -38,8 +39,7 @@ from pyomo.core.expr.numvalue import value from pyomo.core.base.suffix import Suffix from pyomo.common.collections import ComponentMap - -import logging +from pyomo.opt.base import subprocess_timeout logger = logging.getLogger(__name__) @@ -229,7 +229,7 @@ def version(self, config=None): else: results = subprocess.run( [str(pth), '--version'], - timeout=1, + timeout=subprocess_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/opt/base/__init__.py b/pyomo/opt/base/__init__.py index 8d11114dd09..c625c09d1c0 100644 --- a/pyomo/opt/base/__init__.py +++ b/pyomo/opt/base/__init__.py @@ -22,3 +22,5 @@ from pyomo.opt.base.results import ReaderFactory, AbstractResultsReader from pyomo.opt.base.problem import AbstractProblemWriter, BranchDirection, WriterFactory from pyomo.opt.base.formats import ProblemFormat, ResultsFormat, guess_format + +subprocess_timeout = 2 diff --git a/pyomo/solvers/plugins/solvers/CONOPT.py b/pyomo/solvers/plugins/solvers/CONOPT.py index 89ee3848805..bde68d32c55 100644 --- a/pyomo/solvers/plugins/solvers/CONOPT.py +++ b/pyomo/solvers/plugins/solvers/CONOPT.py @@ -16,7 +16,7 @@ from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager -from pyomo.opt.base import ProblemFormat, ResultsFormat +from pyomo.opt.base import ProblemFormat, ResultsFormat, subprocess_timeout from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import SolverStatus from pyomo.opt.solver import SystemCallSolver @@ -79,7 +79,7 @@ def _get_version(self): return _extract_version('') results = subprocess.run( [solver_exec], - timeout=1, + timeout=subprocess_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/solvers/plugins/solvers/CPLEX.py b/pyomo/solvers/plugins/solvers/CPLEX.py index b2b8c5e988d..f7a4774b073 100644 --- a/pyomo/solvers/plugins/solvers/CPLEX.py +++ b/pyomo/solvers/plugins/solvers/CPLEX.py @@ -21,7 +21,13 @@ from pyomo.common.tempfiles import TempfileManager from pyomo.common.collections import ComponentMap, Bunch -from pyomo.opt.base import ProblemFormat, ResultsFormat, OptSolver, BranchDirection +from pyomo.opt.base import ( + ProblemFormat, + ResultsFormat, + OptSolver, + BranchDirection, + subprocess_timeout, +) from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import ( SolverResults, @@ -404,7 +410,7 @@ def _get_version(self): return _extract_version('') results = subprocess.run( [solver_exec, '-c', 'quit'], - timeout=1, + timeout=subprocess_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/solvers/plugins/solvers/GLPK.py b/pyomo/solvers/plugins/solvers/GLPK.py index 39948d465f4..2e09aae1668 100644 --- a/pyomo/solvers/plugins/solvers/GLPK.py +++ b/pyomo/solvers/plugins/solvers/GLPK.py @@ -29,6 +29,7 @@ SolutionStatus, ProblemSense, ) +from pyomo.opt.base import subprocess_timeout from pyomo.opt.base.solvers import _extract_version from pyomo.opt.solver import SystemCallSolver from pyomo.solvers.mockmip import MockMIP @@ -137,7 +138,7 @@ def _get_version(self, executable=None): [executable, "--version"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - timeout=1, + timeout=subprocess_timeout, universal_newlines=True, ) return _extract_version(result.stdout) diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index deda4314a52..84017a7596e 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -16,7 +16,7 @@ from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager -from pyomo.opt.base import ProblemFormat, ResultsFormat +from pyomo.opt.base import ProblemFormat, ResultsFormat, subprocess_timeout from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import SolverStatus, SolverResults, TerminationCondition from pyomo.opt.solver import SystemCallSolver @@ -79,7 +79,7 @@ def _get_version(self): return _extract_version('') results = subprocess.run( [solver_exec, "-v"], - timeout=1, + timeout=subprocess_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/solvers/plugins/solvers/SCIPAMPL.py b/pyomo/solvers/plugins/solvers/SCIPAMPL.py index be7415a19ef..50191d82e5e 100644 --- a/pyomo/solvers/plugins/solvers/SCIPAMPL.py +++ b/pyomo/solvers/plugins/solvers/SCIPAMPL.py @@ -18,7 +18,7 @@ from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager -from pyomo.opt.base import ProblemFormat, ResultsFormat +from pyomo.opt.base import ProblemFormat, ResultsFormat, subprocess_timeout from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import ( SolverStatus, @@ -103,7 +103,7 @@ def _get_version(self, solver_exec=None): return _extract_version('') results = subprocess.run( [solver_exec, "--version"], - timeout=1, + timeout=subprocess_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, From 0b252aecfade0cd0e3327abef24cfb8e061ba164 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 11:23:41 -0700 Subject: [PATCH 0875/3044] add tests with external functions in named expressions --- pyomo/util/tests/test_subsystems.py | 51 +++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/pyomo/util/tests/test_subsystems.py b/pyomo/util/tests/test_subsystems.py index 87a4fb3cf28..f102670ba62 100644 --- a/pyomo/util/tests/test_subsystems.py +++ b/pyomo/util/tests/test_subsystems.py @@ -292,7 +292,7 @@ def test_generate_dont_fix_inputs_with_fixed_var(self): self.assertFalse(m.v3.fixed) self.assertTrue(m.v4.fixed) - def _make_model_with_external_functions(self): + def _make_model_with_external_functions(self, named_expressions=False): m = pyo.ConcreteModel() gsl = find_GSL() m.bessel = pyo.ExternalFunction(library=gsl, function="gsl_sf_bessel_J0") @@ -300,9 +300,18 @@ def _make_model_with_external_functions(self): m.v1 = pyo.Var(initialize=1.0) m.v2 = pyo.Var(initialize=2.0) m.v3 = pyo.Var(initialize=3.0) + if named_expressions: + m.subexpr = pyo.Expression(pyo.PositiveIntegers) + subexpr1 = m.subexpr[1] = 2 * m.fermi(m.v1) + subexpr2 = m.subexpr[2] = m.bessel(m.v1) - m.bessel(m.v2) + subexpr3 = m.subexpr[3] = m.subexpr[2] + m.v3 ** 2 + else: + subexpr1 = 2 * m.fermi(m.v1) + subexpr2 = m.bessel(m.v1) - m.bessel(m.v2) + subexpr3 = m.subexpr[2] + m.v3 ** 2 m.con1 = pyo.Constraint(expr=m.v1 == 0.5) - m.con2 = pyo.Constraint(expr=2 * m.fermi(m.v1) + m.v2**2 - m.v3 == 1.0) - m.con3 = pyo.Constraint(expr=m.bessel(m.v1) - m.bessel(m.v2) + m.v3**2 == 2.0) + m.con2 = pyo.Constraint(expr=subexpr1 + m.v2**2 - m.v3 == 1.0) + m.con3 = pyo.Constraint(expr=subexpr3 == 2.0) return m @unittest.skipUnless(find_GSL(), "Could not find the AMPL GSL library") @@ -329,6 +338,15 @@ def test_identify_external_functions(self): pred_fcn_data = {(gsl, "gsl_sf_bessel_J0"), (gsl, "gsl_sf_fermi_dirac_m1")} self.assertEqual(fcn_data, pred_fcn_data) + @unittest.skipUnless(find_GSL(), "Could not find the AMPL GSL library") + def test_local_external_functions_with_named_expressions(self): + m = self._make_model_with_external_functions(named_expressions=True) + variables = list(pyo.component_data_objects(pyo.Var)) + constraints = list(pyo.component_data_objects(pyo.Constraint, active=True)) + b = create_subsystem_block(constraints, variables) + self.assertTrue(isinstance(m._gsl_sf_bessel_J0, pyo.ExternalFunction)) + self.assertTrue(isinstance(m._gsl_sf_fermi_dirac_m1, pyo.ExternalFunction)) + def _solve_ef_model_with_ipopt(self): m = self._make_model_with_external_functions() ipopt = pyo.SolverFactory("ipopt") @@ -362,6 +380,33 @@ def test_with_external_function(self): self.assertAlmostEqual(m.v2.value, m_full.v2.value) self.assertAlmostEqual(m.v3.value, m_full.v3.value) + @unittest.skipUnless(find_GSL(), "Could not find the AMPL GSL library") + @unittest.skipUnless( + pyo.SolverFactory("ipopt").available(), "ipopt is not available" + ) + def test_with_external_function_in_named_expression(self): + m = self._make_model_with_external_functions(named_expressions=True) + 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) + @unittest.skipUnless(find_GSL(), "Could not find the AMPL GSL library") def test_external_function_with_potential_name_collision(self): m = self._make_model_with_external_functions() From 912867918bdda9ac48642a0e24a51cc45489bcff Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 11:30:22 -0700 Subject: [PATCH 0876/3044] document igraph option in generate_scc --- pyomo/contrib/incidence_analysis/scc_solver.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 117554c52de..6c556646a8c 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -47,9 +47,12 @@ def 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 + include_fixed: Bool, optional Indicates whether fixed variables will be included when identifying variables in constraints. + igraph: IncidenceGraphInterface, optional + Incidence graph containing (at least) the provided constraints + and variables. Yields ------ @@ -67,7 +70,7 @@ def generate_strongly_connected_components( _generate_variables_in_constraints( constraints, include_fixed=include_fixed, - #method=IncidenceMethod.ampl_repn + method=IncidenceMethod.ampl_repn, ) ) timer.stop("generate-vars") From 78431b71f895aa87c875d811b5e05cd933ba4f8a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 12:01:07 -0700 Subject: [PATCH 0877/3044] Change implementation: make private-esque attribute that user can alter --- pyomo/contrib/appsi/solvers/ipopt.py | 4 ++-- pyomo/contrib/solver/ipopt.py | 4 ++-- pyomo/opt/base/__init__.py | 2 -- pyomo/opt/solver/shellcmd.py | 1 + pyomo/solvers/plugins/solvers/CONOPT.py | 4 ++-- pyomo/solvers/plugins/solvers/CPLEX.py | 10 ++-------- pyomo/solvers/plugins/solvers/GLPK.py | 4 ++-- pyomo/solvers/plugins/solvers/IPOPT.py | 4 ++-- pyomo/solvers/plugins/solvers/SCIPAMPL.py | 4 ++-- 9 files changed, 15 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 82f851ce02c..54e21d333e5 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -42,7 +42,6 @@ import os from pyomo.contrib.appsi.cmodel import cmodel_available from pyomo.core.staleflag import StaleFlagManager -from pyomo.opt.base import subprocess_timeout logger = logging.getLogger(__name__) @@ -148,6 +147,7 @@ def __init__(self, only_child_vars=False): self._primal_sol = ComponentMap() self._reduced_costs = ComponentMap() self._last_results_object: Optional[Results] = None + self._version_timeout = 2 def available(self): if self.config.executable.path() is None: @@ -159,7 +159,7 @@ def available(self): def version(self): results = subprocess.run( [str(self.config.executable), '--version'], - timeout=subprocess_timeout, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 8c5e13a534e..edc5799ae20 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -39,7 +39,6 @@ from pyomo.core.expr.numvalue import value from pyomo.core.base.suffix import Suffix from pyomo.common.collections import ComponentMap -from pyomo.opt.base import subprocess_timeout logger = logging.getLogger(__name__) @@ -207,6 +206,7 @@ def __init__(self, **kwds): self._writer = NLWriter() self._available_cache = None self._version_cache = None + self._version_timeout = 2 def available(self, config=None): if config is None: @@ -229,7 +229,7 @@ def version(self, config=None): else: results = subprocess.run( [str(pth), '--version'], - timeout=subprocess_timeout, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/opt/base/__init__.py b/pyomo/opt/base/__init__.py index c625c09d1c0..8d11114dd09 100644 --- a/pyomo/opt/base/__init__.py +++ b/pyomo/opt/base/__init__.py @@ -22,5 +22,3 @@ from pyomo.opt.base.results import ReaderFactory, AbstractResultsReader from pyomo.opt.base.problem import AbstractProblemWriter, BranchDirection, WriterFactory from pyomo.opt.base.formats import ProblemFormat, ResultsFormat, guess_format - -subprocess_timeout = 2 diff --git a/pyomo/opt/solver/shellcmd.py b/pyomo/opt/solver/shellcmd.py index 94117779237..baa0369e1d6 100644 --- a/pyomo/opt/solver/shellcmd.py +++ b/pyomo/opt/solver/shellcmd.py @@ -60,6 +60,7 @@ def __init__(self, **kwargs): # a solver plugin may not report execution time. self._last_solve_time = None self._define_signal_handlers = None + self._version_timeout = 2 if executable is not None: self.set_executable(name=executable, validate=validate) diff --git a/pyomo/solvers/plugins/solvers/CONOPT.py b/pyomo/solvers/plugins/solvers/CONOPT.py index bde68d32c55..3455eede67b 100644 --- a/pyomo/solvers/plugins/solvers/CONOPT.py +++ b/pyomo/solvers/plugins/solvers/CONOPT.py @@ -16,7 +16,7 @@ from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager -from pyomo.opt.base import ProblemFormat, ResultsFormat, subprocess_timeout +from pyomo.opt.base import ProblemFormat, ResultsFormat from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import SolverStatus from pyomo.opt.solver import SystemCallSolver @@ -79,7 +79,7 @@ def _get_version(self): return _extract_version('') results = subprocess.run( [solver_exec], - timeout=subprocess_timeout, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/solvers/plugins/solvers/CPLEX.py b/pyomo/solvers/plugins/solvers/CPLEX.py index f7a4774b073..9f876b2d0f8 100644 --- a/pyomo/solvers/plugins/solvers/CPLEX.py +++ b/pyomo/solvers/plugins/solvers/CPLEX.py @@ -21,13 +21,7 @@ from pyomo.common.tempfiles import TempfileManager from pyomo.common.collections import ComponentMap, Bunch -from pyomo.opt.base import ( - ProblemFormat, - ResultsFormat, - OptSolver, - BranchDirection, - subprocess_timeout, -) +from pyomo.opt.base import ProblemFormat, ResultsFormat, OptSolver, BranchDirection from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import ( SolverResults, @@ -410,7 +404,7 @@ def _get_version(self): return _extract_version('') results = subprocess.run( [solver_exec, '-c', 'quit'], - timeout=subprocess_timeout, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/solvers/plugins/solvers/GLPK.py b/pyomo/solvers/plugins/solvers/GLPK.py index 2e09aae1668..e6d8576489d 100644 --- a/pyomo/solvers/plugins/solvers/GLPK.py +++ b/pyomo/solvers/plugins/solvers/GLPK.py @@ -19,6 +19,7 @@ from pyomo.common import Executable from pyomo.common.collections import Bunch +from pyomo.common.errors import ApplicationError from pyomo.opt import ( SolverFactory, OptSolver, @@ -29,7 +30,6 @@ SolutionStatus, ProblemSense, ) -from pyomo.opt.base import subprocess_timeout from pyomo.opt.base.solvers import _extract_version from pyomo.opt.solver import SystemCallSolver from pyomo.solvers.mockmip import MockMIP @@ -138,7 +138,7 @@ def _get_version(self, executable=None): [executable, "--version"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - timeout=subprocess_timeout, + timeout=self._version_timeout, universal_newlines=True, ) return _extract_version(result.stdout) diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index 84017a7596e..4ebbbc07d3b 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -16,7 +16,7 @@ from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager -from pyomo.opt.base import ProblemFormat, ResultsFormat, subprocess_timeout +from pyomo.opt.base import ProblemFormat, ResultsFormat from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import SolverStatus, SolverResults, TerminationCondition from pyomo.opt.solver import SystemCallSolver @@ -79,7 +79,7 @@ def _get_version(self): return _extract_version('') results = subprocess.run( [solver_exec, "-v"], - timeout=subprocess_timeout, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, diff --git a/pyomo/solvers/plugins/solvers/SCIPAMPL.py b/pyomo/solvers/plugins/solvers/SCIPAMPL.py index 50191d82e5e..fd69954b428 100644 --- a/pyomo/solvers/plugins/solvers/SCIPAMPL.py +++ b/pyomo/solvers/plugins/solvers/SCIPAMPL.py @@ -18,7 +18,7 @@ from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager -from pyomo.opt.base import ProblemFormat, ResultsFormat, subprocess_timeout +from pyomo.opt.base import ProblemFormat, ResultsFormat from pyomo.opt.base.solvers import _extract_version, SolverFactory from pyomo.opt.results import ( SolverStatus, @@ -103,7 +103,7 @@ def _get_version(self, solver_exec=None): return _extract_version('') results = subprocess.run( [solver_exec, "--version"], - timeout=subprocess_timeout, + timeout=self._version_timeout, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, From 313a31019b6de36c20cb0bac66ff0340c5fa6e36 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 12:13:05 -0700 Subject: [PATCH 0878/3044] initial implementation of variable visitor that can exploit named expressions --- pyomo/core/expr/visitor.py | 79 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 6a9b7955281..51864044396 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1388,6 +1388,85 @@ def visit(self, node): return node +class _StreamVariableVisitor(StreamBasedExpressionVisitor): + def __init__( + self, + include_fixed=False, + descend_into_named_expressions=True, + ): + self._include_fixed = include_fixed + self._descend_into_named_expressions = descend_into_named_expressions + self.named_expressions = [] + # Should we allow re-use of this visitor for multiple expressions? + + def initializeWalker(self, expr): + self._variables = [] + self._seen = set() + return True, None + + def beforeChild(self, parent, child, index): + if ( + not self._descend_into_named_expressions + and isinstance(child, NumericValue) + and child.is_named_expression_type() + ): + self.named_expressions.append(child) + return False, None + else: + return True, None + + def exitNode(self, node, data): + if node.is_variable_type() and (self._include_fixed or not node.fixed): + if id(node) not in self._seen: + self._seen.add(id(node)) + self._variables.append(node) + + def finalizeResult(self, result): + return self._variables + + def enterNode(self, node): + pass + + def acceptChildResult(self, node, data, child_result, child_idx): + if child_result.__class__ in native_types: + return False, None + return child_result.is_expression_type(), None + + +def identify_variables_in_components(components, include_fixed=True): + visitor = _StreamVariableVisitor( + include_fixed=include_fixed, descend_into_named_expressions=False + ) + all_variables = [] + for comp in components: + all_variables.extend(visitor.walk_expressions(comp.expr)) + + named_expr_set = set() + unique_named_exprs = [] + for expr in visitor.named_expressions: + if id(expr) in named_expr_set: + named_expr_set.add(id(expr)) + unique_named_exprs.append(expr) + + while unique_named_exprs: + expr = unique_named_exprs.pop() + visitor.named_expressions.clear() + all_variables.extend(visitor.walk_expression(expr.expr)) + + for new_expr in visitor.named_expressions: + if id(new_expr) not in named_expr_set: + named_expr_set.add(new_expr) + unique_named_exprs.append(new_expr) + + unique_vars = [] + var_set = set() + for var in all_variables: + if id(var) not in var_set: + var_set.add(id(var)) + unique_vars.append(var) + return unique_vars + + def identify_variables(expr, include_fixed=True): """ A generator that yields a sequence of variables From 07f5234575aeaf6905827fd7d89842e80200cdbd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 12:58:56 -0700 Subject: [PATCH 0879/3044] Update tests to track change in LinearExpression arg types --- pyomo/core/tests/transform/test_add_slacks.py | 56 +-- pyomo/core/tests/unit/test_compare.py | 6 - pyomo/core/tests/unit/test_expression.py | 11 +- pyomo/core/tests/unit/test_numeric_expr.py | 329 ++++-------------- .../core/tests/unit/test_numeric_expr_api.py | 11 +- .../unit/test_numeric_expr_dispatcher.py | 278 ++++++--------- .../unit/test_numeric_expr_zerofilter.py | 274 ++++++--------- pyomo/core/tests/unit/test_visitor.py | 23 +- pyomo/gdp/tests/common_tests.py | 7 +- pyomo/gdp/tests/test_bigm.py | 5 +- pyomo/gdp/tests/test_binary_multiplication.py | 5 +- pyomo/gdp/tests/test_disjunct.py | 24 +- 12 files changed, 302 insertions(+), 727 deletions(-) diff --git a/pyomo/core/tests/transform/test_add_slacks.py b/pyomo/core/tests/transform/test_add_slacks.py index 7896cab7e88..a74a9b75c4f 100644 --- a/pyomo/core/tests/transform/test_add_slacks.py +++ b/pyomo/core/tests/transform/test_add_slacks.py @@ -102,10 +102,7 @@ def checkRule1(self, m): self, cons.body, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.x)), - EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule1)), - ] + [m.x, EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule1))] ), ) @@ -118,14 +115,7 @@ def checkRule3(self, m): self.assertEqual(cons.lower, 0.1) assertExpressionsEqual( - self, - cons.body, - EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.x)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule3)), - ] - ), + self, cons.body, EXPR.LinearExpression([m.x, transBlock._slack_plus_rule3]) ) def test_ub_constraint_modified(self): @@ -154,8 +144,8 @@ def test_both_bounds_constraint_modified(self): cons.body, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression((1, m.y)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule2)), + m.y, + transBlock._slack_plus_rule2, EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule2)), ] ), @@ -184,10 +174,10 @@ def test_new_obj_created(self): obj.expr, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression((1, transBlock._slack_minus_rule1)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule2)), - EXPR.MonomialTermExpression((1, transBlock._slack_minus_rule2)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule3)), + transBlock._slack_minus_rule1, + transBlock._slack_plus_rule2, + transBlock._slack_minus_rule2, + transBlock._slack_plus_rule3, ] ), ) @@ -302,10 +292,7 @@ def checkTargetsObj(self, m): self, obj.expr, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, transBlock._slack_minus_rule1)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule3)), - ] + [transBlock._slack_minus_rule1, transBlock._slack_plus_rule3] ), ) @@ -423,9 +410,9 @@ def test_transformed_constraints_sumexpression_body(self): c.body, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression((1, m.x)), + m.x, EXPR.MonomialTermExpression((-2, m.y)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule4)), + transBlock._slack_plus_rule4, EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule4)), ] ), @@ -518,15 +505,9 @@ def checkTargetObj(self, m): obj.expr, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression( - (1, transBlock.component("_slack_plus_rule1[1]")) - ), - EXPR.MonomialTermExpression( - (1, transBlock.component("_slack_plus_rule1[2]")) - ), - EXPR.MonomialTermExpression( - (1, transBlock.component("_slack_plus_rule1[3]")) - ), + transBlock.component("_slack_plus_rule1[1]"), + transBlock.component("_slack_plus_rule1[2]"), + transBlock.component("_slack_plus_rule1[3]"), ] ), ) @@ -558,14 +539,7 @@ def checkTransformedRule1(self, m, i): EXPR.LinearExpression( [ EXPR.MonomialTermExpression((2, m.x[i])), - EXPR.MonomialTermExpression( - ( - 1, - m._core_add_slack_variables.component( - "_slack_plus_rule1[%s]" % i - ), - ) - ), + m._core_add_slack_variables.component("_slack_plus_rule1[%s]" % i), ] ), ) diff --git a/pyomo/core/tests/unit/test_compare.py b/pyomo/core/tests/unit/test_compare.py index f80753bdb61..7c3536bc084 100644 --- a/pyomo/core/tests/unit/test_compare.py +++ b/pyomo/core/tests/unit/test_compare.py @@ -165,17 +165,11 @@ def test_expr_if(self): 0, (EqualityExpression, 2), (LinearExpression, 2), - (MonomialTermExpression, 2), - 1, m.y, - (MonomialTermExpression, 2), - 1, m.x, 0, (EqualityExpression, 2), (LinearExpression, 2), - (MonomialTermExpression, 2), - 1, m.y, (MonomialTermExpression, 2), -1, diff --git a/pyomo/core/tests/unit/test_expression.py b/pyomo/core/tests/unit/test_expression.py index c9afc6a1f76..678df4c01a8 100644 --- a/pyomo/core/tests/unit/test_expression.py +++ b/pyomo/core/tests/unit/test_expression.py @@ -738,10 +738,10 @@ def test_pprint_oldStyle(self): expr = model.e * model.x**2 + model.E[1] output = """\ -sum(prod(e{sum(mon(1, x), 2)}, pow(x, 2)), E[1]{sum(pow(x, 2), 1)}) +sum(prod(e{sum(x, 2)}, pow(x, 2)), E[1]{sum(pow(x, 2), 1)}) e : Size=1, Index=None Key : Expression - None : sum(mon(1, x), 2) + None : sum(x, 2) E : Size=2, Index={1, 2} Key : Expression 1 : sum(pow(x, 2), 1) @@ -951,12 +951,7 @@ def test_isub(self): assertExpressionsEqual( self, m.e.expr, - EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.x)), - EXPR.MonomialTermExpression((-1, m.y)), - ] - ), + EXPR.LinearExpression([m.x, EXPR.MonomialTermExpression((-1, m.y))]), ) self.assertTrue(compare_expressions(m.e.expr, m.x - m.y)) diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index c1066c292d7..968b3acb6a4 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -638,12 +638,7 @@ def test_simpleSum(self): m.b = Var() e = m.a + m.b # - self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b))] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b])) self.assertRaises(KeyError, e.arg, 3) @@ -654,14 +649,7 @@ def test_simpleSum_API(self): e = m.a + m.b e += 2 * m.a self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((2, m.a)), - ] - ), + e, LinearExpression([m.a, m.b, MonomialTermExpression((2, m.a))]) ) def test_constSum(self): @@ -669,13 +657,9 @@ def test_constSum(self): m = AbstractModel() m.a = Var() # - self.assertExpressionsEqual( - m.a + 5, LinearExpression([MonomialTermExpression((1, m.a)), 5]) - ) + self.assertExpressionsEqual(m.a + 5, LinearExpression([m.a, 5])) - self.assertExpressionsEqual( - 5 + m.a, LinearExpression([5, MonomialTermExpression((1, m.a))]) - ) + self.assertExpressionsEqual(5 + m.a, LinearExpression([5, m.a])) def test_nestedSum(self): # @@ -696,12 +680,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = e1 + 5 - self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b)), 5] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, 5])) # + # / \ @@ -710,12 +689,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = 5 + e1 - self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b)), 5] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, 5])) # + # / \ @@ -724,16 +698,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = e1 + m.c - self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - ] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, m.c])) # + # / \ @@ -742,16 +707,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = m.c + e1 - self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - ] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, m.c])) # + # / \ @@ -762,17 +718,7 @@ def test_nestedSum(self): e2 = m.c + m.d e = e1 + e2 # - self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - MonomialTermExpression((1, m.d)), - ] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, m.c, m.d])) def test_nestedSum2(self): # @@ -798,22 +744,7 @@ def test_nestedSum2(self): self.assertExpressionsEqual( e, - SumExpression( - [ - ProductExpression( - ( - 2, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - ] - ), - ) - ), - m.c, - ] - ), + SumExpression([ProductExpression((2, LinearExpression([m.a, m.b]))), m.c]), ) # * @@ -834,20 +765,7 @@ def test_nestedSum2(self): ( 3, SumExpression( - [ - ProductExpression( - ( - 2, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - ] - ), - ) - ), - m.c, - ] + [ProductExpression((2, LinearExpression([m.a, m.b]))), m.c] ), ) ), @@ -891,10 +809,7 @@ def test_sumOf_nestedTrivialProduct(self): e = e1 + m.b # self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((5, m.a)), MonomialTermExpression((1, m.b))] - ), + e, LinearExpression([MonomialTermExpression((5, m.a)), m.b]) ) # + @@ -905,10 +820,7 @@ def test_sumOf_nestedTrivialProduct(self): e = m.b + e1 # self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.b)), MonomialTermExpression((5, m.a))] - ), + e, LinearExpression([m.b, MonomialTermExpression((5, m.a))]) ) # + @@ -920,14 +832,7 @@ def test_sumOf_nestedTrivialProduct(self): e = e1 + e2 # self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - MonomialTermExpression((5, m.a)), - ] - ), + e, LinearExpression([m.b, m.c, MonomialTermExpression((5, m.a))]) ) # + @@ -939,14 +844,7 @@ def test_sumOf_nestedTrivialProduct(self): e = e2 + e1 # self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - MonomialTermExpression((5, m.a)), - ] - ), + e, LinearExpression([m.b, m.c, MonomialTermExpression((5, m.a))]) ) def test_simpleDiff(self): @@ -962,10 +860,7 @@ def test_simpleDiff(self): # a b e = m.a - m.b self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((-1, m.b))] - ), + e, LinearExpression([m.a, MonomialTermExpression((-1, m.b))]) ) def test_constDiff(self): @@ -978,9 +873,7 @@ def test_constDiff(self): # - # / \ # a 5 - self.assertExpressionsEqual( - m.a - 5, LinearExpression([MonomialTermExpression((1, m.a)), -5]) - ) + self.assertExpressionsEqual(m.a - 5, LinearExpression([m.a, -5])) # - # / \ @@ -1002,10 +895,7 @@ def test_paramDiff(self): # a p e = m.a - m.p self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), NPV_NegationExpression((m.p,))] - ), + e, LinearExpression([m.a, NPV_NegationExpression((m.p,))]) ) # - @@ -1079,14 +969,7 @@ def test_nestedDiff(self): e1 = m.a - m.b e = e1 - 5 self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - -5, - ] - ), + e, LinearExpression([m.a, MonomialTermExpression((-1, m.b)), -5]) ) # - @@ -1102,14 +985,7 @@ def test_nestedDiff(self): [ 5, NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - ] - ), - ) + (LinearExpression([m.a, MonomialTermExpression((-1, m.b))]),) ), ] ), @@ -1126,7 +1002,7 @@ def test_nestedDiff(self): e, LinearExpression( [ - MonomialTermExpression((1, m.a)), + m.a, MonomialTermExpression((-1, m.b)), MonomialTermExpression((-1, m.c)), ] @@ -1146,14 +1022,7 @@ def test_nestedDiff(self): [ m.c, NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - ] - ), - ) + (LinearExpression([m.a, MonomialTermExpression((-1, m.b))]),) ), ] ), @@ -1171,21 +1040,9 @@ def test_nestedDiff(self): e, SumExpression( [ - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - ] - ), + LinearExpression([m.a, MonomialTermExpression((-1, m.b))]), NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.c)), - MonomialTermExpression((-1, m.d)), - ] - ), - ) + (LinearExpression([m.c, MonomialTermExpression((-1, m.d))]),) ), ] ), @@ -1382,10 +1239,7 @@ def test_sumOf_nestedTrivialProduct2(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((NPV_NegationExpression((m.p,)), m.a)), - ] + [m.b, MonomialTermExpression((NPV_NegationExpression((m.p,)), m.a))] ), ) @@ -1403,14 +1257,7 @@ def test_sumOf_nestedTrivialProduct2(self): [ MonomialTermExpression((m.p, m.a)), NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((-1, m.c)), - ] - ), - ) + (LinearExpression([m.b, MonomialTermExpression((-1, m.c))]),) ), ] ), @@ -1428,7 +1275,7 @@ def test_sumOf_nestedTrivialProduct2(self): e, LinearExpression( [ - MonomialTermExpression((1, m.b)), + m.b, MonomialTermExpression((-1, m.c)), MonomialTermExpression((NPV_NegationExpression((m.p,)), m.a)), ] @@ -1598,22 +1445,7 @@ def test_nestedProduct2(self): self.assertExpressionsEqual( e, ProductExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - ] - ), - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.d)), - ] - ), - ) + (LinearExpression([m.a, m.b, m.c]), LinearExpression([m.a, m.b, m.d])) ), ) # Verify shared args... @@ -1638,9 +1470,7 @@ def test_nestedProduct2(self): e3 = e1 * m.d e = e2 * e3 # - inner = LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b))] - ) + inner = LinearExpression([m.a, m.b]) self.assertExpressionsEqual( e, ProductExpression( @@ -2034,10 +1864,10 @@ def test_sum(self): model.p = Param(mutable=True) expr = 5 + model.a + model.a - self.assertEqual("sum(5, mon(1, a), mon(1, a))", str(expr)) + self.assertEqual("sum(5, a, a)", str(expr)) expr += 5 - self.assertEqual("sum(5, mon(1, a), mon(1, a), 5)", str(expr)) + self.assertEqual("sum(5, a, a, 5)", str(expr)) expr = 2 + model.p self.assertEqual("sum(2, p)", str(expr)) @@ -2053,24 +1883,18 @@ def test_linearsum(self): expr = quicksum(i * model.a[i] for i in A) self.assertEqual( - "sum(mon(0, a[0]), mon(1, a[1]), mon(2, a[2]), mon(3, a[3]), " - "mon(4, a[4]))", + "sum(mon(0, a[0]), a[1], mon(2, a[2]), mon(3, a[3]), " "mon(4, a[4]))", str(expr), ) expr = quicksum((i - 2) * model.a[i] for i in A) self.assertEqual( - "sum(mon(-2, a[0]), mon(-1, a[1]), mon(0, a[2]), mon(1, a[3]), " - "mon(2, a[4]))", + "sum(mon(-2, a[0]), mon(-1, a[1]), mon(0, a[2]), a[3], " "mon(2, a[4]))", str(expr), ) expr = quicksum(model.a[i] for i in A) - self.assertEqual( - "sum(mon(1, a[0]), mon(1, a[1]), mon(1, a[2]), mon(1, a[3]), " - "mon(1, a[4]))", - str(expr), - ) + self.assertEqual("sum(a[0], a[1], a[2], a[3], a[4])", str(expr)) model.p[1].value = 0 model.p[3].value = 3 @@ -2138,10 +1962,10 @@ def test_inequality(self): self.assertEqual("5 <= a < 10", str(expr)) expr = 5 <= model.a + 5 - self.assertEqual("5 <= sum(mon(1, a), 5)", str(expr)) + self.assertEqual("5 <= sum(a, 5)", str(expr)) expr = expr < 10 - self.assertEqual("5 <= sum(mon(1, a), 5) < 10", str(expr)) + self.assertEqual("5 <= sum(a, 5) < 10", str(expr)) def test_equality(self): # @@ -2166,10 +1990,10 @@ def test_equality(self): self.assertEqual("a == 10", str(expr)) expr = 5 == model.a + 5 - self.assertEqual("sum(mon(1, a), 5) == 5", str(expr)) + self.assertEqual("sum(a, 5) == 5", str(expr)) expr = model.a + 5 == 5 - self.assertEqual("sum(mon(1, a), 5) == 5", str(expr)) + self.assertEqual("sum(a, 5) == 5", str(expr)) def test_getitem(self): m = ConcreteModel() @@ -2206,7 +2030,7 @@ def test_small_expression(self): expr = abs(expr) self.assertEqual( "abs(neg(pow(2, div(2, prod(2, sum(1, neg(pow(div(prod(sum(" - "mon(1, a), 1, -1), a), a), b)), 1))))))", + "a, 1, -1), a), a), b)), 1))))))", str(expr), ) @@ -3754,13 +3578,7 @@ def test_summation1(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - ] + [self.m.a[1], self.m.a[2], self.m.a[3], self.m.a[4], self.m.a[5]] ), ) @@ -3872,16 +3690,16 @@ def test_summation_compression(self): e, LinearExpression( [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - MonomialTermExpression((1, self.m.b[1])), - MonomialTermExpression((1, self.m.b[2])), - MonomialTermExpression((1, self.m.b[3])), - MonomialTermExpression((1, self.m.b[4])), - MonomialTermExpression((1, self.m.b[5])), + self.m.a[1], + self.m.a[2], + self.m.a[3], + self.m.a[4], + self.m.a[5], + self.m.b[1], + self.m.b[2], + self.m.b[3], + self.m.b[4], + self.m.b[5], ] ), ) @@ -3912,13 +3730,7 @@ def test_deprecation(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - ] + [self.m.a[1], self.m.a[2], self.m.a[3], self.m.a[4], self.m.a[5]] ), ) @@ -3928,13 +3740,7 @@ def test_summation1(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - ] + [self.m.a[1], self.m.a[2], self.m.a[3], self.m.a[4], self.m.a[5]] ), ) @@ -4156,15 +3962,15 @@ def test_SumExpression(self): self.assertEqual(expr2(), 15) self.assertNotEqual(id(expr1), id(expr2)) self.assertNotEqual(id(expr1._args_), id(expr2._args_)) - self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) - self.assertIs(expr1.arg(1).arg(1), expr2.arg(1).arg(1)) + self.assertIs(expr1.arg(0), expr2.arg(0)) + self.assertIs(expr1.arg(1), expr2.arg(1)) expr1 += self.m.b self.assertEqual(expr1(), 25) self.assertEqual(expr2(), 15) self.assertNotEqual(id(expr1), id(expr2)) self.assertNotEqual(id(expr1._args_), id(expr2._args_)) - self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) - self.assertIs(expr1.arg(1).arg(1), expr2.arg(1).arg(1)) + self.assertIs(expr1.arg(0), expr2.arg(0)) + self.assertIs(expr1.arg(1), expr2.arg(1)) # total = counter.count - start self.assertEqual(total, 1) @@ -4341,9 +4147,9 @@ def test_productOfExpressions(self): self.assertEqual(expr1.arg(1).nargs(), 2) self.assertEqual(expr2.arg(1).nargs(), 2) - self.assertIs(expr1.arg(0).arg(0).arg(1), expr2.arg(0).arg(0).arg(1)) - self.assertIs(expr1.arg(0).arg(1).arg(1), expr2.arg(0).arg(1).arg(1)) - self.assertIs(expr1.arg(1).arg(0).arg(1), expr2.arg(1).arg(0).arg(1)) + self.assertIs(expr1.arg(0).arg(0), expr2.arg(0).arg(0)) + self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) + self.assertIs(expr1.arg(1).arg(0), expr2.arg(1).arg(0)) expr1 *= self.m.b self.assertEqual(expr1(), 1500) @@ -4382,8 +4188,8 @@ def test_productOfExpressions_div(self): self.assertEqual(expr1.arg(1).nargs(), 2) self.assertEqual(expr2.arg(1).nargs(), 2) - self.assertIs(expr1.arg(0).arg(0).arg(1), expr2.arg(0).arg(0).arg(1)) - self.assertIs(expr1.arg(0).arg(1).arg(1), expr2.arg(0).arg(1).arg(1)) + self.assertIs(expr1.arg(0).arg(0), expr2.arg(0).arg(0)) + self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) expr1 /= self.m.b self.assertAlmostEqual(expr1(), 0.15) @@ -5214,18 +5020,7 @@ def test_pow_other(self): e += m.v[0] + m.v[1] e = m.v[0] ** e self.assertExpressionsEqual( - e, - PowExpression( - ( - m.v[0], - LinearExpression( - [ - MonomialTermExpression((1, m.v[0])), - MonomialTermExpression((1, m.v[1])), - ] - ), - ) - ), + e, PowExpression((m.v[0], LinearExpression([m.v[0], m.v[1]]))) ) diff --git a/pyomo/core/tests/unit/test_numeric_expr_api.py b/pyomo/core/tests/unit/test_numeric_expr_api.py index 4e0af126315..923f78af1be 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_api.py +++ b/pyomo/core/tests/unit/test_numeric_expr_api.py @@ -223,7 +223,7 @@ def test_negation(self): self.assertEqual(is_fixed(e), False) self.assertEqual(value(e), -15) self.assertEqual(str(e), "- (x + 2*x)") - self.assertEqual(e.to_string(verbose=True), "neg(sum(mon(1, x), mon(2, x)))") + self.assertEqual(e.to_string(verbose=True), "neg(sum(x, mon(2, x)))") # This can't occur through operator overloading, but could # through expression substitution @@ -634,8 +634,7 @@ def test_linear(self): self.assertEqual(value(e), 1 + 4 + 5 + 2) self.assertEqual(str(e), "0*x[0] + x[1] + 2*x[2] + 5 + y - 3") self.assertEqual( - e.to_string(verbose=True), - "sum(mon(0, x[0]), mon(1, x[1]), mon(2, x[2]), 5, mon(1, y), -3)", + e.to_string(verbose=True), "sum(mon(0, x[0]), x[1], mon(2, x[2]), 5, y, -3)" ) self.assertIs(type(e), LinearExpression) @@ -701,7 +700,7 @@ def test_expr_if(self): ) self.assertEqual( e.to_string(verbose=True), - "Expr_if( ( 5 <= y ), then=( sum(mon(1, x[0]), 5) ), else=( pow(x[1], 2) ) )", + "Expr_if( ( 5 <= y ), then=( sum(x[0], 5) ), else=( pow(x[1], 2) ) )", ) m.y.fix() @@ -972,9 +971,7 @@ def test_sum(self): f = e.create_node_with_local_data((m.p, m.x)) self.assertIsNot(f, e) self.assertIs(type(f), LinearExpression) - assertExpressionsStructurallyEqual( - self, f.args, [m.p, MonomialTermExpression((1, m.x))] - ) + assertExpressionsStructurallyEqual(self, f.args, [m.p, m.x]) f = e.create_node_with_local_data((m.p, m.x**2)) self.assertIsNot(f, e) diff --git a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py index 7c6e2af9974..bb7a291e67d 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py +++ b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py @@ -123,8 +123,6 @@ def setUp(self): self.mutable_l3 = _MutableNPVSumExpression([self.npv]) # often repeated reference expressions - self.mon_bin = MonomialTermExpression((1, self.bin)) - self.mon_var = MonomialTermExpression((1, self.var)) self.minus_bin = MonomialTermExpression((-1, self.bin)) self.minus_npv = NPV_NegationExpression((self.npv,)) self.minus_param_mut = NPV_NegationExpression((self.param_mut,)) @@ -368,38 +366,34 @@ def test_add_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.one, LinearExpression([self.bin, 1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, 5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, 6])), + (self.asbinary, self.native, LinearExpression([self.bin, 5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.npv])), + (self.asbinary, self.param, LinearExpression([self.bin, 6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.param_mut]), + LinearExpression([self.bin, self.param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.mon_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.mon_native]), + LinearExpression([self.bin, self.mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.mon_param]), - ), - ( - self.asbinary, - self.mon_npv, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_param]), ), + (self.asbinary, self.mon_npv, LinearExpression([self.bin, self.mon_npv])), # 12: ( self.asbinary, self.linear, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.asbinary, self.sum, SumExpression(self.sum.args + [self.bin])), (self.asbinary, self.other, SumExpression([self.bin, self.other])), @@ -408,7 +402,7 @@ def test_add_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_npv]), ), ( self.asbinary, @@ -416,13 +410,9 @@ def test_add_asbinary(self): SumExpression(self.mutable_l2.args + [self.bin]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.param1, LinearExpression([self.bin, 1])), # 20: - ( - self.asbinary, - self.mutable_l3, - LinearExpression([self.mon_bin, self.npv]), - ), + (self.asbinary, self.mutable_l3, LinearExpression([self.bin, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -462,7 +452,7 @@ def test_add_zero(self): def test_add_one(self): tests = [ (self.one, self.invalid, NotImplemented), - (self.one, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.one, self.asbinary, LinearExpression([1, self.bin])), (self.one, self.zero, 1), (self.one, self.one, 2), # 4: @@ -471,7 +461,7 @@ def test_add_one(self): (self.one, self.param, 7), (self.one, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.one, self.var, LinearExpression([1, self.mon_var])), + (self.one, self.var, LinearExpression([1, self.var])), (self.one, self.mon_native, LinearExpression([1, self.mon_native])), (self.one, self.mon_param, LinearExpression([1, self.mon_param])), (self.one, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -494,7 +484,7 @@ def test_add_one(self): def test_add_native(self): tests = [ (self.native, self.invalid, NotImplemented), - (self.native, self.asbinary, LinearExpression([5, self.mon_bin])), + (self.native, self.asbinary, LinearExpression([5, self.bin])), (self.native, self.zero, 5), (self.native, self.one, 6), # 4: @@ -503,7 +493,7 @@ def test_add_native(self): (self.native, self.param, 11), (self.native, self.param_mut, NPV_SumExpression([5, self.param_mut])), # 8: - (self.native, self.var, LinearExpression([5, self.mon_var])), + (self.native, self.var, LinearExpression([5, self.var])), (self.native, self.mon_native, LinearExpression([5, self.mon_native])), (self.native, self.mon_param, LinearExpression([5, self.mon_param])), (self.native, self.mon_npv, LinearExpression([5, self.mon_npv])), @@ -530,7 +520,7 @@ def test_add_native(self): def test_add_npv(self): tests = [ (self.npv, self.invalid, NotImplemented), - (self.npv, self.asbinary, LinearExpression([self.npv, self.mon_bin])), + (self.npv, self.asbinary, LinearExpression([self.npv, self.bin])), (self.npv, self.zero, self.npv), (self.npv, self.one, NPV_SumExpression([self.npv, 1])), # 4: @@ -539,7 +529,7 @@ def test_add_npv(self): (self.npv, self.param, NPV_SumExpression([self.npv, 6])), (self.npv, self.param_mut, NPV_SumExpression([self.npv, self.param_mut])), # 8: - (self.npv, self.var, LinearExpression([self.npv, self.mon_var])), + (self.npv, self.var, LinearExpression([self.npv, self.var])), (self.npv, self.mon_native, LinearExpression([self.npv, self.mon_native])), (self.npv, self.mon_param, LinearExpression([self.npv, self.mon_param])), (self.npv, self.mon_npv, LinearExpression([self.npv, self.mon_npv])), @@ -570,7 +560,7 @@ def test_add_npv(self): def test_add_param(self): tests = [ (self.param, self.invalid, NotImplemented), - (self.param, self.asbinary, LinearExpression([6, self.mon_bin])), + (self.param, self.asbinary, LinearExpression([6, self.bin])), (self.param, self.zero, 6), (self.param, self.one, 7), # 4: @@ -579,7 +569,7 @@ def test_add_param(self): (self.param, self.param, 12), (self.param, self.param_mut, NPV_SumExpression([6, self.param_mut])), # 8: - (self.param, self.var, LinearExpression([6, self.mon_var])), + (self.param, self.var, LinearExpression([6, self.var])), (self.param, self.mon_native, LinearExpression([6, self.mon_native])), (self.param, self.mon_param, LinearExpression([6, self.mon_param])), (self.param, self.mon_npv, LinearExpression([6, self.mon_npv])), @@ -605,7 +595,7 @@ def test_add_param_mut(self): ( self.param_mut, self.asbinary, - LinearExpression([self.param_mut, self.mon_bin]), + LinearExpression([self.param_mut, self.bin]), ), (self.param_mut, self.zero, self.param_mut), (self.param_mut, self.one, NPV_SumExpression([self.param_mut, 1])), @@ -619,11 +609,7 @@ def test_add_param_mut(self): NPV_SumExpression([self.param_mut, self.param_mut]), ), # 8: - ( - self.param_mut, - self.var, - LinearExpression([self.param_mut, self.mon_var]), - ), + (self.param_mut, self.var, LinearExpression([self.param_mut, self.var])), ( self.param_mut, self.mon_native, @@ -674,37 +660,21 @@ def test_add_param_mut(self): def test_add_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.mon_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, 1])), + (self.var, self.one, LinearExpression([self.var, 1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, 5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.npv])), - (self.var, self.param, LinearExpression([self.mon_var, 6])), - ( - self.var, - self.param_mut, - LinearExpression([self.mon_var, self.param_mut]), - ), + (self.var, self.native, LinearExpression([self.var, 5])), + (self.var, self.npv, LinearExpression([self.var, self.npv])), + (self.var, self.param, LinearExpression([self.var, 6])), + (self.var, self.param_mut, LinearExpression([self.var, self.param_mut])), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.mon_var])), - ( - self.var, - self.mon_native, - LinearExpression([self.mon_var, self.mon_native]), - ), - ( - self.var, - self.mon_param, - LinearExpression([self.mon_var, self.mon_param]), - ), - (self.var, self.mon_npv, LinearExpression([self.mon_var, self.mon_npv])), + (self.var, self.var, LinearExpression([self.var, self.var])), + (self.var, self.mon_native, LinearExpression([self.var, self.mon_native])), + (self.var, self.mon_param, LinearExpression([self.var, self.mon_param])), + (self.var, self.mon_npv, LinearExpression([self.var, self.mon_npv])), # 12: - ( - self.var, - self.linear, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.var, self.linear, LinearExpression(self.linear.args + [self.var])), (self.var, self.sum, SumExpression(self.sum.args + [self.var])), (self.var, self.other, SumExpression([self.var, self.other])), (self.var, self.mutable_l0, self.var), @@ -712,7 +682,7 @@ def test_add_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var] + self.mutable_l1.args), + LinearExpression([self.var] + self.mutable_l1.args), ), ( self.var, @@ -720,13 +690,9 @@ def test_add_var(self): SumExpression(self.mutable_l2.args + [self.var]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, 1])), + (self.var, self.param1, LinearExpression([self.var, 1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([MonomialTermExpression((1, self.var)), self.npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -737,7 +703,7 @@ def test_add_mon_native(self): ( self.mon_native, self.asbinary, - LinearExpression([self.mon_native, self.mon_bin]), + LinearExpression([self.mon_native, self.bin]), ), (self.mon_native, self.zero, self.mon_native), (self.mon_native, self.one, LinearExpression([self.mon_native, 1])), @@ -751,11 +717,7 @@ def test_add_mon_native(self): LinearExpression([self.mon_native, self.param_mut]), ), # 8: - ( - self.mon_native, - self.var, - LinearExpression([self.mon_native, self.mon_var]), - ), + (self.mon_native, self.var, LinearExpression([self.mon_native, self.var])), ( self.mon_native, self.mon_native, @@ -813,7 +775,7 @@ def test_add_mon_param(self): ( self.mon_param, self.asbinary, - LinearExpression([self.mon_param, self.mon_bin]), + LinearExpression([self.mon_param, self.bin]), ), (self.mon_param, self.zero, self.mon_param), (self.mon_param, self.one, LinearExpression([self.mon_param, 1])), @@ -827,11 +789,7 @@ def test_add_mon_param(self): LinearExpression([self.mon_param, self.param_mut]), ), # 8: - ( - self.mon_param, - self.var, - LinearExpression([self.mon_param, self.mon_var]), - ), + (self.mon_param, self.var, LinearExpression([self.mon_param, self.var])), ( self.mon_param, self.mon_native, @@ -882,11 +840,7 @@ def test_add_mon_param(self): def test_add_mon_npv(self): tests = [ (self.mon_npv, self.invalid, NotImplemented), - ( - self.mon_npv, - self.asbinary, - LinearExpression([self.mon_npv, self.mon_bin]), - ), + (self.mon_npv, self.asbinary, LinearExpression([self.mon_npv, self.bin])), (self.mon_npv, self.zero, self.mon_npv), (self.mon_npv, self.one, LinearExpression([self.mon_npv, 1])), # 4: @@ -899,7 +853,7 @@ def test_add_mon_npv(self): LinearExpression([self.mon_npv, self.param_mut]), ), # 8: - (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.mon_var])), + (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.var])), ( self.mon_npv, self.mon_native, @@ -949,7 +903,7 @@ def test_add_linear(self): ( self.linear, self.asbinary, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.linear, self.zero, self.linear), (self.linear, self.one, LinearExpression(self.linear.args + [1])), @@ -963,11 +917,7 @@ def test_add_linear(self): LinearExpression(self.linear.args + [self.param_mut]), ), # 8: - ( - self.linear, - self.var, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.linear, self.var, LinearExpression(self.linear.args + [self.var])), ( self.linear, self.mon_native, @@ -1134,7 +1084,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.asbinary, - LinearExpression(self.mutable_l1.args + [self.mon_bin]), + LinearExpression(self.mutable_l1.args + [self.bin]), ), (self.mutable_l1, self.zero, self.mon_npv), (self.mutable_l1, self.one, LinearExpression(self.mutable_l1.args + [1])), @@ -1159,7 +1109,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.var, - LinearExpression(self.mutable_l1.args + [self.mon_var]), + LinearExpression(self.mutable_l1.args + [self.var]), ), ( self.mutable_l1, @@ -1341,7 +1291,7 @@ def test_add_param0(self): def test_add_param1(self): tests = [ (self.param1, self.invalid, NotImplemented), - (self.param1, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.param1, self.asbinary, LinearExpression([1, self.bin])), (self.param1, self.zero, 1), (self.param1, self.one, 2), # 4: @@ -1350,7 +1300,7 @@ def test_add_param1(self): (self.param1, self.param, 7), (self.param1, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.param1, self.var, LinearExpression([1, self.mon_var])), + (self.param1, self.var, LinearExpression([1, self.var])), (self.param1, self.mon_native, LinearExpression([1, self.mon_native])), (self.param1, self.mon_param, LinearExpression([1, self.mon_param])), (self.param1, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -1380,7 +1330,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.asbinary, - LinearExpression(self.mutable_l3.args + [self.mon_bin]), + LinearExpression(self.mutable_l3.args + [self.bin]), ), (self.mutable_l3, self.zero, self.npv), (self.mutable_l3, self.one, NPV_SumExpression(self.mutable_l3.args + [1])), @@ -1409,7 +1359,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.var, - LinearExpression(self.mutable_l3.args + [self.mon_var]), + LinearExpression(self.mutable_l3.args + [self.var]), ), ( self.mutable_l3, @@ -1515,32 +1465,32 @@ def test_sub_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.one, LinearExpression([self.bin, -1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, -5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.minus_npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, -6])), + (self.asbinary, self.native, LinearExpression([self.bin, -5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.minus_npv])), + (self.asbinary, self.param, LinearExpression([self.bin, -6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.minus_param_mut]), + LinearExpression([self.bin, self.minus_param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.minus_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.minus_var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.minus_mon_native]), + LinearExpression([self.bin, self.minus_mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.minus_mon_param]), + LinearExpression([self.bin, self.minus_mon_param]), ), ( self.asbinary, self.mon_npv, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), # 12: (self.asbinary, self.linear, SumExpression([self.bin, self.minus_linear])), @@ -1551,7 +1501,7 @@ def test_sub_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), ( self.asbinary, @@ -1559,12 +1509,12 @@ def test_sub_asbinary(self): SumExpression([self.bin, self.minus_mutable_l2]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.param1, LinearExpression([self.bin, -1])), # 20: ( self.asbinary, self.mutable_l3, - LinearExpression([self.mon_bin, self.minus_npv]), + LinearExpression([self.bin, self.minus_npv]), ), ] self._run_cases(tests, operator.sub) @@ -1837,35 +1787,31 @@ def test_sub_param_mut(self): def test_sub_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.minus_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.minus_bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, -1])), + (self.var, self.one, LinearExpression([self.var, -1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, -5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.minus_npv])), - (self.var, self.param, LinearExpression([self.mon_var, -6])), + (self.var, self.native, LinearExpression([self.var, -5])), + (self.var, self.npv, LinearExpression([self.var, self.minus_npv])), + (self.var, self.param, LinearExpression([self.var, -6])), ( self.var, self.param_mut, - LinearExpression([self.mon_var, self.minus_param_mut]), + LinearExpression([self.var, self.minus_param_mut]), ), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.minus_var])), + (self.var, self.var, LinearExpression([self.var, self.minus_var])), ( self.var, self.mon_native, - LinearExpression([self.mon_var, self.minus_mon_native]), + LinearExpression([self.var, self.minus_mon_native]), ), ( self.var, self.mon_param, - LinearExpression([self.mon_var, self.minus_mon_param]), - ), - ( - self.var, - self.mon_npv, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_param]), ), + (self.var, self.mon_npv, LinearExpression([self.var, self.minus_mon_npv])), # 12: ( self.var, @@ -1879,7 +1825,7 @@ def test_sub_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_npv]), ), ( self.var, @@ -1887,13 +1833,9 @@ def test_sub_var(self): SumExpression([self.var, self.minus_mutable_l2]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, -1])), + (self.var, self.param1, LinearExpression([self.var, -1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([self.mon_var, self.minus_npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.minus_npv])), ] self._run_cases(tests, operator.sub) self._run_cases(tests, operator.isub) @@ -6511,7 +6453,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([])), (mutable_npv, self.one, _MutableNPVSumExpression([1])), # 4: @@ -6520,7 +6462,7 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.param, _MutableNPVSumExpression([6])), (mutable_npv, self.param_mut, _MutableNPVSumExpression([self.param_mut])), # 8: - (mutable_npv, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([self.var])), (mutable_npv, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_npv, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_npv, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6546,7 +6488,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([10]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), (mutable_npv, self.one, _MutableNPVSumExpression([11])), # 4: @@ -6559,7 +6501,7 @@ def test_mutable_nvp_iadd(self): _MutableNPVSumExpression([10, self.param_mut]), ), # 8: - (mutable_npv, self.var, _MutableLinearExpression([10, self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([10, self.var])), ( mutable_npv, self.mon_native, @@ -6602,7 +6544,7 @@ def test_mutable_lin_iadd(self): mutable_lin = _MutableLinearExpression([]) tests = [ (mutable_lin, self.invalid, NotImplemented), - (mutable_lin, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_lin, self.zero, _MutableLinearExpression([])), (mutable_lin, self.one, _MutableLinearExpression([1])), # 4: @@ -6611,7 +6553,7 @@ def test_mutable_lin_iadd(self): (mutable_lin, self.param, _MutableLinearExpression([6])), (mutable_lin, self.param_mut, _MutableLinearExpression([self.param_mut])), # 8: - (mutable_lin, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_lin, self.var, _MutableLinearExpression([self.var])), (mutable_lin, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_lin, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_lin, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6634,81 +6576,69 @@ def test_mutable_lin_iadd(self): ] self._run_iadd_cases(tests, operator.iadd) - mutable_lin = _MutableLinearExpression([self.mon_bin]) + mutable_lin = _MutableLinearExpression([self.bin]) tests = [ (mutable_lin, self.invalid, NotImplemented), ( mutable_lin, self.asbinary, - _MutableLinearExpression([self.mon_bin, self.mon_bin]), + _MutableLinearExpression([self.bin, self.bin]), ), - (mutable_lin, self.zero, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.one, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.zero, _MutableLinearExpression([self.bin])), + (mutable_lin, self.one, _MutableLinearExpression([self.bin, 1])), # 4: - (mutable_lin, self.native, _MutableLinearExpression([self.mon_bin, 5])), - (mutable_lin, self.npv, _MutableLinearExpression([self.mon_bin, self.npv])), - (mutable_lin, self.param, _MutableLinearExpression([self.mon_bin, 6])), + (mutable_lin, self.native, _MutableLinearExpression([self.bin, 5])), + (mutable_lin, self.npv, _MutableLinearExpression([self.bin, self.npv])), + (mutable_lin, self.param, _MutableLinearExpression([self.bin, 6])), ( mutable_lin, self.param_mut, - _MutableLinearExpression([self.mon_bin, self.param_mut]), + _MutableLinearExpression([self.bin, self.param_mut]), ), # 8: - ( - mutable_lin, - self.var, - _MutableLinearExpression([self.mon_bin, self.mon_var]), - ), + (mutable_lin, self.var, _MutableLinearExpression([self.bin, self.var])), ( mutable_lin, self.mon_native, - _MutableLinearExpression([self.mon_bin, self.mon_native]), + _MutableLinearExpression([self.bin, self.mon_native]), ), ( mutable_lin, self.mon_param, - _MutableLinearExpression([self.mon_bin, self.mon_param]), + _MutableLinearExpression([self.bin, self.mon_param]), ), ( mutable_lin, self.mon_npv, - _MutableLinearExpression([self.mon_bin, self.mon_npv]), + _MutableLinearExpression([self.bin, self.mon_npv]), ), # 12: ( mutable_lin, self.linear, - _MutableLinearExpression([self.mon_bin] + self.linear.args), - ), - ( - mutable_lin, - self.sum, - _MutableSumExpression([self.mon_bin] + self.sum.args), - ), - ( - mutable_lin, - self.other, - _MutableSumExpression([self.mon_bin, self.other]), + _MutableLinearExpression([self.bin] + self.linear.args), ), - (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.sum, _MutableSumExpression([self.bin] + self.sum.args)), + (mutable_lin, self.other, _MutableSumExpression([self.bin, self.other])), + (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.bin])), # 16: ( mutable_lin, self.mutable_l1, - _MutableLinearExpression([self.mon_bin] + self.mutable_l1.args), + _MutableLinearExpression([self.bin] + self.mutable_l1.args), ), ( mutable_lin, self.mutable_l2, - _MutableSumExpression([self.mon_bin] + self.mutable_l2.args), + _MutableSumExpression([self.bin] + self.mutable_l2.args), ), - (mutable_lin, self.param0, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.param1, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.param0, _MutableLinearExpression([self.bin])), + (mutable_lin, self.param1, _MutableLinearExpression([self.bin, 1])), # 20: ( mutable_lin, self.mutable_l3, - _MutableLinearExpression([self.mon_bin, self.npv]), + _MutableLinearExpression([self.bin, self.npv]), ), ] self._run_iadd_cases(tests, operator.iadd) @@ -6854,7 +6784,7 @@ def as_numeric(self): assertExpressionsEqual(self, PowExpression((self.var, 2)), e) e = obj + obj - assertExpressionsEqual(self, LinearExpression((self.mon_var, self.mon_var)), e) + assertExpressionsEqual(self, LinearExpression((self.var, self.var)), e) def test_categorize_arg_type(self): class CustomAsNumeric(NumericValue): diff --git a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py index 34d2e1cc2c2..19968640a21 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py +++ b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py @@ -102,38 +102,34 @@ def test_add_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.one, LinearExpression([self.bin, 1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, 5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, 6])), + (self.asbinary, self.native, LinearExpression([self.bin, 5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.npv])), + (self.asbinary, self.param, LinearExpression([self.bin, 6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.param_mut]), + LinearExpression([self.bin, self.param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.mon_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.mon_native]), + LinearExpression([self.bin, self.mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.mon_param]), - ), - ( - self.asbinary, - self.mon_npv, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_param]), ), + (self.asbinary, self.mon_npv, LinearExpression([self.bin, self.mon_npv])), # 12: ( self.asbinary, self.linear, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.asbinary, self.sum, SumExpression(self.sum.args + [self.bin])), (self.asbinary, self.other, SumExpression([self.bin, self.other])), @@ -142,7 +138,7 @@ def test_add_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_npv]), ), ( self.asbinary, @@ -150,13 +146,9 @@ def test_add_asbinary(self): SumExpression(self.mutable_l2.args + [self.bin]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.param1, LinearExpression([self.bin, 1])), # 20: - ( - self.asbinary, - self.mutable_l3, - LinearExpression([self.mon_bin, self.npv]), - ), + (self.asbinary, self.mutable_l3, LinearExpression([self.bin, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -196,7 +188,7 @@ def test_add_zero(self): def test_add_one(self): tests = [ (self.one, self.invalid, NotImplemented), - (self.one, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.one, self.asbinary, LinearExpression([1, self.bin])), (self.one, self.zero, 1), (self.one, self.one, 2), # 4: @@ -205,7 +197,7 @@ def test_add_one(self): (self.one, self.param, 7), (self.one, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.one, self.var, LinearExpression([1, self.mon_var])), + (self.one, self.var, LinearExpression([1, self.var])), (self.one, self.mon_native, LinearExpression([1, self.mon_native])), (self.one, self.mon_param, LinearExpression([1, self.mon_param])), (self.one, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -228,7 +220,7 @@ def test_add_one(self): def test_add_native(self): tests = [ (self.native, self.invalid, NotImplemented), - (self.native, self.asbinary, LinearExpression([5, self.mon_bin])), + (self.native, self.asbinary, LinearExpression([5, self.bin])), (self.native, self.zero, 5), (self.native, self.one, 6), # 4: @@ -237,7 +229,7 @@ def test_add_native(self): (self.native, self.param, 11), (self.native, self.param_mut, NPV_SumExpression([5, self.param_mut])), # 8: - (self.native, self.var, LinearExpression([5, self.mon_var])), + (self.native, self.var, LinearExpression([5, self.var])), (self.native, self.mon_native, LinearExpression([5, self.mon_native])), (self.native, self.mon_param, LinearExpression([5, self.mon_param])), (self.native, self.mon_npv, LinearExpression([5, self.mon_npv])), @@ -264,7 +256,7 @@ def test_add_native(self): def test_add_npv(self): tests = [ (self.npv, self.invalid, NotImplemented), - (self.npv, self.asbinary, LinearExpression([self.npv, self.mon_bin])), + (self.npv, self.asbinary, LinearExpression([self.npv, self.bin])), (self.npv, self.zero, self.npv), (self.npv, self.one, NPV_SumExpression([self.npv, 1])), # 4: @@ -273,7 +265,7 @@ def test_add_npv(self): (self.npv, self.param, NPV_SumExpression([self.npv, 6])), (self.npv, self.param_mut, NPV_SumExpression([self.npv, self.param_mut])), # 8: - (self.npv, self.var, LinearExpression([self.npv, self.mon_var])), + (self.npv, self.var, LinearExpression([self.npv, self.var])), (self.npv, self.mon_native, LinearExpression([self.npv, self.mon_native])), (self.npv, self.mon_param, LinearExpression([self.npv, self.mon_param])), (self.npv, self.mon_npv, LinearExpression([self.npv, self.mon_npv])), @@ -304,7 +296,7 @@ def test_add_npv(self): def test_add_param(self): tests = [ (self.param, self.invalid, NotImplemented), - (self.param, self.asbinary, LinearExpression([6, self.mon_bin])), + (self.param, self.asbinary, LinearExpression([6, self.bin])), (self.param, self.zero, 6), (self.param, self.one, 7), # 4: @@ -313,7 +305,7 @@ def test_add_param(self): (self.param, self.param, 12), (self.param, self.param_mut, NPV_SumExpression([6, self.param_mut])), # 8: - (self.param, self.var, LinearExpression([6, self.mon_var])), + (self.param, self.var, LinearExpression([6, self.var])), (self.param, self.mon_native, LinearExpression([6, self.mon_native])), (self.param, self.mon_param, LinearExpression([6, self.mon_param])), (self.param, self.mon_npv, LinearExpression([6, self.mon_npv])), @@ -339,7 +331,7 @@ def test_add_param_mut(self): ( self.param_mut, self.asbinary, - LinearExpression([self.param_mut, self.mon_bin]), + LinearExpression([self.param_mut, self.bin]), ), (self.param_mut, self.zero, self.param_mut), (self.param_mut, self.one, NPV_SumExpression([self.param_mut, 1])), @@ -353,11 +345,7 @@ def test_add_param_mut(self): NPV_SumExpression([self.param_mut, self.param_mut]), ), # 8: - ( - self.param_mut, - self.var, - LinearExpression([self.param_mut, self.mon_var]), - ), + (self.param_mut, self.var, LinearExpression([self.param_mut, self.var])), ( self.param_mut, self.mon_native, @@ -408,37 +396,21 @@ def test_add_param_mut(self): def test_add_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.mon_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, 1])), + (self.var, self.one, LinearExpression([self.var, 1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, 5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.npv])), - (self.var, self.param, LinearExpression([self.mon_var, 6])), - ( - self.var, - self.param_mut, - LinearExpression([self.mon_var, self.param_mut]), - ), + (self.var, self.native, LinearExpression([self.var, 5])), + (self.var, self.npv, LinearExpression([self.var, self.npv])), + (self.var, self.param, LinearExpression([self.var, 6])), + (self.var, self.param_mut, LinearExpression([self.var, self.param_mut])), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.mon_var])), - ( - self.var, - self.mon_native, - LinearExpression([self.mon_var, self.mon_native]), - ), - ( - self.var, - self.mon_param, - LinearExpression([self.mon_var, self.mon_param]), - ), - (self.var, self.mon_npv, LinearExpression([self.mon_var, self.mon_npv])), + (self.var, self.var, LinearExpression([self.var, self.var])), + (self.var, self.mon_native, LinearExpression([self.var, self.mon_native])), + (self.var, self.mon_param, LinearExpression([self.var, self.mon_param])), + (self.var, self.mon_npv, LinearExpression([self.var, self.mon_npv])), # 12: - ( - self.var, - self.linear, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.var, self.linear, LinearExpression(self.linear.args + [self.var])), (self.var, self.sum, SumExpression(self.sum.args + [self.var])), (self.var, self.other, SumExpression([self.var, self.other])), (self.var, self.mutable_l0, self.var), @@ -446,7 +418,7 @@ def test_add_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var] + self.mutable_l1.args), + LinearExpression([self.var] + self.mutable_l1.args), ), ( self.var, @@ -454,13 +426,9 @@ def test_add_var(self): SumExpression(self.mutable_l2.args + [self.var]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, 1])), + (self.var, self.param1, LinearExpression([self.var, 1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([MonomialTermExpression((1, self.var)), self.npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -471,7 +439,7 @@ def test_add_mon_native(self): ( self.mon_native, self.asbinary, - LinearExpression([self.mon_native, self.mon_bin]), + LinearExpression([self.mon_native, self.bin]), ), (self.mon_native, self.zero, self.mon_native), (self.mon_native, self.one, LinearExpression([self.mon_native, 1])), @@ -485,11 +453,7 @@ def test_add_mon_native(self): LinearExpression([self.mon_native, self.param_mut]), ), # 8: - ( - self.mon_native, - self.var, - LinearExpression([self.mon_native, self.mon_var]), - ), + (self.mon_native, self.var, LinearExpression([self.mon_native, self.var])), ( self.mon_native, self.mon_native, @@ -547,7 +511,7 @@ def test_add_mon_param(self): ( self.mon_param, self.asbinary, - LinearExpression([self.mon_param, self.mon_bin]), + LinearExpression([self.mon_param, self.bin]), ), (self.mon_param, self.zero, self.mon_param), (self.mon_param, self.one, LinearExpression([self.mon_param, 1])), @@ -561,11 +525,7 @@ def test_add_mon_param(self): LinearExpression([self.mon_param, self.param_mut]), ), # 8: - ( - self.mon_param, - self.var, - LinearExpression([self.mon_param, self.mon_var]), - ), + (self.mon_param, self.var, LinearExpression([self.mon_param, self.var])), ( self.mon_param, self.mon_native, @@ -616,11 +576,7 @@ def test_add_mon_param(self): def test_add_mon_npv(self): tests = [ (self.mon_npv, self.invalid, NotImplemented), - ( - self.mon_npv, - self.asbinary, - LinearExpression([self.mon_npv, self.mon_bin]), - ), + (self.mon_npv, self.asbinary, LinearExpression([self.mon_npv, self.bin])), (self.mon_npv, self.zero, self.mon_npv), (self.mon_npv, self.one, LinearExpression([self.mon_npv, 1])), # 4: @@ -633,7 +589,7 @@ def test_add_mon_npv(self): LinearExpression([self.mon_npv, self.param_mut]), ), # 8: - (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.mon_var])), + (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.var])), ( self.mon_npv, self.mon_native, @@ -683,7 +639,7 @@ def test_add_linear(self): ( self.linear, self.asbinary, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.linear, self.zero, self.linear), (self.linear, self.one, LinearExpression(self.linear.args + [1])), @@ -697,11 +653,7 @@ def test_add_linear(self): LinearExpression(self.linear.args + [self.param_mut]), ), # 8: - ( - self.linear, - self.var, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.linear, self.var, LinearExpression(self.linear.args + [self.var])), ( self.linear, self.mon_native, @@ -868,7 +820,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.asbinary, - LinearExpression(self.mutable_l1.args + [self.mon_bin]), + LinearExpression(self.mutable_l1.args + [self.bin]), ), (self.mutable_l1, self.zero, self.mon_npv), (self.mutable_l1, self.one, LinearExpression(self.mutable_l1.args + [1])), @@ -893,7 +845,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.var, - LinearExpression(self.mutable_l1.args + [self.mon_var]), + LinearExpression(self.mutable_l1.args + [self.var]), ), ( self.mutable_l1, @@ -1075,7 +1027,7 @@ def test_add_param0(self): def test_add_param1(self): tests = [ (self.param1, self.invalid, NotImplemented), - (self.param1, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.param1, self.asbinary, LinearExpression([1, self.bin])), (self.param1, self.zero, 1), (self.param1, self.one, 2), # 4: @@ -1084,7 +1036,7 @@ def test_add_param1(self): (self.param1, self.param, 7), (self.param1, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.param1, self.var, LinearExpression([1, self.mon_var])), + (self.param1, self.var, LinearExpression([1, self.var])), (self.param1, self.mon_native, LinearExpression([1, self.mon_native])), (self.param1, self.mon_param, LinearExpression([1, self.mon_param])), (self.param1, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -1114,7 +1066,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.asbinary, - LinearExpression(self.mutable_l3.args + [self.mon_bin]), + LinearExpression(self.mutable_l3.args + [self.bin]), ), (self.mutable_l3, self.zero, self.npv), (self.mutable_l3, self.one, NPV_SumExpression(self.mutable_l3.args + [1])), @@ -1143,7 +1095,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.var, - LinearExpression(self.mutable_l3.args + [self.mon_var]), + LinearExpression(self.mutable_l3.args + [self.var]), ), ( self.mutable_l3, @@ -1249,32 +1201,32 @@ def test_sub_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.one, LinearExpression([self.bin, -1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, -5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.minus_npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, -6])), + (self.asbinary, self.native, LinearExpression([self.bin, -5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.minus_npv])), + (self.asbinary, self.param, LinearExpression([self.bin, -6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.minus_param_mut]), + LinearExpression([self.bin, self.minus_param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.minus_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.minus_var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.minus_mon_native]), + LinearExpression([self.bin, self.minus_mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.minus_mon_param]), + LinearExpression([self.bin, self.minus_mon_param]), ), ( self.asbinary, self.mon_npv, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), # 12: (self.asbinary, self.linear, SumExpression([self.bin, self.minus_linear])), @@ -1285,7 +1237,7 @@ def test_sub_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), ( self.asbinary, @@ -1293,12 +1245,12 @@ def test_sub_asbinary(self): SumExpression([self.bin, self.minus_mutable_l2]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.param1, LinearExpression([self.bin, -1])), # 20: ( self.asbinary, self.mutable_l3, - LinearExpression([self.mon_bin, self.minus_npv]), + LinearExpression([self.bin, self.minus_npv]), ), ] self._run_cases(tests, operator.sub) @@ -1571,35 +1523,31 @@ def test_sub_param_mut(self): def test_sub_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.minus_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.minus_bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, -1])), + (self.var, self.one, LinearExpression([self.var, -1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, -5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.minus_npv])), - (self.var, self.param, LinearExpression([self.mon_var, -6])), + (self.var, self.native, LinearExpression([self.var, -5])), + (self.var, self.npv, LinearExpression([self.var, self.minus_npv])), + (self.var, self.param, LinearExpression([self.var, -6])), ( self.var, self.param_mut, - LinearExpression([self.mon_var, self.minus_param_mut]), + LinearExpression([self.var, self.minus_param_mut]), ), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.minus_var])), + (self.var, self.var, LinearExpression([self.var, self.minus_var])), ( self.var, self.mon_native, - LinearExpression([self.mon_var, self.minus_mon_native]), + LinearExpression([self.var, self.minus_mon_native]), ), ( self.var, self.mon_param, - LinearExpression([self.mon_var, self.minus_mon_param]), - ), - ( - self.var, - self.mon_npv, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_param]), ), + (self.var, self.mon_npv, LinearExpression([self.var, self.minus_mon_npv])), # 12: ( self.var, @@ -1613,7 +1561,7 @@ def test_sub_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_npv]), ), ( self.var, @@ -1621,13 +1569,9 @@ def test_sub_var(self): SumExpression([self.var, self.minus_mutable_l2]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, -1])), + (self.var, self.param1, LinearExpression([self.var, -1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([self.mon_var, self.minus_npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.minus_npv])), ] self._run_cases(tests, operator.sub) self._run_cases(tests, operator.isub) @@ -6039,7 +5983,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([])), (mutable_npv, self.one, _MutableNPVSumExpression([1])), # 4: @@ -6048,7 +5992,7 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.param, _MutableNPVSumExpression([6])), (mutable_npv, self.param_mut, _MutableNPVSumExpression([self.param_mut])), # 8: - (mutable_npv, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([self.var])), (mutable_npv, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_npv, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_npv, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6074,7 +6018,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([10]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), (mutable_npv, self.one, _MutableNPVSumExpression([11])), # 4: @@ -6087,7 +6031,7 @@ def test_mutable_nvp_iadd(self): _MutableNPVSumExpression([10, self.param_mut]), ), # 8: - (mutable_npv, self.var, _MutableLinearExpression([10, self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([10, self.var])), ( mutable_npv, self.mon_native, @@ -6130,7 +6074,7 @@ def test_mutable_lin_iadd(self): mutable_lin = _MutableLinearExpression([]) tests = [ (mutable_lin, self.invalid, NotImplemented), - (mutable_lin, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_lin, self.zero, _MutableLinearExpression([])), (mutable_lin, self.one, _MutableLinearExpression([1])), # 4: @@ -6139,7 +6083,7 @@ def test_mutable_lin_iadd(self): (mutable_lin, self.param, _MutableLinearExpression([6])), (mutable_lin, self.param_mut, _MutableLinearExpression([self.param_mut])), # 8: - (mutable_lin, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_lin, self.var, _MutableLinearExpression([self.var])), (mutable_lin, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_lin, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_lin, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6162,81 +6106,69 @@ def test_mutable_lin_iadd(self): ] self._run_iadd_cases(tests, operator.iadd) - mutable_lin = _MutableLinearExpression([self.mon_bin]) + mutable_lin = _MutableLinearExpression([self.bin]) tests = [ (mutable_lin, self.invalid, NotImplemented), ( mutable_lin, self.asbinary, - _MutableLinearExpression([self.mon_bin, self.mon_bin]), + _MutableLinearExpression([self.bin, self.bin]), ), - (mutable_lin, self.zero, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.one, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.zero, _MutableLinearExpression([self.bin])), + (mutable_lin, self.one, _MutableLinearExpression([self.bin, 1])), # 4: - (mutable_lin, self.native, _MutableLinearExpression([self.mon_bin, 5])), - (mutable_lin, self.npv, _MutableLinearExpression([self.mon_bin, self.npv])), - (mutable_lin, self.param, _MutableLinearExpression([self.mon_bin, 6])), + (mutable_lin, self.native, _MutableLinearExpression([self.bin, 5])), + (mutable_lin, self.npv, _MutableLinearExpression([self.bin, self.npv])), + (mutable_lin, self.param, _MutableLinearExpression([self.bin, 6])), ( mutable_lin, self.param_mut, - _MutableLinearExpression([self.mon_bin, self.param_mut]), + _MutableLinearExpression([self.bin, self.param_mut]), ), # 8: - ( - mutable_lin, - self.var, - _MutableLinearExpression([self.mon_bin, self.mon_var]), - ), + (mutable_lin, self.var, _MutableLinearExpression([self.bin, self.var])), ( mutable_lin, self.mon_native, - _MutableLinearExpression([self.mon_bin, self.mon_native]), + _MutableLinearExpression([self.bin, self.mon_native]), ), ( mutable_lin, self.mon_param, - _MutableLinearExpression([self.mon_bin, self.mon_param]), + _MutableLinearExpression([self.bin, self.mon_param]), ), ( mutable_lin, self.mon_npv, - _MutableLinearExpression([self.mon_bin, self.mon_npv]), + _MutableLinearExpression([self.bin, self.mon_npv]), ), # 12: ( mutable_lin, self.linear, - _MutableLinearExpression([self.mon_bin] + self.linear.args), - ), - ( - mutable_lin, - self.sum, - _MutableSumExpression([self.mon_bin] + self.sum.args), - ), - ( - mutable_lin, - self.other, - _MutableSumExpression([self.mon_bin, self.other]), + _MutableLinearExpression([self.bin] + self.linear.args), ), - (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.sum, _MutableSumExpression([self.bin] + self.sum.args)), + (mutable_lin, self.other, _MutableSumExpression([self.bin, self.other])), + (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.bin])), # 16: ( mutable_lin, self.mutable_l1, - _MutableLinearExpression([self.mon_bin] + self.mutable_l1.args), + _MutableLinearExpression([self.bin] + self.mutable_l1.args), ), ( mutable_lin, self.mutable_l2, - _MutableSumExpression([self.mon_bin] + self.mutable_l2.args), + _MutableSumExpression([self.bin] + self.mutable_l2.args), ), - (mutable_lin, self.param0, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.param1, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.param0, _MutableLinearExpression([self.bin])), + (mutable_lin, self.param1, _MutableLinearExpression([self.bin, 1])), # 20: ( mutable_lin, self.mutable_l3, - _MutableLinearExpression([self.mon_bin, self.npv]), + _MutableLinearExpression([self.bin, self.npv]), ), ] self._run_iadd_cases(tests, operator.iadd) diff --git a/pyomo/core/tests/unit/test_visitor.py b/pyomo/core/tests/unit/test_visitor.py index fada7d6f6b2..12fb98d1d19 100644 --- a/pyomo/core/tests/unit/test_visitor.py +++ b/pyomo/core/tests/unit/test_visitor.py @@ -437,9 +437,7 @@ def test_replacement_linear_expression_with_constant(self): sub_map = dict() sub_map[id(m.x)] = 5 e2 = replace_expressions(e, sub_map) - assertExpressionsEqual( - self, e2, LinearExpression([10, MonomialTermExpression((1, m.y))]) - ) + assertExpressionsEqual(self, e2, LinearExpression([10, m.y])) e = LinearExpression(linear_coefs=[2, 3], linear_vars=[m.x, m.y]) sub_map = dict() @@ -886,20 +884,7 @@ def test_replace(self): assertExpressionsEqual( self, SumExpression( - [ - LinearExpression( - [ - MonomialTermExpression((1, m.y[1])), - MonomialTermExpression((1, m.y[2])), - ] - ), - LinearExpression( - [ - MonomialTermExpression((1, m.y[2])), - MonomialTermExpression((1, m.y[3])), - ] - ), - ] + [LinearExpression([m.y[1], m.y[2]]), LinearExpression([m.y[2], m.y[3]])] ) == 0, f, @@ -930,9 +915,7 @@ def test_npv_sum(self): e3 = replace_expressions(e1, {id(m.p1): m.x}) assertExpressionsEqual(self, e2, m.p2 + 2) - assertExpressionsEqual( - self, e3, LinearExpression([MonomialTermExpression((1, m.x)), 2]) - ) + assertExpressionsEqual(self, e3, LinearExpression([m.x, 2])) def test_npv_negation(self): m = ConcreteModel() diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index 28025816262..5d0d6f6c21b 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -425,12 +425,7 @@ def check_two_term_disjunction_xor(self, xor, disj1, disj2): assertExpressionsEqual( self, xor.body, - EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, disj1.binary_indicator_var)), - EXPR.MonomialTermExpression((1, disj2.binary_indicator_var)), - ] - ), + EXPR.LinearExpression([disj1.binary_indicator_var, disj2.binary_indicator_var]), ) self.assertEqual(xor.lower, 1) self.assertEqual(xor.upper, 1) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index 2383d4587f5..c6ac49f6d36 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -155,10 +155,7 @@ def test_or_constraints(self): self, orcons.body, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.d[0].binary_indicator_var)), - EXPR.MonomialTermExpression((1, m.d[1].binary_indicator_var)), - ] + [m.d[0].binary_indicator_var, m.d[1].binary_indicator_var] ), ) self.assertEqual(orcons.lower, 1) diff --git a/pyomo/gdp/tests/test_binary_multiplication.py b/pyomo/gdp/tests/test_binary_multiplication.py index aa846c4710a..ae2c44b899e 100644 --- a/pyomo/gdp/tests/test_binary_multiplication.py +++ b/pyomo/gdp/tests/test_binary_multiplication.py @@ -146,10 +146,7 @@ def test_or_constraints(self): self, orcons.body, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.d[0].binary_indicator_var)), - EXPR.MonomialTermExpression((1, m.d[1].binary_indicator_var)), - ] + [m.d[0].binary_indicator_var, m.d[1].binary_indicator_var] ), ) self.assertEqual(orcons.lower, 1) diff --git a/pyomo/gdp/tests/test_disjunct.py b/pyomo/gdp/tests/test_disjunct.py index d969b245ee7..f93ac31fb0f 100644 --- a/pyomo/gdp/tests/test_disjunct.py +++ b/pyomo/gdp/tests/test_disjunct.py @@ -632,19 +632,13 @@ def test_cast_to_binary(self): out = StringIO() with LoggingIntercept(out): e = m.iv + 1 - assertExpressionsEqual( - self, e, EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), 1]) - ) + assertExpressionsEqual(self, e, EXPR.LinearExpression([m.biv, 1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() with LoggingIntercept(out): e = m.iv - 1 - assertExpressionsEqual( - self, - e, - EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), -1]), - ) + assertExpressionsEqual(self, e, EXPR.LinearExpression([m.biv, -1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() @@ -665,9 +659,7 @@ def test_cast_to_binary(self): out = StringIO() with LoggingIntercept(out): e = 1 + m.iv - assertExpressionsEqual( - self, e, EXPR.LinearExpression([1, EXPR.MonomialTermExpression((1, m.biv))]) - ) + assertExpressionsEqual(self, e, EXPR.LinearExpression([1, m.biv])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() @@ -699,20 +691,14 @@ def test_cast_to_binary(self): with LoggingIntercept(out): a = m.iv a += 1 - assertExpressionsEqual( - self, a, EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), 1]) - ) + assertExpressionsEqual(self, a, EXPR.LinearExpression([m.biv, 1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() with LoggingIntercept(out): a = m.iv a -= 1 - assertExpressionsEqual( - self, - a, - EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), -1]), - ) + assertExpressionsEqual(self, a, EXPR.LinearExpression([m.biv, -1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() From 7299a79db5481a5ac4faf683d1d71bba074c01b7 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 13:05:52 -0700 Subject: [PATCH 0880/3044] remove redundant implementation of acceptChildResult --- pyomo/util/subsystems.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 9a2a8b6635d..35deaab7605 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -59,9 +59,6 @@ def finalizeResult(self, result): def enterNode(self, node): pass - def acceptChildResult(self, node, data, child_result, child_idx): - pass - def acceptChildResult(self, node, data, child_result, child_idx): if child_result.__class__ in native_types: return False, None From 8b66e10c08ac149272ebd94b7f847cfed8dba7b6 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 13:06:47 -0700 Subject: [PATCH 0881/3044] comment out unnecessary walker methods --- pyomo/util/subsystems.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 35deaab7605..58921b37cca 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -56,13 +56,13 @@ def exitNode(self, node, data): def finalizeResult(self, result): return self._functions - def enterNode(self, node): - pass + #def enterNode(self, node): + # pass - def acceptChildResult(self, node, data, child_result, child_idx): - if child_result.__class__ in native_types: - return False, None - return child_result.is_expression_type(), None + #def acceptChildResult(self, node, data, child_result, child_idx): + # if child_result.__class__ in native_types: + # return False, None + # return child_result.is_expression_type(), None def identify_external_functions(expr): From e7a4c948e7e05455f44745c472b9af4f5265edd2 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Thu, 7 Mar 2024 16:09:42 -0700 Subject: [PATCH 0882/3044] add failing test --- pyomo/contrib/appsi/tests/test_fbbt.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pyomo/contrib/appsi/tests/test_fbbt.py b/pyomo/contrib/appsi/tests/test_fbbt.py index a3f520e7bd6..97af611c572 100644 --- a/pyomo/contrib/appsi/tests/test_fbbt.py +++ b/pyomo/contrib/appsi/tests/test_fbbt.py @@ -151,3 +151,16 @@ def test_named_exprs(self): for x in m.x.values(): self.assertAlmostEqual(x.lb, 0) self.assertAlmostEqual(x.ub, 0) + + def test_named_exprs_nest(self): + # test for issue #3184 + m = pe.ConcreteModel() + m.x = pe.Var() + m.e = pe.Expression(expr=m.x + 1) + m.f = pe.Expression(expr=m.e) + m.c = pe.Constraint(expr=(0, m.f, 0)) + it = appsi.fbbt.IntervalTightener() + it.perform_fbbt(m) + for x in m.x.values(): + self.assertAlmostEqual(x.lb, -1) + self.assertAlmostEqual(x.ub, -1) From cd8c6ae6a9e30c4ce8f998252a8b82e4b80bfc93 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Thu, 7 Mar 2024 16:12:08 -0700 Subject: [PATCH 0883/3044] apply patch --- pyomo/contrib/appsi/cmodel/src/expression.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/cmodel/src/expression.cpp b/pyomo/contrib/appsi/cmodel/src/expression.cpp index 234ef47e86f..f1446c6a21b 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.cpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.cpp @@ -1789,7 +1789,8 @@ int build_expression_tree(py::handle pyomo_expr, if (expr_types.expr_type_map[py::type::of(pyomo_expr)].cast() == named_expr) - pyomo_expr = pyomo_expr.attr("expr"); + return build_expression_tree(pyomo_expr.attr("expr"), appsi_expr, var_map, + param_map, expr_types); if (appsi_expr->is_leaf()) { ; From 46b89e657ac60867fa29f4c763bf8521097d3426 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 7 Mar 2024 16:37:47 -0700 Subject: [PATCH 0884/3044] Change around the logic to use nobjectives --- pyomo/contrib/solver/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 54871e90c2f..55b013facb1 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -41,7 +41,8 @@ class SolverBase(abc.ABC): """ This base class defines the methods required for all solvers: - - available: Determines whether the solver is able to be run, combining both whether it can be found on the system and if the license is valid. + - available: Determines whether the solver is able to be run, + combining both whether it can be found on the system and if the license is valid. - solve: The main method of every solver - version: The version of the solver - is_persistent: Set to false for all non-persistent solvers. @@ -420,12 +421,11 @@ def _map_results(self, model, results): legacy_results.solver.termination_message = str(results.termination_condition) legacy_results.problem.number_of_constraints = model.nconstraints() legacy_results.problem.number_of_variables = model.nvariables() - if model.nobjectives() == 0: - legacy_results.problem.number_of_objectives = 0 - else: + number_of_objectives = model.nobjectives() + legacy_results.problem.number_of_objectives = number_of_objectives + if number_of_objectives > 0: obj = get_objective(model) legacy_results.problem.sense = obj.sense - legacy_results.problem.number_of_objectives = len(obj) if obj.sense == minimize: legacy_results.problem.lower_bound = results.objective_bound From 70f225e6c0f854252fb94ecfb1eef0d58a6b818d Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 16:46:08 -0700 Subject: [PATCH 0885/3044] apply black --- pyomo/util/subsystems.py | 7 ++++--- pyomo/util/tests/test_subsystems.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 58921b37cca..ff5f6dedd58 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -25,7 +25,6 @@ class _ExternalFunctionVisitor(StreamBasedExpressionVisitor): - def __init__(self, descend_into_named_expressions=True): super().__init__() self._descend_into_named_expressions = descend_into_named_expressions @@ -56,10 +55,10 @@ def exitNode(self, node, data): def finalizeResult(self, result): return self._functions - #def enterNode(self, node): + # def enterNode(self, node): # pass - #def acceptChildResult(self, node, data, child_result, child_idx): + # def acceptChildResult(self, node, data, child_result, child_idx): # if child_result.__class__ in native_types: # return False, None # return child_result.is_expression_type(), None @@ -114,6 +113,8 @@ def add_local_external_functions(block): from pyomo.common.timing import HierarchicalTimer + + def create_subsystem_block( constraints, variables=None, diff --git a/pyomo/util/tests/test_subsystems.py b/pyomo/util/tests/test_subsystems.py index f102670ba62..a9b8a215fcc 100644 --- a/pyomo/util/tests/test_subsystems.py +++ b/pyomo/util/tests/test_subsystems.py @@ -304,11 +304,11 @@ def _make_model_with_external_functions(self, named_expressions=False): m.subexpr = pyo.Expression(pyo.PositiveIntegers) subexpr1 = m.subexpr[1] = 2 * m.fermi(m.v1) subexpr2 = m.subexpr[2] = m.bessel(m.v1) - m.bessel(m.v2) - subexpr3 = m.subexpr[3] = m.subexpr[2] + m.v3 ** 2 + subexpr3 = m.subexpr[3] = m.subexpr[2] + m.v3**2 else: subexpr1 = 2 * m.fermi(m.v1) subexpr2 = m.bessel(m.v1) - m.bessel(m.v2) - subexpr3 = m.subexpr[2] + m.v3 ** 2 + subexpr3 = m.subexpr[2] + m.v3**2 m.con1 = pyo.Constraint(expr=m.v1 == 0.5) m.con2 = pyo.Constraint(expr=subexpr1 + m.v2**2 - m.v3 == 1.0) m.con3 = pyo.Constraint(expr=subexpr3 == 2.0) From 1c6a6c79ec2297c142367da385145c6ce7ba4884 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 7 Mar 2024 17:02:03 -0700 Subject: [PATCH 0886/3044] type hints --- pyomo/core/base/block.py | 5 ++++- pyomo/core/base/constraint.py | 5 ++++- pyomo/core/base/set.py | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 908e0ef1abd..f3d9c7458e1 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2265,8 +2265,11 @@ class IndexedBlock(Block): def __init__(self, *args, **kwds): Block.__init__(self, *args, **kwds) + @overload def __getitem__(self, index) -> _BlockData: - return super().__getitem__(index) + ... + + __getitem__ = IndexedComponent.__getitem__ # type: ignore # diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index a36bc679e49..899bc8c9499 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -1033,8 +1033,11 @@ def add(self, index, expr): """Add a constraint with a given index.""" return self.__setitem__(index, expr) + @overload def __getitem__(self, index) -> _GeneralConstraintData: - return super().__getitem__(index) + ... + + __getitem__ = IndexedComponent.__getitem__ # type: ignore @ModelComponentFactory.register("A list of constraint expressions.") diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index b8ddae14e9f..9217c09866e 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -2382,8 +2382,11 @@ def data(self): "Return a dict containing the data() of each Set in this IndexedSet" return {k: v.data() for k, v in self.items()} + @overload def __getitem__(self, index) -> _SetData: - return super().__getitem__(index) + ... + + __getitem__ = IndexedComponent.__getitem__ # type: ignore class FiniteScalarSet(_FiniteSetData, Set): From 00d2a977471d04a032079c374eb6f4e9e7f2d6cc Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 7 Mar 2024 17:06:30 -0700 Subject: [PATCH 0887/3044] run black --- pyomo/core/base/block.py | 3 +-- pyomo/core/base/constraint.py | 3 +-- pyomo/core/base/set.py | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index f3d9c7458e1..2918ef78b00 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2266,8 +2266,7 @@ def __init__(self, *args, **kwds): Block.__init__(self, *args, **kwds) @overload - def __getitem__(self, index) -> _BlockData: - ... + def __getitem__(self, index) -> _BlockData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 899bc8c9499..8916777e9c8 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -1034,8 +1034,7 @@ def add(self, index, expr): return self.__setitem__(index, expr) @overload - def __getitem__(self, index) -> _GeneralConstraintData: - ... + def __getitem__(self, index) -> _GeneralConstraintData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 9217c09866e..b3277ab3260 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -2383,8 +2383,7 @@ def data(self): return {k: v.data() for k, v in self.items()} @overload - def __getitem__(self, index) -> _SetData: - ... + def __getitem__(self, index) -> _SetData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore From b96d12eea18f9c0bf570ef0b420e4fcbd0f26370 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 17:17:27 -0700 Subject: [PATCH 0888/3044] arguments on one line to keep black happy --- pyomo/contrib/incidence_analysis/scc_solver.py | 6 +----- pyomo/util/subsystems.py | 8 ++------ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 6c556646a8c..86b02c94194 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -26,11 +26,7 @@ def generate_strongly_connected_components( - constraints, - variables=None, - include_fixed=False, - igraph=None, - timer=None, + constraints, variables=None, include_fixed=False, igraph=None, timer=None ): """Yield in order ``_BlockData`` that each contain the variables and constraints of a single diagonal block in a block lower triangularization diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index ff5f6dedd58..79fbdd2d281 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -75,8 +75,7 @@ def add_local_external_functions(block): named_expressions = [] visitor = _ExternalFunctionVisitor(descend_into_named_expressions=False) for comp in block.component_data_objects( - (Constraint, Expression, Objective), - active=True, + (Constraint, Expression, Objective), active=True ): ef_exprs.extend(visitor.walk_expression(comp.expr)) named_expr_set = ComponentSet(visitor.named_expressions) @@ -116,10 +115,7 @@ def add_local_external_functions(block): def create_subsystem_block( - constraints, - variables=None, - include_fixed=False, - timer=None, + constraints, variables=None, include_fixed=False, timer=None ): """This function creates a block to serve as a subsystem with the specified variables and constraints. To satisfy certain writers, other From 67b9b2f958772e3e0fe699c0ca687912cf5585a4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 17:37:13 -0700 Subject: [PATCH 0889/3044] Update PyROS to admit VarData in LinearExpressions --- .../contrib/pyros/pyros_algorithm_methods.py | 20 ++++++---- pyomo/contrib/pyros/tests/test_grcs.py | 40 +++++++++++-------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 45b652447ff..f847a3a73dc 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -26,6 +26,7 @@ ) from pyomo.contrib.pyros.util import get_main_elapsed_time, coefficient_matching from pyomo.core.base import value +from pyomo.core.expr import MonomialTermExpression from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.core.base.var import _VarData as VarData from itertools import chain @@ -69,14 +70,17 @@ def get_dr_var_to_scaled_expr_map( ssv_dr_eq_zip = zip(second_stage_vars, decision_rule_eqns) for ssv_idx, (ssv, dr_eq) in enumerate(ssv_dr_eq_zip): for term in dr_eq.body.args: - is_ssv_term = ( - isinstance(term.args[0], int) - and term.args[0] == -1 - and isinstance(term.args[1], VarData) - ) - if not is_ssv_term: - dr_var = term.args[1] - var_to_scaled_expr_map[dr_var] = term + if isinstance(term, MonomialTermExpression): + is_ssv_term = ( + isinstance(term.args[0], int) + and term.args[0] == -1 + and isinstance(term.args[1], VarData) + ) + if not is_ssv_term: + dr_var = term.args[1] + var_to_scaled_expr_map[dr_var] = term + elif isinstance(term, VarData): + var_to_scaled_expr_map[term] = MonomialTermExpression((1, term)) return var_to_scaled_expr_map diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index df3568e42a4..c308f0d6990 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -19,6 +19,7 @@ from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.core.base.set_types import NonNegativeIntegers +from pyomo.core.base.var import _VarData from pyomo.core.expr import ( identify_variables, identify_mutable_parameters, @@ -571,22 +572,30 @@ def test_dr_eqns_form_correct(self): dr_polynomial_terms, indexed_dr_var.values(), dr_monomial_param_combos ) for idx, (term, dr_var, param_combo) in enumerate(dr_polynomial_zip): - # term should be a monomial expression of form - # (uncertain parameter product) * (decision rule variable) - # so length of expression object should be 2 - self.assertEqual( - len(term.args), - 2, - msg=( - f"Length of `args` attribute of term {str(term)} " - f"of DR equation {dr_eq.name!r} is not as expected. " - f"Args: {term.args}" - ), - ) + # term should be either a monomial expression or scalar variable + if isinstance(term, MonomialTermExpression): + # should be of form (uncertain parameter product) * + # (decision rule variable) so length of expression + # object should be 2 + self.assertEqual( + len(term.args), + 2, + msg=( + f"Length of `args` attribute of term {str(term)} " + f"of DR equation {dr_eq.name!r} is not as expected. " + f"Args: {term.args}" + ), + ) + + # check that uncertain parameters participating in + # the monomial are as expected + param_product_multiplicand = term.args[0] + dr_var_multiplicand = term.args[1] + else: + self.assertIsInstance(term, _VarData) + param_product_multiplicand = 1 + dr_var_multiplicand = term - # check that uncertain parameters participating in - # the monomial are as expected - param_product_multiplicand = term.args[0] if idx == 0: # static DR term param_combo_found_in_term = (param_product_multiplicand,) @@ -612,7 +621,6 @@ def test_dr_eqns_form_correct(self): # check that DR variable participating in the monomial # is as expected - dr_var_multiplicand = term.args[1] self.assertIs( dr_var_multiplicand, dr_var, From ec4f733a1b6c7de8ef9f624ece8befd78f081128 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 18:02:27 -0700 Subject: [PATCH 0890/3044] use_calc_var default should be True --- pyomo/contrib/incidence_analysis/scc_solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 86b02c94194..76eb7f91cb0 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -100,7 +100,7 @@ def solve_strongly_connected_components( *, solver=None, solve_kwds=None, - use_calc_var=False, + use_calc_var=True, calc_var_kwds=None, timer=None, ): From cd54e6f72563f46adbfd8b61ac76e6d7f0d53824 Mon Sep 17 00:00:00 2001 From: robbybp Date: Thu, 7 Mar 2024 18:05:22 -0700 Subject: [PATCH 0891/3044] re-add exception I accidentally deleted --- pyomo/contrib/incidence_analysis/scc_solver.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 76eb7f91cb0..eff4f5ae5fa 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -175,7 +175,15 @@ def solve_strongly_connected_components( ) timer.stop("calc-var-from-con") else: - inputs = list(scc.input_vars.values()) + if solver is None: + var_names = [var.name for var in scc.vars.values()][:10] + con_names = [con.name for con in scc.cons.values()][:10] + 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 of size %sx%s including" + " components:\n%s\n%s" % (N, N, var_names, con_names) + ) if log_blocks: _log.debug(f"Solving {N}x{N} block.") timer.start("scc-subsolver") From c7b34d043876653b8a53d6d0aeca3937d45e5319 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 18:08:39 -0700 Subject: [PATCH 0892/3044] Resolve incompatibility with Python<=3.10 --- pyomo/core/expr/template_expr.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index 6ac4c8c041f..a7f301e32f1 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -117,18 +117,10 @@ def _to_string(self, values, verbose, smap): return "%s[%s]" % (values[0], ','.join(values[1:])) def _resolve_template(self, args): - return args[0][*args[1:]] + return args[0].__getitem__(args[1:]) def _apply_operation(self, result): - args = tuple( - ( - arg - if arg.__class__ in native_types or not arg.is_numeric_type() - else value(arg) - ) - for arg in result[1:] - ) - return result[0][*result[1:]] + return result[0].__getitem__(result[1:]) class Numeric_GetItemExpression(GetItemExpression, NumericExpression): From fadca016f1efcf6f59bdfce2c6f7a5e926836a0e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 18:09:06 -0700 Subject: [PATCH 0893/3044] Minor code readibility improvement --- pyomo/core/expr/template_expr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index a7f301e32f1..d30046e9d82 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -251,8 +251,8 @@ def nargs(self): return 2 def _apply_operation(self, result): - assert len(result) == 2 - return getattr(result[0], result[1]) + obj, attr = result + return getattr(obj, attr) def _to_string(self, values, verbose, smap): assert len(values) == 2 From f7b70038e58fe5c0eb3a98a8eaf005cb6113ae0a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 21:09:54 -0700 Subject: [PATCH 0894/3044] Update doc tests to track change in LinearExpression arg types --- doc/OnlineDocs/src/expr/managing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/src/expr/managing.py b/doc/OnlineDocs/src/expr/managing.py index 00d521d16ab..ff149e4fd5c 100644 --- a/doc/OnlineDocs/src/expr/managing.py +++ b/doc/OnlineDocs/src/expr/managing.py @@ -181,7 +181,7 @@ def clone_expression(expr): # x[0] + 5*x[1] print(str(ce)) # x[0] + 5*x[1] -print(e.arg(0) is not ce.arg(0)) +print(e.arg(0) is ce.arg(0)) # True print(e.arg(1) is not ce.arg(1)) # True From 7e66907075ccc0dc304270ebd059afda95012c5a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 8 Mar 2024 09:23:26 -0700 Subject: [PATCH 0895/3044] NFC: update docs --- pyomo/core/expr/numeric_expr.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index 2cf4073b49f..9b624d2b8bd 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -1234,9 +1234,11 @@ class LinearExpression(SumExpression): """An expression object for linear polynomials. This is a derived :py:class`SumExpression` that guarantees all - arguments are either not potentially variable (e.g., native types, - Params, or NPV expressions) OR :py:class:`MonomialTermExpression` - objects. + arguments are one of the following types: + + - not potentially variable (e.g., native types, Params, or NPV expressions) + - :py:class:`MonomialTermExpression` + - :py:class:`_VarData` Args: args (tuple): Children nodes @@ -1253,7 +1255,7 @@ def __init__(self, args=None, constant=None, linear_coefs=None, linear_vars=None You can specify `args` OR (`constant`, `linear_coefs`, and `linear_vars`). If `args` is provided, it should be a list that - contains only constants, NPV objects/expressions, or + contains only constants, NPV objects/expressions, variables, or :py:class:`MonomialTermExpression` objects. Alternatively, you can specify the constant, the list of linear_coefs and the list of linear_vars separately. Note that these lists are NOT From e5c8027420cb29333ba4e9fe30c5617387568c6f Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 8 Mar 2024 14:13:07 -0700 Subject: [PATCH 0896/3044] Add missing import --- pyomo/core/base/constraint.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 8916777e9c8..fde1160e563 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -44,6 +44,7 @@ ActiveIndexedComponent, UnindexedComponent_set, rule_wrapper, + IndexedComponent, ) from pyomo.core.base.set import Set from pyomo.core.base.disable_methods import disable_methods From 476fa8d7bdc80c37ba0d3aeca5a2a9c5d586c849 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 09:11:35 -0700 Subject: [PATCH 0897/3044] Allow bare variables in LinearExpression nodes --- pyomo/core/expr/numeric_expr.py | 39 ++++++++++++++++-------------- pyomo/repn/linear.py | 38 ++++++++++++++++------------- pyomo/repn/plugins/baron_writer.py | 19 ++++++++++++--- pyomo/repn/plugins/gams_writer.py | 10 +++++++- pyomo/repn/plugins/nl_writer.py | 14 +++++++++++ pyomo/repn/quadratic.py | 25 +++++++------------ pyomo/repn/standard_repn.py | 22 +++++++++++++++++ pyomo/repn/tests/test_linear.py | 2 +- 8 files changed, 112 insertions(+), 57 deletions(-) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index c1199ffdcad..8ce7ee81c9a 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -1298,8 +1298,14 @@ def _build_cache(self): if arg.__class__ is MonomialTermExpression: coef.append(arg._args_[0]) var.append(arg._args_[1]) - else: + elif arg.__class__ in native_numeric_types: const += arg + elif not arg.is_potentially_variable(): + const += arg + else: + assert arg.is_potentially_variable() + coef.append(1) + var.append(arg) LinearExpression._cache = (self, const, coef, var) @property @@ -1325,7 +1331,7 @@ def create_node_with_local_data(self, args, classtype=None): classtype = self.__class__ if type(args) is not list: args = list(args) - for i, arg in enumerate(args): + for arg in args: if arg.__class__ in self._allowable_linear_expr_arg_types: # 99% of the time, the arg type hasn't changed continue @@ -1336,8 +1342,7 @@ def create_node_with_local_data(self, args, classtype=None): # NPV expressions are OK pass elif arg.is_variable_type(): - # vars are OK, but need to be mapped to monomial terms - args[i] = MonomialTermExpression((1, arg)) + # vars are OK continue else: # For anything else, convert this to a general sum @@ -1820,7 +1825,7 @@ def _add_native_param(a, b): def _add_native_var(a, b): if not a: return b - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_native_monomial(a, b): @@ -1871,7 +1876,7 @@ def _add_npv_param(a, b): def _add_npv_var(a, b): - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_npv_monomial(a, b): @@ -1929,7 +1934,7 @@ def _add_param_var(a, b): a = a.value if not a: return b - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_param_monomial(a, b): @@ -1972,11 +1977,11 @@ def _add_param_other(a, b): def _add_var_native(a, b): if not b: return a - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_npv(a, b): - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_param(a, b): @@ -1984,21 +1989,19 @@ def _add_var_param(a, b): b = b.value if not b: return a - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_var(a, b): - return LinearExpression( - [MonomialTermExpression((1, a)), MonomialTermExpression((1, b))] - ) + return LinearExpression([a, b]) def _add_var_monomial(a, b): - return LinearExpression([MonomialTermExpression((1, a)), b]) + return LinearExpression([a, b]) def _add_var_linear(a, b): - return b._trunc_append(MonomialTermExpression((1, a))) + return b._trunc_append(a) def _add_var_sum(a, b): @@ -2033,7 +2036,7 @@ def _add_monomial_param(a, b): def _add_monomial_var(a, b): - return LinearExpression([a, MonomialTermExpression((1, b))]) + return LinearExpression([a, b]) def _add_monomial_monomial(a, b): @@ -2076,7 +2079,7 @@ def _add_linear_param(a, b): def _add_linear_var(a, b): - return a._trunc_append(MonomialTermExpression((1, b))) + return a._trunc_append(b) def _add_linear_monomial(a, b): @@ -2401,7 +2404,7 @@ def _iadd_mutablelinear_param(a, b): def _iadd_mutablelinear_var(a, b): - a._args_.append(MonomialTermExpression((1, b))) + a._args_.append(b) a._nargs += 1 return a diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 6ab4abfdaf5..d601ccbcd7c 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -31,8 +31,8 @@ MonomialTermExpression, LinearExpression, SumExpression, - NPV_SumExpression, ExternalFunctionExpression, + mutable_expression, ) from pyomo.core.expr.relational_expr import ( EqualityExpression, @@ -120,22 +120,14 @@ def to_expression(self, visitor): ans = 0 if self.linear: var_map = visitor.var_map - if len(self.linear) == 1: - vid, coef = next(iter(self.linear.items())) - if coef == 1: - ans += var_map[vid] - elif coef: - ans += MonomialTermExpression((coef, var_map[vid])) - else: - pass - else: - ans += LinearExpression( - [ - MonomialTermExpression((coef, var_map[vid])) - for vid, coef in self.linear.items() - if coef - ] - ) + with mutable_expression() as e: + for vid, coef in self.linear.items(): + if coef: + e += coef * var_map[vid] + if e.nargs() > 1: + ans += e + elif e.nargs() == 1: + ans += e.arg(0) if self.constant: ans += self.constant if self.multiplier != 1: @@ -704,6 +696,18 @@ def _before_linear(visitor, child): linear[_id] = arg1 elif arg.__class__ in native_numeric_types: const += arg + elif arg.is_variable_type(): + _id = id(arg) + if _id not in var_map: + if arg.fixed: + const += visitor.check_constant(arg.value, arg) + continue + LinearBeforeChildDispatcher._record_var(visitor, arg) + linear[_id] = 1 + elif _id in linear: + linear[_id] += 1 + else: + linear[_id] = 1 else: try: const += visitor.check_constant(visitor.evaluate(arg), arg) diff --git a/pyomo/repn/plugins/baron_writer.py b/pyomo/repn/plugins/baron_writer.py index de19b5aad73..ab673b0c1c3 100644 --- a/pyomo/repn/plugins/baron_writer.py +++ b/pyomo/repn/plugins/baron_writer.py @@ -174,15 +174,26 @@ def _monomial_to_string(self, node): return self.smap.getSymbol(var) return ftoa(const, True) + '*' + self.smap.getSymbol(var) + def _var_to_string(self, node): + if node.is_fixed(): + return ftoa(node.value, True) + self.variables.add(id(node)) + return self.smap.getSymbol(node) + def _linear_to_string(self, node): values = [ ( self._monomial_to_string(arg) - if ( - arg.__class__ is EXPR.MonomialTermExpression - and not arg.arg(1).is_fixed() + if arg.__class__ is EXPR.MonomialTermExpression + else ( + ftoa(arg) + if arg.__class__ in native_numeric_types + else ( + self._var_to_string(arg) + if arg.is_variable_type() + else ftoa(value(arg), True) + ) ) - else ftoa(value(arg)) ) for arg in node.args ] diff --git a/pyomo/repn/plugins/gams_writer.py b/pyomo/repn/plugins/gams_writer.py index 5f94f176762..0756cb64920 100644 --- a/pyomo/repn/plugins/gams_writer.py +++ b/pyomo/repn/plugins/gams_writer.py @@ -183,7 +183,15 @@ def _linear_to_string(self, node): ( self._monomial_to_string(arg) if arg.__class__ is EXPR.MonomialTermExpression - else ftoa(arg, True) + else ( + ftoa(arg, True) + if arg.__class__ in native_numeric_types + else ( + self.smap.getSymbol(arg) + if arg.is_variable_type() and (not arg.fixed or self.output_fixed_variables) + else ftoa(value(arg), True) + ) + ) ) for arg in node.args ] diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index a256cd1b900..b82d4df77e2 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2780,6 +2780,20 @@ def _before_linear(visitor, child): linear[_id] = arg1 elif arg.__class__ in native_types: const += arg + elif arg.is_variable_type(): + _id = id(arg) + if _id not in var_map: + if arg.fixed: + if _id not in visitor.fixed_vars: + visitor.cache_fixed_var(_id, arg) + const += visitor.fixed_vars[_id] + continue + _before_child_handlers._record_var(visitor, arg) + linear[_id] = 1 + elif _id in linear: + linear[_id] += 1 + else: + linear[_id] = 1 else: try: const += visitor.check_constant(visitor.evaluate(arg), arg) diff --git a/pyomo/repn/quadratic.py b/pyomo/repn/quadratic.py index c538d1efc7f..0ddfda829ed 100644 --- a/pyomo/repn/quadratic.py +++ b/pyomo/repn/quadratic.py @@ -98,22 +98,15 @@ def to_expression(self, visitor): e += coef * (var_map[x1] * var_map[x2]) ans += e if self.linear: - if len(self.linear) == 1: - vid, coef = next(iter(self.linear.items())) - if coef == 1: - ans += var_map[vid] - elif coef: - ans += MonomialTermExpression((coef, var_map[vid])) - else: - pass - else: - ans += LinearExpression( - [ - MonomialTermExpression((coef, var_map[vid])) - for vid, coef in self.linear.items() - if coef - ] - ) + var_map = visitor.var_map + with mutable_expression() as e: + for vid, coef in self.linear.items(): + if coef: + e += coef * var_map[vid] + if e.nargs() > 1: + ans += e + elif e.nargs() == 1: + ans += e.arg(0) if self.constant: ans += self.constant if self.multiplier != 1: diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 8700872f04f..8600a8a50f6 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -321,6 +321,16 @@ def generate_standard_repn( linear_vars[id_] = v elif arg.__class__ in native_numeric_types: C_ += arg + elif arg.is_variable_type(): + if arg.fixed: + C_ += arg.value + continue + id_ = id(arg) + if id_ in linear_coefs: + linear_coefs[id_] += 1 + else: + linear_coefs[id_] = 1 + linear_vars[id_] = arg else: C_ += EXPR.evaluate_expression(arg) else: # compute_values == False @@ -336,6 +346,18 @@ def generate_standard_repn( else: linear_coefs[id_] = c linear_vars[id_] = v + elif arg.__class__ in native_numeric_types: + C_ += arg + elif arg.is_variable_type(): + if arg.fixed: + C_ += arg + continue + id_ = id(arg) + if id_ in linear_coefs: + linear_coefs[id_] += 1 + else: + linear_coefs[id_] = 1 + linear_vars[id_] = arg else: C_ += arg diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index 6843650d0c2..0fd428fd8ee 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -1589,7 +1589,7 @@ def test_to_expression(self): expr.constant = 0 expr.linear[id(m.x)] = 0 expr.linear[id(m.y)] = 0 - assertExpressionsEqual(self, expr.to_expression(visitor), LinearExpression()) + assertExpressionsEqual(self, expr.to_expression(visitor), 0) @unittest.skipUnless(numpy_available, "Test requires numpy") def test_nonnumeric(self): From 0785422bb3f97359a48128434ddbeb4e99f6c5b1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 09:34:52 -0700 Subject: [PATCH 0898/3044] NFC: apply black --- pyomo/repn/plugins/gams_writer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/gams_writer.py b/pyomo/repn/plugins/gams_writer.py index 0756cb64920..a0f407d7952 100644 --- a/pyomo/repn/plugins/gams_writer.py +++ b/pyomo/repn/plugins/gams_writer.py @@ -188,7 +188,8 @@ def _linear_to_string(self, node): if arg.__class__ in native_numeric_types else ( self.smap.getSymbol(arg) - if arg.is_variable_type() and (not arg.fixed or self.output_fixed_variables) + if arg.is_variable_type() + and (not arg.fixed or self.output_fixed_variables) else ftoa(value(arg), True) ) ) From 1507ffd2a6c32ca4d5352488997dddab251d282d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 12:58:56 -0700 Subject: [PATCH 0899/3044] Update tests to track change in LinearExpression arg types --- pyomo/core/tests/transform/test_add_slacks.py | 56 +-- pyomo/core/tests/unit/test_compare.py | 6 - pyomo/core/tests/unit/test_expression.py | 11 +- pyomo/core/tests/unit/test_numeric_expr.py | 329 ++++-------------- .../core/tests/unit/test_numeric_expr_api.py | 11 +- .../unit/test_numeric_expr_dispatcher.py | 278 ++++++--------- .../unit/test_numeric_expr_zerofilter.py | 274 ++++++--------- pyomo/core/tests/unit/test_visitor.py | 23 +- pyomo/gdp/tests/common_tests.py | 7 +- pyomo/gdp/tests/test_bigm.py | 5 +- pyomo/gdp/tests/test_binary_multiplication.py | 5 +- pyomo/gdp/tests/test_disjunct.py | 24 +- 12 files changed, 302 insertions(+), 727 deletions(-) diff --git a/pyomo/core/tests/transform/test_add_slacks.py b/pyomo/core/tests/transform/test_add_slacks.py index 7896cab7e88..a74a9b75c4f 100644 --- a/pyomo/core/tests/transform/test_add_slacks.py +++ b/pyomo/core/tests/transform/test_add_slacks.py @@ -102,10 +102,7 @@ def checkRule1(self, m): self, cons.body, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.x)), - EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule1)), - ] + [m.x, EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule1))] ), ) @@ -118,14 +115,7 @@ def checkRule3(self, m): self.assertEqual(cons.lower, 0.1) assertExpressionsEqual( - self, - cons.body, - EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.x)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule3)), - ] - ), + self, cons.body, EXPR.LinearExpression([m.x, transBlock._slack_plus_rule3]) ) def test_ub_constraint_modified(self): @@ -154,8 +144,8 @@ def test_both_bounds_constraint_modified(self): cons.body, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression((1, m.y)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule2)), + m.y, + transBlock._slack_plus_rule2, EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule2)), ] ), @@ -184,10 +174,10 @@ def test_new_obj_created(self): obj.expr, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression((1, transBlock._slack_minus_rule1)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule2)), - EXPR.MonomialTermExpression((1, transBlock._slack_minus_rule2)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule3)), + transBlock._slack_minus_rule1, + transBlock._slack_plus_rule2, + transBlock._slack_minus_rule2, + transBlock._slack_plus_rule3, ] ), ) @@ -302,10 +292,7 @@ def checkTargetsObj(self, m): self, obj.expr, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, transBlock._slack_minus_rule1)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule3)), - ] + [transBlock._slack_minus_rule1, transBlock._slack_plus_rule3] ), ) @@ -423,9 +410,9 @@ def test_transformed_constraints_sumexpression_body(self): c.body, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression((1, m.x)), + m.x, EXPR.MonomialTermExpression((-2, m.y)), - EXPR.MonomialTermExpression((1, transBlock._slack_plus_rule4)), + transBlock._slack_plus_rule4, EXPR.MonomialTermExpression((-1, transBlock._slack_minus_rule4)), ] ), @@ -518,15 +505,9 @@ def checkTargetObj(self, m): obj.expr, EXPR.LinearExpression( [ - EXPR.MonomialTermExpression( - (1, transBlock.component("_slack_plus_rule1[1]")) - ), - EXPR.MonomialTermExpression( - (1, transBlock.component("_slack_plus_rule1[2]")) - ), - EXPR.MonomialTermExpression( - (1, transBlock.component("_slack_plus_rule1[3]")) - ), + transBlock.component("_slack_plus_rule1[1]"), + transBlock.component("_slack_plus_rule1[2]"), + transBlock.component("_slack_plus_rule1[3]"), ] ), ) @@ -558,14 +539,7 @@ def checkTransformedRule1(self, m, i): EXPR.LinearExpression( [ EXPR.MonomialTermExpression((2, m.x[i])), - EXPR.MonomialTermExpression( - ( - 1, - m._core_add_slack_variables.component( - "_slack_plus_rule1[%s]" % i - ), - ) - ), + m._core_add_slack_variables.component("_slack_plus_rule1[%s]" % i), ] ), ) diff --git a/pyomo/core/tests/unit/test_compare.py b/pyomo/core/tests/unit/test_compare.py index f80753bdb61..7c3536bc084 100644 --- a/pyomo/core/tests/unit/test_compare.py +++ b/pyomo/core/tests/unit/test_compare.py @@ -165,17 +165,11 @@ def test_expr_if(self): 0, (EqualityExpression, 2), (LinearExpression, 2), - (MonomialTermExpression, 2), - 1, m.y, - (MonomialTermExpression, 2), - 1, m.x, 0, (EqualityExpression, 2), (LinearExpression, 2), - (MonomialTermExpression, 2), - 1, m.y, (MonomialTermExpression, 2), -1, diff --git a/pyomo/core/tests/unit/test_expression.py b/pyomo/core/tests/unit/test_expression.py index c9afc6a1f76..678df4c01a8 100644 --- a/pyomo/core/tests/unit/test_expression.py +++ b/pyomo/core/tests/unit/test_expression.py @@ -738,10 +738,10 @@ def test_pprint_oldStyle(self): expr = model.e * model.x**2 + model.E[1] output = """\ -sum(prod(e{sum(mon(1, x), 2)}, pow(x, 2)), E[1]{sum(pow(x, 2), 1)}) +sum(prod(e{sum(x, 2)}, pow(x, 2)), E[1]{sum(pow(x, 2), 1)}) e : Size=1, Index=None Key : Expression - None : sum(mon(1, x), 2) + None : sum(x, 2) E : Size=2, Index={1, 2} Key : Expression 1 : sum(pow(x, 2), 1) @@ -951,12 +951,7 @@ def test_isub(self): assertExpressionsEqual( self, m.e.expr, - EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.x)), - EXPR.MonomialTermExpression((-1, m.y)), - ] - ), + EXPR.LinearExpression([m.x, EXPR.MonomialTermExpression((-1, m.y))]), ) self.assertTrue(compare_expressions(m.e.expr, m.x - m.y)) diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index c1066c292d7..968b3acb6a4 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -638,12 +638,7 @@ def test_simpleSum(self): m.b = Var() e = m.a + m.b # - self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b))] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b])) self.assertRaises(KeyError, e.arg, 3) @@ -654,14 +649,7 @@ def test_simpleSum_API(self): e = m.a + m.b e += 2 * m.a self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((2, m.a)), - ] - ), + e, LinearExpression([m.a, m.b, MonomialTermExpression((2, m.a))]) ) def test_constSum(self): @@ -669,13 +657,9 @@ def test_constSum(self): m = AbstractModel() m.a = Var() # - self.assertExpressionsEqual( - m.a + 5, LinearExpression([MonomialTermExpression((1, m.a)), 5]) - ) + self.assertExpressionsEqual(m.a + 5, LinearExpression([m.a, 5])) - self.assertExpressionsEqual( - 5 + m.a, LinearExpression([5, MonomialTermExpression((1, m.a))]) - ) + self.assertExpressionsEqual(5 + m.a, LinearExpression([5, m.a])) def test_nestedSum(self): # @@ -696,12 +680,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = e1 + 5 - self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b)), 5] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, 5])) # + # / \ @@ -710,12 +689,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = 5 + e1 - self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b)), 5] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, 5])) # + # / \ @@ -724,16 +698,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = e1 + m.c - self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - ] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, m.c])) # + # / \ @@ -742,16 +707,7 @@ def test_nestedSum(self): # a b e1 = m.a + m.b e = m.c + e1 - self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - ] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, m.c])) # + # / \ @@ -762,17 +718,7 @@ def test_nestedSum(self): e2 = m.c + m.d e = e1 + e2 # - self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - MonomialTermExpression((1, m.d)), - ] - ), - ) + self.assertExpressionsEqual(e, LinearExpression([m.a, m.b, m.c, m.d])) def test_nestedSum2(self): # @@ -798,22 +744,7 @@ def test_nestedSum2(self): self.assertExpressionsEqual( e, - SumExpression( - [ - ProductExpression( - ( - 2, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - ] - ), - ) - ), - m.c, - ] - ), + SumExpression([ProductExpression((2, LinearExpression([m.a, m.b]))), m.c]), ) # * @@ -834,20 +765,7 @@ def test_nestedSum2(self): ( 3, SumExpression( - [ - ProductExpression( - ( - 2, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - ] - ), - ) - ), - m.c, - ] + [ProductExpression((2, LinearExpression([m.a, m.b]))), m.c] ), ) ), @@ -891,10 +809,7 @@ def test_sumOf_nestedTrivialProduct(self): e = e1 + m.b # self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((5, m.a)), MonomialTermExpression((1, m.b))] - ), + e, LinearExpression([MonomialTermExpression((5, m.a)), m.b]) ) # + @@ -905,10 +820,7 @@ def test_sumOf_nestedTrivialProduct(self): e = m.b + e1 # self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.b)), MonomialTermExpression((5, m.a))] - ), + e, LinearExpression([m.b, MonomialTermExpression((5, m.a))]) ) # + @@ -920,14 +832,7 @@ def test_sumOf_nestedTrivialProduct(self): e = e1 + e2 # self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - MonomialTermExpression((5, m.a)), - ] - ), + e, LinearExpression([m.b, m.c, MonomialTermExpression((5, m.a))]) ) # + @@ -939,14 +844,7 @@ def test_sumOf_nestedTrivialProduct(self): e = e2 + e1 # self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - MonomialTermExpression((5, m.a)), - ] - ), + e, LinearExpression([m.b, m.c, MonomialTermExpression((5, m.a))]) ) def test_simpleDiff(self): @@ -962,10 +860,7 @@ def test_simpleDiff(self): # a b e = m.a - m.b self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((-1, m.b))] - ), + e, LinearExpression([m.a, MonomialTermExpression((-1, m.b))]) ) def test_constDiff(self): @@ -978,9 +873,7 @@ def test_constDiff(self): # - # / \ # a 5 - self.assertExpressionsEqual( - m.a - 5, LinearExpression([MonomialTermExpression((1, m.a)), -5]) - ) + self.assertExpressionsEqual(m.a - 5, LinearExpression([m.a, -5])) # - # / \ @@ -1002,10 +895,7 @@ def test_paramDiff(self): # a p e = m.a - m.p self.assertExpressionsEqual( - e, - LinearExpression( - [MonomialTermExpression((1, m.a)), NPV_NegationExpression((m.p,))] - ), + e, LinearExpression([m.a, NPV_NegationExpression((m.p,))]) ) # - @@ -1079,14 +969,7 @@ def test_nestedDiff(self): e1 = m.a - m.b e = e1 - 5 self.assertExpressionsEqual( - e, - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - -5, - ] - ), + e, LinearExpression([m.a, MonomialTermExpression((-1, m.b)), -5]) ) # - @@ -1102,14 +985,7 @@ def test_nestedDiff(self): [ 5, NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - ] - ), - ) + (LinearExpression([m.a, MonomialTermExpression((-1, m.b))]),) ), ] ), @@ -1126,7 +1002,7 @@ def test_nestedDiff(self): e, LinearExpression( [ - MonomialTermExpression((1, m.a)), + m.a, MonomialTermExpression((-1, m.b)), MonomialTermExpression((-1, m.c)), ] @@ -1146,14 +1022,7 @@ def test_nestedDiff(self): [ m.c, NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - ] - ), - ) + (LinearExpression([m.a, MonomialTermExpression((-1, m.b))]),) ), ] ), @@ -1171,21 +1040,9 @@ def test_nestedDiff(self): e, SumExpression( [ - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((-1, m.b)), - ] - ), + LinearExpression([m.a, MonomialTermExpression((-1, m.b))]), NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.c)), - MonomialTermExpression((-1, m.d)), - ] - ), - ) + (LinearExpression([m.c, MonomialTermExpression((-1, m.d))]),) ), ] ), @@ -1382,10 +1239,7 @@ def test_sumOf_nestedTrivialProduct2(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((NPV_NegationExpression((m.p,)), m.a)), - ] + [m.b, MonomialTermExpression((NPV_NegationExpression((m.p,)), m.a))] ), ) @@ -1403,14 +1257,7 @@ def test_sumOf_nestedTrivialProduct2(self): [ MonomialTermExpression((m.p, m.a)), NegationExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.b)), - MonomialTermExpression((-1, m.c)), - ] - ), - ) + (LinearExpression([m.b, MonomialTermExpression((-1, m.c))]),) ), ] ), @@ -1428,7 +1275,7 @@ def test_sumOf_nestedTrivialProduct2(self): e, LinearExpression( [ - MonomialTermExpression((1, m.b)), + m.b, MonomialTermExpression((-1, m.c)), MonomialTermExpression((NPV_NegationExpression((m.p,)), m.a)), ] @@ -1598,22 +1445,7 @@ def test_nestedProduct2(self): self.assertExpressionsEqual( e, ProductExpression( - ( - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.c)), - ] - ), - LinearExpression( - [ - MonomialTermExpression((1, m.a)), - MonomialTermExpression((1, m.b)), - MonomialTermExpression((1, m.d)), - ] - ), - ) + (LinearExpression([m.a, m.b, m.c]), LinearExpression([m.a, m.b, m.d])) ), ) # Verify shared args... @@ -1638,9 +1470,7 @@ def test_nestedProduct2(self): e3 = e1 * m.d e = e2 * e3 # - inner = LinearExpression( - [MonomialTermExpression((1, m.a)), MonomialTermExpression((1, m.b))] - ) + inner = LinearExpression([m.a, m.b]) self.assertExpressionsEqual( e, ProductExpression( @@ -2034,10 +1864,10 @@ def test_sum(self): model.p = Param(mutable=True) expr = 5 + model.a + model.a - self.assertEqual("sum(5, mon(1, a), mon(1, a))", str(expr)) + self.assertEqual("sum(5, a, a)", str(expr)) expr += 5 - self.assertEqual("sum(5, mon(1, a), mon(1, a), 5)", str(expr)) + self.assertEqual("sum(5, a, a, 5)", str(expr)) expr = 2 + model.p self.assertEqual("sum(2, p)", str(expr)) @@ -2053,24 +1883,18 @@ def test_linearsum(self): expr = quicksum(i * model.a[i] for i in A) self.assertEqual( - "sum(mon(0, a[0]), mon(1, a[1]), mon(2, a[2]), mon(3, a[3]), " - "mon(4, a[4]))", + "sum(mon(0, a[0]), a[1], mon(2, a[2]), mon(3, a[3]), " "mon(4, a[4]))", str(expr), ) expr = quicksum((i - 2) * model.a[i] for i in A) self.assertEqual( - "sum(mon(-2, a[0]), mon(-1, a[1]), mon(0, a[2]), mon(1, a[3]), " - "mon(2, a[4]))", + "sum(mon(-2, a[0]), mon(-1, a[1]), mon(0, a[2]), a[3], " "mon(2, a[4]))", str(expr), ) expr = quicksum(model.a[i] for i in A) - self.assertEqual( - "sum(mon(1, a[0]), mon(1, a[1]), mon(1, a[2]), mon(1, a[3]), " - "mon(1, a[4]))", - str(expr), - ) + self.assertEqual("sum(a[0], a[1], a[2], a[3], a[4])", str(expr)) model.p[1].value = 0 model.p[3].value = 3 @@ -2138,10 +1962,10 @@ def test_inequality(self): self.assertEqual("5 <= a < 10", str(expr)) expr = 5 <= model.a + 5 - self.assertEqual("5 <= sum(mon(1, a), 5)", str(expr)) + self.assertEqual("5 <= sum(a, 5)", str(expr)) expr = expr < 10 - self.assertEqual("5 <= sum(mon(1, a), 5) < 10", str(expr)) + self.assertEqual("5 <= sum(a, 5) < 10", str(expr)) def test_equality(self): # @@ -2166,10 +1990,10 @@ def test_equality(self): self.assertEqual("a == 10", str(expr)) expr = 5 == model.a + 5 - self.assertEqual("sum(mon(1, a), 5) == 5", str(expr)) + self.assertEqual("sum(a, 5) == 5", str(expr)) expr = model.a + 5 == 5 - self.assertEqual("sum(mon(1, a), 5) == 5", str(expr)) + self.assertEqual("sum(a, 5) == 5", str(expr)) def test_getitem(self): m = ConcreteModel() @@ -2206,7 +2030,7 @@ def test_small_expression(self): expr = abs(expr) self.assertEqual( "abs(neg(pow(2, div(2, prod(2, sum(1, neg(pow(div(prod(sum(" - "mon(1, a), 1, -1), a), a), b)), 1))))))", + "a, 1, -1), a), a), b)), 1))))))", str(expr), ) @@ -3754,13 +3578,7 @@ def test_summation1(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - ] + [self.m.a[1], self.m.a[2], self.m.a[3], self.m.a[4], self.m.a[5]] ), ) @@ -3872,16 +3690,16 @@ def test_summation_compression(self): e, LinearExpression( [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - MonomialTermExpression((1, self.m.b[1])), - MonomialTermExpression((1, self.m.b[2])), - MonomialTermExpression((1, self.m.b[3])), - MonomialTermExpression((1, self.m.b[4])), - MonomialTermExpression((1, self.m.b[5])), + self.m.a[1], + self.m.a[2], + self.m.a[3], + self.m.a[4], + self.m.a[5], + self.m.b[1], + self.m.b[2], + self.m.b[3], + self.m.b[4], + self.m.b[5], ] ), ) @@ -3912,13 +3730,7 @@ def test_deprecation(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - ] + [self.m.a[1], self.m.a[2], self.m.a[3], self.m.a[4], self.m.a[5]] ), ) @@ -3928,13 +3740,7 @@ def test_summation1(self): self.assertExpressionsEqual( e, LinearExpression( - [ - MonomialTermExpression((1, self.m.a[1])), - MonomialTermExpression((1, self.m.a[2])), - MonomialTermExpression((1, self.m.a[3])), - MonomialTermExpression((1, self.m.a[4])), - MonomialTermExpression((1, self.m.a[5])), - ] + [self.m.a[1], self.m.a[2], self.m.a[3], self.m.a[4], self.m.a[5]] ), ) @@ -4156,15 +3962,15 @@ def test_SumExpression(self): self.assertEqual(expr2(), 15) self.assertNotEqual(id(expr1), id(expr2)) self.assertNotEqual(id(expr1._args_), id(expr2._args_)) - self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) - self.assertIs(expr1.arg(1).arg(1), expr2.arg(1).arg(1)) + self.assertIs(expr1.arg(0), expr2.arg(0)) + self.assertIs(expr1.arg(1), expr2.arg(1)) expr1 += self.m.b self.assertEqual(expr1(), 25) self.assertEqual(expr2(), 15) self.assertNotEqual(id(expr1), id(expr2)) self.assertNotEqual(id(expr1._args_), id(expr2._args_)) - self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) - self.assertIs(expr1.arg(1).arg(1), expr2.arg(1).arg(1)) + self.assertIs(expr1.arg(0), expr2.arg(0)) + self.assertIs(expr1.arg(1), expr2.arg(1)) # total = counter.count - start self.assertEqual(total, 1) @@ -4341,9 +4147,9 @@ def test_productOfExpressions(self): self.assertEqual(expr1.arg(1).nargs(), 2) self.assertEqual(expr2.arg(1).nargs(), 2) - self.assertIs(expr1.arg(0).arg(0).arg(1), expr2.arg(0).arg(0).arg(1)) - self.assertIs(expr1.arg(0).arg(1).arg(1), expr2.arg(0).arg(1).arg(1)) - self.assertIs(expr1.arg(1).arg(0).arg(1), expr2.arg(1).arg(0).arg(1)) + self.assertIs(expr1.arg(0).arg(0), expr2.arg(0).arg(0)) + self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) + self.assertIs(expr1.arg(1).arg(0), expr2.arg(1).arg(0)) expr1 *= self.m.b self.assertEqual(expr1(), 1500) @@ -4382,8 +4188,8 @@ def test_productOfExpressions_div(self): self.assertEqual(expr1.arg(1).nargs(), 2) self.assertEqual(expr2.arg(1).nargs(), 2) - self.assertIs(expr1.arg(0).arg(0).arg(1), expr2.arg(0).arg(0).arg(1)) - self.assertIs(expr1.arg(0).arg(1).arg(1), expr2.arg(0).arg(1).arg(1)) + self.assertIs(expr1.arg(0).arg(0), expr2.arg(0).arg(0)) + self.assertIs(expr1.arg(0).arg(1), expr2.arg(0).arg(1)) expr1 /= self.m.b self.assertAlmostEqual(expr1(), 0.15) @@ -5214,18 +5020,7 @@ def test_pow_other(self): e += m.v[0] + m.v[1] e = m.v[0] ** e self.assertExpressionsEqual( - e, - PowExpression( - ( - m.v[0], - LinearExpression( - [ - MonomialTermExpression((1, m.v[0])), - MonomialTermExpression((1, m.v[1])), - ] - ), - ) - ), + e, PowExpression((m.v[0], LinearExpression([m.v[0], m.v[1]]))) ) diff --git a/pyomo/core/tests/unit/test_numeric_expr_api.py b/pyomo/core/tests/unit/test_numeric_expr_api.py index 4e0af126315..923f78af1be 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_api.py +++ b/pyomo/core/tests/unit/test_numeric_expr_api.py @@ -223,7 +223,7 @@ def test_negation(self): self.assertEqual(is_fixed(e), False) self.assertEqual(value(e), -15) self.assertEqual(str(e), "- (x + 2*x)") - self.assertEqual(e.to_string(verbose=True), "neg(sum(mon(1, x), mon(2, x)))") + self.assertEqual(e.to_string(verbose=True), "neg(sum(x, mon(2, x)))") # This can't occur through operator overloading, but could # through expression substitution @@ -634,8 +634,7 @@ def test_linear(self): self.assertEqual(value(e), 1 + 4 + 5 + 2) self.assertEqual(str(e), "0*x[0] + x[1] + 2*x[2] + 5 + y - 3") self.assertEqual( - e.to_string(verbose=True), - "sum(mon(0, x[0]), mon(1, x[1]), mon(2, x[2]), 5, mon(1, y), -3)", + e.to_string(verbose=True), "sum(mon(0, x[0]), x[1], mon(2, x[2]), 5, y, -3)" ) self.assertIs(type(e), LinearExpression) @@ -701,7 +700,7 @@ def test_expr_if(self): ) self.assertEqual( e.to_string(verbose=True), - "Expr_if( ( 5 <= y ), then=( sum(mon(1, x[0]), 5) ), else=( pow(x[1], 2) ) )", + "Expr_if( ( 5 <= y ), then=( sum(x[0], 5) ), else=( pow(x[1], 2) ) )", ) m.y.fix() @@ -972,9 +971,7 @@ def test_sum(self): f = e.create_node_with_local_data((m.p, m.x)) self.assertIsNot(f, e) self.assertIs(type(f), LinearExpression) - assertExpressionsStructurallyEqual( - self, f.args, [m.p, MonomialTermExpression((1, m.x))] - ) + assertExpressionsStructurallyEqual(self, f.args, [m.p, m.x]) f = e.create_node_with_local_data((m.p, m.x**2)) self.assertIsNot(f, e) diff --git a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py index 3787f00de47..37833d7e8a4 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py +++ b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py @@ -123,8 +123,6 @@ def setUp(self): self.mutable_l3 = _MutableNPVSumExpression([self.npv]) # often repeated reference expressions - self.mon_bin = MonomialTermExpression((1, self.bin)) - self.mon_var = MonomialTermExpression((1, self.var)) self.minus_bin = MonomialTermExpression((-1, self.bin)) self.minus_npv = NPV_NegationExpression((self.npv,)) self.minus_param_mut = NPV_NegationExpression((self.param_mut,)) @@ -368,38 +366,34 @@ def test_add_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.one, LinearExpression([self.bin, 1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, 5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, 6])), + (self.asbinary, self.native, LinearExpression([self.bin, 5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.npv])), + (self.asbinary, self.param, LinearExpression([self.bin, 6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.param_mut]), + LinearExpression([self.bin, self.param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.mon_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.mon_native]), + LinearExpression([self.bin, self.mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.mon_param]), - ), - ( - self.asbinary, - self.mon_npv, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_param]), ), + (self.asbinary, self.mon_npv, LinearExpression([self.bin, self.mon_npv])), # 12: ( self.asbinary, self.linear, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.asbinary, self.sum, SumExpression(self.sum.args + [self.bin])), (self.asbinary, self.other, SumExpression([self.bin, self.other])), @@ -408,7 +402,7 @@ def test_add_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_npv]), ), ( self.asbinary, @@ -416,13 +410,9 @@ def test_add_asbinary(self): SumExpression(self.mutable_l2.args + [self.bin]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.param1, LinearExpression([self.bin, 1])), # 20: - ( - self.asbinary, - self.mutable_l3, - LinearExpression([self.mon_bin, self.npv]), - ), + (self.asbinary, self.mutable_l3, LinearExpression([self.bin, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -462,7 +452,7 @@ def test_add_zero(self): def test_add_one(self): tests = [ (self.one, self.invalid, NotImplemented), - (self.one, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.one, self.asbinary, LinearExpression([1, self.bin])), (self.one, self.zero, 1), (self.one, self.one, 2), # 4: @@ -471,7 +461,7 @@ def test_add_one(self): (self.one, self.param, 7), (self.one, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.one, self.var, LinearExpression([1, self.mon_var])), + (self.one, self.var, LinearExpression([1, self.var])), (self.one, self.mon_native, LinearExpression([1, self.mon_native])), (self.one, self.mon_param, LinearExpression([1, self.mon_param])), (self.one, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -494,7 +484,7 @@ def test_add_one(self): def test_add_native(self): tests = [ (self.native, self.invalid, NotImplemented), - (self.native, self.asbinary, LinearExpression([5, self.mon_bin])), + (self.native, self.asbinary, LinearExpression([5, self.bin])), (self.native, self.zero, 5), (self.native, self.one, 6), # 4: @@ -503,7 +493,7 @@ def test_add_native(self): (self.native, self.param, 11), (self.native, self.param_mut, NPV_SumExpression([5, self.param_mut])), # 8: - (self.native, self.var, LinearExpression([5, self.mon_var])), + (self.native, self.var, LinearExpression([5, self.var])), (self.native, self.mon_native, LinearExpression([5, self.mon_native])), (self.native, self.mon_param, LinearExpression([5, self.mon_param])), (self.native, self.mon_npv, LinearExpression([5, self.mon_npv])), @@ -530,7 +520,7 @@ def test_add_native(self): def test_add_npv(self): tests = [ (self.npv, self.invalid, NotImplemented), - (self.npv, self.asbinary, LinearExpression([self.npv, self.mon_bin])), + (self.npv, self.asbinary, LinearExpression([self.npv, self.bin])), (self.npv, self.zero, self.npv), (self.npv, self.one, NPV_SumExpression([self.npv, 1])), # 4: @@ -539,7 +529,7 @@ def test_add_npv(self): (self.npv, self.param, NPV_SumExpression([self.npv, 6])), (self.npv, self.param_mut, NPV_SumExpression([self.npv, self.param_mut])), # 8: - (self.npv, self.var, LinearExpression([self.npv, self.mon_var])), + (self.npv, self.var, LinearExpression([self.npv, self.var])), (self.npv, self.mon_native, LinearExpression([self.npv, self.mon_native])), (self.npv, self.mon_param, LinearExpression([self.npv, self.mon_param])), (self.npv, self.mon_npv, LinearExpression([self.npv, self.mon_npv])), @@ -570,7 +560,7 @@ def test_add_npv(self): def test_add_param(self): tests = [ (self.param, self.invalid, NotImplemented), - (self.param, self.asbinary, LinearExpression([6, self.mon_bin])), + (self.param, self.asbinary, LinearExpression([6, self.bin])), (self.param, self.zero, 6), (self.param, self.one, 7), # 4: @@ -579,7 +569,7 @@ def test_add_param(self): (self.param, self.param, 12), (self.param, self.param_mut, NPV_SumExpression([6, self.param_mut])), # 8: - (self.param, self.var, LinearExpression([6, self.mon_var])), + (self.param, self.var, LinearExpression([6, self.var])), (self.param, self.mon_native, LinearExpression([6, self.mon_native])), (self.param, self.mon_param, LinearExpression([6, self.mon_param])), (self.param, self.mon_npv, LinearExpression([6, self.mon_npv])), @@ -605,7 +595,7 @@ def test_add_param_mut(self): ( self.param_mut, self.asbinary, - LinearExpression([self.param_mut, self.mon_bin]), + LinearExpression([self.param_mut, self.bin]), ), (self.param_mut, self.zero, self.param_mut), (self.param_mut, self.one, NPV_SumExpression([self.param_mut, 1])), @@ -619,11 +609,7 @@ def test_add_param_mut(self): NPV_SumExpression([self.param_mut, self.param_mut]), ), # 8: - ( - self.param_mut, - self.var, - LinearExpression([self.param_mut, self.mon_var]), - ), + (self.param_mut, self.var, LinearExpression([self.param_mut, self.var])), ( self.param_mut, self.mon_native, @@ -674,37 +660,21 @@ def test_add_param_mut(self): def test_add_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.mon_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, 1])), + (self.var, self.one, LinearExpression([self.var, 1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, 5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.npv])), - (self.var, self.param, LinearExpression([self.mon_var, 6])), - ( - self.var, - self.param_mut, - LinearExpression([self.mon_var, self.param_mut]), - ), + (self.var, self.native, LinearExpression([self.var, 5])), + (self.var, self.npv, LinearExpression([self.var, self.npv])), + (self.var, self.param, LinearExpression([self.var, 6])), + (self.var, self.param_mut, LinearExpression([self.var, self.param_mut])), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.mon_var])), - ( - self.var, - self.mon_native, - LinearExpression([self.mon_var, self.mon_native]), - ), - ( - self.var, - self.mon_param, - LinearExpression([self.mon_var, self.mon_param]), - ), - (self.var, self.mon_npv, LinearExpression([self.mon_var, self.mon_npv])), + (self.var, self.var, LinearExpression([self.var, self.var])), + (self.var, self.mon_native, LinearExpression([self.var, self.mon_native])), + (self.var, self.mon_param, LinearExpression([self.var, self.mon_param])), + (self.var, self.mon_npv, LinearExpression([self.var, self.mon_npv])), # 12: - ( - self.var, - self.linear, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.var, self.linear, LinearExpression(self.linear.args + [self.var])), (self.var, self.sum, SumExpression(self.sum.args + [self.var])), (self.var, self.other, SumExpression([self.var, self.other])), (self.var, self.mutable_l0, self.var), @@ -712,7 +682,7 @@ def test_add_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var] + self.mutable_l1.args), + LinearExpression([self.var] + self.mutable_l1.args), ), ( self.var, @@ -720,13 +690,9 @@ def test_add_var(self): SumExpression(self.mutable_l2.args + [self.var]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, 1])), + (self.var, self.param1, LinearExpression([self.var, 1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([MonomialTermExpression((1, self.var)), self.npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -737,7 +703,7 @@ def test_add_mon_native(self): ( self.mon_native, self.asbinary, - LinearExpression([self.mon_native, self.mon_bin]), + LinearExpression([self.mon_native, self.bin]), ), (self.mon_native, self.zero, self.mon_native), (self.mon_native, self.one, LinearExpression([self.mon_native, 1])), @@ -751,11 +717,7 @@ def test_add_mon_native(self): LinearExpression([self.mon_native, self.param_mut]), ), # 8: - ( - self.mon_native, - self.var, - LinearExpression([self.mon_native, self.mon_var]), - ), + (self.mon_native, self.var, LinearExpression([self.mon_native, self.var])), ( self.mon_native, self.mon_native, @@ -813,7 +775,7 @@ def test_add_mon_param(self): ( self.mon_param, self.asbinary, - LinearExpression([self.mon_param, self.mon_bin]), + LinearExpression([self.mon_param, self.bin]), ), (self.mon_param, self.zero, self.mon_param), (self.mon_param, self.one, LinearExpression([self.mon_param, 1])), @@ -827,11 +789,7 @@ def test_add_mon_param(self): LinearExpression([self.mon_param, self.param_mut]), ), # 8: - ( - self.mon_param, - self.var, - LinearExpression([self.mon_param, self.mon_var]), - ), + (self.mon_param, self.var, LinearExpression([self.mon_param, self.var])), ( self.mon_param, self.mon_native, @@ -882,11 +840,7 @@ def test_add_mon_param(self): def test_add_mon_npv(self): tests = [ (self.mon_npv, self.invalid, NotImplemented), - ( - self.mon_npv, - self.asbinary, - LinearExpression([self.mon_npv, self.mon_bin]), - ), + (self.mon_npv, self.asbinary, LinearExpression([self.mon_npv, self.bin])), (self.mon_npv, self.zero, self.mon_npv), (self.mon_npv, self.one, LinearExpression([self.mon_npv, 1])), # 4: @@ -899,7 +853,7 @@ def test_add_mon_npv(self): LinearExpression([self.mon_npv, self.param_mut]), ), # 8: - (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.mon_var])), + (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.var])), ( self.mon_npv, self.mon_native, @@ -949,7 +903,7 @@ def test_add_linear(self): ( self.linear, self.asbinary, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.linear, self.zero, self.linear), (self.linear, self.one, LinearExpression(self.linear.args + [1])), @@ -963,11 +917,7 @@ def test_add_linear(self): LinearExpression(self.linear.args + [self.param_mut]), ), # 8: - ( - self.linear, - self.var, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.linear, self.var, LinearExpression(self.linear.args + [self.var])), ( self.linear, self.mon_native, @@ -1134,7 +1084,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.asbinary, - LinearExpression(self.mutable_l1.args + [self.mon_bin]), + LinearExpression(self.mutable_l1.args + [self.bin]), ), (self.mutable_l1, self.zero, self.mon_npv), (self.mutable_l1, self.one, LinearExpression(self.mutable_l1.args + [1])), @@ -1159,7 +1109,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.var, - LinearExpression(self.mutable_l1.args + [self.mon_var]), + LinearExpression(self.mutable_l1.args + [self.var]), ), ( self.mutable_l1, @@ -1341,7 +1291,7 @@ def test_add_param0(self): def test_add_param1(self): tests = [ (self.param1, self.invalid, NotImplemented), - (self.param1, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.param1, self.asbinary, LinearExpression([1, self.bin])), (self.param1, self.zero, 1), (self.param1, self.one, 2), # 4: @@ -1350,7 +1300,7 @@ def test_add_param1(self): (self.param1, self.param, 7), (self.param1, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.param1, self.var, LinearExpression([1, self.mon_var])), + (self.param1, self.var, LinearExpression([1, self.var])), (self.param1, self.mon_native, LinearExpression([1, self.mon_native])), (self.param1, self.mon_param, LinearExpression([1, self.mon_param])), (self.param1, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -1380,7 +1330,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.asbinary, - LinearExpression(self.mutable_l3.args + [self.mon_bin]), + LinearExpression(self.mutable_l3.args + [self.bin]), ), (self.mutable_l3, self.zero, self.npv), (self.mutable_l3, self.one, NPV_SumExpression(self.mutable_l3.args + [1])), @@ -1409,7 +1359,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.var, - LinearExpression(self.mutable_l3.args + [self.mon_var]), + LinearExpression(self.mutable_l3.args + [self.var]), ), ( self.mutable_l3, @@ -1515,32 +1465,32 @@ def test_sub_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.one, LinearExpression([self.bin, -1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, -5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.minus_npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, -6])), + (self.asbinary, self.native, LinearExpression([self.bin, -5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.minus_npv])), + (self.asbinary, self.param, LinearExpression([self.bin, -6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.minus_param_mut]), + LinearExpression([self.bin, self.minus_param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.minus_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.minus_var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.minus_mon_native]), + LinearExpression([self.bin, self.minus_mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.minus_mon_param]), + LinearExpression([self.bin, self.minus_mon_param]), ), ( self.asbinary, self.mon_npv, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), # 12: (self.asbinary, self.linear, SumExpression([self.bin, self.minus_linear])), @@ -1551,7 +1501,7 @@ def test_sub_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), ( self.asbinary, @@ -1559,12 +1509,12 @@ def test_sub_asbinary(self): SumExpression([self.bin, self.minus_mutable_l2]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.param1, LinearExpression([self.bin, -1])), # 20: ( self.asbinary, self.mutable_l3, - LinearExpression([self.mon_bin, self.minus_npv]), + LinearExpression([self.bin, self.minus_npv]), ), ] self._run_cases(tests, operator.sub) @@ -1837,35 +1787,31 @@ def test_sub_param_mut(self): def test_sub_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.minus_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.minus_bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, -1])), + (self.var, self.one, LinearExpression([self.var, -1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, -5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.minus_npv])), - (self.var, self.param, LinearExpression([self.mon_var, -6])), + (self.var, self.native, LinearExpression([self.var, -5])), + (self.var, self.npv, LinearExpression([self.var, self.minus_npv])), + (self.var, self.param, LinearExpression([self.var, -6])), ( self.var, self.param_mut, - LinearExpression([self.mon_var, self.minus_param_mut]), + LinearExpression([self.var, self.minus_param_mut]), ), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.minus_var])), + (self.var, self.var, LinearExpression([self.var, self.minus_var])), ( self.var, self.mon_native, - LinearExpression([self.mon_var, self.minus_mon_native]), + LinearExpression([self.var, self.minus_mon_native]), ), ( self.var, self.mon_param, - LinearExpression([self.mon_var, self.minus_mon_param]), - ), - ( - self.var, - self.mon_npv, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_param]), ), + (self.var, self.mon_npv, LinearExpression([self.var, self.minus_mon_npv])), # 12: ( self.var, @@ -1879,7 +1825,7 @@ def test_sub_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_npv]), ), ( self.var, @@ -1887,13 +1833,9 @@ def test_sub_var(self): SumExpression([self.var, self.minus_mutable_l2]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, -1])), + (self.var, self.param1, LinearExpression([self.var, -1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([self.mon_var, self.minus_npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.minus_npv])), ] self._run_cases(tests, operator.sub) self._run_cases(tests, operator.isub) @@ -6511,7 +6453,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([])), (mutable_npv, self.one, _MutableNPVSumExpression([1])), # 4: @@ -6520,7 +6462,7 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.param, _MutableNPVSumExpression([6])), (mutable_npv, self.param_mut, _MutableNPVSumExpression([self.param_mut])), # 8: - (mutable_npv, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([self.var])), (mutable_npv, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_npv, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_npv, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6546,7 +6488,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([10]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), (mutable_npv, self.one, _MutableNPVSumExpression([10, 1])), # 4: @@ -6559,7 +6501,7 @@ def test_mutable_nvp_iadd(self): _MutableNPVSumExpression([10, self.param_mut]), ), # 8: - (mutable_npv, self.var, _MutableLinearExpression([10, self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([10, self.var])), ( mutable_npv, self.mon_native, @@ -6602,7 +6544,7 @@ def test_mutable_lin_iadd(self): mutable_lin = _MutableLinearExpression([]) tests = [ (mutable_lin, self.invalid, NotImplemented), - (mutable_lin, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_lin, self.zero, _MutableLinearExpression([])), (mutable_lin, self.one, _MutableLinearExpression([1])), # 4: @@ -6611,7 +6553,7 @@ def test_mutable_lin_iadd(self): (mutable_lin, self.param, _MutableLinearExpression([6])), (mutable_lin, self.param_mut, _MutableLinearExpression([self.param_mut])), # 8: - (mutable_lin, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_lin, self.var, _MutableLinearExpression([self.var])), (mutable_lin, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_lin, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_lin, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6634,81 +6576,69 @@ def test_mutable_lin_iadd(self): ] self._run_iadd_cases(tests, operator.iadd) - mutable_lin = _MutableLinearExpression([self.mon_bin]) + mutable_lin = _MutableLinearExpression([self.bin]) tests = [ (mutable_lin, self.invalid, NotImplemented), ( mutable_lin, self.asbinary, - _MutableLinearExpression([self.mon_bin, self.mon_bin]), + _MutableLinearExpression([self.bin, self.bin]), ), - (mutable_lin, self.zero, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.one, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.zero, _MutableLinearExpression([self.bin])), + (mutable_lin, self.one, _MutableLinearExpression([self.bin, 1])), # 4: - (mutable_lin, self.native, _MutableLinearExpression([self.mon_bin, 5])), - (mutable_lin, self.npv, _MutableLinearExpression([self.mon_bin, self.npv])), - (mutable_lin, self.param, _MutableLinearExpression([self.mon_bin, 6])), + (mutable_lin, self.native, _MutableLinearExpression([self.bin, 5])), + (mutable_lin, self.npv, _MutableLinearExpression([self.bin, self.npv])), + (mutable_lin, self.param, _MutableLinearExpression([self.bin, 6])), ( mutable_lin, self.param_mut, - _MutableLinearExpression([self.mon_bin, self.param_mut]), + _MutableLinearExpression([self.bin, self.param_mut]), ), # 8: - ( - mutable_lin, - self.var, - _MutableLinearExpression([self.mon_bin, self.mon_var]), - ), + (mutable_lin, self.var, _MutableLinearExpression([self.bin, self.var])), ( mutable_lin, self.mon_native, - _MutableLinearExpression([self.mon_bin, self.mon_native]), + _MutableLinearExpression([self.bin, self.mon_native]), ), ( mutable_lin, self.mon_param, - _MutableLinearExpression([self.mon_bin, self.mon_param]), + _MutableLinearExpression([self.bin, self.mon_param]), ), ( mutable_lin, self.mon_npv, - _MutableLinearExpression([self.mon_bin, self.mon_npv]), + _MutableLinearExpression([self.bin, self.mon_npv]), ), # 12: ( mutable_lin, self.linear, - _MutableLinearExpression([self.mon_bin] + self.linear.args), - ), - ( - mutable_lin, - self.sum, - _MutableSumExpression([self.mon_bin] + self.sum.args), - ), - ( - mutable_lin, - self.other, - _MutableSumExpression([self.mon_bin, self.other]), + _MutableLinearExpression([self.bin] + self.linear.args), ), - (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.sum, _MutableSumExpression([self.bin] + self.sum.args)), + (mutable_lin, self.other, _MutableSumExpression([self.bin, self.other])), + (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.bin])), # 16: ( mutable_lin, self.mutable_l1, - _MutableLinearExpression([self.mon_bin] + self.mutable_l1.args), + _MutableLinearExpression([self.bin] + self.mutable_l1.args), ), ( mutable_lin, self.mutable_l2, - _MutableSumExpression([self.mon_bin] + self.mutable_l2.args), + _MutableSumExpression([self.bin] + self.mutable_l2.args), ), - (mutable_lin, self.param0, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.param1, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.param0, _MutableLinearExpression([self.bin])), + (mutable_lin, self.param1, _MutableLinearExpression([self.bin, 1])), # 20: ( mutable_lin, self.mutable_l3, - _MutableLinearExpression([self.mon_bin, self.npv]), + _MutableLinearExpression([self.bin, self.npv]), ), ] self._run_iadd_cases(tests, operator.iadd) @@ -6854,7 +6784,7 @@ def as_numeric(self): assertExpressionsEqual(self, PowExpression((self.var, 2)), e) e = obj + obj - assertExpressionsEqual(self, LinearExpression((self.mon_var, self.mon_var)), e) + assertExpressionsEqual(self, LinearExpression((self.var, self.var)), e) def test_categorize_arg_type(self): class CustomAsNumeric(NumericValue): diff --git a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py index 162d664e0f8..8e75ccc3feb 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py +++ b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py @@ -102,38 +102,34 @@ def test_add_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.one, LinearExpression([self.bin, 1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, 5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, 6])), + (self.asbinary, self.native, LinearExpression([self.bin, 5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.npv])), + (self.asbinary, self.param, LinearExpression([self.bin, 6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.param_mut]), + LinearExpression([self.bin, self.param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.mon_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.mon_native]), + LinearExpression([self.bin, self.mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.mon_param]), - ), - ( - self.asbinary, - self.mon_npv, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_param]), ), + (self.asbinary, self.mon_npv, LinearExpression([self.bin, self.mon_npv])), # 12: ( self.asbinary, self.linear, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.asbinary, self.sum, SumExpression(self.sum.args + [self.bin])), (self.asbinary, self.other, SumExpression([self.bin, self.other])), @@ -142,7 +138,7 @@ def test_add_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.mon_npv]), + LinearExpression([self.bin, self.mon_npv]), ), ( self.asbinary, @@ -150,13 +146,9 @@ def test_add_asbinary(self): SumExpression(self.mutable_l2.args + [self.bin]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, 1])), + (self.asbinary, self.param1, LinearExpression([self.bin, 1])), # 20: - ( - self.asbinary, - self.mutable_l3, - LinearExpression([self.mon_bin, self.npv]), - ), + (self.asbinary, self.mutable_l3, LinearExpression([self.bin, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -196,7 +188,7 @@ def test_add_zero(self): def test_add_one(self): tests = [ (self.one, self.invalid, NotImplemented), - (self.one, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.one, self.asbinary, LinearExpression([1, self.bin])), (self.one, self.zero, 1), (self.one, self.one, 2), # 4: @@ -205,7 +197,7 @@ def test_add_one(self): (self.one, self.param, 7), (self.one, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.one, self.var, LinearExpression([1, self.mon_var])), + (self.one, self.var, LinearExpression([1, self.var])), (self.one, self.mon_native, LinearExpression([1, self.mon_native])), (self.one, self.mon_param, LinearExpression([1, self.mon_param])), (self.one, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -228,7 +220,7 @@ def test_add_one(self): def test_add_native(self): tests = [ (self.native, self.invalid, NotImplemented), - (self.native, self.asbinary, LinearExpression([5, self.mon_bin])), + (self.native, self.asbinary, LinearExpression([5, self.bin])), (self.native, self.zero, 5), (self.native, self.one, 6), # 4: @@ -237,7 +229,7 @@ def test_add_native(self): (self.native, self.param, 11), (self.native, self.param_mut, NPV_SumExpression([5, self.param_mut])), # 8: - (self.native, self.var, LinearExpression([5, self.mon_var])), + (self.native, self.var, LinearExpression([5, self.var])), (self.native, self.mon_native, LinearExpression([5, self.mon_native])), (self.native, self.mon_param, LinearExpression([5, self.mon_param])), (self.native, self.mon_npv, LinearExpression([5, self.mon_npv])), @@ -264,7 +256,7 @@ def test_add_native(self): def test_add_npv(self): tests = [ (self.npv, self.invalid, NotImplemented), - (self.npv, self.asbinary, LinearExpression([self.npv, self.mon_bin])), + (self.npv, self.asbinary, LinearExpression([self.npv, self.bin])), (self.npv, self.zero, self.npv), (self.npv, self.one, NPV_SumExpression([self.npv, 1])), # 4: @@ -273,7 +265,7 @@ def test_add_npv(self): (self.npv, self.param, NPV_SumExpression([self.npv, 6])), (self.npv, self.param_mut, NPV_SumExpression([self.npv, self.param_mut])), # 8: - (self.npv, self.var, LinearExpression([self.npv, self.mon_var])), + (self.npv, self.var, LinearExpression([self.npv, self.var])), (self.npv, self.mon_native, LinearExpression([self.npv, self.mon_native])), (self.npv, self.mon_param, LinearExpression([self.npv, self.mon_param])), (self.npv, self.mon_npv, LinearExpression([self.npv, self.mon_npv])), @@ -304,7 +296,7 @@ def test_add_npv(self): def test_add_param(self): tests = [ (self.param, self.invalid, NotImplemented), - (self.param, self.asbinary, LinearExpression([6, self.mon_bin])), + (self.param, self.asbinary, LinearExpression([6, self.bin])), (self.param, self.zero, 6), (self.param, self.one, 7), # 4: @@ -313,7 +305,7 @@ def test_add_param(self): (self.param, self.param, 12), (self.param, self.param_mut, NPV_SumExpression([6, self.param_mut])), # 8: - (self.param, self.var, LinearExpression([6, self.mon_var])), + (self.param, self.var, LinearExpression([6, self.var])), (self.param, self.mon_native, LinearExpression([6, self.mon_native])), (self.param, self.mon_param, LinearExpression([6, self.mon_param])), (self.param, self.mon_npv, LinearExpression([6, self.mon_npv])), @@ -339,7 +331,7 @@ def test_add_param_mut(self): ( self.param_mut, self.asbinary, - LinearExpression([self.param_mut, self.mon_bin]), + LinearExpression([self.param_mut, self.bin]), ), (self.param_mut, self.zero, self.param_mut), (self.param_mut, self.one, NPV_SumExpression([self.param_mut, 1])), @@ -353,11 +345,7 @@ def test_add_param_mut(self): NPV_SumExpression([self.param_mut, self.param_mut]), ), # 8: - ( - self.param_mut, - self.var, - LinearExpression([self.param_mut, self.mon_var]), - ), + (self.param_mut, self.var, LinearExpression([self.param_mut, self.var])), ( self.param_mut, self.mon_native, @@ -408,37 +396,21 @@ def test_add_param_mut(self): def test_add_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.mon_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, 1])), + (self.var, self.one, LinearExpression([self.var, 1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, 5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.npv])), - (self.var, self.param, LinearExpression([self.mon_var, 6])), - ( - self.var, - self.param_mut, - LinearExpression([self.mon_var, self.param_mut]), - ), + (self.var, self.native, LinearExpression([self.var, 5])), + (self.var, self.npv, LinearExpression([self.var, self.npv])), + (self.var, self.param, LinearExpression([self.var, 6])), + (self.var, self.param_mut, LinearExpression([self.var, self.param_mut])), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.mon_var])), - ( - self.var, - self.mon_native, - LinearExpression([self.mon_var, self.mon_native]), - ), - ( - self.var, - self.mon_param, - LinearExpression([self.mon_var, self.mon_param]), - ), - (self.var, self.mon_npv, LinearExpression([self.mon_var, self.mon_npv])), + (self.var, self.var, LinearExpression([self.var, self.var])), + (self.var, self.mon_native, LinearExpression([self.var, self.mon_native])), + (self.var, self.mon_param, LinearExpression([self.var, self.mon_param])), + (self.var, self.mon_npv, LinearExpression([self.var, self.mon_npv])), # 12: - ( - self.var, - self.linear, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.var, self.linear, LinearExpression(self.linear.args + [self.var])), (self.var, self.sum, SumExpression(self.sum.args + [self.var])), (self.var, self.other, SumExpression([self.var, self.other])), (self.var, self.mutable_l0, self.var), @@ -446,7 +418,7 @@ def test_add_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var] + self.mutable_l1.args), + LinearExpression([self.var] + self.mutable_l1.args), ), ( self.var, @@ -454,13 +426,9 @@ def test_add_var(self): SumExpression(self.mutable_l2.args + [self.var]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, 1])), + (self.var, self.param1, LinearExpression([self.var, 1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([MonomialTermExpression((1, self.var)), self.npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.npv])), ] self._run_cases(tests, operator.add) self._run_cases(tests, operator.iadd) @@ -471,7 +439,7 @@ def test_add_mon_native(self): ( self.mon_native, self.asbinary, - LinearExpression([self.mon_native, self.mon_bin]), + LinearExpression([self.mon_native, self.bin]), ), (self.mon_native, self.zero, self.mon_native), (self.mon_native, self.one, LinearExpression([self.mon_native, 1])), @@ -485,11 +453,7 @@ def test_add_mon_native(self): LinearExpression([self.mon_native, self.param_mut]), ), # 8: - ( - self.mon_native, - self.var, - LinearExpression([self.mon_native, self.mon_var]), - ), + (self.mon_native, self.var, LinearExpression([self.mon_native, self.var])), ( self.mon_native, self.mon_native, @@ -547,7 +511,7 @@ def test_add_mon_param(self): ( self.mon_param, self.asbinary, - LinearExpression([self.mon_param, self.mon_bin]), + LinearExpression([self.mon_param, self.bin]), ), (self.mon_param, self.zero, self.mon_param), (self.mon_param, self.one, LinearExpression([self.mon_param, 1])), @@ -561,11 +525,7 @@ def test_add_mon_param(self): LinearExpression([self.mon_param, self.param_mut]), ), # 8: - ( - self.mon_param, - self.var, - LinearExpression([self.mon_param, self.mon_var]), - ), + (self.mon_param, self.var, LinearExpression([self.mon_param, self.var])), ( self.mon_param, self.mon_native, @@ -616,11 +576,7 @@ def test_add_mon_param(self): def test_add_mon_npv(self): tests = [ (self.mon_npv, self.invalid, NotImplemented), - ( - self.mon_npv, - self.asbinary, - LinearExpression([self.mon_npv, self.mon_bin]), - ), + (self.mon_npv, self.asbinary, LinearExpression([self.mon_npv, self.bin])), (self.mon_npv, self.zero, self.mon_npv), (self.mon_npv, self.one, LinearExpression([self.mon_npv, 1])), # 4: @@ -633,7 +589,7 @@ def test_add_mon_npv(self): LinearExpression([self.mon_npv, self.param_mut]), ), # 8: - (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.mon_var])), + (self.mon_npv, self.var, LinearExpression([self.mon_npv, self.var])), ( self.mon_npv, self.mon_native, @@ -683,7 +639,7 @@ def test_add_linear(self): ( self.linear, self.asbinary, - LinearExpression(self.linear.args + [self.mon_bin]), + LinearExpression(self.linear.args + [self.bin]), ), (self.linear, self.zero, self.linear), (self.linear, self.one, LinearExpression(self.linear.args + [1])), @@ -697,11 +653,7 @@ def test_add_linear(self): LinearExpression(self.linear.args + [self.param_mut]), ), # 8: - ( - self.linear, - self.var, - LinearExpression(self.linear.args + [self.mon_var]), - ), + (self.linear, self.var, LinearExpression(self.linear.args + [self.var])), ( self.linear, self.mon_native, @@ -868,7 +820,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.asbinary, - LinearExpression(self.mutable_l1.args + [self.mon_bin]), + LinearExpression(self.mutable_l1.args + [self.bin]), ), (self.mutable_l1, self.zero, self.mon_npv), (self.mutable_l1, self.one, LinearExpression(self.mutable_l1.args + [1])), @@ -893,7 +845,7 @@ def test_add_mutable_l1(self): ( self.mutable_l1, self.var, - LinearExpression(self.mutable_l1.args + [self.mon_var]), + LinearExpression(self.mutable_l1.args + [self.var]), ), ( self.mutable_l1, @@ -1075,7 +1027,7 @@ def test_add_param0(self): def test_add_param1(self): tests = [ (self.param1, self.invalid, NotImplemented), - (self.param1, self.asbinary, LinearExpression([1, self.mon_bin])), + (self.param1, self.asbinary, LinearExpression([1, self.bin])), (self.param1, self.zero, 1), (self.param1, self.one, 2), # 4: @@ -1084,7 +1036,7 @@ def test_add_param1(self): (self.param1, self.param, 7), (self.param1, self.param_mut, NPV_SumExpression([1, self.param_mut])), # 8: - (self.param1, self.var, LinearExpression([1, self.mon_var])), + (self.param1, self.var, LinearExpression([1, self.var])), (self.param1, self.mon_native, LinearExpression([1, self.mon_native])), (self.param1, self.mon_param, LinearExpression([1, self.mon_param])), (self.param1, self.mon_npv, LinearExpression([1, self.mon_npv])), @@ -1114,7 +1066,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.asbinary, - LinearExpression(self.mutable_l3.args + [self.mon_bin]), + LinearExpression(self.mutable_l3.args + [self.bin]), ), (self.mutable_l3, self.zero, self.npv), (self.mutable_l3, self.one, NPV_SumExpression(self.mutable_l3.args + [1])), @@ -1143,7 +1095,7 @@ def test_add_mutable_l3(self): ( self.mutable_l3, self.var, - LinearExpression(self.mutable_l3.args + [self.mon_var]), + LinearExpression(self.mutable_l3.args + [self.var]), ), ( self.mutable_l3, @@ -1249,32 +1201,32 @@ def test_sub_asbinary(self): # BooleanVar objects do not support addition (self.asbinary, self.asbinary, NotImplemented), (self.asbinary, self.zero, self.bin), - (self.asbinary, self.one, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.one, LinearExpression([self.bin, -1])), # 4: - (self.asbinary, self.native, LinearExpression([self.mon_bin, -5])), - (self.asbinary, self.npv, LinearExpression([self.mon_bin, self.minus_npv])), - (self.asbinary, self.param, LinearExpression([self.mon_bin, -6])), + (self.asbinary, self.native, LinearExpression([self.bin, -5])), + (self.asbinary, self.npv, LinearExpression([self.bin, self.minus_npv])), + (self.asbinary, self.param, LinearExpression([self.bin, -6])), ( self.asbinary, self.param_mut, - LinearExpression([self.mon_bin, self.minus_param_mut]), + LinearExpression([self.bin, self.minus_param_mut]), ), # 8: - (self.asbinary, self.var, LinearExpression([self.mon_bin, self.minus_var])), + (self.asbinary, self.var, LinearExpression([self.bin, self.minus_var])), ( self.asbinary, self.mon_native, - LinearExpression([self.mon_bin, self.minus_mon_native]), + LinearExpression([self.bin, self.minus_mon_native]), ), ( self.asbinary, self.mon_param, - LinearExpression([self.mon_bin, self.minus_mon_param]), + LinearExpression([self.bin, self.minus_mon_param]), ), ( self.asbinary, self.mon_npv, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), # 12: (self.asbinary, self.linear, SumExpression([self.bin, self.minus_linear])), @@ -1285,7 +1237,7 @@ def test_sub_asbinary(self): ( self.asbinary, self.mutable_l1, - LinearExpression([self.mon_bin, self.minus_mon_npv]), + LinearExpression([self.bin, self.minus_mon_npv]), ), ( self.asbinary, @@ -1293,12 +1245,12 @@ def test_sub_asbinary(self): SumExpression([self.bin, self.minus_mutable_l2]), ), (self.asbinary, self.param0, self.bin), - (self.asbinary, self.param1, LinearExpression([self.mon_bin, -1])), + (self.asbinary, self.param1, LinearExpression([self.bin, -1])), # 20: ( self.asbinary, self.mutable_l3, - LinearExpression([self.mon_bin, self.minus_npv]), + LinearExpression([self.bin, self.minus_npv]), ), ] self._run_cases(tests, operator.sub) @@ -1571,35 +1523,31 @@ def test_sub_param_mut(self): def test_sub_var(self): tests = [ (self.var, self.invalid, NotImplemented), - (self.var, self.asbinary, LinearExpression([self.mon_var, self.minus_bin])), + (self.var, self.asbinary, LinearExpression([self.var, self.minus_bin])), (self.var, self.zero, self.var), - (self.var, self.one, LinearExpression([self.mon_var, -1])), + (self.var, self.one, LinearExpression([self.var, -1])), # 4: - (self.var, self.native, LinearExpression([self.mon_var, -5])), - (self.var, self.npv, LinearExpression([self.mon_var, self.minus_npv])), - (self.var, self.param, LinearExpression([self.mon_var, -6])), + (self.var, self.native, LinearExpression([self.var, -5])), + (self.var, self.npv, LinearExpression([self.var, self.minus_npv])), + (self.var, self.param, LinearExpression([self.var, -6])), ( self.var, self.param_mut, - LinearExpression([self.mon_var, self.minus_param_mut]), + LinearExpression([self.var, self.minus_param_mut]), ), # 8: - (self.var, self.var, LinearExpression([self.mon_var, self.minus_var])), + (self.var, self.var, LinearExpression([self.var, self.minus_var])), ( self.var, self.mon_native, - LinearExpression([self.mon_var, self.minus_mon_native]), + LinearExpression([self.var, self.minus_mon_native]), ), ( self.var, self.mon_param, - LinearExpression([self.mon_var, self.minus_mon_param]), - ), - ( - self.var, - self.mon_npv, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_param]), ), + (self.var, self.mon_npv, LinearExpression([self.var, self.minus_mon_npv])), # 12: ( self.var, @@ -1613,7 +1561,7 @@ def test_sub_var(self): ( self.var, self.mutable_l1, - LinearExpression([self.mon_var, self.minus_mon_npv]), + LinearExpression([self.var, self.minus_mon_npv]), ), ( self.var, @@ -1621,13 +1569,9 @@ def test_sub_var(self): SumExpression([self.var, self.minus_mutable_l2]), ), (self.var, self.param0, self.var), - (self.var, self.param1, LinearExpression([self.mon_var, -1])), + (self.var, self.param1, LinearExpression([self.var, -1])), # 20: - ( - self.var, - self.mutable_l3, - LinearExpression([self.mon_var, self.minus_npv]), - ), + (self.var, self.mutable_l3, LinearExpression([self.var, self.minus_npv])), ] self._run_cases(tests, operator.sub) self._run_cases(tests, operator.isub) @@ -6039,7 +5983,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([])), (mutable_npv, self.one, _MutableNPVSumExpression([1])), # 4: @@ -6048,7 +5992,7 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.param, _MutableNPVSumExpression([6])), (mutable_npv, self.param_mut, _MutableNPVSumExpression([self.param_mut])), # 8: - (mutable_npv, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([self.var])), (mutable_npv, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_npv, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_npv, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6074,7 +6018,7 @@ def test_mutable_nvp_iadd(self): mutable_npv = _MutableNPVSumExpression([10]) tests = [ (mutable_npv, self.invalid, NotImplemented), - (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), + (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), (mutable_npv, self.one, _MutableNPVSumExpression([10, 1])), # 4: @@ -6087,7 +6031,7 @@ def test_mutable_nvp_iadd(self): _MutableNPVSumExpression([10, self.param_mut]), ), # 8: - (mutable_npv, self.var, _MutableLinearExpression([10, self.mon_var])), + (mutable_npv, self.var, _MutableLinearExpression([10, self.var])), ( mutable_npv, self.mon_native, @@ -6130,7 +6074,7 @@ def test_mutable_lin_iadd(self): mutable_lin = _MutableLinearExpression([]) tests = [ (mutable_lin, self.invalid, NotImplemented), - (mutable_lin, self.asbinary, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.asbinary, _MutableLinearExpression([self.bin])), (mutable_lin, self.zero, _MutableLinearExpression([])), (mutable_lin, self.one, _MutableLinearExpression([1])), # 4: @@ -6139,7 +6083,7 @@ def test_mutable_lin_iadd(self): (mutable_lin, self.param, _MutableLinearExpression([6])), (mutable_lin, self.param_mut, _MutableLinearExpression([self.param_mut])), # 8: - (mutable_lin, self.var, _MutableLinearExpression([self.mon_var])), + (mutable_lin, self.var, _MutableLinearExpression([self.var])), (mutable_lin, self.mon_native, _MutableLinearExpression([self.mon_native])), (mutable_lin, self.mon_param, _MutableLinearExpression([self.mon_param])), (mutable_lin, self.mon_npv, _MutableLinearExpression([self.mon_npv])), @@ -6162,81 +6106,69 @@ def test_mutable_lin_iadd(self): ] self._run_iadd_cases(tests, operator.iadd) - mutable_lin = _MutableLinearExpression([self.mon_bin]) + mutable_lin = _MutableLinearExpression([self.bin]) tests = [ (mutable_lin, self.invalid, NotImplemented), ( mutable_lin, self.asbinary, - _MutableLinearExpression([self.mon_bin, self.mon_bin]), + _MutableLinearExpression([self.bin, self.bin]), ), - (mutable_lin, self.zero, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.one, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.zero, _MutableLinearExpression([self.bin])), + (mutable_lin, self.one, _MutableLinearExpression([self.bin, 1])), # 4: - (mutable_lin, self.native, _MutableLinearExpression([self.mon_bin, 5])), - (mutable_lin, self.npv, _MutableLinearExpression([self.mon_bin, self.npv])), - (mutable_lin, self.param, _MutableLinearExpression([self.mon_bin, 6])), + (mutable_lin, self.native, _MutableLinearExpression([self.bin, 5])), + (mutable_lin, self.npv, _MutableLinearExpression([self.bin, self.npv])), + (mutable_lin, self.param, _MutableLinearExpression([self.bin, 6])), ( mutable_lin, self.param_mut, - _MutableLinearExpression([self.mon_bin, self.param_mut]), + _MutableLinearExpression([self.bin, self.param_mut]), ), # 8: - ( - mutable_lin, - self.var, - _MutableLinearExpression([self.mon_bin, self.mon_var]), - ), + (mutable_lin, self.var, _MutableLinearExpression([self.bin, self.var])), ( mutable_lin, self.mon_native, - _MutableLinearExpression([self.mon_bin, self.mon_native]), + _MutableLinearExpression([self.bin, self.mon_native]), ), ( mutable_lin, self.mon_param, - _MutableLinearExpression([self.mon_bin, self.mon_param]), + _MutableLinearExpression([self.bin, self.mon_param]), ), ( mutable_lin, self.mon_npv, - _MutableLinearExpression([self.mon_bin, self.mon_npv]), + _MutableLinearExpression([self.bin, self.mon_npv]), ), # 12: ( mutable_lin, self.linear, - _MutableLinearExpression([self.mon_bin] + self.linear.args), - ), - ( - mutable_lin, - self.sum, - _MutableSumExpression([self.mon_bin] + self.sum.args), - ), - ( - mutable_lin, - self.other, - _MutableSumExpression([self.mon_bin, self.other]), + _MutableLinearExpression([self.bin] + self.linear.args), ), - (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.mon_bin])), + (mutable_lin, self.sum, _MutableSumExpression([self.bin] + self.sum.args)), + (mutable_lin, self.other, _MutableSumExpression([self.bin, self.other])), + (mutable_lin, self.mutable_l0, _MutableLinearExpression([self.bin])), # 16: ( mutable_lin, self.mutable_l1, - _MutableLinearExpression([self.mon_bin] + self.mutable_l1.args), + _MutableLinearExpression([self.bin] + self.mutable_l1.args), ), ( mutable_lin, self.mutable_l2, - _MutableSumExpression([self.mon_bin] + self.mutable_l2.args), + _MutableSumExpression([self.bin] + self.mutable_l2.args), ), - (mutable_lin, self.param0, _MutableLinearExpression([self.mon_bin])), - (mutable_lin, self.param1, _MutableLinearExpression([self.mon_bin, 1])), + (mutable_lin, self.param0, _MutableLinearExpression([self.bin])), + (mutable_lin, self.param1, _MutableLinearExpression([self.bin, 1])), # 20: ( mutable_lin, self.mutable_l3, - _MutableLinearExpression([self.mon_bin, self.npv]), + _MutableLinearExpression([self.bin, self.npv]), ), ] self._run_iadd_cases(tests, operator.iadd) diff --git a/pyomo/core/tests/unit/test_visitor.py b/pyomo/core/tests/unit/test_visitor.py index fada7d6f6b2..12fb98d1d19 100644 --- a/pyomo/core/tests/unit/test_visitor.py +++ b/pyomo/core/tests/unit/test_visitor.py @@ -437,9 +437,7 @@ def test_replacement_linear_expression_with_constant(self): sub_map = dict() sub_map[id(m.x)] = 5 e2 = replace_expressions(e, sub_map) - assertExpressionsEqual( - self, e2, LinearExpression([10, MonomialTermExpression((1, m.y))]) - ) + assertExpressionsEqual(self, e2, LinearExpression([10, m.y])) e = LinearExpression(linear_coefs=[2, 3], linear_vars=[m.x, m.y]) sub_map = dict() @@ -886,20 +884,7 @@ def test_replace(self): assertExpressionsEqual( self, SumExpression( - [ - LinearExpression( - [ - MonomialTermExpression((1, m.y[1])), - MonomialTermExpression((1, m.y[2])), - ] - ), - LinearExpression( - [ - MonomialTermExpression((1, m.y[2])), - MonomialTermExpression((1, m.y[3])), - ] - ), - ] + [LinearExpression([m.y[1], m.y[2]]), LinearExpression([m.y[2], m.y[3]])] ) == 0, f, @@ -930,9 +915,7 @@ def test_npv_sum(self): e3 = replace_expressions(e1, {id(m.p1): m.x}) assertExpressionsEqual(self, e2, m.p2 + 2) - assertExpressionsEqual( - self, e3, LinearExpression([MonomialTermExpression((1, m.x)), 2]) - ) + assertExpressionsEqual(self, e3, LinearExpression([m.x, 2])) def test_npv_negation(self): m = ConcreteModel() diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index 28025816262..5d0d6f6c21b 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -425,12 +425,7 @@ def check_two_term_disjunction_xor(self, xor, disj1, disj2): assertExpressionsEqual( self, xor.body, - EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, disj1.binary_indicator_var)), - EXPR.MonomialTermExpression((1, disj2.binary_indicator_var)), - ] - ), + EXPR.LinearExpression([disj1.binary_indicator_var, disj2.binary_indicator_var]), ) self.assertEqual(xor.lower, 1) self.assertEqual(xor.upper, 1) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index 2383d4587f5..c6ac49f6d36 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -155,10 +155,7 @@ def test_or_constraints(self): self, orcons.body, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.d[0].binary_indicator_var)), - EXPR.MonomialTermExpression((1, m.d[1].binary_indicator_var)), - ] + [m.d[0].binary_indicator_var, m.d[1].binary_indicator_var] ), ) self.assertEqual(orcons.lower, 1) diff --git a/pyomo/gdp/tests/test_binary_multiplication.py b/pyomo/gdp/tests/test_binary_multiplication.py index aa846c4710a..ae2c44b899e 100644 --- a/pyomo/gdp/tests/test_binary_multiplication.py +++ b/pyomo/gdp/tests/test_binary_multiplication.py @@ -146,10 +146,7 @@ def test_or_constraints(self): self, orcons.body, EXPR.LinearExpression( - [ - EXPR.MonomialTermExpression((1, m.d[0].binary_indicator_var)), - EXPR.MonomialTermExpression((1, m.d[1].binary_indicator_var)), - ] + [m.d[0].binary_indicator_var, m.d[1].binary_indicator_var] ), ) self.assertEqual(orcons.lower, 1) diff --git a/pyomo/gdp/tests/test_disjunct.py b/pyomo/gdp/tests/test_disjunct.py index d969b245ee7..f93ac31fb0f 100644 --- a/pyomo/gdp/tests/test_disjunct.py +++ b/pyomo/gdp/tests/test_disjunct.py @@ -632,19 +632,13 @@ def test_cast_to_binary(self): out = StringIO() with LoggingIntercept(out): e = m.iv + 1 - assertExpressionsEqual( - self, e, EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), 1]) - ) + assertExpressionsEqual(self, e, EXPR.LinearExpression([m.biv, 1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() with LoggingIntercept(out): e = m.iv - 1 - assertExpressionsEqual( - self, - e, - EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), -1]), - ) + assertExpressionsEqual(self, e, EXPR.LinearExpression([m.biv, -1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() @@ -665,9 +659,7 @@ def test_cast_to_binary(self): out = StringIO() with LoggingIntercept(out): e = 1 + m.iv - assertExpressionsEqual( - self, e, EXPR.LinearExpression([1, EXPR.MonomialTermExpression((1, m.biv))]) - ) + assertExpressionsEqual(self, e, EXPR.LinearExpression([1, m.biv])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() @@ -699,20 +691,14 @@ def test_cast_to_binary(self): with LoggingIntercept(out): a = m.iv a += 1 - assertExpressionsEqual( - self, a, EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), 1]) - ) + assertExpressionsEqual(self, a, EXPR.LinearExpression([m.biv, 1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() with LoggingIntercept(out): a = m.iv a -= 1 - assertExpressionsEqual( - self, - a, - EXPR.LinearExpression([EXPR.MonomialTermExpression((1, m.biv)), -1]), - ) + assertExpressionsEqual(self, a, EXPR.LinearExpression([m.biv, -1])) self.assertIn(deprecation_msg, out.getvalue()) out = StringIO() From 9da87b6f0af339e116c898be6981e562a7c1d7e0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 17:37:13 -0700 Subject: [PATCH 0900/3044] Update PyROS to admit VarData in LinearExpressions --- .../contrib/pyros/pyros_algorithm_methods.py | 20 ++++++---- pyomo/contrib/pyros/tests/test_grcs.py | 40 +++++++++++-------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index f0e32a284bb..5987db074e6 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -26,6 +26,7 @@ ) from pyomo.contrib.pyros.util import get_main_elapsed_time, coefficient_matching from pyomo.core.base import value +from pyomo.core.expr import MonomialTermExpression from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.core.base.var import _VarData as VarData from itertools import chain @@ -69,14 +70,17 @@ def get_dr_var_to_scaled_expr_map( ssv_dr_eq_zip = zip(second_stage_vars, decision_rule_eqns) for ssv_idx, (ssv, dr_eq) in enumerate(ssv_dr_eq_zip): for term in dr_eq.body.args: - is_ssv_term = ( - isinstance(term.args[0], int) - and term.args[0] == -1 - and isinstance(term.args[1], VarData) - ) - if not is_ssv_term: - dr_var = term.args[1] - var_to_scaled_expr_map[dr_var] = term + if isinstance(term, MonomialTermExpression): + is_ssv_term = ( + isinstance(term.args[0], int) + and term.args[0] == -1 + and isinstance(term.args[1], VarData) + ) + if not is_ssv_term: + dr_var = term.args[1] + var_to_scaled_expr_map[dr_var] = term + elif isinstance(term, VarData): + var_to_scaled_expr_map[term] = MonomialTermExpression((1, term)) return var_to_scaled_expr_map diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index df3568e42a4..c308f0d6990 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -19,6 +19,7 @@ from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.core.base.set_types import NonNegativeIntegers +from pyomo.core.base.var import _VarData from pyomo.core.expr import ( identify_variables, identify_mutable_parameters, @@ -571,22 +572,30 @@ def test_dr_eqns_form_correct(self): dr_polynomial_terms, indexed_dr_var.values(), dr_monomial_param_combos ) for idx, (term, dr_var, param_combo) in enumerate(dr_polynomial_zip): - # term should be a monomial expression of form - # (uncertain parameter product) * (decision rule variable) - # so length of expression object should be 2 - self.assertEqual( - len(term.args), - 2, - msg=( - f"Length of `args` attribute of term {str(term)} " - f"of DR equation {dr_eq.name!r} is not as expected. " - f"Args: {term.args}" - ), - ) + # term should be either a monomial expression or scalar variable + if isinstance(term, MonomialTermExpression): + # should be of form (uncertain parameter product) * + # (decision rule variable) so length of expression + # object should be 2 + self.assertEqual( + len(term.args), + 2, + msg=( + f"Length of `args` attribute of term {str(term)} " + f"of DR equation {dr_eq.name!r} is not as expected. " + f"Args: {term.args}" + ), + ) + + # check that uncertain parameters participating in + # the monomial are as expected + param_product_multiplicand = term.args[0] + dr_var_multiplicand = term.args[1] + else: + self.assertIsInstance(term, _VarData) + param_product_multiplicand = 1 + dr_var_multiplicand = term - # check that uncertain parameters participating in - # the monomial are as expected - param_product_multiplicand = term.args[0] if idx == 0: # static DR term param_combo_found_in_term = (param_product_multiplicand,) @@ -612,7 +621,6 @@ def test_dr_eqns_form_correct(self): # check that DR variable participating in the monomial # is as expected - dr_var_multiplicand = term.args[1] self.assertIs( dr_var_multiplicand, dr_var, From 976f88df1dcd7d75e0c331da105ac6e8b8ac7282 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 21:09:54 -0700 Subject: [PATCH 0901/3044] Update doc tests to track change in LinearExpression arg types --- doc/OnlineDocs/src/expr/managing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/src/expr/managing.py b/doc/OnlineDocs/src/expr/managing.py index 00d521d16ab..ff149e4fd5c 100644 --- a/doc/OnlineDocs/src/expr/managing.py +++ b/doc/OnlineDocs/src/expr/managing.py @@ -181,7 +181,7 @@ def clone_expression(expr): # x[0] + 5*x[1] print(str(ce)) # x[0] + 5*x[1] -print(e.arg(0) is not ce.arg(0)) +print(e.arg(0) is ce.arg(0)) # True print(e.arg(1) is not ce.arg(1)) # True From 1bfeebbbd91107da54c826b479e23f903c86ab21 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 8 Mar 2024 09:23:26 -0700 Subject: [PATCH 0902/3044] NFC: update docs --- pyomo/core/expr/numeric_expr.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index 8ce7ee81c9a..25d83ca20f4 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -1234,9 +1234,11 @@ class LinearExpression(SumExpression): """An expression object for linear polynomials. This is a derived :py:class`SumExpression` that guarantees all - arguments are either not potentially variable (e.g., native types, - Params, or NPV expressions) OR :py:class:`MonomialTermExpression` - objects. + arguments are one of the following types: + + - not potentially variable (e.g., native types, Params, or NPV expressions) + - :py:class:`MonomialTermExpression` + - :py:class:`_VarData` Args: args (tuple): Children nodes @@ -1253,7 +1255,7 @@ def __init__(self, args=None, constant=None, linear_coefs=None, linear_vars=None You can specify `args` OR (`constant`, `linear_coefs`, and `linear_vars`). If `args` is provided, it should be a list that - contains only constants, NPV objects/expressions, or + contains only constants, NPV objects/expressions, variables, or :py:class:`MonomialTermExpression` objects. Alternatively, you can specify the constant, the list of linear_coefs and the list of linear_vars separately. Note that these lists are NOT From 6135b8e61b89e2830b9ed227bfba8d66ff3bf340 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 22:22:09 -0700 Subject: [PATCH 0903/3044] Improve automatic flattening of LinearExpression args --- pyomo/core/expr/numeric_expr.py | 33 +++++++------ pyomo/core/expr/template_expr.py | 6 +-- .../unit/test_numeric_expr_dispatcher.py | 8 ++-- .../unit/test_numeric_expr_zerofilter.py | 8 ++-- pyomo/core/tests/unit/test_template_expr.py | 48 +++++++++---------- 5 files changed, 53 insertions(+), 50 deletions(-) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index c1199ffdcad..e8f7227208c 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -2283,8 +2283,11 @@ def _iadd_mutablenpvsum_mutable(a, b): def _iadd_mutablenpvsum_native(a, b): if not b: return a - a._args_.append(b) - a._nargs += 1 + if a._args_ and a._args_[-1].__class__ in native_numeric_types: + a._args_[-1] += b + else: + a._args_.append(b) + a._nargs += 1 return a @@ -2296,9 +2299,7 @@ def _iadd_mutablenpvsum_npv(a, b): def _iadd_mutablenpvsum_param(a, b): if b.is_constant(): - b = b.value - if not b: - return a + return _iadd_mutablesum_native(a, b.value) a._args_.append(b) a._nargs += 1 return a @@ -2379,8 +2380,11 @@ def _iadd_mutablelinear_mutable(a, b): def _iadd_mutablelinear_native(a, b): if not b: return a - a._args_.append(b) - a._nargs += 1 + if a._args_ and a._args_[-1].__class__ in native_numeric_types: + a._args_[-1] += b + else: + a._args_.append(b) + a._nargs += 1 return a @@ -2392,9 +2396,7 @@ def _iadd_mutablelinear_npv(a, b): def _iadd_mutablelinear_param(a, b): if b.is_constant(): - b = b.value - if not b: - return a + return _iadd_mutablesum_native(a, b.value) a._args_.append(b) a._nargs += 1 return a @@ -2478,8 +2480,11 @@ def _iadd_mutablesum_mutable(a, b): def _iadd_mutablesum_native(a, b): if not b: return a - a._args_.append(b) - a._nargs += 1 + if a._args_ and a._args_[-1].__class__ in native_numeric_types: + a._args_[-1] += b + else: + a._args_.append(b) + a._nargs += 1 return a @@ -2491,9 +2496,7 @@ def _iadd_mutablesum_npv(a, b): def _iadd_mutablesum_param(a, b): if b.is_constant(): - b = b.value - if not b: - return a + return _iadd_mutablesum_native(a, b.value) a._args_.append(b) a._nargs += 1 return a diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index f65a1f2b9b0..f982ef38d1d 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -116,7 +116,7 @@ def _to_string(self, values, verbose, smap): return "%s[%s]" % (values[0], ','.join(values[1:])) def _resolve_template(self, args): - return args[0].__getitem__(tuple(args[1:])) + return args[0][*args[1:]] def _apply_operation(self, result): args = tuple( @@ -127,7 +127,7 @@ def _apply_operation(self, result): ) for arg in result[1:] ) - return result[0].__getitem__(tuple(result[1:])) + return result[0][*result[1:]] class Numeric_GetItemExpression(GetItemExpression, NumericExpression): @@ -273,7 +273,7 @@ def _to_string(self, values, verbose, smap): return "%s.%s" % (values[0], attr) def _resolve_template(self, args): - return getattr(*tuple(args)) + return getattr(*args) class Numeric_GetAttrExpression(GetAttrExpression, NumericExpression): diff --git a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py index 3787f00de47..7c6e2af9974 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py +++ b/pyomo/core/tests/unit/test_numeric_expr_dispatcher.py @@ -6548,11 +6548,11 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.invalid, NotImplemented), (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), - (mutable_npv, self.one, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.one, _MutableNPVSumExpression([11])), # 4: - (mutable_npv, self.native, _MutableNPVSumExpression([10, 5])), + (mutable_npv, self.native, _MutableNPVSumExpression([15])), (mutable_npv, self.npv, _MutableNPVSumExpression([10, self.npv])), - (mutable_npv, self.param, _MutableNPVSumExpression([10, 6])), + (mutable_npv, self.param, _MutableNPVSumExpression([16])), ( mutable_npv, self.param_mut, @@ -6592,7 +6592,7 @@ def test_mutable_nvp_iadd(self): _MutableSumExpression([10] + self.mutable_l2.args), ), (mutable_npv, self.param0, _MutableNPVSumExpression([10])), - (mutable_npv, self.param1, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.param1, _MutableNPVSumExpression([11])), # 20: (mutable_npv, self.mutable_l3, _MutableNPVSumExpression([10, self.npv])), ] diff --git a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py index 162d664e0f8..34d2e1cc2c2 100644 --- a/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py +++ b/pyomo/core/tests/unit/test_numeric_expr_zerofilter.py @@ -6076,11 +6076,11 @@ def test_mutable_nvp_iadd(self): (mutable_npv, self.invalid, NotImplemented), (mutable_npv, self.asbinary, _MutableLinearExpression([10, self.mon_bin])), (mutable_npv, self.zero, _MutableNPVSumExpression([10])), - (mutable_npv, self.one, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.one, _MutableNPVSumExpression([11])), # 4: - (mutable_npv, self.native, _MutableNPVSumExpression([10, 5])), + (mutable_npv, self.native, _MutableNPVSumExpression([15])), (mutable_npv, self.npv, _MutableNPVSumExpression([10, self.npv])), - (mutable_npv, self.param, _MutableNPVSumExpression([10, 6])), + (mutable_npv, self.param, _MutableNPVSumExpression([16])), ( mutable_npv, self.param_mut, @@ -6120,7 +6120,7 @@ def test_mutable_nvp_iadd(self): _MutableSumExpression([10] + self.mutable_l2.args), ), (mutable_npv, self.param0, _MutableNPVSumExpression([10])), - (mutable_npv, self.param1, _MutableNPVSumExpression([10, 1])), + (mutable_npv, self.param1, _MutableNPVSumExpression([11])), # 20: (mutable_npv, self.mutable_l3, _MutableNPVSumExpression([10, self.npv])), ] diff --git a/pyomo/core/tests/unit/test_template_expr.py b/pyomo/core/tests/unit/test_template_expr.py index 4f255e3567a..4c872e1e11d 100644 --- a/pyomo/core/tests/unit/test_template_expr.py +++ b/pyomo/core/tests/unit/test_template_expr.py @@ -490,14 +490,14 @@ def c(m): self.assertEqual( str(resolve_template(template)), 'x[1,1,10] + ' - '(x[2,1,10] + x[2,1,20]) + ' - '(x[3,1,10] + x[3,1,20] + x[3,1,30]) + ' - '(x[1,2,10]) + ' - '(x[2,2,10] + x[2,2,20]) + ' - '(x[3,2,10] + x[3,2,20] + x[3,2,30]) + ' - '(x[1,3,10]) + ' - '(x[2,3,10] + x[2,3,20]) + ' - '(x[3,3,10] + x[3,3,20] + x[3,3,30]) <= 0', + 'x[2,1,10] + x[2,1,20] + ' + 'x[3,1,10] + x[3,1,20] + x[3,1,30] + ' + 'x[1,2,10] + ' + 'x[2,2,10] + x[2,2,20] + ' + 'x[3,2,10] + x[3,2,20] + x[3,2,30] + ' + 'x[1,3,10] + ' + 'x[2,3,10] + x[2,3,20] + ' + 'x[3,3,10] + x[3,3,20] + x[3,3,30] <= 0', ) def test_multidim_nested_sum_rule(self): @@ -566,14 +566,14 @@ def c(m): self.assertEqual( str(resolve_template(template)), 'x[1,1,10] + ' - '(x[2,1,10] + x[2,1,20]) + ' - '(x[3,1,10] + x[3,1,20] + x[3,1,30]) + ' - '(x[1,2,10]) + ' - '(x[2,2,10] + x[2,2,20]) + ' - '(x[3,2,10] + x[3,2,20] + x[3,2,30]) + ' - '(x[1,3,10]) + ' - '(x[2,3,10] + x[2,3,20]) + ' - '(x[3,3,10] + x[3,3,20] + x[3,3,30]) <= 0', + 'x[2,1,10] + x[2,1,20] + ' + 'x[3,1,10] + x[3,1,20] + x[3,1,30] + ' + 'x[1,2,10] + ' + 'x[2,2,10] + x[2,2,20] + ' + 'x[3,2,10] + x[3,2,20] + x[3,2,30] + ' + 'x[1,3,10] + ' + 'x[2,3,10] + x[2,3,20] + ' + 'x[3,3,10] + x[3,3,20] + x[3,3,30] <= 0', ) def test_multidim_nested_getattr_sum_rule(self): @@ -609,14 +609,14 @@ def c(m): self.assertEqual( str(resolve_template(template)), 'x[1,1,10] + ' - '(x[2,1,10] + x[2,1,20]) + ' - '(x[3,1,10] + x[3,1,20] + x[3,1,30]) + ' - '(x[1,2,10]) + ' - '(x[2,2,10] + x[2,2,20]) + ' - '(x[3,2,10] + x[3,2,20] + x[3,2,30]) + ' - '(x[1,3,10]) + ' - '(x[2,3,10] + x[2,3,20]) + ' - '(x[3,3,10] + x[3,3,20] + x[3,3,30]) <= 0', + 'x[2,1,10] + x[2,1,20] + ' + 'x[3,1,10] + x[3,1,20] + x[3,1,30] + ' + 'x[1,2,10] + ' + 'x[2,2,10] + x[2,2,20] + ' + 'x[3,2,10] + x[3,2,20] + x[3,2,30] + ' + 'x[1,3,10] + ' + 'x[2,3,10] + x[2,3,20] + ' + 'x[3,3,10] + x[3,3,20] + x[3,3,30] <= 0', ) def test_eval_getattr(self): From 15b7ecb93e5dd19740deed88e91b7e715054a93a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 22:23:03 -0700 Subject: [PATCH 0904/3044] bugfix: resolution of TemplateSumExpression --- pyomo/core/expr/template_expr.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index f982ef38d1d..6ac4c8c041f 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -19,11 +19,12 @@ from pyomo.core.expr.base import ExpressionBase, ExpressionArgs_Mixin, NPV_Mixin from pyomo.core.expr.logical_expr import BooleanExpression from pyomo.core.expr.numeric_expr import ( + ARG_TYPE, NumericExpression, - SumExpression, Numeric_NPV_Mixin, + SumExpression, + mutable_expression, register_arg_type, - ARG_TYPE, _balanced_parens, ) from pyomo.core.expr.numvalue import ( @@ -521,7 +522,15 @@ def _to_string(self, values, verbose, smap): return 'SUM(%s %s)' % (val, iterStr) def _resolve_template(self, args): - return SumExpression(args) + with mutable_expression() as e: + for arg in args: + e += arg + if e.nargs() > 1: + return e + elif not e.nargs(): + return 0 + else: + return e.arg(0) class IndexTemplate(NumericValue): From e173506ab0c119aea350f96d3a1d07517e1f01eb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 5 Mar 2024 22:23:23 -0700 Subject: [PATCH 0905/3044] NFC: remove coverage pragma --- pyomo/core/expr/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/expr/base.py b/pyomo/core/expr/base.py index f506956e478..6e2066afcc5 100644 --- a/pyomo/core/expr/base.py +++ b/pyomo/core/expr/base.py @@ -360,7 +360,7 @@ def size(self): """ return visitor.sizeof_expression(self) - def _apply_operation(self, result): # pragma: no cover + def _apply_operation(self, result): """ Compute the values of this node given the values of its children. From fb0ca2627618ba28a57cfd5d205c1ccf60529fa7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 18:08:39 -0700 Subject: [PATCH 0906/3044] Resolve incompatibility with Python<=3.10 --- pyomo/core/expr/template_expr.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index 6ac4c8c041f..a7f301e32f1 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -117,18 +117,10 @@ def _to_string(self, values, verbose, smap): return "%s[%s]" % (values[0], ','.join(values[1:])) def _resolve_template(self, args): - return args[0][*args[1:]] + return args[0].__getitem__(args[1:]) def _apply_operation(self, result): - args = tuple( - ( - arg - if arg.__class__ in native_types or not arg.is_numeric_type() - else value(arg) - ) - for arg in result[1:] - ) - return result[0][*result[1:]] + return result[0].__getitem__(result[1:]) class Numeric_GetItemExpression(GetItemExpression, NumericExpression): From d51fe311bfbeb62edcd22a30f51c6180396cf7e7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 7 Mar 2024 18:09:06 -0700 Subject: [PATCH 0907/3044] Minor code readibility improvement --- pyomo/core/expr/template_expr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index a7f301e32f1..d30046e9d82 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -251,8 +251,8 @@ def nargs(self): return 2 def _apply_operation(self, result): - assert len(result) == 2 - return getattr(result[0], result[1]) + obj, attr = result + return getattr(obj, attr) def _to_string(self, values, verbose, smap): assert len(values) == 2 From e13033408b433bafc301e7ec75686dacb5d36cce Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 11 Mar 2024 07:46:26 -0600 Subject: [PATCH 0908/3044] Support storing expression templates in Objective/Constraint expressions --- pyomo/core/base/constraint.py | 13 ++++++++- pyomo/core/base/indexed_component.py | 6 +++- pyomo/core/base/objective.py | 12 ++++++++ pyomo/core/expr/template_expr.py | 42 ++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 8cf3c48ad0a..bd221123974 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -15,7 +15,7 @@ from pyomo.common.pyomo_typing import overload from pyomo.common.deprecation import RenamedClass -from pyomo.common.errors import DeveloperError +from pyomo.common.errors import DeveloperError, TemplateExpressionError from pyomo.common.formatting import tabular_writer from pyomo.common.log import is_debug_set from pyomo.common.modeling import NOTSET @@ -36,6 +36,7 @@ InequalityExpression, RangedExpression, ) +from pyomo.core.expr.template_expr import TemplatizedDataStore, templatize_constraint from pyomo.core.base.component import ActiveComponentData, ModelComponentFactory from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import ( @@ -54,6 +55,8 @@ logger = logging.getLogger('pyomo.core') +TEMPLATIZE_CONSTRAINTS = False + _inf = float('inf') _nonfinite_values = {_inf, -_inf} _known_relational_expressions = { @@ -795,6 +798,14 @@ def construct(self, data=None): # indices to be created at a later time). pass else: + if TEMPLATIZE_CONSTRAINTS: + try: + expr, indices = templatize_constraint(self) + self._data = TemplatizedDataStore(self, expr, indices) + return + except TemplateExpressionError: + pass + # Bypass the index validation and create the member directly for index in self.index_set(): self._setitem_when_not_present(index, rule(block, index)) diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index 0d498da091d..ac72315d445 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -347,7 +347,11 @@ def _create_objects_for_deepcopy(self, memo, component_list): # (where the _data points back to self) and references # (where the data may be stored outside this block tree and # therefore may not be cloned) - if self.is_indexed() and not self.is_reference(): + if ( + self.is_indexed() + and not self.is_reference() + and isinstance(self._data, dict) + ): # Because we are already checking / updating the memo # for the _data dict, we can effectively "deepcopy" it # right now (almost for free!) diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index fcc63755f2b..9382b98f8b9 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -15,12 +15,14 @@ from pyomo.common.pyomo_typing import overload from pyomo.common.deprecation import RenamedClass +from pyomo.common.errors import TemplateExpressionError from pyomo.common.log import is_debug_set from pyomo.common.modeling import NOTSET from pyomo.common.formatting import tabular_writer from pyomo.common.timing import ConstructionTimer from pyomo.core.expr.numvalue import value +from pyomo.core.expr.template_expr import TemplatizedDataStore, templatize_rule from pyomo.core.base.component import ActiveComponentData, ModelComponentFactory from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import ( @@ -39,6 +41,8 @@ logger = logging.getLogger('pyomo.core') +TEMPLATIZE_OBJECTIVES = False + _rule_returned_none_error = """Objective '%s': rule returned None. Objective rules must return either a valid expression, numeric value, or @@ -313,6 +317,14 @@ def construct(self, data=None): # indices to be created at a later time). pass else: + if TEMPLATIZE_OBJECTIVES: + try: + expr, indices = templatize_rule(block, rule, self.index_set()) + self._data = TemplatizedDataStore(self, expr, indices) + return + except TemplateExpressionError: + pass + # Bypass the index validation and create the member directly for index in self.index_set(): ans = self._setitem_when_not_present(index, rule(block, index)) diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index d30046e9d82..72eea730555 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -15,7 +15,9 @@ import builtins from contextlib import nullcontext +from pyomo.common.collections import MutableMapping from pyomo.common.errors import TemplateExpressionError +from pyomo.common.gc_manager import PauseGC from pyomo.core.expr.base import ExpressionBase, ExpressionArgs_Mixin, NPV_Mixin from pyomo.core.expr.logical_expr import BooleanExpression from pyomo.core.expr.numeric_expr import ( @@ -1181,3 +1183,43 @@ def templatize_constraint(con): if expr.__class__ is tuple: expr = tuple_to_relational_expr(expr) return expr, indices + + +class TemplatizedDataStore(MutableMapping): + def __init__(self, component, expr, indices): + self._component = component + self._expr = expr + self._indices = indices + + def __getitem__(self, item): + if self._component._data is self: + self._replace_with_dict() + return self._component._data[item] + + def __setitem__(self, item, value): + if self._component._data is self: + self._replace_with_dict() + self._component._data[item] = value + + def __delitem__(self, index): + if self._component._data is self: + self._replace_with_dict() + del self._component._data[item] + + def __iter__(self): + return iter(self._component.index_set()) + + def __len__(self): + return len(self._component.index_set()) + + def __bool__(self): + return bool(self._component.index_set()) + + def _replace_with_dict(self): + comp = self._component + comp._data = {} + rule = comp.rule + block = comp.parent_block() + with PauseGC(): + for index in comp.index_set(): + comp._setitem_when_not_present(index, rule(block, index)) From e5c0be6173a36b15228ba988d7633bb94913d3ce Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 11 Mar 2024 07:47:37 -0600 Subject: [PATCH 0909/3044] -Add hooks for tempate expression nodes in BeforeChildDispatcher --- pyomo/repn/util.py | 45 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 49cca32eaf9..7288cc1d635 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -282,10 +282,21 @@ def register_dispatcher(self, visitor, child): else: self[child_type] = self._before_invalid elif not child.is_expression_type(): - if child.is_potentially_variable(): - self[child_type] = self._before_var + if child.is_indexed(): + cdata = child._ComponentDataClass(child) + if cdata.is_expression_type(): + self[child_type] = self._before_indexed_expr + elif cdata.is_potentially_variable(): + self[child_type] = self._before_indexed_var + else: + self[child_type] = self._before_indexed_param else: - self[child_type] = self._before_param + if child.is_potentially_variable(): + self[child_type] = self._before_var + elif isinstance(child, EXPR.IndexTemplate): + self[child_type] = self._before_index_template + else: + self[child_type] = self._before_param elif not child.is_potentially_variable(): self[child_type] = self._before_npv pv_base_type = child.potentially_variable_base_class() @@ -357,6 +368,34 @@ def _before_npv(visitor, child): def _before_param(visitor, child): return False, (_CONSTANT, visitor.check_constant(child.value, child)) + @staticmethod + def _before_index_template(visitor, child): + raise NotImplementedError( + f"{visitor.__class__.__name__} can not handle template expressions " + f"containing {child.__class__} nodes" + ) + + @staticmethod + def _before_indexed_expr(visitor, child): + raise NotImplementedError( + f"{visitor.__class__.__name__} can not handle template expressions " + f"containing {child.__class__} nodes" + ) + + @staticmethod + def _before_indexed_param(visitor, child): + raise NotImplementedError( + f"{visitor.__class__.__name__} can not handle template expressions " + f"containing {child.__class__} nodes" + ) + + @staticmethod + def _before_indexed_var(visitor, child): + raise NotImplementedError( + f"{visitor.__class__.__name__} can not handle template expressions " + f"containing {child.__class__} nodes" + ) + # # The following methods must be defined by derivative classes (along # with any other special-case handling they want to implement; From 3879cf5ad0bc142de9ee92f15bef4af51af65b16 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 11 Mar 2024 11:37:19 -0600 Subject: [PATCH 0910/3044] Additional PyROS update to track change in LinearExpression args --- pyomo/contrib/pyros/master_problem_methods.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index abf02809396..4d2609576ff 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -398,10 +398,17 @@ def construct_dr_polishing_problem(model_data, config): all_ub_cons.append(polishing_absolute_value_ub_cons) # get monomials; ensure second-stage variable term excluded + # + # the dr_eq is a linear sum where teh first term is the + # second-stage variable: the remainder of the terms will be + # either MonomialTermExpressions or bare VarData dr_expr_terms = dr_eq.body.args[:-1] for dr_eq_term in dr_expr_terms: - dr_var_in_term = dr_eq_term.args[-1] + if dr_eq_term.is_expression_type(): + dr_var_in_term = dr_eq_term.args[-1] + else: + dr_var_in_term = dr_eq_term dr_var_in_term_idx = dr_var_in_term.index() # get corresponding polishing variable From 8f9cf83a99b5238eb17b2fd77049f6132691fb59 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 11 Mar 2024 11:42:07 -0600 Subject: [PATCH 0911/3044] NFC: fix spelling --- pyomo/contrib/pyros/master_problem_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 4d2609576ff..8b9e85b90e9 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -399,7 +399,7 @@ def construct_dr_polishing_problem(model_data, config): # get monomials; ensure second-stage variable term excluded # - # the dr_eq is a linear sum where teh first term is the + # the dr_eq is a linear sum where the first term is the # second-stage variable: the remainder of the terms will be # either MonomialTermExpressions or bare VarData dr_expr_terms = dr_eq.body.args[:-1] From a99165c1b4c724681b94f368c1155bd317bc25c3 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 13:29:31 -0600 Subject: [PATCH 0912/3044] fix attribute error --- pyomo/util/tests/test_subsystems.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/util/tests/test_subsystems.py b/pyomo/util/tests/test_subsystems.py index a9b8a215fcc..05b1bf9f8f4 100644 --- a/pyomo/util/tests/test_subsystems.py +++ b/pyomo/util/tests/test_subsystems.py @@ -304,11 +304,11 @@ def _make_model_with_external_functions(self, named_expressions=False): m.subexpr = pyo.Expression(pyo.PositiveIntegers) subexpr1 = m.subexpr[1] = 2 * m.fermi(m.v1) subexpr2 = m.subexpr[2] = m.bessel(m.v1) - m.bessel(m.v2) - subexpr3 = m.subexpr[3] = m.subexpr[2] + m.v3**2 + subexpr3 = m.subexpr[3] = subexpr2 + m.v3**2 else: subexpr1 = 2 * m.fermi(m.v1) subexpr2 = m.bessel(m.v1) - m.bessel(m.v2) - subexpr3 = m.subexpr[2] + m.v3**2 + subexpr3 = subexpr2 + m.v3**2 m.con1 = pyo.Constraint(expr=m.v1 == 0.5) m.con2 = pyo.Constraint(expr=subexpr1 + m.v2**2 - m.v3 == 1.0) m.con3 = pyo.Constraint(expr=subexpr3 == 2.0) From b3307c9abefe08a0409f51bed80f70adfd0310f6 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 14:29:25 -0600 Subject: [PATCH 0913/3044] check class in native_types rather than isinstance NumericValue --- pyomo/util/subsystems.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 79fbdd2d281..d497d132748 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -36,15 +36,15 @@ def initializeWalker(self, expr): return True, None def beforeChild(self, parent, child, index): - if ( + if child.__class__ in native_types: + return False, None + elif ( not self._descend_into_named_expressions - and isinstance(child, NumericValue) and child.is_named_expression_type() ): self.named_expressions.append(child) return False, None - else: - return True, None + return True, None def exitNode(self, node, data): if type(node) is ExternalFunctionExpression: From a27f90d537daa382cb67420876c2796a8282400e Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 14:30:05 -0600 Subject: [PATCH 0914/3044] remove unnecessary exitNode and acceptChildResult implementations --- pyomo/util/subsystems.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index d497d132748..0ed6ade756d 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -55,14 +55,6 @@ def exitNode(self, node, data): def finalizeResult(self, result): return self._functions - # def enterNode(self, node): - # pass - - # def acceptChildResult(self, node, data, child_result, child_idx): - # if child_result.__class__ in native_types: - # return False, None - # return child_result.is_expression_type(), None - def identify_external_functions(expr): # TODO: Potentially support descend_into_named_expressions argument here. From 2ae71d7321e44c294e9ce9b0249fc1a6e514fd07 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 11 Mar 2024 14:32:48 -0600 Subject: [PATCH 0915/3044] Add clarity for specifically a single objective --- pyomo/contrib/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 55b013facb1..43d168a98a0 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -423,7 +423,7 @@ def _map_results(self, model, results): legacy_results.problem.number_of_variables = model.nvariables() number_of_objectives = model.nobjectives() legacy_results.problem.number_of_objectives = number_of_objectives - if number_of_objectives > 0: + if number_of_objectives == 1: obj = get_objective(model) legacy_results.problem.sense = obj.sense From 3ecd3bb80d145070e33eac81b348cf5a80eb529c Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 14:34:24 -0600 Subject: [PATCH 0916/3044] remove deferred todo comment --- pyomo/util/subsystems.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 0ed6ade756d..f2e2eae8444 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -57,8 +57,6 @@ def finalizeResult(self, result): def identify_external_functions(expr): - # TODO: Potentially support descend_into_named_expressions argument here. - # This will likely require converting from a generator to a function. yield from _ExternalFunctionVisitor().walk_expression(expr) From fde5edac7cb378d5d06bd1e7b929f70597155981 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 14:36:42 -0600 Subject: [PATCH 0917/3044] remove HierarchicalTimer use from subsystem calls --- pyomo/util/subsystems.py | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index f2e2eae8444..5789829ac54 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -101,12 +101,7 @@ def add_local_external_functions(block): return fcn_comp_map -from pyomo.common.timing import HierarchicalTimer - - -def create_subsystem_block( - constraints, variables=None, include_fixed=False, timer=None -): +def create_subsystem_block(constraints, variables=None, include_fixed=False): """This function creates a block to serve as a subsystem with the specified variables and constraints. To satisfy certain writers, other variables that appear in the constraints must be added to the block as @@ -130,36 +125,24 @@ def create_subsystem_block( as well as other variables present in the constraints """ - if timer is None: - timer = HierarchicalTimer() if variables is None: variables = [] - timer.start("block") block = Block(concrete=True) - timer.stop("block") - timer.start("reference") block.vars = Reference(variables) block.cons = Reference(constraints) - timer.stop("reference") var_set = ComponentSet(variables) input_vars = [] - timer.start("identify-vars") for con in constraints: for var in identify_variables(con.expr, include_fixed=include_fixed): if var not in var_set: input_vars.append(var) var_set.add(var) - timer.stop("identify-vars") - timer.start("reference") block.input_vars = Reference(input_vars) - timer.stop("reference") - timer.start("external-fcns") add_local_external_functions(block) - timer.stop("external-fcns") return block -def generate_subsystem_blocks(subsystems, include_fixed=False, timer=None): +def generate_subsystem_blocks(subsystems, include_fixed=False): """Generates blocks that contain subsystems of variables and constraints. Arguments @@ -178,10 +161,8 @@ def generate_subsystem_blocks(subsystems, include_fixed=False, timer=None): not specified are contained in the input_vars component. """ - if timer is None: - timer = HierarchicalTimer() for cons, vars in subsystems: - block = create_subsystem_block(cons, vars, include_fixed, timer=timer) + block = create_subsystem_block(cons, vars, include_fixed) yield block, list(block.input_vars.values()) From 40debe7f52d4370440824f43ef26c550a30f8713 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 14:40:32 -0600 Subject: [PATCH 0918/3044] remove hierarchical timer usage from scc_solver module --- .../contrib/incidence_analysis/scc_solver.py | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index eff4f5ae5fa..8c38333e058 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -11,7 +11,6 @@ import logging -from pyomo.common.timing import HierarchicalTimer from pyomo.core.base.constraint import Constraint from pyomo.util.calc_var_value import calculate_variable_from_constraint from pyomo.util.subsystems import TemporarySubsystemManager, generate_subsystem_blocks @@ -26,7 +25,7 @@ def generate_strongly_connected_components( - constraints, variables=None, include_fixed=False, igraph=None, timer=None + constraints, variables=None, include_fixed=False, igraph=None ): """Yield in order ``_BlockData`` that each contain the variables and constraints of a single diagonal block in a block lower triangularization @@ -58,10 +57,7 @@ def generate_strongly_connected_components( "input variables" for that block. """ - if timer is None: - timer = HierarchicalTimer() if variables is None: - timer.start("generate-vars") variables = list( _generate_variables_in_constraints( constraints, @@ -69,30 +65,23 @@ def generate_strongly_connected_components( method=IncidenceMethod.ampl_repn, ) ) - timer.stop("generate-vars") assert len(variables) == len(constraints) if igraph is None: igraph = IncidenceGraphInterface() - timer.start("block-triang") var_blocks, con_blocks = igraph.block_triangularize( variables=variables, constraints=constraints ) - timer.stop("block-triang") subsets = [(cblock, vblock) for vblock, cblock in zip(var_blocks, con_blocks)] - timer.start("generate-block") for block, inputs in generate_subsystem_blocks( - subsets, include_fixed=include_fixed, timer=timer + subsets, include_fixed=include_fixed ): - timer.stop("generate-block") # TODO: How does len scale for reference-to-list? assert len(block.vars) == len(block.cons) yield (block, inputs) # Note that this code, after the last yield, I believe is only called # at time of GC. - timer.start("generate-block") - timer.stop("generate-block") def solve_strongly_connected_components( @@ -102,7 +91,6 @@ def solve_strongly_connected_components( solve_kwds=None, use_calc_var=True, calc_var_kwds=None, - timer=None, ): """Solve a square system of variables and equality constraints by solving strongly connected components individually. @@ -142,10 +130,7 @@ def solve_strongly_connected_components( solve_kwds = {} if calc_var_kwds is None: calc_var_kwds = {} - if timer is None: - timer = HierarchicalTimer() - timer.start("igraph") igraph = IncidenceGraphInterface( block, active=True, @@ -153,27 +138,22 @@ def solve_strongly_connected_components( include_inequality=False, method=IncidenceMethod.ampl_repn, ) - timer.stop("igraph") constraints = igraph.constraints variables = igraph.variables res_list = [] log_blocks = _log.isEnabledFor(logging.DEBUG) - timer.start("generate-scc") for scc, inputs in generate_strongly_connected_components( - constraints, variables, timer=timer, igraph=igraph + constraints, variables, igraph=igraph ): - timer.stop("generate-scc") with TemporarySubsystemManager(to_fix=inputs, remove_bounds_on_fix=True): N = len(scc.vars) if N == 1 and use_calc_var: if log_blocks: _log.debug(f"Solving 1x1 block: {scc.cons[0].name}.") - timer.start("calc-var-from-con") results = calculate_variable_from_constraint( scc.vars[0], scc.cons[0], **calc_var_kwds ) - timer.stop("calc-var-from-con") else: if solver is None: var_names = [var.name for var in scc.vars.values()][:10] @@ -186,10 +166,6 @@ def solve_strongly_connected_components( ) if log_blocks: _log.debug(f"Solving {N}x{N} block.") - timer.start("scc-subsolver") results = solver.solve(scc, **solve_kwds) - timer.stop("scc-subsolver") res_list.append(results) - timer.start("generate-scc") - timer.stop("generate-scc") return res_list From e9e63a00da4014195b6d82549f9f48f034d447b7 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 15:28:58 -0600 Subject: [PATCH 0919/3044] use new variable visitor in get_vars_from_components rather than identify_variables_in_expressions --- pyomo/core/expr/visitor.py | 34 ----------------------------- pyomo/util/vars_from_expressions.py | 31 +++++++++++++++++++------- 2 files changed, 23 insertions(+), 42 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 51864044396..bccf0eda899 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1433,40 +1433,6 @@ def acceptChildResult(self, node, data, child_result, child_idx): return child_result.is_expression_type(), None -def identify_variables_in_components(components, include_fixed=True): - visitor = _StreamVariableVisitor( - include_fixed=include_fixed, descend_into_named_expressions=False - ) - all_variables = [] - for comp in components: - all_variables.extend(visitor.walk_expressions(comp.expr)) - - named_expr_set = set() - unique_named_exprs = [] - for expr in visitor.named_expressions: - if id(expr) in named_expr_set: - named_expr_set.add(id(expr)) - unique_named_exprs.append(expr) - - while unique_named_exprs: - expr = unique_named_exprs.pop() - visitor.named_expressions.clear() - all_variables.extend(visitor.walk_expression(expr.expr)) - - for new_expr in visitor.named_expressions: - if id(new_expr) not in named_expr_set: - named_expr_set.add(new_expr) - unique_named_exprs.append(new_expr) - - unique_vars = [] - var_set = set() - for var in all_variables: - if id(var) not in var_set: - var_set.add(id(var)) - unique_vars.append(var) - return unique_vars - - def identify_variables(expr, include_fixed=True): """ A generator that yields a sequence of variables diff --git a/pyomo/util/vars_from_expressions.py b/pyomo/util/vars_from_expressions.py index f9b3f1ab8ae..1fe614273ab 100644 --- a/pyomo/util/vars_from_expressions.py +++ b/pyomo/util/vars_from_expressions.py @@ -17,7 +17,7 @@ actually in the subtree or not. """ from pyomo.core import Block -import pyomo.core.expr as EXPR +from pyomo.core.expr.visitor import _StreamVariableVisitor def get_vars_from_components( @@ -42,7 +42,10 @@ def get_vars_from_components( descend_into: Ctypes to descend into when finding Constraints descent_order: Traversal strategy for finding the objects of type ctype """ - seen = set() + visitor = _StreamVariableVisitor( + include_fixed=include_fixed, descend_into_named_expressions=False + ) + variables = [] for constraint in block.component_data_objects( ctype, active=active, @@ -50,9 +53,21 @@ def get_vars_from_components( descend_into=descend_into, descent_order=descent_order, ): - for var in EXPR.identify_variables( - constraint.expr, include_fixed=include_fixed - ): - if id(var) not in seen: - seen.add(id(var)) - yield var + variables.extend(visitor.walk_expression(constraint.expr)) + seen_named_exprs = set() + named_expr_stack = list(visitor.named_expressions) + while named_expr_stack: + expr = named_expr_stack.pop() + # Clear visitor's named expression cache so we only identify new + # named expressions + visitor.named_expressions.clear() + variables.extend(visitor.walk_expression(expr.expr)) + for new_expr in visitor.named_expressions: + if id(new_expr) not in seen_named_exprs: + seen_named_exprs.add(id(new_expr)) + named_expr_stack.append(new_expr) + seen = set() + for var in variables: + if id(var) not in seen: + seen.add(id(var)) + yield var From 38c78a329b282b12a50be1df5055f2d1b6978a59 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 15:31:24 -0600 Subject: [PATCH 0920/3044] remove unnecessary walker callbacks --- pyomo/core/expr/visitor.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index bccf0eda899..3c1e486d38f 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1424,14 +1424,6 @@ def exitNode(self, node, data): def finalizeResult(self, result): return self._variables - def enterNode(self, node): - pass - - def acceptChildResult(self, node, data, child_result, child_idx): - if child_result.__class__ in native_types: - return False, None - return child_result.is_expression_type(), None - def identify_variables(expr, include_fixed=True): """ From 0e9af931d9e4149728bc192bddc0aaf65d772168 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 18:03:43 -0600 Subject: [PATCH 0921/3044] fix typos in test --- pyomo/util/tests/test_subsystems.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/util/tests/test_subsystems.py b/pyomo/util/tests/test_subsystems.py index 05b1bf9f8f4..fe093b4723f 100644 --- a/pyomo/util/tests/test_subsystems.py +++ b/pyomo/util/tests/test_subsystems.py @@ -341,11 +341,11 @@ def test_identify_external_functions(self): @unittest.skipUnless(find_GSL(), "Could not find the AMPL GSL library") def test_local_external_functions_with_named_expressions(self): m = self._make_model_with_external_functions(named_expressions=True) - variables = list(pyo.component_data_objects(pyo.Var)) - constraints = list(pyo.component_data_objects(pyo.Constraint, active=True)) + variables = list(m.component_data_objects(pyo.Var)) + constraints = list(m.component_data_objects(pyo.Constraint, active=True)) b = create_subsystem_block(constraints, variables) - self.assertTrue(isinstance(m._gsl_sf_bessel_J0, pyo.ExternalFunction)) - self.assertTrue(isinstance(m._gsl_sf_fermi_dirac_m1, pyo.ExternalFunction)) + self.assertTrue(isinstance(b._gsl_sf_bessel_J0, pyo.ExternalFunction)) + self.assertTrue(isinstance(b._gsl_sf_fermi_dirac_m1, pyo.ExternalFunction)) def _solve_ef_model_with_ipopt(self): m = self._make_model_with_external_functions() From 016d6d1b4536e6147984e792a23dbb0590dc5439 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 18:06:44 -0600 Subject: [PATCH 0922/3044] function args on single line --- pyomo/contrib/incidence_analysis/scc_solver.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 8c38333e058..aa59b698ce9 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -85,12 +85,7 @@ def generate_strongly_connected_components( def solve_strongly_connected_components( - block, - *, - solver=None, - solve_kwds=None, - use_calc_var=True, - calc_var_kwds=None, + block, *, solver=None, solve_kwds=None, use_calc_var=True, calc_var_kwds=None ): """Solve a square system of variables and equality constraints by solving strongly connected components individually. From f32bd2e26ca0f2ecc6ea8d883b40851a4dcd2a4a Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 18:15:38 -0600 Subject: [PATCH 0923/3044] method args on single line --- pyomo/core/expr/visitor.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 3c1e486d38f..3d5608bda4c 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1389,11 +1389,7 @@ def visit(self, node): class _StreamVariableVisitor(StreamBasedExpressionVisitor): - def __init__( - self, - include_fixed=False, - descend_into_named_expressions=True, - ): + def __init__(self, include_fixed=False, descend_into_named_expressions=True): self._include_fixed = include_fixed self._descend_into_named_expressions = descend_into_named_expressions self.named_expressions = [] From 87644ca8d4d9d31e05809b0492ade63b90e8c184 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 19:42:51 -0600 Subject: [PATCH 0924/3044] update condition for skipping named expression in variable visitor --- pyomo/core/expr/visitor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 3d5608bda4c..b31fa20f77c 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1401,9 +1401,10 @@ def initializeWalker(self, expr): return True, None def beforeChild(self, parent, child, index): - if ( + if child.__class__ in native_types: + return False, None + elif ( not self._descend_into_named_expressions - and isinstance(child, NumericValue) and child.is_named_expression_type() ): self.named_expressions.append(child) From e6e7259a258d560d633e8b2dcf5d93ae406d2337 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 11 Mar 2024 19:47:41 -0600 Subject: [PATCH 0925/3044] super.__init__ call in variable visitor --- pyomo/core/expr/visitor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index b31fa20f77c..befdef0be71 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1390,6 +1390,7 @@ def visit(self, node): class _StreamVariableVisitor(StreamBasedExpressionVisitor): def __init__(self, include_fixed=False, descend_into_named_expressions=True): + super().__init__() self._include_fixed = include_fixed self._descend_into_named_expressions = descend_into_named_expressions self.named_expressions = [] From db18d03e5f4403d014f6fdaaa5a4ca494fee28c5 Mon Sep 17 00:00:00 2001 From: "Philipp Christophel (phchri)" Date: Tue, 12 Mar 2024 05:43:11 -0400 Subject: [PATCH 0926/3044] Add session options, fix non-optimal return codes and version checking --- pyomo/solvers/plugins/solvers/SAS.py | 180 +++++++++++-------------- pyomo/solvers/tests/checks/test_SAS.py | 17 +-- 2 files changed, 87 insertions(+), 110 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/SAS.py b/pyomo/solvers/plugins/solvers/SAS.py index f5840b5d6f3..87ee31a08af 100644 --- a/pyomo/solvers/plugins/solvers/SAS.py +++ b/pyomo/solvers/plugins/solvers/SAS.py @@ -96,23 +96,6 @@ } -CAS_OPTION_NAMES = [ - "hostname", - "port", - "username", - "password", - "session", - "locale", - "name", - "nworkers", - "authinfo", - "protocol", - "path", - "ssl_ca_list", - "authcode", -] - - @SolverFactory.register("sas", doc="The SAS LP/MIP solver") class SAS(OptSolver): """The SAS optimization solver""" @@ -120,7 +103,7 @@ class SAS(OptSolver): def __new__(cls, *args, **kwds): mode = kwds.pop("solver_io", None) if mode != None: - return SolverFactory(mode) + return SolverFactory(mode, **kwds) else: # Choose solver factory automatically # based on what can be loaded. @@ -216,6 +199,7 @@ def _create_results_from_status(self, status, solution_status): results = SolverResults() results.solver.name = "SAS" results.solver.status = STATUS_TO_SOLVERSTATUS[status] + results.solver.hasSolution = False if results.solver.status == SolverStatus.ok: results.solver.termination_condition = SOLSTATUS_TO_TERMINATIONCOND[ solution_status @@ -226,11 +210,14 @@ def _create_results_from_status(self, status, solution_status): results.solver.status = TerminationCondition.to_solver_status( results.solver.termination_condition ) + if "OPTIMAL" in solution_status or "_SOL" in solution_status: + results.solver.hasSolution = True elif results.solver.status == SolverStatus.aborted: results.solver.termination_condition = TerminationCondition.userInterrupt - results.solver.message = ( - results.solver.termination_message - ) = SOLSTATUS_TO_MESSAGE["ABORTED"] + if solution_status != "ERROR": + results.solver.message = ( + results.solver.termination_message + ) = SOLSTATUS_TO_MESSAGE[solution_status] else: results.solver.termination_condition = TerminationCondition.error results.solver.message = ( @@ -288,6 +275,9 @@ def __init__(self, **kwds): # Create the session only as its needed self._sas_session = None + # Store other options for the SAS session + self._session_options = kwds + def __del__(self): # Close the session, if we created one if self._sas_session: @@ -326,10 +316,6 @@ def _apply_solver(self): # Check if there are integer variables, this might be slow proc = "OPTMILP" if self._has_integer_variables() else "OPTLP" - # Remove CAS options in case they were specified - for opt in CAS_OPTION_NAMES: - self.options.pop(opt, None) - # Get the rootnode options decomp_str = self._create_statement_str("decomp") decompmaster_str = self._create_statement_str("decompmaster") @@ -373,9 +359,7 @@ def _apply_solver(self): sas_options = "option notes nonumber nodate nosource pagesize=max;" # Get the current SAS session, submit the code and return the results - sas = self._sas_session - if sas == None: - sas = self._sas_session = self._sas.SASsession() + sas = self._sas_session = self._sas.SASsession(**self._session_options) # Find the version of 9.4 we are using self._sasver = sas.sasver @@ -396,12 +380,13 @@ def _apply_solver(self): upload_pin = True # Using a function call to make it easier to moch the version check - version = self.sas_version().split("M", 1)[1][0] - if int(version) < 5: + major_version = self.sas_version()[0] + minor_version = self.sas_version().split("M", 1)[1][0] + if major_version == "9" and int(minor_version) < 5: raise NotImplementedError( "Support for SAS 9.4 M4 and earlier is no implemented." ) - elif int(version) == 5: + elif major_version == "9" and int(minor_version) == 5: # In 9.4M5 we have to create an MPS data set from an MPS file first # Earlier versions will not work because the MPS format in incompatible mps_dataset_name = "mps" + unique @@ -436,7 +421,7 @@ def _apply_solver(self): ) sas.sasdata(mps_dataset_name).delete(quiet=True) else: - # Since 9.4M6+ optlp/optmilp can read mps files directly + # Since 9.4M6+ optlp/optmilp can read mps files directly (this includes Viya-based local installs) res = sas.submit( """ {sas_options} @@ -512,12 +497,12 @@ def _apply_solver(self): results.problem.sense = ProblemSense.minimize # Prepare the solution information - if results.solver.termination_condition == TerminationCondition.optimal: + if results.solver.hasSolution: sol = results.solution.add() # Store status in solution sol.status = SolutionStatus.feasible - sol.termination_condition = TerminationCondition.optimal + sol.termination_condition = SOLSTATUS_TO_TERMINATIONCOND[self._macro.get("SOLUTION_STATUS", "ERROR")] # Store objective value in solution sol.objective["__default_objective__"] = {"Value": self._macro["OBJECTIVE"]} @@ -606,6 +591,7 @@ def __init__(self, **kwds): # Create the session only as its needed self._sas_session = None + self._session_options = kwds def __del__(self): # Close the session, if we created one @@ -619,13 +605,6 @@ def _apply_solver(self): # Set return code to issue an error if we get interrupted self._rc = -1 - # Extract CAS connection options - cas_opts = {} - for opt in CAS_OPTION_NAMES: - val = self.options.pop(opt, None) - if val != None: - cas_opts[opt] = val - # Figure out if the problem has integer variables with_opt = self.options.pop("with", None) if with_opt == "lp": @@ -643,7 +622,7 @@ def _apply_solver(self): with redirect_stdout(SASLogWriter(self._tee)) as self._log_writer: s = self._sas_session if s == None: - s = self._sas_session = self._sas.CAS(**cas_opts) + s = self._sas_session = self._sas.CAS(**self._session_options) try: # Load the optimization action set s.loadactionset("optimization") @@ -651,8 +630,9 @@ def _apply_solver(self): # Declare a unique table name for the mps table mpsdata_table_name = "mps" + unique - # Upload mps file to CAS - if stat(self._problem_files[0]).st_size >= 2 * 1024**3: + # Upload mps file to CAS, if the file is larger than 2 GB, we need to use convertMps instead of loadMps + # Note that technically it is 2 Gibibytes file size that trigger the issue, but 2 GB is the safer threshold + if stat(self._problem_files[0]).st_size > 2E9: # For files larger than 2 GB (this is a limitation of the loadMps action used in the else part). # Use convertMPS, first create file for upload. mpsWithIdFileName = TempfileManager.create_tempfile( @@ -731,62 +711,64 @@ def _apply_solver(self): r.get("status", "ERROR"), r.get("solutionStatus", "ERROR") ) - if r.ProblemSummary["cValue1"][1] == "Maximization": - results.problem.sense = ProblemSense.maximize - else: - results.problem.sense = ProblemSense.minimize - - # Prepare the solution information - if ( - results.solver.termination_condition - == TerminationCondition.optimal - ): - sol = results.solution.add() - - # Store status in solution - sol.status = SolutionStatus.feasible - sol.termination_condition = TerminationCondition.optimal - - # Store objective value in solution - sol.objective["__default_objective__"] = { - "Value": r["objective"] - } - - if action == "solveMilp": - primal_out = s.CASTable(name=primalout_table_name) - # Use pandas functions for efficiency - primal_out = primal_out[["_VAR_", "_VALUE_"]] - sol.variable = {} - for row in primal_out.itertuples(index=False): - sol.variable[row[0]] = {"Value": row[1]} + if results.solver.status != SolverStatus.error: + if r.ProblemSummary["cValue1"][1] == "Maximization": + results.problem.sense = ProblemSense.maximize else: - # Convert primal out data set to variable dictionary - # Use panda functions for efficiency - primal_out = s.CASTable(name=primalout_table_name) - primal_out = primal_out[ - ["_VAR_", "_VALUE_", "_STATUS_", "_R_COST_"] - ] - sol.variable = {} - for row in primal_out.itertuples(index=False): - sol.variable[row[0]] = { - "Value": row[1], - "Status": row[2], - "rc": row[3], - } - - # Convert dual out data set to constraint dictionary - # Use pandas functions for efficiency - dual_out = s.CASTable(name=dualout_table_name) - dual_out = dual_out[ - ["_ROW_", "_VALUE_", "_STATUS_", "_ACTIVITY_"] - ] - sol.constraint = {} - for row in dual_out.itertuples(index=False): - sol.constraint[row[0]] = { - "dual": row[1], - "Status": row[2], - "slack": row[3], - } + results.problem.sense = ProblemSense.minimize + + # Prepare the solution information + if results.solver.hasSolution: + sol = results.solution.add() + + # Store status in solution + sol.status = SolutionStatus.feasible + sol.termination_condition = SOLSTATUS_TO_TERMINATIONCOND[r.get("solutionStatus", "ERROR")] + + # Store objective value in solution + sol.objective["__default_objective__"] = { + "Value": r["objective"] + } + + if action == "solveMilp": + primal_out = s.CASTable(name=primalout_table_name) + # Use pandas functions for efficiency + primal_out = primal_out[["_VAR_", "_VALUE_"]] + sol.variable = {} + for row in primal_out.itertuples(index=False): + sol.variable[row[0]] = {"Value": row[1]} + else: + # Convert primal out data set to variable dictionary + # Use panda functions for efficiency + primal_out = s.CASTable(name=primalout_table_name) + primal_out = primal_out[ + ["_VAR_", "_VALUE_", "_STATUS_", "_R_COST_"] + ] + sol.variable = {} + for row in primal_out.itertuples(index=False): + sol.variable[row[0]] = { + "Value": row[1], + "Status": row[2], + "rc": row[3], + } + + # Convert dual out data set to constraint dictionary + # Use pandas functions for efficiency + dual_out = s.CASTable(name=dualout_table_name) + dual_out = dual_out[ + ["_ROW_", "_VALUE_", "_STATUS_", "_ACTIVITY_"] + ] + sol.constraint = {} + for row in dual_out.itertuples(index=False): + sol.constraint[row[0]] = { + "dual": row[1], + "Status": row[2], + "slack": row[3], + } + else: + raise ValueError( + "The SAS solver returned an error status." + ) else: results = self.results = SolverResults() results.solver.name = "SAS" diff --git a/pyomo/solvers/tests/checks/test_SAS.py b/pyomo/solvers/tests/checks/test_SAS.py index 1a6bbd80f1d..7b0e2cccd9a 100644 --- a/pyomo/solvers/tests/checks/test_SAS.py +++ b/pyomo/solvers/tests/checks/test_SAS.py @@ -30,11 +30,11 @@ class SASTestAbc: solver_io = "_sas94" - base_options = {} + session_options = {} @classmethod def setUpClass(cls): - cls.opt_sas = SolverFactory("sas", solver_io=cls.solver_io) + cls.opt_sas = SolverFactory("sas", solver_io=cls.solver_io, **cls.session_options) @classmethod def tearDownClass(cls): @@ -73,11 +73,6 @@ def run_solver(self, **kwargs): opt_sas = self.opt_sas instance = self.instance - # Add base options for connection data etc. - options = kwargs.get("options", {}) - if self.base_options: - kwargs["options"] = {**options, **self.base_options} - # Call the solver self.results = opt_sas.solve(instance, **kwargs) @@ -285,7 +280,7 @@ def test_solver_with_milp(self): class SASTestLP94(SASTestLP, unittest.TestCase): @mock.patch( "pyomo.solvers.plugins.solvers.SAS.SAS94.sas_version", - return_value="2sd45s39M4234232", + return_value="9.sd45s39M4234232", ) def test_solver_versionM4(self, sas): with self.assertRaises(NotImplementedError): @@ -293,7 +288,7 @@ def test_solver_versionM4(self, sas): @mock.patch( "pyomo.solvers.plugins.solvers.SAS.SAS94.sas_version", - return_value="234897293M5324u98", + return_value="9.34897293M5324u98", ) def test_solver_versionM5(self, sas): self.run_solver() @@ -317,7 +312,7 @@ def test_solver_error(self, submit_mock, symget_mock): @unittest.skipIf(not sas_available, "The SAS solver is not available") class SASTestLPCAS(SASTestLP, unittest.TestCase): solver_io = "_sascas" - base_options = CAS_OPTIONS + session_options = CAS_OPTIONS @mock.patch("pyomo.solvers.plugins.solvers.SAS.stat") def test_solver_large_file(self, os_stat): @@ -522,7 +517,7 @@ class SASTestMILP94(SASTestMILP, unittest.TestCase): @unittest.skipIf(not sas_available, "The SAS solver is not available") class SASTestMILPCAS(SASTestMILP, unittest.TestCase): solver_io = "_sascas" - base_options = CAS_OPTIONS + session_options = CAS_OPTIONS if __name__ == "__main__": From e17a2e5b77d33c6d63e2836667de97dd854d3c41 Mon Sep 17 00:00:00 2001 From: "Philipp Christophel (phchri)" Date: Tue, 12 Mar 2024 07:26:58 -0400 Subject: [PATCH 0927/3044] Black formatting --- pyomo/solvers/plugins/solvers/SAS.py | 32 ++++++++++++++------------ pyomo/solvers/tests/checks/test_SAS.py | 4 +++- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/SAS.py b/pyomo/solvers/plugins/solvers/SAS.py index 87ee31a08af..bd06f6a1ef7 100644 --- a/pyomo/solvers/plugins/solvers/SAS.py +++ b/pyomo/solvers/plugins/solvers/SAS.py @@ -204,9 +204,9 @@ def _create_results_from_status(self, status, solution_status): results.solver.termination_condition = SOLSTATUS_TO_TERMINATIONCOND[ solution_status ] - results.solver.message = ( - results.solver.termination_message - ) = SOLSTATUS_TO_MESSAGE[solution_status] + results.solver.message = results.solver.termination_message = ( + SOLSTATUS_TO_MESSAGE[solution_status] + ) results.solver.status = TerminationCondition.to_solver_status( results.solver.termination_condition ) @@ -215,14 +215,14 @@ def _create_results_from_status(self, status, solution_status): elif results.solver.status == SolverStatus.aborted: results.solver.termination_condition = TerminationCondition.userInterrupt if solution_status != "ERROR": - results.solver.message = ( - results.solver.termination_message - ) = SOLSTATUS_TO_MESSAGE[solution_status] + results.solver.message = results.solver.termination_message = ( + SOLSTATUS_TO_MESSAGE[solution_status] + ) else: results.solver.termination_condition = TerminationCondition.error - results.solver.message = ( - results.solver.termination_message - ) = SOLSTATUS_TO_MESSAGE["FAILED"] + results.solver.message = results.solver.termination_message = ( + SOLSTATUS_TO_MESSAGE["FAILED"] + ) return results @abstractmethod @@ -502,7 +502,9 @@ def _apply_solver(self): # Store status in solution sol.status = SolutionStatus.feasible - sol.termination_condition = SOLSTATUS_TO_TERMINATIONCOND[self._macro.get("SOLUTION_STATUS", "ERROR")] + sol.termination_condition = SOLSTATUS_TO_TERMINATIONCOND[ + self._macro.get("SOLUTION_STATUS", "ERROR") + ] # Store objective value in solution sol.objective["__default_objective__"] = {"Value": self._macro["OBJECTIVE"]} @@ -632,7 +634,7 @@ def _apply_solver(self): # Upload mps file to CAS, if the file is larger than 2 GB, we need to use convertMps instead of loadMps # Note that technically it is 2 Gibibytes file size that trigger the issue, but 2 GB is the safer threshold - if stat(self._problem_files[0]).st_size > 2E9: + if stat(self._problem_files[0]).st_size > 2e9: # For files larger than 2 GB (this is a limitation of the loadMps action used in the else part). # Use convertMPS, first create file for upload. mpsWithIdFileName = TempfileManager.create_tempfile( @@ -723,7 +725,9 @@ def _apply_solver(self): # Store status in solution sol.status = SolutionStatus.feasible - sol.termination_condition = SOLSTATUS_TO_TERMINATIONCOND[r.get("solutionStatus", "ERROR")] + sol.termination_condition = SOLSTATUS_TO_TERMINATIONCOND[ + r.get("solutionStatus", "ERROR") + ] # Store objective value in solution sol.objective["__default_objective__"] = { @@ -766,9 +770,7 @@ def _apply_solver(self): "slack": row[3], } else: - raise ValueError( - "The SAS solver returned an error status." - ) + raise ValueError("The SAS solver returned an error status.") else: results = self.results = SolverResults() results.solver.name = "SAS" diff --git a/pyomo/solvers/tests/checks/test_SAS.py b/pyomo/solvers/tests/checks/test_SAS.py index 7b0e2cccd9a..922209ef88b 100644 --- a/pyomo/solvers/tests/checks/test_SAS.py +++ b/pyomo/solvers/tests/checks/test_SAS.py @@ -34,7 +34,9 @@ class SASTestAbc: @classmethod def setUpClass(cls): - cls.opt_sas = SolverFactory("sas", solver_io=cls.solver_io, **cls.session_options) + cls.opt_sas = SolverFactory( + "sas", solver_io=cls.solver_io, **cls.session_options + ) @classmethod def tearDownClass(cls): From 24644ff06414f689937551f204eea7fc81cef7b2 Mon Sep 17 00:00:00 2001 From: robbybp Date: Tue, 12 Mar 2024 10:30:19 -0600 Subject: [PATCH 0928/3044] remove outdated comment --- pyomo/contrib/incidence_analysis/scc_solver.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index aa59b698ce9..0c59fe8703e 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -80,8 +80,6 @@ def generate_strongly_connected_components( # TODO: How does len scale for reference-to-list? assert len(block.vars) == len(block.cons) yield (block, inputs) - # Note that this code, after the last yield, I believe is only called - # at time of GC. def solve_strongly_connected_components( From d12d69b4b1a409ef7450ae5f22229d19d3401dc1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 12 Mar 2024 13:39:55 -0600 Subject: [PATCH 0929/3044] Temporarily pinning to highspy pre-release for testing --- .github/workflows/test_pr_and_main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 76ec6de951a..a2060240391 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -605,7 +605,8 @@ jobs: if: ${{ ! matrix.slim }} shell: bash run: | - $PYTHON_EXE -m pip install --cache-dir cache/pip highspy \ + echo "NOTE: temporarily pinning to highspy pre-release for testing" + $PYTHON_EXE -m pip install --cache-dir cache/pip highspy==1.7.1.dev1 \ || echo "WARNING: highspy is not available" - name: Set up coverage tracking From de294ee874602cf1c94615dfa2a49ed162439464 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 12 Mar 2024 15:39:12 -0600 Subject: [PATCH 0930/3044] fix variable assignment --- pyomo/util/tests/test_subsystems.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyomo/util/tests/test_subsystems.py b/pyomo/util/tests/test_subsystems.py index fe093b4723f..089888bd6a9 100644 --- a/pyomo/util/tests/test_subsystems.py +++ b/pyomo/util/tests/test_subsystems.py @@ -302,9 +302,12 @@ def _make_model_with_external_functions(self, named_expressions=False): m.v3 = pyo.Var(initialize=3.0) if named_expressions: m.subexpr = pyo.Expression(pyo.PositiveIntegers) - subexpr1 = m.subexpr[1] = 2 * m.fermi(m.v1) - subexpr2 = m.subexpr[2] = m.bessel(m.v1) - m.bessel(m.v2) - subexpr3 = m.subexpr[3] = subexpr2 + m.v3**2 + m.subexpr[1] = 2 * m.fermi(m.v1) + m.subexpr[2] = m.bessel(m.v1) - m.bessel(m.v2) + m.subexpr[3] = m.subexpr[2] + m.v3**2 + subexpr1 = m.subexpr[1] + subexpr2 = m.subexpr[2] + subexpr3 = m.subexpr[3] else: subexpr1 = 2 * m.fermi(m.v1) subexpr2 = m.bessel(m.v1) - m.bessel(m.v2) From 8371c8ae548aa57e80ec55fa69a3d4d7220bebb0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 12 Mar 2024 16:18:57 -0600 Subject: [PATCH 0931/3044] Catch when NLv2 presolve identifies/removes independent linear subsystems --- pyomo/repn/plugins/nl_writer.py | 32 ++++++++++++- pyomo/repn/tests/ampl/test_nlv2.py | 72 ++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index b82d4df77e2..2db3e66cbb6 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1081,9 +1081,37 @@ def write(self, model): # Update any eliminated variables to point to the (potentially # scaled) substituted variables - for _id, expr_info in eliminated_vars.items(): + for _id, expr_info in list(eliminated_vars.items()): nl, args, _ = expr_info.compile_repn(visitor) - _vmap[_id] = nl.rstrip() % tuple(_vmap[_id] for _id in args) + for _i in args: + # It is possible that the eliminated variable could + # reference another variable that is no longer part of + # the model and therefore does not have a _vmap entry. + # This can happen when there is an underdetermined + # independent linear subsystem and the presolve removed + # all the constraints from the subsystem. Because the + # free variables in the subsystem are not referenced + # anywhere else in the model, they are not part of the + # `varaibles` list. Implicitly "fix" it to an arbitrary + # valid value from the presolved domain (see #3192). + if _i not in _vmap: + lb, ub = var_bounds[_i] + if lb is None: + lb = -inf + if ub is None: + ub = inf + if lb <= 0 <= ub: + val = 0 + else: + val = lb if abs(lb) < abs(ub) else ub + eliminated_vars[_i] = AMPLRepn(val, {}, None) + _vmap[_i] = expr_info.compile_repn(visitor)[0] + logger.warning( + "presolve identified an underdetermined independent " + "linear subsystem that was removed from the model. " + f"Setting '{var_map[_i]}' == {val}" + ) + _vmap[_id] = nl.rstrip() % tuple(_vmap[_i] for _i in args) r_lines = [None] * n_cons for idx, (con, expr_info, lb, ub) in enumerate(constraints): diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 86eb43d9a37..be72025edcd 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -1812,6 +1812,78 @@ def test_presolve_zero_coef(self): ) ) + def test_presolve_independent_subsystem(self): + # This is derived from the example in #3192 + m = ConcreteModel() + m.x = Var() + m.y = Var() + m.z = Var() + m.d = Constraint(expr=m.z == m.y) + m.c = Constraint(expr=m.y == m.x) + m.o = Objective(expr=0) + + ref = """g3 1 1 0 #problem unknown + 0 0 1 0 0 #vars, constraints, objectives, ranges, eqns + 0 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 0 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 0 0 #nonzeros in Jacobian, obj. gradient + 1 0 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +O0 0 #o +n0 +x0 #initial guess +r #0 ranges (rhs's) +b #0 bounds (on variables) +k-1 #intermediate Jacobian column lengths +""" + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual( + LOG.getvalue(), + "presolve identified an underdetermined independent linear subsystem " + "that was removed from the model. Setting 'z' == 0\n", + ) + + self.assertEqual(*nl_diff(ref, OUT.getvalue())) + + m.x.lb = 5.0 + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual( + LOG.getvalue(), + "presolve identified an underdetermined independent linear subsystem " + "that was removed from the model. Setting 'z' == 5.0\n", + ) + + self.assertEqual(*nl_diff(ref, OUT.getvalue())) + + m.x.lb = -5.0 + m.z.ub = -2.0 + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual( + LOG.getvalue(), + "presolve identified an underdetermined independent linear subsystem " + "that was removed from the model. Setting 'z' == -2.0\n", + ) + + self.assertEqual(*nl_diff(ref, OUT.getvalue())) + def test_scaling(self): m = pyo.ConcreteModel() m.x = pyo.Var(initialize=0) From b3a4b06e9bd1cfb3c3cecc9a4b1a85c262cfbdd3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 12 Mar 2024 16:23:05 -0600 Subject: [PATCH 0932/3044] NFC: fix typo --- pyomo/repn/plugins/nl_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 2db3e66cbb6..ee5b65149ae 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1092,7 +1092,7 @@ def write(self, model): # all the constraints from the subsystem. Because the # free variables in the subsystem are not referenced # anywhere else in the model, they are not part of the - # `varaibles` list. Implicitly "fix" it to an arbitrary + # `variables` list. Implicitly "fix" it to an arbitrary # valid value from the presolved domain (see #3192). if _i not in _vmap: lb, ub = var_bounds[_i] From 440bf86c9cb9fabff38daa5d27cbdd95d18be1f4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 13 Mar 2024 13:12:58 -0600 Subject: [PATCH 0933/3044] Minor logic reordering to make the intent more clear --- pyomo/repn/util.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index a51ee1c6d64..1b3056738bf 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -400,14 +400,15 @@ def __init__(self, *args, **kwargs): def __missing__(self, key): if type(key) is tuple: - node_class = key[0] - node_args = key[1:] # Only lookup/cache argument-specific handlers for unary, # binary and ternary operators - if len(key) > 3: - key = node_class - if key in self: - return self[key] + if len(key) <= 3: + node_class = key[0] + node_args = key[1:] + else: + node_class = key = key[0] + if node_class in self: + return self[node_class] else: node_class = key bases = node_class.__mro__ @@ -446,7 +447,7 @@ def __missing__(self, key): def unexpected_expression_type(self, visitor, node, *args): raise DeveloperError( f"Unexpected expression node type '{type(node).__name__}' " - f"found while walking expression tree in {type(self).__name__}." + f"found while walking expression tree in {type(visitor).__name__}." ) From c2b42b5c99726340466bb9488c50076f494c7eba Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 13 Mar 2024 13:43:43 -0600 Subject: [PATCH 0934/3044] Initializing dual transform --- pyomo/core/plugins/transform/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/core/plugins/transform/__init__.py b/pyomo/core/plugins/transform/__init__.py index 21e762047ca..7bf3e3229c2 100644 --- a/pyomo/core/plugins/transform/__init__.py +++ b/pyomo/core/plugins/transform/__init__.py @@ -24,3 +24,4 @@ import pyomo.core.plugins.transform.add_slack_vars import pyomo.core.plugins.transform.scaling import pyomo.core.plugins.transform.logical_to_linear +import pyomo.core.plugins.transform.lp_dual From 8fccf370042040cfbd0927278cf27895c60ced64 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 13 Mar 2024 13:45:26 -0600 Subject: [PATCH 0935/3044] Whoops, adding LP dual transform and tests --- pyomo/core/plugins/transform/lp_dual.py | 65 +++++++++++++++++++++++++ pyomo/core/tests/unit/test_lp_dual.py | 65 +++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 pyomo/core/plugins/transform/lp_dual.py create mode 100644 pyomo/core/tests/unit/test_lp_dual.py diff --git a/pyomo/core/plugins/transform/lp_dual.py b/pyomo/core/plugins/transform/lp_dual.py new file mode 100644 index 00000000000..88182e30d7f --- /dev/null +++ b/pyomo/core/plugins/transform/lp_dual.py @@ -0,0 +1,65 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.autoslots import AutoSlots +from pyomo.common.collections import ComponentMap +from pyomo.common.errors import MouseTrap +from pyomo.common.dependencies import scipy +from pyomo.core import ( + ConcreteModel, Var, Constraint, Objective, TransformationFactory, + NonPositiveReals, maximize +) +from pyomo.opt import WriterFactory + +@TransformationFactory.register( + 'core.lp_dual', 'Generate the linear programming dual of the given model') +class LinearProgrammingDual(object): + def apply_to(self, model, **options): + raise MouseTrap( + "The 'core.lp_dual' transformation does not currently implement " + "apply_to since it is a bit ambiguous what it means to take a dual " + "in place. Please use 'create_using' and do what you wish with the " + "returned model." + ) + + def create_using(self, model, ostream=None, **options): + """Take linear programming dual of a model + + Returns + ------- + ConcreteModel containing linear programming dual + + Parameters + ---------- + model: ConcreteModel + The concrete Pyomo model to take the dual of + + ostream: None + This is provided for API compatibility with other writers + and is ignored here. + + """ + std_form = WriterFactory('compile_standard_form').write(model, + nonnegative_vars=True) + dual = ConcreteModel(name="%s dual" % model.name) + A_transpose = scipy.sparse.csc_matrix.transpose(std_form.A) + rows = range(A_transpose.shape[0]) + cols = range(A_transpose.shape[1]) + dual.x = Var(cols, domain=NonPositiveReals) + dual.constraints = Constraint(rows) + for i in rows: + dual.constraints[i] = sum(A_transpose[i, j]*dual.x[j] for j in cols) <= \ + std_form.c[0, i] + + dual.obj = Objective(expr=sum(std_form.rhs[j]*dual.x[j] for j in cols), + sense=maximize) + + return dual diff --git a/pyomo/core/tests/unit/test_lp_dual.py b/pyomo/core/tests/unit/test_lp_dual.py new file mode 100644 index 00000000000..487ae01d877 --- /dev/null +++ b/pyomo/core/tests/unit/test_lp_dual.py @@ -0,0 +1,65 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import scipy_available +import pyomo.common.unittest as unittest +from pyomo.environ import ( + ConcreteModel, + Constraint, + NonNegativeReals, + NonPositiveReals, + Objective, + Reals, + Suffix, + TerminationCondition, + TransformationFactory, + value, + Var, +) +from pyomo.opt import SolverFactory, WriterFactory + +from pytest import set_trace + +@unittest.skipUnless(scipy_available, "Scipy not available") +class TestLPDual(unittest.TestCase): + @unittest.skipUnless(SolverFactory('gurobi').available(exception_flag=False) and + SolverFactory('gurobi').license_is_valid(), + "Gurobi is not available") + def test_lp_dual_solve(self): + m = ConcreteModel() + m.x = Var(domain=NonNegativeReals) + m.y = Var(domain=NonPositiveReals) + m.z = Var(domain=Reals) + + m.obj = Objective(expr=m.x + 2*m.y - 3*m.z) + m.c1 = Constraint(expr=-4*m.x - 2*m.y - m.z <= -5) + m.c2 = Constraint(expr=m.x + m.y <= 3) + m.c3 = Constraint(expr=- m.y - m.z <= -4.2) + m.c4 = Constraint(expr=m.z <= 42) + m.dual = Suffix(direction=Suffix.IMPORT) + + lp_dual = TransformationFactory('core.lp_dual') + dual = lp_dual.create_using(m) + dual.dual = Suffix(direction=Suffix.IMPORT) + + opt = SolverFactory('gurobi') + results = opt.solve(m) + self.assertEqual(results.solver.termination_condition, + TerminationCondition.optimal) + results = opt.solve(dual) + self.assertEqual(results.solver.termination_condition, + TerminationCondition.optimal) + + self.assertAlmostEqual(value(m.obj), value(dual.obj)) + for idx, cons in enumerate([m.c1, m.c2, m.c3, m.c4]): + self.assertAlmostEqual(value(dual.x[idx]), value(m.dual[cons])) + # for idx, (mult, v) in enumerate([(1, m.x), (-1, m.y), (1, m.z)]): + # self.assertAlmostEqual(mult*value(v), value(dual.dual[dual_cons])) From e1fa2569252d849884da0569c95f8011716d3507 Mon Sep 17 00:00:00 2001 From: robbybp Date: Wed, 13 Mar 2024 23:06:44 -0600 Subject: [PATCH 0936/3044] [WIP] initial attempt at implementing identify_variables with a named expression cache --- pyomo/core/expr/visitor.py | 105 +++++++++++++++++++++------- pyomo/util/vars_from_expressions.py | 63 +++++++++++------ 2 files changed, 122 insertions(+), 46 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index befdef0be71..20d8d72fcbb 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1389,12 +1389,20 @@ def visit(self, node): class _StreamVariableVisitor(StreamBasedExpressionVisitor): - def __init__(self, include_fixed=False, descend_into_named_expressions=True): + def __init__( + self, + include_fixed=False, + #descend_into_named_expressions=True, + named_expression_cache=None, + ): super().__init__() self._include_fixed = include_fixed - self._descend_into_named_expressions = descend_into_named_expressions + #self._descend_into_named_expressions = descend_into_named_expressions self.named_expressions = [] - # Should we allow re-use of this visitor for multiple expressions? + if named_expression_cache is None: + named_expression_cache = {} + self._named_expression_cache = named_expression_cache + self._active_named_expressions = [] def initializeWalker(self, expr): self._variables = [] @@ -1404,12 +1412,26 @@ def initializeWalker(self, expr): def beforeChild(self, parent, child, index): if child.__class__ in native_types: return False, None - elif ( - not self._descend_into_named_expressions - and child.is_named_expression_type() - ): - self.named_expressions.append(child) - return False, None + #elif ( + # not self._descend_into_named_expressions + # and child.is_named_expression_type() + #): + # self.named_expressions.append(child) + # return False, None + elif child.is_named_expression_type(): + if id(child) in self._named_expression_cache: + # We have already encountered this named expression. We just add + # the cached variables to our list and don't descend. + for var in self._named_expression_cache[id(child)][0]: + if id(var) not in self._seen: + self._variables.append(var) + return False, None + else: + # If we are descending into a new named expression, initialize + # a cache to store the expression's local variables. + self._named_expression_cache[id(child)] = ([], set()) + self._active_named_expressions.append(id(child)) + return True, None else: return True, None @@ -1418,12 +1440,35 @@ def exitNode(self, node, data): if id(node) not in self._seen: self._seen.add(id(node)) self._variables.append(node) + if self._active_named_expressions: + # If we are in a named expression, add new variables to the cache. + eid = self._active_named_expressions[-1] + local_vars, local_var_set = self._named_expression_cache[eid] + if id(node) not in local_var_set: + local_var_set.add(id(node)) + local_vars.append(node) + elif node.is_named_expression_type(): + # If we are returning from a named expression, we have at least one + # active named expression. + eid = self._active_named_expressions.pop() + if self._active_named_expressions: + # If we still are in a named expression, we update that expression's + # cache with any new variables encountered. + new_eid = self._active_named_expressions[-1] + old_expr_vars, old_expr_var_set = self._named_expression_cache[eid] + new_expr_vars, new_expr_var_set = self._named_expression_cache[new_eid] + + for var in old_expr_vars: + if id(var) not in new_expr_var_set: + new_expr_var_set.add(id(var)) + new_expr_vars.append(var) def finalizeResult(self, result): return self._variables -def identify_variables(expr, include_fixed=True): +# TODO: descend_into_named_expressions option? +def identify_variables(expr, include_fixed=True, named_expression_cache=None): """ A generator that yields a sequence of variables in an expression tree. @@ -1437,22 +1482,34 @@ def identify_variables(expr, include_fixed=True): Yields: Each variable that is found. """ - visitor = _VariableVisitor() - if include_fixed: - for v in visitor.xbfs_yield_leaves(expr): - if isinstance(v, tuple): - yield from v - else: - yield v + if named_expression_cache is None: + named_expression_cache = {} + + NEW = True + if NEW: + visitor = _StreamVariableVisitor( + named_expression_cache=named_expression_cache, + include_fixed=False, + ) + variables = visitor.walk_expression(expr) + yield from variables else: - for v in visitor.xbfs_yield_leaves(expr): - if isinstance(v, tuple): - for v_i in v: - if not v_i.is_fixed(): - yield v_i - else: - if not v.is_fixed(): + visitor = _VariableVisitor() + if include_fixed: + for v in visitor.xbfs_yield_leaves(expr): + if isinstance(v, tuple): + yield from v + else: yield v + else: + for v in visitor.xbfs_yield_leaves(expr): + if isinstance(v, tuple): + for v_i in v: + if not v_i.is_fixed(): + yield v_i + else: + if not v.is_fixed(): + yield v # ===================================================== diff --git a/pyomo/util/vars_from_expressions.py b/pyomo/util/vars_from_expressions.py index 1fe614273ab..22e6e6dab8d 100644 --- a/pyomo/util/vars_from_expressions.py +++ b/pyomo/util/vars_from_expressions.py @@ -18,6 +18,7 @@ """ from pyomo.core import Block from pyomo.core.expr.visitor import _StreamVariableVisitor +from pyomo.core.expr import identify_variables def get_vars_from_components( @@ -42,10 +43,38 @@ def get_vars_from_components( descend_into: Ctypes to descend into when finding Constraints descent_order: Traversal strategy for finding the objects of type ctype """ - visitor = _StreamVariableVisitor( - include_fixed=include_fixed, descend_into_named_expressions=False - ) - variables = [] + #visitor = _StreamVariableVisitor( + # include_fixed=include_fixed, descend_into_named_expressions=False + #) + #variables = [] + #for constraint in block.component_data_objects( + # ctype, + # active=active, + # sort=sort, + # descend_into=descend_into, + # descent_order=descent_order, + #): + # variables.extend(visitor.walk_expression(constraint.expr)) + # seen_named_exprs = set() + # named_expr_stack = list(visitor.named_expressions) + # while named_expr_stack: + # expr = named_expr_stack.pop() + # # Clear visitor's named expression cache so we only identify new + # # named expressions + # visitor.named_expressions.clear() + # variables.extend(visitor.walk_expression(expr.expr)) + # for new_expr in visitor.named_expressions: + # if id(new_expr) not in seen_named_exprs: + # seen_named_exprs.add(id(new_expr)) + # named_expr_stack.append(new_expr) + #seen = set() + #for var in variables: + # if id(var) not in seen: + # seen.add(id(var)) + # yield var + + seen = set() + named_expression_cache = {} for constraint in block.component_data_objects( ctype, active=active, @@ -53,21 +82,11 @@ def get_vars_from_components( descend_into=descend_into, descent_order=descent_order, ): - variables.extend(visitor.walk_expression(constraint.expr)) - seen_named_exprs = set() - named_expr_stack = list(visitor.named_expressions) - while named_expr_stack: - expr = named_expr_stack.pop() - # Clear visitor's named expression cache so we only identify new - # named expressions - visitor.named_expressions.clear() - variables.extend(visitor.walk_expression(expr.expr)) - for new_expr in visitor.named_expressions: - if id(new_expr) not in seen_named_exprs: - seen_named_exprs.add(id(new_expr)) - named_expr_stack.append(new_expr) - seen = set() - for var in variables: - if id(var) not in seen: - seen.add(id(var)) - yield var + for var in identify_variables( + constraint.expr, + include_fixed=include_fixed, + named_expression_cache=named_expression_cache, + ): + if id(var) not in seen: + seen.add(id(var)) + yield var From 53899315af8ba53ae0c0e22851b7d7894a03ed33 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 14 Mar 2024 10:19:33 -0600 Subject: [PATCH 0937/3044] Add link to the companion notebooks for Hands-on Mathematival Optimization with Python --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 95558e52a42..12c3ce8ed9a 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,8 @@ version, we will remove testing for that Python version. * [Pyomo Workshop Slides](https://github.com/Pyomo/pyomo-tutorials/blob/main/Pyomo-Workshop-December-2023.pdf) * [Prof. Jeffrey Kantor's Pyomo Cookbook](https://jckantor.github.io/ND-Pyomo-Cookbook/) +* The [companion notebooks](https://mobook.github.io/MO-book/intro.html) + for *Hands-On Mathematical Optimization with Python* * [Pyomo Gallery](https://github.com/Pyomo/PyomoGallery) ### Getting Help From cca02a124c4d39fe38ff1e5fe749c089d9cc5f27 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 14 Mar 2024 10:26:48 -0600 Subject: [PATCH 0938/3044] Syncing tutorials/examples list between README and RTD --- doc/OnlineDocs/tutorial_examples.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/tutorial_examples.rst b/doc/OnlineDocs/tutorial_examples.rst index dc58b6a6f59..6a40949ef90 100644 --- a/doc/OnlineDocs/tutorial_examples.rst +++ b/doc/OnlineDocs/tutorial_examples.rst @@ -9,7 +9,9 @@ Additional Pyomo tutorials and examples can be found at the following links: `Prof. Jeffrey Kantor's Pyomo Cookbook `_ -`Pyomo Gallery -`_ +The `companion notebooks `_ +for *Hands-On Mathematical Optimization with Python* + +`Pyomo Gallery `_ From 4eb2fac76b152f836948392e3d595bbaebbd291d Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 14 Mar 2024 13:19:36 -0400 Subject: [PATCH 0939/3044] update comment --- pyomo/contrib/piecewise/transform/incremental.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 510b591ea82..dd26b8c5182 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -87,7 +87,9 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # is nonempty # TODO: One way to make this true will be to use the union_jack__triangulate.py # script to generate the triangulation, but it is also possible for other - # triangulations to be correct. This should be checkable using a MIP. + # triangulations to be correct. This should be checkable using a MIP. It + # is known that there is a correct ordering for any triangulation of a + # domain homeomorphic to a disc in R^2 (Wilson 1998). self.simplex_ordering = { n: n for n in transBlock.simplex_indices } From 5ea6ae75c4f92c6b66fd4c839c2d1a8825bb1a39 Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 16:56:44 -0600 Subject: [PATCH 0940/3044] potentially working implementation of identify-variables with efficient named expression handling --- pyomo/core/expr/visitor.py | 88 ++++++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 23 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 20d8d72fcbb..7ae1900f9b8 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1405,26 +1405,46 @@ def __init__( self._active_named_expressions = [] def initializeWalker(self, expr): - self._variables = [] - self._seen = set() - return True, None + if expr.__class__ in native_types: + return False, [] + elif expr.is_named_expression_type(): + eid = id(expr) + if eid in self._named_expression_cache: + variables, var_set = self._named_expression_cache[eid] + return False, variables + else: + self._variables = [] + self._seen = set() + self._named_expression_cache[eid] = [], set() + self._active_named_expressions.append(eid) + return True, expr + else: + self._variables = [] + self._seen = set() + return True, expr def beforeChild(self, parent, child, index): if child.__class__ in native_types: return False, None - #elif ( - # not self._descend_into_named_expressions - # and child.is_named_expression_type() - #): - # self.named_expressions.append(child) - # return False, None elif child.is_named_expression_type(): - if id(child) in self._named_expression_cache: + eid = id(child) + if eid in self._named_expression_cache: # We have already encountered this named expression. We just add # the cached variables to our list and don't descend. - for var in self._named_expression_cache[id(child)][0]: - if id(var) not in self._seen: - self._variables.append(var) + if self._active_named_expressions: + # If we are in another named expression, we update the + # parent expression's cache + parent_eid = self._active_named_expressions[-1] + variables, var_set = self._named_expression_cache[parent_eid] + else: + # If we are not in a named expression, we update the global + # list + variables = self._variables + var_set = self._seen + for var in self._named_expression_cache[eid][0]: + if id(var) not in var_set: + var_set.add(id(var)) + variables.append(var) return False, None else: # If we are descending into a new named expression, initialize @@ -1432,6 +1452,18 @@ def beforeChild(self, parent, child, index): self._named_expression_cache[id(child)] = ([], set()) self._active_named_expressions.append(id(child)) return True, None + elif child.is_variable_type() and (self._include_fixed or not child.fixed): + if id(child) not in self._seen: + self._seen.add(id(child)) + self._variables.append(child) + if self._active_named_expressions: + # If we are in a named expression, add new variables to the cache. + eid = self._active_named_expressions[-1] + local_vars, local_var_set = self._named_expression_cache[eid] + if id(child) not in local_var_set: + local_var_set.add(id(child)) + local_vars.append(child) + return False, None else: return True, None @@ -1449,19 +1481,29 @@ def exitNode(self, node, data): local_vars.append(node) elif node.is_named_expression_type(): # If we are returning from a named expression, we have at least one - # active named expression. + # active named expression. We must make sure that we properly + # handle the variables for the named expression we just exited. eid = self._active_named_expressions.pop() if self._active_named_expressions: # If we still are in a named expression, we update that expression's # cache with any new variables encountered. - new_eid = self._active_named_expressions[-1] - old_expr_vars, old_expr_var_set = self._named_expression_cache[eid] - new_expr_vars, new_expr_var_set = self._named_expression_cache[new_eid] - - for var in old_expr_vars: - if id(var) not in new_expr_var_set: - new_expr_var_set.add(id(var)) - new_expr_vars.append(var) + #new_eid = self._active_named_expressions[-1] + #old_expr_vars, old_expr_var_set = self._named_expression_cache[eid] + #new_expr_vars, new_expr_var_set = self._named_expression_cache[new_eid] + + #for var in old_expr_vars: + # if id(var) not in new_expr_var_set: + # new_expr_var_set.add(id(var)) + # new_expr_vars.append(var) + parent_eid = self._active_named_expressions[-1] + variables, var_set = self._named_expression_cache[parent_eid] + else: + variables = self._variables + var_set = self._seen + for var in self._named_expression_cache[eid][0]: + if id(var) not in var_set: + var_set.add(id(var)) + variables.append(var) def finalizeResult(self, result): return self._variables @@ -1489,7 +1531,7 @@ def identify_variables(expr, include_fixed=True, named_expression_cache=None): if NEW: visitor = _StreamVariableVisitor( named_expression_cache=named_expression_cache, - include_fixed=False, + include_fixed=include_fixed, ) variables = visitor.walk_expression(expr) yield from variables From a82c7509ade2c6fb65f69c3b38472ee81ce6519b Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 17:21:02 -0600 Subject: [PATCH 0941/3044] update identify_variables tests to use ComponentSet to not rely on variable order --- pyomo/core/tests/unit/test_visitor.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pyomo/core/tests/unit/test_visitor.py b/pyomo/core/tests/unit/test_visitor.py index 12fb98d1d19..d6d83f84e67 100644 --- a/pyomo/core/tests/unit/test_visitor.py +++ b/pyomo/core/tests/unit/test_visitor.py @@ -145,7 +145,8 @@ def test_identify_vars_vars(self): 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.b[2], m.a, m.b[1]] + ComponentSet(identify_variables(m.a ** m.b[1] + m.b[2])), + ComponentSet([m.b[2], m.a, m.b[1]]), ) self.assertEqual( list(identify_variables(m.a ** m.b[1] + m.b[2] * m.b[3] * m.b[2])), @@ -159,14 +160,20 @@ def test_identify_vars_vars(self): # Identify variables in the arguments to functions # self.assertEqual( - list(identify_variables(m.x(m.a, 'string_param', 1, []) * m.b[1])), - [m.b[1], m.a], + ComponentSet(identify_variables(m.x(m.a, 'string_param', 1, []) * m.b[1])), + ComponentSet([m.b[1], m.a]), ) 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.b[1], m.a]) - self.assertEqual(list(identify_variables(abs(m.a) * m.b[1])), [m.b[1], m.a]) + self.assertEqual( + ComponentSet(identify_variables(tanh(m.a) * m.b[1])), + ComponentSet([m.b[1], m.a]), + ) + self.assertEqual( + ComponentSet(identify_variables(abs(m.a) * m.b[1])), + ComponentSet([m.b[1], m.a]), + ) # # Check logic for allowing duplicates # From f8299f6bf6526b20d5085b34e44ea7321a668e43 Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 17:25:36 -0600 Subject: [PATCH 0942/3044] remove commented code and old identify_variables implementation --- pyomo/core/expr/visitor.py | 43 ++++++-------------------------------- 1 file changed, 6 insertions(+), 37 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 7ae1900f9b8..b284c7fa38f 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1392,12 +1392,10 @@ class _StreamVariableVisitor(StreamBasedExpressionVisitor): def __init__( self, include_fixed=False, - #descend_into_named_expressions=True, named_expression_cache=None, ): super().__init__() self._include_fixed = include_fixed - #self._descend_into_named_expressions = descend_into_named_expressions self.named_expressions = [] if named_expression_cache is None: named_expression_cache = {} @@ -1487,14 +1485,6 @@ def exitNode(self, node, data): if self._active_named_expressions: # If we still are in a named expression, we update that expression's # cache with any new variables encountered. - #new_eid = self._active_named_expressions[-1] - #old_expr_vars, old_expr_var_set = self._named_expression_cache[eid] - #new_expr_vars, new_expr_var_set = self._named_expression_cache[new_eid] - - #for var in old_expr_vars: - # if id(var) not in new_expr_var_set: - # new_expr_var_set.add(id(var)) - # new_expr_vars.append(var) parent_eid = self._active_named_expressions[-1] variables, var_set = self._named_expression_cache[parent_eid] else: @@ -1509,7 +1499,6 @@ def finalizeResult(self, result): return self._variables -# TODO: descend_into_named_expressions option? def identify_variables(expr, include_fixed=True, named_expression_cache=None): """ A generator that yields a sequence of variables @@ -1526,32 +1515,12 @@ def identify_variables(expr, include_fixed=True, named_expression_cache=None): """ if named_expression_cache is None: named_expression_cache = {} - - NEW = True - if NEW: - visitor = _StreamVariableVisitor( - named_expression_cache=named_expression_cache, - include_fixed=include_fixed, - ) - variables = visitor.walk_expression(expr) - yield from variables - else: - visitor = _VariableVisitor() - if include_fixed: - for v in visitor.xbfs_yield_leaves(expr): - if isinstance(v, tuple): - yield from v - else: - yield v - else: - for v in visitor.xbfs_yield_leaves(expr): - if isinstance(v, tuple): - for v_i in v: - if not v_i.is_fixed(): - yield v_i - else: - if not v.is_fixed(): - yield v + visitor = _StreamVariableVisitor( + named_expression_cache=named_expression_cache, + include_fixed=include_fixed, + ) + variables = visitor.walk_expression(expr) + yield from variables # ===================================================== From d232fcab6a66add09f746febb706947df9f534ff Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 17:27:35 -0600 Subject: [PATCH 0943/3044] handle variable at root in initializeWalker rather than exitNode --- pyomo/core/expr/visitor.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index b284c7fa38f..4cdc77df41e 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1416,6 +1416,8 @@ def initializeWalker(self, expr): self._named_expression_cache[eid] = [], set() self._active_named_expressions.append(eid) return True, expr + elif expr.is_variable_type(): + return False, [expr] else: self._variables = [] self._seen = set() @@ -1466,18 +1468,7 @@ def beforeChild(self, parent, child, index): return True, None def exitNode(self, node, data): - if node.is_variable_type() and (self._include_fixed or not node.fixed): - if id(node) not in self._seen: - self._seen.add(id(node)) - self._variables.append(node) - if self._active_named_expressions: - # If we are in a named expression, add new variables to the cache. - eid = self._active_named_expressions[-1] - local_vars, local_var_set = self._named_expression_cache[eid] - if id(node) not in local_var_set: - local_var_set.add(id(node)) - local_vars.append(node) - elif node.is_named_expression_type(): + if node.is_named_expression_type(): # If we are returning from a named expression, we have at least one # active named expression. We must make sure that we properly # handle the variables for the named expression we just exited. From 796ccef2e5c69a104ef3a7e9e307e5c323a26907 Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 17:28:21 -0600 Subject: [PATCH 0944/3044] remove previous vars_from_expressions implementation --- pyomo/util/vars_from_expressions.py | 30 ----------------------------- 1 file changed, 30 deletions(-) diff --git a/pyomo/util/vars_from_expressions.py b/pyomo/util/vars_from_expressions.py index 22e6e6dab8d..62953af456b 100644 --- a/pyomo/util/vars_from_expressions.py +++ b/pyomo/util/vars_from_expressions.py @@ -43,36 +43,6 @@ def get_vars_from_components( descend_into: Ctypes to descend into when finding Constraints descent_order: Traversal strategy for finding the objects of type ctype """ - #visitor = _StreamVariableVisitor( - # include_fixed=include_fixed, descend_into_named_expressions=False - #) - #variables = [] - #for constraint in block.component_data_objects( - # ctype, - # active=active, - # sort=sort, - # descend_into=descend_into, - # descent_order=descent_order, - #): - # variables.extend(visitor.walk_expression(constraint.expr)) - # seen_named_exprs = set() - # named_expr_stack = list(visitor.named_expressions) - # while named_expr_stack: - # expr = named_expr_stack.pop() - # # Clear visitor's named expression cache so we only identify new - # # named expressions - # visitor.named_expressions.clear() - # variables.extend(visitor.walk_expression(expr.expr)) - # for new_expr in visitor.named_expressions: - # if id(new_expr) not in seen_named_exprs: - # seen_named_exprs.add(id(new_expr)) - # named_expr_stack.append(new_expr) - #seen = set() - #for var in variables: - # if id(var) not in seen: - # seen.add(id(var)) - # yield var - seen = set() named_expression_cache = {} for constraint in block.component_data_objects( From 7c130ef1b6d62b2f1a2ffdf6ae399dbb8fad047a Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 17:38:43 -0600 Subject: [PATCH 0945/3044] add docstring and comments to _StreamVariableVisitor --- pyomo/core/expr/visitor.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 4cdc77df41e..6dd587cf2d4 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1394,12 +1394,25 @@ def __init__( include_fixed=False, named_expression_cache=None, ): + """Visitor that collects all unique variables participating in an + expression + + Args: + include_fixed (bool): Whether to include fixed variables + named_expression_cache (optional, dict): Dict mapping ids of named + expressions to a tuple of the list of all variables and the + set of all variable ids contained in the named expression. + + """ super().__init__() self._include_fixed = include_fixed - self.named_expressions = [] if named_expression_cache is None: + # This cache will map named expression ids to the + # tuple: ([variables], {variable ids}) named_expression_cache = {} self._named_expression_cache = named_expression_cache + # Stack of active named expressions. This holds the id of + # expressions we are currently in. self._active_named_expressions = [] def initializeWalker(self, expr): From 0e3015dcf9e2daece51a485868537f9be8443e03 Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 17:44:10 -0600 Subject: [PATCH 0946/3044] arguments on single line --- pyomo/core/expr/visitor.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 6dd587cf2d4..1cd2ce3213a 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1389,11 +1389,7 @@ def visit(self, node): class _StreamVariableVisitor(StreamBasedExpressionVisitor): - def __init__( - self, - include_fixed=False, - named_expression_cache=None, - ): + def __init__(self, include_fixed=False, named_expression_cache=None): """Visitor that collects all unique variables participating in an expression @@ -1520,8 +1516,7 @@ def identify_variables(expr, include_fixed=True, named_expression_cache=None): if named_expression_cache is None: named_expression_cache = {} visitor = _StreamVariableVisitor( - named_expression_cache=named_expression_cache, - include_fixed=include_fixed, + named_expression_cache=named_expression_cache, include_fixed=include_fixed ) variables = visitor.walk_expression(expr) yield from variables From c64dcf459c261ed615e054179cfd614627629dc4 Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 15 Mar 2024 21:56:54 -0600 Subject: [PATCH 0947/3044] consolidate logic for adding variable to set --- pyomo/core/expr/visitor.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 1cd2ce3213a..7b519e0f63e 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1417,9 +1417,14 @@ def initializeWalker(self, expr): elif expr.is_named_expression_type(): eid = id(expr) if eid in self._named_expression_cache: + # If we were given a named expression that is already cached, + # just do nothing and return the expression's variables variables, var_set = self._named_expression_cache[eid] return False, variables else: + # We were given a named expression that is not cached. + # Initialize data structures and add this expression to the + # stack. This expression will get popped in exitNode. self._variables = [] self._seen = set() self._named_expression_cache[eid] = [], set() @@ -1442,12 +1447,14 @@ def beforeChild(self, parent, child, index): # the cached variables to our list and don't descend. if self._active_named_expressions: # If we are in another named expression, we update the - # parent expression's cache + # parent expression's cache. We don't need to update the + # global list as we will do this when we exit the active + # named expression. parent_eid = self._active_named_expressions[-1] variables, var_set = self._named_expression_cache[parent_eid] else: # If we are not in a named expression, we update the global - # list + # list. variables = self._variables var_set = self._seen for var in self._named_expression_cache[eid][0]: @@ -1462,16 +1469,16 @@ def beforeChild(self, parent, child, index): self._active_named_expressions.append(id(child)) return True, None elif child.is_variable_type() and (self._include_fixed or not child.fixed): - if id(child) not in self._seen: - self._seen.add(id(child)) - self._variables.append(child) if self._active_named_expressions: # If we are in a named expression, add new variables to the cache. eid = self._active_named_expressions[-1] - local_vars, local_var_set = self._named_expression_cache[eid] - if id(child) not in local_var_set: - local_var_set.add(id(child)) - local_vars.append(child) + variables, var_set = self._named_expression_cache[eid] + else: + variables = self._variables + var_set = self._seen + if id(child) not in local_var_set: + var_set.add(id(child)) + variables.append(child) return False, None else: return True, None From aac6e2f1c49cfb1437b129f76f8edbb017eccda5 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 16 Mar 2024 20:12:48 -0400 Subject: [PATCH 0948/3044] Standardize PyROS subordinate solver calls --- pyomo/contrib/pyros/master_problem_methods.py | 96 ++++++------------- .../pyros/separation_problem_methods.py | 45 +++------ pyomo/contrib/pyros/util.py | 78 +++++++++++++++ 3 files changed, 120 insertions(+), 99 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 8b9e85b90e9..2af38c1d582 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -27,6 +27,7 @@ from pyomo.core.expr import value from pyomo.core.base.set_types import NonNegativeIntegers, NonNegativeReals from pyomo.contrib.pyros.util import ( + call_solver, selective_clone, ObjectiveType, pyrosTerminationCondition, @@ -239,31 +240,18 @@ def solve_master_feasibility_problem(model_data, config): else: solver = config.local_solver - timer = TicTocTimer() - orig_setting, custom_setting_present = adjust_solver_time_settings( - model_data.timing, solver, config - ) - model_data.timing.start_timer("main.master_feasibility") - timer.tic(msg=None) - try: - results = solver.solve(model, tee=config.tee, load_solutions=False) - except ApplicationError: - # account for possible external subsolver errors - # (such as segmentation faults, function evaluation - # errors, etc.) - config.progress_logger.error( + results = call_solver( + model=model, + solver=solver, + config=config, + timing_obj=model_data.timing, + timer_name="main.master_feasibility", + err_msg=( f"Optimizer {repr(solver)} encountered exception " "attempting to solve master feasibility problem in iteration " f"{model_data.iteration}." - ) - raise - else: - setattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR, timer.toc(msg=None)) - model_data.timing.stop_timer("main.master_feasibility") - finally: - revert_solver_max_time_adjustment( - solver, orig_setting, custom_setting_present, config - ) + ), + ) feasible_terminations = { tc.optimal, @@ -482,28 +470,18 @@ def minimize_dr_vars(model_data, config): config.progress_logger.debug(f" Initial DR norm: {value(polishing_obj)}") # === Solve the polishing model - timer = TicTocTimer() - orig_setting, custom_setting_present = adjust_solver_time_settings( - model_data.timing, solver, config - ) - model_data.timing.start_timer("main.dr_polishing") - timer.tic(msg=None) - try: - results = solver.solve(polishing_model, tee=config.tee, load_solutions=False) - except ApplicationError: - config.progress_logger.error( + results = call_solver( + model=polishing_model, + solver=solver, + config=config, + timing_obj=model_data.timing, + timer_name="main.dr_polishing", + err_msg=( f"Optimizer {repr(solver)} encountered an exception " "attempting to solve decision rule polishing problem " f"in iteration {model_data.iteration}" - ) - raise - else: - setattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR, timer.toc(msg=None)) - model_data.timing.stop_timer("main.dr_polishing") - finally: - revert_solver_max_time_adjustment( - solver, orig_setting, custom_setting_present, config - ) + ), + ) # interested in the time and termination status for debugging # purposes @@ -726,7 +704,6 @@ def solver_call_master(model_data, config, solver, solve_data): solve_mode = "global" if config.solve_master_globally else "local" config.progress_logger.debug("Solving master problem") - timer = TicTocTimer() for idx, opt in enumerate(solvers): if idx > 0: config.progress_logger.warning( @@ -734,35 +711,18 @@ def solver_call_master(model_data, config, solver, solve_data): f"(solver {idx + 1} of {len(solvers)}) for " f"master problem of iteration {model_data.iteration}." ) - orig_setting, custom_setting_present = adjust_solver_time_settings( - model_data.timing, opt, config - ) - model_data.timing.start_timer("main.master") - timer.tic(msg=None) - try: - results = opt.solve( - nlp_model, - tee=config.tee, - load_solutions=False, - symbolic_solver_labels=True, - ) - except ApplicationError: - # account for possible external subsolver errors - # (such as segmentation faults, function evaluation - # errors, etc.) - config.progress_logger.error( + results = call_solver( + model=nlp_model, + solver=opt, + config=config, + timing_obj=model_data.timing, + timer_name="main.master", + err_msg=( f"Optimizer {repr(opt)} ({idx + 1} of {len(solvers)}) " "encountered exception attempting to " f"solve master problem in iteration {model_data.iteration}" - ) - raise - else: - setattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR, timer.toc(msg=None)) - model_data.timing.stop_timer("main.master") - finally: - revert_solver_max_time_adjustment( - solver, orig_setting, custom_setting_present, config - ) + ), + ) optimal_termination = check_optimal_termination(results) infeasible = results.solver.termination_condition == tc.infeasible diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index b5939ff5b19..18d0925bab0 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -18,7 +18,6 @@ from pyomo.core.base import Var, Param from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.common.dependencies import numpy as np -from pyomo.contrib.pyros.util import ObjectiveType, get_time_from_solver from pyomo.contrib.pyros.solve_data import ( DiscreteSeparationSolveCallResults, SeparationSolveCallResults, @@ -37,9 +36,11 @@ from pyomo.contrib.pyros.util import ABS_CON_CHECK_FEAS_TOL from pyomo.common.timing import TicTocTimer from pyomo.contrib.pyros.util import ( - TIC_TOC_SOLVE_TIME_ATTR, adjust_solver_time_settings, + call_solver, + ObjectiveType, revert_solver_max_time_adjustment, + TIC_TOC_SOLVE_TIME_ATTR, ) import os from copy import deepcopy @@ -1070,6 +1071,7 @@ def solver_call_separation( separation_obj.activate() + solve_mode_adverb = "globally" if solve_globally else "locally" solve_call_results = SeparationSolveCallResults( solved_globally=solve_globally, time_out=False, @@ -1077,7 +1079,6 @@ def solver_call_separation( found_violation=False, subsolver_error=False, ) - timer = TicTocTimer() for idx, opt in enumerate(solvers): if idx > 0: config.progress_logger.warning( @@ -1086,37 +1087,19 @@ def solver_call_separation( f"separation of performance constraint {con_name_repr} " f"in iteration {model_data.iteration}." ) - orig_setting, custom_setting_present = adjust_solver_time_settings( - model_data.timing, opt, config - ) - model_data.timing.start_timer(f"main.{solve_mode}_separation") - timer.tic(msg=None) - try: - results = opt.solve( - nlp_model, - tee=config.tee, - load_solutions=False, - symbolic_solver_labels=True, - ) - except ApplicationError: - # account for possible external subsolver errors - # (such as segmentation faults, function evaluation - # errors, etc.) - adverb = "globally" if solve_globally else "locally" - config.progress_logger.error( + results = call_solver( + model=nlp_model, + solver=opt, + config=config, + timing_obj=model_data.timing, + timer_name=f"main.{solve_mode}_separation", + err_msg=( f"Optimizer {repr(opt)} ({idx + 1} of {len(solvers)}) " f"encountered exception attempting " - f"to {adverb} solve separation problem for constraint " + f"to {solve_mode_adverb} solve separation problem for constraint " f"{con_name_repr} in iteration {model_data.iteration}." - ) - raise - else: - setattr(results.solver, TIC_TOC_SOLVE_TIME_ATTR, timer.toc(msg=None)) - model_data.timing.stop_timer(f"main.{solve_mode}_separation") - finally: - revert_solver_max_time_adjustment( - opt, orig_setting, custom_setting_present, config - ) + ), + ) # record termination condition for this particular solver solver_status_dict[str(opt)] = results.solver.termination_condition diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index a3ab3464aa8..33551115148 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -16,7 +16,9 @@ import copy from enum import Enum, auto from pyomo.common.collections import ComponentSet, ComponentMap +from pyomo.common.errors import ApplicationError from pyomo.common.modeling import unique_component_name +from pyomo.common.timing import TicTocTimer from pyomo.core.base import ( Constraint, Var, @@ -1731,6 +1733,82 @@ def process_termination_condition_master_problem(config, results): ) +def call_solver(model, solver, config, timing_obj, timer_name, err_msg): + """ + Solve a model with a given optimizer, keeping track of + wall time requirements. + + Parameters + ---------- + model : ConcreteModel + Model of interest. + solver : Pyomo solver type + Subordinate optimizer. + config : ConfigDict + PyROS solver settings. + timing_obj : TimingData + PyROS solver timing data object. + timer_name : str + Name of sub timer under the hierarchical timer contained in + ``timing_obj`` to start/stop for keeping track of solve + time requirements. + err_msg : str + Message to log through ``config.progress_logger.exception()`` + in event an ApplicationError is raised while attempting to + solve the model. + + Returns + ------- + SolverResults + Solve results. Note that ``results.solver`` contains + an additional attribute, named after + ``TIC_TOC_SOLVE_TIME_ATTR``, of which the value is set to the + recorded solver wall time. + + Raises + ------ + ApplicationError + If ApplicationError is raised by the solver. + In this case, `err_msg` is logged through + ``config.progress_logger.exception()`` before + the excception is raised. + """ + tt_timer = TicTocTimer() + + orig_setting, custom_setting_present = adjust_solver_time_settings( + timing_obj, solver, config + ) + timing_obj.start_timer(timer_name) + tt_timer.tic(msg=None) + + try: + results = solver.solve( + model, + tee=config.tee, + load_solutions=False, + symbolic_solver_labels=True, + ) + except ApplicationError: + # account for possible external subsolver errors + # (such as segmentation faults, function evaluation + # errors, etc.) + config.progress_logger.error(err_msg) + raise + else: + setattr( + results.solver, + TIC_TOC_SOLVE_TIME_ATTR, + tt_timer.toc(msg=None, delta=True), + ) + finally: + timing_obj.stop_timer(timer_name) + revert_solver_max_time_adjustment( + solver, orig_setting, custom_setting_present, config + ) + + return results + + class IterationLogRecord: """ PyROS solver iteration log record. From d9f22516d0b79d204462ffb91095b408423de524 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 17 Mar 2024 15:57:18 -0400 Subject: [PATCH 0949/3044] Account for user settings in subsolver time limit adjustment --- pyomo/contrib/pyros/util.py | 68 +++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 33551115148..7d40d357863 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -232,15 +232,15 @@ def get_main_elapsed_time(timing_data_obj): def adjust_solver_time_settings(timing_data_obj, solver, config): """ - Adjust solver max time setting based on current PyROS elapsed - time. + Adjust maximum time allowed for subordinate solver, based + on total PyROS solver elapsed time up to this point. Parameters ---------- timing_data_obj : Bunch PyROS timekeeper. solver : solver type - Solver for which to adjust the max time setting. + Subordinate solver for which to adjust the max time setting. config : ConfigDict PyROS solver config. @@ -262,26 +262,40 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): ---- (1) Adjustment only supported for GAMS, BARON, and IPOPT interfaces. This routine can be generalized to other solvers - after a generic interface to the time limit setting + after a generic Pyomo interface to the time limit setting is introduced. - (2) For IPOPT, and probably also BARON, the CPU time limit - rather than the wallclock time limit, is adjusted, as - no interface to wallclock limit available. - For this reason, extra 30s is added to time remaining - for subsolver time limit. - (The extra 30s is large enough to ensure solver - elapsed time is not beneath elapsed time - user time limit, - but not so large as to overshoot the user-specified time limit - by an inordinate margin.) + (2) For IPOPT and BARON, the CPU time limit, + rather than the wallclock time limit, may be adjusted, + as there may be no means by which to specify the wall time + limit explicitly. + (3) For GAMS, we adjust the time limit through the GAMS Reslim + option. However, this may be overriden by any user + specifications included in a GAMS optfile, which may be + difficult to track down. + (3) To ensure the time limit is specified to a strictly + positive value, the time limit is adjusted to a value of + at least 1 second. """ + # in case there is no time remaining: we set time limit + # to a minimum of 1s, as some solvers require a strictly + # positive time limit + time_limit_buffer = 1 + if config.time_limit is not None: time_remaining = config.time_limit - get_main_elapsed_time(timing_data_obj) if isinstance(solver, type(SolverFactory("gams", solver_io="shell"))): original_max_time_setting = solver.options["add_options"] custom_setting_present = "add_options" in solver.options - # adjust GAMS solver time - reslim_str = f"option reslim={max(30, 30 + time_remaining)};" + # note: our time limit will be overriden by any + # time limits specified by the user through a + # GAMS optfile, but tracking down the optfile + # and/or the GAMS subsolver specific option + # is more difficult + reslim_str = ( + "option reslim=" + f"{max(time_limit_buffer, time_remaining)};" + ) if isinstance(solver.options["add_options"], list): solver.options["add_options"].append(reslim_str) else: @@ -291,7 +305,13 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): if isinstance(solver, SolverFactory.get_class("baron")): options_key = "MaxTime" elif isinstance(solver, SolverFactory.get_class("ipopt")): - options_key = "max_cpu_time" + options_key = ( + # IPOPT 3.14.0+ added support for specifying + # wall time limit explicitly; this is preferred + # over CPU time limit + "max_wall_time" if solver.version() >= (3, 14, 0, 0) + else "max_cpu_time" + ) else: options_key = None @@ -299,8 +319,20 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): custom_setting_present = options_key in solver.options original_max_time_setting = solver.options[options_key] - # ensure positive value assigned to avoid application error - solver.options[options_key] = max(30, 30 + time_remaining) + # account for elapsed time remaining and + # original time limit setting. + # if no original time limit is set, then we assume + # there is no time limit, rather than tracking + # down the solver-specific default + orig_max_time = ( + float("inf") + if original_max_time_setting is None + else original_max_time_setting + ) + solver.options[options_key] = min( + max(time_limit_buffer, time_remaining), + orig_max_time, + ) else: custom_setting_present = False original_max_time_setting = None From 2593fce468e4f8095a2c2c35698323155035d2e8 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 17 Mar 2024 18:05:38 -0400 Subject: [PATCH 0950/3044] Fix test error message string --- pyomo/contrib/pyros/tests/test_grcs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index c308f0d6990..754ab6678ea 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -4398,8 +4398,8 @@ def test_gams_successful_time_limit(self): results.pyros_termination_condition, pyrosTerminationCondition.robust_optimal, msg=( - f"Returned termination condition with local " - "subsolver {idx + 1} of 2 is not robust_optimal." + "Returned termination condition with local " + f"subsolver {idx + 1} of 2 is not robust_optimal." ), ) From 679dc7ee4620a1d424d055250363e84d24bca86c Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 17 Mar 2024 18:38:48 -0400 Subject: [PATCH 0951/3044] Add support for SCIP time limit adjustment --- pyomo/contrib/pyros/tests/test_grcs.py | 77 +++++++------------------- pyomo/contrib/pyros/util.py | 4 ++ 2 files changed, 24 insertions(+), 57 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 754ab6678ea..92532677a80 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -4345,10 +4345,10 @@ def test_separation_terminate_time_limit(self): and SolverFactory('baron').license_is_valid(), "Global NLP solver is not available and licensed.", ) - def test_gams_successful_time_limit(self): + def test_pyros_subsolver_time_limit_adjustment(self): """ - Test PyROS time limit status returned in event - separation problem times out. + Check that PyROS does not ultimately alter state of + subordinate solver options due to time limit adjustments. """ m = ConcreteModel() m.x1 = Var(initialize=0, bounds=(0, None)) @@ -4367,20 +4367,26 @@ def test_gams_successful_time_limit(self): # Instantiate the PyROS solver pyros_solver = SolverFactory("pyros") - # Define subsolvers utilized in the algorithm - # two GAMS solvers, one of which has reslim set - # (overridden when invoked in PyROS) + # subordinate solvers to test. + # for testing, we pass each as the 'local' solver, + # and the BARON solver without custom options + # as the 'global' solver + baron_no_options = SolverFactory("baron") local_subsolvers = [ SolverFactory("gams:conopt"), SolverFactory("gams:conopt"), SolverFactory("ipopt"), + SolverFactory("ipopt", options={"max_cpu_time": 300}), + SolverFactory("scip"), + SolverFactory("scip", options={"limits/time": 300}), + baron_no_options, + SolverFactory("baron", options={"MaxTime": 300}), ] local_subsolvers[0].options["add_options"] = ["option reslim=100;"] - global_subsolver = SolverFactory("baron") - global_subsolver.options["MaxTime"] = 300 # Call the PyROS solver for idx, opt in enumerate(local_subsolvers): + original_solver_options = opt.options.copy() results = pyros_solver.solve( model=m, first_stage_variables=[m.x1, m.x2], @@ -4388,12 +4394,11 @@ def test_gams_successful_time_limit(self): uncertain_params=[m.u], uncertainty_set=interval, local_solver=opt, - global_solver=global_subsolver, + global_solver=baron_no_options, objective_focus=ObjectiveType.worst_case, solve_master_globally=True, time_limit=100, ) - self.assertEqual( results.pyros_termination_condition, pyrosTerminationCondition.robust_optimal, @@ -4402,54 +4407,12 @@ def test_gams_successful_time_limit(self): f"subsolver {idx + 1} of 2 is not robust_optimal." ), ) - - # check first local subsolver settings - # remain unchanged after PyROS exit - self.assertEqual( - len(list(local_subsolvers[0].options["add_options"])), - 1, - msg=( - f"Local subsolver {local_subsolvers[0]} options 'add_options'" - "were changed by PyROS" - ), - ) - self.assertEqual( - local_subsolvers[0].options["add_options"][0], - "option reslim=100;", - msg=( - f"Local subsolver {local_subsolvers[0]} setting " - "'add_options' was modified " - "by PyROS, but changes were not properly undone" - ), - ) - - # check global subsolver settings unchanged - self.assertEqual( - len(list(global_subsolver.options.keys())), - 1, - msg=(f"Global subsolver {global_subsolver} options were changed by PyROS"), - ) - self.assertEqual( - global_subsolver.options["MaxTime"], - 300, - msg=( - f"Global subsolver {global_subsolver} setting " - "'MaxTime' was modified " - "by PyROS, but changes were not properly undone" - ), - ) - - # check other local subsolvers remain unchanged - for slvr, key in zip(local_subsolvers[1:], ["add_options", "max_cpu_time"]): - # no custom options were added to the `options` - # attribute of the optimizer, so any attribute - # of `options` should be `None` - self.assertIs( - getattr(slvr.options, key, None), - None, + self.assertEqual( + opt.options, + original_solver_options, msg=( - f"Local subsolver {slvr} setting '{key}' was added " - "by PyROS, but not reverted" + f"Options for subordinate solver {opt} were changed " + "by PyROS, and the changes wee not properly reverted." ), ) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 7d40d357863..bdec2213d43 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -312,6 +312,8 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): "max_wall_time" if solver.version() >= (3, 14, 0, 0) else "max_cpu_time" ) + elif isinstance(solver, SolverFactory.get_class("scip")): + options_key = "limits/time" else: options_key = None @@ -379,6 +381,8 @@ def revert_solver_max_time_adjustment( options_key = "MaxTime" elif isinstance(solver, SolverFactory.get_class("ipopt")): options_key = "max_cpu_time" + elif isinstance(solver, SolverFactory.get_class("scip")): + options_key = "limits/time" else: options_key = None From fcb28193147e55e18f69128d10060d2b8839ca8b Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 17 Mar 2024 18:42:21 -0400 Subject: [PATCH 0952/3044] Simplify time limit adjustment reversion --- pyomo/contrib/pyros/util.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index bdec2213d43..fa423e37450 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -397,12 +397,7 @@ def revert_solver_max_time_adjustment( if isinstance(solver, type(SolverFactory("gams", solver_io="shell"))): solver.options[options_key].pop() else: - # remove the max time specification introduced. - # All lines are needed here to completely remove the option - # from access through getattr and dictionary reference. delattr(solver.options, options_key) - if options_key in solver.options.keys(): - del solver.options[options_key] class PreformattedLogger(logging.Logger): From 0c8afa56489f3c32d987b75ab65038538d3e9735 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 17 Mar 2024 18:56:22 -0400 Subject: [PATCH 0953/3044] Update solver test availability and license check --- pyomo/contrib/pyros/tests/test_grcs.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 92532677a80..41223b30899 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -4341,9 +4341,11 @@ def test_separation_terminate_time_limit(self): ) @unittest.skipUnless( - SolverFactory('gams').license_is_valid() - and SolverFactory('baron').license_is_valid(), - "Global NLP solver is not available and licensed.", + ipopt_available + and SolverFactory('gams').license_is_valid() + and SolverFactory('baron').license_is_valid() + and SolverFactory("scip").license_is_valid(), + "IPOPT not available or one of GAMS/BARON/SCIP not licensed", ) def test_pyros_subsolver_time_limit_adjustment(self): """ From ec830e6c19600419a3a187c7c63c9f1700bedfcf Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 17 Mar 2024 19:05:33 -0400 Subject: [PATCH 0954/3044] Move PyROS timer start to before argument validation --- pyomo/contrib/pyros/pyros.py | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 6de42d7299e..c74daf34c5f 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -330,32 +330,24 @@ def solve( Summary of PyROS termination outcome. """ - kwds.update( - dict( - first_stage_variables=first_stage_variables, - second_stage_variables=second_stage_variables, - uncertain_params=uncertain_params, - uncertainty_set=uncertainty_set, - local_solver=local_solver, - global_solver=global_solver, - ) - ) - config, state_vars = self._resolve_and_validate_pyros_args(model, **kwds) - - # === Create data containers model_data = ROSolveResults() - model_data.timing = Bunch() - - # === Start timer, run the algorithm model_data.timing = TimingData() with time_code( timing_data_obj=model_data.timing, code_block_name="main", is_main_timer=True, ): - # output intro and disclaimer - self._log_intro(logger=config.progress_logger, level=logging.INFO) - self._log_disclaimer(logger=config.progress_logger, level=logging.INFO) + kwds.update( + dict( + first_stage_variables=first_stage_variables, + second_stage_variables=second_stage_variables, + uncertain_params=uncertain_params, + uncertainty_set=uncertainty_set, + local_solver=local_solver, + global_solver=global_solver, + ) + ) + config, state_vars = self._resolve_and_validate_pyros_args(model, **kwds) self._log_config( logger=config.progress_logger, config=config, From 348a896bb77f2ad2634043647303e325edd1e06f Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 17 Mar 2024 19:14:24 -0400 Subject: [PATCH 0955/3044] Fix typos --- pyomo/contrib/pyros/util.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index fa423e37450..306141e9829 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -269,7 +269,7 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): as there may be no means by which to specify the wall time limit explicitly. (3) For GAMS, we adjust the time limit through the GAMS Reslim - option. However, this may be overriden by any user + option. However, this may be overridden by any user specifications included in a GAMS optfile, which may be difficult to track down. (3) To ensure the time limit is specified to a strictly @@ -287,15 +287,12 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): original_max_time_setting = solver.options["add_options"] custom_setting_present = "add_options" in solver.options - # note: our time limit will be overriden by any + # note: our time limit will be overridden by any # time limits specified by the user through a # GAMS optfile, but tracking down the optfile # and/or the GAMS subsolver specific option # is more difficult - reslim_str = ( - "option reslim=" - f"{max(time_limit_buffer, time_remaining)};" - ) + reslim_str = "option reslim=" f"{max(time_limit_buffer, time_remaining)};" if isinstance(solver.options["add_options"], list): solver.options["add_options"].append(reslim_str) else: @@ -309,7 +306,8 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): # IPOPT 3.14.0+ added support for specifying # wall time limit explicitly; this is preferred # over CPU time limit - "max_wall_time" if solver.version() >= (3, 14, 0, 0) + "max_wall_time" + if solver.version() >= (3, 14, 0, 0) else "max_cpu_time" ) elif isinstance(solver, SolverFactory.get_class("scip")): @@ -332,8 +330,7 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): else original_max_time_setting ) solver.options[options_key] = min( - max(time_limit_buffer, time_remaining), - orig_max_time, + max(time_limit_buffer, time_remaining), orig_max_time ) else: custom_setting_present = False @@ -1814,10 +1811,7 @@ def call_solver(model, solver, config, timing_obj, timer_name, err_msg): try: results = solver.solve( - model, - tee=config.tee, - load_solutions=False, - symbolic_solver_labels=True, + model, tee=config.tee, load_solutions=False, symbolic_solver_labels=True ) except ApplicationError: # account for possible external subsolver errors @@ -1827,9 +1821,7 @@ def call_solver(model, solver, config, timing_obj, timer_name, err_msg): raise else: setattr( - results.solver, - TIC_TOC_SOLVE_TIME_ATTR, - tt_timer.toc(msg=None, delta=True), + results.solver, TIC_TOC_SOLVE_TIME_ATTR, tt_timer.toc(msg=None, delta=True) ) finally: timing_obj.stop_timer(timer_name) From fed3c33dc2626e78d51e6025af9d93751b5e4313 Mon Sep 17 00:00:00 2001 From: robbybp Date: Sun, 17 Mar 2024 20:38:50 -0600 Subject: [PATCH 0956/3044] fix typo --- pyomo/core/expr/visitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 7b519e0f63e..2fddca22c5f 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1476,7 +1476,7 @@ def beforeChild(self, parent, child, index): else: variables = self._variables var_set = self._seen - if id(child) not in local_var_set: + if id(child) not in var_set: var_set.add(id(child)) variables.append(child) return False, None From 27ac97ff96f37c4f43c10501c0448d3f42532c67 Mon Sep 17 00:00:00 2001 From: Utkarsh-Detha Date: Mon, 18 Mar 2024 12:25:46 +0100 Subject: [PATCH 0957/3044] Fix: mosek_direct updated to use putqconk instead of putqcon This fix concerns QCQP models when solved using mosek. In MOSEK's Optimizer API, the putqcon method resets the Q matrix entries for all constraints to zero, while putqconk does so only for k-th constraint. The _add_constraints method in mosek_direct would call putqcon, but this would lead to loss of Q info with every subsequent call to the _add_constraints (if new Q info was given). This is now fixed, because the Q matrix in each constraint is updated in its own call to putqconk. --- pyomo/solvers/plugins/solvers/mosek_direct.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/mosek_direct.py b/pyomo/solvers/plugins/solvers/mosek_direct.py index 5000a2f35c4..f4225651907 100644 --- a/pyomo/solvers/plugins/solvers/mosek_direct.py +++ b/pyomo/solvers/plugins/solvers/mosek_direct.py @@ -492,13 +492,10 @@ def _add_constraints(self, con_seq): ptrb = (0,) + ptre[:-1] asubs = tuple(itertools.chain.from_iterable(l_ids)) avals = tuple(itertools.chain.from_iterable(l_coefs)) - qcsubi = tuple(itertools.chain.from_iterable(q_is)) - qcsubj = tuple(itertools.chain.from_iterable(q_js)) - qcval = tuple(itertools.chain.from_iterable(q_vals)) - qcsubk = tuple(i for i in sub for j in range(len(q_is[i - con_num]))) self._solver_model.appendcons(num_lq) self._solver_model.putarowlist(sub, ptrb, ptre, asubs, avals) - self._solver_model.putqcon(qcsubk, qcsubi, qcsubj, qcval) + for k, i, j, v in zip(sub, q_is, q_js, q_vals): + self._solver_model.putqconk(k, i, j, v) self._solver_model.putconboundlist(sub, bound_types, lbs, ubs) for i, s_n in enumerate(sub_names): self._solver_model.putconname(sub[i], s_n) From dbe0529350c26de25f9acf71657e595ae22d90d9 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 18 Mar 2024 12:57:50 -0400 Subject: [PATCH 0958/3044] Update version number, changelog --- pyomo/contrib/pyros/CHANGELOG.txt | 11 +++++++++++ pyomo/contrib/pyros/pyros.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/CHANGELOG.txt b/pyomo/contrib/pyros/CHANGELOG.txt index 94f4848edb2..52cd7a6db47 100644 --- a/pyomo/contrib/pyros/CHANGELOG.txt +++ b/pyomo/contrib/pyros/CHANGELOG.txt @@ -2,6 +2,17 @@ PyROS CHANGELOG =============== +------------------------------------------------------------------------------- +PyROS 1.2.11 17 Mar 2024 +------------------------------------------------------------------------------- +- Standardize calls to subordinate solvers across all PyROS subproblem types +- Account for user-specified subsolver time limits when automatically + adjusting subsolver time limits +- Add support for automatic adjustment of SCIP subsolver time limit +- Move start point of main PyROS solver timer to just before argument + validation begins + + ------------------------------------------------------------------------------- PyROS 1.2.10 07 Feb 2024 ------------------------------------------------------------------------------- diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index c74daf34c5f..c3335588b7b 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -44,7 +44,7 @@ from datetime import datetime -__version__ = "1.2.10" +__version__ = "1.2.11" default_pyros_solver_logger = setup_pyros_logger() From 927c46c660189526e4728c8c0b37fcf82ae94bce Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 12:25:11 -0600 Subject: [PATCH 0959/3044] Add 'mixed' option to standard form writer --- pyomo/repn/plugins/standard_form.py | 37 ++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index 239cd845930..d0e1014d549 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -139,6 +139,15 @@ class LinearStandardFormCompiler(object): description='Add slack variables and return `min cTx s.t. Ax == b`', ), ) + CONFIG.declare( + 'mixed_form', + ConfigValue( + default=False, + domain=bool, + description='Return A in mixed form (the comparison operator is a ' + 'mix of <=, ==, and >=)', + ), + ) CONFIG.declare( 'show_section_timing', ConfigValue( @@ -332,6 +341,9 @@ def write(self, model): # Tabulate constraints # slack_form = self.config.slack_form + mixed_form = self.config.mixed_form + if slack_form and mixed_form: + raise ValueError("cannot specify both slack_form and mixed_form") rows = [] rhs = [] con_data = [] @@ -372,7 +384,30 @@ def write(self, model): f"model contains a trivially infeasible constraint, '{con.name}'" ) - if slack_form: + if mixed_form: + N = len(repn.linear) + _data = np.fromiter(repn.linear.values(), float, N) + _index = np.fromiter(map(var_order.__getitem__, repn.linear), float, N) + if ub == lb: + rows.append(RowEntry(con, 0)) + rhs.append(ub - offset) + con_data.append(_data) + con_index.append(_index) + con_index_ptr.append(con_index_ptr[-1] + N) + else: + if ub is not None: + rows.append(RowEntry(con, 1)) + rhs.append(ub - offset) + con_data.append(_data) + con_index.append(_index) + con_index_ptr.append(con_index_ptr[-1] + N) + if lb is not None: + rows.append(RowEntry(con, -1)) + rhs.append(lb - offset) + con_data.append(_data) + con_index.append(_index) + con_index_ptr.append(con_index_ptr[-1] + N) + elif slack_form: _data = list(repn.linear.values()) _index = list(map(var_order.__getitem__, repn.linear)) if lb == ub: # TODO: add tolerance? From 64211e187f5a3daa2d0d1c4c4061aae4866c4b44 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 12:25:37 -0600 Subject: [PATCH 0960/3044] Fix error when removing unused variables --- pyomo/repn/plugins/standard_form.py | 26 ++++++++++++-------------- pyomo/repn/tests/test_standard_form.py | 13 +++++++++++++ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index d0e1014d549..ea7b6a6a9e6 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -472,24 +472,22 @@ def write(self, model): # at the index pointer list (an O(num_var) operation). c_ip = c.indptr A_ip = A.indptr - active_var_idx = list( - filter( - lambda i: A_ip[i] != A_ip[i + 1] or c_ip[i] != c_ip[i + 1], - range(len(columns)), - ) - ) - nCol = len(active_var_idx) + active_var_mask = (A_ip[1:] > A_ip[:-1]) | (c_ip[1:] > c_ip[:-1]) + + # Masks on NumPy arrays are very fast. Build the reduced A + # indptr and then check if we actually have to manipulate the + # columns + augmented_mask = np.concatenate((active_var_mask, [True])) + reduced_A_indptr = A.indptr[augmented_mask] + nCol = len(reduced_A_indptr) - 1 if nCol != len(columns): - # Note that the indptr can't just use range() because a var - # may only appear in the objectives or the constraints. - columns = list(map(columns.__getitem__, active_var_idx)) - active_var_idx.append(c.indptr[-1]) + columns = [v for k, v in zip(active_var_mask, columns) if k] c = scipy.sparse.csc_array( - (c.data, c.indices, c.indptr.take(active_var_idx)), [c.shape[0], nCol] + (c.data, c.indices, c.indptr[augmented_mask]), [c.shape[0], nCol] ) - active_var_idx[-1] = A.indptr[-1] + # active_var_idx[-1] = len(columns) A = scipy.sparse.csc_array( - (A.data, A.indices, A.indptr.take(active_var_idx)), [A.shape[0], nCol] + (A.data, A.indices, reduced_A_indptr), [A.shape[0], nCol] ) if self.config.nonnegative_vars: diff --git a/pyomo/repn/tests/test_standard_form.py b/pyomo/repn/tests/test_standard_form.py index e24195edfde..c8b914deca5 100644 --- a/pyomo/repn/tests/test_standard_form.py +++ b/pyomo/repn/tests/test_standard_form.py @@ -43,6 +43,19 @@ def test_linear_model(self): self.assertTrue(np.all(repn.A == np.array([[-1, -2, 0], [0, 1, 4]]))) self.assertTrue(np.all(repn.rhs == np.array([-3, 5]))) + def test_almost_dense_linear_model(self): + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.y = pyo.Var([1, 2, 3]) + m.c = pyo.Constraint(expr=m.x + 2 * m.y[1] + 4 * m.y[3] >= 10) + m.d = pyo.Constraint(expr=5 * m.x + 6 * m.y[1] + 8 * m.y[3] <= 20) + + repn = LinearStandardFormCompiler().write(m) + + self.assertTrue(np.all(repn.c == np.array([0, 0, 0]))) + self.assertTrue(np.all(repn.A == np.array([[-1, -2, -4], [5, 6, 8]]))) + self.assertTrue(np.all(repn.rhs == np.array([-10, 20]))) + def test_linear_model_row_col_order(self): m = pyo.ConcreteModel() m.x = pyo.Var() From 4110f005d2f3a7b1bc97e9b5db997853da42c238 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 12:34:00 -0600 Subject: [PATCH 0961/3044] Make LegacySolverWrapper compatible with Pyomo script --- pyomo/contrib/solver/base.py | 110 ++++++++++++++++++++++------------- 1 file changed, 68 insertions(+), 42 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 43d168a98a0..29b2569278c 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -19,9 +19,10 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.common.config import document_kwargs_from_configdict +from pyomo.common.config import document_kwargs_from_configdict, ConfigValue from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning +from pyomo.common.modeling import NOTSET from pyomo.opt.results.results_ import SolverResults as LegacySolverResults from pyomo.opt.results.solution import Solution as LegacySolution from pyomo.core.kernel.objective import minimize @@ -347,6 +348,11 @@ class LegacySolverWrapper: interface. Necessary for backwards compatibility. """ + def __init__(self, solver_io=None, **kwargs): + if solver_io is not None: + raise NotImplementedError('Still working on this') + super().__init__(**kwargs) + # # Support "with" statements # @@ -358,51 +364,57 @@ def __exit__(self, t, v, traceback): def _map_config( self, - tee, - load_solutions, - symbolic_solver_labels, - timelimit, - # Report timing is no longer a valid option. We now always return a - # timer object that can be inspected. - report_timing, - raise_exception_on_nonoptimal_result, - solver_io, - suffixes, - logfile, - keepfiles, - solnfile, - options, + tee=NOTSET, + load_solutions=NOTSET, + symbolic_solver_labels=NOTSET, + timelimit=NOTSET, + report_timing=NOTSET, + raise_exception_on_nonoptimal_result=NOTSET, + solver_io=NOTSET, + suffixes=NOTSET, + logfile=NOTSET, + keepfiles=NOTSET, + solnfile=NOTSET, + options=NOTSET, ): """Map between legacy and new interface configuration options""" self.config = self.config() - self.config.tee = tee - self.config.load_solutions = load_solutions - self.config.symbolic_solver_labels = symbolic_solver_labels - self.config.time_limit = timelimit - self.config.solver_options.set_value(options) + if tee is not NOTSET: + self.config.tee = tee + if load_solutions is not NOTSET: + self.config.load_solutions = load_solutions + if symbolic_solver_labels is not NOTSET: + self.config.symbolic_solver_labels = symbolic_solver_labels + if timelimit is not NOTSET: + self.config.time_limit = timelimit + if report_timing is not NOTSET: + self.config.report_timing = report_timing + if options is not NOTSET: + self.config.solver_options.set_value(options) # This is a new flag in the interface. To preserve backwards compatibility, # its default is set to "False" - self.config.raise_exception_on_nonoptimal_result = ( - raise_exception_on_nonoptimal_result - ) - if solver_io is not None: + if raise_exception_on_nonoptimal_result is not NOTSET: + self.config.raise_exception_on_nonoptimal_result = ( + raise_exception_on_nonoptimal_result + ) + if solver_io is not NOTSET: raise NotImplementedError('Still working on this') - if suffixes is not None: + if suffixes is not NOTSET: raise NotImplementedError('Still working on this') - if logfile is not None: + if logfile is not NOTSET: raise NotImplementedError('Still working on this') if keepfiles or 'keepfiles' in self.config: cwd = os.getcwd() deprecation_warning( "`keepfiles` has been deprecated in the new solver interface. " - "Use `working_dir` instead to designate a directory in which " - f"files should be generated and saved. Setting `working_dir` to `{cwd}`.", + "Use `working_dir` instead to designate a directory in which files " + f"should be generated and saved. Setting `working_dir` to `{cwd}`.", version='6.7.1', ) self.config.working_dir = cwd # I believe this currently does nothing; however, it is unclear what # our desired behavior is for this. - if solnfile is not None: + if solnfile is not NOTSET: if 'filename' in self.config: filename = os.path.splitext(solnfile)[0] self.config.filename = filename @@ -504,20 +516,24 @@ def solve( """ original_config = self.config - self._map_config( - tee, - load_solutions, - symbolic_solver_labels, - timelimit, - report_timing, - raise_exception_on_nonoptimal_result, - solver_io, - suffixes, - logfile, - keepfiles, - solnfile, - options, + + map_args = ( + 'tee', + 'load_solutions', + 'symbolic_solver_labels', + 'timelimit', + 'report_timing', + 'raise_exception_on_nonoptimal_result', + 'solver_io', + 'suffixes', + 'logfile', + 'keepfiles', + 'solnfile', + 'options', ) + loc = locals() + filtered_args = {k: loc[k] for k in map_args if loc.get(k, None) is not None} + self._map_config(**filtered_args) results: Results = super().solve(model) legacy_results, legacy_soln = self._map_results(model, results) @@ -555,3 +571,13 @@ def license_is_valid(self) -> bool: """ return bool(self.available()) + + def config_block(self, init=False): + from pyomo.scripting.solve_config import default_config_block + + return default_config_block(self, init)[0] + + def set_options(self, options): + opts = {k: v for k, v in options.value().items() if v is not None} + if opts: + self._map_config(**opts) From 62b861a9811021f9c54a6aed4fe73ca841164136 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 12:34:33 -0600 Subject: [PATCH 0962/3044] Make report_timing 'work' in LegactSolverInterface --- pyomo/contrib/solver/base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 29b2569278c..9e0356c9c21 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -379,6 +379,10 @@ def _map_config( ): """Map between legacy and new interface configuration options""" self.config = self.config() + if 'report_timing' not in self.config: + self.config.declare( + 'report_timing', ConfigValue(domain=bool, default=False) + ) if tee is not NOTSET: self.config.tee = tee if load_solutions is not NOTSET: @@ -537,11 +541,13 @@ def solve( results: Results = super().solve(model) legacy_results, legacy_soln = self._map_results(model, results) - legacy_results = self._solution_handler( load_solutions, model, results, legacy_results, legacy_soln ) + if self.config.report_timing: + print(results.timing_info.timer) + self.config = original_config return legacy_results From c097f03d92a248835365823db52945ef05f3fe93 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 12:37:06 -0600 Subject: [PATCH 0963/3044] Initial draft of a new numpy-based Gurobi Direct interface --- pyomo/contrib/solver/gurobi_direct.py | 349 ++++++++++++++++++++++++++ pyomo/contrib/solver/plugins.py | 6 + 2 files changed, 355 insertions(+) create mode 100644 pyomo/contrib/solver/gurobi_direct.py diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py new file mode 100644 index 00000000000..56047b6c2c7 --- /dev/null +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -0,0 +1,349 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import datetime +import io +import math + +from pyomo.common.config import ConfigValue +from pyomo.common.dependencies import attempt_import +from pyomo.common.shutdown import python_is_shutting_down +from pyomo.common.tee import capture_output, TeeStream +from pyomo.common.timing import HierarchicalTimer + +from pyomo.contrib.solver.base import SolverBase +from pyomo.contrib.solver.config import BranchAndBoundConfig +from pyomo.contrib.solver.results import Results, SolutionStatus, TerminationCondition +from pyomo.contrib.solver.solution import SolutionLoaderBase + +from pyomo.core.staleflag import StaleFlagManager + +from pyomo.repn.plugins.standard_form import LinearStandardFormCompiler + +gurobipy, gurobipy_available = attempt_import('gurobipy') + + +class GurobiConfig(BranchAndBoundConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(GurobiConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + self.use_mipstart: bool = self.declare( + 'use_mipstart', + ConfigValue( + default=False, + domain=bool, + description="If True, the values of the integer variables will be passed to Gurobi.", + ), + ) + + +class GurobiDirectSolutionLoader(SolutionLoaderBase): + def __init__(self, grb_model, grb_vars, pyo_vars): + self._grb_model = grb_model + self._grb_vars = grb_vars + self._pyo_vars = pyo_vars + GurobiDirect._num_instances += 1 + + def __del__(self): + if not python_is_shutting_down(): + GurobiDirect._num_instances -= 1 + if GurobiDirect._num_instances == 0: + GurobiDirect.release_license() + + def load_vars(self, vars_to_load=None, solution_number=0): + assert vars_to_load is None + assert solution_number == 0 + for p_var, g_var in zip(self._pyo_vars, self._grb_vars.x.tolist()): + p_var.set_value(g_var, skip_validation=True) + + def get_primals(self, vars_to_load=None): + assert vars_to_load is None + assert solution_number == 0 + return ComponentMap(zip(self._pyo_vars, self._grb_vars.x.tolist())) + + +class GurobiDirect(SolverBase): + CONFIG = GurobiConfig() + + _available = None + _num_instances = 0 + + def __init__(self, **kwds): + super().__init__(**kwds) + GurobiDirect._num_instances += 1 + + def available(self): + if not gurobipy_available: # this triggers the deferred import + return self.Availability.NotFound + elif self._available == self.Availability.BadVersion: + return self.Availability.BadVersion + else: + return self._check_license() + + def _check_license(self): + avail = False + try: + # Gurobipy writes out license file information when creating + # the environment + with capture_output(capture_fd=True): + m = gurobipy.Model() + avail = True + except gurobipy.GurobiError: + avail = False + + if avail: + if self._available is None: + self._available = GurobiDirect._check_full_license(m) + return self._available + else: + return self.Availability.BadLicense + + @classmethod + def _check_full_license(cls, model=None): + if model is None: + model = gurobipy.Model() + model.setParam('OutputFlag', 0) + try: + model.addVars(range(2001)) + model.optimize() + return cls.Availability.FullLicense + except gurobipy.GurobiError: + return cls.Availability.LimitedLicense + + def __del__(self): + if not python_is_shutting_down(): + GurobiDirect._num_instances -= 1 + if GurobiDirect._num_instances == 0: + self.release_license() + + @staticmethod + def release_license(): + if gurobipy_available: + with capture_output(capture_fd=True): + gurobipy.disposeDefaultEnv() + + def version(self): + version = ( + gurobipy.GRB.VERSION_MAJOR, + gurobipy.GRB.VERSION_MINOR, + gurobipy.GRB.VERSION_TECHNICAL, + ) + return version + + def solve(self, model, **kwds) -> Results: + start_timestamp = datetime.datetime.now(datetime.timezone.utc) + self._config = config = self.config(value=kwds, preserve_implicit=True) + StaleFlagManager.mark_all_as_stale() + if config.timer is None: + config.timer = HierarchicalTimer() + timer = config.timer + + timer.start('compile_model') + repn = LinearStandardFormCompiler().write(model, mixed_form=True) + timer.stop('compile_model') + + timer.start('prepare_matrices') + inf = float('inf') + ninf = -inf + lb = [] + ub = [] + for v in repn.columns: + _l, _u = v.bounds + if _l is None: + _l = ninf + if _u is None: + _u = inf + lb.append(_l) + ub.append(_u) + vtype = [ + ( + gurobipy.GRB.CONTINUOUS + if v.is_continuous() + else ( + gurobipy.GRB.BINARY + if v.is_binary() + else gurobipy.GRB.INTEGER if v.is_integer() else '?' + ) + ) + for v in repn.columns + ] + sense_type = '>=<' + sense = [sense_type[r[1] + 1] for r in repn.rows] + timer.stop('prepare_matrices') + + ostreams = [io.StringIO()] + config.tee + + try: + orig_cwd = os.getcwd() + if self._config.working_directory: + os.chdir(self._config.working_directory) + with TeeStream(*ostreams) as t, capture_output(t.STDOUT, capture_fd=False): + gurobi_model = gurobipy.Model() + + timer.start('transfer_model') + x = gurobi_model.addMVar( + len(repn.columns), + lb=lb, + ub=ub, + obj=repn.c.todense()[0], + vtype=vtype, + ) + A = gurobi_model.addMConstr(repn.A, x, sense, repn.rhs) + # gurobi_model.update() + timer.stop('transfer_model') + + options = config.solver_options + + gurobi_model.setParam('LogToConsole', 1) + + if config.threads is not None: + gurobi_model.setParam('Threads', config.threads) + if config.time_limit is not None: + gurobi_model.setParam('TimeLimit', config.time_limit) + if config.rel_gap is not None: + gurobi_model.setParam('MIPGap', config.rel_gap) + if config.abs_gap is not None: + gurobi_model.setParam('MIPGapAbs', config.abs_gap) + + if config.use_mipstart: + raise MouseTrap("MIPSTART not yet supported") + + for key, option in options.items(): + gurobi_model.setParam(key, option) + + timer.start('optimize') + gurobi_model.optimize() + timer.stop('optimize') + finally: + os.chdir(orig_cwd) + + res = self._postsolve( + timer, GurobiDirectSolutionLoader(gurobi_model, x, repn.columns) + ) + res.solver_configuration = config + res.solver_name = 'Gurobi' + res.solver_version = self.version() + res.solver_log = ostreams[0].getvalue() + + end_timestamp = datetime.datetime.now(datetime.timezone.utc) + res.timing_info.start_timestamp = start_timestamp + res.timing_info.wall_time = (end_timestamp - start_timestamp).total_seconds() + res.timing_info.timer = timer + return res + + def _postsolve(self, timer: HierarchicalTimer, loader): + config = self._config + + gprob = loader._grb_model + grb = gurobipy.GRB + status = gprob.Status + + results = Results() + results.solution_loader = loader + results.timing_info.gurobi_time = gprob.Runtime + + if gprob.SolCount > 0: + if status == grb.OPTIMAL: + results.solution_status = SolutionStatus.optimal + else: + results.solution_status = SolutionStatus.feasible + else: + results.solution_status = SolutionStatus.noSolution + + if status == grb.LOADED: # problem is loaded, but no solution + results.termination_condition = TerminationCondition.unknown + elif status == grb.OPTIMAL: # optimal + results.termination_condition = ( + TerminationCondition.convergenceCriteriaSatisfied + ) + elif status == grb.INFEASIBLE: + results.termination_condition = TerminationCondition.provenInfeasible + elif status == grb.INF_OR_UNBD: + results.termination_condition = TerminationCondition.infeasibleOrUnbounded + elif status == grb.UNBOUNDED: + results.termination_condition = TerminationCondition.unbounded + elif status == grb.CUTOFF: + results.termination_condition = TerminationCondition.objectiveLimit + elif status == grb.ITERATION_LIMIT: + results.termination_condition = TerminationCondition.iterationLimit + elif status == grb.NODE_LIMIT: + results.termination_condition = TerminationCondition.iterationLimit + elif status == grb.TIME_LIMIT: + results.termination_condition = TerminationCondition.maxTimeLimit + elif status == grb.SOLUTION_LIMIT: + results.termination_condition = TerminationCondition.unknown + elif status == grb.INTERRUPTED: + results.termination_condition = TerminationCondition.interrupted + elif status == grb.NUMERIC: + results.termination_condition = TerminationCondition.unknown + elif status == grb.SUBOPTIMAL: + results.termination_condition = TerminationCondition.unknown + elif status == grb.USER_OBJ_LIMIT: + results.termination_condition = TerminationCondition.objectiveLimit + else: + results.termination_condition = TerminationCondition.unknown + + if ( + results.termination_condition + != TerminationCondition.convergenceCriteriaSatisfied + and config.raise_exception_on_nonoptimal_result + ): + raise RuntimeError( + 'Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' + ) + + results.incumbent_objective = None + results.objective_bound = None + try: + results.incumbent_objective = gprob.ObjVal + except (gurobipy.GurobiError, AttributeError): + results.incumbent_objective = None + try: + results.objective_bound = gprob.ObjBound + except (gurobipy.GurobiError, AttributeError): + if self._objective.sense == minimize: + results.objective_bound = -math.inf + else: + results.objective_bound = math.inf + + if results.incumbent_objective is not None and not math.isfinite( + results.incumbent_objective + ): + results.incumbent_objective = None + + results.iteration_count = gprob.getAttr('IterCount') + + timer.start('load solution') + if config.load_solutions: + if gprob.SolCount > 0: + results.solution_loader.load_vars() + else: + raise RuntimeError( + 'A feasible solution was not found, so no solution can be loaded.' + 'Please set opt.config.load_solutions=False and check ' + 'results.solution_status and ' + 'results.incumbent_objective before loading a solution.' + ) + timer.stop('load solution') + + return results diff --git a/pyomo/contrib/solver/plugins.py b/pyomo/contrib/solver/plugins.py index c7da41463a2..b0beef185de 100644 --- a/pyomo/contrib/solver/plugins.py +++ b/pyomo/contrib/solver/plugins.py @@ -13,6 +13,7 @@ from .factory import SolverFactory from .ipopt import Ipopt from .gurobi import Gurobi +from .gurobi_direct import GurobiDirect def load(): @@ -22,3 +23,8 @@ def load(): SolverFactory.register( name='gurobi', legacy_name='gurobi_v2', doc='New interface to Gurobi' )(Gurobi) + SolverFactory.register( + name='gurobi_direct', + legacy_name='gurobi_direct_v2', + doc='Direct (scipy-based) interface to Gurobi', + )(GurobiDirect) From 3f2b62a2d9f31881f8130882d0d7fe22daa495ad Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 12:35:12 -0600 Subject: [PATCH 0964/3044] Clean up automatic LegacySolverFactory registrations --- pyomo/contrib/solver/factory.py | 4 +++- pyomo/contrib/solver/ipopt.py | 2 -- pyomo/contrib/solver/plugins.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index 91ce92a9dee..8861534bd01 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -27,7 +27,9 @@ def decorator(cls): class LegacySolver(LegacySolverWrapper, cls): pass - LegacySolverFactory.register(legacy_name, doc)(LegacySolver) + LegacySolverFactory.register(legacy_name + " (new interface)", doc)( + LegacySolver + ) return cls diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index edc5799ae20..5f601b7a9f7 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -30,7 +30,6 @@ from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo from pyomo.contrib.solver.base import SolverBase from pyomo.contrib.solver.config import SolverConfig -from pyomo.contrib.solver.factory import SolverFactory from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus from pyomo.contrib.solver.sol_reader import parse_sol_file from pyomo.contrib.solver.solution import SolSolutionLoader @@ -197,7 +196,6 @@ def get_reduced_costs( } -@SolverFactory.register('ipopt_v2', doc='The ipopt NLP solver (new interface)') class Ipopt(SolverBase): CONFIG = IpoptConfig() diff --git a/pyomo/contrib/solver/plugins.py b/pyomo/contrib/solver/plugins.py index c7da41463a2..1a471d3bd06 100644 --- a/pyomo/contrib/solver/plugins.py +++ b/pyomo/contrib/solver/plugins.py @@ -17,8 +17,8 @@ def load(): SolverFactory.register( - name='ipopt', legacy_name='ipopt_v2', doc='The IPOPT NLP solver (new interface)' + name='ipopt', legacy_name='ipopt_v2', doc='The IPOPT NLP solver' )(Ipopt) SolverFactory.register( - name='gurobi', legacy_name='gurobi_v2', doc='New interface to Gurobi' + name='gurobi', legacy_name='gurobi_v2', doc='Persistent interface to Gurobi' )(Gurobi) From f10ef5654828975d532354178f5fa7f96ac037d8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 15:22:32 -0600 Subject: [PATCH 0965/3044] Adding missing import --- pyomo/contrib/solver/gurobi_direct.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 56047b6c2c7..be06c17b63b 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -12,6 +12,7 @@ import datetime import io import math +import os from pyomo.common.config import ConfigValue from pyomo.common.dependencies import attempt_import From 7f0f3004a15baa555f9ae32e95c1ae6fa81b24c2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 16:05:41 -0600 Subject: [PATCH 0966/3044] Accept / ignore None in certain _map_config arguments --- pyomo/contrib/solver/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 9e0356c9c21..8840265763e 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -401,11 +401,11 @@ def _map_config( self.config.raise_exception_on_nonoptimal_result = ( raise_exception_on_nonoptimal_result ) - if solver_io is not NOTSET: + if solver_io is not NOTSET and solver_io is not None: raise NotImplementedError('Still working on this') - if suffixes is not NOTSET: + if suffixes is not NOTSET and suffixes is not None: raise NotImplementedError('Still working on this') - if logfile is not NOTSET: + if logfile is not NOTSET and logfile is not None: raise NotImplementedError('Still working on this') if keepfiles or 'keepfiles' in self.config: cwd = os.getcwd() From 2e210538774ab647486512f743a765f504a53f05 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 16:06:05 -0600 Subject: [PATCH 0967/3044] Update tests to track changes in the LegacySolverWrapper --- pyomo/contrib/solver/tests/solvers/test_ipopt.py | 2 +- pyomo/contrib/solver/tests/unit/test_base.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/tests/solvers/test_ipopt.py b/pyomo/contrib/solver/tests/solvers/test_ipopt.py index dc6bcf24855..d5d82981ed8 100644 --- a/pyomo/contrib/solver/tests/solvers/test_ipopt.py +++ b/pyomo/contrib/solver/tests/solvers/test_ipopt.py @@ -48,7 +48,7 @@ def test_ipopt_config(self): self.assertIsInstance(config.executable, ExecutableData) # Test custom initialization - solver = SolverFactory('ipopt_v2', executable='/path/to/exe') + solver = SolverFactory('ipopt', executable='/path/to/exe') self.assertFalse(solver.config.tee) self.assertTrue(solver.config.executable.startswith('/path')) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 74c495b86cc..5c138a6522b 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -178,7 +178,7 @@ def test_context_manager(self): class TestLegacySolverWrapper(unittest.TestCase): def test_class_method_list(self): - expected_list = ['available', 'license_is_valid', 'solve'] + expected_list = ['available', 'config_block', 'license_is_valid', 'set_options', 'solve'] method_list = [ method for method in dir(base.LegacySolverWrapper) @@ -207,9 +207,7 @@ def test_map_config(self): self.assertTrue(instance.config.tee) self.assertFalse(instance.config.load_solutions) self.assertEqual(instance.config.time_limit, 20) - # Report timing shouldn't be created because it no longer exists - with self.assertRaises(AttributeError): - print(instance.config.report_timing) + self.assertEqual(instance.config.report_timing, True) # Keepfiles should not be created because we did not declare keepfiles on # the original config with self.assertRaises(AttributeError): From 0465c89d94b58223694fed84334ce603c9d66f15 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 16:09:37 -0600 Subject: [PATCH 0968/3044] bugfix: correct option name --- pyomo/contrib/solver/gurobi_direct.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index be06c17b63b..7b5ec6ed904 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -196,8 +196,8 @@ def solve(self, model, **kwds) -> Results: try: orig_cwd = os.getcwd() - if self._config.working_directory: - os.chdir(self._config.working_directory) + if self._config.working_dir: + os.chdir(self._config.working_dir) with TeeStream(*ostreams) as t, capture_output(t.STDOUT, capture_fd=False): gurobi_model = gurobipy.Model() From cc0a9ecfdc06e4e52df260a91e09a9b962756c86 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 18 Mar 2024 17:26:20 -0600 Subject: [PATCH 0969/3044] Adding infrastructure to both choose a triangulation and record the choice, but not J1 implementation yet. --- pyomo/contrib/piecewise/__init__.py | 1 + .../piecewise/piecewise_linear_function.py | 41 +++++++++++++++---- .../tests/test_piecewise_linear_function.py | 17 +++++++- pyomo/contrib/piecewise/triangulations.py | 35 ++++++++++++++++ 4 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 pyomo/contrib/piecewise/triangulations.py diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 37873c83b3b..832932c9b7d 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -33,3 +33,4 @@ from pyomo.contrib.piecewise.transform.convex_combination import ( ConvexCombinationTransformation, ) +from pyomo.contrib.piecewise.triangulations import Triangulation diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 66ca02ad125..0b82531f09c 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -19,6 +19,10 @@ from pyomo.contrib.piecewise.piecewise_linear_expression import ( PiecewiseLinearExpression, ) +from pyomo.contrib.piecewise.triangulations import ( + get_j1_triangulation, + Triangulation, +) from pyomo.core import Any, NonNegativeIntegers, value, Var from pyomo.core.base.block import _BlockData, Block from pyomo.core.base.component import ModelComponentFactory @@ -49,6 +53,11 @@ def __init__(self, component=None): # These will always be tuples, even when we only have one dimension. self._points = [] self._linear_functions = [] + self._triangulation = None + + @property + def triangulation(self): + return self._triangulation def __call__(self, *args): """ @@ -251,6 +260,7 @@ def __init__(self, *args, **kwargs): _linear_functions = kwargs.pop('linear_functions', None) _tabular_data_arg = kwargs.pop('tabular_data', None) _tabular_data_rule_arg = kwargs.pop('tabular_data_rule', None) + _triangulation_rule_arg = kwargs.pop('triangulation', Triangulation.Delaunay) kwargs.setdefault('ctype', PiecewiseLinearFunction) Block.__init__(self, *args, **kwargs) @@ -269,6 +279,8 @@ def __init__(self, *args, **kwargs): self._tabular_data_rule = Initializer( _tabular_data_rule_arg, treat_sequences_as_mappings=False ) + self._triangulation_rule = Initializer(_triangulation_rule_arg, + treat_sequences_as_mappings=False) def _get_dimension_from_points(self, points): if len(points) < 1: @@ -284,12 +296,23 @@ def _get_dimension_from_points(self, points): return dimension - def _construct_simplices_from_multivariate_points(self, obj, points, dimension): - try: - triangulation = spatial.Delaunay(points) - except (spatial.QhullError, ValueError) as error: - logger.error("Unable to triangulate the set of input points.") - raise + def _construct_simplices_from_multivariate_points(self, obj, parent, points, + dimension): + tri = self._triangulation_rule(parent, obj._index) + if tri == Triangulation.Delaunay: + try: + triangulation = spatial.Delaunay(points) + except (spatial.QhullError, ValueError) as error: + logger.error("Unable to triangulate the set of input points.") + raise + obj._triangulation = tri + elif tri == Triangulation.J1: + triangulation = get_j1_triangulation(points, dimension) + obj._triangulation = tri + else: + raise ValueError( + "Unrecognized triangulation specified for '%s': %s" + % (obj, tri)) # Get the points for the triangulation because they might not all be # there if any were coplanar. @@ -350,7 +373,8 @@ def _construct_from_function_and_points(self, obj, parent, nonlinear_function): obj, nonlinear_function ) - self._construct_simplices_from_multivariate_points(obj, points, dimension) + self._construct_simplices_from_multivariate_points(obj, parent, points, + dimension) return self._construct_from_function_and_simplices( obj, parent, nonlinear_function, simplices_are_user_defined=False ) @@ -460,7 +484,8 @@ def _construct_from_tabular_data(self, obj, parent, nonlinear_function): obj, _tabular_data_functor(tabular_data, tupleize=True) ) - self._construct_simplices_from_multivariate_points(obj, points, dimension) + self._construct_simplices_from_multivariate_points(obj, parent, points, + dimension) return self._construct_from_function_and_simplices( obj, parent, _tabular_data_functor(tabular_data) ) diff --git a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py index 571601fefbc..5dd331d7943 100644 --- a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py @@ -16,7 +16,7 @@ from pyomo.common.dependencies import attempt_import from pyomo.common.log import LoggingIntercept import pyomo.common.unittest as unittest -from pyomo.contrib.piecewise import PiecewiseLinearFunction +from pyomo.contrib.piecewise import PiecewiseLinearFunction, Triangulation from pyomo.core.expr.compare import ( assertExpressionsEqual, assertExpressionsStructurallyEqual, @@ -118,6 +118,13 @@ def test_pw_linear_approx_of_ln_x_tabular_data(self): ) self.check_ln_x_approx(m.pw, m.x) + def test_pw_linear_approx_of_ln_x_j1(self): + m = self.make_ln_x_model() + m.pw = PiecewiseLinearFunction( + points=[1, 3, 6, 10], triangulation=Triangulation.J1, function=m.f) + self.check_ln_x_approx(m.pw, m.x) + self.assertEqual(m.pw.triangulation, Triangulation.J1) + def test_use_pw_function_in_constraint(self): m = self.make_ln_x_model() m.pw = PiecewiseLinearFunction( @@ -302,6 +309,14 @@ def test_pw_linear_approx_of_paraboloid_points(self): ) self.check_pw_linear_approximation(m) + def test_pw_linear_approx_of_paraboloid_j1(self): + m = self.make_model() + m.pw = PiecewiseLinearFunction( + points=[(0, 1), (0, 4), (0, 7), (3, 1), (3, 4), (3, 7)], function=m.g, + triangulation=Triangulation.J1 + ) + self.check_pw_linear_approximation(m) + @unittest.skipUnless(scipy_available, "scipy is not available") def test_pw_linear_approx_tabular_data(self): m = self.make_model() diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py new file mode 100644 index 00000000000..d67cd302060 --- /dev/null +++ b/pyomo/contrib/piecewise/triangulations.py @@ -0,0 +1,35 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pytest import set_trace + + +class Triangulation: + Delaunay = 1 + J1 = 2 + +def get_j1_triangulation(points, dimension): + if dimension == 2: + return _get_j1_triangulation_2d(points, dimension) + elif dimension == 3: + return _get_j1_triangulation_3d(points, dimension) + else: + return _get_j1_triangulation_for_more_than_4d(points, dimension) + +def _get_j1_triangulation_2d(points, dimension): + # I think this means coding up the proof by picture... + pass + +def _get_j1_triangulation_3d(points, dimension): + pass + +def _get_j1_triangulation_for_more_than_4d(points, dimension): + pass From 9d1b91de17f843b5625cf1292df85db7d19d2985 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 21:27:27 -0600 Subject: [PATCH 0970/3044] NFC: apply black --- pyomo/contrib/solver/tests/unit/test_base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 5c138a6522b..179d9823679 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -178,7 +178,13 @@ def test_context_manager(self): class TestLegacySolverWrapper(unittest.TestCase): def test_class_method_list(self): - expected_list = ['available', 'config_block', 'license_is_valid', 'set_options', 'solve'] + expected_list = [ + 'available', + 'config_block', + 'license_is_valid', + 'set_options', + 'solve', + ] method_list = [ method for method in dir(base.LegacySolverWrapper) From 9fd202e7b4bca74a11befbd422da925015216c60 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 18 Mar 2024 22:58:58 -0600 Subject: [PATCH 0971/3044] update GDP baselines to reflect change in variable order? --- pyomo/gdp/tests/jobshop_large_hull.lp | 356 +++++++++++++------------- pyomo/gdp/tests/jobshop_small_hull.lp | 68 ++--- 2 files changed, 212 insertions(+), 212 deletions(-) diff --git a/pyomo/gdp/tests/jobshop_large_hull.lp b/pyomo/gdp/tests/jobshop_large_hull.lp index ee8ee0a73d2..f0a9d3ccbf0 100644 --- a/pyomo/gdp/tests/jobshop_large_hull.lp +++ b/pyomo/gdp/tests/jobshop_large_hull.lp @@ -66,17 +66,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: +1 t(C) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(6)_: +1 t(D) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ @@ -114,17 +114,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(11)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(12)_: -+1 t(A) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(13)_: +1 t(F) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(13)_: ++1 t(A) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(14)_: +1 t(F) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ @@ -150,29 +150,29 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(17)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(18)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(19)_: +1 t(C) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(20)_: +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(19)_: +1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(21)_: +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(20)_: +1 t(D) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(21)_: ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(22)_: +1 t(D) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ @@ -186,17 +186,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(23)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(24)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(25)_: +1 t(E) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(25)_: ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(26)_: +1 t(E) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ @@ -234,17 +234,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(31)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(32)_: -+1 t(B) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(33)_: +1 t(G) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(33)_: ++1 t(B) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(34)_: +1 t(G) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_disaggregatedVars__t(G)_ @@ -294,17 +294,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(41)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(42)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(43)_: +1 t(F) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(43)_: ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(44)_: +1 t(F) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ @@ -342,17 +342,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(49)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(50)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(51)_: +1 t(E) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(51)_: ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(52)_: +1 t(E) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ @@ -390,17 +390,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(57)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(58)_: -+1 t(D) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(59)_: +1 t(G) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(59)_: ++1 t(D) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(60)_: +1 t(G) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ @@ -426,17 +426,17 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(63)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(64)_: -+1 t(E) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ -= 0 - -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(65)_: +1 t(G) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ = 0 +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(65)_: ++1 t(E) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ += 0 + c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(66)_: +1 t(G) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ @@ -701,34 +701,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +6.0 NoClash(A_C_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ -92 NoClash(A_C_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ -92 NoClash(A_C_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ +3.0 NoClash(A_C_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ -92 NoClash(A_C_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ -92 NoClash(A_C_1_1)_binary_indicator_var <= 0 @@ -827,34 +827,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)__t(A)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +2.0 NoClash(A_F_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ -92 NoClash(A_F_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ -92 NoClash(A_F_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ +3.0 NoClash(A_F_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ -92 NoClash(A_F_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ -92 NoClash(A_F_1_1)_binary_indicator_var <= 0 @@ -923,66 +923,66 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)__t(A)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +9.0 NoClash(B_C_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ -92 NoClash(B_C_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ -92 NoClash(B_C_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ -3.0 NoClash(B_C_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ -92 NoClash(B_C_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ -92 NoClash(B_C_2_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +8.0 NoClash(B_D_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ -92 NoClash(B_D_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ -92 NoClash(B_D_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ +3.0 NoClash(B_D_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ -92 NoClash(B_D_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ -92 NoClash(B_D_2_1)_binary_indicator_var <= 0 @@ -1019,34 +1019,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)__t(B)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +4.0 NoClash(B_E_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ -92 NoClash(B_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ -92 NoClash(B_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ +3.0 NoClash(B_E_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ -92 NoClash(B_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ -92 NoClash(B_E_2_1)_binary_indicator_var <= 0 @@ -1146,34 +1146,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)__t(B)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +8.0 NoClash(B_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ -92 NoClash(B_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ -92 NoClash(B_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ +3.0 NoClash(B_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ -92 NoClash(B_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ -92 NoClash(B_G_2_1)_binary_indicator_var <= 0 @@ -1306,34 +1306,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)__t(C)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +2.0 NoClash(C_F_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ -92 NoClash(C_F_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ -92 NoClash(C_F_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ +6.0 NoClash(C_F_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(F)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ -92 NoClash(C_F_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(F)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ -92 NoClash(C_F_1_1)_binary_indicator_var <= 0 @@ -1434,34 +1434,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)__t(C)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +4.0 NoClash(D_E_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ -92 NoClash(D_E_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ -92 NoClash(D_E_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ +8.0 NoClash(D_E_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ -92 NoClash(D_E_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ -92 NoClash(D_E_2_1)_binary_indicator_var <= 0 @@ -1562,34 +1562,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)__t(D)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +8.0 NoClash(D_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ -92 NoClash(D_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ -92 NoClash(D_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ +8.0 NoClash(D_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(D)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ -92 NoClash(D_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)__t(D)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ -92 NoClash(D_G_2_1)_binary_indicator_var <= 0 @@ -1657,34 +1657,34 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)__t(E)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +8.0 NoClash(E_G_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ -92 NoClash(E_G_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ -92 NoClash(E_G_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ +4.0 NoClash(E_G_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(E)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(G)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ -92 NoClash(E_G_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(G)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)__t(E)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ -92 NoClash(E_G_2_1)_binary_indicator_var <= 0 @@ -1769,10 +1769,10 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(7)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(6)_disaggregatedVars__t(A)_ <= 92 @@ -1785,10 +1785,10 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(10)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(11)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(12)_disaggregatedVars__t(A)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(13)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(15)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(14)_disaggregatedVars__t(A)_ <= 92 @@ -1797,22 +1797,22 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(16)_disaggregatedVars__t(A)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(17)_disaggregatedVars__t(A)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(18)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(19)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(20)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(21)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(22)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(23)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(24)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(25)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(27)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(26)_disaggregatedVars__t(B)_ <= 92 @@ -1825,10 +1825,10 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(30)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(31)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(32)_disaggregatedVars__t(B)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(33)_disaggregatedVars__t(B)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(35)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(34)_disaggregatedVars__t(B)_ <= 92 @@ -1845,10 +1845,10 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(40)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(41)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(F)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(42)_disaggregatedVars__t(C)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(43)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(45)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(44)_disaggregatedVars__t(C)_ <= 92 @@ -1861,10 +1861,10 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(48)_disaggregatedVars__t(C)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(49)_disaggregatedVars__t(C)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(50)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(51)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(53)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(52)_disaggregatedVars__t(D)_ <= 92 @@ -1877,10 +1877,10 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(56)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(57)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(58)_disaggregatedVars__t(D)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(59)_disaggregatedVars__t(D)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(61)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(60)_disaggregatedVars__t(D)_ <= 92 @@ -1889,10 +1889,10 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(F)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(62)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(63)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ <= 92 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(G)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(64)_disaggregatedVars__t(E)_ <= 92 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(65)_disaggregatedVars__t(E)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(67)_disaggregatedVars__t(G)_ <= 92 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(66)_disaggregatedVars__t(E)_ <= 92 diff --git a/pyomo/gdp/tests/jobshop_small_hull.lp b/pyomo/gdp/tests/jobshop_small_hull.lp index ae2d738d29c..eccaa800600 100644 --- a/pyomo/gdp/tests/jobshop_small_hull.lp +++ b/pyomo/gdp/tests/jobshop_small_hull.lp @@ -34,29 +34,29 @@ c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(1)_: = 0 c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(2)_: ++1 t(C) +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ += 0 + +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: +1 t(A) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(3)_: +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: +1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(4)_: +c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: +1 t(B) -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ = 0 -c_e__pyomo_gdp_hull_reformulation_disaggregationConstraints(5)_: -+1 t(C) --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ -= 0 - c_e__pyomo_gdp_hull_reformulation_disj_xor(A_B_3)_: +1 NoClash(A_B_3_0)_binary_indicator_var +1 NoClash(A_B_3_1)_binary_indicator_var @@ -104,66 +104,66 @@ c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)__t(A)_bounds_(ub)_: <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +2.0 NoClash(A_C_1_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ -19 NoClash(A_C_1_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ -19 NoClash(A_C_1_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ +5.0 NoClash(A_C_1_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ -19 NoClash(A_C_1_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)__t(A)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ -19 NoClash(A_C_1_1)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_transformedConstraints(c_0_ub)_: --1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ +1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +-1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ +6.0 NoClash(B_C_2_0)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ -19 NoClash(B_C_2_0)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ -19 NoClash(B_C_2_0)_binary_indicator_var <= 0 c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_transformedConstraints(c_0_ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ -1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ +1 NoClash(B_C_2_1)_binary_indicator_var <= 0.0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(B)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ -19 NoClash(B_C_2_1)_binary_indicator_var <= 0 -c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(C)_bounds_(ub)_: -+1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ +c_u__pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)__t(B)_bounds_(ub)_: ++1 _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ -19 NoClash(B_C_2_1)_binary_indicator_var <= 0 @@ -176,14 +176,14 @@ bounds 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(B)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(0)_disaggregatedVars__t(A)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(1)_disaggregatedVars__t(A)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(C)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(C)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ <= 19 - 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(2)_disaggregatedVars__t(A)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(3)_disaggregatedVars__t(A)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(C)_ <= 19 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(C)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(4)_disaggregatedVars__t(B)_ <= 19 + 0 <= _pyomo_gdp_hull_reformulation_relaxedDisjuncts(5)_disaggregatedVars__t(B)_ <= 19 0 <= NoClash(A_B_3_0)_binary_indicator_var <= 1 0 <= NoClash(A_B_3_1)_binary_indicator_var <= 1 0 <= NoClash(A_C_1_0)_binary_indicator_var <= 1 From ae439ad8be5df810443fea62d1d0ba6cffe41feb Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 18 Mar 2024 23:04:07 -0600 Subject: [PATCH 0972/3044] use get_vars_from_components in create_subsystem_block --- pyomo/util/subsystems.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 5789829ac54..4a9b96fa89b 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -14,7 +14,7 @@ from pyomo.core.expr.visitor import identify_variables from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.common.modeling import unique_component_name - +from pyomo.util.vars_from_expressions import get_vars_from_components from pyomo.core.base.constraint import Constraint from pyomo.core.base.expression import Expression from pyomo.core.base.objective import Objective @@ -131,12 +131,12 @@ def create_subsystem_block(constraints, variables=None, include_fixed=False): block.vars = Reference(variables) block.cons = Reference(constraints) var_set = ComponentSet(variables) - input_vars = [] - for con in constraints: - for var in identify_variables(con.expr, include_fixed=include_fixed): - if var not in var_set: - input_vars.append(var) - var_set.add(var) + input_vars = [ + var for var in get_vars_from_components( + block, Constraint, include_fixed=include_fixed + ) + if var not in var_set + ] block.input_vars = Reference(input_vars) add_local_external_functions(block) return block From be31a20946cd24a5a8b58ebff24992ea051427bf Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 18 Mar 2024 23:09:52 -0600 Subject: [PATCH 0973/3044] formatting fix --- pyomo/util/subsystems.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 4a9b96fa89b..00c3b85ce47 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -131,12 +131,10 @@ def create_subsystem_block(constraints, variables=None, include_fixed=False): block.vars = Reference(variables) block.cons = Reference(constraints) var_set = ComponentSet(variables) - input_vars = [ - var for var in get_vars_from_components( - block, Constraint, include_fixed=include_fixed - ) - if var not in var_set - ] + input_vars = [] + for var in get_vars_from_components(block, Constraint, include_fixed=include_fixed): + if var not in var_set: + input_vars.append(var) block.input_vars = Reference(input_vars) add_local_external_functions(block) return block From 08b9d93c3635776948b6145b07e0b03dfde65c87 Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 18 Mar 2024 23:17:39 -0600 Subject: [PATCH 0974/3044] remove unused imports --- pyomo/util/vars_from_expressions.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyomo/util/vars_from_expressions.py b/pyomo/util/vars_from_expressions.py index 62953af456b..878a1a13b58 100644 --- a/pyomo/util/vars_from_expressions.py +++ b/pyomo/util/vars_from_expressions.py @@ -17,8 +17,7 @@ actually in the subtree or not. """ from pyomo.core import Block -from pyomo.core.expr.visitor import _StreamVariableVisitor -from pyomo.core.expr import identify_variables +import pyomo.core.expr as EXPR def get_vars_from_components( @@ -52,7 +51,7 @@ def get_vars_from_components( descend_into=descend_into, descent_order=descent_order, ): - for var in identify_variables( + for var in EXPR.identify_variables( constraint.expr, include_fixed=include_fixed, named_expression_cache=named_expression_cache, From 443826da5ab3485a20439369b167c5e6a363ceff Mon Sep 17 00:00:00 2001 From: robbybp Date: Mon, 18 Mar 2024 23:22:16 -0600 Subject: [PATCH 0975/3044] remove old _VariableVisitor and rename new visitor to _VariableVisitor --- pyomo/core/expr/visitor.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/pyomo/core/expr/visitor.py b/pyomo/core/expr/visitor.py index 2fddca22c5f..08015f8b42c 100644 --- a/pyomo/core/expr/visitor.py +++ b/pyomo/core/expr/visitor.py @@ -1373,22 +1373,7 @@ def identify_components(expr, component_types): # ===================================================== -class _VariableVisitor(SimpleExpressionVisitor): - def __init__(self): - self.seen = set() - - def visit(self, node): - if node.__class__ in nonpyomo_leaf_types: - return - - if node.is_variable_type(): - if id(node) in self.seen: - return - self.seen.add(id(node)) - return node - - -class _StreamVariableVisitor(StreamBasedExpressionVisitor): +class _VariableVisitor(StreamBasedExpressionVisitor): def __init__(self, include_fixed=False, named_expression_cache=None): """Visitor that collects all unique variables participating in an expression @@ -1522,7 +1507,7 @@ def identify_variables(expr, include_fixed=True, named_expression_cache=None): """ if named_expression_cache is None: named_expression_cache = {} - visitor = _StreamVariableVisitor( + visitor = _VariableVisitor( named_expression_cache=named_expression_cache, include_fixed=include_fixed ) variables = visitor.walk_expression(expr) From 3e7e7a2ad9559e3ffbfdeab2ca21932d55179d4a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 23:22:17 -0600 Subject: [PATCH 0976/3044] bugfix: update doc not solver name --- pyomo/contrib/solver/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index 8861534bd01..99fbcc3a6d0 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -27,7 +27,7 @@ def decorator(cls): class LegacySolver(LegacySolverWrapper, cls): pass - LegacySolverFactory.register(legacy_name + " (new interface)", doc)( + LegacySolverFactory.register(legacy_name, doc + " (new interface)")( LegacySolver ) From db5d6dc96181f734f5f3410987ce4c0a1e3a6e7b Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 20 Mar 2024 10:53:00 +0100 Subject: [PATCH 0977/3044] Fixed maingopy import --- pyomo/contrib/appsi/solvers/maingo.py | 246 +---------------- .../appsi/solvers/maingo_solvermodel.py | 257 ++++++++++++++++++ 2 files changed, 272 insertions(+), 231 deletions(-) create mode 100644 pyomo/contrib/appsi/solvers/maingo_solvermodel.py diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 614e12d227b..29464e6a876 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -25,7 +25,7 @@ from pyomo.core.base.expression import ScalarExpression from pyomo.core.base.param import _ParamData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.var import Var, _GeneralVarData +from pyomo.core.base.var import Var, ScalarVar, _GeneralVarData import pyomo.core.expr.expr_common as common import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import ( @@ -40,7 +40,18 @@ from pyomo.core.staleflag import StaleFlagManager from pyomo.repn.util import valid_expr_ctypes_minlp -_plusMinusOne = {-1, 1} + +def _import_SolverModel(): + try: + from . import maingo_solvermodel + except ImportError: + raise + return maingo_solvermodel + + +maingo_solvermodel, solvermodel_available = attempt_import( + "maingo_solvermodel", importer=_import_SolverModel +) MaingoVar = namedtuple("MaingoVar", "type name lb ub init") @@ -103,234 +114,6 @@ def __init__(self, solver): self.solution_loader = MAiNGOSolutionLoader(solver=solver) -class SolverModel(maingopy.MAiNGOmodel): - def __init__(self, var_list, objective, con_list, idmap): - maingopy.MAiNGOmodel.__init__(self) - self._var_list = var_list - self._con_list = con_list - self._objective = objective - self._idmap = idmap - - def build_maingo_objective(self, obj, visitor): - maingo_obj = visitor.dfs_postorder_stack(obj.expr) - if obj.sense == maximize: - maingo_obj *= -1 - return maingo_obj - - def build_maingo_constraints(self, cons, visitor): - eqs = [] - ineqs = [] - for con in cons: - if con.equality: - eqs += [visitor.dfs_postorder_stack(con.body - con.lower)] - elif con.has_ub() and con.has_lb(): - ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] - ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] - elif con.has_ub(): - ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] - elif con.has_ub(): - ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] - else: - raise ValueError( - "Constraint does not have a lower " - "or an upper bound: {0} \n".format(con) - ) - return eqs, ineqs - - def get_variables(self): - return [ - maingopy.OptimizationVariable( - maingopy.Bounds(var.lb, var.ub), var.type, var.name - ) - for var in self._var_list - ] - - def get_initial_point(self): - return [ - var.init if not var.init is None else (var.lb + var.ub) / 2.0 - for var in self._var_list - ] - - def evaluate(self, maingo_vars): - visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) - result = maingopy.EvaluationContainer() - result.objective = self.build_maingo_objective(self._objective, visitor) - eqs, ineqs = self.build_maingo_constraints(self._con_list, visitor) - result.eq = eqs - result.ineq = ineqs - return result - - -LEFT_TO_RIGHT = common.OperatorAssociativity.LEFT_TO_RIGHT -RIGHT_TO_LEFT = common.OperatorAssociativity.RIGHT_TO_LEFT - - -class ToMAiNGOVisitor(EXPR.ExpressionValueVisitor): - def __init__(self, variables, idmap): - super(ToMAiNGOVisitor, self).__init__() - self.variables = variables - self.idmap = idmap - self._pyomo_func_to_maingo_func = { - "log": maingopy.log, - "log10": ToMAiNGOVisitor.maingo_log10, - "sin": maingopy.sin, - "cos": maingopy.cos, - "tan": maingopy.tan, - "cosh": maingopy.cosh, - "sinh": maingopy.sinh, - "tanh": maingopy.tanh, - "asin": maingopy.asin, - "acos": maingopy.acos, - "atan": maingopy.atan, - "exp": maingopy.exp, - "sqrt": maingopy.sqrt, - "asinh": ToMAiNGOVisitor.maingo_asinh, - "acosh": ToMAiNGOVisitor.maingo_acosh, - "atanh": ToMAiNGOVisitor.maingo_atanh, - } - - @classmethod - def maingo_log10(cls, x): - return maingopy.log(x) / math.log(10) - - @classmethod - def maingo_asinh(cls, x): - return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) + 1)) - - @classmethod - def maingo_acosh(cls, x): - return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) - 1)) - - @classmethod - def maingo_atanh(cls, x): - return 0.5 * maingopy.log(x + 1) - 0.5 * maingopy.log(1 - x) - - def visit(self, node, values): - """Visit nodes that have been expanded""" - for i, val in enumerate(values): - arg = node._args_[i] - - if arg is None: - values[i] = "Undefined" - elif arg.__class__ in native_numeric_types: - pass - elif arg.__class__ in nonpyomo_leaf_types: - values[i] = val - else: - parens = False - if arg.is_expression_type() and node.PRECEDENCE is not None: - if arg.PRECEDENCE is None: - pass - elif node.PRECEDENCE < arg.PRECEDENCE: - parens = True - elif node.PRECEDENCE == arg.PRECEDENCE: - if i == 0: - parens = node.ASSOCIATIVITY != LEFT_TO_RIGHT - elif i == len(node._args_) - 1: - parens = node.ASSOCIATIVITY != RIGHT_TO_LEFT - else: - parens = True - if parens: - values[i] = val - - if node.__class__ in EXPR.NPV_expression_types: - return value(node) - - if node.__class__ in {EXPR.ProductExpression, EXPR.MonomialTermExpression}: - return values[0] * values[1] - - if node.__class__ in {EXPR.SumExpression}: - return sum(values) - - if node.__class__ in {EXPR.PowExpression}: - return maingopy.pow(values[0], values[1]) - - if node.__class__ in {EXPR.DivisionExpression}: - return values[0] / values[1] - - if node.__class__ in {EXPR.NegationExpression}: - return -values[0] - - if node.__class__ in {EXPR.AbsExpression}: - return maingopy.abs(values[0]) - - if node.__class__ in {EXPR.UnaryFunctionExpression}: - pyomo_func = node.getname() - maingo_func = self._pyomo_func_to_maingo_func[pyomo_func] - return maingo_func(values[0]) - - if node.__class__ in {ScalarExpression}: - return values[0] - - raise ValueError(f"Unknown function expression encountered: {node.getname()}") - - def visiting_potential_leaf(self, node): - """ - Visiting a potential leaf. - - Return True if the node is not expanded. - """ - if node.__class__ in native_types: - return True, node - - if node.is_expression_type(): - if node.__class__ is EXPR.MonomialTermExpression: - return True, self._monomial_to_maingo(node) - if node.__class__ is EXPR.LinearExpression: - return True, self._linear_to_maingo(node) - return False, None - - if node.is_component_type(): - if node.ctype not in valid_expr_ctypes_minlp: - # Make sure all components in active constraints - # are basic ctypes we know how to deal with. - raise RuntimeError( - "Unallowable component '%s' of type %s found in an active " - "constraint or objective.\nMAiNGO cannot export " - "expressions with this component type." - % (node.name, node.ctype.__name__) - ) - - if node.is_fixed(): - return True, node() - else: - assert node.is_variable_type() - maingo_var_id = self.idmap[id(node)] - maingo_var = self.variables[maingo_var_id] - return True, maingo_var - - def _monomial_to_maingo(self, node): - const, var = node.args - maingo_var_id = self.idmap[id(var)] - maingo_var = self.variables[maingo_var_id] - if const.__class__ not in native_types: - const = value(const) - if var.is_fixed(): - return const * var.value - if not const: - return 0 - if const in _plusMinusOne: - if const < 0: - return -maingo_var - else: - return maingo_var - return const * maingo_var - - def _linear_to_maingo(self, node): - values = [ - ( - self._monomial_to_maingo(arg) - if ( - arg.__class__ is EXPR.MonomialTermExpression - and not arg.arg(1).is_fixed() - ) - else value(arg) - ) - for arg in node.args - ] - return sum(values) - - class MAiNGO(PersistentBase, PersistentSolver): """ Interface to MAiNGO @@ -536,7 +319,8 @@ def set_instance(self, model): self._labeler = NumericLabeler("x") self.add_block(model) - self._solver_model = SolverModel( + + self._solver_model = maingo_solvermodel.SolverModel( var_list=self._maingo_vars, con_list=self._cons, objective=self._objective, diff --git a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py new file mode 100644 index 00000000000..4abc53ae290 --- /dev/null +++ b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py @@ -0,0 +1,257 @@ +import math + +from pyomo.common.dependencies import attempt_import +from pyomo.core.base.var import ScalarVar +import pyomo.core.expr.expr_common as common +import pyomo.core.expr as EXPR +from pyomo.core.expr.numvalue import ( + value, + is_constant, + is_fixed, + native_numeric_types, + native_types, + nonpyomo_leaf_types, +) +from pyomo.core.kernel.objective import minimize, maximize +from pyomo.repn.util import valid_expr_ctypes_minlp + + +def _import_maingopy(): + try: + import maingopy + except ImportError: + raise + return maingopy + + +maingopy, maingopy_available = attempt_import("maingopy", importer=_import_maingopy) + +_plusMinusOne = {1, -1} + +LEFT_TO_RIGHT = common.OperatorAssociativity.LEFT_TO_RIGHT +RIGHT_TO_LEFT = common.OperatorAssociativity.RIGHT_TO_LEFT + + +class ToMAiNGOVisitor(EXPR.ExpressionValueVisitor): + def __init__(self, variables, idmap): + super(ToMAiNGOVisitor, self).__init__() + self.variables = variables + self.idmap = idmap + self._pyomo_func_to_maingo_func = { + "log": maingopy.log, + "log10": ToMAiNGOVisitor.maingo_log10, + "sin": maingopy.sin, + "cos": maingopy.cos, + "tan": maingopy.tan, + "cosh": maingopy.cosh, + "sinh": maingopy.sinh, + "tanh": maingopy.tanh, + "asin": maingopy.asin, + "acos": maingopy.acos, + "atan": maingopy.atan, + "exp": maingopy.exp, + "sqrt": maingopy.sqrt, + "asinh": ToMAiNGOVisitor.maingo_asinh, + "acosh": ToMAiNGOVisitor.maingo_acosh, + "atanh": ToMAiNGOVisitor.maingo_atanh, + } + + @classmethod + def maingo_log10(cls, x): + return maingopy.log(x) / math.log(10) + + @classmethod + def maingo_asinh(cls, x): + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) + 1)) + + @classmethod + def maingo_acosh(cls, x): + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) - 1)) + + @classmethod + def maingo_atanh(cls, x): + return 0.5 * maingopy.log(x + 1) - 0.5 * maingopy.log(1 - x) + + def visit(self, node, values): + """Visit nodes that have been expanded""" + for i, val in enumerate(values): + arg = node._args_[i] + + if arg is None: + values[i] = "Undefined" + elif arg.__class__ in native_numeric_types: + pass + elif arg.__class__ in nonpyomo_leaf_types: + values[i] = val + else: + parens = False + if arg.is_expression_type() and node.PRECEDENCE is not None: + if arg.PRECEDENCE is None: + pass + elif node.PRECEDENCE < arg.PRECEDENCE: + parens = True + elif node.PRECEDENCE == arg.PRECEDENCE: + if i == 0: + parens = node.ASSOCIATIVITY != LEFT_TO_RIGHT + elif i == len(node._args_) - 1: + parens = node.ASSOCIATIVITY != RIGHT_TO_LEFT + else: + parens = True + if parens: + values[i] = val + + if node.__class__ in EXPR.NPV_expression_types: + return value(node) + + if node.__class__ in {EXPR.ProductExpression, EXPR.MonomialTermExpression}: + return values[0] * values[1] + + if node.__class__ in {EXPR.SumExpression}: + return sum(values) + + if node.__class__ in {EXPR.PowExpression}: + return maingopy.pow(values[0], values[1]) + + if node.__class__ in {EXPR.DivisionExpression}: + return values[0] / values[1] + + if node.__class__ in {EXPR.NegationExpression}: + return -values[0] + + if node.__class__ in {EXPR.AbsExpression}: + return maingopy.abs(values[0]) + + if node.__class__ in {EXPR.UnaryFunctionExpression}: + pyomo_func = node.getname() + maingo_func = self._pyomo_func_to_maingo_func[pyomo_func] + return maingo_func(values[0]) + + if node.__class__ in {ScalarExpression}: + return values[0] + + raise ValueError(f"Unknown function expression encountered: {node.getname()}") + + def visiting_potential_leaf(self, node): + """ + Visiting a potential leaf. + + Return True if the node is not expanded. + """ + if node.__class__ in native_types: + return True, node + + if node.is_expression_type(): + if node.__class__ is EXPR.MonomialTermExpression: + return True, self._monomial_to_maingo(node) + if node.__class__ is EXPR.LinearExpression: + return True, self._linear_to_maingo(node) + return False, None + + if node.is_component_type(): + if node.ctype not in valid_expr_ctypes_minlp: + # Make sure all components in active constraints + # are basic ctypes we know how to deal with. + raise RuntimeError( + "Unallowable component '%s' of type %s found in an active " + "constraint or objective.\nMAiNGO cannot export " + "expressions with this component type." + % (node.name, node.ctype.__name__) + ) + + if node.is_fixed(): + return True, node() + else: + assert node.is_variable_type() + maingo_var_id = self.idmap[id(node)] + maingo_var = self.variables[maingo_var_id] + return True, maingo_var + + def _monomial_to_maingo(self, node): + if node.__class__ is ScalarVar: + var = node + const = 1 + else: + const, var = node.args + maingo_var_id = self.idmap[id(var)] + maingo_var = self.variables[maingo_var_id] + if const.__class__ not in native_types: + const = value(const) + if var.is_fixed(): + return const * var.value + if not const: + return 0 + if const in _plusMinusOne: + if const < 0: + return -maingo_var + else: + return maingo_var + return const * maingo_var + + def _linear_to_maingo(self, node): + values = [ + ( + self._monomial_to_maingo(arg) + if (arg.__class__ in {EXPR.MonomialTermExpression, ScalarVar}) + else (value(arg)) + ) + for arg in node.args + ] + return sum(values) + + +class SolverModel(maingopy.MAiNGOmodel): + def __init__(self, var_list, objective, con_list, idmap): + maingopy.MAiNGOmodel.__init__(self) + self._var_list = var_list + self._con_list = con_list + self._objective = objective + self._idmap = idmap + + def build_maingo_objective(self, obj, visitor): + maingo_obj = visitor.dfs_postorder_stack(obj.expr) + if obj.sense == maximize: + maingo_obj *= -1 + return maingo_obj + + def build_maingo_constraints(self, cons, visitor): + eqs = [] + ineqs = [] + for con in cons: + if con.equality: + eqs += [visitor.dfs_postorder_stack(con.body - con.lower)] + elif con.has_ub() and con.has_lb(): + ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] + ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] + elif con.has_ub(): + ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] + elif con.has_ub(): + ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] + else: + raise ValueError( + "Constraint does not have a lower " + "or an upper bound: {0} \n".format(con) + ) + return eqs, ineqs + + def get_variables(self): + return [ + maingopy.OptimizationVariable( + maingopy.Bounds(var.lb, var.ub), var.type, var.name + ) + for var in self._var_list + ] + + def get_initial_point(self): + return [ + var.init if not var.init is None else (var.lb + var.ub) / 2.0 + for var in self._var_list + ] + + def evaluate(self, maingo_vars): + visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) + result = maingopy.EvaluationContainer() + result.objective = self.build_maingo_objective(self._objective, visitor) + eqs, ineqs = self.build_maingo_constraints(self._con_list, visitor) + result.eq = eqs + result.ineq = ineqs + return result From 56b513dcf2ec852ce993400d3245b58cb46c5fc4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 15:38:29 -0600 Subject: [PATCH 0978/3044] add tests for mixed standard form --- pyomo/repn/tests/test_standard_form.py | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pyomo/repn/tests/test_standard_form.py b/pyomo/repn/tests/test_standard_form.py index c8b914deca5..9dee2b1d25d 100644 --- a/pyomo/repn/tests/test_standard_form.py +++ b/pyomo/repn/tests/test_standard_form.py @@ -42,6 +42,8 @@ def test_linear_model(self): self.assertTrue(np.all(repn.c == np.array([0, 0, 0]))) self.assertTrue(np.all(repn.A == np.array([[-1, -2, 0], [0, 1, 4]]))) self.assertTrue(np.all(repn.rhs == np.array([-3, 5]))) + self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1)]) + self.assertEqual(repn.columns, [m.x, m.y[1], m.y[3]]) def test_almost_dense_linear_model(self): m = pyo.ConcreteModel() @@ -55,6 +57,8 @@ def test_almost_dense_linear_model(self): self.assertTrue(np.all(repn.c == np.array([0, 0, 0]))) self.assertTrue(np.all(repn.A == np.array([[-1, -2, -4], [5, 6, 8]]))) self.assertTrue(np.all(repn.rhs == np.array([-10, 20]))) + self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1)]) + self.assertEqual(repn.columns, [m.x, m.y[1], m.y[3]]) def test_linear_model_row_col_order(self): m = pyo.ConcreteModel() @@ -70,6 +74,8 @@ def test_linear_model_row_col_order(self): self.assertTrue(np.all(repn.c == np.array([0, 0, 0]))) self.assertTrue(np.all(repn.A == np.array([[4, 0, 1], [0, -1, -2]]))) self.assertTrue(np.all(repn.rhs == np.array([5, -3]))) + self.assertEqual(repn.rows, [(m.d, 1), (m.c, -1)]) + self.assertEqual(repn.columns, [m.y[3], m.x, m.y[1]]) def test_suffix_warning(self): m = pyo.ConcreteModel() @@ -235,6 +241,40 @@ def test_alternative_forms(self): ) self._verify_solution(soln, repn, True) + repn = LinearStandardFormCompiler().write( + m, mixed_form=True, column_order=col_order + ) + + self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1), (m.e, 1), (m.e, -1), (m.f, 0)]) + self.assertEqual( + list(map(str, repn.x)), + ['x', 'y[0]', 'y[1]', 'y[3]'], + ) + self.assertEqual( + list(v.bounds for v in repn.x), + [(None, None), (0, 10), (-5, 10), (-5, -2)], + ) + ref = np.array( + [ + [1, 0, 2, 0], + [0, 0, 1, 4], + [0, 1, 6, 0], + [0, 1, 6, 0], + [1, 1, 0, 0], + ] + ) + self.assertTrue(np.all(repn.A == ref)) + print(repn) + print(repn.b) + self.assertTrue(np.all(repn.b == np.array([3, 5, 6, -3, 8]))) + self.assertTrue( + np.all( + repn.c == np.array([[-1, 0, -5, 0], [1, 0, 0, 15]]) + ) + ) + # Note that the solution is a mix of inequality and equality constraints + # self._verify_solution(soln, repn, False) + repn = LinearStandardFormCompiler().write( m, slack_form=True, nonnegative_vars=True, column_order=col_order ) From a0b9a927e4997b767b267bba19d3598d561c2b8b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:20:15 -0600 Subject: [PATCH 0979/3044] Renamed _ArcData -> ArcData --- pyomo/core/base/component.py | 2 +- pyomo/network/arc.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 22c2bc4b804..c91167379fd 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -806,7 +806,7 @@ class ComponentData(_ComponentBase): # _GeneralExpressionData, _LogicalConstraintData, # _GeneralLogicalConstraintData, _GeneralObjectiveData, # _ParamData,_GeneralVarData, _GeneralBooleanVarData, _DisjunctionData, - # _ArcData, _PortData, _LinearConstraintData, and + # ArcData, _PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! def __init__(self, component): diff --git a/pyomo/network/arc.py b/pyomo/network/arc.py index 42b7c6ea075..5e68f181a38 100644 --- a/pyomo/network/arc.py +++ b/pyomo/network/arc.py @@ -52,7 +52,7 @@ def _iterable_to_dict(vals, directed, name): return vals -class _ArcData(ActiveComponentData): +class ArcData(ActiveComponentData): """ This class defines the data for a single Arc @@ -246,6 +246,11 @@ def _validate_ports(self, source, destination, ports): ) +class _ArcData(metaclass=RenamedClass): + __renamed__new_class__ = ArcData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("Component used for connecting two Ports.") class Arc(ActiveIndexedComponent): """ @@ -267,7 +272,7 @@ class Arc(ActiveIndexedComponent): or a two-member iterable of ports """ - _ComponentDataClass = _ArcData + _ComponentDataClass = ArcData def __new__(cls, *args, **kwds): if cls != Arc: @@ -373,9 +378,9 @@ def _pprint(self): ) -class ScalarArc(_ArcData, Arc): +class ScalarArc(ArcData, Arc): def __init__(self, *args, **kwds): - _ArcData.__init__(self, self) + ArcData.__init__(self, self) Arc.__init__(self, *args, **kwds) self.index = UnindexedComponent_index From 2f3e94039bfffac5da785978b0c7bf8c6aa20c9e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:31:31 -0600 Subject: [PATCH 0980/3044] Renamed _BlockData -> BlockData --- pyomo/contrib/appsi/base.py | 12 ++-- pyomo/contrib/appsi/fbbt.py | 6 +- pyomo/contrib/appsi/solvers/cbc.py | 6 +- pyomo/contrib/appsi/solvers/cplex.py | 6 +- pyomo/contrib/appsi/solvers/ipopt.py | 6 +- pyomo/contrib/appsi/solvers/wntr.py | 4 +- pyomo/contrib/appsi/writers/lp_writer.py | 4 +- pyomo/contrib/appsi/writers/nl_writer.py | 4 +- pyomo/contrib/benders/benders_cuts.py | 6 +- pyomo/contrib/cp/interval_var.py | 6 +- .../logical_to_disjunctive_program.py | 4 +- pyomo/contrib/incidence_analysis/interface.py | 6 +- .../contrib/incidence_analysis/scc_solver.py | 4 +- .../tests/test_interface.py | 2 +- pyomo/contrib/latex_printer/latex_printer.py | 6 +- .../piecewise/piecewise_linear_function.py | 6 +- .../piecewise_to_gdp_transformation.py | 4 +- .../pynumero/interfaces/external_grey_box.py | 6 +- pyomo/contrib/solver/base.py | 14 ++-- pyomo/contrib/viewer/report.py | 2 +- pyomo/core/base/block.py | 65 ++++++++++--------- pyomo/core/base/piecewise.py | 6 +- .../plugins/transform/logical_to_linear.py | 4 +- pyomo/core/tests/unit/test_block.py | 14 ++-- pyomo/core/tests/unit/test_component.py | 8 +-- pyomo/core/tests/unit/test_indexed_slice.py | 4 +- pyomo/core/tests/unit/test_suffix.py | 4 +- pyomo/dae/flatten.py | 8 +-- pyomo/gdp/disjunct.py | 8 +-- pyomo/gdp/plugins/gdp_var_mover.py | 2 +- pyomo/gdp/tests/common_tests.py | 10 +-- pyomo/gdp/transformed_disjunct.py | 6 +- pyomo/gdp/util.py | 8 +-- pyomo/mpec/complementarity.py | 4 +- pyomo/opt/base/solvers.py | 8 +-- pyomo/repn/util.py | 4 +- .../solvers/direct_or_persistent_solver.py | 4 +- .../solvers/plugins/solvers/direct_solver.py | 8 +-- pyomo/solvers/plugins/solvers/mosek_direct.py | 2 +- .../plugins/solvers/persistent_solver.py | 8 +-- pyomo/util/report_scaling.py | 8 +-- pyomo/util/slices.py | 2 +- 42 files changed, 156 insertions(+), 153 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 201e5975ac9..b4ade16a597 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -25,7 +25,7 @@ from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint from pyomo.core.base.var import _GeneralVarData, Var from pyomo.core.base.param import _ParamData, Param -from pyomo.core.base.block import _BlockData, Block +from pyomo.core.base.block import BlockData, Block from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.collections import ComponentMap from .utils.get_objective import get_objective @@ -621,13 +621,13 @@ def __str__(self): return self.name @abc.abstractmethod - def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> Results: + def solve(self, model: BlockData, timer: HierarchicalTimer = None) -> Results: """ Solve a Pyomo model. Parameters ---------- - model: _BlockData + model: BlockData The Pyomo model to be solved timer: HierarchicalTimer An option timer for reporting timing @@ -811,7 +811,7 @@ def add_constraints(self, cons: List[_GeneralConstraintData]): pass @abc.abstractmethod - def add_block(self, block: _BlockData): + def add_block(self, block: BlockData): pass @abc.abstractmethod @@ -827,7 +827,7 @@ def remove_constraints(self, cons: List[_GeneralConstraintData]): pass @abc.abstractmethod - def remove_block(self, block: _BlockData): + def remove_block(self, block: BlockData): pass @abc.abstractmethod @@ -1529,7 +1529,7 @@ def update(self, timer: HierarchicalTimer = None): class LegacySolverInterface(object): def solve( self, - model: _BlockData, + model: BlockData, tee: bool = False, load_solutions: bool = True, logfile: Optional[str] = None, diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 8b6cc52d2aa..c6bbdb5bf3b 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -23,7 +23,7 @@ from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.objective import _GeneralObjectiveData, minimize, maximize -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base import SymbolMap, TextLabeler from pyomo.common.errors import InfeasibleConstraintException @@ -275,7 +275,7 @@ def _deactivate_satisfied_cons(self): c.deactivate() def perform_fbbt( - self, model: _BlockData, symbolic_solver_labels: Optional[bool] = None + self, model: BlockData, symbolic_solver_labels: Optional[bool] = None ): if model is not self._model: self.set_instance(model, symbolic_solver_labels=symbolic_solver_labels) @@ -304,7 +304,7 @@ def perform_fbbt( self._deactivate_satisfied_cons() return n_iter - def perform_fbbt_with_seed(self, model: _BlockData, seed_var: _GeneralVarData): + def perform_fbbt_with_seed(self, model: BlockData, seed_var: _GeneralVarData): if model is not self._model: self.set_instance(model) else: diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 7f04ffbfce7..57bbf1b4c21 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -28,7 +28,7 @@ from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer @@ -173,7 +173,7 @@ def add_params(self, params: List[_ParamData]): def add_constraints(self, cons: List[_GeneralConstraintData]): self._writer.add_constraints(cons) - def add_block(self, block: _BlockData): + def add_block(self, block: BlockData): self._writer.add_block(block) def remove_variables(self, variables: List[_GeneralVarData]): @@ -185,7 +185,7 @@ def remove_params(self, params: List[_ParamData]): def remove_constraints(self, cons: List[_GeneralConstraintData]): self._writer.remove_constraints(cons) - def remove_block(self, block: _BlockData): + def remove_block(self, block: BlockData): self._writer.remove_block(block) def set_objective(self, obj: _GeneralObjectiveData): diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 1b7ab5000d2..2e04a979fda 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -24,7 +24,7 @@ from typing import Optional, Sequence, NoReturn, List, Mapping, Dict from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer @@ -188,7 +188,7 @@ def add_params(self, params: List[_ParamData]): def add_constraints(self, cons: List[_GeneralConstraintData]): self._writer.add_constraints(cons) - def add_block(self, block: _BlockData): + def add_block(self, block: BlockData): self._writer.add_block(block) def remove_variables(self, variables: List[_GeneralVarData]): @@ -200,7 +200,7 @@ def remove_params(self, params: List[_ParamData]): def remove_constraints(self, cons: List[_GeneralConstraintData]): self._writer.remove_constraints(cons) - def remove_block(self, block: _BlockData): + def remove_block(self, block: BlockData): self._writer.remove_block(block) def set_objective(self, obj: _GeneralObjectiveData): diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 54e21d333e5..19ec5f8031c 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -30,7 +30,7 @@ from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer @@ -237,7 +237,7 @@ def add_params(self, params: List[_ParamData]): def add_constraints(self, cons: List[_GeneralConstraintData]): self._writer.add_constraints(cons) - def add_block(self, block: _BlockData): + def add_block(self, block: BlockData): self._writer.add_block(block) def remove_variables(self, variables: List[_GeneralVarData]): @@ -249,7 +249,7 @@ def remove_params(self, params: List[_ParamData]): def remove_constraints(self, cons: List[_GeneralConstraintData]): self._writer.remove_constraints(cons) - def remove_block(self, block: _BlockData): + def remove_block(self, block: BlockData): self._writer.remove_block(block) def set_objective(self, obj: _GeneralObjectiveData): diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 00c0598c687..928eda2b514 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -39,7 +39,7 @@ from pyomo.common.collections import ComponentMap from pyomo.core.expr.numvalue import native_numeric_types from typing import Dict, Optional, List -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.constraint import _GeneralConstraintData @@ -178,7 +178,7 @@ def _solve(self, timer: HierarchicalTimer): ) return results - def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> Results: + def solve(self, model: BlockData, timer: HierarchicalTimer = None) -> Results: StaleFlagManager.mark_all_as_stale() if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 9984cb7465d..518be5fac99 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -15,7 +15,7 @@ from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn from pyomo.core.expr.numvalue import value from pyomo.contrib.appsi.base import PersistentBase @@ -167,7 +167,7 @@ def _set_objective(self, obj: _GeneralObjectiveData): cobj.name = cname self._writer.objective = cobj - def write(self, model: _BlockData, filename: str, timer: HierarchicalTimer = None): + def write(self, model: BlockData, filename: str, timer: HierarchicalTimer = None): if timer is None: timer = HierarchicalTimer() if model is not self._model: diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index bd24a86216a..75b026ab521 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -15,7 +15,7 @@ from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn from pyomo.core.expr.numvalue import value from pyomo.contrib.appsi.base import PersistentBase @@ -232,7 +232,7 @@ def _set_objective(self, obj: _GeneralObjectiveData): cobj.sense = sense self._writer.objective = cobj - def write(self, model: _BlockData, filename: str, timer: HierarchicalTimer = None): + def write(self, model: BlockData, filename: str, timer: HierarchicalTimer = None): if timer is None: timer = HierarchicalTimer() if model is not self._model: diff --git a/pyomo/contrib/benders/benders_cuts.py b/pyomo/contrib/benders/benders_cuts.py index cf96ba26164..0653be55986 100644 --- a/pyomo/contrib/benders/benders_cuts.py +++ b/pyomo/contrib/benders/benders_cuts.py @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.core.base.block import _BlockData, declare_custom_block +from pyomo.core.base.block import BlockData, declare_custom_block import pyomo.environ as pyo from pyomo.solvers.plugins.solvers.persistent_solver import PersistentSolver from pyomo.core.expr.visitor import identify_variables @@ -166,13 +166,13 @@ def _setup_subproblem(b, root_vars, relax_subproblem_cons): @declare_custom_block(name='BendersCutGenerator') -class BendersCutGeneratorData(_BlockData): +class BendersCutGeneratorData(BlockData): def __init__(self, component): if not mpi4py_available: raise ImportError('BendersCutGenerator requires mpi4py.') if not numpy_available: raise ImportError('BendersCutGenerator requires numpy.') - _BlockData.__init__(self, component) + BlockData.__init__(self, component) self.num_subproblems_by_rank = 0 # np.zeros(self.comm.Get_size()) self.subproblems = list() diff --git a/pyomo/contrib/cp/interval_var.py b/pyomo/contrib/cp/interval_var.py index 953b859ea20..ff11d6e3a9f 100644 --- a/pyomo/contrib/cp/interval_var.py +++ b/pyomo/contrib/cp/interval_var.py @@ -18,7 +18,7 @@ from pyomo.core import Integers, value from pyomo.core.base import Any, ScalarVar, ScalarBooleanVar -from pyomo.core.base.block import _BlockData, Block +from pyomo.core.base.block import BlockData, Block from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.indexed_component import IndexedComponent, UnindexedComponent_set @@ -87,14 +87,14 @@ def get_associated_interval_var(self): return self.parent_block() -class IntervalVarData(_BlockData): +class IntervalVarData(BlockData): """This class defines the abstract interface for a single interval variable.""" # We will put our four variables on this, and everything else is off limits. _Block_reserved_words = Any def __init__(self, component=None): - _BlockData.__init__(self, component) + BlockData.__init__(self, component) with self._declare_reserved_components(): self.is_present = IntervalVarPresence() diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py index e318e621e88..c29bf3f2675 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py @@ -26,7 +26,7 @@ Transformation, NonNegativeIntegers, ) -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base import SortComponents from pyomo.core.util import target_list from pyomo.gdp import Disjunct, Disjunction @@ -73,7 +73,7 @@ def _apply_to(self, model, **kwds): transBlocks = {} visitor = LogicalToDisjunctiveVisitor() for t in targets: - if t.ctype is Block or isinstance(t, _BlockData): + if t.ctype is Block or isinstance(t, BlockData): self._transform_block(t, model, visitor, transBlocks) elif t.ctype is LogicalConstraint: if t.is_indexed(): diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 50cb84daaf5..b798dafced7 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -15,7 +15,7 @@ import enum import textwrap -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.var import Var from pyomo.core.base.constraint import Constraint from pyomo.core.base.objective import Objective @@ -279,7 +279,7 @@ def __init__(self, model=None, active=True, include_inequality=True, **kwds): self._incidence_graph = None self._variables = None self._constraints = None - elif isinstance(model, _BlockData): + elif isinstance(model, BlockData): self._constraints = [ con for con in model.component_data_objects(Constraint, active=active) @@ -348,7 +348,7 @@ def __init__(self, model=None, active=True, include_inequality=True, **kwds): else: raise TypeError( "Unsupported type for incidence graph. Expected PyomoNLP" - " or _BlockData but got %s." % type(model) + " or BlockData but got %s." % type(model) ) @property diff --git a/pyomo/contrib/incidence_analysis/scc_solver.py b/pyomo/contrib/incidence_analysis/scc_solver.py index 0c59fe8703e..378647c190c 100644 --- a/pyomo/contrib/incidence_analysis/scc_solver.py +++ b/pyomo/contrib/incidence_analysis/scc_solver.py @@ -27,7 +27,7 @@ def generate_strongly_connected_components( constraints, variables=None, include_fixed=False, igraph=None ): - """Yield in order ``_BlockData`` that each contain the variables and + """Yield in order ``BlockData`` that each contain the variables and constraints of a single diagonal block in a block lower triangularization of the incidence matrix of constraints and variables @@ -51,7 +51,7 @@ def generate_strongly_connected_components( Yields ------ - Tuple of ``_BlockData``, list-of-variables + Tuple of ``BlockData``, list-of-variables Blocks containing the variables and constraints of every strongly connected component, in a topological order. The variables are the "input variables" for that block. diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 4b77d60d8ba..117e2e53b6d 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1888,7 +1888,7 @@ def test_block_data_obj(self): self.assertEqual(len(var_dmp.unmatched), 1) self.assertEqual(len(con_dmp.unmatched), 1) - msg = "Unsupported type.*_BlockData" + msg = "Unsupported type.*BlockData" with self.assertRaisesRegex(TypeError, msg): igraph = IncidenceGraphInterface(m.block) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 0a595dd8e1b..42fc9083953 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -64,7 +64,7 @@ from pyomo.core.base.external import _PythonCallbackFunctionID from pyomo.core.base.enums import SortComponents -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.repn.util import ExprType @@ -587,7 +587,7 @@ def latex_printer( Parameters ---------- - pyomo_component: _BlockData or Model or Objective or Constraint or Expression + pyomo_component: BlockData or Model or Objective or Constraint or Expression The Pyomo component to be printed latex_component_map: pyomo.common.collections.component_map.ComponentMap @@ -674,7 +674,7 @@ def latex_printer( use_equation_environment = True isSingle = True - elif isinstance(pyomo_component, _BlockData): + elif isinstance(pyomo_component, BlockData): objectives = [ obj for obj in pyomo_component.component_data_objects( diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 66ca02ad125..e92edacc756 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -20,7 +20,7 @@ PiecewiseLinearExpression, ) from pyomo.core import Any, NonNegativeIntegers, value, Var -from pyomo.core.base.block import _BlockData, Block +from pyomo.core.base.block import BlockData, Block from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.expression import Expression from pyomo.core.base.global_set import UnindexedComponent_index @@ -36,11 +36,11 @@ logger = logging.getLogger(__name__) -class PiecewiseLinearFunctionData(_BlockData): +class PiecewiseLinearFunctionData(BlockData): _Block_reserved_words = Any def __init__(self, component=None): - _BlockData.__init__(self, component) + BlockData.__init__(self, component) with self._declare_reserved_components(): self._expressions = Expression(NonNegativeIntegers) diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py index 2e056c47a15..779bb601c71 100644 --- a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py +++ b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py @@ -33,7 +33,7 @@ Any, ) from pyomo.core.base import Transformation -from pyomo.core.base.block import _BlockData, Block +from pyomo.core.base.block import BlockData, Block from pyomo.core.util import target_list from pyomo.gdp import Disjunct, Disjunction from pyomo.gdp.util import is_child_of @@ -147,7 +147,7 @@ def _apply_to_impl(self, instance, **kwds): self._transform_piecewise_linear_function( t, config.descend_into_expressions ) - elif t.ctype is Block or isinstance(t, _BlockData): + elif t.ctype is Block or isinstance(t, BlockData): self._transform_block(t, config.descend_into_expressions) elif t.ctype is Constraint: if not config.descend_into_expressions: diff --git a/pyomo/contrib/pynumero/interfaces/external_grey_box.py b/pyomo/contrib/pynumero/interfaces/external_grey_box.py index 7e42f161bee..68e652575cc 100644 --- a/pyomo/contrib/pynumero/interfaces/external_grey_box.py +++ b/pyomo/contrib/pynumero/interfaces/external_grey_box.py @@ -18,7 +18,7 @@ from pyomo.common.log import is_debug_set from pyomo.common.timing import ConstructionTimer from pyomo.core.base import Var, Set, Constraint, value -from pyomo.core.base.block import _BlockData, Block, declare_custom_block +from pyomo.core.base.block import BlockData, Block, declare_custom_block from pyomo.core.base.global_set import UnindexedComponent_index from pyomo.core.base.initializer import Initializer from pyomo.core.base.set import UnindexedComponent_set @@ -316,7 +316,7 @@ def evaluate_jacobian_outputs(self): # -class ExternalGreyBoxBlockData(_BlockData): +class ExternalGreyBoxBlockData(BlockData): def set_external_model(self, external_grey_box_model, inputs=None, outputs=None): """ Parameters @@ -424,7 +424,7 @@ class ScalarExternalGreyBoxBlock(ExternalGreyBoxBlockData, ExternalGreyBoxBlock) def __init__(self, *args, **kwds): ExternalGreyBoxBlockData.__init__(self, component=self) ExternalGreyBoxBlock.__init__(self, *args, **kwds) - # The above inherit from Block and _BlockData, so it's not until here + # The above inherit from Block and BlockData, so it's not until here # that we know it's scalar. So we set the index accordingly. self._index = UnindexedComponent_index diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 8840265763e..4b7da383a57 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -17,7 +17,7 @@ from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.common.config import document_kwargs_from_configdict, ConfigValue from pyomo.common.errors import ApplicationError @@ -108,13 +108,13 @@ def __str__(self): @document_kwargs_from_configdict(CONFIG) @abc.abstractmethod - def solve(self, model: _BlockData, **kwargs) -> Results: + def solve(self, model: BlockData, **kwargs) -> Results: """ Solve a Pyomo model. Parameters ---------- - model: _BlockData + model: BlockData The Pyomo model to be solved **kwargs Additional keyword arguments (including solver_options - passthrough @@ -182,7 +182,7 @@ class PersistentSolverBase(SolverBase): @document_kwargs_from_configdict(PersistentSolverConfig()) @abc.abstractmethod - def solve(self, model: _BlockData, **kwargs) -> Results: + def solve(self, model: BlockData, **kwargs) -> Results: super().solve(model, kwargs) def is_persistent(self): @@ -300,7 +300,7 @@ def add_constraints(self, cons: List[_GeneralConstraintData]): """ @abc.abstractmethod - def add_block(self, block: _BlockData): + def add_block(self, block: BlockData): """ Add a block to the model """ @@ -324,7 +324,7 @@ def remove_constraints(self, cons: List[_GeneralConstraintData]): """ @abc.abstractmethod - def remove_block(self, block: _BlockData): + def remove_block(self, block: BlockData): """ Remove a block from the model """ @@ -496,7 +496,7 @@ def _solution_handler( def solve( self, - model: _BlockData, + model: BlockData, tee: bool = False, load_solutions: bool = True, logfile: Optional[str] = None, diff --git a/pyomo/contrib/viewer/report.py b/pyomo/contrib/viewer/report.py index f83a53c608d..a1f893bba31 100644 --- a/pyomo/contrib/viewer/report.py +++ b/pyomo/contrib/viewer/report.py @@ -149,7 +149,7 @@ def degrees_of_freedom(blk): Return the degrees of freedom. Args: - blk (Block or _BlockData): Block to count degrees of freedom in + blk (Block or BlockData): Block to count degrees of freedom in Returns: (int): Number of degrees of freedom """ diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 2918ef78b00..8f4e86fe697 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -254,7 +254,7 @@ class _BlockConstruction(object): class PseudoMap(AutoSlots.Mixin): """ This class presents a "mock" dict interface to the internal - _BlockData data structures. We return this object to the + BlockData data structures. We return this object to the user to preserve the historical "{ctype : {name : obj}}" interface without actually regenerating that dict-of-dicts data structure. @@ -487,7 +487,7 @@ def iteritems(self): return self.items() -class _BlockData(ActiveComponentData): +class BlockData(ActiveComponentData): """ This class holds the fundamental block data. """ @@ -537,9 +537,9 @@ def __init__(self, component): # _ctypes: { ctype -> [1st idx, last idx, count] } # _decl: { name -> idx } # _decl_order: list( tuples( obj, next_type_idx ) ) - super(_BlockData, self).__setattr__('_ctypes', {}) - super(_BlockData, self).__setattr__('_decl', {}) - super(_BlockData, self).__setattr__('_decl_order', []) + super(BlockData, self).__setattr__('_ctypes', {}) + super(BlockData, self).__setattr__('_decl', {}) + super(BlockData, self).__setattr__('_decl_order', []) self._private_data = None def __getattr__(self, val) -> Union[Component, IndexedComponent, Any]: @@ -574,7 +574,7 @@ def __setattr__(self, name: str, val: Union[Component, IndexedComponent, Any]): # Other Python objects are added with the standard __setattr__ # method. # - super(_BlockData, self).__setattr__(name, val) + super(BlockData, self).__setattr__(name, val) # # Case 2. The attribute exists and it is a component in the # list of declarations in this block. We will use the @@ -628,11 +628,11 @@ def __setattr__(self, name: str, val: Union[Component, IndexedComponent, Any]): # else: # - # NB: This is important: the _BlockData is either a scalar + # NB: This is important: the BlockData is either a scalar # Block (where _parent and _component are defined) or a # single block within an Indexed Block (where only # _component is defined). Regardless, the - # _BlockData.__init__() method declares these methods and + # BlockData.__init__() method declares these methods and # sets them either to None or a weakref. Thus, we will # never have a problem converting these objects from # weakrefs into Blocks and back (when pickling); the @@ -647,23 +647,23 @@ def __setattr__(self, name: str, val: Union[Component, IndexedComponent, Any]): # return True, this shouldn't be too inefficient. # if name == '_parent': - if val is not None and not isinstance(val(), _BlockData): + if val is not None and not isinstance(val(), BlockData): raise ValueError( "Cannot set the '_parent' attribute of Block '%s' " "to a non-Block object (with type=%s); Did you " "try to create a model component named '_parent'?" % (self.name, type(val)) ) - super(_BlockData, self).__setattr__(name, val) + super(BlockData, self).__setattr__(name, val) elif name == '_component': - if val is not None and not isinstance(val(), _BlockData): + if val is not None and not isinstance(val(), BlockData): raise ValueError( "Cannot set the '_component' attribute of Block '%s' " "to a non-Block object (with type=%s); Did you " "try to create a model component named '_component'?" % (self.name, type(val)) ) - super(_BlockData, self).__setattr__(name, val) + super(BlockData, self).__setattr__(name, val) # # At this point, we should only be seeing non-component data # the user is hanging on the blocks (uncommon) or the @@ -680,7 +680,7 @@ def __setattr__(self, name: str, val: Union[Component, IndexedComponent, Any]): delattr(self, name) self.add_component(name, val) else: - super(_BlockData, self).__setattr__(name, val) + super(BlockData, self).__setattr__(name, val) def __delattr__(self, name): """ @@ -703,7 +703,7 @@ def __delattr__(self, name): # Other Python objects are removed with the standard __detattr__ # method. # - super(_BlockData, self).__delattr__(name) + super(BlockData, self).__delattr__(name) def _compact_decl_storage(self): idxMap = {} @@ -775,11 +775,11 @@ def transfer_attributes_from(self, src): Parameters ---------- - src: _BlockData or dict + src: BlockData or dict The Block or mapping that contains the new attributes to assign to this block. """ - if isinstance(src, _BlockData): + if isinstance(src, BlockData): # There is a special case where assigning a parent block to # this block creates a circular hierarchy if src is self: @@ -788,7 +788,7 @@ def transfer_attributes_from(self, src): while p_block is not None: if p_block is src: raise ValueError( - "_BlockData.transfer_attributes_from(): Cannot set a " + "BlockData.transfer_attributes_from(): Cannot set a " "sub-block (%s) to a parent block (%s): creates a " "circular hierarchy" % (self, src) ) @@ -804,7 +804,7 @@ def transfer_attributes_from(self, src): del_src_comp = lambda x: None else: raise ValueError( - "_BlockData.transfer_attributes_from(): expected a " + "BlockData.transfer_attributes_from(): expected a " "Block or dict; received %s" % (type(src).__name__,) ) @@ -878,7 +878,7 @@ def collect_ctypes(self, active=None, descend_into=True): def model(self): # - # Special case: the "Model" is always the top-level _BlockData, + # Special case: the "Model" is always the top-level BlockData, # so if this is the top-level block, it must be the model # # Also note the interesting and intentional characteristic for @@ -1035,7 +1035,7 @@ def add_component(self, name, val): # is inappropriate here. The correct way to add the attribute # is to delegate the work to the next class up the MRO. # - super(_BlockData, self).__setattr__(name, val) + super(BlockData, self).__setattr__(name, val) # # Update the ctype linked lists # @@ -1106,7 +1106,7 @@ def add_component(self, name, val): # This is tricky: If we are in the middle of # constructing an indexed block, the block component # already has _constructed=True. Now, if the - # _BlockData.__init__() defines any local variables + # BlockData.__init__() defines any local variables # (like pyomo.gdp.Disjunct's indicator_var), name(True) # will fail: this block data exists and has a parent(), # but it has not yet been added to the parent's _data @@ -1194,7 +1194,7 @@ def del_component(self, name_or_object): # Note: 'del self.__dict__[name]' is inappropriate here. The # correct way to add the attribute is to delegate the work to # the next class up the MRO. - super(_BlockData, self).__delattr__(name) + super(BlockData, self).__delattr__(name) def reclassify_component_type( self, name_or_object, new_ctype, preserve_declaration_order=True @@ -1994,6 +1994,11 @@ def private_data(self, scope=None): return self._private_data[scope] +class _BlockData(metaclass=RenamedClass): + __renamed__new_class__ = BlockData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register( "A component that contains one or more model components." ) @@ -2007,7 +2012,7 @@ class Block(ActiveIndexedComponent): is deferred. """ - _ComponentDataClass = _BlockData + _ComponentDataClass = BlockData _private_data_initializers = defaultdict(lambda: dict) @overload @@ -2100,7 +2105,7 @@ def _getitem_when_not_present(self, idx): # components declared by the rule have the opportunity # to be initialized with data from # _BlockConstruction.data as they are transferred over. - if obj is not _block and isinstance(obj, _BlockData): + if obj is not _block and isinstance(obj, BlockData): _block.transfer_attributes_from(obj) finally: if data is not None and _block is not self: @@ -2221,7 +2226,7 @@ def display(self, filename=None, ostream=None, prefix=""): ostream = sys.stdout for key in sorted(self): - _BlockData.display(self[key], filename, ostream, prefix) + BlockData.display(self[key], filename, ostream, prefix) @staticmethod def register_private_data_initializer(initializer, scope=None): @@ -2241,9 +2246,9 @@ def register_private_data_initializer(initializer, scope=None): Block._private_data_initializers[scope] = initializer -class ScalarBlock(_BlockData, Block): +class ScalarBlock(BlockData, Block): def __init__(self, *args, **kwds): - _BlockData.__init__(self, component=self) + BlockData.__init__(self, component=self) Block.__init__(self, *args, **kwds) # Initialize the data dict so that (abstract) attribute # assignment will work. Note that we do not trigger @@ -2266,7 +2271,7 @@ def __init__(self, *args, **kwds): Block.__init__(self, *args, **kwds) @overload - def __getitem__(self, index) -> _BlockData: ... + def __getitem__(self, index) -> BlockData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore @@ -2325,7 +2330,7 @@ def components_data(block, ctype, sort=None, sort_by_keys=False, sort_by_names=F # Create a Block and record all the default attributes, methods, etc. # These will be assumed to be the set of illegal component names. # -_BlockData._Block_reserved_words = set(dir(Block())) +BlockData._Block_reserved_words = set(dir(Block())) class _IndexedCustomBlockMeta(type): @@ -2376,7 +2381,7 @@ def declare_custom_block(name, new_ctype=None): """Decorator to declare components for a custom block data class >>> @declare_custom_block(name=FooBlock) - ... class FooBlockData(_BlockData): + ... class FooBlockData(BlockData): ... # custom block data class ... pass """ diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index 7817a61b2f2..b15def13ccb 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -43,7 +43,7 @@ from pyomo.common.deprecation import deprecation_warning from pyomo.common.numeric_types import value from pyomo.common.timing import ConstructionTimer -from pyomo.core.base.block import Block, _BlockData +from pyomo.core.base.block import Block, BlockData from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.constraint import Constraint, ConstraintList from pyomo.core.base.sos import SOSConstraint @@ -214,14 +214,14 @@ def _characterize_function(name, tol, f_rule, model, points, *index): return 0, values, False -class _PiecewiseData(_BlockData): +class _PiecewiseData(BlockData): """ This class defines the base class for all linearization and piecewise constraint generators.. """ def __init__(self, parent): - _BlockData.__init__(self, parent) + BlockData.__init__(self, parent) self._constructed = True self._bound_type = None self._domain_pts = None diff --git a/pyomo/core/plugins/transform/logical_to_linear.py b/pyomo/core/plugins/transform/logical_to_linear.py index 7aa541a5fdd..69328032004 100644 --- a/pyomo/core/plugins/transform/logical_to_linear.py +++ b/pyomo/core/plugins/transform/logical_to_linear.py @@ -29,7 +29,7 @@ BooleanVarList, SortComponents, ) -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.boolean_var import _DeprecatedImplicitAssociatedBinaryVariable from pyomo.core.expr.cnf_walker import to_cnf from pyomo.core.expr import ( @@ -100,7 +100,7 @@ def _apply_to(self, model, **kwds): # the GDP will be solved, and it would be wrong to assume that a GDP # will *necessarily* be solved as an algebraic model. The star # example of not doing so being GDPopt.) - if t.ctype is Block or isinstance(t, _BlockData): + if t.ctype is Block or isinstance(t, BlockData): self._transform_block(t, model, new_var_lists, transBlocks) elif t.ctype is LogicalConstraint: if t.is_indexed(): diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index 71e80d90a73..660f65f1944 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -54,7 +54,7 @@ from pyomo.core.base.block import ( ScalarBlock, SubclassOf, - _BlockData, + BlockData, declare_custom_block, ) import pyomo.core.expr as EXPR @@ -851,7 +851,7 @@ class DerivedBlock(ScalarBlock): _Block_reserved_words = None DerivedBlock._Block_reserved_words = ( - set(['a', 'b', 'c']) | _BlockData._Block_reserved_words + set(['a', 'b', 'c']) | BlockData._Block_reserved_words ) m = ConcreteModel() @@ -965,7 +965,7 @@ def __init__(self, *args, **kwds): b.c.d.e = Block() with self.assertRaisesRegex( ValueError, - r'_BlockData.transfer_attributes_from\(\): ' + r'BlockData.transfer_attributes_from\(\): ' r'Cannot set a sub-block \(c.d.e\) to a parent block \(c\):', ): b.c.d.e.transfer_attributes_from(b.c) @@ -974,7 +974,7 @@ def __init__(self, *args, **kwds): b = Block(concrete=True) with self.assertRaisesRegex( ValueError, - r'_BlockData.transfer_attributes_from\(\): expected a Block ' + r'BlockData.transfer_attributes_from\(\): expected a Block ' 'or dict; received str', ): b.transfer_attributes_from('foo') @@ -2977,7 +2977,7 @@ def test_write_exceptions(self): def test_override_pprint(self): @declare_custom_block('TempBlock') - class TempBlockData(_BlockData): + class TempBlockData(BlockData): def pprint(self, ostream=None, verbose=False, prefix=""): ostream.write('Testing pprint of a custom block.') @@ -3052,9 +3052,9 @@ def test_derived_block_construction(self): class ConcreteBlock(Block): pass - class ScalarConcreteBlock(_BlockData, ConcreteBlock): + class ScalarConcreteBlock(BlockData, ConcreteBlock): def __init__(self, *args, **kwds): - _BlockData.__init__(self, component=self) + BlockData.__init__(self, component=self) ConcreteBlock.__init__(self, *args, **kwds) _buf = [] diff --git a/pyomo/core/tests/unit/test_component.py b/pyomo/core/tests/unit/test_component.py index 175c4c47d46..b12db9af047 100644 --- a/pyomo/core/tests/unit/test_component.py +++ b/pyomo/core/tests/unit/test_component.py @@ -66,19 +66,17 @@ def test_getname(self): ) m.b[2]._component = None - self.assertEqual( - m.b[2].getname(fully_qualified=True), "[Unattached _BlockData]" - ) + self.assertEqual(m.b[2].getname(fully_qualified=True), "[Unattached BlockData]") # I think that getname() should do this: # self.assertEqual(m.b[2].c[2,4].getname(fully_qualified=True), - # "[Unattached _BlockData].c[2,4]") + # "[Unattached BlockData].c[2,4]") # but it doesn't match current behavior. I will file a PEP to # propose changing the behavior later and proceed to test # current behavior. self.assertEqual(m.b[2].c[2, 4].getname(fully_qualified=True), "c[2,4]") self.assertEqual( - m.b[2].getname(fully_qualified=False), "[Unattached _BlockData]" + m.b[2].getname(fully_qualified=False), "[Unattached BlockData]" ) self.assertEqual(m.b[2].c[2, 4].getname(fully_qualified=False), "c[2,4]") diff --git a/pyomo/core/tests/unit/test_indexed_slice.py b/pyomo/core/tests/unit/test_indexed_slice.py index babd3f3c46a..40aaad9fec9 100644 --- a/pyomo/core/tests/unit/test_indexed_slice.py +++ b/pyomo/core/tests/unit/test_indexed_slice.py @@ -17,7 +17,7 @@ import pyomo.common.unittest as unittest from pyomo.environ import Var, Block, ConcreteModel, RangeSet, Set, Any -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.indexed_component_slice import IndexedComponent_slice from pyomo.core.base.set import normalize_index @@ -64,7 +64,7 @@ def tearDown(self): self.m = None def test_simple_getitem(self): - self.assertIsInstance(self.m.b[1, 4], _BlockData) + self.assertIsInstance(self.m.b[1, 4], BlockData) def test_simple_getslice(self): _slicer = self.m.b[:, 4] diff --git a/pyomo/core/tests/unit/test_suffix.py b/pyomo/core/tests/unit/test_suffix.py index d2e861cceb5..9597bad7571 100644 --- a/pyomo/core/tests/unit/test_suffix.py +++ b/pyomo/core/tests/unit/test_suffix.py @@ -1603,7 +1603,7 @@ def test_clone_IndexedBlock(self): self.assertEqual(inst.junk.get(model.b[1]), None) self.assertEqual(inst.junk.get(inst.b[1]), 1.0) - def test_clone_BlockData(self): + def test_cloneBlockData(self): model = ConcreteModel() model.b = Block([1, 2, 3]) model.junk = Suffix() @@ -1761,7 +1761,7 @@ def test_pickle_IndexedBlock(self): self.assertEqual(inst.junk.get(model.b[1]), None) self.assertEqual(inst.junk.get(inst.b[1]), 1.0) - def test_pickle_BlockData(self): + def test_pickleBlockData(self): model = ConcreteModel() model.b = Block([1, 2, 3]) model.junk = Suffix() diff --git a/pyomo/dae/flatten.py b/pyomo/dae/flatten.py index febaf7c10c9..3d90cc443c1 100644 --- a/pyomo/dae/flatten.py +++ b/pyomo/dae/flatten.py @@ -259,7 +259,7 @@ def generate_sliced_components( Parameters ---------- - b: _BlockData + b: BlockData Block whose components will be sliced index_stack: list @@ -267,7 +267,7 @@ def generate_sliced_components( component, that have been sliced. This is necessary to return the sets that have been sliced. - slice_: IndexedComponent_slice or _BlockData + slice_: IndexedComponent_slice or BlockData Slice generated so far. This function will yield extensions to this slice at the current level of the block hierarchy. @@ -443,7 +443,7 @@ def flatten_components_along_sets(m, sets, ctype, indices=None, active=None): Parameters ---------- - m: _BlockData + m: BlockData Block whose components (and their sub-components) will be partitioned @@ -546,7 +546,7 @@ def flatten_dae_components(model, time, ctype, indices=None, active=None): Parameters ---------- - model: _BlockData + model: BlockData Block whose components are partitioned time: Set diff --git a/pyomo/gdp/disjunct.py b/pyomo/gdp/disjunct.py index d6e5fcfec57..e6d8d709425 100644 --- a/pyomo/gdp/disjunct.py +++ b/pyomo/gdp/disjunct.py @@ -41,7 +41,7 @@ ComponentData, ) from pyomo.core.base.global_set import UnindexedComponent_index -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.misc import apply_indexed_rule from pyomo.core.base.indexed_component import ActiveIndexedComponent from pyomo.core.expr.expr_common import ExpressionType @@ -412,7 +412,7 @@ def process(arg): return (_Initializer.deferred_value, arg) -class _DisjunctData(_BlockData): +class _DisjunctData(BlockData): __autoslot_mappers__ = {'_transformation_block': AutoSlots.weakref_mapper} _Block_reserved_words = set() @@ -424,7 +424,7 @@ def transformation_block(self): ) def __init__(self, component): - _BlockData.__init__(self, component) + BlockData.__init__(self, component) with self._declare_reserved_components(): self.indicator_var = AutoLinkedBooleanVar() self.binary_indicator_var = AutoLinkedBinaryVar(self.indicator_var) @@ -498,7 +498,7 @@ def _activate_without_unfixing_indicator(self): class ScalarDisjunct(_DisjunctData, Disjunct): def __init__(self, *args, **kwds): ## FIXME: This is a HACK to get around a chicken-and-egg issue - ## where _BlockData creates the indicator_var *before* + ## where BlockData creates the indicator_var *before* ## Block.__init__ declares the _defer_construction flag. self._defer_construction = True self._suppress_ctypes = set() diff --git a/pyomo/gdp/plugins/gdp_var_mover.py b/pyomo/gdp/plugins/gdp_var_mover.py index 5402b576368..7b1df0bb68f 100644 --- a/pyomo/gdp/plugins/gdp_var_mover.py +++ b/pyomo/gdp/plugins/gdp_var_mover.py @@ -115,7 +115,7 @@ def _apply_to(self, instance, **kwds): disjunct_component, Block ) # HACK: activate the block, but do not activate the - # _BlockData objects + # BlockData objects super(ActiveIndexedComponent, disjunct_component).activate() # Deactivate all constraints. Note that we only need to diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index 5d0d6f6c21b..e15a7c66d8a 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -30,7 +30,7 @@ from pyomo.gdp import Disjunct, Disjunction, GDP_Error from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.core.base import constraint, ComponentUID -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.repn import generate_standard_repn import pyomo.core.expr as EXPR import pyomo.gdp.tests.models as models @@ -1704,10 +1704,10 @@ def check_all_components_transformed(self, m): # makeNestedDisjunctions_NestedDisjuncts model. self.assertIsInstance(m.disj.algebraic_constraint, Constraint) self.assertIsInstance(m.d1.disj2.algebraic_constraint, Constraint) - self.assertIsInstance(m.d1.transformation_block, _BlockData) - self.assertIsInstance(m.d2.transformation_block, _BlockData) - self.assertIsInstance(m.d1.d3.transformation_block, _BlockData) - self.assertIsInstance(m.d1.d4.transformation_block, _BlockData) + self.assertIsInstance(m.d1.transformation_block, BlockData) + self.assertIsInstance(m.d2.transformation_block, BlockData) + self.assertIsInstance(m.d1.d3.transformation_block, BlockData) + self.assertIsInstance(m.d1.d4.transformation_block, BlockData) def check_transformation_blocks_nestedDisjunctions(self, m, transformation): diff --git a/pyomo/gdp/transformed_disjunct.py b/pyomo/gdp/transformed_disjunct.py index 6cf60abf414..287d5ed1652 100644 --- a/pyomo/gdp/transformed_disjunct.py +++ b/pyomo/gdp/transformed_disjunct.py @@ -10,11 +10,11 @@ # ___________________________________________________________________________ from pyomo.common.autoslots import AutoSlots -from pyomo.core.base.block import _BlockData, IndexedBlock +from pyomo.core.base.block import BlockData, IndexedBlock from pyomo.core.base.global_set import UnindexedComponent_index, UnindexedComponent_set -class _TransformedDisjunctData(_BlockData): +class _TransformedDisjunctData(BlockData): __slots__ = ('_src_disjunct',) __autoslot_mappers__ = {'_src_disjunct': AutoSlots.weakref_mapper} @@ -23,7 +23,7 @@ def src_disjunct(self): return None if self._src_disjunct is None else self._src_disjunct() def __init__(self, component): - _BlockData.__init__(self, component) + BlockData.__init__(self, component) # pointer to the Disjunct whose transformation block this is. self._src_disjunct = None diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index fe11975954d..55d273938c5 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -22,7 +22,7 @@ LogicalConstraint, value, ) -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.common.collections import ComponentMap, ComponentSet, OrderedSet from pyomo.opt import TerminationCondition, SolverStatus @@ -330,7 +330,7 @@ def get_gdp_tree(targets, instance, knownBlocks=None): "Target '%s' is not a component on instance " "'%s'!" % (t.name, instance.name) ) - if t.ctype is Block or isinstance(t, _BlockData): + if t.ctype is Block or isinstance(t, BlockData): _blocks = t.values() if t.is_indexed() else (t,) for block in _blocks: if not block.active: @@ -387,7 +387,7 @@ def is_child_of(parent, child, knownBlocks=None): if knownBlocks is None: knownBlocks = {} tmp = set() - node = child if isinstance(child, (Block, _BlockData)) else child.parent_block() + node = child if isinstance(child, (Block, BlockData)) else child.parent_block() while True: known = knownBlocks.get(node) if known: @@ -452,7 +452,7 @@ def get_src_disjunct(transBlock): Parameters ---------- - transBlock: _BlockData which is in the relaxedDisjuncts IndexedBlock + transBlock: BlockData which is in the relaxedDisjuncts IndexedBlock on a transformation block. """ if ( diff --git a/pyomo/mpec/complementarity.py b/pyomo/mpec/complementarity.py index 79f76a9fc34..3982c7a87ba 100644 --- a/pyomo/mpec/complementarity.py +++ b/pyomo/mpec/complementarity.py @@ -19,7 +19,7 @@ from pyomo.core import Constraint, Var, Block, Set from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.global_set import UnindexedComponent_index -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.base.disable_methods import disable_methods from pyomo.core.base.initializer import ( Initializer, @@ -43,7 +43,7 @@ def complements(a, b): return ComplementarityTuple(a, b) -class _ComplementarityData(_BlockData): +class _ComplementarityData(BlockData): def _canonical_expression(self, e): # Note: as the complimentarity component maintains references to # the original expression (e), it is NOT safe or valid to bypass diff --git a/pyomo/opt/base/solvers.py b/pyomo/opt/base/solvers.py index f1f9d653a8a..c0698165603 100644 --- a/pyomo/opt/base/solvers.py +++ b/pyomo/opt/base/solvers.py @@ -536,15 +536,15 @@ def solve(self, *args, **kwds): # If the inputs are models, then validate that they have been # constructed! Collect suffix names to try and import from solution. # - from pyomo.core.base.block import _BlockData + from pyomo.core.base.block import BlockData import pyomo.core.base.suffix from pyomo.core.kernel.block import IBlock import pyomo.core.kernel.suffix _model = None for arg in args: - if isinstance(arg, (_BlockData, IBlock)): - if isinstance(arg, _BlockData): + if isinstance(arg, (BlockData, IBlock)): + if isinstance(arg, BlockData): if not arg.is_constructed(): raise RuntimeError( "Attempting to solve model=%s with unconstructed " @@ -553,7 +553,7 @@ def solve(self, *args, **kwds): _model = arg # import suffixes must be on the top-level model - if isinstance(arg, _BlockData): + if isinstance(arg, BlockData): model_suffixes = list( name for ( diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 49cca32eaf9..b4a21a2108f 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -486,7 +486,7 @@ def categorize_valid_components( Parameters ---------- - model: _BlockData + model: BlockData The model tree to walk active: True or None @@ -507,7 +507,7 @@ def categorize_valid_components( Returns ------- - component_map: Dict[type, List[_BlockData]] + component_map: Dict[type, List[BlockData]] A dict mapping component type to a list of block data objects that contain declared component of that type. diff --git a/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py b/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py index c131b8ad10a..de38a0372d0 100644 --- a/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py +++ b/pyomo/solvers/plugins/solvers/direct_or_persistent_solver.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.core.base.PyomoModel import Model -from pyomo.core.base.block import Block, _BlockData +from pyomo.core.base.block import Block, BlockData from pyomo.core.kernel.block import IBlock from pyomo.opt.base.solvers import OptSolver from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler @@ -177,7 +177,7 @@ def _postsolve(self): """ This method should be implemented by subclasses.""" def _set_instance(self, model, kwds={}): - if not isinstance(model, (Model, IBlock, Block, _BlockData)): + if not isinstance(model, (Model, IBlock, Block, BlockData)): msg = ( "The problem instance supplied to the {0} plugin " "'_presolve' method must be a Model or a Block".format(type(self)) diff --git a/pyomo/solvers/plugins/solvers/direct_solver.py b/pyomo/solvers/plugins/solvers/direct_solver.py index 3eab658391c..609a81b2018 100644 --- a/pyomo/solvers/plugins/solvers/direct_solver.py +++ b/pyomo/solvers/plugins/solvers/direct_solver.py @@ -15,7 +15,7 @@ from pyomo.solvers.plugins.solvers.direct_or_persistent_solver import ( DirectOrPersistentSolver, ) -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.kernel.block import IBlock from pyomo.core.base.suffix import active_import_suffix_generator from pyomo.core.kernel.suffix import import_suffix_generator @@ -79,8 +79,8 @@ def solve(self, *args, **kwds): # _model = None for arg in args: - if isinstance(arg, (_BlockData, IBlock)): - if isinstance(arg, _BlockData): + if isinstance(arg, (BlockData, IBlock)): + if isinstance(arg, BlockData): if not arg.is_constructed(): raise RuntimeError( "Attempting to solve model=%s with unconstructed " @@ -89,7 +89,7 @@ def solve(self, *args, **kwds): _model = arg # import suffixes must be on the top-level model - if isinstance(arg, _BlockData): + if isinstance(arg, BlockData): model_suffixes = list( name for (name, comp) in active_import_suffix_generator(arg) ) diff --git a/pyomo/solvers/plugins/solvers/mosek_direct.py b/pyomo/solvers/plugins/solvers/mosek_direct.py index 5000a2f35c4..5c07c73b94e 100644 --- a/pyomo/solvers/plugins/solvers/mosek_direct.py +++ b/pyomo/solvers/plugins/solvers/mosek_direct.py @@ -558,7 +558,7 @@ def _add_block(self, block): Parameters ---------- - block: Block (scalar Block or single _BlockData) + block: Block (scalar Block or single BlockData) """ var_seq = tuple( block.component_data_objects( diff --git a/pyomo/solvers/plugins/solvers/persistent_solver.py b/pyomo/solvers/plugins/solvers/persistent_solver.py index 29aa3f2bbf5..d69c050291b 100644 --- a/pyomo/solvers/plugins/solvers/persistent_solver.py +++ b/pyomo/solvers/plugins/solvers/persistent_solver.py @@ -12,7 +12,7 @@ from pyomo.solvers.plugins.solvers.direct_or_persistent_solver import ( DirectOrPersistentSolver, ) -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.core.kernel.block import IBlock from pyomo.core.base.suffix import active_import_suffix_generator from pyomo.core.kernel.suffix import import_suffix_generator @@ -96,7 +96,7 @@ def add_block(self, block): Parameters ---------- - block: Block (scalar Block or single _BlockData) + block: Block (scalar Block or single BlockData) """ if self._pyomo_model is None: @@ -295,7 +295,7 @@ def remove_block(self, block): Parameters ---------- - block: Block (scalar Block or a single _BlockData) + block: Block (scalar Block or a single BlockData) """ # see PR #366 for discussion about handling indexed @@ -455,7 +455,7 @@ def solve(self, *args, **kwds): self.available(exception_flag=True) # Collect suffix names to try and import from solution. - if isinstance(self._pyomo_model, _BlockData): + if isinstance(self._pyomo_model, BlockData): model_suffixes = list( name for (name, comp) in active_import_suffix_generator(self._pyomo_model) diff --git a/pyomo/util/report_scaling.py b/pyomo/util/report_scaling.py index 201319ea92a..5ae28baa715 100644 --- a/pyomo/util/report_scaling.py +++ b/pyomo/util/report_scaling.py @@ -11,7 +11,7 @@ import pyomo.environ as pyo import math -from pyomo.core.base.block import _BlockData +from pyomo.core.base.block import BlockData from pyomo.common.collections import ComponentSet from pyomo.core.base.var import _GeneralVarData from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr @@ -42,7 +42,7 @@ def _print_var_set(var_set): return s -def _check_var_bounds(m: _BlockData, too_large: float): +def _check_var_bounds(m: BlockData, too_large: float): vars_without_bounds = ComponentSet() vars_with_large_bounds = ComponentSet() for v in m.component_data_objects(pyo.Var, descend_into=True): @@ -90,7 +90,7 @@ def _check_coefficients( def report_scaling( - m: _BlockData, too_large: float = 5e4, too_small: float = 1e-6 + m: BlockData, too_large: float = 5e4, too_small: float = 1e-6 ) -> bool: """ This function logs potentially poorly scaled parts of the model. @@ -107,7 +107,7 @@ def report_scaling( Parameters ---------- - m: _BlockData + m: BlockData The pyomo model or block too_large: float Values above too_large will generate a log entry diff --git a/pyomo/util/slices.py b/pyomo/util/slices.py index 53f6d364219..d85aa3fa926 100644 --- a/pyomo/util/slices.py +++ b/pyomo/util/slices.py @@ -98,7 +98,7 @@ def slice_component_along_sets(comp, sets, context=None): sets: `pyomo.common.collections.ComponentSet` Contains the sets to replace with slices context: `pyomo.core.base.block.Block` or - `pyomo.core.base.block._BlockData` + `pyomo.core.base.block.BlockData` Block below which to search for sets Returns: From c59f91bb1f0d6b94b9e9fb7bbce05ecb6c033c4c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:32:28 -0600 Subject: [PATCH 0981/3044] Renamed _BooleanVarData -> BooleanVarData --- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/boolean_var.py | 13 +++++++++---- pyomo/core/base/component.py | 2 +- pyomo/core/plugins/transform/logical_to_linear.py | 4 ++-- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 4bbd0c9dc44..98eceb45490 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -60,7 +60,7 @@ from pyomo.core.base.var import Var, _VarData, _GeneralVarData, ScalarVar, VarList from pyomo.core.base.boolean_var import ( BooleanVar, - _BooleanVarData, + BooleanVarData, _GeneralBooleanVarData, BooleanVarList, ScalarBooleanVar, diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index 246dcea6214..bf9d6159754 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -68,7 +68,7 @@ def __setstate__(self, state): self._boolvar = weakref_ref(state) -class _BooleanVarData(ComponentData, BooleanValue): +class BooleanVarData(ComponentData, BooleanValue): """ This class defines the data for a single variable. @@ -177,6 +177,11 @@ def free(self): return self.unfix() +class _BooleanVarData(metaclass=RenamedClass): + __renamed__new_class__ = BooleanVarData + __renamed__version__ = '6.7.2.dev0' + + def _associated_binary_mapper(encode, val): if val is None: return None @@ -189,7 +194,7 @@ def _associated_binary_mapper(encode, val): return val -class _GeneralBooleanVarData(_BooleanVarData): +class _GeneralBooleanVarData(BooleanVarData): """ This class defines the data for a single Boolean variable. @@ -222,7 +227,7 @@ def __init__(self, component=None): # # These lines represent in-lining of the # following constructors: - # - _BooleanVarData + # - BooleanVarData # - ComponentData # - BooleanValue self._component = weakref_ref(component) if (component is not None) else None @@ -390,7 +395,7 @@ def construct(self, data=None): _set.construct() # - # Construct _BooleanVarData objects for all index values + # Construct BooleanVarData objects for all index values # if not self.is_indexed(): self._data[None] = self diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index c91167379fd..0618d6d9d56 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -802,7 +802,7 @@ class ComponentData(_ComponentBase): __autoslot_mappers__ = {'_component': AutoSlots.weakref_mapper} # NOTE: This constructor is in-lined in the constructors for the following - # classes: _BooleanVarData, _ConnectorData, _ConstraintData, + # classes: BooleanVarData, _ConnectorData, _ConstraintData, # _GeneralExpressionData, _LogicalConstraintData, # _GeneralLogicalConstraintData, _GeneralObjectiveData, # _ParamData,_GeneralVarData, _GeneralBooleanVarData, _DisjunctionData, diff --git a/pyomo/core/plugins/transform/logical_to_linear.py b/pyomo/core/plugins/transform/logical_to_linear.py index 69328032004..da69ca113bd 100644 --- a/pyomo/core/plugins/transform/logical_to_linear.py +++ b/pyomo/core/plugins/transform/logical_to_linear.py @@ -285,7 +285,7 @@ class CnfToLinearVisitor(StreamBasedExpressionVisitor): """Convert CNF logical constraint to linear constraints. Expected expression node types: AndExpression, OrExpression, NotExpression, - AtLeastExpression, AtMostExpression, ExactlyExpression, _BooleanVarData + AtLeastExpression, AtMostExpression, ExactlyExpression, BooleanVarData """ @@ -372,7 +372,7 @@ def beforeChild(self, node, child, child_idx): if child.is_expression_type(): return True, None - # Only thing left should be _BooleanVarData + # Only thing left should be BooleanVarData # # TODO: After the expr_multiple_dispatch is merged, this should # be switched to using as_numeric. From 0c72d9faa267b4f02d3d8b11daad3581701fbb99 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:32:28 -0600 Subject: [PATCH 0982/3044] Renamed _ComplementarityData -> ComplementarityData --- pyomo/mpec/complementarity.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pyomo/mpec/complementarity.py b/pyomo/mpec/complementarity.py index 3982c7a87ba..aa8db922145 100644 --- a/pyomo/mpec/complementarity.py +++ b/pyomo/mpec/complementarity.py @@ -43,7 +43,7 @@ def complements(a, b): return ComplementarityTuple(a, b) -class _ComplementarityData(BlockData): +class ComplementarityData(BlockData): def _canonical_expression(self, e): # Note: as the complimentarity component maintains references to # the original expression (e), it is NOT safe or valid to bypass @@ -179,9 +179,14 @@ def set_value(self, cc): ) +class _ComplementarityData(metaclass=RenamedClass): + __renamed__new_class__ = ComplementarityData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("Complementarity conditions.") class Complementarity(Block): - _ComponentDataClass = _ComplementarityData + _ComponentDataClass = ComplementarityData def __new__(cls, *args, **kwds): if cls != Complementarity: @@ -298,9 +303,9 @@ def _conditional_block_printer(ostream, idx, data): ) -class ScalarComplementarity(_ComplementarityData, Complementarity): +class ScalarComplementarity(ComplementarityData, Complementarity): def __init__(self, *args, **kwds): - _ComplementarityData.__init__(self, self) + ComplementarityData.__init__(self, self) Complementarity.__init__(self, *args, **kwds) self._data[None] = self self._index = UnindexedComponent_index From e3fe3162f7b314ecdc84834b727fad6148bfa0ba Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:32:59 -0600 Subject: [PATCH 0983/3044] Renamed _ConnectorData -> ConnectorData --- pyomo/core/base/component.py | 2 +- pyomo/core/base/connector.py | 15 ++++++++++----- pyomo/core/plugins/transform/expand_connectors.py | 4 ++-- pyomo/repn/standard_repn.py | 4 ++-- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 0618d6d9d56..244d9b6a8f5 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -802,7 +802,7 @@ class ComponentData(_ComponentBase): __autoslot_mappers__ = {'_component': AutoSlots.weakref_mapper} # NOTE: This constructor is in-lined in the constructors for the following - # classes: BooleanVarData, _ConnectorData, _ConstraintData, + # classes: BooleanVarData, ConnectorData, _ConstraintData, # _GeneralExpressionData, _LogicalConstraintData, # _GeneralLogicalConstraintData, _GeneralObjectiveData, # _ParamData,_GeneralVarData, _GeneralBooleanVarData, _DisjunctionData, diff --git a/pyomo/core/base/connector.py b/pyomo/core/base/connector.py index 435a2c2fccb..e383b52fc11 100644 --- a/pyomo/core/base/connector.py +++ b/pyomo/core/base/connector.py @@ -28,7 +28,7 @@ logger = logging.getLogger('pyomo.core') -class _ConnectorData(ComponentData, NumericValue): +class ConnectorData(ComponentData, NumericValue): """Holds the actual connector information""" __slots__ = ('vars', 'aggregators') @@ -105,6 +105,11 @@ def _iter_vars(self): yield v +class _ConnectorData(metaclass=RenamedClass): + __renamed__new_class__ = ConnectorData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register( "A bundle of variables that can be manipulated together." ) @@ -157,7 +162,7 @@ def __init__(self, *args, **kwd): # IndexedComponent # def _getitem_when_not_present(self, idx): - _conval = self._data[idx] = _ConnectorData(component=self) + _conval = self._data[idx] = ConnectorData(component=self) return _conval def construct(self, data=None): @@ -170,7 +175,7 @@ def construct(self, data=None): timer = ConstructionTimer(self) self._constructed = True # - # Construct _ConnectorData objects for all index values + # Construct ConnectorData objects for all index values # if self.is_indexed(): self._initialize_members(self._index_set) @@ -258,9 +263,9 @@ def _line_generator(k, v): ) -class ScalarConnector(Connector, _ConnectorData): +class ScalarConnector(Connector, ConnectorData): def __init__(self, *args, **kwd): - _ConnectorData.__init__(self, component=self) + ConnectorData.__init__(self, component=self) Connector.__init__(self, *args, **kwd) self._index = UnindexedComponent_index diff --git a/pyomo/core/plugins/transform/expand_connectors.py b/pyomo/core/plugins/transform/expand_connectors.py index 8c02f3e5698..82ec546e593 100644 --- a/pyomo/core/plugins/transform/expand_connectors.py +++ b/pyomo/core/plugins/transform/expand_connectors.py @@ -25,7 +25,7 @@ Var, SortComponents, ) -from pyomo.core.base.connector import _ConnectorData, ScalarConnector +from pyomo.core.base.connector import ConnectorData, ScalarConnector @TransformationFactory.register( @@ -69,7 +69,7 @@ def _apply_to(self, instance, **kwds): # The set of connectors found in the current constraint found = ComponentSet() - connector_types = set([ScalarConnector, _ConnectorData]) + connector_types = set([ScalarConnector, ConnectorData]) for constraint in instance.component_data_objects( Constraint, sort=SortComponents.deterministic ): diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 8600a8a50f6..455e7bd9444 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -1136,7 +1136,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra EXPR.RangedExpression: _collect_comparison, EXPR.EqualityExpression: _collect_comparison, EXPR.ExternalFunctionExpression: _collect_external_fn, - # _ConnectorData : _collect_linear_connector, + # ConnectorData : _collect_linear_connector, # ScalarConnector : _collect_linear_connector, _ParamData: _collect_const, ScalarParam: _collect_const, @@ -1536,7 +1536,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): #EXPR.EqualityExpression : _linear_collect_comparison, #EXPR.ExternalFunctionExpression : _linear_collect_external_fn, ##EXPR.LinearSumExpression : _collect_linear_sum, - ##_ConnectorData : _collect_linear_connector, + ##ConnectorData : _collect_linear_connector, ##ScalarConnector : _collect_linear_connector, ##param._ParamData : _collect_linear_const, ##param.ScalarParam : _collect_linear_const, From d4b72d2b56193ef68589913d9abd7135242609ba Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:36:53 -0600 Subject: [PATCH 0984/3044] Renamed _ConstraintData -> ConstraintData --- pyomo/contrib/viewer/report.py | 2 +- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/component.py | 2 +- pyomo/core/base/constraint.py | 15 ++++++++++----- pyomo/core/base/logical_constraint.py | 2 +- pyomo/core/base/matrix_constraint.py | 8 ++++---- pyomo/core/beta/dict_objects.py | 4 ++-- pyomo/core/beta/list_objects.py | 4 ++-- pyomo/core/plugins/transform/add_slack_vars.py | 6 +++--- .../core/plugins/transform/equality_transform.py | 4 ++-- pyomo/core/plugins/transform/model.py | 4 ++-- pyomo/core/plugins/transform/scaling.py | 4 ++-- pyomo/core/tests/unit/test_con.py | 2 +- pyomo/gdp/disjunct.py | 2 +- pyomo/gdp/plugins/hull.py | 6 +++--- pyomo/gdp/tests/test_bigm.py | 8 ++++---- pyomo/gdp/util.py | 4 ++-- pyomo/repn/beta/matrix.py | 14 +++++++------- pyomo/repn/plugins/nl_writer.py | 6 +++--- pyomo/repn/plugins/standard_form.py | 4 ++-- pyomo/solvers/plugins/solvers/mosek_persistent.py | 8 ++++---- .../solvers/plugins/solvers/persistent_solver.py | 6 +++--- .../solvers/tests/checks/test_CPLEXPersistent.py | 2 +- .../tests/checks/test_gurobi_persistent.py | 2 +- .../tests/checks/test_xpress_persistent.py | 2 +- pyomo/util/calc_var_value.py | 6 +++--- 26 files changed, 67 insertions(+), 62 deletions(-) diff --git a/pyomo/contrib/viewer/report.py b/pyomo/contrib/viewer/report.py index a1f893bba31..a28e0082212 100644 --- a/pyomo/contrib/viewer/report.py +++ b/pyomo/contrib/viewer/report.py @@ -50,7 +50,7 @@ def get_residual(ui_data, c): values of the constraint body. This function uses the cached values and will not trigger recalculation. If variable values have changed, this may not yield accurate results. - c(_ConstraintData): a constraint or constraint data + c(ConstraintData): a constraint or constraint data Returns: (float) residual """ diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 98eceb45490..9a5337ac2c8 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -70,7 +70,7 @@ simple_constraintlist_rule, ConstraintList, Constraint, - _ConstraintData, + ConstraintData, ) from pyomo.core.base.logical_constraint import ( LogicalConstraint, diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 244d9b6a8f5..7d6fc903632 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -802,7 +802,7 @@ class ComponentData(_ComponentBase): __autoslot_mappers__ = {'_component': AutoSlots.weakref_mapper} # NOTE: This constructor is in-lined in the constructors for the following - # classes: BooleanVarData, ConnectorData, _ConstraintData, + # classes: BooleanVarData, ConnectorData, ConstraintData, # _GeneralExpressionData, _LogicalConstraintData, # _GeneralLogicalConstraintData, _GeneralObjectiveData, # _ParamData,_GeneralVarData, _GeneralBooleanVarData, _DisjunctionData, diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index fde1160e563..cbae828a459 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -130,7 +130,7 @@ def C_rule(model, i, j): # -class _ConstraintData(ActiveComponentData): +class ConstraintData(ActiveComponentData): """ This class defines the data for a single constraint. @@ -165,7 +165,7 @@ def __init__(self, component=None): # # These lines represent in-lining of the # following constructors: - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None @@ -280,7 +280,12 @@ def get_value(self): raise NotImplementedError -class _GeneralConstraintData(_ConstraintData): +class _ConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = ConstraintData + __renamed__version__ = '6.7.2.dev0' + + +class _GeneralConstraintData(ConstraintData): """ This class defines the data for a single general constraint. @@ -312,7 +317,7 @@ def __init__(self, expr=None, component=None): # # These lines represent in-lining of the # following constructors: - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None @@ -897,7 +902,7 @@ def __init__(self, *args, **kwds): # currently in place). So during initialization only, we will # treat them as "indexed" objects where things like # Constraint.Skip are managed. But after that they will behave - # like _ConstraintData objects where set_value does not handle + # like ConstraintData objects where set_value does not handle # Constraint.Skip but expects a valid expression or None. # @property diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index f32d727931a..3a7bca75960 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -373,7 +373,7 @@ def display(self, prefix="", ostream=None): # # Checks flags like Constraint.Skip, etc. before actually creating a - # constraint object. Returns the _ConstraintData object when it should be + # constraint object. Returns the ConstraintData object when it should be # added to the _data dict; otherwise, None is returned or an exception # is raised. # diff --git a/pyomo/core/base/matrix_constraint.py b/pyomo/core/base/matrix_constraint.py index adc9742302e..8dac7c3d24b 100644 --- a/pyomo/core/base/matrix_constraint.py +++ b/pyomo/core/base/matrix_constraint.py @@ -19,7 +19,7 @@ from pyomo.core.expr.numvalue import value from pyomo.core.expr.numeric_expr import LinearExpression from pyomo.core.base.component import ModelComponentFactory -from pyomo.core.base.constraint import IndexedConstraint, _ConstraintData +from pyomo.core.base.constraint import IndexedConstraint, ConstraintData from pyomo.repn.standard_repn import StandardRepn from collections.abc import Mapping @@ -28,7 +28,7 @@ logger = logging.getLogger('pyomo.core') -class _MatrixConstraintData(_ConstraintData): +class _MatrixConstraintData(ConstraintData): """ This class defines the data for a single linear constraint derived from a canonical form Ax=b constraint. @@ -104,7 +104,7 @@ def __init__(self, index, component_ref): # # These lines represent in-lining of the # following constructors: - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = component_ref @@ -209,7 +209,7 @@ def index(self): return self._index # - # Abstract Interface (_ConstraintData) + # Abstract Interface (ConstraintData) # @property diff --git a/pyomo/core/beta/dict_objects.py b/pyomo/core/beta/dict_objects.py index a8298b08e63..53d39939db2 100644 --- a/pyomo/core/beta/dict_objects.py +++ b/pyomo/core/beta/dict_objects.py @@ -15,7 +15,7 @@ from pyomo.common.log import is_debug_set from pyomo.core.base.set_types import Any from pyomo.core.base.var import IndexedVar, _VarData -from pyomo.core.base.constraint import IndexedConstraint, _ConstraintData +from pyomo.core.base.constraint import IndexedConstraint, ConstraintData from pyomo.core.base.objective import IndexedObjective, _ObjectiveData from pyomo.core.base.expression import IndexedExpression, _ExpressionData @@ -193,7 +193,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentDict needs to # go last in order to handle any initialization # iterable as an argument - ComponentDict.__init__(self, _ConstraintData, *args, **kwds) + ComponentDict.__init__(self, ConstraintData, *args, **kwds) class ObjectiveDict(ComponentDict, IndexedObjective): diff --git a/pyomo/core/beta/list_objects.py b/pyomo/core/beta/list_objects.py index f53997fed17..e8b40e6da53 100644 --- a/pyomo/core/beta/list_objects.py +++ b/pyomo/core/beta/list_objects.py @@ -15,7 +15,7 @@ from pyomo.common.log import is_debug_set from pyomo.core.base.set_types import Any from pyomo.core.base.var import IndexedVar, _VarData -from pyomo.core.base.constraint import IndexedConstraint, _ConstraintData +from pyomo.core.base.constraint import IndexedConstraint, ConstraintData from pyomo.core.base.objective import IndexedObjective, _ObjectiveData from pyomo.core.base.expression import IndexedExpression, _ExpressionData @@ -241,7 +241,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentList needs to # go last in order to handle any initialization # iterable as an argument - ComponentList.__init__(self, _ConstraintData, *args, **kwds) + ComponentList.__init__(self, ConstraintData, *args, **kwds) class XObjectiveList(ComponentList, IndexedObjective): diff --git a/pyomo/core/plugins/transform/add_slack_vars.py b/pyomo/core/plugins/transform/add_slack_vars.py index 6b5096d315c..0007f8de7ad 100644 --- a/pyomo/core/plugins/transform/add_slack_vars.py +++ b/pyomo/core/plugins/transform/add_slack_vars.py @@ -23,7 +23,7 @@ from pyomo.core.plugins.transform.hierarchy import NonIsomorphicTransformation from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.core.base import ComponentUID -from pyomo.core.base.constraint import _ConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.common.deprecation import deprecation_warning @@ -42,7 +42,7 @@ def target_list(x): # [ESJ 07/15/2020] We have to just pass it through because we need the # instance in order to be able to do anything about it... return [x] - elif isinstance(x, (Constraint, _ConstraintData)): + elif isinstance(x, (Constraint, ConstraintData)): return [x] elif hasattr(x, '__iter__'): ans = [] @@ -53,7 +53,7 @@ def target_list(x): deprecation_msg = None # same as above... ans.append(i) - elif isinstance(i, (Constraint, _ConstraintData)): + elif isinstance(i, (Constraint, ConstraintData)): ans.append(i) else: raise ValueError( diff --git a/pyomo/core/plugins/transform/equality_transform.py b/pyomo/core/plugins/transform/equality_transform.py index a1a1b72f146..99291c2227c 100644 --- a/pyomo/core/plugins/transform/equality_transform.py +++ b/pyomo/core/plugins/transform/equality_transform.py @@ -66,7 +66,7 @@ def _create_using(self, model, **kwds): con = equality.__getattribute__(con_name) # - # Get all _ConstraintData objects + # Get all ConstraintData objects # # We need to get the keys ahead of time because we are modifying # con._data on-the-fly. @@ -104,7 +104,7 @@ def _create_using(self, model, **kwds): con.add(ub_name, new_expr) # Since we explicitly `continue` for equality constraints, we - # can safely remove the old _ConstraintData object + # can safely remove the old ConstraintData object del con._data[ndx] return equality.create() diff --git a/pyomo/core/plugins/transform/model.py b/pyomo/core/plugins/transform/model.py index db8376afd29..7ee268a4292 100644 --- a/pyomo/core/plugins/transform/model.py +++ b/pyomo/core/plugins/transform/model.py @@ -55,8 +55,8 @@ def to_standard_form(self): # N.B. Structure hierarchy: # # active_components: {class: {attr_name: object}} - # object -> Constraint: ._data: {ndx: _ConstraintData} - # _ConstraintData: .lower, .body, .upper + # object -> Constraint: ._data: {ndx: ConstraintData} + # ConstraintData: .lower, .body, .upper # # So, altogether, we access a lower bound via # diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index ad894b31fde..6b83a2378d1 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -15,7 +15,7 @@ Var, Constraint, Objective, - _ConstraintData, + ConstraintData, _ObjectiveData, Suffix, value, @@ -197,7 +197,7 @@ def _apply_to(self, model, rename=True): already_scaled.add(id(c)) # perform the constraint/objective scaling and variable sub scaling_factor = component_scaling_factor_map[c] - if isinstance(c, _ConstraintData): + if isinstance(c, ConstraintData): body = scaling_factor * replace_expressions( expr=c.body, substitution_map=variable_substitution_dict, diff --git a/pyomo/core/tests/unit/test_con.py b/pyomo/core/tests/unit/test_con.py index 6ed19c1bcfd..2fa6c24de9c 100644 --- a/pyomo/core/tests/unit/test_con.py +++ b/pyomo/core/tests/unit/test_con.py @@ -1388,7 +1388,7 @@ def test_empty_singleton(self): # Even though we construct a ScalarConstraint, # if it is not initialized that means it is "empty" # and we should encounter errors when trying to access the - # _ConstraintData interface methods until we assign + # ConstraintData interface methods until we assign # something to the constraint. # self.assertEqual(a._constructed, True) diff --git a/pyomo/gdp/disjunct.py b/pyomo/gdp/disjunct.py index e6d8d709425..de021d37547 100644 --- a/pyomo/gdp/disjunct.py +++ b/pyomo/gdp/disjunct.py @@ -542,7 +542,7 @@ def __init__(self, component=None): # # These lines represent in-lining of the # following constructors: - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 5b9d2ad08a9..134a3d16d66 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -750,20 +750,20 @@ def _transform_constraint( if obj.is_indexed(): newConstraint.add((name, i, 'eq'), newConsExpr) - # map the _ConstraintDatas (we mapped the container above) + # map the ConstraintDatas (we mapped the container above) constraint_map.transformed_constraints[c].append( newConstraint[name, i, 'eq'] ) constraint_map.src_constraint[newConstraint[name, i, 'eq']] = c else: newConstraint.add((name, 'eq'), newConsExpr) - # map to the _ConstraintData (And yes, for + # map to the ConstraintData (And yes, for # ScalarConstraints, this is overwriting the map to the # container we made above, and that is what I want to # happen. ScalarConstraints will map to lists. For # IndexedConstraints, we can map the container to the # container, but more importantly, we are mapping the - # _ConstraintDatas to each other above) + # ConstraintDatas to each other above) constraint_map.transformed_constraints[c].append( newConstraint[name, 'eq'] ) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index c6ac49f6d36..bf0239d15e0 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -27,7 +27,7 @@ value, ) from pyomo.gdp import Disjunct, Disjunction, GDP_Error -from pyomo.core.base import constraint, _ConstraintData +from pyomo.core.base import constraint, ConstraintData from pyomo.core.expr.compare import ( assertExpressionsEqual, assertExpressionsStructurallyEqual, @@ -653,14 +653,14 @@ def test_disjunct_and_constraint_maps(self): if src[0]: # equality self.assertEqual(len(transformed), 2) - self.assertIsInstance(transformed[0], _ConstraintData) - self.assertIsInstance(transformed[1], _ConstraintData) + self.assertIsInstance(transformed[0], ConstraintData) + self.assertIsInstance(transformed[1], ConstraintData) self.assertIs(bigm.get_src_constraint(transformed[0]), srcDisjunct.c) self.assertIs(bigm.get_src_constraint(transformed[1]), srcDisjunct.c) else: # >= self.assertEqual(len(transformed), 1) - self.assertIsInstance(transformed[0], _ConstraintData) + self.assertIsInstance(transformed[0], ConstraintData) # check reverse map from the container self.assertIs(bigm.get_src_constraint(transformed[0]), srcDisjunct.c) diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index 55d273938c5..2164671ea16 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -525,13 +525,13 @@ def get_transformed_constraints(srcConstraint): Parameters ---------- - srcConstraint: ScalarConstraint or _ConstraintData, which must be in + srcConstraint: ScalarConstraint or ConstraintData, which must be in the subtree of a transformed Disjunct """ if srcConstraint.is_indexed(): raise GDP_Error( "Argument to get_transformed_constraint should be " - "a ScalarConstraint or _ConstraintData. (If you " + "a ScalarConstraint or ConstraintData. (If you " "want the container for all transformed constraints " "from an IndexedDisjunction, this is the parent " "component of a transformed constraint originating " diff --git a/pyomo/repn/beta/matrix.py b/pyomo/repn/beta/matrix.py index 916b0daf755..0201c46eb18 100644 --- a/pyomo/repn/beta/matrix.py +++ b/pyomo/repn/beta/matrix.py @@ -24,7 +24,7 @@ Constraint, IndexedConstraint, ScalarConstraint, - _ConstraintData, + ConstraintData, ) from pyomo.core.expr.numvalue import native_numeric_types from pyomo.repn import generate_standard_repn @@ -247,7 +247,7 @@ def _get_bound(exp): constraint_containers_removed += 1 for constraint, index in constraint_data_to_remove: # Note that this del is not needed: assigning Constraint.Skip - # above removes the _ConstraintData from the _data dict. + # above removes the ConstraintData from the _data dict. # del constraint[index] constraints_removed += 1 for block, constraint in constraint_containers_to_remove: @@ -348,12 +348,12 @@ def _get_bound(exp): ) -# class _LinearConstraintData(_ConstraintData,LinearCanonicalRepn): +# class _LinearConstraintData(ConstraintData,LinearCanonicalRepn): # # This change breaks this class, but it's unclear whether this # is being used... # -class _LinearConstraintData(_ConstraintData): +class _LinearConstraintData(ConstraintData): """ This class defines the data for a single linear constraint in canonical form. @@ -393,7 +393,7 @@ def __init__(self, index, component=None): # # These lines represent in-lining of the # following constructors: - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None @@ -442,7 +442,7 @@ def __init__(self, index, component=None): # These lines represent in-lining of the # following constructors: # - _LinearConstraintData - # - _ConstraintData, + # - ConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None @@ -584,7 +584,7 @@ def constant(self): return sum(terms) # - # Abstract Interface (_ConstraintData) + # Abstract Interface (ConstraintData) # @property diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index ee5b65149ae..29d841248da 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -69,7 +69,7 @@ minimize, ) from pyomo.core.base.component import ActiveComponent -from pyomo.core.base.constraint import _ConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData from pyomo.core.base.objective import ( ScalarObjective, @@ -134,7 +134,7 @@ class NLWriterInfo(object): The list of (unfixed) Pyomo model variables in the order written to the NL file - constraints: List[_ConstraintData] + constraints: List[ConstraintData] The list of (active) Pyomo model constraints in the order written to the NL file @@ -466,7 +466,7 @@ def compile(self, column_order, row_order, obj_order, model_id): self.obj[obj_order[_id]] = val elif _id == model_id: self.prob[0] = val - elif isinstance(obj, (_VarData, _ConstraintData, _ObjectiveData)): + elif isinstance(obj, (_VarData, ConstraintData, _ObjectiveData)): missing_component_data.add(obj) elif isinstance(obj, (Var, Constraint, Objective)): # Expand this indexed component to store the diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index 239cd845930..e6dc217acc9 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -76,11 +76,11 @@ class LinearStandardFormInfo(object): The constraint right-hand sides. - rows : List[Tuple[_ConstraintData, int]] + rows : List[Tuple[ConstraintData, int]] The list of Pyomo constraint objects corresponding to the rows in `A`. Each element in the list is a 2-tuple of - (_ConstraintData, row_multiplier). The `row_multiplier` will be + (ConstraintData, row_multiplier). The `row_multiplier` will be +/- 1 indicating if the row was multiplied by -1 (corresponding to a constraint lower bound) or +1 (upper bound). diff --git a/pyomo/solvers/plugins/solvers/mosek_persistent.py b/pyomo/solvers/plugins/solvers/mosek_persistent.py index 97f88e0cb9a..9e7f8de1b41 100644 --- a/pyomo/solvers/plugins/solvers/mosek_persistent.py +++ b/pyomo/solvers/plugins/solvers/mosek_persistent.py @@ -85,7 +85,7 @@ def add_constraints(self, con_seq): Parameters ---------- - con_seq: tuple/list of Constraint (scalar Constraint or single _ConstraintData) + con_seq: tuple/list of Constraint (scalar Constraint or single ConstraintData) """ self._add_constraints(con_seq) @@ -137,7 +137,7 @@ def remove_constraint(self, solver_con): To remove a conic-domain, you should use the remove_block method. Parameters ---------- - solver_con: Constraint (scalar Constraint or single _ConstraintData) + solver_con: Constraint (scalar Constraint or single ConstraintData) """ self.remove_constraints(solver_con) @@ -151,7 +151,7 @@ def remove_constraints(self, *solver_cons): Parameters ---------- - *solver_cons: Constraint (scalar Constraint or single _ConstraintData) + *solver_cons: Constraint (scalar Constraint or single ConstraintData) """ lq_cons = tuple( itertools.filterfalse(lambda x: isinstance(x, _ConicBase), solver_cons) @@ -205,7 +205,7 @@ def update_vars(self, *solver_vars): changing variable types and bounds. Parameters ---------- - *solver_var: Constraint (scalar Constraint or single _ConstraintData) + *solver_var: Constraint (scalar Constraint or single ConstraintData) """ try: var_ids = [] diff --git a/pyomo/solvers/plugins/solvers/persistent_solver.py b/pyomo/solvers/plugins/solvers/persistent_solver.py index d69c050291b..79cd669dd71 100644 --- a/pyomo/solvers/plugins/solvers/persistent_solver.py +++ b/pyomo/solvers/plugins/solvers/persistent_solver.py @@ -132,7 +132,7 @@ def add_constraint(self, con): Parameters ---------- - con: Constraint (scalar Constraint or single _ConstraintData) + con: Constraint (scalar Constraint or single ConstraintData) """ if self._pyomo_model is None: @@ -208,7 +208,7 @@ def add_column(self, model, var, obj_coef, constraints, coefficients): model: pyomo ConcreteModel to which the column will be added var: Var (scalar Var or single _VarData) obj_coef: float, pyo.Param - constraints: list of scalar Constraints of single _ConstraintDatas + constraints: list of scalar Constraints of single ConstraintDatas coefficients: list of the coefficient to put on var in the associated constraint """ @@ -328,7 +328,7 @@ def remove_constraint(self, con): Parameters ---------- - con: Constraint (scalar Constraint or single _ConstraintData) + con: Constraint (scalar Constraint or single ConstraintData) """ # see PR #366 for discussion about handling indexed diff --git a/pyomo/solvers/tests/checks/test_CPLEXPersistent.py b/pyomo/solvers/tests/checks/test_CPLEXPersistent.py index 91a60eee9dd..442212d4fbb 100644 --- a/pyomo/solvers/tests/checks/test_CPLEXPersistent.py +++ b/pyomo/solvers/tests/checks/test_CPLEXPersistent.py @@ -101,7 +101,7 @@ def test_add_column_exceptions(self): # add indexed constraint self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.ci], [1]) - # add something not a _ConstraintData + # add something not a ConstraintData self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.x], [1]) # constraint not on solver model diff --git a/pyomo/solvers/tests/checks/test_gurobi_persistent.py b/pyomo/solvers/tests/checks/test_gurobi_persistent.py index a2c089207e5..812390c23a4 100644 --- a/pyomo/solvers/tests/checks/test_gurobi_persistent.py +++ b/pyomo/solvers/tests/checks/test_gurobi_persistent.py @@ -382,7 +382,7 @@ def test_add_column_exceptions(self): # add indexed constraint self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.ci], [1]) - # add something not a _ConstraintData + # add something not a ConstraintData self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.x], [1]) # constraint not on solver model diff --git a/pyomo/solvers/tests/checks/test_xpress_persistent.py b/pyomo/solvers/tests/checks/test_xpress_persistent.py index ddae860cd92..dcd36780f62 100644 --- a/pyomo/solvers/tests/checks/test_xpress_persistent.py +++ b/pyomo/solvers/tests/checks/test_xpress_persistent.py @@ -262,7 +262,7 @@ def test_add_column_exceptions(self): # add indexed constraint self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.ci], [1]) - # add something not a _ConstraintData + # add something not a ConstraintData self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.x], [1]) # constraint not on solver model diff --git a/pyomo/util/calc_var_value.py b/pyomo/util/calc_var_value.py index b5e620fea07..d5bceb5c67b 100644 --- a/pyomo/util/calc_var_value.py +++ b/pyomo/util/calc_var_value.py @@ -12,7 +12,7 @@ from pyomo.common.errors import IterationLimitError from pyomo.common.numeric_types import native_numeric_types, native_complex_types, value from pyomo.core.expr.calculus.derivatives import differentiate -from pyomo.core.base.constraint import Constraint, _ConstraintData +from pyomo.core.base.constraint import Constraint, ConstraintData import logging @@ -55,7 +55,7 @@ def calculate_variable_from_constraint( ----------- variable: :py:class:`_VarData` The variable to solve for - constraint: :py:class:`_ConstraintData` or relational expression or `tuple` + constraint: :py:class:`ConstraintData` or relational expression or `tuple` The equality constraint to use to solve for the variable value. May be a `ConstraintData` object or any valid argument for ``Constraint(expr=<>)`` (i.e., a relational expression or 2- or @@ -81,7 +81,7 @@ def calculate_variable_from_constraint( """ # Leverage all the Constraint logic to process the incoming tuple/expression - if not isinstance(constraint, _ConstraintData): + if not isinstance(constraint, ConstraintData): constraint = Constraint(expr=constraint, name=type(constraint).__name__) constraint.construct() From 61c91d065eaf51cbcef2636a836566777239edc0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:37:15 -0600 Subject: [PATCH 0985/3044] Renamed _DisjunctData -> DisjunctData --- pyomo/contrib/gdp_bounds/info.py | 4 ++-- pyomo/gdp/disjunct.py | 27 ++++++++++++++++----------- pyomo/gdp/plugins/hull.py | 2 +- pyomo/gdp/tests/test_bigm.py | 2 +- pyomo/gdp/util.py | 4 ++-- 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/gdp_bounds/info.py b/pyomo/contrib/gdp_bounds/info.py index 6f39af5908d..db3f6d0846d 100644 --- a/pyomo/contrib/gdp_bounds/info.py +++ b/pyomo/contrib/gdp_bounds/info.py @@ -37,8 +37,8 @@ def disjunctive_bound(var, scope): Args: var (_VarData): Variable for which to compute bound scope (Component): The scope in which to compute the bound. If not a - _DisjunctData, it will walk up the tree and use the scope of the - most immediate enclosing _DisjunctData. + DisjunctData, it will walk up the tree and use the scope of the + most immediate enclosing DisjunctData. Returns: numeric: the tighter of either the disjunctive lower bound, the diff --git a/pyomo/gdp/disjunct.py b/pyomo/gdp/disjunct.py index de021d37547..dd9d2b4638c 100644 --- a/pyomo/gdp/disjunct.py +++ b/pyomo/gdp/disjunct.py @@ -412,7 +412,7 @@ def process(arg): return (_Initializer.deferred_value, arg) -class _DisjunctData(BlockData): +class DisjunctData(BlockData): __autoslot_mappers__ = {'_transformation_block': AutoSlots.weakref_mapper} _Block_reserved_words = set() @@ -434,23 +434,28 @@ def __init__(self, component): self._transformation_block = None def activate(self): - super(_DisjunctData, self).activate() + super(DisjunctData, self).activate() self.indicator_var.unfix() def deactivate(self): - super(_DisjunctData, self).deactivate() + super(DisjunctData, self).deactivate() self.indicator_var.fix(False) def _deactivate_without_fixing_indicator(self): - super(_DisjunctData, self).deactivate() + super(DisjunctData, self).deactivate() def _activate_without_unfixing_indicator(self): - super(_DisjunctData, self).activate() + super(DisjunctData, self).activate() + + +class _DisjunctData(metaclass=RenamedClass): + __renamed__new_class__ = DisjunctData + __renamed__version__ = '6.7.2.dev0' @ModelComponentFactory.register("Disjunctive blocks.") class Disjunct(Block): - _ComponentDataClass = _DisjunctData + _ComponentDataClass = DisjunctData def __new__(cls, *args, **kwds): if cls != Disjunct: @@ -475,7 +480,7 @@ def __init__(self, *args, **kwargs): # def _deactivate_without_fixing_indicator(self): # # Ideally, this would be a super call from this class. However, # # doing that would trigger a call to deactivate() on all the - # # _DisjunctData objects (exactly what we want to avoid!) + # # DisjunctData objects (exactly what we want to avoid!) # # # # For the time being, we will do something bad and directly call # # the base class method from where we would otherwise want to @@ -484,7 +489,7 @@ def __init__(self, *args, **kwargs): def _activate_without_unfixing_indicator(self): # Ideally, this would be a super call from this class. However, # doing that would trigger a call to deactivate() on all the - # _DisjunctData objects (exactly what we want to avoid!) + # DisjunctData objects (exactly what we want to avoid!) # # For the time being, we will do something bad and directly call # the base class method from where we would otherwise want to @@ -495,7 +500,7 @@ def _activate_without_unfixing_indicator(self): component_data._activate_without_unfixing_indicator() -class ScalarDisjunct(_DisjunctData, Disjunct): +class ScalarDisjunct(DisjunctData, Disjunct): def __init__(self, *args, **kwds): ## FIXME: This is a HACK to get around a chicken-and-egg issue ## where BlockData creates the indicator_var *before* @@ -503,7 +508,7 @@ def __init__(self, *args, **kwds): self._defer_construction = True self._suppress_ctypes = set() - _DisjunctData.__init__(self, self) + DisjunctData.__init__(self, self) Disjunct.__init__(self, *args, **kwds) self._data[None] = self self._index = UnindexedComponent_index @@ -524,7 +529,7 @@ def active(self): return any(d.active for d in self._data.values()) -_DisjunctData._Block_reserved_words = set(dir(Disjunct())) +DisjunctData._Block_reserved_words = set(dir(Disjunct())) class _DisjunctionData(ActiveComponentData): diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 134a3d16d66..854366c0cf0 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -42,7 +42,7 @@ Binary, ) from pyomo.gdp import Disjunct, Disjunction, GDP_Error -from pyomo.gdp.disjunct import _DisjunctData +from pyomo.gdp.disjunct import DisjunctData from pyomo.gdp.plugins.gdp_to_mip_transformation import GDP_to_MIP_Transformation from pyomo.gdp.transformed_disjunct import _TransformedDisjunct from pyomo.gdp.util import ( diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index bf0239d15e0..d5dcef3ba58 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -2196,7 +2196,7 @@ def test_do_not_assume_nested_indicators_local(self): class IndexedDisjunction(unittest.TestCase): # this tests that if the targets are a subset of the - # _DisjunctDatas in an IndexedDisjunction that the xor constraint + # DisjunctDatas in an IndexedDisjunction that the xor constraint # created on the parent block will still be indexed as expected. def test_xor_constraint(self): ct.check_indexed_xor_constraints_with_targets(self, 'bigm') diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index 2164671ea16..686253b0179 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.gdp import GDP_Error, Disjunction -from pyomo.gdp.disjunct import _DisjunctData, Disjunct +from pyomo.gdp.disjunct import DisjunctData, Disjunct import pyomo.core.expr as EXPR from pyomo.core.base.component import _ComponentBase @@ -493,7 +493,7 @@ def get_src_constraint(transformedConstraint): def _find_parent_disjunct(constraint): # traverse up until we find the disjunct this constraint lives on parent_disjunct = constraint.parent_block() - while not isinstance(parent_disjunct, _DisjunctData): + while not isinstance(parent_disjunct, DisjunctData): if parent_disjunct is None: raise GDP_Error( "Constraint '%s' is not on a disjunct and so was not " From 47a7e26da00a1520e5903e16baeb6ce076b155c6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:37:32 -0600 Subject: [PATCH 0986/3044] Renamed _DisjunctionData -> DisjunctionData --- pyomo/core/base/component.py | 2 +- pyomo/gdp/disjunct.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 7d6fc903632..dcaf976356b 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -805,7 +805,7 @@ class ComponentData(_ComponentBase): # classes: BooleanVarData, ConnectorData, ConstraintData, # _GeneralExpressionData, _LogicalConstraintData, # _GeneralLogicalConstraintData, _GeneralObjectiveData, - # _ParamData,_GeneralVarData, _GeneralBooleanVarData, _DisjunctionData, + # _ParamData,_GeneralVarData, _GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! diff --git a/pyomo/gdp/disjunct.py b/pyomo/gdp/disjunct.py index dd9d2b4638c..658ead27783 100644 --- a/pyomo/gdp/disjunct.py +++ b/pyomo/gdp/disjunct.py @@ -532,7 +532,7 @@ def active(self): DisjunctData._Block_reserved_words = set(dir(Disjunct())) -class _DisjunctionData(ActiveComponentData): +class DisjunctionData(ActiveComponentData): __slots__ = ('disjuncts', 'xor', '_algebraic_constraint', '_transformation_map') __autoslot_mappers__ = {'_algebraic_constraint': AutoSlots.weakref_mapper} _NoArgument = (0,) @@ -625,9 +625,14 @@ def set_value(self, expr): self.disjuncts.append(disjunct) +class _DisjunctionData(metaclass=RenamedClass): + __renamed__new_class__ = DisjunctionData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("Disjunction expressions.") class Disjunction(ActiveIndexedComponent): - _ComponentDataClass = _DisjunctionData + _ComponentDataClass = DisjunctionData def __new__(cls, *args, **kwds): if cls != Disjunction: @@ -768,9 +773,9 @@ def _pprint(self): ) -class ScalarDisjunction(_DisjunctionData, Disjunction): +class ScalarDisjunction(DisjunctionData, Disjunction): def __init__(self, *args, **kwds): - _DisjunctionData.__init__(self, component=self) + DisjunctionData.__init__(self, component=self) Disjunction.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -781,7 +786,7 @@ def __init__(self, *args, **kwds): # currently in place). So during initialization only, we will # treat them as "indexed" objects where things like # Constraint.Skip are managed. But after that they will behave - # like _DisjunctionData objects where set_value does not handle + # like DisjunctionData objects where set_value does not handle # Disjunction.Skip but expects a valid expression or None. # From 4320bc1c068ea2b812f22e149378fe9fc7770291 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:38:42 -0600 Subject: [PATCH 0987/3044] Renamed _ExpressionData -> ExpressionData --- pyomo/contrib/mcpp/pyomo_mcpp.py | 4 ++-- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/expression.py | 11 ++++++++--- pyomo/core/base/objective.py | 4 ++-- pyomo/core/beta/dict_objects.py | 4 ++-- pyomo/core/beta/list_objects.py | 4 ++-- pyomo/gdp/tests/test_util.py | 6 +++--- pyomo/repn/plugins/ampl/ampl_.py | 4 ++-- pyomo/repn/standard_repn.py | 6 +++--- pyomo/repn/util.py | 4 ++-- 10 files changed, 27 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/mcpp/pyomo_mcpp.py b/pyomo/contrib/mcpp/pyomo_mcpp.py index 35e883f98da..1375ae61c50 100644 --- a/pyomo/contrib/mcpp/pyomo_mcpp.py +++ b/pyomo/contrib/mcpp/pyomo_mcpp.py @@ -20,7 +20,7 @@ from pyomo.common.fileutils import Library from pyomo.core import value, Expression from pyomo.core.base.block import SubclassOf -from pyomo.core.base.expression import _ExpressionData +from pyomo.core.base.expression import ExpressionData from pyomo.core.expr.numvalue import nonpyomo_leaf_types from pyomo.core.expr.numeric_expr import ( AbsExpression, @@ -307,7 +307,7 @@ def exitNode(self, node, data): ans = self.mcpp.newConstant(node) elif not node.is_expression_type(): ans = self.register_num(node) - elif type(node) in SubclassOf(Expression) or isinstance(node, _ExpressionData): + elif type(node) in SubclassOf(Expression) or isinstance(node, ExpressionData): ans = data[0] else: raise RuntimeError("Unhandled expression type: %s" % (type(node))) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 9a5337ac2c8..d875065d502 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -36,7 +36,7 @@ from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base.config import PyomoOptions -from pyomo.core.base.expression import Expression, _ExpressionData +from pyomo.core.base.expression import Expression, ExpressionData from pyomo.core.base.label import ( CuidLabeler, CounterLabeler, diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index 3ce998b62a4..e21613fcbb1 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -36,7 +36,7 @@ logger = logging.getLogger('pyomo.core') -class _ExpressionData(numeric_expr.NumericValue): +class ExpressionData(numeric_expr.NumericValue): """ An object that defines a named expression. @@ -137,13 +137,18 @@ def is_fixed(self): """A boolean indicating whether this expression is fixed.""" raise NotImplementedError - # _ExpressionData should never return False because + # ExpressionData should never return False because # they can store subexpressions that contain variables def is_potentially_variable(self): return True -class _GeneralExpressionDataImpl(_ExpressionData): +class _ExpressionData(metaclass=RenamedClass): + __renamed__new_class__ = ExpressionData + __renamed__version__ = '6.7.2.dev0' + + +class _GeneralExpressionDataImpl(ExpressionData): """ An object that defines an expression that is never cloned diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index fcc63755f2b..d259358dcd7 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -28,7 +28,7 @@ UnindexedComponent_set, rule_wrapper, ) -from pyomo.core.base.expression import _ExpressionData, _GeneralExpressionDataImpl +from pyomo.core.base.expression import ExpressionData, _GeneralExpressionDataImpl from pyomo.core.base.set import Set from pyomo.core.base.initializer import ( Initializer, @@ -86,7 +86,7 @@ def O_rule(model, i, j): # -class _ObjectiveData(_ExpressionData): +class _ObjectiveData(ExpressionData): """ This class defines the data for a single objective. diff --git a/pyomo/core/beta/dict_objects.py b/pyomo/core/beta/dict_objects.py index 53d39939db2..2b23d81e91a 100644 --- a/pyomo/core/beta/dict_objects.py +++ b/pyomo/core/beta/dict_objects.py @@ -17,7 +17,7 @@ from pyomo.core.base.var import IndexedVar, _VarData from pyomo.core.base.constraint import IndexedConstraint, ConstraintData from pyomo.core.base.objective import IndexedObjective, _ObjectiveData -from pyomo.core.base.expression import IndexedExpression, _ExpressionData +from pyomo.core.base.expression import IndexedExpression, ExpressionData from collections.abc import MutableMapping from collections.abc import Mapping @@ -211,4 +211,4 @@ def __init__(self, *args, **kwds): # Constructor for ComponentDict needs to # go last in order to handle any initialization # iterable as an argument - ComponentDict.__init__(self, _ExpressionData, *args, **kwds) + ComponentDict.__init__(self, ExpressionData, *args, **kwds) diff --git a/pyomo/core/beta/list_objects.py b/pyomo/core/beta/list_objects.py index e8b40e6da53..dd199eb70cd 100644 --- a/pyomo/core/beta/list_objects.py +++ b/pyomo/core/beta/list_objects.py @@ -17,7 +17,7 @@ from pyomo.core.base.var import IndexedVar, _VarData from pyomo.core.base.constraint import IndexedConstraint, ConstraintData from pyomo.core.base.objective import IndexedObjective, _ObjectiveData -from pyomo.core.base.expression import IndexedExpression, _ExpressionData +from pyomo.core.base.expression import IndexedExpression, ExpressionData from collections.abc import MutableSequence @@ -259,4 +259,4 @@ def __init__(self, *args, **kwds): # Constructor for ComponentList needs to # go last in order to handle any initialization # iterable as an argument - ComponentList.__init__(self, _ExpressionData, *args, **kwds) + ComponentList.__init__(self, ExpressionData, *args, **kwds) diff --git a/pyomo/gdp/tests/test_util.py b/pyomo/gdp/tests/test_util.py index fd555fc2f59..8ea72af37da 100644 --- a/pyomo/gdp/tests/test_util.py +++ b/pyomo/gdp/tests/test_util.py @@ -13,7 +13,7 @@ from pyomo.core import ConcreteModel, Var, Expression, Block, RangeSet, Any import pyomo.core.expr as EXPR -from pyomo.core.base.expression import _ExpressionData +from pyomo.core.base.expression import ExpressionData from pyomo.gdp.util import ( clone_without_expression_components, is_child_of, @@ -40,7 +40,7 @@ def test_clone_without_expression_components(self): test = clone_without_expression_components(base, {}) self.assertIsNot(base, test) self.assertEqual(base(), test()) - self.assertIsInstance(base, _ExpressionData) + self.assertIsInstance(base, ExpressionData) self.assertIsInstance(test, EXPR.SumExpression) test = clone_without_expression_components(base, {id(m.x): m.y}) self.assertEqual(3**2 + 3 - 1, test()) @@ -51,7 +51,7 @@ def test_clone_without_expression_components(self): self.assertEqual(base(), test()) self.assertIsInstance(base, EXPR.SumExpression) self.assertIsInstance(test, EXPR.SumExpression) - self.assertIsInstance(base.arg(0), _ExpressionData) + self.assertIsInstance(base.arg(0), ExpressionData) self.assertIsInstance(test.arg(0), EXPR.SumExpression) test = clone_without_expression_components(base, {id(m.x): m.y}) self.assertEqual(3**2 + 3 - 1 + 3, test()) diff --git a/pyomo/repn/plugins/ampl/ampl_.py b/pyomo/repn/plugins/ampl/ampl_.py index f422a085a3c..c6357cbecd9 100644 --- a/pyomo/repn/plugins/ampl/ampl_.py +++ b/pyomo/repn/plugins/ampl/ampl_.py @@ -33,7 +33,7 @@ from pyomo.core.base import ( SymbolMap, NameLabeler, - _ExpressionData, + ExpressionData, SortComponents, var, param, @@ -724,7 +724,7 @@ def _print_nonlinear_terms_NL(self, exp): self._print_nonlinear_terms_NL(exp.arg(0)) self._print_nonlinear_terms_NL(exp.arg(1)) - elif isinstance(exp, (_ExpressionData, IIdentityExpression)): + elif isinstance(exp, (ExpressionData, IIdentityExpression)): self._print_nonlinear_terms_NL(exp.expr) else: diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 455e7bd9444..70368dd3d7e 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -20,7 +20,7 @@ import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import NumericConstant from pyomo.core.base.objective import _GeneralObjectiveData, ScalarObjective -from pyomo.core.base import _ExpressionData, Expression +from pyomo.core.base import ExpressionData, Expression from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData from pyomo.core.base.var import ScalarVar, Var, _GeneralVarData, value from pyomo.core.base.param import ScalarParam, _ParamData @@ -1152,7 +1152,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra ScalarExpression: _collect_identity, expression: _collect_identity, noclone: _collect_identity, - _ExpressionData: _collect_identity, + ExpressionData: _collect_identity, Expression: _collect_identity, _GeneralObjectiveData: _collect_identity, ScalarObjective: _collect_identity, @@ -1551,7 +1551,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): ScalarExpression : _linear_collect_identity, expression : _linear_collect_identity, noclone : _linear_collect_identity, - _ExpressionData : _linear_collect_identity, + ExpressionData : _linear_collect_identity, Expression : _linear_collect_identity, _GeneralObjectiveData : _linear_collect_identity, ScalarObjective : _linear_collect_identity, diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index b4a21a2108f..7351ea51c58 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -40,7 +40,7 @@ SortComponents, ) from pyomo.core.base.component import ActiveComponent -from pyomo.core.base.expression import _ExpressionData +from pyomo.core.base.expression import ExpressionData from pyomo.core.expr.numvalue import is_fixed, value import pyomo.core.expr as EXPR import pyomo.core.kernel as kernel @@ -55,7 +55,7 @@ EXPR.NPV_SumExpression, } _named_subexpression_types = ( - _ExpressionData, + ExpressionData, kernel.expression.expression, kernel.objective.objective, ) From 86192304769e54cfcd3900e7878d71a767b5446a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:40:16 -0600 Subject: [PATCH 0988/3044] Renamed _FiniteRangeSetData -> FiniteRangeSetData --- pyomo/core/base/set.py | 17 +++++++++++------ pyomo/core/tests/unit/test_set.py | 6 +++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index b3277ab3260..319f3b0e5ae 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -2648,7 +2648,7 @@ def ranges(self): return iter(self._ranges) -class _FiniteRangeSetData( +class FiniteRangeSetData( _SortedSetMixin, _OrderedSetMixin, _FiniteSetMixin, _InfiniteRangeSetData ): __slots__ = () @@ -2672,7 +2672,7 @@ def _iter_impl(self): # iterate over it nIters = len(self._ranges) - 1 if not nIters: - yield from _FiniteRangeSetData._range_gen(self._ranges[0]) + yield from FiniteRangeSetData._range_gen(self._ranges[0]) return # The trick here is that we need to remove any duplicates from @@ -2683,7 +2683,7 @@ def _iter_impl(self): for r in self._ranges: # Note: there should always be at least 1 member in each # NumericRange - i = _FiniteRangeSetData._range_gen(r) + i = FiniteRangeSetData._range_gen(r) iters.append([next(i), i]) iters.sort(reverse=True, key=lambda x: x[0]) @@ -2756,6 +2756,11 @@ def ord(self, item): domain = _InfiniteRangeSetData.domain +class _FiniteRangeSetData(metaclass=RenamedClass): + __renamed__new_class__ = FiniteRangeSetData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register( "A sequence of numeric values. RangeSet(start,end,step) is a sequence " "starting a value 'start', and increasing in values by 'step' until a " @@ -3120,7 +3125,7 @@ def construct(self, data=None): old_ranges.reverse() while old_ranges: r = old_ranges.pop() - for i, val in enumerate(_FiniteRangeSetData._range_gen(r)): + for i, val in enumerate(FiniteRangeSetData._range_gen(r)): if not _filter(_block, val): split_r = r.range_difference((NumericRange(val, val, 0),)) if len(split_r) == 2: @@ -3233,9 +3238,9 @@ class InfiniteSimpleRangeSet(metaclass=RenamedClass): __renamed__version__ = '6.0' -class FiniteScalarRangeSet(_ScalarOrderedSetMixin, _FiniteRangeSetData, RangeSet): +class FiniteScalarRangeSet(_ScalarOrderedSetMixin, FiniteRangeSetData, RangeSet): def __init__(self, *args, **kwds): - _FiniteRangeSetData.__init__(self, component=self) + FiniteRangeSetData.__init__(self, component=self) RangeSet.__init__(self, *args, **kwds) self._index = UnindexedComponent_index diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 4bbac6ecaa0..3639aa82c73 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -60,7 +60,7 @@ FiniteSetOf, InfiniteSetOf, RangeSet, - _FiniteRangeSetData, + FiniteRangeSetData, _InfiniteRangeSetData, FiniteScalarRangeSet, InfiniteScalarRangeSet, @@ -1285,13 +1285,13 @@ def test_is_functions(self): self.assertTrue(i.isdiscrete()) self.assertTrue(i.isfinite()) self.assertTrue(i.isordered()) - self.assertIsInstance(i, _FiniteRangeSetData) + self.assertIsInstance(i, FiniteRangeSetData) i = RangeSet(1, 3) self.assertTrue(i.isdiscrete()) self.assertTrue(i.isfinite()) self.assertTrue(i.isordered()) - self.assertIsInstance(i, _FiniteRangeSetData) + self.assertIsInstance(i, FiniteRangeSetData) i = RangeSet(1, 3, 0) self.assertFalse(i.isdiscrete()) From 4ea182da5dab8f6994cbf7bc17a0cd1fc1b2f1ec Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:40:42 -0600 Subject: [PATCH 0989/3044] Renamed _FiniteSetData -> FiniteSetData --- pyomo/core/base/set.py | 17 +++++++++++------ pyomo/core/tests/unit/test_set.py | 8 ++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 319f3b0e5ae..8db64620d5c 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1294,7 +1294,7 @@ def ranges(self): yield NonNumericRange(i) -class _FiniteSetData(_FiniteSetMixin, _SetData): +class FiniteSetData(_FiniteSetMixin, _SetData): """A general unordered iterable Set""" __slots__ = ('_values', '_domain', '_validate', '_filter', '_dimen') @@ -1470,6 +1470,11 @@ def pop(self): return self._values.pop() +class _FiniteSetData(metaclass=RenamedClass): + __renamed__new_class__ = FiniteSetData + __renamed__version__ = '6.7.2.dev0' + + class _ScalarOrderedSetMixin(object): # This mixin is required because scalar ordered sets implement # __getitem__() as an alias of at() @@ -1630,7 +1635,7 @@ def _to_0_based_index(self, item): ) -class _OrderedSetData(_OrderedSetMixin, _FiniteSetData): +class _OrderedSetData(_OrderedSetMixin, FiniteSetData): """ This class defines the base class for an ordered set of concrete data. @@ -1652,7 +1657,7 @@ class _OrderedSetData(_OrderedSetMixin, _FiniteSetData): def __init__(self, component): self._values = {} self._ordered_values = [] - _FiniteSetData.__init__(self, component=component) + FiniteSetData.__init__(self, component=component) def _iter_impl(self): """ @@ -2034,7 +2039,7 @@ def __new__(cls, *args, **kwds): elif ordered is Set.SortedOrder: newObj._ComponentDataClass = _SortedSetData else: - newObj._ComponentDataClass = _FiniteSetData + newObj._ComponentDataClass = FiniteSetData return newObj @overload @@ -2388,9 +2393,9 @@ def __getitem__(self, index) -> _SetData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore -class FiniteScalarSet(_FiniteSetData, Set): +class FiniteScalarSet(FiniteSetData, Set): def __init__(self, **kwds): - _FiniteSetData.__init__(self, component=self) + FiniteSetData.__init__(self, component=self) Set.__init__(self, **kwds) self._index = UnindexedComponent_index diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 3639aa82c73..a9b9fb9469b 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -82,7 +82,7 @@ SetProduct_FiniteSet, SetProduct_OrderedSet, _SetData, - _FiniteSetData, + FiniteSetData, _InsertionOrderSetData, _SortedSetData, _FiniteSetMixin, @@ -4137,9 +4137,9 @@ def test_indexed_set(self): self.assertFalse(m.I[1].isordered()) self.assertFalse(m.I[2].isordered()) self.assertFalse(m.I[3].isordered()) - self.assertIs(type(m.I[1]), _FiniteSetData) - self.assertIs(type(m.I[2]), _FiniteSetData) - self.assertIs(type(m.I[3]), _FiniteSetData) + self.assertIs(type(m.I[1]), FiniteSetData) + self.assertIs(type(m.I[2]), FiniteSetData) + self.assertIs(type(m.I[3]), FiniteSetData) self.assertEqual(m.I.data(), {1: (1,), 2: (2,), 3: (4,)}) # Explicit (constant) construction From 3a86baa8bcb6152edca7c4e687488e6c5de08137 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:41:02 -0600 Subject: [PATCH 0990/3044] Renamed _GeneralBooleanVarData -> GeneralBooleanVarData --- pyomo/contrib/cp/repn/docplex_writer.py | 4 +-- .../logical_to_disjunctive_walker.py | 2 +- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/boolean_var.py | 27 +++++++++++-------- pyomo/core/base/component.py | 2 +- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 8356a1e752f..510bbf4e398 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -60,7 +60,7 @@ ) from pyomo.core.base.boolean_var import ( ScalarBooleanVar, - _GeneralBooleanVarData, + GeneralBooleanVarData, IndexedBooleanVar, ) from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData @@ -964,7 +964,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): _GeneralVarData: _before_var, IndexedVar: _before_indexed_var, ScalarBooleanVar: _before_boolean_var, - _GeneralBooleanVarData: _before_boolean_var, + GeneralBooleanVarData: _before_boolean_var, IndexedBooleanVar: _before_indexed_boolean_var, _GeneralExpressionData: _before_named_expression, ScalarExpression: _before_named_expression, diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index d5f13e91535..548078f55f8 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -209,7 +209,7 @@ def _dispatch_atmost(visitor, node, *args): _before_child_dispatcher = {} _before_child_dispatcher[BV.ScalarBooleanVar] = _dispatch_boolean_var -_before_child_dispatcher[BV._GeneralBooleanVarData] = _dispatch_boolean_var +_before_child_dispatcher[BV.GeneralBooleanVarData] = _dispatch_boolean_var _before_child_dispatcher[AutoLinkedBooleanVar] = _dispatch_boolean_var _before_child_dispatcher[_ParamData] = _dispatch_param _before_child_dispatcher[ScalarParam] = _dispatch_param diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index d875065d502..bb62cb96782 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -61,7 +61,7 @@ from pyomo.core.base.boolean_var import ( BooleanVar, BooleanVarData, - _GeneralBooleanVarData, + GeneralBooleanVarData, BooleanVarList, ScalarBooleanVar, ) diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index bf9d6159754..287851a7f7e 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -194,7 +194,7 @@ def _associated_binary_mapper(encode, val): return val -class _GeneralBooleanVarData(BooleanVarData): +class GeneralBooleanVarData(BooleanVarData): """ This class defines the data for a single Boolean variable. @@ -271,13 +271,13 @@ def stale(self, val): def get_associated_binary(self): """Get the binary _VarData associated with this - _GeneralBooleanVarData""" + GeneralBooleanVarData""" return ( self._associated_binary() if self._associated_binary is not None else None ) def associate_binary_var(self, binary_var): - """Associate a binary _VarData to this _GeneralBooleanVarData""" + """Associate a binary _VarData to this GeneralBooleanVarData""" if ( self._associated_binary is not None and type(self._associated_binary) @@ -300,6 +300,11 @@ def associate_binary_var(self, binary_var): self._associated_binary = weakref_ref(binary_var) +class _GeneralBooleanVarData(metaclass=RenamedClass): + __renamed__new_class__ = GeneralBooleanVarData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("Logical decision variables.") class BooleanVar(IndexedComponent): """A logical variable, which may be defined over an index. @@ -314,7 +319,7 @@ class BooleanVar(IndexedComponent): to True. """ - _ComponentDataClass = _GeneralBooleanVarData + _ComponentDataClass = GeneralBooleanVarData def __new__(cls, *args, **kwds): if cls != BooleanVar: @@ -506,11 +511,11 @@ def _pprint(self): ) -class ScalarBooleanVar(_GeneralBooleanVarData, BooleanVar): +class ScalarBooleanVar(GeneralBooleanVarData, BooleanVar): """A single variable.""" def __init__(self, *args, **kwd): - _GeneralBooleanVarData.__init__(self, component=self) + GeneralBooleanVarData.__init__(self, component=self) BooleanVar.__init__(self, *args, **kwd) self._index = UnindexedComponent_index @@ -526,7 +531,7 @@ def __init__(self, *args, **kwd): def value(self): """Return the value for this variable.""" if self._constructed: - return _GeneralBooleanVarData.value.fget(self) + return GeneralBooleanVarData.value.fget(self) raise ValueError( "Accessing the value of variable '%s' " "before the Var has been constructed (there " @@ -537,7 +542,7 @@ def value(self): def value(self, val): """Set the value for this variable.""" if self._constructed: - return _GeneralBooleanVarData.value.fset(self, val) + return GeneralBooleanVarData.value.fset(self, val) raise ValueError( "Setting the value of variable '%s' " "before the Var has been constructed (there " @@ -546,7 +551,7 @@ def value(self, val): @property def domain(self): - return _GeneralBooleanVarData.domain.fget(self) + return GeneralBooleanVarData.domain.fget(self) def fix(self, value=NOTSET, skip_validation=False): """ @@ -554,7 +559,7 @@ def fix(self, value=NOTSET, skip_validation=False): indicating the variable should be fixed at its current value. """ if self._constructed: - return _GeneralBooleanVarData.fix(self, value, skip_validation) + return GeneralBooleanVarData.fix(self, value, skip_validation) raise ValueError( "Fixing variable '%s' " "before the Var has been constructed (there " @@ -564,7 +569,7 @@ def fix(self, value=NOTSET, skip_validation=False): def unfix(self): """Sets the fixed indicator to False.""" if self._constructed: - return _GeneralBooleanVarData.unfix(self) + return GeneralBooleanVarData.unfix(self) raise ValueError( "Freeing variable '%s' " "before the Var has been constructed (there " diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index dcaf976356b..1317c928686 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -805,7 +805,7 @@ class ComponentData(_ComponentBase): # classes: BooleanVarData, ConnectorData, ConstraintData, # _GeneralExpressionData, _LogicalConstraintData, # _GeneralLogicalConstraintData, _GeneralObjectiveData, - # _ParamData,_GeneralVarData, _GeneralBooleanVarData, DisjunctionData, + # _ParamData,_GeneralVarData, GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! From 80c064ac99ac5870cc1439f4cd3405f7565a3516 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:42:40 -0600 Subject: [PATCH 0991/3044] Renamed _GeneralConstraintData -> GeneralConstraintData --- pyomo/contrib/appsi/base.py | 48 +++++++++---------- pyomo/contrib/appsi/fbbt.py | 6 +-- pyomo/contrib/appsi/solvers/cbc.py | 6 +-- pyomo/contrib/appsi/solvers/cplex.py | 10 ++-- pyomo/contrib/appsi/solvers/gurobi.py | 16 +++---- pyomo/contrib/appsi/solvers/highs.py | 6 +-- pyomo/contrib/appsi/solvers/ipopt.py | 10 ++-- pyomo/contrib/appsi/solvers/wntr.py | 6 +-- pyomo/contrib/appsi/writers/lp_writer.py | 6 +-- pyomo/contrib/appsi/writers/nl_writer.py | 6 +-- pyomo/contrib/solver/base.py | 10 ++-- pyomo/contrib/solver/gurobi.py | 16 +++---- pyomo/contrib/solver/persistent.py | 12 ++--- pyomo/contrib/solver/solution.py | 14 +++--- .../contrib/solver/tests/unit/test_results.py | 6 +-- pyomo/core/base/constraint.py | 27 ++++++----- pyomo/core/tests/unit/test_con.py | 4 +- pyomo/core/tests/unit/test_dict_objects.py | 4 +- pyomo/core/tests/unit/test_list_objects.py | 4 +- pyomo/gdp/tests/common_tests.py | 4 +- pyomo/gdp/tests/test_bigm.py | 4 +- pyomo/gdp/tests/test_hull.py | 4 +- .../plugins/solvers/gurobi_persistent.py | 10 ++-- 23 files changed, 121 insertions(+), 118 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index b4ade16a597..d5982fc72e6 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -21,7 +21,7 @@ Tuple, MutableMapping, ) -from pyomo.core.base.constraint import _GeneralConstraintData, Constraint +from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint from pyomo.core.base.var import _GeneralVarData, Var from pyomo.core.base.param import _ParamData, Param @@ -216,8 +216,8 @@ def get_primals( pass def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: """ Returns a dictionary mapping constraint to dual value. @@ -235,8 +235,8 @@ def get_duals( raise NotImplementedError(f'{type(self)} does not support the get_duals method') def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: """ Returns a dictionary mapping constraint to slack. @@ -319,8 +319,8 @@ def get_primals( return primals def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: if self._duals is None: raise RuntimeError( 'Solution loader does not currently have valid duals. Please ' @@ -336,8 +336,8 @@ def get_duals( return duals def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: if self._slacks is None: raise RuntimeError( 'Solution loader does not currently have valid slacks. Please ' @@ -731,8 +731,8 @@ def get_primals( pass def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: """ Declare sign convention in docstring here. @@ -752,8 +752,8 @@ def get_duals( ) def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: """ Parameters ---------- @@ -807,7 +807,7 @@ def add_params(self, params: List[_ParamData]): pass @abc.abstractmethod - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[GeneralConstraintData]): pass @abc.abstractmethod @@ -823,7 +823,7 @@ def remove_params(self, params: List[_ParamData]): pass @abc.abstractmethod - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[GeneralConstraintData]): pass @abc.abstractmethod @@ -857,14 +857,14 @@ def get_primals(self, vars_to_load=None): return self._solver.get_primals(vars_to_load=vars_to_load) def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: self._assert_solution_still_valid() return self._solver.get_duals(cons_to_load=cons_to_load) def get_slacks( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: self._assert_solution_still_valid() return self._solver.get_slacks(cons_to_load=cons_to_load) @@ -984,7 +984,7 @@ def add_params(self, params: List[_ParamData]): self._add_params(params) @abc.abstractmethod - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): pass def _check_for_new_vars(self, variables: List[_GeneralVarData]): @@ -1004,7 +1004,7 @@ def _check_to_remove_vars(self, variables: List[_GeneralVarData]): vars_to_remove[v_id] = v self.remove_variables(list(vars_to_remove.values())) - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[GeneralConstraintData]): all_fixed_vars = dict() for con in cons: if con in self._named_expressions: @@ -1132,10 +1132,10 @@ def add_block(self, block): self.set_objective(obj) @abc.abstractmethod - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): pass - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[GeneralConstraintData]): self._remove_constraints(cons) for con in cons: if con not in self._named_expressions: @@ -1334,7 +1334,7 @@ def update(self, timer: HierarchicalTimer = None): for c in self._vars_referenced_by_con.keys(): if c not in current_cons_dict and c not in current_sos_dict: if (c.ctype is Constraint) or ( - c.ctype is None and isinstance(c, _GeneralConstraintData) + c.ctype is None and isinstance(c, GeneralConstraintData) ): old_cons.append(c) else: diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index c6bbdb5bf3b..121557414b3 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -20,7 +20,7 @@ from typing import List, Optional from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.objective import _GeneralObjectiveData, minimize, maximize from pyomo.core.base.block import BlockData @@ -154,7 +154,7 @@ def _add_params(self, params: List[_ParamData]): cp = cparams[ndx] cp.name = self._symbol_map.getSymbol(p, self._param_labeler) - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): cmodel.process_fbbt_constraints( self._cmodel, self._pyomo_expr_types, @@ -175,7 +175,7 @@ def _add_sos_constraints(self, cons: List[_SOSConstraintData]): 'IntervalTightener does not support SOS constraints' ) - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): if self._symbolic_solver_labels: for c in cons: self._symbol_map.removeSymbol(c) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 57bbf1b4c21..cc7327df11c 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -27,7 +27,7 @@ from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData @@ -170,7 +170,7 @@ def add_variables(self, variables: List[_GeneralVarData]): def add_params(self, params: List[_ParamData]): self._writer.add_params(params) - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[GeneralConstraintData]): self._writer.add_constraints(cons) def add_block(self, block: BlockData): @@ -182,7 +182,7 @@ def remove_variables(self, variables: List[_GeneralVarData]): def remove_params(self, params: List[_ParamData]): self._writer.remove_params(params) - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[GeneralConstraintData]): self._writer.remove_constraints(cons) def remove_block(self, block: BlockData): diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 2e04a979fda..222c466fb99 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -23,7 +23,7 @@ from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping, Dict from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData @@ -185,7 +185,7 @@ def add_variables(self, variables: List[_GeneralVarData]): def add_params(self, params: List[_ParamData]): self._writer.add_params(params) - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[GeneralConstraintData]): self._writer.add_constraints(cons) def add_block(self, block: BlockData): @@ -197,7 +197,7 @@ def remove_variables(self, variables: List[_GeneralVarData]): def remove_params(self, params: List[_ParamData]): self._writer.remove_params(params) - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[GeneralConstraintData]): self._writer.remove_constraints(cons) def remove_block(self, block: BlockData): @@ -389,8 +389,8 @@ def get_primals( return res def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: if ( self._cplex_model.solution.get_solution_type() == self._cplex_model.solution.type.none diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 1e18862e3bd..e20168034c6 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -24,7 +24,7 @@ from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.core.base.var import Var, _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types @@ -579,7 +579,7 @@ def _get_expr_from_pyomo_expr(self, expr): mutable_quadratic_coefficients, ) - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): for con in cons: conname = self._symbol_map.getSymbol(con, self._labeler) ( @@ -735,7 +735,7 @@ def _add_sos_constraints(self, cons: List[_SOSConstraintData]): self._constraints_added_since_update.update(cons) self._needs_updated = True - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): for con in cons: if con in self._constraints_added_since_update: self._update_gurobi_model() @@ -1195,7 +1195,7 @@ def set_linear_constraint_attr(self, con, attr, val): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be modified. attr: str @@ -1272,7 +1272,7 @@ def get_linear_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1304,7 +1304,7 @@ def get_quadratic_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1425,7 +1425,7 @@ def cbCut(self, con): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The cut to add """ if not con.active: @@ -1510,7 +1510,7 @@ def cbLazy(self, con): """ Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The lazy constraint to add """ if not con.active: diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 3612b9d5014..7773d0624b2 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -21,7 +21,7 @@ from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData from pyomo.core.expr.numvalue import value, is_constant @@ -376,7 +376,7 @@ def set_instance(self, model): if self._objective is None: self.set_objective(None) - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -462,7 +462,7 @@ def _add_sos_constraints(self, cons: List[_SOSConstraintData]): 'Highs interface does not support SOS constraints' ) - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 19ec5f8031c..75ebb10f719 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -29,7 +29,7 @@ from pyomo.core.expr.visitor import replace_expressions from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData from pyomo.core.base.objective import _GeneralObjectiveData @@ -234,7 +234,7 @@ def add_variables(self, variables: List[_GeneralVarData]): def add_params(self, params: List[_ParamData]): self._writer.add_params(params) - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[GeneralConstraintData]): self._writer.add_constraints(cons) def add_block(self, block: BlockData): @@ -246,7 +246,7 @@ def remove_variables(self, variables: List[_GeneralVarData]): def remove_params(self, params: List[_ParamData]): self._writer.remove_params(params) - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[GeneralConstraintData]): self._writer.remove_constraints(cons) def remove_block(self, block: BlockData): @@ -534,9 +534,7 @@ def get_primals( res[v] = self._primal_sol[v] return res - def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ): + def get_duals(self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None): if ( self._last_results_object is None or self._last_results_object.termination_condition diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 928eda2b514..c11536e2e6f 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -42,7 +42,7 @@ from pyomo.core.base.block import BlockData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.common.timing import HierarchicalTimer from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.dependencies import attempt_import @@ -278,7 +278,7 @@ def _add_params(self, params: List[_ParamData]): setattr(self._solver_model, pname, wntr_p) self._pyomo_param_to_solver_param_map[id(p)] = wntr_p - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): aml = wntr.sim.aml.aml for con in cons: if not con.equality: @@ -294,7 +294,7 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): self._pyomo_con_to_solver_con_map[con] = wntr_con self._needs_updated = True - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): for con in cons: solver_con = self._pyomo_con_to_solver_con_map[con] delattr(self._solver_model, solver_con.name) diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 518be5fac99..39298bd1a61 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -12,7 +12,7 @@ from typing import List from pyomo.core.base.param import _ParamData from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import BlockData @@ -99,14 +99,14 @@ def _add_params(self, params: List[_ParamData]): cp.value = p.value self._pyomo_param_to_solver_param_map[id(p)] = cp - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): cmodel.process_lp_constraints(cons, self) def _add_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('LP writer does not yet support SOS constraints') - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): for c in cons: cc = self._pyomo_con_to_solver_con_map.pop(c) self._writer.remove_constraint(cc) diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 75b026ab521..3e13ef4077a 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -12,7 +12,7 @@ from typing import List from pyomo.core.base.param import _ParamData from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import BlockData @@ -111,7 +111,7 @@ def _add_params(self, params: List[_ParamData]): cp = cparams[ndx] cp.name = self._symbol_map.getSymbol(p, self._param_labeler) - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): cmodel.process_nl_constraints( self._writer, self._expr_types, @@ -130,7 +130,7 @@ def _add_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('NL writer does not support SOS constraints') - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): if self.config.symbolic_solver_labels: for c in cons: self._symbol_map.removeSymbol(c) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 4b7da383a57..4b7d8f35ddc 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -14,7 +14,7 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple import os -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.block import BlockData @@ -232,8 +232,8 @@ def _get_primals( ) def _get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: """ Declare sign convention in docstring here. @@ -294,7 +294,7 @@ def add_parameters(self, params: List[_ParamData]): """ @abc.abstractmethod - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[GeneralConstraintData]): """ Add constraints to the model """ @@ -318,7 +318,7 @@ def remove_parameters(self, params: List[_ParamData]): """ @abc.abstractmethod - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[GeneralConstraintData]): """ Remove constraints from the model """ diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index d0ac0d80f45..cc95c0c5f0d 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -23,7 +23,7 @@ from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types @@ -555,7 +555,7 @@ def _get_expr_from_pyomo_expr(self, expr): mutable_quadratic_coefficients, ) - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): for con in cons: conname = self._symbol_map.getSymbol(con, self._labeler) ( @@ -711,7 +711,7 @@ def _add_sos_constraints(self, cons: List[_SOSConstraintData]): self._constraints_added_since_update.update(cons) self._needs_updated = True - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): for con in cons: if con in self._constraints_added_since_update: self._update_gurobi_model() @@ -1125,7 +1125,7 @@ def set_linear_constraint_attr(self, con, attr, val): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be modified. attr: str @@ -1202,7 +1202,7 @@ def get_linear_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1234,7 +1234,7 @@ def get_quadratic_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1355,7 +1355,7 @@ def cbCut(self, con): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The cut to add """ if not con.active: @@ -1440,7 +1440,7 @@ def cbLazy(self, con): """ Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The lazy constraint to add """ if not con.active: diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index 4b1a7c58dcd..9b63e05ce46 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -12,7 +12,7 @@ import abc from typing import List -from pyomo.core.base.constraint import _GeneralConstraintData, Constraint +from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData, Param @@ -84,7 +84,7 @@ def add_parameters(self, params: List[_ParamData]): self._add_parameters(params) @abc.abstractmethod - def _add_constraints(self, cons: List[_GeneralConstraintData]): + def _add_constraints(self, cons: List[GeneralConstraintData]): pass def _check_for_new_vars(self, variables: List[_GeneralVarData]): @@ -104,7 +104,7 @@ def _check_to_remove_vars(self, variables: List[_GeneralVarData]): vars_to_remove[v_id] = v self.remove_variables(list(vars_to_remove.values())) - def add_constraints(self, cons: List[_GeneralConstraintData]): + def add_constraints(self, cons: List[GeneralConstraintData]): all_fixed_vars = {} for con in cons: if con in self._named_expressions: @@ -209,10 +209,10 @@ def add_block(self, block): self.set_objective(obj) @abc.abstractmethod - def _remove_constraints(self, cons: List[_GeneralConstraintData]): + def _remove_constraints(self, cons: List[GeneralConstraintData]): pass - def remove_constraints(self, cons: List[_GeneralConstraintData]): + def remove_constraints(self, cons: List[GeneralConstraintData]): self._remove_constraints(cons) for con in cons: if con not in self._named_expressions: @@ -384,7 +384,7 @@ def update(self, timer: HierarchicalTimer = None): for c in self._vars_referenced_by_con.keys(): if c not in current_cons_dict and c not in current_sos_dict: if (c.ctype is Constraint) or ( - c.ctype is None and isinstance(c, _GeneralConstraintData) + c.ctype is None and isinstance(c, GeneralConstraintData) ): old_cons.append(c) else: diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 32e84d2abca..e8c4631e7fd 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -12,7 +12,7 @@ import abc from typing import Sequence, Dict, Optional, Mapping, NoReturn -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.core.expr import value from pyomo.common.collections import ComponentMap @@ -67,8 +67,8 @@ def get_primals( """ def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: """ Returns a dictionary mapping constraint to dual value. @@ -121,8 +121,8 @@ def get_primals(self, vars_to_load=None): return self._solver._get_primals(vars_to_load=vars_to_load) def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: self._assert_solution_still_valid() return self._solver._get_duals(cons_to_load=cons_to_load) @@ -205,8 +205,8 @@ def get_primals( return res def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 74404aaba4c..38d6a540836 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -15,7 +15,7 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.var import _GeneralVarData from pyomo.common.collections import ComponentMap from pyomo.contrib.solver import results @@ -67,8 +67,8 @@ def get_primals( return primals def get_duals( - self, cons_to_load: Optional[Sequence[_GeneralConstraintData]] = None - ) -> Dict[_GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None + ) -> Dict[GeneralConstraintData, float]: if self._duals is None: raise RuntimeError( 'Solution loader does not currently have valid duals. Please ' diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index cbae828a459..3455d2dde3c 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -285,7 +285,7 @@ class _ConstraintData(metaclass=RenamedClass): __renamed__version__ = '6.7.2.dev0' -class _GeneralConstraintData(ConstraintData): +class GeneralConstraintData(ConstraintData): """ This class defines the data for a single general constraint. @@ -684,6 +684,11 @@ def set_value(self, expr): ) +class _GeneralConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = GeneralConstraintData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("General constraint expressions.") class Constraint(ActiveIndexedComponent): """ @@ -726,7 +731,7 @@ class Constraint(ActiveIndexedComponent): The class type for the derived subclass """ - _ComponentDataClass = _GeneralConstraintData + _ComponentDataClass = GeneralConstraintData class Infeasible(object): pass @@ -884,14 +889,14 @@ def display(self, prefix="", ostream=None): ) -class ScalarConstraint(_GeneralConstraintData, Constraint): +class ScalarConstraint(GeneralConstraintData, Constraint): """ ScalarConstraint is the implementation representing a single, non-indexed constraint. """ def __init__(self, *args, **kwds): - _GeneralConstraintData.__init__(self, component=self, expr=None) + GeneralConstraintData.__init__(self, component=self, expr=None) Constraint.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -915,7 +920,7 @@ def body(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.body.fget(self) + return GeneralConstraintData.body.fget(self) @property def lower(self): @@ -927,7 +932,7 @@ def lower(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.lower.fget(self) + return GeneralConstraintData.lower.fget(self) @property def upper(self): @@ -939,7 +944,7 @@ def upper(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.upper.fget(self) + return GeneralConstraintData.upper.fget(self) @property def equality(self): @@ -951,7 +956,7 @@ def equality(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.equality.fget(self) + return GeneralConstraintData.equality.fget(self) @property def strict_lower(self): @@ -963,7 +968,7 @@ def strict_lower(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.strict_lower.fget(self) + return GeneralConstraintData.strict_lower.fget(self) @property def strict_upper(self): @@ -975,7 +980,7 @@ def strict_upper(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return _GeneralConstraintData.strict_upper.fget(self) + return GeneralConstraintData.strict_upper.fget(self) def clear(self): self._data = {} @@ -1040,7 +1045,7 @@ def add(self, index, expr): return self.__setitem__(index, expr) @overload - def __getitem__(self, index) -> _GeneralConstraintData: ... + def __getitem__(self, index) -> GeneralConstraintData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore diff --git a/pyomo/core/tests/unit/test_con.py b/pyomo/core/tests/unit/test_con.py index 2fa6c24de9c..26ccc7944a7 100644 --- a/pyomo/core/tests/unit/test_con.py +++ b/pyomo/core/tests/unit/test_con.py @@ -44,7 +44,7 @@ InequalityExpression, RangedExpression, ) -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData class TestConstraintCreation(unittest.TestCase): @@ -1074,7 +1074,7 @@ def test_setitem(self): m.c[2] = m.x**2 <= 4 self.assertEqual(len(m.c), 1) self.assertEqual(list(m.c.keys()), [2]) - self.assertIsInstance(m.c[2], _GeneralConstraintData) + self.assertIsInstance(m.c[2], GeneralConstraintData) self.assertEqual(m.c[2].upper, 4) m.c[3] = Constraint.Skip diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index 8260f1ae320..a13e0f25ac8 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -18,7 +18,7 @@ ExpressionDict, ) from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.expression import _GeneralExpressionData @@ -375,7 +375,7 @@ def setUp(self): class TestConstraintDict(_TestActiveComponentDictBase, unittest.TestCase): _ctype = ConstraintDict - _cdatatype = _GeneralConstraintData + _cdatatype = GeneralConstraintData def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index 3eb2e279964..e9c1cceb701 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -18,7 +18,7 @@ XExpressionList, ) from pyomo.core.base.var import _GeneralVarData -from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData from pyomo.core.base.expression import _GeneralExpressionData @@ -392,7 +392,7 @@ def setUp(self): class TestConstraintList(_TestActiveComponentListBase, unittest.TestCase): _ctype = XConstraintList - _cdatatype = _GeneralConstraintData + _cdatatype = GeneralConstraintData def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index e15a7c66d8a..233c3ca9c09 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -953,7 +953,7 @@ def check_disjunction_data_target(self, transformation): self.assertIsInstance(transBlock, Block) self.assertIsInstance(transBlock.component("disjunction_xor"), Constraint) self.assertIsInstance( - transBlock.disjunction_xor[2], constraint._GeneralConstraintData + transBlock.disjunction_xor[2], constraint.GeneralConstraintData ) self.assertIsInstance(transBlock.component("relaxedDisjuncts"), Block) self.assertEqual(len(transBlock.relaxedDisjuncts), 3) @@ -963,7 +963,7 @@ def check_disjunction_data_target(self, transformation): m, targets=[m.disjunction[1]] ) self.assertIsInstance( - m.disjunction[1].algebraic_constraint, constraint._GeneralConstraintData + m.disjunction[1].algebraic_constraint, constraint.GeneralConstraintData ) transBlock = m.component("_pyomo_gdp_%s_reformulation_4" % transformation) self.assertIsInstance(transBlock, Block) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index d5dcef3ba58..efef4c5fb1f 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1323,8 +1323,8 @@ def test_do_not_transform_deactivated_constraintDatas(self): self.assertEqual(len(cons_list), 2) lb = cons_list[0] ub = cons_list[1] - self.assertIsInstance(lb, constraint._GeneralConstraintData) - self.assertIsInstance(ub, constraint._GeneralConstraintData) + self.assertIsInstance(lb, constraint.GeneralConstraintData) + self.assertIsInstance(ub, constraint.GeneralConstraintData) def checkMs( self, m, disj1c1lb, disj1c1ub, disj1c2lb, disj1c2ub, disj2c1ub, disj2c2ub diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 55edf244731..6093e01dc25 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -1253,11 +1253,11 @@ def check_second_iteration(self, model): orig = model.component("_pyomo_gdp_hull_reformulation") self.assertIsInstance( model.disjunctionList[1].algebraic_constraint, - constraint._GeneralConstraintData, + constraint.GeneralConstraintData, ) self.assertIsInstance( model.disjunctionList[0].algebraic_constraint, - constraint._GeneralConstraintData, + constraint.GeneralConstraintData, ) self.assertFalse(model.disjunctionList[1].active) self.assertFalse(model.disjunctionList[0].active) diff --git a/pyomo/solvers/plugins/solvers/gurobi_persistent.py b/pyomo/solvers/plugins/solvers/gurobi_persistent.py index 4522a2151c3..101a5340ea9 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_persistent.py +++ b/pyomo/solvers/plugins/solvers/gurobi_persistent.py @@ -157,7 +157,7 @@ def set_linear_constraint_attr(self, con, attr, val): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be modified. attr: str @@ -384,7 +384,7 @@ def get_linear_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -431,7 +431,7 @@ def get_quadratic_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -569,7 +569,7 @@ def cbCut(self, con): Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The cut to add """ if not con.active: @@ -647,7 +647,7 @@ def cbLazy(self, con): """ Parameters ---------- - con: pyomo.core.base.constraint._GeneralConstraintData + con: pyomo.core.base.constraint.GeneralConstraintData The lazy constraint to add """ if not con.active: From da96ac3f59b5336be1d805cd8ad60e8b9707b8cd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:43:27 -0600 Subject: [PATCH 0992/3044] Renamed _GeneralExpressionData -> GeneralExpressionData --- pyomo/contrib/cp/repn/docplex_writer.py | 6 ++--- .../logical_to_disjunctive_walker.py | 4 +-- pyomo/contrib/fbbt/fbbt.py | 10 +++---- pyomo/contrib/latex_printer/latex_printer.py | 4 +-- pyomo/core/base/component.py | 2 +- pyomo/core/base/expression.py | 27 +++++++++++-------- pyomo/core/base/objective.py | 6 ++--- pyomo/core/tests/unit/test_dict_objects.py | 4 +-- pyomo/core/tests/unit/test_expression.py | 8 +++--- pyomo/core/tests/unit/test_list_objects.py | 4 +-- pyomo/dae/integral.py | 4 +-- pyomo/repn/plugins/nl_writer.py | 2 +- pyomo/repn/standard_repn.py | 6 ++--- 13 files changed, 46 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 510bbf4e398..00b187a585e 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -63,7 +63,7 @@ GeneralBooleanVarData, IndexedBooleanVar, ) -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.param import IndexedParam, ScalarParam, _ParamData from pyomo.core.base.var import ScalarVar, _GeneralVarData, IndexedVar import pyomo.core.expr as EXPR @@ -949,7 +949,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): BeforeExpression: _handle_before_expression_node, AtExpression: _handle_at_expression_node, AlwaysIn: _handle_always_in_node, - _GeneralExpressionData: _handle_named_expression_node, + GeneralExpressionData: _handle_named_expression_node, ScalarExpression: _handle_named_expression_node, } _var_handles = { @@ -966,7 +966,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): ScalarBooleanVar: _before_boolean_var, GeneralBooleanVarData: _before_boolean_var, IndexedBooleanVar: _before_indexed_boolean_var, - _GeneralExpressionData: _before_named_expression, + GeneralExpressionData: _before_named_expression, ScalarExpression: _before_named_expression, IndexedParam: _before_indexed_param, # Because of indirection ScalarParam: _before_param, diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index 548078f55f8..b4fb5e26900 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -27,7 +27,7 @@ value, ) import pyomo.core.base.boolean_var as BV -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.param import ScalarParam, _ParamData from pyomo.core.base.var import ScalarVar, _GeneralVarData from pyomo.gdp.disjunct import AutoLinkedBooleanVar, Disjunct, Disjunction @@ -217,7 +217,7 @@ def _dispatch_atmost(visitor, node, *args): # don't handle them: _before_child_dispatcher[ScalarVar] = _dispatch_var _before_child_dispatcher[_GeneralVarData] = _dispatch_var -_before_child_dispatcher[_GeneralExpressionData] = _dispatch_expression +_before_child_dispatcher[GeneralExpressionData] = _dispatch_expression _before_child_dispatcher[ScalarExpression] = _dispatch_expression diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index bde33b3caa0..86f94506841 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -26,7 +26,7 @@ from pyomo.core.base.constraint import Constraint from pyomo.core.base.var import Var from pyomo.gdp import Disjunct -from pyomo.core.base.expression import _GeneralExpressionData, ScalarExpression +from pyomo.core.base.expression import GeneralExpressionData, ScalarExpression import logging from pyomo.common.errors import InfeasibleConstraintException, PyomoException from pyomo.common.config import ( @@ -340,7 +340,7 @@ def _prop_bnds_leaf_to_root_GeneralExpression(visitor, node, expr): Parameters ---------- visitor: _FBBTVisitorLeafToRoot - node: pyomo.core.base.expression._GeneralExpressionData + node: pyomo.core.base.expression.GeneralExpressionData expr: GeneralExpression arg """ bnds_dict = visitor.bnds_dict @@ -366,7 +366,7 @@ def _prop_bnds_leaf_to_root_GeneralExpression(visitor, node, expr): numeric_expr.UnaryFunctionExpression: _prop_bnds_leaf_to_root_UnaryFunctionExpression, numeric_expr.LinearExpression: _prop_bnds_leaf_to_root_SumExpression, numeric_expr.AbsExpression: _prop_bnds_leaf_to_root_abs, - _GeneralExpressionData: _prop_bnds_leaf_to_root_GeneralExpression, + GeneralExpressionData: _prop_bnds_leaf_to_root_GeneralExpression, ScalarExpression: _prop_bnds_leaf_to_root_GeneralExpression, }, ) @@ -904,7 +904,7 @@ def _prop_bnds_root_to_leaf_GeneralExpression(node, bnds_dict, feasibility_tol): Parameters ---------- - node: pyomo.core.base.expression._GeneralExpressionData + node: pyomo.core.base.expression.GeneralExpressionData bnds_dict: ComponentMap feasibility_tol: float If the bounds computed on the body of a constraint violate the bounds of the constraint by more than @@ -945,7 +945,7 @@ def _prop_bnds_root_to_leaf_GeneralExpression(node, bnds_dict, feasibility_tol): ) _prop_bnds_root_to_leaf_map[numeric_expr.AbsExpression] = _prop_bnds_root_to_leaf_abs -_prop_bnds_root_to_leaf_map[_GeneralExpressionData] = ( +_prop_bnds_root_to_leaf_map[GeneralExpressionData] = ( _prop_bnds_root_to_leaf_GeneralExpression ) _prop_bnds_root_to_leaf_map[ScalarExpression] = ( diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 42fc9083953..0e9e379eb21 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -34,7 +34,7 @@ from pyomo.core.expr.visitor import identify_components from pyomo.core.expr.base import ExpressionBase -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.objective import ScalarObjective, _GeneralObjectiveData import pyomo.core.kernel as kernel from pyomo.core.expr.template_expr import ( @@ -399,7 +399,7 @@ def __init__(self): EqualityExpression: handle_equality_node, InequalityExpression: handle_inequality_node, RangedExpression: handle_ranged_inequality_node, - _GeneralExpressionData: handle_named_expression_node, + GeneralExpressionData: handle_named_expression_node, ScalarExpression: handle_named_expression_node, kernel.expression.expression: handle_named_expression_node, kernel.expression.noclone: handle_named_expression_node, diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 1317c928686..6fd82f80ad4 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -803,7 +803,7 @@ class ComponentData(_ComponentBase): # NOTE: This constructor is in-lined in the constructors for the following # classes: BooleanVarData, ConnectorData, ConstraintData, - # _GeneralExpressionData, _LogicalConstraintData, + # GeneralExpressionData, _LogicalConstraintData, # _GeneralLogicalConstraintData, _GeneralObjectiveData, # _ParamData,_GeneralVarData, GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index e21613fcbb1..f5376381b2d 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -148,7 +148,7 @@ class _ExpressionData(metaclass=RenamedClass): __renamed__version__ = '6.7.2.dev0' -class _GeneralExpressionDataImpl(ExpressionData): +class GeneralExpressionDataImpl(ExpressionData): """ An object that defines an expression that is never cloned @@ -240,7 +240,7 @@ def __ipow__(self, other): return numeric_expr._pow_dispatcher[e.__class__, other.__class__](e, other) -class _GeneralExpressionData(_GeneralExpressionDataImpl, ComponentData): +class GeneralExpressionData(GeneralExpressionDataImpl, ComponentData): """ An object that defines an expression that is never cloned @@ -258,12 +258,17 @@ class _GeneralExpressionData(_GeneralExpressionDataImpl, ComponentData): __slots__ = ('_args_',) def __init__(self, expr=None, component=None): - _GeneralExpressionDataImpl.__init__(self, expr) + GeneralExpressionDataImpl.__init__(self, expr) # Inlining ComponentData.__init__ self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET +class _GeneralExpressionData(metaclass=RenamedClass): + __renamed__new_class__ = GeneralExpressionData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register( "Named expressions that can be used in other expressions." ) @@ -280,7 +285,7 @@ class Expression(IndexedComponent): doc Text describing this component. """ - _ComponentDataClass = _GeneralExpressionData + _ComponentDataClass = GeneralExpressionData # This seems like a copy-paste error, and should be renamed/removed NoConstraint = IndexedComponent.Skip @@ -407,9 +412,9 @@ def construct(self, data=None): timer.report() -class ScalarExpression(_GeneralExpressionData, Expression): +class ScalarExpression(GeneralExpressionData, Expression): def __init__(self, *args, **kwds): - _GeneralExpressionData.__init__(self, expr=None, component=self) + GeneralExpressionData.__init__(self, expr=None, component=self) Expression.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -432,7 +437,7 @@ def __call__(self, exception=True): def expr(self): """Return expression on this expression.""" if self._constructed: - return _GeneralExpressionData.expr.fget(self) + return GeneralExpressionData.expr.fget(self) raise ValueError( "Accessing the expression of Expression '%s' " "before the Expression has been constructed (there " @@ -450,7 +455,7 @@ def clear(self): def set_value(self, expr): """Set the expression on this expression.""" if self._constructed: - return _GeneralExpressionData.set_value(self, expr) + return GeneralExpressionData.set_value(self, expr) raise ValueError( "Setting the expression of Expression '%s' " "before the Expression has been constructed (there " @@ -460,7 +465,7 @@ def set_value(self, expr): def is_constant(self): """A boolean indicating whether this expression is constant.""" if self._constructed: - return _GeneralExpressionData.is_constant(self) + return GeneralExpressionData.is_constant(self) raise ValueError( "Accessing the is_constant flag of Expression '%s' " "before the Expression has been constructed (there " @@ -470,7 +475,7 @@ def is_constant(self): def is_fixed(self): """A boolean indicating whether this expression is fixed.""" if self._constructed: - return _GeneralExpressionData.is_fixed(self) + return GeneralExpressionData.is_fixed(self) raise ValueError( "Accessing the is_fixed flag of Expression '%s' " "before the Expression has been constructed (there " @@ -514,6 +519,6 @@ def add(self, index, expr): """Add an expression with a given index.""" if (type(expr) is tuple) and (expr == Expression.Skip): return None - cdata = _GeneralExpressionData(expr, component=self) + cdata = GeneralExpressionData(expr, component=self) self._data[index] = cdata return cdata diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index d259358dcd7..5cd1a1f93eb 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -28,7 +28,7 @@ UnindexedComponent_set, rule_wrapper, ) -from pyomo.core.base.expression import ExpressionData, _GeneralExpressionDataImpl +from pyomo.core.base.expression import ExpressionData, GeneralExpressionDataImpl from pyomo.core.base.set import Set from pyomo.core.base.initializer import ( Initializer, @@ -120,7 +120,7 @@ def set_sense(self, sense): class _GeneralObjectiveData( - _GeneralExpressionDataImpl, _ObjectiveData, ActiveComponentData + GeneralExpressionDataImpl, _ObjectiveData, ActiveComponentData ): """ This class defines the data for a single objective. @@ -147,7 +147,7 @@ class _GeneralObjectiveData( __slots__ = ("_sense", "_args_") def __init__(self, expr=None, sense=minimize, component=None): - _GeneralExpressionDataImpl.__init__(self, expr) + GeneralExpressionDataImpl.__init__(self, expr) # Inlining ActiveComponentData.__init__ self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index a13e0f25ac8..c82103cefb1 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -20,7 +20,7 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.core.base.expression import _GeneralExpressionData +from pyomo.core.base.expression import GeneralExpressionData class _TestComponentDictBase(object): @@ -360,7 +360,7 @@ def setUp(self): class TestExpressionDict(_TestComponentDictBase, unittest.TestCase): _ctype = ExpressionDict - _cdatatype = _GeneralExpressionData + _cdatatype = GeneralExpressionData def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_expression.py b/pyomo/core/tests/unit/test_expression.py index 678df4c01a8..bf3ce0c2179 100644 --- a/pyomo/core/tests/unit/test_expression.py +++ b/pyomo/core/tests/unit/test_expression.py @@ -29,7 +29,7 @@ value, sum_product, ) -from pyomo.core.base.expression import _GeneralExpressionData +from pyomo.core.base.expression import GeneralExpressionData from pyomo.core.expr.compare import compare_expressions, assertExpressionsEqual from pyomo.common.tee import capture_output @@ -515,10 +515,10 @@ def test_implicit_definition(self): model.E = Expression(model.idx) self.assertEqual(len(model.E), 3) expr = model.E[1] - self.assertIs(type(expr), _GeneralExpressionData) + self.assertIs(type(expr), GeneralExpressionData) model.E[1] = None self.assertIs(expr, model.E[1]) - self.assertIs(type(expr), _GeneralExpressionData) + self.assertIs(type(expr), GeneralExpressionData) self.assertIs(expr.expr, None) model.E[1] = 5 self.assertIs(expr, model.E[1]) @@ -537,7 +537,7 @@ def test_explicit_skip_definition(self): model.E[1] = None expr = model.E[1] - self.assertIs(type(expr), _GeneralExpressionData) + self.assertIs(type(expr), GeneralExpressionData) self.assertIs(expr.expr, None) model.E[1] = 5 self.assertIs(expr, model.E[1]) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index e9c1cceb701..b8e97b464fe 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -20,7 +20,7 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import _GeneralObjectiveData -from pyomo.core.base.expression import _GeneralExpressionData +from pyomo.core.base.expression import GeneralExpressionData class _TestComponentListBase(object): @@ -377,7 +377,7 @@ def setUp(self): class TestExpressionList(_TestComponentListBase, unittest.TestCase): _ctype = XExpressionList - _cdatatype = _GeneralExpressionData + _cdatatype = GeneralExpressionData def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/dae/integral.py b/pyomo/dae/integral.py index 41114296a93..f767e31f18c 100644 --- a/pyomo/dae/integral.py +++ b/pyomo/dae/integral.py @@ -14,7 +14,7 @@ from pyomo.core.base.indexed_component import rule_wrapper from pyomo.core.base.expression import ( Expression, - _GeneralExpressionData, + GeneralExpressionData, ScalarExpression, IndexedExpression, ) @@ -151,7 +151,7 @@ class ScalarIntegral(ScalarExpression, Integral): """ def __init__(self, *args, **kwds): - _GeneralExpressionData.__init__(self, None, component=self) + GeneralExpressionData.__init__(self, None, component=self) Integral.__init__(self, *args, **kwds) def clear(self): diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 29d841248da..a0dc09e2aa6 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -70,7 +70,7 @@ ) from pyomo.core.base.component import ActiveComponent from pyomo.core.base.constraint import ConstraintData -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.objective import ( ScalarObjective, _GeneralObjectiveData, diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 70368dd3d7e..cf2ba334d6c 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -21,7 +21,7 @@ from pyomo.core.expr.numvalue import NumericConstant from pyomo.core.base.objective import _GeneralObjectiveData, ScalarObjective from pyomo.core.base import ExpressionData, Expression -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.var import ScalarVar, Var, _GeneralVarData, value from pyomo.core.base.param import ScalarParam, _ParamData from pyomo.core.kernel.expression import expression, noclone @@ -1148,7 +1148,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra Var: _collect_var, variable: _collect_var, IVariable: _collect_var, - _GeneralExpressionData: _collect_identity, + GeneralExpressionData: _collect_identity, ScalarExpression: _collect_identity, expression: _collect_identity, noclone: _collect_identity, @@ -1547,7 +1547,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): Var : _linear_collect_var, variable : _linear_collect_var, IVariable : _linear_collect_var, - _GeneralExpressionData : _linear_collect_identity, + GeneralExpressionData : _linear_collect_identity, ScalarExpression : _linear_collect_identity, expression : _linear_collect_identity, noclone : _linear_collect_identity, From cd5db6637bdd16a72d8260459c6b7757dd6c3cf0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:43:42 -0600 Subject: [PATCH 0993/3044] Renamed _GeneralLogicalConstraintData -> GeneralLogicalConstraintData --- pyomo/core/base/component.py | 2 +- pyomo/core/base/logical_constraint.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 6fd82f80ad4..720373db809 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -804,7 +804,7 @@ class ComponentData(_ComponentBase): # NOTE: This constructor is in-lined in the constructors for the following # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, _LogicalConstraintData, - # _GeneralLogicalConstraintData, _GeneralObjectiveData, + # GeneralLogicalConstraintData, _GeneralObjectiveData, # _ParamData,_GeneralVarData, GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index 3a7bca75960..9af99c9ce5c 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -99,7 +99,7 @@ def get_value(self): raise NotImplementedError -class _GeneralLogicalConstraintData(_LogicalConstraintData): +class GeneralLogicalConstraintData(_LogicalConstraintData): """ This class defines the data for a single general logical constraint. @@ -173,6 +173,11 @@ def get_value(self): return self._expr +class _GeneralLogicalConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = GeneralLogicalConstraintData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("General logical constraints.") class LogicalConstraint(ActiveIndexedComponent): """ @@ -215,7 +220,7 @@ class LogicalConstraint(ActiveIndexedComponent): The class type for the derived subclass """ - _ComponentDataClass = _GeneralLogicalConstraintData + _ComponentDataClass = GeneralLogicalConstraintData class Infeasible(object): pass @@ -409,14 +414,14 @@ def _check_skip_add(self, index, expr): return expr -class ScalarLogicalConstraint(_GeneralLogicalConstraintData, LogicalConstraint): +class ScalarLogicalConstraint(GeneralLogicalConstraintData, LogicalConstraint): """ ScalarLogicalConstraint is the implementation representing a single, non-indexed logical constraint. """ def __init__(self, *args, **kwds): - _GeneralLogicalConstraintData.__init__(self, component=self, expr=None) + GeneralLogicalConstraintData.__init__(self, component=self, expr=None) LogicalConstraint.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -436,7 +441,7 @@ def body(self): "an expression. There is currently " "nothing to access." % self.name ) - return _GeneralLogicalConstraintData.body.fget(self) + return GeneralLogicalConstraintData.body.fget(self) raise ValueError( "Accessing the body of logical constraint '%s' " "before the LogicalConstraint has been constructed (there " From 6e9d84cb43e1f327fc230c0711769cea598f3d03 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:45:29 -0600 Subject: [PATCH 0994/3044] Renamed _GeneralObjectiveData -> GeneralObjectiveData --- pyomo/contrib/appsi/base.py | 8 ++++---- pyomo/contrib/appsi/fbbt.py | 6 +++--- pyomo/contrib/appsi/solvers/cbc.py | 4 ++-- pyomo/contrib/appsi/solvers/cplex.py | 4 ++-- pyomo/contrib/appsi/solvers/ipopt.py | 4 ++-- pyomo/contrib/appsi/writers/lp_writer.py | 4 ++-- pyomo/contrib/appsi/writers/nl_writer.py | 4 ++-- .../contrib/community_detection/detection.py | 4 ++-- pyomo/contrib/latex_printer/latex_printer.py | 4 ++-- pyomo/contrib/solver/base.py | 4 ++-- pyomo/contrib/solver/persistent.py | 6 +++--- pyomo/core/base/component.py | 2 +- pyomo/core/base/objective.py | 19 ++++++++++++------- pyomo/core/tests/unit/test_dict_objects.py | 4 ++-- pyomo/core/tests/unit/test_list_objects.py | 4 ++-- pyomo/repn/plugins/nl_writer.py | 2 +- pyomo/repn/standard_repn.py | 6 +++--- 17 files changed, 47 insertions(+), 42 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index d5982fc72e6..b1538ef1a35 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -26,7 +26,7 @@ from pyomo.core.base.var import _GeneralVarData, Var from pyomo.core.base.param import _ParamData, Param from pyomo.core.base.block import BlockData, Block -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.collections import ComponentMap from .utils.get_objective import get_objective from .utils.collect_vars_and_named_exprs import collect_vars_and_named_exprs @@ -831,7 +831,7 @@ def remove_block(self, block: BlockData): pass @abc.abstractmethod - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): pass @abc.abstractmethod @@ -1054,10 +1054,10 @@ def add_sos_constraints(self, cons: List[_SOSConstraintData]): self._add_sos_constraints(cons) @abc.abstractmethod - def _set_objective(self, obj: _GeneralObjectiveData): + def _set_objective(self, obj: GeneralObjectiveData): pass - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): if self._objective is not None: for v in self._vars_referenced_by_obj: self._referenced_variables[id(v)][2] = None diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 121557414b3..ca178a49b00 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -22,7 +22,7 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData, minimize, maximize +from pyomo.core.base.objective import GeneralObjectiveData, minimize, maximize from pyomo.core.base.block import BlockData from pyomo.core.base import SymbolMap, TextLabeler from pyomo.common.errors import InfeasibleConstraintException @@ -224,13 +224,13 @@ def update_params(self): cp = self._param_map[p_id] cp.value = p.value - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): if self._symbolic_solver_labels: if self._objective is not None: self._symbol_map.removeSymbol(self._objective) super().set_objective(obj) - def _set_objective(self, obj: _GeneralObjectiveData): + def _set_objective(self, obj: GeneralObjectiveData): if obj is None: ce = cmodel.Constant(0) sense = 0 diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index cc7327df11c..e73d080c02b 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -30,7 +30,7 @@ from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream import sys @@ -188,7 +188,7 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): def remove_block(self, block: BlockData): self._writer.remove_block(block) - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) def update_variables(self, variables: List[_GeneralVarData]): diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 222c466fb99..ffca656735e 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -26,7 +26,7 @@ from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer import sys import time @@ -203,7 +203,7 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): def remove_block(self, block: BlockData): self._writer.remove_block(block) - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) def update_variables(self, variables: List[_GeneralVarData]): diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 75ebb10f719..97d76a9ecb1 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -32,7 +32,7 @@ from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream import sys @@ -252,7 +252,7 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): def remove_block(self, block: BlockData): self._writer.remove_block(block) - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) def update_variables(self, variables: List[_GeneralVarData]): diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 39298bd1a61..696b1c16d61 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -13,7 +13,7 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn @@ -147,7 +147,7 @@ def update_params(self): cp = self._pyomo_param_to_solver_param_map[p_id] cp.value = p.value - def _set_objective(self, obj: _GeneralObjectiveData): + def _set_objective(self, obj: GeneralObjectiveData): cobj = cmodel.process_lp_objective( self._expr_types, obj, diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 3e13ef4077a..33d7c59f08f 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -13,7 +13,7 @@ from pyomo.core.base.param import _ParamData from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn @@ -180,7 +180,7 @@ def update_params(self): cp = self._pyomo_param_to_solver_param_map[p_id] cp.value = p.value - def _set_objective(self, obj: _GeneralObjectiveData): + def _set_objective(self, obj: GeneralObjectiveData): if obj is None: const = cmodel.Constant(0) lin_vars = list() diff --git a/pyomo/contrib/community_detection/detection.py b/pyomo/contrib/community_detection/detection.py index 5bf8187a243..af87fa5eb8b 100644 --- a/pyomo/contrib/community_detection/detection.py +++ b/pyomo/contrib/community_detection/detection.py @@ -31,7 +31,7 @@ Objective, ConstraintList, ) -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.expr.visitor import replace_expressions, identify_variables from pyomo.contrib.community_detection.community_graph import generate_model_graph from pyomo.common.dependencies import networkx as nx @@ -750,7 +750,7 @@ def generate_structured_model(self): # Check to see whether 'stored_constraint' is actually an objective (since constraints and objectives # grouped together) if self.with_objective and isinstance( - stored_constraint, (_GeneralObjectiveData, Objective) + stored_constraint, (GeneralObjectiveData, Objective) ): # If the constraint is actually an objective, we add it to the block as an objective new_objective = Objective( diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 0e9e379eb21..5a2365f9544 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -35,7 +35,7 @@ from pyomo.core.expr.visitor import identify_components from pyomo.core.expr.base import ExpressionBase from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData -from pyomo.core.base.objective import ScalarObjective, _GeneralObjectiveData +from pyomo.core.base.objective import ScalarObjective, GeneralObjectiveData import pyomo.core.kernel as kernel from pyomo.core.expr.template_expr import ( GetItemExpression, @@ -403,7 +403,7 @@ def __init__(self): ScalarExpression: handle_named_expression_node, kernel.expression.expression: handle_named_expression_node, kernel.expression.noclone: handle_named_expression_node, - _GeneralObjectiveData: handle_named_expression_node, + GeneralObjectiveData: handle_named_expression_node, _GeneralVarData: handle_var_node, ScalarObjective: handle_named_expression_node, kernel.objective.objective: handle_named_expression_node, diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 4b7d8f35ddc..0a12f572e5f 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -18,7 +18,7 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.block import BlockData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.config import document_kwargs_from_configdict, ConfigValue from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning @@ -276,7 +276,7 @@ def set_instance(self, model): """ @abc.abstractmethod - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): """ Set current objective for the model """ diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index 9b63e05ce46..97a4067e78b 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -16,7 +16,7 @@ from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData, Param -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.collections import ComponentMap from pyomo.common.timing import HierarchicalTimer from pyomo.core.expr.numvalue import NumericConstant @@ -149,10 +149,10 @@ def add_sos_constraints(self, cons: List[_SOSConstraintData]): self._add_sos_constraints(cons) @abc.abstractmethod - def _set_objective(self, obj: _GeneralObjectiveData): + def _set_objective(self, obj: GeneralObjectiveData): pass - def set_objective(self, obj: _GeneralObjectiveData): + def set_objective(self, obj: GeneralObjectiveData): if self._objective is not None: for v in self._vars_referenced_by_obj: self._referenced_variables[id(v)][2] = None diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 720373db809..1fd30f4212e 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -804,7 +804,7 @@ class ComponentData(_ComponentBase): # NOTE: This constructor is in-lined in the constructors for the following # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, _LogicalConstraintData, - # GeneralLogicalConstraintData, _GeneralObjectiveData, + # GeneralLogicalConstraintData, GeneralObjectiveData, # _ParamData,_GeneralVarData, GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index 5cd1a1f93eb..b89214377ab 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -119,7 +119,7 @@ def set_sense(self, sense): raise NotImplementedError -class _GeneralObjectiveData( +class GeneralObjectiveData( GeneralExpressionDataImpl, _ObjectiveData, ActiveComponentData ): """ @@ -192,6 +192,11 @@ def set_sense(self, sense): ) +class _GeneralObjectiveData(metaclass=RenamedClass): + __renamed__new_class__ = GeneralObjectiveData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("Expressions that are minimized or maximized.") class Objective(ActiveIndexedComponent): """ @@ -240,7 +245,7 @@ class Objective(ActiveIndexedComponent): The class type for the derived subclass """ - _ComponentDataClass = _GeneralObjectiveData + _ComponentDataClass = GeneralObjectiveData NoObjective = ActiveIndexedComponent.Skip def __new__(cls, *args, **kwds): @@ -389,14 +394,14 @@ def display(self, prefix="", ostream=None): ) -class ScalarObjective(_GeneralObjectiveData, Objective): +class ScalarObjective(GeneralObjectiveData, Objective): """ ScalarObjective is the implementation representing a single, non-indexed objective. """ def __init__(self, *args, **kwd): - _GeneralObjectiveData.__init__(self, expr=None, component=self) + GeneralObjectiveData.__init__(self, expr=None, component=self) Objective.__init__(self, *args, **kwd) self._index = UnindexedComponent_index @@ -432,7 +437,7 @@ def expr(self): "a sense or expression (there is currently " "no value to return)." % (self.name) ) - return _GeneralObjectiveData.expr.fget(self) + return GeneralObjectiveData.expr.fget(self) raise ValueError( "Accessing the expression of objective '%s' " "before the Objective has been constructed (there " @@ -455,7 +460,7 @@ def sense(self): "a sense or expression (there is currently " "no value to return)." % (self.name) ) - return _GeneralObjectiveData.sense.fget(self) + return GeneralObjectiveData.sense.fget(self) raise ValueError( "Accessing the sense of objective '%s' " "before the Objective has been constructed (there " @@ -498,7 +503,7 @@ def set_sense(self, sense): if self._constructed: if len(self._data) == 0: self._data[None] = self - return _GeneralObjectiveData.set_sense(self, sense) + return GeneralObjectiveData.set_sense(self, sense) raise ValueError( "Setting the sense of objective '%s' " "before the Objective has been constructed (there " diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index c82103cefb1..6dd8a21e2b4 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -19,7 +19,7 @@ ) from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -384,7 +384,7 @@ def setUp(self): class TestObjectiveDict(_TestActiveComponentDictBase, unittest.TestCase): _ctype = ObjectiveDict - _cdatatype = _GeneralObjectiveData + _cdatatype = GeneralObjectiveData def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index b8e97b464fe..1609f97af90 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -19,7 +19,7 @@ ) from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -401,7 +401,7 @@ def setUp(self): class TestObjectiveList(_TestActiveComponentListBase, unittest.TestCase): _ctype = XObjectiveList - _cdatatype = _GeneralObjectiveData + _cdatatype = GeneralObjectiveData def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index a0dc09e2aa6..e1afb5720f3 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -73,7 +73,7 @@ from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.objective import ( ScalarObjective, - _GeneralObjectiveData, + GeneralObjectiveData, _ObjectiveData, ) from pyomo.core.base.suffix import SuffixFinder diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index cf2ba334d6c..442e4677dbd 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -19,7 +19,7 @@ import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import NumericConstant -from pyomo.core.base.objective import _GeneralObjectiveData, ScalarObjective +from pyomo.core.base.objective import GeneralObjectiveData, ScalarObjective from pyomo.core.base import ExpressionData, Expression from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.var import ScalarVar, Var, _GeneralVarData, value @@ -1154,7 +1154,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra noclone: _collect_identity, ExpressionData: _collect_identity, Expression: _collect_identity, - _GeneralObjectiveData: _collect_identity, + GeneralObjectiveData: _collect_identity, ScalarObjective: _collect_identity, objective: _collect_identity, } @@ -1553,7 +1553,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): noclone : _linear_collect_identity, ExpressionData : _linear_collect_identity, Expression : _linear_collect_identity, - _GeneralObjectiveData : _linear_collect_identity, + GeneralObjectiveData : _linear_collect_identity, ScalarObjective : _linear_collect_identity, objective : _linear_collect_identity, } From aa79e1b4b0e517a28b8bf8360dea9b6a4b91bba3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:48:14 -0600 Subject: [PATCH 0995/3044] Renamed _GeneralVarData -> GeneralVarData --- pyomo/contrib/appsi/base.py | 56 +++++++++---------- pyomo/contrib/appsi/fbbt.py | 10 ++-- pyomo/contrib/appsi/solvers/cbc.py | 16 +++--- pyomo/contrib/appsi/solvers/cplex.py | 16 +++--- pyomo/contrib/appsi/solvers/gurobi.py | 12 ++-- pyomo/contrib/appsi/solvers/highs.py | 8 +-- pyomo/contrib/appsi/solvers/ipopt.py | 16 +++--- pyomo/contrib/appsi/solvers/wntr.py | 8 +-- pyomo/contrib/appsi/writers/lp_writer.py | 8 +-- pyomo/contrib/appsi/writers/nl_writer.py | 8 +-- pyomo/contrib/cp/repn/docplex_writer.py | 4 +- .../logical_to_disjunctive_walker.py | 4 +- pyomo/contrib/latex_printer/latex_printer.py | 10 ++-- pyomo/contrib/parmest/utils/scenario_tree.py | 2 +- pyomo/contrib/solver/base.py | 22 ++++---- pyomo/contrib/solver/gurobi.py | 12 ++-- pyomo/contrib/solver/ipopt.py | 6 +- pyomo/contrib/solver/persistent.py | 18 +++--- pyomo/contrib/solver/solution.py | 22 ++++---- .../contrib/solver/tests/unit/test_results.py | 10 ++-- .../trustregion/tests/test_interface.py | 4 +- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/component.py | 2 +- pyomo/core/base/var.py | 15 +++-- pyomo/core/expr/calculus/derivatives.py | 6 +- pyomo/core/tests/transform/test_add_slacks.py | 2 +- pyomo/core/tests/unit/test_dict_objects.py | 6 +- pyomo/core/tests/unit/test_list_objects.py | 6 +- pyomo/core/tests/unit/test_numeric_expr.py | 4 +- pyomo/core/tests/unit/test_reference.py | 12 ++-- pyomo/repn/standard_repn.py | 6 +- .../plugins/solvers/gurobi_persistent.py | 4 +- pyomo/util/report_scaling.py | 4 +- 33 files changed, 173 insertions(+), 168 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index b1538ef1a35..1ce24220bfd 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -23,7 +23,7 @@ ) from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint -from pyomo.core.base.var import _GeneralVarData, Var +from pyomo.core.base.var import GeneralVarData, Var from pyomo.core.base.param import _ParamData, Param from pyomo.core.base.block import BlockData, Block from pyomo.core.base.objective import GeneralObjectiveData @@ -180,7 +180,7 @@ def __init__( class SolutionLoaderBase(abc.ABC): def load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None ) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -197,8 +197,8 @@ def load_vars( @abc.abstractmethod def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: """ Returns a ComponentMap mapping variable to var value. @@ -256,8 +256,8 @@ def get_slacks( ) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: """ Returns a ComponentMap mapping variable to reduced cost. @@ -303,8 +303,8 @@ def __init__( self._reduced_costs = reduced_costs def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if self._primals is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' @@ -353,8 +353,8 @@ def get_slacks( return slacks def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if self._reduced_costs is None: raise RuntimeError( 'Solution loader does not currently have valid reduced costs. Please ' @@ -709,7 +709,7 @@ def is_persistent(self): return True def load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None ) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -726,8 +726,8 @@ def load_vars( @abc.abstractmethod def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: pass def get_duals( @@ -771,8 +771,8 @@ def get_slacks( ) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: """ Parameters ---------- @@ -799,7 +799,7 @@ def set_instance(self, model): pass @abc.abstractmethod - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[GeneralVarData]): pass @abc.abstractmethod @@ -815,7 +815,7 @@ def add_block(self, block: BlockData): pass @abc.abstractmethod - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[GeneralVarData]): pass @abc.abstractmethod @@ -835,7 +835,7 @@ def set_objective(self, obj: GeneralObjectiveData): pass @abc.abstractmethod - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[GeneralVarData]): pass @abc.abstractmethod @@ -869,8 +869,8 @@ def get_slacks( return self._solver.get_slacks(cons_to_load=cons_to_load) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: self._assert_solution_still_valid() return self._solver.get_reduced_costs(vars_to_load=vars_to_load) @@ -954,10 +954,10 @@ def set_instance(self, model): self.set_objective(None) @abc.abstractmethod - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): pass - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[GeneralVarData]): for v in variables: if id(v) in self._referenced_variables: raise ValueError( @@ -987,7 +987,7 @@ def add_params(self, params: List[_ParamData]): def _add_constraints(self, cons: List[GeneralConstraintData]): pass - def _check_for_new_vars(self, variables: List[_GeneralVarData]): + def _check_for_new_vars(self, variables: List[GeneralVarData]): new_vars = dict() for v in variables: v_id = id(v) @@ -995,7 +995,7 @@ def _check_for_new_vars(self, variables: List[_GeneralVarData]): new_vars[v_id] = v self.add_variables(list(new_vars.values())) - def _check_to_remove_vars(self, variables: List[_GeneralVarData]): + def _check_to_remove_vars(self, variables: List[GeneralVarData]): vars_to_remove = dict() for v in variables: v_id = id(v) @@ -1174,10 +1174,10 @@ def remove_sos_constraints(self, cons: List[_SOSConstraintData]): del self._vars_referenced_by_con[con] @abc.abstractmethod - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): pass - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[GeneralVarData]): self._remove_variables(variables) for v in variables: v_id = id(v) @@ -1246,10 +1246,10 @@ def remove_block(self, block): ) @abc.abstractmethod - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): pass - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[GeneralVarData]): for v in variables: self._vars[id(v)] = ( v, diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index ca178a49b00..a360d2bce84 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -18,7 +18,7 @@ ) from .cmodel import cmodel, cmodel_available from typing import List, Optional -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData @@ -121,7 +121,7 @@ def set_instance(self, model, symbolic_solver_labels: Optional[bool] = None): if self._objective is None: self.set_objective(None) - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): if self._symbolic_solver_labels: set_name = True symbol_map = self._symbol_map @@ -190,7 +190,7 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): 'IntervalTightener does not support SOS constraints' ) - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): if self._symbolic_solver_labels: for v in variables: self._symbol_map.removeSymbol(v) @@ -205,7 +205,7 @@ def _remove_params(self, params: List[_ParamData]): for p in params: del self._param_map[id(p)] - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): cmodel.process_pyomo_vars( self._pyomo_expr_types, variables, @@ -304,7 +304,7 @@ def perform_fbbt( self._deactivate_satisfied_cons() return n_iter - def perform_fbbt_with_seed(self, model: BlockData, seed_var: _GeneralVarData): + def perform_fbbt_with_seed(self, model: BlockData, seed_var: GeneralVarData): if model is not self._model: self.set_instance(model) else: diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index e73d080c02b..cd5158d905e 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -26,7 +26,7 @@ import math from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData @@ -164,7 +164,7 @@ def symbol_map(self): def set_instance(self, model): self._writer.set_instance(model) - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[GeneralVarData]): self._writer.add_variables(variables) def add_params(self, params: List[_ParamData]): @@ -176,7 +176,7 @@ def add_constraints(self, cons: List[GeneralConstraintData]): def add_block(self, block: BlockData): self._writer.add_block(block) - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[GeneralVarData]): self._writer.remove_variables(variables) def remove_params(self, params: List[_ParamData]): @@ -191,7 +191,7 @@ def remove_block(self, block: BlockData): def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[GeneralVarData]): self._writer.update_variables(variables) def update_params(self): @@ -440,8 +440,8 @@ def _check_and_escape_options(): return results def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if ( self._last_results_object is None or self._last_results_object.best_feasible_objective is None @@ -477,8 +477,8 @@ def get_duals(self, cons_to_load=None): return {c: self._dual_sol[c] for c in cons_to_load} def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if ( self._last_results_object is None or self._last_results_object.termination_condition diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index ffca656735e..cdd699105be 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -22,7 +22,7 @@ import math from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping, Dict -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData @@ -179,7 +179,7 @@ def update_config(self): def set_instance(self, model): self._writer.set_instance(model) - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[GeneralVarData]): self._writer.add_variables(variables) def add_params(self, params: List[_ParamData]): @@ -191,7 +191,7 @@ def add_constraints(self, cons: List[GeneralConstraintData]): def add_block(self, block: BlockData): self._writer.add_block(block) - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[GeneralVarData]): self._writer.remove_variables(variables) def remove_params(self, params: List[_ParamData]): @@ -206,7 +206,7 @@ def remove_block(self, block: BlockData): def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[GeneralVarData]): self._writer.update_variables(variables) def update_params(self): @@ -362,8 +362,8 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): return results def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if ( self._cplex_model.solution.get_solution_type() == self._cplex_model.solution.type.none @@ -440,8 +440,8 @@ def get_duals( return res def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if ( self._cplex_model.solution.get_solution_type() == self._cplex_model.solution.type.none diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index e20168034c6..6da59042a80 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -23,7 +23,7 @@ from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.base.var import Var, _GeneralVarData +from pyomo.core.base.var import Var, GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData @@ -458,7 +458,7 @@ def _process_domain_and_bounds( return lb, ub, vtype - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): var_names = list() vtypes = list() lbs = list() @@ -759,7 +759,7 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): del self._pyomo_sos_to_solver_sos_map[con] self._needs_updated = True - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): for var in variables: v_id = id(var) if var in self._vars_added_since_update: @@ -774,7 +774,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): def _remove_params(self, params: List[_ParamData]): pass - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): for var in variables: var_id = id(var) if var_id not in self._pyomo_var_to_solver_var_map: @@ -1221,7 +1221,7 @@ def set_var_attr(self, var, attr, val): Parameters ---------- - var: pyomo.core.base.var._GeneralVarData + var: pyomo.core.base.var.GeneralVarData The pyomo var for which the corresponding gurobi var attribute should be modified. attr: str @@ -1256,7 +1256,7 @@ def get_var_attr(self, var, attr): Parameters ---------- - var: pyomo.core.base.var._GeneralVarData + var: pyomo.core.base.var.GeneralVarData The pyomo var for which the corresponding gurobi var attribute should be retrieved. attr: str diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 7773d0624b2..ded0092f38b 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -20,7 +20,7 @@ from pyomo.common.log import LogStream from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData @@ -308,7 +308,7 @@ def _process_domain_and_bounds(self, var_id): return lb, ub, vtype - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -493,7 +493,7 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): 'Highs interface does not support SOS constraints' ) - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -518,7 +518,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): def _remove_params(self, params: List[_ParamData]): pass - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 97d76a9ecb1..9ccb58095b1 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -28,7 +28,7 @@ from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions from typing import Optional, Sequence, NoReturn, List, Mapping -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import _ParamData @@ -228,7 +228,7 @@ def set_instance(self, model): self._writer.config.symbolic_solver_labels = self.config.symbolic_solver_labels self._writer.set_instance(model) - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[GeneralVarData]): self._writer.add_variables(variables) def add_params(self, params: List[_ParamData]): @@ -240,7 +240,7 @@ def add_constraints(self, cons: List[GeneralConstraintData]): def add_block(self, block: BlockData): self._writer.add_block(block) - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[GeneralVarData]): self._writer.remove_variables(variables) def remove_params(self, params: List[_ParamData]): @@ -255,7 +255,7 @@ def remove_block(self, block: BlockData): def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[GeneralVarData]): self._writer.update_variables(variables) def update_params(self): @@ -514,8 +514,8 @@ def _apply_solver(self, timer: HierarchicalTimer): return results def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if ( self._last_results_object is None or self._last_results_object.best_feasible_objective is None @@ -551,8 +551,8 @@ def get_duals(self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = No return {c: self._dual_sol[c] for c in cons_to_load} def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if ( self._last_results_object is None or self._last_results_object.termination_condition diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index c11536e2e6f..7f633161fe1 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -40,7 +40,7 @@ from pyomo.core.expr.numvalue import native_numeric_types from typing import Dict, Optional, List from pyomo.core.base.block import BlockData -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.common.timing import HierarchicalTimer @@ -239,7 +239,7 @@ def set_instance(self, model): self.add_block(model) - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): aml = wntr.sim.aml.aml for var in variables: varname = self._symbol_map.getSymbol(var, self._labeler) @@ -302,7 +302,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): del self._pyomo_con_to_solver_con_map[con] self._needs_updated = True - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): for var in variables: v_id = id(var) solver_var = self._pyomo_var_to_solver_var_map[v_id] @@ -322,7 +322,7 @@ def _remove_params(self, params: List[_ParamData]): self._symbol_map.removeSymbol(p) del self._pyomo_param_to_solver_param_map[p_id] - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): aml = wntr.sim.aml.aml for var in variables: v_id = id(var) diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 696b1c16d61..94af5ba7e93 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -11,7 +11,7 @@ from typing import List from pyomo.core.base.param import _ParamData -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData @@ -77,7 +77,7 @@ def set_instance(self, model): if self._objective is None: self.set_objective(None) - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): cmodel.process_pyomo_vars( self._expr_types, variables, @@ -117,7 +117,7 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('LP writer does not yet support SOS constraints') - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): for v in variables: cvar = self._pyomo_var_to_solver_var_map.pop(id(v)) del self._solver_var_to_pyomo_var_map[cvar] @@ -128,7 +128,7 @@ def _remove_params(self, params: List[_ParamData]): del self._pyomo_param_to_solver_param_map[id(p)] self._symbol_map.removeSymbol(p) - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): cmodel.process_pyomo_vars( self._expr_types, variables, diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 33d7c59f08f..b7dab1d5a3e 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -11,7 +11,7 @@ from typing import List from pyomo.core.base.param import _ParamData -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.sos import _SOSConstraintData @@ -78,7 +78,7 @@ def set_instance(self, model): self.set_objective(None) self._set_pyomo_amplfunc_env() - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): if self.config.symbolic_solver_labels: set_name = True symbol_map = self._symbol_map @@ -144,7 +144,7 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('NL writer does not support SOS constraints') - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): if self.config.symbolic_solver_labels: for v in variables: self._symbol_map.removeSymbol(v) @@ -161,7 +161,7 @@ def _remove_params(self, params: List[_ParamData]): for p in params: del self._pyomo_param_to_solver_param_map[id(p)] - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): cmodel.process_pyomo_vars( self._expr_types, variables, diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 00b187a585e..eb50a543160 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -65,7 +65,7 @@ ) from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.param import IndexedParam, ScalarParam, _ParamData -from pyomo.core.base.var import ScalarVar, _GeneralVarData, IndexedVar +from pyomo.core.base.var import ScalarVar, GeneralVarData, IndexedVar import pyomo.core.expr as EXPR from pyomo.core.expr.visitor import StreamBasedExpressionVisitor, identify_variables from pyomo.core.base import Set, RangeSet @@ -961,7 +961,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): IntervalVarData: _before_interval_var, IndexedIntervalVar: _before_indexed_interval_var, ScalarVar: _before_var, - _GeneralVarData: _before_var, + GeneralVarData: _before_var, IndexedVar: _before_indexed_var, ScalarBooleanVar: _before_boolean_var, GeneralBooleanVarData: _before_boolean_var, diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index b4fb5e26900..26b63d020a5 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -29,7 +29,7 @@ import pyomo.core.base.boolean_var as BV from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.param import ScalarParam, _ParamData -from pyomo.core.base.var import ScalarVar, _GeneralVarData +from pyomo.core.base.var import ScalarVar, GeneralVarData from pyomo.gdp.disjunct import AutoLinkedBooleanVar, Disjunct, Disjunction @@ -216,7 +216,7 @@ def _dispatch_atmost(visitor, node, *args): # for the moment, these are all just so we can get good error messages when we # don't handle them: _before_child_dispatcher[ScalarVar] = _dispatch_var -_before_child_dispatcher[_GeneralVarData] = _dispatch_var +_before_child_dispatcher[GeneralVarData] = _dispatch_var _before_child_dispatcher[GeneralExpressionData] = _dispatch_expression _before_child_dispatcher[ScalarExpression] = _dispatch_expression diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 5a2365f9544..efcd3016dbf 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -47,7 +47,7 @@ resolve_template, templatize_rule, ) -from pyomo.core.base.var import ScalarVar, _GeneralVarData, IndexedVar +from pyomo.core.base.var import ScalarVar, GeneralVarData, IndexedVar from pyomo.core.base.param import _ParamData, ScalarParam, IndexedParam from pyomo.core.base.set import _SetData, SetOperator from pyomo.core.base.constraint import ScalarConstraint, IndexedConstraint @@ -404,7 +404,7 @@ def __init__(self): kernel.expression.expression: handle_named_expression_node, kernel.expression.noclone: handle_named_expression_node, GeneralObjectiveData: handle_named_expression_node, - _GeneralVarData: handle_var_node, + GeneralVarData: handle_var_node, ScalarObjective: handle_named_expression_node, kernel.objective.objective: handle_named_expression_node, ExternalFunctionExpression: handle_external_function_node, @@ -706,9 +706,9 @@ def latex_printer( temp_comp, temp_indexes = templatize_fcn(pyomo_component) variableList = [] for v in identify_components( - temp_comp, [ScalarVar, _GeneralVarData, IndexedVar] + temp_comp, [ScalarVar, GeneralVarData, IndexedVar] ): - if isinstance(v, _GeneralVarData): + if isinstance(v, GeneralVarData): v_write = v.parent_component() if v_write not in ComponentSet(variableList): variableList.append(v_write) @@ -1275,7 +1275,7 @@ def get_index_names(st, lcm): rep_dict = {} for ky in reversed(list(latex_component_map)): - if isinstance(ky, (pyo.Var, _GeneralVarData)): + if isinstance(ky, (pyo.Var, GeneralVarData)): overwrite_value = latex_component_map[ky] if ky not in existing_components: overwrite_value = overwrite_value.replace('_', '\\_') diff --git a/pyomo/contrib/parmest/utils/scenario_tree.py b/pyomo/contrib/parmest/utils/scenario_tree.py index e71f51877b5..1062e4a2bf4 100644 --- a/pyomo/contrib/parmest/utils/scenario_tree.py +++ b/pyomo/contrib/parmest/utils/scenario_tree.py @@ -25,7 +25,7 @@ def build_vardatalist(self, model, varlist=None): """ - Convert a list of pyomo variables to a list of ScalarVar and _GeneralVarData. If varlist is none, builds a + Convert a list of pyomo variables to a list of ScalarVar and GeneralVarData. If varlist is none, builds a list of all variables in the model. The new list is stored in the vars_to_tighten attribute. By CD Laird Parameters diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 0a12f572e5f..a935a950819 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -15,7 +15,7 @@ import os from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.block import BlockData from pyomo.core.base.objective import GeneralObjectiveData @@ -195,7 +195,7 @@ def is_persistent(self): return True def _load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None ) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -212,19 +212,19 @@ def _load_vars( @abc.abstractmethod def _get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: """ Get mapping of variables to primals. Parameters ---------- - vars_to_load : Optional[Sequence[_GeneralVarData]], optional + vars_to_load : Optional[Sequence[GeneralVarData]], optional Which vars to be populated into the map. The default is None. Returns ------- - Mapping[_GeneralVarData, float] + Mapping[GeneralVarData, float] A map of variables to primals. """ raise NotImplementedError( @@ -251,8 +251,8 @@ def _get_duals( raise NotImplementedError(f'{type(self)} does not support the get_duals method') def _get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: """ Parameters ---------- @@ -282,7 +282,7 @@ def set_objective(self, obj: GeneralObjectiveData): """ @abc.abstractmethod - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[GeneralVarData]): """ Add variables to the model """ @@ -306,7 +306,7 @@ def add_block(self, block: BlockData): """ @abc.abstractmethod - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[GeneralVarData]): """ Remove variables from the model """ @@ -330,7 +330,7 @@ def remove_block(self, block: BlockData): """ @abc.abstractmethod - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[GeneralVarData]): """ Update variables on the model """ diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index cc95c0c5f0d..353798133db 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -22,7 +22,7 @@ from pyomo.common.config import ConfigValue from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.param import _ParamData @@ -438,7 +438,7 @@ def _process_domain_and_bounds( return lb, ub, vtype - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): var_names = list() vtypes = list() lbs = list() @@ -735,7 +735,7 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): del self._pyomo_sos_to_solver_sos_map[con] self._needs_updated = True - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): for var in variables: v_id = id(var) if var in self._vars_added_since_update: @@ -750,7 +750,7 @@ def _remove_variables(self, variables: List[_GeneralVarData]): def _remove_parameters(self, params: List[_ParamData]): pass - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): for var in variables: var_id = id(var) if var_id not in self._pyomo_var_to_solver_var_map: @@ -1151,7 +1151,7 @@ def set_var_attr(self, var, attr, val): Parameters ---------- - var: pyomo.core.base.var._GeneralVarData + var: pyomo.core.base.var.GeneralVarData The pyomo var for which the corresponding gurobi var attribute should be modified. attr: str @@ -1186,7 +1186,7 @@ def get_var_attr(self, var, attr): Parameters ---------- - var: pyomo.core.base.var._GeneralVarData + var: pyomo.core.base.var.GeneralVarData The pyomo var for which the corresponding gurobi var attribute should be retrieved. attr: str diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 5f601b7a9f7..7111ec6e972 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -25,7 +25,7 @@ ) from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.staleflag import StaleFlagManager from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo from pyomo.contrib.solver.base import SolverBase @@ -80,8 +80,8 @@ def __init__( class IpoptSolutionLoader(SolSolutionLoader): def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index 97a4067e78b..aeacc9f87c4 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -14,7 +14,7 @@ from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.param import _ParamData, Param from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.collections import ComponentMap @@ -54,10 +54,10 @@ def set_instance(self, model): self.set_objective(None) @abc.abstractmethod - def _add_variables(self, variables: List[_GeneralVarData]): + def _add_variables(self, variables: List[GeneralVarData]): pass - def add_variables(self, variables: List[_GeneralVarData]): + def add_variables(self, variables: List[GeneralVarData]): for v in variables: if id(v) in self._referenced_variables: raise ValueError( @@ -87,7 +87,7 @@ def add_parameters(self, params: List[_ParamData]): def _add_constraints(self, cons: List[GeneralConstraintData]): pass - def _check_for_new_vars(self, variables: List[_GeneralVarData]): + def _check_for_new_vars(self, variables: List[GeneralVarData]): new_vars = {} for v in variables: v_id = id(v) @@ -95,7 +95,7 @@ def _check_for_new_vars(self, variables: List[_GeneralVarData]): new_vars[v_id] = v self.add_variables(list(new_vars.values())) - def _check_to_remove_vars(self, variables: List[_GeneralVarData]): + def _check_to_remove_vars(self, variables: List[GeneralVarData]): vars_to_remove = {} for v in variables: v_id = id(v) @@ -250,10 +250,10 @@ def remove_sos_constraints(self, cons: List[_SOSConstraintData]): del self._vars_referenced_by_con[con] @abc.abstractmethod - def _remove_variables(self, variables: List[_GeneralVarData]): + def _remove_variables(self, variables: List[GeneralVarData]): pass - def remove_variables(self, variables: List[_GeneralVarData]): + def remove_variables(self, variables: List[GeneralVarData]): self._remove_variables(variables) for v in variables: v_id = id(v) @@ -309,10 +309,10 @@ def remove_block(self, block): ) @abc.abstractmethod - def _update_variables(self, variables: List[_GeneralVarData]): + def _update_variables(self, variables: List[GeneralVarData]): pass - def update_variables(self, variables: List[_GeneralVarData]): + def update_variables(self, variables: List[GeneralVarData]): for v in variables: self._vars[id(v)] = ( v, diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index e8c4631e7fd..3f327c1f280 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -13,7 +13,7 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.expr import value from pyomo.common.collections import ComponentMap from pyomo.common.errors import DeveloperError @@ -31,7 +31,7 @@ class SolutionLoaderBase(abc.ABC): """ def load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None ) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -49,8 +49,8 @@ def load_vars( @abc.abstractmethod def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: """ Returns a ComponentMap mapping variable to var value. @@ -86,8 +86,8 @@ def get_duals( raise NotImplementedError(f'{type(self)} does not support the get_duals method') def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: """ Returns a ComponentMap mapping variable to reduced cost. @@ -127,8 +127,8 @@ def get_duals( return self._solver._get_duals(cons_to_load=cons_to_load) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: self._assert_solution_still_valid() return self._solver._get_reduced_costs(vars_to_load=vars_to_load) @@ -142,7 +142,7 @@ def __init__(self, sol_data: SolFileData, nl_info: NLWriterInfo) -> None: self._nl_info = nl_info def load_vars( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None ) -> NoReturn: if self._nl_info is None: raise RuntimeError( @@ -169,8 +169,8 @@ def load_vars( StaleFlagManager.mark_all_as_stale(delayed=True) def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 38d6a540836..608af04a0ed 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -16,7 +16,7 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.common.collections import ComponentMap from pyomo.contrib.solver import results from pyomo.contrib.solver import solution @@ -51,8 +51,8 @@ def __init__( self._reduced_costs = reduced_costs def get_primals( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if self._primals is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' @@ -84,8 +84,8 @@ def get_duals( return duals def get_reduced_costs( - self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None - ) -> Mapping[_GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[GeneralVarData]] = None + ) -> Mapping[GeneralVarData, float]: if self._reduced_costs is None: raise RuntimeError( 'Solution loader does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/trustregion/tests/test_interface.py b/pyomo/contrib/trustregion/tests/test_interface.py index 148caceddd1..64f76eb887d 100644 --- a/pyomo/contrib/trustregion/tests/test_interface.py +++ b/pyomo/contrib/trustregion/tests/test_interface.py @@ -33,7 +33,7 @@ cos, SolverFactory, ) -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.expr.numeric_expr import ExternalFunctionExpression from pyomo.core.expr.visitor import identify_variables from pyomo.contrib.trustregion.interface import TRFInterface @@ -158,7 +158,7 @@ def test_replaceExternalFunctionsWithVariables(self): self.assertIsInstance(k, ExternalFunctionExpression) self.assertIn(str(self.interface.model.x[0]), str(k)) self.assertIn(str(self.interface.model.x[1]), str(k)) - self.assertIsInstance(i, _GeneralVarData) + self.assertIsInstance(i, GeneralVarData) self.assertEqual(i, self.interface.data.ef_outputs[1]) for i, k in self.interface.data.basis_expressions.items(): self.assertEqual(k, 0) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index bb62cb96782..7003cc3d720 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -57,7 +57,7 @@ from pyomo.core.base.check import BuildCheck from pyomo.core.base.set import Set, SetOf, simple_set_rule, RangeSet from pyomo.core.base.param import Param -from pyomo.core.base.var import Var, _VarData, _GeneralVarData, ScalarVar, VarList +from pyomo.core.base.var import Var, _VarData, GeneralVarData, ScalarVar, VarList from pyomo.core.base.boolean_var import ( BooleanVar, BooleanVarData, diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 1fd30f4212e..faf6553be1b 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -805,7 +805,7 @@ class ComponentData(_ComponentBase): # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, _LogicalConstraintData, # GeneralLogicalConstraintData, GeneralObjectiveData, - # _ParamData,_GeneralVarData, GeneralBooleanVarData, DisjunctionData, + # _ParamData,GeneralVarData, GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index 856a2dc0237..0e45ad44225 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -319,7 +319,7 @@ def free(self): return self.unfix() -class _GeneralVarData(_VarData): +class GeneralVarData(_VarData): """This class defines the data for a single variable.""" __slots__ = ('_value', '_lb', '_ub', '_domain', '_fixed', '_stale') @@ -643,6 +643,11 @@ def _process_bound(self, val, bound_type): return val +class _GeneralVarData(metaclass=RenamedClass): + __renamed__new_class__ = GeneralVarData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("Decision variables.") class Var(IndexedComponent, IndexedComponent_NDArrayMixin): """A numeric variable, which may be defined over an index. @@ -668,7 +673,7 @@ class Var(IndexedComponent, IndexedComponent_NDArrayMixin): doc (str, optional): Text describing this component. """ - _ComponentDataClass = _GeneralVarData + _ComponentDataClass = GeneralVarData @overload def __new__(cls: Type[Var], *args, **kwargs) -> Union[ScalarVar, IndexedVar]: ... @@ -952,11 +957,11 @@ def _pprint(self): ) -class ScalarVar(_GeneralVarData, Var): +class ScalarVar(GeneralVarData, Var): """A single variable.""" def __init__(self, *args, **kwd): - _GeneralVarData.__init__(self, component=self) + GeneralVarData.__init__(self, component=self) Var.__init__(self, *args, **kwd) self._index = UnindexedComponent_index @@ -1057,7 +1062,7 @@ def domain(self, domain): # between potentially variable GetItemExpression objects and # "constant" GetItemExpression objects. That will need to wait for # the expression rework [JDS; Nov 22]. - def __getitem__(self, args) -> _GeneralVarData: + def __getitem__(self, args) -> GeneralVarData: try: return super().__getitem__(args) except RuntimeError: diff --git a/pyomo/core/expr/calculus/derivatives.py b/pyomo/core/expr/calculus/derivatives.py index ecfdce02fd4..cd23cb16b2c 100644 --- a/pyomo/core/expr/calculus/derivatives.py +++ b/pyomo/core/expr/calculus/derivatives.py @@ -39,11 +39,11 @@ def differentiate(expr, wrt=None, wrt_list=None, mode=Modes.reverse_numeric): ---------- expr: pyomo.core.expr.numeric_expr.NumericExpression The expression to differentiate - wrt: pyomo.core.base.var._GeneralVarData + wrt: pyomo.core.base.var.GeneralVarData If specified, this function will return the derivative with - respect to wrt. wrt is normally a _GeneralVarData, but could + respect to wrt. wrt is normally a GeneralVarData, but could also be a _ParamData. wrt and wrt_list cannot both be specified. - wrt_list: list of pyomo.core.base.var._GeneralVarData + wrt_list: list of pyomo.core.base.var.GeneralVarData If specified, this function will return the derivative with respect to each element in wrt_list. A list will be returned where the values are the derivatives with respect to the diff --git a/pyomo/core/tests/transform/test_add_slacks.py b/pyomo/core/tests/transform/test_add_slacks.py index a74a9b75c4f..d66d6fba79e 100644 --- a/pyomo/core/tests/transform/test_add_slacks.py +++ b/pyomo/core/tests/transform/test_add_slacks.py @@ -330,7 +330,7 @@ def test_error_for_non_constraint_noniterable_target(self): self.assertRaisesRegex( ValueError, "Expected Constraint or list of Constraints.\n\tReceived " - "", + "", TransformationFactory('core.add_slack_variables').apply_to, m, targets=m.indexedVar[1], diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index 6dd8a21e2b4..f2c3cad8cc3 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -17,7 +17,7 @@ ObjectiveDict, ExpressionDict, ) -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -348,10 +348,10 @@ def test_active(self): class TestVarDict(_TestComponentDictBase, unittest.TestCase): - # Note: the updated _GeneralVarData class only takes an optional + # Note: the updated GeneralVarData class only takes an optional # parent argument (you no longer pass the domain in) _ctype = VarDict - _cdatatype = lambda self, arg: _GeneralVarData() + _cdatatype = lambda self, arg: GeneralVarData() def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index 1609f97af90..fcc83a95a06 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -17,7 +17,7 @@ XObjectiveList, XExpressionList, ) -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -365,10 +365,10 @@ def test_active(self): class TestVarList(_TestComponentListBase, unittest.TestCase): - # Note: the updated _GeneralVarData class only takes an optional + # Note: the updated GeneralVarData class only takes an optional # parent argument (you no longer pass the domain in) _ctype = XVarList - _cdatatype = lambda self, arg: _GeneralVarData() + _cdatatype = lambda self, arg: GeneralVarData() def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index 968b3acb6a4..8e5e43eac9c 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -112,7 +112,7 @@ from pyomo.core.base.label import NumericLabeler from pyomo.core.expr.template_expr import IndexTemplate from pyomo.core.expr import expr_common -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.repn import generate_standard_repn from pyomo.core.expr.numvalue import NumericValue @@ -294,7 +294,7 @@ def value_check(self, exp, val): class TestExpression_EvaluateVarData(TestExpression_EvaluateNumericValue): def create(self, val, domain): - tmp = _GeneralVarData() + tmp = GeneralVarData() tmp.domain = domain tmp.value = val return tmp diff --git a/pyomo/core/tests/unit/test_reference.py b/pyomo/core/tests/unit/test_reference.py index cfd9b99f945..4fa2f4944e9 100644 --- a/pyomo/core/tests/unit/test_reference.py +++ b/pyomo/core/tests/unit/test_reference.py @@ -800,8 +800,8 @@ def test_reference_indexedcomponent_pprint(self): buf.getvalue(), """r : Size=2, Index={1, 2}, ReferenceTo=x Key : Object - 1 : - 2 : + 1 : + 2 : """, ) m.s = Reference(m.x[:, ...], ctype=IndexedComponent) @@ -811,8 +811,8 @@ def test_reference_indexedcomponent_pprint(self): buf.getvalue(), """s : Size=2, Index={1, 2}, ReferenceTo=x[:, ...] Key : Object - 1 : - 2 : + 1 : + 2 : """, ) @@ -1357,8 +1357,8 @@ def test_pprint_nonfinite_sets_ctypeNone(self): 1 IndexedComponent Declarations ref : Size=2, Index=NonNegativeIntegers, ReferenceTo=v Key : Object - 3 : - 5 : + 3 : + 5 : 2 Declarations: v ref """.strip(), diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 442e4677dbd..907b4a2b115 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -22,7 +22,7 @@ from pyomo.core.base.objective import GeneralObjectiveData, ScalarObjective from pyomo.core.base import ExpressionData, Expression from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData -from pyomo.core.base.var import ScalarVar, Var, _GeneralVarData, value +from pyomo.core.base.var import ScalarVar, Var, GeneralVarData, value from pyomo.core.base.param import ScalarParam, _ParamData from pyomo.core.kernel.expression import expression, noclone from pyomo.core.kernel.variable import IVariable, variable @@ -1143,7 +1143,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra # param.Param : _collect_linear_const, # parameter : _collect_linear_const, NumericConstant: _collect_const, - _GeneralVarData: _collect_var, + GeneralVarData: _collect_var, ScalarVar: _collect_var, Var: _collect_var, variable: _collect_var, @@ -1542,7 +1542,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): ##param.ScalarParam : _collect_linear_const, ##param.Param : _collect_linear_const, ##parameter : _collect_linear_const, - _GeneralVarData : _linear_collect_var, + GeneralVarData : _linear_collect_var, ScalarVar : _linear_collect_var, Var : _linear_collect_var, variable : _linear_collect_var, diff --git a/pyomo/solvers/plugins/solvers/gurobi_persistent.py b/pyomo/solvers/plugins/solvers/gurobi_persistent.py index 101a5340ea9..8a81aad3d3e 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_persistent.py +++ b/pyomo/solvers/plugins/solvers/gurobi_persistent.py @@ -192,7 +192,7 @@ def set_var_attr(self, var, attr, val): Parameters ---------- - con: pyomo.core.base.var._GeneralVarData + con: pyomo.core.base.var.GeneralVarData The pyomo var for which the corresponding gurobi var attribute should be modified. attr: str @@ -342,7 +342,7 @@ def get_var_attr(self, var, attr): Parameters ---------- - var: pyomo.core.base.var._GeneralVarData + var: pyomo.core.base.var.GeneralVarData The pyomo var for which the corresponding gurobi var attribute should be retrieved. attr: str diff --git a/pyomo/util/report_scaling.py b/pyomo/util/report_scaling.py index 5ae28baa715..265564bf12d 100644 --- a/pyomo/util/report_scaling.py +++ b/pyomo/util/report_scaling.py @@ -13,7 +13,7 @@ import math from pyomo.core.base.block import BlockData from pyomo.common.collections import ComponentSet -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.var import GeneralVarData from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd import logging @@ -73,7 +73,7 @@ def _check_coefficients( ): ders = reverse_sd(expr) for _v, _der in ders.items(): - if isinstance(_v, _GeneralVarData): + if isinstance(_v, GeneralVarData): if _v.is_fixed(): continue der_lb, der_ub = compute_bounds_on_expr(_der) From edd83c00ba974bf9d73b95b0e084ed0b63eee71a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:48:36 -0600 Subject: [PATCH 0996/3044] Renamed _InfiniteRangeSetData -> InfiniteRangeSetData --- pyomo/core/base/set.py | 23 ++++++++++++++--------- pyomo/core/tests/unit/test_set.py | 4 ++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 8db64620d5c..f885dfeaa16 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -894,7 +894,7 @@ def _get_continuous_interval(self): @property @deprecated("The 'virtual' attribute is no longer supported", version='5.7') def virtual(self): - return isinstance(self, (_AnySet, SetOperator, _InfiniteRangeSetData)) + return isinstance(self, (_AnySet, SetOperator, InfiniteRangeSetData)) @virtual.setter def virtual(self, value): @@ -2608,7 +2608,7 @@ def ord(self, item): ############################################################################ -class _InfiniteRangeSetData(_SetData): +class InfiniteRangeSetData(_SetData): """Data class for a infinite set. This Set implements an interface to an *infinite set* defined by one @@ -2653,8 +2653,13 @@ def ranges(self): return iter(self._ranges) +class _InfiniteRangeSetData(metaclass=RenamedClass): + __renamed__new_class__ = InfiniteRangeSetData + __renamed__version__ = '6.7.2.dev0' + + class FiniteRangeSetData( - _SortedSetMixin, _OrderedSetMixin, _FiniteSetMixin, _InfiniteRangeSetData + _SortedSetMixin, _OrderedSetMixin, _FiniteSetMixin, InfiniteRangeSetData ): __slots__ = () @@ -2754,11 +2759,11 @@ def ord(self, item): ) # We must redefine ranges(), bounds(), and domain so that we get the - # _InfiniteRangeSetData version and not the one from + # InfiniteRangeSetData version and not the one from # _FiniteSetMixin. - bounds = _InfiniteRangeSetData.bounds - ranges = _InfiniteRangeSetData.ranges - domain = _InfiniteRangeSetData.domain + bounds = InfiniteRangeSetData.bounds + ranges = InfiniteRangeSetData.ranges + domain = InfiniteRangeSetData.domain class _FiniteRangeSetData(metaclass=RenamedClass): @@ -3228,9 +3233,9 @@ def _pprint(self): ) -class InfiniteScalarRangeSet(_InfiniteRangeSetData, RangeSet): +class InfiniteScalarRangeSet(InfiniteRangeSetData, RangeSet): def __init__(self, *args, **kwds): - _InfiniteRangeSetData.__init__(self, component=self) + InfiniteRangeSetData.__init__(self, component=self) RangeSet.__init__(self, *args, **kwds) self._index = UnindexedComponent_index diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index a9b9fb9469b..d669bb38f3b 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -61,7 +61,7 @@ InfiniteSetOf, RangeSet, FiniteRangeSetData, - _InfiniteRangeSetData, + InfiniteRangeSetData, FiniteScalarRangeSet, InfiniteScalarRangeSet, AbstractFiniteScalarRangeSet, @@ -1297,7 +1297,7 @@ def test_is_functions(self): self.assertFalse(i.isdiscrete()) self.assertFalse(i.isfinite()) self.assertFalse(i.isordered()) - self.assertIsInstance(i, _InfiniteRangeSetData) + self.assertIsInstance(i, InfiniteRangeSetData) def test_pprint(self): m = ConcreteModel() From b5c9dbaee26359c842dfeee3d8b962da81a7f513 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:48:49 -0600 Subject: [PATCH 0997/3044] Renamed _InsertionOrderSetData -> InsertionOrderSetData --- pyomo/core/base/set.py | 23 +++++++++++++-------- pyomo/core/tests/unit/test_set.py | 8 +++---- pyomo/core/tests/unit/test_template_expr.py | 2 +- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index f885dfeaa16..c0e9491ed1c 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1642,9 +1642,9 @@ class _OrderedSetData(_OrderedSetMixin, FiniteSetData): In older Pyomo terms, this defines a "concrete" ordered set - that is, a set that "owns" the list of set members. While this class actually implements a set ordered by insertion order, we make the "official" - _InsertionOrderSetData an empty derivative class, so that + InsertionOrderSetData an empty derivative class, so that - issubclass(_SortedSetData, _InsertionOrderSetData) == False + issubclass(_SortedSetData, InsertionOrderSetData) == False Constructor Arguments: component The Set object that owns this data. @@ -1735,7 +1735,7 @@ def ord(self, item): raise ValueError("%s.ord(x): x not in %s" % (self.name, self.name)) -class _InsertionOrderSetData(_OrderedSetData): +class InsertionOrderSetData(_OrderedSetData): """ This class defines the data for a ordered set where the items are ordered in insertion order (similar to Python's OrderedSet. @@ -1756,7 +1756,7 @@ def set_value(self, val): "This WILL potentially lead to nondeterministic behavior " "in Pyomo" % (type(val).__name__,) ) - super(_InsertionOrderSetData, self).set_value(val) + super(InsertionOrderSetData, self).set_value(val) def update(self, values): if type(values) in Set._UnorderedInitializers: @@ -1766,7 +1766,12 @@ def update(self, values): "This WILL potentially lead to nondeterministic behavior " "in Pyomo" % (type(values).__name__,) ) - super(_InsertionOrderSetData, self).update(values) + super(InsertionOrderSetData, self).update(values) + + +class _InsertionOrderSetData(metaclass=RenamedClass): + __renamed__new_class__ = InsertionOrderSetData + __renamed__version__ = '6.7.2.dev0' class _SortedSetMixin(object): @@ -2035,7 +2040,7 @@ def __new__(cls, *args, **kwds): else: newObj = super(Set, cls).__new__(IndexedSet) if ordered is Set.InsertionOrder: - newObj._ComponentDataClass = _InsertionOrderSetData + newObj._ComponentDataClass = InsertionOrderSetData elif ordered is Set.SortedOrder: newObj._ComponentDataClass = _SortedSetData else: @@ -2363,7 +2368,7 @@ def _pprint(self): _ordered = "Sorted" else: _ordered = "{user}" - elif issubclass(_refClass, _InsertionOrderSetData): + elif issubclass(_refClass, InsertionOrderSetData): _ordered = "Insertion" return ( [ @@ -2405,13 +2410,13 @@ class FiniteSimpleSet(metaclass=RenamedClass): __renamed__version__ = '6.0' -class OrderedScalarSet(_ScalarOrderedSetMixin, _InsertionOrderSetData, Set): +class OrderedScalarSet(_ScalarOrderedSetMixin, InsertionOrderSetData, Set): def __init__(self, **kwds): # In case someone inherits from us, we will provide a rational # default for the "ordered" flag kwds.setdefault('ordered', Set.InsertionOrder) - _InsertionOrderSetData.__init__(self, component=self) + InsertionOrderSetData.__init__(self, component=self) Set.__init__(self, **kwds) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index d669bb38f3b..38870d5213e 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -83,7 +83,7 @@ SetProduct_OrderedSet, _SetData, FiniteSetData, - _InsertionOrderSetData, + InsertionOrderSetData, _SortedSetData, _FiniteSetMixin, _OrderedSetMixin, @@ -4155,9 +4155,9 @@ def test_indexed_set(self): self.assertTrue(m.I[1].isordered()) self.assertTrue(m.I[2].isordered()) self.assertTrue(m.I[3].isordered()) - self.assertIs(type(m.I[1]), _InsertionOrderSetData) - self.assertIs(type(m.I[2]), _InsertionOrderSetData) - self.assertIs(type(m.I[3]), _InsertionOrderSetData) + self.assertIs(type(m.I[1]), InsertionOrderSetData) + self.assertIs(type(m.I[2]), InsertionOrderSetData) + self.assertIs(type(m.I[3]), InsertionOrderSetData) self.assertEqual(m.I.data(), {1: (4, 2, 5), 2: (4, 2, 5), 3: (4, 2, 5)}) # Explicit (constant) construction diff --git a/pyomo/core/tests/unit/test_template_expr.py b/pyomo/core/tests/unit/test_template_expr.py index 4f255e3567a..e6bd9d98a7d 100644 --- a/pyomo/core/tests/unit/test_template_expr.py +++ b/pyomo/core/tests/unit/test_template_expr.py @@ -127,7 +127,7 @@ def test_template_scalar_with_set(self): # Note that structural expressions do not implement polynomial_degree with self.assertRaisesRegex( AttributeError, - "'_InsertionOrderSetData' object has " "no attribute 'polynomial_degree'", + "'InsertionOrderSetData' object has " "no attribute 'polynomial_degree'", ): e.polynomial_degree() self.assertEqual(str(e), "s[{I}]") From d4d522a4e9b38b17f1944cdbab4292da2b11a220 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:49:48 -0600 Subject: [PATCH 0998/3044] Renamed _LogicalConstraintData -> LogicalConstraintData --- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/component.py | 2 +- pyomo/core/base/logical_constraint.py | 13 +++++++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 7003cc3d720..7d1bd1401f7 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -75,7 +75,7 @@ from pyomo.core.base.logical_constraint import ( LogicalConstraint, LogicalConstraintList, - _LogicalConstraintData, + LogicalConstraintData, ) from pyomo.core.base.objective import ( simple_objective_rule, diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index faf6553be1b..341cd1506ff 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -803,7 +803,7 @@ class ComponentData(_ComponentBase): # NOTE: This constructor is in-lined in the constructors for the following # classes: BooleanVarData, ConnectorData, ConstraintData, - # GeneralExpressionData, _LogicalConstraintData, + # GeneralExpressionData, LogicalConstraintData, # GeneralLogicalConstraintData, GeneralObjectiveData, # _ParamData,GeneralVarData, GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index 9af99c9ce5c..23a422705df 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -42,7 +42,7 @@ """ -class _LogicalConstraintData(ActiveComponentData): +class LogicalConstraintData(ActiveComponentData): """ This class defines the data for a single logical constraint. @@ -99,7 +99,12 @@ def get_value(self): raise NotImplementedError -class GeneralLogicalConstraintData(_LogicalConstraintData): +class _LogicalConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = LogicalConstraintData + __renamed__version__ = '6.7.2.dev0' + + +class GeneralLogicalConstraintData(LogicalConstraintData): """ This class defines the data for a single general logical constraint. @@ -123,7 +128,7 @@ def __init__(self, expr=None, component=None): # # These lines represent in-lining of the # following constructors: - # - _LogicalConstraintData, + # - LogicalConstraintData, # - ActiveComponentData # - ComponentData self._component = weakref_ref(component) if (component is not None) else None @@ -455,7 +460,7 @@ def body(self): # currently in place). So during initialization only, we will # treat them as "indexed" objects where things like # True are managed. But after that they will behave - # like _LogicalConstraintData objects where set_value expects + # like LogicalConstraintData objects where set_value expects # a valid expression or None. # From c37fc4fb13794b35e94e86b3a185494d2383f432 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:50:02 -0600 Subject: [PATCH 0999/3044] Renamed _ObjectiveData -> ObjectiveData --- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/objective.py | 11 ++++++++--- pyomo/core/beta/dict_objects.py | 4 ++-- pyomo/core/beta/list_objects.py | 4 ++-- pyomo/core/plugins/transform/scaling.py | 4 ++-- pyomo/core/tests/unit/test_obj.py | 2 +- pyomo/core/tests/unit/test_suffix.py | 4 ++-- pyomo/repn/plugins/nl_writer.py | 6 +++--- 8 files changed, 21 insertions(+), 16 deletions(-) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 7d1bd1401f7..408cf16c00e 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -82,7 +82,7 @@ simple_objectivelist_rule, Objective, ObjectiveList, - _ObjectiveData, + ObjectiveData, ) from pyomo.core.base.connector import Connector from pyomo.core.base.sos import SOSConstraint diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index b89214377ab..58cb198e1ae 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -86,7 +86,7 @@ def O_rule(model, i, j): # -class _ObjectiveData(ExpressionData): +class ObjectiveData(ExpressionData): """ This class defines the data for a single objective. @@ -119,8 +119,13 @@ def set_sense(self, sense): raise NotImplementedError +class _ObjectiveData(metaclass=RenamedClass): + __renamed__new_class__ = ObjectiveData + __renamed__version__ = '6.7.2.dev0' + + class GeneralObjectiveData( - GeneralExpressionDataImpl, _ObjectiveData, ActiveComponentData + GeneralExpressionDataImpl, ObjectiveData, ActiveComponentData ): """ This class defines the data for a single objective. @@ -479,7 +484,7 @@ def sense(self, sense): # currently in place). So during initialization only, we will # treat them as "indexed" objects where things like # Objective.Skip are managed. But after that they will behave - # like _ObjectiveData objects where set_value does not handle + # like ObjectiveData objects where set_value does not handle # Objective.Skip but expects a valid expression or None # diff --git a/pyomo/core/beta/dict_objects.py b/pyomo/core/beta/dict_objects.py index 2b23d81e91a..7c44166f189 100644 --- a/pyomo/core/beta/dict_objects.py +++ b/pyomo/core/beta/dict_objects.py @@ -16,7 +16,7 @@ from pyomo.core.base.set_types import Any from pyomo.core.base.var import IndexedVar, _VarData from pyomo.core.base.constraint import IndexedConstraint, ConstraintData -from pyomo.core.base.objective import IndexedObjective, _ObjectiveData +from pyomo.core.base.objective import IndexedObjective, ObjectiveData from pyomo.core.base.expression import IndexedExpression, ExpressionData from collections.abc import MutableMapping @@ -202,7 +202,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentDict needs to # go last in order to handle any initialization # iterable as an argument - ComponentDict.__init__(self, _ObjectiveData, *args, **kwds) + ComponentDict.__init__(self, ObjectiveData, *args, **kwds) class ExpressionDict(ComponentDict, IndexedExpression): diff --git a/pyomo/core/beta/list_objects.py b/pyomo/core/beta/list_objects.py index dd199eb70cd..d10a30e18e2 100644 --- a/pyomo/core/beta/list_objects.py +++ b/pyomo/core/beta/list_objects.py @@ -16,7 +16,7 @@ from pyomo.core.base.set_types import Any from pyomo.core.base.var import IndexedVar, _VarData from pyomo.core.base.constraint import IndexedConstraint, ConstraintData -from pyomo.core.base.objective import IndexedObjective, _ObjectiveData +from pyomo.core.base.objective import IndexedObjective, ObjectiveData from pyomo.core.base.expression import IndexedExpression, ExpressionData from collections.abc import MutableSequence @@ -250,7 +250,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentList needs to # go last in order to handle any initialization # iterable as an argument - ComponentList.__init__(self, _ObjectiveData, *args, **kwds) + ComponentList.__init__(self, ObjectiveData, *args, **kwds) class XExpressionList(ComponentList, IndexedExpression): diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index 6b83a2378d1..ef418f094ae 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -16,7 +16,7 @@ Constraint, Objective, ConstraintData, - _ObjectiveData, + ObjectiveData, Suffix, value, ) @@ -226,7 +226,7 @@ def _apply_to(self, model, rename=True): else: c.set_value((lower, body, upper)) - elif isinstance(c, _ObjectiveData): + elif isinstance(c, ObjectiveData): c.expr = scaling_factor * replace_expressions( expr=c.expr, substitution_map=variable_substitution_dict, diff --git a/pyomo/core/tests/unit/test_obj.py b/pyomo/core/tests/unit/test_obj.py index 3c8a05f7058..dc2e320e63b 100644 --- a/pyomo/core/tests/unit/test_obj.py +++ b/pyomo/core/tests/unit/test_obj.py @@ -78,7 +78,7 @@ def test_empty_singleton(self): # Even though we construct a ScalarObjective, # if it is not initialized that means it is "empty" # and we should encounter errors when trying to access the - # _ObjectiveData interface methods until we assign + # ObjectiveData interface methods until we assign # something to the objective. # self.assertEqual(a._constructed, True) diff --git a/pyomo/core/tests/unit/test_suffix.py b/pyomo/core/tests/unit/test_suffix.py index 9597bad7571..70f028a3eff 100644 --- a/pyomo/core/tests/unit/test_suffix.py +++ b/pyomo/core/tests/unit/test_suffix.py @@ -1567,7 +1567,7 @@ def test_clone_ObjectiveArray(self): self.assertEqual(inst.junk.get(model.obj[1]), None) self.assertEqual(inst.junk.get(inst.obj[1]), 1.0) - def test_clone_ObjectiveData(self): + def test_cloneObjectiveData(self): model = ConcreteModel() model.x = Var([1, 2, 3], dense=True) model.obj = Objective([1, 2, 3], rule=lambda model, i: model.x[i]) @@ -1725,7 +1725,7 @@ def test_pickle_ObjectiveArray(self): self.assertEqual(inst.junk.get(model.obj[1]), None) self.assertEqual(inst.junk.get(inst.obj[1]), 1.0) - def test_pickle_ObjectiveData(self): + def test_pickleObjectiveData(self): model = ConcreteModel() model.x = Var([1, 2, 3], dense=True) model.obj = Objective([1, 2, 3], rule=simple_obj_rule) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index e1afb5720f3..c010cee5e54 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -74,7 +74,7 @@ from pyomo.core.base.objective import ( ScalarObjective, GeneralObjectiveData, - _ObjectiveData, + ObjectiveData, ) from pyomo.core.base.suffix import SuffixFinder from pyomo.core.base.var import _VarData @@ -139,7 +139,7 @@ class NLWriterInfo(object): The list of (active) Pyomo model constraints in the order written to the NL file - objectives: List[_ObjectiveData] + objectives: List[ObjectiveData] The list of (active) Pyomo model objectives in the order written to the NL file @@ -466,7 +466,7 @@ def compile(self, column_order, row_order, obj_order, model_id): self.obj[obj_order[_id]] = val elif _id == model_id: self.prob[0] = val - elif isinstance(obj, (_VarData, ConstraintData, _ObjectiveData)): + elif isinstance(obj, (_VarData, ConstraintData, ObjectiveData)): missing_component_data.add(obj) elif isinstance(obj, (Var, Constraint, Objective)): # Expand this indexed component to store the From 4127432baf064f55d2f09d7c264109734e70f4b7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:50:05 -0600 Subject: [PATCH 1000/3044] Renamed _OrderedSetData -> OrderedSetData --- pyomo/core/base/set.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index c0e9491ed1c..c7a7edc8e4b 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1301,7 +1301,7 @@ class FiniteSetData(_FiniteSetMixin, _SetData): def __init__(self, component): _SetData.__init__(self, component=component) - # Derived classes (like _OrderedSetData) may want to change the + # Derived classes (like OrderedSetData) may want to change the # storage if not hasattr(self, '_values'): self._values = set() @@ -1635,7 +1635,7 @@ def _to_0_based_index(self, item): ) -class _OrderedSetData(_OrderedSetMixin, FiniteSetData): +class OrderedSetData(_OrderedSetMixin, FiniteSetData): """ This class defines the base class for an ordered set of concrete data. @@ -1735,7 +1735,12 @@ def ord(self, item): raise ValueError("%s.ord(x): x not in %s" % (self.name, self.name)) -class InsertionOrderSetData(_OrderedSetData): +class _OrderedSetData(metaclass=RenamedClass): + __renamed__new_class__ = OrderedSetData + __renamed__version__ = '6.7.2.dev0' + + +class InsertionOrderSetData(OrderedSetData): """ This class defines the data for a ordered set where the items are ordered in insertion order (similar to Python's OrderedSet. @@ -1786,7 +1791,7 @@ def sorted_iter(self): return iter(self) -class _SortedSetData(_SortedSetMixin, _OrderedSetData): +class _SortedSetData(_SortedSetMixin, OrderedSetData): """ This class defines the data for a sorted set. @@ -1801,7 +1806,7 @@ class _SortedSetData(_SortedSetMixin, _OrderedSetData): def __init__(self, component): # An empty set is sorted... self._is_sorted = True - _OrderedSetData.__init__(self, component=component) + OrderedSetData.__init__(self, component=component) def _iter_impl(self): """ From ec3f121f81a4f52295caab029d5bfb5e826c569e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:51:57 -0600 Subject: [PATCH 1001/3044] Renamed _ParamData -> ParamData --- pyomo/contrib/appsi/base.py | 14 ++++---- pyomo/contrib/appsi/fbbt.py | 6 ++-- pyomo/contrib/appsi/solvers/cbc.py | 6 ++-- pyomo/contrib/appsi/solvers/cplex.py | 6 ++-- pyomo/contrib/appsi/solvers/gurobi.py | 6 ++-- pyomo/contrib/appsi/solvers/highs.py | 6 ++-- pyomo/contrib/appsi/solvers/ipopt.py | 6 ++-- pyomo/contrib/appsi/solvers/wntr.py | 6 ++-- pyomo/contrib/appsi/writers/lp_writer.py | 6 ++-- pyomo/contrib/appsi/writers/nl_writer.py | 6 ++-- pyomo/contrib/cp/repn/docplex_writer.py | 4 +-- .../logical_to_disjunctive_walker.py | 4 +-- pyomo/contrib/latex_printer/latex_printer.py | 12 +++---- pyomo/contrib/pyros/config.py | 8 ++--- pyomo/contrib/pyros/tests/test_config.py | 8 ++--- pyomo/contrib/solver/base.py | 6 ++-- pyomo/contrib/solver/gurobi.py | 6 ++-- pyomo/contrib/solver/persistent.py | 10 +++--- pyomo/contrib/viewer/model_browser.py | 8 ++--- pyomo/core/base/component.py | 2 +- pyomo/core/base/param.py | 35 +++++++++++-------- pyomo/core/expr/calculus/derivatives.py | 2 +- pyomo/core/tests/unit/test_param.py | 10 +++--- pyomo/core/tests/unit/test_visitor.py | 4 +-- pyomo/repn/plugins/ampl/ampl_.py | 8 ++--- pyomo/repn/standard_repn.py | 6 ++-- 26 files changed, 102 insertions(+), 99 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 1ce24220bfd..e50d5201090 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -24,7 +24,7 @@ from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint from pyomo.core.base.var import GeneralVarData, Var -from pyomo.core.base.param import _ParamData, Param +from pyomo.core.base.param import ParamData, Param from pyomo.core.base.block import BlockData, Block from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.collections import ComponentMap @@ -803,7 +803,7 @@ def add_variables(self, variables: List[GeneralVarData]): pass @abc.abstractmethod - def add_params(self, params: List[_ParamData]): + def add_params(self, params: List[ParamData]): pass @abc.abstractmethod @@ -819,7 +819,7 @@ def remove_variables(self, variables: List[GeneralVarData]): pass @abc.abstractmethod - def remove_params(self, params: List[_ParamData]): + def remove_params(self, params: List[ParamData]): pass @abc.abstractmethod @@ -975,10 +975,10 @@ def add_variables(self, variables: List[GeneralVarData]): self._add_variables(variables) @abc.abstractmethod - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): pass - def add_params(self, params: List[_ParamData]): + def add_params(self, params: List[ParamData]): for p in params: self._params[id(p)] = p self._add_params(params) @@ -1198,10 +1198,10 @@ def remove_variables(self, variables: List[GeneralVarData]): del self._vars[v_id] @abc.abstractmethod - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): pass - def remove_params(self, params: List[_ParamData]): + def remove_params(self, params: List[ParamData]): self._remove_params(params) for p in params: del self._params[id(p)] diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index a360d2bce84..7735318f8ba 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -19,7 +19,7 @@ from .cmodel import cmodel, cmodel_available from typing import List, Optional from pyomo.core.base.var import GeneralVarData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData from pyomo.core.base.objective import GeneralObjectiveData, minimize, maximize @@ -143,7 +143,7 @@ def _add_variables(self, variables: List[GeneralVarData]): False, ) - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): cparams = cmodel.create_params(len(params)) for ndx, p in enumerate(params): cp = cparams[ndx] @@ -198,7 +198,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): cvar = self._var_map.pop(id(v)) del self._rvar_map[cvar] - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): if self._symbolic_solver_labels: for p in params: self._symbol_map.removeSymbol(p) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index cd5158d905e..d03e6e31c54 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -29,7 +29,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream @@ -167,7 +167,7 @@ def set_instance(self, model): def add_variables(self, variables: List[GeneralVarData]): self._writer.add_variables(variables) - def add_params(self, params: List[_ParamData]): + def add_params(self, params: List[ParamData]): self._writer.add_params(params) def add_constraints(self, cons: List[GeneralConstraintData]): @@ -179,7 +179,7 @@ def add_block(self, block: BlockData): def remove_variables(self, variables: List[GeneralVarData]): self._writer.remove_variables(variables) - def remove_params(self, params: List[_ParamData]): + def remove_params(self, params: List[ParamData]): self._writer.remove_params(params) def remove_constraints(self, cons: List[GeneralConstraintData]): diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index cdd699105be..55259244d45 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -25,7 +25,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer import sys @@ -182,7 +182,7 @@ def set_instance(self, model): def add_variables(self, variables: List[GeneralVarData]): self._writer.add_variables(variables) - def add_params(self, params: List[_ParamData]): + def add_params(self, params: List[ParamData]): self._writer.add_params(params) def add_constraints(self, cons: List[GeneralConstraintData]): @@ -194,7 +194,7 @@ def add_block(self, block: BlockData): def remove_variables(self, variables: List[GeneralVarData]): self._writer.remove_variables(variables) - def remove_params(self, params: List[_ParamData]): + def remove_params(self, params: List[ParamData]): self._writer.remove_params(params) def remove_constraints(self, cons: List[GeneralConstraintData]): diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 6da59042a80..4392cdf0839 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -26,7 +26,7 @@ from pyomo.core.base.var import Var, GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression @@ -489,7 +489,7 @@ def _add_variables(self, variables: List[GeneralVarData]): self._vars_added_since_update.update(variables) self._needs_updated = True - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): pass def _reinit(self): @@ -771,7 +771,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): self._mutable_bounds.pop(v_id, None) self._needs_updated = True - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): pass def _update_variables(self, variables: List[GeneralVarData]): diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index ded0092f38b..a6b7c102c91 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -23,7 +23,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression @@ -335,7 +335,7 @@ def _add_variables(self, variables: List[GeneralVarData]): len(vtypes), np.array(indices), np.array(vtypes) ) - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): pass def _reinit(self): @@ -515,7 +515,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): self._pyomo_var_to_solver_var_map.clear() self._pyomo_var_to_solver_var_map.update(new_var_map) - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): pass def _update_variables(self, variables: List[GeneralVarData]): diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 9ccb58095b1..ca75a1b02c8 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -31,7 +31,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream @@ -231,7 +231,7 @@ def set_instance(self, model): def add_variables(self, variables: List[GeneralVarData]): self._writer.add_variables(variables) - def add_params(self, params: List[_ParamData]): + def add_params(self, params: List[ParamData]): self._writer.add_params(params) def add_constraints(self, cons: List[GeneralConstraintData]): @@ -243,7 +243,7 @@ def add_block(self, block: BlockData): def remove_variables(self, variables: List[GeneralVarData]): self._writer.remove_variables(variables) - def remove_params(self, params: List[_ParamData]): + def remove_params(self, params: List[ParamData]): self._writer.remove_params(params) def remove_constraints(self, cons: List[GeneralConstraintData]): diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 7f633161fe1..8f2650dabb6 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -41,7 +41,7 @@ from typing import Dict, Optional, List from pyomo.core.base.block import BlockData from pyomo.core.base.var import GeneralVarData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.common.timing import HierarchicalTimer from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler @@ -270,7 +270,7 @@ def _add_variables(self, variables: List[GeneralVarData]): ) self._needs_updated = True - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): aml = wntr.sim.aml.aml for p in params: pname = self._symbol_map.getSymbol(p, self._labeler) @@ -314,7 +314,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): del self._solver_model._wntr_fixed_var_cons[v_id] self._needs_updated = True - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): for p in params: p_id = id(p) solver_param = self._pyomo_param_to_solver_param_map[p_id] diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 94af5ba7e93..3a168cdcd91 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from typing import List -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData @@ -91,7 +91,7 @@ def _add_variables(self, variables: List[GeneralVarData]): False, ) - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): cparams = cmodel.create_params(len(params)) for ndx, p in enumerate(params): cp = cparams[ndx] @@ -123,7 +123,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): del self._solver_var_to_pyomo_var_map[cvar] self._symbol_map.removeSymbol(v) - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): for p in params: del self._pyomo_param_to_solver_param_map[id(p)] self._symbol_map.removeSymbol(p) diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index b7dab1d5a3e..fced3c5ae10 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from typing import List -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData @@ -100,7 +100,7 @@ def _add_variables(self, variables: List[GeneralVarData]): False, ) - def _add_params(self, params: List[_ParamData]): + def _add_params(self, params: List[ParamData]): cparams = cmodel.create_params(len(params)) for ndx, p in enumerate(params): cp = cparams[ndx] @@ -153,7 +153,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): cvar = self._pyomo_var_to_solver_var_map.pop(id(v)) del self._solver_var_to_pyomo_var_map[cvar] - def _remove_params(self, params: List[_ParamData]): + def _remove_params(self, params: List[ParamData]): if self.config.symbolic_solver_labels: for p in params: self._symbol_map.removeSymbol(p) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index eb50a543160..75095755895 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -64,7 +64,7 @@ IndexedBooleanVar, ) from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData -from pyomo.core.base.param import IndexedParam, ScalarParam, _ParamData +from pyomo.core.base.param import IndexedParam, ScalarParam, ParamData from pyomo.core.base.var import ScalarVar, GeneralVarData, IndexedVar import pyomo.core.expr as EXPR from pyomo.core.expr.visitor import StreamBasedExpressionVisitor, identify_variables @@ -970,7 +970,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): ScalarExpression: _before_named_expression, IndexedParam: _before_indexed_param, # Because of indirection ScalarParam: _before_param, - _ParamData: _before_param, + ParamData: _before_param, } def __init__(self, cpx_model, symbolic_solver_labels=False): diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index 26b63d020a5..a228b1561dd 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -28,7 +28,7 @@ ) import pyomo.core.base.boolean_var as BV from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData -from pyomo.core.base.param import ScalarParam, _ParamData +from pyomo.core.base.param import ScalarParam, ParamData from pyomo.core.base.var import ScalarVar, GeneralVarData from pyomo.gdp.disjunct import AutoLinkedBooleanVar, Disjunct, Disjunction @@ -211,7 +211,7 @@ def _dispatch_atmost(visitor, node, *args): _before_child_dispatcher[BV.ScalarBooleanVar] = _dispatch_boolean_var _before_child_dispatcher[BV.GeneralBooleanVarData] = _dispatch_boolean_var _before_child_dispatcher[AutoLinkedBooleanVar] = _dispatch_boolean_var -_before_child_dispatcher[_ParamData] = _dispatch_param +_before_child_dispatcher[ParamData] = _dispatch_param _before_child_dispatcher[ScalarParam] = _dispatch_param # for the moment, these are all just so we can get good error messages when we # don't handle them: diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index efcd3016dbf..dec058bb5ba 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -48,7 +48,7 @@ templatize_rule, ) from pyomo.core.base.var import ScalarVar, GeneralVarData, IndexedVar -from pyomo.core.base.param import _ParamData, ScalarParam, IndexedParam +from pyomo.core.base.param import ParamData, ScalarParam, IndexedParam from pyomo.core.base.set import _SetData, SetOperator from pyomo.core.base.constraint import ScalarConstraint, IndexedConstraint from pyomo.common.collections.component_map import ComponentMap @@ -417,7 +417,7 @@ def __init__(self): Numeric_GetItemExpression: handle_numericGetItemExpression_node, TemplateSumExpression: handle_templateSumExpression_node, ScalarParam: handle_param_node, - _ParamData: handle_param_node, + ParamData: handle_param_node, IndexedParam: handle_param_node, NPV_Numeric_GetItemExpression: handle_numericGetItemExpression_node, IndexedBlock: handle_indexedBlock_node, @@ -717,10 +717,8 @@ def latex_printer( variableList.append(v) parameterList = [] - for p in identify_components( - temp_comp, [ScalarParam, _ParamData, IndexedParam] - ): - if isinstance(p, _ParamData): + for p in identify_components(temp_comp, [ScalarParam, ParamData, IndexedParam]): + if isinstance(p, ParamData): p_write = p.parent_component() if p_write not in ComponentSet(parameterList): parameterList.append(p_write) @@ -1280,7 +1278,7 @@ def get_index_names(st, lcm): if ky not in existing_components: overwrite_value = overwrite_value.replace('_', '\\_') rep_dict[variableMap[ky]] = overwrite_value - elif isinstance(ky, (pyo.Param, _ParamData)): + elif isinstance(ky, (pyo.Param, ParamData)): overwrite_value = latex_component_map[ky] if ky not in existing_components: overwrite_value = overwrite_value.replace('_', '\\_') diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index bc2bfd591e6..e60b474d037 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -17,7 +17,7 @@ ) from pyomo.common.errors import ApplicationError, PyomoException from pyomo.core.base import Var, _VarData -from pyomo.core.base.param import Param, _ParamData +from pyomo.core.base.param import Param, ParamData from pyomo.opt import SolverFactory from pyomo.contrib.pyros.util import ObjectiveType, setup_pyros_logger from pyomo.contrib.pyros.uncertainty_sets import UncertaintySet @@ -62,7 +62,7 @@ def mutable_param_validator(param_obj): Parameters ---------- - param_obj : Param or _ParamData + param_obj : Param or ParamData Param-like object of interest. Raises @@ -98,7 +98,7 @@ class InputDataStandardizer(object): Pyomo component type, such as Component, Var or Param. cdatatype : type Corresponding Pyomo component data type, such as - _ComponentData, _VarData, or _ParamData. + _ComponentData, _VarData, or ParamData. ctype_validator : callable, optional Validator function for objects of type `ctype`. cdatatype_validator : callable, optional @@ -531,7 +531,7 @@ def pyros_config(): default=[], domain=InputDataStandardizer( ctype=Param, - cdatatype=_ParamData, + cdatatype=ParamData, ctype_validator=mutable_param_validator, allow_repeats=False, ), diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 0f52d04135d..cd635e795fc 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -8,7 +8,7 @@ from pyomo.core.base import ConcreteModel, Var, _VarData from pyomo.common.log import LoggingIntercept from pyomo.common.errors import ApplicationError -from pyomo.core.base.param import Param, _ParamData +from pyomo.core.base.param import Param, ParamData from pyomo.contrib.pyros.config import ( InputDataStandardizer, mutable_param_validator, @@ -201,7 +201,7 @@ def test_standardizer_invalid_uninitialized_params(self): uninitialized entries passed. """ standardizer_func = InputDataStandardizer( - ctype=Param, cdatatype=_ParamData, ctype_validator=mutable_param_validator + ctype=Param, cdatatype=ParamData, ctype_validator=mutable_param_validator ) mdl = ConcreteModel() @@ -217,7 +217,7 @@ def test_standardizer_invalid_immutable_params(self): Param object(s) passed. """ standardizer_func = InputDataStandardizer( - ctype=Param, cdatatype=_ParamData, ctype_validator=mutable_param_validator + ctype=Param, cdatatype=ParamData, ctype_validator=mutable_param_validator ) mdl = ConcreteModel() @@ -237,7 +237,7 @@ def test_standardizer_valid_mutable_params(self): mdl.p2 = Param(["a", "b"], initialize=1, mutable=True) standardizer_func = InputDataStandardizer( - ctype=Param, cdatatype=_ParamData, ctype_validator=mutable_param_validator + ctype=Param, cdatatype=ParamData, ctype_validator=mutable_param_validator ) standardizer_input = [mdl.p1[0], mdl.p2] diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index a935a950819..1b22c17cf48 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -16,7 +16,7 @@ from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.var import GeneralVarData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.block import BlockData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.config import document_kwargs_from_configdict, ConfigValue @@ -288,7 +288,7 @@ def add_variables(self, variables: List[GeneralVarData]): """ @abc.abstractmethod - def add_parameters(self, params: List[_ParamData]): + def add_parameters(self, params: List[ParamData]): """ Add parameters to the model """ @@ -312,7 +312,7 @@ def remove_variables(self, variables: List[GeneralVarData]): """ @abc.abstractmethod - def remove_parameters(self, params: List[_ParamData]): + def remove_parameters(self, params: List[ParamData]): """ Remove parameters from the model """ diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 353798133db..107de15e625 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -25,7 +25,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types from pyomo.repn import generate_standard_repn from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression @@ -469,7 +469,7 @@ def _add_variables(self, variables: List[GeneralVarData]): self._vars_added_since_update.update(variables) self._needs_updated = True - def _add_parameters(self, params: List[_ParamData]): + def _add_parameters(self, params: List[ParamData]): pass def _reinit(self): @@ -747,7 +747,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): self._mutable_bounds.pop(v_id, None) self._needs_updated = True - def _remove_parameters(self, params: List[_ParamData]): + def _remove_parameters(self, params: List[ParamData]): pass def _update_variables(self, variables: List[GeneralVarData]): diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index aeacc9f87c4..558b8cbf314 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -15,7 +15,7 @@ from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint from pyomo.core.base.var import GeneralVarData -from pyomo.core.base.param import _ParamData, Param +from pyomo.core.base.param import ParamData, Param from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.collections import ComponentMap from pyomo.common.timing import HierarchicalTimer @@ -75,10 +75,10 @@ def add_variables(self, variables: List[GeneralVarData]): self._add_variables(variables) @abc.abstractmethod - def _add_parameters(self, params: List[_ParamData]): + def _add_parameters(self, params: List[ParamData]): pass - def add_parameters(self, params: List[_ParamData]): + def add_parameters(self, params: List[ParamData]): for p in params: self._params[id(p)] = p self._add_parameters(params) @@ -274,10 +274,10 @@ def remove_variables(self, variables: List[GeneralVarData]): del self._vars[v_id] @abc.abstractmethod - def _remove_parameters(self, params: List[_ParamData]): + def _remove_parameters(self, params: List[ParamData]): pass - def remove_parameters(self, params: List[_ParamData]): + def remove_parameters(self, params: List[ParamData]): self._remove_parameters(params) for p in params: del self._params[id(p)] diff --git a/pyomo/contrib/viewer/model_browser.py b/pyomo/contrib/viewer/model_browser.py index 5887a577ba0..91dc946c55d 100644 --- a/pyomo/contrib/viewer/model_browser.py +++ b/pyomo/contrib/viewer/model_browser.py @@ -33,7 +33,7 @@ import pyomo.contrib.viewer.qt as myqt from pyomo.contrib.viewer.report import value_no_exception, get_residual -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.environ import ( Block, BooleanVar, @@ -243,7 +243,7 @@ def _get_expr_callback(self): return None def _get_value_callback(self): - if isinstance(self.data, _ParamData): + if isinstance(self.data, ParamData): v = value_no_exception(self.data, div0="divide_by_0") # Check the param value for numpy float and int, sometimes numpy # values can sneak in especially if you set parameters from data @@ -295,7 +295,7 @@ def _get_residual_callback(self): def _get_units_callback(self): if isinstance(self.data, (Var, Var._ComponentDataClass)): return str(units.get_units(self.data)) - if isinstance(self.data, (Param, _ParamData)): + if isinstance(self.data, (Param, ParamData)): return str(units.get_units(self.data)) return self._cache_units @@ -320,7 +320,7 @@ def _set_value_callback(self, val): o.value = val except: return - elif isinstance(self.data, _ParamData): + elif isinstance(self.data, ParamData): if not self.data.parent_component().mutable: return try: diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 341cd1506ff..33b2f5c686c 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -805,7 +805,7 @@ class ComponentData(_ComponentBase): # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, LogicalConstraintData, # GeneralLogicalConstraintData, GeneralObjectiveData, - # _ParamData,GeneralVarData, GeneralBooleanVarData, DisjunctionData, + # ParamData,GeneralVarData, GeneralBooleanVarData, DisjunctionData, # ArcData, _PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 5fcaf92b25a..9af6a37de45 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -118,7 +118,7 @@ def _parent(self, val): pass -class _ParamData(ComponentData, NumericValue): +class ParamData(ComponentData, NumericValue): """ This class defines the data for a mutable parameter. @@ -252,6 +252,11 @@ def _compute_polynomial_degree(self, result): return 0 +class _ParamData(metaclass=RenamedClass): + __renamed__new_class__ = ParamData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register( "Parameter data that is used to define a model instance." ) @@ -285,7 +290,7 @@ class Param(IndexedComponent, IndexedComponent_NDArrayMixin): """ DefaultMutable = False - _ComponentDataClass = _ParamData + _ComponentDataClass = ParamData class NoValue(object): """A dummy type that is pickle-safe that we can use as the default @@ -523,14 +528,14 @@ def store_values(self, new_values, check=True): # instead of incurring the penalty of checking. for index, new_value in new_values.items(): if index not in self._data: - self._data[index] = _ParamData(self) + self._data[index] = ParamData(self) self._data[index]._value = new_value else: # For scalars, we will choose an approach based on # how "dense" the Param is if not self._data: # empty for index in self._index_set: - p = self._data[index] = _ParamData(self) + p = self._data[index] = ParamData(self) p._value = new_values elif len(self._data) == len(self._index_set): for index in self._index_set: @@ -538,7 +543,7 @@ def store_values(self, new_values, check=True): else: for index in self._index_set: if index not in self._data: - self._data[index] = _ParamData(self) + self._data[index] = ParamData(self) self._data[index]._value = new_values else: # @@ -601,9 +606,9 @@ def _getitem_when_not_present(self, index): # a default value, as long as *solving* a model without # reasonable values produces an informative error. if self._mutable: - # Note: _ParamData defaults to Param.NoValue + # Note: ParamData defaults to Param.NoValue if self.is_indexed(): - ans = self._data[index] = _ParamData(self) + ans = self._data[index] = ParamData(self) else: ans = self._data[index] = self ans._index = index @@ -698,8 +703,8 @@ def _setitem_impl(self, index, obj, value): return obj else: old_value, self._data[index] = self._data[index], value - # Because we do not have a _ParamData, we cannot rely on the - # validation that occurs in _ParamData.set_value() + # Because we do not have a ParamData, we cannot rely on the + # validation that occurs in ParamData.set_value() try: self._validate_value(index, value) return value @@ -736,14 +741,14 @@ def _setitem_when_not_present(self, index, value, _check_domain=True): self._index = UnindexedComponent_index return self elif self._mutable: - obj = self._data[index] = _ParamData(self) + obj = self._data[index] = ParamData(self) obj.set_value(value, index) obj._index = index return obj else: self._data[index] = value - # Because we do not have a _ParamData, we cannot rely on the - # validation that occurs in _ParamData.set_value() + # Because we do not have a ParamData, we cannot rely on the + # validation that occurs in ParamData.set_value() self._validate_value(index, value, _check_domain) return value except: @@ -901,9 +906,9 @@ def _pprint(self): return (headers, self.sparse_iteritems(), ("Value",), dataGen) -class ScalarParam(_ParamData, Param): +class ScalarParam(ParamData, Param): def __init__(self, *args, **kwds): - _ParamData.__init__(self, component=self) + ParamData.__init__(self, component=self) Param.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -996,7 +1001,7 @@ def _create_objects_for_deepcopy(self, memo, component_list): # between potentially variable GetItemExpression objects and # "constant" GetItemExpression objects. That will need to wait for # the expression rework [JDS; Nov 22]. - def __getitem__(self, args) -> _ParamData: + def __getitem__(self, args) -> ParamData: try: return super().__getitem__(args) except: diff --git a/pyomo/core/expr/calculus/derivatives.py b/pyomo/core/expr/calculus/derivatives.py index cd23cb16b2c..5df1fd3c65e 100644 --- a/pyomo/core/expr/calculus/derivatives.py +++ b/pyomo/core/expr/calculus/derivatives.py @@ -42,7 +42,7 @@ def differentiate(expr, wrt=None, wrt_list=None, mode=Modes.reverse_numeric): wrt: pyomo.core.base.var.GeneralVarData If specified, this function will return the derivative with respect to wrt. wrt is normally a GeneralVarData, but could - also be a _ParamData. wrt and wrt_list cannot both be specified. + also be a ParamData. wrt and wrt_list cannot both be specified. wrt_list: list of pyomo.core.base.var.GeneralVarData If specified, this function will return the derivative with respect to each element in wrt_list. A list will be returned diff --git a/pyomo/core/tests/unit/test_param.py b/pyomo/core/tests/unit/test_param.py index 9bc0c4b2ad2..b39272879f6 100644 --- a/pyomo/core/tests/unit/test_param.py +++ b/pyomo/core/tests/unit/test_param.py @@ -65,7 +65,7 @@ from pyomo.common.errors import PyomoException from pyomo.common.log import LoggingIntercept from pyomo.common.tempfiles import TempfileManager -from pyomo.core.base.param import _ParamData +from pyomo.core.base.param import ParamData from pyomo.core.base.set import _SetData from pyomo.core.base.units_container import units, pint_available, UnitsError @@ -181,7 +181,7 @@ def test_setitem_preexisting(self): idx = sorted(keys)[0] self.assertEqual(value(self.instance.A[idx]), self.data[idx]) if self.instance.A.mutable: - self.assertTrue(isinstance(self.instance.A[idx], _ParamData)) + self.assertTrue(isinstance(self.instance.A[idx], ParamData)) else: self.assertEqual(type(self.instance.A[idx]), float) @@ -190,7 +190,7 @@ def test_setitem_preexisting(self): if not self.instance.A.mutable: self.fail("Expected setitem[%s] to fail for immutable Params" % (idx,)) self.assertEqual(value(self.instance.A[idx]), 4.3) - self.assertTrue(isinstance(self.instance.A[idx], _ParamData)) + self.assertTrue(isinstance(self.instance.A[idx], ParamData)) except TypeError: # immutable Params should raise a TypeError exception if self.instance.A.mutable: @@ -249,7 +249,7 @@ def test_setitem_default_override(self): self.assertEqual(value(self.instance.A[idx]), self.instance.A._default_val) if self.instance.A.mutable: - self.assertIsInstance(self.instance.A[idx], _ParamData) + self.assertIsInstance(self.instance.A[idx], ParamData) else: self.assertEqual( type(self.instance.A[idx]), type(value(self.instance.A._default_val)) @@ -260,7 +260,7 @@ def test_setitem_default_override(self): if not self.instance.A.mutable: self.fail("Expected setitem[%s] to fail for immutable Params" % (idx,)) self.assertEqual(self.instance.A[idx].value, 4.3) - self.assertIsInstance(self.instance.A[idx], _ParamData) + self.assertIsInstance(self.instance.A[idx], ParamData) except TypeError: # immutable Params should raise a TypeError exception if self.instance.A.mutable: diff --git a/pyomo/core/tests/unit/test_visitor.py b/pyomo/core/tests/unit/test_visitor.py index 12fb98d1d19..ac61a3a24c7 100644 --- a/pyomo/core/tests/unit/test_visitor.py +++ b/pyomo/core/tests/unit/test_visitor.py @@ -72,7 +72,7 @@ RECURSION_LIMIT, get_stack_depth, ) -from pyomo.core.base.param import _ParamData, ScalarParam +from pyomo.core.base.param import ParamData, ScalarParam from pyomo.core.expr.template_expr import IndexTemplate from pyomo.common.collections import ComponentSet from pyomo.common.errors import TemplateExpressionError @@ -685,7 +685,7 @@ def __init__(self, model): self.model = model def visiting_potential_leaf(self, node): - if node.__class__ in (_ParamData, ScalarParam): + if node.__class__ in (ParamData, ScalarParam): if id(node) in self.substitute: return True, self.substitute[id(node)] self.substitute[id(node)] = 2 * self.model.w.add() diff --git a/pyomo/repn/plugins/ampl/ampl_.py b/pyomo/repn/plugins/ampl/ampl_.py index c6357cbecd9..840bee2166c 100644 --- a/pyomo/repn/plugins/ampl/ampl_.py +++ b/pyomo/repn/plugins/ampl/ampl_.py @@ -171,8 +171,8 @@ def _build_op_template(): _op_template[var._VarData] = "v%d{C}\n" _op_comment[var._VarData] = "\t#%s" - _op_template[param._ParamData] = "n%r{C}\n" - _op_comment[param._ParamData] = "" + _op_template[param.ParamData] = "n%r{C}\n" + _op_comment[param.ParamData] = "" _op_template[NumericConstant] = "n%r{C}\n" _op_comment[NumericConstant] = "" @@ -749,8 +749,8 @@ def _print_nonlinear_terms_NL(self, exp): ) ) - elif isinstance(exp, param._ParamData): - OUTPUT.write(self._op_string[param._ParamData] % (value(exp))) + elif isinstance(exp, param.ParamData): + OUTPUT.write(self._op_string[param.ParamData] % (value(exp))) elif isinstance(exp, NumericConstant) or exp.is_fixed(): OUTPUT.write(self._op_string[NumericConstant] % (value(exp))) diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 907b4a2b115..5786d078385 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -23,7 +23,7 @@ from pyomo.core.base import ExpressionData, Expression from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.var import ScalarVar, Var, GeneralVarData, value -from pyomo.core.base.param import ScalarParam, _ParamData +from pyomo.core.base.param import ScalarParam, ParamData from pyomo.core.kernel.expression import expression, noclone from pyomo.core.kernel.variable import IVariable, variable from pyomo.core.kernel.objective import objective @@ -1138,7 +1138,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra EXPR.ExternalFunctionExpression: _collect_external_fn, # ConnectorData : _collect_linear_connector, # ScalarConnector : _collect_linear_connector, - _ParamData: _collect_const, + ParamData: _collect_const, ScalarParam: _collect_const, # param.Param : _collect_linear_const, # parameter : _collect_linear_const, @@ -1538,7 +1538,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): ##EXPR.LinearSumExpression : _collect_linear_sum, ##ConnectorData : _collect_linear_connector, ##ScalarConnector : _collect_linear_connector, - ##param._ParamData : _collect_linear_const, + ##param.ParamData : _collect_linear_const, ##param.ScalarParam : _collect_linear_const, ##param.Param : _collect_linear_const, ##parameter : _collect_linear_const, From 8c968e3e976bc8ea8c551a7c48d275d623e04559 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:51:57 -0600 Subject: [PATCH 1002/3044] Renamed _PiecewiseData -> PiecewiseData --- pyomo/core/base/piecewise.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index b15def13ccb..43f8ddbfef5 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -214,7 +214,7 @@ def _characterize_function(name, tol, f_rule, model, points, *index): return 0, values, False -class _PiecewiseData(BlockData): +class PiecewiseData(BlockData): """ This class defines the base class for all linearization and piecewise constraint generators.. @@ -272,6 +272,11 @@ def __call__(self, x): ) +class _PiecewiseData(metaclass=RenamedClass): + __renamed__new_class__ = PiecewiseData + __renamed__version__ = '6.7.2.dev0' + + class _SimpleSinglePiecewise(object): """ Called when the piecewise points list has only two points @@ -1125,7 +1130,7 @@ def f(model,j,x): not be modified. """ - _ComponentDataClass = _PiecewiseData + _ComponentDataClass = PiecewiseData def __new__(cls, *args, **kwds): if cls != Piecewise: @@ -1541,7 +1546,7 @@ def add(self, index, _is_indexed=None): raise ValueError(msg % (self.name, index, self._pw_rep)) if _is_indexed: - comp = _PiecewiseData(self) + comp = PiecewiseData(self) else: comp = self self._data[index] = comp @@ -1551,9 +1556,9 @@ def add(self, index, _is_indexed=None): comp.build_constraints(func, _self_xvar, _self_yvar) -class SimplePiecewise(_PiecewiseData, Piecewise): +class SimplePiecewise(PiecewiseData, Piecewise): def __init__(self, *args, **kwds): - _PiecewiseData.__init__(self, self) + PiecewiseData.__init__(self, self) Piecewise.__init__(self, *args, **kwds) From 590e21fad08496e01fc6611fe506867b2ae6217d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:52:11 -0600 Subject: [PATCH 1003/3044] Renamed _PortData -> PortData --- pyomo/core/base/component.py | 2 +- pyomo/network/port.py | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 33b2f5c686c..7a4b7e40aab 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -806,7 +806,7 @@ class ComponentData(_ComponentBase): # GeneralExpressionData, LogicalConstraintData, # GeneralLogicalConstraintData, GeneralObjectiveData, # ParamData,GeneralVarData, GeneralBooleanVarData, DisjunctionData, - # ArcData, _PortData, _LinearConstraintData, and + # ArcData, PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! def __init__(self, component): diff --git a/pyomo/network/port.py b/pyomo/network/port.py index 26822d4fee9..ee5c915d8db 100644 --- a/pyomo/network/port.py +++ b/pyomo/network/port.py @@ -36,7 +36,7 @@ logger = logging.getLogger('pyomo.network') -class _PortData(ComponentData): +class PortData(ComponentData): """ This class defines the data for a single Port @@ -285,6 +285,11 @@ def get_split_fraction(self, arc): return res +class _PortData(metaclass=RenamedClass): + __renamed__new_class__ = PortData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register( "A bundle of variables that can be connected to other ports." ) @@ -339,7 +344,7 @@ def __init__(self, *args, **kwd): # IndexedComponent that support implicit definition def _getitem_when_not_present(self, idx): """Returns the default component data value.""" - tmp = self._data[idx] = _PortData(component=self) + tmp = self._data[idx] = PortData(component=self) tmp._index = idx return tmp @@ -357,7 +362,7 @@ def construct(self, data=None): for _set in self._anonymous_sets: _set.construct() - # Construct _PortData objects for all index values + # Construct PortData objects for all index values if self.is_indexed(): self._initialize_members(self._index_set) else: @@ -763,9 +768,9 @@ def _create_evar(member, name, eblock, index_set): return evar -class ScalarPort(Port, _PortData): +class ScalarPort(Port, PortData): def __init__(self, *args, **kwd): - _PortData.__init__(self, component=self) + PortData.__init__(self, component=self) Port.__init__(self, *args, **kwd) self._index = UnindexedComponent_index From 199ee006d445859f7d796f528a8b326cf883f3f6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:55:57 -0600 Subject: [PATCH 1004/3044] Renamed _SetData -> SetData --- pyomo/contrib/latex_printer/latex_printer.py | 4 +- pyomo/core/base/set.py | 51 +++++++++++--------- pyomo/core/base/sets.py | 4 +- pyomo/core/tests/unit/test_param.py | 4 +- pyomo/core/tests/unit/test_set.py | 18 +++---- 5 files changed, 43 insertions(+), 38 deletions(-) diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index dec058bb5ba..28d1ca52943 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -49,7 +49,7 @@ ) from pyomo.core.base.var import ScalarVar, GeneralVarData, IndexedVar from pyomo.core.base.param import ParamData, ScalarParam, IndexedParam -from pyomo.core.base.set import _SetData, SetOperator +from pyomo.core.base.set import SetData, SetOperator from pyomo.core.base.constraint import ScalarConstraint, IndexedConstraint from pyomo.common.collections.component_map import ComponentMap from pyomo.common.collections.component_set import ComponentSet @@ -1283,7 +1283,7 @@ def get_index_names(st, lcm): if ky not in existing_components: overwrite_value = overwrite_value.replace('_', '\\_') rep_dict[parameterMap[ky]] = overwrite_value - elif isinstance(ky, _SetData): + elif isinstance(ky, SetData): # already handled pass elif isinstance(ky, (float, int)): diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index c7a7edc8e4b..3280f512e83 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -87,7 +87,7 @@ 0. `class _SetDataBase(ComponentData)` *(pure virtual interface)* -1. `class _SetData(_SetDataBase)` +1. `class SetData(_SetDataBase)` *(base class for all AML Sets)* 2. `class _FiniteSetMixin(object)` @@ -102,7 +102,7 @@ bounded continuous ranges as well as unbounded discrete ranges). As there are an infinite number of values, iteration is *not* supported. The base class also implements all Python set operations. -Note that `_SetData` does *not* implement `len()`, as Python requires +Note that `SetData` does *not* implement `len()`, as Python requires `len()` to return a positive integer. Finite sets add iteration and support for `len()`. In addition, they @@ -520,7 +520,7 @@ class _SetDataBase(ComponentData): __slots__ = () -class _SetData(_SetDataBase): +class SetData(_SetDataBase): """The base for all Pyomo AML objects that can be used as a component indexing set. @@ -534,13 +534,13 @@ def __contains__(self, value): ans = self.get(value, _NotFound) except TypeError: # In Python 3.x, Sets are unhashable - if isinstance(value, _SetData): + if isinstance(value, SetData): ans = _NotFound else: raise if ans is _NotFound: - if isinstance(value, _SetData): + if isinstance(value, SetData): deprecation_warning( "Testing for set subsets with 'a in b' is deprecated. " "Use 'a.issubset(b)'.", @@ -1188,6 +1188,11 @@ def __gt__(self, other): return self >= other and not self == other +class _SetData(metaclass=RenamedClass): + __renamed__new_class__ = SetData + __renamed__version__ = '6.7.2.dev0' + + class _FiniteSetMixin(object): __slots__ = () @@ -1294,13 +1299,13 @@ def ranges(self): yield NonNumericRange(i) -class FiniteSetData(_FiniteSetMixin, _SetData): +class FiniteSetData(_FiniteSetMixin, SetData): """A general unordered iterable Set""" __slots__ = ('_values', '_domain', '_validate', '_filter', '_dimen') def __init__(self, component): - _SetData.__init__(self, component=component) + SetData.__init__(self, component=component) # Derived classes (like OrderedSetData) may want to change the # storage if not hasattr(self, '_values'): @@ -1986,7 +1991,7 @@ class SortedOrder(object): _UnorderedInitializers = {set} @overload - def __new__(cls: Type[Set], *args, **kwds) -> Union[_SetData, IndexedSet]: ... + def __new__(cls: Type[Set], *args, **kwds) -> Union[SetData, IndexedSet]: ... @overload def __new__(cls: Type[OrderedScalarSet], *args, **kwds) -> OrderedScalarSet: ... @@ -2193,7 +2198,7 @@ def _getitem_when_not_present(self, index): """Returns the default component data value.""" # Because we allow sets within an IndexedSet to have different # dimen, we have moved the tuplization logic from PyomoModel - # into Set (because we cannot know the dimen of a _SetData until + # into Set (because we cannot know the dimen of a SetData until # we are actually constructing that index). This also means # that we need to potentially communicate the dimen to the # (wrapped) value initializer. So, we will get the dimen first, @@ -2353,7 +2358,7 @@ def _pprint(self): # else: # return '{' + str(ans)[1:-1] + "}" - # TBD: In the current design, we force all _SetData within an + # TBD: In the current design, we force all SetData within an # indexed Set to have the same isordered value, so we will only # print it once in the header. Is this a good design? try: @@ -2398,7 +2403,7 @@ def data(self): return {k: v.data() for k, v in self.items()} @overload - def __getitem__(self, index) -> _SetData: ... + def __getitem__(self, index) -> SetData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore @@ -2479,14 +2484,14 @@ class AbstractSortedSimpleSet(metaclass=RenamedClass): ############################################################################ -class SetOf(_SetData, Component): +class SetOf(SetData, Component): """""" def __new__(cls, *args, **kwds): if cls is not SetOf: return super(SetOf, cls).__new__(cls) (reference,) = args - if isinstance(reference, (_SetData, GlobalSetBase)): + if isinstance(reference, (SetData, GlobalSetBase)): if reference.isfinite(): if reference.isordered(): return super(SetOf, cls).__new__(OrderedSetOf) @@ -2500,7 +2505,7 @@ def __new__(cls, *args, **kwds): return super(SetOf, cls).__new__(FiniteSetOf) def __init__(self, reference, **kwds): - _SetData.__init__(self, component=self) + SetData.__init__(self, component=self) kwds.setdefault('ctype', SetOf) Component.__init__(self, **kwds) self._ref = reference @@ -2523,7 +2528,7 @@ def construct(self, data=None): @property def dimen(self): - if isinstance(self._ref, _SetData): + if isinstance(self._ref, SetData): return self._ref.dimen _iter = iter(self) try: @@ -2618,7 +2623,7 @@ def ord(self, item): ############################################################################ -class InfiniteRangeSetData(_SetData): +class InfiniteRangeSetData(SetData): """Data class for a infinite set. This Set implements an interface to an *infinite set* defined by one @@ -2630,7 +2635,7 @@ class InfiniteRangeSetData(_SetData): __slots__ = ('_ranges',) def __init__(self, component): - _SetData.__init__(self, component=component) + SetData.__init__(self, component=component) self._ranges = None def get(self, value, default=None): @@ -3298,11 +3303,11 @@ class AbstractFiniteSimpleRangeSet(metaclass=RenamedClass): ############################################################################ -class SetOperator(_SetData, Set): +class SetOperator(SetData, Set): __slots__ = ('_sets',) def __init__(self, *args, **kwds): - _SetData.__init__(self, component=self) + SetData.__init__(self, component=self) Set.__init__(self, **kwds) self._sets, _anonymous = zip(*(process_setarg(_set) for _set in args)) _anonymous = tuple(filter(None, _anonymous)) @@ -4242,9 +4247,9 @@ def ord(self, item): ############################################################################ -class _AnySet(_SetData, Set): +class _AnySet(SetData, Set): def __init__(self, **kwds): - _SetData.__init__(self, component=self) + SetData.__init__(self, component=self) # There is a chicken-and-egg game here: the SetInitializer uses # Any as part of the processing of the domain/within/bounds # domain restrictions. However, Any has not been declared when @@ -4298,9 +4303,9 @@ def get(self, val, default=None): return super(_AnyWithNoneSet, self).get(val, default) -class _EmptySet(_FiniteSetMixin, _SetData, Set): +class _EmptySet(_FiniteSetMixin, SetData, Set): def __init__(self, **kwds): - _SetData.__init__(self, component=self) + SetData.__init__(self, component=self) Set.__init__(self, **kwds) self.construct() diff --git a/pyomo/core/base/sets.py b/pyomo/core/base/sets.py index ca693cf7d8b..3ebdc6875d1 100644 --- a/pyomo/core/base/sets.py +++ b/pyomo/core/base/sets.py @@ -17,8 +17,8 @@ process_setarg, set_options, simple_set_rule, - _SetDataBase, - _SetData, + SetDataBase, + SetData, Set, SetOf, IndexedSet, diff --git a/pyomo/core/tests/unit/test_param.py b/pyomo/core/tests/unit/test_param.py index b39272879f6..f22674b6bf7 100644 --- a/pyomo/core/tests/unit/test_param.py +++ b/pyomo/core/tests/unit/test_param.py @@ -66,7 +66,7 @@ from pyomo.common.log import LoggingIntercept from pyomo.common.tempfiles import TempfileManager from pyomo.core.base.param import ParamData -from pyomo.core.base.set import _SetData +from pyomo.core.base.set import SetData from pyomo.core.base.units_container import units, pint_available, UnitsError from io import StringIO @@ -1487,7 +1487,7 @@ def test_domain_set_initializer(self): m.I = Set(initialize=[1, 2, 3]) param_vals = {1: 1, 2: 1, 3: -1} m.p = Param(m.I, initialize=param_vals, domain={-1, 1}) - self.assertIsInstance(m.p.domain, _SetData) + self.assertIsInstance(m.p.domain, SetData) @unittest.skipUnless(pint_available, "units test requires pint module") def test_set_value_units(self): diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 38870d5213e..abd5a03c755 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -81,7 +81,7 @@ SetProduct_InfiniteSet, SetProduct_FiniteSet, SetProduct_OrderedSet, - _SetData, + SetData, FiniteSetData, InsertionOrderSetData, _SortedSetData, @@ -4300,7 +4300,7 @@ def _l_tri(model, i, j): # This tests a filter that matches the dimentionality of the # component. construct() needs to recognize that the filter is # returning a constant in construct() and re-assign it to be the - # _filter for each _SetData + # _filter for each SetData def _lt_3(model, i): self.assertIs(model, m) return i < 3 @@ -5297,15 +5297,15 @@ def test_no_normalize_index(self): class TestAbstractSetAPI(unittest.TestCase): - def test_SetData(self): + def testSetData(self): # This tests an anstract non-finite set API m = ConcreteModel() m.I = Set(initialize=[1]) - s = _SetData(m.I) + s = SetData(m.I) # - # _SetData API + # SetData API # with self.assertRaises(DeveloperError): @@ -5395,7 +5395,7 @@ def test_SetData(self): def test_FiniteMixin(self): # This tests an anstract finite set API - class FiniteMixin(_FiniteSetMixin, _SetData): + class FiniteMixin(_FiniteSetMixin, SetData): pass m = ConcreteModel() @@ -5403,7 +5403,7 @@ class FiniteMixin(_FiniteSetMixin, _SetData): s = FiniteMixin(m.I) # - # _SetData API + # SetData API # with self.assertRaises(DeveloperError): @@ -5520,7 +5520,7 @@ class FiniteMixin(_FiniteSetMixin, _SetData): def test_OrderedMixin(self): # This tests an anstract ordered set API - class OrderedMixin(_OrderedSetMixin, _FiniteSetMixin, _SetData): + class OrderedMixin(_OrderedSetMixin, _FiniteSetMixin, SetData): pass m = ConcreteModel() @@ -5528,7 +5528,7 @@ class OrderedMixin(_OrderedSetMixin, _FiniteSetMixin, _SetData): s = OrderedMixin(m.I) # - # _SetData API + # SetData API # with self.assertRaises(DeveloperError): From 43c7c40b032a5d3ef8512d8a9d54969d52f9e45a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:56:14 -0600 Subject: [PATCH 1005/3044] Renamed _SortedSetData -> SortedSetData --- pyomo/core/base/set.py | 27 ++++++++++++++++----------- pyomo/core/tests/unit/test_set.py | 8 ++++---- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 3280f512e83..d94cc86cf7c 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1649,7 +1649,7 @@ class OrderedSetData(_OrderedSetMixin, FiniteSetData): implements a set ordered by insertion order, we make the "official" InsertionOrderSetData an empty derivative class, so that - issubclass(_SortedSetData, InsertionOrderSetData) == False + issubclass(SortedSetData, InsertionOrderSetData) == False Constructor Arguments: component The Set object that owns this data. @@ -1796,7 +1796,7 @@ def sorted_iter(self): return iter(self) -class _SortedSetData(_SortedSetMixin, OrderedSetData): +class SortedSetData(_SortedSetMixin, OrderedSetData): """ This class defines the data for a sorted set. @@ -1819,12 +1819,12 @@ def _iter_impl(self): """ if not self._is_sorted: self._sort() - return super(_SortedSetData, self)._iter_impl() + return super(SortedSetData, self)._iter_impl() def __reversed__(self): if not self._is_sorted: self._sort() - return super(_SortedSetData, self).__reversed__() + return super(SortedSetData, self).__reversed__() def _add_impl(self, value): # Note that the sorted status has no bearing on insertion, @@ -1838,7 +1838,7 @@ def _add_impl(self, value): # def discard(self, val): def clear(self): - super(_SortedSetData, self).clear() + super(SortedSetData, self).clear() self._is_sorted = True def at(self, index): @@ -1850,7 +1850,7 @@ def at(self, index): """ if not self._is_sorted: self._sort() - return super(_SortedSetData, self).at(index) + return super(SortedSetData, self).at(index) def ord(self, item): """ @@ -1862,7 +1862,7 @@ def ord(self, item): """ if not self._is_sorted: self._sort() - return super(_SortedSetData, self).ord(item) + return super(SortedSetData, self).ord(item) def sorted_data(self): return self.data() @@ -1875,6 +1875,11 @@ def _sort(self): self._is_sorted = True +class _SortedSetData(metaclass=RenamedClass): + __renamed__new_class__ = SortedSetData + __renamed__version__ = '6.7.2.dev0' + + ############################################################################ _SET_API = (('__contains__', 'test membership in'), 'get', 'ranges', 'bounds') @@ -2005,7 +2010,7 @@ def __new__(cls, *args, **kwds): # Many things are easier by forcing it to be consistent across # the set (namely, the _ComponentDataClass is constant). # However, it is a bit off that 'ordered' it the only arg NOT - # processed by Initializer. We can mock up a _SortedSetData + # processed by Initializer. We can mock up a SortedSetData # sort function that preserves Insertion Order (lambda x: x), but # the unsorted is harder (it would effectively be insertion # order, but ordered() may not be deterministic based on how the @@ -2052,7 +2057,7 @@ def __new__(cls, *args, **kwds): if ordered is Set.InsertionOrder: newObj._ComponentDataClass = InsertionOrderSetData elif ordered is Set.SortedOrder: - newObj._ComponentDataClass = _SortedSetData + newObj._ComponentDataClass = SortedSetData else: newObj._ComponentDataClass = FiniteSetData return newObj @@ -2435,13 +2440,13 @@ class OrderedSimpleSet(metaclass=RenamedClass): __renamed__version__ = '6.0' -class SortedScalarSet(_ScalarOrderedSetMixin, _SortedSetData, Set): +class SortedScalarSet(_ScalarOrderedSetMixin, SortedSetData, Set): def __init__(self, **kwds): # In case someone inherits from us, we will provide a rational # default for the "ordered" flag kwds.setdefault('ordered', Set.SortedOrder) - _SortedSetData.__init__(self, component=self) + SortedSetData.__init__(self, component=self) Set.__init__(self, **kwds) self._index = UnindexedComponent_index diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index abd5a03c755..f62589a6873 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -84,7 +84,7 @@ SetData, FiniteSetData, InsertionOrderSetData, - _SortedSetData, + SortedSetData, _FiniteSetMixin, _OrderedSetMixin, SetInitializer, @@ -4173,9 +4173,9 @@ def test_indexed_set(self): self.assertTrue(m.I[1].isordered()) self.assertTrue(m.I[2].isordered()) self.assertTrue(m.I[3].isordered()) - self.assertIs(type(m.I[1]), _SortedSetData) - self.assertIs(type(m.I[2]), _SortedSetData) - self.assertIs(type(m.I[3]), _SortedSetData) + self.assertIs(type(m.I[1]), SortedSetData) + self.assertIs(type(m.I[2]), SortedSetData) + self.assertIs(type(m.I[3]), SortedSetData) self.assertEqual(m.I.data(), {1: (2, 4, 5), 2: (2, 4, 5), 3: (2, 4, 5)}) # Explicit (procedural) construction From 63152a0be76006ff10e5f82f1b42de8af0ed6d56 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:57:39 -0600 Subject: [PATCH 1006/3044] Renamed _SOSConstraintData -> SOSConstraintData --- pyomo/contrib/appsi/base.py | 12 ++++++------ pyomo/contrib/appsi/fbbt.py | 6 +++--- pyomo/contrib/appsi/solvers/gurobi.py | 8 ++++---- pyomo/contrib/appsi/solvers/highs.py | 6 +++--- pyomo/contrib/appsi/writers/lp_writer.py | 6 +++--- pyomo/contrib/appsi/writers/nl_writer.py | 6 +++--- pyomo/contrib/solver/gurobi.py | 8 ++++---- pyomo/contrib/solver/persistent.py | 12 ++++++------ pyomo/core/base/sos.py | 15 ++++++++++----- pyomo/repn/plugins/cpxlp.py | 2 +- .../solvers/plugins/solvers/gurobi_persistent.py | 2 +- 11 files changed, 44 insertions(+), 39 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index e50d5201090..409c8e2596c 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -22,7 +22,7 @@ MutableMapping, ) from pyomo.core.base.constraint import GeneralConstraintData, Constraint -from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint +from pyomo.core.base.sos import SOSConstraintData, SOSConstraint from pyomo.core.base.var import GeneralVarData, Var from pyomo.core.base.param import ParamData, Param from pyomo.core.base.block import BlockData, Block @@ -1034,10 +1034,10 @@ def add_constraints(self, cons: List[GeneralConstraintData]): v.fix() @abc.abstractmethod - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): pass - def add_sos_constraints(self, cons: List[_SOSConstraintData]): + def add_sos_constraints(self, cons: List[SOSConstraintData]): for con in cons: if con in self._vars_referenced_by_con: raise ValueError( @@ -1154,10 +1154,10 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): del self._vars_referenced_by_con[con] @abc.abstractmethod - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): pass - def remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def remove_sos_constraints(self, cons: List[SOSConstraintData]): self._remove_sos_constraints(cons) for con in cons: if con not in self._vars_referenced_by_con: @@ -1339,7 +1339,7 @@ def update(self, timer: HierarchicalTimer = None): old_cons.append(c) else: assert (c.ctype is SOSConstraint) or ( - c.ctype is None and isinstance(c, _SOSConstraintData) + c.ctype is None and isinstance(c, SOSConstraintData) ) old_sos.append(c) self.remove_constraints(old_cons) diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 7735318f8ba..122ca5f7ffd 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -21,7 +21,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.param import ParamData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.objective import GeneralObjectiveData, minimize, maximize from pyomo.core.base.block import BlockData from pyomo.core.base import SymbolMap, TextLabeler @@ -169,7 +169,7 @@ def _add_constraints(self, cons: List[GeneralConstraintData]): for c, cc in self._con_map.items(): cc.name = self._symbol_map.getSymbol(c, self._con_labeler) - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError( 'IntervalTightener does not support SOS constraints' @@ -184,7 +184,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): self._cmodel.remove_constraint(cc) del self._rcon_map[cc] - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError( 'IntervalTightener does not support SOS constraints' diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 4392cdf0839..8606d44cd46 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -25,7 +25,7 @@ from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.core.base.var import Var, GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types from pyomo.repn import generate_standard_repn @@ -709,7 +709,7 @@ def _add_constraints(self, cons: List[GeneralConstraintData]): self._constraints_added_since_update.update(cons) self._needs_updated = True - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): for con in cons: conname = self._symbol_map.getSymbol(con, self._labeler) level = con.level @@ -749,7 +749,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): self._mutable_quadratic_helpers.pop(con, None) self._needs_updated = True - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): for con in cons: if con in self._constraints_added_since_update: self._update_gurobi_model() @@ -1288,7 +1288,7 @@ def get_sos_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.sos._SOSConstraintData + con: pyomo.core.base.sos.SOSConstraintData The pyomo SOS constraint for which the corresponding gurobi SOS constraint attribute should be retrieved. attr: str diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index a6b7c102c91..5af7b297684 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -22,7 +22,7 @@ from pyomo.core.base import SymbolMap from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant from pyomo.repn import generate_standard_repn @@ -456,7 +456,7 @@ def _add_constraints(self, cons: List[GeneralConstraintData]): np.array(coef_values, dtype=np.double), ) - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): if cons: raise NotImplementedError( 'Highs interface does not support SOS constraints' @@ -487,7 +487,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): {v: k for k, v in self._pyomo_con_to_solver_con_map.items()} ) - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if cons: raise NotImplementedError( 'Highs interface does not support SOS constraints' diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 3a168cdcd91..3a6682d5c00 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -14,7 +14,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData -from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn from pyomo.core.expr.numvalue import value @@ -102,7 +102,7 @@ def _add_params(self, params: List[ParamData]): def _add_constraints(self, cons: List[GeneralConstraintData]): cmodel.process_lp_constraints(cons, self) - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('LP writer does not yet support SOS constraints') @@ -113,7 +113,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): self._symbol_map.removeSymbol(c) del self._solver_con_to_pyomo_con_map[cc] - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('LP writer does not yet support SOS constraints') diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index fced3c5ae10..c46cb1c0723 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -14,7 +14,7 @@ from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData -from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn from pyomo.core.expr.numvalue import value @@ -126,7 +126,7 @@ def _add_constraints(self, cons: List[GeneralConstraintData]): for c, cc in self._pyomo_con_to_solver_con_map.items(): cc.name = self._symbol_map.getSymbol(c, self._con_labeler) - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('NL writer does not support SOS constraints') @@ -140,7 +140,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): self._writer.remove_constraint(cc) del self._solver_con_to_pyomo_con_map[cc] - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('NL writer does not support SOS constraints') diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index 107de15e625..c5a1c8c1cd8 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -24,7 +24,7 @@ from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.core.base.var import GeneralVarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types from pyomo.repn import generate_standard_repn @@ -685,7 +685,7 @@ def _add_constraints(self, cons: List[GeneralConstraintData]): self._constraints_added_since_update.update(cons) self._needs_updated = True - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): for con in cons: conname = self._symbol_map.getSymbol(con, self._labeler) level = con.level @@ -725,7 +725,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): self._mutable_quadratic_helpers.pop(con, None) self._needs_updated = True - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): for con in cons: if con in self._constraints_added_since_update: self._update_gurobi_model() @@ -1218,7 +1218,7 @@ def get_sos_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.sos._SOSConstraintData + con: pyomo.core.base.sos.SOSConstraintData The pyomo SOS constraint for which the corresponding gurobi SOS constraint attribute should be retrieved. attr: str diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index 558b8cbf314..e98d76b4841 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -13,7 +13,7 @@ from typing import List from pyomo.core.base.constraint import GeneralConstraintData, Constraint -from pyomo.core.base.sos import _SOSConstraintData, SOSConstraint +from pyomo.core.base.sos import SOSConstraintData, SOSConstraint from pyomo.core.base.var import GeneralVarData from pyomo.core.base.param import ParamData, Param from pyomo.core.base.objective import GeneralObjectiveData @@ -130,10 +130,10 @@ def add_constraints(self, cons: List[GeneralConstraintData]): v.fix() @abc.abstractmethod - def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + def _add_sos_constraints(self, cons: List[SOSConstraintData]): pass - def add_sos_constraints(self, cons: List[_SOSConstraintData]): + def add_sos_constraints(self, cons: List[SOSConstraintData]): for con in cons: if con in self._vars_referenced_by_con: raise ValueError( @@ -230,10 +230,10 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): del self._vars_referenced_by_con[con] @abc.abstractmethod - def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def _remove_sos_constraints(self, cons: List[SOSConstraintData]): pass - def remove_sos_constraints(self, cons: List[_SOSConstraintData]): + def remove_sos_constraints(self, cons: List[SOSConstraintData]): self._remove_sos_constraints(cons) for con in cons: if con not in self._vars_referenced_by_con: @@ -389,7 +389,7 @@ def update(self, timer: HierarchicalTimer = None): old_cons.append(c) else: assert (c.ctype is SOSConstraint) or ( - c.ctype is None and isinstance(c, _SOSConstraintData) + c.ctype is None and isinstance(c, SOSConstraintData) ) old_sos.append(c) self.remove_constraints(old_cons) diff --git a/pyomo/core/base/sos.py b/pyomo/core/base/sos.py index 6b8586c9b49..4a8afb05d71 100644 --- a/pyomo/core/base/sos.py +++ b/pyomo/core/base/sos.py @@ -28,7 +28,7 @@ logger = logging.getLogger('pyomo.core') -class _SOSConstraintData(ActiveComponentData): +class SOSConstraintData(ActiveComponentData): """ This class defines the data for a single special ordered set. @@ -101,6 +101,11 @@ def set_items(self, variables, weights): self._weights.append(w) +class _SOSConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = SOSConstraintData + __renamed__version__ = '6.7.2.dev0' + + @ModelComponentFactory.register("SOS constraint expressions.") class SOSConstraint(ActiveIndexedComponent): """ @@ -512,10 +517,10 @@ def add(self, index, variables, weights=None): Add a component data for the specified index. """ if index is None: - # because ScalarSOSConstraint already makes an _SOSConstraintData instance + # because ScalarSOSConstraint already makes an SOSConstraintData instance soscondata = self else: - soscondata = _SOSConstraintData(self) + soscondata = SOSConstraintData(self) self._data[index] = soscondata soscondata._index = index @@ -549,9 +554,9 @@ def pprint(self, ostream=None, verbose=False, prefix=""): ostream.write("\t\t" + str(weight) + ' : ' + var.name + '\n') -class ScalarSOSConstraint(SOSConstraint, _SOSConstraintData): +class ScalarSOSConstraint(SOSConstraint, SOSConstraintData): def __init__(self, *args, **kwd): - _SOSConstraintData.__init__(self, self) + SOSConstraintData.__init__(self, self) SOSConstraint.__init__(self, *args, **kwd) self._index = UnindexedComponent_index diff --git a/pyomo/repn/plugins/cpxlp.py b/pyomo/repn/plugins/cpxlp.py index 46e6b6d5265..6228e7c7286 100644 --- a/pyomo/repn/plugins/cpxlp.py +++ b/pyomo/repn/plugins/cpxlp.py @@ -374,7 +374,7 @@ def _print_expr_canonical( def printSOS(self, symbol_map, labeler, variable_symbol_map, soscondata, output): """ - Prints the SOS constraint associated with the _SOSConstraintData object + Prints the SOS constraint associated with the SOSConstraintData object """ sos_template_string = self.sos_template_string diff --git a/pyomo/solvers/plugins/solvers/gurobi_persistent.py b/pyomo/solvers/plugins/solvers/gurobi_persistent.py index 8a81aad3d3e..585a78e3ef1 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_persistent.py +++ b/pyomo/solvers/plugins/solvers/gurobi_persistent.py @@ -413,7 +413,7 @@ def get_sos_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.sos._SOSConstraintData + con: pyomo.core.base.sos.SOSConstraintData The pyomo SOS constraint for which the corresponding gurobi SOS constraint attribute should be retrieved. attr: str From 430f98207353b1e1e7383504aa0336bc852aeb9c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 17:57:41 -0600 Subject: [PATCH 1007/3044] Renamed _SuffixData -> SuffixData --- pyomo/core/base/suffix.py | 4 ++-- pyomo/repn/plugins/nl_writer.py | 15 ++++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/pyomo/core/base/suffix.py b/pyomo/core/base/suffix.py index be2f732650d..c4b37789773 100644 --- a/pyomo/core/base/suffix.py +++ b/pyomo/core/base/suffix.py @@ -129,7 +129,7 @@ class SuffixDirection(enum.IntEnum): IMPORT_EXPORT = 3 -_SuffixDataTypeDomain = In(SuffixDataType) +SuffixDataTypeDomain = In(SuffixDataType) _SuffixDirectionDomain = In(SuffixDirection) @@ -253,7 +253,7 @@ def datatype(self): def datatype(self, datatype): """Set the suffix datatype.""" if datatype is not None: - datatype = _SuffixDataTypeDomain(datatype) + datatype = SuffixDataTypeDomain(datatype) self._datatype = datatype @property diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index c010cee5e54..1a49238f35b 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -423,7 +423,7 @@ def _generate_symbol_map(self, info): return symbol_map -class _SuffixData(object): +class SuffixData(object): def __init__(self, name): self.name = name self.obj = {} @@ -505,6 +505,11 @@ def compile(self, column_order, row_order, obj_order, model_id): ) +class _SuffixData(metaclass=RenamedClass): + __renamed__new_class__ = SuffixData + __renamed__version__ = '6.7.2.dev0' + + class CachingNumericSuffixFinder(SuffixFinder): scale = True @@ -637,7 +642,7 @@ def write(self, model): continue name = suffix.local_name if name not in suffix_data: - suffix_data[name] = _SuffixData(name) + suffix_data[name] = SuffixData(name) suffix_data[name].update(suffix) # # Data structures to support variable/constraint scaling @@ -994,7 +999,7 @@ def write(self, model): "model. To avoid this error please use only one of " "these methods to define special ordered sets." ) - suffix_data[name] = _SuffixData(name) + suffix_data[name] = SuffixData(name) suffix_data[name].datatype.add(Suffix.INT) sos_id = 0 sosno = suffix_data['sosno'] @@ -1344,7 +1349,7 @@ def write(self, model): if not _vals: continue ostream.write(f"S{_field|_float} {len(_vals)} {name}\n") - # Note: _SuffixData.compile() guarantees the value is int/float + # Note: SuffixData.compile() guarantees the value is int/float ostream.write( ''.join(f"{_id} {_vals[_id]!r}\n" for _id in sorted(_vals)) ) @@ -1454,7 +1459,7 @@ def write(self, model): logger.warning("ignoring 'dual' suffix for Model") if data.con: ostream.write(f"d{len(data.con)}\n") - # Note: _SuffixData.compile() guarantees the value is int/float + # Note: SuffixData.compile() guarantees the value is int/float ostream.write( ''.join(f"{_id} {data.con[_id]!r}\n" for _id in sorted(data.con)) ) From f810600f0520097052fb4bf483920b86d03c80fb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 18:02:15 -0600 Subject: [PATCH 1008/3044] Renamed _VarData -> VarData --- pyomo/common/tests/test_timing.py | 4 +-- .../fme/fourier_motzkin_elimination.py | 4 +-- pyomo/contrib/gdp_bounds/info.py | 2 +- .../algorithms/solvers/pyomo_ext_cyipopt.py | 10 +++---- pyomo/contrib/pyros/config.py | 8 ++--- .../contrib/pyros/pyros_algorithm_methods.py | 2 +- pyomo/contrib/pyros/tests/test_config.py | 16 +++++----- pyomo/contrib/pyros/tests/test_grcs.py | 4 +-- pyomo/contrib/pyros/util.py | 6 ++-- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/boolean_var.py | 4 +-- pyomo/core/base/piecewise.py | 16 +++++----- pyomo/core/base/var.py | 29 +++++++++++-------- pyomo/core/beta/dict_objects.py | 4 +-- pyomo/core/beta/list_objects.py | 4 +-- pyomo/core/expr/numeric_expr.py | 2 +- .../plugins/transform/eliminate_fixed_vars.py | 4 +-- .../plugins/transform/radix_linearization.py | 6 ++-- pyomo/core/tests/unit/test_piecewise.py | 2 +- pyomo/core/tests/unit/test_var_set_bounds.py | 2 +- pyomo/dae/misc.py | 2 +- pyomo/repn/plugins/ampl/ampl_.py | 10 +++---- pyomo/repn/plugins/cpxlp.py | 2 +- pyomo/repn/plugins/mps.py | 2 +- pyomo/repn/plugins/nl_writer.py | 10 +++---- pyomo/repn/plugins/standard_form.py | 6 ++-- .../plugins/solvers/cplex_persistent.py | 4 +-- .../plugins/solvers/gurobi_persistent.py | 4 +-- .../plugins/solvers/mosek_persistent.py | 4 +-- .../plugins/solvers/persistent_solver.py | 4 +-- .../plugins/solvers/xpress_persistent.py | 4 +-- pyomo/util/calc_var_value.py | 2 +- 32 files changed, 95 insertions(+), 90 deletions(-) diff --git a/pyomo/common/tests/test_timing.py b/pyomo/common/tests/test_timing.py index 48288746882..90f4cdcd034 100644 --- a/pyomo/common/tests/test_timing.py +++ b/pyomo/common/tests/test_timing.py @@ -35,7 +35,7 @@ Any, TransformationFactory, ) -from pyomo.core.base.var import _VarData +from pyomo.core.base.var import VarData class _pseudo_component(Var): @@ -62,7 +62,7 @@ def test_raw_construction_timer(self): ) v = Var() v.construct() - a = ConstructionTimer(_VarData(v)) + a = ConstructionTimer(VarData(v)) self.assertRegex( str(a), r"ConstructionTimer object for Var ScalarVar\[NOTSET\]; " diff --git a/pyomo/contrib/fme/fourier_motzkin_elimination.py b/pyomo/contrib/fme/fourier_motzkin_elimination.py index a1b5d744cf4..4636450c58e 100644 --- a/pyomo/contrib/fme/fourier_motzkin_elimination.py +++ b/pyomo/contrib/fme/fourier_motzkin_elimination.py @@ -23,7 +23,7 @@ value, ConstraintList, ) -from pyomo.core.base import TransformationFactory, _VarData +from pyomo.core.base import TransformationFactory, VarData from pyomo.core.plugins.transform.hierarchy import Transformation from pyomo.common.config import ConfigBlock, ConfigValue, NonNegativeFloat from pyomo.common.modeling import unique_component_name @@ -58,7 +58,7 @@ def _check_var_bounds_filter(constraint): def vars_to_eliminate_list(x): - if isinstance(x, (Var, _VarData)): + if isinstance(x, (Var, VarData)): if not x.is_indexed(): return ComponentSet([x]) ans = ComponentSet() diff --git a/pyomo/contrib/gdp_bounds/info.py b/pyomo/contrib/gdp_bounds/info.py index db3f6d0846d..e65df2bfab0 100644 --- a/pyomo/contrib/gdp_bounds/info.py +++ b/pyomo/contrib/gdp_bounds/info.py @@ -35,7 +35,7 @@ def disjunctive_bound(var, scope): """Compute the disjunctive bounds for a variable in a given scope. Args: - var (_VarData): Variable for which to compute bound + var (VarData): Variable for which to compute bound scope (Component): The scope in which to compute the bound. If not a DisjunctData, it will walk up the tree and use the scope of the most immediate enclosing DisjunctData. diff --git a/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py b/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py index 16c5a19a5c6..7f43f6ac7c0 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/pyomo_ext_cyipopt.py @@ -16,7 +16,7 @@ from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP from pyomo.contrib.pynumero.sparse.block_vector import BlockVector from pyomo.environ import Var, Constraint, value -from pyomo.core.base.var import _VarData +from pyomo.core.base.var import VarData from pyomo.common.modeling import unique_component_name """ @@ -109,12 +109,12 @@ def __init__( An instance of a derived class (from ExternalInputOutputModel) that provides the methods to compute the outputs and the derivatives. - inputs : list of Pyomo variables (_VarData) + inputs : list of Pyomo variables (VarData) The Pyomo model needs to have variables to represent the inputs to the external model. This is the list of those input variables in the order that corresponds to the input_values vector provided in the set_inputs call. - outputs : list of Pyomo variables (_VarData) + outputs : list of Pyomo variables (VarData) The Pyomo model needs to have variables to represent the outputs from the external model. This is the list of those output variables in the order that corresponds to the numpy array returned from the evaluate_outputs call. @@ -130,7 +130,7 @@ def __init__( # verify that the inputs and outputs were passed correctly self._inputs = [v for v in inputs] for v in self._inputs: - if not isinstance(v, _VarData): + if not isinstance(v, VarData): raise RuntimeError( 'Argument inputs passed to PyomoExternalCyIpoptProblem must be' ' a list of VarData objects. Note: if you have an indexed variable, pass' @@ -139,7 +139,7 @@ def __init__( self._outputs = [v for v in outputs] for v in self._outputs: - if not isinstance(v, _VarData): + if not isinstance(v, VarData): raise RuntimeError( 'Argument outputs passed to PyomoExternalCyIpoptProblem must be' ' a list of VarData objects. Note: if you have an indexed variable, pass' diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index e60b474d037..59bf9a9ab37 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -16,7 +16,7 @@ Path, ) from pyomo.common.errors import ApplicationError, PyomoException -from pyomo.core.base import Var, _VarData +from pyomo.core.base import Var, VarData from pyomo.core.base.param import Param, ParamData from pyomo.opt import SolverFactory from pyomo.contrib.pyros.util import ObjectiveType, setup_pyros_logger @@ -98,7 +98,7 @@ class InputDataStandardizer(object): Pyomo component type, such as Component, Var or Param. cdatatype : type Corresponding Pyomo component data type, such as - _ComponentData, _VarData, or ParamData. + _ComponentData, VarData, or ParamData. ctype_validator : callable, optional Validator function for objects of type `ctype`. cdatatype_validator : callable, optional @@ -511,7 +511,7 @@ def pyros_config(): "first_stage_variables", ConfigValue( default=[], - domain=InputDataStandardizer(Var, _VarData, allow_repeats=False), + domain=InputDataStandardizer(Var, VarData, allow_repeats=False), description="First-stage (or design) variables.", visibility=1, ), @@ -520,7 +520,7 @@ def pyros_config(): "second_stage_variables", ConfigValue( default=[], - domain=InputDataStandardizer(Var, _VarData, allow_repeats=False), + domain=InputDataStandardizer(Var, VarData, allow_repeats=False), description="Second-stage (or control) variables.", visibility=1, ), diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 5987db074e6..cfb57b08c7f 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -28,7 +28,7 @@ from pyomo.core.base import value from pyomo.core.expr import MonomialTermExpression from pyomo.common.collections import ComponentSet, ComponentMap -from pyomo.core.base.var import _VarData as VarData +from pyomo.core.base.var import VarData as VarData from itertools import chain from pyomo.common.dependencies import numpy as np diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index cd635e795fc..166fbada4ff 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -5,7 +5,7 @@ import logging import unittest -from pyomo.core.base import ConcreteModel, Var, _VarData +from pyomo.core.base import ConcreteModel, Var, VarData from pyomo.common.log import LoggingIntercept from pyomo.common.errors import ApplicationError from pyomo.core.base.param import Param, ParamData @@ -38,7 +38,7 @@ def test_single_component_data(self): mdl = ConcreteModel() mdl.v = Var([0, 1]) - standardizer_func = InputDataStandardizer(Var, _VarData) + standardizer_func = InputDataStandardizer(Var, VarData) standardizer_input = mdl.v[0] standardizer_output = standardizer_func(standardizer_input) @@ -74,7 +74,7 @@ def test_standardizer_indexed_component(self): mdl = ConcreteModel() mdl.v = Var([0, 1]) - standardizer_func = InputDataStandardizer(Var, _VarData) + standardizer_func = InputDataStandardizer(Var, VarData) standardizer_input = mdl.v standardizer_output = standardizer_func(standardizer_input) @@ -113,7 +113,7 @@ def test_standardizer_multiple_components(self): mdl.v = Var([0, 1]) mdl.x = Var(["a", "b"]) - standardizer_func = InputDataStandardizer(Var, _VarData) + standardizer_func = InputDataStandardizer(Var, VarData) standardizer_input = [mdl.v[0], mdl.x] standardizer_output = standardizer_func(standardizer_input) @@ -154,7 +154,7 @@ def test_standardizer_invalid_duplicates(self): mdl.v = Var([0, 1]) mdl.x = Var(["a", "b"]) - standardizer_func = InputDataStandardizer(Var, _VarData, allow_repeats=False) + standardizer_func = InputDataStandardizer(Var, VarData, allow_repeats=False) exc_str = r"Standardized.*list.*contains duplicate entries\." with self.assertRaisesRegex(ValueError, exc_str): @@ -165,7 +165,7 @@ def test_standardizer_invalid_type(self): Test standardizer raises exception as expected when input is of invalid type. """ - standardizer_func = InputDataStandardizer(Var, _VarData) + standardizer_func = InputDataStandardizer(Var, VarData) exc_str = r"Input object .*is not of valid component type.*" with self.assertRaisesRegex(TypeError, exc_str): @@ -178,7 +178,7 @@ def test_standardizer_iterable_with_invalid_type(self): """ mdl = ConcreteModel() mdl.v = Var([0, 1]) - standardizer_func = InputDataStandardizer(Var, _VarData) + standardizer_func = InputDataStandardizer(Var, VarData) exc_str = r"Input object .*entry of iterable.*is not of valid component type.*" with self.assertRaisesRegex(TypeError, exc_str): @@ -189,7 +189,7 @@ def test_standardizer_invalid_str_passed(self): Test standardizer raises exception as expected when input is of invalid type str. """ - standardizer_func = InputDataStandardizer(Var, _VarData) + standardizer_func = InputDataStandardizer(Var, VarData) exc_str = r"Input object .*is not of valid component type.*" with self.assertRaisesRegex(TypeError, exc_str): diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index c308f0d6990..8093e93c8ef 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -19,7 +19,7 @@ from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.core.base.set_types import NonNegativeIntegers -from pyomo.core.base.var import _VarData +from pyomo.core.base.var import VarData from pyomo.core.expr import ( identify_variables, identify_mutable_parameters, @@ -592,7 +592,7 @@ def test_dr_eqns_form_correct(self): param_product_multiplicand = term.args[0] dr_var_multiplicand = term.args[1] else: - self.assertIsInstance(term, _VarData) + self.assertIsInstance(term, VarData) param_product_multiplicand = 1 dr_var_multiplicand = term diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index a3ab3464aa8..65fb0a2c6aa 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -832,7 +832,7 @@ def get_state_vars(blk, first_stage_variables, second_stage_variables): Get state variables of a modeling block. The state variables with respect to `blk` are the unfixed - `_VarData` objects participating in the active objective + `VarData` objects participating in the active objective or constraints descended from `blk` which are not first-stage variables or second-stage variables. @@ -847,7 +847,7 @@ def get_state_vars(blk, first_stage_variables, second_stage_variables): Yields ------ - _VarData + VarData State variable. """ dof_var_set = ComponentSet(first_stage_variables) | ComponentSet( @@ -954,7 +954,7 @@ def validate_variable_partitioning(model, config): Returns ------- - list of _VarData + list of VarData State variables of the model. Raises diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 408cf16c00e..0363380af1f 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -57,7 +57,7 @@ from pyomo.core.base.check import BuildCheck from pyomo.core.base.set import Set, SetOf, simple_set_rule, RangeSet from pyomo.core.base.param import Param -from pyomo.core.base.var import Var, _VarData, GeneralVarData, ScalarVar, VarList +from pyomo.core.base.var import Var, VarData, GeneralVarData, ScalarVar, VarList from pyomo.core.base.boolean_var import ( BooleanVar, BooleanVarData, diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index 287851a7f7e..925dca530a7 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -270,14 +270,14 @@ def stale(self, val): self._stale = StaleFlagManager.get_flag(0) def get_associated_binary(self): - """Get the binary _VarData associated with this + """Get the binary VarData associated with this GeneralBooleanVarData""" return ( self._associated_binary() if self._associated_binary is not None else None ) def associate_binary_var(self, binary_var): - """Associate a binary _VarData to this GeneralBooleanVarData""" + """Associate a binary VarData to this GeneralBooleanVarData""" if ( self._associated_binary is not None and type(self._associated_binary) diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index 43f8ddbfef5..f061ebfbdc8 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -47,7 +47,7 @@ from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.constraint import Constraint, ConstraintList from pyomo.core.base.sos import SOSConstraint -from pyomo.core.base.var import Var, _VarData, IndexedVar +from pyomo.core.base.var import Var, VarData, IndexedVar from pyomo.core.base.set_types import PositiveReals, NonNegativeReals, Binary from pyomo.core.base.util import flatten_tuple @@ -1240,7 +1240,7 @@ def __init__(self, *args, **kwds): # Check that the variables args are actually Pyomo Vars if not ( - isinstance(self._domain_var, _VarData) + isinstance(self._domain_var, VarData) or isinstance(self._domain_var, IndexedVar) ): msg = ( @@ -1249,7 +1249,7 @@ def __init__(self, *args, **kwds): ) raise TypeError(msg % (repr(self._domain_var),)) if not ( - isinstance(self._range_var, _VarData) + isinstance(self._range_var, VarData) or isinstance(self._range_var, IndexedVar) ): msg = ( @@ -1359,22 +1359,22 @@ def add(self, index, _is_indexed=None): _self_yvar = None _self_domain_pts_index = None if not _is_indexed: - # allows one to mix Var and _VarData as input to + # allows one to mix Var and VarData as input to # non-indexed Piecewise, index would be None in this case - # so for Var elements Var[None] is Var, but _VarData[None] would fail + # so for Var elements Var[None] is Var, but VarData[None] would fail _self_xvar = self._domain_var _self_yvar = self._range_var _self_domain_pts_index = self._domain_points[index] else: - # The following allows one to specify a Var or _VarData + # The following allows one to specify a Var or VarData # object even with an indexed Piecewise component. # The most common situation will most likely be a VarArray, # so we try this first. - if not isinstance(self._domain_var, _VarData): + if not isinstance(self._domain_var, VarData): _self_xvar = self._domain_var[index] else: _self_xvar = self._domain_var - if not isinstance(self._range_var, _VarData): + if not isinstance(self._range_var, VarData): _self_yvar = self._range_var[index] else: _self_yvar = self._range_var diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index 0e45ad44225..509238e4e6b 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -88,7 +88,7 @@ ) -class _VarData(ComponentData, NumericValue): +class VarData(ComponentData, NumericValue): """This class defines the abstract interface for a single variable. Note that this "abstract" class is not intended to be directly @@ -319,7 +319,12 @@ def free(self): return self.unfix() -class GeneralVarData(_VarData): +class _VarData(metaclass=RenamedClass): + __renamed__new_class__ = VarData + __renamed__version__ = '6.7.2.dev0' + + +class GeneralVarData(VarData): """This class defines the data for a single variable.""" __slots__ = ('_value', '_lb', '_ub', '_domain', '_fixed', '_stale') @@ -329,7 +334,7 @@ def __init__(self, component=None): # # These lines represent in-lining of the # following constructors: - # - _VarData + # - VarData # - ComponentData # - NumericValue self._component = weakref_ref(component) if (component is not None) else None @@ -448,9 +453,9 @@ def domain(self, domain): ) raise - @_VarData.bounds.getter + @VarData.bounds.getter def bounds(self): - # Custom implementation of _VarData.bounds to avoid unnecessary + # Custom implementation of VarData.bounds to avoid unnecessary # expression generation and duplicate calls to domain.bounds() domain_lb, domain_ub = self.domain.bounds() # lb is the tighter of the domain and bounds @@ -491,9 +496,9 @@ def bounds(self): ub = min(ub, domain_ub) return lb, ub - @_VarData.lb.getter + @VarData.lb.getter def lb(self): - # Custom implementation of _VarData.lb to avoid unnecessary + # Custom implementation of VarData.lb to avoid unnecessary # expression generation domain_lb, domain_ub = self.domain.bounds() # lb is the tighter of the domain and bounds @@ -516,9 +521,9 @@ def lb(self): lb = max(lb, domain_lb) return lb - @_VarData.ub.getter + @VarData.ub.getter def ub(self): - # Custom implementation of _VarData.ub to avoid unnecessary + # Custom implementation of VarData.ub to avoid unnecessary # expression generation domain_lb, domain_ub = self.domain.bounds() # ub is the tighter of the domain and bounds @@ -780,7 +785,7 @@ def add(self, index): def construct(self, data=None): """ - Construct the _VarData objects for this variable + Construct the VarData objects for this variable """ if self._constructed: return @@ -839,7 +844,7 @@ def construct(self, data=None): # initializers that are constant, we can avoid # re-calling (and re-validating) the inputs in certain # cases. To support this, we will create the first - # _VarData and then use it as a template to initialize + # VarData and then use it as a template to initialize # (constant portions of) every VarData so as to not # repeat all the domain/bounds validation. try: @@ -1008,7 +1013,7 @@ def fix(self, value=NOTSET, skip_validation=False): def unfix(self): """Unfix all variables in this :class:`IndexedVar` (treat as variable) - This sets the :attr:`_VarData.fixed` indicator to False for + This sets the :attr:`VarData.fixed` indicator to False for every variable in this :class:`IndexedVar`. """ diff --git a/pyomo/core/beta/dict_objects.py b/pyomo/core/beta/dict_objects.py index 7c44166f189..eedb3c45bf3 100644 --- a/pyomo/core/beta/dict_objects.py +++ b/pyomo/core/beta/dict_objects.py @@ -14,7 +14,7 @@ from pyomo.common.log import is_debug_set from pyomo.core.base.set_types import Any -from pyomo.core.base.var import IndexedVar, _VarData +from pyomo.core.base.var import IndexedVar, VarData from pyomo.core.base.constraint import IndexedConstraint, ConstraintData from pyomo.core.base.objective import IndexedObjective, ObjectiveData from pyomo.core.base.expression import IndexedExpression, ExpressionData @@ -184,7 +184,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentDict needs to # go last in order to handle any initialization # iterable as an argument - ComponentDict.__init__(self, _VarData, *args, **kwds) + ComponentDict.__init__(self, VarData, *args, **kwds) class ConstraintDict(ComponentDict, IndexedConstraint): diff --git a/pyomo/core/beta/list_objects.py b/pyomo/core/beta/list_objects.py index d10a30e18e2..005bfc38a1f 100644 --- a/pyomo/core/beta/list_objects.py +++ b/pyomo/core/beta/list_objects.py @@ -14,7 +14,7 @@ from pyomo.common.log import is_debug_set from pyomo.core.base.set_types import Any -from pyomo.core.base.var import IndexedVar, _VarData +from pyomo.core.base.var import IndexedVar, VarData from pyomo.core.base.constraint import IndexedConstraint, ConstraintData from pyomo.core.base.objective import IndexedObjective, ObjectiveData from pyomo.core.base.expression import IndexedExpression, ExpressionData @@ -232,7 +232,7 @@ def __init__(self, *args, **kwds): # Constructor for ComponentList needs to # go last in order to handle any initialization # iterable as an argument - ComponentList.__init__(self, _VarData, *args, **kwds) + ComponentList.__init__(self, VarData, *args, **kwds) class XConstraintList(ComponentList, IndexedConstraint): diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index 25d83ca20f4..50abaeedbba 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -1238,7 +1238,7 @@ class LinearExpression(SumExpression): - not potentially variable (e.g., native types, Params, or NPV expressions) - :py:class:`MonomialTermExpression` - - :py:class:`_VarData` + - :py:class:`VarData` Args: args (tuple): Children nodes diff --git a/pyomo/core/plugins/transform/eliminate_fixed_vars.py b/pyomo/core/plugins/transform/eliminate_fixed_vars.py index 9312035b8c8..934228afd7c 100644 --- a/pyomo/core/plugins/transform/eliminate_fixed_vars.py +++ b/pyomo/core/plugins/transform/eliminate_fixed_vars.py @@ -11,7 +11,7 @@ from pyomo.core.expr import ExpressionBase, as_numeric from pyomo.core import Constraint, Objective, TransformationFactory -from pyomo.core.base.var import Var, _VarData +from pyomo.core.base.var import Var, VarData from pyomo.core.util import sequence from pyomo.core.plugins.transform.hierarchy import IsomorphicTransformation @@ -77,7 +77,7 @@ def _fix_vars(self, expr, model): if isinstance(expr._args[i], ExpressionBase): _args.append(self._fix_vars(expr._args[i], model)) elif ( - isinstance(expr._args[i], Var) or isinstance(expr._args[i], _VarData) + isinstance(expr._args[i], Var) or isinstance(expr._args[i], VarData) ) and expr._args[i].fixed: if expr._args[i].value != 0.0: _args.append(as_numeric(expr._args[i].value)) diff --git a/pyomo/core/plugins/transform/radix_linearization.py b/pyomo/core/plugins/transform/radix_linearization.py index c67e556d60c..92270655f31 100644 --- a/pyomo/core/plugins/transform/radix_linearization.py +++ b/pyomo/core/plugins/transform/radix_linearization.py @@ -21,7 +21,7 @@ Block, RangeSet, ) -from pyomo.core.base.var import _VarData +from pyomo.core.base.var import VarData import logging @@ -268,8 +268,8 @@ def _collect_bilinear(self, expr, bilin, quad): self._collect_bilinear(e, bilin, quad) # No need to check denominator, as this is poly_degree==2 return - if not isinstance(expr._numerator[0], _VarData) or not isinstance( - expr._numerator[1], _VarData + if not isinstance(expr._numerator[0], VarData) or not isinstance( + expr._numerator[1], VarData ): raise RuntimeError("Cannot yet handle complex subexpressions") if expr._numerator[0] is expr._numerator[1]: diff --git a/pyomo/core/tests/unit/test_piecewise.py b/pyomo/core/tests/unit/test_piecewise.py index af82ef7c06d..7b8e01e6a45 100644 --- a/pyomo/core/tests/unit/test_piecewise.py +++ b/pyomo/core/tests/unit/test_piecewise.py @@ -104,7 +104,7 @@ def test_indexed_with_nonindexed_vars(self): model.con3 = Piecewise(*args, **keywords) # test that nonindexed Piecewise can handle - # _VarData (e.g model.x[1] + # VarData (e.g model.x[1] def test_nonindexed_with_indexed_vars(self): model = ConcreteModel() model.range = Var([1]) diff --git a/pyomo/core/tests/unit/test_var_set_bounds.py b/pyomo/core/tests/unit/test_var_set_bounds.py index bae89556ce3..1686ba4f1c6 100644 --- a/pyomo/core/tests/unit/test_var_set_bounds.py +++ b/pyomo/core/tests/unit/test_var_set_bounds.py @@ -36,7 +36,7 @@ # GAH: These tests been temporarily disabled. It is no longer the job of Var # to validate its domain at the time of construction. It only needs to # ensure that whatever object is passed as its domain is suitable for -# interacting with the _VarData interface (e.g., has a bounds method) +# interacting with the VarData interface (e.g., has a bounds method) # The plan is to start adding functionality to the solver interfaces # that will support custom domains. diff --git a/pyomo/dae/misc.py b/pyomo/dae/misc.py index 3e09a055577..dcb73f60c9e 100644 --- a/pyomo/dae/misc.py +++ b/pyomo/dae/misc.py @@ -263,7 +263,7 @@ def _update_var(v): # Note: This is not required it is handled by the _default method on # Var (which is now a IndexedComponent). However, it # would be much slower to rely on that method to generate new - # _VarData for a large number of new indices. + # VarData for a large number of new indices. new_indices = set(v.index_set()) - set(v._data.keys()) for index in new_indices: v.add(index) diff --git a/pyomo/repn/plugins/ampl/ampl_.py b/pyomo/repn/plugins/ampl/ampl_.py index 840bee2166c..1cff45b30c1 100644 --- a/pyomo/repn/plugins/ampl/ampl_.py +++ b/pyomo/repn/plugins/ampl/ampl_.py @@ -168,8 +168,8 @@ def _build_op_template(): _op_template[EXPR.EqualityExpression] = "o24{C}\n" _op_comment[EXPR.EqualityExpression] = "\t#eq" - _op_template[var._VarData] = "v%d{C}\n" - _op_comment[var._VarData] = "\t#%s" + _op_template[var.VarData] = "v%d{C}\n" + _op_comment[var.VarData] = "\t#%s" _op_template[param.ParamData] = "n%r{C}\n" _op_comment[param.ParamData] = "" @@ -733,16 +733,16 @@ def _print_nonlinear_terms_NL(self, exp): % (exp_type) ) - elif isinstance(exp, (var._VarData, IVariable)) and (not exp.is_fixed()): + elif isinstance(exp, (var.VarData, IVariable)) and (not exp.is_fixed()): # (self._output_fixed_variable_bounds or if not self._symbolic_solver_labels: OUTPUT.write( - self._op_string[var._VarData] + self._op_string[var.VarData] % (self.ampl_var_id[self._varID_map[id(exp)]]) ) else: OUTPUT.write( - self._op_string[var._VarData] + self._op_string[var.VarData] % ( self.ampl_var_id[self._varID_map[id(exp)]], self._name_labeler(exp), diff --git a/pyomo/repn/plugins/cpxlp.py b/pyomo/repn/plugins/cpxlp.py index 6228e7c7286..45f4279f8fe 100644 --- a/pyomo/repn/plugins/cpxlp.py +++ b/pyomo/repn/plugins/cpxlp.py @@ -60,7 +60,7 @@ def __init__(self): # The LP writer tracks which variables are # referenced in constraints, so that a user does not end up with a # zillion "unreferenced variables" warning messages. - # This dictionary maps id(_VarData) -> _VarData. + # This dictionary maps id(VarData) -> VarData. self._referenced_variable_ids = {} # Per ticket #4319, we are using %.17g, which mocks the diff --git a/pyomo/repn/plugins/mps.py b/pyomo/repn/plugins/mps.py index ba26783eea1..e1a0d2187fc 100644 --- a/pyomo/repn/plugins/mps.py +++ b/pyomo/repn/plugins/mps.py @@ -62,7 +62,7 @@ def __init__(self, int_marker=False): # referenced in constraints, so that one doesn't end up with a # zillion "unreferenced variables" warning messages. stored at # the object level to avoid additional method arguments. - # dictionary of id(_VarData)->_VarData. + # dictionary of id(VarData)->VarData. self._referenced_variable_ids = {} # Keven Hunter made a nice point about using %.16g in his attachment diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 1a49238f35b..e1691e75f2f 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -77,7 +77,7 @@ ObjectiveData, ) from pyomo.core.base.suffix import SuffixFinder -from pyomo.core.base.var import _VarData +from pyomo.core.base.var import VarData import pyomo.core.kernel as kernel from pyomo.core.pyomoobject import PyomoObject from pyomo.opt import WriterFactory @@ -129,7 +129,7 @@ class NLWriterInfo(object): Attributes ---------- - variables: List[_VarData] + variables: List[VarData] The list of (unfixed) Pyomo model variables in the order written to the NL file @@ -162,10 +162,10 @@ class NLWriterInfo(object): file in the same order as the :py:attr:`variables` and generated .col file. - eliminated_vars: List[Tuple[_VarData, NumericExpression]] + eliminated_vars: List[Tuple[VarData, NumericExpression]] The list of variables in the model that were eliminated by the - presolve. Each entry is a 2-tuple of (:py:class:`_VarData`, + presolve. Each entry is a 2-tuple of (:py:class:`VarData`, :py:class`NumericExpression`|`float`). The list is in the necessary order for correct evaluation (i.e., all variables appearing in the expression must either have been sent to the @@ -466,7 +466,7 @@ def compile(self, column_order, row_order, obj_order, model_id): self.obj[obj_order[_id]] = val elif _id == model_id: self.prob[0] = val - elif isinstance(obj, (_VarData, ConstraintData, ObjectiveData)): + elif isinstance(obj, (VarData, ConstraintData, ObjectiveData)): missing_component_data.add(obj) elif isinstance(obj, (Var, Constraint, Objective)): # Expand this indexed component to store the diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index e6dc217acc9..434a9b8f35a 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -84,17 +84,17 @@ class LinearStandardFormInfo(object): +/- 1 indicating if the row was multiplied by -1 (corresponding to a constraint lower bound) or +1 (upper bound). - columns : List[_VarData] + columns : List[VarData] The list of Pyomo variable objects corresponding to columns in the `A` and `c` matrices. - eliminated_vars: List[Tuple[_VarData, NumericExpression]] + eliminated_vars: List[Tuple[VarData, NumericExpression]] The list of variables from the original model that do not appear in the standard form (usually because they were replaced by nonnegative variables). Each entry is a 2-tuple of - (:py:class:`_VarData`, :py:class`NumericExpression`|`float`). + (:py:class:`VarData`, :py:class`NumericExpression`|`float`). The list is in the necessary order for correct evaluation (i.e., all variables appearing in the expression must either have appeared in the standard form, or appear *earlier* in this list. diff --git a/pyomo/solvers/plugins/solvers/cplex_persistent.py b/pyomo/solvers/plugins/solvers/cplex_persistent.py index fd396a8c87f..754dadc09e2 100644 --- a/pyomo/solvers/plugins/solvers/cplex_persistent.py +++ b/pyomo/solvers/plugins/solvers/cplex_persistent.py @@ -82,7 +82,7 @@ def update_var(self, var): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) """ # see PR #366 for discussion about handling indexed @@ -130,7 +130,7 @@ def _add_column(self, var, obj_coef, constraints, coefficients): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) obj_coef: float constraints: list of solver constraints coefficients: list of coefficients to put on var in the associated constraint diff --git a/pyomo/solvers/plugins/solvers/gurobi_persistent.py b/pyomo/solvers/plugins/solvers/gurobi_persistent.py index 585a78e3ef1..97a3533c3f9 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_persistent.py +++ b/pyomo/solvers/plugins/solvers/gurobi_persistent.py @@ -111,7 +111,7 @@ def update_var(self, var): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) """ # see PR #366 for discussion about handling indexed @@ -710,7 +710,7 @@ def _add_column(self, var, obj_coef, constraints, coefficients): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) obj_coef: float constraints: list of solver constraints coefficients: list of coefficients to put on var in the associated constraint diff --git a/pyomo/solvers/plugins/solvers/mosek_persistent.py b/pyomo/solvers/plugins/solvers/mosek_persistent.py index 9e7f8de1b41..efcbb7dd9dd 100644 --- a/pyomo/solvers/plugins/solvers/mosek_persistent.py +++ b/pyomo/solvers/plugins/solvers/mosek_persistent.py @@ -95,7 +95,7 @@ def remove_var(self, solver_var): This will keep any other model components intact. Parameters ---------- - solver_var: Var (scalar Var or single _VarData) + solver_var: Var (scalar Var or single VarData) """ self.remove_vars(solver_var) @@ -106,7 +106,7 @@ def remove_vars(self, *solver_vars): This will keep any other model components intact. Parameters ---------- - *solver_var: Var (scalar Var or single _VarData) + *solver_var: Var (scalar Var or single VarData) """ try: var_ids = [] diff --git a/pyomo/solvers/plugins/solvers/persistent_solver.py b/pyomo/solvers/plugins/solvers/persistent_solver.py index 79cd669dd71..3c2a9e52eab 100644 --- a/pyomo/solvers/plugins/solvers/persistent_solver.py +++ b/pyomo/solvers/plugins/solvers/persistent_solver.py @@ -206,7 +206,7 @@ def add_column(self, model, var, obj_coef, constraints, coefficients): Parameters ---------- model: pyomo ConcreteModel to which the column will be added - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) obj_coef: float, pyo.Param constraints: list of scalar Constraints of single ConstraintDatas coefficients: list of the coefficient to put on var in the associated constraint @@ -380,7 +380,7 @@ def remove_var(self, var): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) """ # see PR #366 for discussion about handling indexed diff --git a/pyomo/solvers/plugins/solvers/xpress_persistent.py b/pyomo/solvers/plugins/solvers/xpress_persistent.py index 513a7fbc257..fbdc2866dcf 100644 --- a/pyomo/solvers/plugins/solvers/xpress_persistent.py +++ b/pyomo/solvers/plugins/solvers/xpress_persistent.py @@ -90,7 +90,7 @@ def update_var(self, var): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) """ # see PR #366 for discussion about handling indexed @@ -124,7 +124,7 @@ def _add_column(self, var, obj_coef, constraints, coefficients): Parameters ---------- - var: Var (scalar Var or single _VarData) + var: Var (scalar Var or single VarData) obj_coef: float constraints: list of solver constraints coefficients: list of coefficients to put on var in the associated constraint diff --git a/pyomo/util/calc_var_value.py b/pyomo/util/calc_var_value.py index d5bceb5c67b..254b82c59cd 100644 --- a/pyomo/util/calc_var_value.py +++ b/pyomo/util/calc_var_value.py @@ -53,7 +53,7 @@ def calculate_variable_from_constraint( Parameters: ----------- - variable: :py:class:`_VarData` + variable: :py:class:`VarData` The variable to solve for constraint: :py:class:`ConstraintData` or relational expression or `tuple` The equality constraint to use to solve for the variable value. From 0e994faacb398642756fdd12cf0802a49cf92dc4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 18:06:48 -0600 Subject: [PATCH 1009/3044] Revert "Renamed _SuffixData -> SuffixData" This reverts commit 430f98207353b1e1e7383504aa0336bc852aeb9c. --- pyomo/core/base/suffix.py | 4 ++-- pyomo/repn/plugins/nl_writer.py | 15 +++++---------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/pyomo/core/base/suffix.py b/pyomo/core/base/suffix.py index c4b37789773..be2f732650d 100644 --- a/pyomo/core/base/suffix.py +++ b/pyomo/core/base/suffix.py @@ -129,7 +129,7 @@ class SuffixDirection(enum.IntEnum): IMPORT_EXPORT = 3 -SuffixDataTypeDomain = In(SuffixDataType) +_SuffixDataTypeDomain = In(SuffixDataType) _SuffixDirectionDomain = In(SuffixDirection) @@ -253,7 +253,7 @@ def datatype(self): def datatype(self, datatype): """Set the suffix datatype.""" if datatype is not None: - datatype = SuffixDataTypeDomain(datatype) + datatype = _SuffixDataTypeDomain(datatype) self._datatype = datatype @property diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index e1691e75f2f..23e14104b89 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -423,7 +423,7 @@ def _generate_symbol_map(self, info): return symbol_map -class SuffixData(object): +class _SuffixData(object): def __init__(self, name): self.name = name self.obj = {} @@ -505,11 +505,6 @@ def compile(self, column_order, row_order, obj_order, model_id): ) -class _SuffixData(metaclass=RenamedClass): - __renamed__new_class__ = SuffixData - __renamed__version__ = '6.7.2.dev0' - - class CachingNumericSuffixFinder(SuffixFinder): scale = True @@ -642,7 +637,7 @@ def write(self, model): continue name = suffix.local_name if name not in suffix_data: - suffix_data[name] = SuffixData(name) + suffix_data[name] = _SuffixData(name) suffix_data[name].update(suffix) # # Data structures to support variable/constraint scaling @@ -999,7 +994,7 @@ def write(self, model): "model. To avoid this error please use only one of " "these methods to define special ordered sets." ) - suffix_data[name] = SuffixData(name) + suffix_data[name] = _SuffixData(name) suffix_data[name].datatype.add(Suffix.INT) sos_id = 0 sosno = suffix_data['sosno'] @@ -1349,7 +1344,7 @@ def write(self, model): if not _vals: continue ostream.write(f"S{_field|_float} {len(_vals)} {name}\n") - # Note: SuffixData.compile() guarantees the value is int/float + # Note: _SuffixData.compile() guarantees the value is int/float ostream.write( ''.join(f"{_id} {_vals[_id]!r}\n" for _id in sorted(_vals)) ) @@ -1459,7 +1454,7 @@ def write(self, model): logger.warning("ignoring 'dual' suffix for Model") if data.con: ostream.write(f"d{len(data.con)}\n") - # Note: SuffixData.compile() guarantees the value is int/float + # Note: _SuffixData.compile() guarantees the value is int/float ostream.write( ''.join(f"{_id} {data.con[_id]!r}\n" for _id in sorted(data.con)) ) From b1a7b30cecf901ce0e1c4a9c146814b250e4cc75 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 19:44:54 -0600 Subject: [PATCH 1010/3044] Update ComponentData imports, provide deprecation paths --- pyomo/core/base/__init__.py | 110 +++++++++++++++++++++-------------- pyomo/core/base/piecewise.py | 2 +- pyomo/core/base/sets.py | 2 +- pyomo/gdp/__init__.py | 8 ++- 4 files changed, 75 insertions(+), 47 deletions(-) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 0363380af1f..9a06a3e02bb 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -33,10 +33,14 @@ BooleanValue, native_logical_values, ) + from pyomo.core.kernel.objective import minimize, maximize -from pyomo.core.base.config import PyomoOptions -from pyomo.core.base.expression import Expression, ExpressionData +from pyomo.core.base.component import name, Component, ModelComponentFactory +from pyomo.core.base.componentuid import ComponentUID +from pyomo.core.base.config import PyomoOptions +from pyomo.core.base.enums import SortComponents, TraversalStrategy +from pyomo.core.base.instance2dat import instance2dat from pyomo.core.base.label import ( CuidLabeler, CounterLabeler, @@ -47,17 +51,37 @@ NameLabeler, ShortNameLabeler, ) +from pyomo.core.base.misc import display +from pyomo.core.base.reference import Reference +from pyomo.core.base.symbol_map import symbol_map_from_instance +from pyomo.core.base.transformation import ( + Transformation, + TransformationFactory, + ReverseTransformationToken, +) + +from pyomo.core.base.PyomoModel import ( + global_option, + ModelSolution, + ModelSolutions, + Model, + ConcreteModel, + AbstractModel, +) # # Components # -from pyomo.core.base.component import name, Component, ModelComponentFactory -from pyomo.core.base.componentuid import ComponentUID from pyomo.core.base.action import BuildAction -from pyomo.core.base.check import BuildCheck -from pyomo.core.base.set import Set, SetOf, simple_set_rule, RangeSet -from pyomo.core.base.param import Param -from pyomo.core.base.var import Var, VarData, GeneralVarData, ScalarVar, VarList +from pyomo.core.base.block import ( + Block, + BlockData, + ScalarBlock, + active_components, + components, + active_components_data, + components_data, +) from pyomo.core.base.boolean_var import ( BooleanVar, BooleanVarData, @@ -65,6 +89,8 @@ BooleanVarList, ScalarBooleanVar, ) +from pyomo.core.base.check import BuildCheck +from pyomo.core.base.connector import Connector, ConnectorData from pyomo.core.base.constraint import ( simple_constraint_rule, simple_constraintlist_rule, @@ -72,6 +98,8 @@ Constraint, ConstraintData, ) +from pyomo.core.base.expression import Expression, ExpressionData +from pyomo.core.base.external import ExternalFunction from pyomo.core.base.logical_constraint import ( LogicalConstraint, LogicalConstraintList, @@ -84,19 +112,13 @@ ObjectiveList, ObjectiveData, ) -from pyomo.core.base.connector import Connector -from pyomo.core.base.sos import SOSConstraint -from pyomo.core.base.piecewise import Piecewise -from pyomo.core.base.suffix import ( - active_export_suffix_generator, - active_import_suffix_generator, - Suffix, -) -from pyomo.core.base.external import ExternalFunction -from pyomo.core.base.symbol_map import symbol_map_from_instance -from pyomo.core.base.reference import Reference - +from pyomo.core.base.param import Param, ParamData +from pyomo.core.base.piecewise import Piecewise, PiecewiseData from pyomo.core.base.set import ( + Set, + SetData, + SetOf, + RangeSet, Reals, PositiveReals, NonPositiveReals, @@ -116,34 +138,19 @@ PercentFraction, RealInterval, IntegerInterval, + simple_set_rule, ) -from pyomo.core.base.misc import display -from pyomo.core.base.block import ( - Block, - ScalarBlock, - active_components, - components, - active_components_data, - components_data, -) -from pyomo.core.base.enums import SortComponents, TraversalStrategy -from pyomo.core.base.PyomoModel import ( - global_option, - ModelSolution, - ModelSolutions, - Model, - ConcreteModel, - AbstractModel, -) -from pyomo.core.base.transformation import ( - Transformation, - TransformationFactory, - ReverseTransformationToken, +from pyomo.core.base.sos import SOSConstraint, SOSConstraintData +from pyomo.core.base.suffix import ( + active_export_suffix_generator, + active_import_suffix_generator, + Suffix, ) +from pyomo.core.base.var import Var, VarData, GeneralVarData, ScalarVar, VarList -from pyomo.core.base.instance2dat import instance2dat - +# # These APIs are deprecated and should be removed in the near future +# from pyomo.core.base.set import set_options, RealSet, IntegerSet, BooleanSet from pyomo.common.deprecation import relocated_module_attribute @@ -155,4 +162,19 @@ relocated_module_attribute( 'SimpleBooleanVar', 'pyomo.core.base.boolean_var.SimpleBooleanVar', version='6.0' ) +# Historically, only a subset of "private" component data classes were imported here +for _cdata in ( + 'ConstraintData', + 'LogicalConstraintData', + 'ExpressionData', + 'VarData', + 'GeneralVarData', + 'GeneralBooleanVarData', + 'BooleanVarData', + 'ObjectiveData', +): + relocated_module_attribute( + f'_{_cdata}', f'pyomo.core.base.{_cdata}', version='6.7.2.dev0' + ) +del _cdata del relocated_module_attribute diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index f061ebfbdc8..efe500dbfb1 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -40,7 +40,7 @@ import enum from pyomo.common.log import is_debug_set -from pyomo.common.deprecation import deprecation_warning +from pyomo.common.deprecation import RenamedClass, deprecation_warning from pyomo.common.numeric_types import value from pyomo.common.timing import ConstructionTimer from pyomo.core.base.block import Block, BlockData diff --git a/pyomo/core/base/sets.py b/pyomo/core/base/sets.py index 3ebdc6875d1..72d49479dd3 100644 --- a/pyomo/core/base/sets.py +++ b/pyomo/core/base/sets.py @@ -17,7 +17,7 @@ process_setarg, set_options, simple_set_rule, - SetDataBase, + _SetDataBase, SetData, Set, SetOf, diff --git a/pyomo/gdp/__init__.py b/pyomo/gdp/__init__.py index a18bc03084a..d204369cdba 100644 --- a/pyomo/gdp/__init__.py +++ b/pyomo/gdp/__init__.py @@ -9,7 +9,13 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.gdp.disjunct import GDP_Error, Disjunct, Disjunction +from pyomo.gdp.disjunct import ( + GDP_Error, + Disjunct, + DisjunctData, + Disjunction, + DisjunctionData, +) # Do not import these files: importing them registers the transformation # plugins with the pyomo script so that they get automatically invoked. From 1e5b54e7a30ee6ad9de1a956bd1f975148c7efd9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 20:17:28 -0600 Subject: [PATCH 1011/3044] Mrege GeneralVarData into VarData class --- pyomo/core/base/var.py | 439 +++++++++++++++++------------------------ 1 file changed, 183 insertions(+), 256 deletions(-) diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index 509238e4e6b..b1634b61c44 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -85,246 +85,11 @@ 'value', 'stale', 'fixed', + ('__call__', "access property 'value' on"), ) class VarData(ComponentData, NumericValue): - """This class defines the abstract interface for a single variable. - - Note that this "abstract" class is not intended to be directly - instantiated. - - """ - - __slots__ = () - - # - # Interface - # - - def has_lb(self): - """Returns :const:`False` when the lower bound is - :const:`None` or negative infinity""" - return self.lb is not None - - def has_ub(self): - """Returns :const:`False` when the upper bound is - :const:`None` or positive infinity""" - return self.ub is not None - - # TODO: deprecate this? Properties are generally preferred over "set*()" - def setlb(self, val): - """ - Set the lower bound for this variable after validating that - the value is fixed (or None). - """ - self.lower = val - - # TODO: deprecate this? Properties are generally preferred over "set*()" - def setub(self, val): - """ - Set the upper bound for this variable after validating that - the value is fixed (or None). - """ - self.upper = val - - @property - def bounds(self): - """Returns (or set) the tuple (lower bound, upper bound). - - This returns the current (numeric) values of the lower and upper - bounds as a tuple. If there is no bound, returns None (and not - +/-inf) - - """ - return self.lb, self.ub - - @bounds.setter - def bounds(self, val): - self.lower, self.upper = val - - @property - def lb(self): - """Return (or set) the numeric value of the variable lower bound.""" - lb = value(self.lower) - return None if lb == _ninf else lb - - @lb.setter - def lb(self, val): - self.lower = val - - @property - def ub(self): - """Return (or set) the numeric value of the variable upper bound.""" - ub = value(self.upper) - return None if ub == _inf else ub - - @ub.setter - def ub(self, val): - self.upper = val - - def is_integer(self): - """Returns True when the domain is a contiguous integer range.""" - _id = id(self.domain) - if _id in _known_global_real_domains: - return not _known_global_real_domains[_id] - _interval = self.domain.get_interval() - if _interval is None: - return False - # Note: it is not sufficient to just check the step: the - # starting / ending points must be integers (or not specified) - start, stop, step = _interval - return ( - step == 1 - and (start is None or int(start) == start) - and (stop is None or int(stop) == stop) - ) - - def is_binary(self): - """Returns True when the domain is restricted to Binary values.""" - domain = self.domain - if domain is Binary: - return True - if id(domain) in _known_global_real_domains: - return False - return domain.get_interval() == (0, 1, 1) - - def is_continuous(self): - """Returns True when the domain is a continuous real range""" - _id = id(self.domain) - if _id in _known_global_real_domains: - return _known_global_real_domains[_id] - _interval = self.domain.get_interval() - return _interval is not None and _interval[2] == 0 - - def is_fixed(self): - """Returns True if this variable is fixed, otherwise returns False.""" - return self.fixed - - def is_constant(self): - """Returns False because this is not a constant in an expression.""" - return False - - def is_variable_type(self): - """Returns True because this is a variable.""" - return True - - def is_potentially_variable(self): - """Returns True because this is a variable.""" - return True - - def _compute_polynomial_degree(self, result): - """ - If the variable is fixed, it represents a constant - is a polynomial with degree 0. Otherwise, it has - degree 1. This method is used in expressions to - compute polynomial degree. - """ - if self.fixed: - return 0 - return 1 - - def clear(self): - self.value = None - - def __call__(self, exception=True): - """Compute the value of this variable.""" - return self.value - - # - # Abstract Interface - # - - def set_value(self, val, skip_validation=False): - """Set the current variable value.""" - raise NotImplementedError - - @property - def value(self): - """Return (or set) the value for this variable.""" - raise NotImplementedError - - @property - def domain(self): - """Return (or set) the domain for this variable.""" - raise NotImplementedError - - @property - def lower(self): - """Return (or set) an expression for the variable lower bound.""" - raise NotImplementedError - - @property - def upper(self): - """Return (or set) an expression for the variable upper bound.""" - raise NotImplementedError - - @property - def fixed(self): - """Return (or set) the fixed indicator for this variable. - - Alias for :meth:`is_fixed` / :meth:`fix` / :meth:`unfix`. - - """ - raise NotImplementedError - - @property - def stale(self): - """The stale status for this variable. - - Variables are "stale" if their current value was not updated as - part of the most recent model update. A "model update" can be - one of several things: a solver invocation, loading a previous - solution, or manually updating a non-stale :class:`Var` value. - - Returns - ------- - bool - - Notes - ----- - Fixed :class:`Var` objects will be stale after invoking a solver - (as their value was not updated by the solver). - - Updating a stale :class:`Var` value will not cause other - variable values to be come stale. However, updating the first - non-stale :class:`Var` value after a solve or solution load - *will* cause all other variables to be marked as stale - - """ - raise NotImplementedError - - def fix(self, value=NOTSET, skip_validation=False): - """Fix the value of this variable (treat as nonvariable) - - This sets the :attr:`fixed` indicator to True. If ``value`` is - provided, the value (and the ``skip_validation`` flag) are first - passed to :meth:`set_value()`. - - """ - self.fixed = True - if value is not NOTSET: - self.set_value(value, skip_validation) - - def unfix(self): - """Unfix this variable (treat as variable in solver interfaces) - - This sets the :attr:`fixed` indicator to False. - - """ - self.fixed = False - - def free(self): - """Alias for :meth:`unfix`""" - return self.unfix() - - -class _VarData(metaclass=RenamedClass): - __renamed__new_class__ = VarData - __renamed__version__ = '6.7.2.dev0' - - -class GeneralVarData(VarData): """This class defines the data for a single variable.""" __slots__ = ('_value', '_lb', '_ub', '_domain', '_fixed', '_stale') @@ -365,10 +130,6 @@ def copy(cls, src): self._index = src._index return self - # - # Abstract Interface - # - def set_value(self, val, skip_validation=False): """Set the current variable value. @@ -429,14 +190,20 @@ def set_value(self, val, skip_validation=False): @property def value(self): + """Return (or set) the value for this variable.""" return self._value @value.setter def value(self, val): self.set_value(val) + def __call__(self, exception=True): + """Compute the value of this variable.""" + return self._value + @property def domain(self): + """Return (or set) the domain for this variable.""" return self._domain @domain.setter @@ -453,9 +220,42 @@ def domain(self, domain): ) raise - @VarData.bounds.getter + def has_lb(self): + """Returns :const:`False` when the lower bound is + :const:`None` or negative infinity""" + return self.lb is not None + + def has_ub(self): + """Returns :const:`False` when the upper bound is + :const:`None` or positive infinity""" + return self.ub is not None + + # TODO: deprecate this? Properties are generally preferred over "set*()" + def setlb(self, val): + """ + Set the lower bound for this variable after validating that + the value is fixed (or None). + """ + self.lower = val + + # TODO: deprecate this? Properties are generally preferred over "set*()" + def setub(self, val): + """ + Set the upper bound for this variable after validating that + the value is fixed (or None). + """ + self.upper = val + + @property def bounds(self): - # Custom implementation of VarData.bounds to avoid unnecessary + """Returns (or set) the tuple (lower bound, upper bound). + + This returns the current (numeric) values of the lower and upper + bounds as a tuple. If there is no bound, returns None (and not + +/-inf) + + """ + # Custom implementation of lb / ub to avoid unnecessary # expression generation and duplicate calls to domain.bounds() domain_lb, domain_ub = self.domain.bounds() # lb is the tighter of the domain and bounds @@ -496,10 +296,14 @@ def bounds(self): ub = min(ub, domain_ub) return lb, ub - @VarData.lb.getter + @bounds.setter + def bounds(self, val): + self.lower, self.upper = val + + @property def lb(self): - # Custom implementation of VarData.lb to avoid unnecessary - # expression generation + """Return (or set) the numeric value of the variable lower bound.""" + # Note: Implementation avoids unnecessary expression generation domain_lb, domain_ub = self.domain.bounds() # lb is the tighter of the domain and bounds lb = self._lb @@ -521,10 +325,14 @@ def lb(self): lb = max(lb, domain_lb) return lb - @VarData.ub.getter + @lb.setter + def lb(self, val): + self.lower = val + + @property def ub(self): - # Custom implementation of VarData.ub to avoid unnecessary - # expression generation + """Return (or set) the numeric value of the variable upper bound.""" + # Note: implementation avoids unnecessary expression generation domain_lb, domain_ub = self.domain.bounds() # ub is the tighter of the domain and bounds ub = self._ub @@ -546,6 +354,10 @@ def ub(self): ub = min(ub, domain_ub) return ub + @ub.setter + def ub(self, val): + self.upper = val + @property def lower(self): """Return (or set) an expression for the variable lower bound. @@ -602,8 +414,37 @@ def get_units(self): # component if not scalar return self.parent_component()._units + def fix(self, value=NOTSET, skip_validation=False): + """Fix the value of this variable (treat as nonvariable) + + This sets the :attr:`fixed` indicator to True. If ``value`` is + provided, the value (and the ``skip_validation`` flag) are first + passed to :meth:`set_value()`. + + """ + self.fixed = True + if value is not NOTSET: + self.set_value(value, skip_validation) + + def unfix(self): + """Unfix this variable (treat as variable in solver interfaces) + + This sets the :attr:`fixed` indicator to False. + + """ + self.fixed = False + + def free(self): + """Alias for :meth:`unfix`""" + return self.unfix() + @property def fixed(self): + """Return (or set) the fixed indicator for this variable. + + Alias for :meth:`is_fixed` / :meth:`fix` / :meth:`unfix`. + + """ return self._fixed @fixed.setter @@ -612,6 +453,28 @@ def fixed(self, val): @property def stale(self): + """The stale status for this variable. + + Variables are "stale" if their current value was not updated as + part of the most recent model update. A "model update" can be + one of several things: a solver invocation, loading a previous + solution, or manually updating a non-stale :class:`Var` value. + + Returns + ------- + bool + + Notes + ----- + Fixed :class:`Var` objects will be stale after invoking a solver + (as their value was not updated by the solver). + + Updating a stale :class:`Var` value will not cause other + variable values to be come stale. However, updating the first + non-stale :class:`Var` value after a solve or solution load + *will* cause all other variables to be marked as stale + + """ return StaleFlagManager.is_stale(self._stale) @stale.setter @@ -621,11 +484,70 @@ def stale(self, val): else: self._stale = StaleFlagManager.get_flag(0) - # Note: override the base class definition to avoid a call through a - # property + def is_integer(self): + """Returns True when the domain is a contiguous integer range.""" + _id = id(self.domain) + if _id in _known_global_real_domains: + return not _known_global_real_domains[_id] + _interval = self.domain.get_interval() + if _interval is None: + return False + # Note: it is not sufficient to just check the step: the + # starting / ending points must be integers (or not specified) + start, stop, step = _interval + return ( + step == 1 + and (start is None or int(start) == start) + and (stop is None or int(stop) == stop) + ) + + def is_binary(self): + """Returns True when the domain is restricted to Binary values.""" + domain = self.domain + if domain is Binary: + return True + if id(domain) in _known_global_real_domains: + return False + return domain.get_interval() == (0, 1, 1) + + def is_continuous(self): + """Returns True when the domain is a continuous real range""" + _id = id(self.domain) + if _id in _known_global_real_domains: + return _known_global_real_domains[_id] + _interval = self.domain.get_interval() + return _interval is not None and _interval[2] == 0 + def is_fixed(self): + """Returns True if this variable is fixed, otherwise returns False.""" return self._fixed + def is_constant(self): + """Returns False because this is not a constant in an expression.""" + return False + + def is_variable_type(self): + """Returns True because this is a variable.""" + return True + + def is_potentially_variable(self): + """Returns True because this is a variable.""" + return True + + def clear(self): + self.value = None + + def _compute_polynomial_degree(self, result): + """ + If the variable is fixed, it represents a constant + is a polynomial with degree 0. Otherwise, it has + degree 1. This method is used in expressions to + compute polynomial degree. + """ + if self._fixed: + return 0 + return 1 + def _process_bound(self, val, bound_type): if type(val) in native_numeric_types or val is None: # TODO: warn/error: check if this Var has units: assigning @@ -648,8 +570,13 @@ def _process_bound(self, val, bound_type): return val -class _GeneralVarData(metaclass=RenamedClass): - __renamed__new_class__ = GeneralVarData +class _VarData(metaclass=RenamedClass): + __renamed__new_class__ = VarData + __renamed__version__ = '6.7.2.dev0' + + +class _VarData(metaclass=RenamedClass): + __renamed__new_class__ = VarData __renamed__version__ = '6.7.2.dev0' @@ -678,7 +605,7 @@ class Var(IndexedComponent, IndexedComponent_NDArrayMixin): doc (str, optional): Text describing this component. """ - _ComponentDataClass = GeneralVarData + _ComponentDataClass = VarData @overload def __new__(cls: Type[Var], *args, **kwargs) -> Union[ScalarVar, IndexedVar]: ... @@ -962,11 +889,11 @@ def _pprint(self): ) -class ScalarVar(GeneralVarData, Var): +class ScalarVar(VarData, Var): """A single variable.""" def __init__(self, *args, **kwd): - GeneralVarData.__init__(self, component=self) + VarData.__init__(self, component=self) Var.__init__(self, *args, **kwd) self._index = UnindexedComponent_index @@ -1067,7 +994,7 @@ def domain(self, domain): # between potentially variable GetItemExpression objects and # "constant" GetItemExpression objects. That will need to wait for # the expression rework [JDS; Nov 22]. - def __getitem__(self, args) -> GeneralVarData: + def __getitem__(self, args) -> VarData: try: return super().__getitem__(args) except RuntimeError: From cf7ff538df92247af0cd7e7c3e34ed4f4549d8af Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 20:21:36 -0600 Subject: [PATCH 1012/3044] Update references from GeneralVarData to VarData --- pyomo/contrib/appsi/base.py | 60 +++++++++---------- pyomo/contrib/appsi/cmodel/src/expression.hpp | 6 +- pyomo/contrib/appsi/fbbt.py | 10 ++-- pyomo/contrib/appsi/solvers/cbc.py | 16 ++--- pyomo/contrib/appsi/solvers/cplex.py | 16 ++--- pyomo/contrib/appsi/solvers/gurobi.py | 12 ++-- pyomo/contrib/appsi/solvers/highs.py | 8 +-- pyomo/contrib/appsi/solvers/ipopt.py | 16 ++--- pyomo/contrib/appsi/solvers/wntr.py | 8 +-- pyomo/contrib/appsi/writers/lp_writer.py | 8 +-- pyomo/contrib/appsi/writers/nl_writer.py | 8 +-- pyomo/contrib/cp/repn/docplex_writer.py | 4 +- .../logical_to_disjunctive_walker.py | 4 +- pyomo/contrib/latex_printer/latex_printer.py | 12 ++-- pyomo/contrib/parmest/utils/scenario_tree.py | 2 +- pyomo/contrib/solver/base.py | 24 ++++---- pyomo/contrib/solver/gurobi.py | 12 ++-- pyomo/contrib/solver/ipopt.py | 6 +- pyomo/contrib/solver/persistent.py | 18 +++--- pyomo/contrib/solver/solution.py | 26 ++++---- .../contrib/solver/tests/unit/test_results.py | 10 ++-- .../trustregion/tests/test_interface.py | 4 +- pyomo/core/base/__init__.py | 9 ++- pyomo/core/base/component.py | 2 +- pyomo/core/expr/calculus/derivatives.py | 6 +- pyomo/core/tests/transform/test_add_slacks.py | 2 +- pyomo/core/tests/unit/test_dict_objects.py | 6 +- pyomo/core/tests/unit/test_list_objects.py | 6 +- pyomo/core/tests/unit/test_numeric_expr.py | 4 +- pyomo/core/tests/unit/test_reference.py | 12 ++-- pyomo/repn/standard_repn.py | 6 +- .../plugins/solvers/gurobi_persistent.py | 4 +- pyomo/util/report_scaling.py | 4 +- 33 files changed, 171 insertions(+), 180 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 409c8e2596c..930ff8393e9 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -23,7 +23,7 @@ ) from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import SOSConstraintData, SOSConstraint -from pyomo.core.base.var import GeneralVarData, Var +from pyomo.core.base.var import VarData, Var from pyomo.core.base.param import ParamData, Param from pyomo.core.base.block import BlockData, Block from pyomo.core.base.objective import GeneralObjectiveData @@ -179,9 +179,7 @@ def __init__( class SolutionLoaderBase(abc.ABC): - def load_vars( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> NoReturn: + def load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -197,8 +195,8 @@ def load_vars( @abc.abstractmethod def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Returns a ComponentMap mapping variable to var value. @@ -256,8 +254,8 @@ def get_slacks( ) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Returns a ComponentMap mapping variable to reduced cost. @@ -303,8 +301,8 @@ def __init__( self._reduced_costs = reduced_costs def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if self._primals is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' @@ -353,8 +351,8 @@ def get_slacks( return slacks def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if self._reduced_costs is None: raise RuntimeError( 'Solution loader does not currently have valid reduced costs. Please ' @@ -708,9 +706,7 @@ class PersistentSolver(Solver): def is_persistent(self): return True - def load_vars( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> NoReturn: + def load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -726,8 +722,8 @@ def load_vars( @abc.abstractmethod def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: pass def get_duals( @@ -771,8 +767,8 @@ def get_slacks( ) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Parameters ---------- @@ -799,7 +795,7 @@ def set_instance(self, model): pass @abc.abstractmethod - def add_variables(self, variables: List[GeneralVarData]): + def add_variables(self, variables: List[VarData]): pass @abc.abstractmethod @@ -815,7 +811,7 @@ def add_block(self, block: BlockData): pass @abc.abstractmethod - def remove_variables(self, variables: List[GeneralVarData]): + def remove_variables(self, variables: List[VarData]): pass @abc.abstractmethod @@ -835,7 +831,7 @@ def set_objective(self, obj: GeneralObjectiveData): pass @abc.abstractmethod - def update_variables(self, variables: List[GeneralVarData]): + def update_variables(self, variables: List[VarData]): pass @abc.abstractmethod @@ -869,8 +865,8 @@ def get_slacks( return self._solver.get_slacks(cons_to_load=cons_to_load) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: self._assert_solution_still_valid() return self._solver.get_reduced_costs(vars_to_load=vars_to_load) @@ -954,10 +950,10 @@ def set_instance(self, model): self.set_objective(None) @abc.abstractmethod - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): pass - def add_variables(self, variables: List[GeneralVarData]): + def add_variables(self, variables: List[VarData]): for v in variables: if id(v) in self._referenced_variables: raise ValueError( @@ -987,7 +983,7 @@ def add_params(self, params: List[ParamData]): def _add_constraints(self, cons: List[GeneralConstraintData]): pass - def _check_for_new_vars(self, variables: List[GeneralVarData]): + def _check_for_new_vars(self, variables: List[VarData]): new_vars = dict() for v in variables: v_id = id(v) @@ -995,7 +991,7 @@ def _check_for_new_vars(self, variables: List[GeneralVarData]): new_vars[v_id] = v self.add_variables(list(new_vars.values())) - def _check_to_remove_vars(self, variables: List[GeneralVarData]): + def _check_to_remove_vars(self, variables: List[VarData]): vars_to_remove = dict() for v in variables: v_id = id(v) @@ -1174,10 +1170,10 @@ def remove_sos_constraints(self, cons: List[SOSConstraintData]): del self._vars_referenced_by_con[con] @abc.abstractmethod - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): pass - def remove_variables(self, variables: List[GeneralVarData]): + def remove_variables(self, variables: List[VarData]): self._remove_variables(variables) for v in variables: v_id = id(v) @@ -1246,10 +1242,10 @@ def remove_block(self, block): ) @abc.abstractmethod - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): pass - def update_variables(self, variables: List[GeneralVarData]): + def update_variables(self, variables: List[VarData]): for v in variables: self._vars[id(v)] = ( v, diff --git a/pyomo/contrib/appsi/cmodel/src/expression.hpp b/pyomo/contrib/appsi/cmodel/src/expression.hpp index 0c0777ef468..803bb21b6e2 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.hpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.hpp @@ -680,7 +680,7 @@ class PyomoExprTypes { expr_type_map[np_float32] = py_float; expr_type_map[np_float64] = py_float; expr_type_map[ScalarVar] = var; - expr_type_map[_GeneralVarData] = var; + expr_type_map[_VarData] = var; expr_type_map[AutoLinkedBinaryVar] = var; expr_type_map[ScalarParam] = param; expr_type_map[_ParamData] = param; @@ -732,8 +732,8 @@ class PyomoExprTypes { py::module_::import("pyomo.core.base.param").attr("_ParamData"); py::object ScalarVar = py::module_::import("pyomo.core.base.var").attr("ScalarVar"); - py::object _GeneralVarData = - py::module_::import("pyomo.core.base.var").attr("_GeneralVarData"); + py::object _VarData = + py::module_::import("pyomo.core.base.var").attr("_VarData"); py::object AutoLinkedBinaryVar = py::module_::import("pyomo.gdp.disjunct").attr("AutoLinkedBinaryVar"); py::object numeric_expr = py::module_::import("pyomo.core.expr.numeric_expr"); diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 122ca5f7ffd..1ebb3d40381 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -18,7 +18,7 @@ ) from .cmodel import cmodel, cmodel_available from typing import List, Optional -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import SOSConstraintData @@ -121,7 +121,7 @@ def set_instance(self, model, symbolic_solver_labels: Optional[bool] = None): if self._objective is None: self.set_objective(None) - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): if self._symbolic_solver_labels: set_name = True symbol_map = self._symbol_map @@ -190,7 +190,7 @@ def _remove_sos_constraints(self, cons: List[SOSConstraintData]): 'IntervalTightener does not support SOS constraints' ) - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): if self._symbolic_solver_labels: for v in variables: self._symbol_map.removeSymbol(v) @@ -205,7 +205,7 @@ def _remove_params(self, params: List[ParamData]): for p in params: del self._param_map[id(p)] - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): cmodel.process_pyomo_vars( self._pyomo_expr_types, variables, @@ -304,7 +304,7 @@ def perform_fbbt( self._deactivate_satisfied_cons() return n_iter - def perform_fbbt_with_seed(self, model: BlockData, seed_var: GeneralVarData): + def perform_fbbt_with_seed(self, model: BlockData, seed_var: VarData): if model is not self._model: self.set_instance(model) else: diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index d03e6e31c54..7db9a32764e 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -26,7 +26,7 @@ import math from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData @@ -164,7 +164,7 @@ def symbol_map(self): def set_instance(self, model): self._writer.set_instance(model) - def add_variables(self, variables: List[GeneralVarData]): + def add_variables(self, variables: List[VarData]): self._writer.add_variables(variables) def add_params(self, params: List[ParamData]): @@ -176,7 +176,7 @@ def add_constraints(self, cons: List[GeneralConstraintData]): def add_block(self, block: BlockData): self._writer.add_block(block) - def remove_variables(self, variables: List[GeneralVarData]): + def remove_variables(self, variables: List[VarData]): self._writer.remove_variables(variables) def remove_params(self, params: List[ParamData]): @@ -191,7 +191,7 @@ def remove_block(self, block: BlockData): def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) - def update_variables(self, variables: List[GeneralVarData]): + def update_variables(self, variables: List[VarData]): self._writer.update_variables(variables) def update_params(self): @@ -440,8 +440,8 @@ def _check_and_escape_options(): return results def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._last_results_object is None or self._last_results_object.best_feasible_objective is None @@ -477,8 +477,8 @@ def get_duals(self, cons_to_load=None): return {c: self._dual_sol[c] for c in cons_to_load} def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._last_results_object is None or self._last_results_object.termination_condition diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 55259244d45..0ed3495ac1c 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -22,7 +22,7 @@ import math from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping, Dict -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData @@ -179,7 +179,7 @@ def update_config(self): def set_instance(self, model): self._writer.set_instance(model) - def add_variables(self, variables: List[GeneralVarData]): + def add_variables(self, variables: List[VarData]): self._writer.add_variables(variables) def add_params(self, params: List[ParamData]): @@ -191,7 +191,7 @@ def add_constraints(self, cons: List[GeneralConstraintData]): def add_block(self, block: BlockData): self._writer.add_block(block) - def remove_variables(self, variables: List[GeneralVarData]): + def remove_variables(self, variables: List[VarData]): self._writer.remove_variables(variables) def remove_params(self, params: List[ParamData]): @@ -206,7 +206,7 @@ def remove_block(self, block: BlockData): def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) - def update_variables(self, variables: List[GeneralVarData]): + def update_variables(self, variables: List[VarData]): self._writer.update_variables(variables) def update_params(self): @@ -362,8 +362,8 @@ def _postsolve(self, timer: HierarchicalTimer, solve_time): return results def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._cplex_model.solution.get_solution_type() == self._cplex_model.solution.type.none @@ -440,8 +440,8 @@ def get_duals( return res def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._cplex_model.solution.get_solution_type() == self._cplex_model.solution.type.none diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index 8606d44cd46..e2ecd9b69e7 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -23,7 +23,7 @@ from pyomo.common.config import ConfigValue, NonNegativeInt from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.base.var import Var, GeneralVarData +from pyomo.core.base.var import Var, VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData @@ -458,7 +458,7 @@ def _process_domain_and_bounds( return lb, ub, vtype - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): var_names = list() vtypes = list() lbs = list() @@ -759,7 +759,7 @@ def _remove_sos_constraints(self, cons: List[SOSConstraintData]): del self._pyomo_sos_to_solver_sos_map[con] self._needs_updated = True - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): for var in variables: v_id = id(var) if var in self._vars_added_since_update: @@ -774,7 +774,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): def _remove_params(self, params: List[ParamData]): pass - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): for var in variables: var_id = id(var) if var_id not in self._pyomo_var_to_solver_var_map: @@ -1221,7 +1221,7 @@ def set_var_attr(self, var, attr, val): Parameters ---------- - var: pyomo.core.base.var.GeneralVarData + var: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be modified. attr: str @@ -1256,7 +1256,7 @@ def get_var_attr(self, var, attr): Parameters ---------- - var: pyomo.core.base.var.GeneralVarData + var: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be retrieved. attr: str diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 5af7b297684..c3083ac78d3 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -20,7 +20,7 @@ from pyomo.common.log import LogStream from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData @@ -308,7 +308,7 @@ def _process_domain_and_bounds(self, var_id): return lb, ub, vtype - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -493,7 +493,7 @@ def _remove_sos_constraints(self, cons: List[SOSConstraintData]): 'Highs interface does not support SOS constraints' ) - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -518,7 +518,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): def _remove_params(self, params: List[ParamData]): pass - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index ca75a1b02c8..5cd9a51785d 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -28,7 +28,7 @@ from pyomo.core.expr.numvalue import value from pyomo.core.expr.visitor import replace_expressions from typing import Optional, Sequence, NoReturn, List, Mapping -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData @@ -228,7 +228,7 @@ def set_instance(self, model): self._writer.config.symbolic_solver_labels = self.config.symbolic_solver_labels self._writer.set_instance(model) - def add_variables(self, variables: List[GeneralVarData]): + def add_variables(self, variables: List[VarData]): self._writer.add_variables(variables) def add_params(self, params: List[ParamData]): @@ -240,7 +240,7 @@ def add_constraints(self, cons: List[GeneralConstraintData]): def add_block(self, block: BlockData): self._writer.add_block(block) - def remove_variables(self, variables: List[GeneralVarData]): + def remove_variables(self, variables: List[VarData]): self._writer.remove_variables(variables) def remove_params(self, params: List[ParamData]): @@ -255,7 +255,7 @@ def remove_block(self, block: BlockData): def set_objective(self, obj: GeneralObjectiveData): self._writer.set_objective(obj) - def update_variables(self, variables: List[GeneralVarData]): + def update_variables(self, variables: List[VarData]): self._writer.update_variables(variables) def update_params(self): @@ -514,8 +514,8 @@ def _apply_solver(self, timer: HierarchicalTimer): return results def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._last_results_object is None or self._last_results_object.best_feasible_objective is None @@ -551,8 +551,8 @@ def get_duals(self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = No return {c: self._dual_sol[c] for c in cons_to_load} def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if ( self._last_results_object is None or self._last_results_object.termination_condition diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 8f2650dabb6..62c4b0ed358 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -40,7 +40,7 @@ from pyomo.core.expr.numvalue import native_numeric_types from typing import Dict, Optional, List from pyomo.core.base.block import BlockData -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.common.timing import HierarchicalTimer @@ -239,7 +239,7 @@ def set_instance(self, model): self.add_block(model) - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): aml = wntr.sim.aml.aml for var in variables: varname = self._symbol_map.getSymbol(var, self._labeler) @@ -302,7 +302,7 @@ def _remove_constraints(self, cons: List[GeneralConstraintData]): del self._pyomo_con_to_solver_con_map[con] self._needs_updated = True - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): for var in variables: v_id = id(var) solver_var = self._pyomo_var_to_solver_var_map[v_id] @@ -322,7 +322,7 @@ def _remove_params(self, params: List[ParamData]): self._symbol_map.removeSymbol(p) del self._pyomo_param_to_solver_param_map[p_id] - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): aml = wntr.sim.aml.aml for var in variables: v_id = id(var) diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 3a6682d5c00..4be2b32d83d 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -11,7 +11,7 @@ from typing import List from pyomo.core.base.param import ParamData -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.sos import SOSConstraintData @@ -77,7 +77,7 @@ def set_instance(self, model): if self._objective is None: self.set_objective(None) - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): cmodel.process_pyomo_vars( self._expr_types, variables, @@ -117,7 +117,7 @@ def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('LP writer does not yet support SOS constraints') - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): for v in variables: cvar = self._pyomo_var_to_solver_var_map.pop(id(v)) del self._solver_var_to_pyomo_var_map[cvar] @@ -128,7 +128,7 @@ def _remove_params(self, params: List[ParamData]): del self._pyomo_param_to_solver_param_map[id(p)] self._symbol_map.removeSymbol(p) - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): cmodel.process_pyomo_vars( self._expr_types, variables, diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index c46cb1c0723..70176146a1e 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -11,7 +11,7 @@ from typing import List from pyomo.core.base.param import ParamData -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.sos import SOSConstraintData @@ -78,7 +78,7 @@ def set_instance(self, model): self.set_objective(None) self._set_pyomo_amplfunc_env() - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): if self.config.symbolic_solver_labels: set_name = True symbol_map = self._symbol_map @@ -144,7 +144,7 @@ def _remove_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('NL writer does not support SOS constraints') - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): if self.config.symbolic_solver_labels: for v in variables: self._symbol_map.removeSymbol(v) @@ -161,7 +161,7 @@ def _remove_params(self, params: List[ParamData]): for p in params: del self._pyomo_param_to_solver_param_map[id(p)] - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): cmodel.process_pyomo_vars( self._expr_types, variables, diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 75095755895..221fd61af5b 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -65,7 +65,7 @@ ) from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.param import IndexedParam, ScalarParam, ParamData -from pyomo.core.base.var import ScalarVar, GeneralVarData, IndexedVar +from pyomo.core.base.var import ScalarVar, VarData, IndexedVar import pyomo.core.expr as EXPR from pyomo.core.expr.visitor import StreamBasedExpressionVisitor, identify_variables from pyomo.core.base import Set, RangeSet @@ -961,7 +961,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): IntervalVarData: _before_interval_var, IndexedIntervalVar: _before_indexed_interval_var, ScalarVar: _before_var, - GeneralVarData: _before_var, + VarData: _before_var, IndexedVar: _before_indexed_var, ScalarBooleanVar: _before_boolean_var, GeneralBooleanVarData: _before_boolean_var, diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index a228b1561dd..d9483c0ed14 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -29,7 +29,7 @@ import pyomo.core.base.boolean_var as BV from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.param import ScalarParam, ParamData -from pyomo.core.base.var import ScalarVar, GeneralVarData +from pyomo.core.base.var import ScalarVar, VarData from pyomo.gdp.disjunct import AutoLinkedBooleanVar, Disjunct, Disjunction @@ -216,7 +216,7 @@ def _dispatch_atmost(visitor, node, *args): # for the moment, these are all just so we can get good error messages when we # don't handle them: _before_child_dispatcher[ScalarVar] = _dispatch_var -_before_child_dispatcher[GeneralVarData] = _dispatch_var +_before_child_dispatcher[VarData] = _dispatch_var _before_child_dispatcher[GeneralExpressionData] = _dispatch_expression _before_child_dispatcher[ScalarExpression] = _dispatch_expression diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 28d1ca52943..e11543cb375 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -47,7 +47,7 @@ resolve_template, templatize_rule, ) -from pyomo.core.base.var import ScalarVar, GeneralVarData, IndexedVar +from pyomo.core.base.var import ScalarVar, VarData, IndexedVar from pyomo.core.base.param import ParamData, ScalarParam, IndexedParam from pyomo.core.base.set import SetData, SetOperator from pyomo.core.base.constraint import ScalarConstraint, IndexedConstraint @@ -404,7 +404,7 @@ def __init__(self): kernel.expression.expression: handle_named_expression_node, kernel.expression.noclone: handle_named_expression_node, GeneralObjectiveData: handle_named_expression_node, - GeneralVarData: handle_var_node, + VarData: handle_var_node, ScalarObjective: handle_named_expression_node, kernel.objective.objective: handle_named_expression_node, ExternalFunctionExpression: handle_external_function_node, @@ -705,10 +705,8 @@ def latex_printer( if isSingle: temp_comp, temp_indexes = templatize_fcn(pyomo_component) variableList = [] - for v in identify_components( - temp_comp, [ScalarVar, GeneralVarData, IndexedVar] - ): - if isinstance(v, GeneralVarData): + for v in identify_components(temp_comp, [ScalarVar, VarData, IndexedVar]): + if isinstance(v, VarData): v_write = v.parent_component() if v_write not in ComponentSet(variableList): variableList.append(v_write) @@ -1273,7 +1271,7 @@ def get_index_names(st, lcm): rep_dict = {} for ky in reversed(list(latex_component_map)): - if isinstance(ky, (pyo.Var, GeneralVarData)): + if isinstance(ky, (pyo.Var, VarData)): overwrite_value = latex_component_map[ky] if ky not in existing_components: overwrite_value = overwrite_value.replace('_', '\\_') diff --git a/pyomo/contrib/parmest/utils/scenario_tree.py b/pyomo/contrib/parmest/utils/scenario_tree.py index 1062e4a2bf4..f245e053cad 100644 --- a/pyomo/contrib/parmest/utils/scenario_tree.py +++ b/pyomo/contrib/parmest/utils/scenario_tree.py @@ -25,7 +25,7 @@ def build_vardatalist(self, model, varlist=None): """ - Convert a list of pyomo variables to a list of ScalarVar and GeneralVarData. If varlist is none, builds a + Convert a list of pyomo variables to a list of ScalarVar and VarData. If varlist is none, builds a list of all variables in the model. The new list is stored in the vars_to_tighten attribute. By CD Laird Parameters diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 1b22c17cf48..fdc7361e6b8 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -15,7 +15,7 @@ import os from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData from pyomo.core.base.block import BlockData from pyomo.core.base.objective import GeneralObjectiveData @@ -194,9 +194,7 @@ def is_persistent(self): """ return True - def _load_vars( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> NoReturn: + def _load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -212,19 +210,19 @@ def _load_vars( @abc.abstractmethod def _get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Get mapping of variables to primals. Parameters ---------- - vars_to_load : Optional[Sequence[GeneralVarData]], optional + vars_to_load : Optional[Sequence[VarData]], optional Which vars to be populated into the map. The default is None. Returns ------- - Mapping[GeneralVarData, float] + Mapping[VarData, float] A map of variables to primals. """ raise NotImplementedError( @@ -251,8 +249,8 @@ def _get_duals( raise NotImplementedError(f'{type(self)} does not support the get_duals method') def _get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Parameters ---------- @@ -282,7 +280,7 @@ def set_objective(self, obj: GeneralObjectiveData): """ @abc.abstractmethod - def add_variables(self, variables: List[GeneralVarData]): + def add_variables(self, variables: List[VarData]): """ Add variables to the model """ @@ -306,7 +304,7 @@ def add_block(self, block: BlockData): """ @abc.abstractmethod - def remove_variables(self, variables: List[GeneralVarData]): + def remove_variables(self, variables: List[VarData]): """ Remove variables from the model """ @@ -330,7 +328,7 @@ def remove_block(self, block: BlockData): """ @abc.abstractmethod - def update_variables(self, variables: List[GeneralVarData]): + def update_variables(self, variables: List[VarData]): """ Update variables on the model """ diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index c5a1c8c1cd8..ff4e93f7635 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -22,7 +22,7 @@ from pyomo.common.config import ConfigValue from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData @@ -438,7 +438,7 @@ def _process_domain_and_bounds( return lb, ub, vtype - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): var_names = list() vtypes = list() lbs = list() @@ -735,7 +735,7 @@ def _remove_sos_constraints(self, cons: List[SOSConstraintData]): del self._pyomo_sos_to_solver_sos_map[con] self._needs_updated = True - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): for var in variables: v_id = id(var) if var in self._vars_added_since_update: @@ -750,7 +750,7 @@ def _remove_variables(self, variables: List[GeneralVarData]): def _remove_parameters(self, params: List[ParamData]): pass - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): for var in variables: var_id = id(var) if var_id not in self._pyomo_var_to_solver_var_map: @@ -1151,7 +1151,7 @@ def set_var_attr(self, var, attr, val): Parameters ---------- - var: pyomo.core.base.var.GeneralVarData + var: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be modified. attr: str @@ -1186,7 +1186,7 @@ def get_var_attr(self, var, attr): Parameters ---------- - var: pyomo.core.base.var.GeneralVarData + var: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be retrieved. attr: str diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 7111ec6e972..e4d25e4fea0 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -25,7 +25,7 @@ ) from pyomo.common.tempfiles import TempfileManager from pyomo.common.timing import HierarchicalTimer -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.staleflag import StaleFlagManager from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo from pyomo.contrib.solver.base import SolverBase @@ -80,8 +80,8 @@ def __init__( class IpoptSolutionLoader(SolSolutionLoader): def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index e98d76b4841..103eb3c622f 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -14,7 +14,7 @@ from pyomo.core.base.constraint import GeneralConstraintData, Constraint from pyomo.core.base.sos import SOSConstraintData, SOSConstraint -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData, Param from pyomo.core.base.objective import GeneralObjectiveData from pyomo.common.collections import ComponentMap @@ -54,10 +54,10 @@ def set_instance(self, model): self.set_objective(None) @abc.abstractmethod - def _add_variables(self, variables: List[GeneralVarData]): + def _add_variables(self, variables: List[VarData]): pass - def add_variables(self, variables: List[GeneralVarData]): + def add_variables(self, variables: List[VarData]): for v in variables: if id(v) in self._referenced_variables: raise ValueError( @@ -87,7 +87,7 @@ def add_parameters(self, params: List[ParamData]): def _add_constraints(self, cons: List[GeneralConstraintData]): pass - def _check_for_new_vars(self, variables: List[GeneralVarData]): + def _check_for_new_vars(self, variables: List[VarData]): new_vars = {} for v in variables: v_id = id(v) @@ -95,7 +95,7 @@ def _check_for_new_vars(self, variables: List[GeneralVarData]): new_vars[v_id] = v self.add_variables(list(new_vars.values())) - def _check_to_remove_vars(self, variables: List[GeneralVarData]): + def _check_to_remove_vars(self, variables: List[VarData]): vars_to_remove = {} for v in variables: v_id = id(v) @@ -250,10 +250,10 @@ def remove_sos_constraints(self, cons: List[SOSConstraintData]): del self._vars_referenced_by_con[con] @abc.abstractmethod - def _remove_variables(self, variables: List[GeneralVarData]): + def _remove_variables(self, variables: List[VarData]): pass - def remove_variables(self, variables: List[GeneralVarData]): + def remove_variables(self, variables: List[VarData]): self._remove_variables(variables) for v in variables: v_id = id(v) @@ -309,10 +309,10 @@ def remove_block(self, block): ) @abc.abstractmethod - def _update_variables(self, variables: List[GeneralVarData]): + def _update_variables(self, variables: List[VarData]): pass - def update_variables(self, variables: List[GeneralVarData]): + def update_variables(self, variables: List[VarData]): for v in variables: self._vars[id(v)] = ( v, diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index 3f327c1f280..e089e621f1f 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -13,7 +13,7 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.expr import value from pyomo.common.collections import ComponentMap from pyomo.common.errors import DeveloperError @@ -30,9 +30,7 @@ class SolutionLoaderBase(abc.ABC): Intent of this class and its children is to load the solution back into the model. """ - def load_vars( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> NoReturn: + def load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn: """ Load the solution of the primal variables into the value attribute of the variables. @@ -49,8 +47,8 @@ def load_vars( @abc.abstractmethod def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Returns a ComponentMap mapping variable to var value. @@ -86,8 +84,8 @@ def get_duals( raise NotImplementedError(f'{type(self)} does not support the get_duals method') def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: """ Returns a ComponentMap mapping variable to reduced cost. @@ -127,8 +125,8 @@ def get_duals( return self._solver._get_duals(cons_to_load=cons_to_load) def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: self._assert_solution_still_valid() return self._solver._get_reduced_costs(vars_to_load=vars_to_load) @@ -141,9 +139,7 @@ def __init__(self, sol_data: SolFileData, nl_info: NLWriterInfo) -> None: self._sol_data = sol_data self._nl_info = nl_info - def load_vars( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> NoReturn: + def load_vars(self, vars_to_load: Optional[Sequence[VarData]] = None) -> NoReturn: if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' @@ -169,8 +165,8 @@ def load_vars( StaleFlagManager.mark_all_as_stale(delayed=True) def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 608af04a0ed..6c178d80298 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -16,7 +16,7 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.common.collections import ComponentMap from pyomo.contrib.solver import results from pyomo.contrib.solver import solution @@ -51,8 +51,8 @@ def __init__( self._reduced_costs = reduced_costs def get_primals( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if self._primals is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' @@ -84,8 +84,8 @@ def get_duals( return duals def get_reduced_costs( - self, vars_to_load: Optional[Sequence[GeneralVarData]] = None - ) -> Mapping[GeneralVarData, float]: + self, vars_to_load: Optional[Sequence[VarData]] = None + ) -> Mapping[VarData, float]: if self._reduced_costs is None: raise RuntimeError( 'Solution loader does not currently have valid reduced costs. Please ' diff --git a/pyomo/contrib/trustregion/tests/test_interface.py b/pyomo/contrib/trustregion/tests/test_interface.py index 64f76eb887d..0922ccf950b 100644 --- a/pyomo/contrib/trustregion/tests/test_interface.py +++ b/pyomo/contrib/trustregion/tests/test_interface.py @@ -33,7 +33,7 @@ cos, SolverFactory, ) -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.expr.numeric_expr import ExternalFunctionExpression from pyomo.core.expr.visitor import identify_variables from pyomo.contrib.trustregion.interface import TRFInterface @@ -158,7 +158,7 @@ def test_replaceExternalFunctionsWithVariables(self): self.assertIsInstance(k, ExternalFunctionExpression) self.assertIn(str(self.interface.model.x[0]), str(k)) self.assertIn(str(self.interface.model.x[1]), str(k)) - self.assertIsInstance(i, GeneralVarData) + self.assertIsInstance(i, VarData) self.assertEqual(i, self.interface.data.ef_outputs[1]) for i, k in self.interface.data.basis_expressions.items(): self.assertEqual(k, 0) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 9a06a3e02bb..6851fe4cda0 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -40,7 +40,6 @@ from pyomo.core.base.componentuid import ComponentUID from pyomo.core.base.config import PyomoOptions from pyomo.core.base.enums import SortComponents, TraversalStrategy -from pyomo.core.base.instance2dat import instance2dat from pyomo.core.base.label import ( CuidLabeler, CounterLabeler, @@ -146,7 +145,9 @@ active_import_suffix_generator, Suffix, ) -from pyomo.core.base.var import Var, VarData, GeneralVarData, ScalarVar, VarList +from pyomo.core.base.var import Var, VarData, VarData, ScalarVar, VarList + +from pyomo.core.base.instance2dat import instance2dat # # These APIs are deprecated and should be removed in the near future @@ -163,12 +164,14 @@ 'SimpleBooleanVar', 'pyomo.core.base.boolean_var.SimpleBooleanVar', version='6.0' ) # Historically, only a subset of "private" component data classes were imported here +relocated_module_attribute( + f'_GeneralVarData', f'pyomo.core.base.VarData', version='6.7.2.dev0' +) for _cdata in ( 'ConstraintData', 'LogicalConstraintData', 'ExpressionData', 'VarData', - 'GeneralVarData', 'GeneralBooleanVarData', 'BooleanVarData', 'ObjectiveData', diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 7a4b7e40aab..65844379eca 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -805,7 +805,7 @@ class ComponentData(_ComponentBase): # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, LogicalConstraintData, # GeneralLogicalConstraintData, GeneralObjectiveData, - # ParamData,GeneralVarData, GeneralBooleanVarData, DisjunctionData, + # ParamData,VarData, GeneralBooleanVarData, DisjunctionData, # ArcData, PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! diff --git a/pyomo/core/expr/calculus/derivatives.py b/pyomo/core/expr/calculus/derivatives.py index 5df1fd3c65e..69fe4969938 100644 --- a/pyomo/core/expr/calculus/derivatives.py +++ b/pyomo/core/expr/calculus/derivatives.py @@ -39,11 +39,11 @@ def differentiate(expr, wrt=None, wrt_list=None, mode=Modes.reverse_numeric): ---------- expr: pyomo.core.expr.numeric_expr.NumericExpression The expression to differentiate - wrt: pyomo.core.base.var.GeneralVarData + wrt: pyomo.core.base.var.VarData If specified, this function will return the derivative with - respect to wrt. wrt is normally a GeneralVarData, but could + respect to wrt. wrt is normally a VarData, but could also be a ParamData. wrt and wrt_list cannot both be specified. - wrt_list: list of pyomo.core.base.var.GeneralVarData + wrt_list: list of pyomo.core.base.var.VarData If specified, this function will return the derivative with respect to each element in wrt_list. A list will be returned where the values are the derivatives with respect to the diff --git a/pyomo/core/tests/transform/test_add_slacks.py b/pyomo/core/tests/transform/test_add_slacks.py index d66d6fba79e..b395237b8e4 100644 --- a/pyomo/core/tests/transform/test_add_slacks.py +++ b/pyomo/core/tests/transform/test_add_slacks.py @@ -330,7 +330,7 @@ def test_error_for_non_constraint_noniterable_target(self): self.assertRaisesRegex( ValueError, "Expected Constraint or list of Constraints.\n\tReceived " - "", + "", TransformationFactory('core.add_slack_variables').apply_to, m, targets=m.indexedVar[1], diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index f2c3cad8cc3..0dc5cacd216 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -17,7 +17,7 @@ ObjectiveDict, ExpressionDict, ) -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -348,10 +348,10 @@ def test_active(self): class TestVarDict(_TestComponentDictBase, unittest.TestCase): - # Note: the updated GeneralVarData class only takes an optional + # Note: the updated VarData class only takes an optional # parent argument (you no longer pass the domain in) _ctype = VarDict - _cdatatype = lambda self, arg: GeneralVarData() + _cdatatype = lambda self, arg: VarData() def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index fcc83a95a06..f98b5279fc5 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -17,7 +17,7 @@ XObjectiveList, XExpressionList, ) -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import GeneralObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -365,10 +365,10 @@ def test_active(self): class TestVarList(_TestComponentListBase, unittest.TestCase): - # Note: the updated GeneralVarData class only takes an optional + # Note: the updated VarData class only takes an optional # parent argument (you no longer pass the domain in) _ctype = XVarList - _cdatatype = lambda self, arg: GeneralVarData() + _cdatatype = lambda self, arg: VarData() def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index 8e5e43eac9c..efb01e6d6ce 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -112,7 +112,7 @@ from pyomo.core.base.label import NumericLabeler from pyomo.core.expr.template_expr import IndexTemplate from pyomo.core.expr import expr_common -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.repn import generate_standard_repn from pyomo.core.expr.numvalue import NumericValue @@ -294,7 +294,7 @@ def value_check(self, exp, val): class TestExpression_EvaluateVarData(TestExpression_EvaluateNumericValue): def create(self, val, domain): - tmp = GeneralVarData() + tmp = VarData() tmp.domain = domain tmp.value = val return tmp diff --git a/pyomo/core/tests/unit/test_reference.py b/pyomo/core/tests/unit/test_reference.py index 4fa2f4944e9..7370881612f 100644 --- a/pyomo/core/tests/unit/test_reference.py +++ b/pyomo/core/tests/unit/test_reference.py @@ -800,8 +800,8 @@ def test_reference_indexedcomponent_pprint(self): buf.getvalue(), """r : Size=2, Index={1, 2}, ReferenceTo=x Key : Object - 1 : - 2 : + 1 : + 2 : """, ) m.s = Reference(m.x[:, ...], ctype=IndexedComponent) @@ -811,8 +811,8 @@ def test_reference_indexedcomponent_pprint(self): buf.getvalue(), """s : Size=2, Index={1, 2}, ReferenceTo=x[:, ...] Key : Object - 1 : - 2 : + 1 : + 2 : """, ) @@ -1357,8 +1357,8 @@ def test_pprint_nonfinite_sets_ctypeNone(self): 1 IndexedComponent Declarations ref : Size=2, Index=NonNegativeIntegers, ReferenceTo=v Key : Object - 3 : - 5 : + 3 : + 5 : 2 Declarations: v ref """.strip(), diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 5786d078385..2f5a413e963 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -22,7 +22,7 @@ from pyomo.core.base.objective import GeneralObjectiveData, ScalarObjective from pyomo.core.base import ExpressionData, Expression from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData -from pyomo.core.base.var import ScalarVar, Var, GeneralVarData, value +from pyomo.core.base.var import ScalarVar, Var, VarData, value from pyomo.core.base.param import ScalarParam, ParamData from pyomo.core.kernel.expression import expression, noclone from pyomo.core.kernel.variable import IVariable, variable @@ -1143,7 +1143,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra # param.Param : _collect_linear_const, # parameter : _collect_linear_const, NumericConstant: _collect_const, - GeneralVarData: _collect_var, + VarData: _collect_var, ScalarVar: _collect_var, Var: _collect_var, variable: _collect_var, @@ -1542,7 +1542,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): ##param.ScalarParam : _collect_linear_const, ##param.Param : _collect_linear_const, ##parameter : _collect_linear_const, - GeneralVarData : _linear_collect_var, + VarData : _linear_collect_var, ScalarVar : _linear_collect_var, Var : _linear_collect_var, variable : _linear_collect_var, diff --git a/pyomo/solvers/plugins/solvers/gurobi_persistent.py b/pyomo/solvers/plugins/solvers/gurobi_persistent.py index 97a3533c3f9..17ce33fd95f 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_persistent.py +++ b/pyomo/solvers/plugins/solvers/gurobi_persistent.py @@ -192,7 +192,7 @@ def set_var_attr(self, var, attr, val): Parameters ---------- - con: pyomo.core.base.var.GeneralVarData + con: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be modified. attr: str @@ -342,7 +342,7 @@ def get_var_attr(self, var, attr): Parameters ---------- - var: pyomo.core.base.var.GeneralVarData + var: pyomo.core.base.var.VarData The pyomo var for which the corresponding gurobi var attribute should be retrieved. attr: str diff --git a/pyomo/util/report_scaling.py b/pyomo/util/report_scaling.py index 265564bf12d..7619662c482 100644 --- a/pyomo/util/report_scaling.py +++ b/pyomo/util/report_scaling.py @@ -13,7 +13,7 @@ import math from pyomo.core.base.block import BlockData from pyomo.common.collections import ComponentSet -from pyomo.core.base.var import GeneralVarData +from pyomo.core.base.var import VarData from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd import logging @@ -73,7 +73,7 @@ def _check_coefficients( ): ders = reverse_sd(expr) for _v, _der in ders.items(): - if isinstance(_v, GeneralVarData): + if isinstance(_v, VarData): if _v.is_fixed(): continue der_lb, der_ub = compute_bounds_on_expr(_der) From 458c84e5acabcb95186c780238ec7f951b4e54b4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 20:29:24 -0600 Subject: [PATCH 1013/3044] Merge GeneralBooleanVarData into BooelanVarData class --- pyomo/core/base/boolean_var.py | 205 +++++++++++++-------------------- 1 file changed, 78 insertions(+), 127 deletions(-) diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index 925dca530a7..b6a25cd8f27 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -68,27 +68,61 @@ def __setstate__(self, state): self._boolvar = weakref_ref(state) +def _associated_binary_mapper(encode, val): + if val is None: + return None + if encode: + if val.__class__ is not _DeprecatedImplicitAssociatedBinaryVariable: + return val() + else: + if val.__class__ is not _DeprecatedImplicitAssociatedBinaryVariable: + return weakref_ref(val) + return val + + class BooleanVarData(ComponentData, BooleanValue): """ - This class defines the data for a single variable. + This class defines the data for a single Boolean variable. Constructor Arguments: component The BooleanVar object that owns this data. + Public Class Attributes: + domain The domain of this variable. fixed If True, then this variable is treated as a fixed constant in the model. stale A Boolean indicating whether the value of this variable is - legitimate. This value is true if the value should + legitimiate. This value is true if the value should be considered legitimate for purposes of reporting or other interrogation. value The numeric value of this variable. + + The domain attribute is a property because it is + too widely accessed directly to enforce explicit getter/setter + methods and we need to deter directly modifying or accessing + these attributes in certain cases. """ - __slots__ = () + __slots__ = ('_value', 'fixed', '_stale', '_associated_binary') + __autoslot_mappers__ = { + '_associated_binary': _associated_binary_mapper, + '_stale': StaleFlagManager.stale_mapper, + } def __init__(self, component=None): + # + # These lines represent in-lining of the + # following constructors: + # - BooleanVarData + # - ComponentData + # - BooleanValue self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET + self._value = None + self.fixed = False + self._stale = 0 # True + + self._associated_binary = None def is_fixed(self): """Returns True if this variable is fixed, otherwise returns False.""" @@ -132,118 +166,6 @@ def __call__(self, exception=True): """Compute the value of this variable.""" return self.value - @property - def value(self): - """Return the value for this variable.""" - raise NotImplementedError - - @property - def domain(self): - """Return the domain for this variable.""" - raise NotImplementedError - - @property - def fixed(self): - """Return the fixed indicator for this variable.""" - raise NotImplementedError - - @property - def stale(self): - """Return the stale indicator for this variable.""" - raise NotImplementedError - - def fix(self, value=NOTSET, skip_validation=False): - """Fix the value of this variable (treat as nonvariable) - - This sets the `fixed` indicator to True. If ``value`` is - provided, the value (and the ``skip_validation`` flag) are first - passed to :py:meth:`set_value()`. - - """ - self.fixed = True - if value is not NOTSET: - self.set_value(value, skip_validation) - - def unfix(self): - """Unfix this variable (treat as variable) - - This sets the `fixed` indicator to False. - - """ - self.fixed = False - - def free(self): - """Alias for :py:meth:`unfix`""" - return self.unfix() - - -class _BooleanVarData(metaclass=RenamedClass): - __renamed__new_class__ = BooleanVarData - __renamed__version__ = '6.7.2.dev0' - - -def _associated_binary_mapper(encode, val): - if val is None: - return None - if encode: - if val.__class__ is not _DeprecatedImplicitAssociatedBinaryVariable: - return val() - else: - if val.__class__ is not _DeprecatedImplicitAssociatedBinaryVariable: - return weakref_ref(val) - return val - - -class GeneralBooleanVarData(BooleanVarData): - """ - This class defines the data for a single Boolean variable. - - Constructor Arguments: - component The BooleanVar object that owns this data. - - Public Class Attributes: - domain The domain of this variable. - fixed If True, then this variable is treated as a - fixed constant in the model. - stale A Boolean indicating whether the value of this variable is - legitimiate. This value is true if the value should - be considered legitimate for purposes of reporting or - other interrogation. - value The numeric value of this variable. - - The domain attribute is a property because it is - too widely accessed directly to enforce explicit getter/setter - methods and we need to deter directly modifying or accessing - these attributes in certain cases. - """ - - __slots__ = ('_value', 'fixed', '_stale', '_associated_binary') - __autoslot_mappers__ = { - '_associated_binary': _associated_binary_mapper, - '_stale': StaleFlagManager.stale_mapper, - } - - def __init__(self, component=None): - # - # These lines represent in-lining of the - # following constructors: - # - BooleanVarData - # - ComponentData - # - BooleanValue - self._component = weakref_ref(component) if (component is not None) else None - self._index = NOTSET - self._value = None - self.fixed = False - self._stale = 0 # True - - self._associated_binary = None - - # - # Abstract Interface - # - - # value is an attribute - @property def value(self): """Return (or set) the value for this variable.""" @@ -271,13 +193,13 @@ def stale(self, val): def get_associated_binary(self): """Get the binary VarData associated with this - GeneralBooleanVarData""" + BooleanVarData""" return ( self._associated_binary() if self._associated_binary is not None else None ) def associate_binary_var(self, binary_var): - """Associate a binary VarData to this GeneralBooleanVarData""" + """Associate a binary VarData to this BooleanVarData""" if ( self._associated_binary is not None and type(self._associated_binary) @@ -299,9 +221,38 @@ def associate_binary_var(self, binary_var): if binary_var is not None: self._associated_binary = weakref_ref(binary_var) + def fix(self, value=NOTSET, skip_validation=False): + """Fix the value of this variable (treat as nonvariable) + + This sets the `fixed` indicator to True. If ``value`` is + provided, the value (and the ``skip_validation`` flag) are first + passed to :py:meth:`set_value()`. + + """ + self.fixed = True + if value is not NOTSET: + self.set_value(value, skip_validation) + + def unfix(self): + """Unfix this variable (treat as variable) + + This sets the `fixed` indicator to False. -class _GeneralBooleanVarData(metaclass=RenamedClass): - __renamed__new_class__ = GeneralBooleanVarData + """ + self.fixed = False + + def free(self): + """Alias for :py:meth:`unfix`""" + return self.unfix() + + +class _BooleanVarData(metaclass=RenamedClass): + __renamed__new_class__ = BooleanVarData + __renamed__version__ = '6.7.2.dev0' + + +class _BooleanVarData(metaclass=RenamedClass): + __renamed__new_class__ = BooleanVarData __renamed__version__ = '6.7.2.dev0' @@ -319,7 +270,7 @@ class BooleanVar(IndexedComponent): to True. """ - _ComponentDataClass = GeneralBooleanVarData + _ComponentDataClass = BooleanVarData def __new__(cls, *args, **kwds): if cls != BooleanVar: @@ -511,11 +462,11 @@ def _pprint(self): ) -class ScalarBooleanVar(GeneralBooleanVarData, BooleanVar): +class ScalarBooleanVar(BooleanVarData, BooleanVar): """A single variable.""" def __init__(self, *args, **kwd): - GeneralBooleanVarData.__init__(self, component=self) + BooleanVarData.__init__(self, component=self) BooleanVar.__init__(self, *args, **kwd) self._index = UnindexedComponent_index @@ -531,7 +482,7 @@ def __init__(self, *args, **kwd): def value(self): """Return the value for this variable.""" if self._constructed: - return GeneralBooleanVarData.value.fget(self) + return BooleanVarData.value.fget(self) raise ValueError( "Accessing the value of variable '%s' " "before the Var has been constructed (there " @@ -542,7 +493,7 @@ def value(self): def value(self, val): """Set the value for this variable.""" if self._constructed: - return GeneralBooleanVarData.value.fset(self, val) + return BooleanVarData.value.fset(self, val) raise ValueError( "Setting the value of variable '%s' " "before the Var has been constructed (there " @@ -551,7 +502,7 @@ def value(self, val): @property def domain(self): - return GeneralBooleanVarData.domain.fget(self) + return BooleanVarData.domain.fget(self) def fix(self, value=NOTSET, skip_validation=False): """ @@ -559,7 +510,7 @@ def fix(self, value=NOTSET, skip_validation=False): indicating the variable should be fixed at its current value. """ if self._constructed: - return GeneralBooleanVarData.fix(self, value, skip_validation) + return BooleanVarData.fix(self, value, skip_validation) raise ValueError( "Fixing variable '%s' " "before the Var has been constructed (there " @@ -569,7 +520,7 @@ def fix(self, value=NOTSET, skip_validation=False): def unfix(self): """Sets the fixed indicator to False.""" if self._constructed: - return GeneralBooleanVarData.unfix(self) + return BooleanVarData.unfix(self) raise ValueError( "Freeing variable '%s' " "before the Var has been constructed (there " From 015a4f859d63d9cdf615a488490301f88a2c96cc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 20:30:30 -0600 Subject: [PATCH 1014/3044] Update references from GeneralBooleanVarData to BooelanVarData --- pyomo/contrib/cp/repn/docplex_writer.py | 4 ++-- pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py | 2 +- pyomo/core/base/__init__.py | 6 ++++-- pyomo/core/base/boolean_var.py | 2 +- pyomo/core/base/component.py | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 221fd61af5b..1af153910c0 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -60,7 +60,7 @@ ) from pyomo.core.base.boolean_var import ( ScalarBooleanVar, - GeneralBooleanVarData, + BooleanVarData, IndexedBooleanVar, ) from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData @@ -964,7 +964,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): VarData: _before_var, IndexedVar: _before_indexed_var, ScalarBooleanVar: _before_boolean_var, - GeneralBooleanVarData: _before_boolean_var, + BooleanVarData: _before_boolean_var, IndexedBooleanVar: _before_indexed_boolean_var, GeneralExpressionData: _before_named_expression, ScalarExpression: _before_named_expression, diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index d9483c0ed14..0c493b89321 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -209,7 +209,7 @@ def _dispatch_atmost(visitor, node, *args): _before_child_dispatcher = {} _before_child_dispatcher[BV.ScalarBooleanVar] = _dispatch_boolean_var -_before_child_dispatcher[BV.GeneralBooleanVarData] = _dispatch_boolean_var +_before_child_dispatcher[BV.BooleanVarData] = _dispatch_boolean_var _before_child_dispatcher[AutoLinkedBooleanVar] = _dispatch_boolean_var _before_child_dispatcher[ParamData] = _dispatch_param _before_child_dispatcher[ScalarParam] = _dispatch_param diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 6851fe4cda0..bcc2a0e0e02 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -84,7 +84,7 @@ from pyomo.core.base.boolean_var import ( BooleanVar, BooleanVarData, - GeneralBooleanVarData, + BooleanVarData, BooleanVarList, ScalarBooleanVar, ) @@ -167,12 +167,14 @@ relocated_module_attribute( f'_GeneralVarData', f'pyomo.core.base.VarData', version='6.7.2.dev0' ) +relocated_module_attribute( + f'_GeneralBooleanVarData', f'pyomo.core.base.BooleanVarData', version='6.7.2.dev0' +) for _cdata in ( 'ConstraintData', 'LogicalConstraintData', 'ExpressionData', 'VarData', - 'GeneralBooleanVarData', 'BooleanVarData', 'ObjectiveData', ): diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index b6a25cd8f27..98761dee536 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -251,7 +251,7 @@ class _BooleanVarData(metaclass=RenamedClass): __renamed__version__ = '6.7.2.dev0' -class _BooleanVarData(metaclass=RenamedClass): +class _GeneralBooleanVarData(metaclass=RenamedClass): __renamed__new_class__ = BooleanVarData __renamed__version__ = '6.7.2.dev0' diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 65844379eca..1c73809a25a 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -805,7 +805,7 @@ class ComponentData(_ComponentBase): # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, LogicalConstraintData, # GeneralLogicalConstraintData, GeneralObjectiveData, - # ParamData,VarData, GeneralBooleanVarData, DisjunctionData, + # ParamData,VarData, BooleanVarData, DisjunctionData, # ArcData, PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those # constructors as well! From 0cbfcb7418310b2dbb97aa6e915d4570e02c1d37 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 20:38:00 -0600 Subject: [PATCH 1015/3044] Restore deprecation path for GeneralVarData --- pyomo/core/base/var.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index b1634b61c44..8870fc5b09c 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -575,7 +575,7 @@ class _VarData(metaclass=RenamedClass): __renamed__version__ = '6.7.2.dev0' -class _VarData(metaclass=RenamedClass): +class _GeneralVarData(metaclass=RenamedClass): __renamed__new_class__ = VarData __renamed__version__ = '6.7.2.dev0' From adfe72b6ff4a6900f98d77dad7cac83da7e41250 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 20:38:35 -0600 Subject: [PATCH 1016/3044] Merge GeneralLogicalConstraintData into LogicalConstratintData --- pyomo/core/base/component.py | 2 +- pyomo/core/base/logical_constraint.py | 77 ++++----------------------- 2 files changed, 11 insertions(+), 68 deletions(-) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 1c73809a25a..3dd4d47046d 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -804,7 +804,7 @@ class ComponentData(_ComponentBase): # NOTE: This constructor is in-lined in the constructors for the following # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, LogicalConstraintData, - # GeneralLogicalConstraintData, GeneralObjectiveData, + # LogicalConstraintData, GeneralObjectiveData, # ParamData,VarData, BooleanVarData, DisjunctionData, # ArcData, PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index 23a422705df..1daa5f83e90 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -43,68 +43,6 @@ class LogicalConstraintData(ActiveComponentData): - """ - This class defines the data for a single logical constraint. - - It functions as a pure interface. - - Constructor arguments: - component The LogicalConstraint object that owns this data. - - Public class attributes: - active A boolean that is true if this statement is - active in the model. - body The Pyomo logical expression for this statement - - Private class attributes: - _component The statement component. - _active A boolean that indicates whether this data is active - """ - - __slots__ = () - - def __init__(self, component=None): - # - # These lines represent in-lining of the - # following constructors: - # - ActiveComponentData - # - ComponentData - self._component = weakref_ref(component) if (component is not None) else None - self._index = NOTSET - self._active = True - - # - # Interface - # - def __call__(self, exception=True): - """Compute the value of the body of this logical constraint.""" - if self.body is None: - return None - return self.body(exception=exception) - - # - # Abstract Interface - # - @property - def expr(self): - """Get the expression on this logical constraint.""" - raise NotImplementedError - - def set_value(self, expr): - """Set the expression on this logical constraint.""" - raise NotImplementedError - - def get_value(self): - """Get the expression on this logical constraint.""" - raise NotImplementedError - - -class _LogicalConstraintData(metaclass=RenamedClass): - __renamed__new_class__ = LogicalConstraintData - __renamed__version__ = '6.7.2.dev0' - - -class GeneralLogicalConstraintData(LogicalConstraintData): """ This class defines the data for a single general logical constraint. @@ -178,8 +116,13 @@ def get_value(self): return self._expr +class _LogicalConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = LogicalConstraintData + __renamed__version__ = '6.7.2.dev0' + + class _GeneralLogicalConstraintData(metaclass=RenamedClass): - __renamed__new_class__ = GeneralLogicalConstraintData + __renamed__new_class__ = LogicalConstraintData __renamed__version__ = '6.7.2.dev0' @@ -225,7 +168,7 @@ class LogicalConstraint(ActiveIndexedComponent): The class type for the derived subclass """ - _ComponentDataClass = GeneralLogicalConstraintData + _ComponentDataClass = LogicalConstraintData class Infeasible(object): pass @@ -419,14 +362,14 @@ def _check_skip_add(self, index, expr): return expr -class ScalarLogicalConstraint(GeneralLogicalConstraintData, LogicalConstraint): +class ScalarLogicalConstraint(LogicalConstraintData, LogicalConstraint): """ ScalarLogicalConstraint is the implementation representing a single, non-indexed logical constraint. """ def __init__(self, *args, **kwds): - GeneralLogicalConstraintData.__init__(self, component=self, expr=None) + LogicalConstraintData.__init__(self, component=self, expr=None) LogicalConstraint.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -446,7 +389,7 @@ def body(self): "an expression. There is currently " "nothing to access." % self.name ) - return GeneralLogicalConstraintData.body.fget(self) + return LogicalConstraintData.body.fget(self) raise ValueError( "Accessing the body of logical constraint '%s' " "before the LogicalConstraint has been constructed (there " From 2f1d4a0387995b741028f96e0e05aadc8c4a30a0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 20:44:43 -0600 Subject: [PATCH 1017/3044] NFC: apply black --- pyomo/repn/tests/test_standard_form.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/pyomo/repn/tests/test_standard_form.py b/pyomo/repn/tests/test_standard_form.py index 9dee2b1d25d..591703e6ae8 100644 --- a/pyomo/repn/tests/test_standard_form.py +++ b/pyomo/repn/tests/test_standard_form.py @@ -245,33 +245,21 @@ def test_alternative_forms(self): m, mixed_form=True, column_order=col_order ) - self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1), (m.e, 1), (m.e, -1), (m.f, 0)]) self.assertEqual( - list(map(str, repn.x)), - ['x', 'y[0]', 'y[1]', 'y[3]'], + repn.rows, [(m.c, -1), (m.d, 1), (m.e, 1), (m.e, -1), (m.f, 0)] ) + self.assertEqual(list(map(str, repn.x)), ['x', 'y[0]', 'y[1]', 'y[3]']) self.assertEqual( - list(v.bounds for v in repn.x), - [(None, None), (0, 10), (-5, 10), (-5, -2)], + list(v.bounds for v in repn.x), [(None, None), (0, 10), (-5, 10), (-5, -2)] ) ref = np.array( - [ - [1, 0, 2, 0], - [0, 0, 1, 4], - [0, 1, 6, 0], - [0, 1, 6, 0], - [1, 1, 0, 0], - ] + [[1, 0, 2, 0], [0, 0, 1, 4], [0, 1, 6, 0], [0, 1, 6, 0], [1, 1, 0, 0]] ) self.assertTrue(np.all(repn.A == ref)) print(repn) print(repn.b) self.assertTrue(np.all(repn.b == np.array([3, 5, 6, -3, 8]))) - self.assertTrue( - np.all( - repn.c == np.array([[-1, 0, -5, 0], [1, 0, 0, 15]]) - ) - ) + self.assertTrue(np.all(repn.c == np.array([[-1, 0, -5, 0], [1, 0, 0, 15]]))) # Note that the solution is a mix of inequality and equality constraints # self._verify_solution(soln, repn, False) From a27b8791f864546444824e4d803c86e48b6de606 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 21:33:32 -0600 Subject: [PATCH 1018/3044] Merge GeneralObjectiveData into ObjectiveData --- pyomo/core/base/objective.py | 58 ++++++++---------------------------- 1 file changed, 12 insertions(+), 46 deletions(-) diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index 58cb198e1ae..e4956748b6c 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -81,51 +81,8 @@ def O_rule(model, i, j): return rule_wrapper(rule, {None: ObjectiveList.End}) -# -# This class is a pure interface -# - - -class ObjectiveData(ExpressionData): - """ - This class defines the data for a single objective. - - Public class attributes: - expr The Pyomo expression for this objective - sense The direction for this objective. - """ - - __slots__ = () - - # - # Interface - # - - def is_minimizing(self): - """Return True if this is a minimization objective.""" - return self.sense == minimize - - # - # Abstract Interface - # - - @property - def sense(self): - """Access sense (direction) of this objective.""" - raise NotImplementedError - - def set_sense(self, sense): - """Set the sense (direction) of this objective.""" - raise NotImplementedError - - -class _ObjectiveData(metaclass=RenamedClass): - __renamed__new_class__ = ObjectiveData - __renamed__version__ = '6.7.2.dev0' - - -class GeneralObjectiveData( - GeneralExpressionDataImpl, ObjectiveData, ActiveComponentData +class ObjectiveData( + GeneralExpressionDataImpl, ActiveComponentData ): """ This class defines the data for a single objective. @@ -166,6 +123,10 @@ def __init__(self, expr=None, sense=minimize, component=None): "value: %s'" % (minimize, maximize, sense) ) + def is_minimizing(self): + """Return True if this is a minimization objective.""" + return self.sense == minimize + def set_value(self, expr): if expr is None: raise ValueError(_rule_returned_none_error % (self.name,)) @@ -197,8 +158,13 @@ def set_sense(self, sense): ) +class _ObjectiveData(metaclass=RenamedClass): + __renamed__new_class__ = ObjectiveData + __renamed__version__ = '6.7.2.dev0' + + class _GeneralObjectiveData(metaclass=RenamedClass): - __renamed__new_class__ = GeneralObjectiveData + __renamed__new_class__ = ObjectiveData __renamed__version__ = '6.7.2.dev0' From 13c11e58a8b49de727e3a9d0eeb630b55a19f1be Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 21:38:00 -0600 Subject: [PATCH 1019/3044] Update references from GeneralObjectiveData to ObjectiveData --- pyomo/contrib/appsi/base.py | 8 ++++---- pyomo/contrib/appsi/fbbt.py | 6 +++--- pyomo/contrib/appsi/solvers/cbc.py | 4 ++-- pyomo/contrib/appsi/solvers/cplex.py | 4 ++-- pyomo/contrib/appsi/solvers/ipopt.py | 4 ++-- pyomo/contrib/appsi/writers/lp_writer.py | 4 ++-- pyomo/contrib/appsi/writers/nl_writer.py | 4 ++-- pyomo/contrib/community_detection/detection.py | 4 ++-- pyomo/contrib/latex_printer/latex_printer.py | 4 ++-- pyomo/contrib/solver/base.py | 4 ++-- pyomo/contrib/solver/persistent.py | 6 +++--- pyomo/core/base/component.py | 2 +- pyomo/core/base/objective.py | 16 +++++++--------- pyomo/core/tests/unit/test_dict_objects.py | 4 ++-- pyomo/core/tests/unit/test_list_objects.py | 4 ++-- pyomo/repn/plugins/nl_writer.py | 6 +----- pyomo/repn/standard_repn.py | 6 +++--- 17 files changed, 42 insertions(+), 48 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 930ff8393e9..9d00a56e8b9 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -26,7 +26,7 @@ from pyomo.core.base.var import VarData, Var from pyomo.core.base.param import ParamData, Param from pyomo.core.base.block import BlockData, Block -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.common.collections import ComponentMap from .utils.get_objective import get_objective from .utils.collect_vars_and_named_exprs import collect_vars_and_named_exprs @@ -827,7 +827,7 @@ def remove_block(self, block: BlockData): pass @abc.abstractmethod - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): pass @abc.abstractmethod @@ -1050,10 +1050,10 @@ def add_sos_constraints(self, cons: List[SOSConstraintData]): self._add_sos_constraints(cons) @abc.abstractmethod - def _set_objective(self, obj: GeneralObjectiveData): + def _set_objective(self, obj: ObjectiveData): pass - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): if self._objective is not None: for v in self._vars_referenced_by_obj: self._referenced_variables[id(v)][2] = None diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 1ebb3d40381..4b0d6d4876c 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -22,7 +22,7 @@ from pyomo.core.base.param import ParamData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.sos import SOSConstraintData -from pyomo.core.base.objective import GeneralObjectiveData, minimize, maximize +from pyomo.core.base.objective import ObjectiveData, minimize, maximize from pyomo.core.base.block import BlockData from pyomo.core.base import SymbolMap, TextLabeler from pyomo.common.errors import InfeasibleConstraintException @@ -224,13 +224,13 @@ def update_params(self): cp = self._param_map[p_id] cp.value = p.value - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): if self._symbolic_solver_labels: if self._objective is not None: self._symbol_map.removeSymbol(self._objective) super().set_objective(obj) - def _set_objective(self, obj: GeneralObjectiveData): + def _set_objective(self, obj: ObjectiveData): if obj is None: ce = cmodel.Constant(0) sense = 0 diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index 7db9a32764e..dffd479a5c7 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -30,7 +30,7 @@ from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream import sys @@ -188,7 +188,7 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): def remove_block(self, block: BlockData): self._writer.remove_block(block) - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): self._writer.set_objective(obj) def update_variables(self, variables: List[VarData]): diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 0ed3495ac1c..22c11bdfbe8 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -26,7 +26,7 @@ from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.common.timing import HierarchicalTimer import sys import time @@ -203,7 +203,7 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): def remove_block(self, block: BlockData): self._writer.remove_block(block) - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): self._writer.set_objective(obj) def update_variables(self, variables: List[VarData]): diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 5cd9a51785d..4144fbbecd9 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -32,7 +32,7 @@ from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.common.timing import HierarchicalTimer from pyomo.common.tee import TeeStream import sys @@ -252,7 +252,7 @@ def remove_constraints(self, cons: List[GeneralConstraintData]): def remove_block(self, block: BlockData): self._writer.remove_block(block) - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): self._writer.set_objective(obj) def update_variables(self, variables: List[VarData]): diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 4be2b32d83d..3a6193bd314 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -13,7 +13,7 @@ from pyomo.core.base.param import ParamData from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn @@ -147,7 +147,7 @@ def update_params(self): cp = self._pyomo_param_to_solver_param_map[p_id] cp.value = p.value - def _set_objective(self, obj: GeneralObjectiveData): + def _set_objective(self, obj: ObjectiveData): cobj = cmodel.process_lp_objective( self._expr_types, obj, diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 70176146a1e..754bd179497 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -13,7 +13,7 @@ from pyomo.core.base.param import ParamData from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.block import BlockData from pyomo.repn.standard_repn import generate_standard_repn @@ -180,7 +180,7 @@ def update_params(self): cp = self._pyomo_param_to_solver_param_map[p_id] cp.value = p.value - def _set_objective(self, obj: GeneralObjectiveData): + def _set_objective(self, obj: ObjectiveData): if obj is None: const = cmodel.Constant(0) lin_vars = list() diff --git a/pyomo/contrib/community_detection/detection.py b/pyomo/contrib/community_detection/detection.py index af87fa5eb8b..0e2c3912e06 100644 --- a/pyomo/contrib/community_detection/detection.py +++ b/pyomo/contrib/community_detection/detection.py @@ -31,7 +31,7 @@ Objective, ConstraintList, ) -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.core.expr.visitor import replace_expressions, identify_variables from pyomo.contrib.community_detection.community_graph import generate_model_graph from pyomo.common.dependencies import networkx as nx @@ -750,7 +750,7 @@ def generate_structured_model(self): # Check to see whether 'stored_constraint' is actually an objective (since constraints and objectives # grouped together) if self.with_objective and isinstance( - stored_constraint, (GeneralObjectiveData, Objective) + stored_constraint, (ObjectiveData, Objective) ): # If the constraint is actually an objective, we add it to the block as an objective new_objective = Objective( diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index e11543cb375..13f30f899e4 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -35,7 +35,7 @@ from pyomo.core.expr.visitor import identify_components from pyomo.core.expr.base import ExpressionBase from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData -from pyomo.core.base.objective import ScalarObjective, GeneralObjectiveData +from pyomo.core.base.objective import ScalarObjective, ObjectiveData import pyomo.core.kernel as kernel from pyomo.core.expr.template_expr import ( GetItemExpression, @@ -403,7 +403,7 @@ def __init__(self): ScalarExpression: handle_named_expression_node, kernel.expression.expression: handle_named_expression_node, kernel.expression.noclone: handle_named_expression_node, - GeneralObjectiveData: handle_named_expression_node, + ObjectiveData: handle_named_expression_node, VarData: handle_var_node, ScalarObjective: handle_named_expression_node, kernel.objective.objective: handle_named_expression_node, diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index fdc7361e6b8..c53f917bc2a 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -18,7 +18,7 @@ from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData from pyomo.core.base.block import BlockData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.common.config import document_kwargs_from_configdict, ConfigValue from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning @@ -274,7 +274,7 @@ def set_instance(self, model): """ @abc.abstractmethod - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): """ Set current objective for the model """ diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index 103eb3c622f..81d0df1334f 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -16,7 +16,7 @@ from pyomo.core.base.sos import SOSConstraintData, SOSConstraint from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData, Param -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.common.collections import ComponentMap from pyomo.common.timing import HierarchicalTimer from pyomo.core.expr.numvalue import NumericConstant @@ -149,10 +149,10 @@ def add_sos_constraints(self, cons: List[SOSConstraintData]): self._add_sos_constraints(cons) @abc.abstractmethod - def _set_objective(self, obj: GeneralObjectiveData): + def _set_objective(self, obj: ObjectiveData): pass - def set_objective(self, obj: GeneralObjectiveData): + def set_objective(self, obj: ObjectiveData): if self._objective is not None: for v in self._vars_referenced_by_obj: self._referenced_variables[id(v)][2] = None diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 3dd4d47046d..380aab23cbe 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -804,7 +804,7 @@ class ComponentData(_ComponentBase): # NOTE: This constructor is in-lined in the constructors for the following # classes: BooleanVarData, ConnectorData, ConstraintData, # GeneralExpressionData, LogicalConstraintData, - # LogicalConstraintData, GeneralObjectiveData, + # LogicalConstraintData, ObjectiveData, # ParamData,VarData, BooleanVarData, DisjunctionData, # ArcData, PortData, _LinearConstraintData, and # _LinearMatrixConstraintData. Changes made here need to be made in those diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index e4956748b6c..fea356229fb 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -81,9 +81,7 @@ def O_rule(model, i, j): return rule_wrapper(rule, {None: ObjectiveList.End}) -class ObjectiveData( - GeneralExpressionDataImpl, ActiveComponentData -): +class ObjectiveData(GeneralExpressionDataImpl, ActiveComponentData): """ This class defines the data for a single objective. @@ -216,7 +214,7 @@ class Objective(ActiveIndexedComponent): The class type for the derived subclass """ - _ComponentDataClass = GeneralObjectiveData + _ComponentDataClass = ObjectiveData NoObjective = ActiveIndexedComponent.Skip def __new__(cls, *args, **kwds): @@ -365,14 +363,14 @@ def display(self, prefix="", ostream=None): ) -class ScalarObjective(GeneralObjectiveData, Objective): +class ScalarObjective(ObjectiveData, Objective): """ ScalarObjective is the implementation representing a single, non-indexed objective. """ def __init__(self, *args, **kwd): - GeneralObjectiveData.__init__(self, expr=None, component=self) + ObjectiveData.__init__(self, expr=None, component=self) Objective.__init__(self, *args, **kwd) self._index = UnindexedComponent_index @@ -408,7 +406,7 @@ def expr(self): "a sense or expression (there is currently " "no value to return)." % (self.name) ) - return GeneralObjectiveData.expr.fget(self) + return ObjectiveData.expr.fget(self) raise ValueError( "Accessing the expression of objective '%s' " "before the Objective has been constructed (there " @@ -431,7 +429,7 @@ def sense(self): "a sense or expression (there is currently " "no value to return)." % (self.name) ) - return GeneralObjectiveData.sense.fget(self) + return ObjectiveData.sense.fget(self) raise ValueError( "Accessing the sense of objective '%s' " "before the Objective has been constructed (there " @@ -474,7 +472,7 @@ def set_sense(self, sense): if self._constructed: if len(self._data) == 0: self._data[None] = self - return GeneralObjectiveData.set_sense(self, sense) + return ObjectiveData.set_sense(self, sense) raise ValueError( "Setting the sense of objective '%s' " "before the Objective has been constructed (there " diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index 0dc5cacd216..fae1d21a87e 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -19,7 +19,7 @@ ) from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -384,7 +384,7 @@ def setUp(self): class TestObjectiveDict(_TestActiveComponentDictBase, unittest.TestCase): _ctype = ObjectiveDict - _cdatatype = GeneralObjectiveData + _cdatatype = ObjectiveData def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index f98b5279fc5..32f2fa328cf 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -19,7 +19,7 @@ ) from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData -from pyomo.core.base.objective import GeneralObjectiveData +from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.expression import GeneralExpressionData @@ -401,7 +401,7 @@ def setUp(self): class TestObjectiveList(_TestActiveComponentListBase, unittest.TestCase): _ctype = XObjectiveList - _cdatatype = GeneralObjectiveData + _cdatatype = ObjectiveData def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 23e14104b89..3c3c8539294 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -71,11 +71,7 @@ from pyomo.core.base.component import ActiveComponent from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData -from pyomo.core.base.objective import ( - ScalarObjective, - GeneralObjectiveData, - ObjectiveData, -) +from pyomo.core.base.objective import ScalarObjective, ObjectiveData from pyomo.core.base.suffix import SuffixFinder from pyomo.core.base.var import VarData import pyomo.core.kernel as kernel diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index 2f5a413e963..a23ebf6bb4f 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -19,7 +19,7 @@ import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import NumericConstant -from pyomo.core.base.objective import GeneralObjectiveData, ScalarObjective +from pyomo.core.base.objective import ObjectiveData, ScalarObjective from pyomo.core.base import ExpressionData, Expression from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData from pyomo.core.base.var import ScalarVar, Var, VarData, value @@ -1154,7 +1154,7 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra noclone: _collect_identity, ExpressionData: _collect_identity, Expression: _collect_identity, - GeneralObjectiveData: _collect_identity, + ObjectiveData: _collect_identity, ScalarObjective: _collect_identity, objective: _collect_identity, } @@ -1553,7 +1553,7 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): noclone : _linear_collect_identity, ExpressionData : _linear_collect_identity, Expression : _linear_collect_identity, - GeneralObjectiveData : _linear_collect_identity, + ObjectiveData : _linear_collect_identity, ScalarObjective : _linear_collect_identity, objective : _linear_collect_identity, } From 787eaf02a59cacd723c866c8f67740a82888c14b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 22:38:41 -0600 Subject: [PATCH 1020/3044] Rename _ExpressionData, _GeneralExpressionDataImpl -> NamedExpressionData; _GeneralExpressionData -> ExpressionData --- pyomo/core/base/expression.py | 119 ++++++++++++---------------------- 1 file changed, 41 insertions(+), 78 deletions(-) diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index f5376381b2d..31bb5f835df 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -36,7 +36,7 @@ logger = logging.getLogger('pyomo.core') -class ExpressionData(numeric_expr.NumericValue): +class NamedExpressionData(numeric_expr.NumericValue): """ An object that defines a named expression. @@ -44,15 +44,14 @@ class ExpressionData(numeric_expr.NumericValue): expr The expression owned by this data. """ - __slots__ = () + __slots__ = ('_args_',) EXPRESSION_SYSTEM = EXPR.ExpressionType.NUMERIC PRECEDENCE = 0 ASSOCIATIVITY = EXPR.OperatorAssociativity.NON_ASSOCIATIVE - # - # Interface - # + def __init__(self, expr=None): + self._args_ = (expr,) def __call__(self, exception=True): """Compute the value of this expression.""" @@ -62,6 +61,18 @@ def __call__(self, exception=True): return arg return arg(exception=exception) + def create_node_with_local_data(self, values): + """ + Construct a simple expression after constructing the + contained expression. + + This class provides a consistent interface for constructing a + node, which is used in tree visitor scripts. + """ + obj = self.__class__() + obj._args_ = values + return obj + def is_named_expression_type(self): """A boolean indicating whether this in a named expression.""" return True @@ -110,9 +121,10 @@ def _compute_polynomial_degree(self, result): def _is_fixed(self, values): return values[0] - # - # Abstract Interface - # + # NamedExpressionData should never return False because + # they can store subexpressions that contain variables + def is_potentially_variable(self): + return True @property def expr(self): @@ -125,63 +137,6 @@ def expr(self): def expr(self, value): self.set_value(value) - def set_value(self, expr): - """Set the expression on this expression.""" - raise NotImplementedError - - def is_constant(self): - """A boolean indicating whether this expression is constant.""" - raise NotImplementedError - - def is_fixed(self): - """A boolean indicating whether this expression is fixed.""" - raise NotImplementedError - - # ExpressionData should never return False because - # they can store subexpressions that contain variables - def is_potentially_variable(self): - return True - - -class _ExpressionData(metaclass=RenamedClass): - __renamed__new_class__ = ExpressionData - __renamed__version__ = '6.7.2.dev0' - - -class GeneralExpressionDataImpl(ExpressionData): - """ - An object that defines an expression that is never cloned - - Constructor Arguments - expr The Pyomo expression stored in this expression. - component The Expression object that owns this data. - - Public Class Attributes - expr The expression owned by this data. - """ - - __slots__ = () - - def __init__(self, expr=None): - self._args_ = (expr,) - - def create_node_with_local_data(self, values): - """ - Construct a simple expression after constructing the - contained expression. - - This class provides a consistent interface for constructing a - node, which is used in tree visitor scripts. - """ - obj = ScalarExpression() - obj.construct() - obj._args_ = values - return obj - - # - # Abstract Interface - # - def set_value(self, expr): """Set the expression on this expression.""" if expr is None or expr.__class__ in native_numeric_types: @@ -240,7 +195,16 @@ def __ipow__(self, other): return numeric_expr._pow_dispatcher[e.__class__, other.__class__](e, other) -class GeneralExpressionData(GeneralExpressionDataImpl, ComponentData): +class _ExpressionData(metaclass=RenamedClass): + __renamed__new_class__ = NamedExpressionData + __renamed__version__ = '6.7.2.dev0' + +class _GeneralExpressionDataImpl(metaclass=RenamedClass): + __renamed__new_class__ = NamedExpressionData + __renamed__version__ = '6.7.2.dev0' + + +class ExpressionData(NamedExpressionData, ComponentData): """ An object that defines an expression that is never cloned @@ -255,17 +219,16 @@ class GeneralExpressionData(GeneralExpressionDataImpl, ComponentData): _component The expression component. """ - __slots__ = ('_args_',) + __slots__ = () def __init__(self, expr=None, component=None): - GeneralExpressionDataImpl.__init__(self, expr) - # Inlining ComponentData.__init__ + self._args_ = (expr,) self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET class _GeneralExpressionData(metaclass=RenamedClass): - __renamed__new_class__ = GeneralExpressionData + __renamed__new_class__ = ExpressionData __renamed__version__ = '6.7.2.dev0' @@ -285,7 +248,7 @@ class Expression(IndexedComponent): doc Text describing this component. """ - _ComponentDataClass = GeneralExpressionData + _ComponentDataClass = ExpressionData # This seems like a copy-paste error, and should be renamed/removed NoConstraint = IndexedComponent.Skip @@ -412,9 +375,9 @@ def construct(self, data=None): timer.report() -class ScalarExpression(GeneralExpressionData, Expression): +class ScalarExpression(ExpressionData, Expression): def __init__(self, *args, **kwds): - GeneralExpressionData.__init__(self, expr=None, component=self) + ExpressionData.__init__(self, expr=None, component=self) Expression.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -437,7 +400,7 @@ def __call__(self, exception=True): def expr(self): """Return expression on this expression.""" if self._constructed: - return GeneralExpressionData.expr.fget(self) + return ExpressionData.expr.fget(self) raise ValueError( "Accessing the expression of Expression '%s' " "before the Expression has been constructed (there " @@ -455,7 +418,7 @@ def clear(self): def set_value(self, expr): """Set the expression on this expression.""" if self._constructed: - return GeneralExpressionData.set_value(self, expr) + return ExpressionData.set_value(self, expr) raise ValueError( "Setting the expression of Expression '%s' " "before the Expression has been constructed (there " @@ -465,7 +428,7 @@ def set_value(self, expr): def is_constant(self): """A boolean indicating whether this expression is constant.""" if self._constructed: - return GeneralExpressionData.is_constant(self) + return ExpressionData.is_constant(self) raise ValueError( "Accessing the is_constant flag of Expression '%s' " "before the Expression has been constructed (there " @@ -475,7 +438,7 @@ def is_constant(self): def is_fixed(self): """A boolean indicating whether this expression is fixed.""" if self._constructed: - return GeneralExpressionData.is_fixed(self) + return ExpressionData.is_fixed(self) raise ValueError( "Accessing the is_fixed flag of Expression '%s' " "before the Expression has been constructed (there " @@ -519,6 +482,6 @@ def add(self, index, expr): """Add an expression with a given index.""" if (type(expr) is tuple) and (expr == Expression.Skip): return None - cdata = GeneralExpressionData(expr, component=self) + cdata = ExpressionData(expr, component=self) self._data[index] = cdata return cdata From 74b79181869619d68778eb9361d431dad362361c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 22:40:12 -0600 Subject: [PATCH 1021/3044] Update *ExpressionData* referencces --- pyomo/contrib/appsi/cmodel/src/expression.hpp | 6 ++--- pyomo/contrib/cp/repn/docplex_writer.py | 6 ++--- .../logical_to_disjunctive_walker.py | 4 ++-- pyomo/contrib/fbbt/fbbt.py | 24 ++++++++----------- pyomo/contrib/latex_printer/latex_printer.py | 4 ++-- pyomo/contrib/mcpp/pyomo_mcpp.py | 6 +++-- pyomo/core/base/__init__.py | 6 +++-- pyomo/core/base/component.py | 2 +- pyomo/core/base/expression.py | 9 ++++--- pyomo/core/base/objective.py | 9 +++---- pyomo/core/expr/numeric_expr.py | 2 +- pyomo/core/tests/unit/test_dict_objects.py | 4 ++-- pyomo/core/tests/unit/test_expression.py | 8 +++---- pyomo/core/tests/unit/test_list_objects.py | 4 ++-- pyomo/dae/integral.py | 4 ++-- pyomo/gdp/tests/test_util.py | 6 ++--- pyomo/repn/plugins/ampl/ampl_.py | 4 ++-- pyomo/repn/plugins/nl_writer.py | 2 +- pyomo/repn/standard_repn.py | 16 ++++++++----- pyomo/repn/util.py | 4 ++-- 20 files changed, 67 insertions(+), 63 deletions(-) diff --git a/pyomo/contrib/appsi/cmodel/src/expression.hpp b/pyomo/contrib/appsi/cmodel/src/expression.hpp index 803bb21b6e2..ad1234b3863 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.hpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.hpp @@ -700,7 +700,7 @@ class PyomoExprTypes { expr_type_map[UnaryFunctionExpression] = unary_func; expr_type_map[NPV_UnaryFunctionExpression] = unary_func; expr_type_map[LinearExpression] = linear; - expr_type_map[_GeneralExpressionData] = named_expr; + expr_type_map[_ExpressionData] = named_expr; expr_type_map[ScalarExpression] = named_expr; expr_type_map[Integral] = named_expr; expr_type_map[ScalarIntegral] = named_expr; @@ -765,8 +765,8 @@ class PyomoExprTypes { py::object NumericConstant = py::module_::import("pyomo.core.expr.numvalue").attr("NumericConstant"); py::object expr_module = py::module_::import("pyomo.core.base.expression"); - py::object _GeneralExpressionData = - expr_module.attr("_GeneralExpressionData"); + py::object _ExpressionData = + expr_module.attr("_ExpressionData"); py::object ScalarExpression = expr_module.attr("ScalarExpression"); py::object ScalarIntegral = py::module_::import("pyomo.dae.integral").attr("ScalarIntegral"); diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 1af153910c0..37429d420d2 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -63,7 +63,7 @@ BooleanVarData, IndexedBooleanVar, ) -from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, ExpressionData from pyomo.core.base.param import IndexedParam, ScalarParam, ParamData from pyomo.core.base.var import ScalarVar, VarData, IndexedVar import pyomo.core.expr as EXPR @@ -949,7 +949,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): BeforeExpression: _handle_before_expression_node, AtExpression: _handle_at_expression_node, AlwaysIn: _handle_always_in_node, - GeneralExpressionData: _handle_named_expression_node, + ExpressionData: _handle_named_expression_node, ScalarExpression: _handle_named_expression_node, } _var_handles = { @@ -966,7 +966,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): ScalarBooleanVar: _before_boolean_var, BooleanVarData: _before_boolean_var, IndexedBooleanVar: _before_indexed_boolean_var, - GeneralExpressionData: _before_named_expression, + ExpressionData: _before_named_expression, ScalarExpression: _before_named_expression, IndexedParam: _before_indexed_param, # Because of indirection ScalarParam: _before_param, diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index 0c493b89321..fdcfd5a8308 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -27,7 +27,7 @@ value, ) import pyomo.core.base.boolean_var as BV -from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, ExpressionData from pyomo.core.base.param import ScalarParam, ParamData from pyomo.core.base.var import ScalarVar, VarData from pyomo.gdp.disjunct import AutoLinkedBooleanVar, Disjunct, Disjunction @@ -217,7 +217,7 @@ def _dispatch_atmost(visitor, node, *args): # don't handle them: _before_child_dispatcher[ScalarVar] = _dispatch_var _before_child_dispatcher[VarData] = _dispatch_var -_before_child_dispatcher[GeneralExpressionData] = _dispatch_expression +_before_child_dispatcher[ExpressionData] = _dispatch_expression _before_child_dispatcher[ScalarExpression] = _dispatch_expression diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 86f94506841..eb7155313c4 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -26,7 +26,7 @@ from pyomo.core.base.constraint import Constraint from pyomo.core.base.var import Var from pyomo.gdp import Disjunct -from pyomo.core.base.expression import GeneralExpressionData, ScalarExpression +from pyomo.core.base.expression import ExpressionData, ScalarExpression import logging from pyomo.common.errors import InfeasibleConstraintException, PyomoException from pyomo.common.config import ( @@ -333,15 +333,15 @@ def _prop_bnds_leaf_to_root_UnaryFunctionExpression(visitor, node, arg): _unary_leaf_to_root_map[node.getname()](visitor, node, arg) -def _prop_bnds_leaf_to_root_GeneralExpression(visitor, node, expr): +def _prop_bnds_leaf_to_root_NamedExpression(visitor, node, expr): """ Propagate bounds from children to parent Parameters ---------- visitor: _FBBTVisitorLeafToRoot - node: pyomo.core.base.expression.GeneralExpressionData - expr: GeneralExpression arg + node: pyomo.core.base.expression.ExpressionData + expr: NamedExpressionData arg """ bnds_dict = visitor.bnds_dict if node in bnds_dict: @@ -366,8 +366,8 @@ def _prop_bnds_leaf_to_root_GeneralExpression(visitor, node, expr): numeric_expr.UnaryFunctionExpression: _prop_bnds_leaf_to_root_UnaryFunctionExpression, numeric_expr.LinearExpression: _prop_bnds_leaf_to_root_SumExpression, numeric_expr.AbsExpression: _prop_bnds_leaf_to_root_abs, - GeneralExpressionData: _prop_bnds_leaf_to_root_GeneralExpression, - ScalarExpression: _prop_bnds_leaf_to_root_GeneralExpression, + ExpressionData: _prop_bnds_leaf_to_root_NamedExpression, + ScalarExpression: _prop_bnds_leaf_to_root_NamedExpression, }, ) @@ -898,13 +898,13 @@ def _prop_bnds_root_to_leaf_UnaryFunctionExpression(node, bnds_dict, feasibility ) -def _prop_bnds_root_to_leaf_GeneralExpression(node, bnds_dict, feasibility_tol): +def _prop_bnds_root_to_leaf_NamedExpression(node, bnds_dict, feasibility_tol): """ Propagate bounds from parent to children. Parameters ---------- - node: pyomo.core.base.expression.GeneralExpressionData + node: pyomo.core.base.expression.ExpressionData bnds_dict: ComponentMap feasibility_tol: float If the bounds computed on the body of a constraint violate the bounds of the constraint by more than @@ -945,12 +945,8 @@ def _prop_bnds_root_to_leaf_GeneralExpression(node, bnds_dict, feasibility_tol): ) _prop_bnds_root_to_leaf_map[numeric_expr.AbsExpression] = _prop_bnds_root_to_leaf_abs -_prop_bnds_root_to_leaf_map[GeneralExpressionData] = ( - _prop_bnds_root_to_leaf_GeneralExpression -) -_prop_bnds_root_to_leaf_map[ScalarExpression] = ( - _prop_bnds_root_to_leaf_GeneralExpression -) +_prop_bnds_root_to_leaf_map[ExpressionData] = _prop_bnds_root_to_leaf_NamedExpression +_prop_bnds_root_to_leaf_map[ScalarExpression] = _prop_bnds_root_to_leaf_NamedExpression def _check_and_reset_bounds(var, lb, ub): diff --git a/pyomo/contrib/latex_printer/latex_printer.py b/pyomo/contrib/latex_printer/latex_printer.py index 13f30f899e4..cf286472a66 100644 --- a/pyomo/contrib/latex_printer/latex_printer.py +++ b/pyomo/contrib/latex_printer/latex_printer.py @@ -34,7 +34,7 @@ from pyomo.core.expr.visitor import identify_components from pyomo.core.expr.base import ExpressionBase -from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, ExpressionData from pyomo.core.base.objective import ScalarObjective, ObjectiveData import pyomo.core.kernel as kernel from pyomo.core.expr.template_expr import ( @@ -399,7 +399,7 @@ def __init__(self): EqualityExpression: handle_equality_node, InequalityExpression: handle_inequality_node, RangedExpression: handle_ranged_inequality_node, - GeneralExpressionData: handle_named_expression_node, + ExpressionData: handle_named_expression_node, ScalarExpression: handle_named_expression_node, kernel.expression.expression: handle_named_expression_node, kernel.expression.noclone: handle_named_expression_node, diff --git a/pyomo/contrib/mcpp/pyomo_mcpp.py b/pyomo/contrib/mcpp/pyomo_mcpp.py index 1375ae61c50..0ef0237681b 100644 --- a/pyomo/contrib/mcpp/pyomo_mcpp.py +++ b/pyomo/contrib/mcpp/pyomo_mcpp.py @@ -20,7 +20,7 @@ from pyomo.common.fileutils import Library from pyomo.core import value, Expression from pyomo.core.base.block import SubclassOf -from pyomo.core.base.expression import ExpressionData +from pyomo.core.base.expression import NamedExpressionData from pyomo.core.expr.numvalue import nonpyomo_leaf_types from pyomo.core.expr.numeric_expr import ( AbsExpression, @@ -307,7 +307,9 @@ def exitNode(self, node, data): ans = self.mcpp.newConstant(node) elif not node.is_expression_type(): ans = self.register_num(node) - elif type(node) in SubclassOf(Expression) or isinstance(node, ExpressionData): + elif type(node) in SubclassOf(Expression) or isinstance( + node, NamedExpressionData + ): ans = data[0] else: raise RuntimeError("Unhandled expression type: %s" % (type(node))) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index bcc2a0e0e02..3d1347659db 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -97,7 +97,7 @@ Constraint, ConstraintData, ) -from pyomo.core.base.expression import Expression, ExpressionData +from pyomo.core.base.expression import Expression, NamedExpressionData, ExpressionData from pyomo.core.base.external import ExternalFunction from pyomo.core.base.logical_constraint import ( LogicalConstraint, @@ -170,10 +170,12 @@ relocated_module_attribute( f'_GeneralBooleanVarData', f'pyomo.core.base.BooleanVarData', version='6.7.2.dev0' ) +relocated_module_attribute( + f'_ExpressionData', f'pyomo.core.base.NamedExpressionData', version='6.7.2.dev0' +) for _cdata in ( 'ConstraintData', 'LogicalConstraintData', - 'ExpressionData', 'VarData', 'BooleanVarData', 'ObjectiveData', diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 380aab23cbe..50cf264c799 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -803,7 +803,7 @@ class ComponentData(_ComponentBase): # NOTE: This constructor is in-lined in the constructors for the following # classes: BooleanVarData, ConnectorData, ConstraintData, - # GeneralExpressionData, LogicalConstraintData, + # ExpressionData, LogicalConstraintData, # LogicalConstraintData, ObjectiveData, # ParamData,VarData, BooleanVarData, DisjunctionData, # ArcData, PortData, _LinearConstraintData, and diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index 31bb5f835df..10720366e28 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -44,15 +44,13 @@ class NamedExpressionData(numeric_expr.NumericValue): expr The expression owned by this data. """ - __slots__ = ('_args_',) + # Note: derived classes are expected to declare teh _args_ slot + __slots__ = () EXPRESSION_SYSTEM = EXPR.ExpressionType.NUMERIC PRECEDENCE = 0 ASSOCIATIVITY = EXPR.OperatorAssociativity.NON_ASSOCIATIVE - def __init__(self, expr=None): - self._args_ = (expr,) - def __call__(self, exception=True): """Compute the value of this expression.""" (arg,) = self._args_ @@ -199,6 +197,7 @@ class _ExpressionData(metaclass=RenamedClass): __renamed__new_class__ = NamedExpressionData __renamed__version__ = '6.7.2.dev0' + class _GeneralExpressionDataImpl(metaclass=RenamedClass): __renamed__new_class__ = NamedExpressionData __renamed__version__ = '6.7.2.dev0' @@ -219,7 +218,7 @@ class ExpressionData(NamedExpressionData, ComponentData): _component The expression component. """ - __slots__ = () + __slots__ = ('_args_',) def __init__(self, expr=None, component=None): self._args_ = (expr,) diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index fea356229fb..71cf5ba78f8 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -28,7 +28,7 @@ UnindexedComponent_set, rule_wrapper, ) -from pyomo.core.base.expression import ExpressionData, GeneralExpressionDataImpl +from pyomo.core.base.expression import NamedExpressionData from pyomo.core.base.set import Set from pyomo.core.base.initializer import ( Initializer, @@ -81,7 +81,7 @@ def O_rule(model, i, j): return rule_wrapper(rule, {None: ObjectiveList.End}) -class ObjectiveData(GeneralExpressionDataImpl, ActiveComponentData): +class ObjectiveData(NamedExpressionData, ActiveComponentData): """ This class defines the data for a single objective. @@ -104,10 +104,11 @@ class ObjectiveData(GeneralExpressionDataImpl, ActiveComponentData): _active A boolean that indicates whether this data is active """ - __slots__ = ("_sense", "_args_") + __slots__ = ("_args_", "_sense") def __init__(self, expr=None, sense=minimize, component=None): - GeneralExpressionDataImpl.__init__(self, expr) + # Inlining NamedExpressionData.__init__ + self._args_ = (expr,) # Inlining ActiveComponentData.__init__ self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index 50abaeedbba..49bb2e0280f 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -722,7 +722,7 @@ def args(self): @deprecated( 'The implicit recasting of a "not potentially variable" ' 'expression node to a potentially variable one is no ' - 'longer supported (this violates that immutability ' + 'longer supported (this violates the immutability ' 'promise for Pyomo5 expression trees).', version='6.4.3', ) diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index fae1d21a87e..16b7e0bd2e0 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -20,7 +20,7 @@ from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import ObjectiveData -from pyomo.core.base.expression import GeneralExpressionData +from pyomo.core.base.expression import ExpressionData class _TestComponentDictBase(object): @@ -360,7 +360,7 @@ def setUp(self): class TestExpressionDict(_TestComponentDictBase, unittest.TestCase): _ctype = ExpressionDict - _cdatatype = GeneralExpressionData + _cdatatype = ExpressionData def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_expression.py b/pyomo/core/tests/unit/test_expression.py index bf3ce0c2179..eb16f7c6142 100644 --- a/pyomo/core/tests/unit/test_expression.py +++ b/pyomo/core/tests/unit/test_expression.py @@ -29,7 +29,7 @@ value, sum_product, ) -from pyomo.core.base.expression import GeneralExpressionData +from pyomo.core.base.expression import ExpressionData from pyomo.core.expr.compare import compare_expressions, assertExpressionsEqual from pyomo.common.tee import capture_output @@ -515,10 +515,10 @@ def test_implicit_definition(self): model.E = Expression(model.idx) self.assertEqual(len(model.E), 3) expr = model.E[1] - self.assertIs(type(expr), GeneralExpressionData) + self.assertIs(type(expr), ExpressionData) model.E[1] = None self.assertIs(expr, model.E[1]) - self.assertIs(type(expr), GeneralExpressionData) + self.assertIs(type(expr), ExpressionData) self.assertIs(expr.expr, None) model.E[1] = 5 self.assertIs(expr, model.E[1]) @@ -537,7 +537,7 @@ def test_explicit_skip_definition(self): model.E[1] = None expr = model.E[1] - self.assertIs(type(expr), GeneralExpressionData) + self.assertIs(type(expr), ExpressionData) self.assertIs(expr.expr, None) model.E[1] = 5 self.assertIs(expr, model.E[1]) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index 32f2fa328cf..94913bcbc02 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -20,7 +20,7 @@ from pyomo.core.base.var import VarData from pyomo.core.base.constraint import GeneralConstraintData from pyomo.core.base.objective import ObjectiveData -from pyomo.core.base.expression import GeneralExpressionData +from pyomo.core.base.expression import ExpressionData class _TestComponentListBase(object): @@ -377,7 +377,7 @@ def setUp(self): class TestExpressionList(_TestComponentListBase, unittest.TestCase): _ctype = XExpressionList - _cdatatype = GeneralExpressionData + _cdatatype = ExpressionData def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/dae/integral.py b/pyomo/dae/integral.py index f767e31f18c..8c9512d98dd 100644 --- a/pyomo/dae/integral.py +++ b/pyomo/dae/integral.py @@ -14,7 +14,7 @@ from pyomo.core.base.indexed_component import rule_wrapper from pyomo.core.base.expression import ( Expression, - GeneralExpressionData, + ExpressionData, ScalarExpression, IndexedExpression, ) @@ -151,7 +151,7 @@ class ScalarIntegral(ScalarExpression, Integral): """ def __init__(self, *args, **kwds): - GeneralExpressionData.__init__(self, None, component=self) + ExpressionData.__init__(self, None, component=self) Integral.__init__(self, *args, **kwds) def clear(self): diff --git a/pyomo/gdp/tests/test_util.py b/pyomo/gdp/tests/test_util.py index 8ea72af37da..fa8e953f9f7 100644 --- a/pyomo/gdp/tests/test_util.py +++ b/pyomo/gdp/tests/test_util.py @@ -13,7 +13,7 @@ from pyomo.core import ConcreteModel, Var, Expression, Block, RangeSet, Any import pyomo.core.expr as EXPR -from pyomo.core.base.expression import ExpressionData +from pyomo.core.base.expression import NamedExpressionData from pyomo.gdp.util import ( clone_without_expression_components, is_child_of, @@ -40,7 +40,7 @@ def test_clone_without_expression_components(self): test = clone_without_expression_components(base, {}) self.assertIsNot(base, test) self.assertEqual(base(), test()) - self.assertIsInstance(base, ExpressionData) + self.assertIsInstance(base, NamedExpressionData) self.assertIsInstance(test, EXPR.SumExpression) test = clone_without_expression_components(base, {id(m.x): m.y}) self.assertEqual(3**2 + 3 - 1, test()) @@ -51,7 +51,7 @@ def test_clone_without_expression_components(self): self.assertEqual(base(), test()) self.assertIsInstance(base, EXPR.SumExpression) self.assertIsInstance(test, EXPR.SumExpression) - self.assertIsInstance(base.arg(0), ExpressionData) + self.assertIsInstance(base.arg(0), NamedExpressionData) self.assertIsInstance(test.arg(0), EXPR.SumExpression) test = clone_without_expression_components(base, {id(m.x): m.y}) self.assertEqual(3**2 + 3 - 1 + 3, test()) diff --git a/pyomo/repn/plugins/ampl/ampl_.py b/pyomo/repn/plugins/ampl/ampl_.py index 1cff45b30c1..cc99e9cfdae 100644 --- a/pyomo/repn/plugins/ampl/ampl_.py +++ b/pyomo/repn/plugins/ampl/ampl_.py @@ -33,7 +33,7 @@ from pyomo.core.base import ( SymbolMap, NameLabeler, - ExpressionData, + NamedExpressionData, SortComponents, var, param, @@ -724,7 +724,7 @@ def _print_nonlinear_terms_NL(self, exp): self._print_nonlinear_terms_NL(exp.arg(0)) self._print_nonlinear_terms_NL(exp.arg(1)) - elif isinstance(exp, (ExpressionData, IIdentityExpression)): + elif isinstance(exp, (NamedExpressionData, IIdentityExpression)): self._print_nonlinear_terms_NL(exp.expr) else: diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 3c3c8539294..8cc73b5e3fe 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -70,7 +70,7 @@ ) from pyomo.core.base.component import ActiveComponent from pyomo.core.base.constraint import ConstraintData -from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData +from pyomo.core.base.expression import ScalarExpression, ExpressionData from pyomo.core.base.objective import ScalarObjective, ObjectiveData from pyomo.core.base.suffix import SuffixFinder from pyomo.core.base.var import VarData diff --git a/pyomo/repn/standard_repn.py b/pyomo/repn/standard_repn.py index a23ebf6bb4f..b767ab727af 100644 --- a/pyomo/repn/standard_repn.py +++ b/pyomo/repn/standard_repn.py @@ -20,8 +20,12 @@ import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import NumericConstant from pyomo.core.base.objective import ObjectiveData, ScalarObjective -from pyomo.core.base import ExpressionData, Expression -from pyomo.core.base.expression import ScalarExpression, GeneralExpressionData +from pyomo.core.base import Expression +from pyomo.core.base.expression import ( + ScalarExpression, + NamedExpressionData, + ExpressionData, +) from pyomo.core.base.var import ScalarVar, Var, VarData, value from pyomo.core.base.param import ScalarParam, ParamData from pyomo.core.kernel.expression import expression, noclone @@ -1148,11 +1152,11 @@ def _collect_external_fn(exp, multiplier, idMap, compute_values, verbose, quadra Var: _collect_var, variable: _collect_var, IVariable: _collect_var, - GeneralExpressionData: _collect_identity, + ExpressionData: _collect_identity, ScalarExpression: _collect_identity, expression: _collect_identity, noclone: _collect_identity, - ExpressionData: _collect_identity, + NamedExpressionData: _collect_identity, Expression: _collect_identity, ObjectiveData: _collect_identity, ScalarObjective: _collect_identity, @@ -1547,11 +1551,11 @@ def _linear_collect_pow(exp, multiplier, idMap, compute_values, verbose, coef): Var : _linear_collect_var, variable : _linear_collect_var, IVariable : _linear_collect_var, - GeneralExpressionData : _linear_collect_identity, + ExpressionData : _linear_collect_identity, ScalarExpression : _linear_collect_identity, expression : _linear_collect_identity, noclone : _linear_collect_identity, - ExpressionData : _linear_collect_identity, + NamedExpressionData : _linear_collect_identity, Expression : _linear_collect_identity, ObjectiveData : _linear_collect_identity, ScalarObjective : _linear_collect_identity, diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 7351ea51c58..9a8713c6965 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -40,7 +40,7 @@ SortComponents, ) from pyomo.core.base.component import ActiveComponent -from pyomo.core.base.expression import ExpressionData +from pyomo.core.base.expression import NamedExpressionData from pyomo.core.expr.numvalue import is_fixed, value import pyomo.core.expr as EXPR import pyomo.core.kernel as kernel @@ -55,7 +55,7 @@ EXPR.NPV_SumExpression, } _named_subexpression_types = ( - ExpressionData, + NamedExpressionData, kernel.expression.expression, kernel.objective.objective, ) From 47bb04f00f591f175a7a65e656197a41fc0eae50 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 23:29:02 -0600 Subject: [PATCH 1022/3044] Add missing method from GeneralLogicalConstraintData merge --- pyomo/core/base/logical_constraint.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index 1daa5f83e90..9584078307d 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -77,6 +77,12 @@ def __init__(self, expr=None, component=None): if expr is not None: self.set_value(expr) + def __call__(self, exception=True): + """Compute the value of the body of this logical constraint.""" + if self.body is None: + return None + return self.body(exception=exception) + # # Abstract Interface # From d26a83ff243c7412bcb978f0f571b53ebc737f6c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 21 Mar 2024 10:47:13 -0600 Subject: [PATCH 1023/3044] NFC: fix typo --- pyomo/core/base/expression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index 10720366e28..5638e48ea8b 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -44,7 +44,7 @@ class NamedExpressionData(numeric_expr.NumericValue): expr The expression owned by this data. """ - # Note: derived classes are expected to declare teh _args_ slot + # Note: derived classes are expected to declare the _args_ slot __slots__ = () EXPRESSION_SYSTEM = EXPR.ExpressionType.NUMERIC From db7b6b22ad1c2f3659fce2cb0cd96c69727ef6f3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 21 Mar 2024 10:47:52 -0600 Subject: [PATCH 1024/3044] Update ComponentData references in APPSI --- pyomo/contrib/appsi/cmodel/src/expression.hpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/appsi/cmodel/src/expression.hpp b/pyomo/contrib/appsi/cmodel/src/expression.hpp index ad1234b3863..e91ca0af3b3 100644 --- a/pyomo/contrib/appsi/cmodel/src/expression.hpp +++ b/pyomo/contrib/appsi/cmodel/src/expression.hpp @@ -680,10 +680,10 @@ class PyomoExprTypes { expr_type_map[np_float32] = py_float; expr_type_map[np_float64] = py_float; expr_type_map[ScalarVar] = var; - expr_type_map[_VarData] = var; + expr_type_map[VarData] = var; expr_type_map[AutoLinkedBinaryVar] = var; expr_type_map[ScalarParam] = param; - expr_type_map[_ParamData] = param; + expr_type_map[ParamData] = param; expr_type_map[MonomialTermExpression] = product; expr_type_map[ProductExpression] = product; expr_type_map[NPV_ProductExpression] = product; @@ -700,7 +700,7 @@ class PyomoExprTypes { expr_type_map[UnaryFunctionExpression] = unary_func; expr_type_map[NPV_UnaryFunctionExpression] = unary_func; expr_type_map[LinearExpression] = linear; - expr_type_map[_ExpressionData] = named_expr; + expr_type_map[ExpressionData] = named_expr; expr_type_map[ScalarExpression] = named_expr; expr_type_map[Integral] = named_expr; expr_type_map[ScalarIntegral] = named_expr; @@ -728,12 +728,12 @@ class PyomoExprTypes { py::type np_float64 = np.attr("float64"); py::object ScalarParam = py::module_::import("pyomo.core.base.param").attr("ScalarParam"); - py::object _ParamData = - py::module_::import("pyomo.core.base.param").attr("_ParamData"); + py::object ParamData = + py::module_::import("pyomo.core.base.param").attr("ParamData"); py::object ScalarVar = py::module_::import("pyomo.core.base.var").attr("ScalarVar"); - py::object _VarData = - py::module_::import("pyomo.core.base.var").attr("_VarData"); + py::object VarData = + py::module_::import("pyomo.core.base.var").attr("VarData"); py::object AutoLinkedBinaryVar = py::module_::import("pyomo.gdp.disjunct").attr("AutoLinkedBinaryVar"); py::object numeric_expr = py::module_::import("pyomo.core.expr.numeric_expr"); @@ -765,8 +765,8 @@ class PyomoExprTypes { py::object NumericConstant = py::module_::import("pyomo.core.expr.numvalue").attr("NumericConstant"); py::object expr_module = py::module_::import("pyomo.core.base.expression"); - py::object _ExpressionData = - expr_module.attr("_ExpressionData"); + py::object ExpressionData = + expr_module.attr("ExpressionData"); py::object ScalarExpression = expr_module.attr("ScalarExpression"); py::object ScalarIntegral = py::module_::import("pyomo.dae.integral").attr("ScalarIntegral"); From 5b48eb28989c29f23324d6d3e6cdf1ca452346fc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 21 Mar 2024 12:18:53 -0600 Subject: [PATCH 1025/3044] Merge GeneralConstraintData into ConstraintData --- pyomo/core/base/constraint.py | 244 ++++++++++------------------------ 1 file changed, 70 insertions(+), 174 deletions(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 3455d2dde3c..08c97d7c8ae 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -125,17 +125,13 @@ def C_rule(model, i, j): return rule_wrapper(rule, result_map, map_types=map_types) -# -# This class is a pure interface -# - - -class ConstraintData(ActiveComponentData): +class ConstraintData(ConstraintData): """ - This class defines the data for a single constraint. + This class defines the data for a single general constraint. Constructor arguments: component The Constraint object that owns this data. + expr The Pyomo expression stored in this constraint. Public class attributes: active A boolean that is true if this constraint is @@ -155,164 +151,12 @@ class ConstraintData(ActiveComponentData): _active A boolean that indicates whether this data is active """ - __slots__ = () + __slots__ = ('_body', '_lower', '_upper', '_expr') # Set to true when a constraint class stores its expression # in linear canonical form _linear_canonical_form = False - def __init__(self, component=None): - # - # These lines represent in-lining of the - # following constructors: - # - ConstraintData, - # - ActiveComponentData - # - ComponentData - self._component = weakref_ref(component) if (component is not None) else None - self._index = NOTSET - self._active = True - - # - # Interface - # - - def __call__(self, exception=True): - """Compute the value of the body of this constraint.""" - return value(self.body, exception=exception) - - def has_lb(self): - """Returns :const:`False` when the lower bound is - :const:`None` or negative infinity""" - return self.lb is not None - - def has_ub(self): - """Returns :const:`False` when the upper bound is - :const:`None` or positive infinity""" - return self.ub is not None - - def lslack(self): - """ - Returns the value of f(x)-L for constraints of the form: - L <= f(x) (<= U) - (U >=) f(x) >= L - """ - lb = self.lb - if lb is None: - return _inf - else: - return value(self.body) - lb - - def uslack(self): - """ - Returns the value of U-f(x) for constraints of the form: - (L <=) f(x) <= U - U >= f(x) (>= L) - """ - ub = self.ub - if ub is None: - return _inf - else: - return ub - value(self.body) - - def slack(self): - """ - Returns the smaller of lslack and uslack values - """ - lb = self.lb - ub = self.ub - body = value(self.body) - if lb is None: - return ub - body - elif ub is None: - return body - lb - return min(ub - body, body - lb) - - # - # Abstract Interface - # - - @property - def body(self): - """Access the body of a constraint expression.""" - raise NotImplementedError - - @property - def lower(self): - """Access the lower bound of a constraint expression.""" - raise NotImplementedError - - @property - def upper(self): - """Access the upper bound of a constraint expression.""" - raise NotImplementedError - - @property - def lb(self): - """Access the value of the lower bound of a constraint expression.""" - raise NotImplementedError - - @property - def ub(self): - """Access the value of the upper bound of a constraint expression.""" - raise NotImplementedError - - @property - def equality(self): - """A boolean indicating whether this is an equality constraint.""" - raise NotImplementedError - - @property - def strict_lower(self): - """True if this constraint has a strict lower bound.""" - raise NotImplementedError - - @property - def strict_upper(self): - """True if this constraint has a strict upper bound.""" - raise NotImplementedError - - def set_value(self, expr): - """Set the expression on this constraint.""" - raise NotImplementedError - - def get_value(self): - """Get the expression on this constraint.""" - raise NotImplementedError - - -class _ConstraintData(metaclass=RenamedClass): - __renamed__new_class__ = ConstraintData - __renamed__version__ = '6.7.2.dev0' - - -class GeneralConstraintData(ConstraintData): - """ - This class defines the data for a single general constraint. - - Constructor arguments: - component The Constraint object that owns this data. - expr The Pyomo expression stored in this constraint. - - Public class attributes: - active A boolean that is true if this constraint is - active in the model. - body The Pyomo expression for this constraint - lower The Pyomo expression for the lower bound - upper The Pyomo expression for the upper bound - equality A boolean that indicates whether this is an - equality constraint - strict_lower A boolean that indicates whether this - constraint uses a strict lower bound - strict_upper A boolean that indicates whether this - constraint uses a strict upper bound - - Private class attributes: - _component The objective component. - _active A boolean that indicates whether this data is active - """ - - __slots__ = ('_body', '_lower', '_upper', '_expr') - def __init__(self, expr=None, component=None): # # These lines represent in-lining of the @@ -330,9 +174,9 @@ def __init__(self, expr=None, component=None): if expr is not None: self.set_value(expr) - # - # Abstract Interface - # + def __call__(self, exception=True): + """Compute the value of the body of this constraint.""" + return value(self.body, exception=exception) @property def body(self): @@ -456,6 +300,16 @@ def strict_upper(self): """True if this constraint has a strict upper bound.""" return False + def has_lb(self): + """Returns :const:`False` when the lower bound is + :const:`None` or negative infinity""" + return self.lb is not None + + def has_ub(self): + """Returns :const:`False` when the upper bound is + :const:`None` or positive infinity""" + return self.ub is not None + @property def expr(self): """Return the expression associated with this constraint.""" @@ -683,9 +537,51 @@ def set_value(self, expr): "upper bound (%s)." % (self.name, self._upper) ) + def lslack(self): + """ + Returns the value of f(x)-L for constraints of the form: + L <= f(x) (<= U) + (U >=) f(x) >= L + """ + lb = self.lb + if lb is None: + return _inf + else: + return value(self.body) - lb + + def uslack(self): + """ + Returns the value of U-f(x) for constraints of the form: + (L <=) f(x) <= U + U >= f(x) (>= L) + """ + ub = self.ub + if ub is None: + return _inf + else: + return ub - value(self.body) + + def slack(self): + """ + Returns the smaller of lslack and uslack values + """ + lb = self.lb + ub = self.ub + body = value(self.body) + if lb is None: + return ub - body + elif ub is None: + return body - lb + return min(ub - body, body - lb) + + +class _ConstraintData(metaclass=RenamedClass): + __renamed__new_class__ = ConstraintData + __renamed__version__ = '6.7.2.dev0' + class _GeneralConstraintData(metaclass=RenamedClass): - __renamed__new_class__ = GeneralConstraintData + __renamed__new_class__ = ConstraintData __renamed__version__ = '6.7.2.dev0' @@ -731,7 +627,7 @@ class Constraint(ActiveIndexedComponent): The class type for the derived subclass """ - _ComponentDataClass = GeneralConstraintData + _ComponentDataClass = ConstraintData class Infeasible(object): pass @@ -889,14 +785,14 @@ def display(self, prefix="", ostream=None): ) -class ScalarConstraint(GeneralConstraintData, Constraint): +class ScalarConstraint(ConstraintData, Constraint): """ ScalarConstraint is the implementation representing a single, non-indexed constraint. """ def __init__(self, *args, **kwds): - GeneralConstraintData.__init__(self, component=self, expr=None) + ConstraintData.__init__(self, component=self, expr=None) Constraint.__init__(self, *args, **kwds) self._index = UnindexedComponent_index @@ -920,7 +816,7 @@ def body(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return GeneralConstraintData.body.fget(self) + return ConstraintData.body.fget(self) @property def lower(self): @@ -932,7 +828,7 @@ def lower(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return GeneralConstraintData.lower.fget(self) + return ConstraintData.lower.fget(self) @property def upper(self): @@ -944,7 +840,7 @@ def upper(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return GeneralConstraintData.upper.fget(self) + return ConstraintData.upper.fget(self) @property def equality(self): @@ -956,7 +852,7 @@ def equality(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return GeneralConstraintData.equality.fget(self) + return ConstraintData.equality.fget(self) @property def strict_lower(self): @@ -968,7 +864,7 @@ def strict_lower(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return GeneralConstraintData.strict_lower.fget(self) + return ConstraintData.strict_lower.fget(self) @property def strict_upper(self): @@ -980,7 +876,7 @@ def strict_upper(self): "an expression. There is currently " "nothing to access." % (self.name) ) - return GeneralConstraintData.strict_upper.fget(self) + return ConstraintData.strict_upper.fget(self) def clear(self): self._data = {} @@ -1045,7 +941,7 @@ def add(self, index, expr): return self.__setitem__(index, expr) @overload - def __getitem__(self, index) -> GeneralConstraintData: ... + def __getitem__(self, index) -> ConstraintData: ... __getitem__ = IndexedComponent.__getitem__ # type: ignore From 97ad3c0945ca1426729a107320a06c21d54653c0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 21 Mar 2024 15:15:03 -0600 Subject: [PATCH 1026/3044] Update references from GeneralConstraintData to ConstraintData --- pyomo/contrib/appsi/base.py | 48 +++++++++---------- pyomo/contrib/appsi/fbbt.py | 6 +-- pyomo/contrib/appsi/solvers/cbc.py | 6 +-- pyomo/contrib/appsi/solvers/cplex.py | 10 ++-- pyomo/contrib/appsi/solvers/gurobi.py | 16 +++---- pyomo/contrib/appsi/solvers/highs.py | 6 +-- pyomo/contrib/appsi/solvers/ipopt.py | 8 ++-- pyomo/contrib/appsi/solvers/wntr.py | 6 +-- pyomo/contrib/appsi/writers/lp_writer.py | 6 +-- pyomo/contrib/appsi/writers/nl_writer.py | 6 +-- pyomo/contrib/solver/base.py | 10 ++-- pyomo/contrib/solver/gurobi.py | 16 +++---- pyomo/contrib/solver/persistent.py | 12 ++--- pyomo/contrib/solver/solution.py | 14 +++--- .../contrib/solver/tests/unit/test_results.py | 6 +-- pyomo/core/tests/unit/test_con.py | 4 +- pyomo/core/tests/unit/test_dict_objects.py | 4 +- pyomo/core/tests/unit/test_list_objects.py | 4 +- pyomo/gdp/tests/common_tests.py | 6 +-- pyomo/gdp/tests/test_bigm.py | 4 +- pyomo/gdp/tests/test_hull.py | 6 +-- .../plugins/solvers/gurobi_persistent.py | 10 ++-- 22 files changed, 105 insertions(+), 109 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 9d00a56e8b9..6655ec26524 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -21,7 +21,7 @@ Tuple, MutableMapping, ) -from pyomo.core.base.constraint import GeneralConstraintData, Constraint +from pyomo.core.base.constraint import ConstraintData, Constraint from pyomo.core.base.sos import SOSConstraintData, SOSConstraint from pyomo.core.base.var import VarData, Var from pyomo.core.base.param import ParamData, Param @@ -214,8 +214,8 @@ def get_primals( pass def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Returns a dictionary mapping constraint to dual value. @@ -233,8 +233,8 @@ def get_duals( raise NotImplementedError(f'{type(self)} does not support the get_duals method') def get_slacks( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Returns a dictionary mapping constraint to slack. @@ -317,8 +317,8 @@ def get_primals( return primals def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: if self._duals is None: raise RuntimeError( 'Solution loader does not currently have valid duals. Please ' @@ -334,8 +334,8 @@ def get_duals( return duals def get_slacks( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: if self._slacks is None: raise RuntimeError( 'Solution loader does not currently have valid slacks. Please ' @@ -727,8 +727,8 @@ def get_primals( pass def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Declare sign convention in docstring here. @@ -748,8 +748,8 @@ def get_duals( ) def get_slacks( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Parameters ---------- @@ -803,7 +803,7 @@ def add_params(self, params: List[ParamData]): pass @abc.abstractmethod - def add_constraints(self, cons: List[GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): pass @abc.abstractmethod @@ -819,7 +819,7 @@ def remove_params(self, params: List[ParamData]): pass @abc.abstractmethod - def remove_constraints(self, cons: List[GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): pass @abc.abstractmethod @@ -853,14 +853,14 @@ def get_primals(self, vars_to_load=None): return self._solver.get_primals(vars_to_load=vars_to_load) def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: self._assert_solution_still_valid() return self._solver.get_duals(cons_to_load=cons_to_load) def get_slacks( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: self._assert_solution_still_valid() return self._solver.get_slacks(cons_to_load=cons_to_load) @@ -980,7 +980,7 @@ def add_params(self, params: List[ParamData]): self._add_params(params) @abc.abstractmethod - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): pass def _check_for_new_vars(self, variables: List[VarData]): @@ -1000,7 +1000,7 @@ def _check_to_remove_vars(self, variables: List[VarData]): vars_to_remove[v_id] = v self.remove_variables(list(vars_to_remove.values())) - def add_constraints(self, cons: List[GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): all_fixed_vars = dict() for con in cons: if con in self._named_expressions: @@ -1128,10 +1128,10 @@ def add_block(self, block): self.set_objective(obj) @abc.abstractmethod - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): pass - def remove_constraints(self, cons: List[GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): self._remove_constraints(cons) for con in cons: if con not in self._named_expressions: @@ -1330,7 +1330,7 @@ def update(self, timer: HierarchicalTimer = None): for c in self._vars_referenced_by_con.keys(): if c not in current_cons_dict and c not in current_sos_dict: if (c.ctype is Constraint) or ( - c.ctype is None and isinstance(c, GeneralConstraintData) + c.ctype is None and isinstance(c, ConstraintData) ): old_cons.append(c) else: diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 4b0d6d4876c..8e0c74b00e9 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -20,7 +20,7 @@ from typing import List, Optional from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.objective import ObjectiveData, minimize, maximize from pyomo.core.base.block import BlockData @@ -154,7 +154,7 @@ def _add_params(self, params: List[ParamData]): cp = cparams[ndx] cp.name = self._symbol_map.getSymbol(p, self._param_labeler) - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): cmodel.process_fbbt_constraints( self._cmodel, self._pyomo_expr_types, @@ -175,7 +175,7 @@ def _add_sos_constraints(self, cons: List[SOSConstraintData]): 'IntervalTightener does not support SOS constraints' ) - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): if self._symbolic_solver_labels: for c in cons: self._symbol_map.removeSymbol(c) diff --git a/pyomo/contrib/appsi/solvers/cbc.py b/pyomo/contrib/appsi/solvers/cbc.py index dffd479a5c7..08833e747e2 100644 --- a/pyomo/contrib/appsi/solvers/cbc.py +++ b/pyomo/contrib/appsi/solvers/cbc.py @@ -27,7 +27,7 @@ from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData from pyomo.core.base.objective import ObjectiveData @@ -170,7 +170,7 @@ def add_variables(self, variables: List[VarData]): def add_params(self, params: List[ParamData]): self._writer.add_params(params) - def add_constraints(self, cons: List[GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): self._writer.add_constraints(cons) def add_block(self, block: BlockData): @@ -182,7 +182,7 @@ def remove_variables(self, variables: List[VarData]): def remove_params(self, params: List[ParamData]): self._writer.remove_params(params) - def remove_constraints(self, cons: List[GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): self._writer.remove_constraints(cons) def remove_block(self, block: BlockData): diff --git a/pyomo/contrib/appsi/solvers/cplex.py b/pyomo/contrib/appsi/solvers/cplex.py index 22c11bdfbe8..10de981ce7d 100644 --- a/pyomo/contrib/appsi/solvers/cplex.py +++ b/pyomo/contrib/appsi/solvers/cplex.py @@ -23,7 +23,7 @@ from pyomo.common.collections import ComponentMap from typing import Optional, Sequence, NoReturn, List, Mapping, Dict from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData from pyomo.core.base.objective import ObjectiveData @@ -185,7 +185,7 @@ def add_variables(self, variables: List[VarData]): def add_params(self, params: List[ParamData]): self._writer.add_params(params) - def add_constraints(self, cons: List[GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): self._writer.add_constraints(cons) def add_block(self, block: BlockData): @@ -197,7 +197,7 @@ def remove_variables(self, variables: List[VarData]): def remove_params(self, params: List[ParamData]): self._writer.remove_params(params) - def remove_constraints(self, cons: List[GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): self._writer.remove_constraints(cons) def remove_block(self, block: BlockData): @@ -389,8 +389,8 @@ def get_primals( return res def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: if ( self._cplex_model.solution.get_solution_type() == self._cplex_model.solution.type.none diff --git a/pyomo/contrib/appsi/solvers/gurobi.py b/pyomo/contrib/appsi/solvers/gurobi.py index e2ecd9b69e7..2719ecc2a00 100644 --- a/pyomo/contrib/appsi/solvers/gurobi.py +++ b/pyomo/contrib/appsi/solvers/gurobi.py @@ -24,7 +24,7 @@ from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.core.base.var import Var, VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types @@ -579,7 +579,7 @@ def _get_expr_from_pyomo_expr(self, expr): mutable_quadratic_coefficients, ) - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): for con in cons: conname = self._symbol_map.getSymbol(con, self._labeler) ( @@ -735,7 +735,7 @@ def _add_sos_constraints(self, cons: List[SOSConstraintData]): self._constraints_added_since_update.update(cons) self._needs_updated = True - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): for con in cons: if con in self._constraints_added_since_update: self._update_gurobi_model() @@ -1195,7 +1195,7 @@ def set_linear_constraint_attr(self, con, attr, val): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be modified. attr: str @@ -1272,7 +1272,7 @@ def get_linear_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1304,7 +1304,7 @@ def get_quadratic_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1425,7 +1425,7 @@ def cbCut(self, con): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The cut to add """ if not con.active: @@ -1510,7 +1510,7 @@ def cbLazy(self, con): """ Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The lazy constraint to add """ if not con.active: diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index c3083ac78d3..6410700c569 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -21,7 +21,7 @@ from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant @@ -376,7 +376,7 @@ def set_instance(self, model): if self._objective is None: self.set_objective(None) - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() @@ -462,7 +462,7 @@ def _add_sos_constraints(self, cons: List[SOSConstraintData]): 'Highs interface does not support SOS constraints' ) - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): self._sol = None if self._last_results_object is not None: self._last_results_object.solution_loader.invalidate() diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 4144fbbecd9..76cd204e36d 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -29,7 +29,7 @@ from pyomo.core.expr.visitor import replace_expressions from typing import Optional, Sequence, NoReturn, List, Mapping from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.block import BlockData from pyomo.core.base.param import ParamData from pyomo.core.base.objective import ObjectiveData @@ -234,7 +234,7 @@ def add_variables(self, variables: List[VarData]): def add_params(self, params: List[ParamData]): self._writer.add_params(params) - def add_constraints(self, cons: List[GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): self._writer.add_constraints(cons) def add_block(self, block: BlockData): @@ -246,7 +246,7 @@ def remove_variables(self, variables: List[VarData]): def remove_params(self, params: List[ParamData]): self._writer.remove_params(params) - def remove_constraints(self, cons: List[GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): self._writer.remove_constraints(cons) def remove_block(self, block: BlockData): @@ -534,7 +534,7 @@ def get_primals( res[v] = self._primal_sol[v] return res - def get_duals(self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None): + def get_duals(self, cons_to_load: Optional[Sequence[ConstraintData]] = None): if ( self._last_results_object is None or self._last_results_object.termination_condition diff --git a/pyomo/contrib/appsi/solvers/wntr.py b/pyomo/contrib/appsi/solvers/wntr.py index 62c4b0ed358..0a66cc640e5 100644 --- a/pyomo/contrib/appsi/solvers/wntr.py +++ b/pyomo/contrib/appsi/solvers/wntr.py @@ -42,7 +42,7 @@ from pyomo.core.base.block import BlockData from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.common.timing import HierarchicalTimer from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.common.dependencies import attempt_import @@ -278,7 +278,7 @@ def _add_params(self, params: List[ParamData]): setattr(self._solver_model, pname, wntr_p) self._pyomo_param_to_solver_param_map[id(p)] = wntr_p - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): aml = wntr.sim.aml.aml for con in cons: if not con.equality: @@ -294,7 +294,7 @@ def _add_constraints(self, cons: List[GeneralConstraintData]): self._pyomo_con_to_solver_con_map[con] = wntr_con self._needs_updated = True - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): for con in cons: solver_con = self._pyomo_con_to_solver_con_map[con] delattr(self._solver_model, solver_con.name) diff --git a/pyomo/contrib/appsi/writers/lp_writer.py b/pyomo/contrib/appsi/writers/lp_writer.py index 3a6193bd314..788dfde7892 100644 --- a/pyomo/contrib/appsi/writers/lp_writer.py +++ b/pyomo/contrib/appsi/writers/lp_writer.py @@ -12,7 +12,7 @@ from typing import List from pyomo.core.base.param import ParamData from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.block import BlockData @@ -99,14 +99,14 @@ def _add_params(self, params: List[ParamData]): cp.value = p.value self._pyomo_param_to_solver_param_map[id(p)] = cp - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): cmodel.process_lp_constraints(cons, self) def _add_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('LP writer does not yet support SOS constraints') - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): for c in cons: cc = self._pyomo_con_to_solver_con_map.pop(c) self._writer.remove_constraint(cc) diff --git a/pyomo/contrib/appsi/writers/nl_writer.py b/pyomo/contrib/appsi/writers/nl_writer.py index 754bd179497..27cdca004cb 100644 --- a/pyomo/contrib/appsi/writers/nl_writer.py +++ b/pyomo/contrib/appsi/writers/nl_writer.py @@ -12,7 +12,7 @@ from typing import List from pyomo.core.base.param import ParamData from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.block import BlockData @@ -111,7 +111,7 @@ def _add_params(self, params: List[ParamData]): cp = cparams[ndx] cp.name = self._symbol_map.getSymbol(p, self._param_labeler) - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): cmodel.process_nl_constraints( self._writer, self._expr_types, @@ -130,7 +130,7 @@ def _add_sos_constraints(self, cons: List[SOSConstraintData]): if len(cons) != 0: raise NotImplementedError('NL writer does not support SOS constraints') - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): if self.config.symbolic_solver_labels: for c in cons: self._symbol_map.removeSymbol(c) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index c53f917bc2a..45b5cca0179 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -14,7 +14,7 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple import os -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData from pyomo.core.base.block import BlockData @@ -230,8 +230,8 @@ def _get_primals( ) def _get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Declare sign convention in docstring here. @@ -292,7 +292,7 @@ def add_parameters(self, params: List[ParamData]): """ @abc.abstractmethod - def add_constraints(self, cons: List[GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): """ Add constraints to the model """ @@ -316,7 +316,7 @@ def remove_parameters(self, params: List[ParamData]): """ @abc.abstractmethod - def remove_constraints(self, cons: List[GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): """ Remove constraints from the model """ diff --git a/pyomo/contrib/solver/gurobi.py b/pyomo/contrib/solver/gurobi.py index ff4e93f7635..10d8120c8b3 100644 --- a/pyomo/contrib/solver/gurobi.py +++ b/pyomo/contrib/solver/gurobi.py @@ -23,7 +23,7 @@ from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.sos import SOSConstraintData from pyomo.core.base.param import ParamData from pyomo.core.expr.numvalue import value, is_constant, is_fixed, native_numeric_types @@ -555,7 +555,7 @@ def _get_expr_from_pyomo_expr(self, expr): mutable_quadratic_coefficients, ) - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): for con in cons: conname = self._symbol_map.getSymbol(con, self._labeler) ( @@ -711,7 +711,7 @@ def _add_sos_constraints(self, cons: List[SOSConstraintData]): self._constraints_added_since_update.update(cons) self._needs_updated = True - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): for con in cons: if con in self._constraints_added_since_update: self._update_gurobi_model() @@ -1125,7 +1125,7 @@ def set_linear_constraint_attr(self, con, attr, val): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be modified. attr: str @@ -1202,7 +1202,7 @@ def get_linear_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1234,7 +1234,7 @@ def get_quadratic_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -1355,7 +1355,7 @@ def cbCut(self, con): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The cut to add """ if not con.active: @@ -1440,7 +1440,7 @@ def cbLazy(self, con): """ Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The lazy constraint to add """ if not con.active: diff --git a/pyomo/contrib/solver/persistent.py b/pyomo/contrib/solver/persistent.py index 81d0df1334f..71322b7043e 100644 --- a/pyomo/contrib/solver/persistent.py +++ b/pyomo/contrib/solver/persistent.py @@ -12,7 +12,7 @@ import abc from typing import List -from pyomo.core.base.constraint import GeneralConstraintData, Constraint +from pyomo.core.base.constraint import ConstraintData, Constraint from pyomo.core.base.sos import SOSConstraintData, SOSConstraint from pyomo.core.base.var import VarData from pyomo.core.base.param import ParamData, Param @@ -84,7 +84,7 @@ def add_parameters(self, params: List[ParamData]): self._add_parameters(params) @abc.abstractmethod - def _add_constraints(self, cons: List[GeneralConstraintData]): + def _add_constraints(self, cons: List[ConstraintData]): pass def _check_for_new_vars(self, variables: List[VarData]): @@ -104,7 +104,7 @@ def _check_to_remove_vars(self, variables: List[VarData]): vars_to_remove[v_id] = v self.remove_variables(list(vars_to_remove.values())) - def add_constraints(self, cons: List[GeneralConstraintData]): + def add_constraints(self, cons: List[ConstraintData]): all_fixed_vars = {} for con in cons: if con in self._named_expressions: @@ -209,10 +209,10 @@ def add_block(self, block): self.set_objective(obj) @abc.abstractmethod - def _remove_constraints(self, cons: List[GeneralConstraintData]): + def _remove_constraints(self, cons: List[ConstraintData]): pass - def remove_constraints(self, cons: List[GeneralConstraintData]): + def remove_constraints(self, cons: List[ConstraintData]): self._remove_constraints(cons) for con in cons: if con not in self._named_expressions: @@ -384,7 +384,7 @@ def update(self, timer: HierarchicalTimer = None): for c in self._vars_referenced_by_con.keys(): if c not in current_cons_dict and c not in current_sos_dict: if (c.ctype is Constraint) or ( - c.ctype is None and isinstance(c, GeneralConstraintData) + c.ctype is None and isinstance(c, ConstraintData) ): old_cons.append(c) else: diff --git a/pyomo/contrib/solver/solution.py b/pyomo/contrib/solver/solution.py index e089e621f1f..a3e66475982 100644 --- a/pyomo/contrib/solver/solution.py +++ b/pyomo/contrib/solver/solution.py @@ -12,7 +12,7 @@ import abc from typing import Sequence, Dict, Optional, Mapping, NoReturn -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.var import VarData from pyomo.core.expr import value from pyomo.common.collections import ComponentMap @@ -65,8 +65,8 @@ def get_primals( """ def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: """ Returns a dictionary mapping constraint to dual value. @@ -119,8 +119,8 @@ def get_primals(self, vars_to_load=None): return self._solver._get_primals(vars_to_load=vars_to_load) def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: self._assert_solution_still_valid() return self._solver._get_duals(cons_to_load=cons_to_load) @@ -201,8 +201,8 @@ def get_primals( return res def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: if self._nl_info is None: raise RuntimeError( 'Solution loader does not currently have a valid solution. Please ' diff --git a/pyomo/contrib/solver/tests/unit/test_results.py b/pyomo/contrib/solver/tests/unit/test_results.py index 6c178d80298..a15c9b87253 100644 --- a/pyomo/contrib/solver/tests/unit/test_results.py +++ b/pyomo/contrib/solver/tests/unit/test_results.py @@ -15,7 +15,7 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.var import VarData from pyomo.common.collections import ComponentMap from pyomo.contrib.solver import results @@ -67,8 +67,8 @@ def get_primals( return primals def get_duals( - self, cons_to_load: Optional[Sequence[GeneralConstraintData]] = None - ) -> Dict[GeneralConstraintData, float]: + self, cons_to_load: Optional[Sequence[ConstraintData]] = None + ) -> Dict[ConstraintData, float]: if self._duals is None: raise RuntimeError( 'Solution loader does not currently have valid duals. Please ' diff --git a/pyomo/core/tests/unit/test_con.py b/pyomo/core/tests/unit/test_con.py index 26ccc7944a7..15f190e281e 100644 --- a/pyomo/core/tests/unit/test_con.py +++ b/pyomo/core/tests/unit/test_con.py @@ -44,7 +44,7 @@ InequalityExpression, RangedExpression, ) -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData class TestConstraintCreation(unittest.TestCase): @@ -1074,7 +1074,7 @@ def test_setitem(self): m.c[2] = m.x**2 <= 4 self.assertEqual(len(m.c), 1) self.assertEqual(list(m.c.keys()), [2]) - self.assertIsInstance(m.c[2], GeneralConstraintData) + self.assertIsInstance(m.c[2], ConstraintData) self.assertEqual(m.c[2].upper, 4) m.c[3] = Constraint.Skip diff --git a/pyomo/core/tests/unit/test_dict_objects.py b/pyomo/core/tests/unit/test_dict_objects.py index 16b7e0bd2e0..ef9f330bfff 100644 --- a/pyomo/core/tests/unit/test_dict_objects.py +++ b/pyomo/core/tests/unit/test_dict_objects.py @@ -18,7 +18,7 @@ ExpressionDict, ) from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.expression import ExpressionData @@ -375,7 +375,7 @@ def setUp(self): class TestConstraintDict(_TestActiveComponentDictBase, unittest.TestCase): _ctype = ConstraintDict - _cdatatype = GeneralConstraintData + _cdatatype = ConstraintData def setUp(self): _TestComponentDictBase.setUp(self) diff --git a/pyomo/core/tests/unit/test_list_objects.py b/pyomo/core/tests/unit/test_list_objects.py index 94913bcbc02..671a8429e06 100644 --- a/pyomo/core/tests/unit/test_list_objects.py +++ b/pyomo/core/tests/unit/test_list_objects.py @@ -18,7 +18,7 @@ XExpressionList, ) from pyomo.core.base.var import VarData -from pyomo.core.base.constraint import GeneralConstraintData +from pyomo.core.base.constraint import ConstraintData from pyomo.core.base.objective import ObjectiveData from pyomo.core.base.expression import ExpressionData @@ -392,7 +392,7 @@ def setUp(self): class TestConstraintList(_TestActiveComponentListBase, unittest.TestCase): _ctype = XConstraintList - _cdatatype = GeneralConstraintData + _cdatatype = ConstraintData def setUp(self): _TestComponentListBase.setUp(self) diff --git a/pyomo/gdp/tests/common_tests.py b/pyomo/gdp/tests/common_tests.py index 233c3ca9c09..50bc8b05f86 100644 --- a/pyomo/gdp/tests/common_tests.py +++ b/pyomo/gdp/tests/common_tests.py @@ -952,9 +952,7 @@ def check_disjunction_data_target(self, transformation): transBlock = m.component("_pyomo_gdp_%s_reformulation" % transformation) self.assertIsInstance(transBlock, Block) self.assertIsInstance(transBlock.component("disjunction_xor"), Constraint) - self.assertIsInstance( - transBlock.disjunction_xor[2], constraint.GeneralConstraintData - ) + self.assertIsInstance(transBlock.disjunction_xor[2], constraint.ConstraintData) self.assertIsInstance(transBlock.component("relaxedDisjuncts"), Block) self.assertEqual(len(transBlock.relaxedDisjuncts), 3) @@ -963,7 +961,7 @@ def check_disjunction_data_target(self, transformation): m, targets=[m.disjunction[1]] ) self.assertIsInstance( - m.disjunction[1].algebraic_constraint, constraint.GeneralConstraintData + m.disjunction[1].algebraic_constraint, constraint.ConstraintData ) transBlock = m.component("_pyomo_gdp_%s_reformulation_4" % transformation) self.assertIsInstance(transBlock, Block) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index efef4c5fb1f..cf42eb260ff 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1323,8 +1323,8 @@ def test_do_not_transform_deactivated_constraintDatas(self): self.assertEqual(len(cons_list), 2) lb = cons_list[0] ub = cons_list[1] - self.assertIsInstance(lb, constraint.GeneralConstraintData) - self.assertIsInstance(ub, constraint.GeneralConstraintData) + self.assertIsInstance(lb, constraint.ConstraintData) + self.assertIsInstance(ub, constraint.ConstraintData) def checkMs( self, m, disj1c1lb, disj1c1ub, disj1c2lb, disj1c2ub, disj2c1ub, disj2c2ub diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 6093e01dc25..07876a9d213 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -1252,12 +1252,10 @@ def check_second_iteration(self, model): orig = model.component("_pyomo_gdp_hull_reformulation") self.assertIsInstance( - model.disjunctionList[1].algebraic_constraint, - constraint.GeneralConstraintData, + model.disjunctionList[1].algebraic_constraint, constraint.ConstraintData ) self.assertIsInstance( - model.disjunctionList[0].algebraic_constraint, - constraint.GeneralConstraintData, + model.disjunctionList[0].algebraic_constraint, constraint.ConstraintData ) self.assertFalse(model.disjunctionList[1].active) self.assertFalse(model.disjunctionList[0].active) diff --git a/pyomo/solvers/plugins/solvers/gurobi_persistent.py b/pyomo/solvers/plugins/solvers/gurobi_persistent.py index 17ce33fd95f..94a2ac6b734 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_persistent.py +++ b/pyomo/solvers/plugins/solvers/gurobi_persistent.py @@ -157,7 +157,7 @@ def set_linear_constraint_attr(self, con, attr, val): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be modified. attr: str @@ -384,7 +384,7 @@ def get_linear_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -431,7 +431,7 @@ def get_quadratic_constraint_attr(self, con, attr): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The pyomo constraint for which the corresponding gurobi constraint attribute should be retrieved. attr: str @@ -569,7 +569,7 @@ def cbCut(self, con): Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The cut to add """ if not con.active: @@ -647,7 +647,7 @@ def cbLazy(self, con): """ Parameters ---------- - con: pyomo.core.base.constraint.GeneralConstraintData + con: pyomo.core.base.constraint.ConstraintData The lazy constraint to add """ if not con.active: From ef522d9338eb8722737d0522fe65bfd8e3c96ec9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 21 Mar 2024 15:25:22 -0600 Subject: [PATCH 1027/3044] Fix base class --- pyomo/core/base/constraint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 08c97d7c8ae..3a71758d55d 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -125,7 +125,7 @@ def C_rule(model, i, j): return rule_wrapper(rule, result_map, map_types=map_types) -class ConstraintData(ConstraintData): +class ConstraintData(ActiveComponentData): """ This class defines the data for a single general constraint. From e55007991d8ec0cadc84bc12bbddfcb01225bdcb Mon Sep 17 00:00:00 2001 From: Eslick Date: Fri, 22 Mar 2024 14:47:31 -0400 Subject: [PATCH 1028/3044] Fix for duplicate add --- pyomo/solvers/plugins/solvers/ASL.py | 4 +++- pyomo/solvers/plugins/solvers/IPOPT.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/ASL.py b/pyomo/solvers/plugins/solvers/ASL.py index ae7ad82c870..3ebe5c3b422 100644 --- a/pyomo/solvers/plugins/solvers/ASL.py +++ b/pyomo/solvers/plugins/solvers/ASL.py @@ -160,7 +160,9 @@ def create_command_line(self, executable, problem_files): # if 'PYOMO_AMPLFUNC' in env: if 'AMPLFUNC' in env: - env['AMPLFUNC'] += "\n" + env['PYOMO_AMPLFUNC'] + for line in env['PYOMO_AMPLFUNC'].split('\n'): + if line not in env['AMPLFUNC']: + env['AMPLFUNC'] += "\n" + line else: env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index 4ebbbc07d3b..17b68da6364 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -121,7 +121,9 @@ def create_command_line(self, executable, problem_files): # if 'PYOMO_AMPLFUNC' in env: if 'AMPLFUNC' in env: - env['AMPLFUNC'] += "\n" + env['PYOMO_AMPLFUNC'] + for line in env['PYOMO_AMPLFUNC'].split('\n'): + if line not in env['AMPLFUNC']: + env['AMPLFUNC'] += "\n" + line else: env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] From 035cf9c6ff5e3482a283fb6ea21714db35a56709 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 22 Mar 2024 18:57:03 -0400 Subject: [PATCH 1029/3044] replace an error check that should never happen with a comment --- pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py index edb1a03afe6..d5de010f308 100644 --- a/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py @@ -191,9 +191,8 @@ def x_constraint(b, i): # Not a Gray code, just a regular binary representation # TODO test the Gray codes too + # note: Must have num != 0 and ceil(log2(num)) > length to be valid def _get_binary_vector(self, num, length): - if num != 0 and ceil(log2(num)) > length: - raise DeveloperError("Invalid input in _get_binary_vector") # Use python's string formatting instead of bothering with modular # arithmetic. Hopefully not slow. return tuple(int(x) for x in format(num, f"0{length}b")) From 9f63effd966bbe232e4774d26c27cfb242391976 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Fri, 22 Mar 2024 21:16:49 -0600 Subject: [PATCH 1030/3044] initial implementation of function to perform the full (fine and coarse) dulmage-mendelsohn decomposition --- pyomo/contrib/incidence_analysis/config.py | 7 +++ pyomo/contrib/incidence_analysis/interface.py | 47 ++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/config.py b/pyomo/contrib/incidence_analysis/config.py index 128273b4dec..2a7734ba433 100644 --- a/pyomo/contrib/incidence_analysis/config.py +++ b/pyomo/contrib/incidence_analysis/config.py @@ -36,6 +36,13 @@ class IncidenceMethod(enum.Enum): """Use ``pyomo.repn.plugins.nl_writer.AMPLRepnVisitor``""" +class IncidenceOrder(enum.Enum): + + dulmage_mendelsohn_upper = 0 + + dulmage_mendelsohn_lower = 1 + + _include_fixed = ConfigValue( default=False, domain=bool, diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 50cb84daaf5..2136a4ffc24 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -29,7 +29,7 @@ plotly, ) from pyomo.common.deprecation import deprecated -from pyomo.contrib.incidence_analysis.config import get_config_from_kwds +from pyomo.contrib.incidence_analysis.config import get_config_from_kwds, IncidenceOrder from pyomo.contrib.incidence_analysis.matching import maximum_matching from pyomo.contrib.incidence_analysis.connected import get_independent_submatrices from pyomo.contrib.incidence_analysis.triangularize import ( @@ -995,3 +995,48 @@ def add_edge(self, variable, constraint): con_id = self._con_index_map[constraint] self._incidence_graph.add_edge(var_id, con_id) + + def partition_variables_and_constraints( + self, + variables=None, + constraints=None, + order=IncidenceOrder.dulmage_mendelsohn_upper, + ): + """Partition variables and constraints in an incidence graph + """ + variables, constraints = self._validate_input(variables, constraints) + vdmp, cdmp = self.dulmage_mendelsohn(variables=variables, constraints=constraints) + + ucv = vdmp.unmatched + vdmp.underconstrained + ucc = cdmp.underconstrained + + ocv = vdmp.overconstrained + occ = cdmp.overconstrained + cdmp.unmatched + + ucvblocks, uccblocks = self.get_connected_components( + variables=ucv, constraints=ucc + ) + ocvblocks, occblocks = self.get_connected_components( + variables=ocv, constraints=occ + ) + wcvblocks, wccblocks = self.block_triangularize( + variables=vdmp.square, constraints=cdmp.square + ) + # By default, we block-*lower* triangularize. By default, however, we want + # the Dulmage-Mendelsohn decomposition to be block-*upper* triangular. + wcvblocks.reverse() + wccblocks.reverse() + vpartition = [ucvblocks, wcvblocks, ocvblocks] + cpartition = [uccblocks, wccblocks, occblocks] + + if order == IncidenceOrder.dulmage_mendelsohn_lower: + # If a block-lower triangular matrix was requested, we need to reverse + # both the inner and outer partitions + vpartition.reverse() + cpartition.reverse() + for vb in vpartition: + vb.reverse() + for cb in cpartition: + cb.reverse() + + return vpartition, cpartition From 87517fae3ec0a41f6abd93c39c8af5fe93494f4d Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Fri, 22 Mar 2024 22:41:52 -0600 Subject: [PATCH 1031/3044] draft of function to plot incidence matrix in dulmage-mendelsohn order --- pyomo/contrib/incidence_analysis/interface.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 2136a4ffc24..6e6dff7ba48 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -1040,3 +1040,84 @@ def partition_variables_and_constraints( cb.reverse() return vpartition, cpartition + + +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle + +def _get_rectangle_around_coords( + ij1, + ij2, + linewidth=2, +): + i1, j1 = ij1 + i2, j2 = ij2 + buffer = 0.5 + ll_corner = (min(i1, i2)-buffer, min(j1, j2)-buffer) + width = abs(i1 - i2) + 2*buffer + height = abs(j1 - j2) + 2*buffer + rect = Rectangle( + ll_corner, + width, + height, + clip_on=False, + fill=False, + edgecolor="orange", + linewidth=linewidth, + ) + return rect + + +def spy_dulmage_mendelsohn( + model, + order=IncidenceOrder.dulmage_mendelsohn_upper, + highlight_coarse=True, + highlight_fine=True, + ax=None, +): + igraph = IncidenceGraphInterface(model) + vpart, cpart = igraph.partition_variables_and_constraints(order=order) + vpart_fine = sum(vpart, []) + cpart_fine = sum(cpart, []) + vorder = sum(vpart_fine, []) + corder = sum(cpart_fine, []) + + imat = get_structural_incidence_matrix(vorder, corder) + + if ax is None: + fig, ax = plt.subplots() + else: + fig = None + + # TODO: Options to configure: + # - tick direction/presence + # - rectangle linewidth/linestyle + # - spy markersize + # markersize and linewidth should probably be set automatically + # based on size of problem + + ax.spy( + imat, + # TODO: pass keyword args + markersize=0.2, + ) + ax.tick_params(length=0) + if highlight_coarse: + start = (0, 0) + for vblocks, cblocks in zip(vpart, cpart): + # Get the total number of variables/constraints in this part + # of the coarse partition + nv = sum(len(vb) for vb in vblocks) + nc = sum(len(cb) for cb in cblocks) + stop = (start[0] + nv - 1, start[1] + nc - 1) + ax.add_patch(_get_rectangle_around_coords(start, stop)) + start = (stop[0] + 1, stop[1] + 1) + + if highlight_fine: + start = (0, 0) + for vb, cb in zip(vpart_fine, cpart_fine): + stop = (start[0] + len(vb) - 1, start[1] + len(cb) - 1) + ax.add_patch(_get_rectangle_around_coords(start, stop)) + start = (stop[0] + 1, stop[1] + 1) + + return fig, ax From a573244f008af69eceb39c0812cd3d30b09df268 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Mar 2024 06:32:30 -0600 Subject: [PATCH 1032/3044] Rename _ComponentBase -> ComponentBase --- pyomo/core/base/component.py | 12 +++++++++--- pyomo/core/base/set.py | 4 ++-- pyomo/core/util.py | 6 +++--- pyomo/gdp/util.py | 1 - 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 50cf264c799..7dea5b7dde5 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -20,6 +20,7 @@ from pyomo.common.autoslots import AutoSlots, fast_deepcopy from pyomo.common.collections import OrderedDict from pyomo.common.deprecation import ( + RenamedClass, deprecated, deprecation_warning, relocated_module_attribute, @@ -79,7 +80,7 @@ class CloneError(pyomo.common.errors.PyomoException): pass -class _ComponentBase(PyomoObject): +class ComponentBase(PyomoObject): """A base class for Component and ComponentData This class defines some fundamental methods and properties that are @@ -474,7 +475,12 @@ def _pprint_base_impl( ostream.write(_data) -class Component(_ComponentBase): +class _ComponentBase(metaclass=RenamedClass): + __renamed__new_class__ = ComponentBase + __renamed__version__ = '6.7.2.dev0' + + +class Component(ComponentBase): """ This is the base class for all Pyomo modeling components. @@ -779,7 +785,7 @@ def deactivate(self): self._active = False -class ComponentData(_ComponentBase): +class ComponentData(ComponentBase): """ This is the base class for the component data used in Pyomo modeling components. Subclasses of ComponentData are diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index d94cc86cf7c..fbf1ac60900 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -50,7 +50,7 @@ RangeDifferenceError, ) from pyomo.core.base.component import ( - _ComponentBase, + ComponentBase, Component, ComponentData, ModelComponentFactory, @@ -140,7 +140,7 @@ def process_setarg(arg): _anonymous.update(arg._anonymous_sets) return arg, _anonymous - elif isinstance(arg, _ComponentBase): + elif isinstance(arg, ComponentBase): if isinstance(arg, IndexedComponent) and arg.is_indexed(): raise TypeError( "Cannot apply a Set operator to an " diff --git a/pyomo/core/util.py b/pyomo/core/util.py index f337b487cef..4b6cc8f3320 100644 --- a/pyomo/core/util.py +++ b/pyomo/core/util.py @@ -18,7 +18,7 @@ from pyomo.core.expr.numeric_expr import mutable_expression, NPV_SumExpression from pyomo.core.base.var import Var from pyomo.core.base.expression import Expression -from pyomo.core.base.component import _ComponentBase +from pyomo.core.base.component import ComponentBase import logging logger = logging.getLogger(__name__) @@ -238,12 +238,12 @@ def sequence(*args): def target_list(x): - if isinstance(x, _ComponentBase): + if isinstance(x, ComponentBase): return [x] elif hasattr(x, '__iter__'): ans = [] for i in x: - if isinstance(i, _ComponentBase): + if isinstance(i, ComponentBase): ans.append(i) else: raise ValueError( diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index 686253b0179..932a2ddf451 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -13,7 +13,6 @@ from pyomo.gdp.disjunct import DisjunctData, Disjunct import pyomo.core.expr as EXPR -from pyomo.core.base.component import _ComponentBase from pyomo.core import ( Block, Suffix, From b75a974378012d4319668961f99a91668aab6446 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 23 Mar 2024 06:46:04 -0600 Subject: [PATCH 1033/3044] Remove _SetDataBase --- pyomo/core/base/reference.py | 6 +++--- pyomo/core/base/set.py | 26 ++++++++++---------------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/pyomo/core/base/reference.py b/pyomo/core/base/reference.py index 2279db067a6..fd6ba192c70 100644 --- a/pyomo/core/base/reference.py +++ b/pyomo/core/base/reference.py @@ -18,7 +18,7 @@ Sequence, ) from pyomo.common.modeling import NOTSET -from pyomo.core.base.set import DeclareGlobalSet, Set, SetOf, OrderedSetOf, _SetDataBase +from pyomo.core.base.set import DeclareGlobalSet, Set, SetOf, OrderedSetOf, SetData from pyomo.core.base.component import Component, ComponentData from pyomo.core.base.global_set import UnindexedComponent_set from pyomo.core.base.enums import SortComponents @@ -774,10 +774,10 @@ def Reference(reference, ctype=NOTSET): # is that within the subsets list, and set is a wildcard set. index = wildcards[0][1] # index is the first wildcard set. - if not isinstance(index, _SetDataBase): + if not isinstance(index, SetData): index = SetOf(index) for lvl, idx in wildcards[1:]: - if not isinstance(idx, _SetDataBase): + if not isinstance(idx, SetData): idx = SetOf(idx) index = index * idx # index is now either a single Set, or a SetProduct of the diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index fbf1ac60900..b9a2fe72e1d 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -84,10 +84,7 @@ All Sets implement one of the following APIs: -0. `class _SetDataBase(ComponentData)` - *(pure virtual interface)* - -1. `class SetData(_SetDataBase)` +1. `class SetData(ComponentData)` *(base class for all AML Sets)* 2. `class _FiniteSetMixin(object)` @@ -128,7 +125,7 @@ def process_setarg(arg): - if isinstance(arg, _SetDataBase): + if isinstance(arg, SetData): if ( getattr(arg, '_parent', None) is not None or getattr(arg, '_anonymous_sets', None) is GlobalSetBase @@ -512,16 +509,8 @@ class _NotFound(object): pass -# A trivial class that we can use to test if an object is a "legitimate" -# set (either ScalarSet, or a member of an IndexedSet) -class _SetDataBase(ComponentData): - """The base for all objects that can be used as a component indexing set.""" - - __slots__ = () - - -class SetData(_SetDataBase): - """The base for all Pyomo AML objects that can be used as a component +class SetData(ComponentData): + """The base for all Pyomo objects that can be used as a component indexing set. Derived versions of this class can be used as the Index for any @@ -1193,6 +1182,11 @@ class _SetData(metaclass=RenamedClass): __renamed__version__ = '6.7.2.dev0' +class _SetDataBase(metaclass=RenamedClass): + __renamed__new_class__ = SetData + __renamed__version__ = '6.7.2.dev0' + + class _FiniteSetMixin(object): __slots__ = () @@ -3496,7 +3490,7 @@ def _domain(self, val): def _checkArgs(*sets): ans = [] for s in sets: - if isinstance(s, _SetDataBase): + if isinstance(s, SetData): ans.append((s.isordered(), s.isfinite())) elif type(s) in {tuple, list}: ans.append((True, True)) From fb2212ad07ab7abbae86d4e1a0a7d98e894e07a1 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 23 Mar 2024 12:42:53 -0600 Subject: [PATCH 1034/3044] document plotting function and automatically calculate markersize if not provided --- pyomo/contrib/incidence_analysis/interface.py | 115 +++++++++++++++--- 1 file changed, 99 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 6e6dff7ba48..a4f08c737ec 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -1049,6 +1049,7 @@ def _get_rectangle_around_coords( ij1, ij2, linewidth=2, + linestyle="-", ): i1, j1 = ij1 i2, j2 = ij2 @@ -1064,18 +1065,82 @@ def _get_rectangle_around_coords( fill=False, edgecolor="orange", linewidth=linewidth, + linestyle=linestyle, ) return rect def spy_dulmage_mendelsohn( model, + *, + incidence_kwds=None, order=IncidenceOrder.dulmage_mendelsohn_upper, highlight_coarse=True, highlight_fine=True, + skip_wellconstrained=False, ax=None, + linewidth=2, + spy_kwds=None, ): - igraph = IncidenceGraphInterface(model) + """Plot sparsity structure in Dulmage-Mendelsohn order on Matplotlib + axes + + This is a wrapper around the Matplotlib ``Axes.spy`` method for plotting + an incidence matrix in Dulmage-Mendelsohn order, with coarse and/or fine + partitions highlighted. The coarse partition refers to the under-constrained, + over-constrained, and well-constrained subsystems, while the fine partition + refers to block diagonal or block triangular partitions of the former + subsystems. + + Parameters + ---------- + + model: ``ConcreteModel`` + Input model to plot sparsity structure of + + incidence_kwds: dict, optional + Config options for ``IncidenceGraphInterface`` + + order: ``IncidenceOrder``, optional + Order in which to plot sparsity structure + + highlight_coarse: bool, optional + Whether to draw a rectange around the coarse partition + + highlight_fine: bool, optional + Whether to draw a rectangle around the fine partition + + skip_wellconstrained: bool, optional + Whether to skip highlighting the well-constrained subsystem of the + coarse partition. Default False + + ax: ``matplotlib.pyplot.Axes``, optional + Axes object on which to plot. If not provided, new figure + and axes are created. + + linewidth: int, optional + Line width of for rectangle used to highlight. Default 2 + + spy_kwds: dict, optional + Keyword arguments for ``Axes.spy`` + + Returns + ------- + + fig: ``matplotlib.pyplot.Figure`` or ``None`` + Figure on which the sparsity structure is plotted. ``None`` if axes + are provided + + ax: ``matplotlib.pyplot.Axes`` + Axes on which the sparsity structure is plotted + + """ + if incidence_kwds is None: + incidence_kwds = {} + if spy_kwds is None: + spy_kwds = {} + + igraph = IncidenceGraphInterface(model, **incidence_kwds) vpart, cpart = igraph.partition_variables_and_constraints(order=order) vpart_fine = sum(vpart, []) cpart_fine = sum(cpart, []) @@ -1083,41 +1148,59 @@ def spy_dulmage_mendelsohn( corder = sum(cpart_fine, []) imat = get_structural_incidence_matrix(vorder, corder) + nvar = len(vorder) + ncon = len(corder) if ax is None: fig, ax = plt.subplots() else: fig = None - # TODO: Options to configure: - # - tick direction/presence - # - rectangle linewidth/linestyle - # - spy markersize - # markersize and linewidth should probably be set automatically - # based on size of problem - - ax.spy( - imat, - # TODO: pass keyword args - markersize=0.2, - ) + markersize = spy_kwds.pop("markersize", None) + if markersize is None: + # At 10000 vars/cons, we want markersize=0.2 + # At 20 vars/cons, we want markersize=10 + # We assume we want a linear relationship between 1/nvar + # and the markersize. + markersize = ( + (10.0 - 0.2) / (1/20 - 1/10000) * (1/max(nvar, ncon) - 1/10000) + + 0.2 + ) + + ax.spy(imat, markersize=markersize, **spy_kwds) ax.tick_params(length=0) if highlight_coarse: start = (0, 0) - for vblocks, cblocks in zip(vpart, cpart): + for i, (vblocks, cblocks) in enumerate(zip(vpart, cpart)): # Get the total number of variables/constraints in this part # of the coarse partition nv = sum(len(vb) for vb in vblocks) nc = sum(len(cb) for cb in cblocks) stop = (start[0] + nv - 1, start[1] + nc - 1) - ax.add_patch(_get_rectangle_around_coords(start, stop)) + if not (i == 1 and skip_wellconstrained): + # Regardless of whether we are plotting in upper or lower + # triangular order, the well-constrained subsystem is at + # position 1 + ax.add_patch( + _get_rectangle_around_coords(start, stop, linewidth=linewidth) + ) start = (stop[0] + 1, stop[1] + 1) if highlight_fine: + # Use dashed lines to distinguish inner from outer partitions + # if we are highlighting both + linestyle = "--" if highlight_coarse else "-" start = (0, 0) for vb, cb in zip(vpart_fine, cpart_fine): stop = (start[0] + len(vb) - 1, start[1] + len(cb) - 1) - ax.add_patch(_get_rectangle_around_coords(start, stop)) + ax.add_patch( + _get_rectangle_around_coords( + start, + stop, + linestyle=linestyle, + linewidth=linewidth, + ) + ) start = (stop[0] + 1, stop[1] + 1) return fig, ax From 6464db3587e4c3be7c311ca2cc18d907cd4e3cf0 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 23 Mar 2024 13:09:35 -0600 Subject: [PATCH 1035/3044] move spy_dulmage_mendelsohn to visualize module --- pyomo/contrib/incidence_analysis/interface.py | 211 +---------------- pyomo/contrib/incidence_analysis/visualize.py | 222 ++++++++++++++++++ 2 files changed, 223 insertions(+), 210 deletions(-) create mode 100644 pyomo/contrib/incidence_analysis/visualize.py diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index a4f08c737ec..50cb84daaf5 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -29,7 +29,7 @@ plotly, ) from pyomo.common.deprecation import deprecated -from pyomo.contrib.incidence_analysis.config import get_config_from_kwds, IncidenceOrder +from pyomo.contrib.incidence_analysis.config import get_config_from_kwds from pyomo.contrib.incidence_analysis.matching import maximum_matching from pyomo.contrib.incidence_analysis.connected import get_independent_submatrices from pyomo.contrib.incidence_analysis.triangularize import ( @@ -995,212 +995,3 @@ def add_edge(self, variable, constraint): con_id = self._con_index_map[constraint] self._incidence_graph.add_edge(var_id, con_id) - - def partition_variables_and_constraints( - self, - variables=None, - constraints=None, - order=IncidenceOrder.dulmage_mendelsohn_upper, - ): - """Partition variables and constraints in an incidence graph - """ - variables, constraints = self._validate_input(variables, constraints) - vdmp, cdmp = self.dulmage_mendelsohn(variables=variables, constraints=constraints) - - ucv = vdmp.unmatched + vdmp.underconstrained - ucc = cdmp.underconstrained - - ocv = vdmp.overconstrained - occ = cdmp.overconstrained + cdmp.unmatched - - ucvblocks, uccblocks = self.get_connected_components( - variables=ucv, constraints=ucc - ) - ocvblocks, occblocks = self.get_connected_components( - variables=ocv, constraints=occ - ) - wcvblocks, wccblocks = self.block_triangularize( - variables=vdmp.square, constraints=cdmp.square - ) - # By default, we block-*lower* triangularize. By default, however, we want - # the Dulmage-Mendelsohn decomposition to be block-*upper* triangular. - wcvblocks.reverse() - wccblocks.reverse() - vpartition = [ucvblocks, wcvblocks, ocvblocks] - cpartition = [uccblocks, wccblocks, occblocks] - - if order == IncidenceOrder.dulmage_mendelsohn_lower: - # If a block-lower triangular matrix was requested, we need to reverse - # both the inner and outer partitions - vpartition.reverse() - cpartition.reverse() - for vb in vpartition: - vb.reverse() - for cb in cpartition: - cb.reverse() - - return vpartition, cpartition - - -import matplotlib.pyplot as plt -from matplotlib.patches import Rectangle - -def _get_rectangle_around_coords( - ij1, - ij2, - linewidth=2, - linestyle="-", -): - i1, j1 = ij1 - i2, j2 = ij2 - buffer = 0.5 - ll_corner = (min(i1, i2)-buffer, min(j1, j2)-buffer) - width = abs(i1 - i2) + 2*buffer - height = abs(j1 - j2) + 2*buffer - rect = Rectangle( - ll_corner, - width, - height, - clip_on=False, - fill=False, - edgecolor="orange", - linewidth=linewidth, - linestyle=linestyle, - ) - return rect - - -def spy_dulmage_mendelsohn( - model, - *, - incidence_kwds=None, - order=IncidenceOrder.dulmage_mendelsohn_upper, - highlight_coarse=True, - highlight_fine=True, - skip_wellconstrained=False, - ax=None, - linewidth=2, - spy_kwds=None, -): - """Plot sparsity structure in Dulmage-Mendelsohn order on Matplotlib - axes - - This is a wrapper around the Matplotlib ``Axes.spy`` method for plotting - an incidence matrix in Dulmage-Mendelsohn order, with coarse and/or fine - partitions highlighted. The coarse partition refers to the under-constrained, - over-constrained, and well-constrained subsystems, while the fine partition - refers to block diagonal or block triangular partitions of the former - subsystems. - - Parameters - ---------- - - model: ``ConcreteModel`` - Input model to plot sparsity structure of - - incidence_kwds: dict, optional - Config options for ``IncidenceGraphInterface`` - - order: ``IncidenceOrder``, optional - Order in which to plot sparsity structure - - highlight_coarse: bool, optional - Whether to draw a rectange around the coarse partition - - highlight_fine: bool, optional - Whether to draw a rectangle around the fine partition - - skip_wellconstrained: bool, optional - Whether to skip highlighting the well-constrained subsystem of the - coarse partition. Default False - - ax: ``matplotlib.pyplot.Axes``, optional - Axes object on which to plot. If not provided, new figure - and axes are created. - - linewidth: int, optional - Line width of for rectangle used to highlight. Default 2 - - spy_kwds: dict, optional - Keyword arguments for ``Axes.spy`` - - Returns - ------- - - fig: ``matplotlib.pyplot.Figure`` or ``None`` - Figure on which the sparsity structure is plotted. ``None`` if axes - are provided - - ax: ``matplotlib.pyplot.Axes`` - Axes on which the sparsity structure is plotted - - """ - if incidence_kwds is None: - incidence_kwds = {} - if spy_kwds is None: - spy_kwds = {} - - igraph = IncidenceGraphInterface(model, **incidence_kwds) - vpart, cpart = igraph.partition_variables_and_constraints(order=order) - vpart_fine = sum(vpart, []) - cpart_fine = sum(cpart, []) - vorder = sum(vpart_fine, []) - corder = sum(cpart_fine, []) - - imat = get_structural_incidence_matrix(vorder, corder) - nvar = len(vorder) - ncon = len(corder) - - if ax is None: - fig, ax = plt.subplots() - else: - fig = None - - markersize = spy_kwds.pop("markersize", None) - if markersize is None: - # At 10000 vars/cons, we want markersize=0.2 - # At 20 vars/cons, we want markersize=10 - # We assume we want a linear relationship between 1/nvar - # and the markersize. - markersize = ( - (10.0 - 0.2) / (1/20 - 1/10000) * (1/max(nvar, ncon) - 1/10000) - + 0.2 - ) - - ax.spy(imat, markersize=markersize, **spy_kwds) - ax.tick_params(length=0) - if highlight_coarse: - start = (0, 0) - for i, (vblocks, cblocks) in enumerate(zip(vpart, cpart)): - # Get the total number of variables/constraints in this part - # of the coarse partition - nv = sum(len(vb) for vb in vblocks) - nc = sum(len(cb) for cb in cblocks) - stop = (start[0] + nv - 1, start[1] + nc - 1) - if not (i == 1 and skip_wellconstrained): - # Regardless of whether we are plotting in upper or lower - # triangular order, the well-constrained subsystem is at - # position 1 - ax.add_patch( - _get_rectangle_around_coords(start, stop, linewidth=linewidth) - ) - start = (stop[0] + 1, stop[1] + 1) - - if highlight_fine: - # Use dashed lines to distinguish inner from outer partitions - # if we are highlighting both - linestyle = "--" if highlight_coarse else "-" - start = (0, 0) - for vb, cb in zip(vpart_fine, cpart_fine): - stop = (start[0] + len(vb) - 1, start[1] + len(cb) - 1) - ax.add_patch( - _get_rectangle_around_coords( - start, - stop, - linestyle=linestyle, - linewidth=linewidth, - ) - ) - start = (stop[0] + 1, stop[1] + 1) - - return fig, ax diff --git a/pyomo/contrib/incidence_analysis/visualize.py b/pyomo/contrib/incidence_analysis/visualize.py new file mode 100644 index 00000000000..929f8a77d4e --- /dev/null +++ b/pyomo/contrib/incidence_analysis/visualize.py @@ -0,0 +1,222 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +"""Module for visualizing results of incidence graph or matrix analysis + +""" +from pyomo.contrib.incidence_analysis.config import IncidenceOrder +from pyomo.contrib.incidence_analysis.interface import ( + IncidenceGraphInterface, + get_structural_incidence_matrix, +) +from pyomo.common.dependencies import matplotlib + + +def _partition_variables_and_constraints( + model, order=IncidenceOrder.dulmage_mendelsohn_upper, **kwds +): + """Partition variables and constraints in an incidence graph + """ + igraph = IncidenceGraphInterface(model, **kwds) + vdmp, cdmp = igraph.dulmage_mendelsohn() + + ucv = vdmp.unmatched + vdmp.underconstrained + ucc = cdmp.underconstrained + + ocv = vdmp.overconstrained + occ = cdmp.overconstrained + cdmp.unmatched + + ucvblocks, uccblocks = igraph.get_connected_components( + variables=ucv, constraints=ucc + ) + ocvblocks, occblocks = igraph.get_connected_components( + variables=ocv, constraints=occ + ) + wcvblocks, wccblocks = igraph.block_triangularize( + variables=vdmp.square, constraints=cdmp.square + ) + # By default, we block-*lower* triangularize. By default, however, we want + # the Dulmage-Mendelsohn decomposition to be block-*upper* triangular. + wcvblocks.reverse() + wccblocks.reverse() + vpartition = [ucvblocks, wcvblocks, ocvblocks] + cpartition = [uccblocks, wccblocks, occblocks] + + if order == IncidenceOrder.dulmage_mendelsohn_lower: + # If a block-lower triangular matrix was requested, we need to reverse + # both the inner and outer partitions + vpartition.reverse() + cpartition.reverse() + for vb in vpartition: + vb.reverse() + for cb in cpartition: + cb.reverse() + + return vpartition, cpartition + + +def _get_rectangle_around_coords( + ij1, + ij2, + linewidth=2, + linestyle="-", +): + i1, j1 = ij1 + i2, j2 = ij2 + buffer = 0.5 + ll_corner = (min(i1, i2)-buffer, min(j1, j2)-buffer) + width = abs(i1 - i2) + 2*buffer + height = abs(j1 - j2) + 2*buffer + rect = matplotlib.patches.Rectangle( + ll_corner, + width, + height, + clip_on=False, + fill=False, + edgecolor="orange", + linewidth=linewidth, + linestyle=linestyle, + ) + return rect + + +def spy_dulmage_mendelsohn( + model, + *, + incidence_kwds=None, + order=IncidenceOrder.dulmage_mendelsohn_upper, + highlight_coarse=True, + highlight_fine=True, + skip_wellconstrained=False, + ax=None, + linewidth=2, + spy_kwds=None, +): + """Plot sparsity structure in Dulmage-Mendelsohn order on Matplotlib axes + + This is a wrapper around the Matplotlib ``Axes.spy`` method for plotting + an incidence matrix in Dulmage-Mendelsohn order, with coarse and/or fine + partitions highlighted. The coarse partition refers to the under-constrained, + over-constrained, and well-constrained subsystems, while the fine partition + refers to block diagonal or block triangular partitions of the former + subsystems. + + Parameters + ---------- + + model: ``ConcreteModel`` + Input model to plot sparsity structure of + + incidence_kwds: dict, optional + Config options for ``IncidenceGraphInterface`` + + order: ``IncidenceOrder``, optional + Order in which to plot sparsity structure + + highlight_coarse: bool, optional + Whether to draw a rectange around the coarse partition + + highlight_fine: bool, optional + Whether to draw a rectangle around the fine partition + + skip_wellconstrained: bool, optional + Whether to skip highlighting the well-constrained subsystem of the + coarse partition. Default False + + ax: ``matplotlib.pyplot.Axes``, optional + Axes object on which to plot. If not provided, new figure + and axes are created. + + linewidth: int, optional + Line width of for rectangle used to highlight. Default 2 + + spy_kwds: dict, optional + Keyword arguments for ``Axes.spy`` + + Returns + ------- + + fig: ``matplotlib.pyplot.Figure`` or ``None`` + Figure on which the sparsity structure is plotted. ``None`` if axes + are provided + + ax: ``matplotlib.pyplot.Axes`` + Axes on which the sparsity structure is plotted + + """ + plt = matplotlib.pyplot + if incidence_kwds is None: + incidence_kwds = {} + if spy_kwds is None: + spy_kwds = {} + + vpart, cpart = _partition_variables_and_constraints(model, order=order) + vpart_fine = sum(vpart, []) + cpart_fine = sum(cpart, []) + vorder = sum(vpart_fine, []) + corder = sum(cpart_fine, []) + + imat = get_structural_incidence_matrix(vorder, corder) + nvar = len(vorder) + ncon = len(corder) + + if ax is None: + fig, ax = plt.subplots() + else: + fig = None + + markersize = spy_kwds.pop("markersize", None) + if markersize is None: + # At 10000 vars/cons, we want markersize=0.2 + # At 20 vars/cons, we want markersize=10 + # We assume we want a linear relationship between 1/nvar + # and the markersize. + markersize = ( + (10.0 - 0.2) / (1/20 - 1/10000) * (1/max(nvar, ncon) - 1/10000) + + 0.2 + ) + + ax.spy(imat, markersize=markersize, **spy_kwds) + ax.tick_params(length=0) + if highlight_coarse: + start = (0, 0) + for i, (vblocks, cblocks) in enumerate(zip(vpart, cpart)): + # Get the total number of variables/constraints in this part + # of the coarse partition + nv = sum(len(vb) for vb in vblocks) + nc = sum(len(cb) for cb in cblocks) + stop = (start[0] + nv - 1, start[1] + nc - 1) + if not (i == 1 and skip_wellconstrained): + # Regardless of whether we are plotting in upper or lower + # triangular order, the well-constrained subsystem is at + # position 1 + ax.add_patch( + _get_rectangle_around_coords(start, stop, linewidth=linewidth) + ) + start = (stop[0] + 1, stop[1] + 1) + + if highlight_fine: + # Use dashed lines to distinguish inner from outer partitions + # if we are highlighting both + linestyle = "--" if highlight_coarse else "-" + start = (0, 0) + for vb, cb in zip(vpart_fine, cpart_fine): + stop = (start[0] + len(vb) - 1, start[1] + len(cb) - 1) + ax.add_patch( + _get_rectangle_around_coords( + start, + stop, + linestyle=linestyle, + linewidth=linewidth, + ) + ) + start = (stop[0] + 1, stop[1] + 1) + + return fig, ax From 4f78f7ac1587f717a3a39d7ef90ec58ef02f5c5e Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 23 Mar 2024 13:19:57 -0600 Subject: [PATCH 1036/3044] module to "test" visualization --- .../tests/test_visualize.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 pyomo/contrib/incidence_analysis/tests/test_visualize.py diff --git a/pyomo/contrib/incidence_analysis/tests/test_visualize.py b/pyomo/contrib/incidence_analysis/tests/test_visualize.py new file mode 100644 index 00000000000..ceb36c33e34 --- /dev/null +++ b/pyomo/contrib/incidence_analysis/tests/test_visualize.py @@ -0,0 +1,41 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.common.dependencies import matplotlib, matplotlib_available +from pyomo.contrib.incidence_analysis.visualize import spy_dulmage_mendelsohn +from pyomo.contrib.incidence_analysis.tests.models_for_testing import ( + make_gas_expansion_model, + make_dynamic_model, + make_degenerate_solid_phase_model, +) + + +@unittest.skipUnless(matplotlib_available, "Matplotlib is not available") +class TestSpy(unittest.TestCase): + + def test_spy_dulmage_mendelsohn(self): + models = [ + make_gas_expansion_model(), + make_dynamic_model(), + make_degenerate_solid_phase_model(), + ] + for m in models: + fig, ax = spy_dulmage_mendelsohn(m) + # Note that this is a weak test. We just test that we can call the + # plot method, it doesn't raise an error, and gives us back the + # types we expect. We don't attemt to validate the resulting plot. + self.assertTrue(isinstance(fig, matplotlib.pyplot.Figure)) + self.assertTrue(isinstance(ax, matplotlib.pyplot.Axes)) + + +if __name__ == "__main__": + unittest.main() From d10e549bbaa15a3a50a13f02c5d74f0c94321ea6 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 23 Mar 2024 13:40:49 -0600 Subject: [PATCH 1037/3044] apply black --- .../tests/test_visualize.py | 1 - pyomo/contrib/incidence_analysis/visualize.py | 30 +++++++------------ 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/tests/test_visualize.py b/pyomo/contrib/incidence_analysis/tests/test_visualize.py index ceb36c33e34..ea740e86c27 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_visualize.py +++ b/pyomo/contrib/incidence_analysis/tests/test_visualize.py @@ -21,7 +21,6 @@ @unittest.skipUnless(matplotlib_available, "Matplotlib is not available") class TestSpy(unittest.TestCase): - def test_spy_dulmage_mendelsohn(self): models = [ make_gas_expansion_model(), diff --git a/pyomo/contrib/incidence_analysis/visualize.py b/pyomo/contrib/incidence_analysis/visualize.py index 929f8a77d4e..e198d859db5 100644 --- a/pyomo/contrib/incidence_analysis/visualize.py +++ b/pyomo/contrib/incidence_analysis/visualize.py @@ -22,8 +22,7 @@ def _partition_variables_and_constraints( model, order=IncidenceOrder.dulmage_mendelsohn_upper, **kwds ): - """Partition variables and constraints in an incidence graph - """ + """Partition variables and constraints in an incidence graph""" igraph = IncidenceGraphInterface(model, **kwds) vdmp, cdmp = igraph.dulmage_mendelsohn() @@ -62,18 +61,13 @@ def _partition_variables_and_constraints( return vpartition, cpartition -def _get_rectangle_around_coords( - ij1, - ij2, - linewidth=2, - linestyle="-", -): +def _get_rectangle_around_coords(ij1, ij2, linewidth=2, linestyle="-"): i1, j1 = ij1 i2, j2 = ij2 buffer = 0.5 - ll_corner = (min(i1, i2)-buffer, min(j1, j2)-buffer) - width = abs(i1 - i2) + 2*buffer - height = abs(j1 - j2) + 2*buffer + ll_corner = (min(i1, i2) - buffer, min(j1, j2) - buffer) + width = abs(i1 - i2) + 2 * buffer + height = abs(j1 - j2) + 2 * buffer rect = matplotlib.patches.Rectangle( ll_corner, width, @@ -136,7 +130,7 @@ def spy_dulmage_mendelsohn( linewidth: int, optional Line width of for rectangle used to highlight. Default 2 - + spy_kwds: dict, optional Keyword arguments for ``Axes.spy`` @@ -178,10 +172,9 @@ def spy_dulmage_mendelsohn( # At 20 vars/cons, we want markersize=10 # We assume we want a linear relationship between 1/nvar # and the markersize. - markersize = ( - (10.0 - 0.2) / (1/20 - 1/10000) * (1/max(nvar, ncon) - 1/10000) - + 0.2 - ) + markersize = (10.0 - 0.2) / (1 / 20 - 1 / 10000) * ( + 1 / max(nvar, ncon) - 1 / 10000 + ) + 0.2 ax.spy(imat, markersize=markersize, **spy_kwds) ax.tick_params(length=0) @@ -211,10 +204,7 @@ def spy_dulmage_mendelsohn( stop = (start[0] + len(vb) - 1, start[1] + len(cb) - 1) ax.add_patch( _get_rectangle_around_coords( - start, - stop, - linestyle=linestyle, - linewidth=linewidth, + start, stop, linestyle=linestyle, linewidth=linewidth ) ) start = (stop[0] + 1, stop[1] + 1) From 3da70ddcab8322cde137cb782001cfc431af546d Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 23 Mar 2024 13:46:18 -0600 Subject: [PATCH 1038/3044] fix typos --- pyomo/contrib/incidence_analysis/tests/test_visualize.py | 2 +- pyomo/contrib/incidence_analysis/visualize.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/tests/test_visualize.py b/pyomo/contrib/incidence_analysis/tests/test_visualize.py index ea740e86c27..3a6a403810e 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_visualize.py +++ b/pyomo/contrib/incidence_analysis/tests/test_visualize.py @@ -31,7 +31,7 @@ def test_spy_dulmage_mendelsohn(self): fig, ax = spy_dulmage_mendelsohn(m) # Note that this is a weak test. We just test that we can call the # plot method, it doesn't raise an error, and gives us back the - # types we expect. We don't attemt to validate the resulting plot. + # types we expect. We don't attempt to validate the resulting plot. self.assertTrue(isinstance(fig, matplotlib.pyplot.Figure)) self.assertTrue(isinstance(ax, matplotlib.pyplot.Axes)) diff --git a/pyomo/contrib/incidence_analysis/visualize.py b/pyomo/contrib/incidence_analysis/visualize.py index e198d859db5..05c0661070a 100644 --- a/pyomo/contrib/incidence_analysis/visualize.py +++ b/pyomo/contrib/incidence_analysis/visualize.py @@ -115,7 +115,7 @@ def spy_dulmage_mendelsohn( Order in which to plot sparsity structure highlight_coarse: bool, optional - Whether to draw a rectange around the coarse partition + Whether to draw a rectangle around the coarse partition highlight_fine: bool, optional Whether to draw a rectangle around the fine partition From 86bdcab77b1f26df97d94c7616f95e6a38a33626 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Sat, 23 Mar 2024 22:24:04 -0600 Subject: [PATCH 1039/3044] skip test if scipy/networkx not available --- pyomo/contrib/incidence_analysis/tests/test_visualize.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/tests/test_visualize.py b/pyomo/contrib/incidence_analysis/tests/test_visualize.py index 3a6a403810e..7c5538b671f 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_visualize.py +++ b/pyomo/contrib/incidence_analysis/tests/test_visualize.py @@ -10,7 +10,12 @@ # ___________________________________________________________________________ import pyomo.common.unittest as unittest -from pyomo.common.dependencies import matplotlib, matplotlib_available +from pyomo.common.dependencies import ( + matplotlib, + matplotlib_available, + scipy_available, + networkx_available, +) from pyomo.contrib.incidence_analysis.visualize import spy_dulmage_mendelsohn from pyomo.contrib.incidence_analysis.tests.models_for_testing import ( make_gas_expansion_model, @@ -20,6 +25,8 @@ @unittest.skipUnless(matplotlib_available, "Matplotlib is not available") +@unittest.skipUnless(scipy_available, "SciPy is not available") +@unittest.skipUnless(networkx_available, "NetworkX is not available") class TestSpy(unittest.TestCase): def test_spy_dulmage_mendelsohn(self): models = [ From a89ef831321118862ad8082f5992886d906fc42b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 08:59:40 -0600 Subject: [PATCH 1040/3044] Bugfix: bound methods don't work on ScalarBlock --- pyomo/contrib/solver/base.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 8840265763e..12cb2e83918 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -14,11 +14,11 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple import os -from pyomo.core.base.constraint import _GeneralConstraintData -from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.constraint import Constraint, _GeneralConstraintData +from pyomo.core.base.var import Var, _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData -from pyomo.core.base.objective import _GeneralObjectiveData +from pyomo.core.base.objective import Objective, _GeneralObjectiveData from pyomo.common.config import document_kwargs_from_configdict, ConfigValue from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning @@ -435,9 +435,9 @@ def _map_results(self, model, results): ] legacy_soln.status = legacy_solution_status_map[results.solution_status] legacy_results.solver.termination_message = str(results.termination_condition) - legacy_results.problem.number_of_constraints = model.nconstraints() - legacy_results.problem.number_of_variables = model.nvariables() - number_of_objectives = model.nobjectives() + legacy_results.problem.number_of_constraints = len(list(model.component_map(ctype=Constraint))) + legacy_results.problem.number_of_variables = len(list(model.component_map(ctype=Var))) + number_of_objectives = len(list(model.component_map(ctype=Objective))) legacy_results.problem.number_of_objectives = number_of_objectives if number_of_objectives == 1: obj = get_objective(model) From 617cc59a141a551e1e5d8b65551d7e7c734f4ac8 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 10:41:45 -0600 Subject: [PATCH 1041/3044] Apply black --- pyomo/contrib/solver/base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 12cb2e83918..91398ba5970 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -435,8 +435,12 @@ def _map_results(self, model, results): ] legacy_soln.status = legacy_solution_status_map[results.solution_status] legacy_results.solver.termination_message = str(results.termination_condition) - legacy_results.problem.number_of_constraints = len(list(model.component_map(ctype=Constraint))) - legacy_results.problem.number_of_variables = len(list(model.component_map(ctype=Var))) + legacy_results.problem.number_of_constraints = len( + list(model.component_map(ctype=Constraint)) + ) + legacy_results.problem.number_of_variables = len( + list(model.component_map(ctype=Var)) + ) number_of_objectives = len(list(model.component_map(ctype=Objective))) legacy_results.problem.number_of_objectives = number_of_objectives if number_of_objectives == 1: From 8c2bb52b8142bc2aa824fe20c0336cc8a5c62176 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 11:40:06 -0600 Subject: [PATCH 1042/3044] Update installation documentation to include Cython instructions --- doc/OnlineDocs/installation.rst | 35 ++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/doc/OnlineDocs/installation.rst b/doc/OnlineDocs/installation.rst index ecba05e13fb..2ed42f35f4e 100644 --- a/doc/OnlineDocs/installation.rst +++ b/doc/OnlineDocs/installation.rst @@ -12,7 +12,7 @@ version, Pyomo will remove testing for that Python version. Using CONDA ~~~~~~~~~~~ -We recommend installation with *conda*, which is included with the +We recommend installation with ``conda``, which is included with the Anaconda distribution of Python. You can install Pyomo in your system Python installation by executing the following in a shell: @@ -21,7 +21,7 @@ Python installation by executing the following in a shell: conda install -c conda-forge pyomo Optimization solvers are not installed with Pyomo, but some open source -optimization solvers can be installed with conda as well: +optimization solvers can be installed with ``conda`` as well: :: @@ -31,7 +31,7 @@ optimization solvers can be installed with conda as well: Using PIP ~~~~~~~~~ -The standard utility for installing Python packages is *pip*. You +The standard utility for installing Python packages is ``pip``. You can install Pyomo in your system Python installation by executing the following in a shell: @@ -43,14 +43,14 @@ the following in a shell: Conditional Dependencies ~~~~~~~~~~~~~~~~~~~~~~~~ -Extensions to Pyomo, and many of the contributions in `pyomo.contrib`, +Extensions to Pyomo, and many of the contributions in ``pyomo.contrib``, often have conditional dependencies on a variety of third-party Python packages including but not limited to: matplotlib, networkx, numpy, openpyxl, pandas, pint, pymysql, pyodbc, pyro4, scipy, sympy, and xlrd. A full list of conditional dependencies can be found in Pyomo's -`setup.py` and displayed using: +``setup.py`` and displayed using: :: @@ -72,3 +72,28 @@ with the standard Anaconda installation. You can check which Python packages you have installed using the command ``conda list`` or ``pip list``. Additional Python packages may be installed as needed. + + +Installation with Cython +~~~~~~~~~~~~~~~~~~~~~~~~ + +Users can opt to install Pyomo with +`cython `_ +initialized. + +.. note:: + This can only be done via ``pip`` or from source. + +Via ``pip``: + +:: + + pip install pyomo --global-option="--with-cython" + +From source: + +:: + + git clone https://github.com/Pyomo/pyomo.git + cd pyomo + python setup.py install --with-cython From e17f27559b01ddd07aed2011e68032289493d8fa Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 12:58:02 -0600 Subject: [PATCH 1043/3044] Add links to Pyomo Book Springer page --- README.md | 1 + doc/OnlineDocs/bibliography.rst | 2 ++ doc/OnlineDocs/tutorial_examples.rst | 17 ++++++++++------- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 12c3ce8ed9a..707f1a06c5a 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ version, we will remove testing for that Python version. ### Tutorials and Examples +* [Pyomo — Optimization Modeling in Python](https://link.springer.com/book/10.1007/978-3-030-68928-5) * [Pyomo Workshop Slides](https://github.com/Pyomo/pyomo-tutorials/blob/main/Pyomo-Workshop-December-2023.pdf) * [Prof. Jeffrey Kantor's Pyomo Cookbook](https://jckantor.github.io/ND-Pyomo-Cookbook/) * The [companion notebooks](https://mobook.github.io/MO-book/intro.html) diff --git a/doc/OnlineDocs/bibliography.rst b/doc/OnlineDocs/bibliography.rst index 6cbb96d3bfb..c12d3f81d8c 100644 --- a/doc/OnlineDocs/bibliography.rst +++ b/doc/OnlineDocs/bibliography.rst @@ -39,6 +39,8 @@ Bibliography John D. Siirola, Jean-Paul Watson, and David L. Woodruff. Pyomo - Optimization Modeling in Python, 3rd Edition. Vol. 67. Springer, 2021. + doi: `10.1007/978-3-030-68928-5 + `_ .. [PyomoJournal] William E. Hart, Jean-Paul Watson, David L. Woodruff. "Pyomo: modeling and solving mathematical programs in diff --git a/doc/OnlineDocs/tutorial_examples.rst b/doc/OnlineDocs/tutorial_examples.rst index 6a40949ef90..a18f9d77d42 100644 --- a/doc/OnlineDocs/tutorial_examples.rst +++ b/doc/OnlineDocs/tutorial_examples.rst @@ -3,15 +3,18 @@ Pyomo Tutorial Examples Additional Pyomo tutorials and examples can be found at the following links: -`Pyomo Workshop Slides and Exercises -`_ +* `Pyomo — Optimization Modeling in Python + `_ ([PyomoBookIII]_) -`Prof. Jeffrey Kantor's Pyomo Cookbook -`_ +* `Pyomo Workshop Slides and Exercises + `_ -The `companion notebooks `_ -for *Hands-On Mathematical Optimization with Python* +* `Prof. Jeffrey Kantor's Pyomo Cookbook + `_ -`Pyomo Gallery `_ +* The `companion notebooks `_ + for *Hands-On Mathematical Optimization with Python* + +* `Pyomo Gallery `_ From 399cbf54cae48803933316e856374adf7ddec766 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 13:04:51 -0600 Subject: [PATCH 1044/3044] Add recommendationg for advanced users --- doc/OnlineDocs/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/installation.rst b/doc/OnlineDocs/installation.rst index 2ed42f35f4e..83cd08e7a4a 100644 --- a/doc/OnlineDocs/installation.rst +++ b/doc/OnlineDocs/installation.rst @@ -90,7 +90,7 @@ Via ``pip``: pip install pyomo --global-option="--with-cython" -From source: +From source (recommended for advanced users only): :: From 9b00edd4105fcfbba775a4b85f485aa0cca40d3a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 14:11:58 -0600 Subject: [PATCH 1045/3044] Hack for model.solutions --- pyomo/contrib/solver/base.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 91398ba5970..c193b2c0789 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -468,7 +468,15 @@ def _solution_handler( """Method to handle the preferred action for the solution""" symbol_map = SymbolMap() symbol_map.default_labeler = NumericLabeler('x') - model.solutions.add_symbol_map(symbol_map) + try: + model.solutions.add_symbol_map(symbol_map) + except AttributeError: + # Something wacky happens in IDAES due to the usage of ScalarBlock + # instead of PyomoModel. This is an attempt to fix that. + from pyomo.core.base.PyomoModel import ModelSolutions + + setattr(model.solutions, ModelSolutions()) + model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) delete_legacy_soln = True if load_solutions: From ac64a5a552a8feede8624ce0a7d952fdea52e2c2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 15:01:32 -0600 Subject: [PATCH 1046/3044] Typo: would be nice if setattr was used correctly --- pyomo/contrib/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index c193b2c0789..cdf58416659 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -475,7 +475,7 @@ def _solution_handler( # instead of PyomoModel. This is an attempt to fix that. from pyomo.core.base.PyomoModel import ModelSolutions - setattr(model.solutions, ModelSolutions()) + setattr(model, 'solutions', ModelSolutions()) model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) delete_legacy_soln = True From ca793605d99036c5513d58dad6d67a31ce2e2eba Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 25 Mar 2024 15:11:55 -0600 Subject: [PATCH 1047/3044] Need an instance in there --- pyomo/contrib/solver/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index cdf58416659..07efbaed449 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -475,7 +475,7 @@ def _solution_handler( # instead of PyomoModel. This is an attempt to fix that. from pyomo.core.base.PyomoModel import ModelSolutions - setattr(model, 'solutions', ModelSolutions()) + setattr(model, 'solutions', ModelSolutions(model)) model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) delete_legacy_soln = True From f1177a287c2cf3dd0c2d9a79818209d49b1c533f Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 25 Mar 2024 16:12:52 -0600 Subject: [PATCH 1048/3044] dont draw box around DM subsystems if they are empty --- pyomo/contrib/incidence_analysis/visualize.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/visualize.py b/pyomo/contrib/incidence_analysis/visualize.py index 05c0661070a..8cefbcf61c9 100644 --- a/pyomo/contrib/incidence_analysis/visualize.py +++ b/pyomo/contrib/incidence_analysis/visualize.py @@ -186,10 +186,16 @@ def spy_dulmage_mendelsohn( nv = sum(len(vb) for vb in vblocks) nc = sum(len(cb) for cb in cblocks) stop = (start[0] + nv - 1, start[1] + nc - 1) - if not (i == 1 and skip_wellconstrained): + if ( + not (i == 1 and skip_wellconstrained) + and nv > 0 and nc > 0 + ): # Regardless of whether we are plotting in upper or lower # triangular order, the well-constrained subsystem is at # position 1 + # + # The get-rectangle function doesn't look good if we give it + # an "empty region" to box. ax.add_patch( _get_rectangle_around_coords(start, stop, linewidth=linewidth) ) @@ -202,6 +208,7 @@ def spy_dulmage_mendelsohn( start = (0, 0) for vb, cb in zip(vpart_fine, cpart_fine): stop = (start[0] + len(vb) - 1, start[1] + len(cb) - 1) + # Note that the subset's we're boxing here can't be empty. ax.add_patch( _get_rectangle_around_coords( start, stop, linestyle=linestyle, linewidth=linewidth From 6b94db6177424f0a33ebdf14514369ed02ece50e Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 25 Mar 2024 17:51:39 -0600 Subject: [PATCH 1049/3044] reformat --- pyomo/contrib/incidence_analysis/visualize.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/visualize.py b/pyomo/contrib/incidence_analysis/visualize.py index 8cefbcf61c9..9360d8ddfc6 100644 --- a/pyomo/contrib/incidence_analysis/visualize.py +++ b/pyomo/contrib/incidence_analysis/visualize.py @@ -186,10 +186,7 @@ def spy_dulmage_mendelsohn( nv = sum(len(vb) for vb in vblocks) nc = sum(len(cb) for cb in cblocks) stop = (start[0] + nv - 1, start[1] + nc - 1) - if ( - not (i == 1 and skip_wellconstrained) - and nv > 0 and nc > 0 - ): + if not (i == 1 and skip_wellconstrained) and nv > 0 and nc > 0: # Regardless of whether we are plotting in upper or lower # triangular order, the well-constrained subsystem is at # position 1 From 07df0c69a9d62e0174d94077f3fff323bea53c87 Mon Sep 17 00:00:00 2001 From: "Philipp Christophel (phchri)" Date: Tue, 26 Mar 2024 09:17:14 -0400 Subject: [PATCH 1050/3044] Use TeeStream, simplify _apply_solver and other small fixes --- pyomo/solvers/plugins/solvers/SAS.py | 392 +++++++++++++------------ pyomo/solvers/tests/checks/test_SAS.py | 11 + 2 files changed, 208 insertions(+), 195 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/SAS.py b/pyomo/solvers/plugins/solvers/SAS.py index bd06f6a1ef7..bccb7d34077 100644 --- a/pyomo/solvers/plugins/solvers/SAS.py +++ b/pyomo/solvers/plugins/solvers/SAS.py @@ -1,13 +1,20 @@ -__all__ = ["SAS"] +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ import logging import sys from os import stat import uuid - -from io import StringIO from abc import ABC, abstractmethod -from contextlib import redirect_stdout +from io import StringIO from pyomo.opt.base import ProblemFormat, ResultsFormat, OptSolver from pyomo.opt.base.solvers import SolverFactory @@ -23,6 +30,8 @@ from pyomo.core.base import Var from pyomo.core.base.block import _BlockData from pyomo.core.kernel.block import IBlock +from pyomo.common.log import LogStream +from pyomo.common.tee import capture_output, TeeStream logger = logging.getLogger("pyomo.solvers") @@ -252,6 +261,7 @@ class SAS94(SASAbc): """ Solver interface for SAS 9.4 using saspy. See the saspy documentation about how to create a connection. + The swat connection options can be specified on the SolverFactory call. """ def __init__(self, **kwds): @@ -541,37 +551,12 @@ def _apply_solver(self): return Bunch(rc=self._rc, log=self._log) -class SASLogWriter: - """Helper class to take the log from stdout and put it also in a StringIO.""" - - def __init__(self, tee): - """Set up the two outputs.""" - self.tee = tee - self._log = StringIO() - self.stdout = sys.stdout - - def write(self, message): - """If the tee options is specified, write to both outputs.""" - if self.tee: - self.stdout.write(message) - self._log.write(message) - - def flush(self): - """Nothing to do, just here for compatibility reasons.""" - # Do nothing since we flush right away - pass - - def log(self): - """ "Get the log as a string.""" - return self._log.getvalue() - - @SolverFactory.register("_sascas", doc="SAS Viya CAS Server interface") class SASCAS(SASAbc): """ Solver interface connection to a SAS Viya CAS server using swat. See the documentation for the swat package about how to create a connection. - The swat connection options can be passed as options to the solve function. + The swat connection options can be specified on the SolverFactory call. """ def __init__(self, **kwds): @@ -600,6 +585,106 @@ def __del__(self): if self._sas_session: self._sas_session.close() + def _uploadMpsFile(self, s, unique): + # Declare a unique table name for the mps table + mpsdata_table_name = "mps" + unique + + # Upload mps file to CAS, if the file is larger than 2 GB, we need to use convertMps instead of loadMps + # Note that technically it is 2 Gibibytes file size that trigger the issue, but 2 GB is the safer threshold + if stat(self._problem_files[0]).st_size > 2e9: + # For files larger than 2 GB (this is a limitation of the loadMps action used in the else part). + # Use convertMPS, first create file for upload. + mpsWithIdFileName = TempfileManager.create_tempfile(".mps.csv", text=True) + with open(mpsWithIdFileName, "w") as mpsWithId: + mpsWithId.write("_ID_\tText\n") + with open(self._problem_files[0], "r") as f: + id = 0 + for line in f: + id += 1 + mpsWithId.write(str(id) + "\t" + line.rstrip() + "\n") + + # Upload .mps.csv file + mpscsv_table_name = "csv" + unique + s.upload_file( + mpsWithIdFileName, + casout={"name": mpscsv_table_name, "replace": True}, + importoptions={"filetype": "CSV", "delimiter": "\t"}, + ) + + # Convert .mps.csv file to .mps + s.optimization.convertMps( + data=mpscsv_table_name, + casOut={"name": mpsdata_table_name, "replace": True}, + format="FREE", + ) + + # Delete the table we don't need anymore + if mpscsv_table_name: + s.dropTable(name=mpscsv_table_name, quiet=True) + else: + # For small files (less than 2 GB), use loadMps + with open(self._problem_files[0], "r") as mps_file: + s.optimization.loadMps( + mpsFileString=mps_file.read(), + casout={"name": mpsdata_table_name, "replace": True}, + format="FREE", + ) + return mpsdata_table_name + + def _uploadPrimalin(self, s, unique): + # Upload warmstart file to CAS with a unique name + primalin_table_name = "pin" + unique + s.upload_file( + self._warm_start_file_name, + casout={"name": primalin_table_name, "replace": True}, + importoptions={"filetype": "CSV"}, + ) + self.options["primalin"] = primalin_table_name + return primalin_table_name + + def _retrieveSolution( + self, s, r, results, action, primalout_table_name, dualout_table_name + ): + # Create solution + sol = results.solution.add() + + # Store status in solution + sol.status = SolutionStatus.feasible + sol.termination_condition = SOLSTATUS_TO_TERMINATIONCOND[ + r.get("solutionStatus", "ERROR") + ] + + # Store objective value in solution + sol.objective["__default_objective__"] = {"Value": r["objective"]} + + if action == "solveMilp": + primal_out = s.CASTable(name=primalout_table_name) + # Use pandas functions for efficiency + primal_out = primal_out[["_VAR_", "_VALUE_"]] + sol.variable = {} + for row in primal_out.itertuples(index=False): + sol.variable[row[0]] = {"Value": row[1]} + else: + # Convert primal out data set to variable dictionary + # Use panda functions for efficiency + primal_out = s.CASTable(name=primalout_table_name) + primal_out = primal_out[["_VAR_", "_VALUE_", "_STATUS_", "_R_COST_"]] + sol.variable = {} + for row in primal_out.itertuples(index=False): + sol.variable[row[0]] = {"Value": row[1], "Status": row[2], "rc": row[3]} + + # Convert dual out data set to constraint dictionary + # Use pandas functions for efficiency + dual_out = s.CASTable(name=dualout_table_name) + dual_out = dual_out[["_ROW_", "_VALUE_", "_STATUS_", "_ACTIVITY_"]] + sol.constraint = {} + for row in dual_out.itertuples(index=False): + sol.constraint[row[0]] = { + "dual": row[1], + "Status": row[2], + "slack": row[3], + } + def _apply_solver(self): """ "Prepare the options and run the solver. Then store the data to be returned.""" logger.debug("Running SAS Viya") @@ -620,175 +705,92 @@ def _apply_solver(self): # Get a unique identifier, always use the same with different prefixes unique = uuid.uuid4().hex[:16] + # Creat the output stream, we want to print to a log string as well as to the console + self._log = StringIO() + ostreams = [LogStream(level=logging.INFO, logger=logger)] + ostreams.append(self._log) + if self._tee: + ostreams.append(sys.stdout) + # Connect to CAS server - with redirect_stdout(SASLogWriter(self._tee)) as self._log_writer: - s = self._sas_session - if s == None: - s = self._sas_session = self._sas.CAS(**self._session_options) - try: - # Load the optimization action set - s.loadactionset("optimization") - - # Declare a unique table name for the mps table - mpsdata_table_name = "mps" + unique - - # Upload mps file to CAS, if the file is larger than 2 GB, we need to use convertMps instead of loadMps - # Note that technically it is 2 Gibibytes file size that trigger the issue, but 2 GB is the safer threshold - if stat(self._problem_files[0]).st_size > 2e9: - # For files larger than 2 GB (this is a limitation of the loadMps action used in the else part). - # Use convertMPS, first create file for upload. - mpsWithIdFileName = TempfileManager.create_tempfile( - ".mps.csv", text=True - ) - with open(mpsWithIdFileName, "w") as mpsWithId: - mpsWithId.write("_ID_\tText\n") - with open(self._problem_files[0], "r") as f: - id = 0 - for line in f: - id += 1 - mpsWithId.write(str(id) + "\t" + line.rstrip() + "\n") - - # Upload .mps.csv file - mpscsv_table_name = "csv" + unique - s.upload_file( - mpsWithIdFileName, - casout={"name": mpscsv_table_name, "replace": True}, - importoptions={"filetype": "CSV", "delimiter": "\t"}, - ) - - # Convert .mps.csv file to .mps - s.optimization.convertMps( - data=mpscsv_table_name, - casOut={"name": mpsdata_table_name, "replace": True}, - format="FREE", - ) - - # Delete the table we don't need anymore - if mpscsv_table_name: - s.dropTable(name=mpscsv_table_name, quiet=True) - else: - # For small files (less than 2 GB), use loadMps - with open(self._problem_files[0], "r") as mps_file: - s.optimization.loadMps( - mpsFileString=mps_file.read(), - casout={"name": mpsdata_table_name, "replace": True}, - format="FREE", + with TeeStream(*ostreams) as t: + with capture_output(output=t.STDOUT, capture_fd=False): + s = self._sas_session + if s == None: + s = self._sas_session = self._sas.CAS(**self._session_options) + try: + # Load the optimization action set + s.loadactionset("optimization") + + mpsdata_table_name = self._uploadMpsFile(s, unique) + + primalin_table_name = None + if self.warmstart_flag: + primalin_table_name = self._uploadPrimalin(s, unique) + + # Define output table names + primalout_table_name = "pout" + unique + dualout_table_name = None + + # Solve the problem in CAS + if action == "solveMilp": + r = s.optimization.solveMilp( + data={"name": mpsdata_table_name}, + primalOut={"name": primalout_table_name, "replace": True}, + **self.options + ) + else: + dualout_table_name = "dout" + unique + r = s.optimization.solveLp( + data={"name": mpsdata_table_name}, + primalOut={"name": primalout_table_name, "replace": True}, + dualOut={"name": dualout_table_name, "replace": True}, + **self.options ) - primalin_table_name = None - if self.warmstart_flag: - primalin_table_name = "pin" + unique - # Upload warmstart file to CAS - s.upload_file( - self._warm_start_file_name, - casout={"name": primalin_table_name, "replace": True}, - importoptions={"filetype": "CSV"}, - ) - self.options["primalin"] = primalin_table_name - - # Define output table names - primalout_table_name = "pout" + unique - dualout_table_name = None - - # Solve the problem in CAS - if action == "solveMilp": - r = s.optimization.solveMilp( - data={"name": mpsdata_table_name}, - primalOut={"name": primalout_table_name, "replace": True}, - **self.options - ) - else: - dualout_table_name = "dout" + unique - r = s.optimization.solveLp( - data={"name": mpsdata_table_name}, - primalOut={"name": primalout_table_name, "replace": True}, - dualOut={"name": dualout_table_name, "replace": True}, - **self.options - ) - - # Prepare the solver results - if r: - # Get back the primal and dual solution data sets - results = self.results = self._create_results_from_status( - r.get("status", "ERROR"), r.get("solutionStatus", "ERROR") - ) - - if results.solver.status != SolverStatus.error: - if r.ProblemSummary["cValue1"][1] == "Maximization": - results.problem.sense = ProblemSense.maximize - else: - results.problem.sense = ProblemSense.minimize - - # Prepare the solution information - if results.solver.hasSolution: - sol = results.solution.add() - - # Store status in solution - sol.status = SolutionStatus.feasible - sol.termination_condition = SOLSTATUS_TO_TERMINATIONCOND[ - r.get("solutionStatus", "ERROR") - ] - - # Store objective value in solution - sol.objective["__default_objective__"] = { - "Value": r["objective"] - } - - if action == "solveMilp": - primal_out = s.CASTable(name=primalout_table_name) - # Use pandas functions for efficiency - primal_out = primal_out[["_VAR_", "_VALUE_"]] - sol.variable = {} - for row in primal_out.itertuples(index=False): - sol.variable[row[0]] = {"Value": row[1]} + # Prepare the solver results + if r: + # Get back the primal and dual solution data sets + results = self.results = self._create_results_from_status( + r.get("status", "ERROR"), r.get("solutionStatus", "ERROR") + ) + + if results.solver.status != SolverStatus.error: + if r.ProblemSummary["cValue1"][1] == "Maximization": + results.problem.sense = ProblemSense.maximize else: - # Convert primal out data set to variable dictionary - # Use panda functions for efficiency - primal_out = s.CASTable(name=primalout_table_name) - primal_out = primal_out[ - ["_VAR_", "_VALUE_", "_STATUS_", "_R_COST_"] - ] - sol.variable = {} - for row in primal_out.itertuples(index=False): - sol.variable[row[0]] = { - "Value": row[1], - "Status": row[2], - "rc": row[3], - } - - # Convert dual out data set to constraint dictionary - # Use pandas functions for efficiency - dual_out = s.CASTable(name=dualout_table_name) - dual_out = dual_out[ - ["_ROW_", "_VALUE_", "_STATUS_", "_ACTIVITY_"] - ] - sol.constraint = {} - for row in dual_out.itertuples(index=False): - sol.constraint[row[0]] = { - "dual": row[1], - "Status": row[2], - "slack": row[3], - } + results.problem.sense = ProblemSense.minimize + + # Prepare the solution information + if results.solver.hasSolution: + self._retrieveSolution( + s, + r, + results, + action, + primalout_table_name, + dualout_table_name, + ) + else: + raise ValueError("The SAS solver returned an error status.") else: - raise ValueError("The SAS solver returned an error status.") - else: - results = self.results = SolverResults() - results.solver.name = "SAS" - results.solver.status = SolverStatus.error - raise ValueError( - "An option passed to the SAS solver caused a syntax error." - ) - - finally: - if mpsdata_table_name: - s.dropTable(name=mpsdata_table_name, quiet=True) - if primalin_table_name: - s.dropTable(name=primalin_table_name, quiet=True) - if primalout_table_name: - s.dropTable(name=primalout_table_name, quiet=True) - if dualout_table_name: - s.dropTable(name=dualout_table_name, quiet=True) - - self._log = self._log_writer.log() + results = self.results = SolverResults() + results.solver.name = "SAS" + results.solver.status = SolverStatus.error + raise ValueError( + "An option passed to the SAS solver caused a syntax error." + ) + + finally: + if mpsdata_table_name: + s.dropTable(name=mpsdata_table_name, quiet=True) + if primalin_table_name: + s.dropTable(name=primalin_table_name, quiet=True) + if primalout_table_name: + s.dropTable(name=primalout_table_name, quiet=True) + if dualout_table_name: + s.dropTable(name=dualout_table_name, quiet=True) + + self._log = self._log.getvalue() self._rc = 0 return Bunch(rc=self._rc, log=self._log) diff --git a/pyomo/solvers/tests/checks/test_SAS.py b/pyomo/solvers/tests/checks/test_SAS.py index 922209ef88b..75534e0e001 100644 --- a/pyomo/solvers/tests/checks/test_SAS.py +++ b/pyomo/solvers/tests/checks/test_SAS.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import os import pyomo.common.unittest as unittest from unittest import mock From 53635a9bbb1817ef2d3e4881779736cdabd87345 Mon Sep 17 00:00:00 2001 From: Eslick Date: Tue, 26 Mar 2024 10:56:25 -0400 Subject: [PATCH 1051/3044] Fix black and substring issue --- pyomo/solvers/plugins/solvers/ASL.py | 3 ++- pyomo/solvers/plugins/solvers/IPOPT.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/ASL.py b/pyomo/solvers/plugins/solvers/ASL.py index 3ebe5c3b422..38a9fc1df58 100644 --- a/pyomo/solvers/plugins/solvers/ASL.py +++ b/pyomo/solvers/plugins/solvers/ASL.py @@ -160,8 +160,9 @@ def create_command_line(self, executable, problem_files): # if 'PYOMO_AMPLFUNC' in env: if 'AMPLFUNC' in env: + existing = set(env['AMPLFUNC'].split("\n")) for line in env['PYOMO_AMPLFUNC'].split('\n'): - if line not in env['AMPLFUNC']: + if line not in existing: env['AMPLFUNC'] += "\n" + line else: env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index 17b68da6364..8f5190a4a07 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -121,8 +121,9 @@ def create_command_line(self, executable, problem_files): # if 'PYOMO_AMPLFUNC' in env: if 'AMPLFUNC' in env: + existing = set(env['AMPLFUNC'].split("\n")) for line in env['PYOMO_AMPLFUNC'].split('\n'): - if line not in env['AMPLFUNC']: + if line not in existing: env['AMPLFUNC'] += "\n" + line else: env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] From dd3bb6a3adddec75c432505fac42b6e987bd4bd8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 26 Mar 2024 14:07:05 -0600 Subject: [PATCH 1052/3044] Adding a test for the bug --- pyomo/gdp/tests/test_bigm.py | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index c6ac49f6d36..79ad24ae782 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -19,8 +19,11 @@ Set, Constraint, ComponentMap, + LogicalConstraint, + Objective, SolverFactory, Suffix, + TerminationCondition, ConcreteModel, Var, Any, @@ -2193,6 +2196,43 @@ def test_decl_order_opposite_instantiation_order(self): def test_do_not_assume_nested_indicators_local(self): ct.check_do_not_assume_nested_indicators_local(self, 'gdp.bigm') + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + def test_constraints_not_enforced_when_an_ancestor_indicator_is_False(self): + m = ConcreteModel() + m.x = Var(bounds=(0, 30)) + + m.left = Disjunct() + m.left.left = Disjunct() + m.left.left.c = Constraint(expr=m.x >= 10) + m.left.right = Disjunct() + m.left.right.c = Constraint(expr=m.x >= 9) + m.left.disjunction = Disjunction(expr=[m.left.left, m.left.right]) + m.right = Disjunct() + m.right.left = Disjunct() + m.right.left.c = Constraint(expr=m.x >= 11) + m.right.right = Disjunct() + m.right.right.c = Constraint(expr=m.x >= 8) + m.right.disjunction = Disjunction(expr=[m.right.left, m.right.right]) + m.disjunction = Disjunction(expr=[m.left, m.right]) + + m.equiv_left = LogicalConstraint(expr=m.left.left.indicator_var.equivalent_to( + m.right.left.indicator_var)) + m.equiv_right = LogicalConstraint(expr=m.left.right.indicator_var.equivalent_to( + m.right.right.indicator_var)) + + m.obj = Objective(expr=m.x) + + TransformationFactory('gdp.bigm').apply_to(m) + results = SolverFactory('gurobi').solve(m) + self.assertEqual(results.solver.termination_condition, + TerminationCondition.optimal) + self.assertTrue(value(m.right.indicator_var)) + self.assertFalse(value(m.left.indicator_var)) + self.assertTrue(value(m.right.right.indicator_var)) + self.assertFalse(value(m.right.left.indicator_var)) + self.assertTrue(value(m.left.right.indicator_var)) + self.assertAlmostEqual(value(m.x), 8) + class IndexedDisjunction(unittest.TestCase): # this tests that if the targets are a subset of the From ff67f852c02068f01f4140305383867f8774f75d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 26 Mar 2024 14:31:00 -0600 Subject: [PATCH 1053/3044] Generalizing how nested Constraints are relaxed in bigm so that they aren't enforced if any parent indicator_var is False --- pyomo/gdp/plugins/bigm.py | 28 ++++++++++++++++++---------- pyomo/gdp/plugins/bigm_mixin.py | 9 ++++++--- pyomo/gdp/util.py | 4 ++-- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index 3f450dbbd4f..118fa6935d7 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -217,17 +217,16 @@ def _apply_to_impl(self, instance, **kwds): t, t.index(), bigM, - parent_disjunct=gdp_tree.parent(t), - root_disjunct=gdp_tree.root_disjunct(t), + gdp_tree, ) # issue warnings about anything that was in the bigM args dict that we # didn't use _warn_for_unused_bigM_args(bigM, self.used_args, logger) - def _transform_disjunctionData( - self, obj, index, bigM, parent_disjunct=None, root_disjunct=None - ): + def _transform_disjunctionData(self, obj, index, bigM, gdp_tree): + parent_disjunct = gdp_tree.parent(obj) + root_disjunct = gdp_tree.root_disjunct(obj) (transBlock, xorConstraint) = self._setup_transform_disjunctionData( obj, root_disjunct ) @@ -236,7 +235,7 @@ def _transform_disjunctionData( or_expr = 0 for disjunct in obj.disjuncts: or_expr += disjunct.binary_indicator_var - self._transform_disjunct(disjunct, bigM, transBlock) + self._transform_disjunct(disjunct, bigM, transBlock, gdp_tree) if obj.xor: xorConstraint[index] = or_expr == 1 @@ -249,7 +248,7 @@ def _transform_disjunctionData( # and deactivate for the writers obj.deactivate() - def _transform_disjunct(self, obj, bigM, transBlock): + def _transform_disjunct(self, obj, bigM, transBlock, gdp_tree): # We're not using the preprocessed list here, so this could be # inactive. We've already done the error checking in preprocessing, so # we just skip it here. @@ -261,6 +260,12 @@ def _transform_disjunct(self, obj, bigM, transBlock): relaxationBlock = self._get_disjunct_transformation_block(obj, transBlock) + indicator_expression = 0 + node = obj + while node is not None: + indicator_expression += 1 - node.binary_indicator_var + node = gdp_tree.parent_disjunct(node) + # This is crazy, but if the disjunction has been previously # relaxed, the disjunct *could* be deactivated. This is a big # deal for Hull, as it uses the component_objects / @@ -270,13 +275,15 @@ def _transform_disjunct(self, obj, bigM, transBlock): # comparing the two relaxations. # # Transform each component within this disjunct - self._transform_block_components(obj, obj, bigM, arg_list, suffix_list) + self._transform_block_components(obj, obj, bigM, arg_list, suffix_list, + indicator_expression) # deactivate disjunct to keep the writers happy obj._deactivate_without_fixing_indicator() def _transform_constraint( - self, obj, disjunct, bigMargs, arg_list, disjunct_suffix_list + self, obj, disjunct, bigMargs, arg_list, disjunct_suffix_list, + indicator_expression ): # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() @@ -348,7 +355,8 @@ def _transform_constraint( bigm_src[c] = (lower, upper) self._add_constraint_expressions( - c, i, M, disjunct.binary_indicator_var, newConstraint, constraint_map + c, i, M, disjunct.binary_indicator_var, newConstraint, constraint_map, + indicator_expression=indicator_expression ) # deactivate because we relaxed diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index 510b36b5102..300509d81f8 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -232,7 +232,8 @@ def _estimate_M(self, expr, constraint): return tuple(M) def _add_constraint_expressions( - self, c, i, M, indicator_var, newConstraint, constraint_map + self, c, i, M, indicator_var, newConstraint, constraint_map, + indicator_expression=None ): # Since we are both combining components from multiple blocks and using # local names, we need to make sure that the first index for @@ -244,6 +245,8 @@ def _add_constraint_expressions( # over the constraint indices, but I don't think it matters a lot.) unique = len(newConstraint) name = c.local_name + "_%s" % unique + if indicator_expression is None: + indicator_expression = 1 - indicator_var if c.lower is not None: if M[0] is None: @@ -251,7 +254,7 @@ def _add_constraint_expressions( "Cannot relax disjunctive constraint '%s' " "because M is not defined." % name ) - M_expr = M[0] * (1 - indicator_var) + M_expr = M[0] * indicator_expression newConstraint.add((name, i, 'lb'), c.lower <= c.body - M_expr) constraint_map.transformed_constraints[c].append( newConstraint[name, i, 'lb'] @@ -263,7 +266,7 @@ def _add_constraint_expressions( "Cannot relax disjunctive constraint '%s' " "because M is not defined." % name ) - M_expr = M[1] * (1 - indicator_var) + M_expr = M[1] * indicator_expression newConstraint.add((name, i, 'ub'), c.body - M_expr <= c.upper) constraint_map.transformed_constraints[c].append( newConstraint[name, i, 'ub'] diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index fe11975954d..a8c6393f0b3 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -144,13 +144,13 @@ def parent(self, u): Arg: u : A node in the tree """ + if u in self._parent: + return self._parent[u] if u not in self._vertices: raise ValueError( "'%s' is not a vertex in the GDP tree. Cannot " "retrieve its parent." % u ) - if u in self._parent: - return self._parent[u] else: return None From e062625ba66012efcbd7ae6b0b4c849cb623456f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 26 Mar 2024 14:53:33 -0600 Subject: [PATCH 1054/3044] Fixing the first couple tests I broke --- pyomo/gdp/tests/test_bigm.py | 66 ++++++++++++++---------------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index 79ad24ae782..8d0fa8bd633 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -2091,35 +2091,6 @@ def innerIndexed(d, i): m._pyomo_gdp_bigm_reformulation.relaxedDisjuncts, ) - def check_first_disjunct_constraint(self, disj1c, x, ind_var): - self.assertEqual(len(disj1c), 1) - cons = disj1c[0] - self.assertIsNone(cons.lower) - self.assertEqual(cons.upper, 1) - repn = generate_standard_repn(cons.body) - self.assertTrue(repn.is_quadratic()) - self.assertEqual(len(repn.linear_vars), 1) - self.assertEqual(len(repn.quadratic_vars), 4) - ct.check_linear_coef(self, repn, ind_var, 143) - self.assertEqual(repn.constant, -143) - for i in range(1, 5): - ct.check_squared_term_coef(self, repn, x[i], 1) - - def check_second_disjunct_constraint(self, disj2c, x, ind_var): - self.assertEqual(len(disj2c), 1) - cons = disj2c[0] - self.assertIsNone(cons.lower) - self.assertEqual(cons.upper, 1) - repn = generate_standard_repn(cons.body) - self.assertTrue(repn.is_quadratic()) - self.assertEqual(len(repn.linear_vars), 5) - self.assertEqual(len(repn.quadratic_vars), 4) - self.assertEqual(repn.constant, -63) # M = 99, so this is 36 - 99 - ct.check_linear_coef(self, repn, ind_var, 99) - for i in range(1, 5): - ct.check_squared_term_coef(self, repn, x[i], 1) - ct.check_linear_coef(self, repn, x[i], -6) - def simplify_cons(self, cons, leq): visitor = LinearRepnVisitor({}, {}, {}, None) repn = visitor.walk_expression(cons.body) @@ -2145,30 +2116,45 @@ def check_hierarchical_nested_model(self, m, bigm): # outer disjunction constraints disj1c = bigm.get_transformed_constraints(m.disj1.c) - self.check_first_disjunct_constraint(disj1c, m.x, m.disj1.binary_indicator_var) + self.assertEqual(len(disj1c), 1) + cons = disj1c[0] + assertExpressionsEqual( + self, + cons.expr, + m.x[1]**2 + m.x[2]**2 + m.x[3]**2 + m.x[4]**2 - 143.0*(1 - m.disj1.binary_indicator_var) <= 1.0 + ) disj2c = bigm.get_transformed_constraints(m.disjunct_block.disj2.c) - self.check_second_disjunct_constraint( - disj2c, m.x, m.disjunct_block.disj2.binary_indicator_var + self.assertEqual(len(disj2c), 1) + cons = disj2c[0] + cons.pprint() + assertExpressionsEqual( + self, + cons.expr, + (3 - m.x[1])**2 + (3 - m.x[2])**2 + (3 - m.x[3])**2 + (3 - m.x[4])**2 - 99.0*(1 - m.disjunct_block.disj2.binary_indicator_var) <= 1.0 ) # inner disjunction constraints innerd1c = bigm.get_transformed_constraints( m.disjunct_block.disj2.disjunction_disjuncts[0].constraint[1] ) - self.check_first_disjunct_constraint( - innerd1c, - m.x, - m.disjunct_block.disj2.disjunction_disjuncts[0].binary_indicator_var, + self.assertEqual(len(innerd1c), 1) + cons = innerd1c[0] + assertExpressionsEqual( + self, + cons.expr, + m.x[1]**2 + m.x[2]**2 + m.x[3]**2 + m.x[4]**2 - 143.0*(1 - m.disjunct_block.disj2.disjunction_disjuncts[0].binary_indicator_var + 1 - m.disjunct_block.disj2.binary_indicator_var) <= 1.0 ) innerd2c = bigm.get_transformed_constraints( m.disjunct_block.disj2.disjunction_disjuncts[1].constraint[1] ) - self.check_second_disjunct_constraint( - innerd2c, - m.x, - m.disjunct_block.disj2.disjunction_disjuncts[1].binary_indicator_var, + self.assertEqual(len(innerd2c), 1) + cons = innerd2c[0] + assertExpressionsEqual( + self, + cons.expr, + (3 - m.x[1])**2 + (3 - m.x[2])**2 + (3 - m.x[3])**2 + (3 - m.x[4])**2 - 99.0*(1 - m.disjunct_block.disj2.disjunction_disjuncts[1].binary_indicator_var + 1 - m.disjunct_block.disj2.binary_indicator_var) <= 1.0 ) def test_hierarchical_badly_ordered_targets(self): From 01f7ebe58af5ded0325b028c462906d8aa2c4179 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 26 Mar 2024 16:14:05 -0600 Subject: [PATCH 1055/3044] require variables and constraints to be specified separately in `remove_nodes`; update to raise error on invalid components --- pyomo/contrib/incidence_analysis/interface.py | 55 +++++++++++++++---- .../tests/test_interface.py | 33 +++++++---- 2 files changed, 65 insertions(+), 23 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 50cb84daaf5..0ed9b34b0f8 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -453,11 +453,29 @@ def _validate_input(self, variables, constraints): raise ValueError("Neither variables nor a model have been provided.") else: variables = self.variables + elif self._incidence_graph is not None: + # If variables were provided and an incidence graph is cached, + # make sure the provided variables exist in the graph. + for var in variables: + if var not in self._var_index_map: + raise KeyError( + f"Variable {var} does not exist in the cached" + " incidence graph." + ) if constraints is None: if self._incidence_graph is None: raise ValueError("Neither constraints nor a model have been provided.") else: constraints = self.constraints + elif self._incidence_graph is not None: + # If constraints were provided and an incidence graph is cached, + # make sure the provided constraints exist in the graph. + for con in constraints: + if con not in self._con_index_map: + raise KeyError( + f"Constraint {con} does not exist in the cached" + " incidence graph." + ) _check_unindexed(variables + constraints) return variables, constraints @@ -854,7 +872,7 @@ def dulmage_mendelsohn(self, variables=None, constraints=None): # Hopefully this does not get too confusing... return var_partition, con_partition - def remove_nodes(self, nodes, constraints=None): + def remove_nodes(self, variables=None, constraints=None): """Removes the specified variables and constraints (columns and rows) from the cached incidence matrix. @@ -866,35 +884,48 @@ def remove_nodes(self, nodes, constraints=None): Parameters ---------- - nodes: list - VarData or ConData objects whose columns or rows will be - removed from the incidence matrix. + variables: list + VarData objects whose nodes will be removed from the incidence graph constraints: list - VarData or ConData objects whose columns or rows will be - removed from the incidence matrix. + ConData objects whose nodes will be removed from the incidence graph + + .. note:: + + **Breaking change in Pyomo vTBD** + + The pre-TBD implementation of ``remove_nodes`` allowed variables and + constraints to remove to be specified in a single list. This made + error checking difficult, and indeed, if invalid components were + provided, we carried on silently instead of throwing an error or + warning. As part of a fix to raise an error if an invalid component + (one that is not part of the incidence graph) is provided, we now require + variables and constraints to be specified separately. """ if constraints is None: constraints = [] + if variables is None: + variables = [] if self._incidence_graph is None: raise RuntimeError( "Attempting to remove variables and constraints from cached " "incidence matrix,\nbut no incidence matrix has been cached." ) - to_exclude = ComponentSet(nodes) - to_exclude.update(constraints) - vars_to_include = [v for v in self.variables if v not in to_exclude] - cons_to_include = [c for c in self.constraints if c not in to_exclude] + variables, constraints = self._validate_input(variables, constraints) + v_exclude = ComponentSet(variables) + c_exclude = ComponentSet(constraints) + vars_to_include = [v for v in self.variables if v not in v_exclude] + cons_to_include = [c for c in self.constraints if c not in c_exclude] incidence_graph = self._extract_subgraph(vars_to_include, cons_to_include) # update attributes self._variables = vars_to_include self._constraints = cons_to_include self._incidence_graph = incidence_graph self._var_index_map = ComponentMap( - (var, i) for i, var in enumerate(self.variables) + (var, i) for i, var in enumerate(vars_to_include) ) self._con_index_map = ComponentMap( - (con, i) for i, con in enumerate(self._constraints) + (con, i) for i, con in enumerate(cons_to_include) ) def plot(self, variables=None, constraints=None, title=None, show=True): diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 4b77d60d8ba..3b2439ed2af 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -634,17 +634,15 @@ def test_exception(self): nlp = PyomoNLP(model) igraph = IncidenceGraphInterface(nlp) - with self.assertRaises(RuntimeError) as exc: + with self.assertRaisesRegex(KeyError, "does not exist"): variables = [model.P] constraints = [model.ideal_gas] igraph.maximum_matching(variables, constraints) - self.assertIn("must be unindexed", str(exc.exception)) - with self.assertRaises(RuntimeError) as exc: + with self.assertRaisesRegex(KeyError, "does not exist"): variables = [model.P] constraints = [model.ideal_gas] igraph.block_triangularize(variables, constraints) - self.assertIn("must be unindexed", str(exc.exception)) @unittest.skipUnless(networkx_available, "networkx is not available.") @@ -885,17 +883,15 @@ def test_exception(self): model = make_gas_expansion_model() igraph = IncidenceGraphInterface(model) - with self.assertRaises(RuntimeError) as exc: + with self.assertRaisesRegex(KeyError, "does not exist"): variables = [model.P] constraints = [model.ideal_gas] igraph.maximum_matching(variables, constraints) - self.assertIn("must be unindexed", str(exc.exception)) - with self.assertRaises(RuntimeError) as exc: + with self.assertRaisesRegex(KeyError, "does not exist"): variables = [model.P] constraints = [model.ideal_gas] igraph.block_triangularize(variables, constraints) - self.assertIn("must be unindexed", str(exc.exception)) @unittest.skipUnless(scipy_available, "scipy is not available.") def test_remove(self): @@ -923,7 +919,7 @@ def test_remove(self): # Say we know that these variables and constraints should # be matched... vars_to_remove = [model.F[0], model.F[2]] - cons_to_remove = (model.mbal[1], model.mbal[2]) + cons_to_remove = [model.mbal[1], model.mbal[2]] igraph.remove_nodes(vars_to_remove, cons_to_remove) variable_set = ComponentSet(igraph.variables) self.assertNotIn(model.F[0], variable_set) @@ -1309,7 +1305,7 @@ def test_remove(self): # matrix. vars_to_remove = [m.flow_comp[1]] cons_to_remove = [m.flow_eqn[1]] - igraph.remove_nodes(vars_to_remove + cons_to_remove) + igraph.remove_nodes(vars_to_remove, cons_to_remove) var_dmp, con_dmp = igraph.dulmage_mendelsohn() var_con_set = ComponentSet(igraph.variables + igraph.constraints) underconstrained_set = ComponentSet( @@ -1460,6 +1456,21 @@ def test_remove_no_matrix(self): with self.assertRaisesRegex(RuntimeError, "no incidence matrix"): igraph.remove_nodes([m.v1]) + def test_remove_bad_node(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3]) + m.eq = pyo.Constraint(pyo.PositiveIntegers) + m.eq[1] = m.x[1] * m.x[2] == m.x[3] + m.eq[2] = m.x[1] + 2 * m.x[2] == 3 * m.x[3] + igraph = IncidenceGraphInterface(m) + with self.assertRaisesRegex(KeyError, "does not exist"): + # Suppose we think something like this should work. We should get + # an error, and not silently do nothing. + igraph.remove_nodes([m.x], [m.eq]) + + with self.assertRaisesRegex(KeyError, "does not exist"): + igraph.remove_nodes([[m.x[1], m.x[2]], [m.eq[1]]]) + @unittest.skipUnless(networkx_available, "networkx is not available.") @unittest.skipUnless(scipy_available, "scipy is not available.") @@ -1840,7 +1851,7 @@ def test_var_elim(self): for adj_con in igraph.get_adjacent_to(m.x[1]): for adj_var in igraph.get_adjacent_to(m.eq4): igraph.add_edge(adj_var, adj_con) - igraph.remove_nodes([m.x[1], m.eq4]) + igraph.remove_nodes([m.x[1]], [m.eq4]) assert ComponentSet(igraph.variables) == ComponentSet([m.x[2], m.x[3], m.x[4]]) assert ComponentSet(igraph.constraints) == ComponentSet([m.eq1, m.eq2, m.eq3]) From 5ae3cf4a90dceb97cb1f707ccfa76754e783f9f8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 26 Mar 2024 20:58:24 -0600 Subject: [PATCH 1056/3044] Fixing the last test that I broke --- pyomo/gdp/tests/test_bigm.py | 56 +++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index 8d0fa8bd633..95c4652e387 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1882,12 +1882,11 @@ def test_m_value_mappings(self): # many of the transformed constraints look like this, so can call this # function to test them. def check_bigM_constraint(self, cons, variable, M, indicator_var): - repn = generate_standard_repn(cons.body) - self.assertTrue(repn.is_linear()) - self.assertEqual(repn.constant, -M) - self.assertEqual(len(repn.linear_vars), 2) - ct.check_linear_coef(self, repn, variable, 1) - ct.check_linear_coef(self, repn, indicator_var, M) + assertExpressionsEqual( + self, + cons.body, + variable - float(M) * (1 - indicator_var.get_associated_binary()) + ) def check_inner_xor_constraint(self, inner_disjunction, outer_disjunct, bigm): inner_xor = inner_disjunction.algebraic_constraint @@ -1952,6 +1951,14 @@ def test_transformed_constraints(self): .binary_indicator_var, ) ), + 1, + EXPR.MonomialTermExpression( + ( + -1, + m.disjunct[1] + .binary_indicator_var, + ) + ), ] ), ) @@ -1961,37 +1968,41 @@ def test_transformed_constraints(self): ] ), ) - self.assertIsNone(cons1ub.lower) - self.assertEqual(cons1ub.upper, 0) - self.check_bigM_constraint( - cons1ub, m.z, 10, m.disjunct[1].innerdisjunct[0].indicator_var + assertExpressionsEqual( + self, + cons1ub.expr, + m.z - 10.0*(1 - m.disjunct[1].innerdisjunct[0].binary_indicator_var + + 1 - m.disjunct[1].binary_indicator_var) <= 0.0 ) cons2 = bigm.get_transformed_constraints(m.disjunct[1].innerdisjunct[1].c) self.assertEqual(len(cons2), 1) cons2lb = cons2[0] - self.assertEqual(cons2lb.lower, 5) - self.assertIsNone(cons2lb.upper) - self.check_bigM_constraint( - cons2lb, m.z, -5, m.disjunct[1].innerdisjunct[1].indicator_var + assertExpressionsEqual( + self, + cons2lb.expr, + 5.0 <= m.z - (-5.0)*(1 - m.disjunct[1].innerdisjunct[1].binary_indicator_var + + 1 - m.disjunct[1].binary_indicator_var) ) cons3 = bigm.get_transformed_constraints(m.simpledisjunct.innerdisjunct0.c) self.assertEqual(len(cons3), 1) cons3ub = cons3[0] - self.assertEqual(cons3ub.upper, 2) - self.assertIsNone(cons3ub.lower) - self.check_bigM_constraint( - cons3ub, m.x, 7, m.simpledisjunct.innerdisjunct0.indicator_var + assertExpressionsEqual( + self, + cons3ub.expr, + m.x - 7.0*(1 - m.simpledisjunct.innerdisjunct0.binary_indicator_var + 1 - + m.simpledisjunct.binary_indicator_var) <= 2.0 ) cons4 = bigm.get_transformed_constraints(m.simpledisjunct.innerdisjunct1.c) self.assertEqual(len(cons4), 1) cons4lb = cons4[0] - self.assertEqual(cons4lb.lower, 4) - self.assertIsNone(cons4lb.upper) - self.check_bigM_constraint( - cons4lb, m.x, -13, m.simpledisjunct.innerdisjunct1.indicator_var + assertExpressionsEqual( + self, + cons4lb.expr, + m.x - (-13.0)*(1 - m.simpledisjunct.innerdisjunct1.binary_indicator_var + + 1 - m.simpledisjunct.binary_indicator_var) >= 4.0 ) # Here we check that the xor constraint from @@ -2127,7 +2138,6 @@ def check_hierarchical_nested_model(self, m, bigm): disj2c = bigm.get_transformed_constraints(m.disjunct_block.disj2.c) self.assertEqual(len(disj2c), 1) cons = disj2c[0] - cons.pprint() assertExpressionsEqual( self, cons.expr, From 0c25598b30c3150851c3434d703aaa929c559911 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 26 Mar 2024 21:00:19 -0600 Subject: [PATCH 1057/3044] black --- pyomo/gdp/plugins/bigm.py | 30 +++++---- pyomo/gdp/plugins/bigm_mixin.py | 10 ++- pyomo/gdp/tests/test_bigm.py | 107 +++++++++++++++++++++++++------- 3 files changed, 109 insertions(+), 38 deletions(-) diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index 118fa6935d7..d715d913db8 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -213,12 +213,7 @@ def _apply_to_impl(self, instance, **kwds): bigM = self._config.bigM for t in preprocessed_targets: if t.ctype is Disjunction: - self._transform_disjunctionData( - t, - t.index(), - bigM, - gdp_tree, - ) + self._transform_disjunctionData(t, t.index(), bigM, gdp_tree) # issue warnings about anything that was in the bigM args dict that we # didn't use @@ -275,15 +270,21 @@ def _transform_disjunct(self, obj, bigM, transBlock, gdp_tree): # comparing the two relaxations. # # Transform each component within this disjunct - self._transform_block_components(obj, obj, bigM, arg_list, suffix_list, - indicator_expression) + self._transform_block_components( + obj, obj, bigM, arg_list, suffix_list, indicator_expression + ) # deactivate disjunct to keep the writers happy obj._deactivate_without_fixing_indicator() def _transform_constraint( - self, obj, disjunct, bigMargs, arg_list, disjunct_suffix_list, - indicator_expression + self, + obj, + disjunct, + bigMargs, + arg_list, + disjunct_suffix_list, + indicator_expression, ): # add constraint to the transformation block, we'll transform it there. transBlock = disjunct._transformation_block() @@ -355,8 +356,13 @@ def _transform_constraint( bigm_src[c] = (lower, upper) self._add_constraint_expressions( - c, i, M, disjunct.binary_indicator_var, newConstraint, constraint_map, - indicator_expression=indicator_expression + c, + i, + M, + disjunct.binary_indicator_var, + newConstraint, + constraint_map, + indicator_expression=indicator_expression, ) # deactivate because we relaxed diff --git a/pyomo/gdp/plugins/bigm_mixin.py b/pyomo/gdp/plugins/bigm_mixin.py index 300509d81f8..1c3fcb2c64a 100644 --- a/pyomo/gdp/plugins/bigm_mixin.py +++ b/pyomo/gdp/plugins/bigm_mixin.py @@ -232,8 +232,14 @@ def _estimate_M(self, expr, constraint): return tuple(M) def _add_constraint_expressions( - self, c, i, M, indicator_var, newConstraint, constraint_map, - indicator_expression=None + self, + c, + i, + M, + indicator_var, + newConstraint, + constraint_map, + indicator_expression=None, ): # Since we are both combining components from multiple blocks and using # local names, we need to make sure that the first index for diff --git a/pyomo/gdp/tests/test_bigm.py b/pyomo/gdp/tests/test_bigm.py index 95c4652e387..3174a95292e 100644 --- a/pyomo/gdp/tests/test_bigm.py +++ b/pyomo/gdp/tests/test_bigm.py @@ -1885,7 +1885,7 @@ def check_bigM_constraint(self, cons, variable, M, indicator_var): assertExpressionsEqual( self, cons.body, - variable - float(M) * (1 - indicator_var.get_associated_binary()) + variable - float(M) * (1 - indicator_var.get_associated_binary()), ) def check_inner_xor_constraint(self, inner_disjunction, outer_disjunct, bigm): @@ -1953,11 +1953,7 @@ def test_transformed_constraints(self): ), 1, EXPR.MonomialTermExpression( - ( - -1, - m.disjunct[1] - .binary_indicator_var, - ) + (-1, m.disjunct[1].binary_indicator_var) ), ] ), @@ -1971,8 +1967,15 @@ def test_transformed_constraints(self): assertExpressionsEqual( self, cons1ub.expr, - m.z - 10.0*(1 - m.disjunct[1].innerdisjunct[0].binary_indicator_var + - 1 - m.disjunct[1].binary_indicator_var) <= 0.0 + m.z + - 10.0 + * ( + 1 + - m.disjunct[1].innerdisjunct[0].binary_indicator_var + + 1 + - m.disjunct[1].binary_indicator_var + ) + <= 0.0, ) cons2 = bigm.get_transformed_constraints(m.disjunct[1].innerdisjunct[1].c) @@ -1981,8 +1984,15 @@ def test_transformed_constraints(self): assertExpressionsEqual( self, cons2lb.expr, - 5.0 <= m.z - (-5.0)*(1 - m.disjunct[1].innerdisjunct[1].binary_indicator_var - + 1 - m.disjunct[1].binary_indicator_var) + 5.0 + <= m.z + - (-5.0) + * ( + 1 + - m.disjunct[1].innerdisjunct[1].binary_indicator_var + + 1 + - m.disjunct[1].binary_indicator_var + ), ) cons3 = bigm.get_transformed_constraints(m.simpledisjunct.innerdisjunct0.c) @@ -1991,8 +2001,15 @@ def test_transformed_constraints(self): assertExpressionsEqual( self, cons3ub.expr, - m.x - 7.0*(1 - m.simpledisjunct.innerdisjunct0.binary_indicator_var + 1 - - m.simpledisjunct.binary_indicator_var) <= 2.0 + m.x + - 7.0 + * ( + 1 + - m.simpledisjunct.innerdisjunct0.binary_indicator_var + + 1 + - m.simpledisjunct.binary_indicator_var + ) + <= 2.0, ) cons4 = bigm.get_transformed_constraints(m.simpledisjunct.innerdisjunct1.c) @@ -2001,8 +2018,15 @@ def test_transformed_constraints(self): assertExpressionsEqual( self, cons4lb.expr, - m.x - (-13.0)*(1 - m.simpledisjunct.innerdisjunct1.binary_indicator_var - + 1 - m.simpledisjunct.binary_indicator_var) >= 4.0 + m.x + - (-13.0) + * ( + 1 + - m.simpledisjunct.innerdisjunct1.binary_indicator_var + + 1 + - m.simpledisjunct.binary_indicator_var + ) + >= 4.0, ) # Here we check that the xor constraint from @@ -2132,7 +2156,12 @@ def check_hierarchical_nested_model(self, m, bigm): assertExpressionsEqual( self, cons.expr, - m.x[1]**2 + m.x[2]**2 + m.x[3]**2 + m.x[4]**2 - 143.0*(1 - m.disj1.binary_indicator_var) <= 1.0 + m.x[1] ** 2 + + m.x[2] ** 2 + + m.x[3] ** 2 + + m.x[4] ** 2 + - 143.0 * (1 - m.disj1.binary_indicator_var) + <= 1.0, ) disj2c = bigm.get_transformed_constraints(m.disjunct_block.disj2.c) @@ -2141,7 +2170,12 @@ def check_hierarchical_nested_model(self, m, bigm): assertExpressionsEqual( self, cons.expr, - (3 - m.x[1])**2 + (3 - m.x[2])**2 + (3 - m.x[3])**2 + (3 - m.x[4])**2 - 99.0*(1 - m.disjunct_block.disj2.binary_indicator_var) <= 1.0 + (3 - m.x[1]) ** 2 + + (3 - m.x[2]) ** 2 + + (3 - m.x[3]) ** 2 + + (3 - m.x[4]) ** 2 + - 99.0 * (1 - m.disjunct_block.disj2.binary_indicator_var) + <= 1.0, ) # inner disjunction constraints @@ -2153,7 +2187,18 @@ def check_hierarchical_nested_model(self, m, bigm): assertExpressionsEqual( self, cons.expr, - m.x[1]**2 + m.x[2]**2 + m.x[3]**2 + m.x[4]**2 - 143.0*(1 - m.disjunct_block.disj2.disjunction_disjuncts[0].binary_indicator_var + 1 - m.disjunct_block.disj2.binary_indicator_var) <= 1.0 + m.x[1] ** 2 + + m.x[2] ** 2 + + m.x[3] ** 2 + + m.x[4] ** 2 + - 143.0 + * ( + 1 + - m.disjunct_block.disj2.disjunction_disjuncts[0].binary_indicator_var + + 1 + - m.disjunct_block.disj2.binary_indicator_var + ) + <= 1.0, ) innerd2c = bigm.get_transformed_constraints( @@ -2164,7 +2209,18 @@ def check_hierarchical_nested_model(self, m, bigm): assertExpressionsEqual( self, cons.expr, - (3 - m.x[1])**2 + (3 - m.x[2])**2 + (3 - m.x[3])**2 + (3 - m.x[4])**2 - 99.0*(1 - m.disjunct_block.disj2.disjunction_disjuncts[1].binary_indicator_var + 1 - m.disjunct_block.disj2.binary_indicator_var) <= 1.0 + (3 - m.x[1]) ** 2 + + (3 - m.x[2]) ** 2 + + (3 - m.x[3]) ** 2 + + (3 - m.x[4]) ** 2 + - 99.0 + * ( + 1 + - m.disjunct_block.disj2.disjunction_disjuncts[1].binary_indicator_var + + 1 + - m.disjunct_block.disj2.binary_indicator_var + ) + <= 1.0, ) def test_hierarchical_badly_ordered_targets(self): @@ -2211,17 +2267,20 @@ def test_constraints_not_enforced_when_an_ancestor_indicator_is_False(self): m.right.disjunction = Disjunction(expr=[m.right.left, m.right.right]) m.disjunction = Disjunction(expr=[m.left, m.right]) - m.equiv_left = LogicalConstraint(expr=m.left.left.indicator_var.equivalent_to( - m.right.left.indicator_var)) - m.equiv_right = LogicalConstraint(expr=m.left.right.indicator_var.equivalent_to( - m.right.right.indicator_var)) + m.equiv_left = LogicalConstraint( + expr=m.left.left.indicator_var.equivalent_to(m.right.left.indicator_var) + ) + m.equiv_right = LogicalConstraint( + expr=m.left.right.indicator_var.equivalent_to(m.right.right.indicator_var) + ) m.obj = Objective(expr=m.x) TransformationFactory('gdp.bigm').apply_to(m) results = SolverFactory('gurobi').solve(m) - self.assertEqual(results.solver.termination_condition, - TerminationCondition.optimal) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) self.assertTrue(value(m.right.indicator_var)) self.assertFalse(value(m.left.indicator_var)) self.assertTrue(value(m.right.right.indicator_var)) From fe4a4e0815ac425bab389abd2d44fac5d91acf91 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 26 Mar 2024 21:33:16 -0600 Subject: [PATCH 1058/3044] Updating GDPopt call to _transform_constraint --- pyomo/contrib/gdpopt/util.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/gdpopt/util.py b/pyomo/contrib/gdpopt/util.py index 2cb70f0ea60..babe0245d57 100644 --- a/pyomo/contrib/gdpopt/util.py +++ b/pyomo/contrib/gdpopt/util.py @@ -553,6 +553,13 @@ def _add_bigm_constraint_to_transformed_model(m, constraint, block): # making a Reference to the ComponentData so that it will look like an # indexed component for now. If I redesign bigm at some point, then this # could be prettier. - bigm._transform_constraint(Reference(constraint), parent_disjunct, None, [], []) + bigm._transform_constraint( + Reference(constraint), + parent_disjunct, + None, + [], + [], + 1 - parent_disjunct.binary_indicator_var, + ) # Now get rid of it because this is a class attribute! del bigm._config From 6e1d351126e5e3f0b775d678b1778ceb38de5938 Mon Sep 17 00:00:00 2001 From: Eslick Date: Wed, 27 Mar 2024 08:18:07 -0400 Subject: [PATCH 1059/3044] Add Robbybp's patch --- pyomo/contrib/pynumero/interfaces/pyomo_nlp.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py index 51edd09311a..ce148f50ecf 100644 --- a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py @@ -92,15 +92,13 @@ def __init__(self, pyomo_model, nl_file_options=None): # The NL writer advertises the external function libraries # through the PYOMO_AMPLFUNC environment variable; merge it # with any preexisting AMPLFUNC definitions - amplfunc = "\n".join( - filter( - None, - ( - os.environ.get('AMPLFUNC', None), - os.environ.get('PYOMO_AMPLFUNC', None), - ), - ) - ) + amplfunc_lines = os.environ.get("AMPLFUNC", "").split("\n") + existing = set(amplfunc_lines) + for line in os.environ.get("PYOMO_AMPLFUNC", "").split("\n"): + # Skip (a) empty lines and (b) lines we already have + if line != "" and line not in existing: + amplfunc_lines.append(line) + amplfunc = "\n".join(amplfunc_lines) with CtypesEnviron(AMPLFUNC=amplfunc): super(PyomoNLP, self).__init__(nl_file) From 0e7fa12f6915e7eacfbaa24b1a6d36af8d46dc3c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 27 Mar 2024 07:09:26 -0600 Subject: [PATCH 1060/3044] Adding some test skips that whatever partial environment I'm living in this morning caught --- pyomo/contrib/gdpopt/tests/test_LBB.py | 1 + pyomo/contrib/gdpopt/tests/test_gdpopt.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/gdpopt/tests/test_LBB.py b/pyomo/contrib/gdpopt/tests/test_LBB.py index 273327b02a4..8a553398fa6 100644 --- a/pyomo/contrib/gdpopt/tests/test_LBB.py +++ b/pyomo/contrib/gdpopt/tests/test_LBB.py @@ -59,6 +59,7 @@ def test_infeasible_GDP(self): self.assertIsNone(m.d.disjuncts[0].indicator_var.value) self.assertIsNone(m.d.disjuncts[1].indicator_var.value) + @unittest.skipUnless(z3_available, "Z3 SAT solver is not available") def test_infeasible_GDP_check_sat(self): """Test for infeasible GDP with check_sat option True.""" m = ConcreteModel() diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index 005df56ced5..98750f6e78a 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -1050,7 +1050,8 @@ def assert_correct_disjuncts_active( self.assertTrue(fabs(value(eight_process.profit.expr) - 68) <= 1e-2) - @unittest.skipUnless(Gurobi().available(), "APPSI Gurobi solver is not available") + @unittest.skipUnless(Gurobi().available() and Gurobi().license_is_valid(), + "APPSI Gurobi solver is not available") def test_auto_persistent_solver(self): exfile = import_file(join(exdir, 'eight_process', 'eight_proc_model.py')) m = exfile.build_eight_process_flowsheet() From 897704ded324a13bd946ce7191597c5d65d21f1d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 27 Mar 2024 08:39:45 -0600 Subject: [PATCH 1061/3044] Fixing license check, though that's not the real issue --- pyomo/contrib/gdpopt/tests/test_gdpopt.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index 98750f6e78a..c33e0172def 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -22,6 +22,7 @@ from pyomo.common.collections import Bunch from pyomo.common.config import ConfigDict, ConfigValue from pyomo.common.fileutils import import_file, PYOMO_ROOT_DIR +from pyomo.contrib.appsi.base import Solver from pyomo.contrib.appsi.solvers.gurobi import Gurobi from pyomo.contrib.gdpopt.create_oa_subproblems import ( add_util_block, @@ -1050,8 +1051,9 @@ def assert_correct_disjuncts_active( self.assertTrue(fabs(value(eight_process.profit.expr) - 68) <= 1e-2) - @unittest.skipUnless(Gurobi().available() and Gurobi().license_is_valid(), - "APPSI Gurobi solver is not available") + @unittest.skipUnless(SolverFactory('appsi_gurobi').available( + exception_flag=False) and SolverFactory('appsi_gurobi').license_is_valid(), + "Legacy APPSI Gurobi solver is not available") def test_auto_persistent_solver(self): exfile = import_file(join(exdir, 'eight_process', 'eight_proc_model.py')) m = exfile.build_eight_process_flowsheet() From 60acf1c2f807f449ae822b43989884fdff41ba00 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 27 Mar 2024 08:40:30 -0600 Subject: [PATCH 1062/3044] black --- pyomo/contrib/gdpopt/tests/test_gdpopt.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index c33e0172def..bf295897ec0 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -1051,9 +1051,11 @@ def assert_correct_disjuncts_active( self.assertTrue(fabs(value(eight_process.profit.expr) - 68) <= 1e-2) - @unittest.skipUnless(SolverFactory('appsi_gurobi').available( - exception_flag=False) and SolverFactory('appsi_gurobi').license_is_valid(), - "Legacy APPSI Gurobi solver is not available") + @unittest.skipUnless( + SolverFactory('appsi_gurobi').available(exception_flag=False) + and SolverFactory('appsi_gurobi').license_is_valid(), + "Legacy APPSI Gurobi solver is not available", + ) def test_auto_persistent_solver(self): exfile = import_file(join(exdir, 'eight_process', 'eight_proc_model.py')) m = exfile.build_eight_process_flowsheet() From bb3fafb6231118f64b0e66a95ac74c5db6c11f9f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 27 Mar 2024 09:07:40 -0600 Subject: [PATCH 1063/3044] Debugging GH Actions failures --- pyomo/contrib/gdpopt/solve_subproblem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/gdpopt/solve_subproblem.py b/pyomo/contrib/gdpopt/solve_subproblem.py index e3980c3c784..6ae3cc8e244 100644 --- a/pyomo/contrib/gdpopt/solve_subproblem.py +++ b/pyomo/contrib/gdpopt/solve_subproblem.py @@ -46,6 +46,8 @@ def configure_and_call_solver(model, solver, args, problem_type, timing, time_li solver_args.get('time_limit', float('inf')), remaining ) try: + ## DEBUG + solver_args['tee'] = True results = opt.solve(model, **solver_args) except ValueError as err: if 'Cannot load a SolverResults object with bad status: error' in str(err): From 53684194112384adbb524faa6a36f8077cbc8745 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 27 Mar 2024 11:29:10 -0600 Subject: [PATCH 1064/3044] Skipping 8PP logical problem tests when we don't have a baron license--the transformation of the logical stuff has nested structures and so grew to beyond demo size --- pyomo/contrib/gdpopt/solve_subproblem.py | 2 -- pyomo/contrib/gdpopt/tests/test_gdpopt.py | 9 +++++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/gdpopt/solve_subproblem.py b/pyomo/contrib/gdpopt/solve_subproblem.py index 6ae3cc8e244..e3980c3c784 100644 --- a/pyomo/contrib/gdpopt/solve_subproblem.py +++ b/pyomo/contrib/gdpopt/solve_subproblem.py @@ -46,8 +46,6 @@ def configure_and_call_solver(model, solver, args, problem_type, timing, time_li solver_args.get('time_limit', float('inf')), remaining ) try: - ## DEBUG - solver_args['tee'] = True results = opt.solve(model, **solver_args) except ValueError as err: if 'Cannot load a SolverResults object with bad status: error' in str(err): diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index bf295897ec0..9fe8e450cba 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -768,6 +768,9 @@ def test_time_limit(self): results.solver.termination_condition, TerminationCondition.maxTimeLimit ) + @unittest.skipUnless( + license_is_valid, "No BARON license--8PP logical problem exceeds demo size" + ) def test_LOA_8PP_logical_default_init(self): """Test logic-based outer approximation with 8PP.""" exfile = import_file(join(exdir, 'eight_process', 'eight_proc_logical.py')) @@ -871,6 +874,9 @@ def test_LOA_8PP_maxBinary(self): ) ct.check_8PP_solution(self, eight_process, results) + @unittest.skipUnless( + license_is_valid, "No BARON license--8PP logical problem exceeds demo size" + ) def test_LOA_8PP_logical_maxBinary(self): """Test logic-based OA with max_binary initialization.""" exfile = import_file(join(exdir, 'eight_process', 'eight_proc_logical.py')) @@ -1131,6 +1137,9 @@ def test_RIC_8PP_default_init(self): ) ct.check_8PP_solution(self, eight_process, results) + @unittest.skipUnless( + license_is_valid, "No BARON license--8PP logical problem exceeds demo size" + ) def test_RIC_8PP_logical_default_init(self): """Test logic-based outer approximation with 8PP.""" exfile = import_file(join(exdir, 'eight_process', 'eight_proc_logical.py')) From 096bf542a903b8f232bea240be8028f2694d44de Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 27 Mar 2024 11:42:20 -0600 Subject: [PATCH 1065/3044] whoops, I can read and type and stuff --- pyomo/contrib/gdpopt/tests/test_gdpopt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index 9fe8e450cba..3ac532116aa 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -769,7 +769,7 @@ def test_time_limit(self): ) @unittest.skipUnless( - license_is_valid, "No BARON license--8PP logical problem exceeds demo size" + license_available, "No BARON license--8PP logical problem exceeds demo size" ) def test_LOA_8PP_logical_default_init(self): """Test logic-based outer approximation with 8PP.""" @@ -875,7 +875,7 @@ def test_LOA_8PP_maxBinary(self): ct.check_8PP_solution(self, eight_process, results) @unittest.skipUnless( - license_is_valid, "No BARON license--8PP logical problem exceeds demo size" + license_available, "No BARON license--8PP logical problem exceeds demo size" ) def test_LOA_8PP_logical_maxBinary(self): """Test logic-based OA with max_binary initialization.""" @@ -1138,7 +1138,7 @@ def test_RIC_8PP_default_init(self): ct.check_8PP_solution(self, eight_process, results) @unittest.skipUnless( - license_is_valid, "No BARON license--8PP logical problem exceeds demo size" + license_available, "No BARON license--8PP logical problem exceeds demo size" ) def test_RIC_8PP_logical_default_init(self): """Test logic-based outer approximation with 8PP.""" From 065b422803f91a929e403df5357cd91d85c9e7c7 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 27 Mar 2024 17:48:28 -0600 Subject: [PATCH 1066/3044] update docstring --- pyomo/contrib/incidence_analysis/visualize.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/visualize.py b/pyomo/contrib/incidence_analysis/visualize.py index 9360d8ddfc6..af1bdbbb918 100644 --- a/pyomo/contrib/incidence_analysis/visualize.py +++ b/pyomo/contrib/incidence_analysis/visualize.py @@ -112,13 +112,16 @@ def spy_dulmage_mendelsohn( Config options for ``IncidenceGraphInterface`` order: ``IncidenceOrder``, optional - Order in which to plot sparsity structure + Order in which to plot sparsity structure. Default is + ``IncidenceOrder.dulmage_mendelsohn_upper`` for a block-upper triangular + matrix. Set to ``IncidenceOrder.dulmage_mendelsohn_lower`` for a + block-lower triangular matrix. highlight_coarse: bool, optional - Whether to draw a rectangle around the coarse partition + Whether to draw a rectangle around the coarse partition. Default True highlight_fine: bool, optional - Whether to draw a rectangle around the fine partition + Whether to draw a rectangle around the fine partition. Default True skip_wellconstrained: bool, optional Whether to skip highlighting the well-constrained subsystem of the From 81245460c156f187ead1baafaa58195c21389e60 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 27 Mar 2024 20:31:17 -0400 Subject: [PATCH 1067/3044] add highs version check and load_solutions attributes --- pyomo/contrib/mindtpy/algorithm_base_class.py | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 8d25f3c1d3a..0394110675d 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -152,7 +152,9 @@ def __init__(self, **kwds): # Store the OA cuts generated in the mip_start_process. self.mip_start_lazy_oa_cuts = [] # Whether to load solutions in solve() function - self.load_solutions = True + self.mip_load_solutions = True + self.nlp_load_solutions = True + self.regularization_mip_load_solutions = True # Support use as a context manager under current solver API def __enter__(self): @@ -302,7 +304,7 @@ def model_is_valid(self): results = self.mip_opt.solve( self.original_model, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.mip_load_solutions, **config.mip_solver_args, ) if len(results.solution) > 0: @@ -846,7 +848,7 @@ def init_rNLP(self, add_oa_cuts=True): results = self.nlp_opt.solve( self.rnlp, tee=config.nlp_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: @@ -868,7 +870,7 @@ def init_rNLP(self, add_oa_cuts=True): results = self.nlp_opt.solve( self.rnlp, tee=config.nlp_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: @@ -999,7 +1001,10 @@ def init_max_binaries(self): mip_args = dict(config.mip_solver_args) update_solver_timelimit(self.mip_opt, config.mip_solver, self.timing, config) results = self.mip_opt.solve( - m, tee=config.mip_solver_tee, load_solutions=self.load_solutions, **mip_args + m, + tee=config.mip_solver_tee, + load_solutions=self.mip_load_solutions, + **mip_args, ) if len(results.solution) > 0: m.solutions.load_from(results) @@ -1119,7 +1124,7 @@ def solve_subproblem(self): results = self.nlp_opt.solve( self.fixed_nlp, tee=config.nlp_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: @@ -1586,7 +1591,7 @@ def fix_dual_bound(self, last_iter_cuts): main_mip_results = self.mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.mip_load_solutions, **mip_args, ) if len(main_mip_results.solution) > 0: @@ -1674,7 +1679,7 @@ def solve_main(self): main_mip_results = self.mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.mip_load_solutions, **mip_args, ) # update_attributes should be before load_from(main_mip_results), since load_from(main_mip_results) may fail. @@ -1735,7 +1740,7 @@ def solve_fp_main(self): main_mip_results = self.mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.mip_load_solutions, **mip_args, ) # update_attributes should be before load_from(main_mip_results), since load_from(main_mip_results) may fail. @@ -1778,7 +1783,7 @@ def solve_regularization_main(self): main_mip_results = self.regularization_mip_opt.solve( self.mip, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.regularization_mip_load_solutions, **dict(config.mip_solver_args), ) if len(main_mip_results.solution) > 0: @@ -1994,7 +1999,7 @@ def handle_main_unbounded(self, main_mip): main_mip_results = self.mip_opt.solve( main_mip, tee=config.mip_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.mip_load_solutions, **config.mip_solver_args, ) if len(main_mip_results.solution) > 0: @@ -2277,6 +2282,11 @@ def check_subsolver_validity(self): raise ValueError(self.config.mip_solver + ' is not available.') if not self.mip_opt.license_is_valid(): raise ValueError(self.config.mip_solver + ' is not licensed.') + if self.config.mip_solver == "appsi_highs": + if self.mip_opt.version() < (1, 7, 0): + raise ValueError( + "MindtPy requires the use of HIGHS version 1.7.0 or higher for full compatibility." + ) if not self.nlp_opt.available(): raise ValueError(self.config.nlp_solver + ' is not available.') if not self.nlp_opt.license_is_valid(): @@ -2324,15 +2334,15 @@ def check_config(self): config.mip_solver = 'cplex_persistent' # related to https://github.com/Pyomo/pyomo/issues/2363 + if 'appsi' in config.mip_solver: + self.mip_load_solutions = False + if 'appsi' in config.nlp_solver: + self.nlp_load_solutions = False if ( - 'appsi' in config.mip_solver - or 'appsi' in config.nlp_solver - or ( - config.mip_regularization_solver is not None - and 'appsi' in config.mip_regularization_solver - ) + config.mip_regularization_solver is not None + and 'appsi' in config.mip_regularization_solver ): - self.load_solutions = False + self.regularization_mip_load_solutions = False ################################################################################################################################ # Feasibility Pump @@ -2400,7 +2410,7 @@ def solve_fp_subproblem(self): results = self.nlp_opt.solve( fp_nlp, tee=config.nlp_solver_tee, - load_solutions=self.load_solutions, + load_solutions=self.nlp_load_solutions, **nlp_args, ) if len(results.solution) > 0: From 34c2c36c35260e8207a5850edcc5a985e8b3d55d Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 27 Mar 2024 20:39:26 -0400 Subject: [PATCH 1068/3044] add version check for highs in tests --- pyomo/contrib/mindtpy/tests/test_mindtpy.py | 7 ++++++- pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py | 8 +++++++- pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py | 8 +++++++- pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py | 9 ++++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy.py b/pyomo/contrib/mindtpy/tests/test_mindtpy.py index 27c57370ba2..d0364378ed8 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy.py @@ -56,7 +56,12 @@ QCP_model._generate_model() extreme_model_list = [LP_model.model, QCP_model.model] -required_solvers = ('ipopt', 'appsi_highs') +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('ipopt', 'appsi_highs') +else: + required_solvers = ('ipopt', 'glpk') if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py index fb78be6b2f1..dda0f74147e 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_ECP.py @@ -23,7 +23,13 @@ from pyomo.environ import SolverFactory, value from pyomo.opt import TerminationCondition -required_solvers = ('ipopt', 'appsi_highs') +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('ipopt', 'appsi_highs') +else: + required_solvers = ('ipopt', 'glpk') + if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py index b8f889e6920..0baa361910e 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_feas_pump.py @@ -28,7 +28,13 @@ from pyomo.contrib.mindtpy.tests.feasibility_pump1 import FeasPump1 from pyomo.contrib.mindtpy.tests.feasibility_pump2 import FeasPump2 -required_solvers = ('ipopt', 'appsi_highs') +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('ipopt', 'appsi_highs') +else: + required_solvers = ('ipopt', 'glpk') + if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py b/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py index d50a41ad000..e01558d48ef 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy_grey_box.py @@ -18,7 +18,14 @@ from pyomo.contrib.mindtpy.tests.MINLP_simple import SimpleMINLP as SimpleMINLP model_list = [SimpleMINLP(grey_box=True)] -required_solvers = ('cyipopt', 'glpk') + +if SolverFactory('appsi_highs').available(exception_flag=False) and SolverFactory( + 'appsi_highs' +).version() >= (1, 7, 0): + required_solvers = ('cyipopt', 'appsi_highs') +else: + required_solvers = ('cyipopt', 'glpk') + if all(SolverFactory(s).available(exception_flag=False) for s in required_solvers): subsolvers_available = True else: From bd475d1a45ae34fff431694f74cb5f0d7b934535 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 28 Mar 2024 11:34:26 -0400 Subject: [PATCH 1069/3044] Fix docstring typo --- pyomo/contrib/pyros/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 306141e9829..5d386240609 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -272,7 +272,7 @@ def adjust_solver_time_settings(timing_data_obj, solver, config): option. However, this may be overridden by any user specifications included in a GAMS optfile, which may be difficult to track down. - (3) To ensure the time limit is specified to a strictly + (4) To ensure the time limit is specified to a strictly positive value, the time limit is adjusted to a value of at least 1 second. """ From a99277df9afaad4fd2011376dfeb4a59acc50898 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 10:59:48 -0600 Subject: [PATCH 1070/3044] Allow multiple definitions of solver options --- pyomo/contrib/solver/base.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 07efbaed449..7e93bacd54b 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -348,9 +348,9 @@ class LegacySolverWrapper: interface. Necessary for backwards compatibility. """ - def __init__(self, solver_io=None, **kwargs): - if solver_io is not None: - raise NotImplementedError('Still working on this') + def __init__(self, **kwargs): + if 'options' in kwargs: + self.options = kwargs.pop('options') super().__init__(**kwargs) # @@ -393,8 +393,14 @@ def _map_config( self.config.time_limit = timelimit if report_timing is not NOTSET: self.config.report_timing = report_timing + if hasattr(self, 'options'): + self.config.solver_options.set_value(self.options) if options is not NOTSET: + # This block is trying to mimic the existing logic in the legacy + # interface that allows users to pass initialized options to + # the solver object and override them in the solve call. self.config.solver_options.set_value(options) + # This is a new flag in the interface. To preserve backwards compatibility, # its default is set to "False" if raise_exception_on_nonoptimal_result is not NOTSET: From ef1464c5a3a02d032242ffe10df5ef3f7e753d22 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 28 Mar 2024 13:17:55 -0400 Subject: [PATCH 1071/3044] Restore PyROS intro and disclaimer logging --- pyomo/contrib/pyros/pyros.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index c3335588b7b..582233c4a56 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -12,7 +12,6 @@ # pyros.py: Generalized Robust Cutting-Set Algorithm for Pyomo import logging from pyomo.common.config import document_kwargs_from_configdict -from pyomo.common.collections import Bunch from pyomo.core.base.block import Block from pyomo.core.expr import value from pyomo.core.base.var import Var @@ -20,7 +19,7 @@ from pyomo.contrib.pyros.util import time_code from pyomo.common.modeling import unique_component_name from pyomo.opt import SolverFactory -from pyomo.contrib.pyros.config import pyros_config +from pyomo.contrib.pyros.config import pyros_config, logger_domain from pyomo.contrib.pyros.util import ( recast_to_min_obj, add_decision_rule_constraints, @@ -347,6 +346,23 @@ def solve( global_solver=global_solver, ) ) + + # we want to log the intro and disclaimer in + # advance of assembling the config. + # this helps clarify to the user that any + # messages logged during assembly of the config + # were, in fact, logged after PyROS was initiated + progress_logger = logger_domain( + kwds.get( + "progress_logger", + kwds.get("options", dict()).get( + "progress_logger", default_pyros_solver_logger + ), + ) + ) + self._log_intro(logger=progress_logger, level=logging.INFO) + self._log_disclaimer(logger=progress_logger, level=logging.INFO) + config, state_vars = self._resolve_and_validate_pyros_args(model, **kwds) self._log_config( logger=config.progress_logger, From eb83c2e62485aefa0b0d92cd896b073f6a3ed010 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 11:24:55 -0600 Subject: [PATCH 1072/3044] Add test for option setting behavior --- pyomo/contrib/solver/tests/unit/test_base.py | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 179d9823679..ecf788b17d9 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -272,6 +272,41 @@ def test_map_config(self): with self.assertRaises(AttributeError): print(instance.config.keepfiles) + def test_solver_options_behavior(self): + # options can work in multiple ways (set from instantiation, set + # after instantiation, set during solve). + # Test case 1: Set at instantiation + solver = base.LegacySolverWrapper(options={'max_iter': 6}) + self.assertEqual(solver.options, {'max_iter': 6}) + + # Test case 2: Set later + solver = base.LegacySolverWrapper() + solver.options = {'max_iter': 4, 'foo': 'bar'} + self.assertEqual(solver.options, {'max_iter': 4, 'foo': 'bar'}) + + # Test case 3: pass some options to the mapping (aka, 'solve' command) + solver = base.LegacySolverWrapper() + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + solver._map_config(options={'max_iter': 4}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4}) + + # Test case 4: Set at instantiation and override during 'solve' call + solver = base.LegacySolverWrapper(options={'max_iter': 6}) + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + solver._map_config(options={'max_iter': 4}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4}) + self.assertEqual(solver.options, {'max_iter': 6}) + def test_map_results(self): # Unclear how to test this pass From d9ca9879032a9da21a41f0ece5bf623e4553398f Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 28 Mar 2024 13:32:37 -0400 Subject: [PATCH 1073/3044] Update PyROS solver logging docs example --- doc/OnlineDocs/contributed_packages/pyros.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index 76a751dd994..9faa6d1365f 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -903,10 +903,10 @@ Observe that the log contains the following information: :linenos: ============================================================================== - PyROS: The Pyomo Robust Optimization Solver, v1.2.9. - Pyomo version: 6.7.0 + PyROS: The Pyomo Robust Optimization Solver, v1.2.11. + Pyomo version: 6.7.2 Commit hash: unknown - Invoked at UTC 2023-12-16T00:00:00.000000 + Invoked at UTC 2024-03-28T00:00:00.000000 Developed by: Natalie M. Isenberg (1), Jason A. F. Sherman (1), John D. Siirola (2), Chrysanthos E. Gounaris (1) From 0f6fe16a4a46f2147dec052345de4d9291594313 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 28 Mar 2024 11:45:57 -0600 Subject: [PATCH 1074/3044] Adding initial draft of nonlinear to pwl transformation --- pyomo/contrib/piecewise/__init__.py | 3 + .../piecewise/transform/nonlinear_to_pwl.py | 380 ++++++++++++++++++ 2 files changed, 383 insertions(+) create mode 100644 pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 37873c83b3b..b5452dd2bd5 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -33,3 +33,6 @@ from pyomo.contrib.piecewise.transform.convex_combination import ( ConvexCombinationTransformation, ) +from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( + NonlinearToPWL, +) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py new file mode 100644 index 00000000000..519736723f6 --- /dev/null +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -0,0 +1,380 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import numpy as np +import itertools + +from pyomo.environ import ( + TransformationFactory, + Transformation, + Var, + Constraint, + Objective, + Any, + value, +) +from pyomo.core.expr.numeric_expr import SumExpression +from pyomo.core.expr import identify_variables +from pyomo.repn.quadratic import QuadraticRepnVisitor +from pyomo.core.expr import SumExpression +from pyomo.contrib.piecewise import PiecewiseLinearExpression + + +# TODO remove +MAX_DIM = 5 + +# This should be safe to use many times; declare it globally +_quadratic_repn_visitor = QuadraticRepnVisitor( + subexpression_cache={}, var_map={}, var_order={}, sorter=None +) + + +def get_pwl_function_approximation(func, method, n, bounds, **kwargs): + """ + Get a piecewise-linear approximation to a function, given: + + func: function to approximate + method: method to use for the approximation, current options are: + - 'simple_random_point_grid' + - 'simple_uniform_point_grid' + - 'naive_lmt' + n: parameter controlling fineness of the approximation based on the specified method + bounds: list of tuples giving upper and lower bounds for each of func's arguments + kwargs: additional arguments to be specified to the method used + """ + + points = None + match (method): + case 'simple_random_point_grid': + points = get_simple_random_point_grid(bounds, n) + case 'simple_uniform_point_grid': + points = get_simple_uniform_point_grid(bounds, n) + case 'naive_lmt': + points = get_points_naive_lmt(bounds, n, func, randomize=True) + case 'naive_lmt_uniform': + points = get_points_naive_lmt(bounds, n, func, randomize=False) + case _: + raise NotImplementedError(f"Invalid method: {method}") + + # Default path: after getting the points, construct PWLF using the + # function-and-list-of-points constructor + + # DUCT TAPE WARNING: work around deficiency in PiecewiseLinearFunction constructor. TODO + dim = len(points[0]) + if dim == 1: + points = [pt[0] for pt in points] + + print( + f" Constructing PWLF with {len(points)} points, each of which are {dim}-dimensional" + ) + return PiecewiseLinearFunction(points=points, function=func) + + +def get_simple_random_point_grid(bounds, n, seed=42): + # Generate randomized grid of points + linspaces = [] + for b in bounds: + np.random.seed(seed) + linspaces.append(np.random.uniform(b[0], b[1], n)) + return list(itertools.product(*linspaces)) + + +def get_simple_uniform_point_grid(bounds, n): + # Generate non-randomized grid of points + linspaces = [] + for b in bounds: + # Issues happen when exactly using the boundary + nudge = (b[1] - b[0]) * 1e-4 + linspaces.append( + # np.linspace(b[0], b[1], n) + np.linspace(b[0] + nudge, b[1] - nudge, n) + ) + return list(itertools.product(*linspaces)) + + +# TODO this was copypasted from shumeng; make it better +def get_points_naive_lmt(bounds, n, func, seed=42, randomize=True): + from lineartree import LinearTreeRegressor + from sklearn.linear_model import LinearRegression + from sklearn.metrics import mean_squared_error + from sklearn.model_selection import train_test_split + import PWLTransformation.lmt as lmtutils + + points = None + if randomize: + points = get_simple_random_point_grid(bounds, n, seed=seed) + else: + points = get_simple_uniform_point_grid(bounds, n) + # perturb(points, 0.01) + x_list = np.array(points) + y_list = [] + for point in points: + y_list.append(func(*point)) + regr = LinearTreeRegressor( + LinearRegression(), + criterion='mse', + max_bins=120, + min_samples_leaf=4, + max_depth=5, + ) + + # Using train_test_split is silly. TODO: remove this and just sample my own + # extra points if I want to estimate the error. + X_train, X_test, y_train, y_test = train_test_split( + x_list, y_list, test_size=0.2, random_state=seed + ) + regr.fit(X_train, y_train) + y_pred = regr.predict(X_test) + error = mean_squared_error(y_test, y_pred) + + leaves, splits, ths = lmtutils.parse_linear_tree_regressor(regr, bounds) + + # This was originally part of the LMT_Model_component and used to calculate + # avg_leaves for the output data. TODO: get this back + # self.total_leaves += len(leaves) + + # bound_point_list = lmt.generate_bound(leaves) + bound_point_list = lmtutils.generate_bound_points(leaves, bounds) + # duct tape to fix possible issues from unknown bugs. TODO should this go + # here? + return bound_point_list + + +@TransformationFactory.register( + 'contrib.piecewise.nonlinear_to_pwl', + doc="Convert nonlinear constraints and objectives to piecewise-linear approximations.", +) +class NonlinearToPWL(Transformation): + """ + Convert nonlinear constraints and objectives to piecewise-linear approximations. + """ + + def __init__(self): + super(Transformation).__init__() + + def _apply_to( + self, + model, + n=3, + method='simple_uniform_point_grid', + allow_quadratic_cons=True, + allow_quadratic_objs=True, + additively_decompose=True, + ): + """Apply the transformation""" + + # Check ahead of time whether there are any unbounded variables. If + # there are, we'll have to bail out + # But deactivated variables can be left alone -- or should they be? + # Let's not, for now. + for v in model.component_objects(Var): + if None in v.bounds: + print( + "Error: cannot apply transformation to model with unbounded variables" + ) + raise NotImplementedError( + "Cannot apply transformation to model with unbounded variables" + ) + + # Upcoming steps will trash the values of the vars, since I don't know + # a better way. But what if the user set them with initialize= ? We'd + # better restore them after we're done. + orig_var_map = {id(var): var.value for var in model.component_objects(Var)} + + # Now we are ready to start + original_cons = list(model.component_data_objects(Constraint)) + original_objs = list(model.component_data_objects(Objective)) + + model._pwl_quadratic_count = 0 + model._pwl_nonlinear_count = 0 + + # Let's put all our new constraints in one big index + model._pwl_cons = Constraint(Any) + + for con in original_cons: + repn = _quadratic_repn_visitor.walk_expression(con.body) + if repn.nonlinear is None: + if repn.quadratic is None: + # Linear constraint. Always skip. + continue + else: + model._pwl_quadratic_count += 1 + if allow_quadratic_cons: + continue + else: + model._pwl_nonlinear_count += 1 + _replace_con( + model, con, method, n, allow_quadratic_cons, additively_decompose + ) + + # And do the same for objectives + for obj in original_objs: + repn = _quadratic_repn_visitor.walk_expression(obj) + if repn.nonlinear is None: + if repn.quadratic is None: + # Linear objective. Skip. + continue + else: + model._pwl_quadratic_count += 1 + if allow_quadratic_objs: + continue + else: + model._pwl_nonlinear_count += 1 + _replace_obj( + model, obj, method, n, allow_quadratic_objs, additively_decompose + ) + + # Before we're done, replace the old variable values + for var in model.component_objects(Var): + var.value = orig_var_map[id(var)] + + +# Check whether a term should be skipped for approximation. Do not touch +# model's quadratic or nonlinear counts; those are only for top-level +# expressions which were already checked +def _check_skip_approx(expr, allow_quadratic, model): + repn = _quadratic_repn_visitor.walk_expression(expr) + if repn.nonlinear is None: + if repn.quadratic is None: + # Linear expression. Skip. + return True + else: + # model._pwl_quadratic_count += 1 + if allow_quadratic: + return True + else: + pass + # model._pwl_nonlinear_count += 1 + dim = len(list(identify_variables(expr))) + if dim > MAX_DIM: + print(f"Refusing to approximate function with {dim}-dimensional component.") + raise RuntimeError( + f"Refusing to approximate function with {dim}-dimensional component." + ) + return False + + +def _replace_con(model, con, method, n, allow_quadratic_cons, additively_decompose): + vars = list(identify_variables(con.body)) + bounds = [(v.bounds[0], v.bounds[1]) for v in vars] + + # Alright, let's do it like this. Additively decompose con.body and work on the pieces + func_pieces = [] + for k, expr in enumerate( + _additively_decompose_expr(con.body) if additively_decompose else [con.body] + ): + # First, check if we actually need to do anything + if _check_skip_approx(expr, allow_quadratic_cons, model): + # We're skipping this term. Just add expr directly to the pieces + func_pieces.append(expr) + continue + + vars_inner = list(identify_variables(expr)) + bounds = [(v.bounds[0], v.bounds[1]) for v in vars_inner] + + def eval_con_func(*args): + # sanity check + assert len(args) == len( + vars_inner + ), f"eval_con_func was called with {len(args)} arguments, but expected {len(vars_inner)}" + for i, v in enumerate(vars_inner): + v.value = args[i] + return value(con.body) + + pwlf = get_pwl_function_approximation(eval_con_func, method, n, bounds) + + con_name = con.getname(fully_qualified=False) + model.add_component(f"_pwle_{con_name}_{k}", pwlf) + # func_pieces.append(pwlf(*vars_inner).expr) + func_pieces.append(pwlf(*vars_inner)) + + pwl_func = sum(func_pieces) + + # Change the constraint. This is hard to do in-place, so I'll + # remake it and deactivate the old one as was done originally. + + # Now we need a ton of if statements to properly set up the constraint + if con.equality: + model._pwl_cons[str(con)] = pwl_func == con.ub + elif con.strict_lower: + model._pwl_cons[str(con)] = pwl_func > con.lb + elif con.strict_upper: + model._pwl_cons[str(con)] = pwl_func < con.ub + elif con.has_lb(): + if con.has_ub(): # constraint is of the form lb <= expr <= ub + model._pwl_cons[str(con)] = (con.lb, pwl_func, con.ub) + else: + model._pwl_cons[str(con)] = pwl_func >= con.lb + elif con.has_ub(): + model._pwl_cons[str(con)] = pwl_func <= con.ub + else: + assert ( + False + ), f"unreachable: original Constraint '{con_name}' did not have any upper or lower bound" + con.deactivate() + + +def _replace_obj(model, obj, method, n, allow_quadratic_obj, additively_decompose): + vars = list(identify_variables(obj)) + bounds = [(v.bounds[0], v.bounds[1]) for v in vars] + + func_pieces = [] + for k, expr in enumerate( + _additively_decompose_expr(obj.expr) if additively_decompose else [obj.expr] + ): + # First, check if we actually need to do anything + if _check_skip_approx(expr, allow_quadratic_obj, model): + # We're skipping this term. Just add expr directly to the pieces + func_pieces.append(expr) + continue + + vars_inner = list(identify_variables(expr)) + bounds = [(v.bounds[0], v.bounds[1]) for v in vars_inner] + + def eval_obj_func(*args): + # sanity check + assert len(args) == len( + vars_inner + ), f"eval_obj_func was called with {len(args)} arguments, but expected {len(vars_inner)}" + for i, v in enumerate(vars_inner): + v.value = args[i] + return value(obj) + + pwlf = get_pwl_function_approximation(eval_obj_func, method, n, bounds) + + obj_name = obj.getname(fully_qualified=False) + model.add_component(f"_pwle_{obj_name}_{k}", pwlf) + func_pieces.append(pwlf(*vars_inner)) + + pwl_func = sum(func_pieces[1:], func_pieces[0]) + + # Add the new objective + obj_name = obj.getname(fully_qualified=False) + # model.add_component(f"_pwle_{obj_name}", pwl_func) + model.add_component( + f"_pwl_obj_{obj_name}", Objective(expr=pwl_func, sense=obj.sense) + ) + obj.deactivate() + + +# Copypasted from gdp/plugins/partition_disjuncts.py for now. This is the +# stupid approach that will not properly catch all additive separability; to do +# it better we need a walker. +def _additively_decompose_expr(input_expr): + if input_expr.__class__ is not SumExpression: + # print(f"couldn't decompose: input_expr.__class__ was {input_expr.__class__}, not SumExpression") + # This isn't separable, so we just have the one expression + return [input_expr] + # else, it was a SumExpression, and we will break it into the summands + summands = list(input_expr.args) + # print(f"len(summands) is {len(summands)}") + # print(f"summands is {summands}") + return summands From b3024f5f74f903cd170c896de751966483695d39 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 12:49:14 -0600 Subject: [PATCH 1075/3044] Make error message more clear --- pyomo/contrib/solver/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 7e93bacd54b..e5794a8088c 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -577,7 +577,10 @@ def available(self, exception_flag=True): """ ans = super().available() if exception_flag and not ans: - raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).') + raise ApplicationError( + f'Solver "{self.name}" is not available. ' + f'The returned status is: {ans}.' + ) return bool(ans) def license_is_valid(self) -> bool: From 9eb3da0032f54851fd4743d1244ee88e7b3672c4 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 13:32:01 -0600 Subject: [PATCH 1076/3044] Update options to allow both options and solver_options and writer_config --- pyomo/contrib/solver/base.py | 35 ++++++++++- pyomo/contrib/solver/tests/unit/test_base.py | 64 ++++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index e5794a8088c..918f436a212 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -349,8 +349,15 @@ class LegacySolverWrapper: """ def __init__(self, **kwargs): - if 'options' in kwargs: + if 'options' in kwargs and 'solver_options' in kwargs: + raise ApplicationError( + "Both 'options' and 'solver_options' were requested. " + "Please use one or the other, not both." + ) + elif 'options' in kwargs: self.options = kwargs.pop('options') + elif 'solver_options' in kwargs: + self.solver_options = kwargs.pop('solver_options') super().__init__(**kwargs) # @@ -376,6 +383,8 @@ def _map_config( keepfiles=NOTSET, solnfile=NOTSET, options=NOTSET, + solver_options=NOTSET, + writer_config=NOTSET, ): """Map between legacy and new interface configuration options""" self.config = self.config() @@ -395,12 +404,27 @@ def _map_config( self.config.report_timing = report_timing if hasattr(self, 'options'): self.config.solver_options.set_value(self.options) - if options is not NOTSET: + if hasattr(self, 'solver_options'): + self.config.solver_options.set_value(self.solver_options) + if (options is not NOTSET) and (solver_options is not NOTSET): + # There is no reason for a user to be trying to mix both old + # and new options. That is silly. So we will yell at them. + # Example that would raise an error: + # solver.solve(model, options={'foo' : 'bar'}, solver_options={'foo' : 'not_bar'}) + raise ApplicationError( + "Both 'options' and 'solver_options' were declared " + "in the 'solve' call. Please use one or the other, " + "not both." + ) + elif options is not NOTSET: # This block is trying to mimic the existing logic in the legacy # interface that allows users to pass initialized options to # the solver object and override them in the solve call. self.config.solver_options.set_value(options) - + elif solver_options is not NOTSET: + self.config.solver_options.set_value(solver_options) + if writer_config is not NOTSET: + self.config.writer_config.set_value(writer_config) # This is a new flag in the interface. To preserve backwards compatibility, # its default is set to "False" if raise_exception_on_nonoptimal_result is not NOTSET: @@ -526,7 +550,10 @@ def solve( options: Optional[Dict] = None, keepfiles: bool = False, symbolic_solver_labels: bool = False, + # These are for forward-compatibility raise_exception_on_nonoptimal_result: bool = False, + solver_options: Optional[Dict] = None, + writer_config: Optional[Dict] = None, ): """ Solve method: maps new solve method style to backwards compatible version. @@ -552,6 +579,8 @@ def solve( 'keepfiles', 'solnfile', 'options', + 'solver_options', + 'writer_config', ) loc = locals() filtered_args = {k: loc[k] for k in map_args if loc.get(k, None) is not None} diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index ecf788b17d9..287116008ab 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -14,6 +14,7 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict from pyomo.contrib.solver import base +from pyomo.common.errors import ApplicationError class TestSolverBase(unittest.TestCase): @@ -307,6 +308,69 @@ def test_solver_options_behavior(self): self.assertEqual(solver.config.solver_options, {'max_iter': 4}) self.assertEqual(solver.options, {'max_iter': 6}) + # solver_options are also supported + # Test case 1: set at instantiation + solver = base.LegacySolverWrapper(solver_options={'max_iter': 6}) + self.assertEqual(solver.solver_options, {'max_iter': 6}) + + # Test case 2: Set later + solver = base.LegacySolverWrapper() + solver.solver_options = {'max_iter': 4, 'foo': 'bar'} + self.assertEqual(solver.solver_options, {'max_iter': 4, 'foo': 'bar'}) + + # Test case 3: pass some solver_options to the mapping (aka, 'solve' command) + solver = base.LegacySolverWrapper() + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + solver._map_config(solver_options={'max_iter': 4}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4}) + + # Test case 4: Set at instantiation and override during 'solve' call + solver = base.LegacySolverWrapper(solver_options={'max_iter': 6}) + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + solver._map_config(solver_options={'max_iter': 4}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4}) + self.assertEqual(solver.solver_options, {'max_iter': 6}) + + # users can mix... sort of + # Test case 1: Initialize with options, solve with solver_options + solver = base.LegacySolverWrapper(options={'max_iter': 6}) + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + solver._map_config(solver_options={'max_iter': 4}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4}) + + # users CANNOT initialize both values at the same time, because how + # do we know what to do with it then? + # Test case 1: Class instance + with self.assertRaises(ApplicationError): + solver = base.LegacySolverWrapper( + options={'max_iter': 6}, solver_options={'max_iter': 4} + ) + # Test case 2: Passing to `solve` + solver = base.LegacySolverWrapper() + config = ConfigDict(implicit=True) + config.declare( + 'solver_options', + ConfigDict(implicit=True, description="Options to pass to the solver."), + ) + solver.config = config + with self.assertRaises(ApplicationError): + solver._map_config(solver_options={'max_iter': 4}, options={'max_iter': 6}) + def test_map_results(self): # Unclear how to test this pass From b34820b8f78f1ecb7e83fd49224a4c3c79d3474f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 28 Mar 2024 14:19:43 -0600 Subject: [PATCH 1077/3044] Not screaming about fixed Var bounds, but this still doesn't work because the space isn't full-dimensional --- .../piecewise/transform/nonlinear_to_pwl.py | 216 ++++++++++++++---- 1 file changed, 174 insertions(+), 42 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 519736723f6..3db29af6784 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -9,9 +9,13 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import numpy as np import itertools +from lineartree import LinearTreeRegressor +import lineartree + +import numpy as np + from pyomo.environ import ( TransformationFactory, Transformation, @@ -25,7 +29,15 @@ from pyomo.core.expr import identify_variables from pyomo.repn.quadratic import QuadraticRepnVisitor from pyomo.core.expr import SumExpression -from pyomo.contrib.piecewise import PiecewiseLinearExpression +from pyomo.contrib.piecewise import ( + PiecewiseLinearExpression, + PiecewiseLinearFunction +) + +from sklearn.linear_model import LinearRegression +import random +from sklearn.metrics import mean_squared_error +from sklearn.model_selection import train_test_split # TODO remove @@ -36,7 +48,6 @@ subexpression_cache={}, var_map={}, var_order={}, sorter=None ) - def get_pwl_function_approximation(func, method, n, bounds, **kwargs): """ Get a piecewise-linear approximation to a function, given: @@ -81,33 +92,28 @@ def get_pwl_function_approximation(func, method, n, bounds, **kwargs): def get_simple_random_point_grid(bounds, n, seed=42): # Generate randomized grid of points linspaces = [] - for b in bounds: + for (lb, ub) in bounds: np.random.seed(seed) - linspaces.append(np.random.uniform(b[0], b[1], n)) + linspaces.append(np.random.uniform(lb, ub, n)) return list(itertools.product(*linspaces)) def get_simple_uniform_point_grid(bounds, n): # Generate non-randomized grid of points linspaces = [] - for b in bounds: + for (lb, ub) in bounds: # Issues happen when exactly using the boundary - nudge = (b[1] - b[0]) * 1e-4 + nudge = (ub - lb) * 1e-4 linspaces.append( # np.linspace(b[0], b[1], n) - np.linspace(b[0] + nudge, b[1] - nudge, n) + np.linspace(lb + nudge, ub - nudge, n) ) return list(itertools.product(*linspaces)) # TODO this was copypasted from shumeng; make it better def get_points_naive_lmt(bounds, n, func, seed=42, randomize=True): - from lineartree import LinearTreeRegressor - from sklearn.linear_model import LinearRegression - from sklearn.metrics import mean_squared_error - from sklearn.model_selection import train_test_split - import PWLTransformation.lmt as lmtutils - + points = None if randomize: points = get_simple_random_point_grid(bounds, n, seed=seed) @@ -135,19 +141,149 @@ def get_points_naive_lmt(bounds, n, func, seed=42, randomize=True): y_pred = regr.predict(X_test) error = mean_squared_error(y_test, y_pred) - leaves, splits, ths = lmtutils.parse_linear_tree_regressor(regr, bounds) + leaves, splits, ths = parse_linear_tree_regressor(regr, bounds) # This was originally part of the LMT_Model_component and used to calculate # avg_leaves for the output data. TODO: get this back # self.total_leaves += len(leaves) # bound_point_list = lmt.generate_bound(leaves) - bound_point_list = lmtutils.generate_bound_points(leaves, bounds) + bound_point_list = generate_bound_points(leaves, bounds) # duct tape to fix possible issues from unknown bugs. TODO should this go # here? return bound_point_list +# TODO: this is still horrible. Maybe I should put these back together into +# a wrapper class again, but better this time? + + +# Given a leaves dict (as generated by parse_tree) and a list of tuples +# representing variable bounds, generate the set of vertices separating each +# subset of the domain +def generate_bound_points(leaves, bounds): + bound_points = [] + for leaf in leaves.values(): + lower_corner_list = [] + upper_corner_list = [] + for var_bound in leaf['bounds'].values(): + lower_corner_list.append(var_bound[0]) + upper_corner_list.append(var_bound[1]) + + # Duct tape to fix issues from unknown bugs + for pt in [lower_corner_list, upper_corner_list]: + for i in range(len(pt)): + # clamp within bounds range + pt[i] = max(pt[i], bounds[i][0]) + pt[i] = min(pt[i], bounds[i][1]) + + if tuple(lower_corner_list) not in bound_points: + bound_points.append(tuple(lower_corner_list)) + if tuple(upper_corner_list) not in bound_points: + bound_points.append(tuple(upper_corner_list)) + + # This process should have gotten every interior bound point. However, all + # but two of the corners of the overall bounding box should have been + # missed. Let's fix that now. + for outer_corner in itertools.product(*bounds): + if outer_corner not in bound_points: + bound_points.append(outer_corner) + return bound_points + + +# Parse a LinearTreeRegressor and identify features such as bounds, slope, and +# intercept for leaves. Return some dicts. +def parse_linear_tree_regressor(linear_tree_regressor, bounds): + leaves = linear_tree_regressor.summary(only_leaves=True) + splits = linear_tree_regressor.summary() + + for key, leaf in leaves.items(): + del splits[key] + leaf['bounds'] = {} + leaf['slope'] = list(leaf['models'].coef_) + leaf['intercept'] = leaf['models'].intercept_ + + L = np.array(list(leaves.keys())) + features = np.arange(0, len(leaves[L[0]]['slope'])) + + for node in splits.values(): + left_child_node = node['children'][0] # find its left child + right_child_node = node['children'][1] # find its right child + # create the list to save leaves + node['left_leaves'], node['right_leaves'] = [], [] + if left_child_node in leaves: # if left child is a leaf node + node['left_leaves'].append(left_child_node) + else: # traverse its left node by calling function to find all the leaves from its left node + node['left_leaves'] = find_leaves(splits, leaves, splits[left_child_node]) + if right_child_node in leaves: # if right child is a leaf node + node['right_leaves'].append(right_child_node) + else: # traverse its right node by calling function to find all the leaves from its right node + node['right_leaves'] = find_leaves(splits, leaves, splits[right_child_node]) + + # For each feature in each leaf, initialize lower and upper bounds to None + for th in features: + for leaf in leaves: + leaves[leaf]['bounds'][th] = [None, None] + for split in splits: + var = splits[split]['col'] + for leaf in splits[split]['left_leaves']: + leaves[leaf]['bounds'][var][1] = splits[split]['th'] + + for leaf in splits[split]['right_leaves']: + leaves[leaf]['bounds'][var][0] = splits[split]['th'] + + leaves_new = reassign_none_bounds(leaves, bounds) + splitting_thresholds = {} + for split in splits: + var = splits[split]['col'] + splitting_thresholds[var] = {} + for split in splits: + var = splits[split]['col'] + splitting_thresholds[var][split] = splits[split]['th'] + # Make sure every nested dictionary in the splitting_thresholds dictionary + # is sorted by value + for var in splitting_thresholds: + splitting_thresholds[var] = dict( + sorted(splitting_thresholds[var].items(), key=lambda x: x[1]) + ) + + return leaves_new, splits, splitting_thresholds + + +# Populate the "None" bounds with the bounding box bounds for a leaves-dict-tree +# amalgamation. +def reassign_none_bounds(leaves, input_bounds): + L = np.array(list(leaves.keys())) + features = np.arange(0, len(leaves[L[0]]['slope'])) + + for l in L: + for f in features: + if leaves[l]['bounds'][f][0] == None: + leaves[l]['bounds'][f][0] = input_bounds[f][0] + if leaves[l]['bounds'][f][1] == None: + leaves[l]['bounds'][f][1] = input_bounds[f][1] + return leaves + + +def find_leaves(splits, leaves, input_node): + root_node = input_node + leaves_list = [] + queue = [root_node] + while queue: + node = queue.pop() + node_left = node['children'][0] + node_right = node['children'][1] + if node_left in leaves: + leaves_list.append(node_left) + else: + queue.append(splits[node_left]) + if node_right in leaves: + leaves_list.append(node_right) + else: + queue.append(splits[node_right]) + return leaves_list + + @TransformationFactory.register( 'contrib.piecewise.nonlinear_to_pwl', doc="Convert nonlinear constraints and objectives to piecewise-linear approximations.", @@ -159,7 +295,7 @@ class NonlinearToPWL(Transformation): def __init__(self): super(Transformation).__init__() - + # TODO: ConfigDict def _apply_to( self, model, @@ -169,25 +305,12 @@ def _apply_to( allow_quadratic_objs=True, additively_decompose=True, ): - """Apply the transformation""" - - # Check ahead of time whether there are any unbounded variables. If - # there are, we'll have to bail out - # But deactivated variables can be left alone -- or should they be? - # Let's not, for now. - for v in model.component_objects(Var): - if None in v.bounds: - print( - "Error: cannot apply transformation to model with unbounded variables" - ) - raise NotImplementedError( - "Cannot apply transformation to model with unbounded variables" - ) + """TODO: docstring""" # Upcoming steps will trash the values of the vars, since I don't know # a better way. But what if the user set them with initialize= ? We'd # better restore them after we're done. - orig_var_map = {id(var): var.value for var in model.component_objects(Var)} + orig_var_map = {id(var): var.value for var in model.component_data_objects(Var)} # Now we are ready to start original_cons = list(model.component_data_objects(Constraint)) @@ -233,7 +356,7 @@ def _apply_to( ) # Before we're done, replace the old variable values - for var in model.component_objects(Var): + for var in model.component_data_objects(Var): var.value = orig_var_map[id(var)] @@ -262,11 +385,23 @@ def _check_skip_approx(expr, allow_quadratic, model): return False -def _replace_con(model, con, method, n, allow_quadratic_cons, additively_decompose): - vars = list(identify_variables(con.body)) - bounds = [(v.bounds[0], v.bounds[1]) for v in vars] +def _generate_bounds_list(vars_inner, con): + bounds = [] + for v in vars_inner: + if v.fixed: + bounds.append((value(v), value(v))) + elif None in v.bounds: + raise ValueError( + "Cannot automatically approximate constraints with unbounded " + "variables. Var '%s' appearining in component '%s' is missing " + "at least one bound" % (con.name, v.name)) + else: + bounds.append(v.bounds) + return bounds + - # Alright, let's do it like this. Additively decompose con.body and work on the pieces +def _replace_con(model, con, method, n, allow_quadratic_cons, additively_decompose): + # Additively decompose con.body and work on the pieces func_pieces = [] for k, expr in enumerate( _additively_decompose_expr(con.body) if additively_decompose else [con.body] @@ -278,7 +413,7 @@ def _replace_con(model, con, method, n, allow_quadratic_cons, additively_decompo continue vars_inner = list(identify_variables(expr)) - bounds = [(v.bounds[0], v.bounds[1]) for v in vars_inner] + bounds = _generate_bounds_list(vars_inner, con) def eval_con_func(*args): # sanity check @@ -323,9 +458,6 @@ def eval_con_func(*args): def _replace_obj(model, obj, method, n, allow_quadratic_obj, additively_decompose): - vars = list(identify_variables(obj)) - bounds = [(v.bounds[0], v.bounds[1]) for v in vars] - func_pieces = [] for k, expr in enumerate( _additively_decompose_expr(obj.expr) if additively_decompose else [obj.expr] @@ -337,7 +469,7 @@ def _replace_obj(model, obj, method, n, allow_quadratic_obj, additively_decompos continue vars_inner = list(identify_variables(expr)) - bounds = [(v.bounds[0], v.bounds[1]) for v in vars_inner] + bounds = _generate_bounds_list(vars_inner, obj) def eval_obj_func(*args): # sanity check From a96cd1074d9bcdcb555b98278226b034462df04f Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 14:42:11 -0600 Subject: [PATCH 1078/3044] Add a helpful comment --- pyomo/contrib/solver/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 918f436a212..756babc6b20 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -349,6 +349,8 @@ class LegacySolverWrapper: """ def __init__(self, **kwargs): + # There is no reason for a user to be trying to mix both old + # and new options. That is silly. So we will yell at them. if 'options' in kwargs and 'solver_options' in kwargs: raise ApplicationError( "Both 'options' and 'solver_options' were requested. " From 5cd0e653c2386e3e4436769c1464f98645091c6a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 14:48:57 -0600 Subject: [PATCH 1079/3044] Accidentally removed solver_io check --- pyomo/contrib/solver/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 756babc6b20..064c411c74d 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -349,6 +349,8 @@ class LegacySolverWrapper: """ def __init__(self, **kwargs): + if 'solver_io' in kwargs: + raise NotImplementedError('Still working on this') # There is no reason for a user to be trying to mix both old # and new options. That is silly. So we will yell at them. if 'options' in kwargs and 'solver_options' in kwargs: From 71709fd7bc6ed63ae82dca1ce05cbdcb1a4fcc36 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 15:04:05 -0600 Subject: [PATCH 1080/3044] Add information about options to docs --- .../developer_reference/solvers.rst | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 6168da3480e..94fb684236f 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -84,6 +84,37 @@ be used with other Pyomo tools / capabilities. ... 3 Declarations: x y obj +In keeping with our commitment to backwards compatibility, both the legacy and +future methods of specifying solver options are supported: + +.. testcode:: + :skipif: not ipopt_available + + import pyomo.environ as pyo + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + # Backwards compatible + status = pyo.SolverFactory('ipopt_v2').solve(model, options={'max_iter' : 6}) + # Forwards compatible + status = pyo.SolverFactory('ipopt_v2').solve(model, solver_options={'max_iter' : 6}) + model.pprint() + +.. testoutput:: + :skipif: not ipopt_available + :hide: + + 2 Var Declarations + ... + 3 Declarations: x y obj + Using the new interfaces directly ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From cd33b434126e77769462641f0cdc40cdfdab8211 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 28 Mar 2024 17:03:03 -0600 Subject: [PATCH 1081/3044] Consolidate into just self.options --- pyomo/contrib/solver/base.py | 9 +++------ pyomo/contrib/solver/tests/unit/test_base.py | 13 ++++--------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 064c411c74d..79e677b6226 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -361,7 +361,7 @@ def __init__(self, **kwargs): elif 'options' in kwargs: self.options = kwargs.pop('options') elif 'solver_options' in kwargs: - self.solver_options = kwargs.pop('solver_options') + self.options = kwargs.pop('solver_options') super().__init__(**kwargs) # @@ -408,17 +408,14 @@ def _map_config( self.config.report_timing = report_timing if hasattr(self, 'options'): self.config.solver_options.set_value(self.options) - if hasattr(self, 'solver_options'): - self.config.solver_options.set_value(self.solver_options) if (options is not NOTSET) and (solver_options is not NOTSET): # There is no reason for a user to be trying to mix both old # and new options. That is silly. So we will yell at them. # Example that would raise an error: # solver.solve(model, options={'foo' : 'bar'}, solver_options={'foo' : 'not_bar'}) raise ApplicationError( - "Both 'options' and 'solver_options' were declared " - "in the 'solve' call. Please use one or the other, " - "not both." + "Both 'options' and 'solver_options' were requested. " + "Please use one or the other, not both." ) elif options is not NOTSET: # This block is trying to mimic the existing logic in the legacy diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index 287116008ab..fb8020bedf6 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -311,14 +311,9 @@ def test_solver_options_behavior(self): # solver_options are also supported # Test case 1: set at instantiation solver = base.LegacySolverWrapper(solver_options={'max_iter': 6}) - self.assertEqual(solver.solver_options, {'max_iter': 6}) - - # Test case 2: Set later - solver = base.LegacySolverWrapper() - solver.solver_options = {'max_iter': 4, 'foo': 'bar'} - self.assertEqual(solver.solver_options, {'max_iter': 4, 'foo': 'bar'}) + self.assertEqual(solver.options, {'max_iter': 6}) - # Test case 3: pass some solver_options to the mapping (aka, 'solve' command) + # Test case 2: pass some solver_options to the mapping (aka, 'solve' command) solver = base.LegacySolverWrapper() config = ConfigDict(implicit=True) config.declare( @@ -329,7 +324,7 @@ def test_solver_options_behavior(self): solver._map_config(solver_options={'max_iter': 4}) self.assertEqual(solver.config.solver_options, {'max_iter': 4}) - # Test case 4: Set at instantiation and override during 'solve' call + # Test case 3: Set at instantiation and override during 'solve' call solver = base.LegacySolverWrapper(solver_options={'max_iter': 6}) config = ConfigDict(implicit=True) config.declare( @@ -339,7 +334,7 @@ def test_solver_options_behavior(self): solver.config = config solver._map_config(solver_options={'max_iter': 4}) self.assertEqual(solver.config.solver_options, {'max_iter': 4}) - self.assertEqual(solver.solver_options, {'max_iter': 6}) + self.assertEqual(solver.options, {'max_iter': 6}) # users can mix... sort of # Test case 1: Initialize with options, solve with solver_options From 33fd778cce713598dbcd84296b3922c2bf9ffa59 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 29 Mar 2024 15:09:56 -0600 Subject: [PATCH 1082/3044] Complete rewrite of nonlinear to piecewise linear --- .../piecewise/transform/nonlinear_to_pwl.py | 645 ++++++++++-------- 1 file changed, 378 insertions(+), 267 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 3db29af6784..fde08103446 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -9,11 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import enum import itertools from lineartree import LinearTreeRegressor import lineartree - +import logging import numpy as np from pyomo.environ import ( @@ -24,72 +25,54 @@ Objective, Any, value, + BooleanVar, + Connector, + Expression, + Suffix, + Param, + Set, + SetOf, + RangeSet, + Block, + ExternalFunction, + SortComponents, + LogicalConstraint ) +from pyomo.common.collections import ComponentMap, ComponentSet +from pyomo.common.config import ConfigDict, ConfigValue, PositiveInt, InEnum +from pyomo.common.modeling import unique_component_name from pyomo.core.expr.numeric_expr import SumExpression from pyomo.core.expr import identify_variables -from pyomo.repn.quadratic import QuadraticRepnVisitor from pyomo.core.expr import SumExpression +from pyomo.core.util import target_list from pyomo.contrib.piecewise import ( PiecewiseLinearExpression, PiecewiseLinearFunction ) +from pyomo.gdp import Disjunct, Disjunction +from pyomo.network import Port +from pyomo.repn.quadratic import QuadraticRepnVisitor from sklearn.linear_model import LinearRegression import random from sklearn.metrics import mean_squared_error from sklearn.model_selection import train_test_split +logger = logging.getLogger(__name__) -# TODO remove -MAX_DIM = 5 +class DomainPartitioningMethod(enum.IntEnum): + RANDOM_GRID = 1 + UNIFORM_GRID = 2 + LINEAR_MODEL_TREE_UNIFORM = 3 + LINEAR_MODEL_TREE_RANDOM = 4 # This should be safe to use many times; declare it globally _quadratic_repn_visitor = QuadraticRepnVisitor( subexpression_cache={}, var_map={}, var_order={}, sorter=None ) -def get_pwl_function_approximation(func, method, n, bounds, **kwargs): - """ - Get a piecewise-linear approximation to a function, given: - - func: function to approximate - method: method to use for the approximation, current options are: - - 'simple_random_point_grid' - - 'simple_uniform_point_grid' - - 'naive_lmt' - n: parameter controlling fineness of the approximation based on the specified method - bounds: list of tuples giving upper and lower bounds for each of func's arguments - kwargs: additional arguments to be specified to the method used - """ - - points = None - match (method): - case 'simple_random_point_grid': - points = get_simple_random_point_grid(bounds, n) - case 'simple_uniform_point_grid': - points = get_simple_uniform_point_grid(bounds, n) - case 'naive_lmt': - points = get_points_naive_lmt(bounds, n, func, randomize=True) - case 'naive_lmt_uniform': - points = get_points_naive_lmt(bounds, n, func, randomize=False) - case _: - raise NotImplementedError(f"Invalid method: {method}") - - # Default path: after getting the points, construct PWLF using the - # function-and-list-of-points constructor - - # DUCT TAPE WARNING: work around deficiency in PiecewiseLinearFunction constructor. TODO - dim = len(points[0]) - if dim == 1: - points = [pt[0] for pt in points] - - print( - f" Constructing PWLF with {len(points)} points, each of which are {dim}-dimensional" - ) - return PiecewiseLinearFunction(points=points, function=func) - -def get_simple_random_point_grid(bounds, n, seed=42): +def get_random_point_grid(bounds, n, func, seed=42): # Generate randomized grid of points linspaces = [] for (lb, ub) in bounds: @@ -98,7 +81,7 @@ def get_simple_random_point_grid(bounds, n, seed=42): return list(itertools.product(*linspaces)) -def get_simple_uniform_point_grid(bounds, n): +def get_uniform_point_grid(bounds, n, func): # Generate non-randomized grid of points linspaces = [] for (lb, ub) in bounds: @@ -111,15 +94,17 @@ def get_simple_uniform_point_grid(bounds, n): return list(itertools.product(*linspaces)) -# TODO this was copypasted from shumeng; make it better -def get_points_naive_lmt(bounds, n, func, seed=42, randomize=True): - - points = None - if randomize: - points = get_simple_random_point_grid(bounds, n, seed=seed) - else: - points = get_simple_uniform_point_grid(bounds, n) - # perturb(points, 0.01) +def get_points_lmt_random_sample(bounds, n, func, seed=42): + points = get_random_point_grid(bounds, n, func, seed=seed) + return get_points_lmt(points, bounds, func, seed) + + +def get_points_lmt_uniform_sample(bounds, n, func, seed=42): + points = get_uniform_point_grid(bounds, n, func) + return get_points_lmt(points, bounds, func, seed) + + +def get_points_lmt(points, bounds, func, seed): x_list = np.array(points) y_list = [] for point in points: @@ -153,6 +138,36 @@ def get_points_naive_lmt(bounds, n, func, seed=42, randomize=True): # here? return bound_point_list +_partition_method_dispatcher = { + DomainPartitioningMethod.RANDOM_GRID: get_random_point_grid, + DomainPartitioningMethod.UNIFORM_GRID: get_uniform_point_grid, + DomainPartitioningMethod.LINEAR_MODEL_TREE_UNIFORM: get_points_lmt_uniform_sample, + DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM: get_points_lmt_random_sample, +} + +def get_pwl_function_approximation(func, method, n, bounds): + """ + Get a piecewise-linear approximation of a function, given: + + func: function to approximate + method: method to use for the approximation, member of DomainPartitioningMethod + n: parameter controlling fineness of the approximation based on the specified method + bounds: list of tuples giving upper and lower bounds for each of func's arguments + """ + points = _partition_method_dispatcher[method](bounds, n, func) + + # DUCT TAPE WARNING: work around deficiency in PiecewiseLinearFunction + # constructor. TODO + dim = len(points[0]) + if dim == 1: + points = [pt[0] for pt in points] + + # After getting the points, construct PWLF using the + # function-and-list-of-points constructor + logger.debug(f"Constructing PWLF with {len(points)} points, each of which " + f"are {dim}-dimensional") + return PiecewiseLinearFunction(points=points, function=func) + # TODO: this is still horrible. Maybe I should put these back together into # a wrapper class again, but better this time? @@ -213,11 +228,13 @@ def parse_linear_tree_regressor(linear_tree_regressor, bounds): node['left_leaves'], node['right_leaves'] = [], [] if left_child_node in leaves: # if left child is a leaf node node['left_leaves'].append(left_child_node) - else: # traverse its left node by calling function to find all the leaves from its left node + else: # traverse its left node by calling function to find all the + # leaves from its left node node['left_leaves'] = find_leaves(splits, leaves, splits[left_child_node]) if right_child_node in leaves: # if right child is a leaf node node['right_leaves'].append(right_child_node) - else: # traverse its right node by calling function to find all the leaves from its right node + else: # traverse its right node by calling function to find all the + # leaves from its right node node['right_leaves'] = find_leaves(splits, leaves, splits[right_child_node]) # For each feature in each leaf, initialize lower and upper bounds to None @@ -250,6 +267,16 @@ def parse_linear_tree_regressor(linear_tree_regressor, bounds): return leaves_new, splits, splitting_thresholds +# This doesn't catch all additively separable expressions--we really need a +# walker (as does gdp.partition_disjuncts) +def _additively_decompose_expr(input_expr): + if input_expr.__class__ is not SumExpression: + # This isn't separable, so we just have the one expression + return [input_expr] + # else, it was a SumExpression, and we will break it into the summands + return list(input_expr.args) + + # Populate the "None" bounds with the bounding box bounds for a leaves-dict-tree # amalgamation. def reassign_none_bounds(leaves, input_bounds): @@ -286,227 +313,311 @@ def find_leaves(splits, leaves, input_node): @TransformationFactory.register( 'contrib.piecewise.nonlinear_to_pwl', - doc="Convert nonlinear constraints and objectives to piecewise-linear approximations.", + doc="Convert nonlinear constraints and objectives to piecewise-linear " + "approximations.", ) class NonlinearToPWL(Transformation): """ Convert nonlinear constraints and objectives to piecewise-linear approximations. """ - + CONFIG = ConfigDict('contrib.piecewise.nonlinear_to_pwl') + CONFIG.declare( + 'targets', + ConfigValue( + default=None, + domain=target_list, + description="target or list of targets that will be approximated", + doc=""" + This specifies the list of components to approximate. If None (default), + the entire model is transformed. Note that if the transformation is + done out of place, the list of targets should be attached to the model + before it is cloned, and the list will specify the targets on the cloned + instance.""", + ), + ) + CONFIG.declare( + 'num_points', + ConfigValue( + default=3, + domain=PositiveInt, + description="Number of breakpoints for each piecewise-linear approximation", + doc=""" + Specifies the number of points in each function domain to triangulate in + order to construct the piecewise-linear approximation. Must be an integer + greater than 1.""", + ), + ) + CONFIG.declare( + 'domain_partitioning_method', + ConfigValue( + default=DomainPartitioningMethod.UNIFORM_GRID, + domain=InEnum(DomainPartitioningMethod), + description="Method for sampling points that will partition function " + "domains.", + doc=""" + The method by which the points used to partition each function domain + are selected. By default, the range of each variable is partitioned + uniformly, however it is possible to sample randomly or to use the + partitions from training a linear model tree based on either uniform + or random samples of the ranges.""", + ), + ) + CONFIG.declare( + 'approximate_quadratic_constraints', + ConfigValue( + default=True, + domain=bool, + description="Whether or not to approximate quadratic constraints.", + doc=""" + Whether or not to calculate piecewise-linear approximations for + quadratic constraints. If True, the resulting approximation will be + a mixed-integer linear program. If False, the resulting approximation + will be a mixed-integer quadratic program.""", + ), + ) + CONFIG.declare( + 'approximate_quadratic_objectives', + ConfigValue( + default=True, + domain=bool, + description="Whether or not to approximate quadratic objectives.", + doc=""" + Whether or not to calculate piecewise-linear approximations for + quadratic objectives. If True, the resulting approximation will be + a mixed-integer linear program. If False, the resulting approximation + will be a mixed-integer quadratic program.""", + ), + ) + CONFIG.declare( + 'additively_decompose', + ConfigValue( + default=False, + domain=bool, + description="Whether or not to additively decompose constraints and " + "approximate the summands separately.", + doc=""" + If False, each nonlinear constraint expression will be approximated by + exactly one piecewise-linear function. If True, constraints will be + additively decomposed, and each of the resulting summands will be + approximated by a separate piecewise-linear function. + + It is recommended to leave this False as long as no nonlinear constraint + involves more than about 5-6 variables. For constraints with higher- + dimmensional nonlinear functions, additive decomposition will improve + the scalability of the approximation (since paritioning the domain is + subject to the curse of dimensionality).""", + ), + ) + CONFIG.declare( + 'max_dimension', + ConfigValue( + default=5, + domain=PositiveInt, + description="The maximum dimension of functions that will be approximated.", + doc=""" + Specifies the maximum dimension function the transformation should + attempt to approximate. If a nonlinear function dimension exceeds + 'max_dimension' the transformation will log a warning and leave the + expression as-is. For functions with dimension significantly the default + (5), it is likely that this transformation will stall triangulating the + points in order to partition the function domain.""", + ), + ) def __init__(self): super(Transformation).__init__() - # TODO: ConfigDict - def _apply_to( - self, - model, - n=3, - method='simple_uniform_point_grid', - allow_quadratic_cons=True, - allow_quadratic_objs=True, - additively_decompose=True, - ): - """TODO: docstring""" - - # Upcoming steps will trash the values of the vars, since I don't know - # a better way. But what if the user set them with initialize= ? We'd - # better restore them after we're done. - orig_var_map = {id(var): var.value for var in model.component_data_objects(Var)} - - # Now we are ready to start - original_cons = list(model.component_data_objects(Constraint)) - original_objs = list(model.component_data_objects(Objective)) - - model._pwl_quadratic_count = 0 - model._pwl_nonlinear_count = 0 - - # Let's put all our new constraints in one big index - model._pwl_cons = Constraint(Any) - - for con in original_cons: - repn = _quadratic_repn_visitor.walk_expression(con.body) - if repn.nonlinear is None: - if repn.quadratic is None: - # Linear constraint. Always skip. - continue - else: - model._pwl_quadratic_count += 1 - if allow_quadratic_cons: - continue + self._handlers = { + Constraint: self._transform_constraint, + Objective: self._transform_objective, + Var: False, + BooleanVar: False, + Connector: False, + Expression: False, + Suffix: False, + Param: False, + Set: False, + SetOf: False, + RangeSet: False, + Disjunction: False, + Disjunct: self._transform_block_components, + Block: self._transform_block_components, + ExternalFunction: False, + Port: False, + PiecewiseLinearFunction: False, + LogicalConstraint: False, + } + self._transformation_blocks = {} + self._transformation_block_set = ComponentSet() + + def _apply_to(self, instance, **kwds): + try: + self._apply_to_impl(instance, **kwds) + finally: + self._transformation_blocks.clear() + self._transformation_block_set.clear() + + def _apply_to_impl( self, model, **kwds): + config = self.CONFIG(kwds.pop('options', {})) + config.set_value(kwds) + + targets = config.targets + if targets is None: + targets = (model,) + + for target in targets: + if target.ctype is Block or target.ctype is Disjunct: + self._transform_block_components(target, config) + elif target.ctype is Constraint: + self._transform_constraint(target, config) + elif target.ctype is Objective: + self._transform_objective(target, config) else: - model._pwl_nonlinear_count += 1 - _replace_con( - model, con, method, n, allow_quadratic_cons, additively_decompose - ) + raise ValueError( + "Target '%s' is not a Block, Constraint, or Objective. It " + "is of type '%s' and cannot be transformed." + % (target.name, type(t)) + ) + + def _get_transformation_block(self, parent): + if parent in self._transformation_blocks: + return self._transformation_blocks[parent] + + nm = unique_component_name( + parent, '_pyomo_contrib_nonlinear_to_pwl' + ) + self._transformation_blocks[parent] = transBlock = Block() + parent.add_component(nm, transBlock) + self._transformation_block_set.add(transBlock) - # And do the same for objectives - for obj in original_objs: - repn = _quadratic_repn_visitor.walk_expression(obj) - if repn.nonlinear is None: - if repn.quadratic is None: - # Linear objective. Skip. + transBlock._pwl_cons = Constraint(Any) + return transBlock + + def _transform_block_components(self, block, config): + blocks = block.values() if block.is_indexed() else (block,) + for b in blocks: + for obj in b.component_objects( + active=True, + descend_into=False, + sort=SortComponents.deterministic + ): + if obj in self._transformation_block_set: + # This is a Block we created--we know we don't need to look + # on it. + continue + handler = self._handlers.get(obj.ctype, None) + if not handler: + if handler is None: + raise RuntimeError( + "No transformation handler registered for modeling " + "components of type '%s'." % obj.ctype + ) continue - else: - model._pwl_quadratic_count += 1 - if allow_quadratic_objs: - continue + handler(obj, config) + + def _transform_constraint(self, cons, config): + trans_block = self._get_transformation_block(cons.parent_block()) + constraints = cons.values() if cons.is_indexed() else (cons,) + for c in constraints: + pw_approx = self._approximate_expression( + c.body, c, trans_block, config, + config.approximate_quadratic_constraints) + + if pw_approx is None: + # Didn't need approximated, nothing to do + continue + + trans_block._pwl_cons[c.name, len(trans_block._pwl_cons)] = (c.lower, + pw_approx, + c.upper) + # deactivate original + c.deactivate() + + def _transform_objective(self, objective, config): + trans_block = self._get_transformation_block(objective.parent_block()) + objectives = objective.values() if objective.is_indexed() else (objective,) + for obj in objectives: + pw_approx = self._approximate_expression( + obj.expr, obj, trans_block, config, + config.approximate_quadratic_objectives) + + if pw_approx is None: + # Didn't need approximated, nothing to do + continue + + trans_block.add_component( + unique_component_name(trans_block, obj.name), + Objective(expr=pw_approx, sense=obj.sense) + ) + obj.deactivate() + + def _get_bounds_list(self, var_list, parent_component): + bounds = [] + for v in var_list: + if None in v.bounds: + raise ValueError( + "Cannot automatically approximate constraints with unbounded " + "variables. Var '%s' appearining in component '%s' is missing " + "at least one bound" % (con.name, v.name)) else: - model._pwl_nonlinear_count += 1 - _replace_obj( - model, obj, method, n, allow_quadratic_objs, additively_decompose + bounds.append(v.bounds) + return bounds + + def _needs_approximating(self, expr, approximate_quadratic): + repn = _quadratic_repn_visitor.walk_expression(expr) + if repn.nonlinear is None: + if repn.quadratic is None: + # Linear constraint. Always skip. + return False + else: + if not approximate_quadratic: + # Didn't need approximated, nothing to do + return False + return True + + def _approximate_expression(self, obj, parent_component, trans_block, + config, approximate_quadratic): + if not self._needs_approximating(obj, approximate_quadratic): + return + + # Additively decompose obj and work on the pieces + pwl_func = 0 + for k, expr in enumerate(_additively_decompose_expr(obj) if + config.additively_decompose else (obj,)): + # First check is this is a good idea + expr_vars = list(identify_variables(expr, include_fixed=False)) + orig_values = ComponentMap((v, v.value) for v in expr_vars) + + dim = len(expr_vars) + if dim > config.max_dimension: + logger.warning( + "Not approximating expression for component '%s' as " + "it exceeds the maximum dimension of %s. Try increasing " + "'max_dimension' or additively separating the expression." + % (parent_component.name, config.max_dimension)) + pwl_func += expr + continue + elif not self._needs_approximating(expr, approximate_quadratic): + pwl_func += expr + continue + + def eval_expr(*args): + for i, v in enumerate(expr_vars): + v.value = args[i] + return value(expr) + + pwlf = get_pwl_function_approximation( + eval_expr, config.domain_partitioning_method, + config.num_points, + self._get_bounds_list(expr_vars, parent_component) ) + name = unique_component_name( + trans_block, + parent_component.getname(fully_qualified=False) + ) + trans_block.add_component(f"_pwle_{name}_{k}", pwlf) + pwl_func += pwlf(*expr_vars) - # Before we're done, replace the old variable values - for var in model.component_data_objects(Var): - var.value = orig_var_map[id(var)] - - -# Check whether a term should be skipped for approximation. Do not touch -# model's quadratic or nonlinear counts; those are only for top-level -# expressions which were already checked -def _check_skip_approx(expr, allow_quadratic, model): - repn = _quadratic_repn_visitor.walk_expression(expr) - if repn.nonlinear is None: - if repn.quadratic is None: - # Linear expression. Skip. - return True - else: - # model._pwl_quadratic_count += 1 - if allow_quadratic: - return True - else: - pass - # model._pwl_nonlinear_count += 1 - dim = len(list(identify_variables(expr))) - if dim > MAX_DIM: - print(f"Refusing to approximate function with {dim}-dimensional component.") - raise RuntimeError( - f"Refusing to approximate function with {dim}-dimensional component." - ) - return False - - -def _generate_bounds_list(vars_inner, con): - bounds = [] - for v in vars_inner: - if v.fixed: - bounds.append((value(v), value(v))) - elif None in v.bounds: - raise ValueError( - "Cannot automatically approximate constraints with unbounded " - "variables. Var '%s' appearining in component '%s' is missing " - "at least one bound" % (con.name, v.name)) - else: - bounds.append(v.bounds) - return bounds - - -def _replace_con(model, con, method, n, allow_quadratic_cons, additively_decompose): - # Additively decompose con.body and work on the pieces - func_pieces = [] - for k, expr in enumerate( - _additively_decompose_expr(con.body) if additively_decompose else [con.body] - ): - # First, check if we actually need to do anything - if _check_skip_approx(expr, allow_quadratic_cons, model): - # We're skipping this term. Just add expr directly to the pieces - func_pieces.append(expr) - continue - - vars_inner = list(identify_variables(expr)) - bounds = _generate_bounds_list(vars_inner, con) - - def eval_con_func(*args): - # sanity check - assert len(args) == len( - vars_inner - ), f"eval_con_func was called with {len(args)} arguments, but expected {len(vars_inner)}" - for i, v in enumerate(vars_inner): - v.value = args[i] - return value(con.body) - - pwlf = get_pwl_function_approximation(eval_con_func, method, n, bounds) - - con_name = con.getname(fully_qualified=False) - model.add_component(f"_pwle_{con_name}_{k}", pwlf) - # func_pieces.append(pwlf(*vars_inner).expr) - func_pieces.append(pwlf(*vars_inner)) - - pwl_func = sum(func_pieces) - - # Change the constraint. This is hard to do in-place, so I'll - # remake it and deactivate the old one as was done originally. - - # Now we need a ton of if statements to properly set up the constraint - if con.equality: - model._pwl_cons[str(con)] = pwl_func == con.ub - elif con.strict_lower: - model._pwl_cons[str(con)] = pwl_func > con.lb - elif con.strict_upper: - model._pwl_cons[str(con)] = pwl_func < con.ub - elif con.has_lb(): - if con.has_ub(): # constraint is of the form lb <= expr <= ub - model._pwl_cons[str(con)] = (con.lb, pwl_func, con.ub) - else: - model._pwl_cons[str(con)] = pwl_func >= con.lb - elif con.has_ub(): - model._pwl_cons[str(con)] = pwl_func <= con.ub - else: - assert ( - False - ), f"unreachable: original Constraint '{con_name}' did not have any upper or lower bound" - con.deactivate() - - -def _replace_obj(model, obj, method, n, allow_quadratic_obj, additively_decompose): - func_pieces = [] - for k, expr in enumerate( - _additively_decompose_expr(obj.expr) if additively_decompose else [obj.expr] - ): - # First, check if we actually need to do anything - if _check_skip_approx(expr, allow_quadratic_obj, model): - # We're skipping this term. Just add expr directly to the pieces - func_pieces.append(expr) - continue - - vars_inner = list(identify_variables(expr)) - bounds = _generate_bounds_list(vars_inner, obj) - - def eval_obj_func(*args): - # sanity check - assert len(args) == len( - vars_inner - ), f"eval_obj_func was called with {len(args)} arguments, but expected {len(vars_inner)}" - for i, v in enumerate(vars_inner): - v.value = args[i] - return value(obj) - - pwlf = get_pwl_function_approximation(eval_obj_func, method, n, bounds) - - obj_name = obj.getname(fully_qualified=False) - model.add_component(f"_pwle_{obj_name}_{k}", pwlf) - func_pieces.append(pwlf(*vars_inner)) - - pwl_func = sum(func_pieces[1:], func_pieces[0]) - - # Add the new objective - obj_name = obj.getname(fully_qualified=False) - # model.add_component(f"_pwle_{obj_name}", pwl_func) - model.add_component( - f"_pwl_obj_{obj_name}", Objective(expr=pwl_func, sense=obj.sense) - ) - obj.deactivate() - + # restore var values + for v, val in orig_values.items(): + v.value = val -# Copypasted from gdp/plugins/partition_disjuncts.py for now. This is the -# stupid approach that will not properly catch all additive separability; to do -# it better we need a walker. -def _additively_decompose_expr(input_expr): - if input_expr.__class__ is not SumExpression: - # print(f"couldn't decompose: input_expr.__class__ was {input_expr.__class__}, not SumExpression") - # This isn't separable, so we just have the one expression - return [input_expr] - # else, it was a SumExpression, and we will break it into the summands - summands = list(input_expr.args) - # print(f"len(summands) is {len(summands)}") - # print(f"summands is {summands}") - return summands + return pwl_func From 3271a0ead174d17ad2135fb1bf44b65cf163b024 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 29 Mar 2024 15:10:46 -0600 Subject: [PATCH 1083/3044] Adding handler to safely ignore LogicalConstraints in piecewise linear to GDP transformations --- .../piecewise/transform/piecewise_to_gdp_transformation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py index 2e056c47a15..f36c222b4e0 100644 --- a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py +++ b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py @@ -31,6 +31,7 @@ Connector, SortComponents, Any, + LogicalConstraint, ) from pyomo.core.base import Transformation from pyomo.core.base.block import _BlockData, Block @@ -102,6 +103,7 @@ def __init__(self): ExternalFunction: False, Port: False, PiecewiseLinearFunction: self._transform_piecewise_linear_function, + LogicalConstraint: False, } self._transformation_blocks = {} From 159766a4e39e00190fa3da0c501a1e04549b2441 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 29 Mar 2024 17:01:01 -0600 Subject: [PATCH 1084/3044] Starting to add tests for nonlinear to pw linear transformation --- .../piecewise/tests/test_nonlinear_to_pwl.py | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py new file mode 100644 index 00000000000..4d7af9dc7a6 --- /dev/null +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -0,0 +1,88 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.contrib.piecewise import PiecewiseLinearFunction +from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( + NonlinearToPWL, + DomainPartitioningMethod +) +from pyomo.core.expr.compare import ( + assertExpressionsStructurallyEqual, +) +from pyomo.environ import ( + ConcreteModel, + Var, + Constraint, + TransformationFactory, + log, +) + +## debug +from pytest import set_trace + +class TestNonlinearToPWL_1D(unittest.TestCase): + def make_model(self): + m = ConcreteModel() + m.x = Var(bounds=(1, 10)) + m.cons = Constraint(expr=log(m.x) >= 0.35) + + return m + + def test_log_constraint_uniform_grid(self): + m = self.make_model() + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, + num_points=3, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + ) + + # cons is transformed + self.assertFalse(m.cons.active) + + pwlf = list(m.component_data_objects(PiecewiseLinearFunction, + descend_into=True)) + self.assertEqual(len(pwlf), 1) + pwlf = pwlf[0] + + points = [(1.0009,), (5.5,), (9.9991,)] + self.assertEqual(pwlf._simplices, [(0, 1), (1, 2)]) + self.assertEqual(pwlf._points, points) + self.assertEqual(len(pwlf._linear_functions), 2) + + x1 = 1.0009 + x2 = 5.5 + assertExpressionsStructurallyEqual( + self, + pwlf._linear_functions[0](m.x), + ((log(x2) - log(x1))/(x2 - x1))*m.x + + (log(x2) - ((log(x2) - log(x1))/(x2 - x1))*x2), + places=7 + ) + x1 = 5.5 + x2 = 9.9991 + assertExpressionsStructurallyEqual( + self, + pwlf._linear_functions[1](m.x), + ((log(x2) - log(x1))/(x2 - x1))*m.x + + (log(x2) - ((log(x2) - log(x1))/(x2 - x1))*x2), + places=7 + ) + + self.assertEqual(len(pwlf._expressions), 1) + new_cons = n_to_pwl.get_transformed_component(m.cons) + self.assertTrue(new_cons.active) + self.assertIs(new_cons.body, pwlf._expressions[id(new_cons.body.expr)]) + self.assertIsNone(new_cons.ub) + self.assertEqual(new_cons.lb, 0.35) + self.assertIs(n_to_pwl.get_src_component(new_cons), m.cons) From 4d8e1c3d7c6229dcfe712d7f71e125bc74f127cc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 31 Mar 2024 13:23:48 -0600 Subject: [PATCH 1085/3044] Move from isinstance to ctype for verifying component type --- .../piecewise_to_gdp_transformation.py | 4 +-- .../core/plugins/transform/add_slack_vars.py | 5 ++-- pyomo/core/plugins/transform/scaling.py | 6 ++--- pyomo/repn/plugins/nl_writer.py | 25 +++++++++++-------- pyomo/util/calc_var_value.py | 4 +-- pyomo/util/report_scaling.py | 4 +-- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py index 779bb601c71..5417cbc17f4 100644 --- a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py +++ b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py @@ -33,7 +33,7 @@ Any, ) from pyomo.core.base import Transformation -from pyomo.core.base.block import BlockData, Block +from pyomo.core.base.block import Block from pyomo.core.util import target_list from pyomo.gdp import Disjunct, Disjunction from pyomo.gdp.util import is_child_of @@ -147,7 +147,7 @@ def _apply_to_impl(self, instance, **kwds): self._transform_piecewise_linear_function( t, config.descend_into_expressions ) - elif t.ctype is Block or isinstance(t, BlockData): + elif issubclass(t.ctype, Block): self._transform_block(t, config.descend_into_expressions) elif t.ctype is Constraint: if not config.descend_into_expressions: diff --git a/pyomo/core/plugins/transform/add_slack_vars.py b/pyomo/core/plugins/transform/add_slack_vars.py index 0007f8de7ad..39903384729 100644 --- a/pyomo/core/plugins/transform/add_slack_vars.py +++ b/pyomo/core/plugins/transform/add_slack_vars.py @@ -23,7 +23,6 @@ from pyomo.core.plugins.transform.hierarchy import NonIsomorphicTransformation from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.core.base import ComponentUID -from pyomo.core.base.constraint import ConstraintData from pyomo.common.deprecation import deprecation_warning @@ -42,7 +41,7 @@ def target_list(x): # [ESJ 07/15/2020] We have to just pass it through because we need the # instance in order to be able to do anything about it... return [x] - elif isinstance(x, (Constraint, ConstraintData)): + elif getattr(x, 'ctype', None) is Constraint: return [x] elif hasattr(x, '__iter__'): ans = [] @@ -53,7 +52,7 @@ def target_list(x): deprecation_msg = None # same as above... ans.append(i) - elif isinstance(i, (Constraint, ConstraintData)): + elif getattr(i, 'ctype', None) is Constraint: ans.append(i) else: raise ValueError( diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index ef418f094ae..e962352668c 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -15,8 +15,6 @@ Var, Constraint, Objective, - ConstraintData, - ObjectiveData, Suffix, value, ) @@ -197,7 +195,7 @@ def _apply_to(self, model, rename=True): already_scaled.add(id(c)) # perform the constraint/objective scaling and variable sub scaling_factor = component_scaling_factor_map[c] - if isinstance(c, ConstraintData): + if c.ctype is Constraint: body = scaling_factor * replace_expressions( expr=c.body, substitution_map=variable_substitution_dict, @@ -226,7 +224,7 @@ def _apply_to(self, model, rename=True): else: c.set_value((lower, body, upper)) - elif isinstance(c, ObjectiveData): + elif c.ctype is Objective: c.expr = scaling_factor * replace_expressions( expr=c.expr, substitution_map=variable_substitution_dict, diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 8cc73b5e3fe..76599f74228 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -437,6 +437,7 @@ def store(self, obj, val): self.values[obj] = val def compile(self, column_order, row_order, obj_order, model_id): + var_con_obj = {Var, Constraint, Objective} missing_component_data = ComponentSet() unknown_data = ComponentSet() queue = [self.values.items()] @@ -462,18 +463,20 @@ def compile(self, column_order, row_order, obj_order, model_id): self.obj[obj_order[_id]] = val elif _id == model_id: self.prob[0] = val - elif isinstance(obj, (VarData, ConstraintData, ObjectiveData)): - missing_component_data.add(obj) - elif isinstance(obj, (Var, Constraint, Objective)): - # Expand this indexed component to store the - # individual ComponentDatas, but ONLY if the - # component data is not in the original dictionary - # of values that we extracted from the Suffixes - queue.append( - product( - filterfalse(self.values.__contains__, obj.values()), (val,) + elif getattr(obj, 'ctype', None) in var_con_obj: + if obj.is_indexed(): + # Expand this indexed component to store the + # individual ComponentDatas, but ONLY if the + # component data is not in the original dictionary + # of values that we extracted from the Suffixes + queue.append( + product( + filterfalse(self.values.__contains__, obj.values()), + (val,), + ) ) - ) + else: + missing_component_data.add(obj) else: unknown_data.add(obj) if missing_component_data: diff --git a/pyomo/util/calc_var_value.py b/pyomo/util/calc_var_value.py index 254b82c59cd..156ad56dffb 100644 --- a/pyomo/util/calc_var_value.py +++ b/pyomo/util/calc_var_value.py @@ -12,7 +12,7 @@ from pyomo.common.errors import IterationLimitError from pyomo.common.numeric_types import native_numeric_types, native_complex_types, value from pyomo.core.expr.calculus.derivatives import differentiate -from pyomo.core.base.constraint import Constraint, ConstraintData +from pyomo.core.base.constraint import Constraint import logging @@ -81,7 +81,7 @@ def calculate_variable_from_constraint( """ # Leverage all the Constraint logic to process the incoming tuple/expression - if not isinstance(constraint, ConstraintData): + if not getattr(constraint, 'ctype', None) is Constraint: constraint = Constraint(expr=constraint, name=type(constraint).__name__) constraint.construct() diff --git a/pyomo/util/report_scaling.py b/pyomo/util/report_scaling.py index 7619662c482..02b3710c334 100644 --- a/pyomo/util/report_scaling.py +++ b/pyomo/util/report_scaling.py @@ -13,7 +13,7 @@ import math from pyomo.core.base.block import BlockData from pyomo.common.collections import ComponentSet -from pyomo.core.base.var import VarData +from pyomo.core.base.var import Var from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd import logging @@ -73,7 +73,7 @@ def _check_coefficients( ): ders = reverse_sd(expr) for _v, _der in ders.items(): - if isinstance(_v, VarData): + if getattr(_v, 'ctype', None) is Var: if _v.is_fixed(): continue der_lb, der_ub = compute_bounds_on_expr(_der) From 812a1b7fd80be976c5cba7ff93c5bc68d9f795da Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 31 Mar 2024 18:52:18 -0600 Subject: [PATCH 1086/3044] Remove giant status if tree --- pyomo/contrib/solver/gurobi_direct.py | 65 +++++++++++++-------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 7b5ec6ed904..f5f1bca7184 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -88,6 +88,7 @@ class GurobiDirect(SolverBase): _available = None _num_instances = 0 + _tc_map = None def __init__(self, **kwds): super().__init__(**kwds) @@ -256,7 +257,6 @@ def _postsolve(self, timer: HierarchicalTimer, loader): config = self._config gprob = loader._grb_model - grb = gurobipy.GRB status = gprob.Status results = Results() @@ -264,45 +264,16 @@ def _postsolve(self, timer: HierarchicalTimer, loader): results.timing_info.gurobi_time = gprob.Runtime if gprob.SolCount > 0: - if status == grb.OPTIMAL: + if status == gurobipy.GRB.OPTIMAL: results.solution_status = SolutionStatus.optimal else: results.solution_status = SolutionStatus.feasible else: results.solution_status = SolutionStatus.noSolution - if status == grb.LOADED: # problem is loaded, but no solution - results.termination_condition = TerminationCondition.unknown - elif status == grb.OPTIMAL: # optimal - results.termination_condition = ( - TerminationCondition.convergenceCriteriaSatisfied - ) - elif status == grb.INFEASIBLE: - results.termination_condition = TerminationCondition.provenInfeasible - elif status == grb.INF_OR_UNBD: - results.termination_condition = TerminationCondition.infeasibleOrUnbounded - elif status == grb.UNBOUNDED: - results.termination_condition = TerminationCondition.unbounded - elif status == grb.CUTOFF: - results.termination_condition = TerminationCondition.objectiveLimit - elif status == grb.ITERATION_LIMIT: - results.termination_condition = TerminationCondition.iterationLimit - elif status == grb.NODE_LIMIT: - results.termination_condition = TerminationCondition.iterationLimit - elif status == grb.TIME_LIMIT: - results.termination_condition = TerminationCondition.maxTimeLimit - elif status == grb.SOLUTION_LIMIT: - results.termination_condition = TerminationCondition.unknown - elif status == grb.INTERRUPTED: - results.termination_condition = TerminationCondition.interrupted - elif status == grb.NUMERIC: - results.termination_condition = TerminationCondition.unknown - elif status == grb.SUBOPTIMAL: - results.termination_condition = TerminationCondition.unknown - elif status == grb.USER_OBJ_LIMIT: - results.termination_condition = TerminationCondition.objectiveLimit - else: - results.termination_condition = TerminationCondition.unknown + results.termination_condition = self._get_tc_map().get( + status, TerminationCondition.unknown + ) if ( results.termination_condition @@ -310,7 +281,9 @@ def _postsolve(self, timer: HierarchicalTimer, loader): and config.raise_exception_on_nonoptimal_result ): raise RuntimeError( - 'Solver did not find the optimal solution. Set opt.config.raise_exception_on_nonoptimal_result = False to bypass this error.' + 'Solver did not find the optimal solution. Set ' + 'opt.config.raise_exception_on_nonoptimal_result=False ' + 'to bypass this error.' ) results.incumbent_objective = None @@ -348,3 +321,25 @@ def _postsolve(self, timer: HierarchicalTimer, loader): timer.stop('load solution') return results + + def _get_tc_map(self): + if GurobiDirect._tc_map is None: + grb = gurobipy.GRB + tc = TerminationCondition + GurobiDirect._tc_map = { + grb.LOADED: tc.unknown, # problem is loaded, but no solution + grb.OPTIMAL: tc.convergenceCriteriaSatisfied, + grb.INFEASIBLE: tc.provenInfeasible, + grb.INF_OR_UNBD: tc.infeasibleOrUnbounded, + grb.UNBOUNDED: tc.unbounded, + grb.CUTOFF: tc.objectiveLimit, + grb.ITERATION_LIMIT: tc.iterationLimit, + grb.NODE_LIMIT: tc.iterationLimit, + grb.TIME_LIMIT: tc.maxTimeLimit, + grb.SOLUTION_LIMIT: tc.unknown, + grb.INTERRUPTED: tc.interrupted, + grb.NUMERIC: tc.unknown, + grb.SUBOPTIMAL: tc.unknown, + grb.USER_OBJ_LIMIT: tc.objectiveLimit, + } + return GurobiDirect._tc_map From 9d4b9131c76b337a41ff12cf4ea6f55062ae3115 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 31 Mar 2024 19:41:34 -0600 Subject: [PATCH 1087/3044] NFC: apply black --- pyomo/core/plugins/transform/scaling.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index e962352668c..11d4ac8c493 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -10,14 +10,7 @@ # ___________________________________________________________________________ from pyomo.common.collections import ComponentMap -from pyomo.core.base import ( - Block, - Var, - Constraint, - Objective, - Suffix, - value, -) +from pyomo.core.base import Block, Var, Constraint, Objective, Suffix, value from pyomo.core.plugins.transform.hierarchy import Transformation from pyomo.core.base import TransformationFactory from pyomo.core.base.suffix import SuffixFinder From 711fafe517ce8f4d21a89c462e9083a40dfb55ca Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 31 Mar 2024 19:42:37 -0600 Subject: [PATCH 1088/3044] NFC: apply black --- pyomo/contrib/solver/gurobi_direct.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index f5f1bca7184..1164686f0f1 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -328,7 +328,7 @@ def _get_tc_map(self): tc = TerminationCondition GurobiDirect._tc_map = { grb.LOADED: tc.unknown, # problem is loaded, but no solution - grb.OPTIMAL: tc.convergenceCriteriaSatisfied, + grb.OPTIMAL: tc.convergenceCriteriaSatisfied, grb.INFEASIBLE: tc.provenInfeasible, grb.INF_OR_UNBD: tc.infeasibleOrUnbounded, grb.UNBOUNDED: tc.unbounded, From 8c4fb774a30a621e7890dd006cc9e83931c545e6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 31 Mar 2024 20:17:52 -0600 Subject: [PATCH 1089/3044] Remove debugging; expand comment explaining difference in cut-and-paste tests --- pyomo/repn/tests/test_standard_form.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/tests/test_standard_form.py b/pyomo/repn/tests/test_standard_form.py index 591703e6ae8..4c66ae87c41 100644 --- a/pyomo/repn/tests/test_standard_form.py +++ b/pyomo/repn/tests/test_standard_form.py @@ -256,11 +256,11 @@ def test_alternative_forms(self): [[1, 0, 2, 0], [0, 0, 1, 4], [0, 1, 6, 0], [0, 1, 6, 0], [1, 1, 0, 0]] ) self.assertTrue(np.all(repn.A == ref)) - print(repn) - print(repn.b) self.assertTrue(np.all(repn.b == np.array([3, 5, 6, -3, 8]))) self.assertTrue(np.all(repn.c == np.array([[-1, 0, -5, 0], [1, 0, 0, 15]]))) - # Note that the solution is a mix of inequality and equality constraints + # Note that the mixed_form solution is a mix of inequality and + # equality constraints, so we cannot (easily) reuse the + # _verify_solutions helper (as in the above cases): # self._verify_solution(soln, repn, False) repn = LinearStandardFormCompiler().write( From 7d9d490e05a2dca6a4b97f731c383295c8b40970 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 1 Apr 2024 13:04:36 -0600 Subject: [PATCH 1090/3044] Change check for options --- pyomo/contrib/solver/base.py | 19 +++++++++---------- pyomo/contrib/solver/tests/unit/test_base.py | 5 ++--- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 79e677b6226..9c19fccaa89 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -353,14 +353,13 @@ def __init__(self, **kwargs): raise NotImplementedError('Still working on this') # There is no reason for a user to be trying to mix both old # and new options. That is silly. So we will yell at them. - if 'options' in kwargs and 'solver_options' in kwargs: - raise ApplicationError( - "Both 'options' and 'solver_options' were requested. " - "Please use one or the other, not both." - ) - elif 'options' in kwargs: - self.options = kwargs.pop('options') - elif 'solver_options' in kwargs: + self.options = kwargs.pop('options', None) + if 'solver_options' in kwargs: + if self.options is not None: + raise ValueError( + "Both 'options' and 'solver_options' were requested. " + "Please use one or the other, not both." + ) self.options = kwargs.pop('solver_options') super().__init__(**kwargs) @@ -406,14 +405,14 @@ def _map_config( self.config.time_limit = timelimit if report_timing is not NOTSET: self.config.report_timing = report_timing - if hasattr(self, 'options'): + if self.options is not None: self.config.solver_options.set_value(self.options) if (options is not NOTSET) and (solver_options is not NOTSET): # There is no reason for a user to be trying to mix both old # and new options. That is silly. So we will yell at them. # Example that would raise an error: # solver.solve(model, options={'foo' : 'bar'}, solver_options={'foo' : 'not_bar'}) - raise ApplicationError( + raise ValueError( "Both 'options' and 'solver_options' were requested. " "Please use one or the other, not both." ) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index fb8020bedf6..b52f96ba903 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -14,7 +14,6 @@ from pyomo.common import unittest from pyomo.common.config import ConfigDict from pyomo.contrib.solver import base -from pyomo.common.errors import ApplicationError class TestSolverBase(unittest.TestCase): @@ -351,7 +350,7 @@ def test_solver_options_behavior(self): # users CANNOT initialize both values at the same time, because how # do we know what to do with it then? # Test case 1: Class instance - with self.assertRaises(ApplicationError): + with self.assertRaises(ValueError): solver = base.LegacySolverWrapper( options={'max_iter': 6}, solver_options={'max_iter': 4} ) @@ -363,7 +362,7 @@ def test_solver_options_behavior(self): ConfigDict(implicit=True, description="Options to pass to the solver."), ) solver.config = config - with self.assertRaises(ApplicationError): + with self.assertRaises(ValueError): solver._map_config(solver_options={'max_iter': 4}, options={'max_iter': 6}) def test_map_results(self): From bfb9beb7488e722aa6b336b8a4c89ce39561a81c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 1 Apr 2024 13:28:51 -0600 Subject: [PATCH 1091/3044] March 2024: Typos Update --- .github/workflows/typos.toml | 24 ++++++++++++++++++++++++ pyomo/contrib/latex_printer/__init__.py | 13 +------------ pyomo/core/base/component.py | 4 ++-- pyomo/core/base/indexed_component.py | 2 +- pyomo/core/base/reference.py | 2 +- pyomo/gdp/plugins/fix_disjuncts.py | 2 +- pyomo/gdp/tests/models.py | 2 +- pyomo/network/foqus_graph.py | 4 ++-- pyomo/solvers/plugins/solvers/GAMS.py | 20 ++++++++++---------- 9 files changed, 43 insertions(+), 30 deletions(-) diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index 23f94fc8afd..f98a6122ffd 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -40,4 +40,28 @@ WRONLY = "WRONLY" Hax = "Hax" # Big Sur Sur = "Sur" +# Ignore the shorthand ans for and +ans = "ans" +# Ignore the keyword arange +arange = "arange" +# Ignore IIS +IIS = "IIS" +iis = "iis" +# Ignore PN +PN = "PN" +# Ignore hd +hd = "hd" +# Ignore opf +opf = "opf" +# Ignore FRE +FRE = "FRE" +# Ignore MCH +MCH = "MCH" +# Ignore RO +ro = "ro" +RO = "RO" +# Ignore EOF - end of file +EOF = "EOF" +# Ignore lst as shorthand for list +lst = "lst" # AS NEEDED: Add More Words Below diff --git a/pyomo/contrib/latex_printer/__init__.py b/pyomo/contrib/latex_printer/__init__.py index c434b53dfe1..02eaa636a36 100644 --- a/pyomo/contrib/latex_printer/__init__.py +++ b/pyomo/contrib/latex_printer/__init__.py @@ -9,22 +9,11 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2023 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - # Recommended just to build all of the appropriate things import pyomo.environ # Remove one layer of .latex_printer -# import statemnt is now: +# import statement is now: # from pyomo.contrib.latex_printer import latex_printer try: from pyomo.contrib.latex_printer.latex_printer import latex_printer diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 22c2bc4b804..9b1929daa06 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -368,7 +368,7 @@ def pprint(self, ostream=None, verbose=False, prefix=""): @property def name(self): - """Get the fully qualifed component name.""" + """Get the fully qualified component name.""" return self.getname(fully_qualified=True) # Adding a setter here to help users adapt to the new @@ -664,7 +664,7 @@ def getname(self, fully_qualified=False, name_buffer=None, relative_to=None): @property def name(self): - """Get the fully qualifed component name.""" + """Get the fully qualified component name.""" return self.getname(fully_qualified=True) # Allow setting a component's name if it is not owned by a parent diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index e1be613d666..37a62e5c4d7 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -731,7 +731,7 @@ def __delitem__(self, index): # this supports "del m.x[:,1]" through a simple recursive call if index.__class__ is IndexedComponent_slice: - # Assert that this slice ws just generated + # Assert that this slice was just generated assert len(index._call_stack) == 1 # Make a copy of the slicer items *before* we start # iterating over it (since we will be removing items!). diff --git a/pyomo/core/base/reference.py b/pyomo/core/base/reference.py index 2279db067a6..84cccec9749 100644 --- a/pyomo/core/base/reference.py +++ b/pyomo/core/base/reference.py @@ -579,7 +579,7 @@ def Reference(reference, ctype=NOTSET): :py:class:`IndexedComponent`. If the indices associated with wildcards in the component slice all - refer to the same :py:class:`Set` objects for all data identifed by + refer to the same :py:class:`Set` objects for all data identified by the slice, then the resulting indexed component will be indexed by the product of those sets. However, if all data do not share common set objects, or only a subset of indices in a multidimentional set diff --git a/pyomo/gdp/plugins/fix_disjuncts.py b/pyomo/gdp/plugins/fix_disjuncts.py index 44a9d91d513..172363caab7 100644 --- a/pyomo/gdp/plugins/fix_disjuncts.py +++ b/pyomo/gdp/plugins/fix_disjuncts.py @@ -52,7 +52,7 @@ class GDP_Disjunct_Fixer(Transformation): This reclassifies all disjuncts in the passed model instance as ctype Block and deactivates the constraints and disjunctions within inactive disjuncts. - In addition, it transforms relvant LogicalConstraints and BooleanVars so + In addition, it transforms relevant LogicalConstraints and BooleanVars so that the resulting model is a (MI)(N)LP (where it is only mixed-integer if the model contains integer-domain Vars or BooleanVars which were not indicator_vars of Disjuncs. diff --git a/pyomo/gdp/tests/models.py b/pyomo/gdp/tests/models.py index 0b84641899c..2995cacb450 100644 --- a/pyomo/gdp/tests/models.py +++ b/pyomo/gdp/tests/models.py @@ -840,7 +840,7 @@ def makeAnyIndexedDisjunctionOfDisjunctDatas(): build from DisjunctDatas. Identical mathematically to makeDisjunctionOfDisjunctDatas. - Used to test that the right things happen for a case where soemone + Used to test that the right things happen for a case where someone implements an algorithm which iteratively generates disjuncts and retransforms""" m = ConcreteModel() diff --git a/pyomo/network/foqus_graph.py b/pyomo/network/foqus_graph.py index e4cf3b92014..d904fa54008 100644 --- a/pyomo/network/foqus_graph.py +++ b/pyomo/network/foqus_graph.py @@ -358,9 +358,9 @@ def scc_calculation_order(self, sccNodes, ie, oe): done = False for i in range(len(sccNodes)): for j in range(len(sccNodes)): - for ine in ie[i]: + for in_e in ie[i]: for oute in oe[j]: - if ine == oute: + if in_e == oute: adj[j].append(i) adjR[i].append(j) done = True diff --git a/pyomo/solvers/plugins/solvers/GAMS.py b/pyomo/solvers/plugins/solvers/GAMS.py index be3499a2f6b..606098e5b7b 100644 --- a/pyomo/solvers/plugins/solvers/GAMS.py +++ b/pyomo/solvers/plugins/solvers/GAMS.py @@ -198,8 +198,8 @@ def _get_version(self): return _extract_version('') from gams import GamsWorkspace - ws = GamsWorkspace() - version = tuple(int(i) for i in ws._version.split('.')[:4]) + workspace = GamsWorkspace() + version = tuple(int(i) for i in workspace._version.split('.')[:4]) while len(version) < 4: version += (0,) return version @@ -209,8 +209,8 @@ def _run_simple_model(self, n): try: from gams import GamsWorkspace, DebugLevel - ws = GamsWorkspace(debug=DebugLevel.Off, working_directory=tmpdir) - t1 = ws.add_job_from_string(self._simple_model(n)) + workspace = GamsWorkspace(debug=DebugLevel.Off, working_directory=tmpdir) + t1 = workspace.add_job_from_string(self._simple_model(n)) t1.run() return True except: @@ -330,12 +330,12 @@ def solve(self, *args, **kwds): if tmpdir is not None and os.path.exists(tmpdir): newdir = False - ws = GamsWorkspace( + workspace = GamsWorkspace( debug=DebugLevel.KeepFiles if keepfiles else DebugLevel.Off, working_directory=tmpdir, ) - t1 = ws.add_job_from_string(output_file.getvalue()) + t1 = workspace.add_job_from_string(output_file.getvalue()) try: with OutputStream(tee=tee, logfile=logfile) as output_stream: @@ -349,7 +349,7 @@ def solve(self, *args, **kwds): # Always name working directory or delete files, # regardless of any errors. if keepfiles: - print("\nGAMS WORKING DIRECTORY: %s\n" % ws.working_directory) + print("\nGAMS WORKING DIRECTORY: %s\n" % workspace.working_directory) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted @@ -359,7 +359,7 @@ def solve(self, *args, **kwds): except: # Catch other errors and remove files first if keepfiles: - print("\nGAMS WORKING DIRECTORY: %s\n" % ws.working_directory) + print("\nGAMS WORKING DIRECTORY: %s\n" % workspace.working_directory) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted @@ -398,7 +398,7 @@ def solve(self, *args, **kwds): extract_rc = 'rc' in model_suffixes results = SolverResults() - results.problem.name = os.path.join(ws.working_directory, t1.name + '.gms') + results.problem.name = os.path.join(workspace.working_directory, t1.name + '.gms') results.problem.lower_bound = t1.out_db["OBJEST"].find_record().value results.problem.upper_bound = t1.out_db["OBJEST"].find_record().value results.problem.number_of_variables = t1.out_db["NUMVAR"].find_record().value @@ -587,7 +587,7 @@ def solve(self, *args, **kwds): results.solution.insert(soln) if keepfiles: - print("\nGAMS WORKING DIRECTORY: %s\n" % ws.working_directory) + print("\nGAMS WORKING DIRECTORY: %s\n" % workspace.working_directory) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted From ee92b7001c599a373c520be75097fdac05cfdd68 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 1 Apr 2024 13:30:52 -0600 Subject: [PATCH 1092/3044] Comment is misleading --- .github/workflows/typos.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index f98a6122ffd..4d69cde34e1 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -40,7 +40,7 @@ WRONLY = "WRONLY" Hax = "Hax" # Big Sur Sur = "Sur" -# Ignore the shorthand ans for and +# Ignore the shorthand ans for answer ans = "ans" # Ignore the keyword arange arange = "arange" From a69c1b8615fd58db538de28186f54fcd6b594cb9 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 1 Apr 2024 13:35:51 -0600 Subject: [PATCH 1093/3044] Address @jsiirola 's comments and apply black --- pyomo/network/foqus_graph.py | 4 ++-- pyomo/solvers/plugins/solvers/GAMS.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pyomo/network/foqus_graph.py b/pyomo/network/foqus_graph.py index d904fa54008..7c6c05256d9 100644 --- a/pyomo/network/foqus_graph.py +++ b/pyomo/network/foqus_graph.py @@ -359,8 +359,8 @@ def scc_calculation_order(self, sccNodes, ie, oe): for i in range(len(sccNodes)): for j in range(len(sccNodes)): for in_e in ie[i]: - for oute in oe[j]: - if in_e == oute: + for out_e in oe[j]: + if in_e == out_e: adj[j].append(i) adjR[i].append(j) done = True diff --git a/pyomo/solvers/plugins/solvers/GAMS.py b/pyomo/solvers/plugins/solvers/GAMS.py index 606098e5b7b..c0bab4dc23e 100644 --- a/pyomo/solvers/plugins/solvers/GAMS.py +++ b/pyomo/solvers/plugins/solvers/GAMS.py @@ -349,7 +349,9 @@ def solve(self, *args, **kwds): # Always name working directory or delete files, # regardless of any errors. if keepfiles: - print("\nGAMS WORKING DIRECTORY: %s\n" % workspace.working_directory) + print( + "\nGAMS WORKING DIRECTORY: %s\n" % workspace.working_directory + ) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted @@ -398,7 +400,9 @@ def solve(self, *args, **kwds): extract_rc = 'rc' in model_suffixes results = SolverResults() - results.problem.name = os.path.join(workspace.working_directory, t1.name + '.gms') + results.problem.name = os.path.join( + workspace.working_directory, t1.name + '.gms' + ) results.problem.lower_bound = t1.out_db["OBJEST"].find_record().value results.problem.upper_bound = t1.out_db["OBJEST"].find_record().value results.problem.number_of_variables = t1.out_db["NUMVAR"].find_record().value From 6e182bed965e98ead3578c1a2d79244c93760e00 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 2 Apr 2024 11:29:38 +0200 Subject: [PATCH 1094/3044] Add MAiNGO to test_perisistent_solvers.py --- .../solvers/tests/test_persistent_solvers.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index ae189aca701..c063adc2bfe 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -17,7 +17,7 @@ parameterized = parameterized.parameterized from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available -from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs +from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs, MAiNGO from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression import os @@ -36,11 +36,23 @@ ('cplex', Cplex), ('cbc', Cbc), ('highs', Highs), + ('maingo', MAiNGO), ] -mip_solvers = [('gurobi', Gurobi), ('cplex', Cplex), ('cbc', Cbc), ('highs', Highs)] -nlp_solvers = [('ipopt', Ipopt)] -qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt), ('cplex', Cplex)] -miqcqp_solvers = [('gurobi', Gurobi), ('cplex', Cplex)] +mip_solvers = [ + ('gurobi', Gurobi), + ('cplex', Cplex), + ('cbc', Cbc), + ('highs', Highs), + ('maingo', MAiNGO), +] +nlp_solvers = [('ipopt', Ipopt), ('maingo', MAiNGO)] +qcp_solvers = [ + ('gurobi', Gurobi), + ('ipopt', Ipopt), + ('cplex', Cplex), + ('maingo', MAiNGO), +] +miqcqp_solvers = [('gurobi', Gurobi), ('cplex', Cplex), ('maingo', MAiNGO)] only_child_vars_options = [True, False] From fc4bf437bc6ba5919bebed55ef24f95a77a80aa2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 2 Apr 2024 10:26:48 -0600 Subject: [PATCH 1095/3044] Skip test under CyIpopt 1.4.0 --- .../contrib/pynumero/examples/tests/test_cyipopt_examples.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py index 408a0197382..bcd3b5d8bf5 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py @@ -266,6 +266,11 @@ def test_cyipopt_functor(self): s = df['ca_bal'] self.assertAlmostEqual(s.iloc[6], 0, places=3) + @unittest.skipIf( + cyipopt_core.__version__ == "1.4.0", + "Terminating Ipopt through a user callback is broken in CyIpopt 1.4.0 " + "(see mechmotum/cyipopt#249", + ) def test_cyipopt_callback_halt(self): ex = import_file( os.path.join(example_dir, 'callback', 'cyipopt_callback_halt.py') From 38d49d43293c45f50ccfbc9fbe7e3d57ed638a03 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 2 Apr 2024 10:27:20 -0600 Subject: [PATCH 1096/3044] Fix bug in retrieving cyipopt version --- .../contrib/pynumero/algorithms/solvers/cyipopt_solver.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py index cdea542295b..53616298415 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py @@ -319,7 +319,13 @@ def license_is_valid(self): return True def version(self): - return tuple(int(_) for _ in cyipopt.__version__.split(".")) + def _int(x): + try: + return int(x) + except: + return x + + return tuple(_int(_) for _ in cyipopt_interface.cyipopt.__version__.split(".")) def solve(self, model, **kwds): config = self.config(kwds, preserve_implicit=True) From c74427aeee68050d21c7d80929f9a1d440253c26 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 2 Apr 2024 10:27:43 -0600 Subject: [PATCH 1097/3044] Fix import from a deprecated location --- .../contrib/pynumero/examples/tests/test_cyipopt_examples.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py index bcd3b5d8bf5..55dccd6a0ed 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py @@ -44,11 +44,13 @@ raise unittest.SkipTest("Pynumero needs the ASL extension to run CyIpopt tests") import pyomo.contrib.pynumero.algorithms.solvers.cyipopt_solver as cyipopt_solver +from pyomo.contrib.pynumero.interfaces.cyipopt_interface import cyipopt_available -if not cyipopt_solver.cyipopt_available: +if not cyipopt_available: raise unittest.SkipTest("PyNumero needs CyIpopt installed to run CyIpopt tests") import cyipopt as cyipopt_core + example_dir = os.path.join(this_file_dir(), '..') From 55c78059667d5412df28cab3dd0905e66aa197da Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 2 Apr 2024 10:35:02 -0600 Subject: [PATCH 1098/3044] NFC: fix a message typo --- pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py index 55dccd6a0ed..a0e17df918a 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py @@ -271,7 +271,7 @@ def test_cyipopt_functor(self): @unittest.skipIf( cyipopt_core.__version__ == "1.4.0", "Terminating Ipopt through a user callback is broken in CyIpopt 1.4.0 " - "(see mechmotum/cyipopt#249", + "(see mechmotum/cyipopt#249)", ) def test_cyipopt_callback_halt(self): ex = import_file( From 50e2316be4d928694efb53fb11ea94546ee24e9f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 2 Apr 2024 10:38:37 -0600 Subject: [PATCH 1099/3044] Use our version() method to test cyipopt version --- pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py index a0e17df918a..2df43c1e797 100644 --- a/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py +++ b/pyomo/contrib/pynumero/examples/tests/test_cyipopt_examples.py @@ -269,7 +269,7 @@ def test_cyipopt_functor(self): self.assertAlmostEqual(s.iloc[6], 0, places=3) @unittest.skipIf( - cyipopt_core.__version__ == "1.4.0", + cyipopt_solver.PyomoCyIpoptSolver().version() == (1, 4, 0), "Terminating Ipopt through a user callback is broken in CyIpopt 1.4.0 " "(see mechmotum/cyipopt#249)", ) From 643865c5f1828c581609f8920d3c0fe8b0c4da41 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 2 Apr 2024 11:27:10 -0600 Subject: [PATCH 1100/3044] Change to NaNs except for objectives --- pyomo/contrib/solver/base.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 9c19fccaa89..6359b49d945 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -465,13 +465,14 @@ def _map_results(self, model, results): ] legacy_soln.status = legacy_solution_status_map[results.solution_status] legacy_results.solver.termination_message = str(results.termination_condition) - legacy_results.problem.number_of_constraints = len( - list(model.component_map(ctype=Constraint)) - ) - legacy_results.problem.number_of_variables = len( - list(model.component_map(ctype=Var)) + legacy_results.problem.number_of_constraints = float('nan') + legacy_results.problem.number_of_variables = float('nan') + number_of_objectives = sum( + 1 + for _ in model.component_data_objects( + Objective, active=True, descend_into=True + ) ) - number_of_objectives = len(list(model.component_map(ctype=Objective))) legacy_results.problem.number_of_objectives = number_of_objectives if number_of_objectives == 1: obj = get_objective(model) From 1456e56857f4b45fbab945034f6916fff416799a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 2 Apr 2024 11:31:37 -0600 Subject: [PATCH 1101/3044] Remove extra solutions logic, per commit to @andrewlee94's IDAES branch --- pyomo/contrib/solver/base.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 6359b49d945..cec392271f6 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -499,15 +499,7 @@ def _solution_handler( """Method to handle the preferred action for the solution""" symbol_map = SymbolMap() symbol_map.default_labeler = NumericLabeler('x') - try: - model.solutions.add_symbol_map(symbol_map) - except AttributeError: - # Something wacky happens in IDAES due to the usage of ScalarBlock - # instead of PyomoModel. This is an attempt to fix that. - from pyomo.core.base.PyomoModel import ModelSolutions - - setattr(model, 'solutions', ModelSolutions(model)) - model.solutions.add_symbol_map(symbol_map) + model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) delete_legacy_soln = True if load_solutions: From f2fd07c893fe5d13a6926407324fe4594a799f75 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 2 Apr 2024 13:49:44 -0600 Subject: [PATCH 1102/3044] Skip Tests on Draft and WIP Pull Requests --- .github/workflows/test_pr_and_main.yml | 9 ++++++++- doc/OnlineDocs/contribution_guide.rst | 13 ++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 76ec6de951a..72366eb1353 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -7,6 +7,11 @@ on: pull_request: branches: - main + types: + - opened + - reopened + - synchronize + - ready_for_review workflow_dispatch: inputs: git-ref: @@ -34,6 +39,8 @@ jobs: lint: name: lint/style-and-typos runs-on: ubuntu-latest + if: | + contains(github.event.pull_request.title, '[WIP]') != true && !github.event.pull_request.draft steps: - name: Checkout Pyomo source uses: actions/checkout@v4 @@ -733,7 +740,7 @@ jobs: cover: name: process-coverage-${{ matrix.TARGET }} needs: build - if: always() # run even if a build job fails + if: success() || failure() # run even if a build job fails, but not if cancelled runs-on: ${{ matrix.os }} timeout-minutes: 10 strategy: diff --git a/doc/OnlineDocs/contribution_guide.rst b/doc/OnlineDocs/contribution_guide.rst index 10670627546..285bb656406 100644 --- a/doc/OnlineDocs/contribution_guide.rst +++ b/doc/OnlineDocs/contribution_guide.rst @@ -71,6 +71,10 @@ at least 70% coverage of the lines modified in the PR and prefer coverage closer to 90%. We also require that all tests pass before a PR will be merged. +.. note:: + If you are having issues getting tests to pass on your Pull Request, + please tag any of the core developers to ask for help. + The Pyomo main branch provides a Github Actions workflow (configured in the ``.github/`` directory) that will test any changes pushed to a branch with a subset of the complete test harness that includes @@ -82,13 +86,16 @@ This will enable the tests to run automatically with each push to your fork. At any point in the development cycle, a "work in progress" pull request may be opened by including '[WIP]' at the beginning of the PR -title. This allows your code changes to be tested by the full suite of -Pyomo's automatic -testing infrastructure. Any pull requests marked '[WIP]' will not be +title. Any pull requests marked '[WIP]' or draft will not be reviewed or merged by the core development team. However, any '[WIP]' pull request left open for an extended period of time without active development may be marked 'stale' and closed. +.. note:: + Draft and WIP Pull Requests will **NOT** trigger tests. This is an effort to + keep our backlog as available as possible. Please liberally use the provided + branch testing for draft functionality. + Python Version Support ++++++++++++++++++++++ From b082419502a7c024990c28a54d91bb3069bea011 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 2 Apr 2024 13:57:37 -0600 Subject: [PATCH 1103/3044] Change language for branch test request --- doc/OnlineDocs/contribution_guide.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/contribution_guide.rst b/doc/OnlineDocs/contribution_guide.rst index 285bb656406..6054a8d2ba9 100644 --- a/doc/OnlineDocs/contribution_guide.rst +++ b/doc/OnlineDocs/contribution_guide.rst @@ -93,8 +93,8 @@ active development may be marked 'stale' and closed. .. note:: Draft and WIP Pull Requests will **NOT** trigger tests. This is an effort to - keep our backlog as available as possible. Please liberally use the provided - branch testing for draft functionality. + keep our backlog as available as possible. Please make use of the provided + branch test suite for evaluating / testing draft functionality. Python Version Support ++++++++++++++++++++++ From a3d6a430f21cd624aebf4559a7d60c8b5df12663 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 15:29:18 -0600 Subject: [PATCH 1104/3044] Removing unused imports --- pyomo/contrib/gdpopt/tests/test_gdpopt.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/contrib/gdpopt/tests/test_gdpopt.py b/pyomo/contrib/gdpopt/tests/test_gdpopt.py index 3ac532116aa..873bafabc76 100644 --- a/pyomo/contrib/gdpopt/tests/test_gdpopt.py +++ b/pyomo/contrib/gdpopt/tests/test_gdpopt.py @@ -22,8 +22,6 @@ from pyomo.common.collections import Bunch from pyomo.common.config import ConfigDict, ConfigValue from pyomo.common.fileutils import import_file, PYOMO_ROOT_DIR -from pyomo.contrib.appsi.base import Solver -from pyomo.contrib.appsi.solvers.gurobi import Gurobi from pyomo.contrib.gdpopt.create_oa_subproblems import ( add_util_block, add_disjunct_list, From 009f0d9af07c726977fa401502bc26fca52249c7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 2 Apr 2024 15:37:44 -0600 Subject: [PATCH 1105/3044] Replace ProblemSense and the constants in kernel.objective with ObjectiveSense enum --- pyomo/common/enums.py | 38 ++++++++++++++++++++++++++++++++++ pyomo/core/kernel/objective.py | 5 +---- pyomo/opt/results/problem.py | 38 +++++++++++++++++++++++----------- 3 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 pyomo/common/enums.py diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py new file mode 100644 index 00000000000..c685843b41c --- /dev/null +++ b/pyomo/common/enums.py @@ -0,0 +1,38 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import enum + +from pyomo.common.deprecation import RenamedClass + +class ObjectiveSense(enum.IntEnum): + """Flag indicating if an objective is minimizing (1) or maximizing (-1). + + While the numeric values are arbitrary, there are parts of Pyomo + that rely on this particular choice of value. These values are also + consistent with some solvers (notably Gurobi). + + """ + minimize = 1 + maximize = -1 + + # Overloading __str__ is needed to match the behavior of the old + # pyutilib.enum class (removed June 2020). There are spots in the + # code base that expect the string representation for items in the + # enum to not include the class name. New uses of enum shouldn't + # need to do this. + def __str__(self): + return self.name + +minimize = ObjectiveSense.minimize +maximize = ObjectiveSense.maximize + + diff --git a/pyomo/core/kernel/objective.py b/pyomo/core/kernel/objective.py index 9aa8e3315ef..840c7cfd7e0 100644 --- a/pyomo/core/kernel/objective.py +++ b/pyomo/core/kernel/objective.py @@ -9,15 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from pyomo.common.enums import minimize, maximize from pyomo.core.expr.numvalue import as_numeric from pyomo.core.kernel.base import _abstract_readwrite_property from pyomo.core.kernel.container_utils import define_simple_containers from pyomo.core.kernel.expression import IExpression -# Constants used to define the optimization sense -minimize = 1 -maximize = -1 - class IObjective(IExpression): """ diff --git a/pyomo/opt/results/problem.py b/pyomo/opt/results/problem.py index 98f749f3aeb..e35bb155355 100644 --- a/pyomo/opt/results/problem.py +++ b/pyomo/opt/results/problem.py @@ -12,19 +12,33 @@ import enum from pyomo.opt.results.container import MapContainer +from pyomo.common.deprecation import deprecated, deprecation_warning +from pyomo.common.enums import ObjectiveSense -class ProblemSense(str, enum.Enum): - unknown = 'unknown' - minimize = 'minimize' - maximize = 'maximize' - # Overloading __str__ is needed to match the behavior of the old - # pyutilib.enum class (removed June 2020). There are spots in the - # code base that expect the string representation for items in the - # enum to not include the class name. New uses of enum shouldn't - # need to do this. - def __str__(self): - return self.value +class ProblemSenseType(type): + @deprecated( + "pyomo.opt.results.problem.ProblemSense has been replaced by " + "pyomo.common.enums.ObjectiveSense", + version="6.7.2.dev0", + ) + def __getattr__(cls, attr): + if attr == 'minimize': + return ObjectiveSense.minimize + if attr == 'maximize': + return ObjectiveSense.maximize + if attr == 'unknown': + deprecation_warning( + "ProblemSense.unknown is no longer an allowable option. " + "Mapping 'unknown' to 'minimize'", + version="6.7.2.dev0", + ) + return ObjectiveSense.minimize + raise AttributeError(attr) + + +class ProblemSense(metaclass=ProblemSenseType): + pass class ProblemInformation(MapContainer): @@ -40,4 +54,4 @@ def __init__(self): self.declare('number_of_integer_variables') self.declare('number_of_continuous_variables') self.declare('number_of_nonzeros') - self.declare('sense', value=ProblemSense.unknown, required=True) + self.declare('sense', value=ProblemSense.minimize, required=True) From f295490fa203e5963f70a47204e15c2bcc98f8bf Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 21:23:15 -0600 Subject: [PATCH 1106/3044] Adding mapping between original and transformed components and starting on more tests --- .../piecewise/tests/test_nonlinear_to_pwl.py | 30 +++++++++++++ .../piecewise/transform/nonlinear_to_pwl.py | 45 +++++++++++++++++-- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 4d7af9dc7a6..020edee3924 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -36,6 +36,10 @@ def make_model(self): m.cons = Constraint(expr=log(m.x) >= 0.35) return m + + def check_pw_linear_log_x(self, m, points): + x1 = points[0][0] + x2 = points def test_log_constraint_uniform_grid(self): m = self.make_model() @@ -86,3 +90,29 @@ def test_log_constraint_uniform_grid(self): self.assertIsNone(new_cons.ub) self.assertEqual(new_cons.lb, 0.35) self.assertIs(n_to_pwl.get_src_component(new_cons), m.cons) + + def test_log_constraint_random_grid(self): + m = self.make_model() + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + # [ESJ 3/30/24]: The seed is actually set in the function for getting + # the points right now, so this will be deterministic. + n_to_pwl.apply_to( + m, + num_points=3, + domain_partitioning_method=DomainPartitioningMethod.RANDOM_GRID, + ) + + # cons is transformed + self.assertFalse(m.cons.active) + + pwlf = list(m.component_data_objects(PiecewiseLinearFunction, + descend_into=True)) + self.assertEqual(len(pwlf), 1) + pwlf = pwlf[0] + + set_trace() + points = [(4.370861069626263,), (7.587945476302646,), (9.556428757689245,)] + self.assertEqual(pwlf._simplices, [(0, 1), (1, 2)]) + self.assertEqual(pwlf._points, points) + self.assertEqual(len(pwlf._linear_functions), 2) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index fde08103446..d819e893d5f 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -38,6 +38,7 @@ SortComponents, LogicalConstraint ) +from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import ComponentMap, ComponentSet from pyomo.common.config import ConfigDict, ConfigValue, PositiveInt, InEnum from pyomo.common.modeling import unique_component_name @@ -71,6 +72,13 @@ class DomainPartitioningMethod(enum.IntEnum): subexpression_cache={}, var_map={}, var_order={}, sorter=None ) +class _NonlinearToPWLTransformationData(AutoSlots.Mixin): + __slots__ = ('transformed_component', 'src_component') + + def __init__(self): + self.transformed_component = ComponentMap() + self.src_component = ComponentMap() +Block.register_private_data_initializer(_NonlinearToPWLTransformationData) def get_random_point_grid(bounds, n, func, seed=42): # Generate randomized grid of points @@ -515,6 +523,8 @@ def _transform_block_components(self, block, config): def _transform_constraint(self, cons, config): trans_block = self._get_transformation_block(cons.parent_block()) + trans_data_dict = trans_block.private_data() + src_data_dict = cons.parent_block().private_data() constraints = cons.values() if cons.is_indexed() else (cons,) for c in constraints: pw_approx = self._approximate_expression( @@ -525,15 +535,20 @@ def _transform_constraint(self, cons, config): # Didn't need approximated, nothing to do continue - trans_block._pwl_cons[c.name, len(trans_block._pwl_cons)] = (c.lower, - pw_approx, - c.upper) + idx = len(trans_block._pwl_cons) + trans_block._pwl_cons[c.name, idx] = (c.lower, pw_approx, c.upper) + new_cons = trans_block._pwl_cons[c.name, idx] + trans_data_dict.src_component[new_cons] = c + src_data_dict.transformed_component[c] = new_cons + # deactivate original c.deactivate() def _transform_objective(self, objective, config): trans_block = self._get_transformation_block(objective.parent_block()) + trans_data_dict = trans_block.private_data() objectives = objective.values() if objective.is_indexed() else (objective,) + src_data_dict = objective.parent_block().private_data() for obj in objectives: pw_approx = self._approximate_expression( obj.expr, obj, trans_block, config, @@ -543,10 +558,14 @@ def _transform_objective(self, objective, config): # Didn't need approximated, nothing to do continue + new_obj = Objective(expr=pw_approx, sense=obj.sense) trans_block.add_component( unique_component_name(trans_block, obj.name), - Objective(expr=pw_approx, sense=obj.sense) + new_obj ) + trans_data_dict.src_component[new_obj] = obj + src_data_dict.transformed_component[obj] = new_obj + obj.deactivate() def _get_bounds_list(self, var_list, parent_component): @@ -621,3 +640,21 @@ def eval_expr(*args): v.value = val return pwl_func + + def get_src_component(self, cons): + data = cons.parent_block().private_data().src_component + if cons in data: + return data[cons] + else: + raise ValueError( + "It does not appear that '%s' is a transformed Constraint " + "created by the 'nonlinear_to_pwl' transformation." % cons.name) + + def get_transformed_component(self, cons): + data = cons.parent_block().private_data().transformed_component + if cons in data: + return data[cons] + else: + raise ValueError( + "It does not appear that '%s' is a Constraint that was " + "transformed by the 'nonlinear_to_pwl' transformation." % cons.name) From 75f577fb65f1eb5febb1e18a716ad02816194fc5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 21:34:21 -0600 Subject: [PATCH 1107/3044] Removing a base class I didn't use --- .../cp/scheduling_expr/sequence_expressions.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py index d88504ac7e4..865a914b847 100644 --- a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py @@ -11,15 +11,8 @@ from pyomo.core.expr.logical_expr import BooleanExpression -# ESJ TODO: The naming in this file needs more thought, and it appears I do not -# need the base class. - -class SequenceVarExpression(BooleanExpression): - pass - - -class NoOverlapExpression(SequenceVarExpression): +class NoOverlapExpression(BooleanExpression): """ Expression representing that none of the IntervalVars in a SequenceVar overlap (if they are scheduled) @@ -35,7 +28,7 @@ def _to_string(self, values, verbose, smap): return "no_overlap(%s)" % values[0] -class FirstInSequenceExpression(SequenceVarExpression): +class FirstInSequenceExpression(BooleanExpression): """ Expression representing that the specified IntervalVar is the first in the sequence specified by SequenceVar (if it is scheduled) @@ -52,7 +45,7 @@ def _to_string(self, values, verbose, smap): return "first_in(%s, %s)" % (values[0], values[1]) -class LastInSequenceExpression(SequenceVarExpression): +class LastInSequenceExpression(BooleanExpression): """ Expression representing that the specified IntervalVar is the last in the sequence specified by SequenceVar (if it is scheduled) @@ -69,7 +62,7 @@ def _to_string(self, values, verbose, smap): return "last_in(%s, %s)" % (values[0], values[1]) -class BeforeInSequenceExpression(SequenceVarExpression): +class BeforeInSequenceExpression(BooleanExpression): """ Expression representing that one IntervalVar occurs before another in the sequence specified by the given SequenceVar (if both are scheduled) @@ -86,7 +79,7 @@ def _to_string(self, values, verbose, smap): return "before_in(%s, %s, %s)" % (values[0], values[1], values[2]) -class PredecessorToExpression(SequenceVarExpression): +class PredecessorToExpression(BooleanExpression): """ Expression representing that one IntervalVar is a direct predecessor to another in the sequence specified by the given SequenceVar (if both are scheduled) From 5bf1000b3b3be4e93da98f167d0921e3df9c7ae9 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 21:37:28 -0600 Subject: [PATCH 1108/3044] Correcting copyright year --- pyomo/contrib/cp/debugging.py | 2 +- pyomo/contrib/cp/scheduling_expr/scheduling_logic.py | 2 +- pyomo/contrib/cp/scheduling_expr/sequence_expressions.py | 2 +- pyomo/contrib/cp/sequence_var.py | 2 +- pyomo/contrib/cp/tests/test_sequence_expressions.py | 2 +- pyomo/contrib/cp/tests/test_sequence_var.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/cp/debugging.py b/pyomo/contrib/cp/debugging.py index 41c4d208de6..34fb105a571 100644 --- a/pyomo/contrib/cp/debugging.py +++ b/pyomo/contrib/cp/debugging.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py index fc9cefebf4d..b28d536b594 100644 --- a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py +++ b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py index 865a914b847..3ba799074de 100644 --- a/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/sequence_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py index b242b362f9d..486776f58da 100644 --- a/pyomo/contrib/cp/sequence_var.py +++ b/pyomo/contrib/cp/sequence_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/tests/test_sequence_expressions.py b/pyomo/contrib/cp/tests/test_sequence_expressions.py index 218a4c0e1a0..62c868abfaf 100644 --- a/pyomo/contrib/cp/tests/test_sequence_expressions.py +++ b/pyomo/contrib/cp/tests/test_sequence_expressions.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index 404e21ca39c..ebff465a376 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain From 1090b6447ff831f2b07dac332f5634ec75888cce Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 21:41:18 -0600 Subject: [PATCH 1109/3044] Removing a stupid test --- pyomo/contrib/cp/tests/test_docplex_walker.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 2d08f6881dc..59d4f685c00 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -473,25 +473,6 @@ def test_all_diff_expression(self): self.assertTrue(expr[1].equals(cp.all_diff(a[i] for i in m.I))) - def test_Boolean_args_in_all_diff_expression(self): - m = self.get_model() - m.a.domain = Integers - m.a.bounds = (11, 20) - m.c = LogicalConstraint(expr=all_different(m.a[1] == 13, m.b)) - - visitor = self.get_visitor() - expr = visitor.walk_expression((m.c.body, m.c, 0)) - - self.assertIn(id(m.a[1]), visitor.var_map) - a0 = visitor.var_map[id(m.a[1])] - self.assertIn(id(m.b), visitor.var_map) - b = visitor.var_map[id(m.b)] - - self.assertTrue(expr[1].equals(cp.all_diff(a0 == 13, b))) - - self.assertIs(visitor.pyomo_to_docplex[m.a[1]], a0) - self.assertTrue(b.equals(visitor.pyomo_to_docplex[m.b] == 1)) - def test_count_if_expression(self): m = self.get_model() m.a.domain = Integers From 2899c82ee2c43a0407cee8839da1f98341697f77 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 22:14:48 -0600 Subject: [PATCH 1110/3044] Removing handling for IndexedSequenceVars because they can't appear in expressions right now --- pyomo/contrib/cp/repn/docplex_writer.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 439530eaf04..93b9974434a 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -551,18 +551,6 @@ def _before_sequence_var(visitor, child): return False, (_GENERAL, visitor.var_map[_id]) -def _before_indexed_sequence_var(visitor, child): - # ESJ TODO: I'm not sure we can encounter an indexed sequence var in an - # expression right now? - cpx_vars = {} - for i, v in child.items(): - cpx_sequence_var = _get_docplex_sequence_var(visitor, v) - visitor.var_map[id(v)] = cpx_sequence_var - visitor.pyomo_to_docplex[v] = cpx_sequence_var - cpx_vars[i] = cpx_sequence_var - return False, (_GENERAL, cpx_vars) - - def _before_interval_var(visitor, child): _id = id(child) if _id not in visitor.var_map: @@ -1063,7 +1051,6 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): IndexedIntervalVar: _before_indexed_interval_var, ScalarSequenceVar: _before_sequence_var, _SequenceVarData: _before_sequence_var, - IndexedSequenceVar: _before_indexed_sequence_var, ScalarVar: _before_var, _GeneralVarData: _before_var, IndexedVar: _before_indexed_var, From f820efd887cf30884f22fd2403e9ef49f0b43b16 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 22:15:02 -0600 Subject: [PATCH 1111/3044] Adding some monomial expression tests --- pyomo/contrib/cp/tests/test_docplex_walker.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 59d4f685c00..4ad07c10ee7 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -237,6 +237,35 @@ def test_expression_with_mutable_param(self): self.assertTrue(expr[1].equals(4 * x)) + def test_monomial_expressions(self): + m = ConcreteModel() + m.x = Var(domain=Integers, bounds=(1, 4)) + m.p = Param(initialize=4, mutable=True) + + visitor = self.get_visitor() + + const_expr = 3 * m.x + nested_expr = (1 / m.p) * m.x + pow_expr = (m.p ** (0.5)) * m.x + + e = m.x * 4 + expr = visitor.walk_expression((e, e, 0)) + self.assertIn(id(m.x), visitor.var_map) + x = visitor.var_map[id(m.x)] + self.assertTrue(expr[1].equals(4 * x)) + + e = 1.0 * m.x + expr = visitor.walk_expression((e, e, 0)) + self.assertTrue(expr[1].equals(x)) + + e = (1 / m.p) * m.x + expr = visitor.walk_expression((e, e, 0)) + self.assertTrue(expr[1].equals(0.25 * x)) + + e = (m.p ** (0.5)) * m.x + expr = visitor.walk_expression((e, e, 0)) + self.assertTrue(expr[1].equals(2 * x)) + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_LogicalExpressions(CommonTest): From ad69e15e9708279e757bbf53cb4e952886acf6b8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 22:15:24 -0600 Subject: [PATCH 1112/3044] Adding a test for my debugging utility, but I'm not sure how to not make it dumb yet --- pyomo/contrib/cp/tests/test_debugging.py | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 pyomo/contrib/cp/tests/test_debugging.py diff --git a/pyomo/contrib/cp/tests/test_debugging.py b/pyomo/contrib/cp/tests/test_debugging.py new file mode 100644 index 00000000000..4561b5e9f04 --- /dev/null +++ b/pyomo/contrib/cp/tests/test_debugging.py @@ -0,0 +1,28 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest + +from pyomo.environ import ( + ConcreteModel, + Constraint, + Var, +) + +class TestCPDebugging(unittest.TestCase): + def test_debug_infeasibility(self): + m = ConcreteModel() + m.x = Var(domain=Integers, bounds=(2, 5)) + m.y = Var(domain=Integers, bounds=(7, 12)) + m.c = Constraint(expr=m.y <= m.x) + + # ESJ TODO: I don't know how to do this without a baseline, which we + # really don't want... From ea7c23704b7b97e08029e283891e8213f9e771bb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 11:12:23 -0600 Subject: [PATCH 1113/3044] Make ProblemSense an extension of the ObjectiveSense enum --- pyomo/common/enums.py | 102 +++++++++++++++++++++++++++++++++-- pyomo/opt/results/problem.py | 38 ++++--------- 2 files changed, 110 insertions(+), 30 deletions(-) diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index c685843b41c..7f00e87a85f 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -10,8 +10,97 @@ # ___________________________________________________________________________ import enum +import itertools + + +class ExtendedEnumType(enum.EnumType): + """Metaclass for creating an :py:class:`Enum` that extends another Enum + + In general, :py:class:`Enum` classes are not extensible: that is, + they are frozen when defined and cannot be the base class of another + Enum. This Metaclass provides a workaround for creating a new Enum + that extends an existing enum. Members in the base Enum are all + present as members on the extended enum. + + Example + ------- + + .. testcode:: + :hide: + + import enum + from pyomo.common.enums import ExtendedEnumType + + .. testcode:: + + class ObjectiveSense(enum.IntEnum): + minimize = 1 + maximize = -1 + + class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType): + __base_enum__ = ObjectiveSense + + unknown = 0 + + .. doctest:: + + >>> list(ProblemSense) + [, , ] + >>> ProblemSense.unknown + + >>> ProblemSense.maximize + + >>> ProblemSense(0) + + >>> ProblemSense(1) + + >>> ProblemSense('unknown') + + >>> ProblemSense('maximize') + + >>> hasattr(ProblemSense, 'minimize') + True + >>> hasattr(ProblemSense, 'unknown') + True + >>> ProblemSense.minimize is ObjectiveSense.minimize + True + + """ + + def __getattr__(cls, attr): + try: + return getattr(cls.__base_enum__, attr) + except: + return super().__getattr__(attr) + + def __iter__(cls): + # The members of this Enum are the base enum members joined with + # the local members + return itertools.chain(super().__iter__(), cls.__base_enum__.__iter__()) + + def __instancecheck__(cls, instance): + if cls.__subclasscheck__(type(instance)): + return True + # Also pretend that members of the extended enum are subclasses + # of the __base_enum__. This is needed to circumvent error + # checking in enum.__new__ (e.g., for `ProblemSense('minimize')`) + return cls.__base_enum__.__subclasscheck__(type(instance)) + + def _missing_(cls, value): + # Support attribute lookup by value or name + for attr in ('value', 'name'): + for member in cls: + if getattr(member, attr) == value: + return member + return None + + def __new__(metacls, cls, bases, classdict, **kwds): + # Support lookup by name - but only if the new Enum doesn't + # specify it's own implementation of _missing_ + if '_missing_' not in classdict: + classdict['_missing_'] = classmethod(ExtendedEnumType._missing_) + return super().__new__(metacls, cls, bases, classdict, **kwds) -from pyomo.common.deprecation import RenamedClass class ObjectiveSense(enum.IntEnum): """Flag indicating if an objective is minimizing (1) or maximizing (-1). @@ -21,6 +110,7 @@ class ObjectiveSense(enum.IntEnum): consistent with some solvers (notably Gurobi). """ + minimize = 1 maximize = -1 @@ -32,7 +122,13 @@ class ObjectiveSense(enum.IntEnum): def __str__(self): return self.name -minimize = ObjectiveSense.minimize -maximize = ObjectiveSense.maximize + @classmethod + def _missing_(cls, value): + for member in cls: + if member.name == value: + return member + return None +minimize = ObjectiveSense.minimize +maximize = ObjectiveSense.maximize diff --git a/pyomo/opt/results/problem.py b/pyomo/opt/results/problem.py index e35bb155355..34da8f91918 100644 --- a/pyomo/opt/results/problem.py +++ b/pyomo/opt/results/problem.py @@ -13,32 +13,16 @@ from pyomo.opt.results.container import MapContainer from pyomo.common.deprecation import deprecated, deprecation_warning -from pyomo.common.enums import ObjectiveSense - - -class ProblemSenseType(type): - @deprecated( - "pyomo.opt.results.problem.ProblemSense has been replaced by " - "pyomo.common.enums.ObjectiveSense", - version="6.7.2.dev0", - ) - def __getattr__(cls, attr): - if attr == 'minimize': - return ObjectiveSense.minimize - if attr == 'maximize': - return ObjectiveSense.maximize - if attr == 'unknown': - deprecation_warning( - "ProblemSense.unknown is no longer an allowable option. " - "Mapping 'unknown' to 'minimize'", - version="6.7.2.dev0", - ) - return ObjectiveSense.minimize - raise AttributeError(attr) - - -class ProblemSense(metaclass=ProblemSenseType): - pass +from pyomo.common.enums import ExtendedEnumType, ObjectiveSense + + +class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType): + __base_enum__ = ObjectiveSense + + unknown = 0 + + def __str__(self): + return self.name class ProblemInformation(MapContainer): @@ -54,4 +38,4 @@ def __init__(self): self.declare('number_of_integer_variables') self.declare('number_of_continuous_variables') self.declare('number_of_nonzeros') - self.declare('sense', value=ProblemSense.minimize, required=True) + self.declare('sense', value=ProblemSense.unknown, required=True) From 297ce1d1f5f0a2d64c61a121ae35d63fd3f1509e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 11:27:57 -0600 Subject: [PATCH 1114/3044] Switch Objective.sense to store ObjectiveSense enum values --- pyomo/core/__init__.py | 2 +- pyomo/core/base/__init__.py | 2 +- pyomo/core/base/objective.py | 26 ++++---------------------- pyomo/core/kernel/objective.py | 11 ++--------- 4 files changed, 8 insertions(+), 33 deletions(-) diff --git a/pyomo/core/__init__.py b/pyomo/core/__init__.py index bce79faacc5..f0d168d98f9 100644 --- a/pyomo/core/__init__.py +++ b/pyomo/core/__init__.py @@ -101,7 +101,7 @@ BooleanValue, native_logical_values, ) -from pyomo.core.kernel.objective import minimize, maximize +from pyomo.core.base import minimize, maximize from pyomo.core.base.config import PyomoOptions from pyomo.core.base.expression import Expression diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 4bbd0c9dc44..df5ce743888 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -12,6 +12,7 @@ # TODO: this import is for historical backwards compatibility and should # probably be removed from pyomo.common.collections import ComponentMap +from pyomo.common.enums import minimize, maximize from pyomo.core.expr.symbol_map import SymbolMap from pyomo.core.expr.numvalue import ( @@ -33,7 +34,6 @@ BooleanValue, native_logical_values, ) -from pyomo.core.kernel.objective import minimize, maximize from pyomo.core.base.config import PyomoOptions from pyomo.core.base.expression import Expression, _ExpressionData diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index fcc63755f2b..10cc853dafb 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -15,6 +15,7 @@ from pyomo.common.pyomo_typing import overload from pyomo.common.deprecation import RenamedClass +from pyomo.common.enums import ObjectiveSense, minimize, maximize from pyomo.common.log import is_debug_set from pyomo.common.modeling import NOTSET from pyomo.common.formatting import tabular_writer @@ -35,7 +36,6 @@ IndexedCallInitializer, CountedCallInitializer, ) -from pyomo.core.base import minimize, maximize logger = logging.getLogger('pyomo.core') @@ -152,14 +152,7 @@ def __init__(self, expr=None, sense=minimize, component=None): self._component = weakref_ref(component) if (component is not None) else None self._index = NOTSET self._active = True - self._sense = sense - - if (self._sense != minimize) and (self._sense != maximize): - raise ValueError( - "Objective sense must be set to one of " - "'minimize' (%s) or 'maximize' (%s). Invalid " - "value: %s'" % (minimize, maximize, sense) - ) + self._sense = ObjectiveSense(sense) def set_value(self, expr): if expr is None: @@ -182,14 +175,7 @@ def sense(self, sense): def set_sense(self, sense): """Set the sense (direction) of this objective.""" - if sense in {minimize, maximize}: - self._sense = sense - else: - raise ValueError( - "Objective sense must be set to one of " - "'minimize' (%s) or 'maximize' (%s). Invalid " - "value: %s'" % (minimize, maximize, sense) - ) + self._sense = ObjectiveSense(sense) @ModelComponentFactory.register("Expressions that are minimized or maximized.") @@ -353,11 +339,7 @@ def _pprint(self): ], self._data.items(), ("Active", "Sense", "Expression"), - lambda k, v: [ - v.active, - ("minimize" if (v.sense == minimize) else "maximize"), - v.expr, - ], + lambda k, v: [v.active, v.sense, v.expr], ) def display(self, prefix="", ostream=None): diff --git a/pyomo/core/kernel/objective.py b/pyomo/core/kernel/objective.py index 840c7cfd7e0..ac6f22d07d3 100644 --- a/pyomo/core/kernel/objective.py +++ b/pyomo/core/kernel/objective.py @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.common.enums import minimize, maximize +from pyomo.common.enums import ObjectiveSense, minimize, maximize from pyomo.core.expr.numvalue import as_numeric from pyomo.core.kernel.base import _abstract_readwrite_property from pyomo.core.kernel.container_utils import define_simple_containers @@ -81,14 +81,7 @@ def sense(self): @sense.setter def sense(self, sense): """Set the sense (direction) of this objective.""" - if (sense == minimize) or (sense == maximize): - self._sense = sense - else: - raise ValueError( - "Objective sense must be set to one of: " - "[minimize (%s), maximize (%s)]. Invalid " - "value: %s'" % (minimize, maximize, sense) - ) + self._sense = ObjectiveSense(sense) # inserts class definitions for simple _tuple, _list, and From ab59255c9a574d40a63c8bfdd827efe15b8f7639 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 11:42:00 -0600 Subject: [PATCH 1115/3044] Remove reserences to ProblemSense --- pyomo/contrib/mindtpy/algorithm_base_class.py | 12 ++---------- pyomo/contrib/mindtpy/util.py | 1 - .../pynumero/algorithms/solvers/cyipopt_solver.py | 5 ++--- pyomo/solvers/plugins/solvers/CBCplugin.py | 12 ++++++------ pyomo/solvers/plugins/solvers/CPLEX.py | 14 +++++++------- pyomo/solvers/plugins/solvers/GAMS.py | 7 ++----- pyomo/solvers/plugins/solvers/GLPK.py | 8 +++----- pyomo/solvers/plugins/solvers/GUROBI.py | 8 ++++---- pyomo/solvers/plugins/solvers/SCIPAMPL.py | 9 ++------- pyomo/solvers/tests/checks/test_CBCplugin.py | 14 +++++++------- 10 files changed, 35 insertions(+), 55 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 785a89d8982..8c703f8d842 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -27,13 +27,7 @@ from operator import itemgetter from pyomo.common.errors import DeveloperError from pyomo.solvers.plugins.solvers.gurobi_direct import gurobipy -from pyomo.opt import ( - SolverFactory, - SolverResults, - ProblemSense, - SolutionStatus, - SolverStatus, -) +from pyomo.opt import SolverFactory, SolverResults, SolutionStatus, SolverStatus from pyomo.core import ( minimize, maximize, @@ -633,9 +627,7 @@ def process_objective(self, update_var_con_list=True): raise ValueError('Model has multiple active objectives.') else: main_obj = active_objectives[0] - self.results.problem.sense = ( - ProblemSense.minimize if main_obj.sense == 1 else ProblemSense.maximize - ) + self.results.problem.sense = main_obj.sense self.objective_sense = main_obj.sense # Move the objective to the constraints if it is nonlinear or move_objective is True. diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 1543497838f..7345af8a3e2 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -29,7 +29,6 @@ from pyomo.contrib.mcpp.pyomo_mcpp import mcpp_available, McCormick from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr import pyomo.core.expr as EXPR -from pyomo.opt import ProblemSense from pyomo.contrib.gdpopt.util import get_main_elapsed_time, time_code from pyomo.util.model_size import build_model_size_report from pyomo.common.dependencies import attempt_import diff --git a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py index 53616298415..0999550711c 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py @@ -65,7 +65,7 @@ from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.common.timing import TicTocTimer from pyomo.core.base import Block, Objective, minimize -from pyomo.opt import SolverStatus, SolverResults, TerminationCondition, ProblemSense +from pyomo.opt import SolverStatus, SolverResults, TerminationCondition from pyomo.opt.results.solution import Solution logger = logging.getLogger(__name__) @@ -447,11 +447,10 @@ def solve(self, model, **kwds): results.problem.name = model.name obj = next(model.component_data_objects(Objective, active=True)) + results.problem.sense = obj.sense if obj.sense == minimize: - results.problem.sense = ProblemSense.minimize results.problem.upper_bound = info["obj_val"] else: - results.problem.sense = ProblemSense.maximize results.problem.lower_bound = info["obj_val"] results.problem.number_of_objectives = 1 results.problem.number_of_constraints = ng diff --git a/pyomo/solvers/plugins/solvers/CBCplugin.py b/pyomo/solvers/plugins/solvers/CBCplugin.py index eb6c2c2e1bd..f22fb117c8b 100644 --- a/pyomo/solvers/plugins/solvers/CBCplugin.py +++ b/pyomo/solvers/plugins/solvers/CBCplugin.py @@ -16,6 +16,7 @@ import subprocess from pyomo.common import Executable +from pyomo.common.enums import maximize, minimize from pyomo.common.errors import ApplicationError from pyomo.common.collections import Bunch from pyomo.common.tempfiles import TempfileManager @@ -29,7 +30,6 @@ SolverStatus, TerminationCondition, SolutionStatus, - ProblemSense, Solution, ) from pyomo.opt.solver import SystemCallSolver @@ -443,7 +443,7 @@ def process_logfile(self): # # Parse logfile lines # - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize results.problem.name = None optim_value = float('inf') lower_bound = None @@ -578,7 +578,7 @@ def process_logfile(self): 'CoinLpIO::readLp(): Maximization problem reformulated as minimization' in ' '.join(tokens) ): - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L3047 elif n_tokens > 3 and tokens[:2] == ('Result', '-'): if tokens[2:4] in [('Run', 'abandoned'), ('User', 'ctrl-c')]: @@ -752,9 +752,9 @@ def process_logfile(self): "maxIterations parameter." ) soln.gap = gap - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: upper_bound = optim_value - elif results.problem.sense == ProblemSense.maximize: + elif results.problem.sense == maximize: _ver = self.version() if _ver and _ver[:3] < (2, 10, 2): optim_value *= -1 @@ -824,7 +824,7 @@ def process_soln_file(self, results): INPUT = [] _ver = self.version() - invert_objective_sense = results.problem.sense == ProblemSense.maximize and ( + invert_objective_sense = results.problem.sense == maximize and ( _ver and _ver[:3] < (2, 10, 2) ) diff --git a/pyomo/solvers/plugins/solvers/CPLEX.py b/pyomo/solvers/plugins/solvers/CPLEX.py index 9f876b2d0f8..3a08257c87c 100644 --- a/pyomo/solvers/plugins/solvers/CPLEX.py +++ b/pyomo/solvers/plugins/solvers/CPLEX.py @@ -17,6 +17,7 @@ import subprocess from pyomo.common import Executable +from pyomo.common.enums import maximize, minimize from pyomo.common.errors import ApplicationError from pyomo.common.tempfiles import TempfileManager @@ -28,7 +29,6 @@ SolverStatus, TerminationCondition, SolutionStatus, - ProblemSense, Solution, ) from pyomo.opt.solver import ILMLicensedSystemCallSolver @@ -547,9 +547,9 @@ def process_logfile(self): ): # CPLEX 11.2 and subsequent has two Nonzeros sections. results.problem.number_of_nonzeros = int(tokens[2]) elif len(tokens) >= 5 and tokens[4] == "MINIMIZE": - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize elif len(tokens) >= 5 and tokens[4] == "MAXIMIZE": - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize elif ( len(tokens) >= 4 and tokens[0] == "Solution" @@ -859,9 +859,9 @@ def process_soln_file(self, results): else: sense = tokens[0].lower() if sense in ['max', 'maximize']: - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize if sense in ['min', 'minimize']: - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize break tINPUT.close() @@ -952,7 +952,7 @@ def process_soln_file(self, results): ) if primal_feasible == 1: soln.status = SolutionStatus.feasible - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: results.problem.upper_bound = soln.objective[ '__default_objective__' ]['Value'] @@ -964,7 +964,7 @@ def process_soln_file(self, results): soln.status = SolutionStatus.infeasible if self._best_bound is not None: - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: results.problem.lower_bound = self._best_bound else: results.problem.upper_bound = self._best_bound diff --git a/pyomo/solvers/plugins/solvers/GAMS.py b/pyomo/solvers/plugins/solvers/GAMS.py index c0bab4dc23e..035bd0b7603 100644 --- a/pyomo/solvers/plugins/solvers/GAMS.py +++ b/pyomo/solvers/plugins/solvers/GAMS.py @@ -36,7 +36,6 @@ Solution, SolutionStatus, TerminationCondition, - ProblemSense, ) from pyomo.common.dependencies import attempt_import @@ -422,11 +421,10 @@ def solve(self, *args, **kwds): assert len(obj) == 1, 'Only one objective is allowed.' obj = obj[0] objctvval = t1.out_db["OBJVAL"].find_record().value + results.problem.sense = obj.sense if obj.is_minimizing(): - results.problem.sense = ProblemSense.minimize results.problem.upper_bound = objctvval else: - results.problem.sense = ProblemSense.maximize results.problem.lower_bound = objctvval results.solver.name = "GAMS " + str(self.version()) @@ -984,11 +982,10 @@ def solve(self, *args, **kwds): assert len(obj) == 1, 'Only one objective is allowed.' obj = obj[0] objctvval = stat_vars["OBJVAL"] + results.problem.sense = obj.sense if obj.is_minimizing(): - results.problem.sense = ProblemSense.minimize results.problem.upper_bound = objctvval else: - results.problem.sense = ProblemSense.maximize results.problem.lower_bound = objctvval results.solver.name = "GAMS " + str(self.version()) diff --git a/pyomo/solvers/plugins/solvers/GLPK.py b/pyomo/solvers/plugins/solvers/GLPK.py index e6d8576489d..c8d5bc14237 100644 --- a/pyomo/solvers/plugins/solvers/GLPK.py +++ b/pyomo/solvers/plugins/solvers/GLPK.py @@ -19,6 +19,7 @@ from pyomo.common import Executable from pyomo.common.collections import Bunch +from pyomo.common.enums import maximize, minimize from pyomo.common.errors import ApplicationError from pyomo.opt import ( SolverFactory, @@ -28,7 +29,6 @@ SolverResults, TerminationCondition, SolutionStatus, - ProblemSense, ) from pyomo.opt.base.solvers import _extract_version from pyomo.opt.solver import SystemCallSolver @@ -308,10 +308,8 @@ def process_soln_file(self, results): ): raise ValueError - self.is_integer = 'mip' == ptype and True or False - prob.sense = ( - 'min' == psense and ProblemSense.minimize or ProblemSense.maximize - ) + self.is_integer = 'mip' == ptype + prob.sense = minimize if 'min' == psense else maximize prob.number_of_constraints = prows prob.number_of_nonzeros = pnonz prob.number_of_variables = pcols diff --git a/pyomo/solvers/plugins/solvers/GUROBI.py b/pyomo/solvers/plugins/solvers/GUROBI.py index c8b0912970e..3a3a4d52322 100644 --- a/pyomo/solvers/plugins/solvers/GUROBI.py +++ b/pyomo/solvers/plugins/solvers/GUROBI.py @@ -18,6 +18,7 @@ from pyomo.common import Executable from pyomo.common.collections import Bunch +from pyomo.common.enums import maximize, minimize from pyomo.common.fileutils import this_file_dir from pyomo.common.tee import capture_output from pyomo.common.tempfiles import TempfileManager @@ -28,7 +29,6 @@ SolverStatus, TerminationCondition, SolutionStatus, - ProblemSense, Solution, ) from pyomo.opt.solver import ILMLicensedSystemCallSolver @@ -472,7 +472,7 @@ def process_soln_file(self, results): soln.objective['__default_objective__'] = { 'Value': float(tokens[1]) } - if results.problem.sense == ProblemSense.minimize: + if results.problem.sense == minimize: results.problem.upper_bound = float(tokens[1]) else: results.problem.lower_bound = float(tokens[1]) @@ -514,9 +514,9 @@ def process_soln_file(self, results): elif section == 1: if tokens[0] == 'sense': if tokens[1] == 'minimize': - results.problem.sense = ProblemSense.minimize + results.problem.sense = minimize elif tokens[1] == 'maximize': - results.problem.sense = ProblemSense.maximize + results.problem.sense = maximize else: try: val = eval(tokens[1]) diff --git a/pyomo/solvers/plugins/solvers/SCIPAMPL.py b/pyomo/solvers/plugins/solvers/SCIPAMPL.py index fd69954b428..6940ad7b5fe 100644 --- a/pyomo/solvers/plugins/solvers/SCIPAMPL.py +++ b/pyomo/solvers/plugins/solvers/SCIPAMPL.py @@ -20,12 +20,7 @@ from pyomo.opt.base import ProblemFormat, ResultsFormat from pyomo.opt.base.solvers import _extract_version, SolverFactory -from pyomo.opt.results import ( - SolverStatus, - TerminationCondition, - SolutionStatus, - ProblemSense, -) +from pyomo.opt.results import SolverStatus, TerminationCondition, SolutionStatus from pyomo.opt.solver import SystemCallSolver import logging @@ -374,7 +369,7 @@ def _postsolve(self): if len(results.solution) > 0: results.solution(0).status = SolutionStatus.optimal try: - if results.problem.sense == ProblemSense.minimize: + if results.solver.primal_bound < results.solver.dual_bound: results.problem.lower_bound = results.solver.primal_bound else: results.problem.upper_bound = results.solver.primal_bound diff --git a/pyomo/solvers/tests/checks/test_CBCplugin.py b/pyomo/solvers/tests/checks/test_CBCplugin.py index 2ea0e55c5f4..ad8846509ea 100644 --- a/pyomo/solvers/tests/checks/test_CBCplugin.py +++ b/pyomo/solvers/tests/checks/test_CBCplugin.py @@ -29,7 +29,7 @@ maximize, minimize, ) -from pyomo.opt import SolverFactory, ProblemSense, TerminationCondition, SolverStatus +from pyomo.opt import SolverFactory, TerminationCondition, SolverStatus from pyomo.solvers.plugins.solvers.CBCplugin import CBCSHELL cbc_available = SolverFactory('cbc', solver_io='lp').available(exception_flag=False) @@ -62,7 +62,7 @@ def test_infeasible_lp(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.infeasible, results.solver.termination_condition ) @@ -81,7 +81,7 @@ def test_unbounded_lp(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.maximize, results.problem.sense) + self.assertEqual(maximize, results.problem.sense) self.assertEqual( TerminationCondition.unbounded, results.solver.termination_condition ) @@ -99,7 +99,7 @@ def test_optimal_lp(self): self.assertEqual(0.0, results.problem.lower_bound) self.assertEqual(0.0, results.problem.upper_bound) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.optimal, results.solver.termination_condition ) @@ -118,7 +118,7 @@ def test_infeasible_mip(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.infeasible, results.solver.termination_condition ) @@ -134,7 +134,7 @@ def test_unbounded_mip(self): results = self.opt.solve(self.model) - self.assertEqual(ProblemSense.minimize, results.problem.sense) + self.assertEqual(minimize, results.problem.sense) self.assertEqual( TerminationCondition.unbounded, results.solver.termination_condition ) @@ -159,7 +159,7 @@ def test_optimal_mip(self): self.assertEqual(1.0, results.problem.upper_bound) self.assertEqual(results.problem.number_of_binary_variables, 2) self.assertEqual(results.problem.number_of_integer_variables, 4) - self.assertEqual(ProblemSense.maximize, results.problem.sense) + self.assertEqual(maximize, results.problem.sense) self.assertEqual( TerminationCondition.optimal, results.solver.termination_condition ) From f80ff256c0da8a65f8be956e011246dc16f15583 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 11:44:22 -0600 Subject: [PATCH 1116/3044] Report both lower and upper bounds from SCIP --- pyomo/solvers/plugins/solvers/SCIPAMPL.py | 2 ++ pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/solvers/plugins/solvers/SCIPAMPL.py b/pyomo/solvers/plugins/solvers/SCIPAMPL.py index 6940ad7b5fe..c309ad29d96 100644 --- a/pyomo/solvers/plugins/solvers/SCIPAMPL.py +++ b/pyomo/solvers/plugins/solvers/SCIPAMPL.py @@ -371,7 +371,9 @@ def _postsolve(self): try: if results.solver.primal_bound < results.solver.dual_bound: results.problem.lower_bound = results.solver.primal_bound + results.problem.upper_bound = results.solver.dual_bound else: + results.problem.lower_bound = results.solver.dual_bound results.problem.upper_bound = results.solver.primal_bound except AttributeError: """ diff --git a/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline b/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline index a3eb9ffacec..976e4a1b82e 100644 --- a/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline +++ b/pyomo/solvers/tests/mip/test_scip_solve_from_instance.baseline @@ -1,7 +1,7 @@ { "Problem": [ { - "Lower bound": -Infinity, + "Lower bound": 1.0, "Number of constraints": 0, "Number of objectives": 1, "Number of variables": 1, From 2f59e44f0eb0e1f360ee41fd3daebaba3404f7c3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 12:26:12 -0600 Subject: [PATCH 1117/3044] Updating baselines; ironically this makes the expected output actually match what is advertised in the Book --- examples/pyomobook/pyomo-components-ch/obj_declaration.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pyomobook/pyomo-components-ch/obj_declaration.txt b/examples/pyomobook/pyomo-components-ch/obj_declaration.txt index 607586a1fb3..e4d4b02a252 100644 --- a/examples/pyomobook/pyomo-components-ch/obj_declaration.txt +++ b/examples/pyomobook/pyomo-components-ch/obj_declaration.txt @@ -55,7 +55,7 @@ Model unknown None value x[Q] + 2*x[R] -1 +minimize 6.5 Model unknown From e1ad8e55b1b8ce26a4695f275b371f67c18062a8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 13:04:43 -0600 Subject: [PATCH 1118/3044] Adding portability fixes for Python<3.11 --- pyomo/common/enums.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index 7f00e87a85f..ee934acd35f 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -11,9 +11,14 @@ import enum import itertools +import sys +if sys.version_info[:2] < (3, 11): + _EnumType = enum.EnumMeta +else: + _EnumType = enum.EnumType -class ExtendedEnumType(enum.EnumType): +class ExtendedEnumType(_EnumType): """Metaclass for creating an :py:class:`Enum` that extends another Enum In general, :py:class:`Enum` classes are not extensible: that is, From 4aa861cef0c91a5d335cb83d358a8ecf0074fb3c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 13:50:58 -0600 Subject: [PATCH 1119/3044] NFC: apply black --- pyomo/common/enums.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index ee934acd35f..7de8b13b81f 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -18,6 +18,7 @@ else: _EnumType = enum.EnumType + class ExtendedEnumType(_EnumType): """Metaclass for creating an :py:class:`Enum` that extends another Enum From d095e2409ded5cc027fa50e0b07b5bb17b77c06f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 15:14:10 -0600 Subject: [PATCH 1120/3044] Integrate common.enums into online docs --- .../library_reference/common/enums.rst | 7 +++++ .../library_reference/common/index.rst | 1 + pyomo/common/enums.py | 26 +++++++++++++++++-- 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 doc/OnlineDocs/library_reference/common/enums.rst diff --git a/doc/OnlineDocs/library_reference/common/enums.rst b/doc/OnlineDocs/library_reference/common/enums.rst new file mode 100644 index 00000000000..5ed2dbb1e80 --- /dev/null +++ b/doc/OnlineDocs/library_reference/common/enums.rst @@ -0,0 +1,7 @@ + +pyomo.common.enums +================== + +.. automodule:: pyomo.common.enums + :members: + :member-order: bysource diff --git a/doc/OnlineDocs/library_reference/common/index.rst b/doc/OnlineDocs/library_reference/common/index.rst index c9c99008250..c03436600f2 100644 --- a/doc/OnlineDocs/library_reference/common/index.rst +++ b/doc/OnlineDocs/library_reference/common/index.rst @@ -11,6 +11,7 @@ or rely on any other parts of Pyomo. config.rst dependencies.rst deprecation.rst + enums.rst errors.rst fileutils.rst formatting.rst diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index 7de8b13b81f..9988beedbff 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -9,6 +9,23 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +"""This module provides standard :py:class:`enum.Enum` definitions used in +Pyomo, along with additional utilities for working with custom Enums + +Utilities: + +.. autosummary:: + + ExtendedEnumType + +Standard Enums: + +.. autosummary:: + + ObjectiveSense + +""" + import enum import itertools import sys @@ -20,9 +37,9 @@ class ExtendedEnumType(_EnumType): - """Metaclass for creating an :py:class:`Enum` that extends another Enum + """Metaclass for creating an :py:class:`enum.Enum` that extends another Enum - In general, :py:class:`Enum` classes are not extensible: that is, + In general, :py:class:`enum.Enum` classes are not extensible: that is, they are frozen when defined and cannot be the base class of another Enum. This Metaclass provides a workaround for creating a new Enum that extends an existing enum. Members in the base Enum are all @@ -84,6 +101,11 @@ def __iter__(cls): # the local members return itertools.chain(super().__iter__(), cls.__base_enum__.__iter__()) + def __contains__(cls, member): + # This enum "containts" both it's local members and the members + # in the __base_enum__ (necessary for good auto-enum[sphinx] docs) + return super().__contains__(member) or member in cls.__base_enum__ + def __instancecheck__(cls, instance): if cls.__subclasscheck__(type(instance)): return True From 9a9c6843296b30bd45c0641c626736317cc785e2 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 3 Apr 2024 15:20:35 -0600 Subject: [PATCH 1121/3044] allow variables/constraints to be specified in same list in remove_nodes, but raise deprecation warning --- pyomo/contrib/incidence_analysis/interface.py | 28 +++++++++++++++++-- .../tests/test_interface.py | 18 ++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 0ed9b34b0f8..f8d7ea855d4 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -28,7 +28,7 @@ scipy as sp, plotly, ) -from pyomo.common.deprecation import deprecated +from pyomo.common.deprecation import deprecated, deprecation_warning from pyomo.contrib.incidence_analysis.config import get_config_from_kwds from pyomo.contrib.incidence_analysis.matching import maximum_matching from pyomo.contrib.incidence_analysis.connected import get_independent_submatrices @@ -911,7 +911,31 @@ def remove_nodes(self, variables=None, constraints=None): "Attempting to remove variables and constraints from cached " "incidence matrix,\nbut no incidence matrix has been cached." ) - variables, constraints = self._validate_input(variables, constraints) + + vars_to_validate = [] + cons_to_validate = [] + depr_msg = ( + "In IncidenceGraphInterface.remove_nodes, passing variables and" + " constraints in the same list is deprecated. Please separate your" + " variables and constraints and pass them in the order variables," + " constraints." + ) + for var in variables: + if var in self._con_index_map: + deprecation_warning(depr_msg, version="TBD") + cons_to_validate.append(var) + else: + vars_to_validate.append(var) + for con in constraints: + if con in self._var_index_map: + deprecation_warning(depr_msg, version="TBD") + vars_to_validate.append(con) + else: + cons_to_validate.append(con) + + variables, constraints = self._validate_input( + vars_to_validate, cons_to_validate + ) v_exclude = ComponentSet(variables) c_exclude = ComponentSet(constraints) vars_to_include = [v for v in self.variables if v not in v_exclude] diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 3b2439ed2af..816c8cbe3d3 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1471,6 +1471,24 @@ def test_remove_bad_node(self): with self.assertRaisesRegex(KeyError, "does not exist"): igraph.remove_nodes([[m.x[1], m.x[2]], [m.eq[1]]]) + def test_remove_varcon_samelist_deprecated(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2, 3]) + m.eq = pyo.Constraint(pyo.PositiveIntegers) + m.eq[1] = m.x[1] * m.x[2] == m.x[3] + m.eq[2] = m.x[1] + 2 * m.x[2] == 3 * m.x[3] + + igraph = IncidenceGraphInterface(m) + # This raises a deprecation warning. When the deprecated functionality + # is removed, this will fail, and this test should be updated accordingly. + igraph.remove_nodes([m.eq[1], m.x[1]]) + self.assertEqual(len(igraph.variables), 2) + self.assertEqual(len(igraph.constraints), 1) + + igraph.remove_nodes([], [m.eq[2], m.x[2]]) + self.assertEqual(len(igraph.variables), 1) + self.assertEqual(len(igraph.constraints), 0) + @unittest.skipUnless(networkx_available, "networkx is not available.") @unittest.skipUnless(scipy_available, "scipy is not available.") From 025e429ec66af22c0fa1fbaeae51f8d54a1d4421 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 3 Apr 2024 15:23:39 -0600 Subject: [PATCH 1122/3044] rephrase "breaking change" as "deprecation" --- pyomo/contrib/incidence_analysis/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index f8d7ea855d4..64551788a8b 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -891,7 +891,7 @@ def remove_nodes(self, variables=None, constraints=None): .. note:: - **Breaking change in Pyomo vTBD** + **Deprecation in Pyomo vTBD** The pre-TBD implementation of ``remove_nodes`` allowed variables and constraints to remove to be specified in a single list. This made From f6b4ea93e8c25cd6b535051b0177c4620162a499 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 15:33:20 -0600 Subject: [PATCH 1123/3044] Add unit tests --- pyomo/common/enums.py | 4 +- pyomo/common/tests/test_enums.py | 99 ++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 pyomo/common/tests/test_enums.py diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index 9988beedbff..0dd65829026 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -83,10 +83,10 @@ class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType): >>> hasattr(ProblemSense, 'minimize') True - >>> hasattr(ProblemSense, 'unknown') - True >>> ProblemSense.minimize is ObjectiveSense.minimize True + >>> ProblemSense.minimize in ProblemSense + True """ diff --git a/pyomo/common/tests/test_enums.py b/pyomo/common/tests/test_enums.py new file mode 100644 index 00000000000..2d5ab01b6e3 --- /dev/null +++ b/pyomo/common/tests/test_enums.py @@ -0,0 +1,99 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import enum + +import pyomo.common.unittest as unittest + +from pyomo.common.enums import ExtendedEnumType, ObjectiveSense + + +class ProblemSense(enum.IntEnum, metaclass=ExtendedEnumType): + __base_enum__ = ObjectiveSense + + unknown = 0 + + +class TestExtendedEnumType(unittest.TestCase): + def test_members(self): + self.assertEqual( + list(ProblemSense), + [ProblemSense.unknown, ObjectiveSense.minimize, ObjectiveSense.maximize], + ) + + def test_isinstance(self): + self.assertIsInstance(ProblemSense.unknown, ProblemSense) + self.assertIsInstance(ProblemSense.minimize, ProblemSense) + self.assertIsInstance(ProblemSense.maximize, ProblemSense) + + self.assertTrue(ProblemSense.__instancecheck__(ProblemSense.unknown)) + self.assertTrue(ProblemSense.__instancecheck__(ProblemSense.minimize)) + self.assertTrue(ProblemSense.__instancecheck__(ProblemSense.maximize)) + + def test_getattr(self): + self.assertIs(ProblemSense.unknown, ProblemSense.unknown) + self.assertIs(ProblemSense.minimize, ObjectiveSense.minimize) + self.assertIs(ProblemSense.maximize, ObjectiveSense.maximize) + + def test_hasattr(self): + self.assertTrue(hasattr(ProblemSense, 'unknown')) + self.assertTrue(hasattr(ProblemSense, 'minimize')) + self.assertTrue(hasattr(ProblemSense, 'maximize')) + + def test_call(self): + self.assertIs(ProblemSense(0), ProblemSense.unknown) + self.assertIs(ProblemSense(1), ObjectiveSense.minimize) + self.assertIs(ProblemSense(-1), ObjectiveSense.maximize) + + self.assertIs(ProblemSense('unknown'), ProblemSense.unknown) + self.assertIs(ProblemSense('minimize'), ObjectiveSense.minimize) + self.assertIs(ProblemSense('maximize'), ObjectiveSense.maximize) + + with self.assertRaisesRegex( + ValueError, "'foo' is not a valid ProblemSense" + ): + ProblemSense('foo') + + def test_contains(self): + self.assertIn(ProblemSense.unknown, ProblemSense) + self.assertIn(ProblemSense.minimize, ProblemSense) + self.assertIn(ProblemSense.maximize, ProblemSense) + + self.assertNotIn(ProblemSense.unknown, ObjectiveSense) + self.assertIn(ProblemSense.minimize, ObjectiveSense) + self.assertIn(ProblemSense.maximize, ObjectiveSense) + +class TestObjectiveSense(unittest.TestCase): + def test_members(self): + self.assertEqual( + list(ObjectiveSense), + [ObjectiveSense.minimize, ObjectiveSense.maximize], + ) + + def test_hasattr(self): + self.assertTrue(hasattr(ProblemSense, 'minimize')) + self.assertTrue(hasattr(ProblemSense, 'maximize')) + + def test_call(self): + self.assertIs(ObjectiveSense(1), ObjectiveSense.minimize) + self.assertIs(ObjectiveSense(-1), ObjectiveSense.maximize) + + self.assertIs(ObjectiveSense('minimize'), ObjectiveSense.minimize) + self.assertIs(ObjectiveSense('maximize'), ObjectiveSense.maximize) + + with self.assertRaisesRegex( + ValueError, "'foo' is not a valid ObjectiveSense" + ): + ObjectiveSense('foo') + + def test_str(self): + self.assertEqual(str(ObjectiveSense.minimize), 'minimize') + self.assertEqual(str(ObjectiveSense.maximize), 'maximize') From 98c960e937cdd30b1f7be2d5d00387a90fbe99af Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 15:39:27 -0600 Subject: [PATCH 1124/3044] NFC: fix typos --- pyomo/common/enums.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index 0dd65829026..4d969bf7a9e 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -102,8 +102,8 @@ def __iter__(cls): return itertools.chain(super().__iter__(), cls.__base_enum__.__iter__()) def __contains__(cls, member): - # This enum "containts" both it's local members and the members - # in the __base_enum__ (necessary for good auto-enum[sphinx] docs) + # This enum "contains" both its local members and the members in + # the __base_enum__ (necessary for good auto-enum[sphinx] docs) return super().__contains__(member) or member in cls.__base_enum__ def __instancecheck__(cls, instance): @@ -124,7 +124,7 @@ def _missing_(cls, value): def __new__(metacls, cls, bases, classdict, **kwds): # Support lookup by name - but only if the new Enum doesn't - # specify it's own implementation of _missing_ + # specify its own implementation of _missing_ if '_missing_' not in classdict: classdict['_missing_'] = classmethod(ExtendedEnumType._missing_) return super().__new__(metacls, cls, bases, classdict, **kwds) From 6c3245310aadd3211c2c7120d293adfb135517e2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 3 Apr 2024 15:40:45 -0600 Subject: [PATCH 1125/3044] NFC: apply black --- pyomo/common/tests/test_enums.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/pyomo/common/tests/test_enums.py b/pyomo/common/tests/test_enums.py index 2d5ab01b6e3..52ee1c5abb3 100644 --- a/pyomo/common/tests/test_enums.py +++ b/pyomo/common/tests/test_enums.py @@ -57,9 +57,7 @@ def test_call(self): self.assertIs(ProblemSense('minimize'), ObjectiveSense.minimize) self.assertIs(ProblemSense('maximize'), ObjectiveSense.maximize) - with self.assertRaisesRegex( - ValueError, "'foo' is not a valid ProblemSense" - ): + with self.assertRaisesRegex(ValueError, "'foo' is not a valid ProblemSense"): ProblemSense('foo') def test_contains(self): @@ -71,11 +69,11 @@ def test_contains(self): self.assertIn(ProblemSense.minimize, ObjectiveSense) self.assertIn(ProblemSense.maximize, ObjectiveSense) + class TestObjectiveSense(unittest.TestCase): def test_members(self): self.assertEqual( - list(ObjectiveSense), - [ObjectiveSense.minimize, ObjectiveSense.maximize], + list(ObjectiveSense), [ObjectiveSense.minimize, ObjectiveSense.maximize] ) def test_hasattr(self): @@ -89,9 +87,7 @@ def test_call(self): self.assertIs(ObjectiveSense('minimize'), ObjectiveSense.minimize) self.assertIs(ObjectiveSense('maximize'), ObjectiveSense.maximize) - with self.assertRaisesRegex( - ValueError, "'foo' is not a valid ObjectiveSense" - ): + with self.assertRaisesRegex(ValueError, "'foo' is not a valid ObjectiveSense"): ObjectiveSense('foo') def test_str(self): From c7db40f70332d047fbb9197f8e2a9e694b4b34e6 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 3 Apr 2024 16:32:41 -0600 Subject: [PATCH 1126/3044] update remove_nodes tests for better coverage --- pyomo/contrib/incidence_analysis/tests/test_interface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/incidence_analysis/tests/test_interface.py b/pyomo/contrib/incidence_analysis/tests/test_interface.py index 816c8cbe3d3..b0a9661aa54 100644 --- a/pyomo/contrib/incidence_analysis/tests/test_interface.py +++ b/pyomo/contrib/incidence_analysis/tests/test_interface.py @@ -1466,7 +1466,10 @@ def test_remove_bad_node(self): with self.assertRaisesRegex(KeyError, "does not exist"): # Suppose we think something like this should work. We should get # an error, and not silently do nothing. - igraph.remove_nodes([m.x], [m.eq]) + igraph.remove_nodes([m.x], [m.eq[1]]) + + with self.assertRaisesRegex(KeyError, "does not exist"): + igraph.remove_nodes(None, [m.eq]) with self.assertRaisesRegex(KeyError, "does not exist"): igraph.remove_nodes([[m.x[1], m.x[2]], [m.eq[1]]]) From d6af9b3b2d809ed45796fee016c2961acc9ecd89 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 3 Apr 2024 20:42:18 -0600 Subject: [PATCH 1127/3044] only log deprecation warning once --- pyomo/contrib/incidence_analysis/interface.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 64551788a8b..ce6c3633e8d 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -920,15 +920,20 @@ def remove_nodes(self, variables=None, constraints=None): " variables and constraints and pass them in the order variables," " constraints." ) + if ( + any(var in self._con_index_map for var in variables) + or any(con in self._var_index_map for con in constraints) + ): + deprecation_warning(depr_msg, version="6.7.2.dev0") + # If we received variables/constraints in the same list, sort them. + # Any unrecognized objects will be caught by _validate_input. for var in variables: if var in self._con_index_map: - deprecation_warning(depr_msg, version="TBD") cons_to_validate.append(var) else: vars_to_validate.append(var) for con in constraints: if con in self._var_index_map: - deprecation_warning(depr_msg, version="TBD") vars_to_validate.append(con) else: cons_to_validate.append(con) From ac75f8380b59b9981e6853a37879d753a8676ef4 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 3 Apr 2024 20:50:18 -0600 Subject: [PATCH 1128/3044] reformat --- pyomo/contrib/incidence_analysis/interface.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index ce6c3633e8d..8dee0539cb3 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -920,9 +920,8 @@ def remove_nodes(self, variables=None, constraints=None): " variables and constraints and pass them in the order variables," " constraints." ) - if ( - any(var in self._con_index_map for var in variables) - or any(con in self._var_index_map for con in constraints) + if any(var in self._con_index_map for var in variables) or any( + con in self._var_index_map for con in constraints ): deprecation_warning(depr_msg, version="6.7.2.dev0") # If we received variables/constraints in the same list, sort them. From ce34115f48d6610cda97e2438f91aae10886209a Mon Sep 17 00:00:00 2001 From: Eslick Date: Thu, 4 Apr 2024 08:32:43 -0400 Subject: [PATCH 1129/3044] Centralize function to merge AMPLFUNC and PYOMO_AMPLFUNC --- .../contrib/pynumero/interfaces/pyomo_nlp.py | 16 +++++++------- pyomo/solvers/amplfunc_merge.py | 21 +++++++++++++++++++ pyomo/solvers/plugins/solvers/ASL.py | 6 ++---- pyomo/solvers/plugins/solvers/IPOPT.py | 7 +++---- 4 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 pyomo/solvers/amplfunc_merge.py diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py index ce148f50ecf..bfd22ede86b 100644 --- a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py @@ -22,6 +22,7 @@ import pyomo.core.base as pyo from pyomo.common.collections import ComponentMap from pyomo.common.env import CtypesEnviron +from pyomo.solvers.amplfunc_merge import amplfunc_merge from ..sparse.block_matrix import BlockMatrix from pyomo.contrib.pynumero.interfaces.ampl_nlp import AslNLP from pyomo.contrib.pynumero.interfaces.nlp import NLP @@ -92,13 +93,14 @@ def __init__(self, pyomo_model, nl_file_options=None): # The NL writer advertises the external function libraries # through the PYOMO_AMPLFUNC environment variable; merge it # with any preexisting AMPLFUNC definitions - amplfunc_lines = os.environ.get("AMPLFUNC", "").split("\n") - existing = set(amplfunc_lines) - for line in os.environ.get("PYOMO_AMPLFUNC", "").split("\n"): - # Skip (a) empty lines and (b) lines we already have - if line != "" and line not in existing: - amplfunc_lines.append(line) - amplfunc = "\n".join(amplfunc_lines) + if 'PYOMO_AMPLFUNC' in os.environ: + if 'AMPLFUNC' in os.environ: + amplfunc = amplfunc_merge( + os.environ['AMPLFUNC'], os.environ['PYOMO_AMPLFUNC'] + ) + else: + amplfunc = os.environ['PYOMO_AMPLFUNC'] + with CtypesEnviron(AMPLFUNC=amplfunc): super(PyomoNLP, self).__init__(nl_file) diff --git a/pyomo/solvers/amplfunc_merge.py b/pyomo/solvers/amplfunc_merge.py new file mode 100644 index 00000000000..72c6587f552 --- /dev/null +++ b/pyomo/solvers/amplfunc_merge.py @@ -0,0 +1,21 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +def amplfunc_merge(amplfunc, pyomo_amplfunc): + """Merge two AMPLFUNC variable strings eliminating duplicate lines""" + amplfunc_lines = amplfunc.split("\n") + existing = set(amplfunc_lines) + for line in pyomo_amplfunc.split("\n"): + # Skip lines we already have + if line not in existing: + amplfunc_lines.append(line) + return "\n".join(amplfunc_lines) diff --git a/pyomo/solvers/plugins/solvers/ASL.py b/pyomo/solvers/plugins/solvers/ASL.py index 38a9fc1df58..6d3e08af259 100644 --- a/pyomo/solvers/plugins/solvers/ASL.py +++ b/pyomo/solvers/plugins/solvers/ASL.py @@ -23,6 +23,7 @@ from pyomo.opt.solver import SystemCallSolver from pyomo.core.kernel.block import IBlock from pyomo.solvers.mockmip import MockMIP +from pyomo.solvers.amplfunc_merge import amplfunc_merge from pyomo.core import TransformationFactory import logging @@ -160,10 +161,7 @@ def create_command_line(self, executable, problem_files): # if 'PYOMO_AMPLFUNC' in env: if 'AMPLFUNC' in env: - existing = set(env['AMPLFUNC'].split("\n")) - for line in env['PYOMO_AMPLFUNC'].split('\n'): - if line not in existing: - env['AMPLFUNC'] += "\n" + line + env['AMPLFUNC'] = amplfunc_merge(env['AMPLFUNC'], env['PYOMO_AMPLFUNC']) else: env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index 8f5190a4a07..a3c6b6beb28 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -21,6 +21,8 @@ from pyomo.opt.results import SolverStatus, SolverResults, TerminationCondition from pyomo.opt.solver import SystemCallSolver +from pyomo.solvers.amplfunc_merge import amplfunc_merge + import logging logger = logging.getLogger('pyomo.solvers') @@ -121,10 +123,7 @@ def create_command_line(self, executable, problem_files): # if 'PYOMO_AMPLFUNC' in env: if 'AMPLFUNC' in env: - existing = set(env['AMPLFUNC'].split("\n")) - for line in env['PYOMO_AMPLFUNC'].split('\n'): - if line not in existing: - env['AMPLFUNC'] += "\n" + line + env['AMPLFUNC'] = amplfunc_merge(env['AMPLFUNC'], env['PYOMO_AMPLFUNC']) else: env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] From 11d11e0672ef963631d9c75fb5946dc683e6592c Mon Sep 17 00:00:00 2001 From: Eslick Date: Thu, 4 Apr 2024 09:29:56 -0400 Subject: [PATCH 1130/3044] Add tests --- pyomo/solvers/amplfunc_merge.py | 6 ++ .../tests/checks/test_amplfunc_merge.py | 93 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 pyomo/solvers/tests/checks/test_amplfunc_merge.py diff --git a/pyomo/solvers/amplfunc_merge.py b/pyomo/solvers/amplfunc_merge.py index 72c6587f552..88babc2f43c 100644 --- a/pyomo/solvers/amplfunc_merge.py +++ b/pyomo/solvers/amplfunc_merge.py @@ -12,10 +12,16 @@ def amplfunc_merge(amplfunc, pyomo_amplfunc): """Merge two AMPLFUNC variable strings eliminating duplicate lines""" + # Assume that the strings amplfunc and pyomo_amplfunc don't contain duplicates + # Assume that the path separator is correct for the OS so we don't need to + # worry about comparing Unix and Windows paths. amplfunc_lines = amplfunc.split("\n") existing = set(amplfunc_lines) for line in pyomo_amplfunc.split("\n"): # Skip lines we already have if line not in existing: amplfunc_lines.append(line) + # Remove empty lines which could happen if one or both of the strings is + # empty or there are two new lines in a row for whatever reason. + amplfunc_lines = [s for s in amplfunc_lines if s != ""] return "\n".join(amplfunc_lines) diff --git a/pyomo/solvers/tests/checks/test_amplfunc_merge.py b/pyomo/solvers/tests/checks/test_amplfunc_merge.py new file mode 100644 index 00000000000..de31720010c --- /dev/null +++ b/pyomo/solvers/tests/checks/test_amplfunc_merge.py @@ -0,0 +1,93 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.solvers.amplfunc_merge import amplfunc_merge + + +class TestAMPLFUNCMerge(unittest.TestCase): + def test_merge_no_dup(self): + s1 = "my/place/l1.so\nanother/place/l1.so" + s2 = "my/place/l2.so" + sm = amplfunc_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 3) + # The order of lines should be maintained with the second string + # following the first + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + self.assertEqual(sm_list[2], "my/place/l2.so") + + def test_merge_empty1(self): + s1 = "" + s2 = "my/place/l2.so" + sm = amplfunc_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "my/place/l2.so") + + def test_merge_empty2(self): + s1 = "my/place/l2.so" + s2 = "" + sm = amplfunc_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "my/place/l2.so") + + def test_merge_empty_both(self): + s1 = "" + s2 = "" + sm = amplfunc_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "") + + def test_merge_bad_type(self): + self.assertRaises(AttributeError, amplfunc_merge, "", 3) + self.assertRaises(AttributeError, amplfunc_merge, 3, "") + self.assertRaises(AttributeError, amplfunc_merge, 3, 3) + self.assertRaises(AttributeError, amplfunc_merge, None, "") + self.assertRaises(AttributeError, amplfunc_merge, "", None) + self.assertRaises(AttributeError, amplfunc_merge, 2.3, "") + self.assertRaises(AttributeError, amplfunc_merge, "", 2.3) + + def test_merge_duplicate1(self): + s1 = "my/place/l1.so\nanother/place/l1.so" + s2 = "my/place/l1.so\nanother/place/l1.so" + sm = amplfunc_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + # The order of lines should be maintained with the second string + # following the first + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + def test_merge_duplicate2(self): + s1 = "my/place/l1.so\nanother/place/l1.so" + s2 = "my/place/l1.so" + sm = amplfunc_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + # The order of lines should be maintained with the second string + # following the first + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + def test_merge_extra_linebreaks(self): + s1 = "\nmy/place/l1.so\nanother/place/l1.so\n" + s2 = "\nmy/place/l1.so\n\n" + sm = amplfunc_merge(s1, s2) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + # The order of lines should be maintained with the second string + # following the first + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") From 30ffed7a4ee79435bd472f7846c0d95f67768712 Mon Sep 17 00:00:00 2001 From: Eslick Date: Thu, 4 Apr 2024 10:13:25 -0400 Subject: [PATCH 1131/3044] Fix error in pyomo_nlp missing undefined amplfunc --- .../contrib/pynumero/interfaces/pyomo_nlp.py | 8 +- pyomo/solvers/amplfunc_merge.py | 6 +- pyomo/solvers/plugins/solvers/ASL.py | 8 +- pyomo/solvers/plugins/solvers/IPOPT.py | 8 +- .../tests/checks/test_amplfunc_merge.py | 114 +++++++++++++++--- 5 files changed, 110 insertions(+), 34 deletions(-) diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py index bfd22ede86b..e12d0cf568b 100644 --- a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py @@ -93,13 +93,7 @@ def __init__(self, pyomo_model, nl_file_options=None): # The NL writer advertises the external function libraries # through the PYOMO_AMPLFUNC environment variable; merge it # with any preexisting AMPLFUNC definitions - if 'PYOMO_AMPLFUNC' in os.environ: - if 'AMPLFUNC' in os.environ: - amplfunc = amplfunc_merge( - os.environ['AMPLFUNC'], os.environ['PYOMO_AMPLFUNC'] - ) - else: - amplfunc = os.environ['PYOMO_AMPLFUNC'] + amplfunc = amplfunc_merge(os.environ) with CtypesEnviron(AMPLFUNC=amplfunc): super(PyomoNLP, self).__init__(nl_file) diff --git a/pyomo/solvers/amplfunc_merge.py b/pyomo/solvers/amplfunc_merge.py index 88babc2f43c..4c77f080ca1 100644 --- a/pyomo/solvers/amplfunc_merge.py +++ b/pyomo/solvers/amplfunc_merge.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ -def amplfunc_merge(amplfunc, pyomo_amplfunc): +def amplfunc_string_merge(amplfunc, pyomo_amplfunc): """Merge two AMPLFUNC variable strings eliminating duplicate lines""" # Assume that the strings amplfunc and pyomo_amplfunc don't contain duplicates # Assume that the path separator is correct for the OS so we don't need to @@ -25,3 +25,7 @@ def amplfunc_merge(amplfunc, pyomo_amplfunc): # empty or there are two new lines in a row for whatever reason. amplfunc_lines = [s for s in amplfunc_lines if s != ""] return "\n".join(amplfunc_lines) + +def amplfunc_merge(env): + """Merge AMPLFUNC and PYOMO_AMPLFuNC in an environment var dict""" + return amplfunc_string_merge(env.get("AMPLFUNC", ""), env.get("PYOMO_AMPLFUNC", "")) \ No newline at end of file diff --git a/pyomo/solvers/plugins/solvers/ASL.py b/pyomo/solvers/plugins/solvers/ASL.py index 6d3e08af259..bb8174a013e 100644 --- a/pyomo/solvers/plugins/solvers/ASL.py +++ b/pyomo/solvers/plugins/solvers/ASL.py @@ -159,11 +159,9 @@ def create_command_line(self, executable, problem_files): # Pyomo/Pyomo) with any user-specified external function # libraries # - if 'PYOMO_AMPLFUNC' in env: - if 'AMPLFUNC' in env: - env['AMPLFUNC'] = amplfunc_merge(env['AMPLFUNC'], env['PYOMO_AMPLFUNC']) - else: - env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] + amplfunc = amplfunc_merge(env) + if amplfunc: + env['AMPLFUNC'] = amplfunc cmd = [executable, problem_files[0], '-AMPL'] if self._timer: diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index a3c6b6beb28..21045cb7b4f 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -121,11 +121,9 @@ def create_command_line(self, executable, problem_files): # Pyomo/Pyomo) with any user-specified external function # libraries # - if 'PYOMO_AMPLFUNC' in env: - if 'AMPLFUNC' in env: - env['AMPLFUNC'] = amplfunc_merge(env['AMPLFUNC'], env['PYOMO_AMPLFUNC']) - else: - env['AMPLFUNC'] = env['PYOMO_AMPLFUNC'] + amplfunc = amplfunc_merge(env) + if amplfunc: + env['AMPLFUNC'] = amplfunc cmd = [executable, problem_files[0], '-AMPL'] if self._timer: diff --git a/pyomo/solvers/tests/checks/test_amplfunc_merge.py b/pyomo/solvers/tests/checks/test_amplfunc_merge.py index de31720010c..fb7701e9282 100644 --- a/pyomo/solvers/tests/checks/test_amplfunc_merge.py +++ b/pyomo/solvers/tests/checks/test_amplfunc_merge.py @@ -10,14 +10,14 @@ # ___________________________________________________________________________ import pyomo.common.unittest as unittest -from pyomo.solvers.amplfunc_merge import amplfunc_merge +from pyomo.solvers.amplfunc_merge import amplfunc_string_merge, amplfunc_merge -class TestAMPLFUNCMerge(unittest.TestCase): +class TestAMPLFUNCStringMerge(unittest.TestCase): def test_merge_no_dup(self): s1 = "my/place/l1.so\nanother/place/l1.so" s2 = "my/place/l2.so" - sm = amplfunc_merge(s1, s2) + sm = amplfunc_string_merge(s1, s2) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 3) # The order of lines should be maintained with the second string @@ -29,7 +29,7 @@ def test_merge_no_dup(self): def test_merge_empty1(self): s1 = "" s2 = "my/place/l2.so" - sm = amplfunc_merge(s1, s2) + sm = amplfunc_string_merge(s1, s2) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 1) self.assertEqual(sm_list[0], "my/place/l2.so") @@ -37,7 +37,7 @@ def test_merge_empty1(self): def test_merge_empty2(self): s1 = "my/place/l2.so" s2 = "" - sm = amplfunc_merge(s1, s2) + sm = amplfunc_string_merge(s1, s2) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 1) self.assertEqual(sm_list[0], "my/place/l2.so") @@ -45,24 +45,24 @@ def test_merge_empty2(self): def test_merge_empty_both(self): s1 = "" s2 = "" - sm = amplfunc_merge(s1, s2) + sm = amplfunc_string_merge(s1, s2) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 1) self.assertEqual(sm_list[0], "") def test_merge_bad_type(self): - self.assertRaises(AttributeError, amplfunc_merge, "", 3) - self.assertRaises(AttributeError, amplfunc_merge, 3, "") - self.assertRaises(AttributeError, amplfunc_merge, 3, 3) - self.assertRaises(AttributeError, amplfunc_merge, None, "") - self.assertRaises(AttributeError, amplfunc_merge, "", None) - self.assertRaises(AttributeError, amplfunc_merge, 2.3, "") - self.assertRaises(AttributeError, amplfunc_merge, "", 2.3) + self.assertRaises(AttributeError, amplfunc_string_merge, "", 3) + self.assertRaises(AttributeError, amplfunc_string_merge, 3, "") + self.assertRaises(AttributeError, amplfunc_string_merge, 3, 3) + self.assertRaises(AttributeError, amplfunc_string_merge, None, "") + self.assertRaises(AttributeError, amplfunc_string_merge, "", None) + self.assertRaises(AttributeError, amplfunc_string_merge, 2.3, "") + self.assertRaises(AttributeError, amplfunc_string_merge, "", 2.3) def test_merge_duplicate1(self): s1 = "my/place/l1.so\nanother/place/l1.so" s2 = "my/place/l1.so\nanother/place/l1.so" - sm = amplfunc_merge(s1, s2) + sm = amplfunc_string_merge(s1, s2) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 2) # The order of lines should be maintained with the second string @@ -73,7 +73,7 @@ def test_merge_duplicate1(self): def test_merge_duplicate2(self): s1 = "my/place/l1.so\nanother/place/l1.so" s2 = "my/place/l1.so" - sm = amplfunc_merge(s1, s2) + sm = amplfunc_string_merge(s1, s2) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 2) # The order of lines should be maintained with the second string @@ -84,10 +84,92 @@ def test_merge_duplicate2(self): def test_merge_extra_linebreaks(self): s1 = "\nmy/place/l1.so\nanother/place/l1.so\n" s2 = "\nmy/place/l1.so\n\n" - sm = amplfunc_merge(s1, s2) + sm = amplfunc_string_merge(s1, s2) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 2) # The order of lines should be maintained with the second string # following the first self.assertEqual(sm_list[0], "my/place/l1.so") self.assertEqual(sm_list[1], "another/place/l1.so") + +class TestAMPLFUNCMerge(unittest.TestCase): + def test_merge_no_dup(self): + env = { + "AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", + "PYOMO_AMPLFUNC": "my/place/l2.so", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 3) + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + self.assertEqual(sm_list[2], "my/place/l2.so") + + def test_merge_empty1(self): + env = { + "AMPLFUNC": "", + "PYOMO_AMPLFUNC": "my/place/l2.so", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "my/place/l2.so") + + def test_merge_empty2(self): + env = { + "AMPLFUNC": "my/place/l2.so", + "PYOMO_AMPLFUNC": "", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "my/place/l2.so") + + def test_merge_empty_both(self): + env = { + "AMPLFUNC": "", + "PYOMO_AMPLFUNC": "", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "") + + def test_merge_duplicate1(self): + env = { + "AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", + "PYOMO_AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + def test_merge_no_pyomo(self): + env = { + "AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + def test_merge_no_user(self): + env = { + "PYOMO_AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", + } + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 2) + self.assertEqual(sm_list[0], "my/place/l1.so") + self.assertEqual(sm_list[1], "another/place/l1.so") + + def test_merge_nothing(self): + env = {} + sm = amplfunc_merge(env) + sm_list = sm.split("\n") + self.assertEqual(len(sm_list), 1) + self.assertEqual(sm_list[0], "") + From d4c9ddf35c28dbd2ce20f81e8fff485d3fec13b5 Mon Sep 17 00:00:00 2001 From: Eslick Date: Thu, 4 Apr 2024 10:20:44 -0400 Subject: [PATCH 1132/3044] Run black --- pyomo/solvers/amplfunc_merge.py | 3 ++- pyomo/solvers/tests/checks/test_amplfunc_merge.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/solvers/amplfunc_merge.py b/pyomo/solvers/amplfunc_merge.py index 4c77f080ca1..9d127a94396 100644 --- a/pyomo/solvers/amplfunc_merge.py +++ b/pyomo/solvers/amplfunc_merge.py @@ -26,6 +26,7 @@ def amplfunc_string_merge(amplfunc, pyomo_amplfunc): amplfunc_lines = [s for s in amplfunc_lines if s != ""] return "\n".join(amplfunc_lines) + def amplfunc_merge(env): """Merge AMPLFUNC and PYOMO_AMPLFuNC in an environment var dict""" - return amplfunc_string_merge(env.get("AMPLFUNC", ""), env.get("PYOMO_AMPLFUNC", "")) \ No newline at end of file + return amplfunc_string_merge(env.get("AMPLFUNC", ""), env.get("PYOMO_AMPLFUNC", "")) diff --git a/pyomo/solvers/tests/checks/test_amplfunc_merge.py b/pyomo/solvers/tests/checks/test_amplfunc_merge.py index fb7701e9282..00885feb5a4 100644 --- a/pyomo/solvers/tests/checks/test_amplfunc_merge.py +++ b/pyomo/solvers/tests/checks/test_amplfunc_merge.py @@ -92,6 +92,7 @@ def test_merge_extra_linebreaks(self): self.assertEqual(sm_list[0], "my/place/l1.so") self.assertEqual(sm_list[1], "another/place/l1.so") + class TestAMPLFUNCMerge(unittest.TestCase): def test_merge_no_dup(self): env = { @@ -172,4 +173,3 @@ def test_merge_nothing(self): sm_list = sm.split("\n") self.assertEqual(len(sm_list), 1) self.assertEqual(sm_list[0], "") - From 4849ec515cee9d7ea1fd48747dc5f087a56752cb Mon Sep 17 00:00:00 2001 From: Eslick Date: Thu, 4 Apr 2024 10:30:58 -0400 Subject: [PATCH 1133/3044] Run black again --- .../tests/checks/test_amplfunc_merge.py | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/pyomo/solvers/tests/checks/test_amplfunc_merge.py b/pyomo/solvers/tests/checks/test_amplfunc_merge.py index 00885feb5a4..2c819404d2f 100644 --- a/pyomo/solvers/tests/checks/test_amplfunc_merge.py +++ b/pyomo/solvers/tests/checks/test_amplfunc_merge.py @@ -107,30 +107,21 @@ def test_merge_no_dup(self): self.assertEqual(sm_list[2], "my/place/l2.so") def test_merge_empty1(self): - env = { - "AMPLFUNC": "", - "PYOMO_AMPLFUNC": "my/place/l2.so", - } + env = {"AMPLFUNC": "", "PYOMO_AMPLFUNC": "my/place/l2.so"} sm = amplfunc_merge(env) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 1) self.assertEqual(sm_list[0], "my/place/l2.so") def test_merge_empty2(self): - env = { - "AMPLFUNC": "my/place/l2.so", - "PYOMO_AMPLFUNC": "", - } + env = {"AMPLFUNC": "my/place/l2.so", "PYOMO_AMPLFUNC": ""} sm = amplfunc_merge(env) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 1) self.assertEqual(sm_list[0], "my/place/l2.so") def test_merge_empty_both(self): - env = { - "AMPLFUNC": "", - "PYOMO_AMPLFUNC": "", - } + env = {"AMPLFUNC": "", "PYOMO_AMPLFUNC": ""} sm = amplfunc_merge(env) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 1) @@ -148,9 +139,7 @@ def test_merge_duplicate1(self): self.assertEqual(sm_list[1], "another/place/l1.so") def test_merge_no_pyomo(self): - env = { - "AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", - } + env = {"AMPLFUNC": "my/place/l1.so\nanother/place/l1.so"} sm = amplfunc_merge(env) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 2) @@ -158,9 +147,7 @@ def test_merge_no_pyomo(self): self.assertEqual(sm_list[1], "another/place/l1.so") def test_merge_no_user(self): - env = { - "PYOMO_AMPLFUNC": "my/place/l1.so\nanother/place/l1.so", - } + env = {"PYOMO_AMPLFUNC": "my/place/l1.so\nanother/place/l1.so"} sm = amplfunc_merge(env) sm_list = sm.split("\n") self.assertEqual(len(sm_list), 2) From 24d3baf9b174beab9f34f39e02010fc87194a193 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 4 Apr 2024 09:03:56 -0600 Subject: [PATCH 1134/3044] replace TBD with dev version in docstring --- pyomo/contrib/incidence_analysis/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index 8dee0539cb3..0f47e03d0a2 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -891,9 +891,9 @@ def remove_nodes(self, variables=None, constraints=None): .. note:: - **Deprecation in Pyomo vTBD** + **Deprecation in Pyomo v6.7.2.dev0** - The pre-TBD implementation of ``remove_nodes`` allowed variables and + The pre-6.7.2.dev0 implementation of ``remove_nodes`` allowed variables and constraints to remove to be specified in a single list. This made error checking difficult, and indeed, if invalid components were provided, we carried on silently instead of throwing an error or From b830879053cdbed2a6746bf0bf7b4af2f15ac073 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 4 Apr 2024 10:22:04 -0600 Subject: [PATCH 1135/3044] adding 'synchronize' expression --- pyomo/contrib/cp/__init__.py | 6 +++++- pyomo/contrib/cp/repn/docplex_writer.py | 6 ++++++ .../cp/scheduling_expr/scheduling_logic.py | 15 +++++++++++++ pyomo/contrib/cp/tests/test_docplex_walker.py | 21 +++++++++++++++++++ .../cp/tests/test_sequence_expressions.py | 15 +++++++++++++ 5 files changed, 62 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/cp/__init__.py b/pyomo/contrib/cp/__init__.py index 96ef037853a..d206fe95251 100644 --- a/pyomo/contrib/cp/__init__.py +++ b/pyomo/contrib/cp/__init__.py @@ -25,7 +25,11 @@ before_in_sequence, predecessor_to, ) -from pyomo.contrib.cp.scheduling_expr.scheduling_logic import alternative, spans +from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( + alternative, + spans, + synchronize, +) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, Step, diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 93b9974434a..98d3e07e8ed 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -39,6 +39,7 @@ from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( AlternativeExpression, SpanExpression, + SynchronizeExpression, ) from pyomo.contrib.cp.scheduling_expr.precedence_expressions import ( BeforeExpression, @@ -992,6 +993,10 @@ def _handle_alternative_expression_node(visitor, node, *args): return _GENERAL, cp.alternative(args[0][1], [arg[1] for arg in args[1:]]) +def _handle_synchronize_expression_node(visitor, node, *args): + return _GENERAL, cp.synchronize(args[0][1], [arg[1] for arg in args[1:]]) + + class LogicalToDoCplex(StreamBasedExpressionVisitor): _operator_handles = { EXPR.GetItemExpression: _handle_getitem, @@ -1040,6 +1045,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): PredecessorToExpression: _handle_predecessor_to_expression_node, SpanExpression: _handle_span_expression_node, AlternativeExpression: _handle_alternative_expression_node, + SynchronizeExpression: _handle_synchronize_expression_node, } _var_handles = { IntervalVarStartTime: _before_interval_var_start_time, diff --git a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py index b28d536b594..3556c3083fd 100644 --- a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py +++ b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py @@ -36,6 +36,15 @@ def _to_string(self, values, verbose, smap): return "alternative(%s, [%s])" % (values[0], ", ".join(values[1:])) +class SynchronizeExpression(NaryBooleanExpression): + """ + + """ + + def _to_string(self, values, verbose, smap): + return "synchronize(%s, [%s])" % (values[0], ", ".join(values[1:])) + + def spans(*args): """Creates a new SpanExpression""" @@ -46,3 +55,9 @@ def alternative(*args): """Creates a new AlternativeExpression""" return AlternativeExpression(list(_flattened(args))) + + +def synchronize(*args): + """Creates a new SynchronizeExpression""" + + return SynchronizeExpression(list(_flattened(args))) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 4ad07c10ee7..cce7306d3f9 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -20,6 +20,7 @@ before_in_sequence, predecessor_to, alternative, + synchronize, ) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( AlwaysIn, @@ -1555,6 +1556,26 @@ def test_alternative(self): expr[1].equals(cp.alternative(whole_enchilada, [iv[i] for i in [1, 2, 3]])) ) + def test_synchronize(self): + m = self.get_model() + e = synchronize(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) + + visitor = self.get_visitor() + expr = visitor.walk_expression((e, e, 0)) + + self.assertIn(id(m.whole_enchilada), visitor.var_map) + whole_enchilada = visitor.var_map[id(m.whole_enchilada)] + self.assertIs(visitor.pyomo_to_docplex[m.whole_enchilada], whole_enchilada) + + iv = {} + for i in [1, 2, 3]: + self.assertIn(id(m.iv[i]), visitor.var_map) + iv[i] = visitor.var_map[id(m.iv[i])] + + self.assertTrue( + expr[1].equals(cp.synchronize(whole_enchilada, [iv[i] for i in [1, 2, 3]])) + ) + @unittest.skipIf(not docplex_available, "docplex is not available") class TestCPExpressionWalker_CumulFuncExpressions(CommonTest): diff --git a/pyomo/contrib/cp/tests/test_sequence_expressions.py b/pyomo/contrib/cp/tests/test_sequence_expressions.py index 62c868abfaf..b676881e379 100644 --- a/pyomo/contrib/cp/tests/test_sequence_expressions.py +++ b/pyomo/contrib/cp/tests/test_sequence_expressions.py @@ -15,8 +15,10 @@ from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( AlternativeExpression, SpanExpression, + SynchronizeExpression, alternative, spans, + synchronize, ) from pyomo.contrib.cp.scheduling_expr.sequence_expressions import ( NoOverlapExpression, @@ -159,3 +161,16 @@ def test_alternative(self): self.assertIs(e.args[i], m.iv[i]) self.assertEqual(str(e), "alternative(whole_enchilada, [iv[1], iv[2], iv[3]])") + + def test_synchronize(self): + m = self.make_model() + e = synchronize(m.whole_enchilada, [m.iv[i] for i in [1, 2, 3]]) + + self.assertIsInstance(e, SynchronizeExpression) + self.assertEqual(e.nargs(), 4) + self.assertEqual(len(e.args), 4) + self.assertIs(e.args[0], m.whole_enchilada) + for i in [1, 2, 3]: + self.assertIs(e.args[i], m.iv[i]) + + self.assertEqual(str(e), "synchronize(whole_enchilada, [iv[1], iv[2], iv[3]])") From 535dda6fbcf876b8ad31f226fd26748e6dc4e81f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 4 Apr 2024 10:23:11 -0600 Subject: [PATCH 1136/3044] black --- pyomo/contrib/cp/scheduling_expr/scheduling_logic.py | 4 +--- pyomo/contrib/cp/tests/test_debugging.py | 7 ++----- pyomo/contrib/cp/tests/test_docplex_walker.py | 4 ++-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py index 3556c3083fd..98e0c1ceabd 100644 --- a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py +++ b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py @@ -37,9 +37,7 @@ def _to_string(self, values, verbose, smap): class SynchronizeExpression(NaryBooleanExpression): - """ - - """ + """ """ def _to_string(self, values, verbose, smap): return "synchronize(%s, [%s])" % (values[0], ", ".join(values[1:])) diff --git a/pyomo/contrib/cp/tests/test_debugging.py b/pyomo/contrib/cp/tests/test_debugging.py index 4561b5e9f04..8e24e545724 100644 --- a/pyomo/contrib/cp/tests/test_debugging.py +++ b/pyomo/contrib/cp/tests/test_debugging.py @@ -11,11 +11,8 @@ import pyomo.common.unittest as unittest -from pyomo.environ import ( - ConcreteModel, - Constraint, - Var, -) +from pyomo.environ import ConcreteModel, Constraint, Var + class TestCPDebugging(unittest.TestCase): def test_debug_infeasibility(self): diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index cce7306d3f9..d14e0bc2d6f 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -248,7 +248,7 @@ def test_monomial_expressions(self): const_expr = 3 * m.x nested_expr = (1 / m.p) * m.x pow_expr = (m.p ** (0.5)) * m.x - + e = m.x * 4 expr = visitor.walk_expression((e, e, 0)) self.assertIn(id(m.x), visitor.var_map) @@ -1574,7 +1574,7 @@ def test_synchronize(self): self.assertTrue( expr[1].equals(cp.synchronize(whole_enchilada, [iv[i] for i in [1, 2, 3]])) - ) + ) @unittest.skipIf(not docplex_available, "docplex is not available") From 347b5950ba5e8515541783e6480e79c50d05ec88 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:24:33 -0600 Subject: [PATCH 1137/3044] standard_form: return objective list, offsets --- pyomo/repn/plugins/standard_form.py | 39 ++++++++++++++++++----------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index ea7b6a6a9e6..d09537e4eee 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -61,12 +61,16 @@ class LinearStandardFormInfo(object): Attributes ---------- - c : scipy.sparse.csr_array + c : scipy.sparse.csc_array The objective coefficients. Note that this is a sparse array and may contain multiple rows (for multiobjective problems). The objectives may be calculated by "c @ x" + c_offset : numpy.ndarray + + The list of objective constant offsets + A : scipy.sparse.csc_array The constraint coefficients. The constraint bodies may be @@ -89,6 +93,10 @@ class LinearStandardFormInfo(object): The list of Pyomo variable objects corresponding to columns in the `A` and `c` matrices. + objectives : List[_ObjectiveData] + + The list of Pyomo objective objects correcponding to the active objectives + eliminated_vars: List[Tuple[_VarData, NumericExpression]] The list of variables from the original model that do not appear @@ -101,12 +109,14 @@ class LinearStandardFormInfo(object): """ - def __init__(self, c, A, rhs, rows, columns, eliminated_vars): + def __init__(self, c, c_offset, A, rhs, rows, columns, objectives, eliminated_vars): self.c = c + self.c_offset = c_offset self.A = A self.rhs = rhs self.rows = rows self.columns = columns + self.objectives = objectives self.eliminated_vars = eliminated_vars @property @@ -305,21 +315,18 @@ def write(self, model): # # Process objective # - if not component_map[Objective]: - objectives = [Objective(expr=1)] - objectives[0].construct() - else: - objectives = [] - for blk in component_map[Objective]: - objectives.extend( - blk.component_data_objects( - Objective, active=True, descend_into=False, sort=sorter - ) + objectives = [] + for blk in component_map[Objective]: + objectives.extend( + blk.component_data_objects( + Objective, active=True, descend_into=False, sort=sorter ) + ) + obj_offset = [] obj_data = [] obj_index = [] obj_index_ptr = [0] - for i, obj in enumerate(objectives): + for obj in objectives: repn = visitor.walk_expression(obj.expr) if repn.nonlinear is not None: raise ValueError( @@ -328,8 +335,10 @@ def write(self, model): ) N = len(repn.linear) obj_data.append(np.fromiter(repn.linear.values(), float, N)) + obj_offset.append(repn.constant) if obj.sense == maximize: obj_data[-1] *= -1 + obj_offset[-1] *= -1 obj_index.append( np.fromiter(map(var_order.__getitem__, repn.linear), float, N) ) @@ -495,7 +504,9 @@ def write(self, model): else: eliminated_vars = [] - info = LinearStandardFormInfo(c, A, rhs, rows, columns, eliminated_vars) + info = LinearStandardFormInfo( + c, np.array(obj_offset), A, rhs, rows, columns, objectives, eliminated_vars + ) timer.toc("Generated linear standard form representation", delta=False) return info From 89556c3da721c10ad7390cc2be0a0cc07e1124db Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:25:19 -0600 Subject: [PATCH 1138/3044] standard_form: allow empty objectives, constraints --- pyomo/repn/plugins/standard_form.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index d09537e4eee..0211ba44387 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -465,13 +465,17 @@ def write(self, model): # Get the variable list columns = list(var_map.values()) # Convert the compiled data to scipy sparse matrices + if obj_data: + obj_data = np.concatenate(obj_data) + obj_index = np.concatenate(obj_index) c = scipy.sparse.csr_array( - (np.concatenate(obj_data), np.concatenate(obj_index), obj_index_ptr), - [len(obj_index_ptr) - 1, len(columns)], + (obj_data, obj_index, obj_index_ptr), [len(obj_index_ptr) - 1, len(columns)] ).tocsc() + if rows: + con_data = np.concatenate(con_data) + con_index = np.concatenate(con_index) A = scipy.sparse.csr_array( - (np.concatenate(con_data), np.concatenate(con_index), con_index_ptr), - [len(rows), len(columns)], + (con_data, con_index, con_index_ptr), [len(rows), len(columns)] ).tocsc() # Some variables in the var_map may not actually appear in the From e60c0dea7749100c94da6a697395d143e720f3c5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:31:24 -0600 Subject: [PATCH 1139/3044] gurobi_direct: support (partial) loading duals, reduced costs --- pyomo/contrib/solver/gurobi_direct.py | 77 ++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 1164686f0f1..f10cc8f619f 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -15,6 +15,7 @@ import os from pyomo.common.config import ConfigValue +from pyomo.common.collections import ComponentMap, ComponentSet from pyomo.common.dependencies import attempt_import from pyomo.common.shutdown import python_is_shutting_down from pyomo.common.tee import capture_output, TeeStream @@ -59,10 +60,13 @@ def __init__( class GurobiDirectSolutionLoader(SolutionLoaderBase): - def __init__(self, grb_model, grb_vars, pyo_vars): + def __init__(self, grb_model, grb_cons, grb_vars, pyo_cons, pyo_vars, pyo_obj): self._grb_model = grb_model + self._grb_cons = grb_cons self._grb_vars = grb_vars + self._pyo_cons = pyo_cons self._pyo_vars = pyo_vars + self._pyo_obj = pyo_obj GurobiDirect._num_instances += 1 def __del__(self): @@ -72,15 +76,70 @@ def __del__(self): GurobiDirect.release_license() def load_vars(self, vars_to_load=None, solution_number=0): - assert vars_to_load is None assert solution_number == 0 - for p_var, g_var in zip(self._pyo_vars, self._grb_vars.x.tolist()): + if self._grb_model.SolCount == 0: + raise RuntimeError( + 'Solver does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + + iterator = zip(self._pyo_vars, self._grb_vars.x.tolist()) + if vars_to_load: + vars_to_load = ComponentSet(vars_to_load) + iterator = filter(lambda var_val: var_val[0] in vars_to_load, iterator) + for p_var, g_var in iterator: p_var.set_value(g_var, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) - def get_primals(self, vars_to_load=None): - assert vars_to_load is None + def get_primals(self, vars_to_load=None, solution_number=0): assert solution_number == 0 - return ComponentMap(zip(self._pyo_vars, self._grb_vars.x.tolist())) + if self._grb_model.SolCount == 0: + raise RuntimeError( + 'Solver does not currently have a valid solution. Please ' + 'check the termination condition.' + ) + + iterator = zip(self._pyo_vars, self._grb_vars.x.tolist()) + if vars_to_load: + vars_to_load = ComponentSet(vars_to_load) + iterator = filter(lambda var_val: var_val[0] in vars_to_load, iterator) + return ComponentMap(iterator) + + def get_duals(self, cons_to_load=None): + if self._grb_model.Status != gurobipy.GRB.OPTIMAL: + raise RuntimeError( + 'Solver does not currently have valid duals. Please ' + 'check the termination condition.' + ) + + def dedup(_iter): + last = None + for con_info_dual in _iter: + if not con_info_dual[1] and con_info_dual[0][0] is last: + continue + last = con_info_dual[0][0] + yield con_info_dual + + iterator = dedup(zip(self._pyo_cons, self._grb_cons.getAttr('Pi').tolist())) + if cons_to_load: + cons_to_load = set(cons_to_load) + iterator = filter( + lambda con_info_dual: con_info_dual[0][0] in cons_to_load, iterator + ) + return {con_info[0]: dual for con_info, dual in iterator} + + def get_reduced_costs(self, vars_to_load=None): + if self._grb_model.Status != gurobipy.GRB.OPTIMAL: + raise RuntimeError( + 'Solver does not currently have valid reduced costs. Please ' + 'check the termination condition.' + ) + + iterator = zip(self._pyo_vars, self._grb_vars.getAttr('Rc').tolist()) + if vars_to_load: + vars_to_load = ComponentSet(vars_to_load) + iterator = filter(lambda var_rc: var_rc[0] in vars_to_load, iterator) + return ComponentMap(iterator) class GurobiDirect(SolverBase): @@ -240,7 +299,11 @@ def solve(self, model, **kwds) -> Results: os.chdir(orig_cwd) res = self._postsolve( - timer, GurobiDirectSolutionLoader(gurobi_model, x, repn.columns) + timer, + config, + GurobiDirectSolutionLoader( + gurobi_model, A, x, repn.rows, repn.columns, repn.objectives + ), ) res.solver_configuration = config res.solver_name = 'Gurobi' From dbefc5cab0a824283b8ac00d92950e569a10760f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:39:44 -0600 Subject: [PATCH 1140/3044] gurobi_direct: do not store ephemeral config on instance; rename gprob --- pyomo/contrib/solver/gurobi_direct.py | 38 +++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index f10cc8f619f..1d4b1871654 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -213,12 +213,13 @@ def version(self): def solve(self, model, **kwds) -> Results: start_timestamp = datetime.datetime.now(datetime.timezone.utc) - self._config = config = self.config(value=kwds, preserve_implicit=True) - StaleFlagManager.mark_all_as_stale() + config = self.config(value=kwds, preserve_implicit=True) if config.timer is None: config.timer = HierarchicalTimer() timer = config.timer + StaleFlagManager.mark_all_as_stale() + timer.start('compile_model') repn = LinearStandardFormCompiler().write(model, mixed_form=True) timer.stop('compile_model') @@ -256,8 +257,8 @@ def solve(self, model, **kwds) -> Results: try: orig_cwd = os.getcwd() - if self._config.working_dir: - os.chdir(self._config.working_dir) + if config.working_dir: + os.chdir(config.working_dir) with TeeStream(*ostreams) as t, capture_output(t.STDOUT, capture_fd=False): gurobi_model = gurobipy.Model() @@ -316,17 +317,15 @@ def solve(self, model, **kwds) -> Results: res.timing_info.timer = timer return res - def _postsolve(self, timer: HierarchicalTimer, loader): - config = self._config - - gprob = loader._grb_model - status = gprob.Status + def _postsolve(self, timer: HierarchicalTimer, config, loader): + grb_model = loader._grb_model + status = grb_model.Status results = Results() results.solution_loader = loader - results.timing_info.gurobi_time = gprob.Runtime + results.timing_info.gurobi_time = grb_model.Runtime - if gprob.SolCount > 0: + if grb_model.SolCount > 0: if status == gurobipy.GRB.OPTIMAL: results.solution_status = SolutionStatus.optimal else: @@ -349,30 +348,31 @@ def _postsolve(self, timer: HierarchicalTimer, loader): 'to bypass this error.' ) - results.incumbent_objective = None - results.objective_bound = None try: - results.incumbent_objective = gprob.ObjVal + if math.isfinite(grb_model.ObjVal): + results.incumbent_objective = grb_model.ObjVal + else: + results.incumbent_objective = None except (gurobipy.GurobiError, AttributeError): results.incumbent_objective = None try: - results.objective_bound = gprob.ObjBound + results.objective_bound = grb_model.ObjBound except (gurobipy.GurobiError, AttributeError): - if self._objective.sense == minimize: + if grb_model.ModelSense == OptimizationSense.minimize: results.objective_bound = -math.inf else: results.objective_bound = math.inf - if results.incumbent_objective is not None and not math.isfinite( results.incumbent_objective ): results.incumbent_objective = None + results.objective_bound = None - results.iteration_count = gprob.getAttr('IterCount') + results.iteration_count = grb_model.getAttr('IterCount') timer.start('load solution') if config.load_solutions: - if gprob.SolCount > 0: + if grb_model.SolCount > 0: results.solution_loader.load_vars() else: raise RuntimeError( From e11fd66b7c8abeff4aa7c9b728de252ec20741d9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:41:10 -0600 Subject: [PATCH 1141/3044] gurobi_direct: make var processing more efficient --- pyomo/contrib/solver/gurobi_direct.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 1d4b1871654..54ef2c5306e 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -237,20 +237,19 @@ def solve(self, model, **kwds) -> Results: _u = inf lb.append(_l) ub.append(_u) + CON = gurobipy.GRB.CONTINUOUS + BIN = gurobipy.GRB.BINARY + INT = gurobipy.GRB.INTEGER vtype = [ ( - gurobipy.GRB.CONTINUOUS + CON if v.is_continuous() - else ( - gurobipy.GRB.BINARY - if v.is_binary() - else gurobipy.GRB.INTEGER if v.is_integer() else '?' - ) + else (BIN if v.is_binary() else INT if v.is_integer() else '?') ) for v in repn.columns ] - sense_type = '>=<' - sense = [sense_type[r[1] + 1] for r in repn.rows] + sense_type = '=<>' # Note: ordering matches 0, 1, -1 + sense = [sense_type[r[1]] for r in repn.rows] timer.stop('prepare_matrices') ostreams = [io.StringIO()] + config.tee From 99a7efc2cbc93456c2a0e69850593444613b4a24 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:41:57 -0600 Subject: [PATCH 1142/3044] gurobi_direct: support models with no objectives --- pyomo/contrib/solver/gurobi_direct.py | 36 ++++++++++++++------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 54ef2c5306e..82491e88a42 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -266,10 +266,13 @@ def solve(self, model, **kwds) -> Results: len(repn.columns), lb=lb, ub=ub, - obj=repn.c.todense()[0], + obj=repn.c.todense()[0] if repn.c.shape[0] else 0, vtype=vtype, ) A = gurobi_model.addMConstr(repn.A, x, sense, repn.rhs) + if repn.c.shape[0]: + gurobi_model.setAttr('ObjCon', repn.c_offset[0]) + gurobi_model.setAttr('ModelSense', int(repn.objectives[0].sense)) # gurobi_model.update() timer.stop('transfer_model') @@ -347,23 +350,22 @@ def _postsolve(self, timer: HierarchicalTimer, config, loader): 'to bypass this error.' ) - try: - if math.isfinite(grb_model.ObjVal): - results.incumbent_objective = grb_model.ObjVal - else: + if loader._pyo_obj: + try: + if math.isfinite(grb_model.ObjVal): + results.incumbent_objective = grb_model.ObjVal + else: + results.incumbent_objective = None + except (gurobipy.GurobiError, AttributeError): results.incumbent_objective = None - except (gurobipy.GurobiError, AttributeError): - results.incumbent_objective = None - try: - results.objective_bound = grb_model.ObjBound - except (gurobipy.GurobiError, AttributeError): - if grb_model.ModelSense == OptimizationSense.minimize: - results.objective_bound = -math.inf - else: - results.objective_bound = math.inf - if results.incumbent_objective is not None and not math.isfinite( - results.incumbent_objective - ): + try: + results.objective_bound = grb_model.ObjBound + except (gurobipy.GurobiError, AttributeError): + if grb_model.ModelSense == OptimizationSense.minimize: + results.objective_bound = -math.inf + else: + results.objective_bound = math.inf + else: results.incumbent_objective = None results.objective_bound = None From 7e4938f8e4fe32eef50c1d4816c378d0e28b6413 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:42:19 -0600 Subject: [PATCH 1143/3044] NFC: wrap long line --- pyomo/contrib/solver/gurobi_direct.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 82491e88a42..ab5a07bc5f5 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -54,7 +54,8 @@ def __init__( ConfigValue( default=False, domain=bool, - description="If True, the values of the integer variables will be passed to Gurobi.", + description="If True, the current values of the integer variables " + "will be passed to Gurobi.", ), ) From 93590c6eabbb1c2f0b06592f6687d2feefb0243e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:42:45 -0600 Subject: [PATCH 1144/3044] gurobi_direct: add error checking for MO problems --- pyomo/contrib/solver/gurobi_direct.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index ab5a07bc5f5..e0e0d7d32b0 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -225,6 +225,12 @@ def solve(self, model, **kwds) -> Results: repn = LinearStandardFormCompiler().write(model, mixed_form=True) timer.stop('compile_model') + if len(repn.objectives) > 1: + raise ValueError( + f"The {self.__class__.__name__} solver only supports models " + f"with zero or one objectives (received {len(repn.objectives)})." + ) + timer.start('prepare_matrices') inf = float('inf') ninf = -inf From 4ce69351e41e3dac0fa4a9f63f03c9e51805996a Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 4 Apr 2024 10:43:03 -0600 Subject: [PATCH 1145/3044] NFC: adding docstrings --- .../cp/scheduling_expr/scheduling_logic.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py index 98e0c1ceabd..e5695b57c5c 100644 --- a/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py +++ b/pyomo/contrib/cp/scheduling_expr/scheduling_logic.py @@ -29,15 +29,26 @@ def _to_string(self, values, verbose, smap): class AlternativeExpression(NaryBooleanExpression): """ - TODO/ + Expression over IntervalVars representing that if the first arg is present, + then exactly one of the following args must be present. The first arg is + absent if and only if all the others are absent. """ + # [ESJ 4/4/24]: docplex takes an optional 'cardinality' argument with this + # too--it generalized to "exactly n" of the intervals have to exist, + # basically. It would be nice to include this eventually, but this is + # probably fine for now. + def _to_string(self, values, verbose, smap): return "alternative(%s, [%s])" % (values[0], ", ".join(values[1:])) class SynchronizeExpression(NaryBooleanExpression): - """ """ + """ + Expression over IntervalVars synchronizing the first argument with all of the + following arguments. That is, if the first argument is present, the remaining + arguments start and end at the same time as it. + """ def _to_string(self, values, verbose, smap): return "synchronize(%s, [%s])" % (values[0], ", ".join(values[1:])) From 6fa6196a3e66c42b91a19e69ef9275c6289c493c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:48:34 -0600 Subject: [PATCH 1146/3044] standard_form: add option to control the final optimization sense --- pyomo/contrib/solver/gurobi_direct.py | 5 ++++- pyomo/repn/plugins/standard_form.py | 12 +++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index e0e0d7d32b0..36a783cba02 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -17,6 +17,7 @@ from pyomo.common.config import ConfigValue from pyomo.common.collections import ComponentMap, ComponentSet from pyomo.common.dependencies import attempt_import +from pyomo.common.enums import OptimizationSense from pyomo.common.shutdown import python_is_shutting_down from pyomo.common.tee import capture_output, TeeStream from pyomo.common.timing import HierarchicalTimer @@ -222,7 +223,9 @@ def solve(self, model, **kwds) -> Results: StaleFlagManager.mark_all_as_stale() timer.start('compile_model') - repn = LinearStandardFormCompiler().write(model, mixed_form=True) + repn = LinearStandardFormCompiler().write( + model, mixed_form=True, set_sense=None + ) timer.stop('compile_model') if len(repn.objectives) > 1: diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index 0211ba44387..566d0d8d932 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -20,6 +20,7 @@ document_kwargs_from_configdict, ) from pyomo.common.dependencies import scipy, numpy as np +from pyomo.common.enums import OptimizationSense from pyomo.common.gc_manager import PauseGC from pyomo.common.timing import TicTocTimer @@ -158,6 +159,14 @@ class LinearStandardFormCompiler(object): 'mix of <=, ==, and >=)', ), ) + CONFIG.declare( + 'set_sense', + ConfigValue( + default=OptimizationSense.minimize, + domain=InEnum(OptimizationSense), + description='If not None, map all objectives to the specified sense.', + ), + ) CONFIG.declare( 'show_section_timing', ConfigValue( @@ -315,6 +324,7 @@ def write(self, model): # # Process objective # + set_sense = self.config.set_sense objectives = [] for blk in component_map[Objective]: objectives.extend( @@ -336,7 +346,7 @@ def write(self, model): N = len(repn.linear) obj_data.append(np.fromiter(repn.linear.values(), float, N)) obj_offset.append(repn.constant) - if obj.sense == maximize: + if set_sense is not None and set_sense != obj.sense: obj_data[-1] *= -1 obj_offset[-1] *= -1 obj_index.append( From d546a248493a34623e61e46f002f3d85b8ef2b66 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:49:00 -0600 Subject: [PATCH 1147/3044] Add gurobi_direcct to the solver test suite --- pyomo/contrib/solver/tests/solvers/test_solvers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index a4f4a3bc389..f91de2287b7 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -21,6 +21,7 @@ from pyomo.contrib.solver.base import SolverBase from pyomo.contrib.solver.ipopt import Ipopt from pyomo.contrib.solver.gurobi import Gurobi +from pyomo.contrib.solver.gurobi_direct import GurobiDirect from pyomo.core.expr.numeric_expr import LinearExpression @@ -32,8 +33,8 @@ if not param_available: raise unittest.SkipTest('Parameterized is not available.') -all_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] -mip_solvers = [('gurobi', Gurobi)] +all_solvers = [('gurobi', Gurobi), ('gurobi_direct', GurobiDirect), ('ipopt', Ipopt)] +mip_solvers = [('gurobi', Gurobi), ('gurobi_direct', GurobiDirect)] nlp_solvers = [('ipopt', Ipopt)] qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt)] miqcqp_solvers = [('gurobi', Gurobi)] From a234fd42f44ef0acd4d789acfae05cbff8dcec70 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 4 Apr 2024 10:49:59 -0600 Subject: [PATCH 1148/3044] Taking the debugging util stuff out of this PR--I'll think about how to do it better later --- pyomo/contrib/cp/debugging.py | 29 ------------------------ pyomo/contrib/cp/tests/test_debugging.py | 25 -------------------- 2 files changed, 54 deletions(-) delete mode 100644 pyomo/contrib/cp/debugging.py delete mode 100644 pyomo/contrib/cp/tests/test_debugging.py diff --git a/pyomo/contrib/cp/debugging.py b/pyomo/contrib/cp/debugging.py deleted file mode 100644 index 34fb105a571..00000000000 --- a/pyomo/contrib/cp/debugging.py +++ /dev/null @@ -1,29 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.opt import WriterFactory - - -def write_conflict_set(m, filename): - """ - For debugging infeasible CPs: writes the conflict set found by CP optimizer - to a file with the specified filename. - - Args: - m: Pyomo CP model - filename: string filename - """ - - cpx_mod, var_map = WriterFactory('docplex_model').write( - m, symbolic_solver_labels=True - ) - conflict = cpx_mod.refine_conflict() - conflict.write(filename) diff --git a/pyomo/contrib/cp/tests/test_debugging.py b/pyomo/contrib/cp/tests/test_debugging.py deleted file mode 100644 index 8e24e545724..00000000000 --- a/pyomo/contrib/cp/tests/test_debugging.py +++ /dev/null @@ -1,25 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pyomo.common.unittest as unittest - -from pyomo.environ import ConcreteModel, Constraint, Var - - -class TestCPDebugging(unittest.TestCase): - def test_debug_infeasibility(self): - m = ConcreteModel() - m.x = Var(domain=Integers, bounds=(2, 5)) - m.y = Var(domain=Integers, bounds=(7, 12)) - m.c = Constraint(expr=m.y <= m.x) - - # ESJ TODO: I don't know how to do this without a baseline, which we - # really don't want... From 30490d24bcdf58763330f3216cbc30c93e559089 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 10:58:14 -0600 Subject: [PATCH 1149/3044] Correct import of ObjectiveSense enum --- pyomo/contrib/solver/gurobi_direct.py | 4 ++-- pyomo/repn/plugins/standard_form.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 36a783cba02..7b80651ccae 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -17,7 +17,7 @@ from pyomo.common.config import ConfigValue from pyomo.common.collections import ComponentMap, ComponentSet from pyomo.common.dependencies import attempt_import -from pyomo.common.enums import OptimizationSense +from pyomo.common.enums import ObjectiveSense from pyomo.common.shutdown import python_is_shutting_down from pyomo.common.tee import capture_output, TeeStream from pyomo.common.timing import HierarchicalTimer @@ -371,7 +371,7 @@ def _postsolve(self, timer: HierarchicalTimer, config, loader): try: results.objective_bound = grb_model.ObjBound except (gurobipy.GurobiError, AttributeError): - if grb_model.ModelSense == OptimizationSense.minimize: + if grb_model.ModelSense == ObjectiveSense.minimize: results.objective_bound = -math.inf else: results.objective_bound = math.inf diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index 566d0d8d932..a5aaece8531 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -20,7 +20,7 @@ document_kwargs_from_configdict, ) from pyomo.common.dependencies import scipy, numpy as np -from pyomo.common.enums import OptimizationSense +from pyomo.common.enums import ObjectiveSense from pyomo.common.gc_manager import PauseGC from pyomo.common.timing import TicTocTimer @@ -162,8 +162,8 @@ class LinearStandardFormCompiler(object): CONFIG.declare( 'set_sense', ConfigValue( - default=OptimizationSense.minimize, - domain=InEnum(OptimizationSense), + default=ObjectiveSense.minimize, + domain=InEnum(ObjectiveSense), description='If not None, map all objectives to the specified sense.', ), ) From b57adf1bc3aaa5c9c93e1988e31da94eb6ad81d7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 13:17:29 -0600 Subject: [PATCH 1150/3044] Add gurobi_direct to the docs --- doc/OnlineDocs/developer_reference/solvers.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/OnlineDocs/developer_reference/solvers.rst index 94fb684236f..9e3281246f4 100644 --- a/doc/OnlineDocs/developer_reference/solvers.rst +++ b/doc/OnlineDocs/developer_reference/solvers.rst @@ -45,9 +45,12 @@ with existing interfaces). * - Ipopt - ``ipopt`` - ``ipopt_v2`` - * - Gurobi + * - Gurobi (persistent) - ``gurobi`` - ``gurobi_v2`` + * - Gurobi (direct) + - ``gurobi_direct`` + - ``gurobi_direct_v2`` Using the new interfaces through the legacy interface ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 942df952e5c79e9ddffe53a2fe98c36fce96132b Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Thu, 4 Apr 2024 13:40:45 -0600 Subject: [PATCH 1151/3044] NFC: Typo fix in amplfunc_merge.py --- pyomo/solvers/amplfunc_merge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solvers/amplfunc_merge.py b/pyomo/solvers/amplfunc_merge.py index 9d127a94396..e49fd20e20f 100644 --- a/pyomo/solvers/amplfunc_merge.py +++ b/pyomo/solvers/amplfunc_merge.py @@ -28,5 +28,5 @@ def amplfunc_string_merge(amplfunc, pyomo_amplfunc): def amplfunc_merge(env): - """Merge AMPLFUNC and PYOMO_AMPLFuNC in an environment var dict""" + """Merge AMPLFUNC and PYOMO_AMPLFUNC in an environment var dict""" return amplfunc_string_merge(env.get("AMPLFUNC", ""), env.get("PYOMO_AMPLFUNC", "")) From 9790e080a7f3412841125e7c38ada2506e583ea2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 13:55:08 -0600 Subject: [PATCH 1152/3044] NFC: fix typo --- pyomo/repn/plugins/standard_form.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index a5aaece8531..110e95c3c6d 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -96,7 +96,7 @@ class LinearStandardFormInfo(object): objectives : List[_ObjectiveData] - The list of Pyomo objective objects correcponding to the active objectives + The list of Pyomo objective objects corresponding to the active objectives eliminated_vars: List[Tuple[_VarData, NumericExpression]] From 5dac102e6391311ff7a5615b870199dc3776842a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 4 Apr 2024 17:13:34 -0600 Subject: [PATCH 1153/3044] Adding test requested by PR review --- pyomo/common/tests/test_enums.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/common/tests/test_enums.py b/pyomo/common/tests/test_enums.py index 52ee1c5abb3..80d081505e9 100644 --- a/pyomo/common/tests/test_enums.py +++ b/pyomo/common/tests/test_enums.py @@ -59,6 +59,8 @@ def test_call(self): with self.assertRaisesRegex(ValueError, "'foo' is not a valid ProblemSense"): ProblemSense('foo') + with self.assertRaisesRegex(ValueError, "2 is not a valid ProblemSense"): + ProblemSense(2) def test_contains(self): self.assertIn(ProblemSense.unknown, ProblemSense) From 7b46ea5f2c6fecd38a260ccd56968493acb70911 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 5 Apr 2024 08:09:05 -0400 Subject: [PATCH 1154/3044] Make `symbolic_solver_labels` configurable --- pyomo/contrib/pyros/config.py | 15 +++++++++++++++ pyomo/contrib/pyros/tests/test_grcs.py | 2 ++ pyomo/contrib/pyros/util.py | 7 +++++-- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index bc2bfd591e6..8ab24939349 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -503,6 +503,21 @@ def pyros_config(): ), ), ) + CONFIG.declare( + 'symbolic_solver_labels', + ConfigValue( + default=False, + domain=bool, + description=( + """ + True to ensure the component names given to the + subordinate solvers for every subproblem reflect + the names of the corresponding Pyomo modeling components, + False otherwise. + """ + ), + ), + ) # ================================================ # === Required User Inputs diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 41223b30899..d49ed6b1002 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -3795,6 +3795,7 @@ def test_solve_master(self): config.declare( "progress_logger", ConfigValue(default=logging.getLogger(__name__)) ) + config.declare("symbolic_solver_labels", ConfigValue(default=False)) with time_code(master_data.timing, "main", is_main_timer=True): master_soln = solve_master(master_data, config) @@ -6171,6 +6172,7 @@ def test_log_config(self): " keepfiles=False\n" " tee=False\n" " load_solution=True\n" + " symbolic_solver_labels=False\n" " objective_focus=\n" " nominal_uncertain_param_vals=[0.5]\n" " decision_rule_order=0\n" diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 5d386240609..23cde45d0cf 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -1799,7 +1799,7 @@ def call_solver(model, solver, config, timing_obj, timer_name, err_msg): If ApplicationError is raised by the solver. In this case, `err_msg` is logged through ``config.progress_logger.exception()`` before - the excception is raised. + the exception is raised. """ tt_timer = TicTocTimer() @@ -1811,7 +1811,10 @@ def call_solver(model, solver, config, timing_obj, timer_name, err_msg): try: results = solver.solve( - model, tee=config.tee, load_solutions=False, symbolic_solver_labels=True + model, + tee=config.tee, + load_solutions=False, + symbolic_solver_labels=config.symbolic_solver_labels, ) except ApplicationError: # account for possible external subsolver errors From f766b33b2f7cea168a1a1b1ba49d49b8b7e4f546 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 5 Apr 2024 08:10:55 -0400 Subject: [PATCH 1155/3044] Make log example reflective of `symbolic_solver_labels` --- doc/OnlineDocs/contributed_packages/pyros.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index 9faa6d1365f..95049eded8a 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -926,6 +926,7 @@ Observe that the log contains the following information: keepfiles=False tee=False load_solution=True + symbolic_solver_labels=False objective_focus= nominal_uncertain_param_vals=[0.13248000000000001, 4.97, 4.97, 1800] decision_rule_order=1 From b0217c86ec3baf36126cdcb8371b1e4dbf7e3874 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 5 Apr 2024 15:18:56 -0600 Subject: [PATCH 1156/3044] Moving exit node dispatcher onto base class from pyomo.repn.util and fixing the monomial expression tests --- pyomo/contrib/cp/repn/docplex_writer.py | 20 ++++++++++++++----- pyomo/contrib/cp/tests/test_docplex_walker.py | 4 ++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 98d3e07e8ed..5dd355dc70d 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -88,6 +88,7 @@ from pyomo.core.expr.visitor import StreamBasedExpressionVisitor, identify_variables from pyomo.core.base import Set, RangeSet from pyomo.core.base.set import SetProduct +from pyomo.repn.util import ExitNodeDispatcher from pyomo.opt import WriterFactory, SolverFactory, TerminationCondition, SolverResults ### FIXME: Remove the following as soon as non-active components no @@ -707,9 +708,11 @@ def _get_bool_valued_expr(arg): def _handle_monomial_expr(visitor, node, arg1, arg2): # Monomial terms show up a lot. This handles some common # simplifications (necessary in part for the unit tests) + print(arg1) + print(arg2) if arg2[1].__class__ in EXPR.native_types: return _GENERAL, arg1[1] * arg2[1] - elif arg1[1] == 1: + elif arg1[1].__class__ in EXPR.native_types and arg1[1] == 1: return arg2 return (_GENERAL, cp.times(_get_int_valued_expr(arg1), _get_int_valued_expr(arg2))) @@ -997,8 +1000,7 @@ def _handle_synchronize_expression_node(visitor, node, *args): return _GENERAL, cp.synchronize(args[0][1], [arg[1] for arg in args[1:]]) -class LogicalToDoCplex(StreamBasedExpressionVisitor): - _operator_handles = { +_operator_handles = { EXPR.GetItemExpression: _handle_getitem, EXPR.Structural_GetItemExpression: _handle_getitem, EXPR.Numeric_GetItemExpression: _handle_getitem, @@ -1047,6 +1049,14 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): AlternativeExpression: _handle_alternative_expression_node, SynchronizeExpression: _handle_synchronize_expression_node, } + + +class LogicalToDoCplex(StreamBasedExpressionVisitor): + exit_node_dispatcher = ExitNodeDispatcher( + _operator_handles + ) + # NOTE: Because of indirection, we can encounter indexed Params and Vars in + # expressions _var_handles = { IntervalVarStartTime: _before_interval_var_start_time, IntervalVarEndTime: _before_interval_var_end_time, @@ -1065,7 +1075,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): IndexedBooleanVar: _before_indexed_boolean_var, _GeneralExpressionData: _before_named_expression, ScalarExpression: _before_named_expression, - IndexedParam: _before_indexed_param, # Because of indirection + IndexedParam: _before_indexed_param, ScalarParam: _before_param, _ParamData: _before_param, } @@ -1101,7 +1111,7 @@ def beforeChild(self, node, child, child_idx): return True, None def exitNode(self, node, data): - return self._operator_handles[node.__class__](self, node, *data) + return self.exit_node_dispatcher[node.__class__](self, node, *data) finalizeResult = None diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index d14e0bc2d6f..a7e537ee15b 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -261,11 +261,11 @@ def test_monomial_expressions(self): e = (1 / m.p) * m.x expr = visitor.walk_expression((e, e, 0)) - self.assertTrue(expr[1].equals(0.25 * x)) + self.assertTrue(expr[1].equals(cp.float_div(1, 4) * x)) e = (m.p ** (0.5)) * m.x expr = visitor.walk_expression((e, e, 0)) - self.assertTrue(expr[1].equals(2 * x)) + self.assertTrue(expr[1].equals(cp.power(4, 0.5) * x)) @unittest.skipIf(not docplex_available, "docplex is not available") From 5424f604281bcd1f09ae16d710e55509a9de368e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 5 Apr 2024 15:22:35 -0600 Subject: [PATCH 1157/3044] Removing some redundant operator handlers now that I have the subclass magic for automatic registration. --- pyomo/contrib/cp/repn/docplex_writer.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 5dd355dc70d..50f5f9e4294 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -1002,13 +1002,7 @@ def _handle_synchronize_expression_node(visitor, node, *args): _operator_handles = { EXPR.GetItemExpression: _handle_getitem, - EXPR.Structural_GetItemExpression: _handle_getitem, - EXPR.Numeric_GetItemExpression: _handle_getitem, - EXPR.Boolean_GetItemExpression: _handle_getitem, EXPR.GetAttrExpression: _handle_getattr, - EXPR.Structural_GetAttrExpression: _handle_getattr, - EXPR.Numeric_GetAttrExpression: _handle_getattr, - EXPR.Boolean_GetAttrExpression: _handle_getattr, EXPR.CallExpression: _handle_call, EXPR.NegationExpression: _handle_negation_node, EXPR.ProductExpression: _handle_product_node, @@ -1017,7 +1011,6 @@ def _handle_synchronize_expression_node(visitor, node, *args): EXPR.AbsExpression: _handle_abs_node, EXPR.MonomialTermExpression: _handle_monomial_expr, EXPR.SumExpression: _handle_sum_node, - EXPR.LinearExpression: _handle_sum_node, EXPR.MinExpression: _handle_min_node, EXPR.MaxExpression: _handle_max_node, EXPR.NotExpression: _handle_not_node, From e6f0a10f64c802eb7f2a2fcb24d8bb9bf41a1f2d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 5 Apr 2024 16:58:35 -0600 Subject: [PATCH 1158/3044] black --- pyomo/contrib/cp/repn/docplex_writer.py | 86 ++++++++++++------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 50f5f9e4294..0263b6b82a1 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -1001,53 +1001,51 @@ def _handle_synchronize_expression_node(visitor, node, *args): _operator_handles = { - EXPR.GetItemExpression: _handle_getitem, - EXPR.GetAttrExpression: _handle_getattr, - EXPR.CallExpression: _handle_call, - EXPR.NegationExpression: _handle_negation_node, - EXPR.ProductExpression: _handle_product_node, - EXPR.DivisionExpression: _handle_division_node, - EXPR.PowExpression: _handle_pow_node, - EXPR.AbsExpression: _handle_abs_node, - EXPR.MonomialTermExpression: _handle_monomial_expr, - EXPR.SumExpression: _handle_sum_node, - EXPR.MinExpression: _handle_min_node, - EXPR.MaxExpression: _handle_max_node, - EXPR.NotExpression: _handle_not_node, - EXPR.EquivalenceExpression: _handle_equivalence_node, - EXPR.ImplicationExpression: _handle_implication_node, - EXPR.AndExpression: _handle_and_node, - EXPR.OrExpression: _handle_or_node, - EXPR.XorExpression: _handle_xor_node, - EXPR.ExactlyExpression: _handle_exactly_node, - EXPR.AtMostExpression: _handle_at_most_node, - EXPR.AtLeastExpression: _handle_at_least_node, - EXPR.AllDifferentExpression: _handle_all_diff_node, - EXPR.CountIfExpression: _handle_count_if_node, - EXPR.EqualityExpression: _handle_equality_node, - EXPR.NotEqualExpression: _handle_not_equal_node, - EXPR.InequalityExpression: _handle_inequality_node, - EXPR.RangedExpression: _handle_ranged_inequality_node, - BeforeExpression: _handle_before_expression_node, - AtExpression: _handle_at_expression_node, - AlwaysIn: _handle_always_in_node, - _GeneralExpressionData: _handle_named_expression_node, - ScalarExpression: _handle_named_expression_node, - NoOverlapExpression: _handle_no_overlap_expression_node, - FirstInSequenceExpression: _handle_first_in_sequence_expression_node, - LastInSequenceExpression: _handle_last_in_sequence_expression_node, - BeforeInSequenceExpression: _handle_before_in_sequence_expression_node, - PredecessorToExpression: _handle_predecessor_to_expression_node, - SpanExpression: _handle_span_expression_node, - AlternativeExpression: _handle_alternative_expression_node, - SynchronizeExpression: _handle_synchronize_expression_node, - } + EXPR.GetItemExpression: _handle_getitem, + EXPR.GetAttrExpression: _handle_getattr, + EXPR.CallExpression: _handle_call, + EXPR.NegationExpression: _handle_negation_node, + EXPR.ProductExpression: _handle_product_node, + EXPR.DivisionExpression: _handle_division_node, + EXPR.PowExpression: _handle_pow_node, + EXPR.AbsExpression: _handle_abs_node, + EXPR.MonomialTermExpression: _handle_monomial_expr, + EXPR.SumExpression: _handle_sum_node, + EXPR.MinExpression: _handle_min_node, + EXPR.MaxExpression: _handle_max_node, + EXPR.NotExpression: _handle_not_node, + EXPR.EquivalenceExpression: _handle_equivalence_node, + EXPR.ImplicationExpression: _handle_implication_node, + EXPR.AndExpression: _handle_and_node, + EXPR.OrExpression: _handle_or_node, + EXPR.XorExpression: _handle_xor_node, + EXPR.ExactlyExpression: _handle_exactly_node, + EXPR.AtMostExpression: _handle_at_most_node, + EXPR.AtLeastExpression: _handle_at_least_node, + EXPR.AllDifferentExpression: _handle_all_diff_node, + EXPR.CountIfExpression: _handle_count_if_node, + EXPR.EqualityExpression: _handle_equality_node, + EXPR.NotEqualExpression: _handle_not_equal_node, + EXPR.InequalityExpression: _handle_inequality_node, + EXPR.RangedExpression: _handle_ranged_inequality_node, + BeforeExpression: _handle_before_expression_node, + AtExpression: _handle_at_expression_node, + AlwaysIn: _handle_always_in_node, + _GeneralExpressionData: _handle_named_expression_node, + ScalarExpression: _handle_named_expression_node, + NoOverlapExpression: _handle_no_overlap_expression_node, + FirstInSequenceExpression: _handle_first_in_sequence_expression_node, + LastInSequenceExpression: _handle_last_in_sequence_expression_node, + BeforeInSequenceExpression: _handle_before_in_sequence_expression_node, + PredecessorToExpression: _handle_predecessor_to_expression_node, + SpanExpression: _handle_span_expression_node, + AlternativeExpression: _handle_alternative_expression_node, + SynchronizeExpression: _handle_synchronize_expression_node, +} class LogicalToDoCplex(StreamBasedExpressionVisitor): - exit_node_dispatcher = ExitNodeDispatcher( - _operator_handles - ) + exit_node_dispatcher = ExitNodeDispatcher(_operator_handles) # NOTE: Because of indirection, we can encounter indexed Params and Vars in # expressions _var_handles = { From 558df7097a4ef5ee757847b8099bd15b8db8ac5d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 5 Apr 2024 17:17:34 -0600 Subject: [PATCH 1159/3044] Removing a lot of unused imports --- pyomo/contrib/cp/repn/docplex_writer.py | 3 +-- .../contrib/cp/scheduling_expr/step_function_expressions.py | 1 - pyomo/contrib/cp/tests/test_docplex_walker.py | 5 ----- pyomo/contrib/cp/tests/test_sequence_expressions.py | 5 ++--- pyomo/contrib/cp/tests/test_sequence_var.py | 2 +- pyomo/contrib/cp/transform/logical_to_disjunctive_program.py | 1 - pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py | 4 ---- 7 files changed, 4 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 0263b6b82a1..c48d5858b0e 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -33,7 +33,6 @@ from pyomo.contrib.cp.sequence_var import ( SequenceVar, ScalarSequenceVar, - IndexedSequenceVar, _SequenceVarData, ) from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( @@ -81,7 +80,7 @@ _GeneralBooleanVarData, IndexedBooleanVar, ) -from pyomo.core.base.expression import ScalarExpression, _GeneralExpressionData +from pyomo.core.base.expression import _GeneralExpressionData, ScalarExpression from pyomo.core.base.param import IndexedParam, ScalarParam, _ParamData from pyomo.core.base.var import ScalarVar, _GeneralVarData, IndexedVar import pyomo.core.expr as EXPR diff --git a/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py b/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py index b75306f72c9..129dff66b48 100644 --- a/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py +++ b/pyomo/contrib/cp/scheduling_expr/step_function_expressions.py @@ -15,7 +15,6 @@ IntervalVarStartTime, IntervalVarEndTime, ) -from pyomo.core.base.component import Component from pyomo.core.expr.base import ExpressionBase from pyomo.core.expr.logical_expr import BooleanExpression diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index a7e537ee15b..9aa91b5185f 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -17,13 +17,10 @@ no_overlap, first_in_sequence, last_in_sequence, - before_in_sequence, - predecessor_to, alternative, synchronize, ) from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( - AlwaysIn, Step, Pulse, ) @@ -56,8 +53,6 @@ Integers, inequality, Expression, - Reals, - Set, Param, ) diff --git a/pyomo/contrib/cp/tests/test_sequence_expressions.py b/pyomo/contrib/cp/tests/test_sequence_expressions.py index b676881e379..c7cf94f23d5 100644 --- a/pyomo/contrib/cp/tests/test_sequence_expressions.py +++ b/pyomo/contrib/cp/tests/test_sequence_expressions.py @@ -9,7 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from io import StringIO import pyomo.common.unittest as unittest from pyomo.contrib.cp.interval_var import IntervalVar from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( @@ -32,8 +31,8 @@ first_in_sequence, last_in_sequence, ) -from pyomo.contrib.cp.sequence_var import SequenceVar, IndexedSequenceVar -from pyomo.environ import ConcreteModel, Integers, LogicalConstraint, Set, value, Var +from pyomo.contrib.cp.sequence_var import SequenceVar +from pyomo.environ import ConcreteModel, LogicalConstraint, Set class TestSequenceVarExpressions(unittest.TestCase): diff --git a/pyomo/contrib/cp/tests/test_sequence_var.py b/pyomo/contrib/cp/tests/test_sequence_var.py index ebff465a376..c1e205c6326 100644 --- a/pyomo/contrib/cp/tests/test_sequence_var.py +++ b/pyomo/contrib/cp/tests/test_sequence_var.py @@ -13,7 +13,7 @@ import pyomo.common.unittest as unittest from pyomo.contrib.cp.interval_var import IntervalVar from pyomo.contrib.cp.sequence_var import SequenceVar, IndexedSequenceVar -from pyomo.environ import ConcreteModel, Integers, Set, value, Var +from pyomo.environ import ConcreteModel, Set class TestScalarSequenceVar(unittest.TestCase): diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py index e318e621e88..3c6960bc198 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_program.py @@ -12,7 +12,6 @@ from pyomo.contrib.cp.transform.logical_to_disjunctive_walker import ( LogicalToDisjunctiveVisitor, ) -from pyomo.common.collections import ComponentMap from pyomo.common.modeling import unique_component_name from pyomo.common.config import ConfigDict, ConfigValue diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index d5f13e91535..09bba403850 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -9,14 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import collections - from pyomo.common.collections import ComponentMap from pyomo.common.errors import MouseTrap from pyomo.core.expr.expr_common import ExpressionType from pyomo.core.expr.visitor import StreamBasedExpressionVisitor -from pyomo.core.expr.numeric_expr import NumericExpression -from pyomo.core.expr.relational_expr import RelationalExpression import pyomo.core.expr as EXPR from pyomo.core.base import ( Binary, From a4a78ed5c629ab3f4b953f591537b10450b0a977 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 5 Apr 2024 17:17:59 -0600 Subject: [PATCH 1160/3044] black --- pyomo/contrib/cp/tests/test_docplex_walker.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 9aa91b5185f..1173ae66eab 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -20,10 +20,7 @@ alternative, synchronize, ) -from pyomo.contrib.cp.scheduling_expr.step_function_expressions import ( - Step, - Pulse, -) +from pyomo.contrib.cp.scheduling_expr.step_function_expressions import Step, Pulse from pyomo.contrib.cp.repn.docplex_writer import docplex_available, LogicalToDoCplex from pyomo.core.base.range import NumericRange From 3e09bd2f5ca5bec451ba4e07d3886615173c1b9b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 5 Apr 2024 17:19:16 -0600 Subject: [PATCH 1161/3044] removing debugging --- pyomo/contrib/cp/repn/docplex_writer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index c48d5858b0e..c8a1143a7b5 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -707,8 +707,6 @@ def _get_bool_valued_expr(arg): def _handle_monomial_expr(visitor, node, arg1, arg2): # Monomial terms show up a lot. This handles some common # simplifications (necessary in part for the unit tests) - print(arg1) - print(arg2) if arg2[1].__class__ in EXPR.native_types: return _GENERAL, arg1[1] * arg2[1] elif arg1[1].__class__ in EXPR.native_types and arg1[1] == 1: From ad7011f12e352ca25212bd845893b3c2bb978318 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Mon, 8 Apr 2024 12:09:58 -0600 Subject: [PATCH 1162/3044] check _skip_trivial_costraints before the constraint body --- pyomo/solvers/plugins/solvers/gurobi_direct.py | 5 ++--- pyomo/solvers/plugins/solvers/xpress_direct.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/gurobi_direct.py b/pyomo/solvers/plugins/solvers/gurobi_direct.py index 1d88eced629..ed66a4e0e7b 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_direct.py +++ b/pyomo/solvers/plugins/solvers/gurobi_direct.py @@ -493,9 +493,8 @@ def _add_constraint(self, con): if not con.active: return None - if is_fixed(con.body): - if self._skip_trivial_constraints: - return None + if self._skip_trivial_constraints and is_fixed(con.body): + return None conname = self._symbol_map.getSymbol(con, self._labeler) diff --git a/pyomo/solvers/plugins/solvers/xpress_direct.py b/pyomo/solvers/plugins/solvers/xpress_direct.py index 75cf8f921df..c62f76d85ce 100644 --- a/pyomo/solvers/plugins/solvers/xpress_direct.py +++ b/pyomo/solvers/plugins/solvers/xpress_direct.py @@ -667,9 +667,8 @@ def _add_constraint(self, con): if not con.active: return None - if is_fixed(con.body): - if self._skip_trivial_constraints: - return None + if self._skip_trivial_constraints and is_fixed(con.body): + return None conname = self._symbol_map.getSymbol(con, self._labeler) From f3b9b12f5fe4048472aaa7b2314f3448ea83b73f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 8 Apr 2024 16:48:23 -0600 Subject: [PATCH 1163/3044] Adding initial implementation of linear walker that only walks with respect to specified variables, treating others as data --- pyomo/repn/linear_wrt.py | 77 +++++++++++++++++++++++++++++ pyomo/repn/tests/test_linear_wrt.py | 40 +++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 pyomo/repn/linear_wrt.py create mode 100644 pyomo/repn/tests/test_linear_wrt.py diff --git a/pyomo/repn/linear_wrt.py b/pyomo/repn/linear_wrt.py new file mode 100644 index 00000000000..0d86528056c --- /dev/null +++ b/pyomo/repn/linear_wrt.py @@ -0,0 +1,77 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.collections import ComponentSet +from pyomo.core import Var +from pyomo.core.expr.logical_expr import _flattened +from pyomo.core.expr.numeric_expr import ( + LinearExpression, + MonomialTermExpression, + SumExpression, +) +from pyomo.repn.linear import LinearBeforeChildDispatcher, LinearRepnVisitor +from pyomo.repn.util import ExprType + + +class MultiLevelLinearBeforeChildDispatcher(LinearBeforeChildDispatcher): + def __init__(self): + super().__init__() + self[Var] = self._before_var + self[MonomialTermExpression] = self._before_monomial + self[LinearExpression] = self._before_linear + self[SumExpression] = self._before_general_expression + + @staticmethod + def _before_linear(visitor, child): + return True, None + + @staticmethod + def _before_monomial(visitor, child): + return True, None + + @staticmethod + def _before_general_expression(visitor, child): + return True, None + + @staticmethod + def _before_var(visitor, child): + if child in visitor.wrt: + # This is a normal situation + print("NORMAL: %s" % child) + _id = id(child) + if _id not in visitor.var_map: + if child.fixed: + return False, ( + ExprType.CONSTANT, + visitor.check_constant(child.value, child), + ) + MultiLevelLinearBeforeChildDispatcher._record_var(visitor, child) + ans = visitor.Result() + ans.linear[_id] = 1 + return False, (ExprType.LINEAR, ans) + else: + print("DATA: %s" % child) + # We aren't treating this Var as a Var for the purposes of this walker + return False, (ExprType.CONSTANT, child) + + +_before_child_dispatcher = MultiLevelLinearBeforeChildDispatcher() + + +class MultilevelLinearRepnVisitor(LinearRepnVisitor): + def __init__(self, subexpression_cache, var_map, var_order, sorter, wrt): + super().__init__(subexpression_cache, var_map, var_order, sorter) + self.wrt = ComponentSet(_flattened(wrt)) + + def beforeChild(self, node, child, child_idx): + print("before child %s" % child) + print(child.__class__) + return _before_child_dispatcher[child.__class__](self, child) diff --git a/pyomo/repn/tests/test_linear_wrt.py b/pyomo/repn/tests/test_linear_wrt.py new file mode 100644 index 00000000000..29c8f69ad03 --- /dev/null +++ b/pyomo/repn/tests/test_linear_wrt.py @@ -0,0 +1,40 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +from pyomo.environ import Binary, ConcreteModel, Var +from pyomo.repn.linear_wrt import MultilevelLinearRepnVisitor +from pyomo.repn.tests.test_linear import VisitorConfig + + +class TestMultilevelLinearRepnVisitor(unittest.TestCase): + def make_model(self): + m = ConcreteModel() + m.x = Var(bounds=(0, 45)) + m.y = Var(domain=Binary) + + return m + + def test_walk_sum(self): + m = self.make_model() + e = m.x + m.y + cfg = VisitorConfig() + print("constructing") + visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + + repn = visitor.walk_expression(e) + + self.assertIsNone(repn.nonlinear) + self.assertEqual(len(repn.linear), 1) + self.assertIn(id(m.x), repn.linear) + self.assertEqual(repn.linear[id(m.x)], 1) + self.assertIs(repn.constant, m.y) + self.assertEqual(repn.multiplier, 1) From 194ddd2198980c531f058bac7d8b2fa9e4a6f37d Mon Sep 17 00:00:00 2001 From: whart222 Date: Mon, 8 Apr 2024 16:57:45 -0600 Subject: [PATCH 1164/3044] Working through tests --- pyomo/contrib/alternative_solutions/README.md | 8 + .../alternative_solutions/aos_utils.py | 277 +++++++++------ pyomo/contrib/alternative_solutions/balas.py | 183 +++++----- .../contrib/alternative_solutions/lp_enum.py | 327 +++++++++-------- pyomo/contrib/alternative_solutions/obbt.py | 183 +++++----- .../alternative_solutions/shifted_lp.py | 188 +++++----- .../contrib/alternative_solutions/solnpool.py | 94 +++-- .../contrib/alternative_solutions/solution.py | 109 ++++-- .../tests/run_lp_enum.py | 8 +- .../tests/test_aos_utils.py | 227 ++++++------ .../alternative_solutions/tests/test_balas.py | 22 +- .../alternative_solutions/tests/test_cases.py | 331 ++++++++++-------- .../alternative_solutions/tests/test_obbt.py | 73 ++-- .../tests/test_shifted_lp.py | 28 +- .../tests/test_solnpool.py | 109 ++++-- .../tests/test_solution.py | 44 +-- 16 files changed, 1275 insertions(+), 936 deletions(-) create mode 100644 pyomo/contrib/alternative_solutions/README.md diff --git a/pyomo/contrib/alternative_solutions/README.md b/pyomo/contrib/alternative_solutions/README.md new file mode 100644 index 00000000000..b6e387aceee --- /dev/null +++ b/pyomo/contrib/alternative_solutions/README.md @@ -0,0 +1,8 @@ +# alternative_solutions + +pyomo.contrib.alternative_solutions is a collection of functions that +that generate a set of alternative (near-)optimal solutions +(AOS). These functions rely on a pyomo solver to search for solutions, +and they iteratively adapt the search process to find a variety of +alternative solutions. + diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index 6867c570669..34572475d25 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -17,115 +17,144 @@ from pyomo.common.collections import ComponentSet import pyomo.util.vars_from_expressions as vfe -def _get_active_objective(model): - ''' - Finds and returns the active objective function for a model. Currently + +def get_active_objective(model): + """ + Finds and returns the active objective function for a model. Currently assume that there is exactly one active objective. - ''' - + """ + active_objs = [] for o in model.component_data_objects(pe.Objective, active=True): objs = o.values() if o.is_indexed() else (o,) for obj in objs: active_objs.append(obj) - assert len(active_objs) == 1, \ - "Model has {} active objective functions, exactly one is required.".\ - format(len(active_objs)) - + assert ( + len(active_objs) == 1 + ), "Model has {} active objective functions, exactly one is required.".format( + len(active_objs) + ) + return active_objs[0] -def _add_aos_block(model, name='_aos_block'): - '''Adds an alternative optimal solution block with a unique name.''' + +def _add_aos_block(model, name="_aos_block"): + """Adds an alternative optimal solution block with a unique name.""" aos_block = pe.Block() model.add_component(unique_component_name(model, name), aos_block) return aos_block -def _add_objective_constraint(aos_block, objective, objective_value, - rel_opt_gap, abs_opt_gap): - ''' - Adds a relative and/or absolute objective function constraint to the + +def _add_objective_constraint( + aos_block, objective, objective_value, rel_opt_gap, abs_opt_gap +): + """ + Adds a relative and/or absolute objective function constraint to the specified block. - ''' - - assert rel_opt_gap is None or rel_opt_gap >= 0.0, \ - 'rel_opt_gap must be None of >= 0.0' - assert abs_opt_gap is None or abs_opt_gap >= 0.0, \ - 'abs_opt_gap must be None of >= 0.0' - + """ + + assert ( + rel_opt_gap is None or rel_opt_gap >= 0.0 + ), "rel_opt_gap must be None of >= 0.0" + assert ( + abs_opt_gap is None or abs_opt_gap >= 0.0 + ), "abs_opt_gap must be None of >= 0.0" + objective_constraints = [] - + objective_is_min = objective.is_minimizing() objective_expr = objective.expr objective_sense = -1 if objective_is_min: objective_sense = 1 - + if rel_opt_gap is not None: - objective_cutoff = objective_value + objective_sense * rel_opt_gap *\ - abs(objective_value) + objective_cutoff = objective_value + objective_sense * rel_opt_gap * abs( + objective_value + ) if objective_is_min: - aos_block.optimality_tol_rel = \ - pe.Constraint(expr=objective_expr <= \ - objective_cutoff) + aos_block.optimality_tol_rel = pe.Constraint( + expr=objective_expr <= objective_cutoff + ) else: - aos_block.optimality_tol_rel = \ - pe.Constraint(expr=objective_expr >= \ - objective_cutoff) + aos_block.optimality_tol_rel = pe.Constraint( + expr=objective_expr >= objective_cutoff + ) objective_constraints.append(aos_block.optimality_tol_rel) - + if abs_opt_gap is not None: - objective_cutoff = objective_value + objective_sense \ - * abs_opt_gap + objective_cutoff = objective_value + objective_sense * abs_opt_gap if objective_is_min: - aos_block.optimality_tol_abs = \ - pe.Constraint(expr=objective_expr <= \ - objective_cutoff) + aos_block.optimality_tol_abs = pe.Constraint( + expr=objective_expr <= objective_cutoff + ) else: - aos_block.optimality_tol_abs = \ - pe.Constraint(expr=objective_expr >= \ - objective_cutoff) + aos_block.optimality_tol_abs = pe.Constraint( + expr=objective_expr >= objective_cutoff + ) objective_constraints.append(aos_block.optimality_tol_abs) - + return objective_constraints + def _get_random_direction(num_dimensions): - ''' - Get a unit vector of dimension num_dimensions by sampling from and + """ + Get a unit vector of dimension num_dimensions by sampling from and normalizing a standard multivariate Gaussian distribution. - ''' - + """ + iterations = 1000 min_norm = 1e-4 idx = 0 while idx < iterations: samples = normal(size=num_dimensions) samples_norm = norm(samples) - if samples_norm > 1e-4: + if samples_norm > min_norm: return samples / samples_norm idx += 1 - raise Exception(("Generated {} sequential Gaussian draws with a norm of " - "less than {}.".format(iterations, min_norm))) + raise Exception( + ( + "Generated {} sequential Gaussian draws with a norm of " + "less than {}.".format(iterations, min_norm) + ) + ) + -def _filter_model_variables(variable_set, var_generator, - include_continuous=True, include_binary=True, - include_integer=True, include_fixed=False): - '''Filters variables from a variable generator and adds them to a set.''' +def _filter_model_variables( + variable_set, + var_generator, + include_continuous=True, + include_binary=True, + include_integer=True, + include_fixed=False, +): + """ + Filters variables from a variable generator and adds them to a set. + """ for var in var_generator: - if var in variable_set or var.is_fixed() and not include_fixed: + if var in variable_set or (var.is_fixed() and not include_fixed): continue - if (var.is_continuous() and include_continuous or - var.is_binary() and include_binary or - var.is_integer() and include_integer): + if ( + (var.is_continuous() and include_continuous) + or (var.is_binary() and include_binary) + or (var.is_integer() and include_integer) + ): variable_set.add(var) -def get_model_variables(model, components='all', include_continuous=True, - include_binary=True, include_integer=True, - include_fixed=False): - ''' - Gathers and returns all variables or a subset of variables from a Pyomo + +def get_model_variables( + model, + components="all", + include_continuous=True, + include_binary=True, + include_integer=True, + include_fixed=False, +): + """ + Gathers and returns all variables or a subset of variables from a Pyomo model. Parameters @@ -133,13 +162,13 @@ def get_model_variables(model, components='all', include_continuous=True, model : ConcreteModel A concrete Pyomo model. components: 'all' or a collection Pyomo components - The components from which variables should be collected. 'all' - indicates that all variables will be included. Alternatively, a + The components from which variables should be collected. 'all' + indicates that all variables will be included. Alternatively, a collection of Pyomo Blocks, Constraints, or Variables (indexed or - non-indexed) from which variables will be gathered can be provided. - If a Block is provided, all variables associated with constraints - in that that block and its sub-blocks will be returned. To exclude - sub-blocks, a tuple element with the format (Block, False) can be + non-indexed) from which variables will be gathered can be provided. + If a Block is provided, all variables associated with constraints + in that that block and its sub-blocks will be returned. To exclude + sub-blocks, a tuple element with the format (Block, False) can be used. include_continuous : boolean Boolean indicating that continuous variables should be included. @@ -149,59 +178,93 @@ def get_model_variables(model, components='all', include_continuous=True, Boolean indicating that integer variables should be included. include_fixed : boolean Boolean indicating that fixed variables should be included. - + Returns ------- variable_set A Pyomo ComponentSet containing _GeneralVarData variables. - ''' - + """ + component_list = (pe.Objective, pe.Constraint) variable_set = ComponentSet() - if components == 'all': - var_generator = vfe.get_vars_from_components(model, component_list, - include_fixed=\ - include_fixed) - _filter_model_variables(variable_set, var_generator, - include_continuous, include_binary, - include_integer, include_fixed) - else: + if components == "all": + var_generator = vfe.get_vars_from_components( + model, component_list, include_fixed=include_fixed + ) + _filter_model_variables( + variable_set, + var_generator, + include_continuous, + include_binary, + include_integer, + include_fixed, + ) + else: for comp in components: - if (hasattr(comp, 'ctype') and comp.ctype == pe.Block): + if hasattr(comp, "ctype") and comp.ctype == pe.Block: blocks = comp.values() if comp.is_indexed() else (comp,) for item in blocks: - variables = vfe.get_vars_from_components(item, - component_list, include_fixed=include_fixed) - _filter_model_variables(variable_set, variables, - include_continuous, include_binary, include_integer, - include_fixed) - elif (isinstance(comp, tuple) and hasattr(comp[0], 'ctype') \ - and comp[0].ctype == pe.Block): + variables = vfe.get_vars_from_components( + item, component_list, include_fixed=include_fixed + ) + _filter_model_variables( + variable_set, + variables, + include_continuous, + include_binary, + include_integer, + include_fixed, + ) + elif ( + isinstance(comp, tuple) + and hasattr(comp[0], "ctype") + and comp[0].ctype == pe.Block + ): block = comp[0] descend_into = pe.Block if comp[1] else False blocks = block.values() if block.is_indexed() else (block,) for item in blocks: - variables = vfe.get_vars_from_components(item, - component_list, include_fixed=include_fixed, - descend_into=descend_into) - _filter_model_variables(variable_set, variables, - include_continuous, include_binary, include_integer, - include_fixed) - elif hasattr(comp, 'ctype') and comp.ctype in component_list: + variables = vfe.get_vars_from_components( + item, + component_list, + include_fixed=include_fixed, + descend_into=descend_into, + ) + _filter_model_variables( + variable_set, + variables, + include_continuous, + include_binary, + include_integer, + include_fixed, + ) + elif hasattr(comp, "ctype") and comp.ctype in component_list: constraints = comp.values() if comp.is_indexed() else (comp,) for item in constraints: - variables = pe.expr.identify_variables(item.expr, - include_fixed=include_fixed) - _filter_model_variables(variable_set, variables, - include_continuous, include_binary, include_integer, - include_fixed) - elif (hasattr(comp, 'ctype') and comp.ctype == pe.Var): + variables = pe.expr.identify_variables( + item.expr, include_fixed=include_fixed + ) + _filter_model_variables( + variable_set, + variables, + include_continuous, + include_binary, + include_integer, + include_fixed, + ) + elif hasattr(comp, "ctype") and comp.ctype == pe.Var: variables = comp.values() if comp.is_indexed() else (comp,) - _filter_model_variables(variable_set, variables, - include_continuous, include_binary, include_integer, - include_fixed) + _filter_model_variables( + variable_set, + variables, + include_continuous, + include_binary, + include_integer, + include_fixed, + ) else: - print(('No variables added for unrecognized component {}.'). - format(comp)) - - return variable_set \ No newline at end of file + print( + ("No variables added for unrecognized component {}.").format(comp) + ) + + return variable_set diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index 2498a8e0651..10e32feb114 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -13,12 +13,20 @@ from pyomo.common.collections import ComponentSet from pyomo.contrib.alternative_solutions import aos_utils, solution -def enumerate_binary_solutions(model, num_solutions=10, variables='all', - rel_opt_gap=None, abs_opt_gap=None, - search_mode='optimal', solver='gurobi', - solver_options={}, tee=False): - ''' - Finds alternative optimal solutions for a binary problem using no-good + +def enumerate_binary_solutions( + model, + num_solutions=10, + variables="all", + rel_opt_gap=None, + abs_opt_gap=None, + search_mode="optimal", + solver="gurobi", + solver_options={}, + tee=False, +): + """ + Finds alternative optimal solutions for a binary problem using no-good cuts. Parameters @@ -28,22 +36,22 @@ def enumerate_binary_solutions(model, num_solutions=10, variables='all', num_solutions : int The maximum number of solutions to generate. variables: 'all' or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. 'all' indicates + The variables for which bounds will be generated. 'all' indicates that all variables will be included. Alternatively, a collection of _GenereralVarData variables can be provided. rel_opt_gap : float or None - The relative optimality gap for the original objective for which - variable bounds will be found. None indicates that a relative gap + The relative optimality gap for the original objective for which + variable bounds will be found. None indicates that a relative gap constraint will not be added to the model. abs_opt_gap : float or None - The absolute optimality gap for the original objective for which - variable bounds will be found. None indicates that an absolute gap + The absolute optimality gap for the original objective for which + variable bounds will be found. None indicates that an absolute gap constraint will not be added to the model. search_mode : 'optimal', 'random', or 'hamming' Indicates the mode that is used to generate alternative solutions. The optimal mode finds the next best solution. The random mode finds an alternative solution in the direction of a random ray. The - hamming mode iteratively finds solution that maximize the hamming + hamming mode iteratively finds solution that maximize the hamming distance from previously discovered solutions. solver : string The solver to be used. @@ -51,23 +59,26 @@ def enumerate_binary_solutions(model, num_solutions=10, variables='all', Solver option-value pairs to be passed to the solver. tee : boolean Boolean indicating that the solver output should be displayed. - + Returns ------- solutions A list of Solution objects. [Solution] - ''' - - print('STARTING NO-GOOD CUT ANALYSIS') - - assert search_mode in ['optimal', 'random', 'hamming'], \ - 'search mode must be "optimal", "random", or "hamming".' - - if variables == 'all': - binary_variables = aos_utils.get_model_variables(model, 'all', - include_continuous=False, - include_integer=False) + """ + + print("STARTING NO-GOOD CUT ANALYSIS") + + assert search_mode in [ + "optimal", + "random", + "hamming", + ], 'search mode must be "optimal", "random", or "hamming".' + + if variables == "all": + binary_variables = aos_utils.get_model_variables( + model, "all", include_continuous=False, include_integer=False + ) else: binary_variables = ComponentSet() non_binary_variables = [] @@ -77,20 +88,23 @@ def enumerate_binary_solutions(model, num_solutions=10, variables='all', else: non_binary_variables.append(var.name) if len(non_binary_variables) > 0: - print(('Warning: The following non-binary variables were included' - 'in the variable list and will be ignored:')) + print( + ( + "Warning: The following non-binary variables were included" + "in the variable list and will be ignored:" + ) + ) print(", ".join(non_binary_variables)) - all_variables = aos_utils.get_model_variables(model, 'all', - include_fixed=True) - - orig_objective = aos_utils._get_active_objective(model) - + all_variables = aos_utils.get_model_variables(model, "all", include_fixed=True) + + orig_objective = aos_utils.get_active_objective(model) + opt = pe.SolverFactory(solver) for parameter, value in solver_options.items(): opt.options[parameter] = value - + use_appsi = False - if 'appsi' in solver: + if "appsi" in solver: use_appsi = True opt.update_config.update_constraints = False opt.update_config.check_for_new_or_removed_constraints = True @@ -100,41 +114,44 @@ def enumerate_binary_solutions(model, num_solutions=10, variables='all', opt.update_config.update_params = False opt.update_config.update_named_expressions = False opt.update_config.treat_fixed_vars_as_params = False - - if search_mode == 'hamming': + + if search_mode == "hamming": opt.update_config.check_for_new_objective = True opt.update_config.update_objective = True - elif search_mode == 'random': + elif search_mode == "random": opt.update_config.check_for_new_objective = True - opt.update_config.update_objective = False + opt.update_config.update_objective = False else: opt.update_config.check_for_new_objective = False - opt.update_config.update_objective = False - - print('Peforming initial solve of model.') + opt.update_config.update_objective = False + + print("Peforming initial solve of model.") results = opt.solve(model, tee=tee) status = results.solver.status condition = results.solver.termination_condition if condition != pe.TerminationCondition.optimal: - raise Exception(('No-good cut analysis cannot be applied, ' - 'SolverStatus = {}, ' - 'TerminationCondition = {}').format(status.value, - condition.value)) - + raise Exception( + ( + "No-good cut analysis cannot be applied, " + "SolverStatus = {}, " + "TerminationCondition = {}" + ).format(status.value, condition.value) + ) + orig_objective_value = pe.value(orig_objective) - print('Found optimal solution, value = {}.'.format(orig_objective_value)) + print("Found optimal solution, value = {}.".format(orig_objective_value)) solutions = [solution.Solution(model, all_variables)] - - aos_block = aos_utils._add_aos_block(model, name='_balas') - print('Added block {} to the model.'.format(aos_block)) + + aos_block = aos_utils._add_aos_block(model, name="_balas") + print("Added block {} to the model.".format(aos_block)) aos_block.no_good_cuts = pe.ConstraintList() - aos_utils._add_objective_constraint(aos_block, orig_objective, - orig_objective_value, rel_opt_gap, - abs_opt_gap) - - if search_mode in ['random', 'hamming']: + aos_utils._add_objective_constraint( + aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap + ) + + if search_mode in ["random", "hamming"]: orig_objective.deactivate() - + solution_number = 2 while solution_number <= num_solutions: @@ -144,53 +161,57 @@ def enumerate_binary_solutions(model, num_solutions=10, variables='all', expr += 1 - var else: expr += var - - aos_block.no_good_cuts.add(expr= expr >= 1) - if search_mode == 'hamming': - if hasattr(aos_block, 'hamming_objective'): + aos_block.no_good_cuts.add(expr=expr >= 1) + + if search_mode == "hamming": + if hasattr(aos_block, "hamming_objective"): aos_block.hamming_objective.expr += expr if use_appsi and opt.update_config.check_for_new_objective: opt.update_config.check_for_new_objective = False else: - aos_block.hamming_objective = pe.Objective(expr=expr, - sense=pe.maximize) - - if search_mode == 'random': - if hasattr(aos_block, 'random_objective'): - aos_block.del_component('random_objective') + aos_block.hamming_objective = pe.Objective(expr=expr, sense=pe.maximize) + + if search_mode == "random": + if hasattr(aos_block, "random_objective"): + aos_block.del_component("random_objective") vector = aos_utils._get_random_direction(len(binary_variables)) idx = 0 expr = 0 for var in binary_variables: expr += vector[idx] * var idx += 1 - aos_block.random_objective = \ - pe.Objective(expr=expr, sense=pe.maximize) - + aos_block.random_objective = pe.Objective(expr=expr, sense=pe.maximize) + results = opt.solve(model, tee=tee) status = results.solver.status condition = results.solver.termination_condition if condition == pe.TerminationCondition.optimal: orig_obj_val = pe.value(orig_objective) - print("Iteration {}: objective = {}".format(solution_number, - orig_obj_val)) + print("Iteration {}: objective = {}".format(solution_number, orig_obj_val)) solutions.append(solution.Solution(model, all_variables)) solution_number += 1 - elif (condition == pe.TerminationCondition.infeasibleOrUnbounded or - condition == pe.TerminationCondition.infeasible): - print("Iteration {}: Infeasible, no additional binary solutions.".\ - format(solution_number)) + elif ( + condition == pe.TerminationCondition.infeasibleOrUnbounded + or condition == pe.TerminationCondition.infeasible + ): + print( + "Iteration {}: Infeasible, no additional binary solutions.".format( + solution_number + ) + ) break else: - print(("Iteration {}: Unexpected condition, SolverStatus = {}, " - "TerminationCondition = {}").format(solution_number, - status.value, - condition.value)) + print( + ( + "Iteration {}: Unexpected condition, SolverStatus = {}, " + "TerminationCondition = {}" + ).format(solution_number, status.value, condition.value) + ) break aos_block.deactivate() orig_objective.activate() - print('COMPLETED NO-GOOD CUT ANALYSIS') - - return solutions \ No newline at end of file + print("COMPLETED NO-GOOD CUT ANALYSIS") + + return solutions diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index 21418fad1ce..ba4e2c0be52 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -10,14 +10,24 @@ # ___________________________________________________________________________ import pyomo.environ as pe -from pyomo.contrib.alternative_solutions import aos_utils, shifted_lp, \ - solution, solnpool - -def enumerate_linear_solutions_soln_pool(model, num_solutions=10, - variables='all', rel_opt_gap=None, - abs_opt_gap=None, - solver_options={}, tee=False): - ''' +from pyomo.contrib.alternative_solutions import ( + aos_utils, + shifted_lp, + solution, + solnpool, +) + + +def enumerate_linear_solutions_soln_pool( + model, + num_solutions=10, + variables="all", + rel_opt_gap=None, + abs_opt_gap=None, + solver_options={}, + tee=False, +): + """ Finds alternative optimal solutions a (mixed-integer) linear program using Gurobi's solution pool feature. @@ -28,59 +38,62 @@ def enumerate_linear_solutions_soln_pool(model, num_solutions=10, num_solutions : int The maximum number of solutions to generate. variables: 'all' or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. 'all' indicates + The variables for which bounds will be generated. 'all' indicates that all variables will be included. Alternatively, a collection of _GenereralVarData variables can be provided. rel_opt_gap : float or None - The relative optimality gap for the original objective for which - variable bounds will be found. None indicates that a relative gap + The relative optimality gap for the original objective for which + variable bounds will be found. None indicates that a relative gap constraint will not be added to the model. abs_opt_gap : float or None - The absolute optimality gap for the original objective for which - variable bounds will be found. None indicates that an absolute gap + The absolute optimality gap for the original objective for which + variable bounds will be found. None indicates that an absolute gap constraint will not be added to the model. solver_options : dict Solver option-value pairs to be passed to the solver. tee : boolean Boolean indicating that the solver output should be displayed. - + Returns ------- solutions A list of Solution objects. [Solution] - ''' - opt = pe.SolverFactory('gurobi') - print('STARTING LP ENUMERATION ANALYSIS USING GUROBI SOLUTION POOL') - + """ + opt = pe.SolverFactory("gurobi") + print("STARTING LP ENUMERATION ANALYSIS USING GUROBI SOLUTION POOL") + # For now keeping things simple # TODO: Relax this - assert variables == 'all' + assert variables == "all" - opt = pe.SolverFactory('gurobi') + opt = pe.SolverFactory("gurobi") for parameter, value in solver_options.items(): opt.options[parameter] = value - - print('Peforming initial solve of model.') + + print("Peforming initial solve of model.") results = opt.solve(model, tee=tee) status = results.solver.status condition = results.solver.termination_condition if condition != pe.TerminationCondition.optimal: - raise Exception(('Model could not be solve. LP enumeration analysis ' - 'cannot be applied, SolverStatus = {}, ' - 'TerminationCondition = {}').format(status.value, - condition.value)) - - orig_objective = aos_utils._get_active_objective(model) + raise Exception( + ( + "Model could not be solve. LP enumeration analysis " + "cannot be applied, SolverStatus = {}, " + "TerminationCondition = {}" + ).format(status.value, condition.value) + ) + + orig_objective = aos_utils.get_active_objective(model) orig_objective_value = pe.value(orig_objective) - print('Found optimal solution, value = {}.'.format(orig_objective_value)) - - aos_block = aos_utils._add_aos_block(model, name='_lp_enum') - print('Added block {} to the model.'.format(aos_block)) - aos_utils._add_objective_constraint(aos_block, orig_objective, - orig_objective_value, rel_opt_gap, - abs_opt_gap) - + print("Found optimal solution, value = {}.".format(orig_objective_value)) + + aos_block = aos_utils._add_aos_block(model, name="_lp_enum") + print("Added block {} to the model.".format(aos_block)) + aos_utils._add_objective_constraint( + aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap + ) + cannonical_block = shifted_lp.get_shifted_linear_model(model) cb = cannonical_block @@ -88,22 +101,31 @@ def enumerate_linear_solutions_soln_pool(model, num_solutions=10, cb.basic_lower = pe.Var(cb.var_lower_index, domain=pe.Binary) cb.basic_upper = pe.Var(cb.var_upper_index, domain=pe.Binary) cb.basic_slack = pe.Var(cb.slack_index, domain=pe.Binary) - + # w upper bounds constraints def bound_lower_rule(m, var_index): - return m.var_lower[var_index] <= m.var_lower[var_index].ub \ - * m.basic_lower[var_index] - cb.bound_lower = pe.Constraint(cb.var_lower_index,rule=bound_lower_rule) - + return ( + m.var_lower[var_index] + <= m.var_lower[var_index].ub * m.basic_lower[var_index] + ) + + cb.bound_lower = pe.Constraint(cb.var_lower_index, rule=bound_lower_rule) + def bound_upper_rule(m, var_index): - return m.var_upper[var_index] <= m.var_upper[var_index].ub \ - * m.basic_upper[var_index] - cb.bound_upper = pe.Constraint(cb.var_upper_index,rule=bound_upper_rule) - + return ( + m.var_upper[var_index] + <= m.var_upper[var_index].ub * m.basic_upper[var_index] + ) + + cb.bound_upper = pe.Constraint(cb.var_upper_index, rule=bound_upper_rule) + def bound_slack_rule(m, var_index): - return m.slack_vars[var_index] <= m.slack_vars[var_index].ub \ - * m.basic_slack[var_index] - cb.bound_slack = pe.Constraint(cb.slack_index,rule=bound_slack_rule) + return ( + m.slack_vars[var_index] + <= m.slack_vars[var_index].ub * m.basic_slack[var_index] + ) + + cb.bound_slack = pe.Constraint(cb.slack_index, rule=bound_slack_rule) cb.pprint() results = solnpool.gurobi_generate_solutions(cb, num_solutions) @@ -114,7 +136,7 @@ def bound_slack_rule(m, var_index): # if condition == pe.TerminationCondition.optimal: # for var, index in cb.var_map.items(): # var.set_value(var.lb + cb.var_lower[index].value) - # sol = solution.Solution(model, all_variables, + # sol = solution.Solution(model, all_variables, # objective=orig_objective) # solutions.append(sol) # orig_objective_value = sol.objective[1] @@ -127,19 +149,19 @@ def bound_slack_rule(m, var_index): # cb.del_component('link_in_out') # if hasattr(cb, 'basic_last_lower'): - # cb.del_component('basic_last_lower') + # cb.del_component('basic_last_lower') # if hasattr(cb, 'basic_last_upper'): # cb.del_component('basic_last_upper') # if hasattr(cb, 'basic_last_slack'): # cb.del_component('basic_last_slack') - + # cb.link_in_out = pe.Constraint(pe.Any) # cb.basic_last_lower = pe.Var(pe.Any, domain=pe.Binary, dense=False) # cb.basic_last_upper = pe.Var(pe.Any, domain=pe.Binary, dense=False) # cb.basic_last_slack = pe.Var(pe.Any, domain=pe.Binary, dense=False) - # basic_last_list = [cb.basic_last_lower, cb.basic_last_upper, + # basic_last_list = [cb.basic_last_lower, cb.basic_last_upper, # cb.basic_last_slack] - + # num_non_zero = 0 # force_out_expr = -1 # non_zero_basic_expr = 1 @@ -159,14 +181,22 @@ def bound_slack_rule(m, var_index): # aos_block.deactivate() # print('COMPLETED LP ENUMERATION ANALYSIS') - + # return solutions -def enumerate_linear_solutions(model, num_solutions=10, variables='all', - rel_opt_gap=None, abs_opt_gap=None, - search_mode='optimal', solver='gurobi', - solver_options={}, tee=False): - ''' + +def enumerate_linear_solutions( + model, + num_solutions=10, + variables="all", + rel_opt_gap=None, + abs_opt_gap=None, + search_mode="optimal", + solver="gurobi", + solver_options={}, + tee=False, +): + """ Finds alternative optimal solutions a (mixed-integer) linear program. Parameters @@ -176,22 +206,22 @@ def enumerate_linear_solutions(model, num_solutions=10, variables='all', num_solutions : int The maximum number of solutions to generate. variables: 'all' or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. 'all' indicates + The variables for which bounds will be generated. 'all' indicates that all variables will be included. Alternatively, a collection of _GenereralVarData variables can be provided. rel_opt_gap : float or None - The relative optimality gap for the original objective for which - variable bounds will be found. None indicates that a relative gap + The relative optimality gap for the original objective for which + variable bounds will be found. None indicates that a relative gap constraint will not be added to the model. abs_opt_gap : float or None - The absolute optimality gap for the original objective for which - variable bounds will be found. None indicates that an absolute gap + The absolute optimality gap for the original objective for which + variable bounds will be found. None indicates that an absolute gap constraint will not be added to the model. search_mode : 'optimal', 'random', or 'norm' Indicates the mode that is used to generate alternative solutions. The optimal mode finds the next best solution. The random mode finds an alternative solution in the direction of a random ray. The - norm mode iteratively finds solution that maximize the L2 distance + norm mode iteratively finds solution that maximize the L2 distance from previously discovered solutions. solver : string The solver to be used. @@ -199,26 +229,29 @@ def enumerate_linear_solutions(model, num_solutions=10, variables='all', Solver option-value pairs to be passed to the solver. tee : boolean Boolean indicating that the solver output should be displayed. - + Returns ------- solutions A list of Solution objects. [Solution] - ''' + """ # TODO: Set this intelligently zero_threshold = 1e-5 - print('STARTING LP ENUMERATION ANALYSIS') - + print("STARTING LP ENUMERATION ANALYSIS") + # For now keeping things simple # TODO: See if this can be relaxed - assert variables == 'all' - - assert search_mode in ['optimal', 'random', 'norm'], \ - 'search mode must be "optimal", "random", or "norm".' - - if variables == 'all': - all_variables = aos_utils.get_model_variables(model, 'all') + assert variables == "all" + + assert search_mode in [ + "optimal", + "random", + "norm", + ], 'search mode must be "optimal", "random", or "norm".' + + if variables == "all": + all_variables = aos_utils.get_model_variables(model, "all") # else: # binary_variables = ComponentSet() # non_binary_variables = [] @@ -231,20 +264,20 @@ def enumerate_linear_solutions(model, num_solutions=10, variables='all', # print(('Warning: The following non-binary variables were included' # 'in the variable list and will be ignored:')) # print(", ".join(non_binary_variables)) - # all_variables = aos_utils.get_model_variables(model, 'all', + # all_variables = aos_utils.get_model_variables(model, 'all', # include_fixed=True) - + # TODO: Relax this if possible for var in all_variables: - assert var.is_continuous(), 'Model must be an LP' + assert var.is_continuous(), "Model must be an LP" opt = pe.SolverFactory(solver) for parameter, value in solver_options.items(): opt.options[parameter] = value - + use_appsi = False # TODO Check all this once implemented - if 'appsi' in solver: + if "appsi" in solver: use_appsi = True opt.update_config.check_for_new_or_removed_constraints = True opt.update_config.update_constraints = False @@ -254,40 +287,43 @@ def enumerate_linear_solutions(model, num_solutions=10, variables='all', opt.update_config.update_params = False opt.update_config.update_named_expressions = False opt.update_config.treat_fixed_vars_as_params = False - - if search_mode == 'norm': + + if search_mode == "norm": opt.update_config.check_for_new_objective = True opt.update_config.update_objective = True - elif search_mode == 'random': + elif search_mode == "random": opt.update_config.check_for_new_objective = True - opt.update_config.update_objective = False + opt.update_config.update_objective = False else: opt.update_config.check_for_new_objective = False opt.update_config.update_objective = False - - print('Peforming initial solve of model.') + + print("Peforming initial solve of model.") results = opt.solve(model, tee=tee) status = results.solver.status condition = results.solver.termination_condition if condition != pe.TerminationCondition.optimal: - raise Exception(('Model could not be solved. LP enumeration analysis ' - 'cannot be applied, SolverStatus = {}, ' - 'TerminationCondition = {}').format(status.value, - condition.value)) - - orig_objective = aos_utils._get_active_objective(model) + raise Exception( + ( + "Model could not be solved. LP enumeration analysis " + "cannot be applied, SolverStatus = {}, " + "TerminationCondition = {}" + ).format(status.value, condition.value) + ) + + orig_objective = aos_utils.get_active_objective(model) orig_objective_value = pe.value(orig_objective) - print('Found optimal solution, value = {}.'.format(orig_objective_value)) - - aos_block = aos_utils._add_aos_block(model, name='_lp_enum') - print('Added block {} to the model.'.format(aos_block)) - aos_utils._add_objective_constraint(aos_block, orig_objective, - orig_objective_value, rel_opt_gap, - abs_opt_gap) - + print("Found optimal solution, value = {}.".format(orig_objective_value)) + + aos_block = aos_utils._add_aos_block(model, name="_lp_enum") + print("Added block {} to the model.".format(aos_block)) + aos_utils._add_objective_constraint( + aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap + ) + canon_block = shifted_lp.get_shifted_linear_model(model) cb = canon_block - + # Set K cb.iteration = pe.Set(pe.PositiveIntegers) @@ -295,55 +331,59 @@ def enumerate_linear_solutions(model, num_solutions=10, variables='all', cb.basic_lower = pe.Var(pe.Any, domain=pe.Binary, dense=False) cb.basic_upper = pe.Var(pe.Any, domain=pe.Binary, dense=False) cb.basic_slack = pe.Var(pe.Any, domain=pe.Binary, dense=False) - + # w upper bounds constraints cb.bound_lower = pe.Constraint(pe.Any) cb.bound_upper = pe.Constraint(pe.Any) cb.bound_slack = pe.Constraint(pe.Any) - + # non-zero basic variable no-good cut set cb.cut_set = pe.Constraint(pe.PositiveIntegers) - - variable_groups = [(cb.var_lower, cb.basic_lower, cb.bound_lower), - (cb.var_upper, cb.basic_upper, cb.bound_upper), - (cb.slack_vars, cb.basic_slack, cb.bound_slack)] + + variable_groups = [ + (cb.var_lower, cb.basic_lower, cb.bound_lower), + (cb.var_upper, cb.basic_upper, cb.bound_upper), + (cb.slack_vars, cb.basic_slack, cb.bound_slack), + ] solution_number = 1 - solutions = [] + solutions = [] while solution_number <= num_solutions: - print('Solving Iteration {}: '.format(solution_number), end='') + print("Solving Iteration {}: ".format(solution_number), end="") results = opt.solve(cb, tee=tee) status = results.solver.status condition = results.solver.termination_condition if condition == pe.TerminationCondition.optimal: for var, index in cb.var_map.items(): var.set_value(var.lb + cb.var_lower[index].value) - sol = solution.Solution(model, all_variables, - objective=orig_objective) + sol = solution.Solution(model, all_variables, objective=orig_objective) solutions.append(sol) orig_objective_value = sol.objective[1] - print('Solved, objective = {}'.format(orig_objective_value)) + print("Solved, objective = {}".format(orig_objective_value)) for var, index in cb.var_map.items(): - print('{} = {}'.format(var.name, var.lb + cb.var_lower[index].value)) - if hasattr(cb, 'force_out'): - cb.del_component('force_out') - if hasattr(cb, 'link_in_out'): - cb.del_component('link_in_out') - - if hasattr(cb, 'basic_last_lower'): - cb.del_component('basic_last_lower') - if hasattr(cb, 'basic_last_upper'): - cb.del_component('basic_last_upper') - if hasattr(cb, 'basic_last_slack'): - cb.del_component('basic_last_slack') - + print("{} = {}".format(var.name, var.lb + cb.var_lower[index].value)) + if hasattr(cb, "force_out"): + cb.del_component("force_out") + if hasattr(cb, "link_in_out"): + cb.del_component("link_in_out") + + if hasattr(cb, "basic_last_lower"): + cb.del_component("basic_last_lower") + if hasattr(cb, "basic_last_upper"): + cb.del_component("basic_last_upper") + if hasattr(cb, "basic_last_slack"): + cb.del_component("basic_last_slack") + cb.link_in_out = pe.Constraint(pe.Any) cb.basic_last_lower = pe.Var(pe.Any, domain=pe.Binary, dense=False) cb.basic_last_upper = pe.Var(pe.Any, domain=pe.Binary, dense=False) cb.basic_last_slack = pe.Var(pe.Any, domain=pe.Binary, dense=False) - basic_last_list = [cb.basic_last_lower, cb.basic_last_upper, - cb.basic_last_slack] - + basic_last_list = [ + cb.basic_last_lower, + cb.basic_last_upper, + cb.basic_last_slack, + ] + num_non_zero = 0 force_out_expr = -1 non_zero_basic_expr = 1 @@ -354,27 +394,34 @@ def enumerate_linear_solutions(model, num_solutions=10, variables='all', num_non_zero += 1 if var not in binary_var: binary_var[var] - constraint[var] = continuous_var[var] <= \ - continuous_var[var].ub * binary_var[var] + constraint[var] = ( + continuous_var[var] + <= continuous_var[var].ub * binary_var[var] + ) non_zero_basic_expr += binary_var[var] basic_var = basic_last_list[idx][var] force_out_expr += basic_var cb.link_in_out[var] = basic_var + binary_var[var] <= 1 cb.force_out = pe.Constraint(expr=force_out_expr >= 0) cb.cut_set[solution_number] = non_zero_basic_expr <= num_non_zero - + solution_number += 1 - elif (condition == pe.TerminationCondition.infeasibleOrUnbounded or - condition == pe.TerminationCondition.infeasible): + elif ( + condition == pe.TerminationCondition.infeasibleOrUnbounded + or condition == pe.TerminationCondition.infeasible + ): print("Infeasible, all alternative solutions have been found.") break else: - print(("Unexpected solver condition. Stopping LP enumeration. " - "SolverStatus = {}, TerminationCondition = {}").format( - status.value, condition.value)) + print( + ( + "Unexpected solver condition. Stopping LP enumeration. " + "SolverStatus = {}, TerminationCondition = {}" + ).format(status.value, condition.value) + ) break aos_block.deactivate() - print('COMPLETED LP ENUMERATION ANALYSIS') - - return solutions \ No newline at end of file + print("COMPLETED LP ENUMERATION ANALYSIS") + + return solutions diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 691078bf51f..26b76b54930 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -14,13 +14,22 @@ from pyomo.contrib import appsi import pdb -def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, - refine_discrete_bounds=False, warmstart=True, - solver='gurobi', solver_options={}, tee=False): - ''' - Calculates the bounds on each variable by solving a series of min and max + +def obbt_analysis( + model, + variables="all", + rel_opt_gap=None, + abs_opt_gap=None, + refine_discrete_bounds=False, + warmstart=True, + solver="gurobi", + solver_options={}, + tee=False, +): + """ + Calculates the bounds on each variable by solving a series of min and max optimization problems where each variable is used as the objective function - This can be applied to any class of problem supported by the selected + This can be applied to any class of problem supported by the selected solver. Parameters @@ -28,23 +37,23 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, model : ConcreteModel A concrete Pyomo model. variables: 'all' or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. 'all' indicates + The variables for which bounds will be generated. 'all' indicates that all variables will be included. Alternatively, a collection of _GenereralVarData variables can be provided. rel_opt_gap : float or None - The relative optimality gap for the original objective for which - variable bounds will be found. None indicates that a relative gap + The relative optimality gap for the original objective for which + variable bounds will be found. None indicates that a relative gap constraint will not be added to the model. abs_opt_gap : float or None - The absolute optimality gap for the original objective for which - variable bounds will be found. None indicates that an absolute gap + The absolute optimality gap for the original objective for which + variable bounds will be found. None indicates that an absolute gap constraint will not be added to the model. refine_discrete_bounds : boolean - Boolean indicating that new constraints should be added to the + Boolean indicating that new constraints should be added to the model at each iteration to tighten the bounds for discrete variables. warmstart : boolean - Boolean indicating that the solver should be warmstarted from the + Boolean indicating that the solver should be warmstarted from the best previously discovered solution. solver : string The solver to be used. @@ -52,32 +61,30 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, Solver option-value pairs to be passed to the solver. tee : boolean Boolean indicating that the solver output should be displayed. - + Returns ------- variable_ranges A Pyomo ComponentMap containing the bounds for each variable. {variable: (lower_bound, upper_bound)}. A None value indicates the solver encountered an issue. - ''' - - print('STARTING OBBT ANALYSIS') - if variables == 'all' or warmstart: - all_variables = aos_utils.get_model_variables(model, 'all', - include_fixed=False) + """ + + print("STARTING OBBT ANALYSIS") + if variables == "all" or warmstart: + all_variables = aos_utils.get_model_variables(model, "all", include_fixed=False) variable_list = all_variables if warmstart: solutions = pe.ComponentMap() for var in all_variables: solutions[var] = [] - + num_vars = len(variable_list) - print('Analyzing {} variables ({} total solves).'.format(num_vars, - 2 * num_vars)) - orig_objective = aos_utils._get_active_objective(model) - + print("Analyzing {} variables ({} total solves).".format(num_vars, 2 * num_vars)) + orig_objective = aos_utils.get_active_objective(model) + use_appsi = False - if 'appsi' in solver: + if "appsi" in solver: opt = appsi.solvers.Gurobi() for parameter, value in solver_options.items(): opt.gurobi_options[parameter] = var_value @@ -85,10 +92,9 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, results = opt.solve(model) condition = results.termination_condition optimal_tc = appsi.base.TerminationCondition.optimal - infeas_or_unbdd_tc = appsi.base.TerminationCondition.\ - infeasibleOrUnbounded + infeas_or_unbdd_tc = appsi.base.TerminationCondition.infeasibleOrUnbounded unbdd_tc = appsi.base.TerminationCondition.unbounded - use_appsi = True + use_appsi = True else: opt = pe.SolverFactory(solver) for parameter, value in solver_options.items(): @@ -98,27 +104,28 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, optimal_tc = pe.TerminationCondition.optimal infeas_or_unbdd_tc = pe.TerminationCondition.infeasibleOrUnbounded unbdd_tc = pe.TerminationCondition.unbounded - print('Peforming initial solve of model.') + print("Peforming initial solve of model.") if condition != optimal_tc: - raise Exception(('OBBT cannot be applied, ' - 'TerminationCondition = {}').format(condition.value)) + raise Exception( + ("OBBT cannot be applied, " "TerminationCondition = {}").format( + condition.value + ) + ) if warmstart: _add_solution(solutions) orig_objective_value = pe.value(orig_objective) - print('Found optimal solution, value = {}.'.format(orig_objective_value)) - aos_block = aos_utils._add_aos_block(model, name='_obbt') - print('Added block {} to the model.'.format(aos_block)) - obj_constraints = aos_utils._add_objective_constraint(aos_block, - orig_objective, - orig_objective_value, - rel_opt_gap, - abs_opt_gap) + print("Found optimal solution, value = {}.".format(orig_objective_value)) + aos_block = aos_utils._add_aos_block(model, name="_obbt") + print("Added block {} to the model.".format(aos_block)) + obj_constraints = aos_utils._add_objective_constraint( + aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap + ) new_constraint = False if len(obj_constraints) > 0: new_constraint = True orig_objective.deactivate() - + if use_appsi: opt.update_config.check_for_new_or_removed_constraints = new_constraint opt.update_config.check_for_new_or_removed_vars = False @@ -130,32 +137,31 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, opt.update_config.update_named_expressions = False opt.update_config.update_objective = False opt.update_config.treat_fixed_vars_as_params = False - + variable_bounds = pe.ComponentMap() - - senses = [(pe.minimize, 'LB'), (pe.maximize, 'UB')] - + + senses = [(pe.minimize, "LB"), (pe.maximize, "UB")] + iteration = 1 - total_iterations = len(senses) * num_vars + total_iterations = len(senses) * num_vars for idx in range(len(senses)): sense = senses[idx][0] bound_dir = senses[idx][1] - + for var in variable_list: if idx == 0: variable_bounds[var] = [None, None] - - if hasattr(aos_block, 'var_objective'): - aos_block.del_component('var_objective') - - aos_block.var_objective = pe.Objective(expr=var, sense=sense) - + + if hasattr(aos_block, "var_objective"): + aos_block.del_component("var_objective") + + aos_block.var_objective = pe.Objective(expr=var, sense=sense) + if warmstart: _update_values(var, bound_dir, solutions) - + if use_appsi: - opt.update_config.check_for_new_or_removed_constraints = \ - new_constraint + opt.update_config.check_for_new_or_removed_constraints = new_constraint if use_appsi: opt.config.stream_solver = tee try: @@ -170,7 +176,7 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, except: pass new_constraint = False - + if condition == optimal_tc: if warmstart: _add_solution(solutions) @@ -179,66 +185,67 @@ def obbt_analysis(model, variables='all', rel_opt_gap=None, abs_opt_gap=None, if refine_discrete_bounds and not var.is_continuous(): if sense == pe.minimize and var.lb < obj_val: - bound_name = var.name + '_' + str.lower(bound_dir) - bound = pe.Constraint(expr= var >= obj_val) + bound_name = var.name + "_" + str.lower(bound_dir) + bound = pe.Constraint(expr=var >= obj_val) setattr(aos_block, bound_name, bound) new_constraint = True - + if sense == pe.maximize and var.ub > obj_val: - bound_name = var.name + '_' + str.lower(bound_dir) - bound = pe.Constraint(expr= var <= obj_val) + bound_name = var.name + "_" + str.lower(bound_dir) + bound = pe.Constraint(expr=var <= obj_val) setattr(aos_block, bound_name, bound) new_constraint = True - + # An infeasibleOrUnbounded status code will imply the problem is # unbounded since feasibility has been established previously - elif (condition == infeas_or_unbdd_tc or - condition == unbdd_tc): + elif condition == infeas_or_unbdd_tc or condition == unbdd_tc: if sense == pe.minimize: - variable_bounds[var][idx] = float('-inf') + variable_bounds[var][idx] = float("-inf") else: - variable_bounds[var][idx] = float('inf') + variable_bounds[var][idx] = float("inf") else: - print(('Unexpected condition for the variable {} {} problem.' - 'TerminationCondition = {}').\ - format(var.name, bound_dir, - condition.value)) - + print( + ( + "Unexpected condition for the variable {} {} problem." + "TerminationCondition = {}" + ).format(var.name, bound_dir, condition.value) + ) + var_value = variable_bounds[var][idx] - print('Iteration {}/{}: {}_{} = {}'.format(iteration, - total_iterations, - var.name, - bound_dir, - var_value)) - + print( + "Iteration {}/{}: {}_{} = {}".format( + iteration, total_iterations, var.name, bound_dir, var_value + ) + ) + if idx == 1: variable_bounds[var] = tuple(variable_bounds[var]) - + iteration += 1 - aos_block.deactivate() orig_objective.activate() - - print('COMPLETED OBBT ANALYSIS') - + + print("COMPLETED OBBT ANALYSIS") + return variable_bounds + def _add_solution(solutions): - '''Add the current variable values to the solution list.''' + """Add the current variable values to the solution list.""" for var in solutions: solutions[var].append(pe.value(var)) + def _update_values(var, bound_dir, solutions): - ''' + """ Set the values of all variables to the best solution seen previously for the current objective function. - ''' - if bound_dir == 'LB': + """ + if bound_dir == "LB": value = min(solutions[var]) else: value = max(solutions[var]) idx = solutions[var].index(value) for variable in solutions: variable.set_value(solutions[variable][idx]) - \ No newline at end of file diff --git a/pyomo/contrib/alternative_solutions/shifted_lp.py b/pyomo/contrib/alternative_solutions/shifted_lp.py index f182ddb7157..2f111ade877 100644 --- a/pyomo/contrib/alternative_solutions/shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/shifted_lp.py @@ -14,54 +14,57 @@ from pyomo.gdp.util import clone_without_expression_components from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.alternative_solutions import aos_utils - + + def _get_unique_name(collection, name): - '''Create a unique name for an item that will be added to a collection.''' + """Create a unique name for an item that will be added to a collection.""" if name not in collection: return name else: i = 1 - while '{}_{}'.format(name, i) not in collection: + while "{}_{}".format(name, i) not in collection: i += 1 - return '{}_{}'.format(name, i) + return "{}_{}".format(name, i) + def _set_slack_ub(expression, slack_var): - ''' + """ Use FBBT to compute an upper bound for a slack variable on an equality - expression.''' + expression.""" slack_lb, slack_ub = compute_bounds_on_expr(expression) assert slack_ub >= 0 slack_var.setub(slack_ub) + def get_shifted_linear_model(model, block=None): - ''' - Converts an (MI)LP with bounded (discrete and) continuous variables + """ + Converts an (MI)LP with bounded (discrete and) continuous variables (l <= x <= u) into a standard form where where all continuous variables - are non-negative reals and all contraints are equalities. For a pure LP of + are non-negative reals and all contraints are equalities. For a pure LP of the form, - + min/max cx s.t. A_1 * x = b_1 A_2 * x <= b_2 l <= x <= u - + a problem of the form, - + min/max c'z s.t. Bz = q z >= 0 - - will be created and added to the returned block. z consists of var_lower - and var_upper variables that are substituted into the original x variables, - and slack_vars that are used to convert the original inequalities to - equalities. Bounds are provided on all variables in z. For MILPs, only the + + will be created and added to the returned block. z consists of var_lower + and var_upper variables that are substituted into the original x variables, + and slack_vars that are used to convert the original inequalities to + equalities. Bounds are provided on all variables in z. For MILPs, only the continuous part of the problem is converted. - - See Lee, Sangbum., C. Phalakornkule, M. Domach, I. Grossmann, Recursive - MILP model for finding all the alternate optima in LP models for metabolic - networks, Computers & Chemical Engineering, Volume 24, Issues 2–7, 2000, + + See Lee, Sangbum., C. Phalakornkule, M. Domach, I. Grossmann, Recursive + MILP model for finding all the alternate optima in LP models for metabolic + networks, Computers & Chemical Engineering, Volume 24, Issues 2–7, 2000, page 712 for additional details. Parameters @@ -70,74 +73,85 @@ def get_shifted_linear_model(model, block=None): A concrete Pyomo model block : Block The Pyomo block that the new model should be added to. - + Returns ------- block The block that holds the reformulated model. - ''' - + """ + # Gather all variables and confirm the model is bounded - all_vars = aos_utils.get_model_variables(model, 'all') + all_vars = aos_utils.get_model_variables(model, "all") new_vars = {} all_vars_new = {} var_map = ComponentMap() var_range = {} for var in all_vars: - assert var.lb is not None , ('Variable {} does not have a ' - 'lower bound. All variables must be ' - 'bounded.'.format(var.name)) - assert var.ub is not None , ('Variable {} does not have an ' - 'upper bound. All variables must be ' - 'bounded.'.format(var.name)) + assert var.lb is not None, ( + "Variable {} does not have a " + "lower bound. All variables must be " + "bounded.".format(var.name) + ) + assert var.ub is not None, ( + "Variable {} does not have an " + "upper bound. All variables must be " + "bounded.".format(var.name) + ) if var.is_continuous(): var_name = _get_unique_name(new_vars.keys(), var.name) new_vars[var_name] = var all_vars_new[var_name] = var var_map[var] = var_name - var_range[var_name] = (0,var.ub-var.lb) + var_range[var_name] = (0, var.ub - var.lb) else: all_vars_new[var.name] = var - + if block is None: block = model - shifted_lp = aos_utils._add_aos_block(block, name='_shifted_lp') - - # Replace original variables with shifted lower and upper variables - shifted_lp.var_lower = pe.Var(new_vars.keys(), domain=pe.NonNegativeReals, - bounds=var_range) - shifted_lp.var_upper = pe.Var(new_vars.keys(), domain=pe.NonNegativeReals, - bounds=var_range) - + shifted_lp = aos_utils._add_aos_block(block, name="_shifted_lp") + + # Replace original variables with shifted lower and upper variables + shifted_lp.var_lower = pe.Var( + new_vars.keys(), domain=pe.NonNegativeReals, bounds=var_range + ) + shifted_lp.var_upper = pe.Var( + new_vars.keys(), domain=pe.NonNegativeReals, bounds=var_range + ) + # Link the shifted lower and upper variables def link_vars_rule(m, var_index): - return m.var_lower[var_index] + m.var_upper[var_index] == \ - m.var_upper[var_index].ub + return ( + m.var_lower[var_index] + m.var_upper[var_index] == m.var_upper[var_index].ub + ) + shifted_lp.link_vars = pe.Constraint(new_vars.keys(), rule=link_vars_rule) - + # Map the lower and upper variables to the original variables and their # lower bounds. This will be used to substitute x with var_lower + x.lb. - var_lower_map = {id(var): shifted_lp.var_lower[i] for i, var in \ - new_vars.items()} + var_lower_map = {id(var): shifted_lp.var_lower[i] for i, var in new_vars.items()} var_lower_bounds = {id(var): var.lb for var in new_vars.values()} var_zeros = {id(var): 0 for var in all_vars_new.values()} - + # Substitute the new s variables into the objective function # The c_fix_zeros calculation is used to find any constant terms that exist # in the objective expression to avoid double counting - active_objective = aos_utils._get_active_objective(model) - c_var_lower = clone_without_expression_components(active_objective.expr, - substitute=var_lower_map) - c_fix_lower = clone_without_expression_components(active_objective.expr, - substitute=var_lower_bounds) - c_fix_zeros = clone_without_expression_components(active_objective.expr, - substitute=var_zeros) - shifted_lp.objective = pe.Objective(expr=c_var_lower - c_fix_zeros + \ - c_fix_lower, - name=active_objective.name + '_shifted', - sense=active_objective.sense) - - # Identify all of the shifted constraints and associated slack variables + active_objective = aos_utils.get_active_objective(model) + c_var_lower = clone_without_expression_components( + active_objective.expr, substitute=var_lower_map + ) + c_fix_lower = clone_without_expression_components( + active_objective.expr, substitute=var_lower_bounds + ) + c_fix_zeros = clone_without_expression_components( + active_objective.expr, substitute=var_zeros + ) + shifted_lp.objective = pe.Objective( + expr=c_var_lower - c_fix_zeros + c_fix_lower, + name=active_objective.name + "_shifted", + sense=active_objective.sense, + ) + + # Identify all of the shifted constraints and associated slack variables # that will need to be created new_constraints = {} constraint_map = ComponentMap() @@ -147,65 +161,65 @@ def link_vars_rule(m, var_index): if constraint.parent_block() == shifted_lp: continue if constraint.equality: - constraint_name = constraint.name + '_equal' - constraint_name = _get_unique_name(new_constraints.keys(), - constraint.name) + constraint_name = constraint.name + "_equal" + constraint_name = _get_unique_name(new_constraints.keys(), constraint.name) new_constraints[constraint_name] = constraint constraint_map[constraint] = constraint_name constraint_type[constraint_name] = 0 else: if constraint.lb is not None: - constraint_name = constraint.name + '_lower' - constraint_name = _get_unique_name(new_constraints.keys(), - constraint.name) + constraint_name = constraint.name + "_lower" + constraint_name = _get_unique_name( + new_constraints.keys(), constraint.name + ) new_constraints[constraint_name] = constraint constraint_map[constraint] = constraint_name constraint_type[constraint_name] = -1 slacks.append(constraint_name) if constraint.ub is not None: - constraint_name = constraint.name + '_upper' - constraint_name = _get_unique_name(new_constraints.keys(), - constraint.name) + constraint_name = constraint.name + "_upper" + constraint_name = _get_unique_name( + new_constraints.keys(), constraint.name + ) new_constraints[constraint_name] = constraint constraint_map[constraint] = constraint_name constraint_type[constraint_name] = 1 slacks.append(constraint_name) shifted_lp.constraint_index = pe.Set(initialize=new_constraints.keys()) shifted_lp.slack_index = pe.Set(initialize=slacks) - shifted_lp.slack_vars = pe.Var(shifted_lp.slack_index, - domain=pe.NonNegativeReals) + shifted_lp.slack_vars = pe.Var(shifted_lp.slack_index, domain=pe.NonNegativeReals) shifted_lp.constraints = pe.Constraint(shifted_lp.constraint_index) - + for constraint_name, constraint in new_constraints.items(): # The c_fix_zeros calculation is used to find any constant terms that # exist in the constraint expression to avoid double counting - a_sub_var_lower = clone_without_expression_components(constraint.body, - substitute=var_lower_map) - a_sub_fix_lower = clone_without_expression_components(constraint.body, - substitute=var_lower_bounds) - a_sub_fix_zeros = clone_without_expression_components(constraint.body, - substitute=var_zeros) + a_sub_var_lower = clone_without_expression_components( + constraint.body, substitute=var_lower_map + ) + a_sub_fix_lower = clone_without_expression_components( + constraint.body, substitute=var_lower_bounds + ) + a_sub_fix_zeros = clone_without_expression_components( + constraint.body, substitute=var_zeros + ) b_lower = constraint.lb b_upper = constraint.ub con_type = constraint_type[constraint_name] if con_type == 0: - expr = a_sub_var_lower - a_sub_fix_zeros + a_sub_fix_lower \ - - b_lower == 0 + expr = a_sub_var_lower - a_sub_fix_zeros + a_sub_fix_lower - b_lower == 0 elif con_type == -1: - expr_rhs = a_sub_var_lower - a_sub_fix_zeros + a_sub_fix_lower \ - - b_lower + expr_rhs = a_sub_var_lower - a_sub_fix_zeros + a_sub_fix_lower - b_lower expr = shifted_lp.slack_vars[constraint_name] == expr_rhs _set_slack_ub(expr_rhs, shifted_lp.slack_vars[constraint_name]) elif con_type == 1: - expr_rhs = b_upper - a_sub_var_lower + a_sub_fix_zeros \ - - a_sub_fix_lower + expr_rhs = b_upper - a_sub_var_lower + a_sub_fix_zeros - a_sub_fix_lower expr = shifted_lp.slack_vars[constraint_name] == expr_rhs _set_slack_ub(expr_rhs, shifted_lp.slack_vars[constraint_name]) shifted_lp.constraints[constraint_name] = expr - + shifted_lp.var_map = var_map shifted_lp.new_vars = new_vars shifted_lp.constraint_map = constraint_map shifted_lp.new_constraints = new_constraints - - return shifted_lp \ No newline at end of file + + return shifted_lp diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 9158cb8f838..8bbf48a8f08 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -15,67 +15,95 @@ import gurobipy import pdb -def gurobi_generate_solutions(model, num_solutions=10, rel_opt_gap=None, - abs_opt_gap=None, solver_options={}, tee=True): - ''' - Finds alternative optimal solutions for discrete variables using Gurobi's + +def gurobi_generate_solutions( + model, + num_solutions=10, + rel_opt_gap=None, + abs_opt_gap=None, + solver_options={}, + tee=False, + quiet=False, +): + """ + Finds alternative optimal solutions for discrete variables using Gurobi's built-in Solution Pool capability. See the Gurobi Solution Pool - documentation for additional details. + documentation for additional details. + Parameters ---------- model : ConcreteModel A concrete Pyomo model. num_solutions : int - The maximum number of solutions to generate. This parameter maps to + The maximum number of solutions to generate. This parameter maps to the PoolSolutions parameter in Gurobi. rel_opt_gap : non-negative float or None The relative optimality gap for allowable alternative solutions. - None implies that there is no limit on the relative optimality gap + None implies that there is no limit on the relative optimality gap (i.e. that any feasible solution can be considered by Gurobi). This parameter maps to the PoolGap parameter in Gurobi. abs_opt_gap : non-negative float or None The absolute optimality gap for allowable alternative solutions. - None implies that there is no limit on the absolute optimality gap + None implies that there is no limit on the absolute optimality gap (i.e. that any feasible solution can be considered by Gurobi). This parameter maps to the PoolGapAbs parameter in Gurobi. solver_options : dict Solver option-value pairs to be passed to the Gurobi solver. tee : boolean Boolean indicating that the solver output should be displayed. - + Returns ------- solutions - A list of Solution objects. - [Solution] - ''' - + A list of Solution objects. [Solution] + """ + # + # Setup gurobi + # opt = appsi.solvers.Gurobi() - for parameter, value in solver_options.items(): - opt.gurobi_options[parameter] = value + if not opt.available(): + return [] - opt.gurobi_options['PoolSolutions'] = num_solutions - opt.gurobi_options['PoolSearchMode'] = 2 opt.config.stream_solver = tee + opt.gurobi_options["PoolSolutions"] = num_solutions + opt.gurobi_options["PoolSearchMode"] = 2 if rel_opt_gap is not None: - opt.gurobi_options['PoolGap'] = rel_opt_gap + opt.gurobi_options["PoolGap"] = rel_opt_gap if abs_opt_gap is not None: - opt.gurobi_options['PoolGapAbs'] = abs_opt_gap + opt.gurobi_options["PoolGapAbs"] = abs_opt_gap + for parameter, value in solver_options.items(): + opt.gurobi_options[parameter] = value + # + # Run gurobi + # results = opt.solve(model) - condition = results.termination_condition - solutions = [] - if condition == appsi.base.TerminationCondition.optimal: - solution_count = opt.get_model_attr('SolCount') + if not (condition == appsi.base.TerminationCondition.optimal): + if not quiet: + print( + ( + "Model cannot be solved, SolverStatus = {}, " + "TerminationCondition = {}" + ).format(status.value, condition.value) + ) + return [] + # + # Collect solutions + # + solution_count = opt.get_model_attr("SolCount") + if not quiet: print("{} solutions found.".format(solution_count)) - variables = aos_utils.get_model_variables(model, 'all', - include_fixed=True) - for i in range(solution_count): - results.solution_loader.load_vars(solution_number=i) - solutions.append(solution.Solution(model, variables)) - else: - print(('Model cannot be solved, SolverStatus = {}, ' - 'TerminationCondition = {}').format(status.value, - condition.value)) + variables = aos_utils.get_model_variables(model, "all", include_fixed=True) + solutions = [] + for i in range(solution_count): + # + # Load the i-th solution into the model + # + results.solution_loader.load_vars(solution_number=i) + # + # Pull the solution from the model into a Solution object, + # and append to our list of solutions + # + solutions.append(solution.Solution(model, variables)) - return solutions \ No newline at end of file + return solutions diff --git a/pyomo/contrib/alternative_solutions/solution.py b/pyomo/contrib/alternative_solutions/solution.py index 1f98c2f4548..4a40f7916a7 100644 --- a/pyomo/contrib/alternative_solutions/solution.py +++ b/pyomo/contrib/alternative_solutions/solution.py @@ -9,14 +9,16 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import json import pyomo.environ as pe from pyomo.common.collections import ComponentMap, ComponentSet from pyomo.contrib.alternative_solutions import aos_utils + class Solution: """ A class to store solutions from a Pyomo model. - + Attributes ---------- variables : ComponentMap @@ -25,7 +27,7 @@ class Solution: The set of Pyomo variables that are fixed in a solution. objectives : ComponentMap A map between Pyomo objectives and their values for a solution. - + Methods ------- pprint(): @@ -37,9 +39,8 @@ class Solution: def get_objective_name_values(self): Get a dictionary of objective name-objective value pairs. """ - - def __init__(self, model, variable_list, include_fixed=True, - objective=None): + + def __init__(self, model, variable_list, include_fixed=True, objective=None): """ Constructs a Pyomo Solution object. @@ -50,14 +51,14 @@ def __init__(self, model, variable_list, include_fixed=True, variable_list: A collection of Pyomo _GenereralVarData variables The variables for which the solution will be stored. include_fixed : boolean - Boolean indicating that fixed variables should be added to the + Boolean indicating that fixed variables should be added to the solution. objective: None or Objective The objective functions for which the value will be saved. None indicates that the active objective should be used, but a different objective can be stored as well. """ - + self.variables = ComponentMap() self.fixed_vars = ComponentSet() for var in variable_list: @@ -66,34 +67,76 @@ def __init__(self, model, variable_list, include_fixed=True, self.fixed_vars.add(var) if include_fixed or not is_fixed: self.variables[var] = pe.value(var) - + if objective is None: - objective = aos_utils._get_active_objective(model) + objective = aos_utils.get_active_objective(model) self.objective = (objective, pe.value(objective)) - - def _round_variable_value(self, variable, value, round_discrete=True): - return value if not round_discrete or variable.is_continuous() \ - else round(value) - - def pprint(self, round_discrete=True): - '''Print the solution variables and objective values.''' - fixed_string = " (Fixed)" - print() - print("Variable\tValue") + + def pprint(self, round_discrete=True, sort_keys=True, indent=4): + """ + Print the solution variables and objective values. + + Parameters + ---------- + rounded_discrete : boolean + If True, then round discrete variable values before printing. + """ + print(self.to_string(round_discrete=round_discrete, sort_keys=sort_keys, indent=indent)) + + def to_string(self, round_discrete=True, sort_keys=True, indent=4): + return json.dumps(self.to_dict(round_discrete=round_discrete), sort_keys=sort_keys, indent=indent) + + def to_dict(self, round_discrete=True): + ans = {} + ans["objective"] = str(self.objective[0]) + ans["objective_value"] = self.objective[1] + soln = {} for variable, value in self.variables.items(): - fxd = fixed_string if variable in self.fixed_vars else "" val = self._round_variable_value(variable, value, round_discrete) - print("{}\t\t\t{}{}".format(variable.name, val, fxd)) - print() - print("Objective value for {} = {}".format(*self.objective)) - - def get_variable_name_values(self, include_fixed=True, - round_discrete=True): - '''Get a dictionary of variable name-variable value pairs.''' - return {var.name: self._round_variable_value(var, val, round_discrete) - for var, val in self.variables.items() \ - if include_fixed or not var in self.fixed_vars} - + soln[variable.name] = val + ans["solution"] = soln + ans["fixed_variables"] = [str(v) for v in self.fixed_vars] + return ans + + def __str__(self): + return self.to_string() + + __repn__ = __str__ + + def get_variable_name_values(self, include_fixed=True, round_discrete=True): + """ + Get a dictionary of variable name-variable value pairs. + + Parameters + ---------- + include_fixed : boolean + If True, then include fixed variables in the dictionary. + round_discrete : boolean + If True, then round discrete variable values in the dictionary. + + Returns + ------- + Dictionary mapping variable names to variable values. + """ + return { + var.name: self._round_variable_value(var, val, round_discrete) + for var, val in self.variables.items() + if include_fixed or not var in self.fixed_vars + } + def get_fixed_variable_names(self): - '''Get a list of fixed-variable names.''' - return [var.name for var in self.fixed_vars] \ No newline at end of file + """ + Get a list of fixed-variable names. + + Returns + ------- + A list of the variable names that are fixed. + """ + return [var.name for var in self.fixed_vars] + + def _round_variable_value(self, variable, value, round_discrete=True): + """ + Returns a rounded value unless the variable is discrete or rounded_discrete is False. + """ + return value if not round_discrete or variable.is_continuous() else round(value) + diff --git a/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py b/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py index e69ce95fb75..c34d6843c30 100644 --- a/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py +++ b/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py @@ -11,13 +11,13 @@ m = tc.get_3d_polyhedron_problem() m.o.deactivate() -m.obj = pe.Objective(expr = m.x[0] + m.x[1] + m.x[2]) -sols = lp_enum.enumerate_linear_solutions(m, solver='gurobi') +m.obj = pe.Objective(expr=m.x[0] + m.x[1] + m.x[2]) +sols = lp_enum.enumerate_linear_solutions(m, solver="gurobi") n = tc.get_pentagonal_pyramid_mip() n.o.sense = pe.minimize n.x.domain = pe.Reals n.y.domain = pe.Reals -sols = lp_enum.enumerate_linear_solutions(n, solver='gurobi') -n.pprint() \ No newline at end of file +sols = lp_enum.enumerate_linear_solutions(n, solver="gurobi") +n.pprint() diff --git a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py index 1fad3e8fcb8..3a1ab6758f6 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py +++ b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py @@ -16,89 +16,92 @@ import pyomo.common.unittest as unittest import pyomo.contrib.alternative_solutions.aos_utils as au + class TestAOSUtilsUnit(unittest.TestCase): def get_multiple_objective_model(self): - '''Create a simple model with three objectives.''' + """Create a simple model with three objectives.""" m = pe.ConcreteModel() m.b1 = pe.Block() m.b2 = pe.Block() m.x = pe.Var() m.y = pe.Var() m.b1.o = pe.Objective(expr=m.x) - m.b2.o = pe.Objective([0,1]) + m.b2.o = pe.Objective([0, 1]) m.b2.o[0] = pe.Objective(expr=m.y) - m.b2.o[1] = pe.Objective(expr=m.x+m.y) + m.b2.o[1] = pe.Objective(expr=m.x + m.y) return m - + def test_multiple_objectives(self): - '''Check that an error is thrown with multiple objectives.''' + """Check that an error is thrown with multiple objectives.""" m = self.get_multiple_objective_model() - assert_text = ("Model has 3 active objective functions, exactly one " - "is required.") + assert_text = ( + "Model has 3 active objective functions, exactly one " "is required." + ) with self.assertRaisesRegex(AssertionError, assert_text): - au._get_active_objective(m) - + au.get_active_objective(m) + def test_no_objectives(self): - '''Check that an error is thrown with no objectives.''' + """Check that an error is thrown with no objectives.""" m = self.get_multiple_objective_model() m.b1.o.deactivate() m.b2.o.deactivate() - assert_text = ("Model has 0 active objective functions, exactly one " - "is required.") + assert_text = ( + "Model has 0 active objective functions, exactly one " "is required." + ) with self.assertRaisesRegex(AssertionError, assert_text): - au._get_active_objective(m) + au.get_active_objective(m) def test_one_objective(self): - ''' - Check that the active objective is returned, when there is just one + """ + Check that the active objective is returned, when there is just one objective. - ''' + """ m = self.get_multiple_objective_model() m.b1.o.deactivate() m.b2.o[0].deactivate() - self.assertEqual(m.b2.o[1], au._get_active_objective(m)) - + self.assertEqual(m.b2.o[1], au.get_active_objective(m)) + def test_aos_block(self): - '''Ensure that an alternative solution block is added.''' + """Ensure that an alternative solution block is added.""" m = self.get_multiple_objective_model() - block_name = 'test_block' + block_name = "test_block" b = au._add_aos_block(m, block_name) self.assertEqual(b.name, block_name) self.assertEqual(b.ctype, pe.Block) - - def get_simple_model(self, sense = pe.minimize): - '''Create a simple 2d linear program with an objective.''' + + def get_simple_model(self, sense=pe.minimize): + """Create a simple 2d linear program with an objective.""" m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() - m.o = pe.Objective(expr=m.x+m.y, sense=sense) + m.o = pe.Objective(expr=m.x + m.y, sense=sense) return m - + def test_no_obj_constraint(self): - '''Ensure that no objective constraints are added.''' + """Ensure that no objective constraints are added.""" m = self.get_simple_model() cons = au._add_objective_constraint(m, m.o, 2, None, None) self.assertEqual(cons, []) - self.assertEqual(m.find_component('optimality_tol_rel'), None) - self.assertEqual(m.find_component('optimality_tol_abs'), None) - + self.assertEqual(m.find_component("optimality_tol_rel"), None) + self.assertEqual(m.find_component("optimality_tol_abs"), None) + def test_min_rel_obj_constraint(self): - '''Ensure that the correct relative objective constraint is added.''' + """Ensure that the correct relative objective constraint is added.""" m = self.get_simple_model() cons = au._add_objective_constraint(m, m.o, 2, 0.1, None) self.assertEqual(len(cons), 1) - self.assertEqual(m.find_component('optimality_tol_rel'), cons[0]) - self.assertEqual(m.find_component('optimality_tol_abs'), None) + self.assertEqual(m.find_component("optimality_tol_rel"), cons[0]) + self.assertEqual(m.find_component("optimality_tol_abs"), None) self.assertEqual(2.2, cons[0].upper) self.assertEqual(None, cons[0].lower) def test_min_abs_obj_constraint(self): - '''Ensure that the correct absolute objective constraint is added.''' + """Ensure that the correct absolute objective constraint is added.""" m = self.get_simple_model() cons = au._add_objective_constraint(m, m.o, 2, None, 1) self.assertEqual(len(cons), 1) - self.assertEqual(m.find_component('optimality_tol_rel'), None) - self.assertEqual(m.find_component('optimality_tol_abs'), cons[0]) + self.assertEqual(m.find_component("optimality_tol_rel"), None) + self.assertEqual(m.find_component("optimality_tol_abs"), cons[0]) self.assertEqual(3, cons[0].upper) self.assertEqual(None, cons[0].lower) @@ -106,172 +109,174 @@ def test_min_both_obj_constraint(self): m = self.get_simple_model() cons = au._add_objective_constraint(m, m.o, -10, 0.3, 5) self.assertEqual(len(cons), 2) - self.assertEqual(m.find_component('optimality_tol_rel'), cons[0]) - self.assertEqual(m.find_component('optimality_tol_abs'), cons[1]) + self.assertEqual(m.find_component("optimality_tol_rel"), cons[0]) + self.assertEqual(m.find_component("optimality_tol_abs"), cons[1]) self.assertEqual(-7, cons[0].upper) self.assertEqual(None, cons[0].lower) self.assertEqual(-5, cons[1].upper) self.assertEqual(None, cons[1].lower) - + def test_max_both_obj_constraint(self): - ''' + """ Ensure that the correct relative and absolute objective constraints are added. - ''' + """ m = self.get_simple_model(sense=pe.maximize) cons = au._add_objective_constraint(m, m.o, -1, 0.3, 1) self.assertEqual(len(cons), 2) - self.assertEqual(m.find_component('optimality_tol_rel'), cons[0]) - self.assertEqual(m.find_component('optimality_tol_abs'), cons[1]) + self.assertEqual(m.find_component("optimality_tol_rel"), cons[0]) + self.assertEqual(m.find_component("optimality_tol_abs"), cons[1]) self.assertEqual(None, cons[0].upper) self.assertEqual(-1.3, cons[0].lower) self.assertEqual(None, cons[1].upper) self.assertEqual(-2, cons[1].lower) def test_max_both_obj_constraint2(self): - ''' + """ Ensure that the correct relative and absolute objective constraints are added. - ''' + """ m = self.get_simple_model(sense=pe.maximize) cons = au._add_objective_constraint(m, m.o, 20, 0.5, 11) self.assertEqual(len(cons), 2) - self.assertEqual(m.find_component('optimality_tol_rel'), cons[0]) - self.assertEqual(m.find_component('optimality_tol_abs'), cons[1]) + self.assertEqual(m.find_component("optimality_tol_rel"), cons[0]) + self.assertEqual(m.find_component("optimality_tol_abs"), cons[1]) self.assertEqual(None, cons[0].upper) self.assertEqual(10, cons[0].lower) self.assertEqual(None, cons[1].upper) self.assertEqual(9, cons[1].lower) - + def test_random_direction(self): - ''' + """ Ensure that _get_random_direction returns a normal vector. - ''' + """ vector = au._get_random_direction(10) self.assertAlmostEqual(1.0, norm(vector)) def get_var_model(self): - ''' - Create a model with multiple variables that are nested over several + """ + Create a model with multiple variables that are nested over several layers of blocks. - ''' - - indices = [0,1,2,3] - + """ + + indices = [0, 1, 2, 3] + m = pe.ConcreteModel() - + m.b1 = pe.Block() m.b2 = pe.Block() m.b1.sb1 = pe.Block() m.b2.sb2 = pe.Block() - + m.x = pe.Var(domain=pe.Reals) m.b1.y = pe.Var(domain=pe.Binary) m.b2.z = pe.Var(domain=pe.Integers) - + m.x_f = pe.Var(domain=pe.Reals) m.b1.y_f = pe.Var(domain=pe.Binary) m.b2.z_f = pe.Var(domain=pe.Integers) m.x_f.fix(0) m.b1.y_f.fix(0) m.b2.z_f.fix(0) - + m.b1.sb1.x_l = pe.Var(indices, domain=pe.Reals) m.b1.sb1.y_l = pe.Var(indices, domain=pe.Binary) m.b2.sb2.z_l = pe.Var(indices, domain=pe.Integers) - + m.b1.sb1.x_l[3].fix(0) m.b1.sb1.y_l[3].fix(0) m.b2.sb2.z_l[3].fix(0) - - vars_minus_x = [m.b1.y, m.b2.z, m.x_f, m.b1.y_f, m.b2.z_f] + \ - [m.b1.sb1.x_l[i] for i in indices] + \ - [m.b1.sb1.y_l[i] for i in indices] + \ - [m.b2.sb2.z_l[i] for i in indices] - + + vars_minus_x = ( + [m.b1.y, m.b2.z, m.x_f, m.b1.y_f, m.b2.z_f] + + [m.b1.sb1.x_l[i] for i in indices] + + [m.b1.sb1.y_l[i] for i in indices] + + [m.b2.sb2.z_l[i] for i in indices] + ) + m.con = pe.Constraint(expr=sum(v for v in vars_minus_x) <= 1) - m.b1.con = pe.Constraint(expr=m.b1.y<= 1) - m.b1.sb1.con = pe.Constraint(expr=m.b1.sb1.y_l[0]<= 1) + m.b1.con = pe.Constraint(expr=m.b1.y <= 1) + m.b1.sb1.con = pe.Constraint(expr=m.b1.sb1.y_l[0] <= 1) m.obj = pe.Objective(expr=m.x) - m.all_vars = ComponentSet([m.x] + vars_minus_x) - m.unfixed_vars = ComponentSet([var for var in m.all_vars \ - if not var.is_fixed()]) - + m.all_vars = ComponentSet([m.x] + vars_minus_x) + m.unfixed_vars = ComponentSet([var for var in m.all_vars if not var.is_fixed()]) + return m - + def test_get_all_variables_unfixed(self): - '''Check that all unfixed variables are gathered.''' + """Check that all unfixed variables are gathered.""" m = self.get_var_model() var = au.get_model_variables(m) self.assertEqual(var, m.unfixed_vars) - + def test_get_all_variables(self): - '''Check that all fixed and unfixed variables are gathered.''' + """Check that all fixed and unfixed variables are gathered.""" m = self.get_var_model() var = au.get_model_variables(m, include_fixed=True) self.assertEqual(var, m.all_vars) - + def test_get_all_continuous(self): - '''Check that all continuous variables are gathered.''' + """Check that all continuous variables are gathered.""" m = self.get_var_model() - var = au.get_model_variables(m, - include_continuous=True, - include_binary=False, - include_integer=False) - continuous_vars = ComponentSet(var for var in m.unfixed_vars \ - if var.is_continuous()) + var = au.get_model_variables( + m, include_continuous=True, include_binary=False, include_integer=False + ) + continuous_vars = ComponentSet( + var for var in m.unfixed_vars if var.is_continuous() + ) self.assertEqual(var, continuous_vars) - + def test_get_all_binary(self): - '''Check that all binary variables are gathered.''' + """Check that all binary variables are gathered.""" m = self.get_var_model() - var = au.get_model_variables(m, - include_continuous=False, - include_binary=True, - include_integer=False) - binary_vars = ComponentSet(var for var in m.unfixed_vars \ - if var.is_binary()) + var = au.get_model_variables( + m, include_continuous=False, include_binary=True, include_integer=False + ) + binary_vars = ComponentSet(var for var in m.unfixed_vars if var.is_binary()) self.assertEqual(var, binary_vars) def test_get_all_integer(self): - '''Check that all integer variables are gathered.''' + """Check that all integer variables are gathered.""" m = self.get_var_model() - var = au.get_model_variables(m, - include_continuous=False, - include_binary=False, - include_integer=True) - continuous_vars = ComponentSet(var for var in m.unfixed_vars \ - if var.is_integer()) + var = au.get_model_variables( + m, include_continuous=False, include_binary=False, include_integer=True + ) + continuous_vars = ComponentSet( + var for var in m.unfixed_vars if var.is_integer() + ) self.assertEqual(var, continuous_vars) def test_get_specific_vars(self): - '''Check that all variables from a list are gathered.''' + """Check that all variables from a list are gathered.""" m = self.get_var_model() components = [m.x, m.b1.sb1.y_l[0], m.b2.sb2.z_l] var = au.get_model_variables(m, components=components) - specific_vars = ComponentSet([m.x, m.b1.sb1.y_l[0], m.b2.sb2.z_l[0], - m.b2.sb2.z_l[1], m.b2.sb2.z_l[2]]) + specific_vars = ComponentSet( + [m.x, m.b1.sb1.y_l[0], m.b2.sb2.z_l[0], m.b2.sb2.z_l[1], m.b2.sb2.z_l[2]] + ) self.assertEqual(var, specific_vars) - + def test_get_block_vars(self): - ''' - Check that all variables from block are gathered (without + """ + Check that all variables from block are gathered (without descending into subblocks). - ''' + """ m = self.get_var_model() components = [m.b2.sb2.z_l, (m.b1, False)] var = au.get_model_variables(m, components=components) - specific_vars = ComponentSet([m.b1.y, m.b2.sb2.z_l[0], - m.b2.sb2.z_l[1], m.b2.sb2.z_l[2]]) + specific_vars = ComponentSet( + [m.b1.y, m.b2.sb2.z_l[0], m.b2.sb2.z_l[1], m.b2.sb2.z_l[2]] + ) self.assertEqual(var, specific_vars) def test_get_constraint_vars(self): - '''Check that all variables constraints and objectives are gathered.''' + """Check that all variables constraints and objectives are gathered.""" m = self.get_var_model() components = [m.con, m.obj] var = au.get_model_variables(m, components=components) self.assertEqual(var, m.unfixed_vars) -if __name__ == '__main__': - unittest.main() \ No newline at end of file + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/alternative_solutions/tests/test_balas.py b/pyomo/contrib/alternative_solutions/tests/test_balas.py index 11e50a4a8b9..bcca6723823 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_balas.py +++ b/pyomo/contrib/alternative_solutions/tests/test_balas.py @@ -15,21 +15,23 @@ import pyomo.contrib.alternative_solutions.balas import pyomo.contrib.alternative_solutions.tests.test_cases as tc + class TestBalasUnit(unittest.TestCase): - - #TODO: Add test cases - ''' + + # TODO: Add test cases + """ Repeat a lot of the test from solnpool to check that the various arguments work correct. The main difference will be that we will only want to check binary problems here. The knapsack problem should be useful (just set the bounds to 0-1). - - The only other thing to test is the different search modes. They should still enumerate + + The only other thing to test is the different search modes. They should still enumerate all of the solutions, just in a different sequence. - - ''' - + + """ + def test_(self): pass -if __name__ == '__main__': - unittest.main() \ No newline at end of file + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index c8eb34f2c0a..ef6b67fb742 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -11,33 +11,14 @@ from itertools import product from math import ceil, floor - -import numpy as np from collections import Counter - +import numpy as np import pyomo.environ as pe -import pdb -# TODO: Add more test probelms as needed. -''' +""" This script has collection of test cases that can be used to enumerate solutions. That is, simple problems where the alternative solutions can be found manually. - -I started on a few problems here. I tired to enumerate all of the solutions for -disrete cases. This should make it easy to find all feasible points, and/or -all points within some percent/value of optimality. - -I created some pure continuous problems and found bounds and extreme points for those -but more work is needed to be able to find the bounds and extreme points within some -threshold optimality. - -I have not done any mixed cases yet, but an case with those would be useful. -get_2d_diamond_problem does let make x or y discrete, but I have not found the bounds -and extreme points for these cases yet. - -Other cases come to mind? A quadtratic maybe? - -''' +""" def _is_satified(constraint, feasability_tol=1e-6): @@ -48,41 +29,46 @@ def _is_satified(constraint, feasability_tol=1e-6): return False return True + def get_2d_diamond_problem(discrete_x=False, discrete_y=False): - '''Simple 2d problem where the feasible is diamond-shaped.''' + """Simple 2d problem where the feasible is diamond-shaped.""" m = pe.ConcreteModel() m.x = pe.Var(within=pe.Integers if discrete_x else pe.Reals) m.y = pe.Var(within=pe.Integers if discrete_y else pe.Reals) - - m.o = pe.Objective(expr = m.x + m.y, sense=pe.maximize) - - m.c1 = pe.Constraint(expr= -4/5 * m.x - 4 <= m.y) - m.c2 = pe.Constraint(expr= 5/9 * m.x - 5 <= m.y) - m.c3 = pe.Constraint(expr= 2/9 * m.x + 2 >= m.y) - m.c4 = pe.Constraint(expr= -1/2 * m.x + 3 >= m.y) + + m.o = pe.Objective(expr=m.x + m.y, sense=pe.maximize) + + m.c1 = pe.Constraint(expr=-4 / 5 * m.x - 4 <= m.y) + m.c2 = pe.Constraint(expr=5 / 9 * m.x - 5 <= m.y) + m.c3 = pe.Constraint(expr=2 / 9 * m.x + 2 >= m.y) + m.c4 = pe.Constraint(expr=-1 / 2 * m.x + 3 >= m.y) # Continuous exteme points and bounds - m.extreme_points = {(0.737704918, -4.590163934), - (-5.869565217, 0.695652174), - (1.384615385, 2.307692308), - (7.578947368, -0.789473684)} + m.extreme_points = { + (0.737704918, -4.590163934), + (-5.869565217, 0.695652174), + (1.384615385, 2.307692308), + (7.578947368, -0.789473684), + } m.continuous_bounds = pe.ComponentMap() m.continuous_bounds[m.x] = (-5.869565217, 7.578947368) m.continuous_bounds[m.y] = (-4.590163934, 2.307692308) - # Continuous exteme points and bounds for the case where an objective - # constraint is added within a 100% relative gap of optimality or an + # Continuous exteme points and bounds for the case where an objective + # constraint is added within a 100% relative gap of optimality or an # absolute gap of 6.789473684 - - m.extreme_points_cut = {(45/14, -45/14), - (-18/11, 18/11), - (1.384615385, 2.307692308), - (7.578947368, -0.789473684)} + + m.extreme_points_cut = { + (45 / 14, -45 / 14), + (-18 / 11, 18 / 11), + (1.384615385, 2.307692308), + (7.578947368, -0.789473684), + } m.continuous_bounds_cut = pe.ComponentMap() - m.continuous_bounds_cut[m.x] = (-18/11, 7.578947368) - m.continuous_bounds_cut[m.y] = (-45/14, 2.307692308) + m.continuous_bounds_cut[m.x] = (-18 / 11, 7.578947368) + m.continuous_bounds_cut[m.y] = (-45 / 14, 2.307692308) # Discrete feasible solutions and bounds feasible_sols = [] @@ -90,14 +76,14 @@ def get_2d_diamond_problem(discrete_x=False, discrete_y=False): x_upper_bound = None y_lower_bound = None y_upper_bound = None - + x_lower = ceil(m.continuous_bounds[m.x][0]) x_upper = floor(m.continuous_bounds[m.x][1]) y_lower = ceil(m.continuous_bounds[m.y][0]) y_upper = floor(m.continuous_bounds[m.y][1]) cons = [m.c1, m.c2, m.c3, m.c4] - for x_value in range(x_lower, x_upper+1): - for y_value in range(y_lower, y_upper+1): + for x_value in range(x_lower, x_upper + 1): + for y_value in range(y_lower, y_upper + 1): m.x.set_value(x_value) m.y.set_value(y_value) is_feasible = True @@ -115,20 +101,20 @@ def get_2d_diamond_problem(discrete_x=False, discrete_y=False): if y_upper_bound is None or y_value > y_upper_bound: y_upper_bound = y_value feasible_sols.append(((x_value, y_value), x_value + y_value)) - m.discrete_feasible = sorted(feasible_sols, key=lambda sol: sol[1], - reverse=True) + m.discrete_feasible = sorted(feasible_sols, key=lambda sol: sol[1], reverse=True) m.discrete_bounds = pe.ComponentMap() m.discrete_bounds[m.x] = (x_lower_bound, x_upper_bound) m.discrete_bounds[m.y] = (y_lower_bound, y_upper_bound) return m + def get_3d_polyhedron_problem(): - ''' + """ Simple 3d polyhedron that is expressed using all types of linear constraints - ''' + """ m = pe.ConcreteModel() - m.x = pe.Var([0,1,2], within=pe.Reals) + m.x = pe.Var([0, 1, 2], within=pe.Reals) m.x[0].setlb(-1) m.x[0].setub(1) m.x[1].setlb(-2) @@ -147,61 +133,71 @@ def _constraint_switch_rule(m, i): return -m.x[0] + m.x[1] >= -2 elif i == 4: return m.x[0] + m.x[1] + m.x[2] == 4 - m.c = pe.Constraint([i for i in range(5)], rule = _constraint_switch_rule) - m.o = pe.Objective(expr=m.x[0] + m.x[2], sense=pe.maximize) + m.c = pe.Constraint([i for i in range(5)], rule=_constraint_switch_rule) + + m.o = pe.Objective(expr=m.x[0] + m.x[2], sense=pe.maximize) return m + def get_2d_unbounded_problem(): - ''' + """ Simple 2d problem where the feasible region is unbounded, but the problem - has an optimal solution.''' + has an optimal solution. + """ m = pe.ConcreteModel() m.x = pe.Var(within=pe.Reals) m.y = pe.Var(within=pe.Reals) - - m.o = pe.Objective(expr = m.y - m.x) - - m.c1 = pe.Constraint(expr= m.x <= 4) - m.c2 = pe.Constraint(expr= m.y >= 2) + + m.o = pe.Objective(expr=m.y - m.x) + + m.c1 = pe.Constraint(expr=m.x <= 4) + m.c2 = pe.Constraint(expr=m.y >= 2) m.extreme_points = {(4, 2)} m.continuous_bounds = pe.ComponentMap() - m.continuous_bounds[m.x] = (float('-inf'), 4) - m.continuous_bounds[m.y] = (2, float('inf')) + m.continuous_bounds[m.x] = (float("-inf"), 4) + m.continuous_bounds[m.y] = (2, float("inf")) return m + def get_2d_degenerate_lp(): - ''' - Simple 2d problem that includes a redundant contraint such that three - constraints are active at optimality.''' + """ + Simple 2d problem that includes a redundant contraint such that three + constraints are active at optimality. + """ m = pe.ConcreteModel() - - m.x = pe.Var(within=pe.Reals, bounds=(-1,3)) - m.y = pe.Var(within=pe.Reals, bounds=(-3,2)) - - m.obj = pe.Objective(expr=m.x+2*m.y, sense=pe.maximize) - - m.con1 = pe.Constraint(expr=m.x+m.y<=3) - m.con2 = pe.Constraint(expr=m.x+2*m.y<=5) - m.con3 = pe.Constraint(expr=m.x+m.y>=-1) - + + m.x = pe.Var(within=pe.Reals, bounds=(-1, 3)) + m.y = pe.Var(within=pe.Reals, bounds=(-3, 2)) + + m.obj = pe.Objective(expr=m.x + 2 * m.y, sense=pe.maximize) + + m.con1 = pe.Constraint(expr=m.x + m.y <= 3) + m.con2 = pe.Constraint(expr=m.x + 2 * m.y <= 5) + m.con3 = pe.Constraint(expr=m.x + m.y >= -1) + return m + def get_triangle_ip(): - ''' + """ Simple 2d discrete problem where the feasible region looks like a 90-45-45 - right triangle and the optimal solutions fall along the hypotenuse. - ''' + right triangle and the optimal solutions fall along the hypotenuse, where + x + y == 5. Alternative near-optimal have integer objective values from 0 to 4. + """ var_max = 5 m = pe.ConcreteModel() - m.x = pe.Var(within=pe.NonNegativeIntegers, bounds=(0,var_max)) - m.y = pe.Var(within=pe.NonNegativeIntegers, bounds=(0,var_max)) - + m.x = pe.Var(within=pe.NonNegativeIntegers, bounds=(0, var_max)) + m.y = pe.Var(within=pe.NonNegativeIntegers, bounds=(0, var_max)) + m.o = pe.Objective(expr=m.x + m.y, sense=pe.maximize) - m.c = pe.Constraint(expr= m.x + m.y <= var_max) - + m.c = pe.Constraint(expr=m.x + m.y <= var_max) + + # + # Enumerate all feasible solutions + # feasible_sols = [] for i in range(var_max + 1): for j in range(var_max + 1): @@ -209,25 +205,29 @@ def get_triangle_ip(): feasible_sols.append(((i, j), i + j)) feasible_sols = sorted(feasible_sols, key=lambda sol: sol[1], reverse=True) m.feasible_sols = feasible_sols - m.num_ranked_solns = [6,5,4,3,2,1] - + # + # Count of solutions from best to worst + # + m.num_ranked_solns = [6, 5, 4, 3, 2, 1] + return m + def get_implied_bound_ip(): - ''' + """ 2d discrete problem where the bounds of z are impled by x and y. This facilitate testing cases where the impled bounds are tighter than the given bounds for the variable. - ''' + """ m = pe.ConcreteModel() - m.x = pe.Var(within=pe.NonNegativeIntegers, bounds=(0,5)) - m.y = pe.Var(within=pe.NonNegativeIntegers, bounds=(0,5)) - m.z = pe.Var(within=pe.NonNegativeIntegers, bounds=(0,5)) - - m.o = pe.Objective(expr = m.x + m.z) - - m.c1 = pe.Constraint(expr= m.x + m.y == 3) - m.c2 = pe.Constraint(expr= m.x + m.y + m.z <= 5) + m.x = pe.Var(within=pe.NonNegativeIntegers, bounds=(0, 5)) + m.y = pe.Var(within=pe.NonNegativeIntegers, bounds=(0, 5)) + m.z = pe.Var(within=pe.NonNegativeIntegers, bounds=(0, 5)) + + m.o = pe.Objective(expr=m.x + m.z) + + m.c1 = pe.Constraint(expr=m.x + m.y == 3) + m.c2 = pe.Constraint(expr=m.x + m.y + m.z <= 5) m.extreme_points = {(4, 2)} @@ -235,42 +235,41 @@ def get_implied_bound_ip(): m.var_bounds[m.x] = (0, 3) m.var_bounds[m.y] = (0, 3) m.var_bounds[m.z] = (0, 2) - + return m - + def get_aos_test_knapsack(var_max, weights, values, capacity_fraction): - ''' + """ Creates a knapsack problem, given arrays of weights and values, and returns all feasible solutions. The capacity represents the percent of the total max weight that can be selected (sum weights * var_max). The var_max parameter sets the upper bound on all variables, teh max number of times they can be selected. - ''' - assert len(weights) == len(values), \ - 'weights and values must be the same length.' - assert 0 <= capacity_fraction and capacity_fraction <= 1, \ - 'capacity_fraction must be between 0 and 1.' - + """ + assert len(weights) == len(values), "weights and values must be the same length." + assert ( + 0 <= capacity_fraction and capacity_fraction <= 1 + ), "capacity_fraction must be between 0 and 1." + num_vars = len(weights) capacity = sum(weights) * var_max * capacity_fraction - + m = pe.ConcreteModel() - m.i = pe.RangeSet(0,num_vars-1) - - if var_max == 1: + m.i = pe.RangeSet(0, num_vars - 1) + + if var_max == 1: m.x = pe.Var(m.i, within=pe.Binary) else: - m.x = pe.Var(m.i, within=pe.NonNegativeIntegers, bounds=(0,var_max)) + m.x = pe.Var(m.i, within=pe.NonNegativeIntegers, bounds=(0, var_max)) + + m.o = pe.Objective(expr=sum(values[i] * m.x[i] for i in m.i), sense=pe.maximize) - m.o = pe.Objective(expr=sum(values[i]*m.x[i] for i in m.i), - sense=pe.maximize) + m.c = pe.Constraint(expr=sum(weights[i] * m.x[i] for i in m.i) <= capacity) - m.c = pe.Constraint(expr=sum(weights[i]*m.x[i] for i in m.i) <= capacity) - - var_domain = range(var_max+1) + var_domain = range(var_max + 1) all_combos = product(var_domain, repeat=num_vars) - + feasible_sols = [] for sol in all_combos: if np.dot(sol, weights) <= capacity: @@ -278,73 +277,117 @@ def get_aos_test_knapsack(var_max, weights, values, capacity_fraction): feasible_sols = sorted(feasible_sols, key=lambda sol: sol[1], reverse=True) return m + def get_pentagonal_pyramid_mip(): - ''' - Pentagonal pyramid with integer coordinates in the first two dimensions and + """ + Pentagonal pyramid with integer coordinates in the first two dimensions and a third continuous dimension. - - ''' + """ var_max = 5 m = pe.ConcreteModel() - m.x = pe.Var(within=pe.Integers, bounds=(-var_max,var_max)) - m.y = pe.Var(within=pe.Integers, bounds=(-var_max,var_max)) - m.z = pe.Var(within=pe.NonNegativeReals, bounds=(0,var_max)) + m.x = pe.Var(within=pe.Integers, bounds=(-var_max, var_max)) + m.y = pe.Var(within=pe.Integers, bounds=(-var_max, var_max)) + m.z = pe.Var(within=pe.NonNegativeReals, bounds=(0, var_max)) m.o = pe.Objective(expr=m.z, sense=pe.maximize) - base_points = np.array([[0, var_max, 0], [var_max, 0, 0], [var_max/2.0, -var_max, 0], [-var_max/2.0, -var_max, 0], [-var_max, 0, 0]]) + + base_points = np.array( + [ + [0, var_max, 0], + [var_max, 0, 0], + [var_max / 2.0, -var_max, 0], + [-var_max / 2.0, -var_max, 0], + [-var_max, 0, 0], + ] + ) apex_point = np.array([0, 0, var_max]) m.c = pe.ConstraintList() for i in range(5): vec_1 = base_points[i] - apex_point - vec_2 = base_points[(i+1) % var_max] - base_points[i] + vec_2 = base_points[(i + 1) % var_max] - base_points[i] n = np.cross(vec_1, vec_2) - m.c.add(n[0]*(m.x - apex_point[0]) + n[1]*(m.y - apex_point[1]) + n[2]*(m.z - apex_point[2]) >= 0) + m.c.add( + n[0] * (m.x - apex_point[0]) + + n[1] * (m.y - apex_point[1]) + + n[2] * (m.z - apex_point[2]) + >= 0 + ) + # + # Count of solutions from best to worst + # m.num_ranked_solns = [1, 4, 2, 8, 2, 12, 4, 16, 4, 20] return m + def get_indexed_pentagonal_pyramid_mip(): - ''' - Pentagonal pyramid with integer coordinates in the first two dimensions and + """ + Pentagonal pyramid with integer coordinates in the first two dimensions and a third continuous dimension. - - ''' + """ var_max = 5 m = pe.ConcreteModel() - m.x = pe.Var([1,2], within=pe.Integers, bounds=(-var_max,var_max)) - m.z = pe.Var(within=pe.NonNegativeReals, bounds=(0,var_max)) + m.x = pe.Var([1, 2], within=pe.Integers, bounds=(-var_max, var_max)) + m.z = pe.Var(within=pe.NonNegativeReals, bounds=(0, var_max)) m.o = pe.Objective(expr=m.z, sense=pe.maximize) - base_points = np.array([[0, var_max, 0], [var_max, 0, 0], [var_max/2.0, -var_max, 0], [-var_max/2.0, -var_max, 0], [-var_max, 0, 0]]) + base_points = np.array( + [ + [0, var_max, 0], + [var_max, 0, 0], + [var_max / 2.0, -var_max, 0], + [-var_max / 2.0, -var_max, 0], + [-var_max, 0, 0], + ] + ) apex_point = np.array([0, 0, var_max]) def _con_rule(m, i): vec_1 = base_points[i] - apex_point - vec_2 = base_points[(i+1) % var_max] - base_points[i] + vec_2 = base_points[(i + 1) % var_max] - base_points[i] n = np.cross(vec_1, vec_2) - expr = n[0]*(m.x[1] - apex_point[0]) + n[1]*(m.x[2] - apex_point[1]) + n[2]*(m.z - apex_point[2]) + expr = ( + n[0] * (m.x[1] - apex_point[0]) + + n[1] * (m.x[2] - apex_point[1]) + + n[2] * (m.z - apex_point[2]) + ) return expr >= 0 + m.c = pe.Constraint([i for i in range(5)], rule=_con_rule) m.num_ranked_solns = [1, 4, 2, 8, 2, 12, 4, 16, 4, 20] return m + def get_bloated_pentagonal_pyramid_mip(): - ''' - Pentagonal pyramid with integer coordinates in the first two dimensions and + """ + Pentagonal pyramid with integer coordinates in the first two dimensions and a third continuous dimension. Bounds are artificially widened for obbt testing purposes - ''' + """ var_max = 5 m = pe.ConcreteModel() - m.x = pe.Var(within=pe.Integers, bounds=(-2*var_max, 2*var_max)) - m.y = pe.Var(within=pe.Integers, bounds=(-2*var_max, var_max)) - m.z = pe.Var(within=pe.NonNegativeReals, bounds=(0, 2*var_max)) + m.x = pe.Var(within=pe.Integers, bounds=(-2 * var_max, 2 * var_max)) + m.y = pe.Var(within=pe.Integers, bounds=(-2 * var_max, var_max)) + m.z = pe.Var(within=pe.NonNegativeReals, bounds=(0, 2 * var_max)) m.var_bounds = pe.ComponentMap() m.o = pe.Objective(expr=m.z, sense=pe.maximize) - base_points = np.array([[0, var_max, 0], [var_max, 0, 0], [var_max/2.0, -var_max, 0], [-var_max/2.0, -var_max, 0], [-var_max, 0, 0]]) + base_points = np.array( + [ + [0, var_max, 0], + [var_max, 0, 0], + [var_max / 2.0, -var_max, 0], + [-var_max / 2.0, -var_max, 0], + [-var_max, 0, 0], + ] + ) apex_point = np.array([0, 0, var_max]) m.c = pe.ConstraintList() for i in range(5): vec_1 = base_points[i] - apex_point - vec_2 = base_points[(i+1) % var_max] - base_points[i] + vec_2 = base_points[(i + 1) % var_max] - base_points[i] n = np.cross(vec_1, vec_2) - m.c.add(n[0]*(m.x - apex_point[0]) + n[1]*(m.y - apex_point[1]) + n[2]*(m.z - apex_point[2]) >= 0) - return m \ No newline at end of file + m.c.add( + n[0] * (m.x - apex_point[0]) + + n[1] * (m.y - apex_point[1]) + + n[2] * (m.z - apex_point[2]) + >= 0 + ) + return m diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index 2f98f4a37bb..7d7d7f6d1ab 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -18,33 +18,34 @@ import pyomo.contrib.alternative_solutions.tests.test_cases as tc import pdb -mip_solver = 'gurobi_appsi' -#mip_solver = 'gurobi' +mip_solver = "gurobi_appsi" +# mip_solver = 'gurobi' + class TestOBBTUnit(unittest.TestCase): - - #TODO: Add more test cases - ''' + + # TODO: Add more test cases + """ So far I have added test cases for the feasibility problems, we should test cases where we put objective constraints in as well based on the absolute and relative difference. - + Add a case where bounds are only found for a subset of variables. - + Try cases where refine_discrete_bounds is set to true to ensure that new constraints are added to refine the bounds. I created the problem get_implied_bound_ip to facilitate this - + Check to see that warm starting works for a MIP and MILP case - + We should also check that warmstarting and refining bounds works for gurobi and appsi_gurobi - + We should pass at least one solver_options to ensure this work (e.g. time limit) - + I only looked at linear cases here, so you think others are worth testing, some simple non-linear (convex) cases? - - ''' - + + """ + def test_obbt_continuous(self): - '''Check that the correct bounds are found for a continuous problem.''' + """Check that the correct bounds are found for a continuous problem.""" m = tc.get_2d_diamond_problem() results = obbt_analysis(m, solver=mip_solver) self.assertEqual(results.keys(), m.continuous_bounds.keys()) @@ -52,33 +53,32 @@ def test_obbt_continuous(self): assert_array_almost_equal(bounds, m.continuous_bounds[var]) def test_mip_rel_objective(self): - '''Check that relative mip gap constraints are added for a mip with indexed vars and constraints''' + """Check that relative mip gap constraints are added for a mip with indexed vars and constraints""" m = tc.get_indexed_pentagonal_pyramid_mip() results = obbt_analysis(m, rel_opt_gap=0.5) self.assertAlmostEqual(m._obbt.optimality_tol_rel.lb, 2.5) - def test_mip_abs_objective(self): - '''Check that absolute mip gap constraints are added''' + """Check that absolute mip gap constraints are added""" m = tc.get_pentagonal_pyramid_mip() results = obbt_analysis(m, abs_opt_gap=1.99) self.assertAlmostEqual(m._obbt.optimality_tol_abs.lb, 3.01) def test_obbt_warmstart(self): - '''Check that warmstarting works.''' + """Check that warmstarting works.""" m = tc.get_2d_diamond_problem() m.x.value = 0 m.y.value = 0 - results = obbt_analysis(m, solver=mip_solver, warmstart = True, tee = True) + results = obbt_analysis(m, solver=mip_solver, warmstart=True, tee=True) self.assertEqual(results.keys(), m.continuous_bounds.keys()) for var, bounds in results.items(): - assert_array_almost_equal(bounds, m.continuous_bounds[var]) + assert_array_almost_equal(bounds, m.continuous_bounds[var]) def test_obbt_mip(self): - '''Check that bound tightening only occurs for continuous variables - that can be tightened.''' + """Check that bound tightening only occurs for continuous variables + that can be tightened.""" m = tc.get_bloated_pentagonal_pyramid_mip() - results = obbt_analysis(m, solver=mip_solver, tee = True) + results = obbt_analysis(m, solver=mip_solver, tee=True) bounds_tightened = False bounds_not_tightned = False for var, bounds in results.items(): @@ -94,7 +94,7 @@ def test_obbt_mip(self): self.assertTrue(bounds_not_tightened) def test_obbt_unbounded(self): - '''Check that the correct bounds are found for an unbounded problem.''' + """Check that the correct bounds are found for an unbounded problem.""" m = tc.get_2d_unbounded_problem() results = obbt_analysis(m, solver=mip_solver) self.assertEqual(results.keys(), m.continuous_bounds.keys()) @@ -102,9 +102,9 @@ def test_obbt_unbounded(self): assert_array_almost_equal(bounds, m.continuous_bounds[var]) def test_bound_tightening(self): - ''' - Check that the correct bounds are found for a discrete problem where - more restrictive bounds are implied by the constraints.''' + """ + Check that the correct bounds are found for a discrete problem where + more restrictive bounds are implied by the constraints.""" m = tc.get_implied_bound_ip() results = obbt_analysis(m, solver=mip_solver) self.assertEqual(results.keys(), m.var_bounds.keys()) @@ -112,10 +112,10 @@ def test_bound_tightening(self): assert_array_almost_equal(bounds, m.var_bounds[var]) def test_bound_refinement(self): - ''' - Check that the correct bounds are found for a discrete problem where + """ + Check that the correct bounds are found for a discrete problem where more restrictive bounds are implied by the constraints and constraints - are added.''' + are added.""" m = tc.get_implied_bound_ip() results = obbt_analysis(m, solver=mip_solver, refine_discrete_bounds=True) for var, bounds in results.items(): @@ -123,13 +123,14 @@ def test_bound_refinement(self): self.assertTrue(hasattr(m._obbt, var.name + "_lb")) if m.var_bounds[var][1] < var.ub: self.assertTrue(hasattr(m._obbt, var.name + "_ub")) - + def test_obbt_infeasible(self): - '''Check that code catches cases where the problem is infeasible.''' + """Check that code catches cases where the problem is infeasible.""" m = tc.get_2d_diamond_problem() - m.infeasible_constraint = pe.Constraint(expr=m.x>=10) + m.infeasible_constraint = pe.Constraint(expr=m.x >= 10) with self.assertRaises(Exception): obbt_analysis(m, solver=mip_solver) -if __name__ == '__main__': - unittest.main() \ No newline at end of file + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py index d2fc9f061f8..d253ac6d98d 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py @@ -20,31 +20,33 @@ # ___________________________________________________________________________ -mip_solver = 'gurobi_appsi' -#mip_solver = 'gurobi' +mip_solver = "gurobi_appsi" +# mip_solver = 'gurobi' + class TestShiftedIP(unittest.TestCase): - + def test_mip_abs_objective(self): - '''COMMENT''' + """COMMENT""" m = tc.get_indexed_pentagonal_pyramid_mip() m.x.domain = pe.Reals - opt = pe.SolverFactory('gurobi') - old_results = opt.solve(m, tee = True) + opt = pe.SolverFactory("gurobi") + old_results = opt.solve(m, tee=True) old_obj = pe.value(m.o) new_model = shifted_lp.get_shifted_linear_model(m) - new_results = opt.solve(new_model, tee = True) + new_results = opt.solve(new_model, tee=True) new_obj = pe.value(new_model.objective) self.assertAlmostEqual(old_obj, new_obj) - + def test_polyhedron(self): m = tc.get_3d_polyhedron_problem() - opt = pe.SolverFactory('gurobi') - old_results = opt.solve(m, tee = True) + opt = pe.SolverFactory("gurobi") + old_results = opt.solve(m, tee=True) old_obj = pe.value(m.o) new_model = shifted_lp.get_shifted_linear_model(m) - new_results = opt.solve(new_model, tee = True) + new_results = opt.solve(new_model, tee=True) new_obj = pe.value(new_model.objective) -if __name__ == '__main__': - unittest.main() \ No newline at end of file + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index 5bcef9a9792..dc12e1617b5 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -20,29 +20,27 @@ from collections import Counter import pdb -mip_solver = 'gurobi' + +@unittest.pytest.mark.solver("gurobi") class TestSolnPoolUnit(unittest.TestCase): - #TODO: Add test cases. - ''' - Cases to cover: - MIP feasability, - MILP feasability, - LP feasability (for an LP just one solution should be returned since gurobi cant enumerate over continuous vars) - For a MIP or MILP we should check that num solutions, rel_opt_gap and abs_opt_gap work - Pass at least one solver option to make sure that work, e.g. time limit + """ + Cases to cover: - I have the triagnle problem which should be easy to test with, there is - also the knapsack problem. For the LP case we can use the 2d diamond problem - I don't really have MILP case worked out though, so we may need to create one. - - We probably also need a utility to check that a two sets of solutions are the same. + LP feasability (for an LP just one solution should be returned since gurobi cannot enumerate over continuous vars) + + Pass at least one solver option to make sure that work, e.g. time limit + + We need a utility to check that a two sets of solutions are the same. Maybe this should be an AOS utility since it may be a thing we will want to do often. - ''' + """ def test_ip_feasibility(self): - '''Check that the correct number of alternate solutions are found for - each objective value in an ip with known solutions''' + """ + Enumerate all solutions for an ip: triangle_ip. + + Check that the correct number of alternate solutions are found. + """ m = tc.get_triangle_ip() results = sp.gurobi_generate_solutions(m, 100) objectives = [round(result.objective[1], 2) for result in results] @@ -50,32 +48,70 @@ def test_ip_feasibility(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) + def test_ip_num_solutions(self): + """ + Enumerate 8 solutions for an ip: triangle_ip. + + Check that the correct number of alternate solutions are found. + """ + m = tc.get_triangle_ip() + results = sp.gurobi_generate_solutions(m, 8) + for r in results: + print(r) + assert len(results) == 8 + objectives = [round(result.objective[1], 2) for result in results] + actual_solns_by_obj = [6, 2] + unique_solns_by_obj = [val for val in Counter(objectives).values()] + assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) + def test_mip_feasibility(self): - ''' - Check that the correct number of alternate solutions are found for - each objective value in a mip with known solutions''' + """ + Enumerate all solutions for a mip: indexed_pentagonal_pyramid_mip. + + Check that the correct number of alternate solutions are found. + """ m = tc.get_indexed_pentagonal_pyramid_mip() - results = sp.gurobi_generate_solutions(m, 100, tee = True) + results = sp.gurobi_generate_solutions(m, 100) objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) def test_mip_rel_feasibility(self): - ''' - Check that relative mip gap constraints are added and the correct - number of alternative solutions are found''' + """ + Enumerate solutions for a mip: indexed_pentagonal_pyramid_mip. + + Check that only solutions within a relative tolerance of 0.2 are + found. + """ + m = tc.get_pentagonal_pyramid_mip() + results = sp.gurobi_generate_solutions(m, 100, rel_opt_gap=0.2) + objectives = [round(result.objective[1], 2) for result in results] + actual_solns_by_obj = m.num_ranked_solns[0:2] + unique_solns_by_obj = [val for val in Counter(objectives).values()] + assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) + + def test_mip_rel_feasibility_options(self): + """ + Enumerate solutions for a mip: indexed_pentagonal_pyramid_mip. + + Check that only solutions within a relative tolerance of 0.2 are + found. + """ m = tc.get_pentagonal_pyramid_mip() - results = sp.gurobi_generate_solutions(m, 100, rel_opt_gap=.2) + results = sp.gurobi_generate_solutions(m, 100, solver_options={"PoolGap":0.2}) objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns[0:2] unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) def test_mip_abs_feasibility(self): - ''' - Check that absolute mip gap constraints are added and the correct - number of alternative solutions are found''' + """ + Enumerate solutions for a mip: indexed_pentagonal_pyramid_mip. + + Check that only solutions within an absolute tolerance of 1.99 are + found. + """ m = tc.get_pentagonal_pyramid_mip() results = sp.gurobi_generate_solutions(m, 100, abs_opt_gap=1.99) objectives = [round(result.objective[1], 2) for result in results] @@ -83,5 +119,20 @@ def test_mip_abs_feasibility(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) -if __name__ == '__main__': + def test_mip_no_time(self): + """ + Enumerate solutions for a mip: indexed_pentagonal_pyramid_mip. + + Check that no solutions are returned with a timelimit of 0. + """ + m = tc.get_pentagonal_pyramid_mip() + results = sp.gurobi_generate_solutions(m, 100, solver_options={"TimeLimit":0.0}) + assert len(results) == 0 + #objectives = [round(result.objective[1], 2) for result in results] + #actual_solns_by_obj = m.num_ranked_solns[0:2] + #unique_solns_by_obj = [val for val in Counter(objectives).values()] + #assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) + + +if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/alternative_solutions/tests/test_solution.py b/pyomo/contrib/alternative_solutions/tests/test_solution.py index 19c35c8ca5c..cc9cb02186b 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solution.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solution.py @@ -14,50 +14,54 @@ import pyomo.contrib.alternative_solutions.aos_utils as au import pyomo.contrib.alternative_solutions.solution as sol -mip_solver = 'gurobi' +mip_solver = "gurobi" + class TestSolutionUnit(unittest.TestCase): def get_model(self): - ''' + """ Simple model with all variable types and fixed variables to test the Solution code. - ''' + """ m = pe.ConcreteModel() m.x = pe.Var(domain=pe.NonNegativeReals) m.y = pe.Var(domain=pe.Binary) m.z = pe.Var(domain=pe.NonNegativeIntegers) m.f = pe.Var(domain=pe.Reals) - + m.f.fix(1) m.obj = pe.Objective(expr=m.x + m.y + m.z + m.f, sense=pe.maximize) - + m.con_x = pe.Constraint(expr=m.x <= 1.5) m.con_y = pe.Constraint(expr=m.y <= 1) m.con_z = pe.Constraint(expr=m.z <= 3) return m - - @unittest.skipUnless(pe.SolverFactory(mip_solver).available(), - "MIP solver not available") + + @unittest.skipUnless( + pe.SolverFactory(mip_solver).available(), "MIP solver not available" + ) def test_solution(self): - ''' + """ Create a Solution Object, call its functions, and ensure the correct data is returned. - ''' + """ model = self.get_model() opt = pe.SolverFactory(mip_solver) opt.solve(model) all_vars = au.get_model_variables(model, include_fixed=True) - + solution = sol.Solution(model, all_vars, include_fixed=False) solution.pprint() - + solution = sol.Solution(model, all_vars) solution.pprint(round_discrete=True) - - sol_val = solution.get_variable_name_values(include_fixed=True, - round_discrete=True) - self.assertEqual(set(sol_val.keys()), {'x','y','z','f'}) - self.assertEqual(set(solution.get_fixed_variable_names()), {'f'}) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file + + sol_val = solution.get_variable_name_values( + include_fixed=True, round_discrete=True + ) + self.assertEqual(set(sol_val.keys()), {"x", "y", "z", "f"}) + self.assertEqual(set(solution.get_fixed_variable_names()), {"f"}) + + +if __name__ == "__main__": + unittest.main() From bb2423724c2346b25ad4843a84ba201f92da4a3c Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 9 Apr 2024 10:43:23 -0600 Subject: [PATCH 1165/3044] Two changes 1. Adding quiet flag to suppress output 2. Adding comments to suppres coverage failures --- pyomo/contrib/alternative_solutions/aos_utils.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index 34572475d25..b37ebee6229 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -114,8 +114,8 @@ def _get_random_direction(num_dimensions): samples_norm = norm(samples) if samples_norm > min_norm: return samples / samples_norm - idx += 1 - raise Exception( + idx += 1 # pragma: no cover + raise Exception( # pragma: no cover ( "Generated {} sequential Gaussian draws with a norm of " "less than {}.".format(iterations, min_norm) @@ -152,6 +152,7 @@ def get_model_variables( include_binary=True, include_integer=True, include_fixed=False, + quiet=True, ): """ Gathers and returns all variables or a subset of variables from a Pyomo @@ -262,9 +263,10 @@ def get_model_variables( include_integer, include_fixed, ) - else: - print( - ("No variables added for unrecognized component {}.").format(comp) - ) + else: #pragma: no cover + if not quiet: + print( + ("No variables added for unrecognized component {}.").format(comp) + ) return variable_set From 654a2d2d05481ed98cf9df6fb1a4d497936c279e Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 9 Apr 2024 10:44:25 -0600 Subject: [PATCH 1166/3044] Adding test to improve coverage --- .../alternative_solutions/tests/test_aos_utils.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py index 3a1ab6758f6..a7164cf6157 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py +++ b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py @@ -18,6 +18,7 @@ class TestAOSUtilsUnit(unittest.TestCase): + def get_multiple_objective_model(self): """Create a simple model with three objectives.""" m = pe.ConcreteModel() @@ -257,7 +258,7 @@ def test_get_specific_vars(self): ) self.assertEqual(var, specific_vars) - def test_get_block_vars(self): + def test_get_block_vars1(self): """ Check that all variables from block are gathered (without descending into subblocks). @@ -270,6 +271,17 @@ def test_get_block_vars(self): ) self.assertEqual(var, specific_vars) + def test_get_block_vars2(self): + """ + Check that all variables from block are gathered (without + descending into subblocks). + """ + m = self.get_var_model() + components = [m.b1] + var = au.get_model_variables(m, components=components) + specific_vars = ComponentSet([m.b1.y, m.b1.sb1.y_l[0]]) + self.assertEqual(var, specific_vars) + def test_get_constraint_vars(self): """Check that all variables constraints and objectives are gathered.""" m = self.get_var_model() From a8c4fcca39af633993e1873e4cba6311b7d41410 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 9 Apr 2024 10:47:09 -0600 Subject: [PATCH 1167/3044] Testing output with strings --- .../tests/test_solution.py | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/tests/test_solution.py b/pyomo/contrib/alternative_solutions/tests/test_solution.py index cc9cb02186b..42ea0019a68 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solution.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solution.py @@ -18,6 +18,7 @@ class TestSolutionUnit(unittest.TestCase): + def get_model(self): """ Simple model with all variable types and fixed variables to test the @@ -51,10 +52,35 @@ def test_solution(self): all_vars = au.get_model_variables(model, include_fixed=True) solution = sol.Solution(model, all_vars, include_fixed=False) - solution.pprint() + sol_str = """{ + "fixed_variables": [ + "f" + ], + "objective": "obj", + "objective_value": 6.5, + "solution": { + "x": 1.5, + "y": 1, + "z": 3 + } +}""" + assert str(solution) == sol_str solution = sol.Solution(model, all_vars) - solution.pprint(round_discrete=True) + sol_str = """{ + "fixed_variables": [ + "f" + ], + "objective": "obj", + "objective_value": 6.5, + "solution": { + "f": 1, + "x": 1.5, + "y": 1, + "z": 3 + } +}""" + assert solution.to_string(round_discrete=True) == sol_str sol_val = solution.get_variable_name_values( include_fixed=True, round_discrete=True From d10da793565ca0b3e9dea9731a69ac37b6f9cb43 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 9 Apr 2024 10:52:25 -0600 Subject: [PATCH 1168/3044] Adding solnpool tests Plus misc edits to shifted documentation --- .../alternative_solutions/shifted_lp.py | 2 +- .../contrib/alternative_solutions/solnpool.py | 11 +++++------ .../contrib/alternative_solutions/solution.py | 2 +- .../tests/test_shifted_lp.py | 18 +++--------------- .../tests/test_solnpool.py | 9 ++------- 5 files changed, 12 insertions(+), 30 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/shifted_lp.py b/pyomo/contrib/alternative_solutions/shifted_lp.py index 2f111ade877..4014e151640 100644 --- a/pyomo/contrib/alternative_solutions/shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/shifted_lp.py @@ -40,7 +40,7 @@ def get_shifted_linear_model(model, block=None): """ Converts an (MI)LP with bounded (discrete and) continuous variables (l <= x <= u) into a standard form where where all continuous variables - are non-negative reals and all contraints are equalities. For a pure LP of + are non-negative reals and all constraints are equalities. For a pure LP of the form, min/max cx diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 8bbf48a8f08..ba0a57b5632 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -23,7 +23,7 @@ def gurobi_generate_solutions( abs_opt_gap=None, solver_options={}, tee=False, - quiet=False, + quiet=True, ): """ Finds alternative optimal solutions for discrete variables using Gurobi's @@ -61,10 +61,11 @@ def gurobi_generate_solutions( # Setup gurobi # opt = appsi.solvers.Gurobi() - if not opt.available(): + if not opt.available(): #pragma: no cover return [] opt.config.stream_solver = tee + opt.config.load_solution = False opt.gurobi_options["PoolSolutions"] = num_solutions opt.gurobi_options["PoolSearchMode"] = 2 if rel_opt_gap is not None: @@ -82,17 +83,15 @@ def gurobi_generate_solutions( if not quiet: print( ( - "Model cannot be solved, SolverStatus = {}, " + "Model cannot be solved, " "TerminationCondition = {}" - ).format(status.value, condition.value) + ).format(condition.value) ) return [] # # Collect solutions # solution_count = opt.get_model_attr("SolCount") - if not quiet: - print("{} solutions found.".format(solution_count)) variables = aos_utils.get_model_variables(model, "all", include_fixed=True) solutions = [] for i in range(solution_count): diff --git a/pyomo/contrib/alternative_solutions/solution.py b/pyomo/contrib/alternative_solutions/solution.py index 4a40f7916a7..7909eeecff7 100644 --- a/pyomo/contrib/alternative_solutions/solution.py +++ b/pyomo/contrib/alternative_solutions/solution.py @@ -81,7 +81,7 @@ def pprint(self, round_discrete=True, sort_keys=True, indent=4): rounded_discrete : boolean If True, then round discrete variable values before printing. """ - print(self.to_string(round_discrete=round_discrete, sort_keys=sort_keys, indent=indent)) + print(self.to_string(round_discrete=round_discrete, sort_keys=sort_keys, indent=indent)) #pragma: no cover def to_string(self, round_discrete=True, sort_keys=True, indent=4): return json.dumps(self.to_dict(round_discrete=round_discrete), sort_keys=sort_keys, indent=indent) diff --git a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py index d253ac6d98d..682eee6618b 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py @@ -2,22 +2,8 @@ import pyomo.environ as pe import pyomo.common.unittest as unittest - import pyomo.contrib.alternative_solutions.tests.test_cases as tc from pyomo.contrib.alternative_solutions import shifted_lp -import pdb - - -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ mip_solver = "gurobi_appsi" @@ -27,7 +13,9 @@ class TestShiftedIP(unittest.TestCase): def test_mip_abs_objective(self): - """COMMENT""" + """ + COMMENT + """ m = tc.get_indexed_pentagonal_pyramid_mip() m.x.domain = pe.Reals opt = pe.SolverFactory("gurobi") diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index dc12e1617b5..4a6211c01c1 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -56,8 +56,6 @@ def test_ip_num_solutions(self): """ m = tc.get_triangle_ip() results = sp.gurobi_generate_solutions(m, 8) - for r in results: - print(r) assert len(results) == 8 objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = [6, 2] @@ -126,12 +124,9 @@ def test_mip_no_time(self): Check that no solutions are returned with a timelimit of 0. """ m = tc.get_pentagonal_pyramid_mip() - results = sp.gurobi_generate_solutions(m, 100, solver_options={"TimeLimit":0.0}) + # Use quiet=False to test error message + results = sp.gurobi_generate_solutions(m, 100, solver_options={"TimeLimit":0.0}, quiet=False) assert len(results) == 0 - #objectives = [round(result.objective[1], 2) for result in results] - #actual_solns_by_obj = m.num_ranked_solns[0:2] - #unique_solns_by_obj = [val for val in Counter(objectives).values()] - #assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) if __name__ == "__main__": From c49af49dcf71a9d27f73819e239f6d07ac55e51a Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 9 Apr 2024 11:36:08 -0600 Subject: [PATCH 1169/3044] Reformatting with black --- pyomo/contrib/alternative_solutions/aos_utils.py | 10 ++++++---- pyomo/contrib/alternative_solutions/solnpool.py | 9 ++++----- pyomo/contrib/alternative_solutions/solution.py | 13 ++++++++++--- .../alternative_solutions/tests/test_solnpool.py | 9 +++++---- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index b37ebee6229..81fae824fa3 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -114,8 +114,8 @@ def _get_random_direction(num_dimensions): samples_norm = norm(samples) if samples_norm > min_norm: return samples / samples_norm - idx += 1 # pragma: no cover - raise Exception( # pragma: no cover + idx += 1 # pragma: no cover + raise Exception( # pragma: no cover ( "Generated {} sequential Gaussian draws with a norm of " "less than {}.".format(iterations, min_norm) @@ -263,10 +263,12 @@ def get_model_variables( include_integer, include_fixed, ) - else: #pragma: no cover + else: # pragma: no cover if not quiet: print( - ("No variables added for unrecognized component {}.").format(comp) + ("No variables added for unrecognized component {}.").format( + comp + ) ) return variable_set diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index ba0a57b5632..09dbf6a540e 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -61,7 +61,7 @@ def gurobi_generate_solutions( # Setup gurobi # opt = appsi.solvers.Gurobi() - if not opt.available(): #pragma: no cover + if not opt.available(): # pragma: no cover return [] opt.config.stream_solver = tee @@ -82,10 +82,9 @@ def gurobi_generate_solutions( if not (condition == appsi.base.TerminationCondition.optimal): if not quiet: print( - ( - "Model cannot be solved, " - "TerminationCondition = {}" - ).format(condition.value) + ("Model cannot be solved, " "TerminationCondition = {}").format( + condition.value + ) ) return [] # diff --git a/pyomo/contrib/alternative_solutions/solution.py b/pyomo/contrib/alternative_solutions/solution.py index 7909eeecff7..6c61fa17e73 100644 --- a/pyomo/contrib/alternative_solutions/solution.py +++ b/pyomo/contrib/alternative_solutions/solution.py @@ -81,10 +81,18 @@ def pprint(self, round_discrete=True, sort_keys=True, indent=4): rounded_discrete : boolean If True, then round discrete variable values before printing. """ - print(self.to_string(round_discrete=round_discrete, sort_keys=sort_keys, indent=indent)) #pragma: no cover + print( + self.to_string( + round_discrete=round_discrete, sort_keys=sort_keys, indent=indent + ) + ) # pragma: no cover def to_string(self, round_discrete=True, sort_keys=True, indent=4): - return json.dumps(self.to_dict(round_discrete=round_discrete), sort_keys=sort_keys, indent=indent) + return json.dumps( + self.to_dict(round_discrete=round_discrete), + sort_keys=sort_keys, + indent=indent, + ) def to_dict(self, round_discrete=True): ans = {} @@ -139,4 +147,3 @@ def _round_variable_value(self, variable, value, round_discrete=True): Returns a rounded value unless the variable is discrete or rounded_discrete is False. """ return value if not round_discrete or variable.is_continuous() else round(value) - diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index 4a6211c01c1..9831a3317d3 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -21,12 +21,11 @@ import pdb - @unittest.pytest.mark.solver("gurobi") class TestSolnPoolUnit(unittest.TestCase): """ Cases to cover: - + LP feasability (for an LP just one solution should be returned since gurobi cannot enumerate over continuous vars) Pass at least one solver option to make sure that work, e.g. time limit @@ -97,7 +96,7 @@ def test_mip_rel_feasibility_options(self): found. """ m = tc.get_pentagonal_pyramid_mip() - results = sp.gurobi_generate_solutions(m, 100, solver_options={"PoolGap":0.2}) + results = sp.gurobi_generate_solutions(m, 100, solver_options={"PoolGap": 0.2}) objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns[0:2] unique_solns_by_obj = [val for val in Counter(objectives).values()] @@ -125,7 +124,9 @@ def test_mip_no_time(self): """ m = tc.get_pentagonal_pyramid_mip() # Use quiet=False to test error message - results = sp.gurobi_generate_solutions(m, 100, solver_options={"TimeLimit":0.0}, quiet=False) + results = sp.gurobi_generate_solutions( + m, 100, solver_options={"TimeLimit": 0.0}, quiet=False + ) assert len(results) == 0 From 7614ff0367558558d27bb48d9ea1393278cd9291 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 9 Apr 2024 14:44:15 -0600 Subject: [PATCH 1170/3044] Overriding finalizeResult to account for the fact that mult might not be constant --- pyomo/repn/linear.py | 31 ++++++++++++++++------------- pyomo/repn/linear_wrt.py | 14 +++++++++++++ pyomo/repn/tests/test_linear_wrt.py | 16 ++++++++++++++- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index ba08c7ef245..913b36f6f16 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -803,6 +803,22 @@ def exitNode(self, node, data): self, node, *data ) + def _factor_multiplier_into_linear_terms(self, ans, mult): + linear = ans.linear + zeros = [] + for vid, coef in linear.items(): + if coef: + linear[vid] = coef * mult + else: + zeros.append(vid) + for vid in zeros: + del linear[vid] + if ans.nonlinear is not None: + ans.nonlinear *= mult + if ans.constant: + ans.constant *= mult + ans.multiplier = 1 + def finalizeResult(self, result): ans = result[1] if ans.__class__ is self.Result: @@ -831,20 +847,7 @@ def finalizeResult(self, result): else: # mult not in {0, 1}: factor it into the constant, # linear coefficients, and nonlinear term - linear = ans.linear - zeros = [] - for vid, coef in linear.items(): - if coef: - linear[vid] = coef * mult - else: - zeros.append(vid) - for vid in zeros: - del linear[vid] - if ans.nonlinear is not None: - ans.nonlinear *= mult - if ans.constant: - ans.constant *= mult - ans.multiplier = 1 + self._factor_mult_into_linear_terms(ans, mult) return ans ans = self.Result() assert result[0] is _CONSTANT diff --git a/pyomo/repn/linear_wrt.py b/pyomo/repn/linear_wrt.py index 0d86528056c..46451d3d64c 100644 --- a/pyomo/repn/linear_wrt.py +++ b/pyomo/repn/linear_wrt.py @@ -10,6 +10,7 @@ # ___________________________________________________________________________ from pyomo.common.collections import ComponentSet +from pyomo.common.numeric_types import native_numeric_types from pyomo.core import Var from pyomo.core.expr.logical_expr import _flattened from pyomo.core.expr.numeric_expr import ( @@ -66,6 +67,7 @@ def _before_var(visitor, child): _before_child_dispatcher = MultiLevelLinearBeforeChildDispatcher() +# LinearSubsystemRepnVisitor class MultilevelLinearRepnVisitor(LinearRepnVisitor): def __init__(self, subexpression_cache, var_map, var_order, sorter, wrt): super().__init__(subexpression_cache, var_map, var_order, sorter) @@ -75,3 +77,15 @@ def beforeChild(self, node, child, child_idx): print("before child %s" % child) print(child.__class__) return _before_child_dispatcher[child.__class__](self, child) + + def finalizeResult(self, result): + ans = result[1] + if ans.__class__ is self.Result: + mult = ans.multiplier + if not mult.__class__ in native_numeric_types: + # mult is an expression--we should push it back into the other terms + self._factor_multiplier_into_linear_terms(ans, mult) + return ans + + # In all other cases, the base class implementation is correct + return super().finalizeResult(result) diff --git a/pyomo/repn/tests/test_linear_wrt.py b/pyomo/repn/tests/test_linear_wrt.py index 29c8f69ad03..384412b25ce 100644 --- a/pyomo/repn/tests/test_linear_wrt.py +++ b/pyomo/repn/tests/test_linear_wrt.py @@ -27,7 +27,6 @@ def test_walk_sum(self): m = self.make_model() e = m.x + m.y cfg = VisitorConfig() - print("constructing") visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) repn = visitor.walk_expression(e) @@ -38,3 +37,18 @@ def test_walk_sum(self): self.assertEqual(repn.linear[id(m.x)], 1) self.assertIs(repn.constant, m.y) self.assertEqual(repn.multiplier, 1) + + def test_bilinear_term(self): + m = self.make_model() + e = m.x * m.y + cfg = VisitorConfig() + visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + + repn = visitor.walk_expression(e) + + self.assertIsNone(repn.nonlinear) + self.assertEqual(len(repn.linear), 1) + self.assertIn(id(m.x), repn.linear) + self.assertIs(repn.linear[id(m.x)], m.y) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.multiplier, 1) From f162e9748df78f7bc38fbe615fc133c0c2d7cdd1 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 9 Apr 2024 14:57:53 -0600 Subject: [PATCH 1171/3044] Enable seeding of numpy RNG --- pyomo/contrib/alternative_solutions/aos_utils.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index 81fae824fa3..90843ed76c9 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -9,7 +9,8 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from numpy.random import normal +import numpy.random +#from numpy.random import normal from numpy.linalg import norm import pyomo.environ as pe @@ -100,17 +101,23 @@ def _add_objective_constraint( return objective_constraints +rng = numpy.random.default_rng(9283749387) + +def _set_numpy_rng(seed): + global rng + rng = numpy.random.default_rng(seed) + def _get_random_direction(num_dimensions): """ Get a unit vector of dimension num_dimensions by sampling from and normalizing a standard multivariate Gaussian distribution. """ - + global rng iterations = 1000 min_norm = 1e-4 idx = 0 while idx < iterations: - samples = normal(size=num_dimensions) + samples = rng.normal(size=num_dimensions) samples_norm = norm(samples) if samples_norm > min_norm: return samples / samples_norm From 28e31a09b97b87e07ba2c04616aaaa812af4e276 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 9 Apr 2024 14:58:27 -0600 Subject: [PATCH 1172/3044] Changes to enable top-level imports --- pyomo/contrib/alternative_solutions/solnpool.py | 9 +++++---- pyomo/contrib/alternative_solutions/solution.py | 9 +++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 09dbf6a540e..65743360596 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -9,15 +9,16 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import gurobipy import pyomo.environ as pe from pyomo.contrib import appsi -from pyomo.contrib.alternative_solutions import aos_utils, solution -import gurobipy -import pdb +import pyomo.contrib.alternative_solutions.aos_utils as aos_utils +from pyomo.contrib.alternative_solutions import Solution def gurobi_generate_solutions( model, + *, num_solutions=10, rel_opt_gap=None, abs_opt_gap=None, @@ -102,6 +103,6 @@ def gurobi_generate_solutions( # Pull the solution from the model into a Solution object, # and append to our list of solutions # - solutions.append(solution.Solution(model, variables)) + solutions.append(Solution(model, variables)) return solutions diff --git a/pyomo/contrib/alternative_solutions/solution.py b/pyomo/contrib/alternative_solutions/solution.py index 6c61fa17e73..68344dcbc07 100644 --- a/pyomo/contrib/alternative_solutions/solution.py +++ b/pyomo/contrib/alternative_solutions/solution.py @@ -72,6 +72,15 @@ def __init__(self, model, variable_list, include_fixed=True, objective=None): objective = aos_utils.get_active_objective(model) self.objective = (objective, pe.value(objective)) + @property + def objective_value(self): + """ + Returns + ------- + The value of the objective. + """ + return self.objective[1] + def pprint(self, round_discrete=True, sort_keys=True, indent=4): """ Print the solution variables and objective values. From c3ae9e789da38ed70eecfef316a60685c28e05b9 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 9 Apr 2024 14:58:40 -0600 Subject: [PATCH 1173/3044] Various changes needed to make tests work --- pyomo/contrib/alternative_solutions/balas.py | 119 +++++++++++-------- 1 file changed, 72 insertions(+), 47 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index 10e32feb114..7c4451acb22 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -1,21 +1,12 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - import pyomo.environ as pe from pyomo.common.collections import ComponentSet -from pyomo.contrib.alternative_solutions import aos_utils, solution +from pyomo.contrib.alternative_solutions import Solution +import pyomo.contrib.alternative_solutions.aos_utils as aos_utils def enumerate_binary_solutions( model, + *, num_solutions=10, variables="all", rel_opt_gap=None, @@ -24,6 +15,8 @@ def enumerate_binary_solutions( solver="gurobi", solver_options={}, tee=False, + quiet=True, + seed=None, ): """ Finds alternative optimal solutions for a binary problem using no-good @@ -59,6 +52,10 @@ def enumerate_binary_solutions( Solver option-value pairs to be passed to the solver. tee : boolean Boolean indicating that the solver output should be displayed. + quiet : boolean + Boolean indicating whether to suppress all output. + seed : int + Optional integer seed for the numpy random number generator Returns ------- @@ -66,8 +63,8 @@ def enumerate_binary_solutions( A list of Solution objects. [Solution] """ - - print("STARTING NO-GOOD CUT ANALYSIS") + if not quiet: #pragma: no cover + print("STARTING NO-GOOD CUT ANALYSIS") assert search_mode in [ "optimal", @@ -75,6 +72,9 @@ def enumerate_binary_solutions( "hamming", ], 'search mode must be "optimal", "random", or "hamming".' + if seed is not None: + aos_utils._set_numpy_rng(seed) + if variables == "all": binary_variables = aos_utils.get_model_variables( model, "all", include_continuous=False, include_integer=False @@ -84,25 +84,31 @@ def enumerate_binary_solutions( non_binary_variables = [] for var in variables: if var.is_binary(): - binary_variables.append(var) + binary_variables.add(var) else: non_binary_variables.append(var.name) if len(non_binary_variables) > 0: - print( - ( - "Warning: The following non-binary variables were included" - "in the variable list and will be ignored:" + if not quiet: + print( + ( + "Warning: The following non-binary variables were included" + "in the variable list and will be ignored:" + ) ) - ) - print(", ".join(non_binary_variables)) - all_variables = aos_utils.get_model_variables(model, "all", include_fixed=True) + print(", ".join(non_binary_variables)) + all_variables = aos_utils.get_model_variables(model, "all", include_fixed=True) orig_objective = aos_utils.get_active_objective(model) + # + # Setup solver + # opt = pe.SolverFactory(solver) for parameter, value in solver_options.items(): opt.options[parameter] = value - + # + # Appsi-specific configurations + # use_appsi = False if "appsi" in solver: use_appsi = True @@ -125,11 +131,15 @@ def enumerate_binary_solutions( opt.update_config.check_for_new_objective = False opt.update_config.update_objective = False - print("Peforming initial solve of model.") - results = opt.solve(model, tee=tee) + # + # Initial solve of the model + # + if not quiet: #pragma: no cover + print("Peforming initial solve of model.") + results = opt.solve(model, tee=tee, load_solutions=False) status = results.solver.status condition = results.solver.termination_condition - if condition != pe.TerminationCondition.optimal: + if not pe.check_optimal_termination(results): raise Exception( ( "No-good cut analysis cannot be applied, " @@ -138,12 +148,20 @@ def enumerate_binary_solutions( ).format(status.value, condition.value) ) + model.solutions.load_from(results) orig_objective_value = pe.value(orig_objective) - print("Found optimal solution, value = {}.".format(orig_objective_value)) - solutions = [solution.Solution(model, all_variables)] + if not quiet: #pragma: no cover + print("Found optimal solution, value = {}.".format(orig_objective_value)) + solutions = [Solution(model, all_variables, objective=orig_objective)] + # + # Return just this solution if there are no binary variables + # + if len(binary_variables) == 0: + return solutions aos_block = aos_utils._add_aos_block(model, name="_balas") - print("Added block {} to the model.".format(aos_block)) + if not quiet: #pragma: no cover + print("Added block {} to the model.".format(aos_block)) aos_block.no_good_cuts = pe.ConstraintList() aos_utils._add_objective_constraint( aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap @@ -172,7 +190,7 @@ def enumerate_binary_solutions( else: aos_block.hamming_objective = pe.Objective(expr=expr, sense=pe.maximize) - if search_mode == "random": + elif search_mode == "random": if hasattr(aos_block, "random_objective"): aos_block.del_component("random_objective") vector = aos_utils._get_random_direction(len(binary_variables)) @@ -183,35 +201,42 @@ def enumerate_binary_solutions( idx += 1 aos_block.random_objective = pe.Objective(expr=expr, sense=pe.maximize) - results = opt.solve(model, tee=tee) + results = opt.solve(model, tee=tee, load_solutions=False) status = results.solver.status condition = results.solver.termination_condition - if condition == pe.TerminationCondition.optimal: - orig_obj_val = pe.value(orig_objective) - print("Iteration {}: objective = {}".format(solution_number, orig_obj_val)) - solutions.append(solution.Solution(model, all_variables)) + if pe.check_optimal_termination(results): + model.solutions.load_from(results) + orig_obj_value = pe.value(orig_objective) + orig_obj_value = pe.value(orig_objective) + if not quiet: #pragma: no cover + print("Iteration {}: objective = {}".format(solution_number, orig_obj_value)) + solutions.append(Solution(model, all_variables, objective=orig_objective)) solution_number += 1 elif ( condition == pe.TerminationCondition.infeasibleOrUnbounded or condition == pe.TerminationCondition.infeasible ): - print( - "Iteration {}: Infeasible, no additional binary solutions.".format( - solution_number + if not quiet: #pragma: no cover + print( + "Iteration {}: Infeasible, no additional binary solutions.".format( + solution_number + ) ) - ) break - else: - print( - ( - "Iteration {}: Unexpected condition, SolverStatus = {}, " - "TerminationCondition = {}" - ).format(solution_number, status.value, condition.value) - ) + else: #pragma: no cover + if not quiet: + print( + ( + "Iteration {}: Unexpected condition, SolverStatus = {}, " + "TerminationCondition = {}" + ).format(solution_number, status.value, condition.value) + ) break aos_block.deactivate() orig_objective.activate() - print("COMPLETED NO-GOOD CUT ANALYSIS") + + if not quiet: #pragma: no cover + print("COMPLETED NO-GOOD CUT ANALYSIS") return solutions From 8f563e353b9399de0055a480a0519d33dc3faeb5 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 9 Apr 2024 14:59:01 -0600 Subject: [PATCH 1174/3044] Adding Balas tests Plus various misc test edits --- .../tests/test_aos_utils.py | 14 +-- .../alternative_solutions/tests/test_balas.py | 98 ++++++++++++++++--- .../alternative_solutions/tests/test_cases.py | 21 ++-- .../alternative_solutions/tests/test_obbt.py | 12 --- .../tests/test_shifted_lp.py | 4 - .../tests/test_solnpool.py | 33 ++----- .../tests/test_solution.py | 17 +--- 7 files changed, 106 insertions(+), 93 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py index a7164cf6157..19ddbc83725 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py +++ b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py @@ -1,19 +1,9 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - from numpy.linalg import norm import pyomo.environ as pe -from pyomo.common.collections import ComponentSet import pyomo.common.unittest as unittest +from pyomo.common.collections import ComponentSet + import pyomo.contrib.alternative_solutions.aos_utils as au diff --git a/pyomo/contrib/alternative_solutions/tests/test_balas.py b/pyomo/contrib/alternative_solutions/tests/test_balas.py index bcca6723823..1e469b2959f 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_balas.py +++ b/pyomo/contrib/alternative_solutions/tests/test_balas.py @@ -1,22 +1,20 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ +import pytest +from numpy.testing import assert_array_almost_equal +from collections import Counter import pyomo.environ as pe import pyomo.common.unittest as unittest +import pyomo.opt -import pyomo.contrib.alternative_solutions.balas +from pyomo.contrib.alternative_solutions import enumerate_binary_solutions import pyomo.contrib.alternative_solutions.tests.test_cases as tc -class TestBalasUnit(unittest.TestCase): +solvers = list(pyomo.opt.check_available_solvers('glpk', 'gurobi', 'appsi_gurobi')) +pytestmark = pytest.mark.parametrize("mip_solver", solvers) + +@unittest.pytest.mark.default +class TestBalasUnit: # TODO: Add test cases """ @@ -29,9 +27,81 @@ class TestBalasUnit(unittest.TestCase): """ - def test_(self): - pass + def test_ip_feasibility(self, mip_solver): + """ + Enumerate solutions for an ip: triangle_ip. + + Check that there is just one solution when the # of binary variables is 0. + """ + m = tc.get_triangle_ip() + results = enumerate_binary_solutions(m, num_solutions=100, solver=mip_solver) + assert len(results) == 1 + assert results[0].objective_value == pytest.approx(5) + + def test_no_time(self, mip_solver): + """ + Enumerate solutions for an ip: triangle_ip. + + Check that something sensible happens when the solver times out. + """ + m = tc.get_triangle_ip() + with pytest.raises(Exception): + results = enumerate_binary_solutions(m, num_solutions=100, solver=mip_solver, solver_options={"TimeLimit":0}) + + def test_knapsack_all(self, mip_solver): + """ + Enumerate solutions for a binary problem: knapsack + + """ + m = tc.get_aos_test_knapsack(1, weights=[3,4,6,5], values=[2,3,1,4], capacity=8) + results = enumerate_binary_solutions(m, num_solutions=100, solver=mip_solver) + objectives = list(sorted((round(result.objective[1], 2) for result in results), reverse=True)) + assert_array_almost_equal(objectives, m.ranked_solution_values) + unique_solns_by_obj = [val for val in Counter(objectives).values()] + assert_array_almost_equal(unique_solns_by_obj, m.num_ranked_solns) + + def test_knapsack_x0_x1(self, mip_solver): + """ + Enumerate solutions for a binary problem: knapsack + + Check that we only see 4 solutions that enumerate alternatives of x[1] and x[1] + """ + m = tc.get_aos_test_knapsack(1, weights=[3,4,6,5], values=[2,3,1,4], capacity=8) + results = enumerate_binary_solutions(m, num_solutions=100, solver=mip_solver, variables=[m.x[0], m.x[1]]) + objectives = list(sorted((round(result.objective[1], 2) for result in results), reverse=True)) + assert_array_almost_equal(objectives, [6,5,4,3]) + unique_solns_by_obj = [val for val in Counter(objectives).values()] + assert_array_almost_equal(unique_solns_by_obj, [1,1,1,1]) + + def test_knapsack_optimal_3(self, mip_solver): + """ + Enumerate solutions for a binary problem: knapsack + + """ + m = tc.get_aos_test_knapsack(1, weights=[3,4,6,5], values=[2,3,1,4], capacity=8) + results = enumerate_binary_solutions(m, num_solutions=3, solver=mip_solver) + objectives = list(sorted((round(result.objective[1], 2) for result in results), reverse=True)) + assert_array_almost_equal(objectives, m.ranked_solution_values[:3]) + + def test_knapsack_hamming_3(self, mip_solver): + """ + Enumerate solutions for a binary problem: knapsack + + """ + m = tc.get_aos_test_knapsack(1, weights=[3,4,6,5], values=[2,3,1,4], capacity=8) + results = enumerate_binary_solutions(m, num_solutions=3, solver=mip_solver, search_mode="hamming") + objectives = list(sorted((round(result.objective[1], 2) for result in results), reverse=True)) + assert_array_almost_equal(objectives, [6, 3, 1]) + + def test_knapsack_random_3(self, mip_solver): + """ + Enumerate solutions for a binary problem: knapsack + """ + m = tc.get_aos_test_knapsack(1, weights=[3,4,6,5], values=[2,3,1,4], capacity=8) + results = enumerate_binary_solutions(m, num_solutions=3, solver=mip_solver, search_mode="random", seed=1118798374) + objectives = list(sorted((round(result.objective[1], 2) for result in results), reverse=True)) + assert_array_almost_equal(objectives, [6, 4, 1]) if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index ef6b67fb742..f7c3d97659f 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -1,18 +1,8 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - from itertools import product from math import ceil, floor from collections import Counter import numpy as np + import pyomo.environ as pe """ @@ -239,12 +229,12 @@ def get_implied_bound_ip(): return m -def get_aos_test_knapsack(var_max, weights, values, capacity_fraction): +def get_aos_test_knapsack(var_max, weights, values, capacity=None, capacity_fraction=1.0): """ Creates a knapsack problem, given arrays of weights and values, and returns all feasible solutions. The capacity represents the percent of the total max weight that can be selected (sum weights * var_max). The var_max - parameter sets the upper bound on all variables, teh max number of times + parameter sets the upper bound on all variables, the max number of times they can be selected. """ assert len(weights) == len(values), "weights and values must be the same length." @@ -253,7 +243,8 @@ def get_aos_test_knapsack(var_max, weights, values, capacity_fraction): ), "capacity_fraction must be between 0 and 1." num_vars = len(weights) - capacity = sum(weights) * var_max * capacity_fraction + if capacity is None: + capacity = sum(weights) * var_max * capacity_fraction m = pe.ConcreteModel() m.i = pe.RangeSet(0, num_vars - 1) @@ -275,6 +266,8 @@ def get_aos_test_knapsack(var_max, weights, values, capacity_fraction): if np.dot(sol, weights) <= capacity: feasible_sols.append((sol, np.dot(sol, values))) feasible_sols = sorted(feasible_sols, key=lambda sol: sol[1], reverse=True) + m.ranked_solution_values = list(sorted([v for x,v in feasible_sols], reverse=True)) + m.num_ranked_solns = list(Counter([v for x,v in feasible_sols]).values()) return m diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index 7d7d7f6d1ab..cdf7a9f5225 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -1,14 +1,3 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - from numpy.testing import assert_array_almost_equal import pyomo.environ as pe @@ -16,7 +5,6 @@ from pyomo.contrib.alternative_solutions.obbt import obbt_analysis import pyomo.contrib.alternative_solutions.tests.test_cases as tc -import pdb mip_solver = "gurobi_appsi" # mip_solver = 'gurobi' diff --git a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py index 682eee6618b..ac7b96b5547 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py @@ -6,10 +6,6 @@ from pyomo.contrib.alternative_solutions import shifted_lp -mip_solver = "gurobi_appsi" -# mip_solver = 'gurobi' - - class TestShiftedIP(unittest.TestCase): def test_mip_abs_objective(self): diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index 9831a3317d3..0ac9c1f1334 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -1,24 +1,11 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import unittest from numpy.testing import assert_array_almost_equal +from collections import Counter import pyomo.environ as pe import pyomo.common.unittest as unittest -import pyomo.contrib.alternative_solutions.solnpool as sp +from pyomo.contrib.alternative_solutions import gurobi_generate_solutions import pyomo.contrib.alternative_solutions.tests.test_cases as tc -from collections import Counter -import pdb @unittest.pytest.mark.solver("gurobi") @@ -41,7 +28,7 @@ def test_ip_feasibility(self): Check that the correct number of alternate solutions are found. """ m = tc.get_triangle_ip() - results = sp.gurobi_generate_solutions(m, 100) + results = gurobi_generate_solutions(m, num_solutions=100) objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns unique_solns_by_obj = [val for val in Counter(objectives).values()] @@ -54,7 +41,7 @@ def test_ip_num_solutions(self): Check that the correct number of alternate solutions are found. """ m = tc.get_triangle_ip() - results = sp.gurobi_generate_solutions(m, 8) + results = gurobi_generate_solutions(m, num_solutions=8) assert len(results) == 8 objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = [6, 2] @@ -68,7 +55,7 @@ def test_mip_feasibility(self): Check that the correct number of alternate solutions are found. """ m = tc.get_indexed_pentagonal_pyramid_mip() - results = sp.gurobi_generate_solutions(m, 100) + results = gurobi_generate_solutions(m, num_solutions=100) objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns unique_solns_by_obj = [val for val in Counter(objectives).values()] @@ -82,7 +69,7 @@ def test_mip_rel_feasibility(self): found. """ m = tc.get_pentagonal_pyramid_mip() - results = sp.gurobi_generate_solutions(m, 100, rel_opt_gap=0.2) + results = gurobi_generate_solutions(m, num_solutions=100, rel_opt_gap=0.2) objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns[0:2] unique_solns_by_obj = [val for val in Counter(objectives).values()] @@ -96,7 +83,7 @@ def test_mip_rel_feasibility_options(self): found. """ m = tc.get_pentagonal_pyramid_mip() - results = sp.gurobi_generate_solutions(m, 100, solver_options={"PoolGap": 0.2}) + results = gurobi_generate_solutions(m, num_solutions=100, solver_options={"PoolGap": 0.2}) objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns[0:2] unique_solns_by_obj = [val for val in Counter(objectives).values()] @@ -110,7 +97,7 @@ def test_mip_abs_feasibility(self): found. """ m = tc.get_pentagonal_pyramid_mip() - results = sp.gurobi_generate_solutions(m, 100, abs_opt_gap=1.99) + results = gurobi_generate_solutions(m, num_solutions=100, abs_opt_gap=1.99) objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns[0:3] unique_solns_by_obj = [val for val in Counter(objectives).values()] @@ -124,8 +111,8 @@ def test_mip_no_time(self): """ m = tc.get_pentagonal_pyramid_mip() # Use quiet=False to test error message - results = sp.gurobi_generate_solutions( - m, 100, solver_options={"TimeLimit": 0.0}, quiet=False + results = gurobi_generate_solutions( + m, num_solutions=100, solver_options={"TimeLimit": 0.0}, quiet=False ) assert len(results) == 0 diff --git a/pyomo/contrib/alternative_solutions/tests/test_solution.py b/pyomo/contrib/alternative_solutions/tests/test_solution.py index 42ea0019a68..fe4b84fa1b1 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solution.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solution.py @@ -1,18 +1,7 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - import pyomo.environ as pe import pyomo.common.unittest as unittest import pyomo.contrib.alternative_solutions.aos_utils as au -import pyomo.contrib.alternative_solutions.solution as sol +from pyomo.contrib.alternative_solutions import Solution mip_solver = "gurobi" @@ -51,7 +40,7 @@ def test_solution(self): opt.solve(model) all_vars = au.get_model_variables(model, include_fixed=True) - solution = sol.Solution(model, all_vars, include_fixed=False) + solution = Solution(model, all_vars, include_fixed=False) sol_str = """{ "fixed_variables": [ "f" @@ -66,7 +55,7 @@ def test_solution(self): }""" assert str(solution) == sol_str - solution = sol.Solution(model, all_vars) + solution = Solution(model, all_vars) sol_str = """{ "fixed_variables": [ "f" From 146a05b3424b73dff268bcf1af64bab69d1e13e8 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 9 Apr 2024 15:00:08 -0600 Subject: [PATCH 1175/3044] Adding top-level imports --- pyomo/contrib/alternative_solutions/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/contrib/alternative_solutions/__init__.py b/pyomo/contrib/alternative_solutions/__init__.py index e69de29bb2d..756fb71353e 100644 --- a/pyomo/contrib/alternative_solutions/__init__.py +++ b/pyomo/contrib/alternative_solutions/__init__.py @@ -0,0 +1,3 @@ +from pyomo.contrib.alternative_solutions.solution import Solution +from pyomo.contrib.alternative_solutions.solnpool import gurobi_generate_solutions +from pyomo.contrib.alternative_solutions.balas import enumerate_binary_solutions From 9bf65310dc34fd555918dcc39a58b57349156a4d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 9 Apr 2024 15:01:53 -0600 Subject: [PATCH 1176/3044] Bug fixes: name and solutions attributes --- pyomo/contrib/solver/base.py | 11 +++++++---- pyomo/contrib/solver/factory.py | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index cec392271f6..69f32b45075 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -61,10 +61,7 @@ def __init__(self, **kwds) -> None: # We allow the user and/or developer to name the solver something else, # if they really desire. Otherwise it defaults to the class name (all lowercase) if "name" in kwds: - self.name = kwds["name"] - kwds.pop('name') - else: - self.name = type(self).__name__.lower() + self.name = kwds.pop('name') self.config = self.CONFIG(value=kwds) # @@ -499,6 +496,12 @@ def _solution_handler( """Method to handle the preferred action for the solution""" symbol_map = SymbolMap() symbol_map.default_labeler = NumericLabeler('x') + if not hasattr(model, 'solutions'): + # This logic gets around Issue #2130 in which + # solutions is not an attribute on Blocks + from pyomo.core.base.PyomoModel import ModelSolutions + + setattr(model, 'solutions', ModelSolutions(model)) model.solutions.add_symbol_map(symbol_map) legacy_results._smap_id = id(symbol_map) delete_legacy_soln = True diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index 99fbcc3a6d0..71bf81ee15b 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -31,6 +31,7 @@ class LegacySolver(LegacySolverWrapper, cls): LegacySolver ) + cls.name = name return cls return decorator From 98be8f74fba80c474704349e450c917aa3f9883c Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 9 Apr 2024 15:04:58 -0600 Subject: [PATCH 1177/3044] Reformatting with black --- .../alternative_solutions/aos_utils.py | 5 +- pyomo/contrib/alternative_solutions/balas.py | 22 ++++--- .../alternative_solutions/tests/test_balas.py | 64 ++++++++++++++----- .../alternative_solutions/tests/test_cases.py | 8 ++- .../tests/test_solnpool.py | 4 +- 5 files changed, 72 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index 90843ed76c9..3070470494a 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -10,7 +10,8 @@ # ___________________________________________________________________________ import numpy.random -#from numpy.random import normal + +# from numpy.random import normal from numpy.linalg import norm import pyomo.environ as pe @@ -103,10 +104,12 @@ def _add_objective_constraint( rng = numpy.random.default_rng(9283749387) + def _set_numpy_rng(seed): global rng rng = numpy.random.default_rng(seed) + def _get_random_direction(num_dimensions): """ Get a unit vector of dimension num_dimensions by sampling from and diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index 7c4451acb22..5ff21239b91 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -63,7 +63,7 @@ def enumerate_binary_solutions( A list of Solution objects. [Solution] """ - if not quiet: #pragma: no cover + if not quiet: # pragma: no cover print("STARTING NO-GOOD CUT ANALYSIS") assert search_mode in [ @@ -134,7 +134,7 @@ def enumerate_binary_solutions( # # Initial solve of the model # - if not quiet: #pragma: no cover + if not quiet: # pragma: no cover print("Peforming initial solve of model.") results = opt.solve(model, tee=tee, load_solutions=False) status = results.solver.status @@ -150,7 +150,7 @@ def enumerate_binary_solutions( model.solutions.load_from(results) orig_objective_value = pe.value(orig_objective) - if not quiet: #pragma: no cover + if not quiet: # pragma: no cover print("Found optimal solution, value = {}.".format(orig_objective_value)) solutions = [Solution(model, all_variables, objective=orig_objective)] # @@ -160,7 +160,7 @@ def enumerate_binary_solutions( return solutions aos_block = aos_utils._add_aos_block(model, name="_balas") - if not quiet: #pragma: no cover + if not quiet: # pragma: no cover print("Added block {} to the model.".format(aos_block)) aos_block.no_good_cuts = pe.ConstraintList() aos_utils._add_objective_constraint( @@ -208,22 +208,26 @@ def enumerate_binary_solutions( model.solutions.load_from(results) orig_obj_value = pe.value(orig_objective) orig_obj_value = pe.value(orig_objective) - if not quiet: #pragma: no cover - print("Iteration {}: objective = {}".format(solution_number, orig_obj_value)) + if not quiet: # pragma: no cover + print( + "Iteration {}: objective = {}".format( + solution_number, orig_obj_value + ) + ) solutions.append(Solution(model, all_variables, objective=orig_objective)) solution_number += 1 elif ( condition == pe.TerminationCondition.infeasibleOrUnbounded or condition == pe.TerminationCondition.infeasible ): - if not quiet: #pragma: no cover + if not quiet: # pragma: no cover print( "Iteration {}: Infeasible, no additional binary solutions.".format( solution_number ) ) break - else: #pragma: no cover + else: # pragma: no cover if not quiet: print( ( @@ -236,7 +240,7 @@ def enumerate_binary_solutions( aos_block.deactivate() orig_objective.activate() - if not quiet: #pragma: no cover + if not quiet: # pragma: no cover print("COMPLETED NO-GOOD CUT ANALYSIS") return solutions diff --git a/pyomo/contrib/alternative_solutions/tests/test_balas.py b/pyomo/contrib/alternative_solutions/tests/test_balas.py index 1e469b2959f..64d1b6ac33f 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_balas.py +++ b/pyomo/contrib/alternative_solutions/tests/test_balas.py @@ -10,9 +10,10 @@ import pyomo.contrib.alternative_solutions.tests.test_cases as tc -solvers = list(pyomo.opt.check_available_solvers('glpk', 'gurobi', 'appsi_gurobi')) +solvers = list(pyomo.opt.check_available_solvers("glpk", "gurobi", "appsi_gurobi")) pytestmark = pytest.mark.parametrize("mip_solver", solvers) + @unittest.pytest.mark.default class TestBalasUnit: @@ -46,16 +47,22 @@ def test_no_time(self, mip_solver): """ m = tc.get_triangle_ip() with pytest.raises(Exception): - results = enumerate_binary_solutions(m, num_solutions=100, solver=mip_solver, solver_options={"TimeLimit":0}) + results = enumerate_binary_solutions( + m, num_solutions=100, solver=mip_solver, solver_options={"TimeLimit": 0} + ) def test_knapsack_all(self, mip_solver): """ Enumerate solutions for a binary problem: knapsack """ - m = tc.get_aos_test_knapsack(1, weights=[3,4,6,5], values=[2,3,1,4], capacity=8) + m = tc.get_aos_test_knapsack( + 1, weights=[3, 4, 6, 5], values=[2, 3, 1, 4], capacity=8 + ) results = enumerate_binary_solutions(m, num_solutions=100, solver=mip_solver) - objectives = list(sorted((round(result.objective[1], 2) for result in results), reverse=True)) + objectives = list( + sorted((round(result.objective[1], 2) for result in results), reverse=True) + ) assert_array_almost_equal(objectives, m.ranked_solution_values) unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, m.num_ranked_solns) @@ -66,21 +73,31 @@ def test_knapsack_x0_x1(self, mip_solver): Check that we only see 4 solutions that enumerate alternatives of x[1] and x[1] """ - m = tc.get_aos_test_knapsack(1, weights=[3,4,6,5], values=[2,3,1,4], capacity=8) - results = enumerate_binary_solutions(m, num_solutions=100, solver=mip_solver, variables=[m.x[0], m.x[1]]) - objectives = list(sorted((round(result.objective[1], 2) for result in results), reverse=True)) - assert_array_almost_equal(objectives, [6,5,4,3]) + m = tc.get_aos_test_knapsack( + 1, weights=[3, 4, 6, 5], values=[2, 3, 1, 4], capacity=8 + ) + results = enumerate_binary_solutions( + m, num_solutions=100, solver=mip_solver, variables=[m.x[0], m.x[1]] + ) + objectives = list( + sorted((round(result.objective[1], 2) for result in results), reverse=True) + ) + assert_array_almost_equal(objectives, [6, 5, 4, 3]) unique_solns_by_obj = [val for val in Counter(objectives).values()] - assert_array_almost_equal(unique_solns_by_obj, [1,1,1,1]) + assert_array_almost_equal(unique_solns_by_obj, [1, 1, 1, 1]) def test_knapsack_optimal_3(self, mip_solver): """ Enumerate solutions for a binary problem: knapsack """ - m = tc.get_aos_test_knapsack(1, weights=[3,4,6,5], values=[2,3,1,4], capacity=8) + m = tc.get_aos_test_knapsack( + 1, weights=[3, 4, 6, 5], values=[2, 3, 1, 4], capacity=8 + ) results = enumerate_binary_solutions(m, num_solutions=3, solver=mip_solver) - objectives = list(sorted((round(result.objective[1], 2) for result in results), reverse=True)) + objectives = list( + sorted((round(result.objective[1], 2) for result in results), reverse=True) + ) assert_array_almost_equal(objectives, m.ranked_solution_values[:3]) def test_knapsack_hamming_3(self, mip_solver): @@ -88,9 +105,15 @@ def test_knapsack_hamming_3(self, mip_solver): Enumerate solutions for a binary problem: knapsack """ - m = tc.get_aos_test_knapsack(1, weights=[3,4,6,5], values=[2,3,1,4], capacity=8) - results = enumerate_binary_solutions(m, num_solutions=3, solver=mip_solver, search_mode="hamming") - objectives = list(sorted((round(result.objective[1], 2) for result in results), reverse=True)) + m = tc.get_aos_test_knapsack( + 1, weights=[3, 4, 6, 5], values=[2, 3, 1, 4], capacity=8 + ) + results = enumerate_binary_solutions( + m, num_solutions=3, solver=mip_solver, search_mode="hamming" + ) + objectives = list( + sorted((round(result.objective[1], 2) for result in results), reverse=True) + ) assert_array_almost_equal(objectives, [6, 3, 1]) def test_knapsack_random_3(self, mip_solver): @@ -98,10 +121,17 @@ def test_knapsack_random_3(self, mip_solver): Enumerate solutions for a binary problem: knapsack """ - m = tc.get_aos_test_knapsack(1, weights=[3,4,6,5], values=[2,3,1,4], capacity=8) - results = enumerate_binary_solutions(m, num_solutions=3, solver=mip_solver, search_mode="random", seed=1118798374) - objectives = list(sorted((round(result.objective[1], 2) for result in results), reverse=True)) + m = tc.get_aos_test_knapsack( + 1, weights=[3, 4, 6, 5], values=[2, 3, 1, 4], capacity=8 + ) + results = enumerate_binary_solutions( + m, num_solutions=3, solver=mip_solver, search_mode="random", seed=1118798374 + ) + objectives = list( + sorted((round(result.objective[1], 2) for result in results), reverse=True) + ) assert_array_almost_equal(objectives, [6, 4, 1]) + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index f7c3d97659f..c45bf62cfdc 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -229,7 +229,9 @@ def get_implied_bound_ip(): return m -def get_aos_test_knapsack(var_max, weights, values, capacity=None, capacity_fraction=1.0): +def get_aos_test_knapsack( + var_max, weights, values, capacity=None, capacity_fraction=1.0 +): """ Creates a knapsack problem, given arrays of weights and values, and returns all feasible solutions. The capacity represents the percent of the @@ -266,8 +268,8 @@ def get_aos_test_knapsack(var_max, weights, values, capacity=None, capacity_frac if np.dot(sol, weights) <= capacity: feasible_sols.append((sol, np.dot(sol, values))) feasible_sols = sorted(feasible_sols, key=lambda sol: sol[1], reverse=True) - m.ranked_solution_values = list(sorted([v for x,v in feasible_sols], reverse=True)) - m.num_ranked_solns = list(Counter([v for x,v in feasible_sols]).values()) + m.ranked_solution_values = list(sorted([v for x, v in feasible_sols], reverse=True)) + m.num_ranked_solns = list(Counter([v for x, v in feasible_sols]).values()) return m diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index 0ac9c1f1334..b4c593eb001 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -83,7 +83,9 @@ def test_mip_rel_feasibility_options(self): found. """ m = tc.get_pentagonal_pyramid_mip() - results = gurobi_generate_solutions(m, num_solutions=100, solver_options={"PoolGap": 0.2}) + results = gurobi_generate_solutions( + m, num_solutions=100, solver_options={"PoolGap": 0.2} + ) objectives = [round(result.objective[1], 2) for result in results] actual_solns_by_obj = m.num_ranked_solns[0:2] unique_solns_by_obj = [val for val in Counter(objectives).values()] From 7268a954a83bfe83ee01620a9d16ddcfba9082ab Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 9 Apr 2024 15:05:51 -0600 Subject: [PATCH 1178/3044] Add back in logic for if there is no name attr --- pyomo/contrib/solver/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 69f32b45075..8d49344fbcf 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -14,8 +14,8 @@ from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple import os -from pyomo.core.base.constraint import Constraint, _GeneralConstraintData -from pyomo.core.base.var import Var, _GeneralVarData +from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.param import _ParamData from pyomo.core.base.block import _BlockData from pyomo.core.base.objective import Objective, _GeneralObjectiveData @@ -62,6 +62,8 @@ def __init__(self, **kwds) -> None: # if they really desire. Otherwise it defaults to the class name (all lowercase) if "name" in kwds: self.name = kwds.pop('name') + elif not hasattr(self, 'name'): + self.name = type(self).__name__.lower() self.config = self.CONFIG(value=kwds) # From 9b4fd6f4f2a2a5f3b959cb9397d7923498280b1d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 9 Apr 2024 15:12:27 -0600 Subject: [PATCH 1179/3044] Add relevant comments --- pyomo/contrib/solver/base.py | 5 ++++- pyomo/contrib/solver/factory.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 8d49344fbcf..736bf8b5a7c 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -59,7 +59,10 @@ class SolverBase(abc.ABC): def __init__(self, **kwds) -> None: # We allow the user and/or developer to name the solver something else, - # if they really desire. Otherwise it defaults to the class name (all lowercase) + # if they really desire. + # Otherwise it defaults to the name defined when the solver was registered + # in the SolverFactory or the class name (all lowercase), whichever is + # applicable if "name" in kwds: self.name = kwds.pop('name') elif not hasattr(self, 'name'): diff --git a/pyomo/contrib/solver/factory.py b/pyomo/contrib/solver/factory.py index 71bf81ee15b..d3ca1329af3 100644 --- a/pyomo/contrib/solver/factory.py +++ b/pyomo/contrib/solver/factory.py @@ -31,6 +31,7 @@ class LegacySolver(LegacySolverWrapper, cls): LegacySolver ) + # Preserve the preferred name, as registered in the Factory cls.name = name return cls From e055273b1df9f711c9ec23f2d62e49672a921f87 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 9 Apr 2024 15:50:45 -0600 Subject: [PATCH 1180/3044] Fixing some solver interface issues --- .../contrib/alternative_solutions/__init__.py | 1 + pyomo/contrib/alternative_solutions/obbt.py | 58 ++++++++---- .../alternative_solutions/tests/test_balas.py | 11 --- .../alternative_solutions/tests/test_cases.py | 4 +- .../alternative_solutions/tests/test_obbt.py | 90 +++++++++++-------- 5 files changed, 101 insertions(+), 63 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/__init__.py b/pyomo/contrib/alternative_solutions/__init__.py index 756fb71353e..f76b2ce835b 100644 --- a/pyomo/contrib/alternative_solutions/__init__.py +++ b/pyomo/contrib/alternative_solutions/__init__.py @@ -1,3 +1,4 @@ from pyomo.contrib.alternative_solutions.solution import Solution from pyomo.contrib.alternative_solutions.solnpool import gurobi_generate_solutions from pyomo.contrib.alternative_solutions.balas import enumerate_binary_solutions +from pyomo.contrib.alternative_solutions.obbt import obbt_analysis diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 26b76b54930..e7686d863b3 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -17,6 +17,7 @@ def obbt_analysis( model, + *, variables="all", rel_opt_gap=None, abs_opt_gap=None, @@ -25,6 +26,7 @@ def obbt_analysis( solver="gurobi", solver_options={}, tee=False, + quiet=True, ): """ Calculates the bounds on each variable by solving a series of min and max @@ -61,6 +63,8 @@ def obbt_analysis( Solver option-value pairs to be passed to the solver. tee : boolean Boolean indicating that the solver output should be displayed. + quiet : boolean + Boolean indicating whether to suppress all output. Returns ------- @@ -70,7 +74,8 @@ def obbt_analysis( the solver encountered an issue. """ - print("STARTING OBBT ANALYSIS") + if not quiet: #pragma: no cover + print("STARTING OBBT ANALYSIS") if variables == "all" or warmstart: all_variables = aos_utils.get_model_variables(model, "all", include_fixed=False) variable_list = all_variables @@ -80,7 +85,8 @@ def obbt_analysis( solutions[var] = [] num_vars = len(variable_list) - print("Analyzing {} variables ({} total solves).".format(num_vars, 2 * num_vars)) + if not quiet: #pragma: no cover + print("Analyzing {} variables ({} total solves).".format(num_vars, 2 * num_vars)) orig_objective = aos_utils.get_active_objective(model) use_appsi = False @@ -89,6 +95,7 @@ def obbt_analysis( for parameter, value in solver_options.items(): opt.gurobi_options[parameter] = var_value opt.config.stream_solver = tee + opt.config.load_solution = False results = opt.solve(model) condition = results.termination_condition optimal_tc = appsi.base.TerminationCondition.optimal @@ -99,12 +106,19 @@ def obbt_analysis( opt = pe.SolverFactory(solver) for parameter, value in solver_options.items(): opt.options[parameter] = value - results = opt.solve(model, warmstart=warmstart, tee=tee) + try: + results = opt.solve(model, warmstart=warmstart, tee=tee, load_solutions=False) + except: + # Assume that we failed b.c. of warm starts + results = None + if results is None: + results = opt.solve(model, tee=tee, load_solutions=False) condition = results.solver.termination_condition optimal_tc = pe.TerminationCondition.optimal infeas_or_unbdd_tc = pe.TerminationCondition.infeasibleOrUnbounded unbdd_tc = pe.TerminationCondition.unbounded - print("Peforming initial solve of model.") + if not quiet: #pragma: no cover + print("Peforming initial solve of model.") if condition != optimal_tc: raise Exception( @@ -112,12 +126,18 @@ def obbt_analysis( condition.value ) ) + if use_appsi: + results.solution_loader.load_vars(solution_number=0) + else: + model.solutions.load_from(results) if warmstart: _add_solution(solutions) orig_objective_value = pe.value(orig_objective) - print("Found optimal solution, value = {}.".format(orig_objective_value)) + if not quiet: #pragma: no cover + print("Found optimal solution, value = {}.".format(orig_objective_value)) aos_block = aos_utils._add_aos_block(model, name="_obbt") - print("Added block {} to the model.".format(aos_block)) + if not quiet: #pragma: no cover + print("Added block {} to the model.".format(aos_block)) obj_constraints = aos_utils._add_objective_constraint( aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap ) @@ -171,13 +191,19 @@ def obbt_analysis( pass else: try: - results = opt.solve(model, warmstart=warmstart, tee=tee) - condition = results.solver.termination_condition + results = opt.solve(model, warmstart=warmstart, tee=tee, load_solutions=False) except: - pass + results = None + if results is None: + results = opt.solve(model, tee=tee, load_solutions=False) + condition = results.solver.termination_condition new_constraint = False if condition == optimal_tc: + if use_appsi: + results.solution_loader.load_vars(solution_number=0) + else: + model.solutions.load_from(results) if warmstart: _add_solution(solutions) obj_val = pe.value(var) @@ -203,7 +229,7 @@ def obbt_analysis( variable_bounds[var][idx] = float("-inf") else: variable_bounds[var][idx] = float("inf") - else: + else: #pragma: no cover print( ( "Unexpected condition for the variable {} {} problem." @@ -212,11 +238,12 @@ def obbt_analysis( ) var_value = variable_bounds[var][idx] - print( - "Iteration {}/{}: {}_{} = {}".format( - iteration, total_iterations, var.name, bound_dir, var_value + if not quiet: #pragma: no cover + print( + "Iteration {}/{}: {}_{} = {}".format( + iteration, total_iterations, var.name, bound_dir, var_value + ) ) - ) if idx == 1: variable_bounds[var] = tuple(variable_bounds[var]) @@ -226,7 +253,8 @@ def obbt_analysis( aos_block.deactivate() orig_objective.activate() - print("COMPLETED OBBT ANALYSIS") + if not quiet: #pragma: no cover + print("COMPLETED OBBT ANALYSIS") return variable_bounds diff --git a/pyomo/contrib/alternative_solutions/tests/test_balas.py b/pyomo/contrib/alternative_solutions/tests/test_balas.py index 64d1b6ac33f..17b0591f668 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_balas.py +++ b/pyomo/contrib/alternative_solutions/tests/test_balas.py @@ -17,17 +17,6 @@ @unittest.pytest.mark.default class TestBalasUnit: - # TODO: Add test cases - """ - Repeat a lot of the test from solnpool to check that the various arguments work correct. - The main difference will be that we will only want to check binary problems here. - The knapsack problem should be useful (just set the bounds to 0-1). - - The only other thing to test is the different search modes. They should still enumerate - all of the solutions, just in a different sequence. - - """ - def test_ip_feasibility(self, mip_solver): """ Enumerate solutions for an ip: triangle_ip. diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index c45bf62cfdc..570c6424783 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -21,7 +21,9 @@ def _is_satified(constraint, feasability_tol=1e-6): def get_2d_diamond_problem(discrete_x=False, discrete_y=False): - """Simple 2d problem where the feasible is diamond-shaped.""" + """ + Simple 2d problem where the feasible is diamond-shaped. + """ m = pe.ConcreteModel() m.x = pe.Var(within=pe.Integers if discrete_x else pe.Reals) m.y = pe.Var(within=pe.Integers if discrete_y else pe.Reals) diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index cdf7a9f5225..c3de2b2e834 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -1,16 +1,19 @@ from numpy.testing import assert_array_almost_equal +import pytest import pyomo.environ as pe import pyomo.common.unittest as unittest -from pyomo.contrib.alternative_solutions.obbt import obbt_analysis +import pyomo.opt +from pyomo.contrib.alternative_solutions import obbt_analysis import pyomo.contrib.alternative_solutions.tests.test_cases as tc -mip_solver = "gurobi_appsi" -# mip_solver = 'gurobi' +solvers = list(pyomo.opt.check_available_solvers("glpk", "gurobi", "appsi_gurobi")) +pytestmark = pytest.mark.parametrize("mip_solver", solvers) -class TestOBBTUnit(unittest.TestCase): +@unittest.pytest.mark.default +class TestOBBTUnit: # TODO: Add more test cases """ @@ -32,41 +35,50 @@ class TestOBBTUnit(unittest.TestCase): """ - def test_obbt_continuous(self): - """Check that the correct bounds are found for a continuous problem.""" + def test_obbt_continuous(self, mip_solver): + """ + Check that the correct bounds are found for a continuous problem. + """ m = tc.get_2d_diamond_problem() results = obbt_analysis(m, solver=mip_solver) - self.assertEqual(results.keys(), m.continuous_bounds.keys()) + assert results.keys() == m.continuous_bounds.keys() for var, bounds in results.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - def test_mip_rel_objective(self): - """Check that relative mip gap constraints are added for a mip with indexed vars and constraints""" + def test_mip_rel_objective(self, mip_solver): + """ + Check that relative mip gap constraints are added for a mip with indexed vars and constraints + """ m = tc.get_indexed_pentagonal_pyramid_mip() results = obbt_analysis(m, rel_opt_gap=0.5) - self.assertAlmostEqual(m._obbt.optimality_tol_rel.lb, 2.5) + assert m._obbt.optimality_tol_rel.lb == pytest.approx(2.5) - def test_mip_abs_objective(self): - """Check that absolute mip gap constraints are added""" + def test_mip_abs_objective(self, mip_solver): + """ + Check that absolute mip gap constraints are added""" m = tc.get_pentagonal_pyramid_mip() results = obbt_analysis(m, abs_opt_gap=1.99) - self.assertAlmostEqual(m._obbt.optimality_tol_abs.lb, 3.01) + assert m._obbt.optimality_tol_abs.lb == pytest.approx(3.01) - def test_obbt_warmstart(self): - """Check that warmstarting works.""" + def test_obbt_warmstart(self, mip_solver): + """ + Check that warmstarting works. + """ m = tc.get_2d_diamond_problem() m.x.value = 0 m.y.value = 0 - results = obbt_analysis(m, solver=mip_solver, warmstart=True, tee=True) - self.assertEqual(results.keys(), m.continuous_bounds.keys()) + results = obbt_analysis(m, solver=mip_solver, warmstart=True, tee=False) + assert results.keys() == m.continuous_bounds.keys() for var, bounds in results.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - def test_obbt_mip(self): - """Check that bound tightening only occurs for continuous variables - that can be tightened.""" + def test_obbt_mip(self, mip_solver): + """ + Check that bound tightening only occurs for continuous variables + that can be tightened. + """ m = tc.get_bloated_pentagonal_pyramid_mip() - results = obbt_analysis(m, solver=mip_solver, tee=True) + results = obbt_analysis(m, solver=mip_solver, tee=False) bounds_tightened = False bounds_not_tightned = False for var, bounds in results.items(): @@ -78,45 +90,51 @@ def test_obbt_mip(self): bounds_tightened = True else: bounds_not_tightened = True - self.assertTrue(bounds_tightened) - self.assertTrue(bounds_not_tightened) + assert bounds_tightened + assert bounds_not_tightened - def test_obbt_unbounded(self): - """Check that the correct bounds are found for an unbounded problem.""" + def test_obbt_unbounded(self, mip_solver): + """ + Check that the correct bounds are found for an unbounded problem. + """ m = tc.get_2d_unbounded_problem() results = obbt_analysis(m, solver=mip_solver) - self.assertEqual(results.keys(), m.continuous_bounds.keys()) + assert results.keys() == m.continuous_bounds.keys() for var, bounds in results.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - def test_bound_tightening(self): + def test_bound_tightening(self, mip_solver): """ Check that the correct bounds are found for a discrete problem where - more restrictive bounds are implied by the constraints.""" + more restrictive bounds are implied by the constraints. + """ m = tc.get_implied_bound_ip() results = obbt_analysis(m, solver=mip_solver) - self.assertEqual(results.keys(), m.var_bounds.keys()) + assert results.keys() == m.var_bounds.keys() for var, bounds in results.items(): assert_array_almost_equal(bounds, m.var_bounds[var]) - def test_bound_refinement(self): + def test_bound_refinement(self, mip_solver): """ Check that the correct bounds are found for a discrete problem where more restrictive bounds are implied by the constraints and constraints - are added.""" + are added. + """ m = tc.get_implied_bound_ip() results = obbt_analysis(m, solver=mip_solver, refine_discrete_bounds=True) for var, bounds in results.items(): if m.var_bounds[var][0] > var.lb: - self.assertTrue(hasattr(m._obbt, var.name + "_lb")) + assert hasattr(m._obbt, var.name + "_lb") if m.var_bounds[var][1] < var.ub: - self.assertTrue(hasattr(m._obbt, var.name + "_ub")) + assert hasattr(m._obbt, var.name + "_ub") - def test_obbt_infeasible(self): - """Check that code catches cases where the problem is infeasible.""" + def test_obbt_infeasible(self, mip_solver): + """ + Check that code catches cases where the problem is infeasible. + """ m = tc.get_2d_diamond_problem() m.infeasible_constraint = pe.Constraint(expr=m.x >= 10) - with self.assertRaises(Exception): + with pytest.raises(Exception): obbt_analysis(m, solver=mip_solver) From 4ddcb5b5dc465c2a808e17bfe7cb32d643e7731b Mon Sep 17 00:00:00 2001 From: whart222 Date: Wed, 10 Apr 2024 03:44:11 -0600 Subject: [PATCH 1181/3044] Various changes to improve test coverage --- pyomo/contrib/alternative_solutions/obbt.py | 20 +++++---- .../alternative_solutions/tests/test_cases.py | 3 +- .../alternative_solutions/tests/test_obbt.py | 44 +++++++++++-------- 3 files changed, 38 insertions(+), 29 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index e7686d863b3..935c7ab5fb7 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -70,15 +70,20 @@ def obbt_analysis( ------- variable_ranges A Pyomo ComponentMap containing the bounds for each variable. - {variable: (lower_bound, upper_bound)}. A None value indicates + {variable: (lower_bound, upper_bound)}. An exception is raised when the solver encountered an issue. """ if not quiet: #pragma: no cover print("STARTING OBBT ANALYSIS") - if variables == "all" or warmstart: + + if warmstart: + assert variables == "all", "Cannot restrict variable list when warmstart is specified" + if variables == "all": all_variables = aos_utils.get_model_variables(model, "all", include_fixed=False) variable_list = all_variables + else: + variable_list = list(variables) if warmstart: solutions = pe.ComponentMap() for var in all_variables: @@ -93,7 +98,7 @@ def obbt_analysis( if "appsi" in solver: opt = appsi.solvers.Gurobi() for parameter, value in solver_options.items(): - opt.gurobi_options[parameter] = var_value + opt.gurobi_options[parameter] = value opt.config.stream_solver = tee opt.config.load_solution = False results = opt.solve(model) @@ -121,7 +126,7 @@ def obbt_analysis( print("Peforming initial solve of model.") if condition != optimal_tc: - raise Exception( + raise RuntimeError( ("OBBT cannot be applied, " "TerminationCondition = {}").format( condition.value ) @@ -184,11 +189,8 @@ def obbt_analysis( opt.update_config.check_for_new_or_removed_constraints = new_constraint if use_appsi: opt.config.stream_solver = tee - try: - results = opt.solve(model) - condition = results.termination_condition - except: - pass + results = opt.solve(model) + condition = results.termination_condition else: try: results = opt.solve(model, warmstart=warmstart, tee=tee, load_solutions=False) diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index 570c6424783..04eb7103d83 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -220,13 +220,14 @@ def get_implied_bound_ip(): m.c1 = pe.Constraint(expr=m.x + m.y == 3) m.c2 = pe.Constraint(expr=m.x + m.y + m.z <= 5) + m.c3 = pe.Constraint(expr=m.x + m.y + m.z >= 4) m.extreme_points = {(4, 2)} m.var_bounds = pe.ComponentMap() m.var_bounds[m.x] = (0, 3) m.var_bounds[m.y] = (0, 3) - m.var_bounds[m.z] = (0, 2) + m.var_bounds[m.z] = (1, 2) return m diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index c3de2b2e834..9a4746939ae 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -11,29 +11,25 @@ solvers = list(pyomo.opt.check_available_solvers("glpk", "gurobi", "appsi_gurobi")) pytestmark = pytest.mark.parametrize("mip_solver", solvers) +timelimit={"gurobi":"TimeLimit", "appsi_gurobi":"TimeLimit", "glpk":"tmlim"} @unittest.pytest.mark.default class TestOBBTUnit: - # TODO: Add more test cases - """ - So far I have added test cases for the feasibility problems, we should test cases - where we put objective constraints in as well based on the absolute and relative difference. - - Add a case where bounds are only found for a subset of variables. - - Try cases where refine_discrete_bounds is set to true to ensure that new constraints are - added to refine the bounds. I created the problem get_implied_bound_ip to facilitate this - - Check to see that warm starting works for a MIP and MILP case - - We should also check that warmstarting and refining bounds works for gurobi and appsi_gurobi - - We should pass at least one solver_options to ensure this work (e.g. time limit) - - I only looked at linear cases here, so you think others are worth testing, some simple non-linear (convex) cases? + def test_obbt_error1(self, mip_solver): + m = tc.get_2d_diamond_problem() + with pytest.raises(AssertionError): + obbt_analysis(m, variables=[m.x], solver=mip_solver) - """ + def test_obbt_some_vars(self, mip_solver): + """ + Check that the correct bounds are found for a continuous problem. + """ + m = tc.get_2d_diamond_problem() + results = obbt_analysis(m, variables=[m.x], warmstart=False, solver=mip_solver) + assert len(results) == 1 + for var, bounds in results.items(): + assert_array_almost_equal(bounds, m.continuous_bounds[var]) def test_obbt_continuous(self, mip_solver): """ @@ -55,7 +51,8 @@ def test_mip_rel_objective(self, mip_solver): def test_mip_abs_objective(self, mip_solver): """ - Check that absolute mip gap constraints are added""" + Check that absolute mip gap constraints are added + """ m = tc.get_pentagonal_pyramid_mip() results = obbt_analysis(m, abs_opt_gap=1.99) assert m._obbt.optimality_tol_abs.lb == pytest.approx(3.01) @@ -114,6 +111,15 @@ def test_bound_tightening(self, mip_solver): for var, bounds in results.items(): assert_array_almost_equal(bounds, m.var_bounds[var]) + def test_no_time(self, mip_solver): + """ + Check that the correct bounds are found for a discrete problem where + more restrictive bounds are implied by the constraints. + """ + m = tc.get_implied_bound_ip() + with pytest.raises(RuntimeError): + obbt_analysis(m, solver=mip_solver, solver_options={timelimit[mip_solver]: 0}) + def test_bound_refinement(self, mip_solver): """ Check that the correct bounds are found for a discrete problem where From 88535d42141f0aa678ec88b8a86cf23684faf9aa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 10 Apr 2024 09:24:12 -0600 Subject: [PATCH 1182/3044] Remove remaining references to '_.*Data' classes --- pyomo/contrib/pyros/config.py | 2 +- pyomo/core/base/block.py | 18 +++++++++--------- pyomo/gdp/util.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index 59bf9a9ab37..31e462223da 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -98,7 +98,7 @@ class InputDataStandardizer(object): Pyomo component type, such as Component, Var or Param. cdatatype : type Corresponding Pyomo component data type, such as - _ComponentData, VarData, or ParamData. + ComponentData, VarData, or ParamData. ctype_validator : callable, optional Validator function for objects of type `ctype`. cdatatype_validator : callable, optional diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 8f4e86fe697..3eb18dde7a9 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -160,13 +160,13 @@ def __init__(self): self.seen_data = set() def unique(self, comp, items, are_values): - """Returns generator that filters duplicate _ComponentData objects from items + """Returns generator that filters duplicate ComponentData objects from items Parameters ---------- comp: ComponentBase The Component (indexed or scalar) that contains all - _ComponentData returned by the `items` generator. `comp` may + ComponentData returned by the `items` generator. `comp` may be an IndexedComponent generated by :py:func:`Reference` (and hence may not own the component datas in `items`) @@ -175,8 +175,8 @@ def unique(self, comp, items, are_values): `comp` Component. are_values: bool - If `True`, `items` yields _ComponentData objects, otherwise, - `items` yields `(index, _ComponentData)` tuples. + If `True`, `items` yields ComponentData objects, otherwise, + `items` yields `(index, ComponentData)` tuples. """ if comp.is_reference(): @@ -1399,7 +1399,7 @@ def _component_data_iteritems(self, ctype, active, sort, dedup): Generator that returns a nested 2-tuple of - ((component name, index value), _ComponentData) + ((component name, index value), ComponentData) for every component data in the block matching the specified ctype(s). @@ -1416,7 +1416,7 @@ def _component_data_iteritems(self, ctype, active, sort, dedup): Iterate over the components in a specified sorted order dedup: _DeduplicateInfo - Deduplicator to prevent returning the same _ComponentData twice + Deduplicator to prevent returning the same ComponentData twice """ for name, comp in PseudoMap(self, ctype, active, sort).items(): # NOTE: Suffix has a dict interface (something other derived @@ -1452,7 +1452,7 @@ def _component_data_iteritems(self, ctype, active, sort, dedup): yield from dedup.unique(comp, _items, False) def _component_data_itervalues(self, ctype, active, sort, dedup): - """Generator that returns the _ComponentData for every component data + """Generator that returns the ComponentData for every component data in the block. Parameters @@ -1467,7 +1467,7 @@ def _component_data_itervalues(self, ctype, active, sort, dedup): Iterate over the components in a specified sorted order dedup: _DeduplicateInfo - Deduplicator to prevent returning the same _ComponentData twice + Deduplicator to prevent returning the same ComponentData twice """ for comp in PseudoMap(self, ctype, active, sort).values(): # NOTE: Suffix has a dict interface (something other derived @@ -1573,7 +1573,7 @@ def component_data_iterindex( generator recursively descends into sub-blocks. The tuple is - ((component name, index value), _ComponentData) + ((component name, index value), ComponentData) """ dedup = _DeduplicateInfo() diff --git a/pyomo/gdp/util.py b/pyomo/gdp/util.py index ee905791c26..2fe8e9e1dee 100644 --- a/pyomo/gdp/util.py +++ b/pyomo/gdp/util.py @@ -534,7 +534,7 @@ def get_transformed_constraints(srcConstraint): "want the container for all transformed constraints " "from an IndexedDisjunction, this is the parent " "component of a transformed constraint originating " - "from any of its _ComponentDatas.)" + "from any of its ComponentDatas.)" ) transBlock = _get_constraint_transBlock(srcConstraint) transformed_constraints = transBlock.private_data( From b235295174500eacb3bc0c2171d298b3ce3b8d48 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 10 Apr 2024 09:31:02 -0600 Subject: [PATCH 1183/3044] calculate_variable_from_constraint: add check for indexed constraint --- pyomo/util/calc_var_value.py | 7 +++++++ pyomo/util/tests/test_calc_var_value.py | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/pyomo/util/calc_var_value.py b/pyomo/util/calc_var_value.py index 156ad56dffb..42ee3119361 100644 --- a/pyomo/util/calc_var_value.py +++ b/pyomo/util/calc_var_value.py @@ -85,6 +85,13 @@ def calculate_variable_from_constraint( constraint = Constraint(expr=constraint, name=type(constraint).__name__) constraint.construct() + if constraint.is_indexed(): + raise ValueError( + 'calculate_variable_from_constraint(): constraint must be a ' + 'scalar constraint or a single ConstraintData. Received ' + f'{constraint.__class__.__name__} ("{constraint.name}")' + ) + body = constraint.body lower = constraint.lb upper = constraint.ub diff --git a/pyomo/util/tests/test_calc_var_value.py b/pyomo/util/tests/test_calc_var_value.py index a02d7a7d838..4bed4d5c843 100644 --- a/pyomo/util/tests/test_calc_var_value.py +++ b/pyomo/util/tests/test_calc_var_value.py @@ -101,6 +101,15 @@ def test_initialize_value(self): ): calculate_variable_from_constraint(m.x, m.lt) + m.indexed = Constraint([1, 2], rule=lambda m, i: m.x <= i) + with self.assertRaisesRegex( + ValueError, + r"calculate_variable_from_constraint\(\): constraint must be a scalar " + r"constraint or a single ConstraintData. Received IndexedConstraint " + r'\("indexed"\)', + ): + calculate_variable_from_constraint(m.x, m.indexed) + def test_linear(self): m = ConcreteModel() m.x = Var() From 76cd7469b5faeb970ccf5b62e7f7e85e6c96cabd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 10 Apr 2024 09:44:42 -0600 Subject: [PATCH 1184/3044] NFC: Update docstring to fix typo and copy/paste errors --- pyomo/core/base/boolean_var.py | 44 ++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index 98761dee536..67c06bdacce 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -81,26 +81,30 @@ def _associated_binary_mapper(encode, val): class BooleanVarData(ComponentData, BooleanValue): - """ - This class defines the data for a single Boolean variable. - - Constructor Arguments: - component The BooleanVar object that owns this data. - - Public Class Attributes: - domain The domain of this variable. - fixed If True, then this variable is treated as a - fixed constant in the model. - stale A Boolean indicating whether the value of this variable is - legitimiate. This value is true if the value should - be considered legitimate for purposes of reporting or - other interrogation. - value The numeric value of this variable. - - The domain attribute is a property because it is - too widely accessed directly to enforce explicit getter/setter - methods and we need to deter directly modifying or accessing - these attributes in certain cases. + """This class defines the data for a single Boolean variable. + + Parameters + ---------- + component: Component + The BooleanVar object that owns this data. + + Attributes + ---------- + domain: SetData + The domain of this variable. + + fixed: bool + If True, then this variable is treated as a fixed constant in + the model. + + stale: bool + A Boolean indicating whether the value of this variable is + Consistent with the most recent solve. `True` indicates that + this variable's value was set prior to the most recent solve and + was not updated by the results returned by the solve. + + value: bool + The value of this variable. """ __slots__ = ('_value', 'fixed', '_stale', '_associated_binary') From d19c8c9b5d9c5aeb3e04ee55d543add5e75a7cf7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 11 Apr 2024 08:30:41 -0600 Subject: [PATCH 1185/3044] Disable the use of universal newlines in the ipopt_v2 NL file --- pyomo/contrib/solver/ipopt.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index 5f601b7a9f7..588e06ad74c 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -307,7 +307,12 @@ def solve(self, model, **kwds): raise RuntimeError( f"NL file with the same name {basename + '.nl'} already exists!" ) - with open(basename + '.nl', 'w') as nl_file, open( + # Note: the ASL has an issue where string constants written + # to the NL file (e.g. arguments in external functions) MUST + # be terminated with '\n' regardless of platform. We will + # disable universal newlines in the NL file to prevent + # Python from mapping those '\n' to '\r\n' on Windows. + with open(basename + '.nl', 'w', newline='\n') as nl_file, open( basename + '.row', 'w' ) as row_file, open(basename + '.col', 'w') as col_file: timer.start('write_nl_file') From d0cef4682f93e279395f669628f69590181b45fc Mon Sep 17 00:00:00 2001 From: whart222 Date: Fri, 12 Apr 2024 05:06:57 -0600 Subject: [PATCH 1186/3044] Various updates to improve coverage Adding LP enum tests as well --- .../contrib/alternative_solutions/__init__.py | 6 +- pyomo/contrib/alternative_solutions/balas.py | 4 +- .../contrib/alternative_solutions/lp_enum.py | 280 +++++------------- .../alternative_solutions/lp_enum_solnpool.py | 189 ++++++++++++ pyomo/contrib/alternative_solutions/obbt.py | 136 +++++++-- .../contrib/alternative_solutions/solution.py | 11 - .../tests/run_lp_enum.py | 23 -- .../alternative_solutions/tests/test_cases.py | 6 +- .../tests/test_lp_enum.py | 73 +++++ .../alternative_solutions/tests/test_obbt.py | 91 ++++-- .../tests/test_shifted_lp.py | 44 ++- 11 files changed, 555 insertions(+), 308 deletions(-) create mode 100644 pyomo/contrib/alternative_solutions/lp_enum_solnpool.py delete mode 100644 pyomo/contrib/alternative_solutions/tests/run_lp_enum.py create mode 100644 pyomo/contrib/alternative_solutions/tests/test_lp_enum.py diff --git a/pyomo/contrib/alternative_solutions/__init__.py b/pyomo/contrib/alternative_solutions/__init__.py index f76b2ce835b..2dc7e153117 100644 --- a/pyomo/contrib/alternative_solutions/__init__.py +++ b/pyomo/contrib/alternative_solutions/__init__.py @@ -1,4 +1,8 @@ from pyomo.contrib.alternative_solutions.solution import Solution from pyomo.contrib.alternative_solutions.solnpool import gurobi_generate_solutions from pyomo.contrib.alternative_solutions.balas import enumerate_binary_solutions -from pyomo.contrib.alternative_solutions.obbt import obbt_analysis +from pyomo.contrib.alternative_solutions.obbt import ( + obbt_analysis, + obbt_analysis_bounds_and_solutions, +) +from pyomo.contrib.alternative_solutions.lp_enum import enumerate_linear_solutions diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index 5ff21239b91..677f6d31137 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -85,10 +85,10 @@ def enumerate_binary_solutions( for var in variables: if var.is_binary(): binary_variables.add(var) - else: + else: # pragma: no cover non_binary_variables.append(var.name) if len(non_binary_variables) > 0: - if not quiet: + if not quiet: # pragma: no cover print( ( "Warning: The following non-binary variables were included" diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index ba4e2c0be52..64ba5ae8bab 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -16,177 +16,12 @@ solution, solnpool, ) - - -def enumerate_linear_solutions_soln_pool( - model, - num_solutions=10, - variables="all", - rel_opt_gap=None, - abs_opt_gap=None, - solver_options={}, - tee=False, -): - """ - Finds alternative optimal solutions a (mixed-integer) linear program using - Gurobi's solution pool feature. - - Parameters - ---------- - model : ConcreteModel - A concrete Pyomo model - num_solutions : int - The maximum number of solutions to generate. - variables: 'all' or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. 'all' indicates - that all variables will be included. Alternatively, a collection of - _GenereralVarData variables can be provided. - rel_opt_gap : float or None - The relative optimality gap for the original objective for which - variable bounds will be found. None indicates that a relative gap - constraint will not be added to the model. - abs_opt_gap : float or None - The absolute optimality gap for the original objective for which - variable bounds will be found. None indicates that an absolute gap - constraint will not be added to the model. - solver_options : dict - Solver option-value pairs to be passed to the solver. - tee : boolean - Boolean indicating that the solver output should be displayed. - - Returns - ------- - solutions - A list of Solution objects. - [Solution] - """ - opt = pe.SolverFactory("gurobi") - print("STARTING LP ENUMERATION ANALYSIS USING GUROBI SOLUTION POOL") - - # For now keeping things simple - # TODO: Relax this - assert variables == "all" - - opt = pe.SolverFactory("gurobi") - for parameter, value in solver_options.items(): - opt.options[parameter] = value - - print("Peforming initial solve of model.") - results = opt.solve(model, tee=tee) - status = results.solver.status - condition = results.solver.termination_condition - if condition != pe.TerminationCondition.optimal: - raise Exception( - ( - "Model could not be solve. LP enumeration analysis " - "cannot be applied, SolverStatus = {}, " - "TerminationCondition = {}" - ).format(status.value, condition.value) - ) - - orig_objective = aos_utils.get_active_objective(model) - orig_objective_value = pe.value(orig_objective) - print("Found optimal solution, value = {}.".format(orig_objective_value)) - - aos_block = aos_utils._add_aos_block(model, name="_lp_enum") - print("Added block {} to the model.".format(aos_block)) - aos_utils._add_objective_constraint( - aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap - ) - - cannonical_block = shifted_lp.get_shifted_linear_model(model) - cb = cannonical_block - - # w variables - cb.basic_lower = pe.Var(cb.var_lower_index, domain=pe.Binary) - cb.basic_upper = pe.Var(cb.var_upper_index, domain=pe.Binary) - cb.basic_slack = pe.Var(cb.slack_index, domain=pe.Binary) - - # w upper bounds constraints - def bound_lower_rule(m, var_index): - return ( - m.var_lower[var_index] - <= m.var_lower[var_index].ub * m.basic_lower[var_index] - ) - - cb.bound_lower = pe.Constraint(cb.var_lower_index, rule=bound_lower_rule) - - def bound_upper_rule(m, var_index): - return ( - m.var_upper[var_index] - <= m.var_upper[var_index].ub * m.basic_upper[var_index] - ) - - cb.bound_upper = pe.Constraint(cb.var_upper_index, rule=bound_upper_rule) - - def bound_slack_rule(m, var_index): - return ( - m.slack_vars[var_index] - <= m.slack_vars[var_index].ub * m.basic_slack[var_index] - ) - - cb.bound_slack = pe.Constraint(cb.slack_index, rule=bound_slack_rule) - cb.pprint() - results = solnpool.gurobi_generate_solutions(cb, num_solutions) - - # print('Solving Iteration {}: '.format(solution_number), end='') - # results = opt.solve(cb, tee=tee) - # status = results.solver.status - # condition = results.solver.termination_condition - # if condition == pe.TerminationCondition.optimal: - # for var, index in cb.var_map.items(): - # var.set_value(var.lb + cb.var_lower[index].value) - # sol = solution.Solution(model, all_variables, - # objective=orig_objective) - # solutions.append(sol) - # orig_objective_value = sol.objective[1] - # print('Solved, objective = {}'.format(orig_objective_value)) - # for var, index in cb.var_map.items(): - # print('{} = {}'.format(var.name, var.lb + cb.var_lower[index].value)) - # if hasattr(cb, 'force_out'): - # cb.del_component('force_out') - # if hasattr(cb, 'link_in_out'): - # cb.del_component('link_in_out') - - # if hasattr(cb, 'basic_last_lower'): - # cb.del_component('basic_last_lower') - # if hasattr(cb, 'basic_last_upper'): - # cb.del_component('basic_last_upper') - # if hasattr(cb, 'basic_last_slack'): - # cb.del_component('basic_last_slack') - - # cb.link_in_out = pe.Constraint(pe.Any) - # cb.basic_last_lower = pe.Var(pe.Any, domain=pe.Binary, dense=False) - # cb.basic_last_upper = pe.Var(pe.Any, domain=pe.Binary, dense=False) - # cb.basic_last_slack = pe.Var(pe.Any, domain=pe.Binary, dense=False) - # basic_last_list = [cb.basic_last_lower, cb.basic_last_upper, - # cb.basic_last_slack] - - # num_non_zero = 0 - # force_out_expr = -1 - # non_zero_basic_expr = 1 - # for idx in range(len(variable_groups)): - # continuous_var, binary_var, constraint = variable_groups[idx] - # for var in continuous_var: - # if continuous_var[var].value > zero_threshold: - # num_non_zero += 1 - # if var not in binary_var: - # binary_var[var] - # constraint[var] = continuous_var[var] <= \ - # continuous_var[var].ub * binary_var[var] - # non_zero_basic_expr += binary_var[var] - # basic_var = basic_last_list[idx][var] - # force_out_expr += basic_var - # cb.link_in_out[var] = basic_var + binary_var[var] <= 1 - - # aos_block.deactivate() - # print('COMPLETED LP ENUMERATION ANALYSIS') - - # return solutions +from pyomo.contrib import appsi def enumerate_linear_solutions( model, + *, num_solutions=10, variables="all", rel_opt_gap=None, @@ -195,6 +30,8 @@ def enumerate_linear_solutions( solver="gurobi", solver_options={}, tee=False, + quiet=True, + seed=None, ): """ Finds alternative optimal solutions a (mixed-integer) linear program. @@ -229,6 +66,10 @@ def enumerate_linear_solutions( Solver option-value pairs to be passed to the solver. tee : boolean Boolean indicating that the solver output should be displayed. + quiet : boolean + Boolean indicating whether to suppress all output. + seed : int + Optional integer seed for the numpy random number generator Returns ------- @@ -236,9 +77,11 @@ def enumerate_linear_solutions( A list of Solution objects. [Solution] """ + if not quiet: # pragma: no cover + print("STARTING LP ENUMERATION ANALYSIS") + # TODO: Set this intelligently zero_threshold = 1e-5 - print("STARTING LP ENUMERATION ANALYSIS") # For now keeping things simple # TODO: See if this can be relaxed @@ -271,14 +114,13 @@ def enumerate_linear_solutions( for var in all_variables: assert var.is_continuous(), "Model must be an LP" - opt = pe.SolverFactory(solver) - for parameter, value in solver_options.items(): - opt.options[parameter] = value - use_appsi = False # TODO Check all this once implemented if "appsi" in solver: use_appsi = True + opt = appsi.solvers.Gurobi() + opt.config.load_solution = False + opt.config.stream_solver = tee opt.update_config.check_for_new_or_removed_constraints = True opt.update_config.update_constraints = False opt.update_config.check_for_new_or_removed_vars = True @@ -297,29 +139,48 @@ def enumerate_linear_solutions( else: opt.update_config.check_for_new_objective = False opt.update_config.update_objective = False - - print("Peforming initial solve of model.") - results = opt.solve(model, tee=tee) - status = results.solver.status - condition = results.solver.termination_condition - if condition != pe.TerminationCondition.optimal: + for parameter, value in solver_options.items(): + opt.gurobi_options[parameter] = value + else: + opt = pe.SolverFactory(solver) + for parameter, value in solver_options.items(): + opt.options[parameter] = value + + if not quiet: # pragma: no cover + print("Peforming initial solve of model.") + + if use_appsi: + results = opt.solve(model) + condition = results.termination_condition + optimal_tc = appsi.base.TerminationCondition.optimal + else: + results = opt.solve(model, tee=tee, load_solutions=False) + condition = results.solver.termination_condition + optimal_tc = pe.TerminationCondition.optimal + if condition != optimal_tc: raise Exception( ( "Model could not be solved. LP enumeration analysis " - "cannot be applied, SolverStatus = {}, " + "cannot be applied, " "TerminationCondition = {}" - ).format(status.value, condition.value) + ).format(condition.value) ) + if use_appsi: + results.solution_loader.load_vars(solution_number=0) + else: + model.solutions.load_from(results) orig_objective = aos_utils.get_active_objective(model) orig_objective_value = pe.value(orig_objective) - print("Found optimal solution, value = {}.".format(orig_objective_value)) + if not quiet: # pragma: no cover + print("Found optimal solution, value = {}.".format(orig_objective_value)) aos_block = aos_utils._add_aos_block(model, name="_lp_enum") - print("Added block {} to the model.".format(aos_block)) aos_utils._add_objective_constraint( aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap ) + if not quiet: # pragma: no cover + print("Added block {} to the model.".format(aos_block)) canon_block = shifted_lp.get_shifted_linear_model(model) cb = canon_block @@ -349,24 +210,38 @@ def enumerate_linear_solutions( solution_number = 1 solutions = [] while solution_number <= num_solutions: - print("Solving Iteration {}: ".format(solution_number), end="") - results = opt.solve(cb, tee=tee) - status = results.solver.status - condition = results.solver.termination_condition - if condition == pe.TerminationCondition.optimal: + if not quiet: # pragma: no cover + print("Solving Iteration {}: ".format(solution_number), end="") + + if use_appsi: + results = opt.solve(model) + condition = results.termination_condition + else: + results = opt.solve(cb, tee=tee, load_solutions=False) + condition = results.solver.termination_condition + if condition == optimal_tc: + if use_appsi: + results.solution_loader.load_vars(solution_number=0) + else: + model.solutions.load_from(results) + for var, index in cb.var_map.items(): var.set_value(var.lb + cb.var_lower[index].value) sol = solution.Solution(model, all_variables, objective=orig_objective) solutions.append(sol) orig_objective_value = sol.objective[1] - print("Solved, objective = {}".format(orig_objective_value)) - for var, index in cb.var_map.items(): - print("{} = {}".format(var.name, var.lb + cb.var_lower[index].value)) + + if not quiet: # pragma: no cover + print("Solved, objective = {}".format(orig_objective_value)) + for var, index in cb.var_map.items(): + print( + "{} = {}".format(var.name, var.lb + cb.var_lower[index].value) + ) + if hasattr(cb, "force_out"): cb.del_component("force_out") if hasattr(cb, "link_in_out"): cb.del_component("link_in_out") - if hasattr(cb, "basic_last_lower"): cb.del_component("basic_last_lower") if hasattr(cb, "basic_last_upper"): @@ -410,18 +285,23 @@ def enumerate_linear_solutions( condition == pe.TerminationCondition.infeasibleOrUnbounded or condition == pe.TerminationCondition.infeasible ): - print("Infeasible, all alternative solutions have been found.") + if not quiet: # pragma: no cover + print("Infeasible, all alternative solutions have been found.") break else: - print( - ( - "Unexpected solver condition. Stopping LP enumeration. " - "SolverStatus = {}, TerminationCondition = {}" - ).format(status.value, condition.value) - ) + if not quiet: # pragma: no cover + status = results.solver.status + print( + ( + "Unexpected solver condition. Stopping LP enumeration. " + "SolverStatus = {}, TerminationCondition = {}" + ).format(status.value, condition.value) + ) break - aos_block.deactivate() - print("COMPLETED LP ENUMERATION ANALYSIS") + model.del_component("aos_block") + + if not quiet: # pragma: no cover + print("COMPLETED LP ENUMERATION ANALYSIS") return solutions diff --git a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py new file mode 100644 index 00000000000..f72ad11e9e3 --- /dev/null +++ b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py @@ -0,0 +1,189 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.environ as pe +from pyomo.contrib.alternative_solutions import ( + aos_utils, + shifted_lp, + solution, + solnpool, +) + +# +# A draft enum tool using the gurobi solution pool +# + + +def enumerate_linear_solutions_soln_pool( + model, + num_solutions=10, + variables="all", + rel_opt_gap=None, + abs_opt_gap=None, + solver_options={}, + tee=False, +): + """ + Finds alternative optimal solutions a (mixed-integer) linear program using + Gurobi's solution pool feature. + + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model + num_solutions : int + The maximum number of solutions to generate. + variables: 'all' or a collection of Pyomo _GeneralVarData variables + The variables for which bounds will be generated. 'all' indicates + that all variables will be included. Alternatively, a collection of + _GenereralVarData variables can be provided. + rel_opt_gap : float or None + The relative optimality gap for the original objective for which + variable bounds will be found. None indicates that a relative gap + constraint will not be added to the model. + abs_opt_gap : float or None + The absolute optimality gap for the original objective for which + variable bounds will be found. None indicates that an absolute gap + constraint will not be added to the model. + solver_options : dict + Solver option-value pairs to be passed to the solver. + tee : boolean + Boolean indicating that the solver output should be displayed. + + Returns + ------- + solutions + A list of Solution objects. + [Solution] + """ + opt = pe.SolverFactory("gurobi") + print("STARTING LP ENUMERATION ANALYSIS USING GUROBI SOLUTION POOL") + + # For now keeping things simple + # TODO: Relax this + assert variables == "all" + + opt = pe.SolverFactory("gurobi") + for parameter, value in solver_options.items(): + opt.options[parameter] = value + + print("Peforming initial solve of model.") + results = opt.solve(model, tee=tee) + status = results.solver.status + condition = results.solver.termination_condition + if condition != pe.TerminationCondition.optimal: + raise Exception( + ( + "Model could not be solve. LP enumeration analysis " + "cannot be applied, SolverStatus = {}, " + "TerminationCondition = {}" + ).format(status.value, condition.value) + ) + + orig_objective = aos_utils.get_active_objective(model) + orig_objective_value = pe.value(orig_objective) + print("Found optimal solution, value = {}.".format(orig_objective_value)) + + aos_block = aos_utils._add_aos_block(model, name="_lp_enum") + print("Added block {} to the model.".format(aos_block)) + aos_utils._add_objective_constraint( + aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap + ) + + cannonical_block = shifted_lp.get_shifted_linear_model(model) + cb = cannonical_block + + # w variables + cb.basic_lower = pe.Var(cb.var_lower_index, domain=pe.Binary) + cb.basic_upper = pe.Var(cb.var_upper_index, domain=pe.Binary) + cb.basic_slack = pe.Var(cb.slack_index, domain=pe.Binary) + + # w upper bounds constraints + def bound_lower_rule(m, var_index): + return ( + m.var_lower[var_index] + <= m.var_lower[var_index].ub * m.basic_lower[var_index] + ) + + cb.bound_lower = pe.Constraint(cb.var_lower_index, rule=bound_lower_rule) + + def bound_upper_rule(m, var_index): + return ( + m.var_upper[var_index] + <= m.var_upper[var_index].ub * m.basic_upper[var_index] + ) + + cb.bound_upper = pe.Constraint(cb.var_upper_index, rule=bound_upper_rule) + + def bound_slack_rule(m, var_index): + return ( + m.slack_vars[var_index] + <= m.slack_vars[var_index].ub * m.basic_slack[var_index] + ) + + cb.bound_slack = pe.Constraint(cb.slack_index, rule=bound_slack_rule) + cb.pprint() + results = solnpool.gurobi_generate_solutions(cb, num_solutions) + + # print('Solving Iteration {}: '.format(solution_number), end='') + # results = opt.solve(cb, tee=tee) + # status = results.solver.status + # condition = results.solver.termination_condition + # if condition == pe.TerminationCondition.optimal: + # for var, index in cb.var_map.items(): + # var.set_value(var.lb + cb.var_lower[index].value) + # sol = solution.Solution(model, all_variables, + # objective=orig_objective) + # solutions.append(sol) + # orig_objective_value = sol.objective[1] + # print('Solved, objective = {}'.format(orig_objective_value)) + # for var, index in cb.var_map.items(): + # print('{} = {}'.format(var.name, var.lb + cb.var_lower[index].value)) + # if hasattr(cb, 'force_out'): + # cb.del_component('force_out') + # if hasattr(cb, 'link_in_out'): + # cb.del_component('link_in_out') + + # if hasattr(cb, 'basic_last_lower'): + # cb.del_component('basic_last_lower') + # if hasattr(cb, 'basic_last_upper'): + # cb.del_component('basic_last_upper') + # if hasattr(cb, 'basic_last_slack'): + # cb.del_component('basic_last_slack') + + # cb.link_in_out = pe.Constraint(pe.Any) + # cb.basic_last_lower = pe.Var(pe.Any, domain=pe.Binary, dense=False) + # cb.basic_last_upper = pe.Var(pe.Any, domain=pe.Binary, dense=False) + # cb.basic_last_slack = pe.Var(pe.Any, domain=pe.Binary, dense=False) + # basic_last_list = [cb.basic_last_lower, cb.basic_last_upper, + # cb.basic_last_slack] + + # num_non_zero = 0 + # force_out_expr = -1 + # non_zero_basic_expr = 1 + # for idx in range(len(variable_groups)): + # continuous_var, binary_var, constraint = variable_groups[idx] + # for var in continuous_var: + # if continuous_var[var].value > zero_threshold: + # num_non_zero += 1 + # if var not in binary_var: + # binary_var[var] + # constraint[var] = continuous_var[var] <= \ + # continuous_var[var].ub * binary_var[var] + # non_zero_basic_expr += binary_var[var] + # basic_var = basic_last_list[idx][var] + # force_out_expr += basic_var + # cb.link_in_out[var] = basic_var + binary_var[var] <= 1 + + # aos_block.deactivate() + # print('COMPLETED LP ENUMERATION ANALYSIS') + + # return solutions diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 935c7ab5fb7..7b79dc9d6fc 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -1,18 +1,7 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - import pyomo.environ as pe from pyomo.contrib.alternative_solutions import aos_utils +from pyomo.contrib.alternative_solutions import Solution from pyomo.contrib import appsi -import pdb def obbt_analysis( @@ -70,17 +59,98 @@ def obbt_analysis( ------- variable_ranges A Pyomo ComponentMap containing the bounds for each variable. - {variable: (lower_bound, upper_bound)}. An exception is raised when + {variable: (lower_bound, upper_bound)}. An exception is raised when the solver encountered an issue. + solutions + [Solution] """ + bounds, solns = obbt_analysis_bounds_and_solutions( + model, + variables=variables, + rel_opt_gap=rel_opt_gap, + abs_opt_gap=abs_opt_gap, + refine_discrete_bounds=refine_discrete_bounds, + warmstart=warmstart, + solver=solver, + solver_options=solver_options, + tee=tee, + quiet=quiet, + ) + return bounds + - if not quiet: #pragma: no cover +def obbt_analysis_bounds_and_solutions( + model, + *, + variables="all", + rel_opt_gap=None, + abs_opt_gap=None, + refine_discrete_bounds=False, + warmstart=True, + solver="gurobi", + solver_options={}, + tee=False, + quiet=True, +): + """ + Calculates the bounds on each variable by solving a series of min and max + optimization problems where each variable is used as the objective function + This can be applied to any class of problem supported by the selected + solver. + + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model. + variables: 'all' or a collection of Pyomo _GeneralVarData variables + The variables for which bounds will be generated. 'all' indicates + that all variables will be included. Alternatively, a collection of + _GenereralVarData variables can be provided. + rel_opt_gap : float or None + The relative optimality gap for the original objective for which + variable bounds will be found. None indicates that a relative gap + constraint will not be added to the model. + abs_opt_gap : float or None + The absolute optimality gap for the original objective for which + variable bounds will be found. None indicates that an absolute gap + constraint will not be added to the model. + refine_discrete_bounds : boolean + Boolean indicating that new constraints should be added to the + model at each iteration to tighten the bounds for discrete + variables. + warmstart : boolean + Boolean indicating that the solver should be warmstarted from the + best previously discovered solution. + solver : string + The solver to be used. + solver_options : dict + Solver option-value pairs to be passed to the solver. + tee : boolean + Boolean indicating that the solver output should be displayed. + quiet : boolean + Boolean indicating whether to suppress all output. + + Returns + ------- + variable_ranges + A Pyomo ComponentMap containing the bounds for each variable. + {variable: (lower_bound, upper_bound)}. An exception is raised when + the solver encountered an issue. + solutions + [Solution] + """ + + # TODO - parallelization + + if not quiet: # pragma: no cover print("STARTING OBBT ANALYSIS") if warmstart: - assert variables == "all", "Cannot restrict variable list when warmstart is specified" + assert ( + variables == "all" + ), "Cannot restrict variable list when warmstart is specified" + all_variables = aos_utils.get_model_variables(model, "all", include_fixed=False) if variables == "all": - all_variables = aos_utils.get_model_variables(model, "all", include_fixed=False) variable_list = all_variables else: variable_list = list(variables) @@ -90,8 +160,10 @@ def obbt_analysis( solutions[var] = [] num_vars = len(variable_list) - if not quiet: #pragma: no cover - print("Analyzing {} variables ({} total solves).".format(num_vars, 2 * num_vars)) + if not quiet: # pragma: no cover + print( + "Analyzing {} variables ({} total solves).".format(num_vars, 2 * num_vars) + ) orig_objective = aos_utils.get_active_objective(model) use_appsi = False @@ -112,7 +184,9 @@ def obbt_analysis( for parameter, value in solver_options.items(): opt.options[parameter] = value try: - results = opt.solve(model, warmstart=warmstart, tee=tee, load_solutions=False) + results = opt.solve( + model, warmstart=warmstart, tee=tee, load_solutions=False + ) except: # Assume that we failed b.c. of warm starts results = None @@ -122,7 +196,7 @@ def obbt_analysis( optimal_tc = pe.TerminationCondition.optimal infeas_or_unbdd_tc = pe.TerminationCondition.infeasibleOrUnbounded unbdd_tc = pe.TerminationCondition.unbounded - if not quiet: #pragma: no cover + if not quiet: # pragma: no cover print("Peforming initial solve of model.") if condition != optimal_tc: @@ -138,10 +212,10 @@ def obbt_analysis( if warmstart: _add_solution(solutions) orig_objective_value = pe.value(orig_objective) - if not quiet: #pragma: no cover + if not quiet: # pragma: no cover print("Found optimal solution, value = {}.".format(orig_objective_value)) aos_block = aos_utils._add_aos_block(model, name="_obbt") - if not quiet: #pragma: no cover + if not quiet: # pragma: no cover print("Added block {} to the model.".format(aos_block)) obj_constraints = aos_utils._add_objective_constraint( aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap @@ -164,6 +238,7 @@ def obbt_analysis( opt.update_config.treat_fixed_vars_as_params = False variable_bounds = pe.ComponentMap() + solns = [Solution(model, all_variables, objective=orig_objective)] senses = [(pe.minimize, "LB"), (pe.maximize, "UB")] @@ -193,7 +268,9 @@ def obbt_analysis( condition = results.termination_condition else: try: - results = opt.solve(model, warmstart=warmstart, tee=tee, load_solutions=False) + results = opt.solve( + model, warmstart=warmstart, tee=tee, load_solutions=False + ) except: results = None if results is None: @@ -206,6 +283,8 @@ def obbt_analysis( results.solution_loader.load_vars(solution_number=0) else: model.solutions.load_from(results) + solns.append(Solution(model, all_variables, objective=orig_objective)) + if warmstart: _add_solution(solutions) obj_val = pe.value(var) @@ -231,7 +310,7 @@ def obbt_analysis( variable_bounds[var][idx] = float("-inf") else: variable_bounds[var][idx] = float("inf") - else: #pragma: no cover + else: # pragma: no cover print( ( "Unexpected condition for the variable {} {} problem." @@ -240,10 +319,10 @@ def obbt_analysis( ) var_value = variable_bounds[var][idx] - if not quiet: #pragma: no cover + if not quiet: # pragma: no cover print( "Iteration {}/{}: {}_{} = {}".format( - iteration, total_iterations, var.name, bound_dir, var_value + iteration, total_iterations, var.name, bound_dir, var_value ) ) @@ -252,13 +331,14 @@ def obbt_analysis( iteration += 1 + # TODO - Remove this block aos_block.deactivate() orig_objective.activate() - if not quiet: #pragma: no cover + if not quiet: # pragma: no cover print("COMPLETED OBBT ANALYSIS") - return variable_bounds + return variable_bounds, solns def _add_solution(solutions): diff --git a/pyomo/contrib/alternative_solutions/solution.py b/pyomo/contrib/alternative_solutions/solution.py index 68344dcbc07..c215880cc0e 100644 --- a/pyomo/contrib/alternative_solutions/solution.py +++ b/pyomo/contrib/alternative_solutions/solution.py @@ -1,14 +1,3 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - import json import pyomo.environ as pe from pyomo.common.collections import ComponentMap, ComponentSet diff --git a/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py b/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py deleted file mode 100644 index c34d6843c30..00000000000 --- a/pyomo/contrib/alternative_solutions/tests/run_lp_enum.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Fri Oct 20 11:55:46 2023 - -@author: jlgearh -""" - -import pyomo.contrib.alternative_solutions.tests.test_cases as tc -from pyomo.contrib.alternative_solutions import lp_enum -import pyomo.environ as pe - -m = tc.get_3d_polyhedron_problem() -m.o.deactivate() -m.obj = pe.Objective(expr=m.x[0] + m.x[1] + m.x[2]) -sols = lp_enum.enumerate_linear_solutions(m, solver="gurobi") - - -n = tc.get_pentagonal_pyramid_mip() -n.o.sense = pe.minimize -n.x.domain = pe.Reals -n.y.domain = pe.Reals -sols = lp_enum.enumerate_linear_solutions(n, solver="gurobi") -n.pprint() diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index 04eb7103d83..1df3d59df43 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -25,8 +25,8 @@ def get_2d_diamond_problem(discrete_x=False, discrete_y=False): Simple 2d problem where the feasible is diamond-shaped. """ m = pe.ConcreteModel() - m.x = pe.Var(within=pe.Integers if discrete_x else pe.Reals) - m.y = pe.Var(within=pe.Integers if discrete_y else pe.Reals) + m.x = pe.Var(within=pe.Integers if discrete_x else pe.Reals, bounds=(-10, 10)) + m.y = pe.Var(within=pe.Integers if discrete_y else pe.Reals, bounds=(-10, 10)) m.o = pe.Objective(expr=m.x + m.y, sense=pe.maximize) @@ -222,8 +222,6 @@ def get_implied_bound_ip(): m.c2 = pe.Constraint(expr=m.x + m.y + m.z <= 5) m.c3 = pe.Constraint(expr=m.x + m.y + m.z >= 4) - m.extreme_points = {(4, 2)} - m.var_bounds = pe.ComponentMap() m.var_bounds[m.x] = (0, 3) m.var_bounds[m.y] = (0, 3) diff --git a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py new file mode 100644 index 00000000000..cdb633c5d1a --- /dev/null +++ b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py @@ -0,0 +1,73 @@ +import pytest + +import pyomo.environ as pe +import pyomo.common.unittest as unittest +import pyomo.opt + +import pyomo.contrib.alternative_solutions.tests.test_cases as tc +from pyomo.contrib.alternative_solutions import lp_enum + +# +# Find available solvers. Just use GLPK if it's available. +# +solvers = list(pyomo.opt.check_available_solvers("glpk", "gurobi", "appsi_gurobi")) +pytestmark = pytest.mark.parametrize("mip_solver", solvers) + +timelimit = {"gurobi": "TimeLimit", "appsi_gurobi": "TimeLimit", "glpk": "tmlim"} + + +@unittest.pytest.mark.default +class TestLPEnum: + + def test_no_time(self, mip_solver): + """ + Check that the correct bounds are found for a discrete problem where + more restrictive bounds are implied by the constraints. + """ + m = tc.get_3d_polyhedron_problem() + with pytest.raises(Exception): + lp_enum.enumerate_linear_solutions( + m, solver=mip_solver, solver_options={timelimit[mip_solver]: 0} + ) + + def test_3d_polyhedron(self, mip_solver): + m = tc.get_3d_polyhedron_problem() + m.o.deactivate() + m.obj = pe.Objective(expr=m.x[0] + m.x[1] + m.x[2]) + + sols = lp_enum.enumerate_linear_solutions(m, solver=mip_solver) + assert len(sols) == 2 + for s in sols: + assert s.objective_value == pytest.approx(4) + + def test_3d_polyhedron(self, mip_solver): + m = tc.get_3d_polyhedron_problem() + m.o.deactivate() + m.obj = pe.Objective(expr=m.x[0] + 2 * m.x[1] + 3 * m.x[2]) + + sols = lp_enum.enumerate_linear_solutions(m, solver=mip_solver) + assert len(sols) == 2 + for s in sols: + assert s.objective_value == pytest.approx( + 9 + ) or s.objective_value == pytest.approx(10) + + def test_2d_diamond_problem(self, mip_solver): + m = tc.get_2d_diamond_problem() + sols = lp_enum.enumerate_linear_solutions(m, solver=mip_solver, num_solutions=2) + assert len(sols) == 2 + for s in sols: + print(s) + assert sols[0].objective_value == pytest.approx(6.789473684210527) + assert sols[1].objective_value == pytest.approx(3.6923076923076916) + + def test_pentagonal_pyramid(self, mip_solver): + n = tc.get_pentagonal_pyramid_mip() + n.o.sense = pe.minimize + n.x.domain = pe.Reals + n.y.domain = pe.Reals + + sols = lp_enum.enumerate_linear_solutions(n, solver=mip_solver) + for s in sols: + print(s) + assert len(sols) == 6 diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index 9a4746939ae..452af966580 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -1,34 +1,52 @@ from numpy.testing import assert_array_almost_equal import pytest +import math import pyomo.environ as pe import pyomo.common.unittest as unittest import pyomo.opt -from pyomo.contrib.alternative_solutions import obbt_analysis +from pyomo.contrib.alternative_solutions import ( + obbt_analysis_bounds_and_solutions, + obbt_analysis, +) import pyomo.contrib.alternative_solutions.tests.test_cases as tc solvers = list(pyomo.opt.check_available_solvers("glpk", "gurobi", "appsi_gurobi")) pytestmark = pytest.mark.parametrize("mip_solver", solvers) -timelimit={"gurobi":"TimeLimit", "appsi_gurobi":"TimeLimit", "glpk":"tmlim"} +timelimit = {"gurobi": "TimeLimit", "appsi_gurobi": "TimeLimit", "glpk": "tmlim"} + @unittest.pytest.mark.default class TestOBBTUnit: + def test_obbt_analysis(self, mip_solver): + """ + Check that the correct bounds are found for a continuous problem. + """ + m = tc.get_2d_diamond_problem() + all_bounds = obbt_analysis(m, solver=mip_solver) + assert all_bounds.keys() == m.continuous_bounds.keys() + for var, bounds in all_bounds.items(): + assert_array_almost_equal(bounds, m.continuous_bounds[var]) + def test_obbt_error1(self, mip_solver): m = tc.get_2d_diamond_problem() with pytest.raises(AssertionError): - obbt_analysis(m, variables=[m.x], solver=mip_solver) + obbt_analysis_bounds_and_solutions(m, variables=[m.x], solver=mip_solver) def test_obbt_some_vars(self, mip_solver): """ Check that the correct bounds are found for a continuous problem. """ m = tc.get_2d_diamond_problem() - results = obbt_analysis(m, variables=[m.x], warmstart=False, solver=mip_solver) - assert len(results) == 1 - for var, bounds in results.items(): + all_bounds, solns = obbt_analysis_bounds_and_solutions( + m, variables=[m.x], warmstart=False, solver=mip_solver + ) + assert len(all_bounds) == 1 + assert len(solns) == 2 * len(all_bounds) + 1 + for var, bounds in all_bounds.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) def test_obbt_continuous(self, mip_solver): @@ -36,9 +54,10 @@ def test_obbt_continuous(self, mip_solver): Check that the correct bounds are found for a continuous problem. """ m = tc.get_2d_diamond_problem() - results = obbt_analysis(m, solver=mip_solver) - assert results.keys() == m.continuous_bounds.keys() - for var, bounds in results.items(): + all_bounds, solns = obbt_analysis_bounds_and_solutions(m, solver=mip_solver) + assert len(solns) == 2 * len(all_bounds) + 1 + assert all_bounds.keys() == m.continuous_bounds.keys() + for var, bounds in all_bounds.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) def test_mip_rel_objective(self, mip_solver): @@ -46,7 +65,8 @@ def test_mip_rel_objective(self, mip_solver): Check that relative mip gap constraints are added for a mip with indexed vars and constraints """ m = tc.get_indexed_pentagonal_pyramid_mip() - results = obbt_analysis(m, rel_opt_gap=0.5) + all_bounds, solns = obbt_analysis_bounds_and_solutions(m, rel_opt_gap=0.5) + assert len(solns) == 2 * len(all_bounds) + 1 assert m._obbt.optimality_tol_rel.lb == pytest.approx(2.5) def test_mip_abs_objective(self, mip_solver): @@ -54,7 +74,8 @@ def test_mip_abs_objective(self, mip_solver): Check that absolute mip gap constraints are added """ m = tc.get_pentagonal_pyramid_mip() - results = obbt_analysis(m, abs_opt_gap=1.99) + all_bounds, solns = obbt_analysis_bounds_and_solutions(m, abs_opt_gap=1.99) + assert len(solns) == 2 * len(all_bounds) + 1 assert m._obbt.optimality_tol_abs.lb == pytest.approx(3.01) def test_obbt_warmstart(self, mip_solver): @@ -64,9 +85,12 @@ def test_obbt_warmstart(self, mip_solver): m = tc.get_2d_diamond_problem() m.x.value = 0 m.y.value = 0 - results = obbt_analysis(m, solver=mip_solver, warmstart=True, tee=False) - assert results.keys() == m.continuous_bounds.keys() - for var, bounds in results.items(): + all_bounds, solns = obbt_analysis_bounds_and_solutions( + m, solver=mip_solver, warmstart=True, tee=False + ) + assert len(solns) == 2 * len(all_bounds) + 1 + assert all_bounds.keys() == m.continuous_bounds.keys() + for var, bounds in all_bounds.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) def test_obbt_mip(self, mip_solver): @@ -75,10 +99,13 @@ def test_obbt_mip(self, mip_solver): that can be tightened. """ m = tc.get_bloated_pentagonal_pyramid_mip() - results = obbt_analysis(m, solver=mip_solver, tee=False) + all_bounds, solns = obbt_analysis_bounds_and_solutions( + m, solver=mip_solver, tee=False + ) + assert len(solns) == 2 * len(all_bounds) + 1 bounds_tightened = False bounds_not_tightned = False - for var, bounds in results.items(): + for var, bounds in all_bounds.items(): if bounds[0] > var.lb: bounds_tightened = True else: @@ -95,10 +122,16 @@ def test_obbt_unbounded(self, mip_solver): Check that the correct bounds are found for an unbounded problem. """ m = tc.get_2d_unbounded_problem() - results = obbt_analysis(m, solver=mip_solver) - assert results.keys() == m.continuous_bounds.keys() - for var, bounds in results.items(): + all_bounds, solns = obbt_analysis_bounds_and_solutions(m, solver=mip_solver) + assert all_bounds.keys() == m.continuous_bounds.keys() + num = 1 + for var, bounds in all_bounds.items(): + if not math.isinf(bounds[0]): + num += 1 + if not math.isinf(bounds[1]): + num += 1 assert_array_almost_equal(bounds, m.continuous_bounds[var]) + assert len(solns) == num def test_bound_tightening(self, mip_solver): """ @@ -106,9 +139,10 @@ def test_bound_tightening(self, mip_solver): more restrictive bounds are implied by the constraints. """ m = tc.get_implied_bound_ip() - results = obbt_analysis(m, solver=mip_solver) - assert results.keys() == m.var_bounds.keys() - for var, bounds in results.items(): + all_bounds, solns = obbt_analysis_bounds_and_solutions(m, solver=mip_solver) + assert len(solns) == 2 * len(all_bounds) + 1 + assert all_bounds.keys() == m.var_bounds.keys() + for var, bounds in all_bounds.items(): assert_array_almost_equal(bounds, m.var_bounds[var]) def test_no_time(self, mip_solver): @@ -118,7 +152,9 @@ def test_no_time(self, mip_solver): """ m = tc.get_implied_bound_ip() with pytest.raises(RuntimeError): - obbt_analysis(m, solver=mip_solver, solver_options={timelimit[mip_solver]: 0}) + obbt_analysis_bounds_and_solutions( + m, solver=mip_solver, solver_options={timelimit[mip_solver]: 0} + ) def test_bound_refinement(self, mip_solver): """ @@ -127,8 +163,11 @@ def test_bound_refinement(self, mip_solver): are added. """ m = tc.get_implied_bound_ip() - results = obbt_analysis(m, solver=mip_solver, refine_discrete_bounds=True) - for var, bounds in results.items(): + all_bounds, solns = obbt_analysis_bounds_and_solutions( + m, solver=mip_solver, refine_discrete_bounds=True + ) + assert len(solns) == 2 * len(all_bounds) + 1 + for var, bounds in all_bounds.items(): if m.var_bounds[var][0] > var.lb: assert hasattr(m._obbt, var.name + "_lb") if m.var_bounds[var][1] < var.ub: @@ -141,7 +180,7 @@ def test_obbt_infeasible(self, mip_solver): m = tc.get_2d_diamond_problem() m.infeasible_constraint = pe.Constraint(expr=m.x >= 10) with pytest.raises(Exception): - obbt_analysis(m, solver=mip_solver) + obbt_analysis_bounds_and_solutions(m, solver=mip_solver) if __name__ == "__main__": diff --git a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py index ac7b96b5547..8dddd8d94ba 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py @@ -1,36 +1,54 @@ +import pytest from numpy.testing import assert_array_almost_equal import pyomo.environ as pe +import pyomo.opt import pyomo.common.unittest as unittest + import pyomo.contrib.alternative_solutions.tests.test_cases as tc from pyomo.contrib.alternative_solutions import shifted_lp +# TODO: add checks that confirm the shifted constraints make sense + +# +# Find available solvers. Just use GLPK if it's available. +# +solvers = list(pyomo.opt.check_available_solvers("glpk", "gurobi")) +if "glpk" in solvers: + solver = ["glpk"] +pytestmark = pytest.mark.parametrize("lp_solver", solvers) + -class TestShiftedIP(unittest.TestCase): +@unittest.pytest.mark.default +class TestShiftedIP: - def test_mip_abs_objective(self): - """ - COMMENT - """ + def test_mip_abs_objective(self, lp_solver): m = tc.get_indexed_pentagonal_pyramid_mip() m.x.domain = pe.Reals - opt = pe.SolverFactory("gurobi") - old_results = opt.solve(m, tee=True) + + opt = pe.SolverFactory(lp_solver) + old_results = opt.solve(m, tee=False) old_obj = pe.value(m.o) + new_model = shifted_lp.get_shifted_linear_model(m) - new_results = opt.solve(new_model, tee=True) + new_results = opt.solve(new_model, tee=False) new_obj = pe.value(new_model.objective) - self.assertAlmostEqual(old_obj, new_obj) - def test_polyhedron(self): + assert old_obj == pytest.approx(new_obj) + + def test_polyhedron(self, lp_solver): m = tc.get_3d_polyhedron_problem() - opt = pe.SolverFactory("gurobi") - old_results = opt.solve(m, tee=True) + + opt = pe.SolverFactory(lp_solver) + old_results = opt.solve(m, tee=False) old_obj = pe.value(m.o) + new_model = shifted_lp.get_shifted_linear_model(m) - new_results = opt.solve(new_model, tee=True) + new_results = opt.solve(new_model, tee=False) new_obj = pe.value(new_model.objective) + assert old_obj == pytest.approx(new_obj) + if __name__ == "__main__": unittest.main() From b63358c5eb6578102917d07e00380fa3188973e3 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 16 Apr 2024 14:57:55 -0400 Subject: [PATCH 1187/3044] add highs version requirements for pr workflow --- .github/workflows/test_pr_and_main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index a2060240391..28c72541a13 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -606,7 +606,7 @@ jobs: shell: bash run: | echo "NOTE: temporarily pinning to highspy pre-release for testing" - $PYTHON_EXE -m pip install --cache-dir cache/pip highspy==1.7.1.dev1 \ + $PYTHON_EXE -m pip install --cache-dir cache/pip highspy>=1.7.1.dev1 \ || echo "WARNING: highspy is not available" - name: Set up coverage tracking From 747da89e8124d04955ebc161a74c227f658289e6 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 16 Apr 2024 14:43:14 -0600 Subject: [PATCH 1188/3044] TEMPORARY FIX: Pin to mpi4py 3.1.5 --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 55f903a37f9..a15240194f2 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -92,7 +92,7 @@ jobs: skip_doctest: 1 TARGET: linux PYENV: conda - PACKAGES: mpi4py + PACKAGES: mpi4py==3.1.5 - os: ubuntu-latest python: '3.10' diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 76ec6de951a..2615f6b838e 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -93,7 +93,7 @@ jobs: skip_doctest: 1 TARGET: linux PYENV: conda - PACKAGES: mpi4py + PACKAGES: mpi4py==3.1.5 - os: ubuntu-latest python: '3.11' From 28f5080a4b3506200380d89bbe2cfedf9e23a282 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 16 Apr 2024 20:09:07 -0600 Subject: [PATCH 1189/3044] nlv2: fix error reporting number of nonlinear discrete variables --- pyomo/repn/plugins/nl_writer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index ee5b65149ae..d629da2ee87 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1277,8 +1277,8 @@ def write(self, model): len(linear_binary_vars), len(linear_integer_vars), len(both_vars_nonlinear.intersection(discrete_vars)), - len(con_vars_nonlinear.intersection(discrete_vars)), - len(obj_vars_nonlinear.intersection(discrete_vars)), + len(con_only_nonlinear_vars.intersection(discrete_vars)), + len(obj_only_nonlinear_vars.intersection(discrete_vars)), ) ) # From be3ca3192d5b9a91a9834f001fa07b352b30b8aa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 16 Apr 2024 20:09:50 -0600 Subject: [PATCH 1190/3044] nlv2: map integer variables over [0,1] t binary --- pyomo/repn/plugins/nl_writer.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index d629da2ee87..ab74f0ab44d 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -113,6 +113,7 @@ TOL = 1e-8 inf = float('inf') minus_inf = -inf +zero_one = {0, 1} _CONSTANT = ExprType.CONSTANT _MONOMIAL = ExprType.MONOMIAL @@ -882,7 +883,13 @@ def write(self, model): elif v.is_binary(): binary_vars.add(_id) elif v.is_integer(): - integer_vars.add(_id) + bnd = var_bounds[_id] + # Note: integer variables whose bounds are in {0, 1} + # should be classified as binary + if bnd[1] in zero_one and bnd[0] in zero_one: + binary_vars.add(_id) + else: + integer_vars.add(_id) else: raise ValueError( f"Variable '{v.name}' has a domain that is not Real, " From 2f585db8187952149c0974d14e6ce99ab20b4f31 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 16 Apr 2024 20:10:09 -0600 Subject: [PATCH 1191/3044] nlv2: add tests for variable categorization --- pyomo/repn/tests/ampl/test_nlv2.py | 134 ++++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index be72025edcd..0f2bacaea8b 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -42,6 +42,8 @@ Suffix, Constraint, Expression, + Binary, + Integers, ) import pyomo.environ as pyo @@ -1266,7 +1268,7 @@ def test_nonfloat_constants(self): 0 0 #network constraints: nonlinear, linear 0 0 0 #nonlinear vars in constraints, objectives, both 0 0 0 1 #linear network variables; functions; arith, flags - 0 4 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 4 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) 4 4 #nonzeros in Jacobian, obj. gradient 6 4 #max name lengths: constraints, variables 0 0 0 0 0 #common exprs: b,c,o,c1,o1 @@ -2165,6 +2167,136 @@ def test_named_expressions(self): 0 0 1 0 2 0 +""", + OUT.getvalue(), + ) + ) + + def test_discrete_var_tabulation(self): + # This tests an error reported in #3235 + # + # Among other issues, this verifies that nonlinear discrete + # variables are tabulated correctly (header line 7), and that + # integer variables with bounds in {0, 1} are mapped to binary + # variables. + m = ConcreteModel() + m.p1 = Var(bounds=(0.85, 1.15)) + m.p2 = Var(bounds=(0.68, 0.92)) + m.c1 = Var(bounds=(-0.0, 0.7)) + m.c2 = Var(bounds=(-0.0, 0.7)) + m.t1 = Var(within=Binary, bounds=(0, 1)) + m.t2 = Var(within=Binary, bounds=(0, 1)) + m.t3 = Var(within=Binary, bounds=(0, 1)) + m.t4 = Var(within=Binary, bounds=(0, 1)) + m.t5 = Var(within=Integers, bounds=(0, None)) + m.t6 = Var(within=Integers, bounds=(0, None)) + m.x1 = Var(within=Binary) + m.x2 = Var(within=Integers, bounds=(0, 1)) + m.x3 = Var(within=Integers, bounds=(0, None)) + m.const = Constraint(expr=((0.7 - (m.c1*m.t1 + m.c2*m.t2)) <= (m.p1*m.t1 + m.p2*m.t2 + m.p1*m.t4 + m.t6*m.t5))) + m.OBJ = Objective(expr=(m.p1*m.t1 + m.p2*m.t2 + m.p2*m.t3 + m.x1 + m.x2 + m.x3)) + + OUT = io.StringIO() + nl_writer.NLWriter().write(m, OUT, symbolic_solver_labels=True) + + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 13 1 1 0 0 #vars, constraints, objectives, ranges, eqns + 1 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 9 10 4 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 2 1 2 3 1 #discrete variables: binary, integer, nonlinear (b,c,o) + 9 8 #nonzeros in Jacobian, obj. gradient + 5 2 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +C0 #const +o0 #+ +o16 #- +o0 #+ +o2 #* +v4 #c1 +v2 #t1 +o2 #* +v5 #c2 +v3 #t2 +o16 #- +o54 #sumlist +4 #(n) +o2 #* +v0 #p1 +v2 #t1 +o2 #* +v1 #p2 +v3 #t2 +o2 #* +v0 #p1 +v6 #t4 +o2 #* +v7 #t6 +v8 #t5 +O0 0 #OBJ +o54 #sumlist +3 #(n) +o2 #* +v0 #p1 +v2 #t1 +o2 #* +v1 #p2 +v3 #t2 +o2 #* +v1 #p2 +v9 #t3 +x0 #initial guess +r #1 ranges (rhs's) +1 -0.7 #const +b #13 bounds (on variables) +0 0.85 1.15 #p1 +0 0.68 0.92 #p2 +0 0 1 #t1 +0 0 1 #t2 +0 -0.0 0.7 #c1 +0 -0.0 0.7 #c2 +0 0 1 #t4 +2 0 #t6 +2 0 #t5 +0 0 1 #t3 +0 0 1 #x1 +0 0 1 #x2 +2 0 #x3 +k12 #intermediate Jacobian column lengths +1 +2 +3 +4 +5 +6 +7 +8 +9 +9 +9 +9 +J0 9 #const +0 0 +1 0 +2 0 +3 0 +4 0 +5 0 +6 0 +7 0 +8 0 +G0 8 #OBJ +0 0 +1 0 +2 0 +3 0 +9 0 +10 1 +11 1 +12 1 """, OUT.getvalue(), ) From 25a7344df15fc60378a168a1adfb18496873f375 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 16 Apr 2024 20:25:10 -0600 Subject: [PATCH 1192/3044] NFC: apply black --- pyomo/repn/tests/ampl/test_nlv2.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 0f2bacaea8b..27d129ca886 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2193,8 +2193,15 @@ def test_discrete_var_tabulation(self): m.x1 = Var(within=Binary) m.x2 = Var(within=Integers, bounds=(0, 1)) m.x3 = Var(within=Integers, bounds=(0, None)) - m.const = Constraint(expr=((0.7 - (m.c1*m.t1 + m.c2*m.t2)) <= (m.p1*m.t1 + m.p2*m.t2 + m.p1*m.t4 + m.t6*m.t5))) - m.OBJ = Objective(expr=(m.p1*m.t1 + m.p2*m.t2 + m.p2*m.t3 + m.x1 + m.x2 + m.x3)) + m.const = Constraint( + expr=( + (0.7 - (m.c1 * m.t1 + m.c2 * m.t2)) + <= (m.p1 * m.t1 + m.p2 * m.t2 + m.p1 * m.t4 + m.t6 * m.t5) + ) + ) + m.OBJ = Objective( + expr=(m.p1 * m.t1 + m.p2 * m.t2 + m.p2 * m.t3 + m.x1 + m.x2 + m.x3) + ) OUT = io.StringIO() nl_writer.NLWriter().write(m, OUT, symbolic_solver_labels=True) From 6b1e2960e94172a10802326b6e7b848c4c7b4ab4 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 17 Apr 2024 00:36:04 -0400 Subject: [PATCH 1193/3044] fix test pr highs version bug --- .github/workflows/test_pr_and_main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 9eb6d362bb8..711e134e401 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -606,7 +606,7 @@ jobs: shell: bash run: | echo "NOTE: temporarily pinning to highspy pre-release for testing" - $PYTHON_EXE -m pip install --cache-dir cache/pip highspy>=1.7.1.dev1 \ + $PYTHON_EXE -m pip install --cache-dir cache/pip "highspy>=1.7.1.dev1" \ || echo "WARNING: highspy is not available" - name: Set up coverage tracking From 76ba0eb525b5a2037220b84e17edde93715cb599 Mon Sep 17 00:00:00 2001 From: MAiNGO-github <139969768+MAiNGO-github@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:29:14 +0200 Subject: [PATCH 1194/3044] Update pyomo/contrib/appsi/solvers/maingo.py Co-authored-by: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> --- pyomo/contrib/appsi/solvers/maingo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 29464e6a876..017841d1b8c 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -384,7 +384,7 @@ def _postsolve(self, timer: HierarchicalTimer): results.termination_condition = TerminationCondition.optimal if status == maingopy.FEASIBLE_POINT: logger.warning( - "MAiNGO did only find a feasible solution but did not prove its global optimality." + "MAiNGO found a feasible solution but did not prove its global optimality." ) elif status == maingopy.INFEASIBLE: results.termination_condition = TerminationCondition.infeasible From f9ada2946d295da4d53e56c3a7cc2f78e5bbaa1e Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:47:11 +0200 Subject: [PATCH 1195/3044] Restrict y to positive values (MAiNGO cannot handle 1/0) --- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index c063adc2bfe..58806d1e86c 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -688,7 +688,7 @@ def test_fixed_vars_4( raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var() - m.y = pe.Var() + m.y = pe.Var(bounds=(1e-6, None)) m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.x == 2 / m.y) m.y.fix(1) From 42fcd17429cc2b608cc26544dc19c8cd3074bbb9 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:48:15 +0200 Subject: [PATCH 1196/3044] Restrict y to positive values (MAiNGO cannot handle log(negative)) --- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 58806d1e86c..5a46c1d3e5b 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -858,7 +858,7 @@ def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() - m.x = pe.Var(initialize=1) + m.x = pe.Var(initialize=1, bounds=(1e-6, None)) m.y = pe.Var() m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.y <= pe.log(m.x)) From bec9be7e9e3f4c5d221ddb076ddcf7697616182b Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:49:48 +0200 Subject: [PATCH 1197/3044] Exluded MAiNGO for checking unbounded termination criterion --- .../solvers/tests/test_persistent_solvers.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 5a46c1d3e5b..660ba60f26f 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1090,13 +1090,14 @@ def test_objective_changes( m.obj.sense = pe.maximize opt.config.load_solution = False res = opt.solve(m) - self.assertIn( - res.termination_condition, - { - TerminationCondition.unbounded, - TerminationCondition.infeasibleOrUnbounded, - }, - ) + if not isinstance(opt, MAiNGO): + self.assertIn( + res.termination_condition, + { + TerminationCondition.unbounded, + TerminationCondition.infeasibleOrUnbounded, + }, + ) m.obj.sense = pe.minimize opt.config.load_solution = True m.obj = pe.Objective(expr=m.x * m.y) From 6db9970a41760b64cf5e43ddc73eb3f8eb0aefc0 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:54:44 +0200 Subject: [PATCH 1198/3044] Create MAiNGOTest class with tighter tolerances to pass tests --- pyomo/contrib/appsi/solvers/maingo.py | 16 ++++++++++++++++ .../solvers/tests/test_persistent_solvers.py | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 29464e6a876..bff8d6f594e 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -201,6 +201,9 @@ def _solve(self, timer: HierarchicalTimer): self._mymaingo.set_option("loggingDestination", 2) self._mymaingo.set_log_file_name(config.logfile) + self._mymaingo.set_option("epsilonA", 1e-4) + self._mymaingo.set_option("epsilonR", 1e-4) + self._set_maingo_options() if config.time_limit is not None: self._mymaingo.set_option("maxTime", config.time_limit) @@ -480,3 +483,16 @@ def get_reduced_costs(self, vars_to_load=None): def get_duals(self, cons_to_load=None): raise ValueError("MAiNGO does not support returning Duals") + + + def _set_maingo_options(self): + pass + + +# Solver class with tighter tolerances for testing +class MAiNGOTest(MAiNGO): + def _set_maingo_options(self): + self._mymaingo.set_option("epsilonA", 1e-8) + self._mymaingo.set_option("epsilonR", 1e-8) + self._mymaingo.set_option("deltaIneq", 1e-9) + self._mymaingo.set_option("deltaEq", 1e-9) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 660ba60f26f..23440065491 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -17,7 +17,8 @@ parameterized = parameterized.parameterized from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available -from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs, MAiNGO +from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs +from pyomo.contrib.appsi.solvers import MAiNGOTest as MAiNGO from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression import os From de7fc5c68360bb1c49d351eca21ffff3d7cf4d60 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:56:01 +0200 Subject: [PATCH 1199/3044] Exclude duals and RCs for tests with MAiNGO --- .../solvers/tests/test_persistent_solvers.py | 176 ++++++++++-------- 1 file changed, 99 insertions(+), 77 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 23440065491..f50461af373 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -185,14 +185,16 @@ def test_range_constraint( res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c], 1) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c], 1) m.obj.sense = pe.maximize res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c], 1) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs( @@ -209,9 +211,10 @@ def test_reduced_costs( self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, -2) - rc = opt.get_reduced_costs() - self.assertAlmostEqual(rc[m.x], 3) - self.assertAlmostEqual(rc[m.y], 4) + if not opt_class is MAiNGO: + rc = opt.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 3) + self.assertAlmostEqual(rc[m.y], 4) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs2( @@ -226,14 +229,16 @@ def test_reduced_costs2( res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) - rc = opt.get_reduced_costs() - self.assertAlmostEqual(rc[m.x], 1) + if not opt_class is MAiNGO: + rc = opt.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) m.obj.sense = pe.maximize res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) - rc = opt.get_reduced_costs() - self.assertAlmostEqual(rc[m.x], 1) + if not opt_class is MAiNGO: + rc = opt.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_param_changes( @@ -265,9 +270,10 @@ def test_param_changes( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_immutable_param( @@ -303,9 +309,10 @@ def test_immutable_param( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_equality( @@ -337,9 +344,10 @@ def test_equality( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_linear_expression( @@ -407,9 +415,10 @@ def test_no_objective( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertEqual(res.best_feasible_objective, None) self.assertEqual(res.best_objective_bound, None) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], 0) - self.assertAlmostEqual(duals[m.c2], 0) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], 0) + self.assertAlmostEqual(duals[m.c2], 0) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_remove_cons( @@ -436,9 +445,10 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) m.c3 = pe.Constraint(expr=m.y >= a3 * m.x + b3) res = opt.solve(m) @@ -447,10 +457,11 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) - self.assertAlmostEqual(duals[m.c2], 0) - self.assertAlmostEqual(duals[m.c3], a1 / (a3 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) + self.assertAlmostEqual(duals[m.c2], 0) + self.assertAlmostEqual(duals[m.c3], a1 / (a3 - a1)) del m.c3 res = opt.solve(m) @@ -459,9 +470,10 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_results_infeasible( @@ -500,14 +512,15 @@ def test_results_infeasible( RuntimeError, '.*does not currently have a valid solution.*' ): res.solution_loader.load_vars() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid duals.*' - ): - res.solution_loader.get_duals() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid reduced costs.*' - ): - res.solution_loader.get_reduced_costs() + if not opt_class is MAiNGO: + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid duals.*' + ): + res.solution_loader.get_duals() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid reduced costs.*' + ): + res.solution_loader.get_reduced_costs() @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): @@ -524,13 +537,14 @@ def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_va res = opt.solve(m) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 1) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], 0.5) - self.assertAlmostEqual(duals[m.c2], 0.5) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], 0.5) + self.assertAlmostEqual(duals[m.c2], 0.5) - duals = opt.get_duals(cons_to_load=[m.c1]) - self.assertAlmostEqual(duals[m.c1], 0.5) - self.assertNotIn(m.c2, duals) + duals = opt.get_duals(cons_to_load=[m.c1]) + self.assertAlmostEqual(duals[m.c1], 0.5) + self.assertNotIn(m.c2, duals) @parameterized.expand(input=_load_tests(qcp_solvers, only_child_vars_options)) def test_mutable_quadratic_coefficient( @@ -778,17 +792,19 @@ def test_mutable_param_with_range( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) self.assertTrue(res.best_objective_bound <= m.y.value + 1e-12) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) - self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) + self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) else: self.assertAlmostEqual(m.x.value, (c2 - c1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) self.assertTrue(res.best_objective_bound >= m.y.value - 1e-12) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) - self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) + self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_and_remove_vars( @@ -986,24 +1002,25 @@ def test_solution_loader( self.assertNotIn(m.x, primals) self.assertIn(m.y, primals) self.assertAlmostEqual(primals[m.y], 1) - reduced_costs = res.solution_loader.get_reduced_costs() - self.assertIn(m.x, reduced_costs) - self.assertIn(m.y, reduced_costs) - self.assertAlmostEqual(reduced_costs[m.x], 1) - self.assertAlmostEqual(reduced_costs[m.y], 0) - reduced_costs = res.solution_loader.get_reduced_costs([m.y]) - self.assertNotIn(m.x, reduced_costs) - self.assertIn(m.y, reduced_costs) - self.assertAlmostEqual(reduced_costs[m.y], 0) - duals = res.solution_loader.get_duals() - self.assertIn(m.c1, duals) - self.assertIn(m.c2, duals) - self.assertAlmostEqual(duals[m.c1], 1) - self.assertAlmostEqual(duals[m.c2], 0) - duals = res.solution_loader.get_duals([m.c1]) - self.assertNotIn(m.c2, duals) - self.assertIn(m.c1, duals) - self.assertAlmostEqual(duals[m.c1], 1) + if not opt_class is MAiNGO: + reduced_costs = res.solution_loader.get_reduced_costs() + self.assertIn(m.x, reduced_costs) + self.assertIn(m.y, reduced_costs) + self.assertAlmostEqual(reduced_costs[m.x], 1) + self.assertAlmostEqual(reduced_costs[m.y], 0) + reduced_costs = res.solution_loader.get_reduced_costs([m.y]) + self.assertNotIn(m.x, reduced_costs) + self.assertIn(m.y, reduced_costs) + self.assertAlmostEqual(reduced_costs[m.y], 0) + duals = res.solution_loader.get_duals() + self.assertIn(m.c1, duals) + self.assertIn(m.c2, duals) + self.assertAlmostEqual(duals[m.c1], 1) + self.assertAlmostEqual(duals[m.c2], 0) + duals = res.solution_loader.get_duals([m.c1]) + self.assertNotIn(m.c2, duals) + self.assertIn(m.c1, duals) + self.assertAlmostEqual(duals[m.c1], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_time_limit( @@ -1373,7 +1390,8 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): m.obj = pe.Objective(expr=m.y) m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) - m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) + if not opt_class is MAiNGO: + m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] for a1, a2, b1, b2 in params_to_test: @@ -1385,8 +1403,9 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): pe.assert_optimal_termination(res) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(m.dual[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) + if not opt_class is MAiNGO: + self.assertAlmostEqual(m.dual[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=all_solvers) def test_load_solutions(self, name: str, opt_class: Type[PersistentSolver]): @@ -1397,11 +1416,14 @@ def test_load_solutions(self, name: str, opt_class: Type[PersistentSolver]): m.x = pe.Var() m.obj = pe.Objective(expr=m.x) m.c = pe.Constraint(expr=(-1, m.x, 1)) - m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) + if not opt_class is MAiNGO: + m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) res = opt.solve(m, load_solutions=False) pe.assert_optimal_termination(res) self.assertIsNone(m.x.value) - self.assertNotIn(m.c, m.dual) + if not opt_class is MAiNGO: + self.assertNotIn(m.c, m.dual) m.solutions.load_from(res) self.assertAlmostEqual(m.x.value, -1) - self.assertAlmostEqual(m.dual[m.c], 1) + if not opt_class is MAiNGO: + self.assertAlmostEqual(m.dual[m.c], 1) From 334b06762bbae40d954ae0ca54720f4f6c448893 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:56:31 +0200 Subject: [PATCH 1200/3044] MAiNGOTest class in __init__ --- pyomo/contrib/appsi/solvers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/__init__.py b/pyomo/contrib/appsi/solvers/__init__.py index 352571b98f8..c1ebdf28780 100644 --- a/pyomo/contrib/appsi/solvers/__init__.py +++ b/pyomo/contrib/appsi/solvers/__init__.py @@ -15,4 +15,4 @@ from .cplex import Cplex from .highs import Highs from .wntr import Wntr, WntrResults -from .maingo import MAiNGO +from .maingo import MAiNGO, MAiNGOTest From a1e6b92c7518027f8234069acedc9dac86d8b04a Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:57:07 +0200 Subject: [PATCH 1201/3044] Add copyright statement --- pyomo/contrib/appsi/solvers/maingo.py | 11 +++++++++++ pyomo/contrib/appsi/solvers/maingo_solvermodel.py | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index bff8d6f594e..d48e9874712 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from collections import namedtuple import logging import math diff --git a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py index 4abc53ae290..686d7c54657 100644 --- a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py +++ b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import math from pyomo.common.dependencies import attempt_import From 1fab90b84754bd652263a8eed467f081d0de603a Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:58:03 +0200 Subject: [PATCH 1202/3044] Set default for ConfigValues --- pyomo/contrib/appsi/solvers/maingo.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index d48e9874712..ee85d4549a5 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -98,13 +98,11 @@ def __init__( visibility=visibility, ) - self.declare("logfile", ConfigValue(domain=str)) - self.declare("solver_output_logger", ConfigValue()) - self.declare("log_level", ConfigValue(domain=NonNegativeInt)) - - self.logfile = "" - self.solver_output_logger = logger - self.log_level = logging.INFO + self.declare("logfile", ConfigValue(domain=str, default="")) + self.declare("solver_output_logger", ConfigValue(default=logger)) + self.declare( + "log_level", ConfigValue(domain=NonNegativeInt, default=logging.INFO) + ) class MAiNGOSolutionLoader(PersistentSolutionLoader): From 5b70135f03575af6c1ef6ddce6bd98c12846194e Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:58:30 +0200 Subject: [PATCH 1203/3044] Remove check for Python version --- pyomo/contrib/appsi/solvers/maingo.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index ee85d4549a5..0886ce21c75 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -151,15 +151,9 @@ def available(self): return self._available def version(self): - # Check if Python >= 3.8 - if sys.version_info.major >= 3 and sys.version_info.minor >= 8: - from importlib.metadata import version + import pkg_resources - version = version('maingopy') - else: - import pkg_resources - - version = pkg_resources.get_distribution('maingopy').version + version = pkg_resources.get_distribution('maingopy').version return tuple(int(k) for k in version.split('.')) From 55b7dff375cc9c82cd2d425698c521090711a816 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 16:00:41 +0200 Subject: [PATCH 1204/3044] Add missing functionalities and fix bugs --- pyomo/contrib/appsi/solvers/maingo.py | 81 ++++++++++++++----- .../appsi/solvers/maingo_solvermodel.py | 51 ++++++++---- 2 files changed, 97 insertions(+), 35 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 0886ce21c75..eabfdd36267 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -120,6 +120,7 @@ def __init__(self, solver): super(MAiNGOResults, self).__init__() self.wallclock_time = None self.cpu_time = None + self.globally_optimal = None self.solution_loader = MAiNGOSolutionLoader(solver=solver) @@ -228,9 +229,14 @@ def solve(self, model, timer: HierarchicalTimer = None): self._last_results_object.solution_loader.invalidate() if timer is None: timer = HierarchicalTimer() - timer.start("set_instance") - self.set_instance(model) - timer.stop("set_instance") + if model is not self._model: + timer.start("set_instance") + self.set_instance(model) + timer.stop("set_instance") + else: + timer.start("Update") + self.update(timer=timer) + timer.stop("Update") res = self._solve(timer) self._last_results_object = res if self.config.report_timing: @@ -285,7 +291,7 @@ def _process_domain_and_bounds(self, var): return lb, ub, vtype def _add_variables(self, variables: List[_GeneralVarData]): - for ndx, var in enumerate(variables): + for var in variables: varname = self._symbol_map.getSymbol(var, self._labeler) lb, ub, vtype = self._process_domain_and_bounds(var) self._maingo_vars.append( @@ -331,10 +337,11 @@ def set_instance(self, model): con_list=self._cons, objective=self._objective, idmap=self._pyomo_var_to_solver_var_id_map, + logger=logger, ) def _add_constraints(self, cons: List[_GeneralConstraintData]): - self._cons = cons + self._cons += cons def _add_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) >= 1: @@ -344,7 +351,8 @@ def _add_sos_constraints(self, cons: List[_SOSConstraintData]): pass def _remove_constraints(self, cons: List[_GeneralConstraintData]): - pass + for con in cons: + self._cons.remove(con) def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) >= 1: @@ -354,28 +362,48 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): pass def _remove_variables(self, variables: List[_GeneralVarData]): - pass + removed_maingo_vars = [] + for var in variables: + varname = self._symbol_map.getSymbol(var, self._labeler) + del self._maingo_vars[self._pyomo_var_to_solver_var_id_map[id(var)]] + removed_maingo_vars += [self._pyomo_var_to_solver_var_id_map[id(var)]] + del self._pyomo_var_to_solver_var_id_map[id(var)] + + for pyomo_var, maingo_var_id in self._pyomo_var_to_solver_var_id_map.items(): + # How many variables before current var where removed? + num_removed = 0 + for removed_var in removed_maingo_vars: + if removed_var <= maingo_var_id: + num_removed += 1 + self._pyomo_var_to_solver_var_id_map[pyomo_var] = ( + maingo_var_id - num_removed + ) def _remove_params(self, params: List[_ParamData]): pass def _update_variables(self, variables: List[_GeneralVarData]): - pass + for var in variables: + if id(var) not in self._pyomo_var_to_solver_var_id_map: + raise ValueError( + 'The Var provided to update_var needs to be added first: {0}'.format( + var + ) + ) + lb, ub, vtype = self._process_domain_and_bounds(var) + self._maingo_vars[self._pyomo_var_to_solver_var_id_map[id(var)]] = ( + MaingoVar(name=var.name, type=vtype, lb=lb, ub=ub, init=var.value) + ) def update_params(self): - pass + vars = [var[0] for var in self._vars.values()] + self._update_variables(vars) def _set_objective(self, obj): - if obj is None: - raise NotImplementedError( - "MAiNGO needs a objective. Please set a dummy objective." - ) - else: - if not obj.sense in {minimize, maximize}: - raise ValueError( - "Objective sense is not recognized: {0}".format(obj.sense) - ) - self._objective = obj + + if not obj.sense in {minimize, maximize}: + raise ValueError("Objective sense is not recognized: {0}".format(obj.sense)) + self._objective = obj def _postsolve(self, timer: HierarchicalTimer): config = self.config @@ -388,7 +416,9 @@ def _postsolve(self, timer: HierarchicalTimer): if status in {maingopy.GLOBALLY_OPTIMAL, maingopy.FEASIBLE_POINT}: results.termination_condition = TerminationCondition.optimal + results.globally_optimal = True if status == maingopy.FEASIBLE_POINT: + results.globally_optimal = False logger.warning( "MAiNGO did only find a feasible solution but did not prove its global optimality." ) @@ -425,8 +455,8 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start("load solution") if config.load_solution: - if not results.best_feasible_objective is None: - if results.termination_condition != TerminationCondition.optimal: + if results.termination_condition is TerminationCondition.optimal: + if not results.globally_optimal: logger.warning( "Loading a feasible but suboptimal solution. " "Please set load_solution=False and check " @@ -487,6 +517,15 @@ def get_reduced_costs(self, vars_to_load=None): def get_duals(self, cons_to_load=None): raise ValueError("MAiNGO does not support returning Duals") + def update(self, timer: HierarchicalTimer = None): + super(MAiNGO, self).update(timer=timer) + self._solver_model = maingo_solvermodel.SolverModel( + var_list=self._maingo_vars, + con_list=self._cons, + objective=self._objective, + idmap=self._pyomo_var_to_solver_var_id_map, + logger=logger, + ) def _set_maingo_options(self): pass diff --git a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py index 686d7c54657..ca746c4a9b7 100644 --- a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py +++ b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py @@ -13,6 +13,7 @@ from pyomo.common.dependencies import attempt_import from pyomo.core.base.var import ScalarVar +from pyomo.core.base.expression import ScalarExpression import pyomo.core.expr.expr_common as common import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import ( @@ -178,19 +179,14 @@ def visiting_potential_leaf(self, node): return True, maingo_var def _monomial_to_maingo(self, node): - if node.__class__ is ScalarVar: - var = node - const = 1 - else: - const, var = node.args - maingo_var_id = self.idmap[id(var)] - maingo_var = self.variables[maingo_var_id] + const, var = node.args if const.__class__ not in native_types: const = value(const) if var.is_fixed(): return const * var.value if not const: return 0 + maingo_var = self._var_to_maingo(var) if const in _plusMinusOne: if const < 0: return -maingo_var @@ -198,12 +194,25 @@ def _monomial_to_maingo(self, node): return maingo_var return const * maingo_var + def _var_to_maingo(self, var): + maingo_var_id = self.idmap[id(var)] + maingo_var = self.variables[maingo_var_id] + return maingo_var + def _linear_to_maingo(self, node): values = [ ( self._monomial_to_maingo(arg) - if (arg.__class__ in {EXPR.MonomialTermExpression, ScalarVar}) - else (value(arg)) + if (arg.__class__ is EXPR.MonomialTermExpression) + else ( + value(arg) + if arg.__class__ in native_numeric_types + else ( + self._var_to_maingo(arg) + if arg.is_variable_type() + else value(arg) + ) + ) ) for arg in node.args ] @@ -211,17 +220,25 @@ def _linear_to_maingo(self, node): class SolverModel(maingopy.MAiNGOmodel): - def __init__(self, var_list, objective, con_list, idmap): + def __init__(self, var_list, objective, con_list, idmap, logger): maingopy.MAiNGOmodel.__init__(self) self._var_list = var_list self._con_list = con_list self._objective = objective self._idmap = idmap + self._logger = logger + self._no_objective = False + + if self._objective is None: + self._logger.warning("No objective given, setting a dummy objective of 1.") + self._no_objective = True def build_maingo_objective(self, obj, visitor): + if self._no_objective: + return visitor.variables[-1] maingo_obj = visitor.dfs_postorder_stack(obj.expr) if obj.sense == maximize: - maingo_obj *= -1 + return -1 * maingo_obj return maingo_obj def build_maingo_constraints(self, cons, visitor): @@ -235,7 +252,7 @@ def build_maingo_constraints(self, cons, visitor): ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] elif con.has_ub(): ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] - elif con.has_ub(): + elif con.has_lb(): ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] else: raise ValueError( @@ -245,18 +262,24 @@ def build_maingo_constraints(self, cons, visitor): return eqs, ineqs def get_variables(self): - return [ + vars = [ maingopy.OptimizationVariable( maingopy.Bounds(var.lb, var.ub), var.type, var.name ) for var in self._var_list ] + if self._no_objective: + vars += [maingopy.OptimizationVariable(maingopy.Bounds(1, 1), "dummy_obj")] + return vars def get_initial_point(self): - return [ + initial = [ var.init if not var.init is None else (var.lb + var.ub) / 2.0 for var in self._var_list ] + if self._no_objective: + initial += [1] + return initial def evaluate(self, maingo_vars): visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) From 380f32cb5b1a044002c7c93f5fac394c5ca9c3e2 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 09:28:54 +0200 Subject: [PATCH 1205/3044] Register MAiNGO in SolverFactory --- pyomo/contrib/appsi/plugins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index fbe81484eba..3e1b639ce3b 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -11,7 +11,7 @@ from pyomo.common.extensions import ExtensionBuilderFactory from .base import SolverFactory -from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs +from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs, MAiNGO from .build import AppsiBuilder @@ -30,3 +30,6 @@ def load(): SolverFactory.register(name='highs', doc='Automated persistent interface to Highs')( Highs ) + SolverFactory.register( + name='maingo', doc='Automated persistent interface to MAiNGO' + )(MAiNGO) From cf560a626c56aac304c269c02c06d783c42957c0 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 09:31:50 +0200 Subject: [PATCH 1206/3044] Reformulate confusing comment --- pyomo/contrib/appsi/solvers/maingo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index d542542f543..f95a943bed3 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -369,8 +369,8 @@ def _remove_variables(self, variables: List[_GeneralVarData]): removed_maingo_vars += [self._pyomo_var_to_solver_var_id_map[id(var)]] del self._pyomo_var_to_solver_var_id_map[id(var)] + # Update _pyomo_var_to_solver_var_id_map to account for removed variables for pyomo_var, maingo_var_id in self._pyomo_var_to_solver_var_id_map.items(): - # How many variables before current var where removed? num_removed = 0 for removed_var in removed_maingo_vars: if removed_var <= maingo_var_id: From 3e477c929883d8a555fdde8afeb99b435f0be829 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 09:33:45 +0200 Subject: [PATCH 1207/3044] Skip tests with problematic log and 1/x --- .../appsi/solvers/tests/test_persistent_solvers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index f50461af373..b569b305a07 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -699,11 +699,11 @@ def test_fixed_vars_4( ): opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True - if not opt.available(): + if not opt.available() or opt_class in MAiNGO: raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var() - m.y = pe.Var(bounds=(1e-6, None)) + m.y = pe.Var() m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.x == 2 / m.y) m.y.fix(1) @@ -872,10 +872,10 @@ def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): opt = opt_class(only_child_vars=only_child_vars) - if not opt.available(): + if not opt.available() or opt_class in MAiNGO: raise unittest.SkipTest m = pe.ConcreteModel() - m.x = pe.Var(initialize=1, bounds=(1e-6, None)) + m.x = pe.Var(initialize=1) m.y = pe.Var() m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.y <= pe.log(m.x)) From d892473bca89ae62fd69b53eb0585b769cd91a60 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 09:42:04 +0200 Subject: [PATCH 1208/3044] Add maingo to options --- pyomo/contrib/appsi/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 201e5975ac9..13a841437ac 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -1665,7 +1665,7 @@ def license_is_valid(self) -> bool: @property def options(self): - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs', 'maingo']: if hasattr(self, solver_name + '_options'): return getattr(self, solver_name + '_options') raise NotImplementedError('Could not find the correct options') @@ -1673,7 +1673,7 @@ def options(self): @options.setter def options(self, val): found = False - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs', 'maingo']: if hasattr(self, solver_name + '_options'): setattr(self, solver_name + '_options', val) found = True From d026ee135f72788536b0586e6815fcc561a0ba86 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 09:42:30 +0200 Subject: [PATCH 1209/3044] Rewrite check for MAiNGO --- .../solvers/tests/test_persistent_solvers.py | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index b569b305a07..2207ba70f4e 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -185,14 +185,14 @@ def test_range_constraint( res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) m.obj.sense = pe.maximize res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) @@ -211,7 +211,7 @@ def test_reduced_costs( self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, -2) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 3) self.assertAlmostEqual(rc[m.y], 4) @@ -229,14 +229,14 @@ def test_reduced_costs2( res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) m.obj.sense = pe.maximize res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) @@ -270,7 +270,7 @@ def test_param_changes( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -309,7 +309,7 @@ def test_immutable_param( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -344,7 +344,7 @@ def test_equality( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @@ -415,7 +415,7 @@ def test_no_objective( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertEqual(res.best_feasible_objective, None) self.assertEqual(res.best_objective_bound, None) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], 0) self.assertAlmostEqual(duals[m.c2], 0) @@ -445,7 +445,7 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -457,7 +457,7 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) self.assertAlmostEqual(duals[m.c2], 0) @@ -470,7 +470,7 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -512,7 +512,7 @@ def test_results_infeasible( RuntimeError, '.*does not currently have a valid solution.*' ): res.solution_loader.load_vars() - if not opt_class is MAiNGO: + if opt_class != MAiNGO: with self.assertRaisesRegex( RuntimeError, '.*does not currently have valid duals.*' ): @@ -537,7 +537,7 @@ def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_va res = opt.solve(m) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], 0.5) self.assertAlmostEqual(duals[m.c2], 0.5) @@ -699,7 +699,7 @@ def test_fixed_vars_4( ): opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True - if not opt.available() or opt_class in MAiNGO: + if not opt.available() or opt_class == MAiNGO: raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var() @@ -792,7 +792,7 @@ def test_mutable_param_with_range( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) self.assertTrue(res.best_objective_bound <= m.y.value + 1e-12) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @@ -801,7 +801,7 @@ def test_mutable_param_with_range( self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) self.assertTrue(res.best_objective_bound >= m.y.value - 1e-12) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @@ -872,7 +872,7 @@ def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): opt = opt_class(only_child_vars=only_child_vars) - if not opt.available() or opt_class in MAiNGO: + if not opt.available() or opt_class == MAiNGO: raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var(initialize=1) @@ -1002,7 +1002,7 @@ def test_solution_loader( self.assertNotIn(m.x, primals) self.assertIn(m.y, primals) self.assertAlmostEqual(primals[m.y], 1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: reduced_costs = res.solution_loader.get_reduced_costs() self.assertIn(m.x, reduced_costs) self.assertIn(m.y, reduced_costs) @@ -1108,7 +1108,7 @@ def test_objective_changes( m.obj.sense = pe.maximize opt.config.load_solution = False res = opt.solve(m) - if not isinstance(opt, MAiNGO): + if opt_class != MAiNGO: self.assertIn( res.termination_condition, { @@ -1390,7 +1390,7 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): m.obj = pe.Objective(expr=m.y) m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] @@ -1403,7 +1403,7 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): pe.assert_optimal_termination(res) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: self.assertAlmostEqual(m.dual[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) @@ -1416,14 +1416,14 @@ def test_load_solutions(self, name: str, opt_class: Type[PersistentSolver]): m.x = pe.Var() m.obj = pe.Objective(expr=m.x) m.c = pe.Constraint(expr=(-1, m.x, 1)) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) res = opt.solve(m, load_solutions=False) pe.assert_optimal_termination(res) self.assertIsNone(m.x.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: self.assertNotIn(m.c, m.dual) m.solutions.load_from(res) self.assertAlmostEqual(m.x.value, -1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: self.assertAlmostEqual(m.dual[m.c], 1) From cf3c9da2a17cff71953e2ced4f24f999835542ba Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 12:26:32 +0200 Subject: [PATCH 1210/3044] Add ConfigDict for tolerances, change test tolerances --- pyomo/contrib/appsi/solvers/__init__.py | 2 +- pyomo/contrib/appsi/solvers/maingo.py | 60 ++++++++++++++----- .../solvers/tests/test_persistent_solvers.py | 27 ++++----- 3 files changed, 58 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/__init__.py b/pyomo/contrib/appsi/solvers/__init__.py index c1ebdf28780..352571b98f8 100644 --- a/pyomo/contrib/appsi/solvers/__init__.py +++ b/pyomo/contrib/appsi/solvers/__init__.py @@ -15,4 +15,4 @@ from .cplex import Cplex from .highs import Highs from .wntr import Wntr, WntrResults -from .maingo import MAiNGO, MAiNGOTest +from .maingo import MAiNGO diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index f95a943bed3..944673be53d 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -25,7 +25,12 @@ ) from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.common.collections import ComponentMap -from pyomo.common.config import ConfigValue, NonNegativeInt +from pyomo.common.config import ( + ConfigValue, + ConfigDict, + NonNegativeInt, + NonNegativeFloat, +) from pyomo.common.dependencies import attempt_import from pyomo.common.errors import PyomoException from pyomo.common.log import LogStream @@ -97,7 +102,41 @@ def __init__( implicit_domain=implicit_domain, visibility=visibility, ) + self.tolerances: ConfigDict = self.declare( + 'tolerances', ConfigDict(implicit=True) + ) + + self.tolerances.epsilonA: Optional[float] = self.tolerances.declare( + 'epsilonA', + ConfigValue( + domain=NonNegativeFloat, + default=1e-4, + description="Absolute optimality tolerance", + ), + ) + self.tolerances.epsilonR: Optional[float] = self.tolerances.declare( + 'epsilonR', + ConfigValue( + domain=NonNegativeFloat, + default=1e-4, + description="Relative optimality tolerance", + ), + ) + self.tolerances.deltaEq: Optional[float] = self.tolerances.declare( + 'deltaEq', + ConfigValue( + domain=NonNegativeFloat, default=1e-6, description="Equality tolerance" + ), + ) + self.tolerances.deltaIneq: Optional[float] = self.tolerances.declare( + 'deltaIneq', + ConfigValue( + domain=NonNegativeFloat, + default=1e-6, + description="Inequality tolerance", + ), + ) self.declare("logfile", ConfigValue(domain=str, default="")) self.declare("solver_output_logger", ConfigValue(default=logger)) self.declare( @@ -205,9 +244,10 @@ def _solve(self, timer: HierarchicalTimer): self._mymaingo.set_option("loggingDestination", 2) self._mymaingo.set_log_file_name(config.logfile) - self._mymaingo.set_option("epsilonA", 1e-4) - self._mymaingo.set_option("epsilonR", 1e-4) - self._set_maingo_options() + self._mymaingo.set_option("epsilonA", config.tolerances.epsilonA) + self._mymaingo.set_option("epsilonR", config.tolerances.epsilonR) + self._mymaingo.set_option("deltaEq", config.tolerances.deltaEq) + self._mymaingo.set_option("deltaIneq", config.tolerances.deltaIneq) if config.time_limit is not None: self._mymaingo.set_option("maxTime", config.time_limit) @@ -526,15 +566,3 @@ def update(self, timer: HierarchicalTimer = None): idmap=self._pyomo_var_to_solver_var_id_map, logger=logger, ) - - def _set_maingo_options(self): - pass - - -# Solver class with tighter tolerances for testing -class MAiNGOTest(MAiNGO): - def _set_maingo_options(self): - self._mymaingo.set_option("epsilonA", 1e-8) - self._mymaingo.set_option("epsilonR", 1e-8) - self._mymaingo.set_option("deltaIneq", 1e-9) - self._mymaingo.set_option("deltaEq", 1e-9) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 2207ba70f4e..d6df1710a03 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -17,8 +17,7 @@ parameterized = parameterized.parameterized from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available -from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs -from pyomo.contrib.appsi.solvers import MAiNGOTest as MAiNGO +from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs, MAiNGO from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression import os @@ -866,8 +865,8 @@ def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.y >= pe.exp(m.x)) res = opt.solve(m) - self.assertAlmostEqual(m.x.value, -0.42630274815985264) - self.assertAlmostEqual(m.y.value, 0.6529186341994245) + self.assertAlmostEqual(m.x.value, -0.42630274815985264, 6) + self.assertAlmostEqual(m.y.value, 0.6529186341994245, 6) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): @@ -1212,19 +1211,19 @@ def test_fixed_binaries( m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0, 6) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0, 6) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( @@ -1248,16 +1247,16 @@ def test_with_gdp( pe.TransformationFactory("gdp.bigm").apply_to(m) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) - self.assertAlmostEqual(m.x.value, 0) - self.assertAlmostEqual(m.y.value, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(m.x.value, 0, 6) + self.assertAlmostEqual(m.y.value, 1, 6) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.use_extensions = True res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) - self.assertAlmostEqual(m.x.value, 0) - self.assertAlmostEqual(m.y.value, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(m.x.value, 0, 6) + self.assertAlmostEqual(m.y.value, 1, 6) @parameterized.expand(input=all_solvers) def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]): From e0b6277f72fb00347d8ed482a534e5471ffd9688 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Apr 2024 07:20:31 -0600 Subject: [PATCH 1211/3044] Attempt installing mpich first --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index a15240194f2..ce7d03f6898 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -92,7 +92,7 @@ jobs: skip_doctest: 1 TARGET: linux PYENV: conda - PACKAGES: mpi4py==3.1.5 + PACKAGES: mpich mpi4py - os: ubuntu-latest python: '3.10' From d4aced699a5ac0466ebcb375a05249bfabf25901 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Apr 2024 07:51:59 -0600 Subject: [PATCH 1212/3044] Switch to openmpi --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index ce7d03f6898..1885f6a00e2 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -92,7 +92,7 @@ jobs: skip_doctest: 1 TARGET: linux PYENV: conda - PACKAGES: mpich mpi4py + PACKAGES: openmpi mpi4py - os: ubuntu-latest python: '3.10' From 13830955da64e801bd61a583582783e48243c742 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 08:47:04 -0600 Subject: [PATCH 1213/3044] Remove unused import --- pyomo/opt/results/problem.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/opt/results/problem.py b/pyomo/opt/results/problem.py index 34da8f91918..a8eca1e3b41 100644 --- a/pyomo/opt/results/problem.py +++ b/pyomo/opt/results/problem.py @@ -12,7 +12,6 @@ import enum from pyomo.opt.results.container import MapContainer -from pyomo.common.deprecation import deprecated, deprecation_warning from pyomo.common.enums import ExtendedEnumType, ObjectiveSense From 0e68f4bab1adc62e29652f2005d787fb31fc1ec7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 08:58:17 -0600 Subject: [PATCH 1214/3044] nlv2: simplify / improve performance of binary domain check --- pyomo/repn/plugins/nl_writer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index ab74f0ab44d..207846787fd 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -113,7 +113,7 @@ TOL = 1e-8 inf = float('inf') minus_inf = -inf -zero_one = {0, 1} +allowable_binary_var_bounds = {(0,0), (0,1), (1,1)} _CONSTANT = ExprType.CONSTANT _MONOMIAL = ExprType.MONOMIAL @@ -883,10 +883,9 @@ def write(self, model): elif v.is_binary(): binary_vars.add(_id) elif v.is_integer(): - bnd = var_bounds[_id] # Note: integer variables whose bounds are in {0, 1} # should be classified as binary - if bnd[1] in zero_one and bnd[0] in zero_one: + if var_bounds[_id] in allowable_binary_var_bounds: binary_vars.add(_id) else: integer_vars.add(_id) From ee05e18625ba7193e6cf739bccd66cb773fc7d2d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 09:05:46 -0600 Subject: [PATCH 1215/3044] NFC: apply black --- pyomo/repn/plugins/nl_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 207846787fd..86da2a3622b 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -113,7 +113,7 @@ TOL = 1e-8 inf = float('inf') minus_inf = -inf -allowable_binary_var_bounds = {(0,0), (0,1), (1,1)} +allowable_binary_var_bounds = {(0, 0), (0, 1), (1, 1)} _CONSTANT = ExprType.CONSTANT _MONOMIAL = ExprType.MONOMIAL From 232be203e8669d174b6aaa97ecfa0ddd1d639eeb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 09:52:27 -0600 Subject: [PATCH 1216/3044] Remove duplicate imports --- pyomo/core/base/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 3d1347659db..341af677b0e 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -84,7 +84,6 @@ from pyomo.core.base.boolean_var import ( BooleanVar, BooleanVarData, - BooleanVarData, BooleanVarList, ScalarBooleanVar, ) @@ -145,7 +144,7 @@ active_import_suffix_generator, Suffix, ) -from pyomo.core.base.var import Var, VarData, VarData, ScalarVar, VarList +from pyomo.core.base.var import Var, VarData, ScalarVar, VarList from pyomo.core.base.instance2dat import instance2dat From 354cadf178d6210ebe386687ff1af15d8481c253 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 09:53:00 -0600 Subject: [PATCH 1217/3044] Revert accidental test name change --- pyomo/core/tests/unit/test_suffix.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/core/tests/unit/test_suffix.py b/pyomo/core/tests/unit/test_suffix.py index 70f028a3eff..d2e861cceb5 100644 --- a/pyomo/core/tests/unit/test_suffix.py +++ b/pyomo/core/tests/unit/test_suffix.py @@ -1567,7 +1567,7 @@ def test_clone_ObjectiveArray(self): self.assertEqual(inst.junk.get(model.obj[1]), None) self.assertEqual(inst.junk.get(inst.obj[1]), 1.0) - def test_cloneObjectiveData(self): + def test_clone_ObjectiveData(self): model = ConcreteModel() model.x = Var([1, 2, 3], dense=True) model.obj = Objective([1, 2, 3], rule=lambda model, i: model.x[i]) @@ -1603,7 +1603,7 @@ def test_clone_IndexedBlock(self): self.assertEqual(inst.junk.get(model.b[1]), None) self.assertEqual(inst.junk.get(inst.b[1]), 1.0) - def test_cloneBlockData(self): + def test_clone_BlockData(self): model = ConcreteModel() model.b = Block([1, 2, 3]) model.junk = Suffix() @@ -1725,7 +1725,7 @@ def test_pickle_ObjectiveArray(self): self.assertEqual(inst.junk.get(model.obj[1]), None) self.assertEqual(inst.junk.get(inst.obj[1]), 1.0) - def test_pickleObjectiveData(self): + def test_pickle_ObjectiveData(self): model = ConcreteModel() model.x = Var([1, 2, 3], dense=True) model.obj = Objective([1, 2, 3], rule=simple_obj_rule) @@ -1761,7 +1761,7 @@ def test_pickle_IndexedBlock(self): self.assertEqual(inst.junk.get(model.b[1]), None) self.assertEqual(inst.junk.get(inst.b[1]), 1.0) - def test_pickleBlockData(self): + def test_pickle_BlockData(self): model = ConcreteModel() model.b = Block([1, 2, 3]) model.junk = Suffix() From 37915b4b409c3e468d70e0ed64faaee0b7c83ef5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 09:53:59 -0600 Subject: [PATCH 1218/3044] Fix docstring, add Objectives as known type to FBBT --- pyomo/contrib/fbbt/fbbt.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index eb7155313c4..1507c4a3cc5 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -24,9 +24,10 @@ import math from pyomo.core.base.block import Block from pyomo.core.base.constraint import Constraint +from pyomo.core.base.expression import ExpressionData, ScalarExpression +from pyomo.core.base.objective import ObjectiveData, ScalarObjective from pyomo.core.base.var import Var from pyomo.gdp import Disjunct -from pyomo.core.base.expression import ExpressionData, ScalarExpression import logging from pyomo.common.errors import InfeasibleConstraintException, PyomoException from pyomo.common.config import ( @@ -340,7 +341,7 @@ def _prop_bnds_leaf_to_root_NamedExpression(visitor, node, expr): Parameters ---------- visitor: _FBBTVisitorLeafToRoot - node: pyomo.core.base.expression.ExpressionData + node: pyomo.core.base.expression.NamedExpressionData expr: NamedExpressionData arg """ bnds_dict = visitor.bnds_dict @@ -368,6 +369,8 @@ def _prop_bnds_leaf_to_root_NamedExpression(visitor, node, expr): numeric_expr.AbsExpression: _prop_bnds_leaf_to_root_abs, ExpressionData: _prop_bnds_leaf_to_root_NamedExpression, ScalarExpression: _prop_bnds_leaf_to_root_NamedExpression, + ObjectiveData: _prop_bnds_leaf_to_root_NamedExpression, + ScalarObjective: _prop_bnds_leaf_to_root_NamedExpression, }, ) @@ -904,7 +907,7 @@ def _prop_bnds_root_to_leaf_NamedExpression(node, bnds_dict, feasibility_tol): Parameters ---------- - node: pyomo.core.base.expression.ExpressionData + node: pyomo.core.base.expression.NamedExpressionData bnds_dict: ComponentMap feasibility_tol: float If the bounds computed on the body of a constraint violate the bounds of the constraint by more than @@ -947,6 +950,8 @@ def _prop_bnds_root_to_leaf_NamedExpression(node, bnds_dict, feasibility_tol): _prop_bnds_root_to_leaf_map[ExpressionData] = _prop_bnds_root_to_leaf_NamedExpression _prop_bnds_root_to_leaf_map[ScalarExpression] = _prop_bnds_root_to_leaf_NamedExpression +_prop_bnds_root_to_leaf_map[ObjectiveData] = _prop_bnds_root_to_leaf_NamedExpression +_prop_bnds_root_to_leaf_map[ScalarObjective] = _prop_bnds_root_to_leaf_NamedExpression def _check_and_reset_bounds(var, lb, ub): From e35d537d30e6e28d2c54737408adcf24d7ae361d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 09:54:18 -0600 Subject: [PATCH 1219/3044] Fix docstring --- pyomo/core/base/constraint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 3a71758d55d..eb4af76fdc1 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -127,7 +127,7 @@ def C_rule(model, i, j): class ConstraintData(ActiveComponentData): """ - This class defines the data for a single general constraint. + This class defines the data for a single algebraic constraint. Constructor arguments: component The Constraint object that owns this data. From 256d5bb8db73c11fb75679ed8b5770f99b6820d3 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 18 Apr 2024 11:30:24 -0600 Subject: [PATCH 1220/3044] Update PR test --- .github/workflows/test_pr_and_main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 2615f6b838e..619a5e695e2 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -93,7 +93,7 @@ jobs: skip_doctest: 1 TARGET: linux PYENV: conda - PACKAGES: mpi4py==3.1.5 + PACKAGES: openmpi mpi4py - os: ubuntu-latest python: '3.11' From 31474401ef9034b90946ca4a094f45b89b3f912a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20J=C3=BAnior?= <16216517+henriquejsfj@users.noreply.github.com> Date: Sun, 14 Apr 2024 13:19:02 -0300 Subject: [PATCH 1221/3044] Fix: Get SCIP solving time considering float number with some text This fix does not change previous behavior and handles the case of a string with a float number plus some text. --- pyomo/solvers/plugins/solvers/SCIPAMPL.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solvers/plugins/solvers/SCIPAMPL.py b/pyomo/solvers/plugins/solvers/SCIPAMPL.py index fd69954b428..966fb1e1a1d 100644 --- a/pyomo/solvers/plugins/solvers/SCIPAMPL.py +++ b/pyomo/solvers/plugins/solvers/SCIPAMPL.py @@ -455,7 +455,7 @@ def read_scip_log(filename: str): solver_status = scip_lines[0][colon_position + 2 : scip_lines[0].index('\n')] solving_time = float( - scip_lines[1][colon_position + 2 : scip_lines[1].index('\n')] + scip_lines[1][colon_position + 2 : scip_lines[1].index('\n')].split(' ')[0] ) try: From 3ce74167beac24f4ffd963fef8d0f57a0979ea69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20J=C3=BAnior?= <16216517+henriquejsfj@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:05:40 -0300 Subject: [PATCH 1222/3044] Test the Scip reoptimization option --- pyomo/solvers/tests/mip/test_scip.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/solvers/tests/mip/test_scip.py b/pyomo/solvers/tests/mip/test_scip.py index 01de0d16826..ad54daeddc0 100644 --- a/pyomo/solvers/tests/mip/test_scip.py +++ b/pyomo/solvers/tests/mip/test_scip.py @@ -106,6 +106,12 @@ def test_scip_solve_from_instance_options(self): results.write(filename=_out, times=False, format='json') self.compare_json(_out, join(currdir, "test_scip_solve_from_instance.baseline")) + def test_scip_solve_from_instance_with_reoptimization(self): + # Test scip with re-optimization option enabled + # This case changes the Scip output results which may break the results parser + self.scip.options['reoptimization/enable'] = True + self.test_scip_solve_from_instance() + if __name__ == "__main__": deleteFiles = False From 7537bb517b2bc9b841ad368a433b63f0700e9d86 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Apr 2024 13:54:42 -0600 Subject: [PATCH 1223/3044] Propogating domain to substitution var in the var aggregator --- pyomo/contrib/preprocessing/plugins/var_aggregator.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/preprocessing/plugins/var_aggregator.py b/pyomo/contrib/preprocessing/plugins/var_aggregator.py index d862f167fd7..5b714cf439d 100644 --- a/pyomo/contrib/preprocessing/plugins/var_aggregator.py +++ b/pyomo/contrib/preprocessing/plugins/var_aggregator.py @@ -13,7 +13,8 @@ from pyomo.common.collections import ComponentMap, ComponentSet -from pyomo.core.base import Block, Constraint, VarList, Objective, TransformationFactory +from pyomo.core.base import (Block, Constraint, VarList, Objective, Reals, + TransformationFactory) from pyomo.core.expr import ExpressionReplacementVisitor from pyomo.core.expr.numvalue import value from pyomo.core.plugins.transform.hierarchy import IsomorphicTransformation @@ -248,6 +249,12 @@ def _apply_to(self, model, detect_fixed_vars=True): # the variables in its equality set. z_agg.setlb(max_if_not_None(v.lb for v in eq_set if v.has_lb())) z_agg.setub(min_if_not_None(v.ub for v in eq_set if v.has_ub())) + # Set the domain of the aggregate variable to the intersection of + # the domains of the variables in its equality set + domain = Reals + for v in eq_set: + domain = domain & v.domain + z_agg.domain = domain # Set the fixed status of the aggregate var fixed_vars = [v for v in eq_set if v.fixed] From fe6ab256e9cc53c190161dea836b8b998716bb66 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Apr 2024 13:54:59 -0600 Subject: [PATCH 1224/3044] Testing var aggregator with vars with different domains --- .../tests/test_var_aggregator.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/pyomo/contrib/preprocessing/tests/test_var_aggregator.py b/pyomo/contrib/preprocessing/tests/test_var_aggregator.py index 6f6d02f2180..ff1ea843d23 100644 --- a/pyomo/contrib/preprocessing/tests/test_var_aggregator.py +++ b/pyomo/contrib/preprocessing/tests/test_var_aggregator.py @@ -19,12 +19,16 @@ max_if_not_None, min_if_not_None, ) +from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.environ import ( + Binary, ConcreteModel, Constraint, ConstraintList, + maximize, Objective, RangeSet, + Reals, SolverFactory, TransformationFactory, Var, @@ -210,6 +214,43 @@ def test_var_update(self): self.assertEqual(m.x.value, 0) self.assertEqual(m.y.value, 0) + def test_binary_inequality(self): + m = ConcreteModel() + m.x = Var(domain=Binary) + m.y = Var(domain=Binary) + m.c = Constraint(expr=m.x == m.y) + m.o = Objective(expr=0.5*m.x + m.y, sense=maximize) + TransformationFactory('contrib.aggregate_vars').apply_to(m) + var_to_z = m._var_aggregator_info.var_to_z + z = var_to_z[m.x] + self.assertIs(var_to_z[m.y], z) + self.assertEqual(z.domain, Binary) + self.assertEqual(z.lb, 0) + self.assertEqual(z.ub, 1) + assertExpressionsEqual( + self, + m.o.expr, + 0.5 * z + z + ) + + def test_equality_different_domains(self): + m = ConcreteModel() + m.x = Var(domain=Reals, bounds=(1, 2)) + m.y = Var(domain=Binary) + m.c = Constraint(expr=m.x == m.y) + m.o = Objective(expr=0.5*m.x + m.y, sense=maximize) + TransformationFactory('contrib.aggregate_vars').apply_to(m) + var_to_z = m._var_aggregator_info.var_to_z + z = var_to_z[m.x] + self.assertIs(var_to_z[m.y], z) + self.assertEqual(z.lb, 1) + self.assertEqual(z.ub, 1) + self.assertEqual(z.domain, Binary) + assertExpressionsEqual( + self, + m.o.expr, + 0.5 * z + z + ) if __name__ == '__main__': unittest.main() From 127f8c68f9a97da4a34a5a52f61d0d08c6acf0bb Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Apr 2024 13:56:16 -0600 Subject: [PATCH 1225/3044] black --- .../preprocessing/plugins/var_aggregator.py | 10 ++++++++-- .../preprocessing/tests/test_var_aggregator.py | 17 +++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/preprocessing/plugins/var_aggregator.py b/pyomo/contrib/preprocessing/plugins/var_aggregator.py index 5b714cf439d..3430d29de3a 100644 --- a/pyomo/contrib/preprocessing/plugins/var_aggregator.py +++ b/pyomo/contrib/preprocessing/plugins/var_aggregator.py @@ -13,8 +13,14 @@ from pyomo.common.collections import ComponentMap, ComponentSet -from pyomo.core.base import (Block, Constraint, VarList, Objective, Reals, - TransformationFactory) +from pyomo.core.base import ( + Block, + Constraint, + VarList, + Objective, + Reals, + TransformationFactory, +) from pyomo.core.expr import ExpressionReplacementVisitor from pyomo.core.expr.numvalue import value from pyomo.core.plugins.transform.hierarchy import IsomorphicTransformation diff --git a/pyomo/contrib/preprocessing/tests/test_var_aggregator.py b/pyomo/contrib/preprocessing/tests/test_var_aggregator.py index ff1ea843d23..b0b672b76b0 100644 --- a/pyomo/contrib/preprocessing/tests/test_var_aggregator.py +++ b/pyomo/contrib/preprocessing/tests/test_var_aggregator.py @@ -219,7 +219,7 @@ def test_binary_inequality(self): m.x = Var(domain=Binary) m.y = Var(domain=Binary) m.c = Constraint(expr=m.x == m.y) - m.o = Objective(expr=0.5*m.x + m.y, sense=maximize) + m.o = Objective(expr=0.5 * m.x + m.y, sense=maximize) TransformationFactory('contrib.aggregate_vars').apply_to(m) var_to_z = m._var_aggregator_info.var_to_z z = var_to_z[m.x] @@ -227,18 +227,14 @@ def test_binary_inequality(self): self.assertEqual(z.domain, Binary) self.assertEqual(z.lb, 0) self.assertEqual(z.ub, 1) - assertExpressionsEqual( - self, - m.o.expr, - 0.5 * z + z - ) + assertExpressionsEqual(self, m.o.expr, 0.5 * z + z) def test_equality_different_domains(self): m = ConcreteModel() m.x = Var(domain=Reals, bounds=(1, 2)) m.y = Var(domain=Binary) m.c = Constraint(expr=m.x == m.y) - m.o = Objective(expr=0.5*m.x + m.y, sense=maximize) + m.o = Objective(expr=0.5 * m.x + m.y, sense=maximize) TransformationFactory('contrib.aggregate_vars').apply_to(m) var_to_z = m._var_aggregator_info.var_to_z z = var_to_z[m.x] @@ -246,11 +242,8 @@ def test_equality_different_domains(self): self.assertEqual(z.lb, 1) self.assertEqual(z.ub, 1) self.assertEqual(z.domain, Binary) - assertExpressionsEqual( - self, - m.o.expr, - 0.5 * z + z - ) + assertExpressionsEqual(self, m.o.expr, 0.5 * z + z) + if __name__ == '__main__': unittest.main() From 4f0c8d84ccb1f6cabbd90a0b0790db2e9d2311f5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 18 Apr 2024 14:07:07 -0600 Subject: [PATCH 1226/3044] Simplify constant NL expressions in defined vars due to the presolver --- pyomo/repn/plugins/nl_writer.py | 125 +++++++++++++++++++-- pyomo/repn/tests/ampl/test_nlv2.py | 167 +++++++++++++++++++++++++++++ 2 files changed, 282 insertions(+), 10 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index ee5b65149ae..2de44b4f68b 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -11,6 +11,8 @@ import ctypes import logging +import math +import operator import os from collections import deque, defaultdict, namedtuple from contextlib import nullcontext @@ -1835,7 +1837,7 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): fixed_vars.append(x) eliminated_cons.add(con_id) else: - return eliminated_cons, eliminated_vars + break for con_id, expr_info in comp_by_linear_var[_id]: # Note that if we were aggregating (i.e., _id was # from two_var), then one of these info's will be @@ -1888,6 +1890,32 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): elif a: expr_info.linear[x] = c * a + # Note: the ASL will (silently) produce incorrect answers if the + # nonlinear portion of a defined variable is a constant + # expression. This may now be the case if all the variables in + # the original nonlinear expression have been fixed. + for expr, info, _ in self.subexpression_cache.values(): + if not info.nonlinear: + continue + print(info.nonlinear) + nl, args = info.nonlinear + if not args or any(vid not in eliminated_vars for vid in args): + continue + # Ideally, we would just evaluate the named expression. + # However, there might be a linear portion of the named + # expression that still has free variables, and there is no + # guarantee that the user actually initialized the + # variables. So, we will fall back on parsing the (now + # constant) nonlinear fragment and evaluating it. + if info.linear is None: + info.linear = {} + info.nonlinear = None + info.const += _evaluate_constant_nl( + nl % tuple(template.const % eliminated_vars[i].const for i in args) + ) + + return eliminated_cons, eliminated_vars + def _record_named_expression_usage(self, named_exprs, src, comp_type): self.used_named_expressions.update(named_exprs) src = id(src) @@ -2263,6 +2291,40 @@ class text_nl_debug_template(object): _create_strict_inequality_map(vars()) +nl_operators = { + 0: (2, operator.add), + 2: (2, operator.mul), + 3: (2, operator.truediv), + 5: (2, operator.pow), + 15: (1, operator.abs), + 16: (1, operator.neg), + 54: (None, lambda *x: sum(x)), + 35: (3, lambda a, b, c: b if a else c), + 21: (2, operator.and_), + 22: (2, operator.lt), + 23: (2, operator.le), + 24: (2, operator.eq), + 43: (2, math.log), + 42: (2, math.log10), + 41: (2, math.sin), + 46: (2, math.cos), + 38: (2, math.tan), + 40: (2, math.sinh), + 45: (2, math.cosh), + 37: (2, math.tanh), + 51: (2, math.asin), + 53: (2, math.acos), + 49: (2, math.atan), + 44: (2, math.exp), + 39: (2, math.sqrt), + 50: (2, math.asinh), + 52: (2, math.acosh), + 47: (2, math.atanh), + 14: (2, math.ceil), + 13: (2, math.floor), +} + + def _strip_template_comments(vars_, base_): vars_['unary'] = {k: v[: v.find('\t#')] + '\n' for k, v in base_.unary.items()} for k, v in base_.__dict__.items(): @@ -2515,6 +2577,15 @@ def handle_named_expression_node(visitor, node, arg1): expression_source, ) + # As we will eventually need the compiled form of any nonlinear + # expression, we will go ahead and compile it here. We do not + # do the same for the linear component as we will only need the + # linear component compiled to a dict if we are emitting the + # original (linear + nonlinear) V line (which will not happen if + # the V line is part of a larger linear operator). + if repn.nonlinear.__class__ is list: + repn.compile_nonlinear_fragment(visitor) + if not visitor.use_named_exprs: return _GENERAL, repn.duplicate() @@ -2527,15 +2598,6 @@ def handle_named_expression_node(visitor, node, arg1): repn.nl = (visitor.template.var, (_id,)) if repn.nonlinear: - # As we will eventually need the compiled form of any nonlinear - # expression, we will go ahead and compile it here. We do not - # do the same for the linear component as we will only need the - # linear component compiled to a dict if we are emitting the - # original (linear + nonlinear) V line (which will not happen if - # the V line is part of a larger linear operator). - if repn.nonlinear.__class__ is list: - repn.compile_nonlinear_fragment(visitor) - if repn.linear: # If this expression has both linear and nonlinear # components, we will follow the ASL convention and break @@ -3016,3 +3078,46 @@ def finalizeResult(self, result): # self.active_expression_source = None return ans + + +def _evaluate_constant_nl(nl): + expr = nl.splitlines() + stack = [] + while expr: + line = expr.pop() + tokens = line.split() + # remove tokens after the first comment + for i, t in enumerate(tokens): + if t.startswith('#'): + tokens = tokens[:i] + break + if len(tokens) != 1: + # skip blank lines + if not tokens: + continue + raise DeveloperError( + f"Unsupported line format _evaluate_nl() (we expect each line " + f"to contain a single token): '{line}'" + ) + term = tokens[0] + # the "command" can be determined by the first character on the line + cmd = term[0] + # Note that we will unpack the line into the expected number of + # explicit arguments as a form of error checking + if cmd == 'n': + stack.append(float(term[1:])) + elif cmd == 'o': + # operator + nargs, fcn = nl_operators[int(term[1:])] + if nargs is None: + nargs = int(stack.pop()) + stack.append(fcn(*(stack.pop() for i in range(nargs)))) + elif cmd in '1234567890': + # this is either a single int (e.g., the nargs in a nary + # sum) or a string argument. Preserve it as-is until later + # when we know which we are expecting. + stack.append(term) + else: + raise DeveloperError(f"Unsupported NL operator in _evaluate_nl(): '{line}'") + assert len(stack) == 1 + return stack[0] diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index be72025edcd..54b0d93b52b 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2165,6 +2165,173 @@ def test_named_expressions(self): 0 0 1 0 2 0 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_fixes_nl_defined_variables(self): + # This tests a workaround for a bug in the ASL where defined + # variables with nonstant expressions in the NL portion are not + # evaluated correctly. + m = ConcreteModel() + m.x = Var() + m.y = Var(bounds=(3, None)) + m.z = Var(bounds=(None, 3)) + m.e = Expression(expr=m.x + m.y * m.z + m.y**2 + 3 / m.z) + m.c1 = Constraint(expr=m.y * m.e + m.x >= 0) + m.c2 = Constraint(expr=m.y == m.z) + + OUT = io.StringIO() + nl_writer.NLWriter().write( + m, + OUT, + symbolic_solver_labels=True, + linear_presolve=True, + export_defined_variables=True, + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 1 0 0 0 #vars, constraints, objectives, ranges, eqns + 1 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 1 0 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 1 0 #nonzeros in Jacobian, obj. gradient + 2 1 #max name lengths: constraints, variables + 0 0 0 2 0 #common exprs: b,c,o,c1,o1 +V1 0 1 #nl(e) +n19 +V2 1 1 #e +0 1 +v1 #nl(e) +C0 #c1 +o2 #* +n3 +v2 #e +x0 #initial guess +r #1 ranges (rhs's) +2 0 #c1 +b #1 bounds (on variables) +3 #x +k0 #intermediate Jacobian column lengths +J0 1 #c1 +0 1 +""", + OUT.getvalue(), + ) + ) + + OUT = io.StringIO() + nl_writer.NLWriter().write( + m, + OUT, + symbolic_solver_labels=True, + linear_presolve=True, + export_defined_variables=False, + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 1 0 0 0 #vars, constraints, objectives, ranges, eqns + 1 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 1 0 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 1 0 #nonzeros in Jacobian, obj. gradient + 2 1 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +C0 #c1 +o2 #* +n3 +o0 #+ +v0 #x +o54 #sumlist +3 #(n) +o2 #* +n3 +n3 +o5 #^ +n3 +n2 +o3 #/ +n3 +n3 +x0 #initial guess +r #1 ranges (rhs's) +2 0 #c1 +b #1 bounds (on variables) +3 #x +k0 #intermediate Jacobian column lengths +J0 1 #c1 +0 1 +""", + OUT.getvalue(), + ) + ) + + OUT = io.StringIO() + nl_writer.NLWriter().write( + m, + OUT, + symbolic_solver_labels=True, + linear_presolve=False, + export_defined_variables=True, + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 3 2 0 0 1 #vars, constraints, objectives, ranges, eqns + 1 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 3 0 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 5 0 #nonzeros in Jacobian, obj. gradient + 2 1 #max name lengths: constraints, variables + 0 0 0 2 0 #common exprs: b,c,o,c1,o1 +V3 0 1 #nl(e) +o54 #sumlist +3 #(n) + o2 #* + v0 #y +v2 #z +o5 #^ +v0 #y +n2 +o3 #/ +n3 +v2 #z +V4 1 1 #e +1 1 +v3 #nl(e) +C0 #c1 +o2 #* +v0 #y +v4 #e +C1 #c2 +n0 +x0 #initial guess +r #2 ranges (rhs's) +2 0 #c1 +4 0 #c2 +b #3 bounds (on variables) +2 3 #y +3 #x +1 3 #z +k2 #intermediate Jacobian column lengths +2 +3 +J0 3 #c1 +0 0 +1 1 +2 0 +J1 2 #c2 +0 1 +2 -1 """, OUT.getvalue(), ) From 0451815e320b77689bb56ab263b4c1c4134174e5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Apr 2024 14:30:14 -0600 Subject: [PATCH 1227/3044] Rewriting append and to_expression in LinearRepn to not assume that constant and multiplier are numeric types --- pyomo/repn/linear.py | 61 ++++++++++++------- pyomo/repn/linear_wrt.py | 4 +- pyomo/repn/tests/test_linear.py | 2 +- pyomo/repn/tests/test_linear_wrt.py | 91 ++++++++++++++++++++++++++++- 4 files changed, 131 insertions(+), 27 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 913b36f6f16..fd5eae13fc2 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -128,9 +128,10 @@ def to_expression(self, visitor): ans += e elif e.nargs() == 1: ans += e.arg(0) - if self.constant: + if self.constant.__class__ not in native_numeric_types or self.constant: ans += self.constant - if self.multiplier != 1: + if (self.multiplier.__class__ not in native_numeric_types or + self.multiplier != 1): ans *= self.multiplier return ans @@ -147,33 +148,49 @@ def append(self, other): callback). """ - # Note that self.multiplier will always be 1 (we only call append() - # within a sum, so there is no opportunity for self.multiplier to - # change). Omitting the assertion for efficiency. - # assert self.multiplier == 1 _type, other = other if _type is _CONSTANT: self.constant += other return mult = other.multiplier - if not mult: - # 0 * other, so there is nothing to add/change about - # self. We can just exit now. - return - if other.constant: - self.constant += mult * other.constant - if other.linear: - _merge_dict(self.linear, mult, other.linear) - if other.nonlinear is not None: - if mult != 1: + try: + _mult = bool(mult) + if not _mult: + return + if mult == 1: + _mult = False + except: + _mult = True + + const = other.constant + try: + _const = bool(const) + except: + _const = True + + if _mult: + if _const: + self.constant += mult * const + if other.linear: + _merge_dict(self.linear, mult, other.linear) + if other.nonlinear is not None: nl = mult * other.nonlinear - else: + if self.nonlinear is None: + self.nonlinear = nl + else: + self.nonlinear += nl + else: + if _const: + self.constant += const + if other.linear: + _merge_dict(self.linear, 1, other.linear) + if other.nonlinear is not None: nl = other.nonlinear - if self.nonlinear is None: - self.nonlinear = nl - else: - self.nonlinear += nl + if self.nonlinear is None: + self.nonlinear = nl + else: + self.nonlinear += nl def to_expression(visitor, arg): @@ -847,7 +864,7 @@ def finalizeResult(self, result): else: # mult not in {0, 1}: factor it into the constant, # linear coefficients, and nonlinear term - self._factor_mult_into_linear_terms(ans, mult) + self._factor_multiplier_into_linear_terms(ans, mult) return ans ans = self.Result() assert result[0] is _CONSTANT diff --git a/pyomo/repn/linear_wrt.py b/pyomo/repn/linear_wrt.py index 46451d3d64c..b58ff32ba71 100644 --- a/pyomo/repn/linear_wrt.py +++ b/pyomo/repn/linear_wrt.py @@ -74,15 +74,13 @@ def __init__(self, subexpression_cache, var_map, var_order, sorter, wrt): self.wrt = ComponentSet(_flattened(wrt)) def beforeChild(self, node, child, child_idx): - print("before child %s" % child) - print(child.__class__) return _before_child_dispatcher[child.__class__](self, child) def finalizeResult(self, result): ans = result[1] if ans.__class__ is self.Result: mult = ans.multiplier - if not mult.__class__ in native_numeric_types: + if mult.__class__ not in native_numeric_types: # mult is an expression--we should push it back into the other terms self._factor_multiplier_into_linear_terms(ans, mult) return ans diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index 0fd428fd8ee..badb7f407f5 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -1643,7 +1643,7 @@ def test_zero_elimination(self): self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, None) + self.assertIsNone(repn.nonlinear) m.p = Param(mutable=True, within=Any, initialize=None) e = m.p * m.x[0] + m.p * m.x[1] * m.x[2] + m.p * log(m.x[3]) diff --git a/pyomo/repn/tests/test_linear_wrt.py b/pyomo/repn/tests/test_linear_wrt.py index 384412b25ce..6bfbd51c01e 100644 --- a/pyomo/repn/tests/test_linear_wrt.py +++ b/pyomo/repn/tests/test_linear_wrt.py @@ -10,7 +10,8 @@ # ___________________________________________________________________________ import pyomo.common.unittest as unittest -from pyomo.environ import Binary, ConcreteModel, Var +from pyomo.core.expr.compare import assertExpressionsEqual +from pyomo.environ import Binary, ConcreteModel, Var, log from pyomo.repn.linear_wrt import MultilevelLinearRepnVisitor from pyomo.repn.tests.test_linear import VisitorConfig @@ -52,3 +53,91 @@ def test_bilinear_term(self): self.assertIs(repn.linear[id(m.x)], m.y) self.assertEqual(repn.constant, 0) self.assertEqual(repn.multiplier, 1) + + def test_distributed_bilinear_term(self): + m = self.make_model() + e = m.y * (m.x + 7) + cfg = VisitorConfig() + visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + + repn = visitor.walk_expression(e) + + self.assertIsNone(repn.nonlinear) + self.assertEqual(len(repn.linear), 1) + self.assertIn(id(m.x), repn.linear) + self.assertIs(repn.linear[id(m.x)], m.y) + assertExpressionsEqual( + self, + repn.constant, + m.y * 7 + ) + self.assertEqual(repn.multiplier, 1) + + def test_monomial(self): + m = self.make_model() + e = 45 * m.y + cfg = VisitorConfig() + visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.y]) + + repn = visitor.walk_expression(e) + + self.assertIsNone(repn.nonlinear) + self.assertEqual(len(repn.linear), 1) + self.assertIn(id(m.y), repn.linear) + self.assertEqual(repn.linear[id(m.y)], 45) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.multiplier, 1) + + def test_constant(self): + m = self.make_model() + e = 45 * m.y + cfg = VisitorConfig() + visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + + repn = visitor.walk_expression(e) + + self.assertIsNone(repn.nonlinear) + self.assertEqual(len(repn.linear), 0) + assertExpressionsEqual( + self, + repn.constant, + 45 * m.y + ) + self.assertEqual(repn.multiplier, 1) + + def test_fixed_var(self): + m = self.make_model() + m.x.fix(42) + e = (m.y ** 2) * (m.x + m.x ** 2) + + cfg = VisitorConfig() + visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + + repn = visitor.walk_expression(e) + + self.assertIsNone(repn.nonlinear) + self.assertEqual(len(repn.linear), 0) + assertExpressionsEqual( + self, + repn.constant, + (m.y ** 2) * 1806 + ) + self.assertEqual(repn.multiplier, 1) + + def test_nonlinear(self): + m = self.make_model() + e = (m.y * log(m.x)) * (m.y + 2) / m.x + + cfg = VisitorConfig() + visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + + repn = visitor.walk_expression(e) + + self.assertEqual(len(repn.linear), 0) + self.assertEqual(repn.multiplier, 1) + print(repn.nonlinear) + assertExpressionsEqual( + self, + repn.nonlinear, + log(m.x) * (m.y *(m.y + 2))/m.x + ) From a1636f085c0dec9d8c7d320c1dfe021e25bde9d0 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Apr 2024 14:31:43 -0600 Subject: [PATCH 1228/3044] black --- pyomo/repn/linear.py | 6 ++++-- pyomo/repn/tests/test_linear_wrt.py | 30 +++++++---------------------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index fd5eae13fc2..c3b84940a71 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -130,8 +130,10 @@ def to_expression(self, visitor): ans += e.arg(0) if self.constant.__class__ not in native_numeric_types or self.constant: ans += self.constant - if (self.multiplier.__class__ not in native_numeric_types or - self.multiplier != 1): + if ( + self.multiplier.__class__ not in native_numeric_types + or self.multiplier != 1 + ): ans *= self.multiplier return ans diff --git a/pyomo/repn/tests/test_linear_wrt.py b/pyomo/repn/tests/test_linear_wrt.py index 6bfbd51c01e..fe159874186 100644 --- a/pyomo/repn/tests/test_linear_wrt.py +++ b/pyomo/repn/tests/test_linear_wrt.py @@ -66,11 +66,7 @@ def test_distributed_bilinear_term(self): self.assertEqual(len(repn.linear), 1) self.assertIn(id(m.x), repn.linear) self.assertIs(repn.linear[id(m.x)], m.y) - assertExpressionsEqual( - self, - repn.constant, - m.y * 7 - ) + assertExpressionsEqual(self, repn.constant, m.y * 7) self.assertEqual(repn.multiplier, 1) def test_monomial(self): @@ -95,20 +91,16 @@ def test_constant(self): visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) repn = visitor.walk_expression(e) - + self.assertIsNone(repn.nonlinear) self.assertEqual(len(repn.linear), 0) - assertExpressionsEqual( - self, - repn.constant, - 45 * m.y - ) + assertExpressionsEqual(self, repn.constant, 45 * m.y) self.assertEqual(repn.multiplier, 1) def test_fixed_var(self): m = self.make_model() m.x.fix(42) - e = (m.y ** 2) * (m.x + m.x ** 2) + e = (m.y**2) * (m.x + m.x**2) cfg = VisitorConfig() visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) @@ -117,17 +109,13 @@ def test_fixed_var(self): self.assertIsNone(repn.nonlinear) self.assertEqual(len(repn.linear), 0) - assertExpressionsEqual( - self, - repn.constant, - (m.y ** 2) * 1806 - ) + assertExpressionsEqual(self, repn.constant, (m.y**2) * 1806) self.assertEqual(repn.multiplier, 1) def test_nonlinear(self): m = self.make_model() e = (m.y * log(m.x)) * (m.y + 2) / m.x - + cfg = VisitorConfig() visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) @@ -136,8 +124,4 @@ def test_nonlinear(self): self.assertEqual(len(repn.linear), 0) self.assertEqual(repn.multiplier, 1) print(repn.nonlinear) - assertExpressionsEqual( - self, - repn.nonlinear, - log(m.x) * (m.y *(m.y + 2))/m.x - ) + assertExpressionsEqual(self, repn.nonlinear, log(m.x) * (m.y * (m.y + 2)) / m.x) From 549d8b00d25b5698f57100331a2fa7ab245a6741 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Apr 2024 14:58:03 -0600 Subject: [PATCH 1229/3044] fixing another to_expression spot where we assume the coefficient is numeric --- pyomo/repn/linear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index c3b84940a71..2a578827afd 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -122,7 +122,7 @@ def to_expression(self, visitor): var_map = visitor.var_map with mutable_expression() as e: for vid, coef in self.linear.items(): - if coef: + if coef.__class__ not in native_numeric_types or coef: e += coef * var_map[vid] if e.nargs() > 1: ans += e From 02be1cc6636a08276787a4707f211775783bd100 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Apr 2024 15:24:40 -0600 Subject: [PATCH 1230/3044] Generalizing merge_dict for non-constant multipliers --- pyomo/repn/linear.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 2a578827afd..78534789fbe 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -62,7 +62,12 @@ def _merge_dict(dest_dict, mult, src_dict): - if mult == 1: + try: + _mult = mult != 1 + except: + _mult = True + + if not _mult: for vid, coef in src_dict.items(): if vid in dest_dict: dest_dict[vid] += coef From a91eb4d5a3dee95c11148f4d32a03c9fcc1e2888 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Apr 2024 15:25:06 -0600 Subject: [PATCH 1231/3044] Completely overriding finalizeResult because of non-constant coefficients --- pyomo/repn/linear_wrt.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/linear_wrt.py b/pyomo/repn/linear_wrt.py index b58ff32ba71..42543340f48 100644 --- a/pyomo/repn/linear_wrt.py +++ b/pyomo/repn/linear_wrt.py @@ -84,6 +84,33 @@ def finalizeResult(self, result): # mult is an expression--we should push it back into the other terms self._factor_multiplier_into_linear_terms(ans, mult) return ans + if mult == 1: + for vid, coef in ans.linear.items(): + if coef.__class__ in native_numeric_types and not coef: + del ans.linear[vid] + elif not mult: + # the mulltiplier has cleared out the entire expression. + # Warn if this is suppressing a NaN (unusual, and + # non-standard, but we will wait to remove this behavior + # for the time being) + if ans.constant != ans.constant or any( + c != c for c in ans.linear.values() + ): + deprecation_warning( + f"Encountered {str(mult)}*nan in expression tree. " + "Mapping the NaN result to 0 for compatibility " + "with the lp_v1 writer. In the future, this NaN " + "will be preserved/emitted to comply with IEEE-754.", + version='6.6.0', + ) + return self.Result() + else: + # mult not in {0, 1}: factor it into the constant, + # linear coefficients, and nonlinear term + self._factor_multiplier_into_linear_terms(ans, mult) + return ans - # In all other cases, the base class implementation is correct - return super().finalizeResult(result) + ans = self.Result() + assert result[0] is ExprType.CONSTANT + ans.constant = result[1] + return ans From c9aa8b8565521963e49ee304ab3fbc6637bc774a Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Apr 2024 15:25:28 -0600 Subject: [PATCH 1232/3044] Testing to_expression --- pyomo/repn/tests/test_linear_wrt.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/tests/test_linear_wrt.py b/pyomo/repn/tests/test_linear_wrt.py index fe159874186..89bc9ee6abf 100644 --- a/pyomo/repn/tests/test_linear_wrt.py +++ b/pyomo/repn/tests/test_linear_wrt.py @@ -38,6 +38,27 @@ def test_walk_sum(self): self.assertEqual(repn.linear[id(m.x)], 1) self.assertIs(repn.constant, m.y) self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.to_expression(visitor), m.x + m.y) + + def test_walk_triple_sum(self): + m = self.make_model() + m.z = Var() + e = m.x + m.z*m.y + m.z + + cfg = VisitorConfig() + visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x, m.y]) + + repn = visitor.walk_expression(e) + + self.assertIsNone(repn.nonlinear) + self.assertEqual(len(repn.linear), 2) + self.assertIn(id(m.x), repn.linear) + self.assertIn(id(m.y), repn.linear) + self.assertEqual(repn.linear[id(m.x)], 1) + self.assertIs(repn.linear[id(m.y)], m.z) + self.assertIs(repn.constant, m.z) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.to_expression(visitor), m.x + m.z * m.y + m.z) def test_bilinear_term(self): m = self.make_model() @@ -53,6 +74,7 @@ def test_bilinear_term(self): self.assertIs(repn.linear[id(m.x)], m.y) self.assertEqual(repn.constant, 0) self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.to_expression(visitor), m.y * m.x) def test_distributed_bilinear_term(self): m = self.make_model() @@ -68,6 +90,7 @@ def test_distributed_bilinear_term(self): self.assertIs(repn.linear[id(m.x)], m.y) assertExpressionsEqual(self, repn.constant, m.y * 7) self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.to_expression(visitor), m.y * m.x + m.y * 7) def test_monomial(self): m = self.make_model() @@ -83,6 +106,7 @@ def test_monomial(self): self.assertEqual(repn.linear[id(m.y)], 45) self.assertEqual(repn.constant, 0) self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.to_expression(visitor), 45 * m.y) def test_constant(self): m = self.make_model() @@ -96,6 +120,7 @@ def test_constant(self): self.assertEqual(len(repn.linear), 0) assertExpressionsEqual(self, repn.constant, 45 * m.y) self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.to_expression(visitor), 45 * m.y) def test_fixed_var(self): m = self.make_model() @@ -111,6 +136,7 @@ def test_fixed_var(self): self.assertEqual(len(repn.linear), 0) assertExpressionsEqual(self, repn.constant, (m.y**2) * 1806) self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.to_expression(visitor), (m.y**2) * 1806) def test_nonlinear(self): m = self.make_model() @@ -123,5 +149,6 @@ def test_nonlinear(self): self.assertEqual(len(repn.linear), 0) self.assertEqual(repn.multiplier, 1) - print(repn.nonlinear) assertExpressionsEqual(self, repn.nonlinear, log(m.x) * (m.y * (m.y + 2)) / m.x) + assertExpressionsEqual(self, repn.to_expression(visitor), + log(m.x) * (m.y * (m.y + 2)) / m.x) From 9fb61265d4a9975683c045dec3b5f5713d4f89b2 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Apr 2024 15:26:04 -0600 Subject: [PATCH 1233/3044] Black --- pyomo/repn/tests/test_linear_wrt.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/tests/test_linear_wrt.py b/pyomo/repn/tests/test_linear_wrt.py index 89bc9ee6abf..35e7160a351 100644 --- a/pyomo/repn/tests/test_linear_wrt.py +++ b/pyomo/repn/tests/test_linear_wrt.py @@ -43,7 +43,7 @@ def test_walk_sum(self): def test_walk_triple_sum(self): m = self.make_model() m.z = Var() - e = m.x + m.z*m.y + m.z + e = m.x + m.z * m.y + m.z cfg = VisitorConfig() visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x, m.y]) @@ -150,5 +150,6 @@ def test_nonlinear(self): self.assertEqual(len(repn.linear), 0) self.assertEqual(repn.multiplier, 1) assertExpressionsEqual(self, repn.nonlinear, log(m.x) * (m.y * (m.y + 2)) / m.x) - assertExpressionsEqual(self, repn.to_expression(visitor), - log(m.x) * (m.y * (m.y + 2)) / m.x) + assertExpressionsEqual( + self, repn.to_expression(visitor), log(m.x) * (m.y * (m.y + 2)) / m.x + ) From afa542f8e565b43a05b73e62398d29e7f2753f6b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 19 Apr 2024 09:06:00 -0600 Subject: [PATCH 1234/3044] Improve comments/error messages --- pyomo/repn/plugins/nl_writer.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index f91f88ff5cf..fc8b8b10309 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -3102,8 +3102,8 @@ def _evaluate_constant_nl(nl): if not tokens: continue raise DeveloperError( - f"Unsupported line format _evaluate_nl() (we expect each line " - f"to contain a single token): '{line}'" + f"Unsupported line format _evaluate_constant_nl() " + f"(we expect each line to contain a single token): '{line}'" ) term = tokens[0] # the "command" can be determined by the first character on the line @@ -3111,6 +3111,7 @@ def _evaluate_constant_nl(nl): # Note that we will unpack the line into the expected number of # explicit arguments as a form of error checking if cmd == 'n': + # numeric constant stack.append(float(term[1:])) elif cmd == 'o': # operator @@ -3124,6 +3125,8 @@ def _evaluate_constant_nl(nl): # when we know which we are expecting. stack.append(term) else: - raise DeveloperError(f"Unsupported NL operator in _evaluate_nl(): '{line}'") + raise DeveloperError( + f"Unsupported NL operator in _evaluate_constant_nl(): '{line}'" + ) assert len(stack) == 1 return stack[0] From 1a8ab9621c0252fedca294e3e599f363253ce158 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 19 Apr 2024 09:06:53 -0600 Subject: [PATCH 1235/3044] Fix typo in baseline --- pyomo/repn/tests/ampl/test_nlv2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 64830d36921..f2378e56b4c 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2435,8 +2435,8 @@ def test_presolve_fixes_nl_defined_variables(self): V3 0 1 #nl(e) o54 #sumlist 3 #(n) - o2 #* - v0 #y +o2 #* +v0 #y v2 #z o5 #^ v0 #y From 4ec32ffd5ecb6c683c072124f8a8aaa3d57f3911 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 19 Apr 2024 13:56:32 -0600 Subject: [PATCH 1236/3044] Fixing even more places where we assume constant and coefficients are not expressions --- pyomo/repn/linear.py | 6 +- pyomo/repn/linear_wrt.py | 11 +-- pyomo/repn/tests/test_linear_wrt.py | 111 +++++++++++++++++++++++++++- 3 files changed, 119 insertions(+), 9 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 78534789fbe..079fbb86489 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -831,15 +831,15 @@ def _factor_multiplier_into_linear_terms(self, ans, mult): linear = ans.linear zeros = [] for vid, coef in linear.items(): - if coef: - linear[vid] = coef * mult + if coef.__class__ not in native_numeric_types or coef: + linear[vid] = mult * coef else: zeros.append(vid) for vid in zeros: del linear[vid] if ans.nonlinear is not None: ans.nonlinear *= mult - if ans.constant: + if ans.constant.__class__ not in native_numeric_types or ans.constant: ans.constant *= mult ans.multiplier = 1 diff --git a/pyomo/repn/linear_wrt.py b/pyomo/repn/linear_wrt.py index 42543340f48..8fa1f9b4546 100644 --- a/pyomo/repn/linear_wrt.py +++ b/pyomo/repn/linear_wrt.py @@ -46,7 +46,6 @@ def _before_general_expression(visitor, child): def _before_var(visitor, child): if child in visitor.wrt: # This is a normal situation - print("NORMAL: %s" % child) _id = id(child) if _id not in visitor.var_map: if child.fixed: @@ -59,7 +58,6 @@ def _before_var(visitor, child): ans.linear[_id] = 1 return False, (ExprType.LINEAR, ans) else: - print("DATA: %s" % child) # We aren't treating this Var as a Var for the purposes of this walker return False, (ExprType.CONSTANT, child) @@ -85,14 +83,17 @@ def finalizeResult(self, result): self._factor_multiplier_into_linear_terms(ans, mult) return ans if mult == 1: - for vid, coef in ans.linear.items(): - if coef.__class__ in native_numeric_types and not coef: - del ans.linear[vid] + zeros = [(vid, coef) for vid, coef in ans.linear.items() if + coef.__class__ in native_numeric_types and not coef] + for vid, coef in zeros: + del ans.linear[vid] elif not mult: # the mulltiplier has cleared out the entire expression. # Warn if this is suppressing a NaN (unusual, and # non-standard, but we will wait to remove this behavior # for the time being) + # ESJ TODO: This won't work either actually... + # I'm not sure how to do it. if ans.constant != ans.constant or any( c != c for c in ans.linear.values() ): diff --git a/pyomo/repn/tests/test_linear_wrt.py b/pyomo/repn/tests/test_linear_wrt.py index 35e7160a351..d00668d5c00 100644 --- a/pyomo/repn/tests/test_linear_wrt.py +++ b/pyomo/repn/tests/test_linear_wrt.py @@ -9,9 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from pyomo.common.log import LoggingIntercept import pyomo.common.unittest as unittest from pyomo.core.expr.compare import assertExpressionsEqual -from pyomo.environ import Binary, ConcreteModel, Var, log +from pyomo.environ import Any, Binary, ConcreteModel, log, Param, Var from pyomo.repn.linear_wrt import MultilevelLinearRepnVisitor from pyomo.repn.tests.test_linear import VisitorConfig @@ -153,3 +154,111 @@ def test_nonlinear(self): assertExpressionsEqual( self, repn.to_expression(visitor), log(m.x) * (m.y * (m.y + 2)) / m.x ) + + def test_finalize(self): + m = self.make_model() + m.z = Var() + m.w = Var() + + e = m.x + 2 * m.w**2 * m.y - m.x - m.w * m.z + + cfg = VisitorConfig() + repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x, m.y, m.z]).walk_expression(e) + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y, id(m.z): m.z}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1, id(m.z): 2}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(len(repn.linear), 2) + self.assertIn(id(m.y), repn.linear) + assertExpressionsEqual( + self, + repn.linear[id(m.y)], + 2 * m.w ** 2 + ) + self.assertIn(id(m.z), repn.linear) + assertExpressionsEqual( + self, + repn.linear[id(m.z)], + -m.w + ) + self.assertEqual(repn.nonlinear, None) + + e *= 5 + + cfg = VisitorConfig() + repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x, m.y, m.z]).walk_expression(e) + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y, id(m.z): m.z}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1, id(m.z): 2}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(len(repn.linear), 2) + self.assertIn(id(m.y), repn.linear) + print(repn.linear[id(m.y)]) + assertExpressionsEqual( + self, + repn.linear[id(m.y)], + 5 * (2 * m.w ** 2) + ) + self.assertIn(id(m.z), repn.linear) + assertExpressionsEqual( + self, + repn.linear[id(m.z)], + -5 * m.w + ) + self.assertEqual(repn.nonlinear, None) + + e = 5 * (m.w * m.y + m.z**2 + 3 * m.w * m.y**3) + + cfg = VisitorConfig() + repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x, m.y, m.z]).walk_expression(e) + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.y): m.y, id(m.z): m.z}) + self.assertEqual(cfg.var_order, {id(m.y): 0, id(m.z): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(len(repn.linear), 1) + self.assertIn(id(m.y), repn.linear) + assertExpressionsEqual( + self, + repn.linear[id(m.y)], + 5 * m.w + ) + assertExpressionsEqual(self, repn.nonlinear, (m.z**2 + 3 * m.w * m.y**3) * 5) + + def test_errors_propogate_nan(self): + m = ConcreteModel() + m.p = Param(mutable=True, initialize=0, domain=Any) + m.x = Var() + m.y = Var() + m.z = Var() + m.y.fix(1) + + expr = m.y + m.x + m.z + ((3 * m.z * m.x) / m.p) / m.y + cfg = VisitorConfig() + with LoggingIntercept() as LOG: + repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(expr) + self.assertEqual( + LOG.getvalue(), + "Exception encountered evaluating expression 'div(3, 0)'\n" + "\tmessage: division by zero\n" + "\texpression: 3/p\n", + ) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual( + self, + repn.constant, + 1 + m.z + ) + self.assertEqual(len(repn.linear), 1) + self.assertEqual(str(repn.linear[id(m.x)]), 'InvalidNumber(nan)') + self.assertEqual(repn.nonlinear, None) + + m.y.fix(None) + expr = m.z * log(m.y) + 3 + repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(expr) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.nonlinear, None) From ebe0159071ef2a60a6e71dd3131913907e5aa64b Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 19 Apr 2024 23:15:33 -0600 Subject: [PATCH 1237/3044] start adding an inventory of code to readme --- pyomo/contrib/pynumero/README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pyomo/contrib/pynumero/README.md b/pyomo/contrib/pynumero/README.md index 0d165dbc39c..d4b6344ec76 100644 --- a/pyomo/contrib/pynumero/README.md +++ b/pyomo/contrib/pynumero/README.md @@ -71,3 +71,34 @@ Prerequisites - cmake - a C/C++ compiler - MA57 library or COIN-HSL Full + +Code organization +================= + +PyNumero was initially designed around three core components: linear solver +interfaces, an interface for function and derivative callbacks, and block +vector and matrix classes. Since then, it has incorporated additional +functionality in an ad-hoc manner. The following is a rough overview of +PyNumero, by directory: + +`linalg` +-------- + +Python interfaces to linear solvers. This is core functionality. + +`interfaces` +------------ + +- Classes that define and implement an API for function and derivative callbacks +required by nonlinear optimization solvers, e.g. `nlp.py` and `pyomo_nlp.py` +- Various wrappers around these NLP classes to support "hybrid" implementations, +e.g. `PyomoNLPWithGreyBoxBlocks` +- The `ExternalGreyBoxBlock` Pyomo modeling component and +`ExternalGreyBoxModel` API +- The `ExternalPyomoModel` implementation of `ExternalGreyBoxModel`, which allows +definition of an external grey box via an implicit function +- The `CyIpoptNLP` class, which wraps an object implementing the NLP API in +the interface required by CyIpopt + +`src` +----- From d4dcb0e81bbc20dc6597fc76b1400b81c02cd100 Mon Sep 17 00:00:00 2001 From: robbybp Date: Fri, 19 Apr 2024 23:16:14 -0600 Subject: [PATCH 1238/3044] add a note about backward compatibility to pynumero doc --- .../pynumero/backward_compatibility.rst | 14 ++++++++++++++ .../contributed_packages/pynumero/index.rst | 1 + 2 files changed, 15 insertions(+) create mode 100644 doc/OnlineDocs/contributed_packages/pynumero/backward_compatibility.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/backward_compatibility.rst b/doc/OnlineDocs/contributed_packages/pynumero/backward_compatibility.rst new file mode 100644 index 00000000000..036a00bee62 --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/pynumero/backward_compatibility.rst @@ -0,0 +1,14 @@ +Backward Compatibility +====================== + +While PyNumero is a third-party contribution to Pyomo, we intend to maintain +the stability of its core functionality. The core functionality of PyNumero +consists of: + +1. The ``NLP`` API and ``PyomoNLP`` implementation of this API +2. HSL and MUMPS linear solver interfaces +3. ``BlockVector`` and ``BlockMatrix`` classes +4. CyIpopt and SciPy solver interfaces + +Other parts of PyNumero, such as ``ExternalGreyBoxBlock`` and +``ImplicitFunctionSolver``, are experimental and subject to change without notice. diff --git a/doc/OnlineDocs/contributed_packages/pynumero/index.rst b/doc/OnlineDocs/contributed_packages/pynumero/index.rst index 6ff8b29f812..711bb83eb3b 100644 --- a/doc/OnlineDocs/contributed_packages/pynumero/index.rst +++ b/doc/OnlineDocs/contributed_packages/pynumero/index.rst @@ -13,6 +13,7 @@ PyNumero. For more details, see the API documentation (:ref:`pynumero_api`). installation.rst tutorial.rst api.rst + backward_compatibility.rst Developers From 102223f541f82b38085c15489d2884744dfbb8f8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 22 Apr 2024 15:25:46 -0600 Subject: [PATCH 1239/3044] NFC: clarify NamedExpressionData docstring --- pyomo/core/base/expression.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index 5638e48ea8b..013c388e6e5 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -37,11 +37,14 @@ class NamedExpressionData(numeric_expr.NumericValue): - """ - An object that defines a named expression. + """An object that defines a generic "named expression". + + This is the base class for both :py:class:`ExpressionData` and + :py:class:`ObjectiveData`. Public Class Attributes expr The expression owned by this data. + """ # Note: derived classes are expected to declare the _args_ slot From a5c79317bfa68a91e5081682e36873559acea5ab Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 22 Apr 2024 15:27:21 -0600 Subject: [PATCH 1240/3044] Remove debugging --- pyomo/repn/plugins/nl_writer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index fc8b8b10309..e4a2cea0bad 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1903,7 +1903,6 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): for expr, info, _ in self.subexpression_cache.values(): if not info.nonlinear: continue - print(info.nonlinear) nl, args = info.nonlinear if not args or any(vid not in eliminated_vars for vid in args): continue From 8390ce4321934eae0288dd8fe756fe0cbfaea5e6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 22 Apr 2024 15:30:21 -0600 Subject: [PATCH 1241/3044] Fix the arg count for unary operators --- pyomo/repn/plugins/nl_writer.py | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index e4a2cea0bad..d4e7485cce4 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2309,24 +2309,24 @@ class text_nl_debug_template(object): 22: (2, operator.lt), 23: (2, operator.le), 24: (2, operator.eq), - 43: (2, math.log), - 42: (2, math.log10), - 41: (2, math.sin), - 46: (2, math.cos), - 38: (2, math.tan), - 40: (2, math.sinh), - 45: (2, math.cosh), - 37: (2, math.tanh), - 51: (2, math.asin), - 53: (2, math.acos), - 49: (2, math.atan), - 44: (2, math.exp), - 39: (2, math.sqrt), - 50: (2, math.asinh), - 52: (2, math.acosh), - 47: (2, math.atanh), - 14: (2, math.ceil), - 13: (2, math.floor), + 43: (1, math.log), + 42: (1, math.log10), + 41: (1, math.sin), + 46: (1, math.cos), + 38: (1, math.tan), + 40: (1, math.sinh), + 45: (1, math.cosh), + 37: (1, math.tanh), + 51: (1, math.asin), + 53: (1, math.acos), + 49: (1, math.atan), + 44: (1, math.exp), + 39: (1, math.sqrt), + 50: (1, math.asinh), + 52: (1, math.acosh), + 47: (1, math.atanh), + 14: (1, math.ceil), + 13: (1, math.floor), } From 259ee576ed2673fa30dacab41283e1c152b9ffe6 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 23 Apr 2024 15:09:30 -0600 Subject: [PATCH 1242/3044] Reverting my changes to linear--I'll override stuff that needs to handle expressions so that I don't kill performance --- pyomo/repn/linear.py | 101 ++++++++++++++++--------------------------- 1 file changed, 37 insertions(+), 64 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 079fbb86489..ba08c7ef245 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -62,12 +62,7 @@ def _merge_dict(dest_dict, mult, src_dict): - try: - _mult = mult != 1 - except: - _mult = True - - if not _mult: + if mult == 1: for vid, coef in src_dict.items(): if vid in dest_dict: dest_dict[vid] += coef @@ -127,18 +122,15 @@ def to_expression(self, visitor): var_map = visitor.var_map with mutable_expression() as e: for vid, coef in self.linear.items(): - if coef.__class__ not in native_numeric_types or coef: + if coef: e += coef * var_map[vid] if e.nargs() > 1: ans += e elif e.nargs() == 1: ans += e.arg(0) - if self.constant.__class__ not in native_numeric_types or self.constant: + if self.constant: ans += self.constant - if ( - self.multiplier.__class__ not in native_numeric_types - or self.multiplier != 1 - ): + if self.multiplier != 1: ans *= self.multiplier return ans @@ -155,49 +147,33 @@ def append(self, other): callback). """ + # Note that self.multiplier will always be 1 (we only call append() + # within a sum, so there is no opportunity for self.multiplier to + # change). Omitting the assertion for efficiency. + # assert self.multiplier == 1 _type, other = other if _type is _CONSTANT: self.constant += other return mult = other.multiplier - try: - _mult = bool(mult) - if not _mult: - return - if mult == 1: - _mult = False - except: - _mult = True - - const = other.constant - try: - _const = bool(const) - except: - _const = True - - if _mult: - if _const: - self.constant += mult * const - if other.linear: - _merge_dict(self.linear, mult, other.linear) - if other.nonlinear is not None: + if not mult: + # 0 * other, so there is nothing to add/change about + # self. We can just exit now. + return + if other.constant: + self.constant += mult * other.constant + if other.linear: + _merge_dict(self.linear, mult, other.linear) + if other.nonlinear is not None: + if mult != 1: nl = mult * other.nonlinear - if self.nonlinear is None: - self.nonlinear = nl - else: - self.nonlinear += nl - else: - if _const: - self.constant += const - if other.linear: - _merge_dict(self.linear, 1, other.linear) - if other.nonlinear is not None: + else: nl = other.nonlinear - if self.nonlinear is None: - self.nonlinear = nl - else: - self.nonlinear += nl + if self.nonlinear is None: + self.nonlinear = nl + else: + self.nonlinear += nl def to_expression(visitor, arg): @@ -827,22 +803,6 @@ def exitNode(self, node, data): self, node, *data ) - def _factor_multiplier_into_linear_terms(self, ans, mult): - linear = ans.linear - zeros = [] - for vid, coef in linear.items(): - if coef.__class__ not in native_numeric_types or coef: - linear[vid] = mult * coef - else: - zeros.append(vid) - for vid in zeros: - del linear[vid] - if ans.nonlinear is not None: - ans.nonlinear *= mult - if ans.constant.__class__ not in native_numeric_types or ans.constant: - ans.constant *= mult - ans.multiplier = 1 - def finalizeResult(self, result): ans = result[1] if ans.__class__ is self.Result: @@ -871,7 +831,20 @@ def finalizeResult(self, result): else: # mult not in {0, 1}: factor it into the constant, # linear coefficients, and nonlinear term - self._factor_multiplier_into_linear_terms(ans, mult) + linear = ans.linear + zeros = [] + for vid, coef in linear.items(): + if coef: + linear[vid] = coef * mult + else: + zeros.append(vid) + for vid in zeros: + del linear[vid] + if ans.nonlinear is not None: + ans.nonlinear *= mult + if ans.constant: + ans.constant *= mult + ans.multiplier = 1 return ans ans = self.Result() assert result[0] is _CONSTANT From 7886519da92c7da509aad346304df7daa5f9a2b1 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 23 Apr 2024 15:20:54 -0600 Subject: [PATCH 1243/3044] Adding a new LinearSubsystemRepn class to handle LinearRepns with 'constants' that are actually Pyomo expressions --- pyomo/repn/linear_wrt.py | 156 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 152 insertions(+), 4 deletions(-) diff --git a/pyomo/repn/linear_wrt.py b/pyomo/repn/linear_wrt.py index 8fa1f9b4546..bbd4c6a7d25 100644 --- a/pyomo/repn/linear_wrt.py +++ b/pyomo/repn/linear_wrt.py @@ -9,6 +9,8 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import copy + from pyomo.common.collections import ComponentSet from pyomo.common.numeric_types import native_numeric_types from pyomo.core import Var @@ -16,10 +18,122 @@ from pyomo.core.expr.numeric_expr import ( LinearExpression, MonomialTermExpression, + mutable_expression, + ProductExpression, SumExpression, ) -from pyomo.repn.linear import LinearBeforeChildDispatcher, LinearRepnVisitor +from pyomo.repn.linear import ( + ExitNodeDispatcher, + _initialize_exit_node_dispatcher, + LinearBeforeChildDispatcher, + LinearRepn, + LinearRepnVisitor, +) from pyomo.repn.util import ExprType +from . import linear + +_CONSTANT = ExprType.CONSTANT + + +def _merge_dict(dest_dict, mult, src_dict): + if mult.__class__ not in native_numeric_types or mult != 1: + for vid, coef in src_dict.items(): + if vid in dest_dict: + dest_dict[vid] += mult * coef + else: + dest_dict[vid] = mult * coef + else: + for vid, coef in src_dict.items(): + if vid in dest_dict: + dest_dict[vid] += coef + else: + dest_dict[vid] = coef + + +class LinearSubsystemRepn(LinearRepn): + def to_expression(self, visitor): + if self.nonlinear is not None: + # We want to start with the nonlinear term (and use + # assignment) in case the term is a non-numeric node (like a + # relational expression) + ans = self.nonlinear + else: + ans = 0 + if self.linear: + var_map = visitor.var_map + with mutable_expression() as e: + for vid, coef in self.linear.items(): + if coef.__class__ not in native_numeric_types or coef: + e += coef * var_map[vid] + if e.nargs() > 1: + ans += e + elif e.nargs() == 1: + ans += e.arg(0) + if self.constant.__class__ not in native_numeric_types or self.constant: + ans += self.constant + if ( + self.multiplier.__class__ not in native_numeric_types + or self.multiplier != 1 + ): + ans *= self.multiplier + return ans + + def append(self, other): + """Append a child result from acceptChildResult + + Notes + ----- + This method assumes that the operator was "+". It is implemented + so that we can directly use a LinearRepn() as a `data` object in + the expression walker (thereby allowing us to use the default + implementation of acceptChildResult [which calls + `data.append()`] and avoid the function call for a custom + callback). + + """ + _type, other = other + if _type is _CONSTANT: + self.constant += other + return + + mult = other.multiplier + try: + _mult = bool(mult) + if not _mult: + return + if mult == 1: + _mult = False + except: + _mult = True + + const = other.constant + try: + _const = bool(const) + except: + _const = True + + if _mult: + if _const: + self.constant += mult * const + if other.linear: + _merge_dict(self.linear, mult, other.linear) + if other.nonlinear is not None: + nl = mult * other.nonlinear + if self.nonlinear is None: + self.nonlinear = nl + else: + self.nonlinear += nl + else: + if _const: + self.constant += const + if other.linear: + _merge_dict(self.linear, 1, other.linear) + if other.nonlinear is not None: + nl = other.nonlinear + if self.nonlinear is None: + self.nonlinear = nl + else: + self.nonlinear += nl class MultiLevelLinearBeforeChildDispatcher(LinearBeforeChildDispatcher): @@ -50,7 +164,7 @@ def _before_var(visitor, child): if _id not in visitor.var_map: if child.fixed: return False, ( - ExprType.CONSTANT, + _CONSTANT, visitor.check_constant(child.value, child), ) MultiLevelLinearBeforeChildDispatcher._record_var(visitor, child) @@ -59,14 +173,32 @@ def _before_var(visitor, child): return False, (ExprType.LINEAR, ans) else: # We aren't treating this Var as a Var for the purposes of this walker - return False, (ExprType.CONSTANT, child) + return False, (_CONSTANT, child) _before_child_dispatcher = MultiLevelLinearBeforeChildDispatcher() +_exit_node_handlers = copy.deepcopy(linear._exit_node_handlers) +def _handle_product_constant_constant(visitor, node, arg1, arg2): + # ESJ: Can I do this? Just let the potential nans go through? + return _CONSTANT, arg1[1] * arg2[1] + +_exit_node_handlers[ProductExpression].update( + { + (_CONSTANT, _CONSTANT): _handle_product_constant_constant, + } +) + + # LinearSubsystemRepnVisitor class MultilevelLinearRepnVisitor(LinearRepnVisitor): + Result = LinearSubsystemRepn + exit_node_handlers = _exit_node_handlers + exit_node_dispatcher = ExitNodeDispatcher( + _initialize_exit_node_dispatcher(_exit_node_handlers) + ) + def __init__(self, subexpression_cache, var_map, var_order, sorter, wrt): super().__init__(subexpression_cache, var_map, var_order, sorter) self.wrt = ComponentSet(_flattened(wrt)) @@ -74,6 +206,22 @@ def __init__(self, subexpression_cache, var_map, var_order, sorter, wrt): def beforeChild(self, node, child, child_idx): return _before_child_dispatcher[child.__class__](self, child) + def _factor_multiplier_into_linear_terms(self, ans, mult): + linear = ans.linear + zeros = [] + for vid, coef in linear.items(): + if coef.__class__ not in native_numeric_types or coef: + linear[vid] = mult * coef + else: + zeros.append(vid) + for vid in zeros: + del linear[vid] + if ans.nonlinear is not None: + ans.nonlinear *= mult + if ans.constant.__class__ not in native_numeric_types or ans.constant: + ans.constant *= mult + ans.multiplier = 1 + def finalizeResult(self, result): ans = result[1] if ans.__class__ is self.Result: @@ -112,6 +260,6 @@ def finalizeResult(self, result): return ans ans = self.Result() - assert result[0] is ExprType.CONSTANT + assert result[0] is _CONSTANT ans.constant = result[1] return ans From cc2803483b05f6c9348b303aac4105d181346e49 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Tue, 23 Apr 2024 21:14:32 -0400 Subject: [PATCH 1244/3044] Fixed error about if values being ambiguous --- pyomo/contrib/doe/measurements.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index 5a3c44a76e4..11b84b4231b 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -93,17 +93,17 @@ def add_variables( upper_bounds, ) - if values: + if values is not None: # this dictionary keys are special set, values are its value self.variable_names_value.update(zip(added_names, values)) # if a scalar (int or float) is given, set it as the lower bound for all variables - if lower_bounds: + if lower_bounds is not None: if type(lower_bounds) in [int, float]: lower_bounds = [lower_bounds] * len(added_names) self.lower_bounds.update(zip(added_names, lower_bounds)) - if upper_bounds: + if upper_bounds is not None: if type(upper_bounds) in [int, float]: upper_bounds = [upper_bounds] * len(added_names) self.upper_bounds.update(zip(added_names, upper_bounds)) @@ -177,20 +177,20 @@ def _check_valid_input( raise ValueError("time index cannot be found in indices.") # if given a list, check if bounds have the same length with flattened variable - if values and len(values) != len_indices: + if values is not None and len(values) != len_indices: raise ValueError("Values is of different length with indices.") if ( - lower_bounds - and type(lower_bounds) == list - and len(lower_bounds) != len_indices + lower_bounds is not None # ensure not None + and type(lower_bounds) == list # ensure list + and len(lower_bounds) != len_indices # ensure same length ): raise ValueError("Lowerbounds is of different length with indices.") if ( - upper_bounds - and type(upper_bounds) == list - and len(upper_bounds) != len_indices + upper_bounds is not None # ensure None + and type(upper_bounds) == list # ensure list + and len(upper_bounds) != len_indices # ensure same length ): raise ValueError("Upperbounds is of different length with indices.") From 6d421a428c46a7621fb1f356406d392b3bc0e9e3 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Tue, 23 Apr 2024 21:31:39 -0400 Subject: [PATCH 1245/3044] Added exception to prevent cryptic error messages later. --- pyomo/contrib/doe/doe.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index d2ba2f277d6..eba22b954cf 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -101,6 +101,8 @@ def __init__( """ # parameters + if type(param_init) != dict: + raise ValueError("param_init should be a dictionary.") self.param = param_init # design variable name self.design_name = design_vars.variable_names From e09953a615370ec0f1c97c35d2ace2781377db0a Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 24 Apr 2024 09:54:11 -0600 Subject: [PATCH 1246/3044] Correcting nan propogation test assertion --- pyomo/repn/tests/test_linear_wrt.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/tests/test_linear_wrt.py b/pyomo/repn/tests/test_linear_wrt.py index d00668d5c00..43bc0a51914 100644 --- a/pyomo/repn/tests/test_linear_wrt.py +++ b/pyomo/repn/tests/test_linear_wrt.py @@ -241,15 +241,15 @@ def test_errors_propogate_nan(self): repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(expr) self.assertEqual( LOG.getvalue(), - "Exception encountered evaluating expression 'div(3, 0)'\n" + "Exception encountered evaluating expression 'div(3*z, 0)'\n" "\tmessage: division by zero\n" - "\texpression: 3/p\n", + "\texpression: 3*z*x/p\n", ) self.assertEqual(repn.multiplier, 1) assertExpressionsEqual( self, repn.constant, - 1 + m.z + m.y + m.z ) self.assertEqual(len(repn.linear), 1) self.assertEqual(str(repn.linear[id(m.x)]), 'InvalidNumber(nan)') From f1d480fe970472acc1919f4b32c8f9808bc2a179 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 24 Apr 2024 09:55:31 -0600 Subject: [PATCH 1247/3044] Fixing a bug where a division by 0 wasn't trapped and propogated as an invalid number --- pyomo/repn/linear.py | 2 +- pyomo/repn/tests/test_linear.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index ba08c7ef245..6d084067511 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -292,7 +292,7 @@ def _handle_division_constant_constant(visitor, node, arg1, arg2): def _handle_division_ANY_constant(visitor, node, arg1, arg2): - arg1[1].multiplier /= arg2[1] + arg1[1].multiplier = apply_node_operation(node, (arg1[1].multiplier, arg2[1])) return arg1 diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index 0fd428fd8ee..861fecc7888 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -1436,6 +1436,22 @@ def test_errors_propagate_nan(self): m.z = Var() m.y.fix(1) + expr = (m.x + 1) / m.p + cfg = VisitorConfig() + with LoggingIntercept() as LOG: + repn = LinearRepnVisitor(*cfg).walk_expression(expr) + self.assertEqual( + LOG.getvalue(), + "Exception encountered evaluating expression 'div(1, 0)'\n" + "\tmessage: division by zero\n" + "\texpression: (x + 1)/p\n", + ) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertEqual(len(repn.linear), 1) + self.assertEqual(str(repn.linear[id(m.x)]), 'InvalidNumber(nan)') + self.assertEqual(repn.nonlinear, None) + expr = m.y + m.x + m.z + ((3 * m.x) / m.p) / m.y cfg = VisitorConfig() with LoggingIntercept() as LOG: From d51bb99711b20ce3661c2e112fd804f9fd45d9b3 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 24 Apr 2024 11:40:35 -0600 Subject: [PATCH 1248/3044] Adding handling of nan in assertExpressionsEqual --- pyomo/core/expr/compare.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py index 790bc30aaee..e57e65a08f0 100644 --- a/pyomo/core/expr/compare.py +++ b/pyomo/core/expr/compare.py @@ -230,10 +230,14 @@ def assertExpressionsEqual(test, a, b, include_named_exprs=True, places=None): test.assertEqual(len(prefix_a), len(prefix_b)) for _a, _b in zip(prefix_a, prefix_b): test.assertIs(_a.__class__, _b.__class__) - if places is None: - test.assertEqual(_a, _b) + # If _a is nan, check _b is nan + if _a != _a: + test.assertTrue(_b != _b) else: - test.assertAlmostEqual(_a, _b, places=places) + if places is None: + test.assertEqual(_a, _b) + else: + test.assertAlmostEqual(_a, _b, places=places) except (PyomoException, AssertionError): test.fail( f"Expressions not equal:\n\t" @@ -292,10 +296,13 @@ def assertExpressionsStructurallyEqual( for _a, _b in zip(prefix_a, prefix_b): if _a.__class__ not in native_types and _b.__class__ not in native_types: test.assertIs(_a.__class__, _b.__class__) - if places is None: - test.assertEqual(_a, _b) + if _a != _a: + test.assertTrue(_b != _b) else: - test.assertAlmostEqual(_a, _b, places=places) + if places is None: + test.assertEqual(_a, _b) + else: + test.assertAlmostEqual(_a, _b, places=places) except (PyomoException, AssertionError): test.fail( f"Expressions not structurally equal:\n\t" From ec5879b5191aa1fbc525867d3ed48d0e55294e50 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 24 Apr 2024 11:44:22 -0600 Subject: [PATCH 1249/3044] nan propogation tests are finally passing --- pyomo/repn/tests/test_linear_wrt.py | 43 +++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/tests/test_linear_wrt.py b/pyomo/repn/tests/test_linear_wrt.py index 43bc0a51914..ef86aed47f8 100644 --- a/pyomo/repn/tests/test_linear_wrt.py +++ b/pyomo/repn/tests/test_linear_wrt.py @@ -15,6 +15,7 @@ from pyomo.environ import Any, Binary, ConcreteModel, log, Param, Var from pyomo.repn.linear_wrt import MultilevelLinearRepnVisitor from pyomo.repn.tests.test_linear import VisitorConfig +from pyomo.repn.util import InvalidNumber class TestMultilevelLinearRepnVisitor(unittest.TestCase): @@ -227,6 +228,34 @@ def test_finalize(self): ) assertExpressionsEqual(self, repn.nonlinear, (m.z**2 + 3 * m.w * m.y**3) * 5) + def test_ANY_over_constant_division(self): + m = ConcreteModel() + m.p = Param(mutable=True, initialize=2, domain=Any) + m.x = Var() + m.y = Var() + m.z = Var() + # We aren't treating this as a Var, so we don't really care that it's fixed. + m.y.fix(1) + + expr = m.y + m.x + m.z + ((3 * m.z * m.x) / m.p) / m.y + cfg = VisitorConfig() + repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(expr) + + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual( + self, + repn.constant, + m.y + m.z + ) + self.assertEqual(len(repn.linear), 1) + print(repn.linear[id(m.x)]) + assertExpressionsEqual( + self, + repn.linear[id(m.x)], + 1 + 1.5 * m.z / m.y + ) + self.assertEqual(repn.nonlinear, None) + def test_errors_propogate_nan(self): m = ConcreteModel() m.p = Param(mutable=True, initialize=0, domain=Any) @@ -252,13 +281,23 @@ def test_errors_propogate_nan(self): m.y + m.z ) self.assertEqual(len(repn.linear), 1) - self.assertEqual(str(repn.linear[id(m.x)]), 'InvalidNumber(nan)') + self.assertIsInstance(repn.linear[id(m.x)], InvalidNumber) + assertExpressionsEqual( + self, + repn.linear[id(m.x)].value, + 1 + float('nan')/m.y + ) self.assertEqual(repn.nonlinear, None) m.y.fix(None) expr = m.z * log(m.y) + 3 repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(expr) self.assertEqual(repn.multiplier, 1) - self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') + self.assertIsInstance(repn.constant, InvalidNumber) + assertExpressionsEqual( + self, + repn.constant.value, + float('nan')*m.z + 3 + ) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) From 910cf2d1247f0d75e80b6bd776236e8e05ae6beb Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Wed, 24 Apr 2024 21:04:11 -0400 Subject: [PATCH 1250/3044] Added units. --- pyomo/contrib/doe/doe.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index eba22b954cf..8c83e3145ce 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -240,12 +240,12 @@ def stochastic_program( if self.optimize: analysis_optimize = self._optimize_stochastic_program(m) dT = sp_timer.toc(msg=None) - self.logger.info("elapsed time: %0.1f" % dT) + self.logger.info("elapsed time: %0.1f seconds" % dT) return analysis_square, analysis_optimize else: dT = sp_timer.toc(msg=None) - self.logger.info("elapsed time: %0.1f" % dT) + self.logger.info("elapsed time: %0.1f seconds" % dT) return analysis_square def _compute_stochastic_program(self, m, optimize_option): @@ -389,7 +389,7 @@ def compute_FIM( FIM_analysis = self._direct_kaug() dT = square_timer.toc(msg=None) - self.logger.info("elapsed time: %0.1f" % dT) + self.logger.info("elapsed time: %0.1f seconds" % dT) return FIM_analysis From 5df54523e46be0e66aac827e21286a9977592011 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Wed, 24 Apr 2024 21:52:51 -0400 Subject: [PATCH 1251/3044] Switched to isinstance --- pyomo/contrib/doe/doe.py | 6 +++--- pyomo/contrib/doe/measurements.py | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 8c83e3145ce..b55f9aac0ac 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -38,7 +38,7 @@ from pyomo.contrib.sensitivity_toolbox.sens import get_dsdp from pyomo.contrib.doe.scenario import ScenarioGenerator, FiniteDifferenceStep from pyomo.contrib.doe.result import FisherResults, GridSearchResult - +import collections class CalculationMode(Enum): sequential_finite = "sequential_finite" @@ -101,7 +101,7 @@ def __init__( """ # parameters - if type(param_init) != dict: + if not isinstance(param_init, collections.Mapping): raise ValueError("param_init should be a dictionary.") self.param = param_init # design variable name @@ -777,7 +777,7 @@ def run_grid_search( # update the controlled value of certain time points for certain design variables for i, names in enumerate(design_dimension_names): # if the element is a list, all design variables in this list share the same values - if type(names) is list or type(names) is tuple: + if isinstance(names, collections.Sequence): for n in names: design_iter[n] = list(design_set_iter)[i] else: diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index 11b84b4231b..aa196ec9a49 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -26,6 +26,7 @@ # ___________________________________________________________________________ import itertools +import collections class VariablesWithIndices: @@ -171,7 +172,7 @@ def _check_valid_input( """ Check if the measurement information provided are valid to use. """ - assert type(var_name) is str, "var_name should be a string." + assert isinstance(var_name, str), "var_name should be a string." if time_index_position not in indices: raise ValueError("time index cannot be found in indices.") @@ -182,14 +183,14 @@ def _check_valid_input( if ( lower_bounds is not None # ensure not None - and type(lower_bounds) == list # ensure list + and isinstance(lower_bounds, collections.Sequence) # ensure list-like and len(lower_bounds) != len_indices # ensure same length ): raise ValueError("Lowerbounds is of different length with indices.") if ( upper_bounds is not None # ensure None - and type(upper_bounds) == list # ensure list + and isinstance(upper_bounds, collections.Sequence) # ensure list-like and len(upper_bounds) != len_indices # ensure same length ): raise ValueError("Upperbounds is of different length with indices.") From ea9d226f368959e3e028a4bd9a4a7bb77ea29938 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 24 Apr 2024 22:08:44 -0600 Subject: [PATCH 1252/3044] Skip black 24.4.1 due to a bug in the parser --- .github/workflows/test_branches.yml | 3 ++- .github/workflows/test_pr_and_main.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 1885f6a00e2..75db5d66431 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -40,7 +40,8 @@ jobs: python-version: '3.10' - name: Black Formatting Check run: | - pip install black + # Note v24.4.1 fails due to a bug in the parser + pip install 'black!=24.4.1' black . -S -C --check --diff --exclude examples/pyomobook/python-ch/BadIndent.py - name: Spell Check uses: crate-ci/typos@master diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 619a5e695e2..eb059a7ef82 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -43,7 +43,8 @@ jobs: python-version: '3.10' - name: Black Formatting Check run: | - pip install black + # Note v24.4.1 fails due to a bug in the parser + pip install 'black!=24.4.1' black . -S -C --check --diff --exclude examples/pyomobook/python-ch/BadIndent.py - name: Spell Check uses: crate-ci/typos@master From 98ee3ec8ad70b49f25a8aade1453c1d3be58ac8a Mon Sep 17 00:00:00 2001 From: robbybp Date: Wed, 24 Apr 2024 22:16:20 -0600 Subject: [PATCH 1253/3044] add more description to readme --- pyomo/contrib/pynumero/README.md | 43 ++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pynumero/README.md b/pyomo/contrib/pynumero/README.md index d4b6344ec76..d68c055e97c 100644 --- a/pyomo/contrib/pynumero/README.md +++ b/pyomo/contrib/pynumero/README.md @@ -78,8 +78,13 @@ Code organization PyNumero was initially designed around three core components: linear solver interfaces, an interface for function and derivative callbacks, and block vector and matrix classes. Since then, it has incorporated additional -functionality in an ad-hoc manner. The following is a rough overview of -PyNumero, by directory: +functionality in an ad-hoc manner. The original "core functionality" of +PyNumero, as well as the solver interfaces accessible through +`SolverFactory`, should be considered stable and will only change after +appropriate deprecation warnings. Other functionality should be considered +experimental and subject to change without warning. + +The following is a rough overview of PyNumero, by directory: `linalg` -------- @@ -100,5 +105,39 @@ definition of an external grey box via an implicit function - The `CyIpoptNLP` class, which wraps an object implementing the NLP API in the interface required by CyIpopt +Of the above, only `PyomoNLP` and the `NLP` base class should be considered core +functionality. + `src` ----- + +C++ interfaces to ASL, MA27, and MA57. The ASL and MA27 interfaces are +core functionality. + +`sparse` +-------- + +Block vector and block matrix classes, including MPI variations. +These are core functionality. + +`algorithms` +------------ + +Originally intended to hold various useful algorithms implemented +on NLP objects rather than Pyomo models. Any files added here should +be considered experimental. + +`algorithms/solvers` +-------------------- + +Interfaces to Python solvers using the NLP API defined in `interfaces`. +Only the solvers accessible through `SolverFactory`, e.g. `PyomoCyIpoptSolver` +and `PyomoFsolveSolver`, should be considered core functionality. + +`examples` +---------- + +The examples demonstrated in `nlp_interface.py`, `nlp_interface_2.py1`, +`feasibility.py`, `mumps_example.py`, `sensitivity.py`, `sqp.py`, +`parallel_matvec.py`, and `parallel_vector_ops.py` are stable. All other +examples should be considered experimental. From 5689e7406574d5baa8d80c38f91264f7d9fc59ed Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 25 Apr 2024 09:33:06 -0600 Subject: [PATCH 1254/3044] add note that location of pyomo solvers is subject to change --- pyomo/contrib/pynumero/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/pynumero/README.md b/pyomo/contrib/pynumero/README.md index d68c055e97c..f881e400d51 100644 --- a/pyomo/contrib/pynumero/README.md +++ b/pyomo/contrib/pynumero/README.md @@ -133,6 +133,8 @@ be considered experimental. Interfaces to Python solvers using the NLP API defined in `interfaces`. Only the solvers accessible through `SolverFactory`, e.g. `PyomoCyIpoptSolver` and `PyomoFsolveSolver`, should be considered core functionality. +The supported way to access these solvers is via `SolverFactory`. *The locations +of the underlying solver objects are subject to change without warning.* `examples` ---------- From a0bc0891f900993692c65c88c2c0e9aacc435d88 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Thu, 25 Apr 2024 11:30:23 -0600 Subject: [PATCH 1255/3044] Incorporate suggestion on wording Co-authored-by: Bethany Nicholson --- doc/OnlineDocs/contribution_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contribution_guide.rst b/doc/OnlineDocs/contribution_guide.rst index 6054a8d2ba9..b98dcc3d014 100644 --- a/doc/OnlineDocs/contribution_guide.rst +++ b/doc/OnlineDocs/contribution_guide.rst @@ -93,7 +93,7 @@ active development may be marked 'stale' and closed. .. note:: Draft and WIP Pull Requests will **NOT** trigger tests. This is an effort to - keep our backlog as available as possible. Please make use of the provided + reduce our CI backlog. Please make use of the provided branch test suite for evaluating / testing draft functionality. Python Version Support From 312f5722b1a703413133c887b415a0d249d1e032 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 25 Apr 2024 21:46:59 -0400 Subject: [PATCH 1256/3044] Initialization improvements and degree of freedom fixes are important! --- pyomo/contrib/doe/doe.py | 116 +++++++++++++++++++++++++++--- pyomo/contrib/doe/measurements.py | 6 +- 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index b55f9aac0ac..cac9bfd9271 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -38,7 +38,9 @@ from pyomo.contrib.sensitivity_toolbox.sens import get_dsdp from pyomo.contrib.doe.scenario import ScenarioGenerator, FiniteDifferenceStep from pyomo.contrib.doe.result import FisherResults, GridSearchResult -import collections +import collections.abc + +import inspect class CalculationMode(Enum): sequential_finite = "sequential_finite" @@ -101,7 +103,7 @@ def __init__( """ # parameters - if not isinstance(param_init, collections.Mapping): + if not isinstance(param_init, collections.abc.Mapping): raise ValueError("param_init should be a dictionary.") self.param = param_init # design variable name @@ -238,6 +240,10 @@ def stochastic_program( m, analysis_square = self._compute_stochastic_program(m, optimize_opt) if self.optimize: + # set max_iter to 0 to debug the initialization + # self.solver.options["max_iter"] = 0 + # self.solver.options["bound_push"] = 1e-10 + analysis_optimize = self._optimize_stochastic_program(m) dT = sp_timer.toc(msg=None) self.logger.info("elapsed time: %0.1f seconds" % dT) @@ -588,13 +594,34 @@ def _create_block(self): # Set for block/scenarios mod.scenario = pyo.Set(initialize=self.scenario_data.scenario_indices) + + # Determine if create_model takes theta as an optional input + # print(inspect.getfullargspec(self.create_model)) + pass_theta_to_initialize= ('theta' in inspect.getfullargspec(self.create_model).args) + #print("pass_theta_to_initialize =", pass_theta_to_initialize) # Allow user to self-define complex design variables self.create_model(mod=mod, model_option=ModelOptionLib.stage1) + # Fix parameter values in the copy of the stage1 model (if they exist) + for par in self.param: + cuid = pyo.ComponentUID(par) + var = cuid.find_component_on(mod) + if var is not None: + # Fix the parameter value + # Otherwise, the parameter does not exist on the stage 1 model + var.fix(self.param[par]) + def block_build(b, s): # create block scenarios - self.create_model(mod=b, model_option=ModelOptionLib.stage2) + # idea: check if create_model takes theta as an optional input, if so, pass parameter values to create_model + + if pass_theta_to_initialize: + theta_initialize = self.scenario_data.scenario[s] + #print("Initializing with theta=", theta_initialize) + self.create_model(mod=b, model_option=ModelOptionLib.stage2, theta=theta_initialize) + else: + self.create_model(mod=b, model_option=ModelOptionLib.stage2) # fix parameter values to perturbed values for par in self.param: @@ -605,7 +632,7 @@ def block_build(b, s): mod.block = pyo.Block(mod.scenario, rule=block_build) # discretize the model - if self.discretize_model: + if self.discretize_model is not None: mod = self.discretize_model(mod) # force design variables in blocks to be equal to global design values @@ -777,7 +804,7 @@ def run_grid_search( # update the controlled value of certain time points for certain design variables for i, names in enumerate(design_dimension_names): # if the element is a list, all design variables in this list share the same values - if isinstance(names, collections.Sequence): + if isinstance(names, collections.abc.Sequence): for n in names: design_iter[n] = list(design_set_iter)[i] else: @@ -881,20 +908,39 @@ def identity_matrix(m, i, j): else: return 0 + if self.jac_initial is not None: + dict_jac_initialize = {} + for i, bu in enumerate(model.regression_parameters): + for j, un in enumerate(model.measured_variables): + if isinstance(self.jac_initial, dict): + # Jacobian is a dictionary of arrays or lists where the key is the regression parameter name + dict_jac_initialize[(bu, un)] = self.jac_initial[bu][j] + elif isinstance(self.jac_initial, np.ndarray): + # Jacobian is a numpy array, rows are regression parameters, columns are measured variables + dict_jac_initialize[(bu, un)] = self.jac_initial[i][j] + + def initialize_jac(m, i, j): + if self.jac_initial is not None: + return dict_jac_initialize[(i, j)] + else: + return 0.1 + model.sensitivity_jacobian = pyo.Var( - model.regression_parameters, model.measured_variables, initialize=0.1 + model.regression_parameters, model.measured_variables, initialize=initialize_jac ) - if self.fim_initial: + if self.fim_initial is not None: dict_fim_initialize = {} for i, bu in enumerate(model.regression_parameters): for j, un in enumerate(model.regression_parameters): dict_fim_initialize[(bu, un)] = self.fim_initial[i][j] + + #print(dict_fim_initialize) def initialize_fim(m, j, d): return dict_fim_initialize[(j, d)] - if self.fim_initial: + if self.fim_initial is not None: model.fim = pyo.Var( model.regression_parameters, model.regression_parameters, @@ -1013,6 +1059,32 @@ def fim_rule(m, p, q): return model def _add_objective(self, m): + + ### Initialize the Cholesky decomposition matrix + if self.Cholesky_option: + + # Assemble the FIM matrix + fim = np.zeros((len(self.param), len(self.param))) + for i, bu in enumerate(m.regression_parameters): + for j, un in enumerate(m.regression_parameters): + fim[i][j] = m.fim[bu, un].value + + # Calculate the eigenvalues of the FIM matrix + eig = np.linalg.eigvals(fim) + + # If the smallest eigenvalue is (pratcially) negative, add a diagonal matrix to make it positive definite + small_number = 1E-10 + if min(eig) < small_number: + fim = fim + np.eye(len(self.param)) * (small_number - min(eig)) + + # Compute the Cholesky decomposition of the FIM matrix + L = np.linalg.cholesky(fim) + + # Initialize the Cholesky matrix + for i, c in enumerate(m.regression_parameters): + for j, d in enumerate(m.regression_parameters): + m.L_ele[c, d].value = L[i, j] + def cholesky_imp(m, c, d): """ Calculate Cholesky L matrix using algebraic constraints @@ -1103,14 +1175,20 @@ def _fix_design(self, m, design_val, fix_opt=True, optimize_option=None): m: model """ for name in self.design_name: + # Loop over design variables + # Get Pyomo variable object cuid = pyo.ComponentUID(name) var = cuid.find_component_on(m) if fix_opt: + # If fix_opt is True, fix the design variable var.fix(design_val[name]) else: + # Otherwise check optimize_option if optimize_option is None: + # If optimize_option is None, unfix all design variables var.unfix() else: + # Otherwise, unfix only the design variables listed in optimize_option with value True if optimize_option[name]: var.unfix() return m @@ -1126,7 +1204,7 @@ def _get_default_ipopt_solver(self): def _solve_doe(self, m, fix=False, opt_option=None): """Solve DOE model. If it's a square problem, fix design variable and solve. - Else, fix design variable and solve square problem firstly, then unfix them and solve the optimization problem + Else, fix design variable and solve square problem first, then unfix them and solve the optimization problem Parameters ---------- @@ -1140,14 +1218,30 @@ def _solve_doe(self, m, fix=False, opt_option=None): ------- solver_results: solver results """ - ### Solve square problem + # if fix = False, solve the optimization problem + # if fix = True, solve the square problem + + # either fix or unfix the design variables mod = self._fix_design( m, self.design_values, fix_opt=fix, optimize_option=opt_option ) + ''' + # This is for initialization diagnostics + # Remove before merging the PR + if not fix: + # halt at initial point + self.solver.options['max_iter'] = 0 + self.solver.options['bound_push'] = 1E-10 + else: + # resort to defaults + self.solver.options['max_iter'] = 3000 + self.solver.options['bound_push'] = 0.01 + ''' + # if user gives solver, use this solver. if not, use default IPOPT solver solver_result = self.solver.solve(mod, tee=self.tee_opt) - + return solver_result def _sgn(self, p): diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index aa196ec9a49..ae5b3519498 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -26,7 +26,7 @@ # ___________________________________________________________________________ import itertools -import collections +import collections.abc class VariablesWithIndices: @@ -183,14 +183,14 @@ def _check_valid_input( if ( lower_bounds is not None # ensure not None - and isinstance(lower_bounds, collections.Sequence) # ensure list-like + and isinstance(lower_bounds, collections.abc.Sequence) # ensure list-like and len(lower_bounds) != len_indices # ensure same length ): raise ValueError("Lowerbounds is of different length with indices.") if ( upper_bounds is not None # ensure None - and isinstance(upper_bounds, collections.Sequence) # ensure list-like + and isinstance(upper_bounds, collections.abc.Sequence) # ensure list-like and len(upper_bounds) != len_indices # ensure same length ): raise ValueError("Upperbounds is of different length with indices.") From 78c8558d3c0bc0beca6683ffe6b124223e501f6d Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 25 Apr 2024 21:53:56 -0400 Subject: [PATCH 1257/3044] Updated one type check and removed extra code from debugging. --- pyomo/contrib/doe/doe.py | 29 +++++++---------------------- pyomo/contrib/doe/measurements.py | 6 +++--- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index cac9bfd9271..d10fc1b7fcc 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -230,6 +230,7 @@ def stochastic_program( # FIM = Jacobian.T@Jacobian, the FIM is scaled by squared value the Jacobian is scaled self.fim_scale_constant_value = self.scale_constant_value**2 + # Start timer sp_timer = TicTocTimer() sp_timer.tic(msg=None) @@ -240,18 +241,17 @@ def stochastic_program( m, analysis_square = self._compute_stochastic_program(m, optimize_opt) if self.optimize: - # set max_iter to 0 to debug the initialization - # self.solver.options["max_iter"] = 0 - # self.solver.options["bound_push"] = 1e-10 - + # If set to optimize, solve the optimization problem (with degrees of freedom) analysis_optimize = self._optimize_stochastic_program(m) dT = sp_timer.toc(msg=None) self.logger.info("elapsed time: %0.1f seconds" % dT) + # Return both square problem and optimization problem results return analysis_square, analysis_optimize else: dT = sp_timer.toc(msg=None) self.logger.info("elapsed time: %0.1f seconds" % dT) + # Return only square problem results return analysis_square def _compute_stochastic_program(self, m, optimize_option): @@ -596,9 +596,7 @@ def _create_block(self): mod.scenario = pyo.Set(initialize=self.scenario_data.scenario_indices) # Determine if create_model takes theta as an optional input - # print(inspect.getfullargspec(self.create_model)) pass_theta_to_initialize= ('theta' in inspect.getfullargspec(self.create_model).args) - #print("pass_theta_to_initialize =", pass_theta_to_initialize) # Allow user to self-define complex design variables self.create_model(mod=mod, model_option=ModelOptionLib.stage1) @@ -617,10 +615,12 @@ def block_build(b, s): # idea: check if create_model takes theta as an optional input, if so, pass parameter values to create_model if pass_theta_to_initialize: + # Grab the values of theta for this scenario/block theta_initialize = self.scenario_data.scenario[s] - #print("Initializing with theta=", theta_initialize) + # Add model on block with theta values self.create_model(mod=b, model_option=ModelOptionLib.stage2, theta=theta_initialize) else: + # Otherwise add model on block without theta values self.create_model(mod=b, model_option=ModelOptionLib.stage2) # fix parameter values to perturbed values @@ -934,8 +934,6 @@ def initialize_jac(m, i, j): for i, bu in enumerate(model.regression_parameters): for j, un in enumerate(model.regression_parameters): dict_fim_initialize[(bu, un)] = self.fim_initial[i][j] - - #print(dict_fim_initialize) def initialize_fim(m, j, d): return dict_fim_initialize[(j, d)] @@ -1226,19 +1224,6 @@ def _solve_doe(self, m, fix=False, opt_option=None): m, self.design_values, fix_opt=fix, optimize_option=opt_option ) - ''' - # This is for initialization diagnostics - # Remove before merging the PR - if not fix: - # halt at initial point - self.solver.options['max_iter'] = 0 - self.solver.options['bound_push'] = 1E-10 - else: - # resort to defaults - self.solver.options['max_iter'] = 3000 - self.solver.options['bound_push'] = 0.01 - ''' - # if user gives solver, use this solver. if not, use default IPOPT solver solver_result = self.solver.solve(mod, tee=self.tee_opt) diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index ae5b3519498..dcaac1f14fd 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -27,7 +27,7 @@ import itertools import collections.abc - +from pyomo.common.numeric_types import native_numeric_types class VariablesWithIndices: def __init__(self): @@ -100,12 +100,12 @@ def add_variables( # if a scalar (int or float) is given, set it as the lower bound for all variables if lower_bounds is not None: - if type(lower_bounds) in [int, float]: + if type(lower_bounds) in native_numeric_types: lower_bounds = [lower_bounds] * len(added_names) self.lower_bounds.update(zip(added_names, lower_bounds)) if upper_bounds is not None: - if type(upper_bounds) in [int, float]: + if type(upper_bounds) in native_numeric_types: upper_bounds = [upper_bounds] * len(added_names) self.upper_bounds.update(zip(added_names, upper_bounds)) From 845fc3fdcae84180cd7f777887a771f55074e2bf Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 25 Apr 2024 21:58:33 -0400 Subject: [PATCH 1258/3044] Ran black --- pyomo/contrib/doe/doe.py | 27 +++++++++++++++++---------- pyomo/contrib/doe/measurements.py | 13 +++++++------ pyomo/contrib/doe/result.py | 2 +- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index d10fc1b7fcc..ed5e81027dd 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -42,6 +42,7 @@ import inspect + class CalculationMode(Enum): sequential_finite = "sequential_finite" direct_kaug = "direct_kaug" @@ -228,7 +229,7 @@ def stochastic_program( # calculate how much the FIM element is scaled by a constant number # FIM = Jacobian.T@Jacobian, the FIM is scaled by squared value the Jacobian is scaled - self.fim_scale_constant_value = self.scale_constant_value**2 + self.fim_scale_constant_value = self.scale_constant_value ** 2 # Start timer sp_timer = TicTocTimer() @@ -241,7 +242,7 @@ def stochastic_program( m, analysis_square = self._compute_stochastic_program(m, optimize_opt) if self.optimize: - # If set to optimize, solve the optimization problem (with degrees of freedom) + # If set to optimize, solve the optimization problem (with degrees of freedom) analysis_optimize = self._optimize_stochastic_program(m) dT = sp_timer.toc(msg=None) self.logger.info("elapsed time: %0.1f seconds" % dT) @@ -382,7 +383,7 @@ def compute_FIM( # calculate how much the FIM element is scaled by a constant number # As FIM~Jacobian.T@Jacobian, FIM is scaled twice the number the Q is scaled - self.fim_scale_constant_value = self.scale_constant_value**2 + self.fim_scale_constant_value = self.scale_constant_value ** 2 square_timer = TicTocTimer() square_timer.tic(msg=None) @@ -594,9 +595,11 @@ def _create_block(self): # Set for block/scenarios mod.scenario = pyo.Set(initialize=self.scenario_data.scenario_indices) - + # Determine if create_model takes theta as an optional input - pass_theta_to_initialize= ('theta' in inspect.getfullargspec(self.create_model).args) + pass_theta_to_initialize = ( + 'theta' in inspect.getfullargspec(self.create_model).args + ) # Allow user to self-define complex design variables self.create_model(mod=mod, model_option=ModelOptionLib.stage1) @@ -618,7 +621,9 @@ def block_build(b, s): # Grab the values of theta for this scenario/block theta_initialize = self.scenario_data.scenario[s] # Add model on block with theta values - self.create_model(mod=b, model_option=ModelOptionLib.stage2, theta=theta_initialize) + self.create_model( + mod=b, model_option=ModelOptionLib.stage2, theta=theta_initialize + ) else: # Otherwise add model on block without theta values self.create_model(mod=b, model_option=ModelOptionLib.stage2) @@ -773,7 +778,7 @@ def run_grid_search( self.store_optimality_as_csv = store_optimality_as_csv # calculate how much the FIM element is scaled - self.fim_scale_constant_value = scale_constant_value**2 + self.fim_scale_constant_value = scale_constant_value ** 2 # to store all FIM results result_combine = {} @@ -926,7 +931,9 @@ def initialize_jac(m, i, j): return 0.1 model.sensitivity_jacobian = pyo.Var( - model.regression_parameters, model.measured_variables, initialize=initialize_jac + model.regression_parameters, + model.measured_variables, + initialize=initialize_jac, ) if self.fim_initial is not None: @@ -1071,7 +1078,7 @@ def _add_objective(self, m): eig = np.linalg.eigvals(fim) # If the smallest eigenvalue is (pratcially) negative, add a diagonal matrix to make it positive definite - small_number = 1E-10 + small_number = 1e-10 if min(eig) < small_number: fim = fim + np.eye(len(self.param)) * (small_number - min(eig)) @@ -1226,7 +1233,7 @@ def _solve_doe(self, m, fix=False, opt_option=None): # if user gives solver, use this solver. if not, use default IPOPT solver solver_result = self.solver.solve(mod, tee=self.tee_opt) - + return solver_result def _sgn(self, p): diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index dcaac1f14fd..fd3962f7888 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -29,6 +29,7 @@ import collections.abc from pyomo.common.numeric_types import native_numeric_types + class VariablesWithIndices: def __init__(self): """This class provides utility methods for DesignVariables and MeasurementVariables to create @@ -182,16 +183,16 @@ def _check_valid_input( raise ValueError("Values is of different length with indices.") if ( - lower_bounds is not None # ensure not None - and isinstance(lower_bounds, collections.abc.Sequence) # ensure list-like - and len(lower_bounds) != len_indices # ensure same length + lower_bounds is not None # ensure not None + and isinstance(lower_bounds, collections.abc.Sequence) # ensure list-like + and len(lower_bounds) != len_indices # ensure same length ): raise ValueError("Lowerbounds is of different length with indices.") if ( - upper_bounds is not None # ensure None - and isinstance(upper_bounds, collections.abc.Sequence) # ensure list-like - and len(upper_bounds) != len_indices # ensure same length + upper_bounds is not None # ensure None + and isinstance(upper_bounds, collections.abc.Sequence) # ensure list-like + and len(upper_bounds) != len_indices # ensure same length ): raise ValueError("Upperbounds is of different length with indices.") diff --git a/pyomo/contrib/doe/result.py b/pyomo/contrib/doe/result.py index 1593214c30a..8f98e74f159 100644 --- a/pyomo/contrib/doe/result.py +++ b/pyomo/contrib/doe/result.py @@ -81,7 +81,7 @@ def __init__( self.prior_FIM = prior_FIM self.store_FIM = store_FIM self.scale_constant_value = scale_constant_value - self.fim_scale_constant_value = scale_constant_value**2 + self.fim_scale_constant_value = scale_constant_value ** 2 self.max_condition_number = max_condition_number self.logger = logging.getLogger(__name__) self.logger.setLevel(level=logging.WARN) From 5c7cab5bb4629f2dc5ed6dde08d006c78b3735bf Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 25 Apr 2024 21:59:45 -0400 Subject: [PATCH 1259/3044] Reran black. --- pyomo/contrib/doe/doe.py | 6 +++--- pyomo/contrib/doe/result.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index ed5e81027dd..0d2c7c2f982 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -229,7 +229,7 @@ def stochastic_program( # calculate how much the FIM element is scaled by a constant number # FIM = Jacobian.T@Jacobian, the FIM is scaled by squared value the Jacobian is scaled - self.fim_scale_constant_value = self.scale_constant_value ** 2 + self.fim_scale_constant_value = self.scale_constant_value**2 # Start timer sp_timer = TicTocTimer() @@ -383,7 +383,7 @@ def compute_FIM( # calculate how much the FIM element is scaled by a constant number # As FIM~Jacobian.T@Jacobian, FIM is scaled twice the number the Q is scaled - self.fim_scale_constant_value = self.scale_constant_value ** 2 + self.fim_scale_constant_value = self.scale_constant_value**2 square_timer = TicTocTimer() square_timer.tic(msg=None) @@ -778,7 +778,7 @@ def run_grid_search( self.store_optimality_as_csv = store_optimality_as_csv # calculate how much the FIM element is scaled - self.fim_scale_constant_value = scale_constant_value ** 2 + self.fim_scale_constant_value = scale_constant_value**2 # to store all FIM results result_combine = {} diff --git a/pyomo/contrib/doe/result.py b/pyomo/contrib/doe/result.py index 8f98e74f159..1593214c30a 100644 --- a/pyomo/contrib/doe/result.py +++ b/pyomo/contrib/doe/result.py @@ -81,7 +81,7 @@ def __init__( self.prior_FIM = prior_FIM self.store_FIM = store_FIM self.scale_constant_value = scale_constant_value - self.fim_scale_constant_value = scale_constant_value ** 2 + self.fim_scale_constant_value = scale_constant_value**2 self.max_condition_number = max_condition_number self.logger = logging.getLogger(__name__) self.logger.setLevel(level=logging.WARN) From a8a5450fc5021b1c30daafcdb282f0e936a8d9ad Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 25 Apr 2024 22:06:38 -0400 Subject: [PATCH 1260/3044] Added more comments. --- pyomo/contrib/doe/doe.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 0d2c7c2f982..28dad1f20c7 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -913,6 +913,9 @@ def identity_matrix(m, i, j): else: return 0 + ### Initialize the Jacobian if provided by the user + + # If the user provides an initial Jacobian, convert it to a dictionary if self.jac_initial is not None: dict_jac_initialize = {} for i, bu in enumerate(model.regression_parameters): @@ -924,9 +927,12 @@ def identity_matrix(m, i, j): # Jacobian is a numpy array, rows are regression parameters, columns are measured variables dict_jac_initialize[(bu, un)] = self.jac_initial[i][j] + # Initialize the Jacobian matrix def initialize_jac(m, i, j): + # If provided by the user, use the values now stored in the dictionary if self.jac_initial is not None: return dict_jac_initialize[(i, j)] + # Otherwise initialize to 0.1 (which is an arbitrary non-zero value) else: return 0.1 From 1bac09e842eaaeeeaef8191e30e500f2e311fefb Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 25 Apr 2024 22:21:24 -0400 Subject: [PATCH 1261/3044] Reran black --- pyomo/contrib/doe/doe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 28dad1f20c7..ab9a5ad9f85 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -914,7 +914,7 @@ def identity_matrix(m, i, j): return 0 ### Initialize the Jacobian if provided by the user - + # If the user provides an initial Jacobian, convert it to a dictionary if self.jac_initial is not None: dict_jac_initialize = {} From 4f5f50e64f6cddbc4ae9dd227deba5225575e320 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 29 Apr 2024 13:59:50 -0600 Subject: [PATCH 1262/3044] More sum tests --- pyomo/repn/tests/test_linear_wrt.py | 33 +++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/pyomo/repn/tests/test_linear_wrt.py b/pyomo/repn/tests/test_linear_wrt.py index ef86aed47f8..6853c0df7f6 100644 --- a/pyomo/repn/tests/test_linear_wrt.py +++ b/pyomo/repn/tests/test_linear_wrt.py @@ -62,6 +62,39 @@ def test_walk_triple_sum(self): self.assertEqual(repn.multiplier, 1) assertExpressionsEqual(self, repn.to_expression(visitor), m.x + m.z * m.y + m.z) + def test_sum_two_of_the_same(self): + # This hits the mult == 1 and vid in dest_dict case in _merge_dict + m = self.make_model() + e = m.x + m.x + cfg = VisitorConfig() + visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x, m.y]) + + repn = visitor.walk_expression(e) + + self.assertIsNone(repn.nonlinear) + self.assertEqual(len(repn.linear), 1) + self.assertIn(id(m.x), repn.linear) + self.assertEqual(repn.linear[id(m.x)], 2) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.to_expression(visitor), 2*m.x) + + def test_sum_with_mult_0(self): + m = self.make_model() + e = 0*m.x + m.x + m.y + + cfg = VisitorConfig() + visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + + repn = visitor.walk_expression(e) + self.assertIsNone(repn.nonlinear) + self.assertEqual(len(repn.linear), 1) + self.assertIn(id(m.x), repn.linear) + self.assertEqual(repn.linear[id(m.x)], 1) + self.assertIs(repn.constant, m.y) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.to_expression(visitor), m.x + m.y) + def test_bilinear_term(self): m = self.make_model() e = m.x * m.y From c9659c8c5a243959f7752f1d75a4f79ee53d6e4c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 29 Apr 2024 14:24:51 -0600 Subject: [PATCH 1263/3044] Tests for everything in append --- pyomo/repn/tests/test_linear_wrt.py | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/pyomo/repn/tests/test_linear_wrt.py b/pyomo/repn/tests/test_linear_wrt.py index 6853c0df7f6..a9a31ce8232 100644 --- a/pyomo/repn/tests/test_linear_wrt.py +++ b/pyomo/repn/tests/test_linear_wrt.py @@ -95,6 +95,71 @@ def test_sum_with_mult_0(self): self.assertEqual(repn.multiplier, 1) assertExpressionsEqual(self, repn.to_expression(visitor), m.x + m.y) + def test_sum_nonlinear_to_linear(self): + m = self.make_model() + e = m.y * m.x**2 + m.y * m.x + 3 + + cfg = VisitorConfig() + visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + + repn = visitor.walk_expression(e) + assertExpressionsEqual( + self, + repn.nonlinear, + m.y * m.x ** 2 + ) + self.assertEqual(len(repn.linear), 1) + self.assertIn(id(m.x), repn.linear) + self.assertIs(repn.linear[id(m.x)], m.y) + self.assertEqual(repn.constant, 3) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.to_expression(visitor), m.y * m.x ** 2 + + m.y * m.x + 3) + + def test_sum_nonlinear_to_nonlinear(self): + m = self.make_model() + e = m.x ** 3 + 3 + m.x**2 + + cfg = VisitorConfig() + visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + + repn = visitor.walk_expression(e) + assertExpressionsEqual( + self, + repn.nonlinear, + m.x ** 3 + m.x ** 2 + ) + self.assertEqual(repn.constant, 3) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.to_expression(visitor), m.x ** 3 + + m.x ** 2 + 3) + + def test_sum_to_linear_expr(self): + m = self.make_model() + e = m.x + m.y * (m.x + 5) + + cfg = VisitorConfig() + visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + + repn = visitor.walk_expression(e) + self.assertEqual(len(repn.linear), 1) + self.assertIn(id(m.x), repn.linear) + assertExpressionsEqual( + self, + repn.linear[id(m.x)], + 1 + m.y + ) + assertExpressionsEqual( + self, + repn.constant, + m.y * 5 + ) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual( + self, + repn.to_expression(visitor), (1 + m.y) * m.x + m.y * 5 + ) + def test_bilinear_term(self): m = self.make_model() e = m.x * m.y From f4e989fc390cd08ec068dcdcb2b1826683bbb88d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Apr 2024 14:37:24 -0600 Subject: [PATCH 1264/3044] Add fileutils patch so find_library returns absolute path on Linux --- pyomo/common/fileutils.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pyomo/common/fileutils.py b/pyomo/common/fileutils.py index 2cade36154d..80fd47a6377 100644 --- a/pyomo/common/fileutils.py +++ b/pyomo/common/fileutils.py @@ -38,6 +38,7 @@ import os import platform import importlib.util +import subprocess import sys from . import envvar @@ -375,9 +376,25 @@ def find_library(libname, cwd=True, include_PATH=True, pathlist=None): if libname_base.startswith('lib') and _system() != 'windows': libname_base = libname_base[3:] if ext.lower().startswith(('.so', '.dll', '.dylib')): - return ctypes.util.find_library(libname_base) + lib = ctypes.util.find_library(libname_base) else: - return ctypes.util.find_library(libname) + lib = ctypes.util.find_library(libname) + if lib and os.path.sep not in lib: + # work around https://github.com/python/cpython/issues/65241, + # where python does not return the absolute path on *nix + try: + libname = lib + ' ' + with subprocess.Popen(['/sbin/ldconfig', '-p'], + stdin=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdout=subprocess.PIPE, + env={'LC_ALL': 'C', 'LANG': 'C'}) as p: + for line in os.fsdecode(p.stdout.read()).splitlines(): + if line.lstrip().startswith(libname): + return os.path.realpath(line.split()[-1]) + except: + pass + return lib def find_executable(exename, cwd=True, include_PATH=True, pathlist=None): From ebb4d0768f30dbfaf09d56db55f7973ce6bb554a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Apr 2024 14:38:21 -0600 Subject: [PATCH 1265/3044] bugfix: add missing import --- pyomo/contrib/simplification/build.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 2c7b1830ff6..79bc9970241 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -17,6 +17,7 @@ from distutils.dist import Distribution from pybind11.setup_helpers import Pybind11Extension, build_ext +from pyomo.common.cmake_builder import handleReadonly from pyomo.common.envvar import PYOMO_CONFIG_DIR from pyomo.common.fileutils import find_library, this_file_dir From 16d49e13605cf1c8c4a3ba1a4079b5d751d55791 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Apr 2024 14:38:54 -0600 Subject: [PATCH 1266/3044] NFC: clarify ginac builder exception message --- pyomo/contrib/simplification/build.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 79bc9970241..508acb2d5a1 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -34,7 +34,9 @@ def build_ginac_interface(args=None): ginac_lib = find_library('ginac') if ginac_lib is None: raise RuntimeError( - 'could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable' + 'could not find the GiNaC library; please make sure either to install ' + 'the library and development headers system-wide, or include the ' + 'path tt the library in the LD_LIBRARY_PATH environment variable' ) ginac_lib_dir = os.path.dirname(ginac_lib) ginac_build_dir = os.path.dirname(ginac_lib_dir) @@ -45,7 +47,9 @@ def build_ginac_interface(args=None): cln_lib = find_library('cln') if cln_lib is None: raise RuntimeError( - 'could not find CLN library; please make sure it is in the LD_LIBRARY_PATH environment variable' + 'could not find the CLN library; please make sure either to install ' + 'the library and development headers system-wide, or include the ' + 'path tt the library in the LD_LIBRARY_PATH environment variable' ) cln_lib_dir = os.path.dirname(cln_lib) cln_build_dir = os.path.dirname(cln_lib_dir) From bd3299d4e50ac307947a055a2723ea85bdc6c534 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Apr 2024 14:40:15 -0600 Subject: [PATCH 1267/3044] Register the GiNaC interface builder with the ExtensionBuilder --- pyomo/contrib/simplification/build.py | 8 ++++++++ pyomo/contrib/simplification/plugins.py | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 pyomo/contrib/simplification/plugins.py diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 508acb2d5a1..4952ac6dade 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -109,5 +109,13 @@ def run(self): dist.run_command('build_ext') +class GiNaCInterfaceBuilder(object): + def __call__(self, parallel): + return build_ginac_interface() + + def skip(self): + return not find_library('ginac') + + if __name__ == '__main__': build_ginac_interface(sys.argv[1:]) diff --git a/pyomo/contrib/simplification/plugins.py b/pyomo/contrib/simplification/plugins.py new file mode 100644 index 00000000000..6b08f7be4d7 --- /dev/null +++ b/pyomo/contrib/simplification/plugins.py @@ -0,0 +1,17 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.extensions import ExtensionBuilderFactory +from .build import GiNaCInterfaceBuilder + + +def load(): + ExtensionBuilderFactory.register('ginac')(GiNaCInterfaceBuilder) From 29b207246e4ca83f26c96fec1989b6cea24b79e9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Apr 2024 14:41:22 -0600 Subject: [PATCH 1268/3044] Rework GiNaC interface builder (in development - testing several things) - attempt to install from OS package repo - attempt local build ginac, add the built library to the download cache --- .github/workflows/test_branches.yml | 56 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 3bfb902b9e0..a23a603430c 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -157,24 +157,6 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - - name: install GiNaC - if: matrix.other == '/singletest' - run: | - cd .. - curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 - tar -xvf cln-1.3.7.tar.bz2 - cd cln-1.3.7 - ./configure - make -j 2 - sudo make install - cd .. - curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 - tar -xvf ginac-1.8.7.tar.bz2 - cd ginac-1.8.7 - ./configure - make -j 2 - sudo make install - - name: TPL package download cache uses: actions/cache@v4 if: ${{ ! matrix.slim }} @@ -206,7 +188,7 @@ jobs: # Notes: # - install glpk # - pyodbc needs: gcc pkg-config unixodbc freetds - for pkg in bash pkg-config unixodbc freetds glpk; do + for pkg in bash pkg-config unixodbc freetds glpk ginac; do brew list $pkg || brew install $pkg done @@ -218,7 +200,7 @@ jobs: # - install glpk # - ipopt needs: libopenblas-dev gfortran liblapack-dev sudo apt-get -o Dir::Cache=${GITHUB_WORKSPACE}/cache/os \ - install libopenblas-dev gfortran liblapack-dev glpk-utils + install libopenblas-dev gfortran liblapack-dev glpk-utils libginac-dev sudo chmod -R 777 ${GITHUB_WORKSPACE}/cache/os - name: Update Windows @@ -581,6 +563,32 @@ jobs: echo "$GJH_DIR" ls -l $GJH_DIR + - name: Install GiNaC + if: ${{ ! matrix.slim }} + run: | + if test ! -e "${DOWNLOAD_DIR}/ginac.tar.gz"; then + mkdir -p "${GITHUB_WORKSPACE}/cache/build/ginac" + cd "${GITHUB_WORKSPACE}/cache/build/ginac" + curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 + tar -xvf cln-1.3.7.tar.bz2 + cd cln-1.3.7 + ./configure --prefix "$TPL_DIR/ginac" --disable-static + make -j 4 + make install + cd "${GITHUB_WORKSPACE}/cache/build/ginac" + curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 + tar -xvf ginac-1.8.7.tar.bz2 + cd ginac-1.8.7 + ./configure --prefix "$TPL_DIR/ginac" --disable-static + make -j 4 + make install + cd "$TPL_DIR" + tar -czf "${DOWNLOAD_DIR}/ginac.tar.gz" ginac + else + cd "$TPL_DIR" + tar -xzf "${DOWNLOAD_DIR}/ginac.tar.gz" + fi + - name: Install Pyomo run: | echo "" @@ -635,14 +643,6 @@ jobs: echo "" pyomo build-extensions --parallel 2 - - name: Install GiNaC Interface - if: matrix.other == '/singletest' - run: | - export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH - echo "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV - cd pyomo/contrib/simplification/ - $PYTHON_EXE build.py --inplace - - name: Report pyomo plugin information run: | echo "$PATH" From 1b1f944dbd00ca0e0b27d2fd2b70ee681b8e44ae Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 29 Apr 2024 16:58:04 -0600 Subject: [PATCH 1269/3044] bugfix --- pyomo/contrib/simplification/simplify.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 5e251ca326a..27da5f5ca34 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -11,7 +11,7 @@ from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression from pyomo.core.expr.numeric_expr import NumericExpression -from pyomo.core.expr.numvalue import is_fixed, value +from pyomo.core.expr.numvalue import value, is_constant import logging import warnings @@ -28,15 +28,19 @@ def simplify_with_sympy(expr: NumericExpression): + if is_constant(expr): + return value(expr) om, se = sympyify_expression(expr) se = se.simplify() new_expr = sympy2pyomo_expression(se, om) - if is_fixed(new_expr): + if is_constant(new_expr): new_expr = value(new_expr) return new_expr def simplify_with_ginac(expr: NumericExpression, ginac_interface): + if is_constant(expr): + return value(expr) gi = ginac_interface ginac_expr = gi.to_ginac(expr) ginac_expr = ginac_expr.normal() From 74052eebd65a15cf03f36ade73846ec9fcf048e5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Apr 2024 21:58:32 -0600 Subject: [PATCH 1270/3044] Disable local build of GiNaC --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 5410c58495f..d1b6b96807b 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -565,7 +565,7 @@ jobs: ls -l $GJH_DIR - name: Install GiNaC - if: ${{ ! matrix.slim }} + if: ${{ 0 && ! matrix.slim }} run: | if test ! -e "${DOWNLOAD_DIR}/ginac.tar.gz"; then mkdir -p "${GITHUB_WORKSPACE}/cache/build/ginac" From 6bb0f7f375e7a10d4daa05841c4dd5544eff99ff Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Apr 2024 21:59:34 -0600 Subject: [PATCH 1271/3044] NFC: apply black --- pyomo/common/fileutils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyomo/common/fileutils.py b/pyomo/common/fileutils.py index 80fd47a6377..7b6520327a0 100644 --- a/pyomo/common/fileutils.py +++ b/pyomo/common/fileutils.py @@ -384,11 +384,13 @@ def find_library(libname, cwd=True, include_PATH=True, pathlist=None): # where python does not return the absolute path on *nix try: libname = lib + ' ' - with subprocess.Popen(['/sbin/ldconfig', '-p'], - stdin=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - stdout=subprocess.PIPE, - env={'LC_ALL': 'C', 'LANG': 'C'}) as p: + with subprocess.Popen( + ['/sbin/ldconfig', '-p'], + stdin=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdout=subprocess.PIPE, + env={'LC_ALL': 'C', 'LANG': 'C'}, + ) as p: for line in os.fsdecode(p.stdout.read()).splitlines(): if line.lstrip().startswith(libname): return os.path.realpath(line.split()[-1]) From 39643b336ef3149fe7f57007ffaf31b684b6151d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:05:00 -0600 Subject: [PATCH 1272/3044] remove repeated code --- pyomo/contrib/appsi/build.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pyomo/contrib/appsi/build.py b/pyomo/contrib/appsi/build.py index b3d78467f01..38f8cb713ca 100644 --- a/pyomo/contrib/appsi/build.py +++ b/pyomo/contrib/appsi/build.py @@ -16,15 +16,6 @@ import tempfile -def handleReadonly(function, path, excinfo): - excvalue = excinfo[1] - if excvalue.errno == errno.EACCES: - os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777 - function(path) - else: - raise - - def get_appsi_extension(in_setup=False, appsi_root=None): from pybind11.setup_helpers import Pybind11Extension @@ -66,6 +57,7 @@ def build_appsi(args=[]): from setuptools import Distribution from pybind11.setup_helpers import build_ext import pybind11.setup_helpers + from pyomo.common.cmake_builder import handleReadonly from pyomo.common.envvar import PYOMO_CONFIG_DIR from pyomo.common.fileutils import this_file_dir From 89ace9bed0c1a0366ac14dca0d627df0a89b1f01 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:18:31 -0600 Subject: [PATCH 1273/3044] Add support for download tar archives to theFileDownloader --- pyomo/common/download.py | 48 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/pyomo/common/download.py b/pyomo/common/download.py index 5332287cfc7..95713e9ef76 100644 --- a/pyomo/common/download.py +++ b/pyomo/common/download.py @@ -29,6 +29,7 @@ urllib_error = attempt_import('urllib.error')[0] ssl = attempt_import('ssl')[0] zipfile = attempt_import('zipfile')[0] +tarfile = attempt_import('tarfile')[0] gzip = attempt_import('gzip')[0] distro, distro_available = attempt_import('distro') @@ -371,7 +372,7 @@ def get_zip_archive(self, url, dirOffset=0): # Simple sanity checks for info in zip_file.infolist(): f = info.filename - if f[0] in '\\/' or '..' in f: + if f[0] in '\\/' or '..' in f or os.path.isabs(f): logger.error( "malformed (potentially insecure) filename (%s) " "found in zip archive. Skipping file." % (f,) @@ -387,6 +388,51 @@ def get_zip_archive(self, url, dirOffset=0): info.filename = target[-1] + '/' if f[-1] == '/' else target[-1] zip_file.extract(f, os.path.join(self._fname, *tuple(target[dirOffset:-1]))) + def get_tar_archive(self, url, dirOffset=0): + if self._fname is None: + raise DeveloperError( + "target file name has not been initialized " + "with set_destination_filename" + ) + if os.path.exists(self._fname) and not os.path.isdir(self._fname): + raise RuntimeError( + "Target directory (%s) exists, but is not a directory" % (self._fname,) + ) + tar_file = tarfile.open(fileobj=io.BytesIO(self.retrieve_url(url))) + dest = os.path.realpath(self._fname) + + def filter_fcn(info): + # this mocks up the `tarfile` filter introduced in Python + # 3.12 and backported to later releases of Python (e.g., + # 3.8.17, 3.9.17, 3.10.12, and 3.11.4) + f = info.name + if os.path.isabs(f) or '..' in f or f.startswith(('/', os.sep)): + logger.error( + "malformed (potentially insecure) filename (%s) " + "found in tar archive. Skipping file." % (f,) + ) + return False + target = os.path.realpath(os.path.join(dest, f)) + if os.path.commonpath([target, dest]) != dest: + logger.error( + "malformed (potentially insecure) filename (%s) " + "found in zip archive. Skipping file." % (f,) + ) + return False + target = self._splitpath(f) + if len(target) <= dirOffset: + if not info.isdir(): + logger.warning( + "Skipping file (%s) in zip archive due to dirOffset" % (f,) + ) + return False + info.name = '/'.join(target[dirOffset:]) + # Strip high bits & group/other write bits + info.mode &= 0o755 + return True + + tar_file.extractall(dest, filter(filter_fcn, tar_file.getmembers())) + def get_gzipped_binary_file(self, url): if self._fname is None: raise DeveloperError( From bb274ff9d30960f72ffbbf001850d64a14caa6f6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:20:08 -0600 Subject: [PATCH 1274/3044] Switch GiNaC interface builder to use TempfileManager --- pyomo/contrib/simplification/build.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 4952ac6dade..d30f582bcea 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -71,13 +71,13 @@ def build_ginac_interface(args=None): class ginacBuildExt(build_ext): def run(self): basedir = os.path.abspath(os.path.curdir) - if self.inplace: - tmpdir = this_file_dir() - else: - tmpdir = os.path.abspath(tempfile.mkdtemp()) - print("Building in '%s'" % tmpdir) - os.chdir(tmpdir) - try: + with TempfileManager.new_context() as tempfile: + if self.inplace: + tmpdir = this_file_dir() + else: + tmpdir = os.path.abspath(tempfile.mkdtemp()) + print("Building in '%s'" % tmpdir) + os.chdir(tmpdir) super(ginacBuildExt, self).run() if not self.inplace: library = glob.glob("build/*/ginac_interface.*")[0] @@ -91,10 +91,6 @@ def run(self): if not os.path.exists(target): os.makedirs(target) shutil.copy(library, target) - finally: - os.chdir(basedir) - if not self.inplace: - shutil.rmtree(tmpdir, onerror=handleReadonly) package_config = { 'name': 'ginac_interface', From adaefbca72f94548b33328b7a28c2e03a4012a9d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:20:39 -0600 Subject: [PATCH 1275/3044] Add function for downloading and installing GiNaC and CLN --- pyomo/contrib/simplification/build.py | 83 ++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index d30f582bcea..3ea7748cdbf 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -10,19 +10,71 @@ # ___________________________________________________________________________ import glob +import logging import os import shutil import sys -import tempfile -from distutils.dist import Distribution +import subprocess -from pybind11.setup_helpers import Pybind11Extension, build_ext -from pyomo.common.cmake_builder import handleReadonly +from pyomo.common.download import FileDownloader from pyomo.common.envvar import PYOMO_CONFIG_DIR from pyomo.common.fileutils import find_library, this_file_dir +from pyomo.common.tempfiles import TempfileManager -def build_ginac_interface(args=None): +logger = logging.getLogger(__name__) + + +def build_ginac_library(parallel=None, argv=None): + print("\n**** Building GiNaC library ****") + + configure_cmd = ['configure', '--prefix=' + PYOMO_CONFIG_DIR, '--disable-static'] + make_cmd = ['make'] + if parallel: + make_cmd.append(f'-j{parallel}') + install_cmd = ['make', 'install'] + + with TempfileManager.new_context() as tempfile: + tmpdir = tempfile.mkdtemp() + + downloader = FileDownloader() + if argv: + downloader.parse_args(argv) + + url = 'https://www.ginac.de/CLN/cln-1.3.7.tar.bz2' + cln_dir = os.path.join(tmpdir, 'cln') + downloader.set_destination_filename(cln_dir) + logger.info( + "Fetching CLN from %s and installing it to %s" + % (url, downloader.destination()) + ) + downloader.get_tar_archive(url, dirOffset=1) + assert subprocess.run(configure_cmd, cwd=cln_dir).returncode == 0 + logger.info("\nBuilding CLN\n") + assert subprocess.run(make_cmd, cwd=cln_dir).returncode == 0 + assert subprocess.run(install_cmd, cwd=cln_dir).returncode == 0 + + url = 'https://www.ginac.de/ginac-1.8.7.tar.bz2' + ginac_dir = os.path.join(tmpdir, 'ginac') + downloader.set_destination_filename(ginac_dir) + logger.info( + "Fetching GiNaC from %s and installing it to %s" + % (url, downloader.destination()) + ) + downloader.get_tar_archive(url, dirOffset=1) + assert subprocess.run(configure_cmd, cwd=ginac_dir).returncode == 0 + logger.info("\nBuilding GiNaC\n") + assert subprocess.run(make_cmd, cwd=ginac_dir).returncode == 0 + assert subprocess.run(install_cmd, cwd=ginac_dir).returncode == 0 + + +def build_ginac_interface(parallel=None, args=None): + from distutils.dist import Distribution + from pybind11.setup_helpers import Pybind11Extension, build_ext + from pyomo.common.cmake_builder import handleReadonly + + print("\n**** Building GiNaC interface ****") + if args is None: args = list() dname = this_file_dir() @@ -107,11 +159,28 @@ def run(self): class GiNaCInterfaceBuilder(object): def __call__(self, parallel): - return build_ginac_interface() + return build_ginac_interface(parallel) def skip(self): return not find_library('ginac') if __name__ == '__main__': - build_ginac_interface(sys.argv[1:]) + logging.getLogger('pyomo').setLevel(logging.DEBUG) + parallel = None + for i, arg in enumerate(sys.argv): + if arg == '-j': + parallel = int(sys.argv.pop(i + 1)) + sys.argv.pop(i) + break + if arg.startswith('-j'): + if '=' in arg: + parallel = int(arg.split('=')[1]) + else: + parallel = int(arg[2:]) + sys.argv.pop(i) + break + if '--build-deps' in sys.argv: + sys.argv.remove('--build-deps') + build_ginac_library(parallel, []) + build_ginac_interface(parallel, sys.argv[1:]) From c7a8f8e243c2a51bf5e74803b1fc118fd4060816 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:21:10 -0600 Subject: [PATCH 1276/3044] Hook GiNaC builder into pyomo command --- pyomo/environ/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/environ/__init__.py b/pyomo/environ/__init__.py index c1ceb8cb890..07b3dfad680 100644 --- a/pyomo/environ/__init__.py +++ b/pyomo/environ/__init__.py @@ -50,6 +50,7 @@ def _do_import(pkg_name): 'pyomo.contrib.multistart', 'pyomo.contrib.preprocessing', 'pyomo.contrib.pynumero', + 'pyomo.contrib.simplification', 'pyomo.contrib.solver', 'pyomo.contrib.trustregion', ] From a29bb3ed8149dc3b1221ce858cce5640716e5c14 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:23:53 -0600 Subject: [PATCH 1277/3044] Remove simplification test marker --- .github/workflows/test_branches.yml | 5 ----- pyomo/contrib/simplification/tests/test_simplification.py | 1 - setup.cfg | 1 - 3 files changed, 7 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index d1b6b96807b..558d6dc2591 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -651,11 +651,6 @@ jobs: pyomo help --transformations || exit 1 pyomo help --writers || exit 1 - - name: Run Simplification Tests - if: matrix.other == '/singletest' - run: | - pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" - - name: Run Pyomo tests if: matrix.mpi == 0 run: | diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 1a5ae1e0036..be61631e9f3 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -106,7 +106,6 @@ class TestSimplificationSympy(TestCase, SimplificationMixin): @unittest.skipIf(not ginac_available, 'GiNaC is not available') -@unittest.pytest.mark.simplification class TestSimplificationGiNaC(TestCase, SimplificationMixin): def test_param(self): m = pe.ConcreteModel() diff --git a/setup.cfg b/setup.cfg index 855717490b3..b606138f38c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,4 +22,3 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests - simplification: tests for expression simplification that have expensive (to install) dependencies From c475fe791ff6c60aa75ae8eb87a5cabb8d8786ab Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:33:38 -0600 Subject: [PATCH 1278/3044] Switching output to sys.stdout, adding debugging --- pyomo/contrib/simplification/build.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 3ea7748cdbf..5e613cf873f 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -26,7 +26,7 @@ def build_ginac_library(parallel=None, argv=None): - print("\n**** Building GiNaC library ****") + sys.stdout.write("\n**** Building GiNaC library ****") configure_cmd = ['configure', '--prefix=' + PYOMO_CONFIG_DIR, '--disable-static'] make_cmd = ['make'] @@ -73,7 +73,7 @@ def build_ginac_interface(parallel=None, args=None): from pybind11.setup_helpers import Pybind11Extension, build_ext from pyomo.common.cmake_builder import handleReadonly - print("\n**** Building GiNaC interface ****") + sys.stdout.write("\n**** Building GiNaC interface ****") if args is None: args = list() @@ -90,6 +90,7 @@ def build_ginac_interface(parallel=None, args=None): 'the library and development headers system-wide, or include the ' 'path tt the library in the LD_LIBRARY_PATH environment variable' ) + print("Found GiNaC library:", ginac_lib) ginac_lib_dir = os.path.dirname(ginac_lib) ginac_build_dir = os.path.dirname(ginac_lib_dir) ginac_include_dir = os.path.join(ginac_build_dir, 'include') @@ -103,6 +104,7 @@ def build_ginac_interface(parallel=None, args=None): 'the library and development headers system-wide, or include the ' 'path tt the library in the LD_LIBRARY_PATH environment variable' ) + print("Found CLN library:", cln_lib) cln_lib_dir = os.path.dirname(cln_lib) cln_build_dir = os.path.dirname(cln_lib_dir) cln_include_dir = os.path.join(cln_build_dir, 'include') @@ -128,7 +130,7 @@ def run(self): tmpdir = this_file_dir() else: tmpdir = os.path.abspath(tempfile.mkdtemp()) - print("Building in '%s'" % tmpdir) + sys.stdout.write("Building in '%s'" % tmpdir) os.chdir(tmpdir) super(ginacBuildExt, self).run() if not self.inplace: From 7053690e90b0a693ec8901d9b4c73279a85a41aa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 00:45:14 -0600 Subject: [PATCH 1279/3044] Support walking up the directory tree looking for ginac headers (this should better support debian system installations) --- pyomo/contrib/simplification/build.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 5e613cf873f..e6f300ae058 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -68,6 +68,16 @@ def build_ginac_library(parallel=None, argv=None): assert subprocess.run(install_cmd, cwd=ginac_dir).returncode == 0 +def _find_include(libdir, incpaths): + while 1: + basedir = os.path.dirname(libdir) + if not basedir or basedir == libdir: + return None + if os.path.exists(os.path.join(basedir, *incpaths)): + return os.path.join(basedir, *(incpaths[:-1]))): + libdir = basedir + + def build_ginac_interface(parallel=None, args=None): from distutils.dist import Distribution from pybind11.setup_helpers import Pybind11Extension, build_ext @@ -90,11 +100,9 @@ def build_ginac_interface(parallel=None, args=None): 'the library and development headers system-wide, or include the ' 'path tt the library in the LD_LIBRARY_PATH environment variable' ) - print("Found GiNaC library:", ginac_lib) ginac_lib_dir = os.path.dirname(ginac_lib) - ginac_build_dir = os.path.dirname(ginac_lib_dir) - ginac_include_dir = os.path.join(ginac_build_dir, 'include') - if not os.path.exists(os.path.join(ginac_include_dir, 'ginac', 'ginac.h')): + ginac_include_dir = _find_include(ginac_lib_dir, ('ginac', 'ginac.h')) + if not ginac_include_dir: raise RuntimeError('could not find GiNaC include directory') cln_lib = find_library('cln') @@ -104,11 +112,9 @@ def build_ginac_interface(parallel=None, args=None): 'the library and development headers system-wide, or include the ' 'path tt the library in the LD_LIBRARY_PATH environment variable' ) - print("Found CLN library:", cln_lib) cln_lib_dir = os.path.dirname(cln_lib) - cln_build_dir = os.path.dirname(cln_lib_dir) - cln_include_dir = os.path.join(cln_build_dir, 'include') - if not os.path.exists(os.path.join(cln_include_dir, 'cln', 'cln.h')): + cln_include_dir = _find_include(cln_lib_dir, ('cln', 'cln.h')) + if cln_include_dir: raise RuntimeError('could not find CLN include directory') extra_args = ['-std=c++11'] From 7126fb7a0258524d4a1233fd16a5931ef064fe08 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 10:57:09 +0200 Subject: [PATCH 1280/3044] Modified two tests --- .../appsi/solvers/tests/test_persistent_solvers.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index d6df1710a03..d38563844ff 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1026,7 +1026,7 @@ def test_time_limit( self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) - if not opt.available(): + if not opt.available() or opt_class == MAiNGO: raise unittest.SkipTest from sys import platform @@ -1210,20 +1210,23 @@ def test_fixed_binaries( m.obj = pe.Objective(expr=m.y) m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) + + if type(opt) is MAiNGO: + opt.config.mip_gap = 1e-6 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0, 6) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(res.best_feasible_objective, 1) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0, 6) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(res.best_feasible_objective, 1) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( From 673fd372e55c54496cd01446e248ba9ab3d96024 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 12:33:04 +0200 Subject: [PATCH 1281/3044] Modify test_persistent_solvers.py --- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index d38563844ff..3db2ae3cba1 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1211,7 +1211,7 @@ def test_fixed_binaries( m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) - if type(opt) is MAiNGO: + if opt_class == MAiNGO: opt.config.mip_gap = 1e-6 res = opt.solve(m) self.assertAlmostEqual(res.best_feasible_objective, 0) From 0127d29aeadf2034fde7b95b962d89c6c58488a6 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 13:21:54 +0200 Subject: [PATCH 1282/3044] Set default mipgap higher --- pyomo/contrib/appsi/solvers/maingo.py | 4 ++-- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 944673be53d..e52130061f7 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -110,7 +110,7 @@ def __init__( 'epsilonA', ConfigValue( domain=NonNegativeFloat, - default=1e-4, + default=1e-5, description="Absolute optimality tolerance", ), ) @@ -118,7 +118,7 @@ def __init__( 'epsilonR', ConfigValue( domain=NonNegativeFloat, - default=1e-4, + default=1e-5, description="Relative optimality tolerance", ), ) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 3db2ae3cba1..6ab36ccc981 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1211,8 +1211,6 @@ def test_fixed_binaries( m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) - if opt_class == MAiNGO: - opt.config.mip_gap = 1e-6 res = opt.solve(m) self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.fix(1) From 389740c54cdae96ef27375fc2b03e59c9002d036 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 13:42:54 +0200 Subject: [PATCH 1283/3044] Modify test_fixed_binaries --- .../appsi/solvers/tests/test_persistent_solvers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 6ab36ccc981..7ff193b38e4 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1212,19 +1212,19 @@ def test_fixed_binaries( m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0, 6) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0, 6) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( From be6922453be5ef7da898235b122d251d1222ad04 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 07:31:44 -0600 Subject: [PATCH 1284/3044] Fix several typos / include search logic --- pyomo/contrib/simplification/build.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index e6f300ae058..fb571c273eb 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -69,12 +69,13 @@ def build_ginac_library(parallel=None, argv=None): def _find_include(libdir, incpaths): + rel_path = ('include',) + incpaths while 1: basedir = os.path.dirname(libdir) if not basedir or basedir == libdir: return None - if os.path.exists(os.path.join(basedir, *incpaths)): - return os.path.join(basedir, *(incpaths[:-1]))): + if os.path.exists(os.path.join(basedir, *rel_path)): + return os.path.join(basedir, *(rel_path[:-len(incpaths)])) libdir = basedir @@ -86,15 +87,13 @@ def build_ginac_interface(parallel=None, args=None): sys.stdout.write("\n**** Building GiNaC interface ****") if args is None: - args = list() + args = [] dname = this_file_dir() _sources = ['ginac_interface.cpp'] - sources = list() - for fname in _sources: - sources.append(os.path.join(dname, fname)) + sources = [os.path.join(dname, fname) for fname in _sources] ginac_lib = find_library('ginac') - if ginac_lib is None: + if not ginac_lib: raise RuntimeError( 'could not find the GiNaC library; please make sure either to install ' 'the library and development headers system-wide, or include the ' @@ -106,7 +105,7 @@ def build_ginac_interface(parallel=None, args=None): raise RuntimeError('could not find GiNaC include directory') cln_lib = find_library('cln') - if cln_lib is None: + if not cln_lib: raise RuntimeError( 'could not find the CLN library; please make sure either to install ' 'the library and development headers system-wide, or include the ' @@ -114,7 +113,7 @@ def build_ginac_interface(parallel=None, args=None): ) cln_lib_dir = os.path.dirname(cln_lib) cln_include_dir = _find_include(cln_lib_dir, ('cln', 'cln.h')) - if cln_include_dir: + if not cln_include_dir: raise RuntimeError('could not find CLN include directory') extra_args = ['-std=c++11'] From 547b3015eed3157b3a4023a5e31ca53d1b598e57 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 15:32:58 +0200 Subject: [PATCH 1285/3044] Set epsilonA for one test --- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 7ff193b38e4..7fa2a62a8be 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1210,7 +1210,8 @@ def test_fixed_binaries( m.obj = pe.Objective(expr=m.y) m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) - + if type(opt) is MAiNGO: + opt.maingo_options["epsilonA"] = 1e-6 res = opt.solve(m) self.assertAlmostEqual(res.best_feasible_objective, 0, 6) m.x.fix(1) From 4ac04a6f3e8c4511f922d00c5eb7430bb8ee9b2a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 07:33:23 -0600 Subject: [PATCH 1286/3044] NFC: apply black --- pyomo/contrib/simplification/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index fb571c273eb..a4094f993fa 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -75,7 +75,7 @@ def _find_include(libdir, incpaths): if not basedir or basedir == libdir: return None if os.path.exists(os.path.join(basedir, *rel_path)): - return os.path.join(basedir, *(rel_path[:-len(incpaths)])) + return os.path.join(basedir, *(rel_path[: -len(incpaths)])) libdir = basedir From abe5f8bb136bd1faadbcc4340deabfa42061d566 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 30 Apr 2024 07:47:07 -0600 Subject: [PATCH 1287/3044] Resync GHA workflows, remove ginac build code --- .github/workflows/test_branches.yml | 31 +++------------------ .github/workflows/test_pr_and_main.yml | 37 +++----------------------- 2 files changed, 7 insertions(+), 61 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 558d6dc2591..de40066b50f 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -201,7 +201,8 @@ jobs: # - install glpk # - ipopt needs: libopenblas-dev gfortran liblapack-dev sudo apt-get -o Dir::Cache=${GITHUB_WORKSPACE}/cache/os \ - install libopenblas-dev gfortran liblapack-dev glpk-utils libginac-dev + install libopenblas-dev gfortran liblapack-dev glpk-utils \ + libginac-dev sudo chmod -R 777 ${GITHUB_WORKSPACE}/cache/os - name: Update Windows @@ -346,7 +347,7 @@ jobs: echo "*** Install Pyomo dependencies ***" # Note: this will fail the build if any installation fails (or # possibly if it outputs messages to stderr) - conda install --update-deps -y $CONDA_DEPENDENCIES + conda install --update-deps -q -y $CONDA_DEPENDENCIES if test -z "${{matrix.slim}}"; then PYVER=$(echo "py${{matrix.python}}" | sed 's/\.//g') echo "Installing for $PYVER" @@ -564,32 +565,6 @@ jobs: echo "$GJH_DIR" ls -l $GJH_DIR - - name: Install GiNaC - if: ${{ 0 && ! matrix.slim }} - run: | - if test ! -e "${DOWNLOAD_DIR}/ginac.tar.gz"; then - mkdir -p "${GITHUB_WORKSPACE}/cache/build/ginac" - cd "${GITHUB_WORKSPACE}/cache/build/ginac" - curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 - tar -xvf cln-1.3.7.tar.bz2 - cd cln-1.3.7 - ./configure --prefix "$TPL_DIR/ginac" --disable-static - make -j 4 - make install - cd "${GITHUB_WORKSPACE}/cache/build/ginac" - curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 - tar -xvf ginac-1.8.7.tar.bz2 - cd ginac-1.8.7 - ./configure --prefix "$TPL_DIR/ginac" --disable-static - make -j 4 - make install - cd "$TPL_DIR" - tar -czf "${DOWNLOAD_DIR}/ginac.tar.gz" ginac - else - cd "$TPL_DIR" - tar -xzf "${DOWNLOAD_DIR}/ginac.tar.gz" - fi - - name: Install Pyomo run: | echo "" diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 7c84ed14093..cdc42718cba 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -187,24 +187,6 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - - name: install GiNaC - if: matrix.other == '/singletest' - run: | - cd .. - curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 - tar -xvf cln-1.3.7.tar.bz2 - cd cln-1.3.7 - ./configure - make -j 2 - sudo make install - cd .. - curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 - tar -xvf ginac-1.8.7.tar.bz2 - cd ginac-1.8.7 - ./configure - make -j 2 - sudo make install - - name: TPL package download cache uses: actions/cache@v4 if: ${{ ! matrix.slim }} @@ -236,7 +218,7 @@ jobs: # Notes: # - install glpk # - pyodbc needs: gcc pkg-config unixodbc freetds - for pkg in bash pkg-config unixodbc freetds glpk; do + for pkg in bash pkg-config unixodbc freetds glpk ginac; do brew list $pkg || brew install $pkg done @@ -248,7 +230,8 @@ jobs: # - install glpk # - ipopt needs: libopenblas-dev gfortran liblapack-dev sudo apt-get -o Dir::Cache=${GITHUB_WORKSPACE}/cache/os \ - install libopenblas-dev gfortran liblapack-dev glpk-utils + install libopenblas-dev gfortran liblapack-dev glpk-utils \ + libginac-dev sudo chmod -R 777 ${GITHUB_WORKSPACE}/cache/os - name: Update Windows @@ -389,6 +372,7 @@ jobs: CONDA_DEPENDENCIES="$CONDA_DEPENDENCIES $PKG" fi done + echo "" echo "*** Install Pyomo dependencies ***" # Note: this will fail the build if any installation fails (or # possibly if it outputs messages to stderr) @@ -664,14 +648,6 @@ jobs: echo "" pyomo build-extensions --parallel 2 - - name: Install GiNaC Interface - if: matrix.other == '/singletest' - run: | - export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH - echo "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV - cd pyomo/contrib/simplification/ - $PYTHON_EXE build.py --inplace - - name: Report pyomo plugin information run: | echo "$PATH" @@ -679,11 +655,6 @@ jobs: pyomo help --transformations || exit 1 pyomo help --writers || exit 1 - - name: Run Simplification Tests - if: matrix.other == '/singletest' - run: | - pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" - - name: Run Pyomo tests if: matrix.mpi == 0 run: | From 0c14380b675f55a49f238b675055a1557e191ce5 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 16:02:17 +0200 Subject: [PATCH 1288/3044] Modify fixed_binary-test --- .../appsi/solvers/tests/test_persistent_solvers.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 7fa2a62a8be..67088297cf4 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1210,22 +1210,20 @@ def test_fixed_binaries( m.obj = pe.Objective(expr=m.y) m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) - if type(opt) is MAiNGO: - opt.maingo_options["epsilonA"] = 1e-6 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0, 6) + self.assertAlmostEqual(res.best_feasible_objective, 0, 5) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(res.best_feasible_objective, 1, 5) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0, 6) + self.assertAlmostEqual(res.best_feasible_objective, 0, 5) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(res.best_feasible_objective, 1, 5) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( From 539d62dc2fc9dea7afe4c838dac5018921f67171 Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 30 Apr 2024 08:36:34 -0700 Subject: [PATCH 1289/3044] simplified use of suffix update and removed extra prints --- .../simple_reaction_parmest_example.py | 8 +++---- .../reactor_design/bootstrap_example.py | 2 +- .../confidence_region_example.py | 4 ++-- .../reactor_design/datarec_example.py | 24 +++++++++---------- .../reactor_design/leaveNout_example.py | 2 +- .../likelihood_ratio_example.py | 2 +- .../multisensor_data_example.py | 15 ++++++------ .../parameter_estimation_example.py | 2 +- .../examples/reactor_design/reactor_design.py | 8 +++---- .../reactor_design/timeseries_data_example.py | 2 +- .../rooney_biegler/bootstrap_example.py | 2 +- .../likelihood_ratio_example.py | 2 +- .../parameter_estimation_example.py | 2 +- .../examples/rooney_biegler/rooney_biegler.py | 4 ++-- .../rooney_biegler_with_constraint.py | 4 ++-- .../semibatch/parameter_estimation_example.py | 2 +- .../examples/semibatch/scenario_example.py | 2 +- pyomo/contrib/parmest/tests/test_parmest.py | 8 +++---- 18 files changed, 47 insertions(+), 48 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py index e5bfd99c84f..dcfca900f28 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py @@ -89,9 +89,9 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.x1, self.data['x1'])]) - m.experiment_outputs.update([(m.x2, self.data['x2'])]) - m.experiment_outputs.update([(m.y, self.data['y'])]) + m.experiment_outputs.update([(m.x1, self.data['x1']), + (m.x2, self.data['x2']), + (m.y, self.data['y'])]) return m @@ -156,7 +156,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() # ======================================================================= # Parameter estimation without covariance estimate diff --git a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py index f845930ab79..598fef32b60 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/bootstrap_example.py @@ -31,7 +31,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() pest = parmest.Estimator(exp_list, obj_function='SSE') diff --git a/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py b/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py index 8aee6e9d67c..73129baf5cb 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/confidence_region_example.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -31,7 +31,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() pest = parmest.Estimator(exp_list, obj_function='SSE') diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index e05b69aa4cc..db03b268178 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -40,17 +40,17 @@ def label_model(self): # experiment outputs m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.ca, self.data_i['ca'])]) - m.experiment_outputs.update([(m.cb, self.data_i['cb'])]) - m.experiment_outputs.update([(m.cc, self.data_i['cc'])]) - m.experiment_outputs.update([(m.cd, self.data_i['cd'])]) + m.experiment_outputs.update([(m.ca, self.data_i['ca']), + (m.cb, self.data_i['cb']), + (m.cc, self.data_i['cc']), + (m.cd, self.data_i['cd'])]) # experiment standard deviations m.experiment_outputs_std = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs_std.update([(m.ca, self.data_std['ca'])]) - m.experiment_outputs_std.update([(m.cb, self.data_std['cb'])]) - m.experiment_outputs_std.update([(m.cc, self.data_std['cc'])]) - m.experiment_outputs_std.update([(m.cd, self.data_std['cd'])]) + m.experiment_outputs_std.update([(m.ca, self.data_std['ca']), + (m.cb, self.data_std['cb']), + (m.cc, self.data_std['cc']), + (m.cd, self.data_std['cd'])]) # no unknowns (theta names) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) @@ -71,10 +71,10 @@ def label_model(self): # add experiment standard deviations m.experiment_outputs_std = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs_std.update([(m.ca, self.data_std['ca'])]) - m.experiment_outputs_std.update([(m.cb, self.data_std['cb'])]) - m.experiment_outputs_std.update([(m.cc, self.data_std['cc'])]) - m.experiment_outputs_std.update([(m.cd, self.data_std['cd'])]) + m.experiment_outputs_std.update([(m.ca, self.data_std['ca']), + (m.cb, self.data_std['cb']), + (m.cc, self.data_std['cc']), + (m.cd, self.data_std['cd'])]) return m diff --git a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py index c735b191e0c..9560981ca5c 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/leaveNout_example.py @@ -38,7 +38,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() pest = parmest.Estimator(exp_list, obj_function='SSE') diff --git a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py index 45adaa27e7f..c2bff254077 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/likelihood_ratio_example.py @@ -32,7 +32,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() pest = parmest.Estimator(exp_list, obj_function='SSE') diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index 95bcf211207..d0136fa6f92 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -41,12 +41,11 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update( - [(m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']])] - ) - m.experiment_outputs.update([(m.cb, [self.data_i['cb']])]) - m.experiment_outputs.update([(m.cc, [self.data_i['cc1'], self.data_i['cc2']])]) - m.experiment_outputs.update([(m.cd, [self.data_i['cd']])]) + m.experiment_outputs.update([ + (m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']]), + (m.cb, [self.data_i['cb']]), + (m.cc, [self.data_i['cc1'], self.data_i['cc2']]), + (m.cd, [self.data_i['cd']])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( @@ -80,8 +79,8 @@ def SSE_multisensor(model): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) - # print(SSE_multisensor(exp0_model)) + # exp0_model.pprint() + # SSE_multisensor(exp0_model) pest = parmest.Estimator(exp_list, obj_function=SSE_multisensor) obj, theta = pest.theta_est() diff --git a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py index d72b7aa9878..a84a3fde5e7 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py @@ -31,7 +31,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() pest = parmest.Estimator(exp_list, obj_function='SSE') diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index a2025b8a324..7918d8a14cd 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -107,10 +107,10 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.ca, self.data_i['ca'])]) - m.experiment_outputs.update([(m.cb, self.data_i['cb'])]) - m.experiment_outputs.update([(m.cc, self.data_i['cc'])]) - m.experiment_outputs.update([(m.cd, self.data_i['cd'])]) + m.experiment_outputs.update([(m.ca, self.data_i['ca']), + (m.cb, self.data_i['cb']), + (m.cc, self.data_i['cc']), + (m.cd, self.data_i['cd'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( diff --git a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py index 1e457bf1e89..04a64850f40 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py @@ -68,7 +68,7 @@ def SSE_timeseries(model): # View one model & SSE # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() # print(SSE_timeseries(exp0_model)) pest = parmest.Estimator(exp_list, obj_function=SSE_timeseries) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py index 04917e1a817..944a01ac95e 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/bootstrap_example.py @@ -39,7 +39,7 @@ def SSE(model): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() # Create an instance of the parmest estimator pest = parmest.Estimator(exp_list, obj_function=SSE) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py index d8b572890ba..54343993286 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/likelihood_ratio_example.py @@ -40,7 +40,7 @@ def SSE(model): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() # Create an instance of the parmest estimator pest = parmest.Estimator(exp_list, obj_function=SSE) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py index 18c4904787b..3c9a93100bb 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/parameter_estimation_example.py @@ -39,7 +39,7 @@ def SSE(model): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() # Create an instance of the parmest estimator pest = parmest.Estimator(exp_list, obj_function=SSE) diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py index a9918cf2268..7b4dc289061 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py @@ -61,8 +61,8 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data['hour'])]) - m.experiment_outputs.update([(m.y, self.data['y'])]) + m.experiment_outputs.update([(m.hour, self.data['hour']), + (m.y, self.data['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py index 259fa45785a..4a2a07a052d 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py @@ -65,8 +65,8 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data['hour'])]) - m.experiment_outputs.update([(m.y, self.data['y'])]) + m.experiment_outputs.update([(m.hour, self.data['hour']), + (m.y, self.data['y'])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( diff --git a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py index 0d5416c714a..7eafdd2b9c3 100644 --- a/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/parameter_estimation_example.py @@ -33,7 +33,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() # Note, the model already includes a 'SecondStageCost' expression # for sum of squared error that will be used in parameter estimation diff --git a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py index b2e9f7bd7ee..697cb9ac7a5 100644 --- a/pyomo/contrib/parmest/examples/semibatch/scenario_example.py +++ b/pyomo/contrib/parmest/examples/semibatch/scenario_example.py @@ -34,7 +34,7 @@ def main(): # View one model # exp0_model = exp_list[0].get_labeled_model() - # print(exp0_model.pprint()) + # exp0_model.pprint() pest = parmest.Estimator(exp_list) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 9c65a31352f..e9cbfcebb7f 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -427,8 +427,8 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data["hour"])]) - m.experiment_outputs.update([(m.y, self.data["y"])]) + m.experiment_outputs.update([(m.hour, self.data["hour"]), + (m.y, self.data["y"])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) @@ -506,8 +506,8 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data["hour"])]) - m.experiment_outputs.update([(m.y, self.data["y"])]) + m.experiment_outputs.update([(m.hour, self.data["hour"]), + (m.y, self.data["y"])]) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) From b7dc0f1ac535f46ba7e1051211e6250bbb2824d6 Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 30 Apr 2024 08:50:23 -0700 Subject: [PATCH 1290/3044] minor update, added print --- .../parmest/examples/reactor_design/multisensor_data_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index d0136fa6f92..f2820edf6a6 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -80,7 +80,7 @@ def SSE_multisensor(model): # View one model # exp0_model = exp_list[0].get_labeled_model() # exp0_model.pprint() - # SSE_multisensor(exp0_model) + # print(SSE_multisensor(exp0_model)) pest = parmest.Estimator(exp_list, obj_function=SSE_multisensor) obj, theta = pest.theta_est() From 032fd3102ec6e933eb51350ed1467d106cc0d986 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 30 Apr 2024 10:07:39 -0600 Subject: [PATCH 1291/3044] Update deprecation version in scenariocreator.py --- pyomo/contrib/parmest/scenariocreator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index f2798ad2e94..2208bde91a0 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -17,7 +17,7 @@ from pyomo.common.deprecation import deprecated from pyomo.common.deprecation import deprecation_warning -DEPRECATION_VERSION = '6.7.0' +DEPRECATION_VERSION = '6.7.2.dev0' import logging From 9c5f8df5266a7adaa271ab1904c1a0c6e3c1dd36 Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 30 Apr 2024 09:12:39 -0700 Subject: [PATCH 1292/3044] reformatted lists --- .../simple_reaction_parmest_example.py | 6 ++-- .../reactor_design/datarec_example.py | 36 ++++++++++++------- .../multisensor_data_example.py | 13 ++++--- .../examples/reactor_design/reactor_design.py | 12 ++++--- .../examples/rooney_biegler/rooney_biegler.py | 5 +-- .../rooney_biegler_with_constraint.py | 5 +-- pyomo/contrib/parmest/tests/test_parmest.py | 10 +++--- 7 files changed, 55 insertions(+), 32 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py index dcfca900f28..5c8a0219946 100644 --- a/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py +++ b/pyomo/contrib/parmest/examples/reaction_kinetics/simple_reaction_parmest_example.py @@ -89,9 +89,9 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.x1, self.data['x1']), - (m.x2, self.data['x2']), - (m.y, self.data['y'])]) + m.experiment_outputs.update( + [(m.x1, self.data['x1']), (m.x2, self.data['x2']), (m.y, self.data['y'])] + ) return m diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index db03b268178..ba41bbfb7b8 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -40,17 +40,25 @@ def label_model(self): # experiment outputs m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.ca, self.data_i['ca']), - (m.cb, self.data_i['cb']), - (m.cc, self.data_i['cc']), - (m.cd, self.data_i['cd'])]) + m.experiment_outputs.update( + [ + (m.ca, self.data_i['ca']), + (m.cb, self.data_i['cb']), + (m.cc, self.data_i['cc']), + (m.cd, self.data_i['cd']) + ] + ) # experiment standard deviations m.experiment_outputs_std = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs_std.update([(m.ca, self.data_std['ca']), - (m.cb, self.data_std['cb']), - (m.cc, self.data_std['cc']), - (m.cd, self.data_std['cd'])]) + m.experiment_outputs_std.update( + [ + (m.ca, self.data_std['ca']), + (m.cb, self.data_std['cb']), + (m.cc, self.data_std['cc']), + (m.cd, self.data_std['cd']) + ] + ) # no unknowns (theta names) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) @@ -71,10 +79,14 @@ def label_model(self): # add experiment standard deviations m.experiment_outputs_std = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs_std.update([(m.ca, self.data_std['ca']), - (m.cb, self.data_std['cb']), - (m.cc, self.data_std['cc']), - (m.cd, self.data_std['cd'])]) + m.experiment_outputs_std.update( + [ + (m.ca, self.data_std['ca']), + (m.cb, self.data_std['cb']), + (m.cc, self.data_std['cc']), + (m.cd, self.data_std['cd']) + ] + ) return m diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index f2820edf6a6..e7e4fb1b04d 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -41,11 +41,14 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([ - (m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']]), - (m.cb, [self.data_i['cb']]), - (m.cc, [self.data_i['cc1'], self.data_i['cc2']]), - (m.cd, [self.data_i['cd']])]) + m.experiment_outputs.update( + [ + (m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']]), + (m.cb, [self.data_i['cb']]), + (m.cc, [self.data_i['cc1'], self.data_i['cc2']]), + (m.cd, [self.data_i['cd']]) + ] + ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index 7918d8a14cd..7f6e46cc723 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -107,10 +107,14 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.ca, self.data_i['ca']), - (m.cb, self.data_i['cb']), - (m.cc, self.data_i['cc']), - (m.cd, self.data_i['cd'])]) + m.experiment_outputs.update( + [ + (m.ca, self.data_i['ca']), + (m.cb, self.data_i['cb']), + (m.cc, self.data_i['cc']), + (m.cd, self.data_i['cd']) + ] + ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py index 7b4dc289061..9625ab32ea3 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler.py @@ -61,8 +61,9 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data['hour']), - (m.y, self.data['y'])]) + m.experiment_outputs.update( + [(m.hour, self.data['hour']), (m.y, self.data['y'])] + ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( diff --git a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py index 4a2a07a052d..dd82b50cf7a 100644 --- a/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py +++ b/pyomo/contrib/parmest/examples/rooney_biegler/rooney_biegler_with_constraint.py @@ -65,8 +65,9 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data['hour']), - (m.y, self.data['y'])]) + m.experiment_outputs.update( + [(m.hour, self.data['hour']), (m.y, self.data['y'])] + ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index e9cbfcebb7f..e9a8e089335 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -427,8 +427,9 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data["hour"]), - (m.y, self.data["y"])]) + m.experiment_outputs.update( + [(m.hour, self.data["hour"]), (m.y, self.data["y"])] + ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) @@ -506,8 +507,9 @@ def label_model(self): m = self.model m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update([(m.hour, self.data["hour"]), - (m.y, self.data["y"])]) + m.experiment_outputs.update( + [(m.hour, self.data["hour"]), (m.y, self.data["y"])] + ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.ComponentUID(k)) for k in [m.theta]) From bf865f4383e93402548db9cd548ebbace383bb80 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 30 Apr 2024 10:14:20 -0600 Subject: [PATCH 1293/3044] Update deprecation version in parmest.py --- pyomo/contrib/parmest/parmest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index aecc9d5ebc2..a1200e2c3a5 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -78,7 +78,7 @@ from pyomo.common.deprecation import deprecated from pyomo.common.deprecation import deprecation_warning -DEPRECATION_VERSION = '6.7.0' +DEPRECATION_VERSION = '6.7.2.dev0' parmest_available = numpy_available & pandas_available & scipy_available From db48a25a987ad0f978e6ef3ebc873eabf33f1a92 Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 30 Apr 2024 09:29:22 -0700 Subject: [PATCH 1294/3044] added missing commas --- .../parmest/examples/reactor_design/datarec_example.py | 6 +++--- .../examples/reactor_design/multisensor_data_example.py | 2 +- .../parmest/examples/reactor_design/reactor_design.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index ba41bbfb7b8..02aa13fceab 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -45,7 +45,7 @@ def label_model(self): (m.ca, self.data_i['ca']), (m.cb, self.data_i['cb']), (m.cc, self.data_i['cc']), - (m.cd, self.data_i['cd']) + (m.cd, self.data_i['cd']), ] ) @@ -56,7 +56,7 @@ def label_model(self): (m.ca, self.data_std['ca']), (m.cb, self.data_std['cb']), (m.cc, self.data_std['cc']), - (m.cd, self.data_std['cd']) + (m.cd, self.data_std['cd']), ] ) @@ -84,7 +84,7 @@ def label_model(self): (m.ca, self.data_std['ca']), (m.cb, self.data_std['cb']), (m.cc, self.data_std['cc']), - (m.cd, self.data_std['cd']) + (m.cd, self.data_std['cd']), ] ) diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index e7e4fb1b04d..48a7bca52ca 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -46,7 +46,7 @@ def label_model(self): (m.ca, [self.data_i['ca1'], self.data_i['ca2'], self.data_i['ca3']]), (m.cb, [self.data_i['cb']]), (m.cc, [self.data_i['cc1'], self.data_i['cc2']]), - (m.cd, [self.data_i['cd']]) + (m.cd, [self.data_i['cd']]), ] ) diff --git a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py index 7f6e46cc723..a396c1ea721 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +++ b/pyomo/contrib/parmest/examples/reactor_design/reactor_design.py @@ -112,7 +112,7 @@ def label_model(self): (m.ca, self.data_i['ca']), (m.cb, self.data_i['cb']), (m.cc, self.data_i['cc']), - (m.cd, self.data_i['cd']) + (m.cd, self.data_i['cd']), ] ) From e68c415176439e9299769e59ac4c25bdbf5f0fad Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 30 Apr 2024 09:31:22 -0700 Subject: [PATCH 1295/3044] removed _treemaker, not used --- pyomo/contrib/parmest/parmest.py | 37 -------------------------------- 1 file changed, 37 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index a1200e2c3a5..c9826a57b1d 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -16,7 +16,6 @@ # TODO: move use_mpisppy to a Pyomo configuration option # Redesign TODOS -# TODO: _treemaker is not used in parmest, the code could be moved to scenario tree if needed # TODO: Create additional built in objective expressions in an Enum class which includes SSE (see SSE function below) # TODO: Clean up the use of theta_names through out the code. The Experiment returns the CUID of each theta and this can be used directly (instead of the name) # TODO: Clean up the use of updated_theta_names, model_theta_names, estimator_theta_names. Not sure if estimator_theta_names is the union or intersect of thetas in each model @@ -239,42 +238,6 @@ def _experiment_instance_creation_callback( return instance -# # ============================================= -# def _treemaker(scenlist): -# """ -# Makes a scenario tree (avoids dependence on daps) - -# Parameters -# ---------- -# scenlist (list of `int`): experiment (i.e. scenario) numbers - -# Returns -# ------- -# a `ConcreteModel` that is the scenario tree -# """ - -# num_scenarios = len(scenlist) -# m = scenario_tree.tree_structure_model.CreateAbstractScenarioTreeModel() -# m = m.create_instance() -# m.Stages.add('Stage1') -# m.Stages.add('Stage2') -# m.Nodes.add('RootNode') -# for i in scenlist: -# m.Nodes.add('LeafNode_Experiment' + str(i)) -# m.Scenarios.add('Experiment' + str(i)) -# m.NodeStage['RootNode'] = 'Stage1' -# m.ConditionalProbability['RootNode'] = 1.0 -# for node in m.Nodes: -# if node != 'RootNode': -# m.NodeStage[node] = 'Stage2' -# m.Children['RootNode'].add(node) -# m.Children[node].clear() -# m.ConditionalProbability[node] = 1.0 / num_scenarios -# m.ScenarioLeafNode[node.replace('LeafNode_', '')] = node - -# return m - - def SSE(model): """ Sum of squared error between `experiment_output` model and data values From 63634c99a913944864aa1450399a05e6c900ab53 Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 30 Apr 2024 09:40:05 -0700 Subject: [PATCH 1296/3044] removed _SecondStageCostExpr class, call objective function directly --- pyomo/contrib/parmest/parmest.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index c9826a57b1d..ac2c7fdb0aa 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -246,18 +246,6 @@ def SSE(model): return expr -class _SecondStageCostExpr(object): - """ - Class to pass objective expression into the Pyomo model - """ - - def __init__(self, ssc_function): - self._ssc_function = ssc_function - - def __call__(self, model): - return self._ssc_function(model) - - class Estimator(object): """ Parameter estimation class @@ -419,10 +407,10 @@ def _create_parmest_model(self, experiment_number): # TODO, this needs to be turned a enum class of options that still support custom functions if self.obj_function == 'SSE': - second_stage_rule = _SecondStageCostExpr(SSE) + second_stage_rule = SSE else: # A custom function uses model.experiment_outputs as data - second_stage_rule = _SecondStageCostExpr(self.obj_function) + second_stage_rule = self.obj_function model.FirstStageCost = pyo.Expression(expr=0) model.SecondStageCost = pyo.Expression(rule=second_stage_rule) From a92b4f58ed95a148aa2a9750018c81761f643794 Mon Sep 17 00:00:00 2001 From: kaklise Date: Tue, 30 Apr 2024 09:53:35 -0700 Subject: [PATCH 1297/3044] changed yhat to y_hat --- .../parmest/examples/reactor_design/datarec_example.py | 4 ++-- .../examples/reactor_design/multisensor_data_example.py | 6 +++--- .../examples/reactor_design/timeseries_data_example.py | 6 +++--- pyomo/contrib/parmest/parmest.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py index 02aa13fceab..be08e727be9 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/datarec_example.py @@ -132,8 +132,8 @@ def main(): # Define sum of squared error objective function for data rec def SSE_with_std(model): expr = sum( - ((y - yhat) / model.experiment_outputs_std[y]) ** 2 - for y, yhat in model.experiment_outputs.items() + ((y - y_hat) / model.experiment_outputs_std[y]) ** 2 + for y, y_hat in model.experiment_outputs.items() ) return expr diff --git a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py index 48a7bca52ca..208981a784a 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/multisensor_data_example.py @@ -74,10 +74,10 @@ def main(): # Define sum of squared error def SSE_multisensor(model): expr = 0 - for y, yhat in model.experiment_outputs.items(): - num_outputs = len(yhat) + for y, y_hat in model.experiment_outputs.items(): + num_outputs = len(y_hat) for i in range(num_outputs): - expr += ((y - yhat[i]) ** 2) * (1 / num_outputs) + expr += ((y - y_hat[i]) ** 2) * (1 / num_outputs) return expr # View one model diff --git a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py index 04a64850f40..4eb191afd6d 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/timeseries_data_example.py @@ -59,10 +59,10 @@ def main(): def SSE_timeseries(model): expr = 0 - for y, yhat in model.experiment_outputs.items(): - num_time_points = len(yhat) + for y, y_hat in model.experiment_outputs.items(): + num_time_points = len(y_hat) for i in range(num_time_points): - expr += ((y - yhat[i]) ** 2) * (1 / num_time_points) + expr += ((y - y_hat[i]) ** 2) * (1 / num_time_points) return expr diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ac2c7fdb0aa..6bc69c78bcd 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -242,7 +242,7 @@ def SSE(model): """ Sum of squared error between `experiment_output` model and data values """ - expr = sum((y - yhat) ** 2 for y, yhat in model.experiment_outputs.items()) + expr = sum((y - y_hat) ** 2 for y, y_hat in model.experiment_outputs.items()) return expr From 097849dbaaadbc1873b8b1dc3a83e24103a55aaf Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 30 Apr 2024 11:45:11 -0600 Subject: [PATCH 1298/3044] Update Estimator constructor in parmest to more robustly support both the new and deprecated APIs --- pyomo/contrib/parmest/parmest.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 6bc69c78bcd..ae6a6ffe184 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -254,7 +254,7 @@ class Estimator(object): ---------- experiment_list: list of Experiments A list of experiment objects which creates one labeled model for - each expeirment + each experiment obj_function: string or function (optional) Built in objective (currently only "SSE") or custom function used to formulate parameter estimation objective. @@ -271,26 +271,25 @@ class Estimator(object): # backwards compatible constructor will accept the old deprecated inputs # as well as the new inputs using experiment lists - def __init__(self, *args, **kwargs): - - # check that we have at least one argument - assert len(args) > 0 + # TODO: when the deprecated Parmest API is removed, *args, can be removed from this constructor + def __init__(self, experiment_list, *args, obj_function=None, tee=False, diagnostic_mode=False, solver_options=None): # use deprecated interface self.pest_deprecated = None - if callable(args[0]): + if callable(experiment_list): deprecation_warning( - 'Using deprecated parmest inputs (model_function, ' - + 'data, theta_names), please use experiment lists instead.', + 'Using deprecated parmest interface (model_function, ' + 'data, theta_names). This interface will be removed in a future release, ' + 'please update to the new parmest interface using experiment lists.', version=DEPRECATION_VERSION, ) - self.pest_deprecated = _DeprecatedEstimator(*args, **kwargs) + self.pest_deprecated = _DeprecatedEstimator(experiment_list, *args, obj_function, tee, diagnostic_mode, solver_options) return # check that we have a (non-empty) list of experiments - assert isinstance(args[0], list) - assert len(args[0]) > 0 - self.exp_list = args[0] + assert isinstance(experiment_list, list) + assert len(args) == 0 + self.exp_list = experiment_list # check that an experiment has experiment_outputs and unknown_parameters model = self.exp_list[0].get_labeled_model() @@ -308,10 +307,10 @@ def __init__(self, *args, **kwargs): ) # populate keyword argument options - self.obj_function = kwargs.get('obj_function', None) - self.tee = kwargs.get('tee', False) - self.diagnostic_mode = kwargs.get('diagnostic_mode', False) - self.solver_options = kwargs.get('solver_options', None) + self.obj_function = obj_function + self.tee = tee + self.diagnostic_mode = diagnostic_mode + self.solver_options = solver_options # TODO This might not be needed here. # We could collect the union (or intersect?) of thetas when the models are built From 7f224c8c1f4c0ae79b35d75269441042b8429186 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 30 Apr 2024 13:43:52 -0600 Subject: [PATCH 1299/3044] Not blindly evaluating unary functions, but that actually causes me a whole conundrum about fixed variables --- pyomo/repn/linear_wrt.py | 24 ++++++++++++- pyomo/repn/tests/test_linear_wrt.py | 55 +++++++++++++++++++++++------ 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/pyomo/repn/linear_wrt.py b/pyomo/repn/linear_wrt.py index bbd4c6a7d25..c5cd6c130b0 100644 --- a/pyomo/repn/linear_wrt.py +++ b/pyomo/repn/linear_wrt.py @@ -16,11 +16,13 @@ from pyomo.core import Var from pyomo.core.expr.logical_expr import _flattened from pyomo.core.expr.numeric_expr import ( + AbsExpression, LinearExpression, MonomialTermExpression, mutable_expression, ProductExpression, SumExpression, + UnaryFunctionExpression, ) from pyomo.repn.linear import ( ExitNodeDispatcher, @@ -183,13 +185,33 @@ def _before_var(visitor, child): def _handle_product_constant_constant(visitor, node, arg1, arg2): # ESJ: Can I do this? Just let the potential nans go through? return _CONSTANT, arg1[1] * arg2[1] + _exit_node_handlers[ProductExpression].update( { (_CONSTANT, _CONSTANT): _handle_product_constant_constant, } ) - + +def _handle_unary_constant(visitor, node, arg): + # We override this because we can't blindly use apply_node_operation in this case + if arg.__class__ not in native_numeric_types: + return _CONSTANT, node.create_node_with_local_data( + (linear.to_expression(visitor, arg),)) + # otherwise do the usual: + ans = apply_node_operation(node, (arg[1],)) + # Unary includes sqrt() which can return complex numbers + if ans.__class__ in native_complex_types: + ans = complex_number_error(ans, visitor, node) + return _CONSTANT, ans + +_exit_node_handlers[UnaryFunctionExpression].update( + { + (_CONSTANT,): _handle_unary_constant + } +) +_exit_node_handlers[AbsExpression] = _exit_node_handlers[UnaryFunctionExpression] + # LinearSubsystemRepnVisitor class MultilevelLinearRepnVisitor(LinearRepnVisitor): diff --git a/pyomo/repn/tests/test_linear_wrt.py b/pyomo/repn/tests/test_linear_wrt.py index a9a31ce8232..fa7aaf7799b 100644 --- a/pyomo/repn/tests/test_linear_wrt.py +++ b/pyomo/repn/tests/test_linear_wrt.py @@ -23,6 +23,7 @@ def make_model(self): m = ConcreteModel() m.x = Var(bounds=(0, 45)) m.y = Var(domain=Binary) + m.z = Var() return m @@ -44,7 +45,6 @@ def test_walk_sum(self): def test_walk_triple_sum(self): m = self.make_model() - m.z = Var() e = m.x + m.z * m.y + m.z cfg = VisitorConfig() @@ -81,7 +81,7 @@ def test_sum_two_of_the_same(self): def test_sum_with_mult_0(self): m = self.make_model() - e = 0*m.x + m.x + m.y + e = 0*m.x + m.x - m.y cfg = VisitorConfig() visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) @@ -91,13 +91,17 @@ def test_sum_with_mult_0(self): self.assertEqual(len(repn.linear), 1) self.assertIn(id(m.x), repn.linear) self.assertEqual(repn.linear[id(m.x)], 1) - self.assertIs(repn.constant, m.y) + assertExpressionsEqual( + self, + repn.constant, + - m.y + ) self.assertEqual(repn.multiplier, 1) - assertExpressionsEqual(self, repn.to_expression(visitor), m.x + m.y) + assertExpressionsEqual(self, repn.to_expression(visitor), m.x - m.y) def test_sum_nonlinear_to_linear(self): m = self.make_model() - e = m.y * m.x**2 + m.y * m.x + 3 + e = m.y * m.x**2 + m.y * m.x - 3 cfg = VisitorConfig() visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) @@ -111,10 +115,10 @@ def test_sum_nonlinear_to_linear(self): self.assertEqual(len(repn.linear), 1) self.assertIn(id(m.x), repn.linear) self.assertIs(repn.linear[id(m.x)], m.y) - self.assertEqual(repn.constant, 3) + self.assertEqual(repn.constant, -3) self.assertEqual(repn.multiplier, 1) assertExpressionsEqual(self, repn.to_expression(visitor), m.y * m.x ** 2 - + m.y * m.x + 3) + + m.y * m.x - 3) def test_sum_nonlinear_to_nonlinear(self): m = self.make_model() @@ -256,7 +260,6 @@ def test_nonlinear(self): def test_finalize(self): m = self.make_model() - m.z = Var() m.w = Var() e = m.x + 2 * m.w**2 * m.y - m.x - m.w * m.z @@ -330,8 +333,8 @@ def test_ANY_over_constant_division(self): m = ConcreteModel() m.p = Param(mutable=True, initialize=2, domain=Any) m.x = Var() - m.y = Var() m.z = Var() + m.y = Var() # We aren't treating this as a Var, so we don't really care that it's fixed. m.y.fix(1) @@ -358,8 +361,8 @@ def test_errors_propogate_nan(self): m = ConcreteModel() m.p = Param(mutable=True, initialize=0, domain=Any) m.x = Var() - m.y = Var() m.z = Var() + m.y = Var() m.y.fix(1) expr = m.y + m.x + m.z + ((3 * m.z * m.x) / m.p) / m.y @@ -399,3 +402,35 @@ def test_errors_propogate_nan(self): ) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) + + def test_negation_constant(self): + m = self.make_model() + e = - (m.y * m.z + 17) + + cfg = VisitorConfig() + repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(e) + + self.assertEqual(len(repn.linear), 0) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual( + self, + repn.constant, + - 1 * (m.y * m.z + 17) + ) + self.assertIsNone(repn.nonlinear) + + def test_product_nonlinear(self): + m = self.make_model() + e = (m.x ** 2) * (log(m.y) * m.z ** 4) * m.y + cfg = VisitorConfig() + repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x, m.z]).walk_expression(e) + + self.assertEqual(len(repn.linear), 0) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + print(repn.nonlinear) + assertExpressionsEqual( + self, + repn.nonlinear, + (m.x ** 2) * (m.z ** 4 * log(m.y)) * m.y + ) From 2a9c5337a52ebefadfc72e392d6b72299503d006 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 30 Apr 2024 13:59:21 -0600 Subject: [PATCH 1300/3044] Fixing a typo --- pyomo/contrib/cp/tests/test_docplex_walker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/cp/tests/test_docplex_walker.py b/pyomo/contrib/cp/tests/test_docplex_walker.py index 1173ae66eab..f7abb3d2b3c 100644 --- a/pyomo/contrib/cp/tests/test_docplex_walker.py +++ b/pyomo/contrib/cp/tests/test_docplex_walker.py @@ -1610,7 +1610,7 @@ def test_always_in(self): def test_always_in_single_pulse(self): # This is a bit silly as you can tell whether or not it is feasible - # structurally, but there's not reason it couldn't happen. + # structurally, but there's no reason it couldn't happen. m = self.get_model() f = Pulse((m.i, 3)) m.c = LogicalConstraint(expr=f.within((0, 3), (0, 10))) From efe4dabc5cc14aada83165f7abc3693f7e061bec Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 30 Apr 2024 16:22:21 -0600 Subject: [PATCH 1301/3044] Fixing formatting in parmest --- pyomo/contrib/parmest/parmest.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ae6a6ffe184..c350f315fe4 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -272,7 +272,15 @@ class Estimator(object): # backwards compatible constructor will accept the old deprecated inputs # as well as the new inputs using experiment lists # TODO: when the deprecated Parmest API is removed, *args, can be removed from this constructor - def __init__(self, experiment_list, *args, obj_function=None, tee=False, diagnostic_mode=False, solver_options=None): + def __init__( + self, + experiment_list, + *args, + obj_function=None, + tee=False, + diagnostic_mode=False, + solver_options=None, + ): # use deprecated interface self.pest_deprecated = None @@ -283,7 +291,14 @@ def __init__(self, experiment_list, *args, obj_function=None, tee=False, diagnos 'please update to the new parmest interface using experiment lists.', version=DEPRECATION_VERSION, ) - self.pest_deprecated = _DeprecatedEstimator(experiment_list, *args, obj_function, tee, diagnostic_mode, solver_options) + self.pest_deprecated = _DeprecatedEstimator( + experiment_list, + *args, + obj_function, + tee, + diagnostic_mode, + solver_options, + ) return # check that we have a (non-empty) list of experiments @@ -309,7 +324,7 @@ def __init__(self, experiment_list, *args, obj_function=None, tee=False, diagnos # populate keyword argument options self.obj_function = obj_function self.tee = tee - self.diagnostic_mode = diagnostic_mode + self.diagnostic_mode = diagnostic_mode self.solver_options = solver_options # TODO This might not be needed here. From 8638268d73639866b5f8d885c69efd6db93b5a1d Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 30 Apr 2024 16:33:54 -0600 Subject: [PATCH 1302/3044] Fix formatting in parmest --- pyomo/contrib/parmest/parmest.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index c350f315fe4..ded40a87aff 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -262,7 +262,7 @@ class Estimator(object): "as is" and should be defined with a "FirstStageCost" and "SecondStageCost" expression that are used to build an objective. tee: bool, optional - Indicates that ef solver output should be teed + If True, print the solver output to the screen diagnostic_mode: bool, optional If True, print diagnostics from the solver solver_options: dict, optional @@ -273,12 +273,12 @@ class Estimator(object): # as well as the new inputs using experiment lists # TODO: when the deprecated Parmest API is removed, *args, can be removed from this constructor def __init__( - self, - experiment_list, - *args, - obj_function=None, - tee=False, - diagnostic_mode=False, + self, + experiment_list, + *args, + obj_function=None, + tee=False, + diagnostic_mode=False, solver_options=None, ): @@ -292,11 +292,11 @@ def __init__( version=DEPRECATION_VERSION, ) self.pest_deprecated = _DeprecatedEstimator( - experiment_list, - *args, - obj_function, - tee, - diagnostic_mode, + experiment_list, + *args, + obj_function, + tee, + diagnostic_mode, solver_options, ) return From eeec88ed9f8c550f4f91d9afea155fbf4c218d2b Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 30 Apr 2024 18:18:29 -0600 Subject: [PATCH 1303/3044] Reworking parmest Estimator constructor --- pyomo/contrib/parmest/parmest.py | 58 ++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index ded40a87aff..cdff785899e 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -53,7 +53,9 @@ import logging import types import json +from collections.abc import Callable from itertools import combinations +from functools import singledispatchmethod from pyomo.common.dependencies import ( attempt_import, @@ -271,39 +273,18 @@ class Estimator(object): # backwards compatible constructor will accept the old deprecated inputs # as well as the new inputs using experiment lists - # TODO: when the deprecated Parmest API is removed, *args, can be removed from this constructor + @singledispatchmethod def __init__( self, experiment_list, - *args, obj_function=None, tee=False, diagnostic_mode=False, solver_options=None, ): - # use deprecated interface - self.pest_deprecated = None - if callable(experiment_list): - deprecation_warning( - 'Using deprecated parmest interface (model_function, ' - 'data, theta_names). This interface will be removed in a future release, ' - 'please update to the new parmest interface using experiment lists.', - version=DEPRECATION_VERSION, - ) - self.pest_deprecated = _DeprecatedEstimator( - experiment_list, - *args, - obj_function, - tee, - diagnostic_mode, - solver_options, - ) - return - # check that we have a (non-empty) list of experiments assert isinstance(experiment_list, list) - assert len(args) == 0 self.exp_list = experiment_list # check that an experiment has experiment_outputs and unknown_parameters @@ -326,6 +307,9 @@ def __init__( self.tee = tee self.diagnostic_mode = diagnostic_mode self.solver_options = solver_options + self.pest_deprecated = ( + None # TODO: delete this when deprecated interface is removed + ) # TODO This might not be needed here. # We could collect the union (or intersect?) of thetas when the models are built @@ -339,6 +323,36 @@ def __init__( # boolean to indicate if model is initialized using a square solve self.model_initialized = False + # use deprecated interface + @__init__.register(Callable) + def _deprecated_init( + self, + model_function, + data, + theta_names, + obj_function=None, + tee=False, + diagnostic_mode=False, + solver_options=None, + ): + + deprecation_warning( + "You're using the deprecated parmest interface (model_function, " + "data, theta_names). This interface will be removed in a future release, " + "please update to the new parmest interface using experiment lists.", + version=DEPRECATION_VERSION, + ) + self.pest_deprecated = _DeprecatedEstimator( + model_function, + data, + theta_names, + obj_function, + tee, + diagnostic_mode, + solver_options, + ) + return + def _return_theta_names(self): """ Return list of fitted model parameter names From add489cb99a8e7ea7d2f65d4fb32662ecb07e57e Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Wed, 1 May 2024 09:42:45 -0400 Subject: [PATCH 1304/3044] update log of call_before_subproblem_solve --- pyomo/contrib/mindtpy/algorithm_base_class.py | 6 +++--- pyomo/contrib/mindtpy/config_options.py | 4 ++-- pyomo/contrib/mindtpy/single_tree.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/mindtpy/algorithm_base_class.py b/pyomo/contrib/mindtpy/algorithm_base_class.py index 5547350ed44..bbcb8f5fc56 100644 --- a/pyomo/contrib/mindtpy/algorithm_base_class.py +++ b/pyomo/contrib/mindtpy/algorithm_base_class.py @@ -2950,7 +2950,7 @@ def MindtPy_iteration_loop(self): ) if self.curr_int_sol not in set(self.integer_list): # Call the NLP pre-solve callback - with time_code(self.timing, 'Call after subproblem solve'): + with time_code(self.timing, 'Call before subproblem solve'): config.call_before_subproblem_solve(self.fixed_nlp) fixed_nlp, fixed_nlp_result = self.solve_subproblem() @@ -2965,7 +2965,7 @@ def MindtPy_iteration_loop(self): # The constraint linearization happens in the handlers if not config.solution_pool: # Call the NLP pre-solve callback - with time_code(self.timing, 'Call after subproblem solve'): + with time_code(self.timing, 'Call before subproblem solve'): config.call_before_subproblem_solve(self.fixed_nlp) fixed_nlp, fixed_nlp_result = self.solve_subproblem() @@ -3002,7 +3002,7 @@ def MindtPy_iteration_loop(self): self.integer_list.append(self.curr_int_sol) # Call the NLP pre-solve callback - with time_code(self.timing, 'Call after subproblem solve'): + with time_code(self.timing, 'Call before subproblem solve'): config.call_before_subproblem_solve(self.fixed_nlp) fixed_nlp, fixed_nlp_result = self.solve_subproblem() diff --git a/pyomo/contrib/mindtpy/config_options.py b/pyomo/contrib/mindtpy/config_options.py index 019c6933d76..0d0b536525a 100644 --- a/pyomo/contrib/mindtpy/config_options.py +++ b/pyomo/contrib/mindtpy/config_options.py @@ -328,8 +328,8 @@ def _add_common_configs(CONFIG): ConfigValue( default=_DoNothing(), domain=None, - description='Function to be executed after every subproblem', - doc='Callback hook after a solution of the nonlinear subproblem.', + description='Function to be executed before every subproblem', + doc='Callback hook before a solution of the nonlinear subproblem.', ), ) CONFIG.declare( diff --git a/pyomo/contrib/mindtpy/single_tree.py b/pyomo/contrib/mindtpy/single_tree.py index bc0f5d3cf4f..6b501ef874d 100644 --- a/pyomo/contrib/mindtpy/single_tree.py +++ b/pyomo/contrib/mindtpy/single_tree.py @@ -774,7 +774,7 @@ def __call__(self): # solve subproblem # Call the NLP pre-solve callback - with time_code(mindtpy_solver.timing, 'Call after subproblem solve'): + with time_code(mindtpy_solver.timing, 'Call before subproblem solve'): config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp) # The constraint linearization happens in the handlers fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() @@ -923,7 +923,7 @@ def LazyOACallback_gurobi(cb_m, cb_opt, cb_where, mindtpy_solver, config): # solve subproblem # Call the NLP pre-solve callback - with time_code(mindtpy_solver.timing, 'Call after subproblem solve'): + with time_code(mindtpy_solver.timing, 'Call before subproblem solve'): config.call_before_subproblem_solve(mindtpy_solver.fixed_nlp) # The constraint linearization happens in the handlers fixed_nlp, fixed_nlp_result = mindtpy_solver.solve_subproblem() From dcd5db165e64dcd9b4077401e184266a88719adc Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 1 May 2024 09:42:13 -0600 Subject: [PATCH 1305/3044] Adding comments to parmest Estimator constructor logic --- pyomo/contrib/parmest/parmest.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index cdff785899e..3516c52d19d 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -271,8 +271,11 @@ class Estimator(object): Provides options to the solver (also the name of an attribute) """ - # backwards compatible constructor will accept the old deprecated inputs - # as well as the new inputs using experiment lists + # The singledispatchmethod decorator is used here as a deprecation + # shim to be able to support the now deprecated Estimator interface + # which had a different number of arguments. When the deprecated API + # is removed this decorator and the _deprecated_init method below + # can be removed @singledispatchmethod def __init__( self, @@ -307,9 +310,9 @@ def __init__( self.tee = tee self.diagnostic_mode = diagnostic_mode self.solver_options = solver_options - self.pest_deprecated = ( - None # TODO: delete this when deprecated interface is removed - ) + + # TODO: delete this when the deprecated interface is removed + self.pest_deprecated = None # TODO This might not be needed here. # We could collect the union (or intersect?) of thetas when the models are built @@ -323,7 +326,11 @@ def __init__( # boolean to indicate if model is initialized using a square solve self.model_initialized = False - # use deprecated interface + # The deprecated Estimator constructor + # This works by checking the type of the first argument passed to + # the class constructor. If it matches the old interface (i.e. is + # callable) then this _deprecated_init method is called and the + # deprecation warning is displayed. @__init__.register(Callable) def _deprecated_init( self, From 927476913b1c0c6be56ac79c7095ad3414775d38 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 1 May 2024 09:48:10 -0600 Subject: [PATCH 1306/3044] Adding default values to the parmest Estimator docstring --- pyomo/contrib/parmest/parmest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 3516c52d19d..c63bb10b89e 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -263,12 +263,14 @@ class Estimator(object): If no function is specified, the model is used "as is" and should be defined with a "FirstStageCost" and "SecondStageCost" expression that are used to build an objective. + Default is None. tee: bool, optional - If True, print the solver output to the screen + If True, print the solver output to the screen. Default is False. diagnostic_mode: bool, optional - If True, print diagnostics from the solver + If True, print diagnostics from the solver. Default is False. solver_options: dict, optional - Provides options to the solver (also the name of an attribute) + Provides options to the solver (also the name of an attribute). + Default is None. """ # The singledispatchmethod decorator is used here as a deprecation From 48624f4f4198592b32e181c588ac508e7daa0923 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 1 May 2024 09:54:31 -0600 Subject: [PATCH 1307/3044] Removing list of parmest TODO items that was opened as a GitHub issue --- pyomo/contrib/parmest/parmest.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index c63bb10b89e..f3d35f41013 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -15,16 +15,6 @@ # TODO: move use_mpisppy to a Pyomo configuration option -# Redesign TODOS -# TODO: Create additional built in objective expressions in an Enum class which includes SSE (see SSE function below) -# TODO: Clean up the use of theta_names through out the code. The Experiment returns the CUID of each theta and this can be used directly (instead of the name) -# TODO: Clean up the use of updated_theta_names, model_theta_names, estimator_theta_names. Not sure if estimator_theta_names is the union or intersect of thetas in each model -# TODO: _return_theta_names should no longer be needed -# TODO: generally, theta ordering is not preserved by pyomo, so we should check that ordering -# matches values for each function, otherwise results will be wrong and/or inconsistent -# TODO: return model object (m.k1) and CUIDs in dataframes instead of names ("k1") - - # False implies always use the EF that is local to parmest use_mpisppy = True # Use it if we can but use local if not. if use_mpisppy: From e01830e117bf27f593fbc49689348c83ae5dc2ea Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 1 May 2024 11:35:33 -0600 Subject: [PATCH 1308/3044] Simplify checking for naming conflicts in parmest --- pyomo/contrib/parmest/parmest.py | 35 +++++++++++++++----------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index f3d35f41013..28506521524 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -350,14 +350,13 @@ def _deprecated_init( diagnostic_mode, solver_options, ) - return def _return_theta_names(self): """ Return list of fitted model parameter names """ # check for deprecated inputs - if self.pest_deprecated is not None: + if self.pest_deprecated: # if fitted model parameter names differ from theta_names # created when Estimator object is created @@ -365,9 +364,9 @@ def _return_theta_names(self): return self.pest_deprecated.theta_names_updated else: - return ( - self.pest_deprecated.theta_names - ) # default theta_names, created when Estimator object is created + + # default theta_names, created when Estimator object is created + return self.pest_deprecated.theta_names else: @@ -377,9 +376,9 @@ def _return_theta_names(self): return self.theta_names_updated else: - return ( - self.estimator_theta_names - ) # default theta_names, created when Estimator object is created + + # default theta_names, created when Estimator object is created + return self.estimator_theta_names def _expand_indexed_unknowns(self, model_temp): """ @@ -417,21 +416,19 @@ def _create_parmest_model(self, experiment_number): # Add objective function (optional) if self.obj_function: - for obj in model.component_objects(pyo.Objective): - if obj.name in ["Total_Cost_Objective"]: - raise RuntimeError( - "Parmest will not override the existing model Objective named " - + obj.name - ) - obj.deactivate() - for expr in model.component_data_objects(pyo.Expression): - if expr.name in ["FirstStageCost", "SecondStageCost"]: + # Check for component naming conflicts + reserved_names = ['Total_Cost_Objective', 'FirstStageCost', 'SecondStageCost'] + for n in reserved_names: + if model.component(n) or hasattr(model, n): raise RuntimeError( - "Parmest will not override the existing model Expression named " - + expr.name + f"Parmest will not override the existing model component named {n}" ) + # Deactivate any existing objective functions + for obj in model.component_objects(pyo.Objective): + obj.deactivate() + # TODO, this needs to be turned a enum class of options that still support custom functions if self.obj_function == 'SSE': second_stage_rule = SSE From d5a289b884738ee536fb7118517d1c7093dd3ec7 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 1 May 2024 12:15:29 -0600 Subject: [PATCH 1309/3044] Simplifying _expand_indexed_unknowns --- pyomo/contrib/parmest/parmest.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 28506521524..1acd63c976d 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -384,22 +384,14 @@ def _expand_indexed_unknowns(self, model_temp): """ Expand indexed variables to get full list of thetas """ - model_theta_list = [k.name for k, v in model_temp.unknown_parameters.items()] - - # check for indexed theta items - indexed_theta_list = [] - for theta_i in model_theta_list: - var_cuid = ComponentUID(theta_i) - var_validate = var_cuid.find_component_on(model_temp) - for ind in var_validate.index_set(): - if ind is not None: - indexed_theta_list.append(theta_i + '[' + str(ind) + ']') - else: - indexed_theta_list.append(theta_i) - # if we found indexed thetas, use expanded list - if len(indexed_theta_list) > len(model_theta_list): - model_theta_list = indexed_theta_list + model_theta_list = [] + for c in model_temp.unknown_parameters.keys(): + if c.is_indexed(): + for _, ci in c.items(): + model_theta_list.append(ci.name) + else: + model_theta_list.append(c.name) return model_theta_list @@ -418,7 +410,11 @@ def _create_parmest_model(self, experiment_number): if self.obj_function: # Check for component naming conflicts - reserved_names = ['Total_Cost_Objective', 'FirstStageCost', 'SecondStageCost'] + reserved_names = [ + 'Total_Cost_Objective', + 'FirstStageCost', + 'SecondStageCost', + ] for n in reserved_names: if model.component(n) or hasattr(model, n): raise RuntimeError( From 27e84a8fd9d92b2e5e945650fcc8c1f6fb50570d Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 1 May 2024 16:04:55 -0600 Subject: [PATCH 1310/3044] Cleaning up logic in parmest --- pyomo/contrib/parmest/parmest.py | 34 +++++---------------- pyomo/contrib/parmest/tests/test_parmest.py | 2 +- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 1acd63c976d..2d4c323b9b8 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -390,7 +390,7 @@ def _expand_indexed_unknowns(self, model_temp): if c.is_indexed(): for _, ci in c.items(): model_theta_list.append(ci.name) - else: + else: model_theta_list.append(c.name) return model_theta_list @@ -401,7 +401,6 @@ def _create_parmest_model(self, experiment_number): """ model = self.exp_list[experiment_number].get_labeled_model() - self.theta_names = [k.name for k, v in model.unknown_parameters.items()] if len(model.unknown_parameters) == 0: model.parmest_dummy_var = pyo.Var(initialize=1.0) @@ -443,29 +442,10 @@ def TotalCost_rule(model): ) # Convert theta Params to Vars, and unfix theta Vars - model = utils.convert_params_to_vars(model, self.theta_names) + theta_names = [k.name for k, v in model.unknown_parameters.items()] + parmest_model = utils.convert_params_to_vars(model, theta_names, fix_vars=False) - # Update theta names list to use CUID string representation - for i, theta in enumerate(self.theta_names): - var_cuid = ComponentUID(theta) - var_validate = var_cuid.find_component_on(model) - if var_validate is None: - logger.warning( - "theta_name[%s] (%s) was not found on the model", (i, theta) - ) - else: - try: - # If the component is not a variable, - # this will generate an exception (and the warning - # in the 'except') - var_validate.unfix() - self.theta_names[i] = repr(var_cuid) - except: - logger.warning(theta + ' is not a variable') - - self.parmest_model = model - - return model + return parmest_model def _instance_creation_callback(self, experiment_number=None, cb_data=None): model = self._create_parmest_model(experiment_number) @@ -1186,12 +1166,14 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): # create a local instance of the pyomo model to access model variables and parameters model_temp = self._create_parmest_model(0) model_theta_list = self._expand_indexed_unknowns(model_temp) + # TODO: check if model_theta_list is correct if original unknown parameters + # are declared as params and transformed to vars during call to create_parmest_model - # if self.theta_names is not the same as temp model_theta_list, + # if self.estimator_theta_names is not the same as temp model_theta_list, # create self.theta_names_updated if set(self.estimator_theta_names) == set(model_theta_list) and len( self.estimator_theta_names - ) == set(model_theta_list): + ) == len(set(model_theta_list)): pass else: self.theta_names_updated = model_theta_list diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index e9a8e089335..0590f165da3 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -176,7 +176,7 @@ def test_diagnostic_mode(self): asym = np.arange(10, 30, 2) rate = np.arange(0, 1.5, 0.25) theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=self.pest.theta_names + list(product(asym, rate)), columns=self.pest.estimator_theta_names ) obj_at_theta = self.pest.objective_at_theta(theta_vals) From 20dd5aaf73bc279273c145f2ae6979c920bf5855 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 2 May 2024 15:57:06 -0600 Subject: [PATCH 1311/3044] Renaming my walker and repn to something sane, but that meant changing the meaning of the 'wrt' argument --- ...{linear_wrt.py => parameterized_linear.py} | 31 +-- ...ar_wrt.py => test_parameterized_linear.py} | 179 ++++++------------ 2 files changed, 77 insertions(+), 133 deletions(-) rename pyomo/repn/{linear_wrt.py => parameterized_linear.py} (94%) rename pyomo/repn/tests/{test_linear_wrt.py => test_parameterized_linear.py} (73%) diff --git a/pyomo/repn/linear_wrt.py b/pyomo/repn/parameterized_linear.py similarity index 94% rename from pyomo/repn/linear_wrt.py rename to pyomo/repn/parameterized_linear.py index c5cd6c130b0..9df0ea458db 100644 --- a/pyomo/repn/linear_wrt.py +++ b/pyomo/repn/parameterized_linear.py @@ -52,7 +52,7 @@ def _merge_dict(dest_dict, mult, src_dict): dest_dict[vid] = coef -class LinearSubsystemRepn(LinearRepn): +class ParameterizedLinearRepn(LinearRepn): def to_expression(self, visitor): if self.nonlinear is not None: # We want to start with the nonlinear term (and use @@ -160,7 +160,7 @@ def _before_general_expression(visitor, child): @staticmethod def _before_var(visitor, child): - if child in visitor.wrt: + if child not in visitor.wrt: # This is a normal situation _id = id(child) if _id not in visitor.var_map: @@ -185,19 +185,19 @@ def _before_var(visitor, child): def _handle_product_constant_constant(visitor, node, arg1, arg2): # ESJ: Can I do this? Just let the potential nans go through? return _CONSTANT, arg1[1] * arg2[1] - + _exit_node_handlers[ProductExpression].update( - { - (_CONSTANT, _CONSTANT): _handle_product_constant_constant, - } + {(_CONSTANT, _CONSTANT): _handle_product_constant_constant} ) + def _handle_unary_constant(visitor, node, arg): # We override this because we can't blindly use apply_node_operation in this case if arg.__class__ not in native_numeric_types: return _CONSTANT, node.create_node_with_local_data( - (linear.to_expression(visitor, arg),)) + (linear.to_expression(visitor, arg),) + ) # otherwise do the usual: ans = apply_node_operation(node, (arg[1],)) # Unary includes sqrt() which can return complex numbers @@ -205,17 +205,15 @@ def _handle_unary_constant(visitor, node, arg): ans = complex_number_error(ans, visitor, node) return _CONSTANT, ans + _exit_node_handlers[UnaryFunctionExpression].update( - { - (_CONSTANT,): _handle_unary_constant - } + {(_CONSTANT,): _handle_unary_constant} ) _exit_node_handlers[AbsExpression] = _exit_node_handlers[UnaryFunctionExpression] -# LinearSubsystemRepnVisitor -class MultilevelLinearRepnVisitor(LinearRepnVisitor): - Result = LinearSubsystemRepn +class ParameterizedLinearRepnVisitor(LinearRepnVisitor): + Result = ParameterizedLinearRepn exit_node_handlers = _exit_node_handlers exit_node_dispatcher = ExitNodeDispatcher( _initialize_exit_node_dispatcher(_exit_node_handlers) @@ -253,8 +251,11 @@ def finalizeResult(self, result): self._factor_multiplier_into_linear_terms(ans, mult) return ans if mult == 1: - zeros = [(vid, coef) for vid, coef in ans.linear.items() if - coef.__class__ in native_numeric_types and not coef] + zeros = [ + (vid, coef) + for vid, coef in ans.linear.items() + if coef.__class__ in native_numeric_types and not coef + ] for vid, coef in zeros: del ans.linear[vid] elif not mult: diff --git a/pyomo/repn/tests/test_linear_wrt.py b/pyomo/repn/tests/test_parameterized_linear.py similarity index 73% rename from pyomo/repn/tests/test_linear_wrt.py rename to pyomo/repn/tests/test_parameterized_linear.py index fa7aaf7799b..32f58dbfc13 100644 --- a/pyomo/repn/tests/test_linear_wrt.py +++ b/pyomo/repn/tests/test_parameterized_linear.py @@ -13,12 +13,12 @@ import pyomo.common.unittest as unittest from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.environ import Any, Binary, ConcreteModel, log, Param, Var -from pyomo.repn.linear_wrt import MultilevelLinearRepnVisitor +from pyomo.repn.parameterized_linear import ParameterizedLinearRepnVisitor from pyomo.repn.tests.test_linear import VisitorConfig from pyomo.repn.util import InvalidNumber -class TestMultilevelLinearRepnVisitor(unittest.TestCase): +class TestParameterizedLinearRepnVisitor(unittest.TestCase): def make_model(self): m = ConcreteModel() m.x = Var(bounds=(0, 45)) @@ -31,7 +31,7 @@ def test_walk_sum(self): m = self.make_model() e = m.x + m.y cfg = VisitorConfig() - visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) @@ -48,7 +48,7 @@ def test_walk_triple_sum(self): e = m.x + m.z * m.y + m.z cfg = VisitorConfig() - visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x, m.y]) + visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.z]) repn = visitor.walk_expression(e) @@ -67,7 +67,7 @@ def test_sum_two_of_the_same(self): m = self.make_model() e = m.x + m.x cfg = VisitorConfig() - visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x, m.y]) + visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.z]) repn = visitor.walk_expression(e) @@ -77,25 +77,21 @@ def test_sum_two_of_the_same(self): self.assertEqual(repn.linear[id(m.x)], 2) self.assertEqual(repn.constant, 0) self.assertEqual(repn.multiplier, 1) - assertExpressionsEqual(self, repn.to_expression(visitor), 2*m.x) + assertExpressionsEqual(self, repn.to_expression(visitor), 2 * m.x) def test_sum_with_mult_0(self): m = self.make_model() - e = 0*m.x + m.x - m.y - + e = 0 * m.x + m.x - m.y + cfg = VisitorConfig() - visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) self.assertIsNone(repn.nonlinear) self.assertEqual(len(repn.linear), 1) self.assertIn(id(m.x), repn.linear) self.assertEqual(repn.linear[id(m.x)], 1) - assertExpressionsEqual( - self, - repn.constant, - - m.y - ) + assertExpressionsEqual(self, repn.constant, -m.y) self.assertEqual(repn.multiplier, 1) assertExpressionsEqual(self, repn.to_expression(visitor), m.x - m.y) @@ -104,71 +100,56 @@ def test_sum_nonlinear_to_linear(self): e = m.y * m.x**2 + m.y * m.x - 3 cfg = VisitorConfig() - visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) - assertExpressionsEqual( - self, - repn.nonlinear, - m.y * m.x ** 2 - ) + assertExpressionsEqual(self, repn.nonlinear, m.y * m.x**2) self.assertEqual(len(repn.linear), 1) self.assertIn(id(m.x), repn.linear) self.assertIs(repn.linear[id(m.x)], m.y) self.assertEqual(repn.constant, -3) self.assertEqual(repn.multiplier, 1) - assertExpressionsEqual(self, repn.to_expression(visitor), m.y * m.x ** 2 - + m.y * m.x - 3) + assertExpressionsEqual( + self, repn.to_expression(visitor), m.y * m.x**2 + m.y * m.x - 3 + ) def test_sum_nonlinear_to_nonlinear(self): m = self.make_model() - e = m.x ** 3 + 3 + m.x**2 + e = m.x**3 + 3 + m.x**2 cfg = VisitorConfig() - visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) - assertExpressionsEqual( - self, - repn.nonlinear, - m.x ** 3 + m.x ** 2 - ) + assertExpressionsEqual(self, repn.nonlinear, m.x**3 + m.x**2) self.assertEqual(repn.constant, 3) self.assertEqual(repn.multiplier, 1) - assertExpressionsEqual(self, repn.to_expression(visitor), m.x ** 3 - + m.x ** 2 + 3) + assertExpressionsEqual( + self, repn.to_expression(visitor), m.x**3 + m.x**2 + 3 + ) def test_sum_to_linear_expr(self): m = self.make_model() e = m.x + m.y * (m.x + 5) cfg = VisitorConfig() - visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) self.assertEqual(len(repn.linear), 1) self.assertIn(id(m.x), repn.linear) - assertExpressionsEqual( - self, - repn.linear[id(m.x)], - 1 + m.y - ) - assertExpressionsEqual( - self, - repn.constant, - m.y * 5 - ) + assertExpressionsEqual(self, repn.linear[id(m.x)], 1 + m.y) + assertExpressionsEqual(self, repn.constant, m.y * 5) self.assertEqual(repn.multiplier, 1) assertExpressionsEqual( - self, - repn.to_expression(visitor), (1 + m.y) * m.x + m.y * 5 + self, repn.to_expression(visitor), (1 + m.y) * m.x + m.y * 5 ) def test_bilinear_term(self): m = self.make_model() e = m.x * m.y cfg = VisitorConfig() - visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) @@ -184,7 +165,7 @@ def test_distributed_bilinear_term(self): m = self.make_model() e = m.y * (m.x + 7) cfg = VisitorConfig() - visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) @@ -200,7 +181,7 @@ def test_monomial(self): m = self.make_model() e = 45 * m.y cfg = VisitorConfig() - visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.y]) + visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.x, m.z]) repn = visitor.walk_expression(e) @@ -216,7 +197,7 @@ def test_constant(self): m = self.make_model() e = 45 * m.y cfg = VisitorConfig() - visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) @@ -232,7 +213,7 @@ def test_fixed_var(self): e = (m.y**2) * (m.x + m.x**2) cfg = VisitorConfig() - visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) @@ -247,7 +228,7 @@ def test_nonlinear(self): e = (m.y * log(m.x)) * (m.y + 2) / m.x cfg = VisitorConfig() - visitor = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]) + visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) @@ -265,7 +246,7 @@ def test_finalize(self): e = m.x + 2 * m.w**2 * m.y - m.x - m.w * m.z cfg = VisitorConfig() - repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x, m.y, m.z]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.w]).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y, id(m.z): m.z}) self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1, id(m.z): 2}) @@ -273,23 +254,15 @@ def test_finalize(self): self.assertEqual(repn.constant, 0) self.assertEqual(len(repn.linear), 2) self.assertIn(id(m.y), repn.linear) - assertExpressionsEqual( - self, - repn.linear[id(m.y)], - 2 * m.w ** 2 - ) + assertExpressionsEqual(self, repn.linear[id(m.y)], 2 * m.w**2) self.assertIn(id(m.z), repn.linear) - assertExpressionsEqual( - self, - repn.linear[id(m.z)], - -m.w - ) + assertExpressionsEqual(self, repn.linear[id(m.z)], -m.w) self.assertEqual(repn.nonlinear, None) e *= 5 cfg = VisitorConfig() - repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x, m.y, m.z]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.w]).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y, id(m.z): m.z}) self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1, id(m.z): 2}) @@ -298,23 +271,15 @@ def test_finalize(self): self.assertEqual(len(repn.linear), 2) self.assertIn(id(m.y), repn.linear) print(repn.linear[id(m.y)]) - assertExpressionsEqual( - self, - repn.linear[id(m.y)], - 5 * (2 * m.w ** 2) - ) + assertExpressionsEqual(self, repn.linear[id(m.y)], 5 * (2 * m.w**2)) self.assertIn(id(m.z), repn.linear) - assertExpressionsEqual( - self, - repn.linear[id(m.z)], - -5 * m.w - ) + assertExpressionsEqual(self, repn.linear[id(m.z)], -5 * m.w) self.assertEqual(repn.nonlinear, None) e = 5 * (m.w * m.y + m.z**2 + 3 * m.w * m.y**3) cfg = VisitorConfig() - repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x, m.y, m.z]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.w]).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.y): m.y, id(m.z): m.z}) self.assertEqual(cfg.var_order, {id(m.y): 0, id(m.z): 1}) @@ -322,12 +287,10 @@ def test_finalize(self): self.assertEqual(repn.constant, 0) self.assertEqual(len(repn.linear), 1) self.assertIn(id(m.y), repn.linear) + assertExpressionsEqual(self, repn.linear[id(m.y)], 5 * m.w) assertExpressionsEqual( - self, - repn.linear[id(m.y)], - 5 * m.w + self, repn.nonlinear, (m.z**2 + 3 * m.w * m.y**3) * 5 ) - assertExpressionsEqual(self, repn.nonlinear, (m.z**2 + 3 * m.w * m.y**3) * 5) def test_ANY_over_constant_division(self): m = ConcreteModel() @@ -340,21 +303,15 @@ def test_ANY_over_constant_division(self): expr = m.y + m.x + m.z + ((3 * m.z * m.x) / m.p) / m.y cfg = VisitorConfig() - repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(expr) + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]).walk_expression( + expr + ) self.assertEqual(repn.multiplier, 1) - assertExpressionsEqual( - self, - repn.constant, - m.y + m.z - ) + assertExpressionsEqual(self, repn.constant, m.y + m.z) self.assertEqual(len(repn.linear), 1) print(repn.linear[id(m.x)]) - assertExpressionsEqual( - self, - repn.linear[id(m.x)], - 1 + 1.5 * m.z / m.y - ) + assertExpressionsEqual(self, repn.linear[id(m.x)], 1 + 1.5 * m.z / m.y) self.assertEqual(repn.nonlinear, None) def test_errors_propogate_nan(self): @@ -368,7 +325,9 @@ def test_errors_propogate_nan(self): expr = m.y + m.x + m.z + ((3 * m.z * m.x) / m.p) / m.y cfg = VisitorConfig() with LoggingIntercept() as LOG: - repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(expr) + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]).walk_expression( + expr + ) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'div(3*z, 0)'\n" @@ -376,61 +335,45 @@ def test_errors_propogate_nan(self): "\texpression: 3*z*x/p\n", ) self.assertEqual(repn.multiplier, 1) - assertExpressionsEqual( - self, - repn.constant, - m.y + m.z - ) + assertExpressionsEqual(self, repn.constant, m.y + m.z) self.assertEqual(len(repn.linear), 1) self.assertIsInstance(repn.linear[id(m.x)], InvalidNumber) - assertExpressionsEqual( - self, - repn.linear[id(m.x)].value, - 1 + float('nan')/m.y - ) + assertExpressionsEqual(self, repn.linear[id(m.x)].value, 1 + float('nan') / m.y) self.assertEqual(repn.nonlinear, None) m.y.fix(None) expr = m.z * log(m.y) + 3 - repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(expr) + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]).walk_expression( + expr + ) self.assertEqual(repn.multiplier, 1) self.assertIsInstance(repn.constant, InvalidNumber) - assertExpressionsEqual( - self, - repn.constant.value, - float('nan')*m.z + 3 - ) + assertExpressionsEqual(self, repn.constant.value, float('nan') * m.z + 3) self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) def test_negation_constant(self): m = self.make_model() - e = - (m.y * m.z + 17) + e = -(m.y * m.z + 17) cfg = VisitorConfig() - repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]).walk_expression(e) self.assertEqual(len(repn.linear), 0) self.assertEqual(repn.multiplier, 1) - assertExpressionsEqual( - self, - repn.constant, - - 1 * (m.y * m.z + 17) - ) + assertExpressionsEqual(self, repn.constant, -1 * (m.y * m.z + 17)) self.assertIsNone(repn.nonlinear) - + def test_product_nonlinear(self): m = self.make_model() - e = (m.x ** 2) * (log(m.y) * m.z ** 4) * m.y + e = (m.x**2) * (log(m.y) * m.z**4) * m.y cfg = VisitorConfig() - repn = MultilevelLinearRepnVisitor(*cfg, wrt=[m.x, m.z]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]).walk_expression(e) self.assertEqual(len(repn.linear), 0) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) print(repn.nonlinear) assertExpressionsEqual( - self, - repn.nonlinear, - (m.x ** 2) * (m.z ** 4 * log(m.y)) * m.y + self, repn.nonlinear, (m.x**2) * (m.z**4 * log(m.y)) * m.y ) From ae2c5ab44f884dfe637321e4ab07849f5d9d1ce0 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Thu, 2 May 2024 17:46:02 -0600 Subject: [PATCH 1312/3044] More parmest cleanup --- pyomo/contrib/parmest/parmest.py | 2 +- pyomo/contrib/parmest/scenariocreator.py | 7 +--- pyomo/contrib/parmest/tests/test_examples.py | 5 ++- pyomo/contrib/parmest/tests/test_utils.py | 36 ++++++++------------ pyomo/contrib/parmest/utils/model_utils.py | 14 ++++++++ 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 2d4c323b9b8..105419dcb13 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -14,7 +14,6 @@ #### Redesign with Experiment class Dec 2023 # TODO: move use_mpisppy to a Pyomo configuration option - # False implies always use the EF that is local to parmest use_mpisppy = True # Use it if we can but use local if not. if use_mpisppy: @@ -1194,6 +1193,7 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): ], "Theta name {} in 'theta_values' not in 'theta_names' {}".format( theta_temp, model_theta_list ) + assert len(list(theta_names)) == len(model_theta_list) all_thetas = theta_values.to_dict('records') diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 2208bde91a0..7988cfa3f5f 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -169,12 +169,7 @@ def ScenariosFromExperiments(self, addtoSet): opt = pyo.SolverFactory(self.solvername) results = opt.solve(model) # solves and updates model ## pyo.check_termination_optimal(results) - ThetaVals = dict() - for theta in self.pest.theta_names: - tvar = eval('model.' + theta) - tval = pyo.value(tvar) - ##print(" theta, tval=", tvar, tval) - ThetaVals[theta] = tval + ThetaVals = {k.name: pyo.value(k) for k in model.unknown_parameters.keys()} addtoSet.addone(ParmestScen("ExpScen" + str(exp_num), ThetaVals, prob)) def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index 59a3e0adde2..dca05026e80 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -181,7 +181,10 @@ def test_multisensor_data_example(self): multisensor_data_example.main() - @unittest.skipUnless(matplotlib_available, "test requires matplotlib") + @unittest.skipUnless( + matplotlib_available and seaborn_available, + "test requires matplotlib and seaborn", + ) def test_datarec_example(self): from pyomo.contrib.parmest.examples.reactor_design import datarec_example diff --git a/pyomo/contrib/parmest/tests/test_utils.py b/pyomo/contrib/parmest/tests/test_utils.py index 611d67c1abb..d5e66ab58d5 100644 --- a/pyomo/contrib/parmest/tests/test_utils.py +++ b/pyomo/contrib/parmest/tests/test_utils.py @@ -25,18 +25,12 @@ ) @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") class TestUtils(unittest.TestCase): - @classmethod - def setUpClass(self): - pass - @classmethod - def tearDownClass(self): - pass - - @unittest.pytest.mark.expensive def test_convert_param_to_var(self): + # TODO: Check that this works for different structured models (indexed, blocks, etc) + from pyomo.contrib.parmest.examples.reactor_design.reactor_design import ( - reactor_design_model, + ReactorDesignExperiment, ) data = pd.DataFrame( @@ -49,24 +43,22 @@ def test_convert_param_to_var(self): ) # make model - instance = reactor_design_model() - - # add caf, sv - instance.caf = data.iloc[0]['caf'] - instance.sv = data.iloc[0]['sv'] - - solver = pyo.SolverFactory("ipopt") - solver.solve(instance) + exp = ReactorDesignExperiment(data, 0) + instance = exp.get_labeled_model() theta_names = ['k1', 'k2', 'k3'] - instance_vars = parmest.utils.convert_params_to_vars( + m_vars = parmest.utils.convert_params_to_vars( instance, theta_names, fix_vars=True ) - solver.solve(instance_vars) - assert instance.k1() == instance_vars.k1() - assert instance.k2() == instance_vars.k2() - assert instance.k3() == instance_vars.k3() + for v in theta_names: + self.assertTrue(hasattr(m_vars, v)) + c = m_vars.find_component(v) + self.assertIsInstance(c, pyo.Var) + self.assertTrue(c.fixed) + c_old = instance.find_component(v) + self.assertEqual(pyo.value(c), pyo.value(c_old)) + self.assertTrue(c in m_vars.unknown_parameters) if __name__ == "__main__": diff --git a/pyomo/contrib/parmest/utils/model_utils.py b/pyomo/contrib/parmest/utils/model_utils.py index 77491f74b02..7778ebcc9f1 100644 --- a/pyomo/contrib/parmest/utils/model_utils.py +++ b/pyomo/contrib/parmest/utils/model_utils.py @@ -15,6 +15,7 @@ from pyomo.core.expr import replace_expressions, identify_mutable_parameters from pyomo.core.base.var import IndexedVar from pyomo.core.base.param import IndexedParam +from pyomo.common.collections import ComponentMap from pyomo.environ import ComponentUID @@ -49,6 +50,7 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): # Convert Params to Vars, unfix Vars, and create a substitution map substitution_map = {} + comp_map = ComponentMap() for i, param_name in enumerate(param_names): # Leverage the parser in ComponentUID to locate the component. theta_cuid = ComponentUID(param_name) @@ -65,6 +67,7 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): theta_var_cuid = ComponentUID(theta_object.name) theta_var_object = theta_var_cuid.find_component_on(model) substitution_map[id(theta_object)] = theta_var_object + comp_map[theta_object] = theta_var_object # Indexed Param elif isinstance(theta_object, IndexedParam): @@ -90,6 +93,7 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): # Update substitution map (map each indexed param to indexed var) theta_var_cuid = ComponentUID(theta_object.name) theta_var_object = theta_var_cuid.find_component_on(model) + comp_map[theta_object] = theta_var_object var_theta_objects = [] for theta_obj in theta_var_object: theta_cuid = ComponentUID( @@ -101,6 +105,7 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): param_theta_objects, var_theta_objects ): substitution_map[id(param_theta_obj)] = var_theta_obj + comp_map[param_theta_obj] = var_theta_obj # Var or Indexed Var elif isinstance(theta_object, IndexedVar) or theta_object.is_variable_type(): @@ -182,6 +187,15 @@ def convert_params_to_vars(model, param_names=None, fix_vars=False): model.del_component(obj) model.add_component(obj.name, pyo.Objective(rule=expr, sense=obj.sense)) + # Convert Params to Vars in Suffixes + for s in model.component_objects(pyo.Suffix): + current_keys = list(s.keys()) + for c in current_keys: + if c in comp_map: + s[comp_map[c]] = s.pop(c) + + assert len(current_keys) == len(s.keys()) + # print('--- Updated Model ---') # model.pprint() # solver = pyo.SolverFactory('ipopt') From 88c12276c2d314eb01b3e0569ba5efbb29e41cf3 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Thu, 2 May 2024 18:09:32 -0600 Subject: [PATCH 1313/3044] Remove fixed TODO and fix typo in parmest test --- pyomo/contrib/parmest/parmest.py | 2 -- pyomo/contrib/parmest/tests/test_parmest.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 105419dcb13..41e1724f94f 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -1165,8 +1165,6 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): # create a local instance of the pyomo model to access model variables and parameters model_temp = self._create_parmest_model(0) model_theta_list = self._expand_indexed_unknowns(model_temp) - # TODO: check if model_theta_list is correct if original unknown parameters - # are declared as params and transformed to vars during call to create_parmest_model # if self.estimator_theta_names is not the same as temp model_theta_list, # create self.theta_names_updated diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 0590f165da3..5f288154dcd 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -135,7 +135,7 @@ def test_likelihood_ratio(self): asym = np.arange(10, 30, 2) rate = np.arange(0, 1.5, 0.25) theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=self.pest.theta_names + list(product(asym, rate)), columns=self.pest.estimator_theta_names ) obj_at_theta = self.pest.objective_at_theta(theta_vals) From 9cd9986227f23149dc814d731725da87cb6f958d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 11:51:52 -0600 Subject: [PATCH 1314/3044] Resolve (and test) error in RenamedClass when derived classes have multiple bases --- pyomo/common/deprecation.py | 2 +- pyomo/common/tests/test_deprecated.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/common/deprecation.py b/pyomo/common/deprecation.py index 5a6ca456079..c674dcddc78 100644 --- a/pyomo/common/deprecation.py +++ b/pyomo/common/deprecation.py @@ -542,7 +542,7 @@ def __renamed__warning__(msg): if new_class is None and '__renamed__new_class__' not in classdict: if not any( - hasattr(base, '__renamed__new_class__') + hasattr(mro, '__renamed__new_class__') for mro in itertools.chain.from_iterable( base.__mro__ for base in renamed_bases ) diff --git a/pyomo/common/tests/test_deprecated.py b/pyomo/common/tests/test_deprecated.py index 377e229c775..37e1ba81bb3 100644 --- a/pyomo/common/tests/test_deprecated.py +++ b/pyomo/common/tests/test_deprecated.py @@ -529,7 +529,10 @@ class DeprecatedClassSubclass(DeprecatedClass): out = StringIO() with LoggingIntercept(out): - class DeprecatedClassSubSubclass(DeprecatedClassSubclass): + class otherClass: + pass + + class DeprecatedClassSubSubclass(DeprecatedClassSubclass, otherClass): attr = 'DeprecatedClassSubSubclass' self.assertEqual(out.getvalue(), "") From 79e2e3650d49fb4d29bb65cd39a4edbe35b6835b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 11:53:34 -0600 Subject: [PATCH 1315/3044] Resolve backwards compatibility from renaming / removing pyomo_constant_types import --- pyomo/core/expr/numvalue.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index b656eea1bcd..3b335bd5fc4 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -44,6 +44,16 @@ "be treated as if they were bool (as was the case for the other " "native_*_types sets). Users likely should use native_logical_types.", ) +relocated_module_attribute( + 'pyomo_constant_types', + 'pyomo.common.numeric_types._pyomo_constant_types', + version='6.7.2.dev0', + f_globals=globals(), + msg="The pyomo_constant_types set will be removed in the future: the set " + "contained only NumericConstant and _PythonCallbackFunctionID, and provided " + "no meaningful value to clients or walkers. Users should likely handle " + "these types in the same manner as immutable Params.", +) relocated_module_attribute( 'RegisterNumericType', 'pyomo.common.numeric_types.RegisterNumericType', From 2d818adbfc0629d07a1111f66accd90d543eacdf Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 11:54:15 -0600 Subject: [PATCH 1316/3044] Overhaul declare_custom_block to avoid using metaclasses --- pyomo/core/base/block.py | 87 ++++++++++++++--------------- pyomo/core/tests/unit/test_block.py | 31 ++++++++++ 2 files changed, 73 insertions(+), 45 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 3eb18dde7a9..a27ca81bbfd 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2333,48 +2333,42 @@ def components_data(block, ctype, sort=None, sort_by_keys=False, sort_by_names=F BlockData._Block_reserved_words = set(dir(Block())) -class _IndexedCustomBlockMeta(type): - """Metaclass for creating an indexed custom block.""" - - pass - - -class _ScalarCustomBlockMeta(type): - """Metaclass for creating a scalar custom block.""" - - def __new__(meta, name, bases, dct): - def __init__(self, *args, **kwargs): - # bases[0] is the custom block data object - bases[0].__init__(self, component=self) - # bases[1] is the custom block object that - # is used for declaration - bases[1].__init__(self, *args, **kwargs) - - dct["__init__"] = __init__ - return type.__new__(meta, name, bases, dct) +class ScalarCustomBlockMixin(object): + def __init__(self, *args, **kwargs): + # __bases__ for the ScalarCustomBlock is + # + # (ScalarCustomBlockMixin, {custom_data}, {custom_block}) + # + # Unfortunately, we cannot guarantee that this is being called + # from the ScalarCustomBlock (someone could have inherited from + # that class to make another scalar class). We will walk up the + # MRO to find the Scalar class (which should be the only class + # that has this Mixin as the first base class) + for cls in self.__class__.__mro__: + if cls.__bases__[0] is ScalarCustomBlockMixin: + _mixin, _data, _block = cls.__bases__ + _data.__init__(self, component=self) + _block.__init__(self, *args, **kwargs) + break class CustomBlock(Block): """The base class used by instances of custom block components""" - def __init__(self, *args, **kwds): + def __init__(self, *args, **kwargs): if self._default_ctype is not None: - kwds.setdefault('ctype', self._default_ctype) - Block.__init__(self, *args, **kwds) + kwargs.setdefault('ctype', self._default_ctype) + Block.__init__(self, *args, **kwargs) - def __new__(cls, *args, **kwds): - if cls.__name__.startswith('_Indexed') or cls.__name__.startswith('_Scalar'): + def __new__(cls, *args, **kwargs): + if cls.__bases__[0] is not CustomBlock: # we are entering here the second time (recursive) # therefore, we need to create what we have - return super(CustomBlock, cls).__new__(cls) + return super().__new__(cls, *args, **kwargs) if not args or (args[0] is UnindexedComponent_set and len(args) == 1): - n = _ScalarCustomBlockMeta( - "_Scalar%s" % (cls.__name__,), (cls._ComponentDataClass, cls), {} - ) - return n.__new__(n) + return super().__new__(cls._scalar_custom_block, *args, **kwargs) else: - n = _IndexedCustomBlockMeta("_Indexed%s" % (cls.__name__,), (cls,), {}) - return n.__new__(n) + return super().__new__(cls._indexed_custom_block, *args, **kwargs) def declare_custom_block(name, new_ctype=None): @@ -2386,9 +2380,9 @@ def declare_custom_block(name, new_ctype=None): ... pass """ - def proc_dec(cls): - # this is the decorator function that - # creates the block component class + def block_data_decorator(cls): + # this is the decorator function that creates the block + # component classes # Default (derived) Block attributes clsbody = { @@ -2399,7 +2393,7 @@ def proc_dec(cls): "_default_ctype": None, } - c = type( + c = type(CustomBlock)( name, # name of new class (CustomBlock,), # base classes clsbody, # class body definitions (will populate __dict__) @@ -2408,7 +2402,7 @@ def proc_dec(cls): if new_ctype is not None: if new_ctype is True: c._default_ctype = c - elif type(new_ctype) is type: + elif isinstance(new_ctype, type): c._default_ctype = new_ctype else: raise ValueError( @@ -2416,15 +2410,18 @@ def proc_dec(cls): "or 'True'; received: %s" % (new_ctype,) ) - # Register the new Block type in the same module as the BlockData - setattr(sys.modules[cls.__module__], name, c) - # TODO: can we also register concrete Indexed* and Scalar* - # classes into the original BlockData module (instead of relying - # on metaclasses)? + # Declare Indexed and Scalar versions of the custom blocks. We + # will register them both with the calling module scope, and + # with the CustomBlock (so that __new__ can route the object + # creation to the correct class) + c._indexed_custom_block = type(c)("Indexed" + name, (c,), {}) + c._scalar_custom_block = type(c)( + "Scalar" + name, (ScalarCustomBlockMixin, cls, c), {} + ) - # are these necessary? - setattr(cls, '_orig_name', name) - setattr(cls, '_orig_module', cls.__module__) + # Register the new Block types in the same module as the BlockData + for _cls in (c, c._indexed_custom_block, c._scalar_custom_block): + setattr(sys.modules[cls.__module__], _cls.__name__, _cls) return cls - return proc_dec + return block_data_decorator diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index 660f65f1944..33d6d2c8adc 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -13,6 +13,7 @@ # from io import StringIO +import logging import os import sys import types @@ -2975,6 +2976,36 @@ def test_write_exceptions(self): with self.assertRaisesRegex(ValueError, ".*Cannot write model in format"): m.write(format="bogus") + def test_custom_block(self): + @declare_custom_block('TestingBlock') + class TestingBlockData(BlockData): + def __init__(self, component): + BlockData.__init__(self, component) + logging.getLogger(__name__).warning("TestingBlockData.__init__") + + self.assertIn('TestingBlock', globals()) + self.assertIn('ScalarTestingBlock', globals()) + self.assertIn('IndexedTestingBlock', globals()) + + with LoggingIntercept() as LOG: + obj = TestingBlock() + self.assertIs(type(obj), ScalarTestingBlock) + self.assertEqual(LOG.getvalue().strip(), "TestingBlockData.__init__") + + with LoggingIntercept() as LOG: + obj = TestingBlock([1, 2]) + self.assertIs(type(obj), IndexedTestingBlock) + self.assertEqual(LOG.getvalue(), "") + + # Test that we can derive from a ScalarCustomBlock + class DerivedScalarTstingBlock(ScalarTestingBlock): + pass + + with LoggingIntercept() as LOG: + obj = DerivedScalarTstingBlock() + self.assertIs(type(obj), DerivedScalarTstingBlock) + self.assertEqual(LOG.getvalue().strip(), "TestingBlockData.__init__") + def test_override_pprint(self): @declare_custom_block('TempBlock') class TempBlockData(BlockData): From e4d26b3e37d2f074e58d5e272ea60a2c1604fada Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 11:55:00 -0600 Subject: [PATCH 1317/3044] NFC: clarify comment --- pyomo/core/base/block.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index a27ca81bbfd..5513d405f4d 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2412,8 +2412,8 @@ def block_data_decorator(cls): # Declare Indexed and Scalar versions of the custom blocks. We # will register them both with the calling module scope, and - # with the CustomBlock (so that __new__ can route the object - # creation to the correct class) + # with the CustomBlock (so that CustomBlock.__new__ can route + # the object creation to the correct class) c._indexed_custom_block = type(c)("Indexed" + name, (c,), {}) c._scalar_custom_block = type(c)( "Scalar" + name, (ScalarCustomBlockMixin, cls, c), {} From cff93a17664803915b0d0ea4d3bdf9f675a3a1d7 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 3 May 2024 14:11:47 -0600 Subject: [PATCH 1318/3044] Fixing non deterministic fragile test in parmest --- pyomo/contrib/parmest/tests/test_parmest.py | 23 ++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 5f288154dcd..69155dadb45 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -118,9 +118,9 @@ def test_bootstrap(self): CR = self.pest.confidence_region_test(theta_est, "MVN", [0.5, 0.75, 1.0]) self.assertTrue(set(CR.columns) >= set([0.5, 0.75, 1.0])) - self.assertTrue(CR[0.5].sum() == 5) - self.assertTrue(CR[0.75].sum() == 7) - self.assertTrue(CR[1.0].sum() == 10) # all true + self.assertEqual(CR[0.5].sum(), 5) + self.assertEqual(CR[0.75].sum(), 7) + self.assertEqual(CR[1.0].sum(), 10) # all true graphics.pairwise_plot(theta_est) graphics.pairwise_plot(theta_est, thetavals) @@ -135,17 +135,16 @@ def test_likelihood_ratio(self): asym = np.arange(10, 30, 2) rate = np.arange(0, 1.5, 0.25) theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=self.pest.estimator_theta_names + list(product(asym, rate)), columns=['asymptote', 'rate_constant'] ) - obj_at_theta = self.pest.objective_at_theta(theta_vals) LR = self.pest.likelihood_ratio_test(obj_at_theta, objval, [0.8, 0.9, 1.0]) self.assertTrue(set(LR.columns) >= set([0.8, 0.9, 1.0])) - self.assertTrue(LR[0.8].sum() == 6) - self.assertTrue(LR[0.9].sum() == 10) - self.assertTrue(LR[1.0].sum() == 60) # all true + self.assertEqual(LR[0.8].sum(), 6) + self.assertEqual(LR[0.9].sum(), 10) + self.assertEqual(LR[1.0].sum(), 60) # all true graphics.pairwise_plot(LR, thetavals, 0.8) @@ -164,9 +163,9 @@ def test_leaveNout(self): self.assertTrue(samples == [1]) # sample 1 was left out self.assertTrue(lno_theta.shape[0] == 1) # lno estimate for sample 1 self.assertTrue(set(lno_theta.columns) >= set([0.5, 1.0])) - self.assertTrue(lno_theta[1.0].sum() == 1) # all true - self.assertTrue(bootstrap_theta.shape[0] == 3) # bootstrap for sample 1 - self.assertTrue(bootstrap_theta[1.0].sum() == 3) # all true + self.assertEqual(lno_theta[1.0].sum(), 1) # all true + self.assertEqual(bootstrap_theta.shape[0], 3) # bootstrap for sample 1 + self.assertEqual(bootstrap_theta[1.0].sum(), 3) # all true def test_diagnostic_mode(self): self.pest.diagnostic_mode = True @@ -176,7 +175,7 @@ def test_diagnostic_mode(self): asym = np.arange(10, 30, 2) rate = np.arange(0, 1.5, 0.25) theta_vals = pd.DataFrame( - list(product(asym, rate)), columns=self.pest.estimator_theta_names + list(product(asym, rate)), columns=['asymptote', 'rate_constant'] ) obj_at_theta = self.pest.objective_at_theta(theta_vals) From cefb6d84a0a117ecb198c682b00465a924c00163 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 14:30:18 -0600 Subject: [PATCH 1319/3044] Fixing typo in class name --- pyomo/core/tests/unit/test_block.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index 33d6d2c8adc..063db0e428e 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -2998,12 +2998,12 @@ def __init__(self, component): self.assertEqual(LOG.getvalue(), "") # Test that we can derive from a ScalarCustomBlock - class DerivedScalarTstingBlock(ScalarTestingBlock): + class DerivedScalarTestingBlock(ScalarTestingBlock): pass with LoggingIntercept() as LOG: - obj = DerivedScalarTstingBlock() - self.assertIs(type(obj), DerivedScalarTstingBlock) + obj = DerivedScalarTestingBlock() + self.assertIs(type(obj), DerivedScalarTestingBlock) self.assertEqual(LOG.getvalue().strip(), "TestingBlockData.__init__") def test_override_pprint(self): From e96b382392eef69a5feac0d85c6593486e7e2d42 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 3 May 2024 15:11:13 -0600 Subject: [PATCH 1320/3044] Try relaxing gurobipy version --- .github/workflows/test_branches.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 75db5d66431..3396eca0176 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -111,6 +111,14 @@ jobs: TARGET: win PYENV: pip + - os: ubuntu-latest + python: '3.11' + other: /singletest + category: "-m 'neos or importtest'" + skip_doctest: 1 + TARGET: linux + PYENV: pip + steps: - name: Checkout Pyomo source uses: actions/checkout@v4 @@ -265,7 +273,7 @@ jobs: python -m pip install --cache-dir cache/pip cplex docplex \ || echo "WARNING: CPLEX Community Edition is not available" python -m pip install --cache-dir cache/pip \ - -i https://pypi.gurobi.com gurobipy==10.0.3 \ + -i https://pypi.gurobi.com gurobipy \ || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" From 151c4aa10364be25deb2427950c0ead38cc549eb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 15:24:01 -0600 Subject: [PATCH 1321/3044] Ensure all custom block classes are assigned to the module scope --- pyomo/core/base/block.py | 22 ++++++++++++++++------ pyomo/core/tests/unit/test_block.py | 3 +++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 5513d405f4d..376ed30e1dd 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2380,13 +2380,13 @@ def declare_custom_block(name, new_ctype=None): ... pass """ - def block_data_decorator(cls): + def block_data_decorator(block_data): # this is the decorator function that creates the block # component classes # Default (derived) Block attributes clsbody = { - "__module__": cls.__module__, # magic to fix the module + "__module__": block_data.__module__, # magic to fix the module # Default IndexedComponent data object is the decorated class: "_ComponentDataClass": cls, # By default this new block does not declare a new ctype @@ -2414,14 +2414,24 @@ def block_data_decorator(cls): # will register them both with the calling module scope, and # with the CustomBlock (so that CustomBlock.__new__ can route # the object creation to the correct class) - c._indexed_custom_block = type(c)("Indexed" + name, (c,), {}) + c._indexed_custom_block = type(c)( + "Indexed" + name, + (c,), + { # ensure the created class is associated with the calling module + "__module__": block_data.__module__ + }, + ) c._scalar_custom_block = type(c)( - "Scalar" + name, (ScalarCustomBlockMixin, cls, c), {} + "Scalar" + name, + (ScalarCustomBlockMixin, block_data, c), + { # ensure the created class is associated with the calling module + "__module__": block_data.__module__ + }, ) # Register the new Block types in the same module as the BlockData for _cls in (c, c._indexed_custom_block, c._scalar_custom_block): - setattr(sys.modules[cls.__module__], _cls.__name__, _cls) - return cls + setattr(sys.modules[block_data.__module__], _cls.__name__, _cls) + return block_data return block_data_decorator diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index 063db0e428e..bf4a5d58636 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -2986,6 +2986,9 @@ def __init__(self, component): self.assertIn('TestingBlock', globals()) self.assertIn('ScalarTestingBlock', globals()) self.assertIn('IndexedTestingBlock', globals()) + self.assertIs(TestingBlock.__module__, __name__) + self.assertIs(ScalarTestingBlock.__module__, __name__) + self.assertIs(IndexedTestingBlock.__module__, __name__) with LoggingIntercept() as LOG: obj = TestingBlock() From 04a9708e169b6f417d6a470b14b6cb38b9d756b3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 15:24:26 -0600 Subject: [PATCH 1322/3044] Improve documentation --- pyomo/core/base/block.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 376ed30e1dd..72d66c67c60 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2362,9 +2362,16 @@ def __init__(self, *args, **kwargs): def __new__(cls, *args, **kwargs): if cls.__bases__[0] is not CustomBlock: - # we are entering here the second time (recursive) - # therefore, we need to create what we have + # we are creating a class other than the "generic" derived + # custom block class. We can assume that the routing of the + # generic block class to the specific Scalar or Indexed + # subclass has already occurred and we can pass control up + # to (toward) object.__new__() return super().__new__(cls, *args, **kwargs) + # If the first base class is this CustomBlock class, then the + # user is attempting to create the "generic" block class. + # Depending on the arguments, we need to map this to either the + # Scalar or Indexed block subclass. if not args or (args[0] is UnindexedComponent_set and len(args) == 1): return super().__new__(cls._scalar_custom_block, *args, **kwargs) else: @@ -2374,7 +2381,7 @@ def __new__(cls, *args, **kwargs): def declare_custom_block(name, new_ctype=None): """Decorator to declare components for a custom block data class - >>> @declare_custom_block(name=FooBlock) + >>> @declare_custom_block(name="FooBlock") ... class FooBlockData(BlockData): ... # custom block data class ... pass @@ -2384,19 +2391,24 @@ def block_data_decorator(block_data): # this is the decorator function that creates the block # component classes - # Default (derived) Block attributes - clsbody = { - "__module__": block_data.__module__, # magic to fix the module - # Default IndexedComponent data object is the decorated class: - "_ComponentDataClass": cls, - # By default this new block does not declare a new ctype - "_default_ctype": None, - } - + # Declare the new Block (derived from CustomBlock) corresponding + # to the BlockData that we are decorating + # + # Note the use of `type(CustomBlock)` to pick up the metaclass + # that was used to create the CustomBlock (in general, it should + # be `type`) c = type(CustomBlock)( name, # name of new class (CustomBlock,), # base classes - clsbody, # class body definitions (will populate __dict__) + # class body definitions (populate the new class' __dict__) + { + # ensure the created class is associated with the calling module + "__module__": block_data.__module__, + # Default IndexedComponent data object is the decorated class: + "_ComponentDataClass": block_data, + # By default this new block does not declare a new ctype + "_default_ctype": None, + }, ) if new_ctype is not None: @@ -2410,7 +2422,7 @@ def block_data_decorator(block_data): "or 'True'; received: %s" % (new_ctype,) ) - # Declare Indexed and Scalar versions of the custom blocks. We + # Declare Indexed and Scalar versions of the custom block. We # will register them both with the calling module scope, and # with the CustomBlock (so that CustomBlock.__new__ can route # the object creation to the correct class) From a934e9f86540058d9a81a0989432458cb702521f Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 3 May 2024 15:28:53 -0600 Subject: [PATCH 1323/3044] Try grabbing gurobipy straight from pypi --- .github/workflows/test_branches.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 3396eca0176..dc0bdecff18 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -272,8 +272,7 @@ jobs: if test -z "${{matrix.slim}}"; then python -m pip install --cache-dir cache/pip cplex docplex \ || echo "WARNING: CPLEX Community Edition is not available" - python -m pip install --cache-dir cache/pip \ - -i https://pypi.gurobi.com gurobipy \ + python -m pip install --cache-dir cache/pip gurobipy\ || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" From fea6ca8f6cd62491acedb89c130e7749a26693b7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 15:34:55 -0600 Subject: [PATCH 1324/3044] Improve variable naming --- pyomo/core/base/block.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 72d66c67c60..26f2d7071b1 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -2391,13 +2391,13 @@ def block_data_decorator(block_data): # this is the decorator function that creates the block # component classes - # Declare the new Block (derived from CustomBlock) corresponding - # to the BlockData that we are decorating + # Declare the new Block component (derived from CustomBlock) + # corresponding to the BlockData that we are decorating # # Note the use of `type(CustomBlock)` to pick up the metaclass # that was used to create the CustomBlock (in general, it should # be `type`) - c = type(CustomBlock)( + comp = type(CustomBlock)( name, # name of new class (CustomBlock,), # base classes # class body definitions (populate the new class' __dict__) @@ -2413,9 +2413,9 @@ def block_data_decorator(block_data): if new_ctype is not None: if new_ctype is True: - c._default_ctype = c + comp._default_ctype = comp elif isinstance(new_ctype, type): - c._default_ctype = new_ctype + comp._default_ctype = new_ctype else: raise ValueError( "Expected new_ctype to be either type " @@ -2426,23 +2426,23 @@ def block_data_decorator(block_data): # will register them both with the calling module scope, and # with the CustomBlock (so that CustomBlock.__new__ can route # the object creation to the correct class) - c._indexed_custom_block = type(c)( + comp._indexed_custom_block = type(comp)( "Indexed" + name, - (c,), + (comp,), { # ensure the created class is associated with the calling module "__module__": block_data.__module__ }, ) - c._scalar_custom_block = type(c)( + comp._scalar_custom_block = type(comp)( "Scalar" + name, - (ScalarCustomBlockMixin, block_data, c), + (ScalarCustomBlockMixin, block_data, comp), { # ensure the created class is associated with the calling module "__module__": block_data.__module__ }, ) # Register the new Block types in the same module as the BlockData - for _cls in (c, c._indexed_custom_block, c._scalar_custom_block): + for _cls in (comp, comp._indexed_custom_block, comp._scalar_custom_block): setattr(sys.modules[block_data.__module__], _cls.__name__, _cls) return block_data From 6d94224f965fa1415f8da78da974d50df9d58046 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 3 May 2024 15:56:03 -0600 Subject: [PATCH 1325/3044] Repinning gurobipy version --- .github/workflows/test_branches.yml | 10 +--------- .github/workflows/test_pr_and_main.yml | 3 +-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index dc0bdecff18..611875fb456 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -111,14 +111,6 @@ jobs: TARGET: win PYENV: pip - - os: ubuntu-latest - python: '3.11' - other: /singletest - category: "-m 'neos or importtest'" - skip_doctest: 1 - TARGET: linux - PYENV: pip - steps: - name: Checkout Pyomo source uses: actions/checkout@v4 @@ -272,7 +264,7 @@ jobs: if test -z "${{matrix.slim}}"; then python -m pip install --cache-dir cache/pip cplex docplex \ || echo "WARNING: CPLEX Community Edition is not available" - python -m pip install --cache-dir cache/pip gurobipy\ + python -m pip install --cache-dir cache/pip gurobipy==10.0.3\ || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 5a484dccbc8..5b1bca70ede 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -301,8 +301,7 @@ jobs: if test -z "${{matrix.slim}}"; then python -m pip install --cache-dir cache/pip cplex docplex \ || echo "WARNING: CPLEX Community Edition is not available" - python -m pip install --cache-dir cache/pip \ - -i https://pypi.gurobi.com gurobipy==10.0.3 \ + python -m pip install --cache-dir cache/pip gurobipy==10.0.3 \ || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" From 97b3b0a9efda63e1021d038de1430cd021692bc7 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 3 May 2024 17:16:21 -0600 Subject: [PATCH 1326/3044] Minor edits to scenariocreator in parmest --- pyomo/contrib/parmest/scenariocreator.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 7988cfa3f5f..1729c6e6c72 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -14,11 +14,6 @@ import pyomo.environ as pyo -from pyomo.common.deprecation import deprecated -from pyomo.common.deprecation import deprecation_warning - -DEPRECATION_VERSION = '6.7.2.dev0' - import logging logger = logging.getLogger(__name__) @@ -129,14 +124,9 @@ class ScenarioCreator(object): def __init__(self, pest, solvername): - # is this a deprecated pest object? + # Check if we're using the deprecated parmest API self.scen_deprecated = None if pest.pest_deprecated is not None: - deprecation_warning( - "Using a deprecated parmest object for scenario " - + "creator, please recreate object using experiment lists.", - version=DEPRECATION_VERSION, - ) self.scen_deprecated = _ScenarioCreatorDeprecated( pest.pest_deprecated, solvername ) @@ -218,7 +208,7 @@ def ScenariosFromExperiments(self, addtoSet): a ScenarioSet """ - # assert isinstance(addtoSet, ScenarioSet) + assert isinstance(addtoSet, ScenarioSet) scenario_numbers = list(range(len(self.pest.callback_data))) @@ -247,7 +237,7 @@ def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): numtomake (int) : number of scenarios to create """ - # assert isinstance(addtoSet, ScenarioSet) + assert isinstance(addtoSet, ScenarioSet) bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) addtoSet.append_bootstrap(bootstrap_thetas) From ff26f636f2d312505bd67822efbf7504d38b6445 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 3 May 2024 17:48:00 -0600 Subject: [PATCH 1327/3044] Simplify scenariocreator code in parmest to remove duplication --- pyomo/contrib/parmest/scenariocreator.py | 108 ++++++----------------- 1 file changed, 25 insertions(+), 83 deletions(-) diff --git a/pyomo/contrib/parmest/scenariocreator.py b/pyomo/contrib/parmest/scenariocreator.py index 1729c6e6c72..e887dd2e8be 100644 --- a/pyomo/contrib/parmest/scenariocreator.py +++ b/pyomo/contrib/parmest/scenariocreator.py @@ -124,78 +124,6 @@ class ScenarioCreator(object): def __init__(self, pest, solvername): - # Check if we're using the deprecated parmest API - self.scen_deprecated = None - if pest.pest_deprecated is not None: - self.scen_deprecated = _ScenarioCreatorDeprecated( - pest.pest_deprecated, solvername - ) - else: - self.pest = pest - self.solvername = solvername - - def ScenariosFromExperiments(self, addtoSet): - """Creates new self.Scenarios list using the experiments only. - - Args: - addtoSet (ScenarioSet): the scenarios will be added to this set - Returns: - a ScenarioSet - """ - - # check if using deprecated pest object - if self.scen_deprecated is not None: - self.scen_deprecated.ScenariosFromExperiments(addtoSet) - return - - assert isinstance(addtoSet, ScenarioSet) - - scenario_numbers = list(range(len(self.pest.exp_list))) - - prob = 1.0 / len(scenario_numbers) - for exp_num in scenario_numbers: - ##print("Experiment number=", exp_num) - model = self.pest._instance_creation_callback(exp_num) - opt = pyo.SolverFactory(self.solvername) - results = opt.solve(model) # solves and updates model - ## pyo.check_termination_optimal(results) - ThetaVals = {k.name: pyo.value(k) for k in model.unknown_parameters.keys()} - addtoSet.addone(ParmestScen("ExpScen" + str(exp_num), ThetaVals, prob)) - - def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): - """Creates new self.Scenarios list using the experiments only. - - Args: - addtoSet (ScenarioSet): the scenarios will be added to this set - numtomake (int) : number of scenarios to create - """ - - # check if using deprecated pest object - if self.scen_deprecated is not None: - self.scen_deprecated.ScenariosFromBootstrap(addtoSet, numtomake, seed=seed) - return - - assert isinstance(addtoSet, ScenarioSet) - - bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) - addtoSet.append_bootstrap(bootstrap_thetas) - - -################################ -# deprecated functions/classes # -################################ - - -class _ScenarioCreatorDeprecated(object): - """Create scenarios from parmest. - - Args: - pest (Estimator): the parmest object - solvername (str): name of the solver (e.g. "ipopt") - - """ - - def __init__(self, pest, solvername): self.pest = pest self.solvername = solvername @@ -210,23 +138,32 @@ def ScenariosFromExperiments(self, addtoSet): assert isinstance(addtoSet, ScenarioSet) - scenario_numbers = list(range(len(self.pest.callback_data))) + if self.pest.pest_deprecated is not None: + scenario_numbers = list(range(len(self.pest.pest_deprecated.callback_data))) + else: + scenario_numbers = list(range(len(self.pest.exp_list))) prob = 1.0 / len(scenario_numbers) for exp_num in scenario_numbers: ##print("Experiment number=", exp_num) - model = self.pest._instance_creation_callback( - exp_num, self.pest.callback_data - ) + if self.pest.pest_deprecated is not None: + model = self.pest.pest_deprecated._instance_creation_callback( + exp_num, self.pest.pest_deprecated.callback_data + ) + else: + model = self.pest._instance_creation_callback(exp_num) opt = pyo.SolverFactory(self.solvername) results = opt.solve(model) # solves and updates model ## pyo.check_termination_optimal(results) - ThetaVals = dict() - for theta in self.pest.theta_names: - tvar = eval('model.' + theta) - tval = pyo.value(tvar) - ##print(" theta, tval=", tvar, tval) - ThetaVals[theta] = tval + if self.pest.pest_deprecated is not None: + ThetaVals = { + theta: pyo.value(model.find_component(theta)) + for theta in self.pest.pest_deprecated.theta_names + } + else: + ThetaVals = { + k.name: pyo.value(k) for k in model.unknown_parameters.keys() + } addtoSet.addone(ParmestScen("ExpScen" + str(exp_num), ThetaVals, prob)) def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): @@ -239,5 +176,10 @@ def ScenariosFromBootstrap(self, addtoSet, numtomake, seed=None): assert isinstance(addtoSet, ScenarioSet) - bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) + if self.pest.pest_deprecated is not None: + bootstrap_thetas = self.pest.pest_deprecated.theta_est_bootstrap( + numtomake, seed=seed + ) + else: + bootstrap_thetas = self.pest.theta_est_bootstrap(numtomake, seed=seed) addtoSet.append_bootstrap(bootstrap_thetas) From e13edeea1401b4d105c795cff610557321291c95 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 3 May 2024 22:14:07 -0600 Subject: [PATCH 1328/3044] Test ctype management in declare_custom_block() --- pyomo/core/tests/unit/test_block.py | 30 ++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/pyomo/core/tests/unit/test_block.py b/pyomo/core/tests/unit/test_block.py index bf4a5d58636..3d578f7dc88 100644 --- a/pyomo/core/tests/unit/test_block.py +++ b/pyomo/core/tests/unit/test_block.py @@ -3009,7 +3009,35 @@ class DerivedScalarTestingBlock(ScalarTestingBlock): self.assertIs(type(obj), DerivedScalarTestingBlock) self.assertEqual(LOG.getvalue().strip(), "TestingBlockData.__init__") - def test_override_pprint(self): + def test_custom_block_ctypes(self): + @declare_custom_block('TestingBlock') + class TestingBlockData(BlockData): + pass + + self.assertIs(TestingBlock().ctype, Block) + + @declare_custom_block('TestingBlock', True) + class TestingBlockData(BlockData): + pass + + self.assertIs(TestingBlock().ctype, TestingBlock) + + @declare_custom_block('TestingBlock', Constraint) + class TestingBlockData(BlockData): + pass + + self.assertIs(TestingBlock().ctype, Constraint) + + with self.assertRaisesRegex( + ValueError, + r"Expected new_ctype to be either type or 'True'; received: \[\]", + ): + + @declare_custom_block('TestingBlock', []) + class TestingBlockData(BlockData): + pass + + def test_custom_block_override_pprint(self): @declare_custom_block('TempBlock') class TempBlockData(BlockData): def pprint(self, ostream=None, verbose=False, prefix=""): From 54bd3d393b73d1dcfa6bdec1854b69893eaec679 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 10:08:04 -0600 Subject: [PATCH 1329/3044] Move the ginac interface sources to a subdirectory --- pyomo/contrib/simplification/build.py | 9 ++-- .../contrib/simplification/ginac/__init__.py | 52 +++++++++++++++++++ .../{ => ginac/src}/ginac_interface.cpp | 0 .../{ => ginac/src}/ginac_interface.hpp | 0 pyomo/contrib/simplification/simplify.py | 14 ++--- 5 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 pyomo/contrib/simplification/ginac/__init__.py rename pyomo/contrib/simplification/{ => ginac/src}/ginac_interface.cpp (100%) rename pyomo/contrib/simplification/{ => ginac/src}/ginac_interface.hpp (100%) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index a4094f993fa..0ae883bc55c 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -88,9 +88,10 @@ def build_ginac_interface(parallel=None, args=None): if args is None: args = [] - dname = this_file_dir() - _sources = ['ginac_interface.cpp'] - sources = [os.path.join(dname, fname) for fname in _sources] + sources = [ + os.path.join(this_file_dir(), 'ginac', 'src', fname) + for fname in ['ginac_interface.cpp'] + ] ginac_lib = find_library('ginac') if not ginac_lib: @@ -132,7 +133,7 @@ def run(self): basedir = os.path.abspath(os.path.curdir) with TempfileManager.new_context() as tempfile: if self.inplace: - tmpdir = this_file_dir() + tmpdir = os.path.join(this_file_dir(), 'ginac') else: tmpdir = os.path.abspath(tempfile.mkdtemp()) sys.stdout.write("Building in '%s'" % tmpdir) diff --git a/pyomo/contrib/simplification/ginac/__init__.py b/pyomo/contrib/simplification/ginac/__init__.py new file mode 100644 index 00000000000..af6511944de --- /dev/null +++ b/pyomo/contrib/simplification/ginac/__init__.py @@ -0,0 +1,52 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import attempt_import as _attempt_import + + +def _importer(): + import os + import sys + from ctypes import cdll + from pyomo.common.envvar import PYOMO_CONFIG_DIR + from pyomo.common.fileutils import find_library + + try: + pyomo_config_dir = os.path.join( + PYOMO_CONFIG_DIR, + 'lib', + 'python%s.%s' % sys.version_info[:2], + 'site-packages', + ) + sys.path.insert(0, pyomo_config_dir) + # GiNaC needs 2 libraries that are generally dynamically linked + # to the interface library. If we built those ourselves, then + # the libraries will be PYOMO_CONFIG_DIR/lib ... but that + # directlor is very likely to NOT be on the library search path + # when the Python interpreter was started. We will manually + # look for those two libraries, and if we find them, load them + # into this process (so the interface can find them) + for lib in ('cln', 'ginac'): + fname = find_library(lib) + if fname is not None: + cdll.LoadLibrary(fname) + + import ginac_interface + except ImportError: + from . import ginac_interface + finally: + assert sys.path[0] == pyomo_config_dir + sys.path.pop(0) + + return ginac_interface + + +interface, interface_available = _attempt_import('ginac_interface', importer=_importer) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac/src/ginac_interface.cpp similarity index 100% rename from pyomo/contrib/simplification/ginac_interface.cpp rename to pyomo/contrib/simplification/ginac/src/ginac_interface.cpp diff --git a/pyomo/contrib/simplification/ginac_interface.hpp b/pyomo/contrib/simplification/ginac/src/ginac_interface.hpp similarity index 100% rename from pyomo/contrib/simplification/ginac_interface.hpp rename to pyomo/contrib/simplification/ginac/src/ginac_interface.hpp diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 27da5f5ca34..94f0ceaa33f 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -15,14 +15,10 @@ import logging import warnings -try: - from pyomo.contrib.simplification.ginac_interface import GinacInterface - - ginac_available = True -except: - GinacInterface = None - ginac_available = False - +from pyomo.contrib.simplification.ginac import ( + interface as ginac_interface, + interface_available as ginac_available, +) logger = logging.getLogger(__name__) @@ -51,7 +47,7 @@ def simplify_with_ginac(expr: NumericExpression, ginac_interface): class Simplifier(object): def __init__(self, suppress_no_ginac_warnings: bool = False) -> None: if ginac_available: - self.gi = GinacInterface(False) + self.gi = ginac_interface.GinacInterface(False) self.suppress_no_ginac_warnings = suppress_no_ginac_warnings def simplify(self, expr: NumericExpression): From 8a37c8b1e0a4a54702eb81996d5730a4e55c0da5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 10:08:50 -0600 Subject: [PATCH 1330/3044] Update builder to use argparse, clean up output --- pyomo/contrib/simplification/build.py | 73 +++++++++++++++------------ 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 0ae883bc55c..d540991b010 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -21,12 +21,11 @@ from pyomo.common.fileutils import find_library, this_file_dir from pyomo.common.tempfiles import TempfileManager +logger = logging.getLogger(__name__ if __name__ != '__main__' else 'pyomo') -logger = logging.getLogger(__name__) - -def build_ginac_library(parallel=None, argv=None): - sys.stdout.write("\n**** Building GiNaC library ****") +def build_ginac_library(parallel=None, argv=None, env=None): + sys.stdout.write("\n**** Building GiNaC library ****\n") configure_cmd = ['configure', '--prefix=' + PYOMO_CONFIG_DIR, '--disable-static'] make_cmd = ['make'] @@ -34,6 +33,12 @@ def build_ginac_library(parallel=None, argv=None): make_cmd.append(f'-j{parallel}') install_cmd = ['make', 'install'] + env = dict(os.environ) + pcdir = os.path.join(PYOMO_CONFIG_DIR, 'lib', 'pkgconfig') + if 'PKG_CONFIG_PATH' in env: + pcdir += os.pathsep + env['PKG_CONFIG_PATH'] + env['PKG_CONFIG_PATH'] = pcdir + with TempfileManager.new_context() as tempfile: tmpdir = tempfile.mkdtemp() @@ -49,10 +54,10 @@ def build_ginac_library(parallel=None, argv=None): % (url, downloader.destination()) ) downloader.get_tar_archive(url, dirOffset=1) - assert subprocess.run(configure_cmd, cwd=cln_dir).returncode == 0 + assert subprocess.run(configure_cmd, cwd=cln_dir, env=env).returncode == 0 logger.info("\nBuilding CLN\n") - assert subprocess.run(make_cmd, cwd=cln_dir).returncode == 0 - assert subprocess.run(install_cmd, cwd=cln_dir).returncode == 0 + assert subprocess.run(make_cmd, cwd=cln_dir, env=env).returncode == 0 + assert subprocess.run(install_cmd, cwd=cln_dir, env=env).returncode == 0 url = 'https://www.ginac.de/ginac-1.8.7.tar.bz2' ginac_dir = os.path.join(tmpdir, 'ginac') @@ -62,10 +67,10 @@ def build_ginac_library(parallel=None, argv=None): % (url, downloader.destination()) ) downloader.get_tar_archive(url, dirOffset=1) - assert subprocess.run(configure_cmd, cwd=ginac_dir).returncode == 0 + assert subprocess.run(configure_cmd, cwd=ginac_dir, env=env).returncode == 0 logger.info("\nBuilding GiNaC\n") - assert subprocess.run(make_cmd, cwd=ginac_dir).returncode == 0 - assert subprocess.run(install_cmd, cwd=ginac_dir).returncode == 0 + assert subprocess.run(make_cmd, cwd=ginac_dir, env=env).returncode == 0 + assert subprocess.run(install_cmd, cwd=ginac_dir, env=env).returncode == 0 def _find_include(libdir, incpaths): @@ -84,7 +89,7 @@ def build_ginac_interface(parallel=None, args=None): from pybind11.setup_helpers import Pybind11Extension, build_ext from pyomo.common.cmake_builder import handleReadonly - sys.stdout.write("\n**** Building GiNaC interface ****") + sys.stdout.write("\n**** Building GiNaC interface ****\n") if args is None: args = [] @@ -98,7 +103,7 @@ def build_ginac_interface(parallel=None, args=None): raise RuntimeError( 'could not find the GiNaC library; please make sure either to install ' 'the library and development headers system-wide, or include the ' - 'path tt the library in the LD_LIBRARY_PATH environment variable' + 'path to the library in the LD_LIBRARY_PATH environment variable' ) ginac_lib_dir = os.path.dirname(ginac_lib) ginac_include_dir = _find_include(ginac_lib_dir, ('ginac', 'ginac.h')) @@ -110,7 +115,7 @@ def build_ginac_interface(parallel=None, args=None): raise RuntimeError( 'could not find the CLN library; please make sure either to install ' 'the library and development headers system-wide, or include the ' - 'path tt the library in the LD_LIBRARY_PATH environment variable' + 'path to the library in the LD_LIBRARY_PATH environment variable' ) cln_lib_dir = os.path.dirname(cln_lib) cln_include_dir = _find_include(cln_lib_dir, ('cln', 'cln.h')) @@ -136,7 +141,7 @@ def run(self): tmpdir = os.path.join(this_file_dir(), 'ginac') else: tmpdir = os.path.abspath(tempfile.mkdtemp()) - sys.stdout.write("Building in '%s'" % tmpdir) + sys.stdout.write("Building in '%s'\n" % tmpdir) os.chdir(tmpdir) super(ginacBuildExt, self).run() if not self.inplace: @@ -174,21 +179,25 @@ def skip(self): if __name__ == '__main__': - logging.getLogger('pyomo').setLevel(logging.DEBUG) - parallel = None - for i, arg in enumerate(sys.argv): - if arg == '-j': - parallel = int(sys.argv.pop(i + 1)) - sys.argv.pop(i) - break - if arg.startswith('-j'): - if '=' in arg: - parallel = int(arg.split('=')[1]) - else: - parallel = int(arg[2:]) - sys.argv.pop(i) - break - if '--build-deps' in sys.argv: - sys.argv.remove('--build-deps') - build_ginac_library(parallel, []) - build_ginac_interface(parallel, sys.argv[1:]) + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument( + "-j", + dest='parallel', + type=int, + default=None, + help="Enable parallel build with PARALLEL cores", + ) + parser.add_argument( + "--build-deps", + dest='build_deps', + action='store_true', + default=False, + help="Download and build the CLN/GiNaC libraries", + ) + options, argv = parser.parse_known_args(sys.argv) + logging.getLogger('pyomo').setLevel(logging.INFO) + if options.build_deps: + build_ginac_library(options.parallel, []) + build_ginac_interface(options.parallel, argv[1:]) From c2c63bc52acedace36de75d6ce09c0d062d1ef84 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 10:09:09 -0600 Subject: [PATCH 1331/3044] Run sympy tests any time sympy is installed --- pyomo/contrib/simplification/tests/test_simplification.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index be61631e9f3..27208d42229 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -100,12 +100,12 @@ def test_unary(self): assertExpressionsEqual(self, e, e2) -@unittest.skipIf((not sympy_available) or (ginac_available), 'sympy is not available') +@unittest.skipUnless(sympy_available, 'sympy is not available') class TestSimplificationSympy(TestCase, SimplificationMixin): pass -@unittest.skipIf(not ginac_available, 'GiNaC is not available') +@unittest.skipUnless(ginac_available, 'GiNaC is not available') class TestSimplificationGiNaC(TestCase, SimplificationMixin): def test_param(self): m = pe.ConcreteModel() From b53bbfc67a5e2cdea65008de2b4651670a7df66a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 12:00:31 -0600 Subject: [PATCH 1332/3044] Define NamedIntEnum --- pyomo/common/enums.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index 4d969bf7a9e..121155d4ae8 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -17,6 +17,7 @@ .. autosummary:: ExtendedEnumType + NamedIntEnum Standard Enums: @@ -130,7 +131,21 @@ def __new__(metacls, cls, bases, classdict, **kwds): return super().__new__(metacls, cls, bases, classdict, **kwds) -class ObjectiveSense(enum.IntEnum): +class NamedIntEnum(enum.IntEnum): + """An extended version of :py:class:`enum.IntEnum` that supports + creating members by name as well as value. + + """ + + @classmethod + def _missing_(cls, value): + for member in cls: + if member.name == value: + return member + return None + + +class ObjectiveSense(NamedIntEnum): """Flag indicating if an objective is minimizing (1) or maximizing (-1). While the numeric values are arbitrary, there are parts of Pyomo @@ -150,13 +165,6 @@ class ObjectiveSense(enum.IntEnum): def __str__(self): return self.name - @classmethod - def _missing_(cls, value): - for member in cls: - if member.name == value: - return member - return None - minimize = ObjectiveSense.minimize maximize = ObjectiveSense.maximize From e7540f237b774a7297afed8fa62d6848aae2480a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 12:02:12 -0600 Subject: [PATCH 1333/3044] Rework Simplifier so we can force the backend mode --- pyomo/contrib/simplification/simplify.py | 66 +++++++++++-------- .../tests/test_simplification.py | 43 +++++------- 2 files changed, 56 insertions(+), 53 deletions(-) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 94f0ceaa33f..840f3a1c1da 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -9,26 +9,25 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import logging +import warnings + +from pyomo.common.enums import NamedIntEnum from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression from pyomo.core.expr.numeric_expr import NumericExpression from pyomo.core.expr.numvalue import value, is_constant -import logging -import warnings from pyomo.contrib.simplification.ginac import ( interface as ginac_interface, interface_available as ginac_available, ) -logger = logging.getLogger(__name__) - def simplify_with_sympy(expr: NumericExpression): if is_constant(expr): return value(expr) - om, se = sympyify_expression(expr) - se = se.simplify() - new_expr = sympy2pyomo_expression(se, om) + object_map, sympy_expr = sympyify_expression(expr) + new_expr = sympy2pyomo_expression(sympy_expr.simplify(), object_map) if is_constant(new_expr): new_expr = value(new_expr) return new_expr @@ -37,29 +36,40 @@ def simplify_with_sympy(expr: NumericExpression): def simplify_with_ginac(expr: NumericExpression, ginac_interface): if is_constant(expr): return value(expr) - gi = ginac_interface - ginac_expr = gi.to_ginac(expr) - ginac_expr = ginac_expr.normal() - new_expr = gi.from_ginac(ginac_expr) - return new_expr + ginac_expr = ginac_interface.to_ginac(expr) + return ginac_interface.from_ginac(ginac_expr.normal()) class Simplifier(object): - def __init__(self, suppress_no_ginac_warnings: bool = False) -> None: - if ginac_available: - self.gi = ginac_interface.GinacInterface(False) - self.suppress_no_ginac_warnings = suppress_no_ginac_warnings + class Mode(NamedIntEnum): + auto = 0 + sympy = 1 + ginac = 2 + + def __init__( + self, suppress_no_ginac_warnings: bool = False, mode: Mode = Mode.auto + ) -> None: + if mode == Simplifier.Mode.auto: + if ginac_available: + mode = Simplifier.Mode.ginac + else: + if not suppress_no_ginac_warnings: + msg = ( + "GiNaC does not seem to be available. Using SymPy. " + + "Note that the GiNaC interface is significantly faster." + ) + logging.getLogger(__name__).warning(msg) + warnings.warn(msg) + mode = Simplifier.Mode.sympy - def simplify(self, expr: NumericExpression): - if ginac_available: - return simplify_with_ginac(expr, self.gi) + if mode == Simplifier.Mode.ginac: + self.gi = ginac_interface.GinacInterface(False) + self.simplify = self._simplify_with_ginac else: - if not self.suppress_no_ginac_warnings: - msg = ( - "GiNaC does not seem to be available. Using SymPy. " - + "Note that the GiNac interface is significantly faster." - ) - logger.warning(msg) - warnings.warn(msg) - self.suppress_no_ginac_warnings = True - return simplify_with_sympy(expr) + self.simplify = self._simplify_with_sympy + + def _simplify_with_ginac(self, expr: NumericExpression): + return simplify_with_ginac(expr, self.gi) + + def _simplify_with_sympy(self, expr: NumericExpression): + return simplify_with_sympy(expr) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 27208d42229..efa9f903adc 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -9,17 +9,15 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.common.unittest import TestCase from pyomo.common import unittest +from pyomo.common.fileutils import this_file_dir from pyomo.contrib.simplification import Simplifier from pyomo.contrib.simplification.simplify import ginac_available from pyomo.core.expr.compare import assertExpressionsEqual, compare_expressions -import pyomo.environ as pe from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd -from pyomo.common.dependencies import attempt_import - +from pyomo.core.expr.sympy_tools import sympy_available -sympy, sympy_available = attempt_import('sympy') +import pyomo.environ as pe class SimplificationMixin: @@ -37,8 +35,7 @@ def test_simplify(self): e = x * pe.log(x) der1 = reverse_sd(e)[x] der2 = reverse_sd(der1)[x] - simp = Simplifier() - der2_simp = simp.simplify(der2) + der2_simp = self.simp.simplify(der2) expected = x**-1.0 assertExpressionsEqual(self, expected, der2_simp) @@ -46,8 +43,7 @@ def test_mul(self): m = pe.ConcreteModel() x = m.x = pe.Var() e = 2 * x - simp = Simplifier() - e2 = simp.simplify(e) + e2 = self.simp.simplify(e) expected = 2.0 * x assertExpressionsEqual(self, expected, e2) @@ -55,16 +51,14 @@ def test_sum(self): m = pe.ConcreteModel() x = m.x = pe.Var() e = 2 + x - simp = Simplifier() - e2 = simp.simplify(e) + e2 = self.simp.simplify(e) self.compare_against_possible_results(e2, [2.0 + x, x + 2.0]) def test_neg(self): m = pe.ConcreteModel() x = m.x = pe.Var() e = -pe.log(x) - simp = Simplifier() - e2 = simp.simplify(e) + e2 = self.simp.simplify(e) self.compare_against_possible_results( e2, [(-1.0) * pe.log(x), pe.log(x) * (-1.0), -pe.log(x)] ) @@ -73,8 +67,7 @@ def test_pow(self): m = pe.ConcreteModel() x = m.x = pe.Var() e = x**2.0 - simp = Simplifier() - e2 = simp.simplify(e) + e2 = self.simp.simplify(e) assertExpressionsEqual(self, e, e2) def test_div(self): @@ -82,9 +75,7 @@ def test_div(self): x = m.x = pe.Var() y = m.y = pe.Var() e = x / y + y / x - x / y - simp = Simplifier() - e2 = simp.simplify(e) - print(e2) + e2 = self.simp.simplify(e) self.compare_against_possible_results( e2, [y / x, y * (1.0 / x), y * x**-1.0, x**-1.0 * y] ) @@ -95,25 +86,27 @@ def test_unary(self): func_list = [pe.log, pe.sin, pe.cos, pe.tan, pe.asin, pe.acos, pe.atan] for func in func_list: e = func(x) - simp = Simplifier() - e2 = simp.simplify(e) + e2 = self.simp.simplify(e) assertExpressionsEqual(self, e, e2) @unittest.skipUnless(sympy_available, 'sympy is not available') -class TestSimplificationSympy(TestCase, SimplificationMixin): - pass +class TestSimplificationSympy(unittest.TestCase, SimplificationMixin): + def setUp(self): + self.simp = Simplifier(mode=Simplifier.Mode.sympy) @unittest.skipUnless(ginac_available, 'GiNaC is not available') -class TestSimplificationGiNaC(TestCase, SimplificationMixin): +class TestSimplificationGiNaC(unittest.TestCase, SimplificationMixin): + def setUp(self): + self.simp = Simplifier(mode=Simplifier.Mode.ginac) + def test_param(self): m = pe.ConcreteModel() x = m.x = pe.Var() p = m.p = pe.Param(mutable=True) e1 = p * x**2 + p * x + p * x**2 - simp = Simplifier() - e2 = simp.simplify(e1) + e2 = self.simp.simplify(e1) self.compare_against_possible_results( e2, [ From 9c220b05e34bfff25e3fae4172b187ed169183c1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 12:56:34 -0600 Subject: [PATCH 1334/3044] Improve robustness of tar filter --- pyomo/common/download.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pyomo/common/download.py b/pyomo/common/download.py index 95713e9ef76..8361798817f 100644 --- a/pyomo/common/download.py +++ b/pyomo/common/download.py @@ -408,25 +408,25 @@ def filter_fcn(info): f = info.name if os.path.isabs(f) or '..' in f or f.startswith(('/', os.sep)): logger.error( - "malformed (potentially insecure) filename (%s) " - "found in tar archive. Skipping file." % (f,) - ) - return False - target = os.path.realpath(os.path.join(dest, f)) - if os.path.commonpath([target, dest]) != dest: - logger.error( - "malformed (potentially insecure) filename (%s) " - "found in zip archive. Skipping file." % (f,) + "malformed or potentially insecure filename (%s). " + "Skipping file." % (f,) ) return False target = self._splitpath(f) if len(target) <= dirOffset: if not info.isdir(): logger.warning( - "Skipping file (%s) in zip archive due to dirOffset" % (f,) + "Skipping file (%s) in tar archive due to dirOffset." % (f,) ) return False - info.name = '/'.join(target[dirOffset:]) + info.name = f = '/'.join(target[dirOffset:]) + target = os.path.realpath(os.path.join(dest, f)) + if os.path.commonpath([target, dest]) != dest: + logger.error( + "potentially insecure filename (%s) resolves outside target " + "directory. Skipping file." % (f,) + ) + return False # Strip high bits & group/other write bits info.mode &= 0o755 return True From e597bb5c390432555990394ab8fa474bfee8dbaa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 12:57:07 -0600 Subject: [PATCH 1335/3044] Ensure tar file is closed --- pyomo/common/download.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/common/download.py b/pyomo/common/download.py index 8361798817f..2a91553c728 100644 --- a/pyomo/common/download.py +++ b/pyomo/common/download.py @@ -398,8 +398,6 @@ def get_tar_archive(self, url, dirOffset=0): raise RuntimeError( "Target directory (%s) exists, but is not a directory" % (self._fname,) ) - tar_file = tarfile.open(fileobj=io.BytesIO(self.retrieve_url(url))) - dest = os.path.realpath(self._fname) def filter_fcn(info): # this mocks up the `tarfile` filter introduced in Python @@ -431,7 +429,9 @@ def filter_fcn(info): info.mode &= 0o755 return True - tar_file.extractall(dest, filter(filter_fcn, tar_file.getmembers())) + with tarfile.open(fileobj=io.BytesIO(self.retrieve_url(url))) as TAR: + dest = os.path.realpath(self._fname) + TAR.extractall(dest, filter(filter_fcn, TAR.getmembers())) def get_gzipped_binary_file(self, url): if self._fname is None: From 70166ffbeb3d39d5ed59cba6f479d041d3da0eb1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 5 May 2024 12:58:19 -0600 Subject: [PATCH 1336/3044] test get_tar_archive() --- pyomo/common/tests/test_download.py | 70 ++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/pyomo/common/tests/test_download.py b/pyomo/common/tests/test_download.py index 87108be1c59..8fee0ba7e31 100644 --- a/pyomo/common/tests/test_download.py +++ b/pyomo/common/tests/test_download.py @@ -9,12 +9,14 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import io import os import platform import re import shutil -import tempfile import subprocess +import tarfile +import tempfile import pyomo.common.unittest as unittest import pyomo.common.envvar as envvar @@ -22,6 +24,7 @@ from pyomo.common import DeveloperError from pyomo.common.fileutils import this_file from pyomo.common.download import FileDownloader, distro_available +from pyomo.common.log import LoggingIntercept from pyomo.common.tee import capture_output @@ -242,7 +245,7 @@ def test_get_files_requires_set_destination(self): ): f.get_gzipped_binary_file('bogus') - def test_get_test_binary_file(self): + def test_get_text_binary_file(self): tmpdir = tempfile.mkdtemp() try: f = FileDownloader() @@ -263,3 +266,66 @@ def test_get_test_binary_file(self): self.assertEqual(os.path.getsize(target), len(os.linesep)) finally: shutil.rmtree(tmpdir) + + def test_get_tar_archive(self): + tmpdir = tempfile.mkdtemp() + try: + f = FileDownloader() + + # Mock retrieve_url so network connections are not necessary + buf = io.BytesIO() + with tarfile.open(mode="w:gz", fileobj=buf) as TAR: + info = tarfile.TarInfo('b/lnk') + info.size = 0 + info.type = tarfile.SYMTYPE + info.linkname = envvar.PYOMO_CONFIG_DIR + TAR.addfile(info) + for fname in ('a', 'b/c', 'b/d', '/root', 'b/lnk/test'): + info = tarfile.TarInfo(fname) + info.size = 0 + info.type = tarfile.REGTYPE + info.mode = 0o644 + info.mtime = info.uid = info.gid = 0 + info.uname = info.gname = 'root' + TAR.addfile(info) + f.retrieve_url = lambda url: buf.getvalue() + + with self.assertRaisesRegex( + DeveloperError, + r"(?s)target file name has not been initialized " + r"with set_destination_filename".replace(' ', r'\s+'), + ): + f.get_tar_archive(None, 1) + + _tmp = os.path.join(tmpdir, 'a_file') + with open(_tmp, 'w'): + pass + f.set_destination_filename(_tmp) + with self.assertRaisesRegex( + RuntimeError, + r"Target directory \(.*a_file\) exists, but is not a directory", + ): + f.get_tar_archive(None, 1) + + f.set_destination_filename(tmpdir) + with LoggingIntercept() as LOG: + f.get_tar_archive(None, 1) + + self.assertEqual( + LOG.getvalue().strip(), + """ +Skipping file (a) in tar archive due to dirOffset. +malformed or potentially insecure filename (/root). Skipping file. +potentially insecure filename (lnk/test) resolves outside target directory. Skipping file. +""".strip(), + ) + for f in ('c', 'd'): + fname = os.path.join(tmpdir, f) + self.assertTrue(os.path.exists(fname)) + self.assertTrue(os.path.isfile(fname)) + for f in ('lnk',): + fname = os.path.join(tmpdir, f) + self.assertTrue(os.path.exists(fname)) + self.assertTrue(os.path.islink(fname)) + finally: + shutil.rmtree(tmpdir) From e77be8c59c80fd6028c2bb0d2a456c485501b88b Mon Sep 17 00:00:00 2001 From: David L Woodruff Date: Sun, 5 May 2024 17:36:00 -0700 Subject: [PATCH 1337/3044] Code for infeasibility diagnostics called mis (#3172) * getting started moving mis code into Pyomo contrib * we have a test for mis, but it needs more coverage * now testing some exceptions * slight change to doc * black * fixing _get_constraint test * removing some spelling errors * more spelling errors removed * update typos.toml for mis * I forgot to push the __init__.py file in tests * a little documentation cleanup * moved mis to be part of iis * correct bad import in mis test * I didn't realize it would run every py file in the test directory * trying to get the Windows tests to pass by explicitly releasing the logger file handle * run black on test_mis.py * trying to manage the temp dir using the tempfilemanager as a context * catch the error that kills windows tests * run black again * windows started passing, but linux failing; one quick check to see if logging.info helps: * run black again * On windows we are just going to have to leave a log file from the test * add a test for a feasible model * Update pyomo/contrib/iis/mis.py Co-authored-by: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> * Changes suggested by Miranda * run black again * simplifying the code * take care of Miranda's helpful comments * add sorely needed f to format error messages * added suggestions from R. Parker to the comments --------- Co-authored-by: Bernard Knueven Co-authored-by: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> --- .github/workflows/typos.toml | 3 + doc/OnlineDocs/conf.py | 1 + doc/OnlineDocs/contributed_packages/iis.rst | 129 +++++++ pyomo/contrib/iis/__init__.py | 1 + pyomo/contrib/iis/mis.py | 377 ++++++++++++++++++++ pyomo/contrib/iis/tests/test_mis.py | 125 +++++++ pyomo/contrib/iis/tests/trivial_mis.py | 24 ++ 7 files changed, 660 insertions(+) create mode 100644 pyomo/contrib/iis/mis.py create mode 100644 pyomo/contrib/iis/tests/test_mis.py create mode 100644 pyomo/contrib/iis/tests/trivial_mis.py diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index 4d69cde34e1..7a38164898b 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -40,6 +40,9 @@ WRONLY = "WRONLY" Hax = "Hax" # Big Sur Sur = "Sur" +# contrib package named mis and the acronym whence the name comes +mis = "mis" +MIS = "MIS" # Ignore the shorthand ans for answer ans = "ans" # Ignore the keyword arange diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 1aab4cd76c2..a06ccfbc9bd 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -84,6 +84,7 @@ 'sphinx.ext.todo', 'sphinx_copybutton', 'enum_tools.autoenum', + 'sphinx.ext.autosectionlabel', #'sphinx.ext.githubpages', ] diff --git a/doc/OnlineDocs/contributed_packages/iis.rst b/doc/OnlineDocs/contributed_packages/iis.rst index 98cb9e30771..fa97c2f8c61 100644 --- a/doc/OnlineDocs/contributed_packages/iis.rst +++ b/doc/OnlineDocs/contributed_packages/iis.rst @@ -1,6 +1,135 @@ +Infeasibility Diagnostics +!!!!!!!!!!!!!!!!!!!!!!!!! + +There are two closely related tools for infeasibility diagnosis: + + - :ref:`Infeasible Irreducible System (IIS) Tool` + - :ref:`Minimal Intractable System finder (MIS) Tool` + +The first simply provides a conduit for solvers that compute an +infeasible irreducible system (e.g., Cplex, Gurobi, or Xpress). The +second provides similar functionality, but uses the ``mis`` package +contributed to Pyomo. + + Infeasible Irreducible System (IIS) Tool ======================================== .. automodule:: pyomo.contrib.iis.iis .. autofunction:: pyomo.contrib.iis.write_iis + +Minimal Intractable System finder (MIS) Tool +============================================ + +The file ``mis.py`` finds sets of actions that each, independently, +would result in feasibility. The zero-tolerance is whatever the +solver uses, so users may want to post-process output if it is going +to be used for analysis. It also computes a minimal intractable system +(which is not guaranteed to be unique). It was written by Ben Knueven +as part of the watertap project (https://github.com/watertap-org/watertap) +and is therefore governed by a license shown +at the top of ``mis.py``. + +The algorithms come from John Chinneck's slides, see: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf + +Solver +------ + +At the time of this writing, you need to use IPopt even for LPs. + +Quick Start +----------- + +The file ``trivial_mis.py`` is a tiny example listed at the bottom of +this help file, which references a Pyomo model with the Python variable +`m` and has these lines: + +.. code-block:: python + + from pyomo.contrib.mis import compute_infeasibility_explanation + ipopt = pyo.SolverFactory("ipopt") + compute_infeasibility_explanation(m, solver=ipopt) + +.. Note:: + This is done instead of solving the problem. + +.. Note:: + IDAES users can pass ``get_solver()`` imported from ``ideas.core.solvers`` + as the solver. + +Interpreting the Output +----------------------- + +Assuming the dependencies are installed, running ``trivial_mis.py`` +(shown below) will +produce a lot of warnings from IPopt and then meaningful output (using a logger). + +Repair Options +^^^^^^^^^^^^^^ + +This output for the trivial example shows three independent ways that the model could be rendered feasible: + + +.. code-block:: text + + Model Trivial Quad may be infeasible. A feasible solution was found with only the following variable bounds relaxed: + ub of var x[1] by 4.464126126706818e-05 + lb of var x[2] by 0.9999553410114216 + Another feasible solution was found with only the following variable bounds relaxed: + lb of var x[1] by 0.7071067726864677 + ub of var x[2] by 0.41421355687130673 + ub of var y by 0.7071067651855212 + Another feasible solution was found with only the following inequality constraints, equality constraints, and/or variable bounds relaxed: + constraint: c by 0.9999999861866736 + + +Minimal Intractable System (MIS) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This output shows a minimal intractable system: + + +.. code-block:: text + + Computed Minimal Intractable System (MIS)! + Constraints / bounds in MIS: + lb of var x[2] + lb of var x[1] + constraint: c + +Constraints / bounds in guards for stability +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This part of the report is for nonlinear programs (NLPs). + +When we’re trying to reduce the constraint set, for an NLP there may be constraints that when missing cause the solver +to fail in some catastrophic fashion. In this implementation this is interpreted as failing to get a `results` +object back from the call to `solve`. In these cases we keep the constraint in the problem but it’s in the +set of “guard” constraints – we can’t really be sure they’re a source of infeasibility or not, +just that “bad things” happen when they’re not included. + +Perhaps ideally we would put a constraint in the “guard” set if IPopt failed to converge, and only put it in the +MIS if IPopt converged to a point of local infeasibility. However, right now the code generally makes the +assumption that if IPopt fails to converge the subproblem is infeasible, though obviously that is far from the truth. +Hence for difficult NLPs even the “Phase 1” may “fail” – in that when finished the subproblem containing just the +constraints in the elastic filter may be feasible -- because IPopt failed to converge and we assumed that meant the +subproblem was not feasible. + +Dealing with NLPs is far from clean, but that doesn’t mean the tool can’t return useful results even when its assumptions are not satisfied. + +trivial_mis.py +-------------- + +.. code-block:: python + + import pyomo.environ as pyo + m = pyo.ConcreteModel("Trivial Quad") + m.x = pyo.Var([1,2], bounds=(0,1)) + m.y = pyo.Var(bounds=(0, 1)) + m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) + m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) + + from pyomo.contrib.mis import compute_infeasibility_explanation + ipopt = pyo.SolverFactory("ipopt") + compute_infeasibility_explanation(m, solver=ipopt) diff --git a/pyomo/contrib/iis/__init__.py b/pyomo/contrib/iis/__init__.py index e8d6a7ac2c3..961ac576d42 100644 --- a/pyomo/contrib/iis/__init__.py +++ b/pyomo/contrib/iis/__init__.py @@ -10,3 +10,4 @@ # ___________________________________________________________________________ from pyomo.contrib.iis.iis import write_iis +from pyomo.contrib.iis.mis import compute_infeasibility_explanation diff --git a/pyomo/contrib/iis/mis.py b/pyomo/contrib/iis/mis.py new file mode 100644 index 00000000000..6b6cca8e29c --- /dev/null +++ b/pyomo/contrib/iis/mis.py @@ -0,0 +1,377 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +""" +WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, National Renewable Energy Laboratory, and National Energy Technology Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy). All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + Neither the name of the University of California, Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, National Renewable Energy Laboratory, National Energy Technology Laboratory, U.S. Dept. of Energy nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +You are under no obligation whatsoever to provide any bug fixes, patches, or upgrades to the features, functionality or performance of the source code ("Enhancements") to anyone; however, if you choose to make your Enhancements available either publicly, or directly to Lawrence Berkeley National Laboratory, without imposing a separate written license agreement for such Enhancements, then you hereby grant the following license: a non-exclusive, royalty-free perpetual license to install, use, modify, prepare derivative works, incorporate into other computer software, distribute, and sublicense such enhancements or derivative works thereof, in binary and source code form. +""" +""" +Minimal Intractable System (MIS) finder +Originally written by Ben Knueven as part of the WaterTAP project: + https://github.com/watertap-org/watertap +That's why this file has the watertap copyright notice. + +copied by DLW 18Feb2024 and edited + +See: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf +""" + +import logging +import pyomo.environ as pyo + +from pyomo.core.plugins.transform.add_slack_vars import AddSlackVariables + +from pyomo.core.plugins.transform.hierarchy import IsomorphicTransformation + +from pyomo.common.modeling import unique_component_name +from pyomo.common.collections import ComponentMap, ComponentSet + +from pyomo.opt import WriterFactory + +logger = logging.getLogger("pyomo.contrib.iis") +logger.setLevel(logging.INFO) + + +class _VariableBoundsAsConstraints(IsomorphicTransformation): + """Replace all variables bounds and domain information with constraints. + + Leaves fixed Vars untouched (for now) + """ + + def _apply_to(self, instance, **kwds): + + bound_constr_block_name = unique_component_name(instance, "_variable_bounds") + instance.add_component(bound_constr_block_name, pyo.Block()) + bound_constr_block = instance.component(bound_constr_block_name) + + for v in instance.component_data_objects(pyo.Var, descend_into=True): + if v.fixed: + continue + lb, ub = v.bounds + if lb is None and ub is None: + continue + var_name = v.getname(fully_qualified=True) + if lb is not None: + con_name = "lb_for_" + var_name + con = pyo.Constraint(expr=(lb, v, None)) + bound_constr_block.add_component(con_name, con) + if ub is not None: + con_name = "ub_for_" + var_name + con = pyo.Constraint(expr=(None, v, ub)) + bound_constr_block.add_component(con_name, con) + + # now we deactivate the variable bounds / domain + v.domain = pyo.Reals + v.setlb(None) + v.setub(None) + + +def compute_infeasibility_explanation( + model, solver, tee=False, tolerance=1e-8, logger=logger +): + """ + This function attempts to determine why a given model is infeasible. It deploys + two main algorithms: + + 1. Successfully relaxes the constraints of the problem, and reports to the user + some sets of constraints and variable bounds, which when relaxed, creates a + feasible model. + 2. Uses the information collected from (1) to attempt to compute a Minimal + Infeasible System (MIS), which is a set of constraints and variable bounds + which appear to be in conflict with each other. It is minimal in the sense + that removing any single constraint or variable bound would result in a + feasible subsystem. + + Args + ---- + model: A pyomo block + solver: A pyomo solver object or a string for SolverFactory + tee (optional): Display intermediate solves conducted (False) + tolerance (optional): The feasibility tolerance to use when declaring a + constraint feasible (1e-08) + logger:logging.Logger + A logger for messages. Uses pyomo.contrib.mis logger by default. + + """ + # Suggested enhancement: It might be useful to return sets of names for each set of relaxed components, as well as the final minimal infeasible system + + # hold the original harmless + modified_model = model.clone() + + if solver is None: + raise ValueError("A solver must be supplied") + elif isinstance(solver, str): + solver = pyo.SolverFactory(solver) + else: + # assume we have a solver + assert solver.available() + + # first, cache the values we get + _value_cache = ComponentMap() + for v in model.component_data_objects(pyo.Var, descend_into=True): + _value_cache[v] = v.value + + # finding proper reference + if model.parent_block() is None: + common_name = "" + else: + common_name = model.name + "." + + _modified_model_var_to_original_model_var = ComponentMap() + _modified_model_value_cache = ComponentMap() + + for v in model.component_data_objects(pyo.Var, descend_into=True): + modified_model_var = modified_model.find_component(v.name[len(common_name) :]) + + _modified_model_var_to_original_model_var[modified_model_var] = v + _modified_model_value_cache[modified_model_var] = _value_cache[v] + modified_model_var.set_value(_value_cache[v], skip_validation=True) + + # TODO: For WT / IDAES models, we should probably be more + # selective in *what* we elasticize. E.g., it probably + # does not make sense to elasticize property calculations + # and maybe certain other equality constraints calculating + # values. Maybe we shouldn't elasticize *any* equality + # constraints. + # For example, elasticizing the calculation of mass fraction + # makes absolutely no sense and will just be noise for the + # modeler to sift through. We could try to sort the constraints + # such that we look for those with linear coefficients `1` on + # some term and leave those be. + # Alternatively, we could apply this tool to a version of the + # model that has as many as possible of these constraints + # "substituted out". + # move the variable bounds to the constraints + _VariableBoundsAsConstraints().apply_to(modified_model) + + AddSlackVariables().apply_to(modified_model) + slack_block = modified_model._core_add_slack_variables + + for v in slack_block.component_data_objects(pyo.Var): + v.fix(0) + # start with variable bounds -- these are the easiest to interpret + for c in modified_model._variable_bounds.component_data_objects( + pyo.Constraint, descend_into=True + ): + plus = slack_block.component(f"_slack_plus_{c.name}") + minus = slack_block.component(f"_slack_minus_{c.name}") + assert not (plus is None and minus is None) + if plus is not None: + plus.unfix() + if minus is not None: + minus.unfix() + + # TODO: Elasticizing too much at once seems to cause Ipopt trouble. + # After an initial sweep, we should just fix one elastic variable + # and put everything else on a stack of "constraints to elasticize". + # We elasticize one constraint at a time and fix one constraint at a time. + # After fixing an elastic variable, we elasticize a single constraint it + # appears in and put the remaining constraints on the stack. If the resulting problem + # is feasible, we keep going "down the tree". If the resulting problem is + # infeasible or cannot be solved, we elasticize a single constraint from + # the top of the stack. + # The algorithm stops when the stack is empty and the subproblem is infeasible. + # Along the way, any time the current problem is infeasible we can check to + # see if the current set of constraints in the filter is as a collection of + # infeasible constraints -- to terminate early. + # However, while more stable, this is much more computationally intensive. + # So, we leave the implementation simpler for now and consider this as + # a potential extension if this tool sometimes cannot report a good answer. + # Phase 1 -- build the initial set of constraints, or prove feasibility + msg = "" + fixed_slacks = ComponentSet() + elastic_filter = ComponentSet() + + def _constraint_loop(relaxed_things, msg): + if msg == "": + msg += f"Model {model.name} may be infeasible. A feasible solution was found with only the following {relaxed_things} relaxed:\n" + else: + msg += f"Another feasible solution was found with only the following {relaxed_things} relaxed:\n" + while True: + + def _constraint_generator(): + elastic_filter_size_initial = len(elastic_filter) + for v in slack_block.component_data_objects(pyo.Var): + if v.value > tolerance: + constr = _get_constraint(modified_model, v) + yield constr, v.value + v.fix(0) + fixed_slacks.add(v) + elastic_filter.add(constr) + if len(elastic_filter) == elastic_filter_size_initial: + raise Exception(f"Found model {model.name} to be feasible!") + + msg = _get_results_with_value(_constraint_generator(), msg) + for var, val in _modified_model_value_cache.items(): + var.set_value(val, skip_validation=True) + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg += f"Another feasible solution was found with only the following {relaxed_things} relaxed:\n" + else: + break + return msg + + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg = _constraint_loop("variable bounds", msg) + + # next, try relaxing the inequality constraints + for v in slack_block.component_data_objects(pyo.Var): + c = _get_constraint(modified_model, v) + if c.equality: + # equality constraint + continue + if v not in fixed_slacks: + v.unfix() + + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg = _constraint_loop("inequality constraints and/or variable bounds", msg) + + for v in slack_block.component_data_objects(pyo.Var): + if v not in fixed_slacks: + v.unfix() + + results = solver.solve(modified_model, tee=tee) + if pyo.check_optimal_termination(results): + msg = _constraint_loop( + "inequality constraints, equality constraints, and/or variable bounds", msg + ) + + if len(elastic_filter) == 0: + # load the feasible solution into the original model + for modified_model_var, v in _modified_model_var_to_original_model_var.items(): + v.set_value(modified_model_var.value, skip_validation=True) + results = solver.solve(model, tee=tee) + if pyo.check_optimal_termination(results): + logger.info(f"A feasible solution was found!") + else: + logger.info( + f"Could not find a feasible solution with violated constraints or bounds. This model is likely unstable" + ) + + # Phase 2 -- deletion filter + # remove slacks by fixing them to 0 + for v in slack_block.component_data_objects(pyo.Var): + v.fix(0) + for o in modified_model.component_data_objects(pyo.Objective, descend_into=True): + o.deactivate() + + # mark all constraints not in the filter as inactive + for c in modified_model.component_data_objects(pyo.Constraint): + if c in elastic_filter: + continue + else: + c.deactivate() + + try: + results = solver.solve(modified_model, tee=tee) + except: + results = None + + if pyo.check_optimal_termination(results): + msg += "Could not determine Minimal Intractable System\n" + else: + deletion_filter = [] + guards = [] + for constr in elastic_filter: + constr.deactivate() + for var, val in _modified_model_value_cache.items(): + var.set_value(val, skip_validation=True) + math_failure = False + try: + results = solver.solve(modified_model, tee=tee) + except: + math_failure = True + + if math_failure: + constr.activate() + guards.append(constr) + elif pyo.check_optimal_termination(results): + constr.activate() + deletion_filter.append(constr) + else: # still infeasible without this constraint + pass + + msg += "Computed Minimal Intractable System (MIS)!\n" + msg += "Constraints / bounds in MIS:\n" + msg = _get_results(deletion_filter, msg) + msg += "Constraints / bounds in guards for stability:" + msg = _get_results(guards, msg) + + logger.info(msg) + + +def _get_results_with_value(constr_value_generator, msg=None): + # note that "lb_for_" and "ub_for_" are 7 characters long + if msg is None: + msg = "" + for c, value in constr_value_generator: + c_name = c.name + if "_variable_bounds" in c_name: + name = c.local_name + if "lb" in name: + msg += f"\tlb of var {name[7:]} by {value}\n" + elif "ub" in name: + msg += f"\tub of var {name[7:]} by {value}\n" + else: + raise RuntimeError("unrecognized var name") + else: + msg += f"\tconstraint: {c_name} by {value}\n" + return msg + + +def _get_results(constr_generator, msg=None): + # note that "lb_for_" and "ub_for_" are 7 characters long + if msg is None: + msg = "" + for c in constr_generator: + c_name = c.name + if "_variable_bounds" in c_name: + name = c.local_name + if "lb" in name: + msg += f"\tlb of var {name[7:]}\n" + elif "ub" in name: + msg += f"\tub of var {name[7:]}\n" + else: + raise RuntimeError("unrecognized var name") + else: + msg += f"\tconstraint: {c_name}\n" + return msg + + +def _get_constraint(modified_model, v): + if "_slack_plus_" in v.name: + constr = modified_model.find_component(v.local_name[len("_slack_plus_") :]) + if constr is None: + raise RuntimeError( + f"Bad constraint name {v.local_name[len('_slack_plus_'):]}" + ) + return constr + elif "_slack_minus_" in v.name: + constr = modified_model.find_component(v.local_name[len("_slack_minus_") :]) + if constr is None: + raise RuntimeError( + f"Bad constraint name {v.local_name[len('_slack_minus_'):]}" + ) + return constr + else: + raise RuntimeError(f"Bad var name {v.name}") diff --git a/pyomo/contrib/iis/tests/test_mis.py b/pyomo/contrib/iis/tests/test_mis.py new file mode 100644 index 00000000000..bbdb2367016 --- /dev/null +++ b/pyomo/contrib/iis/tests/test_mis.py @@ -0,0 +1,125 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +import pyomo.environ as pyo +import pyomo.contrib.iis.mis as mis +from pyomo.contrib.iis.mis import _get_constraint +from pyomo.common.tempfiles import TempfileManager + +import logging +import os + + +def _get_infeasible_model(): + m = pyo.ConcreteModel("trivial4test") + m.x = pyo.Var(within=pyo.Binary) + m.y = pyo.Var(within=pyo.NonNegativeReals) + + m.c1 = pyo.Constraint(expr=m.y <= 100.0 * m.x) + m.c2 = pyo.Constraint(expr=m.y <= -100.0 * m.x) + m.c3 = pyo.Constraint(expr=m.x >= 0.5) + + m.o = pyo.Objective(expr=-m.y) + + return m + + +def _get_feasible_model(): + m = pyo.ConcreteModel("Trivial Feasible Quad") + m.x = pyo.Var([1, 2], bounds=(0, 1)) + m.y = pyo.Var(bounds=(0, 1)) + m.c = pyo.Constraint(expr=m.x[1] * m.x[2] >= -1) + m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) + + return m + + +class TestMIS(unittest.TestCase): + @unittest.skipUnless( + pyo.SolverFactory("ipopt").available(exception_flag=False), + "ipopt not available", + ) + def test_write_mis_ipopt(self): + _test_mis("ipopt") + + def test__get_constraint_errors(self): + # A not-completely-cynical way to get the coverage up. + m = _get_infeasible_model() # not modified + fct = _get_constraint + + m.foo_slack_plus_ = pyo.Var() + self.assertRaises(RuntimeError, fct, m, m.foo_slack_plus_) + m.foo_slack_minus_ = pyo.Var() + self.assertRaises(RuntimeError, fct, m, m.foo_slack_minus_) + m.foo_bar = pyo.Var() + self.assertRaises(RuntimeError, fct, m, m.foo_bar) + + def test_feasible_model(self): + m = _get_feasible_model() + opt = pyo.SolverFactory("ipopt") + self.assertRaises(Exception, mis.compute_infeasibility_explanation, m, opt) + + +def _check_output(file_name): + # pretty simple check for now + with open(file_name, "r+") as file1: + lines = file1.readlines() + trigger = "Constraints / bounds in MIS:" + nugget = "lb of var y" + live = False # (long i) + found_nugget = False + for line in lines: + if trigger in line: + live = True + if live: + if nugget in line: + found_nugget = True + if not found_nugget: + raise RuntimeError(f"Did not find '{nugget}' after '{trigger}' in output") + else: + pass + + +def _test_mis(solver_name): + m = _get_infeasible_model() + opt = pyo.SolverFactory(solver_name) + + # This test seems to fail on Windows as it unlinks the tempfile, so live with it + # On a Windows machine, we will not use a temp dir and just try to delete the log file + if os.name == "nt": + file_name = f"_test_mis_{solver_name}.log" + logger = logging.getLogger(f"test_mis_{solver_name}") + logger.setLevel(logging.INFO) + fh = logging.FileHandler(file_name) + fh.setLevel(logging.DEBUG) + logger.addHandler(fh) + + mis.compute_infeasibility_explanation(m, opt, logger=logger) + _check_output(file_name) + # os.remove(file_name) cannot remove it on Windows. Still in use. + + else: # not windows + with TempfileManager.new_context() as tmpmgr: + tmp_path = tmpmgr.mkdtemp() + file_name = os.path.join(tmp_path, f"_test_mis_{solver_name}.log") + logger = logging.getLogger(f"test_mis_{solver_name}") + logger.setLevel(logging.INFO) + fh = logging.FileHandler(file_name) + fh.setLevel(logging.DEBUG) + logger.addHandler(fh) + + mis.compute_infeasibility_explanation(m, opt, logger=logger) + _check_output(file_name) + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/contrib/iis/tests/trivial_mis.py b/pyomo/contrib/iis/tests/trivial_mis.py new file mode 100644 index 00000000000..4cf0dd7a357 --- /dev/null +++ b/pyomo/contrib/iis/tests/trivial_mis.py @@ -0,0 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +import pyomo.environ as pyo + +m = pyo.ConcreteModel("Trivial Quad") +m.x = pyo.Var([1, 2], bounds=(0, 1)) +m.y = pyo.Var(bounds=(0, 1)) +m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) +m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) + +from pyomo.contrib.iis.mis import compute_infeasibility_explanation + +# Note: this particular little problem is quadratic +# As of 18Feb2024 DLW is not sure the explanation code works with solvers other than ipopt +ipopt = pyo.SolverFactory("ipopt") +compute_infeasibility_explanation(m, solver=ipopt) From de1c782ee1ffac58c31ea2f164c40d66abe93b6c Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 6 May 2024 13:52:35 -0600 Subject: [PATCH 1338/3044] Cleaning up a few docstrings in parmest --- pyomo/contrib/parmest/parmest.py | 34 +++++++++++++++++++------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 41e1724f94f..70f9de8b84c 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -423,7 +423,8 @@ def _create_parmest_model(self, experiment_number): for obj in model.component_objects(pyo.Objective): obj.deactivate() - # TODO, this needs to be turned a enum class of options that still support custom functions + # TODO, this needs to be turned into an enum class of options that still support + # custom functions if self.obj_function == 'SSE': second_stage_rule = SSE else: @@ -635,7 +636,7 @@ def _Q_at_theta(self, thetavals, initialize_parmest_model=False): initialize_parmest_model: boolean If True: Solve square problem instance, build extensive form of the model for - parameter estimation, and set flag model_initialized to True + parameter estimation, and set flag model_initialized to True. Default is False. Returns ------- @@ -866,10 +867,11 @@ def theta_est( return_values: list, optional List of Variable names, used to return values from the model for data reconciliation calc_cov: boolean, optional - If True, calculate and return the covariance matrix (only for "ef_ipopt" solver) + If True, calculate and return the covariance matrix (only for "ef_ipopt" solver). + Default is False. cov_n: int, optional If calc_cov=True, then the user needs to supply the number of datapoints - that are used in the objective function + that are used in the objective function. Returns ------- @@ -902,9 +904,10 @@ def theta_est( for experiment in self.exp_list ] ) - assert isinstance( - cov_n, int - ), "The number of datapoints that are used in the objective function is required to calculate the covariance matrix" + assert isinstance(cov_n, int), ( + "The number of datapoints that are used in the objective function is " + "required to calculate the covariance matrix" + ) assert ( cov_n > num_unknowns ), "The number of datapoints must be greater than the number of parameters to estimate" @@ -936,11 +939,12 @@ def theta_est_bootstrap( Size of each bootstrap sample. If samplesize=None, samplesize will be set to the number of samples in the data replacement: bool, optional - Sample with or without replacement + Sample with or without replacement. Default is True. seed: int or None, optional Random seed return_samples: bool, optional - Return a list of sample numbers used in each bootstrap estimation + Return a list of sample numbers used in each bootstrap estimation. + Default is False. Returns ------- @@ -1006,7 +1010,7 @@ def theta_est_leaveNout( seed: int or None, optional Random seed return_samples: bool, optional - Return a list of sample numbers that were left out + Return a list of sample numbers that were left out. Default is False. Returns ------- @@ -1080,7 +1084,7 @@ def leaveNout_bootstrap_test( Random seed Returns - ---------- + ------- List of tuples with one entry per lNo_sample: * The first item in each tuple is the list of N samples that are left @@ -1141,8 +1145,9 @@ def objective_at_theta(self, theta_values=None, initialize_parmest_model=False): Values of theta used to compute the objective initialize_parmest_model: boolean - If True: Solve square problem instance, build extensive form of the model for - parameter estimation, and set flag model_initialized to True + If True: Solve square problem instance, build extensive form + of the model for parameter estimation, and set flag + model_initialized to True. Default is False. Returns @@ -1243,7 +1248,7 @@ def likelihood_ratio_test( alphas: list List of alpha values to use in the chi2 test return_thresholds: bool, optional - Return the threshold value for each alpha + Return the threshold value for each alpha. Default is False. Returns ------- @@ -1305,6 +1310,7 @@ def confidence_region_test( to determine if they are inside or outside. Returns + ------- training_results: pd.DataFrame Theta value used to generate the confidence region along with True (inside) or False (outside) for each alpha From 7f27d2f69b0be7027a2907c2de766ede4fab302d Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 6 May 2024 14:26:55 -0600 Subject: [PATCH 1339/3044] Cleaning up a few assert statements in test_parmest.py --- pyomo/contrib/parmest/tests/test_parmest.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 69155dadb45..65e2e4a3b06 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -59,7 +59,8 @@ def setUp(self): RooneyBieglerExperiment, ) - # Note, the data used in this test has been corrected to use data.loc[5,'hour'] = 7 (instead of 6) + # Note, the data used in this test has been corrected to use + # data.loc[5,'hour'] = 7 (instead of 6) data = pd.DataFrame( data=[[1, 8.3], [2, 10.3], [3, 19.0], [4, 16.0], [5, 15.6], [7, 19.8]], columns=["hour", "y"], @@ -109,7 +110,7 @@ def test_bootstrap(self): theta_est = self.pest.theta_est_bootstrap(num_bootstraps, return_samples=True) num_samples = theta_est["samples"].apply(len) - self.assertTrue(len(theta_est.index), 10) + self.assertEqual(len(theta_est.index), 10) self.assertTrue(num_samples.equals(pd.Series([6] * 10))) del theta_est["samples"] @@ -155,13 +156,13 @@ def test_leaveNout(self): results = self.pest.leaveNout_bootstrap_test( 1, None, 3, "Rect", [0.5, 1.0], seed=5436 ) - self.assertTrue(len(results) == 6) # 6 lNo samples + self.assertEqual(len(results), 6) # 6 lNo samples i = 1 samples = results[i][0] # list of N samples that are left out lno_theta = results[i][1] bootstrap_theta = results[i][2] self.assertTrue(samples == [1]) # sample 1 was left out - self.assertTrue(lno_theta.shape[0] == 1) # lno estimate for sample 1 + self.assertEqual(lno_theta.shape[0], 1) # lno estimate for sample 1 self.assertTrue(set(lno_theta.columns) >= set([0.5, 1.0])) self.assertEqual(lno_theta[1.0].sum(), 1) # all true self.assertEqual(bootstrap_theta.shape[0], 3) # bootstrap for sample 1 @@ -205,7 +206,7 @@ def test_parallel_parmest(self): retcode = ret.returncode else: retcode = subprocess.call(rlist) - assert retcode == 0 + self.assertEqual(retcode, 0) @unittest.skip("Most folks don't have k_aug installed") def test_theta_k_aug_for_Hessian(self): From 5439e9560b1d4b8602350d8de3c9006e1c5fd615 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 6 May 2024 15:34:15 -0600 Subject: [PATCH 1340/3044] Fix typo in doe.py --- pyomo/contrib/doe/doe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index ab9a5ad9f85..0fc3e8770fe 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1083,7 +1083,7 @@ def _add_objective(self, m): # Calculate the eigenvalues of the FIM matrix eig = np.linalg.eigvals(fim) - # If the smallest eigenvalue is (pratcially) negative, add a diagonal matrix to make it positive definite + # If the smallest eigenvalue is (practically) negative, add a diagonal matrix to make it positive definite small_number = 1e-10 if min(eig) < small_number: fim = fim + np.eye(len(self.param)) * (small_number - min(eig)) From 3165c9d67b2b47a8d7ff9e601781ed4e2eecc8b2 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 6 May 2024 16:14:10 -0600 Subject: [PATCH 1341/3044] Minor edit to APPSI Highs version method to support older versions of Highs --- pyomo/contrib/appsi/solvers/highs.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 3612b9d5014..29c3698b277 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -176,11 +176,23 @@ def available(self): return self.Availability.NotFound def version(self): - version = ( - highspy.HIGHS_VERSION_MAJOR, - highspy.HIGHS_VERSION_MINOR, - highspy.HIGHS_VERSION_PATCH, - ) + try: + version = ( + highspy.HIGHS_VERSION_MAJOR, + highspy.HIGHS_VERSION_MINOR, + highspy.HIGHS_VERSION_PATCH, + ) + except AttributeError: + # Older versions of Highs do not have the above attributes + # and the solver version can only be obtained by making + # an instance of the solver class. + tmp = highspy.Highs() + version = ( + tmp.versionMajor(), + tmp.versionMinor(), + tmp.versionPatch(), + ) + return version @property From b425c4db8e4b6edacf7352b5a0e85de0c69b1558 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 6 May 2024 16:21:03 -0600 Subject: [PATCH 1342/3044] Fixing black formatting in highs.py --- pyomo/contrib/appsi/solvers/highs.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 75975a4e0a8..87b9557269f 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -187,11 +187,7 @@ def version(self): # and the solver version can only be obtained by making # an instance of the solver class. tmp = highspy.Highs() - version = ( - tmp.versionMajor(), - tmp.versionMinor(), - tmp.versionPatch(), - ) + version = (tmp.versionMajor(), tmp.versionMinor(), tmp.versionPatch()) return version From c610a20cf594be2dcf34e847aaf63dfb08bfde2b Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 6 May 2024 16:22:49 -0600 Subject: [PATCH 1343/3044] Removing whitespace in highs.py --- pyomo/contrib/appsi/solvers/highs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 87b9557269f..c948444839d 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -176,7 +176,7 @@ def available(self): return self.Availability.NotFound def version(self): - try: + try: version = ( highspy.HIGHS_VERSION_MAJOR, highspy.HIGHS_VERSION_MINOR, From fc58199106b62604946d9c20dde95f2b0362e70f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 7 May 2024 06:54:32 -0600 Subject: [PATCH 1344/3044] Clarify comment --- pyomo/contrib/solver/gurobi_direct.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/gurobi_direct.py b/pyomo/contrib/solver/gurobi_direct.py index 7b80651ccae..edca7018f92 100644 --- a/pyomo/contrib/solver/gurobi_direct.py +++ b/pyomo/contrib/solver/gurobi_direct.py @@ -283,7 +283,8 @@ def solve(self, model, **kwds) -> Results: if repn.c.shape[0]: gurobi_model.setAttr('ObjCon', repn.c_offset[0]) gurobi_model.setAttr('ModelSense', int(repn.objectives[0].sense)) - # gurobi_model.update() + # Note: calling gurobi_model.update() here is not + # necessary (it will happen as part of optimize()) timer.stop('transfer_model') options = config.solver_options From 4446d364877ccbaa5faa33f9a100efecbd2975c9 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 7 May 2024 10:23:43 -0600 Subject: [PATCH 1345/3044] fix tests --- pyomo/environ/tests/test_package_layout.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/environ/tests/test_package_layout.py b/pyomo/environ/tests/test_package_layout.py index 4e1574ab158..47c6422a879 100644 --- a/pyomo/environ/tests/test_package_layout.py +++ b/pyomo/environ/tests/test_package_layout.py @@ -38,6 +38,7 @@ _NON_MODULE_DIRS = { join('contrib', 'ampl_function_demo', 'src'), join('contrib', 'appsi', 'cmodel', 'src'), + join('contrib', 'simplification', 'ginac', 'src'), join('contrib', 'pynumero', 'src'), join('core', 'tests', 'data', 'baselines'), join('core', 'tests', 'diet', 'baselines'), From 90b1783197210cdca5dfe3c0b45bb467e6d58148 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 7 May 2024 13:09:01 -0600 Subject: [PATCH 1346/3044] Update tar filter to handle ValueError from commonpath() --- pyomo/common/download.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pyomo/common/download.py b/pyomo/common/download.py index 2a91553c728..ad672e8c79b 100644 --- a/pyomo/common/download.py +++ b/pyomo/common/download.py @@ -419,11 +419,17 @@ def filter_fcn(info): return False info.name = f = '/'.join(target[dirOffset:]) target = os.path.realpath(os.path.join(dest, f)) - if os.path.commonpath([target, dest]) != dest: - logger.error( - "potentially insecure filename (%s) resolves outside target " - "directory. Skipping file." % (f,) - ) + try: + if os.path.commonpath([target, dest]) != dest: + logger.error( + "potentially insecure filename (%s) resolves outside target " + "directory. Skipping file." % (f,) + ) + return False + except ValueError: + # commonpath() will raise ValueError for paths that + # don't have anything in common (notably, when files are + # on different drives on Windows) return False # Strip high bits & group/other write bits info.mode &= 0o755 From 5aae45946ebd302cfe0e33d54c5927d2f193a362 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 7 May 2024 13:20:22 -0600 Subject: [PATCH 1347/3044] keep mutable parameters in sympy conversion --- pyomo/core/expr/sympy_tools.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pyomo/core/expr/sympy_tools.py b/pyomo/core/expr/sympy_tools.py index 48bd542be0f..05c9885cc8c 100644 --- a/pyomo/core/expr/sympy_tools.py +++ b/pyomo/core/expr/sympy_tools.py @@ -175,10 +175,11 @@ def sympyVars(self): class Pyomo2SympyVisitor(EXPR.StreamBasedExpressionVisitor): - def __init__(self, object_map): + def __init__(self, object_map, keep_mutable_parameters=True): sympy.Add # this ensures _configure_sympy gets run super(Pyomo2SympyVisitor, self).__init__() self.object_map = object_map + self.keep_mutable_parameters = keep_mutable_parameters def initializeWalker(self, expr): return self.beforeChild(None, expr, None) @@ -212,6 +213,8 @@ def beforeChild(self, node, child, child_idx): # # Everything else is a constant... # + if self.keep_mutable_parameters and child.is_parameter_type() and child.mutable: + return False, self.object_map.getSympySymbol(child) return False, value(child) @@ -245,13 +248,15 @@ def beforeChild(self, node, child, child_idx): return True, None -def sympyify_expression(expr): +def sympyify_expression(expr, keep_mutable_parameters=True): """Convert a Pyomo expression to a Sympy expression""" # # Create the visitor and call it. # object_map = PyomoSympyBimap() - visitor = Pyomo2SympyVisitor(object_map) + visitor = Pyomo2SympyVisitor( + object_map, keep_mutable_parameters=keep_mutable_parameters + ) return object_map, visitor.walk_expression(expr) From ae5ebd3ed492405ae488921ef3c6b36c886f098a Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 7 May 2024 13:26:35 -0600 Subject: [PATCH 1348/3044] update defaults for mutable parameters when using sympy --- pyomo/contrib/simplification/simplify.py | 2 +- pyomo/core/expr/sympy_tools.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 840f3a1c1da..874b5b1e801 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -26,7 +26,7 @@ def simplify_with_sympy(expr: NumericExpression): if is_constant(expr): return value(expr) - object_map, sympy_expr = sympyify_expression(expr) + object_map, sympy_expr = sympyify_expression(expr, keep_mutable_parameters=True) new_expr = sympy2pyomo_expression(sympy_expr.simplify(), object_map) if is_constant(new_expr): new_expr = value(new_expr) diff --git a/pyomo/core/expr/sympy_tools.py b/pyomo/core/expr/sympy_tools.py index 05c9885cc8c..6c184f0e4c4 100644 --- a/pyomo/core/expr/sympy_tools.py +++ b/pyomo/core/expr/sympy_tools.py @@ -175,7 +175,7 @@ def sympyVars(self): class Pyomo2SympyVisitor(EXPR.StreamBasedExpressionVisitor): - def __init__(self, object_map, keep_mutable_parameters=True): + def __init__(self, object_map, keep_mutable_parameters=False): sympy.Add # this ensures _configure_sympy gets run super(Pyomo2SympyVisitor, self).__init__() self.object_map = object_map @@ -248,7 +248,7 @@ def beforeChild(self, node, child, child_idx): return True, None -def sympyify_expression(expr, keep_mutable_parameters=True): +def sympyify_expression(expr, keep_mutable_parameters=False): """Convert a Pyomo expression to a Sympy expression""" # # Create the visitor and call it. From e4920cbd229fc9712bf03e972c7788e9e0cb1eb6 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Tue, 7 May 2024 15:46:50 -0400 Subject: [PATCH 1349/3044] add test for call_before_subproblem_solve --- pyomo/contrib/mindtpy/tests/test_mindtpy.py | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pyomo/contrib/mindtpy/tests/test_mindtpy.py b/pyomo/contrib/mindtpy/tests/test_mindtpy.py index 37969276d55..f54e766baa4 100644 --- a/pyomo/contrib/mindtpy/tests/test_mindtpy.py +++ b/pyomo/contrib/mindtpy/tests/test_mindtpy.py @@ -101,6 +101,30 @@ def test_OA_rNLP(self): ) self.check_optimal_solution(model) + def test_OA_callback(self): + """Test the outer approximation decomposition algorithm.""" + with SolverFactory('mindtpy') as opt: + + def callback(model): + model.Y[1].value = 0 + model.Y[2].value = 0 + model.Y[3].value = 0 + + model = SimpleMINLP2() + # The callback function will make the OA method cycling. + results = opt.solve( + model, + strategy='OA', + init_strategy='rNLP', + mip_solver=required_solvers[1], + nlp_solver=required_solvers[0], + call_before_subproblem_solve=callback, + ) + self.assertIs( + results.solver.termination_condition, TerminationCondition.feasible + ) + self.assertAlmostEqual(value(results.problem.lower_bound), 5, places=1) + def test_OA_extreme_model(self): """Test the outer approximation decomposition algorithm.""" with SolverFactory('mindtpy') as opt: From 2b3bd4eea0d00e4e7e2dce3483aceaa04dcfb767 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 7 May 2024 14:38:14 -0600 Subject: [PATCH 1350/3044] update tests --- .../tests/test_simplification.py | 25 ++++++++++--------- setup.py | 1 + 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index efa9f903adc..acef0af502e 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -89,18 +89,6 @@ def test_unary(self): e2 = self.simp.simplify(e) assertExpressionsEqual(self, e, e2) - -@unittest.skipUnless(sympy_available, 'sympy is not available') -class TestSimplificationSympy(unittest.TestCase, SimplificationMixin): - def setUp(self): - self.simp = Simplifier(mode=Simplifier.Mode.sympy) - - -@unittest.skipUnless(ginac_available, 'GiNaC is not available') -class TestSimplificationGiNaC(unittest.TestCase, SimplificationMixin): - def setUp(self): - self.simp = Simplifier(mode=Simplifier.Mode.ginac) - def test_param(self): m = pe.ConcreteModel() x = m.x = pe.Var() @@ -116,5 +104,18 @@ def test_param(self): p * x + 2.0 * p * x**2.0, x**2.0 * p * 2.0 + p * x, p * x + x**2.0 * p * 2.0, + p * x * (1 + 2 * x), ], ) + + +@unittest.skipUnless(sympy_available, 'sympy is not available') +class TestSimplificationSympy(unittest.TestCase, SimplificationMixin): + def setUp(self): + self.simp = Simplifier(mode=Simplifier.Mode.sympy) + + +@unittest.skipUnless(ginac_available, 'GiNaC is not available') +class TestSimplificationGiNaC(unittest.TestCase, SimplificationMixin): + def setUp(self): + self.simp = Simplifier(mode=Simplifier.Mode.ginac) diff --git a/setup.py b/setup.py index 70c1626a650..a125b02b2fe 100644 --- a/setup.py +++ b/setup.py @@ -306,6 +306,7 @@ def __ne__(self, other): "pyomo.contrib.mcpp": ["*.cpp"], "pyomo.contrib.pynumero": ['src/*', 'src/tests/*'], "pyomo.contrib.viewer": ["*.ui"], + "pyomo.contrib.simplification.ginac": ["src/*.cpp", "src/*.hpp"], }, ext_modules=ext_modules, entry_points=""" From 7c0741a1cebff1e5b17228543e55f77c6f1413de Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 07:38:05 -0600 Subject: [PATCH 1351/3044] Make _SequenceVarData public to match recent change in pyomo/main --- pyomo/contrib/cp/repn/docplex_writer.py | 4 ++-- pyomo/contrib/cp/sequence_var.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/cp/repn/docplex_writer.py b/pyomo/contrib/cp/repn/docplex_writer.py index 53d14495c4b..6a0eb7749a8 100644 --- a/pyomo/contrib/cp/repn/docplex_writer.py +++ b/pyomo/contrib/cp/repn/docplex_writer.py @@ -33,7 +33,7 @@ from pyomo.contrib.cp.sequence_var import ( SequenceVar, ScalarSequenceVar, - _SequenceVarData, + SequenceVarData, ) from pyomo.contrib.cp.scheduling_expr.scheduling_logic import ( AlternativeExpression, @@ -1055,7 +1055,7 @@ class LogicalToDoCplex(StreamBasedExpressionVisitor): IntervalVarData: _before_interval_var, IndexedIntervalVar: _before_indexed_interval_var, ScalarSequenceVar: _before_sequence_var, - _SequenceVarData: _before_sequence_var, + SequenceVarData: _before_sequence_var, ScalarVar: _before_var, VarData: _before_var, IndexedVar: _before_indexed_var, diff --git a/pyomo/contrib/cp/sequence_var.py b/pyomo/contrib/cp/sequence_var.py index 486776f58da..cb42f445dc3 100644 --- a/pyomo/contrib/cp/sequence_var.py +++ b/pyomo/contrib/cp/sequence_var.py @@ -26,7 +26,7 @@ logger = logging.getLogger(__name__) -class _SequenceVarData(ActiveComponentData): +class SequenceVarData(ActiveComponentData): """This class defines the abstract interface for a single sequence variable.""" __slots__ = ('interval_vars',) @@ -62,7 +62,7 @@ def set_value(self, expr): @ModelComponentFactory.register("Sequences of IntervalVars") class SequenceVar(ActiveIndexedComponent): - _ComponentDataClass = _SequenceVarData + _ComponentDataClass = SequenceVarData def __new__(cls, *args, **kwds): if cls != SequenceVar: @@ -100,7 +100,7 @@ def _getitem_when_not_present(self, index): def construct(self, data=None): """ - Construct the _SequenceVarData objects for this SequenceVar + Construct the SequenceVarData objects for this SequenceVar """ if self._constructed: return @@ -140,9 +140,9 @@ def _pprint(self): ) -class ScalarSequenceVar(_SequenceVarData, SequenceVar): +class ScalarSequenceVar(SequenceVarData, SequenceVar): def __init__(self, *args, **kwds): - _SequenceVarData.__init__(self, component=self) + SequenceVarData.__init__(self, component=self) SequenceVar.__init__(self, *args, **kwds) self._index = UnindexedComponent_index From f76108584d5be2f12259055cfac0529a39c7e8c1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Wed, 8 May 2024 08:19:01 -0600 Subject: [PATCH 1352/3044] Create basic autodoc for MAiNGO --- .../appsi/appsi.solvers.maingo.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst new file mode 100644 index 00000000000..21e61c38d51 --- /dev/null +++ b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst @@ -0,0 +1,14 @@ +MAiNGO +====== + +.. autoclass:: pyomo.contrib.appsi.solvers.maingo.MAiNGOConfig + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pyomo.contrib.appsi.solvers.maingo.MAiNGO + :members: + :inherited-members: + :undoc-members: + :show-inheritance: From fbd9a0ab2d8a73babc3d67acfb6cc71d4d92677e Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Wed, 8 May 2024 08:22:32 -0600 Subject: [PATCH 1353/3044] Update APPSI TOC --- doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst index 1c598d95628..f4dcb81b4be 100644 --- a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst +++ b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst @@ -13,3 +13,4 @@ Solvers appsi.solvers.cplex appsi.solvers.cbc appsi.solvers.highs + appsi.solvers.maingo From 82dfda15e1ce4650ff3d7b7a2abfd2c90735a8bd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 08:28:12 -0600 Subject: [PATCH 1354/3044] Ensure the same output is logged on Windows and other platforms --- pyomo/common/download.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/common/download.py b/pyomo/common/download.py index ad672e8c79b..ad3b64060e9 100644 --- a/pyomo/common/download.py +++ b/pyomo/common/download.py @@ -430,6 +430,10 @@ def filter_fcn(info): # commonpath() will raise ValueError for paths that # don't have anything in common (notably, when files are # on different drives on Windows) + logger.error( + "potentially insecure filename (%s) resolves outside target " + "directory. Skipping file." % (f,) + ) return False # Strip high bits & group/other write bits info.mode &= 0o755 From 8ebd61ccd5c8a3e8f5e62418024ea73130961c68 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 09:54:39 -0600 Subject: [PATCH 1355/3044] Generate binary vectors without going through strings --- .../piecewise/transform/disaggregated_logarithmic.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py index d5de010f308..d582cdcfff5 100644 --- a/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py @@ -193,6 +193,10 @@ def x_constraint(b, i): # TODO test the Gray codes too # note: Must have num != 0 and ceil(log2(num)) > length to be valid def _get_binary_vector(self, num, length): - # Use python's string formatting instead of bothering with modular - # arithmetic. Hopefully not slow. - return tuple(int(x) for x in format(num, f"0{length}b")) + ans = [] + for i in range(length): + ans.append(num & 1) + num >>= 1 + assert not num + ans.reverse() + return tuple(ans) From 3572c445145b8e901d1de61cc842914c5ea60d8a Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 8 May 2024 10:37:39 -0600 Subject: [PATCH 1356/3044] ginac cleanup --- pyomo/contrib/simplification/build.py | 1 + pyomo/contrib/simplification/ginac/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index d540991b010..dfb9d2cf1c8 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -71,6 +71,7 @@ def build_ginac_library(parallel=None, argv=None, env=None): logger.info("\nBuilding GiNaC\n") assert subprocess.run(make_cmd, cwd=ginac_dir, env=env).returncode == 0 assert subprocess.run(install_cmd, cwd=ginac_dir, env=env).returncode == 0 + print("Installed GiNaC to %s" % (ginac_dir,)) def _find_include(libdir, incpaths): diff --git a/pyomo/contrib/simplification/ginac/__init__.py b/pyomo/contrib/simplification/ginac/__init__.py index af6511944de..6896bec12c4 100644 --- a/pyomo/contrib/simplification/ginac/__init__.py +++ b/pyomo/contrib/simplification/ginac/__init__.py @@ -30,7 +30,7 @@ def _importer(): # GiNaC needs 2 libraries that are generally dynamically linked # to the interface library. If we built those ourselves, then # the libraries will be PYOMO_CONFIG_DIR/lib ... but that - # directlor is very likely to NOT be on the library search path + # directory is very likely to NOT be on the library search path # when the Python interpreter was started. We will manually # look for those two libraries, and if we find them, load them # into this process (so the interface can find them) From a53c6f7ec4929a5821203ee0a87e8d31e88edb1d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 10:39:54 -0600 Subject: [PATCH 1357/3044] Add 'builders' marker for testing custom library builds (currently just ginac) --- .jenkins.sh | 7 +++++++ pyomo/contrib/simplification/tests/test_simplification.py | 2 ++ setup.cfg | 1 + 3 files changed, 10 insertions(+) diff --git a/.jenkins.sh b/.jenkins.sh index 37be6113ed9..37a9238f983 100644 --- a/.jenkins.sh +++ b/.jenkins.sh @@ -122,6 +122,13 @@ if test -z "$MODE" -o "$MODE" == setup; then echo "PYOMO_CONFIG_DIR=$PYOMO_CONFIG_DIR" echo "" + # Call Pyomo build scripts to build TPLs that would normally be + # skipped by the pyomo download-extensions / build-extensions + # actions below + if test [ " $CATEGORY " == *" builders "*; then + python pyomo/contrib/simplification/build.py --build-deps || exit 1 + fi + # Use Pyomo to download & compile binary extensions i=0 while /bin/true; do diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index acef0af502e..1ff9f5a3cc4 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -115,6 +115,8 @@ def setUp(self): self.simp = Simplifier(mode=Simplifier.Mode.sympy) +@unittest.pytest.mark.default +@unittest.pytest.mark.builders @unittest.skipUnless(ginac_available, 'GiNaC is not available') class TestSimplificationGiNaC(unittest.TestCase, SimplificationMixin): def setUp(self): diff --git a/setup.cfg b/setup.cfg index b606138f38c..d9ccbbb7c5e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,3 +22,4 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests + builders: thests that should be run when testing custom (extension) builders \ No newline at end of file From 8f33eed1ed2a82c24be5d5b3dff77dcab472f67e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 10:40:36 -0600 Subject: [PATCH 1358/3044] Support multiple markers (categories) in jenkins driver --- .jenkins.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.jenkins.sh b/.jenkins.sh index 37a9238f983..8c72edf41c0 100644 --- a/.jenkins.sh +++ b/.jenkins.sh @@ -43,8 +43,8 @@ fi if test -z "$SLIM"; then export VENV_SYSTEM_PACKAGES='--system-site-packages' fi -if test ! -z "$CATEGORY"; then - export PY_CAT="-m $CATEGORY" +if test -n "$CATEGORY"; then + export PY_CAT="-m '"`echo "$CATEGORY" | sed -r "s/ +/ or /g"`"'" fi if test "$WORKSPACE" != "`pwd`"; then From 4dc4e893aa861bcbeaf777e971788f5e8ce39983 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 10:41:01 -0600 Subject: [PATCH 1359/3044] Additional (debugging) output in ginac_interface builder --- pyomo/contrib/simplification/build.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index d540991b010..a1332490b1b 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -155,6 +155,7 @@ def run(self): ) if not os.path.exists(target): os.makedirs(target) + sys.stdout.write(f"Installing {library} in {target}\n") shutil.copy(library, target) package_config = { From 22ee3c76e9974960e156f567b332ddf6746c6021 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 8 May 2024 10:57:24 -0600 Subject: [PATCH 1360/3044] Updating CHANGELOG in preparation for the 6.7.2 release --- CHANGELOG.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c06e0f71378..11a9f4a3020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,78 @@ Pyomo CHANGELOG =============== +------------------------------------------------------------------------------- +Pyomo 6.7.2 (9 May 2024) +------------------------------------------------------------------------------- + +- General + - Support config domains with either method or attribute domain_name (#3159) + - Update TPL package list due to contrib.solver (#3164) + - Automate TPL callback registrations (#3167) + - Fix type registrations for ExternalFunction arguments (#3168) + - Only modify module path and spec for deferred import modules (#3176) + - Add "mixed" standard form representation (#3201) + - Support "default" dispatchers in `ExitNodeDispatcher` (#3194) + - Redefine objective sense as a proper `IntEnum` (#3224) + - Fix division-by-0 bug in linear walker (#3246) +- Core + - Allow `Var` objects in `LinearExpression.args` (#3189) + - Add type hints to components (#3173) + - Simplify expressions generated by `TemplateSumExpression` (#3196) + - Make component data public classes (#3221, #3253) + - Exploit repeated named expressions in `identify_variables` (#3190) +- Documentation + - NFC: Add link to the HOMOWP companion notebooks (#3195) + - Update installation documentation to include Cython instructions (#3208) + - Add links to the Pyomo Book Springer page (#3211) +- Solver Interfaces + - Fix division by zero error in linear presolve (#3161) + - Subprocess timeout update (#3183) + - Solver Refactor - Allow no objective (#3181) + - NLv2: handle presolved independent linear subsystems (#3193) + - Update `LegacySolverWrapper` to be compatible with the `pyomo` script (#3202) + - Fix mosek_direct to use putqconk instead of putqcon (#3199) + - Solver Refactor - Bug fixes for IDAES Integration (#3214) + - Check _skip_trivial_constraints before the constraint body (#3226) + - Fix AMPL solver duplicate funcadd (#3206) + - Solver Refactor - Fix bugs in setting `name` and `solutions` attributes (#3228) + - Disable the use of universal newlines in the ipopt_v2 NL file (#3231) + - NLv2: fix reporting numbers of nonlinear discrete variables (#3238) + - Fix: Get SCIP solving time considering float number with some text (#3234) + - Solver Refactor - Add `gurobi_direct` implementation (#3225) +- Testing + - Set maxDiff=None on the base TestCase class (#3171) + - Testing infrastructure updates (#3175) + - Typos update for March 2024 (#3219) + - Add openmpi to testing environment to work around issue in mpi4py (#3236, #3239) + - Skip black 24.4.1 due to a bug in the parser (#3247) + - Skip tests on draft and WIP pull requests (#3223) + - Update GHA to grab gurobipy from PyPI (#3254) +- GDP + - Use private_data for all mappings between original and transformed components (#3166) + - Fix a bug in gdp.bigm transformation for nested GDPs (#3213) +- Contributed Packages + - APPSI: Allow cmodel to handle non-mutable params in var and constraint bounds (#3182) + - APPSI: Allow APPSI FBBT to handle nested named Expressions (#3185) + - APPSI: Add MAiNGO solver interface (#3165) + - DoE: Bug fixes (#3245) + - incidence_analysis: Improve performance of `solve_strongly_connected_components` for + models with named expressions (#3186) + - incidence_analysis: Add function to plot incidence graph in Dulmage-Mendelsohn order (#3207) + - incidence_analysis: Require variables and constraints to be specified separately in + `IncidenceGraphInterface.remove_nodes` (#3212) + - latex_printer: Resolve errors for set operations / multidimensional sets (#3177) + - MindtPy: Add Highs support (#2971) + - MindtPy: Add call_before_subproblem_solve callback (#3251) + - Parmest: New UI using experiment lists (#3160) + - preprocessing: Fix bug where variable aggregator did not intersect domains (#3241) + - PyNumero: Allow CyIpopt to solve problems without objectives (#3163) + - PyNumero: Work around bug in CyIpopt 1.4.0 (#3222) + - PyNumero: Include "inventory" in readme (#3248) + - PyROS: Simplify custom domain validators (#3169) + - PyROS: Fix iteration logging for edge case involving discrete sets (#3170) + - PyROS: Update solver timing system (#3198) + ------------------------------------------------------------------------------- Pyomo 6.7.1 (21 Feb 2024) ------------------------------------------------------------------------------- From 3bfa3bd1e8b1610217052fdf48cf4ff63a9b5f1f Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 8 May 2024 10:57:58 -0600 Subject: [PATCH 1361/3044] Pinning to numpy<2.0.0 for the release --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 70c1626a650..8817649ecc2 100644 --- a/setup.py +++ b/setup.py @@ -256,7 +256,7 @@ def __ne__(self, other): 'sphinx-toolbox>=2.16.0', 'sphinx-jinja2-compat>=0.1.1', 'enum_tools', - 'numpy', # Needed by autodoc for pynumero + 'numpy<2.0.0', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero ], 'optional': [ @@ -271,7 +271,7 @@ def __ne__(self, other): # installed on python 3.8 'networkx<3.2; python_version<"3.9"', 'networkx; python_version>="3.9"', - 'numpy', + 'numpy<2.0.0', 'openpyxl', # dataportals #'pathos', # requested for #963, but PR currently closed 'pint', # units From 1d1f131b940e00a351fae66f8ad9260f9029084c Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 8 May 2024 11:01:22 -0600 Subject: [PATCH 1362/3044] More updates to the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a9f4a3020..683551ba03a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ Pyomo 6.7.2 (9 May 2024) - APPSI: Allow cmodel to handle non-mutable params in var and constraint bounds (#3182) - APPSI: Allow APPSI FBBT to handle nested named Expressions (#3185) - APPSI: Add MAiNGO solver interface (#3165) + - CP: Add SequenceVar and other logical expressions for scheduling (#3227) - DoE: Bug fixes (#3245) - incidence_analysis: Improve performance of `solve_strongly_connected_components` for models with named expressions (#3186) From 38b966298ace3b1b06f73501a8e436f4031a51be Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Wed, 8 May 2024 11:13:38 -0600 Subject: [PATCH 1363/3044] Reorder and merge some items --- CHANGELOG.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 683551ba03a..d61758f7c1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ Pyomo 6.7.2 (9 May 2024) - General - Support config domains with either method or attribute domain_name (#3159) - - Update TPL package list due to contrib.solver (#3164) - Automate TPL callback registrations (#3167) - Fix type registrations for ExternalFunction arguments (#3168) - Only modify module path and spec for deferred import modules (#3176) @@ -29,19 +28,18 @@ Pyomo 6.7.2 (9 May 2024) - Solver Interfaces - Fix division by zero error in linear presolve (#3161) - Subprocess timeout update (#3183) - - Solver Refactor - Allow no objective (#3181) + - Solver Refactor - Bug fixes for various components (#3181, #3214, #3228) - NLv2: handle presolved independent linear subsystems (#3193) - Update `LegacySolverWrapper` to be compatible with the `pyomo` script (#3202) - Fix mosek_direct to use putqconk instead of putqcon (#3199) - - Solver Refactor - Bug fixes for IDAES Integration (#3214) - Check _skip_trivial_constraints before the constraint body (#3226) - Fix AMPL solver duplicate funcadd (#3206) - - Solver Refactor - Fix bugs in setting `name` and `solutions` attributes (#3228) - Disable the use of universal newlines in the ipopt_v2 NL file (#3231) - NLv2: fix reporting numbers of nonlinear discrete variables (#3238) - Fix: Get SCIP solving time considering float number with some text (#3234) - Solver Refactor - Add `gurobi_direct` implementation (#3225) - Testing + - Update TPL package list due to `contrib.solver` (#3164) - Set maxDiff=None on the base TestCase class (#3171) - Testing infrastructure updates (#3175) - Typos update for March 2024 (#3219) @@ -64,7 +62,7 @@ Pyomo 6.7.2 (9 May 2024) - incidence_analysis: Require variables and constraints to be specified separately in `IncidenceGraphInterface.remove_nodes` (#3212) - latex_printer: Resolve errors for set operations / multidimensional sets (#3177) - - MindtPy: Add Highs support (#2971) + - MindtPy: Add HiGHS support (#2971) - MindtPy: Add call_before_subproblem_solve callback (#3251) - Parmest: New UI using experiment lists (#3160) - preprocessing: Fix bug where variable aggregator did not intersect domains (#3241) From e847f10f032fc2a04080af7784d671847a51adcc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 11:25:24 -0600 Subject: [PATCH 1364/3044] NFC: fix typo --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index d9ccbbb7c5e..f670cef8f68 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,4 +22,4 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests - builders: thests that should be run when testing custom (extension) builders \ No newline at end of file + builders: tests that should be run when testing custom (extension) builders \ No newline at end of file From 8b11a1c789a73219054fec5d25b8db22f42e8da8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 11:27:51 -0600 Subject: [PATCH 1365/3044] NFC: resyncing test_branches and test_pr_and_main --- .github/workflows/test_branches.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index df6455568b9..d9c36e78fc4 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -96,7 +96,7 @@ jobs: PACKAGES: openmpi mpi4py - os: ubuntu-latest - python: 3.11 + python: '3.11' other: /singletest category: "-m 'neos or importtest'" skip_doctest: 1 @@ -273,7 +273,7 @@ jobs: if test -z "${{matrix.slim}}"; then python -m pip install --cache-dir cache/pip cplex docplex \ || echo "WARNING: CPLEX Community Edition is not available" - python -m pip install --cache-dir cache/pip gurobipy==10.0.3\ + python -m pip install --cache-dir cache/pip gurobipy==10.0.3 \ || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" From 3a33d89ff10707c707fc735285afcd8213868f94 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 8 May 2024 16:49:53 -0600 Subject: [PATCH 1366/3044] More edits to the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d61758f7c1c..954231f9f2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ Pyomo 6.7.2 (9 May 2024) - MindtPy: Add HiGHS support (#2971) - MindtPy: Add call_before_subproblem_solve callback (#3251) - Parmest: New UI using experiment lists (#3160) + - piecewise: Add piecewise linear transformations (#3036) - preprocessing: Fix bug where variable aggregator did not intersect domains (#3241) - PyNumero: Allow CyIpopt to solve problems without objectives (#3163) - PyNumero: Work around bug in CyIpopt 1.4.0 (#3222) From fa2cc317eb90941c87997bb7b7935aca488cc1a6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 17:04:32 -0600 Subject: [PATCH 1367/3044] improve handling of category quotation --- .jenkins.sh | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.jenkins.sh b/.jenkins.sh index 8c72edf41c0..a00b42eac4e 100644 --- a/.jenkins.sh +++ b/.jenkins.sh @@ -43,9 +43,6 @@ fi if test -z "$SLIM"; then export VENV_SYSTEM_PACKAGES='--system-site-packages' fi -if test -n "$CATEGORY"; then - export PY_CAT="-m '"`echo "$CATEGORY" | sed -r "s/ +/ or /g"`"'" -fi if test "$WORKSPACE" != "`pwd`"; then echo "ERROR: pwd is not WORKSPACE" @@ -185,7 +182,7 @@ if test -z "$MODE" -o "$MODE" == test; then python -m pytest -v \ -W ignore::Warning \ --junitxml="TEST-pyomo.xml" \ - $PY_CAT $TEST_SUITES $PYTEST_EXTRA_ARGS + -m "$CATEGORY" $TEST_SUITES $PYTEST_EXTRA_ARGS # Combine the coverage results and upload if test -z "$DISABLE_COVERAGE"; then From bd5f10cb7d0dbd96aae6114e9ee51407e9b4dd13 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 17:07:16 -0600 Subject: [PATCH 1368/3044] Improve conftest.py efficiency --- conftest.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/conftest.py b/conftest.py index 7faad6fc89b..00abdecfa12 100644 --- a/conftest.py +++ b/conftest.py @@ -11,6 +11,8 @@ import pytest +_implicit_markers = {'default',} +_extended_implicit_markers = _implicit_markers.union({'solver',}) def pytest_runtest_setup(item): """ @@ -32,13 +34,10 @@ def pytest_runtest_setup(item): the default mode; but if solver tests are also marked with an explicit category (e.g., "expensive"), we will skip them. """ - marker = item.iter_markers() solvernames = [mark.args[0] for mark in item.iter_markers(name="solver")] solveroption = item.config.getoption("--solver") markeroption = item.config.getoption("-m") - implicit_markers = ['default'] - extended_implicit_markers = implicit_markers + ['solver'] - item_markers = set(mark.name for mark in marker) + item_markers = set(mark.name for mark in item.iter_markers()) if solveroption: if solveroption not in solvernames: pytest.skip("SKIPPED: Test not marked {!r}".format(solveroption)) @@ -46,9 +45,9 @@ def pytest_runtest_setup(item): elif markeroption: return elif item_markers: - if not set(implicit_markers).issubset( + if not _implicit_markers.issubset( item_markers - ) and not item_markers.issubset(set(extended_implicit_markers)): + ) and not item_markers.issubset(_extended_implicit_markers): pytest.skip('SKIPPED: Only running default, solver, and unmarked tests.') From 354feb2c9a81473bc9ca95138bcd8e4d4eadd85c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 17:07:46 -0600 Subject: [PATCH 1369/3044] Ensure that all unmarked tests are marked with the implicit markers --- conftest.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/conftest.py b/conftest.py index 00abdecfa12..6e8043c2c92 100644 --- a/conftest.py +++ b/conftest.py @@ -14,6 +14,18 @@ _implicit_markers = {'default',} _extended_implicit_markers = _implicit_markers.union({'solver',}) +def pytest_collection_modifyitems(items): + """ + This method will mark any unmarked tests with the implicit marker ('default') + + """ + for item in items: + try: + next(item.iter_markers()) + except StopIteration: + for marker in _implicit_markers: + item.add_marker(getattr(pytest.mark, marker)) + def pytest_runtest_setup(item): """ This method overrides pytest's default behavior for marked tests. From 92edcc5d7b486a767f0033514e13ce6549d8e81d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 17:08:29 -0600 Subject: [PATCH 1370/3044] NFC: apply black --- conftest.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/conftest.py b/conftest.py index 6e8043c2c92..34b366f9fd6 100644 --- a/conftest.py +++ b/conftest.py @@ -11,8 +11,9 @@ import pytest -_implicit_markers = {'default',} -_extended_implicit_markers = _implicit_markers.union({'solver',}) +_implicit_markers = {'default'} +_extended_implicit_markers = _implicit_markers.union({'solver'}) + def pytest_collection_modifyitems(items): """ @@ -26,6 +27,7 @@ def pytest_collection_modifyitems(items): for marker in _implicit_markers: item.add_marker(getattr(pytest.mark, marker)) + def pytest_runtest_setup(item): """ This method overrides pytest's default behavior for marked tests. @@ -57,9 +59,9 @@ def pytest_runtest_setup(item): elif markeroption: return elif item_markers: - if not _implicit_markers.issubset( - item_markers - ) and not item_markers.issubset(_extended_implicit_markers): + if not _implicit_markers.issubset(item_markers) and not item_markers.issubset( + _extended_implicit_markers + ): pytest.skip('SKIPPED: Only running default, solver, and unmarked tests.') From 7ae72756ad5e0019370ed3aaf9f327b86fd717d1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 17:47:16 -0600 Subject: [PATCH 1371/3044] Prevent APPSI / GiNaC interfaces from exposing module symbols globally --- pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp | 5 +++-- pyomo/contrib/simplification/ginac/src/ginac_interface.cpp | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp b/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp index 6acc1d79845..5a838ffd786 100644 --- a/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp +++ b/pyomo/contrib/appsi/cmodel/src/cmodel_bindings.cpp @@ -63,7 +63,8 @@ PYBIND11_MODULE(appsi_cmodel, m) { m.def("appsi_exprs_from_pyomo_exprs", &appsi_exprs_from_pyomo_exprs); m.def("appsi_expr_from_pyomo_expr", &appsi_expr_from_pyomo_expr); m.def("prep_for_repn", &prep_for_repn); - py::class_(m, "PyomoExprTypes").def(py::init<>()); + py::class_(m, "PyomoExprTypes", py::module_local()) + .def(py::init<>()); py::class_>(m, "Node") .def("is_variable_type", &Node::is_variable_type) .def("is_param_type", &Node::is_param_type) @@ -165,7 +166,7 @@ PYBIND11_MODULE(appsi_cmodel, m) { .def(py::init<>()) .def("write", &LPWriter::write) .def("get_solve_cons", &LPWriter::get_solve_cons); - py::enum_(m, "ExprType") + py::enum_(m, "ExprType", py::module_local()) .value("py_float", ExprType::py_float) .value("var", ExprType::var) .value("param", ExprType::param) diff --git a/pyomo/contrib/simplification/ginac/src/ginac_interface.cpp b/pyomo/contrib/simplification/ginac/src/ginac_interface.cpp index 1060f87161c..9b05baf71ca 100644 --- a/pyomo/contrib/simplification/ginac/src/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac/src/ginac_interface.cpp @@ -298,7 +298,8 @@ py::object GinacInterface::from_ginac(ex &ge) { PYBIND11_MODULE(ginac_interface, m) { m.def("pyomo_to_ginac", &pyomo_to_ginac); - py::class_(m, "PyomoExprTypes").def(py::init<>()); + py::class_(m, "PyomoExprTypes", py::module_local()) + .def(py::init<>()); py::class_(m, "ginac_expression") .def("expand", [](ex &ge) { return ge.expand(); @@ -313,7 +314,7 @@ PYBIND11_MODULE(ginac_interface, m) { .def(py::init()) .def("to_ginac", &GinacInterface::to_ginac) .def("from_ginac", &GinacInterface::from_ginac); - py::enum_(m, "ExprType") + py::enum_(m, "ExprType", py::module_local()) .value("py_float", ExprType::py_float) .value("var", ExprType::var) .value("param", ExprType::param) From 1409aa2956159b8a062e12ed831db79b57dda189 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 18:09:15 -0600 Subject: [PATCH 1372/3044] Fix bug in Jenkins driver --- .jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.jenkins.sh b/.jenkins.sh index a00b42eac4e..842733e471b 100644 --- a/.jenkins.sh +++ b/.jenkins.sh @@ -122,7 +122,7 @@ if test -z "$MODE" -o "$MODE" == setup; then # Call Pyomo build scripts to build TPLs that would normally be # skipped by the pyomo download-extensions / build-extensions # actions below - if test [ " $CATEGORY " == *" builders "*; then + if test [[ " $CATEGORY " == *" builders "* ]]; then python pyomo/contrib/simplification/build.py --build-deps || exit 1 fi From aa756017fc23091764a63721dc80c94181cbe01a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 18:29:43 -0600 Subject: [PATCH 1373/3044] Fix typo in Jenkins driver --- .jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.jenkins.sh b/.jenkins.sh index 842733e471b..0f4e70d3cf1 100644 --- a/.jenkins.sh +++ b/.jenkins.sh @@ -122,7 +122,7 @@ if test -z "$MODE" -o "$MODE" == setup; then # Call Pyomo build scripts to build TPLs that would normally be # skipped by the pyomo download-extensions / build-extensions # actions below - if test [[ " $CATEGORY " == *" builders "* ]]; then + if [[ " $CATEGORY " == *" builders "* ]]; then python pyomo/contrib/simplification/build.py --build-deps || exit 1 fi From cdaff17f6a7e7c36428df887acdc7a7a59ec836a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 18:53:56 -0600 Subject: [PATCH 1374/3044] Add info to the build log --- .jenkins.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.jenkins.sh b/.jenkins.sh index 0f4e70d3cf1..696847fd92c 100644 --- a/.jenkins.sh +++ b/.jenkins.sh @@ -123,13 +123,19 @@ if test -z "$MODE" -o "$MODE" == setup; then # skipped by the pyomo download-extensions / build-extensions # actions below if [[ " $CATEGORY " == *" builders "* ]]; then + echo "" + echo "Running local build scripts..." + echo "" + set -x python pyomo/contrib/simplification/build.py --build-deps || exit 1 + set +x fi # Use Pyomo to download & compile binary extensions i=0 while /bin/true; do i=$[$i+1] + echo "" echo "Downloading pyomo extensions (attempt $i)" pyomo download-extensions $PYOMO_DOWNLOAD_ARGS if test $? == 0; then From 76d53de4d383cd03eadaa4997f4deecebec6d4f1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 18:54:19 -0600 Subject: [PATCH 1375/3044] Explicitly call out CWD when running configure --- pyomo/contrib/simplification/build.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 53133c2fb4e..b4bec63088a 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -27,7 +27,11 @@ def build_ginac_library(parallel=None, argv=None, env=None): sys.stdout.write("\n**** Building GiNaC library ****\n") - configure_cmd = ['configure', '--prefix=' + PYOMO_CONFIG_DIR, '--disable-static'] + configure_cmd = [ + os.path.join('.', 'configure'), + '--prefix=' + PYOMO_CONFIG_DIR, + '--disable-static', + ] make_cmd = ['make'] if parallel: make_cmd.append(f'-j{parallel}') From 35b71f87a1d070b10553119c86489f99875a6587 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 8 May 2024 20:17:31 -0600 Subject: [PATCH 1376/3044] Removing singletest from test_branches --- .github/workflows/test_branches.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index d9c36e78fc4..5063571c65f 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -95,14 +95,6 @@ jobs: PYENV: conda PACKAGES: openmpi mpi4py - - os: ubuntu-latest - python: '3.11' - other: /singletest - category: "-m 'neos or importtest'" - skip_doctest: 1 - TARGET: linux - PYENV: pip - - os: ubuntu-latest python: '3.10' other: /cython From f650f9645b27536e1ea45d36fc15af9b3fbc6f6e Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Thu, 9 May 2024 08:35:51 -0600 Subject: [PATCH 1377/3044] More edits to the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 954231f9f2a..922e072250e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ Pyomo 6.7.2 (9 May 2024) - PyROS: Simplify custom domain validators (#3169) - PyROS: Fix iteration logging for edge case involving discrete sets (#3170) - PyROS: Update solver timing system (#3198) + - simplification: New module for expression simplification using GiNaC or SymPy (#3088) ------------------------------------------------------------------------------- Pyomo 6.7.1 (21 Feb 2024) From 15b52dbabddcbc53a37ecc368d3a38d01e1482fa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 9 May 2024 09:27:25 -0600 Subject: [PATCH 1378/3044] Setting final deprecation version strings --- pyomo/common/dependencies.py | 6 +++--- pyomo/common/numeric_types.py | 2 +- pyomo/contrib/incidence_analysis/interface.py | 6 +++--- pyomo/contrib/parmest/parmest.py | 6 ++---- pyomo/core/base/__init__.py | 8 ++++---- pyomo/core/base/block.py | 2 +- pyomo/core/base/boolean_var.py | 4 ++-- pyomo/core/base/component.py | 6 +++--- pyomo/core/base/connector.py | 2 +- pyomo/core/base/constraint.py | 4 ++-- pyomo/core/base/expression.py | 6 +++--- pyomo/core/base/logical_constraint.py | 4 ++-- pyomo/core/base/objective.py | 4 ++-- pyomo/core/base/param.py | 2 +- pyomo/core/base/piecewise.py | 2 +- pyomo/core/base/set.py | 16 ++++++++-------- pyomo/core/base/sos.py | 2 +- pyomo/core/base/var.py | 4 ++-- pyomo/core/expr/numvalue.py | 2 +- pyomo/gdp/disjunct.py | 4 ++-- pyomo/mpec/complementarity.py | 2 +- pyomo/network/arc.py | 2 +- pyomo/network/port.py | 2 +- 23 files changed, 48 insertions(+), 50 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index ea9efe370f7..4c9e43002ef 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -611,7 +611,7 @@ def attempt_import( want to import/return the first one that is available. defer_check: bool, optional - DEPRECATED: renamed to ``defer_import`` (deprecated in version 6.7.2.dev0) + DEPRECATED: renamed to ``defer_import`` (deprecated in version 6.7.2) defer_import: bool, optional If True, then the attempted import is deferred until the first @@ -674,7 +674,7 @@ def attempt_import( if defer_check is not None: deprecation_warning( 'defer_check=%s is deprecated. Please use defer_import' % (defer_check,), - version='6.7.2.dev0', + version='6.7.2', ) assert defer_import is None defer_import = defer_check @@ -787,7 +787,7 @@ def _perform_import( @deprecated( "``declare_deferred_modules_as_importable()`` is deprecated. " "Use the :py:class:`declare_modules_as_importable` context manager.", - version='6.7.2.dev0', + version='6.7.2', ) def declare_deferred_modules_as_importable(globals_dict): """Make all :py:class:`DeferredImportModules` in ``globals_dict`` importable diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index 8b48c77b5b2..2b63038e125 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -64,7 +64,7 @@ relocated_module_attribute( 'pyomo_constant_types', 'pyomo.common.numeric_types._pyomo_constant_types', - version='6.7.2.dev0', + version='6.7.2', msg="The pyomo_constant_types set will be removed in the future: the set " "contained only NumericConstant and _PythonCallbackFunctionID, and provided " "no meaningful value to clients or walkers. Users should likely handle " diff --git a/pyomo/contrib/incidence_analysis/interface.py b/pyomo/contrib/incidence_analysis/interface.py index b73ec17f36c..73d9722eb7e 100644 --- a/pyomo/contrib/incidence_analysis/interface.py +++ b/pyomo/contrib/incidence_analysis/interface.py @@ -891,9 +891,9 @@ def remove_nodes(self, variables=None, constraints=None): .. note:: - **Deprecation in Pyomo v6.7.2.dev0** + **Deprecation in Pyomo v6.7.2** - The pre-6.7.2.dev0 implementation of ``remove_nodes`` allowed variables and + The pre-6.7.2 implementation of ``remove_nodes`` allowed variables and constraints to remove to be specified in a single list. This made error checking difficult, and indeed, if invalid components were provided, we carried on silently instead of throwing an error or @@ -923,7 +923,7 @@ def remove_nodes(self, variables=None, constraints=None): if any(var in self._con_index_map for var in variables) or any( con in self._var_index_map for con in constraints ): - deprecation_warning(depr_msg, version="6.7.2.dev0") + deprecation_warning(depr_msg, version="6.7.2") # If we received variables/constraints in the same list, sort them. # Any unrecognized objects will be caught by _validate_input. for var in variables: diff --git a/pyomo/contrib/parmest/parmest.py b/pyomo/contrib/parmest/parmest.py index 70f9de8b84c..41e7792570b 100644 --- a/pyomo/contrib/parmest/parmest.py +++ b/pyomo/contrib/parmest/parmest.py @@ -68,8 +68,6 @@ from pyomo.common.deprecation import deprecated from pyomo.common.deprecation import deprecation_warning -DEPRECATION_VERSION = '6.7.2.dev0' - parmest_available = numpy_available & pandas_available & scipy_available inverse_reduced_hessian, inverse_reduced_hessian_available = attempt_import( @@ -338,7 +336,7 @@ def _deprecated_init( "You're using the deprecated parmest interface (model_function, " "data, theta_names). This interface will be removed in a future release, " "please update to the new parmest interface using experiment lists.", - version=DEPRECATION_VERSION, + version='6.7.2', ) self.pest_deprecated = _DeprecatedEstimator( model_function, @@ -1386,7 +1384,7 @@ def confidence_region_test( ################################ -@deprecated(version=DEPRECATION_VERSION) +@deprecated(version='6.7.2') def group_data(data, groupby_column_name, use_mean=None): """ Group data by scenario diff --git a/pyomo/core/base/__init__.py b/pyomo/core/base/__init__.py index 2b21725d82f..6b295196864 100644 --- a/pyomo/core/base/__init__.py +++ b/pyomo/core/base/__init__.py @@ -163,13 +163,13 @@ ) # Historically, only a subset of "private" component data classes were imported here relocated_module_attribute( - f'_GeneralVarData', f'pyomo.core.base.VarData', version='6.7.2.dev0' + f'_GeneralVarData', f'pyomo.core.base.VarData', version='6.7.2' ) relocated_module_attribute( - f'_GeneralBooleanVarData', f'pyomo.core.base.BooleanVarData', version='6.7.2.dev0' + f'_GeneralBooleanVarData', f'pyomo.core.base.BooleanVarData', version='6.7.2' ) relocated_module_attribute( - f'_ExpressionData', f'pyomo.core.base.NamedExpressionData', version='6.7.2.dev0' + f'_ExpressionData', f'pyomo.core.base.NamedExpressionData', version='6.7.2' ) for _cdata in ( 'ConstraintData', @@ -179,7 +179,7 @@ 'ObjectiveData', ): relocated_module_attribute( - f'_{_cdata}', f'pyomo.core.base.{_cdata}', version='6.7.2.dev0' + f'_{_cdata}', f'pyomo.core.base.{_cdata}', version='6.7.2' ) del _cdata del relocated_module_attribute diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 2f5bdf85f6a..653809e0419 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -1983,7 +1983,7 @@ def private_data(self, scope=None): class _BlockData(metaclass=RenamedClass): __renamed__new_class__ = BlockData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register( diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index 67c06bdacce..db9a41fceda 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -252,12 +252,12 @@ def free(self): class _BooleanVarData(metaclass=RenamedClass): __renamed__new_class__ = BooleanVarData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _GeneralBooleanVarData(metaclass=RenamedClass): __renamed__new_class__ = BooleanVarData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Logical decision variables.") diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index d06b85dcdd4..966ce8c0737 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -477,7 +477,7 @@ def _pprint_base_impl( class _ComponentBase(metaclass=RenamedClass): __renamed__new_class__ = ComponentBase - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class Component(ComponentBase): @@ -663,7 +663,7 @@ def getname(self, fully_qualified=False, name_buffer=None, relative_to=None): "use of this argument poses risks if the buffer contains " "names relative to different Blocks in the model hierarchy or " "a mixture of local and fully_qualified names.", - version='TODO', + version='6.4.1', ) name_buffer[id(self)] = ans return ans @@ -922,7 +922,7 @@ def getname(self, fully_qualified=False, name_buffer=None, relative_to=None): "use of this argument poses risks if the buffer contains " "names relative to different Blocks in the model hierarchy or " "a mixture of local and fully_qualified names.", - version='TODO', + version='6.4.1', ) if id(self) in name_buffer: # Return the name if it is in the buffer diff --git a/pyomo/core/base/connector.py b/pyomo/core/base/connector.py index e383b52fc11..1363f5abd65 100644 --- a/pyomo/core/base/connector.py +++ b/pyomo/core/base/connector.py @@ -107,7 +107,7 @@ def _iter_vars(self): class _ConnectorData(metaclass=RenamedClass): __renamed__new_class__ = ConnectorData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register( diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index eb4af76fdc1..e12860991c2 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -577,12 +577,12 @@ def slack(self): class _ConstraintData(metaclass=RenamedClass): __renamed__new_class__ = ConstraintData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _GeneralConstraintData(metaclass=RenamedClass): __renamed__new_class__ = ConstraintData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("General constraint expressions.") diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index 013c388e6e5..a5120759236 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -198,12 +198,12 @@ def __ipow__(self, other): class _ExpressionData(metaclass=RenamedClass): __renamed__new_class__ = NamedExpressionData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _GeneralExpressionDataImpl(metaclass=RenamedClass): __renamed__new_class__ = NamedExpressionData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class ExpressionData(NamedExpressionData, ComponentData): @@ -231,7 +231,7 @@ def __init__(self, expr=None, component=None): class _GeneralExpressionData(metaclass=RenamedClass): __renamed__new_class__ = ExpressionData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register( diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index 9584078307d..cc0780fd9bd 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -124,12 +124,12 @@ def get_value(self): class _LogicalConstraintData(metaclass=RenamedClass): __renamed__new_class__ = LogicalConstraintData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _GeneralLogicalConstraintData(metaclass=RenamedClass): __renamed__new_class__ = LogicalConstraintData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("General logical constraints.") diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index e388d25aab4..f1204f2a09c 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -145,12 +145,12 @@ def set_sense(self, sense): class _ObjectiveData(metaclass=RenamedClass): __renamed__new_class__ = ObjectiveData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _GeneralObjectiveData(metaclass=RenamedClass): __renamed__new_class__ = ObjectiveData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Expressions that are minimized or maximized.") diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 9af6a37de45..45de3286589 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -254,7 +254,7 @@ def _compute_polynomial_degree(self, result): class _ParamData(metaclass=RenamedClass): __renamed__new_class__ = ParamData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register( diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index efe500dbfb1..8c5f34d2b53 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -274,7 +274,7 @@ def __call__(self, x): class _PiecewiseData(metaclass=RenamedClass): __renamed__new_class__ = PiecewiseData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _SimpleSinglePiecewise(object): diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index b9a2fe72e1d..8b7c2a246d6 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1179,12 +1179,12 @@ def __gt__(self, other): class _SetData(metaclass=RenamedClass): __renamed__new_class__ = SetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _SetDataBase(metaclass=RenamedClass): __renamed__new_class__ = SetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _FiniteSetMixin(object): @@ -1471,7 +1471,7 @@ def pop(self): class _FiniteSetData(metaclass=RenamedClass): __renamed__new_class__ = FiniteSetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _ScalarOrderedSetMixin(object): @@ -1736,7 +1736,7 @@ def ord(self, item): class _OrderedSetData(metaclass=RenamedClass): __renamed__new_class__ = OrderedSetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class InsertionOrderSetData(OrderedSetData): @@ -1775,7 +1775,7 @@ def update(self, values): class _InsertionOrderSetData(metaclass=RenamedClass): __renamed__new_class__ = InsertionOrderSetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _SortedSetMixin(object): @@ -1871,7 +1871,7 @@ def _sort(self): class _SortedSetData(metaclass=RenamedClass): __renamed__new_class__ = SortedSetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' ############################################################################ @@ -2669,7 +2669,7 @@ def ranges(self): class _InfiniteRangeSetData(metaclass=RenamedClass): __renamed__new_class__ = InfiniteRangeSetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class FiniteRangeSetData( @@ -2782,7 +2782,7 @@ def ord(self, item): class _FiniteRangeSetData(metaclass=RenamedClass): __renamed__new_class__ = FiniteRangeSetData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register( diff --git a/pyomo/core/base/sos.py b/pyomo/core/base/sos.py index 4a8afb05d71..afd52c111bc 100644 --- a/pyomo/core/base/sos.py +++ b/pyomo/core/base/sos.py @@ -103,7 +103,7 @@ def set_items(self, variables, weights): class _SOSConstraintData(metaclass=RenamedClass): __renamed__new_class__ = SOSConstraintData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("SOS constraint expressions.") diff --git a/pyomo/core/base/var.py b/pyomo/core/base/var.py index 8870fc5b09c..38d1d38a864 100644 --- a/pyomo/core/base/var.py +++ b/pyomo/core/base/var.py @@ -572,12 +572,12 @@ def _process_bound(self, val, bound_type): class _VarData(metaclass=RenamedClass): __renamed__new_class__ = VarData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' class _GeneralVarData(metaclass=RenamedClass): __renamed__new_class__ = VarData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Decision variables.") diff --git a/pyomo/core/expr/numvalue.py b/pyomo/core/expr/numvalue.py index 3b335bd5fc4..96e2f50b3f8 100644 --- a/pyomo/core/expr/numvalue.py +++ b/pyomo/core/expr/numvalue.py @@ -47,7 +47,7 @@ relocated_module_attribute( 'pyomo_constant_types', 'pyomo.common.numeric_types._pyomo_constant_types', - version='6.7.2.dev0', + version='6.7.2', f_globals=globals(), msg="The pyomo_constant_types set will be removed in the future: the set " "contained only NumericConstant and _PythonCallbackFunctionID, and provided " diff --git a/pyomo/gdp/disjunct.py b/pyomo/gdp/disjunct.py index 658ead27783..637f55cbed1 100644 --- a/pyomo/gdp/disjunct.py +++ b/pyomo/gdp/disjunct.py @@ -450,7 +450,7 @@ def _activate_without_unfixing_indicator(self): class _DisjunctData(metaclass=RenamedClass): __renamed__new_class__ = DisjunctData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Disjunctive blocks.") @@ -627,7 +627,7 @@ def set_value(self, expr): class _DisjunctionData(metaclass=RenamedClass): __renamed__new_class__ = DisjunctionData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Disjunction expressions.") diff --git a/pyomo/mpec/complementarity.py b/pyomo/mpec/complementarity.py index aa8db922145..26968ef9fca 100644 --- a/pyomo/mpec/complementarity.py +++ b/pyomo/mpec/complementarity.py @@ -181,7 +181,7 @@ def set_value(self, cc): class _ComplementarityData(metaclass=RenamedClass): __renamed__new_class__ = ComplementarityData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Complementarity conditions.") diff --git a/pyomo/network/arc.py b/pyomo/network/arc.py index 5e68f181a38..f2597b4c1bd 100644 --- a/pyomo/network/arc.py +++ b/pyomo/network/arc.py @@ -248,7 +248,7 @@ def _validate_ports(self, source, destination, ports): class _ArcData(metaclass=RenamedClass): __renamed__new_class__ = ArcData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register("Component used for connecting two Ports.") diff --git a/pyomo/network/port.py b/pyomo/network/port.py index ee5c915d8db..f6706dce644 100644 --- a/pyomo/network/port.py +++ b/pyomo/network/port.py @@ -287,7 +287,7 @@ def get_split_fraction(self, arc): class _PortData(metaclass=RenamedClass): __renamed__new_class__ = PortData - __renamed__version__ = '6.7.2.dev0' + __renamed__version__ = '6.7.2' @ModelComponentFactory.register( From 28c158c9dbfce3928d8d14afbc8fab2bd9017d4a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 9 May 2024 09:27:53 -0600 Subject: [PATCH 1379/3044] Finalizing release information --- .coin-or/projDesc.xml | 4 ++-- CHANGELOG.md | 26 ++++++++++++++------------ RELEASE.md | 9 +++++++-- pyomo/version/info.py | 4 ++-- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/.coin-or/projDesc.xml b/.coin-or/projDesc.xml index da977677d1f..073efd968a7 100644 --- a/.coin-or/projDesc.xml +++ b/.coin-or/projDesc.xml @@ -227,8 +227,8 @@ Carl D. Laird, Chair, Pyomo Management Committee, claird at andrew dot cmu dot e Use explicit overrides to disable use of automated version reporting. --> - 6.7.1 - 6.7.1 + 6.7.2 + 6.7.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 922e072250e..11b4ecbf785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ Pyomo 6.7.2 (9 May 2024) - Subprocess timeout update (#3183) - Solver Refactor - Bug fixes for various components (#3181, #3214, #3228) - NLv2: handle presolved independent linear subsystems (#3193) - - Update `LegacySolverWrapper` to be compatible with the `pyomo` script (#3202) + - Update `LegacySolverWrapper` compatibility with the `pyomo` script (#3202) - Fix mosek_direct to use putqconk instead of putqcon (#3199) - Check _skip_trivial_constraints before the constraint body (#3226) - Fix AMPL solver duplicate funcadd (#3206) @@ -43,37 +43,39 @@ Pyomo 6.7.2 (9 May 2024) - Set maxDiff=None on the base TestCase class (#3171) - Testing infrastructure updates (#3175) - Typos update for March 2024 (#3219) - - Add openmpi to testing environment to work around issue in mpi4py (#3236, #3239) + - Add openmpi to testing environment to resolve issue in mpi4py (#3236, #3239) - Skip black 24.4.1 due to a bug in the parser (#3247) - Skip tests on draft and WIP pull requests (#3223) - Update GHA to grab gurobipy from PyPI (#3254) - GDP - - Use private_data for all mappings between original and transformed components (#3166) + - Use private_data for all original / transformed component mappings (#3166) - Fix a bug in gdp.bigm transformation for nested GDPs (#3213) - Contributed Packages - - APPSI: Allow cmodel to handle non-mutable params in var and constraint bounds (#3182) + - APPSI: cmodel: handle non-mutable params in var / constraint bounds (#3182) - APPSI: Allow APPSI FBBT to handle nested named Expressions (#3185) - APPSI: Add MAiNGO solver interface (#3165) - CP: Add SequenceVar and other logical expressions for scheduling (#3227) - DoE: Bug fixes (#3245) - - incidence_analysis: Improve performance of `solve_strongly_connected_components` for - models with named expressions (#3186) - - incidence_analysis: Add function to plot incidence graph in Dulmage-Mendelsohn order (#3207) - - incidence_analysis: Require variables and constraints to be specified separately in - `IncidenceGraphInterface.remove_nodes` (#3212) - - latex_printer: Resolve errors for set operations / multidimensional sets (#3177) + - iis: Add minimal intractable system infeasibility diagnostics (#3172) + - incidence_analysis: Improve `solve_strongly_connected_components` + performance for models with named expressions (#3186) + - incidence_analysis: Add function to plot incidence graph in + Dulmage-Mendelsohn order (#3207) + - incidence_analysis: Require variables and constraints to be specified + separately in `IncidenceGraphInterface.remove_nodes` (#3212) + - latex_printer: bugfix for set operations / multidimensional sets (#3177) - MindtPy: Add HiGHS support (#2971) - MindtPy: Add call_before_subproblem_solve callback (#3251) - Parmest: New UI using experiment lists (#3160) - piecewise: Add piecewise linear transformations (#3036) - - preprocessing: Fix bug where variable aggregator did not intersect domains (#3241) + - preprocessing: bugfix: intersect domains in variable aggregator (#3241) - PyNumero: Allow CyIpopt to solve problems without objectives (#3163) - PyNumero: Work around bug in CyIpopt 1.4.0 (#3222) - PyNumero: Include "inventory" in readme (#3248) - PyROS: Simplify custom domain validators (#3169) - PyROS: Fix iteration logging for edge case involving discrete sets (#3170) - PyROS: Update solver timing system (#3198) - - simplification: New module for expression simplification using GiNaC or SymPy (#3088) + - simplification: expression simplification using GiNaC or SymPy (#3088) ------------------------------------------------------------------------------- Pyomo 6.7.1 (21 Feb 2024) diff --git a/RELEASE.md b/RELEASE.md index 9b101e0999a..b0228e53944 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,4 +1,4 @@ -We are pleased to announce the release of Pyomo 6.7.1. +We are pleased to announce the release of Pyomo 6.7.2. Pyomo is a collection of Python software packages that supports a diverse set of optimization capabilities for formulating and analyzing @@ -10,9 +10,14 @@ The following are highlights of the 6.7 release series: - Removed support for Python 3.7 - New writer for converting linear models to matrix form - Improved handling of nested GDPs + - Redesigned user API for parameter estimation - New packages: - - latex_printer (print Pyomo models to a LaTeX compatible format) + - iis: new capability for identifying minimal intractable systems + - latex_printer: print Pyomo models to a LaTeX compatible format - contrib.solver: preview of redesigned solver interfaces + - simplification: simplify Pyomo expressions + - New solver interfaces + - MAiNGO: Mixed-integer nonlinear global optimization - ...and of course numerous minor bug fixes and performance enhancements A full list of updates and changes is available in the diff --git a/pyomo/version/info.py b/pyomo/version/info.py index de2efe83fb6..b3538ad5868 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -27,8 +27,8 @@ major = 6 minor = 7 micro = 2 -releaselevel = 'invalid' -# releaselevel = 'final' +# releaselevel = 'invalid' +releaselevel = 'final' serial = 0 if releaselevel == 'final': From d5e5136b317603f0fe0e25b1d31457e25388212f Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Thu, 9 May 2024 11:05:29 -0600 Subject: [PATCH 1380/3044] Unpinning numpy version requirement --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 2e8cf1d5095..a125b02b2fe 100644 --- a/setup.py +++ b/setup.py @@ -256,7 +256,7 @@ def __ne__(self, other): 'sphinx-toolbox>=2.16.0', 'sphinx-jinja2-compat>=0.1.1', 'enum_tools', - 'numpy<2.0.0', # Needed by autodoc for pynumero + 'numpy', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero ], 'optional': [ @@ -271,7 +271,7 @@ def __ne__(self, other): # installed on python 3.8 'networkx<3.2; python_version<"3.9"', 'networkx; python_version>="3.9"', - 'numpy<2.0.0', + 'numpy', 'openpyxl', # dataportals #'pathos', # requested for #963, but PR currently closed 'pint', # units From 167f2b14bf4c9d6a719f08a343128880bb04342c Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Thu, 9 May 2024 11:06:37 -0600 Subject: [PATCH 1381/3044] Resetting main for development (6.7.3.dev0) --- pyomo/version/info.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/version/info.py b/pyomo/version/info.py index b3538ad5868..2d50dfe7b5e 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -26,9 +26,9 @@ # main and needs a hard reference to "suitably new" development. major = 6 minor = 7 -micro = 2 -# releaselevel = 'invalid' -releaselevel = 'final' +micro = 3 +releaselevel = 'invalid' +#releaselevel = 'final' serial = 0 if releaselevel == 'final': From 64a96147500cf0e71de8f955b3dfa92bf370799b Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Thu, 9 May 2024 11:10:11 -0600 Subject: [PATCH 1382/3044] Update info.py --- pyomo/version/info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/version/info.py b/pyomo/version/info.py index 2d50dfe7b5e..36945e8e011 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -28,7 +28,7 @@ minor = 7 micro = 3 releaselevel = 'invalid' -#releaselevel = 'final' +# releaselevel = 'final' serial = 0 if releaselevel == 'final': From b8d91c86473fd8709fc6b430fecd4bcd2fbddff6 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 12 May 2024 11:54:04 -0600 Subject: [PATCH 1383/3044] Fixing before_var handler so that we always use the values of fixed Vars regardless of if they are parameters or Vars from the perspective of the walker --- pyomo/repn/parameterized_linear.py | 33 ++++++++++--------- pyomo/repn/tests/test_parameterized_linear.py | 13 +++++--- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index 9df0ea458db..b180ee91862 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -160,22 +160,23 @@ def _before_general_expression(visitor, child): @staticmethod def _before_var(visitor, child): - if child not in visitor.wrt: + _id = id(child) + if _id not in visitor.var_map: + if child.fixed: + return False, ( + _CONSTANT, + visitor.check_constant(child.value, child), + ) + if child in visitor.wrt: + # psueudo-constant + # We aren't treating this Var as a Var for the purposes of this walker + return False, (_CONSTANT, child) # This is a normal situation - _id = id(child) - if _id not in visitor.var_map: - if child.fixed: - return False, ( - _CONSTANT, - visitor.check_constant(child.value, child), - ) - MultiLevelLinearBeforeChildDispatcher._record_var(visitor, child) - ans = visitor.Result() - ans.linear[_id] = 1 - return False, (ExprType.LINEAR, ans) - else: - # We aren't treating this Var as a Var for the purposes of this walker - return False, (_CONSTANT, child) + # TODO: override record var to not record things in wrt + MultiLevelLinearBeforeChildDispatcher._record_var(visitor, child) + ans = visitor.Result() + ans.linear[_id] = 1 + return False, (ExprType.LINEAR, ans) _before_child_dispatcher = MultiLevelLinearBeforeChildDispatcher() @@ -259,7 +260,7 @@ def finalizeResult(self, result): for vid, coef in zeros: del ans.linear[vid] elif not mult: - # the mulltiplier has cleared out the entire expression. + # the multiplier has cleared out the entire expression. # Warn if this is suppressing a NaN (unusual, and # non-standard, but we will wait to remove this behavior # for the time being) diff --git a/pyomo/repn/tests/test_parameterized_linear.py b/pyomo/repn/tests/test_parameterized_linear.py index 32f58dbfc13..b5e0a1a0348 100644 --- a/pyomo/repn/tests/test_parameterized_linear.py +++ b/pyomo/repn/tests/test_parameterized_linear.py @@ -298,7 +298,8 @@ def test_ANY_over_constant_division(self): m.x = Var() m.z = Var() m.y = Var() - # We aren't treating this as a Var, so we don't really care that it's fixed. + # We will use the fixed value regardless of the fact that we aren't + # treating this as a Var. m.y.fix(1) expr = m.y + m.x + m.z + ((3 * m.z * m.x) / m.p) / m.y @@ -308,10 +309,10 @@ def test_ANY_over_constant_division(self): ) self.assertEqual(repn.multiplier, 1) - assertExpressionsEqual(self, repn.constant, m.y + m.z) + assertExpressionsEqual(self, repn.constant, 1 + m.z) self.assertEqual(len(repn.linear), 1) print(repn.linear[id(m.x)]) - assertExpressionsEqual(self, repn.linear[id(m.x)], 1 + 1.5 * m.z / m.y) + assertExpressionsEqual(self, repn.linear[id(m.x)], 1 + 1.5 * m.z) self.assertEqual(repn.nonlinear, None) def test_errors_propogate_nan(self): @@ -335,10 +336,10 @@ def test_errors_propogate_nan(self): "\texpression: 3*z*x/p\n", ) self.assertEqual(repn.multiplier, 1) - assertExpressionsEqual(self, repn.constant, m.y + m.z) + assertExpressionsEqual(self, repn.constant, 1 + m.z) self.assertEqual(len(repn.linear), 1) self.assertIsInstance(repn.linear[id(m.x)], InvalidNumber) - assertExpressionsEqual(self, repn.linear[id(m.x)].value, 1 + float('nan') / m.y) + assertExpressionsEqual(self, repn.linear[id(m.x)].value, 1 + float('nan')) self.assertEqual(repn.nonlinear, None) m.y.fix(None) @@ -347,6 +348,8 @@ def test_errors_propogate_nan(self): expr ) self.assertEqual(repn.multiplier, 1) + # TODO: Is this expected to just wrap up into a single InvalidNumber? + print(repn.constant) self.assertIsInstance(repn.constant, InvalidNumber) assertExpressionsEqual(self, repn.constant.value, float('nan') * m.z + 3) self.assertEqual(repn.linear, {}) From b4101b7bf7eb88fde08133839aa080056deab853 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Sun, 12 May 2024 12:01:14 -0600 Subject: [PATCH 1384/3044] Extending the ExprType enum to include 'pseudo constant', bwahahaha --- pyomo/repn/parameterized_linear.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index b180ee91862..2163b544184 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -10,8 +10,10 @@ # ___________________________________________________________________________ import copy +import enum from pyomo.common.collections import ComponentSet +from pyomo.common.enums import ExtendedEnumType from pyomo.common.numeric_types import native_numeric_types from pyomo.core import Var from pyomo.core.expr.logical_expr import _flattened @@ -34,7 +36,12 @@ from pyomo.repn.util import ExprType from . import linear -_CONSTANT = ExprType.CONSTANT + +class ParameterizedExprType(enum.IntEnum, metaclass=ExtendedEnumType): + __base_enum__ = ExprType + PSUEDO_CONSTANT = 50 + +_CONSTANT = ParameterizedExprType.CONSTANT def _merge_dict(dest_dict, mult, src_dict): From ccb66b17630efcebbe719167dcd964e81ee00088 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 13 May 2024 13:16:04 -0600 Subject: [PATCH 1385/3044] Add URL checking to MD and RST files --- .github/workflows/test_branches.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 5063571c65f..7e5e5aff3ad 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -47,7 +47,17 @@ jobs: uses: crate-ci/typos@master with: config: ./.github/workflows/typos.toml - + - name: URL Checker + uses: urlstechie/urlchecker-action@0.0.34 + with: + # A comma-separated list of file types to cover in the URL checks + file_types: .md,.rst + # Choose whether to include file with no URLs in the prints. + print_all: false + # More verbose summary at the end of a run + verbose: true + # How many times to retry a failed request (defaults to 1) + retry_count: 3 build: name: ${{ matrix.TARGET }}/${{ matrix.python }}${{ matrix.other }} From ecbe3193c47ed2a872b59b1ea75bce4308c03e58 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 13 May 2024 13:24:16 -0600 Subject: [PATCH 1386/3044] Did this section break everything? --- .github/workflows/test_branches.yml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 7e5e5aff3ad..5063571c65f 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -47,17 +47,7 @@ jobs: uses: crate-ci/typos@master with: config: ./.github/workflows/typos.toml - - name: URL Checker - uses: urlstechie/urlchecker-action@0.0.34 - with: - # A comma-separated list of file types to cover in the URL checks - file_types: .md,.rst - # Choose whether to include file with no URLs in the prints. - print_all: false - # More verbose summary at the end of a run - verbose: true - # How many times to retry a failed request (defaults to 1) - retry_count: 3 + build: name: ${{ matrix.TARGET }}/${{ matrix.python }}${{ matrix.other }} From c4c3de08b8db83a211b5395f54faf842243d4ac3 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 13 May 2024 14:26:11 -0600 Subject: [PATCH 1387/3044] starting to add pseudo-constant handlers but they don't work yet --- pyomo/repn/parameterized_linear.py | 95 ++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 5 deletions(-) diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index 2163b544184..29e6583d19d 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -19,9 +19,12 @@ from pyomo.core.expr.logical_expr import _flattened from pyomo.core.expr.numeric_expr import ( AbsExpression, + DivisionExpression, LinearExpression, MonomialTermExpression, + NegationExpression, mutable_expression, + PowExpression, ProductExpression, SumExpression, UnaryFunctionExpression, @@ -41,7 +44,10 @@ class ParameterizedExprType(enum.IntEnum, metaclass=ExtendedEnumType): __base_enum__ = ExprType PSUEDO_CONSTANT = 50 +_PSEUDO_CONSTANT = ParameterizedExprType.PSUEDO_CONSTANT _CONSTANT = ParameterizedExprType.CONSTANT +_LINEAR = ParameterizedExprType.LINEAR +_GENERAL = ParameterizedExprType.GENERAL def _merge_dict(dest_dict, mult, src_dict): @@ -177,7 +183,7 @@ def _before_var(visitor, child): if child in visitor.wrt: # psueudo-constant # We aren't treating this Var as a Var for the purposes of this walker - return False, (_CONSTANT, child) + return False, (_PSEUDO_CONSTANT, child) # This is a normal situation # TODO: override record var to not record things in wrt MultiLevelLinearBeforeChildDispatcher._record_var(visitor, child) @@ -189,14 +195,93 @@ def _before_var(visitor, child): _before_child_dispatcher = MultiLevelLinearBeforeChildDispatcher() _exit_node_handlers = copy.deepcopy(linear._exit_node_handlers) +# +# NEGATION handler +# + +def _handle_negation_pseudo_constant(visitor, node, arg): + return (_PSEUDO_CONSTANT, -1 * arg[1]) -def _handle_product_constant_constant(visitor, node, arg1, arg2): - # ESJ: Can I do this? Just let the potential nans go through? - return _CONSTANT, arg1[1] * arg2[1] + +_exit_node_handlers[NegationExpression].update( + {(_PSEUDO_CONSTANT,): _handle_negation_pseudo_constant,} +) + + +# +# PRODUCT handler +# + + +def _handle_product_pseudo_constant_pseudo_constant(visitor, node, arg1, arg2): + return _PSEUDO_CONSTANT, arg1[1] * arg2[1] + + +def _handle_product_pseudo_constant_constant(visitor, node, arg1, arg2): + return _PSEUDO_CONSTANT, arg1[1] * arg2[1] _exit_node_handlers[ProductExpression].update( - {(_CONSTANT, _CONSTANT): _handle_product_constant_constant} + { + (_PSEUDO_CONSTANT, _PSEUDO_CONSTANT): _handle_product_pseudo_constant_pseudo_constant, + (_PSEUDO_CONSTANT, _CONSTANT): _handle_product_pseudo_constant_constant, + (_CONSTANT, _PSEUDO_CONSTANT): _handle_product_pseudo_constant_constant, + (_PSEUDO_CONSTANT, _LINEAR): linear._handle_product_constant_ANY, + (_LINEAR, _PSEUDO_CONSTANT): linear._handle_product_ANY_constant, + (_PSEUDO_CONSTANT, _GENERAL): linear._handle_product_constant_ANY, + (_GENERAL, _PSEUDO_CONSTANT): linear._handle_product_ANY_constant, + } +) +_exit_node_handlers[MonomialTermExpression].update(_exit_node_handlers[ProductExpression]) + +# +# DIVISION handlers +# + +def _handle_division_pseudo_constant_pseudo_constant(visitor, node, arg1, arg2): + return _PSEUDO_CONSTANT, arg1[1] / arg2[1] + + +def _handle_division_pseudo_constant_constant(visitor, node, arg1, arg2): + return _PSEUDO_CONSTANT, arg[1] / arg2[1] + + +def _handle_division_ANY_pseudo_constant(visitor, node, arg1, arg2): + arg1[1].multiplier = arg1[1].multiplier / arg2[1] + return arg1 + + +_exit_node_handlers[DivisionExpression].update( + { + (_PSEUDO_CONSTANT, _PSEUDO_CONSTANT): _handle_division_pseudo_constant_pseudo_constant, + (_PSEUDO_CONSTANT, _CONSTANT): _handle_division_pseudo_constant_constant, + (_CONSTANT, _PSEUDO_CONSTANT): _handle_division_pseudo_constant_constant, + (_LINEAR, _PSEUDO_CONSTANT): _handle_division_ANY_pseudo_constant, + (_GENERAL, _PSEUDO_CONSTANT): _handle_division_ANY_pseudo_constant, + } +) + +# +# EXPONENTIATION handlers +# + +def _handle_pow_pseudo_constant_pseudo_constant(visitor, node, arg1, arg2): + return _PSEUDO_CONSTANT, node.create_node_with_local_data( + linear.to_expression(visitor, arg1), linear.to_expression(visitor, arg2)) + + +def _handle_pow_ANY_pseudo_constant(visitor, node, arg1, arg2): + # TODO + pass + + +_exit_node_handlers[PowExpression].update( + { + (_PSEUDO_CONSTANT, _PSEUDO_CONSTANT): _handle_pow_pseudo_constant_pseudo_constant, + (_PSEUDO_CONSTANT, _CONSTANT): _handle_pow_pseudo_constant_pseudo_constant, + (_CONSTANT, _PSEUDO_CONSTANT): _handle_pow_pseudo_constant_pseudo_constant, + (_LINEAR, _PSEUDO_CONSTANT): _handle_pow_ANY_pseudo_constant, + } ) From 9a68aab60ad71993b6a5ab89db07e869e4a95cdd Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 13 May 2024 15:25:25 -0600 Subject: [PATCH 1388/3044] Fix broken links; ignore pyomo-jenkins --- .github/workflows/test_branches.yml | 14 +++++++++++++- .../pynumero/pynumero.sparse.block_vector.rst | 2 +- doc/OnlineDocs/contribution_guide.rst | 2 +- doc/OnlineDocs/modeling_extensions/dae.rst | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 5063571c65f..20b5b869304 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -47,7 +47,19 @@ jobs: uses: crate-ci/typos@master with: config: ./.github/workflows/typos.toml - + - name: URL Checker + uses: urlstechie/urlchecker-action@0.0.34 + with: + # A comma-separated list of file types to cover in the URL checks + file_types: .md,.rst + # Choose whether to include file with no URLs in the prints. + print_all: false + # More verbose summary at the end of a run + verbose: true + # How many times to retry a failed request (defaults to 1) + retry_count: 3 + # Exclude Jenkins because it's behind a firewall + exclude_urls: https://pyomo-jenkins.sandia.gov build: name: ${{ matrix.TARGET }}/${{ matrix.python }}${{ matrix.other }} diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.sparse.block_vector.rst b/doc/OnlineDocs/contributed_packages/pynumero/pynumero.sparse.block_vector.rst index 6e1dc1f20e5..c17d3d1df86 100644 --- a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.sparse.block_vector.rst +++ b/doc/OnlineDocs/contributed_packages/pynumero/pynumero.sparse.block_vector.rst @@ -77,7 +77,7 @@ NumPy compatible functions: * `numpy.arccos() `_ * `numpy.sinh() `_ * `numpy.cosh() `_ - * `numpy.abs() `_ + * `numpy.abs() `_ * `numpy.tanh() `_ * `numpy.arccosh() `_ * `numpy.arcsinh() `_ diff --git a/doc/OnlineDocs/contribution_guide.rst b/doc/OnlineDocs/contribution_guide.rst index b98dcc3d014..832bb70a78a 100644 --- a/doc/OnlineDocs/contribution_guide.rst +++ b/doc/OnlineDocs/contribution_guide.rst @@ -412,7 +412,7 @@ Including External Packages +++++++++++++++++++++++++++ The `pyomocontrib_simplemodel -`_ package +`_ package is derived from Pyomo, and it defines the class SimpleModel that illustrates how Pyomo can be used in a simple, less object-oriented manner. Specifically, this class mimics the modeling style supported diff --git a/doc/OnlineDocs/modeling_extensions/dae.rst b/doc/OnlineDocs/modeling_extensions/dae.rst index 703e83f4f14..ff0fb75e610 100644 --- a/doc/OnlineDocs/modeling_extensions/dae.rst +++ b/doc/OnlineDocs/modeling_extensions/dae.rst @@ -738,7 +738,7 @@ supported by CasADi. A list of available integrators for each package is given below. Please refer to the `SciPy `_ and `CasADi -`_ documentation directly for the most up-to-date information about +`_ documentation directly for the most up-to-date information about these packages and for more information about the various integrators and options. From 17646cc7bcca03ab4508afd9d698ad9efb6dcd70 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 13 May 2024 15:30:59 -0600 Subject: [PATCH 1389/3044] We don't actually support 'external' contrib packages --- .github/workflows/test_branches.yml | 2 +- doc/OnlineDocs/contribution_guide.rst | 44 ++------------------------- 2 files changed, 3 insertions(+), 43 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 20b5b869304..9fd15ebcb19 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -59,7 +59,7 @@ jobs: # How many times to retry a failed request (defaults to 1) retry_count: 3 # Exclude Jenkins because it's behind a firewall - exclude_urls: https://pyomo-jenkins.sandia.gov + exclude_urls: https://pyomo-jenkins.sandia.gov/ build: name: ${{ matrix.TARGET }}/${{ matrix.python }}${{ matrix.other }} diff --git a/doc/OnlineDocs/contribution_guide.rst b/doc/OnlineDocs/contribution_guide.rst index 832bb70a78a..9ad5bdfee0e 100644 --- a/doc/OnlineDocs/contribution_guide.rst +++ b/doc/OnlineDocs/contribution_guide.rst @@ -404,50 +404,10 @@ Contrib packages will be tested along with Pyomo. If test failures arise, then these packages will be disabled and an issue will be created to resolve these test failures. -The following two examples illustrate the two ways -that ``pyomo.contrib`` can be used to integrate third-party -contributions. - -Including External Packages -+++++++++++++++++++++++++++ - -The `pyomocontrib_simplemodel -`_ package -is derived from Pyomo, and it defines the class SimpleModel that -illustrates how Pyomo can be used in a simple, less object-oriented -manner. Specifically, this class mimics the modeling style supported -by `PuLP `_. - -While ``pyomocontrib_simplemodel`` can be installed and used separate -from Pyomo, this package is included in ``pyomo/contrib/simplemodel``. -This allows this package to be referenced as if were defined as a -subpackage of ``pyomo.contrib``. For example:: - - from pyomo.contrib.simplemodel import * - from math import pi - - m = SimpleModel() - - r = m.var('r', bounds=(0,None)) - h = m.var('h', bounds=(0,None)) - - m += 2*pi*r*(r + h) - m += pi*h*r**2 == 355 - - status = m.solve("ipopt") - -This example illustrates that a package can be distributed separate -from Pyomo while appearing to be included in the ``pyomo.contrib`` -subpackage. Pyomo requires a separate directory be defined under -``pyomo/contrib`` for each such package, and the Pyomo developer -team will approve the inclusion of third-party packages in this -manner. - - Contrib Packages within Pyomo +++++++++++++++++++++++++++++ -Third-party contributions can also be included directly within the +Third-party contributions can be included directly within the ``pyomo.contrib`` package. The ``pyomo/contrib/example`` package provides an example of how this can be done, including a directory for plugins and package tests. For example, this package can be @@ -465,7 +425,7 @@ import this package, but if an import failure occurs, Pyomo will silently ignore it. Otherwise, this pyomo package will be treated like any other. Specifically: -* Plugin classes defined in this package are loaded when `pyomo.environ` is loaded. +* Plugin classes defined in this package are loaded when ``pyomo.environ`` is loaded. * Tests in this package are run with other Pyomo tests. From 495c012943a17a272737454400a5ced9260e866a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 13 May 2024 15:45:34 -0600 Subject: [PATCH 1390/3044] Add to PR file --- .github/workflows/test_pr_and_main.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index a45fdd54f03..d0fe40c05bf 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -57,6 +57,19 @@ jobs: uses: crate-ci/typos@master with: config: ./.github/workflows/typos.toml + - name: URL Checker + uses: urlstechie/urlchecker-action@0.0.34 + with: + # A comma-separated list of file types to cover in the URL checks + file_types: .md,.rst + # Choose whether to include file with no URLs in the prints. + print_all: false + # More verbose summary at the end of a run + verbose: true + # How many times to retry a failed request (defaults to 1) + retry_count: 3 + # Exclude Jenkins because it's behind a firewall + exclude_urls: https://pyomo-jenkins.sandia.gov/ build: From 293807c1a0953c8d5ea15069fc684ba11cfd70e5 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 13 May 2024 15:46:16 -0600 Subject: [PATCH 1391/3044] Apparently removed one line in branches file --- .github/workflows/test_branches.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 9fd15ebcb19..11a3fde709e 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -61,6 +61,7 @@ jobs: # Exclude Jenkins because it's behind a firewall exclude_urls: https://pyomo-jenkins.sandia.gov/ + build: name: ${{ matrix.TARGET }}/${{ matrix.python }}${{ matrix.other }} runs-on: ${{ matrix.os }} From d6e66f398c721572af0a34f556dfc9de5a9c7134 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 14 May 2024 12:46:57 -0600 Subject: [PATCH 1392/3044] Turn on .py files --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 11a3fde709e..4a5894bb6d3 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -51,7 +51,7 @@ jobs: uses: urlstechie/urlchecker-action@0.0.34 with: # A comma-separated list of file types to cover in the URL checks - file_types: .md,.rst + file_types: .md,.rst,.py # Choose whether to include file with no URLs in the prints. print_all: false # More verbose summary at the end of a run From 94ef83df768d512d350b11704e9136d60fda692c Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 14 May 2024 13:19:54 -0600 Subject: [PATCH 1393/3044] Fix broken URLs in py files --- examples/dae/ReactionKinetics.py | 5 ++++- pyomo/common/gsl.py | 4 ++-- pyomo/dataportal/plugins/db_table.py | 6 ++++-- pyomo/scripting/plugins/download.py | 6 +++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/examples/dae/ReactionKinetics.py b/examples/dae/ReactionKinetics.py index fa747cf8b21..ce097f7c748 100644 --- a/examples/dae/ReactionKinetics.py +++ b/examples/dae/ReactionKinetics.py @@ -304,7 +304,10 @@ def regression_model(): # Model & data from: # - # http://www.doiserbia.nb.rs/img/doi/0367-598X/2014/0367-598X1300037A.pdf + # https://doiserbia.nb.rs/img/doi/0367-598X/2014/0367-598X1300037A.pdf + # Almagrbi, A. M., Hatami, T., Glišić, S., & Orlović, A. (2014). + # Determination of kinetic parameters for complex transesterification + # reaction by standard optimisation methods. Hemijska industrija, 68(2), 149-159. # model = ConcreteModel() diff --git a/pyomo/common/gsl.py b/pyomo/common/gsl.py index 1c14b64bd70..96fab8623b3 100644 --- a/pyomo/common/gsl.py +++ b/pyomo/common/gsl.py @@ -23,8 +23,8 @@ ) def get_gsl(downloader): logger.info( - "As of February 9, 2023, AMPL GSL can no longer be downloaded\ - through download-extensions. Visit https://portal.ampl.com/\ + "As of February 9, 2023, AMPL GSL can no longer be downloaded \ + through download-extensions. Visit https://portal.ampl.com/ \ to download the AMPL GSL binaries." ) diff --git a/pyomo/dataportal/plugins/db_table.py b/pyomo/dataportal/plugins/db_table.py index a39705a6058..7c570757bf4 100644 --- a/pyomo/dataportal/plugins/db_table.py +++ b/pyomo/dataportal/plugins/db_table.py @@ -385,8 +385,10 @@ def __init__(self, filename=None, data=None): will override that in the file. """ - # ugh hardcoded strings. See following URL for info: - # http://publib.boulder.ibm.com/infocenter/idshelp/v10/index.jsp?topic=/com.ibm.odbc.doc/odbc58.htm + # These hardcoded strings were originally explained via a link + # to documentation that has since been moved and deleted. + # We have lost the historical knowledge as to why these strings + # are hardcoded as such. self.ODBC_DS_KEY = 'ODBC Data Sources' self.ODBC_INFO_KEY = 'ODBC' diff --git a/pyomo/scripting/plugins/download.py b/pyomo/scripting/plugins/download.py index eea858a737f..afe56988009 100644 --- a/pyomo/scripting/plugins/download.py +++ b/pyomo/scripting/plugins/download.py @@ -38,9 +38,9 @@ def _call_impl(self, args, unparsed, logger): self.downloader.cacert = args.cacert self.downloader.insecure = args.insecure logger.info( - "As of February 9, 2023, AMPL GSL can no longer be downloaded\ - through download-extensions. Visit https://portal.ampl.com/\ - to download the AMPL GSL binaries." + "As of February 9, 2023, AMPL GSL can no longer be downloaded \ + through download-extensions. Visit https://portal.ampl.com/ \ + to download the AMPL GSL binaries." ) for target in DownloadFactory: try: From b41f78290cd3b54de6099e5f3a39d871066e84b2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 14 May 2024 13:22:27 -0600 Subject: [PATCH 1394/3044] Remove non-English journal title because it triggers spell check --- examples/dae/ReactionKinetics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/dae/ReactionKinetics.py b/examples/dae/ReactionKinetics.py index ce097f7c748..2e474ae40d3 100644 --- a/examples/dae/ReactionKinetics.py +++ b/examples/dae/ReactionKinetics.py @@ -307,7 +307,7 @@ def regression_model(): # https://doiserbia.nb.rs/img/doi/0367-598X/2014/0367-598X1300037A.pdf # Almagrbi, A. M., Hatami, T., Glišić, S., & Orlović, A. (2014). # Determination of kinetic parameters for complex transesterification - # reaction by standard optimisation methods. Hemijska industrija, 68(2), 149-159. + # reaction by standard optimisation methods. # model = ConcreteModel() From c30a73a6aea981cc8d9db1f6ee30f6bfe1502f38 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 14 May 2024 13:28:54 -0600 Subject: [PATCH 1395/3044] Missed a file; ignore a magic URL --- .github/workflows/test_branches.yml | 5 +++-- pyomo/solvers/plugins/solvers/CBCplugin.py | 21 ++++----------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 4a5894bb6d3..8ba04eec466 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -58,8 +58,9 @@ jobs: verbose: true # How many times to retry a failed request (defaults to 1) retry_count: 3 - # Exclude Jenkins because it's behind a firewall - exclude_urls: https://pyomo-jenkins.sandia.gov/ + # Exclude Jenkins because it's behind a firewall; ignore RTD because + # a magically-generated string is triggering a failure + exclude_urls: https://pyomo-jenkins.sandia.gov/,https://pyomo.readthedocs.io/en/%s/errors.html build: diff --git a/pyomo/solvers/plugins/solvers/CBCplugin.py b/pyomo/solvers/plugins/solvers/CBCplugin.py index f22fb117c8b..22ebf83f770 100644 --- a/pyomo/solvers/plugins/solvers/CBCplugin.py +++ b/pyomo/solvers/plugins/solvers/CBCplugin.py @@ -455,7 +455,6 @@ def process_logfile(self): tokens = tuple(re.split('[ \t]+', line.strip())) n_tokens = len(tokens) if n_tokens > 1: - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L3769 if n_tokens > 4 and tokens[:4] == ( 'Continuous', 'objective', @@ -539,7 +538,6 @@ def process_logfile(self): results.problem.name = results.problem.name.split('/')[-1] if '\\' in results.problem.name: results.problem.name = results.problem.name.split('\\')[-1] - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L10840 elif tokens[0] == 'Presolve': if n_tokens > 9 and tokens[3] == 'rows,' and tokens[6] == 'columns': results.problem.number_of_variables = int(tokens[4]) - int( @@ -551,7 +549,6 @@ def process_logfile(self): results.problem.number_of_objectives = 1 elif n_tokens > 6 and tokens[6] == 'infeasible': soln.status = SolutionStatus.infeasible - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L11105 elif ( n_tokens > 11 and tokens[:2] == ('Problem', 'has') @@ -563,7 +560,6 @@ def process_logfile(self): results.problem.number_of_constraints = int(tokens[2]) results.problem.number_of_nonzeros = int(tokens[6][1:]) results.problem.number_of_objectives = 1 - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L10814 elif ( n_tokens > 8 and tokens[:3] == ('Original', 'problem', 'has') @@ -579,7 +575,6 @@ def process_logfile(self): in ' '.join(tokens) ): results.problem.sense = maximize - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L3047 elif n_tokens > 3 and tokens[:2] == ('Result', '-'): if tokens[2:4] in [('Run', 'abandoned'), ('User', 'ctrl-c')]: results.solver.termination_condition = ( @@ -609,15 +604,12 @@ def process_logfile(self): 'solution': TerminationCondition.other, 'iterations': TerminationCondition.maxIterations, }.get(tokens[4], TerminationCondition.other) - # perhaps from https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L12318 elif n_tokens > 3 and tokens[2] == "Finished": soln.status = SolutionStatus.optimal optim_value = _float(tokens[4]) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7904 elif n_tokens >= 3 and tokens[:2] == ('Objective', 'value:'): # parser for log file generetated with discrete variable optim_value = _float(tokens[2]) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7904 elif n_tokens >= 4 and tokens[:4] == ( 'No', 'feasible', @@ -630,25 +622,19 @@ def process_logfile(self): lower_bound is None ): # Only use if not already found since this is to less decimal places results.problem.lower_bound = _float(tokens[2]) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7918 elif tokens[0] == 'Gap:': # This is relative and only to 2 decimal places - could calculate explicitly using lower bound gap = _float(tokens[1]) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7923 elif n_tokens > 2 and tokens[:2] == ('Enumerated', 'nodes:'): nodes = int(tokens[2]) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7926 elif n_tokens > 2 and tokens[:2] == ('Total', 'iterations:'): results.solver.statistics.black_box.number_of_iterations = int( tokens[2] ) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7930 elif n_tokens > 3 and tokens[:3] == ('Time', '(CPU', 'seconds):'): results.solver.system_time = _float(tokens[3]) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7933 elif n_tokens > 3 and tokens[:3] == ('Time', '(Wallclock', 'Seconds):'): results.solver.wallclock_time = _float(tokens[3]) - # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L10477 elif n_tokens > 4 and tokens[:4] == ( 'Total', 'time', @@ -833,9 +819,10 @@ def process_soln_file(self, results): n_tokens = len(tokens) # # These are the only header entries CBC will generate (identified via browsing CbcSolver.cpp) - # See https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp - # Search for (no integer solution - continuous used) Currently line 9912 as of rev2497 - # Note that since this possibly also covers old CBC versions, we shall not be removing any functionality, + # See https://github.com/coin-or/Cbc/tree/master/src + # Search for (no integer solution - continuous used) + # Note that since this possibly also covers old CBC versions, + # we shall not be removing any functionality, # even if it is not seen in the current revision # if not header_processed: From 155c5b209a184b9bf061878a3c4440501071a4bb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 14 May 2024 13:48:05 -0600 Subject: [PATCH 1396/3044] Add to PR test workflow --- .github/workflows/test_pr_and_main.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index d0fe40c05bf..8161e6186e4 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -61,15 +61,16 @@ jobs: uses: urlstechie/urlchecker-action@0.0.34 with: # A comma-separated list of file types to cover in the URL checks - file_types: .md,.rst + file_types: .md,.rst,.py # Choose whether to include file with no URLs in the prints. print_all: false # More verbose summary at the end of a run verbose: true # How many times to retry a failed request (defaults to 1) retry_count: 3 - # Exclude Jenkins because it's behind a firewall - exclude_urls: https://pyomo-jenkins.sandia.gov/ + # Exclude Jenkins because it's behind a firewall; ignore RTD because + # a magically-generated string is triggering a failure + exclude_urls: https://pyomo-jenkins.sandia.gov/,https://pyomo.readthedocs.io/en/%s/errors.html build: From 12920b41f52e644c3762000f0b1535d6009c24e6 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 15 May 2024 16:40:36 -0400 Subject: [PATCH 1397/3044] j1 triangulate in 2d, but no ordering --- pyomo/contrib/piecewise/triangulations.py | 66 +++++++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index d67cd302060..d0a8cb1d34d 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -10,6 +10,9 @@ # ___________________________________________________________________________ from pytest import set_trace +import math +import itertools +from functools import cmp_to_key class Triangulation: @@ -17,16 +20,71 @@ class Triangulation: J1 = 2 def get_j1_triangulation(points, dimension): + points_map, K = _process_points_j1(points, dimension) if dimension == 2: - return _get_j1_triangulation_2d(points, dimension) + return _get_j1_triangulation_2d(points_map, K) elif dimension == 3: return _get_j1_triangulation_3d(points, dimension) else: return _get_j1_triangulation_for_more_than_4d(points, dimension) -def _get_j1_triangulation_2d(points, dimension): - # I think this means coding up the proof by picture... - pass + +# Does some validation but mostly assumes the user did the right thing +def _process_points_j1(points, dimension): + if not len(points[0]) == dimension: + raise ValueError("Points not consistent with specified dimension") + K = math.floor(len(points) ** (1 / dimension)) + if not len(points) == K**dimension: + raise ValueError("'points' must have points forming an n-dimensional grid with straight grid lines and the same odd number of points in each axis") + if not K % 2 == 1: + raise ValueError("'points' must have points forming an n-dimensional grid with straight grid lines and the same odd number of points in each axis") + + # munge the points into an organized map with n-dimensional keys + points.sort(key=cmp_to_key(_compare_lexicographic(dimension))) + points_map = {} + for point_index in itertools.product(range(K), repeat=dimension): + point_flat_index = 0 + for n in range(dimension): + point_flat_index += point_index[dimension - 1 - n] * K**n + points_map[point_index] = points[point_flat_index] + return points_map, K + +def _compare_lexicographic(dimension): + def compare_lexicographic_real(x, y): + for n in range(dimension): + if x[n] < y[n]: + return -1 + elif y[n] < x[n]: + return 1 + return 0 + return compare_lexicographic_real + +def _get_j1_triangulation_2d(points_map, K): + # Each square needs two triangles in it, orientation determined by the parity of + # the bottom-left corner's coordinate indices (x and y). Same parity = top-left + # and bottom-right triangles; different parity = top-right and bottom-left triangles. + simplices = [] + for i in range(K): + for j in range(K): + if i % 2 == j % 2: + simplices.append( + (points_map[i, j], + points_map[i + 1, j + 1], + points_map[i, j + 1])) + simplices.append( + (points_map[i, j], + points_map[i + 1, j + 1], + points_map[i + 1, j])) + else: + simplices.append( + (points_map[i + 1, j], + points_map[i, j + 1], + points_map[i, j])) + simplices.append( + (points_map[i + 1, j], + points_map[i, j + 1], + points_map[i + 1, j + 1])) + return simplices def _get_j1_triangulation_3d(points, dimension): pass From 909fd1e73cf04aa890616816cba789b8c0bb7d01 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 16 May 2024 00:49:38 -0600 Subject: [PATCH 1398/3044] Support deferred final resolution of external function args --- pyomo/repn/plugins/nl_writer.py | 63 +++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index d4e7485cce4..763ed3cce0e 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -16,7 +16,7 @@ import os from collections import deque, defaultdict, namedtuple from contextlib import nullcontext -from itertools import filterfalse, product +from itertools import filterfalse, product, chain from math import log10 as _log10 from operator import itemgetter, attrgetter, setitem @@ -549,6 +549,7 @@ def __init__(self, ostream, rowstream, colstream, config): self.external_functions = {} self.used_named_expressions = set() self.var_map = {} + self.var_id_to_nl = None self.sorter = FileDeterminism_to_SortComponents(config.file_determinism) self.visitor = AMPLRepnVisitor( self.template, @@ -1646,6 +1647,9 @@ def _categorize_vars(self, comp_list, linear_by_comp): Count of the number of components that each var appears in. """ + subexpression_cache = self.subexpression_cache + used_named_expressions = self.used_named_expressions + var_map = self.var_map all_linear_vars = set() all_nonlinear_vars = set() nnz_by_var = {} @@ -1673,9 +1677,14 @@ def _categorize_vars(self, comp_list, linear_by_comp): # Process the nonlinear portion of this component if expr_info.nonlinear: nonlinear_vars = set() - for _id in expr_info.nonlinear[1]: + _id_src = [expr_info.nonlinear[1]] + for _id in chain.from_iterable(_id_src): if _id in nonlinear_vars: continue + if _id not in var_map and _id not in used_named_expressions: + _sub_info = subexpression_cache[_id][1] + _id_src.append(_sub_info.nonlinear[1]) + continue if _id in linear_by_comp: nonlinear_vars.update(linear_by_comp[_id]) else: @@ -1945,7 +1954,22 @@ def _write_nl_expression(self, repn, include_const): # Add the constant to the NL expression. AMPL adds the # constant as the second argument, so we will too. nl = self.template.binary_sum + nl + self.template.const % repn.const - self.ostream.write(nl % tuple(map(self.var_id_to_nl.__getitem__, args))) + try: + self.ostream.write(nl % tuple(map(self.var_id_to_nl.__getitem__, args))) + except KeyError: + final_args = [] + for arg in args: + if arg in self.var_id_to_nl: + final_args.append(self.var_id_to_nl[arg]) + else: + _nl, _ids, _ = self.subexpression_cache[arg][1].compile_repn( + self.visitor + ) + final_args.append( + _nl % tuple(map(self.var_id_to_nl.__getitem__, _ids)) + ) + self.ostream.write(nl % tuple(final_args)) + elif include_const: self.ostream.write(self.template.const % repn.const) else: @@ -2708,14 +2732,33 @@ def handle_external_function_node(visitor, node, *args): else: visitor.external_functions[func] = (len(visitor.external_functions), node._fcn) comment = f'\t#{node.local_name}' if visitor.symbolic_solver_labels else '' - nonlin = node_result_to_amplrepn(args[0]).compile_repn( - visitor, - visitor.template.external_fcn - % (visitor.external_functions[func][0], len(args), comment), + nl = visitor.template.external_fcn % ( + visitor.external_functions[func][0], + len(args), + comment, + ) + arg_ids = [] + for arg in args: + _id = id(arg) + arg_ids.append(_id) + named_exprs = set() + visitor.subexpression_cache[_id] = ( + arg, + AMPLRepn( + 0, + None, + node_result_to_amplrepn(arg).compile_repn( + visitor, named_exprs=named_exprs + ), + ), + (None, None, True), + ) + if not named_exprs: + named_exprs = None + return ( + _GENERAL, + AMPLRepn(0, None, (nl + '%s' * len(arg_ids), arg_ids, named_exprs)), ) - for arg in args[1:]: - nonlin = node_result_to_amplrepn(arg).compile_repn(visitor, *nonlin) - return (_GENERAL, AMPLRepn(0, None, nonlin)) _operator_handles = ExitNodeDispatcher( From 2a2fe4e3cc9c2625bcab35bfdd8a89483007a7bd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 16 May 2024 11:19:55 -0600 Subject: [PATCH 1399/3044] Attempt to propagate initial values for presolved variables --- pyomo/repn/plugins/nl_writer.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index bed073ea1d7..fc6db2c33fb 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -774,9 +774,10 @@ def write(self, model): # expressions, or when users provide superfluous variables in # the column ordering. var_bounds = {_id: v.bounds for _id, v in var_map.items()} + var_values = {_id: v.value for _id, v in var_map.items()} eliminated_cons, eliminated_vars = self._linear_presolve( - comp_by_linear_var, lcon_by_linear_nnz, var_bounds + comp_by_linear_var, lcon_by_linear_nnz, var_bounds, var_values ) del comp_by_linear_var del lcon_by_linear_nnz @@ -1472,7 +1473,7 @@ def write(self, model): # _init_lines = [ (var_idx, val if val.__class__ in int_float else float(val)) - for var_idx, val in enumerate(var_map[_id].value for _id in variables) + for var_idx, val in enumerate(map(var_values.__getitem__, variables)) if val is not None ] if scale_model: @@ -1746,7 +1747,9 @@ def _count_subexpression_occurrences(self): n_subexpressions[0] += 1 return n_subexpressions - def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): + def _linear_presolve( + self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds, var_values + ): eliminated_vars = {} eliminated_cons = set() if not self.config.linear_presolve: @@ -1819,7 +1822,7 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): ): _id, id2 = id2, _id coef, coef2 = coef2, coef - # substituting _id with a*x + b + # eliminating _id and replacing it with a*x + b a = -coef2 / coef x = id2 b = expr_info.const = (lb - expr_info.const) / coef @@ -1849,6 +1852,25 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): var_bounds[x] = x_lb, x_ub if x_lb == x_ub and x_lb is not None: fixed_vars.append(x) + # Given that we are eliminating a variable, we want to + # attempt to sanely resolve the initial variable values. + y_init = var_values[_id] + if y_init is not None: + # Y has a value + x_init = var_values[x] + if x_init is None: + # X does not; just use the one calculated from Y + x_init = (y_init - b) / a + else: + # X does too, use the average of the two values + x_init = (x_init + (y_init - b) / a) / 2.0 + # Ensure that the initial value respects the + # tightened bounds + if x_ub is not None and x_init > x_ub: + x_init = x_ub + if x_lb is not None and x_init < x_lb: + x_init = x_lb + var_values[x] = x_init eliminated_cons.add(con_id) else: break From cf8ac853e0d4cff39863f5f1548aa7427c1a7883 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 16 May 2024 11:30:15 -0600 Subject: [PATCH 1400/3044] Reorder definitions to avoid NameError in some situations --- pyomo/core/expr/sympy_tools.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/pyomo/core/expr/sympy_tools.py b/pyomo/core/expr/sympy_tools.py index 6c184f0e4c4..b9381148544 100644 --- a/pyomo/core/expr/sympy_tools.py +++ b/pyomo/core/expr/sympy_tools.py @@ -10,12 +10,13 @@ # ___________________________________________________________________________ import operator import sys +from math import prod as _prod +import pyomo.core.expr as EXPR from pyomo.common import DeveloperError from pyomo.common.collections import ComponentMap from pyomo.common.dependencies import attempt_import from pyomo.common.errors import NondifferentiableError -import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import value, native_types # @@ -113,18 +114,6 @@ def _configure_sympy(sympy, available): sympy, sympy_available = attempt_import('sympy', callback=_configure_sympy) -if sys.version_info[:2] < (3, 8): - - def _prod(args): - ans = 1 - for arg in args: - ans *= arg - return ans - -else: - from math import prod as _prod - - def _nondifferentiable(x): if type(x[1]) is tuple: # sympy >= 1.3 returns tuples (var, order) From fef9947f428125fa8d02887d8f596a87bd61c87c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 16 May 2024 11:33:18 -0600 Subject: [PATCH 1401/3044] Remove unused import --- pyomo/core/expr/sympy_tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/core/expr/sympy_tools.py b/pyomo/core/expr/sympy_tools.py index b9381148544..b1fd9f8245c 100644 --- a/pyomo/core/expr/sympy_tools.py +++ b/pyomo/core/expr/sympy_tools.py @@ -9,7 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ import operator -import sys from math import prod as _prod import pyomo.core.expr as EXPR From 02034790d49a49019bb5e04dedb60de1b8693b7f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 16 May 2024 12:36:55 -0600 Subject: [PATCH 1402/3044] Add additional matplotlib import to fix error in community_detection plotting --- pyomo/common/dependencies.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 4c9e43002ef..d30885e4860 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -965,6 +965,7 @@ def _finalize_matplotlib(module, available): import matplotlib.pyplot import matplotlib.pylab import matplotlib.backends + import matplotlib.cm # explicit import required for matplotlib>=3.9.0 def _finalize_numpy(np, available): From 8ae8348a3df281b032101255536f19fab8c049e3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 16 May 2024 13:30:39 -0600 Subject: [PATCH 1403/3044] Change source of get_cmap; set minimum matplotlib version --- pyomo/common/dependencies.py | 1 - pyomo/contrib/community_detection/detection.py | 2 +- setup.py | 4 +++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index d30885e4860..4c9e43002ef 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -965,7 +965,6 @@ def _finalize_matplotlib(module, available): import matplotlib.pyplot import matplotlib.pylab import matplotlib.backends - import matplotlib.cm # explicit import required for matplotlib>=3.9.0 def _finalize_numpy(np, available): diff --git a/pyomo/contrib/community_detection/detection.py b/pyomo/contrib/community_detection/detection.py index 0e2c3912e06..db3bb8f5a20 100644 --- a/pyomo/contrib/community_detection/detection.py +++ b/pyomo/contrib/community_detection/detection.py @@ -580,7 +580,7 @@ def visualize_model_graph( pos = nx.spring_layout(model_graph) # Define color_map - color_map = plt.cm.get_cmap('viridis', len(numbered_community_map)) + color_map = plt.get_cmap('viridis', len(numbered_community_map)) # Create the figure and draw the graph fig = plt.figure() diff --git a/setup.py b/setup.py index a125b02b2fe..6d28e4d184b 100644 --- a/setup.py +++ b/setup.py @@ -264,7 +264,9 @@ def __ne__(self, other): 'ipython', # contrib.viewer # Note: matplotlib 3.6.1 has bug #24127, which breaks # seaborn's histplot (triggering parmest failures) - 'matplotlib!=3.6.1', + # Note: minimum version from community_detection use of + # matplotlib.pyplot.get_cmap() + 'matplotlib>=3.6.0,!=3.6.1', # network, incidence_analysis, community_detection # Note: networkx 3.2 is Python>-3.9, but there is a broken # 3.2 package on conda-forge that will get implicitly From b185716ac7613a93cef9be2edf2fcb552fed0f5a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 16 May 2024 17:13:15 -0400 Subject: [PATCH 1404/3044] add a MIP to order the j1 triangulation --- .../transform/disagreggated_logarithmic.py | 183 ------------------ pyomo/contrib/piecewise/triangulations.py | 124 +++++++++++- 2 files changed, 122 insertions(+), 185 deletions(-) delete mode 100644 pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py deleted file mode 100644 index e86d5539367..00000000000 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ /dev/null @@ -1,183 +0,0 @@ -from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, -) -from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet -from pyomo.core.base import TransformationFactory -from pyomo.gdp import Disjunct, Disjunction -from pyomo.common.errors import DeveloperError -from pyomo.core.expr.visitor import SimpleExpressionVisitor -from pyomo.core.expr.current import identify_components -from math import ceil, log2 - - -@TransformationFactory.register( - "contrib.piecewise.disaggregated_logarithmic", - doc=""" - Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; - GDP is not used. This method of logarithmically formulating the piecewise - linear function imposes no restrictions on the family of polytopes, but we - assume we have simplces in this code. This method is due to Vielma et al., 2010. - """, -) -class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): - """ - Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; - GDP is not used. This method of logarithmically formulating the piecewise - linear function imposes no restrictions on the family of polytopes, but we - assume we have simplces in this code. This method is due to Vielma et al., 2010. - """ - - CONFIG = PiecewiseLinearToGDP.CONFIG() - _transformation_name = "pw_linear_disaggregated_log" - - # Implement to use PiecewiseLinearToGDP. This function returns the Var - # that replaces the transformed piecewise linear expr - def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - - # Get a new Block for our transformationin transformation_block.transformed_functions, - # which is a Block(Any). This is where we will put our new components. - transBlock = transformation_block.transformed_functions[ - len(transformation_block.transformed_functions) - ] - - # Dimensionality of the PWLF - dimension = pw_expr.nargs() - transBlock.dimension_indices = RangeSet(0, dimension - 1) - - # Substitute Var that will hold the value of the PWLE - substitute_var = transBlock.substitute_var = Var() - pw_linear_func.map_transformation_var(pw_expr, substitute_var) - - # Bounds for the substitute_var that we will widen - self.substitute_var_lb = float("inf") - self.substitute_var_ub = -float("inf") - - # Simplices are tuples of indices of points. Give them their own indices, too - simplices = pw_linear_func._simplices - num_simplices = len(simplices) - transBlock.simplex_indices = RangeSet(0, num_simplices - 1) - # Assumption: the simplices are really simplices and all have the same number of points, - # which is dimension + 1 - transBlock.simplex_point_indices = RangeSet(0, dimension) - - # Enumeration of simplices: map from simplex number to simplex object - self.idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} - - # List of tuples of simplex indices with their linear function - simplex_indices_and_lin_funcs = list(zip(transBlock.simplex_indices, pw_linear_func._linear_functions)) - - # We don't seem to get a convenient opportunity later, so let's just widen - # the bounds here. All we need to do is go through the corners of each simplex. - for P, linear_func in simplex_indices_and_lin_funcs: - for v in transBlock.simplex_point_indices: - val = linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) - if val < self.substitute_var_lb: - self.substitute_var_lb = val - if val > self.substitute_var_ub: - self.substitute_var_ub = val - # Now set those bounds - transBlock.substitute_var.setlb(self.substitute_var_lb) - transBlock.substitute_var.setub(self.substitute_var_ub) - - log_dimension = ceil(log2(num_simplices)) - transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) - transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) - - # Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices - # (really just polytopes are required) with binary vectors. Any injective function - # is enough here. - B = {} - for i in transBlock.simplex_indices: - # map index(P) -> corresponding vector in {0, 1}^n - B[i] = self._get_binary_vector(i, log_dimension) - - # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it - transBlock.lambdas = Var(transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1)) - - # Sum of all lambdas is one (6b) - transBlock.convex_combo = Constraint( - expr=sum( - transBlock.lambdas[P, v] - for P in transBlock.simplex_indices - for v in transBlock.simplex_point_indices - ) - == 1 - ) - - # The branching rules, establishing using the binaries that only one simplex's lambdas - # may be nonzero - @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) - def simplex_choice_1(b, l): - return ( - sum( - transBlock.lambdas[P, v] - for P in self._P_plus(B, l, transBlock.simplex_indices) - for v in transBlock.simplex_point_indices - ) - <= transBlock.binaries[l] - ) - - @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) - def simplex_choice_2(b, l): - return ( - sum( - transBlock.lambdas[P, v] - for P in self._P_0(B, l, transBlock.simplex_indices) - for v in transBlock.simplex_point_indices - ) - <= 1 - transBlock.binaries[l] - ) - - # for i, (simplex, pwlf) in enumerate(choices): - # x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P)) - @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) - def x_constraint(b, i): - return pw_expr.args[i] == sum( - transBlock.lambdas[P, v] - * pw_linear_func._points[self.idx_to_simplex[P][v]][i] - for P in transBlock.simplex_indices - for v in transBlock.simplex_point_indices - ) - - # Make the substitute Var equal the PWLE (6a.2) - #for P, linear_func in simplices_and_lin_funcs: - # print(f"P, linear_func = {P}, {linear_func}") - # for v in transBlock.simplex_point_indices: - # print(f" v={v}") - # print(f" pt={pw_linear_func._points[P[v]]}") - # print( - # f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" - # ) - transBlock.set_substitute = Constraint( - expr=substitute_var - == sum( - sum( - transBlock.lambdas[P, v] - * linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) - for v in transBlock.simplex_point_indices - ) - for (P, linear_func) in simplex_indices_and_lin_funcs - ) - ) - - return substitute_var - - # Not a gray code, just a regular binary representation - # TODO this may not be optimal, test the gray codes too - def _get_binary_vector(self, num, length): - if num != 0 and ceil(log2(num)) > length: - raise DeveloperError("Invalid input in _get_binary_vector") - # Hack: use python's string formatting instead of bothering with modular - # arithmetic. May be slow. - return tuple(int(x) for x in format(num, f"0{length}b")) - - # Return {P \in \mathcal{P} | B(P)_l = 0} - def _P_0(self, B, l, simplex_indices): - return [p for p in simplex_indices if B[p][l] == 0] - - # Return {P \in \mathcal{P} | B(P)_l = 1} - def _P_plus(self, B, l, simplex_indices): - return [p for p in simplex_indices if B[p][l] == 1] diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index d0a8cb1d34d..23ff61e6478 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -13,7 +13,18 @@ import math import itertools from functools import cmp_to_key - +from pyomo.environ import ( + ConcreteModel, + RangeSet, + Var, + Binary, + Constraint, + Param, + SolverFactory, + value, + Objective, + TerminationCondition, +) class Triangulation: Delaunay = 1 @@ -40,7 +51,9 @@ def _process_points_j1(points, dimension): raise ValueError("'points' must have points forming an n-dimensional grid with straight grid lines and the same odd number of points in each axis") # munge the points into an organized map with n-dimensional keys - points.sort(key=cmp_to_key(_compare_lexicographic(dimension))) + #points.sort(key=cmp_to_key(_compare_lexicographic(dimension))) + # verify: does this do correct sorting by default? + points.sort() points_map = {} for point_index in itertools.product(range(K), repeat=dimension): point_flat_index = 0 @@ -91,3 +104,110 @@ def _get_j1_triangulation_3d(points, dimension): def _get_j1_triangulation_for_more_than_4d(points, dimension): pass + +def get_incremental_simplex_ordering(simplices, subsolver='gurobi'): + # Set up a MIP (err, MIQCP) that orders our simplices and their vertices for us + # in the following way: + # + # (1) The simplices are ordered T_1, ..., T_N such that T_i has nonempty intersection + # with T_{i+1}. It doesn't have to be a whole face; just a vertex is enough. + # (2) On each simplex T_i, the vertices are ordered T_i^1, ..., T_i^n such + # that T_i^n = T_{i+1}^1 + m = ConcreteModel() + + # Sets and Params + m.SimplicesCount = Param(value=len(simplices)) + m.SIMPLICES = RangeSet(0, m.SimplicesCount - 1) + # For each of the simplices we need to choose an initial and a final vertex. + # The rest we can order arbitrarily after finishing the MIP solve. + m.SimplexVerticesCount = Param(value=len(simplices[0])) + m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) + @m.Param(m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) + def TestVerticesEqual(m, i, n, j, k): + return 1 if simplices[i][n] == simplices[j][k] else 0 + + # Vars + # x_ij means simplex i is placed in slot j + m.x = Var(m.SIMPLICES, m.SIMPLICES, domain=Binary) + m.vertex_is_first = Var(m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) + m.vertex_is_last = Var(m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) + + + # Constraints + # Each simplex should have a slot and each slot should have a simplex + @m.Constraint(m.SIMPLICES) + def schedule_each_simplex(m, i): + return sum(m.x[i, j] for j in m.SIMPLICES) == 1 + @m.Constraint(m.SIMPLICES) + def schedule_each_slot(m, j): + return sum(m.x[i, j] for i in m.SIMPLICES) == 1 + + # Enforce property (1) + @m.Constraint(m.SIMPLICES) + def simplex_order(m, i): + if i == m.SimplicesCount - 1: + return Constraint.Skip # no ordering for the last one + # anything with at least a vertex in common is a neighbor + neighbors = [s for s in m.SIMPLICES if sum(TestVerticesEqual[i, n, s, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) >= 1] + return sum(m.x[i, j] * m.x[k, j+1] for j in m.SIMPLICES for k in neighbors) == 1 + + # Each simplex needs exactly one first and exactly one last vertex + @m.Constraint(m.SIMPLICES) + def one_first_vertex(m, i): + return sum(m.vertex_is_first[i, n] for n in m.VERTEX_INDICES) == 1 + @m.Constraint(m.SIMPLICES) + def one_last_vertex(m, i): + return sum(m.vertex_is_last[i, n] for n in m.VERTEX_INDICES) == 1 + + # Enforce property (2) + @m.Constraint(m.SIMPLICES, m.SIMPLICES) + def vertex_order(m, i, j): + if i == m.SimplicesCount - 1: + return Constraint.Skip # no ordering for the last one + # Enforce only when j is the simplex following i. If not, RHS is zero + return ( + sum(m.vertex_is_last[i, n] * m.vertex_is_first[j, k] * m.TestVerticesEqual[i, n, j, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) + >= sum(m.x[i, p] * m.x[j, p + 1] for p in m.SIMPLICES if p != m.SimplicesCount - 1) + ) + + # Trivial objective (do I need this?) + m.obj = Objective(expr=0) + + # Solve model + results = SolverFactory(subsolver).solve(m) + match(results.solver.termination_condition): + case TerminationCondition.infeasible: + raise ValueError("The triangulation was impossible to suitably order for the incremental transformation. Try a different triangulation, such as J1.") + case TerminationCondition.feasible: + pass + case _: + raise ValueError(f"Failed to generate suitable ordering for incremental transformation due to unexpected termination condition {results.solver.termination_condition}") + + # Retrieve data + simplex_ordering = {} + for i in m.SIMPLICES: + for j in m.SIMPLICES: + if abs(value(m.x[i, j]) - 1) < 1e-5: + simplex_ordering[i] = j + break + vertex_ordering = {} + for i in m.SIMPLICES: + first = None + last = None + for n in m.VERTEX_INDICES: + if abs(value(m.vertex_is_first[i, n]) - 1) < 1e-5: + first = n + vertex_ordering[i, 0] = first + if abs(value(m.vertex_is_last[i, n]) - 1) < 1e-5: + last = n + vertex_ordering[i, m.SimplexVerticesCount - 1] = last + if first is not None and last is not None: + break + # Fill in the middle ones arbitrarily + idx = 1 + for j in range(m.SimplexVerticesCount): + if j != first and j != last: + vertex_ordering[idx] = j + idx += 1 + + return simplex_ordering, vertex_ordering From 8f1d61884edcff657b7f5da64cb72caba0a1880a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 16 May 2024 15:45:25 -0600 Subject: [PATCH 1405/3044] Resolve definition ordering --- pyomo/core/expr/sympy_tools.py | 38 +++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pyomo/core/expr/sympy_tools.py b/pyomo/core/expr/sympy_tools.py index b1fd9f8245c..d751ca35e5f 100644 --- a/pyomo/core/expr/sympy_tools.py +++ b/pyomo/core/expr/sympy_tools.py @@ -28,6 +28,25 @@ _functionMap = {} +def _nondifferentiable(x): + if type(x[1]) is tuple: + # sympy >= 1.3 returns tuples (var, order) + wrt = x[1][0] + else: + # early versions of sympy returned the bare var + wrt = x[1] + raise NondifferentiableError( + "The sub-expression '%s' is not differentiable with respect to %s" % (x[0], wrt) + ) + + +def _external_fcn(*x): + raise TypeError( + "Expressions containing external functions are not convertible to " + f"sympy expressions (found 'f{x}')" + ) + + def _configure_sympy(sympy, available): if not available: return @@ -113,25 +132,6 @@ def _configure_sympy(sympy, available): sympy, sympy_available = attempt_import('sympy', callback=_configure_sympy) -def _nondifferentiable(x): - if type(x[1]) is tuple: - # sympy >= 1.3 returns tuples (var, order) - wrt = x[1][0] - else: - # early versions of sympy returned the bare var - wrt = x[1] - raise NondifferentiableError( - "The sub-expression '%s' is not differentiable with respect to %s" % (x[0], wrt) - ) - - -def _external_fcn(*x): - raise TypeError( - "Expressions containing external functions are not convertible to " - f"sympy expressions (found 'f{x}')" - ) - - class PyomoSympyBimap(object): def __init__(self): self.pyomo2sympy = ComponentMap() From 5399127aa8926d185d880a1d0f282779686d6a77 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Fri, 17 May 2024 08:32:54 -0600 Subject: [PATCH 1406/3044] Update with newer links --- pyomo/dataportal/plugins/db_table.py | 7 +++---- pyomo/solvers/plugins/solvers/CBCplugin.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/pyomo/dataportal/plugins/db_table.py b/pyomo/dataportal/plugins/db_table.py index 7c570757bf4..71fd499f725 100644 --- a/pyomo/dataportal/plugins/db_table.py +++ b/pyomo/dataportal/plugins/db_table.py @@ -385,10 +385,9 @@ def __init__(self, filename=None, data=None): will override that in the file. """ - # These hardcoded strings were originally explained via a link - # to documentation that has since been moved and deleted. - # We have lost the historical knowledge as to why these strings - # are hardcoded as such. + # Hardcoded string required here. + # See documentation: + # https://www.ibm.com/docs/en/informix-servers/12.10?topic=SSGU8G_12.1.0/com.ibm.odbc.doc/ids_odbc_062.html self.ODBC_DS_KEY = 'ODBC Data Sources' self.ODBC_INFO_KEY = 'ODBC' diff --git a/pyomo/solvers/plugins/solvers/CBCplugin.py b/pyomo/solvers/plugins/solvers/CBCplugin.py index 22ebf83f770..4d49c5cc58d 100644 --- a/pyomo/solvers/plugins/solvers/CBCplugin.py +++ b/pyomo/solvers/plugins/solvers/CBCplugin.py @@ -455,6 +455,7 @@ def process_logfile(self): tokens = tuple(re.split('[ \t]+', line.strip())) n_tokens = len(tokens) if n_tokens > 1: + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L3769 if n_tokens > 4 and tokens[:4] == ( 'Continuous', 'objective', @@ -538,6 +539,7 @@ def process_logfile(self): results.problem.name = results.problem.name.split('/')[-1] if '\\' in results.problem.name: results.problem.name = results.problem.name.split('\\')[-1] + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L10840 elif tokens[0] == 'Presolve': if n_tokens > 9 and tokens[3] == 'rows,' and tokens[6] == 'columns': results.problem.number_of_variables = int(tokens[4]) - int( @@ -549,6 +551,7 @@ def process_logfile(self): results.problem.number_of_objectives = 1 elif n_tokens > 6 and tokens[6] == 'infeasible': soln.status = SolutionStatus.infeasible + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L11105 elif ( n_tokens > 11 and tokens[:2] == ('Problem', 'has') @@ -560,6 +563,7 @@ def process_logfile(self): results.problem.number_of_constraints = int(tokens[2]) results.problem.number_of_nonzeros = int(tokens[6][1:]) results.problem.number_of_objectives = 1 + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L10814 elif ( n_tokens > 8 and tokens[:3] == ('Original', 'problem', 'has') @@ -575,6 +579,7 @@ def process_logfile(self): in ' '.join(tokens) ): results.problem.sense = maximize + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L3047 elif n_tokens > 3 and tokens[:2] == ('Result', '-'): if tokens[2:4] in [('Run', 'abandoned'), ('User', 'ctrl-c')]: results.solver.termination_condition = ( @@ -604,12 +609,15 @@ def process_logfile(self): 'solution': TerminationCondition.other, 'iterations': TerminationCondition.maxIterations, }.get(tokens[4], TerminationCondition.other) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L12318 elif n_tokens > 3 and tokens[2] == "Finished": soln.status = SolutionStatus.optimal optim_value = _float(tokens[4]) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L7904 elif n_tokens >= 3 and tokens[:2] == ('Objective', 'value:'): # parser for log file generetated with discrete variable optim_value = _float(tokens[2]) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L7904 elif n_tokens >= 4 and tokens[:4] == ( 'No', 'feasible', @@ -622,19 +630,25 @@ def process_logfile(self): lower_bound is None ): # Only use if not already found since this is to less decimal places results.problem.lower_bound = _float(tokens[2]) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L7918 elif tokens[0] == 'Gap:': # This is relative and only to 2 decimal places - could calculate explicitly using lower bound gap = _float(tokens[1]) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L7923 elif n_tokens > 2 and tokens[:2] == ('Enumerated', 'nodes:'): nodes = int(tokens[2]) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L7926 elif n_tokens > 2 and tokens[:2] == ('Total', 'iterations:'): results.solver.statistics.black_box.number_of_iterations = int( tokens[2] ) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L7930 elif n_tokens > 3 and tokens[:3] == ('Time', '(CPU', 'seconds):'): results.solver.system_time = _float(tokens[3]) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L7933 elif n_tokens > 3 and tokens[:3] == ('Time', '(Wallclock', 'Seconds):'): results.solver.wallclock_time = _float(tokens[3]) + # https://github.com/coin-or/Cbc/blob/cb6bf98/Cbc/src/CbcSolver.cpp#L10477 elif n_tokens > 4 and tokens[:4] == ( 'Total', 'time', From e8bfc8cb06f7e00334d969f874c5a56cf052ee3b Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 17 May 2024 17:12:43 -0400 Subject: [PATCH 1407/3044] Finish implementing a MIP to order triangulations for the incremental transformation. However, gurobi is not magical enough to see the structure here so this basically amounts to asking for a Hamiltonian path in a big graph, and then some. I think I will have to do the 70s stuff after all. Some optimizations would reduce this to "only" asking for a Hamiltonian path on a sparser graph but that is still terrible. --- .../piecewise/tests/test_triangulations.py | 84 ++++++++ pyomo/contrib/piecewise/triangulations.py | 185 +++++++++++------- 2 files changed, 202 insertions(+), 67 deletions(-) create mode 100644 pyomo/contrib/piecewise/tests/test_triangulations.py diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py new file mode 100644 index 00000000000..5eb36da4250 --- /dev/null +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -0,0 +1,84 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +import itertools +from unittest import skipUnless +import pyomo.common.unittest as unittest +from pyomo.contrib.piecewise.triangulations import ( + get_j1_triangulation, + get_incremental_simplex_ordering +) + +class TestTriangulations(unittest.TestCase): + + def test_J1_small(self): + points = [ + [0, 0], [0, 1], [0, 2], + [1, 0], [1, 1], [1, 2], + [2, 0], [2, 1], [2, 2], + ] + triangulation = get_j1_triangulation(points, 2) + self.assertEqual(triangulation.simplices, + { + 0: [[0, 0], [0, 1], [1, 1]], + 1: [[0, 1], [0, 2], [1, 1]], + 2: [[1, 1], [2, 0], [2, 1]], + 3: [[1, 1], [2, 1], [2, 2]], + 4: [[0, 0], [1, 0], [1, 1]], + 5: [[0, 2], [1, 1], [1, 2]], + 6: [[1, 0], [1, 1], [2, 0]], + 7: [[1, 1], [1, 2], [2, 2]], + }) + + # check that the points_map functionality does what it should + def test_J1_small_offset(self): + points = [ + [0.5, 0.5], [0.5, 1.5], [0.5, 2.5], + [1.5, 0.5], [1.5, 1.5], [1.5, 2.5], + [2.5, 0.5], [2.5, 1.5], [2.5, 2.5], + ] + triangulation = get_j1_triangulation(points, 2) + self.assertEqual(triangulation.simplices, + { + 0: [[0.5, 0.5], [0.5, 1.5], [1.5, 1.5]], + 1: [[0.5, 1.5], [0.5, 2.5], [1.5, 1.5]], + 2: [[1.5, 1.5], [2.5, 0.5], [2.5, 1.5]], + 3: [[1.5, 1.5], [2.5, 1.5], [2.5, 2.5]], + 4: [[0.5, 0.5], [1.5, 0.5], [1.5, 1.5]], + 5: [[0.5, 2.5], [1.5, 1.5], [1.5, 2.5]], + 6: [[1.5, 0.5], [1.5, 1.5], [2.5, 0.5]], + 7: [[1.5, 1.5], [1.5, 2.5], [2.5, 2.5]], + }) + + def test_J1_small_ordering(self): + points = [ + [0.5, 0.5], [0.5, 1.5], [0.5, 2.5], + [1.5, 0.5], [1.5, 1.5], [1.5, 2.5], + [2.5, 0.5], [2.5, 1.5], [2.5, 2.5], + ] + triangulation = get_j1_triangulation(points, 2) + reordered_simplices = get_incremental_simplex_ordering(triangulation.simplices) + for idx, first_simplex in reordered_simplices.items(): + if idx != len(triangulation.points) - 1: + second_simplex = reordered_simplices[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + def test_J1_medium_ordering(self): + points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6])) + triangulation = get_j1_triangulation(points, 2) + reordered_simplices = get_incremental_simplex_ordering(triangulation.simplices) + for idx, first_simplex in reordered_simplices.items(): + if idx != len(triangulation.points) - 1: + second_simplex = reordered_simplices[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 23ff61e6478..d522c247a0f 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -12,6 +12,7 @@ from pytest import set_trace import math import itertools +from types import SimpleNamespace from functools import cmp_to_key from pyomo.environ import ( ConcreteModel, @@ -31,23 +32,33 @@ class Triangulation: J1 = 2 def get_j1_triangulation(points, dimension): - points_map, K = _process_points_j1(points, dimension) - if dimension == 2: - return _get_j1_triangulation_2d(points_map, K) - elif dimension == 3: - return _get_j1_triangulation_3d(points, dimension) - else: - return _get_j1_triangulation_for_more_than_4d(points, dimension) + points_map, num_pts = _process_points_j1(points, dimension) + simplices_list = _get_j1_triangulation(points_map, num_pts - 1, dimension) + # make a duck-typed thing that superficially looks like an instance of + # scipy.spatial.Delaunay (these are NDarrays in the original) + triangulation = SimpleNamespace() + triangulation.points = list(range(len(simplices_list))) + triangulation.simplices = {i: simplices_list[i] for i in triangulation.points} + triangulation.coplanar = [] + + return triangulation + + #if dimension == 2: + # return _get_j1_triangulation_2d(points_map, num_pts) + #elif dimension == 3: + # return _get_j1_triangulation_3d(points, dimension) + #else: + # return _get_j1_triangulation_for_more_than_4d(points, dimension) # Does some validation but mostly assumes the user did the right thing def _process_points_j1(points, dimension): if not len(points[0]) == dimension: raise ValueError("Points not consistent with specified dimension") - K = math.floor(len(points) ** (1 / dimension)) - if not len(points) == K**dimension: + num_pts = math.floor(len(points) ** (1 / dimension)) + if not len(points) == num_pts**dimension: raise ValueError("'points' must have points forming an n-dimensional grid with straight grid lines and the same odd number of points in each axis") - if not K % 2 == 1: + if not num_pts % 2 == 1: raise ValueError("'points' must have points forming an n-dimensional grid with straight grid lines and the same odd number of points in each axis") # munge the points into an organized map with n-dimensional keys @@ -55,30 +66,57 @@ def _process_points_j1(points, dimension): # verify: does this do correct sorting by default? points.sort() points_map = {} - for point_index in itertools.product(range(K), repeat=dimension): + for point_index in itertools.product(range(num_pts), repeat=dimension): point_flat_index = 0 for n in range(dimension): - point_flat_index += point_index[dimension - 1 - n] * K**n + point_flat_index += point_index[dimension - 1 - n] * num_pts**n points_map[point_index] = points[point_flat_index] - return points_map, K + return points_map, num_pts -def _compare_lexicographic(dimension): - def compare_lexicographic_real(x, y): - for n in range(dimension): - if x[n] < y[n]: - return -1 - elif y[n] < x[n]: - return 1 - return 0 - return compare_lexicographic_real - -def _get_j1_triangulation_2d(points_map, K): +#def _compare_lexicographic(dimension): +# def compare_lexicographic_real(x, y): +# for n in range(dimension): +# if x[n] < y[n]: +# return -1 +# elif y[n] < x[n]: +# return 1 +# return 0 +# return compare_lexicographic_real + +# This implements the J1 "Union Jack" triangulation (Todd 77) as explained by +# Vielma 2010. +# Triangulate {0, ..., K}^n for even K using the J1 triangulation, mapping the +# obtained simplices through the points_map for a slight generalization. +def _get_j1_triangulation(points_map, K, n): + if K % 2 != 0: + raise ValueError("K must be even") + # 1, 3, ..., K - 1 + axis_odds = range(1, K, 2) + V_0 = itertools.product(axis_odds, repeat=n) + big_iterator = itertools.product(V_0, + itertools.permutations(range(0, n), n), + itertools.product((-1, 1), repeat=n)) + ret = [] + for v_0, pi, s in big_iterator: + simplex = [] + current = list(v_0) + simplex.append(points_map[*current]) + for i in range(0, n): + current = current.copy() + current[pi[i]] += s[pi[i]] + simplex.append(points_map[*current]) + # sort this because it might happen again later and we'd like to stay + # consistent. Undo this if it's slow. + ret.append(sorted(simplex)) + return ret + +def _get_j1_triangulation_2d(points_map, num_pts): # Each square needs two triangles in it, orientation determined by the parity of # the bottom-left corner's coordinate indices (x and y). Same parity = top-left # and bottom-right triangles; different parity = top-right and bottom-left triangles. simplices = [] - for i in range(K): - for j in range(K): + for i in range(num_pts): + for j in range(num_pts): if i % 2 == j % 2: simplices.append( (points_map[i, j], @@ -113,14 +151,28 @@ def get_incremental_simplex_ordering(simplices, subsolver='gurobi'): # with T_{i+1}. It doesn't have to be a whole face; just a vertex is enough. # (2) On each simplex T_i, the vertices are ordered T_i^1, ..., T_i^n such # that T_i^n = T_{i+1}^1 + # + # Note that (2) implies (1), so we only need to enforce that. + # + # TODO: issue: I don't think gurobi is magical enough to notice the special structure + # of this so it's basically looking for a hamiltonian path in a big graph... + # If we want to resolve this, we need to at least partially go back to the 70s thing + # + # An alternative approach is to order the simplices instead of the vertices. To + # do this, the condition (1) should be that they share a 1-face, not just a + # vertex. Then there is always a consistent way to choose distinct first and + # last vertices, which would otherwise be the issue - the rest of the vertex + # ordering can be arbitrary. Then we are really looking for a hamiltonian + # path which is what Todd did. However, we then fail to find orderings for + # strange triangulations such as two triangles intersecting at a point. m = ConcreteModel() # Sets and Params - m.SimplicesCount = Param(value=len(simplices)) + m.SimplicesCount = Param(initialize=len(simplices)) m.SIMPLICES = RangeSet(0, m.SimplicesCount - 1) # For each of the simplices we need to choose an initial and a final vertex. # The rest we can order arbitrarily after finishing the MIP solve. - m.SimplexVerticesCount = Param(value=len(simplices[0])) + m.SimplexVerticesCount = Param(initialize=len(simplices[0])) m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) @m.Param(m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) def TestVerticesEqual(m, i, n, j, k): @@ -142,14 +194,12 @@ def schedule_each_simplex(m, i): def schedule_each_slot(m, j): return sum(m.x[i, j] for i in m.SIMPLICES) == 1 - # Enforce property (1) - @m.Constraint(m.SIMPLICES) - def simplex_order(m, i): - if i == m.SimplicesCount - 1: - return Constraint.Skip # no ordering for the last one - # anything with at least a vertex in common is a neighbor - neighbors = [s for s in m.SIMPLICES if sum(TestVerticesEqual[i, n, s, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) >= 1] - return sum(m.x[i, j] * m.x[k, j+1] for j in m.SIMPLICES for k in neighbors) == 1 + # Enforce property (1), but this is guaranteed by (2) so unnecessary + #@m.Constraint(m.SIMPLICES) + #def simplex_order(m, i): + # # anything with at least a vertex in common is a neighbor + # neighbors = [s for s in m.SIMPLICES if sum(m.TestVerticesEqual[i, n, s, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) >= 1] + # return sum(m.x[i, j] * m.x[k, j+1] for j in m.SIMPLICES if j != m.SimplicesCount - 1 for k in neighbors) == 1 # Each simplex needs exactly one first and exactly one last vertex @m.Constraint(m.SIMPLICES) @@ -159,11 +209,14 @@ def one_first_vertex(m, i): def one_last_vertex(m, i): return sum(m.vertex_is_last[i, n] for n in m.VERTEX_INDICES) == 1 - # Enforce property (2) + # The last vertex cannot be the same as the first vertex + @m.Constraint(m.SIMPLICES, m.VERTEX_INDICES) + def first_last_distinct(m, i, n): + return m.vertex_is_first[i, n] * m.vertex_is_last[i, n] == 0 + + # Enforce property (2). This also guarantees property (1) @m.Constraint(m.SIMPLICES, m.SIMPLICES) def vertex_order(m, i, j): - if i == m.SimplicesCount - 1: - return Constraint.Skip # no ordering for the last one # Enforce only when j is the simplex following i. If not, RHS is zero return ( sum(m.vertex_is_last[i, n] * m.vertex_is_first[j, k] * m.TestVerticesEqual[i, n, j, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) @@ -174,40 +227,38 @@ def vertex_order(m, i, j): m.obj = Objective(expr=0) # Solve model - results = SolverFactory(subsolver).solve(m) + results = SolverFactory(subsolver).solve(m, tee=True) match(results.solver.termination_condition): case TerminationCondition.infeasible: raise ValueError("The triangulation was impossible to suitably order for the incremental transformation. Try a different triangulation, such as J1.") - case TerminationCondition.feasible: + case TerminationCondition.optimal: pass case _: - raise ValueError(f"Failed to generate suitable ordering for incremental transformation due to unexpected termination condition {results.solver.termination_condition}") + raise ValueError(f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}") # Retrieve data - simplex_ordering = {} - for i in m.SIMPLICES: - for j in m.SIMPLICES: + #m.pprint() + new_simplices = {} + for j in m.SIMPLICES: + for i in m.SIMPLICES: if abs(value(m.x[i, j]) - 1) < 1e-5: - simplex_ordering[i] = j - break - vertex_ordering = {} - for i in m.SIMPLICES: - first = None - last = None - for n in m.VERTEX_INDICES: - if abs(value(m.vertex_is_first[i, n]) - 1) < 1e-5: - first = n - vertex_ordering[i, 0] = first - if abs(value(m.vertex_is_last[i, n]) - 1) < 1e-5: - last = n - vertex_ordering[i, m.SimplexVerticesCount - 1] = last - if first is not None and last is not None: + # The jth slot is occupied by the ith simplex + old_simplex = simplices[i] + # Reorder its vertices, too + first = None + last = None + for n in m.VERTEX_INDICES: + if abs(value(m.vertex_is_first[i, n]) - 1) < 1e-5: + first = n + if abs(value(m.vertex_is_last[i, n]) - 1) < 1e-5: + last = n + if first is not None and last is not None: + break + new_simplex = [old_simplex[first]] + for n in m.VERTEX_INDICES: + if n != first and n != last: + new_simplex.append(old_simplex[n]) + new_simplex.append(old_simplex[last]) + new_simplices[j] = new_simplex break - # Fill in the middle ones arbitrarily - idx = 1 - for j in range(m.SimplexVerticesCount): - if j != first and j != last: - vertex_ordering[idx] = j - idx += 1 - - return simplex_ordering, vertex_ordering + return new_simplices \ No newline at end of file From 2d2057d24b457ef2e6cb00019d10376a14f21081 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 17 May 2024 15:29:34 -0600 Subject: [PATCH 1408/3044] Update/clarify comment and link --- pyomo/solvers/plugins/solvers/CBCplugin.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/CBCplugin.py b/pyomo/solvers/plugins/solvers/CBCplugin.py index 4d49c5cc58d..96844e8ac59 100644 --- a/pyomo/solvers/plugins/solvers/CBCplugin.py +++ b/pyomo/solvers/plugins/solvers/CBCplugin.py @@ -832,12 +832,15 @@ def process_soln_file(self, results): tokens = tuple(re.split('[ \t]+', line.strip())) n_tokens = len(tokens) # - # These are the only header entries CBC will generate (identified via browsing CbcSolver.cpp) - # See https://github.com/coin-or/Cbc/tree/master/src - # Search for (no integer solution - continuous used) - # Note that since this possibly also covers old CBC versions, - # we shall not be removing any functionality, - # even if it is not seen in the current revision + # These are the only header entries CBC will generate + # (identified via browsing CbcSolver.cpp) See + # https://github.com/coin-or/Cbc/tree/master/src/CbcSolver.cpp + # Search for "(no integer solution - continuous used)" + # (L10796 as of cb855c7) + # + # Note that since this possibly also covers old CBC + # versions, we shall not be removing any functionality, even + # if it is not seen in the current revision # if not header_processed: if tokens[0] == 'Optimal': From 7d25d1386117231391b26abefe653d267d2f5f3e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 17 May 2024 15:31:09 -0600 Subject: [PATCH 1409/3044] NFC: update doc/fix typo --- pyomo/solvers/plugins/solvers/CBCplugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/CBCplugin.py b/pyomo/solvers/plugins/solvers/CBCplugin.py index 96844e8ac59..20876b07331 100644 --- a/pyomo/solvers/plugins/solvers/CBCplugin.py +++ b/pyomo/solvers/plugins/solvers/CBCplugin.py @@ -833,12 +833,12 @@ def process_soln_file(self, results): n_tokens = len(tokens) # # These are the only header entries CBC will generate - # (identified via browsing CbcSolver.cpp) See + # (identified via browsing CbcSolver.cpp). See # https://github.com/coin-or/Cbc/tree/master/src/CbcSolver.cpp # Search for "(no integer solution - continuous used)" # (L10796 as of cb855c7) # - # Note that since this possibly also covers old CBC + # Note that since this possibly also supports old CBC # versions, we shall not be removing any functionality, even # if it is not seen in the current revision # From 6f679844a8b4c79e2ef8e0a89237bc5b0ce7f52f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 17 May 2024 16:32:52 -0600 Subject: [PATCH 1410/3044] Deprecate pyomo.core.plugins.transform.model --- pyomo/core/plugins/transform/model.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyomo/core/plugins/transform/model.py b/pyomo/core/plugins/transform/model.py index 7ee268a4292..9f370c96304 100644 --- a/pyomo/core/plugins/transform/model.py +++ b/pyomo/core/plugins/transform/model.py @@ -16,10 +16,17 @@ # because we may support an explicit matrix representation for models. # +from pyomo.common.deprecation import deprecated from pyomo.core.base import Objective, Constraint import array +@deprecated( + "to_standard_form() is deprecated. " + "Please use WriterFactory('compile_standard_form')", + version='6.7.3.dev0', + remove_in='6.8.0', +) def to_standard_form(self): """ Produces a standard-form representation of the model. Returns From 0eb5545e72bd5f0a8981a5ead7ac2788ee1985ef Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sun, 19 May 2024 15:48:10 -0400 Subject: [PATCH 1411/3044] Fixed some bugs associated with specifying experiment design variables with no indices or a single (float) value/bound --- pyomo/contrib/doe/measurements.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index fd3962f7888..e7e16db6283 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -96,16 +96,21 @@ def add_variables( ) if values is not None: + # if a scalar (int or float) is given, set it as the value for all variables + if type(values) in native_numeric_types: + values = [values] * len(added_names) # this dictionary keys are special set, values are its value self.variable_names_value.update(zip(added_names, values)) - # if a scalar (int or float) is given, set it as the lower bound for all variables + if lower_bounds is not None: + # if a scalar (int or float) is given, set it as the lower bound for all variables if type(lower_bounds) in native_numeric_types: lower_bounds = [lower_bounds] * len(added_names) self.lower_bounds.update(zip(added_names, lower_bounds)) if upper_bounds is not None: + # if a scalar (int or float) is given, set it as the upper bound for all variables if type(upper_bounds) in native_numeric_types: upper_bounds = [upper_bounds] * len(added_names) self.upper_bounds.update(zip(added_names, upper_bounds)) @@ -129,7 +134,7 @@ def _generate_variable_names_with_indices( """ # first combine all indices into a list all_index_list = [] # contains all index lists - if indices: + if indices is not None: for index_pointer in indices: all_index_list.append(indices[index_pointer]) @@ -143,8 +148,11 @@ def _generate_variable_names_with_indices( added_names = [] # iterate over index combinations ["CA", 1], ["CA", 2], ..., ["CC", 2], ["CC", 3] for index_instance in all_variable_indices: - var_name_index_string = var_name + "[" + var_name_index_string = var_name for i, idx in enumerate(index_instance): + # if i is the first index, open the [] + if i == 0: + var_name_index_string += "[" # use repr() is different from using str() # with repr(), "CA" is "CA", with str(), "CA" is CA. The first is not valid in our interface. var_name_index_string += str(idx) @@ -175,22 +183,31 @@ def _check_valid_input( """ assert isinstance(var_name, str), "var_name should be a string." - if time_index_position not in indices: + # check if time_index_position is in indices + if (indices is not None # ensure not None + and time_index_position is None # ensure not None + and time_index_position not in indices # ensure time_index_position is in indices + ): raise ValueError("time index cannot be found in indices.") # if given a list, check if bounds have the same length with flattened variable - if values is not None and len(values) != len_indices: + if (values is not None # ensure not None + and not type(values) in native_numeric_types # skip this test if scalar (int or float) + and len(values) != len_indices + ): raise ValueError("Values is of different length with indices.") if ( lower_bounds is not None # ensure not None + and not type(lower_bounds) in native_numeric_types # skip this test if scalar (int or float) and isinstance(lower_bounds, collections.abc.Sequence) # ensure list-like and len(lower_bounds) != len_indices # ensure same length ): raise ValueError("Lowerbounds is of different length with indices.") if ( - upper_bounds is not None # ensure None + upper_bounds is not None # ensure not None + and not type(upper_bounds) in native_numeric_types # skip this test if scalar (int or float) and isinstance(upper_bounds, collections.abc.Sequence) # ensure list-like and len(upper_bounds) != len_indices # ensure same length ): From 7b798ac989b8691232992f82d584c6c19e0f7d16 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sun, 19 May 2024 21:31:36 -0400 Subject: [PATCH 1412/3044] Fixed bugs in sensitivity analysis. --- pyomo/contrib/doe/doe.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 0fc3e8770fe..b00e24e18dd 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -71,6 +71,7 @@ def __init__( prior_FIM=None, discretize_model=None, args=None, + logger_level=logging.INFO ): """ This package enables model-based design of experiments analysis with Pyomo. @@ -101,6 +102,8 @@ def __init__( A user-specified ``function`` that discretizes the model. Only use with Pyomo.DAE, default=None args: Additional arguments for the create_model function. + logger_level: + Specify the level of the logger. Changer to logging.DEBUG for all messages. """ # parameters @@ -136,7 +139,7 @@ def __init__( # if print statements self.logger = logging.getLogger(__name__) - self.logger.setLevel(level=logging.INFO) + self.logger.setLevel(level=logger_level) def _check_inputs(self): """ @@ -727,6 +730,7 @@ def run_grid_search( store_optimality_as_csv=None, formula="central", step=0.001, + post_processing_function=None ): """ Enumerate through full grid search for any number of design variables; @@ -768,6 +772,10 @@ def run_grid_search( This option is only used for CalculationMode.sequential_finite. step: Sensitivity perturbation step size, a fraction between [0,1]. default is 0.001 + post_processing_function: + An optional function that executes after each solve of the grid search. + The function should take one input: the Pyomo model. This could be a plotting function. + Default is None. Returns ------- @@ -808,12 +816,18 @@ def run_grid_search( design_iter = self.design_vars.variable_names_value.copy() # update the controlled value of certain time points for certain design variables for i, names in enumerate(design_dimension_names): - # if the element is a list, all design variables in this list share the same values - if isinstance(names, collections.abc.Sequence): + print("names =",names,"for i=",i) + if isinstance(names, str): + # if 'names' is simply a string, copy the new value + design_iter[names] = list(design_set_iter)[i] + elif isinstance(names, collections.abc.Sequence): + # if the element is a list, all design variables in this list share the same values for n in names: design_iter[n] = list(design_set_iter)[i] else: - design_iter[names] = list(design_set_iter)[i] + # otherwise just copy the value + # design_iter[names] = list(design_set_iter)[i] + raise NotImplementedError('You should not see this error message. Please report it to the Pyomo.DoE developers.') self.design_vars.variable_names_value = design_iter iter_timer = TicTocTimer() @@ -828,7 +842,7 @@ def run_grid_search( else: store_output_name = store_name + str(count) - if read_name: + if read_name is not None: read_input_name = read_name + str(count) else: read_input_name = None @@ -856,12 +870,16 @@ def run_grid_search( # give run information at each iteration self.logger.info('This is run %s out of %s.', count, total_count) - self.logger.info('The code has run %s seconds.', sum(time_set)) + self.logger.info('The code has run %s seconds.', round(sum(time_set),2)) self.logger.info( 'Estimated remaining time: %s seconds', - (sum(time_set) / (count + 1) * (total_count - count - 1)), + round(sum(time_set) / (count) * (total_count - count), 2), # need to check this math... it gives a negative number for the final count ) + if post_processing_function is not None: + # Call the post processing function + post_processing_function(self.model) + # the combined result object are organized as a dictionary, keys are a tuple of the design variable values, values are a result object result_combine[tuple(design_set_iter)] = result_iter From c2a0c8e24c66941c00eccba8efd6c48155bd7d0e Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sun, 19 May 2024 21:52:40 -0400 Subject: [PATCH 1413/3044] Removed an extra print statement from debugging --- pyomo/contrib/doe/doe.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index b00e24e18dd..ddd091a24e2 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -816,7 +816,6 @@ def run_grid_search( design_iter = self.design_vars.variable_names_value.copy() # update the controlled value of certain time points for certain design variables for i, names in enumerate(design_dimension_names): - print("names =",names,"for i=",i) if isinstance(names, str): # if 'names' is simply a string, copy the new value design_iter[names] = list(design_set_iter)[i] From 0f3a1ff9277ff97f9c66747f9bbbc4a4af207358 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 20 May 2024 10:37:19 -0400 Subject: [PATCH 1414/3044] Updated useable for optional post-processing function in sensitivity analysis --- pyomo/contrib/doe/doe.py | 13 ++++++++++--- pyomo/contrib/doe/result.py | 16 ++++++++-------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index ddd091a24e2..ed44a18d262 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -423,6 +423,9 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): # solve model square_result = self._solve_doe(mod, fix=True) + # save model from optional post processing function + self._square_model_from_compute_FIM = mod + if extract_single_model: mod_name = store_output + '.csv' dataframe = extract_single_model(mod, square_result) @@ -487,10 +490,10 @@ def _direct_kaug(self): mod = self.create_model(model_option=ModelOptionLib.parmest) # discretize if needed - if self.discretize_model: + if self.discretize_model is not None: mod = self.discretize_model(mod, block=False) - # add objective function + # add zero (dummy/placeholder) objective function mod.Obj = pyo.Objective(expr=0, sense=pyo.minimize) # set ub and lb to parameters @@ -505,6 +508,10 @@ def _direct_kaug(self): # call k_aug get_dsdp function square_result = self._solve_doe(mod, fix=True) + + # save model from optional post processing function + self._square_model_from_compute_FIM = mod + dsdp_re, col = get_dsdp( mod, list(self.param.keys()), self.param, tee=self.tee_opt ) @@ -877,7 +884,7 @@ def run_grid_search( if post_processing_function is not None: # Call the post processing function - post_processing_function(self.model) + post_processing_function(self._square_model_from_compute_FIM) # the combined result object are organized as a dictionary, keys are a tuple of the design variable values, values are a result object result_combine[tuple(design_set_iter)] = result_iter diff --git a/pyomo/contrib/doe/result.py b/pyomo/contrib/doe/result.py index 1593214c30a..d8ed343352a 100644 --- a/pyomo/contrib/doe/result.py +++ b/pyomo/contrib/doe/result.py @@ -549,7 +549,7 @@ def _curve1D( ax.scatter(x_range, y_range_A) ax.set_ylabel('$log_{10}$ Trace') ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ' - A optimality') + plt.pyplot.title(title_text + ': A-optimality') plt.pyplot.show() # Draw D-optimality @@ -565,7 +565,7 @@ def _curve1D( ax.scatter(x_range, y_range_D) ax.set_ylabel('$log_{10}$ Determinant') ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ' - D optimality') + plt.pyplot.title(title_text + ': D-optimality') plt.pyplot.show() # Draw E-optimality @@ -581,7 +581,7 @@ def _curve1D( ax.scatter(x_range, y_range_E) ax.set_ylabel('$log_{10}$ Minimal eigenvalue') ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ' - E optimality') + plt.pyplot.title(title_text + ': E-optimality') plt.pyplot.show() # Draw Modified E-optimality @@ -597,7 +597,7 @@ def _curve1D( ax.scatter(x_range, y_range_ME) ax.set_ylabel('$log_{10}$ Condition number') ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ' - Modified E optimality') + plt.pyplot.title(title_text + ': Modified E-optimality') plt.pyplot.show() def _heatmap( @@ -691,7 +691,7 @@ def _heatmap( im = ax.imshow(hes_a.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) ba.set_label('log10(trace(FIM))') - plt.pyplot.title(title_text + ' - A optimality') + plt.pyplot.title(title_text + ': A-optimality') plt.pyplot.show() # D-optimality @@ -712,7 +712,7 @@ def _heatmap( im = ax.imshow(hes_d.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) ba.set_label('log10(det(FIM))') - plt.pyplot.title(title_text + ' - D optimality') + plt.pyplot.title(title_text + ': D-optimality') plt.pyplot.show() # E-optimality @@ -733,7 +733,7 @@ def _heatmap( im = ax.imshow(hes_e.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) ba.set_label('log10(minimal eig(FIM))') - plt.pyplot.title(title_text + ' - E optimality') + plt.pyplot.title(title_text + ': E-optimality') plt.pyplot.show() # modified E-optimality @@ -754,5 +754,5 @@ def _heatmap( im = ax.imshow(hes_e2.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) ba.set_label('log10(cond(FIM))') - plt.pyplot.title(title_text + ' - Modified E-optimality') + plt.pyplot.title(title_text + ': Modified E-optimality') plt.pyplot.show() From 2a9abeae16467245d5fbc1bc2246488f6d25958c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 20 May 2024 10:29:43 -0600 Subject: [PATCH 1415/3044] Full draft of new exit node handlers for psuedo constant expressions, all tests passing --- pyomo/repn/parameterized_linear.py | 83 ++++++++++--------- pyomo/repn/tests/test_parameterized_linear.py | 8 +- 2 files changed, 45 insertions(+), 46 deletions(-) diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index 29e6583d19d..9556cae1cf7 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -44,6 +44,7 @@ class ParameterizedExprType(enum.IntEnum, metaclass=ExtendedEnumType): __base_enum__ = ExprType PSUEDO_CONSTANT = 50 + _PSEUDO_CONSTANT = ParameterizedExprType.PSUEDO_CONSTANT _CONSTANT = ParameterizedExprType.CONSTANT _LINEAR = ParameterizedExprType.LINEAR @@ -65,6 +66,13 @@ def _merge_dict(dest_dict, mult, src_dict): dest_dict[vid] = coef +def to_expression(visitor, arg): + if arg[0] in (_CONSTANT, _PSEUDO_CONSTANT): + return arg[1] + else: + return arg[1].to_expression(visitor) + + class ParameterizedLinearRepn(LinearRepn): def to_expression(self, visitor): if self.nonlinear is not None: @@ -107,7 +115,7 @@ def append(self, other): """ _type, other = other - if _type is _CONSTANT: + if _type in (_CONSTANT, _PSEUDO_CONSTANT): self.constant += other return @@ -176,10 +184,7 @@ def _before_var(visitor, child): _id = id(child) if _id not in visitor.var_map: if child.fixed: - return False, ( - _CONSTANT, - visitor.check_constant(child.value, child), - ) + return False, (_CONSTANT, visitor.check_constant(child.value, child)) if child in visitor.wrt: # psueudo-constant # We aren't treating this Var as a Var for the purposes of this walker @@ -199,12 +204,13 @@ def _before_var(visitor, child): # NEGATION handler # + def _handle_negation_pseudo_constant(visitor, node, arg): return (_PSEUDO_CONSTANT, -1 * arg[1]) _exit_node_handlers[NegationExpression].update( - {(_PSEUDO_CONSTANT,): _handle_negation_pseudo_constant,} + {(_PSEUDO_CONSTANT,): _handle_negation_pseudo_constant} ) @@ -213,17 +219,13 @@ def _handle_negation_pseudo_constant(visitor, node, arg): # -def _handle_product_pseudo_constant_pseudo_constant(visitor, node, arg1, arg2): - return _PSEUDO_CONSTANT, arg1[1] * arg2[1] - - def _handle_product_pseudo_constant_constant(visitor, node, arg1, arg2): return _PSEUDO_CONSTANT, arg1[1] * arg2[1] _exit_node_handlers[ProductExpression].update( { - (_PSEUDO_CONSTANT, _PSEUDO_CONSTANT): _handle_product_pseudo_constant_pseudo_constant, + (_PSEUDO_CONSTANT, _PSEUDO_CONSTANT): _handle_product_pseudo_constant_constant, (_PSEUDO_CONSTANT, _CONSTANT): _handle_product_pseudo_constant_constant, (_CONSTANT, _PSEUDO_CONSTANT): _handle_product_pseudo_constant_constant, (_PSEUDO_CONSTANT, _LINEAR): linear._handle_product_constant_ANY, @@ -232,18 +234,17 @@ def _handle_product_pseudo_constant_constant(visitor, node, arg1, arg2): (_GENERAL, _PSEUDO_CONSTANT): linear._handle_product_ANY_constant, } ) -_exit_node_handlers[MonomialTermExpression].update(_exit_node_handlers[ProductExpression]) +_exit_node_handlers[MonomialTermExpression].update( + _exit_node_handlers[ProductExpression] +) # # DIVISION handlers # -def _handle_division_pseudo_constant_pseudo_constant(visitor, node, arg1, arg2): - return _PSEUDO_CONSTANT, arg1[1] / arg2[1] - def _handle_division_pseudo_constant_constant(visitor, node, arg1, arg2): - return _PSEUDO_CONSTANT, arg[1] / arg2[1] + return _PSEUDO_CONSTANT, arg1[1] / arg2[1] def _handle_division_ANY_pseudo_constant(visitor, node, arg1, arg2): @@ -253,7 +254,7 @@ def _handle_division_ANY_pseudo_constant(visitor, node, arg1, arg2): _exit_node_handlers[DivisionExpression].update( { - (_PSEUDO_CONSTANT, _PSEUDO_CONSTANT): _handle_division_pseudo_constant_pseudo_constant, + (_PSEUDO_CONSTANT, _PSEUDO_CONSTANT): _handle_division_pseudo_constant_constant, (_PSEUDO_CONSTANT, _CONSTANT): _handle_division_pseudo_constant_constant, (_CONSTANT, _PSEUDO_CONSTANT): _handle_division_pseudo_constant_constant, (_LINEAR, _PSEUDO_CONSTANT): _handle_division_ANY_pseudo_constant, @@ -265,42 +266,44 @@ def _handle_division_ANY_pseudo_constant(visitor, node, arg1, arg2): # EXPONENTIATION handlers # -def _handle_pow_pseudo_constant_pseudo_constant(visitor, node, arg1, arg2): - return _PSEUDO_CONSTANT, node.create_node_with_local_data( - linear.to_expression(visitor, arg1), linear.to_expression(visitor, arg2)) + +def _handle_pow_pseudo_constant_constant(visitor, node, arg1, arg2): + print("creating node") + print(to_expression(visitor, arg1)) + print(to_expression(visitor, arg2)) + return _PSEUDO_CONSTANT, to_expression(visitor, arg1) ** to_expression( + visitor, arg2 + ) -def _handle_pow_ANY_pseudo_constant(visitor, node, arg1, arg2): - # TODO - pass +def _handle_pow_ANY_psuedo_constant(visitor, node, arg1, arg2): + return linear._handle_pow_nonlinear(visitor, node, arg1, arg2) _exit_node_handlers[PowExpression].update( { - (_PSEUDO_CONSTANT, _PSEUDO_CONSTANT): _handle_pow_pseudo_constant_pseudo_constant, - (_PSEUDO_CONSTANT, _CONSTANT): _handle_pow_pseudo_constant_pseudo_constant, - (_CONSTANT, _PSEUDO_CONSTANT): _handle_pow_pseudo_constant_pseudo_constant, - (_LINEAR, _PSEUDO_CONSTANT): _handle_pow_ANY_pseudo_constant, + (_PSEUDO_CONSTANT, _PSEUDO_CONSTANT): _handle_pow_pseudo_constant_constant, + (_PSEUDO_CONSTANT, _CONSTANT): _handle_pow_pseudo_constant_constant, + (_CONSTANT, _PSEUDO_CONSTANT): _handle_pow_pseudo_constant_constant, + (_LINEAR, _PSEUDO_CONSTANT): _handle_pow_pseudo_constant_constant, + (_GENERAL, _PSEUDO_CONSTANT): _handle_pow_ANY_psuedo_constant, } ) +# +# ABS and UNARY handlers +# + -def _handle_unary_constant(visitor, node, arg): +def _handle_unary_pseudo_constant(visitor, node, arg): # We override this because we can't blindly use apply_node_operation in this case - if arg.__class__ not in native_numeric_types: - return _CONSTANT, node.create_node_with_local_data( - (linear.to_expression(visitor, arg),) - ) - # otherwise do the usual: - ans = apply_node_operation(node, (arg[1],)) - # Unary includes sqrt() which can return complex numbers - if ans.__class__ in native_complex_types: - ans = complex_number_error(ans, visitor, node) - return _CONSTANT, ans + return _PSEUDO_CONSTANT, node.create_node_with_local_data( + (to_expression(visitor, arg),) + ) _exit_node_handlers[UnaryFunctionExpression].update( - {(_CONSTANT,): _handle_unary_constant} + {(_PSEUDO_CONSTANT,): _handle_unary_pseudo_constant} ) _exit_node_handlers[AbsExpression] = _exit_node_handlers[UnaryFunctionExpression] @@ -376,6 +379,6 @@ def finalizeResult(self, result): return ans ans = self.Result() - assert result[0] is _CONSTANT + assert result[0] in (_CONSTANT, _PSEUDO_CONSTANT) ans.constant = result[1] return ans diff --git a/pyomo/repn/tests/test_parameterized_linear.py b/pyomo/repn/tests/test_parameterized_linear.py index b5e0a1a0348..068afe16929 100644 --- a/pyomo/repn/tests/test_parameterized_linear.py +++ b/pyomo/repn/tests/test_parameterized_linear.py @@ -124,9 +124,7 @@ def test_sum_nonlinear_to_nonlinear(self): assertExpressionsEqual(self, repn.nonlinear, m.x**3 + m.x**2) self.assertEqual(repn.constant, 3) self.assertEqual(repn.multiplier, 1) - assertExpressionsEqual( - self, repn.to_expression(visitor), m.x**3 + m.x**2 + 3 - ) + assertExpressionsEqual(self, repn.to_expression(visitor), m.x**3 + m.x**2 + 3) def test_sum_to_linear_expr(self): m = self.make_model() @@ -288,9 +286,7 @@ def test_finalize(self): self.assertEqual(len(repn.linear), 1) self.assertIn(id(m.y), repn.linear) assertExpressionsEqual(self, repn.linear[id(m.y)], 5 * m.w) - assertExpressionsEqual( - self, repn.nonlinear, (m.z**2 + 3 * m.w * m.y**3) * 5 - ) + assertExpressionsEqual(self, repn.nonlinear, (m.z**2 + 3 * m.w * m.y**3) * 5) def test_ANY_over_constant_division(self): m = ConcreteModel() From 2f744561e7e26ff79b34c59f97dc283c7d438b37 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 20 May 2024 12:50:52 -0400 Subject: [PATCH 1416/3044] Fixed maximizing log(trace) --- pyomo/contrib/doe/doe.py | 71 ++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index ed44a18d262..8b110bbf0a4 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -988,19 +988,22 @@ def initialize_fim(m, j, d): initialize=identity_matrix, ) - # move the L matrix initial point to a dictionary - if type(self.L_initial) != type(None): - dict_cho = {} - for i, bu in enumerate(model.regression_parameters): - for j, un in enumerate(model.regression_parameters): - dict_cho[(bu, un)] = self.L_initial[i][j] - - # use the L dictionary to initialize L matrix - def init_cho(m, i, j): - return dict_cho[(i, j)] - # if cholesky, define L elements as variables - if self.Cholesky_option: + if self.Cholesky_option and self.objective_option == ObjectiveLib.det: + + # move the L matrix initial point to a dictionary + if type(self.L_initial) != type(None): + dict_cho = {} + # Loop over rows + for i, bu in enumerate(model.regression_parameters): + # Loop over columns + for j, un in enumerate(model.regression_parameters): + dict_cho[(bu, un)] = self.L_initial[i][j] + + # use the L dictionary to initialize L matrix + def init_cho(m, i, j): + return dict_cho[(i, j)] + # Define elements of Cholesky decomposition matrix as Pyomo variables and either # Initialize with L in L_initial if type(self.L_initial) != type(None): @@ -1095,14 +1098,22 @@ def fim_rule(m, p, q): def _add_objective(self, m): + small_number = 1E-10 + + # Assemble the FIM matrix. This is helpful for initialization! + fim = np.zeros((len(self.param), len(self.param))) + for i, bu in enumerate(m.regression_parameters): + for j, un in enumerate(m.regression_parameters): + # Copy value from Pyomo model into numpy array + fim[i][j] = m.fim[bu, un].value + + # Set lower bound to ensure diagonal elements are (almost) non-negative + # m.fim[bu, un].setlb(-small_number) + ### Initialize the Cholesky decomposition matrix - if self.Cholesky_option: + if self.Cholesky_option and self.objective_option == ObjectiveLib.det: + - # Assemble the FIM matrix - fim = np.zeros((len(self.param), len(self.param))) - for i, bu in enumerate(m.regression_parameters): - for j, un in enumerate(m.regression_parameters): - fim[i][j] = m.fim[bu, un].value # Calculate the eigenvalues of the FIM matrix eig = np.linalg.eigvals(fim) @@ -1115,10 +1126,10 @@ def _add_objective(self, m): # Compute the Cholesky decomposition of the FIM matrix L = np.linalg.cholesky(fim) - # Initialize the Cholesky matrix - for i, c in enumerate(m.regression_parameters): - for j, d in enumerate(m.regression_parameters): - m.L_ele[c, d].value = L[i, j] + # Initialize the Cholesky matrix + for i, c in enumerate(m.regression_parameters): + for j, d in enumerate(m.regression_parameters): + m.L_ele[c, d].value = L[i, j] def cholesky_imp(m, c, d): """ @@ -1173,7 +1184,7 @@ def det_general(m): ) return m.det == det_perm - if self.Cholesky_option: + if self.Cholesky_option and self.objective_option == ObjectiveLib.det: m.cholesky_cons = pyo.Constraint( m.regression_parameters, m.regression_parameters, rule=cholesky_imp ) @@ -1181,16 +1192,26 @@ def det_general(m): expr=2 * sum(pyo.log(m.L_ele[j, j]) for j in m.regression_parameters), sense=pyo.maximize, ) - # if not cholesky but determinant, calculating det and evaluate the OBJ with det + elif self.objective_option == ObjectiveLib.det: + # if not cholesky but determinant, calculating det and evaluate the OBJ with det + m.det = pyo.Var(initialize=np.linalg.det(fim), bounds=(small_number, None)) m.det_rule = pyo.Constraint(rule=det_general) m.Obj = pyo.Objective(expr=pyo.log(m.det), sense=pyo.maximize) - # if not determinant or cholesky, calculating the OBJ with trace + elif self.objective_option == ObjectiveLib.trace: + # if not determinant or cholesky, calculating the OBJ with trace + m.trace = pyo.Var(initialize=np.trace(fim), bounds=(small_number, None)) m.trace_rule = pyo.Constraint(rule=trace_calc) m.Obj = pyo.Objective(expr=pyo.log(m.trace), sense=pyo.maximize) + #m.Obj = pyo.Objective(expr=m.trace, sense=pyo.maximize) + elif self.objective_option == ObjectiveLib.zero: + # add dummy objective function m.Obj = pyo.Objective(expr=0) + else: + # something went wrong! + raise ValueError("Objective option not recognized. Please contact the developers as you should not see this error.") return m From 2ae726dbba0c80b73dd73a67ed50c811515c51ea Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 20 May 2024 13:31:18 -0400 Subject: [PATCH 1417/3044] Ran black. --- pyomo/contrib/doe/doe.py | 40 ++++++++++++++++++------------- pyomo/contrib/doe/measurements.py | 21 +++++++++------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 8b110bbf0a4..3243bec6e1c 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -71,7 +71,7 @@ def __init__( prior_FIM=None, discretize_model=None, args=None, - logger_level=logging.INFO + logger_level=logging.INFO, ): """ This package enables model-based design of experiments analysis with Pyomo. @@ -508,10 +508,10 @@ def _direct_kaug(self): # call k_aug get_dsdp function square_result = self._solve_doe(mod, fix=True) - + # save model from optional post processing function self._square_model_from_compute_FIM = mod - + dsdp_re, col = get_dsdp( mod, list(self.param.keys()), self.param, tee=self.tee_opt ) @@ -737,7 +737,7 @@ def run_grid_search( store_optimality_as_csv=None, formula="central", step=0.001, - post_processing_function=None + post_processing_function=None, ): """ Enumerate through full grid search for any number of design variables; @@ -833,7 +833,9 @@ def run_grid_search( else: # otherwise just copy the value # design_iter[names] = list(design_set_iter)[i] - raise NotImplementedError('You should not see this error message. Please report it to the Pyomo.DoE developers.') + raise NotImplementedError( + 'You should not see this error message. Please report it to the Pyomo.DoE developers.' + ) self.design_vars.variable_names_value = design_iter iter_timer = TicTocTimer() @@ -876,10 +878,14 @@ def run_grid_search( # give run information at each iteration self.logger.info('This is run %s out of %s.', count, total_count) - self.logger.info('The code has run %s seconds.', round(sum(time_set),2)) + self.logger.info( + 'The code has run %s seconds.', round(sum(time_set), 2) + ) self.logger.info( 'Estimated remaining time: %s seconds', - round(sum(time_set) / (count) * (total_count - count), 2), # need to check this math... it gives a negative number for the final count + round( + sum(time_set) / (count) * (total_count - count), 2 + ), # need to check this math... it gives a negative number for the final count ) if post_processing_function is not None: @@ -990,7 +996,7 @@ def initialize_fim(m, j, d): # if cholesky, define L elements as variables if self.Cholesky_option and self.objective_option == ObjectiveLib.det: - + # move the L matrix initial point to a dictionary if type(self.L_initial) != type(None): dict_cho = {} @@ -1003,7 +1009,7 @@ def initialize_fim(m, j, d): # use the L dictionary to initialize L matrix def init_cho(m, i, j): return dict_cho[(i, j)] - + # Define elements of Cholesky decomposition matrix as Pyomo variables and either # Initialize with L in L_initial if type(self.L_initial) != type(None): @@ -1098,7 +1104,7 @@ def fim_rule(m, p, q): def _add_objective(self, m): - small_number = 1E-10 + small_number = 1e-10 # Assemble the FIM matrix. This is helpful for initialization! fim = np.zeros((len(self.param), len(self.param))) @@ -1113,8 +1119,6 @@ def _add_objective(self, m): ### Initialize the Cholesky decomposition matrix if self.Cholesky_option and self.objective_option == ObjectiveLib.det: - - # Calculate the eigenvalues of the FIM matrix eig = np.linalg.eigvals(fim) @@ -1192,26 +1196,28 @@ def det_general(m): expr=2 * sum(pyo.log(m.L_ele[j, j]) for j in m.regression_parameters), sense=pyo.maximize, ) - + elif self.objective_option == ObjectiveLib.det: # if not cholesky but determinant, calculating det and evaluate the OBJ with det m.det = pyo.Var(initialize=np.linalg.det(fim), bounds=(small_number, None)) m.det_rule = pyo.Constraint(rule=det_general) m.Obj = pyo.Objective(expr=pyo.log(m.det), sense=pyo.maximize) - + elif self.objective_option == ObjectiveLib.trace: # if not determinant or cholesky, calculating the OBJ with trace m.trace = pyo.Var(initialize=np.trace(fim), bounds=(small_number, None)) m.trace_rule = pyo.Constraint(rule=trace_calc) m.Obj = pyo.Objective(expr=pyo.log(m.trace), sense=pyo.maximize) - #m.Obj = pyo.Objective(expr=m.trace, sense=pyo.maximize) - + # m.Obj = pyo.Objective(expr=m.trace, sense=pyo.maximize) + elif self.objective_option == ObjectiveLib.zero: # add dummy objective function m.Obj = pyo.Objective(expr=0) else: # something went wrong! - raise ValueError("Objective option not recognized. Please contact the developers as you should not see this error.") + raise ValueError( + "Objective option not recognized. Please contact the developers as you should not see this error." + ) return m diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index e7e16db6283..47df09d27c1 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -102,7 +102,6 @@ def add_variables( # this dictionary keys are special set, values are its value self.variable_names_value.update(zip(added_names, values)) - if lower_bounds is not None: # if a scalar (int or float) is given, set it as the lower bound for all variables if type(lower_bounds) in native_numeric_types: @@ -184,22 +183,27 @@ def _check_valid_input( assert isinstance(var_name, str), "var_name should be a string." # check if time_index_position is in indices - if (indices is not None # ensure not None - and time_index_position is None # ensure not None - and time_index_position not in indices # ensure time_index_position is in indices + if ( + indices is not None # ensure not None + and time_index_position is None # ensure not None + and time_index_position + not in indices # ensure time_index_position is in indices ): raise ValueError("time index cannot be found in indices.") # if given a list, check if bounds have the same length with flattened variable - if (values is not None # ensure not None - and not type(values) in native_numeric_types # skip this test if scalar (int or float) + if ( + values is not None # ensure not None + and not type(values) + in native_numeric_types # skip this test if scalar (int or float) and len(values) != len_indices ): raise ValueError("Values is of different length with indices.") if ( lower_bounds is not None # ensure not None - and not type(lower_bounds) in native_numeric_types # skip this test if scalar (int or float) + and not type(lower_bounds) + in native_numeric_types # skip this test if scalar (int or float) and isinstance(lower_bounds, collections.abc.Sequence) # ensure list-like and len(lower_bounds) != len_indices # ensure same length ): @@ -207,7 +211,8 @@ def _check_valid_input( if ( upper_bounds is not None # ensure not None - and not type(upper_bounds) in native_numeric_types # skip this test if scalar (int or float) + and not type(upper_bounds) + in native_numeric_types # skip this test if scalar (int or float) and isinstance(upper_bounds, collections.abc.Sequence) # ensure list-like and len(upper_bounds) != len_indices # ensure same length ): From 405ac567a4c2d793adf79400b77609466bbc226d Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 20 May 2024 13:46:46 -0400 Subject: [PATCH 1418/3044] Added note to help with additional debugging. --- pyomo/contrib/doe/measurements.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index 47df09d27c1..229fb7f7830 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -182,6 +182,7 @@ def _check_valid_input( """ assert isinstance(var_name, str), "var_name should be a string." + # debugging note: what is an integer versus a list versus a dictionary here? # check if time_index_position is in indices if ( indices is not None # ensure not None From 50c8ffc71be56ce8569bb3cca54d82122d58507a Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 20 May 2024 14:17:07 -0400 Subject: [PATCH 1419/3044] Enact design variable bounds --- pyomo/contrib/doe/doe.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 3243bec6e1c..f84147841c6 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -662,6 +662,13 @@ def fix1(mod, s): con_name = "con" + name mod.add_component(con_name, pyo.Constraint(mod.scenario, expr=fix1)) + # Add user-defined design variable bounds + cuid = pyo.ComponentUID(name) + design_var_global = cuid.find_component_on(mod) + # Set the lower and upper bounds of the design variables + design_var_global.setlb(self.design_vars.lower_bounds[name]) + design_var_global.setub(self.design_vars.upper_bounds[name]) + return mod def _finite_calculation(self, output_record): From dbd3d8f0c8f753881fa66faf9783e2395487121f Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 20 May 2024 14:19:46 -0400 Subject: [PATCH 1420/3044] Enacted user-passed model arguments --- pyomo/contrib/doe/doe.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index f84147841c6..02ac1908b5d 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -70,7 +70,7 @@ def __init__( solver=None, prior_FIM=None, discretize_model=None, - args=None, + args={}, logger_level=logging.INFO, ): """ @@ -487,7 +487,7 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): def _direct_kaug(self): # create model - mod = self.create_model(model_option=ModelOptionLib.parmest) + mod = self.create_model(model_option=ModelOptionLib.parmest, **self.args) # discretize if needed if self.discretize_model is not None: @@ -612,7 +612,7 @@ def _create_block(self): ) # Allow user to self-define complex design variables - self.create_model(mod=mod, model_option=ModelOptionLib.stage1) + self.create_model(mod=mod, model_option=ModelOptionLib.stage1, **self.args) # Fix parameter values in the copy of the stage1 model (if they exist) for par in self.param: @@ -632,11 +632,11 @@ def block_build(b, s): theta_initialize = self.scenario_data.scenario[s] # Add model on block with theta values self.create_model( - mod=b, model_option=ModelOptionLib.stage2, theta=theta_initialize + mod=b, model_option=ModelOptionLib.stage2, theta=theta_initialize, **self.args, ) else: # Otherwise add model on block without theta values - self.create_model(mod=b, model_option=ModelOptionLib.stage2) + self.create_model(mod=b, model_option=ModelOptionLib.stage2, **self.args) # fix parameter values to perturbed values for par in self.param: From 0d99834c446045efbf58a615cb2c4ae81f23f06a Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 20 May 2024 13:59:38 -0600 Subject: [PATCH 1421/3044] Fixing a bug where psuedo constants were getting labeled as constants in walker_exitNode --- pyomo/repn/parameterized_linear.py | 27 +++-- pyomo/repn/tests/test_parameterized_linear.py | 105 +++++++++++++++++- 2 files changed, 120 insertions(+), 12 deletions(-) diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index 9556cae1cf7..67fb4a7421e 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -74,6 +74,16 @@ def to_expression(visitor, arg): class ParameterizedLinearRepn(LinearRepn): + def walker_exitNode(self): + if self.nonlinear is not None: + return _GENERAL, self + elif self.linear: + return _LINEAR, self + elif self.constant.__class__ in native_numeric_types: + return _CONSTANT, self.multiplier * self.constant + else: + return _PSEUDO_CONSTANT, self.multiplier * self.constant + def to_expression(self, visitor): if self.nonlinear is not None: # We want to start with the nonlinear term (and use @@ -268,16 +278,17 @@ def _handle_division_ANY_pseudo_constant(visitor, node, arg1, arg2): def _handle_pow_pseudo_constant_constant(visitor, node, arg1, arg2): - print("creating node") - print(to_expression(visitor, arg1)) - print(to_expression(visitor, arg2)) return _PSEUDO_CONSTANT, to_expression(visitor, arg1) ** to_expression( visitor, arg2 ) -def _handle_pow_ANY_psuedo_constant(visitor, node, arg1, arg2): - return linear._handle_pow_nonlinear(visitor, node, arg1, arg2) +def _handle_pow_nonlinear(visitor, node, arg1, arg2): + # ESJ: We override this because we need our own to_expression implementation + # if pseudo constants are involved. + ans = visitor.Result() + ans.nonlinear = to_expression(visitor, arg1) ** to_expression(visitor, arg2) + return _GENERAL, ans _exit_node_handlers[PowExpression].update( @@ -285,8 +296,10 @@ def _handle_pow_ANY_psuedo_constant(visitor, node, arg1, arg2): (_PSEUDO_CONSTANT, _PSEUDO_CONSTANT): _handle_pow_pseudo_constant_constant, (_PSEUDO_CONSTANT, _CONSTANT): _handle_pow_pseudo_constant_constant, (_CONSTANT, _PSEUDO_CONSTANT): _handle_pow_pseudo_constant_constant, - (_LINEAR, _PSEUDO_CONSTANT): _handle_pow_pseudo_constant_constant, - (_GENERAL, _PSEUDO_CONSTANT): _handle_pow_ANY_psuedo_constant, + (_LINEAR, _PSEUDO_CONSTANT): _handle_pow_nonlinear, + (_PSEUDO_CONSTANT, _LINEAR): _handle_pow_nonlinear, + (_GENERAL, _PSEUDO_CONSTANT): _handle_pow_nonlinear, + (_PSEUDO_CONSTANT, _GENERAL): _handle_pow_nonlinear, } ) diff --git a/pyomo/repn/tests/test_parameterized_linear.py b/pyomo/repn/tests/test_parameterized_linear.py index 068afe16929..de4de301a83 100644 --- a/pyomo/repn/tests/test_parameterized_linear.py +++ b/pyomo/repn/tests/test_parameterized_linear.py @@ -268,7 +268,6 @@ def test_finalize(self): self.assertEqual(repn.constant, 0) self.assertEqual(len(repn.linear), 2) self.assertIn(id(m.y), repn.linear) - print(repn.linear[id(m.y)]) assertExpressionsEqual(self, repn.linear[id(m.y)], 5 * (2 * m.w**2)) self.assertIn(id(m.z), repn.linear) assertExpressionsEqual(self, repn.linear[id(m.z)], -5 * m.w) @@ -307,7 +306,6 @@ def test_ANY_over_constant_division(self): self.assertEqual(repn.multiplier, 1) assertExpressionsEqual(self, repn.constant, 1 + m.z) self.assertEqual(len(repn.linear), 1) - print(repn.linear[id(m.x)]) assertExpressionsEqual(self, repn.linear[id(m.x)], 1 + 1.5 * m.z) self.assertEqual(repn.nonlinear, None) @@ -344,8 +342,6 @@ def test_errors_propogate_nan(self): expr ) self.assertEqual(repn.multiplier, 1) - # TODO: Is this expected to just wrap up into a single InvalidNumber? - print(repn.constant) self.assertIsInstance(repn.constant, InvalidNumber) assertExpressionsEqual(self, repn.constant.value, float('nan') * m.z + 3) self.assertEqual(repn.linear, {}) @@ -372,7 +368,106 @@ def test_product_nonlinear(self): self.assertEqual(len(repn.linear), 0) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) - print(repn.nonlinear) assertExpressionsEqual( self, repn.nonlinear, (m.x**2) * (m.z**4 * log(m.y)) * m.y ) + + def test_division_pseudo_constant_constant(self): + m = self.make_model() + e = m.x / 4 + m.y + + cfg = VisitorConfig() + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(e) + + self.assertEqual(len(repn.linear), 1) + self.assertIn(id(m.y), repn.linear) + self.assertEqual(repn.linear[id(m.y)], 1) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, m.x / 4) + self.assertIsNone(repn.nonlinear) + + e = 4 / m.x + m.y + cfg = VisitorConfig() + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(e) + + self.assertEqual(len(repn.linear), 1) + self.assertIn(id(m.y), repn.linear) + self.assertEqual(repn.linear[id(m.y)], 1) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, 4 / m.x) + self.assertIsNone(repn.nonlinear) + + e = m.z / m.x + m.y + cfg = VisitorConfig() + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.x, m.z]).walk_expression(e) + + self.assertEqual(len(repn.linear), 1) + self.assertIn(id(m.y), repn.linear) + self.assertEqual(repn.linear[id(m.y)], 1) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, m.z / m.x) + self.assertIsNone(repn.nonlinear) + + def test_division_ANY_psuedo_constant(self): + m = self.make_model() + e = (m.x + 3 * m.z) / m.y + + cfg = VisitorConfig() + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]).walk_expression(e) + + self.assertEqual(len(repn.linear), 2) + self.assertIn(id(m.x), repn.linear) + assertExpressionsEqual(self, repn.linear[id(m.x)], 1 / m.y) + self.assertIn(id(m.z), repn.linear) + assertExpressionsEqual(self, repn.linear[id(m.z)], (1 / m.y) * 3) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertIsNone(repn.nonlinear) + + def test_pow_ANY_psuedo_constant(self): + m = self.make_model() + e = (m.x**2 + 3 * m.z) ** m.y + + cfg = VisitorConfig() + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]).walk_expression(e) + + self.assertEqual(len(repn.linear), 0) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + assertExpressionsEqual(self, repn.nonlinear, (m.x**2 + 3 * m.z) ** m.y) + + def test_pow_psuedo_constant_ANY(self): + m = self.make_model() + e = m.y ** (m.x**2 + 3 * m.z) + + cfg = VisitorConfig() + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]).walk_expression(e) + + self.assertEqual(len(repn.linear), 0) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + assertExpressionsEqual(self, repn.nonlinear, m.y ** (m.x**2 + 3 * m.z)) + + def test_pow_linear_pseudo_constant(self): + m = self.make_model() + e = (m.x + 3 * m.z) ** m.y + + cfg = VisitorConfig() + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]).walk_expression(e) + + self.assertEqual(len(repn.linear), 0) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + assertExpressionsEqual(self, repn.nonlinear, (m.x + 3 * m.z) ** m.y) + + def test_pow_pseudo_constant_linear(self): + m = self.make_model() + e = m.y ** (m.x + 3 * m.z) + + cfg = VisitorConfig() + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]).walk_expression(e) + + self.assertEqual(len(repn.linear), 0) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + assertExpressionsEqual(self, repn.nonlinear, m.y ** (m.x + 3 * m.z)) From 9ded80f156e3f1e0139d98bf4e036dc30c100e05 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 20 May 2024 16:12:25 -0600 Subject: [PATCH 1422/3044] Resolve KeyError with export_nonlinear_variables --- pyomo/repn/plugins/nl_writer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index fc6db2c33fb..7a991a989f0 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -851,6 +851,7 @@ def write(self, model): if _id not in var_map: var_map[_id] = _v var_bounds[_id] = _v.bounds + var_values[_id] = _v.value con_vars_nonlinear.add(_id) con_nnz = sum(con_nnz_by_var.values()) From 5badeba53f1b47685c885fff48df3a6e887ef718 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 20 May 2024 16:13:18 -0600 Subject: [PATCH 1423/3044] Fix error when ressolving constant arguments to external functions --- pyomo/repn/plugins/nl_writer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 7a991a989f0..331c45b78a5 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1683,8 +1683,9 @@ def _categorize_vars(self, comp_list, linear_by_comp): if _id in nonlinear_vars: continue if _id not in var_map and _id not in used_named_expressions: - _sub_info = subexpression_cache[_id][1] - _id_src.append(_sub_info.nonlinear[1]) + _sub_info = subexpression_cache[_id][1].nonlinear + if _sub_info: + _id_src.append(_sub_info[1]) continue if _id in linear_by_comp: nonlinear_vars.update(linear_by_comp[_id]) From 6c1ced9ab4c922dca1b8823911ceea910f853acd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 20 May 2024 16:14:24 -0600 Subject: [PATCH 1424/3044] Add test for external functions whose arguments presolve to constants --- pyomo/repn/tests/ampl/test_nlv2.py | 120 ++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index f2378e56b4c..784a277c118 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -25,6 +25,7 @@ from pyomo.common.dependencies import numpy, numpy_available from pyomo.common.errors import MouseTrap +from pyomo.common.gsl import find_GSL from pyomo.common.log import LoggingIntercept from pyomo.common.tee import capture_output from pyomo.common.tempfiles import TempfileManager @@ -2311,7 +2312,7 @@ def test_discrete_var_tabulation(self): def test_presolve_fixes_nl_defined_variables(self): # This tests a workaround for a bug in the ASL where defined - # variables with nonstant expressions in the NL portion are not + # variables with constant expressions in the NL portion are not # evaluated correctly. m = ConcreteModel() m.x = Var() @@ -2471,6 +2472,123 @@ def test_presolve_fixes_nl_defined_variables(self): J1 2 #c2 0 1 2 -1 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_fixes_nl_exernal_function(self): + # This tests a workaround for a bug in the ASL where external + # functions with constant argument expressions are not + # evaluated correctly. + DLL = find_GSL() + if not DLL: + self.skipTest("Could not find the amplgsl.dll library") + + m = ConcreteModel() + m.hypot = ExternalFunction(library=DLL, function="gsl_hypot") + m.p = Param(initialize=1, mutable=True) + m.x = Var(bounds=(None, 3)) + m.y = Var(bounds=(3, None)) + m.z = Var(initialize=1) + m.o = Objective(expr=m.z**2 * m.hypot(m.p * m.x, m.p + m.y) ** 2) + m.c = Constraint(expr=m.x == m.y) + + OUT = io.StringIO() + nl_writer.NLWriter().write( + m, + OUT, + symbolic_solver_labels=True, + linear_presolve=False, + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 #problem unknown + 3 1 1 0 1 #vars, constraints, objectives, ranges, eqns + 0 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 3 0 #nonlinear vars in constraints, objectives, both + 0 1 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 2 3 #nonzeros in Jacobian, obj. gradient + 1 1 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +F0 1 -1 gsl_hypot +C0 #c +n0 +O0 0 #o +o2 #* +o5 #^ +v0 #z +n2 +o5 #^ +f0 2 #hypot +v1 #x +o0 #+ +v2 #y +n1 +n2 +x1 #initial guess +0 1 #z +r #1 ranges (rhs's) +4 0 #c +b #3 bounds (on variables) +3 #z +1 3 #x +2 3 #y +k2 #intermediate Jacobian column lengths +0 +1 +J0 2 #c +1 1 +2 -1 +G0 3 #o +0 0 +1 0 +2 0 +""", + OUT.getvalue(), + ) + ) + + OUT = io.StringIO() + nl_writer.NLWriter().write( + m, + OUT, + symbolic_solver_labels=True, + linear_presolve=True, + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 #problem unknown + 1 0 1 0 0 #vars, constraints, objectives, ranges, eqns + 0 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 1 0 #nonlinear vars in constraints, objectives, both + 0 1 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 0 1 #nonzeros in Jacobian, obj. gradient + 1 1 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +F0 1 -1 gsl_hypot +O0 0 #o +o2 #* +o5 #^ +v0 #z +n2 +o5 #^ +f0 2 #hypot +n3 +n4 +n2 +x1 #initial guess +0 1 #z +r #0 ranges (rhs's) +b #1 bounds (on variables) +3 #z +k0 #intermediate Jacobian column lengths +G0 1 #o +0 0 """, OUT.getvalue(), ) From f13d3ab5905f9bba4c67af6b9ea6cd9a68b38d52 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 20 May 2024 16:17:53 -0600 Subject: [PATCH 1425/3044] Making the new walker compliant with IEEE 754 when multiplying 0 and nan --- pyomo/repn/parameterized_linear.py | 15 ++---- pyomo/repn/tests/test_parameterized_linear.py | 53 +++++++++++++++++++ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index 67fb4a7421e..6748c406bb7 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -200,7 +200,6 @@ def _before_var(visitor, child): # We aren't treating this Var as a Var for the purposes of this walker return False, (_PSEUDO_CONSTANT, child) # This is a normal situation - # TODO: override record var to not record things in wrt MultiLevelLinearBeforeChildDispatcher._record_var(visitor, child) ans = visitor.Result() ans.linear[_id] = 1 @@ -228,6 +227,8 @@ def _handle_negation_pseudo_constant(visitor, node, arg): # PRODUCT handler # +def _handle_product_constant_constant(visitor, node, arg1, arg2): + return _CONSTANT, arg1[1] * arg2[1] def _handle_product_pseudo_constant_constant(visitor, node, arg1, arg2): return _PSEUDO_CONSTANT, arg1[1] * arg2[1] @@ -235,6 +236,7 @@ def _handle_product_pseudo_constant_constant(visitor, node, arg1, arg2): _exit_node_handlers[ProductExpression].update( { + (_CONSTANT, _CONSTANT): _handle_product_constant_constant, (_PSEUDO_CONSTANT, _PSEUDO_CONSTANT): _handle_product_pseudo_constant_constant, (_PSEUDO_CONSTANT, _CONSTANT): _handle_product_pseudo_constant_constant, (_CONSTANT, _PSEUDO_CONSTANT): _handle_product_pseudo_constant_constant, @@ -372,18 +374,11 @@ def finalizeResult(self, result): # Warn if this is suppressing a NaN (unusual, and # non-standard, but we will wait to remove this behavior # for the time being) - # ESJ TODO: This won't work either actually... - # I'm not sure how to do it. if ans.constant != ans.constant or any( c != c for c in ans.linear.values() ): - deprecation_warning( - f"Encountered {str(mult)}*nan in expression tree. " - "Mapping the NaN result to 0 for compatibility " - "with the lp_v1 writer. In the future, this NaN " - "will be preserved/emitted to comply with IEEE-754.", - version='6.6.0', - ) + # There's a nan in here, so we keep it + self._factor_multiplier_into_linear_terms(ans, mult) return self.Result() else: # mult not in {0, 1}: factor it into the constant, diff --git a/pyomo/repn/tests/test_parameterized_linear.py b/pyomo/repn/tests/test_parameterized_linear.py index de4de301a83..2148e3053f4 100644 --- a/pyomo/repn/tests/test_parameterized_linear.py +++ b/pyomo/repn/tests/test_parameterized_linear.py @@ -471,3 +471,56 @@ def test_pow_pseudo_constant_linear(self): self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) assertExpressionsEqual(self, repn.nonlinear, m.y ** (m.x + 3 * m.z)) + + def test_0_mult(self): + m = self.make_model() + m.p = Var() + m.p.fix(0) + e = m.p * (m.y ** 2 + m.z) + + cfg = VisitorConfig() + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.z]).walk_expression(e) + + self.assertEqual(len(repn.linear), 0) + self.assertEqual(repn.multiplier, 1) + self.assertIsNone(repn.nonlinear) + self.assertEqual(repn.constant, 0) + + def test_0_mult_nan(self): + m = self.make_model() + m.p = Param(initialize=0, mutable=True) + m.y.domain = Any + m.y.fix(float('nan')) + e = m.p * (m.y ** 2 + m.x) + + cfg = VisitorConfig() + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(e) + + self.assertEqual(len(repn.linear), 0) + self.assertEqual(repn.multiplier, 1) + self.assertIsNone(repn.nonlinear) + self.assertIsInstance(repn.constant, InvalidNumber) + assertExpressionsEqual( + self, + repn.constant.value, + 0 * (float('nan') + m.x) + ) + + def test_0_mult_nan_param(self): + m = self.make_model() + m.p = Param(initialize=0, mutable=True) + m.y.fix(float('nan')) + e = m.p * (m.y ** 2) + + cfg = VisitorConfig() + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]).walk_expression(e) + + self.assertEqual(len(repn.linear), 0) + self.assertEqual(repn.multiplier, 1) + self.assertIsNone(repn.nonlinear) + self.assertIsInstance(repn.constant, InvalidNumber) + assertExpressionsEqual( + self, + repn.constant.value, + 0 * float('nan') + ) From 42ff2932f5c88ad5025971593ced3e48345240b7 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 20 May 2024 16:18:21 -0600 Subject: [PATCH 1426/3044] Black --- pyomo/repn/parameterized_linear.py | 2 ++ pyomo/repn/tests/test_parameterized_linear.py | 20 ++++++------------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index 6748c406bb7..5928d1f18d9 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -227,9 +227,11 @@ def _handle_negation_pseudo_constant(visitor, node, arg): # PRODUCT handler # + def _handle_product_constant_constant(visitor, node, arg1, arg2): return _CONSTANT, arg1[1] * arg2[1] + def _handle_product_pseudo_constant_constant(visitor, node, arg1, arg2): return _PSEUDO_CONSTANT, arg1[1] * arg2[1] diff --git a/pyomo/repn/tests/test_parameterized_linear.py b/pyomo/repn/tests/test_parameterized_linear.py index 2148e3053f4..1b0ab630462 100644 --- a/pyomo/repn/tests/test_parameterized_linear.py +++ b/pyomo/repn/tests/test_parameterized_linear.py @@ -476,7 +476,7 @@ def test_0_mult(self): m = self.make_model() m.p = Var() m.p.fix(0) - e = m.p * (m.y ** 2 + m.z) + e = m.p * (m.y**2 + m.z) cfg = VisitorConfig() repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.z]).walk_expression(e) @@ -491,7 +491,7 @@ def test_0_mult_nan(self): m.p = Param(initialize=0, mutable=True) m.y.domain = Any m.y.fix(float('nan')) - e = m.p * (m.y ** 2 + m.x) + e = m.p * (m.y**2 + m.x) cfg = VisitorConfig() repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(e) @@ -500,17 +500,13 @@ def test_0_mult_nan(self): self.assertEqual(repn.multiplier, 1) self.assertIsNone(repn.nonlinear) self.assertIsInstance(repn.constant, InvalidNumber) - assertExpressionsEqual( - self, - repn.constant.value, - 0 * (float('nan') + m.x) - ) - + assertExpressionsEqual(self, repn.constant.value, 0 * (float('nan') + m.x)) + def test_0_mult_nan_param(self): m = self.make_model() m.p = Param(initialize=0, mutable=True) m.y.fix(float('nan')) - e = m.p * (m.y ** 2) + e = m.p * (m.y**2) cfg = VisitorConfig() repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]).walk_expression(e) @@ -519,8 +515,4 @@ def test_0_mult_nan_param(self): self.assertEqual(repn.multiplier, 1) self.assertIsNone(repn.nonlinear) self.assertIsInstance(repn.constant, InvalidNumber) - assertExpressionsEqual( - self, - repn.constant.value, - 0 * float('nan') - ) + assertExpressionsEqual(self, repn.constant.value, 0 * float('nan')) From fb78c898b9f1c8fe1aa3e45fa1bb43b923fbe518 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 20 May 2024 16:21:49 -0600 Subject: [PATCH 1427/3044] Fixing some typos --- pyomo/repn/parameterized_linear.py | 6 +++--- pyomo/repn/tests/test_parameterized_linear.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index 5928d1f18d9..8200b4cf01e 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -42,10 +42,10 @@ class ParameterizedExprType(enum.IntEnum, metaclass=ExtendedEnumType): __base_enum__ = ExprType - PSUEDO_CONSTANT = 50 + PSEUDO_CONSTANT = 50 -_PSEUDO_CONSTANT = ParameterizedExprType.PSUEDO_CONSTANT +_PSEUDO_CONSTANT = ParameterizedExprType.PSEUDO_CONSTANT _CONSTANT = ParameterizedExprType.CONSTANT _LINEAR = ParameterizedExprType.LINEAR _GENERAL = ParameterizedExprType.GENERAL @@ -196,7 +196,7 @@ def _before_var(visitor, child): if child.fixed: return False, (_CONSTANT, visitor.check_constant(child.value, child)) if child in visitor.wrt: - # psueudo-constant + # pseudo-constant # We aren't treating this Var as a Var for the purposes of this walker return False, (_PSEUDO_CONSTANT, child) # This is a normal situation diff --git a/pyomo/repn/tests/test_parameterized_linear.py b/pyomo/repn/tests/test_parameterized_linear.py index 1b0ab630462..7d99acf8bb8 100644 --- a/pyomo/repn/tests/test_parameterized_linear.py +++ b/pyomo/repn/tests/test_parameterized_linear.py @@ -309,7 +309,7 @@ def test_ANY_over_constant_division(self): assertExpressionsEqual(self, repn.linear[id(m.x)], 1 + 1.5 * m.z) self.assertEqual(repn.nonlinear, None) - def test_errors_propogate_nan(self): + def test_errors_propagate_nan(self): m = ConcreteModel() m.p = Param(mutable=True, initialize=0, domain=Any) m.x = Var() From 26c0e4bf3d13a9fbd707a5c7809ef7be7e680567 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 20 May 2024 16:27:49 -0600 Subject: [PATCH 1428/3044] NFC: apply black --- pyomo/repn/tests/ampl/test_nlv2.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 784a277c118..97c93ad1649 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2496,10 +2496,7 @@ def test_presolve_fixes_nl_exernal_function(self): OUT = io.StringIO() nl_writer.NLWriter().write( - m, - OUT, - symbolic_solver_labels=True, - linear_presolve=False, + m, OUT, symbolic_solver_labels=True, linear_presolve=False ) self.assertEqual( *nl_diff( @@ -2553,10 +2550,7 @@ def test_presolve_fixes_nl_exernal_function(self): OUT = io.StringIO() nl_writer.NLWriter().write( - m, - OUT, - symbolic_solver_labels=True, - linear_presolve=True, + m, OUT, symbolic_solver_labels=True, linear_presolve=True ) self.assertEqual( *nl_diff( From 5d13a38bd6d2f699412f447b85101e4351e0823b Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Tue, 21 May 2024 07:17:14 -0400 Subject: [PATCH 1429/3044] Removed bounds on FIM diagonal --- pyomo/contrib/doe/doe.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 02ac1908b5d..412f387f8b5 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1121,7 +1121,8 @@ def _add_objective(self, m): fim[i][j] = m.fim[bu, un].value # Set lower bound to ensure diagonal elements are (almost) non-negative - # m.fim[bu, un].setlb(-small_number) + # if i == j: + # m.fim[bu, un].setlb(-small_number) ### Initialize the Cholesky decomposition matrix if self.Cholesky_option and self.objective_option == ObjectiveLib.det: From d2b75f36f0dca7e6fc243719e3e7db542ca23dfd Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Tue, 21 May 2024 07:21:14 -0400 Subject: [PATCH 1430/3044] Added suggestion for future improvement --- pyomo/contrib/doe/doe.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 412f387f8b5..f5c053876bd 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1082,6 +1082,11 @@ def read_prior(m, i, j): model.regression_parameters, model.regression_parameters, rule=read_prior ) + # TODO: explore exploiting the symmetry of the FIM matrix + # The off-diagonal elements are symmetric, thus only half of the elements need to be calculated + # Syntax challenge: determine the order of p and q, i.e., if p > q, then replace with + # equality constraint fim[p, q] == fim[q, p] + def fim_rule(m, p, q): """ m: Pyomo model From 5abdce0b6a329bc896bf19429fb3d6c399e3dc9c Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 21 May 2024 05:34:54 -0600 Subject: [PATCH 1431/3044] Adding some documentation --- .../contrib/alternative_solutions/lp_enum.py | 111 +++++++++++------- 1 file changed, 70 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index 64ba5ae8bab..7eb16dcdacf 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -31,51 +31,61 @@ def enumerate_linear_solutions( solver_options={}, tee=False, quiet=True, + debug=False, seed=None, ): """ Finds alternative optimal solutions a (mixed-integer) linear program. - Parameters - ---------- - model : ConcreteModel - A concrete Pyomo model - num_solutions : int - The maximum number of solutions to generate. - variables: 'all' or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. 'all' indicates - that all variables will be included. Alternatively, a collection of - _GenereralVarData variables can be provided. - rel_opt_gap : float or None - The relative optimality gap for the original objective for which - variable bounds will be found. None indicates that a relative gap - constraint will not be added to the model. - abs_opt_gap : float or None - The absolute optimality gap for the original objective for which - variable bounds will be found. None indicates that an absolute gap - constraint will not be added to the model. - search_mode : 'optimal', 'random', or 'norm' - Indicates the mode that is used to generate alternative solutions. - The optimal mode finds the next best solution. The random mode - finds an alternative solution in the direction of a random ray. The - norm mode iteratively finds solution that maximize the L2 distance - from previously discovered solutions. - solver : string - The solver to be used. - solver_options : dict - Solver option-value pairs to be passed to the solver. - tee : boolean - Boolean indicating that the solver output should be displayed. - quiet : boolean - Boolean indicating whether to suppress all output. - seed : int - Optional integer seed for the numpy random number generator - - Returns - ------- - solutions - A list of Solution objects. - [Solution] + This function implements the technique described here: + + S. Lee, C. Phalakornkule, M.M. Domach, and I.E. Grossmann, + "Recursive MILP model for finding all the alternative optima in LP + models for metabolic networks", Computers and Chemical Engineering, + 24 (2000) 711-716. + + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model + num_solutions : int + The maximum number of solutions to generate. + variables: 'all' or a collection of Pyomo _GeneralVarData variables + The variables for which bounds will be generated. 'all' indicates + that all variables will be included. Alternatively, a collection of + _GenereralVarData variables can be provided. + rel_opt_gap : float or None + The relative optimality gap for the original objective for which + variable bounds will be found. None indicates that a relative gap + constraint will not be added to the model. + abs_opt_gap : float or None + The absolute optimality gap for the original objective for which + variable bounds will be found. None indicates that an absolute gap + constraint will not be added to the model. + search_mode : 'optimal', 'random', or 'norm' + Indicates the mode that is used to generate alternative solutions. + The optimal mode finds the next best solution. The random mode + finds an alternative solution in the direction of a random ray. The + norm mode iteratively finds solution that maximize the L2 distance + from previously discovered solutions. + solver : string + The solver to be used. + solver_options : dict + Solver option-value pairs to be passed to the solver. + tee : boolean + Boolean indicating that the solver output should be displayed. + quiet : boolean + Boolean indicating whether to suppress all output. + debug : boolean + Boolean indicating whether to include debugging output. + seed : int + Optional integer seed for the numpy random number generator + + Returns + ------- + solutions + A list of Solution objects. + [Solution] """ if not quiet: # pragma: no cover print("STARTING LP ENUMERATION ANALYSIS") @@ -202,7 +212,11 @@ def enumerate_linear_solutions( cb.cut_set = pe.Constraint(pe.PositiveIntegers) variable_groups = [ - (cb.var_lower, cb.basic_lower, cb.bound_lower), + ( + cb.var_lower, + cb.basic_lower, + cb.bound_lower, + ), # (continuous, binary, constraint) (cb.var_upper, cb.basic_upper, cb.bound_upper), (cb.slack_vars, cb.basic_slack, cb.bound_slack), ] @@ -213,12 +227,15 @@ def enumerate_linear_solutions( if not quiet: # pragma: no cover print("Solving Iteration {}: ".format(solution_number), end="") + if debug: + model.pprint() if use_appsi: results = opt.solve(model) condition = results.termination_condition else: results = opt.solve(cb, tee=tee, load_solutions=False) condition = results.solver.termination_condition + if condition == optimal_tc: if use_appsi: results.solution_loader.load_vars(solution_number=0) @@ -237,6 +254,8 @@ def enumerate_linear_solutions( print( "{} = {}".format(var.name, var.lb + cb.var_lower[index].value) ) + if debug: + model.display() if hasattr(cb, "force_out"): cb.del_component("force_out") @@ -276,8 +295,14 @@ def enumerate_linear_solutions( non_zero_basic_expr += binary_var[var] basic_var = basic_last_list[idx][var] force_out_expr += basic_var + # Eqn (4): if binary choice variable is selected, then + # basic variable is zero cb.link_in_out[var] = basic_var + binary_var[var] <= 1 + # Eqn (1): at least one of the non-zero basic variables in the + # previous solution is selected cb.force_out = pe.Constraint(expr=force_out_expr >= 0) + # Eqn (2): At most (# non-zero basic variables)-1 binary choice + # variables can be selected cb.cut_set[solution_number] = non_zero_basic_expr <= num_non_zero solution_number += 1 @@ -298,6 +323,10 @@ def enumerate_linear_solutions( ).format(status.value, condition.value) ) break + if debug: + print("") + print("=" * 80) + print("") model.del_component("aos_block") From 8ac2c6e5b3994ad1588356477dc4b897a8d264ba Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 21 May 2024 05:36:20 -0600 Subject: [PATCH 1432/3044] Adding a shifted pentagonal test --- .../alternative_solutions/tests/test_cases.py | 37 +++++++++++++++++++ .../tests/test_lp_enum.py | 12 +++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index 1df3d59df43..eab6d3952af 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -274,6 +274,43 @@ def get_aos_test_knapsack( return m +def get_pentagonal_lp(): + """ + Pentagonal LP + """ + var_max = 5 + m = pe.ConcreteModel() + m.x = pe.Var(within=pe.Reals, bounds=(0, 2*var_max)) + m.y = pe.Var(within=pe.Reals, bounds=(0, 2*var_max)) + m.z = pe.Var(within=pe.NonNegativeReals, bounds=(0, 2*var_max)) + m.o = pe.Objective(expr=m.z, sense=pe.minimize) + + base_points = np.array( + [ + [var_max, 2*var_max, 0], + [2*var_max, var_max, 0], + [3.0*var_max/2.0, 0, 0], + [var_max/2.0, 0, 0], + [0, var_max, 0], + ] + ) + apex_point = np.array([var_max, var_max, var_max]) + + m.c = pe.ConstraintList() + for i in range(5): + vec_1 = base_points[i] - apex_point + vec_2 = base_points[(i + 1) % var_max] - base_points[i] + n = np.cross(vec_1, vec_2) + m.c.add( + n[0] * (m.x - apex_point[0]) + + n[1] * (m.y - apex_point[1]) + + n[2] * (m.z - apex_point[2]) + >= 0 + ) + + return m + + def get_pentagonal_pyramid_mip(): """ Pentagonal pyramid with integer coordinates in the first two dimensions and diff --git a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py index cdb633c5d1a..6dc87ea46f0 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py +++ b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py @@ -10,7 +10,7 @@ # # Find available solvers. Just use GLPK if it's available. # -solvers = list(pyomo.opt.check_available_solvers("glpk", "gurobi", "appsi_gurobi")) +solvers = list(pyomo.opt.check_available_solvers("glpk", "gurobi")) #, "appsi_gurobi")) pytestmark = pytest.mark.parametrize("mip_solver", solvers) timelimit = {"gurobi": "TimeLimit", "appsi_gurobi": "TimeLimit", "glpk": "tmlim"} @@ -67,7 +67,15 @@ def test_pentagonal_pyramid(self, mip_solver): n.x.domain = pe.Reals n.y.domain = pe.Reals - sols = lp_enum.enumerate_linear_solutions(n, solver=mip_solver) + sols = lp_enum.enumerate_linear_solutions(n, solver=mip_solver, quiet=False, debug=True) + for s in sols: + print(s) + assert len(sols) == 6 + + def test_pentagon(self, mip_solver): + n = tc.get_pentagonal_lp() + + sols = lp_enum.enumerate_linear_solutions(n, solver=mip_solver, quiet=False, debug=True) for s in sols: print(s) assert len(sols) == 6 From da840b193c3af0620bebe63e724137cf3ec5f448 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 21 May 2024 06:26:41 -0600 Subject: [PATCH 1433/3044] Reformatting with black --- .../contrib/alternative_solutions/lp_enum.py | 27 ++++++++++--------- .../alternative_solutions/tests/test_cases.py | 16 +++++------ .../tests/test_lp_enum.py | 12 ++++++--- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index 7eb16dcdacf..94a09d49bc2 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -203,7 +203,7 @@ def enumerate_linear_solutions( cb.basic_upper = pe.Var(pe.Any, domain=pe.Binary, dense=False) cb.basic_slack = pe.Var(pe.Any, domain=pe.Binary, dense=False) - # w upper bounds constraints + # w upper bounds constraints (Eqn (3)) cb.bound_lower = pe.Constraint(pe.Any) cb.bound_upper = pe.Constraint(pe.Any) cb.bound_slack = pe.Constraint(pe.Any) @@ -211,12 +211,9 @@ def enumerate_linear_solutions( # non-zero basic variable no-good cut set cb.cut_set = pe.Constraint(pe.PositiveIntegers) + # [ (continuous, binary, constraint) ] variable_groups = [ - ( - cb.var_lower, - cb.basic_lower, - cb.bound_lower, - ), # (continuous, binary, constraint) + (cb.var_lower, cb.basic_lower, cb.bound_lower), (cb.var_upper, cb.basic_upper, cb.bound_upper), (cb.slack_vars, cb.basic_slack, cb.bound_slack), ] @@ -269,6 +266,7 @@ def enumerate_linear_solutions( cb.del_component("basic_last_slack") cb.link_in_out = pe.Constraint(pe.Any) + # y variables cb.basic_last_lower = pe.Var(pe.Any, domain=pe.Binary, dense=False) cb.basic_last_upper = pe.Var(pe.Any, domain=pe.Binary, dense=False) cb.basic_last_slack = pe.Var(pe.Any, domain=pe.Binary, dense=False) @@ -286,12 +284,17 @@ def enumerate_linear_solutions( for var in continuous_var: if continuous_var[var].value > zero_threshold: num_non_zero += 1 - if var not in binary_var: - binary_var[var] - constraint[var] = ( - continuous_var[var] - <= continuous_var[var].ub * binary_var[var] - ) + # WEH - I don't think you need to add the binary variable. It + # should be automaticaly added when used. + # if var not in binary_var: + # binary_var[var] + + # Eqn (3): if binary choice variable is not selected, then + # continuous variable is zero. + constraint[var] = ( + continuous_var[var] + <= continuous_var[var].ub * binary_var[var] + ) non_zero_basic_expr += binary_var[var] basic_var = basic_last_list[idx][var] force_out_expr += basic_var diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index eab6d3952af..2fcd6da772a 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -280,18 +280,18 @@ def get_pentagonal_lp(): """ var_max = 5 m = pe.ConcreteModel() - m.x = pe.Var(within=pe.Reals, bounds=(0, 2*var_max)) - m.y = pe.Var(within=pe.Reals, bounds=(0, 2*var_max)) - m.z = pe.Var(within=pe.NonNegativeReals, bounds=(0, 2*var_max)) + m.x = pe.Var(within=pe.Reals, bounds=(0, 2 * var_max)) + m.y = pe.Var(within=pe.Reals, bounds=(0, 2 * var_max)) + m.z = pe.Var(within=pe.NonNegativeReals, bounds=(0, 2 * var_max)) m.o = pe.Objective(expr=m.z, sense=pe.minimize) base_points = np.array( [ - [var_max, 2*var_max, 0], - [2*var_max, var_max, 0], - [3.0*var_max/2.0, 0, 0], - [var_max/2.0, 0, 0], - [0, var_max, 0], + [var_max, 2 * var_max, 0], + [2 * var_max, var_max, 0], + [3.0 * var_max / 2.0, 0, 0], + [var_max / 2.0, 0, 0], + [0, var_max, 0], ] ) apex_point = np.array([var_max, var_max, var_max]) diff --git a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py index 6dc87ea46f0..d9bd7a8117e 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py +++ b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py @@ -10,7 +10,9 @@ # # Find available solvers. Just use GLPK if it's available. # -solvers = list(pyomo.opt.check_available_solvers("glpk", "gurobi")) #, "appsi_gurobi")) +solvers = list( + pyomo.opt.check_available_solvers("glpk", "gurobi") +) # , "appsi_gurobi")) pytestmark = pytest.mark.parametrize("mip_solver", solvers) timelimit = {"gurobi": "TimeLimit", "appsi_gurobi": "TimeLimit", "glpk": "tmlim"} @@ -67,7 +69,9 @@ def test_pentagonal_pyramid(self, mip_solver): n.x.domain = pe.Reals n.y.domain = pe.Reals - sols = lp_enum.enumerate_linear_solutions(n, solver=mip_solver, quiet=False, debug=True) + sols = lp_enum.enumerate_linear_solutions( + n, solver=mip_solver, quiet=False, debug=True + ) for s in sols: print(s) assert len(sols) == 6 @@ -75,7 +79,9 @@ def test_pentagonal_pyramid(self, mip_solver): def test_pentagon(self, mip_solver): n = tc.get_pentagonal_lp() - sols = lp_enum.enumerate_linear_solutions(n, solver=mip_solver, quiet=False, debug=True) + sols = lp_enum.enumerate_linear_solutions( + n, solver=mip_solver, quiet=False, debug=True + ) for s in sols: print(s) assert len(sols) == 6 From 95fa5245de86ebef4ad45f0ebff122a28ebf7f2e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 21 May 2024 09:21:07 -0600 Subject: [PATCH 1434/3044] Fixing more typos --- pyomo/repn/tests/test_parameterized_linear.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/tests/test_parameterized_linear.py b/pyomo/repn/tests/test_parameterized_linear.py index 7d99acf8bb8..4e92c5f11f2 100644 --- a/pyomo/repn/tests/test_parameterized_linear.py +++ b/pyomo/repn/tests/test_parameterized_linear.py @@ -408,7 +408,7 @@ def test_division_pseudo_constant_constant(self): assertExpressionsEqual(self, repn.constant, m.z / m.x) self.assertIsNone(repn.nonlinear) - def test_division_ANY_psuedo_constant(self): + def test_division_ANY_pseudo_constant(self): m = self.make_model() e = (m.x + 3 * m.z) / m.y @@ -424,7 +424,7 @@ def test_division_ANY_psuedo_constant(self): self.assertEqual(repn.constant, 0) self.assertIsNone(repn.nonlinear) - def test_pow_ANY_psuedo_constant(self): + def test_pow_ANY_pseudo_constant(self): m = self.make_model() e = (m.x**2 + 3 * m.z) ** m.y @@ -436,7 +436,7 @@ def test_pow_ANY_psuedo_constant(self): self.assertEqual(repn.constant, 0) assertExpressionsEqual(self, repn.nonlinear, (m.x**2 + 3 * m.z) ** m.y) - def test_pow_psuedo_constant_ANY(self): + def test_pow_pseudo_constant_ANY(self): m = self.make_model() e = m.y ** (m.x**2 + 3 * m.z) From 794a3bf2ccaa5561c8def704b6c7b1111c0746e4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 21 May 2024 10:59:12 -0600 Subject: [PATCH 1435/3044] Distributing 0 if there are nans present during finalizeResult --- pyomo/repn/parameterized_linear.py | 10 +++++----- pyomo/repn/tests/test_parameterized_linear.py | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index 8200b4cf01e..c1647e44732 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -372,15 +372,15 @@ def finalizeResult(self, result): for vid, coef in zeros: del ans.linear[vid] elif not mult: - # the multiplier has cleared out the entire expression. - # Warn if this is suppressing a NaN (unusual, and - # non-standard, but we will wait to remove this behavior - # for the time being) + # the multiplier has cleared out the entire expression. Check + # if this is suppressing a NaN because we can't clear everything + # out if it is if ans.constant != ans.constant or any( c != c for c in ans.linear.values() ): - # There's a nan in here, so we keep it + # There's a nan in here, so we distribute the 0 self._factor_multiplier_into_linear_terms(ans, mult) + return ans return self.Result() else: # mult not in {0, 1}: factor it into the constant, diff --git a/pyomo/repn/tests/test_parameterized_linear.py b/pyomo/repn/tests/test_parameterized_linear.py index 4e92c5f11f2..fd2f2aaec68 100644 --- a/pyomo/repn/tests/test_parameterized_linear.py +++ b/pyomo/repn/tests/test_parameterized_linear.py @@ -516,3 +516,23 @@ def test_0_mult_nan_param(self): self.assertIsNone(repn.nonlinear) self.assertIsInstance(repn.constant, InvalidNumber) assertExpressionsEqual(self, repn.constant.value, 0 * float('nan')) + + def test_0_mult_linear_with_nan(self): + m = self.make_model() + m.p = Param(initialize=0, mutable=True) + m.x.domain = Any + m.x.fix(float('nan')) + e = m.p * (3 * m.x * m.y + m.z) + + cfg = VisitorConfig() + repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(e) + + self.assertEqual(len(repn.linear), 2) + self.assertIn(id(m.y), repn.linear) + self.assertIsInstance(repn.linear[id(m.y)], InvalidNumber) + assertExpressionsEqual(self, repn.linear[id(m.y)].value, 0 * 3 * float('nan')) + self.assertIn(id(m.z), repn.linear) + self.assertEqual(repn.linear[id(m.z)], 0) + self.assertEqual(repn.multiplier, 1) + self.assertIsNone(repn.nonlinear) + self.assertEqual(repn.constant, 0) From 2c4a1cbd9cc309e1925a484444a468e09fd4fed6 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 21 May 2024 11:23:03 -0600 Subject: [PATCH 1436/3044] Testing duplicate in ParameterizedLinearRepn, changing the string representation to not be the same as LinearRepn --- pyomo/repn/parameterized_linear.py | 6 ++++++ pyomo/repn/tests/test_parameterized_linear.py | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index c1647e44732..eb6dd619168 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -74,6 +74,12 @@ def to_expression(visitor, arg): class ParameterizedLinearRepn(LinearRepn): + def __str__(self): + return ( + f"ParameterizedLinearRepn(mult={self.multiplier}, const={self.constant}, " + f"linear={self.linear}, nonlinear={self.nonlinear})" + ) + def walker_exitNode(self): if self.nonlinear is not None: return _GENERAL, self diff --git a/pyomo/repn/tests/test_parameterized_linear.py b/pyomo/repn/tests/test_parameterized_linear.py index fd2f2aaec68..624f8390d16 100644 --- a/pyomo/repn/tests/test_parameterized_linear.py +++ b/pyomo/repn/tests/test_parameterized_linear.py @@ -424,6 +424,20 @@ def test_division_ANY_pseudo_constant(self): self.assertEqual(repn.constant, 0) self.assertIsNone(repn.nonlinear) + def test_duplicate(self): + m = self.make_model() + e = (1 + m.x) ** 2 + m.y + + cfg = VisitorConfig() + visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]) + visitor.max_exponential_expansion = 2 + repn = visitor.walk_expression(e) + + self.assertEqual(len(repn.linear), 0) + self.assertEqual(repn.multiplier, 1) + self.assertIs(repn.constant, m.y) + assertExpressionsEqual(self, repn.nonlinear, (m.x + 1) * (m.x + 1)) + def test_pow_ANY_pseudo_constant(self): m = self.make_model() e = (m.x**2 + 3 * m.z) ** m.y From 7d4270108a46eca83d288d1d78a4593f4a8f1ad4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 21 May 2024 11:28:56 -0600 Subject: [PATCH 1437/3044] NFC: fixing some comment typos --- pyomo/repn/parameterized_linear.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index eb6dd619168..f976523a37f 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -216,7 +216,7 @@ def _before_var(visitor, child): _exit_node_handlers = copy.deepcopy(linear._exit_node_handlers) # -# NEGATION handler +# NEGATION handlers # @@ -230,7 +230,7 @@ def _handle_negation_pseudo_constant(visitor, node, arg): # -# PRODUCT handler +# PRODUCT handlers # From 71d8801f277d7b40510aeff23e0f46c3839121bd Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 21 May 2024 11:40:18 -0600 Subject: [PATCH 1438/3044] Whoops, one last class name change I missed --- pyomo/repn/parameterized_linear.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index f976523a37f..0633c285155 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -175,7 +175,7 @@ def append(self, other): self.nonlinear += nl -class MultiLevelLinearBeforeChildDispatcher(LinearBeforeChildDispatcher): +class ParameterizedLinearBeforeChildDispatcher(LinearBeforeChildDispatcher): def __init__(self): super().__init__() self[Var] = self._before_var @@ -206,13 +206,13 @@ def _before_var(visitor, child): # We aren't treating this Var as a Var for the purposes of this walker return False, (_PSEUDO_CONSTANT, child) # This is a normal situation - MultiLevelLinearBeforeChildDispatcher._record_var(visitor, child) + ParameterizedLinearBeforeChildDispatcher._record_var(visitor, child) ans = visitor.Result() ans.linear[_id] = 1 return False, (ExprType.LINEAR, ans) -_before_child_dispatcher = MultiLevelLinearBeforeChildDispatcher() +_before_child_dispatcher = ParameterizedLinearBeforeChildDispatcher() _exit_node_handlers = copy.deepcopy(linear._exit_node_handlers) # From f616aded6a98327ad057e517f6bad5cfef7bcffc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 21 May 2024 14:08:19 -0600 Subject: [PATCH 1439/3044] Remove subexpression_order from AMPLRepnVisitor --- pyomo/contrib/incidence_analysis/config.py | 2 -- pyomo/repn/plugins/nl_writer.py | 23 +++++++--------------- pyomo/repn/tests/ampl/test_nlv2.py | 2 -- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/config.py b/pyomo/contrib/incidence_analysis/config.py index 2a7734ba433..9fac48c8a26 100644 --- a/pyomo/contrib/incidence_analysis/config.py +++ b/pyomo/contrib/incidence_analysis/config.py @@ -130,7 +130,6 @@ def get_config_from_kwds(**kwds): and kwds.get("_ampl_repn_visitor", None) is None ): subexpression_cache = {} - subexpression_order = [] external_functions = {} var_map = {} used_named_expressions = set() @@ -143,7 +142,6 @@ def get_config_from_kwds(**kwds): amplvisitor = AMPLRepnVisitor( text_nl_template, subexpression_cache, - subexpression_order, external_functions, var_map, used_named_expressions, diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 331c45b78a5..04d8c42540f 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -544,7 +544,7 @@ def __init__(self, ostream, rowstream, colstream, config): else: self.template = text_nl_template self.subexpression_cache = {} - self.subexpression_order = [] + self.subexpression_order = None # set to [] later self.external_functions = {} self.used_named_expressions = set() self.var_map = {} @@ -553,7 +553,6 @@ def __init__(self, ostream, rowstream, colstream, config): self.visitor = AMPLRepnVisitor( self.template, self.subexpression_cache, - self.subexpression_order, self.external_functions, self.var_map, self.used_named_expressions, @@ -802,7 +801,7 @@ def write(self, model): # Filter out any unused named expressions self.subexpression_order = list( - filter(self.used_named_expressions.__contains__, self.subexpression_order) + filter(self.used_named_expressions.__contains__, self.subexpression_cache) ) # linear contribution by (constraint, objective, variable) component. @@ -824,10 +823,7 @@ def write(self, model): # We need to categorize the named subexpressions first so that # we know their linear / nonlinear vars when we encounter them # in constraints / objectives - self._categorize_vars( - map(self.subexpression_cache.__getitem__, self.subexpression_order), - linear_by_comp, - ) + self._categorize_vars(self.subexpression_cache.values(), linear_by_comp) n_subexpressions = self._count_subexpression_occurrences() obj_vars_linear, obj_vars_nonlinear, obj_nnz_by_var = self._categorize_vars( objectives, linear_by_comp @@ -2672,8 +2668,10 @@ def handle_named_expression_node(visitor, node, arg1): nl_info = list(expression_source) visitor.subexpression_cache[sub_id] = (sub_node, sub_repn, nl_info) # It is important that the NL subexpression comes before the - # main named expression: - visitor.subexpression_order.append(sub_id) + # main named expression: re-insert the original named + # expression (so that the nonlinear sub_node comes first + # when iterating over subexpression_cache) + visitor.subexpression_cache[_id] = visitor.subexpression_cache.pop(_id) else: nl_info = expression_source else: @@ -2716,11 +2714,6 @@ def handle_named_expression_node(visitor, node, arg1): else: return (_CONSTANT, repn.const) - # Defer recording this _id until after we know that this repn will - # not be directly substituted (and to ensure that the NL fragment is - # added to the order first). - visitor.subexpression_order.append(_id) - return (_GENERAL, repn.duplicate()) @@ -2989,7 +2982,6 @@ def __init__( self, template, subexpression_cache, - subexpression_order, external_functions, var_map, used_named_expressions, @@ -3000,7 +2992,6 @@ def __init__( super().__init__() self.template = template self.subexpression_cache = subexpression_cache - self.subexpression_order = subexpression_order self.external_functions = external_functions self.active_expression_source = None self.var_map = var_map diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 97c93ad1649..0aa9fab96f9 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -58,7 +58,6 @@ def __init__(self, symbolic=False): else: self.template = nl_writer.text_nl_template self.subexpression_cache = {} - self.subexpression_order = [] self.external_functions = {} self.var_map = {} self.used_named_expressions = set() @@ -67,7 +66,6 @@ def __init__(self, symbolic=False): self.visitor = nl_writer.AMPLRepnVisitor( self.template, self.subexpression_cache, - self.subexpression_order, self.external_functions, self.var_map, self.used_named_expressions, From b87325a7df44d222763e5ded71be0d8fede8aa9b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 21 May 2024 14:09:54 -0600 Subject: [PATCH 1440/3044] Revert some previous changes that were not necessary: because we work leaf-to-root, unnamed subexpressions will be handled correctly --- pyomo/repn/plugins/nl_writer.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 04d8c42540f..74a7251a1fe 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1644,9 +1644,6 @@ def _categorize_vars(self, comp_list, linear_by_comp): Count of the number of components that each var appears in. """ - subexpression_cache = self.subexpression_cache - used_named_expressions = self.used_named_expressions - var_map = self.var_map all_linear_vars = set() all_nonlinear_vars = set() nnz_by_var = {} @@ -1674,15 +1671,9 @@ def _categorize_vars(self, comp_list, linear_by_comp): # Process the nonlinear portion of this component if expr_info.nonlinear: nonlinear_vars = set() - _id_src = [expr_info.nonlinear[1]] - for _id in chain.from_iterable(_id_src): + for _id in expr_info.nonlinear[1]: if _id in nonlinear_vars: continue - if _id not in var_map and _id not in used_named_expressions: - _sub_info = subexpression_cache[_id][1].nonlinear - if _sub_info: - _id_src.append(_sub_info[1]) - continue if _id in linear_by_comp: nonlinear_vars.update(linear_by_comp[_id]) else: From 254ca0486965f9f02e0e6f37e342a0d56989e7a1 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 21 May 2024 14:10:27 -0600 Subject: [PATCH 1441/3044] Avoid error cor constant AMPLRepn objects --- pyomo/repn/plugins/nl_writer.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 74a7251a1fe..93f0832acf0 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1691,12 +1691,13 @@ def _categorize_vars(self, comp_list, linear_by_comp): expr_info.linear = dict.fromkeys(nonlinear_vars, 0) all_nonlinear_vars.update(nonlinear_vars) - # Update the count of components that each variable appears in - for v in expr_info.linear: - if v in nnz_by_var: - nnz_by_var[v] += 1 - else: - nnz_by_var[v] = 1 + if expr_info.linear: + # Update the count of components that each variable appears in + for v in expr_info.linear: + if v in nnz_by_var: + nnz_by_var[v] += 1 + else: + nnz_by_var[v] = 1 # Record all nonzero variable ids for this component linear_by_comp[id(comp_info[0])] = expr_info.linear # Linear models (or objectives) are common. Avoid the set From 0fecf80f6c0a6ee170413d5a41211025be39ef99 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 21 May 2024 14:10:55 -0600 Subject: [PATCH 1442/3044] Resolve potential information leak when duplicating AMPLRepn --- pyomo/repn/plugins/nl_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 93f0832acf0..b0fb7099894 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2077,7 +2077,7 @@ def duplicate(self): ans.const = self.const ans.linear = None if self.linear is None else dict(self.linear) ans.nonlinear = self.nonlinear - ans.named_exprs = self.named_exprs + ans.named_exprs = None if self.named_exprs is None else set(self.named_exprs) return ans def compile_repn(self, visitor, prefix='', args=None, named_exprs=None): From 7a9a83eb6e808a4ca437dad51ac46108114de295 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 21 May 2024 14:11:24 -0600 Subject: [PATCH 1443/3044] Additional error checking when substituting simple named expressions --- pyomo/repn/plugins/nl_writer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index b0fb7099894..5344348d0a0 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2702,7 +2702,8 @@ def handle_named_expression_node(visitor, node, arg1): if expression_source[2]: if repn.linear: - return (_MONOMIAL, next(iter(repn.linear)), 1) + assert len(repn.linear) == 1 and not repn.const + return (_MONOMIAL,) + next(iter(repn.linear.items())) else: return (_CONSTANT, repn.const) From 69b7d3011f23aedf3631e8c46a4438e97ee90cba Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 21 May 2024 14:11:56 -0600 Subject: [PATCH 1444/3044] Resolve error when collecting named expressions used in external functions --- pyomo/repn/plugins/nl_writer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 5344348d0a0..e174ce796c6 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2747,10 +2747,10 @@ def handle_external_function_node(visitor, node, *args): comment, ) arg_ids = [] + named_exprs = set() for arg in args: _id = id(arg) arg_ids.append(_id) - named_exprs = set() visitor.subexpression_cache[_id] = ( arg, AMPLRepn( @@ -2762,8 +2762,8 @@ def handle_external_function_node(visitor, node, *args): ), (None, None, True), ) - if not named_exprs: - named_exprs = None + if not named_exprs: + named_exprs = None return ( _GENERAL, AMPLRepn(0, None, (nl + '%s' * len(arg_ids), arg_ids, named_exprs)), From 0ceedc61ae67589866694e1b09527c4570196dd2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 21 May 2024 14:12:44 -0600 Subject: [PATCH 1445/3044] Track changes in AMPLRepnVisitor (handling of external functions) --- pyomo/contrib/incidence_analysis/incidence.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index 96cbf77c47d..030ee2b0f79 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -83,6 +83,17 @@ def _get_incident_via_standard_repn( def _get_incident_via_ampl_repn(expr, linear_only, visitor): + def _nonlinear_var_id_collector(idlist): + for _id in idlist: + if _id in visitor.subexpression_cache: + info = visitor.subexpression_cache[_id][1] + if info.nonlinear: + yield from _nonlinear_var_id_collector(info.nonlinear[1]) + if info.linear: + yield from _nonlinear_var_id_collector(info.linear) + else: + yield _id + var_map = visitor.var_map orig_activevisitor = AMPLRepn.ActiveVisitor AMPLRepn.ActiveVisitor = visitor @@ -91,13 +102,13 @@ def _get_incident_via_ampl_repn(expr, linear_only, visitor): finally: AMPLRepn.ActiveVisitor = orig_activevisitor - nonlinear_var_ids = [] if repn.nonlinear is None else repn.nonlinear[1] nonlinear_var_id_set = set() unique_nonlinear_var_ids = [] - for v_id in nonlinear_var_ids: - if v_id not in nonlinear_var_id_set: - nonlinear_var_id_set.add(v_id) - unique_nonlinear_var_ids.append(v_id) + if repn.nonlinear: + for v_id in _nonlinear_var_id_collector(repn.nonlinear[1]): + if v_id not in nonlinear_var_id_set: + nonlinear_var_id_set.add(v_id) + unique_nonlinear_var_ids.append(v_id) nonlinear_vars = [var_map[v_id] for v_id in unique_nonlinear_var_ids] linear_only_vars = [ From b0cbeeeb8b218b54eacce61305a49ead6a1449e5 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 21 May 2024 15:36:08 -0600 Subject: [PATCH 1446/3044] Update test_pr_and_main.yml --- .github/workflows/test_pr_and_main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 8161e6186e4..bdf1f7e1aa5 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -86,6 +86,11 @@ jobs: other: [""] category: [""] + # win/3.8 conda builds no longer work due to environment not being able + # to resolve. We are skipping it now. + exclude: + - os: windows-latest + python: 3.8 include: - os: ubuntu-latest TARGET: linux From f912d7fa06e67eab5d5c96f53a6bb6788cc82746 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 21 May 2024 19:25:57 -0600 Subject: [PATCH 1447/3044] Configure Gurobi to avoid using MIP heuristics When there is degeneracy at the root, Gurobi can return as solution that is not at a vertex. This is problematic for this algorithm. --- pyomo/contrib/alternative_solutions/lp_enum.py | 6 ++++++ pyomo/contrib/alternative_solutions/tests/test_lp_enum.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index 94a09d49bc2..671b84512be 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -155,6 +155,10 @@ def enumerate_linear_solutions( opt = pe.SolverFactory(solver) for parameter, value in solver_options.items(): opt.options[parameter] = value + if solver == "gurobi": + # Disable gurobi heuristics, which can return + # solutions not at a vertex + opt.options["Heuristics"] = 0.0 if not quiet: # pragma: no cover print("Peforming initial solve of model.") @@ -226,6 +230,8 @@ def enumerate_linear_solutions( if debug: model.pprint() + # print("Writing test{}.lp".format(solution_number)) + # cb.write("test{}.lp".format(solution_number)) if use_appsi: results = opt.solve(model) condition = results.termination_condition diff --git a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py index d9bd7a8117e..0f6991c47d4 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py +++ b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py @@ -70,7 +70,7 @@ def test_pentagonal_pyramid(self, mip_solver): n.y.domain = pe.Reals sols = lp_enum.enumerate_linear_solutions( - n, solver=mip_solver, quiet=False, debug=True + n, solver=mip_solver, quiet=True, debug=False, tee=False ) for s in sols: print(s) @@ -80,7 +80,7 @@ def test_pentagon(self, mip_solver): n = tc.get_pentagonal_lp() sols = lp_enum.enumerate_linear_solutions( - n, solver=mip_solver, quiet=False, debug=True + n, solver=mip_solver, quiet=True, debug=False ) for s in sols: print(s) From 45ddc79204d22afeaa7144f51325303e5a7fd189 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 21 May 2024 19:38:37 -0600 Subject: [PATCH 1448/3044] Fixing misspellings --- pyomo/contrib/alternative_solutions/balas.py | 2 +- pyomo/contrib/alternative_solutions/lp_enum.py | 4 ++-- .../contrib/alternative_solutions/lp_enum_solnpool.py | 6 +++--- pyomo/contrib/alternative_solutions/obbt.py | 2 +- .../contrib/alternative_solutions/tests/test_cases.py | 10 +++++----- .../alternative_solutions/tests/test_solnpool.py | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index 677f6d31137..3bd8675fca4 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -135,7 +135,7 @@ def enumerate_binary_solutions( # Initial solve of the model # if not quiet: # pragma: no cover - print("Peforming initial solve of model.") + print("Performing initial solve of model.") results = opt.solve(model, tee=tee, load_solutions=False) status = results.solver.status condition = results.solver.termination_condition diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index 671b84512be..026d26aa52a 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -161,7 +161,7 @@ def enumerate_linear_solutions( opt.options["Heuristics"] = 0.0 if not quiet: # pragma: no cover - print("Peforming initial solve of model.") + print("Performing initial solve of model.") if use_appsi: results = opt.solve(model) @@ -291,7 +291,7 @@ def enumerate_linear_solutions( if continuous_var[var].value > zero_threshold: num_non_zero += 1 # WEH - I don't think you need to add the binary variable. It - # should be automaticaly added when used. + # should be automatically added when used. # if var not in binary_var: # binary_var[var] diff --git a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py index f72ad11e9e3..1ec8f3f367b 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py @@ -75,7 +75,7 @@ def enumerate_linear_solutions_soln_pool( for parameter, value in solver_options.items(): opt.options[parameter] = value - print("Peforming initial solve of model.") + print("Performing initial solve of model.") results = opt.solve(model, tee=tee) status = results.solver.status condition = results.solver.termination_condition @@ -98,8 +98,8 @@ def enumerate_linear_solutions_soln_pool( aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap ) - cannonical_block = shifted_lp.get_shifted_linear_model(model) - cb = cannonical_block + canonical_block = shifted_lp.get_shifted_linear_model(model) + cb = canonical_block # w variables cb.basic_lower = pe.Var(cb.var_lower_index, domain=pe.Binary) diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 7b79dc9d6fc..5c611c3ab5d 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -197,7 +197,7 @@ def obbt_analysis_bounds_and_solutions( infeas_or_unbdd_tc = pe.TerminationCondition.infeasibleOrUnbounded unbdd_tc = pe.TerminationCondition.unbounded if not quiet: # pragma: no cover - print("Peforming initial solve of model.") + print("Performing initial solve of model.") if condition != optimal_tc: raise RuntimeError( diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index 2fcd6da772a..afeb5f707ac 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -11,11 +11,11 @@ """ -def _is_satified(constraint, feasability_tol=1e-6): +def _is_satisfied(constraint, feasibility_tol=1e-6): value = pe.value(constraint.body) - if constraint.has_lb() and value < constraint.lb - feasability_tol: + if constraint.has_lb() and value < constraint.lb - feasibility_tol: return False - if constraint.has_ub() and value > constraint.ub + feasability_tol: + if constraint.has_ub() and value > constraint.ub + feasibility_tol: return False return True @@ -80,7 +80,7 @@ def get_2d_diamond_problem(discrete_x=False, discrete_y=False): m.y.set_value(y_value) is_feasible = True for con in cons: - if not _is_satified(con): + if not _is_satisfied(con): is_feasible = False break if is_feasible: @@ -156,7 +156,7 @@ def get_2d_unbounded_problem(): def get_2d_degenerate_lp(): """ - Simple 2d problem that includes a redundant contraint such that three + Simple 2d problem that includes a redundant constraint such that three constraints are active at optimality. """ m = pe.ConcreteModel() diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index b4c593eb001..155d3d88762 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -13,7 +13,7 @@ class TestSolnPoolUnit(unittest.TestCase): """ Cases to cover: - LP feasability (for an LP just one solution should be returned since gurobi cannot enumerate over continuous vars) + LP feasibility (for an LP just one solution should be returned since gurobi cannot enumerate over continuous vars) Pass at least one solver option to make sure that work, e.g. time limit From 46b4f11667cc141350a7ddc3f5bc8c2f1816a4cb Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 21 May 2024 19:47:30 -0600 Subject: [PATCH 1449/3044] Remove strong dependence on gurobipy --- pyomo/contrib/alternative_solutions/solnpool.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 65743360596..6bb3bf359e6 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -9,7 +9,11 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import gurobipy +try: + import gurobipy + gurobi_available=True +except: + gurobi_available=False import pyomo.environ as pe from pyomo.contrib import appsi import pyomo.contrib.alternative_solutions.aos_utils as aos_utils @@ -61,6 +65,8 @@ def gurobi_generate_solutions( # # Setup gurobi # + if not gurobi_available: + return [] opt = appsi.solvers.Gurobi() if not opt.available(): # pragma: no cover return [] From ab58d805b1aa212e196046f9d585525be280f497 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 22 May 2024 12:41:38 -0600 Subject: [PATCH 1450/3044] First draft of 'mixed' LP dual as well as adding option for parameterized dual --- pyomo/core/plugins/transform/lp_dual.py | 126 +++++++++++++++++++++--- pyomo/core/tests/unit/test_lp_dual.py | 17 ++++ 2 files changed, 130 insertions(+), 13 deletions(-) diff --git a/pyomo/core/plugins/transform/lp_dual.py b/pyomo/core/plugins/transform/lp_dual.py index 88182e30d7f..c38c7c89f99 100644 --- a/pyomo/core/plugins/transform/lp_dual.py +++ b/pyomo/core/plugins/transform/lp_dual.py @@ -11,17 +11,64 @@ from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import ComponentMap +from pyomo.common.config import ConfigDict, ConfigValue from pyomo.common.errors import MouseTrap from pyomo.common.dependencies import scipy from pyomo.core import ( - ConcreteModel, Var, Constraint, Objective, TransformationFactory, - NonPositiveReals, maximize + ConcreteModel, + Var, + Constraint, + Objective, + TransformationFactory, + NonNegativeReals, + NonPositiveReals, + maximize, + minimize, + Reals, ) from pyomo.opt import WriterFactory + +# ESJ: TODO: copied from FME basically, should centralize. +def var_list(x): + if x.ctype is Var: + if not x.is_indexed(): + return ComponentSet([x]) + ans = ComponentSet() + for j in x.index_set(): + ans.add(x[j]) + return ans + elif hasattr(x, '__iter__'): + ans = ComponentSet() + for i in x: + ans.update(vars_to_eliminate_list(i)) + return ans + else: + raise ValueError("Expected Var or list of Vars.\n\tReceived %s" % type(x)) + + @TransformationFactory.register( - 'core.lp_dual', 'Generate the linear programming dual of the given model') + 'core.lp_dual', 'Generate the linear programming dual of the given model' +) class LinearProgrammingDual(object): + CONFIG = ConfigDict("core.lp_dual") + CONFIG.declare( + 'parameterize_wrt', + ConfigValue( + default=None, + domain=var_list, + description="Vars to treat as data for the purposes of taking the dual", + doc=""" + Optional list of Vars to be treated as data while taking the LP dual. + + For example, if this is the dual of the inner problem in a multilevel + optimization problem, then the outer problem's Vars would be specified + in this list since they are not variables from the perspective of the + inner problem. + """, + ), + ) + def apply_to(self, model, **options): raise MouseTrap( "The 'core.lp_dual' transformation does not currently implement " @@ -30,7 +77,7 @@ def apply_to(self, model, **options): "returned model." ) - def create_using(self, model, ostream=None, **options): + def create_using(self, model, ostream=None, **kwds): """Take linear programming dual of a model Returns @@ -47,19 +94,72 @@ def create_using(self, model, ostream=None, **options): and is ignored here. """ - std_form = WriterFactory('compile_standard_form').write(model, - nonnegative_vars=True) + config = self.CONFIG(kwds.pop('options', {})) + config.set_value(kwds) + + if config.parameterize_wrt is None: + return self._take_dual(model) + + return self._take_parameterized_dual(model, config.parameterize_wrt) + + def _take_dual(self, model): + std_form = WriterFactory('compile_standard_form').write( + model, mixed_form=True, set_sense=None + ) + if len(std_form.objectives) != 1: + raise ValueError( + "Model '%s' has n o objective or multiple active objectives. Cannot " + "take dual with more than one objective!" % model.name + ) + primal_sense = std_form.objectives[0].sense + dual = ConcreteModel(name="%s dual" % model.name) A_transpose = scipy.sparse.csc_matrix.transpose(std_form.A) rows = range(A_transpose.shape[0]) cols = range(A_transpose.shape[1]) - dual.x = Var(cols, domain=NonPositiveReals) + dual.x = Var(cols, domain=NonNegativeReals) + for j, (primal_cons, ineq) in enumerate(std_form.rows): + if primal_sense is minimize and ineq == 1: + dual.x[j].domain = NonPositiveReals + elif primal_sense is maximzie and ineq == -1: + dual.x[j].domain = NonPositiveReals + from pytest import set_trace + set_trace() + dual.constraints = Constraint(rows) - for i in rows: - dual.constraints[i] = sum(A_transpose[i, j]*dual.x[j] for j in cols) <= \ - std_form.c[0, i] - - dual.obj = Objective(expr=sum(std_form.rhs[j]*dual.x[j] for j in cols), - sense=maximize) + for i, primal in enumerate(std_form.columns): + if primal_sense is minimize: + if primal.domain is NonNegativeReals: + dual.constraints[i] = ( + sum(A_transpose[i, j] * dual.x[j] for j in cols) + >= std_form.c[0, i] + ) + elif primal.domain is NonPositiveReals: + dual.constraints[i] = ( + sum(A_transpose[i, j] * dual.x[j] for j in cols) + <= std_form.c[0, i] + ) + else: + if primal.domain is NonNegativeReals: + dual.constraints[i] = ( + sum(A_transpose[i, j] * dual.x[j] for j in cols) + <= std_form.c[0, i] + ) + elif primal.domain is NonPositiveReals: + dual.constraints[i] = ( + sum(A_transpose[i, j] * dual.x[j] for j in cols) + >= std_form.c[0, i] + ) + if primal.domain is Reals: + dual.constraints[i] = ( + sum(A_transpose[i, j] * dual.x[j] for j in cols) == std_form.c[0, i] + ) + + dual.obj = Objective( + expr=sum(std_form.rhs[j] * dual.x[j] for j in cols), sense=-primal_sense + ) return dual + + def _take_parameterized_dual(self, model, wrt): + pass diff --git a/pyomo/core/tests/unit/test_lp_dual.py b/pyomo/core/tests/unit/test_lp_dual.py index 487ae01d877..c3bfe74afe4 100644 --- a/pyomo/core/tests/unit/test_lp_dual.py +++ b/pyomo/core/tests/unit/test_lp_dual.py @@ -63,3 +63,20 @@ def test_lp_dual_solve(self): self.assertAlmostEqual(value(dual.x[idx]), value(m.dual[cons])) # for idx, (mult, v) in enumerate([(1, m.x), (-1, m.y), (1, m.z)]): # self.assertAlmostEqual(mult*value(v), value(dual.dual[dual_cons])) + + + def test_lp_dual(self): + m = ConcreteModel() + m.x = Var(domain=NonNegativeReals) + m.y = Var(domain=NonPositiveReals) + m.z = Var(domain=Reals) + + m.obj = Objective(expr=m.x + 2*m.y - 3*m.z) + m.c1 = Constraint(expr=-4*m.x - 2*m.y - m.z <= -5) + m.c2 = Constraint(expr=m.x + m.y >= 3) + m.c3 = Constraint(expr=- m.y - m.z == -4.2) + m.c4 = Constraint(expr=m.z <= 42) + m.dual = Suffix(direction=Suffix.IMPORT) + + lp_dual = TransformationFactory('core.lp_dual') + dual = lp_dual.create_using(m) From 91f0aaa7fe7e8e14940fddd8a037981b2245d7cc Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 22 May 2024 12:54:54 -0600 Subject: [PATCH 1451/3044] Addressing John's comments --- pyomo/repn/parameterized_linear.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index 0633c285155..892edd5643d 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -37,12 +37,12 @@ LinearRepnVisitor, ) from pyomo.repn.util import ExprType -from . import linear +import pyomo.repn.linear as linear class ParameterizedExprType(enum.IntEnum, metaclass=ExtendedEnumType): __base_enum__ = ExprType - PSEUDO_CONSTANT = 50 + PSEUDO_CONSTANT = 5 _PSEUDO_CONSTANT = ParameterizedExprType.PSEUDO_CONSTANT @@ -123,7 +123,7 @@ def append(self, other): Notes ----- This method assumes that the operator was "+". It is implemented - so that we can directly use a LinearRepn() as a `data` object in + so that we can directly use a ParameterizedLinearRepn() as a `data` object in the expression walker (thereby allowing us to use the default implementation of acceptChildResult [which calls `data.append()`] and avoid the function call for a custom @@ -131,7 +131,7 @@ def append(self, other): """ _type, other = other - if _type in (_CONSTANT, _PSEUDO_CONSTANT): + if _type is _CONSTANT or type is _PSEUDO_CONSTANT: self.constant += other return @@ -235,6 +235,8 @@ def _handle_negation_pseudo_constant(visitor, node, arg): def _handle_product_constant_constant(visitor, node, arg1, arg2): + # [ESJ 5/22/24]: Overriding this handler to exclude the deprecation path for + # 0 * nan. It doesn't need overridden when that deprecation path goes away. return _CONSTANT, arg1[1] * arg2[1] From 5aebb02fce467f10bfc4cacc653e8dbce3894cfa Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 22 May 2024 13:46:29 -0600 Subject: [PATCH 1452/3044] Whoops, bad typo --- pyomo/repn/parameterized_linear.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index 892edd5643d..ae0856cfe76 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -131,7 +131,7 @@ def append(self, other): """ _type, other = other - if _type is _CONSTANT or type is _PSEUDO_CONSTANT: + if _type is _CONSTANT or _type is _PSEUDO_CONSTANT: self.constant += other return From 6be83b57981238ba94c81e4582819e340b7fbdc6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 22 May 2024 22:00:30 -0600 Subject: [PATCH 1453/3044] NFC: clarify comment wrt string args --- pyomo/repn/plugins/nl_writer.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index e174ce796c6..fa8a49c345b 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2714,9 +2714,12 @@ def handle_external_function_node(visitor, node, *args): func = node._fcn._function # There is a special case for external functions: these are the only # expressions that can accept string arguments. As we currently pass - # these as 'precompiled' general NL fragments, the normal trap for - # constant subexpressions will miss constant external function calls - # that contain strings. We will catch that case here. + # these as 'precompiled' GENERAL AMPLRepns, the normal trap for + # constant subexpressions will miss string arguments. We will catch + # that case here by looking for NL fragments with no variable + # references. Note that the NL fragment is NOT the raw string + # argument that we want to evaluate: the raw string is in the + # `const` field. if all( arg[0] is _CONSTANT or (arg[0] is _GENERAL and arg[1].nl and not arg[1].nl[1]) for arg in args From 04313dcd8a990880287f6350e22ad033ec49850d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 22 May 2024 22:00:55 -0600 Subject: [PATCH 1454/3044] fix detection of variables replaces with expressions --- pyomo/repn/plugins/nl_writer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index fa8a49c345b..0a6f9f3da30 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1924,7 +1924,10 @@ def _linear_presolve( if not info.nonlinear: continue nl, args = info.nonlinear - if not args or any(vid not in eliminated_vars for vid in args): + if not args or any( + vid not in eliminated_vars or eliminated_vars[vid].linear + for vid in args + ): continue # Ideally, we would just evaluate the named expression. # However, there might be a linear portion of the named From c85d38cfedac6393b407abc0a160f0a6c1d648bf Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 09:10:06 -0400 Subject: [PATCH 1455/3044] Implemented most suggestions from JS. Still need to debug failing test and some string manipulation logic. --- pyomo/contrib/doe/doe.py | 49 ++++++++++++++++++------------- pyomo/contrib/doe/measurements.py | 3 ++ 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index f5c053876bd..0c37364642b 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -70,7 +70,7 @@ def __init__( solver=None, prior_FIM=None, discretize_model=None, - args={}, + args=None, logger_level=logging.INFO, ): """ @@ -103,7 +103,7 @@ def __init__( args: Additional arguments for the create_model function. logger_level: - Specify the level of the logger. Changer to logging.DEBUG for all messages. + Specify the level of the logger. Change to logging.DEBUG for all messages. """ # parameters @@ -114,6 +114,9 @@ def __init__( self.design_name = design_vars.variable_names self.design_vars = design_vars self.create_model = create_model + + if args is None: + args = {} self.args = args # create the measurement information object @@ -145,7 +148,7 @@ def _check_inputs(self): """ Check if the prior FIM is N*N matrix, where N is the number of parameter """ - if type(self.prior_FIM) != type(None): + if self.prior_FIM is not None: if np.shape(self.prior_FIM)[0] != np.shape(self.prior_FIM)[1]: raise ValueError('Found wrong prior information matrix shape.') elif np.shape(self.prior_FIM)[0] != len(self.param): @@ -980,10 +983,12 @@ def initialize_jac(m, i, j): ) if self.fim_initial is not None: - dict_fim_initialize = {} - for i, bu in enumerate(model.regression_parameters): - for j, un in enumerate(model.regression_parameters): - dict_fim_initialize[(bu, un)] = self.fim_initial[i][j] + dict_fim_initialize = { + (bu, un): self.fim_initial[i][j] + for i, bu in enumerate(model.regression_parameters) + for j, un in enumerate(model.regression_parameters) + + } def initialize_fim(m, j, d): return dict_fim_initialize[(j, d)] @@ -1005,13 +1010,12 @@ def initialize_fim(m, j, d): if self.Cholesky_option and self.objective_option == ObjectiveLib.det: # move the L matrix initial point to a dictionary - if type(self.L_initial) != type(None): - dict_cho = {} - # Loop over rows - for i, bu in enumerate(model.regression_parameters): - # Loop over columns - for j, un in enumerate(model.regression_parameters): - dict_cho[(bu, un)] = self.L_initial[i][j] + if self.L_initial is not None: + dict_cho = { + (bu, un): self.L_initial[i][j] + for i, bu in enumerate(model.regression_parameters) + for j, un in enumerate(model.regression_parameters) + } # use the L dictionary to initialize L matrix def init_cho(m, i, j): @@ -1019,7 +1023,7 @@ def init_cho(m, i, j): # Define elements of Cholesky decomposition matrix as Pyomo variables and either # Initialize with L in L_initial - if type(self.L_initial) != type(None): + if self.L_initial is not None: model.L_ele = pyo.Var( model.regression_parameters, model.regression_parameters, @@ -1070,10 +1074,11 @@ def jacobian_rule(m, p, n): # A constraint to calculate elements in Hessian matrix # transfer prior FIM to be Expressions - fim_initial_dict = {} - for i, bu in enumerate(model.regression_parameters): - for j, un in enumerate(model.regression_parameters): - fim_initial_dict[(bu, un)] = self.prior_FIM[i][j] + fim_initial_dict = { + (bu, un): self.prior_FIM[i][j] + for i, bu in enumerate(model.regression_parameters) + for j, un in enumerate(model.regression_parameters) + } def read_prior(m, i, j): return fim_initial_dict[(i, j)] @@ -1119,6 +1124,10 @@ def _add_objective(self, m): small_number = 1e-10 # Assemble the FIM matrix. This is helpful for initialization! + # + # Suggestion from JS: "It might be more efficient to form the NP array in one shot + # (from a list or using fromiter), and then reshaping to the 2-D matrix" + # fim = np.zeros((len(self.param), len(self.param))) for i, bu in enumerate(m.regression_parameters): for j, un in enumerate(m.regression_parameters): @@ -1228,7 +1237,7 @@ def det_general(m): m.Obj = pyo.Objective(expr=0) else: # something went wrong! - raise ValueError( + raise DeveloperError( "Objective option not recognized. Please contact the developers as you should not see this error." ) diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index 229fb7f7830..ff40aa6143f 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -148,6 +148,9 @@ def _generate_variable_names_with_indices( # iterate over index combinations ["CA", 1], ["CA", 2], ..., ["CC", 2], ["CC", 3] for index_instance in all_variable_indices: var_name_index_string = var_name + # + # Suggestion from JS: "Can you re-use name_repr and index_repr from pyomo.core.base.component_namer here?" + # for i, idx in enumerate(index_instance): # if i is the first index, open the [] if i == 0: From 459b593b8790945d2fd552a49f05ad20c7232d5b Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 09:52:32 -0400 Subject: [PATCH 1456/3044] Added more tests. --- pyomo/contrib/doe/measurements.py | 3 +- pyomo/contrib/doe/tests/test_fim_doe.py | 124 ++++++++++++++++++++++-- 2 files changed, 119 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index ff40aa6143f..1b47c78c65c 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -183,7 +183,8 @@ def _check_valid_input( """ Check if the measurement information provided are valid to use. """ - assert isinstance(var_name, str), "var_name should be a string." + if not isinstance(var_name, str): + raise TypeError("Variable name must be a string.") # debugging note: what is an integer versus a list versus a dictionary here? # check if time_index_position is in indices diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py index 31d250f0d10..a41ad552228 100644 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ b/pyomo/contrib/doe/tests/test_fim_doe.py @@ -38,18 +38,128 @@ class TestMeasurementError(unittest.TestCase): - def test(self): - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] + + def test_with_time_plus_one_extra_index(self): + """ This tests confirms the typical usage with a time index plus one extra index. + + This test should execute without throwing any errors. + + """ + + MeasurementVariables().add_variables( + "C", + indices={0: ['A', 'B', 'C'], 1: [0, 0.5, 1.0]}, + time_index_position=1 + ) + + def test_with_time_plus_two_extra_indices(self): + """ This tests confirms the typical usage with a time index plus two extra indices. + + This test should execute without throwing any errors. + + """ + + MeasurementVariables().add_variables( + "C", + indices={0: ['A', 'B', 'C'], # species + 1: [0, 0.5, 1.0], # time + 2: [1, 2, 3]}, # position + time_index_position=1 + ) + + def test_time_index_position_out_of_bounds(self): + """ This test confirms that an error is thrown when the time index position is out of bounds. + + """ + + # if time index is not in indices, an value error is thrown. + with self.assertRaises(ValueError): + MeasurementVariables().add_variables( + "C", + indices={0: ['CA', 'CB', 'CC'], # species + 1: [0, 0.5, 1.0],}, # time + time_index_position=2 # this is out of bounds + ) + + def test_single_measurement_variable(self): + """ This test confirms we can specify a single measurement variable without + specifying the indices. + + The test should execute with no errors. + """ + measurements = MeasurementVariables() + measurements.add_variables( + "HelloWorld", + indices=None, + time_index_position=None) + + def test_without_time_index(self): + """ This test confirms we can add a measurement variable without specifying the time index. + + The test should execute with no errors. + + """ variable_name = "C" - indices = {0: ['CA', 'CB', 'CC'], 1: t_control} + indices = {0: ['CA', 'CB', 'CC']} # specify the indices + # no time index + # measurement object measurements = MeasurementVariables() - # if time index is not in indices, an value error is thrown. - with self.assertRaises(ValueError): - measurements.add_variables( - variable_name, indices=indices, time_index_position=2 + measurements.add_variables( + variable_name, indices=indices, time_index_position=None ) + + def test_only_time_index(self): + """ This test confirms we can add a measurement variable without specifying the variable name. + + The test should execute with no errors. + + """ + + MeasurementVariables().add_variables( + "HelloWorld", # name of the variable + indices={0: [0, 0.5, 1.0]}, + time_index_position=0 + ) + + def test_with_no_measurement_name(self): + """ This test confirms that an error is thrown when None is used as the measurement name. + """ + + with self.assertRaises(TypeError): + MeasurementVariables().add_variables( + None, + indices={0: [0, 0.5, 1.0]}, + time_index_position=0 + ) + + def test_with_non_string_measurement_name(self): + """ This test confirms that an error is thrown when a non-string is used as the measurement name. + + """ + + with self.assertRaises(TypeError): + MeasurementVariables().add_variables( + 1, + indices={0: [0, 0.5, 1.0]}, + time_index_position=0 + ) + + def test_non_integer_index_keys(self): + """ This test confirms that strings can be used as keys for specifying the indices. + + Warning: it is possible this usage breaks something else in Pyomo.DoE. + There may be an implicit assumption that the order of the keys must match the order + of the indices in the Pyomo model. + + """ + + MeasurementVariables().add_variables( + "C", + indices={"species": ['CA', 'CB', 'CC'], "time": [0, 0.5, 1.0]}, + time_index_position="time" + ) class TestDesignError(unittest.TestCase): def test(self): From a1667ad79e9e12131a5fe76c49b9fa08f2fc4167 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 10:43:50 -0400 Subject: [PATCH 1457/3044] Fixed mistake in test logic. --- pyomo/contrib/doe/measurements.py | 10 +++++----- pyomo/contrib/doe/tests/test_fim_doe.py | 11 ++++------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py index 1b47c78c65c..31a9dc19dbb 100644 --- a/pyomo/contrib/doe/measurements.py +++ b/pyomo/contrib/doe/measurements.py @@ -190,13 +190,13 @@ def _check_valid_input( # check if time_index_position is in indices if ( indices is not None # ensure not None - and time_index_position is None # ensure not None + and time_index_position is not None # ensure not None and time_index_position - not in indices # ensure time_index_position is in indices + not in indices.keys() # ensure time_index_position is in indices ): raise ValueError("time index cannot be found in indices.") - # if given a list, check if bounds have the same length with flattened variable + # if given a list, check if values have the same length with flattened variable if ( values is not None # ensure not None and not type(values) @@ -212,7 +212,7 @@ def _check_valid_input( and isinstance(lower_bounds, collections.abc.Sequence) # ensure list-like and len(lower_bounds) != len_indices # ensure same length ): - raise ValueError("Lowerbounds is of different length with indices.") + raise ValueError("Lowerbounds have a different length with indices.") if ( upper_bounds is not None # ensure not None @@ -221,7 +221,7 @@ def _check_valid_input( and isinstance(upper_bounds, collections.abc.Sequence) # ensure list-like and len(upper_bounds) != len_indices # ensure same length ): - raise ValueError("Upperbounds is of different length with indices.") + raise ValueError("Upperbounds have a different length with indices.") class MeasurementVariables(VariablesWithIndices): diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py index a41ad552228..d2431f7bd45 100644 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ b/pyomo/contrib/doe/tests/test_fim_doe.py @@ -99,14 +99,11 @@ def test_without_time_index(self): The test should execute with no errors. """ - variable_name = "C" - indices = {0: ['CA', 'CB', 'CC']} # specify the indices - # no time index - # measurement object - measurements = MeasurementVariables() - measurements.add_variables( - variable_name, indices=indices, time_index_position=None + MeasurementVariables().add_variables( + "C", + indices= {0: ['CA', 'CB', 'CC']}, # species as only index + time_index_position=None # no time index ) def test_only_time_index(self): From 62af537940e163e52863e9f0954308263dbb8b7a Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 10:53:09 -0400 Subject: [PATCH 1458/3044] Implemented caching suggestion from JS; ran black. --- pyomo/contrib/doe/doe.py | 68 ++--- pyomo/contrib/doe/result.py | 174 ++++++------- pyomo/contrib/doe/scenario.py | 2 +- pyomo/contrib/doe/tests/test_example.py | 2 +- pyomo/contrib/doe/tests/test_fim_doe.py | 237 +++++++++--------- .../contrib/doe/tests/test_reactor_example.py | 12 +- 6 files changed, 245 insertions(+), 250 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 0c37364642b..9356cce360b 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -114,7 +114,7 @@ def __init__( self.design_name = design_vars.variable_names self.design_vars = design_vars self.create_model = create_model - + if args is None: args = {} self.args = args @@ -150,9 +150,9 @@ def _check_inputs(self): """ if self.prior_FIM is not None: if np.shape(self.prior_FIM)[0] != np.shape(self.prior_FIM)[1]: - raise ValueError('Found wrong prior information matrix shape.') + raise ValueError("Found wrong prior information matrix shape.") elif np.shape(self.prior_FIM)[0] != len(self.param): - raise ValueError('Found wrong prior information matrix shape.') + raise ValueError("Found wrong prior information matrix shape.") def stochastic_program( self, @@ -411,7 +411,7 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): # if measurements are provided if read_output: - with open(read_output, 'rb') as f: + with open(read_output, "rb") as f: output_record = pickle.load(f) f.close() jac = self._finite_calculation(output_record) @@ -430,7 +430,7 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): self._square_model_from_compute_FIM = mod if extract_single_model: - mod_name = store_output + '.csv' + mod_name = store_output + ".csv" dataframe = extract_single_model(mod, square_result) dataframe.to_csv(mod_name) @@ -452,10 +452,10 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): output_record[s] = output_iter - output_record['design'] = self.design_values + output_record["design"] = self.design_values if store_output: - f = open(store_output, 'wb') + f = open(store_output, "wb") pickle.dump(output_record, f) f.close() @@ -537,7 +537,7 @@ def _direct_kaug(self): dsdp_extract.append(dsdp_array[kaug_no]) except: # k_aug does not provide value for fixed variables - self.logger.debug('The variable is fixed: %s', mname) + self.logger.debug("The variable is fixed: %s", mname) # produce the sensitivity for fixed variables zero_sens = np.zeros(len(self.param)) # for fixed variables, the sensitivity are a zero vector @@ -611,7 +611,7 @@ def _create_block(self): # Determine if create_model takes theta as an optional input pass_theta_to_initialize = ( - 'theta' in inspect.getfullargspec(self.create_model).args + "theta" in inspect.getfullargspec(self.create_model).args ) # Allow user to self-define complex design variables @@ -635,11 +635,16 @@ def block_build(b, s): theta_initialize = self.scenario_data.scenario[s] # Add model on block with theta values self.create_model( - mod=b, model_option=ModelOptionLib.stage2, theta=theta_initialize, **self.args, + mod=b, + model_option=ModelOptionLib.stage2, + theta=theta_initialize, + **self.args, ) else: # Otherwise add model on block without theta values - self.create_model(mod=b, model_option=ModelOptionLib.stage2, **self.args) + self.create_model( + mod=b, model_option=ModelOptionLib.stage2, **self.args + ) # fix parameter values to perturbed values for par in self.param: @@ -831,27 +836,31 @@ def run_grid_search( # generate the design variable dictionary needed for running compute_FIM # first copy value from design_values design_iter = self.design_vars.variable_names_value.copy() + + # convert to a list and cache + list_design_set_iter = list(design_set_iter) + # update the controlled value of certain time points for certain design variables for i, names in enumerate(design_dimension_names): if isinstance(names, str): # if 'names' is simply a string, copy the new value - design_iter[names] = list(design_set_iter)[i] + design_iter[names] = list_design_set_iter[i] elif isinstance(names, collections.abc.Sequence): # if the element is a list, all design variables in this list share the same values for n in names: - design_iter[n] = list(design_set_iter)[i] + design_iter[n] = list_design_set_iter[i] else: # otherwise just copy the value # design_iter[names] = list(design_set_iter)[i] raise NotImplementedError( - 'You should not see this error message. Please report it to the Pyomo.DoE developers.' + "You should not see this error message. Please report it to the Pyomo.DoE developers." ) self.design_vars.variable_names_value = design_iter iter_timer = TicTocTimer() - self.logger.info('=======Iteration Number: %s =====', count + 1) + self.logger.info("=======Iteration Number: %s =====", count + 1) self.logger.debug( - 'Design variable values of this iteration: %s', design_iter + "Design variable values of this iteration: %s", design_iter ) iter_timer.tic(msg=None) # generate store name @@ -887,12 +896,12 @@ def run_grid_search( time_set.append(iter_t) # give run information at each iteration - self.logger.info('This is run %s out of %s.', count, total_count) + self.logger.info("This is run %s out of %s.", count, total_count) self.logger.info( - 'The code has run %s seconds.', round(sum(time_set), 2) + "The code has run %s seconds.", round(sum(time_set), 2) ) self.logger.info( - 'Estimated remaining time: %s seconds', + "Estimated remaining time: %s seconds", round( sum(time_set) / (count) * (total_count - count), 2 ), # need to check this math... it gives a negative number for the final count @@ -907,11 +916,11 @@ def run_grid_search( except: self.logger.warning( - ':::::::::::Warning: Cannot converge this run.::::::::::::' + ":::::::::::Warning: Cannot converge this run.::::::::::::" ) count += 1 failed_count += 1 - self.logger.warning('failed count:', failed_count) + self.logger.warning("failed count:", failed_count) result_combine[tuple(design_set_iter)] = None # For user's access @@ -925,7 +934,7 @@ def run_grid_search( store_optimality_name=store_optimality_as_csv, ) - self.logger.info('Overall wall clock time [s]: %s', sum(time_set)) + self.logger.info("Overall wall clock time [s]: %s", sum(time_set)) return figure_draw_object @@ -987,7 +996,6 @@ def initialize_jac(m, i, j): (bu, un): self.fim_initial[i][j] for i, bu in enumerate(model.regression_parameters) for j, un in enumerate(model.regression_parameters) - } def initialize_fim(m, j, d): @@ -1012,7 +1020,7 @@ def initialize_fim(m, j, d): # move the L matrix initial point to a dictionary if self.L_initial is not None: dict_cho = { - (bu, un): self.L_initial[i][j] + (bu, un): self.L_initial[i][j] for i, bu in enumerate(model.regression_parameters) for j, un in enumerate(model.regression_parameters) } @@ -1124,8 +1132,8 @@ def _add_objective(self, m): small_number = 1e-10 # Assemble the FIM matrix. This is helpful for initialization! - # - # Suggestion from JS: "It might be more efficient to form the NP array in one shot + # + # Suggestion from JS: "It might be more efficient to form the NP array in one shot # (from a list or using fromiter), and then reshaping to the 2-D matrix" # fim = np.zeros((len(self.param), len(self.param))) @@ -1279,10 +1287,10 @@ def _fix_design(self, m, design_val, fix_opt=True, optimize_option=None): def _get_default_ipopt_solver(self): """Default solver""" - solver = SolverFactory('ipopt') - solver.options['linear_solver'] = 'ma57' - solver.options['halt_on_ampl_error'] = 'yes' - solver.options['max_iter'] = 3000 + solver = SolverFactory("ipopt") + solver.options["linear_solver"] = "ma57" + solver.options["halt_on_ampl_error"] = "yes" + solver.options["max_iter"] = 3000 return solver def _solve_doe(self, m, fix=False, opt_option=None): diff --git a/pyomo/contrib/doe/result.py b/pyomo/contrib/doe/result.py index d8ed343352a..f7145ae2a46 100644 --- a/pyomo/contrib/doe/result.py +++ b/pyomo/contrib/doe/result.py @@ -123,9 +123,9 @@ def result_analysis(self, result=None): if self.prior_FIM is not None: try: fim = fim + self.prior_FIM - self.logger.info('Existed information has been added.') + self.logger.info("Existed information has been added.") except: - raise ValueError('Check the shape of prior FIM.') + raise ValueError("Check the shape of prior FIM.") if np.linalg.cond(fim) > self.max_condition_number: self.logger.info( @@ -133,7 +133,7 @@ def result_analysis(self, result=None): np.linalg.cond(fim), ) self.logger.info( - 'A condition number bigger than %s is considered near singular.', + "A condition number bigger than %s is considered near singular.", self.max_condition_number, ) @@ -239,10 +239,10 @@ def _print_FIM_info(self, FIM): self.eig_vecs = np.linalg.eig(FIM)[1] self.logger.info( - 'FIM: %s; \n Trace: %s; \n Determinant: %s;', self.FIM, self.trace, self.det + "FIM: %s; \n Trace: %s; \n Determinant: %s;", self.FIM, self.trace, self.det ) self.logger.info( - 'Condition number: %s; \n Min eigenvalue: %s.', self.cond, self.min_eig + "Condition number: %s; \n Min eigenvalue: %s.", self.cond, self.min_eig ) def _solution_info(self, m, dv_set): @@ -268,11 +268,11 @@ def _solution_info(self, m, dv_set): # When scaled with constant values, the effect of the scaling factors are removed here # For determinant, the scaling factor to determinant is scaling factor ** (Dim of FIM) # For trace, the scaling factor to trace is the scaling factor. - if self.obj == 'det': + if self.obj == "det": self.obj_det = np.exp(value(m.obj)) / (self.fim_scale_constant_value) ** ( len(self.parameter_names) ) - elif self.obj == 'trace': + elif self.obj == "trace": self.obj_trace = np.exp(value(m.obj)) / (self.fim_scale_constant_value) design_variable_names = list(dv_set.keys()) @@ -314,11 +314,11 @@ def _get_solver_info(self): if (self.result.solver.status == SolverStatus.ok) and ( self.result.solver.termination_condition == TerminationCondition.optimal ): - self.status = 'converged' + self.status = "converged" elif ( self.result.solver.termination_condition == TerminationCondition.infeasible ): - self.status = 'infeasible' + self.status = "infeasible" else: self.status = self.result.solver.status @@ -399,10 +399,10 @@ def extract_criteria(self): column_names.append(i) # Each design criteria has a column to store values - column_names.append('A') - column_names.append('D') - column_names.append('E') - column_names.append('ME') + column_names.append("A") + column_names.append("D") + column_names.append("E") + column_names.append("ME") # generate the dataframe store_all_results = np.asarray(store_all_results) self.store_all_results_dataframe = pd.DataFrame( @@ -458,7 +458,7 @@ def figure_drawing( self.design_names ): raise ValueError( - 'Error: All dimensions except for those the figures are drawn by should be fixed.' + "Error: All dimensions except for those the figures are drawn by should be fixed." ) if len(self.sensitivity_dimension) not in [1, 2]: @@ -467,15 +467,15 @@ def figure_drawing( # generate a combination of logic sentences to filter the results of the DOF needed. # an example filter: (self.store_all_results_dataframe["CA0"]==5). if len(self.fixed_design_names) != 0: - filter = '' + filter = "" for i in range(len(self.fixed_design_names)): - filter += '(self.store_all_results_dataframe[' + filter += "(self.store_all_results_dataframe[" filter += str(self.fixed_design_names[i]) - filter += ']==' + filter += "]==" filter += str(self.fixed_design_values[i]) - filter += ')' + filter += ")" if i != (len(self.fixed_design_names) - 1): - filter += '&' + filter += "&" # extract results with other dimensions fixed figure_result_data = self.store_all_results_dataframe.loc[eval(filter)] # if there is no other fixed dimensions @@ -526,78 +526,78 @@ def _curve1D( # decide if the results are log scaled if log_scale: - y_range_A = np.log10(self.figure_result_data['A'].values.tolist()) - y_range_D = np.log10(self.figure_result_data['D'].values.tolist()) - y_range_E = np.log10(self.figure_result_data['E'].values.tolist()) - y_range_ME = np.log10(self.figure_result_data['ME'].values.tolist()) + y_range_A = np.log10(self.figure_result_data["A"].values.tolist()) + y_range_D = np.log10(self.figure_result_data["D"].values.tolist()) + y_range_E = np.log10(self.figure_result_data["E"].values.tolist()) + y_range_ME = np.log10(self.figure_result_data["ME"].values.tolist()) else: - y_range_A = self.figure_result_data['A'].values.tolist() - y_range_D = self.figure_result_data['D'].values.tolist() - y_range_E = self.figure_result_data['E'].values.tolist() - y_range_ME = self.figure_result_data['ME'].values.tolist() + y_range_A = self.figure_result_data["A"].values.tolist() + y_range_D = self.figure_result_data["D"].values.tolist() + y_range_E = self.figure_result_data["E"].values.tolist() + y_range_ME = self.figure_result_data["ME"].values.tolist() # Draw A-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} # plt.rcParams.update(params) ax.plot(x_range, y_range_A) ax.scatter(x_range, y_range_A) - ax.set_ylabel('$log_{10}$ Trace') + ax.set_ylabel("$log_{10}$ Trace") ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ': A-optimality') + plt.pyplot.title(title_text + ": A-optimality") plt.pyplot.show() # Draw D-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} # plt.rcParams.update(params) ax.plot(x_range, y_range_D) ax.scatter(x_range, y_range_D) - ax.set_ylabel('$log_{10}$ Determinant') + ax.set_ylabel("$log_{10}$ Determinant") ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ': D-optimality') + plt.pyplot.title(title_text + ": D-optimality") plt.pyplot.show() # Draw E-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} # plt.rcParams.update(params) ax.plot(x_range, y_range_E) ax.scatter(x_range, y_range_E) - ax.set_ylabel('$log_{10}$ Minimal eigenvalue') + ax.set_ylabel("$log_{10}$ Minimal eigenvalue") ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ': E-optimality') + plt.pyplot.title(title_text + ": E-optimality") plt.pyplot.show() # Draw Modified E-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} # plt.rcParams.update(params) ax.plot(x_range, y_range_ME) ax.scatter(x_range, y_range_ME) - ax.set_ylabel('$log_{10}$ Condition number') + ax.set_ylabel("$log_{10}$ Condition number") ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ': Modified E-optimality') + plt.pyplot.title(title_text + ": Modified E-optimality") plt.pyplot.show() def _heatmap( @@ -641,10 +641,10 @@ def _heatmap( y_range = sensitivity_dict[self.sensitivity_dimension[1]] # extract the design criteria values - A_range = self.figure_result_data['A'].values.tolist() - D_range = self.figure_result_data['D'].values.tolist() - E_range = self.figure_result_data['E'].values.tolist() - ME_range = self.figure_result_data['ME'].values.tolist() + A_range = self.figure_result_data["A"].values.tolist() + D_range = self.figure_result_data["D"].values.tolist() + E_range = self.figure_result_data["E"].values.tolist() + ME_range = self.figure_result_data["ME"].values.tolist() # reshape the design criteria values for heatmaps cri_a = np.asarray(A_range).reshape(len(x_range), len(y_range)) @@ -675,12 +675,12 @@ def _heatmap( # A-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} plt.pyplot.rcParams.update(params) ax.set_yticks(range(len(yLabel))) ax.set_yticklabels(yLabel) @@ -690,18 +690,18 @@ def _heatmap( ax.set_xlabel(xlabel_text) im = ax.imshow(hes_a.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) - ba.set_label('log10(trace(FIM))') - plt.pyplot.title(title_text + ': A-optimality') + ba.set_label("log10(trace(FIM))") + plt.pyplot.title(title_text + ": A-optimality") plt.pyplot.show() # D-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} plt.pyplot.rcParams.update(params) ax.set_yticks(range(len(yLabel))) ax.set_yticklabels(yLabel) @@ -711,18 +711,18 @@ def _heatmap( ax.set_xlabel(xlabel_text) im = ax.imshow(hes_d.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) - ba.set_label('log10(det(FIM))') - plt.pyplot.title(title_text + ': D-optimality') + ba.set_label("log10(det(FIM))") + plt.pyplot.title(title_text + ": D-optimality") plt.pyplot.show() # E-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} plt.pyplot.rcParams.update(params) ax.set_yticks(range(len(yLabel))) ax.set_yticklabels(yLabel) @@ -732,18 +732,18 @@ def _heatmap( ax.set_xlabel(xlabel_text) im = ax.imshow(hes_e.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) - ba.set_label('log10(minimal eig(FIM))') - plt.pyplot.title(title_text + ': E-optimality') + ba.set_label("log10(minimal eig(FIM))") + plt.pyplot.title(title_text + ": E-optimality") plt.pyplot.show() # modified E-optimality fig = plt.pyplot.figure() - plt.pyplot.rc('axes', titlesize=font_axes) - plt.pyplot.rc('axes', labelsize=font_axes) - plt.pyplot.rc('xtick', labelsize=font_tick) - plt.pyplot.rc('ytick', labelsize=font_tick) + plt.pyplot.rc("axes", titlesize=font_axes) + plt.pyplot.rc("axes", labelsize=font_axes) + plt.pyplot.rc("xtick", labelsize=font_tick) + plt.pyplot.rc("ytick", labelsize=font_tick) ax = fig.add_subplot(111) - params = {'mathtext.default': 'regular'} + params = {"mathtext.default": "regular"} plt.pyplot.rcParams.update(params) ax.set_yticks(range(len(yLabel))) ax.set_yticklabels(yLabel) @@ -753,6 +753,6 @@ def _heatmap( ax.set_xlabel(xlabel_text) im = ax.imshow(hes_e2.T, cmap=plt.pyplot.cm.hot_r) ba = plt.pyplot.colorbar(im) - ba.set_label('log10(cond(FIM))') - plt.pyplot.title(title_text + ': Modified E-optimality') + ba.set_label("log10(cond(FIM))") + plt.pyplot.title(title_text + ": Modified E-optimality") plt.pyplot.show() diff --git a/pyomo/contrib/doe/scenario.py b/pyomo/contrib/doe/scenario.py index 6c6f5ef7d1b..b44ce1ab4d3 100644 --- a/pyomo/contrib/doe/scenario.py +++ b/pyomo/contrib/doe/scenario.py @@ -150,5 +150,5 @@ def generate_scenario(self): # store scenario if self.store: - with open('scenario_simultaneous.pickle', 'wb') as f: + with open("scenario_simultaneous.pickle", "wb") as f: pickle.dump(self.scenario_data, f) diff --git a/pyomo/contrib/doe/tests/test_example.py b/pyomo/contrib/doe/tests/test_example.py index b59014a8110..8153e07018a 100644 --- a/pyomo/contrib/doe/tests/test_example.py +++ b/pyomo/contrib/doe/tests/test_example.py @@ -38,7 +38,7 @@ from pyomo.opt import SolverFactory -ipopt_available = SolverFactory('ipopt').available() +ipopt_available = SolverFactory("ipopt").available() class TestReactorExample(unittest.TestCase): diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py index d2431f7bd45..05664b0a795 100644 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ b/pyomo/contrib/doe/tests/test_fim_doe.py @@ -40,124 +40,111 @@ class TestMeasurementError(unittest.TestCase): def test_with_time_plus_one_extra_index(self): - """ This tests confirms the typical usage with a time index plus one extra index. + """This tests confirms the typical usage with a time index plus one extra index. This test should execute without throwing any errors. - + """ MeasurementVariables().add_variables( - "C", - indices={0: ['A', 'B', 'C'], 1: [0, 0.5, 1.0]}, - time_index_position=1 + "C", indices={0: ["A", "B", "C"], 1: [0, 0.5, 1.0]}, time_index_position=1 ) def test_with_time_plus_two_extra_indices(self): - """ This tests confirms the typical usage with a time index plus two extra indices. + """This tests confirms the typical usage with a time index plus two extra indices. This test should execute without throwing any errors. - + """ MeasurementVariables().add_variables( "C", - indices={0: ['A', 'B', 'C'], # species - 1: [0, 0.5, 1.0], # time - 2: [1, 2, 3]}, # position - time_index_position=1 + indices={ + 0: ["A", "B", "C"], # species + 1: [0, 0.5, 1.0], # time + 2: [1, 2, 3], + }, # position + time_index_position=1, ) def test_time_index_position_out_of_bounds(self): - """ This test confirms that an error is thrown when the time index position is out of bounds. - - """ + """This test confirms that an error is thrown when the time index position is out of bounds.""" # if time index is not in indices, an value error is thrown. with self.assertRaises(ValueError): MeasurementVariables().add_variables( - "C", - indices={0: ['CA', 'CB', 'CC'], # species - 1: [0, 0.5, 1.0],}, # time - time_index_position=2 # this is out of bounds + "C", + indices={0: ["CA", "CB", "CC"], 1: [0, 0.5, 1.0]}, # species # time + time_index_position=2, # this is out of bounds ) def test_single_measurement_variable(self): - """ This test confirms we can specify a single measurement variable without + """This test confirms we can specify a single measurement variable without specifying the indices. The test should execute with no errors. """ measurements = MeasurementVariables() - measurements.add_variables( - "HelloWorld", - indices=None, - time_index_position=None) + measurements.add_variables("HelloWorld", indices=None, time_index_position=None) def test_without_time_index(self): - """ This test confirms we can add a measurement variable without specifying the time index. + """This test confirms we can add a measurement variable without specifying the time index. The test should execute with no errors. """ MeasurementVariables().add_variables( - "C", - indices= {0: ['CA', 'CB', 'CC']}, # species as only index - time_index_position=None # no time index - ) - + "C", + indices={0: ["CA", "CB", "CC"]}, # species as only index + time_index_position=None, # no time index + ) + def test_only_time_index(self): - """ This test confirms we can add a measurement variable without specifying the variable name. + """This test confirms we can add a measurement variable without specifying the variable name. The test should execute with no errors. """ MeasurementVariables().add_variables( - "HelloWorld", # name of the variable - indices={0: [0, 0.5, 1.0]}, - time_index_position=0 + "HelloWorld", # name of the variable + indices={0: [0, 0.5, 1.0]}, + time_index_position=0, ) def test_with_no_measurement_name(self): - """ This test confirms that an error is thrown when None is used as the measurement name. - - """ + """This test confirms that an error is thrown when None is used as the measurement name.""" with self.assertRaises(TypeError): MeasurementVariables().add_variables( - None, - indices={0: [0, 0.5, 1.0]}, - time_index_position=0 + None, indices={0: [0, 0.5, 1.0]}, time_index_position=0 ) - - def test_with_non_string_measurement_name(self): - """ This test confirms that an error is thrown when a non-string is used as the measurement name. - """ + def test_with_non_string_measurement_name(self): + """This test confirms that an error is thrown when a non-string is used as the measurement name.""" with self.assertRaises(TypeError): MeasurementVariables().add_variables( - 1, - indices={0: [0, 0.5, 1.0]}, - time_index_position=0 + 1, indices={0: [0, 0.5, 1.0]}, time_index_position=0 ) def test_non_integer_index_keys(self): - """ This test confirms that strings can be used as keys for specifying the indices. + """This test confirms that strings can be used as keys for specifying the indices. Warning: it is possible this usage breaks something else in Pyomo.DoE. - There may be an implicit assumption that the order of the keys must match the order + There may be an implicit assumption that the order of the keys must match the order of the indices in the Pyomo model. """ MeasurementVariables().add_variables( "C", - indices={"species": ['CA', 'CB', 'CC'], "time": [0, 0.5, 1.0]}, - time_index_position="time" + indices={"species": ["CA", "CB", "CC"], "time": [0, 0.5, 1.0]}, + time_index_position="time", ) + class TestDesignError(unittest.TestCase): def test(self): t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] @@ -165,7 +152,7 @@ def test(self): exp_design = DesignVariables() # add T as design variable - var_T = 'T' + var_T = "T" indices_T = {0: t_control} exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] @@ -201,7 +188,7 @@ def test(self): t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] # measurement object variable_name = "C" - indices = {0: ['CA', 'CB', 'CC'], 1: t_control} + indices = {0: ["CA", "CB", "CC"], 1: t_control} measurements = MeasurementVariables() measurements.add_variables( @@ -212,7 +199,7 @@ def test(self): exp_design = DesignVariables() # add CAO as design variable - var_C = 'CA0' + var_C = "CA0" indices_C = {0: [0]} exp1_C = [5] exp_design.add_variables( @@ -225,7 +212,7 @@ def test(self): ) # add T as design variable - var_T = 'T' + var_T = "T" indices_T = {0: t_control} exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] @@ -272,7 +259,7 @@ def test_setup(self): # add variable C variable_name = "C" - indices = {0: ['CA', 'CB', 'CC'], 1: t_control} + indices = {0: ["CA", "CB", "CC"], 1: t_control} measurements.add_variables( variable_name, indices=indices, time_index_position=1 ) @@ -285,36 +272,36 @@ def test_setup(self): ) # check variable names - self.assertEqual(measurements.variable_names[0], 'C[CA,0]') - self.assertEqual(measurements.variable_names[1], 'C[CA,0.125]') - self.assertEqual(measurements.variable_names[-1], 'T[5,0.8]') - self.assertEqual(measurements.variable_names[-2], 'T[5,0.6]') - self.assertEqual(measurements.variance['T[5,0.4]'], 10) - self.assertEqual(measurements.variance['T[5,0.6]'], 10) - self.assertEqual(measurements.variance['T[5,0.4]'], 10) - self.assertEqual(measurements.variance['T[5,0.6]'], 10) + self.assertEqual(measurements.variable_names[0], "C[CA,0]") + self.assertEqual(measurements.variable_names[1], "C[CA,0.125]") + self.assertEqual(measurements.variable_names[-1], "T[5,0.8]") + self.assertEqual(measurements.variable_names[-2], "T[5,0.6]") + self.assertEqual(measurements.variance["T[5,0.4]"], 10) + self.assertEqual(measurements.variance["T[5,0.6]"], 10) + self.assertEqual(measurements.variance["T[5,0.4]"], 10) + self.assertEqual(measurements.variance["T[5,0.6]"], 10) ### specify function var_names = [ - 'C[CA,0]', - 'C[CA,0.125]', - 'C[CA,0.875]', - 'C[CA,1]', - 'C[CB,0]', - 'C[CB,0.125]', - 'C[CB,0.25]', - 'C[CB,0.375]', - 'C[CC,0]', - 'C[CC,0.125]', - 'C[CC,0.25]', - 'C[CC,0.375]', + "C[CA,0]", + "C[CA,0.125]", + "C[CA,0.875]", + "C[CA,1]", + "C[CB,0]", + "C[CB,0.125]", + "C[CB,0.25]", + "C[CB,0.375]", + "C[CC,0]", + "C[CC,0.125]", + "C[CC,0.25]", + "C[CC,0.375]", ] measurements2 = MeasurementVariables() measurements2.set_variable_name_list(var_names) - self.assertEqual(measurements2.variable_names[1], 'C[CA,0.125]') - self.assertEqual(measurements2.variable_names[-1], 'C[CC,0.375]') + self.assertEqual(measurements2.variable_names[1], "C[CA,0.125]") + self.assertEqual(measurements2.variable_names[-1], "C[CC,0.375]") ### check_subset function self.assertTrue(measurements.check_subset(measurements2)) @@ -330,7 +317,7 @@ def test_setup(self): exp_design = DesignVariables() # add CAO as design variable - var_C = 'CA0' + var_C = "CA0" indices_C = {0: [0]} exp1_C = [5] exp_design.add_variables( @@ -343,7 +330,7 @@ def test_setup(self): ) # add T as design variable - var_T = 'T' + var_T = "T" indices_T = {0: t_control} exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] @@ -359,31 +346,31 @@ def test_setup(self): self.assertEqual( exp_design.variable_names, [ - 'CA0[0]', - 'T[0]', - 'T[0.125]', - 'T[0.25]', - 'T[0.375]', - 'T[0.5]', - 'T[0.625]', - 'T[0.75]', - 'T[0.875]', - 'T[1]', + "CA0[0]", + "T[0]", + "T[0.125]", + "T[0.25]", + "T[0.375]", + "T[0.5]", + "T[0.625]", + "T[0.75]", + "T[0.875]", + "T[1]", ], ) - self.assertEqual(exp_design.variable_names_value['CA0[0]'], 5) - self.assertEqual(exp_design.variable_names_value['T[0]'], 470) - self.assertEqual(exp_design.upper_bounds['CA0[0]'], 5) - self.assertEqual(exp_design.upper_bounds['T[0]'], 700) - self.assertEqual(exp_design.lower_bounds['CA0[0]'], 1) - self.assertEqual(exp_design.lower_bounds['T[0]'], 300) + self.assertEqual(exp_design.variable_names_value["CA0[0]"], 5) + self.assertEqual(exp_design.variable_names_value["T[0]"], 470) + self.assertEqual(exp_design.upper_bounds["CA0[0]"], 5) + self.assertEqual(exp_design.upper_bounds["T[0]"], 700) + self.assertEqual(exp_design.lower_bounds["CA0[0]"], 1) + self.assertEqual(exp_design.lower_bounds["T[0]"], 300) design_names = exp_design.variable_names exp1 = [4, 600, 300, 300, 300, 300, 300, 300, 300, 300] exp1_design_dict = dict(zip(design_names, exp1)) exp_design.update_values(exp1_design_dict) - self.assertEqual(exp_design.variable_names_value['CA0[0]'], 4) - self.assertEqual(exp_design.variable_names_value['T[0]'], 600) + self.assertEqual(exp_design.variable_names_value["CA0[0]"], 4) + self.assertEqual(exp_design.variable_names_value["T[0]"], 600) class TestParameter(unittest.TestCase): @@ -391,19 +378,19 @@ class TestParameter(unittest.TestCase): def test_setup(self): # set up parameter class - param_dict = {'A1': 84.79, 'A2': 371.72, 'E1': 7.78, 'E2': 15.05} + param_dict = {"A1": 84.79, "A2": 371.72, "E1": 7.78, "E2": 15.05} scenario_gene = ScenarioGenerator(param_dict, formula="central", step=0.1) parameter_set = scenario_gene.ScenarioData - self.assertAlmostEqual(parameter_set.eps_abs['A1'], 16.9582, places=1) - self.assertAlmostEqual(parameter_set.eps_abs['E1'], 1.5554, places=1) - self.assertEqual(parameter_set.scena_num['A2'], [2, 3]) - self.assertEqual(parameter_set.scena_num['E1'], [4, 5]) - self.assertAlmostEqual(parameter_set.scenario[0]['A1'], 93.2699, places=1) - self.assertAlmostEqual(parameter_set.scenario[2]['A2'], 408.8895, places=1) - self.assertAlmostEqual(parameter_set.scenario[-1]['E2'], 13.54, places=1) - self.assertAlmostEqual(parameter_set.scenario[-2]['E2'], 16.55, places=1) + self.assertAlmostEqual(parameter_set.eps_abs["A1"], 16.9582, places=1) + self.assertAlmostEqual(parameter_set.eps_abs["E1"], 1.5554, places=1) + self.assertEqual(parameter_set.scena_num["A2"], [2, 3]) + self.assertEqual(parameter_set.scena_num["E1"], [4, 5]) + self.assertAlmostEqual(parameter_set.scenario[0]["A1"], 93.2699, places=1) + self.assertAlmostEqual(parameter_set.scenario[2]["A2"], 408.8895, places=1) + self.assertAlmostEqual(parameter_set.scenario[-1]["E2"], 13.54, places=1) + self.assertAlmostEqual(parameter_set.scenario[-2]["E2"], 16.55, places=1) class TestVariablesWithIndices(unittest.TestCase): @@ -414,7 +401,7 @@ def test_setup(self): t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] ### add_element function # add CAO as design variable - var_C = 'CA0' + var_C = "CA0" indices_C = {0: [0]} exp1_C = [5] special.add_variables( @@ -427,7 +414,7 @@ def test_setup(self): ) # add T as design variable - var_T = 'T' + var_T = "T" indices_T = {0: t_control} exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] @@ -443,25 +430,25 @@ def test_setup(self): self.assertEqual( special.variable_names, [ - 'CA0[0]', - 'T[0]', - 'T[0.125]', - 'T[0.25]', - 'T[0.375]', - 'T[0.5]', - 'T[0.625]', - 'T[0.75]', - 'T[0.875]', - 'T[1]', + "CA0[0]", + "T[0]", + "T[0.125]", + "T[0.25]", + "T[0.375]", + "T[0.5]", + "T[0.625]", + "T[0.75]", + "T[0.875]", + "T[1]", ], ) - self.assertEqual(special.variable_names_value['CA0[0]'], 5) - self.assertEqual(special.variable_names_value['T[0]'], 470) - self.assertEqual(special.upper_bounds['CA0[0]'], 5) - self.assertEqual(special.upper_bounds['T[0]'], 700) - self.assertEqual(special.lower_bounds['CA0[0]'], 1) - self.assertEqual(special.lower_bounds['T[0]'], 300) + self.assertEqual(special.variable_names_value["CA0[0]"], 5) + self.assertEqual(special.variable_names_value["T[0]"], 470) + self.assertEqual(special.upper_bounds["CA0[0]"], 5) + self.assertEqual(special.upper_bounds["T[0]"], 700) + self.assertEqual(special.lower_bounds["CA0[0]"], 1) + self.assertEqual(special.lower_bounds["T[0]"], 300) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/doe/tests/test_reactor_example.py b/pyomo/contrib/doe/tests/test_reactor_example.py index daf2ee89194..3fca93b5ded 100644 --- a/pyomo/contrib/doe/tests/test_reactor_example.py +++ b/pyomo/contrib/doe/tests/test_reactor_example.py @@ -34,7 +34,7 @@ from pyomo.contrib.doe.examples.reactor_kinetics import create_model, disc_for_measure from pyomo.opt import SolverFactory -ipopt_available = SolverFactory('ipopt').available() +ipopt_available = SolverFactory("ipopt").available() class Test_example_options(unittest.TestCase): @@ -70,11 +70,11 @@ def test_setUP(self): # Control time set [h] t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] # Define parameter nominal value - parameter_dict = {'A1': 84.79, 'A2': 371.72, 'E1': 7.78, 'E2': 15.05} + parameter_dict = {"A1": 84.79, "A2": 371.72, "E1": 7.78, "E2": 15.05} # measurement object variable_name = "C" - indices = {0: ['CA', 'CB', 'CC'], 1: t_control} + indices = {0: ["CA", "CB", "CC"], 1: t_control} measurements = MeasurementVariables() measurements.add_variables( @@ -85,7 +85,7 @@ def test_setUP(self): exp_design = DesignVariables() # add CAO as design variable - var_C = 'CA0' + var_C = "CA0" indices_C = {0: [0]} exp1_C = [5] exp_design.add_variables( @@ -98,7 +98,7 @@ def test_setUP(self): ) # add T as design variable - var_T = 'T' + var_T = "T" indices_T = {0: t_control} exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] @@ -216,5 +216,5 @@ def test_setUP(self): self.assertAlmostEqual(value(optimize_result.model.T[0.5]), 300, places=2) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 35edf83785e4db3071080903b92ccd6c809c32bd Mon Sep 17 00:00:00 2001 From: Eslick Date: Thu, 23 May 2024 14:33:53 -0400 Subject: [PATCH 1459/3044] Change the way models in main are handeled --- pyomo/contrib/viewer/model_select.py | 14 ++++++----- pyomo/contrib/viewer/pyomo_viewer.py | 2 +- pyomo/contrib/viewer/tests/test_qt.py | 16 ++++++------ pyomo/contrib/viewer/ui.py | 35 ++++++++++++++++++++++++--- pyomo/contrib/viewer/ui_data.py | 13 +++++++++- 5 files changed, 61 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/viewer/model_select.py b/pyomo/contrib/viewer/model_select.py index e9c82740708..1e65e91a089 100644 --- a/pyomo/contrib/viewer/model_select.py +++ b/pyomo/contrib/viewer/model_select.py @@ -60,31 +60,33 @@ def select_model(self): items = self.tableWidget.selectedItems() if len(items) == 0: return - self.ui_data.model = self.models[items[0].row()] + self.ui_data.model_var_name_in_main = self.models[items[0].row()][1] + self.ui_data.model = self.models[items[0].row()][0] self.close() def update_models(self): import __main__ - s = __main__.__dict__ + s = dir(__main__) keys = [] for k in s: - if isinstance(s[k], pyo.Block): + if isinstance(getattr(__main__, k), pyo.Block): keys.append(k) self.tableWidget.clearContents() self.tableWidget.setRowCount(len(keys)) self.models = [] for row, k in enumerate(sorted(keys)): + model = getattr(__main__, k) item = myqt.QTableWidgetItem() item.setText(k) self.tableWidget.setItem(row, 0, item) item = myqt.QTableWidgetItem() try: - item.setText(s[k].name) + item.setText(model.name) except: item.setText("None") self.tableWidget.setItem(row, 1, item) item = myqt.QTableWidgetItem() - item.setText(str(type(s[k]))) + item.setText(str(type(model))) self.tableWidget.setItem(row, 2, item) - self.models.append(s[k]) + self.models.append((model, k)) diff --git a/pyomo/contrib/viewer/pyomo_viewer.py b/pyomo/contrib/viewer/pyomo_viewer.py index 6a24e12aa61..e4f75c86840 100644 --- a/pyomo/contrib/viewer/pyomo_viewer.py +++ b/pyomo/contrib/viewer/pyomo_viewer.py @@ -41,7 +41,7 @@ class QtApp( model except NameError: model=None - ui, model = get_mainwindow(model=model, ask_close=False) + ui = get_mainwindow(model=model, ask_close=False) ui.setWindowTitle('Pyomo Model Viewer -- {}')""" _kernel_cmd_hide_ui = """try: diff --git a/pyomo/contrib/viewer/tests/test_qt.py b/pyomo/contrib/viewer/tests/test_qt.py index e71921500f9..b7250729cd9 100644 --- a/pyomo/contrib/viewer/tests/test_qt.py +++ b/pyomo/contrib/viewer/tests/test_qt.py @@ -103,7 +103,7 @@ def blackbox(a, b): @unittest.skipIf(not available, "Qt packages are not available.") def test_get_mainwindow(qtbot): m = get_model() - mw, m = get_mainwindow(model=m, testing=True) + mw = get_mainwindow(model=m, testing=True) assert hasattr(mw, "menuBar") assert isinstance(mw.variables, ModelBrowser) assert isinstance(mw.constraints, ModelBrowser) @@ -113,13 +113,13 @@ def test_get_mainwindow(qtbot): @unittest.skipIf(not available, "Qt packages are not available.") def test_close_mainwindow(qtbot): - mw, m = get_mainwindow(model=None, testing=True) + mw = get_mainwindow(model=None, testing=True) mw.exit_action() @unittest.skipIf(not available, "Qt packages are not available.") def test_show_model_select_no_models(qtbot): - mw, m = get_mainwindow(model=None, testing=True) + mw = get_mainwindow(model=None, testing=True) ms = mw.show_model_select() ms.update_models() ms.select_model() @@ -128,7 +128,7 @@ def test_show_model_select_no_models(qtbot): @unittest.skipIf(not available, "Qt packages are not available.") def test_model_information(qtbot): m = get_model() - mw, m = get_mainwindow(model=m, testing=True) + mw = get_mainwindow(model=m, testing=True) mw.model_information() assert isinstance(mw._dialog, QMessageBox) text = mw._dialog.text() @@ -149,7 +149,7 @@ def test_model_information(qtbot): @unittest.skipIf(not available, "Qt packages are not available.") def test_tree_expand_collapse(qtbot): m = get_model() - mw, m = get_mainwindow(model=m, testing=True) + mw = get_mainwindow(model=m, testing=True) mw.variables.treeView.expandAll() mw.variables.treeView.collapseAll() @@ -157,7 +157,7 @@ def test_tree_expand_collapse(qtbot): @unittest.skipIf(not available, "Qt packages are not available.") def test_residual_table(qtbot): m = get_model() - mw, m = get_mainwindow(model=m, testing=True) + mw = get_mainwindow(model=m, testing=True) mw.residuals_restart() mw.ui_data.calculate_expressions() mw.residuals.calculate() @@ -184,7 +184,7 @@ def test_residual_table(qtbot): @unittest.skipIf(not available, "Qt packages are not available.") def test_var_tree(qtbot): m = get_model() - mw, m = get_mainwindow(model=m, testing=True) + mw = get_mainwindow(model=m, testing=True) qtbot.addWidget(mw) mw.variables.treeView.expandAll() root_index = mw.variables.datmodel.index(0, 0) @@ -218,7 +218,7 @@ def test_var_tree(qtbot): @unittest.skipIf(not available, "Qt packages are not available.") def test_bad_view(qtbot): m = get_model() - mw, m = get_mainwindow(model=m, testing=True) + mw = get_mainwindow(model=m, testing=True) err = None try: mw.badTree = mw._tree_restart( diff --git a/pyomo/contrib/viewer/ui.py b/pyomo/contrib/viewer/ui.py index 374af8a26f0..75b786b9796 100644 --- a/pyomo/contrib/viewer/ui.py +++ b/pyomo/contrib/viewer/ui.py @@ -79,16 +79,31 @@ def get_mainwindow(model=None, show=True, ask_close=True, testing=False): (ui, model): ui is the MainWindow widget, and model is the linked Pyomo model. If no model is provided a new ConcreteModel is created """ + model_name = None if model is None: - model = pyo.ConcreteModel(name="Default") - ui = MainWindow(model=model, ask_close=ask_close, testing=testing) + import __main__ + if "model" in dir(__main__): + if isinstance(getattr(__main__, "model"), pyo.Block): + model = getattr(__main__, "model") + model_name = "model" + for s in dir(__main__): + if isinstance(getattr(__main__, s), pyo.Block): + model = getattr(__main__, s) + model_name = s + break + ui = MainWindow( + model=model, + model_var_name_in_main=model_name, + ask_close=ask_close, + testing=testing, + ) try: get_ipython().events.register("post_execute", ui.refresh_on_execute) except AttributeError: pass # not in ipy kernel, so is fine to not register callback if show: ui.show() - return ui, model + return ui class MainWindow(_MainWindow, _MainWindowUI): @@ -97,6 +112,7 @@ def __init__(self, *args, **kwargs): main = self.main = kwargs.pop("main", None) ask_close = self.ask_close = kwargs.pop("ask_close", True) self.testing = kwargs.pop("testing", False) + model_var_name_in_main = kwargs.pop("model_var_name_in_main", None) flags = kwargs.pop("flags", 0) self.ui_data = UIData(model=model) super().__init__(*args, **kwargs) @@ -128,6 +144,7 @@ def __init__(self, *args, **kwargs): self.actionCalculateExpressions.triggered.connect( self.ui_data.calculate_expressions ) + self.ui_data.model_var_name_in_main = model_var_name_in_main self.actionTile.triggered.connect(self.mdiArea.tileSubWindows) self.actionCascade.triggered.connect(self.mdiArea.cascadeSubWindows) self.actionTabs.triggered.connect(self.toggle_tabs) @@ -256,6 +273,18 @@ def refresh_on_execute(self): ipython kernel. The main purpose of this right now it to refresh the UI display so that it matches the current state of the model. """ + if self.ui_data.model_var_name_in_main is not None: + import __main__ + + try: + mname = self.ui_data.model_var_name_in_main + mid = id(getattr(__main__, mname)) + if id(self.ui_data.model) != mid: + self.ui_data.model = getattr(__main__, mname) + self.update_model + return + except AttributeError: + pass for w in self._refresh_list: try: w.refresh() diff --git a/pyomo/contrib/viewer/ui_data.py b/pyomo/contrib/viewer/ui_data.py index c716cfeedf6..8d83be91e5f 100644 --- a/pyomo/contrib/viewer/ui_data.py +++ b/pyomo/contrib/viewer/ui_data.py @@ -39,16 +39,27 @@ class UIDataNoUi(object): UIData. The class is split this way for testing when PyQt is not available. """ - def __init__(self, model=None): + def __init__(self, model=None, model_var_name_in_main=None): """ This class holds the basic UI setup, but doesn't depend on Qt. It shouldn't really be used except for testing when Qt is not available. Args: model: The Pyomo model to view + model_var_name_in_main: if this is set, check that the model variable + which points to a model object in __main__ has the same id when + the UI is refreshed due to a command being executed in jupyter + notebook or QtConsole, if not the same id, then update the model + Since the model viewer is not necessarily pointed at a model in the + __main__ namespace only set this if you want the model to auto + update. Since the model selector dialog lets you choose models + from the __main__ namespace it sets this when you select a model. + This is useful if you run a script repeatedly that replaces a model + preventing you from looking at a previous version of the model. """ super().__init__() self._model = None + self.model_var_name_in_main = model_var_name_in_main self._begin_update = False self.value_cache = ComponentMap() self.value_cache_units = ComponentMap() From 01b5ef233935a1b7f958b49b8c13dacd35c21fde Mon Sep 17 00:00:00 2001 From: Eslick Date: Thu, 23 May 2024 14:58:31 -0400 Subject: [PATCH 1460/3044] add arg --- pyomo/contrib/viewer/ui.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/viewer/ui.py b/pyomo/contrib/viewer/ui.py index 75b786b9796..ac96e58eea9 100644 --- a/pyomo/contrib/viewer/ui.py +++ b/pyomo/contrib/viewer/ui.py @@ -66,7 +66,9 @@ class _MainWindow(object): _log.error(_err) -def get_mainwindow(model=None, show=True, ask_close=True, testing=False): +def get_mainwindow( + model=None, show=True, ask_close=True, model_var_name_in_main=None, testing=False +): """ Create a UI MainWindow. @@ -79,18 +81,19 @@ def get_mainwindow(model=None, show=True, ask_close=True, testing=False): (ui, model): ui is the MainWindow widget, and model is the linked Pyomo model. If no model is provided a new ConcreteModel is created """ - model_name = None + model_name = model_var_name_in_main if model is None: import __main__ - if "model" in dir(__main__): - if isinstance(getattr(__main__, "model"), pyo.Block): - model = getattr(__main__, "model") - model_name = "model" - for s in dir(__main__): - if isinstance(getattr(__main__, s), pyo.Block): - model = getattr(__main__, s) - model_name = s - break + + if model_name in dir(__main__): + if isinstance(getattr(__main__, model_name), pyo.Block): + model = getattr(__main__, model_name) + else: + for s in dir(__main__): + if isinstance(getattr(__main__, s), pyo.Block): + model = getattr(__main__, s) + model_name = s + break ui = MainWindow( model=model, model_var_name_in_main=model_name, From a7f21c5c9c36b39f0982c3db12aaaa29258240a6 Mon Sep 17 00:00:00 2001 From: Eslick Date: Thu, 23 May 2024 15:03:52 -0400 Subject: [PATCH 1461/3044] Update the doc --- pyomo/contrib/viewer/README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pyomo/contrib/viewer/README.md b/pyomo/contrib/viewer/README.md index cfc50b54ce2..93d773e3829 100644 --- a/pyomo/contrib/viewer/README.md +++ b/pyomo/contrib/viewer/README.md @@ -42,6 +42,24 @@ ui = get_mainwindow(model=model) # Do model things, the viewer will stay in sync with the Pyomo model ``` +If you are working in Jupyter notebook, Jupyter qtconsole, or other Jupyter- +based IDEs, and your model is in the __main__ namespace (this is the usual case), +you can specify the model by its variable name as below. The advantage of this +is that if you replace the model with a new model having the same variable name, +the UI will automatically update without having to manually reset the model pointer. + +```python +%gui qt #Enables IPython's GUI event loop integration. +# Execute the above in its own cell and wait for it to finish before moving on. +from pyomo.contrib.viewer.ui import get_mainwindow +import pyomo.environ as pyo + +model = pyo.ConcreteModel() # could import an existing model here +ui = get_mainwindow(model_var_name_in_main="model") + +# Do model things, the viewer will stay in sync with the Pyomo model +``` + **Note:** the ```%gui qt``` cell must be executed in its own cell and execution must complete before running any other cells (you can't use "run all"). From 16fdff487b344b7a3ec5494ffc445a58a65c15b4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 23 May 2024 13:18:42 -0600 Subject: [PATCH 1462/3044] Beginning of mixed form LP dal, no parameterized, with tests that don't work --- pyomo/core/plugins/transform/lp_dual.py | 63 +++++++++++++++++++++++-- pyomo/core/tests/unit/test_lp_dual.py | 47 ++++++++++++++++++ 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/pyomo/core/plugins/transform/lp_dual.py b/pyomo/core/plugins/transform/lp_dual.py index c38c7c89f99..59ca6c89f71 100644 --- a/pyomo/core/plugins/transform/lp_dual.py +++ b/pyomo/core/plugins/transform/lp_dual.py @@ -16,6 +16,7 @@ from pyomo.common.dependencies import scipy from pyomo.core import ( ConcreteModel, + Block, Var, Constraint, Objective, @@ -47,6 +48,18 @@ def var_list(x): raise ValueError("Expected Var or list of Vars.\n\tReceived %s" % type(x)) +class _LPDualData(AutoSlots.Mixin): + __slots__ = ('primal_var', 'dual_var', 'primal_constraint', 'dual_constraint') + def __init__(self): + self.primal_var = {} + self.dual_var = {} + self.primal_constraint = ComponentMap() + self.dual_constraint = ComponentMap() + + +Block.register_private_data_initializer(_LPDualData) + + @TransformationFactory.register( 'core.lp_dual', 'Generate the linear programming dual of the given model' ) @@ -118,13 +131,17 @@ def _take_dual(self, model): rows = range(A_transpose.shape[0]) cols = range(A_transpose.shape[1]) dual.x = Var(cols, domain=NonNegativeReals) + trans_info = model.private_data() for j, (primal_cons, ineq) in enumerate(std_form.rows): if primal_sense is minimize and ineq == 1: dual.x[j].domain = NonPositiveReals - elif primal_sense is maximzie and ineq == -1: + elif primal_sense is maximize and ineq == -1: dual.x[j].domain = NonPositiveReals - from pytest import set_trace - set_trace() + if ineq == 0: + # equality + dual.x[j].domain = Reals + trans_info.primal_constraint[dual.x[j]] = primal_cons + trans_info.dual_var[primal_cons] = dual.x[j] dual.constraints = Constraint(rows) for i, primal in enumerate(std_form.columns): @@ -154,6 +171,8 @@ def _take_dual(self, model): dual.constraints[i] = ( sum(A_transpose[i, j] * dual.x[j] for j in cols) == std_form.c[0, i] ) + trans_info.dual_constraint[primal] = dual.constraints[i] + trans_info.primal_var[dual.constraints[i]] = primal dual.obj = Objective( expr=sum(std_form.rhs[j] * dual.x[j] for j in cols), sense=-primal_sense @@ -163,3 +182,41 @@ def _take_dual(self, model): def _take_parameterized_dual(self, model, wrt): pass + + def get_primal_constraint(self, model, dual_var): + primal_constraint = model.private_data().primal_constraint + if dual_var in primal_constraint: + return primal_constraint[dual_var] + else: + raise ValueError( + "It does not appear that Var '%s' is a dual variable on model '%s'" + % (dual_var.name, model.name) + ) + + def get_dual_constraint(self, model, primal_var): + dual_constraint = model.private_data().dual_constraint + if primal_var in dual_constraint: + return dual_constraint[primal_var] + else: + raise ValueError( + "It does not appear that Var '%s' is a primal variable from model '%s'" + % (primal_var.name, model.name) + ) + + def get_primal_var(self, model, dual_constraint): + primal_var = model.private_data().primal_var + if dual_constraint in primal_var: + return primal_var[dual_constraint] + else: + raise ValueError( + "It does not appear that Constraint '%s' is a dual constraint on " + "model '%s'" % (dual_constraint.name, model.name)) + + def get_dual_var(self, model, primal_constraint): + dual_var = model.private_data().dual_var + if primal_constraint in dual_var: + return dual_var[primal_constraint] + else: + raise ValueError( + "It does not appear that Constraint '%s' is a primal constraint from " + "model '%s'" % (primal_constraint.name, model.name)) diff --git a/pyomo/core/tests/unit/test_lp_dual.py b/pyomo/core/tests/unit/test_lp_dual.py index c3bfe74afe4..1bfdcf4c6cc 100644 --- a/pyomo/core/tests/unit/test_lp_dual.py +++ b/pyomo/core/tests/unit/test_lp_dual.py @@ -80,3 +80,50 @@ def test_lp_dual(self): lp_dual = TransformationFactory('core.lp_dual') dual = lp_dual.create_using(m) + + alpha = lp_dual.get_dual_var(m.c1) + beta = lp_dual.get_dual_var(m.c2) + lamb = lp_dual.get_dual_var(m.c3) + xi = lp_dual.get_dual_var(m.c4) + + self.assertIs(lp_dual.get_primal_constraint[alpha], m.c1) + self.assertIs(lp_dual.get_primal_constraint[beta], m.c2) + self.assertIs(lp_dual.get_primal_constraint[lamb], m.c3) + self.assertIs(lp_dual.get_primal_constraint[xi], m.c4) + + dx = lp_dual.get_dual_constraint[m.x] + dy = lp_dual.get_dual_constraint[m.y] + dz = lp_dual.get_dual_constraint[m.z] + + self.assertIs(lp_dual.get_primal_var[dx], m.x) + self.assertIs(lp_dual.get_primal_var[dy], m.y) + self.assertIs(lp_dual.get_primal_var[dz], m.z) + + self.assertIsInstance(alpha, Var) + self.assertIs(alpha.domain, NonPositiveReals) + self.assertEqual(alpha.ub, 0) + self.assertIsNone(alpha.lb) + self.assertIsInstance(beta, Var) + self.assertIs(alpha.domain, NonNegativeReals) + self.assertEqual(alpha.lb, 0) + self.assertIsNone(alpha.ub) + self.assertIsInstance(lamb, Var) + self.assertIs(lamb.domain, Reals) + self.assertIsNone(lamb.ub) + self.assertIsNone(lamb.lb) + self.assertIsInstance(xi, Var) + self.assertIs(xi.domain, NonPositiveReals) + self.assertEqual(xi.ub, 0) + self.assertIsNone(xi.lb) + + self.assertIsInstance(dx, Constraint) + self.assertIsInstance(dy, Constraint) + self.assertIsInstance(dz, Constraint) + + assertExpressionsEqual( + self, + dx.expr, + -4 * alpha + beta <= 1 + ) + + # TODO: map objectives, and test them From 1bf27f02b0f94569a98e06cb0c08f7b978630f3c Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 21:21:35 -0400 Subject: [PATCH 1463/3044] Added reactor_design example for Pyomo.DoE. This is still being debugged. --- pyomo/contrib/doe/examples/reactor_design.py | 174 +++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 pyomo/contrib/doe/examples/reactor_design.py diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py new file mode 100644 index 00000000000..81a64a0a46a --- /dev/null +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -0,0 +1,174 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# +# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation +# Initiative (CCSI), and is copyright (c) 2022 by the software owners: +# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., +# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, +# Battelle Memorial Institute, University of Notre Dame, +# The University of Pittsburgh, The University of Texas at Austin, +# University of Toledo, West Virginia University, et al. All rights reserved. +# +# NOTICE. This Software was developed under funding from the +# U.S. Department of Energy and the U.S. Government consequently retains +# certain rights. As such, the U.S. Government has been granted for itself +# and others acting on its behalf a paid-up, nonexclusive, irrevocable, +# worldwide license in the Software to reproduce, distribute copies to the +# public, prepare derivative works, and perform publicly and display +# publicly, and to permit other to do so. +# ___________________________________________________________________________ + +# from pyomo.contrib.parmest.examples.reactor_design import reactor_design_model +# if we refactor to use the same create_model function as parmest, +# we can just import instead of redefining the model + +import pyomo.environ as pyo +from pyomo.dae import ContinuousSet, DerivativeVar +from pyomo.contrib.doe import ModelOptionLib, DesignOfExperiments, MeasurementVariables, DesignVariables + +def create_model( + mod=None, + model_option="stage2"): + + model_option = ModelOptionLib(model_option) + + model = mod + + if model_option == ModelOptionLib.parmest: + model = pyo.ConcreteModel() + return_m = True + elif model_option == ModelOptionLib.stage1 or model_option == ModelOptionLib.stage2: + if model is None: + raise ValueError( + "If model option is stage1 or stage2, a created model needs to be provided." + ) + return_m = False + else: + raise ValueError( + "model_option needs to be defined as parmest, stage1, or stage2." + ) + + # Rate constants + model.k1 = pyo.Var( + initialize=5.0 / 6.0, within=pyo.PositiveReals + ) # min^-1 + model.k2 = pyo.Var( + initialize=5.0 / 3.0, within=pyo.PositiveReals + ) # min^-1 + model.k3 = pyo.Var( + initialize=1.0 / 6000.0, within=pyo.PositiveReals + ) # m^3/(gmol min) + + # Inlet concentration of A, gmol/m^3 + model.caf = pyo.Var(initialize=10000, within=pyo.PositiveReals) + + # Space velocity (flowrate/volume) + model.sv = pyo.Var(initialize=1.0, within=pyo.PositiveReals) + + # Outlet concentration of each component + model.ca = pyo.Var(initialize=5000.0, within=pyo.PositiveReals) + model.cb = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cc = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) + model.cd = pyo.Var(initialize=1000.0, within=pyo.PositiveReals) + + # Objective + model.obj = pyo.Objective(expr=model.cb, sense=pyo.maximize) + + # Constraints + model.ca_bal = pyo.Constraint( + expr=( + 0 + == model.sv * model.caf + - model.sv * model.ca + - model.k1 * model.ca + - 2.0 * model.k3 * model.ca**2.0 + ) + ) + + model.cb_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb) + ) + + model.cc_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cc + model.k2 * model.cb) + ) + + model.cd_bal = pyo.Constraint( + expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) + ) + + if return_m: + return model + +def main(): + + # measurement object + measurements = MeasurementVariables() + measurements.add_variables( + "ca", + indices=None, + time_index_position=None, + ) + measurements.add_variables( + "cb", + indices=None, + time_index_position=None + ) + measurements.add_variables( + "cc", + indices=None, + time_index_position=None + ) + measurements.add_variables( + "cd", + indices=None, + time_index_position=None + ) + + # design object + exp_design = DesignVariables() + exp_design.add_variables( + "sv", + indices=None, + time_index_position=None, + values=1.0, + lower_bounds=0.1, + upper_bounds=10.0 + ) + exp_design.add_variables( + "caf", + indices=None, + time_index_position=None, + values=10000, + lower_bounds=5000, + upper_bounds=15000 + ) + + theta_values = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} + + doe1 = DesignOfExperiments( + theta_values, + measurements, + exp_design, + create_model, + prior_FIM=None + ) + + doe1.compute_FIM( + mode="sequential_finite", # calculation mode + scale_nominal_param_value=True, # scale nominal parameter value + formula="central", # formula for finite difference + ) + + doe1.result.result_analysis() + +if __name__ == "__main__": + main() + From 0932c2d308e22eb1875d26f9200d1d36cb6a9a8e Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 21:28:03 -0400 Subject: [PATCH 1464/3044] Debugged some syntax issues in example --- pyomo/contrib/doe/examples/reactor_design.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 81a64a0a46a..18d6b07c9fb 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -114,7 +114,7 @@ def main(): measurements.add_variables( "ca", indices=None, - time_index_position=None, + time_index_position=None ) measurements.add_variables( "cb", @@ -155,8 +155,8 @@ def main(): doe1 = DesignOfExperiments( theta_values, - measurements, exp_design, + measurements, create_model, prior_FIM=None ) @@ -167,7 +167,7 @@ def main(): formula="central", # formula for finite difference ) - doe1.result.result_analysis() + doe1.result_analysis() if __name__ == "__main__": main() From 0db9100d22b7a8236c9f6bec9dc1775cfb593d75 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 21:29:09 -0400 Subject: [PATCH 1465/3044] A few more syntax mistakes --- pyomo/contrib/doe/examples/reactor_design.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 18d6b07c9fb..10096f17e69 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -161,13 +161,13 @@ def main(): prior_FIM=None ) - doe1.compute_FIM( + result = doe1.compute_FIM( mode="sequential_finite", # calculation mode scale_nominal_param_value=True, # scale nominal parameter value formula="central", # formula for finite difference ) - doe1.result_analysis() + result.result_analysis() if __name__ == "__main__": main() From ecc4600dcf77a9e234403b85df11e6512122abd2 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 21:51:11 -0400 Subject: [PATCH 1466/3044] Removed objective from example. --- pyomo/contrib/doe/doe.py | 5 +++++ pyomo/contrib/doe/examples/reactor_design.py | 3 --- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 9356cce360b..0718493a826 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -423,6 +423,11 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): # dict for storing model outputs output_record = {} + # add zero (dummy/placeholder) objective function + mod.Obj = pyo.Objective(expr=0, sense=pyo.minimize) + + mod.pprint() + # solve model square_result = self._solve_doe(mod, fix=True) diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 10096f17e69..273d97c88c5 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -78,9 +78,6 @@ def create_model( model.cc = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) model.cd = pyo.Var(initialize=1000.0, within=pyo.PositiveReals) - # Objective - model.obj = pyo.Objective(expr=model.cb, sense=pyo.maximize) - # Constraints model.ca_bal = pyo.Constraint( expr=( From 3fd4784803a86a4aed56ac95a277d2d11a824e78 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Thu, 23 May 2024 22:01:08 -0400 Subject: [PATCH 1467/3044] Finished adding tests. --- pyomo/contrib/doe/examples/reactor_design.py | 80 ++++++++++---------- pyomo/contrib/doe/tests/test_example.py | 8 ++ 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 273d97c88c5..450a2800ae1 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -26,17 +26,22 @@ # ___________________________________________________________________________ # from pyomo.contrib.parmest.examples.reactor_design import reactor_design_model -# if we refactor to use the same create_model function as parmest, +# if we refactor to use the same create_model function as parmest, # we can just import instead of redefining the model import pyomo.environ as pyo from pyomo.dae import ContinuousSet, DerivativeVar -from pyomo.contrib.doe import ModelOptionLib, DesignOfExperiments, MeasurementVariables, DesignVariables +from pyomo.contrib.doe import ( + ModelOptionLib, + DesignOfExperiments, + MeasurementVariables, + DesignVariables, +) +from pyomo.common.dependencies import numpy as np + + +def create_model(mod=None, model_option="stage2"): -def create_model( - mod=None, - model_option="stage2"): - model_option = ModelOptionLib(model_option) model = mod @@ -54,14 +59,10 @@ def create_model( raise ValueError( "model_option needs to be defined as parmest, stage1, or stage2." ) - + # Rate constants - model.k1 = pyo.Var( - initialize=5.0 / 6.0, within=pyo.PositiveReals - ) # min^-1 - model.k2 = pyo.Var( - initialize=5.0 / 3.0, within=pyo.PositiveReals - ) # min^-1 + model.k1 = pyo.Var(initialize=5.0 / 6.0, within=pyo.PositiveReals) # min^-1 + model.k2 = pyo.Var(initialize=5.0 / 3.0, within=pyo.PositiveReals) # min^-1 model.k3 = pyo.Var( initialize=1.0 / 6000.0, within=pyo.PositiveReals ) # m^3/(gmol min) @@ -103,31 +104,16 @@ def create_model( if return_m: return model - + + def main(): # measurement object measurements = MeasurementVariables() - measurements.add_variables( - "ca", - indices=None, - time_index_position=None - ) - measurements.add_variables( - "cb", - indices=None, - time_index_position=None - ) - measurements.add_variables( - "cc", - indices=None, - time_index_position=None - ) - measurements.add_variables( - "cd", - indices=None, - time_index_position=None - ) + measurements.add_variables("ca", indices=None, time_index_position=None) + measurements.add_variables("cb", indices=None, time_index_position=None) + measurements.add_variables("cc", indices=None, time_index_position=None) + measurements.add_variables("cd", indices=None, time_index_position=None) # design object exp_design = DesignVariables() @@ -137,7 +123,7 @@ def main(): time_index_position=None, values=1.0, lower_bounds=0.1, - upper_bounds=10.0 + upper_bounds=10.0, ) exp_design.add_variables( "caf", @@ -145,17 +131,13 @@ def main(): time_index_position=None, values=10000, lower_bounds=5000, - upper_bounds=15000 + upper_bounds=15000, ) theta_values = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} doe1 = DesignOfExperiments( - theta_values, - exp_design, - measurements, - create_model, - prior_FIM=None + theta_values, exp_design, measurements, create_model, prior_FIM=None ) result = doe1.compute_FIM( @@ -166,6 +148,20 @@ def main(): result.result_analysis() + # print("log10 Trace of FIM: ", np.log10(result.trace)) + # print("log10 Determinant of FIM: ", np.log10(result.det)) + + # test result + relative_error_trace = abs(np.log10(result.trace) - 6.815) + assert ( + relative_error_trace < 0.01 + ), "log10(tr(FIM)) regression test failed, answer does not match previous result" + + relative_error_det = abs(np.log10(result.det) - 18.719) + assert ( + relative_error_det < 0.01 + ), "log10(det(FIM)) regression test failed, answer does not match previous result" + + if __name__ == "__main__": main() - diff --git a/pyomo/contrib/doe/tests/test_example.py b/pyomo/contrib/doe/tests/test_example.py index 8153e07018a..c92725efd3b 100644 --- a/pyomo/contrib/doe/tests/test_example.py +++ b/pyomo/contrib/doe/tests/test_example.py @@ -65,6 +65,14 @@ def test_reactor_grid_search(self): reactor_grid_search.main() + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not pandas_available, "pandas is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_design(self): + from pyomo.contrib.doe.examples import reactor_design + + reactor_design.main() + if __name__ == "__main__": unittest.main() From 9d1e6533e114b4a4026993d278f7dcda71c1646e Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 24 May 2024 10:28:28 -0400 Subject: [PATCH 1468/3044] add a slightly smaller but still unsolvable mip for when you can make mild assumptions --- .../piecewise/tests/test_triangulations.py | 23 ++- pyomo/contrib/piecewise/triangulations.py | 142 +++++++++++++++--- 2 files changed, 143 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index 5eb36da4250..86bda3105eb 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -15,7 +15,8 @@ import pyomo.common.unittest as unittest from pyomo.contrib.piecewise.triangulations import ( get_j1_triangulation, - get_incremental_simplex_ordering + get_incremental_simplex_ordering, + get_incremental_simplex_ordering_assume_connected_by_n_face, ) class TestTriangulations(unittest.TestCase): @@ -82,3 +83,23 @@ def test_J1_medium_ordering(self): second_simplex = reordered_simplices[idx + 1] # test property (2) which also guarantees property (1) self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + def test_J1_medium_ordering_alt(self): + points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6])) + triangulation = get_j1_triangulation(points, 2) + reordered_simplices = get_incremental_simplex_ordering_assume_connected_by_n_face(triangulation.simplices, 1) + for idx, first_simplex in reordered_simplices.items(): + if idx != len(triangulation.points) - 1: + second_simplex = reordered_simplices[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + def test_J1_medium_ordering_3d(self): + points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6], [-5, -1, 0.2, 3, 10])) + triangulation = get_j1_triangulation(points, 3) + reordered_simplices = get_incremental_simplex_ordering_assume_connected_by_n_face(triangulation.simplices, 2) + for idx, first_simplex in reordered_simplices.items(): + if idx != len(triangulation.points) - 1: + second_simplex = reordered_simplices[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index d522c247a0f..f33fd3f4304 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -14,6 +14,7 @@ import itertools from types import SimpleNamespace from functools import cmp_to_key +from pyomo.common.errors import DeveloperError from pyomo.environ import ( ConcreteModel, RangeSet, @@ -26,6 +27,10 @@ Objective, TerminationCondition, ) +from pyomo.common.dependencies import attempt_import +nx, nx_available = attempt_import( + 'networkx', 'Networkx is required to calculate incremental ordering.' +) class Triangulation: Delaunay = 1 @@ -55,15 +60,13 @@ def get_j1_triangulation(points, dimension): def _process_points_j1(points, dimension): if not len(points[0]) == dimension: raise ValueError("Points not consistent with specified dimension") - num_pts = math.floor(len(points) ** (1 / dimension)) + num_pts = round(len(points) ** (1 / dimension)) if not len(points) == num_pts**dimension: raise ValueError("'points' must have points forming an n-dimensional grid with straight grid lines and the same odd number of points in each axis") if not num_pts % 2 == 1: raise ValueError("'points' must have points forming an n-dimensional grid with straight grid lines and the same odd number of points in each axis") # munge the points into an organized map with n-dimensional keys - #points.sort(key=cmp_to_key(_compare_lexicographic(dimension))) - # verify: does this do correct sorting by default? points.sort() points_map = {} for point_index in itertools.product(range(num_pts), repeat=dimension): @@ -73,16 +76,6 @@ def _process_points_j1(points, dimension): points_map[point_index] = points[point_flat_index] return points_map, num_pts -#def _compare_lexicographic(dimension): -# def compare_lexicographic_real(x, y): -# for n in range(dimension): -# if x[n] < y[n]: -# return -1 -# elif y[n] < x[n]: -# return 1 -# return 0 -# return compare_lexicographic_real - # This implements the J1 "Union Jack" triangulation (Todd 77) as explained by # Vielma 2010. # Triangulate {0, ..., K}^n for even K using the J1 triangulation, mapping the @@ -193,13 +186,6 @@ def schedule_each_simplex(m, i): @m.Constraint(m.SIMPLICES) def schedule_each_slot(m, j): return sum(m.x[i, j] for i in m.SIMPLICES) == 1 - - # Enforce property (1), but this is guaranteed by (2) so unnecessary - #@m.Constraint(m.SIMPLICES) - #def simplex_order(m, i): - # # anything with at least a vertex in common is a neighbor - # neighbors = [s for s in m.SIMPLICES if sum(m.TestVerticesEqual[i, n, s, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) >= 1] - # return sum(m.x[i, j] * m.x[k, j+1] for j in m.SIMPLICES if j != m.SimplicesCount - 1 for k in neighbors) == 1 # Each simplex needs exactly one first and exactly one last vertex @m.Constraint(m.SIMPLICES) @@ -261,4 +247,118 @@ def vertex_order(m, i, j): new_simplex.append(old_simplex[last]) new_simplices[j] = new_simplex break - return new_simplices \ No newline at end of file + return new_simplices + +# If we have the assumption that our ordering is possible such that consecutively +# ordered simplices share at least a one-face, then getting an order for the +# simplices is enough to get one for the edges and we "just" need to find a +# Hamiltonian path +def get_incremental_simplex_ordering_assume_connected_by_n_face(simplices, connected_face_dim, subsolver='gurobi'): + if connected_face_dim == 0: + return get_incremental_simplex_ordering(simplices) + #if not nx_available: + # raise ImportError('Missing Networkx') + #G = nx.Graph() + #G.add_nodes_from(range(len(simplices))) + #for i in range(len(simplices)): + # for j in range(i + 1, len(simplices)): + # if len(set(simplices[i]) & set(simplices[j])) >= n + 1: + # G.add_edge(i, j) + + # ask Gurobi again because networkx doesn't seem to have a general hamiltonian + # path and I don't want to implement it myself + + m = ConcreteModel() + + # Sets and Params + m.SimplicesCount = Param(initialize=len(simplices)) + m.SIMPLICES = RangeSet(0, m.SimplicesCount - 1) + # For each of the simplices we need to choose an initial and a final vertex. + # The rest we can order arbitrarily after finishing the MIP solve. + m.SimplexVerticesCount = Param(initialize=len(simplices[0])) + m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) + @m.Param(m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) + def TestVerticesEqual(m, i, n, j, k): + return 1 if simplices[i][n] == simplices[j][k] else 0 + + # Vars + # x_ij means simplex i is placed in slot j + m.x = Var(m.SIMPLICES, m.SIMPLICES, domain=Binary) + + # Constraints + # Each simplex should have a slot and each slot should have a simplex + @m.Constraint(m.SIMPLICES) + def schedule_each_simplex(m, i): + return sum(m.x[i, j] for j in m.SIMPLICES) == 1 + @m.Constraint(m.SIMPLICES) + def schedule_each_slot(m, j): + return sum(m.x[i, j] for i in m.SIMPLICES) == 1 + + # Enforce property (1) + @m.Constraint(m.SIMPLICES) + def simplex_order(m, i): + # anything with at least a vertex in common is a neighbor + neighbors = [s for s in m.SIMPLICES if sum(m.TestVerticesEqual[i, n, s, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) >= connected_face_dim + 1 and s != i] + #print(f'neighbors of {i} are {neighbors}') + return sum(m.x[i, j] * m.x[k, j + 1] for j in m.SIMPLICES if j != m.SimplicesCount - 1 for k in neighbors) + m.x[i, m.SimplicesCount - 1] == 1 + + # Trivial objective (do I need this?) + m.obj = Objective(expr=0) + + #m.pprint() + # Solve model + results = SolverFactory(subsolver).solve(m, tee=True) + match(results.solver.termination_condition): + case TerminationCondition.infeasible: + raise ValueError(f"The triangulation was impossible to suitably order for the incremental transformation under the assumption that consecutive simplices share {connected_face_dim}-faces. Try relaxing that assumption, or try a different triangulation, such as J1.") + case TerminationCondition.optimal: + pass + case _: + raise ValueError(f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}") + + # Retrieve data + new_simplices = {} + for j in m.SIMPLICES: + for i in m.SIMPLICES: + if abs(value(m.x[i, j]) - 1) < 1e-5: + # The jth slot is occupied by the ith simplex + new_simplices[j] = simplices[i] + # Note vertices need to be fixed after the fact now + break + fix_vertices_incremental_order(new_simplices) + return new_simplices + +# Fix vertices (in place) when the simplices are right but vertices are not +def fix_vertices_incremental_order(simplices): + last_vertex_index = len(simplices[0]) - 1 + for i, simplex in simplices.items(): + # Choose vertices like this: first is always the same as last + # of the previous simplex. Last is arbitrarily chosen from the + # intersection with the next simplex. + first = None + last = None + if i == 0: + first = 0 + else: + for n in range(last_vertex_index + 1): + if simplex[n] == simplices[i - 1][last_vertex_index]: + first = n + break + + if i == len(simplices) - 1: + last = last_vertex_index + else: + for n in range(last_vertex_index + 1): + if simplex[n] in simplices[i + 1] and n != first: + last = n + break + if first == None or last == None: + raise DeveloperError("Couldn't fix vertex ordering for incremental.") + + # reorder the simplex with the desired first and last + new_simplex = [simplex[first]] + for n in range(last_vertex_index + 1): + if n != first and n != last: + new_simplex.append(simplex[n]) + new_simplex.append(simplex[last]) + simplices[i] = new_simplex \ No newline at end of file From 095c4e1f31b783160264fa17f2281e9e58fbf53b Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 24 May 2024 14:27:31 -0400 Subject: [PATCH 1469/3044] implement proof by picture, now 2d is fast --- .../piecewise/tests/test_triangulations.py | 40 ++++ pyomo/contrib/piecewise/triangulations.py | 198 +++++++++++++++--- 2 files changed, 210 insertions(+), 28 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index 86bda3105eb..28ee61909da 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -103,3 +103,43 @@ def test_J1_medium_ordering_3d(self): second_simplex = reordered_simplices[idx + 1] # test property (2) which also guarantees property (1) self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + def test_J1_2d_ordering_0(self): + points = list(itertools.product([0, 1, 2], [1, 2.4, 3])) + ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices + self.assertEqual(len(ordered_triangulation), 8) + for idx, first_simplex in ordered_triangulation.items(): + if idx != len(ordered_triangulation) - 1: + second_simplex = ordered_triangulation[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + def test_J1_2d_ordering_1(self): + points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6])) + ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices + self.assertEqual(len(ordered_triangulation), 32) + for idx, first_simplex in ordered_triangulation.items(): + if idx != len(ordered_triangulation) - 1: + second_simplex = ordered_triangulation[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + def test_J1_2d_ordering_2(self): + points = list(itertools.product([0, 1, 2, 4, 5, 6.3, 7.1], [1, 2.4, 3, 5, 6, 9.1, 10])) + ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices + self.assertEqual(len(ordered_triangulation), 72) + for idx, first_simplex in ordered_triangulation.items(): + if idx != len(ordered_triangulation) - 1: + second_simplex = ordered_triangulation[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + def test_J1_2d_ordering_3(self): + points = list(itertools.product([0, 1, 2, 4, 5, 6.3, 7.1, 7.2, 7.3], [1, 2.4, 3, 5, 6, 9.1, 10, 11, 12])) + ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices + self.assertEqual(len(ordered_triangulation), 128) + for idx, first_simplex in ordered_triangulation.items(): + if idx != len(ordered_triangulation) - 1: + second_simplex = ordered_triangulation[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index f33fd3f4304..51090dadcb0 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -13,6 +13,7 @@ import math import itertools from types import SimpleNamespace +from enum import Enum from functools import cmp_to_key from pyomo.common.errors import DeveloperError from pyomo.environ import ( @@ -36,9 +37,13 @@ class Triangulation: Delaunay = 1 J1 = 2 -def get_j1_triangulation(points, dimension): +def get_j1_triangulation(points, dimension, ordered=False): points_map, num_pts = _process_points_j1(points, dimension) - simplices_list = _get_j1_triangulation(points_map, num_pts - 1, dimension) + + if ordered and dimension == 2: + simplices_list = _get_j1_triangulation_2d_ordered(points_map, num_pts - 1) + else: + simplices_list = _get_j1_triangulation(points_map, num_pts - 1, dimension) # make a duck-typed thing that superficially looks like an instance of # scipy.spatial.Delaunay (these are NDarrays in the original) triangulation = SimpleNamespace() @@ -103,32 +108,169 @@ def _get_j1_triangulation(points_map, K, n): ret.append(sorted(simplex)) return ret -def _get_j1_triangulation_2d(points_map, num_pts): - # Each square needs two triangles in it, orientation determined by the parity of - # the bottom-left corner's coordinate indices (x and y). Same parity = top-left - # and bottom-right triangles; different parity = top-right and bottom-left triangles. - simplices = [] - for i in range(num_pts): - for j in range(num_pts): - if i % 2 == j % 2: - simplices.append( - (points_map[i, j], - points_map[i + 1, j + 1], - points_map[i, j + 1])) - simplices.append( - (points_map[i, j], - points_map[i + 1, j + 1], - points_map[i + 1, j])) - else: - simplices.append( - (points_map[i + 1, j], - points_map[i, j + 1], - points_map[i, j])) - simplices.append( - (points_map[i + 1, j], - points_map[i, j + 1], - points_map[i + 1, j + 1])) - return simplices +# Implement proof-by-picture from Todd 1977. I do the reverse order he does +# and also keep the pictures slightly more regular to make things easier to +# implement. +def _get_j1_triangulation_2d_ordered(points_map, num_pts): + # check when square has simplices in top-left and bottom-right + square_parity_tlbr = lambda x, y: x % 2 == y % 2 + # check when we are in a "turnaround square" as seen in the picture + is_turnaround = lambda x, y: x >= num_pts / 2 and y == (num_pts / 2) - 1 + class Direction(Enum): + left = 0 + down = 1 + up = 2 + right = 3 + facing = None + + simplices = {} + start_square = (num_pts - 1, (num_pts / 2) - 1) + + # make it easier to read what I'm doing + def add_bottom_right(): + simplices[len(simplices)] = (points_map[x, y], points_map[x + 1, y], points_map[x + 1, y + 1]) + def add_top_right(): + simplices[len(simplices)] = (points_map[x, y + 1], points_map[x + 1, y], points_map[x + 1, y + 1]) + def add_bottom_left(): + simplices[len(simplices)] = (points_map[x, y], points_map[x, y + 1], points_map[x + 1, y]) + def add_top_left(): + simplices[len(simplices)] = (points_map[x, y], points_map[x, y + 1], points_map[x + 1, y + 1]) + + + # identify square by bottom-left corner + x, y = start_square + used_squares = set() # not used for the turnaround squares + + # depending on parity we will need to go either up or down to start + if square_parity_tlbr(x, y): + add_bottom_right() + facing = Direction.down + y -= 1 + else: + add_top_right() + facing = Direction.up + y += 1 + + # state machine + while (True): + match(facing): + case Direction.left: + if square_parity_tlbr(x, y): + add_bottom_right() + add_top_left() + else: + add_top_right() + add_bottom_left() + used_squares.add((x, y)) + if (x - 1, y) in used_squares or x == 0: + # can't keep going left so we need to go up or down depending + # on parity + if square_parity_tlbr(x, y): + y += 1 + facing = Direction.up + continue + else: + y -= 1 + facing = Direction.down + continue + else: + x -= 1 + continue + case Direction.right: + if is_turnaround(x, y): + # finished; this case should always eventually be reached + add_bottom_left() + fix_vertices_incremental_order(simplices) + return simplices + else: + if square_parity_tlbr(x, y): + add_top_left() + add_bottom_right() + else: + add_bottom_left() + add_top_right() + used_squares.add((x, y)) + if (x + 1, y) in used_squares or x == num_pts - 1: + # can't keep going right so we need to go up or down depending + # on parity + if square_parity_tlbr(x, y): + y -= 1 + facing = Direction.down + continue + else: + y += 1 + facing = Direction.up + continue + else: + x += 1 + continue + case Direction.down: + if is_turnaround(x, y): + # we are always in a TLBR square. Take the TL of this, the TR + # of the one on the left, and continue upwards one to the left + assert square_parity_tlbr(x, y), "uh oh" + add_top_left() + x -= 1 + add_top_right() + y += 1 + facing = Direction.up + continue + else: + if square_parity_tlbr(x, y): + add_top_left() + add_bottom_right() + else: + add_top_right() + add_bottom_left() + used_squares.add((x, y)) + if (x, y - 1) in used_squares or y == 0: + # can't keep going down so we need to turn depending + # on our parity + if square_parity_tlbr(x, y): + x += 1 + facing = Direction.right + continue + else: + x -= 1 + facing = Direction.left + continue + else: + y -= 1 + continue + case Direction.up: + if is_turnaround(x, y): + # we are always in a non-TLBR square. Take the BL of this, the BR + # of the one on the left, and continue downwards one to the left + assert not square_parity_tlbr(x, y), "uh oh" + add_bottom_left() + x -= 1 + add_bottom_right() + y -= 1 + facing = Direction.down + continue + else: + if square_parity_tlbr(x, y): + add_bottom_right() + add_top_left() + else: + add_bottom_left() + add_top_right() + used_squares.add((x, y)) + if (x, y + 1) in used_squares or y == num_pts - 1: + # can't keep going up so we need to turn depending + # on our parity + if square_parity_tlbr(x, y): + x -= 1 + facing = Direction.left + continue + else: + x += 1 + facing = Direction.right + continue + else: + y += 1 + continue + def _get_j1_triangulation_3d(points, dimension): pass From db5744b91f310c1ff4f4992bdff792887d235c1a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 24 May 2024 14:32:10 -0400 Subject: [PATCH 1470/3044] edit comment --- pyomo/contrib/piecewise/triangulations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 51090dadcb0..2e2f4e07cad 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -110,7 +110,8 @@ def _get_j1_triangulation(points_map, K, n): # Implement proof-by-picture from Todd 1977. I do the reverse order he does # and also keep the pictures slightly more regular to make things easier to -# implement. +# implement. Also remember that Todd's drawing is misleading to the point of +# almost being wrong so make sure you draw it properly first. def _get_j1_triangulation_2d_ordered(points_map, num_pts): # check when square has simplices in top-left and bottom-right square_parity_tlbr = lambda x, y: x % 2 == y % 2 From 27df1e5e7b4e4005608efbc2e57c6fb5bec813ea Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 24 May 2024 16:23:35 -0600 Subject: [PATCH 1471/3044] Defer categorizing constraints until after linear presolve --- pyomo/repn/plugins/nl_writer.py | 62 ++++++++++++++++++------------ pyomo/repn/tests/ampl/test_nlv2.py | 56 ++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 25 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 0a6f9f3da30..9d66b37a429 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -702,8 +702,7 @@ def write(self, model): objectives.extend(linear_objs) n_objs = len(objectives) - constraints = [] - linear_cons = [] + all_constraints = [] n_ranges = 0 n_equality = 0 n_complementarity_nonlin = 0 @@ -740,22 +739,7 @@ def write(self, model): ub = ub * scale if scale < 0: lb, ub = ub, lb - if expr_info.nonlinear: - constraints.append((con, expr_info, lb, ub)) - elif expr_info.linear: - linear_cons.append((con, expr_info, lb, ub)) - elif not self.config.skip_trivial_constraints: - linear_cons.append((con, expr_info, lb, ub)) - else: # constant constraint and skip_trivial_constraints - c = expr_info.const - if (lb is not None and lb - c > TOL) or ( - ub is not None and ub - c < -TOL - ): - raise InfeasibleConstraintException( - "model contains a trivially infeasible " - f"constraint '{con.name}' (fixed body value " - f"{c} outside bounds [{lb}, {ub}])." - ) + all_constraints.append((con, expr_info, lb, ub)) if linear_presolve: con_id = id(con) if not expr_info.nonlinear and lb == ub and lb is not None: @@ -766,7 +750,7 @@ def write(self, model): # report the last constraint timer.toc('Constraint %s', last_parent, level=logging.DEBUG) else: - timer.toc('Processed %s constraints', len(constraints)) + timer.toc('Processed %s constraints', len(all_constraints)) # This may fetch more bounds than needed, but only in the cases # where variables were completely eliminated while walking the @@ -781,14 +765,44 @@ def write(self, model): del comp_by_linear_var del lcon_by_linear_nnz - # Order the constraints, moving all nonlinear constraints to - # the beginning - n_nonlinear_cons = len(constraints) + # Note: defer categorizing constraints until after presolve, as + # the presolver could result in nonlinear constraints to become + # linear (or trivial) + constraints = [] + linear_cons = [] if eliminated_cons: _removed = eliminated_cons.__contains__ - constraints.extend(filterfalse(lambda c: _removed(id(c[0])), linear_cons)) + _constraints = filterfalse(lambda c: _removed(id(c[0])), all_constraints) else: - constraints.extend(linear_cons) + _constraints = all_constraints + for info in _constraints: + expr_info = info[1] + if expr_info.nonlinear: + if expr_info.nonlinear[1]: + constraints.append(info) + continue + expr_info.const += _evaluate_constant_nl(expr_info.nonlinear[0]) + expr_info.nonlinear = None + if expr_info.linear: + linear_cons.append(info) + elif not self.config.skip_trivial_constraints: + linear_cons.append(info) + else: # constant constraint and skip_trivial_constraints + c = expr_info.const + con, expr_info, lb, ub = info + if (lb is not None and lb - c > TOL) or ( + ub is not None and ub - c < -TOL + ): + raise InfeasibleConstraintException( + "model contains a trivially infeasible " + f"constraint '{con.name}' (fixed body value " + f"{c} outside bounds [{lb}, {ub}])." + ) + + # Order the constraints, moving all nonlinear constraints to + # the beginning + n_nonlinear_cons = len(constraints) + constraints.extend(linear_cons) n_cons = len(constraints) # diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 0aa9fab96f9..09936b45bbc 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -1763,7 +1763,11 @@ def test_presolve_zero_coef(self): OUT = io.StringIO() with LoggingIntercept() as LOG: nlinfo = nl_writer.NLWriter().write( - m, OUT, symbolic_solver_labels=True, linear_presolve=True + m, + OUT, + symbolic_solver_labels=True, + linear_presolve=True, + skip_trivial_constraints=False, ) self.assertEqual(LOG.getvalue(), "") @@ -1808,6 +1812,56 @@ def test_presolve_zero_coef(self): k0 #intermediate Jacobian column lengths G0 1 #obj 0 0 +""", + OUT.getvalue(), + ) + ) + + OUT = io.StringIO() + with LoggingIntercept() as LOG: + nlinfo = nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual(LOG.getvalue(), "") + + self.assertIs(nlinfo.eliminated_vars[0][0], m.y) + self.assertExpressionsEqual( + nlinfo.eliminated_vars[0][1], LinearExpression([-1.0 * m.z]) + ) + self.assertEqual(nlinfo.eliminated_vars[1], (m.x, 2)) + + self.assertEqual( + *nl_diff( + """g3 1 1 0 # problem unknown + 1 0 1 0 0 #vars, constraints, objectives, ranges, eqns + 0 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 1 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 0 1 #nonzeros in Jacobian, obj. gradient + 3 1 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +O0 0 #obj +o54 #sumlist +3 #(n) +o5 #^ +n2 +n2 +o5 #^ +o16 #- +v0 #z +n2 +o5 #^ +v0 #z +n2 +x0 #initial guess +r #1 ranges (rhs's) +b #1 bounds (on variables) +3 #z +k0 #intermediate Jacobian column lengths +G0 1 #obj +0 0 """, OUT.getvalue(), ) From 374fd7df4577b6eceb068b97c7bce41496a7f3e3 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Fri, 24 May 2024 19:37:04 -0400 Subject: [PATCH 1472/3044] Added logic to support slimmer create_model. I still need to debug one part. --- pyomo/contrib/doe/doe.py | 103 ++++++++++++++----- pyomo/contrib/doe/examples/reactor_design.py | 11 +- 2 files changed, 90 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 0718493a826..d8f1781051c 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -42,6 +42,7 @@ import inspect +import pyomo.contrib.parmest.utils as utils class CalculationMode(Enum): sequential_finite = "sequential_finite" @@ -115,6 +116,15 @@ def __init__( self.design_vars = design_vars self.create_model = create_model + # check if create model function conforms to the original + # Pyomo.DoE interface + model_option_arg = "model_option" in inspect.getfullargspec(self.create_model).args + mod_arg = "mod" in inspect.getfullargspec(self.create_model).args + if model_option_arg and mod_arg: + self._original_create_model_interface = True + else: + self._original_create_model_interface = False + if args is None: args = {} self.args = args @@ -423,10 +433,17 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): # dict for storing model outputs output_record = {} + # Deactivate any existing objective functions + for obj in mod.component_objects(pyo.Objective): + obj.deactivate() + # add zero (dummy/placeholder) objective function mod.Obj = pyo.Objective(expr=0, sense=pyo.minimize) - mod.pprint() + # convert params to vars + # print("self.param.keys():", self.param.keys()) + # mod = utils.convert_params_to_vars(mod, self.param.keys(), fix_vars=True) + # mod.pprint() # solve model square_result = self._solve_doe(mod, fix=True) @@ -495,15 +512,25 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): def _direct_kaug(self): # create model - mod = self.create_model(model_option=ModelOptionLib.parmest, **self.args) + if self._original_create_model_interface: + mod = self.create_model(model_option=ModelOptionLib.parmest, **self.args) + else: + mod = self.create_model(**self.args) # discretize if needed if self.discretize_model is not None: mod = self.discretize_model(mod, block=False) + # Deactivate any existing objective functions + for obj in mod.component_objects(pyo.Objective): + obj.deactivate() + # add zero (dummy/placeholder) objective function mod.Obj = pyo.Objective(expr=0, sense=pyo.minimize) + # convert params to vars + # mod = utils.convert_params_to_vars(mod, self.param.keys(), fix_vars=True) + # set ub and lb to parameters for par in self.param.keys(): cuid = pyo.ComponentUID(par) @@ -608,19 +635,37 @@ def _create_block(self): self.eps_abs = self.scenario_data.eps_abs self.scena_gen = scena_gen - # Create a global model - mod = pyo.ConcreteModel() - - # Set for block/scenarios - mod.scenario = pyo.Set(initialize=self.scenario_data.scenario_indices) - # Determine if create_model takes theta as an optional input pass_theta_to_initialize = ( "theta" in inspect.getfullargspec(self.create_model).args ) # Allow user to self-define complex design variables - self.create_model(mod=mod, model_option=ModelOptionLib.stage1, **self.args) + if self._original_create_model_interface: + + # Create a global model + mod = pyo.ConcreteModel() + + if pass_theta_to_initialize: + # Add model on block with theta values + self.create_model( + mod=mod, + model_option=ModelOptionLib.stage1, + theta=self.param, + **self.args, + ) + else: + # Add model on block without theta values + self.create_model(mod=mod, + model_option=ModelOptionLib.stage1, + **self.args) + + else: + # Create a global model + mod = self.create_model(**self.args) + + # Set for block/scenarios + mod.scenario = pyo.Set(initialize=self.scenario_data.scenario_indices) # Fix parameter values in the copy of the stage1 model (if they exist) for par in self.param: @@ -635,21 +680,33 @@ def block_build(b, s): # create block scenarios # idea: check if create_model takes theta as an optional input, if so, pass parameter values to create_model - if pass_theta_to_initialize: - # Grab the values of theta for this scenario/block - theta_initialize = self.scenario_data.scenario[s] - # Add model on block with theta values - self.create_model( - mod=b, - model_option=ModelOptionLib.stage2, - theta=theta_initialize, - **self.args, - ) + # TODO: Check if this is correct syntax for adding a model to a block + + if self._original_create_model_interface: + if pass_theta_to_initialize: + # Grab the values of theta for this scenario/block + theta_initialize = self.scenario_data.scenario[s] + # Add model on block with theta values + self.create_model( + mod=b, + model_option=ModelOptionLib.stage2, + theta=theta_initialize, + **self.args, + ) + else: + # Otherwise add model on block without theta values + self.create_model( + mod=b, model_option=ModelOptionLib.stage2, **self.args + ) else: - # Otherwise add model on block without theta values - self.create_model( - mod=b, model_option=ModelOptionLib.stage2, **self.args - ) + # Add model on block + if pass_theta_to_initialize: + # Grab the values of theta for this scenario/block + theta_initialize = self.scenario_data.scenario[s] + # This syntax is not yet correct :( + b = self.create_model(theta=theta_initialize, **self.args) + else: + b = self.create_model(**self.args) # fix parameter values to perturbed values for par in self.param: diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 450a2800ae1..4fbc979041e 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -40,8 +40,10 @@ from pyomo.common.dependencies import numpy as np -def create_model(mod=None, model_option="stage2"): +def create_model(): + # This is the old Pyomo.DoE interface + ''' model_option = ModelOptionLib(model_option) model = mod @@ -59,6 +61,10 @@ def create_model(mod=None, model_option="stage2"): raise ValueError( "model_option needs to be defined as parmest, stage1, or stage2." ) + ''' + + # This is the streamlined Pyomo.DoE interface + model = pyo.ConcreteModel() # Rate constants model.k1 = pyo.Var(initialize=5.0 / 6.0, within=pyo.PositiveReals) # min^-1 @@ -102,8 +108,11 @@ def create_model(mod=None, model_option="stage2"): expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) ) + ''' if return_m: return model + ''' + return model def main(): From ffefdef8217e62b92c0e27f63d20dae50ab923e5 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Fri, 24 May 2024 21:27:01 -0400 Subject: [PATCH 1473/3044] Added and tested support for a "slim" create model interface. This will make the workshop examples much easier. --- pyomo/contrib/doe/doe.py | 21 +++++--- pyomo/contrib/doe/examples/reactor_design.py | 55 +++++++++++++------- pyomo/contrib/doe/tests/test_example.py | 14 +++-- 3 files changed, 61 insertions(+), 29 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index d8f1781051c..47fee913ba8 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -680,8 +680,6 @@ def block_build(b, s): # create block scenarios # idea: check if create_model takes theta as an optional input, if so, pass parameter values to create_model - # TODO: Check if this is correct syntax for adding a model to a block - if self._original_create_model_interface: if pass_theta_to_initialize: # Grab the values of theta for this scenario/block @@ -698,22 +696,28 @@ def block_build(b, s): self.create_model( mod=b, model_option=ModelOptionLib.stage2, **self.args ) + + # save block in a temporary variable + mod_ = b else: # Add model on block if pass_theta_to_initialize: # Grab the values of theta for this scenario/block theta_initialize = self.scenario_data.scenario[s] - # This syntax is not yet correct :( - b = self.create_model(theta=theta_initialize, **self.args) + mod_ = self.create_model(theta=theta_initialize, **self.args) else: - b = self.create_model(**self.args) + mod_ = self.create_model(**self.args) # fix parameter values to perturbed values for par in self.param: cuid = pyo.ComponentUID(par) - var = cuid.find_component_on(b) + var = cuid.find_component_on(mod_) var.fix(self.scenario_data.scenario[s][par]) + if not self._original_create_model_interface: + # for the "new"/"slim" interface, we need to add the block to the model + return mod_ + mod.block = pyo.Block(mod.scenario, rule=block_build) # discretize the model @@ -1377,7 +1381,10 @@ def _solve_doe(self, m, fix=False, opt_option=None): # either fix or unfix the design variables mod = self._fix_design( - m, self.design_values, fix_opt=fix, optimize_option=opt_option + m, + self.design_values, + fix_opt=fix, + optimize_option=opt_option ) # if user gives solver, use this solver. if not, use default IPOPT solver diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 4fbc979041e..5c1ecb8a79d 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -40,10 +40,7 @@ from pyomo.common.dependencies import numpy as np -def create_model(): - - # This is the old Pyomo.DoE interface - ''' +def create_model_legacy(mod=None, model_option=None): model_option = ModelOptionLib(model_option) model = mod @@ -61,10 +58,18 @@ def create_model(): raise ValueError( "model_option needs to be defined as parmest, stage1, or stage2." ) - ''' + + model = _create_model_details(model) + + if return_m: + return model + - # This is the streamlined Pyomo.DoE interface +def create_model(): model = pyo.ConcreteModel() + return _create_model_details(model) + +def _create_model_details(model): # Rate constants model.k1 = pyo.Var(initialize=5.0 / 6.0, within=pyo.PositiveReals) # min^-1 @@ -108,14 +113,10 @@ def create_model(): expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) ) - ''' - if return_m: - return model - ''' return model -def main(): +def main(legacy_create_model_interface=False): # measurement object measurements = MeasurementVariables() @@ -145,32 +146,50 @@ def main(): theta_values = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} + if legacy_create_model_interface: + create_model_ = create_model_legacy + else: + create_model_ = create_model + doe1 = DesignOfExperiments( - theta_values, exp_design, measurements, create_model, prior_FIM=None + theta_values, + exp_design, + measurements, + create_model_, + prior_FIM=None ) + + result = doe1.compute_FIM( mode="sequential_finite", # calculation mode scale_nominal_param_value=True, # scale nominal parameter value formula="central", # formula for finite difference ) + doe1.model.pprint() + result.result_analysis() + # print("FIM =\n",result.FIM) + # print("jac =\n",result.jaco_information) # print("log10 Trace of FIM: ", np.log10(result.trace)) # print("log10 Determinant of FIM: ", np.log10(result.det)) # test result - relative_error_trace = abs(np.log10(result.trace) - 6.815) + expected_log10_trace = 6.815 + log10_trace = np.log10(result.trace) + relative_error_trace = abs(log10_trace - 6.815) assert ( relative_error_trace < 0.01 - ), "log10(tr(FIM)) regression test failed, answer does not match previous result" + ), "log10(tr(FIM)) regression test failed, answer "+str(round(log10_trace,3))+" does not match expected answer of "+str(expected_log10_trace) - relative_error_det = abs(np.log10(result.det) - 18.719) + expected_log10_det = 18.719 + log10_det = np.log10(result.det) + relative_error_det = abs(log10_det - 18.719) assert ( relative_error_det < 0.01 - ), "log10(det(FIM)) regression test failed, answer does not match previous result" - + ), "log10(det(FIM)) regression test failed, answer "+str(round(log10_det,3))+" does not match expected answer of "+str(expected_log10_det) if __name__ == "__main__": - main() + main(legacy_create_model_interface=False) diff --git a/pyomo/contrib/doe/tests/test_example.py b/pyomo/contrib/doe/tests/test_example.py index c92725efd3b..d9fb5e39ed4 100644 --- a/pyomo/contrib/doe/tests/test_example.py +++ b/pyomo/contrib/doe/tests/test_example.py @@ -68,11 +68,17 @@ def test_reactor_grid_search(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_reactor_design(self): + def test_reactor_design_slim_create_model_interface(self): from pyomo.contrib.doe.examples import reactor_design - - reactor_design.main() - + reactor_design.main(legacy_create_model_interface=False) + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not pandas_available, "pandas is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + + def test_reactor_design_legacy_create_model_interface(self): + from pyomo.contrib.doe.examples import reactor_design + reactor_design.main(legacy_create_model_interface=True) if __name__ == "__main__": unittest.main() From 6b43724ce21480163e3f31589b001cf3c129d9ca Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Fri, 24 May 2024 21:39:41 -0400 Subject: [PATCH 1474/3044] Ran black --- pyomo/contrib/doe/doe.py | 16 ++++----- pyomo/contrib/doe/examples/reactor_design.py | 34 +++++++++++--------- pyomo/contrib/doe/tests/test_example.py | 6 ++-- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 47fee913ba8..4ae57bb1030 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -44,6 +44,7 @@ import pyomo.contrib.parmest.utils as utils + class CalculationMode(Enum): sequential_finite = "sequential_finite" direct_kaug = "direct_kaug" @@ -118,7 +119,9 @@ def __init__( # check if create model function conforms to the original # Pyomo.DoE interface - model_option_arg = "model_option" in inspect.getfullargspec(self.create_model).args + model_option_arg = ( + "model_option" in inspect.getfullargspec(self.create_model).args + ) mod_arg = "mod" in inspect.getfullargspec(self.create_model).args if model_option_arg and mod_arg: self._original_create_model_interface = True @@ -656,9 +659,9 @@ def _create_block(self): ) else: # Add model on block without theta values - self.create_model(mod=mod, - model_option=ModelOptionLib.stage1, - **self.args) + self.create_model( + mod=mod, model_option=ModelOptionLib.stage1, **self.args + ) else: # Create a global model @@ -1381,10 +1384,7 @@ def _solve_doe(self, m, fix=False, opt_option=None): # either fix or unfix the design variables mod = self._fix_design( - m, - self.design_values, - fix_opt=fix, - optimize_option=opt_option + m, self.design_values, fix_opt=fix, optimize_option=opt_option ) # if user gives solver, use this solver. if not, use default IPOPT solver diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 5c1ecb8a79d..82aa33bb5a9 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -58,17 +58,18 @@ def create_model_legacy(mod=None, model_option=None): raise ValueError( "model_option needs to be defined as parmest, stage1, or stage2." ) - + model = _create_model_details(model) - + if return_m: return model - + def create_model(): model = pyo.ConcreteModel() return _create_model_details(model) + def _create_model_details(model): # Rate constants @@ -152,15 +153,9 @@ def main(legacy_create_model_interface=False): create_model_ = create_model doe1 = DesignOfExperiments( - theta_values, - exp_design, - measurements, - create_model_, - prior_FIM=None + theta_values, exp_design, measurements, create_model_, prior_FIM=None ) - - result = doe1.compute_FIM( mode="sequential_finite", # calculation mode scale_nominal_param_value=True, # scale nominal parameter value @@ -180,16 +175,23 @@ def main(legacy_create_model_interface=False): expected_log10_trace = 6.815 log10_trace = np.log10(result.trace) relative_error_trace = abs(log10_trace - 6.815) - assert ( - relative_error_trace < 0.01 - ), "log10(tr(FIM)) regression test failed, answer "+str(round(log10_trace,3))+" does not match expected answer of "+str(expected_log10_trace) + assert relative_error_trace < 0.01, ( + "log10(tr(FIM)) regression test failed, answer " + + str(round(log10_trace, 3)) + + " does not match expected answer of " + + str(expected_log10_trace) + ) expected_log10_det = 18.719 log10_det = np.log10(result.det) relative_error_det = abs(log10_det - 18.719) - assert ( - relative_error_det < 0.01 - ), "log10(det(FIM)) regression test failed, answer "+str(round(log10_det,3))+" does not match expected answer of "+str(expected_log10_det) + assert relative_error_det < 0.01, ( + "log10(det(FIM)) regression test failed, answer " + + str(round(log10_det, 3)) + + " does not match expected answer of " + + str(expected_log10_det) + ) + if __name__ == "__main__": main(legacy_create_model_interface=False) diff --git a/pyomo/contrib/doe/tests/test_example.py b/pyomo/contrib/doe/tests/test_example.py index d9fb5e39ed4..635bc3ed82e 100644 --- a/pyomo/contrib/doe/tests/test_example.py +++ b/pyomo/contrib/doe/tests/test_example.py @@ -70,15 +70,17 @@ def test_reactor_grid_search(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_design_slim_create_model_interface(self): from pyomo.contrib.doe.examples import reactor_design + reactor_design.main(legacy_create_model_interface=False) - + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_reactor_design_legacy_create_model_interface(self): from pyomo.contrib.doe.examples import reactor_design + reactor_design.main(legacy_create_model_interface=True) + if __name__ == "__main__": unittest.main() From 8cbc854580d3bc0e5871396da7943b3669fc2b85 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sat, 25 May 2024 19:59:38 -0400 Subject: [PATCH 1475/3044] Added optimization regression test for reactor design example --- pyomo/contrib/doe/examples/reactor_design.py | 47 ++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 82aa33bb5a9..3fc5d805c12 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -192,6 +192,53 @@ def main(legacy_create_model_interface=False): + str(expected_log10_det) ) + doe2 = DesignOfExperiments( + theta_values, + exp_design, + measurements, + create_model_, + prior_FIM=None + ) + + square_result2, optimize_result2 = doe2.stochastic_program( + if_optimize=True, + if_Cholesky=True, + scale_nominal_param_value=True, + objective_option="det", + jac_initial=result.jaco_information.copy(), + step = 0.1 + ) + + optimize_result2.result_analysis() + log_det = np.log(optimize_result2.det) + print("log(det) = ",round(log_det,3)) + log_det_expected = 45.199 + assert abs(log_det - log_det_expected) < 0.01, "log(det) regression test failed" + + doe3 = DesignOfExperiments( + theta_values, + exp_design, + measurements, + create_model_, + prior_FIM=None + ) + + square_result3, optimize_result3 = doe3.stochastic_program( + if_optimize=True, + scale_nominal_param_value=True, + objective_option="trace", + jac_initial=result.jaco_information.copy(), + step = 0.1 + ) + + optimize_result3.result_analysis() + log_trace = np.log(optimize_result3.trace) + log_trace_expected = 17.29 + print("log(trace) = ",round(log_trace,3)) + assert abs(log_trace - log_trace_expected) < 0.01, "log(trace) regression test failed" + + + if __name__ == "__main__": main(legacy_create_model_interface=False) From 27ea04185c739511dd04bb78dd823f8517e473f1 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sat, 25 May 2024 19:59:58 -0400 Subject: [PATCH 1476/3044] Ran black --- pyomo/contrib/doe/examples/reactor_design.py | 26 +++++++------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 3fc5d805c12..55e23c4a955 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -193,11 +193,7 @@ def main(legacy_create_model_interface=False): ) doe2 = DesignOfExperiments( - theta_values, - exp_design, - measurements, - create_model_, - prior_FIM=None + theta_values, exp_design, measurements, create_model_, prior_FIM=None ) square_result2, optimize_result2 = doe2.stochastic_program( @@ -206,21 +202,17 @@ def main(legacy_create_model_interface=False): scale_nominal_param_value=True, objective_option="det", jac_initial=result.jaco_information.copy(), - step = 0.1 + step=0.1, ) optimize_result2.result_analysis() log_det = np.log(optimize_result2.det) - print("log(det) = ",round(log_det,3)) + print("log(det) = ", round(log_det, 3)) log_det_expected = 45.199 assert abs(log_det - log_det_expected) < 0.01, "log(det) regression test failed" doe3 = DesignOfExperiments( - theta_values, - exp_design, - measurements, - create_model_, - prior_FIM=None + theta_values, exp_design, measurements, create_model_, prior_FIM=None ) square_result3, optimize_result3 = doe3.stochastic_program( @@ -228,17 +220,17 @@ def main(legacy_create_model_interface=False): scale_nominal_param_value=True, objective_option="trace", jac_initial=result.jaco_information.copy(), - step = 0.1 + step=0.1, ) optimize_result3.result_analysis() log_trace = np.log(optimize_result3.trace) log_trace_expected = 17.29 - print("log(trace) = ",round(log_trace,3)) - assert abs(log_trace - log_trace_expected) < 0.01, "log(trace) regression test failed" + print("log(trace) = ", round(log_trace, 3)) + assert ( + abs(log_trace - log_trace_expected) < 0.01 + ), "log(trace) regression test failed" - - if __name__ == "__main__": main(legacy_create_model_interface=False) From 9bed23cc6aaf42f458b6c53423ba1f1743f052c2 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sat, 25 May 2024 20:05:06 -0400 Subject: [PATCH 1477/3044] Easiest implementation of exploiting symmetry. For the reactor design example, this did not change the D-opt iterations but reduced the number needed for A-opt from ~84 to ~54. --- pyomo/contrib/doe/doe.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 4ae57bb1030..9033ed28cf7 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1175,17 +1175,21 @@ def fim_rule(m, p, q): p: parameter q: parameter """ - return ( - m.fim[p, q] - == sum( - 1 - / self.measurement_vars.variance[n] - * m.sensitivity_jacobian[p, n] - * m.sensitivity_jacobian[q, n] - for n in model.measured_variables + + if p > q: + return m.fim[p, q] == m.fim[q, p] + else: + return ( + m.fim[p, q] + == sum( + 1 + / self.measurement_vars.variance[n] + * m.sensitivity_jacobian[p, n] + * m.sensitivity_jacobian[q, n] + for n in model.measured_variables + ) + + m.priorFIM[p, q] * self.fim_scale_constant_value ) - + m.priorFIM[p, q] * self.fim_scale_constant_value - ) model.jacobian_constraint = pyo.Constraint( model.regression_parameters, model.measured_variables, rule=jacobian_rule From bfa597e9b920cf6a7cb9acee5f3650af9fb82b4f Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sat, 25 May 2024 20:39:13 -0400 Subject: [PATCH 1478/3044] Added option to only compute lower elements of FIM. --- pyomo/contrib/doe/doe.py | 19 +++++++++++++++++-- pyomo/contrib/doe/examples/reactor_design.py | 5 +++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 9033ed28cf7..752479be269 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -74,6 +74,7 @@ def __init__( discretize_model=None, args=None, logger_level=logging.INFO, + only_compute_fim_lower=True, ): """ This package enables model-based design of experiments analysis with Pyomo. @@ -106,6 +107,8 @@ def __init__( Additional arguments for the create_model function. logger_level: Specify the level of the logger. Change to logging.DEBUG for all messages. + only_compute_fim_lower: + If True, only the lower triangle of the FIM is computed. Default is True. """ # parameters @@ -157,6 +160,8 @@ def __init__( self.logger = logging.getLogger(__name__) self.logger.setLevel(level=logger_level) + self.only_compute_fim_lower = only_compute_fim_lower + def _check_inputs(self): """ Check if the prior FIM is N*N matrix, where N is the number of parameter @@ -342,6 +347,7 @@ def compute_FIM( extract_single_model=None, formula="central", step=0.001, + only_compute_fim_lower=False, ): """ This function calculates the Fisher information matrix (FIM) using sensitivity information obtained @@ -1019,6 +1025,12 @@ def _create_doe_model(self, no_obj=True): ------- model: the DOE model """ + + # Developer recommendation: use the Cholesky decomposition for D-optimality + # The explicit formula is available for benchmarking purposes and is NOT recommended + if self.only_compute_fim_lower and self.objective_option == ObjectiveLib.det and not self.Cholesky_option: + raise ValueError("Cannot compute determinant with explicit formula if only_compute_fim_lower is True.") + model = self._create_block() # variables for jacobian and FIM @@ -1177,7 +1189,10 @@ def fim_rule(m, p, q): """ if p > q: - return m.fim[p, q] == m.fim[q, p] + if self.only_compute_fim_lower: + return pyo.Constraint.Skip + else: + return m.fim[p, q] == m.fim[q, p] else: return ( m.fim[p, q] @@ -1260,7 +1275,7 @@ def trace_calc(m): return m.trace == sum(m.fim[j, j] for j in m.regression_parameters) def det_general(m): - r"""Calculate determinant. Can be applied to FIM of any size. + """Calculate determinant. Can be applied to FIM of any size. det(A) = sum_{\sigma \in \S_n} (sgn(\sigma) * \Prod_{i=1}^n a_{i,\sigma_i}) Use permutation() to get permutations, sgn() to get signature """ diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 55e23c4a955..0fed262d74f 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -162,7 +162,7 @@ def main(legacy_create_model_interface=False): formula="central", # formula for finite difference ) - doe1.model.pprint() + # doe1.model.pprint() result.result_analysis() @@ -208,7 +208,8 @@ def main(legacy_create_model_interface=False): optimize_result2.result_analysis() log_det = np.log(optimize_result2.det) print("log(det) = ", round(log_det, 3)) - log_det_expected = 45.199 + #log_det_expected = 45.199 + log_det_expected = 44.362 assert abs(log_det - log_det_expected) < 0.01, "log(det) regression test failed" doe3 = DesignOfExperiments( From c7c47a92b493ade3ea512a9e53f025c90042415b Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sat, 25 May 2024 20:48:15 -0400 Subject: [PATCH 1479/3044] Changed objective scaling from log to log10. This is easier to interpret. --- pyomo/contrib/doe/doe.py | 6 +++--- pyomo/contrib/doe/examples/reactor_design.py | 9 ++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 752479be269..ef4a7c7c194 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1311,7 +1311,7 @@ def det_general(m): m.regression_parameters, m.regression_parameters, rule=cholesky_imp ) m.Obj = pyo.Objective( - expr=2 * sum(pyo.log(m.L_ele[j, j]) for j in m.regression_parameters), + expr=2 * sum(pyo.log10(m.L_ele[j, j]) for j in m.regression_parameters), sense=pyo.maximize, ) @@ -1319,13 +1319,13 @@ def det_general(m): # if not cholesky but determinant, calculating det and evaluate the OBJ with det m.det = pyo.Var(initialize=np.linalg.det(fim), bounds=(small_number, None)) m.det_rule = pyo.Constraint(rule=det_general) - m.Obj = pyo.Objective(expr=pyo.log(m.det), sense=pyo.maximize) + m.Obj = pyo.Objective(expr=pyo.log10(m.det), sense=pyo.maximize) elif self.objective_option == ObjectiveLib.trace: # if not determinant or cholesky, calculating the OBJ with trace m.trace = pyo.Var(initialize=np.trace(fim), bounds=(small_number, None)) m.trace_rule = pyo.Constraint(rule=trace_calc) - m.Obj = pyo.Objective(expr=pyo.log(m.trace), sense=pyo.maximize) + m.Obj = pyo.Objective(expr=pyo.log10(m.trace), sense=pyo.maximize) # m.Obj = pyo.Objective(expr=m.trace, sense=pyo.maximize) elif self.objective_option == ObjectiveLib.zero: diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py index 0fed262d74f..67d6ff02fd2 100644 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ b/pyomo/contrib/doe/examples/reactor_design.py @@ -206,10 +206,9 @@ def main(legacy_create_model_interface=False): ) optimize_result2.result_analysis() - log_det = np.log(optimize_result2.det) + log_det = np.log10(optimize_result2.det) print("log(det) = ", round(log_det, 3)) - #log_det_expected = 45.199 - log_det_expected = 44.362 + log_det_expected = 19.266 assert abs(log_det - log_det_expected) < 0.01, "log(det) regression test failed" doe3 = DesignOfExperiments( @@ -225,8 +224,8 @@ def main(legacy_create_model_interface=False): ) optimize_result3.result_analysis() - log_trace = np.log(optimize_result3.trace) - log_trace_expected = 17.29 + log_trace = np.log10(optimize_result3.trace) + log_trace_expected = 7.509 print("log(trace) = ", round(log_trace, 3)) assert ( abs(log_trace - log_trace_expected) < 0.01 From 48350655cd1b2e0b15909e976dfd1ddb097ada7d Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sat, 25 May 2024 20:49:15 -0400 Subject: [PATCH 1480/3044] Ran black --- pyomo/contrib/doe/doe.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index ef4a7c7c194..d7d7cbd1395 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1028,8 +1028,14 @@ def _create_doe_model(self, no_obj=True): # Developer recommendation: use the Cholesky decomposition for D-optimality # The explicit formula is available for benchmarking purposes and is NOT recommended - if self.only_compute_fim_lower and self.objective_option == ObjectiveLib.det and not self.Cholesky_option: - raise ValueError("Cannot compute determinant with explicit formula if only_compute_fim_lower is True.") + if ( + self.only_compute_fim_lower + and self.objective_option == ObjectiveLib.det + and not self.Cholesky_option + ): + raise ValueError( + "Cannot compute determinant with explicit formula if only_compute_fim_lower is True." + ) model = self._create_block() From 67afe32c30d89c12efadd2922b2af0f4f944821f Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Sun, 26 May 2024 15:32:41 -0400 Subject: [PATCH 1481/3044] Divided reaction kinetics unit tests. Changed the assert statements to check the objective instead of the optimal solution. --- pyomo/contrib/doe/tests/test_example.py | 2 +- .../contrib/doe/tests/test_reactor_example.py | 204 +++++++++--------- 2 files changed, 109 insertions(+), 97 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_example.py b/pyomo/contrib/doe/tests/test_example.py index 635bc3ed82e..e4ffbe89142 100644 --- a/pyomo/contrib/doe/tests/test_example.py +++ b/pyomo/contrib/doe/tests/test_example.py @@ -41,7 +41,7 @@ ipopt_available = SolverFactory("ipopt").available() -class TestReactorExample(unittest.TestCase): +class TestReactorExamples(unittest.TestCase): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not scipy_available, "scipy is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") diff --git a/pyomo/contrib/doe/tests/test_reactor_example.py b/pyomo/contrib/doe/tests/test_reactor_example.py index 3fca93b5ded..d8d28a03d76 100644 --- a/pyomo/contrib/doe/tests/test_reactor_example.py +++ b/pyomo/contrib/doe/tests/test_reactor_example.py @@ -37,10 +37,9 @@ ipopt_available = SolverFactory("ipopt").available() -class Test_example_options(unittest.TestCase): - """Test the three options in the kinetics example.""" - - def test_setUP(self): +class Test_Reaction_Kinetics_Example(unittest.TestCase): + def test_reaction_kinetics_create_model(self): + """Test the three options in the kinetics example.""" # parmest option mod = create_model(model_option="parmest") @@ -56,16 +55,116 @@ def test_setUP(self): create_model(model_option="stage2") with self.assertRaises(ValueError): - create_model(model_option="NotDefine") + create_model(model_option="NotDefined") + + @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + @unittest.skipIf(not pandas_available, "Pandas is not available") + def test_kinetics_example_sequential_finite_then_optimize(self): + """Test the kinetics example with sequential_finite mode and then optimization""" + doe_object = self.specify_reaction_kinetics() + + # Test FIM calculation at nominal values + sensi_opt = "sequential_finite" + result = doe_object.compute_FIM( + mode=sensi_opt, scale_nominal_param_value=True, formula="central" + ) + result.result_analysis() + self.assertAlmostEqual(np.log10(result.trace), 2.7885, places=2) + self.assertAlmostEqual(np.log10(result.det), 2.8218, places=2) + self.assertAlmostEqual(np.log10(result.min_eig), -1.0123, places=2) + + ### check subset feature + sub_name = "C" + sub_indices = {0: ["CB", "CC"], 1: [0.125, 0.25, 0.5, 0.75, 0.875]} + + measure_subset = MeasurementVariables() + measure_subset.add_variables( + sub_name, indices=sub_indices, time_index_position=1 + ) + sub_result = result.subset(measure_subset) + sub_result.result_analysis() + self.assertAlmostEqual(np.log10(sub_result.trace), 2.5535, places=2) + self.assertAlmostEqual(np.log10(sub_result.det), 1.3464, places=2) + self.assertAlmostEqual(np.log10(sub_result.min_eig), -1.5386, places=2) -class Test_doe_object(unittest.TestCase): - """Test the kinetics example with both the sequential_finite mode and the direct_kaug mode""" + ### Test stochastic_program mode + # Prior information (scaled FIM with T=500 and T=300 experiments) + prior = np.asarray( + [ + [28.67892806, 5.41249739, -81.73674601, -24.02377324], + [5.41249739, 26.40935036, -12.41816477, -139.23992532], + [-81.73674601, -12.41816477, 240.46276004, 58.76422806], + [-24.02377324, -139.23992532, 58.76422806, 767.25584508], + ] + ) + doe_object2 = self.specify_reaction_kinetics(prior=prior) + + square_result, optimize_result = doe_object2.stochastic_program( + if_optimize=True, + if_Cholesky=True, + scale_nominal_param_value=True, + objective_option="det", + L_initial=np.linalg.cholesky(prior), + jac_initial=result.jaco_information.copy(), + tee_opt=True, + ) + + optimize_result.result_analysis() + ## 2024-May-26: changing this to test the objective instead of the optimal solution + ## It's possible the objective is flat and the optimal solution is not unique + # self.assertAlmostEqual(value(optimize_result.model.CA0[0]), 5.0, places=2) + # self.assertAlmostEqual(value(optimize_result.model.T[0.5]), 300, places=2) + self.assertAlmostEqual(np.log10(optimize_result.det), 5.744, places=2) + + square_result, optimize_result = doe_object2.stochastic_program( + if_optimize=True, + scale_nominal_param_value=True, + objective_option="trace", + jac_initial=result.jaco_information.copy(), + tee_opt=True, + ) + + optimize_result.result_analysis() + ## 2024-May-26: changing this to test the objective instead of the optimal solution + ## It's possible the objective is flat and the optimal solution is not unique + # self.assertAlmostEqual(value(optimize_result.model.CA0[0]), 5.0, places=2) + # self.assertAlmostEqual(value(optimize_result.model.T[0.5]), 300, places=2) + self.assertAlmostEqual(np.log10(optimize_result.trace), 3.340, places=2) @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") @unittest.skipIf(not pandas_available, "Pandas is not available") - def test_setUP(self): + def test_kinetics_example_direct_k_aug(self): + doe_object = self.specify_reaction_kinetics() + + # Test FIM calculation at nominal values + sensi_opt = "direct_kaug" + result = doe_object.compute_FIM( + mode=sensi_opt, scale_nominal_param_value=True, formula="central" + ) + result.result_analysis() + self.assertAlmostEqual(np.log10(result.trace), 2.7211, places=2) + self.assertAlmostEqual(np.log10(result.det), 2.0845, places=2) + self.assertAlmostEqual(np.log10(result.min_eig), -1.3510, places=2) + + ### check subset feature + sub_name = "C" + sub_indices = {0: ["CB", "CC"], 1: [0.125, 0.25, 0.5, 0.75, 0.875]} + + measure_subset = MeasurementVariables() + measure_subset.add_variables( + sub_name, indices=sub_indices, time_index_position=1 + ) + sub_result = result.subset(measure_subset) + sub_result.result_analysis() + + self.assertAlmostEqual(np.log10(sub_result.trace), 2.5535, places=2) + self.assertAlmostEqual(np.log10(sub_result.det), 1.3464, places=2) + self.assertAlmostEqual(np.log10(sub_result.min_eig), -1.5386, places=2) + + def specify_reaction_kinetics(self, prior=None): ### Define inputs # Control time set [h] t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] @@ -111,9 +210,6 @@ def test_setUP(self): upper_bounds=700, ) - ### Test sequential_finite mode - sensi_opt = "sequential_finite" - design_names = exp_design.variable_names exp1 = [5, 570, 300, 300, 300, 300, 300, 300, 300, 300] exp1_design_dict = dict(zip(design_names, exp1)) @@ -126,94 +222,10 @@ def test_setUP(self): measurements, create_model, discretize_model=disc_for_measure, - ) - - result = doe_object.compute_FIM( - mode=sensi_opt, scale_nominal_param_value=True, formula="central" - ) - - result.result_analysis() - - self.assertAlmostEqual(np.log10(result.trace), 2.7885, places=2) - self.assertAlmostEqual(np.log10(result.det), 2.8218, places=2) - self.assertAlmostEqual(np.log10(result.min_eig), -1.0123, places=2) - - ### check subset feature - sub_name = "C" - sub_indices = {0: ["CB", "CC"], 1: [0.125, 0.25, 0.5, 0.75, 0.875]} - - measure_subset = MeasurementVariables() - measure_subset.add_variables( - sub_name, indices=sub_indices, time_index_position=1 - ) - sub_result = result.subset(measure_subset) - sub_result.result_analysis() - - self.assertAlmostEqual(np.log10(sub_result.trace), 2.5535, places=2) - self.assertAlmostEqual(np.log10(sub_result.det), 1.3464, places=2) - self.assertAlmostEqual(np.log10(sub_result.min_eig), -1.5386, places=2) - - ### Test direct_kaug mode - sensi_opt = "direct_kaug" - # Define a new experiment - - exp1 = [5, 570, 400, 300, 300, 300, 300, 300, 300, 300] - exp1_design_dict = dict(zip(design_names, exp1)) - exp_design.update_values(exp1_design_dict) - - doe_object = DesignOfExperiments( - parameter_dict, - exp_design, - measurements, - create_model, - discretize_model=disc_for_measure, - ) - - result = doe_object.compute_FIM( - mode=sensi_opt, scale_nominal_param_value=True, formula="central" - ) - - result.result_analysis() - - self.assertAlmostEqual(np.log10(result.trace), 2.7211, places=2) - self.assertAlmostEqual(np.log10(result.det), 2.0845, places=2) - self.assertAlmostEqual(np.log10(result.min_eig), -1.3510, places=2) - - ### Test stochastic_program mode - - exp1 = [5, 570, 300, 300, 300, 300, 300, 300, 300, 300] - exp1_design_dict = dict(zip(design_names, exp1)) - exp_design.update_values(exp1_design_dict) - - # add a prior information (scaled FIM with T=500 and T=300 experiments) - prior = np.asarray( - [ - [28.67892806, 5.41249739, -81.73674601, -24.02377324], - [5.41249739, 26.40935036, -12.41816477, -139.23992532], - [-81.73674601, -12.41816477, 240.46276004, 58.76422806], - [-24.02377324, -139.23992532, 58.76422806, 767.25584508], - ] - ) - - doe_object2 = DesignOfExperiments( - parameter_dict, - exp_design, - measurements, - create_model, prior_FIM=prior, - discretize_model=disc_for_measure, - ) - - square_result, optimize_result = doe_object2.stochastic_program( - if_optimize=True, - if_Cholesky=True, - scale_nominal_param_value=True, - objective_option="det", - L_initial=np.linalg.cholesky(prior), ) - self.assertAlmostEqual(value(optimize_result.model.CA0[0]), 5.0, places=2) - self.assertAlmostEqual(value(optimize_result.model.T[0.5]), 300, places=2) + return doe_object if __name__ == "__main__": From 94e8ebfdb28fd1afa15b688d704e5cd7086b3a16 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 27 May 2024 09:41:57 -0400 Subject: [PATCH 1482/3044] Fix elements above the diagonal of FIM; this ensures the correct number of degrees of freedom. --- pyomo/contrib/doe/doe.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index d7d7cbd1395..7ac754aaa3d 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1182,11 +1182,7 @@ def read_prior(m, i, j): model.regression_parameters, model.regression_parameters, rule=read_prior ) - # TODO: explore exploiting the symmetry of the FIM matrix # The off-diagonal elements are symmetric, thus only half of the elements need to be calculated - # Syntax challenge: determine the order of p and q, i.e., if p > q, then replace with - # equality constraint fim[p, q] == fim[q, p] - def fim_rule(m, p, q): """ m: Pyomo model @@ -1219,6 +1215,15 @@ def fim_rule(m, p, q): model.regression_parameters, model.regression_parameters, rule=fim_rule ) + if self.only_compute_fim_lower: + # Fix the upper half of the FIM matrix elements to be 0.0. + # This eliminates extra variables and ensures the expected number of + # degrees of freedom in the optimization problem. + for p in model.regression_parameters: + for q in model.regression_parameters: + if p > q: + model.fim[p, q].fix(0.0) + return model def _add_objective(self, m): From e80d74f6e72db362bcc3afe56264b1699fc518c5 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 27 May 2024 09:45:17 -0400 Subject: [PATCH 1483/3044] Updated test. --- pyomo/contrib/doe/tests/test_reactor_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/tests/test_reactor_example.py b/pyomo/contrib/doe/tests/test_reactor_example.py index d8d28a03d76..aba20c77fa6 100644 --- a/pyomo/contrib/doe/tests/test_reactor_example.py +++ b/pyomo/contrib/doe/tests/test_reactor_example.py @@ -145,7 +145,7 @@ def test_kinetics_example_direct_k_aug(self): mode=sensi_opt, scale_nominal_param_value=True, formula="central" ) result.result_analysis() - self.assertAlmostEqual(np.log10(result.trace), 2.7211, places=2) + self.assertAlmostEqual(np.log10(result.trace), 2.789, places=2) self.assertAlmostEqual(np.log10(result.det), 2.0845, places=2) self.assertAlmostEqual(np.log10(result.min_eig), -1.3510, places=2) From c6655b677954c598a8dc14aa1d8c3f82ad57b035 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 27 May 2024 10:09:29 -0400 Subject: [PATCH 1484/3044] Updated test that uses k_aug. --- pyomo/contrib/doe/tests/test_reactor_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/tests/test_reactor_example.py b/pyomo/contrib/doe/tests/test_reactor_example.py index aba20c77fa6..d00b87d3b5f 100644 --- a/pyomo/contrib/doe/tests/test_reactor_example.py +++ b/pyomo/contrib/doe/tests/test_reactor_example.py @@ -146,7 +146,7 @@ def test_kinetics_example_direct_k_aug(self): ) result.result_analysis() self.assertAlmostEqual(np.log10(result.trace), 2.789, places=2) - self.assertAlmostEqual(np.log10(result.det), 2.0845, places=2) + self.assertAlmostEqual(np.log10(result.det), 2.8247, places=2) self.assertAlmostEqual(np.log10(result.min_eig), -1.3510, places=2) ### check subset feature From c262c5fe8defdac3b4090fca5d4e88722ab4850b Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 27 May 2024 11:05:02 -0400 Subject: [PATCH 1485/3044] Hopefully, this is the final update to the k_aug test. --- pyomo/contrib/doe/tests/test_reactor_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/tests/test_reactor_example.py b/pyomo/contrib/doe/tests/test_reactor_example.py index d00b87d3b5f..19fb4e61820 100644 --- a/pyomo/contrib/doe/tests/test_reactor_example.py +++ b/pyomo/contrib/doe/tests/test_reactor_example.py @@ -147,7 +147,7 @@ def test_kinetics_example_direct_k_aug(self): result.result_analysis() self.assertAlmostEqual(np.log10(result.trace), 2.789, places=2) self.assertAlmostEqual(np.log10(result.det), 2.8247, places=2) - self.assertAlmostEqual(np.log10(result.min_eig), -1.3510, places=2) + self.assertAlmostEqual(np.log10(result.min_eig), -1.0112, places=2) ### check subset feature sub_name = "C" From e8c098e40bfa9cc80cb0eff50f20f2040d06cdc8 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Mon, 27 May 2024 18:20:32 -0400 Subject: [PATCH 1486/3044] Added error check if there are no measurements. This mistake came up a few times in the tutorial. --- pyomo/contrib/doe/doe.py | 8 ++++++++ pyomo/contrib/doe/examples/fim_doe_tutorial.ipynb | 1 + pyomo/contrib/doe/tests/test_fim_doe.py | 14 ++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 7ac754aaa3d..29e45ec4f5c 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -139,6 +139,14 @@ def __init__( self.measurement_vars = measurement_vars self.measure_name = self.measurement_vars.variable_names + if ( + self.measurement_vars.variable_names is None + or not self.measurement_vars.variable_names + ): + raise ValueError( + "There are no measurement variables. Check for a modeling mistake." + ) + # check if user-defined solver is given if solver: self.solver = solver diff --git a/pyomo/contrib/doe/examples/fim_doe_tutorial.ipynb b/pyomo/contrib/doe/examples/fim_doe_tutorial.ipynb index 12d5a610db4..36ec42fbe49 100644 --- a/pyomo/contrib/doe/examples/fim_doe_tutorial.ipynb +++ b/pyomo/contrib/doe/examples/fim_doe_tutorial.ipynb @@ -87,6 +87,7 @@ "if \"google.colab\" in sys.modules:\n", " !wget \"https://raw.githubusercontent.com/IDAES/idaes-pse/main/scripts/colab_helper.py\"\n", " import colab_helper\n", + "\n", " colab_helper.install_idaes()\n", " colab_helper.install_ipopt()\n", "\n", diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py index 05664b0a795..d9a8d60fdb4 100644 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ b/pyomo/contrib/doe/tests/test_fim_doe.py @@ -144,6 +144,20 @@ def test_non_integer_index_keys(self): time_index_position="time", ) + def test_no_measurements(self): + """This test confirms that an error is thrown when the user forgets to add any measurements. + + It's okay to have no decision variables. With no measurement variables, the FIM is the zero matrix. + This (no measurements) is a common user mistake. + """ + + with self.assertRaises(ValueError): + decisions = DesignVariables() + measurements = MeasurementVariables() + DesignOfExperiments( + {}, decisions, measurements, create_model, disc_for_measure + ) + class TestDesignError(unittest.TestCase): def test(self): From 4957eea10eeab0c63c61883eaa68023debbd5cc9 Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Tue, 28 May 2024 08:17:26 -0400 Subject: [PATCH 1487/3044] Removed some LaTeX syntax from doc string. --- pyomo/contrib/doe/doe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 29e45ec4f5c..5c7fe4f6e77 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1295,7 +1295,7 @@ def trace_calc(m): def det_general(m): """Calculate determinant. Can be applied to FIM of any size. - det(A) = sum_{\sigma \in \S_n} (sgn(\sigma) * \Prod_{i=1}^n a_{i,\sigma_i}) + det(A) = sum_{sigma in Sn} (sgn(sigma) * Prod_{i=1}^n a_{i,sigma_i}) Use permutation() to get permutations, sgn() to get signature """ r_list = list(range(len(m.regression_parameters))) From 746737b08dd9d4143e099b07b44ac70b2cf89d15 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 28 May 2024 07:51:18 -0600 Subject: [PATCH 1488/3044] Support simplifying/eliminating constant defined variables --- pyomo/repn/plugins/nl_writer.py | 27 ++++++++++++++++++--------- pyomo/repn/tests/ampl/test_nlv2.py | 10 ++++------ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 9d66b37a429..21d64df2a18 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -548,7 +548,7 @@ def __init__(self, ostream, rowstream, colstream, config): self.external_functions = {} self.used_named_expressions = set() self.var_map = {} - self.var_id_to_nl = None + self.var_id_to_nl = {} self.sorter = FileDeterminism_to_SortComponents(config.file_determinism) self.visitor = AMPLRepnVisitor( self.template, @@ -1056,8 +1056,8 @@ def write(self, model): row_comments = [f'\t#{lbl}' for lbl in row_labels] col_labels = [labeler(var_map[_id]) for _id in variables] col_comments = [f'\t#{lbl}' for lbl in col_labels] - self.var_id_to_nl = { - _id: f'v{var_idx}{col_comments[var_idx]}' + id2nl = { + _id: f'v{var_idx}{col_comments[var_idx]}\n' for var_idx, _id in enumerate(variables) } # Write out the .row and .col data @@ -1070,10 +1070,12 @@ def write(self, model): else: row_labels = row_comments = [''] * (n_cons + n_objs) col_labels = col_comments = [''] * len(variables) - self.var_id_to_nl = { - _id: f"v{var_idx}" for var_idx, _id in enumerate(variables) - } + id2nl = {_id: f"v{var_idx}\n" for var_idx, _id in enumerate(variables)} + if self.var_id_to_nl: + self.var_id_to_nl.update(id2nl) + else: + self.var_id_to_nl = id2nl _vmap = self.var_id_to_nl if scale_model: template = self.template @@ -1934,7 +1936,7 @@ def _linear_presolve( # nonlinear portion of a defined variable is a constant # expression. This may now be the case if all the variables in # the original nonlinear expression have been fixed. - for expr, info, _ in self.subexpression_cache.values(): + for _id, (expr, info, sub) in self.subexpression_cache.items(): if not info.nonlinear: continue nl, args = info.nonlinear @@ -1949,12 +1951,19 @@ def _linear_presolve( # guarantee that the user actually initialized the # variables. So, we will fall back on parsing the (now # constant) nonlinear fragment and evaluating it. - if info.linear is None: - info.linear = {} info.nonlinear = None info.const += _evaluate_constant_nl( nl % tuple(template.const % eliminated_vars[i].const for i in args) ) + if not info.linear: + # This has resolved to a constant: the ASL will fail for + # defined variables containing ONLY a constant. We + # need to substitute the constant directly into the + # original constraint/objective expression(s) + info.linear = {} + self.used_named_expressions.discard(_id) + self.var_id_to_nl[_id] = self.template.const % info.const + self.subexpression_cache[_id] = (expr, info, [None, None, True]) return eliminated_cons, eliminated_vars diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 09936b45bbc..b33bf6963dc 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2393,16 +2393,14 @@ def test_presolve_fixes_nl_defined_variables(self): 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) 1 0 #nonzeros in Jacobian, obj. gradient 2 1 #max name lengths: constraints, variables - 0 0 0 2 0 #common exprs: b,c,o,c1,o1 -V1 0 1 #nl(e) -n19 -V2 1 1 #e + 0 0 0 1 0 #common exprs: b,c,o,c1,o1 +V1 1 1 #e 0 1 -v1 #nl(e) +n19 C0 #c1 o2 #* n3 -v2 #e +v1 #e x0 #initial guess r #1 ranges (rhs's) 2 0 #c1 From 6aa55f2bf5990eae0549cd1b3cd03cb0e8f102ef Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 28 May 2024 07:52:10 -0600 Subject: [PATCH 1489/3044] Simplify handling of NL file newlines around var identifiers --- pyomo/repn/plugins/nl_writer.py | 17 ++++++++++------- pyomo/repn/tests/ampl/test_nlv2.py | 28 ++++++++++++++-------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 21d64df2a18..e542a177247 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1097,9 +1097,7 @@ def write(self, model): ub *= scale var_bounds[_id] = lb, ub # Update _vmap to output scaled variables in NL expressions - _vmap[_id] = ( - template.division + _vmap[_id] + '\n' + template.const % scale - ).rstrip() + _vmap[_id] = template.division + _vmap[_id] + template.const % scale # Update any eliminated variables to point to the (potentially # scaled) substituted variables @@ -1133,7 +1131,7 @@ def write(self, model): "linear subsystem that was removed from the model. " f"Setting '{var_map[_i]}' == {val}" ) - _vmap[_id] = nl.rstrip() % tuple(_vmap[_i] for _i in args) + _vmap[_id] = nl % tuple(_vmap[_i] for _i in args) r_lines = [None] * n_cons for idx, (con, expr_info, lb, ub) in enumerate(constraints): @@ -2020,7 +2018,7 @@ def _write_v_line(self, expr_id, k): lbl = '\t#%s' % info[0].name else: lbl = '' - self.var_id_to_nl[expr_id] = f"v{self.next_V_line_id}{lbl}" + self.var_id_to_nl[expr_id] = f"v{self.next_V_line_id}{lbl}\n" # Do NOT write out 0 coefficients here: doing so fouls up the # ASL's logic for calculating derivatives, leading to 'nan' in # the Hessian results. @@ -2348,7 +2346,9 @@ class text_nl_debug_template(object): less_equal = 'o23\t# le\n' equality = 'o24\t# eq\n' external_fcn = 'f%d %d%s\n' - var = '%s\n' # NOTE: to support scaling, we do NOT include the 'v' here + # NOTE: to support scaling and substitutions, we do NOT include the + # 'v' or the EOL here: + var = '%s' const = 'n%r\n' string = 'h%d:%s\n' monomial = product + const + var.replace('%', '%%') @@ -2392,7 +2392,10 @@ class text_nl_debug_template(object): def _strip_template_comments(vars_, base_): - vars_['unary'] = {k: v[: v.find('\t#')] + '\n' for k, v in base_.unary.items()} + vars_['unary'] = { + k: v[: v.find('\t#')] + '\n' if v[-1] == '\n' else '' + for k, v in base_.unary.items() + } for k, v in base_.__dict__.items(): if type(v) is str and '\t#' in v: v_lines = v.split('\n') diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index b33bf6963dc..9318f1c5d0f 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -98,7 +98,7 @@ def test_divide(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o5\n%s\nn2\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o5\n%sn2\n', [id(m.x)])) m.p = 2 @@ -150,7 +150,7 @@ def test_divide(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o2\nn0.5\no5\n%s\nn2\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o2\nn0.5\no5\n%sn2\n', [id(m.x)])) info = INFO() with LoggingIntercept() as LOG: @@ -160,7 +160,7 @@ def test_divide(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o3\no43\n%s\n%s\n', [id(m.x), id(m.x)])) + self.assertEqual(repn.nonlinear, ('o3\no43\n%s%s', [id(m.x), id(m.x)])) def test_errors_divide_by_0(self): m = ConcreteModel() @@ -255,7 +255,7 @@ def test_pow(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o5\n%s\nn2\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o5\n%sn2\n', [id(m.x)])) m.p = 1 info = INFO() @@ -542,7 +542,7 @@ def test_errors_propagate_nan(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, InvalidNumber(None)) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear[0], 'o16\no2\no2\n%s\n%s\n%s\n') + self.assertEqual(repn.nonlinear[0], 'o16\no2\no2\n%s%s%s') self.assertEqual(repn.nonlinear[1], [id(m.z[2]), id(m.z[3]), id(m.z[4])]) m.z[3].fix(float('nan')) @@ -592,7 +592,7 @@ def test_eval_pow(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o5\n%s\nn0.5\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o5\n%sn0.5\n', [id(m.x)])) m.x.fix() info = INFO() @@ -617,7 +617,7 @@ def test_eval_abs(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o15\n%s\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o15\n%s', [id(m.x)])) m.x.fix() info = INFO() @@ -642,7 +642,7 @@ def test_eval_unary_func(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o43\n%s\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o43\n%s', [id(m.x)])) m.x.fix() info = INFO() @@ -671,7 +671,7 @@ def test_eval_expr_if_lessEq(self): self.assertEqual(repn.linear, {}) self.assertEqual( repn.nonlinear, - ('o35\no23\n%s\nn4\no5\n%s\nn2\n%s\n', [id(m.x), id(m.x), id(m.y)]), + ('o35\no23\n%sn4\no5\n%sn2\n%s', [id(m.x), id(m.x), id(m.y)]), ) m.x.fix() @@ -712,7 +712,7 @@ def test_eval_expr_if_Eq(self): self.assertEqual(repn.linear, {}) self.assertEqual( repn.nonlinear, - ('o35\no24\n%s\nn4\no5\n%s\nn2\n%s\n', [id(m.x), id(m.x), id(m.y)]), + ('o35\no24\n%sn4\no5\n%sn2\n%s', [id(m.x), id(m.x), id(m.y)]), ) m.x.fix() @@ -754,7 +754,7 @@ def test_eval_expr_if_ranged(self): self.assertEqual( repn.nonlinear, ( - 'o35\no21\no23\nn1\n%s\no23\n%s\nn4\no5\n%s\nn2\n%s\n', + 'o35\no21\no23\nn1\n%so23\n%sn4\no5\n%sn2\n%s', [id(m.x), id(m.x), id(m.x), id(m.y)], ), ) @@ -815,7 +815,7 @@ class CustomExpression(ScalarExpression): self.assertEqual(len(info.subexpression_cache), 1) obj, repn, info = info.subexpression_cache[id(m.e)] self.assertIs(obj, m.e) - self.assertEqual(repn.nl, ('%s\n', (id(m.e),))) + self.assertEqual(repn.nl, ('%s', (id(m.e),))) self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 3) self.assertEqual(repn.linear, {id(m.x): 1}) @@ -842,7 +842,7 @@ def test_nested_operator_zero_arg(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.nonlinear, ('o24\no3\nn1\n%s\nn0\n', [id(m.x)])) + self.assertEqual(repn.nonlinear, ('o24\no3\nn1\n%sn0\n', [id(m.x)])) def test_duplicate_shared_linear_expressions(self): # This tests an issue where AMPLRepn.duplicate() was not copying @@ -929,7 +929,7 @@ def test_AMPLRepn_to_expr(self): self.assertEqual(repn.mult, 1) self.assertEqual(repn.const, 0) self.assertEqual(repn.linear, {id(m.x[2]): 4, id(m.x[3]): 9, id(m.x[4]): 16}) - self.assertEqual(repn.nonlinear, ('o5\n%s\nn2\n', [id(m.x[2])])) + self.assertEqual(repn.nonlinear, ('o5\n%sn2\n', [id(m.x[2])])) with self.assertRaisesRegex( MouseTrap, "Cannot convert nonlinear AMPLRepn to Pyomo Expression" ): From b7145d9b238df4c64d037e5df9f4d284c8c9d0c9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 28 May 2024 07:52:21 -0600 Subject: [PATCH 1490/3044] Add additional testing --- pyomo/repn/tests/ampl/test_nlv2.py | 64 ++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 9318f1c5d0f..5af34fab149 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2637,3 +2637,67 @@ def test_presolve_fixes_nl_exernal_function(self): OUT.getvalue(), ) ) + + def test_presolve_defined_var_to_const(self): + # This test is derived from a step in an IDAES initiaization + # where the presolver is able to fix enough variables to cause + # the defined variable to be reduced to a constant. We must not + # emit the defined variable (because doing so generates an error + # in the ASL) + m = ConcreteModel() + m.eq = Var(initialize=100) + m.co2 = Var() + m.n2 = Var() + m.E = Expression(expr=60 / (3 * m.co2 - 4 * m.n2 - 5)) + m.con1 = Constraint(expr=m.co2 == 6) + m.con2 = Constraint(expr=m.n2 == 7) + m.con3 = Constraint(expr=8 / m.E == m.eq) + + OUT = io.StringIO() + nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=True + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 #problem unknown + 1 1 0 0 1 #vars, constraints, objectives, ranges, eqns + 1 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 0 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 1 0 #nonzeros in Jacobian, obj. gradient + 4 2 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +C0 #con3 +o3 #/ +n8 +n-4 +x1 #initial guess +0 100 #eq +r #1 ranges (rhs's) +4 0 #con3 +b #1 bounds (on variables) +3 #eq +k0 #intermediate Jacobian column lengths +J0 1 #con3 +0 -1 +""", + OUT.getvalue(), + ) + ) + + def test_presolve_check_invalid_monomial_constraints(self): + # This checks issue #3272 + m = ConcreteModel() + m.x = Var() + m.c = Constraint(expr=m.x == 5) + m.d = Constraint(expr=m.x >= 10) + + OUT = io.StringIO() + with self.assertRaisesRegex( + nl_writer.InfeasibleConstraintException, + r"model contains a trivially infeasible constraint 'd' " + r"\(fixed body value 5.0 outside bounds \[10, None\]\)\.", + ): + nl_writer.NLWriter().write(m, OUT, linear_presolve=True) From 4d10cdfc91f19a747ac3b2d7e6c830242de7f95d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 28 May 2024 08:43:16 -0600 Subject: [PATCH 1491/3044] NFC: fix spelling --- pyomo/repn/tests/ampl/test_nlv2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 5af34fab149..01df5a93257 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2527,7 +2527,7 @@ def test_presolve_fixes_nl_defined_variables(self): ) ) - def test_presolve_fixes_nl_exernal_function(self): + def test_presolve_fixes_nl_external_function(self): # This tests a workaround for a bug in the ASL where external # functions with constant argument expressions are not # evaluated correctly. From dc043523d8465f3271f09971e1016a699fc62238 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 28 May 2024 13:20:03 -0600 Subject: [PATCH 1492/3044] handle uninitialized variable in propagate_solution of scaling transformation --- pyomo/core/plugins/transform/scaling.py | 11 +++++++---- pyomo/core/tests/transform/test_scaling.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index 11d4ac8c493..654903773bd 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -313,10 +313,13 @@ def propagate_solution(self, scaled_model, original_model): original_v = original_model.find_component(original_v_path) for k in scaled_v: - original_v[k].set_value( - value(scaled_v[k]) / component_scaling_factor_map[scaled_v[k]], - skip_validation=True, - ) + if scaled_v[k].value is not None: + # NOTE: if the variable is set to None in the scaled model, + # we don't attempt to change its value in the original model + original_v[k].set_value( + value(scaled_v[k]) / component_scaling_factor_map[scaled_v[k]], + skip_validation=True, + ) if check_reduced_costs and scaled_v[k] in scaled_model.rc: original_model.rc[original_v[k]] = ( scaled_model.rc[scaled_v[k]] diff --git a/pyomo/core/tests/transform/test_scaling.py b/pyomo/core/tests/transform/test_scaling.py index d0fbfab61bd..e354916c309 100644 --- a/pyomo/core/tests/transform/test_scaling.py +++ b/pyomo/core/tests/transform/test_scaling.py @@ -690,6 +690,23 @@ def test_get_float_scaling_factor_intermediate_level(self): sf = ScaleModel()._get_float_scaling_factor(m.b1.b2.b3.v3) assert sf == float(0.3) + def test_propagate_solution_uninitialized_variable(self): + m = pyo.ConcreteModel() + m.x = pyo.Var([1, 2], initialize=1.0) + m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT) + m.scaling_factor[m.x[1]] = 10.0 + m.scaling_factor[m.x[2]] = 10.0 + scaled_model = pyo.TransformationFactory("core.scale_model").create_using(m) + scaled_model.scaled_x[1] = 20.0 + scaled_model.scaled_x[2] = None + pyo.TransformationFactory("core.scale_model").propagate_solution( + scaled_model, m + ) + self.assertAlmostEqual(m.x[1].value, 2.0, delta=1e-8) + # Note that because x[2] was None in the scaled model, its value is unchanged + # (and has not been overridden and set to None). + self.assertEqual(m.x[2].value, 1.0) + if __name__ == "__main__": unittest.main() From cd62d5bdd12836ebd7dcf2f8301d88bc1233f2fa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 29 May 2024 02:10:20 -0600 Subject: [PATCH 1493/3044] Improve resolution of constant external function / defined variable subexpressions --- pyomo/repn/plugins/nl_writer.py | 106 ++++++++++++++++++++------------ 1 file changed, 66 insertions(+), 40 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index e542a177247..3c90cf7687f 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -548,7 +548,7 @@ def __init__(self, ostream, rowstream, colstream, config): self.external_functions = {} self.used_named_expressions = set() self.var_map = {} - self.var_id_to_nl = {} + self.var_id_to_nl_map = {} self.sorter = FileDeterminism_to_SortComponents(config.file_determinism) self.visitor = AMPLRepnVisitor( self.template, @@ -622,6 +622,7 @@ def write(self, model): ostream = self.ostream linear_presolve = self.config.linear_presolve + nl_map = self.var_id_to_nl_map var_map = self.var_map initialize_var_map_from_column_order(model, self.config, var_map) timer.toc('Initialized column order', level=logging.DEBUG) @@ -752,6 +753,12 @@ def write(self, model): else: timer.toc('Processed %s constraints', len(all_constraints)) + # We have identified all the external functions (resolving them + # by name). Now we may need to resolve the function by the + # (local) FID, which we know is indexed by integers starting at + # 0. We will convert the dict to a list for efficient lookup. + self.external_functions = list(self.external_functions.values()) + # This may fetch more bounds than needed, but only in the cases # where variables were completely eliminated while walking the # expressions, or when users provide superfluous variables in @@ -766,7 +773,7 @@ def write(self, model): del lcon_by_linear_nnz # Note: defer categorizing constraints until after presolve, as - # the presolver could result in nonlinear constraints to become + # the presolver could result in nonlinear constraints becoming # linear (or trivial) constraints = [] linear_cons = [] @@ -778,10 +785,13 @@ def write(self, model): for info in _constraints: expr_info = info[1] if expr_info.nonlinear: - if expr_info.nonlinear[1]: + nl, args = expr_info.nonlinear + if any(vid not in nl_map for vid in args): constraints.append(info) continue - expr_info.const += _evaluate_constant_nl(expr_info.nonlinear[0]) + expr_info.const += _evaluate_constant_nl( + nl % tuple(nl_map[i] for i in args), self.external_functions + ) expr_info.nonlinear = None if expr_info.linear: linear_cons.append(info) @@ -1072,11 +1082,10 @@ def write(self, model): col_labels = col_comments = [''] * len(variables) id2nl = {_id: f"v{var_idx}\n" for var_idx, _id in enumerate(variables)} - if self.var_id_to_nl: - self.var_id_to_nl.update(id2nl) + if nl_map: + nl_map.update(id2nl) else: - self.var_id_to_nl = id2nl - _vmap = self.var_id_to_nl + self.var_id_to_nl_map = nl_map = id2nl if scale_model: template = self.template objective_scaling = [scaling_cache[id(info[0])] for info in objectives] @@ -1096,8 +1105,8 @@ def write(self, model): if ub is not None: ub *= scale var_bounds[_id] = lb, ub - # Update _vmap to output scaled variables in NL expressions - _vmap[_id] = template.division + _vmap[_id] + template.const % scale + # Update nl_map to output scaled variables in NL expressions + nl_map[_id] = template.division + nl_map[_id] + template.const % scale # Update any eliminated variables to point to the (potentially # scaled) substituted variables @@ -1106,7 +1115,7 @@ def write(self, model): for _i in args: # It is possible that the eliminated variable could # reference another variable that is no longer part of - # the model and therefore does not have a _vmap entry. + # the model and therefore does not have a nl_map entry. # This can happen when there is an underdetermined # independent linear subsystem and the presolve removed # all the constraints from the subsystem. Because the @@ -1114,7 +1123,7 @@ def write(self, model): # anywhere else in the model, they are not part of the # `variables` list. Implicitly "fix" it to an arbitrary # valid value from the presolved domain (see #3192). - if _i not in _vmap: + if _i not in nl_map: lb, ub = var_bounds[_i] if lb is None: lb = -inf @@ -1125,13 +1134,13 @@ def write(self, model): else: val = lb if abs(lb) < abs(ub) else ub eliminated_vars[_i] = AMPLRepn(val, {}, None) - _vmap[_i] = expr_info.compile_repn(visitor)[0] + nl_map[_i] = expr_info.compile_repn(visitor)[0] logger.warning( "presolve identified an underdetermined independent " "linear subsystem that was removed from the model. " f"Setting '{var_map[_i]}' == {val}" ) - _vmap[_id] = nl % tuple(_vmap[_i] for _i in args) + nl_map[_id] = nl % tuple(nl_map[_i] for _i in args) r_lines = [None] * n_cons for idx, (con, expr_info, lb, ub) in enumerate(constraints): @@ -1330,7 +1339,7 @@ def write(self, model): # "F" lines (external function definitions) # amplfunc_libraries = set() - for fid, fcn in sorted(self.external_functions.values()): + for fid, fcn in self.external_functions: amplfunc_libraries.add(fcn._library) ostream.write("F%d 1 -1 %s\n" % (fid, fcn._function)) @@ -1774,6 +1783,7 @@ def _linear_presolve( var_map = self.var_map substitutions_by_linear_var = defaultdict(set) template = self.template + nl_map = self.var_id_to_nl_map one_var = lcon_by_linear_nnz[1] two_var = lcon_by_linear_nnz[2] while 1: @@ -1783,6 +1793,7 @@ def _linear_presolve( b, _ = var_bounds[_id] logger.debug("NL presolve: bounds fixed %s := %s", var_map[_id], b) eliminated_vars[_id] = AMPLRepn(b, {}, None) + nl_map[_id] = template.const % b elif one_var: con_id, info = one_var.popitem() expr_info, lb = info @@ -1792,6 +1803,7 @@ def _linear_presolve( b = expr_info.const = (lb - expr_info.const) / coef logger.debug("NL presolve: substituting %s := %s", var_map[_id], b) eliminated_vars[_id] = expr_info + nl_map[_id] = template.const % b lb, ub = var_bounds[_id] if (lb is not None and lb - b > TOL) or ( ub is not None and ub - b < -TOL @@ -1929,30 +1941,31 @@ def _linear_presolve( expr_info.linear[x] += c * a elif a: expr_info.linear[x] = c * a + elif not expr_info.linear: + nl_map[resubst] = template.const % expr_info.const # Note: the ASL will (silently) produce incorrect answers if the # nonlinear portion of a defined variable is a constant # expression. This may now be the case if all the variables in # the original nonlinear expression have been fixed. for _id, (expr, info, sub) in self.subexpression_cache.items(): - if not info.nonlinear: - continue - nl, args = info.nonlinear - if not args or any( - vid not in eliminated_vars or eliminated_vars[vid].linear - for vid in args - ): - continue - # Ideally, we would just evaluate the named expression. - # However, there might be a linear portion of the named - # expression that still has free variables, and there is no - # guarantee that the user actually initialized the - # variables. So, we will fall back on parsing the (now - # constant) nonlinear fragment and evaluating it. - info.nonlinear = None - info.const += _evaluate_constant_nl( - nl % tuple(template.const % eliminated_vars[i].const for i in args) - ) + if info.nonlinear: + nl, args = info.nonlinear + # Note: 'not args' skips string arguments + # Note: 'vid in nl_map' skips eliminated + # variables and defined variables reduced to constants + if not args or any(vid not in nl_map for vid in args): + continue + # Ideally, we would just evaluate the named expression. + # However, there might be a linear portion of the named + # expression that still has free variables, and there is no + # guarantee that the user actually initialized the + # variables. So, we will fall back on parsing the (now + # constant) nonlinear fragment and evaluating it. + info.nonlinear = None + info.const += _evaluate_constant_nl( + nl % tuple(nl_map[i] for i in args), self.external_functions + ) if not info.linear: # This has resolved to a constant: the ASL will fail for # defined variables containing ONLY a constant. We @@ -1960,7 +1973,7 @@ def _linear_presolve( # original constraint/objective expression(s) info.linear = {} self.used_named_expressions.discard(_id) - self.var_id_to_nl[_id] = self.template.const % info.const + nl_map[_id] = template.const % info.const self.subexpression_cache[_id] = (expr, info, [None, None, True]) return eliminated_cons, eliminated_vars @@ -1990,18 +2003,20 @@ def _write_nl_expression(self, repn, include_const): # constant as the second argument, so we will too. nl = self.template.binary_sum + nl + self.template.const % repn.const try: - self.ostream.write(nl % tuple(map(self.var_id_to_nl.__getitem__, args))) + self.ostream.write( + nl % tuple(map(self.var_id_to_nl_map.__getitem__, args)) + ) except KeyError: final_args = [] for arg in args: - if arg in self.var_id_to_nl: - final_args.append(self.var_id_to_nl[arg]) + if arg in self.var_id_to_nl_map: + final_args.append(self.var_id_to_nl_map[arg]) else: _nl, _ids, _ = self.subexpression_cache[arg][1].compile_repn( self.visitor ) final_args.append( - _nl % tuple(map(self.var_id_to_nl.__getitem__, _ids)) + _nl % tuple(map(self.var_id_to_nl_map.__getitem__, _ids)) ) self.ostream.write(nl % tuple(final_args)) @@ -2018,7 +2033,7 @@ def _write_v_line(self, expr_id, k): lbl = '\t#%s' % info[0].name else: lbl = '' - self.var_id_to_nl[expr_id] = f"v{self.next_V_line_id}{lbl}\n" + self.var_id_to_nl_map[expr_id] = f"v{self.next_V_line_id}{lbl}\n" # Do NOT write out 0 coefficients here: doing so fouls up the # ASL's logic for calculating derivatives, leading to 'nan' in # the Hessian results. @@ -3167,7 +3182,7 @@ def finalizeResult(self, result): return ans -def _evaluate_constant_nl(nl): +def _evaluate_constant_nl(nl, external_functions): expr = nl.splitlines() stack = [] while expr: @@ -3182,6 +3197,15 @@ def _evaluate_constant_nl(nl): # skip blank lines if not tokens: continue + if tokens[0][0] == 'f': + # external function + fid, nargs = tokens + fid = int(fid[1:]) + nargs = int(nargs) + fcn_id, ef = external_functions[fid] + assert fid == fcn_id + stack.append(ef.evaluate(tuple(stack.pop() for i in range(nargs)))) + continue raise DeveloperError( f"Unsupported line format _evaluate_constant_nl() " f"(we expect each line to contain a single token): '{line}'" @@ -3205,6 +3229,8 @@ def _evaluate_constant_nl(nl): # sum) or a string argument. Preserve it as-is until later # when we know which we are expecting. stack.append(term) + elif cmd == 'h': + stack.append(term.split(':', 1)[1]) else: raise DeveloperError( f"Unsupported NL operator in _evaluate_constant_nl(): '{line}'" From 5d4ce0808ab96e8003e14983fc01f80edc9271dd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 29 May 2024 02:10:43 -0600 Subject: [PATCH 1494/3044] bugfix: undefined variable --- pyomo/repn/plugins/nl_writer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 3c90cf7687f..8a41eb568e7 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2779,8 +2779,8 @@ def handle_external_function_node(visitor, node, *args): "correctly." % ( func, - visitor.external_byFcn[func]._library, - visitor.external_byFcn[func]._library.name, + visitor.external_functions[func]._library, + visitor.external_functions[func]._library.name, node._fcn._library, node._fcn.name, ) From 2f0e57a497c9e8e9e2f1ca739d114d41cfd4115f Mon Sep 17 00:00:00 2001 From: Alex Dowling Date: Wed, 29 May 2024 09:29:34 -0400 Subject: [PATCH 1495/3044] Addressed feedback, ran black. --- pyomo/contrib/doe/doe.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 5c7fe4f6e77..a120add4200 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -42,7 +42,7 @@ import inspect -import pyomo.contrib.parmest.utils as utils +from pyomo.common import DeveloperError class CalculationMode(Enum): @@ -457,11 +457,6 @@ def _sequential_finite(self, read_output, extract_single_model, store_output): # add zero (dummy/placeholder) objective function mod.Obj = pyo.Objective(expr=0, sense=pyo.minimize) - # convert params to vars - # print("self.param.keys():", self.param.keys()) - # mod = utils.convert_params_to_vars(mod, self.param.keys(), fix_vars=True) - # mod.pprint() - # solve model square_result = self._solve_doe(mod, fix=True) @@ -545,9 +540,6 @@ def _direct_kaug(self): # add zero (dummy/placeholder) objective function mod.Obj = pyo.Objective(expr=0, sense=pyo.minimize) - # convert params to vars - # mod = utils.convert_params_to_vars(mod, self.param.keys(), fix_vars=True) - # set ub and lb to parameters for par in self.param.keys(): cuid = pyo.ComponentUID(par) @@ -1294,8 +1286,8 @@ def trace_calc(m): return m.trace == sum(m.fim[j, j] for j in m.regression_parameters) def det_general(m): - """Calculate determinant. Can be applied to FIM of any size. - det(A) = sum_{sigma in Sn} (sgn(sigma) * Prod_{i=1}^n a_{i,sigma_i}) + r"""Calculate determinant. Can be applied to FIM of any size. + det(A) = \sum_{\sigma in \S_n} (sgn(\sigma) * \Prod_{i=1}^n a_{i,\sigma_i}) Use permutation() to get permutations, sgn() to get signature """ r_list = list(range(len(m.regression_parameters))) From 15088dee1f4b05e7225c5fb9cf3a58931ef1e79b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 29 May 2024 07:38:45 -0600 Subject: [PATCH 1496/3044] Updating baseline to reflect improved linear constraint detection --- pyomo/repn/tests/ampl/test_nlv2.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 01df5a93257..3ca41e97ba4 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2657,11 +2657,15 @@ def test_presolve_defined_var_to_const(self): nl_writer.NLWriter().write( m, OUT, symbolic_solver_labels=True, linear_presolve=True ) + # Note that the presolve will end up recognizing con3 as a + # linear constraint; however, it does not do so until processing + # the constraints after presolve (so the constraint is not + # actually removed and the eq variable still appears in the model) self.assertEqual( *nl_diff( """g3 1 1 0 #problem unknown 1 1 0 0 1 #vars, constraints, objectives, ranges, eqns - 1 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb 0 0 #network constraints: nonlinear, linear 0 0 0 #nonlinear vars in constraints, objectives, both 0 0 0 1 #linear network variables; functions; arith, flags @@ -2670,13 +2674,11 @@ def test_presolve_defined_var_to_const(self): 4 2 #max name lengths: constraints, variables 0 0 0 0 0 #common exprs: b,c,o,c1,o1 C0 #con3 -o3 #/ -n8 -n-4 +n0 x1 #initial guess 0 100 #eq r #1 ranges (rhs's) -4 0 #con3 +4 2.0 #con3 b #1 bounds (on variables) 3 #eq k0 #intermediate Jacobian column lengths From 1bed2c601ec8ee515892e4981a684cec7a0a7fd0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 29 May 2024 11:27:57 -0600 Subject: [PATCH 1497/3044] NFC: fix typos --- pyomo/repn/plugins/nl_writer.py | 2 +- pyomo/repn/tests/ampl/test_nlv2.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 8a41eb568e7..43fd2fade68 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1946,7 +1946,7 @@ def _linear_presolve( # Note: the ASL will (silently) produce incorrect answers if the # nonlinear portion of a defined variable is a constant - # expression. This may now be the case if all the variables in + # expression. This may not be the case if all the variables in # the original nonlinear expression have been fixed. for _id, (expr, info, sub) in self.subexpression_cache.items(): if info.nonlinear: diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 01df5a93257..7d3b499a4ae 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2639,7 +2639,7 @@ def test_presolve_fixes_nl_external_function(self): ) def test_presolve_defined_var_to_const(self): - # This test is derived from a step in an IDAES initiaization + # This test is derived from a step in an IDAES initialization # where the presolver is able to fix enough variables to cause # the defined variable to be reduced to a constant. We must not # emit the defined variable (because doing so generates an error From c212c13a4ab468ba61dd88c29322f889ca9ee150 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 29 May 2024 13:29:15 -0600 Subject: [PATCH 1498/3044] Updating CHANGELOG in preparation for the 6.7.3 release --- CHANGELOG.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b4ecbf785..b39165297f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ Pyomo CHANGELOG =============== +------------------------------------------------------------------------------- +Pyomo 6.7.3 (29 May 2024) +------------------------------------------------------------------------------- + +- Core + - Deprecate `pyomo.core.plugins.transform.model.to_standard_form()` (#3265) + - Reorder definitions to avoid `NameError` in some situations (#3264) +- Testing + - Add URL checking to GHA linting job (#3259, #3261) + - Skip Windows Python 3.8 conda GHA job (#3269) +- Contributed Packages + - DoE: Bug fixes for workshop (#3267) + ------------------------------------------------------------------------------- Pyomo 6.7.2 (9 May 2024) ------------------------------------------------------------------------------- @@ -57,7 +70,7 @@ Pyomo 6.7.2 (9 May 2024) - CP: Add SequenceVar and other logical expressions for scheduling (#3227) - DoE: Bug fixes (#3245) - iis: Add minimal intractable system infeasibility diagnostics (#3172) - - incidence_analysis: Improve `solve_strongly_connected_components` + - incidence_analysis: Improve `solve_strongly_connected_components` performance for models with named expressions (#3186) - incidence_analysis: Add function to plot incidence graph in Dulmage-Mendelsohn order (#3207) From 97a6ae08aa967a7aaa6fe9ef6e42bafe1eb29eca Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 29 May 2024 14:37:14 -0600 Subject: [PATCH 1499/3044] More edits to the CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b39165297f3..f9051e80ade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ Pyomo 6.7.3 (29 May 2024) - Core - Deprecate `pyomo.core.plugins.transform.model.to_standard_form()` (#3265) - Reorder definitions to avoid `NameError` in some situations (#3264) +- Solver Interfaces + - NLv2: Fix linear presolver with constant defined vars/external fcns (#3276) - Testing - Add URL checking to GHA linting job (#3259, #3261) - Skip Windows Python 3.8 conda GHA job (#3269) From ba2d2cac411e0000bab03eb48b16bb0d573c2fed Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 29 May 2024 14:42:45 -0600 Subject: [PATCH 1500/3044] Updating deprecation version to 6.7.3 --- pyomo/core/plugins/transform/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/plugins/transform/model.py b/pyomo/core/plugins/transform/model.py index 9f370c96304..8fe828854ce 100644 --- a/pyomo/core/plugins/transform/model.py +++ b/pyomo/core/plugins/transform/model.py @@ -24,7 +24,7 @@ @deprecated( "to_standard_form() is deprecated. " "Please use WriterFactory('compile_standard_form')", - version='6.7.3.dev0', + version='6.7.3', remove_in='6.8.0', ) def to_standard_form(self): From 32e6431e876b90f5268ca448581c50124d7adf68 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 29 May 2024 14:54:47 -0600 Subject: [PATCH 1501/3044] Update guard for pint import to possibly avoid recursion error on FreeBSD --- pyomo/contrib/viewer/tests/test_data_model_item.py | 9 ++------- pyomo/contrib/viewer/tests/test_data_model_tree.py | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/viewer/tests/test_data_model_item.py b/pyomo/contrib/viewer/tests/test_data_model_item.py index 781ca25508a..d780b315044 100644 --- a/pyomo/contrib/viewer/tests/test_data_model_item.py +++ b/pyomo/contrib/viewer/tests/test_data_model_item.py @@ -46,15 +46,10 @@ from pyomo.contrib.viewer.model_browser import ComponentDataItem from pyomo.contrib.viewer.ui_data import UIData from pyomo.common.dependencies import DeferredImportError +from pyomo.core.base.units_container import pint_available -try: - x = pyo.units.m - units_available = True -except DeferredImportError: - units_available = False - -@unittest.skipIf(not units_available, "Pyomo units are not available") +@unittest.skipIf(not pint_available, "Pyomo units are not available") class TestDataModelItem(unittest.TestCase): def setUp(self): # Borrowed this test model from the trust region tests diff --git a/pyomo/contrib/viewer/tests/test_data_model_tree.py b/pyomo/contrib/viewer/tests/test_data_model_tree.py index d517c91b353..2e5c3592198 100644 --- a/pyomo/contrib/viewer/tests/test_data_model_tree.py +++ b/pyomo/contrib/viewer/tests/test_data_model_tree.py @@ -42,12 +42,7 @@ from pyomo.contrib.viewer.model_browser import ComponentDataModel import pyomo.contrib.viewer.qt as myqt from pyomo.common.dependencies import DeferredImportError - -try: - _x = pyo.units.m - units_available = True -except DeferredImportError: - units_available = False +from pyomo.core.base.units_container import pint_available available = myqt.available @@ -63,7 +58,7 @@ def __init__(*args, **kwargs): pass -@unittest.skipIf(not available or not units_available, "PyQt or units not available") +@unittest.skipIf(not available or not pint_available, "PyQt or units not available") class TestDataModel(unittest.TestCase): def setUp(self): # Borrowed this test model from the trust region tests From f12825eb98f96807cecbfc9515f41b2b8c0e9b16 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 29 May 2024 15:14:28 -0600 Subject: [PATCH 1502/3044] More edits to the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9051e80ade..8d1d1e45e3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Pyomo 6.7.3 (29 May 2024) - Skip Windows Python 3.8 conda GHA job (#3269) - Contributed Packages - DoE: Bug fixes for workshop (#3267) + - viewer: Update guard for pint import (#3277) ------------------------------------------------------------------------------- Pyomo 6.7.2 (9 May 2024) From 93e5dab925115d564af09f8a33ff4099b0247083 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 29 May 2024 15:23:21 -0600 Subject: [PATCH 1503/3044] Finalizing 6.7.3 release files --- .coin-or/projDesc.xml | 4 ++-- RELEASE.md | 2 +- pyomo/version/info.py | 4 ++-- setup.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.coin-or/projDesc.xml b/.coin-or/projDesc.xml index 073efd968a7..d13ac8804cf 100644 --- a/.coin-or/projDesc.xml +++ b/.coin-or/projDesc.xml @@ -227,8 +227,8 @@ Carl D. Laird, Chair, Pyomo Management Committee, claird at andrew dot cmu dot e Use explicit overrides to disable use of automated version reporting. --> - 6.7.2 - 6.7.2 + 6.7.3 + 6.7.3 diff --git a/RELEASE.md b/RELEASE.md index b0228e53944..e42469cbad5 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,4 +1,4 @@ -We are pleased to announce the release of Pyomo 6.7.2. +We are pleased to announce the release of Pyomo 6.7.3. Pyomo is a collection of Python software packages that supports a diverse set of optimization capabilities for formulating and analyzing diff --git a/pyomo/version/info.py b/pyomo/version/info.py index 36945e8e011..cba680d50ee 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -27,8 +27,8 @@ major = 6 minor = 7 micro = 3 -releaselevel = 'invalid' -# releaselevel = 'final' +# releaselevel = 'invalid' +releaselevel = 'final' serial = 0 if releaselevel == 'final': diff --git a/setup.py b/setup.py index 6d28e4d184b..9dfa253815e 100644 --- a/setup.py +++ b/setup.py @@ -256,7 +256,7 @@ def __ne__(self, other): 'sphinx-toolbox>=2.16.0', 'sphinx-jinja2-compat>=0.1.1', 'enum_tools', - 'numpy', # Needed by autodoc for pynumero + 'numpy<2.0.0', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero ], 'optional': [ @@ -273,7 +273,7 @@ def __ne__(self, other): # installed on python 3.8 'networkx<3.2; python_version<"3.9"', 'networkx; python_version>="3.9"', - 'numpy', + 'numpy<2.0.0', 'openpyxl', # dataportals #'pathos', # requested for #963, but PR currently closed 'pint', # units From 5c3af84e27c19b10987ab451ab892496f5bb9630 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 29 May 2024 16:03:44 -0600 Subject: [PATCH 1504/3044] Resetting main for development (6.7.4.dev0) --- pyomo/version/info.py | 6 +++--- setup.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/version/info.py b/pyomo/version/info.py index cba680d50ee..825483a70a0 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -26,9 +26,9 @@ # main and needs a hard reference to "suitably new" development. major = 6 minor = 7 -micro = 3 -# releaselevel = 'invalid' -releaselevel = 'final' +micro = 4 +releaselevel = 'invalid' +# releaselevel = 'final' serial = 0 if releaselevel == 'final': diff --git a/setup.py b/setup.py index 9dfa253815e..6d28e4d184b 100644 --- a/setup.py +++ b/setup.py @@ -256,7 +256,7 @@ def __ne__(self, other): 'sphinx-toolbox>=2.16.0', 'sphinx-jinja2-compat>=0.1.1', 'enum_tools', - 'numpy<2.0.0', # Needed by autodoc for pynumero + 'numpy', # Needed by autodoc for pynumero 'scipy', # Needed by autodoc for pynumero ], 'optional': [ @@ -273,7 +273,7 @@ def __ne__(self, other): # installed on python 3.8 'networkx<3.2; python_version<"3.9"', 'networkx; python_version>="3.9"', - 'numpy<2.0.0', + 'numpy', 'openpyxl', # dataportals #'pathos', # requested for #963, but PR currently closed 'pint', # units From 67fbe0bb958c7ea1f47bba71a38e4bf094f511a7 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 29 May 2024 19:18:56 -0400 Subject: [PATCH 1505/3044] Make PyROS temporarily adjust Pyomo NL writer tol --- pyomo/contrib/pyros/util.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 3b0187af7dd..8f86f0cc179 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -38,6 +38,7 @@ from pyomo.core.expr import value from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression from pyomo.repn.standard_repn import generate_standard_repn +from pyomo.repn.plugins import nl_writer as pyomo_nl_writer from pyomo.core.expr.visitor import ( identify_variables, identify_mutable_parameters, @@ -1809,6 +1810,16 @@ def call_solver(model, solver, config, timing_obj, timer_name, err_msg): timing_obj.start_timer(timer_name) tt_timer.tic(msg=None) + # tentative: reduce risk of InfeasibleConstraintException + # occurring due to discrepancies between Pyomo NL writer + # tolerance and (default) subordinate solver (e.g. IPOPT) + # feasibility tolerances. + # e.g., a Var fixed outside bounds beyond the Pyomo NL writer + # tolerance, but still within the default IPOPT feasibility + # tolerance + current_nl_writer_tol = pyomo_nl_writer.TOL + pyomo_nl_writer.TOL = 1e-4 + try: results = solver.solve( model, @@ -1827,6 +1838,8 @@ def call_solver(model, solver, config, timing_obj, timer_name, err_msg): results.solver, TIC_TOC_SOLVE_TIME_ATTR, tt_timer.toc(msg=None, delta=True) ) finally: + pyomo_nl_writer.TOL = current_nl_writer_tol + timing_obj.stop_timer(timer_name) revert_solver_max_time_adjustment( solver, orig_setting, custom_setting_present, config From 7460625becccdcd5adf2f3e8b228d76884d28c01 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 29 May 2024 19:40:20 -0400 Subject: [PATCH 1506/3044] Add test for adjustment of NL writer tolerance --- pyomo/contrib/pyros/tests/test_grcs.py | 72 +++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index f7efec4d6e7..5e323ad7a78 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -42,6 +42,7 @@ from pyomo.contrib.pyros.util import get_vars_from_component from pyomo.contrib.pyros.util import identify_objective_functions from pyomo.common.collections import Bunch +from pyomo.repn.plugins import nl_writer as pyomo_nl_writer import time import math from pyomo.contrib.pyros.util import time_code @@ -68,7 +69,7 @@ from pyomo.common.dependencies import numpy as np, numpy_available from pyomo.common.dependencies import scipy as sp, scipy_available from pyomo.environ import maximize as pyo_max -from pyomo.common.errors import ApplicationError +from pyomo.common.errors import ApplicationError, InfeasibleConstraintException from pyomo.opt import ( SolverResults, SolverStatus, @@ -4616,6 +4617,75 @@ def test_discrete_separation_subsolver_error(self): ), ) + def test_pyros_nl_writer_tol(self): + """ + Test PyROS subsolver call routine behavior + with respect to the NL writer tolerance is as + expected. + """ + m = ConcreteModel() + m.q = Param(initialize=1, mutable=True) + m.x1 = Var(initialize=1, bounds=(0, 1)) + m.x2 = Var(initialize=2, bounds=(0, m.q)) + m.obj = Objective(expr=m.x1 + m.x2) + + # fixed just inside the PyROS-specified NL writer tolerance. + m.x1.fix(m.x1.upper + 9.9e-5) + + current_nl_writer_tol = pyomo_nl_writer.TOL + ipopt_solver = SolverFactory("ipopt") + pyros_solver = SolverFactory("pyros") + + pyros_solver.solve( + model=m, + first_stage_variables=[m.x1], + second_stage_variables=[m.x2], + uncertain_params=[m.q], + uncertainty_set=BoxSet([[0, 1]]), + local_solver=ipopt_solver, + global_solver=ipopt_solver, + decision_rule_order=0, + solve_master_globally=False, + bypass_global_separation=True, + ) + + self.assertEqual( + pyomo_nl_writer.TOL, + current_nl_writer_tol, + msg="Pyomo NL writer tolerance not restored as expected.", + ) + + # fixed just outside the PyROS-specified NL writer tolerance. + # this should be exceptional. + m.x1.fix(m.x1.upper + 1.01e-4) + + err_msg = ( + "model contains a trivially infeasible variable.*x1" + ".*fixed.*outside bounds" + ) + with self.assertRaisesRegex(InfeasibleConstraintException, err_msg): + pyros_solver.solve( + model=m, + first_stage_variables=[m.x1], + second_stage_variables=[m.x2], + uncertain_params=[m.q], + uncertainty_set=BoxSet([[0, 1]]), + local_solver=ipopt_solver, + global_solver=ipopt_solver, + decision_rule_order=0, + solve_master_globally=False, + bypass_global_separation=True, + ) + + self.assertEqual( + pyomo_nl_writer.TOL, + current_nl_writer_tol, + msg=( + "Pyomo NL writer tolerance not restored as expected " + "after exceptional test." + ), + ) + @unittest.skipUnless( baron_license_is_valid, "Global NLP solver is not available and licensed." ) From 4f2cda6bf374a21d8bb16098140edfcd41154112 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 29 May 2024 20:07:08 -0400 Subject: [PATCH 1507/3044] Fix IPOPT subsolver wall time limit restoration --- pyomo/contrib/pyros/util.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 8f86f0cc179..ecabca8f115 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -378,7 +378,14 @@ def revert_solver_max_time_adjustment( elif isinstance(solver, SolverFactory.get_class("baron")): options_key = "MaxTime" elif isinstance(solver, SolverFactory.get_class("ipopt")): - options_key = "max_cpu_time" + options_key = ( + # IPOPT 3.14.0+ added support for specifying + # wall time limit explicitly; this is preferred + # over CPU time limit + "max_wall_time" + if solver.version() >= (3, 14, 0, 0) + else "max_cpu_time" + ) elif isinstance(solver, SolverFactory.get_class("scip")): options_key = "limits/time" else: From edeaf1d2c75ae999596072ef130253024f14b3dd Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 29 May 2024 22:07:01 -0400 Subject: [PATCH 1508/3044] Add IPOPT availability check to new test --- pyomo/contrib/pyros/tests/test_grcs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 5e323ad7a78..f2954750a16 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -4617,6 +4617,7 @@ def test_discrete_separation_subsolver_error(self): ), ) + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_pyros_nl_writer_tol(self): """ Test PyROS subsolver call routine behavior From 57b055065417be5bf59fe5edd70a784ecebbc7a5 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 30 May 2024 16:34:45 -0400 Subject: [PATCH 1509/3044] Update PyROS solver variable scope --- pyomo/contrib/pyros/pyros.py | 11 ++-- pyomo/contrib/pyros/tests/test_grcs.py | 64 ++++++++----------- pyomo/contrib/pyros/util.py | 87 +++++++++++--------------- 3 files changed, 71 insertions(+), 91 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 582233c4a56..4283a548568 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -275,9 +275,9 @@ def _resolve_and_validate_pyros_args(self, model, **kwds): """ config = self.CONFIG(kwds.pop("options", {})) config = config(kwds) - state_vars = validate_pyros_inputs(model, config) + var_partitioning = validate_pyros_inputs(model, config) - return config, state_vars + return config, var_partitioning @document_kwargs_from_configdict( config=CONFIG, @@ -363,7 +363,10 @@ def solve( self._log_intro(logger=progress_logger, level=logging.INFO) self._log_disclaimer(logger=progress_logger, level=logging.INFO) - config, state_vars = self._resolve_and_validate_pyros_args(model, **kwds) + config, var_partitioning = self._resolve_and_validate_pyros_args( + model, + **kwds, + ) self._log_config( logger=config.progress_logger, config=config, @@ -379,7 +382,7 @@ def solve( util = Block(concrete=True) util.first_stage_variables = config.first_stage_variables util.second_stage_variables = config.second_stage_variables - util.state_vars = state_vars + util.state_vars = var_partitioning.state_variables util.uncertain_params = config.uncertain_params model_data.util_block = unique_component_name(model, 'util') diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index f2954750a16..23646beb6b2 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -6711,47 +6711,37 @@ def test_pyros_vars_not_in_model(self): global_solver = SimpleTestSolver() pyros = SolverFactory("pyros") - mdl.bad_con = Constraint(expr=mdl2.x1 + mdl2.x2 >= 1) - - desc_dof_map = [ - ("first-stage", [mdl2.x1], [], 2), - ("second-stage", [], [mdl2.x2], 2), - ("state", [mdl.x1], [], 3), - ] + mdl.bad_con = Constraint(expr=mdl.x1 + mdl2.x2 >= 1) + mdl2.x3 = Var(initialize=1) # now perform checks - for vardesc, first_stage_vars, second_stage_vars, numlines in desc_dof_map: - with LoggingIntercept(level=logging.ERROR) as LOG: - exc_str = ( - "Found entries of " - f"{vardesc} variables not descended from.*model.*" + with LoggingIntercept(level=logging.ERROR) as LOG: + exc_str = ( + "Found Vars.*active.*" + "not descended from.*model.*" + ) + with self.assertRaisesRegex(ValueError, exc_str): + pyros.solve( + model=mdl, + first_stage_variables=[mdl.x1, mdl.x2], + second_stage_variables=[mdl2.x3], + uncertain_params=[mdl.u], + uncertainty_set=BoxSet([[1 / 4, 2]]), + local_solver=local_solver, + global_solver=global_solver, ) - with self.assertRaisesRegex(ValueError, exc_str): - pyros.solve( - model=mdl, - first_stage_variables=first_stage_vars, - second_stage_variables=second_stage_vars, - uncertain_params=[mdl.u], - uncertainty_set=BoxSet([[1 / 4, 2]]), - local_solver=local_solver, - global_solver=global_solver, - ) - - log_msgs = LOG.getvalue().split("\n")[:-1] - # check detailed log message is as expected - self.assertEqual( - len(log_msgs), - numlines, - "Error-level log message does not contain expected number of lines.", - ) - self.assertRegex( - text=log_msgs[0], - expected_regex=( - f"The following {vardesc} variables" - ".*not descended from.*model with name 'model1'" - ), - ) + log_msgs = LOG.getvalue().split("\n") + invalid_vars_strs_list = log_msgs[1:-1] + self.assertEqual( + len(invalid_vars_strs_list), + 1, + msg="Number of lines referencing name of invalid Vars not as expected.", + ) + self.assertRegex( + text=invalid_vars_strs_list[0], + expected_regex=f"{mdl2.x2.name!r}", + ) def test_pyros_non_continuous_vars(self): """ diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index ecabca8f115..6cffdd5d911 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -13,6 +13,7 @@ Utility functions for the PyROS solver ''' +from collections import namedtuple import copy from enum import Enum, auto from pyomo.common.collections import ComponentSet, ComponentMap @@ -859,44 +860,12 @@ def check_components_descended_from_model(model, components, components_name, co f"{comp_names_str}" ) raise ValueError( - f"Found entries of {components_name} " + f"Found {components_name} " "not descended from input model. " "Check logger output messages." ) -def get_state_vars(blk, first_stage_variables, second_stage_variables): - """ - Get state variables of a modeling block. - - The state variables with respect to `blk` are the unfixed - `VarData` objects participating in the active objective - or constraints descended from `blk` which are not - first-stage variables or second-stage variables. - - Parameters - ---------- - blk : ScalarBlock - Block of interest. - first_stage_variables : Iterable of VarData - First-stage variables. - second_stage_variables : Iterable of VarData - Second-stage variables. - - Yields - ------ - VarData - State variable. - """ - dof_var_set = ComponentSet(first_stage_variables) | ComponentSet( - second_stage_variables - ) - for var in get_vars_from_component(blk, (Objective, Constraint)): - is_state_var = not var.fixed and var not in dof_var_set - if is_state_var: - yield var - - def check_variables_continuous(model, vars, config): """ Check that all DOF and state variables of the model @@ -977,6 +946,12 @@ def validate_model(model, config): ) +VariablePartitioning = namedtuple( + "VariablePartitioning", + ("first_stage_variables", "second_stage_variables", "state_variables"), +) + + def validate_variable_partitioning(model, config): """ Check that partitioning of the first-stage variables, @@ -1025,27 +1000,39 @@ def validate_variable_partitioning(model, config): "contain at least one common Var object." ) - state_vars = list( - get_state_vars( - model, - first_stage_variables=config.first_stage_variables, - second_stage_variables=config.second_stage_variables, + active_model_vars = ComponentSet( + get_vars_from_components( + block=model, + active=True, + include_fixed=False, + descend_into=True, + ctype=(Objective, Constraint), ) ) - var_type_list_map = { - "first-stage variables": config.first_stage_variables, - "second-stage variables": config.second_stage_variables, - "state variables": state_vars, - } - for desc, vars in var_type_list_map.items(): - check_components_descended_from_model( - model=model, components=vars, components_name=desc, config=config - ) + check_components_descended_from_model( + model=model, + components=active_model_vars, + components_name=( + "Vars participating in the " + "active model Objective/Constraint expressions " + ), + config=config, + ) + check_variables_continuous(model, active_model_vars, config) - all_vars = config.first_stage_variables + config.second_stage_variables + state_vars - check_variables_continuous(model, all_vars, config) + first_stage_vars = ( + ComponentSet(config.first_stage_variables) & active_model_vars + ) + second_stage_vars = ( + ComponentSet(config.second_stage_variables) & active_model_vars + ) + state_vars = active_model_vars - (first_stage_vars | second_stage_vars) - return state_vars + return VariablePartitioning( + list(first_stage_vars), + list(second_stage_vars), + list(state_vars), + ) def validate_uncertainty_specification(model, config): From 5734e5238a7947d213de80582656d038d0241796 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 30 May 2024 18:31:24 -0400 Subject: [PATCH 1510/3044] Modularize the preprocessor --- pyomo/contrib/pyros/pyros.py | 83 ++---------------------------------- pyomo/contrib/pyros/util.py | 73 +++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 79 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 4283a548568..a616dfddca3 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -14,24 +14,15 @@ from pyomo.common.config import document_kwargs_from_configdict from pyomo.core.base.block import Block from pyomo.core.expr import value -from pyomo.core.base.var import Var -from pyomo.core.base.objective import Objective from pyomo.contrib.pyros.util import time_code -from pyomo.common.modeling import unique_component_name from pyomo.opt import SolverFactory from pyomo.contrib.pyros.config import pyros_config, logger_domain from pyomo.contrib.pyros.util import ( - recast_to_min_obj, - add_decision_rule_constraints, - add_decision_rule_variables, load_final_solution, pyrosTerminationCondition, ObjectiveType, - identify_objective_functions, validate_pyros_inputs, - transform_to_standard_form, - turn_bounds_to_constraints, - replace_uncertain_bounds_with_constraints, + preprocess_model_data, IterationLogRecord, setup_pyros_logger, TimingData, @@ -331,6 +322,7 @@ def solve( """ model_data = ROSolveResults() model_data.timing = TimingData() + model_data.original_model = model with time_code( timing_data_obj=model_data.timing, code_block_name="main", @@ -374,77 +366,9 @@ def solve( level=logging.INFO, ) - # begin preprocessing config.progress_logger.info("Preprocessing...") model_data.timing.start_timer("main.preprocessing") - - # === A block to hold list-type data to make cloning easy - util = Block(concrete=True) - util.first_stage_variables = config.first_stage_variables - util.second_stage_variables = config.second_stage_variables - util.state_vars = var_partitioning.state_variables - util.uncertain_params = config.uncertain_params - - model_data.util_block = unique_component_name(model, 'util') - model.add_component(model_data.util_block, util) - # Note: model.component(model_data.util_block) is util - - # === Leads to a logger warning here for inactive obj when cloning - model_data.original_model = model - # === For keeping track of variables after cloning - cname = unique_component_name(model_data.original_model, 'tmp_var_list') - src_vars = list(model_data.original_model.component_data_objects(Var)) - setattr(model_data.original_model, cname, src_vars) - model_data.working_model = model_data.original_model.clone() - - # identify active objective function - # (there should only be one at this point) - # recast to minimization if necessary - active_objs = list( - model_data.working_model.component_data_objects( - Objective, active=True, descend_into=True - ) - ) - assert len(active_objs) == 1 - active_obj = active_objs[0] - active_obj_original_sense = active_obj.sense - recast_to_min_obj(model_data.working_model, active_obj) - - # === Determine first and second-stage objectives - identify_objective_functions(model_data.working_model, active_obj) - active_obj.deactivate() - - # === Put model in standard form - transform_to_standard_form(model_data.working_model) - - # === Replace variable bounds depending on uncertain params with - # explicit inequality constraints - replace_uncertain_bounds_with_constraints( - model_data.working_model, model_data.working_model.util.uncertain_params - ) - - # === Add decision rule information - add_decision_rule_variables(model_data, config) - add_decision_rule_constraints(model_data, config) - - # === Move bounds on control variables to explicit ineq constraints - wm_util = model_data.working_model - - # cast bounds on second-stage and state variables to - # explicit constraints for separation objectives - for c in model_data.working_model.util.second_stage_variables: - turn_bounds_to_constraints(c, wm_util, config) - for c in model_data.working_model.util.state_vars: - turn_bounds_to_constraints(c, wm_util, config) - - # === Make control_variable_bounds array - wm_util.ssv_bounds = [] - for c in model_data.working_model.component_data_objects( - Constraint, descend_into=True - ): - if "bound_con" in c.name: - wm_util.ssv_bounds.append(c) - + preprocess_model_data(model_data, config, var_partitioning) model_data.timing.stop_timer("main.preprocessing") preprocessing_time = model_data.timing.get_total_time("main.preprocessing") config.progress_logger.info( @@ -472,6 +396,7 @@ def solve( # when reporting the final PyROS (master) objective, # since maximization objective is changed to # minimization objective during preprocessing + active_obj_original_sense = model_data.active_obj_original_sense if config.objective_focus == ObjectiveType.nominal: return_soln.final_objective_value = ( active_obj_original_sense diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 6cffdd5d911..f1150017c44 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -1151,6 +1151,79 @@ def validate_pyros_inputs(model, config): return state_vars +def preprocess_model_data(model_data, config, var_partitioning): + """ + Preprocess model data. + """ + original_model = model_data.original_model + + # temporary block to track variable partitioning + # and uncertain parameters after cloning. + # TODO: model may already have an attribute called `util`; + # fix that edge case + original_model.util = Block(concrete=True) + original_model.util.first_stage_variables = var_partitioning.first_stage_variables + original_model.util.second_stage_variables = var_partitioning.second_stage_variables + original_model.util.state_vars = var_partitioning.state_variables + original_model.util.uncertain_params = config.uncertain_params + + model_data.util_block = original_model.util + + # keep track of variables after cloning + cname = unique_component_name(model_data.original_model, 'tmp_var_list') + src_vars = list(model_data.original_model.component_data_objects(Var)) + setattr(model_data.original_model, cname, src_vars) + model_data.working_model = model_data.original_model.clone() + + # identify active objective function. + # (there should only be one at this point) + # recast to minimization if necessary + active_objs = list( + model_data.working_model.component_data_objects( + Objective, active=True, descend_into=True + ) + ) + assert len(active_objs) == 1 + active_obj = active_objs[0] + model_data.active_obj_original_sense = active_obj.sense + recast_to_min_obj(model_data.working_model, active_obj) + + # === Determine first and second-stage objectives + identify_objective_functions(model_data.working_model, active_obj) + active_obj.deactivate() + + # === Put model in standard form + transform_to_standard_form(model_data.working_model) + + # === Replace variable bounds depending on uncertain params with + # explicit inequality constraints + replace_uncertain_bounds_with_constraints( + model_data.working_model, model_data.working_model.util.uncertain_params + ) + + # === Add decision rule information + add_decision_rule_variables(model_data, config) + add_decision_rule_constraints(model_data, config) + + # === Move bounds on control variables to explicit ineq constraints + wm_util = model_data.working_model + + # cast bounds on second-stage and state variables to + # explicit constraints for separation objectives + for c in model_data.working_model.util.second_stage_variables: + turn_bounds_to_constraints(c, wm_util, config) + for c in model_data.working_model.util.state_vars: + turn_bounds_to_constraints(c, wm_util, config) + + # === Make control_variable_bounds array + wm_util.ssv_bounds = [] + for c in model_data.working_model.component_data_objects( + Constraint, descend_into=True + ): + if "bound_con" in c.name: + wm_util.ssv_bounds.append(c) + + def substitute_ssv_in_dr_constraints(model, constraint): ''' Generate the standard_repn for the dr constraints. Generate new expression with replace_expression to ignore From dc8480649baaa62b261d0b2154fdb2e4d02ba514 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 30 May 2024 23:31:32 -0400 Subject: [PATCH 1511/3044] Add method for partitioning vars by adjustability --- pyomo/contrib/pyros/util.py | 272 ++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index f1150017c44..6abc9dc927e 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -14,6 +14,7 @@ ''' from collections import namedtuple +from itertools import count import copy from enum import Enum, auto from pyomo.common.collections import ComponentSet, ComponentMap @@ -1151,6 +1152,273 @@ def validate_pyros_inputs(model, config): return state_vars +def resolve_certain_and_uncertain_bounds( + domain_bound, + interval_bound, + uncertain_params, + bound_type, + ): + """ + Resolve the (lower or upper) + domain and interval bound of a variable + to a certain and uncertain bound, + based on whether the interval bound is an expression + in the uncertain parameters. + """ + assert bound_type in ["lower", "upper"] + + if interval_bound is not None: + uncertain_params_in_interval_bound = ( + ComponentSet(uncertain_params) + & ComponentSet(identify_mutable_parameters(interval_bound)) + ) + else: + uncertain_params_in_interval_bound = False + + if not uncertain_params_in_interval_bound: + uncertain_bound = None + + if interval_bound is None: + certain_bound = domain_bound + elif domain_bound is None: + certain_bound = interval_bound + else: + if bound_type == "lower": + certain_bound = ( + interval_bound if value(interval_bound) >= domain_bound + else domain_bound + ) + else: + certain_bound = ( + interval_bound if value(interval_bound) <= domain_bound + else domain_bound + ) + else: + uncertain_bound = interval_bound + certain_bound = domain_bound + + return certain_bound, uncertain_bound + + +VariableBounds = namedtuple( + "VariableBounds", + ("lower", "eq", "upper"), +) + + +def resolve_bound_types(lower_bound, upper_bound): + """ + Resolve lower and upper bound into lower bound, equality bound, + and upper bound, by comparison of the lower and upper bounds. + """ + if lower_bound is not None and lower_bound is upper_bound: + eq_bound = upper_bound + lower_bound = None + upper_bound = None + else: + eq_bound = None + + return VariableBounds(lower_bound, eq_bound, upper_bound) + + +def get_var_bounds(var, uncertain_params): + """ + Get variable bounds. + """ + # temporarily set domain to Reals to cleanly retrieve + # the interval bound expressions + orig_var_domain = var.domain + var.domain = Reals + + domain_lb, domain_ub = orig_var_domain.bounds() + interval_lb, interval_ub = var.lower, var.upper + + certain_lb, uncertain_lb = resolve_certain_and_uncertain_bounds( + domain_bound=domain_lb, + interval_bound=interval_lb, + uncertain_params=uncertain_params, + bound_type="lower", + ) + certain_ub, uncertain_ub = resolve_certain_and_uncertain_bounds( + domain_bound=domain_ub, + interval_bound=interval_ub, + uncertain_params=uncertain_params, + bound_type="upper", + ) + + certain_bounds = resolve_bound_types( + lower_bound=certain_lb, upper_bound=certain_ub, + ) + uncertain_bounds = resolve_bound_types( + lower_bound=uncertain_lb, upper_bound=uncertain_ub + ) + + # restore variable domain + var.domain = orig_var_domain + + return certain_bounds, uncertain_bounds + + +def get_effective_var_partitioning(model_data, config): + """ + Establish effective variable partitioning. + """ + working_model = model_data.working_model + util_blk = working_model.util + + # variables constrained to a single value: + # - explicitly by the user (`fixed=True`), + # - implicitly by domains/bounds + # - implicitly by equality constraints + effective_fixed_var_to_val_map = ComponentMap() + + # truly nonadjustable variables + effective_first_stage_vars = [] + + # the following variables are immediately known to be nonadjustable: + # - first-stage variables + # - (if decision rule order is 0) second-stage variables + # - all variables fixed explicitly by user or implicitly by bounds + var_type_zip = ( + ("first-stage", util_blk.first_stage_variables), + ("second-stage", util_blk.second_stage_variables), + ("state", util_blk.state_vars), + ) + for vartype, varlist in var_type_zip: + for wvar in varlist: + certain_var_bounds, _ = get_var_bounds(wvar, util_blk.uncertain_params) + + # keep track of fixed variables and the values to + # which they are fixed + if wvar.fixed: + config.progress_logger.debug( + f"The {vartype} variable with name {wvar.name!r} " + "is explicitly fixed." + ) + effective_fixed_var_to_val_map[wvar] = value(wvar) + if certain_var_bounds.eq is not None: + config.progress_logger.debug( + f"The {vartype} variable with name {wvar.name!r} " + "is fixed implicitly by its domain/bounds." + ) + effective_fixed_var_to_val_map[wvar] = value(certain_var_bounds.eq) + + is_var_nonadjustable = ( + wvar.fixed + or certain_var_bounds.eq is not None + or vartype == "first-stage" + or (vartype == "second-stage" and config.decision_rule_order == 0) + ) + if is_var_nonadjustable: + effective_first_stage_vars.append(wvar) + if vartype != "first-stage": + config.progress_logger.debug( + f"Found {vartype} variable with name {wvar.name!r} " + "to be nonadjustable." + ) + + # PRETRIANGULARIZATION + uncertain_params_set = ComponentSet(util_blk.uncertain_params) + applicable_eq_cons = [] + for wcon in working_model.component_data_objects(Constraint, active=True): + if not wcon.equality: + continue + if ComponentSet(identify_mutable_parameters(wcon.expr)) & uncertain_params_set: + continue + + adjustable_vars_in_con = ( + ComponentSet(identify_variables(wcon.body)) + - ComponentSet(effective_first_stage_vars) + ) + if adjustable_vars_in_con: + applicable_eq_cons.append(wcon) + + pretriangular_con_var_map = ComponentMap() + for num_passes in count(1): + new_pretriangular_cons_and_vars = ComponentMap() + for con in applicable_eq_cons: + nonadj_vars_in_con = list( + ComponentSet(identify_variables(con.body)) + - effective_first_stage_vars + ) + if len(nonadj_vars_in_con) == 1: + new_pretriangular_cons_and_vars[con] = nonadj_vars_in_con[0] + if not new_pretriangular_cons_and_vars: + break + + for eqcon, nonadj_var in new_pretriangular_cons_and_vars.items(): + expr_without_fixed_vars = replace_expressions( + expr=eqcon.body - eqcon.upper, + substitution_map={ + id(var): val for var, val in effective_fixed_var_to_val_map.items() + }, + ) + expr_repn = generate_standard_repn( + expr=expr_without_fixed_vars, + quadratic=False, + ) + if nonadj_var in ComponentSet(expr_repn.linear_vars): + if nonadj_var not in ComponentSet(effective_first_stage_vars): + config.progress_logger.debug( + f"Identified pretriangular constraint {eqcon.name!r} " + f"and nonadjustable variable {nonadj_var.name!r}." + ) + effective_first_stage_vars.append(nonadj_var) + pretriangular_con_var_map.setdefault(eqcon, []).append(nonadj_var) + if not expr_repn.nonlinear_vars and len(expr_repn.linear_vars) == 1: + implicit_var_val = ( + -expr_repn.constant / expr_repn.linear_coefs[0] + ) + known_var_val = effective_fixed_var_to_val_map.get( + nonadj_var, + implicit_var_val, + ) + inconsistent_fixed_vals = not math.isclose( + implicit_var_val, + known_var_val, + ) + if inconsistent_fixed_vals: + raise ValueError( + f"The pretriangular var " + "is implicitly fixed by the paired constraint " + f"to the value {implicit_var_val}, which " + "does not match the currently recorded value " + f"{known_var_val}." + ) + config.progress_logger.debug( + "The pretriangular var " + "is fixed implicitly by the paired " + f"constraint to the value {implicit_var_val}." + ) + effective_fixed_var_to_val_map[nonadj_var] = implicit_var_val + + num_pretriangular_vars = ( + sum(len(pvars) for pvars in pretriangular_con_var_map.values()) + ) + config.progress_logger.debug( + f"Identified {len(pretriangular_con_var_map)} pretriangular constraints " + f"and {num_pretriangular_vars} pretriangular nonadjustable variables." + ) + + effective_first_stage_var_set = ComponentSet(effective_first_stage_vars) + effective_second_stage_vars = [ + var + for var in util_blk.second_stage_variables + if var not in effective_first_stage_var_set + ] + effective_state_vars = [ + var + for var in util_blk.state_vars + if var not in effective_first_stage_var_set + ] + + return VariablePartitioning( + first_stage_variables=effective_first_stage_vars, + second_stage_variables=effective_second_stage_vars, + state_variables=effective_state_vars, + ) + + def preprocess_model_data(model_data, config, var_partitioning): """ Preprocess model data. @@ -1175,6 +1443,10 @@ def preprocess_model_data(model_data, config, var_partitioning): setattr(model_data.original_model, cname, src_vars) model_data.working_model = model_data.original_model.clone() + # # extract as many truly nonadjustable variables as possible + # # from the second-stage and state variables + # get_effective_var_partitioning(model_data, config) + # identify active objective function. # (there should only be one at this point) # recast to minimization if necessary From 96355df4552fb1ba8b96231b8320c2876707801f Mon Sep 17 00:00:00 2001 From: Atalay Kutlay Date: Sat, 1 Jun 2024 23:50:59 -0400 Subject: [PATCH 1512/3044] bug: Sort indices before sending to solver --- pyomo/contrib/appsi/solvers/highs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index c948444839d..57a7b1eac72 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -481,7 +481,7 @@ def _remove_constraints(self, cons: List[ConstraintData]): indices_to_remove.append(con_ndx) self._mutable_helpers.pop(con, None) self._solver_model.deleteRows( - len(indices_to_remove), np.array(indices_to_remove) + len(indices_to_remove), np.sort(np.array(indices_to_remove)) ) con_ndx = 0 new_con_map = dict() From 9385c2bbcf4e5589a860d223eea31de3d298bb33 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 2 Jun 2024 13:17:12 -0400 Subject: [PATCH 1513/3044] Update effective variable partitioning subroutine --- pyomo/contrib/pyros/util.py | 302 +++++++++++++++++++++++++----------- 1 file changed, 215 insertions(+), 87 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 6abc9dc927e..fd7377bcfb9 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -1261,7 +1261,16 @@ def get_var_bounds(var, uncertain_params): def get_effective_var_partitioning(model_data, config): """ - Establish effective variable partitioning. + Establish effective variable partitioning + using pretriangularization. + + TODO: + ----- + - Simplify this to the less extensive algorithm + which does not consider + implicit fixing of pretriangular variables. + - Check all comments, docstrings + - Add comprehensive tests """ working_model = model_data.working_model util_blk = working_model.util @@ -1270,147 +1279,266 @@ def get_effective_var_partitioning(model_data, config): # - explicitly by the user (`fixed=True`), # - implicitly by domains/bounds # - implicitly by equality constraints - effective_fixed_var_to_val_map = ComponentMap() + fixed_var_to_val_map = ComponentMap() + fixed_var_set = ComponentSet() # truly nonadjustable variables - effective_first_stage_vars = [] + nonadjustable_var_set = ComponentSet() # the following variables are immediately known to be nonadjustable: # - first-stage variables # - (if decision rule order is 0) second-stage variables # - all variables fixed explicitly by user or implicitly by bounds - var_type_zip = ( + var_type_list_pairs = ( ("first-stage", util_blk.first_stage_variables), ("second-stage", util_blk.second_stage_variables), ("state", util_blk.state_vars), ) - for vartype, varlist in var_type_zip: + for vartype, varlist in var_type_list_pairs: for wvar in varlist: certain_var_bounds, _ = get_var_bounds(wvar, util_blk.uncertain_params) - # keep track of fixed variables and the values to - # which they are fixed + is_var_nonadjustable = ( + vartype == "first-stage" + or (config.decision_rule_order == 0 and vartype == "second-stage") + or wvar.fixed + or certain_var_bounds.eq is not None + ) + if is_var_nonadjustable: + nonadjustable_var_set.add(wvar) + config.progress_logger.debug( + f"The {vartype} variable {wvar.name!r} " + "is nonadjustable, for the following reasons:" + ) + + if vartype == "first-stage": + config.progress_logger.debug(f" the variable has a {vartype} status") + + if config.decision_rule_order == 0 and vartype == "second-stage": + config.progress_logger.debug( + f" the variable is {vartype} and the decision rules are static " + ) + if wvar.fixed: config.progress_logger.debug( - f"The {vartype} variable with name {wvar.name!r} " - "is explicitly fixed." + " the variable is fixed explicitly" ) - effective_fixed_var_to_val_map[wvar] = value(wvar) + # track fixed variables + # and the values to which they are fixed + fixed_var_to_val_map[wvar] = value(wvar) + fixed_var_set.add(wvar) + if certain_var_bounds.eq is not None: config.progress_logger.debug( - f"The {vartype} variable with name {wvar.name!r} " - "is fixed implicitly by its domain/bounds." + " the variable is fixed by domain/bounds" ) - effective_fixed_var_to_val_map[wvar] = value(certain_var_bounds.eq) + # to identify as many nonadjustable variables + # as possible, we also track variables fixed by bounds + fixed_var_to_val_map[wvar] = value(certain_var_bounds.eq) + fixed_var_set.add(wvar) - is_var_nonadjustable = ( - wvar.fixed - or certain_var_bounds.eq is not None - or vartype == "first-stage" - or (vartype == "second-stage" and config.decision_rule_order == 0) - ) - if is_var_nonadjustable: - effective_first_stage_vars.append(wvar) - if vartype != "first-stage": - config.progress_logger.debug( - f"Found {vartype} variable with name {wvar.name!r} " - "to be nonadjustable." - ) - - # PRETRIANGULARIZATION uncertain_params_set = ComponentSet(util_blk.uncertain_params) - applicable_eq_cons = [] + + # determine constraints that are potentially applicable for + # pretriangularization + certain_eq_cons = ComponentSet() for wcon in working_model.component_data_objects(Constraint, active=True): if not wcon.equality: continue if ComponentSet(identify_mutable_parameters(wcon.expr)) & uncertain_params_set: continue + certain_eq_cons.add(wcon) + # identify nonadjustable variables fixed by constraints + initial_nonadjustable_fixing_cons = ComponentSet() + for cert_con in certain_eq_cons: + unfixed_vars_in_con = ( + ComponentSet(identify_variables(cert_con.body)) - fixed_var_set + ) adjustable_vars_in_con = ( - ComponentSet(identify_variables(wcon.body)) - - ComponentSet(effective_first_stage_vars) + ComponentSet(identify_variables(cert_con.body)) - nonadjustable_var_set ) - if adjustable_vars_in_con: - applicable_eq_cons.append(wcon) + if not adjustable_vars_in_con and len(unfixed_vars_in_con) == 1: + nonadj_expr_without_fixed_vars = replace_expressions( + expr=cert_con.body - cert_con.upper, + substitution_map={ + id(var): val for var, val in fixed_var_to_val_map.items() + } + ) + nonadj_var_in_expr = next( + iter(identify_variables(nonadj_expr_without_fixed_vars)) + ) + nonadj_expr_repn = generate_standard_repn( + expr=nonadj_expr_without_fixed_vars, + compute_values=True, + quadratic=False, + ) + num_nonadj_linear_vars = len(nonadj_expr_repn.linear_vars) + if not nonadj_expr_repn.nonlinear_vars and num_nonadj_linear_vars == 1: + initial_nonadjustable_fixing_cons.add(cert_con) + + # the nonadjustable variable is fixed + nonadj_var_fixing_val = ( + -nonadj_expr_repn.constant / nonadj_expr_repn.linear_coefs[0] + ) + if nonadj_var_in_expr not in fixed_var_set: + fixed_var_set.add(nonadj_var_in_expr) + fixed_var_to_val_map[nonadj_var_in_expr] = ( + nonadj_var_fixing_val + ) + config.progress_logger.debug( + f"The nonadjustable variable {nonadj_var_in_expr.name!r} " + f"is fixed to the value {nonadj_var_fixing_val} " + f"by the constraint {cert_con.name!r}" + ) + else: + nonadj_var_fixing_inconsistent = ( + nonadj_var_fixing_val + != fixed_var_to_val_map[nonadj_var_in_expr] + ) + if nonadj_var_fixing_inconsistent: + raise ValueError( + "Model constraints are inconsistent: " + f"the nonadjustable variable {nonadj_var_in_expr.name!r}, " + "already fixed (implicitly or explicitly) to the value " + f"{fixed_var_to_val_map[nonadj_var_in_expr]}, " + "has now been restricted to the value " + f"{nonadj_var_fixing_val} " + f"by the constraint {cert_con.name!r}. " + ) + certain_eq_cons -= initial_nonadjustable_fixing_cons + pretriangular_fixing_cons = ComponentSet() + pretriangular_other_cons = ComponentSet() pretriangular_con_var_map = ComponentMap() for num_passes in count(1): - new_pretriangular_cons_and_vars = ComponentMap() - for con in applicable_eq_cons: - nonadj_vars_in_con = list( - ComponentSet(identify_variables(con.body)) - - effective_first_stage_vars - ) - if len(nonadj_vars_in_con) == 1: - new_pretriangular_cons_and_vars[con] = nonadj_vars_in_con[0] - if not new_pretriangular_cons_and_vars: - break + config.progress_logger.debug( + f"Performing pass number {num_passes} over the certain constraints." + ) + new_pretriangular_con_var_map = ComponentMap() + for ccon in certain_eq_cons: + vars_in_con = ComponentSet(identify_variables(ccon.body - ccon.upper)) + adj_vars_in_con = vars_in_con - nonadjustable_var_set + if len(adj_vars_in_con) == 1: + adj_var_in_con = next(iter(adj_vars_in_con)) + ccon_expr_without_fixed_vars = replace_expressions( + expr=ccon.body - ccon.upper, + substitution_map={ + id(var): val + for var, val in fixed_var_to_val_map.items() + } + ) + ccon_expr_repn = generate_standard_repn( + expr=ccon_expr_without_fixed_vars, + quadratic=False, + compute_values=True, + ) + is_adj_var_linear = ( + adj_var_in_con in ComponentSet(ccon_expr_repn.linear_vars) + ) + if not ccon_expr_repn.nonlinear_vars and is_adj_var_linear: + new_pretriangular_con_var_map[ccon] = adj_var_in_con + config.progress_logger.debug( + f" The variable {adj_var_in_con.name!r} is " + "made nonadjustable by the pretriangular constraint " + f"{ccon.name!r}." + ) + + pretriangular_other_cons.update(new_pretriangular_con_var_map.keys()) + nonadjustable_var_set.update(new_pretriangular_con_var_map.values()) + pretriangular_con_var_map.update(new_pretriangular_con_var_map) - for eqcon, nonadj_var in new_pretriangular_cons_and_vars.items(): - expr_without_fixed_vars = replace_expressions( - expr=eqcon.body - eqcon.upper, + new_pretriangular_fixing_cons = ComponentSet() + for precon, prevar in pretriangular_con_var_map.items(): + precon_expr_without_fixed_vars = replace_expressions( + expr=precon.body - precon.upper, substitution_map={ - id(var): val for var, val in effective_fixed_var_to_val_map.items() + id(var): val + for var, val in fixed_var_to_val_map.items() + if var is not prevar }, ) - expr_repn = generate_standard_repn( - expr=expr_without_fixed_vars, + precon_expr_repn = generate_standard_repn( + expr=precon_expr_without_fixed_vars, quadratic=False, + compute_values=True, + ) + + assert precon_expr_repn.linear_vars + is_fixing_con = ( + not precon_expr_repn.nonlinear_vars + and len(precon_expr_repn.linear_vars) == 1 ) - if nonadj_var in ComponentSet(expr_repn.linear_vars): - if nonadj_var not in ComponentSet(effective_first_stage_vars): + if is_fixing_con: + assert precon_expr_repn.linear_vars[0] is prevar + prevar_fixing_val = ( + - precon_expr_repn.constant / precon_expr_repn.linear_coefs[0] + ) + if prevar not in fixed_var_set: + fixed_var_set.add(prevar) + fixed_var_to_val_map[prevar] = prevar_fixing_val + new_pretriangular_fixing_cons.add(precon) + pretriangular_other_cons.remove(precon) + pretriangular_fixing_cons.add(precon) config.progress_logger.debug( - f"Identified pretriangular constraint {eqcon.name!r} " - f"and nonadjustable variable {nonadj_var.name!r}." - ) - effective_first_stage_vars.append(nonadj_var) - pretriangular_con_var_map.setdefault(eqcon, []).append(nonadj_var) - if not expr_repn.nonlinear_vars and len(expr_repn.linear_vars) == 1: - implicit_var_val = ( - -expr_repn.constant / expr_repn.linear_coefs[0] - ) - known_var_val = effective_fixed_var_to_val_map.get( - nonadj_var, - implicit_var_val, - ) - inconsistent_fixed_vals = not math.isclose( - implicit_var_val, - known_var_val, + f" The pretriangular variable {prevar.name!r} " + f"is fixed to the value {prevar_fixing_val} " + f"by the constraint {precon.name!r}." ) - if inconsistent_fixed_vals: + else: + if prevar_fixing_val != fixed_var_to_val_map[prevar]: raise ValueError( - f"The pretriangular var " - "is implicitly fixed by the paired constraint " - f"to the value {implicit_var_val}, which " - "does not match the currently recorded value " - f"{known_var_val}." + "Model constraints are inconsistent: " + f"the pretriangular variable {prevar.name!r}, " + "already fixed (implicitly or explicitly) to the value " + f"{fixed_var_to_val_map[prevar]}, " + "has now been restricted to the value " + f"{prevar_fixing_val} " + f"by the constraint {precon.name!r}." ) - config.progress_logger.debug( - "The pretriangular var " - "is fixed implicitly by the paired " - f"constraint to the value {implicit_var_val}." - ) - effective_fixed_var_to_val_map[nonadj_var] = implicit_var_val - num_pretriangular_vars = ( - sum(len(pvars) for pvars in pretriangular_con_var_map.values()) - ) + certain_eq_cons -= ComponentSet(new_pretriangular_con_var_map.keys()) + new_pretriangular_cons = ComponentSet(new_pretriangular_con_var_map.keys()) + if not new_pretriangular_cons | new_pretriangular_fixing_cons: + break + + pretriangular_vars = ComponentSet(pretriangular_con_var_map.values()) config.progress_logger.debug( - f"Identified {len(pretriangular_con_var_map)} pretriangular constraints " - f"and {num_pretriangular_vars} pretriangular nonadjustable variables." + f"Identified {len(pretriangular_con_var_map)} pretriangular " + f"constraints and {len(pretriangular_vars)} pretriangular variables " + f"in {num_passes} passes over the certain constraints." ) - effective_first_stage_var_set = ComponentSet(effective_first_stage_vars) + effective_first_stage_vars = list(nonadjustable_var_set) effective_second_stage_vars = [ var for var in util_blk.second_stage_variables - if var not in effective_first_stage_var_set + if var not in nonadjustable_var_set ] effective_state_vars = [ var for var in util_blk.state_vars - if var not in effective_first_stage_var_set + if var not in nonadjustable_var_set ] + num_vars = len( + effective_first_stage_vars + + effective_second_stage_vars + + effective_state_vars + ) + + config.progress_logger.debug("Effective partitioning statistics:") + config.progress_logger.debug( + f" Variables: {num_vars}" + ) + config.progress_logger.debug( + f" Effective first-stage variables: {len(effective_first_stage_vars)}" + ) + config.progress_logger.debug( + f" Effective second-stage variables: {len(effective_second_stage_vars)}" + ) + config.progress_logger.debug( + f" Effective state variables: {len(effective_state_vars)}" + ) return VariablePartitioning( first_stage_variables=effective_first_stage_vars, From 3d4eb861bfffee9d82049f10e5805474996cbf78 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 4 Jun 2024 12:42:15 -0400 Subject: [PATCH 1514/3044] further work on ordered J1 triangulation; add Gn_hamiltonian function --- .../piecewise/piecewise_linear_function.py | 4 + .../piecewise/tests/test_incremental.py | 139 ++++-- .../piecewise/tests/test_triangulations.py | 41 ++ .../piecewise/transform/incremental.py | 12 +- pyomo/contrib/piecewise/triangulations.py | 409 +++++++++++++++--- .../piecewise/union_jack_triangulate.py | 81 ---- 6 files changed, 499 insertions(+), 187 deletions(-) delete mode 100644 pyomo/contrib/piecewise/union_jack_triangulate.py diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 650145a143e..6285e1cd46f 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -21,6 +21,7 @@ ) from pyomo.contrib.piecewise.triangulations import ( get_j1_triangulation, + get_ordered_j1_triangulation, Triangulation, ) from pyomo.core import Any, NonNegativeIntegers, value, Var @@ -309,6 +310,9 @@ def _construct_simplices_from_multivariate_points(self, obj, parent, points, elif tri == Triangulation.J1: triangulation = get_j1_triangulation(points, dimension) obj._triangulation = tri + elif tri == Triangulation.OrderedJ1: + triangulation = get_ordered_j1_triangulation(points, dimension) + obj._triangulation = tri else: raise ValueError( "Unrecognized triangulation specified for '%s': %s" diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index 36bade33381..854629dccf5 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -12,6 +12,7 @@ import pyomo.common.unittest as unittest from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct +from pyomo.contrib.piecewise.triangulations import Triangulation from pyomo.core.base import TransformationFactory from pyomo.core.expr.compare import ( assertExpressionsEqual, @@ -21,59 +22,119 @@ from pyomo.environ import Constraint, SolverFactory, Var, ConcreteModel, Objective, log, value, maximize from pyomo.contrib.piecewise import PiecewiseLinearFunction -from pyomo.contrib.piecewise.transform.incremental import IncrementalInnerGDPTransformation -from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import ( - DisaggregatedLogarithmicInnerGDPTransformation -) +from pyomo.contrib.piecewise.transform.incremental import IncrementalGDPTransformation + +class TestTransformPiecewiseModelToIncrementalMIP(unittest.TestCase): + + def test_solve_log_model(self): + m = make_log_x_model_ordered() + TransformationFactory( + 'contrib.piecewise.incremental' + ).apply_to(m) + TransformationFactory( + 'gdp.bigm' + ).apply_to(m) + SolverFactory('gurobi').solve(m) + ct.check_log_x_model_soln(self, m) + + #def test_solve_univariate_log_model(self): + # m = ConcreteModel() + # m.x = Var(bounds=(1, 10)) + # m.pw_log = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=log) + + # # Here are the linear functions, for safe keeping. + # def f1(x): + # return (log(3) / 2) * x - log(3) / 2 + + # m.f1 = f1 + + # def f2(x): + # return (log(2) / 3) * x + log(3 / 2) + + # m.f2 = f2 + + # def f3(x): + # return (log(5 / 3) / 4) * x + log(6 / ((5 / 3) ** (3 / 2))) + + # m.f3 = f3 -class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): + # m.log_expr = m.pw_log(m.x) + # m.obj = Objective(expr=m.log_expr, sense=maximize) - #def test_solve_log_model(self): - # m = models.make_log_x_model() # TransformationFactory( # 'contrib.piecewise.incremental' # ).apply_to(m) + # m.pprint() # TransformationFactory( # 'gdp.bigm' # ).apply_to(m) + # print('####### PPRINTNG AGAIN AFTER BIGM #######') + # m.pprint() + # # log is increasing so the optimal value should be log(10) # SolverFactory('gurobi').solve(m) - # ct.check_log_x_model_soln(self, m) - - def test_solve_univariate_log_model(self): - m = ConcreteModel() - m.x = Var(bounds=(1, 10)) - m.pw_log = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=log) + # print(f"optimal value is {value(m.obj)}") + # self.assertTrue(abs(value(m.obj) - log(10)) < 0.001) - # Here are the linear functions, for safe keeping. - def f1(x): - return (log(3) / 2) * x - log(3) / 2 - m.f1 = f1 +# Make a version of the log_x model with the simplices properly ordered for the +# incremental transform +def make_log_x_model_ordered(): + m = ConcreteModel() + m.x = Var(bounds=(1, 10)) + m.pw_log = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=log) + m.pw_log._triangulation = Triangulation.AssumeValid - def f2(x): - return (log(2) / 3) * x + log(3 / 2) + # Here are the linear functions, for safe keeping. + def f1(x): + return (log(3) / 2) * x - log(3) / 2 - m.f2 = f2 + m.f1 = f1 - def f3(x): - return (log(5 / 3) / 4) * x + log(6 / ((5 / 3) ** (3 / 2))) + def f2(x): + return (log(2) / 3) * x + log(3 / 2) - m.f3 = f3 + m.f2 = f2 - m.log_expr = m.pw_log(m.x) - m.obj = Objective(expr=m.log_expr, sense=maximize) + def f3(x): + return (log(5 / 3) / 4) * x + log(6 / ((5 / 3) ** (3 / 2))) - TransformationFactory( - 'contrib.piecewise.incremental' - #'contrib.piecewise.disaggregated_logarithmic' - ).apply_to(m) - m.pprint() - TransformationFactory( - 'gdp.bigm' - ).apply_to(m) - print('####### PPRINTNG AGAIN AFTER BIGM #######') - m.pprint() - # log is increasing so the optimal value should be log(10) - SolverFactory('gurobi').solve(m) - print(f"optimal value is {value(m.obj)}") - self.assertTrue(abs(value(m.obj) - log(10)) < 0.001) \ No newline at end of file + m.f3 = f3 + + m.log_expr = m.pw_log(m.x) + m.obj = Objective(expr=m.log_expr) + + m.x1 = Var(bounds=(0, 3)) + m.x2 = Var(bounds=(1, 7)) + + ## apprximates paraboloid x1**2 + x2**2 + def g1(x1, x2): + return 3 * x1 + 5 * x2 - 4 + + m.g1 = g1 + + def g2(x1, x2): + return 3 * x1 + 11 * x2 - 28 + + m.g2 = g2 + # order for incremental transformation + simplices = [ + [(0, 1), (3, 1), (3, 4)], + [(3, 4), (0, 1), (0, 4)], + [(0, 4), (0, 7), (3, 4)], + [(3, 4), (3, 7), (0, 7)], + ] + m.pw_paraboloid = PiecewiseLinearFunction( + simplices=simplices, linear_functions=[g1, g1, g2, g2] + ) + m.pw_paraboloid._triangulation = Triangulation.AssumeValid + m.paraboloid_expr = m.pw_paraboloid(m.x1, m.x2) + + def c_rule(m, i): + if i == 0: + return m.x >= m.paraboloid_expr + else: + return (1, m.x1, 2) + + m.indexed_c = Constraint([0, 1], rule=c_rule) + + return m \ No newline at end of file diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index 28ee61909da..19b17b24d36 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -17,7 +17,10 @@ get_j1_triangulation, get_incremental_simplex_ordering, get_incremental_simplex_ordering_assume_connected_by_n_face, + get_Gn_hamiltonian, ) +from math import factorial +import itertools class TestTriangulations(unittest.TestCase): @@ -143,3 +146,41 @@ def test_J1_2d_ordering_3(self): second_simplex = ordered_triangulation[idx + 1] # test property (2) which also guarantees property (1) self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + def check_Gn_hamiltonian_path(self, n, start_permutation, target_symbol, last): + path = get_Gn_hamiltonian(n, start_permutation, target_symbol, last) + self.assertEqual(len(path), factorial(n)) + self.assertEqual(path[0], start_permutation) + if last: + self.assertEqual(path[-1][-1], target_symbol) + else: + self.assertEqual(path[-1][0], target_symbol) + for pi in itertools.permutations(range(1, n + 1), n): + self.assertTrue(tuple(pi) in path) + for i in range(len(path) - 1): + diff_indices = [j for j in range(n) if path[i][j] != path[i + 1][j]] + self.assertEqual(len(diff_indices), 2) + self.assertEqual(diff_indices[0], diff_indices[1] - 1) + self.assertEqual(path[i][diff_indices[0]], path[i + 1][diff_indices[1]]) + self.assertEqual(path[i][diff_indices[1]], path[i + 1][diff_indices[0]]) + + def test_Gn_hamiltonian_paths(self): + # each of the base cases + self.check_Gn_hamiltonian_path(4, (1, 2, 3, 4), 1, False) + self.check_Gn_hamiltonian_path(4, (1, 2, 3, 4), 2, False) + self.check_Gn_hamiltonian_path(4, (1, 2, 3, 4), 3, False) + self.check_Gn_hamiltonian_path(4, (1, 2, 3, 4), 4, False) + # some variants with start permutations and/or last + self.check_Gn_hamiltonian_path(4, (3, 4, 1, 2), 2, False) + self.check_Gn_hamiltonian_path(4, (1, 3, 2, 4), 3, True) + self.check_Gn_hamiltonian_path(4, (1, 4, 2, 3), 4, True) + self.check_Gn_hamiltonian_path(4, (1, 2, 3, 4), 2, True) + # some recursive cases + self.check_Gn_hamiltonian_path(5, (1, 2, 3, 4, 5), 1, False) + self.check_Gn_hamiltonian_path(5, (1, 2, 3, 4, 5), 3, False) + self.check_Gn_hamiltonian_path(5, (1, 2, 3, 4, 5), 5, False) + self.check_Gn_hamiltonian_path(5, (1, 2, 4, 3, 5), 5, True) + self.check_Gn_hamiltonian_path(6, (6, 1, 2, 4, 3, 5), 5, True) + self.check_Gn_hamiltonian_path(6, (6, 1, 2, 4, 3, 5), 5, False) + self.check_Gn_hamiltonian_path(7, (1, 2, 3, 4, 5, 6, 7), 7, False) + diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index dd26b8c5182..266784297e0 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -10,9 +10,10 @@ # ___________________________________________________________________________ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( + PiecewiseLinearTransformationBase, ) +from pyomo.contrib.piecewise.triangulations import Triangulation from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet, Param from pyomo.core.base import TransformationFactory from pyomo.gdp import Disjunct, Disjunction @@ -20,6 +21,7 @@ from pyomo.core.expr.visitor import SimpleExpressionVisitor from pyomo.core.expr.current import identify_components from math import ceil, log2 +import logging @TransformationFactory.register( "contrib.piecewise.incremental", @@ -27,18 +29,20 @@ TODO document """, ) -class IncrementalInnerGDPTransformation(PiecewiseLinearToGDP): +class IncrementalGDPTransformation(PiecewiseLinearTransformationBase): """ TODO document """ - CONFIG = PiecewiseLinearToGDP.CONFIG() + CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = "pw_linear_incremental" # Implement to use PiecewiseLinearToGDP. This function returns the Var # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): self.DEBUG = False + if not (pw_linear_func.triangulation == Triangulation.OrderedJ1 or pw_linear_func.triangulation == Triangulation.AssumeValid): + logging.getLogger('pyomo.contrib.piecewise.transform.incremental').warning("Incremental transformation specified, but the triangulation may not be appropriately ordered. This is likely to lead to incorrect results!") # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 2e2f4e07cad..c5eaaead327 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -29,23 +29,42 @@ TerminationCondition, ) from pyomo.common.dependencies import attempt_import + nx, nx_available = attempt_import( 'networkx', 'Networkx is required to calculate incremental ordering.' ) + class Triangulation: + AssumeValid = 0 Delaunay = 1 J1 = 2 + OrderedJ1 = 3 + def get_j1_triangulation(points, dimension, ordered=False): points_map, num_pts = _process_points_j1(points, dimension) + simplices_list = _get_j1_triangulation(points_map, num_pts - 1, dimension) + # make a duck-typed thing that superficially looks like an instance of + # scipy.spatial.Delaunay (these are NDarrays in the original) + triangulation = SimpleNamespace() + triangulation.points = list(range(len(simplices_list))) + triangulation.simplices = {i: simplices_list[i] for i in triangulation.points} + triangulation.coplanar = [] - if ordered and dimension == 2: + return triangulation + + +def get_ordered_j1_triangulation(points, dimension): + points_map, num_pts = _process_points_j1(points, dimension) + if dimension == 2: simplices_list = _get_j1_triangulation_2d_ordered(points_map, num_pts - 1) else: - simplices_list = _get_j1_triangulation(points_map, num_pts - 1, dimension) - # make a duck-typed thing that superficially looks like an instance of - # scipy.spatial.Delaunay (these are NDarrays in the original) + raise DeveloperError("Unimplemented!") + # elif dimension == 3: + # return _get_j1_triangulation_3d(points_map, num_pts - 1) + # else: + # return _get_j1_triangulation_for_more_than_4d(points_map, num_pts - 1) triangulation = SimpleNamespace() triangulation.points = list(range(len(simplices_list))) triangulation.simplices = {i: simplices_list[i] for i in triangulation.points} @@ -53,13 +72,6 @@ def get_j1_triangulation(points, dimension, ordered=False): return triangulation - #if dimension == 2: - # return _get_j1_triangulation_2d(points_map, num_pts) - #elif dimension == 3: - # return _get_j1_triangulation_3d(points, dimension) - #else: - # return _get_j1_triangulation_for_more_than_4d(points, dimension) - # Does some validation but mostly assumes the user did the right thing def _process_points_j1(points, dimension): @@ -67,10 +79,14 @@ def _process_points_j1(points, dimension): raise ValueError("Points not consistent with specified dimension") num_pts = round(len(points) ** (1 / dimension)) if not len(points) == num_pts**dimension: - raise ValueError("'points' must have points forming an n-dimensional grid with straight grid lines and the same odd number of points in each axis") + raise ValueError( + "'points' must have points forming an n-dimensional grid with straight grid lines and the same odd number of points in each axis" + ) if not num_pts % 2 == 1: - raise ValueError("'points' must have points forming an n-dimensional grid with straight grid lines and the same odd number of points in each axis") - + raise ValueError( + "'points' must have points forming an n-dimensional grid with straight grid lines and the same odd number of points in each axis" + ) + # munge the points into an organized map with n-dimensional keys points.sort() points_map = {} @@ -81,6 +97,7 @@ def _process_points_j1(points, dimension): points_map[point_index] = points[point_flat_index] return points_map, num_pts + # This implements the J1 "Union Jack" triangulation (Todd 77) as explained by # Vielma 2010. # Triangulate {0, ..., K}^n for even K using the J1 triangulation, mapping the @@ -91,9 +108,11 @@ def _get_j1_triangulation(points_map, K, n): # 1, 3, ..., K - 1 axis_odds = range(1, K, 2) V_0 = itertools.product(axis_odds, repeat=n) - big_iterator = itertools.product(V_0, - itertools.permutations(range(0, n), n), - itertools.product((-1, 1), repeat=n)) + big_iterator = itertools.product( + V_0, + itertools.permutations(range(0, n), n), + itertools.product((-1, 1), repeat=n), + ) ret = [] for v_0, pi, s in big_iterator: simplex = [] @@ -105,9 +124,10 @@ def _get_j1_triangulation(points_map, K, n): simplex.append(points_map[*current]) # sort this because it might happen again later and we'd like to stay # consistent. Undo this if it's slow. - ret.append(sorted(simplex)) + ret.append(sorted(simplex)) return ret + # Implement proof-by-picture from Todd 1977. I do the reverse order he does # and also keep the pictures slightly more regular to make things easier to # implement. Also remember that Todd's drawing is misleading to the point of @@ -117,11 +137,13 @@ def _get_j1_triangulation_2d_ordered(points_map, num_pts): square_parity_tlbr = lambda x, y: x % 2 == y % 2 # check when we are in a "turnaround square" as seen in the picture is_turnaround = lambda x, y: x >= num_pts / 2 and y == (num_pts / 2) - 1 + class Direction(Enum): left = 0 down = 1 up = 2 right = 3 + facing = None simplices = {} @@ -129,18 +151,36 @@ class Direction(Enum): # make it easier to read what I'm doing def add_bottom_right(): - simplices[len(simplices)] = (points_map[x, y], points_map[x + 1, y], points_map[x + 1, y + 1]) + simplices[len(simplices)] = ( + points_map[x, y], + points_map[x + 1, y], + points_map[x + 1, y + 1], + ) + def add_top_right(): - simplices[len(simplices)] = (points_map[x, y + 1], points_map[x + 1, y], points_map[x + 1, y + 1]) + simplices[len(simplices)] = ( + points_map[x, y + 1], + points_map[x + 1, y], + points_map[x + 1, y + 1], + ) + def add_bottom_left(): - simplices[len(simplices)] = (points_map[x, y], points_map[x, y + 1], points_map[x + 1, y]) - def add_top_left(): - simplices[len(simplices)] = (points_map[x, y], points_map[x, y + 1], points_map[x + 1, y + 1]) + simplices[len(simplices)] = ( + points_map[x, y], + points_map[x, y + 1], + points_map[x + 1, y], + ) + def add_top_left(): + simplices[len(simplices)] = ( + points_map[x, y], + points_map[x, y + 1], + points_map[x + 1, y + 1], + ) # identify square by bottom-left corner x, y = start_square - used_squares = set() # not used for the turnaround squares + used_squares = set() # not used for the turnaround squares # depending on parity we will need to go either up or down to start if square_parity_tlbr(x, y): @@ -151,10 +191,10 @@ def add_top_left(): add_top_right() facing = Direction.up y += 1 - + # state machine - while (True): - match(facing): + while True: + match (facing): case Direction.left: if square_parity_tlbr(x, y): add_bottom_right() @@ -276,9 +316,11 @@ def add_top_left(): def _get_j1_triangulation_3d(points, dimension): pass + def _get_j1_triangulation_for_more_than_4d(points, dimension): pass + def get_incremental_simplex_ordering(simplices, subsolver='gurobi'): # Set up a MIP (err, MIQCP) that orders our simplices and their vertices for us # in the following way: @@ -287,7 +329,7 @@ def get_incremental_simplex_ordering(simplices, subsolver='gurobi'): # with T_{i+1}. It doesn't have to be a whole face; just a vertex is enough. # (2) On each simplex T_i, the vertices are ordered T_i^1, ..., T_i^n such # that T_i^n = T_{i+1}^1 - # + # # Note that (2) implies (1), so we only need to enforce that. # # TODO: issue: I don't think gurobi is magical enough to notice the special structure @@ -310,7 +352,10 @@ def get_incremental_simplex_ordering(simplices, subsolver='gurobi'): # The rest we can order arbitrarily after finishing the MIP solve. m.SimplexVerticesCount = Param(initialize=len(simplices[0])) m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) - @m.Param(m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) + + @m.Param( + m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary + ) def TestVerticesEqual(m, i, n, j, k): return 1 if simplices[i][n] == simplices[j][k] else 0 @@ -320,12 +365,12 @@ def TestVerticesEqual(m, i, n, j, k): m.vertex_is_first = Var(m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) m.vertex_is_last = Var(m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) - # Constraints # Each simplex should have a slot and each slot should have a simplex @m.Constraint(m.SIMPLICES) def schedule_each_simplex(m, i): return sum(m.x[i, j] for j in m.SIMPLICES) == 1 + @m.Constraint(m.SIMPLICES) def schedule_each_slot(m, j): return sum(m.x[i, j] for i in m.SIMPLICES) == 1 @@ -334,39 +379,49 @@ def schedule_each_slot(m, j): @m.Constraint(m.SIMPLICES) def one_first_vertex(m, i): return sum(m.vertex_is_first[i, n] for n in m.VERTEX_INDICES) == 1 + @m.Constraint(m.SIMPLICES) def one_last_vertex(m, i): return sum(m.vertex_is_last[i, n] for n in m.VERTEX_INDICES) == 1 - + # The last vertex cannot be the same as the first vertex @m.Constraint(m.SIMPLICES, m.VERTEX_INDICES) def first_last_distinct(m, i, n): return m.vertex_is_first[i, n] * m.vertex_is_last[i, n] == 0 - + # Enforce property (2). This also guarantees property (1) @m.Constraint(m.SIMPLICES, m.SIMPLICES) def vertex_order(m, i, j): # Enforce only when j is the simplex following i. If not, RHS is zero - return ( - sum(m.vertex_is_last[i, n] * m.vertex_is_first[j, k] * m.TestVerticesEqual[i, n, j, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) - >= sum(m.x[i, p] * m.x[j, p + 1] for p in m.SIMPLICES if p != m.SimplicesCount - 1) + return sum( + m.vertex_is_last[i, n] + * m.vertex_is_first[j, k] + * m.TestVerticesEqual[i, n, j, k] + for n in m.VERTEX_INDICES + for k in m.VERTEX_INDICES + ) >= sum( + m.x[i, p] * m.x[j, p + 1] for p in m.SIMPLICES if p != m.SimplicesCount - 1 ) - + # Trivial objective (do I need this?) m.obj = Objective(expr=0) - + # Solve model results = SolverFactory(subsolver).solve(m, tee=True) - match(results.solver.termination_condition): + match (results.solver.termination_condition): case TerminationCondition.infeasible: - raise ValueError("The triangulation was impossible to suitably order for the incremental transformation. Try a different triangulation, such as J1.") + raise ValueError( + "The triangulation was impossible to suitably order for the incremental transformation. Try a different triangulation, such as J1." + ) case TerminationCondition.optimal: pass case _: - raise ValueError(f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}") - + raise ValueError( + f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}" + ) + # Retrieve data - #m.pprint() + # m.pprint() new_simplices = {} for j in m.SIMPLICES: for i in m.SIMPLICES: @@ -392,22 +447,25 @@ def vertex_order(m, i, j): break return new_simplices + # If we have the assumption that our ordering is possible such that consecutively # ordered simplices share at least a one-face, then getting an order for the -# simplices is enough to get one for the edges and we "just" need to find a +# simplices is enough to get one for the edges and we "just" need to find a # Hamiltonian path -def get_incremental_simplex_ordering_assume_connected_by_n_face(simplices, connected_face_dim, subsolver='gurobi'): +def get_incremental_simplex_ordering_assume_connected_by_n_face( + simplices, connected_face_dim, subsolver='gurobi' +): if connected_face_dim == 0: return get_incremental_simplex_ordering(simplices) - #if not nx_available: + # if not nx_available: # raise ImportError('Missing Networkx') - #G = nx.Graph() - #G.add_nodes_from(range(len(simplices))) - #for i in range(len(simplices)): + # G = nx.Graph() + # G.add_nodes_from(range(len(simplices))) + # for i in range(len(simplices)): # for j in range(i + 1, len(simplices)): # if len(set(simplices[i]) & set(simplices[j])) >= n + 1: # G.add_edge(i, j) - + # ask Gurobi again because networkx doesn't seem to have a general hamiltonian # path and I don't want to implement it myself @@ -420,7 +478,10 @@ def get_incremental_simplex_ordering_assume_connected_by_n_face(simplices, conne # The rest we can order arbitrarily after finishing the MIP solve. m.SimplexVerticesCount = Param(initialize=len(simplices[0])) m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) - @m.Param(m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) + + @m.Param( + m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary + ) def TestVerticesEqual(m, i, n, j, k): return 1 if simplices[i][n] == simplices[j][k] else 0 @@ -433,31 +494,55 @@ def TestVerticesEqual(m, i, n, j, k): @m.Constraint(m.SIMPLICES) def schedule_each_simplex(m, i): return sum(m.x[i, j] for j in m.SIMPLICES) == 1 + @m.Constraint(m.SIMPLICES) def schedule_each_slot(m, j): return sum(m.x[i, j] for i in m.SIMPLICES) == 1 - + # Enforce property (1) @m.Constraint(m.SIMPLICES) def simplex_order(m, i): # anything with at least a vertex in common is a neighbor - neighbors = [s for s in m.SIMPLICES if sum(m.TestVerticesEqual[i, n, s, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) >= connected_face_dim + 1 and s != i] - #print(f'neighbors of {i} are {neighbors}') - return sum(m.x[i, j] * m.x[k, j + 1] for j in m.SIMPLICES if j != m.SimplicesCount - 1 for k in neighbors) + m.x[i, m.SimplicesCount - 1] == 1 - + neighbors = [ + s + for s in m.SIMPLICES + if sum( + m.TestVerticesEqual[i, n, s, k] + for n in m.VERTEX_INDICES + for k in m.VERTEX_INDICES + ) + >= connected_face_dim + 1 + and s != i + ] + # print(f'neighbors of {i} are {neighbors}') + return ( + sum( + m.x[i, j] * m.x[k, j + 1] + for j in m.SIMPLICES + if j != m.SimplicesCount - 1 + for k in neighbors + ) + + m.x[i, m.SimplicesCount - 1] + == 1 + ) + # Trivial objective (do I need this?) m.obj = Objective(expr=0) - - #m.pprint() + + # m.pprint() # Solve model results = SolverFactory(subsolver).solve(m, tee=True) - match(results.solver.termination_condition): + match (results.solver.termination_condition): case TerminationCondition.infeasible: - raise ValueError(f"The triangulation was impossible to suitably order for the incremental transformation under the assumption that consecutive simplices share {connected_face_dim}-faces. Try relaxing that assumption, or try a different triangulation, such as J1.") + raise ValueError( + f"The triangulation was impossible to suitably order for the incremental transformation under the assumption that consecutive simplices share {connected_face_dim}-faces. Try relaxing that assumption, or try a different triangulation, such as J1." + ) case TerminationCondition.optimal: pass case _: - raise ValueError(f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}") + raise ValueError( + f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}" + ) # Retrieve data new_simplices = {} @@ -471,6 +556,7 @@ def simplex_order(m, i): fix_vertices_incremental_order(new_simplices) return new_simplices + # Fix vertices (in place) when the simplices are right but vertices are not def fix_vertices_incremental_order(simplices): last_vertex_index = len(simplices[0]) - 1 @@ -487,7 +573,7 @@ def fix_vertices_incremental_order(simplices): if simplex[n] == simplices[i - 1][last_vertex_index]: first = n break - + if i == len(simplices) - 1: last = last_vertex_index else: @@ -497,11 +583,208 @@ def fix_vertices_incremental_order(simplices): break if first == None or last == None: raise DeveloperError("Couldn't fix vertex ordering for incremental.") - + # reorder the simplex with the desired first and last new_simplex = [simplex[first]] for n in range(last_vertex_index + 1): if n != first and n != last: new_simplex.append(simplex[n]) new_simplex.append(simplex[last]) - simplices[i] = new_simplex \ No newline at end of file + simplices[i] = new_simplex + + +# G_n is the graph on n! vertices where the vertices are permutations in S_n and +# two vertices are adjacent if they are related by swapping the values of +# pi(i - 1) and pi(i) for some i in {2, ..., n}. +# +# This function gets a hamiltonian path through G_n, starting from a fixed +# starting permutation, such that a fixed target symbol is either the image +# rho(1), or it is rho(n), depending on whether first or last is requested, +# where rho is the final permutation. +def get_Gn_hamiltonian(n, start_permutation, target_symbol, last): + if n < 4: + raise ValueError("n must be at least 4 for this operation to be possible") + # first is enough because we can just reverse every permutation + if last: + return [ + tuple(reversed(pi)) + for pi in get_Gn_hamiltonian( + n, tuple(reversed(start_permutation)), target_symbol, False + ) + ] + # trivial start permutation is enough because we can map it through at the end + if start_permutation != tuple(range(1, n + 1)): + new_target_symbol = [ + x for x in range(1, n + 1) if start_permutation[x - 1] == target_symbol + ][0] # pi^-1(j) + return [ + tuple(start_permutation[pi[i] - 1] for i in range(n)) + for pi in _get_Gn_hamiltonian(n, new_target_symbol) + ] + else: + return _get_Gn_hamiltonian(n, target_symbol) + + +# Assume the starting permutation is (1, ..., n) and the target symbol needs to +# be in the first position of the last permutation +def _get_Gn_hamiltonian(n, target_symbol): + # base case: proof by picture from Todd, Figure 2 + # note: Figure 2 contains an error, like half the figures and paragraphs do + if n == 4: + if target_symbol == 1: + return [ + (1, 2, 3, 4), + (2, 1, 3, 4), + (2, 1, 4, 3), + (2, 4, 1, 3), + (4, 2, 1, 3), + (4, 2, 3, 1), + (2, 4, 3, 1), + (2, 3, 4, 1), + (2, 3, 1, 4), + (3, 2, 1, 4), + (3, 2, 4, 1), + (3, 4, 2, 1), + (4, 3, 2, 1), + (4, 3, 1, 2), + (3, 4, 1, 2), + (3, 1, 4, 2), + (3, 1, 2, 4), + (1, 3, 2, 4), + (1, 3, 4, 2), + (1, 4, 3, 2), + (4, 1, 3, 2), + (4, 1, 2, 3), + (1, 4, 2, 3), + (1, 2, 4, 3), + ] + elif target_symbol == 2: + return [ + (1, 2, 3, 4), + (1, 2, 4, 3), + (1, 4, 2, 3), + (4, 1, 2, 3), + (4, 1, 3, 2), + (1, 4, 3, 2), + (1, 3, 4, 2), + (1, 3, 2, 4), + (3, 1, 2, 4), + (3, 1, 4, 2), + (3, 4, 1, 2), + (4, 3, 1, 2), + (4, 3, 2, 1), + (3, 4, 2, 1), + (3, 2, 4, 1), + (3, 2, 1, 4), + (2, 3, 1, 4), + (2, 3, 4, 1), + (2, 4, 3, 1), + (4, 2, 3, 1), + (4, 2, 1, 3), + (2, 4, 1, 3), + (2, 1, 4, 3), + (2, 1, 3, 4), + ] + elif target_symbol == 3: + return [ + (1, 2, 3, 4), + (1, 2, 4, 3), + (1, 4, 2, 3), + (4, 1, 2, 3), + (4, 1, 3, 2), + (1, 4, 3, 2), + (1, 3, 4, 2), + (1, 3, 2, 4), + (3, 1, 2, 4), + (3, 1, 4, 2), + (3, 4, 1, 2), + (4, 3, 1, 2), + (4, 3, 2, 1), + (3, 4, 2, 1), + (3, 2, 4, 1), + (2, 3, 4, 1), + (2, 4, 3, 1), + (4, 2, 3, 1), + (4, 2, 1, 3), + (2, 4, 1, 3), + (2, 1, 4, 3), + (2, 1, 3, 4), + (2, 3, 1, 4), + (3, 2, 1, 4), + ] + elif target_symbol == 4: + return [ + (1, 2, 3, 4), + (2, 1, 3, 4), + (2, 3, 1, 4), + (3, 2, 1, 4), + (3, 1, 2, 4), + (1, 3, 2, 4), + (1, 3, 4, 2), + (3, 1, 4, 2), + (3, 4, 1, 2), + (3, 4, 2, 1), + (3, 2, 4, 1), + (2, 3, 4, 1), + (2, 4, 3, 1), + (2, 4, 1, 3), + (2, 1, 4, 3), + (1, 2, 4, 3), + (1, 4, 2, 3), + (1, 4, 3, 2), + (4, 1, 3, 2), + (4, 3, 1, 2), + (4, 3, 2, 1), + (4, 2, 3, 1), + (4, 2, 1, 3), + (4, 1, 2, 3), + ] + # unreachable + else: + # recursive case + if target_symbol < n: # non-awful case + # Well, it's still pretty awful. + idx = n - 1 + facing = -1 + ret = [] + for pi in _get_Gn_hamiltonian(n - 1, target_symbol): + for _ in range(n): + l = list(pi) + l.insert(idx, n) + ret.append(tuple(l)) + idx += facing + if (idx == -1 or idx == n): # went too far + facing *= -1 + idx += facing # stay once because we get a new pi + return ret + else: # awful case, target_symbol = n + idx = 0 + facing = 1 + ret = [] + for pi in _get_Gn_hamiltonian(n - 1, n - 1): + for _ in range(n): + l = [x + 1 for x in pi] + l.insert(idx, 1) + ret.append(tuple(l)) + idx += facing + if (idx == -1 or idx == n): # went too far + facing *= -1 + idx += facing # stay once because we get a new pi + # now we almost have a correct sequence, but it ends with (1, n, ...) + # instead of (n, 1, ...) so we need to do some surgery + last = ret.pop() # of form (1, n, i, j, ...) + second_last = ret.pop() # of form (n, 1, i, j, ...) + i = last[2] + j = last[3] + test = list(last) + test[0] = n + test[1] = 1 + test[2] = j + test[3] = i + idx = ret.index(tuple(test)) + ret.insert(idx, second_last) + ret.insert(idx, last) + return ret + + + diff --git a/pyomo/contrib/piecewise/union_jack_triangulate.py b/pyomo/contrib/piecewise/union_jack_triangulate.py deleted file mode 100644 index 2853add8161..00000000000 --- a/pyomo/contrib/piecewise/union_jack_triangulate.py +++ /dev/null @@ -1,81 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import itertools -from math import factorial -import time - -# This implements the J1 "Union Jack" triangulation (Todd 77) as explained by -# Vielma 2010. - -# Triangulate {0, ..., K}^n for even K using the J1 triangulation. -def triangulate(K, n): - if K % 2 != 0: - raise ValueError("K must be even") - # 1, 3, ..., K - 1 - axis_odds = range(1, K, 2) - V_0 = itertools.product(axis_odds, repeat=n) - big_iterator = itertools.product(V_0, - itertools.permutations(range(0, n), n), - itertools.product((-1, 1), repeat=n)) - J1 = [] - for v_0, pi, s in big_iterator: - simplex = [] - current = list(v_0) - simplex.append(current) - for i in range(0, n): - current = current.copy() - current[pi[i]] += s[pi[i]] - simplex.append(current) - J1.append(simplex) - return J1 - -if __name__ == '__main__': - # do some tests. TODO move to real test file - start0 = time.time() - small_2d = triangulate(2, 2) - elapsed0 = time.time() - start0 - print(f"triangulated small_2d in {elapsed0} sec.") - assert len(small_2d) == 8 - assert small_2d == [[[1, 1], [0, 1], [0, 0]], - [[1, 1], [0, 1], [0, 2]], - [[1, 1], [2, 1], [2, 0]], - [[1, 1], [2, 1], [2, 2]], - [[1, 1], [1, 0], [0, 0]], - [[1, 1], [1, 2], [0, 2]], - [[1, 1], [1, 0], [2, 0]], - [[1, 1], [1, 2], [2, 2]]] - start1 = time.time() - bigger_2d = triangulate(4, 2) - elapsed1 = time.time() - start1 - print(f"triangulated bigger_2d in {elapsed1} sec.") - assert len(bigger_2d) == 32 - - start2 = time.time() - medium_3d = triangulate(12, 3) - elapsed2 = time.time() - start2 - print(f"triangulated medium_3d in {elapsed2} sec.") - # A J1 triangulation of {0, ..., K}^n has K^n * n! simplices - assert len(medium_3d) == 12**3 * factorial(3) - - start3 = time.time() - big_4d = triangulate(20, 4) - elapsed3 = time.time() - start3 - print(f"triangulated big_4d in {elapsed3} sec.") - assert len(big_4d) == 20**4 * factorial(4) - - print("starting huge_5d") - start4 = time.time() - huge_5d = triangulate(10, 5) - elapsed4 = time.time() - start4 - print(f"triangulated huge_5d in {elapsed4} sec.") - - print("Success") From c19f91d013729b0ae9df8b18b88662d87576e78c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 4 Jun 2024 11:06:10 -0600 Subject: [PATCH 1515/3044] Archiving current state of OnlineDocs --- doc/{OnlineDocs => Archive}/Makefile | 0 doc/{OnlineDocs => Archive}/README.md | 0 .../_static/theme_overrides.css | 0 .../advanced_topics/flattener/index.rst | 0 .../advanced_topics/flattener/motivation.rst | 0 .../advanced_topics/flattener/reference.rst | 0 .../advanced_topics/index.rst | 0 .../advanced_topics/linearexpression.rst | 0 .../advanced_topics/persistent_solvers.rst | 0 .../advanced_topics/sos_constraints.rst | 0 .../advanced_topics/units_container.rst | 0 doc/{OnlineDocs => Archive}/bibliography.rst | 0 doc/{OnlineDocs => Archive}/citing_pyomo.rst | 0 doc/{OnlineDocs => Archive}/conf.py | 0 .../contributed_packages/communities_8pp.png | Bin .../contributed_packages/communities_decode_1.png | Bin .../contributed_packages/community.rst | 0 .../contributed_packages/doe/CCSI-license.txt | 0 .../contributed_packages/doe/doe.rst | 0 .../contributed_packages/doe/flowchart.png | Bin .../contributed_packages/doe/grid-1.png | Bin .../contributed_packages/doe/reactor.png | Bin .../contributed_packages/doe/uml.png | Bin .../contributed_packages/gdpopt.rst | 0 .../contributed_packages/gdpopt_flowchart.png | Bin .../contributed_packages/iis.rst | 0 .../contributed_packages/incidence/api.rst | 0 .../contributed_packages/incidence/config.rst | 0 .../contributed_packages/incidence/connected.rst | 0 .../incidence/dulmage_mendelsohn.rst | 0 .../contributed_packages/incidence/incidence.rst | 0 .../contributed_packages/incidence/index.rst | 0 .../contributed_packages/incidence/interface.rst | 0 .../contributed_packages/incidence/matching.rst | 0 .../contributed_packages/incidence/overview.rst | 0 .../contributed_packages/incidence/scc_solver.rst | 0 .../incidence/triangularize.rst | 0 .../contributed_packages/incidence/tutorial.bt.rst | 0 .../incidence/tutorial.btsolve.rst | 0 .../contributed_packages/incidence/tutorial.dm.rst | 0 .../contributed_packages/incidence/tutorial.rst | 0 .../contributed_packages/index.rst | 0 .../contributed_packages/latex_printer.rst | 0 .../contributed_packages/mcpp.rst | 0 .../contributed_packages/mindtpy.rst | 0 .../contributed_packages/mpc/api.rst | 0 .../contributed_packages/mpc/conversion.rst | 0 .../contributed_packages/mpc/data.rst | 0 .../contributed_packages/mpc/examples.rst | 0 .../contributed_packages/mpc/faq.rst | 0 .../contributed_packages/mpc/index.rst | 0 .../contributed_packages/mpc/interface.rst | 0 .../contributed_packages/mpc/modeling.rst | 0 .../contributed_packages/mpc/overview.rst | 0 .../contributed_packages/multistart.rst | 0 .../contributed_packages/parmest/api.rst | 0 .../contributed_packages/parmest/boxplot.png | Bin .../contributed_packages/parmest/covariance.rst | 0 .../contributed_packages/parmest/datarec.rst | 0 .../contributed_packages/parmest/driver.rst | 0 .../contributed_packages/parmest/examples.rst | 0 .../contributed_packages/parmest/graphics.rst | 0 .../contributed_packages/parmest/index.rst | 0 .../contributed_packages/parmest/installation.rst | 0 .../contributed_packages/parmest/overview.rst | 0 .../parmest/pairwise_plot_CI.png | Bin .../parmest/pairwise_plot_LR.png | Bin .../contributed_packages/parmest/parallel.rst | 0 .../contributed_packages/parmest/scencreate.rst | 0 .../contributed_packages/preprocessing.rst | 0 .../contributed_packages/pynumero/api.rst | 0 .../pynumero/backward_compatibility.rst | 0 .../contributed_packages/pynumero/index.rst | 0 .../contributed_packages/pynumero/installation.rst | 0 .../pynumero/pynumero.interfaces.ampl_nlp.rst | 0 .../pynumero/pynumero.interfaces.asl_nlp.rst | 0 .../pynumero/pynumero.interfaces.extended_nlp.rst | 0 .../pynumero.interfaces.external_grey_box_model.rst | 0 .../pynumero/pynumero.interfaces.nlp.rst | 0 .../pynumero/pynumero.interfaces.projected_nlp.rst | 0 .../pynumero.interfaces.pyomo_grey_box_nlp.rst | 0 .../pynumero/pynumero.interfaces.pyomo_nlp.rst | 0 .../pynumero/pynumero.interfaces.rst | 0 .../pynumero/pynumero.linalg.base.rst | 0 .../pynumero/pynumero.linalg.ma27.rst | 0 .../pynumero/pynumero.linalg.ma57.rst | 0 .../pynumero/pynumero.linalg.mumps.rst | 0 .../pynumero/pynumero.linalg.rst | 0 .../pynumero/pynumero.linalg.scipy.rst | 0 .../pynumero/pynumero.sparse.block_vector.rst | 0 .../pynumero/pynumero.sparse.rst | 0 .../tutorial.block_vectors_and_matrices.rst | 0 .../pynumero/tutorial.linear_solver_interfaces.rst | 0 .../pynumero/tutorial.mpi_blocks.rst | 0 .../pynumero/tutorial.nlp_interfaces.rst | 0 .../contributed_packages/pynumero/tutorial.rst | 0 .../contributed_packages/pyros.rst | 0 .../contributed_packages/satsolver.rst | 0 .../contributed_packages/sensitivity_toolbox.rst | 0 .../contributed_packages/trustregion.rst | 0 doc/{OnlineDocs => Archive}/contribution_guide.rst | 0 .../developer_reference/config.rst | 0 .../developer_reference/deprecation.rst | 0 .../developer_reference/expressions/design.rst | 0 .../developer_reference/expressions/index.rst | 0 .../developer_reference/expressions/managing.rst | 0 .../developer_reference/expressions/overview.rst | 0 .../developer_reference/expressions/performance.rst | 0 .../developer_reference/future.rst | 0 .../developer_reference/index.rst | 0 .../developer_reference/solvers.rst | 0 doc/{OnlineDocs => Archive}/docutils.conf | 0 doc/{OnlineDocs => Archive}/errors.rst | 0 doc/{OnlineDocs => Archive}/index.rst | 0 doc/{OnlineDocs => Archive}/installation.rst | 0 .../library_reference/aml/index.rst | 0 .../library_reference/appsi/appsi.base.rst | 0 .../library_reference/appsi/appsi.rst | 0 .../library_reference/appsi/appsi.solvers.cbc.rst | 0 .../library_reference/appsi/appsi.solvers.cplex.rst | 0 .../appsi/appsi.solvers.gurobi.rst | 0 .../library_reference/appsi/appsi.solvers.highs.rst | 0 .../library_reference/appsi/appsi.solvers.ipopt.rst | 0 .../appsi/appsi.solvers.maingo.rst | 0 .../library_reference/appsi/appsi.solvers.rst | 0 .../library_reference/common/config.rst | 0 .../library_reference/common/dependencies.rst | 0 .../library_reference/common/deprecation.rst | 0 .../library_reference/common/enums.rst | 0 .../library_reference/common/errors.rst | 0 .../library_reference/common/fileutils.rst | 0 .../library_reference/common/formatting.rst | 0 .../library_reference/common/index.rst | 0 .../library_reference/common/tempfiles.rst | 0 .../library_reference/common/timing.rst | 0 .../library_reference/data/index.rst | 0 .../library_reference/expressions/building.rst | 0 .../library_reference/expressions/classes.rst | 0 .../expressions/context_managers.rst | 0 .../library_reference/expressions/index.rst | 0 .../library_reference/expressions/managing.rst | 0 .../library_reference/expressions/visitors.rst | 0 .../library_reference/index.rst | 0 .../library_reference/kernel/base.rst | 0 .../library_reference/kernel/block.rst | 0 .../library_reference/kernel/conic.rst | 0 .../library_reference/kernel/constraint.rst | 0 .../library_reference/kernel/dict_container.rst | 0 .../kernel/examples/aml_example.py | 0 .../library_reference/kernel/examples/conic.py | 0 .../kernel/examples/kernel_containers.py | 0 .../kernel/examples/kernel_example.py | 0 .../kernel/examples/kernel_solving.py | 0 .../kernel/examples/kernel_subclassing.py | 0 .../kernel/examples/transformer.py | 0 .../library_reference/kernel/expression.rst | 0 .../kernel/heterogeneous_container.rst | 0 .../kernel/homogeneous_container.rst | 0 .../library_reference/kernel/index.rst | 0 .../library_reference/kernel/list_container.rst | 0 .../library_reference/kernel/objective.rst | 0 .../library_reference/kernel/parameter.rst | 0 .../library_reference/kernel/piecewise/index.rst | 0 .../kernel/piecewise/piecewise.rst | 0 .../kernel/piecewise/piecewise_nd.rst | 0 .../library_reference/kernel/piecewise/util.rst | 0 .../library_reference/kernel/sos.rst | 0 .../library_reference/kernel/suffix.rst | 0 .../library_reference/kernel/syntax_comparison.rst | 0 .../library_reference/kernel/tuple_container.rst | 0 .../library_reference/kernel/variable.rst | 0 .../library_reference/solvers/cplex_persistent.rst | 0 .../library_reference/solvers/gams.rst | 0 .../library_reference/solvers/gurobi_direct.rst | 0 .../library_reference/solvers/gurobi_persistent.rst | 0 .../library_reference/solvers/index.rst | 0 .../library_reference/solvers/xpress_persistent.rst | 0 doc/{OnlineDocs => Archive}/make.bat | 0 doc/{OnlineDocs => Archive}/model_debugging/FAQ.rst | 0 .../model_debugging/getting_help.rst | 0 .../model_debugging/index.rst | 0 .../model_debugging/model_interrogation.rst | 0 .../model_transformations/index.rst | 0 .../model_transformations/scaling.rst | 0 .../modeling_extensions/__init__.py | 0 .../modeling_extensions/bilevel.rst | 0 .../modeling_extensions/dae.rst | 0 .../modeling_extensions/gdp/concepts.rst | 0 .../modeling_extensions/gdp/index.rst | 0 .../modeling_extensions/gdp/modeling.rst | 0 .../modeling_extensions/gdp/solving.rst | 0 .../modeling_extensions/index.rst | 0 .../modeling_extensions/mpec.rst | 0 .../modeling_extensions/network.rst | 0 .../modeling_extensions/reduce_points_demo.png | Bin .../modeling_extensions/stochastic_programming.rst | 0 .../pyomo_modeling_components/Constraints.rst | 0 .../pyomo_modeling_components/Expressions.rst | 0 .../pyomo_modeling_components/Objectives.rst | 0 .../pyomo_modeling_components/Parameters.rst | 0 .../pyomo_modeling_components/Sets.rst | 0 .../pyomo_modeling_components/Suffixes.rst | 0 .../pyomo_modeling_components/Variables.rst | 0 .../pyomo_modeling_components/index.rst | 0 .../pyomo_overview/abstract_concrete.rst | 0 .../pyomo_overview/index.rst | 0 .../pyomo_overview/math_modeling.rst | 0 .../pyomo_overview/overview_components.rst | 0 .../pyomo_overview/simple_examples.rst | 0 doc/{OnlineDocs => Archive}/related_packages.rst | 0 .../solving_pyomo_models.rst | 0 doc/{OnlineDocs => Archive}/src/data/A.tab | 0 doc/{OnlineDocs => Archive}/src/data/ABCD.tab | 0 doc/{OnlineDocs => Archive}/src/data/ABCD.txt | 0 doc/{OnlineDocs => Archive}/src/data/ABCD.xls | Bin doc/{OnlineDocs => Archive}/src/data/ABCD1.dat | 0 doc/{OnlineDocs => Archive}/src/data/ABCD1.py | 0 doc/{OnlineDocs => Archive}/src/data/ABCD1.txt | 0 doc/{OnlineDocs => Archive}/src/data/ABCD2.dat | 0 doc/{OnlineDocs => Archive}/src/data/ABCD2.py | 0 doc/{OnlineDocs => Archive}/src/data/ABCD2.txt | 0 doc/{OnlineDocs => Archive}/src/data/ABCD3.dat | 0 doc/{OnlineDocs => Archive}/src/data/ABCD3.py | 0 doc/{OnlineDocs => Archive}/src/data/ABCD3.txt | 0 doc/{OnlineDocs => Archive}/src/data/ABCD4.dat | 0 doc/{OnlineDocs => Archive}/src/data/ABCD4.py | 0 doc/{OnlineDocs => Archive}/src/data/ABCD4.txt | 0 doc/{OnlineDocs => Archive}/src/data/ABCD5.dat | 0 doc/{OnlineDocs => Archive}/src/data/ABCD5.py | 0 doc/{OnlineDocs => Archive}/src/data/ABCD5.txt | 0 doc/{OnlineDocs => Archive}/src/data/ABCD6.dat | 0 doc/{OnlineDocs => Archive}/src/data/ABCD6.py | 0 doc/{OnlineDocs => Archive}/src/data/ABCD6.txt | 0 doc/{OnlineDocs => Archive}/src/data/ABCD7.dat | 0 doc/{OnlineDocs => Archive}/src/data/ABCD7.py | 0 doc/{OnlineDocs => Archive}/src/data/ABCD7.txt | 0 doc/{OnlineDocs => Archive}/src/data/ABCD8.bad | 0 doc/{OnlineDocs => Archive}/src/data/ABCD8.dat | 0 doc/{OnlineDocs => Archive}/src/data/ABCD8.py | 0 doc/{OnlineDocs => Archive}/src/data/ABCD9.bad | 0 doc/{OnlineDocs => Archive}/src/data/ABCD9.dat | 0 doc/{OnlineDocs => Archive}/src/data/ABCD9.py | 0 doc/{OnlineDocs => Archive}/src/data/C.tab | 0 doc/{OnlineDocs => Archive}/src/data/D.tab | 0 doc/{OnlineDocs => Archive}/src/data/U.tab | 0 doc/{OnlineDocs => Archive}/src/data/Y.tab | 0 doc/{OnlineDocs => Archive}/src/data/Z.tab | 0 .../src/data/data_managers.txt | 0 doc/{OnlineDocs => Archive}/src/data/diet.dat | 0 doc/{OnlineDocs => Archive}/src/data/diet.sql | 0 doc/{OnlineDocs => Archive}/src/data/diet.sqlite | Bin .../src/data/diet.sqlite.dat | 0 doc/{OnlineDocs => Archive}/src/data/diet1.py | 0 doc/{OnlineDocs => Archive}/src/data/ex.dat | 0 doc/{OnlineDocs => Archive}/src/data/ex.py | 0 doc/{OnlineDocs => Archive}/src/data/ex.txt | 0 doc/{OnlineDocs => Archive}/src/data/ex1.dat | 0 doc/{OnlineDocs => Archive}/src/data/ex2.dat | 0 .../src/data/import1.tab.dat | 0 doc/{OnlineDocs => Archive}/src/data/import1.tab.py | 0 .../src/data/import1.tab.txt | 0 .../src/data/import2.tab.dat | 0 doc/{OnlineDocs => Archive}/src/data/import2.tab.py | 0 .../src/data/import2.tab.txt | 0 .../src/data/import3.tab.dat | 0 doc/{OnlineDocs => Archive}/src/data/import3.tab.py | 0 .../src/data/import3.tab.txt | 0 .../src/data/import4.tab.dat | 0 doc/{OnlineDocs => Archive}/src/data/import4.tab.py | 0 .../src/data/import4.tab.txt | 0 .../src/data/import5.tab.dat | 0 doc/{OnlineDocs => Archive}/src/data/import5.tab.py | 0 .../src/data/import5.tab.txt | 0 .../src/data/import6.tab.dat | 0 doc/{OnlineDocs => Archive}/src/data/import6.tab.py | 0 .../src/data/import6.tab.txt | 0 .../src/data/import7.tab.dat | 0 doc/{OnlineDocs => Archive}/src/data/import7.tab.py | 0 .../src/data/import7.tab.txt | 0 .../src/data/import8.tab.dat | 0 doc/{OnlineDocs => Archive}/src/data/import8.tab.py | 0 .../src/data/import8.tab.txt | 0 doc/{OnlineDocs => Archive}/src/data/namespace1.dat | 0 doc/{OnlineDocs => Archive}/src/data/param1.dat | 0 doc/{OnlineDocs => Archive}/src/data/param1.py | 0 doc/{OnlineDocs => Archive}/src/data/param1.txt | 0 doc/{OnlineDocs => Archive}/src/data/param2.dat | 0 doc/{OnlineDocs => Archive}/src/data/param2.py | 0 doc/{OnlineDocs => Archive}/src/data/param2.txt | 0 doc/{OnlineDocs => Archive}/src/data/param2a.dat | 0 doc/{OnlineDocs => Archive}/src/data/param2a.py | 0 doc/{OnlineDocs => Archive}/src/data/param2a.txt | 0 doc/{OnlineDocs => Archive}/src/data/param3.dat | 0 doc/{OnlineDocs => Archive}/src/data/param3.py | 0 doc/{OnlineDocs => Archive}/src/data/param3.txt | 0 doc/{OnlineDocs => Archive}/src/data/param3a.dat | 0 doc/{OnlineDocs => Archive}/src/data/param3a.py | 0 doc/{OnlineDocs => Archive}/src/data/param3a.txt | 0 doc/{OnlineDocs => Archive}/src/data/param3b.dat | 0 doc/{OnlineDocs => Archive}/src/data/param3b.py | 0 doc/{OnlineDocs => Archive}/src/data/param3b.txt | 0 doc/{OnlineDocs => Archive}/src/data/param3c.dat | 0 doc/{OnlineDocs => Archive}/src/data/param3c.py | 0 doc/{OnlineDocs => Archive}/src/data/param3c.txt | 0 doc/{OnlineDocs => Archive}/src/data/param4.dat | 0 doc/{OnlineDocs => Archive}/src/data/param4.py | 0 doc/{OnlineDocs => Archive}/src/data/param4.txt | 0 doc/{OnlineDocs => Archive}/src/data/param5.dat | 0 doc/{OnlineDocs => Archive}/src/data/param5.py | 0 doc/{OnlineDocs => Archive}/src/data/param5.txt | 0 doc/{OnlineDocs => Archive}/src/data/param5a.dat | 0 doc/{OnlineDocs => Archive}/src/data/param5a.py | 0 doc/{OnlineDocs => Archive}/src/data/param5a.txt | 0 doc/{OnlineDocs => Archive}/src/data/param6.dat | 0 doc/{OnlineDocs => Archive}/src/data/param6.py | 0 doc/{OnlineDocs => Archive}/src/data/param6.txt | 0 doc/{OnlineDocs => Archive}/src/data/param6a.dat | 0 doc/{OnlineDocs => Archive}/src/data/param6a.py | 0 doc/{OnlineDocs => Archive}/src/data/param6a.txt | 0 doc/{OnlineDocs => Archive}/src/data/param7a.dat | 0 doc/{OnlineDocs => Archive}/src/data/param7a.py | 0 doc/{OnlineDocs => Archive}/src/data/param7a.txt | 0 doc/{OnlineDocs => Archive}/src/data/param7b.dat | 0 doc/{OnlineDocs => Archive}/src/data/param7b.py | 0 doc/{OnlineDocs => Archive}/src/data/param7b.txt | 0 doc/{OnlineDocs => Archive}/src/data/param8a.dat | 0 doc/{OnlineDocs => Archive}/src/data/param8a.py | 0 doc/{OnlineDocs => Archive}/src/data/param8a.txt | 0 doc/{OnlineDocs => Archive}/src/data/pyomo.diet1.sh | 0 .../src/data/pyomo.diet1.txt | 0 doc/{OnlineDocs => Archive}/src/data/pyomo.diet2.sh | 0 .../src/data/pyomo.diet2.txt | 0 doc/{OnlineDocs => Archive}/src/data/set1.dat | 0 doc/{OnlineDocs => Archive}/src/data/set1.py | 0 doc/{OnlineDocs => Archive}/src/data/set1.txt | 0 doc/{OnlineDocs => Archive}/src/data/set2.dat | 0 doc/{OnlineDocs => Archive}/src/data/set2.py | 0 doc/{OnlineDocs => Archive}/src/data/set2.txt | 0 doc/{OnlineDocs => Archive}/src/data/set2a.dat | 0 doc/{OnlineDocs => Archive}/src/data/set2a.py | 0 doc/{OnlineDocs => Archive}/src/data/set2a.txt | 0 doc/{OnlineDocs => Archive}/src/data/set3.dat | 0 doc/{OnlineDocs => Archive}/src/data/set3.py | 0 doc/{OnlineDocs => Archive}/src/data/set3.txt | 0 doc/{OnlineDocs => Archive}/src/data/set4.dat | 0 doc/{OnlineDocs => Archive}/src/data/set4.py | 0 doc/{OnlineDocs => Archive}/src/data/set4.txt | 0 doc/{OnlineDocs => Archive}/src/data/set5.dat | 0 doc/{OnlineDocs => Archive}/src/data/set5.py | 0 doc/{OnlineDocs => Archive}/src/data/set5.txt | 0 doc/{OnlineDocs => Archive}/src/data/table0.dat | 0 doc/{OnlineDocs => Archive}/src/data/table0.py | 0 doc/{OnlineDocs => Archive}/src/data/table0.txt | 0 doc/{OnlineDocs => Archive}/src/data/table0.ul.dat | 0 doc/{OnlineDocs => Archive}/src/data/table0.ul.py | 0 doc/{OnlineDocs => Archive}/src/data/table0.ul.txt | 0 doc/{OnlineDocs => Archive}/src/data/table1.dat | 0 doc/{OnlineDocs => Archive}/src/data/table1.py | 0 doc/{OnlineDocs => Archive}/src/data/table1.txt | 0 doc/{OnlineDocs => Archive}/src/data/table2.dat | 0 doc/{OnlineDocs => Archive}/src/data/table2.py | 0 doc/{OnlineDocs => Archive}/src/data/table2.txt | 0 doc/{OnlineDocs => Archive}/src/data/table3.dat | 0 doc/{OnlineDocs => Archive}/src/data/table3.py | 0 doc/{OnlineDocs => Archive}/src/data/table3.txt | 0 doc/{OnlineDocs => Archive}/src/data/table3.ul.dat | 0 doc/{OnlineDocs => Archive}/src/data/table3.ul.py | 0 doc/{OnlineDocs => Archive}/src/data/table3.ul.txt | 0 doc/{OnlineDocs => Archive}/src/data/table4.dat | 0 doc/{OnlineDocs => Archive}/src/data/table4.py | 0 doc/{OnlineDocs => Archive}/src/data/table4.txt | 0 doc/{OnlineDocs => Archive}/src/data/table4.ul.dat | 0 doc/{OnlineDocs => Archive}/src/data/table4.ul.py | 0 doc/{OnlineDocs => Archive}/src/data/table4.ul.txt | 0 doc/{OnlineDocs => Archive}/src/data/table5.dat | 0 doc/{OnlineDocs => Archive}/src/data/table5.py | 0 doc/{OnlineDocs => Archive}/src/data/table5.txt | 0 doc/{OnlineDocs => Archive}/src/data/table6.dat | 0 doc/{OnlineDocs => Archive}/src/data/table6.py | 0 doc/{OnlineDocs => Archive}/src/data/table6.txt | 0 doc/{OnlineDocs => Archive}/src/data/table7.dat | 0 doc/{OnlineDocs => Archive}/src/data/table7.py | 0 doc/{OnlineDocs => Archive}/src/data/table7.txt | 0 doc/{OnlineDocs => Archive}/src/dataportal/A.tab | 0 doc/{OnlineDocs => Archive}/src/dataportal/C.tab | 0 doc/{OnlineDocs => Archive}/src/dataportal/D.tab | 0 doc/{OnlineDocs => Archive}/src/dataportal/PP.csv | 0 doc/{OnlineDocs => Archive}/src/dataportal/PP.json | 0 .../src/dataportal/PP.sqlite | Bin doc/{OnlineDocs => Archive}/src/dataportal/PP.tab | 0 doc/{OnlineDocs => Archive}/src/dataportal/PP.xml | 0 doc/{OnlineDocs => Archive}/src/dataportal/PP.yaml | 0 .../src/dataportal/PP_sqlite.py | 0 .../src/dataportal/Pyomo_mysql | 0 doc/{OnlineDocs => Archive}/src/dataportal/S.tab | 0 doc/{OnlineDocs => Archive}/src/dataportal/T.json | 0 doc/{OnlineDocs => Archive}/src/dataportal/T.yaml | 0 doc/{OnlineDocs => Archive}/src/dataportal/U.tab | 0 doc/{OnlineDocs => Archive}/src/dataportal/XW.tab | 0 doc/{OnlineDocs => Archive}/src/dataportal/Y.tab | 0 doc/{OnlineDocs => Archive}/src/dataportal/Z.tab | 0 .../src/dataportal/dataportal_tab.py | 0 .../src/dataportal/dataportal_tab.txt | 0 .../src/dataportal/excel.xls | Bin .../src/dataportal/param_initialization.py | 0 .../src/dataportal/param_initialization.txt | 0 .../src/dataportal/set_initialization.py | 0 .../src/dataportal/set_initialization.txt | 0 doc/{OnlineDocs => Archive}/src/expr/design.py | 0 doc/{OnlineDocs => Archive}/src/expr/design.txt | 0 doc/{OnlineDocs => Archive}/src/expr/index.py | 0 doc/{OnlineDocs => Archive}/src/expr/index.txt | 0 doc/{OnlineDocs => Archive}/src/expr/managing.py | 0 doc/{OnlineDocs => Archive}/src/expr/managing.txt | 0 doc/{OnlineDocs => Archive}/src/expr/overview.py | 0 doc/{OnlineDocs => Archive}/src/expr/overview.txt | 0 doc/{OnlineDocs => Archive}/src/expr/performance.py | 0 .../src/expr/performance.txt | 0 doc/{OnlineDocs => Archive}/src/expr/quicksum.log | 0 doc/{OnlineDocs => Archive}/src/expr/quicksum.py | 0 doc/{OnlineDocs => Archive}/src/kernel/examples.sh | 0 doc/{OnlineDocs => Archive}/src/kernel/examples.txt | 0 .../src/scripting/AbstractSuffixes.py | 0 .../src/scripting/Isinglebuild.py | 0 .../src/scripting/Isinglecomm.dat | 0 .../src/scripting/NodesIn_init.py | 0 doc/{OnlineDocs => Archive}/src/scripting/Z_init.py | 0 .../src/scripting/abstract1.dat | 0 .../src/scripting/abstract2.dat | 0 .../src/scripting/abstract2.py | 0 .../src/scripting/abstract2a.dat | 0 .../src/scripting/abstract2piece.py | 0 .../src/scripting/abstract2piecebuild.py | 0 .../src/scripting/block_iter_example.py | 0 .../src/scripting/concrete1.py | 0 .../src/scripting/doubleA.py | 0 .../src/scripting/driveabs2.py | 0 .../src/scripting/driveconc1.py | 0 .../src/scripting/iterative1.py | 0 .../src/scripting/iterative2.py | 0 .../src/scripting/noiteration1.py | 0 .../src/scripting/parallel.py | 0 .../src/scripting/spy4Constraints.py | 0 .../src/scripting/spy4Expressions.py | 0 .../src/scripting/spy4PyomoCommand.py | 0 .../src/scripting/spy4Variables.py | 0 .../src/scripting/spy4scripts.py | 0 doc/{OnlineDocs => Archive}/src/strip_examples.py | 0 doc/{OnlineDocs => Archive}/src/test_examples.py | 0 doc/{OnlineDocs => Archive}/tutorial_examples.rst | 0 .../working_abstractmodels/BuildAction.rst | 0 .../working_abstractmodels/data/ABCD.pdf | Bin .../working_abstractmodels/data/ABCD.png | Bin .../working_abstractmodels/data/PP.png | Bin .../working_abstractmodels/data/dataportals.rst | 0 .../working_abstractmodels/data/datfiles.rst | 0 .../working_abstractmodels/data/index.rst | 0 .../working_abstractmodels/data/native.rst | 0 .../working_abstractmodels/data/raw_dicts.rst | 0 .../working_abstractmodels/data/storing_data.rst | 0 .../working_abstractmodels/index.rst | 0 .../working_abstractmodels/instantiating_models.rst | 0 .../working_abstractmodels/pyomo_command.rst | 0 doc/{OnlineDocs => Archive}/working_models.rst | 0 464 files changed, 0 insertions(+), 0 deletions(-) rename doc/{OnlineDocs => Archive}/Makefile (100%) rename doc/{OnlineDocs => Archive}/README.md (100%) rename doc/{OnlineDocs => Archive}/_static/theme_overrides.css (100%) rename doc/{OnlineDocs => Archive}/advanced_topics/flattener/index.rst (100%) rename doc/{OnlineDocs => Archive}/advanced_topics/flattener/motivation.rst (100%) rename doc/{OnlineDocs => Archive}/advanced_topics/flattener/reference.rst (100%) rename doc/{OnlineDocs => Archive}/advanced_topics/index.rst (100%) rename doc/{OnlineDocs => Archive}/advanced_topics/linearexpression.rst (100%) rename doc/{OnlineDocs => Archive}/advanced_topics/persistent_solvers.rst (100%) rename doc/{OnlineDocs => Archive}/advanced_topics/sos_constraints.rst (100%) rename doc/{OnlineDocs => Archive}/advanced_topics/units_container.rst (100%) rename doc/{OnlineDocs => Archive}/bibliography.rst (100%) rename doc/{OnlineDocs => Archive}/citing_pyomo.rst (100%) rename doc/{OnlineDocs => Archive}/conf.py (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/communities_8pp.png (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/communities_decode_1.png (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/community.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/doe/CCSI-license.txt (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/doe/doe.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/doe/flowchart.png (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/doe/grid-1.png (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/doe/reactor.png (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/doe/uml.png (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/gdpopt.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/gdpopt_flowchart.png (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/iis.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/incidence/api.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/incidence/config.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/incidence/connected.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/incidence/dulmage_mendelsohn.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/incidence/incidence.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/incidence/index.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/incidence/interface.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/incidence/matching.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/incidence/overview.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/incidence/scc_solver.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/incidence/triangularize.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/incidence/tutorial.bt.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/incidence/tutorial.btsolve.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/incidence/tutorial.dm.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/incidence/tutorial.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/index.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/latex_printer.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/mcpp.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/mindtpy.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/mpc/api.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/mpc/conversion.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/mpc/data.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/mpc/examples.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/mpc/faq.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/mpc/index.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/mpc/interface.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/mpc/modeling.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/mpc/overview.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/multistart.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/parmest/api.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/parmest/boxplot.png (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/parmest/covariance.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/parmest/datarec.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/parmest/driver.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/parmest/examples.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/parmest/graphics.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/parmest/index.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/parmest/installation.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/parmest/overview.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/parmest/pairwise_plot_CI.png (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/parmest/pairwise_plot_LR.png (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/parmest/parallel.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/parmest/scencreate.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/preprocessing.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/api.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/backward_compatibility.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/index.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/installation.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.interfaces.nlp.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.interfaces.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.linalg.base.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.linalg.ma27.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.linalg.ma57.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.linalg.mumps.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.linalg.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.linalg.scipy.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.sparse.block_vector.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/pynumero.sparse.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/tutorial.mpi_blocks.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/tutorial.nlp_interfaces.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pynumero/tutorial.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/pyros.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/satsolver.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/sensitivity_toolbox.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/trustregion.rst (100%) rename doc/{OnlineDocs => Archive}/contribution_guide.rst (100%) rename doc/{OnlineDocs => Archive}/developer_reference/config.rst (100%) rename doc/{OnlineDocs => Archive}/developer_reference/deprecation.rst (100%) rename doc/{OnlineDocs => Archive}/developer_reference/expressions/design.rst (100%) rename doc/{OnlineDocs => Archive}/developer_reference/expressions/index.rst (100%) rename doc/{OnlineDocs => Archive}/developer_reference/expressions/managing.rst (100%) rename doc/{OnlineDocs => Archive}/developer_reference/expressions/overview.rst (100%) rename doc/{OnlineDocs => Archive}/developer_reference/expressions/performance.rst (100%) rename doc/{OnlineDocs => Archive}/developer_reference/future.rst (100%) rename doc/{OnlineDocs => Archive}/developer_reference/index.rst (100%) rename doc/{OnlineDocs => Archive}/developer_reference/solvers.rst (100%) rename doc/{OnlineDocs => Archive}/docutils.conf (100%) rename doc/{OnlineDocs => Archive}/errors.rst (100%) rename doc/{OnlineDocs => Archive}/index.rst (100%) rename doc/{OnlineDocs => Archive}/installation.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/aml/index.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/appsi/appsi.base.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/appsi/appsi.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/appsi/appsi.solvers.cbc.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/appsi/appsi.solvers.cplex.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/appsi/appsi.solvers.gurobi.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/appsi/appsi.solvers.highs.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/appsi/appsi.solvers.ipopt.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/appsi/appsi.solvers.maingo.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/appsi/appsi.solvers.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/common/config.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/common/dependencies.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/common/deprecation.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/common/enums.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/common/errors.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/common/fileutils.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/common/formatting.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/common/index.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/common/tempfiles.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/common/timing.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/data/index.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/expressions/building.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/expressions/classes.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/expressions/context_managers.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/expressions/index.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/expressions/managing.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/expressions/visitors.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/index.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/base.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/block.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/conic.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/constraint.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/dict_container.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/examples/aml_example.py (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/examples/conic.py (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/examples/kernel_containers.py (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/examples/kernel_example.py (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/examples/kernel_solving.py (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/examples/kernel_subclassing.py (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/examples/transformer.py (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/expression.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/heterogeneous_container.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/homogeneous_container.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/index.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/list_container.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/objective.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/parameter.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/piecewise/index.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/piecewise/piecewise.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/piecewise/piecewise_nd.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/piecewise/util.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/sos.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/suffix.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/syntax_comparison.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/tuple_container.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/kernel/variable.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/solvers/cplex_persistent.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/solvers/gams.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/solvers/gurobi_direct.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/solvers/gurobi_persistent.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/solvers/index.rst (100%) rename doc/{OnlineDocs => Archive}/library_reference/solvers/xpress_persistent.rst (100%) rename doc/{OnlineDocs => Archive}/make.bat (100%) rename doc/{OnlineDocs => Archive}/model_debugging/FAQ.rst (100%) rename doc/{OnlineDocs => Archive}/model_debugging/getting_help.rst (100%) rename doc/{OnlineDocs => Archive}/model_debugging/index.rst (100%) rename doc/{OnlineDocs => Archive}/model_debugging/model_interrogation.rst (100%) rename doc/{OnlineDocs => Archive}/model_transformations/index.rst (100%) rename doc/{OnlineDocs => Archive}/model_transformations/scaling.rst (100%) rename doc/{OnlineDocs => Archive}/modeling_extensions/__init__.py (100%) rename doc/{OnlineDocs => Archive}/modeling_extensions/bilevel.rst (100%) rename doc/{OnlineDocs => Archive}/modeling_extensions/dae.rst (100%) rename doc/{OnlineDocs => Archive}/modeling_extensions/gdp/concepts.rst (100%) rename doc/{OnlineDocs => Archive}/modeling_extensions/gdp/index.rst (100%) rename doc/{OnlineDocs => Archive}/modeling_extensions/gdp/modeling.rst (100%) rename doc/{OnlineDocs => Archive}/modeling_extensions/gdp/solving.rst (100%) rename doc/{OnlineDocs => Archive}/modeling_extensions/index.rst (100%) rename doc/{OnlineDocs => Archive}/modeling_extensions/mpec.rst (100%) rename doc/{OnlineDocs => Archive}/modeling_extensions/network.rst (100%) rename doc/{OnlineDocs => Archive}/modeling_extensions/reduce_points_demo.png (100%) rename doc/{OnlineDocs => Archive}/modeling_extensions/stochastic_programming.rst (100%) rename doc/{OnlineDocs => Archive}/pyomo_modeling_components/Constraints.rst (100%) rename doc/{OnlineDocs => Archive}/pyomo_modeling_components/Expressions.rst (100%) rename doc/{OnlineDocs => Archive}/pyomo_modeling_components/Objectives.rst (100%) rename doc/{OnlineDocs => Archive}/pyomo_modeling_components/Parameters.rst (100%) rename doc/{OnlineDocs => Archive}/pyomo_modeling_components/Sets.rst (100%) rename doc/{OnlineDocs => Archive}/pyomo_modeling_components/Suffixes.rst (100%) rename doc/{OnlineDocs => Archive}/pyomo_modeling_components/Variables.rst (100%) rename doc/{OnlineDocs => Archive}/pyomo_modeling_components/index.rst (100%) rename doc/{OnlineDocs => Archive}/pyomo_overview/abstract_concrete.rst (100%) rename doc/{OnlineDocs => Archive}/pyomo_overview/index.rst (100%) rename doc/{OnlineDocs => Archive}/pyomo_overview/math_modeling.rst (100%) rename doc/{OnlineDocs => Archive}/pyomo_overview/overview_components.rst (100%) rename doc/{OnlineDocs => Archive}/pyomo_overview/simple_examples.rst (100%) rename doc/{OnlineDocs => Archive}/related_packages.rst (100%) rename doc/{OnlineDocs => Archive}/solving_pyomo_models.rst (100%) rename doc/{OnlineDocs => Archive}/src/data/A.tab (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD.tab (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD.xls (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD1.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD1.py (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD1.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD2.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD2.py (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD2.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD3.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD3.py (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD3.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD4.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD4.py (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD4.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD5.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD5.py (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD5.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD6.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD6.py (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD6.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD7.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD7.py (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD7.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD8.bad (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD8.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD8.py (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD9.bad (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD9.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/ABCD9.py (100%) rename doc/{OnlineDocs => Archive}/src/data/C.tab (100%) rename doc/{OnlineDocs => Archive}/src/data/D.tab (100%) rename doc/{OnlineDocs => Archive}/src/data/U.tab (100%) rename doc/{OnlineDocs => Archive}/src/data/Y.tab (100%) rename doc/{OnlineDocs => Archive}/src/data/Z.tab (100%) rename doc/{OnlineDocs => Archive}/src/data/data_managers.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/diet.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/diet.sql (100%) rename doc/{OnlineDocs => Archive}/src/data/diet.sqlite (100%) rename doc/{OnlineDocs => Archive}/src/data/diet.sqlite.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/diet1.py (100%) rename doc/{OnlineDocs => Archive}/src/data/ex.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/ex.py (100%) rename doc/{OnlineDocs => Archive}/src/data/ex.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/ex1.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/ex2.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/import1.tab.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/import1.tab.py (100%) rename doc/{OnlineDocs => Archive}/src/data/import1.tab.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/import2.tab.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/import2.tab.py (100%) rename doc/{OnlineDocs => Archive}/src/data/import2.tab.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/import3.tab.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/import3.tab.py (100%) rename doc/{OnlineDocs => Archive}/src/data/import3.tab.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/import4.tab.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/import4.tab.py (100%) rename doc/{OnlineDocs => Archive}/src/data/import4.tab.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/import5.tab.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/import5.tab.py (100%) rename doc/{OnlineDocs => Archive}/src/data/import5.tab.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/import6.tab.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/import6.tab.py (100%) rename doc/{OnlineDocs => Archive}/src/data/import6.tab.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/import7.tab.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/import7.tab.py (100%) rename doc/{OnlineDocs => Archive}/src/data/import7.tab.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/import8.tab.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/import8.tab.py (100%) rename doc/{OnlineDocs => Archive}/src/data/import8.tab.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/namespace1.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param1.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param1.py (100%) rename doc/{OnlineDocs => Archive}/src/data/param1.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/param2.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param2.py (100%) rename doc/{OnlineDocs => Archive}/src/data/param2.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/param2a.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param2a.py (100%) rename doc/{OnlineDocs => Archive}/src/data/param2a.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/param3.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param3.py (100%) rename doc/{OnlineDocs => Archive}/src/data/param3.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/param3a.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param3a.py (100%) rename doc/{OnlineDocs => Archive}/src/data/param3a.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/param3b.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param3b.py (100%) rename doc/{OnlineDocs => Archive}/src/data/param3b.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/param3c.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param3c.py (100%) rename doc/{OnlineDocs => Archive}/src/data/param3c.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/param4.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param4.py (100%) rename doc/{OnlineDocs => Archive}/src/data/param4.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/param5.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param5.py (100%) rename doc/{OnlineDocs => Archive}/src/data/param5.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/param5a.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param5a.py (100%) rename doc/{OnlineDocs => Archive}/src/data/param5a.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/param6.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param6.py (100%) rename doc/{OnlineDocs => Archive}/src/data/param6.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/param6a.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param6a.py (100%) rename doc/{OnlineDocs => Archive}/src/data/param6a.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/param7a.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param7a.py (100%) rename doc/{OnlineDocs => Archive}/src/data/param7a.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/param7b.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param7b.py (100%) rename doc/{OnlineDocs => Archive}/src/data/param7b.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/param8a.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/param8a.py (100%) rename doc/{OnlineDocs => Archive}/src/data/param8a.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/pyomo.diet1.sh (100%) rename doc/{OnlineDocs => Archive}/src/data/pyomo.diet1.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/pyomo.diet2.sh (100%) rename doc/{OnlineDocs => Archive}/src/data/pyomo.diet2.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/set1.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/set1.py (100%) rename doc/{OnlineDocs => Archive}/src/data/set1.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/set2.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/set2.py (100%) rename doc/{OnlineDocs => Archive}/src/data/set2.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/set2a.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/set2a.py (100%) rename doc/{OnlineDocs => Archive}/src/data/set2a.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/set3.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/set3.py (100%) rename doc/{OnlineDocs => Archive}/src/data/set3.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/set4.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/set4.py (100%) rename doc/{OnlineDocs => Archive}/src/data/set4.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/set5.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/set5.py (100%) rename doc/{OnlineDocs => Archive}/src/data/set5.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/table0.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/table0.py (100%) rename doc/{OnlineDocs => Archive}/src/data/table0.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/table0.ul.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/table0.ul.py (100%) rename doc/{OnlineDocs => Archive}/src/data/table0.ul.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/table1.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/table1.py (100%) rename doc/{OnlineDocs => Archive}/src/data/table1.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/table2.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/table2.py (100%) rename doc/{OnlineDocs => Archive}/src/data/table2.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/table3.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/table3.py (100%) rename doc/{OnlineDocs => Archive}/src/data/table3.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/table3.ul.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/table3.ul.py (100%) rename doc/{OnlineDocs => Archive}/src/data/table3.ul.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/table4.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/table4.py (100%) rename doc/{OnlineDocs => Archive}/src/data/table4.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/table4.ul.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/table4.ul.py (100%) rename doc/{OnlineDocs => Archive}/src/data/table4.ul.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/table5.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/table5.py (100%) rename doc/{OnlineDocs => Archive}/src/data/table5.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/table6.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/table6.py (100%) rename doc/{OnlineDocs => Archive}/src/data/table6.txt (100%) rename doc/{OnlineDocs => Archive}/src/data/table7.dat (100%) rename doc/{OnlineDocs => Archive}/src/data/table7.py (100%) rename doc/{OnlineDocs => Archive}/src/data/table7.txt (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/A.tab (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/C.tab (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/D.tab (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/PP.csv (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/PP.json (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/PP.sqlite (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/PP.tab (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/PP.xml (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/PP.yaml (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/PP_sqlite.py (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/Pyomo_mysql (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/S.tab (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/T.json (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/T.yaml (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/U.tab (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/XW.tab (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/Y.tab (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/Z.tab (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/dataportal_tab.py (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/dataportal_tab.txt (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/excel.xls (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/param_initialization.py (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/param_initialization.txt (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/set_initialization.py (100%) rename doc/{OnlineDocs => Archive}/src/dataportal/set_initialization.txt (100%) rename doc/{OnlineDocs => Archive}/src/expr/design.py (100%) rename doc/{OnlineDocs => Archive}/src/expr/design.txt (100%) rename doc/{OnlineDocs => Archive}/src/expr/index.py (100%) rename doc/{OnlineDocs => Archive}/src/expr/index.txt (100%) rename doc/{OnlineDocs => Archive}/src/expr/managing.py (100%) rename doc/{OnlineDocs => Archive}/src/expr/managing.txt (100%) rename doc/{OnlineDocs => Archive}/src/expr/overview.py (100%) rename doc/{OnlineDocs => Archive}/src/expr/overview.txt (100%) rename doc/{OnlineDocs => Archive}/src/expr/performance.py (100%) rename doc/{OnlineDocs => Archive}/src/expr/performance.txt (100%) rename doc/{OnlineDocs => Archive}/src/expr/quicksum.log (100%) rename doc/{OnlineDocs => Archive}/src/expr/quicksum.py (100%) rename doc/{OnlineDocs => Archive}/src/kernel/examples.sh (100%) rename doc/{OnlineDocs => Archive}/src/kernel/examples.txt (100%) rename doc/{OnlineDocs => Archive}/src/scripting/AbstractSuffixes.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/Isinglebuild.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/Isinglecomm.dat (100%) rename doc/{OnlineDocs => Archive}/src/scripting/NodesIn_init.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/Z_init.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/abstract1.dat (100%) rename doc/{OnlineDocs => Archive}/src/scripting/abstract2.dat (100%) rename doc/{OnlineDocs => Archive}/src/scripting/abstract2.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/abstract2a.dat (100%) rename doc/{OnlineDocs => Archive}/src/scripting/abstract2piece.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/abstract2piecebuild.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/block_iter_example.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/concrete1.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/doubleA.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/driveabs2.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/driveconc1.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/iterative1.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/iterative2.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/noiteration1.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/parallel.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/spy4Constraints.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/spy4Expressions.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/spy4PyomoCommand.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/spy4Variables.py (100%) rename doc/{OnlineDocs => Archive}/src/scripting/spy4scripts.py (100%) rename doc/{OnlineDocs => Archive}/src/strip_examples.py (100%) rename doc/{OnlineDocs => Archive}/src/test_examples.py (100%) rename doc/{OnlineDocs => Archive}/tutorial_examples.rst (100%) rename doc/{OnlineDocs => Archive}/working_abstractmodels/BuildAction.rst (100%) rename doc/{OnlineDocs => Archive}/working_abstractmodels/data/ABCD.pdf (100%) rename doc/{OnlineDocs => Archive}/working_abstractmodels/data/ABCD.png (100%) rename doc/{OnlineDocs => Archive}/working_abstractmodels/data/PP.png (100%) rename doc/{OnlineDocs => Archive}/working_abstractmodels/data/dataportals.rst (100%) rename doc/{OnlineDocs => Archive}/working_abstractmodels/data/datfiles.rst (100%) rename doc/{OnlineDocs => Archive}/working_abstractmodels/data/index.rst (100%) rename doc/{OnlineDocs => Archive}/working_abstractmodels/data/native.rst (100%) rename doc/{OnlineDocs => Archive}/working_abstractmodels/data/raw_dicts.rst (100%) rename doc/{OnlineDocs => Archive}/working_abstractmodels/data/storing_data.rst (100%) rename doc/{OnlineDocs => Archive}/working_abstractmodels/index.rst (100%) rename doc/{OnlineDocs => Archive}/working_abstractmodels/instantiating_models.rst (100%) rename doc/{OnlineDocs => Archive}/working_abstractmodels/pyomo_command.rst (100%) rename doc/{OnlineDocs => Archive}/working_models.rst (100%) diff --git a/doc/OnlineDocs/Makefile b/doc/Archive/Makefile similarity index 100% rename from doc/OnlineDocs/Makefile rename to doc/Archive/Makefile diff --git a/doc/OnlineDocs/README.md b/doc/Archive/README.md similarity index 100% rename from doc/OnlineDocs/README.md rename to doc/Archive/README.md diff --git a/doc/OnlineDocs/_static/theme_overrides.css b/doc/Archive/_static/theme_overrides.css similarity index 100% rename from doc/OnlineDocs/_static/theme_overrides.css rename to doc/Archive/_static/theme_overrides.css diff --git a/doc/OnlineDocs/advanced_topics/flattener/index.rst b/doc/Archive/advanced_topics/flattener/index.rst similarity index 100% rename from doc/OnlineDocs/advanced_topics/flattener/index.rst rename to doc/Archive/advanced_topics/flattener/index.rst diff --git a/doc/OnlineDocs/advanced_topics/flattener/motivation.rst b/doc/Archive/advanced_topics/flattener/motivation.rst similarity index 100% rename from doc/OnlineDocs/advanced_topics/flattener/motivation.rst rename to doc/Archive/advanced_topics/flattener/motivation.rst diff --git a/doc/OnlineDocs/advanced_topics/flattener/reference.rst b/doc/Archive/advanced_topics/flattener/reference.rst similarity index 100% rename from doc/OnlineDocs/advanced_topics/flattener/reference.rst rename to doc/Archive/advanced_topics/flattener/reference.rst diff --git a/doc/OnlineDocs/advanced_topics/index.rst b/doc/Archive/advanced_topics/index.rst similarity index 100% rename from doc/OnlineDocs/advanced_topics/index.rst rename to doc/Archive/advanced_topics/index.rst diff --git a/doc/OnlineDocs/advanced_topics/linearexpression.rst b/doc/Archive/advanced_topics/linearexpression.rst similarity index 100% rename from doc/OnlineDocs/advanced_topics/linearexpression.rst rename to doc/Archive/advanced_topics/linearexpression.rst diff --git a/doc/OnlineDocs/advanced_topics/persistent_solvers.rst b/doc/Archive/advanced_topics/persistent_solvers.rst similarity index 100% rename from doc/OnlineDocs/advanced_topics/persistent_solvers.rst rename to doc/Archive/advanced_topics/persistent_solvers.rst diff --git a/doc/OnlineDocs/advanced_topics/sos_constraints.rst b/doc/Archive/advanced_topics/sos_constraints.rst similarity index 100% rename from doc/OnlineDocs/advanced_topics/sos_constraints.rst rename to doc/Archive/advanced_topics/sos_constraints.rst diff --git a/doc/OnlineDocs/advanced_topics/units_container.rst b/doc/Archive/advanced_topics/units_container.rst similarity index 100% rename from doc/OnlineDocs/advanced_topics/units_container.rst rename to doc/Archive/advanced_topics/units_container.rst diff --git a/doc/OnlineDocs/bibliography.rst b/doc/Archive/bibliography.rst similarity index 100% rename from doc/OnlineDocs/bibliography.rst rename to doc/Archive/bibliography.rst diff --git a/doc/OnlineDocs/citing_pyomo.rst b/doc/Archive/citing_pyomo.rst similarity index 100% rename from doc/OnlineDocs/citing_pyomo.rst rename to doc/Archive/citing_pyomo.rst diff --git a/doc/OnlineDocs/conf.py b/doc/Archive/conf.py similarity index 100% rename from doc/OnlineDocs/conf.py rename to doc/Archive/conf.py diff --git a/doc/OnlineDocs/contributed_packages/communities_8pp.png b/doc/Archive/contributed_packages/communities_8pp.png similarity index 100% rename from doc/OnlineDocs/contributed_packages/communities_8pp.png rename to doc/Archive/contributed_packages/communities_8pp.png diff --git a/doc/OnlineDocs/contributed_packages/communities_decode_1.png b/doc/Archive/contributed_packages/communities_decode_1.png similarity index 100% rename from doc/OnlineDocs/contributed_packages/communities_decode_1.png rename to doc/Archive/contributed_packages/communities_decode_1.png diff --git a/doc/OnlineDocs/contributed_packages/community.rst b/doc/Archive/contributed_packages/community.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/community.rst rename to doc/Archive/contributed_packages/community.rst diff --git a/doc/OnlineDocs/contributed_packages/doe/CCSI-license.txt b/doc/Archive/contributed_packages/doe/CCSI-license.txt similarity index 100% rename from doc/OnlineDocs/contributed_packages/doe/CCSI-license.txt rename to doc/Archive/contributed_packages/doe/CCSI-license.txt diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/Archive/contributed_packages/doe/doe.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/doe/doe.rst rename to doc/Archive/contributed_packages/doe/doe.rst diff --git a/doc/OnlineDocs/contributed_packages/doe/flowchart.png b/doc/Archive/contributed_packages/doe/flowchart.png similarity index 100% rename from doc/OnlineDocs/contributed_packages/doe/flowchart.png rename to doc/Archive/contributed_packages/doe/flowchart.png diff --git a/doc/OnlineDocs/contributed_packages/doe/grid-1.png b/doc/Archive/contributed_packages/doe/grid-1.png similarity index 100% rename from doc/OnlineDocs/contributed_packages/doe/grid-1.png rename to doc/Archive/contributed_packages/doe/grid-1.png diff --git a/doc/OnlineDocs/contributed_packages/doe/reactor.png b/doc/Archive/contributed_packages/doe/reactor.png similarity index 100% rename from doc/OnlineDocs/contributed_packages/doe/reactor.png rename to doc/Archive/contributed_packages/doe/reactor.png diff --git a/doc/OnlineDocs/contributed_packages/doe/uml.png b/doc/Archive/contributed_packages/doe/uml.png similarity index 100% rename from doc/OnlineDocs/contributed_packages/doe/uml.png rename to doc/Archive/contributed_packages/doe/uml.png diff --git a/doc/OnlineDocs/contributed_packages/gdpopt.rst b/doc/Archive/contributed_packages/gdpopt.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/gdpopt.rst rename to doc/Archive/contributed_packages/gdpopt.rst diff --git a/doc/OnlineDocs/contributed_packages/gdpopt_flowchart.png b/doc/Archive/contributed_packages/gdpopt_flowchart.png similarity index 100% rename from doc/OnlineDocs/contributed_packages/gdpopt_flowchart.png rename to doc/Archive/contributed_packages/gdpopt_flowchart.png diff --git a/doc/OnlineDocs/contributed_packages/iis.rst b/doc/Archive/contributed_packages/iis.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/iis.rst rename to doc/Archive/contributed_packages/iis.rst diff --git a/doc/OnlineDocs/contributed_packages/incidence/api.rst b/doc/Archive/contributed_packages/incidence/api.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/incidence/api.rst rename to doc/Archive/contributed_packages/incidence/api.rst diff --git a/doc/OnlineDocs/contributed_packages/incidence/config.rst b/doc/Archive/contributed_packages/incidence/config.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/incidence/config.rst rename to doc/Archive/contributed_packages/incidence/config.rst diff --git a/doc/OnlineDocs/contributed_packages/incidence/connected.rst b/doc/Archive/contributed_packages/incidence/connected.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/incidence/connected.rst rename to doc/Archive/contributed_packages/incidence/connected.rst diff --git a/doc/OnlineDocs/contributed_packages/incidence/dulmage_mendelsohn.rst b/doc/Archive/contributed_packages/incidence/dulmage_mendelsohn.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/incidence/dulmage_mendelsohn.rst rename to doc/Archive/contributed_packages/incidence/dulmage_mendelsohn.rst diff --git a/doc/OnlineDocs/contributed_packages/incidence/incidence.rst b/doc/Archive/contributed_packages/incidence/incidence.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/incidence/incidence.rst rename to doc/Archive/contributed_packages/incidence/incidence.rst diff --git a/doc/OnlineDocs/contributed_packages/incidence/index.rst b/doc/Archive/contributed_packages/incidence/index.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/incidence/index.rst rename to doc/Archive/contributed_packages/incidence/index.rst diff --git a/doc/OnlineDocs/contributed_packages/incidence/interface.rst b/doc/Archive/contributed_packages/incidence/interface.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/incidence/interface.rst rename to doc/Archive/contributed_packages/incidence/interface.rst diff --git a/doc/OnlineDocs/contributed_packages/incidence/matching.rst b/doc/Archive/contributed_packages/incidence/matching.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/incidence/matching.rst rename to doc/Archive/contributed_packages/incidence/matching.rst diff --git a/doc/OnlineDocs/contributed_packages/incidence/overview.rst b/doc/Archive/contributed_packages/incidence/overview.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/incidence/overview.rst rename to doc/Archive/contributed_packages/incidence/overview.rst diff --git a/doc/OnlineDocs/contributed_packages/incidence/scc_solver.rst b/doc/Archive/contributed_packages/incidence/scc_solver.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/incidence/scc_solver.rst rename to doc/Archive/contributed_packages/incidence/scc_solver.rst diff --git a/doc/OnlineDocs/contributed_packages/incidence/triangularize.rst b/doc/Archive/contributed_packages/incidence/triangularize.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/incidence/triangularize.rst rename to doc/Archive/contributed_packages/incidence/triangularize.rst diff --git a/doc/OnlineDocs/contributed_packages/incidence/tutorial.bt.rst b/doc/Archive/contributed_packages/incidence/tutorial.bt.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/incidence/tutorial.bt.rst rename to doc/Archive/contributed_packages/incidence/tutorial.bt.rst diff --git a/doc/OnlineDocs/contributed_packages/incidence/tutorial.btsolve.rst b/doc/Archive/contributed_packages/incidence/tutorial.btsolve.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/incidence/tutorial.btsolve.rst rename to doc/Archive/contributed_packages/incidence/tutorial.btsolve.rst diff --git a/doc/OnlineDocs/contributed_packages/incidence/tutorial.dm.rst b/doc/Archive/contributed_packages/incidence/tutorial.dm.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/incidence/tutorial.dm.rst rename to doc/Archive/contributed_packages/incidence/tutorial.dm.rst diff --git a/doc/OnlineDocs/contributed_packages/incidence/tutorial.rst b/doc/Archive/contributed_packages/incidence/tutorial.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/incidence/tutorial.rst rename to doc/Archive/contributed_packages/incidence/tutorial.rst diff --git a/doc/OnlineDocs/contributed_packages/index.rst b/doc/Archive/contributed_packages/index.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/index.rst rename to doc/Archive/contributed_packages/index.rst diff --git a/doc/OnlineDocs/contributed_packages/latex_printer.rst b/doc/Archive/contributed_packages/latex_printer.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/latex_printer.rst rename to doc/Archive/contributed_packages/latex_printer.rst diff --git a/doc/OnlineDocs/contributed_packages/mcpp.rst b/doc/Archive/contributed_packages/mcpp.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/mcpp.rst rename to doc/Archive/contributed_packages/mcpp.rst diff --git a/doc/OnlineDocs/contributed_packages/mindtpy.rst b/doc/Archive/contributed_packages/mindtpy.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/mindtpy.rst rename to doc/Archive/contributed_packages/mindtpy.rst diff --git a/doc/OnlineDocs/contributed_packages/mpc/api.rst b/doc/Archive/contributed_packages/mpc/api.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/mpc/api.rst rename to doc/Archive/contributed_packages/mpc/api.rst diff --git a/doc/OnlineDocs/contributed_packages/mpc/conversion.rst b/doc/Archive/contributed_packages/mpc/conversion.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/mpc/conversion.rst rename to doc/Archive/contributed_packages/mpc/conversion.rst diff --git a/doc/OnlineDocs/contributed_packages/mpc/data.rst b/doc/Archive/contributed_packages/mpc/data.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/mpc/data.rst rename to doc/Archive/contributed_packages/mpc/data.rst diff --git a/doc/OnlineDocs/contributed_packages/mpc/examples.rst b/doc/Archive/contributed_packages/mpc/examples.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/mpc/examples.rst rename to doc/Archive/contributed_packages/mpc/examples.rst diff --git a/doc/OnlineDocs/contributed_packages/mpc/faq.rst b/doc/Archive/contributed_packages/mpc/faq.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/mpc/faq.rst rename to doc/Archive/contributed_packages/mpc/faq.rst diff --git a/doc/OnlineDocs/contributed_packages/mpc/index.rst b/doc/Archive/contributed_packages/mpc/index.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/mpc/index.rst rename to doc/Archive/contributed_packages/mpc/index.rst diff --git a/doc/OnlineDocs/contributed_packages/mpc/interface.rst b/doc/Archive/contributed_packages/mpc/interface.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/mpc/interface.rst rename to doc/Archive/contributed_packages/mpc/interface.rst diff --git a/doc/OnlineDocs/contributed_packages/mpc/modeling.rst b/doc/Archive/contributed_packages/mpc/modeling.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/mpc/modeling.rst rename to doc/Archive/contributed_packages/mpc/modeling.rst diff --git a/doc/OnlineDocs/contributed_packages/mpc/overview.rst b/doc/Archive/contributed_packages/mpc/overview.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/mpc/overview.rst rename to doc/Archive/contributed_packages/mpc/overview.rst diff --git a/doc/OnlineDocs/contributed_packages/multistart.rst b/doc/Archive/contributed_packages/multistart.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/multistart.rst rename to doc/Archive/contributed_packages/multistart.rst diff --git a/doc/OnlineDocs/contributed_packages/parmest/api.rst b/doc/Archive/contributed_packages/parmest/api.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/parmest/api.rst rename to doc/Archive/contributed_packages/parmest/api.rst diff --git a/doc/OnlineDocs/contributed_packages/parmest/boxplot.png b/doc/Archive/contributed_packages/parmest/boxplot.png similarity index 100% rename from doc/OnlineDocs/contributed_packages/parmest/boxplot.png rename to doc/Archive/contributed_packages/parmest/boxplot.png diff --git a/doc/OnlineDocs/contributed_packages/parmest/covariance.rst b/doc/Archive/contributed_packages/parmest/covariance.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/parmest/covariance.rst rename to doc/Archive/contributed_packages/parmest/covariance.rst diff --git a/doc/OnlineDocs/contributed_packages/parmest/datarec.rst b/doc/Archive/contributed_packages/parmest/datarec.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/parmest/datarec.rst rename to doc/Archive/contributed_packages/parmest/datarec.rst diff --git a/doc/OnlineDocs/contributed_packages/parmest/driver.rst b/doc/Archive/contributed_packages/parmest/driver.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/parmest/driver.rst rename to doc/Archive/contributed_packages/parmest/driver.rst diff --git a/doc/OnlineDocs/contributed_packages/parmest/examples.rst b/doc/Archive/contributed_packages/parmest/examples.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/parmest/examples.rst rename to doc/Archive/contributed_packages/parmest/examples.rst diff --git a/doc/OnlineDocs/contributed_packages/parmest/graphics.rst b/doc/Archive/contributed_packages/parmest/graphics.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/parmest/graphics.rst rename to doc/Archive/contributed_packages/parmest/graphics.rst diff --git a/doc/OnlineDocs/contributed_packages/parmest/index.rst b/doc/Archive/contributed_packages/parmest/index.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/parmest/index.rst rename to doc/Archive/contributed_packages/parmest/index.rst diff --git a/doc/OnlineDocs/contributed_packages/parmest/installation.rst b/doc/Archive/contributed_packages/parmest/installation.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/parmest/installation.rst rename to doc/Archive/contributed_packages/parmest/installation.rst diff --git a/doc/OnlineDocs/contributed_packages/parmest/overview.rst b/doc/Archive/contributed_packages/parmest/overview.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/parmest/overview.rst rename to doc/Archive/contributed_packages/parmest/overview.rst diff --git a/doc/OnlineDocs/contributed_packages/parmest/pairwise_plot_CI.png b/doc/Archive/contributed_packages/parmest/pairwise_plot_CI.png similarity index 100% rename from doc/OnlineDocs/contributed_packages/parmest/pairwise_plot_CI.png rename to doc/Archive/contributed_packages/parmest/pairwise_plot_CI.png diff --git a/doc/OnlineDocs/contributed_packages/parmest/pairwise_plot_LR.png b/doc/Archive/contributed_packages/parmest/pairwise_plot_LR.png similarity index 100% rename from doc/OnlineDocs/contributed_packages/parmest/pairwise_plot_LR.png rename to doc/Archive/contributed_packages/parmest/pairwise_plot_LR.png diff --git a/doc/OnlineDocs/contributed_packages/parmest/parallel.rst b/doc/Archive/contributed_packages/parmest/parallel.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/parmest/parallel.rst rename to doc/Archive/contributed_packages/parmest/parallel.rst diff --git a/doc/OnlineDocs/contributed_packages/parmest/scencreate.rst b/doc/Archive/contributed_packages/parmest/scencreate.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/parmest/scencreate.rst rename to doc/Archive/contributed_packages/parmest/scencreate.rst diff --git a/doc/OnlineDocs/contributed_packages/preprocessing.rst b/doc/Archive/contributed_packages/preprocessing.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/preprocessing.rst rename to doc/Archive/contributed_packages/preprocessing.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/api.rst b/doc/Archive/contributed_packages/pynumero/api.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/api.rst rename to doc/Archive/contributed_packages/pynumero/api.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/backward_compatibility.rst b/doc/Archive/contributed_packages/pynumero/backward_compatibility.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/backward_compatibility.rst rename to doc/Archive/contributed_packages/pynumero/backward_compatibility.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/index.rst b/doc/Archive/contributed_packages/pynumero/index.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/index.rst rename to doc/Archive/contributed_packages/pynumero/index.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/installation.rst b/doc/Archive/contributed_packages/pynumero/installation.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/installation.rst rename to doc/Archive/contributed_packages/pynumero/installation.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.nlp.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.nlp.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.nlp.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.interfaces.nlp.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.interfaces.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.interfaces.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.linalg.base.rst b/doc/Archive/contributed_packages/pynumero/pynumero.linalg.base.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.linalg.base.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.linalg.base.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.linalg.ma27.rst b/doc/Archive/contributed_packages/pynumero/pynumero.linalg.ma27.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.linalg.ma27.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.linalg.ma27.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.linalg.ma57.rst b/doc/Archive/contributed_packages/pynumero/pynumero.linalg.ma57.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.linalg.ma57.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.linalg.ma57.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.linalg.mumps.rst b/doc/Archive/contributed_packages/pynumero/pynumero.linalg.mumps.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.linalg.mumps.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.linalg.mumps.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.linalg.rst b/doc/Archive/contributed_packages/pynumero/pynumero.linalg.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.linalg.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.linalg.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.linalg.scipy.rst b/doc/Archive/contributed_packages/pynumero/pynumero.linalg.scipy.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.linalg.scipy.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.linalg.scipy.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.sparse.block_vector.rst b/doc/Archive/contributed_packages/pynumero/pynumero.sparse.block_vector.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.sparse.block_vector.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.sparse.block_vector.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/pynumero.sparse.rst b/doc/Archive/contributed_packages/pynumero/pynumero.sparse.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/pynumero.sparse.rst rename to doc/Archive/contributed_packages/pynumero/pynumero.sparse.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst b/doc/Archive/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst rename to doc/Archive/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst b/doc/Archive/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst rename to doc/Archive/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/tutorial.mpi_blocks.rst b/doc/Archive/contributed_packages/pynumero/tutorial.mpi_blocks.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/tutorial.mpi_blocks.rst rename to doc/Archive/contributed_packages/pynumero/tutorial.mpi_blocks.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/tutorial.nlp_interfaces.rst b/doc/Archive/contributed_packages/pynumero/tutorial.nlp_interfaces.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/tutorial.nlp_interfaces.rst rename to doc/Archive/contributed_packages/pynumero/tutorial.nlp_interfaces.rst diff --git a/doc/OnlineDocs/contributed_packages/pynumero/tutorial.rst b/doc/Archive/contributed_packages/pynumero/tutorial.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pynumero/tutorial.rst rename to doc/Archive/contributed_packages/pynumero/tutorial.rst diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/Archive/contributed_packages/pyros.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/pyros.rst rename to doc/Archive/contributed_packages/pyros.rst diff --git a/doc/OnlineDocs/contributed_packages/satsolver.rst b/doc/Archive/contributed_packages/satsolver.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/satsolver.rst rename to doc/Archive/contributed_packages/satsolver.rst diff --git a/doc/OnlineDocs/contributed_packages/sensitivity_toolbox.rst b/doc/Archive/contributed_packages/sensitivity_toolbox.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/sensitivity_toolbox.rst rename to doc/Archive/contributed_packages/sensitivity_toolbox.rst diff --git a/doc/OnlineDocs/contributed_packages/trustregion.rst b/doc/Archive/contributed_packages/trustregion.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/trustregion.rst rename to doc/Archive/contributed_packages/trustregion.rst diff --git a/doc/OnlineDocs/contribution_guide.rst b/doc/Archive/contribution_guide.rst similarity index 100% rename from doc/OnlineDocs/contribution_guide.rst rename to doc/Archive/contribution_guide.rst diff --git a/doc/OnlineDocs/developer_reference/config.rst b/doc/Archive/developer_reference/config.rst similarity index 100% rename from doc/OnlineDocs/developer_reference/config.rst rename to doc/Archive/developer_reference/config.rst diff --git a/doc/OnlineDocs/developer_reference/deprecation.rst b/doc/Archive/developer_reference/deprecation.rst similarity index 100% rename from doc/OnlineDocs/developer_reference/deprecation.rst rename to doc/Archive/developer_reference/deprecation.rst diff --git a/doc/OnlineDocs/developer_reference/expressions/design.rst b/doc/Archive/developer_reference/expressions/design.rst similarity index 100% rename from doc/OnlineDocs/developer_reference/expressions/design.rst rename to doc/Archive/developer_reference/expressions/design.rst diff --git a/doc/OnlineDocs/developer_reference/expressions/index.rst b/doc/Archive/developer_reference/expressions/index.rst similarity index 100% rename from doc/OnlineDocs/developer_reference/expressions/index.rst rename to doc/Archive/developer_reference/expressions/index.rst diff --git a/doc/OnlineDocs/developer_reference/expressions/managing.rst b/doc/Archive/developer_reference/expressions/managing.rst similarity index 100% rename from doc/OnlineDocs/developer_reference/expressions/managing.rst rename to doc/Archive/developer_reference/expressions/managing.rst diff --git a/doc/OnlineDocs/developer_reference/expressions/overview.rst b/doc/Archive/developer_reference/expressions/overview.rst similarity index 100% rename from doc/OnlineDocs/developer_reference/expressions/overview.rst rename to doc/Archive/developer_reference/expressions/overview.rst diff --git a/doc/OnlineDocs/developer_reference/expressions/performance.rst b/doc/Archive/developer_reference/expressions/performance.rst similarity index 100% rename from doc/OnlineDocs/developer_reference/expressions/performance.rst rename to doc/Archive/developer_reference/expressions/performance.rst diff --git a/doc/OnlineDocs/developer_reference/future.rst b/doc/Archive/developer_reference/future.rst similarity index 100% rename from doc/OnlineDocs/developer_reference/future.rst rename to doc/Archive/developer_reference/future.rst diff --git a/doc/OnlineDocs/developer_reference/index.rst b/doc/Archive/developer_reference/index.rst similarity index 100% rename from doc/OnlineDocs/developer_reference/index.rst rename to doc/Archive/developer_reference/index.rst diff --git a/doc/OnlineDocs/developer_reference/solvers.rst b/doc/Archive/developer_reference/solvers.rst similarity index 100% rename from doc/OnlineDocs/developer_reference/solvers.rst rename to doc/Archive/developer_reference/solvers.rst diff --git a/doc/OnlineDocs/docutils.conf b/doc/Archive/docutils.conf similarity index 100% rename from doc/OnlineDocs/docutils.conf rename to doc/Archive/docutils.conf diff --git a/doc/OnlineDocs/errors.rst b/doc/Archive/errors.rst similarity index 100% rename from doc/OnlineDocs/errors.rst rename to doc/Archive/errors.rst diff --git a/doc/OnlineDocs/index.rst b/doc/Archive/index.rst similarity index 100% rename from doc/OnlineDocs/index.rst rename to doc/Archive/index.rst diff --git a/doc/OnlineDocs/installation.rst b/doc/Archive/installation.rst similarity index 100% rename from doc/OnlineDocs/installation.rst rename to doc/Archive/installation.rst diff --git a/doc/OnlineDocs/library_reference/aml/index.rst b/doc/Archive/library_reference/aml/index.rst similarity index 100% rename from doc/OnlineDocs/library_reference/aml/index.rst rename to doc/Archive/library_reference/aml/index.rst diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.base.rst b/doc/Archive/library_reference/appsi/appsi.base.rst similarity index 100% rename from doc/OnlineDocs/library_reference/appsi/appsi.base.rst rename to doc/Archive/library_reference/appsi/appsi.base.rst diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.rst b/doc/Archive/library_reference/appsi/appsi.rst similarity index 100% rename from doc/OnlineDocs/library_reference/appsi/appsi.rst rename to doc/Archive/library_reference/appsi/appsi.rst diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.cbc.rst b/doc/Archive/library_reference/appsi/appsi.solvers.cbc.rst similarity index 100% rename from doc/OnlineDocs/library_reference/appsi/appsi.solvers.cbc.rst rename to doc/Archive/library_reference/appsi/appsi.solvers.cbc.rst diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.cplex.rst b/doc/Archive/library_reference/appsi/appsi.solvers.cplex.rst similarity index 100% rename from doc/OnlineDocs/library_reference/appsi/appsi.solvers.cplex.rst rename to doc/Archive/library_reference/appsi/appsi.solvers.cplex.rst diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.gurobi.rst b/doc/Archive/library_reference/appsi/appsi.solvers.gurobi.rst similarity index 100% rename from doc/OnlineDocs/library_reference/appsi/appsi.solvers.gurobi.rst rename to doc/Archive/library_reference/appsi/appsi.solvers.gurobi.rst diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.highs.rst b/doc/Archive/library_reference/appsi/appsi.solvers.highs.rst similarity index 100% rename from doc/OnlineDocs/library_reference/appsi/appsi.solvers.highs.rst rename to doc/Archive/library_reference/appsi/appsi.solvers.highs.rst diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.ipopt.rst b/doc/Archive/library_reference/appsi/appsi.solvers.ipopt.rst similarity index 100% rename from doc/OnlineDocs/library_reference/appsi/appsi.solvers.ipopt.rst rename to doc/Archive/library_reference/appsi/appsi.solvers.ipopt.rst diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst b/doc/Archive/library_reference/appsi/appsi.solvers.maingo.rst similarity index 100% rename from doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst rename to doc/Archive/library_reference/appsi/appsi.solvers.maingo.rst diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst b/doc/Archive/library_reference/appsi/appsi.solvers.rst similarity index 100% rename from doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst rename to doc/Archive/library_reference/appsi/appsi.solvers.rst diff --git a/doc/OnlineDocs/library_reference/common/config.rst b/doc/Archive/library_reference/common/config.rst similarity index 100% rename from doc/OnlineDocs/library_reference/common/config.rst rename to doc/Archive/library_reference/common/config.rst diff --git a/doc/OnlineDocs/library_reference/common/dependencies.rst b/doc/Archive/library_reference/common/dependencies.rst similarity index 100% rename from doc/OnlineDocs/library_reference/common/dependencies.rst rename to doc/Archive/library_reference/common/dependencies.rst diff --git a/doc/OnlineDocs/library_reference/common/deprecation.rst b/doc/Archive/library_reference/common/deprecation.rst similarity index 100% rename from doc/OnlineDocs/library_reference/common/deprecation.rst rename to doc/Archive/library_reference/common/deprecation.rst diff --git a/doc/OnlineDocs/library_reference/common/enums.rst b/doc/Archive/library_reference/common/enums.rst similarity index 100% rename from doc/OnlineDocs/library_reference/common/enums.rst rename to doc/Archive/library_reference/common/enums.rst diff --git a/doc/OnlineDocs/library_reference/common/errors.rst b/doc/Archive/library_reference/common/errors.rst similarity index 100% rename from doc/OnlineDocs/library_reference/common/errors.rst rename to doc/Archive/library_reference/common/errors.rst diff --git a/doc/OnlineDocs/library_reference/common/fileutils.rst b/doc/Archive/library_reference/common/fileutils.rst similarity index 100% rename from doc/OnlineDocs/library_reference/common/fileutils.rst rename to doc/Archive/library_reference/common/fileutils.rst diff --git a/doc/OnlineDocs/library_reference/common/formatting.rst b/doc/Archive/library_reference/common/formatting.rst similarity index 100% rename from doc/OnlineDocs/library_reference/common/formatting.rst rename to doc/Archive/library_reference/common/formatting.rst diff --git a/doc/OnlineDocs/library_reference/common/index.rst b/doc/Archive/library_reference/common/index.rst similarity index 100% rename from doc/OnlineDocs/library_reference/common/index.rst rename to doc/Archive/library_reference/common/index.rst diff --git a/doc/OnlineDocs/library_reference/common/tempfiles.rst b/doc/Archive/library_reference/common/tempfiles.rst similarity index 100% rename from doc/OnlineDocs/library_reference/common/tempfiles.rst rename to doc/Archive/library_reference/common/tempfiles.rst diff --git a/doc/OnlineDocs/library_reference/common/timing.rst b/doc/Archive/library_reference/common/timing.rst similarity index 100% rename from doc/OnlineDocs/library_reference/common/timing.rst rename to doc/Archive/library_reference/common/timing.rst diff --git a/doc/OnlineDocs/library_reference/data/index.rst b/doc/Archive/library_reference/data/index.rst similarity index 100% rename from doc/OnlineDocs/library_reference/data/index.rst rename to doc/Archive/library_reference/data/index.rst diff --git a/doc/OnlineDocs/library_reference/expressions/building.rst b/doc/Archive/library_reference/expressions/building.rst similarity index 100% rename from doc/OnlineDocs/library_reference/expressions/building.rst rename to doc/Archive/library_reference/expressions/building.rst diff --git a/doc/OnlineDocs/library_reference/expressions/classes.rst b/doc/Archive/library_reference/expressions/classes.rst similarity index 100% rename from doc/OnlineDocs/library_reference/expressions/classes.rst rename to doc/Archive/library_reference/expressions/classes.rst diff --git a/doc/OnlineDocs/library_reference/expressions/context_managers.rst b/doc/Archive/library_reference/expressions/context_managers.rst similarity index 100% rename from doc/OnlineDocs/library_reference/expressions/context_managers.rst rename to doc/Archive/library_reference/expressions/context_managers.rst diff --git a/doc/OnlineDocs/library_reference/expressions/index.rst b/doc/Archive/library_reference/expressions/index.rst similarity index 100% rename from doc/OnlineDocs/library_reference/expressions/index.rst rename to doc/Archive/library_reference/expressions/index.rst diff --git a/doc/OnlineDocs/library_reference/expressions/managing.rst b/doc/Archive/library_reference/expressions/managing.rst similarity index 100% rename from doc/OnlineDocs/library_reference/expressions/managing.rst rename to doc/Archive/library_reference/expressions/managing.rst diff --git a/doc/OnlineDocs/library_reference/expressions/visitors.rst b/doc/Archive/library_reference/expressions/visitors.rst similarity index 100% rename from doc/OnlineDocs/library_reference/expressions/visitors.rst rename to doc/Archive/library_reference/expressions/visitors.rst diff --git a/doc/OnlineDocs/library_reference/index.rst b/doc/Archive/library_reference/index.rst similarity index 100% rename from doc/OnlineDocs/library_reference/index.rst rename to doc/Archive/library_reference/index.rst diff --git a/doc/OnlineDocs/library_reference/kernel/base.rst b/doc/Archive/library_reference/kernel/base.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/base.rst rename to doc/Archive/library_reference/kernel/base.rst diff --git a/doc/OnlineDocs/library_reference/kernel/block.rst b/doc/Archive/library_reference/kernel/block.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/block.rst rename to doc/Archive/library_reference/kernel/block.rst diff --git a/doc/OnlineDocs/library_reference/kernel/conic.rst b/doc/Archive/library_reference/kernel/conic.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/conic.rst rename to doc/Archive/library_reference/kernel/conic.rst diff --git a/doc/OnlineDocs/library_reference/kernel/constraint.rst b/doc/Archive/library_reference/kernel/constraint.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/constraint.rst rename to doc/Archive/library_reference/kernel/constraint.rst diff --git a/doc/OnlineDocs/library_reference/kernel/dict_container.rst b/doc/Archive/library_reference/kernel/dict_container.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/dict_container.rst rename to doc/Archive/library_reference/kernel/dict_container.rst diff --git a/doc/OnlineDocs/library_reference/kernel/examples/aml_example.py b/doc/Archive/library_reference/kernel/examples/aml_example.py similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/examples/aml_example.py rename to doc/Archive/library_reference/kernel/examples/aml_example.py diff --git a/doc/OnlineDocs/library_reference/kernel/examples/conic.py b/doc/Archive/library_reference/kernel/examples/conic.py similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/examples/conic.py rename to doc/Archive/library_reference/kernel/examples/conic.py diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_containers.py b/doc/Archive/library_reference/kernel/examples/kernel_containers.py similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/examples/kernel_containers.py rename to doc/Archive/library_reference/kernel/examples/kernel_containers.py diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_example.py b/doc/Archive/library_reference/kernel/examples/kernel_example.py similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/examples/kernel_example.py rename to doc/Archive/library_reference/kernel/examples/kernel_example.py diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_solving.py b/doc/Archive/library_reference/kernel/examples/kernel_solving.py similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/examples/kernel_solving.py rename to doc/Archive/library_reference/kernel/examples/kernel_solving.py diff --git a/doc/OnlineDocs/library_reference/kernel/examples/kernel_subclassing.py b/doc/Archive/library_reference/kernel/examples/kernel_subclassing.py similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/examples/kernel_subclassing.py rename to doc/Archive/library_reference/kernel/examples/kernel_subclassing.py diff --git a/doc/OnlineDocs/library_reference/kernel/examples/transformer.py b/doc/Archive/library_reference/kernel/examples/transformer.py similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/examples/transformer.py rename to doc/Archive/library_reference/kernel/examples/transformer.py diff --git a/doc/OnlineDocs/library_reference/kernel/expression.rst b/doc/Archive/library_reference/kernel/expression.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/expression.rst rename to doc/Archive/library_reference/kernel/expression.rst diff --git a/doc/OnlineDocs/library_reference/kernel/heterogeneous_container.rst b/doc/Archive/library_reference/kernel/heterogeneous_container.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/heterogeneous_container.rst rename to doc/Archive/library_reference/kernel/heterogeneous_container.rst diff --git a/doc/OnlineDocs/library_reference/kernel/homogeneous_container.rst b/doc/Archive/library_reference/kernel/homogeneous_container.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/homogeneous_container.rst rename to doc/Archive/library_reference/kernel/homogeneous_container.rst diff --git a/doc/OnlineDocs/library_reference/kernel/index.rst b/doc/Archive/library_reference/kernel/index.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/index.rst rename to doc/Archive/library_reference/kernel/index.rst diff --git a/doc/OnlineDocs/library_reference/kernel/list_container.rst b/doc/Archive/library_reference/kernel/list_container.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/list_container.rst rename to doc/Archive/library_reference/kernel/list_container.rst diff --git a/doc/OnlineDocs/library_reference/kernel/objective.rst b/doc/Archive/library_reference/kernel/objective.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/objective.rst rename to doc/Archive/library_reference/kernel/objective.rst diff --git a/doc/OnlineDocs/library_reference/kernel/parameter.rst b/doc/Archive/library_reference/kernel/parameter.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/parameter.rst rename to doc/Archive/library_reference/kernel/parameter.rst diff --git a/doc/OnlineDocs/library_reference/kernel/piecewise/index.rst b/doc/Archive/library_reference/kernel/piecewise/index.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/piecewise/index.rst rename to doc/Archive/library_reference/kernel/piecewise/index.rst diff --git a/doc/OnlineDocs/library_reference/kernel/piecewise/piecewise.rst b/doc/Archive/library_reference/kernel/piecewise/piecewise.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/piecewise/piecewise.rst rename to doc/Archive/library_reference/kernel/piecewise/piecewise.rst diff --git a/doc/OnlineDocs/library_reference/kernel/piecewise/piecewise_nd.rst b/doc/Archive/library_reference/kernel/piecewise/piecewise_nd.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/piecewise/piecewise_nd.rst rename to doc/Archive/library_reference/kernel/piecewise/piecewise_nd.rst diff --git a/doc/OnlineDocs/library_reference/kernel/piecewise/util.rst b/doc/Archive/library_reference/kernel/piecewise/util.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/piecewise/util.rst rename to doc/Archive/library_reference/kernel/piecewise/util.rst diff --git a/doc/OnlineDocs/library_reference/kernel/sos.rst b/doc/Archive/library_reference/kernel/sos.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/sos.rst rename to doc/Archive/library_reference/kernel/sos.rst diff --git a/doc/OnlineDocs/library_reference/kernel/suffix.rst b/doc/Archive/library_reference/kernel/suffix.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/suffix.rst rename to doc/Archive/library_reference/kernel/suffix.rst diff --git a/doc/OnlineDocs/library_reference/kernel/syntax_comparison.rst b/doc/Archive/library_reference/kernel/syntax_comparison.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/syntax_comparison.rst rename to doc/Archive/library_reference/kernel/syntax_comparison.rst diff --git a/doc/OnlineDocs/library_reference/kernel/tuple_container.rst b/doc/Archive/library_reference/kernel/tuple_container.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/tuple_container.rst rename to doc/Archive/library_reference/kernel/tuple_container.rst diff --git a/doc/OnlineDocs/library_reference/kernel/variable.rst b/doc/Archive/library_reference/kernel/variable.rst similarity index 100% rename from doc/OnlineDocs/library_reference/kernel/variable.rst rename to doc/Archive/library_reference/kernel/variable.rst diff --git a/doc/OnlineDocs/library_reference/solvers/cplex_persistent.rst b/doc/Archive/library_reference/solvers/cplex_persistent.rst similarity index 100% rename from doc/OnlineDocs/library_reference/solvers/cplex_persistent.rst rename to doc/Archive/library_reference/solvers/cplex_persistent.rst diff --git a/doc/OnlineDocs/library_reference/solvers/gams.rst b/doc/Archive/library_reference/solvers/gams.rst similarity index 100% rename from doc/OnlineDocs/library_reference/solvers/gams.rst rename to doc/Archive/library_reference/solvers/gams.rst diff --git a/doc/OnlineDocs/library_reference/solvers/gurobi_direct.rst b/doc/Archive/library_reference/solvers/gurobi_direct.rst similarity index 100% rename from doc/OnlineDocs/library_reference/solvers/gurobi_direct.rst rename to doc/Archive/library_reference/solvers/gurobi_direct.rst diff --git a/doc/OnlineDocs/library_reference/solvers/gurobi_persistent.rst b/doc/Archive/library_reference/solvers/gurobi_persistent.rst similarity index 100% rename from doc/OnlineDocs/library_reference/solvers/gurobi_persistent.rst rename to doc/Archive/library_reference/solvers/gurobi_persistent.rst diff --git a/doc/OnlineDocs/library_reference/solvers/index.rst b/doc/Archive/library_reference/solvers/index.rst similarity index 100% rename from doc/OnlineDocs/library_reference/solvers/index.rst rename to doc/Archive/library_reference/solvers/index.rst diff --git a/doc/OnlineDocs/library_reference/solvers/xpress_persistent.rst b/doc/Archive/library_reference/solvers/xpress_persistent.rst similarity index 100% rename from doc/OnlineDocs/library_reference/solvers/xpress_persistent.rst rename to doc/Archive/library_reference/solvers/xpress_persistent.rst diff --git a/doc/OnlineDocs/make.bat b/doc/Archive/make.bat similarity index 100% rename from doc/OnlineDocs/make.bat rename to doc/Archive/make.bat diff --git a/doc/OnlineDocs/model_debugging/FAQ.rst b/doc/Archive/model_debugging/FAQ.rst similarity index 100% rename from doc/OnlineDocs/model_debugging/FAQ.rst rename to doc/Archive/model_debugging/FAQ.rst diff --git a/doc/OnlineDocs/model_debugging/getting_help.rst b/doc/Archive/model_debugging/getting_help.rst similarity index 100% rename from doc/OnlineDocs/model_debugging/getting_help.rst rename to doc/Archive/model_debugging/getting_help.rst diff --git a/doc/OnlineDocs/model_debugging/index.rst b/doc/Archive/model_debugging/index.rst similarity index 100% rename from doc/OnlineDocs/model_debugging/index.rst rename to doc/Archive/model_debugging/index.rst diff --git a/doc/OnlineDocs/model_debugging/model_interrogation.rst b/doc/Archive/model_debugging/model_interrogation.rst similarity index 100% rename from doc/OnlineDocs/model_debugging/model_interrogation.rst rename to doc/Archive/model_debugging/model_interrogation.rst diff --git a/doc/OnlineDocs/model_transformations/index.rst b/doc/Archive/model_transformations/index.rst similarity index 100% rename from doc/OnlineDocs/model_transformations/index.rst rename to doc/Archive/model_transformations/index.rst diff --git a/doc/OnlineDocs/model_transformations/scaling.rst b/doc/Archive/model_transformations/scaling.rst similarity index 100% rename from doc/OnlineDocs/model_transformations/scaling.rst rename to doc/Archive/model_transformations/scaling.rst diff --git a/doc/OnlineDocs/modeling_extensions/__init__.py b/doc/Archive/modeling_extensions/__init__.py similarity index 100% rename from doc/OnlineDocs/modeling_extensions/__init__.py rename to doc/Archive/modeling_extensions/__init__.py diff --git a/doc/OnlineDocs/modeling_extensions/bilevel.rst b/doc/Archive/modeling_extensions/bilevel.rst similarity index 100% rename from doc/OnlineDocs/modeling_extensions/bilevel.rst rename to doc/Archive/modeling_extensions/bilevel.rst diff --git a/doc/OnlineDocs/modeling_extensions/dae.rst b/doc/Archive/modeling_extensions/dae.rst similarity index 100% rename from doc/OnlineDocs/modeling_extensions/dae.rst rename to doc/Archive/modeling_extensions/dae.rst diff --git a/doc/OnlineDocs/modeling_extensions/gdp/concepts.rst b/doc/Archive/modeling_extensions/gdp/concepts.rst similarity index 100% rename from doc/OnlineDocs/modeling_extensions/gdp/concepts.rst rename to doc/Archive/modeling_extensions/gdp/concepts.rst diff --git a/doc/OnlineDocs/modeling_extensions/gdp/index.rst b/doc/Archive/modeling_extensions/gdp/index.rst similarity index 100% rename from doc/OnlineDocs/modeling_extensions/gdp/index.rst rename to doc/Archive/modeling_extensions/gdp/index.rst diff --git a/doc/OnlineDocs/modeling_extensions/gdp/modeling.rst b/doc/Archive/modeling_extensions/gdp/modeling.rst similarity index 100% rename from doc/OnlineDocs/modeling_extensions/gdp/modeling.rst rename to doc/Archive/modeling_extensions/gdp/modeling.rst diff --git a/doc/OnlineDocs/modeling_extensions/gdp/solving.rst b/doc/Archive/modeling_extensions/gdp/solving.rst similarity index 100% rename from doc/OnlineDocs/modeling_extensions/gdp/solving.rst rename to doc/Archive/modeling_extensions/gdp/solving.rst diff --git a/doc/OnlineDocs/modeling_extensions/index.rst b/doc/Archive/modeling_extensions/index.rst similarity index 100% rename from doc/OnlineDocs/modeling_extensions/index.rst rename to doc/Archive/modeling_extensions/index.rst diff --git a/doc/OnlineDocs/modeling_extensions/mpec.rst b/doc/Archive/modeling_extensions/mpec.rst similarity index 100% rename from doc/OnlineDocs/modeling_extensions/mpec.rst rename to doc/Archive/modeling_extensions/mpec.rst diff --git a/doc/OnlineDocs/modeling_extensions/network.rst b/doc/Archive/modeling_extensions/network.rst similarity index 100% rename from doc/OnlineDocs/modeling_extensions/network.rst rename to doc/Archive/modeling_extensions/network.rst diff --git a/doc/OnlineDocs/modeling_extensions/reduce_points_demo.png b/doc/Archive/modeling_extensions/reduce_points_demo.png similarity index 100% rename from doc/OnlineDocs/modeling_extensions/reduce_points_demo.png rename to doc/Archive/modeling_extensions/reduce_points_demo.png diff --git a/doc/OnlineDocs/modeling_extensions/stochastic_programming.rst b/doc/Archive/modeling_extensions/stochastic_programming.rst similarity index 100% rename from doc/OnlineDocs/modeling_extensions/stochastic_programming.rst rename to doc/Archive/modeling_extensions/stochastic_programming.rst diff --git a/doc/OnlineDocs/pyomo_modeling_components/Constraints.rst b/doc/Archive/pyomo_modeling_components/Constraints.rst similarity index 100% rename from doc/OnlineDocs/pyomo_modeling_components/Constraints.rst rename to doc/Archive/pyomo_modeling_components/Constraints.rst diff --git a/doc/OnlineDocs/pyomo_modeling_components/Expressions.rst b/doc/Archive/pyomo_modeling_components/Expressions.rst similarity index 100% rename from doc/OnlineDocs/pyomo_modeling_components/Expressions.rst rename to doc/Archive/pyomo_modeling_components/Expressions.rst diff --git a/doc/OnlineDocs/pyomo_modeling_components/Objectives.rst b/doc/Archive/pyomo_modeling_components/Objectives.rst similarity index 100% rename from doc/OnlineDocs/pyomo_modeling_components/Objectives.rst rename to doc/Archive/pyomo_modeling_components/Objectives.rst diff --git a/doc/OnlineDocs/pyomo_modeling_components/Parameters.rst b/doc/Archive/pyomo_modeling_components/Parameters.rst similarity index 100% rename from doc/OnlineDocs/pyomo_modeling_components/Parameters.rst rename to doc/Archive/pyomo_modeling_components/Parameters.rst diff --git a/doc/OnlineDocs/pyomo_modeling_components/Sets.rst b/doc/Archive/pyomo_modeling_components/Sets.rst similarity index 100% rename from doc/OnlineDocs/pyomo_modeling_components/Sets.rst rename to doc/Archive/pyomo_modeling_components/Sets.rst diff --git a/doc/OnlineDocs/pyomo_modeling_components/Suffixes.rst b/doc/Archive/pyomo_modeling_components/Suffixes.rst similarity index 100% rename from doc/OnlineDocs/pyomo_modeling_components/Suffixes.rst rename to doc/Archive/pyomo_modeling_components/Suffixes.rst diff --git a/doc/OnlineDocs/pyomo_modeling_components/Variables.rst b/doc/Archive/pyomo_modeling_components/Variables.rst similarity index 100% rename from doc/OnlineDocs/pyomo_modeling_components/Variables.rst rename to doc/Archive/pyomo_modeling_components/Variables.rst diff --git a/doc/OnlineDocs/pyomo_modeling_components/index.rst b/doc/Archive/pyomo_modeling_components/index.rst similarity index 100% rename from doc/OnlineDocs/pyomo_modeling_components/index.rst rename to doc/Archive/pyomo_modeling_components/index.rst diff --git a/doc/OnlineDocs/pyomo_overview/abstract_concrete.rst b/doc/Archive/pyomo_overview/abstract_concrete.rst similarity index 100% rename from doc/OnlineDocs/pyomo_overview/abstract_concrete.rst rename to doc/Archive/pyomo_overview/abstract_concrete.rst diff --git a/doc/OnlineDocs/pyomo_overview/index.rst b/doc/Archive/pyomo_overview/index.rst similarity index 100% rename from doc/OnlineDocs/pyomo_overview/index.rst rename to doc/Archive/pyomo_overview/index.rst diff --git a/doc/OnlineDocs/pyomo_overview/math_modeling.rst b/doc/Archive/pyomo_overview/math_modeling.rst similarity index 100% rename from doc/OnlineDocs/pyomo_overview/math_modeling.rst rename to doc/Archive/pyomo_overview/math_modeling.rst diff --git a/doc/OnlineDocs/pyomo_overview/overview_components.rst b/doc/Archive/pyomo_overview/overview_components.rst similarity index 100% rename from doc/OnlineDocs/pyomo_overview/overview_components.rst rename to doc/Archive/pyomo_overview/overview_components.rst diff --git a/doc/OnlineDocs/pyomo_overview/simple_examples.rst b/doc/Archive/pyomo_overview/simple_examples.rst similarity index 100% rename from doc/OnlineDocs/pyomo_overview/simple_examples.rst rename to doc/Archive/pyomo_overview/simple_examples.rst diff --git a/doc/OnlineDocs/related_packages.rst b/doc/Archive/related_packages.rst similarity index 100% rename from doc/OnlineDocs/related_packages.rst rename to doc/Archive/related_packages.rst diff --git a/doc/OnlineDocs/solving_pyomo_models.rst b/doc/Archive/solving_pyomo_models.rst similarity index 100% rename from doc/OnlineDocs/solving_pyomo_models.rst rename to doc/Archive/solving_pyomo_models.rst diff --git a/doc/OnlineDocs/src/data/A.tab b/doc/Archive/src/data/A.tab similarity index 100% rename from doc/OnlineDocs/src/data/A.tab rename to doc/Archive/src/data/A.tab diff --git a/doc/OnlineDocs/src/data/ABCD.tab b/doc/Archive/src/data/ABCD.tab similarity index 100% rename from doc/OnlineDocs/src/data/ABCD.tab rename to doc/Archive/src/data/ABCD.tab diff --git a/doc/OnlineDocs/src/data/ABCD.txt b/doc/Archive/src/data/ABCD.txt similarity index 100% rename from doc/OnlineDocs/src/data/ABCD.txt rename to doc/Archive/src/data/ABCD.txt diff --git a/doc/OnlineDocs/src/data/ABCD.xls b/doc/Archive/src/data/ABCD.xls similarity index 100% rename from doc/OnlineDocs/src/data/ABCD.xls rename to doc/Archive/src/data/ABCD.xls diff --git a/doc/OnlineDocs/src/data/ABCD1.dat b/doc/Archive/src/data/ABCD1.dat similarity index 100% rename from doc/OnlineDocs/src/data/ABCD1.dat rename to doc/Archive/src/data/ABCD1.dat diff --git a/doc/OnlineDocs/src/data/ABCD1.py b/doc/Archive/src/data/ABCD1.py similarity index 100% rename from doc/OnlineDocs/src/data/ABCD1.py rename to doc/Archive/src/data/ABCD1.py diff --git a/doc/OnlineDocs/src/data/ABCD1.txt b/doc/Archive/src/data/ABCD1.txt similarity index 100% rename from doc/OnlineDocs/src/data/ABCD1.txt rename to doc/Archive/src/data/ABCD1.txt diff --git a/doc/OnlineDocs/src/data/ABCD2.dat b/doc/Archive/src/data/ABCD2.dat similarity index 100% rename from doc/OnlineDocs/src/data/ABCD2.dat rename to doc/Archive/src/data/ABCD2.dat diff --git a/doc/OnlineDocs/src/data/ABCD2.py b/doc/Archive/src/data/ABCD2.py similarity index 100% rename from doc/OnlineDocs/src/data/ABCD2.py rename to doc/Archive/src/data/ABCD2.py diff --git a/doc/OnlineDocs/src/data/ABCD2.txt b/doc/Archive/src/data/ABCD2.txt similarity index 100% rename from doc/OnlineDocs/src/data/ABCD2.txt rename to doc/Archive/src/data/ABCD2.txt diff --git a/doc/OnlineDocs/src/data/ABCD3.dat b/doc/Archive/src/data/ABCD3.dat similarity index 100% rename from doc/OnlineDocs/src/data/ABCD3.dat rename to doc/Archive/src/data/ABCD3.dat diff --git a/doc/OnlineDocs/src/data/ABCD3.py b/doc/Archive/src/data/ABCD3.py similarity index 100% rename from doc/OnlineDocs/src/data/ABCD3.py rename to doc/Archive/src/data/ABCD3.py diff --git a/doc/OnlineDocs/src/data/ABCD3.txt b/doc/Archive/src/data/ABCD3.txt similarity index 100% rename from doc/OnlineDocs/src/data/ABCD3.txt rename to doc/Archive/src/data/ABCD3.txt diff --git a/doc/OnlineDocs/src/data/ABCD4.dat b/doc/Archive/src/data/ABCD4.dat similarity index 100% rename from doc/OnlineDocs/src/data/ABCD4.dat rename to doc/Archive/src/data/ABCD4.dat diff --git a/doc/OnlineDocs/src/data/ABCD4.py b/doc/Archive/src/data/ABCD4.py similarity index 100% rename from doc/OnlineDocs/src/data/ABCD4.py rename to doc/Archive/src/data/ABCD4.py diff --git a/doc/OnlineDocs/src/data/ABCD4.txt b/doc/Archive/src/data/ABCD4.txt similarity index 100% rename from doc/OnlineDocs/src/data/ABCD4.txt rename to doc/Archive/src/data/ABCD4.txt diff --git a/doc/OnlineDocs/src/data/ABCD5.dat b/doc/Archive/src/data/ABCD5.dat similarity index 100% rename from doc/OnlineDocs/src/data/ABCD5.dat rename to doc/Archive/src/data/ABCD5.dat diff --git a/doc/OnlineDocs/src/data/ABCD5.py b/doc/Archive/src/data/ABCD5.py similarity index 100% rename from doc/OnlineDocs/src/data/ABCD5.py rename to doc/Archive/src/data/ABCD5.py diff --git a/doc/OnlineDocs/src/data/ABCD5.txt b/doc/Archive/src/data/ABCD5.txt similarity index 100% rename from doc/OnlineDocs/src/data/ABCD5.txt rename to doc/Archive/src/data/ABCD5.txt diff --git a/doc/OnlineDocs/src/data/ABCD6.dat b/doc/Archive/src/data/ABCD6.dat similarity index 100% rename from doc/OnlineDocs/src/data/ABCD6.dat rename to doc/Archive/src/data/ABCD6.dat diff --git a/doc/OnlineDocs/src/data/ABCD6.py b/doc/Archive/src/data/ABCD6.py similarity index 100% rename from doc/OnlineDocs/src/data/ABCD6.py rename to doc/Archive/src/data/ABCD6.py diff --git a/doc/OnlineDocs/src/data/ABCD6.txt b/doc/Archive/src/data/ABCD6.txt similarity index 100% rename from doc/OnlineDocs/src/data/ABCD6.txt rename to doc/Archive/src/data/ABCD6.txt diff --git a/doc/OnlineDocs/src/data/ABCD7.dat b/doc/Archive/src/data/ABCD7.dat similarity index 100% rename from doc/OnlineDocs/src/data/ABCD7.dat rename to doc/Archive/src/data/ABCD7.dat diff --git a/doc/OnlineDocs/src/data/ABCD7.py b/doc/Archive/src/data/ABCD7.py similarity index 100% rename from doc/OnlineDocs/src/data/ABCD7.py rename to doc/Archive/src/data/ABCD7.py diff --git a/doc/OnlineDocs/src/data/ABCD7.txt b/doc/Archive/src/data/ABCD7.txt similarity index 100% rename from doc/OnlineDocs/src/data/ABCD7.txt rename to doc/Archive/src/data/ABCD7.txt diff --git a/doc/OnlineDocs/src/data/ABCD8.bad b/doc/Archive/src/data/ABCD8.bad similarity index 100% rename from doc/OnlineDocs/src/data/ABCD8.bad rename to doc/Archive/src/data/ABCD8.bad diff --git a/doc/OnlineDocs/src/data/ABCD8.dat b/doc/Archive/src/data/ABCD8.dat similarity index 100% rename from doc/OnlineDocs/src/data/ABCD8.dat rename to doc/Archive/src/data/ABCD8.dat diff --git a/doc/OnlineDocs/src/data/ABCD8.py b/doc/Archive/src/data/ABCD8.py similarity index 100% rename from doc/OnlineDocs/src/data/ABCD8.py rename to doc/Archive/src/data/ABCD8.py diff --git a/doc/OnlineDocs/src/data/ABCD9.bad b/doc/Archive/src/data/ABCD9.bad similarity index 100% rename from doc/OnlineDocs/src/data/ABCD9.bad rename to doc/Archive/src/data/ABCD9.bad diff --git a/doc/OnlineDocs/src/data/ABCD9.dat b/doc/Archive/src/data/ABCD9.dat similarity index 100% rename from doc/OnlineDocs/src/data/ABCD9.dat rename to doc/Archive/src/data/ABCD9.dat diff --git a/doc/OnlineDocs/src/data/ABCD9.py b/doc/Archive/src/data/ABCD9.py similarity index 100% rename from doc/OnlineDocs/src/data/ABCD9.py rename to doc/Archive/src/data/ABCD9.py diff --git a/doc/OnlineDocs/src/data/C.tab b/doc/Archive/src/data/C.tab similarity index 100% rename from doc/OnlineDocs/src/data/C.tab rename to doc/Archive/src/data/C.tab diff --git a/doc/OnlineDocs/src/data/D.tab b/doc/Archive/src/data/D.tab similarity index 100% rename from doc/OnlineDocs/src/data/D.tab rename to doc/Archive/src/data/D.tab diff --git a/doc/OnlineDocs/src/data/U.tab b/doc/Archive/src/data/U.tab similarity index 100% rename from doc/OnlineDocs/src/data/U.tab rename to doc/Archive/src/data/U.tab diff --git a/doc/OnlineDocs/src/data/Y.tab b/doc/Archive/src/data/Y.tab similarity index 100% rename from doc/OnlineDocs/src/data/Y.tab rename to doc/Archive/src/data/Y.tab diff --git a/doc/OnlineDocs/src/data/Z.tab b/doc/Archive/src/data/Z.tab similarity index 100% rename from doc/OnlineDocs/src/data/Z.tab rename to doc/Archive/src/data/Z.tab diff --git a/doc/OnlineDocs/src/data/data_managers.txt b/doc/Archive/src/data/data_managers.txt similarity index 100% rename from doc/OnlineDocs/src/data/data_managers.txt rename to doc/Archive/src/data/data_managers.txt diff --git a/doc/OnlineDocs/src/data/diet.dat b/doc/Archive/src/data/diet.dat similarity index 100% rename from doc/OnlineDocs/src/data/diet.dat rename to doc/Archive/src/data/diet.dat diff --git a/doc/OnlineDocs/src/data/diet.sql b/doc/Archive/src/data/diet.sql similarity index 100% rename from doc/OnlineDocs/src/data/diet.sql rename to doc/Archive/src/data/diet.sql diff --git a/doc/OnlineDocs/src/data/diet.sqlite b/doc/Archive/src/data/diet.sqlite similarity index 100% rename from doc/OnlineDocs/src/data/diet.sqlite rename to doc/Archive/src/data/diet.sqlite diff --git a/doc/OnlineDocs/src/data/diet.sqlite.dat b/doc/Archive/src/data/diet.sqlite.dat similarity index 100% rename from doc/OnlineDocs/src/data/diet.sqlite.dat rename to doc/Archive/src/data/diet.sqlite.dat diff --git a/doc/OnlineDocs/src/data/diet1.py b/doc/Archive/src/data/diet1.py similarity index 100% rename from doc/OnlineDocs/src/data/diet1.py rename to doc/Archive/src/data/diet1.py diff --git a/doc/OnlineDocs/src/data/ex.dat b/doc/Archive/src/data/ex.dat similarity index 100% rename from doc/OnlineDocs/src/data/ex.dat rename to doc/Archive/src/data/ex.dat diff --git a/doc/OnlineDocs/src/data/ex.py b/doc/Archive/src/data/ex.py similarity index 100% rename from doc/OnlineDocs/src/data/ex.py rename to doc/Archive/src/data/ex.py diff --git a/doc/OnlineDocs/src/data/ex.txt b/doc/Archive/src/data/ex.txt similarity index 100% rename from doc/OnlineDocs/src/data/ex.txt rename to doc/Archive/src/data/ex.txt diff --git a/doc/OnlineDocs/src/data/ex1.dat b/doc/Archive/src/data/ex1.dat similarity index 100% rename from doc/OnlineDocs/src/data/ex1.dat rename to doc/Archive/src/data/ex1.dat diff --git a/doc/OnlineDocs/src/data/ex2.dat b/doc/Archive/src/data/ex2.dat similarity index 100% rename from doc/OnlineDocs/src/data/ex2.dat rename to doc/Archive/src/data/ex2.dat diff --git a/doc/OnlineDocs/src/data/import1.tab.dat b/doc/Archive/src/data/import1.tab.dat similarity index 100% rename from doc/OnlineDocs/src/data/import1.tab.dat rename to doc/Archive/src/data/import1.tab.dat diff --git a/doc/OnlineDocs/src/data/import1.tab.py b/doc/Archive/src/data/import1.tab.py similarity index 100% rename from doc/OnlineDocs/src/data/import1.tab.py rename to doc/Archive/src/data/import1.tab.py diff --git a/doc/OnlineDocs/src/data/import1.tab.txt b/doc/Archive/src/data/import1.tab.txt similarity index 100% rename from doc/OnlineDocs/src/data/import1.tab.txt rename to doc/Archive/src/data/import1.tab.txt diff --git a/doc/OnlineDocs/src/data/import2.tab.dat b/doc/Archive/src/data/import2.tab.dat similarity index 100% rename from doc/OnlineDocs/src/data/import2.tab.dat rename to doc/Archive/src/data/import2.tab.dat diff --git a/doc/OnlineDocs/src/data/import2.tab.py b/doc/Archive/src/data/import2.tab.py similarity index 100% rename from doc/OnlineDocs/src/data/import2.tab.py rename to doc/Archive/src/data/import2.tab.py diff --git a/doc/OnlineDocs/src/data/import2.tab.txt b/doc/Archive/src/data/import2.tab.txt similarity index 100% rename from doc/OnlineDocs/src/data/import2.tab.txt rename to doc/Archive/src/data/import2.tab.txt diff --git a/doc/OnlineDocs/src/data/import3.tab.dat b/doc/Archive/src/data/import3.tab.dat similarity index 100% rename from doc/OnlineDocs/src/data/import3.tab.dat rename to doc/Archive/src/data/import3.tab.dat diff --git a/doc/OnlineDocs/src/data/import3.tab.py b/doc/Archive/src/data/import3.tab.py similarity index 100% rename from doc/OnlineDocs/src/data/import3.tab.py rename to doc/Archive/src/data/import3.tab.py diff --git a/doc/OnlineDocs/src/data/import3.tab.txt b/doc/Archive/src/data/import3.tab.txt similarity index 100% rename from doc/OnlineDocs/src/data/import3.tab.txt rename to doc/Archive/src/data/import3.tab.txt diff --git a/doc/OnlineDocs/src/data/import4.tab.dat b/doc/Archive/src/data/import4.tab.dat similarity index 100% rename from doc/OnlineDocs/src/data/import4.tab.dat rename to doc/Archive/src/data/import4.tab.dat diff --git a/doc/OnlineDocs/src/data/import4.tab.py b/doc/Archive/src/data/import4.tab.py similarity index 100% rename from doc/OnlineDocs/src/data/import4.tab.py rename to doc/Archive/src/data/import4.tab.py diff --git a/doc/OnlineDocs/src/data/import4.tab.txt b/doc/Archive/src/data/import4.tab.txt similarity index 100% rename from doc/OnlineDocs/src/data/import4.tab.txt rename to doc/Archive/src/data/import4.tab.txt diff --git a/doc/OnlineDocs/src/data/import5.tab.dat b/doc/Archive/src/data/import5.tab.dat similarity index 100% rename from doc/OnlineDocs/src/data/import5.tab.dat rename to doc/Archive/src/data/import5.tab.dat diff --git a/doc/OnlineDocs/src/data/import5.tab.py b/doc/Archive/src/data/import5.tab.py similarity index 100% rename from doc/OnlineDocs/src/data/import5.tab.py rename to doc/Archive/src/data/import5.tab.py diff --git a/doc/OnlineDocs/src/data/import5.tab.txt b/doc/Archive/src/data/import5.tab.txt similarity index 100% rename from doc/OnlineDocs/src/data/import5.tab.txt rename to doc/Archive/src/data/import5.tab.txt diff --git a/doc/OnlineDocs/src/data/import6.tab.dat b/doc/Archive/src/data/import6.tab.dat similarity index 100% rename from doc/OnlineDocs/src/data/import6.tab.dat rename to doc/Archive/src/data/import6.tab.dat diff --git a/doc/OnlineDocs/src/data/import6.tab.py b/doc/Archive/src/data/import6.tab.py similarity index 100% rename from doc/OnlineDocs/src/data/import6.tab.py rename to doc/Archive/src/data/import6.tab.py diff --git a/doc/OnlineDocs/src/data/import6.tab.txt b/doc/Archive/src/data/import6.tab.txt similarity index 100% rename from doc/OnlineDocs/src/data/import6.tab.txt rename to doc/Archive/src/data/import6.tab.txt diff --git a/doc/OnlineDocs/src/data/import7.tab.dat b/doc/Archive/src/data/import7.tab.dat similarity index 100% rename from doc/OnlineDocs/src/data/import7.tab.dat rename to doc/Archive/src/data/import7.tab.dat diff --git a/doc/OnlineDocs/src/data/import7.tab.py b/doc/Archive/src/data/import7.tab.py similarity index 100% rename from doc/OnlineDocs/src/data/import7.tab.py rename to doc/Archive/src/data/import7.tab.py diff --git a/doc/OnlineDocs/src/data/import7.tab.txt b/doc/Archive/src/data/import7.tab.txt similarity index 100% rename from doc/OnlineDocs/src/data/import7.tab.txt rename to doc/Archive/src/data/import7.tab.txt diff --git a/doc/OnlineDocs/src/data/import8.tab.dat b/doc/Archive/src/data/import8.tab.dat similarity index 100% rename from doc/OnlineDocs/src/data/import8.tab.dat rename to doc/Archive/src/data/import8.tab.dat diff --git a/doc/OnlineDocs/src/data/import8.tab.py b/doc/Archive/src/data/import8.tab.py similarity index 100% rename from doc/OnlineDocs/src/data/import8.tab.py rename to doc/Archive/src/data/import8.tab.py diff --git a/doc/OnlineDocs/src/data/import8.tab.txt b/doc/Archive/src/data/import8.tab.txt similarity index 100% rename from doc/OnlineDocs/src/data/import8.tab.txt rename to doc/Archive/src/data/import8.tab.txt diff --git a/doc/OnlineDocs/src/data/namespace1.dat b/doc/Archive/src/data/namespace1.dat similarity index 100% rename from doc/OnlineDocs/src/data/namespace1.dat rename to doc/Archive/src/data/namespace1.dat diff --git a/doc/OnlineDocs/src/data/param1.dat b/doc/Archive/src/data/param1.dat similarity index 100% rename from doc/OnlineDocs/src/data/param1.dat rename to doc/Archive/src/data/param1.dat diff --git a/doc/OnlineDocs/src/data/param1.py b/doc/Archive/src/data/param1.py similarity index 100% rename from doc/OnlineDocs/src/data/param1.py rename to doc/Archive/src/data/param1.py diff --git a/doc/OnlineDocs/src/data/param1.txt b/doc/Archive/src/data/param1.txt similarity index 100% rename from doc/OnlineDocs/src/data/param1.txt rename to doc/Archive/src/data/param1.txt diff --git a/doc/OnlineDocs/src/data/param2.dat b/doc/Archive/src/data/param2.dat similarity index 100% rename from doc/OnlineDocs/src/data/param2.dat rename to doc/Archive/src/data/param2.dat diff --git a/doc/OnlineDocs/src/data/param2.py b/doc/Archive/src/data/param2.py similarity index 100% rename from doc/OnlineDocs/src/data/param2.py rename to doc/Archive/src/data/param2.py diff --git a/doc/OnlineDocs/src/data/param2.txt b/doc/Archive/src/data/param2.txt similarity index 100% rename from doc/OnlineDocs/src/data/param2.txt rename to doc/Archive/src/data/param2.txt diff --git a/doc/OnlineDocs/src/data/param2a.dat b/doc/Archive/src/data/param2a.dat similarity index 100% rename from doc/OnlineDocs/src/data/param2a.dat rename to doc/Archive/src/data/param2a.dat diff --git a/doc/OnlineDocs/src/data/param2a.py b/doc/Archive/src/data/param2a.py similarity index 100% rename from doc/OnlineDocs/src/data/param2a.py rename to doc/Archive/src/data/param2a.py diff --git a/doc/OnlineDocs/src/data/param2a.txt b/doc/Archive/src/data/param2a.txt similarity index 100% rename from doc/OnlineDocs/src/data/param2a.txt rename to doc/Archive/src/data/param2a.txt diff --git a/doc/OnlineDocs/src/data/param3.dat b/doc/Archive/src/data/param3.dat similarity index 100% rename from doc/OnlineDocs/src/data/param3.dat rename to doc/Archive/src/data/param3.dat diff --git a/doc/OnlineDocs/src/data/param3.py b/doc/Archive/src/data/param3.py similarity index 100% rename from doc/OnlineDocs/src/data/param3.py rename to doc/Archive/src/data/param3.py diff --git a/doc/OnlineDocs/src/data/param3.txt b/doc/Archive/src/data/param3.txt similarity index 100% rename from doc/OnlineDocs/src/data/param3.txt rename to doc/Archive/src/data/param3.txt diff --git a/doc/OnlineDocs/src/data/param3a.dat b/doc/Archive/src/data/param3a.dat similarity index 100% rename from doc/OnlineDocs/src/data/param3a.dat rename to doc/Archive/src/data/param3a.dat diff --git a/doc/OnlineDocs/src/data/param3a.py b/doc/Archive/src/data/param3a.py similarity index 100% rename from doc/OnlineDocs/src/data/param3a.py rename to doc/Archive/src/data/param3a.py diff --git a/doc/OnlineDocs/src/data/param3a.txt b/doc/Archive/src/data/param3a.txt similarity index 100% rename from doc/OnlineDocs/src/data/param3a.txt rename to doc/Archive/src/data/param3a.txt diff --git a/doc/OnlineDocs/src/data/param3b.dat b/doc/Archive/src/data/param3b.dat similarity index 100% rename from doc/OnlineDocs/src/data/param3b.dat rename to doc/Archive/src/data/param3b.dat diff --git a/doc/OnlineDocs/src/data/param3b.py b/doc/Archive/src/data/param3b.py similarity index 100% rename from doc/OnlineDocs/src/data/param3b.py rename to doc/Archive/src/data/param3b.py diff --git a/doc/OnlineDocs/src/data/param3b.txt b/doc/Archive/src/data/param3b.txt similarity index 100% rename from doc/OnlineDocs/src/data/param3b.txt rename to doc/Archive/src/data/param3b.txt diff --git a/doc/OnlineDocs/src/data/param3c.dat b/doc/Archive/src/data/param3c.dat similarity index 100% rename from doc/OnlineDocs/src/data/param3c.dat rename to doc/Archive/src/data/param3c.dat diff --git a/doc/OnlineDocs/src/data/param3c.py b/doc/Archive/src/data/param3c.py similarity index 100% rename from doc/OnlineDocs/src/data/param3c.py rename to doc/Archive/src/data/param3c.py diff --git a/doc/OnlineDocs/src/data/param3c.txt b/doc/Archive/src/data/param3c.txt similarity index 100% rename from doc/OnlineDocs/src/data/param3c.txt rename to doc/Archive/src/data/param3c.txt diff --git a/doc/OnlineDocs/src/data/param4.dat b/doc/Archive/src/data/param4.dat similarity index 100% rename from doc/OnlineDocs/src/data/param4.dat rename to doc/Archive/src/data/param4.dat diff --git a/doc/OnlineDocs/src/data/param4.py b/doc/Archive/src/data/param4.py similarity index 100% rename from doc/OnlineDocs/src/data/param4.py rename to doc/Archive/src/data/param4.py diff --git a/doc/OnlineDocs/src/data/param4.txt b/doc/Archive/src/data/param4.txt similarity index 100% rename from doc/OnlineDocs/src/data/param4.txt rename to doc/Archive/src/data/param4.txt diff --git a/doc/OnlineDocs/src/data/param5.dat b/doc/Archive/src/data/param5.dat similarity index 100% rename from doc/OnlineDocs/src/data/param5.dat rename to doc/Archive/src/data/param5.dat diff --git a/doc/OnlineDocs/src/data/param5.py b/doc/Archive/src/data/param5.py similarity index 100% rename from doc/OnlineDocs/src/data/param5.py rename to doc/Archive/src/data/param5.py diff --git a/doc/OnlineDocs/src/data/param5.txt b/doc/Archive/src/data/param5.txt similarity index 100% rename from doc/OnlineDocs/src/data/param5.txt rename to doc/Archive/src/data/param5.txt diff --git a/doc/OnlineDocs/src/data/param5a.dat b/doc/Archive/src/data/param5a.dat similarity index 100% rename from doc/OnlineDocs/src/data/param5a.dat rename to doc/Archive/src/data/param5a.dat diff --git a/doc/OnlineDocs/src/data/param5a.py b/doc/Archive/src/data/param5a.py similarity index 100% rename from doc/OnlineDocs/src/data/param5a.py rename to doc/Archive/src/data/param5a.py diff --git a/doc/OnlineDocs/src/data/param5a.txt b/doc/Archive/src/data/param5a.txt similarity index 100% rename from doc/OnlineDocs/src/data/param5a.txt rename to doc/Archive/src/data/param5a.txt diff --git a/doc/OnlineDocs/src/data/param6.dat b/doc/Archive/src/data/param6.dat similarity index 100% rename from doc/OnlineDocs/src/data/param6.dat rename to doc/Archive/src/data/param6.dat diff --git a/doc/OnlineDocs/src/data/param6.py b/doc/Archive/src/data/param6.py similarity index 100% rename from doc/OnlineDocs/src/data/param6.py rename to doc/Archive/src/data/param6.py diff --git a/doc/OnlineDocs/src/data/param6.txt b/doc/Archive/src/data/param6.txt similarity index 100% rename from doc/OnlineDocs/src/data/param6.txt rename to doc/Archive/src/data/param6.txt diff --git a/doc/OnlineDocs/src/data/param6a.dat b/doc/Archive/src/data/param6a.dat similarity index 100% rename from doc/OnlineDocs/src/data/param6a.dat rename to doc/Archive/src/data/param6a.dat diff --git a/doc/OnlineDocs/src/data/param6a.py b/doc/Archive/src/data/param6a.py similarity index 100% rename from doc/OnlineDocs/src/data/param6a.py rename to doc/Archive/src/data/param6a.py diff --git a/doc/OnlineDocs/src/data/param6a.txt b/doc/Archive/src/data/param6a.txt similarity index 100% rename from doc/OnlineDocs/src/data/param6a.txt rename to doc/Archive/src/data/param6a.txt diff --git a/doc/OnlineDocs/src/data/param7a.dat b/doc/Archive/src/data/param7a.dat similarity index 100% rename from doc/OnlineDocs/src/data/param7a.dat rename to doc/Archive/src/data/param7a.dat diff --git a/doc/OnlineDocs/src/data/param7a.py b/doc/Archive/src/data/param7a.py similarity index 100% rename from doc/OnlineDocs/src/data/param7a.py rename to doc/Archive/src/data/param7a.py diff --git a/doc/OnlineDocs/src/data/param7a.txt b/doc/Archive/src/data/param7a.txt similarity index 100% rename from doc/OnlineDocs/src/data/param7a.txt rename to doc/Archive/src/data/param7a.txt diff --git a/doc/OnlineDocs/src/data/param7b.dat b/doc/Archive/src/data/param7b.dat similarity index 100% rename from doc/OnlineDocs/src/data/param7b.dat rename to doc/Archive/src/data/param7b.dat diff --git a/doc/OnlineDocs/src/data/param7b.py b/doc/Archive/src/data/param7b.py similarity index 100% rename from doc/OnlineDocs/src/data/param7b.py rename to doc/Archive/src/data/param7b.py diff --git a/doc/OnlineDocs/src/data/param7b.txt b/doc/Archive/src/data/param7b.txt similarity index 100% rename from doc/OnlineDocs/src/data/param7b.txt rename to doc/Archive/src/data/param7b.txt diff --git a/doc/OnlineDocs/src/data/param8a.dat b/doc/Archive/src/data/param8a.dat similarity index 100% rename from doc/OnlineDocs/src/data/param8a.dat rename to doc/Archive/src/data/param8a.dat diff --git a/doc/OnlineDocs/src/data/param8a.py b/doc/Archive/src/data/param8a.py similarity index 100% rename from doc/OnlineDocs/src/data/param8a.py rename to doc/Archive/src/data/param8a.py diff --git a/doc/OnlineDocs/src/data/param8a.txt b/doc/Archive/src/data/param8a.txt similarity index 100% rename from doc/OnlineDocs/src/data/param8a.txt rename to doc/Archive/src/data/param8a.txt diff --git a/doc/OnlineDocs/src/data/pyomo.diet1.sh b/doc/Archive/src/data/pyomo.diet1.sh similarity index 100% rename from doc/OnlineDocs/src/data/pyomo.diet1.sh rename to doc/Archive/src/data/pyomo.diet1.sh diff --git a/doc/OnlineDocs/src/data/pyomo.diet1.txt b/doc/Archive/src/data/pyomo.diet1.txt similarity index 100% rename from doc/OnlineDocs/src/data/pyomo.diet1.txt rename to doc/Archive/src/data/pyomo.diet1.txt diff --git a/doc/OnlineDocs/src/data/pyomo.diet2.sh b/doc/Archive/src/data/pyomo.diet2.sh similarity index 100% rename from doc/OnlineDocs/src/data/pyomo.diet2.sh rename to doc/Archive/src/data/pyomo.diet2.sh diff --git a/doc/OnlineDocs/src/data/pyomo.diet2.txt b/doc/Archive/src/data/pyomo.diet2.txt similarity index 100% rename from doc/OnlineDocs/src/data/pyomo.diet2.txt rename to doc/Archive/src/data/pyomo.diet2.txt diff --git a/doc/OnlineDocs/src/data/set1.dat b/doc/Archive/src/data/set1.dat similarity index 100% rename from doc/OnlineDocs/src/data/set1.dat rename to doc/Archive/src/data/set1.dat diff --git a/doc/OnlineDocs/src/data/set1.py b/doc/Archive/src/data/set1.py similarity index 100% rename from doc/OnlineDocs/src/data/set1.py rename to doc/Archive/src/data/set1.py diff --git a/doc/OnlineDocs/src/data/set1.txt b/doc/Archive/src/data/set1.txt similarity index 100% rename from doc/OnlineDocs/src/data/set1.txt rename to doc/Archive/src/data/set1.txt diff --git a/doc/OnlineDocs/src/data/set2.dat b/doc/Archive/src/data/set2.dat similarity index 100% rename from doc/OnlineDocs/src/data/set2.dat rename to doc/Archive/src/data/set2.dat diff --git a/doc/OnlineDocs/src/data/set2.py b/doc/Archive/src/data/set2.py similarity index 100% rename from doc/OnlineDocs/src/data/set2.py rename to doc/Archive/src/data/set2.py diff --git a/doc/OnlineDocs/src/data/set2.txt b/doc/Archive/src/data/set2.txt similarity index 100% rename from doc/OnlineDocs/src/data/set2.txt rename to doc/Archive/src/data/set2.txt diff --git a/doc/OnlineDocs/src/data/set2a.dat b/doc/Archive/src/data/set2a.dat similarity index 100% rename from doc/OnlineDocs/src/data/set2a.dat rename to doc/Archive/src/data/set2a.dat diff --git a/doc/OnlineDocs/src/data/set2a.py b/doc/Archive/src/data/set2a.py similarity index 100% rename from doc/OnlineDocs/src/data/set2a.py rename to doc/Archive/src/data/set2a.py diff --git a/doc/OnlineDocs/src/data/set2a.txt b/doc/Archive/src/data/set2a.txt similarity index 100% rename from doc/OnlineDocs/src/data/set2a.txt rename to doc/Archive/src/data/set2a.txt diff --git a/doc/OnlineDocs/src/data/set3.dat b/doc/Archive/src/data/set3.dat similarity index 100% rename from doc/OnlineDocs/src/data/set3.dat rename to doc/Archive/src/data/set3.dat diff --git a/doc/OnlineDocs/src/data/set3.py b/doc/Archive/src/data/set3.py similarity index 100% rename from doc/OnlineDocs/src/data/set3.py rename to doc/Archive/src/data/set3.py diff --git a/doc/OnlineDocs/src/data/set3.txt b/doc/Archive/src/data/set3.txt similarity index 100% rename from doc/OnlineDocs/src/data/set3.txt rename to doc/Archive/src/data/set3.txt diff --git a/doc/OnlineDocs/src/data/set4.dat b/doc/Archive/src/data/set4.dat similarity index 100% rename from doc/OnlineDocs/src/data/set4.dat rename to doc/Archive/src/data/set4.dat diff --git a/doc/OnlineDocs/src/data/set4.py b/doc/Archive/src/data/set4.py similarity index 100% rename from doc/OnlineDocs/src/data/set4.py rename to doc/Archive/src/data/set4.py diff --git a/doc/OnlineDocs/src/data/set4.txt b/doc/Archive/src/data/set4.txt similarity index 100% rename from doc/OnlineDocs/src/data/set4.txt rename to doc/Archive/src/data/set4.txt diff --git a/doc/OnlineDocs/src/data/set5.dat b/doc/Archive/src/data/set5.dat similarity index 100% rename from doc/OnlineDocs/src/data/set5.dat rename to doc/Archive/src/data/set5.dat diff --git a/doc/OnlineDocs/src/data/set5.py b/doc/Archive/src/data/set5.py similarity index 100% rename from doc/OnlineDocs/src/data/set5.py rename to doc/Archive/src/data/set5.py diff --git a/doc/OnlineDocs/src/data/set5.txt b/doc/Archive/src/data/set5.txt similarity index 100% rename from doc/OnlineDocs/src/data/set5.txt rename to doc/Archive/src/data/set5.txt diff --git a/doc/OnlineDocs/src/data/table0.dat b/doc/Archive/src/data/table0.dat similarity index 100% rename from doc/OnlineDocs/src/data/table0.dat rename to doc/Archive/src/data/table0.dat diff --git a/doc/OnlineDocs/src/data/table0.py b/doc/Archive/src/data/table0.py similarity index 100% rename from doc/OnlineDocs/src/data/table0.py rename to doc/Archive/src/data/table0.py diff --git a/doc/OnlineDocs/src/data/table0.txt b/doc/Archive/src/data/table0.txt similarity index 100% rename from doc/OnlineDocs/src/data/table0.txt rename to doc/Archive/src/data/table0.txt diff --git a/doc/OnlineDocs/src/data/table0.ul.dat b/doc/Archive/src/data/table0.ul.dat similarity index 100% rename from doc/OnlineDocs/src/data/table0.ul.dat rename to doc/Archive/src/data/table0.ul.dat diff --git a/doc/OnlineDocs/src/data/table0.ul.py b/doc/Archive/src/data/table0.ul.py similarity index 100% rename from doc/OnlineDocs/src/data/table0.ul.py rename to doc/Archive/src/data/table0.ul.py diff --git a/doc/OnlineDocs/src/data/table0.ul.txt b/doc/Archive/src/data/table0.ul.txt similarity index 100% rename from doc/OnlineDocs/src/data/table0.ul.txt rename to doc/Archive/src/data/table0.ul.txt diff --git a/doc/OnlineDocs/src/data/table1.dat b/doc/Archive/src/data/table1.dat similarity index 100% rename from doc/OnlineDocs/src/data/table1.dat rename to doc/Archive/src/data/table1.dat diff --git a/doc/OnlineDocs/src/data/table1.py b/doc/Archive/src/data/table1.py similarity index 100% rename from doc/OnlineDocs/src/data/table1.py rename to doc/Archive/src/data/table1.py diff --git a/doc/OnlineDocs/src/data/table1.txt b/doc/Archive/src/data/table1.txt similarity index 100% rename from doc/OnlineDocs/src/data/table1.txt rename to doc/Archive/src/data/table1.txt diff --git a/doc/OnlineDocs/src/data/table2.dat b/doc/Archive/src/data/table2.dat similarity index 100% rename from doc/OnlineDocs/src/data/table2.dat rename to doc/Archive/src/data/table2.dat diff --git a/doc/OnlineDocs/src/data/table2.py b/doc/Archive/src/data/table2.py similarity index 100% rename from doc/OnlineDocs/src/data/table2.py rename to doc/Archive/src/data/table2.py diff --git a/doc/OnlineDocs/src/data/table2.txt b/doc/Archive/src/data/table2.txt similarity index 100% rename from doc/OnlineDocs/src/data/table2.txt rename to doc/Archive/src/data/table2.txt diff --git a/doc/OnlineDocs/src/data/table3.dat b/doc/Archive/src/data/table3.dat similarity index 100% rename from doc/OnlineDocs/src/data/table3.dat rename to doc/Archive/src/data/table3.dat diff --git a/doc/OnlineDocs/src/data/table3.py b/doc/Archive/src/data/table3.py similarity index 100% rename from doc/OnlineDocs/src/data/table3.py rename to doc/Archive/src/data/table3.py diff --git a/doc/OnlineDocs/src/data/table3.txt b/doc/Archive/src/data/table3.txt similarity index 100% rename from doc/OnlineDocs/src/data/table3.txt rename to doc/Archive/src/data/table3.txt diff --git a/doc/OnlineDocs/src/data/table3.ul.dat b/doc/Archive/src/data/table3.ul.dat similarity index 100% rename from doc/OnlineDocs/src/data/table3.ul.dat rename to doc/Archive/src/data/table3.ul.dat diff --git a/doc/OnlineDocs/src/data/table3.ul.py b/doc/Archive/src/data/table3.ul.py similarity index 100% rename from doc/OnlineDocs/src/data/table3.ul.py rename to doc/Archive/src/data/table3.ul.py diff --git a/doc/OnlineDocs/src/data/table3.ul.txt b/doc/Archive/src/data/table3.ul.txt similarity index 100% rename from doc/OnlineDocs/src/data/table3.ul.txt rename to doc/Archive/src/data/table3.ul.txt diff --git a/doc/OnlineDocs/src/data/table4.dat b/doc/Archive/src/data/table4.dat similarity index 100% rename from doc/OnlineDocs/src/data/table4.dat rename to doc/Archive/src/data/table4.dat diff --git a/doc/OnlineDocs/src/data/table4.py b/doc/Archive/src/data/table4.py similarity index 100% rename from doc/OnlineDocs/src/data/table4.py rename to doc/Archive/src/data/table4.py diff --git a/doc/OnlineDocs/src/data/table4.txt b/doc/Archive/src/data/table4.txt similarity index 100% rename from doc/OnlineDocs/src/data/table4.txt rename to doc/Archive/src/data/table4.txt diff --git a/doc/OnlineDocs/src/data/table4.ul.dat b/doc/Archive/src/data/table4.ul.dat similarity index 100% rename from doc/OnlineDocs/src/data/table4.ul.dat rename to doc/Archive/src/data/table4.ul.dat diff --git a/doc/OnlineDocs/src/data/table4.ul.py b/doc/Archive/src/data/table4.ul.py similarity index 100% rename from doc/OnlineDocs/src/data/table4.ul.py rename to doc/Archive/src/data/table4.ul.py diff --git a/doc/OnlineDocs/src/data/table4.ul.txt b/doc/Archive/src/data/table4.ul.txt similarity index 100% rename from doc/OnlineDocs/src/data/table4.ul.txt rename to doc/Archive/src/data/table4.ul.txt diff --git a/doc/OnlineDocs/src/data/table5.dat b/doc/Archive/src/data/table5.dat similarity index 100% rename from doc/OnlineDocs/src/data/table5.dat rename to doc/Archive/src/data/table5.dat diff --git a/doc/OnlineDocs/src/data/table5.py b/doc/Archive/src/data/table5.py similarity index 100% rename from doc/OnlineDocs/src/data/table5.py rename to doc/Archive/src/data/table5.py diff --git a/doc/OnlineDocs/src/data/table5.txt b/doc/Archive/src/data/table5.txt similarity index 100% rename from doc/OnlineDocs/src/data/table5.txt rename to doc/Archive/src/data/table5.txt diff --git a/doc/OnlineDocs/src/data/table6.dat b/doc/Archive/src/data/table6.dat similarity index 100% rename from doc/OnlineDocs/src/data/table6.dat rename to doc/Archive/src/data/table6.dat diff --git a/doc/OnlineDocs/src/data/table6.py b/doc/Archive/src/data/table6.py similarity index 100% rename from doc/OnlineDocs/src/data/table6.py rename to doc/Archive/src/data/table6.py diff --git a/doc/OnlineDocs/src/data/table6.txt b/doc/Archive/src/data/table6.txt similarity index 100% rename from doc/OnlineDocs/src/data/table6.txt rename to doc/Archive/src/data/table6.txt diff --git a/doc/OnlineDocs/src/data/table7.dat b/doc/Archive/src/data/table7.dat similarity index 100% rename from doc/OnlineDocs/src/data/table7.dat rename to doc/Archive/src/data/table7.dat diff --git a/doc/OnlineDocs/src/data/table7.py b/doc/Archive/src/data/table7.py similarity index 100% rename from doc/OnlineDocs/src/data/table7.py rename to doc/Archive/src/data/table7.py diff --git a/doc/OnlineDocs/src/data/table7.txt b/doc/Archive/src/data/table7.txt similarity index 100% rename from doc/OnlineDocs/src/data/table7.txt rename to doc/Archive/src/data/table7.txt diff --git a/doc/OnlineDocs/src/dataportal/A.tab b/doc/Archive/src/dataportal/A.tab similarity index 100% rename from doc/OnlineDocs/src/dataportal/A.tab rename to doc/Archive/src/dataportal/A.tab diff --git a/doc/OnlineDocs/src/dataportal/C.tab b/doc/Archive/src/dataportal/C.tab similarity index 100% rename from doc/OnlineDocs/src/dataportal/C.tab rename to doc/Archive/src/dataportal/C.tab diff --git a/doc/OnlineDocs/src/dataportal/D.tab b/doc/Archive/src/dataportal/D.tab similarity index 100% rename from doc/OnlineDocs/src/dataportal/D.tab rename to doc/Archive/src/dataportal/D.tab diff --git a/doc/OnlineDocs/src/dataportal/PP.csv b/doc/Archive/src/dataportal/PP.csv similarity index 100% rename from doc/OnlineDocs/src/dataportal/PP.csv rename to doc/Archive/src/dataportal/PP.csv diff --git a/doc/OnlineDocs/src/dataportal/PP.json b/doc/Archive/src/dataportal/PP.json similarity index 100% rename from doc/OnlineDocs/src/dataportal/PP.json rename to doc/Archive/src/dataportal/PP.json diff --git a/doc/OnlineDocs/src/dataportal/PP.sqlite b/doc/Archive/src/dataportal/PP.sqlite similarity index 100% rename from doc/OnlineDocs/src/dataportal/PP.sqlite rename to doc/Archive/src/dataportal/PP.sqlite diff --git a/doc/OnlineDocs/src/dataportal/PP.tab b/doc/Archive/src/dataportal/PP.tab similarity index 100% rename from doc/OnlineDocs/src/dataportal/PP.tab rename to doc/Archive/src/dataportal/PP.tab diff --git a/doc/OnlineDocs/src/dataportal/PP.xml b/doc/Archive/src/dataportal/PP.xml similarity index 100% rename from doc/OnlineDocs/src/dataportal/PP.xml rename to doc/Archive/src/dataportal/PP.xml diff --git a/doc/OnlineDocs/src/dataportal/PP.yaml b/doc/Archive/src/dataportal/PP.yaml similarity index 100% rename from doc/OnlineDocs/src/dataportal/PP.yaml rename to doc/Archive/src/dataportal/PP.yaml diff --git a/doc/OnlineDocs/src/dataportal/PP_sqlite.py b/doc/Archive/src/dataportal/PP_sqlite.py similarity index 100% rename from doc/OnlineDocs/src/dataportal/PP_sqlite.py rename to doc/Archive/src/dataportal/PP_sqlite.py diff --git a/doc/OnlineDocs/src/dataportal/Pyomo_mysql b/doc/Archive/src/dataportal/Pyomo_mysql similarity index 100% rename from doc/OnlineDocs/src/dataportal/Pyomo_mysql rename to doc/Archive/src/dataportal/Pyomo_mysql diff --git a/doc/OnlineDocs/src/dataportal/S.tab b/doc/Archive/src/dataportal/S.tab similarity index 100% rename from doc/OnlineDocs/src/dataportal/S.tab rename to doc/Archive/src/dataportal/S.tab diff --git a/doc/OnlineDocs/src/dataportal/T.json b/doc/Archive/src/dataportal/T.json similarity index 100% rename from doc/OnlineDocs/src/dataportal/T.json rename to doc/Archive/src/dataportal/T.json diff --git a/doc/OnlineDocs/src/dataportal/T.yaml b/doc/Archive/src/dataportal/T.yaml similarity index 100% rename from doc/OnlineDocs/src/dataportal/T.yaml rename to doc/Archive/src/dataportal/T.yaml diff --git a/doc/OnlineDocs/src/dataportal/U.tab b/doc/Archive/src/dataportal/U.tab similarity index 100% rename from doc/OnlineDocs/src/dataportal/U.tab rename to doc/Archive/src/dataportal/U.tab diff --git a/doc/OnlineDocs/src/dataportal/XW.tab b/doc/Archive/src/dataportal/XW.tab similarity index 100% rename from doc/OnlineDocs/src/dataportal/XW.tab rename to doc/Archive/src/dataportal/XW.tab diff --git a/doc/OnlineDocs/src/dataportal/Y.tab b/doc/Archive/src/dataportal/Y.tab similarity index 100% rename from doc/OnlineDocs/src/dataportal/Y.tab rename to doc/Archive/src/dataportal/Y.tab diff --git a/doc/OnlineDocs/src/dataportal/Z.tab b/doc/Archive/src/dataportal/Z.tab similarity index 100% rename from doc/OnlineDocs/src/dataportal/Z.tab rename to doc/Archive/src/dataportal/Z.tab diff --git a/doc/OnlineDocs/src/dataportal/dataportal_tab.py b/doc/Archive/src/dataportal/dataportal_tab.py similarity index 100% rename from doc/OnlineDocs/src/dataportal/dataportal_tab.py rename to doc/Archive/src/dataportal/dataportal_tab.py diff --git a/doc/OnlineDocs/src/dataportal/dataportal_tab.txt b/doc/Archive/src/dataportal/dataportal_tab.txt similarity index 100% rename from doc/OnlineDocs/src/dataportal/dataportal_tab.txt rename to doc/Archive/src/dataportal/dataportal_tab.txt diff --git a/doc/OnlineDocs/src/dataportal/excel.xls b/doc/Archive/src/dataportal/excel.xls similarity index 100% rename from doc/OnlineDocs/src/dataportal/excel.xls rename to doc/Archive/src/dataportal/excel.xls diff --git a/doc/OnlineDocs/src/dataportal/param_initialization.py b/doc/Archive/src/dataportal/param_initialization.py similarity index 100% rename from doc/OnlineDocs/src/dataportal/param_initialization.py rename to doc/Archive/src/dataportal/param_initialization.py diff --git a/doc/OnlineDocs/src/dataportal/param_initialization.txt b/doc/Archive/src/dataportal/param_initialization.txt similarity index 100% rename from doc/OnlineDocs/src/dataportal/param_initialization.txt rename to doc/Archive/src/dataportal/param_initialization.txt diff --git a/doc/OnlineDocs/src/dataportal/set_initialization.py b/doc/Archive/src/dataportal/set_initialization.py similarity index 100% rename from doc/OnlineDocs/src/dataportal/set_initialization.py rename to doc/Archive/src/dataportal/set_initialization.py diff --git a/doc/OnlineDocs/src/dataportal/set_initialization.txt b/doc/Archive/src/dataportal/set_initialization.txt similarity index 100% rename from doc/OnlineDocs/src/dataportal/set_initialization.txt rename to doc/Archive/src/dataportal/set_initialization.txt diff --git a/doc/OnlineDocs/src/expr/design.py b/doc/Archive/src/expr/design.py similarity index 100% rename from doc/OnlineDocs/src/expr/design.py rename to doc/Archive/src/expr/design.py diff --git a/doc/OnlineDocs/src/expr/design.txt b/doc/Archive/src/expr/design.txt similarity index 100% rename from doc/OnlineDocs/src/expr/design.txt rename to doc/Archive/src/expr/design.txt diff --git a/doc/OnlineDocs/src/expr/index.py b/doc/Archive/src/expr/index.py similarity index 100% rename from doc/OnlineDocs/src/expr/index.py rename to doc/Archive/src/expr/index.py diff --git a/doc/OnlineDocs/src/expr/index.txt b/doc/Archive/src/expr/index.txt similarity index 100% rename from doc/OnlineDocs/src/expr/index.txt rename to doc/Archive/src/expr/index.txt diff --git a/doc/OnlineDocs/src/expr/managing.py b/doc/Archive/src/expr/managing.py similarity index 100% rename from doc/OnlineDocs/src/expr/managing.py rename to doc/Archive/src/expr/managing.py diff --git a/doc/OnlineDocs/src/expr/managing.txt b/doc/Archive/src/expr/managing.txt similarity index 100% rename from doc/OnlineDocs/src/expr/managing.txt rename to doc/Archive/src/expr/managing.txt diff --git a/doc/OnlineDocs/src/expr/overview.py b/doc/Archive/src/expr/overview.py similarity index 100% rename from doc/OnlineDocs/src/expr/overview.py rename to doc/Archive/src/expr/overview.py diff --git a/doc/OnlineDocs/src/expr/overview.txt b/doc/Archive/src/expr/overview.txt similarity index 100% rename from doc/OnlineDocs/src/expr/overview.txt rename to doc/Archive/src/expr/overview.txt diff --git a/doc/OnlineDocs/src/expr/performance.py b/doc/Archive/src/expr/performance.py similarity index 100% rename from doc/OnlineDocs/src/expr/performance.py rename to doc/Archive/src/expr/performance.py diff --git a/doc/OnlineDocs/src/expr/performance.txt b/doc/Archive/src/expr/performance.txt similarity index 100% rename from doc/OnlineDocs/src/expr/performance.txt rename to doc/Archive/src/expr/performance.txt diff --git a/doc/OnlineDocs/src/expr/quicksum.log b/doc/Archive/src/expr/quicksum.log similarity index 100% rename from doc/OnlineDocs/src/expr/quicksum.log rename to doc/Archive/src/expr/quicksum.log diff --git a/doc/OnlineDocs/src/expr/quicksum.py b/doc/Archive/src/expr/quicksum.py similarity index 100% rename from doc/OnlineDocs/src/expr/quicksum.py rename to doc/Archive/src/expr/quicksum.py diff --git a/doc/OnlineDocs/src/kernel/examples.sh b/doc/Archive/src/kernel/examples.sh similarity index 100% rename from doc/OnlineDocs/src/kernel/examples.sh rename to doc/Archive/src/kernel/examples.sh diff --git a/doc/OnlineDocs/src/kernel/examples.txt b/doc/Archive/src/kernel/examples.txt similarity index 100% rename from doc/OnlineDocs/src/kernel/examples.txt rename to doc/Archive/src/kernel/examples.txt diff --git a/doc/OnlineDocs/src/scripting/AbstractSuffixes.py b/doc/Archive/src/scripting/AbstractSuffixes.py similarity index 100% rename from doc/OnlineDocs/src/scripting/AbstractSuffixes.py rename to doc/Archive/src/scripting/AbstractSuffixes.py diff --git a/doc/OnlineDocs/src/scripting/Isinglebuild.py b/doc/Archive/src/scripting/Isinglebuild.py similarity index 100% rename from doc/OnlineDocs/src/scripting/Isinglebuild.py rename to doc/Archive/src/scripting/Isinglebuild.py diff --git a/doc/OnlineDocs/src/scripting/Isinglecomm.dat b/doc/Archive/src/scripting/Isinglecomm.dat similarity index 100% rename from doc/OnlineDocs/src/scripting/Isinglecomm.dat rename to doc/Archive/src/scripting/Isinglecomm.dat diff --git a/doc/OnlineDocs/src/scripting/NodesIn_init.py b/doc/Archive/src/scripting/NodesIn_init.py similarity index 100% rename from doc/OnlineDocs/src/scripting/NodesIn_init.py rename to doc/Archive/src/scripting/NodesIn_init.py diff --git a/doc/OnlineDocs/src/scripting/Z_init.py b/doc/Archive/src/scripting/Z_init.py similarity index 100% rename from doc/OnlineDocs/src/scripting/Z_init.py rename to doc/Archive/src/scripting/Z_init.py diff --git a/doc/OnlineDocs/src/scripting/abstract1.dat b/doc/Archive/src/scripting/abstract1.dat similarity index 100% rename from doc/OnlineDocs/src/scripting/abstract1.dat rename to doc/Archive/src/scripting/abstract1.dat diff --git a/doc/OnlineDocs/src/scripting/abstract2.dat b/doc/Archive/src/scripting/abstract2.dat similarity index 100% rename from doc/OnlineDocs/src/scripting/abstract2.dat rename to doc/Archive/src/scripting/abstract2.dat diff --git a/doc/OnlineDocs/src/scripting/abstract2.py b/doc/Archive/src/scripting/abstract2.py similarity index 100% rename from doc/OnlineDocs/src/scripting/abstract2.py rename to doc/Archive/src/scripting/abstract2.py diff --git a/doc/OnlineDocs/src/scripting/abstract2a.dat b/doc/Archive/src/scripting/abstract2a.dat similarity index 100% rename from doc/OnlineDocs/src/scripting/abstract2a.dat rename to doc/Archive/src/scripting/abstract2a.dat diff --git a/doc/OnlineDocs/src/scripting/abstract2piece.py b/doc/Archive/src/scripting/abstract2piece.py similarity index 100% rename from doc/OnlineDocs/src/scripting/abstract2piece.py rename to doc/Archive/src/scripting/abstract2piece.py diff --git a/doc/OnlineDocs/src/scripting/abstract2piecebuild.py b/doc/Archive/src/scripting/abstract2piecebuild.py similarity index 100% rename from doc/OnlineDocs/src/scripting/abstract2piecebuild.py rename to doc/Archive/src/scripting/abstract2piecebuild.py diff --git a/doc/OnlineDocs/src/scripting/block_iter_example.py b/doc/Archive/src/scripting/block_iter_example.py similarity index 100% rename from doc/OnlineDocs/src/scripting/block_iter_example.py rename to doc/Archive/src/scripting/block_iter_example.py diff --git a/doc/OnlineDocs/src/scripting/concrete1.py b/doc/Archive/src/scripting/concrete1.py similarity index 100% rename from doc/OnlineDocs/src/scripting/concrete1.py rename to doc/Archive/src/scripting/concrete1.py diff --git a/doc/OnlineDocs/src/scripting/doubleA.py b/doc/Archive/src/scripting/doubleA.py similarity index 100% rename from doc/OnlineDocs/src/scripting/doubleA.py rename to doc/Archive/src/scripting/doubleA.py diff --git a/doc/OnlineDocs/src/scripting/driveabs2.py b/doc/Archive/src/scripting/driveabs2.py similarity index 100% rename from doc/OnlineDocs/src/scripting/driveabs2.py rename to doc/Archive/src/scripting/driveabs2.py diff --git a/doc/OnlineDocs/src/scripting/driveconc1.py b/doc/Archive/src/scripting/driveconc1.py similarity index 100% rename from doc/OnlineDocs/src/scripting/driveconc1.py rename to doc/Archive/src/scripting/driveconc1.py diff --git a/doc/OnlineDocs/src/scripting/iterative1.py b/doc/Archive/src/scripting/iterative1.py similarity index 100% rename from doc/OnlineDocs/src/scripting/iterative1.py rename to doc/Archive/src/scripting/iterative1.py diff --git a/doc/OnlineDocs/src/scripting/iterative2.py b/doc/Archive/src/scripting/iterative2.py similarity index 100% rename from doc/OnlineDocs/src/scripting/iterative2.py rename to doc/Archive/src/scripting/iterative2.py diff --git a/doc/OnlineDocs/src/scripting/noiteration1.py b/doc/Archive/src/scripting/noiteration1.py similarity index 100% rename from doc/OnlineDocs/src/scripting/noiteration1.py rename to doc/Archive/src/scripting/noiteration1.py diff --git a/doc/OnlineDocs/src/scripting/parallel.py b/doc/Archive/src/scripting/parallel.py similarity index 100% rename from doc/OnlineDocs/src/scripting/parallel.py rename to doc/Archive/src/scripting/parallel.py diff --git a/doc/OnlineDocs/src/scripting/spy4Constraints.py b/doc/Archive/src/scripting/spy4Constraints.py similarity index 100% rename from doc/OnlineDocs/src/scripting/spy4Constraints.py rename to doc/Archive/src/scripting/spy4Constraints.py diff --git a/doc/OnlineDocs/src/scripting/spy4Expressions.py b/doc/Archive/src/scripting/spy4Expressions.py similarity index 100% rename from doc/OnlineDocs/src/scripting/spy4Expressions.py rename to doc/Archive/src/scripting/spy4Expressions.py diff --git a/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py b/doc/Archive/src/scripting/spy4PyomoCommand.py similarity index 100% rename from doc/OnlineDocs/src/scripting/spy4PyomoCommand.py rename to doc/Archive/src/scripting/spy4PyomoCommand.py diff --git a/doc/OnlineDocs/src/scripting/spy4Variables.py b/doc/Archive/src/scripting/spy4Variables.py similarity index 100% rename from doc/OnlineDocs/src/scripting/spy4Variables.py rename to doc/Archive/src/scripting/spy4Variables.py diff --git a/doc/OnlineDocs/src/scripting/spy4scripts.py b/doc/Archive/src/scripting/spy4scripts.py similarity index 100% rename from doc/OnlineDocs/src/scripting/spy4scripts.py rename to doc/Archive/src/scripting/spy4scripts.py diff --git a/doc/OnlineDocs/src/strip_examples.py b/doc/Archive/src/strip_examples.py similarity index 100% rename from doc/OnlineDocs/src/strip_examples.py rename to doc/Archive/src/strip_examples.py diff --git a/doc/OnlineDocs/src/test_examples.py b/doc/Archive/src/test_examples.py similarity index 100% rename from doc/OnlineDocs/src/test_examples.py rename to doc/Archive/src/test_examples.py diff --git a/doc/OnlineDocs/tutorial_examples.rst b/doc/Archive/tutorial_examples.rst similarity index 100% rename from doc/OnlineDocs/tutorial_examples.rst rename to doc/Archive/tutorial_examples.rst diff --git a/doc/OnlineDocs/working_abstractmodels/BuildAction.rst b/doc/Archive/working_abstractmodels/BuildAction.rst similarity index 100% rename from doc/OnlineDocs/working_abstractmodels/BuildAction.rst rename to doc/Archive/working_abstractmodels/BuildAction.rst diff --git a/doc/OnlineDocs/working_abstractmodels/data/ABCD.pdf b/doc/Archive/working_abstractmodels/data/ABCD.pdf similarity index 100% rename from doc/OnlineDocs/working_abstractmodels/data/ABCD.pdf rename to doc/Archive/working_abstractmodels/data/ABCD.pdf diff --git a/doc/OnlineDocs/working_abstractmodels/data/ABCD.png b/doc/Archive/working_abstractmodels/data/ABCD.png similarity index 100% rename from doc/OnlineDocs/working_abstractmodels/data/ABCD.png rename to doc/Archive/working_abstractmodels/data/ABCD.png diff --git a/doc/OnlineDocs/working_abstractmodels/data/PP.png b/doc/Archive/working_abstractmodels/data/PP.png similarity index 100% rename from doc/OnlineDocs/working_abstractmodels/data/PP.png rename to doc/Archive/working_abstractmodels/data/PP.png diff --git a/doc/OnlineDocs/working_abstractmodels/data/dataportals.rst b/doc/Archive/working_abstractmodels/data/dataportals.rst similarity index 100% rename from doc/OnlineDocs/working_abstractmodels/data/dataportals.rst rename to doc/Archive/working_abstractmodels/data/dataportals.rst diff --git a/doc/OnlineDocs/working_abstractmodels/data/datfiles.rst b/doc/Archive/working_abstractmodels/data/datfiles.rst similarity index 100% rename from doc/OnlineDocs/working_abstractmodels/data/datfiles.rst rename to doc/Archive/working_abstractmodels/data/datfiles.rst diff --git a/doc/OnlineDocs/working_abstractmodels/data/index.rst b/doc/Archive/working_abstractmodels/data/index.rst similarity index 100% rename from doc/OnlineDocs/working_abstractmodels/data/index.rst rename to doc/Archive/working_abstractmodels/data/index.rst diff --git a/doc/OnlineDocs/working_abstractmodels/data/native.rst b/doc/Archive/working_abstractmodels/data/native.rst similarity index 100% rename from doc/OnlineDocs/working_abstractmodels/data/native.rst rename to doc/Archive/working_abstractmodels/data/native.rst diff --git a/doc/OnlineDocs/working_abstractmodels/data/raw_dicts.rst b/doc/Archive/working_abstractmodels/data/raw_dicts.rst similarity index 100% rename from doc/OnlineDocs/working_abstractmodels/data/raw_dicts.rst rename to doc/Archive/working_abstractmodels/data/raw_dicts.rst diff --git a/doc/OnlineDocs/working_abstractmodels/data/storing_data.rst b/doc/Archive/working_abstractmodels/data/storing_data.rst similarity index 100% rename from doc/OnlineDocs/working_abstractmodels/data/storing_data.rst rename to doc/Archive/working_abstractmodels/data/storing_data.rst diff --git a/doc/OnlineDocs/working_abstractmodels/index.rst b/doc/Archive/working_abstractmodels/index.rst similarity index 100% rename from doc/OnlineDocs/working_abstractmodels/index.rst rename to doc/Archive/working_abstractmodels/index.rst diff --git a/doc/OnlineDocs/working_abstractmodels/instantiating_models.rst b/doc/Archive/working_abstractmodels/instantiating_models.rst similarity index 100% rename from doc/OnlineDocs/working_abstractmodels/instantiating_models.rst rename to doc/Archive/working_abstractmodels/instantiating_models.rst diff --git a/doc/OnlineDocs/working_abstractmodels/pyomo_command.rst b/doc/Archive/working_abstractmodels/pyomo_command.rst similarity index 100% rename from doc/OnlineDocs/working_abstractmodels/pyomo_command.rst rename to doc/Archive/working_abstractmodels/pyomo_command.rst diff --git a/doc/OnlineDocs/working_models.rst b/doc/Archive/working_models.rst similarity index 100% rename from doc/OnlineDocs/working_models.rst rename to doc/Archive/working_models.rst From 5c1924ff011b7e30614eb2d8d7d36e2e83ba6e28 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 4 Jun 2024 17:49:27 -0400 Subject: [PATCH 1516/3044] implement and test J1 for 4d and above --- .../piecewise/piecewise_linear_function.py | 4 +- .../piecewise/tests/test_triangulations.py | 164 +++++++++--------- pyomo/contrib/piecewise/triangulations.py | 117 +++++++++++-- 3 files changed, 184 insertions(+), 101 deletions(-) diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 6285e1cd46f..1050836626b 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -20,7 +20,7 @@ PiecewiseLinearExpression, ) from pyomo.contrib.piecewise.triangulations import ( - get_j1_triangulation, + get_unordered_j1_triangulation, get_ordered_j1_triangulation, Triangulation, ) @@ -308,7 +308,7 @@ def _construct_simplices_from_multivariate_points(self, obj, parent, points, raise obj._triangulation = tri elif tri == Triangulation.J1: - triangulation = get_j1_triangulation(points, dimension) + triangulation = get_unordered_j1_triangulation(points, dimension) obj._triangulation = tri elif tri == Triangulation.OrderedJ1: triangulation = get_ordered_j1_triangulation(points, dimension) diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index 19b17b24d36..ffad3b18f13 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -14,10 +14,13 @@ from unittest import skipUnless import pyomo.common.unittest as unittest from pyomo.contrib.piecewise.triangulations import ( - get_j1_triangulation, + get_unordered_j1_triangulation, + get_ordered_j1_triangulation, get_incremental_simplex_ordering, get_incremental_simplex_ordering_assume_connected_by_n_face, get_Gn_hamiltonian, + get_grid_hamiltonian, + ) from math import factorial import itertools @@ -30,7 +33,7 @@ def test_J1_small(self): [1, 0], [1, 1], [1, 2], [2, 0], [2, 1], [2, 2], ] - triangulation = get_j1_triangulation(points, 2) + triangulation = get_unordered_j1_triangulation(points, 2) self.assertEqual(triangulation.simplices, { 0: [[0, 0], [0, 1], [1, 1]], @@ -50,7 +53,7 @@ def test_J1_small_offset(self): [1.5, 0.5], [1.5, 1.5], [1.5, 2.5], [2.5, 0.5], [2.5, 1.5], [2.5, 2.5], ] - triangulation = get_j1_triangulation(points, 2) + triangulation = get_unordered_j1_triangulation(points, 2) self.assertEqual(triangulation.simplices, { 0: [[0.5, 0.5], [0.5, 1.5], [1.5, 1.5]], @@ -62,90 +65,73 @@ def test_J1_small_offset(self): 6: [[1.5, 0.5], [1.5, 1.5], [2.5, 0.5]], 7: [[1.5, 1.5], [1.5, 2.5], [2.5, 2.5]], }) - - def test_J1_small_ordering(self): - points = [ - [0.5, 0.5], [0.5, 1.5], [0.5, 2.5], - [1.5, 0.5], [1.5, 1.5], [1.5, 2.5], - [2.5, 0.5], [2.5, 1.5], [2.5, 2.5], - ] - triangulation = get_j1_triangulation(points, 2) - reordered_simplices = get_incremental_simplex_ordering(triangulation.simplices) - for idx, first_simplex in reordered_simplices.items(): - if idx != len(triangulation.points) - 1: - second_simplex = reordered_simplices[idx + 1] - # test property (2) which also guarantees property (1) - self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") - - def test_J1_medium_ordering(self): - points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6])) - triangulation = get_j1_triangulation(points, 2) - reordered_simplices = get_incremental_simplex_ordering(triangulation.simplices) - for idx, first_simplex in reordered_simplices.items(): - if idx != len(triangulation.points) - 1: - second_simplex = reordered_simplices[idx + 1] - # test property (2) which also guarantees property (1) - self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") - - def test_J1_medium_ordering_alt(self): - points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6])) - triangulation = get_j1_triangulation(points, 2) - reordered_simplices = get_incremental_simplex_ordering_assume_connected_by_n_face(triangulation.simplices, 1) - for idx, first_simplex in reordered_simplices.items(): - if idx != len(triangulation.points) - 1: - second_simplex = reordered_simplices[idx + 1] - # test property (2) which also guarantees property (1) - self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") - - def test_J1_medium_ordering_3d(self): - points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6], [-5, -1, 0.2, 3, 10])) - triangulation = get_j1_triangulation(points, 3) - reordered_simplices = get_incremental_simplex_ordering_assume_connected_by_n_face(triangulation.simplices, 2) - for idx, first_simplex in reordered_simplices.items(): - if idx != len(triangulation.points) - 1: - second_simplex = reordered_simplices[idx + 1] - # test property (2) which also guarantees property (1) - self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") - - def test_J1_2d_ordering_0(self): - points = list(itertools.product([0, 1, 2], [1, 2.4, 3])) - ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices - self.assertEqual(len(ordered_triangulation), 8) - for idx, first_simplex in ordered_triangulation.items(): - if idx != len(ordered_triangulation) - 1: - second_simplex = ordered_triangulation[idx + 1] - # test property (2) which also guarantees property (1) - self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") - def test_J1_2d_ordering_1(self): - points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6])) - ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices - self.assertEqual(len(ordered_triangulation), 32) - for idx, first_simplex in ordered_triangulation.items(): - if idx != len(ordered_triangulation) - 1: - second_simplex = ordered_triangulation[idx + 1] - # test property (2) which also guarantees property (1) - self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") - - def test_J1_2d_ordering_2(self): - points = list(itertools.product([0, 1, 2, 4, 5, 6.3, 7.1], [1, 2.4, 3, 5, 6, 9.1, 10])) - ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices - self.assertEqual(len(ordered_triangulation), 72) + def check_J1_ordered(self, points, num_points, dim): + ordered_triangulation = get_ordered_j1_triangulation(points, dim).simplices + #print(ordered_triangulation) + self.assertEqual(len(ordered_triangulation), factorial(dim) * (num_points - 1) ** dim) for idx, first_simplex in ordered_triangulation.items(): if idx != len(ordered_triangulation) - 1: second_simplex = ordered_triangulation[idx + 1] # test property (2) which also guarantees property (1) self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + # The way I am constructing these, they should always share an (n-1)-face. + # Check that too for good measure. + count = 0 + for pt in first_simplex: + if pt in second_simplex: + count += 1 + #print(f"first_simplex={first_simplex}; second_simplex={second_simplex}") + self.assertEqual(count, dim) # (n-1)-face has n points + #if count != dim: + # print(f"error: count {count} was not the correct {dim}") + + def test_J1_ordered_2d(self): + self.check_J1_ordered( + list(itertools.product([0, 1, 2], [1, 2.4, 3])), + 3, + 2, + ) + self.check_J1_ordered( + list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6])), + 5, + 2, + ) + self.check_J1_ordered( + list(itertools.product([0, 1, 2, 4, 5, 6.3, 7.1], [1, 2.4, 3, 5, 6, 9.1, 10])), + 7, + 2, + ) + self.check_J1_ordered( + list(itertools.product([0, 1, 2, 4, 5, 6.3, 7.1, 7.2, 7.3], [1, 2.4, 3, 5, 6, 9.1, 10, 11, 12])), + 9, + 2, + ) + + def test_J1_ordered_3d(self): + self.check_J1_ordered( + list(itertools.product([0, 1, 2], [1, 2.4, 3], [2, 3, 4])), + 3, + 3, + ) + self.check_J1_ordered( + list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6], [-1, 0, 1, 2, 3])), + 5, + 3, + ) + + def test_J1_ordered_4d_and_above(self): + self.check_J1_ordered( + list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6], [-1, 0, 1, 2, 3], [1, 2, 3, 4, 5])), + 5, + 4, + ) + self.check_J1_ordered( + list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6], [-1, 0, 1, 2, 3], [1, 2, 3, 4, 5], [2, 3, 4, 5, 6])), + 5, + 5, + ) - def test_J1_2d_ordering_3(self): - points = list(itertools.product([0, 1, 2, 4, 5, 6.3, 7.1, 7.2, 7.3], [1, 2.4, 3, 5, 6, 9.1, 10, 11, 12])) - ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices - self.assertEqual(len(ordered_triangulation), 128) - for idx, first_simplex in ordered_triangulation.items(): - if idx != len(ordered_triangulation) - 1: - second_simplex = ordered_triangulation[idx + 1] - # test property (2) which also guarantees property (1) - self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") def check_Gn_hamiltonian_path(self, n, start_permutation, target_symbol, last): path = get_Gn_hamiltonian(n, start_permutation, target_symbol, last) @@ -183,4 +169,22 @@ def test_Gn_hamiltonian_paths(self): self.check_Gn_hamiltonian_path(6, (6, 1, 2, 4, 3, 5), 5, True) self.check_Gn_hamiltonian_path(6, (6, 1, 2, 4, 3, 5), 5, False) self.check_Gn_hamiltonian_path(7, (1, 2, 3, 4, 5, 6, 7), 7, False) + + def check_grid_hamiltonian(self, dim, length): + path = get_grid_hamiltonian(dim, length) + self.assertEqual(len(path), length ** dim) + for x in itertools.product(range(length), repeat=dim): + self.assertTrue(list(x) in path) + for i in range(len(path) - 1): + diff_indices = [j for j in range(dim) if path[i][j] != path[i + 1][j]] + self.assertEqual(len(diff_indices), 1) + self.assertEqual(abs(path[i][diff_indices[0]] - path[i + 1][diff_indices[0]]), 1) + + def test_grid_hamiltonian_paths(self): + self.check_grid_hamiltonian(1, 5) + self.check_grid_hamiltonian(2, 5) + self.check_grid_hamiltonian(2, 8) + self.check_grid_hamiltonian(3, 5) + self.check_grid_hamiltonian(4, 3) + diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index c5eaaead327..699d55931a5 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -28,21 +28,15 @@ Objective, TerminationCondition, ) -from pyomo.common.dependencies import attempt_import -nx, nx_available = attempt_import( - 'networkx', 'Networkx is required to calculate incremental ordering.' -) - - -class Triangulation: +class Triangulation(Enum): AssumeValid = 0 Delaunay = 1 J1 = 2 OrderedJ1 = 3 -def get_j1_triangulation(points, dimension, ordered=False): +def get_unordered_j1_triangulation(points, dimension): points_map, num_pts = _process_points_j1(points, dimension) simplices_list = _get_j1_triangulation(points_map, num_pts - 1, dimension) # make a duck-typed thing that superficially looks like an instance of @@ -58,13 +52,12 @@ def get_j1_triangulation(points, dimension, ordered=False): def get_ordered_j1_triangulation(points, dimension): points_map, num_pts = _process_points_j1(points, dimension) if dimension == 2: - simplices_list = _get_j1_triangulation_2d_ordered(points_map, num_pts - 1) - else: + simplices_list = _get_ordered_j1_triangulation_2d(points_map, num_pts - 1) + elif dimension == 3: raise DeveloperError("Unimplemented!") - # elif dimension == 3: - # return _get_j1_triangulation_3d(points_map, num_pts - 1) - # else: - # return _get_j1_triangulation_for_more_than_4d(points_map, num_pts - 1) + #simplices_list = _get_ordered_j1_triangulation_3d(points_map, num_pts - 1) + else: + simplices_list = _get_ordered_j1_triangulation_4d_and_above(points_map, num_pts - 1, dimension) triangulation = SimpleNamespace() triangulation.points = list(range(len(simplices_list))) triangulation.simplices = {i: simplices_list[i] for i in triangulation.points} @@ -132,7 +125,7 @@ def _get_j1_triangulation(points_map, K, n): # and also keep the pictures slightly more regular to make things easier to # implement. Also remember that Todd's drawing is misleading to the point of # almost being wrong so make sure you draw it properly first. -def _get_j1_triangulation_2d_ordered(points_map, num_pts): +def _get_ordered_j1_triangulation_2d(points_map, num_pts): # check when square has simplices in top-left and bottom-right square_parity_tlbr = lambda x, y: x % 2 == y % 2 # check when we are in a "turnaround square" as seen in the picture @@ -313,12 +306,98 @@ def add_top_left(): continue -def _get_j1_triangulation_3d(points, dimension): +def _get_ordered_j1_triangulation_3d(points_map, num_pts): pass -def _get_j1_triangulation_for_more_than_4d(points, dimension): - pass +def _get_ordered_j1_triangulation_4d_and_above(points_map, num_pts, dim): + # step one: get a hamiltonian path in the appropriate grid graph (low-coordinate + # corners of the grid squares) + grid_hamiltonian = get_grid_hamiltonian(dim, num_pts) + + # step 1.5: get a starting simplex. Anything that is *not* adjacent to the + # second square is fine. Since we always go from [0, ..., 0] to [0, ..., 1], + # i.e., j=`dim`, anything where `dim` is not the first or last symbol should + # always work. Let's stick it in the second place + start_perm = tuple([1] + [dim] + list(range(2, dim))) + + # step two: for each square, get a sequence of simplices from a starting simplex, + # through the square, and then ending with a simplex adjacent to the next square. + # Then find the appropriate adjacent simplex to start on the next square + simplices = {} + for i in range(len(grid_hamiltonian) - 1): + current_corner = grid_hamiltonian[i] + next_corner = grid_hamiltonian[i + 1] + # differing index + j = [k + 1 for k in range(dim) if current_corner[k] != next_corner[k]][0] + # border x_j value between this square and next + c = max(current_corner[j - 1], next_corner[j - 1]) + v_0, sign = get_nearest_odd_and_sign_vec(current_corner) + # According to Todd, what we need is to end with a permutation where rho(n) = j + # if c is odd, and end with one where rho(1) = j if c is even. I think this + # is right -- basically the sign from the sign vector sometimes cancels + # out the sign from whether we are entering in the +c or -c direction. + if c % 2 == 0: + perm_sequence = get_Gn_hamiltonian(dim, start_perm, j, False) + for pi in perm_sequence: + simplices[len(simplices)] = get_one_j1_simplex(v_0, pi, sign, dim, points_map) + else: + perm_sequence = get_Gn_hamiltonian(dim, start_perm, j, True) + for pi in perm_sequence: + simplices[len(simplices)] = get_one_j1_simplex(v_0, pi, sign, dim, points_map) + # should be true regardless of odd or even? I hope + start_perm = perm_sequence[-1] + + # step three: finish out the last square + # Any final permutation is fine; we are going nowhere after this + v_0, sign = get_nearest_odd_and_sign_vec(grid_hamiltonian[-1]) + for pi in get_Gn_hamiltonian(dim, start_perm, 1, False): + simplices[len(simplices)] = get_one_j1_simplex(v_0, pi, sign, dim, points_map) + + # fix vertices and return + fix_vertices_incremental_order(simplices) + return simplices + +def get_one_j1_simplex(v_0, pi, sign, dim, points_map): + simplex = [] + current = list(v_0) + simplex.append(points_map[*current]) + for i in range(0, dim): + current = current.copy() + current[pi[i] - 1] += sign[pi[i] - 1] + simplex.append(points_map[*current]) + return sorted(simplex) + +# get the v_0 and sign vectors corresponding to a given square, identified by its +# low-coordinate corner +def get_nearest_odd_and_sign_vec(corner): + v_0 = [] + sign = [] + for x in corner: + if x % 2 == 0: + v_0.append(x + 1) + sign.append(-1) + else: + v_0.append(x) + sign.append(1) + return v_0, sign + +def get_grid_hamiltonian(dim, length): + if dim == 1: + return [[n] for n in range(length)] + else: + ret = [] + prev = get_grid_hamiltonian(dim - 1, length) + for n in range(length): + # if n is even, add the previous hamiltonian with n in its new first + # coordinate. If odd, do the same with the previous hamiltonian in reverse. + if n % 2 == 0: + for x in prev: + ret.append([n] + x) + else: + for x in reversed(prev): + ret.append([n] + x) + return ret def get_incremental_simplex_ordering(simplices, subsolver='gurobi'): @@ -776,7 +855,7 @@ def _get_Gn_hamiltonian(n, target_symbol): second_last = ret.pop() # of form (n, 1, i, j, ...) i = last[2] j = last[3] - test = list(last) + test = list(last) # want permutation of form (n, 1, j, i, ...) with same tail test[0] = n test[1] = 1 test[2] = j From 85e100302a6741837f1beb0fd3b1b7649fa30d4f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 4 Jun 2024 17:35:31 -0600 Subject: [PATCH 1517/3044] Initial reorganization of Pyomo documentation (replicating 91e9f59 and 6e7cd39 by @blnicho) --- doc/OnlineDocs/Makefile | 20 + doc/OnlineDocs/_static/theme_overrides.css | 74 ++ doc/OnlineDocs/conf.py | 291 +++++ doc/OnlineDocs/contribution_guide.rst | 431 +++++++ doc/OnlineDocs/developer_guide/config.rst | 3 + .../developer_guide/deprecation.rst | 62 + .../developer_guide/expressions/design.rst | 268 ++++ .../developer_guide/expressions/index.rst | 55 + .../developer_guide/expressions/managing.rst | 272 +++++ .../developer_guide/expressions/overview.rst | 300 +++++ .../expressions/performance.rst | 171 +++ doc/OnlineDocs/developer_guide/future.rst | 3 + doc/OnlineDocs/developer_guide/index.rst | 15 + doc/OnlineDocs/developer_guide/solvers.rst | 351 ++++++ doc/OnlineDocs/docutils.conf | 2 + doc/OnlineDocs/getting_started/index.rst | 5 + .../getting_started/installation.rst | 99 ++ .../pyomo_overview/abstract_concrete.rst | 54 + .../getting_started/pyomo_overview/index.rst | 10 + .../pyomo_overview/math_modeling.rst | 102 ++ .../pyomo_overview/overview_components.rst | 42 + .../pyomo_overview/simple_examples.rst | 416 +++++++ doc/OnlineDocs/index.rst | 71 ++ .../reference_guide/bibliography.rst | 68 ++ doc/OnlineDocs/reference_guide/index.rst | 9 + .../library_reference/aml/index.rst | 85 ++ .../library_reference/appsi/appsi.base.rst | 47 + .../library_reference/appsi/appsi.rst | 106 ++ .../appsi/appsi.solvers.cbc.rst | 15 + .../appsi/appsi.solvers.cplex.rst | 21 + .../appsi/appsi.solvers.gurobi.rst | 55 + .../appsi/appsi.solvers.highs.rst | 14 + .../appsi/appsi.solvers.ipopt.rst | 14 + .../appsi/appsi.solvers.maingo.rst | 14 + .../library_reference/appsi/appsi.solvers.rst | 16 + .../library_reference/common/config.rst | 85 ++ .../library_reference/common/dependencies.rst | 7 + .../library_reference/common/deprecation.rst | 6 + .../library_reference/common/enums.rst | 7 + .../library_reference/common/errors.rst | 6 + .../library_reference/common/fileutils.rst | 6 + .../library_reference/common/formatting.rst | 6 + .../library_reference/common/index.rst | 19 + .../library_reference/common/tempfiles.rst | 7 + .../library_reference/common/timing.rst | 7 + .../library_reference/data/index.rst | 11 + .../expressions/building.rst | 10 + .../library_reference/expressions/classes.rst | 105 ++ .../expressions/context_managers.rst | 10 + .../library_reference/expressions/index.rst | 13 + .../expressions/managing.rst | 19 + .../expressions/visitors.rst | 20 + .../library_reference/index.rst | 27 + .../library_reference/kernel/base.rst | 6 + .../library_reference/kernel/block.rst | 26 + .../library_reference/kernel/conic.rst | 42 + .../library_reference/kernel/constraint.rst | 34 + .../kernel/dict_container.rst | 8 + .../kernel/examples/aml_example.py | 193 +++ .../kernel/examples/conic.py | 33 + .../kernel/examples/kernel_containers.py | 18 + .../kernel/examples/kernel_example.py | 174 +++ .../kernel/examples/kernel_solving.py | 22 + .../kernel/examples/kernel_subclassing.py | 93 ++ .../kernel/examples/transformer.py | 66 + .../library_reference/kernel/expression.rst | 26 + .../kernel/heterogeneous_container.rst | 6 + .../kernel/homogeneous_container.rst | 6 + .../library_reference/kernel/index.rst | 210 ++++ .../kernel/list_container.rst | 8 + .../library_reference/kernel/objective.rst | 26 + .../library_reference/kernel/parameter.rst | 30 + .../kernel/piecewise/index.rst | 11 + .../kernel/piecewise/piecewise.rst | 53 + .../kernel/piecewise/piecewise_nd.rst | 25 + .../kernel/piecewise/util.rst | 6 + .../library_reference/kernel/sos.rst | 30 + .../library_reference/kernel/suffix.rst | 6 + .../kernel/syntax_comparison.rst | 133 ++ .../kernel/tuple_container.rst | 8 + .../library_reference/kernel/variable.rst | 26 + .../solvers/cplex_persistent.rst | 7 + .../library_reference/solvers/gams.rst | 43 + .../solvers/gurobi_direct.rst | 18 + .../solvers/gurobi_persistent.rst | 39 + .../library_reference/solvers/index.rst | 11 + .../solvers/xpress_persistent.rst | 7 + doc/OnlineDocs/related_packages.rst | 65 + .../contributed_packages/communities_8pp.png | Bin 0 -> 256159 bytes .../communities_decode_1.png | Bin 0 -> 157034 bytes .../contributed_packages/community.rst | 389 ++++++ .../contributed_packages/doe/CCSI-license.txt | 43 + .../contributed_packages/doe/doe.rst | 291 +++++ .../contributed_packages/doe/flowchart.png | Bin 0 -> 160954 bytes .../contributed_packages/doe/grid-1.png | Bin 0 -> 611702 bytes .../contributed_packages/doe/reactor.png | Bin 0 -> 114480 bytes .../contributed_packages/doe/uml.png | Bin 0 -> 199419 bytes .../contributed_packages/gdpopt.rst | 213 ++++ .../contributed_packages/gdpopt_flowchart.png | Bin 0 -> 71538 bytes .../user_guide/contributed_packages/iis.rst | 135 +++ .../contributed_packages/incidence/api.rst | 14 + .../contributed_packages/incidence/config.rst | 5 + .../incidence/connected.rst | 5 + .../incidence/dulmage_mendelsohn.rst | 5 + .../incidence/incidence.rst | 5 + .../contributed_packages/incidence/index.rst | 19 + .../incidence/interface.rst | 5 + .../incidence/matching.rst | 5 + .../incidence/overview.rst | 50 + .../incidence/scc_solver.rst | 5 + .../incidence/triangularize.rst | 5 + .../incidence/tutorial.bt.rst | 107 ++ .../incidence/tutorial.btsolve.rst | 72 ++ .../incidence/tutorial.dm.rst | 191 +++ .../incidence/tutorial.rst | 14 + .../user_guide/contributed_packages/index.rst | 46 + .../contributed_packages/latex_printer.rst | 127 ++ .../user_guide/contributed_packages/mcpp.rst | 62 + .../contributed_packages/mindtpy.rst | 319 +++++ .../contributed_packages/mpc/api.rst | 10 + .../contributed_packages/mpc/conversion.rst | 5 + .../contributed_packages/mpc/data.rst | 17 + .../contributed_packages/mpc/examples.rst | 6 + .../contributed_packages/mpc/faq.rst | 16 + .../contributed_packages/mpc/index.rst | 32 + .../contributed_packages/mpc/interface.rst | 8 + .../contributed_packages/mpc/modeling.rst | 11 + .../contributed_packages/mpc/overview.rst | 210 ++++ .../contributed_packages/multistart.rst | 34 + .../contributed_packages/parmest/api.rst | 25 + .../contributed_packages/parmest/boxplot.png | Bin 0 -> 19354 bytes .../parmest/covariance.rst | 16 + .../contributed_packages/parmest/datarec.rst | 54 + .../contributed_packages/parmest/driver.rst | 165 +++ .../contributed_packages/parmest/examples.rst | 44 + .../contributed_packages/parmest/graphics.rst | 55 + .../contributed_packages/parmest/index.rst | 35 + .../parmest/installation.rst | 33 + .../contributed_packages/parmest/overview.rst | 72 ++ .../parmest/pairwise_plot_CI.png | Bin 0 -> 84454 bytes .../parmest/pairwise_plot_LR.png | Bin 0 -> 49578 bytes .../contributed_packages/parmest/parallel.rst | 52 + .../parmest/scencreate.rst | 22 + .../contributed_packages/preprocessing.rst | 151 +++ .../contributed_packages/pynumero/api.rst | 14 + .../pynumero/backward_compatibility.rst | 14 + .../contributed_packages/pynumero/index.rst | 51 + .../pynumero/installation.rst | 47 + .../pynumero/pynumero.interfaces.ampl_nlp.rst | 8 + .../pynumero/pynumero.interfaces.asl_nlp.rst | 8 + .../pynumero.interfaces.extended_nlp.rst | 8 + ...ero.interfaces.external_grey_box_model.rst | 8 + .../pynumero/pynumero.interfaces.nlp.rst | 8 + .../pynumero.interfaces.projected_nlp.rst | 8 + ...pynumero.interfaces.pyomo_grey_box_nlp.rst | 8 + .../pynumero.interfaces.pyomo_nlp.rst | 8 + .../pynumero/pynumero.interfaces.rst | 16 + .../pynumero/pynumero.linalg.base.rst | 26 + .../pynumero/pynumero.linalg.ma27.rst | 8 + .../pynumero/pynumero.linalg.ma57.rst | 8 + .../pynumero/pynumero.linalg.mumps.rst | 8 + .../pynumero/pynumero.linalg.rst | 14 + .../pynumero/pynumero.linalg.scipy.rst | 14 + .../pynumero/pynumero.sparse.block_vector.rst | 154 +++ .../pynumero/pynumero.sparse.rst | 9 + .../tutorial.block_vectors_and_matrices.rst | 272 +++++ .../tutorial.linear_solver_interfaces.rst | 76 ++ .../pynumero/tutorial.mpi_blocks.rst | 65 + .../pynumero/tutorial.nlp_interfaces.rst | 115 ++ .../pynumero/tutorial.rst | 11 + .../user_guide/contributed_packages/pyros.rst | 1078 +++++++++++++++++ .../contributed_packages/satsolver.rst | 34 + .../sensitivity_toolbox.rst | 185 +++ .../contributed_packages/trustregion.rst | 189 +++ doc/OnlineDocs/user_guide/errors.rst | 192 +++ .../user_guide/external_tutorials.rst | 20 + doc/OnlineDocs/user_guide/flattener/index.rst | 65 + .../user_guide/flattener/motivation.rst | 26 + .../user_guide/flattener/reference.rst | 14 + doc/OnlineDocs/user_guide/index.rst | 6 + .../modeling_extensions/__init__.py | 10 + .../modeling_extensions/bilevel.rst | 6 + .../user_guide/modeling_extensions/dae.rst | 933 ++++++++++++++ .../modeling_extensions/gdp/concepts.rst | 151 +++ .../modeling_extensions/gdp/index.rst | 79 ++ .../modeling_extensions/gdp/modeling.rst | 419 +++++++ .../modeling_extensions/gdp/solving.rst | 201 +++ .../user_guide/modeling_extensions/index.rst | 12 + .../user_guide/modeling_extensions/mpec.rst | 6 + .../modeling_extensions/network.rst | 331 +++++ .../reduce_points_demo.png | Bin 0 -> 29803 bytes .../stochastic_programming.rst | 17 + .../user_guide/persistent_solvers.rst | 188 +++ .../pyomo_modeling_components/Constraints.rst | 39 + .../pyomo_modeling_components/Expressions.rst | 218 ++++ .../pyomo_modeling_components/Objectives.rst | 39 + .../pyomo_modeling_components/Parameters.rst | 102 ++ .../pyomo_modeling_components/Sets.rst | 519 ++++++++ .../pyomo_modeling_components/Suffixes.rst | 509 ++++++++ .../pyomo_modeling_components/Variables.rst | 46 + .../pyomo_modeling_components/index.rst | 13 + doc/OnlineDocs/user_guide/scaling.rst | 41 + doc/OnlineDocs/user_guide/sos_constraints.rst | 288 +++++ doc/OnlineDocs/user_guide/units_container.rst | 13 + doc/OnlineDocs/user_guide/working_models.rst | 704 +++++++++++ 205 files changed, 16351 insertions(+) create mode 100644 doc/OnlineDocs/Makefile create mode 100644 doc/OnlineDocs/_static/theme_overrides.css create mode 100644 doc/OnlineDocs/conf.py create mode 100644 doc/OnlineDocs/contribution_guide.rst create mode 100644 doc/OnlineDocs/developer_guide/config.rst create mode 100644 doc/OnlineDocs/developer_guide/deprecation.rst create mode 100644 doc/OnlineDocs/developer_guide/expressions/design.rst create mode 100644 doc/OnlineDocs/developer_guide/expressions/index.rst create mode 100644 doc/OnlineDocs/developer_guide/expressions/managing.rst create mode 100644 doc/OnlineDocs/developer_guide/expressions/overview.rst create mode 100644 doc/OnlineDocs/developer_guide/expressions/performance.rst create mode 100644 doc/OnlineDocs/developer_guide/future.rst create mode 100644 doc/OnlineDocs/developer_guide/index.rst create mode 100644 doc/OnlineDocs/developer_guide/solvers.rst create mode 100644 doc/OnlineDocs/docutils.conf create mode 100644 doc/OnlineDocs/getting_started/index.rst create mode 100644 doc/OnlineDocs/getting_started/installation.rst create mode 100644 doc/OnlineDocs/getting_started/pyomo_overview/abstract_concrete.rst create mode 100644 doc/OnlineDocs/getting_started/pyomo_overview/index.rst create mode 100644 doc/OnlineDocs/getting_started/pyomo_overview/math_modeling.rst create mode 100644 doc/OnlineDocs/getting_started/pyomo_overview/overview_components.rst create mode 100644 doc/OnlineDocs/getting_started/pyomo_overview/simple_examples.rst create mode 100644 doc/OnlineDocs/index.rst create mode 100644 doc/OnlineDocs/reference_guide/bibliography.rst create mode 100644 doc/OnlineDocs/reference_guide/index.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/aml/index.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.base.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.cbc.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.cplex.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.gurobi.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.highs.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.ipopt.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.maingo.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/common/config.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/common/dependencies.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/common/deprecation.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/common/enums.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/common/errors.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/common/fileutils.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/common/formatting.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/common/index.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/common/tempfiles.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/common/timing.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/data/index.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/expressions/building.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/expressions/classes.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/expressions/context_managers.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/expressions/index.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/expressions/managing.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/expressions/visitors.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/index.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/base.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/block.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/conic.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/constraint.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/dict_container.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/examples/aml_example.py create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/examples/conic.py create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_containers.py create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_example.py create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_solving.py create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_subclassing.py create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/examples/transformer.py create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/expression.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/heterogeneous_container.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/homogeneous_container.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/index.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/list_container.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/objective.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/parameter.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/index.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/piecewise.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/piecewise_nd.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/util.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/sos.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/suffix.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/syntax_comparison.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/tuple_container.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/kernel/variable.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/solvers/cplex_persistent.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/solvers/gams.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/solvers/gurobi_direct.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/solvers/gurobi_persistent.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/solvers/index.rst create mode 100644 doc/OnlineDocs/reference_guide/library_reference/solvers/xpress_persistent.rst create mode 100644 doc/OnlineDocs/related_packages.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/communities_8pp.png create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/communities_decode_1.png create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/community.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/doe/CCSI-license.txt create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/doe/doe.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/doe/flowchart.png create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/doe/grid-1.png create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/doe/reactor.png create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/doe/uml.png create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/gdpopt.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/gdpopt_flowchart.png create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/iis.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/incidence/api.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/incidence/config.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/incidence/connected.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/incidence/dulmage_mendelsohn.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/incidence/incidence.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/incidence/index.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/incidence/interface.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/incidence/matching.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/incidence/overview.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/incidence/scc_solver.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/incidence/triangularize.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.bt.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.btsolve.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.dm.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/index.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/latex_printer.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/mcpp.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/mindtpy.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/mpc/api.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/mpc/conversion.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/mpc/data.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/mpc/examples.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/mpc/faq.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/mpc/index.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/mpc/interface.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/mpc/modeling.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/mpc/overview.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/multistart.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/parmest/api.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/parmest/boxplot.png create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/parmest/covariance.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/parmest/datarec.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/parmest/driver.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/parmest/examples.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/parmest/graphics.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/parmest/index.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/parmest/installation.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/parmest/overview.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/parmest/pairwise_plot_CI.png create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/parmest/pairwise_plot_LR.png create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/parmest/parallel.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/parmest/scencreate.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/preprocessing.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/api.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/backward_compatibility.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/index.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/installation.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.nlp.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.base.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.ma27.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.ma57.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.mumps.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.scipy.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.sparse.block_vector.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.sparse.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.mpi_blocks.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.nlp_interfaces.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/pyros.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/satsolver.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/sensitivity_toolbox.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/trustregion.rst create mode 100644 doc/OnlineDocs/user_guide/errors.rst create mode 100644 doc/OnlineDocs/user_guide/external_tutorials.rst create mode 100644 doc/OnlineDocs/user_guide/flattener/index.rst create mode 100644 doc/OnlineDocs/user_guide/flattener/motivation.rst create mode 100644 doc/OnlineDocs/user_guide/flattener/reference.rst create mode 100644 doc/OnlineDocs/user_guide/index.rst create mode 100644 doc/OnlineDocs/user_guide/modeling_extensions/__init__.py create mode 100644 doc/OnlineDocs/user_guide/modeling_extensions/bilevel.rst create mode 100644 doc/OnlineDocs/user_guide/modeling_extensions/dae.rst create mode 100644 doc/OnlineDocs/user_guide/modeling_extensions/gdp/concepts.rst create mode 100644 doc/OnlineDocs/user_guide/modeling_extensions/gdp/index.rst create mode 100644 doc/OnlineDocs/user_guide/modeling_extensions/gdp/modeling.rst create mode 100644 doc/OnlineDocs/user_guide/modeling_extensions/gdp/solving.rst create mode 100644 doc/OnlineDocs/user_guide/modeling_extensions/index.rst create mode 100644 doc/OnlineDocs/user_guide/modeling_extensions/mpec.rst create mode 100644 doc/OnlineDocs/user_guide/modeling_extensions/network.rst create mode 100644 doc/OnlineDocs/user_guide/modeling_extensions/reduce_points_demo.png create mode 100644 doc/OnlineDocs/user_guide/modeling_extensions/stochastic_programming.rst create mode 100644 doc/OnlineDocs/user_guide/persistent_solvers.rst create mode 100644 doc/OnlineDocs/user_guide/pyomo_modeling_components/Constraints.rst create mode 100644 doc/OnlineDocs/user_guide/pyomo_modeling_components/Expressions.rst create mode 100644 doc/OnlineDocs/user_guide/pyomo_modeling_components/Objectives.rst create mode 100644 doc/OnlineDocs/user_guide/pyomo_modeling_components/Parameters.rst create mode 100644 doc/OnlineDocs/user_guide/pyomo_modeling_components/Sets.rst create mode 100644 doc/OnlineDocs/user_guide/pyomo_modeling_components/Suffixes.rst create mode 100644 doc/OnlineDocs/user_guide/pyomo_modeling_components/Variables.rst create mode 100644 doc/OnlineDocs/user_guide/pyomo_modeling_components/index.rst create mode 100644 doc/OnlineDocs/user_guide/scaling.rst create mode 100644 doc/OnlineDocs/user_guide/sos_constraints.rst create mode 100644 doc/OnlineDocs/user_guide/units_container.rst create mode 100644 doc/OnlineDocs/user_guide/working_models.rst diff --git a/doc/OnlineDocs/Makefile b/doc/OnlineDocs/Makefile new file mode 100644 index 00000000000..00bc123ad45 --- /dev/null +++ b/doc/OnlineDocs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = Pyomo +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/doc/OnlineDocs/_static/theme_overrides.css b/doc/OnlineDocs/_static/theme_overrides.css new file mode 100644 index 00000000000..43d48693e03 --- /dev/null +++ b/doc/OnlineDocs/_static/theme_overrides.css @@ -0,0 +1,74 @@ +/* links and fixed-with literals should NOT be bold */ +.rst-content code { + font-weight: normal !important; +} + +/* internal reference links should be purple (not grey) */ +code.xref.py { + color: #8C1AFF; +} + +/* method names should be bold */ +code.descname { + font-weight: bold !important; + color: black; +} +/* method argument lists should *not* be bold, argument names in black */ +dl.py.method dt { + font-weight: normal; +} +dl.py.method dt em span.n { + color: black; +} + +/* Fix to RTD theme to allow table cell content to wrap */ +@media screen and (min-width: 767px) { + .wy-table-responsive table td { + white-space: normal !important; + } + .wy-table-responsive { + overflow: visible !important; + } +} + +/* Remove space after tables in definition lists (e.g., for function + "Parameters" lists*/ +.rst-content dl div.wy-table-responsive { + margin-bottom: 12px !important; +} + +/* Define a new "tight-table" class that we can use to format tighter + simple banded tables */ +.rst-content table.tight-table { + border-style: solid; + border-collapse: separate !important; +} +.rst-content table.tight-table td { + border-style: hidden !important; + padding-top: 4px !important; + padding-bottom: 4px !important; + padding-left: 8px !important; + padding-right: 8px !important; +} + + +/* OLD theme overrides + +code.docutils.literal{ + color:#8C1AFF; + border: 0px; + background-color:#fcfcfc; + padding:0px; + font-size: 100%; +} + +.wy-table-responsive table td, .wy-table-responsive table th { + white-space: normal; +} + +.wy-table-responsive { + margin-bottom: 24px; + max-width: 100%; + overflow: visible; +} +*/ diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py new file mode 100644 index 00000000000..630dbfcd030 --- /dev/null +++ b/doc/OnlineDocs/conf.py @@ -0,0 +1,291 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# pyomo documentation build configuration file, created by +# sphinx-quickstart on Mon Dec 12 16:08:36 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import os +import sys + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# assumes pyutilib source is next to the pyomo source directory +sys.path.insert(0, os.path.abspath('../../../pyutilib')) +# top-level pyomo source directory +sys.path.insert(0, os.path.abspath('../..')) + +# -- Options for intersphinx --------------------------------------------- + +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), + 'matplotlib': ('https://matplotlib.org/stable/', None), + 'numpy': ('https://numpy.org/doc/stable/', None), + 'pandas': ('https://pandas.pydata.org/docs/', None), + 'scikit-learn': ('https://scikit-learn.org/stable/', None), + 'scipy': ('https://docs.scipy.org/doc/scipy/', None), + 'Sphinx': ('https://www.sphinx-doc.org/en/master/', None), +} + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +needs_sphinx = '1.8' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.intersphinx', + 'sphinx.ext.autodoc', + 'sphinx.ext.coverage', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', + 'sphinx.ext.napoleon', + 'sphinx.ext.ifconfig', + 'sphinx.ext.inheritance_diagram', + 'sphinx.ext.autosummary', + 'sphinx.ext.doctest', + 'sphinx.ext.todo', + 'sphinx_copybutton', + 'enum_tools.autoenum', + 'sphinx.ext.autosectionlabel', + #'sphinx.ext.githubpages', +] + +viewcode_follow_imported_members = True +# napoleon_include_private_with_doc = True + +copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " +copybutton_prompt_is_regexp = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Pyomo' +copyright = u'2008-2023, Sandia National Laboratories' +author = u'Pyomo Developers' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +import pyomo.version + +version = pyomo.version.__version__ +# The full version, including alpha/beta/rc tags. +release = pyomo.version.__version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + +# If true, doctest flags (comments looking like # doctest: FLAG, ...) at +# the ends of lines and markers are removed for all code +# blocks showing interactive Python sessions (i.e. doctests) +trim_doctest_flags = True + +# If true, figures, tables and code-blocks are automatically numbered if +# they have a caption. +numfig = True + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +# html_theme = 'alabaster' +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' + +html_theme = 'sphinx_rtd_theme' + +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] +html_css_files = ['theme_overrides.css'] + +html_favicon = "../logos/pyomo/favicon.ico" + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'pyomo' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [(master_doc, 'pyomo.tex', 'Pyomo Documentation', 'Pyomo', 'manual')] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(master_doc, 'pyomo', 'Pyomo Documentation', [author], 1)] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + master_doc, + 'pyomo', + 'Pyomo Documentation', + author, + 'Pyomo', + 'One line description of project.', + 'Miscellaneous', + ) +] + +# autodoc_member_order = 'bysource' +# autodoc_member_order = 'groupwise' + +# -- Check which conditional dependencies are available ------------------ +# Used for skipping certain doctests +from sphinx.ext.doctest import doctest + +doctest_default_flags = ( + doctest.ELLIPSIS + + doctest.NORMALIZE_WHITESPACE + + doctest.IGNORE_EXCEPTION_DETAIL + + doctest.DONT_ACCEPT_TRUE_FOR_1 +) + + +class IgnoreResultOutputChecker(doctest.OutputChecker): + IGNORE_RESULT = doctest.register_optionflag('IGNORE_RESULT') + + def check_output(self, want, got, optionflags): + if optionflags & self.IGNORE_RESULT: + return True + return super().check_output(want, got, optionflags) + + +doctest.OutputChecker = IgnoreResultOutputChecker + +doctest_global_setup = ''' +import os, platform, sys +on_github_actions = bool(os.environ.get('GITHUB_ACTIONS', '')) +system_info = ( + sys.platform, + platform.machine(), + platform.python_implementation() +) + +from pyomo.common.dependencies import ( + attempt_import, numpy_available, scipy_available, pandas_available, + yaml_available, networkx_available, matplotlib_available, + pympler_available, dill_available, +) +pint_available = attempt_import('pint', defer_import=False)[1] +from pyomo.contrib.parmest.parmest import parmest_available + +import pyomo.environ as _pe # (trigger all plugin registrations) +import pyomo.opt as _opt + +# Not using SolverFactory to check solver availability because +# as of June 2020 there is no way to suppress warnings when +# solvers are not available +ipopt_available = bool(_opt.check_available_solvers('ipopt')) +sipopt_available = bool(_opt.check_available_solvers('ipopt_sens')) +k_aug_available = bool(_opt.check_available_solvers('k_aug')) +dot_sens_available = bool(_opt.check_available_solvers('dot_sens')) +baron_available = bool(_opt.check_available_solvers('baron')) +glpk_available = bool(_opt.check_available_solvers('glpk')) +gurobipy_available = bool(_opt.check_available_solvers('gurobi_direct')) + +baron = _opt.SolverFactory('baron') + +if numpy_available and scipy_available: + import pyomo.contrib.pynumero.asl as _asl + asl_available = _asl.AmplInterface.available() + import pyomo.contrib.pynumero.linalg.ma27 as _ma27 + ma27_available = _ma27.MA27Interface.available() + from pyomo.contrib.pynumero.linalg.mumps_interface import mumps_available +else: + asl_available = False + ma27_available = False + mumps_available = False +''' diff --git a/doc/OnlineDocs/contribution_guide.rst b/doc/OnlineDocs/contribution_guide.rst new file mode 100644 index 00000000000..9ad5bdfee0e --- /dev/null +++ b/doc/OnlineDocs/contribution_guide.rst @@ -0,0 +1,431 @@ +Contributing to Pyomo +===================== + +We welcome all contributions including bug fixes, feature enhancements, +and documentation improvements. Pyomo manages source code contributions +via GitHub pull requests (PRs). + +Contribution Requirements +------------------------- + +A PR should be 1 set of related changes. PRs for large-scale +non-functional changes (i.e. PEP8, comments) should be +separated from functional changes. This simplifies the review process +and ensures that functional changes aren't obscured by large amounts of +non-functional changes. + +We do not squash and merge PRs so all commits in your branch will appear +in the main history. In addition to well-documented PR descriptions, +we encourage modular/targeted commits with descriptive commit messages. + +Coding Standards +++++++++++++++++ + + * Required: `black `_ + * No use of ``__author__`` + * Inside ``pyomo.contrib``: Contact information for the contribution + maintainer (such as a Github ID) should be included in the Sphinx + documentation + +The first step of Pyomo's GitHub Actions workflow is to run +`black `_ and a +`spell-checker `_ to ensure style +guide compliance and minimize typos. Before opening a pull request, please +run: + +:: + + # Auto-apply correct formatting + pip install black + black -S -C --exclude examples/pyomobook/python-ch/BadIndent.py + # Find typos in files + conda install typos + typos --config .github/workflows/typos.toml + +If the spell-checker returns a failure for a word that is spelled correctly, +please add the word to the ``.github/workflows/typos.toml`` file. + +Online Pyomo documentation is generated using `Sphinx `_ +with the ``napoleon`` extension enabled. For API documentation we use of one of these +`supported styles for docstrings `_, +but we prefer the NumPy standard. Whichever you choose, we require compliant docstrings for: + + * Modules + * Public and Private Classes + * Public and Private Functions + +We also encourage you to include examples, especially for new features +and contributions to ``pyomo.contrib``. + +Testing ++++++++ + +Pyomo uses `unittest `_, +`pytest `_, +`GitHub Actions `_, +and Jenkins +for testing and continuous integration. Submitted code should include +tests to establish the validity of its results and/or effects. Unit +tests are preferred but we also accept integration tests. We require +at least 70% coverage of the lines modified in the PR and prefer coverage +closer to 90%. We also require that all tests pass before a PR will be +merged. + +.. note:: + If you are having issues getting tests to pass on your Pull Request, + please tag any of the core developers to ask for help. + +The Pyomo main branch provides a Github Actions workflow (configured +in the ``.github/`` directory) that will test any changes pushed to +a branch with a subset of the complete test harness that includes +multiple virtual machines (``ubuntu``, ``mac-os``, ``windows``) +and multiple Python versions. For existing forks, fetch and merge +your fork (and branches) with Pyomo's main. For new forks, you will +need to enable GitHub Actions in the 'Actions' tab on your fork. +This will enable the tests to run automatically with each push to your fork. + +At any point in the development cycle, a "work in progress" pull request +may be opened by including '[WIP]' at the beginning of the PR +title. Any pull requests marked '[WIP]' or draft will not be +reviewed or merged by the core development team. However, any +'[WIP]' pull request left open for an extended period of time without +active development may be marked 'stale' and closed. + +.. note:: + Draft and WIP Pull Requests will **NOT** trigger tests. This is an effort to + reduce our CI backlog. Please make use of the provided + branch test suite for evaluating / testing draft functionality. + +Python Version Support +++++++++++++++++++++++ + +By policy, Pyomo supports and tests the currently supported Python versions, +as can be seen on `Status of Python Versions `_. +It is expected that tests will pass for all of the supported and tested +versions of Python, unless otherwise stated. + +At the time of the first Pyomo release after the end-of-life of a minor Python +version, we will remove testing and support for that Python version. + +This will also result in a bump in the minor Pyomo version. + +For example, assume Python 3.A is declared end-of-life while Pyomo is on +version 6.3.Y. After the release of Pyomo 6.3.(Y+1), Python 3.A will be removed, +and the next Pyomo release will be 6.4.0. + +Working on Forks and Branches +----------------------------- + +All Pyomo development should be done on forks of the Pyomo +repository. In order to fork the Pyomo repository, visit +https://github.com/Pyomo/pyomo, click the "Fork" button in the +upper right corner, and follow the instructions. + +This section discusses two recommended workflows for contributing +pull-requests to Pyomo. The first workflow, labeled +:ref:`Working with my fork and the GitHub Online UI `, +does not require the use of 'remotes', and +suggests updating your fork using the GitHub online UI. The second +workflow, labeled +:ref:`Working with remotes and the git command-line `, outlines +a process that defines separate remotes for your fork and the main +Pyomo repository. + +More information on git can be found at +https://git-scm.com/book/en/v2. Section 2.5 has information on working +with remotes. + + +.. _forksgithubui: + +Working with my fork and the GitHub Online UI ++++++++++++++++++++++++++++++++++++++++++++++ + +After creating your fork (per the instructions above), you can +then clone your fork of the repository with + +:: + + git clone https://github.com//pyomo.git + +For new development, we strongly recommend working on feature +branches. When you have a new feature to implement, create +the branch with the following. + +:: + + cd pyomo/ # to make sure you are in the folder managed by git + git branch + git checkout + +Development can now be performed. When you are ready, commit +any changes you make to your local repository. This can be +done multiple times with informative commit messages for +different tasks in the feature development. + +:: + + git add + git status # to check that you have added the correct files + git commit -m 'informative commit message to describe changes' + +In order to push the changes in your local branch to a branch on your fork, use + +:: + + git push origin + + +When you have completed all the changes and are ready for a pull request, make +sure all the changes have been pushed to the branch on your fork. + + * visit https://github.com//pyomo. + * Just above the list of files and directories in the repository, + you should see a button that says "Branch: main". Click on + this button, and choose the correct branch. + * Click the "New pull request" button just to the right of the + "Branch: " button. + * Fill out the pull request template and click the green "Create + pull request" button. + +At times during your development, you may want to merge changes from +the Pyomo main development branch into the feature branch on your +fork and in your local clone of the repository. + +Using GitHub UI to merge Pyomo main into a branch on your fork +**************************************************************** + +To update your fork, you will actually be merging a pull-request from +the head Pyomo repository into your fork. + + * Visit https://github.com/Pyomo/pyomo. + * Click on the "New pull request" button just above the list of + files and directories. + * You will see the title "Compare changes" with some small text + below it which says "Compare changes across branches, commits, + tags, and more below. If you need to, you can also compare + across forks." Click the last part of this: "compare across + forks". + * You should now see four buttons just below this: "base + repository: Pyomo/pyomo", "base: main", "head repository: + Pyomo/pyomo", and "compare: main". Click the leftmost button + and choose "/Pyomo". + * Then click the button which is second to the left, and choose + the branch which you want to merge Pyomo main into. The four + buttons should now read: "base repository: /pyomo", + "base: ", "head repository: Pyomo/pyomo", and + "compare: main". This is setting you up to merge a pull-request + from Pyomo's main branch into your fork's branch. + * You should also now see a pull request template. If you fill out + the pull request template and click "Create pull request", this + will create a pull request which will update your fork and + branch with any changes that have been made to the main branch + of Pyomo. + * You can then merge the pull request by clicking the green "Merge + pull request" button from your fork on GitHub. + +.. _forksremotes: + +Working with remotes and the git command-line ++++++++++++++++++++++++++++++++++++++++++++++ + +After you have created your fork, you can clone the fork and setup +git 'remotes' that allow you to merge changes from (and to) different +remote repositories. Below, we have included a set of recommendations, +but, of course, there are other valid GitHub workflows that you can +adopt. + +The following commands show how to clone your fork and setup +two remotes, one for your fork, and one for the head Pyomo repository. + +:: + + git clone https://github.com//pyomo.git + git remote rename origin my-fork + git remote add head-pyomo https://github.com/pyomo/pyomo.git + +Note, you can see a list of your remotes with + +:: + + git remote -v + +The commands for creating a local branch and performing local commits +are the same as those listed in the previous section above. Below are +some common tasks based on this multi-remote setup. + +If you have changes that have been committed to a local feature branch +(), you can push these changes to the branch on your fork +with, + +:: + + git push my-fork + +In order to update a local branch with changes from a branch of the +Pyomo repository, + +:: + + git checkout + git fetch head-pyomo + git merge head-pyomo/ --ff-only + +The "--ff-only" only allows a merge if the merge can be done by a +fast-forward. If you do not require a fast-forward, you can drop this +option. The most common concrete example of this would be + +:: + + git checkout main + git fetch head-pyomo + git merge head-pyomo/main --ff-only + +The above commands pull changes from the main branch of the head +Pyomo repository into the main branch of your local clone. To push +these changes to the main branch on your fork, + +:: + + git push my-fork main + + +Setting up your development environment ++++++++++++++++++++++++++++++++++++++++ + +After cloning your fork, you will want to install Pyomo from source. + +Step 1 (recommended): Create a new ``conda`` environment. + +:: + + conda create --name pyomodev + +You may change the environment name from ``pyomodev`` as you see fit. +Then activate the environment: + +:: + + conda activate pyomodev + +Step 2 (optional): Install PyUtilib + +The hard dependency on PyUtilib was removed in Pyomo 6.0.0. There is still a +soft dependency for any code related to ``pyomo.dataportal.plugins.sheet``. + +If your contribution requires PyUtilib, you will likely need the main branch of +PyUtilib to contribute. Clone a copy of the repository in a new directory: + +:: + + git clone https://github.com/PyUtilib/pyutilib + +Then in the directory containing the clone of PyUtilib run: + +:: + + python setup.py develop + +Step 3: Install Pyomo + +Finally, move to the directory containing the clone of your Pyomo fork and run: + +:: + + python setup.py develop + +These commands register the cloned code with the active python environment +(``pyomodev``). This way, your changes to the source code for ``pyomo`` are +automatically used by the active environment. You can create another conda +environment to switch to alternate versions of pyomo (e.g., stable). + +Review Process +-------------- + +After a PR is opened it will be reviewed by at least two members of the +core development team. The core development team consists of anyone with +write-access to the Pyomo repository. Pull requests opened by a core +developer only require one review. The reviewers will decide if they +think a PR should be merged or if more changes are necessary. + +Reviewers look for: + + * Outside of ``pyomo.contrib``: Code rigor and standards, edge cases, + side effects, etc. + * Inside of ``pyomo.contrib``: No “glaringly obvious” problems with + the code + * Documentation and tests + +The core development team tries to review pull requests in a timely +manner but we make no guarantees on review timeframes. In addition, PRs +might not be reviewed in the order they are opened in. + +Where to put contributed code +----------------------------- + +In order to contribute to Pyomo, you must first make a fork of the Pyomo +git repository. Next, you should create a branch on your fork dedicated +to the development of the new feature or bug fix you're interested +in. Once you have this branch checked out, you can start coding. Bug +fixes and minor enhancements to existing Pyomo functionality should be +made in the appropriate files in the Pyomo code base. New examples, +features, and packages built on Pyomo should be placed in +``pyomo.contrib``. Follow the link below to find out if +``pyomo.contrib`` is right for your code. + +``pyomo.contrib`` +----------------- + +Pyomo uses the ``pyomo.contrib`` package to facilitate the inclusion +of third-party contributions that enhance Pyomo's core functionality. +The are two ways that ``pyomo.contrib`` can be used to integrate +third-party packages: + +* ``pyomo.contrib`` can provide wrappers for separate Python packages, thereby + allowing these packages to be imported as subpackages of pyomo. + +* ``pyomo.contrib`` can include contributed packages that are developed and + maintained outside of the Pyomo developer team. + +Including contrib packages in the Pyomo source tree provides a +convenient mechanism for defining new functionality that can be +optionally deployed by users. We expect this mechanism to include +Pyomo extensions and experimental modeling capabilities. However, +contrib packages are treated as optional packages, which are not +maintained by the Pyomo developer team. Thus, it is the responsibility +of the code contributor to keep these packages up-to-date. + +Contrib package contributions will be considered as pull-requests, +which will be reviewed by the Pyomo developer team. Specifically, +this review will consider the suitability of the proposed capability, +whether tests are available to check the execution of the code, and +whether documentation is available to describe the capability. +Contrib packages will be tested along with Pyomo. If test failures +arise, then these packages will be disabled and an issue will be +created to resolve these test failures. + +Contrib Packages within Pyomo ++++++++++++++++++++++++++++++ + +Third-party contributions can be included directly within the +``pyomo.contrib`` package. The ``pyomo/contrib/example`` package +provides an example of how this can be done, including a directory +for plugins and package tests. For example, this package can be +imported as a subpackage of ``pyomo.contrib``:: + + from pyomo.environ import * + from pyomo.contrib.example import a + + # Print the value of 'a' defined by this package + print(a) + +Although ``pyomo.contrib.example`` is included in the Pyomo source +tree, it is treated as an optional package. Pyomo will attempt to +import this package, but if an import failure occurs, Pyomo will +silently ignore it. Otherwise, this pyomo package will be treated +like any other. Specifically: + +* Plugin classes defined in this package are loaded when ``pyomo.environ`` is loaded. + +* Tests in this package are run with other Pyomo tests. + diff --git a/doc/OnlineDocs/developer_guide/config.rst b/doc/OnlineDocs/developer_guide/config.rst new file mode 100644 index 00000000000..23d0696ee98 --- /dev/null +++ b/doc/OnlineDocs/developer_guide/config.rst @@ -0,0 +1,3 @@ + +.. automodule:: pyomo.common.config + :noindex: diff --git a/doc/OnlineDocs/developer_guide/deprecation.rst b/doc/OnlineDocs/developer_guide/deprecation.rst new file mode 100644 index 00000000000..7fc5ec2b0ff --- /dev/null +++ b/doc/OnlineDocs/developer_guide/deprecation.rst @@ -0,0 +1,62 @@ +Deprecation and Removal of Functionality +======================================== + +During the course of development, there may be cases where it becomes +necessary to deprecate or remove functionality from the standard Pyomo +offering. + +Deprecation +----------- + +We offer a set of tools to help with deprecation in +``pyomo.common.deprecation``. + +By policy, when deprecating or moving an existing capability, one of the +following utilities should be leveraged. Each has a required +``version`` argument that should be set to current development version (e.g., +``"6.6.2.dev0"``). This version will be updated to the next actual +release as part of the Pyomo release process. The current development version +can be found by running ``pyomo --version`` on your local fork/branch. + +.. currentmodule:: pyomo.common.deprecation + +.. autosummary:: + + deprecated + deprecation_warning + relocated_module + relocated_module_attribute + RenamedClass + +.. autodecorator:: pyomo.common.deprecation.deprecated + :noindex: + +.. autofunction:: pyomo.common.deprecation.deprecation_warning + :noindex: + +.. autofunction:: pyomo.common.deprecation.relocated_module + :noindex: + +.. autofunction:: pyomo.common.deprecation.relocated_module_attribute + :noindex: + +.. autoclass:: pyomo.common.deprecation.RenamedClass + :noindex: + + +Removal +------- + +By policy, functionality should be deprecated with reasonable +warning, pending extenuating circumstances. The functionality should +be deprecated, following the information above. + +If the functionality is documented in the most recent +edition of [`Pyomo - Optimization Modeling in Python`_], it may not be removed +until the next major version release. + +.. _Pyomo - Optimization Modeling in Python: https://doi.org/10.1007/978-3-030-68928-5 + +For other functionality, it is preferred that ample time is given +before removing the functionality. At minimum, significant functionality +removal will result in a minor version bump. diff --git a/doc/OnlineDocs/developer_guide/expressions/design.rst b/doc/OnlineDocs/developer_guide/expressions/design.rst new file mode 100644 index 00000000000..ddecb39ad0c --- /dev/null +++ b/doc/OnlineDocs/developer_guide/expressions/design.rst @@ -0,0 +1,268 @@ +.. |p| raw:: html + +

+ +Design Details +============== + +.. warning:: + Pyomo expression trees are not composed of Python + objects from a single class hierarchy. Consequently, Pyomo + relies on duck typing to ensure that valid expression trees are + created. + +Most Pyomo expression trees have the following form + +1. Interior nodes are objects that inherit from the :class:`ExpressionBase ` class. These objects typically have one or more child nodes. Linear expression nodes do not have child nodes, but they are treated as interior nodes in the expression tree because they references other leaf nodes. + +2. Leaf nodes are numeric values, parameter components and variable components, which represent the *inputs* to the expression. + +Expression Classes +------------------ + +Expression classes typically represent unary and binary operations. The following table +describes the standard operators in Python and their associated Pyomo expression class: + +========== ============= ============================================================================= +Operation Python Syntax Pyomo Class +========== ============= ============================================================================= +sum ``x + y`` :class:`SumExpression ` +product ``x * y`` :class:`ProductExpression ` +negation ``- x`` :class:`NegationExpression ` +division ``x / y`` :class:`DivisionExpression ` +power ``x ** y`` :class:`PowExpression ` +inequality ``x <= y`` :class:`InequalityExpression ` +equality ``x == y`` :class:`EqualityExpression ` +========== ============= ============================================================================= + +Additionally, there are a variety of other Pyomo expression classes that capture more general +logical relationships, which are summarized in the following table: + +==================== ==================================== ======================================================================================== +Operation Example Pyomo Class +==================== ==================================== ======================================================================================== +external function ``myfunc(x,y,z)`` :class:`ExternalFunctionExpression ` +logical if-then-else ``Expr_if(IF=x, THEN=y, ELSE=z)`` :class:`Expr_ifExpression ` +intrinsic function ``sin(x)`` :class:`UnaryFunctionExpression ` +absolute function ``abs(x)`` :class:`AbsExpression ` +==================== ==================================== ======================================================================================== + +Expression objects are immutable. Specifically, the list of +arguments to an expression object (a.k.a. the list of child nodes +in the tree) cannot be changed after an expression class is +constructed. To enforce this property, expression objects have a +standard API for accessing expression arguments: + +* :attr:`args` - a class property that returns a generator that yields the expression arguments +* :attr:`arg(i)` - a function that returns the ``i``-th argument +* :attr:`nargs()` - a function that returns the number of expression arguments + +.. warning:: + + Developers should never use the :attr:`_args_` property directly! + The semantics for the use of this data has changed since earlier + versions of Pyomo. For example, in some expression classes the + the value :func:`nargs()` may not equal :const:`len(_args_)`! + +Expression trees can be categorized in four different ways: + +* constant expressions - expressions that do not contain numeric constants and immutable parameters. +* mutable expressions - expressions that contain mutable parameters but no variables. +* potentially variable expressions - expressions that contain variables, which may be fixed. +* fixed expressions - expressions that contain variables, all of which are fixed. + +These three categories are illustrated with the following example: + +.. literalinclude:: ../../src/expr/design_categories.spy + +The following table describes four different simple expressions +that consist of a single model component, and it shows how they +are categorized: + +======================== ===== ===== ===== ===== +Category m.p m.q m.x m.y +======================== ===== ===== ===== ===== +constant True False False False +not potentially variable True True False False +potentially_variable False False True True +fixed True True False True +======================== ===== ===== ===== ===== + +Expressions classes contain methods to test whether an expression +tree is in each of these categories. Additionally, Pyomo includes +custom expression classes for expression trees that are *not potentially +variable*. These custom classes will not normally be used by +developers, but they provide an optimization of the checks for +potentially variability. + +Special Expression Classes +-------------------------- + +The following classes are *exceptions* to the design principles describe above. + +Named Expressions +~~~~~~~~~~~~~~~~~ + +Named expressions allow for changes to an expression after it has +been constructed. For example, consider the expression ``f`` defined +with the :class:`Expression ` component: + +.. literalinclude:: ../../src/expr/design_named_expression.spy + +Although ``f`` is an immutable expression, whose definition is +fixed, a sub-expressions is the named expression ``M.e``. Named +expressions have a mutable value. In other words, the expression +that they point to can change. Thus, a change to the value of +``M.e`` changes the expression tree for any expression that includes +the named expression. + +.. note:: + + The named expression classes are not implemented as sub-classes + of :class:`NumericExpression `. + This reflects design constraints related to the fact that these + are modeling components that belong to class hierarchies other + than the expression class hierarchy, and Pyomo's design prohibits + the use of multiple inheritance for these classes. + +Linear Expressions +~~~~~~~~~~~~~~~~~~ + +Pyomo includes a special expression class for linear expressions. +The class :class:`LinearExpression +` provides a compact +description of linear polynomials. Specifically, it includes a +constant value :attr:`constant` and two lists for coefficients and +variables: :attr:`linear_coefs` and :attr:`linear_vars`. + +This expression object does not have arguments, and thus it is +treated as a leaf node by Pyomo visitor classes. Further, the +expression API functions described above do not work with this +class. Thus, developers need to treat this class differently when +walking an expression tree (e.g. when developing a problem +transformation). + +Sum Expressions +~~~~~~~~~~~~~~~ + +Pyomo does not have a binary sum expression class. Instead, +it has an ``n``-ary summation class, :class:`SumExpression +`. This expression class +treats sums as ``n``-ary sums for efficiency reasons; many large +optimization models contain large sums. But note that this class +maintains the immutability property described above. This class +shares an underlying list of arguments with other :class:`SumExpression +` objects. A particular +object owns the first ``n`` arguments in the shared list, but +different objects may have different values of ``n``. + +This class acts like a normal immutable expression class, and the +API described above works normally. But direct access to the shared +list could have unexpected results. + +Mutable Expressions +~~~~~~~~~~~~~~~~~~~ + +Finally, Pyomo includes several **mutable** expression classes +that are private. These are not intended to be used by users, but +they might be useful for developers in contexts where the developer +can appropriately control how the classes are used. Specifically, +immutability eliminates side-effects where changes to a sub-expression +unexpectedly create changes to the expression tree. But within the context of +model transformations, developers may be able to limit the use of +expressions to avoid these side-effects. The following mutable private classes +are available in Pyomo: + +:class:`_MutableSumExpression ` + This class + is used in the :data:`nonlinear_expression ` context manager to + efficiently combine sums of nonlinear terms. +:class:`_MutableLinearExpression ` + This class + is used in the :data:`linear_expression ` context manager to + efficiently combine sums of linear terms. + + + +Expression Semantics +-------------------- + +Pyomo clear semantics regarding what is considered a valid leaf and +interior node. + +The following classes are valid interior nodes: + +* Subclasses of :class:`ExpressionBase ` + +* Classes that that are *duck typed* to match the API of the :class:`ExpressionBase ` class. For example, the named expression class :class:`Expression `. + +The following classes are valid leaf nodes: + +* Members of :data:`nonpyomo_leaf_types `, which includes standard numeric data types like :const:`int`, :const:`float` and :const:`long`, as well as numeric data types defined by `numpy` and other commonly used packages. This set also includes :class:`NonNumericValue `, which is used to wrap non-numeric arguments to the :class:`ExternalFunctionExpression ` class. + +* Parameter component classes like :class:`ScalarParam ` and :class:`_ParamData `, which arise in expression trees when the parameters are declared as mutable. (Immutable parameters are identified when generating expressions, and they are replaced with their associated numeric value.) + +* Variable component classes like :class:`ScalarVar ` and :class:`_GeneralVarData `, which often arise in expression trees. `. + +.. note:: + + In some contexts the :class:`LinearExpression + ` class can be treated + as an interior node, and sometimes it can be treated as a leaf. + This expression object does not have any child arguments, so + ``nargs()`` is zero. But this expression references variables + and parameters in a linear expression, so in that sense it does + not represent a leaf node in the tree. + + + +Context Managers +---------------- + +Pyomo defines several context managers that can be used to declare +the form of expressions, and to define a mutable expression object that +efficiently manages sums. + +The :data:`linear_expression ` +object is a context manager that can be used to declare a linear sum. For +example, consider the following two loops: + +.. literalinclude:: ../../src/expr/design_cm1.spy + +The first apparent difference in these loops is that the value of +``s`` is explicitly initialized while ``e`` is initialized when the +context manager is entered. However, a more fundamental difference +is that the expression representation for ``s`` differs from ``e``. +Each term added to ``s`` results in a new, immutable expression. +By contrast, the context manager creates a mutable expression +representation for ``e``. This difference allows for both (a) a +more efficient processing of each sum, and (b) a more compact +representation for the expression. + +The difference between :data:`linear_expression +` and +:data:`nonlinear_expression ` +is the underlying representation that each supports. Note that +both of these are instances of context manager classes. In +singled-threaded applications, these objects can be safely used to +construct different expressions with different context declarations. + +Finally, note that these context managers can be passed into the :attr:`start` +method for the :func:`quicksum ` function. For example: + +.. literalinclude:: ../../src/expr/design_cm2.spy + +This sum contains terms for ``M.x[i]`` and ``M.y[i]``. The syntax +in this example is not intuitive because the sum is being stored +in ``e``. + +.. note:: + + We do not generally expect users or developers to use these + context managers. They are used by the :func:`quicksum + ` and :func:`sum_product + ` functions to accelerate expression + generation, and there are few cases where the direct use of + these context managers would provide additional utility to users + and developers. + diff --git a/doc/OnlineDocs/developer_guide/expressions/index.rst b/doc/OnlineDocs/developer_guide/expressions/index.rst new file mode 100644 index 00000000000..685fde25173 --- /dev/null +++ b/doc/OnlineDocs/developer_guide/expressions/index.rst @@ -0,0 +1,55 @@ +.. |p| raw:: html + +

+ +Pyomo Expressions +================= + +.. warning:: + + This documentation does not explicitly reference objects in + pyomo.core.kernel. While the Pyomo5 expression system works + with pyomo.core.kernel objects, the documentation of these + documents was not sufficient to appropriately describe the use + of kernel objects in expressions. + +Pyomo supports the declaration of symbolic expressions that represent +objectives, constraints and other optimization modeling components. +Pyomo expressions are represented in an expression tree, where the +leaves are operands, such as constants or variables, and the internal +nodes contain operators. Pyomo relies on so-called magic methods +to automate the construction of symbolic expressions. For example, +consider an expression ``e`` declared as follows: + +.. literalinclude:: ../../src/expr/index_simple.spy + +Python determines that the magic method ``__mul__`` is called on +the ``M.v`` object, with the argument ``2``. This method returns +a Pyomo expression object ``ProductExpression`` that has arguments +``M.v`` and ``2``. This represents the following symbolic expression +tree: + +.. graphviz:: + + digraph foo { + "*" -> "v"; + "*" -> "2"; + } + +.. note:: + + End-users will not likely need to know details related to how + symbolic expressions are generated and managed in Pyomo. Thus, + most of the following documentation of expressions in Pyomo is most + useful for Pyomo developers. However, the discussion of runtime + performance in the first section will help end-users write large-scale + models. + +.. toctree:: + :maxdepth: 1 + + performance.rst + overview.rst + design.rst + managing.rst + diff --git a/doc/OnlineDocs/developer_guide/expressions/managing.rst b/doc/OnlineDocs/developer_guide/expressions/managing.rst new file mode 100644 index 00000000000..a4dd2a51436 --- /dev/null +++ b/doc/OnlineDocs/developer_guide/expressions/managing.rst @@ -0,0 +1,272 @@ +.. |p| raw:: html + +

+ +Managing Expressions +==================== + +Creating a String Representation of an Expression +------------------------------------------------- + +There are several ways that string representations can be created +from an expression, but the :func:`expression_to_string +` function provides +the most flexible mechanism for generating a string representation. +The options to this function control distinct aspects of the string +representation. + +Algebraic vs. Nested Functional Form +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The default string representation is an algebraic form, which closely +mimics the Python operations used to construct an expression. The +:data:`verbose` flag can be set to :const:`True` to generate a +string representation that is a nested functional form. For example: + +.. literalinclude:: ../../src/expr/managing_ex1.spy + +Labeler and Symbol Map +~~~~~~~~~~~~~~~~~~~~~~ + +The string representation used for variables in expression can be +customized to define different label formats. If the :data:`labeler` +option is specified, then this function (or class functor) is used to +generate a string label used to represent the variable. Pyomo defines a +variety of labelers in the `pyomo.core.base.label` module. For example, +the :class:`NumericLabeler` defines a functor that can be used to +sequentially generate simple labels with a prefix followed by the +variable count: + +.. literalinclude:: ../../src/expr/managing_ex2.spy + +The :data:`smap` option is used to specify a symbol map object +(:class:`SymbolMap `), which +caches the variable label data. This option is normally specified +in contexts where the string representations for many expressions +are being generated. In that context, a symbol map ensures that +variables in different expressions have a consistent label in their +associated string representations. + + +Other Ways to Generate String Representations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There are two other standard ways to generate string representations: + +* Call the :func:`__str__` magic method (e.g. using the Python + :func:`str()` function. This calls :func:`expression_to_string + `, using the default values for + all arguments. + +* Call the :func:`to_string` method on the + :class:`ExpressionBase` class. This + calls :func:`expression_to_string + ` and accepts the same arguments. + + +Evaluating Expressions +---------------------- + +Expressions can be evaluated when all variables and parameters in +the expression have a value. The :func:`value ` +function can be used to walk the expression tree and compute the +value of an expression. For example: + +.. literalinclude:: ../../src/expr/managing_ex5.spy + +Additionally, expressions define the :func:`__call__` method, so the +following is another way to compute the value of an expression: + +.. literalinclude:: ../../src/expr/managing_ex6.spy + +If a parameter or variable is undefined, then the :func:`value +` function and :func:`__call__` method will +raise an exception. This exception can be suppressed using the +:attr:`exception` option. For example: + +.. literalinclude:: ../../src/expr/managing_ex7.spy + +This option is useful in contexts where adding a try block is inconvenient +in your modeling script. + +.. note:: + + Both the :func:`value ` function and + :func:`__call__` method call the :func:`evaluate_expression + ` function. In + practice, this function will be slightly faster, but the + difference is only meaningful when expressions are evaluated + many times. + +Identifying Components and Variables +------------------------------------ + +Expression transformations sometimes need to find all nodes in an +expression tree that are of a given type. Pyomo contains two utility +functions that support this functionality. First, the +:func:`identify_components ` +function is a generator function that walks the expression tree and yields all +nodes whose type is in a specified set of node types. For example: + +.. literalinclude:: ../../src/expr/managing_ex8.spy + +The :func:`identify_variables ` +function is a generator function that yields all nodes that are +variables. Pyomo uses several different classes to represent variables, +but this set of variable types does not need to be specified by the user. +However, the :attr:`include_fixed` flag can be specified to omit fixed +variables. For example: + +.. literalinclude:: ../../src/expr/managing_ex9.spy + +Walking an Expression Tree with a Visitor Class +----------------------------------------------- + +Many of the utility functions defined above are implemented by +walking an expression tree and performing an operation at nodes in +the tree. For example, evaluating an expression is performed using +a post-order depth-first search process where the value of a node +is computed using the values of its children. + +Walking an expression tree can be tricky, and the code requires intimate +knowledge of the design of the expression system. Pyomo includes +several classes that define visitor patterns for walking expression +tree: + +:class:`StreamBasedExpressionVisitor ` + The most general and extensible visitor class. This visitor + implements an event-based approach for walking the tree inspired by + the ``expat`` library for processing XML files. The visitor has + seven event callbacks that users can hook into, providing very + fine-grained control over the expression walker. + +:class:`SimpleExpressionVisitor ` + A :func:`visitor` method is called for each node in the tree, + and the visitor class collects information about the tree. + +:class:`ExpressionValueVisitor ` + When the :func:`visitor` method is called on each node in the + tree, the *values* of its children have been computed. The + *value* of the node is returned from :func:`visitor`. + +:class:`ExpressionReplacementVisitor ` + When the :func:`visitor` method is called on each node in the + tree, it may clone or otherwise replace the node using objects + for its children (which themselves may be clones or replacements + from the original child objects). The new node object is + returned from :func:`visitor`. + +These classes define a variety of suitable tree search methods: + +* :class:`StreamBasedExpressionVisitor ` + + * ``walk_expression``: depth-first traversal of the expression tree. + +* :class:`ExpressionReplacementVisitor ` + + * ``walk_expression``: depth-first traversal of the expression tree. + +* :class:`SimpleExpressionVisitor ` + + * ``xbfs``: breadth-first search where leaf nodes are immediately visited + * ``xbfs_yield_leaves``: breadth-first search where leaf nodes are + immediately visited, and the visit method yields a value + +* :class:`ExpressionValueVisitor ` + + * ``dfs_postorder_stack``: postorder depth-first search using a + nonrecursive stack + + +To implement a visitor object, a user needs to provide specializations +for specific events. For legacy visitors based on the PyUtilib +visitor pattern (e.g., :class:`SimpleExpressionVisitor` and +:class:`ExpressionValueVisitor`), one must create a subclass of one of these +classes and override at least one of the following: + +:func:`visitor` + Defines the operation that is performed when a node is visited. In + the :class:`ExpressionValueVisitor + ` and + :class:`ExpressionReplacementVisitor + ` visitor classes, + this method returns a value that is used by its parent node. + +:func:`visiting_potential_leaf` + Checks if the search should terminate with this node. If no, + then this method returns the tuple ``(False, None)``. If yes, + then this method returns ``(False, value)``, where *value* is + computed by this method. This method is not used in the + :class:`SimpleExpressionVisitor + ` visitor + class. + +:func:`finalize` + This method defines the final value that is returned from the + visitor. This is not normally redefined. + +For modern visitors based on the :class:`StreamBasedExpressionVisitor +`, one can either define a +subclass, pass the callbacks to an instance of the base class, or assign +the callbacks as attributes on an instance of the base class. The +:class:`StreamBasedExpressionVisitor +` provides seven +callbacks, which are documented in the class documentation. + +Detailed documentation of the APIs for these methods is provided +with the class documentation for these visitors. + +SimpleExpressionVisitor Example +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In this example, we describe an visitor class that counts the number +of nodes in an expression (including leaf nodes). Consider the following +class: + +.. literalinclude:: ../../src/expr/managing_visitor1.spy + +The class constructor creates a counter, and the :func:`visit` method +increments this counter for every node that is visited. The :func:`finalize` +method returns the value of this counter after the tree has been walked. The +following function illustrates this use of this visitor class: + +.. literalinclude:: ../../src/expr/managing_visitor2.spy + + +ExpressionValueVisitor Example +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In this example, we describe an visitor class that clones the +expression tree (including leaf nodes). Consider the following +class: + +.. literalinclude:: ../../src/expr/managing_visitor3.spy + +The :func:`visit` method creates a new expression node with children +specified by :attr:`values`. The :func:`visiting_potential_leaf` +method performs a :func:`deepcopy` on leaf nodes, which are native +Python types or non-expression objects. + +.. literalinclude:: ../../src/expr/managing_visitor4.spy + + +ExpressionReplacementVisitor Example +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In this example, we describe an visitor class that replaces +variables with scaled variables, using a mutable parameter that +can be modified later. the following +class: + +.. literalinclude:: ../../src/expr/managing_visitor5.spy + +No other method need to be defined. The +:func:`beforeChild` method identifies variable nodes +and returns a product expression that contains a mutable parameter. + +.. literalinclude:: ../../src/expr/managing_visitor6.spy + +The :func:`scale_expression` function is called with an expression and +a dictionary, :attr:`scale`, that maps variable ID to model parameter. For example: + +.. literalinclude:: ../../src/expr/managing_visitor7.spy diff --git a/doc/OnlineDocs/developer_guide/expressions/overview.rst b/doc/OnlineDocs/developer_guide/expressions/overview.rst new file mode 100644 index 00000000000..c1962edec22 --- /dev/null +++ b/doc/OnlineDocs/developer_guide/expressions/overview.rst @@ -0,0 +1,300 @@ +.. |p| raw:: html + +

+ +Design Overview +=============== + +Historical Comparison +--------------------- + +This document describes the "Pyomo5" expressions, which were +introduced in Pyomo 5.6. The main differences between "Pyomo5" +expressions and the previous expression system, called "Coopr3", +are: + +* Pyomo5 supports both CPython and PyPy implementations of Python, + while Coopr3 only supports CPython. + + The key difference in these implementations is that Coopr3 relies + on CPython reference counting, which is not part of the Python + language standard. Hence, this implementation is not guaranteed + to run on other implementations of Python. + + Pyomo5 does not rely on reference counting, and it has been tested + with PyPy. In the future, this should allow Pyomo to support + other Python implementations (e.g. Jython). + + |p| + +* Pyomo5 expression objects are immutable, while Coopr3 expression + objects are mutable. + + This difference relates to how expression objects are managed + in Pyomo. Once created, Pyomo5 expression objects cannot be + changed. Further, the user is guaranteed that no "side effects" + occur when expressions change at a later point in time. By + contrast, Coopr3 allows expressions to change in-place, and thus + "side effects" make occur when expressions are changed at a later + point in time. (See discussion of entanglement below.) + + |p| + +* Pyomo5 provides more consistent runtime performance than Coopr3. + + While this documentation does not provide a detailed comparison + of runtime performance between Coopr3 and Pyomo5, the following + performance considerations also motivated the creation of Pyomo5: + + * There were surprising performance inconsistencies in Coopr3. For + example, the following two loops had dramatically different + runtime: + + .. literalinclude:: ../../src/expr/overview_example1.spy + + * Coopr3 eliminates side effects by automatically cloning sub-expressions. + Unfortunately, this can easily lead to unexpected cloning in models, which + can dramatically slow down Pyomo model generation. For example: + + .. literalinclude:: ../../src/expr/overview_example2.spy + + * Coopr3 leverages recursion in many operations, including expression + cloning. Even simple non-linear expressions can result in deep + expression trees where these recursive operations fail because + Python runs out of stack space. + + |p| + + * The immutable representation used in Pyomo5 requires more memory allocations + than Coopr3 in simple loops. Hence, a pure-Python execution of Pyomo5 + can be 10% slower than Coopr3 for model construction. But when Cython is used + to optimize the execution of Pyomo5 expression generation, the + runtimes for Pyomo5 and Coopr3 are about the same. (In principle, + Cython would improve the runtime of Coopr3 as well, but the limitations + noted above motivated a new expression system in any case.) + +Expression Entanglement and Mutability +-------------------------------------- + +Pyomo fundamentally relies on the use of magic methods in Python +to generate expression trees, which means that Pyomo has very limited +control for how expressions are managed in Python. For example: + +* Python variables can point to the same expression tree + + .. literalinclude:: ../../src/expr/overview_tree1.spy + + This is illustrated as follows: + + .. graphviz:: + + digraph foo { + { + e [shape=box] + f [shape=box] + } + "*" -> 2; + "*" -> v; + subgraph cluster { "*"; 2; v; } + e -> "*" [splines=curved, style=dashed]; + f -> "*" [splines=curved, style=dashed]; + } + +* A variable can point to a sub-tree that another variable points to + + .. literalinclude:: ../../src/expr/overview_tree2.spy + + This is illustrated as follows: + + .. graphviz:: + + digraph foo { + { + e [shape=box] + f [shape=box] + } + "*" -> 2; + "*" -> v; + "+" -> "*"; + "+" -> 3; + subgraph cluster { "+"; 3; "*"; 2; v; } + e -> "*" [splines=curved, style=dashed, constraint=false]; + f -> "+" [splines=curved, style=dashed]; + } + +* Two expression trees can point to the same sub-tree + + .. literalinclude:: ../../src/expr/overview_tree3.spy + + This is illustrated as follows: + + .. graphviz:: + + digraph foo { + { + e [shape=box] + f [shape=box] + g [shape=box] + } + x [label="+"]; + "*" -> 2; + "*" -> v; + "+" -> "*"; + "+" -> 3; + x -> 4; + x -> "*"; + subgraph cluster { x; 4; "+"; 3; "*"; 2; v; } + e -> "*" [splines=curved, style=dashed, constraint=false]; + f -> "+" [splines=curved, style=dashed]; + g -> x [splines=curved, style=dashed]; + } + +In each of these examples, it is almost impossible for a Pyomo user +or developer to detect whether expressions are being shared. In +CPython, the reference counting logic can support this to a limited +degree. But no equivalent mechanisms are available in PyPy and +other Python implementations. + +Entangled Sub-Expressions +~~~~~~~~~~~~~~~~~~~~~~~~~ + +We say that expressions are *entangled* if they share one or more +sub-expressions. The first example above does not represent +entanglement, but rather the fact that multiple Python variables +can point to the same expression tree. In the second and third +examples, the expressions are entangled because the subtree represented +by ``e`` is shared. However, if a leave node like ``M.v`` is shared +between expressions, we do not consider those expressions entangled. + +Expression entanglement is problematic because shared expressions complicate +the expected behavior when sub-expressions are changed. Consider the following example: + +.. literalinclude:: ../../src/expr/overview_tree4.spy + +What is the value of ``e`` after ``M.w`` is added to it? What is the +value of ``f``? The answers to these questions are not immediately +obvious, and the fact that Coopr3 uses mutable expression objects +makes them even less clear. However, Pyomo5 and Coopr3 enforce +the following semantics: + +.. pull-quote:: + + A change to an expression *e* that is a sub-expression of *f* + does not change the expression tree for *f*. + +This property ensures a change to an expression does not create side effects that change the +values of other, previously defined expressions. + +For instance, the previous example results in the following (in Pyomo5): + +.. graphviz:: + + digraph foo { + { + e [shape=box] + f [shape=box] + } + x [label="+"]; + "*" -> 2; + "*" -> v; + "+" -> "*"; + "+" -> 3; + x -> "*"; + x -> w; + subgraph cluster { "+"; 3; "*"; 2; v; x; w;} + f -> "+" [splines=curved, style=dashed]; + e -> x [splines=curved, style=dashed]; + } + +With Pyomo5 expressions, each sub-expression is immutable. Thus, +the summation operation generates a new expression ``e`` without +changing existing expression objects referenced in the expression +tree for ``f``. By contrast, Coopr3 imposes the same property by +cloning the expression ``e`` before added ``M.w``, resulting in the following: + +.. graphviz:: + + digraph foo { + { + e [shape=box] + f [shape=box] + } + "*" -> 2; + "*" -> v; + "+" -> "*"; + "+" -> 3; + etimes [label="*"]; + etwo [label=2]; + etimes -> etwo; + etimes -> v; + x [label="+"]; + x -> w; + x -> etimes; + subgraph cluster { "+"; 3; "*"; 2; v; x; w; etimes; etwo;} + f -> "+" [splines=curved, style=dashed]; + e -> x [splines=curved, style=dashed]; + } + +This example also illustrates that leaves may be shared between expressions. + +Mutable Expression Components +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There is one important exception to the entanglement property +described above. The ``Expression`` component is treated as a +mutable expression when shared between expressions. For example: + +.. literalinclude:: ../../src/expr/overview_tree5.spy + +Here, the expression ``M.e`` is a so-called *named expression* that +the user has declared. Named expressions are explicitly intended +for re-use within models, and they provide a convenient mechanism +for changing sub-expressions in complex applications. In this example, the +expression tree is as follows before ``M.w`` is added: + +.. graphviz:: + + digraph foo { + { + f [shape=box] + } + "*" -> 2; + "*" -> v; + "+" -> "M.e"; + "+" -> 3; + "M.e" -> "*"; + subgraph cluster { "+"; 3; "*"; 2; v; "M.e";} + f -> "+" [splines=curved, style=dashed]; + } + + +And the expression tree is as follows after ``M.w`` is added. + +.. graphviz:: + + digraph foo { + { + f [shape=box] + } + x [label="+"]; + "*" -> 2; + "*" -> v; + "+" -> "M.e"; + "+" -> 3; + x -> "*"; + x -> w; + "M.e" -> x; + subgraph cluster { "+"; 3; "*"; 2; v; "M.e"; x; w;} + f -> "+" [splines=curved, style=dashed]; + } + + +When considering named expressions, Pyomo5 and Coopr3 enforce +the following semantics: + +.. pull-quote:: + + A change to a named expression *e* that is a sub-expression of + *f* changes the expression tree for *f*, because *f* continues + to point to *e* after it is changed. + diff --git a/doc/OnlineDocs/developer_guide/expressions/performance.rst b/doc/OnlineDocs/developer_guide/expressions/performance.rst new file mode 100644 index 00000000000..8e344e50982 --- /dev/null +++ b/doc/OnlineDocs/developer_guide/expressions/performance.rst @@ -0,0 +1,171 @@ +.. |p| raw:: html + +

+ +Building Expressions Faster +=========================== + +Expression Generation +--------------------- + +Pyomo expressions can be constructed using native binary operators +in Python. For example, a sum can be created in a simple loop: + +.. literalinclude:: ../../src/expr/performance_loop1.spy + +Additionally, Pyomo expressions can be constructed using functions +that iteratively apply Python binary operators. For example, the +Python :func:`sum` function can be used to replace the previous +loop: + +.. literalinclude:: ../../src/expr/performance_loop2.spy + +The :func:`sum` function is both more compact and more efficient. +Using :func:`sum` avoids the creation of temporary variables, and +the summation logic is executed in the Python interpreter while the +loop is interpreted. + + +Linear, Quadratic and General Nonlinear Expressions +--------------------------------------------------- + +Pyomo can express a very wide range of algebraic expressions, and +there are three general classes of expressions that are recognized +by Pyomo: + + * **linear polynomials** + * **quadratic polynomials** + * **nonlinear expressions**, including higher-order polynomials and + expressions with intrinsic functions + +These classes of expressions are leveraged to efficiently generate +compact representations of expressions, and to transform expression +trees into standard forms used to interface with solvers. Note +that There not all quadratic polynomials are recognized by Pyomo; +in other words, some quadratic expressions are treated as nonlinear +expressions. + +For example, consider the following quadratic polynomial: + +.. literalinclude:: ../../src/expr/performance_loop3.spy + +This quadratic polynomial is treated as a nonlinear expression +unless the expression is explicitly processed to identify quadratic +terms. This *lazy* identification of of quadratic terms allows +Pyomo to tailor the search for quadratic terms only when they are +explicitly needed. + +Pyomo Utility Functions +----------------------- + +Pyomo includes several similar functions that can be used to +create expressions: + +:func:`prod ` + A function to compute a product of Pyomo expressions. + +:func:`quicksum ` + A function to efficiently compute a sum of Pyomo expressions. + +:func:`sum_product ` + A function that computes a generalized dot product. + +prod +~~~~ + +The :func:`prod ` function is analogous to the builtin +:func:`sum` function. Its main argument is a variable length +argument list, :attr:`args`, which represents expressions that are multiplied +together. For example: + +.. literalinclude:: ../../src/expr/performance_prod.spy + +quicksum +~~~~~~~~ + +The behavior of the :func:`quicksum ` function is +similar to the builtin :func:`sum` function, but this function often +generates a more compact Pyomo expression. Its main argument is a +variable length argument list, :attr:`args`, which represents +expressions that are summed together. For example: + +.. literalinclude:: ../../src/expr/performance_quicksum.spy + +The summation is customized based on the :attr:`start` and +:attr:`linear` arguments. The :attr:`start` defines the initial +value for summation, which defaults to zero. If :attr:`start` is +a numeric value, then the :attr:`linear` argument determines how +the sum is processed: + +* If :attr:`linear` is :const:`False`, then the terms in :attr:`args` are assumed to be nonlinear. +* If :attr:`linear` is :const:`True`, then the terms in :attr:`args` are assumed to be linear. +* If :attr:`linear` is :const:`None`, the first term in :attr:`args` is analyze to determine whether the terms are linear or nonlinear. + +This argument allows the :func:`quicksum ` +function to customize the expression representation used, and +specifically a more compact representation is used for linear +polynomials. The :func:`quicksum ` +function can be slower than the builtin :func:`sum` function, +but this compact representation can generate problem representations +more quickly. + +Consider the following example: + +.. literalinclude:: ../../src/expr/quicksum_runtime.spy + +The sum consists of linear terms because the exponents are one. +The following output illustrates that quicksum can identify this +linear structure to generate expressions more quickly: + +.. literalinclude:: ../../src/expr/quicksum.log + :language: none + +If :attr:`start` is not a numeric value, then the :func:`quicksum +` sets the initial value to :attr:`start` +and executes a simple loop to sum the terms. This allows the sum +to be stored in an object that is passed into the function (e.g. the linear context manager +:data:`linear_expression `). + +.. Warning:: + + By default, :attr:`linear` is :const:`None`. While this allows + for efficient expression generation in normal cases, there are + circumstances where the inspection of the first + term in :attr:`args` is misleading. Consider the following + example: + + .. literalinclude:: ../../src/expr/performance_warning.spy + + The first term created by the generator is linear, but the + subsequent terms are nonlinear. Pyomo gracefully transitions + to a nonlinear sum, but in this case :func:`quicksum ` + is doing additional work that is not useful. + +sum_product +~~~~~~~~~~~ + +The :func:`sum_product ` function supports +a generalized dot product. The :attr:`args` argument contains one +or more components that are used to create terms in the summation. +If the :attr:`args` argument contains a single components, then its +sequence of terms are summed together; the sum is equivalent to +calling :func:`quicksum `. If two or more components are +provided, then the result is the summation of their terms multiplied +together. For example: + +.. literalinclude:: ../../src/expr/performance_sum_product1.spy + +The :attr:`denom` argument specifies components whose terms are in +the denominator. For example: + +.. literalinclude:: ../../src/expr/performance_sum_product2.spy + +The terms summed by this function are explicitly specified, so +:func:`sum_product ` can identify +whether the resulting expression is linear, quadratic or nonlinear. +Consequently, this function is typically faster than simple loops, +and it generates compact representations of expressions.. + +Finally, note that the :func:`dot_product ` +function is an alias for :func:`sum_product `. + diff --git a/doc/OnlineDocs/developer_guide/future.rst b/doc/OnlineDocs/developer_guide/future.rst new file mode 100644 index 00000000000..531c0fdb5c6 --- /dev/null +++ b/doc/OnlineDocs/developer_guide/future.rst @@ -0,0 +1,3 @@ + +.. automodule:: pyomo.__future__ + :noindex: diff --git a/doc/OnlineDocs/developer_guide/index.rst b/doc/OnlineDocs/developer_guide/index.rst new file mode 100644 index 00000000000..9adf9eb2648 --- /dev/null +++ b/doc/OnlineDocs/developer_guide/index.rst @@ -0,0 +1,15 @@ +Developer Guide +=============== + +This guide describes utilities and design philosophies useful for Pyomo +developers or anyone interested in developing packages that use or +interrogate Pyomo models. + +.. toctree:: + :maxdepth: 1 + + Configuration System + Deprecation System + Expression System + Future Feature Preview + Solver Interfaces diff --git a/doc/OnlineDocs/developer_guide/solvers.rst b/doc/OnlineDocs/developer_guide/solvers.rst new file mode 100644 index 00000000000..9e3281246f4 --- /dev/null +++ b/doc/OnlineDocs/developer_guide/solvers.rst @@ -0,0 +1,351 @@ +Future Solver Interface Changes +=============================== + +.. note:: + + The new solver interfaces are still under active development. They + are included in the releases as development previews. Please be + aware that APIs and functionality may change with no notice. + + We welcome any feedback and ideas as we develop this capability. + Please post feedback on + `Issue 1030 `_. + +Pyomo offers interfaces into multiple solvers, both commercial and open +source. To support better capabilities for solver interfaces, the Pyomo +team is actively redesigning the existing interfaces to make them more +maintainable and intuitive for use. A preview of the redesigned +interfaces can be found in ``pyomo.contrib.solver``. + +.. currentmodule:: pyomo.contrib.solver + + +New Interface Usage +------------------- + +The new interfaces are not completely backwards compatible with the +existing Pyomo solver interfaces. However, to aid in testing and +evaluation, we are distributing versions of the new solver interfaces +that are compatible with the existing ("legacy") solver interface. +These "legacy" interfaces are registered with the current +``SolverFactory`` using slightly different names (to avoid conflicts +with existing interfaces). + +.. |br| raw:: html + +
+ +.. list-table:: Available Redesigned Solvers and Names Registered + in the SolverFactories + :header-rows: 1 + + * - Solver + - Name registered in the |br| ``pyomo.contrib.solver.factory.SolverFactory`` + - Name registered in the |br| ``pyomo.opt.base.solvers.LegacySolverFactory`` + * - Ipopt + - ``ipopt`` + - ``ipopt_v2`` + * - Gurobi (persistent) + - ``gurobi`` + - ``gurobi_v2`` + * - Gurobi (direct) + - ``gurobi_direct`` + - ``gurobi_direct_v2`` + +Using the new interfaces through the legacy interface +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here we use the new interface as exposed through the existing (legacy) +solver factory and solver interface wrapper. This provides an API that +is compatible with the existing (legacy) Pyomo solver interface and can +be used with other Pyomo tools / capabilities. + +.. testcode:: + :skipif: not ipopt_available + + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + status = pyo.SolverFactory('ipopt_v2').solve(model) + assert_optimal_termination(status) + model.pprint() + +.. testoutput:: + :skipif: not ipopt_available + :hide: + + 2 Var Declarations + ... + 3 Declarations: x y obj + +In keeping with our commitment to backwards compatibility, both the legacy and +future methods of specifying solver options are supported: + +.. testcode:: + :skipif: not ipopt_available + + import pyomo.environ as pyo + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + # Backwards compatible + status = pyo.SolverFactory('ipopt_v2').solve(model, options={'max_iter' : 6}) + # Forwards compatible + status = pyo.SolverFactory('ipopt_v2').solve(model, solver_options={'max_iter' : 6}) + model.pprint() + +.. testoutput:: + :skipif: not ipopt_available + :hide: + + 2 Var Declarations + ... + 3 Declarations: x y obj + +Using the new interfaces directly +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here we use the new interface by importing it directly: + +.. testcode:: + :skipif: not ipopt_available + + # Direct import + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + from pyomo.contrib.solver.ipopt import Ipopt + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + opt = Ipopt() + status = opt.solve(model) + assert_optimal_termination(status) + # Displays important results information; only available through the new interfaces + status.display() + model.pprint() + +.. testoutput:: + :skipif: not ipopt_available + :hide: + + solution_loader: ... + ... + 3 Declarations: x y obj + +Using the new interfaces through the "new" SolverFactory +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Here we use the new interface by retrieving it from the new ``SolverFactory``: + +.. testcode:: + :skipif: not ipopt_available + + # Import through new SolverFactory + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + from pyomo.contrib.solver.factory import SolverFactory + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + opt = SolverFactory('ipopt') + status = opt.solve(model) + assert_optimal_termination(status) + # Displays important results information; only available through the new interfaces + status.display() + model.pprint() + +.. testoutput:: + :skipif: not ipopt_available + :hide: + + solution_loader: ... + ... + 3 Declarations: x y obj + +Switching all of Pyomo to use the new interfaces +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We also provide a mechanism to get a "preview" of the future where we +replace the existing (legacy) SolverFactory and utilities with the new +(development) version (see :doc:`future`): + +.. testcode:: + :skipif: not ipopt_available + + # Change default SolverFactory version + import pyomo.environ as pyo + from pyomo.contrib.solver.util import assert_optimal_termination + from pyomo.__future__ import solver_factory_v3 + + model = pyo.ConcreteModel() + model.x = pyo.Var(initialize=1.5) + model.y = pyo.Var(initialize=1.5) + + def rosenbrock(model): + return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 + + model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) + + status = pyo.SolverFactory('ipopt').solve(model) + assert_optimal_termination(status) + # Displays important results information; only available through the new interfaces + status.display() + model.pprint() + +.. testoutput:: + :skipif: not ipopt_available + :hide: + + solution_loader: ... + ... + 3 Declarations: x y obj + +.. testcode:: + :skipif: not ipopt_available + :hide: + + from pyomo.__future__ import solver_factory_v1 + +Linear Presolve and Scaling +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The new interface allows access to new capabilities in the various +problem writers, including the linear presolve and scaling options +recently incorporated into the redesigned NL writer. For example, you +can control the NL writer in the new ``ipopt`` interface through the +solver's ``writer_config`` configuration option: + +.. autoclass:: pyomo.contrib.solver.ipopt.Ipopt + :members: solve + +.. testcode:: + + from pyomo.contrib.solver.ipopt import Ipopt + opt = Ipopt() + opt.config.writer_config.display() + +.. testoutput:: + + show_section_timing: false + skip_trivial_constraints: true + file_determinism: FileDeterminism.ORDERED + symbolic_solver_labels: false + scale_model: true + export_nonlinear_variables: None + row_order: None + column_order: None + export_defined_variables: true + linear_presolve: true + +Note that, by default, both ``linear_presolve`` and ``scale_model`` are enabled. +Users can manipulate ``linear_presolve`` and ``scale_model`` to their preferred +states by changing their values. + +.. code-block:: python + + >>> opt.config.writer_config.linear_presolve = False + + +Interface Implementation +------------------------ + +All new interfaces should be built upon one of two classes (currently): +:class:`SolverBase` or +:class:`PersistentSolverBase`. + +All solvers should have the following: + +.. autoclass:: pyomo.contrib.solver.base.SolverBase + :members: + +Persistent solvers include additional members as well as other configuration options: + +.. autoclass:: pyomo.contrib.solver.base.PersistentSolverBase + :show-inheritance: + :members: + +Results +------- + +Every solver, at the end of a +:meth:`solve` call, will +return a :class:`Results` +object. This object is a :py:class:`pyomo.common.config.ConfigDict`, +which can be manipulated similar to a standard ``dict`` in Python. + +.. autoclass:: pyomo.contrib.solver.results.Results + :show-inheritance: + :members: + :undoc-members: + + +Termination Conditions +^^^^^^^^^^^^^^^^^^^^^^ + +Pyomo offers a standard set of termination conditions to map to solver +returns. The intent of +:class:`TerminationCondition` +is to notify the user of why the solver exited. The user is expected +to inspect the :class:`Results` +object or any returned solver messages or logs for more information. + +.. autoclass:: pyomo.contrib.solver.results.TerminationCondition + :show-inheritance: + + +Solution Status +^^^^^^^^^^^^^^^ + +Pyomo offers a standard set of solution statuses to map to solver +output. The intent of +:class:`SolutionStatus` +is to notify the user of what the solver returned at a high level. The +user is expected to inspect the +:class:`Results` object or any +returned solver messages or logs for more information. + +.. autoclass:: pyomo.contrib.solver.results.SolutionStatus + :show-inheritance: + + +Solution +-------- + +Solutions can be loaded back into a model using a ``SolutionLoader``. A specific +loader should be written for each unique case. Several have already been +implemented. For example, for ``ipopt``: + +.. autoclass:: pyomo.contrib.solver.ipopt.IpoptSolutionLoader + :show-inheritance: + :members: + :inherited-members: diff --git a/doc/OnlineDocs/docutils.conf b/doc/OnlineDocs/docutils.conf new file mode 100644 index 00000000000..84f89f45e9b --- /dev/null +++ b/doc/OnlineDocs/docutils.conf @@ -0,0 +1,2 @@ +[writers] +table_style=colwidths-auto diff --git a/doc/OnlineDocs/getting_started/index.rst b/doc/OnlineDocs/getting_started/index.rst new file mode 100644 index 00000000000..2cdea1bdf8f --- /dev/null +++ b/doc/OnlineDocs/getting_started/index.rst @@ -0,0 +1,5 @@ +Getting Started +=============== + +TOOO + diff --git a/doc/OnlineDocs/getting_started/installation.rst b/doc/OnlineDocs/getting_started/installation.rst new file mode 100644 index 00000000000..83cd08e7a4a --- /dev/null +++ b/doc/OnlineDocs/getting_started/installation.rst @@ -0,0 +1,99 @@ +Installation +------------ + +Pyomo currently supports the following versions of Python: + +* CPython: 3.8, 3.9, 3.10, 3.11, 3.12 +* PyPy: 3 + +At the time of the first Pyomo release after the end-of-life of a minor Python +version, Pyomo will remove testing for that Python version. + +Using CONDA +~~~~~~~~~~~ + +We recommend installation with ``conda``, which is included with the +Anaconda distribution of Python. You can install Pyomo in your system +Python installation by executing the following in a shell: + +:: + + conda install -c conda-forge pyomo + +Optimization solvers are not installed with Pyomo, but some open source +optimization solvers can be installed with ``conda`` as well: + +:: + + conda install -c conda-forge ipopt glpk + + +Using PIP +~~~~~~~~~ + +The standard utility for installing Python packages is ``pip``. You +can install Pyomo in your system Python installation by executing +the following in a shell: + +:: + + pip install pyomo + + +Conditional Dependencies +~~~~~~~~~~~~~~~~~~~~~~~~ + +Extensions to Pyomo, and many of the contributions in ``pyomo.contrib``, +often have conditional dependencies on a variety of third-party Python +packages including but not limited to: matplotlib, networkx, numpy, +openpyxl, pandas, pint, pymysql, pyodbc, pyro4, scipy, sympy, and +xlrd. + +A full list of conditional dependencies can be found in Pyomo's +``setup.py`` and displayed using: + +:: + + python setup.py dependencies --extra optional + +Pyomo extensions that require any of these packages will generate +an error message for missing dependencies upon use. + +When using *pip*, all conditional dependencies can be installed at once +using the following command: + +:: + + pip install 'pyomo[optional]' + +When using *conda*, many of the conditional dependencies are included +with the standard Anaconda installation. + +You can check which Python packages you have installed using the command +``conda list`` or ``pip list``. Additional Python packages may be +installed as needed. + + +Installation with Cython +~~~~~~~~~~~~~~~~~~~~~~~~ + +Users can opt to install Pyomo with +`cython `_ +initialized. + +.. note:: + This can only be done via ``pip`` or from source. + +Via ``pip``: + +:: + + pip install pyomo --global-option="--with-cython" + +From source (recommended for advanced users only): + +:: + + git clone https://github.com/Pyomo/pyomo.git + cd pyomo + python setup.py install --with-cython diff --git a/doc/OnlineDocs/getting_started/pyomo_overview/abstract_concrete.rst b/doc/OnlineDocs/getting_started/pyomo_overview/abstract_concrete.rst new file mode 100644 index 00000000000..24f2f2b8187 --- /dev/null +++ b/doc/OnlineDocs/getting_started/pyomo_overview/abstract_concrete.rst @@ -0,0 +1,54 @@ +Abstract Versus Concrete Models +------------------------------- + +A mathematical model can be defined using symbols that represent data +values. For example, the following equations represent a linear program +(LP) to find optimal values for the vector :math:`x` with parameters +:math:`n` and :math:`b`, and parameter vectors :math:`a` and :math:`c`: + +.. math:: + :nowrap: + + \begin{array}{lll} + \min & \sum_{j=1}^n c_j x_j &\\ + \mathrm{s.t.} & \sum_{j=1}^n a_{ij} x_j \geq b_i & \forall i = 1 \ldots m\\ + & x_j \geq 0 & \forall j = 1 \ldots n + \end{array} + +.. note:: + + As a convenience, we use the symbol :math:`\forall` to mean "for all" + or "for each." + +We call this an *abstract* or *symbolic* mathematical model since it +relies on unspecified parameter values. Data values can be used to +specify a *model instance*. The ``AbstractModel`` class provides a +context for defining and initializing abstract optimization models in +Pyomo when the data values will be supplied at the time a solution is to +be obtained. + +In many contexts, a mathematical model can and should be directly +defined with the data values supplied at the time of the model +definition. We call these *concrete* mathematical models. For example, +the following LP model is a concrete instance of the previous abstract +model: + +.. math:: + :nowrap: + + \begin{array}{ll} + \min & 2 x_1 + 3 x_2\\ + \mathrm{s.t.} & 3 x_1 + 4 x_2 \geq 1\\ + & x_1, x_2 \geq 0 + \end{array} + +The ``ConcreteModel`` class is used to define concrete optimization +models in Pyomo. + +.. note:: + + Python programmers will probably prefer to write concrete models, + while users of some other algebraic modeling languages may tend to + prefer to write abstract models. The choice is largely a matter of + taste; some applications may be a little more straightforward using + one or the other. diff --git a/doc/OnlineDocs/getting_started/pyomo_overview/index.rst b/doc/OnlineDocs/getting_started/pyomo_overview/index.rst new file mode 100644 index 00000000000..91400825977 --- /dev/null +++ b/doc/OnlineDocs/getting_started/pyomo_overview/index.rst @@ -0,0 +1,10 @@ +Pyomo Overview +============== + +.. toctree:: + :maxdepth: 1 + + math_modeling.rst + overview_components.rst + abstract_concrete.rst + simple_examples.rst diff --git a/doc/OnlineDocs/getting_started/pyomo_overview/math_modeling.rst b/doc/OnlineDocs/getting_started/pyomo_overview/math_modeling.rst new file mode 100644 index 00000000000..ccacca8d58d --- /dev/null +++ b/doc/OnlineDocs/getting_started/pyomo_overview/math_modeling.rst @@ -0,0 +1,102 @@ +Mathematical Modeling +--------------------- + +This section provides an introduction to Pyomo: Python Optimization +Modeling Objects. A more complete description is contained in the +[PyomoBookIII]_ book. Pyomo supports the formulation and analysis of +mathematical models for complex optimization applications. This +capability is commonly associated with commercially available algebraic +modeling languages (AMLs) such as [AMPL]_, [AIMMS]_, and [GAMS]_. +Pyomo's modeling objects are embedded within Python, a full-featured, +high-level programming language that contains a rich set of supporting +libraries. + +Modeling is a fundamental process in many aspects of scientific +research, engineering and business. Modeling involves the formulation +of a simplified representation of a system or real-world object. Thus, +modeling tools like Pyomo can be used in a variety of ways: + +- *Explain phenomena* that arise in a system, + +- *Make predictions* about future states of a system, + +- *Assess key factors* that influence phenomena in a system, + +- *Identify extreme states* in a system, that might represent worst-case + scenarios or minimal cost plans, and + +- *Analyze trade-offs* to support human decision makers. + +Mathematical models represent system knowledge with a formalized +language. The following mathematical concepts are central to modern +modeling activities: + +Variables +********* + + Variables represent unknown or changing parts of a model (e.g., + whether or not to make a decision, or the characteristic of a system + outcome). The values taken by the variables are often referred to as + a *solution* and are usually an output of the optimization process. + +Parameters +********** + + Parameters represents the data that must be supplied to perform the + optimization. In fact, in some settings the word *data* is used in + place of the word *parameters*. + +Relations +********* + + These are equations, inequalities or other mathematical + relationships that define how different parts of a model are + connected to each other. + +Goals +***** + + These are functions that reflect goals and objectives for the system + being modeled. + +The widespread availability of computing resources has made the +numerical analysis of mathematical models a commonplace activity. +Without a modeling language, the process of setting up input files, +executing a solver and extracting the final results from the solver +output is tedious and error-prone. This difficulty is compounded in +complex, large-scale real-world applications which are difficult to +debug when errors occur. Additionally, there are many different formats +used by optimization software packages, and few formats are recognized +by many optimizers. Thus the application of multiple optimization +solvers to analyze a model introduces additional complexities. + + +Pyomo is an AML that extends Python to include objects for mathematical +modeling. [PyomoBookI]_, [PyomoBookII]_, [PyomoBookIII]_, and [PyomoJournal]_ +compare Pyomo with other AMLs. Although many good AMLs have been developed for +optimization models, the following are motivating factors for the +development of Pyomo: + +- *Open Source* + + Pyomo is developed within Pyomo's open source project to promote + transparency of the modeling framework and encourage community + development of Pyomo capabilities. + +- *Customizable Capability* + + Pyomo supports a customizable capability through the extensive use + of plug-ins to modularize software components. + +- *Solver Integration* + + Pyomo models can be optimized with solvers that are written either + in Python or in compiled, low-level languages. + +- *Programming Language* + + Pyomo leverages a high-level programming language, which has several + advantages over custom AMLs: a very robust language, extensive + documentation, a rich set of standard libraries, support for modern + programming features like classes and functions, and portability to + many platforms. diff --git a/doc/OnlineDocs/getting_started/pyomo_overview/overview_components.rst b/doc/OnlineDocs/getting_started/pyomo_overview/overview_components.rst new file mode 100644 index 00000000000..679e2d9bd8b --- /dev/null +++ b/doc/OnlineDocs/getting_started/pyomo_overview/overview_components.rst @@ -0,0 +1,42 @@ +Overview of Modeling Components and Processes +--------------------------------------------- + +Pyomo supports an object-oriented design for the definition of +optimization models. The basic steps of a simple modeling process are: + +* Create model and declare components +* Instantiate the model +* Apply solver +* Interrogate solver results + +In practice, these steps may be applied repeatedly with different data +or with different constraints applied to the model. However, we focus +on this simple modeling process to illustrate different strategies for +modeling with Pyomo. + +A Pyomo *model* consists of a collection of modeling *components* that +define different aspects of the model. Pyomo includes the modeling +components that are commonly supported by modern AMLs: index sets, +symbolic parameters, decision variables, objectives, and constraints. +These modeling components are defined in Pyomo through the following +Python classes: + +Set +*** + set data that is used to define a model instance + +Param +***** + parameter data that is used to define a model instance + +Var +*** + decision variables in a model + +Objective +********* + expressions that are minimized or maximized in a model + +Constraint +********** + constraint expressions that impose restrictions on variable values in a model diff --git a/doc/OnlineDocs/getting_started/pyomo_overview/simple_examples.rst b/doc/OnlineDocs/getting_started/pyomo_overview/simple_examples.rst new file mode 100644 index 00000000000..11305884c54 --- /dev/null +++ b/doc/OnlineDocs/getting_started/pyomo_overview/simple_examples.rst @@ -0,0 +1,416 @@ +Simple Models +============= + +A Simple Concrete Pyomo Model +***************************** + +It is possible to get the same flexible behavior from models +declared to be abstract and models declared to be concrete in Pyomo; +however, we will focus on a straightforward concrete example here where +the data is hard-wired into the model file. Python programmers will +quickly realize that the data could have come from other sources. + +Given the following model from the previous section: + +.. math:: + :nowrap: + + \begin{array}{ll} + \min & 2 x_1 + 3 x_2\\ + \mathrm{s.t.} & 3 x_1 + 4 x_2 \geq 1\\ + & x_1, x_2 \geq 0 + \end{array} + +This can be implemented as a concrete model as follows: + +.. testcode:: + + import pyomo.environ as pyo + + model = pyo.ConcreteModel() + + model.x = pyo.Var([1,2], domain=pyo.NonNegativeReals) + + model.OBJ = pyo.Objective(expr = 2*model.x[1] + 3*model.x[2]) + + model.Constraint1 = pyo.Constraint(expr = 3*model.x[1] + 4*model.x[2] >= 1) + +Although rule functions can also be used to specify constraints and +objectives, in this example we use the ``expr`` option that is available +only in concrete models. This option gives a direct specification of the +expression. + + +A Simple Abstract Pyomo Model +***************************** +We repeat the abstract model from the previous section: + +.. math:: + :nowrap: + + \begin{array}{lll} + \min & \sum_{j=1}^n c_j x_j &\\ + \mathrm{s.t.} & \sum_{j=1}^n a_{ij} x_j \geq b_i & \forall i = 1 \ldots m\\ + & x_j \geq 0 & \forall j = 1 \ldots n + \end{array} + +One way to implement this in Pyomo is as shown as follows: + +.. testcode:: + + import pyomo.environ as pyo + + model = pyo.AbstractModel() + + model.m = pyo.Param(within=pyo.NonNegativeIntegers) + model.n = pyo.Param(within=pyo.NonNegativeIntegers) + + model.I = pyo.RangeSet(1, model.m) + model.J = pyo.RangeSet(1, model.n) + + model.a = pyo.Param(model.I, model.J) + model.b = pyo.Param(model.I) + model.c = pyo.Param(model.J) + + # the next line declares a variable indexed by the set J + model.x = pyo.Var(model.J, domain=pyo.NonNegativeReals) + + def obj_expression(m): + return pyo.summation(m.c, m.x) + + model.OBJ = pyo.Objective(rule=obj_expression) + + def ax_constraint_rule(m, i): + # return the expression for the constraint for i + return sum(m.a[i,j] * m.x[j] for j in m.J) >= m.b[i] + + # the next line creates one constraint for each member of the set model.I + model.AxbConstraint = pyo.Constraint(model.I, rule=ax_constraint_rule) + +.. doctest:: + :hide: + + >>> # Create an instance to verify that the rules fire correctly + >>> inst = model.create_instance('src/scripting/abstract1.dat') + +.. note:: + + Python is interpreted one line at a time. A line continuation + character, ``\`` (backslash), is used for Python statements that need to span + multiple lines. In Python, indentation has meaning and must be + consistent. For example, lines inside a function definition must be + indented and the end of the indentation is used by Python to signal + the end of the definition. + +We will now examine the lines in this example. +The first import line is required in every Pyomo model. Its purpose +is to make the symbols used by Pyomo known to Python. + +.. testcode:: + + import pyomo.environ as pyo + +The declaration of a model is also required. The use of the name ``model`` +is not required. Almost any name could be used, but we will use the name +``model`` in most of our examples. In this example, we are declaring +that it will be an abstract model. + +.. testcode:: + + model = pyo.AbstractModel() + +We declare the parameters :math:`m` and :math:`n` using the Pyomo +:class:`Param` component. This component can take a variety of arguments; this +example illustrates use of the ``within`` option that is used by Pyomo +to validate the data value that is assigned to the parameter. If this +option were not given, then Pyomo would not object to any type of data +being assigned to these parameters. As it is, assignment of a value that +is not a non-negative integer will result in an error. + +.. testcode:: + + model.m = pyo.Param(within=pyo.NonNegativeIntegers) + model.n = pyo.Param(within=pyo.NonNegativeIntegers) + +Although not required, it is convenient to define index sets. In this +example we use the :class:`RangeSet` component to declare that the sets will +be a sequence of integers starting at 1 and ending at a value specified +by the the parameters ``model.m`` and ``model.n``. + +.. testcode:: + + model.I = pyo.RangeSet(1, model.m) + model.J = pyo.RangeSet(1, model.n) + +The coefficient and right-hand-side data are defined as indexed +parameters. When sets are given as arguments to the :class:`Param` component, +they indicate that the set will index the parameter. + +.. testcode:: + + model.a = pyo.Param(model.I, model.J) + model.b = pyo.Param(model.I) + model.c = pyo.Param(model.J) + +The next line that is interpreted by Python as part of the model +declares the variable :math:`x`. The first argument to the :class:`Var` +component is a set, so it is defined as an index set for the variable. In +this case the variable has only one index set, but multiple sets could +be used as was the case for the declaration of the parameter +``model.a``. The second argument specifies a domain for the +variable. This information is part of the model and will passed to the +solver when data is provided and the model is solved. Specification of +the ``NonNegativeReals`` domain implements the requirement that the +variables be greater than or equal to zero. + +.. testcode:: + + # the next line declares a variable indexed by the set J + model.x = pyo.Var(model.J, domain=pyo.NonNegativeReals) + +.. note:: + + In Python, and therefore in Pyomo, any text after pound sign is + considered to be a comment. + +In abstract models, Pyomo expressions are usually provided to objective +and constraint declarations via a function defined with a +Python ``def`` statement. The ``def`` statement establishes a name for a +function along with its arguments. When Pyomo uses a function to get +objective or constraint expressions, it always passes in the +model (i.e., itself) as the the first argument so the model is always +the first formal argument when declaring such functions in Pyomo. +Additional arguments, if needed, follow. Since summation is an extremely +common part of optimization models, Pyomo provides a flexible function +to accommodate it. When given two arguments, the :func:`summation()` function +returns an expression for the sum of the product of the two arguments +over their indexes. This only works, of course, if the two arguments +have the same indexes. If it is given only one argument it returns an +expression for the sum over all indexes of that argument. So in this +example, when :func:`summation` is passed the arguments ``m.c, m.x`` +it returns an internal representation of the expression +:math:`\sum_{j=1}^{n}c_{j} x_{j}`. + +.. testcode:: + + def obj_expression(m): + return pyo.summation(m.c, m.x) + +To declare an objective function, the Pyomo component called +:class:`Objective` is used. The ``rule`` argument gives the name of a +function that returns the objective expression. The default *sense* is +minimization. For maximization, the ``sense=pyo.maximize`` argument must be +used. The name that is declared, which is ``OBJ`` in this case, appears +in some reports and can be almost any name. + +.. testcode:: + + model.OBJ = pyo.Objective(rule=obj_expression) + +Declaration of constraints is similar. A function is declared to generate +the constraint expression. In this case, there can be multiple +constraints of the same form because we index the constraints by +:math:`i` in the expression :math:`\sum_{j=1}^n a_{ij} x_j \geq b_i +\;\;\forall i = 1 \ldots m`, which states that we need a constraint for +each value of :math:`i` from one to :math:`m`. In order to parametrize +the expression by :math:`i` we include it as a formal parameter to the +function that declares the constraint expression. Technically, we could +have used anything for this argument, but that might be confusing. Using +an ``i`` for an :math:`i` seems sensible in this situation. + +.. testcode:: + + def ax_constraint_rule(m, i): + # return the expression for the constraint for i + return sum(m.a[i,j] * m.x[j] for j in m.J) >= m.b[i] + +.. note:: + + In Python, indexes are in square brackets and function arguments are + in parentheses. + +In order to declare constraints that use this expression, we use the +Pyomo :class:`Constraint` component that takes a variety of arguments. In this +case, our model specifies that we can have more than one constraint of +the same form and we have created a set, ``model.I``, over which these +constraints can be indexed so that is the first argument to the +constraint declaration. The next argument gives the rule that +will be used to generate expressions for the constraints. Taken as a +whole, this constraint declaration says that a list of constraints +indexed by the set ``model.I`` will be created and for each member of +``model.I``, the function ``ax_constraint_rule`` will be called and it +will be passed the model object as well as the member of ``model.I`` + +.. testcode:: + + # the next line creates one constraint for each member of the set model.I + model.AxbConstraint = pyo.Constraint(model.I, rule=ax_constraint_rule) + +In the object oriented view of all of this, we would say that ``model`` +object is a class instance of the :class:`AbstractModel` class, and +``model.J`` is a :class:`Set` object that is contained by this model. Many +modeling components in Pyomo can be optionally specified as *indexed* +*components*: collections of components that are referenced using one or +more values. In this example, the parameter ``model.c`` is indexed with +set ``model.J``. + +In order to use this model, data must be given for the values of the +parameters. Here is one file that provides data (in AMPL "``.dat``" format). + +.. doctest:: + :hide: + + >>> # Create an instance to verify that the rules fire correctly + >>> inst = model.create_instance('src/scripting/abstract1.dat') + +.. literalinclude:: ../src/scripting/abstract1.dat + :language: text + +There are multiple formats that can be used to provide data to a Pyomo +model, but the AMPL format works well for our purposes because it +contains the names of the data elements together with the data. In AMPL +data files, text after a pound sign is treated as a comment. Lines +generally do not matter, but statements must be terminated with a +semi-colon. + +For this particular data file, there is one constraint, so the value of +``model.m`` will be one and there are two variables (i.e., the vector +``model.x`` is two elements long) so the value of ``model.n`` will be +two. These two assignments are accomplished with standard +assignments. Notice that in AMPL format input, the name of the model is +omitted. + +:: + + param m := 1 ; + param n := 2 ; + +There is only one constraint, so only two values are needed for +``model.a``. When assigning values to arrays and vectors in AMPL format, +one way to do it is to give the index(es) and the the value. The line 1 +2 4 causes ``model.a[1,2]`` to get the value +4. Since ``model.c`` has only one index, only one index value is needed +so, for example, the line 1 2 causes ``model.c[1]`` to get the +value 2. Line breaks generally do not matter in AMPL format data files, +so the assignment of the value for the single index of ``model.b`` is +given on one line since that is easy to read. + +:: + + param a := + 1 1 3 + 1 2 4 + ; + + param c:= + 1 2 + 2 3 + ; + + param b := 1 1 ; + +.. _abstract2.py: + +.. _abstract2.dat: + +Symbolic Index Sets +******************* + +When working with Pyomo (or any other AML), it is convenient to write +abstract models in a somewhat more abstract way by using index sets that +contain strings rather than index sets that are implied by +:math:`1,\ldots,m` or the summation from 1 to :math:`n`. When this is +done, the size of the set is implied by the input, rather than specified +directly. Furthermore, the index entries may have no real order. Often, +a mixture of integers and indexes and strings as indexes is needed in +the same model. To start with an illustration of general indexes, +consider a slightly different Pyomo implementation of the model we just +presented. + +.. literalinclude:: ../src/scripting/abstract2.py + :language: python + +To get the same instantiated model, the following data file can be used. + +.. literalinclude:: ../src/scripting/abstract2a.dat + :language: none + +However, this model can also be fed different data for problems of the +same general form using meaningful indexes. + +.. literalinclude:: ../src/scripting/abstract2.dat + :language: none + + +Solving the Simple Examples +*************************** + +Pyomo supports modeling and scripting but does not install a solver +automatically. In order to solve a model, there must be a solver +installed on the computer to be used. If there is a solver, then the +``pyomo`` command can be used to solve a problem instance. + +Suppose that the solver named glpk (also known as glpsol) is installed +on the computer. Suppose further that an abstract model is in the file +named ``abstract1.py`` and a data file for it is in the file named +``abstract1.dat``. From the command prompt, with both files in the +current directory, a solution can be obtained with the command: + +:: + + pyomo solve abstract1.py abstract1.dat --solver=glpk + +Since glpk is the default solver, there really is no need specify it so +the ``--solver`` option can be dropped. + +.. note:: + + There are two dashes before the command line option names such as + ``solver``. + +To continue the example, if CPLEX is installed then it can be listed as +the solver. The command to solve with CPLEX is + +:: + + pyomo solve abstract1.py abstract1.dat --solver=cplex + +This yields the following output on the screen: + +:: + + [ 0.00] Setting up Pyomo environment + [ 0.00] Applying Pyomo preprocessing actions + [ 0.07] Creating model + [ 0.15] Applying solver + [ 0.37] Processing results + Number of solutions: 1 + Solution Information + Gap: 0.0 + Status: optimal + Function Value: 0.666666666667 + Solver results file: results.json + [ 0.39] Applying Pyomo postprocessing actions + [ 0.39] Pyomo Finished + +The numbers in square brackets indicate how much time was required for +each step. Results are written to the file named ``results.json``, which +has a special structure that makes it useful for post-processing. To see +a summary of results written to the screen, use the ``--summary`` +option: + +:: + + pyomo solve abstract1.py abstract1.dat --solver=cplex --summary + +To see a list of Pyomo command line options, use: + +:: + + pyomo solve --help + +.. note:: + + There are two dashes before ``help``. + +For a concrete model, no data file is specified on the Pyomo command line. diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst new file mode 100644 index 00000000000..d074b8ce29c --- /dev/null +++ b/doc/OnlineDocs/index.rst @@ -0,0 +1,71 @@ +Pyomo Documentation |release| +============================= + +.. image:: /../logos/pyomo/PyomoNewBlue3.png + :scale: 10% + :align: right + +Pyomo is a Python-based, open-source optimization modeling language +with a diverse set of optimization capabilities. + + +.. list-table:: + :class: index-table + + * - Getting Started + | :doc:`Installation ` + - User Guide + | :doc:`User guide index ` + * - Developer Guide + | :doc:`Index ` + - Reference Guide + | :doc:`Library Reference ` + + +.. toctree:: + :hidden: + :maxdepth: 1 + + index + Getting Started + User Guide + Developer Guide + Reference Guide + + +Pyomo Resources +--------------- + +Pyomo development is hosted at GitHub: + +* https://github.com/Pyomo/pyomo + +See the Pyomo Forum for online discussions of Pyomo or to ask a question: + +* http://groups.google.com/group/pyomo-forum/ + +Ask a question on StackOverflow using the `#pyomo` tag: + +* https://stackoverflow.com/questions/ask?tags=pyomo + + +Contributing to Pyomo +--------------------- + +Interested in contributing code or documentation to the project? Check out our +:doc:`Contribution Guide ` + +Related Packages +---------------- + +Pyomo is a key dependency for a number of other software packages for +specific domains or customized solution strategies. A non-comprehensive +list of Pyomo-related packages may be found :doc:`here `. + + +Citing Pyomo +------------ + +Bynum, Michael L., Gabriel A. Hackebeil, William E. Hart, Carl D. Laird, Bethany L. Nicholson, John D. Siirola, Jean-Paul Watson, and David L. Woodruff. Pyomo - Optimization Modeling in Python, 3rd Edition. Springer, 2021. + +Hart, William E., Jean-Paul Watson, and David L. Woodruff. "Pyomo: modeling and solving mathematical programs in Python." Mathematical Programming Computation 3, no. 3 (2011): 219-260. diff --git a/doc/OnlineDocs/reference_guide/bibliography.rst b/doc/OnlineDocs/reference_guide/bibliography.rst new file mode 100644 index 00000000000..c12d3f81d8c --- /dev/null +++ b/doc/OnlineDocs/reference_guide/bibliography.rst @@ -0,0 +1,68 @@ +Bibliography +============ + +.. [AMPL] R. Fourer, D. M. Gay, and B. W. Kernighan. AMPL: A Modeling + Language for Mathematical Programming, 2nd Edition. Duxbury + Press, 2002. + +.. [AIMMS] http://www.aimms.com/ + +.. [GAMS] http://www.gams.com + +.. [Isenberg_et_al] Isenberg, NM, Akula, P, Eslick, JC, Bhattacharyya, D, + Miller, DC, Gounaris, CE. A generalized cutting‐set approach for + nonlinear robust optimization in process systems + engineering. AIChE J. 2021; 67:e17175. DOI `10.1002/aic.17175 + `_ + +.. [mpisppy] Bernard Knueven, David Mildebrath, Christopher Muir, + John D Siirola, Jean-Paul Watson, and David L Woodruff, A Parallel + Hub-and-Spoke System for Large-Scale Scenario-Based Optimization + Under Uncertainty, pre-print, 2020 + +.. [ParmestPaper] Katherine A. Klise, Bethany L. Nicholson, Andrea + Staid, David L.Woodruff. Parmest: Parameter Estimation Via Pyomo. + Computer Aided Chemical Engineering, 47 (2019): 41-46. + +.. [PyomoBookI] William E. Hart, Carl D. Laird, Jean-Paul Watson, + David L. Woodruff. Pyomo – Optimization Modeling in + Python, Springer, 2012. + +.. [PyomoBookII] W. E. Hart, C. D. Laird, + J.-P. Watson, D. L. Woodruff, G. A. Hackebeil, B. L. Nicholson, + J. D. Siirola. Pyomo - Optimization Modeling in Python, + 2nd Edition. Springer Optimization and Its + Applications, Vol 67. Springer, 2017. + +.. [PyomoBookIII] Bynum, Michael L., Gabriel A. Hackebeil, + William E. Hart, Carl D. Laird, Bethany L. Nicholson, + John D. Siirola, Jean-Paul Watson, and David L. Woodruff. + Pyomo - Optimization Modeling in Python, 3rd Edition. + Vol. 67. Springer, 2021. + doi: `10.1007/978-3-030-68928-5 + `_ + +.. [PyomoJournal] William E. Hart, Jean-Paul Watson, David L. Woodruff. + "Pyomo: modeling and solving mathematical programs in + Python," Mathematical Programming Computation, Volume + 3, Number 3, August 2011 + +.. [PyomoDAE] Bethany Nicholson, John D. Siirola, Jean-Paul Watson, + Victor M. Zavala, and Lorenz T. Biegler. "pyomo.dae: a + modeling and automatic discretization framework for + optimization with differential and algebraic equations." + Mathematical Programming Computation 10(2) (2018): + 187-223. + +.. [RooneyBiegler] W.C. Rooney, L.T. Biegler, "Design for model parameter + uncertainty using nonlinear confidence regions", AIChE + Journal, 47(8), 2001 + +.. [SemiBatch] O. Abel, W. Marquardt, "Scenario-integrated modeling and + optimization of dynamic systems", AIChE Journal, 46(4), 2000 + +.. [Vielma_et_al] J. P. Vielma, S. Ahmed, G. Nemhauser. "Mixed-Integer + Models for Non-separable Piecewise Linear + Optimization: Unifying framework and Extensions", + Operations Research 58, 2010. pp. 303-315. + diff --git a/doc/OnlineDocs/reference_guide/index.rst b/doc/OnlineDocs/reference_guide/index.rst new file mode 100644 index 00000000000..3092b706b14 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/index.rst @@ -0,0 +1,9 @@ +Reference Guide +=============== + +Bibliography +------------ + +:doc:`Bibliography ` + + diff --git a/doc/OnlineDocs/reference_guide/library_reference/aml/index.rst b/doc/OnlineDocs/reference_guide/library_reference/aml/index.rst new file mode 100644 index 00000000000..f06ca35b087 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/aml/index.rst @@ -0,0 +1,85 @@ +AML Library Reference +===================== + +The following modeling components make up the core of the Pyomo +Algebraic Modeling Language (AML). These classes are all available +through the `pyomo.environ` namespace. + +.. currentmodule:: pyomo.environ + +.. autosummary:: + + ConcreteModel + AbstractModel + Block + Set + RangeSet + Param + Var + Objective + Constraint + ExternalFunction + Reference + SOSConstraint + + +AML Component Documentation +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: ConcreteModel + :show-inheritance: + :members: + :inherited-members: + +.. autoclass:: AbstractModel + :show-inheritance: + :members: + :inherited-members: + +.. autoclass:: Block + :show-inheritance: + :members: + :inherited-members: + +.. autoclass:: Constraint + :show-inheritance: + :members: + :inherited-members: + +.. autoclass:: ExternalFunction + :show-inheritance: + :special-members: __init__ + :members: + :inherited-members: + +.. autoclass:: Objective + :show-inheritance: + :members: + :inherited-members: + +.. autoclass:: Param + :show-inheritance: + :members: + :inherited-members: + +.. autoclass:: RangeSet + :show-inheritance: + :members: + :inherited-members: + +.. autofunction:: Reference + +.. autoclass:: Set + :show-inheritance: + :members: + :inherited-members: + +.. autoclass:: Var + :show-inheritance: + :members: + :inherited-members: + +.. autoclass:: SOSConstraint + :show-inheritance: + :members: + :inherited-members: + diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.base.rst b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.base.rst new file mode 100644 index 00000000000..1b6d5761182 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.base.rst @@ -0,0 +1,47 @@ +APPSI Base Classes +================== + +.. autoclass:: pyomo.contrib.appsi.base.TerminationCondition + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pyomo.contrib.appsi.base.Results + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pyomo.contrib.appsi.base.Solver + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pyomo.contrib.appsi.base.PersistentSolver + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pyomo.contrib.appsi.base.SolverConfig + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: NoArgument + +.. autoclass:: pyomo.contrib.appsi.base.MIPSolverConfig + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: NoArgument + +.. autoclass:: pyomo.contrib.appsi.base.UpdateConfig + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: NoArgument diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.rst b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.rst new file mode 100644 index 00000000000..e26e4b0e82a --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.rst @@ -0,0 +1,106 @@ +.. _api_documentation: + +APPSI +===== + +Auto-Persistent Pyomo Solver Interfaces + +.. automodule:: pyomo.contrib.appsi + :members: + :show-inheritance: + +.. toctree:: + + appsi.base + appsi.solvers + +APPSI solver interfaces are designed to work very similarly to most +Pyomo solver interfaces but are very efficient for resolving the same +model with small changes. This is very beneficial for applications +such as Benders' Decomposition, Optimization-Based Bounds Tightening, +Progressive Hedging, Outer-Approximation, and many others. Here is an +example of using an APPSI solver interface. + +.. code-block:: python + + >>> import pyomo.environ as pe + >>> from pyomo.contrib import appsi + >>> import numpy as np + >>> from pyomo.common.timing import HierarchicalTimer + >>> m = pe.ConcreteModel() + >>> m.x = pe.Var() + >>> m.y = pe.Var() + >>> m.p = pe.Param(mutable=True) + >>> m.obj = pe.Objective(expr=m.x**2 + m.y**2) + >>> m.c1 = pe.Constraint(expr=m.y >= pe.exp(m.x)) + >>> m.c2 = pe.Constraint(expr=m.y >= (m.x - m.p)**2) + >>> opt = appsi.solvers.Ipopt() + >>> timer = HierarchicalTimer() + >>> for p_val in np.linspace(1, 10, 100): + >>> m.p.value = float(p_val) + >>> res = opt.solve(m, timer=timer) + >>> assert res.termination_condition == appsi.base.TerminationCondition.optimal + >>> print(res.best_feasible_objective) + >>> print(timer) + +Extra performance improvements can be made if you know exactly what +changes will be made in your model. In the example above, only +parameter values are changed, so we can setup the +:py:class:`~pyomo.contrib.appsi.base.UpdateConfig` so that the solver +does not check for changes in variables or constraints. + +.. code-block:: python + + >>> timer = HierarchicalTimer() + >>> opt.update_config.check_for_new_or_removed_constraints = False + >>> opt.update_config.check_for_new_or_removed_vars = False + >>> opt.update_config.update_constraints = False + >>> opt.update_config.update_vars = False + >>> for p_val in np.linspace(1, 10, 100): + >>> m.p.value = float(p_val) + >>> res = opt.solve(m, timer=timer) + >>> assert res.termination_condition == appsi.base.TerminationCondition.optimal + >>> print(res.best_feasible_objective) + >>> print(timer) + +Solver independent options can be specified with the +:py:class:`~pyomo.contrib.appsi.base.SolverConfig` or derived +classes. For example: + +.. code-block:: python + + >>> opt.config.stream_solver = True + +Solver specific options can be specified with the +:py:meth:`~pyomo.contrib.appsi.base.Solver.solver_options` +attribute. For example: + +.. code-block:: python + + >>> opt.solver_options['max_iter'] = 20 + +Installation +------------ +There are a few ways to install Appsi listed below. + +Option1: + +.. code-block:: + + pyomo build-extensions + +Option2: + +.. code-block:: + + cd pyomo/contrib/appsi/ + python build.py + +Option3: + +.. code-block:: + + python + >>> from pyomo.contrib.appsi.build import build_appsi + >>> build_appsi() + diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.cbc.rst b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.cbc.rst new file mode 100644 index 00000000000..a0a2f7d0f27 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.cbc.rst @@ -0,0 +1,15 @@ +Cbc +=== + +.. autoclass:: pyomo.contrib.appsi.solvers.cbc.CbcConfig + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: NoArgument + +.. autoclass:: pyomo.contrib.appsi.solvers.cbc.Cbc + :members: + :inherited-members: + :undoc-members: + :show-inheritance: diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.cplex.rst b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.cplex.rst new file mode 100644 index 00000000000..0906fd7ea76 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.cplex.rst @@ -0,0 +1,21 @@ +Cplex +===== + +.. autoclass:: pyomo.contrib.appsi.solvers.cplex.CplexConfig + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + :exclude-members: NoArgument + +.. autoclass:: pyomo.contrib.appsi.solvers.cplex.CplexResults + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pyomo.contrib.appsi.solvers.cplex.Cplex + :members: + :inherited-members: + :undoc-members: + :show-inheritance: diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.gurobi.rst b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.gurobi.rst new file mode 100644 index 00000000000..9e0af041410 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.gurobi.rst @@ -0,0 +1,55 @@ +Gurobi +====== + + +Handling Gurobi licenses through the APPSI interface +---------------------------------------------------- + +In order to obtain performance benefits when re-solving a Pyomo model +with Gurobi repeatedly, Pyomo has to keep a reference to a gurobipy +model between calls to +:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.solve()`. Depending +on the Gurobi license type, this may "consume" a license as long as +any APPSI-Gurobi interface exists (i.e., has not been garbage +collected). To release a Gurobi license for other processes, use the +:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.release_license()` +method as shown below. Note that +:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.release_license()` +must be called on every instance for this to actually release the +license. However, releasing the license will delete the gurobipy model +which will have to be reconstructed from scratch the next time +:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.solve()` is +called, negating any performance benefit of the persistent solver +interface. + +.. code-block:: python + + >>> opt = appsi.solvers.Gurobi() # doctest: +SKIP + >>> results = opt.solve(model) # doctest: +SKIP + >>> opt.release_license() # doctest: +SKIP + + +Also note that both the +:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.available()` and +:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.solve()` methods +will construct a gurobipy model, thereby (depending on the type of +license) "consuming" a license. The +:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.available()` +method has to do this so that the availability does not change between +calls to +:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.available()` and +:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.solve()`, leading +to unexpected errors. + + +.. autoclass:: pyomo.contrib.appsi.solvers.gurobi.GurobiResults + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pyomo.contrib.appsi.solvers.gurobi.Gurobi + :members: + :inherited-members: + :undoc-members: + :show-inheritance: diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.highs.rst b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.highs.rst new file mode 100644 index 00000000000..f2f72d0ad85 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.highs.rst @@ -0,0 +1,14 @@ +HiGHS +===== + +.. autoclass:: pyomo.contrib.appsi.solvers.highs.HighsResults + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pyomo.contrib.appsi.solvers.highs.Highs + :members: + :inherited-members: + :undoc-members: + :show-inheritance: diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.ipopt.rst b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.ipopt.rst new file mode 100644 index 00000000000..0d095644100 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.ipopt.rst @@ -0,0 +1,14 @@ +Ipopt +===== + +.. autoclass:: pyomo.contrib.appsi.solvers.ipopt.IpoptConfig + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pyomo.contrib.appsi.solvers.ipopt.Ipopt + :members: + :inherited-members: + :undoc-members: + :show-inheritance: diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.maingo.rst b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.maingo.rst new file mode 100644 index 00000000000..21e61c38d51 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.maingo.rst @@ -0,0 +1,14 @@ +MAiNGO +====== + +.. autoclass:: pyomo.contrib.appsi.solvers.maingo.MAiNGOConfig + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pyomo.contrib.appsi.solvers.maingo.MAiNGO + :members: + :inherited-members: + :undoc-members: + :show-inheritance: diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.rst b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.rst new file mode 100644 index 00000000000..f4dcb81b4be --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.rst @@ -0,0 +1,16 @@ +Solvers +======= + +.. automodule:: pyomo.contrib.appsi.solvers + :members: + :show-inheritance: + :undoc-members: + +.. toctree:: + + appsi.solvers.gurobi + appsi.solvers.ipopt + appsi.solvers.cplex + appsi.solvers.cbc + appsi.solvers.highs + appsi.solvers.maingo diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/config.rst b/doc/OnlineDocs/reference_guide/library_reference/common/config.rst new file mode 100644 index 00000000000..c5dc607977a --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/common/config.rst @@ -0,0 +1,85 @@ +pyomo.common.config +=================== + +.. currentmodule:: pyomo.common.config + +Core classes +~~~~~~~~~~~~ + +.. autosummary:: + + ConfigDict + ConfigList + ConfigValue + +Utilities +~~~~~~~~~ + +.. autosummary:: + + document_kwargs_from_configdict + + +Domain validators +~~~~~~~~~~~~~~~~~ + +.. autosummary:: + + Bool + Integer + PositiveInt + NegativeInt + NonNegativeInt + NonPositiveInt + PositiveFloat + NegativeFloat + NonPositiveFloat + NonNegativeFloat + In + IsInstance + InEnum + ListOf + Module + Path + PathList + DynamicImplicitDomain + +.. autoclass:: ConfigBase + :members: + :undoc-members: + +.. autoclass:: ConfigDict + :show-inheritance: + :members: + :undoc-members: + +.. autoclass:: ConfigList + :show-inheritance: + :members: + :undoc-members: + +.. autoclass:: ConfigValue + :show-inheritance: + :members: + :undoc-members: + +.. autodecorator:: document_kwargs_from_configdict + +.. autofunction:: Bool +.. autofunction:: Integer +.. autofunction:: PositiveInt +.. autofunction:: NegativeInt +.. autofunction:: NonNegativeInt +.. autofunction:: NonPositiveInt +.. autofunction:: PositiveFloat +.. autofunction:: NegativeFloat +.. autofunction:: NonPositiveFloat +.. autofunction:: NonNegativeFloat +.. autoclass:: In +.. autoclass:: IsInstance +.. autoclass:: InEnum +.. autoclass:: ListOf +.. autoclass:: Module +.. autoclass:: Path +.. autoclass:: PathList +.. autoclass:: DynamicImplicitDomain diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/dependencies.rst b/doc/OnlineDocs/reference_guide/library_reference/common/dependencies.rst new file mode 100644 index 00000000000..18d5647681c --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/common/dependencies.rst @@ -0,0 +1,7 @@ + +pyomo.common.dependencies +========================= + +.. automodule:: pyomo.common.dependencies + :members: + :member-order: bysource diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/deprecation.rst b/doc/OnlineDocs/reference_guide/library_reference/common/deprecation.rst new file mode 100644 index 00000000000..41066c040c4 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/common/deprecation.rst @@ -0,0 +1,6 @@ +pyomo.common.deprecation +======================== + +.. automodule:: pyomo.common.deprecation + :members: + :member-order: bysource diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/enums.rst b/doc/OnlineDocs/reference_guide/library_reference/common/enums.rst new file mode 100644 index 00000000000..5ed2dbb1e80 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/common/enums.rst @@ -0,0 +1,7 @@ + +pyomo.common.enums +================== + +.. automodule:: pyomo.common.enums + :members: + :member-order: bysource diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/errors.rst b/doc/OnlineDocs/reference_guide/library_reference/common/errors.rst new file mode 100644 index 00000000000..7b2bd01fe32 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/common/errors.rst @@ -0,0 +1,6 @@ +pyomo.common.errors +=================== + +.. automodule:: pyomo.common.errors + :members: + :member-order: bysource diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/fileutils.rst b/doc/OnlineDocs/reference_guide/library_reference/common/fileutils.rst new file mode 100644 index 00000000000..e582f4c2e94 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/common/fileutils.rst @@ -0,0 +1,6 @@ +pyomo.common.fileutils +====================== + +.. automodule:: pyomo.common.fileutils + :members: + :member-order: bysource diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/formatting.rst b/doc/OnlineDocs/reference_guide/library_reference/common/formatting.rst new file mode 100644 index 00000000000..25f0ef2404c --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/common/formatting.rst @@ -0,0 +1,6 @@ +pyomo.common.formatting +======================= + +.. automodule:: pyomo.common.formatting + :members: + :member-order: bysource diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/index.rst b/doc/OnlineDocs/reference_guide/library_reference/common/index.rst new file mode 100644 index 00000000000..c03436600f2 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/common/index.rst @@ -0,0 +1,19 @@ +Common Utilities +================ + +Pyomo provides a set of general-purpose utilities through +``pyomo.common``. These utilities are self-contained and do not import +or rely on any other parts of Pyomo. + +.. toctree:: + :maxdepth: 1 + + config.rst + dependencies.rst + deprecation.rst + enums.rst + errors.rst + fileutils.rst + formatting.rst + tempfiles.rst + timing.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/tempfiles.rst b/doc/OnlineDocs/reference_guide/library_reference/common/tempfiles.rst new file mode 100644 index 00000000000..03cb056dffe --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/common/tempfiles.rst @@ -0,0 +1,7 @@ + +pyomo.common.tempfiles +====================== + +.. automodule:: pyomo.common.tempfiles + :members: + :member-order: bysource diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/timing.rst b/doc/OnlineDocs/reference_guide/library_reference/common/timing.rst new file mode 100644 index 00000000000..06b6fc0f588 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/common/timing.rst @@ -0,0 +1,7 @@ + +pyomo.common.timing +=================== + +.. automodule:: pyomo.common.timing + :members: + :member-order: bysource diff --git a/doc/OnlineDocs/reference_guide/library_reference/data/index.rst b/doc/OnlineDocs/reference_guide/library_reference/data/index.rst new file mode 100644 index 00000000000..fffb06240f8 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/data/index.rst @@ -0,0 +1,11 @@ +Model Data Management +===================== + +.. autoclass:: pyomo.dataportal.DataPortal.DataPortal + :members: + :special-members: + +.. autoclass:: pyomo.dataportal.TableData.TableData + :members: + :special-members: + diff --git a/doc/OnlineDocs/reference_guide/library_reference/expressions/building.rst b/doc/OnlineDocs/reference_guide/library_reference/expressions/building.rst new file mode 100644 index 00000000000..8ffcca9e310 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/expressions/building.rst @@ -0,0 +1,10 @@ + +Utilities to Build Expressions +============================== + +.. autofunction:: pyomo.core.util.prod +.. autofunction:: pyomo.core.util.quicksum +.. autofunction:: pyomo.core.util.sum_product +.. autodata:: pyomo.core.util.summation +.. autodata:: pyomo.core.util.dot_product + diff --git a/doc/OnlineDocs/reference_guide/library_reference/expressions/classes.rst b/doc/OnlineDocs/reference_guide/library_reference/expressions/classes.rst new file mode 100644 index 00000000000..4d448d2da6a --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/expressions/classes.rst @@ -0,0 +1,105 @@ +Core Classes +============ + +The following are the two core classes documented here: + + * :class:`NumericValue` + * :class:`NumericExpression` + +The remaining classes are the public classes for expressions, which +developers may need to know about. The methods for these classes are not +documented because they are described in the +:class:`NumericExpression` class. + +Sets with Expression Types +-------------------------- + +The following sets can be used to develop visitor patterns for +Pyomo expressions. + +.. autodata:: pyomo.core.expr.numvalue.native_numeric_types +.. autodata:: pyomo.core.expr.numvalue.native_types +.. autodata:: pyomo.core.expr.numvalue.nonpyomo_leaf_types + +NumericValue and NumericExpression +---------------------------------- + +.. autoclass:: pyomo.core.expr.numvalue.NumericValue + :members: + :special-members: + :private-members: + +.. autoclass:: pyomo.core.expr.NumericExpression + :members: + :show-inheritance: + :special-members: + :private-members: + +Other Public Classes +-------------------- + +.. autoclass:: pyomo.core.expr.NegationExpression + :members: + :show-inheritance: + :undoc-members: + :private-members: + +.. autoclass:: pyomo.core.expr.ExternalFunctionExpression + :members: + :show-inheritance: + :undoc-members: + :private-members: + +.. autoclass:: pyomo.core.expr.ProductExpression + :members: + :show-inheritance: + :undoc-members: + :private-members: + +.. autoclass:: pyomo.core.expr.DivisionExpression + :members: + :show-inheritance: + :undoc-members: + :private-members: + +.. autoclass:: pyomo.core.expr.InequalityExpression + :members: + :show-inheritance: + :undoc-members: + :private-members: + +.. autoclass:: pyomo.core.expr.EqualityExpression + :members: + :show-inheritance: + :undoc-members: + :private-members: + +.. autoclass:: pyomo.core.expr.SumExpression + :members: + :show-inheritance: + :undoc-members: + :private-members: + +.. autoclass:: pyomo.core.expr.GetItemExpression + :members: + :show-inheritance: + :undoc-members: + :private-members: + +.. autoclass:: pyomo.core.expr.Expr_ifExpression + :members: + :show-inheritance: + :undoc-members: + :private-members: + +.. autoclass:: pyomo.core.expr.UnaryFunctionExpression + :members: + :show-inheritance: + :undoc-members: + :private-members: + +.. autoclass:: pyomo.core.expr.AbsExpression + :members: + :show-inheritance: + :undoc-members: + :private-members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/expressions/context_managers.rst b/doc/OnlineDocs/reference_guide/library_reference/expressions/context_managers.rst new file mode 100644 index 00000000000..ae6884d684f --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/expressions/context_managers.rst @@ -0,0 +1,10 @@ + +Context Managers +================ + +.. autoclass:: pyomo.core.expr.nonlinear_expression + :members: + +.. autoclass:: pyomo.core.expr.linear_expression + :members: + diff --git a/doc/OnlineDocs/reference_guide/library_reference/expressions/index.rst b/doc/OnlineDocs/reference_guide/library_reference/expressions/index.rst new file mode 100644 index 00000000000..388a7efa452 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/expressions/index.rst @@ -0,0 +1,13 @@ + +Expression Reference +==================== + +.. toctree:: + :maxdepth: 1 + + building.rst + managing.rst + context_managers.rst + classes.rst + visitors.rst + diff --git a/doc/OnlineDocs/reference_guide/library_reference/expressions/managing.rst b/doc/OnlineDocs/reference_guide/library_reference/expressions/managing.rst new file mode 100644 index 00000000000..369dd3aace1 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/expressions/managing.rst @@ -0,0 +1,19 @@ + +Utilities to Manage and Analyze Expressions +=========================================== + +Functions +~~~~~~~~~ + +.. autofunction:: pyomo.core.expr.expression_to_string +.. autofunction:: pyomo.core.expr.decompose_term +.. autofunction:: pyomo.core.expr.clone_expression +.. autofunction:: pyomo.core.expr.evaluate_expression +.. autofunction:: pyomo.core.expr.identify_components +.. autofunction:: pyomo.core.expr.identify_variables +.. autofunction:: pyomo.core.expr.differentiate + +Classes +~~~~~~~ + +.. autoclass:: pyomo.core.expr.symbol_map.SymbolMap diff --git a/doc/OnlineDocs/reference_guide/library_reference/expressions/visitors.rst b/doc/OnlineDocs/reference_guide/library_reference/expressions/visitors.rst new file mode 100644 index 00000000000..77cffe7905f --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/expressions/visitors.rst @@ -0,0 +1,20 @@ + +Visitor Classes +=============== + +.. autoclass:: pyomo.core.expr.StreamBasedExpressionVisitor + :members: + :inherited-members: + +.. autoclass:: pyomo.core.expr.SimpleExpressionVisitor + :members: + :inherited-members: + +.. autoclass:: pyomo.core.expr.ExpressionValueVisitor + :members: + :inherited-members: + +.. autoclass:: pyomo.core.expr.ExpressionReplacementVisitor + :members: + :inherited-members: + diff --git a/doc/OnlineDocs/reference_guide/library_reference/index.rst b/doc/OnlineDocs/reference_guide/library_reference/index.rst new file mode 100644 index 00000000000..35dd8d30307 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/index.rst @@ -0,0 +1,27 @@ +Library Reference +================= + +Pyomo is being increasingly used as a library to support Python +scripts. This section describes library APIs for key elements of +Pyomo's core library. This documentation serves as a reference for +both (1) Pyomo developers and (2) advanced users who are developing +Python scripts using Pyomo. + +.. toctree:: + :maxdepth: 1 + + common/index.rst + aml/index.rst + expressions/index.rst + solvers/index.rst + data/index.rst + APPSI (Auto-Persistent Pyomo Solver Interfaces) + +Pyomo is under active ongoing development. The following API +documentation describes *Beta* functionality. + +.. toctree:: + :maxdepth: 1 + + kernel/index.rst + diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/base.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/base.rst new file mode 100644 index 00000000000..47a2afef68d --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/base.rst @@ -0,0 +1,6 @@ +Base Object Storage Interface +============================= + +.. automodule:: pyomo.core.kernel.base + :show-inheritance: + :members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/block.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/block.rst new file mode 100644 index 00000000000..a61c12610eb --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/block.rst @@ -0,0 +1,26 @@ +Blocks +====== + +Summary +~~~~~~~ +.. autosummary:: + + pyomo.core.kernel.block.block + pyomo.core.kernel.block.block_tuple + pyomo.core.kernel.block.block_list + pyomo.core.kernel.block.block_dict + +Member Documentation +~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: pyomo.core.kernel.block.block + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.block.block_tuple + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.block.block_list + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.block.block_dict + :show-inheritance: + :members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/conic.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/conic.rst new file mode 100644 index 00000000000..34552013623 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/conic.rst @@ -0,0 +1,42 @@ +Conic Constraints +================= + +A collection of classes that provide an easy and performant +way to declare conic constraints. The Mosek solver interface +includes special handling of these objects that recognizes +them as convex constraints. Other solver interfaces will +treat these objects as general nonlinear or quadratic +expressions, and may or may not have the ability to identify +their convexity. + +Summary +~~~~~~~ +.. autosummary:: + + pyomo.core.kernel.conic.quadratic + pyomo.core.kernel.conic.rotated_quadratic + pyomo.core.kernel.conic.primal_exponential + pyomo.core.kernel.conic.primal_power + pyomo.core.kernel.conic.dual_exponential + pyomo.core.kernel.conic.dual_power + +Member Documentation +~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: pyomo.core.kernel.conic.quadratic + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.conic.rotated_quadratic + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.conic.primal_exponential + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.conic.primal_power + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.conic.dual_exponential + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.conic.dual_power + :show-inheritance: + :members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/constraint.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/constraint.rst new file mode 100644 index 00000000000..1645e57f9f2 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/constraint.rst @@ -0,0 +1,34 @@ +Constraints +=========== + +Summary +~~~~~~~ +.. autosummary:: + + pyomo.core.kernel.constraint.constraint + pyomo.core.kernel.constraint.linear_constraint + pyomo.core.kernel.constraint.constraint_tuple + pyomo.core.kernel.constraint.constraint_list + pyomo.core.kernel.constraint.constraint_dict + pyomo.core.kernel.matrix_constraint.matrix_constraint + +Member Documentation +~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: pyomo.core.kernel.constraint.constraint + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.constraint.linear_constraint + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.constraint.constraint_tuple + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.constraint.constraint_list + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.constraint.constraint_dict + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.matrix_constraint.matrix_constraint + :show-inheritance: + :members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/dict_container.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/dict_container.rst new file mode 100644 index 00000000000..6e710fa76eb --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/dict_container.rst @@ -0,0 +1,8 @@ +Dict-like Object Storage +======================== + +.. autoclass:: pyomo.core.kernel.dict_container.DictContainer + :show-inheritance: + :members: + :inherited-members: + :special-members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/aml_example.py b/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/aml_example.py new file mode 100644 index 00000000000..a640b94cc76 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/aml_example.py @@ -0,0 +1,193 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# @Import_Syntax +import pyomo.environ as aml + +# @Import_Syntax + +datafile = None + +# @AbstractModels +m = aml.AbstractModel() +# ... define model ... +instance = m.create_instance(datafile) + +# @AbstractModels +del datafile +del instance +# @ConcreteModels +m = aml.ConcreteModel() +m.b = aml.Block() +# @ConcreteModels + + +# @Sets_1 +m.s = aml.Set(initialize=[1, 2], ordered=True) +# @Sets_1 +# @Sets_2 +# [1,2,3] +m.q = aml.RangeSet(1, 3) +# @Sets_2 + + +# @Parameters_single +m.p = aml.Param(mutable=True, initialize=0) + + +# @Parameters_single +# @Parameters_dict +# pd[1] = 0, pd[2] = 1 +def pd_(m, i): + return m.s.ord(i) - 1 + + +m.pd = aml.Param(m.s, mutable=True, rule=pd_) +# @Parameters_dict +# @Parameters_list + +# +# No ParamList exists +# + + +# @Parameters_list + + +# @Variables_single +m.v = aml.Var(initialize=1.0, bounds=(1, 4)) + +# @Variables_single +# @Variables_dict +m.vd = aml.Var(m.s, bounds=(None, 9)) + + +# @Variables_dict +# @Variables_list +# used 1-based indexing +def vl_(m, i): + return (i, None) + + +m.vl = aml.VarList(bounds=vl_) +for j in m.q: + m.vl.add() +# @Variables_list + +# @Constraints_single +m.c = aml.Constraint(expr=sum(m.vd.values()) <= 9) + + +# @Constraints_single +# @Constraints_dict +def cd_(m, i, j): + return m.vd[i] == j + + +m.cd = aml.Constraint(m.s, m.q, rule=cd_) + + +# @Constraints_dict +# @Constraints_list +# uses 1-based indexing +m.cl = aml.ConstraintList() +for j in m.q: + m.cl.add(aml.inequality(-5, m.vl[j] - m.v, 5)) +# @Constraints_list + + +# @Expressions_single +m.e = aml.Expression(expr=-m.v) + + +# @Expressions_single +# @Expressions_dict +def ed_(m, i): + return -m.vd[i] + + +m.ed = aml.Expression(m.s, rule=ed_) +# @Expressions_dict +# @Expressions_list + +# +# No ExpressionList exists +# + +# @Expressions_list + + +# @Objectives_single +m.o = aml.Objective(expr=-m.v) + + +# @Objectives_single +# @Objectives_dict +def od_(m, i): + return -m.vd[i] + + +m.od = aml.Objective(m.s, rule=od_) +# @Objectives_dict +# @Objectives_list +# uses 1-based indexing +m.ol = aml.ObjectiveList() +for j in m.q: + m.ol.add(-m.vl[j]) + +# @Objectives_list + + +# @SOS_single +m.sos1 = aml.SOSConstraint(var=m.vl, level=1) +m.sos2 = aml.SOSConstraint(var=m.vd, level=2) + + +# @SOS_single +# @SOS_dict +def sd_(m, i): + if i == 1: + t = list(m.vd.values()) + elif i == 2: + t = list(m.vl.values()) + return t + + +m.sd = aml.SOSConstraint([1, 2], rule=sd_, level=1) +# @SOS_dict +# @SOS_list + +# +# No SOSConstraintList exists +# + +# @SOS_list + + +# @Suffix_single +m.dual = aml.Suffix(direction=aml.Suffix.IMPORT) +# @Suffix_single +# @Suffix_dict +# +# No SuffixDict exists +# +# @Suffix_dict + + +# @Piecewise_1d +breakpoints = [1, 2, 3, 4] +values = [1, 2, 1, 2] +m.f = aml.Var() +m.pw = aml.Piecewise(m.f, m.v, pw_pts=breakpoints, f_rule=values, pw_constr_type='EQ') +# @Piecewise_1d + + +m.pprint() diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/conic.py b/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/conic.py new file mode 100644 index 00000000000..0418d188722 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/conic.py @@ -0,0 +1,33 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# @Class +import pyomo.kernel as pmo + +m = pmo.block() +m.x1 = pmo.variable(lb=0) +m.x2 = pmo.variable() +m.r = pmo.variable(lb=0) +m.q = pmo.conic.primal_exponential(x1=m.x1, x2=m.x2, r=m.r) +# @Class +del m + +# @Domain +import pyomo.kernel as pmo +import math + +m = pmo.block() +m.x = pmo.variable(lb=0) +m.y = pmo.variable(lb=0) +m.b = pmo.conic.primal_exponential.as_domain( + x1=math.sqrt(2) * m.x, x2=2.0, r=2 * (m.x + m.y) +) +# @Domain diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_containers.py b/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_containers.py new file mode 100644 index 00000000000..1931c6d9b56 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_containers.py @@ -0,0 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.kernel + +# @all +vlist = pyomo.kernel.variable_list() +vlist.append(pyomo.kernel.variable_dict()) +vlist[0]['x'] = pyomo.kernel.variable() +# @all diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_example.py b/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_example.py new file mode 100644 index 00000000000..1f80bce9788 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_example.py @@ -0,0 +1,174 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# @Import_Syntax +import pyomo.kernel as pmo + +# @Import_Syntax + +data = None + + +# @AbstractModels +def create(data): + instance = pmo.block() + # ... define instance ... + return instance + + +instance = create(data) +# @AbstractModels +del data +del instance +# @ConcreteModels +m = pmo.block() +m.b = pmo.block() +# @ConcreteModels + + +# @Sets_1 +m.s = [1, 2] + +# @Sets_1 +# @Sets_2 +# [0,1,2] +m.q = range(3) +# @Sets_2 + + +# @Parameters_single +m.p = pmo.parameter(0) + +# @Parameters_single +# @Parameters_dict +# pd[1] = 0, pd[2] = 1 +m.pd = pmo.parameter_dict() +for k, i in enumerate(m.s): + m.pd[i] = pmo.parameter(k) + + +# @Parameters_dict +# @Parameters_list +# uses 0-based indexing +# pl[0] = 0, pl[0] = 1, ... +m.pl = pmo.parameter_list() +for j in m.q: + m.pl.append(pmo.parameter(j)) +# @Parameters_list + + +# @Variables_single +m.v = pmo.variable(value=1, lb=1, ub=4) +# @Variables_single +# @Variables_dict +m.vd = pmo.variable_dict() +for i in m.s: + m.vd[i] = pmo.variable(ub=9) +# @Variables_dict +# @Variables_list +# used 0-based indexing +m.vl = pmo.variable_list() +for j in m.q: + m.vl.append(pmo.variable(lb=i)) + +# @Variables_list + + +# @Constraints_single +m.c = pmo.constraint(sum(m.vd.values()) <= 9) +# @Constraints_single +# @Constraints_dict +m.cd = pmo.constraint_dict() +for i in m.s: + for j in m.q: + m.cd[i, j] = pmo.constraint(body=m.vd[i], rhs=j) +# @Constraints_dict +# @Constraints_list +# uses 0-based indexing +m.cl = pmo.constraint_list() +for j in m.q: + m.cl.append(pmo.constraint(lb=-5, body=m.vl[j] - m.v, ub=5)) +# @Constraints_list + + +# @Expressions_single +m.e = pmo.expression(-m.v) +# @Expressions_single +# @Expressions_dict +m.ed = pmo.expression_dict() +for i in m.s: + m.ed[i] = pmo.expression(-m.vd[i]) +# @Expressions_dict +# @Expressions_list +# uses 0-based indexed +m.el = pmo.expression_list() +for j in m.q: + m.el.append(pmo.expression(-m.vl[j])) +# @Expressions_list + + +# @Objectives_single +m.o = pmo.objective(-m.v) +# @Objectives_single +# @Objectives_dict +m.od = pmo.objective_dict() +for i in m.s: + m.od[i] = pmo.objective(-m.vd[i]) +# @Objectives_dict +# @Objectives_list +# uses 0-based indexing +m.ol = pmo.objective_list() +for j in m.q: + m.ol.append(pmo.objective(-m.vl[j])) +# @Objectives_list + + +# @SOS_single +m.sos1 = pmo.sos1(m.vd.values()) + + +m.sos2 = pmo.sos2(m.vl) + + +# @SOS_single +# @SOS_dict +m.sd = pmo.sos_dict() +m.sd[1] = pmo.sos1(m.vd.values()) +m.sd[2] = pmo.sos1(m.vl) + + +# @SOS_dict +# @SOS_list +# uses 0-based indexing +m.sl = pmo.sos_list() +for i in m.s: + m.sl.append(pmo.sos1([m.vl[i], m.vd[i]])) +# @SOS_list + + +# @Suffix_single +m.dual = pmo.suffix(direction=pmo.suffix.IMPORT) +# @Suffix_single +# @Suffix_dict +m.suffixes = pmo.suffix_dict() +m.suffixes['dual'] = pmo.suffix(direction=pmo.suffix.IMPORT) +# @Suffix_dict + + +# @Piecewise_1d +breakpoints = [1, 2, 3, 4] +values = [1, 2, 1, 2] +m.f = pmo.variable() +m.pw = pmo.piecewise(breakpoints, values, input=m.v, output=m.f, bound='eq') +# @Piecewise_1d + + +pmo.pprint(m) diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_solving.py b/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_solving.py new file mode 100644 index 00000000000..13d7efc052a --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_solving.py @@ -0,0 +1,22 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.kernel as pmo + +model = pmo.block() +model.x = pmo.variable() +model.c = pmo.constraint(model.x >= 1) +model.o = pmo.objective(model.x) + +opt = pmo.SolverFactory("ipopt") + +result = opt.solve(model) +assert str(result.solver.termination_condition) == "optimal" diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_subclassing.py b/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_subclassing.py new file mode 100644 index 00000000000..d6e38f6b0e0 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_subclassing.py @@ -0,0 +1,93 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.kernel + + +# @Nonnegative +class NonNegativeVariable(pyomo.kernel.variable): + """A non-negative variable.""" + + __slots__ = () + + def __init__(self, **kwds): + if 'lb' not in kwds: + kwds['lb'] = 0 + if kwds['lb'] < 0: + raise ValueError("lower bound must be non-negative") + super(NonNegativeVariable, self).__init__(**kwds) + + # + # restrict assignments to x.lb to non-negative numbers + # + @property + def lb(self): + # calls the base class property getter + return pyomo.kernel.variable.lb.fget(self) + + @lb.setter + def lb(self, lb): + if lb < 0: + raise ValueError("lower bound must be non-negative") + # calls the base class property setter + pyomo.kernel.variable.lb.fset(self, lb) + + +# @Nonnegative + + +# @Point +class Point(pyomo.kernel.variable_tuple): + """A 3-dimensional point in Cartesian space with the + z coordinate restricted to non-negative values.""" + + __slots__ = () + + def __init__(self): + super(Point, self).__init__( + (pyomo.kernel.variable(), pyomo.kernel.variable(), NonNegativeVariable()) + ) + + @property + def x(self): + return self[0] + + @property + def y(self): + return self[1] + + @property + def z(self): + return self[2] + + +# @Point + + +# @SOC +class SOC(pyomo.kernel.constraint): + """A convex second-order cone constraint""" + + __slots__ = () + + def __init__(self, point): + assert isinstance(point.z, NonNegativeVariable) + super(SOC, self).__init__(point.x**2 + point.y**2 <= point.z**2) + + +# @SOC + +# @Usage +model = pyomo.kernel.block() +model.p = Point() +model.p.z.lb = 0 +model.soc = SOC(model.p) +# @Usage diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/transformer.py b/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/transformer.py new file mode 100644 index 00000000000..43a1d0675bf --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/transformer.py @@ -0,0 +1,66 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.environ +import pyomo.kernel + +import pympler.asizeof + + +def _fmt(num, suffix='B'): + """format memory output""" + if num is None: + return "" + for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']: + if abs(num) < 1000.0: + return "%3.1f %s%s" % (num, unit, suffix) + num /= 1000.0 + return "%.1f %s%s" % (num, 'Yi', suffix) + + +# @kernel +class Transformer(pyomo.kernel.block): + def __init__(self): + super(Transformer, self).__init__() + self._a = pyomo.kernel.parameter() + self._v_in = pyomo.kernel.expression() + self._v_out = pyomo.kernel.expression() + self._c = pyomo.kernel.constraint(self._a * self._v_out == self._v_in) + + def set_ratio(self, a): + assert a > 0 + self._a.value = a + + def connect_v_in(self, v_in): + self._v_in.expr = v_in + + def connect_v_out(self, v_out): + self._v_out.expr = v_out + + +# @kernel + +print("Memory:", _fmt(pympler.asizeof.asizeof(Transformer()))) + + +# @aml +def Transformer(): + b = pyomo.environ.Block(concrete=True) + b._a = pyomo.environ.Param(mutable=True) + b._v_in = pyomo.environ.Expression() + b._v_out = pyomo.environ.Expression() + b._c = pyomo.environ.Constraint(expr=b._a * b._v_out == b._v_in) + return b + + +# @aml + +print("Memory:", _fmt(pympler.asizeof.asizeof(Transformer()))) diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/expression.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/expression.rst new file mode 100644 index 00000000000..b2d4c2d1b35 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/expression.rst @@ -0,0 +1,26 @@ +Expressions +=========== + +Summary +~~~~~~~ +.. autosummary:: + + pyomo.core.kernel.expression.expression + pyomo.core.kernel.expression.expression_tuple + pyomo.core.kernel.expression.expression_list + pyomo.core.kernel.expression.expression_dict + +Member Documentation +~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: pyomo.core.kernel.expression.expression + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.expression.expression_tuple + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.expression.expression_list + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.expression.expression_dict + :show-inheritance: + :members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/heterogeneous_container.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/heterogeneous_container.rst new file mode 100644 index 00000000000..74dad1d754e --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/heterogeneous_container.rst @@ -0,0 +1,6 @@ +Heterogeneous Object Containers +=============================== + +.. automodule:: pyomo.core.kernel.heterogeneous_container + :show-inheritance: + :members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/homogeneous_container.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/homogeneous_container.rst new file mode 100644 index 00000000000..b722e026dc1 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/homogeneous_container.rst @@ -0,0 +1,6 @@ +Homogeneous Object Containers +============================= + +.. automodule:: pyomo.core.kernel.homogeneous_container + :show-inheritance: + :members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/index.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/index.rst new file mode 100644 index 00000000000..70c3cc715a9 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/index.rst @@ -0,0 +1,210 @@ +.. role:: python(code) + :language: python + +.. warning:: + + The :python:`pyomo.kernel` API is still in the beta phase of development. It is fully tested and functional; however, the interface may change as it becomes further integrated with the rest of Pyomo. + +.. warning:: + + Models built with :python:`pyomo.kernel` components are not yet compatible with pyomo extension modules (e.g., :python:`PySP`, :python:`pyomo.dae`, :python:`pyomo.gdp`). + +The Kernel Library +================== + +The :python:`pyomo.kernel` library is an experimental modeling interface designed to provide a better experience for users doing concrete modeling and advanced application development with Pyomo. It includes the basic set of :ref:`modeling components ` necessary to build algebraic models, which have been redesigned from the ground up to make it easier for users to customize and extend. For a side-by-side comparison of :python:`pyomo.kernel` and :python:`pyomo.environ` syntax, visit the link below. + +.. toctree:: + + syntax_comparison.rst + + +Models built from :python:`pyomo.kernel` components are fully compatible with the standard solver interfaces included with Pyomo. A minimal example script that defines and solves a model is shown below. + +.. literalinclude:: examples/kernel_solving.py + :language: python + +Notable Improvements +-------------------- + +More Control of Model Structure +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Containers in :python:`pyomo.kernel` are analogous to indexed components in :python:`pyomo.environ`. However, :python:`pyomo.kernel` containers allow for additional layers of structure as they can be nested within each other as long as they have compatible categories. The following example shows this using :python:`pyomo.kernel.variable` containers. + +.. literalinclude:: examples/kernel_containers_all.spy + :language: python + +As the next section will show, the standard modeling component containers are also compatible with user-defined classes that derive from the existing modeling components. + +Sub-Classing +^^^^^^^^^^^^ + +The existing components and containers in :python:`pyomo.kernel` are designed to make sub-classing easy. User-defined classes that derive from the standard modeling components and containers in :python:`pyomo.kernel` are compatible with existing containers of the same component category. As an example, in the following code we see that the :python:`pyomo.kernel.block_list` container can store both :python:`pyomo.kernel.block` objects as well as a user-defined :python:`Widget` object that derives from :python:`pyomo.kernel.block`. The :python:`Widget` object can also be placed on another block object as an attribute and treated itself as a block. + +.. code-block:: python + + class Widget(pyomo.kernel.block): + ... + + model = pyomo.kernel.block() + model.blist = pyomo.kernel.block_list() + model.blist.append(Widget()) + model.blist.append(pyomo.kernel.block()) + model.w = Widget() + model.w.x = pyomo.kernel.variable() + +The next series of examples goes into more detail on how to implement derived components or containers. + +The following code block shows a class definition for a non-negative variable, starting from :python:`pyomo.kernel.variable` as a base class. + +.. literalinclude:: examples/kernel_subclassing_Nonnegative.spy + :language: python + +The :python:`NonNegativeVariable` class prevents negative values from being stored into its lower bound during initialization or later on through assignment statements (e.g, :python:`x.lb = -1` fails). Note that the :python:`__slots__ == ()` line at the beginning of the class definition is optional, but it is recommended if no additional data members are necessary as it reduces the memory requirement of the new variable type. + +The next code block defines a custom variable container called :python:`Point` that represents a 3-dimensional point in Cartesian space. The new type derives from the :python:`pyomo.kernel.variable_tuple` container and uses the :python:`NonNegativeVariable` type we defined previously in the `z` coordinate. + +.. literalinclude:: examples/kernel_subclassing_Point.spy + :language: python + +The :python:`Point` class can be treated like a tuple storing three variables, and it can be placed inside of other variable containers or added as attributes to blocks. The property methods included in the class definition provide an additional syntax for accessing the three variables it stores, as the next code example will show. + +The following code defines a class for building a convex second-order cone constraint from a :python:`Point` object. It derives from the :python:`pyomo.kernel.constraint` class, overriding the constructor to build the constraint expression and utilizing the property methods on the point class to increase readability. + +.. literalinclude:: examples/kernel_subclassing_SOC.spy + :language: python + + +Reduced Memory Usage +^^^^^^^^^^^^^^^^^^^^ + +The :python:`pyomo.kernel` library offers significant opportunities to reduce memory requirements for highly structured models. The situation where this is most apparent is when expressing a model in terms of many small blocks consisting of singleton components. As an example, consider expressing a model consisting of a large number of voltage transformers. One option for doing so might be to define a `Transformer` component as a subclass of :python:`pyomo.kernel.block`. The example below defines such a component, including some helper methods for connecting input and output voltage variables and updating the transformer ratio. + +.. literalinclude:: examples/transformer_kernel.spy + :language: python + +A simplified version of this using :python:`pyomo.environ` components might look like what is below. + +.. literalinclude:: examples/transformer_aml.spy + :language: python + +The transformer expressed using :python:`pyomo.kernel` components requires roughly 2 KB of memory, whereas the :python:`pyomo.environ` version requires roughly 8.4 KB of memory (an increase of more than 4x). Additionally, the :python:`pyomo.kernel` transformer is fully compatible with all existing :python:`pyomo.kernel` block containers. + +Direct Support For Conic Constraints with Mosek +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pyomo 5.6.3 introduced support into :python:`pyomo.kernel` +for six conic constraint forms that are directly recognized +by the new Mosek solver interface. These are + + - :python:`conic.quadratic`: + + :math:`\;\;\sum_{i}x_i^2 \leq r^2,\;\;r\geq 0` + + - :python:`conic.rotated_quadratic`: + + :math:`\;\;\sum_{i}x_i^2 \leq 2 r_1 r_2,\;\;r_1,r_2\geq 0` + + - :python:`conic.primal_exponential`: + + :math:`\;\;x_1\exp(x_2/x_1) \leq r,\;\;x_1,r\geq 0` + + - :python:`conic.primal_power` (:math:`\alpha` is a constant): + + :math:`\;\;||x||_2 \leq r_1^{\alpha} r_2^{1-\alpha},\;\;r_1,r_2\geq 0,\;0 < \alpha < 1` + + - :python:`conic.dual_exponential`: + + :math:`\;\;-x_2\exp((x_1/x_2)-1) \leq r,\;\;x_2\leq0,\;r\geq 0` + + - :python:`conic.dual_power` (:math:`\alpha` is a constant): + + :math:`\;\;||x||_2 \leq (r_1/\alpha)^{\alpha} (r_2/(1-\alpha))^{1-\alpha},\;\;r_1,r_2\geq 0,\;0 < \alpha < 1` + +Other solver interfaces will treat these objects as general +nonlinear or quadratic constraints, and may or may not have +the ability to identify their convexity. For instance, +Gurobi will recognize the expressions produced by the +:python:`quadratic` and :python:`rotated_quadratic` objects +as representing convex domains as long as the variables +involved satisfy the convexity conditions. However, other +solvers may not include this functionality. + +Each of these conic constraint classes are of the same +category type as standard :python:`pyomo.kernel.constraint` +object, and, thus, are directly supported by the standard +constraint containers (:python:`constraint_tuple`, +:python:`constraint_list`, :python:`constraint_dict`). + +Each conic constraint class supports two methods of +instantiation. The first method is to directly instantiate a +conic constraint object, providing all necessary input +variables: + +.. literalinclude:: examples/conic_Class.spy + :language: python + +This method may be limiting if utilizing the Mosek solver as +the user must ensure that additional conic constraints do +not use variables that are directly involved in any existing +conic constraints (this is a limitation the Mosek solver +itself). + +To overcome this limitation, and to provide a more general +way of defining conic domains, each conic constraint class +provides the :python:`as_domain` class method. This +alternate constructor has the same argument signature as the +class, but in place of each variable, one can optionally +provide a constant, a linear expression, or +:python:`None`. The :python:`as_domain` class method returns +a :python:`block` object that includes the core conic +constraint, auxiliary variables used to express the conic +constraint, as well as auxiliary constraints that link the +inputs (that are not :python:`None`) to the auxiliary +variables. Example: + +.. literalinclude:: examples/conic_Domain.spy + :language: python + +Reference +--------- + +.. _kernel_modeling_components: + +Modeling Components: +^^^^^^^^^^^^^^^^^^^^ + +.. toctree:: + :maxdepth: 1 + + block.rst + variable.rst + constraint.rst + parameter.rst + objective.rst + expression.rst + sos.rst + suffix.rst + piecewise/index.rst + conic.rst + +Base API: +^^^^^^^^^ + +.. toctree:: + :maxdepth: 1 + + base.rst + homogeneous_container.rst + heterogeneous_container.rst + +Containers: +^^^^^^^^^^^ + +.. toctree:: + :maxdepth: 1 + + tuple_container.rst + list_container.rst + dict_container.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/list_container.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/list_container.rst new file mode 100644 index 00000000000..b82c6d9c6f0 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/list_container.rst @@ -0,0 +1,8 @@ +List-like Object Storage +======================== + +.. autoclass:: pyomo.core.kernel.list_container.ListContainer + :show-inheritance: + :members: + :inherited-members: + :special-members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/objective.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/objective.rst new file mode 100644 index 00000000000..77f26d2f441 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/objective.rst @@ -0,0 +1,26 @@ +Objectives +========== + +Summary +~~~~~~~ +.. autosummary:: + + pyomo.core.kernel.objective.objective + pyomo.core.kernel.objective.objective_tuple + pyomo.core.kernel.objective.objective_list + pyomo.core.kernel.objective.objective_dict + +Member Documentation +~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: pyomo.core.kernel.objective.objective + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.objective.objective_tuple + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.objective.objective_list + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.objective.objective_dict + :show-inheritance: + :members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/parameter.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/parameter.rst new file mode 100644 index 00000000000..212b0cb125e --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/parameter.rst @@ -0,0 +1,30 @@ +Parameters +========== + +Summary +~~~~~~~ +.. autosummary:: + + pyomo.core.kernel.parameter.parameter + pyomo.core.kernel.parameter.functional_value + pyomo.core.kernel.parameter.parameter_tuple + pyomo.core.kernel.parameter.parameter_list + pyomo.core.kernel.parameter.parameter_dict + +Member Documentation +~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: pyomo.core.kernel.parameter.parameter + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.parameter.functional_value + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.parameter.parameter_tuple + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.parameter.parameter_list + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.parameter.parameter_dict + :show-inheritance: + :members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/index.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/index.rst new file mode 100644 index 00000000000..2255d0fe116 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/index.rst @@ -0,0 +1,11 @@ +Piecewise Function Library +========================== + +Modules + +.. toctree:: + :maxdepth: 1 + + piecewise.rst + piecewise_nd.rst + util.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/piecewise.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/piecewise.rst new file mode 100644 index 00000000000..25c250d6559 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/piecewise.rst @@ -0,0 +1,53 @@ +Single-variate Piecewise Functions +================================== + +Summary +~~~~~~~ +.. autosummary:: + pyomo.core.kernel.piecewise_library.transforms.piecewise + pyomo.core.kernel.piecewise_library.transforms.PiecewiseLinearFunction + pyomo.core.kernel.piecewise_library.transforms.TransformedPiecewiseLinearFunction + pyomo.core.kernel.piecewise_library.transforms.piecewise_convex + pyomo.core.kernel.piecewise_library.transforms.piecewise_sos2 + pyomo.core.kernel.piecewise_library.transforms.piecewise_dcc + pyomo.core.kernel.piecewise_library.transforms.piecewise_cc + pyomo.core.kernel.piecewise_library.transforms.piecewise_mc + pyomo.core.kernel.piecewise_library.transforms.piecewise_inc + pyomo.core.kernel.piecewise_library.transforms.piecewise_dlog + pyomo.core.kernel.piecewise_library.transforms.piecewise_log + +Member Documentation +~~~~~~~~~~~~~~~~~~~~ +.. autofunction:: pyomo.core.kernel.piecewise_library.transforms.piecewise +.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.PiecewiseLinearFunction + :show-inheritance: + :special-members: __call__ + :members: +.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.TransformedPiecewiseLinearFunction + :show-inheritance: + :special-members: __call__ + :members: +.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_convex + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_sos2 + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_dcc + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_cc + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_mc + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_inc + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_dlog + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_log + :show-inheritance: + :members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/piecewise_nd.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/piecewise_nd.rst new file mode 100644 index 00000000000..e5c71a4ec15 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/piecewise_nd.rst @@ -0,0 +1,25 @@ +Multi-variate Piecewise Functions +================================= + +Summary +~~~~~~~ +.. autosummary:: + pyomo.core.kernel.piecewise_library.transforms_nd.piecewise_nd + pyomo.core.kernel.piecewise_library.transforms_nd.PiecewiseLinearFunctionND + pyomo.core.kernel.piecewise_library.transforms_nd.TransformedPiecewiseLinearFunctionND + pyomo.core.kernel.piecewise_library.transforms_nd.piecewise_nd_cc + +Member Documentation +~~~~~~~~~~~~~~~~~~~~ +.. autofunction:: pyomo.core.kernel.piecewise_library.transforms_nd.piecewise_nd +.. autoclass:: pyomo.core.kernel.piecewise_library.transforms_nd.PiecewiseLinearFunctionND + :show-inheritance: + :special-members: __call__ + :members: +.. autoclass:: pyomo.core.kernel.piecewise_library.transforms_nd.TransformedPiecewiseLinearFunctionND + :show-inheritance: + :special-members: __call__ + :members: +.. autoclass:: pyomo.core.kernel.piecewise_library.transforms_nd.piecewise_nd_cc + :show-inheritance: + :members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/util.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/util.rst new file mode 100644 index 00000000000..52b7b1de8f7 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/util.rst @@ -0,0 +1,6 @@ +Utilities for Piecewise Functions +================================= + +.. automodule:: pyomo.core.kernel.piecewise_library.util + :show-inheritance: + :members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/sos.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/sos.rst new file mode 100644 index 00000000000..0f3f5fedf54 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/sos.rst @@ -0,0 +1,30 @@ +Special Ordered Sets +==================== + +Summary +~~~~~~~ +.. autosummary:: + + pyomo.core.kernel.sos.sos + pyomo.core.kernel.sos.sos1 + pyomo.core.kernel.sos.sos2 + pyomo.core.kernel.sos.sos_tuple + pyomo.core.kernel.sos.sos_list + pyomo.core.kernel.sos.sos_dict + +Member Documentation +~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: pyomo.core.kernel.sos.sos + :show-inheritance: + :members: +.. autofunction:: pyomo.core.kernel.sos.sos1 +.. autofunction:: pyomo.core.kernel.sos.sos2 +.. autoclass:: pyomo.core.kernel.sos.sos_tuple + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.sos.sos_list + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.sos.sos_dict + :show-inheritance: + :members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/suffix.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/suffix.rst new file mode 100644 index 00000000000..d833f56daa9 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/suffix.rst @@ -0,0 +1,6 @@ +Suffixes +======== + +.. automodule:: pyomo.core.kernel.suffix + :show-inheritance: + :members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/syntax_comparison.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/syntax_comparison.rst new file mode 100644 index 00000000000..71c739214e3 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/syntax_comparison.rst @@ -0,0 +1,133 @@ +.. _kernel_syntax_comparison: + +Syntax Comparison Table (pyomo.kernel vs pyomo.environ) +======================================================= + +.. list-table:: + :header-rows: 1 + :align: center + + * - + - **pyomo.kernel** + - **pyomo.environ** + + * - **Import** + - .. literalinclude:: examples/kernel_example_Import_Syntax.spy + :language: python + - .. literalinclude:: examples/aml_example_Import_Syntax.spy + :language: python + * - **Model** [#models_fn]_ + - .. literalinclude:: examples/kernel_example_AbstractModels.spy + :language: python + .. literalinclude:: examples/kernel_example_ConcreteModels.spy + :language: python + - .. literalinclude:: examples/aml_example_AbstractModels.spy + :language: python + .. literalinclude:: examples/aml_example_ConcreteModels.spy + :language: python + * - **Set** [#sets_fn]_ + - .. literalinclude:: examples/kernel_example_Sets_1.spy + :language: python + .. literalinclude:: examples/kernel_example_Sets_2.spy + :language: python + - .. literalinclude:: examples/aml_example_Sets_1.spy + :language: python + .. literalinclude:: examples/aml_example_Sets_2.spy + :language: python + * - **Parameter** [#parameters_fn]_ + - .. literalinclude:: examples/kernel_example_Parameters_single.spy + :language: python + .. literalinclude:: examples/kernel_example_Parameters_dict.spy + :language: python + .. literalinclude:: examples/kernel_example_Parameters_list.spy + :language: python + - .. literalinclude:: examples/aml_example_Parameters_single.spy + :language: python + .. literalinclude:: examples/aml_example_Parameters_dict.spy + :language: python + .. literalinclude:: examples/aml_example_Parameters_list.spy + :language: python + * - **Variable** + - .. literalinclude:: examples/kernel_example_Variables_single.spy + :language: python + .. literalinclude:: examples/kernel_example_Variables_dict.spy + :language: python + .. literalinclude:: examples/kernel_example_Variables_list.spy + :language: python + - .. literalinclude:: examples/aml_example_Variables_single.spy + :language: python + .. literalinclude:: examples/aml_example_Variables_dict.spy + :language: python + .. literalinclude:: examples/aml_example_Variables_list.spy + :language: python + * - **Constraint** + - .. literalinclude:: examples/kernel_example_Constraints_single.spy + :language: python + .. literalinclude:: examples/kernel_example_Constraints_dict.spy + :language: python + .. literalinclude:: examples/kernel_example_Constraints_list.spy + :language: python + - .. literalinclude:: examples/aml_example_Constraints_single.spy + :language: python + .. literalinclude:: examples/aml_example_Constraints_dict.spy + :language: python + .. literalinclude:: examples/aml_example_Constraints_list.spy + :language: python + * - **Expression** + - .. literalinclude:: examples/kernel_example_Expressions_single.spy + :language: python + .. literalinclude:: examples/kernel_example_Expressions_dict.spy + :language: python + .. literalinclude:: examples/kernel_example_Expressions_list.spy + :language: python + - .. literalinclude:: examples/aml_example_Expressions_single.spy + :language: python + .. literalinclude:: examples/aml_example_Expressions_dict.spy + :language: python + .. literalinclude:: examples/aml_example_Expressions_list.spy + :language: python + * - **Objective** + - .. literalinclude:: examples/kernel_example_Objectives_single.spy + :language: python + .. literalinclude:: examples/kernel_example_Objectives_dict.spy + :language: python + .. literalinclude:: examples/kernel_example_Objectives_list.spy + :language: python + - .. literalinclude:: examples/aml_example_Objectives_single.spy + :language: python + .. literalinclude:: examples/aml_example_Objectives_dict.spy + :language: python + .. literalinclude:: examples/aml_example_Objectives_list.spy + :language: python + * - **SOS** [#sos_fn]_ + - .. literalinclude:: examples/kernel_example_SOS_single.spy + :language: python + .. literalinclude:: examples/kernel_example_SOS_dict.spy + :language: python + .. literalinclude:: examples/kernel_example_SOS_list.spy + :language: python + - .. literalinclude:: examples/aml_example_SOS_single.spy + :language: python + .. literalinclude:: examples/aml_example_SOS_dict.spy + :language: python + .. literalinclude:: examples/aml_example_SOS_list.spy + :language: python + * - **Suffix** + - .. literalinclude:: examples/kernel_example_Suffix_single.spy + :language: python + .. literalinclude:: examples/kernel_example_Suffix_dict.spy + :language: python + - .. literalinclude:: examples/aml_example_Suffix_single.spy + :language: python + .. literalinclude:: examples/aml_example_Suffix_dict.spy + :language: python + * - **Piecewise** [#pw_fn]_ + - .. literalinclude:: examples/kernel_example_Piecewise_1d.spy + :language: python + - .. literalinclude:: examples/aml_example_Piecewise_1d.spy + :language: python +.. [#models_fn] :python:`pyomo.kernel` does not include an alternative to the :python:`AbstractModel` component from :python:`pyomo.environ`. All data necessary to build a model must be imported by the user. +.. [#sets_fn] :python:`pyomo.kernel` does not include an alternative to the Pyomo :python:`Set` component from :python:`pyomo.environ`. +.. [#parameters_fn] :python:`pyomo.kernel.parameter` objects are always mutable. +.. [#sos_fn] Special Ordered Sets +.. [#pw_fn] Both :python:`pyomo.kernel.piecewise` and :python:`pyomo.kernel.piecewise_nd` create objects that are sub-classes of :python:`pyomo.kernel.block`. Thus, these objects can be stored in containers such as :python:`pyomo.kernel.block_dict` and :python:`pyomo.kernel.block_list`. diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/tuple_container.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/tuple_container.rst new file mode 100644 index 00000000000..8a2798753c4 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/tuple_container.rst @@ -0,0 +1,8 @@ +Tuple-like Object Storage +========================= + +.. autoclass:: pyomo.core.kernel.tuple_container.TupleContainer + :show-inheritance: + :members: + :inherited-members: + :special-members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/variable.rst b/doc/OnlineDocs/reference_guide/library_reference/kernel/variable.rst new file mode 100644 index 00000000000..f743cee4003 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/kernel/variable.rst @@ -0,0 +1,26 @@ +Variables +========= + +Summary +~~~~~~~ +.. autosummary:: + + pyomo.core.kernel.variable.variable + pyomo.core.kernel.variable.variable_tuple + pyomo.core.kernel.variable.variable_list + pyomo.core.kernel.variable.variable_dict + +Member Documentation +~~~~~~~~~~~~~~~~~~~~ +.. autoclass:: pyomo.core.kernel.variable.variable + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.variable.variable_tuple + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.variable.variable_list + :show-inheritance: + :members: +.. autoclass:: pyomo.core.kernel.variable.variable_dict + :show-inheritance: + :members: diff --git a/doc/OnlineDocs/reference_guide/library_reference/solvers/cplex_persistent.rst b/doc/OnlineDocs/reference_guide/library_reference/solvers/cplex_persistent.rst new file mode 100644 index 00000000000..ee28ecda5e5 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/solvers/cplex_persistent.rst @@ -0,0 +1,7 @@ +CPLEXPersistent +================ + +.. autoclass:: pyomo.solvers.plugins.solvers.cplex_persistent.CPLEXPersistent + :members: + :inherited-members: + :show-inheritance: diff --git a/doc/OnlineDocs/reference_guide/library_reference/solvers/gams.rst b/doc/OnlineDocs/reference_guide/library_reference/solvers/gams.rst new file mode 100644 index 00000000000..f36de5d9e01 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/solvers/gams.rst @@ -0,0 +1,43 @@ +GAMS +==== + +.. currentmodule:: pyomo.solvers.plugins.solvers.GAMS + +GAMSShell Solver +---------------- + +.. autosummary:: + + GAMSShell.available + GAMSShell.executable + GAMSShell.solve + GAMSShell.version + GAMSShell.warm_start_capable + +.. autoclass:: GAMSShell + :members: + +GAMSDirect Solver +----------------- + +.. autosummary:: + + GAMSDirect.available + GAMSDirect.solve + GAMSDirect.version + GAMSDirect.warm_start_capable + +.. autoclass:: GAMSDirect + :members: + +.. currentmodule:: pyomo.repn.plugins.gams_writer + +GAMS Writer +----------- + +This class is most commonly accessed and called upon via +model.write("filename.gms", ...), but is also utilized +by the GAMS solver interfaces. + +.. autoclass:: ProblemWriter_gams + :members: __call__ diff --git a/doc/OnlineDocs/reference_guide/library_reference/solvers/gurobi_direct.rst b/doc/OnlineDocs/reference_guide/library_reference/solvers/gurobi_direct.rst new file mode 100644 index 00000000000..21cb79e5531 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/solvers/gurobi_direct.rst @@ -0,0 +1,18 @@ +GurobiDirect +============ + +.. currentmodule:: pyomo.solvers.plugins.solvers.gurobi_direct + +Methods +------- + +.. autosummary:: + + GurobiDirect.available + GurobiDirect.close + GurobiDirect.close_global + GurobiDirect.solve + GurobiDirect.version + +.. autoclass:: GurobiDirect + :members: available, close, close_global, solve, version diff --git a/doc/OnlineDocs/reference_guide/library_reference/solvers/gurobi_persistent.rst b/doc/OnlineDocs/reference_guide/library_reference/solvers/gurobi_persistent.rst new file mode 100644 index 00000000000..2472599c1ed --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/solvers/gurobi_persistent.rst @@ -0,0 +1,39 @@ +GurobiPersistent +================ + +.. currentmodule:: pyomo.solvers.plugins.solvers.gurobi_persistent + +Methods +------- + +.. autosummary:: + + GurobiPersistent.add_block + GurobiPersistent.add_constraint + GurobiPersistent.set_objective + GurobiPersistent.add_sos_constraint + GurobiPersistent.add_var + GurobiPersistent.available + GurobiPersistent.has_capability + GurobiPersistent.has_instance + GurobiPersistent.load_vars + GurobiPersistent.problem_format + GurobiPersistent.remove_block + GurobiPersistent.remove_constraint + GurobiPersistent.remove_sos_constraint + GurobiPersistent.remove_var + GurobiPersistent.reset + GurobiPersistent.results_format + GurobiPersistent.set_callback + GurobiPersistent.set_instance + GurobiPersistent.set_problem_format + GurobiPersistent.set_results_format + GurobiPersistent.solve + GurobiPersistent.update_var + GurobiPersistent.version + GurobiPersistent.write + +.. autoclass:: GurobiPersistent + :members: + :inherited-members: + :show-inheritance: diff --git a/doc/OnlineDocs/reference_guide/library_reference/solvers/index.rst b/doc/OnlineDocs/reference_guide/library_reference/solvers/index.rst new file mode 100644 index 00000000000..400032df076 --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/solvers/index.rst @@ -0,0 +1,11 @@ +Solver Interfaces +================= + +.. toctree:: + :maxdepth: 1 + + gams.rst + cplex_persistent.rst + gurobi_direct.rst + gurobi_persistent.rst + xpress_persistent.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/solvers/xpress_persistent.rst b/doc/OnlineDocs/reference_guide/library_reference/solvers/xpress_persistent.rst new file mode 100644 index 00000000000..2a98b4a09db --- /dev/null +++ b/doc/OnlineDocs/reference_guide/library_reference/solvers/xpress_persistent.rst @@ -0,0 +1,7 @@ +XpressPersistent +================ + +.. autoclass:: pyomo.solvers.plugins.solvers.xpress_persistent.XpressPersistent + :members: + :inherited-members: + :show-inheritance: diff --git a/doc/OnlineDocs/related_packages.rst b/doc/OnlineDocs/related_packages.rst new file mode 100644 index 00000000000..f32c726e9d6 --- /dev/null +++ b/doc/OnlineDocs/related_packages.rst @@ -0,0 +1,65 @@ +Related Packages +================ + +The following is list of software packages that utilize or build off +of Pyomo. This is certainly not a comprehensive list. [#f1]_ + +Modeling Extensions +------------------- + ++--------------------------+---------------------------------------------------------+---------------------------------------------+ +| Package Name | Link | Description | ++==========================+=========================================================+=============================================+ +| Coramin | https://github.com/coramin/coramin | A suite of tools for developing MINLP | +| | | algorithms | ++--------------------------+---------------------------------------------------------+---------------------------------------------+ +| PAO | https://github.com/or-fusion/pao | Formulation and solution of multilevel | +| | | optimization problems | ++--------------------------+---------------------------------------------------------+---------------------------------------------+ +| OMLT | https://github.com/cog-imperial/OMLT | Represent machine learning models within | +| | | an optimization formulation | ++--------------------------+---------------------------------------------------------+---------------------------------------------+ + + +Solvers and Solution Strategies +------------------------------- + ++--------------------------+---------------------------------------------------------+---------------------------------------------+ +| Package Name | Link | Description | ++==========================+=========================================================+=============================================+ +| Galini | https://github.com/cog-imperial/galini | An extensible, Python-based MIQCQP Solver | ++--------------------------+---------------------------------------------------------+---------------------------------------------+ +| mpi-sppy | https://github.com/pyomo/mpi-sppy | Parallel solution of | +| | | stochastic programming problems | ++--------------------------+---------------------------------------------------------+---------------------------------------------+ +| Parapint | https://github.com/parapint/parapint | Parallel solution of structured | +| | | NLPs. | ++--------------------------+---------------------------------------------------------+---------------------------------------------+ +| Suspect | https://github.com/cog-imperial/suspect | FBBT and convexity detection | ++--------------------------+---------------------------------------------------------+---------------------------------------------+ + + +Domain-Specific Applications +---------------------------- + ++--------------------------+---------------------------------------------------------+---------------------------------------------+ +| Package Name | Link | Description | ++==========================+=========================================================+=============================================+ +| Chama | https://github.com/sandialabs/chama | Sensor placement optimization | ++--------------------------+---------------------------------------------------------+---------------------------------------------+ +| Egret | https://github.com/grid-parity-exchange/egret | Formulation and solution of unit commitment| +| | | and optimal power flow problems | ++--------------------------+---------------------------------------------------------+---------------------------------------------+ +| IDAES | https://github.com/idaes/idaes-pse | Institute for the Design of Advanced | +| | | Energy Systems | ++--------------------------+---------------------------------------------------------+---------------------------------------------+ +| Prescient | https://github.com/grid-parity-exchange/prescient | Production Cost Model for power systems | +| | | simulation and analysis | ++--------------------------+---------------------------------------------------------+---------------------------------------------+ +| PyPSA | https://github.com/pypsa/pypsa | Python for Power system Analysis | ++--------------------------+---------------------------------------------------------+---------------------------------------------+ + + +.. rubric:: Footnotes + +.. [#f1] Please note that the Pyomo team does not evaluate or endorse the packages listed above. diff --git a/doc/OnlineDocs/user_guide/contributed_packages/communities_8pp.png b/doc/OnlineDocs/user_guide/contributed_packages/communities_8pp.png new file mode 100644 index 0000000000000000000000000000000000000000..a9bbd9fd58e473aae274e0147f013c17998f30c5 GIT binary patch literal 256159 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

0=L@0<5uw#A1c<0YNINF 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

_`{QW+isNyjuX2*DBUh($>|CXri{ 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 LyR(1#<>LPU(BzZx literal 0 HcmV?d00001 diff --git a/doc/OnlineDocs/user_guide/contributed_packages/communities_decode_1.png b/doc/OnlineDocs/user_guide/contributed_packages/communities_decode_1.png new file mode 100644 index 0000000000000000000000000000000000000000..7432f3f9178690b19a6972dfb1317ed27caf4fb7 GIT binary patch literal 157034 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
=W{gt%@^^{jg36NKm8fpas4V{%-qauVpB* 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 cNzexl$Yf6@rYe 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≷2>&XUaPA~qlK&!?WaKC0347nB5v&rlFol<^bC2d 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
se)%M zh~LZ>MTYkF%1Y|ACjY)*p}l2&Z>Z*G>5lY>%`#%&Izo{o_I<*A^9uw1kV$DeM;)o? zdX;blcpg4x8?0&`u3YsYm~*>-aayElmQ=(G3(TBv0}bhLIXZ1vv+Z(>Z+fW-_!reD zr~wpne*FP($_DD?uxOTHc3+%``4Uw_bE#9n6R)hur?3#k(>hdAsRF*MWa!h2>iBSf zOKtJ-=VT2xFk*W;!umi2bId`|K;@qTn}svXJI9(U2qm@$tZSJ&)`9+ad*>xeQY6#ArToV%>%MvKL+zObF{4(3gyGZ13&5ye09PTGO+B zYDmz^Wj*0oL{FK3P9F>(FZn$1W)v>VBoXT`IjRgC_4Df&0skgXEKPT$#o9 zLd=lf1^fANk=iu=bQ&>3Z-&^D%%5y`S33N0&uAuX#)h;>o!`;zFZJ2J$~16hWE-8M zjg#IM9OeHQ=|YLH(@E;q{`L-;**< zO*7{xX`UodP#>vz6O_lh7qt=JaS$hSYu2M;Yq==u-mfVlDFv}#_;zxCX2uH`$qevX zY3bW>dLc^Vi2o+SjDVY_Hg)UHvhV5VlV~5^+jLw@r+cAEDJP(}|J+g9l)@>(s9o%<$AW6neI~ppZgK>uBtm z1VLhRtGi(wQt18|qGxb0uP*uIW5kpL8p2mL%-1iSDdb4O*6C_TxDm6e7Rotn!m zX-AGnbHONutYsip4vr_cL0zQbg8eqUDJ?ZxOJ{qOH|AHThTwKecd~R4VN=N9JBAzS zuIpz{WMOO~;MwC`%!nKTpEINJ^C{(4dmfe0FJL<#zgFee8P}zE1vxko0{j0aTF6PG zjg{N*JrvwG^IpDYI*h)qXogOLAG;-JNzoVy*S1()M9>g*U&Y@}Kjc`nQ}vZ)(M^~= z+v{K;#T2&%KE-Ri^3Wh?b}iIWz?-Y5U5U@r*H}89p3yY{Ct;Sp_N}m42C1A-w)QCF zAqna({~@9#%s}zX@`}G5%?qV*mhpV2CwmL#g_n*&Te|taT_Ct$I{ZHbtSH- zh&h?ocR&-VZ6Zb<-yT|Y`18{&h*_7+=ws1aiY~Kv?_27Llr|$S^?0Ea?gfm`Z|_PdH%zIy=?`%|c{VjZm{6*<)IFh}!XMGqU-kwT(Q8vh z+=9!_x8Cse9sa!{4ubjH+T#E1xiDzFHLWPxt~&nHXuZjSMK;8H+j4ua4K1w~zGA2* zliU!lZMNgbked((xrttJ+5_GUX0g8PY(qhr8?%Jd&(?QG{uk_dHQaY`_)Yb;37DVo zCun&`yw*^Z(~ZS}z$WlyiPJiyn`QMgL6fbT&gK`(>>$nzXngX{nOmh9<{ za@S_gnRBrQJ<;L1U<_YvG#2K}>PlE_mMG$c{rM+RaJ)pEG16)l+;>g9#g$UirtY0w|5z(K zdG)e+upb^;1srN1KhTqVJSIn0j9>l7z{u92|>sw zs2&Cc3Emp{lf69x&cf9@tEXFlyb4zW`~4 BP`LmA literal 0 HcmV?d00001 diff --git a/doc/OnlineDocs/user_guide/contributed_packages/community.rst b/doc/OnlineDocs/user_guide/contributed_packages/community.rst new file mode 100644 index 00000000000..b110107e604 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/community.rst @@ -0,0 +1,389 @@ +Community Detection for Pyomo models +==================================== + +This package separates model components (variables, constraints, and objectives) into different communities +distinguished by the degree of connectivity between community members. + +Description of Package and ``detect_communities`` function +---------------------------------------------------------- +The community detection package allows users to obtain a community map of a Pyomo model - a Python dictionary-like +object that maps sequential integer values to communities within the Pyomo model. The package +takes in a model, organizes the model components into a graph of nodes and edges, then uses Louvain +community detection (`Blondel et al, 2008`_) to determine the communities that exist within the model. + +.. _Blondel et al, 2008: https://dx.doi.org/10.1088/1742-5468/2008/10/P10008 + +In graph theory, a community is defined as a subset of nodes that have a greater degree of connectivity within +themselves than they do with the rest of the nodes in the graph. In the context of Pyomo models, a community +represents a subproblem within the overall optimization problem. Identifying these subproblems and then solving them +independently can save computational work compared with trying to solve the entire model at once. Thus, it +can be very useful to know the communities that exist in a model. + +The manner in which the graph of nodes and edges is constructed from the model directly affects the community +detection. Thus, this package provides the user with a lot of control over the construction of the graph. The +function we use for this community detection is shown below: + +.. autofunction:: pyomo.contrib.community_detection.detection.detect_communities + :noindex: + +As stated above, the characteristics of the NetworkX graph of the Pyomo model are very important to the +community detection. The main graph features the user can specify are the type of community map, +whether the graph is weighted or unweighted, and whether the objective function(s) is included +in the graph generation. Below, the significance and reasoning behind including each of these options are +explained in greater depth. + +Type of Community Map (`type_of_community_map`) + In this package's main function (``detect_communities``), the user can select ``'bipartite'``, ``'constraint'``, + or ``'variable'`` as an input for the 'type_of_community_map' argument, and these result in a community map + based on a bipartite graph, a constraint node graph, or a variable node graph (respectively). + + If the user sets ``type_of_community_map='constraint'``, then each entry in the community map (which is a dictionary) contains + a list of all the constraints in the community as well as all the variables contained in those constraints. + For the model graph, a node is created for every active constraint in the model, an edge between two + constraint nodes is created only if those two constraint equations share a variable, and the + weight of each edge is equal to the number of variables the two constraint equations have in common. + + If the user sets ``type_of_community_map='variable'``, then each entry in the community map (which is a dictionary) contains + a list of all the variables in the community as well as all the constraints that contain those variables. + For the model graph, a node is created for every variable in the model, an edge between two variable nodes is + created only if those two variables occur in the same constraint equation, and the weight of each edge is equal + to the number of constraint equations in which the two variables occur together. + + If the user sets ``type_of_community_map='bipartite'``, then each entry in the community map (which is a dictionary) is + simply all of the nodes in the community but split into a list of constraints and a list of variables. + For the model graph, a node is created for every variable and every constraint in the model. An edge is created + between a constraint node and a variable node only if the constraint equation contains the variable. (Edges are + not drawn between nodes of the same type in a bipartite graph.) And as for the edge weights, the edges in the + bipartite graph are unweighted regardless of what the user specifies for the ``weighted_graph`` parameter. (This is + because for our purposes, the number of times a variable appears in a constraint is not particularly + useful.) + +Weighted Graph/Unweighted Graph (`weighted_graph`) + The Louvain community detection algorithm takes edge weights into account, so depending on whether the graph is + weighted or unweighted, the communities that are found will vary. This can be valuable depending on how + the user intends to use the community detection information. For example, if a user plans on feeding that + information into an algorithm, the algorithm may be better suited to the communities detected in a weighted + graph (or vice versa). + +With/Without Objective in the Graph (`with_objective`) + This argument determines whether the objective function(s) will be included when creating the graphical + representation of the model and thus whether the objective function(s) will be included in the community map. + Some models have an objective function that contains so many of the model variables that it obscures potential + communities within a model. Thus, it can be useful to call ``detect_communities(model, with_objective=False)`` + on such a model to see whether isolating the other components of the model provides any new insights. + +External Packages +----------------- +* NetworkX +* Python-Louvain + +The community detection package relies on two external packages, the NetworkX package and the Louvain community +detection package. Both of these packages can be installed at the following URLs (respectively): + +https://pypi.org/project/networkx/ + +https://pypi.org/project/python-louvain/ + +The pip install and conda install commands are included below as well:: + + pip install networkx + pip install python-louvain + + conda install -c anaconda networkx + conda install -c conda-forge python-louvain + +Usage Examples +-------------- + +Let's start off by taking a look at how we can use ``detect_communities`` to create a CommunityMap object. +We'll first use a model from `Allman et al, 2019`_ : + +.. _Allman et al, 2019: https://doi.org/10.1007/s11081-019-09450-5 + +.. doctest:: + :skipif: not networkx_available + + Required Imports + >>> from pyomo.contrib.community_detection.detection import detect_communities, CommunityMap, generate_model_graph + >>> from pyomo.contrib.mindtpy.tests.eight_process_problem import EightProcessFlowsheet + >>> from pyomo.core import ConcreteModel, Var, Constraint + >>> import networkx as nx + + Let's define a model for our use + >>> def decode_model_1(): + ... model = m = ConcreteModel() + ... m.x1 = Var(initialize=-3) + ... m.x2 = Var(initialize=-1) + ... m.x3 = Var(initialize=-3) + ... m.x4 = Var(initialize=-1) + ... m.c1 = Constraint(expr=m.x1 + m.x2 <= 0) + ... m.c2 = Constraint(expr=m.x1 - 3 * m.x2 <= 0) + ... m.c3 = Constraint(expr=m.x2 + m.x3 + 4 * m.x4 ** 2 == 0) + ... m.c4 = Constraint(expr=m.x3 + m.x4 <= 0) + ... m.c5 = Constraint(expr=m.x3 ** 2 + m.x4 ** 2 - 10 == 0) + ... return model + >>> model = m = decode_model_1() + >>> seed = 5 # To be used as a random seed value for the heuristic Louvain community detection + + Let's create an instance of the CommunityMap class (which is what gets returned by the + function detect_communities): + >>> community_map_object = detect_communities(model, type_of_community_map='bipartite', random_seed=seed) + +This community map object has many attributes that contain the relevant information about the +community map itself (such as the parameters used to create it, the networkX representation, and other useful +information). + +An important point to note is that the community_map attribute of the CommunityMap class is the +actual dictionary that maps integers to the communities within the model. It is expected that the user will be +most interested in the actual dictionary itself, so dict-like usage is permitted. + +If a user wishes to modify the actual dictionary (the community_map attribute of the CommunityMap object), +creating a deep copy is highly recommended (or else any destructive modifications could +have unintended consequences): ``new_community_map = copy.deepcopy(community_map_object.community_map)`` + +Let's take a closer look at the actual community map object generated by `detect_communities`: + +.. doctest:: + :skipif: not networkx_available + :hide: + + >>> from pyomo.common.formatting import tostr + >>> if tostr(community_map_object[0]) == "([c3, c4, c5], [x3, x4])": + ... _ = community_map_object.community_map + ... _[0], _[1] = _[1], _[0] + +.. doctest:: + :skipif: not networkx_available + + >>> print(community_map_object) + {0: (['c1', 'c2'], ['x1', 'x2']), 1: (['c3', 'c4', 'c5'], ['x3', 'x4'])} + + + +Printing a community map object is made to be user-friendly (by showing the community map with components +replaced by their strings). However, if the default Pyomo representation of components is desired, then the +community_map attribute or the `repr()` function can be used: + +.. doctest:: + :skipif: not networkx_available + + >>> print(community_map_object.community_map) + {0: ([, ], [, ]), 1: ([, , ], [, ])} + >>> print(repr(community_map_object)) + {0: ([, ], [, ]), 1: ([, , ], [, ])} + +`generate_structured_model` method of CommunityMap objects + It may be useful to create a new model based on the communities found in the model - we can use the + ``generate_structured_model`` method of the CommunityMap class to do this. Calling this method on a CommunityMap object + returns a new model made up of blocks that correspond to each of the communities found in the original model. Let's + take a look at the example below: + + .. doctest:: + :skipif: not networkx_available + + Use the CommunityMap object made from the first code example + >>> structured_model = community_map_object.generate_structured_model() # doctest: +SKIP + >>> structured_model.pprint() # doctest: +SKIP + 2 Set Declarations + b_index : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 2 : {0, 1} + equality_constraint_list_index : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 1 : {1,} + + 1 Var Declarations + x2 : Size=1, Index=None + Key : Lower : Value : Upper : Fixed : Stale : Domain + None : None : None : None : False : True : Reals + + 1 Constraint Declarations + equality_constraint_list : Equality Constraints for the different forms of a given variable + Size=1, Index=equality_constraint_list_index, Active=True + Key : Lower : Body : Upper : Active + 1 : 0.0 : b[0].x2 - x2 : 0.0 : True + + 1 Block Declarations + b : Size=2, Index=b_index, Active=True + b[0] : Active=True + 2 Var Declarations + x1 : Size=1, Index=None + Key : Lower : Value : Upper : Fixed : Stale : Domain + None : None : None : None : False : True : Reals + x2 : Size=1, Index=None + Key : Lower : Value : Upper : Fixed : Stale : Domain + None : None : None : None : False : True : Reals + + 2 Constraint Declarations + c1 : Size=1, Index=None, Active=True + Key : Lower : Body : Upper : Active + None : -Inf : b[0].x1 + b[0].x2 : 0.0 : True + c2 : Size=1, Index=None, Active=True + Key : Lower : Body : Upper : Active + None : -Inf : b[0].x1 - 3*b[0].x2 : 0.0 : True + + 4 Declarations: x1 x2 c1 c2 + b[1] : Active=True + 2 Var Declarations + x3 : Size=1, Index=None + Key : Lower : Value : Upper : Fixed : Stale : Domain + None : None : None : None : False : True : Reals + x4 : Size=1, Index=None + Key : Lower : Value : Upper : Fixed : Stale : Domain + None : None : None : None : False : True : Reals + + 3 Constraint Declarations + c3 : Size=1, Index=None, Active=True + Key : Lower : Body : Upper : Active + None : 0.0 : x2 + b[1].x3 + 4*b[1].x4**2 : 0.0 : True + c4 : Size=1, Index=None, Active=True + Key : Lower : Body : Upper : Active + None : -Inf : b[1].x3 + b[1].x4 : 0.0 : True + c5 : Size=1, Index=None, Active=True + Key : Lower : Body : Upper : Active + None : 0.0 : b[1].x3**2 + b[1].x4**2 - 10 : 0.0 : True + + 5 Declarations: x3 x4 c3 c4 c5 + + 5 Declarations: b_index b x2 equality_constraint_list_index equality_constraint_list + + We see that there is an equality constraint list (`equality_constraint_list`) that has been created. This is due to + the fact that the ``detect_communities`` function can return a community map that has Pyomo components (variables, + constraints, or objectives) in more than one community, and thus, an equality_constraint_list is created to ensure that + the new model still corresponds to the original model. This is explained in more detail below. + + Consider the case where community detection is done on a constraint node graph - this would result in communities + that are made up of the corresponding constraints as well as all the variables that occur in the given constraints. + Thus, it is possible for certain Pyomo components to be in multiple communities (and a similar argument exists + for community detection done on a variable node graph). As a result, our structured model (the model returned by + the ``generate_structured_model`` method) may need to have several "copies" of a certain component. For example, + a variable `original_model.x1` that exists in the original model may have corresponding forms + `structured_model.b[0].x1`, `structured_model.b[0].x1`, `structured_model.x1`. In order for these components to + meaningfully correspond to their counterparts in the original model, they must be bounded by equality constraints. + Thus, we use an `equality_constraint_list` to bind different forms of a component from the original model. + + The last point to make about this method is that variables will be created outside of blocks if (1) an objective + is not inside a block (for example if the community detection is done `with_objective=False`) or if (2) an + objective/constraint contains a variable that is not in the same block as the given objective/constraint. + +`visualize_model_graph` method of CommunityMap objects + If we want a visualization of the communities within the Pyomo model, we can use ``visualize_model_graph`` to do + so. Let's take a look at how this can be done in the following example: + + .. doctest:: + :skipif: not matplotlib_available or not networkx_available + + Create a CommunityMap object (so we can demonstrate the visualize_model_graph method) + >>> community_map_object = cmo = detect_communities(model, type_of_community_map='bipartite', random_seed=seed) + + Generate a matplotlib figure (left_figure) - a constraint graph of the community map + >>> left_figure, _ = cmo.visualize_model_graph(type_of_graph='constraint') + + Now, we will generate the figure on the right (a bipartite graph of the community map) + >>> right_figure, _ = cmo.visualize_model_graph(type_of_graph='bipartite') + +An example of the two separate graphs created for these two function calls is shown below: + .. image:: communities_decode_1.png + :width: 100% + :alt: Graphical representation of the communities in the model 'decode_model_1' for two different types of graphs + + These graph drawings very clearly demonstrate the communities within this model. The constraint graph (which is colored + using the bipartite community map) shows a very simple illustration - one node for each constraint, with only one edge + connecting the two communities (which represents the variable `m.x2` common to `m.c2` and `m.c3` in separate + communities) + The bipartite graph is slightly more complicated and we can see again how there is only one edge between the two + communities and more edges within each community. This is an ideal situation for breaking a + model into separate communities since there is little connectivity between the communities. Also, note that we can + choose different graph types (such as a variable node graph, constraint node graph, or bipartite graph) for a given + community map. + + Let's try a more complicated model (taken from `Duran & Grossmann, 1986`_) - this example will demonstrate how the same + graph can be illustrated using different community maps (in the previous example we illustrated different graphs with a + single community map): + + .. _Duran & Grossmann, 1986: https://dx.doi.org/10.1007/BF02592064 + + .. doctest:: + :skipif: not matplotlib_available or not networkx_available + + Define the model + >>> model = EightProcessFlowsheet() + + Now, we follow steps similar to the example above (see above for explanations) + >>> community_map_object = cmo = detect_communities(model, type_of_community_map='constraint', random_seed=seed) + >>> left_fig, pos = cmo.visualize_model_graph(type_of_graph='variable') + + As we did before, we will use the returned 'pos' to create a consistent graph layout + >>> community_map_object = cmo = detect_communities(model, type_of_community_map='bipartite') + >>> middle_fig, _ = cmo.visualize_model_graph(type_of_graph='variable', pos=pos) + + >>> community_map_object = cmo = detect_communities(model, type_of_community_map='variable') + >>> right_fig, _ = cmo.visualize_model_graph(type_of_graph='variable', pos=pos) + +We can see an example for the three separate graphs created by these three function calls below: + .. image:: communities_8pp.png + :width: 100% + :alt: Graphical representation of the communities in the model 'decode_model_1' for slightly different function calls + + The three graphs above are all variable graphs - which means the nodes represent variables in the model, and the edges + represent constraint equations. The coloring differs because the three graphs rely on community maps that were + created based on a constraint node graph, a bipartite graph, and a variable node graph (from left to right). For + example, the community map that was generated from a constraint node graph (``type_of_community_map='constraint'``) + resulted in three communities (as seen by the purple, yellow, and blue nodes). + +`generate_model_graph` function + Now, we will take a look at ``generate_model_graph`` - this function can be used to create a NetworkX + graph for a Pyomo model (and is used in `detect_communities`). Here, we will create a NetworkX graph from + the model in our first example and then create the edge and adjacency list for the graph. + + ``generate_model_graph`` returns three things: + + * a NetworkX graph of the given model + * a dictionary that maps the numbers used to represent the model components to + the actual components (because Pyomo components cannot be directly added to a NetworkX graph) + * a dictionary that maps constraints to the variables in them. + + For this example, we will only need the NetworkX graph of the model and the number-to-component mapping. + + .. doctest:: + :skipif: not networkx_available + + Define the model + >>> model = decode_model_1() + + See above for the description of the items returned by 'generate_model_graph' + >>> model_graph, number_component_map, constr_var_map = generate_model_graph(model, type_of_graph='constraint') + + The next two lines create and implement a mapping to change the node values from numbers into + strings. The second line uses this mapping to create string_model_graph, which has + the relabeled nodes (strings instead of numbers). + + >>> string_map = dict((number, str(comp)) for number, comp in number_component_map.items()) + >>> string_model_graph = nx.relabel_nodes(model_graph, string_map) + + Now, we print the edge list and the adjacency list: + Edge List: + >>> for line in nx.generate_edgelist(string_model_graph): print(line) # doctest: +SKIP + c1 c2 {'weight': 2} + c1 c3 {'weight': 1} + c2 c3 {'weight': 1} + c3 c5 {'weight': 2} + c3 c4 {'weight': 2} + c4 c5 {'weight': 2} + + Adjacency List: + >>> print(list(nx.generate_adjlist(string_model_graph))) # doctest: +SKIP + ['c1 c2 c3', 'c2 c3', 'c3 c5 c4', 'c4 c5', 'c5'] + + It's worth mentioning that in the code above, we do not have to create ``string_map`` to create an edge list + or adjacency list, but for the sake of having an easily understandable output, it is quite helpful. (Without + relabeling the nodes, the output below would not have the strings of the components but instead would have + integer values.) This code will hopefully make it easier for a user to do the same. + +Functions in this Package +------------------------- +.. automodule:: pyomo.contrib.community_detection.detection + :members: + +.. automodule:: pyomo.contrib.community_detection.community_graph + :members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/doe/CCSI-license.txt b/doc/OnlineDocs/user_guide/contributed_packages/doe/CCSI-license.txt new file mode 100644 index 00000000000..4b0dadd9e06 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/doe/CCSI-license.txt @@ -0,0 +1,43 @@ +# Pyomo.DoE was originally developed as part of the Carbon Capture Simulation for Industry +# Impact (CCSI2) project under the following license: +# +# *** License Agreement *** +# +# Pyomo.DoE Copyright (c) 2022, by the software owners: TRIAD National Security, LLC., Lawrence +# Livermore National Security, LLC., Lawrence Berkeley National Laboratory, +# Pacific Northwest National Laboratory, Battelle Memorial Institute, University of Notre Dame, +# The University of Pittsburgh, The University of Texas at Austin, University of Toledo, +# West Virginia University, et al. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted provided +# that the following conditions are met: +# (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the +# following disclaimer. +# (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and +# the following disclaimer in the documentation and/or other materials provided with the distribution. +# (3) Neither the name of the Carbon Capture Simulation for Industry Impact, +# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., +# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, +# Battelle Memorial Institute, University of Notre Dame, The University of Pittsburgh, +# U.S. Dept. of Energy nor the names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# You are under no obligation whatsoever to provide any bug fixes, patches, or upgrades to the features, +# functionality or performance of the source code ("Enhancements") to anyone; however, if you choose to +# make your Enhancements available either publicly, or directly to Lawrence Berkeley National Laboratory, +# without imposing a separate written license agreement for such Enhancements, then you hereby grant +# the following license: a non-exclusive, royalty-free perpetual license to install, use, modify, prepare +# derivative works, incorporate into other computer software, distribute, and sublicense such +# enhancements or derivative works thereof, in binary and source code form. +# +# Lead Developers: Jialu Wang and Alexander Dowling, University of Notre Dame diff --git a/doc/OnlineDocs/user_guide/contributed_packages/doe/doe.rst b/doc/OnlineDocs/user_guide/contributed_packages/doe/doe.rst new file mode 100644 index 00000000000..8c22ff7370d --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/doe/doe.rst @@ -0,0 +1,291 @@ +Pyomo.DoE +========= + +**Pyomo.DoE** (Pyomo Design of Experiments) is a Python library for model-based design of experiments using science-based models. + +Pyomo.DoE was developed by **Jialu Wang** and **Alexander W. Dowling** at the University of Notre Dame as part of the `Carbon Capture Simulation for Industry Impact (CCSI2) `_. +project, funded through the U.S. Department Of Energy Office of Fossil Energy. + +If you use Pyomo.DoE, please cite: + +[Wang and Dowling, 2022] Wang, Jialu, and Alexander W. Dowling. +"Pyomo.DOE: An open‐source package for model‐based design of experiments in Python." +AIChE Journal 68.12 (2022): e17813. `https://doi.org/10.1002/aic.17813` + +Methodology Overview +--------------------- + +Model-based Design of Experiments (MBDoE) is a technique to maximize the information gain of experiments by directly using science-based models with physically meaningful parameters. It is one key component in the model calibration and uncertainty quantification workflow shown below: + +.. figure:: flowchart.png + :scale: 25 % + + The exploratory analysis, parameter estimation, uncertainty analysis, and MBDoE are combined into an iterative framework to select, refine, and calibrate science-based mathematical models with quantified uncertainty. Currently, Pyomo.DoE focuses on increasing parameter precision. + +Pyomo.DoE provides the exploratory analysis and MBDoE capabilities to the Pyomo ecosystem. The user provides one Pyomo model, a set of parameter nominal values, +the allowable design spaces for design variables, and the assumed observation error model. +During exploratory analysis, Pyomo.DoE checks if the model parameters can be inferred from the postulated measurements or preliminary data. +MBDoE then recommends optimized experimental conditions for collecting more data. +Parameter estimation packages such as `Parmest `_ can perform parameter estimation using the available data to infer values for parameters, +and facilitate an uncertainty analysis to approximate the parameter covariance matrix. +If the parameter uncertainties are sufficiently small, the workflow terminates and returns the final model with quantified parametric uncertainty. +If not, MBDoE recommends optimized experimental conditions to generate new data. + +Below is an overview of the type of optimization models Pyomo.DoE can accommodate: + +* Pyomo.DoE is suitable for optimization models of **continuous** variables +* Pyomo.DoE can handle **equality constraints** defining state variables +* Pyomo.DoE supports (Partial) Differential-Algebraic Equations (PDAE) models via Pyomo.DAE +* Pyomo.DoE also supports models with only algebraic constraints + +The general form of a DAE problem that can be passed into Pyomo.DoE is shown below: + +.. math:: + \begin{align*} + & \dot{\mathbf{x}}(t) = \mathbf{f}(\mathbf{x}(t), \mathbf{z}(t), \mathbf{y}(t), \mathbf{u}(t), \overline{\mathbf{w}}, \boldsymbol{\theta}) \\ + & \mathbf{g}(\mathbf{x}(t), \mathbf{z}(t), \mathbf{y}(t), \mathbf{u}(t), \overline{\mathbf{w}},\boldsymbol{\theta})=\mathbf{0} \\ + & \mathbf{y} =\mathbf{h}(\mathbf{x}(t), \mathbf{z}(t), \mathbf{u}(t), \overline{\mathbf{w}},\boldsymbol{\theta}) \\ + & \mathbf{f}^{\mathbf{0}}\left(\dot{\mathbf{x}}\left(t_{0}\right), \mathbf{x}\left(t_{0}\right), \mathbf{z}(t_0), \mathbf{y}(t_0), \mathbf{u}\left(t_{0}\right), \overline{\mathbf{w}}, \boldsymbol{\theta})\right)=\mathbf{0} \\ + & \mathbf{g}^{\mathbf{0}}\left( \mathbf{x}\left(t_{0}\right),\mathbf{z}(t_0), \mathbf{y}(t_0), \mathbf{u}\left(t_{0}\right), \overline{\mathbf{w}}, \boldsymbol{\theta}\right)=\mathbf{0}\\ + &\mathbf{y}^{\mathbf{0}}\left(t_{0}\right)=\mathbf{h}\left(\mathbf{x}\left(t_{0}\right),\mathbf{z}(t_0), \mathbf{u}\left(t_{0}\right), \overline{\mathbf{w}}, \boldsymbol{\theta}\right) + \end{align*} + +where: + +* :math:`\boldsymbol{\theta} \in \mathbb{R}^{N_p}` are unknown model parameters. +* :math:`\mathbf{x} \subseteq \mathcal{X}` are dynamic state variables which characterize trajectory of the system, :math:`\mathcal{X} \in \mathbb{R}^{N_x \times N_t}`. +* :math:`\mathbf{z} \subseteq \mathcal{Z}` are algebraic state variables, :math:`\mathcal{Z} \in \mathbb{R}^{N_z \times N_t}`. +* :math:`\mathbf{u} \subseteq \mathcal{U}` are time-varying decision variables, :math:`\mathcal{U} \in \mathbb{R}^{N_u \times N_t}`. +* :math:`\overline{\mathbf{w}} \in \mathbb{R}^{N_w}` are time-invariant decision variables. +* :math:`\mathbf{y} \subseteq \mathcal{Y}` are measurement response variables, :math:`\mathcal{Y} \in \mathbb{R}^{N_r \times N_t}`. +* :math:`\mathbf{f}(\cdot)` are differential equations. +* :math:`\mathbf{g}(\cdot)` are algebraic equations. +* :math:`\mathbf{h}(\cdot)` are measurement functions. +* :math:`\mathbf{t} \in \mathbb{R}^{N_t \times 1}` is a union of all time sets. + +.. note:: + * Parameters and design variables should be defined as Pyomo ``Var`` components on the model to use ``direct_kaug`` mode, and can be defined as Pyomo ``Param`` object if not using ``direct_kaug``. + +Based on the above notation, the form of the MBDoE problem addressed in Pyomo.DoE is shown below: + +.. math:: + \begin{equation} + \begin{aligned} + \underset{\boldsymbol{\varphi}}{\max} \quad & \Psi (\mathbf{M}(\mathbf{\hat{y}}, \boldsymbol{\varphi})) \\ + \text{s.t.} \quad & \mathbf{M}(\boldsymbol{\hat{\theta}}, \boldsymbol{\varphi}) = \sum_r^{N_r} \sum_{r'}^{N_r} \tilde{\sigma}_{(r,r')}\mathbf{Q}_r^\mathbf{T} \mathbf{Q}_{r'} + \mathbf{V}^{-1}_{\boldsymbol{\theta}}(\boldsymbol{\hat{\theta}}) \\ + & \dot{\mathbf{x}}(t) = \mathbf{f}(\mathbf{x}(t), \mathbf{z}(t), \mathbf{y}(t), \mathbf{u}(t), \overline{\mathbf{w}}, \boldsymbol{\theta}) \\ + & \mathbf{g}(\mathbf{x}(t), \mathbf{z}(t), \mathbf{y}(t), \mathbf{u}(t), \overline{\mathbf{w}},\boldsymbol{\theta})=\mathbf{0} \\ + & \mathbf{y} =\mathbf{h}(\mathbf{x}(t), \mathbf{z}(t), \mathbf{u}(t), \overline{\mathbf{w}},\boldsymbol{\theta}) \\ + & \mathbf{f}^{\mathbf{0}}\left(\dot{\mathbf{x}}\left(t_{0}\right), \mathbf{x}\left(t_{0}\right), \mathbf{z}(t_0), \mathbf{y}(t_0), \mathbf{u}\left(t_{0}\right), \overline{\mathbf{w}}, \boldsymbol{\theta})\right)=\mathbf{0} \\ + & \mathbf{g}^{\mathbf{0}}\left( \mathbf{x}\left(t_{0}\right),\mathbf{z}(t_0), \mathbf{y}(t_0), \mathbf{u}\left(t_{0}\right), \overline{\mathbf{w}}, \boldsymbol{\theta}\right)=\mathbf{0}\\ + &\mathbf{y}^{\mathbf{0}}\left(t_{0}\right)=\mathbf{h}\left(\mathbf{x}\left(t_{0}\right),\mathbf{z}(t_0), \mathbf{u}\left(t_{0}\right), \overline{\mathbf{w}}, \boldsymbol{\theta}\right) + \end{aligned} + \end{equation} + +where: + +* :math:`\boldsymbol{\varphi}` are design variables, which are manipulated to maximize the information content of experiments. It should consist of one or more of :math:`\mathbf{u}(t), \mathbf{y}^{\mathbf{0}}({t_0}),\overline{\mathbf{w}}`. With a proper model formulation, the timepoints for control or measurements :math:`\mathbf{t}` can also be degrees of freedom. +* :math:`\mathbf{M}` is the Fisher information matrix (FIM), estimated as the inverse of the covariance matrix of parameter estimates :math:`\boldsymbol{\hat{\theta}}`. A large FIM indicates more information contained in the experiment for parameter estimation. +* :math:`\mathbf{Q}` is the dynamic sensitivity matrix, containing the partial derivatives of :math:`\mathbf{y}` with respect to :math:`\boldsymbol{\theta}`. +* :math:`\Psi` is the design criteria to measure FIM. +* :math:`\mathbf{V}_{\boldsymbol{\theta}}(\boldsymbol{\hat{\theta}})^{-1}` is the FIM of previous experiments. + +Pyomo.DoE provides four design criteria :math:`\Psi` to measure the size of FIM: + +.. list-table:: Pyomo.DoE design criteria + :header-rows: 1 + :class: tight-table + + * - Design criterion + - Computation + - Geometrical meaning + * - A-optimality + - :math:`\text{trace}({\mathbf{M}})` + - Dimensions of the enclosing box of the confidence ellipse + * - D-optimality + - :math:`\text{det}({\mathbf{M}})` + - Volume of the confidence ellipse + * - E-optimality + - :math:`\text{min eig}({\mathbf{M}})` + - Size of the longest axis of the confidence ellipse + * - Modified E-optimality + - :math:`\text{cond}({\mathbf{M}})` + - Ratio of the longest axis to the shortest axis of the confidence ellipse + +In order to solve problems of the above, Pyomo.DoE implements the 2-stage stochastic program. Please see Wang and Dowling (2022) for details. + +Pyomo.DoE Required Inputs +-------------------------------- +The required inputs to the Pyomo.DoE solver are the following: + +* A function that creates the process model +* Dictionary of parameters and their nominal value +* A measurement object +* A design variables object +* A Numpy ``array`` containing the Prior FIM +* Optimization solver + +Below is a list of arguments that Pyomo.DoE expects the user to provide. + +parameter_dict : ``dictionary`` + A ``dictionary`` of parameter names and values. If they are an indexed variable, put the variable name and index in a nested ``Dictionary``. + +design_variables: ``DesignVariables`` + A ``DesignVariables`` of design variables, provided by the DesignVariables class. + If this design var is independent of time (constant), set the time to [0] + +measurement_variables : ``MeasurementVariables`` + A ``MeasurementVariables`` of the measurements, provided by the MeasurementVariables class. + +create_model : ``function`` + A ``function`` returning a deterministic process model. + +prior_FIM : ``array`` + An ``array`` defining the Fisher information matrix (FIM) for prior experiments, default is a zero matrix. + +Pyomo.DoE Solver Interface +--------------------------- + +.. figure:: uml.png + :scale: 25 % + + +.. autoclass:: pyomo.contrib.doe.doe.DesignOfExperiments + :members: __init__, stochastic_program, compute_FIM, run_grid_search + +.. Note:: + ``stochastic_program()`` includes the following steps: + #. Build two-stage stochastic programming optimization model where scenarios correspond to finite difference approximations for the Jacobian of the response variables with respect to calibrated model parameters + #. Fix the experiment design decisions and solve a square (i.e., zero degrees of freedom) instance of the two-stage DOE problem. This step is for initialization. + #. Unfix the experiment design decisions and solve the two-stage DOE problem. + +.. autoclass:: pyomo.contrib.doe.measurements.MeasurementVariables + :members: __init__, add_variables + +.. autoclass:: pyomo.contrib.doe.measurements.DesignVariables + :members: __init__, add_variables + +.. autoclass:: pyomo.contrib.doe.scenario.ScenarioGenerator + :special-members: __init__ + +.. autoclass:: pyomo.contrib.doe.result.FisherResults + :members: __init__, result_analysis + +.. autoclass:: pyomo.contrib.doe.result.GridSearchResult + :special-members: __init__ + + +Pyomo.DoE Usage Example +----------------------- + +We illustrate the use of Pyomo.DoE using a reaction kinetics example (Wang and Dowling, 2022). +The Arrhenius equations model the temperature dependence of the reaction rate coefficient :math:`k_1, k_2`. Assuming a first-order reaction mechanism gives the reaction rate model. Further, we assume only species A is fed to the reactor. + + +.. math:: + \begin{equation} + \begin{aligned} + k_1 & = A_1 e^{-\frac{E_1}{RT}} \\ + k_2 & = A_2 e^{-\frac{E_2}{RT}} \\ + \frac{d{C_A}}{dt} & = -k_1{C_A} \\ + \frac{d{C_B}}{dt} & = k_1{C_A} - k_2{C_B} \\ + C_{A0}& = C_A + C_B + C_C \\ + C_B(t_0) & = 0 \\ + C_C(t_0) & = 0 \\ + \end{aligned} + \end{equation} + + + +:math:`C_A(t), C_B(t), C_C(t)` are the time-varying concentrations of the species A, B, C, respectively. +:math:`k_1, k_2` are the rates for the two chemical reactions using an Arrhenius equation with activation energies :math:`E_1, E_2` and pre-exponential factors :math:`A_1, A_2`. +The goal of MBDoE is to optimize the experiment design variables :math:`\boldsymbol{\varphi} = (C_{A0}, T(t))`, where :math:`C_{A0},T(t)` are the initial concentration of species A and the time-varying reactor temperature, to maximize the precision of unknown model parameters :math:`\boldsymbol{\theta} = (A_1, E_1, A_2, E_2)` by measuring :math:`\mathbf{y}(t)=(C_A(t), C_B(t), C_C(t))`. +The observation errors are assumed to be independent both in time and across measurements with a constant standard deviation of 1 M for each species. + + +Step 0: Import Pyomo and the Pyomo.DoE module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. doctest:: + + >>> # === Required import === + >>> import pyomo.environ as pyo + >>> from pyomo.dae import ContinuousSet, DerivativeVar + >>> from pyomo.contrib.doe import DesignOfExperiments, MeasurementVariables, DesignVariables + >>> import numpy as np + +Step 1: Define the Pyomo process model +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The process model for the reaction kinetics problem is shown below. + +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_kinetics.py + :language: python + :pyobject: create_model + +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_kinetics.py + :language: python + :pyobject: disc_for_measure + +.. note:: + The model requires at least two options: "block" and "global". Both options requires the pass of a created empty Pyomo model. + With "global" option, only design variables and their time sets need to be defined; + With "block" option, a full model needs to be defined. + + +Step 2: Define the inputs for Pyomo.DoE +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_compute_FIM.py + :language: python + :start-at: # Control time set + :end-before: ### Compute + + +Step 3: Compute the FIM of a square MBDoE problem +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This method computes an MBDoE optimization problem with no degree of freedom. + +This method can be accomplished by two modes, ``direct_kaug`` and ``sequential_finite``. +``direct_kaug`` mode requires the installation of the solver `k_aug `_. + +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_compute_FIM.py + :language: python + :start-after: ### Compute the FIM + :end-before: # test result + +Step 4: Exploratory analysis (Enumeration) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Exploratory analysis is suggested to enumerate the design space to check if the problem is identifiable, +i.e., ensure that D-, E-optimality metrics are not small numbers near zero, and Modified E-optimality is not a big number. + +Pyomo.DoE accomplishes the exploratory analysis with the ``run_grid_search`` function. +It allows users to define any number of design decisions. Heatmaps can be drawn by two design variables, fixing other design variables. +1D curve can be drawn by one design variable, fixing all other variables. +The function ``run_grid_search`` enumerates over the design space, each MBDoE problem accomplished by ``compute_FIM`` method. +Therefore, ``run_grid_search`` supports only two modes: ``sequential_finite`` and ``direct_kaug``. + +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_grid_search.py + :language: python + :pyobject: main + +Successful run of the above code shows the following figure: + +.. figure:: grid-1.png + :scale: 35 % + +A heatmap shows the change of the objective function, a.k.a. the experimental information content, in the design region. Horizontal and vertical axes are two design variables, while the color of each grid shows the experimental information content. Taking the Fig. Reactor case - A optimality as example, A-optimality shows that the most informative region is around $C_{A0}=5.0$ M, $T=300.0$ K, while the least informative region is around $C_{A0}=1.0$ M, $T=700.0$ K. + +Step 5: Gradient-based optimization +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pyomo.DoE accomplishes gradient-based optimization with the ``stochastic_program`` function for A- and D-optimality design. + +This function solves twice: It solves the square version of the MBDoE problem first, and then unfixes the design variables as degree of freedoms and solves again. In this way the optimization problem can be well initialized. + +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_optimize_doe.py + :language: python + :pyobject: main + + diff --git a/doc/OnlineDocs/user_guide/contributed_packages/doe/flowchart.png b/doc/OnlineDocs/user_guide/contributed_packages/doe/flowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..2e66566d2f66774927be3362919c4cc6d5346916 GIT binary patch literal 160954 zcmeFZWmuHkzW|Dah=`~t2uK)+NQ_7~ihzKGN~eN!NjEquqO>3&%|<1Kkro&jR2u0R zx}{^tp}Fe?#yI=zbN>6e_rv{ge4cS;*1OjFb*?qMQc{p6J4$mD4-bz_=Ju_7cz8#V zczF2kM-GFLq}QR@czDOmOeG|gWF#aQm29nzOf3xY@NT~fQzKTsUqch86%riWgnu&l zlqPxCgp4K5{^z-ekOYGd#zi~r@%Hi(m zFuwDLbg*GdV=;wG^y0F`}O6+*H6)r8GF%QN*zfrzCDNy!=1Fwwe zvyIDj5>{T`qpM{PHnEF4l;Wpn9i)lSVpY55%|oP*gz zdwI05B3Jkoe&aKBwWD;;odfWVuP#^-DX0xs9*TX=&s(l`Na>wzVapvnZhCP6uJ{NN z7j99B8MZOMay*m3C;U&CM;Fhlu8CgYbMnQHkgyYbD@HSHMrHAcrVJBbVDd7tv%iJ_qT5Rz>C@+<#|Z|zSNsD9X- zV(jp7&Jw4jL9}#TjXjl|?cHH?yNfMsOIS_tf!f3A*TO=tKMZ@Zo_?&n^;Qag?L4*q z(em*1#P=pIc^Dl-46CHm?4`~e>1uu^c+Vx~tC!r%5yD~2117QX`153b_a`GpYY&f9 zcm`jO*nUL%5}$&5{wG|Y75J< zi%&=1*xC4AVpjbS6isUT^^C3E%Ra;Pg|tHFd-U2dvYx^cAym-`%M`Hc;lMzZjb{kF zyHCh&;!#vTlqbF~PI)5FzTM$Vob9D!kKv~u;U9XeEOXfS61o4&AujsUPsA8c;dM13 z2jO=QRr@T$B<}raJ^ARE{}}oA>cfra8;D*r60iOs$SIfT2r1*ba&r5dAeAcpG9R&O z#TqJtToxsxu5{`{NHsD0bsl1#SAUhC&?n^dH}RnzKJ=M^o+$ptr=sEh&y5ex4J?y1 zl4Lw*ejfdt_iJBG;>}5N-M~YZ!D4s9{FdkK;93$2TYU12Ok;v{^Cr#SCt_W}(#zy- z&#YBn%S77c*(EO-|4@&6;2Nhtl9hbh=(c3!SK-T~J9LD10^g2vKb@SKnL7GpSnu7I zdx^oZEr}DA71r5beel+jm$6tr&rr!P+isr;tadZ4uSc8VnN%I(M+GxSjIkPi9=`EJ zk3{}+>#-*!pYZap(W?!TY>A&ZLiFy&vDA>-u#T2FZoVA zd4A#U?Q)kv1?vUBtCktXZ}IJwq`w$jtu;#F`;ce94uhG(h$i}D52nr%aD6++ zY3(0S@gP~=;oE~)3m-~ zOz;rNNJW<9Hl+q7dZqr<^z`WT&6FS&s?^5&Fg39RPu=aT4e7Mw?Qyk93JJBTbt%Ou z@@>U!PTVy0QIxmjKKy77Z%#8wHVHB*p|K}pCJQ{QXOJo7EtOHp6M&9-B+n`zK6a@d zzy5l?M3fqLac;F{ho%L4X2-dVqZ#is3-VlZC%ZYiIlBwHN zeC_k6V_IBft0pMzd1IyZ#0PGONm!$qrn)C=?Q`tq9I%u56OPl96NS@Elk({FCZQyB zW!c$|W5+N0WX0G7+xRV*zFoN1aqY_|rf+61bZA#^czyN^E)_@J@xaDD;K5(r;pQm&<4uG<@4$Zw4y8%W|eznduSbR30W6g6w5oR&E9n*cl*o3d;1J= z+n3fGqn~R2av*&8yz$7w%i5C#bg6-CRqj=0RaFYdlCLD+OcqQ=Uu2g~3e*-@?5##L z&^HJ)2*u17Pki=wWU)7W>@saOUu&1~xMWDmj?^xCT6KQ4vhRz|U}@KOAaS38f7OK` zqaawEZrr^_nnnef9D*AWpj%MlP-0LL;Tn%Uzec$EeZ_QnePVhtdfcrEt0*RovOtM= z(!H~gB9WRsyV5q$8$XSzN3lPl4yOFXlfd4@7EG&7y>_Yk!b^r+t_h;Lz_5z$I#;@x zb8KXyr?%j;b#e7pN59yD)#pFWN~5>MTK`o_zb{_4~= z(FWg#Y|{Aq-jzr-(t8Mo&`>iJo?n!Cmn^>x&;_Hgfp> zO+C6ZH47EU6c|9K#kuMdBiH-!v)#h-TFrvn@BlL*Gn?Y)c$4TWiWUkjccOxo!rRV*ao^Om?T*WB>^*D6RKOe?TFO}xT{V_5QZh`ufW`L z`ZkxEvExw!Z^N$~C$(~KEMI25U@q6;GxK&vr)yiFMqtyf-L};>$k(B~Yq<6PouoVQ zEmv)VPs?>DZ7ApnDZ(guX;`u9UGa)x2-bT&AjEFk{nuo7Yeg%p z@l$-(qe{Q0pPt4W4|BP2MH@%fM~O`@Sai%-+64VENTg7w=byD^ z>02~SH<^yL9O^M+&0wv|rzjY=_A0$-T2&Zd;+5HL`Z>pbJVIw!$Fg|DF+OuKEv?%i z*Vu(elN~YFEA()_-Q)b`l=(UH{8{4}m4&G7>eV;o0 zXsjl=J~Vnhp2lgaaxyDftKn9hfEVw~Osd^SJLZPXcx&XK+t7sMyq`maFsxm7^|SjZ zU(i5cu5*5y?n2Q$ul8{>iRqM%5i~v`7D;crJww(IAjkL4I=^2Q=`g ztk=U1wyQg^?LvNpT$bKUgw5^Ia>}Ap@c_2KC7;gi+~V3|=o&LNelf1p^|IS)VabHR ztV%J|$99<01Qq8cA62nubaOuIsu`oxAdKaYyh;nb+6a&0+Bc!i2-Tz6Qd@Bu@xF zX*NFV{eM}<1 z@_1Ll=OcK8hiLGKz^6mtC3=YV=Vz%ySMUh-#_{p+d`$5O59TOhgC44XiCW9zL?xH{@`(d<^Y_C+sW;K3W>uKV)>aw6L-h zbQZa=H$xD7hK6Am826^wn~Pjfmsetxu(ma1TfE>@6-cnQs??B6-e}_cD ze^>V3!RJGP(K%E4<9K-Dcrv$cC_5jT=_7ip-M-C-(Gxze)XOqA$MMCF;v>KJa7$u^ zuN+3~4JB(uhRNeA988~!wH~vax>PCTZ_ljF$eW<1{DKIYO=i0!p&5E);JK6Sg) zEx8Kjt>%1^QTz$sA$-CkC;pQUDfi&?_y`qE>beX6W|_%}6V59){u2N{=cJe&f}_(o zf-3$Mf=7@W$NnQiaB!6Ko|KuZL{ts`XY?N|Bh>Xc{I3?IX^Ic#rPUeO{D*wrhl7t0 zCHPkhRM;6Yn(aTb`2SOR?!kep#s9|&04;#0QJ!$lm|9dn_ivUxDNAqs&lL^85mzBS zf>d>IVGD$o70C`p=y^`2rGA(AXMGJehtoH|lg{LOuyDXPc+qKqYh88^H`rrJ?!ouB zdioeR7L87t4gY6s5+vbal#zm~J!bus2-8bARZr0cIysdQ5C-X_I2Fkeq~po=rvI!6 zJd8jb1d_Q#`{GF8AR^SA&Yt0w2Rz(31@J<+_OSmW4-?n{w0wK*iwEG95kOT}Ix1@+ zfC+wtnn#hx>A4N4>aA%0S=?|ls?S3^SUb5pl$tB z592@6CJ$)ib2QB#8GNuYPG4~oPcSwm0o{of=D^Wqdmd2XcU|KW zgr$Gbld|cx32M22G^}bASiWO!LX4vg=Q2`U00~jBEHd22IDz0M;uCfQ1GEgSl7u7+ z5EADAblTm$><38*zHst{GaZA70mOm-9{M`arFt*2S=>`b%oNw&R30D*%_XM%8wVTz zop$kPpbVCapKl+mt@Z{~aMV6}4PkOk6g(VxXUz6@IUS5J?t&TXh`;D?xZ%8h!Wa;Q zd1*N>ZsWh>ZgL+egFHPIj;=yr9QBfsY#|VqmD1va4(`kTO#eVKuunZ;4!b)=V9E&zxa&fyyej&$o!g-3pgsM02PH-LM0&ym8}6~4-AUS`unnj5%?C+ zr7_eCM;vazsHq|P;QwdTIF|e&?*GlGqlk6+Gz^nE9cMMFjAj1^MJKeq1Z?u1zQ)x9 z$t9ctwx#-jn*zWSp9O;JP+*^Z{&zJ$7-^yg(;HH#ap5tA1AL!K^J(9+y2C&Vs1u_0 z^be;ge<(rlMKJx`nYw#8&B_1}^YP#9xpy<5v3ZVWrp$jV`JEr|1~A=Gs{Sm_%pE{< zR8dlJw6pVe~ezH|dIOWgPab0Afa&$z@3KyMd3& zh}G;S`2!*j62!~|E-*dzE*CCvD(Qgl?JDHy5W?_dz~OO;^DzD6az<$|UEpf?2b>et z1Q0EWq`06JZGiy!*|rvi|5j>y42cEPXZ@o%54a1Tf>Mi2S9LOI{UeZrL>)?F!HM+fcX?%JUU2<8XfRU!8cYxLHB7}}uL>Y)!JF?u0P{dN))dOir8tnw-<4n77)%#> zXLRgYf=;1En;2VkY~~%LBo5J7qRGoG;G30J4Ll{zlM>6gq%>cj|}O z-(~$L9|Qq{-PAk=IN`K20fco{KJ8^>)#5;_2$b*R#EP5rix2;2^8bG|`D%l~m8^); zm*|1rd`I60zd61l@dp*&$$^ncbe%+8kx&0n$tii@cKWYcq4q^A&JJR6hrY_}D`=K1 z;o@0mk?A7zaNt9Ru$gba{0ZEZYORiUsbY~-USRyFL7CAB}xkPyAkl>{%834S>$_#h&@VuPFZowmm~x5ds-Yf;kbyvdtp& z$m+vyw2T|rStd+(9s^0v&!)iWt-+O>gAThW5f$x>GV|A8ae4VZq3=QRDNm9lg!2wZ zLFShJ=}P!5=&=qfFy6d4BV2(6c)b0)d=7*<3uW7$0kcrH z?EodAScXG*KW$pz4DhC=Lu}CyUvL($as^EDmdv_zFeEMu;JkTIv=`cU2p8#{XVJ;V z+FXx5Y<z^G<*^d;b{ch^sRI$E@UxhahN8cfrPivXj#L6C z!yz7E1Q&*~yh7Iwg@bu(Dxi*GhC?|2y^i6z3IM+I&SU>Y5YW7504!fZF)pkn_yN`y zpKY^;Kqz@Y0Z@&C^;egu=_G{Nl}TKBvY$lJL80qqh$s%PDX6nBx1ml4@xUG~&Ov}c zqv#97t|K2}w>Osk&0l&$*TfsSXi*LG9w@2R8+&|OisbOWprk3BldiE(XM zK~z}=9kWhR9}pE8=|0ceZn|Re(=|=gZK{51iQ{^D*m+XA_)LmsI4y) z63JCa2k%<(;Uo%F-4H<4kutsTyH@_rSBNctbYM)h7_o44zc({7Zjc0Fsn(@}TME~N zx;`+g=yVGzVrQx0fZ3S`1*Y<`kIUC{{>H85Z20^Kj}LDGE& z7STO#34o}8OrrvPSJ6Hv2T=mggp`U#I_Hh}{xb_iF z@C4d4o!sFF+$_8Z5=Ku;4?hld@dil2b;av`DmbJ)XP|uiE+?D!!M;s00DpzA)O*^q zeiDjzy|lVPTDwBXLxtA!aW=CgtUri*Wq;X~J(Q_Y-H5_vx%*7IIoR%lGnzlzG6_)j zRz)x2(stuC7aiUyz9AePs$`Gs0ocVSS zw{I-auh~m6525{oq4w5RTkUVGw_8xe9H{C;@jrtUK^h zYj^YEGuillk&z+_)mo+MHVDE#$J3x5gy6Kk%C8}iZodPY(5D;i6;@y#{2oB&9=S0z zM+s6@9(84{pKd1w>QEJ3$&vp(6G*rKrk#1{h)c@Flfd`L#i$qv`?MR7#QIBI%U76> zryZ6xKR^cB9*RxAw#C#x*$+OLL5=aGI&-^yB*kNa2kMad%MvpB6-aoE?FCLa*8VMQ z?fH#A${on!ey_%CK|&stZmaRD%>z#a9Qdz!f`2k-fjQ&T;2cs4KEUu|LHlt_nspYN8^3jzH)#19ZK33JmtqO<09oJ|f9+&(+q0FVS#*T$us1fC4~> zIbD1Gmw|ynDDygZy59=;FRN)V3#K;MUmh56W)@0ttt@B#_Zg7_srE){UM$d;1S=va zuTgRSI!zXb(JYA9qs2L-wj6+@h}uB^?^rhF0iNnioAMq=`;T}hE(xriwRYBMUriur zMNmwyyCRQ^WP7?$1a+P_gTR^EKH-0aT}xo>`LiRq{t8fjGst&#)T$o@vVFT*1agc* zMQPyju!KT@t=-9SXh$VGfFzIFJ=$L;0?AR3CD-x~6Tt`5&WJmb;%KP^->C_rRe5CHTLY<&QU!QG)8uh>)d_i_Sm2K5G#%+zr{FaaVc){bj0jkmoNyjf=6 z_?1Fek><@so1aB72(~<6n$Cy`C2sdU8-59zgDg0l|L9qoKt7X>G2)kil~Vy1!ze4W ze<~FC5G(9E`{W)h07l#cqE|Y0_Kz-B7)Wz6o@cw^n3N4< zTHk@5*%KELKwCwLP!5MApo9c~R7jxyv!MYMYWdZPxH2l36-?3&tJ8<@0{*1%7_xs& zs&}i&hpcC|Jx99taZ5sjMIWx&1=wE*64!c%b+$bT<49D2x&bObKH=txCj&1aRAF>t z&zxm|5h}K4tNvo?2zXqj!T(dYAgtjFfDLqFV()M)YKIP$$W%TZhSpUp?-gDkKptOt zj=m#y@iNZFPNXOR8m2z_sr*V%fO`l63~~7w#e<7qvcRt7L}K<5V=~B?AKbOvt5zWz z5};oFbmDj3-`h8EUmyk5=UwtRDkH%%qgr!Y2%nV00H|5YQy!c&j-0p+9hmfgKk|!A zFwaB-=!YRy1|%eiNMWe`m#W#ZS2xB29i2TLedFLEWa~O$dXLG1)+;%6XoZ=7V?OEi90u6=Kj4Rz0CJCJ{1CI&FuNH3&2;Bp=m8QggC z_L3Kq%fZ+(^B{VW;mS~Ao}RG#zZjDTRg;@M`4Yc|KrR8!>hkWj%;5^BY6YM-LRwtV z-r`{pax&-Ho_Q+F~cROhGO@WdzPnXqt0PK+hH3tN*=4bqLf^BaA1o^O5Bb=7(LCOGI z^wrLf?W2kZ=YZ1RQ&ORj1jTtsLFKT4i0=2NW!Or9=Sc_A+lJmvv_#SNrJApQTR6Xe zcUZ796>17G#^d|U7pi{bGAVWED7~4kYgx=SQhd_W`Q6Q4D~SXN{QEnOUnaWJ6(t7Z zrQFX9xbGbEI2Im9)cr}o-1xF z%G$6U5$$p5lMbJzbgsXL?gd;r3!N9Qk`d|l@U=V>{{Xa`m0h<4OW~Z+->tr%kD4d~ zr4c!gh7^kt3lJ%UJuI*QV_KbkP<*8W4&E|ab}-M9L2c<`(>v+LiS}eKn=b}ZyXR2; zboYgw#2FD_*{qtvV4lZC5g@CkSm^sS9~p9Ws*ejauljZv!FaWD!oaOo>7Y22r(gBk zyJF=j45ih}o|h5ya885DN=30C4vyP{dAEo5>!wl2-kq3o9zfy-DFP-+_0I0C1eZaF zb*3?Y>ZIt?`<=khGAcb!Dkrfm+l4uNgrqMeIXHR>MQP8f967$<_`n|Vr`weCr|07M z$d2juqyx_*bRXrvb*W_OW=J81JjkO9n0h|3@>wiODMkz6&^&dcwQM@UaO_oIf!2ri zQjR@K&BwP!Zp)l=ZE>iGK7yoVIk)G5;k}R(&wNC=*Sa@_T6ZObV|)e^1Pt63z?xsu zD(VW%=C3d)!pGp~Z6^)ZaP3DO3o-3`t`?-B=H6m&L_gLr$dV%S*}H@>MU4vPmDm01 z`&Sv&_Qb*k=Nnk(J}$7(J-h{xs0xvY?X+rFH;pXRnw`hrBY_pukAaOmG042ye`}8f zkjbWVLzpKF1A&}jcG~x^!I41qjB+pU9rBm~Pm~cZJ8Dh`&V17%+D|bmZGy>@W^9z>_P*eu&D$BxlN?o8chv4wQx3|sIAjS zkx6`9P@h=?@YjgRTo+OVMkVOvy*9e|B1E$?lo);;5i}{w^8W$$ zz*vsyUMv8{^7>}?VbD_nXC6_RZ@~7*WxzY}4hWL>{~`VVUQf)miA&X zyq2`hVbEbTg>zwuS+B~C!GmtzYgxs8yPkZZ?`6CJ2vAj6Z@n6u#hq`87e^wp!eD_w zZG#ozP+9VvV%ZPwtw%vNgMJ(nqo~dwXO>N3P&s+oFES~fEh%!CuQ?}~(10n84Wmi& zqr2D#996cr1p@1U4j76yNRUZOEl}G}1B9$3DoSU0Ya>F40H`gGVEst|RHTPlY=4g) z^%GtD9Lz53Nd_DBD-DCC#ZUFHz-I^rV_;F)!LY^Wx%%g)u3|P8Ko|>!l`_Ce7G#Fd zRSO-&dgX69Lq1k0J|lpdh1U}F;@`UEryBHkB?l0VMR8EZ?-qDJvDo#^{7cvBGDZ^? zHrMTO6zQ0e+y#v3S}CN%gg}Q~CZWq&A0fjdCH-l5Aj3^UpZSUXUdos6IBxl!ipi^C z!}h$vB|YWhynHTvmlG!6sp|QvayfTn7A1M;oar{}U<@hs6f@)`2R z37SN%D^t{@lur??Y@%*MXbW%VFAAC&a`k;hpdY4|ou>W?TuglP?eSc zOWYBJR_jCfrvA@1rzNyhOVtkD84wpaa%nx$dn`&c9QMd>ic?S74vxNAnKv|{}9P&k$WkF&8@2J?GRdy9y0nm|mV7ywaz63TiEW)#~@9 zo%2F{js-!W5aV`tq3s3{NV!;_%uqsM4Z4nmpWdW0$P*jU4|!M67UC%Z960cFBYrM(}m8v?+`tgCfak1?+C42;v88HJ>w~_>+GvPq&DPHTjO={ zPFqq8xFO~rRQ4A1M%IAkY<@%elZ$So4T%Ws3{THrSdW8Th47yS;VuRa`J6RU>7; z^cKc0y&&Een0@+6#7iU7NdTJnOl?oz7F{1EooV6j4Z37;38HT6!^BMGjHJ zOntW4ve5lw&f->cE4H`bmU0Vl17C;U|JI*B0}NZ9nEhYH%=Nc?HB>szeeZ1AGZgEn ze!P36D)U5~m=H)#&fC5D9e9m^S3$<8U(#Fd8mf#F=HyWbqeaO=Qsia5O^@+!i1NsY zUOneK^Dna(kH;kFu@#h`%UaA{D!1U0S8>!K0RD3ELh#nu@{HenRY?%?lp+dabt#C= zdMC;_-*os1om#;9*xUSdrg3>-QPB_hXFdUI@kzA@wT!qV7;5DNp6p-vVTVTGhv%Fz zKh`!4$#bJ%nxZvS?0nYW1xMbrmr$pJ2B0`CK3xq}z0f9imRATYN@RCEQJV<-3t+iH% zY%?o5>v-i6=?7(~`VZXcM2$v;>H#0@aq71V7QwNAEI}<033XjT@yiIE+AV(t+E(um zm15zL;yL_ui5~&sIUyPSE(503RDEX}ky{VDE+j_{^O3D5j^1$H-dN=*&V*`u7s|E8 z>G_d-w@~-yI`%O?UUkbaod(lq^b_Ih^;ADDU<7U3-hN(tQ8f(QxZmA(Rd3vrE?<3K z%&TQxqi(sro~VY1V?xJ3>179DDZ|2jZKs}4n{fhI`9$*vMfX^X5EA`?S+!T~5z)mR z`6B(BHNYPx0)IF?5BwpPR+0X+$k8DqL@jhc)6g9Y8PWb(X~6VFj9|l8sQ;CA1NcLa zGeDOALuRPu0ojpXrW-d0^@g)X=>#7I*~|J4o$IPP5Bn$D@0X@*q!~+sbJY)JPCzAB zz>5wf`l|sgGNmRcI@N&o@Yr-#YmKbJG~hRCwc^X0y-*kXcLx1Vy@Ag5r&t>pq7f+N zV?ar3c$k5kPH}(_#k1-*Uo4|D6ysUGzMcLixuSeyk^6>Mg32UE&kIqga|!WM%{|cs+szkNRx}SQ*~*Sv1|VD~AhZ(97WQDk9Rzkb%@(yc}1y+A6%EJf{^s!=*?kWSMvG`(h7E zG_Mep!uF9IbDOIFJQ3wHGQAscrE9a?J6=KP^9ffSmq`J+?&+2Hl=uh%<-0~HUk`OLkaR8MkO*x4by?Q{~vO*uBo?t>&1hlnRJv?XG(dB zTuMy>HIA+)vJ+Q)N5v)zQktJpIgc4$YODn8rV-m&GgzMMV@-;+dZF&ZTu?>KUo_(j%UetZka5CLthZB7nmO79Kc|fqa5+;CO9w^NF|!=@w?B`t#O{hc;LV zSS1YgMiw*#@U!NRrG`>IC@z7toq{n@`G=;EI2D!25w@@4w&KAmVTb;V#L+8Y5-k3JjM6h!@LeJi_ zYgrMs;dzJG#gm{r-eW^1#%p!J7d~7=>bBnJN?Tb(YoVQwUNB8jK`n4jR(HMt3LjM7 zM~+eshjkC`j{AkU8|9(WI4~P!Bi5AkoSts(e8c0Xiq}DwN!m2CIk)vq!H zKCDDkxS@2ke{IIkTJE+{YS_cOeyw| z;j9MFOaZ#VrcWgzOkQlJ3vJLnX?(@JQSKXWbS4#fpO<2nEu!$poVtFhVO_USk5am4 z?8e!6!Bi`IyFriq>A6@vj1OtYA#by_M*4D@4HDij^5V{NV(K{4=u);fj==6-cgFHRH-?>Ik zFN_3ij!( zTZ94yD$Y}G3!7RXXC?YAH?*Q(DDxy&hiU0VVRF1bS%-F3`TGx{83h9@fg+Rs;T(CL zy!z$XtnWAN$_obETZdOFD@9g!4kJ+>8!M#^dXs#TY31 z(m8hJEaVPtj|?$XmN@mD9N3zlKD$}h`S!t6cvg8!T?IZV`@IMRbkTv-quKI+#{ zGE;hcXT5t(_PV6dwr64I!x6)@>w0aoa5Sux{^TaTmkx5Z!8pK(Y`zXAU_6uEo-lz% zD9RM?9Y;^RElbvOr%jG^lSVFm8D}Es&^0aPx5x6la{^I%HqZS6MzgNH-pU#r=ypXx zcblQhv+~K8|CQegi^cjON`pKCQKGtD1>AbMU+>vfG-zciqt_F6dmZg&kbUz#?A2u? z%>K-}uF2=sJFUiV`mn-Et{v}p3UVx-`?N%Fvu49w(1V~61U(%sZ>I5h9?{L^(Cp|K z*D;r@g)yy&Tx(Rt;OF@{A-V1iEW-(eC&o}lOO6>I!JcYgI@{WadUe0C_+tO|pkIhg z2Q|0exooYKAAUtV7{RSpYkkB!MBr%2LVLP#W2ftgp+T$)m6^rj-tl(q>9Yc6_Azdg z9t%w)h=}i>W-jEElMq{+fN~;JM0=>_5oEuT^H+12qI1TCwDMqHVy_jrCIa8TadCAL zG}ONiK^M{cpsJ>n@PA3cdXWsCizpC?4fkvkN3I^RcI#EfrI?Tr&>y!bEpt_*Y*(({HP`Q6$xQJR~_%`Q=zzbY(D1-QSHy_udx8yYfQOSV7iE-FrEGhx2NcDr@cDXol(ijN-+4<{ z1@+7glYT#&OwY}~f&LPjOQc)X-gmbO_$48$56mF;U4_b51&x1I-LZ1b6Ek@A1;aLw z;1*dW8uA~aLfe^Oz|!Ep4l0=hnR(5m{qu5EbkB8l}zQU6q$HtH9CWz$r6I7I4tVX zAYwgnlDX0IkU*hC6m$m7XHD)l7x30K%Cx7q?^V?~O5@$xHr`Ec-P6vNZzk5<*ok$8 z(g2}%Q){tqpMqh6#Tun1liptSR4bEx+v|zG#JZ^Mo#f8RCf4D;A-yx{oIf5>R19re zoVc$jV3Sq9cTd#5r0hNn3EYK~v_B0`694*CT|HgZOo`R<#9fQDLIQBU$Z9IddkLd? z(ufao*glZZ@3c38nD8$e6*;k3(0yPV^~?0fikg9fNw|i}^6Y)Zg=h=wxSTF6Qe-Bw zJIZFD&nbL%>{ocndieb%zF~#&eoFR!rCXF zkvuhBtF@JlbXbLuHHtb^LD1zpZ&i%9o*mDYrlWV5er2Q)OR<_3nb`KS!8gx%w~ZR- zu~g>a_6WU~rb)4)lv?_!>iUyE^tS_e{l?>6t{z74q*z4#$`|TsOF=H?Jh9_892TMb zGD6S2^l=assp8iyrm?r?ac|w?*7X~{BF>O1HiH~BKVGyq435SQxG`rIPc5!(Ov4P8 zCz_j+o#NlxjA`gk&pP>fZg21n-2Arwjl7qZyw^=_3-j&eP1X0(cZ3(-FBCiQy(FSE zTTd;OLCNhnSh+7YV}Q$g^i3ow(y^%1D!EVpDam2V`Gqf=^Oa5J2w#}kF7huuTg$&B zDg9OsTX#2&x2x&;y3HdqhSsK7IbJypxTN#*3f%6nEjuqAp37eRH9_4ui%GV-dr3|d z8zgM+CA?j3u&`kc;}bS?+##ZRwN9nYf7rT6a?41cy5dw5r9zzhTV6!fDinT(sGh51`kdH?k>oyssB4Wvg#k5cfty8 zL^*GKAot~yp-*xe^?YVfdvl^aT{$7IY}$r-mpxEqD`9P+ib3}9caixmn<|2!h+H0# z!ri#tat7XkPp0?3Ep$=oo-DRR8EUvm6Ci-owV27-JibtAjtH-BM*iisdnjxEhED~9 zQy;7AirMa~5E*`_stmqYRB>)e>&g{NAHsFc=l9s4J3V|dd@Tcr=}3_M4pkX8gYixQ zh|-O08+E^V0%C=;(#Hjuc>DBwC=tv7vqZdFbe&(lTNU^(?>J5cXR|VI4QOPocX(Yf zXAm({X5$`T<{aLfm&;^&IO0Zc+BKiJ)EIb?ydUx2%htA4hrH@3DNNHs?D2fx@IZu~ zwy?FZ^HxQHL0W3^t3Joh>sBZi69YS+{s?umuFqrP#c~_@V@_4i6w4!%kG;~eE-HQR z)f%c{6n(J)wLJCWfkUff)en3{lu|WStee*P+ahz(@11%rx;j%a=|XyE$dEg;A;iOr z(Q#MiuuyIN4m)rqDcUteKN>WhLDN!qusM0a7ldZ6%E0XpLq;{NI*I}&kJ(V27F`2K z+PUDWvgwL~(lh8jzB%GsR{W+)%TpTPf`(>aG)lS^0LP|S4P9PP1{ZsIVhcdC16Be~ zg@U1#-Er5qV8=-Pg52Wa40N%frX2ds7O*O!v*1Pp+;V=dBdLjX@uW51! z>}n#nfC)RswOgzq$Zfvpq7}8PWoSQ&{xTeWX|21(ypgWpm6jDUbF}4qP6NG~u9p_u zedk5{-s=sizHe}F6_Az!wW%6FiYo~#^xisu0w@uBdWAj%nV*_ znh}mYJx7t36$3>!nh1t{n1^>p;v=NyMLnW;`OXzkcAi_EndY2b=6bVY zoqk>eeSULRqr-4^<#3e-m9}oj&gjTD{ZI#0c5_7<&Enm_9t#>LDtuJE9#&*S`_iG7 zN;S95Zi|xkkp=19p3V}9`J4t(<6fbeu8`7~z0HW(H@O5WXRtBvK! zGt#H&xkSf5wm$kSa)0mAg*kpf|B|-0d;PbHItuwtF%H3p({CR}b#DfNzJ`7z-Fv7M zhJRp%G6U$>SI7TdW{?w!?SF`|lbJv0$j3)eDDdBf za?Co%!X$o% zFNX%2&%|wbG^LrBORAAs+Vpm285o2aQU*E=c;-}0(^*=uVqAx_e5$&PV_KS1WjE+} zMsr^z*M@UbDrk~grAPz4%Li{DtD>B<@Hj!cd{k$haB z=zYe*=zFe)!20~&Oi%)NK(Xxc(5p`a9-Edjo7}AmR6(M8Q~mv=`F$4K&Y04c2mQM-@D+Gu!z6=c`eTQLFZ=#>07(8 z)Zwj>0)I~rH#JXA>i(?D_y}goI81jPg)Z0D+c)y;7)_}}s6E&RE}lmP>iw&DU+mfM zZG>Yvi^ZqzYyDJ*5YgKVp z>&WwdkMWGXgkvSr-IrzL3$w02+JrLRy3|1{Z{7UZeI%A#YVl2P z5`vviQ;6PWVkb$yVg$rl8sj)8n+0J3=ebE{gJ(QyY^HJw`Mr)l{5x%^h}?n##Jl5N zVjNrJ8cYV=LK;rrhGB}Hb(75EmFj&(0Kw<{Bj821cEh3ch*lU9ziE-93!Dpl0Z-@yHX0iF+j+uPttYBP#Pd0JX_lslOac+oTIL(hpwJvPvnmnc@^+Ivd{sxE$)dVFx7#Cv#^XJT z-dmq~d*^8=gBQ23!(d+CgJD97WByzXh1XO!`vkoax93mRglf*OH_k33#}=3`uug_1 zyZ5`!e|k|WPgO89uj*?Y@0tj#%Le7SJ<{~<#p%@oS>#p=>B4*+huKn1Vg`{ewre}c z<{OV%c)izDNH1zvPe#ku7IU(yg2N41QGb-2F#)3Loa2M7=@>4# zxz{l>UezbF#zl!KdqSD99M`6E*$+F1Dul|5N(!&kx-m7>v|5HH_wsseFSVHKuiV^@ zD(&u3J9W-;G3%i>Gym?X&ABWC_ti;ec^^K)Pq4y`q>y}X(s&dJ^MNAYFeN$b_l3!^(`@` zcc!y#@Y0{?Yoy}iuJgZNnMG`I&SPWwYQDhUEvh++wY8kxiZ2qGEbAlY7LD|N(i3op0TE%ZTAPGi|CFckRHZ(Vr>+;shqV}vUl z3%`1zcFeRHuN5}6+`q3yYT?L>*%%<6_sihrAdA0@Rq&^nu0$II37gFnt+b zr&Ld|xouHWQ?!5~z*07bKD(r}HABl#`Q#AAo806sG_Dg^vj*+~RA|M!tF?5r$e3eg zHs(In#o4T7F`w}XU=fBpy~{gXVb>_EVqq@KKR1f1Q1$A}T4-|K=G%5a&{s4 zbwwN~suVtExf?D?MjqCkr(1PB=RWnhZKaH4qk_c8g7UuN-KZjtqkDg#1a5WGc_y-8 zICp98%9dX@qJV)v!CKq3){%3&jl!wUwP+&=+(l;BWLN?hB=&9t!?!>iJ}Euy&y4AR z$&wuVkWONYn&(`*>nljW+0QF*9HP7^DH)o7yqgpmNG~E79%CK8HkEEOuod;4@A2UC zh4Q-jTZ_5vqYk|%iL1mK1>X&QUhoRR8yrhF@@zCv-jo__R$Gk{hO;sgD+wNsL0)>E zK(^udh#;|zfliR$)NRCht`cPpU5<{(Va=}e`H4=rQ=B@$9`Q4T+IWS z&=KXJG9opRvrfHYbIfCuG3G`5*thIQgZ$_3iulcp%0jorM)M0sy4?m?=hj|O5KAI- z*AbF75(50-g6)mhY_GvjouKnsHLHJB9?eF%h;1$wciN)-MvB3qtR@iiM;*Iof^R>R zl$qarJaNd&w+=>wfktr~v7du9>qnjl~^W=aPx7oRR8h zSk_3GfWTOJt3foKBZs-Q#cljZnQJ-g=GCUVFF~qO+UY(?G+yzLcuogYBmB!L=d)Vp zysU)TUA)F>stg_vna0=au@mVcN{5Hn>f)Wu`*~Eg>}KEE#jV$NhrVzlJ>RRQ=b1Iy z4P{l0rJ~69?DE%gA3dODF1A8R_S(p~99j9wYW*@yZF40xGdcq$B`i8K#B21*WP`A$ zBkqV>TX3s>FyJ~ zbCV-6@AwevrB^~hE&IF-v^66%&FTmI4K<`>qV z)m5VBv@%Zgkm;r^a2L#j`;zQB1svMozT|oz^h+Q^(3m#7WXb#I6^J-+1>%1SwIk4p z(8f|0y3(MrL@UN(<;-|cY`%7?^IU=lJP`b008$Rn70ZLVVl;8GQNppg>*<^)qH~kU zke45#FD-X(wR1n|+S0W31aY}?$8GkWqiKQIJOlC0%7p$ViS#Z{gTPYmu*=97Cbfan zteHs75z`vCsIuu-RrK`6@iwwGYYlP8^@W1wVyF3G4EL9&;RMIyAjvp6ifvJO|Cpm=LuA(URei(1HLB zqqoQn%4mJbyc5nPJLO@CpzqD%m@&Nu?xF-D5*9$2I-v=QM*BEWH2$ym_rRh0MJabs z@_Wj;uYGY_nB^NN2N9zDsW$?t>kyEajm#)azXeVsWREs~z5b$q+x?ipxL5^i>zPGK zX8MiI3AB^;wNK8?H`4JzJ0Orwh&NorByepxG)Qi2vOQhx7-;eFZzXut#vPB^%2`ZR z8}K8!e75u2Wy`@8@@VU`*a5E0(g42FA92$9pe+Ne$PG0V#cB}Wx2xV!x~))Lb+Rgm zG-1ayLabaKt*6s5-{Iy{;ZDj1_|6SFWOJq8RBd|d`EvOcqbe_nZ`5w;osRPqn48~h zC6o|yMKp?TUkcZJkiW(x6T;_GFMt(Zi1&%}m8LHe%kMRry{vX&wR00@vw&&{oJVbD zzTa*!w+cLSID;noV~&~d`r4-ubGz?6PQ5FbRwl~;X46%bnzCtW2G0|99M{!GuKNMA zko0XM1{aFxufGq5(e%BAP0>ZfA?3eo!`<&jfEMN2)Mlr9JSn(6jY|3wiufuz_9Va{ zP$Nl!%m5e>@S3`VNxQ-n5jwlK7b;1=3+Ti+^-?zHNC)X_^&baU)>X5n)Db^_7T5&+ z{P9Y2mJ|4?wZNpx)oW&>)u7c^HL~;-9lpK%jl8*7G5)Q^VvWJVhj7a^zp|G825|cd zy3i>#jV}G!N5I{ssEXW%dW@j7v~o^UpVuuh7?!#G3M6^;pO3QyhJ0$+E#0{qg3%B9 zI-qsz9RJ-rf?bbTjGFT3i9#NuUDMgAWKN{4o9{{lz7)Xz_v=>I{Q(zdWd>fsY1t^Z;y#bpdl3OOzv^s5Q{OWcQD}6=x^64=x9-Z>=%o2? zqYSP`k6rIAx62aCyV!BjaST{X!?+9ISrwGcfUPncPur}D!qJoH#@J+`sylX z_Kj8Q8`EgSyq41<=;Fa-IFReMOT%t4;4Pd{hBJF-NlD-=81BtgdBycuiu5GpCi9k; z4I{-2DUx3eU?@?5!`B=?Snf6o=2AB3yXz64$@cA+>I3PWLwU8)woVU3MIQ zEnLIBgLf}e4Y`?DC8=LCUEFH?CnFVE`&1cthjdG~@vMPNR=lqXD!0w62G7*rU`mhH zQst2=E=|1)19iQfLrZ=wPR*20HFfm#g|gP%S3N_t~*Jre!v08_2zw>s&+;I zG9SR2YTdSS1otOx9MTp*#>nD$PX^8NZ!in?Jj3Vg0}zfJmyKKH$R%}e zJnPC@?V#QJHj%Q4+-Hv6f`+rn`LM8M-BBTf(#~PDaXd%OtX!XYfZ-6aJgVMo36gQ8h_Qmb-(8~IuVe560xirz3sg< zK;-=P>NJ+zd)4PxzbAtV;=V}g-ul~~zQksA;dus*7_qod9^ReY&fYemma0>3C&CQ~o zyGu@k$r-WFp9rZ<;CZ`xe^s>|b2Tl=ZkJ|%zZX!^L-)da0xDnpmw+Eo-SCTQ3k8pQ za|y@iuHTdfVz-zonHT*@K!oUfn*+-VnwwOJ-s%ND2st;{hFalB6)L4OSt=JcUQ@?elOqEfX4a*3KG3epGjR zq;|hF^cv8?k zQ5(z(2zNb|Cj9|v>VIc`SJjaxkglG(?j$E!MMi#-Ns&6P?fVo{;iL^3JKRovd#UJ6 zFR!|Fvs>$Zv%W~_`*){On>x?s2xrx}-8?!b%ZVat|6tG{COW@7z9Z9liez(_{#75; zS^M%S9$BhK=)0~f;hO?et#jKz*&K(inWw`m>J0Y1OO-5aP>3fvC{cJD#jO?gzb8f!pO80Qa*0k8>r~6`F~n z!b>sQSY+?*+%@l*HA`h~E#C_e4DNq@&}8x?m6;-FIm9sH{T2fO%5>X(i5VA~wllk( zmET})Jj3aJn?FA>klMHMO55WZT!`X%CQQ)EdtOKX^PePgS%}Zto_RI*X^r63=&O#S z(Y# zBt&`3#bm?gZDA<2w#)9rb|tr$3fK5Fu;Y+XqQq^14l2t~SAD-)=KUH9$4tP&lCRkL zd?}4*uh{aub(N1PpFvxF9~13?r%c;+`mR6pm$6veSq#oAkm@Mx3jO5Ue-B&$;qwAk z3E<%6=9p8}y8u0nI)2P4a7lGvd)V%o9*+cg%Wtz91E4nE_upTjfHzwy)bt_XueBCo zpwhBN=QQud1}Mt9g<`SofoR_T7DgE*lqu5|K%4jpI$RykQP~V)Z=s?-f3A_~S4z}I zdJnT~LRoo5W&~6aXM$qHwq1^`k&b`2%Nb!#53RO69u2EHmd`Q54bFoIR6)L4F0Gfk z3tB8%t~$V0{|xS1BY^HPo;1-2Jf5BGs=II32r-h}1GgIe9Kp;0br0Vil;+t-d9s$3 zKViDmVPUa3wR8BlJHtJxAkKgrlnaux6M|*UoVMvRgLOQOLA&) z-S0?D{;_F~kvMC=lZJm?&3X@BNi4<20#xnI^So`reA{X0k*vM{Wm_jP{=JrP7XBWr z45Hp*0;K)^XqbKpuX?>Rs9*c(^^s(iUxN0_S=)iMT5s8{L`?6L(GUL ziA*DmRPNhF%H{wUX8#Ak#^%dX+}MxKHCWFr4<2uSF5mny-X$}o3EG-txcmL^Bu?lv zKefbhoCF9Z{b|?0+9_ZD`%n-A=Bb}!$KhX z@iu}ulAFDL&8tw^&6I4S`sal7FkRgQkQ{3Z1*<>~BB+(oysU80=vjWE4-&p~>cI0P zee+QBzDa23V13x^!Le>{&s8{_4*)BrYZh&}>9a6m9OF9vYxicbeJ4<86zg356^6B} z;ohSx65p*Kyy{$^mN%qfmR}3>^>d(B!>aZB7T5BDM$5%6D>s&R^UmuQAirc1zqV{+ z_c}lwewu>N)^R-yP_>wlKUpJrXk~uju0Ny(2Ggp`T4+2R|Im)v@~BR#%}Y=qxdj<7 z&E!4y#fwYT@vTO$z}@=lf}%&J%j>6Y0E!A-LQM3{yPpB_fJr;;X#1qGMjw!MXs@zp zIbUI{nr2Du0~`}YOAEIN`Hh?Rkj!_u@x@w=i+ccQh5|rSnak!-_a5p@vLQ{P0%evZ|IpE!h8N>oW zWXb&xp8i3d3NzNrv7QKi$1k9)egpJuL~99%PTsO#hTd zEfmm;b&pKdB>sN?pY^xL8&M!15;+~UL`9bA*wyz0dfznm!G-0z9nr8mYIH$UX{Am-;9YB9$QJ+nzRj`Y{AqN#4X3pEQC3Y< z_mh^@hQJH96pIYk?J=lK+$gLPmxD+R8x+90)d++0IE{#cl{rYx>$Sih{siGUBvmU2 zjJ`KC;0p3U2kAHmz3Z$!miJu2Yv8P{>K?9e^c*12@KF7524Y zS4EwauK70MuXTp~9^kh(_1btYx<<8b19rU(yT-iJc&y(1Nt3%V3g`s?F5xs~CzNIB z>iNE$L1(5yCe3Rn;ZlfJFW*!G6#%$aC+o{NHtf?QHGSYREejxZ4;dVg6y*P-F=@Gn zo!^HmX3o#R+tcm#KvxkeI9*XTEw|>=biLcuM0MRAKa}i8)dK@%zAVCPDt5PKSx*v4fT=*5_`M8J~ zcP-(NX_(bv0i#1k-kssb_bFp_^(HF)0 z`lZ$Vj*2VCD=^SI+Uzlw2i^F0=`x<(5yFM|CVc~rj>)rAtg72)GLDSH1&~_{`>wP~ z^hvw4oCHGZg z8g|=jscSPZ)zzQi{LJ0D>SNZ;2}uB+eKR1C6)b*(ek<-|)m&PrF&&bUo%VFt6O zuQc2$6Ebis`cm=9XWtHs$HzJM4JRNZ!SF_-&@aRLvOu(kO5cjAAU%6w6UPZ!=iVBU zptrOuR~X}BWm)t{1#YDtk=9-4{@jD%r-4X&cDw(w-O-MpzB&qg1wi^leYpm`@5p36 zrUCAbe#Qev@Zw$)^{%AWGPg{iR`b=+IADReUd^4}-wor3O9Vwe=><#8YZ0+%xeU%V zS)66@0TWVV{r388p&9bqaCH6CE9V;Ca=jJs;_htu-#Z$dOK0O^HdFkhtc^#fhi;iB zWVV)mDZ0Gga<3T4!TO+63G!GM{P+3(wZu50`3EWE)h)QtC|6YZ@k7 z8R1e%u#vU-<7kqITQ_a6W65@Deg*LpDW_<46;zk!egpAi#q?+Ebe%B}`*F}Jnv?n! z)?SMlptdna&?qGQt#up5&$VP&9rL8s;mT~Pv(a0^{~k*w!_SGBp&VZ`{h7+Xs}zHX85LxP^k~CK@Db zKlFyk&F^AV!|ytIqS3{mp2d+24h{0c{Hr%(gkZrqKaY(FRhY*GY@Sd+v^j3X+xUx#*9(dM`&x@k0Q{vQ<22}V9q?`Elb{B zKI!M?we8>}xk6F={q$?~pUjJW*$*yzuSPhQ5!3Q^D8sep7^yD=6On(GaLw$hu^5f% zQ>hhzQHK>$B5i!3>NvOkg{sXBU)j5-)BZ$r8TR_HC(s2)e;!6=>lH0@Jraf$&C@TL zA%tE!H>1?%a2zPQHDmOgSh;_|E{CU-dIrCVZWWHZ5=9n;lWjF7&m}yfT5EWsEdJ{o zLk*Xr26a4@=)QEO;hu&R6H!Y}8?kR?E)QaQqj=SJk(jqLOlsug#GVSRhN5f=cb-(j zHd~S}IF-0VZGv@R^h{*>)8q3Y@$O(+=vs>xsgjBIw8;<=exH)@(jn9h+on=T#(tN& zmmF0mPDh$v-O5__u>X7r+}(7}O&b9=Ldjk>%BSfj&#Y6^^VQ`)weC;GKwuzN&*7^; z0Low@3@T{>2nGelJcb`(xQV!1RNZ~IKr4<^L)yi0(`KpiNpIT*@--EKB#RrWp~+_$ zr%dgEXm)PXZXfvzvlVqZn~R9G?%#EEN)zGRzy{3$f=@tA5O9ZoU_z4yA@F?;`y5Z) zfeelI{@-rxAAzah0r}j6--Qx>t^RGi8i>}j)j}YcNk+R|l>&2aW_()GbcZiGy6g;l zes@rYvmr=e^A+#>anoPrX*~SBRGi<|@P-menQQd(2ILN|XP=r{rpcq^`V%5xoFXhw zr|J~5Y#DMbGscjAA#dd2VTG=L2kP^dz(SY2rChV=XH%`!7{r)leuUD;P+Q7vk^D?w zD6z6msqv8iZk(Mi>9DRMzrPD3MB$G%6{MP<&#tnwIpQ z!YG2ty3)kMNx!FSm4gZ2YEv}vbe2|7-E^cP29(A(>F~Q{-Z$Aa(8Z_T1_(4xYc5!P zE`P<5sag2M{?7yx4-SQgi?RJ^bRG~xam<{(n6`52$7D`0bg^oEM7NbgRZ|nJExbHl zJH#pe?jye_sjIkn#n1eAB9mt`;sTj{33%J!p58i>ti*QXR2ai$$7Wsn@xF+QdvtV^ zQ}H75ygWa2-!|v=P-OYDZ_avi6~n;YPHrI-?sPK!&PWX1mm%(V?Qn(oi=sWLFbKAI zIFt|9Bt2a)E{fw^9TGDJe0uO`H_VK$g6{ftl~XHI80NfT*8AWKEDl|o95MsmQJ&PX zGAg3eDKl-*I-J(09C9A|O)N<>V_Y;3Z93Gry`axWRu<2uypBoVAinaQ+|?E~a!!*) zX6u2wQQF9%2eM)!iN&)xJyUGrKT*UgZdyIb$X>?%b%H9+T76PyGl9H0ozpHEYpISi zI=StJsgN|~j$yMu!r*XyEnm4^$cU#ffxN@da}EKDW13g7{=xC_F=~8=NdLYa!fBoZ zjV9^B|3@1RJ=8a3Uv(JK3T0Z&`*THcS@klahK_+>Y8Bti>hNmpDYt|%R%*Q`(I1-E zk<6EdoeKFns1>$ zML}8(H;82`C(X!pZmK-67}uZST!zCM;66%&oG@cr@FG8m{3PDMhXA?)KQit7$Zvj? z^G^{U%Wbx%30lBk?tq^POOn~TlOg1r^ zSLJ-Jxga7C%*UCpm>4~tKhWV1q44SwjAJvimpegv5DsjOz?r8DyJ>?nOcxA^4fUsV z0m(REx(?yx;pINL%zv3|TV%AqP5+L4cNRAW^iifU&_}n2Yd#nw8DkLTp7R+qZEzY` zaY~zAjf{%*qSnd@4MEM$$2`W``z)iT$9;7WDDd$9^KKC-3Kbu_d)P#IV*4Yr%2MYT z(8u}jv3NZ*vUAcFg75;VPvG6-2qp1*PQMs^`%Yf9?1B$i;pL(O) z0};3P-Z7E#BJ)6&TJ=7PDAx;29unj6LJ5P_dW%m8+yJ4#C;O4UI%*}Ute1#pM1}QJ zyu*v_ApNz{M0)sDe1Qm=L#lHiYLfG*%O+aYZmVP|o~YpuGFa!{N^P)eCp$u;9s)XPDN{f}qp8ITt2>B8PlEZqND=n{yhqv?5T9Emy z4b5FPJm_e(^U7kjsDAj%>1tj2qvJ83jK9f8BY`gi8k7o!=;-I+1FIB4XHCjdM-w;H ziAEVkoTX+X|kxydX3WAxieiP z_}DgGl0f?-lgBHLdONGP3mu2jAi>f;<$I-TH}=s|c_ZK29XbL8rNc;^TA+6EMY=kd zjk4SQM#c&nj{qbhjs){DbIf!&7`+Zx;VXA@@S3n{=3UXv5!Fiz9n( zf#cyuz|HAW#@uhsp=U2~s@g@92~oQ;)b*Ept^}@(4R=KIhFxlv`MOfcpD2eDudV*; zh!#q!)Wf2ipbJ#_>>Sb}>F$C!LOB%RZz`SoEkzjqlsf7omTFC4)2U8&_rPu%0+;)v ze+dj##c%NP`?3#Tg-`TQaA%5@%fE0poJlAPzTHPsD*h%JRWEw=?7&WCysNW#!q{A_ zBStEz3v{tRI0#WJcC%7SSoPxZeC7*qFe2F?!a5~*Zt0_45*K?uf`mBb_7GYPq)B`b zI#lQ>I0PSktN-hKjI!ypmSVu<6hdFx?lrrjSgp&^Y;Z3A;P>P#mriArehNoLw+f+z z>O$4F)o-%(MK1vAnRm6}5gwZ#A+>HUA8N<{r*JL&fNE+4H!v8bLCo$cYe4N3wPYw3 zwT$6v#!itryg1gdp8BF{xB6V3%|ns5yn2ntts?`+0a7LMhQEsYcoeF(=Xn>TR%*db z(gr%6_Rtg3>&Y`~%)$rJ~7TWl}I!-R#>aJp>qV|}8Vw1#_IYxN|i8zYlU z^G6{UBXgV4Q?~zgKq((eJ|LYz^auh|={SY0*AyD)3@aVn^H)J2#K=iIH~&Z?h1`G! zd68jMw&ape$Nf}Q-7^mMTLEXWV{^L)fkU~Ce2{W1#x7E;K}arFXdYoc)%%E3#Z(J&UV8=|BR0qbC{mN>uG(%yE+gbeZ=}Mqln{}=+6CdHCDT03rP4Tt3yz3=q4ttmiDs-d7r+gL>$Q&|6Fzo1nxEmRK z-XE<^wYkT>Ii*U&vF(+%6h`Thw!V*qseGP@J8GPPXMrJ%dyM7So zT^}8R#UlGG)ird+>c}*U8NAS*Yu8n}&O6Iq8aa7w)b^YMISX+%`DyC$rNb*1`De?+ zj_S0Gw^^->ugdca~n*jJaBl`+#Zjfa3=cMexDZlt|pQr-4oeTZK`<2W?wG1 z<347&y64!e`V3K3UdaR^(%w)!6%y5yS>|Ez=-WKoO4J_VaE8!typkzgr1@fr{7MMN zB?PYusR-mN;B8kYUn&DPy0|yjhxf-1WqgX@^n^x4V)I3f#76PJLS$Qd^#*=~X_Eb*e5B+eu?Y%N- zGS4L+o2Z(03IJ2}o&0>3yKgNUu381YunMuV30b=jfX3H-t>$+^gnN3NQXm^?$dspm z^~T5-8~u#vKdZhjfJn%N6eaoQjEXW$p#99DsSVbmY_+gn;>vhNHrzZMC< z6IbV}A?A^2uwAb4lu)ZT3M(?(gP^qX2fa?kCFZ^BC)ct#`zh*9#$3eB87sAbpZ|*b zdBk7EDF|j|aM*=Xe8zNyOIiO8k=AT6$MrW2HoG3SQn8#$BGji5NM@>o zDSrDFawJOWOs;}qM)83(W~j2ImSqYJNrH(|>d|xtHJ75!+L&K3z7vbbpB;N*cf zsGMX=9s11YtV9KbWu}yvml(&Xin_JiK-$Wlvr1?vF(eXSDI#=?jMT*ae*BKH^grWx z_UhEm;)Hkb7`cguthT*^w2TT^eS*)~v*Mr%3)7QnRMvGn_e;5QW zIc`{FuA+6s@Vl{p6~+dJ&%g0_Vlv~tW8Qc|)EG0Jg_ytFQ2MbDrh@acPOi2A>)?wD zO>}ZeqQS84aKQ67-Rteu(_gkfkjOu+8eN%4qP^*$ZRC%{k6m`#aA@329J#_PILH9<7kf zXTbn1+<;d70enc?NrJiMuR=gkl1MKt3u&;pl5ZgdMoB#`o|3FS*2a+|H9S_K^X0%FSAe+#N+N!{rHYH@`1eNyB>qoj=zcm$T68sFq%$G(wB&7*!Jf~ zC@fXu>qf$OttvezqtR%M&`?21nOKGJxvR2Yc5z2LYlKrDz7Xq6_LeXouB|RZ#4DFV z3at{6iYs)dpu<3E{>Da(yZ*~NQ`^uysBlVjLIHJh${=ple4HM$1Q)I%4XS&a0cYGq z_vi5tu7tlz*~YY~sfM2xM<4kb!=U!5778!#yX^G7#6Now&gsbOQmd3>q`?dBZj)m9o!bCh}}8gnoXrtSp&1zgauu$ zis6&TXnad!^hEJ79Id|9a+iOi<5pA4LDBQEv=oxUmE?APh|CVfuj%{{Hvm)N^;B8= zoFh)k2v~oK^-}42O6vm7eaiGh1RygFbc+;f5@H0e6(fCF*1asWyv-J=nVrrfy7 zSudZf9e6pO8@iP?Y>1B1W-Yt6Oi=tKH@5LYI1>N6sop754$!kxgTmqPp`m!h8WTC$zQfpge!bZ0JDxN}npbto!hi*dUyR_i|i zAp-p_CH_#ko2>|RG{oJ=hA6h78-iSgcA7A4JV4Um9x1Rto`IpoKOE2O0}uoGijU^@ z7B5G;Rs`yTHL~vOwbt>|cWOD3t|=TQAzWR(SH>_w?8`%TE)eH9A9^(NXUo(R5dW^` zOU88Hd-|`7sb9@B`+ij#!YpK3DQi+blTZJ_nPaCX%7#?ai#ss4w@wjSX60JjS~q#rsghhb_nANV zo_Uem(v4-yl7qx@>wSMw%dL-l_yqvT;sYVQp+i?GCf}Zx?R$HE#CaF<5Gwrvjv!u8 zLdu}uTL8eP1#l&W$EDd}MJ{@c)%}WzY#4ua%eJm8I`w=>JptB;_>y_D-GUfSF~)=S zk%*gyXg{-+;7*DH#@w5ze7OCo^>lSkDd%{OP)y#hR-CrJSwy+aE-~_4V$SyDll?di zU?wV+R!b$^D#%l9zcIFfoyR-sp?i~9_t3hq*=MTZI356=e z7!LMcSZV_6;DG9ZSt_4P7m@Z&D%|_^S&XaW@@N;1T?cDGBkNlP3Umm-uM%Tb=yfF1 z)mfvrhd#_`WnY%mj!_9IU3R1x<~FWgT?j;1N&HOT{bwb%H<4HB`bKUyba!o%JdqNHtTH|tek$I5QOcv$+MOaQe=aX zz9yeXt&4iIXRMCt=K5sx??g3kn?W1O{v_Q8@_v#(J3vLHk*c@&)0_WIJZU#dUWWEHx zMC6mu2ZC7yNc?P8f^bZczu~1qy?Kg6h5M(6gQXRY+pkcM563!>U!R|Dj=g_;d3%ko zpmttWL1348EZW-tSv@kI`G$I&6G39vC0_$8rsxFfVSd+AnV&&c|4moxf70svQ3P>D zYn3`{0u~g}C%yXg{VFcx@Vjxr7lsIL%pXBW=>QDezMXOqPCXYv+@8tm810c7JI&2QkF!2aq&0`I>S0H&=rG4VUw~S z&v?gtQY#NlCMJDF?D_(u9AsRs3m2eLPPDUJYiF<rFr>WsiI-(V=PJ22Zl6(sJKZFRf4@bc$Z&Q%x^m!P1*JizR$yyTVGor>| zo_J_dYYY+4Y4p<%3~y8MpQ^wEBW9fpgdr6%%9Yv~ZV0}oWSJtCD&&VZd@i6N4p77Y z26=lbTvI3&7g|(qifT}*_9vSPFWR%&MK&t3%>U*^XUoLJSCGfzNufU~XVdTTV2JqR zli&0n-Ry2QLQ!C~q`U2>^{?zx57)g^^!c+z(?7Eiab(WpLR$~$b=qV%^-1$B-QG^| z@+D9dBB{X%cj;8C_>=SC?%tXiN7LDb5+nV2A;zoE$4XYh`d3OFmW{i3j){!%NVd|u zn)8`{89L<2lmHW$bXYNq(g9=Gj&h^SrC$l2VGVvze|nkKCNFz<{+nL+RPXZmP6Jwp zmt~{AYktvDsEh4c)m2IDI&BvKIqw8bD!9ZM69^KZkdW?$E_Gdh{^KFG5Sl-9H;=k& z?FuWPQj`5GY_f)mmPz5 zeA;tbwc;P$I`b0Pxu&y9ogT_Jcd?E&rpe8RA z-LP5MngKSu!^k7Y3{}+&f)y+$)5Txb+U*6RMo`%l(|{>$KGPDB%}K0s>a$wPGfOoI z_?ae(CAx^wj=!yp4w44JzE=4pSu*YA4a&Vp(gttr(eg>1s_XFOXHEQ-oZ_Pl`#9&DpfHIKM)Cq(cr z@~cbCg{(H(7Bz}?X)AwO9nm%gOOm<0Z{sSfLVLA{ySFS``$mh61Tux3jM$00s1+#z zl|yElJd%_qZi)HXYTHKB;gglp>-iwg5mSeYT)zDU{7in;da1UIx@)>Ku1RV1 zLMNtqU5)!*-oU27La}k_?`CwfN!QgFm}2`*P<>w~Tq-dj!|dIkYyQ{jR{vUk3utww z2Aj`z*1DhBoy>3(a5;_YIpmO28C-D@4*QkKd_%{tU55|uqx}?ff54FOqg~zHa3acA zqfw%lsgsZDT8Bxs4n*fp(CEd+=d1e#{?9d5zWFLtoi8SslxarmoCb0{7*SHXJXWEn zwAoWEa40jcI|>Psw3EF$S!S)Z8x_Z9)c^GxHqTr(_={CwlV98xf_?&LFwa^K`hBx6 z?Hoo!=&xJadJMqBv0$T=07N_#gnqpS9v*vJf*L}uG_y|_r|ZoEevgi<+`1pPy6a#1 zce@|8i3=Ni!nez+z-&uZ>eT8}dJOi{;gV$xf5(RhTE`k0)p*}qq!5};3$-&6t(b4(*j zqTh)8gu^j~Y*bxE!9!5r_|E-3U$3LjS38Kl=}Z54>7axq`0A^w{IK5X*$k0mjc?(@ z@_%;$s2RoJ_T53DIgxgEisM!?F($;6)jw~7yB90x1C7Rjh^|?yXLyeYemH0PSthJz z1~Ir5!9p1=z&DR+SINC%fN=|oaJ-1XriJrh9UWedK)Fnl!EUsZyx^5U+k^RjtMq4`dGbe410n?fO6q)> zI4fO5nKIgkgVjD=$1wHW?WvQxWYI1I9uxv*R?N?Q6QajHP4Fq)YeGN!Gn^Mqsd@o4 zf3}jv66ysFnBJj%yy;aSvt+=+b#nDYlhsqNv6@r#$NA704Jf0xBob+SR$oA@qeCHz zca7|lEl8$8va2%7XP#sGz2;X>o6A=EIZkPfTJcjBT9ZA>06j;5qFT5uK&aBtOZBhE1?++HvpLA(Lq!U4o)F{((hJ>@MyBMPIDyKs4p zEs+cd-cT2U)t5RL^tq4R%}9MV{gsWJu6sWo6n|Hr0%BLOT$y-+<{-r}Ww)@u3Nd_~ z(`eUHNCWxl`tP@&G4rk)-`&m^O!QG0o-=D*h2eNJlhPWVLm@|vN-Y*Une|2{n97Nl znak3hM}~ex*AGtQUT-0|caHxqms@KjztS}QG^#6L7|6lJm0KKc& z>S4#>eE*YsgDHR0>4$yT??IGY!chI($b4sPE)evR=QWLq{>t5nGljrGf~4dHV=*cmc!t?fHyT4*sM?ddgQhabwuVdH zVJ8^=F@cbqUr5$WHYbRvHQMJ3r|~5Xl}d3+NpSKb;VoX}8(}i*fWpBr`cx!q-@y?l z=_b}f#Ztr&Pnm>l@pCa4Cr!Z}fhtVJx$j4tFEoBCT?UxeRe-=l7~|(jnqal;{bP+) zaW4vRPWliD@+9R0zLtYWYqfyh_WAjiNj(c6^h<2H>@OGVZGko5)l#7J#x3Yklvgp9EC(U+47NbD zn?EWh&=vV`yCg;K=2im-VpMmi!Vo{KxG6J4Y#$;CCcou23GWCvBVavO#NGHa-~$UrRp-EW7X#0;Q>&p& z=7@6Yv_39}lWgm$Fqki7`ZPEUUQ|kD#cOE2gMHV&M1O%ihoVw#7?f}WWu{;C#|G(6 zl4Y!8UdQmFiIU%85gWejQ`4=Sw-`)uR)ix&EnP1#EKFKe2`A0LRuWbXPp|pT;Ezr> z2@16y-F6Qw1-BK#SPH$-+g`}`x7(PrM-IVlZ};>0r={p2GIgP(w#5k4@%xr7LdUnf zwemB1$+p>k)4pwu5Y>L&IW0dC-unzvwS@xk89R{A!TxDaI~~%XPZ$5EJ-K(7iCcW` zF{|#SK(xYNY7dGxN>OgJcfw{>@~)gfHiN_|STLQ9(V4)SQHfX~&TR2W%n^(zz@9$E z)9CFEF6ntAT4JAHU1lw|J(UdHDe@8!DbP)hke)g-`r^z5Qz6!RtUI|0wQ8f3M2 zcv964jm@vZiDaz3NKwwUFS(^>aqy;upa12}2rE&vRcMlBw3h31!IV`ANybouLzEGM znB@V%DM`T;A?TED-94LgA;{i%ivkI9$!i1R1)U?jUhHVOm3TChTM7WGK(GLnOHEXn z#6?&@Ce0zSJRd9|OiYMYYZ!gh;Gh)+VhW&;q1sFULzQA{IWWyc=n{X5)cF;^!~k8i zrav}Vom90wQQmC=y(1zr*JOvcRO`iDvk+Y~4`YR;mLK!!@Osc-#FDmHRij91+RgVI z`RT<+1(jlxs?6)=vKA1ofq}~7{(>wN{6#KO;^X^V97c!FucAxgjCFIeA{SCpDSNy~ zJB&5ut0*t|E}y5JyEo&#@P+bmSsUOLFb^!J8Fx47*bBSpS{VKvath z`uCEDJe#8DkG4%S@DhcBw6@T^+1qpB;_yKvTTd+%FQmh6%fq9>4`=GY+;y+ zO@XE>Tv4D26Qa^rAwk#Ap@!30as*SYKU|?s>G@V>u)~tfXEN%`)wop8H~d2qou|hY zR51rKq(MBaP6F!fFCX}QRLqR62ULf8_Q%t5?0YY%Qjxluk6B25)sixHNx31)vukE@ zxru&Z96o_(QK{JDpqG&X*KO;zLphOjZ@gQG^&DRR+U}G zog6GR4TRF%l5XwIbf++{m<^6Hdg*m{$Zfa9rZ7a28wV}$NLcd z5ii>KTl?XncTv3QlC~PU7K!k3E60D?J zGn0bcNd<(!91q7Fl&hL#|qIT~s_ep21R;o!AMmzOXgSIwvI7N5syS-B7;rPlFi7d&%Gqsiwn9rtwu^I=pKRPjG3y1@#~<#n6DaeZ||rc$OTe6-NP z>=jMhbsUR%R{TR^9Y(y$qqtZOL5CIE3p}h&`vggQv#Uic-Q3Z3ola|8b*I^JKRL9+ zM369!YP+91PJ;(}C{^V1S>e#Exk8Y2Yma4a6U}9|oSLM1pG>L*8+yov-gk)jg!(SZ zKNULdqTRnLfPhl|sC#4y22I3L0#)*=pJLzSY|-Zxi{M{hh3?1*Q_K-cYHKjcrljB_ zmST_m!qpnxXg8U3B)f{4b5X+>F@I+PewM^+PpRB;xjuKL3u_9uRZa=LpshRs`7LR8 z$Sf0#!MBhC3RDJpLk`cIwj(U;bpQFL^KVV8bDZQty*hJiqhy})FoCAjdgw1s*lo_(4{3!%xxwJQ^^Q#>C)Ec;(SP1yg?9Yg z9*56;6O9*zPfAmkxim9=W_~=`&*XH2P}q;mU}>AOyv!+HEQ@FH?VO_Z;e7P)LL8lQ zMwI{4_lxOYDEOyMfSb|$Yv<%YiTsZ6oqyTF|MgdCaPm*pRT)91lsA96+LwuA7xLJ9 z)}YF5(Lb&6>i<}Fu~1fF5GGF=<6drxK{~iFQm2rkt~T_o$pQeyhiyFmsM#-0zg8}SXQd3=pCz62 z5rnD|K8?3vkN%a=Y#r!g9^-eo%lIDaSyKXUv`^YtYm~23FDYpGXWnn0RDn?d&OJb& zi&TrNsko3I9}-LCs~h=a;xiFnhwcS|?&1jouR54+eI1^=83vE)@o`?5f% z-#446B~|M6zSM1W3LI2d+4zsE=C&&RB^HB>mn({>4kz8}rYe+NZZgBGGnG|H-f{dA zL`F8QE((ZuDs>ls4^b~TCF1XOAjAvF>DQ-EdaH-FYB;rGl77<+0qQxeYL!oNP!AC8 zQUIb|Ne_U4MEEN@;_p(tgd&%YvM~!{$yP_nliipnIp?#U%eDa|Go4gYy`jj@RX_BQKBw|ZKMLwW3G5?eDv z>vBs$P;XQN@^(Q8oFE&r^Y)Ta3YRsSLIA43P;5IAt!ASfGY2u)SJ-^<*hq}Br4h%F zpB}kQ>X)0-{ZSMqfrb#iadTN1aJ@=pC+jt_M@rO%QW)D%GCJ98cH6_Q!rOcwMeYb$ zw+MA-pwN_?T`YS^SPP_6wQW<|{EFQSX7B?iE7dHH7)|}Th$Q8yVNos9AZC}ckhhDS z!Pj|7Wc00ewq#3_+@A;deZ%+|!OgJ7g}*1@NLl7(G(U(sVNXG`o^S@CMgcQ(zSW`e z3LS0DtaX@%hHM{Y4OmX}fc`br5NBTd$67PDtMR+IT1n}xAnn>sM*1=mnI6qFiuU|} zHoq+T=r4BavehP;_(0M4ser?s3*r*T6ZegfCl#lA9KnQgPGO5b53f?RSPGday%B#4 zU2czFSk0LZg3s4DSi^G`^<|H#j)VyJ)vJ{)9+y&UAI^y_0uaPE(RNyR8YzAxZ9tNO zs%zf8teSxfw}1bt4#xKnxi!H#H=S(P@gdF-X)G3M(PwRzV&e0~!+`L26$`&oz-yQ; zoiBR`x*rrGXzSZ|#;NzDOR?-Eim+0z6F8`30(&oTo}hT&oFbN1ibx-iD*~OI4v)t{ z%6f0C3`g;sh#Px3ysp(|NAYk8c=C`3WjHg!+=4Fz%n#Pg7V@GY(LE~4Uity)F(l8H z2tntr5xqbM60OWr}yG zjC#fg%w%IpjG(m^*HJ;s^^h<$UVb8Dd}`#3uT_8gs_hi^gM9^-TDUp?$N>?Cx^DRf?wVP6vB311=R^j5Tgq|fJE}Oz?Q4&|ygowi#P%>X$A?|?2fHGpIC^OWb z9dFsy8AQc=gmk+bcST)1GG>+fsk4wuUeT!+#9&QrTst4qgmmXK5EcC%D!k_C9GZtc zFdYWgEo2tD?^d6b_Wm3{|Dj>aLBsZ&^Hn=ob$B_wjY+w%h?foYbbs*cq zfrZWE=ie-k4_MLSX=I}G*=6V$h5^rw7{KPR--}#u9YE?dy*&^{qcKHt{r1YPD+sD% zG^UZY;(P~?+R*2_Yy<9^kn&B&3e8?&f@ngaVO=} znTJSw|7vmP)0H@YkjDv-2=L$2vj<4;>DeeQAP@?Kx~2OO33Po6ZhSjw3>ob%TGVE0 z!p=Opirg}u0|EqU`^;I5^ODbzi6)ANeYd15bw~C_2gn!<`oH3`C@leyfWqR)e~5(r zw-P9s(*gG8t+^(x0)FQAd~jg+l>2wNga=^vCSMHA?=VB**lh|g56$NPW9u!Wvh1R@ zQAI#P>F(~7?(Xg`>FzG+?(XiCQo0*NI;1($lc}xU z60m~$k;V7boFb!4@a$vJJu;AuE9c5(NGWBF`>dMOWBPz_Q=5ETuohx8(sadD2Q0Eo z2>+F4JB&Wv#2XzP4Il*?ctg5{PQa9JXlr6#C-4Ccc4nbB8|p2@*0Qp7f$)=5S?#(p z+4xbYzI@HE7lz}oo-7`o+K41F=cifX6ccG9TcX7`g%W*dBy8x+udvEf;M=5Hx#pz**4^Y5YjiW-SC>w=-LzW zP5Un;+7?*gHsM;CZ0c3>9{U5Dp6`Y9uWy#WIyPUU=PJI50}Q!#_$r_`!>?vj(o@-51Zq*0?U*$_DQ3uMk6>3E4>J0}P%TI%!S%(=JLl(C2#GY#n3=> zDDGfc&UU(=UG~{-Js;ZkM=?dgM_FkO2Jp?7ph_TGhE+z=rl`lD89gH6i`?K;Np-0^ z>U=igcePy1@?3Q=ODa=RKc{4p`%BqR4JOgSm_G_6C2P&s^o)*|=l#E)h0)2-?9RPk5u!8*1A374$a)1b z*E_RZmIy)tpZu8+rx_qC!c*tqPNr02C7t9{Ugi2~pSFpk?5~Jn+n@4*ogl@`g$UWS z?blC;qyOVh?g7ts%N)i%ATFW<>XR`X+f|{Xh3u_WSh%13}mAaJ1^F}3} zN!i;p*uz=IQ0m`Mj2~Ri1>wya(V;bLR;9EW4?gn2^*=$Pt^4j)Tk);o?o$6cfwLUO zHFmkl>Em;{{wMS8h7Cx#@VdK|qlsTzt{xQWLIUr}tP0MfFXMw|B&p|7*S~u85 zKaK-WQ`#Q<2JL@Hm_CZ#%qRYh`fH>TxjZGi85_c>46d>SZJw?#QyV@ccPG-XD!-&V zH~gGvep?D5KgL~yyI3H1;yb9A?8#{n@g_~&tVtsB3fM`c{AW z)d`SMty)jwo+CWCK#82DPa?z_*6Ni>V?!Y8SraLWyk>Wt*AjXAH^gP z-G_Ph0)w%Q#R`&onYiBcS7(PF_W!$?+CuulhArW^=aODWHIzgtpn7Szd0~OtGG`Dd z#_&xW$7ibqrm{U?Dtq$oLGPoT6X(7;v_ACfqX~!)NtkaFuVqdqlTv>aFGJR8I+0!T zdLLhrMYd`lXIft>uBbwQE# zbU|N%%yy}o?FO4wSk*E_n^sk4@*Vu#fju8Z_IDQU5)VMw2~`m1R$^CueNQ+SRvJrK zQh8#tyD*E`XIYcK9ic$?$x2R!U2R{7yaSxka3?@0SJQ+x!ZGMRAmY>0-BOT$Ejl7E zXUGxo*48)!-a<5LCp4}#upx3h<3bHhEQt?)Tg5o161~W8vi7rvBcLnKTi=R#OIUsl z`NZPJT0TKdqLe3YiI48={al=~XgTb7fLRUAF2hLGD(dj~)gwqW`(kVZ;y*^D`(GQI zB4t9=pJdl8h<4siH2zqL-;_09C;x*3x2lEaM_(uk`*t+D=Al(7_JTJ;Ei# z4P3cew6Wi1xAIJkALolTr{5s-4q^AvuJW+i&gW*f*fF+erkxphHcJz!E+q7+p9r_2 zz@=*cXnKSx0sC1N-SA?qt~9OIdtd8>eP#4%WCF~U=%pju;fscD9%m~AmQ&5&YQ`vX7RsO%@LmNOTnd9cotS| zRVC(j-7ar{BbQ3eb|c^}k`@`Du`@fSsgIWi!(s2M z=ayqcPGNfizc2GL(H>$7Y+0Z|DAStD=Ykn8VuAHLH`2Q6F z1Y}%9a=qOrpbHv?`^xQ-HkrvC0DW_0SJm1TB%vHsT9fWNA<1G^E8q{{IIUSS*42@f zwsX!B$rAooD_`AASDJm{o$<6?30ZDP7KCI>i#R7T_-s+Ut{+l9Xdzg0bO22Bl6`qp zt1ynk5o+_SN8QUoS4>j4?;+)dvLH_Z8gN+HYg*m8288q?^~MGKa*2)J{r8$9Hxyih zY9C@j*79LC@z9@#8yT0oNfJjPn2(PEDidW_Hz%0VIYc-GT?-C}{)5mr&jElJ#0sM= z#O4(MBEM44wuTT z)?3o*7{K?d0#vBJ?8bx`kDsM!69jPeI(?Kf$igsdRzxpmEKK4E*2~5*zoD*<%DhF7 z=12TI>~^|k{m}jQQJ`p_u0#Nwi`6U>bO!B)ca!Pov|fWWq$NVsXd7ZHeDb@ET~Wne zGxIhGkW+a_ET##Ok&8fPt@4DkcQi4&Q|V$_Y?!bO3DGn&ah3*-fNST?+1JQ|D}H2t z|9!L++Ek4!8=SE8nJ!dJSh9;A6EM}UmGK$>Hp=XrN$X~+^{0=-o?N7@a+m1$-U0an zFE#b|Q_rn0rsZwJn=8dq#A!*?lSe5@Bq8udt+DNJ z&wp+h=TF_!`F(F3kBrLnx|K_sS%yC8rlRV^d8dNrFPk;u^gT?W+mm-@-0)%hrWV& zNs4$40oEpOwb5%=zlUl&gRU?bvf zXYtpol@~GAkBYDlQV0g^P7#p@<21u50XVKW-@h{_rAW;c8p*A+Ip)yFMeZ`HChybF z9t6DOwc9n!r2KiebpesA!@L-j;fWxmV=K_0+R6>vvs1`L)O49M>v(hKRx3 zQOaT7i>>}ozanxVz4%X}m1ZzkXl7SwFwNzcK;C!K{r5QzZaz!Wpvw<4VJejw+0{y$ zSC9{iw9h&NPR(LF`ZvQ`lo|wa6>61hj4rBt-j`?xl*y6kIWs2aen`0wRXg!xJkD&# z#=jp*O#w%4V}>Fyy2lft_b8fk!~9%q9lS zaAMt<6or=JV|u(4^gI!rMIMi@vb2-Op!gZnmOZh3Kwnz}s3twiZXt-^NL=>}IYNOa z(sj9S?_e!#rlkG+%!e5|3yH6n%Br-2Wbk<2#aK?QTNtLeyRUpT)v%nR?xZD_cw(ha z3m)5bJR68AYVquH`0%h|Y_&3e_g3S;jGW3kG#C%(f5_Ae(tom-3(&2!fnZ5_W)U_J~-Ac1$o8CYS*4kzrZTSB+Wp95R(dye$D9K1La}acY>{ThO z5;w&U{*0{4B*7hc1#&w6%g9hR?Y)KxFQGH8SevUck~6A1VS0>vP? z&CiB+S2$rC`aYM+=h!H$Vfjpqrkv3bH^Q(``-7EDG~Z2*o4x4N`vukGMUkf* z26Q|QSA>*0UA@Ri4TYq#?-5nS_zH2;(HAB+Im!VzBcy`!sYr(C`Z5{{LcDs z6aZmk^4QZpm`@f=76O=gX4F_h^c>&Y%?_Q<+M?4Uf95N4kXS=a?Q7y*ngR@0=Eb3F zwhtaylNprV<=@)f7ZGSQi_^#YVR?%U&ofOGKZgv?SG2nYpHm1xG!>H%r@+K;+$r%#2I-Mn)t$um zX`gf{g;eF^@%MrI#(rYn3Z7yGPOscJpw+;<&#f+4G!96D+cLc?Y;8A(270x^w7!-X=fnqi6l z1*p}&18WCuRpb^Ra8*V>pt~HYqeR0jx#F@Fz3w0SkhRq6ipo}@qn1`i-qA3G%ckuz z{XS;Kb>F~01S2IoKnG||sXZ=`+FBBNtPqcCR|Ss$#1;#UA{eI5qJSIe$J(Mqj5>An&r-m#%rFa(Mr6%J2g|6 z%H$0x-_-tM#U-*`lX@5t3Z~5w1weZPjkZY1!kcZHj^ujY@O%W`GQ4(&sJ>A6uyPx~ zwv>oB(~LTrekyUBE%Sb>&E(zIkd9}DY?2W1nXN)4LIgT7!$GD}h|4$Q5=aPv&`s;2!orMYS&Jt%Qe$r+U=9)ePB&5Gi{;#I# zkhkjdvZ6lP3j-nm=d^KILQ#+OCE3VtSg3D3E`beWpNP|E|ecl;D2E)8yeV+Sl}RQP)i=0UYB1;_A^IPGc!v_1#vlQN<|)pTyq`uwY@j#!ShRm( zRcf2vdGD{h>GM&wUiWg>9aQA88<;%P(D571q$i=)$|AtpeyOiApPhC1%305VRijRt z)(X`E7pLt1-PVXMNSIt^D7d|kx?lu@fx5eei#`XUAH+lKFIoz;M^uB)p)B*+~I{|Z5aA~28>xzl=S#U<-C@;0y^4l?gyzwG$y6GHs~DX zcY?O!)+J-=#iNawmPz!395p&(OCEDLx;A~TQK>!;o6mZcioD$(!fHAaG9IS37P0^1 zCTPP9I#x<9pLRJiU+Bi z-_L{bc^_h#R;Z8+LP^%wVMJ$V>**`viwrEx6r3-RP>}>@!l5rXY{XNQ+O5be6eE7V zHWbYnz%?0~^LB9R#g=!};Ueif9E%!Pi=4ZiHV`!Md^` z;j9`x2V{Rt`6t=dFppqPkwI7Xyid$UUbq+X`=G2#o65nW*X<=n!(iap^LIPPoaD8% zJQ_qUg+c+Nh7?aOK@s-rkz<~c0oMAk7sER)5|DttSds>>Oo+c0yQdlXn$PdAK~9C4 zr2;B(x;>!f-L2?n4l9$r$USlo=I`3|o4rS4YnZ`RGY+q+^O^1jxRY5*Z=f$hY!gi; zWzy^^^{@j##`>~VEH5qse2r_k`owgePZk-IKj@=5+RJ1OM=n-wq(jI3Qge9%T!$%o z?(~5|p5~x!nbF4i?p}MFSLhw>M8)U3$q)9OJhdN3a2_v&d@YVWo>mQXTD(plSZ#iy z**fJ%0J!Il;O1cs(_&-}-zf|T%Zg1R<_282lKxM*DH7K^%}Qyv9tHbMos_Z`U@4tm zN6{hcOH8iI(SFHA=Lss@{o^P{WfE@abnu!O={sy)IvNJjNGuVC#d`ty0>y`6LjMu1 zbS74NMH$8HW%4qq*Rs$Y=UkO5nvK$d$tw=~$K{B!WVFd6Q!c+@#FVJ4X(pAt#~NUC z)I4rj7iPY4w^&uHn5A4LYshYrbC3i4y!dxx8FgdE)@K#7sIj_g*{ZL6|AB*JR)HA* zDW}Z#r6L^kBA{OFN$Hf!?@ncg{wC0<)=nCd%_UbW10+(Ul^{`AgDfb=gn-N1FUQ|g zW=f~1XmCT`VJ8a1F>q@_I@%JxY;kcuTFR*PirNpdS{7?9Ph-WeP=}7-v-{$0G!Gge zMn65BNt$*zuKC?qV|2~0Ri1p!WuhD7gBUae27Q*jIHSw@c?~Xt3 zb34FyecX}P^9EHWwc`|Wzk9@tgOY30p~7wW#u4IW54;aZxqvH{&*7x`ma{5kGtk?; z1tG;~|N7duB8@RuB~{f%!A?v%xikY_or#bnI7oRYPI{x;e|Aq~86B>U&e`;Nl>3#3 z;W#D|r4kM>TQZu&tXyRCroB^z0m0TlD$ZrSiUTO*{X8T9N zY8Ty-@%cg(f?FSD_tt^&>pKt+sLf}$8rXM(@+xmLB5tso|6sLISir7@9tL*?bfVO< znol?Zm7Df>APMiBZ#99%eY*1WU;yFoy~rkJjIA&+1XlNmx5Z`dzqL+qQ~1?-(UzEY z!T%!MIkmrvGOXFc6$?+$}CHpAt3WOFs4Kfd;x z8;-!uvs>$+c&-oG#u(SkdZ#WGUwvC$s&!Ok5GVgK>zgIb9?+A1l}?_J_Ey%?twGUO zT1e6?@=Z@zsZgk$`pfy*Yij}36ZQMVX|XDz$u|{^GnIe&KJ)Vkc8cMjl!F%pYik$F zxl(EsGf*g`r3;?liP`ISlEll(4IZiyC(%_1_c1;=f&JJ=#?@KkBOX?AEw{Soj%BAczA9<7q~&X8q@&gE~_4f zJnUOh@5eJ1wiA8@0*dbX|^E$;iq zQ`?W!IoS53ttThr_ryJg(7|w685ktN3Be6nnMy588GD5J=afd|5XX&p3+Q!KD@=sU z;Mcqni}!7Ln6EuP_6WWt!SI!Vagt5twkeEA37Ft47Yfy!*r+N}Y0~?@t4MSOiirl& zA;8I~mq}~w3#sldR{v1s?Vw3vBbk)^?N+N8s?+W(K(3GwsWn;7XhViuu~y$<#f0k~ zAO0o>K3F6<{oHEuzr9XZ?cfr+wr5#yi6##lSQ`qrI6>TEEb~k%7~<$b$`csPk4AcZ zAKnq0&P+h5w)vR8kaN!o*&kg9a|vi4TvM@}K{60cHZ7_1A#iJ#PNSMLT~8oLvDz$T zg6ON>>I3Sl1 zSm7EdCDig@D?Z2unsnb>m!IodD{jZVl@yicB4Syl7iP>RbE4|0=?hiXJb~_rsfGzk zCBXCwyD4v6 zU<*@fb{fQ6XAyObg{^G!h?VxfyQSYkqL7@9efJopV#bKENTfxewBmjwb=rQb%C$CPN*3?FE?=fh-AaQ=w zmELh5{GYw7TYU58hr(Rpu#ac7T;+c8P@h1%S;U2QQ?H5F-^;2cx0dLLautpc*rCxV%Mrt`0J@dpafE7#Hm8IceR!r_?3s$ zS5#uB5w_-&G-Zq*fDy%1l3>_n1`b`Vyh0ipllmxWs1!KCWTe`x|EWk)`Py8ldlBq< zstsd*1!jHB>-MjB&$IYM$*9zE-$f}A^NexY6w#pFN2}}UX1myD$MW&_ccVVVu-eXY zrEB)e2hgE9bXr+T6>oxI!pFYk`YBQ~p1}Ry2^P;Wn;o9wmzGy_4;icSuWu<*nCUFb zlMS0`H_{$U{E%pgZEx~?eTq4M<__!%hE*Zd!=Gm3qaF@QzZV43BzZ1Pz6h(bUf;Z( zG-|Y-sDJ%JSv~x^EV2^q4TLHiQ7Ve8+?>>LC!?}&|H!E(;5_Cr?fN5aD$i9;EDkwI zyXmyrd9RxGFb);Km4pJ>8!i?AN;vO@R`0W}kuEvB2OoV zQ|Uxl_8H9=f;UrH+yRX@d|z8^bjkok}9Lha~ho%-BkOQ%SlQBF{4iq5H6jgF_% zR!{Rj|$*{xR?#V<-NgpXE}TDncAR69ECwI#?w4>Np1Fkz-k?kCIs zSTEM!6nZb{qq$1h0{qms=tz3FthQn|xL?R7a?BO7B;R98Oz;? zEdINplovN|iXql%fQ;#wdbKoa=~Pl-z%5v5*S&v*oG28=<@wykIGaez;%A~=>mPJb z{x8UO2g7>E8O85^d0DGSCGi1grXu>3nTM>S>**pSk|UdhL$^ZJtWtgGPjI~M67cd# zx|!0b7DTZ4Wb}V~w~P>=_@isB?Y~f)rLyK`Tk9RgAAN{_Ap>ktR;%B|iO=IB zm{J;T5s^Q}8+^LW4iI4gg3h&AqX__XdN3AM;ajMcb%`?6(dto>Qzpyn1>cXKv7&~) ztrTm)SGcrFR~`csEx=m^)jgmLeI;s2sseGC>5Q^Rnp|vrnSU<+rC;?5Q$Opn=Zr?` zea;^Q42i?mF`>;!N0iIY8QL%rDFQ~k7>j=OH;$SanDx;>wxJzL(}Ol}ht0o;kBvr( zQMX*}#By35`@z4a9%~J(=cXtu?}Sm<3qFSvBEffdcS$-7@rbJ(g9^J8!?CyRqjWVg%rtb2&oc>c#Y=-g>3vbg`yB~g+@gW%;h&C4u zs|41M!g%^D;&SQo*o|0qKfVe-_xx%09$s)cSa`2~7izgwJs^Zg^}ZJsW?UB=vx?6? zKBw4E4+xl1G@_^1mZa|{{{fXRv0ymzW6Gcxiw5B{sBmKH@%pQ$M0i54K3lClR>E;% z|GqsXyi(XjYy5Gst2tfQIFMcxb_-AAz2{npk&elr_eHHawz8w!Hfup8%k0LboBCp{ z+i6EW$1Asiq3?`B!SaXuTg%}!W0zZO%k!9F-g~5d`}zgMF@!^YZ+Ewg5*`+izoBJ#REu zpc)|*qS6fxFLWRLh5aEajA!zg6j506zJuTYmbKISQQYG#ipq=$HI>*gD|ZLK-@V{V z9z}{Nv%*k-O3|a+zF$s7khjC$n&aD0qIdo6rPEsm9h5(AbK;>&nD%?D0I-rn`FDQ} zlV*A;%|Nxp1*1}#yULyWpz}Z0E*9L=k^e6|>{i9GI$|3xEPZJ13!hzB@FINRoCm6q zlWqh1`zXcSYN-;7Pp!`k!5Xi+yZ{IDy(;RgxL~=zT(JatEBI@1Y$d&Q8e~O{J-J<3 z9L9U=nT41Wmq}`f0t~PwNPWm`8yFhZZNzS z;s_H}{dB(}^}-9N)ho3=LmG)JYxPr4H!cz)vqO_P-#y_X>*G^jOYdCPEgnVP&YX#8 z3~WImVkF-(T1xoCOik9Et~}k%xo%_k8=1(VX8@g}ar*gbv~OA7Y)}s&KhxEeeQ`9i}`Ydi;qz0gzOxu*OY8wiA z{{M2=Cw zQTi1_^qM`tcu>v%%wPLpgr|rTnE8Gml`g1IVLtN)#*SMg7-p1)qzpSzAXuc~I?bL* z72Jb-5Bvc1nH)(&@L`fK3JG$Sx+S7owK`^?V$ffTe?Lw~N{$md9)Odin&*1Mx-7L^ zRt4JRRwT0*gie!n)3-ogEDhwVUZID)7x~FQ<^f0jp6>bAQIKRyPXPqp2PSS^!y2*S zl0T?igVPp#Z0f&G=$N6`+XX8hZlAL5bx2(f>$jbqT^Pr_dION1?>G{^e67z}FZwj*9k!R%$QvdapI=fDlAy0T_~~0oAXX}A%15)k z4WBX4DW4%~TwKtY18hiaOzgZ8men0#T4M&|pkxpMgkwP9n>{|*_ z__wNwD$^*Yc(u!3?oGGeZxjv}Vyydm^XvNlaEE(%(3t1`q4^f~r0)4%ft1O$mte(v zw{l4?d{fABO0)!&dB$(Ao9IUak2+E`=l)`!@NNDkTWQD<%2a>nkeJ8{-Lx zJl(b!OH}dM@Lpdv2FyZ`a_=3Ri>iQ-$#!P5_Oh`08=;S%vnSue}^}vr*$%~+H*#9Rq7fnG^+IBnadE+TmaR|Mk@f5KWM!Rde!PQ%dqzL5||7`H8MFbYgyxk;bDU_wBRSv^mAf-{^{< zbQW?6Y@6&_<7v{1yj+v`21ofZ>F8?J^{=%$T|sOO&dJKKcevc zxYtfXU~%}c*}(%+4ZRtK0N}DBGOBNhSKHrl^%!?f!Uc7$9Ix)EIF%4kAK510_l$HR zyev_HF;}im9ynIDz%|tnO5jA4O{by`2)7>Q6_dUsTbA5bXa`T=>O-F=53Y8jRP3Ec!t}Tw5R810o%u_U@goo-10+ zqFQpwb?f%%Yh95xjnaY3Hw4`yJQkPwoDd609+!Wxfig#k z!c81S6go1Ey6#%P+V;JE+sYRUDuU3(ePRUtclXRp=8s6UkJJ7@;8eK%`xo5Ie;H3Y z_tWJbU(}H_+fSQ_IGtn}+APJLBT<-}@V=lpNP6Tn9%IQF?4LOK?uSF8F;w%V!K*`W z$9!1AgRnd$3|M}jhPc%)QNJtSJGRv2_e7Ft`4^qtNBH?Sr5KVfJ65aG_m%@`y`~Uv z2;(QvRUmNx7Tp&l(}_t3>|!onVSSIS+7ZN!ov#c0ham!glba!sB}lQ32QPj9NDSitG|vtCm4nS`oa9C<|&w`HnNV9w`_HHuekx`GT?5j|X4W6CGEkQd{zQPn9 zmuu9}wXQkPA8VP6e~eQDN}Td+5*YIzI5I1xlx0l1bf`K^I#HZIleCx zcJI{a;+Yeo8y&*6@{fSG#%ltO+Tg$KPFZM)%3~A%{kWL!>t|$Kj=7lH)lE!9QyZ^z z9b7JaJ6l<==7>5v^u42tDV{$3f?HQ&z_KS4hBtxA^}R^a)aqy@WxQOq;nRqSa#fm% z24M%#Q@2DDA34n50Qpq8Zd?3Ek86wRNN73-E{HIN)t-UY0C_lb#Hyfo)H{a_w32-% z;Tb&jer3eJtg!-I8cdXF14JvV-`P#TFhN9r5gLjoPQK zT(nz@qFZ*R*}X;j|XjHR|cczoqRwqy&R^=cHd zAtcso3F1wu;6*={sqNL6#X^VEEb>X(eRPfnDS*EPZJBLeyAPU(50|HnkHOOnVBS}_kwR~;Xkw!KY8&e8hbGmOd zqgPDFv8=drsEzxoSf*RfHNgcyLfy>PrX?agX(E>$6}t^=8APykYvQ&2y-8n0^_Yt! zhLQ_zYNp3+v0jURLUzD`&}KYI*eNSe?-YmQ8M)RU4{g3U{h^V>?Nwla7M>N$eY(^k z^#12+%d*MD*u1szpQcud4+?Z07yfZAotUGg?lzh*u4Cp&yitGQ+# zA|aQGFY&rLCxbOO{3UIe6d1QsLhER+{@zATl5hrE`kvcPNsX4-H_JBfi6Eqyi=TGM{?I9!V=1UFx#``~Dtce1s10tH7vQ$!I-nV=szgbiKZ1;tG)l?(P~j4*{Y47K4%WT>RD zIp+bU>Lla~NyuDYuW%qIgP-m<*g5#?7pW8jvIwfe{cLz!35Am{P5crD%a?|n#_kBY zae!ch1#{;tM~5nrmelW=bx$Il-KD+#EE~+Izpas^AkZR*Dyub=!W6HgSq2E@7>R_@WN+t9+!7enr{?b13nN|M+l6 zo}OGG1CGn-Km;tD1N8)8iX0AcHlNEZ1h(JwMvrDIJ%^CHvvu*UY;0LS)qx01!oOz% zug^ykf~wZ{p58$C<=Z`CH{jpU?g_SgLG3nFvAhX}`o>}gpN-mPf(etXU&}7%`lpNK z+xDNy#Y$J_hg}T5p$hd-q5`nN`&jWO^hfAphrg21H|&SiKRnGzzp%0w)vHs8y1)Sv zI9?wIg&X6~K6}UMyyJ6GUl(K}_eZ03IBKaYLC}0i*5W)L#}8qT62v)R0oxwkk?=VE zivn!AA@;wpUYaOKKxFVEqK*Deoga5b+lRI5dU2A*D4E0`bl)qt{<>JT$j1>+00S#i zGVyURA_~NpN{Ut0+HZ990?E|H)u8kJV>gO0Lx?!5fx$pDHazwRbM*cS&7yGFx`q7R zR$JbP3+XTo{qDb|aBix^)fArNMIrmL)ILb;(Z<5j%v);ceqYe!vH0Gh4!u)pw!J)- z8!;#^x59_Ver-+)j%{Gr*D_>%df%jwQn^_&C4WzU*KA5>iVo=WjGgs=&uIzwwE{^; zWzE~H9ij6^RtQ+ht7jYq&T@-$@ILe=9ea2w{p<(U3KcO|`mFENs!fm>)tXa+6+}qj z9koW0(|7ZDu4+a=CB1ua6ZPjf%H;A;(a$ia(<#P_p*KZGLClpp>A^_SeJa+;icwb+ zkyH?dwN?-Nm-{Lw6nbm=in0sSN*#;B4#kx4ytcIR@0Q7vX%JO%y)OxFwHlJojlk+# z|8WTvRCS+YGMh1e9N~);tU^IhbhN#4;d%d?ev{|8Igi@fT9b=%Wp0u&t!5Nb%A;n* z4uOE*m-KinlNi~nh1xns->&~(^O{Ba>a~Y5hCUf|{`xh0Tn3e+a`lfc!f78Ln+2na zolX8VWWF>{fiwMhggP9#Gfl15EK0ECd_enY{fz{XSmm6R zoC!9nzA2Tk$xsqZ-OXEqp&y9H!5TYRsPZ0R$VcFP_6s3u8-3v`x27jJO7Q$;ZWQOW zyL)Q%Ijy(fjW(#Pc0NZV*9X7yMs5g}|L&SA?omS_x^{1dK`myT`m08I-QY+sm0b#L z>mtz?;pL48@0iL00gmG)4kS47+z#KkvbzAgV+Gr~)P-zvW!+Y+gv{=8v0)Bg=fRwd z%wPzihncI@#gRZf)i}jrQMa*K^L{sNJfK?hcP&KLgUuJhf{|VV=L($1ccX>G(fI4$ ztipTjjhFYWy|XsA(Gk;~uGq5ytphNBhG6FE^a+62id~2(c~kFZVfK#V!*w4B4`6Z; zWQ6pAUv(adr-o0h+lZA+NJi)an^_=`FsE1bZm1mOOTQ;aPQ^L?zt1FsR`(8_&y&?A zrW)G4Sf}e%R3KRghCM6PIlm9G978$}`U<ZNu(f5_T^lgQjaOll8!r7mT*Unk2_u7$=FlY%!Z?^RF8jDYs) zv(1)n1(g91%mxwohFOGGwuD|t56nV2mqv-sF&glXx7CcQR;t5;5YXmGx|^v;^27P& zoYA+9D_AZTMZRhJoK605lB#U>#xY?UnZxz3{f&T;c7Kg+{MYl(WqT&z=&iy=$c3%T zya)-0n4o{Ruj+vJR=ezgnq(k>MS5y0mxZN;mF#6e z3-ssUi@DH#s(ZS}G35G{gF}Pd7lMN59KL=9iDJ_<(n7coIyFuoo$(#WLsqgi z5E+g{8IX+!^eK9KdI!lL2RF^xj=-(@n`nwoBoGr*+|`|_tJrYda-Io1QLV7%Ye7#x zM(bc(n8{92mpSg$Zb25S&UafvI|iuHgzZ+<1Zz0U`jCX7G8ds%?n1HaVx9aw1p_;t z+!Uh@Ne7T}tG5K+{BKm4{V*E?C`a> z!9=%TS;O!s9v1-Yv~a|qqbhERg-_GB-Z0ztVa`>oRC(SwuJK^0}fOH9!6$t^3bNc(8RN@6yNdcJCuSlal<*^FNMa9s>Wg z=vw0+a5Y-7T=AJ_iOX;4bbk8`Z=7%HPi`>vm<5{~P70(Q3x~e147w`3H@{%K=Z;Tj zt^M+op3&2XJKo~yF&cDllF8HmhGql^@QL^z&Uxi^TfqI1(NvvnC_}d>8|J%{yrvK* z-BZz00wGRi;#r}x56nZU*Z;aQCu#taf=3P!xGi)=98A541;K11a}h=1>jsv#GmKu4 zJ62Jia5!mY`-+?iX z3F0ig=U^(&K^bP$=W$<-*Y^R-fZtcR=kC=&IJOM$5elOp!~ptpqs#Fi{=wcE`wjLD zqTQ+|O=bqa7~TYS?HR&`+O=lW4g12+*0l;TMqB1T8&v|Ho6MnySq|_DRL1b+-xf%*kjIsVK_PmJ z?yAb*F4hW%rN*GCHD&nU|5DR!^_iyVJ8|C%_KSd64!K))4YBKl@Opd3<0r({G+OKb zrBI8{mk>F1=!>Yw)9op3FjsQPl7n-KrL zZw84T&~-%)40Tbo1kf)dAyW~V{owBzz|Eo~BB~5I=*_hIiR0Oh29OY4zWo5vblCp|o6X~k z0_Jg%bS9%gHfbjU^&cf#k%&9f>(HQmQr&A%DC|hi%c~?7~;nB#MTn@h%uCLr=eZp%mCwDp&eiYti z-JiQO_PDw)F#I9Y`{tITpf0bz4inLFXHeue+f%W^>y#)baqX)c)X73MQm4f%L0+DL zE~~|gvV#SNmIK=FH|;IKTRWY%!(WC4c(+ckONfo&R^(J{7IU%RV*v;wBW?|G zOc^Z3AO09n`@?@Z`;NZer%NSvbtV2pK|jE!NTb67-K-+Rx&WX_Aq0B{1Kzl*zAmce z1dyIujFJtJV4M#T$buVbVP;!s{)t3>`r2cE4p!&@Y5?O*V&NxOf}Te#jcP@}b)ZNG zPzF$|-X5~R*mApSjA(7@!~7y>1*$@R)R?h!8V0lXH8tacQNfGgUY-KUCqa(k`SCr$ z8>fK3_l zC^|B?kx`_Dko7^rd+-^)c7=SiMmMnqxerWb$-_J?453pyni|;T-XL4LpD&rAZn-}H zw8WZg&l0Yy35;FyDsrCciacW*!yLXe4nud+fl7s`V=0Rs4j1xORYU!p&cCTrCdU|R zR?ttYK!CeWyN_Qe9u@kAl7SWj5mZ&I>IjWD*qq(EgFvc(yNKN@{LPW%Rf%7Jq~CHD zdRB!+f&oQj)4b%rwI%u!SZu6s_`($o@U9t1kwiis-dLiD{oZ?GpT)mSpZZ7g{abC@0j}MSkl3OWS0lsKOgxXZle( zpq|rl9|CW^`>n{s4Xb5ov;RWAI27Je`*^YKA(N5A^=Hdqm5SHMxU{KsMCf}}nSLZH ziEmX_oZGw~OIH_^DU=dT4%f|z#?;FTm!ofCEbG2=%4IZSQ7e}s-y-UKB0;bwpAj&> zq#*-XaIqA){PiV>5LHVNisZ0%9WPvOO1|c^(~+>)7jVBD> zvFsG?Rf8>J^oeMRI^G7{b`9kfOIIQpm=qR3I#lR&3Z0r6;X0MekBonVWT{QNH<3gL zE-n<#qHsl*OTVc6G(ZpaW~in=1vgIYhcabM3{=taQNt|B*3IeT6xFmxWp%u~!FTF+ z^8cRc3%1AwAwp?4@S7HK6P=!+P&_tR8A>^e!_hJ$yol%7n=W)q5{WiZ#`d$@MUZ*! z&SaSVM`t*~L|G!!X9mmJ-Z1%`N`+mA?LJmy!%=vfuS^B$7sqovo7ugABpqGej~KwA z3z=iWra-M+G87v$hPX@_M?hkqs=9rodd&~q*FqrU$OinX?=>`(bMau-V%_9?)f5Vd9#XT#n|j0ECY6g5%pKqw%&y;1(5Q6(AELf8Aj+<5R}q!&M!LHZq?GRN6p#if z=~TMAySqWUTS6M7TS7V|&mMf<^Zj)wXZF4Jwbr^q8l-uzfS}}_8JYc0Nw)b8{hxP! zIaOAP^A2j&g}OSwuMfp&ykJUxIj0B>i&2u*F-3p|Es}X<)P0Mn5cJ&&+-rH>i)|Cme=u z=tGG0Ieq|a#c0C|-d9gI$qX4WsxD$miC4B7yuLk$8|xhgzv`}=%C*;>+%>M438C4B zKk54kl~T4*c!r*x$Zx}AvxSJe*A!GD{*Ize;`Lnq)oH{mQ{#z(&Ep|V8UoxrwV?<2 zAC9BNUZZ(LF={BY(82?n;xqaZ$c$99BOFHDcb0jr6@}k`eRCiZ1=iP^b$5xZwH@Jz zxVu?2`eoZB)QVL85nJ7r-p7Rwmcq0tRXibD{Bq^T9lxzQc%CkU>|j#Kpd+@g2(bz_ zALYw?*-`!mC}u4djpt|GB>M%SF@ggH8AWkHgC`n~#ZKD!R{h9mEShD)HMhZ^njJy= zExVrDU7aU&8CkGJsHjEoHzakSaKvAVc zmyG0e9B(XLMLvjmzV-|Sk_`8@u6GyVV#WTvv{Yz>v6$EO^4;BfANm4EsRrup)t}f~ z^_ZkqXO7?mS~|`;V+6yr&94qN8Hd?wx`rBg@k{Y3G3IrnKs6 z9J)ef`#^c}Y110+!X4P1PUr{+;Y|bhw%W$WY`u-)r;8@P z6YnxK6TBulj1K~A;L`k*QU%()-d3OuF)TRr>AmnDbST4{#S;240F{Ft=PyGFn%O*8_ZF4ce|aIOFO#1^bwdaXa@oRZv2oQUcbu042H1c0yVg^)~h-0 z82&$N)6n54YG93oO4g5tm4_I@Me;eiGc{809ESDpMgP@L(`C1!(klD2=z%njr2paL zP;Yxw@I7~g@OV!1TqDHpF%*HQSZwikba&r5-iTW)R8qrjb6&)v)pCCBgp8n`k$q%> zJk-!fIDvmsI&{s2O4FIM9(xkxj*c&$`bUxo6f z7f9mZ?#2jr)`~3nj-BiEknE`r`UF$3F1T^cm{39Qya%HqZkq$iW26@ID0l)1=Q_Vd znodzd_4)%o%EJu_*4<)}o7law#+lLulpIRwgp^{JrZ_qdRttAHbf6x6u_JGw|lYB)Hz%OEFQax``;o?8n>CS5 z8B$xt`BuXeXT0&;jf$?&ze5Ve+9Dzd;O4!-l+)Y5Wd0#EkAr`sk^eju!A|6LjsvUvY#4L`g(T? z)w&yK0zZ)SUHq#+SYO`*ladx#?iqtwiwIPXH;2p3Ozts!=qup;cDU!$hX*wgY=np- zkMVSfTPW{ZJ1d#p-brWBG$KQmRO6Gg8%wtl1{RkfE|GLc(3Z32#XAtcCofA}R zEY{95Ph@qA(w?avN<4mNXQMZqcvrtjRfup~jfJ?yoS+HSg zU*Boq-h}hZMx?a|$!7@$;iU3XP7mr(=;0 zuq)*}3cow_8EvEc;km?Gq~`NR%T%D(q;MqSCJW~GxCsO{v^Z)*e*t`0pPF_H#9@i1 zXB!3WE?n=~8c+lE8L(7{I_D`R(QMojucmbJB-m2fSJd=5#1=%X_0=J&C(jb_dtlmb zympP#?<(OIBq;&rSNn%1p@nOH?S^fEs9VUPX^j%sTBk~X2EZ%F8>;_~8{Bhn9>eJU zu*fUkW1z8#CGT9H{w3RU(4uzw)K!A;(&XQ00u~B9Wg##Xu)Y^j0)CIa%s;+yc)?h7 zV)P}=u=LuDehVEfm{@Vtq>LBhhcA0X+3d3_ipUk(vyL>Y;}{5Y2$33Z@5^taVr;jg z6@`2gdeEliT3WXsODO@S1sCp-fG4IE_{){7VUhcurteUYo0!s4*cbLFjP>OtshOZ= zKtKbQ+-!!y*Yc@*FaLd}Mq8fZ{0q0Dh^)he;X+w+vO2P|KEr_vMIwD%xiNFra$_RA zjGOyC`@f@F=6h2Zn%4x8s&i^XjHM#R=xuAHzNozZoQUL?kj-$t(ACH3AxI?1C52sSHwXn_w$!*Rn(0%mJXpvfiyzKlIGjc8tOh~P10a0iIz_2E zWFYZCi@hhjEQLJEoq)^W4IYODLqc9C)~-@96{$&`rUNRS;~N&q9K58M`BB|kI6#R) zZ;m3MdgP&f7}+5eHf3K?BU|6iR>@D-U^5zg#dI_IY7=wPw% zBygkX=zrT(__nF?cM`3GrjDU_7*D-futUjx}(U3pFWIq^LlJ z_y>*}sGx)>Ej#>5URnDNqW5-0S0OG<%D=7NySaA~k0L(qFHSZaa#|FzTsfvr<@m1Y z5E!R&P)GE91reG+OqMlRP5mpzCK;o+iR`Wq%o;+-IQw>ENdzcD;>S9f3wT|0Nz+f8 z=;MfDKX|pz?>k9*dz;eJG!3^V{DK{ImzTLfTFlOi&}iMReH9XS=_06Kn647?b;dfO zDXdy`qw0w~hIYJw9iUFWJ(ypDtQ$mh5BPB+RV{nt_@L?e27-dAZ}8#GW0@G!BRS#~ zbiuV`e&%b;Lhpkjbwuaa_I!QIf*TJaOj0sH2qi4XK~xCCEdU-T9Dvkg>Ahw(l<)HnOLuESd~5y7Y*s7R1<0?RxHc8 zG!XbOtEYt6V}~~~E&+!tO0G8lQD(=4xLB{{1X|`%lNR}>uNp4g=+m9QiU#g{I;(R3 zFce&hvjWi{JuD(x(t+=xq{PVo!h9kip^EYIH6Ydq-1Dr9kq{AOEebz}o_QpFowUOe z85w9qjusKR*Cg|fVt#w)5uOT6gc1dhzfnbMo5ipfJE^-2;5+GoO^T=w+fxZ>AOueu zt;R&Vj#(@NMuP<7;L_SV$6eq>e>R+z-xG$a;O;g40r6dB*7mb+uoXzHFH2J)RrR?J zz~yR1lgH(l^6XAu(-M=Np2rttUh{1q{!*ZgU0AiMQ^_Rq%xlFMMWi(BpJOhD% z5A_1?ZNh>iE(aPj_pio_^VTWhi(;b$@US-+{x|M4j7k)6J@jt~C@4gMo(Vqz0f%cV zUZf|D%rh|{gqB{T<`?8F{gquFrokMi98xO}AFRRpalmk(F~6S_`+6f(iM@6c`X9{V74fqN$-Vx<)Lt6_&~X^>sHS{K-U42NH*s6M^n4?uP{b| z%yxha*?#idwQ5u-;q57ATLmmm$P?TcCDnEtkypG-JbnsML5t#>OISk4*u+9#kIq%+pgyNyFMW6%Q16vC3mtE zF^m!W={J+|6cDc!Xp{7Gz-CDy^cY*HHHY>z*oswGsBfi%B)OS2u4BRq2hT5gfZ}h{xByaJDy{DRhB~b}nl@y*$R9y#5n0jZ7Kjyx5 zzG`qx(Hstf?#>mw3b-VK!l^BG*uueiV`S!oi~EH$Vt5yM$mTbdoU$NHt+z1i2eQ6- zjsxvfgTPkMfTXW#UG4834RVQiLEf;1W(%G=N*sPi);)I1VofU83lv@KpDBKxd^{35 z2aaj1Zm2(0iYfO2sR%(Na2UdUsebQ#uP@d`mo_54`I~}?e)20K$I*wrw_vf?XKIuF z?-Y=&n)%L(`b2a*rjbC`@QslvqD1-Zw8wtK<}LFH=raUQG!EkEzpbzN?N}i4p1;&Yy;xcsTuw7bZWwR{i&D)7G4eqt_k@d{Zhg} zdiO;x0ILkFT8`BkUwTHx{=ohjdZThCF4ES73MIAuQ*?!p022$CK0=KwP;X5|rj|z5 zV4iI<`a+z?*78<8VtFIm!1(xRr4^H``QmW?`AS<-xpgeH5@8KiIeSL%w61JkxTc$8 zZM*2LAuVnm(;3+dqJG4K)su00^mAxngj{$E~_pS3)E?k+1 zup562lZxn~)DVr*>c750Z zB}%JM1pMM>!<%cVDQO7Cp%;WI2|DpDdwFP^ks${LBL*B@vq7p=ZBk+O4B`` zP6pPdAeBQye+J)KU(YB{2GQGZyek?TECUmvNq~|;@UwJM$a$&W?KR$Xaqhcu-=OH# zhgKdz3}}j^Fxj>VGog*#gyUj|;HWQ(Fc@%tXv)Awga{bUl+FK{So$4^F$fRn^P=^a z3@Qs>j@VGFcrN72Vtid;Duf9s(W+E~ap}IR#uf8MC%i45Ee_yJYf717`!|{aLwt+g zL$@t)8rRRjDIVtAEcB`PVaS)kY|_%zyu}%?D%1jjR{wgSSI@!XjT6@unrUT+tObpP zy5=}c3R(@$LBcm{l^pa(ulWv#o|=;Tr|m#boLc3r?*-tICVn_#Iu%ZrJQgNY7Dluf z2mAIqaa*3JGx6r>OmGr;V*I>oS4Si-S4R9$!Z*c}j;}oL?r3Dc8z<54Oyr6WWHf}a zwcKvLco)h;a*4!{8mY}nO+?KLQ}7J)0Z2eL2RZWPuHV0O8~`O5iMS~V z4nPRNof=4IWYN97-B0$LH}D4F9mENbSLWYXA2x+0>(3=he3*|V(?R3)%3qudLS@5} z)ovoY6lk!1yTe|jS*+XqyiYngOO;~i97R`-P$1acDCCDn7+dRWBr!nz4;A734bB?d z7sByzunfg)I;ewFI*pB_REX7s9~ws$i1$2l(tH8H19PI-$2iESbLA`&r-kAYIb0%{`9jbGNvO9rb3g@Tm?}L6*L~OlCzf#$ABLb z91K?T-Atc9*3N&I+1DTHs|$8?eF+-K?mr@5b1nY&nG1Q7lFyjE)d`PPuPNdgMtuB^K zO}BKuR;jef_5co885YkVw2d1E_RI-9DpS$7xZLCA!5Q~YQbl0i5B33GWa2=OyA-*0 z-6w`1wX%vvFg@E2^ewd$Cux~zrY}Lk3&o1x)!8l-Vv=@7)c7gz(;s}CyhP6<$XYb# z=w(oNqQ#MZP(=Dh_g^77enBj29s*_rpVUX69&ig3$oemKTCHkBKnd{det^B=Y7nm& zo}Tap(!LFG((#^6pv$wT2fS1i0w7Y5_=2&mrTJQ-`|aMXen904+)1AMDRu2xgU6bJ z_Z_zZu7DBmS_F3e#=x>&lXG`XHeBSRczZf%R;vvnft^R@r(bYz+^~RWN5PkH$ooR% zgiUT{Pr&JhoHm~R)Lj^7W2g$>q;RQHM& zXcld;nn=S|z}}MvLC?N1^XNskKkZlz1y$7~^Uos7Iht++;e5n=oRpT;rz`#!E#G1hHY`A8KB@z+-uUKrr1CT8yef1pCOx;L_SPQnUeP#j%!gGy9^;XgA36lTap!mtou%W8{mQY%Pr ze#ZbSVtDiP$$ll+it~9%yxH3XY&E?AG@sBTN_5?@@m1b`eTi7dBuoW^=$r0IwG5F} zLD-aB5M7Ge&8t*s;^xBqKLi%&gr>`bGmI`@3+^elAOzM=35 z9GPdxmNfp0Bt@>im^6wQ@atg-%KtIP8ex3DbXmk?i5;Jgs5y(kFc4N331;QseY4E?8Ms&a`DTUa1tx{nrlz#@?U?;=x)Gg+)KH*oRH#0bZGu zdJLlB>?8f2*bbu7jay}r^x+^}TukU?&ujlz zKH;lgkGC1xQt%bpfxc!a?=M0TXCfvxPzSuKX5hqHvEX7;A z#(eP3XL5pz#jv-pFD(FNfhoLnudZD{6Am7!KY}3MXd9QI%VH4f7lcJ}wr$WT5%PUU zz%tZ4t?_uFzQXzOFm18Yjy{DMnvV#|EImIh;)tq@be1``u1AQHg0CHx1! zhhUuX*6Jo#s8+H0pzL6v)lOe+5{!T89yql;Pyz+$c6C7WY0_GW4MDVgE?bzSzQ}%4 zusIE1M&f{4)b$4Np&=xzPBBQ;nLTWchz%koi#tBd5eg}njKMnSp9rJsSsO%sb^^Tp z^gW|W4g+Qyz*_pbU*uc4+IAt|sY?CsB;}=cr`Ny}tP8Nzr8kwCsd6O|LC%189{jer z$lrILrl6;+Cj=Rl{9Bqi3pGA0-m(;-z-`|wfHnjODU;Z9O#MKRrf`GJ4?ZI&s+I|1 z&LjSBfZCWeJlOD&0as;j15_4}qpVPGu3<*~R}(0Dq|=&(8TUs_`C@9tTvj(Jnj!FH z{W4r=b;_?9hr=O+x1*#rbM&yl@x8272*f;Z@6 z|3gS(je9#n&*@26B)R98#^I#*(2i-&-z4Y|sqkOqbrd>kM7~ovZpFvj-*i7l`PgM~ zd~Dyp42}eQz=zgmeZWNdAWNkczNlV9as0N%_O%vF#Sr$-em$Leyb_i{^s0E?AI78c zLgcx+t?sZ3^_CsyL54Cg7kz2f#y|bMz8|t+>0j0c;c%`pp(ojX?W57ld$-;z<^9(B zj>URgdHN;d2EV57EbxZQ()%gfCnIwz5}bf0w{p`PWcib%fdn*PnD>Br zMo5PBZ(yhh>3a7RGecjQ06##SRHLU3rWaCsN% z`}k0Z1L*whjpdi*iZyE(`(%M2!K0&yip&ny)RYvUTT@ds5m<`30r#e*# zbqDoFZaC;ZrQ}IhJP3^!dtet8HZ-BH#a-^(nMh_;MnP*vk%i$4xY`iuclp2MUSg32 z8V{_r=SeGc=1O^@1b2KKk-S#>Y-YTKzjZ=7+HV;@9tOXtMx~=-7tMBVv-_1S4LTVz zlf%rjO0Q2kd<*d+Gv`5@tR}oKzB*a27#x)!kbWQ`CVqxWDl80AIwD!@RevTv)56V0 z1;J?aAm5(Wm-`_U2qj?&_t!}P!L+L{*g^t|1n&!v+~*U4fGV@!8VYo2GYj>(WL*LF zZCBVlaziA0qVz3+IoPoUE5(=+fv=2W*YMvMK-1Mh_9>b#W5hVB{f=F4db|o1^KBx} z-f$YD?Qxpt&yQkt?MDy^$T!I&X#*2Jl>&yYi-jvPrI|mJIx8)nVW98_Lo3Dvf5ZS# zi@ku=&mLA+mq}xaknVd%!x$=yfiRGPbg}89ZUKc!Fh2-5d_P!?DZL4x=vT5HOvo`n zS_M52!6DQFq*{c&Cx!x~dc@jA2X3H)KyoAeH(KL>292?Qun}PIGi-F%rH4EdNqA?! z$EDli9L|)%AEck?;qDQ|U_xk%Ja2E(7a#Bme8XO9v)lEQ70?u^68@=h+=}E*V`WWW z^k!oe@^1MOT`yY~ORfR;wL(FD6Z>GU3AVO9CvF(FNU@oN4l76NHePXj6K`+`KB;EA z1B8btUwMV%r|i$#DJwO)TAdSeyE4@>#wN4%vc! z>R>cq!OrH^>he-{O$y-k4o)8mJ5%J$bCg7k;6u6iMrZXLnE@^650!F%atS43!5)}n zufJ$cpI3TLassOCZ;iFvoC$#dx1;LpEhDz|8^~Mo8TzeP$4UFuh`z|4$%ID-Qb#DW&UibLpM>+1xS)PakT zbb6h9UCK;AwYedkucoisp6mITCs0Mq!!dk#o5X2N^CPyxav~D8g3`vf0SaCBVo4F` z;JQP!+_0cp8uv7=LdrOZJABpdB}@(~ z+CHD=;NvDhO*{XC!Ty>O2vg>4k7F&XXZV+aKSmZ7N0r)dPW7r!Ul;X$yy0^avf;;8 zqfOSn2tB!8dx2qPN}~KfFMz=`SiLrP&he~*%py)0^@m81y6>9<73pg8*Hk5TJ-pgdwYg~7a(f}@()*3v z-bB4t7wl^Ig40C~m*SXZD!jlGzWA zqxa#KnlYVO073!37p@MSU5#w)4q8eI;2K82=h3iLRNT7-TMqU@h35&3rjH)c^LTo@ zKDQ4`&B4f;Qs7{$G;6uhYvDL&j;a@=-`#EP%QvV?#4!F>UqI0~iU(aOk?!Jr4a(|> z%FiY}%d+R7=Iwc*fftEtR;pgdC~CXS;(E=lAN?5zO$w1Mc(p&&tw8{4)0bN8-G!;c z7OhHm?5~hTFEl(XezzOw^HvFacDW^2Ehq(Cf7r-|$7d$C)Ux!tbt}*H>0E`?Otj zQQl`JQt*#zObqj~<9IaYIkBZ%ZW0FsGR}Fnl%cr(Ab?&%Xs?a%oyW_Q^%PexO1Ul& zi`Dr{iR#Nf+u_HDVVqgpWj++s6>He^R_5c7YWTZs8qQ;gP-jAU|BHsV))nI8sv>z% zSQg*n!a{-DBH>ka*Svr?kk?{&{qR!6kt=W4sy)b!Y!2yAI&j3k(>8kzZuOuxkG==2 zV*yXnbL5VlCkO{PEqoWwww0n?9_4NwUUjWrm}jxMFVQsWZ((JUU~K! z!J2L@uFE2pCRJ*ZRI?|qSGx@3O&8rRZ><-ytHWoD@uAZBIfZP|-qolLg(~y00uq51 zuK-Gi5?B%uq@p6lfq|UP1{!<$Dq+vXX19f0e(Ekdj%C2Gh#Gc#Oz-4r2!f)6161kG zWy9ZV_Y*y-4?c3Ilxn$?N(uKm;?BdG1!P zuNZ7`B(QO$d8fnj4%CJmfNbs)ZIQ3UIwmwt{zCy*8NU90z>&FoUS&wx0!YdNxAwmm z%~XxC0mxQzcBoV)duIE4jn1|sR^pyH*A4SG;wag!pm$3JCy)eEMG)1^Mer72p&=cHy#$s+NfC8%l z2%ML%M97fwIa9Oo35qSpGmRsEN*Bs!jpZDD9IIh-|7eZQXAVRfFBlszzEuO<)3-*F(R-XBZW~L1by=L9GaK38h2?)ya ziIc*nu2is)yP~y^|3j@>ObTJo2mDQ|rYZMQga>fM3dUyw5j zuCzs>=Td}A|7H@-#<}XGI-ev_)naOMM8fSJdHS#qJe-Q(;c;j#$?ZoX1^x{ZA&qFNZoMlv zSe-C(&9gegP%3U*o7sy@6gTPsB3U=kCshGD@fq#7>bx6S0W(Oy7D@*cwX;~o9OE(; z8(-Gy4Fr<;jUD|LnRTBzWQSZeE=Q%9)pd+1hV8s+MfF`mtYM6W4MZR^Jr4X6o zRF%g-Ue9u&1&L_6{Vfc~gMfZEIdQ$!s*nqZ&hfqA^^Ti11la(s}IO|Z1^ zAkVJJn0{6xPjJl9>o`yUOTpcqakovOz_1XK(cB$OFgeayqw}Awv?sdZH z+o4L=$EY?JU)MI1s#4D+p&bcZe1Az@4Pu^&PKnHKdfFwiy@EdE|LcvkLEpTBOt6Pc zj~k1O+8tm#xA+Vctfo#G8*kY254S+C0PT+a?7l2wdxI<$2hn<3{QT<$a7J;5zkYQy zaCsU2t#CYBV>$t^)%7owuy)=ig*%Se*~zp=U?{&VP0KJisK@hE%2U|RCH%P3S5HkH<3r%;lGfWbrs1$JBB;lCjUfL@ko?soA3SXo!6)?HTz6j>)HSk?=x?OI z;l=Ey`8)&1cMY(;Je+MUetz(NtH$?Rw<<3m0ueBfEg&Q9S9vS-h>%B2e!7)` z9{0an8SJNDpgK%VG~!2mP$=MK8F&2@AA75A z5fd0wd+qg06QQDF-$YW#neActUyd(F^~-nKQyp}U6Gr3-=PHo4PY|LXz=LoxLLj^G zaoQZPdu!Kmx_w~*5xkPSYr3i_2wR@2-zZhSb?*sWC&Y9?t&0sj|GRZ&61jmjVKt&V zMRvG7lyiTn{D|()+6NB0dr6m|4p%{zkvN z(dBjX4~%LN<_)w=VRg`4x(%qfzqehV(~pwBlW=Ahjtygu{+6aH`^J^UuKL36Nqb}vF z1MIK;k#>x_qJKhE!tm)sq_AG0|Az?S4w1>+<}}+~UEVC#F9>6zCVl3sbBF59?Y7oV zta4_$eA7lxit=tMW3Hy*MVDV)nklAYmUincA(k~8;%yn{Y&zluiS?^VlqK^6^=)Se zgmMmbfqojDoI}t%pXqZ@?*g#vFW*2r+fUmcVrrQOgX1G}Qkf4%w-)lJe>i{tOve%= z9z`ePcfY^E+zg_SoHC_WDJT7*TKVQOR)VIp{VLS!nB{JX9aO_d0x)mz9Bv4>CCl9M zrT);>!)0?CeJl6AyXzKexA6V)sUbd18~AFH_&?6Vf|F-r_t3YUx9ujy<^;!WNFeML$`mLRemv-8(39 z+EaE=h0?HoE;t!ghMH;PtQXK(U6#_%6o27W|cb#6!BQgxy%tHz7s zcO||P^Vq%!fV_DD9X}+WgjJQe5V-3DW`8%w>oe&-&#cQ`M_Ub~WKaQcp&^`!1=kIt zWcpIRY5H(B72%l%xnwlL?E{ZBBPR_TO)oT33S4NrOy&IyFqkNQykuKb5inltZbi+c z&N`RLkKd{M0i1BAig`s<^KBTl+Y0XxC$g=mSa|FDrF{`qM7&`uiYvK}VZKsrNr0_|O;g^hLPQ-yUZ69wS_qL$lRPa)RUR(=6Mc1|Ry5WZ*qL*nNK^GE@ zi8s$<&2g*A{!WLG#kcZrU^~F2k!*}n!K^+fP_a7QV`mC2^{!!*A5wkzxvH_A4&NU? zNR1*H{I5p6ht#MI{;Ie=$c;*XEd_c_O~L-N{pEJI0EEFiq0A=N-Qk^?DNfI*Y2R5?Hm8sGvfV+Hq-(0GHTIJ`k4_8E9ch3dd8%)P)DOy^{Hm<9>foCK;W4b^H@WUhp zH?iqL<7>%;lHA0$$C3nt!V9_t#y~2Fy({%Yhip14hhm;=2u7FJOE^pskd2J#5%v-+ ziGXu%$i=s%3f-2DdT(pMFGgcE9t}Q?E5R>8Ka1bK<|BRvjB|T6pSJ@?lZ_F4FF@tS zhee|l1S#0^-wiN*5iM@+{w!TE&Cu(GN_7o30yQYC6*`$vK7E&$hC-e(0jifDYCH7I zyw%_~X!<{bt)+UiX2Wka#t_aPxkwn0orLu;%PQc>mbyxS$5WZh<{Ci;DHzHG0e77}ZGUmYt)@h@u@4kYm zH}UslYZ8mg)PZz5rYF6niEI`a9l&|;-5?02$S#3h%iN2N5;aEspY2*A_a*TRT}wRm z(tX)p^}tGZslID&9$$z7Me8~9ZH3j7U?k(ec@q8IfByM|Diy?iU zgEqayKMq9&kE^MGxf)~HyQmq0<}8zy&SHl?LCQRwoGhlF*ZW{dgAZw$9;m*8io~)( zco^2<%hJuPf6lBzwp$5Dpt;rq(?AC)zK3bJ{P%j6a~>bw#9oV>^>GOG!04x0Kt~v> zK`^o!mj*7`u@7ODP9D2Y!+LERYbnWMBT2mHmWsqfpSBrzl{M%Puu;|1@D@KK4>Ad!7aeH(`d zDVpZeo4GY^8DBG*iOeO2mNV0H0Ow$R^CSl@u7vYI80o8NaonCV4H=q4*nZHa+ru`x zVU)^WV4It%(Q4jca~NuTo9c3!qhF(HI+{jhD%yWmUK3RTHT@M0B^;Xl=( zea5i_pA?=YjK)9aqa)-xxLE~EeMrw^^3?O#X3R-CfUhbJ522s|^)hSYuS2c>J3zFW z4wC-2%~LHi%<@3_cz58lag^@|e!>wKqzBsmK^Q@}h@FPX=ju$v-!_h_XR{u|ZGkjy z7Tu_`c^#k##*D)y7)Ex|X9At$iiwqb!W9uj%NqcnHQ3$s6ms*x4n{f;!Rfg9vaJ97GgRge@Rx z_jO2om5l-BJt3vIXc~K64xhupt4nvUs$4?7UlTKXO@FXlFtqtDx&1k532!_{q$Q( z-v;`&B}TB!4M*-8C+O7{+DV4K4@dF38Sc| z2PVzWs<*jGyH1*bkU?m+dA-y;<#=8X;n4GJB=%YmL0qbJrgB*$$pz6b!*xZfj z*Od7dTY{>45M+ACr`^a0Oc%KPv(J;y{Oh|h`zuWO@P=QUHmPya*q670tH$LU;3 z!nd!*@ZTKYQ|%#f`GWUQolhBf2{7krA(brHgFX;cum05^n*YEOCIl=AJ%J^=fSK^Q z&|H_3?Y*O_M%rZy9?Qd~?smUO^B3Ust!O|R66vzns3w8$g@ltU^x6V9(|m)_$bgcz z35I;T$wz9>bhhO=XN8_&xhg%;JC9z2FB}`P-y;>jxxPg+o~_X71DEKDQsB2Zahu2w zvM2*CK6&ThaDq*X#2>No$!Iu>APnwMai#B+V)y(u6c5|#D9aEp>j4SD+=khQKyqOM zsc3rcfhtj(O87l~p5Twh{OyqtNc%jil$j6nUgXtA-T7r>PRHU^Od=M! zftd@O71(4E!GbxDIn;^hCX6U=I4IbnHAmms@w3~Fo=X4|fnju!VP5CGYU=!f2td*9 z+Ym$E? zn@|7`YBDq(FBoXUIt)f_XAC-xSDO;)%RjfxR`qnMB(AMaHEo1NK50pw6YkBI2!KVE z@v=7kf`z5Z7JV%bxH;;0asJNeL84X)M^mcDLtzBM$Ubj_3`9rP*>Ey5IS7Fy$=ZR*^YD0t; zFj=`nEd+zLUlh}FqGyH_hp_CM>*jZ>62x19SW)eZnR*TB<z+HrM*A#ZC5KtFG}YR2MK5`?z=?Pwyj}PP%2eqhvop#M;8w>WA9s za!+IXuevs61RhUhZ1m;%?ZwgO5Lo;?vs&^A7JvH+fyH0@A+WfyxQ^^CnTtKgrgiy* z{Sba>`OR#Jm-bVS4!PE2T;K4xKVzeOWDs3dB+HX;1`J#DUw-29d^bpfz%M1Y9IK-9 zZ;Fs(t(7Kn1oSC<9>lKW^v0OK5Wq~7#g4)CKvXjQ@gInR_yS+)Yy4u-rkbNTZK%TV zzb<|I{Vuu0mNZ+oPV?eqwnP8;jcGSc6lo1xl?+K#;V z+-u`6#EVk=G<)Y`g|4}n2x%#PovF{Z)w^$RBGqHQHT?`HPSh||6E_8xn)gnCIeY{5 zuE?alavAKZ*SCC7(e^ZhCH=S1g6 z)3{(e|F%Lkc9)>x(}?k>!E_}B6MCC-chgdE_DI1#Vc^SVQW|fqQgv;8qk;{su&{2~ z`PBf)db4_2`X#N|&k3%yceV&Vx0fA_lT=~7SZAAyJE^N-Gjlvls8(mj5_$6#<|9ow zZ;ifZkLnuIVbpX$Mv2q{JOL#}L1FcpBM(LE9N zRRCw#Pnd~PPA1;dWb~GbSRA@xmjoM z1+{XmuSA@#Ti)*-Ky_IcqjJGymaD<85Is>e8tKgzn_1%dPP1w8{g3Jee~Ia}R3#W$ z)|zW;mCTzrJ|P|Cy(Ppm6SHfOd+>vjl@`V2^zcAy z-P%PY&)=I=ctST#)vnLo)N&qz7mAB<`EHT0_zDz_fcBv3Q(H8#qyxIiF!oh5O+`E~ z==Zl!Lr6Cm*{2%O{&-55DTJ=_17n8wz2UMRZA`)J$gNv&1UEs^j!-}ob5!vn@he6eyxQnj%daOFT@i_dT- z)!i35>ll<0j~qK!&i(fw#2{qL<4l`RV|FR^cD5ubDNFQDIbLR~Q0q3OPRX=L64}(w zNo;EkS~|G$;9Z9`>Nm7%AW zFEVzYkMZgor4&S-;NKN-0XX0A_?{L!Q7nbV+DGppAL)A=4UVU0hZBOwJTd_Rnn-ul z@*s3Xd|4auS4zow%jv5Uw#Vst6MD!k1F6u=T8&>wZZ3A9-@#Z*Wjp*@+sH3;tm|J% zReWi`y0;Zld{c({dENmBBwJ#o>`~;&Q$IYH^ge(X&wPgrf)C7- z?4;0Ykh$UrU2M!lte7b zLUB|VYw8u(QeK=V%XgEhbj5G3(qMl}3HO`*&1V+t_quIz+D!~c;~vhBNJb%>uy#kk zw2|Yw10uYooHA0l)bK#?cfKr6*~}ZM$6w!CUcwDIToP)j4&w$un(+XkQk>NuzgyF{ z)RB5qm9G4;3_sj5;4=St26b42ZjC0zc6*)AHh--fwY`ua(BzZFgoyL~!v>WkN1Fc? z5kgPee|t==en%GpsZU*PK4RG#`$&|Wit5=XIsVB5xm{r08p&m5S7aK6kj&{JN^;kO zB?3bRw-f_%HhaIxfn>HG8_eocy(EzKKx(}m>wIuzt*8R%9VfzVRq__)GiZV%3sIjL z(v)Wb;T9++BC8EZf7Sv0crP-EaG)k>3YByQm6!I|;;D&D4&JEgbRWS$6i-GF9)s2} z=$FNQB-dI7JP5rIF!qE=|56gm=xc8RB}X9PQ|xka;dPOC`3 zQGRPSj&&ap#uBAft+vk{jS;zoDYcmZCQJNqa=MKcc6PS=`3|O&!nlIo4~S#!FD)iV z_SF>Vt+A%JF@E?8CW{9RYySuwPzJ0v+8Snf4c*gjC zs%P8_XRp2XTyxDe7ljaZNAi)dbv$paI@kg9#d1O+M4QOY_WbCD(wdA;gNuWC&h*{S zP0Ir&K;%ukEwdVhsL{-vaYwpIykw|45vT`Z_S++{xDFKK+xVb&$d_`Meb>^4cQ$>K z;D3P8EhtrSJ5eU#$TLS3lEnG-`}9|>v_X}#G>?Rd0#d;b3;CrUFAa;iqOO`Kut|l) zd;VA2E< zBIV$!JLfHm>`lIX#g1)PwmTXY*Ack;)HHli@(5SQ^c+qq8q9v) z-`|X7vO!!2Vq4|~v-L}|wtAHLPFz*KgKbM-beMq0{1c^83C$mlYLj4ur^2W|Uko~0 z-u#q1HZd_Dg)>aQl#GxjY`WZ?f4%O}&Y6d-IWT7weW5MMZuYU!B*gsFlxdKYy8{gY zx6=#Mt(U&|dt>#)lxR{%#hdm0#S;xjIw#D37K>=GrYiW@*9YEpd_gU?`2Z#IKGYV* z|D93!%lvT<1F{F{vtgC}n5|z5hQAeg^T4=%n~6nKxCtPEajDI@Q9((Xb{x+N!(4Y7j++@ zm#7euIy9}nSH1VL2XKGC30kCDf$*8my8Rp4C&e#cnMCS|KZvkgezc1CHfNmt_1;Q3^WqWSZOAC*Y!H<8J=)kwCL@A1m?aT4)@ zD(uDQxfV+hKs2aOm10sM$r+JEIOBwnyC}8>-i^_hOV*MXY+#gZIQ&T z)(=2gO|U(e5n1dVx|>ea5ZoS+;U`z6E~gP*;W_n#K09=Q#L=!>+4h5{QCBPl&iCsh zOa4J1>D75SXZFV@(S3(!zKl>Xm#J6l`1P)agg$Ou9*m*SY zrAxLVfIm^b4u4gm+sLVN(EArHH3PJyx`Ci5bKhCtg7m&IkU_t#;HkfdP9S_f?)Z8{ zKtK=0COG_1ZLH%=T={a!RmC+o)4XJTA-b_~_O(OPRxtJ~fh`=S50DDX*r#6(iso;4KpnJl~YlH-z}$y8q39V(k!k!DmqdRv;~=F2PGHF z#mUJ>&Dxx1f|u{z>pyFP5>bGR5H4o+0_V>lsG|o$4TnG#a7u#7Vgds;Ddol*6`$89 zH#k;-wZp6pgU^s-DIh+-so*s`m6BeSto9cc(v>PH^~YZ}=wPJr`$Wj-n2okuru@Rk z+@3voL#=k6ql4E+t(i}4@JYFt!h!g@lBsYh^VMraoFH1Aj=mXT7llAb7`NB(W_Pjp zmu6=Z?(vz9H&@|gU*V*l#M>X+(E5gwnZpmgzx0KnFi}ctHOa_Hu{zz8=SNW#_I^dN zZKq7C%=eD@toZ@RQG7K!-h?Io@EuU4$d=X+8&Bj>!xm6uuZxf_mG^A8Q=Hu>FoY>x zo?u8kaG7!1rgsj$ybBpAl-2=gOgd_x%_<(2_8IBBMA+b8=RzWnV;DX+GFoUfKW+(i z+V;9-Qsk;Fu5&$hx`5Ck*6 zjKz!YS#QVAcMr>A?a{p9zB#Nd5Id};G}qw_B`{%;UKSyH3u7|+o~~YqFk;s(9cKqs z5Cy+yquxBH@|R6}+%iM2wjp}eS|Sw`8#gdu`O$?Is7 zvPl}?B2@RH$@4Z(C97g;$BPuK1&M5zKH;`)MvC&g(~Y2g^NC#PvIu=z(-pX?p1`X_ zjG>f2gy>bM3I0SMGjvG_#pQNc?`A~tE!+=z&}_ASvO;1Qnp%<{a*DMle8o+`;gEJr=i z8w_@)Dy+@=D(u%aG+1^>vyIU|r*^D*Th!sp;~I4?G~nJn_jE3_^QE&pwRi$|a)qC? z6?zmf+%Fd!AATru8RXrj-_Jzq@gM!zlk|C2;%(cSc`;qOmvHB{gO5mxX!4`BEW-)U zT&|5&>U(=`S}d(|u0^qi^QBOCiksq@hH-;=#jSEn*O71fY^=7o1sZg@3yP@E7gKf) ze8YoW9b~m~mb)|qQpbf#3nn+CFMjzZo@B2|*6mVxFDv68DRf93@x~i)3?5Gs1{dSr zQoSr0|NM}fO)z!I=hRo`P!pPp;4 z+V>8Qr{l$@mC%i*Vo25AvDIATW?^MBLxE6=_EqI9=Kac|bJ zt$m#I%Fmnqov%qc^NuHF##eH~V%nE&`zsFR6&MG~C&?9ny8({=ZEnie z@u4Q9-6)}+{^UZ&-7Q1HDEWw37x1Dm#yI_lc9hJ-!FX45w(@z!=)IT=rt-Um^R@6Z z)|a;F5Wa zOeRXEta2#lywXWRHox%F<{$>Ncil4U1mfxu5_GRwA2e6@cOvPQw^D-Z!-W!wH|0Ia zwge!fi>wFXI6h7iiV{dRQR8TA&dWNzvqe2gH9t(AE)Q_l2V-$9HCq(VqC_)Axq)zp zzK5X(1(9IhPc>ScLTsUG^@?fOvWGmx&m}2b$~UfpioVQUwsQe8_tq-|{{uiVNW9ED z=}!Q~SPePVfq0I5%_5knEGJdSmH^GftuH-MJ3O$8@))k za&as+;y2_dYF^(czUXYFeXUlJ?gUd^U@`K;B2JX}pQ$6>(D|+-WI|6RD$Fs=5DWgh zt(A%VGV*@U)2gM|joR-U$(+Bx>vU*CvBWAWH#riUY!|cZAQmp`19>ZCiaZ%Dqz{Jx z9c{U*?wKDHOuR$FpojkYoYFr4j_6T2rtJD-Vk%=He7Z^bPMt+*$F2{jQ1PisbqX5D zGv&(@bPN?PMe=_ZQL#4`8m*+{rP| z$+y^aS1eAV>0v}5^$G4z$r`kVU1(XR1JRm>=gu~0J*aC<4Gy-X)j2UU?{8}c58uVk zTwxy;M7c$=x#4sj zDWHZO1mKxWJW<}6e1q1NP(GKdjbP;hx5w{9Wu}a z71>JrGhU93ZY|M%!D$jtB>Cenc5sC~$7_0vQL-3AhRLmtX%;*3o*TL2m1FxSSj`r< z2Mn;JrZXdFDY-HbdEre9k~uNn#oCq}V=K`a?~8O#DeQaK*QlS#B^?p3j7H5oelzKq1BBB8Xctz!>Nbjf8&>cnS% z8ZN`lB0;w1+z~bi#-!V*+L@^G8XN?%-5o}-PDZp-OF73Y(>}KM%Pn{AO+`W#sBLz@ zq7m(`D6z|#Sa#sEBcEINC+DrbXno;r~f~x zS24@>VP$rrsOtjmOYcB~_N{qz|1~UXFH&nHuOO^qfvXULEy0yPUDU zz4lQ%4#AT6x@nZOKtelai}@TC8sfHpLp=Sp)4PEP%4a<;m5}3;fZ&WR%k4(txmonS zxMww%3#9`QYh`P{JHsn(uj*V?zN~;MmffZA8nWpcRU$u8+P`n(;r+N}!5Jl#C_gfG zmd4%dH0X7=gqtR-3QJ77;Gq5#x3!%X$IN3d6vn-jC;_>>M)tARr(G8!i#z<8ct$Lh z?Etg7&7g{fqp{;6b@yrEMBr`^24isAN&@tRfs%XC2^M0%&sF?{Yi+nxt+!Eo0dkHB zmtF;av+GSDrTL@mg-~-u^LC9+M03yPZ{3Re#G(HwDVHac$hO%X!`VIPeMa?$!_dcV zc?m&m=XLOa)>LrToAG9j8fve5wy$}{mj1D@%q;ty-se~hd)dcFEl-|q+QCR9*cV}|8Yp(n+)9tp=W#`{= zQR1T>hsT3gfbEysa}p#&r#&lHGvKINa__&lSF^HS?OXTpf6de4rZScwPv`4>&RuM- z6E9I%)UG=HJXv22viRVyS0$VM+ijQoqgF>+EpALKDC8f@$eiZ-E)G^b7uM zZl>eIU%iJD-wDNPmX@yd_T$I&G-SsBj9fn=z@u<}C zf(6Ms`Oz&kc6`2X>odK0FVX3}6h8j@hiGBGMD^5*T1 z=0eJ$r(#*0c4YGIqeT5lCN528u=CO(Uar=Hvay*98uCm+)q&`-$)uZ4-^B4;Fi*5j zt*xCs=iqD|efKi8qxoDR_hgAmnhH++iv8oH-bH}K9KQ^_CY}><6&^>xHN|X=S!P8D29Jj;>e%Csm~(rJ$xOPhjPxwLW|6L>7ZXb3m&aN2yKsu2$F9O? zy)-v36UT>5uG#Z=ss-NbOElYZ?`CDjv^ZNE_S{g`<1Qwb&5`YFo-Ew~>5QH5fIQNm39m0klm zi}ltl7(9 zk5mc(n!V1mE!phS$2L@Y!Yy_pGD*b)E&wQO=Qa_HG?$cL3>RJfQ@J;;ftO(ZyPt|0 zay3oNw)iou;rzRI9Lq5}jy<8vC%rPdiwdg)N?YS8{!2&Gxe}E|#ilOIiy#7#at50| z0sYInsHmKm4(COcok~N<+HD0}$~YIcPbU)c<{VPDZdWDhiHf(yEVT(IcUf|fLA~dH z1NVsVSbP+t>cI(-+x4CriPI@PR++UqZ|+xyLEl_H(^o9V>m{*2)j$6)bvYM!EuZiR7gL^e2 zGtHgQ7L7^>n0>m{$NYUb-%PTU7=+4_Ts%H=!fGml=RX|*`To8U^!o9E-+QL9NqaGo zp{td$ogK5>#NYROP{o{g2Tq52tO6ey;T(&mgNh{arA8|Rp|fw3 z10j@31qw)OZVAQJU}GX|rJ3^BaV!db`O`;ZkckVvCKxE!*&+)ur5OSR_Ci_^X&8!P zsL+(kyFTUW$J}$;){l0A3#w6qA?40|r6%t@iD-QBTRRN9@OjnSB4|QBzpm=r6;~)m z{RfoB2>);v_d(+vD7`@{OD$hk2#$%KOr8&3A5dtCemj^@z$me(eW7M*8Bn&Y;F<*< zzQJJW_;gf%bp)Lb9|6tYIirDk1i3sDsR4+22J^y2vfk#g$F12?-JAN$UCZ&N_^m+l z&JjaB04!;pSJA2o6Z&9>4V_AR`{%gcVags1)sB=>iySs8H@o>|_m8kQNN8v8 z(0Y>sMzQ%AKnz0zAq0_}4})6Oh_B#rcyf8~9Dyr3J^DB4CmRa!Ulb)+r=RtFb?$pU z0&8fUZzWMQ8jMhApGwT6#4c9u7Wv*J8B}&jV2dLfsxX4WT}{Yv2j%&2pq!C%1wEjV z@~HyfeN5k{HQFr!&U!x;X$vu9D3y@MHa{swWGl_85lrVK+~4(#Z?`<3?dEa1`LxXk zL-O-oJa=S>XGUE(g!hhlH}b5I1k`q>H|%51Lv#gYYs(}PQ7=n~T{r%66WY()!dge| zL`oe8bnJAwRHON;Mi0jtUy2?(U!HO?-Z?e1*P3BPrYiY~o3Q7K^uJ8#tKES;NUS^i zSYPrPaepR@!&9?tkH_q7e6?8t@kW8lTZKzjP0CF z7P#e$R4|%ezkR`FCoST3&+bONIY{f~JZ93i7ZspKr~6&&?&I$0++_-H-?*{e&7eW7 zlSI}nSoOi@!7ubJtQgp)&~eU6>~w1$n}8^{93OtXhO`?&YbA@+Q7byI-Xt=GI16VC zg!-7%JMqSt#`@|#E{qaQcT_9kAQa?+>`MOM$2wlaDzO4r0$s1mQ;AMfQPss7 z%E&z^($|7yFA0*pWmD5f@NXvyx@`q)6^KRHdINzZzOMrrUb0_u@&6VDB7Z_ckxb!}244XlA;)x* zx3xMjn(VgFA6hll?lj65OHY^ocjo?d`-Js{hj#+$Fc%_)#gY)URxW1RU z9^<`hl)WDycD84|GaiwpQK6p$(RxL`b{3jHy)0Q5`xR>G`1F{R%@yt)t|VvJy;g`9 zBJ%s!zE{Qa#7GDo3e^~rCcE_?xl&cASv42hPfi&3o6}BPOfXT=-lgFN=kitH%`4(> z<`cEmFW|QV_y5*>Blk@1h|abqYe%iy*>4u^ORcFN5ea|3RWxT0y`e(PZK}!FcsF3W zsWgU*OjLUJ8`uQ9OBXQ~fBlpxPFy{bskrH{4=pROR4rnmmGLqP6nN<9{olaDjSA}v zvyyRFW7}@cAs-tw7AJH-EHYG#`K1emeRBE4P=a+F@uP7omdzzq?tsS%85V`ao|RWg zkfPX)S&>EwjVyHcC^7q=CryGBQ? z?j+lJB9L@6Tqw-VbPy^EYIG5qCH@AFpoy|?(lIa)4h6X}J~my_X*4Vbrz5k8;>hAa zmAOYy<$V+p5!hy@@1d>NS^t|gE&eBK`hC4wz4^XXopPP9NsUtYzHw9;d#cz|W3DC^ z@z0}Qa$cI(bChw#5?Ne#0*=+zz9&MLwgQD%v8(W&4Hp;>MDl$RD`r&Z{d2C zpg#+gyo*zj>j;NQxpXx;>ShM(=AKLKx`nNYAkG+=0#m5y&i+@bGLyuKnM0Hrve3bR z6vO?bhqJAr(=ESnhD@OJf|BR)q+Cn{yHV05{gD(t+3qBK$uqqI4J!XN%FGnaVr2k1 z!0C!j{LY>UY{8M`wi?TQEG=tE*UT`mMrMN}q$`m-wzLqoBOjV+9@w>Mu7ciO8ZARL z9@kw`_P3`lJpB>ebhr20*O_k&=L|UhO=B>w&n*|-c>S8)S1;TbH8QTfy91ZhJc>iFj%PC}q^f_aL>U&3ZijnVQztH5adl7=1}Exk2x{{rH&-MJ^ureU)==%>F| z^G8K7w6l6B^<56x=WW=xB$b5<|IlQ*rwhGo*D~N##5;j#NYr4nstOUK7@J96deSX8zs;5me(+2oq{R>Q{pUqD;ZYXe@~R|fIY z0!E&oqYFO-dUOVgyQ3nC z*=GF=`5I_xWxF$jf#-g!B4$#IF_^?RL+HflB%Th|)nalVvXD?{K?Bt6Z*|pD4~@{H z?(=4|_qdSD*a>66$~5vA$xA1(3=NocrZ-j8CSfqwaakU19k_S}NLug>%2L$t(><5f_at^X!H+Gj^( zyksI{u%qHe3L@-W^}rcF%sy!OV!X4#XvW2ZUXfS~6E(wm>Q{34LrOdb8j->3Af)?q zkJfT`C}u8dVuUiC-R${kSPCeWVx>*{{_fJ^5pB$W$v)|J=h3o)I#Z`tu=D5a*aK3k za{$-qb|G%ubsW#A0vAQDfrtQh)B(c^JLbPFhh1LqGP8iTFCq_P7l5S{6n?-QKxbl; zOEj40*9DE(U-Zv2|aHWP0$WJjbF zQtNy}D2#;jVQ8W`STM}zW-KdYv z8j!}-kM@JnqXES=A`ROv>2Jd$Um*<+?WZL0Os2*1B<~-5gS#9?k;V{AmC`lNUhgi! z)8T{em8(`psBW?5!ho{5a^P^-T=RnaZ@~APRcx>Z^!SA(y{ykGdP&utevUjnB_gAh zX7lvw`g*5=GYV6VNN2{9W*L=ET^nhj;0`pb9Hmb(djY>1dDRTMw%L0QG7_tV_w()wdDXl)em}%!V@JwnAY5Rq*>@@f~&}p z>H3l4PEXnH-OIpq#m)up4pwl^&DGuvmW`pLLj9EbK;vdOM%yn0(|C5GlMpO;7hi>Nrr%T@!$eqxBqRxp)>iO?*;5cl3_DTxj5MVhMb~NSZOceo2j<+$=h1a zwVw0Dq<@P$tya0#@23qWU=xhYAvT~<_s}KzLoPNjF|vbJjV~50o*a*Kv9&Ef%I03g zAHJFurGupjztR6=uEqlM*Now)=2h2%AMkP`z9VE?IPoJ@ZgQpTVQ}5ThsYJ#b6sBx zPEL=yxGd!BpMd726gP=c*vR6)mnewMio-NwXHC1)gvP~m2QC? z^(ms4U-FsdtmJXNqYg>i;-Ut}_q|QK8B;Q@lW3vSN0XE_$w^!S3F9ai+TSbEK@vK(6B7a9>Ap;+h3Z2}WzZcC!8C+V$4Ev}? zQoH7Eusl6F)0qU>vcNu+D>1+8Ir6QX)A};E3gf-v>H0f0=Yedom}kVLRS1<>H(zdD z(%wnckKj{8gqfFpI+X6J4jD5&tUhOPf0$L6Z#WA(uZJhYONw|mlSO3SpNv^$LS4NE zS9pDGobY{l8olZ+B_3U*2yCKY6j6st!lr7S>feJ0NGkE}FO$Sd3jVU&bzef|*hGf- zcrEz~d2>H4 zP}U2x=V@a~e4(Lc0`Ov$Ep}_{fRb@LpGUaik{Kus7~b)+g-etBAi{KcXAEvwh#zO_ zr~sMp3XsM=9?C>ID4(M2Nh+7RHr;Eq_WZg$EYBAIXI4VqQ)a!VC$i)N3W*qHimSM$ zlLIh}VxN-Rk21}(MyE4B)Fq*CDQ3dKNy-*SQk5-Dx3qhQdqNH%kTY6MOOhQy6-oo* z-2q=sp%z*eoc?(YJ`S`X;(m@`st8@3W{dDir2)O}?EUbD&1=--u+~~0=GEnsf_#;l z_aw3zg^D=Y?_bldGPmvVT%}}KY^g+Gv|3Uw^l&57>hN+2hO8=MVz!>V$*rG3AQw8u zPVK6rM}_fxxh~wm*VexIV+mm;RgY`#5K2E8+Lsb{w`VYHIF6T~aYtJp?pM`Wz(NE-^LTZk^AMW@kZ)r! zybsJzniK3(EcB$PxNqcUbC#(=&zfd@22hTvJh=yGGfXp+bPM<)hA>1JvN8iTT$7S8wV(eQ? z6_0BNh&mU--0lnSXER06rv&Kz0l!l zoxeTHLXc+(A>nf`ODf;C3@v%FQ6c70eIeFTtu+1eMe0oz47{e-Y~ZnDAt@~&&Lwd< z)Tg9E=bg%29}qRT4ZYQ$uj@AM{S>ObT27eKAUu&s!S0EnQR(qn?TQ0*LI^F58F-1` zw{K*}K_#Z2-hrnNlkxw&+u1na>E1m$eZ4WDfX!)HdfVWwsRPo`i9VELVqjM&v`tI& zO~*b{?ILjrOA$%`EjGbluxPW5R`6C#Z*9r_N#V{KEe}WpeM2uf+-6ne0?iYOU76j0 zl}ubjceKnwxp~kksK#2e7o7P}qDSs?7cq7!S?+ijx89e`r1xHqU%kj0Yh7My)+Z{| zsFTO?{G)WL$m!5)GKSg)>M!iz+}8dL*q0fBPh?qVP{yi68~cC8-`h{cNNe3ry+LeNt|<>bf&r#R$yOH4-& zdBpblSDNaMg>HPR_({WC+C1L)bRU&ic%lBtdoxPXC4tEAGQ&V<;y~c0j9+z#M6Pitt;e$H4{5ALfVgc{ zuNhn)t_tos$ZF0LMiw&~CTwz+it|=QnC?Rsf#QboF0E-oV7QV%zSihl@8_vCdM2oK~GlLY=-_gIe1zTc`XyJ@I;Y>O=o zp-6y(yest4PY9|Fm+G(^RmhZ>90uL-?Dz2I$QCi8Bb5xj;h+*q9ip&pB#s=cL@F z3B)A(>QxMY|2ktI`haOdR*+0W112%O9oqIov(aLIyQVW(+UL9Y!jI_5L5Oi6s4%AC zZt|@+Dn67^4_md*uW&fxNwd$1b3DF|Hj}wc=jKu5+q4Zx%pVe#Y{h|PEK)-UKJKG$ zx%j>PYroH3EJ?+8y+koU`Zao_dHb-}`!1^1(R#5htHg&2Nw>30Z`T$$V|uOdyxLEW zo{bgzUF4~>t2Y#0u+47mGpn&5D@U7csUaBrPH_O95*fS-(b3CRj-IW*LiUEsb)+wK4rQ3!z6t_L7Cco~br> z*40#j0!x0iIXby{{}^2c_e{F|)~RJ*Arqv4j?bHV`rw96x7R`Ng3Gj#B);Lg*6!4DZK_Nk9kq+3B`9 z!FE%Qae=+jXgUp`GDESxlInb4!pCMR+Mf+qbL!{&VZe42Q3^_2h2&4 z2fRm*HaM?{4LJ?cfNUt1)vj8ljPz-8VDH*)o_xmTdalNjQU5Xk$k);;h{dxnpG4r; z8?@P3A}@gc!(Lz|^9{5^>{a@ze%9_9u0BjPZ_*!?DnIjcKG?G8mE0_fM=TP{`wt9b zp$aTmB{WvayVz5*_(Xa?iKd(w%K(A%JwFPNA>hg9((nBxa2b5KhN?1fCP++fb@JQQ z>np(&F+}bN83;;Rj;6rUf|v`TGDr~wp-$siJZRK(AKQ#$`Rbcvz!Ckj-|J4#dFQfa zrXCttq#pj~=j*#*Ww{;KI+ZeJP-X@vEA2DFxn4_i|?6sA6duVc8d ztRTK#C#I39f+vZL+=k$917r*On>i_d19zKFGr6?dn&FFWg>>9!;JDSjNm+zpSxN~4 ztO=TYpaF)<;Oq_xv+srK<+9F>H$g4YqP$;Azgvxi6~@4XoxZZ>l*><0IK%h68>&OP z3&kY~Tn{jRxyx*p|EuAYZA%<)_ZSQ)MzAy-*@;MyNk>S|p3nUd42aBMD-VHgmBw>3 zDx4wDJVnbJ)$Wa_$#0|*t7Kjz0r<0GJLB&jK*U`knDcw zDp&)U63yn{6^1hFeoTZyr+kIeR&QTL$p=H~1j?+FA76yz84r>3r7VSI zIUg;xM9z_Vz%~(J!!m{JssZ{YXt85Q2lKWuV(WZqI=D**Jm0z~B~T`O%!Q$=0`uL+h=emOb8!5{g za&;GWg?hOmiAGaKA=B((pqqGw#Qmsw2V>Gc2qgZ?gFH3ID-SNe15vxrSxP2cy|6I~ zws6#EpU%ZbF%YJ9gPbcVeN3_{RJnyDUX~1LHVQ2aR-Ei_s5w19UzWdOM*qnmLH~!0 zc$5zm3uW6wGxd}rgPJ1ytX6h6iES?%<9>v5Sl?Q)8_%jkwO9!`U3v~g9(58|ItP<2 z6Yp)lNd_;y9_J_bBGXnN3WY%hkw#n2o7d_&#oIQR^jEPd2@N>35S?f6nh0et10)NW zi8N*ki9P^S#FbD`soB^jj@HNku(ay}0L@*&p&>2KH|f;GO->K>`#PW0uaEr+1I&SY z&1;nCOv$9zVv$+jReAL$To56bV0$YjONmD3!<_>%yFBpeSP7Ch9wk(}RR4ffa@iI= z#HO?>+rnsxf)BaAex}pz&9~koryrJasaGyDHwlkJN)4VoN*9>Q8U}H-i-*?S&oU=9 zM+vtibQcqUV!m&TzKXK~93-*T7jc!;C_;VgnT#|D*V8C8-dbMVuoN#V4}v#R0nDzj zG87}b!a(^8XL7^X74PlyUZ02@brF0s1S)G%RG5FDm+_zEPK6Y1GRO!ur~8HUVL)}3 zOJ$KiZd?cfbA==8JSS>t)IabXAt&@0dY|~X90ntr!0zx&_I~xa$mJ=wbsepNa%$ax z}qxHHuOlIQ{=R);N{}ozyGd*H| zqUvY+MQ2y*wJOKEVzb3pfSa9~v66x^-9OL)9Zo464IS#4X1_*W9++6J$zJ3pg0lD@ zpe()>!r#wDLq8`S1AP%{g^xe*iZB7L@74Oo1bOAoHLCMDo|*h_C&||TRQzh#*{GR2 z#Kt6ok>^4gx8r=lMyf+|;%r(Wu!-DG&uwpi|A`sexY9nAPA1dhi=tE-DSghxS*?92 zIbZ9hFd07l!W~L|NY5J?IEr_I2ct}PhY<7-)ZKp4yWM+3E9M&=o>6u4g<8ZvG1RS~ zZX(8;d@(GB6<7WZEbvn>okTy~g*+{4m@Snf#|)-)cEWG1zv1k?V+;B0Qp`!NmIenz zSSGBW@A`pht`~R0pu}QZuf(;Fjo~*bL6hDNm-=(8aN%2}S|ivSKDoR4YGaWz9&j#h zI6QEM(hoE#nEXfy)qC@J8SbZ?sSbu@7oj@`~qc%7n&nbQn zh;CN<^$(&?Hv7kX^9^@M$((M%U{YD!Y?a8(Q&2RP9NBt{=Eo7q=K9Ma>iK577gSjn zuBY3`+~OBI7ZFS@((?JLUzIvX<~20#epshfsn?jWX*M~3bjd=(V8*4Fh;P}o!`|+) zOi1hBxG0DzXYlVC^=bzkqVVHU;B>szJi&AKlqR2*7VuUjqz$>>reEWaVyKWhX6?L9 ztbDfTkfeb8O)}vf#J$i{HjVSchJwom$O3=aevu!XHVy>07@GH6aS8i()l@-&o%A?IHixpNLMya!7x^ZSidg1eFGez1>d{lT{4b zqt0g2cG3qQZ0ZHK&-C!iR{@}Fb1&pNtO4Hks6 z4GhlQAI~XnpXP<$%gid0J|l5r{CDI~#aM0KGVx`B#N=%e39QD>%d@|xe`?;6CjL+3#ZgL9Hr~*qXnpxSrWIoccJ3pp;@W-|$1bnDCD>s;@eAOlEc+DCK`w|*DFa8-4P+0oeoV$lR z@3c%dSSa_A#YCZREe+Yxokh62-46~OZVJDVIdT4f<{VY77NJ|TSTr7GfaBgwIuFy0 z9X4ZIJi5fmW{4F*br*Lyk>0?sCed)p5c6tm_nXyM?2p5k95v>OXZJxruV#&HF!uTv zTil^LaC-60?0gXEfk-@fDSO}rtj~eh$UT)}UMLvoEy&rPo&1PYbpBJMNIZ)dUt8!o z-K^WPWmVt0R+lz(gnV`e#wmk5 zKO$NQ(Kg(ur4is9i!%3`tA{k|!W1Eux9Q0TlpkD3Co$Lr*l1st3 zHm1{J6&*vcK~L1HJqKL*KmYaPbB)7coF*1NRG~Agvq~{l+^|wPPB3l?w!0m?=+l3Q zM>m=)Y%bo~rqf;SiQB8ciTLP*boi);qmcyKNuu~}-Mgo30y5f*ETYrLMp*=wk=)c;%#{Jp>IO$%=`#Bv9jd(F5OqBMde##`rlfBnpfKCAW2 zaL6o&q#GF9dw$WU+CBwRv{3T|mhID+;yHuuS4xJM?F@)d5Cpv(i_#GcpSJTfyKv@r z=R)|%jib(2clvnD2W%yuAg9D-bh)NXz$-?0VC}zG&M~xg;my#9OQvmO>l~6v4$qS; z_EJn=dN!Lb0!VeL9TN*hfHhhWtmA^hA>LM?i&6*%ojy?GS6!ikbstAs9n0J=pr=Rs zzn@;NS`{wh{(?TM2dmXuuja09XQ-a?V4P$tD)fni?K|Ad7`m>SE#Sx4)!BB*`0y|%ihZjYLr-7r}uCY zi(fjt5rXcn4*Sd_f1HnXhmk9mzXjbmzHAd(j$OC?Odi=L=@%2Bga$~%PxJ2NZbnNg zdZwiB6oOqLdaHNXcE_h161dYf@JuEwyrB8^ZRKD@;LW-pe|@DX)Q6HvVcisLcc67R zF_F@&#;tqDCN9rZB6CG>vQ=md-Lp$QTLh?1>-9c(c6|+4;^Twid^+GxnDH%7=~Po( zQxbZ->d1}t<(G_$Kdg7ADi}5gV@)_{*epI{X7{sLY7&Xe60?7_z4s3IhFnb}q2ZP2 z!Eu$+dz%ib<9tTvDvA(FlIJ2?>vU~<4M-`&_ggo$&Uv`e`%WzutVB}FNl-|XPhRD~9CsDZo5VQj4iEv`L&KnoyD z0dSZnx~4~DU|qsui*Fm`HI!M^0e{P|hW)n)rvf#RLLzkT2f*Ybl2U_Cl%3e+I>(z; zHg3qR_c1cz2i3}LFaXN2%VP1#CFN{!=A_39I!)|6MH$hYt1@&`Yd1qhC0{3KaBlInt96z^ z1s}C>)Yf7V5}3{P_!|eO$`bG>sR|Feqka*Fr2S2QA31*ZXU?m-!`IQZ_Iu%hgi__K zvD}6DE(?kQ$~Zu8SLM1e!4A-iP{f=L6wB87ftRV16_6sq7-}v&P*>qcrniR-;Iyyd zplxofslS+4SqvoSRa>Y^F4*+%|EQ10!1ybkXg7gS1Ia3h;%wc z&}*IReAa8qmq{9zD>y%be1hBdhzo3GwyPNOjpH2^l@yDPp-v34z*F0s-5JgzbGxle ziEsIL$d*rvDx5z;Y063Bd{I2wI%5w=@?;CXP-u*R#=ZVgLH%=zjQ_>~v&n%wvvQSF z80nDN<9e^_T5$(-UQ5q1idL1#@MVdxIo1XQU@T)4;clQdV(KJ*n@n$-20jg(VWxf* zH_@m5_)c-5fvDcRy@jl5Y;%9aAX{aiK@vcq#?qOu@1AE&Qf@M4@hzHj*P51;_cq-F zR1EbY&E)2RwXw{Z7OA^IoRp3vjCWyL9Wq3UqVDQX`I&ua1HlGW@MnGz;@|as z6lnoz1nH9QP)bBP7Tw)QFY253TC&vr?r)6qzGshd_K)*#tvlwt=G8Ul?N;x%GKa6p zJnc&hl${BdVhPW8FQpRdnRAGW%NYp6Y_34WrW*PX(v^vjuK$}hMf#VFkodmMDq2|; zv?m-9&*_=>`~C=?n3BcHj&4aqy7g+j_W{+zvjQOc%7|diAu|v!MCEK@*%%rP11qW z$pEbr)KEU&U$7=sEqK*QS_Zu9qeWUuNlNqP%#~1yz;*B)sY9kC*ivf1-rmMb1I*+W zrLWg)3Uy=)_DQl+eVOf+mj{_%Ucih1aX=F|kS{Ey#}s+U$0NR@XFrAF!66@SIwgx@ zZa^EI;&kUYonqFh*QGTJRl0};?5$i1&M)2BefW>+wMPi4ka z2cv5W+niZDwA!YpF96+_s028^dN|P zMG4bK$VogTCm)bMNj*VIvo)duX94;sdGSRsTa9Wf6*?^s zA8Hngb<1yf*@oE4_YoVX{Ue_E1*kRTNf3I)WVCX*@*Ee)4l<*JKWWNJ#S6#-DcEo6 z`=~Xc^QbF*=~ux~W&mzzJ2NWz3X4FTi-6EAgV6kjxGBvIM?v>#ym02Yz!g>Q$ST~y%KCzt1=f~#5@c6sIb|Ec}*yb zE}j6vopgA&(QqlNYCmhjCP!b+nTTFa-W}Yi%A|JjzHdS)AprSzaTDYn8p`XWa$aij zh_Fcmuq)vdB4=yXirCE&1z{TN|B3E#P8ktaqWJR3se&_hrY(w`;-Ti1Hrb#wHcU?} z>P{^!Yz;)VJU_KJ`&HF=kyu?+f4Nm`n+3FW&~P&Bso|lf_>jSNo_i~HfllfsbimtY z38iG>qMS)=Em7qqa5N>y$(}St;Dh>ZFC{~mM&!#*Q>FWu!-W=4m;1{f8p~8&qU1XR zy1qgLF+=IK4?f!ux$AuoX%*tHPhFFQCr*TLRJq`4rro+Bj_Icf?-A*<#pxpWF?0@mD-EVPVGgv%V>3pOSt3cCSuv8kK znb-f6c5(m|b_H98dPjkS#K@P^gut3NgtgNDj6dT{BVrb%qe>KPLTrJLrRNrGX?v~x zA`RxQ9#9zPzL@?>CQuB7kvUuk8Ju!giL1Xh+EYaGK2^+66O)RrP}U{;-~|hQd7$&N zJ}+v+m&U7N+-@a9-)JoH}7`|-U8Xa8Q-$A2+79-?gsV* z8bFw19v31gH-Wg2{>T3%6@%DK_xUZraQ2O*0edA2!PmjS2rR)odIR*`vwDIm|tnpRl_O)P7(0hxX zrJeylxfHKs66fApDjm8DE1=o(adM|iG?x)tXQG4z$W`5tBFWy>>f9$QGA|TAc48Mq zo`_@CLk@)*T_LX$N3$KvaCFr5UO*HyBPuZHxf;M@=ZJg zINiQwspJB3BAL^rln6wVBE%h7IS}Lh+u~k5Qq`1`2*QDcqvy1*E>U3HWloDG&xC#N6%DyV+BSKL9=?q{5YxP1Bp1ijk{8=U?Z0*RAI`>>KV1|J2;E zXxJwO*y%>(;+poDz@K3mi$BHKL5vl`u=z7~4(g$scy1sS`3h{8pZ|WkA$d_>is-!M zXk}l&OVW6+FiRBHqc`9Xg8KM^lmWx&6rNS^iQ@&1sBoW8UZ8+G87S z^ONA6BTm`APb7n7_=)1+tJl@Cb3)JuEK8Op9B_*xYT!7fn$M|@4jyVAAR zV3DLa85dj4pxOw&9Kt&dsw@smzQ8Y7BGO;~hGo>B6-E~izV)1;eiam%dZrb=c6-6L zO%)Ts$uZ?t_3PUWP=Q#eQezC-0j>kK82OUZGQN#q%%Vco3W<3TSF_z8B>fx&sjd`Oc{6vdB;ua0m@A}8`yh7J z`h45cZ$7(KAE~-tD+59WQ}zohk1xx+Zh3gzJ@X{!awYyda4N$`aAex^^))h5Zh0Cj z#TI9=U{(q6Wl;KG_zkR$VAafXtz%mlllS+t`WZ6v`4MGdx)SEcdwDHp!aAPBx?fDk z^N1HBF>-Gd_J>2(N=p!+{Vf{^j7V6E4IsZjSsL<-U;XJ9(?EVf`Ja9Pv>o)R-r3xd zIneL(`I3*!Xvc*cChvXos~Ifqt0WZswQoy-&nAOydJBSyjS&GAnx9<-K@rU#>W&9H zTK7PPqtYpR8!dxsqi}qCa^HqQfj6S7YW9)FvnJWNkC*KsCjx5g1s!HutZJ3N0-)>eqY9qo*O&JS@%8J&(?f*nFk%5CDl2jAYNrMljqSc zqaGPNHwrpib&O8p?|Nws;q?esmVa%qcYjm~nvzSc7rel^ZT5?!WU=Mj+|0OG7@t^G zijNr-ACM-ob{%-(KhW%%Kd3ZP->sm^X2pK&rR25y6rL1awTzd-I*@zWO#YdtT6#yJ zz4rZ3p#)IqMR4ymPqLD0x<7|kO5G2FHB+u6?Y2Ni*~ziEYl)nt3j)V3T%5*up6>#V z>%n^4il8DT!b9$WJcxhWLpB>P1{V5R%OxH7XRwchVP+ja`r zuB^YDuywm3A-)si^c;M+1bqLpsYASZt8hFFZmwLAPop)GapA5F?MDl+5fG0{#a6BQ z`6d2h^Tvqwc>QR{4T5hPTINiJmYsN}O3Ve|FoUd)imu-=G4ePR(k@IROipWijqM*XLvHrykxsCBdM);7pSNV7^AxrV5!lVLSK!fiW zx3EtOc0z z)Ph1YhrPM56CYIDQaxom4^d3y0p0qp;++J@NhWbeCK3>I??u$nKKx%Ys>s3!A*AlaxQz-WU7SC$ zmK)}>hRB3kJgK6>_Q7-O1X-^jR#THo=Y4h&90C$A7zpAMb>5)eY5#OAO=FyYg-SNH zcw$w!L|(vyyVB#Nja!w$Vp_15!V6Y2+kQ7t#{7^%5^Ofib82TBXbTRCykBWG)v&8n zPu_!(oAaE_YY(}X?$90VrAt$8lZ?1dz`c6MOocKA=YeLS-imhCkn`iI-mux2=q)jd z@6^SXdbuKadLRs0Ypn>})87FHM7pauD|G*qiobJ4rH9$89?P`|BL=Z~d^1z)yE-47m&M!I9~EDdV>Za1GwMeVLkFFF-yiW(o(F}b&56edw539^;5)F1 z^*?Z8$ZQ8`-8#{`h|T%4$)0#~Vs%u?eCr_gC+Rs4>|{3^UNhze(8=Attn4D8D;vys zvH04asv*1fmYfs`*OQ1fvqo?RW-x=*V}D4I&^%&!&qN(3)al&UV@pGuKl8&M_=(K} z;M`6L#740lW*3gs?z`&ii`c{k%}|xJqS%s+NxWqAY)6iQL-)^d@JWd-;#TesVlhnC z1;0g*hO{22dZc;f6GrZLzxnYop4+aA2E!ho+_0|~etN>H&PibM^vBuw2NQ%g)reZW z7nf%RZk%r0WEHQ~PKrg`00+&zT=3-~8qdC}>b4*bv@qbkoh7X4js*tO_=(v!$ah?a z4*7Yj{hz7xml_r4Wz?E{(}97`118g!;r-2!t6WU8m-^s|po1Yny1===#KotsVLyJF zH`A^u9#6>CG4?j+XY|~7VYjc24?o>;nj`_o_V-Tgr3cMIr)P1R_sSSbZWFggzYZ3K zd!Z-YxDLL5#p^yn^!0W3V#n}x@EzhG(fjA&JM+`eAMn}C)#YywJ&h)iz)dokc}yi9 zfac_yt(Hv$Iubvz;Vgo>NEVYCXXqhop8d?@Ld$_~X%E_loc1_Fz&90DD2K6EhdsRc zBzGp)3Y@>#23&a4nJH~d6R_q6zmbbq+2yw|av=Mm+=%s9#Y4?mx4YbhHqW#nWPnD@ zg$T5Xv6NQrI0_*mzsOqA1dC*vG)V{&qdg~NnyL2R@0hHAktKT$0&E2NsitRqI`9|^IhGF-9_ z9Pn|0ri0xz+id;TU>xXp_0>v@&pOTb4tuJT8E;<%rz19=2#FF&4=Tm5Tip`BxRN{g z*a;i~r;nQ{6r9u_Jq4=A49{+#UmdH+3=gI%fW6Q*>P$(BGQ7qIir?q9SG{rO+DWkm z>Oi9~*x8XF?E?-ZRiZptjN){9Od<3cmpe=Sy1#^rbxxz)BO2*&G}=cIqICw@K?3?F z2RgoOny?qrjiB?Xv*aMK)L}P3dbNZB%N%U6c?AmCM$o6)d^_vTX@OIOqg)>a?eR#L zZ;^272y~^izd{@68ebXW$xsg^GQX|0r_3dNIe6c<|M5^xP8E|{n()y2!j!~&mKd<1 zOQX0odATjj`jU8~K}LjS$&d7kco_b(Pp|-owoy~`T5VWd=xnpQuAawal>hxsARW_x+Z2=l>n-11JpLou zrQ9<_x9aPD*Q2kcr8pWq*YkA!N}O|?eD93N`P(Qe!Y z<`s`Ev21mGq@pChsVDdG(B&Iq_ui3I=l4JEPEhx{pSm_yB?`V^g~#k{3%<(N+4hz9 z;n#-D5TlLBG7e07r^IC4H3Ax`TYhR3{+*><GIb5#p4aw%!*Gl$ggA_h0ui1 zZY+VbAob);;qM}Y!B?j5zgbk8tMl1~&eExV;1)y@SxSZh(V5Y$EPp4*S|0w%H1y6G zl!q>UW*t-Cmp#b0lLJC7v8x%?MlfS?L8m%7%_kp?=n5n1bJ!9i!Eu&#@2h0iAITYe zaxV+{jU5mBH?<#LS-^gd|04Mj5%JiJ%*(^F$Jd>otZC5bYq)|87}YdhsoU<&9m=v- zM-nVko+DN`*sr&4PJNUtnEV#P?HSUb#Cjb#zH7=1AeG;AXM+KxF);Zc(_Y;7pZRAU zmb;lWw^eF3Z_%;#Pl;$=k?kT*HgJ>g(3GLPPOu(P8#`1Y$Dnl1pI#^9zQ*%iikcVb zZ}1O>yi2Y!z!TjO=vU~+p1_fhdI zDdYR;Vm>i}=^r8FW%)F^?YX3PVgP4TrQz+tw>8zu?XNY-jMR22z^vwuIl4d6z9pt$ zHX#&=yti=q?UiHl$^c+msyeldhyVHK(zS>8?>^CO2ALU(rTQd21mb|h)2{;ZSs=sy z?`951yIFv1rh5Nm^nq1U!n~h#a5aOeHI1+7GGTWb%N!dx!Il&A9T^)5LDMDJMY>}cj1f_LMor4k5--dn(|zm_}=PMYCUzPO}+A^lI1A> za)3ZtUrrDK=m-~|@AybQXsz7=vZAnrDxvT5%Ig_HNEJb41Z^GYzXSOz{{aeMtC>6? z8_6A4D|p6DK4aP{n8uTJ$*8`4$*g{d&8iO=s>)i{tJ@=wm%++D>JG zhv;O&GPd-c{AcR8bVHeWmIH@1gy_v}sBy&6u+zlWbdB70%y^&}J z9`rXzX7Y;uGY_PyMEz$_+M?ejzEoCh0=g@cjXG|@yE%6P1xg8cKf5r&?ab4oXHV|E z{+d{Hew>UcAm`R+j^T9Hx6BKa3!mnHih0#kGbm$em}|bGu@`ilykr09nv_eR`dKGW zBEB*cOR(rILKz`;W59i3R@Uc`u1I78c_w=)M+Xu_~{ddcXW@w%z`h>%e4*B`YL0hnhaKk5LMUqMhD&<)#5`0lwN3_;2cgWg{Zx6@q&EV4^99T{7>>4xfpKw>+1rB)HfO# z1f`Ubx1nT+J#@e$1kN)=(_EP=a^*Iv0Yt5Yw4Zul~mfWKG%Of6BCcGl$7( zZta{Mb=#|lCk7qlk4JKx(MB_XCq4k~BPV|w#|)@QC~&I%I>kt;7Rp-|d1qFrg66!6 zH`XqY+w1uPO6-sb|3|TG_zmJw5z!Ceju^;tO<^GH_WuCG%-ADp7XUYo&^}|q_81pn0U=D`K@Q;P z1{%QEf7kAFVz}n6k(Gh=Pe8oUl^C&egLsb#M+Q-mS!cd9ff#xxx-rp>_nVTWZ-@lhbK1yU9olT#oOXu~tn9`rZC3eq zZF0!wa7d+J{Nve!<^^-q{$g4gc@-TK@4-zfM}HV$Wn*;YU2yXWxcMZ1Rr19@XVAb$ z@h~+uqg8{U-*;H39GRyu@G8TtWb7Zl5OF|9&$TjScJ$w5j{^78G0<~2zJh@{I56?3 zf*y;kljbrx`txwSgl>Xo)AdFopm8v#iSa6Dc}i|Tzirh-)>RsDppj#iL;mqx#l+N@ zYExM!ozv$8_aB4#r@$%R8wLh=R3cQ#FUGkvl-5Zt4bDL~u`o3hn#)+Ic_H%X48TOr zIM?Wp3(!q`yvmdiJ56XDsvu2~^{BDox6tnnEAZS^R%m3Kd`UF)a%zA!e$y|eF%g~M z{sU(4mzd}ji|-*6tD{g%OXVm{yHpL0BSKXJR#8(%hy{KpOo8XvHC`N#Z`b?DzLwZs zyQ#Ygko%7(5%!Px8uC%T-Dlf+Yq4<`WreCs;LAGMFUIY%ePqEvBt>ADV|4gSSVWvK zX$r1Pg)z9@`s|?RGt)KyD zgUJ7~yLK_+|Hd z5ML!-!K>_B8Y8dl3*`2Kc)}VRQ^UqY)epfRW;tMyHi9Gmh<`ocmgyT(Hj(u~U)|4G zV1>hgx11%TPO9f|HO>d_Fhlg81~l#S!_ipse+Ot<%K(^h8}0>jrlyW!ylD}3)b~bMm0I|O_$jEU94TIGo+u$E1sJ;x@m1CF zGSju!VVSVS^Q3nZzMwf+T*aN@r%baiEqAXH?2)TOrmuKKq{Zsl<|&KK`02R)EMLu5`m90#1H&*G+#TcYT3)EJf{W z6*Sd2H+leh%!Sa3wK9MH5074mV4(?XC}Zck?MWB)!ZaOurPg*GYS65JqCmh&6{HR% zBzD*f!6SBH#jvq|+GS7gewO!)cUQlZD*PeSJDyL7@Y1ypX+jkc!jW%7n70Lsvn^hg zI=yx~24I_)rvMU_7quPW?#Bfwz;MV{=05<$=$>D-f}s6%&FDS2Cf}sZfnZzcqTrb? znK0yCJ-`?S=Ss*=Cltv8BkBaUHU2q(Cl~<3XZ|u`PVhc60bXVCvWVa%=q*m51O^W| z@?PRyNy62{)daFuStF|k*=hlX|NJh4B zgpv47FhIi<*ffrDoM|cBmZ2 zt#J5*!~nIu1SH3+JZ$q@sWv#|rP4LZCe`WIyDYzn(xZ~#Q)`;toGk&3>d!*8J~*%W zVi$!F1|^a4BDI@Szuy2xrus##DLlpCH;>1F3v-78NLIRY3k;&oj*FrIXBxQul4#Tv zp}CN>4Z=F(36pyNg$_0FPjqyR&D(1D5Z*miFfl9p?hTNlC^d3-+<#Z1u{u>K(k1L{ zaTI;{Gp)tpDx5#r5iczcOtfv*iV{t^NN_>lA`ZzvggBU$ic)iTPv7x8M}X&O{@Ke# z{qzcejh&WiJu2P0|H3H`iiEzX&1^^Fyhj;eqj5Z(`jlVNR6YAVVMiq~{Nd`K9Y(4H z{r^~_NJ1P~(^9(1&ZAQqy8u?*m{w2%q0aLH7G|YE8c8w2!ayqxGKP>6(KL%f3UCc( z(a>^7Dr+?Ye@Tj*54d2b%&AHTjR7aZbIe|Y4+x^bT>*2N8k*Dou_nwy@RT|8WZbE+ zC1hftYgCk@^+9y@^oGz-8segT`idtuxXtuxljL+z=)K2yc#hI%I70kO3#%? z){7?5x!3^02I5AV6u7|7Ba6^no^PR*q-;{1z9aGy zAe~$JOBKuM74TLhKAvOi6FxI<hR6|306VMbK5s#3!k%oArP?KTY5Gc* zxmU3A?9@MLE~g2ov;CB{;#4PbdI1)seloxhF}6qwzyQb7OVNrwPuCQM z6X0UH09;yV>aadIT@pXIP|g&25n?WpD!|5lE*LBEC}SNKL@>B&Ec6zs9Kg{Tztu1lt{DLevVB`c{{RE;ftbiR7om74YK%_|KsIiqq$Ai< zgb&O*!*Esf7m&bDoNUFFXcS5nh)n9YY$?+%!*H2fRY$!I{Ses(2*@-vH9)c2cW{Bp zX7dUH8)=Yy>D<9ZC;{jS=LZ1wOTytL)D<8lHz6qrt201JNfa2v?HmI1ghl`r*PrHd z1iSE3apXhlCT7n;#sG{_raD;?5LK0dT<)ZuFqYYA-V5TleLPa~tQ4<+M z5}Izzf_O{L8=&&)1MlDTFam4ezWV$Be?<2pU{B|kK&96j|VyOrcPMj@c6i@Sq;nVsOg)7q0_*g{Qw0 z0zDK&3z+NH(>e{*VEI7)qh=QS$xe9y06L-$Xj-Ave{iZ479lNkY+}#^jUhJ&mSm?P zGK8|CX^Rvbz7PqmRlVj0=tUV`(C@HWrd#{9%H~gZ0AM@8 z89?7#z=d+cMD)RhurjB&h+&>U>X0q={3-U(Ng;jv1$)&Fbp^z>?*UvX-~VuMrNJ0N zNH0uI7c89grE0(_SP2#wD(5wJ2P!RZ)vR;c9SCvD{D~)LNJLXj2AF;kAb|~HkfgRz zgov_3BKlFMAN!O%A}K&}IMgYl{}j<^NJKZ$Tm*j!kO&13jXv#8vq%VtMOCnb>LtTn z6fKG%5sfVhLvaE4(V+sKD)Mzf$zTrPyOl+3WrQLol>v7r9<9Dcp@H!V7z3|O7d0sO z@!&3?55|23l+b-*1mH8KHX|mg{Qy93GSfppUCTe7_kkgDI5id!nLTp=Xe)PIm{5%I z9)Q8Tk{E_0k2Cv6}4(AQfEIqc=m)(AT9#&l`a2iu~pD_N*1^oYz=I1T?codcE zv2yKH<^FQQa{!5#GSs+{NgYf)BTT?E-V6m12*>Fbqz%$ZcYZrg`jFHrHI4p3ulZHL zawAn=M^TQG2e`1U#KDhnobC_<>TIG73Z3Fwq!r9*DsF!Ll)li7D_|y~U?Y@#&Fg@P zSXf@nKvYiHfEY1%b`+uKp{Wzt!R_zE0%CviFDH1aTaNQQYN99rhp|Z%GI#JEXdjhx zA=iI%Nfi3%{$54@(kE>`KhNlGe?p>2y8jJ26)={Cq~V{c)b=b`u7%wF+*1Yx%m#3t zm7w7RF=IX};O!&Qf$ON1a|2Wq$e;f+piW=|JWp1zItr!aAS49+Ny`LPAgA^%`OY^_ zsp829LIcG!;JErfXw3lBUXj!vfv5>&VmM52kb3Y03Py_RUG00yd zqZBzZ@Mn-$_4Tg_L=uiMMvwLm`Zqt(TeGr1FR0G@dZT;#e~ZUdWjv;ACIA0>On-Y! zj=Q02y6pt<-<~Za5Y+jD z(T2+Pm?o6SfgeFoG==S(f>Ov2o&xZn%7h{Xoeh8TP`K;SC@vKns}1Ww~QstzrnCod{K&powoU_JjD8~)*+dy;~c^^yFM zy0T-EyW%+VY2g*@SjI7X$ZL=nskRVnv;#>D6{VT{XTK03A_|fGeq4J zz^FL|B?#35K_>#G#kyEj5TJ|ng7GuOO?6Pnu%H737p?~ck8{ZsNH_+0iGF20qIbYSV(0!oiYENCxW~)uB2kTQ_?_=$XhUz-vdq* z7ue>3iHM17e?lbba0GAoE7@40)ZyP5RV3L&NWoLa3{i*@55yN#(f|%lu2!TQ{2inv zn9nxjof7N}nl#!q*B#Mi`@<Fmiss_@vRN-!qszn6_)XsNa&aF8yuLz~}gnfinhL9CJJ~7tqRiv*B2uOMJ z)16~`^(uk%Z5a)}zv?o0X&`JY!slv7!Se)ikfX|Hkw%Xe&|EUX2$>ZJG&FkPStUbC zwB~xKZn7q{qbxX?PaXdDAss1C5ufy5!Yxh+5>Y+hT`YgCOT->PU7$+F53R}50tB;z z(l|>+C|!N$*7JP|%ZBaUs-cOwL1(SF0Xjcj2i4En7At>>JXOaTfZt16S5Fh=RB{C1 zLeyE`25>U|g|Fp=R($_NzTa09WdaVXS0DR}-n4jeBu^=YcA6I$i0FI12rnWHRp zOa*;_e@Y_eA*i+VM*I?P;OlS!6l6WEcZUE>7=kS40lv#!)Qp!f1>lo&R?D=k_Tz85 zL}AYi8vCtA1|%I!PIU@`{GOMfH$VJ1i$eHQC|}0@c>$?UO~EXEH-5Wrq|e4v<30Sc zFL^MCAHiKxNdn-TXly6sp$;aOa8b?~G<59=;G+}638ZERSE$IqvIo*$B67YWXMr*2 zmdmSRJYA&7Gc*acbu|G&*Za?OB`-!9d09<<|03Dcue;08n~f}p9?7nAaw{=%WU;#}GP!r-C!_lh~YXE9wT9hYdHY^A5ju7Y^j2 zbREKWf=-k5K`Rc~Q|K|DKxMP?Y6B$Ghyk>S(U77T4YPif9@no#7@kvyuj}7S&BE`R zz9-sl#ebM}{`W&*ump1eak2RpYFt}?uy9zZDk9lK3R!DUzcqw)M!+$6rSxZ8$<(0B zm~lZ(c0ie7EN8%w+sESaqQ3|y@)`g{_|@qhDiSpT7oH|@AzVAWzZ_!7lIY@8?u?e? zofym7k!TNb9n^cbwD(NR37}XN@nZ$>S@BvDDs7vv0|qVn)i%Rf3IKoU@sGxDYxD`I zwT7Ru#RC`z#uTTn9_$82rYsC7me`*X0l_#r&jMBL|a zySk}3dLb4vov531?>3L~9S5c^{>=+Nfi7q%VMpW;5dy{yL2Ffv%p-mSygSRFhQd1@ z1D@kyGONzF2G%QuMGgf!T^#i?ORb%wcy1_ef_}(BD*g3y%2O4O_jf$;2%sq3z7wV8 zM8;F)YZ?@N1g|8yA64CYN9_lq)i-$=tilhD|671;kCfnlC(K^LVJQ>wH6n60f}H*B%*4=*2#6btx%g(jg1 zAy^PYJO)Ec$M(EJEG)qdaNv&H)~VgY(0y?xuMpB7c$3 zvkeSn-`3v!>;DHvZjYDoZJH$BipkwfGq&O1tqjnpK{fXib6j5n>|$49!e z#?Il(-KcOB0l@+=U@22ZzjZ2ToY3el;3PnZAcG_j!CQ1M5tBfJld6rsThjEexOS5E zKzmVxpdRBqt9(xms(^*ygD#-7rD5G>C0WmvdbXgMM;u?04| zh^@AOlvN!?Nw(s7Xm|u9+W)SRKm!Fqv?z}=K*`vACActmu@#BPJ0RULc%9`Xid3Xe z#^*HM{g`oGfI<7|;#oT$I~Ry?(w~{odJ(a@|D%;S5a@?fdW)MVOaz}{i_|l z$oPEvb$e52u4|;r0anvB{v47@+jsnJ%4OVtHfjK^KmjPdSQ>>)(SwyHSc41SR|JsB z#($~3USxPamvpV+84oD=ZPL!hG9Krf*ZjYCkoSH@$Lk&>U3=4+baF81VmZj8sAd~= zPGP+qMIZ2Eo)o~AV^#9oY!3lxH6NNs#v)HaSw9;)^C1dJ<>|Gc#0pSgS)FaAbufZPjVAKH&iJ z^Ws&U2_TV8gPhW3!o{if8vhDPS6Uac0&3{G5s|Xh8$R7eWBR1dGoF?;2RAJ$56UhH zXft@4;S6s~2j)!=Pc?la@DE$@AEfYP#~_OK2%jHHb%;#5h~i?Z4laOa^(l*|2<-rv z*|QkP@=$Rnvgny{14Pb*eKOMUX`D5%11xu`hnx~o%2>pTvF(88ZQXFoC=ua_*?%cp z9dL2CG=Z!-Qpx0rXI!WZ!I;P9!=&o!-BoY2gSAo}v&uoQk@eO}{8IvnIDjJCicIeo z%P$Oju0mB&!NqxINK_L70A31|Zh_VJ)(1c`2#HFr{Af|FsgsO&Z=gsZrJ&B%eCKw7 zHb3p{a{x_j|16*~Y~P^RmhQV20g8N1V5v?dpj8o)dS)=V>Br$bE}#u&5nZ${a`ErDf)VltpS0Pa2JD53gyC6E$Ppnw)V$VK+XgghjPhsUZ*Irt z;W+3S1Iz;Zwaz0HK+sku=mXY}#Bh*kH*VK*G5j52__FHG<~cr1R0q(89D7;m)Mx*ZSqU<4MF> zOIi?uLB$4$v(M)8n-!~N-AT|KXeUGH;{|M2ctc6v+{cJHIzq=*`O;JEg!f-tMLf?z z!C1#v7juYVeq~K8P4!PrV1g;6Ue{Pot;OF_IUu4O698T1p(>(SgCMgSxL}?94#|@@ zfkjL~kDTO0rEhLR!aylMUbN|L01@u5LKeg(|FscCqzn*Z=FwGTg5oitku(;2gE2@D z)pHU8-h+CIRs<4HwYMiHVEFDHPGS^WW<#t&Yfga@32Vr03u8ytjz2-QUwpl{-2hNu zV@B}MCs3vtbh-};H)K(eDf+7e0wSLZSXAGm!8<6%nE@s_oJk+qZutPpjM9F)l6u)u z`f~RZ@DNn6E!y-&%Z=sXgwmLQB_AQ>RRl3!SfL$+KZG*-4yeMc^qm(0a!?51{6|4I zWvEXZXAl|exyu+D{FX-NAc${*HKe4Jq24$7uX(xmz`m>GythJtOa=8Pyr)u#5g+Fg zlTrX4F%<^5(ngJa>S_Gd0stYh2c!>+6ubIRxum~(`a~W965YI;!s}ma`u0N#2#rOs z!IfBvE2oA!CIu8TCJjPh z&nplk1~LH3)U68D0k40m6{hLmE_6iWez?&@Vi;qk^AoVd@a7M)sdxMqt(qqh@=24C zsd2vT!Y8-f;S)Zyf?H8KZN#xC&-T z)IaUyc_E?*rY(4kb%2r}BVfDCgV&J}N)MEq7Vn?{Cn^@5RrUsApKuPba3Zj9ZHIX6 zRCDe(E~};4^IR}xmm}V#L?*w%%#qJKk0cv#+65H~f2b^3kQk~xcanE8Br~bfFx*|* zKf%_6AW{FHgd}dF>6(YHd%;e?nEL^MMoCS7yLA0K+1tU)fuI=np_(x>+6ZOSD*wrM zo2*O&O2)d`uuR+dIHoJ#1|3vOE7-=c{wj)iE&~}b$fdJC4Ka*t0SAenpj8SZ*ahlW znw{2FlN+~tcmEtD)+rEP=2indVZ`ws+?V)`;vF~245uPA#R;kW1>1N#U80P2M=tXC zkDRB(6LgVL>lh1;zD!;Hu0U3hZMAA?XK()Hu5M3SydPz}h4)q6^*s8fN0~YDJxaJ2 zWiPti;#Sjr9ilp7T2oVBeg3FOPqT=0t>Pe{U%Ta^q>M+0wnP()%c^RAZdao{>|jYg zuSMhF>7Amv42Gbhwc?~)#`QsYwkeby&j&gn>NR8k@2UgTuUhSGijYV-sAf${8s4F@Oy8hN7g2^5 zE=p7F11jv3<{&NHMMyEe82&;FI1SSPs{Ld@`7EdLRU*f#qiZ}37DJZLh@`9sms-|b ziKYUD?>?K_Jbz@oxji+uI~q3DGiw*mH~a2@(kZ3&_?E+RNt29KU6E4w@MKbZ6=k;! zk6UxK3D;_AuXro{VtCX^8BF-?L;C2fCrS$vLS;2JhAFe+knpGfkHIep z|NL^&t3Z~CY&L~|H4uY%L33wMPs`<`)h+!^xUzFuDLjS$k)3|E`AjnLTbL$=Mq&HS zlo-Z1=b4ej9TT3dH6B&hodtTamLHs(SsS&Xtb|1l#UHb`4PuPL9oN$%`1z~mGHH;^ zbMt?Ud3e+vdL0jg&$oE+wF#yCw7CYLi>m|9l# z!ZcXhs^>G_&h2Pzqv!98DE>?Vompv4zx!7I?CXVu8P5pvq*UsW?ZIhK5h2Wz>4V02 z5gKP8B-&7MCbnrs#uL_=B+*pqmhxszJm40YfsgvmyBf-Eu**K)VYY*J*7s!#W&8z! zqe@BLm2IBY4vuFe0?Qi`I#Wf8jAdbhyMhcm2QAvP2N}Knqs`^#kG9kL^>dFNtCv@8 zpQB*swI_2F$ykcKMd6v{RuWbTc%9ZH4rP$$O@K`19#$zmcaWG)=yY!&@+ z>=feDlwevSSje1Jg~Isu3Xf*s3~_voC_@WN&0{tDk!*dz&4#)7h8GR9D{=|kk$Yo- z!q*DgxLux^>KS*4&Ba}opuCed9o|;4HK?rbUnIHnG}CGxOH^lmL-di1N5hzLfg(b|oU(~(Mk zgOXh2h3ytrt%MO*?EJa2QvD;mqya zc@A5dnyt}PupX6&cVkY8b@vW&OC}9qZhPE#%Z({fKNxb!|auEKs8=#V8J55j@stS{FUK zvOTd;hbvtFj-wUdZFWz$z%bdAzI_HppYEblJpT2FdVf^q#QkU{Vm8I9k)p&-DSEe= zW3B4IzT08P*>(N6f=#2FM_@rOwBd*J*cUaoxi?l#WVx+|O+CTlRYNM&W66VK=Bh4f z_7%0JMXy^?^pLvFl6{3lt6*8CkNR*IUF zV;6Uu&28^!i>oCguAwAbf;?LJDPR`3URVqR>)#x#5MjE53#yyI*IN4xUyqsgJgiQ zv6|Va5bfMUv=m&SZR?iLJ;%jGqf#90q__h6k8QqfS916?GV^GV)x0BpTDTh0a>4gy zATJ%fwM0Z-Q}yDTr;={fq-=>s{hZx0#AnY_kVcyqsVkK39P}G6Nl&KJQ~Jfb!}FY$ z(FvqTDwc~ZF9?hWB^ri$LyCo&M~%!d*7=@I$99RP2RI!2aD(1AOi`WMe#GS@e>dK~ zYLiE?WsTcoB&QtsUyM$_8@_GD0d+W><4{G7zdN-CA)oGis$V$>+%ZS9Izgb|uyynS zf9HL{sb#<1x8^+RJg{(&U~~oCtFC753SYJ*Sp|scFYpPh)~v01?XZnV@sDh6MAZ4z z-yt(&Px;t-)NCAQ$cbUnPU7rZ7;|oL^yN{6@|`%xt&bt1;i}p*rCWolk9KJNo6|ow zky||>k~`#=)hgXT<8yAfIQaPZF(-gDY0X zWf80C{RNS^+(>k}Tyyx&tfo=$;cViPX0%tTVZ6OgjPqokelI=NbNbYc)xzyYF&o-| zIgQ7z>}v{jlz!B>6j#~>kMue@%(=E=948%f7Pf|WWCF6qt5kOsTGop7TW=kLi1Qxd zGAfCekOavAA;<5HBZJ^VFA+DwQ^Yd@p~@gO&Dj!A{bcZB2KIys6zaHelfJgB;yX&q zBo*^yiO>m;P1-Nw1)8#~H63p0Zh`@nKG7%b%s zx;mRxoNZYi){Q@_d-4*Ol@C71#OhyjiIgNs{75a|L&{&(tiX-jrt7L$-RdvT(r+-- z`z)?t{yt4tNGu_6%QPfXW6F0qTj@LKQd7d=M|)Gac@5`G>p5hFTVZ-zs@zlwWFwnjyZC5A>tB|0}UCImW1 z4$9FZ^*>SzrDcE%Po1QZz0Sz(8s+6;<3*WFT5dp1T>*h9k@FL4m^xanWzD|xt(v_< zTCCy`?FbUr;pV(~fr7@lcsCVJx_!%`&gm$}e27{$1Z-}F0 zIjRm5H{ZfTy*-3?)P4*a1|(Kzvexik8nq03TbIL3&?eAgW;J(K_P+I7bJZG}tms$v z-Ei7eAG%cE;^PsUIbq$16>dK$^srO%Sq<&h4OP(F^bnOL!50m@7Bcp}qWTc-QO|x> z@yAH2Rp+R4*pK*Ej-j_L)5nA=R`7V#J?zqR-VV0a9Kw~Iinwgr-FbcvDciiJof>f% z{azI25*#CU(8V9$(61Y)zV(1>DfX6P=34Q51A7N><2`Tmqy5s^FGPGGvne%u`XA}I zD_-s2Lh5uxds%Q!4YA`2oU|V*twJ@PO)kcYRTfVe@Y`8-;G6cemt_QwhWm{QvUNj? zlmh4}T}q99Jd&QM#fiG-mF%7xYikzcIxL@DKKZJn$A(PKz8Hv}VG%|K++xH3Dw>SqYfWYF0ilab-{gQhqS>XbfBTo=K>Uk(ijj$3s6VZB)DV!VDysi7*gXjO zn)`5vh}xx~x)c1lcsrAaLHyT9h-|iF>CdfHnW{PTCX4ABM4IoEo@~td4GJI6*C~Ho z=;@!XiLpFd;%~3qX!j5tBv*+w3{Rh$i}x@Qq#;tZjx5~?ufQIx0Txr)KG-Shq+-V_ zpfXyzWSF~e%yILB!!gEGX0CX>utlYijL`8s_X$5-KSJ{+OT}tm<<(=C{T)@y`bUz? zS89&nrt+GSq9?TB0S!^dZt&)&joKRUVML+hwB*d`F%1Q&tFpMP0xR76xpNV!g_^D# z`-Zgf4|g0U7v>szs#2S`$DXf*wSF8^6CD+^E;qIQZhCB)=XAii##X~z7N^}9IrTAB z(|BZf;h?zBRM0B__(hoFn>X}vG&yCHlY8*YToqj7YF0vb_me6Iod#N>ck)T@Ec;Sj zA6!L`h&3}7^d@B|g-@z^gpSu=ayQXYC`ga+dsl7QfBA66ua$U>(+wxvqr06~VR*gW zgYcDZC>v|lx>^kI&>bf`eI_kM+XFti@7I{XDip`ekNNdiRQ{(33fa*>!9$VD-(C^O zTw$#Sk)%hn`%XH2<5+-DmXXxYv7(xvqr_~Zj#I>27l-W*O83`bpj0|cld3_Ns(Hy{ zzd_l&?3lR~!$UIm2^@CzeB%sptkUhA83V`__&BF_B{R23LsDVY(uLlW?n{GDJc|$; z7at#|Q%(=7(_9Uuo2yyLR8H9{C9;XS)dISHN1%Sd-ojTgi?P-p_GaA%1yD zkln#+jT(1!%=NqZ@rT2uKKjl}AfyL!RPfVhI;V-$G>g~l8QQL^oAIJkb#j?1L)1It(Omk?pD?(8jmNj1;CzmpoxKKX}HLyBoh* z)-XHoza~<@Wl?!F*1A%1GL_PAe!%S4NOO@nwYJ!n?nM8<+cZaxo#Df^ZQ|P*SD;>Q$8#$ncHW8cG{Y<(!Xu*9=Z4PQJOj`{iKGKbuE` z-XY#2Z}<3olhAp}DAV$_Rel{GW}Y7-bD9_K=sjQNUVm|zBBNPSqQUMoc~tVzuz^n1 zOtrA@F-|MtwQ5aAG>evm(=cXsh{m@Z(529+Yt2*o?x6baXw6LL@E8q4|F^5oT#Q7jV8K2S% zsQN}4WZN$WF(Ws>BCSro74v$O|DAqlx2g)Dt11oS$q)rvZnKAHwQIM>kj7%k%B70> z@hKULRcGL~>ujI#3nL{mjPZDuRWvHz>h|!3ebv(le_$XojLfXr7h_MHZp?ltu(TP| zKEFZ`q3&qWS^8;0Gf;@Ep!K*(ZEWKF3R#4PqYT93Z;~VYGA+xgYtrZMU_Ed@?DP>^ z(GBE~+4RsI%Y zQ$B^mM2A&}a8=#NGfaeqZif@+H)hFl2@8wtIF8W?>c9H2P&U8GV6Fj0qsIT+Hi0z1#m@^`+M9rO`5^LQwCKde#MjZ3jpYCb?J#^#6? zxg@ab_N_M4FHt;$rVnEHaA|$2Q|Cg&A4;&Hnoswq%fY&3#X3Hg3Phv#!tjEG{q2 zxl4CGmGSMfPVEjAYQLx2hJU#dbEEN7U_=q}{Ezb#rp7>=ac^M?Sdz)Y#}%qSmR8&}=k} zcgJ7jd>uM;Kue!6da}34cDzS0?Y#mrfmUnkC367Paqink-*DmXARy0MWcMp}vNky` zV1}i}x;)G(_(I2A0=z(uHcrXm?eWhaj1uF@Hui~^vMOx_PL{j(rOTFx$KW({anrc1 zyJR_e_XN0>^VBqt#jF=itl=iAHPfoH)%%ZYnxgL&H8+WmX1!vrk}!J zk}e*mvmekM`{3RfsY1!TUoyCDVR_71GZexa@lo6pc7gE!u=kx&O?BP|C}Ba{)HI9`evl! zRjB|n^AbHuDH1b78t0wmPf{n%BP1rqZY_ULA1YA$*dHA2br6@0!8ko(D{+F z&8eP_sf`X@47Dc-45pE%6;qknVAQ+bb^jMe(Qw3qOnAgda!}}Xje}T zM)lQAi4l*3pbOVOX646ojDHVFVKo=r%&?4j3kZUOPmUVmTq)jn915E+ls$CJkEOar zoPdMk*n1Q%8+ZM#k&mDb!o)|nR|i8ZM8&yk=Xb1A_(|+cZ(SOoJtmtWqp!)i60@@r znGfTFX2Z8|u&imZEog2IOgv>H!G8NNbn|Y;j%C!?JS&mrjofmPHapRAAMROYPZ&Y@ zBW{dRR$3SFZ2p!ZE(>yk`X)p^_lE<|CPDiAhF^PAP?IF*DpG$56DTq<~m^+ z)Iy*p$*}tUlvBK@`eda6Ef>aqW^irDeZ*jL6};}YtZhH3WO>-7?}T>asyt0ZNpSKz z`wq-S_itnUj$_BlK93_R&%^mDo{%!QNX4iDB~>rPm7I*zci-QK zUb7sP?T(>}MbSu(Lbq6o>SCBLw7tKXSH0=uvZ)A(UQlPk0Z5-$+H;qK7;EOA`DLNp z#LW*n?CG|!n{`O0=*B{C`hDQMn(X@XR?AJfc7qjnI_V{xGwL}iCSmI2fE#~x`7~enm zs)^nx`~K|9!l20`kYD+J790)Kg-U%TXHWo*FAn_?0H2WDjb^M#f3l|;Np>as8FREc zvk^?XHuHTjsUYX1uHODsL~7eC4t;1VImf@UKmPcV+T`$pv3t>%)y)8gn{2-s&QcEX z$#8M=;Sm5Ia$7eeNXQsZl8YvB5i)&yI1JkTU_ZXq@=)e**wthOO$fmFZ{Hy{W-=t} z34WfQ$q;FepcWq~o<>dKLJrmu{dVa{wN0ddhu(pwJD^5{IGPToSFW^idBrR^*f}f@ zjGUr*bAm?V>ef%402x7k>8$-vPEJo>ht~TReU5r6KI%IUWWO*suAUiMM}d=Z3mgM_ z2hKnSia_IhWiUA-^Xxr4J*~@)8BvU!<3mBp-8r}LP^1~)v!fg_YdkEj2&2PwZ<~(uWvRo&$yhl0H7*l@B zbLEQ55@Xf}{F~l8O)s{Ft`(Foq(xdHD#ufbdWu_P!NeG%e9M`Y!_Y>%kVGP0wwmY4&)rNal^TC}uCe}{Hl0YjwEA~pla!BeXUu)qxal+-Jrfw_kF3Tn z;V35IA$Evf%580Q)0T_F^(=YJKd^z#Cj|_66s_Si<(rzX0=A!x~Kc-!xJSF}+KHoA-8j#D=9LFK?K-{8JjUdz9o2fS|F+-pAS`8joQBb#Va3;3pylbTj$U+uf#TE^-T^g94FTpiU8Z58yDJtD>7YKY2kS_$oyQvn2kqwOiaWl!{q#L6m1lf zI>FiCQuHX`sG0RNBn+G!4jK@cJ-U5j+XZkzak#{a>U~^9!|?u$eKl@cI|0ooSw*bP zx=L;yyKCk-Ugpw&hxh?+=LFje5>MZ{*^L^&b*)aUcZj9@<=er6M&69xgEfnaH$9_= zG!qOeMv=QtZUP_1DFFsxXzKlg0R~CF?kJ_-j;?3782|?GJX>i+5}Q7`faFvHf&Df% zJ#heo*4~F1J%LzE^7WZLQ5Ua8mz4FF?KZDKgIz1Xai`elgn6ebDN<3dEd<4xZ?|nw z3fLGzo^MWlKAcc*@HqUE^~n!w#qb(p;a&DAe|ko)&irkhOZdd8PERw8@FvUL z8*$mRV%3$l3u-pK5#5^7MwPx?jGIGbQL?LH)Wf>p)NQ+B2QP}`=i zxu;TmBuM&aS~dH(3V_-gY2|z^dSY*Ch8R^SU)Kah>`tA~aut+wxuiPkj~iT(jpq$L$Y89)AP`_oDCx|^MJK3aMBgybh$ zz?%Q3l8x8`jct~e)~#`c)rp~$MiTP}-tDO^r-Y09{B{ZxA~1_?VX|-Xjx{&Ad6OK$ zd8|ekGleP8KAmRgnMJ)b^zInL6BI@lS=} z=OL|Mb{4v|d^+m1mFe~z_lY150e{25Ep=;f)D|O- z+ex|qVEpUtO;8)o4LMeuH+R1@*)0Cr0&h@+jP$#jY~s#EFkZ4rTti1rMo7%P?SGW8 z==R_JTxV5P*C!so7%pF|z-eNh4JNI201bO!=p0mtg9GF!|-tMetBVVB9|Qz>ta zh~MAihJF3&L?QM5uqD%;jR_1J9EbFNYZhIxyGZ&`jpHAs9IITUx4!FT zvWAjaGFq{uj6t^vRf=6S?$L|wJ{jl*!1XMY4vo5e8?!WVnoL6D-m;(vZ;g88FLtX( z)mBhTUnU8`Tc)n7vw%;oI54{S+>!C=KNHxt%yHdWw{Gg?4-yyf)lJ=s6yv>Qn%PUCyqPrLePXZvIlI-I}Kzgk%&@KTTzDE3w_OrXN-_08rKpRi@L>J zhS~x;*kUS1UHYmfIWsbGPIT??1Fj=2u?tGCz;Rp-AY1rVu=bGPcJl0%n*z0Wg+_H;gu z^Jo%Atv+=i`Z>`KucAezsr5o;t@F+*(8kdOl}p~k*NfU{0{HQAze?XM1Coon`8680 zco_T9<+IVm`%{PKpd8LDkBMP1 zzfU+|0<~sm-{?!pe`&drU~THw(6HLv#D;Wd*ykg#u=@4iaj6~1q7Tz#BDskxXfK^F zY47D?_1ms3o_DLI4~QVChT^m40i*m*RjcJQ=WMrZcMHV&-9vv4xJ{S_xC-q}Ggz(@ zAfX7Qx7<&(og(#oEbJt&NRa!X_kM9Ye9KYOpp!Q$2(S9r@R6jfn74n{odgz;hVLI8 z@^;1=4)h(>P|65d0_hlCQK=GFeE&GZW$(Ocn1ecdIWXv{pwHoJ(_ek_W&SQ^Pn%~tL`awX&@YO zpJqoZwH9O;`Fgf%%2FfRevsGM`X3v&10s{ia(<6U4%Q;1Z1= zpqPzmLw^^mJ1|#QJGwV~1Qa>%wN~K2sM} zlItK^V_-%k0<&Vh0hFYf1B-xt(U}y#^6dlSvg;&FOWI|_!Z&Be_$_D<+qfDJW-Ycz zt!FZGQ~&^8_6<9+W*~Te)s5$|EkA*~P>Isgz73<-zTj{CuR|NI;L6fIL^aWib%z`zfq(s;8&50E3h; zHL+81eU7JFKNM05+bz?Fh73h^E{h=i?`;f=TLiGcZuhD`u&rd4N* zCrdenCZPs09$R#M+(Q$#WOg;!xgN&sRssA!^T{TsdJ#G*L&NNPH>cntNFzmEk(7$EqB?QIO**Sjry>d zUEW#Jl9>N`-{u7hQ&dMY$i-i=GF+Ucg+Jo;tDpxZKjY{jMl$@BYr&>OfTCW9^%-A> z;@*coUWVX&ZPslIGn3(lRVKZ;Sm;o(x#1y2r8QD`^Us>hPSO@zFv;CNNU{7bXlr?U z@{d+!Py;b5zG@9n6h}@;um%pG#_9vYB`>ol?TqTd$_%C%0mJT1`;HTURp%{Q>!L&t z1T>PxHUV2`zx5%>@c>(1%Zl9pO|y{jI7npNG=tEHnM2*`wmTe>oC&^b{T_&Y%OMu% zYjVzyh4rt#eqL7cXJ*bT@?{?%7TV`bawxvF{+xFr$R;Hm!{T@U$I8gxtY==(Fq-y^ zb_s$k>#}=20O+&($jlu1fKzNR*LB#m+(t^k$hje>awmViB_9mt(goNO?B0@WSSYHO zxp0C*JWF*ri^`YinDkE-fUefna!x@6Q~ipO~LYP}_` z@}rSm@s(%tkH$X9ySAkSY}!)YcNnEfxeUBVWE1adn;PlV1!5E6J&L0Ekzy@A%utMI z$u^J_Lk)_!{**-_fWE_3Rel)=NG9@pfFSMpoQ`(pdg4U_jDDav{%>^>Vp0%51yrQt zFQ@c->k_o?kGNK_P!6s;`#VxtFI7((y7b98w>gKeHlcyaHvHX(S#X zp1VYC;priJ`zb;lmFoCfANdf2t}GZ-$n(x3t!0>&VT1jt88o%h$Qt z#`t-MGvcCEeJ;wZgAVb`@1Q>bxnDFOHq=yeg2rk6@mS?}TddsSp56kQsO3VGb9#dd zIIQx3x@krZ0x`|${mV!J>Di52c*Lm-Z`G?K;7;Y&X7EzlcVN0eERdCTs@PcuhAkY! z2C_QN3N>Jv^0~&r^{OKvE{&nFmD&}fJE1*m6>S#c>V2S|<_1?C30ou%{ohRM|C>*~ z<`B##>t#I>QS@*8gv3gGF^nN{*<0cR0tgQu^n~$WflXTp+)FC6{ z#%ln2&z2B&EZJ*2+Yd&4CXrs?|0WmFqp#Si4`m~tO*5Qw~IbFfUZ)j3!1 z)Q@EfBlf$R@`b=N0+6W+{Ut_ba7H99$IX z5FWZXtNFaP*)~&E(0!q(DLnbLi(XBMF^0V+aY2s!6(_DsUvJCF9Boy_{$aPjj6TAd=RIp7NOI!EJY+c_0@Cps`L-+o;H$vh7eBw=BtcZd`L8zuHCA5`2UNf? zMD=d*4oo0R7TCAw1u)ybm3v*3LKCeW;9VlQ11`O?CE@{=C1QS*EY!NQ&fIN2vmFVh zeQOntRh|$W<-Z#k?}2S|{>`D8Oe2RVhhH_Y4kYIG+yv*Jz0f*IYYgaB$j}5< zul%uS8L%^3payA-|KmPFounXURBF~PPhOk|If;c z-!7v6kChik3r7EE2@r_O|5*Zb`pp0B5}odjy=md ze$refI*sku5l-?mUZUh(^$)=(Up3z&oj?vws8VKM!B<_mM*1Ra3UG>ER1xVHP9FW0 zrVetCyQ&-OuYZz`X@j5k;Zci@`@c@#_Kxp$+J&QoTu%!pP%DIp$D|S~o6e zJDz(HSl`#cw}Ri2zUbq1aEgXzYn{)#^M=`7t1`J*E>V9PNC=biXlRDqClLX6=3N@oP`fD^V} zfguxrd=fnU8VzuYwVm*VKO=tI9gJ}pzis=AKbJq21AbaN^JmD>k!PylW;CDQbh!w= z{E_f^(zQJYCoG>Ouk>;LcA69vap07;p2s)-jCkNrFvd(~U5TguTz-un_~~S|AGE)Y zM1h-$KWRH52)?}jF*oVjz+S9~(Q9;Gg#QWH|F(c#qYTW_PHAbLY?^8$z}LoVPpH1T z9GIaJr{+4-&bvCin+&~5Z)zSZ2<6!$ITG<~C;omgbK56_ub z*tU2V(B*xzp7!WvvwHD_L5svEa4D%~h}~kv9fHBd_RjUO&)cY#cRJ)f)EIPd#a>c{LH^&>{SDZL(A1ESNo%sP)(ZdTQfmmTf(;2 zKj>w=*A(tPe=$8?cPhp};oLWM0n81T$wpDUn%Q~Eo7p?b3xh?P(3qRH-OZaPxI75@ zm(S>ch&)T*y~VYXE1udYe)8_!=T;+w25ZX=>#nn#j;e{bUQY?=EAZ&&;+Yw~8K^w@ zDZk^2SBMkxuC(IVBJwL*e^2JZ+V#2&XuU`Wb4HnV^IwVPQ80KEABD6{Iet&ZF>P@D z`?jbu$k9YfAtjn~-D6MBo-}8yq zHkhwo*Q>H~bS3VL^mSV;%)XdD*oUF~Riikf9;S1tRt0N1dN?RVkg}-D=4=VbjQ*Ry zX7CK9X@!rXouRLCfkj0wbEPt-wky@%*-VE12k+B`pAT*O(%vUZ!6b*zMI;Jp#{2g3 z4EwupYd@>u#gTH#uWS_fksA~f&UU80x4#ofy%TtRMKFKx24vD%Y#^uOVNrL-uzM17 zqvPj{iXS8o1-gMSRPWupq8i2NHl!_za?m#sbKExTL-*EQ5j~I^NrGwz65D8LzulhcR6DCR20E@7cGbuhIHHOlkG{7r;C%3W zkaxn8*fKpbh>ppm3=rP9u9KU$x7TW1C&rVmd^>-}IQ^@XE52SRLhF|6p1y6@gk{8T zm+DJ!cgO!9@9z4#A*&9&G8iK1Ie%@0NJD{O4(xjxl!ktRATzJ~`gLmFHfm}t(2tJp z(hcoo>Guk+=~O>|H=6Elmnpqxv_at_=1iK9yxibf!Q{1-Pm@>kTD_TxewH9EEmL1; z5nno&@vu9lYqvW*a-kuXida9TMt_q3_ayTbpyXP(k5J_ zxtqDIJ0`k)U^xK2aodxH{GGKyu?qgf>$od_g7w%*DiA(Q#rKZx?=DuJj2+47#9o zfVbAT#skf9+I?-pOb77@V)Y|iIFkEL-|6#=CBPJo zA9E(|9!2$|P>^wP3982tGaae`Sm75yCVtOYGFw)8<~V}iHd*~dVBhT5B?wp8QjPu3 zNiNr)`tML(Nyg2Dnf#DoP;aP>_R4jGQo;MNuR)iGh1{R?mjaUMGU-ah+y})}t)G$B75xr-+#8rL*!PA-hJRc9$Br`J@!)61FteZ;n zyTaYBA>l}#TCU&s{OlQm1ahO}#w3q2@tzWh?PX2TK*;(mdm4yjGCM-y@t`*sfi51{ zH-vU-mOHi8+Q!_+At-ozqA66MS?}|QK69Aano0ZNSIT5emjM}6UkARTn34UgCtW#} zuEW&{`PEUOWwI$^x;aV^z=zaqgTC+l5dM8$<%qrXp!ck@UYE<(r(=D|C?vjoJnL2E zzWN;03*+NXqn$J3b5UH8-yWs#`~BVQL@t|&^!4!xP^M*z%$+qW&Z{9 z%(%Xbqh(K^=U;`_*msWH8*tmeL@yo4o@J-KTX#v|>w}0I_^~%BkR;?wNGn6DbN0Kd zifRMUy|jlW4{9_SFw?M*_1ugDW$_?wpMs>Q+|STu+o|4fl*hSZSV&wf4r(F{iW zu3cuA5v9W@6!`OW<=?n~8^mz3C$vwqwUKP*S5}yk&n*7#FP;TZf(N_2GrROVM;@~v zrNGdVucAjNzTOT5V6}J^fW9eF-%gt2uIri4@EJLJ30a>s7cV%}wANx2fI*kv6$(y1 zFiBGoPIfJp}#+~dp?I8+Y z0bA{u$KMMB<0A;wc`xd<-`!rD;=SzmHw}9GLo~QZ?8hN7=MSI`MlQxc0(j-1)6rAS z(@`Cg>aE(cQ5mXHbL9o*MfyrKb2j9MvS;a~4UbtvIKUg#X*8W@zVo@w2)JeINm*N@ zFP+r)x^ad+5Yh^{*KSUZ%6Er?#4r?4mu!BK)IIEF;aV&%s5Xi5Q8|Cn*m<*7_mw`_ zvA0F00;N85A*^%-gKXa^v6|Pm`j$=Jj&<5v9ZWmNj zgre9rwsCJtoK8LcvAzA{8VLm@%6jM?RC$@x8M9VQGWk zpmY4_mGUj>ZuLmoO}N_*?_Eo;lP)k34k*}|8@)prUM{6V7S&^yV{*7BPuVEBr5Pob zl?dQOF6s;AuxPivw?7)EEc3!2O5w_2` zM<&iMthZ?A?U!dpfqk7#+Ebl5mmM>u=ueh=&l^3Eo`7%whl{XX5%D5{xYqXeda(Ea zK%H|xoC_Z&_)SNWt!KbOv=`u)P$fo&Rii6?{vnd-Nh~oTOstui$ZiXht>$_EnM-CSYxNV;6Y;9}D^Q0z2z?7|-xN$=4aDDIowHmgfLK^*xz1KaYIZ^-wv z%+1JKE_8LRI<%v{!+FW3WxlHR7^?wH0B+l3OEr=`I+l+%^}c!tf7RC{0ecHs7P9XY zmASX!6sTOBD80KuAjFvh{Mop5;dj+|wGZmO<>4i_kl)dGTjLx+McvkK=Z{d)tJNbw zKyd^J3XUVHc4?#MVrn#0RssU)W<_Rs@2ME(4dqhCDT%G*9v8Not^%`EBBdqWk3cc! zJj>^KpL)4Nu2(fQc$t|MVd!nL-{e^*hNMfTs4V9Z!4OqR(5GM8d-!ymwCBr*2^Eu6 zw1sf}E1z9rAa)%erP~X{))JIJwZct@GMpCIm|x1Xv#Q22qgoTnT!+g2`ai`ujD8tK zvHhT?XVtqRY>Nf`@{(I%PjtriM0CgG+O3Pb()2t$>AwiIosmLu4DBaAqEE6+`Q%U?Y3STpu4jA zTIx(S)I*Wq%^88rla(~ggCm(d_X09~xUU7IUX20U_M3@&VC=LKTf^r2GM*&gXvfB2 zyLrDYc&p!_m3NO$3E0!>TQW_3o{Fzc`l=WzA?}R~*zP>WDE_F>VQ>ZZvQY=q>~|{S zov{b=&v3V`WePK!tT(%1avxkQeCPCA3%HJh1o2{74kdkw{*z2isw}qJ2sFP&k#GNS zJM~&5Oz&o1#`_yBUgiX4F08qv33=35*Q*?5A;;fYsPM>suX>ynJKm#mr1i#G&J{`=lG zS-~f-O`~s#SG z7z{A&`rPrLf5tS2Rr;bmxoA00NQZg1xe2IA4@S3y3n2_T81MJjHDw(*u5oFINbBdb z=RW@eARhIQnL%!}`4$W2GH75+Jairod$Nh5z@24B|G-krwJYZL41UqVo(^(MYAVDn z52vTNFUr(Zs|}5QIS}8;c40BAqL@gQavOZA=w>#12&p7^k{ea}ynxS~?hBBcS7+=( zz%mX$=LE#=mAZ$0;5O(TA7;}>T!wiv%6*#b{PAFxRJ#vT)2WQJDYP#G#D{c+8`m1- zPvsiA8}L>rDFReOpZoStD^Sl$iU1q_vU7hT00j_a7@tV9-w|C|4_-wGV2#1=2neie zQc2nkLq=;EylcpQ+`Ji_H&hOpXo_&Q57!tYz-^-*_uQL=Fu*@$Q{06tTHrFFf>AS zCBL;R%a8hcqCk|~u+N8)ba)Ni2kA6KV}`t(OOa8|V~3PL;uDbyH&>X>U*rZ%h8Weo z+?vh}BezU|4132F^9Kh@EKPI*ucfcq&a!sFY6MNXp%&V)sr#_O%|&sho3>Xhv%Z%i zx=`xpz3Qy8dk2lk%neqs)eZxt=J9>v{v4hi2l5$yvF4j2GNXow_5wX*V(S6VMLpD4 zSehYR50REE2Mxba?cx0_vzB{hcb08FL$PklF(d~f3&}S!u?y(D_vU`1A+fiZ&>@$h z-M7gPnP%D8xo?X_z34To`U-2=)}h(S)#M5b<*+?A3 z?R~2GYXAd9Uy|Eh2&ASrH@IR*TzmM!xY&ErhsZOu85k8iJqddGupV!TnieG|r$LFq_HwErEk7iD7gzuCAN|3-VvKZ}z4|HLBrXL=) zI1jnij%WNvD9=(TvqQoc)t714TcmbU_7Af*9UcB~zd&PQ0Rj52Ab!C0;Jg>o-g;ev z;^lDl)!I8CeSJI*V6^Os5cLYJk!izw?>_kR#y-4kkYquodoa8ok$o?W=PY$4P033g z8^O4IYp3@Vv+q8^$NY%;~BdQNrsa*aJaC&N#mEQj;)u>j}|9Bh%BSRh@N7HqF? zdCHOi>HEnnwWtehQa4Gk(b`|AHnqLH#Isw=G1_?EUqKcN&&2Y64try)`E+!`V9oQj zE^3B1_8^**klQ-Y&E@FP&J1u5J!gSlX5KJmSyuiM{5ZT>FXO3f4+?P?(I%Vya0rci zvg#tmDCv{t7mf1!hlMIIH~7;$Tj#$Gw=Yc&)$TZ%7ufg3Wm0v5!6Hh$*d_)Z7*T0H z*I821RA5f~{KWKTTM2k-4~&(mmUwaBKR&uuyjzieaNuR!lRIulcnX=W&%CVqQSg0l z-hp*AUwmJ&@TN!I5XRhsNZvj}_3!h32`W-I@xwRS@7RcBb)%KQUw7%1c z$vcqO%~GB3bO)$q$7lEP0MZZsHcq;GJe3an1N$b8QP?_$4;FFjZ6@mzFSNoP9ZYM1 zT2Aq@v48p~SmTXoo{{Sfsi4a*?k7a7+94a(ZDIjWQXv_gZyw~T9>dQC`Z4#@+k=vO z0d)x>Iz4BOi95gAg?@;)fn+}HzA)%)SA9H)otyifA-@^>xh8+7FHDt8@_t$5Q;y1%U{QZ0DWk$5qG`d+@EAwMM{>-!>%^Jk|l|wQRZrNH3C7Hkgx*E zl?EQ$h^KEVP{UlgmZ?ePH!gwK*zY&8wR{n%?xEI6eSAg2Z|zHgEF{@C?QU8@OKi-8 zxhLv1J!RQ{{ag(VO#8~#JHbPhF?iU2fPUPTTsLT7!h+Q9>u?|zi1oB&iP#)Yb(+ps z>AJ2g`_XvVHF4jE&rf|$>r9wa%ztUG9~=>Pqs;{QL(~iq{Y=#s1A-?rC7H?0JzjX- z(-BmbC8$fzF%mIQiLp<{wTGwXOwh(PU1xDb;lpm>x%K%DS9D=eIK{hjG9d~4OlR< zCUgIw6y9kaNPC{uZUsp5E{qM=tev9;Su}c4#C#fEXhm4K-KtjA&N0O7+!Bo0l!BBj z2Z5N$4SvN8hO$8S?nJ#dCo%j~OVz0k8!L!_Q;7VZhX3RqNv}=Ol1Bd_*|*;Tl9DZ+ zil2-WOf_;RwahAc=b;wB{T6lw>Me8mzSyz^N9vYyYaI-osZC_ky=C$FqAQ`=J1+U8 z`SJ+m55sJ|hv(8GNn}{@ayGkW{P$@J1js3bU7WZ`hM?(mo?}Qs7j)%xdbh2|lKJ)g z=T~_IAMw@wy%9NJp;@vd>6jXnF^?p$en)`PV6%i+&98y-UcP`$t7B{>RuSCb` zb-`C0<}1=fS-o}7f5cUvRgUUCvG!QriXCTxRUVKs)v6tGIJTF}>fWQ-Zso|Fe9-^o z-rUlaci)XlT0bY9LZ;t+zO0sWwTz1G{ylj5A<8=fNMvP8R3>8i=)BAJmBKc&tos@E z%g0cb`mZ2C-eGD0+*)(X;S64A1D>yxy-i6Yb)mms*E7!m{A`x>1FFsmrda#xD;l{>izi7vb z%~N5I*S-&+{KP653TZ2IfB2LZTqeNJM>ajwo?3|;DxUwQxrko0tMOabz7TkJ%}Se} z(cEp%Nnz}1`PJV7>FMpb+nJASIqb_H`+N2c^Z&_V&jd(ZROY~#@jvR_SlQ_X3HLjL zv}RAF!~-GBiW%b4FPsU^gfDhVj#|{RWv0IlYYb5dHVdZeW;FL=y9Hp z_p7~*)!osp+Kpe3aBVMaGqCFR!CDQ?gax;4!atQ$8Z!mm=}3{f#v8-`L4fnEnC}^d zq?K8VUNyk+I?fsf;IKGvNT$*v?c>f_%V}hvUIl*Qp4`J8!8l2mQu!p0*(OEaO!qR1 zUNbZtgNfsf$UCsCsE=U7_2T>|=jWoVfOxcNfO15Z!;Po0Wg3ifx}0Gar+K!XG?++%hIRGDUPI@b#y$;`35!B}{38^2!lp4KR0%OR*-IW;u~3+Yx@Q7K%~ z2kLc6-d%4a%ISZEzk7X628t2>FEZu(MvY4-x&erD(?Q%kAWmV*qX!d-WrK(75guJ> z$0>j#p}xFxGYT&WCI}xa?7^AcoYV69M$WH*CDBq-{Uaf3)`~SfPWxT&cs+JMJW@8yVF%< z!j?a`ua>E}pCrs+wO-kGJyjArv5WCi8t z+~{xEWrN3W-~nd){!Ubwe&m=wh8 zAB$H1AZ(Y$A9+0aZ}Y_v%(=C1#cKxv@c3mZR_-mk*8lu=cm409sC6oB23K{aJAPSu^j^InUQ&4#MK}c zd5>+43irhGM-85Gq&?)BjoRC&bC~O^df|pd$^9ek*NNF%DV#v0rqUuM%qzOq|N2NU z-AvJ8^a{hE0MaX#l2cGE0R>S_I0 z^$}scm3dGNofqjOMMZ=MMh(vC5f)!6InR@gb{>hm8w2T;^M0iDQA#y3WAIICuR+72 zK#f^pZ*>Yqwdc=X^+C^{Jmlv%i=B%d8JeS2O&W#_R}ZP_xkt*Ejc)15+>Vrk8P|Qv z_j#v~AD?imXx@MucRf*$->h=Xm&sekgK5C~XDMg{&Ud7k6_}Sf^ov<;O6$PqnWBBL zqtR6r(4X-=YPrhoW_`zN%Nw8a49i@Ox?*WPP&r#z|N30hA~y$R)%DoJs_gl50_M^Z z-aDxJFy_A0eD8hZVvy}ZB$4U6ERuc=f(!6@ji9FE@5w6%X%B8KyuKvsW&$ge-$$8I ze{sfLnsYpkc>j6w_HRDvk3xH7E${-%^vqLENMv2J!nC@Rw%E=E!(cI*b( z8AVGm<5%$5Hava%k{Zy3?dUD&sYX|b#!z{mo(m*;pD(W6mcKsK6V{c)6nP z*d^ny2yx_Jd4rz29qrW>L*S;Jy!Ud8bQXDvEr+b@Ui2r4Cl+50}3|oVA0gXVwRfV{0a!E5)gYL zJzPK5NRGb?Ir7E7ZF+ejRG^9-7LhDYwHh20q&j*75o%1{S2oq2U_tbQ7HmB}tZ=se z*6A-pRgrs2r{~1=B{h{5v*uX7u!on8_f6FDUFS{ppGk5Hc9t0zSFL__*tW+MBvUM( ztfa1#Oxd46o>_!C#!hc7_U}l=ltK5zGu;9pMBp80zW0AB62KsYWv~gd|5GG@7sO5$ zdL+%`4Vv)LO=tO(cq{ph1hvbd+x64;5LL;8RtY*Ue4<_UmD;9C_+>oWfa>%9(N6Ik zNOj|B4UgyI!yc6un=R5ZHBZHrWGV$ku65L6JemTw)&*R*+8tvNt)r;r`(F_6da3Oo zMP3IHUFnu474bF+GDO<fInm)wr3eTw2i3Y%AcSyjkK+3Bb8 ztT3_kJ#m-oDZ3yjvV!-Gc{W6A)$!vm#~^7Wg~QKl9gtSwhRvHxA>!`@`3~r+uZ&a# z|0P9}9NA;r#e|Z8w6~TNZ zswuosOv-nYSy*?lba~>Q4Mt>U{f@9(ix(htP(IrVGA}1ghx7A@chYBs{qd0YkSD~Z z2-f#4R-?iW_Q28bHg4g`gZ>|%T%V>mt?i>Nm`Y!1eB2D{vG4CGY%?zV1?C#>*w15B z^Zc+`#?+AfH}?8@hgw2*>HKTQtxuURBBr#yT^oSixJ_BJbW|dwQO=$fnqyc#+OYq+ zK8#s{wBYA(_sgY!(#9))Ks(PQDBoPkE%RdO@%eaYpS*Cx6%ODrR zF%T+XJAx<4M4rn%EOg*CB0tNOM%wO;8(!u)f!O5o-{$^aoaXxO!Wmse2u-EsB@~Jk zn1p%8aVa+1DIeAvC@)_9VyxPoy`J8APbXlRhUIBhlo*tjWQNOyC_N=6G_2s}i4@;$tiK*4}HS?US!66x?C$+Km5e842 zvxg}tB6+MrTd=&b9&NT=pQ9E}X$|6`i~XEDm+bZJu#T5WFmKxLgi;4v1y*_vSfuDBpNrg7II5j&MmLu$GzE1)w1}%znC(EkSYc zt0e)#G<<`hBvY!S(eXTZj6e~>v!CCnJdq^y<-x3SpZAVG=0KR|lX&F+amjf0G5neO))BR1`_fE^2*{@6##2FUAxtQDWWl zu6hLEl5!d4cGX;nMK==dWNy)WSZ)kK@R+is4-V(omp>XYB)PEg7se-+YE$xw;c5U# znug~XDhVVXQj=4CvF)FYMJ8l)nm9|9mihct^3bJSs5IMS2{~Bs5;$WP2$|)r^5Rn< znhja)_(_50LE3v_ds1l(YxLIdzeWZtRwSWGTFR99pT<;yWY}7dyLQgY05Z<+`^Y^S z&=^<&NTa^%;epJ^6$HCBe*%>4eXO~^QQw7uHz3MYlX9n}Wu+Z=sn+@qCXEA-Yqo*n zU80Py07z&?^#MOn+W*Aj56~jQg#uK7FTQeVPq4DlV z-6_e9qNO|>O1pSg&_CK0pv0!|JxlU&$-lkxW=S>)Mp2h?56BjySzor45WN#wvho;q z=lO9bxeqeRCratpdUy&`Ba%-CLToq=huMU_AbdqE-;B30=V#dV=#bqsxYuoAbGN$&(4(-l**}T~3YF$v{0&IP4#Zw+ zBJQqz5*%A+P(iqJ*4|=%HfY@@Q!NfXJz90fKDsVzeL2$4a&>Qjnr&1ii8FuUmRCzP zybiV72EpvNbND|$={hy+K_YnFqLhgRGZ2=&*`)w>J?qI3Z++rF2&?CF0lW75yPK?4 zpJ4AY|3QOh0fZ4L_nBP4FKP5C1woH@@Pavxh2CXt9<{`y`EGBr(SCr@vQh>@U{_H8 zV!Zz2T(9XP&G20+7r);#e3~@FxxyXB2=ztjgZNatX({bDpbf3)RDp{vT6Mm1Nv0C8 zYbdRNF1<1)^cd*$?HU$n&|SPZmK!SXSEbbU_-hZs(IY>>@A7vn zXwi{G&`Ur!OUDKA*nYccN&4NQm`^pgZHLlQv*~rr=w~0iuju2gFwWP?KbdVl?{NOU zA;QfN;o-^8qmg;nK#62}D)v~`iM9UW5vJ6qlw^O*;&kPp#YmmOH$W?$h+ zcyqorwyn@&8jqZUORTei$@{e2y-n%5Y3WD#Ln*YgdTS%n3b&L*L)esBHpst%+biq@ zaWEP81Tg+binq?MjRhXiuX?XD^xSO;KIL-{sA(Q`2IJV&^_aX1 zNV`g%GGOJ?zrKaqT&7Roq%ni3gVx(aGiX#p0FNFidkmY@9010gS-VZmGb_MqupgX4F7MoEQP_<* zxxVyJS9-5~m(4!mLMTJ5Wi~`B;^J0NwqS-^nd_XzK(%8@o)OsZYHffm%}~^=#3iJT z2iD%?zQ16(0~`nK*kB^hLhfBe?^+$&g2WoU<{pxQ_RxXC{9?5pBpAyJ*vGAR57dO{ zJf7|rxcaePB582G#q0qxNr@@2157{nh6Gt#AO(y*QcI(Z^PrcQ^&K)wrQRZpuHd z83BYnz_)7!)RSfO>F-(y1oTTQ)IPbSsqzxP?zHn(&U^)O^VU1(t#`%Ur8}gx251DZ z=*t*hFq0GzQ2-4FHr)&84R*zaSP62;@do*nPkB%ohZDcg8faRNpPDs`eFTv4iiHB3 z1gl?t#&$6os|bcJIjZ(&w_?eRCX_BVyawG$oSJstx8*_A`}aQ_AA-%Kev%|qAI~jZ z=~&~6(mc;8j~4=r09GPZdRvBG2v;cYXgyjh2K3C~QhOh0s8j?(kt0@r(2^UL6Q`v+ zAHOv_mr&04A`OGFA$~TB>c>0Hh~1cr$9PFHQQ7_2N)8Jz!s~Uc`?+q{f~P1P zKK+zddr?0x_sm8`K8}c6#X1JkxsD^fizA8nAbR=ZaxdvP6D+1Z)p8AwB1W|KiZQmO z-2K3ptYfw?(_wK${G$h#l+r2Do9up!eKrTj>lZ=Q+2PcaJ!KwkK1jkODLX>L6(2_U zfl8C=08PAg*M*XF?I}Ac_3=w%&6NE*?n>LFVf3&>%_C%J!|vFvk^s66SweU~W*Zply_GoFFj#t-7YzpwRid z(RAYhFZ683;;TwWNv-C2BR!BI^vn3g-|xQQ_3^xNx&2f>(22%8q*xxMXwZsR4>6~g zre}CrZ}Fg*WdQ-uR1U~~GY|)r6VA3O@4cyc@iOeCj)U7e)ZPBb${47P zgg2SivcgU?&>cDr=ccvOE3{E^s{BM>QbS^s>lkAJ;%RU5L%Fqx&n{W)Y2^oD$hAV|97e6MerUSH|zYuIQs~4s3z*;XVrRb)+eeg!#$>N|z z(|IJlu0`ZD9lMTaajqSHVgz7dpesH3jH}IT&I)~}>@u*zL{+{+e~lLY(FI(}0-Ihu z%FT*-%f+t))S>yh&_~Tj*$t(AO&RKoHpa6;qlTiBoMDF$~z^Hw!!<##a_j9<~&wQEz1h$56!<6SmmKy86;6Ro)+W~m~7 zkVWKBOqSh1E!*+>NC+$;$}|k{D?YG^czv^U{kTb_Er7(tWMHM{T(_T{^qf0@FCJBmEL-BTZ~3DK&?(T|Hr8)7l&5kfo_G*DpylHlC-+7o-mo1L>KSu>nE`b{69Gs#!8U{m0Yz+lHF?Z67vQbu;|nyBH8j~3!r7+bFz(@oNZTvrnktoD79W`RWlkh#s`T85u363NsRBJ} z=678H=_4aRVNMWn>N!bJPM=xv8^n=b9OY0g(0P#oU=M!Eiy)2cB0D$9^%9OIJYO`^ zF9~5gf4{qo0(KsPIJ+zs!A>D+f$zal&|e*!h*c;4I5~h>t6+)EZkI;{{D2=BR~mJ7hJ^|z?Gi~L)oyc5#1lkC2L@7>p+q9+Lp21h8VOP0wY_g zlHr|s^8qkg*9OdoUX^JpQ^G78)Ep3gxDqe%T=kg+NETStOO`(lju|iXr@}XsqAx2Q zs|~;RAvPRkfMe!$cjMIfkgSoWh8PpFI%U(F$!8K`hL{reC81#O|1?2!^nsu;46Fj%WISh6pzjPNaACM!3_zdI{eD4v~ z-}@^>36n(P`;r(+oZ1$MPUXo@R;F?$+ZA2sSyC;0Gb~yn-%>>aC&MiJlnX@Gx3zJ* z#gg@JEKCrmR1M&f8tuh52hG*cN+?cvRzUJs6UpjzLNrmK$gjN)cqi(LJT2bB^IIS+!f*?;e2oq0i5 zb&E9*5+~0SJizwN*)Os#WhA%pdW%sJZboscB{pfbzOL%fOyKG z5LdPbQH9G=dmSTRQL#UJy2qj|m0CT~m5}$!%3!Z^eUvaV>`Aj70FKnGu?4oNC*jhl zZ>zOb+qK3{8Tep^xRfHU%zk^t`-*DLS?$xOPm+6Gm7;SZYL0|xdFl_o z&ORd<#`ZIG>F@9X9Tzh1PKc!MBfSIyq4DQ8#eMrE2tC~NmWmOqF9xUE0q{8l{9OJcEp+t@-`LJBuRJWP+V+za-yCM+vYg>ZEwngmF;AY;KDta z?)-IS&@-Gw7ZKepDft^B=X?}S3f%QK&4>XZhLh3R& zGH$^a4H<*GAu7`rtNwT1Wf#VcVk_$|ora3^HTaGldk)Q90xqXp8FVzJY3_a6@9DWS z#Ad@**wp2D|7u6JPU&UCSjpK~wgI5U$}10i>IK@P18)?iA%7WshCV6RZk(LxfjrDCUaX2k=G66?;LIHYM4@UZ} zpv-&K-ll{J`_$CBi>hhrMwfhF?n~LnPbqN{;#NA!5DQx9H5KEuuxq`IrUOA3+m`t0 zi@B5&eYIwc$zbfRBy&;NOOa@*vuKJdN|9F*BXzI+2xJ57VjiqX&EY8trusHQwz{bH zk2%_U0&)EtfiZ_df|@bqhYkpbZBMEHWN~0QiuY_)v%OI0SzG7Y0jzWBEpSBB9N?&R zz~1jA?E-<#-|5xyi+X){lgJf5Kqf$u_eGTvy7ExE)pT{~&)!BVW3*Iz><8T9;a{Cj z`Jpyz1WJK_SIS#~`?@4}GQUh*4~;`~$9&E~FQ5(K1wCQ+T|Wukja-M0xWV@=Ou;C* z(YeJ2m>4LLs|i-8Wf3`@9^MuT5QqYWl>nC^|D~EhOxS~ZDHUe@b%m=qFy3$@_T9ID2~VsKrvZ>wX-YV74bayuFl?h%9~nS;B{gQliHEOmHoINL zg}0ZdGL#$FS!7><4)l$>EWsdPF-ka*HP~x=&a3iA)xEMJX55h}tieVQh>u>R(1YC& zey83q?)QDR9Xmn(7d*XlD_|@^+p1)PY6sZRV(j2~k|_bH8OXOtVy^}b1CJO$5kLu#~4eIAI2qHNFLec+q7}nV>Li~YcS7CA5h6Aofx6YqGl$wQK z`(^NjNh~eR5TrNL7TEavH7^y1yw`A{ecRSm+~EK>eDH}Pv?VDvwMTda04ZiX{FB5J8H_hVvA~%SWqL7wW1W;ZQoVcAQ~% z{nBE0*KB`;^#DjG>=vgGprsmPR27228hfY%VU2T|7OnnEpvm?~^u@K%TfG7cwQNq0 zf4gG48i0_(_+0jI?e@BX0~hpBMrS`zgKO^HfOhu=>~Ht|pDI4Tx{F>6sz+3A#rQ&ZUq!y$4^dftp4NpoL zF8_TzE0qMBc1wB+vfs~;h}9+g?hodB4n2pU_eIgVn+EZCn(9|2n}W*iatvts%oGB@ zAvS%oclB@k|MjOsjKE;ay|f2TZF!qgF7Pe--)%1fv%8ZIxlY-i*;O&u*DcCg*?|jx zYsnmTjslkwog_BA?M=4#)lZA`Y(EPsGkh)UBIJxAgZBs76N8z^K?oTYD+&3Cw4V;O zht7HBM+#l7zmio?zOl1sa4I1PQ(|zZU7BrI15g_qpKBTJ{Pp7h_G*WK?sH*zNZ5eb z)@___0#|iTvBDF)|9TMQ)S`knBS3HXE|AcxE!9!;@J<2cnfS#=hCC#*=dPYlJf;Je z#p^>X;FwA7S5{9rdQWtgV(+KrY_*~Et31{g9T*R}k7uP2_~t|N zyXfVP1vGQ`jVlEKVT6vHx%U~}1~B0KYZUCG|8V!g;AZ30LbJ9u(rIak+IYD6v((u~ zk656j$sN1>5LMFMY=vk2#IAHCqK=OW8O2ZQe_+w;=n{2e^;oz(`(sqSt1ARX&ZtEc z%E703=|NmIPRvvO=R@6&0ng>2`%9^5-@2|t=b*>%-0a&~kn8z@pIyZ}ww^7$GaUnr z(cOTn4TB&&4(3!(2+(txmBkBn|LA(P5XeO;o>w;0OG@qQawq0_|2TCKfKFLpl!Ys0 z7wrB1aN7jvwsqcq-wqhVP>fuqvjq>m*A)D%DB5N_Vo2)sZc;YCu6t3`zrCiMN=Uoc zK13S~ZqG0zPv#G|2T5J1a@Q_xeShy9_)wJl%MSqbH^UG9)~$R6$aI2mU)r_{~+#42ea+}9K1w%#QDK%T%bhhUfqs=Nn6Oub_H6ZLz1DK4L}8=+MtqiY-f|!rxLDJ9ZYvr1HM|2uk;DNNuH<7T zZd|%>c$1WZ;=F0AaA-I+t;&t+*e^Z)`n$d4 zfYuBRnR>iO{n+Y{qNjT(NW}gxuafre4bA9c1&L=afn=?4LO@{%B`yyd1(MH%G*x5Z z^gMd`CUNmVX$d{v78jScYU~fH4jGc4=}C;+mukN83Mt~czsJF90H_(vC2$SAV3RaA zXmOx}&Enm@6PRSf#h#$lKafn8+oO{KVvgvLYLfwj%@Du!JMsZ0M&9bQ)JXx$M~;|< zie%w-+nJ!k1y}QQS0GA(Z90;yVbx03{MfXmwlGPHLoR}vvo&K?>UBL@gn9L0wA&@a zAj>7B50WR10xi7!Ue$h7)aYsaE&nE+bkr27NIemNl|0t7>SMa|)$?4rPhPY20$_zt zI)jZ#9SeP3B`XINqc4Mx)G^`5m3&>@d!UA{#_7C#>%;_me9`DAXZtmWy6nlvY}_<(s=}}f!nCdt||H{*Z8-4{rS^>XeNqrfeg(w z$?E_RViez0xcYn)jDei{R<%zG$49T`o9B%y?c;_OUIASI2d=c%3^lFw?gCQpet+S) zP}w}H;0H)Zw4#1etO*QPfecF(eyswMW?}0cqTvjXFFBj2WBXvdI}Q+%OttS*lDyy$ zH^EKRDU0s>s+Ke2izUlAB}Q)r;Qeb2yKtxw9{7y|XuPGKZi^hCHnxd7$<}weNTsi@ zDD_21BjWYT68Q{?Liu<=yOV9as{LSotgxqE_rlu42O(188}BWgGE`z34BXe$HJ&el z3K4XuOtE)IZw_1Kqv%^S+or3`F{*~sQ|qP;x@VuP{8TS+>Z+%QMTj_khi$?U0#~d@ z0hNJy>?}6hr2in2?b~yvqw3@%&ORyp6%M~}Ie%H--%(S5q8HCZ1Re0THjttpn@8y6mkV=d}=uNmPW`VJ*n7lI%*+#$5z4Ui~W^ znlS>Jc;7s~{SYYr{+4LqzS4Y>R?)o&XE-nF)A3oSVyIgnm}Z^-558aXBQpcKE7n*PzMfBI->&W=nf&RhV!Leyl}i9Hbu zb!0BMhF;nT;gS##8m^BzhLb}l885x9f-u5Osj>)FbgZk6*m7F-4;GPXrH|+B0V$w{rQU-Zp#jDhv2ULq%jZG+505Q7J#*dtp>5y4(CO7W=V8>(|%o7DM2f zp{T`cq{9`yr$S)HFCi9o0J}Up02H@^E<8RGKC*<*1CV)3tSlpU5P+adE&p2orNj>&pUolMtus zpf+5X+3nRxzL58`%*#NxCx7k(TBUEGY&fHUno%a9C~OfkWgBK3OnNL8h3c_4xy4C) zs?*7d%tJcDuE0v6Y=B=|?8Dx>0IEp_dG-L(KkC`P7RhrM)P3`Lv-Wn|{ui7aiz&W% zKyNqj*E66oG|vC3((16YLy$V!SujPn&iJBoQN}f(;t{}u+9#apfr${Yt+m}zPy{3| z0(O=$Jqb7e(h9tE$Gj_EcuOQB{qAaI3?H)Tt*MWrZioFq5dAp*dHykr<$<{TPhLT?Gh-3jk-#zPa) zSH~C`MGiar&}3I$`&TU5dRaOLlF^ky(?@e#c3ElR!fl7TT_9Cv<6nX^wHBu_ghc2cr>y9{gZ><^{6HT_SxdRm%m(YoHu`0U(~T zF<2x#Ltno59lwF&I!=nAEyeuAK@F})JR7K2g<)K|lUKk2LmrNKu7g;K$VR21w2r~9 z#nfA=8kp)d-Xu97@)c(Vk3_LVGc@}Mjaqg*e-B=I3<#4z!M>EaV11GVWk^FF+wH>brzFf zY31HowpIgF67PkP%*7w;nzug~joR4_wa5xvY2(^8M>{;8=S(-%v|a^pV<_GsJvzQE z<6X%6FW;~p02t%mEA536y#j&_Jd0lcNRj_~4F48k{uLLs$roKb zs@yvt)>q5fZwyB8QGj`^nex5ZZMmz#QD6fJ!2M=i@G*u9^Z?EqGVQ+pC1#XJ;8Ke% zW%M%CnkO#MkQU{iyqnIWmh=s~st)>WeHRm{MpED0cTTYPb8kw3dw^N0Z>z&b6hbY@ zT)3?4Te%Mx2TxZ3=wFeDab+@lZv-nlC)m#UNv6LK=ad(OSlyikPRfu-1+(K|Q;Aya zor7g(Kq7gL-=v`#;vXo!9IA>%gAg)2A?o}TqUg4ChQb2mTb4g@RU?+U9@2m14iF9O z%tMi8GFgtN2Q>}CuH;DJijo$4exd0W6c5c?REH=sGG!3*n}I6Ca*jRic*f-1Lb|S6 zuTKshMW&&LZ5Z}ZkzpmBU|JvAT(9_txKaf&R>ghBSGG+4xeIuGuRFr_3y=~h0O_); zc*!394Ss6MV)V1)Pj>BgNH)hc_Xtk=!vi?laLAuM-~@n?hV{1N7Iz?YvuVvYXP^Xc zxGMRHG@r|mB_bD~`U3qJ+8PPETpG>}fK@uU=1P;UiErSu9<~&LS$M(c4@MBq7wG*1W-BX8Pl- zqe%@-9R&wo@&Hi~&fD?H$cT|l+Ua$|7wS`&lz6|ea_-Uf3y}ETTKo~`wT7<< z8%)Fdh6F@~)xmZ!=4gEE3mkuXdi&_trH#mF4{a_sxnP0-_fH~+Bz!vKti=Ko)^1Hp z731c_qh9V8BzsTHDFZ+0F{^DlD*hk{xQnJ_&%N5zcwhNr4!Mq{5Fz3QaVNvXblCk3qu(tzro#+WTrfyV-_N4~3t z6d1=Q07ctY1SyGXKGg=J+Tq?CA1@nT-%7^}WsEwiPA#8D@fKLC+AsliuOE|ET@Fhc zXU0Y58AV%CBBA`dvFyhUoAf&DORSD!M-PDZcx>#m+4y}eb_b~X!ujb=Lj6d{bKcDX zx&Dc3D7~woad8vGnQmp6XWIsK4@N`h;6#tb{?uL0e=`Q(jJIVlcu7vuDh2WsSrT1T zjRJ zde0k%)Ca-c{%57$#q8g8Oofxm?lAq0{QaBR7vcJ+NPG zw@~&vc50~BDxhm3ub`u0xh!I?HcXg*T6=?B>?PL6%yxH82N_OPok7CNi_#KeG~+4; zXZ$E~P#z+Oxjb(OUqG5HCkL7n%;NIVcWxQJSURJ&Yu7G^x>lWrdZcm9kWZ^ADSUUM zDVwrQCbG(Ix^~9@Yz)?Y{M4jVWL^+VU8z-a^|UtDM8FO6{ixat@6GN%{pKKW3i%}| zd-SCM59>m({IklUhl`yQiYIRDx?>(HujfzG$GiDxzOhJiyb{JctVT%iH>rVS^?U`C9ng@6RBL}i>^jNO@fv6-+d zd10Ja*XFYLqz1mR96wT^y^-2Gv``rbR*uosD;i|xin@3_s>{v;OdB}imrFYH9h$!; zQu!(^Y2Fyl{OZjrCf5BGz3<#eO9j^JPN4SL-`B0qdZ0;F@nU&9kU1BC zy>8ueW(X+7uR~4hHD-<(7}lze$|VL4U%YfwS|!QV{i=m$(0Og*sPlp0x`4wH6e1`Y z0Woci|ApK6)qM|POsO|x^SPX&u41Q?0zuV&43W(?qWit^F;UQ=I#C3vjsTnMYX+76 z7>@&NTpt(m^mIb@IDiKwXnm2vVJ=$}v`lK3o4yX!UNp_n!-|guEd;gRQAhP(VZImGyKE_coSwAXM*-F!>X|$qbl! z)GsBzD|oG6BBgL>=blw)^hjn@>eV#*Q7L4!P&>+VFVzEEEd8k}g1O+SJUU5KuKe}M zNZ5~b;}`>SDrf!e1Urt z2`x|f%X|^}HJ{D+X;2!5qp=OfBcT_Eu4bh*qJ1;bku9&Ha*$ImKADbNOHVG8GLWd;8eIBR zM-JnDO|-@f$0=H+hH_f$>tXGAvWzS9xMG&(b`hXiNS#tO3GQ);L_tj^0n0bF(SV@k z%OEQ2dTk>%0i!Ku&TM`y-=aQrgM)@as9B(ckocOByP3Vj8ZPgqlH{GLE*Yp_Pi>0& zj=OA5JZpBmW=^+EKd`}cyzITamUY0G0885f}(<`|iPbM+M#}FIpJ&^7X?-1F(?Ek8o5v_AdY=2>wgiyls)2j_d;j z2}1omNCS#NERfD zz}@796Xoh>2G4QaJ^HC}rXfUvudEe=Zzb7OSvZsjy|r-baGg$-_?v3M21oY zr?Mhq3p+XVTGRAQlF^@hcQ>kCjL*c^^}Ga53t6Gf`v<25P2V^)PHwC9xXFRn%E;q^ zK>1P}QrKT-2fZ4m>u&M4k}bOy1Id*e;-A)w3I@Vj6L zfysRiDWwla(Vc=|_WryO3ys)SApCWS^T)iylmzOiu1e0R}9Vgu`1 z(iA}+)}FE}Dqnu@ej!T|2knqY<=54UXIIQr(6M?&c3(iyDwFmNS>H{eOJaJ))7&KU z%u&-B?!+11FyxV7fkg{5#sS*l>}cT^$7wW$sh7_?T#)vG5QdfH$09MBc_SGMk+Q>zVwtW!_RByzaHGvHr0^h-v#)e zJub^QA*0IhxCvr$SB`$vII%v%gshUPAiQioC|w|&a#TYz`kePcVzGG~&0=E`hZ&3g#Tpzl-J2IoW@d7YO5Ype*(Qs!F9k*NDYy#rGhMcfr)Ii(74kmV zF9wypJ5e~Kb&0sP>w?RTZGSFqc$XBKSpW7MQuG0y+hsL(bZo^>irL@90ZpnC_#Pwg z%ijK;^oRTA0D^*Pqz`%Iqiz9f48`h>Wd|~mNM_a@@k6y9${iNqV<&o`11<}`M2~RX633Y(BGnQiGOZX`&M<`{(J1_2ys)&A-2gJBZ9Q*#?^=4 z5|kV?lL}JR^&Q%hFC9&f5u<;beoRZ@+tKwr=Puj>#itr*RT42?Nm4WET_Qm<=QgGG z(JaJcyMY|Wra-Y5EMr~iML><0=X>MS^m<$pt93EOqrQ2F9^Rn)ao;0hGSHG)UtRK4(}rKg46 zu9nY|vB)1eMd)#s<#IIkPQ58!9oLQVUAj>XPqRcR6}}L$yQS08M&Wt1Cu@x~ks`=v zKXsNA(9?O;c|JVdB<2s+d;_xP&&Kz+t(gY0<}BsfBfy$vprqm#DI7`wILlnM9HOVBkng1w; zVenN_RRqBq_CC+8Y*8&=A@6fvy2rJ_du!J}Db7EAZ>?wf8dN_9@9&)HSnlWb%FpPv zdHqz~%IVaqcAFH_NfB}e6|eU*F;@;g6#4^1#|b1RG~a;>W{ zt{PMH$_G15yX2iwc%s8V#jSpkY@<4uD#tg;HAUBy`B3iJ_njpk;%0#hdAbc^d?w#6 zBXwPQk2ot%i`K6EYjbVAETsl3Vsd^CLicQj0chfY<1SmR2d$Fl*zg#OX~fC?n|f&)T*p(H z8#s>;@OTjPP{xuwGQ`}+s7G7Uy%BW=r=S_KQnu38t#01$-~XG52SHDH zj8AYYo!Ep;MYprF0|{MsNbzp1jva6i`dTRO8Ld$Cvw;-B<7LW+hr?@}O>qr9hJ*&O zDA>Io_h!TCyjLOeYto7|lW>fle?|2%f56XVBx)FbS(#2~x<@bq@cO0#-G`H+J9l~# zn`In$k8`Q2u`7}_QZ>*q7I<_*g5P9mk4J&5>jU$9y4Y^oQ|lqrCob2nOpKIkpMUT* z)1S{~#zq1TUrD~9#cKfDy)yRXExyDvo<;A9b;#=zA{Wg%W>-IQWDkz|B65(aBa{!} zZ7itEa_?GGQX?(eP8AtX#E*hBw=>(mfNg(NCnsj?X*+_FgPeNjgSb9Ot8Ck}`G22Q z#X~d9oMXmUw>OM1*f3(b#tJ7u-jE074MJ~A(M5fr#2hG-ovwMA@Jez~xq(!|9l&wr z_9;yBbrDC(P*Bn6hF27kdBZ@4J1_iQdhfEnt8p@52|xcF&^HY-;eqil2dEwB*U7Gv zCY0P-HxreZ?or-g(3d-ZK|f0$D$V0$iqDufL^KuM4O`?#JbX^|#H427(E>B#Qo5UE zh1i3OY9ctRvt!YPxkXScSbhbUt7YcqB8oUWY)o}X1^Mee4G|&7Xv}ro@8J(OD3YQ3n?HQ~*bFTX&2jbI1GtycNINpxMF-+hOthOYm3Hi$m ze;j(fcI?uPtr7c_^%cM%>`Rb8KA39|%NhlMQG)ay_ytLKygncXdb1YlAog7f>KgNlPtun(EH_Q5SXX`s2Bw zYOm+ibZ@hRVY`yHBH%rf!c9X4UHE=<(&R%D! z-F+HZzg^$W2+)pD4^qDK*xtrQp9Bvmax$g{yYfZyQRfYnxgt~zR{X{dVKz|ZSJgM- z4S=}Ckx$_46!=tU)j~Gv3^x=vsh*(cdkS63X2Yqesj*}fu^#S(Sz&1}kx^paP`rD$ zUN-S{7%M#L4yP(MNF!BGQ{!T1G%tvph2*qtg4w^ETNB9jEp%l04sP4>Cg-?IgxztQ~*373gH|~F>g74Zv@U5u+gWx z7)`c&`#%p%eAfWgVVt00`tIg=4h$G1J?>9XgpH)`!sL&q2u=>hX&-o?*aC2r;KcR9 zl}hPL5I%J~-2d{^%r9Pe^VhYYU@`aN>w(^^1~EMbwS2pQ{LHQLhvVTw8G+qhL<^29 zmIL=k#ceY2KA?}Y1ob0(+Br=AQUL#J@~{~zyP@QwV9~o{Yt+4Yz@y^zp`+lh*I`h9 zY*)=kG&q2))Y;Z$?CB*whi@k)sgGYka4WZ3BqQUV@7PrgNqY$`ywLu`&US*jYEx^5 zj$6U`4s?9r(KpL`z@omP0&XLv)NTgm38%QIucx(@nRt$5Lw=%7k_O->A#9YNC0+Bj zgw($l3j?9h38b=tt(E&VmYs$q2Gp$vM-+Eqe2ofo^{=J9{rsi;1pDo+_?4^<))LaB z8q+0zvHX3Xuv8SX#o^+M+p75&A;#Q`b~osV#ao`h5z?Jzo1^knMS_s@P75!?{;)`k5C*Qh$k1eakv5Nre7{T$EfUl>=6q^5%isy; zxS~(IY&Ic^by_@_=RT=iL%n;~llc4hZCwm|Pg8QQCx_O*GduI>x*WLVM2(k=zwD-e z?63;3F3hB_+n{8308S$)3%)Z3Ib{Qw0yub}LHqOP@s8JZz_lsL|9WobKR)UwP*}7f ze;a-Tdvke42cvb8^M{R?Kr?IoH8V!2g8gG|axg6wM2h)wt`vV05JOF)QwmheVsUhwlIQKH-5XMX;}Co}kl@K zV6@?Bs zHQ&V-{f~$I`?dPlTWk=(F2)^y{J~KFhtJqNsFbqOZP!-K^QUe5pC3``09H7I)VST> z`M>y=(1D}ur{^gD^F#l!?!Wm&NXaDMO6Ct}z<;gEKVCv7-M)KN%ki5J{a-Fa4{GD( wGhh3?kNTgk_`eVIACKU7>-_)cfiCg-78JH__SN?-*G1dV7gN^_W=;0 zU(CaMeB`;LCPw5$k&TbrGPUQ5=e_x5saN?r%egIyZzufttt~|`=nwea7i1w&mA}hY zQJEn|PxMxfutM3O`?u=jP8g4@-wQ-9Wcp4pnH?+5j`r1n!!l;GkY;k_nN*CiTxs}w z=9?*6&uXPE(Ie;Blq_DZ18XYbN7tzm4g0v3cl3|aG>Hwl{o;o6Z5ivLXM`7p_o)He zbU!*6Ug18`8K9z1i72}Vg4{j%?0CE1EoQG?f2F9T%J?Ik(uhLdJgrMBtcWHc?lv=h z>CZ`_9Qwk&s4)l4$(ygv6^-QQjTSbN6%~>{R9rp2KL2Uc_8M_}q+K^-_v_R51y0@9 zl5*@Ocaw#3hKTR@9=t_9%C80b4Z#5|McHaWd3A6xqs!IrxKw=2jj}}U;R|XnM z5`$UF;ymn%G1(M6D%3&gH1YH_0Xl@}w`vD8STo}Q@j0+D)5Vnq?D~a(wTf1nHV@Z9 z_Xv1|FPb(YZlUwudk**5v!!N##vqdRb;R?9xrqIfj|TqwPW+18GkjHa+xpB*+M%(} zD4imXR3Fu8Ucp#urK{GEqzfT~@>TIto~rY)4Jyhv6#;?1F%PPR-0wclP@cG)rffcw zei&%yD5=efjuHs?5`OIxwe4b5tg6qYDk@R&@FLo^Q&o5$db0E-NJ#Pqyu^3g`?6@Wmq8b8 z=&E?c*hBgGx?x6_U0Z2HSP$KPwz{}^wz(PLUXV*#s3n$_2tym`wVPG6!wm?zadB~x zfU&#pdBwRMHr=w3)%WolV9E}uRey3R+frSdaLS1rCf*`%AW0BDks#yg!P z^o!-!^=I^TVO2bS$z7U)d(5YiHrJU(?W$=p>hJp<`15O?WQ8!bHb@?rQJ8?djzhuRtTIijCya=MUm{#t1UOW1abT2?3 zB6A{nBc(jWzA?-(#Ho=CMX!<_+mK7Vgr?TU*T&W6O{lr@!}yPqH2H{9hU(0!?Vk62g=zPnUU$4sa~(S}I@&+lG}`s$<;cqC^v_)P zluIv-FL)otJT85fGIzi58}lc2A}%7;%tkKA$5CVVi>IF`O?hwK zpOToWo9eAPcCsAY9^_s#-iTb?@nK$f9hdvCpN1R~S#_09Yz}LV2#KC6(HZ$R^n2A| z?e*&Ru-nkq*zgd1Sm$R}iP1Yh-O_|`i6SEny(l&>!E|-v7&Cy6;IJuFtKHp`W7vI7_+et+Bs}opI4Or|Q+}Ti?L8 za=jH(d$~-LKYF#DzRj&?kCM8uZ`f^oAM+XGkip4-vk0@EW|3Ckng&dNu6|W5yBX_i z;XCP@wOKNif3$sc=ZySJnEF#hOhmVJs_F))9fYM@qfhIo z()ZCX)p&x`<*0l5t1j6xXEq0(?FYqheUK8edART?`o4$xrn$c;lc=njbIH3BxcP=T zrdF<&Y(}PG)~m1~(aUmkZu9bc$FS=9{iUHb=e5k`x}p8*^b(_@ammvCIUp8-t(!}j z;|f*`+6$t_ov&@IW^FZe)!0_`JGHHHTr~)_Mz(JKZ~v7HZ^BV9vr9!yF525(NYK15 ze*b;QM*#| zYirPZYi)5Y-EjhKEWdPD#iadyATxpNu1Z5mQklund@L$&9?N|*Ht%t%q#Q@z#Uu(a*i zNQr%kh{f4>eb0tyTKy|`69W@5OUz^DM~_?Do^|}7%Il5#Eh~%=I`X(RRqMSJqj#=1 zE<@^GGHKKJ!LGNk_EQYwm0zAF4&^e9I(=)bhhrm~jE;%1UrW7;Cad$xTYdqc5ape{^eRhtj(Ek1pze)( z{nlSQ*b@g68OM3PNj+`Yi1cwgym45p!HblUJ&^&Qj;@WHCpaW9bsZp!q55*oJN{R3 z2nHp+l#eB;nSyyT2JX$FPiH=}7k1^k%P|BjpHB`}77mmTc!d2*Rh(~{Xh~7)yBD}~SSbsY6`hT5o(vS;9m|}&n zXKqfe97Um>(K?nH&7ami*PJ#fw;FNJ&U=rHQjCJl?}j>EmG6Gr+UN)+xeTa#ZsvE! zzLs@a(>ur3IDMEtZP~9>u9P^p(u_U3H{&wcaF##b1ssd^wvQU7Bpk zb+B@DtW*17yY)UGf~mh7Fux`0udhiEMvZk#+x^ine_JK(c$|RnD(=m>`vSTX|)i!xk z8DyTCCU5|Nng7p8p!I-n2Veh$^Fw2AV?AAYTMsu;Yda4cdr^Nk&p-756#eD#NjH0M zYly#_t2}6vkr5Ra73YDGLm&{vXLb(q z57gBELmYpn#N*`c?I|xN=I7@p>L(@Y@yt<7LQYOjOk7e-Qc?t8LIfV*?rrTa;tuEi zXC?otN6j8?`^?$X+u6e%@~2*F8xJ3EB_5tX4gKfw&-b+VcmA)I+~NPxEqn*X{yY(r z5EU2u&${uVihpwD?>YP1yPBvuyWz(S-v&%VT3SZ&9|HfMNB`C2U!ul;iOR`||1J90 zqyJa*5#0Wns)rlCQE%9PDeOOl|9Ck=N|CIZMy4(|MQLLb;3kv zt8>JFYs{m6SLPp8GZ0e68`1rnt^Bjt{2kuG)|05*IqH)C)jM8eEv5e7^yx)wl=|E7 zT?(Zp4zqu25dPJRTjCZR|IVQOt2|>kMZ#^OC6>TIh1!3A$nnxnR*mxiwu=7I8q2kC z5|r4%fv@NPtkeH`J)Xf`nmGZlv9=5j zPq#7sFYNUNe!NHjSLNwy2SK;QL*5nELG;cKYVm!3?a$ossfBn#w z>uh4ZQu(Y>@HEZfG;Lq@6UCe@{MD(X(bZYYRfD8K-Y#37gjO`R;p!55mD_o7waaxS zq1Bvc$+e%+rOQ>P4m(X%4mi}xJJ1p~a!Wej+JAjPTQ${6r6lK9aM}v zE)1S3)N?tkgl$w-D}|VcDu<4@4LAmjXGR1_D4Vz}F^%O`En0h!Le zGtAWmru6d2%0=Gt z_xNmch`u^U8;yVc=5m>&ym6#u5cu=pxLUsC7`x_Q*2^T&=IQ;THT@UNs6YYR!%rF+ z(65*nsRZ9VtrQi1t24*n+0vxSd3Dldu(y_Xx#l?gz1s4x^{NM+U?`QE6|`~#1ySYe~wC?m~}lvU(BG*gWFhw+8(+0jxRK*k{yX8tkEh{ zu=q%OKt+PMj%&O0LSo(g2gbihpV?a;*$+Qy+C{O`cy*Hw3uaS=1fc6K|Fn2evfnI4 zx^II-Iqn_HhAheIqfOv9*2%HbrMa)v+FJ^?Q^}!)O-mL{ESJk3mkuc{XALb4IONHf zQrwqO!#v!`C`0Z}yTM2=?WFO*~J1Ix98U`A)}IQOA-Rt@(4{0j5bzR;ydMw!11xN5?jHH~^)9eEf~lj@UB zb{GULG6Lebm+1uP7Q_2`ttHj7>S6l)@UBP1qdhr(e4Jhp#M8UwvJH9oK!uo*=Oq z2(ReQXorZ(aj#Lomb2N^n^Z>k6cyU|{pks&`2s>u(?d^3J0E9L)EwqK$nql_`i(BC z;gil#;8~b=X*qH+vfP)pl;Rh>seBrmhgW)N=&)??@Ot&MyJ^$vU9qWyRi7&kWx~e2 z-2Iw<`m%p2-HmvEcarOJfh#w+eD1->ceGmH*V~O&wE>T0xrVqIu~8@Ou=52P%V3T9 zW!YKp;WRsIJu8@KIq8NiKexwFj*>1pEH0`}AH4E;o z3F^I3H6>I^OrU^_yM0u8{-d-gMPi~7>D`Wu=@ZkQvtVNAo=@GL{+!Wk!*+DEzjfttdy4Ua>f$2h(eIid{6Xro>UoXPJC*4(#}#6vKkZOG{w|f z2TFhamxBch5hA=d>+b`-MJ<~Z$@GVx9f34y2k2(}Hx)3a156E1Yy{~1pC47`H3bg9 zbK%nBqR?Z~&>hlL(2J{fvGaC5GGB_3?-u>i1kbK~D-038PKp5~K%+ueIzj%^_zC+E z`nIY{4V?FLNI9#Y=c`q{6HdOdJ+{_h!!{-L`{Uzy?@$`@OC!YYFhcnxB8#(WyJbn) zA8+~aL-Ux9JHVvr)-vcb-q&CjCU!a>!k&(6?$BQS7?DxahDepZ;Vya5ll$jXvc_T(M<@7!?sytq-Tmt z`QF{MAIKW+lOk0`hTZCl++QhrnJ>IWQs$zlC6oPXOuoD!hbXz~FOW8W4-azA?>R0f zU$jR6t3aG*^AR-q;a7~YrQM1C~?3PR?)nBF2PLE9KQ@k8~lB%}$ zH(ApuzixS)M0;c)oYq-40)ByTVjxWi+Q?3yXW8FUNm{UH%^vh*Bw5<}U!L`2e6jn| zJN8vY8uQR_FBhv$`3#+j=(U} zTV5Sn=5@N8YYdUy({?D+VHul#vFWBZ=(R~$Rw7x)5nxafb?)YJ_CBb^zzU4wo~zRa zC-=1`x3bBKEsrQpPqw!u1q^_D>yQfxr86*^rCQKI~E%SC5XM}K*PGHN_% z9Os4`=P4}QEWF;do8V#Aba_~shxmGunA1);l45Z2>t5}0eQRx}n=*HlYI!d)R*^Ru z?5HoEy(LPPhT$bEIr#4Jgn?i;%cD7fzHxl}r^}>upmA)TzDLN`c!=B7x5SlS6K-1M zeluQO>-=H?JyrW5hP?}r z62B&wH>N5FU9h9_;h8s>V10f?y`sh5im|l?7t6D$(Wc`$g`7Tt>jPrZUzaLzp))^Q zPrQ;h;D2FYsOO&vu|xL8$KZ7X(KBmJ>odc zu|`T$WZW6Hbfixs~*&ly_}iyq0{_BGeOfv4{r>e8}Zx-P=%N z?F$*!$;Xl&r?XFuGEYMa6}tfLUzY-}Lh@RnjS)VV)g*}HpHJqi<{$1b5F(kyZVKk2Y|C23fPaEUkgnVJ3z|KcZ$=chl&^pn(ncZgOiW5Iy)-Ay~0 zidLv~1bI@5Tew#jzXCaq4egH_b=-M;%Y8!@YuxD^C1hIc<(qy7gS_H=B7}!St;ZH>^9^FUy#3y|&7GmP@!~ zvY{{TiHqd(Xr|p5N59@JO>z^w;jpPQPJ&9mx90fQ5Z40!yk@h&{phAJ&K^1kB$A$W zMRzC5B9>~>HG^{S2J5XA;{=QL=Q()$=gm z5f}a3wrid537iyB2|ULf)y)cu=jO9&G!l1MyA_Kp1alHDNp8t8Zi17*`_(l~Znzv7 zTS=Lg`+{NU`x6iT!h5QB@dQsbCsX4{ht|wSd{+Ur>6JawV^{mpwx2&Qg)zu`O%`>* zDV@xk3*fr!ZB9sdiLSuxioFkRKwXILs?sRk+IsAlIQzjM6gONryX2cRZ9i4P8SX+A z-vK6NE3F+3+8#fi$6YK#!*{)ufPNFC*dY&Z3&N#a5SQ6Al{@mg%{%(ZcF9W^E<7mZ zGPTAVp^SHM*O&ZJMr8}!9JA2E1cx9r_0Z!U;Q6fmkN<#)N z4YZz1_Z7pAcWf+e2NN#w4xF-Umsya%INMePq7H}sR}1nS=6@|xdI=HlX2G-5e3$;8 z^%<&fl}*Jr`7o?|yGIk@L#Vl6xT(~}Tb=jIisuvKz^k*dh5Q`8>W>^(VZu3zBZrXx>(gH+%1!i%L8Vm`-JbBdNlbQF0!=vYcE!9 zJc78g3v8NR(zNXg8&Fw;A%PLK3d81vS^WD!cz-OzNX4%AzAWEDfY{kRpEBYi} zBc~Gs6j)RB>|67@%<;Ru{tcv2M;{jSGd1_twattY@q%pV{< zxrYrvKNqm+)tWzU1LwH<1vayIrYB&J-#qK9u>HbWY?GT0_ZGQ>0f8C!Ic?OrR@qg4 z+)-RpA-{NQI3#Rzd|E7mxTOsVi}F3sf_+vs)wIxj;*D-ee^JlDHM$phUb)< zNU?nlvDa%C-=|wFbpt+DR?IUb33|O->u9m3STt<30lRolid%*qu39cnn-!EUD`GY_ z9!v^~y9cc$3NNsd&7nT5X2}VRJy>QlL3^Y+BDd7FB#`rjE*D11G29Z-ed*|Ljx*CQ zWY$%&@P z$jPjkCRJAxuqY9?MxaoDUz84JNy4zj)6uB#S{)gfMdG&zAgy$c2bO zyN@H%=Sj<2E34ud=I@^nG|A9-54JEJ4_cl_Pwa|;oM9cQGg*1oU< z44F)dOnh6A@z;GHF3TvnfsDSb36{gBhAmoz!($b40!!GL4iAfwem|m69!Ot{CtB&a zXuJ7rn{4S+YEBBiIRalwkk!v{k%hlx__*CyT<+k$X0N@>>?a)x%pFV1dnqMm@O*0c zqgQ|*-NsYHp+uUHoru)jZ{$&ck==w#%G{bq;AgSVO%_;F1I}9E_G{+uZ z{tc)ZfJk;$gH{p~bct==_Z`Ph29i*OO>8%n#^gk#O24@JwhU8e_&YAgyHj_ZQsaKy zGfI~q-8znBodNR!3GTBF#fP?Ws-Fc2X%eU~a{AAQkFsMs`VV z%-{RI-3pWFL-07QqO65>?rnJuD%wLt{o~frxzrmyg+jq#qz* zw02^4u6%|dQ*R+7Ao_$B-pSl{XZu{QDr)nq279-(N*bL*s4$#|JtbyUjs&wCJf$1P zwqQtlp(pW?)+2I2QRjiU?3zaj=tfLR9fkD=sZ)!y%VkOlzZ7u%u;cRFiWcGg^(xARJQBZG5)b6^;##HvlQcVRL9jgyiE>EudUIn}= zP}xIKJRT=u75#dxFg>29WdK zcE|n3e+BJYxw%_8^~42@8V~k<_okLPFGtPx4Db<_$~{rYj@Uvm3)&)tM71VdHhC)s z=5({BtduKiej;I&0lQ*HOw%T<)R=aYN%wN$$*1JoUTZ%;XzgU}KLEN-zC-xq<6_E6 zWJrjUfImt!>hms|B3@reJeSWbWV) z4)2gGE=(A-{L_45@a5`s3QZJpusF8=v}jo_>Q!>0){a&Z4Ab9nVEt+0@@n4m#+)or zW8kF7D=>;;u~sch{HI?d5(`kST#oZe4ugB@vX`LhJgjD}{Vb$YxC$A1X;D>waWpMB zD_OeC;&(VF(UNI;A)eTo{V_5Xc~1Y@zaXId{xa(K>|NA~A=>|UUi~xgHxY(pR(DTS zvMro=HBUB9D3uBDY_X>Cw}{~))uldW$Jhllrh`o{&;8KC&T4t*L*8Gp-g!butCbog zX#})nUcDx4dHUl9trV94QD$;tCs}PHHW-`L=?Ho~&%cy8)az-#N%4`aM1ZwR{I}I@ z{FIC?d&{@0V2Asp#8yg)$4&&$&inJA(_xv8JP71P^GUI*bM8J8GK!V;?Frm)8Dh7C@E*S+F4n4HzfCNdtyl}_?+(N9z2At5! zr@y^PpYhgXnNa;eIYr`&W$S$&gil9X{$;tp!_97)c4rIVcMcXOzsNeB&`NhXgCY}` z@hA77ntK=iqmE0e(ZNU%Zz(=T0qY>U!Ikp}r5>NB`W`XOw3CYLR zp@X*>b*q;dG&#H0J*_Qh3#QouyKQnC<>)TZ#qbNM!Upq8rtW9 zH_8U&I?Bt6jWK`7l^_SwmOvd0%+{QG*l4*3u51p$hg5xit}c!Z^m!W`8l{%9E0ItB zW;&Xnc@REgGqIV@Lb$Xk-9BgMNDd%d<{L`w@077Yqugy@e!q02sQfVbLN-W|OqL*D zBK8Zi>Q$>kgIqRAi&u-x1j5y{nvGLc@oeY5c+za?DiM1gRTu2}t-6VCebR~yj0dS! zgiBC?ZK3k4P(ZGyd4<&EkRTH)8tTc>rXZt`qjiyDLF~BTqQC1_YpK)L6^{vesxdgk zH2x8e(lDAY7$gl;!NlHn_NJzJ3lF++&N0M=)&e6hm$*+}e7!BYJ)m(1O{V{{h7RqV zN402wUuucbrBN zR_=x@NnPq_G{>zdVFQMZf$IxTw_;@1vrdu9L|yGMu_@95pI*R`Dz5PK^7$&n@xfz` zF-*5}>P)LalmT5vrX_uLqtX1GQu-<*100`|$Hl%>##Mlvz7?2LoH}GEA-(1NcPh43 zzRwRDoKl&ZRML_;U2b5)TNXw0j^(6qX#)O*+q^v|P6ACa8zbEo>;~LbY~mG9ud*pw zTWd77oGVN@-<`-i&CJUjGnq=v+k|5KNKh+I$C$r?Crfr{@{JJK)9bA5j$9$e$%sz|*N`DaR3S zz{R=)rdp8@FS+VwRD`-)T;awIP_Sn_^P2PBauI2+LYXx1sI=(-{Up!{udw8EIWs;O zgdfO;D91&-J5pR~7`_gA;CnB=%gg9QPhY@Zj}(H=KV!0 zXB1Pd*?kkCscfF0guIzK*eywVCze<6_KR;Rb_r%{j$r;VQbOGa^SwQ{50ttnQ4&_> zYDZ-8@Go-L@;u3(7?$8td^k6g?-RJAA)DF4sPCWKPv_;ZK*IERK_C2f9L;R`1J}=x zs%&yxY7S^jqKK^N+mX544W4yL51f`*F)3VsZ0n<3LMxP`XXd8}HQg0;^^QbKHitJ~ z6s&zju{SoR63Z2|)23yWSyxDk`i7k75U5|z(B8;mNFhq_q=X7Q2p%#BbiZtLJdnk* zZsbG^UU+r#uki$9zx~BG9TQq%;a=4OoNd%-QLSp2spJt~7FnsCUqz}SuV?ge#U$Bu ztjNWtqw@2L@^+mF;)X;j%^j+6V_$`{R|#UQ%x5=)v`q;EHznNyA6xEH`LZkQH4>w& zGs?@funpMe8~*$dQO~Ge0Vf%fYZ1I>jK|!*$+(1P&$9DJ3 zkM#*`!flRDzcFV20Kd_>D1`gh)_wFipp1D(0pq0)XZZAi{Xoz1y$6&j=Ple@i>>Px z32LDi;V|?<1~Iq`URw zP3}I}rel4rv)#2-zXQ9=&FWBD@n))b{}+3JzW}I)@Qf1uY=HyQ5 z7DTACZfaIV_=P|;C1-%EFW@*ZNN8FM?M z$fbbdU3os(TIOmwv9eX)7I^Cy!;r!nVm{L98Rwv!O>X~Kv={YVDz|*uHA?QdPx6S6b909-Ckh#XV7~{{~r?~~?LLsihXS~S0 z0FZt!H7A+{Rfr)iF3Fj|!H4|3-L~2Nm@K{{5gys9zmvra`88IrWkaS|WP5^&1JGb} zjKe-y-EfSR;~nz=6MCy+`Uo76IZK{TguE#~c2Wb|{PKvO8+%r@MpY)STH>@@Qqx*9 zTZgJkiibh*9o=A4g>~7kPRGoZO()sJ8>qB^Xoe;k7H|xZGn+0&wCH}tHU|lVK4j6n zO+uw~)?lyktMuD=>T53TpBcgA%bEJ?mDN4<1<~;z#{VJ=ic_F6;z?9~8J(EeCTrqs z_cGbIwQ_mOv&I?-dC2z?rYg2A)Wq9uKSBm;u7xaHh_K9;BTo~-Q)ry_+vHg^v+jJ1 z$VZDK8`q#CyD7)i@^P=UeP-5}ymDHV%5xOdGHf_$2fqt$o@xKtwQ#T6MMEG>Uz~w| z#O$JPp|+iU`jY1OgPm2N(M3sJE7xqki#Mf5fybt+@f!P_Ce02AMpqy1`0JsRVoY-~ z@SPI-#WDB{!{qT^8Tvp*aL{NbVbxThuutC+e7{{*vEBUS!J=iG{V8Hm;io0KQ#Kj4 z5I%K`Jy22pi8eb-wd@X@b?kz=?zs6pF+jzLZ3rV>sHTho*qB6pOGqt0XJq9*LuBuB zW}_4<4oha((|-9mcG=42V)L%HEDCr=3P4e*W;xEWJ+-5<*yjSlf5nW)*=B=E2izKA z>|HbH_aXko4Sj7NF|6Zr=WJI4$Xy)`g>tDHTuR>bOR?zO$u*CVHQg5h(k-4NMsRBp zIi^KDkHFyri-Upp)BbYGVfD*Vp+6eXGZSv2TBd|r^i^xt=fe4D&mz(iU}Es$S4*WP zmmNnJ;lTLuZPRQ2O^pA~qS?k_hp+|IIUY!!BtJ*QiyMl4I#V# zU=wt3HT(!f38jQm|71!O>ZG%d3oU;l7_T%{@ORIyj zacr3@V3SX!nM)D`cUr2bcu$56>Th0mPY9d~qKlk68{X!#2d9Q+DL`9uRNIkWI6~3^ zVtr9~a7VIxhs0eB6LotY?sMLo$fa<1+O3}(bm@xIE`RN87@#LmlnQ@uHaYtTS^D4E z3_aqteomHKhMuTK%)vTxCSGq^Rp%G%OrSRTj+Q^Sl%+%ZoUZbI7{chCv^_21USgFg6bNYpA3!k7g!Msqu;C#O!S+n2~S=%ns_a2B* z+{Z*MZ*vyb@zCRntB&7QT^_en0C}f$2dElP{or=))mq2C559dw%HgMs#;Al-0Z*}% zdH!7b+>Y^7tB^6Ll65@To4J-4dOe#t7n*M+a@ilVcwuFRC>~?6M1+ZyD4T zFMIKBJi;it41g5f=*vt1A5q#K{cH=*K_8nV${o)WHa;6cGUi|6RA~-}pOauZ(&Jhd z9AFRdzR#Ts|BsjaI-mBtDJ1c2240z_Ck2tV?8<#)MZlU;Fh^YOX-ulm-A1F8MzBMf zKoBf9iUN}}Ovx|FcmI2(4!_xJk6r_5&GvHGYO&Aj*7Kj0Suhu?AO`u1zxRBtu!iG9 z)`(b2^Y(lEvB>}kBJq<{8ru$~VJ9`4$|C4aI^Z=WepPPc=!`7Cxs!na@o=jtAf)4c z+_?3o?%3EB@rZ)Se7~Ur-WJ=1^S(qpjbGbmWtEj#x|5N##zzino%5Y`Gfa}_gX@Zd zUN4OXahn5G(#zd7pggOeZWL%Hr9!-?c$W#hm|Q8Mmj{Cw_^<^Of|>_hgg(hva-v63gTaTR%09K}XLZt-N+LY&y@vSMm$HMF~p` zJR7}#-M-yty~H*F7w~@^t+WtJvA|fSCX+mnmZW7A3FTAPXzl7*B#$u_*!iUf z0TUd=7}sdZ3TlA4vejelBL`@yQ)7J$pi~EBAD0n$_SOL=%a$keOsr4+MWBy_vaGgv z4SN+R=o1sb5t@#(@Kstja>9;bMb=b_#DVHz^7t&ciC}x1QZr4}bW>3tR5Z%t|W zR|9D10B~jY`wEEmZTo#X;`H67c~&~z^mybMSyB1n?pwUy>slb52F-QKyjJ|d88X>x zA+r$9l2qqLp8lsN4B=2Psi43zFtHa6T8JbJ7=LhbZ(=|4a*(M0F3o29C`qm=_PvKV zoGT~4@IbFs948PSp=Yq7__b`>ON`LbtdGecHEiorv7?!$v|SE#vyWm(G)I<8tFuPk zlX^xq%p?@Z<)w}zZ{QnTa=Eb}x4tK*r3@cM(~dZ3e8C)E1aY`)0*i#`#HjFdW`{+T_)`M2Jk6Fc`V>EeJj zGtYW|rAsOEeXrLfOg_}~3tkSn6WUKPH1?6_#!Q^Ew_!_MD|Fk{4g3G=(@@0_A)D8( z#sn)sZQ`*c#rK`7EV@i~_P>5`@)JGZqh74Oe|oq7JX(cVZ*tAdHI@ ze@nbi%ZjE~m)9V)NnS~A-+z!K42)0;r<(MsHM+p`E_BXU3K!S9OvWT+?cpw(;nj&gL0QtSGlIv4S1Qxpf-4DNwA0ZUG4 zEyfPhKpAPRsEHdmQJu+s!8>s2BbP}D_w(#GYN62hzRCjTE17XGAE7P}QqJeCuPNJqGmEvNCjF*8ldl~^;{?1>w@>h69F%OHTi+;sFI*VZ`qAqINqlB zi@~B?)~&n-K{WsFh)>n%tQ2##{Pe9LiWoZ3_jJC)MbgZe5hWOnpD}}Pc^HTF^pqDe zeUAb3G@1d47Q3KgCr}7v;3A&8ej*%hltZ6QM*XhU%9QOa?<^s`dBLzO>eW0?u+Gl# zNtfd;WP-h+JTTXdbGx4tZSat-2GQaye9{gcsLg%ur98vP!kXAIkYPoqOmx|585Y`M zED5uSXUXt5UQ0g>TfW_97tqr8GfH-g!@6 zb`Vhz2-TKH@9vOb!oDm>?3X%m8(2adWKBOi?4WP9uREqSW;^BEU;jhta;vG;u`TBN z_~Hm5f859c+HAM(!zx1~A}8RrnwN;iXPpr(iO2AlhFM17F`Q(L-NEGV%-t8ID4Mr< zbGCEJ2OxKE<-jy)9tlVzTl%PIpJK$ZwpF)+gR9G$$HoL&8WeA87>SL!>TZDl_n#Yl z$>ODq$!s~~{1iP9Kf6hZQpHnjgB(sMLkg(59U5b1cxe&S-8lf}x`pcvMnlYt3l=~U zuc0~CiKAmpPNkR=2un9PLf9~^Hj!5a;BiBo^C#D0;`!Ol0pQj~>HIaqSLH5ni}-PP zx2rD``}5eOvk1DqC_9i4kM%ylXkDFrQCD0vlmVHpfNg}6BR!r86mS~Zs#(zy`*6bu ze`l+JZ*|M<8K881fJ<$pT-llf55m~gb@=TZ*BVY8GtsMA^S%dBynMCrhl8>NXsb-e z(YO=!6-9$p&R|{~R}Mqj0mn_Vg)b%GZ&CBSjoJ5fhK|G<$ zn(s_wx$s}I7k(AZdqbvQRR6@RcHp6S+zl{DJTk6;*@cf^9~jsdu+gNK*&qHzZd|JMFwELBKVs^G zW%f-l|D0GZceZU7n8Np^@kVFz<_qa$KS9z5I)7nLYO-+rE{&rg|2L4OKU-CZk0;Qs zY8r-ut;``3$V9z(+j*Z8{*X3x_#)d}T58RvBGr{OsY@4jOK8eEf})XLr8Hnj!!1%_ z<10kb)WCFM&eiPb`3s5 z#3!L6aoEV=fekx!jM#!1y)0S72v%!aj%wfY!|6W0&xKNvIU>pO;do9}sJHN9c_o_n z3fP#|x6v57)RMrFuqrlDhpi_uolU24!fm6L+9Z6f!Z5GVm&;aZ+;)Ym%0(d$y#$iY!2i(UBFW}mvQQ`)6mTdm_uCcnc7NUu`7q8pNDWMvf4AqZ5_Z8uJ zA76lc_p*SlL5{dSu6^QAS2KrliXcVL9D_TyH;BKnIJK{U>ZyA3^F+#p) zUnBDrk-I*^=190Gdi_NedHl45K4Wd-gaOyUWrR2Pbt5G5AR@UnbC;ZlCoPn8FQT{**na$Ta5;qyZNLBu85BJ`8htHlT(Vz z$pJ3sGK53wvq^2Oz-`Nl5O4i<4KTBHH(O;<=RZp=p4|2cl6UTsMh zfh^ZpHs@9H`ZHUr1Kyzn(1GL6te(g=n+laJiSv}jNXPd}=g?(%{6ElBL>%c@;TQ|~ z{ng^|OON_1pbikl|2gG@WoUWzl45~w-3m=E-cPiIIDozv4(T9qWYL-UezhYFzE)!tGyf?P8(6wP_7p;{G7uZd+|b_i~~4 z`Rs;;?CIEImy`fOD6DKAav#-z%rL|XUXqgu$=tLf(iflbe${AIasp}WI{2<2q;-*% zYmO@k-{pUZGx=+|6a^4Y6E4MfL+O&rjaqP{!}l#>%Vx_ob8eHVh`!ba@x2$dQf2)_ zX$|BnK;}b$NfIw_^N3CHOx;-YBv6N{NZQS~ ztqL4Jp!>1?r&5YE^1%QS?h4g0_xlCcReUcJqzLGi)l|CU9do2qtP}@;o#wX_Z6V=a z8lSOG4tl9cRon;O;P)5w0A*P~+~M}!Z)VA3UxbNv5e<1D*$}Pq{QF6#;1*Myf*amd z8X(>sBrvD|lmo3U*Vu!Njef#%K6q$8k7Xfb&ryA_67E&s9sr=xC)H*8iismhBKS9bL zwAOmr7iGuJ*=JLLiRReV7D>7YIVd=@J;0yFgau#RuCLop&qyKA{vA`cqZSo)STaPcY_zo{J0aw0DnaeUAF=u7W+q+tXLyiQt!(0R) zFA@dBksHn$>?h&Auj!k*swYFCtnBx!ODmV~=1bV`>Tp!isoqvOu~@6tTh*_g%(W+L}sJ&t|Xw7PgJ<y!$r4)C=~S&n*QNgUZv$Y#{cgmryEU>7N_h#*ox4Gr`A zZlw4mgqlY(%+k6QdWHbD6E^*ISqqt9nwD>}^S&sDJZWWHgAbKRIosGxSwKCdGn-sp zG;@|&B4f1V`OaN_HMV`H$>;hI1>%P#Etw7#85}FHV=|O3;JX9h!6fANIgexTcyVP{ z%z zFvf9D%Is3eJft{I?dT->Okwq(`o$2sc7litCU{GAwCAD=z#xX^SplN3>75fxg4y+) z*9@Y%NeSjU54;(x^v`XHYP}ICH4aDnWc8upUqh|}w1{j*0r)Y6Y(^+hnqGD};;5fC z6c#7USK#lHz%rEKE<$TbJ$AGpMBo8+v4Cm|muR}7mQmlF*v+bwOgFAqV8|JdlH1m z$?d}MxX6&dO9to&zd*X>=I6&Tev6nLa0xdb=*NC^-t0q^CE#Iw${e?b#Bj#+TfU}` zsjI5|iOpYMk;i=i`V-FYgGIHGIhRG6Il>(@+0_iZa1}i96nE-1;m5_LcvY|kIGW^t z_})E)kO@PmdwgEx47gk8LJ$@gM@S;dWdh{q>F@@?Lgi=F0tkr(Jf!MvEt`Nh5F`e8 zX*pn#&vRJZFR?>5T@RyKn`Y~mv!lbI>HL{YK?N=D8s|=m(5vXiGr^~H+8S-`IR*1n zTsY0!+=GDN5O(8cRx?895>5D7Y5S?gSZ1tod^5|@S4GTpA-L9PW{j6ATO!qyedah(<)BF!biAq3t9CPra&gXl4 zC$OL02X=YhOH}Og^r5_H07YbO8llO&q}lwA)=|hO=Y4V;K4(+G!gxEOY_b?~jqo=5 ztXewmi_3~~Bx=R^e-E^{cm4Y0^` zF{`@g40V5U4^*wNsrq&$M;r?!LAfU8bs=*oR<^k5TTXo|V%o2g0|@&!?Q+_g8CcG* z!NdS*-OEu~%ec>_w7US;~pKtK-@hqEnXg<*qqZH(mWKc=6!(llhsC9c26=g0CGndd?DJ?PRug&1f zUDCZvUN1F)lqd;gFk0JdMk<~ridKu|Y=8ENQ5jLmtE78a}?eX--22WCb;>1f0U zY$GcDu&Z6MhNR+BFywpqFsk;5cUEUD3vmu}F_*2UzcHr8Z#8A{ z`W%EWs)H;f>me23vLtbAzZANf+og>!wc{`Qh;Yx5ZnreX=1X5?fvAbWW>R$qPD$$A z4Nv7wH=Bpf@P4K$e0C9fN}MEALP%?s{e z_AKcM2TULV!+PmfPZ!0b3;bRC>>m`;O=4K4s2CcK_b)wxu_d0-W|0H)HN$mO51P_1 zNDSov1;mLoFhWVi#+s$No}HwGyE>1Nk7HDTTqu@Dz2w*mN2v1m zIt&s@C|V_N>6YZaab8<^rVF`4Gr^s)uOVBRH?lkE^aJ}cV12p zEHtke(R64fi7jvyn0t|b3(xQ&pf}olyKPeoWhQzm54JPzPf>YD9Gry9GBZqDF2~uL zG-0IDvxn{;d?kbaPR9Xa`5bbiHcu*tEdzoxjDALlh0_S6P(-ru+zSH9h8v*^Ep}n? zd*kJLvo)W8({X7i?_0^7#cn)t*!YV{i}#Jd*WwYKD#fPhWi&h(>A0&&s$F?*FyE@G zWR;E{{W<4U`4DF!Y?b+8@ikfVJ2ge5MRy?(^LG<^p5Bc%ApjH%sH3Ki7M8DoX!^&sOT6#WVar;CHuBNFvU8_x%jK!EFP>nIg!%3^pIM%xe@^+L)5Yw zvl$s8wLb9mRTQIrqFcc@*bS(WkGAmRF!Yd{gdtn#73_lS0_2nD7b$vzh>c*p?j|S7 z=F^)ozc@)rzQG4~8OZeBQscDXA_ch9WQcy<6mN%XNDi-)RdS4m*+cDS=RQ~+D#^mu zD1hfZh5&`HC!(EPd+9j;Uo8M1yM(n7mo2*!F)b6bl-|~~Vu!OMw2dC!^(xeo+S#>N z2GRD$o&+r^zR!baKxa4}oxycYkRa#ga96Z#-`z#iX255MIJ%yDScZ=-1Ttb1(Smb> zNH&HTEyWU}Fn0X@f<*z>4A+c|pOj_(_V7`b8(Pv!@=^ChrA8U=Az!SR+W#P<7=qlG zu+(BN?=u?-i+&|S324&7E9Bzq%eYjVC$kk#$0^`l>UL02L-Vrh!SO{^!uZfcZ@K&0 zzMDCvd1R2lN5TYB&FCj<`qTx!AOCc|*o{O@kB~Zey*|P-9!azx1KBA&j-mmQd1BgH zvRTvd=8Szz1+i93yiI8dtO`mJy!i>2jQp8DNzKn65F6M%=vxiylbDZYlRZm_q5Tvxgtk!+5|Lj}Pk$(F(tA%#E#3O}e zJ5Amh!~+qn2d|2Nry=Ii9wbXloh(;~GAoymY@Y5}+5o7!2K*GN0bfx%6=~mZ=E6?z z?MwX5_3uq-#XWL5=8dhnQ?}Opc1b#%SEI>u8Al0aTRHg)MuiajGutpX@EwJ(&MHi& zF+;r(daKEe?(P2&q^lyd6i$=rMGUruS5a`;2YjkxZ@H2MC%)izYuOA5zG23^4O$Cq z$<&Rm<4GDsU)f$44TsYE3Fg4g?6!BB(^gCqM3m=$Bu4Nbfd3Z?U~?Jd{pJ zlode?RHoPob?|?|HwXtv6g0o{@}>fAho!|O9@*k048nhWTCThW^hD-Z)3<^Far>VI zxiYJ_@LLre^+i^P#z~zf&deV=QU_2vL&{)w5R7`CLkWX$o)X(}twI}KCFs1T15}qm z-iAXVs&4%s8|?}Y7qlSz&#;mEC_y5v00Sln=>3dJO=BQz1%C%rkG@lM)mUn57-XR` z|H#)XG?sb!=D$|GDrR>Y{P3Ib()Dii$|Ee# z^;gMLw16Kx-u#3al{p+4pr4_2<$I;VS zb7BMCcN~HnVkaw1wEcUf?N>&F1h4Dx_!uw%riUNSo`Au+MiWge4SB_Xmr5!zHb zM|Y@4MHPE6kDr%F%F4f#N|ao-PST{Jw!G?PvC8L_kTwct?eXCetv{3rMJ~g!FGuPK zO2BJSATn$R5rScbUK4u(|y_>n4Xi;)>}EqYU4q{f1a2Pi(rOS?to~+@flN zo+mH&?=E~jkW=#q!yf#fxuO(fxb>W!SKVXpHvpEW6ldw9>2)nqmVs{uy~bUUylxRmP#E z?XT(u8Tcm|3Iz2EV|~X5u%{5Dz9?$8)~?2m#Zf?;t4jx;OxMo$_)^C}1ShxNagu@d zaOK&xBuX7Mvyv{9I0O>|6&EC(iqIq!7e7oDY!te9Sd!mGRHk0*pYC5^WuOhQbH1i~ z#|?9H0!m4X??)!lRO9QzuF7ix&-un)_7dghb-~a)lMJqU2YN2ltIMaWb$2MEIQ;bf zCBG7VEXOMV32Y&X)QOl1ZRHZ=ni9<$zXfMOq$gyCa|%c+OH%IECGkP{>&$Hphl1?VL3}ndETN}6KXS)3uV|ocOd4sELIx9 z;_cKvcm&5~ir5Ue0M=8Cnxo7!{FD4Xh-tt0m+BBTb(#9AV-J3xl(qkCkUoOG8>Qq& zx(U%wx(wB0|KMcN&aZUw=o1=K3g`mAUT_oDe)-m0lSq{Ng6FD12wym%h{CO!uy)O~BD71N=2Oe+O>JqvI!fi( z+T$@7y=m~*s&6|?T<5v{G3RHBXFkZOUp^+o=}7w;>w+}hGa>=RbgdwjD_m8i;Ig}u6T)uc=r|Ys*&KcSS;J#t!ueK# zW}D^b5e4Q=QxZwIqrs;X5amqie1V;f&X;)hO7z>YNRBs_Zb7sb}g^uOXh-N!jh>4=8`E`^C{eNe^h2^-eTR7^3EH6DcOsoW z$0bI0^O{`RH9*w9b>rx8Jo>7 z_#Bp3I-9Bfo_~wMrI;@-+7TpI$OK3LDP6O%FOva~F%dajP!I}HOvTKC)z~JWn(s_m zfL%}NB4<#7UC11A@%7M7=2KS@9&SZs13l30~V%25VZAnw;_vak3 z79WI`_geb;rPML)iEBZU8i#kL6~i+`wLAKvG< zEhWZ)RUuohQLf;}212wyF7bxG@&tz8p+?atVHL8WFSr}?6YyyZz_F#9;`KDj80!VB z(G57sC`iWFOSA#zKciCkEGLpRYBLm{ujqlW)TQ3|*{n41eSnxD#)2xhyu|=HWz^{& zke8K4iW(hfSv9A4eh!*el=Xm)mhV-V@cCp0urBNcwARK2x3C^Ovm?lK{PxRPIQh|{ ztNhLY({|5uGOX6VwB>&z2UCw%1z|P?!lhqvnJ{POd*5>?Pl&0|PIWO~5TE^Kx1fFq zk_}2H8UU*=l3o*!tF1-Qeb8GK)j$34u~b&32&Qd?y7$`Lc4y-;HiuTq9I5mMWR%zVzvbeHcTy!Ne=0_%mUis_3xSj`?M=Y zy2FLq4F=b5B9wHJ5OEL`I;vUd0wx0m*w;XBRGWOXpfBFJ7|NMeM3-ZP>0i8V1=#e7 z=yd2c;^=->J8w)*Izq4m-xi^J#C0^z5bU|tNj`I07UY1cus$XRfR5SrBnv3(01C=s z$N{0Yy}Uu`U(T<+p?d}WC&$dmYWP){m}SRpukGgFg!&p%fU#5h720(}6uZF^zFBax zY6yI-Pf^${Z%{=xFL2T6a`)oIvA3j!LKEi(4US$|cAd}7K>1{QV6JcEF-oxWf5se(Hu|81NvGK=2-tZhsxPb8U6Lgbo18{0vFqR$UN>t zbfi~kxA4Qq0(!H3CZ-6y;xpe!R1VGHWhHHI4A&r2o`Xr|T_XV>+QEg9q^~PT+Hf6Z z3iNI~{eBd_K0360r0=9T;aHWHVl6|x%f!>*`no@(+4J@y@%0V&F6~P|Pgp`RUB~j& zJ*R|j)HP)gWwLd3f5T6T!esf|G(#}tvQIkyI8baUPd0@96x7D@uHQ-Lbjw7D~?2*IxtJ_3=kxioPYvj4>%6abGq{WRdlQ(x8Vd|aYyU|mO5Gr@G{ zQL?QTli;aDa@F*5MD4Gs*GxGwD7aGwJ?27etegQEQMUAnn|tg`cVr+3v9@z+O7c95 zT;o-FgYEX5si2j2BT%s9^Nj5qzmrT<3T>iCq;1x~nJMl+3eON)ta8{Rh9-=(=LUzx z+o@5!so9*-Tzmk>R!BNESqD2QY|C=ll6Vorzj1HLi7Lb32raIfM;gU%AkVM<+&ncD zrp@*L(xJh^{HUXmQvT{KO5AJ6OVsPm|KP7v^R0@q<%0p|T1CAUTpW8cBl>Hru6SP~ z_Deg~rWZ(~wzs*xoM4qrH@7eDF+zhlGp^oj5Ogs;pfFppCgKOd!J2AU@;5EDO+$8z z{jp3e97-R6p8KHI1%(xy0Ku7^yZhN@khM~iGY9lM=yRss`fEpUo>vl9P{{IE^&GLs zIf~SXM+gAh*{JBEl~o5!$-*#z;CI-W-jbu`0Z85#%a8s37#pRLb1U+q+W6)~iRZ-S zuxz@ym%pP~mYPl{+7ry?X0{ChI*2#;me!Qbed@y34nUJ;fP{nFKs&HC$ohl!Qmuvs zolgSmV~@_U=7rH?es;b&@g!VbmYD`^=*_yL&atgBG6!UEN0|Dl`W!~PYk)T3OhDOQ zG$pl9CSL01L}tHYtOBhO3{Zv~xT_^voQApzyu;lFY}jkYOMh84bOT=2Jv(kzNvG`o(Y}4C z)kim{rn{#<*+nk8##t=~;%5;{l>6eWHyIO3#mNj8EMzGaqmg(5{=rjsgG%Rjo4enAt9b!UF&-hnqX|# zovcL$HjxY=eM)(9vZjfB^z(D>`j_W(%sTAi}Oo1 z*;%@~m^#2dxQGm2iISiS%$Uaa=n>^SOa!a9zp;fw8As`2xwMlK1dz%Mo3Fy-WFj4* zo}f+3GlVt67hE}f+sa#RE^&d2@OZlRfCUR~a-i$U>k-wtPs7p~a6U^iN^;13<$Bc< z-+{`CwCl3(RR@5NFbdkj2@5vgLs!)G^jx#bqv$|AlaWP5B_Cq{#A-Q*2eN zU7&pec$fb2c-AUhn;#Qi(U9{xYO<<ohx^? zjyCDbbIcfbHSyXop*OHIH@@8M0t)M_6%A-ETpMA=GMQG7)wD~e(@0P(g~dFiL2)IW zp7E*WOjxGk!28#&Fp)?n^X^w4zirZIz6#B`o`QMXATsfhC++~R?6s15$%i3UY~C{f zV8em<_`3+b%%MR=qo^n8zTX9CsW(A9Ecaq!h|F*7cgdTF4RQHCa!Gp%;m&|Nw9FSHE>P}6l77TS z#3;Jp`ZXgPket|E&mhJos)GdAQEoWh?@z7K-2mklsKU|44Bd(dTh-&hV@LK-F9%Zp zg%9@+I=nG}mOFc2n(Xd-p1xx_+^~u2UaXthD))W1^dHU&yL^0oZnPofX7}~lV9QPR z7gtt+W;*LUCDbt4k|5J5z~<}*LnB#uU%;5>&V4tt60%|T&N`I)#~B3+fPyFNL64rk zF!}H?-Yx_LG629-BT+bNV0BU3BTnl9pg4yu2Qp#WFp5WXUjtN9b8r@q`#NXVJqra0 zofjd@2JD_5YOzk>VqT^%JnSrVT}0vncW>q>^^SrrctC%HWLUu)H4;BCtc-y>`#+|fk!jSa`?_-VJw5lgw|q{V`aKhd zH0UlNVcstf`=9`npqKEzYP1}w_-$Tqfb}~Om9v^DwRV|g(zUtk^{=Uwg|gg2L9INz zvNcCq&0~82lPUDjy_JNhS`5`+EtX{f{9H`l8ou$*#cdcm4;j5<<3Ft!Uz6z&=51D< zbEP4J=hPN^T7G)z4(BXX4Gp50nMv?8-8hrjeMk4zvG(~Ol`u1qC~jS zOnq5HL@Yia%u}_S*O>E%0X_zczOPnd8+{^ydJf`eB>vLF=T(%{7^G@L$Wq~+ckYceR)>TcTq{qo4^GN-6LhG*oj+hyn9yo;27g}uLFmi3 z!{IpszrCLMJbgQ-Jt0h3SI|h=PQY1l)T`%qy)>-HXKqIth_u6L%}$JjRZPn~+9`0w z&O3bYlfFNv%sW)jMOhYO4H*pv?k~QL2^m?R_e;xAjXk!VZCZ1F)Fb{-i=oeRarCHt zihV;pv{*CCYDy8E?WAc(7q~c~hrN=T@9>DKjn|i10IGzw!kG__ zA!=81JI{b~RXKp~7Op`cHL2#JlD}vTx_Oz196(k27t}kRf^L6h0LTagz=-Y}=!=== z**1D#5b9e5B=_GK#-MPmUj%fs1v|!W_T?Xl`G~8C#r52HnXtqwxnNk3x?aNcQ?Q3O zltZ#Oe;64(h?v@VQZTF@TA~0o-F)HsQxIh5lWI=$YB9#N-N#1|yTvBCZP1iABJFL6 z6{59U^SZypKxWo14%PqSd6MvO5s?O~Ftt*Ux1Bp?qJvXS~DP;s`JpOP$8-EKkN7@OgN_oW6@OX!J*!pgTy&|cob0rm+Ca*maonM^-LwX{xal=(U1uZ< zpU&L+P_u$Voxl-!E^!gBXK6CLvQiTWyTnz$uRPR z`)X=^zjWRo#UWs74gF_gOP~grxgyQW_;?LX9|jy8@pKrV|AE~rW<0$%+gCy(n;V7R zPB`IT2TQ0J__K@esONT`pO-|G4*K+Gj@)&8dYVYh#H!A;r?ZL@(&vf-6Wa^7~cH^X#>O@E&Y z;<7bZv~|FYFTeUPB|RR9=<|6ayd0wHm~yfQMZ`=oJLx5K+NF;$}5cZ zYNMl?DQj_xW5f#?MBZ#xt>ZUhuf-JE|0}&$s*&Vonhg+Dh8+NMf8PtAKdbqjVO)eX zK+7U*1o&x?nIr~hKt@C%A=mWuzl;*wUbgj@Xw?LL6|;}@y2f3C{BaBN-QdalmqQm1 z^^?{S<%m}y23$)Q)FWcTIc5#WztpeoBy7AGFa5fiu4>%{imfb$%+>W#B1(RHHn?;-2YC+Rjo<*~eol^qg1dqq{Y$`ZW|y;ONZd-X23uW{ zpU9l|a#L(~trh3b(sE3x4INw*zPS4KLyQ7bP$l}rWb^#q+?i4V|N43H2yw_Ofy!8l z(B`grP z35U{W)SOuo9~*lEGgDAOedhxhqE_xNDy2Df2r*`KID zc=wN4f`ZZo(TLs(F&qyoKZ)D9n$er-Q)BEEL0acxA9y}UabEX^rVA#VSB7mZ4fBtJeCew?E_jGlY2@lw<1x`F2l1S$OD`efta>SZ~TJ zD~%GgF_eWjmp}XN zymbzjUmdxpQ^0IsTe2J$M?Hj|a`+{|O)wi33KYv-4STfvr3t)^*Z8*CkR+5n|Dyo0 zW?sh4u(!I)zrIW#w#v?ZmqBtkjgflg)EmMrC(m53v%OfiYv3icbFFpL?uK0naf6blmI>DZN{upp80V)BTay zV~Pp`9k);6)}+1#-^{O^T?S;U21T?DDW{xvJ};lo+E>qwa^LQ+U!>2g0BbA?m>x5m zrW7i$FM>6sG7arwem`f32Sl?x4_^_52D`EDi5`~adexSRH5O|!PPe1?`MoE!2^#kk zk`o=KhfjR89E|c_7;oau<7%qEj%{0Q<|XRmCoI5j^sAH-kr|J(SMA!(CP(yN@PSy}##%K*#q}8g{S^FW>%r!YX=vTT`KK(f)q3?XUYar!BT#yZZ^=Dv{Ax zk?FiK<==JR=^NYYiUuq8W(xL9QPJp>A@ZQdkv|=267qBJ@1l7LNOqMc39hQ>+$h?_ zuARk2kT1$aVG97hX=Aw$gf0L`Ibh5Ks&@8s zAZ<*-=}&471MmV3R4sFybFr?tZ71U@L`^X;5sKgUEHd5S#DK-zA$uD)`xsJ;5u&RB z$}H&KLo+hp3FIfy4K_63sC*;^Cbq*yTi)bS3=L76ENDKBFm(Ia33dy zepj-b+cxFXl|6h6E7U_sfS8q%y)$(g%3!MVNGXJPRk^+k;6xO#F%G`Qy12sGFy^`7 z`7y-Boy@2j^jK}gj70outNq}xFDnU71JqF6M5Zo&3@Y#h%H3x8l}+kbXHtFS9l|gq z;g}H}@s)jx&-!b2%C+#IZdyAWk`L(X9N#_M4(1R`^nC{Kv;cD5-FRn*5cM0Xp=b=E z%Jj^ZrkX7zvRoWxyBAf0uH{#yBZ}k)XXqERs+~)RG+8)IpF{Or-=W37e+M_}U|li_ z1)%%{SFyxacHjMQTASXc2iJ(>FV|k}gdUnH^)FJQTl0f+&dggF>`W&YNp zpznt5e&V)AE`VY6^PvdZ`zzxw3rz+at1Kx~bBtEjkk+o*0PLW^JND2h`|Su1U2>u4 zAr3Z-JZ9SUiO_5hNMA)E{y>DLTUVWBviIqaL6WE^$yXjdow1alYIz2`@>j#G+xlbq zmN2UX9k1(!(aKEgUu;ti*#wjiITEk*n~0IXk!10!0&fb@Hj7v;zSf&Py*}XE7wsIsU--* zIOK# zj;YR9{aCHTORv*mSxC-p^9V`(wlb?hwg4e8-2ETTVsQIFOKJGzw=Kd`W@;NUYdl1o zHTkh3Wh3ziXfs%Lp?!A1K z(Wb?it@d4n(2CG0!3~!@CgSpC>0xxf$`bPCrPu40K!GqqMhdDka|DyQF;_T~hNwo= zE;OU^bER|mHAYybl6<%1*?qac*k=xjgj=!7Mi#l;3=vrMeks--42~*0kG9Lb_kI!S z_`h@nPlG$@u0%@6%fqo$aE{^u*dL49Mvjz#l8K-yh4@Vxi z9!jPJ9cSA-He*5COcDcCe)(TIu z?K%4!b3VHkU&8%arFp!}h;WV@=H@-MvODRSKk>9bt8)2n1^Vp9z@G8lP(yI-az?B3 zx_5s|Noa0j3_o}3U@viFyI)Zyqf&AQZDVJ>Iede}UKr&#fi?kaOkB+_r&0tI1RSUH zjHZ{-i|5I`_1%gq!L!LP3r{J?-*>7A2CM0LV}n57ZjG0=#)DWyVBSM6F-DnBYJnPi zi6rmv3NN#X-6`?q{NU|)?Cg>CjWLV1wop##<*YzT=q9fm^Nevkl|1DN%RDj#FScI~ zJ#bs4v(4KBvfDONAtWmB@*&S>xzTStSHijI6xSX` zJ(M}4US!S3{P6DHp{bzE_&UG=qX(yo(N&u`Q*(I;F->1UH>1&|BIOy zTAmKLn)AUe4C-upYg10&q;5TZD7KZhp&;tsu84N=pxVMS)M##E_F}`JBC#F5hYR?g zMY$U86xej;E?R=S%*^=v?cWNVYod|uCxOzw34_7XfYfbu--P;r`_M zh9|m9**~1}qMY*XDA&K#JbgFaEY=-lu=>Sz&n0i?#^koRV}eF$Z0E3=4E^M{wNN1= zZ9oJzn8j#i$dq>>l$2{w+wXqJ_+G-mNY}Lay62&k?7$1bxx7a2{^}Oazq`sv!XUwB zDf0Yc+9?SJM3S2(`%zMhxR5!GB|u))3=(g$xc{I;8YvQwEm10+UB5L3w`Wunq1=#{ zNG`r)rwhj$3#ZHSHd%MnOix9h2&hiBtdF>mAWNUvMi4ryA9HeQu!Mhp=Hpf5lne|e z<5flDNe=9{9-+&xpbpR>j;`JH`28QBGfD*EHc^i%Z3q?l_f-J_(l+o@(HU2&(m;or z^a@{!6>|?=_D!$HKSZ@zbh5>R(1uNWx>}zXFx(KwOL#Kp_V# zv{3={?JXpMx83+|i<5*Fjh|o?WO2 zW+exVdtVLHP<8bO5Ai?pmL9r7O;F>w+rF9f`9ncm4~cxAkXm@vB$yEHi#3wHdDc*V zgPwxq1QJ?Geyxuy^&un>H%@fvD5jr`m{Q_b4O2eO{++ZHu22QIA*T}0itS5WUjx2? z%U-Rx<}H@@G`6Z2h+|jtK2|`nXK{qUwZ$sbMQ=35CM-P>t0H&soJh&)ZhnDxPfYXS zu(lk{`}0L3gs-GGri0{Q?YF=d?3D`WpovaBN4C=W5I(Y~XZX=%N$&E#MOa~PsnNdd zdO}pqDlQ_>)h@)pIf%S02${XmcE0WT-gE!B)ZVz7johV^XZi7@{7#qNaw-XMljBXI zZJ!ZW*y13y_XB(EGmx$qiIjeZkoeR(|1Utb78`;SKm`}TqtiZ{vX;6SqN$i4Hn%f~ zPVV?bN3Efrb%U(JKkY$2Z9EdGP=7$2?Bn@~F|P}@Iw z5=v(C_y9BEMqg1oGlHg1H;-m<-P@kCfu3IJ-qHva4bxHgmOaUm82b}mAcxz`0>esN5NifI9#OmAGlX?n3Cqf^@?L)RmDtw z)qIfJ7uhUw2THBiu1vlHq=Hx^l0KF{BN_pT-=6YPwbsDBX2(<-qFGmQzSK6?<4~6UC;%Q}y5K{#tDH zqQE2%9L*U|TtwdS)HxJ^NZZbKP$bLN`T>d4K zn&0!&Y^XOBe3=^57r4jnGEv)`#Mi&gc$i2TES(QPG&RsyEnK^PH-~L7XtesyVBi4y zm0c}9o%zUpybJ}$$D)5wrIE97*g?|(gG#0fv^cgcxokS!OqZ&I|L6BX&j8rH5u8v3 za_$0OcY~nd`Ypx{y$xnl!F=ug zAA8Zv5b|KF%FWjy-1%Kn=u6M|Qs!F_u%^U%nWoPt(9B}JrshuLZaj*;$p(F{Y5IBn zheq_o^S8TdDEj9f4Mm$ZZF2)v`f`}(ul*j zsL~$p8@6;spz%;4D#aHLF)E*zfX@VXQ}G!BTn!p`{|YLEy(EFaFeDp9jn;1bC-V%j z0Lhr-tS?=}9mtht>i5Pa`B6u*AdNd0{7Yg*u*OFPz&x(@v$Q9nsTTaJoR8fgg2pfG zeGclqY0C5pW(Qowr1DbQ{naeByO8;-2u=pAD$6~TDClQL4ADpiB7Kw&qiug^r>4++ zI)UF0ay#rF7oN*q%ysEb^pB%KQgFY@7xuR&UBeu|tUu@VY({z07*34W5RIIaQyQHn zI5t$rkHRFrc!+LHQC<(&tgMIr753KWvDul=7Y9i3e%^2;)r==6G)bd$k8c+sXyMJK zkjYZ5lv0#YyL(E9$GIWVdBVgf!y5Tijr57osa-=AtLEY_FNemgD)_z($P10*aM=s0 z_g)|O^SzfXctMqmb3$H&R>5(!Bz>B#z*zli=>OTw1m`~v@=U-x1|L5P(hM{UAS3#d zJqO~?BcGur7n5G*1q}xJL)_p%*g|C045e=hix z(;{R1Npp8rf$88<>ZwT5%Ym1Tt6k@Y7LyFyc04mWQw2&5ci%NyDgEg$ z_#otPZCq%sCAnD8NGZiP&bK_k#Y-IWeb)5?yW=j{0euI~BYw9RiCNW6nr`wK+xs&9 z;#>M}fnf5y=v`FViN5_^`)4A$@|<=sU45cxmYwVkS~t>D+FA0!sX{q$|2_oCU@s4% z6?TCT;T&!l8l$({JMUc{`S|Vjt@q`u&ty&4tu6EoUBED02^LoD z34wj~5}$LTRhBpGg~tTk<>jLVFv-q{?>SqoKa+UhEN>_$;kcc*{T34Ul;+H-W1>1| zbnr-?uY8>BcJHFb+J_Mggvay7{#p_dLb~J4VF1`Lhn54`=CSUvPKl~PJEH%0^8O5M z#(-v$IT9~aogBzTO9TchY*cA^f{bC^y>FZ}^pg~7K^-~R6g*JhH0=SRLeUKJjPL9dHD9XTr7dLc5Aw?@t$n zQnG@LSfY)*U_SE@@UhTH)ZAHU>26u75fC{Le3pR=6V?94^h2r*-NC?xbl`ZVLS&d^ zVYo2TwrP`nCGG#Lu0IoGgrQNALQB;?AWsY(WIyh=x2o3692460C)#6+i&aM6v}76X;F zx`3QGitvR)$^Xw7v_eVrwAphyhg3f0wF;rGr>6AT*Q!4*T@g%-L&P#GUl|gewKUa; zVwbI$qk+|2|4dBwlQ&Sv@wodYB1E^o&W*{#DZz?oxm*pE`!(7ig3K!q{q_9M(m;!D z-iNXC&j*r^{BmiwT^(@rhmnvHv*0?@fC@#ezZdI&`xZS=7t4hoGWcH~>QJZVzu)=S zADIE5IPUy_v0E^9z+W#@J@r3);Qu^{1OS8yOm<5}e|-A?_nm+L(`#zfA(}Nmdcy|( z`(^6DWVPA$mrL?HCcaMAx-(ggmd><>28ZVUGy1-k5cH)7%cC$g)qw7t_*v~T=%?T^ zz%rQ1*x4v+USt2j@>vd{cOgb zhjE(+TjVC^&*=^M)COkj)HYv!PdTL+<$ZtNnye{W8^~3iPSiZ*1|70$?BiRG)|=Kl zSv7r(3R`o)}@xRAPty>2bZMI za?!+3Nw+3Gx8xbJQ|Iy@z@l=ziExqyl_d)3b zsSU!y8xl1RdqR&X-S(lLJ)S-PjIKORdW>E4yl@nDdO-Glv90p*>!9ZEW5G0Kmwr$A z&sOf+XWLDb5aaWSYQN$p>rsvf=*hNk7n=caUcquT&8nnJC2KuLwM*^$eq}d5uRL)) zKV{?3_&z+ww;iKThDTYax_|G-X8T1j$-k_*@^plaVLP{c5C^8U=>uNvfEWh{WGv)_c@0?P9yL2bUAnwkTiKf4O}LF&A(=xpF~$ z^pU9IE03w#ROg%q+PsA|zGy+)bQhh9D~<0$m+4~bJf<%Io}+VlzcoHM{PCgUtfl@+ zK@op+EyhT*z1Y*X;mH~TA62JNy{rjOJ_v03vm{Zjr9Qe zRDGtJ|D#};a#*iOM*UNnR^p9MIr1w`?sXG_9}`wz4F+$tjQwdq*uD&CH!0nvrV(PR z){@pWk2kMCuD;Wx`tXbIjcnvJKg}O$<`}R0|JMqiWm@M>PdBfuml_E8Dm2flbZYL7 znDdqcr&f2!859BETZK6Ha?b4+Xv3=I9Y!>B^{dXi+`WnwfOMpDy7Se1S|}FkP#0HAT{Fx=f)3Vo z7>KxF#m>pz)aNkwuCe=py*mdmmf4ccC*xdb#t^q{I5|Lmj;3&%O=0Tc+R_#Kf2 ze5bS-on~-9Xjx>B=$P1GppG;+v(Mv_Gk>StdO4kXG5LQe`_7=IqHkYHfFL!35_%I6 zQ86G8dM`FmEPx;_6qQh7=)DO6LJ^c2ss&Lx(tAsg5=440fdESHop<=Zci-Gkci#KN zoPl9xpS{o8>sQuVhgZ;u1!9_DY1;B8hnYuH-DZdQPUZ#N9bkm%TF?#SlFtKAe*39g z{1IMX)L^MTGie}BptjDc-c4G(g!@LMN=p~dbVQ5uWGlsG zk>5zC`6XXC6D%7|xm74ymQCgHwUoTt{t0?h&56&|@~3OsyW)lYS6))gWzi0fX)=R6 zl_}9FL?7WY7`e~L`}u>yD20LY0#OphLp$w{l=U0G?c^O&|H{kkrka>Hu*hpi8y7df zbRBr6_AO+0b)o1qF=(}WhsOZGO}ZuIUSAI}w2Ee_ zANfmL_t2_H1R}LGOTzR|SA;`t6(OQNWwn-?uW%ze%&f(uw!mT6{RuIUd8xzm<;vY+ z2jUEQ@bw`w6`i-p#Zp)0YET_5opp40G?8qVHh-((_-%!4cgpyqiqB4|=KkI-p-he~ z-LeYVyRYTegUUSEdF4^SSxn4|k^_*n)K4QeKp7j#+`o;oGHF zv!tb8Q^sZqM>ff)`vjG0m6Ij?0`6dBWGg(Y(?~uyKh2oBbRzBxou@Gu745CM?LVxU zFB{}`)POWMAOM4!0IO#PG_v_mEy|1+5hr&2Wveyp^jyP;Ddfw$LmiJVyG5SCjGF3A zY{%uepQDma$bFI3N~bDSQChg`RoIQ&+ALy-u8{{4W2RA%pqujO=*zEKJWAi}Ab9?y zvLOn-$WfI1+u0AbPu-f8ob1|r(&(Y<IalTr&?!%4l1?EF8BcG&bXUVCuci$B-@mSZ`8i9s$$YhoOLpfmpEKW*G?g7&udVt>!J~=5#0bx^bzdC{rS7xQWq>0 z+2wlr^|&?#$W=ZGuW#eS#(5_lCp%-e9F*RZ-Ht&BDCf zpP2z>@zS_8bo|b9kD0%#w9h}_X?Jgu0^{KsDDC)tk(!>yVX%DxZS-Yi?0wNKv%X{p zzcoQ=A9PkHT8%>W<~JdZYH8oauBGN#dE231(7MTS(%eL%l*hZHj|(4AU;WG$Glulv zD49L1GtQ(Ok~I8bnc|K+EvMfTAI?@WBu)OxWjnIk4SMHj(L_~oTuB-qEwc|UQhZub zCIn-y3Fv?0{YBvka+`)P@}Qn;NP5VW^gRE?rG%<`IggbX0w*_l2DslOX*iBB%2sq>TRkg?O?j~gi3(8%MAtTMHh*5njMbiamvN-oZp`XLxb%v7j= z8P+5=8%a%e%#1S2Q8y|YYDg4*;nARZg1#`v#ZR)rE)rqd+=jpZodvHhd)4VV6Gxgo zf+u}mTWI{FddgY=zHnf2N3UiiX3Kb#PYr2~P6winYjH*(_`NXU|`H+2^#Fu2^<|UA`QSz{dSRK)80oMs6xe)82f|r5pqJ4;D z(ZyePbo#GerZTk~{30u86r9QQm$F&KRSc>iTSI>+c^cX}#DvSds&b z`(S1c{eD4_4rE%yEaF>IOR1jm^#`=^@$yu<@Zr&(J&oqs%$;W08fg@r;=IdKPnERP*9*C==m`~^~L8#q`} zX>txvT~g+_?25vICr!D+mXpTidl$ltgz;PBx@I={0+^|@ zZd-feXX`Uwiafb`Uqc_7a~Au=4r*UsQN!ZA>xK_7*YLKO$yUW8f`#&aOgQAfhl(D{ zx*oo45XCQe-iWm?sFShK_;_0yNV`LY1+0AFd`ep!wD4}Ms{-n1^3>Mk&UPg8_u0L+ zlLO=l`%%Rj(p;f_KzXH7??~3S&Ckkza(h_YeecX{ZFS1|Q_Z@?ng!e=UExDQS4 zq~iE$&PD07dv9IU)O^YR&bX>G-d>wr39IT;;B?zhetnDjBgzt&&1R?Zm-e8(?;ipu z#~n?}a~&&fsKKVj4C0-;(<+!&Vn?xGJlMXU(jv^?VK(*Kwe29b6eW=*wNPdg5{f4F zrVw#GEJJ_HUpUp0QR(mg<}7x4@W1=FL$KjC~XWVhW1 z8L+-?vqj;Zx+Y(;lmEZxg=V~%DICf1nV~SukvY8InB&+? z9lqY1i`Ng89upKW);X>?RSs}I1MtjCulI?9WHlBB~_- z&sNGnF~_%KzsuI1C5yPnjP{OkATB#hstMs08f9kp(`%vMFT3;E_6 zGL;YEPboyuGevUp5(AB&Bw%X#9 z%SSnm-w0;f>=l%S0Q?>+j$v1!_lV3sgF}T*znb>OhkSk|gnyDLU#kxxwQOJx5-}S| zuM+lbLgCB-rlGWULRPiMUaiN21^K`)z76WW#-br46v|Y5SPt%w$3gQ{*N^1;+)eld)rFmKWNAJ2e zYF4W|Cjc&G=GZ7B{UU}n!h%BZJ8(Rv)3cV9ZMMZ~Vtn>z!gg*rb=KSVyRkTDq`UmN zt0s^lwijbWL4Pz_r(<4NyBYCGQN@=H+`LJ->!TWhC^X=m0hkmPbu!}~S5VZ*@Zy-{ zsIh#&1$v3PZG&%d0m?`lKf*5lxk!7+ce?Tilp)HyGChVHIPM*n!wSrX-h1vNID~{i z^sn*>;m5S{>Q1z<6=8-7-jPrzy8OZ2d0vxJlNVzucrzh9BGYJ+?m2++-y-+RlVHjd zXKC@!Fdjp^$Cmvk^AO+{e6@4}-)|A`IsP~#yaek(XeWdRNUZ?)#8v!xo-;t9c)T>^L_KVPFAmRYN)f<% z3?*~|zoI!MRged%>@b~ZS5cODS^X0^bm-odE0{_Tw<)N9-FI~l=!EqS`J6ni zJ*qaYcC0q~Se}|Ok6=)X3Lk?A6f$W79sfUZVeHXLBXfGaNgrmVSSKV%u z-<)%WRi++&+`3b9=`C)nLfOGQy7bhOxX978TE2`zl7{ChF&oa|@y9x?9zI4|SOVea z@TYTT7NrsijfoVk3x~$UbByGJkh(Xdt$cvNe>6vIGKe=;+#j$T za_RaI*yM5bB8MOaz-D@{D>v#A_I0qeq3ySX@i5}eEiSs$kI^kg3sTIoQmZKTSX5Bo zblH#@y0HH6yZ2VNtj15JR%tygbWyLU-i7uJAYbTBb8e<@N`6<$Qv6^$n>qj?xC$XA zV!T0i)XXy3tKD+V8`ca0aB=65OgbUHFwW{H|MQ7p!sFauX?!M2lHpD@<-n;nZ9DM| z`L}e&4y)&nXV1{Y#zbF75C0p=N0;M0FoS!{;jPN_vIBH)cUihNB&W&DHtJFpH|;_B zk}47xK$EYSHe-qfyYdBpK*)nKb`_*#h4}nvRZJ=$dEAy^xE|pk$RO)D4jmK^8!pcS zI&zR@EqzhsWg8}L3h#GvXZaPATp;d%vZ41thE;7A{(afQcc=!eYKD&Sn;S}8;1l@G zEtwo(x2lFg2$wo^dyU085KlU|cEN!YtmNUSJXB2W2A(^Roe(P0NM(@1=vYz)IV>{x zr^pTN;W$b2KRw9G=5j*=M*tzvvNWdaOAJ3^=T$5Pj>~qp=lZgxsIa0PD59v9qDlB? zwu9-;n+RxOgvAy~3oA?b+Gnnbb&oK$CxkxFD>sNsKPbanuuf zb)LW?e3kX*x1WlcemPj*A=2`gLr-$jy}lm}6#8At9)^vh?oEGg9M-`?34xZ(G>l%+ zT7HL1Ik9Q#Q`VIN)>sXTc#)t!7DABLA8PTVxNdb?`QF_ReHzZ8)8nc!a%6$UU)R(Y zQw_Y}676~_tCgf?yF(<8#MHCbLiit;ulFD3UFu~5Bxvt6UneJ9y^DPQlh*#sPyt)MGx)=gal0NxZlpTP^*RIR%dg+Pw)TZB5js~ zOzuSdaN?cS*GR3wO}v>;uSy}nE7O53nwBQ`PC#8rA`oeLsYTk3E2ozj$ez142p@8X86YxA!=@NWeh(c{8pz z(3(fA0VwGbJBiQa3Z6bb1X|ebMfxkYOAAw0HOK;@%MC#H(G~zQR?wA+GaAS)bQ>J4 z9O*@fB`k(6$^`S-L9Opt5vD)v12J^%R`p_b5pAZ8ZTyDX9mV8D>qucZW&5G(wG~Vs zGl7GdVOK;leKQI?vXE%{HoRtkHZmC91mM*V=r&8$1i1VS`+#rWVAp%{vqp`t12ib< zsMTM4;6$-&K@7LRk56Jrv9m&WrU~Zq$r%#W{n?m#^w+CaioCyt@O_r{N|8|6D}Nk< zVGC1tJAhDFjM>;vlG(jxb^J$Lw|aI>+lnU#)Vlwn)lN3wKQy(@Igs0^=y(1$Wv=yC zAsePgu* z4`a53KgYut?&9I|{epOjn&lEz5A`eRUP5o!7$}zw+fnIS%TJN;RZroBsM};&T^pck z^kwb?)n9(g16#+3JD8ht0Zd&^Pw!4E&pL5Ya)E8SmiV*u(`iiJ2H4OEEPC=4M*^t- z@*utFwKk+Za^h&MK?S_P@Sa%rI&eC62`AelcH%ep%dOqB^iy%v&Bb6pPh!ykLK;FU zj^!DN!EC%wdU~SW>hTPuqhi*&zqtSO;2E&n+Z{2NU}n+xu})f8i3mzWzcN?&#wla# zz6*r(xG+mTwGvQT)|%`iKQV-{GgC-P-00*|=lR1$l^lwowBoUYKHiM|1MpPLHso8i z>&>cI(w#=Z(4wBL9u@xG87YF=y$m1*s5wJ_s|0Q6v#T&uvu)1~t0U`B+1qj~JS_0P ze0w6AtU~K@?zk)P9_P=z{>K}xb^=6JU6iD`zznB^b(I_uU_OoAU2dNmJ>&$IgPa7G zLmqy5qIJF;f-ku_)%FIs332YmQd6Q|8u^_#Dx4^_&(K z%V4j^Vmna%gu|q7|Jh>kNVdk3pC~RsRPMUS*8YmCRVGwk-5O|*tTe~xy$lSaZAQ^F zVW}wylp|B+W&*x(#p>3IHbW3buTCq$(G1riOl`$Qv~><^oBwLnLa6IRyS7>veRDma zMrPCi1=kwo6py#z;A=6=V$o7mIdskY&Y`M}4FJ)*`i{d4d_`_#5zYbUCo(NiA)*H~ zu>|!JPZc42D&olw5V)Q}c#eQDhCn=ZWjTLBo-dR<+`p+ey4G8I^fJgKc|P>oYH4tkK`yE|gRV!>ZTE2Uobnmp^{SY}CWR3yITEi5fK zR!mbQ(ZRK#M!vQ1cIc&h6)@{m0aS11cQBuYlP;%7ZtB_?xGG9D-U;#$0HKX+D3Oou zeMZMvR#-KZuzZ$Qx1inQrTWz!M>Cu}cq0Jxc6_iRtd)GK^G(~WnO+psoN7laxW#S7 zGsBtnmeW2I!6pm}Fd-$+`grgyL-9d%4=>TAOC;QGJas7R72TC?J}?piE1Z@sWT5p` zoD0V`7q#6-;zm~EM7G9@smmxl$3zbkJ|DmI`}#c62{Y+y9M)Cm?)BW&IfjMm`j}vo zyAYlfeBDY>`D+`W7M7zIb7b7=VXCnglsYkK1Cdae{ zt~uW@5_9dmqDCh{4Qft1f#@Q$_l;|K7ASkS!4fRl+bHFtsfV%??8FJlXTuA+4XZ+5 zMqh9DoNAxOzi*aP*`E&1>gSzb{jKK4(5le8Uko@{jC5tzfP5H@{C*06$?HJ~AQbo@ zvENq(&!4(uIAD`JsYVSzIAgLJ&))GZc$0VEe6;6|{-81514t#lRd&hTRtR|WfJNol z85|@M%wxdP#Al(WCxiSEgL}@;%(rL|t=B zp=ks|<7=WV`^tD~JZIe7+O;%~V@@upuG9B^kxb78b!V9-ZV<{sfvvIWB+Buy{JO)o zQ{c39R(w+fQ9iF4%isGcMAjMiW-B;XRm&Y9aftkE~m*({H3<@PF{%S z#@KPBrdyf-*n1jtK({rO=J$A*_o*Rti5#BWL6F&3r7pC0HZOQYeTLnKC7Z4NijYsY zO)JJb{$3(5KoUP?wduauUU>6WYNzfA4~zW6h^d_8uKg}P{oI^k0G0yCLm5CG!3blC zTDSe2ls)|dLsqXh0QFGe9VH94e%;=ZNKzKK{IB$dtQe-{8sLX z@7gqaaK(z}C|(ZM?89vmSXF}3xy++ibmQ4qR`SGgH?9sVAzov+80H7LE;d%ux)^wk zkdP_y-JnUwvZll|4rK@G`M$$^4q+Bvl=*&zQ+Q;0I=>f9_Yi{avUX_}u(|5X!NInz z9c?qF0&n;>IYl{J<>sXIDC{r0>Nms=w(I!4looCg#CJn1B*btmA0BFYh|LUVepZmU z4qk1GI|vt1PRCxJ6HN^5ttL!8L_U#)ka8h1IjY^9@6E>i znBb~G=lsa)Lq_xL?syHj2yr{)noYB@eg$`GKFkl$8dh!0@1^Pbwr|X}?lbCRy~CK` z^8$FC7QXD5j>oYi=s{No1g!EyNP2XmZOWOw==kELGyw^eo6VCWx*+{a@ z;ij!5vG}3D0KjT=s76bel;0QzYc=;q54c$g2Bh69U1mNl@3D?R7y`6fjUYv^03+@} zErL~2s{MDs4ZPz!3_juKmkUK=5T0jIQAbf0sC4Qg!?Yh)DE{L)-p;*f!G;d}9RAH~Lp~20& z{-~m6oQ-6kNOULkKwyshPe-mJYA<-r6C$7z1d+jtVa)JtIR_cxHuPu?aBWV&gpX!2 zY)UUZ3F`t9;Bfyur=*tvYJO*n^NVPO@7fh$H^Q~C8jr?&pnmk01xU(#(C6m0+h>Gw zO|ag)ll2X&aU`2$Z+_4QFhPrr$|hIH##+w!U-?tE5O`C`l{kZmr0axr%Qtnw0L?O4 z_1qy%jO7j$1WCN!A!=|v?a#tx6kz)e#@25CxxWK;A2j*IE;Ls17B#3Mf&EWz7hNoJ zKLMlz>fP0x5Y{{q&b#aQr9b>{MM-$A>VC~)Z6i#5PAQ1AOjKB7Y=uie#SdO($n8Dn zCW6eIYZXU?;5YMpca{jNq`k?v8BV@K`SLD}krE;ex~-KnYzT2HMcRmYU(ET-Ay;Up z&r-BEA_nzewtDbJSo1HBMnGd2ia8f*PQv>25@wS>4RQ2cU~b0syt(9Lm~T0ZG^_7__ynBr4tYbE$3#hXW#|NK^p0}*=_-9lrVT@_ z41sN=*GE5AT8#~4D)2mtk&d%zWH|gO-?=WTe%EjL9#wS}ft#D4e7N{YO#ruY+Ip2- zFLE0<&D8EGi5KiG&M1xqCd)TeKl|}~An_Jy(g6uyXqVL4XS9s@t zPVN#CehqlgsTmBpkRLBU174j^p#6V1So9@(V*sWIo3}?yNCT)5yKwCbGc5YS2ZO($ z?)r0C&Z1uupDb?T=~^#bbcrvQ&5EZ6lp6fN1^LY7{TID6XLEcoO2KGd)@GD`Sj6J; zDfT+@iu3=CbRW}mm6G5WyY4n7tQ|yehS-ji8k_0ccH{2fqE{!E$rYGwN2_-(DxV#1 z1<&{soPaY4)EgU83SOrZ<+ggfS8IeJ>RdB+A9!X+)8yH7KZMl5 zrKv2k>^wJ;@)mvxQ!RR!PBL+T%e6BHwAw|gj+z{8jGa}kH5`whtnY9*?pT&iq_mK0 z%x`uSDThenS1w)|Q+Hcg7fb$-bAQ6kQ%Rw&K8@4m5&^iIG1@6T+UXHc{a}eYt2?kEO39{R7M_wBvNGBOGN;<#2ZEuJXhCnpl$&+XQJVzL>|3 zwABz_#`a~(UAy{D9-N#4D3u1Uj?s{jEG+*LH#`;n?sIurALA7qZPyW`T|<%}>omzD zG%tuR7DhVuR3+C@iRrZ}g~$_`gih$O^HAM~*NraqOl`kb02toWz;&{+4;e_(S@W8F z!%p7p3GT6S4#Ja)PK;!6HuWCqs1Vedn(7lS-r4bcn`oye(G7W<6mF8`A_EsH`309C z>C{w&+z0r&aAzmB%oBhoeRuKi*~!!yZ--0Dim6+n`d>D1^z;MaND#mSS>mBD9B>@b z@MVW`@PgCI$A>+YqN_DybwwL$?z!fOV}l0#{)K#)cb=!m5mTu$7u$;SBh``eSmg zWaod<4&N%X9|~Rcz}N{Fa_a{rQV^+dw6!6xYT*L-o~`#gd0Vuc-Et5I1mv*xT0s># zMF4(FyOTM~hmS~;y>o8H1<}z%PIQY~y-5esr%|Ori~Jxv8WWt*>U2*MpkoVdE z$ApbC;#e{RR&Vg7pDF#Ci49gKu=pn%g!H9I^W;za69Cd$@(vo`CLcElv*uPYit9C= z%MF%^pEmhtybjGYv4){<`=&OE zw_df|U|qn-5Zm&A5lYsWuH0OKwqxGr_w#YzHd|}cdf?QHi3cSR)Q8WZE}yUblty0I z6^Z;9@no0^Oj!}T9E*J#Vl~Mu7v4ve2G|Qjg;_b?@^}Yo%iPqIAE|ztZjibUR*gWL zztz8J5#@`i!@RUt;~eK79~yrG4w2luVKvY$P#zV6mpR_bIODm_GAv2S`Y(#fBxzwf zHz)lW;;1Tw4bM8WpKXPZP@$42*UJ`c!GWSbKDS zQM}^U#OegU)zq;C)CYdb?n>XsUC=OSMzS!XRuyaRyHaL(de8Z_gzSBn&sOZaK{WlN zNkw;y(IbUGoYP|%qgss!F#z4_ijypUQtXsGhR6{ZhIF0Wy=Rkb5hph`^KE&&j8_Hl z%f|5UKE6XrYOZS3(EUmqs=zFYv)qllO@Ok9NvMZ>^X9S8EHbH*kCt7a;+#*|2z)}7 zV`8i=t9OfsfowYM6tdGMQ8^tny;$NU{JxL9nV`m z*rE3oATYBF_xmNH6feQ}E)80y(IANWl3Kv)66xSCcjVba71-jdFdIKHz+2)q*R9mH z^3ev!D$Rd7SMkXvCatwJQIaZOtK*+}l;oMT>m)D-Ew@-wj1ZKcuQH_us2Vf+9%_Qi z+u8}*aNvg1Foy3|Xiq(NWUiq;3e0<{>nJ=f$J`WJ8_Y~o zm}m#cbg|M z&bW0I1h9w6W@kTDXsvud-lMc<3Qlj63Dj@7YWdnSs2x%X`waD9$O(DaG8lI+l5;*> zqO>{&@cnFn1)}XZYAL!r8#Nvn!#k9QHB7G%P^yndDVwSSrZt8CfdZif3?Zoj$geb7 zmFCK|CQNAWp=uS6KQsz&rnht97{INS`3T3ydvGPJ=7Q-pov`&kue2jRMWP;Sr#)a7 zkD@Ckjxxa|Mg{Sa3ue2XeF*M}i8JrI8vmB4Rds!=xzDeU)gAPLC)fn0gER%fu0#R$zD7j*E1#iyV0qwbq?ZH04O67#c ze(jf1E9hDPKAdmDT|bH`StV;A2{J(fIT38e(>L5*2951#g82=2G^RT6u$+pHd)EJ~ zIAR~#GtmXTPx_#hqo`mrDwe7j3SXcaiT+(ULU+SblU=!-vQyAf!Xi=is|L~v{q3O- zs7tRc`X_}$kf^gG%6!E4O=n>7mp7EH(z_x@f*@xXb!@6qhi|l02ix6A40#BWg>%fdgSz8Rf=4@VJ(6!1O#Z+0PHl*nnD zTfs^Ky(;7u!rQ51kMcG?IB+%nNrz@EvC)fOfI03`hm=V({Cw9J#7?-Csc!rOI03Ur zx!rS4EO{1ao)7XIGtg_B?|h&_;Jh2di!I9dFP`DqQu3_gv=)V0bZL0IJJ!>TeV3c4 zDrD~*_kQQ$T%Fdq#%+^+CFXKH3`!RjQo=^wzo&+MV#`9HBFdCZX<-v5FXsC%nNQy; zx^l{@YzOdYeUvu=_PBho6ob+bngG@zkJQ5Evy z9)qGsN9}C;In?YXV=Pz%peyBO93UJQ2$jXFz_(;0B4lii|NSQGv#Pf%j zD)nO2QyW!@z!C8LX{0?CZI0%bmBILZ7o9dG9fQ?pCT*ioL?v6!`yUOQG*u?20FFHD z%gz<6@mSqb0FBZbdI6D)A7BeMU}XS((`{{BYPMJG0bV0nGd&A)Wi&`I8bAK_#991 zmhV`f#boK@!O7E_6Y*axB`7<@xnO=HHif)RL90B%SQ_21ko(Pq=NUHsfioS-Z z-7bH%av$(g%=9;z)%bu;^GLeLGk zRuLUiPoG|HkR=Qk@;lhM7QiV$i_5CJh`XZO!mQ!v>JKvwl9foG8iEe|5Eh&BhW#)K zYNyxZ)Q!Pg-rFIBs5Y+hE*tR}fHS%a%2e%<&r52mBo3mEXsK{Vmvt`f$tUxI{mlV( z(s5<9EKoBCXK74BwW@=>S8@;hmprL+ZQdU!fuIph8X=DNz(-gCy1KAeHvqg>sw5l5 z5rX{)xEsXbL;GDyJr}9B^`Fc4SmXg$(&aeeBM40SV*K|h44^4aFA~nLEI=Is(#{~W z13-c6M1^kvITP@b#J)JTdiYy$ZwJ)-$+pFpwM+O8O6_ulhek!otpGmli{$U+Hj|^` zbB@ofc*aJ)-)0;3T1aybebbJYh1?DRG#Yo>&|zroY}Zz%=F~ASD!zGVzwv;uTB0}FIOyo0 zy&L35;+?aqqL26Tc%#vjcGT5^c)4BohNG7PB*FRKo_fs*((lHqfOW~4FOg5O^NRIA zB1_H|)BVDmd^upq2{!OzTye_1-2X7o9Z;v}hXa6^YN+MB3`nanQI7|d&Yy}7HqKdT zwtoOC{qg~}(0rkDINzO>ME;=r5Ualfyb~%b9AGmT_#jeY(9hZrT?R9ia9XBEsj|E4F^vsZ_J|$DJaUNYplt^{G8Yp#Ebc!7e^zvpQX_%yvmDP$_|tb@7=|rM z$ROXgua7x3D1rA>_T-umN_;4YrA7c_eB+AFE}$P@x%#XDNOdkX^#WX~vb^#$KR`O7 zsoO*V0hM%-1cZx;huEIY5I83(HykBf*R#UCe*bCru3M%kVaB(pjf?wA0gWCT4S>gaEMClI_7Ko+D6-S8tX82 z)4hFoLGYZ?t@dLPCgf{oeRLNn;Ui6DuZn5uxY65(lb})yNjBJNM&af(FlR}fSgNjz z;+KCO)14i?GmOH2`ATY_u7JKor~xV?hSJbPxgFP8kN2vMrObj5T|;wUS5|Sk-sQIY z2^12dJkZNctuI18lnETBZ$dhlDeCsvx_o~S^Y zBo^5GXh~hvpr~d$GUh+aerpZLFgjKSJKX<~VNL)U<`e13ydm)PTU-UWE9Gv%>Mz0S z{Z~1td!W7mu@8_gc;Bo|fN!Lnq*bz5Rg@iwpCx$tg?{}iQ?(n}yXR7_k3spBx-JdB#PtOo|< z%H}Ey5i&g8*ZJbQq(F7(HSX+Hh}kWmC&43Dv+}bY^RxL=fM*O#1QYHgq)xxn5bgwI zVu^1y%1>}WsCf&w!$IMLR{xPy|6eso?p6=yd#XHy>h*_*$)hSQJDwU%{vp0Xt`yu_9?Hf5l!D$shjf5h zXNL2UkP@34-*6%fi`-3ev|@T>_?(Php_R5gv*h8`zVYZLPVHFu?$2ksjkNz;M*Dx< zze(c+HLlozQZZxMgz>dWX^?qrEbU6Zad{2x1f$JPlADCBoKM>I*S)I==+*+lz z*vDc2^SvCkoW2Xw`_n;Z)AbU;qmV>4d%_HRAXsNHLvmfWSIm;&UVRBRDfTv>U-aBa zIXdwKDA&c5H8fy0c478=UPZ{(}M( zd!#(ocflC3DgDq3$?VS`2K1)*{;IdOjP-p;A;Zb@;a`PFnV!PR&qt&DCWC{}u%xS7 zsefq!Pr{sa^w`^%$I%n_q^V2V=&SL`u6Ry6P+VH3J&$?pq-}6L;QVQl3DqQ+wb8bX zwABx14j00iIjK)(O=ZEAPB5ze+121UP>U9OI%ESBsUw&L0_3Eq$dIDBz_qh(W zyT9sw@MYX>h-$d+`i~vvtm^n>o-@m9j>=V|N{$(0Y65MOzR^{H3p(MpULjkYl+CBJ zG&7cRL_zrwBBEZcP+?>JuZERgd8at{_K@U|E+@BqQJ3nv>KsaHIa7x@{b#Z-Nh9BAcs}>_4oeBoPNZ*D<&C{FbAO?^ z7bAm3gQsxvo}H4Drt&ACuI`NiZ}Ps#j|6YOfbYt>#V>yQJ;m=mfgya#D^!X#j@UB( zJk9VKUf5XL+BaQEETZ8i7Zh`$548nL<+Txx4)+7!Ys$UYbp?)Hqe#$xIA#Di}lIMz0>7th7rxOpErW>IS6MX*SJ{sTTl`2Tb{#H7Fs(W=%`dB=Bw zzMsh-OK0L+b$ez~*y8|c>KM(8mGCS|r#tCO!D94opGP0b z%5;35xcw6k|I&}rola`k!p2Jj(u8BJl#5;!>on&vU~~?$sF}GTRGDmW2G+aH^JX}S zwJUbe8leaj`KcB0ayvXIkpXAo!?nK3%*hh3Q`JB^v|r$9eb2pFSv}_kWR@Hnt>zvx z?*v<@YvL}vj-7Dh1G%y#MKN`_!U)Z%i8g}-7ddFs0F+UYWuvRoNHPgHzX&m z4tBl|(^(ydx~{%AQG(UKDhFtzTE^DOocnH`1b1!Qz=IKHmAj+$gfoW3rHo20n3*sE z{oaz1sf0^iS$c`C7;KEKH;t0JAiov2Asy$H$ZuD00KsIlcnb;Fs+m8utGDm)eIblT zPiRS}()&o&2fYC%#V9>dNz`JmifEF#>6g4OwKg;{8Et`E)E~jJ+ok^4V75S_zSRs8 zxp;!361aXhFZH_~ZrDEM*4@AXH&Q*%fvHol5Mi4FP{*oW9RWvVRiL1iA`1E`b(Vg9 zi7)kOD}0`iY$)8-1PL-n+zEOy^LaXJIwDC$*k@|kij~wgB}j;ZE=b9Mo_#J-0%O1Y zDY>a1`Y<3ZsK|QbiN4aMgd1N|F42Eoe^qXcLfFjVw}z%jF+ftg(Ln2vLCH+5i@^Fb zdP%k4oaG}>P4#(LpjZXqltwIo{;jQ~DXT{TTudE0!}4$7v>uc zO~f;rqT*?2oy951Zcn+H51F5< zIv)XGft% zaam&`k#EqB%Z*jKwc$XKfOsyqcCOZO&fv4<46x5O`O0pk`|q$-(`aFQmyM>x z>{&)WOxtA^)c#FoQNu}3G{Df1-~RchfqzWV0bS7Gn?uqI_VDq}Qx&W%9bVc_m3)NR z@XVDn-c*u)HJg&HkTLjTmwHHYq4e*-JJe?38$CPyDOR8qz^{;%B%5cl7SZYvZSc46 z1ZUC;ch4QQ)}{#KBnl6%gkC9Ue8Vt^$KOeX#3cv3`JP;?5;Q?DFbD67y9vwO_7$>r zZve_nSL^mBf6y6_x|-i>f05L6+f~D65-!&tZ+1>xGBkx8627)BX|*Jflg_n%crBa^ z6xM=eDrw%J`Ick){sC3t?&r`g1Rh8NGb>5DfVqNLE0%YR_SDj#4PUge=09H*n83Wx zsuzm4*^dHQMNNFn@&3r9RJ91FJsCY(sauThaDXfmn6M*Xw^flOe~3D@=OoG5W|Y03 zb^~*gPUXMyPWa71Cmj9zK;Edcp}Q)Uf|A5>x73jwxrGed!ukGirKXeALr$^DD>KTQ zUYt$^p?V-B`n8sRzjZ$Kx6;mY8Taw2Yx0B6-9Jj*dgl7+oo-L{ zkYddQ@x};T<1(fH>V^?^v&OTd%QziszW7zN18S$-eS8e~dw=v4(g#KLCYhK!H_-?t zUsUesNMXA&Hf#kU#gd%$*Y&y%=!@QoellP`zHH@lNhWCI258-&?k^zya+fQ{(`*?i z-OH~zw~tN?WSoFC+sFZD*PuAc3*?$hdH zC4e7ltfbexj5u$BHuwEp13hj0kFkGtquwVEx~ashX5cSWpi;53vHcHlZ_DngVUI>m zrH2^r$hTgU*XNJRC1fVmu^D+=0rsccT3`z*i1OQKZGiG z6qJ&MsEqaf+mP(bF9$hWFTZov5g;TUG@?QfU2bi+LVpwRZQ5Gka5TYP?lkB&pP!8F~eF51nqM?Q?ltMPDSDYOUKi_AKy3 z<$RVQa=lMA5KY=lpXclzXp|iO%;_pgOz}*C?( z(!Btl^pH;JukhFh&eHLpA+7LZ#x1MsA{(57*Y0Q|h>Wn4f97ZMU0?1lYj#v-oTE@$ zHxL!SwxpQx`Y`Q6y0_!>4M`7$4B?VNe(AR=i7Xi@5P2XlUPf0d9Cm}6Zd{A5+f}sZKA?WF82E%`Xj?Oy z&y_51l&%29iYaLIhz$^onBowdTE#{CrW}h>Dd(MB?=eni=FwoU=eIMnswE>;-HAuw zPg9r;ANR-IU|G8R1>PJ=H`E$E(A;xn#p=8B8gkHBGu~>n3=idE*FG0>i&Py}!L{W! z;a8kSVib*RfSlXr-~1@)9F1@odT7&6-HCV^cPqKjVXBMMu^y;@+Yx|8YX~vGRu`K4 zI9T%~tP~fv1YS;rQIyY0^(eA+WFyQkhCHN!F=Udl`I4OFyg>+T+&{2&g$>feTxdBs zO4lF0P{a!Xl!4_LtCp);bR*>LFbDgOor^J35pE(x8KeQIhGQ(X4760?F{#Bn+^`)! z&7+avsbh?oYD37M+_0*BGKW{zJNJy1)zG2U06>rp0D>mlEj&nxm=RcIA9 z+?`p$aoI#@ExbGo`}GFs@gCUseHPP_17xq3CtCjRH;exG6NhMU`x~&k&{{XFkzrcV ze3IGH+|0HOlkPB9PGi7l#g{|%<+?z-XHzD%D9r*9hGST0I#_4n^YYA6UDW7j3+&m` z?YfNFXkhwiU@kxQ@~4a0RjYA*LAS%)R_C*aU0G3!3`BY>pBUN9KTk8`G)rKj#y)BoN&mN1?Fn#3ML=hek-g zA!ZWCPpl-j~CS1{=uo@p^ zAk<&k;?m&)RpsA(<#t08J~9kXa@Ku3gG?4pya}!CJ#CTG$}FdeHgS_R9*) z(S55`xpP6m%0pTbu-p{h31jKQSyF6cnRHjK;%|b>DsA+5KEdol z8Fh4zLxio;&14b_?qOS$EaQ#dk?8n51g`iQBS3?eBC#U# zBI9c<%xkpfIfE1Ds{ZP~Cthxv_$LLFf+cN(GDVx{%S=uZOs8a7p<&PB*qx|gN*!wJ zpPb9JzuRvU+U?Trw4-u_GQbjw+4D=Ok}i2{VZs4^;ba}UUd1*H-(lFJg)h{C zdcPDx>hsL|0`(4coP*Y_1ZVlEc$;!9k3w1Ap8`&}i?N?vf_tx8`LG9EKfYpBOXYEs z@4^A#=hiBcAHNAjp8_?r8Q9KGo@G87u0W{{7bY36`{4iUyoiNtIr0TJbd>jo$WFuRVjUhc9Aqn^DzyR!sV=)ZS^jhw&W0A z@XUiH7hPmfQzzH6_M%A0^1~l++mQ+QVBRLDn@EsS_^r;%PRm2cSLM&I^BPQjPoKd` zDJ0j+$=*|XaRw)R3kh9i2bj_ePwjmHxjY77$oG>jd$XS_qpRo2=*DN++w)41`3XP5 zq55GzGp^FCQ2}M@BJVVcC|1m_e~CUZ?u5!2Y9IbrF93b}5WS6G@1C&PV!6!`KJ5Nd zqS3vT|uu287#C60Z2@mX=0UZSOYrnK2l1y34(Y?QN4VR5A{`T{yX75#oLbZ zfX#6pBOOBJIunDY2QlFBaF^ z1}MU)g74Sh2L5gmhRW81=j1l=a_Hhw$o{j=S|)NwTUjux;RR+jLWI~%8_5z;*9{8r zmgeu4@CJA%6uivn>y@I)_BctN5QNQBKP6NlO3rNQGP{^ zl`X?)6@te>TB5z~~5IEzJz|ZzqClfJoxZT{ZWH^ZWJt_%9X(A#8=rc)~b5p%Mt8d%Tmg;qR9(#wNU4BSQ1EgiSj|a(=AS ze3oD~V(jLKOu16g)#D?%WNs8?Ps68*CLJbOcUWFKN!zb3!r&PmmO?=1S>PN`DY(T+8tB6C%$P|~WoX^|6$Q!rL}42O6vQq+W9>0q{l*WI zwCm86Qt!~@C*L_R7X>W-**gjjak5M`Dhf}YZoL@nNP08X_}aCb&(ePHkF?f}{KwbI zdu-bIBlR?35_;CpPiZJREUCyj)wdA`PRcs{DU5nC=zbQ0z$FX$n(;N&Z;ilL)!pYj z_ht7}GO?NB5NBZ@R=jXrQZ+ndm)90+EQUqpqW8%^htQifJYD4?K2O`vbsVI?O1~mQ zm~&QE?3bLS{Tb8n1JeuT+#7D7r3yx}&d?)Sf8&v!?`9^z=pF-2c7YG@#&o8>s-Q2I zP_Bva8kJY>0;+>Q(DVm9N#-kdQ&Hv*GHBLnjCDDn^grcW^1lo4a5ZnhpkvV7eBOD& z9Gqoa4T|THafn3gz@hV`P&kTR9O}bNhIBIdt?@u7>fJr%Bs=fD9@dVR^FU^#I$N3s ztBwd?=U3apr-kB$q=gc=vd_V=JRQz4ka5&l&F;QyROez&iX5KEz|ly$5h}}Sn_7hc z%#9gF(qE5p-)1WRGh>mrOr`=)bsduu5S(G)A7xePU^0$;9fZW6CV+wZr{9j+xrb;u zZu=rgSj?@8ZXK*z+ygw)0M}fl{At&$VxUtp3hGEhx(*95z`YE8wmJ`RTG|Gk=-b?L zH%>DMvemd@lwQY#w z)VCh7onX?hCBBPqDe+7cP2!*`jNPY%CQ*{nKNbEmYjQgC0+JZB=!Qhnxfl?9oB3#& zP89-E6nGn(_ii8XeKqI(e|PVdNNiSUAS+4{;uF z1N8EzfiGynm!rG!KTTtp5s_A%AcGZsl70Y;LOgd24E_G26MIs`Zf&nF2(u`YVnSTI zY*#r9eB%H7t!9?BsrEkaOwv*n$Nclf6M&r(-a7<3%2Y|T;y@{`U3fmJN~KybL+|AW z^FY0N%0(Yr2H)ROYaK~XER?l>Wn6AqN=;CHj^jBaJCL4;AMih#v}^#KGuDNEf6`qo zF`Z2ZK+y6$J{+DjN7qH(t!UoF3p9bE7zDn5`glP03@bha$+uuQsxF;014&2zbE`dP z$~*gm-Jgm!4@>XzW>61Wlg@1%(2)OAKk@fiGw{U?5vbSVFyHi5rTAoxyit#DiaD|1MxO8<0+PS2c6*xxs6dXjM_6Eeen{Y7vp_tQDi5x{1j|N(O!jS)1LOQl}R- zBiR**H<+Ez>q)uAZ$j?`kaHW7wZ?UT=4v4M`lG*_tAKrHd20WcfGnJWD+qMi+R;*g z!q!BuRmMCDqpI4~KvSa35NaF7;M_^(urpJ4*wKTN_eB^-#5Rn@EGhs97D&C&2l07~ z)u_xkC`RN3WQ+?f0Y_^(|GdBew6OtVBQmaJ`3V#K`K0dtw*8i{ z9roSFguiM5^_P8E2dFbaKsFf@WcnK1bDwkm9tiah*Fcc{KmFbXkTMu3&p(R2JmsK}&>;`R?r zNfU-nQy|YDK)6+f_US}CMsTrG2zPX*pT*qhk=WsxnU?jE|2W{F^4q zWkV-Dt*Vo$bNy%Y&-$gOvJKaj;0d+J33tH3LscWG28RR^`Hv$7NINxuT*^N^ENE#N zg55r0kS=y9p+lpL*Gc+K-c|ZlHi@%O&}PSoBaA+&|01l}_yCHUz0BAcCK#iBQ&^k^ z6eVzVl|_J}#Esl&4=_OP*Lk8DBe7FrR5NH0FzfCR#HzijhU%;xqE?$cI@Q>qh=Qm^ zB%h@>g1gIT95-a)L_eq-Z0}phx#_8XNP}s#TgLa!*i1#q%HBGFd9>E>i|3tQW|GSF zAT<{OiCu*C{c)}p-E-gDb>^TC7ZXu*h8AP(^s4s(U;cY%AQFM12@Q9CQ%Pxd4{@xL zsj=Y^WBUlllo=?-J@~XyxC+8Y`V~3a1^w5)7|amfEza zlA&lzM$a|@OIFW60ukKiB+C68uw>GW3r`TiVdGm0z>;J0ubRMndVm2eHkcC zYw~Pkt{Ixn45&ML2~^{#YkW`lzC0YQBVBt6+~)C;_D>;J8Sn{ttTyQ)A*C{8mw-ID z78|{$j^=b{1%SSU@Rvj4r||L53P(~PWRN2TON>?>UewGrEssN3UrLz4S&tNJnZQId zVBN=yd2GBOc%}q?cV5F5m;po&di>WjfNJn@9>h=T&*!r6X_n8$q>ww{)MvM6GqBR2 zgAeGhBiFgmK0tp{52WYs9sUGubo54#Snpb(y9vX|9j_%i(`ru;LwHe^6*;DDXyN{aOHV|OP*9)(yv!U>Ul4DO&y`v0~CzT8<`Bf-?A znXEA{2+N(WH2w@yDP+C|0|h#hp*Y>Jv9m`f;`ZHB;^3`K$5#1*fR*%l!wckJe^{O% zkqa%@rUOTaNN*Y+WN<`ieC{z|1+`3z1V}k4fs|xACY~5*1{>c7_ZEYt`aJ{q5uCDL zPGrdYnX(rk7|3$b)PrM(p%5bR6fTcZu1E>OP71^bhkNI~5cixB3*s15b3O4{rEevP zz>U>Ygh-+tHZB*rA80HRyqNz-VfCLR9lNB+TX|=2=j2*=8Y|*B98%LIqYr!nGy1qN ze;@++q$9??_6#t#5(rfSe^?$fZ%K_A@d)dM3joN-)tUY7n>PA@Ggs2^1fb(cY(tIe zCJ>B|FWv$gtWv`0u`H0gJ&~<_M=iq#KB0VYg|$3jOi62=0zZ?;L|cgs=6I#GUjO;d zZVJ$3bAQO51LFo~FrlnZ5spkyM|gefdPWB%k{8g2cSiv5JpSe*{;L!NHnpVah_);n zF)-e&YI=ci-<&y2hlJe*#M)gQM6DS0)Xg9ba0kilS)8no{#f+u|K6hgO)s`NI76#u z@2aLPjYDc4C0jIauN)m0lPOYGJM-=_7v@x9s%%D9AvUc*vl>8%gCZ?GeBkY zJ$??@Ti1v;NKTtN0TW6NKBSn+-E7GQ^GyzS_Tl?>2P+4-6VA6!jZ}5Oz2&v+q0iGl z*@ypueL6SWc`^?ilDJ z7FuePIzdGQnBeVCy%K;4NVxcc4vfiBWc7mir&2&)cKApBQ3~3p zC(?14HWDU6=OfVL^1e5ykeVb;u6}_Yz15Zo!kTzE3isBm_PF8ZSdktwiPI=uBj@Zc zmKZu`O>eXlUe^MDLGA6` zhNc&eo8t{Me(Z;y)pjT<mjN1|`|htOIni?{2n2t3mElRm2lx3_C_{JE8Gs3p#`;pLMxo2@Ac`l`*ffNp+` zYfd+>dG4`S!c>CCgya0lgV>Ekf&s%>j9s*zuNeKA0w_;+>x;z>PmpbR=26b!sQXmm zsM&O&=X91Cj&f+Aa}zfR1rPH{1EX*n3Vq7FFFx8i9wV6JyxR?E2QtSSIz+UbpkEy3 zjDFD(@psAD7<|7%kGCuAgK~=cRb-RtvqhW*gZF)9(k<)1P6sBBPM+@^33D523eMH7 z^{j_r36CtI`{~N)(;~}K{To-+rpvhnfv`@!mP>m!3jg&S zV2NRevyGa}fbqy?>U%;|*aBqszrrO!$M^7oXHVAT&eHdNAg{{%@L0qK-@m1#HM_aGh*a-4chFY+M+CtIkhaY@SuPVj9x6$whJU9rhL;q8HQ9! z-ea+UaVDlowWN4OWmFP+X)9p;B<+<*<#2_8d?|(RYaJJUzPGj;HFe*l;X?+s-2;cR zUI50y4~3)H9ZG6sGzFCMsw8K;{p!-m5Z`?%12i@k-ZO{3lCPwQL{G1qo56(? zY-sR>JD`^Whto;AB;!2DM^@pFs!Hpnp5Us=?$WEE?pnd4&C_SesEv!6=JKf(q z{7D}~tIy&~3LfAGTzU4)+j^t^ISZ!3$y6DO=I|jbRlV zvKiz0KR@CdIncSSl(9;e1&t@}5`{=~#Bi+j2j}VDal?Cdx$67!I404xdl;hcrFy~U z8uc?A6+{mJik0O3dXsR71Q!c;vu4hSt0>lA!yUkV?k+~=X~v+rGHAgbXh~;n%-z!B zoG~vQ=W7IHZk>*3t(TwM4Cijzpnxtcq)Ooej%Z<^MI@{bSgV`dM!I1jec52Q$Kj}% z&f4{XklAfw?XRHvE3prKF`hYV7zJb3T^0`tIFjM@;$Hb-52P=&+{p_&= zRKhsyXD-l+(2=()V1NL6{{>9c46dfE2H#Da+wGg+a1u%6Hf(f1&;t>L03A^W;{uk3A);lZF6r!mKDKqi!l#7($?x& zop^74PD-lu8oNt!?7Vw(W}E*V$cTpNrgvM;2QTd}y>;&)lFb;=Ipl6MjieBj{2lWhAb`!j9|ZPVn+8yO^_FmDvauB91r&bt$WMdMj!Sf5)uuKnQLFD)S*qxFG;JG6Y=rk0A`IOg!e&Y&XY5NUko)AN z+|3Owa`0V{E6&iUHK{;BZlPAIT!5*KE)OEvRFglg^aj5yRy6UR@k4^VrTRf}H z@eD(x8NX8`g@*m)<1v$-m$oROdD%(n2|)SA|9a1$>xM;k~#grIW(&tWvkom2;+ z+DUCFeHa?@!!ud|8)jo7L1td-C2(XRg%LHMcSfTxBFZ}1o}P+sBd>$@q$opXTE+GT z0N}}u4#k`S9wY8tk|RfsPVdPiX|r#E+!;e_(b62Qi&6SrrY^u)_*t&ThMY|`!Sm%= zP)LN9S>vO#ZHjH+wUgyKcr4_fnqJI)*4V(eU`BX)nWk!NvVpR)SBY&nREeQh={80IHr z6f#8MHzial(Z!Tnn3jDw?~x49)bB0@ZC=^O#+1aHUZkd3qJS|x_aJO_lt&us+;|}&U{l~ zD-eraYrZS8iznvLm&T^;yQ?o4hjkq)-&7{T>>`s?*U5-gFmo*0zkav zgPdQmG9$v;$Sy%SGR#ywV#30$itr*fyGNeX#5FG{_8N(704FdUWzd{ZkEV57V(x zHkoBOnW`R?qPGJzsnmdKs4}${gzo`Ez-v=r14MyyDM3niRAF2OEj8C8;>KKho72+I>+5hHR?XQ{ig@W5bm*d}#KK=)k z#Rm;4ayO}qPnMYUZ_{U>9e_d32&}!J*_#xTz`T1M2z_yyIR$`*(hhj6Uz4@FbiM&R zQ;x2J+J*M7({7fSqWE7E;RKUYVA_d7{|?W2-D>)+H-;5mOf|YCuc&ZbXvt~b(CQ~4 zgg!LgbiM`~ZW@vez1hub_Qft=Qh6OM(~{chggicmMGHNM-HtsaJxvW57I-4Es@e>& z#2Yrnbf6)`pE@-Ox|1SSRduBx(qh^=0+?=lakO_1;4@PB`+nf)MH{?hi3A+L=9iNi z45?!C)1TR=OPZ;6rzG!T)o9N6_U;wWx=0(Qz)%l(}`Cv$e8x-?#`)nv~n;lTK z!eyRh-xUDj(~K)PWtZ~P{y+{Dt-5~mjV5MVLLyk8S{~;H#d!A~Rs8_SvSb~QrLrXC zA-%%=LN4v>1qS;D-|wT_OYL?04ML1^BIBw7;1m4ck1lv}a$mJ29*_I9uqUOYso;kh9eM_wm!dlqy9uk(BXC{pG9;mk&2gLP6 z8?v3oakpjk=un{MC8&%?x$U+9 z-SYwLzwRmOZYX^Gw2*(E2DjRNLDBaBR;>eWVk2} z0R#H}deLLLQ0O2wfr~Aa@I@ri>32?lGUg?AMSQ8~GUAtoL}BefyJ8ltL+GY?GsbxeA%mTb4?PV{3J+t@==;Jyw2u{vS2$DP2#)vJtTH4>;U_{VM^gkc9;`O}hJL{r@V z?|R|jG(Y1DJHC$=ac92*aLHxoiH}f(vxMX9P8zucJ52ODGFRM(0JGakf0t&DD(hpB zBTo@i>gluKiBQR|!Ok&1pwu6{U28YV^9LRFKD4Q^8qO+Pf!=e6W$(s_GO035^i8A_ ze1Qt^OwLWTPb4Z(flUCSmq3H*Q4oZ+_2O%Gv`Z?!$HQuVGjR_gtkXYA$6&q4+r!}- z&$YM`d#hn8=Fth~}Al=X+fvW1ZClO%~z7}YmgPJDHnZJoZ_75pDxle9g_7aIfnh z)+IR3z=F`|jYW6oIe>;mAE$r;=0+->(H2T{v$)(F%{}gf+QKO#PzSaEQ~Utr;EM3& zThzR8%J6T*$(3oAAC{f7B@;*VTUHwN93dP!=X_^M`%XG3)Npt%j$5vqM9{)HViS;s zD2^NJDv|mYB zo0fV1^KEtQC|JO0pHV%KK*3`wOW}5T_Mc7KvY{t!aZ@)WA|R#FCjUlZL1(*I0g%4Z zfwIP;|8Ie>fIy(D)}`hKl8xhqI|6akNc!xQJr@Fm0M%%cN|eb^Gn6qrj#Y!f#7JqF z35wml6WU#kd>6Z5oCyvvnd9;=#_Dy-Jj~(W-!K4N@@6q9i%8J@n=lrMo?Eng<*M&O_ZLnOMB zH6*O+GX%SkUzHs541U^np2YM*IKRsqsnX-H2{~wY+Kt3^PLzh@TESY#p(-EU?b^U- zV#h34-PVQ>L&AA!+r5zlwx0Ugj$4LXGx7|6d;~`itDi_46$wVu*>pmGc~Nl__Y9u8 z&>zUV^KNMoI2XRMxF>i{u5c9-aKVxg!P=L$<*Za&IWnQGYw%~d51*M%KLz>s4Bx+# zMHK=%qHGIV*NUB9?u0ZxzashKUVJ;g9 zUxKx25aoPs&Z5#n!dVMsXWyS2FRuY%#9w|a+=I=eduCM>ifW3Yv`i}_jOQsXZnAT{ z^r_TP%5GJ8CnYBbc%+LPnBFmQGVO%d);Rwyd5i9k0I&34G~dr7P{(4{+N=n_5&vEU zHBzGgBg2@d{_B;Gp9v2Nvz-v_EXiBK)aL8 zcJSpK-0jR? zkeVGg^eJvw`DFWs?pEC#F}Fh-PEw)+<}Z(E{dDhk`$nYB@fP-Rtup11NAly3t2%`J zWf?#1j!zssUrS(8ynALJ)6y&=Rw!I+S&E#WpamQc--JJT_*`~4O87RrIH<#83ecFr z-<&(xg#Gabh2H6ebR4?*5D8cqmPHWPFUEmK?Eu(d8NeIFf zVl2t_9A}1P_7&pi7`VmyK@EQDEBX;-rx1Q|j-}`Ro$4#~Gtne$fhSnS{~Z z4Y}XG1yM?$25^VDtEkj?%jtk4)<@OA6F>#r7I2t>_IpGXXM@FxEgI40S~EpT|2r{RzXSaj^vGdg(D{f-KMRxgPCZpChslB|ZB_ryS# z!eg5ZUQwoA_RIjW6k58?hWB;$_kRM48!g>0w1B^+CszU)G8KEZM@S})g^va0O#UG| zu$(Euy5^bt*!{eFbRRbovKm0p>^Plv7Px{WYjk=M-j?45ZIyp%A-@y>N5>YbjZPi^ z6(u`QfKD=Kn&BTmR}!;Fcrl{;8&}eKa1=!C16n*6(l&56?&FqiL8=rFEKEZnZNaJ_ zy?jt`bnt6J=xjIlM5LJwtR^l$>al)xk>N0*{@x#s->j$6tOLiRoH_R9RL<&0s~01h zYH!_#nSP^{>c%3R?G8M)bg;gorA`^m8uzKDXQoK5QOY@&o4hs1Le80~oXXjkYH$V# z%Ye*=ZA$k-IO?m@U6E|VdDS6{t>4Fd_gs3b`y_aS4aXP6EjV5SPGblZa0UoUZY4ha z`l?(6<*G+3TNZXn$)EQ6_Bm6iDWhm%{yZTd8R?2#fiR2WF8&Ml9wNcV3iud5cAJR2 z6G?u&sBY9=gqRXHS1Q_;^ z*|(S+>Zv~WILTkl+!aUKr9|vrTkbx)_`(+VJp;?tIpZN)&AV633vaWQZV+ruAA8JK zT{){c>k1v_EfjQrE@38MCID-A!*0G?!X?=2b3BmM8sO<+0PPiswu+{5muzXyw9*^J?;sFaO z&5y_d#RWbAw#<}%t1(`AXzazWo_L3mug^`x(0fawx152MwD&xb|4?!(h0N#uW2avw z4LfnKn=was*pyYbc!v&WjLU{ zS0raXU>3byH%;9fA}sE)m~B|$iXhFkh`l6`4d zyofxzI|<^tuhmP-K!5eH7!wMjd;O z>c<)~7CD6o!6TD-uq8L#M9rc$DLeb%^zwL~Q(Et6v`|<1uCTf&AEc{IN>MbV{{X$4 z)_%5Ld&LSn8i-gbXF`eJmp~yu-X?wNm~7q52)O$AGs$DCRlH{Pr`k-=w*Kse{(0@H zC?tcsEXJSK-r897lT9QYjLq=(pa3m#xxPtJyBA8n!CL?iQM$oA_6QqU&gWBQgpJg7 zCp$|FLj9oLp2!G7P2nMxeW^rNAPhTgCj_ltTN)cp)M@>@7F0;OmdDjAp`#e2si!UF z9o@eyrk+Sb+j;|?w{$~tH66K2p^1H@x%1LrE#w~Xf1C=m3ONSCm~tKBxLNUfgN?|~ zZP2cpzvM>@XPf;RQn52ISAZP5^)YZYijtnXb~=M;8}`;4jtQ_kJuYtmrNfH z`|JoADfu;Lz_4NMgk63qkUh+|Ru|G9l&*=g9j_I;f(XpvxQXYi*UC+;DXqa~sVRYn*rVW7V zO^hT z&|-$+v;|OzjHlghx?FRK$_gznvka8~!d$FM6I$;Z^{borb3F+hx_f+oJ(R?Wmjm`{ zl)?<0qHt`&a$vo{qU^ik6g>ECWh8Y0`0=0Utivv+eb8o-pF znV0|<)hdoJtY*(JxUSsb#;@FmpQ>YxQ<2hTO*fY0%JgrrSsFTR*+$2M-cesy6b-9K zA-(cfa2M*IwkBeNP|?Eq9#KIk8~zs-0oX|M1kFqNxEq^(g^GPOyy+5deUQ?tGiwq& zgf_)uGBA|QZ(@)ZF*3+?xqGy8AA~c&4;YRH)#@dm7Gel97X2nCa%5@Tn5`;Z%*{ow6rXd>&1Gn0 z+}I51VQAdPb)|f0&24rHONXC)RgkguQ&I%vO)&o~2mO>_iZO#~Q5!5#(o( z31tPxwDBY6R@aB`m}J6;?=bz=*JtA@IeIAY=;&e4F9Huf58j|@fFpfX81vXE7%1p6 zaIiLRO!X)@?{D>TKkJ(Yrk}2ZRSc|L;r;0>1Tr+P%x`-d#gV{Ynwwm*4P27#*+%ae zc-Xd&ZUKcVf1m6jNX#px%f<}YMVCMJY>bXl)5Z64HAgQZ2&8mhA7i1I7~7ct{tVXz z_ZS)6NvVbjAeg0S@HF8I%O=fd*))Ny*@!85zWr1UkA!R)xfuH56s24*Layle;M1j} z?P=E4Jy1yWmQ$eA)_ZfN&&HU9R=F^Aaxr>2~WOBDR z7X2bHePnLjSS${>>s<6Qh4$xsTyr`MO!HI_lKg>-W020pYXjG(s zG>U-(CVJgxYU}ud$O=QS`k@o8&)sviAfe3Ezt#dRiy7qvpz5gwmRtC=_?eF&4C21E z6NPp^M@H_&aTqnbZ|a~Srt)~qF)eA>oD0uz&lX#0B_qWsO4K%CB?4i!{ z1q<(|JA(p;ONI}iG~Ms$p-!0%Kq8JVI+$HEHyoquP8Nfxm-@;++G}=lWAF_*DBo)q zQ2H8%g@jT&#V(MVln90Ft%C?%XV!`|{!usJY|03l23}I^KA=_3E9K)~7U)^J7h&2-R9)W=h1HnG zn^M@Ab1p!0{vvDL54k}h>qTVu07~0%2Qw7AzMSF|Zr?pOhPeF_$Bimk9!Gr&DYqnwBM1^<=i(;Fj4tQy%;4hLgw$FrU}8dOwMdLm^x zIZqj@bVOJbXrXfx6E9M29^LvrTEVk zJ8-3Ps`jdMzx5HN-RDmB*}W)HI3yY-!O0 z@3^!njn6^v@ZnBwE4{+d2~I}*>t?@m4>H7;ENb(K9syub-YM`SjV=w^%w9@7M4^`P z$5|qH@^jU<3{;R(b?3B6;6^OJX`w(e{7A>3hYtKuOC^?s5RU&GR;G%9EVW{H2Lsf9 zS;9$B?DYdFg15HtTwVs^|#4A$nC}0^6A=@7SJe(%;fa zGqn5F{hs)_55>7=%2dI`C{8kVXE_M8{f?^nZh_GD1G9;?1gjYP(hU?9L{}xm2yMKY z*vLiqms`M*JO}!G5O75sedW_OxS~Tde(io29AKw7RS=c6-M z_<^@r;T*0aA?<#qInl3yE0ZoX9k|jj(QMLz*VBu5A4rJKMJjswEgT#P{ab~0BYL|| zWAU?k_Hy|el8RXhQwbfJcb&=ARUPIUU!w<5>R1D_rB0~%E zXLWw{!?DHx@=NK5;bV~Aaq6NSaBxAT3P|>*zq0hwU?ZQ8Oyz=4V?RQ60Me{~cyYfV zRBpHNg*2y9K8c&eiIra;WazSYqL)S%dm@**wzxV+2gBBJ+Ju!E!;uHUo_7W6y-1F5 zF@?H4u$DsY&DW32gr9d^heX!7Ki_l`pg6@4eV)tZkj1n-#XO#wYJc$-MJ{%?w+;LkPa@^R&da{Tr{nCU#ngz$ngWQ9z|L;P zQ9=-QzWp6`PD@_xwGgw@iNOcF@>>d3t|lEbJ;fPgPT0LZNcdb`OyMKLE@+W8$9v{m z)INY}>ln-9frqG5E}reCZarf8Nj;2VXZPGrG?2$556k&Y>Bqj5{MHYqk@?M5S|l=G zeM8lA;0ulz0_RD#0Jd@gTf{sz$BHR3iEfYgyFN@c2_XNaQYPq!IaMA-!XWiy?jq--N53_@QJB@VTNn-UA2B_2fEP zWXs^Yqhy<*(s7g7y{eXubd~ivh~{C+&a+$f%ia%YeoE-_`>@T4nw2%cH2eZlx`rnb z@`w{F;H1BO)?JT;EW{hp(99nD;nnW~Fa9IfeK-y6KtzmYJy!92A7~i1K^ar59@1h% zj-Z$yM2MbeQ(%jiK0iy zr<(U7yz)&_fJNxOAlgsRyE{|vO_Jm0-(Bl(m&(ZA!k-FqSFdBI9I=f3p%3U9Qu*S- zArSRG5S43KBIJTNd7Z)SUp<8JaURIzJZkc;HuC&|AxYanQcD<;D-7{q^Pqo|mN(j2 zM)9$a#wY%rrPiJ7UaG{-uWWL4``rHZoVok$&5d_&%LkxoG(Moco5SaGH=MT$BJk+r z(#^7RU7@?V=VvF#7~FnM;j~PLF^bwK((VCgrh3Ptm+S|Q2ZM@(B<$urZyk~U)B@1A zJl@>0#YC1iu8sL{iFq=*?|+w$p2q&=jL`y-3xIkq9zBYF93Xa! zIQ9g&PJk;L`iLrdhL$X5nGXPGk`X%rCSB=llh! zPM`9jmLv*!pstT8bRL%@Hm#d0S!Q&j8?Vf>`nj~<_jh-81_bBb%)(hPP@txPf~-!q z!|=O6PRJR^qwal8b=kpj!u}pKa}&|?0^gtIb!!UTYeAx2sDg76^?3?$2sT?Re|NaF zrH9A-Huep{M`ANu!XH6cZE4lK-XRzSct$A1GkYq(25@a28PCk`=2MeCQ_A;g%B4=+ z{JK1?DQcOmJgBnuRKi$;YiCHi@{^{4W>sXlxki<=oa@wM_Y#)XPkW-H^cFK(8|JBR z{E^pvilZ+Xk+v2RODykf!#6bXy`l*tXB%8CeeyK3t@JQ#`-^Bp^x z5XL~_Shj$VAb-BFbvtKd=w!hTke+|zmN9L-L?>;(_b`*#VXChMnSb4JfhE}12}Gs0 zc%icTCkGm5WoJN-tlP7XlhzVL6?4T7=5yAflbumB=dGNzyN-=%VT&U^NG$dw z`gs~56iP}<1XzBgpS9{OQq)oe2AIs!HtWpjh-9a1WyLouGYmFAi0sz^&vmz2x5OG8ylia^-{+j{?1q65qOW7&Dwz&#sLuFJ))^A3qNCqIoUY>0 z&7+)=W!hVz`URWp%?|^_J}PtMm#<~)@o%=0WEACNW60~Zn|y~@ZiPsYWof!&eR11z z61(NCYSGtI_ing%Lb242DNCPL@5YX)v&V_EhQH-Wx-pw8i9w7rmB_MZqU-IhKnDKx z+NWaYlo2npNsPC4)5tEx4)T*VHk9>w!MVw9)XeGuqbzom-uzHp2M&n>f)%~GN0B7gq^h$J%GaZE(<=WU#<{e;$b0CMfmp=eBVE#E8643Jb z#VTn(tc74}5$4Gac)vyYRkiH|0y^k8NXw#xdVhX9YwGS%-~e7#jT|ZQ)RWMQt^p}N zWuSR~GtFfs&?-u9oZP7&6Oer>Tvt_V0DghyC^oGB%u@ziXYhEMFsEg!V81JOtEeaE zN?We#L9uuI#ZEZqXV4P0AITQtebByS3~nq*sythi`~!@x@6ELM&(F<1#p7ap>EeD< z$uzx2ff;QpR3GcqNPtKNo^>YJ*(lCoD)Yp6<~5uDa-%$aJX*24AiI8iZX&tDZSIYd zm4xE$hpu^PEve*Hca%P*9sf=$hY1E<8|DX|BoL_=l|e^qtu)hU+5321se(|cju(%- zwNs!N8k--6OT&#GZS~)nhCzAe{N1-bnyZynI8r3dB^^{QRsa0PlRMt`MPua2BQ)h<7HM7&kt=gE#*gdA{4{ zW@rY-Ms(f7K(+|*)$umlOi}@&-T3~Ha{lXd<>HTc&#>*$=|i#j#u6RbCK@Xzh{loR zk^a@#?wqwn22i*kmh_I2XIGc!=7zt8n$3iGTvwh6`kpwE9c|m`12G2uI+q++{{9>6 z?-B3hp?B@gKC?ijCaysLJ5gwwk7fZ#10+{?(MAeWW6W>U5aTb1NrwhHq^yQpwK!VN zJ9;}7>mT+i?-VazI}LqH`jR35#e`fBNH}cz+NX(4*wl1b4_!FLW`-~lyue6pUTgEZ zN&3a6o}LQXPmx@419Ci0p8265C})@hW+?LI!OZi;S0*!?mcL1t#f4ly!2&BF5?6g0=}Wb{8|{s7 z6|fcc_O?=syKgU1z3hVJC*}!j`YGKRa<=9w#%s0+%672qI~&7bOg;Vnez*Bt^R6Y&xDiWd>a?_oyI6ro>mX0^zE^}HLZ34deXNjm`hww~2G z2WwBHqwdsEk%KCVbS!ZQpebCRA0aK0lZ(wst-7FV-o z+uz>0Tido#mc|z^sS7S#v5|?JB_rv02xaRB{odF}lY6kK+;dR0+39$lf|NVZK4a6iR(%Dax} zA4>a~8aEoYdZ~S!=fH(XAk)Y{x1FGnUCVW!ENzV$eo~-{RO&24igeRAfvPX&V1jdQ zJw;;rhw~Rf#=7a(`^~JaDw(Wh@h+pFsB;B6uRYUzo^(Hc1JuifMD%wc4KmpL&fKbl z-6Iw3+@LHxLO7*1*~Qx+)WXBW^^b06#aegVYomE`jdBb8zt`nA5vZ&DvpPk$;ph$X zrn0Z=D&fxBa#4>fjeYG(rP&Wnh^$*rYW^2z?*Y{0+IEcsp@{+_ilK{$fQS?+p`#!u z2uKl>jzQ^7dat5j2ntA%st8DL(pwNo=!ojIrNr+Xl)Qa*Ib^NkaParboJ#JX~xHE1kVnv5gKsNrg~00L3vy%Q(Fv%CzR!*Y4|eOGzKBg0>Bcb$E~}`u{P`| z_I=w#=JBb<%2cGZBz%2%p=)bVV6)~`5;7_KeGyqaEMeX@WJR}421$F~BgfRR)RuTg zC;TqaPkr@|D`PJMv%MVR^e@Ec%KL3E^$Yi32rw*t$!NlNcXpe&C{@%Z7UQ&XW`%y> z>22zG@-`dr+dYqUHnYLC`xe(w2ux0wx%%{B_)j9k#q2OVtE<*36Aa&PU9<&Wzl{`< zMNl>tMF3Dx+`!-AaRc8@m=zH;)@eEg{#E8K%De`gdq7NV`iN4^)ooHAKm?-M7uR32zC}vB0u4A2?`?Vd3X+3 z5X_?n0)sGCbvbiH1Ejg!MjLSKjz%PN+|K^{OQ|kj@8bsFjTt3ibo5!O)-I7*n-ni| z*XUr(&uL4TQ0rq=+9YbN9RsFMVh@;j3W^fv9YQReQj+OXvH$|i``vq|SL;_5 zx7Lg2Y;cQP7DPwD$BOG+;=HTMgZbxK^tHJk@`^Y;G{kB|djqV}YQaH_8`Ka@g<_NU z{vi`I6Z=|5j#aQKLCTS1rIx;ohypye-*oYq8Z}4(Z;*J&yKUknTJ2zI zT4B?0v%ffYH4dpz;(UIiK1xaV?C1G3^#+xLwowp5Q*^%856a5UrgQ= z(gE$Rjca)&FUGgrw1m0 ze)FM*n?yj$;(L$CC;zW!Es)?-7ou+ig&L_H5NguZbBw_M^r>Sf*snPb`>#9*R25Bx zUA&8ZDE>ubv$l16>x`ESIjs|tTkXQ1vqxMl?QZMOBK;4%z9`;C6?_J0e*Ax2>E zDvN>ksbKc3Qia%I94GwRMdO5MW-8Li(3!m?WtpBTo&F*dHhM!@gj-hw55eB zd#HtqnN~-|QeZ3gr-A`d6Wf_ArI#s3GN5gAgVuvSubr^LIAs{@#V5fqiW70lU)Rui$}6Gn2Ok3MhSRMpR$&r?w1f!GbG>e1@+I$UnGuly`~zWCbHJ z>50exo@RpCWgsC>?0Y@??A0D&~4uVF`Q zK|EHdz=Yc1Bl%14My2)m%QuvuHfB$60)$Rh@V2uIR);}Fb9Dm15Yk}6^v?$#075G^ zM^%Va&ViU!kwUG08=EC_RAf-;_{rRA`8evC=i)D}1iwYV?^^qyXG~`{4&47Zz8_6s zoK%YSezgTUO?j}v_4e!pYcE8M%4P-zfvXR6M(4oW0+Y8tlASvc#PP4Y#a#zR$%Wbw z=)W3Bhy`8If1q?5xDq& zez=|6yLaJDgq=5-RMZf|$nQTc>3YbBjcFWe*?MqZ@;iK}dcHeVK-~nzn%nzM(>0>a z@*$(rl*D(7fhA?A-JjPVet#Vr_3o)a2LSg!kmLZD?9lY+tH7qrK=-l|)}vGa4{)M6 zYG+|=!>U}1k$@n<{wqcZflWc^IU4tMS<{F@GxuOf7{c_ukNPjsa-J@coSq%pN59 z02SUCpiPW_)l4^BpQz(1WmOIgm!xxiGNoghLa0S;gwwRS2~&>|0j3e#`c+V278?Aw z0f`v|N#|;Kf!GD5?0>i>`nR#s0Zt|=$2zLJpuY1v%pWQp`UcFm$|yhdT#QAolavSc zmRfW(0Qd@JV{aepO0@ct`VX4u2Q3R?D!v*;Wd{tYf~5dv4y(QWm=W+I@>{@ynmKGF z`#PnFg`aoG_W$ui8#VPA{(X6zjuwk63nIj(I5qASg7=#^aa;g^q3^5`en*v{j!B^0 z<=gbvR`&%sS`>d5%j}15(t;;I7$j+W_-DH5`GhkIB_*0_P8eq<=(h6Yb?M#G{)A-~ z+^fI&#TnvbNyt1d0em%;Z@PXU6VGmIuV(!_rNj-WWu$_ntK&R4(T(w-4kFNkrsS-E zmuKVnv}$&w0|&BpdgO9xIKSUUucz;7<6gFXj8NA%QxM1KEjw*U7cDY1;8l@HV{AbZK#yeZ40z^{6R)< zO|@?`j6Tivg_8fVjk$9CBXIoie1I+TpDyHT5aIdT%QYEnKwLosXCM3puZuY-l<}@A zOgyYAJfwDXpL9?4270D@TYjy#ymwX>c{VdMhCNuX2J0u2DWh4OKzz!i^j&6|ib>$C zPCmD$!ep8x?A-EE)MgB@>Bm4GGIY5s$e>PI(kCH4CC)fw9>s|`FY>FNZ+7j5gf57? zKa>B4qyGO-;mAv{iv{m4vgPQ$m-DZX}nGOR+13p zY6~ofDg1JMy;Cc;*Beg#TGD;ZAmRcVGH}lM~t@_EO3RqGDqiv z7tTCU=J`;tQ&WJn?R-M4Xae5GuL4{C1QMdQ#NZo@hG^ru&~>y*Y5mRYMwjTaJ+n`&ba0 zYlm4Kv;D&pAFVJVk06-#ziM3cAprfpM!&NGIi~N$D4-=( z^eew{2gRkOPy-;KltEo#Bn|eM1P9d)7!}6Y92v9592AedNfcAjr zXLYHm;qLSOgXO}SOMJSOKxIpHp4;>g;wlr_2UTuj5Lf)#WBIk*lJk5*@|>u*Xf%)c zPZRBJ6rjR{M@?UD{4R8v1a=U`$sYS~-UdcGMU>;0#Xm8~`hE zxLbdN$#tn>PjuaQU*F7H?WVf8_m1r@5U$?OtPEOK!G7nsGk_r3k_8C~isz)nj_Rlf zr!i57;E{8Mp8_#D$x=d1#08I-9i(4*Yc#j&++GZ?HfS1N3IXng3iKv7vRiYm0J?_R_5)B5L)fXv-;}7Xk}badOO@d{FC%{Z911!33@JQWL1IVpEurge z+jAX$;D??ZqrZL=jze=TC;1tBuZmWHnp|J|Nk6SRZ8sHjK0H4df$VpQR$;(Kv{tg zdo`HH5t)}%93FPOG}1Dz1FZpKvxuW3F7zEgjSu-<`ePav+)tco;dLp4d0LCGCKBQf zXIV;TR4+Vu8zJCmc|JnJTu$?ppP&C~jJ7{;pS_nOk*VVH;xL|kN$v0R6Z+pizfsd8 z^*Q)%<8Xx-!wP4e{iis&ZEHvFYDPFkXs=7FO+aOrxH|6^V2Smvj#ZWRX7mOAj#0As zC{WUD;yHUM+b{E7qVSTu{k2gMq%XKXYZ-CIFTn|QcAo2?OOSBRWCp2T$Fhl`Tz3>) zY~#^E9Qs!A-v9}^?f*^5i{N?&rd;o3Y+Qu={xa$EvFa7hFI?LIoE0mk>VH*jw zXkl0XgWl9H9a41r0}laE@eBHH`r`e503Fe%b{m`gOYAfR#LjcG7&Uo`*`MqlQ^W}-N%AjAldoW0f@%ky#Lb4wEy4yhfv<^HE9zoS+hQC@9dBf*Tlt=cRP zImQXb!1PuP7Ts(KyDWlThIq=1zSDXC6WsBx|Ec@Whi=DyBftI+3KnYYx|ZO?#fq4N zr-MzbOFauTqPtGVq37uCC+ZEj}3oPr7Y_5VHbQGR2>2 z3@D`3?Mz+sLtp^VJSXe5^;IVnDhyaQqXE0}pj_VJ_<7Da=p{$$#N5mW??6$3Z~Q+O ze8&LlS`lv+<@MOi2%m{ILn`!%fU5r$c_97 zI1#T07Wz-lp-lSkebxInAhS%260e?CtCKXY4Y2WB=xJwN?Ej74SzNxZk;-xB7l<^= zf+T6~3qL>IWBkn_xxU;?bCwvZN#&SiYL}BwXW4bpWat+uWg0!WTtmec-6^h6XR)H} zNPH#fp_BQ_P;tShPj_a#NYmUaTgpG@LHN+)NMDb1D32cT*@Hxh{!7-}u{AXmrE>s$ z`xMae1NJ8PptihmZ=+eD8kuV69HRxDY+_V}hv0+&4g3@zDL0m_ou&9jhe8EDHnH`xG5%vyt446?$JwDxGWNTE+GfCWD5$-*wrPjO8NCy%&u0Z zM@n@YZWC1?H2#;!Ah8QnH=4>Z;^f%$uVreW!2v9^1;9eP)1NS!@V5wMIyXDnnv(su;HZt7%blf6yL}3bz;;rmf-!MwMg=XA#!_yJpj#O=%&E| zr&O_91xUg_T-Cn~y&Y&=1Zo#z_av~~fJ_@epnKoA7g@(NrU#~K&>w%<(^`XXmhX{X z`HxmXq3?F1;b`M+?e#wriZC8n90fJgwaw4ki|1N{w>cUS9|jhT4{-Tlrb`X#g%H2z>s} zDGzY3O!iTPtMGM3W|Vj(y{x{R2>f)lVve3?IwECdN-?KqRx9cQww_xtZ!&~lv2$zIEO7g z0}#hO>i|By$CGseP>t4N7&al%?&N6SZl~AJ&WcHp+7JS3a;qti+b|xM5t4vK^gq{N zpoUn(vf1ASB@U+r=nwqzvWc*#OvEUE!0SOMxZoJ+E_$z z(f}5klJwI-up^<)JaqD9JHOVA$UGvzl!wp9;Ce&fqo+(;Zf!dg)ksRj%c*hfgLZoL z-Kc$yn)s!!zH=RnucufZ@z6*s!xOOw&huTZKDLUJFl-oh-mrxdj`$guxNFLi7FksH z7b*A#MVbM%G6)RN4QAaHhng&oL4b8;cb5AyLP7C76cqo_SwT4BHY46$e8+{}OMDMS z2=a~bzm#jD3az?~+|PL~gxLd1m>+1UE5wTib;3UjtquZvBgGd0B;Voh|5;L=ho&b% zAsc>8_@DY#_#J!UB!r9@g@J%d+-^jKZskRqj2i(=i|?UXB(63R50#$1d@H8Ir!6O! zHHl(#d&ur7GgKTg4>qDdD?#V^GoUc=JoehV1?qO3kB@ zH?b5yIS~wSS{zvsGe=MgR;Y--XII{xKZ7QO;tl6VR{00jJGeaj@GlWlCLT+9%pTv{ z@q^QfzRsHb)I{$?dG}q+T;)>LLh|eR!b(14!^TLsyR_H*^sM`}lSh(40k7l=D%*|q zk24N5zi}i2NUW5t`>`)U&MPKgb^zJ6srB}gWEDcrcze$G%^w3u6eJJDJPJAHDe z0mWX*3)k>$U~p-x%J{{zt9~bLLF6%#rwZ+tVEaVwe1rRfSpVP*74f%V@*2M2*Ofxi z*U!BBVu;w$$S;eO^O)3BF>2f!kI{-u5;{qz?UH4pL%~8^Z`?HIB*LGkuDMt#?R(&& zd`ToItC)B5igTH1(u%{4;-2nMYUZ(#G8-}vq-rjTO-A1siNCs{(CSK>$dVYF-O1*= zrMWwVj0{sd+oCKYFCAi>7l{BbpG~=Nz$O_QyB(dyDNK`{hIkN4A~E4|hEAmC?y(S| zSZgj}tABSL614tK$|CNdN?}>*ZEi9wv`OsvXHU5HMlSZZR)nsf_fpS(j^$5sAUuky z{VC+M^~3(?NX@F<5|w414Ftf<*rX^vWoXt?QA5p z)V)6E=3A@q+l&?&7L`fr#$WDSKU3Kybn-z!Mz%4SP(y@&4-lWR-e5vzJ&mC3_Vnv} z%4ZpFe$~GRFg_<}W(gpN6!>gM82NqOqK8!#vc3oRzj?8lUJo~#x7Q9(50kXp!oexe zAA%L{A5boBNt_72Zv;kDwol)r(hiUIH>0v?E>c7}pQB7Ire-9Qe@P^r>O9s`>4d)~ zoVq`%K#X`R8jzeD<6>J{Keg8Y}fHO*vFH< zqd;dd^+s+|bbmV1)DXGeWZHhbwu&1%|H?uGTYZHazHDzbDlmVoJV&lU@y3UF^`2}w z6M4gMEYHGagHVgoi}|bZVdNh+Zw}e=PQPf3CG1$0)O`Je>J+`FC0ds${Nl42V`!>D zfyIjU>_M0slXqGNS@h=SX-2JyYsVK%<+kL;VbdNj6aEi#;vbDnb^@?#ZTi(799pR< zN`n>m6s-St38evi{&_KbwC@`c#JxIv3LX(eC>@vvr+W0TzjjzBBp$An%|uR3L(wHMK3py-c?2j2 zcq6NGH=w_1;CTH~5}T7!%le0PzQIcm`j6g`V>$V1j2riyz-m4LATKalXMst586m=( zcnY38HWfq@jz???s~+d-K5&txJAf|(wVi86F!>%ig42_lyJ@Hxc~5Zf(*5OfMT|c% zE3=2j$R(_g&`nbs{s+;A75<0f?x)AQ%b6sdmr%>n&MSpn3*Tz1CFzIga7v*4G+%R8 zY0yqdE?H-ha*p3j_}3^>%>D^}!UOaigT8<$Oba%iQQ(P|_!$xSb&2nIzGNlFCn0mN_q|TP zuxZ;)DQ4Z6ANDE;3%%(2a?&v+8?YKTuJ0aV8rR%YMP*B9Fnil=a5<*z7|HWTppqbI za`oJQ36B&MOOL&pl_#0TV&;3O%Ma%^OHKoau5)R0alz)R3iia^ zp?0XNOH8V29%if2Z7&|M9 zI-QA_0Py%>9^rCw5aG-jm24O`Qgr(a4L0nd2^hq{e}5P=k8$f=dD%H|y~K#wl$i(p zh$%A9kqnO^+=O?>YGoy{-1T&4xkPjb6(Ddk4F;*7iC?{wVaEUpa6#&~_fL}+zW^nf zi#oU&QdwEy<-a+CAlyMM2-k zzh3Ph_Gfcu{pjM-xPL3dM{-L!RP*M0!kIUZh*6i@%DHPl=bIkU3O$l?6(X@!ygu|; zkS(oA-L27AhX{Y-9sQ`4j6!}oNy_u<9WnYxE9*wTpG1j*bAI{39{uzuJF|yrFTSf9 zg#1o&XbUaQdvtrJQaDzw?&5jlZZf;2O5XMK-<5txF?uo1H%W=Vu#`Ef0nXvp*7HuL zW0nCho$k&dXf1hy8W(VNy+M~ZhGSi324ieUh0LAmbJh9dqI*7{YDtMh&1(7QN7YFk zc6N&0w;fnW&O`gBznqioS%=iJ0k-3@-8^CFBuQ5l(kBzL-bCCKny30S8uP~KD^iRr z<~5_zwmFYMt6Ts%Hw+GFHXF1kgiI#NErq3=WdzMlv))a2C>XOXS?_B zNUkVyTWcGt?72i9zHxrOqNA~kiuQRZ_Z5&0$3l#TLY7;6ImH}5Cn{rYS;c$aeWt7D z-r&C7Q}HFGaqow0#9BG_D96~SvaZekK!Fb~cYznlB3DF35#?*NgOMa~h#_@{UhR73 z>kF!%@yMixp_W;|SV^8>3ls!5YyoT7#J8UqTSmpqhzAyIoxm~Kz2Djf(Ah?++1x}1 zpTZI+OqO-|$xsa|DXilfD!3{&%juhup3c0S4f6LgV>+Mm?1Jcx za?K0tkum1j)KM;f^v7(rL!H3*`C|{j9KBEeC`lSQmih_dbTlNrm1RwIg=|AQB*Qwu zN@fxiWHh10DmhL)IjF^kQuCusYahGHI7g3bwckkI?k7*xs|*WXkuX_X98%i~_e0Y5 z75KA?H}Wzl&!zB>sq*@R28p;UZu#SmPgNup{_=Kl^i}ZzoAf~Bn!%f z?}j!2wv85*zfSC*-k)&ihg`;=Hz#`XT^CHzGo7qiICF?&$aT3aoJcjq`<1uiI=zg- z8g0m-j_R6ogSF`^dtB5Z${ST*Y0;f5#l6eEdu2B>c5Bd<{$SQG%Pa6!RVrWAE`lQ2 z~xq>R!Dh37BdaX zo7H(Dn@J(VjnvUT3E9|)H@PNlLa1r;Wie?=N{zX^+a9Cln)pY+FxQ!aQzlvAO!7-~ zl0bWm{+x9G>u#qnmg1tGL-={un(QvjUf6&HHX53n`SnS0-3=yO92~?7Qit>rVLt93 ze#;?Jy>XT(hAzo)#PKQz;VZ5( z0E-3OlobQ`kdE(xcMBqi>ygZ-+QdS(tOj$yr83SFD+essb{Enk!{70v zsRO8kSIzRvp)qE8VI-^3GXP6#q&dRyJuzVT_19(DF+|E$#{l{Z7qnnkzaE032LHLf6DmB0 z;4GdnAkcR*<_K^|dS)ImHS^5gTUY|p-+pFf{Yd>87ga$Tep?JFe3^ssy0UZ(#S#8% zB5*GD{vF0WQ|Xk+@xlDdd_*!}IsxhmrtsB^Jz1`{2yp^)Iz*BJ6F#%;;d)>|vkI0v z$}~`9ubMqH1+$+54jCsPA%5MTy`vimco1g=Dj%rHj4PUyt?Xo00_h0=cV9EtTn40Q zT|RXyV2wCQRi5x-#pLuLPC6B$j?kE)9rL3?sndq>RIQ$wgE<;83jV8QgFj+T!-axg z7GDmSf%b3;;aTARQy5}8Tc(vnXkJZrmf_79s1tP+B`>8lvla z2T&01N;m)!d519`BK-_fAWrGr`*wM-i4M~WMhk8a-d3v-pKjlz>SAq&>-qk8GT@9w znEE_ReKA<0eB$%`)7Cv+*U#JF$Nl=ctzF2J_JiXJBS%w^t&Onsw~JTNymlCS;X*5n zZHVWgOTBE;(*XPOPXYlV$2UBg4V)z?<3>O!M2>%6DoF;VO2kU6ZiM>m8JM8k#Iwt$oR-_%Bu2&Ba6R00qBz+XogcY`&Ax|R z4ajn-DI0;{<&k>XD&^OA)I+Gg(+1qyziKTZG5%g4Uy|ZY$e`zl3%cBPi;CvVrR4Mo z9zT93T$~9j)mE-E>90abevu0eQ37(fLT3fRdZoQhg)h!!)07+|+}>ke1^>hr0uzAt z8$yF_oPzQFZ1wU=3S0~L9X+*sT+T{vXst}7+*Zbq07wGORZaSa+|$EE5+-NASRuCE z?=M-a;;7JpmB{Db6)k*8C*{l9H_tYxew&*hjOs;S>-*ZM?*7Vp%ryQoa32K!@Z(mtJ!uj>V1SM zQWiud^gc=J3Fi8bx#Ou|+Ap@BN6?6(mLZQJiqDU37{Myf>|F09ZRuhOiP1M72RzDk z9?!4k*8`*rvy)LWwMZOI1+Z?AU|(^teZpeJA}}w3Q^}ssF8b~I->n(?+5a3_IRmv1 zHj34hJ>2NMU^ob9t?ilw3Pa_fvVDG)e)xm+)IR9iFX+rl8+408x4xd6>`=V(>oygZ z`%r5s`pCt&M>X495SYNbli&laa}=_PS`%`zOCw$`01Zb>$wInvq}%;iz})6fq87V1 zhbVtr!n<;Dg*X85ICDztVcZunQp0m=*#F`bUW&XJoYVsMBjut0^m1)XLgqu*Agi6yf-msk7ZlXSb*;Q$(zy zDCKS?N1�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 |K4W2 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

LAG!Z8vBTbUc={&?tORVm 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%0DiZUj*f>OlY#ZSV5; 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�bBL>yw(ALNhMwEdm<~p3d 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

?5#(K?g6iD2o`iew%``i6I6g10An_=?+26w48oM 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


6rXWpUmB4Rw(jbT_s=Zx58j+*1Srd} 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%0D`hbfNbc%U0 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

T@!GIU;_ui0NBn<%s-

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 literal 0 HcmV?d00001 diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/parallel.rst b/doc/OnlineDocs/user_guide/contributed_packages/parmest/parallel.rst new file mode 100644 index 00000000000..3b60c5777de --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/parmest/parallel.rst @@ -0,0 +1,52 @@ +.. _parallelsection: + +Parallel Implementation +======================= + +Parallel implementation in parmest is **preliminary**. To run parmest +in parallel, you need the mpi4py Python package and a *compatible* MPI +installation. If you do NOT have mpi4py or a MPI installation, parmest +still works (you should not get MPI import errors). + +For example, the following command can be used to run the semibatch +model in parallel:: + + mpiexec -n 4 python parallel_example.py + +The file **parallel_example.py** is shown below. +Results are saved to file for later analysis. + +.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/semibatch/parallel_example.py + :language: python + +Installation +------------ + +The mpi4py Python package should be installed using conda. The +following installation instructions were tested on a Mac with Python +3.5. + +Create a conda environment and install mpi4py using the following +commands:: + + conda create -n parmest-parallel python=3.5 + source activate parmest-parallel + conda install -c conda-forge mpi4py + +This should install libgfortran, mpi, mpi4py, and openmpi. + +To verify proper installation, create a Python file with the following:: + + from mpi4py import MPI + import time + comm = MPI.COMM_WORLD + rank = comm.Get_rank() + print('Rank = ',rank) + time.sleep(10) + +Save the file as test_mpi.py and run the following command:: + + time mpiexec -n 4 python test_mpi.py + time python test_mpi.py + +The first one should be faster and should start 4 instances of Python. diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/scencreate.rst b/doc/OnlineDocs/user_guide/contributed_packages/parmest/scencreate.rst new file mode 100644 index 00000000000..b63ac5893c2 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/parmest/scencreate.rst @@ -0,0 +1,22 @@ +Scenario Creation +================= + +In addition to model-based parameter estimation, parmest can create +scenarios for use in optimization under uncertainty. To do this, one +first creates an ``Estimator`` object, then a ``ScenarioCreator`` +object, which has methods to add ``ParmestScen`` scenario objects to a +``ScenarioSet`` object, which can write them to a csv file or output them +via an iterator method. + +This example is in the semibatch subdirectory of the examples directory in +the file ``scenario_example.py``. It creates a csv file with scenarios that +correspond one-to-one with the experiments used as input data. It also +creates a few scenarios using the bootstrap methods and outputs prints the +scenarios to the screen, accessing them via the ``ScensItator`` a ``print`` + +.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/semibatch/scenario_example.py + :language: python + +.. note:: + This example may produce an error message if your version of Ipopt is not based + on a good linear solver. diff --git a/doc/OnlineDocs/user_guide/contributed_packages/preprocessing.rst b/doc/OnlineDocs/user_guide/contributed_packages/preprocessing.rst new file mode 100644 index 00000000000..fd26f2bf6db --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/preprocessing.rst @@ -0,0 +1,151 @@ +Nonlinear Preprocessing Transformations +======================================= + +``pyomo.contrib.preprocessing`` is a contributed library of preprocessing +transformations intended to operate upon nonlinear and mixed-integer nonlinear +programs (NLPs and MINLPs), as well as generalized disjunctive programs (GDPs). + +This contributed package is maintained by `Qi Chen +`_ and `his colleagues from Carnegie Mellon +University `_. + +The following preprocessing transformations are available. However, some may +later be deprecated or combined, depending on their usefulness. + +.. currentmodule:: pyomo.contrib.preprocessing.plugins + +.. autosummary:: + :nosignatures: + + var_aggregator.VariableAggregator + bounds_to_vars.ConstraintToVarBoundTransform + induced_linearity.InducedLinearity + constraint_tightener.TightenConstraintFromVars + deactivate_trivial_constraints.TrivialConstraintDeactivator + detect_fixed_vars.FixedVarDetector + equality_propagate.FixedVarPropagator + equality_propagate.VarBoundPropagator + init_vars.InitMidpoint + init_vars.InitZero + remove_zero_terms.RemoveZeroTerms + strip_bounds.VariableBoundStripper + zero_sum_propagator.ZeroSumPropagator + + +Variable Aggregator +------------------- + +The following code snippet demonstrates usage of the variable aggregation +transformation on a concrete Pyomo model: + +.. doctest:: + + >>> from pyomo.environ import * + >>> m = ConcreteModel() + >>> m.v1 = Var(initialize=1, bounds=(1, 8)) + >>> m.v2 = Var(initialize=2, bounds=(0, 3)) + >>> m.v3 = Var(initialize=3, bounds=(-7, 4)) + >>> m.v4 = Var(initialize=4, bounds=(2, 6)) + >>> m.c1 = Constraint(expr=m.v1 == m.v2) + >>> m.c2 = Constraint(expr=m.v2 == m.v3) + >>> m.c3 = Constraint(expr=m.v3 == m.v4) + >>> TransformationFactory('contrib.aggregate_vars').apply_to(m) + +To see the results of the transformation, you could then use the command + +.. code:: + + >>> m.pprint() + +.. autoclass:: pyomo.contrib.preprocessing.plugins.var_aggregator.VariableAggregator + :members: apply_to, create_using, update_variables + + +Explicit Constraints to Variable Bounds +--------------------------------------- + +.. doctest:: + + >>> from pyomo.environ import * + >>> m = ConcreteModel() + >>> m.v1 = Var(initialize=1) + >>> m.v2 = Var(initialize=2) + >>> m.v3 = Var(initialize=3) + >>> m.c1 = Constraint(expr=m.v1 == 2) + >>> m.c2 = Constraint(expr=m.v2 >= -2) + >>> m.c3 = Constraint(expr=m.v3 <= 5) + >>> TransformationFactory('contrib.constraints_to_var_bounds').apply_to(m) + +.. autoclass:: pyomo.contrib.preprocessing.plugins.bounds_to_vars.ConstraintToVarBoundTransform + :members: apply_to, create_using + + +Induced Linearity Reformulation +------------------------------- + +.. autoclass:: pyomo.contrib.preprocessing.plugins.induced_linearity.InducedLinearity + :members: apply_to, create_using + + +Constraint Bounds Tightener +--------------------------- + +This transformation was developed by `Sunjeev Kale +`_ at Carnegie Mellon University. + +.. autoclass:: pyomo.contrib.preprocessing.plugins.constraint_tightener.TightenConstraintFromVars + :members: apply_to, create_using + +Trivial Constraint Deactivation +------------------------------- + +.. autoclass:: pyomo.contrib.preprocessing.plugins.deactivate_trivial_constraints.TrivialConstraintDeactivator + :members: apply_to, create_using, revert + +Fixed Variable Detection +------------------------ + +.. autoclass:: pyomo.contrib.preprocessing.plugins.detect_fixed_vars.FixedVarDetector + :members: apply_to, create_using, revert + +Fixed Variable Equality Propagator +---------------------------------- + +.. autoclass:: pyomo.contrib.preprocessing.plugins.equality_propagate.FixedVarPropagator + :members: apply_to, create_using, revert + +Variable Bound Equality Propagator +---------------------------------- + +.. autoclass:: pyomo.contrib.preprocessing.plugins.equality_propagate.VarBoundPropagator + :members: apply_to, create_using, revert + +Variable Midpoint Initializer +----------------------------- + +.. autoclass:: pyomo.contrib.preprocessing.plugins.init_vars.InitMidpoint + :members: apply_to, create_using + +Variable Zero Initializer +------------------------- + +.. autoclass:: pyomo.contrib.preprocessing.plugins.init_vars.InitZero + :members: apply_to, create_using + +Zero Term Remover +----------------- + +.. autoclass:: pyomo.contrib.preprocessing.plugins.remove_zero_terms.RemoveZeroTerms + :members: apply_to, create_using + +Variable Bound Remover +---------------------- + +.. autoclass:: pyomo.contrib.preprocessing.plugins.strip_bounds.VariableBoundStripper + :members: apply_to, create_using, revert + +Zero Sum Propagator +------------------- + +.. autoclass:: pyomo.contrib.preprocessing.plugins.zero_sum_propagator.ZeroSumPropagator + :members: apply_to, create_using diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/api.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/api.rst new file mode 100644 index 00000000000..3d1ac8a189e --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/api.rst @@ -0,0 +1,14 @@ +.. _pynumero_api: + +PyNumero API +============ + +.. automodule:: pyomo.contrib.pynumero + :members: + :undoc-members: + +.. toctree:: + + pynumero.sparse + pynumero.interfaces + pynumero.linalg diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/backward_compatibility.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/backward_compatibility.rst new file mode 100644 index 00000000000..036a00bee62 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/backward_compatibility.rst @@ -0,0 +1,14 @@ +Backward Compatibility +====================== + +While PyNumero is a third-party contribution to Pyomo, we intend to maintain +the stability of its core functionality. The core functionality of PyNumero +consists of: + +1. The ``NLP`` API and ``PyomoNLP`` implementation of this API +2. HSL and MUMPS linear solver interfaces +3. ``BlockVector`` and ``BlockMatrix`` classes +4. CyIpopt and SciPy solver interfaces + +Other parts of PyNumero, such as ``ExternalGreyBoxBlock`` and +``ImplicitFunctionSolver``, are experimental and subject to change without notice. diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/index.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/index.rst new file mode 100644 index 00000000000..711bb83eb3b --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/index.rst @@ -0,0 +1,51 @@ +.. _pynumero: + +PyNumero +======== + +PyNumero is a package for developing parallel algorithms for nonlinear +programs (NLPs). This documentation provides a brief introduction to +PyNumero. For more details, see the API documentation (:ref:`pynumero_api`). + +.. toctree:: + :maxdepth: 2 + + installation.rst + tutorial.rst + api.rst + backward_compatibility.rst + + +Developers +---------- + +The development team includes: + +* Jose Santiago Rodriguez +* Michael Bynum +* Carl Laird +* Bethany Nicholson +* Robby Parker +* John Siirola + + +Packages built on PyNumero +-------------------------- + * https://github.com/Pyomo/pyomo/tree/main/pyomo/contrib/interior_point + * https://github.com/parapint/parapint + + +Papers utilizing PyNumero +------------------------- + + * Rodriguez, J. S., Laird, C. D., & Zavala, V. M. (2020). Scalable + preconditioning of block-structured linear algebra systems using + ADMM. Computers & Chemical Engineering, 133, 106478. + + +Indices and Tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/installation.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/installation.rst new file mode 100644 index 00000000000..9ac6961d2de --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/installation.rst @@ -0,0 +1,47 @@ +PyNumero Installation +===================== + +PyNumero is a module within Pyomo. Therefore, Pyomo must be installed +to use PyNumero. PyNumero also has some extensions that need +built. There are many ways to build the PyNumero extensions. Common +use cases are listed below. However, more information can always be +found at +https://github.com/Pyomo/pyomo/blob/main/pyomo/contrib/pynumero/build.py +and +https://github.com/Pyomo/pyomo/blob/main/pyomo/contrib/pynumero/src/CMakeLists.txt. + +Note that you will need a C++ compiler and CMake installed to build the +PyNumero libraries. + +Method 1 +-------- + +One way to build PyNumero extensions is with the pyomo +`download-extensions` and `build-extensions` subcommands. Note that +this approach will build PyNumero without support for the HSL linear +solvers. :: + + pyomo download-extensions + pyomo build-extensions + +Method 2 +-------- + +If you want PyNumero support for the HSL solvers and you have an IPOPT compilation +for your machine, you can build PyNumero using the build script :: + + python -m pyomo.contrib.pynumero.build -DBUILD_ASL=ON -DBUILD_MA27=ON -DIPOPT_DIR= + +Method 3 +-------- + +You can build the PyNumero libraries from source using `cmake`. This +generally works best when building from a source distribution of Pyomo. +Assuming that you are starting in the root of the Pyomo source +distribution, you can follow the normal CMake build process :: + + mkdir build + cd build + ccmake ../pyomo/contrib/pynumero/src + make + make install diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst new file mode 100644 index 00000000000..37dd5852351 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst @@ -0,0 +1,8 @@ +AMPL NLP Interface +================== + +.. autoclass:: pyomo.contrib.pynumero.interfaces.ampl_nlp.AmplNLP + :members: + :undoc-members: + :inherited-members: + :show-inheritance: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst new file mode 100644 index 00000000000..2537bd52fdb --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst @@ -0,0 +1,8 @@ +ASL NLP Interface +================= + +.. autoclass:: pyomo.contrib.pynumero.interfaces.ampl_nlp.AslNLP + :members: + :undoc-members: + :inherited-members: + :show-inheritance: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst new file mode 100644 index 00000000000..75528ac4b45 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst @@ -0,0 +1,8 @@ +Extended NLP Interface +====================== + +.. autoclass:: pyomo.contrib.pynumero.interfaces.nlp.ExtendedNLP + :members: + :undoc-members: + :inherited-members: + :show-inheritance: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst new file mode 100644 index 00000000000..10187b4156e --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst @@ -0,0 +1,8 @@ +External Grey Box Model +======================= + +.. autoclass:: pyomo.contrib.pynumero.interfaces.external_grey_box.ExternalGreyBoxModel + :members: + :undoc-members: + :inherited-members: + :show-inheritance: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.nlp.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.nlp.rst new file mode 100644 index 00000000000..d8532873c22 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.nlp.rst @@ -0,0 +1,8 @@ +NLP Interface +============= + +.. autoclass:: pyomo.contrib.pynumero.interfaces.nlp.NLP + :members: + :undoc-members: + :inherited-members: + :show-inheritance: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst new file mode 100644 index 00000000000..b9c6941bd93 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst @@ -0,0 +1,8 @@ +Projected NLP Interface +======================= + +.. autoclass:: pyomo.contrib.pynumero.interfaces.nlp_projections.ProjectedNLP + :members: + :undoc-members: + :inherited-members: + :show-inheritance: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst new file mode 100644 index 00000000000..c7200038f5e --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst @@ -0,0 +1,8 @@ +Pyomo Grey Box NLP Interface +============================ + +.. autoclass:: pyomo.contrib.pynumero.interfaces.pyomo_nlp.PyomoGreyBoxNLP + :members: + :undoc-members: + :inherited-members: + :show-inheritance: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst new file mode 100644 index 00000000000..e52ce33c2d9 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst @@ -0,0 +1,8 @@ +Pyomo NLP Interface +=================== + +.. autoclass:: pyomo.contrib.pynumero.interfaces.pyomo_nlp.PyomoNLP + :members: + :undoc-members: + :inherited-members: + :show-inheritance: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.rst new file mode 100644 index 00000000000..ec0b94960f6 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.rst @@ -0,0 +1,16 @@ +PyNumero NLP Interfaces +======================= + +.. automodule:: pyomo.contrib.pynumero.interfaces + :members: + +.. toctree:: + + pynumero.interfaces.nlp + pynumero.interfaces.extended_nlp + pynumero.interfaces.asl_nlp + pynumero.interfaces.ampl_nlp + pynumero.interfaces.pyomo_nlp + pynumero.interfaces.projected_nlp + pynumero.interfaces.external_grey_box_model + pynumero.interfaces.pyomo_grey_box_nlp diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.base.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.base.rst new file mode 100644 index 00000000000..0a94f87c6be --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.base.rst @@ -0,0 +1,26 @@ +Linear Solver Base Classes +========================== + +.. autoclass:: pyomo.contrib.pynumero.linalg.base.LinearSolverStatus + :members: + :inherited-members: + :show-inheritance: + :undoc-members: + +.. autoclass:: pyomo.contrib.pynumero.linalg.base.LinearSolverResults + :members: + :inherited-members: + :show-inheritance: + :undoc-members: + +.. autoclass:: pyomo.contrib.pynumero.linalg.base.LinearSolverInterface + :members: + :inherited-members: + :show-inheritance: + :undoc-members: + +.. autoclass:: pyomo.contrib.pynumero.linalg.base.DirectLinearSolverInterface + :members: + :inherited-members: + :show-inheritance: + :undoc-members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.ma27.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.ma27.rst new file mode 100644 index 00000000000..f1d2eed3ed0 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.ma27.rst @@ -0,0 +1,8 @@ +HSL MA27 +======== + +.. autoclass:: pyomo.contrib.pynumero.linalg.ma27_interface.MA27 + :members: + :inherited-members: + :show-inheritance: + :undoc-members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.ma57.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.ma57.rst new file mode 100644 index 00000000000..c97f193b5f8 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.ma57.rst @@ -0,0 +1,8 @@ +HSL MA57 +======== + +.. autoclass:: pyomo.contrib.pynumero.linalg.ma57_interface.MA57 + :members: + :inherited-members: + :show-inheritance: + :undoc-members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.mumps.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.mumps.rst new file mode 100644 index 00000000000..1fd5998dd4d --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.mumps.rst @@ -0,0 +1,8 @@ +MUMPS +===== + +.. autoclass:: pyomo.contrib.pynumero.linalg.mumps_interface.MumpsCentralizedAssembledLinearSolver + :members: + :inherited-members: + :show-inheritance: + :undoc-members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.rst new file mode 100644 index 00000000000..70b091becbd --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.rst @@ -0,0 +1,14 @@ +PyNumero Linear Solver Interfaces +================================= + +.. automodule:: pyomo.contrib.pynumero.linalg + :members: + +.. toctree:: + + pynumero.linalg.base + pynumero.linalg.ma27 + pynumero.linalg.ma57 + pynumero.linalg.mumps + pynumero.linalg.scipy + diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.scipy.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.scipy.rst new file mode 100644 index 00000000000..7e0a1d0b865 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.scipy.rst @@ -0,0 +1,14 @@ +Scipy +===== + +.. autoclass:: pyomo.contrib.pynumero.linalg.scipy_interface.ScipyLU + :members: + :inherited-members: + :show-inheritance: + :undoc-members: + +.. autoclass:: pyomo.contrib.pynumero.linalg.scipy_interface.ScipyIterative + :members: + :inherited-members: + :show-inheritance: + :undoc-members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.sparse.block_vector.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.sparse.block_vector.rst new file mode 100644 index 00000000000..c17d3d1df86 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.sparse.block_vector.rst @@ -0,0 +1,154 @@ +BlockVector +=========== + +Methods specific to :py:class:`pyomo.contrib.pynumero.sparse.block_vector.BlockVector`: + + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_block` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.block_sizes` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block_size` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.is_block_defined` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyfrom` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyto` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copy_structure` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_blocks` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.pprint` + +Attributes specific to :py:class:`pyomo.contrib.pynumero.sparse.block_vector.BlockVector`: + + * :py:attr:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.nblocks` + * :py:attr:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.bshape` + * :py:attr:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.has_none` + + +NumPy compatible methods: + + * `numpy.ndarray.dot() `_ + * `numpy.ndarray.sum() `_ + * `numpy.ndarray.all() `_ + * `numpy.ndarray.any() `_ + * `numpy.ndarray.max() `_ + * `numpy.ndarray.astype() `_ + * `numpy.ndarray.clip() `_ + * `numpy.ndarray.compress() `_ + * `numpy.ndarray.conj() `_ + * `numpy.ndarray.conjugate() `_ + * `numpy.ndarray.nonzero() `_ + * `numpy.ndarray.ptp() `_ + * `numpy.ndarray.round() `_ + * `numpy.ndarray.std() `_ + * `numpy.ndarray.var() `_ + * `numpy.ndarray.tofile() `_ + * `numpy.ndarray.min() `_ + * `numpy.ndarray.mean() `_ + * `numpy.ndarray.prod() `_ + * `numpy.ndarray.fill() `_ + * `numpy.ndarray.tolist() `_ + * `numpy.ndarray.flatten() `_ + * `numpy.ndarray.ravel() `_ + * `numpy.ndarray.argmax() `_ + * `numpy.ndarray.argmin() `_ + * `numpy.ndarray.cumprod() `_ + * `numpy.ndarray.cumsum() `_ + * `numpy.ndarray.copy() `_ + +For example, + +.. code-block:: python + + >>> import numpy as np + >>> from pyomo.contrib.pynumero.sparse import BlockVector + >>> v = BlockVector(2) + >>> v.set_block(0, np.random.normal(size=100)) + >>> v.set_block(1, np.random.normal(size=30)) + >>> avg = v.mean() + +NumPy compatible functions: + + * `numpy.log10() `_ + * `numpy.sin() `_ + * `numpy.cos() `_ + * `numpy.exp() `_ + * `numpy.ceil() `_ + * `numpy.floor() `_ + * `numpy.tan() `_ + * `numpy.arctan() `_ + * `numpy.arcsin() `_ + * `numpy.arccos() `_ + * `numpy.sinh() `_ + * `numpy.cosh() `_ + * `numpy.abs() `_ + * `numpy.tanh() `_ + * `numpy.arccosh() `_ + * `numpy.arcsinh() `_ + * `numpy.arctanh() `_ + * `numpy.fabs() `_ + * `numpy.sqrt() `_ + * `numpy.log() `_ + * `numpy.log2() `_ + * `numpy.absolute() `_ + * `numpy.isfinite() `_ + * `numpy.isinf() `_ + * `numpy.isnan() `_ + * `numpy.log1p() `_ + * `numpy.logical_not() `_ + * `numpy.expm1() `_ + * `numpy.exp2() `_ + * `numpy.sign() `_ + * `numpy.rint() `_ + * `numpy.square() `_ + * `numpy.positive() `_ + * `numpy.negative() `_ + * `numpy.rad2deg() `_ + * `numpy.deg2rad() `_ + * `numpy.conjugate() `_ + * `numpy.reciprocal() `_ + * `numpy.signbit() `_ + * `numpy.add() `_ + * `numpy.multiply() `_ + * `numpy.divide() `_ + * `numpy.subtract() `_ + * `numpy.greater() `_ + * `numpy.greater_equal() `_ + * `numpy.less() `_ + * `numpy.less_equal() `_ + * `numpy.not_equal() `_ + * `numpy.maximum() `_ + * `numpy.minimum() `_ + * `numpy.fmax() `_ + * `numpy.fmin() `_ + * `numpy.equal() `_ + * `numpy.logical_and() `_ + * `numpy.logical_or() `_ + * `numpy.logical_xor() `_ + * `numpy.logaddexp() `_ + * `numpy.logaddexp2() `_ + * `numpy.remainder() `_ + * `numpy.heaviside() `_ + * `numpy.hypot() `_ + +For example, + +.. code-block:: python + + >>> import numpy as np + >>> from pyomo.contrib.pynumero.sparse import BlockVector + >>> v = BlockVector(2) + >>> v.set_block(0, np.random.normal(size=100)) + >>> v.set_block(1, np.random.normal(size=30)) + >>> inf_norm = np.max(np.abs(v)) + +.. autoclass:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector +.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_block +.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block +.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.block_sizes +.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block_size +.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.is_block_defined +.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyfrom +.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyto +.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copy_structure +.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_blocks +.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.pprint +.. autoproperty:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.nblocks +.. autoproperty:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.bshape +.. autoproperty:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.has_none diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.sparse.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.sparse.rst new file mode 100644 index 00000000000..6d903abb5a4 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.sparse.rst @@ -0,0 +1,9 @@ +PyNumero Block Linear Algebra +============================= + +.. automodule:: pyomo.contrib.pynumero.sparse + :members: + +.. toctree:: + + pynumero.sparse.block_vector diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst new file mode 100644 index 00000000000..1ce98ce4a63 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst @@ -0,0 +1,272 @@ +Block Vectors and Matrices +========================== + +Block vectors and matrices +(:py:class:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector` +and +:py:class:`~pyomo.contrib.pynumero.sparse.block_matrix.BlockMatrix`) +provide a mechanism to perform linear algebra operations with very +structured matrices and vectors. + +When a BlockVector or BlockMatrix is constructed, the number of blocks +must be specified. + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> import numpy as np + >>> from scipy.sparse import coo_matrix + >>> from pyomo.contrib.pynumero.sparse import BlockVector, BlockMatrix + >>> v = BlockVector(3) + >>> m = BlockMatrix(3, 3) + +Setting blocks: + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> v.set_block(0, np.array([-0.67025575, -1.2])) + >>> v.set_block(1, np.array([0.1, 1.14872127])) + >>> v.set_block(2, np.array([1.25])) + >>> v.flatten() + array([-0.67025575, -1.2 , 0.1 , 1.14872127, 1.25 ]) + +The `flatten` method converts the BlockVector into a NumPy array. + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> m.set_block(0, 0, coo_matrix(np.array([[1.67025575, 0], [0, 2]]))) + >>> m.set_block(0, 1, coo_matrix(np.array([[0, -1.64872127], [0, 1]]))) + >>> m.set_block(0, 2, coo_matrix(np.array([[-1.0], [-1]]))) + >>> m.set_block(1, 0, coo_matrix(np.array([[0, -1.64872127], [0, 1]])).transpose()) + >>> m.set_block(1, 2, coo_matrix(np.array([[-1.0], [0]]))) + >>> m.set_block(2, 0, coo_matrix(np.array([[-1.0], [-1]])).transpose()) + >>> m.set_block(2, 1, coo_matrix(np.array([[-1.0], [0]])).transpose()) + >>> m.tocoo().toarray() + array([[ 1.67025575, 0. , 0. , -1.64872127, -1. ], + [ 0. , 2. , 0. , 1. , -1. ], + [ 0. , 0. , 0. , 0. , -1. ], + [-1.64872127, 1. , 0. , 0. , 0. ], + [-1. , -1. , -1. , 0. , 0. ]]) + +The `tocoo` method converts the `BlockMatrix` to a SciPy sparse `coo_matrix`. + +Once the dimensions of a block have been set, they cannot be changed: + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> v.set_block(0, np.ones(3)) + Traceback (most recent call last): + ... + ValueError: Incompatible dimensions for block 0; got 3; expected 2 + +Properties: + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> v.shape + (5,) + >>> v.size + 5 + >>> v.nblocks + 3 + >>> v.bshape + (3,) + >>> m.shape + (5, 5) + >>> m.bshape + (3, 3) + >>> m.nnz + 12 + +Much of the `BlockVector` API matches that of NumPy arrays: + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> v.sum() + 0.62846552 + >>> v.max() + 1.25 + >>> np.abs(v).flatten() + array([0.67025575, 1.2 , 0.1 , 1.14872127, 1.25 ]) + >>> (2*v).flatten() + array([-1.3405115 , -2.4 , 0.2 , 2.29744254, 2.5 ]) + >>> (v + v).flatten() + array([-1.3405115 , -2.4 , 0.2 , 2.29744254, 2.5 ]) + >>> v.dot(v) + 4.781303326558476 + +Similarly, `BlockMatrix` behaves very similarly to SciPy sparse matrices: + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> (2*m).tocoo().toarray() + array([[ 3.3405115 , 0. , 0. , -3.29744254, -2. ], + [ 0. , 4. , 0. , 2. , -2. ], + [ 0. , 0. , 0. , 0. , -2. ], + [-3.29744254, 2. , 0. , 0. , 0. ], + [-2. , -2. , -2. , 0. , 0. ]]) + >>> (m - m).tocoo().toarray() + array([[0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.]]) + >>> m * v + BlockVector(3,) + >>> (m * v).flatten() + array([-4.26341971, -2.50127873, -1.25 , -0.09493509, 1.77025575]) + +Accessing blocks + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> v.get_block(1) + array([0.1 , 1.14872127]) + >>> m.get_block(1, 0).toarray() + array([[ 0. , 0. ], + [-1.64872127, 1. ]]) + +Empty blocks in a `BlockMatrix` return `None`: + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> print(m.get_block(1, 1)) + None + +The dimensions of a blocks in a `BlockMatrix` can be set without setting a block: + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> m2 = BlockMatrix(2, 2) + >>> m2.set_row_size(0, 5) + >>> m2.set_block(0, 0, m.get_block(0, 0)) + Traceback (most recent call last): + ... + ValueError: Incompatible row dimensions for row 0; got 2; expected 5.0 + +Note that operations on `BlockVector` and `BlockMatrix` cannot be performed until the dimensions are fully specified: + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> v2 = BlockVector(3) + >>> v + v2 + Traceback (most recent call last): + ... + NotFullyDefinedBlockVectorError: Operation not allowed with None blocks. + >>> m2 = BlockMatrix(3, 3) + >>> m2 * 2 + Traceback (most recent call last): + ... + NotFullyDefinedBlockMatrixError: Operation not allowed with None rows. Specify at least one block in every row + +The `has_none` property can be used to see if a `BlockVector` is fully +specified. If `has_none` returns `True`, then there are `None` blocks, +and the `BlockVector` is not fully specified. + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> v.has_none + False + >>> v2.has_none + True + +For `BlockMatrix`, use the `has_undefined_row_sizes()` and `has_undefined_col_sizes()` methods: + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> m.has_undefined_row_sizes() + False + >>> m.has_undefined_col_sizes() + False + >>> m2.has_undefined_row_sizes() + True + >>> m2.has_undefined_col_sizes() + True + +To efficiently iterate over non-empty blocks in a `BlockMatrix`, use +the `get_block_mask()` method, which returns a 2-D array indicating +where the non-empty blocks are: + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> m.get_block_mask(copy=False) + array([[ True, True, True], + [ True, False, True], + [ True, True, False]]) + >>> for i, j in zip(*np.nonzero(m.get_block_mask(copy=False))): + ... assert m.get_block(i, j) is not None + +Copying data: + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> v2 = v.copy() + >>> v2.flatten() + array([-0.67025575, -1.2 , 0.1 , 1.14872127, 1.25 ]) + >>> v2 = v.copy_structure() + >>> v2.block_sizes() # doctest: +SKIP + array([2, 2, 1]) + >>> v2.copyfrom(v) + >>> v2.flatten() + array([-0.67025575, -1.2 , 0.1 , 1.14872127, 1.25 ]) + >>> m2 = m.copy() + >>> (m - m2).tocoo().toarray() + array([[0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.]]) + >>> m2 = m.copy_structure() + >>> m2.has_undefined_row_sizes() + False + >>> m2.has_undefined_col_sizes() + False + >>> m2.copyfrom(m) + >>> (m - m2).tocoo().toarray() + array([[0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.], + [0., 0., 0., 0., 0.]]) + +Nested blocks: + +.. doctest:: + :skipif: not numpy_available or not scipy_available + + >>> v2 = BlockVector(2) + >>> v2.set_block(0, v) + >>> v2.set_block(1, np.ones(2)) + >>> v2.block_sizes() # doctest: +SKIP + array([5, 2]) + >>> v2.flatten() + array([-0.67025575, -1.2 , 0.1 , 1.14872127, 1.25 , + 1. , 1. ]) + >>> v3 = v2.copy_structure() + >>> v3.fill(1) + >>> (v2 + v3).flatten() + array([ 0.32974425, -0.2 , 1.1 , 2.14872127, 2.25 , + 2. , 2. ]) + >>> np.abs(v2).flatten() + array([0.67025575, 1.2 , 0.1 , 1.14872127, 1.25 , + 1. , 1. ]) + >>> v2.get_block(0) + BlockVector(3,) + +Nested `BlockMatrix` applications work similarly. + +For more information, see the API documentation (:ref:`pynumero_api`). diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst new file mode 100644 index 00000000000..02ffe761778 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst @@ -0,0 +1,76 @@ +Linear Solver Interfaces +======================== + +PyNumero's interfaces to linear solvers are very thin wrappers, and, +hence, are rather low-level. It is relatively easy to wrap these again +for specific applications. For example, see the linear solver +interfaces in +https://github.com/Pyomo/pyomo/tree/main/pyomo/contrib/interior_point/linalg, +which wrap PyNumero's linear solver interfaces. + +The motivation to keep PyNumero's interfaces as such thin wrappers is +that different linear solvers serve different purposes. For example, +HSL's MA27 can factorize symmetric indefinite matrices, while MUMPS +can factorize unsymmetric, symmetric positive definite, or general +symmetric matrices. PyNumero seeks to be independent of the +application, giving more flexibility to algorithm developers. + +Interface to MA27 +----------------- + +.. doctest:: + :skipif: not numpy_available or not scipy_available or not ma27_available + + >>> import numpy as np + >>> from scipy.sparse import coo_matrix + >>> from scipy.sparse import tril + >>> from pyomo.contrib.pynumero.linalg.ma27_interface import MA27 + >>> row = np.array([0, 1, 0, 1, 0, 1, 2, 3, 3, 4, 4, 4]) + >>> col = np.array([0, 1, 3, 3, 4, 4, 4, 0, 1, 0, 1, 2]) + >>> data = np.array([1.67025575, 2, -1.64872127, 1, -1, -1, -1, -1.64872127, 1, -1, -1, -1]) + >>> A = coo_matrix((data, (row, col)), shape=(5,5)) + >>> A.toarray() + array([[ 1.67025575, 0. , 0. , -1.64872127, -1. ], + [ 0. , 2. , 0. , 1. , -1. ], + [ 0. , 0. , 0. , 0. , -1. ], + [-1.64872127, 1. , 0. , 0. , 0. ], + [-1. , -1. , -1. , 0. , 0. ]]) + >>> rhs = np.array([-0.67025575, -1.2, 0.1, 1.14872127, 1.25]) + >>> solver = MA27() + >>> solver.set_cntl(1, 1e-6) # set the pivot tolerance + >>> status = solver.do_symbolic_factorization(A) + >>> status = solver.do_numeric_factorization(A) + >>> x, status = solver.do_back_solve(rhs) + >>> np.max(np.abs(A*x - rhs)) <= 1e-15 + True + + +Interface to MUMPS +------------------ + +.. doctest:: + :skipif: not numpy_available or not scipy_available or not mumps_available + + >>> import numpy as np + >>> from scipy.sparse import coo_matrix + >>> from scipy.sparse import tril + >>> from pyomo.contrib.pynumero.linalg.mumps_interface import MumpsCentralizedAssembledLinearSolver + >>> row = np.array([0, 1, 0, 1, 0, 1, 2, 3, 3, 4, 4, 4]) + >>> col = np.array([0, 1, 3, 3, 4, 4, 4, 0, 1, 0, 1, 2]) + >>> data = np.array([1.67025575, 2, -1.64872127, 1, -1, -1, -1, -1.64872127, 1, -1, -1, -1]) + >>> A = coo_matrix((data, (row, col)), shape=(5,5)) + >>> A.toarray() + array([[ 1.67025575, 0. , 0. , -1.64872127, -1. ], + [ 0. , 2. , 0. , 1. , -1. ], + [ 0. , 0. , 0. , 0. , -1. ], + [-1.64872127, 1. , 0. , 0. , 0. ], + [-1. , -1. , -1. , 0. , 0. ]]) + >>> rhs = np.array([-0.67025575, -1.2, 0.1, 1.14872127, 1.25]) + >>> solver = MumpsCentralizedAssembledLinearSolver(sym=2, par=1, comm=None) # symmetric matrix; solve in serial + >>> solver.do_symbolic_factorization(A) + >>> solver.do_numeric_factorization(A) + >>> x = solver.do_back_solve(rhs) + >>> np.max(np.abs(A*x - rhs)) <= 1e-15 + True + +Of course, SciPy solvers can also be used. See SciPy documentation for details. diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.mpi_blocks.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.mpi_blocks.rst new file mode 100644 index 00000000000..b9cb1d5db7a --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.mpi_blocks.rst @@ -0,0 +1,65 @@ +MPI-Based Block Vectors and Matrices +==================================== + +PyNumero's MPI-based block vectors and matrices +(:py:class:`~pyomo.contrib.pynumero.sparse.mpi_block_vector.MPIBlockVector` +and +:py:class:`~pyomo.contrib.pynumero.sparse.mpi_block_matrix.MPIBlockMatrix`) +behave very similarly to `BlockVector` and `BlockMatrix`. The primary +difference is in construction. With `MPIBlockVector` and +`MPIBlockMatrix`, each block is owned by either a single process/rank +or all processes/ranks. + +Consider the following example (in a file called "parallel_vector_ops.py"). + +.. literalinclude:: ../../../../pyomo/contrib/pynumero/examples/parallel_vector_ops.py + +This example can be run with + +.. code-block:: + + mpirun -np 3 python -m mpi4py parallel_vector_ops.py + +The output is + +.. code-block:: + + [6. 6. 6. 2. 2. 2. 4. 4. 4. 2. 4. 6.] + 56.0 + 3 + +Note that the `make_local_copy()` method is not efficient and should +only be used for debugging. + +The -1 in `owners` means that the block at that index (index 3 in this +example) is owned by all processes. The non-negative integer values +indicate that the block at that index is owned by the process with +rank equal to the value. In this example, rank 0 owns block 1, rank 1 +owns block 2, and rank 2 owns block 0. Block 3 is owned by all ranks. +Note that blocks should only be set if the process/rank owns that +block. + +The operations performed with `MPIBlockVector` are identical to the +same operations performed with `BlockVector` (or even NumPy arrays), +except that the operations are now performed in parallel. + +`MPIBlockMatrix` construction is very similar. Consider the following +example in a file called "parallel_matvec.py". + +.. literalinclude:: ../../../../pyomo/contrib/pynumero/examples/parallel_matvec.py + +Which can be run with + +.. code-block:: + + mpirun -np 3 python -m mpi4py parallel_matvec.py + +The output is + +.. code-block:: + + error: 4.440892098500626e-16 + +The most difficult part of using `MPIBlockVector` and `MPIBlockMatrix` +is determining the best structure and rank ownership to maximize +parallel efficiency. diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.nlp_interfaces.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.nlp_interfaces.rst new file mode 100644 index 00000000000..28818709330 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.nlp_interfaces.rst @@ -0,0 +1,115 @@ +NLP Interfaces +============== + +Below are examples of using PyNumero's interfaces to ASL for function +and derivative evaluation. More information can be found in the API +documentation (:ref:`pynumero_api`). + +Relevant imports + +.. doctest:: + :skipif: not numpy_available or not scipy_available or not asl_available + + >>> import pyomo.environ as pe + >>> from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP + >>> import numpy as np + +Create a Pyomo model + +.. doctest:: + :skipif: not numpy_available or not scipy_available or not asl_available + + >>> m = pe.ConcreteModel() + >>> m.x = pe.Var(bounds=(-5, None)) + >>> m.y = pe.Var(initialize=2.5) + >>> m.obj = pe.Objective(expr=m.x**2 + m.y**2) + >>> m.c1 = pe.Constraint(expr=m.y == (m.x - 1)**2) + >>> m.c2 = pe.Constraint(expr=m.y >= pe.exp(m.x)) + +Create a :py:class:`pyomo.contrib.pynumero.interfaces.pyomo_nlp.PyomoNLP` instance + +.. doctest:: + :skipif: not numpy_available or not scipy_available or not asl_available + + >>> nlp = PyomoNLP(m) + +Get values of primals and duals + +.. doctest:: + :skipif: not numpy_available or not scipy_available or not asl_available + + >>> nlp.get_primals() + array([0. , 2.5]) + >>> nlp.get_duals() + array([0., 0.]) + +Get variable and constraint bounds + +.. doctest:: + :skipif: not numpy_available or not scipy_available or not asl_available + + >>> nlp.primals_lb() + array([ -5., -inf]) + >>> nlp.primals_ub() + array([inf, inf]) + >>> nlp.constraints_lb() + array([ 0., -inf]) + >>> nlp.constraints_ub() + array([0., 0.]) + +Objective and constraint evaluations + +.. doctest:: + :skipif: not numpy_available or not scipy_available or not asl_available + + >>> nlp.evaluate_objective() + 6.25 + >>> nlp.evaluate_constraints() + array([ 1.5, -1.5]) + +Derivative evaluations + +.. doctest:: + :skipif: not numpy_available or not scipy_available or not asl_available + + >>> nlp.evaluate_grad_objective() + array([0., 5.]) + >>> nlp.evaluate_jacobian() # doctest: +SKIP + <2x2 sparse matrix of type '' + with 4 stored elements in COOrdinate format> + >>> nlp.evaluate_jacobian().toarray() + array([[ 2., 1.], + [ 1., -1.]]) + >>> nlp.evaluate_hessian_lag().toarray() + array([[2., 0.], + [0., 2.]]) + +Set values of primals and duals + +.. doctest:: + :skipif: not numpy_available or not scipy_available or not asl_available + + >>> nlp.set_primals(np.array([0, 1])) + >>> nlp.evaluate_constraints() + array([0., 0.]) + >>> nlp.set_duals(np.array([-2/3, 4/3])) + >>> nlp.evaluate_grad_objective() + nlp.evaluate_jacobian().transpose() * nlp.get_duals() + array([0., 0.]) + +Equality and inequality constraints separately + +.. doctest:: + :skipif: not numpy_available or not scipy_available or not asl_available + + >>> nlp.evaluate_eq_constraints() + array([0.]) + >>> nlp.evaluate_jacobian_eq().toarray() + array([[2., 1.]]) + >>> nlp.evaluate_ineq_constraints() + array([0.]) + >>> nlp.evaluate_jacobian_ineq().toarray() + array([[ 1., -1.]]) + >>> nlp.get_duals_eq() + array([-0.66666667]) + >>> nlp.get_duals_ineq() + array([1.33333333]) diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.rst b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.rst new file mode 100644 index 00000000000..83593c94040 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.rst @@ -0,0 +1,11 @@ +10 Minutes to PyNumero +====================== + +.. toctree:: + + tutorial.nlp_interfaces + tutorial.linear_solver_interfaces + tutorial.block_vectors_and_matrices + tutorial.mpi_blocks + +Other examples may be found at https://github.com/Pyomo/pyomo/tree/main/pyomo/contrib/pynumero/examples. diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pyros.rst b/doc/OnlineDocs/user_guide/contributed_packages/pyros.rst new file mode 100644 index 00000000000..95049eded8a --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/pyros.rst @@ -0,0 +1,1078 @@ +############ +PyROS Solver +############ + +PyROS (Pyomo Robust Optimization Solver) is a Pyomo-based meta-solver +for non-convex, two-stage adjustable robust optimization problems. + +It was developed by **Natalie M. Isenberg**, **Jason A. F. Sherman**, +and **Chrysanthos E. Gounaris** of Carnegie Mellon University, +in collaboration with **John D. Siirola** of Sandia National Labs. +The developers gratefully acknowledge support from the U.S. Department of Energy's +`Institute for the Design of Advanced Energy Systems (IDAES) `_. + +Methodology Overview +----------------------------- + +Below is an overview of the type of optimization models PyROS can accommodate. + + +* PyROS is suitable for optimization models of **continuous variables** + that may feature non-linearities (including **non-convexities**) in + both the variables and uncertain parameters. +* PyROS can handle **equality constraints** defining state variables, + including implicit state variables that cannot be eliminated via + reformulation. +* PyROS allows for **two-stage** optimization problems that may + feature both first-stage and second-stage degrees of freedom. + +PyROS is designed to operate on deterministic models of the general form + +.. _deterministic-model: + +.. math:: + \begin{array}{clll} + \displaystyle \min_{\substack{x \in \mathcal{X}, \\ z \in \mathbb{R}^{n_z}, y\in\mathbb{R}^{n_y}}} & ~~ f_1\left(x\right) + f_2(x,z,y; q^{\text{nom}}) & \\ + \displaystyle \text{s.t.} & ~~ g_i(x, z, y; q^{\text{nom}}) \leq 0 & \forall\,i \in \mathcal{I} \\ + & ~~ h_j(x,z,y; q^{\text{nom}}) = 0 & \forall\,j \in \mathcal{J} \\ + \end{array} + +where: + +* :math:`x \in \mathcal{X}` are the "design" variables + (i.e., first-stage degrees of freedom), + where :math:`\mathcal{X} \subseteq \mathbb{R}^{n_x}` is the feasible space defined by the model constraints + (including variable bounds specifications) referencing :math:`x` only. +* :math:`z \in \mathbb{R}^{n_z}` are the "control" variables + (i.e., second-stage degrees of freedom) +* :math:`y \in \mathbb{R}^{n_y}` are the "state" variables +* :math:`q \in \mathbb{R}^{n_q}` is the vector of model parameters considered + uncertain, and :math:`q^{\text{nom}}` is the vector of nominal values + associated with those. +* :math:`f_1\left(x\right)` are the terms of the objective function that depend + only on design variables +* :math:`f_2\left(x, z, y; q\right)` are the terms of the objective function + that depend on all variables and the uncertain parameters +* :math:`g_i\left(x, z, y; q\right)` is the :math:`i^\text{th}` + inequality constraint function in set :math:`\mathcal{I}` + (see :ref:`Note `) +* :math:`h_j\left(x, z, y; q\right)` is the :math:`j^\text{th}` + equality constraint function in set :math:`\mathcal{J}` + (see :ref:`Note `) + +.. _var-bounds-to-ineqs: + +.. note:: + PyROS accepts models in which bounds are directly imposed on + ``Var`` objects representing components of the variables :math:`z` + and :math:`y`. These models are cast to + :ref:`the form above ` + by reformulating the bounds as inequality constraints. + +.. _unique-mapping: + +.. note:: + A key requirement of PyROS is that each value of :math:`\left(x, z, q \right)` + maps to a unique value of :math:`y`, a property that is assumed to + be properly enforced by the system of equality constraints + :math:`\mathcal{J}`. + If the mapping is not unique, then the selection of 'state' + (i.e., not degree of freedom) variables :math:`y` is incorrect, + and one or more of the :math:`y` variables should be appropriately + redesignated to be part of either :math:`x` or :math:`z`. + +In order to cast the robust optimization counterpart of the +:ref:`deterministic model `, +we now assume that the uncertain parameters may attain +any realization in a compact uncertainty set +:math:`\mathcal{Q} \subseteq \mathbb{R}^{n_q}` containing +the nominal value :math:`q^{\text{nom}}`. +The set :math:`\mathcal{Q}` may be **either continuous or discrete**. + +Based on the above notation, the form of the robust counterpart addressed by PyROS is + +.. math:: + \begin{array}{ccclll} + \displaystyle \min_{x \in \mathcal{X}} + & \displaystyle \max_{q \in \mathcal{Q}} + & \displaystyle \min_{\substack{z \in \mathbb{R}^{n_z},\\y \in \mathbb{R}^{n_y}}} \ \ & \displaystyle ~~ f_1\left(x\right) + f_2\left(x, z, y, q\right) \\ + & & \text{s.t.}~ & \displaystyle ~~ g_i\left(x, z, y, q\right) \leq 0 & & \forall\, i \in \mathcal{I}\\ + & & & \displaystyle ~~ h_j\left(x, z, y, q\right) = 0 & & \forall\,j \in \mathcal{J} + \end{array} + +PyROS solves problems of this form using the +Generalized Robust Cutting-Set algorithm developed in [Isenberg_et_al]_. + +When using PyROS, please consider citing the above paper. + +PyROS Required Inputs +----------------------------- +The required inputs to the PyROS solver are: + +* The deterministic optimization model +* List of first-stage ("design") variables +* List of second-stage ("control") variables +* List of parameters considered uncertain +* The uncertainty set +* Subordinate local and global nonlinear programming (NLP) solvers + +These are more elaborately presented in the +:ref:`Solver Interface ` section. + +.. note:: + Any variables in the model not specified to be first-stage or second-stage + variables are automatically considered to be state variables. + +.. _solver-interface: + +PyROS Solver Interface +----------------------------- + +.. autoclass:: pyomo.contrib.pyros.PyROS + :members: solve + +.. note:: + Upon successful convergence of PyROS, the solution returned is + certified to be robust optimal only if: + + 1. master problems are solved to global optimality + (by specifying ``solve_master_globally=True``) + 2. a worst-case objective focus is chosen + (by specifying ``objective_focus=ObjectiveType.worst_case``) + + Otherwise, the solution returned is certified to only be robust feasible. + + +PyROS Uncertainty Sets +----------------------------- +Uncertainty sets are represented by subclasses of +the :class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` +abstract base class. +PyROS provides a suite of pre-implemented subclasses representing +commonly used uncertainty sets. +Custom user-defined uncertainty set types may be implemented by +subclassing the +:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` class. +The intersection of a sequence of concrete +:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` +instances can be easily constructed by instantiating the pre-implemented +:class:`~pyomo.contrib.pyros.uncertainty_sets.IntersectionSet` +subclass. + +The table that follows provides mathematical definitions of +the various abstract and pre-implemented +:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` subclasses. + +.. _table-uncertsets: + +.. list-table:: Mathematical definitions of PyROS uncertainty sets of dimension :math:`n`. + :header-rows: 1 + :class: tight-table + + * - Uncertainty Set Type + - Input Data + - Mathematical Definition + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` + - :math:`\begin{array}{l} q ^{\text{L}} \in \mathbb{R}^{n}, \\ q^{\text{U}} \in \mathbb{R}^{n} \end{array}` + - :math:`\{q \in \mathbb{R}^n \mid q^\mathrm{L} \leq q \leq q^\mathrm{U}\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.CardinalitySet` + - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ \hat{q} \in \mathbb{R}_{+}^{n}, \\ \Gamma \in [0, n] \end{array}` + - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} q = q^{0} + \hat{q} \circ \xi \\ \displaystyle \sum_{i=1}^{n} \xi_{i} \leq \Gamma \\ \xi \in [0, 1]^{n} \end{array} \right\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.BudgetSet` + - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ b \in \mathbb{R}_{+}^{L}, \\ B \in \{0, 1\}^{L \times n} \end{array}` + - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \begin{pmatrix} B \\ -I \end{pmatrix} q \leq \begin{pmatrix} b + Bq^{0} \\ -q^{0} \end{pmatrix} \end{array} \right\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.FactorModelSet` + - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ \Psi \in \mathbb{R}^{n \times F}, \\ \beta \in [0, 1] \end{array}` + - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} q = q^{0} + \Psi \xi \\ \displaystyle\bigg| \sum_{j=1}^{F} \xi_{j} \bigg| \leq \beta F \\ \xi \in [-1, 1]^{F} \\ \end{array} \right\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.PolyhedralSet` + - :math:`\begin{array}{l} A \in \mathbb{R}^{m \times n}, \\ b \in \mathbb{R}^{m}\end{array}` + - :math:`\{q \in \mathbb{R}^{n} \mid A q \leq b\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.AxisAlignedEllipsoidalSet` + - :math:`\begin{array}{l} q^0 \in \mathbb{R}^{n}, \\ \alpha \in \mathbb{R}_{+}^{n} \end{array}` + - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \displaystyle\sum_{\substack{i = 1: \\ \alpha_{i} > 0}}^{n} \left(\frac{q_{i} - q_{i}^{0}}{\alpha_{i}}\right)^2 \leq 1 \\ q_{i} = q_{i}^{0} \,\forall\,i : \alpha_{i} = 0 \end{array} \right\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.EllipsoidalSet` + - :math:`\begin{array}{l} q^0 \in \mathbb{R}^n, \\ P \in \mathbb{S}_{++}^{n}, \\ s \in \mathbb{R}_{+} \end{array}` + - :math:`\{q \in \mathbb{R}^{n} \mid (q - q^{0})^{\intercal} P^{-1} (q - q^{0}) \leq s\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` + - :math:`g: \mathbb{R}^{n} \to \mathbb{R}^{m}` + - :math:`\{q \in \mathbb{R}^{n} \mid g(q) \leq 0\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.DiscreteScenarioSet` + - :math:`q^{1}, q^{2},\dots , q^{S} \in \mathbb{R}^{n}` + - :math:`\{q^{1}, q^{2}, \dots , q^{S}\}` + * - :class:`~pyomo.contrib.pyros.uncertainty_sets.IntersectionSet` + - :math:`\mathcal{Q}_{1}, \mathcal{Q}_{2}, \dots , \mathcal{Q}_{m} \subset \mathbb{R}^{n}` + - :math:`\displaystyle \bigcap_{i=1}^{m} \mathcal{Q}_{i}` + +.. note:: + Each of the PyROS uncertainty set classes inherits from the + :class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` + abstract base class. + +PyROS Uncertainty Set Classes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.BoxSet + :show-inheritance: + :special-members: bounds, type, parameter_bounds, dim, point_in_set + +.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.CardinalitySet + :show-inheritance: + :special-members: origin, positive_deviation, gamma, type, parameter_bounds, dim, point_in_set + +.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.BudgetSet + :show-inheritance: + :special-members: coefficients_mat, rhs_vec, origin, budget_membership_mat, budget_rhs_vec, type, parameter_bounds, dim, point_in_set + +.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.FactorModelSet + :show-inheritance: + :special-members: origin, number_of_factors, psi_mat, beta, type, parameter_bounds, dim, point_in_set + +.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.PolyhedralSet + :show-inheritance: + :special-members: coefficients_mat, rhs_vec, type, parameter_bounds, dim, point_in_set + +.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.AxisAlignedEllipsoidalSet + :show-inheritance: + :special-members: center, half_lengths, type, parameter_bounds, dim, point_in_set + +.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.EllipsoidalSet + :show-inheritance: + :special-members: center, shape_matrix, scale, type, parameter_bounds, dim, point_in_set + +.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.UncertaintySet + :show-inheritance: + :special-members: parameter_bounds, dim, point_in_set + +.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.DiscreteScenarioSet + :show-inheritance: + :special-members: scenarios, type, parameter_bounds, dim, point_in_set + +.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.IntersectionSet + :show-inheritance: + :special-members: all_sets, type, parameter_bounds, dim, point_in_set + + +PyROS Usage Example +----------------------------- + +In this section, we illustrate the usage of PyROS with a modeling example. +The deterministic problem of interest is called *hydro* +(available `here `_), +a QCQP taken from the +`GAMS Model Library `_. +We have converted the model to Pyomo format using the +`GAMS Convert tool `_. + +The *hydro* model features 31 variables, +of which 13 are degrees of freedom and 18 are state variables. +Moreover, there are +6 linear inequality constraints, +12 linear equality constraints, +6 non-linear (quadratic) equality constraints, +and a quadratic objective. +We have extended this model by converting one objective coefficient, +two constraint coefficients, and one constraint right-hand side +into ``Param`` objects so that they can be considered uncertain later on. + +.. note:: + Per our analysis, the *hydro* problem satisfies the requirement that + each value of :math:`\left(x, z, q \right)` maps to a unique + value of :math:`y`, which, in accordance with + :ref:`our earlier note `, + indicates a proper partitioning of the model variables + into (first-stage and second-stage) degrees of freedom and + state variables. + +Step 0: Import Pyomo and the PyROS Module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In anticipation of using the PyROS solver and building the deterministic Pyomo +model: + +.. doctest:: + + >>> # === Required import === + >>> import pyomo.environ as pyo + >>> import pyomo.contrib.pyros as pyros + + >>> # === Instantiate the PyROS solver object === + >>> pyros_solver = pyo.SolverFactory("pyros") + +Step 1: Define the Deterministic Problem +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The deterministic Pyomo model for *hydro* is shown below. + +.. note:: + Primitive data (Python literals) that have been hard-coded within a + deterministic model cannot be later considered uncertain, + unless they are first converted to ``Param`` objects within + the ``ConcreteModel`` object. + Furthermore, any ``Param`` object that is to be later considered + uncertain must have the property ``mutable=True``. + +.. note:: + In case modifying the ``mutable`` property inside the deterministic + model object itself is not straightforward in your context, + you may consider adding the following statement **after** + ``import pyomo.environ as pyo`` but **before** defining the model + object: ``pyo.Param.DefaultMutable = True``. + For all ``Param`` objects declared after this statement, + the attribute ``mutable`` is set to ``True`` by default. + Hence, non-mutable ``Param`` objects are now declared by + explicitly passing the argument ``mutable=False`` to the + ``Param`` constructor. + +.. doctest:: + + + >>> # === Construct the Pyomo model object === + >>> m = pyo.ConcreteModel() + >>> m.name = "hydro" + + >>> # === Define variables === + >>> m.x1 = pyo.Var(within=pyo.Reals,bounds=(150,1500),initialize=150) + >>> m.x2 = pyo.Var(within=pyo.Reals,bounds=(150,1500),initialize=150) + >>> m.x3 = pyo.Var(within=pyo.Reals,bounds=(150,1500),initialize=150) + >>> m.x4 = pyo.Var(within=pyo.Reals,bounds=(150,1500),initialize=150) + >>> m.x5 = pyo.Var(within=pyo.Reals,bounds=(150,1500),initialize=150) + >>> m.x6 = pyo.Var(within=pyo.Reals,bounds=(150,1500),initialize=150) + >>> m.x7 = pyo.Var(within=pyo.Reals,bounds=(0,1000),initialize=0) + >>> m.x8 = pyo.Var(within=pyo.Reals,bounds=(0,1000),initialize=0) + >>> m.x9 = pyo.Var(within=pyo.Reals,bounds=(0,1000),initialize=0) + >>> m.x10 = pyo.Var(within=pyo.Reals,bounds=(0,1000),initialize=0) + >>> m.x11 = pyo.Var(within=pyo.Reals,bounds=(0,1000),initialize=0) + >>> m.x12 = pyo.Var(within=pyo.Reals,bounds=(0,1000),initialize=0) + >>> m.x13 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) + >>> m.x14 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) + >>> m.x15 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) + >>> m.x16 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) + >>> m.x17 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) + >>> m.x18 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) + >>> m.x19 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) + >>> m.x20 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) + >>> m.x21 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) + >>> m.x22 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) + >>> m.x23 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) + >>> m.x24 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) + >>> m.x25 = pyo.Var(within=pyo.Reals,bounds=(100000,100000),initialize=100000) + >>> m.x26 = pyo.Var(within=pyo.Reals,bounds=(60000,120000),initialize=60000) + >>> m.x27 = pyo.Var(within=pyo.Reals,bounds=(60000,120000),initialize=60000) + >>> m.x28 = pyo.Var(within=pyo.Reals,bounds=(60000,120000),initialize=60000) + >>> m.x29 = pyo.Var(within=pyo.Reals,bounds=(60000,120000),initialize=60000) + >>> m.x30 = pyo.Var(within=pyo.Reals,bounds=(60000,120000),initialize=60000) + >>> m.x31 = pyo.Var(within=pyo.Reals,bounds=(60000,120000),initialize=60000) + + >>> # === Define parameters === + >>> m.set_of_params = pyo.Set(initialize=[0, 1, 2, 3]) + >>> nominal_values = {0:82.8*0.0016, 1:4.97, 2:4.97, 3:1800} + >>> m.p = pyo.Param(m.set_of_params, initialize=nominal_values, mutable=True) + + >>> # === Specify the objective function === + >>> m.obj = pyo.Objective(expr=m.p[0]*m.x1**2 + 82.8*8*m.x1 + 82.8*0.0016*m.x2**2 + + ... 82.8*82.8*8*m.x2 + 82.8*0.0016*m.x3**2 + 82.8*8*m.x3 + + ... 82.8*0.0016*m.x4**2 + 82.8*8*m.x4 + 82.8*0.0016*m.x5**2 + + ... 82.8*8*m.x5 + 82.8*0.0016*m.x6**2 + 82.8*8*m.x6 + 248400, + ... sense=pyo.minimize) + + >>> # === Specify the constraints === + >>> m.c2 = pyo.Constraint(expr=-m.x1 - m.x7 + m.x13 + 1200<= 0) + >>> m.c3 = pyo.Constraint(expr=-m.x2 - m.x8 + m.x14 + 1500 <= 0) + >>> m.c4 = pyo.Constraint(expr=-m.x3 - m.x9 + m.x15 + 1100 <= 0) + >>> m.c5 = pyo.Constraint(expr=-m.x4 - m.x10 + m.x16 + m.p[3] <= 0) + >>> m.c6 = pyo.Constraint(expr=-m.x5 - m.x11 + m.x17 + 950 <= 0) + >>> m.c7 = pyo.Constraint(expr=-m.x6 - m.x12 + m.x18 + 1300 <= 0) + >>> m.c8 = pyo.Constraint(expr=12*m.x19 - m.x25 + m.x26 == 24000) + >>> m.c9 = pyo.Constraint(expr=12*m.x20 - m.x26 + m.x27 == 24000) + >>> m.c10 = pyo.Constraint(expr=12*m.x21 - m.x27 + m.x28 == 24000) + >>> m.c11 = pyo.Constraint(expr=12*m.x22 - m.x28 + m.x29 == 24000) + >>> m.c12 = pyo.Constraint(expr=12*m.x23 - m.x29 + m.x30 == 24000) + >>> m.c13 = pyo.Constraint(expr=12*m.x24 - m.x30 + m.x31 == 24000) + >>> m.c14 = pyo.Constraint(expr=-8e-5*m.x7**2 + m.x13 == 0) + >>> m.c15 = pyo.Constraint(expr=-8e-5*m.x8**2 + m.x14 == 0) + >>> m.c16 = pyo.Constraint(expr=-8e-5*m.x9**2 + m.x15 == 0) + >>> m.c17 = pyo.Constraint(expr=-8e-5*m.x10**2 + m.x16 == 0) + >>> m.c18 = pyo.Constraint(expr=-8e-5*m.x11**2 + m.x17 == 0) + >>> m.c19 = pyo.Constraint(expr=-8e-5*m.x12**2 + m.x18 == 0) + >>> m.c20 = pyo.Constraint(expr=-4.97*m.x7 + m.x19 == 330) + >>> m.c21 = pyo.Constraint(expr=-m.p[1]*m.x8 + m.x20 == 330) + >>> m.c22 = pyo.Constraint(expr=-4.97*m.x9 + m.x21 == 330) + >>> m.c23 = pyo.Constraint(expr=-4.97*m.x10 + m.x22 == 330) + >>> m.c24 = pyo.Constraint(expr=-m.p[2]*m.x11 + m.x23 == 330) + >>> m.c25 = pyo.Constraint(expr=-4.97*m.x12 + m.x24 == 330) + +Step 2: Define the Uncertainty +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +First, we need to collect into a list those ``Param`` objects of our model +that represent potentially uncertain parameters. +For the purposes of our example, we shall assume uncertainty in the model +parameters ``[m.p[0], m.p[1], m.p[2], m.p[3]]``, for which we can +conveniently utilize the object ``m.p`` (itself an indexed ``Param`` object). + +.. doctest:: + + >>> # === Specify which parameters are uncertain === + >>> # We can pass IndexedParams this way to PyROS, + >>> # or as an expanded list per index + >>> uncertain_parameters = [m.p] + +.. note:: + Any ``Param`` object that is to be considered uncertain by PyROS + must have the property ``mutable=True``. + +PyROS will seek to identify solutions that remain feasible for any +realization of these parameters included in an uncertainty set. +To that end, we need to construct an +:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` +object. +In our example, let us utilize the +:class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` +constructor to specify +an uncertainty set of simple hyper-rectangular geometry. +For this, we will assume each parameter value is uncertain within a +percentage of its nominal value. Constructing this specific +:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` +object can be done as follows: + +.. doctest:: + + >>> # === Define the pertinent data === + >>> relative_deviation = 0.15 + >>> bounds = [ + ... (nominal_values[i] - relative_deviation*nominal_values[i], + ... nominal_values[i] + relative_deviation*nominal_values[i]) + ... for i in range(4) + ... ] + + >>> # === Construct the desirable uncertainty set === + >>> box_uncertainty_set = pyros.BoxSet(bounds=bounds) + +Step 3: Solve with PyROS +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +PyROS requires the user to supply one local and one global NLP solver to use +for solving sub-problems. +For convenience, we shall have PyROS invoke BARON as both the local and the +global NLP solver: + +.. doctest:: + :skipif: not (baron.available() and baron.license_is_valid()) + + >>> # === Designate local and global NLP solvers === + >>> local_solver = pyo.SolverFactory('baron') + >>> global_solver = pyo.SolverFactory('baron') + +.. note:: + Additional NLP optimizers can be automatically used in the event the primary + subordinate local or global optimizer passed + to the PyROS :meth:`~pyomo.contrib.pyros.PyROS.solve` method + does not successfully solve a subproblem to an appropriate termination + condition. These alternative solvers are provided through the optional + keyword arguments ``backup_local_solvers`` and ``backup_global_solvers``. + +The final step in solving a model with PyROS is to construct the +remaining required inputs, namely +``first_stage_variables`` and ``second_stage_variables``. +Below, we present two separate cases. + +PyROS Termination Conditions +""""""""""""""""""""""""""""" + +PyROS will return one of six termination conditions upon completion. +These termination conditions are defined through the +:class:`~pyomo.contrib.pyros.util.pyrosTerminationCondition` enumeration +and tabulated below. + +.. table:: PyROS termination conditions. + + +----------------------------------------------------------------------------------+----------------------------------------------------------------+ + | Termination Condition | Description | + +==================================================================================+================================================================+ + | :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_optimal` | The final solution is robust optimal | + +----------------------------------------------------------------------------------+----------------------------------------------------------------+ + | :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_feasible` | The final solution is robust feasible | + +----------------------------------------------------------------------------------+----------------------------------------------------------------+ + | :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_infeasible` | The posed problem is robust infeasible | + +----------------------------------------------------------------------------------+----------------------------------------------------------------+ + | :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.max_iter` | Maximum number of GRCS iteration reached | + +----------------------------------------------------------------------------------+----------------------------------------------------------------+ + | :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.time_out` | Maximum number of time reached | + +----------------------------------------------------------------------------------+----------------------------------------------------------------+ + | :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.subsolver_error` | Unacceptable return status(es) from a user-supplied sub-solver| + +----------------------------------------------------------------------------------+----------------------------------------------------------------+ + + +A Single-Stage Problem +""""""""""""""""""""""""" +If we choose to designate all variables as either design or state variables, +without any control variables (i.e., all degrees of freedom are first-stage), +we can use PyROS to solve the single-stage problem as shown below. +In particular, let us instruct PyROS that variables +``m.x1`` through ``m.x6``, ``m.x19`` through ``m.x24``, and ``m.x31`` +correspond to first-stage degrees of freedom. + +.. _single-stage-problem: + +.. doctest:: + :skipif: not (baron.available() and baron.license_is_valid()) + + >>> # === Designate which variables correspond to first-stage + >>> # and second-stage degrees of freedom === + >>> first_stage_variables = [ + ... m.x1, m.x2, m.x3, m.x4, m.x5, m.x6, + ... m.x19, m.x20, m.x21, m.x22, m.x23, m.x24, m.x31, + ... ] + >>> second_stage_variables = [] + >>> # The remaining variables are implicitly designated to be state variables + + >>> # === Call PyROS to solve the robust optimization problem === + >>> results_1 = pyros_solver.solve( + ... model=m, + ... first_stage_variables=first_stage_variables, + ... second_stage_variables=second_stage_variables, + ... uncertain_params=uncertain_parameters, + ... uncertainty_set=box_uncertainty_set, + ... local_solver=local_solver, + ... global_solver=global_solver, + ... objective_focus=pyros.ObjectiveType.worst_case, + ... solve_master_globally=True, + ... load_solution=False, + ... ) + ============================================================================== + PyROS: The Pyomo Robust Optimization Solver... + ... + ------------------------------------------------------------------------------ + Robust optimal solution identified. + ------------------------------------------------------------------------------ + ... + ------------------------------------------------------------------------------ + All done. Exiting PyROS. + ============================================================================== + >>> # === Query results === + >>> time = results_1.time + >>> iterations = results_1.iterations + >>> termination_condition = results_1.pyros_termination_condition + >>> objective = results_1.final_objective_value + >>> # === Print some results === + >>> single_stage_final_objective = round(objective,-1) + >>> print(f"Final objective value: {single_stage_final_objective}") + Final objective value: 48367380.0 + >>> print(f"PyROS termination condition: {termination_condition}") + PyROS termination condition: pyrosTerminationCondition.robust_optimal + +PyROS Results Object +""""""""""""""""""""""""""" +The results object returned by PyROS allows you to query the following information +from the solve call: + +* ``iterations``: total iterations of the algorithm +* ``time``: total wallclock time (or elapsed time) in seconds +* ``pyros_termination_condition``: the GRCS algorithm termination condition +* ``final_objective_value``: the final objective function value. + +The :ref:`preceding code snippet ` +demonstrates how to retrieve this information. + +If we pass ``load_solution=True`` (the default setting) +to the :meth:`~pyomo.contrib.pyros.PyROS.solve` method, +then the solution at which PyROS terminates will be loaded to +the variables of the original deterministic model. +Note that in the :ref:`preceding code snippet `, +we set ``load_solution=False`` to ensure the next set of runs shown here can +utilize the initial point loaded to the original deterministic model, +as the initial point may affect the performance of sub-solvers. + +.. note:: + The reported ``final_objective_value`` and final model variable values + depend on the selection of the option ``objective_focus``. + The ``final_objective_value`` is the sum of first-stage + and second-stage objective functions. + If ``objective_focus = ObjectiveType.nominal``, + second-stage objective and variables are evaluated at + the nominal realization of the uncertain parameters, :math:`q^{\text{nom}}`. + If ``objective_focus = ObjectiveType.worst_case``, second-stage objective + and variables are evaluated at the worst-case realization + of the uncertain parameters, :math:`q^{k^\ast}` + where :math:`k^\ast = \mathrm{argmax}_{k \in \mathcal{K}}~f_2(x,z^k,y^k,q^k)`. + + +A Two-Stage Problem +"""""""""""""""""""""" +For this next set of runs, we will +assume that some of the previously designated first-stage degrees of +freedom are in fact second-stage degrees of freedom. +PyROS handles second-stage degrees of freedom via the use of polynomial +decision rules, of which the degree is controlled through the +optional keyword argument ``decision_rule_order`` to the PyROS +:meth:`~pyomo.contrib.pyros.PyROS.solve` method. +In this example, we select affine decision rules by setting +``decision_rule_order=1``: + +.. _example-two-stg: + +.. doctest:: + :skipif: not (baron.available() and baron.license_is_valid()) + + >>> # === Define the variable partitioning + >>> first_stage_variables =[m.x5, m.x6, m.x19, m.x22, m.x23, m.x24, m.x31] + >>> second_stage_variables = [m.x1, m.x2, m.x3, m.x4, m.x20, m.x21] + >>> # The remaining variables are implicitly designated to be state variables + + >>> # === Call PyROS to solve the robust optimization problem === + >>> results_2 = pyros_solver.solve( + ... model=m, + ... first_stage_variables=first_stage_variables, + ... second_stage_variables=second_stage_variables, + ... uncertain_params=uncertain_parameters, + ... uncertainty_set=box_uncertainty_set, + ... local_solver=local_solver, + ... global_solver=global_solver, + ... objective_focus=pyros.ObjectiveType.worst_case, + ... solve_master_globally=True, + ... decision_rule_order=1, + ... ) + ============================================================================== + PyROS: The Pyomo Robust Optimization Solver... + ... + ------------------------------------------------------------------------------ + Robust optimal solution identified. + ------------------------------------------------------------------------------ + ... + ------------------------------------------------------------------------------ + All done. Exiting PyROS. + ============================================================================== + >>> # === Compare final objective to the single-stage solution + >>> two_stage_final_objective = round( + ... pyo.value(results_2.final_objective_value), + ... -1, + ... ) + >>> percent_difference = 100 * ( + ... two_stage_final_objective - single_stage_final_objective + ... ) / (single_stage_final_objective) + >>> print("Percent objective change relative to constant decision rules " + ... f"objective: {percent_difference:.2f}") + Percent objective change relative to constant decision rules objective: -24... + +For this example, we notice a ~25% decrease in the final objective +value when switching from a static decision rule (no second-stage recourse) +to an affine decision rule. + + +Specifying Arguments Indirectly Through ``options`` +""""""""""""""""""""""""""""""""""""""""""""""""""" +Like other Pyomo solver interface methods, +:meth:`~pyomo.contrib.pyros.PyROS.solve` +provides support for specifying options indirectly by passing +a keyword argument ``options``, whose value must be a :class:`dict` +mapping names of arguments to :meth:`~pyomo.contrib.pyros.PyROS.solve` +to their desired values. +For example, the ``solve()`` statement in the +:ref:`two-stage problem snippet ` +could have been equivalently written as: + +.. doctest:: + :skipif: not (baron.available() and baron.license_is_valid()) + + >>> results_2 = pyros_solver.solve( + ... model=m, + ... first_stage_variables=first_stage_variables, + ... second_stage_variables=second_stage_variables, + ... uncertain_params=uncertain_parameters, + ... uncertainty_set=box_uncertainty_set, + ... local_solver=local_solver, + ... global_solver=global_solver, + ... options={ + ... "objective_focus": pyros.ObjectiveType.worst_case, + ... "solve_master_globally": True, + ... "decision_rule_order": 1, + ... }, + ... ) + ============================================================================== + PyROS: The Pyomo Robust Optimization Solver... + ... + ------------------------------------------------------------------------------ + Robust optimal solution identified. + ------------------------------------------------------------------------------ + ... + ------------------------------------------------------------------------------ + All done. Exiting PyROS. + ============================================================================== + +In the event an argument is passed directly +by position or keyword, *and* indirectly through ``options``, +an appropriate warning is issued, +and the value passed directly takes precedence over the value +passed through ``options``. + + +The Price of Robustness +"""""""""""""""""""""""" +In conjunction with standard Python control flow tools, +PyROS facilitates a "price of robustness" analysis for a model of interest +through the evaluation and comparison of the robust optimal +objective function value across any appropriately constructed hierarchy +of uncertainty sets. +In this example, we consider a sequence of +box uncertainty sets centered on the nominal uncertain +parameter realization, such that each box is parameterized +by a real value specifying a relative box size. +To this end, we construct an iterable called ``relative_deviation_list`` +whose entries are ``float`` values representing the relative sizes. +We then loop through ``relative_deviation_list`` so that for each relative +size, the corresponding robust optimal objective value +can be evaluated by creating an appropriate +:class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` +instance and invoking the PyROS solver: + +.. code:: + + >>> # This takes a long time to run and therefore is not a doctest + >>> # === An array of maximum relative deviations from the nominal uncertain + >>> # parameter values to utilize in constructing box sets + >>> relative_deviation_list = [0.00, 0.10, 0.20, 0.30, 0.40] + >>> # === Final robust optimal objectives + >>> robust_optimal_objectives = [] + >>> for relative_deviation in relative_deviation_list: # doctest: +SKIP + ... bounds = [ + ... (nominal_values[i] - relative_deviation*nominal_values[i], + ... nominal_values[i] + relative_deviation*nominal_values[i]) + ... for i in range(4) + ... ] + ... box_uncertainty_set = pyros.BoxSet(bounds = bounds) + ... results = pyros_solver.solve( + ... model=m, + ... first_stage_variables=first_stage_variables, + ... second_stage_variables=second_stage_variables, + ... uncertain_params=uncertain_parameters, + ... uncertainty_set= box_uncertainty_set, + ... local_solver=local_solver, + ... global_solver=global_solver, + ... objective_focus=pyros.ObjectiveType.worst_case, + ... solve_master_globally=True, + ... decision_rule_order=1, + ... ) + ... is_robust_optimal = ( + ... results.pyros_termination_condition + ... == pyros.pyrosTerminationCondition.robust_optimal + ... ) + ... if not is_robust_optimal: + ... print(f"Instance for relative deviation: {relative_deviation} " + ... "not solved to robust optimality.") + ... robust_optimal_objectives.append("-----") + ... else: + ... robust_optimal_objectives.append(str(results.final_objective_value)) + +For this example, we obtain the following price of robustness results: + +.. table:: Price of robustness results. + + +------------------------------------------+------------------------------+-----------------------------+ + | Uncertainty Set Size (+/-) :sup:`o` | Robust Optimal Objective | % Increase :sup:`x` | + +==========================================+==============================+=============================+ + | 0.00 | 35,837,659.18 | 0.00 % | + +------------------------------------------+------------------------------+-----------------------------+ + | 0.10 | 36,135,182.66 | 0.83 % | + +------------------------------------------+------------------------------+-----------------------------+ + | 0.20 | 36,437,979.81 | 1.68 % | + +------------------------------------------+------------------------------+-----------------------------+ + | 0.30 | 43,478,190.91 | 21.32 % | + +------------------------------------------+------------------------------+-----------------------------+ + | 0.40 | ``robust_infeasible`` | :math:`\text{-----}` | + +------------------------------------------+------------------------------+-----------------------------+ + +Notice that PyROS was successfully able to determine the robust +infeasibility of the problem under the largest uncertainty set. + +:sup:`o` **Relative Deviation from Nominal Realization** + +:sup:`x` **Relative to Deterministic Optimal Objective** + +This example clearly illustrates the potential impact of the uncertainty +set size on the robust optimal objective function value +and demonstrates the ease of implementing a price of robustness study +for a given optimization problem under uncertainty. + +PyROS Solver Log Output +------------------------------- + +The PyROS solver log output is controlled through the optional +``progress_logger`` argument, itself cast to +a standard Python logger (:py:class:`logging.Logger`) object +at the outset of a :meth:`~pyomo.contrib.pyros.PyROS.solve` call. +The level of detail of the solver log output +can be adjusted by adjusting the level of the +logger object; see :ref:`the following table `. +Note that by default, ``progress_logger`` is cast to a logger of level +:py:obj:`logging.INFO`. + +We refer the reader to the +:doc:`official Python logging library documentation ` +for customization of Python logger objects; +for a basic tutorial, see the :doc:`logging HOWTO `. + +.. _table-logging-levels: + +.. list-table:: PyROS solver log output at the various standard Python :py:mod:`logging` levels. + :widths: 10 50 + :header-rows: 1 + + * - Logging Level + - Output Messages + * - :py:obj:`logging.ERROR` + - * Information on the subproblem for which an exception was raised + by a subordinate solver + * Details about failure of the PyROS coefficient matching routine + * - :py:obj:`logging.WARNING` + - * Information about a subproblem not solved to an acceptable status + by the user-provided subordinate optimizers + * Invocation of a backup solver for a particular subproblem + * Caution about solution robustness guarantees in event that + user passes ``bypass_global_separation=True`` + * - :py:obj:`logging.INFO` + - * PyROS version, author, and disclaimer information + * Summary of user options + * Breakdown of model component statistics + * Iteration log table + * Termination details: message, timing breakdown, summary of statistics + * - :py:obj:`logging.DEBUG` + - * Termination outcomes and summary of statistics for + every master feasility, master, and DR polishing problem + * Progress updates for the separation procedure + * Separation subproblem initial point infeasibilities + * Summary of separation loop outcomes: performance constraints + violated, uncertain parameter scenario added to the + master problem + * Uncertain parameter scenarios added to the master problem + thus far + +An example of an output log produced through the default PyROS +progress logger is shown in +:ref:`the snippet that follows `. +Observe that the log contains the following information: + + +* **Introductory information** (lines 1--18). + Includes the version number, author + information, (UTC) time at which the solver was invoked, + and, if available, information on the local Git branch and + commit hash. +* **Summary of solver options** (lines 19--38). +* **Preprocessing information** (lines 39--41). + Wall time required for preprocessing + the deterministic model and associated components, + i.e. standardizing model components and adding the decision rule + variables and equations. +* **Model component statistics** (lines 42--58). + Breakdown of model component statistics. + Includes components added by PyROS, such as the decision rule variables + and equations. +* **Iteration log table** (lines 59--69). + Summary information on the problem iterates and subproblem outcomes. + The constituent columns are defined in detail in + :ref:`the table following the snippet `. +* **Termination message** (lines 70--71). Very brief summary of the termination outcome. +* **Timing statistics** (lines 72--88). + Tabulated breakdown of the solver timing statistics, based on a + :class:`pyomo.common.timing.HierarchicalTimer` printout. + The identifiers are as follows: + + * ``main``: Total time elapsed by the solver. + * ``main.dr_polishing``: Total time elapsed by the subordinate solvers + on polishing of the decision rules. + * ``main.global_separation``: Total time elapsed by the subordinate solvers + on global separation subproblems. + * ``main.local_separation``: Total time elapsed by the subordinate solvers + on local separation subproblems. + * ``main.master``: Total time elapsed by the subordinate solvers on + the master problems. + * ``main.master_feasibility``: Total time elapsed by the subordinate solvers + on the master feasibility problems. + * ``main.preprocessing``: Total preprocessing time. + * ``main.other``: Total overhead time. + +* **Termination statistics** (lines 89--94). Summary of statistics related to the + iterate at which PyROS terminates. +* **Exit message** (lines 95--96). + + +.. _solver-log-snippet: + +.. code-block:: text + :caption: PyROS solver output log for the :ref:`two-stage problem example `. + :linenos: + + ============================================================================== + PyROS: The Pyomo Robust Optimization Solver, v1.2.11. + Pyomo version: 6.7.2 + Commit hash: unknown + Invoked at UTC 2024-03-28T00:00:00.000000 + + Developed by: Natalie M. Isenberg (1), Jason A. F. Sherman (1), + John D. Siirola (2), Chrysanthos E. Gounaris (1) + (1) Carnegie Mellon University, Department of Chemical Engineering + (2) Sandia National Laboratories, Center for Computing Research + + The developers gratefully acknowledge support from the U.S. Department + of Energy's Institute for the Design of Advanced Energy Systems (IDAES). + ============================================================================== + ================================= DISCLAIMER ================================= + PyROS is still under development. + Please provide feedback and/or report any issues by creating a ticket at + https://github.com/Pyomo/pyomo/issues/new/choose + ============================================================================== + Solver options: + time_limit=None + keepfiles=False + tee=False + load_solution=True + symbolic_solver_labels=False + objective_focus= + nominal_uncertain_param_vals=[0.13248000000000001, 4.97, 4.97, 1800] + decision_rule_order=1 + solve_master_globally=True + max_iter=-1 + robust_feasibility_tolerance=0.0001 + separation_priority_order={} + progress_logger= + backup_local_solvers=[] + backup_global_solvers=[] + subproblem_file_directory=None + bypass_local_separation=False + bypass_global_separation=False + p_robustness={} + ------------------------------------------------------------------------------ + Preprocessing... + Done preprocessing; required wall time of 0.175s. + ------------------------------------------------------------------------------ + Model statistics: + Number of variables : 62 + Epigraph variable : 1 + First-stage variables : 7 + Second-stage variables : 6 + State variables : 18 + Decision rule variables : 30 + Number of uncertain parameters : 4 + Number of constraints : 81 + Equality constraints : 24 + Coefficient matching constraints : 0 + Decision rule equations : 6 + All other equality constraints : 18 + Inequality constraints : 57 + First-stage inequalities (incl. certain var bounds) : 10 + Performance constraints (incl. var bounds) : 47 + ------------------------------------------------------------------------------ + Itn Objective 1-Stg Shift 2-Stg Shift #CViol Max Viol Wall Time (s) + ------------------------------------------------------------------------------ + 0 3.5838e+07 - - 5 1.8832e+04 1.741 + 1 3.5838e+07 3.5184e-15 3.9404e-15 10 4.2516e+06 3.766 + 2 3.5993e+07 1.8105e-01 7.1406e-01 13 5.2004e+06 6.288 + 3 3.6285e+07 5.1968e-01 7.7753e-01 4 1.7892e+04 8.247 + 4 3.6285e+07 9.1166e-13 1.9702e-15 0 7.1157e-10g 11.456 + ------------------------------------------------------------------------------ + Robust optimal solution identified. + ------------------------------------------------------------------------------ + Timing breakdown: + + Identifier ncalls cumtime percall % + ----------------------------------------------------------- + main 1 11.457 11.457 100.0 + ------------------------------------------------------ + dr_polishing 4 0.682 0.171 6.0 + global_separation 47 1.109 0.024 9.7 + local_separation 235 5.810 0.025 50.7 + master 5 1.353 0.271 11.8 + master_feasibility 4 0.247 0.062 2.2 + preprocessing 1 0.429 0.429 3.7 + other n/a 1.828 n/a 16.0 + ====================================================== + =========================================================== + + ------------------------------------------------------------------------------ + Termination stats: + Iterations : 5 + Solve time (wall s) : 11.457 + Final objective value : 3.6285e+07 + Termination condition : pyrosTerminationCondition.robust_optimal + ------------------------------------------------------------------------------ + All done. Exiting PyROS. + ============================================================================== + + +The iteration log table is designed to provide, in a concise manner, +important information about the progress of the iterative algorithm for +the problem of interest. +The constituent columns are defined in the +:ref:`table that follows `. + +.. _table-iteration-log-columns: + +.. list-table:: PyROS iteration log table columns. + :widths: 10 50 + :header-rows: 1 + + * - Column Name + - Definition + * - Itn + - Iteration number. + * - Objective + - Master solution objective function value. + If the objective of the deterministic model provided + has a maximization sense, + then the negative of the objective function value is displayed. + Expect this value to trend upward as the iteration number + increases. + If the master problems are solved globally + (by passing ``solve_master_globally=True``), + then after the iteration number exceeds the number of uncertain parameters, + this value should be monotonically nondecreasing + as the iteration number is increased. + A dash ("-") is produced in lieu of a value if the master + problem of the current iteration is not solved successfully. + * - 1-Stg Shift + - Infinity norm of the relative difference between the first-stage + variable vectors of the master solutions of the current + and previous iterations. Expect this value to trend + downward as the iteration number increases. + A dash ("-") is produced in lieu of a value + if the current iteration number is 0, + there are no first-stage variables, + or the master problem of the current iteration is not solved successfully. + * - 2-Stg Shift + - Infinity norm of the relative difference between the second-stage + variable vectors (evaluated subject to the nominal uncertain + parameter realization) of the master solutions of the current + and previous iterations. Expect this value to trend + downward as the iteration number increases. + A dash ("-") is produced in lieu of a value + if the current iteration number is 0, + there are no second-stage variables, + or the master problem of the current iteration is not solved successfully. + * - #CViol + - Number of performance constraints found to be violated during + the separation step of the current iteration. + Unless a custom prioritization of the model's performance constraints + is specified (through the ``separation_priority_order`` argument), + expect this number to trend downward as the iteration number increases. + A "+" is appended if not all of the separation problems + were solved successfully, either due to custom prioritization, a time out, + or an issue encountered by the subordinate optimizers. + A dash ("-") is produced in lieu of a value if the separation + routine is not invoked during the current iteration. + * - Max Viol + - Maximum scaled performance constraint violation. + Expect this value to trend downward as the iteration number increases. + A 'g' is appended to the value if the separation problems were solved + globally during the current iteration. + A dash ("-") is produced in lieu of a value if the separation + routine is not invoked during the current iteration, or if there are + no performance constraints. + * - Wall time (s) + - Total time elapsed by the solver, in seconds, up to the end of the + current iteration. + + +Feedback and Reporting Issues +------------------------------- +Please provide feedback and/or report any problems by opening an issue on +the `Pyomo GitHub page `_. diff --git a/doc/OnlineDocs/user_guide/contributed_packages/satsolver.rst b/doc/OnlineDocs/user_guide/contributed_packages/satsolver.rst new file mode 100644 index 00000000000..4cac9170b55 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/satsolver.rst @@ -0,0 +1,34 @@ +z3 SMT Sat Solver Interface +=========================== + +The z3 Satisfiability Solver interface can convert pyomo variables and expressions for +use with the z3 Satisfiability Solver + +Installation +------------ +z3 is required for use of the Sat Solver can be installed via the command + +.. code:: + + pip install z3-solver + +Using z3 Sat Solver +------------------- +To use the sat solver define your pyomo model as usual: + +.. doctest:: + + Required import + >>> from pyomo.environ import * + >>> from pyomo.contrib.satsolver.satsolver import SMTSatSolver + + Create a simple model + >>> m = ConcreteModel() + >>> m.x = Var() + >>> m.y = Var() + >>> m.obj = Objective(expr=m.x**2 + m.y**2) + >>> m.c = Constraint(expr=m.y >= -2*m.x + 5) + + Invoke the sat solver using optional argument model to automatically process + pyomo model + >>> is_feasible = SMTSatSolver(model = m).check()# doctest: +SKIP diff --git a/doc/OnlineDocs/user_guide/contributed_packages/sensitivity_toolbox.rst b/doc/OnlineDocs/user_guide/contributed_packages/sensitivity_toolbox.rst new file mode 100644 index 00000000000..2a2ccff4b09 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/sensitivity_toolbox.rst @@ -0,0 +1,185 @@ +Sensitivity Toolbox +=================== + +The sensitivity toolbox provides a Pyomo interface to sIPOPT and k_aug to very quickly compute approximate solutions to nonlinear programs with a small perturbation in model parameters. + +See the `sIPOPT documentation `_ or the `following paper `_ for additional details: + + H. Pirnay, R. Lopez-Negrete, and L.T. Biegler, Optimal Sensitivity based on IPOPT, Math. Prog. Comp., 4(4):307--331, 2012. + +The details of `k_aug` can be found in the following link: + + David Thierry (2020). k_aug, https://github.com/dthierry/k_aug + +Using the Sensitivity Toolbox +----------------------------- + +We will start with a motivating example: + +.. math:: + \begin{align*} + \min_{x_1,x_2,x_3} \quad & x_1^2 + x_2^2 + x_3^2 \\ + \mathrm{s.t.} \qquad & 6 x_1 + 3 x_2 + 2 x_3 - p_1 = 0 \\ + & p_2 x_1 + x_2 - x_3 - 1 = 0 \\ + & x_1, x_2, x_3 \geq 0 + \end{align*} + +Here :math:`x_1`, :math:`x_2`, and :math:`x_3` are the decision variables while :math:`p_1` and :math:`p_2` are parameters. At first, let's consider :math:`p_1 = 4.5` and :math:`p_2 = 1.0`. Below is the model implemented in Pyomo. + +.. doctest:: python + + # Import Pyomo and the sensitivity toolbox + >>> from pyomo.environ import * + >>> from pyomo.contrib.sensitivity_toolbox.sens import sensitivity_calculation + + # Create a concrete model + >>> m = ConcreteModel() + + # Define the variables with bounds and initial values + >>> m.x1 = Var(initialize = 0.15, within=NonNegativeReals) + >>> m.x2 = Var(initialize = 0.15, within=NonNegativeReals) + >>> m.x3 = Var(initialize = 0.0, within=NonNegativeReals) + + # Define the parameters + >>> m.eta1 = Param(initialize=4.5,mutable=True) + >>> m.eta2 = Param(initialize=1.0,mutable=True) + + # Define the constraints and objective + >>> m.const1 = Constraint(expr=6*m.x1+3*m.x2+2*m.x3-m.eta1 ==0) + >>> m.const2 = Constraint(expr=m.eta2*m.x1+m.x2-m.x3-1 ==0) + >>> m.cost = Objective(expr=m.x1**2+m.x2**2+m.x3**2) + + +The solution of this optimization problem is :math:`x_1^* = 0.5`, :math:`x_2^* = 0.5`, and :math:`x_3^* = 0.0`. But what if we change the parameter values to :math:`\hat{p}_1 = 4.0` and :math:`\hat{p}_2 = 1.0`? Is there a quick way to approximate the new solution :math:`\hat{x}_1^*`, :math:`\hat{x}_2^*`, and :math:`\hat{x}_3^*`? Yes! This is the main functionality of sIPOPT and k_aug. + +Next we define the perturbed parameter values :math:`\hat{p}_1` and :math:`\hat{p}_2`: + +.. doctest:: python + + >>> m.perturbed_eta1 = Param(initialize = 4.0) + >>> m.perturbed_eta2 = Param(initialize = 1.0) + +And finally we call sIPOPT or k_aug: + +.. doctest:: python + :skipif: not sipopt_available or not k_aug_available or not dot_sens_available + + >>> m_sipopt = sensitivity_calculation('sipopt', m, [m.eta1, m.eta2], [m.perturbed_eta1, m.perturbed_eta2], tee=False) + >>> m_kaug_dsdp = sensitivity_calculation('k_aug', m, [m.eta1, m.eta2], [m.perturbed_eta1, m.perturbed_eta2], tee=False) + +The first argument specifies the method, either 'sipopt' or 'k_aug'. The second argument is the Pyomo model. The third argument is a list of the original parameters. The fourth argument is a list of the perturbed parameters. It's important that these two lists are the same length and in the same order. + +First, we can inspect the initial point: + +.. doctest:: python + :skipif: not sipopt_available or not k_aug_available or not dot_sens_available + + >>> print("eta1 = %0.3f" % m.eta1()) + eta1 = 4.500 + + >>> print("eta2 = %0.3f" % m.eta2()) + eta2 = 1.000 + + # Initial point (not feasible): + >>> print("Objective = %0.3f" % m.cost()) + Objective = 0.045 + + >>> print("x1 = %0.3f" % m.x1()) + x1 = 0.150 + + >>> print("x2 = %0.3f" % m.x2()) + x2 = 0.150 + + >>> print("x3 = %0.3f" % m.x3()) + x3 = 0.000 + +Next, we inspect the solution :math:`x_1^*`, :math:`x_2^*`, and :math:`x_3^*`: + +.. doctest:: python + :skipif: not sipopt_available or not k_aug_available or not dot_sens_available + + # Solution with the original parameter values: + >>> print("Objective = %0.3f" % m_sipopt.cost()) + Objective = 0.500 + + >>> print("x1 = %0.3f" % m_sipopt.x1()) + x1 = 0.500 + + >>> print("x2 = %0.3f" % m_sipopt.x2()) + x2 = 0.500 + + >>> print("x3 = %0.3f" % m_sipopt.x3()) + x3 = 0.000 + +Note that k_aug does not save the solution with the original parameter values. Finally, we inspect the approximate solution :math:`\hat{x}_1^*`, :math:`\hat{x}_2^*`, and :math:`\hat{x}_3^*`: + +.. doctest:: python + :skipif: not sipopt_available or not k_aug_available or not dot_sens_available + + # *sIPOPT* + # New parameter values: + >>> print("eta1 = %0.3f" %m_sipopt.perturbed_eta1()) + eta1 = 4.000 + + >>> print("eta2 = %0.3f" % m_sipopt.perturbed_eta2()) + eta2 = 1.000 + + # (Approximate) solution with the new parameter values: + >>> x1 = m_sipopt.sens_sol_state_1[m_sipopt.x1] + >>> x2 = m_sipopt.sens_sol_state_1[m_sipopt.x2] + >>> x3 = m_sipopt.sens_sol_state_1[m_sipopt.x3] + >>> print("Objective = %0.3f" % (x1**2 + x2**2 + x3**2)) + Objective = 0.556 + + >>> print("x1 = %0.3f" % x1) + x1 = 0.333 + + >>> print("x2 = %0.3f" % x2) + x2 = 0.667 + + >>> print("x3 = %0.3f" % x3) + x3 = -0.000 + + # *k_aug* + # New parameter values: + >>> print("eta1 = %0.3f" %m_kaug_dsdp.perturbed_eta1()) + eta1 = 4.000 + + >>> print("eta2 = %0.3f" % m_kaug_dsdp.perturbed_eta2()) + eta2 = 1.000 + + # (Approximate) solution with the new parameter values: + >>> x1 = m_kaug_dsdp.x1() + >>> x2 = m_kaug_dsdp.x2() + >>> x3 = m_kaug_dsdp.x3() + >>> print("Objective = %0.3f" % (x1**2 + x2**2 + x3**2)) + Objective = 0.556 + + >>> print("x1 = %0.3f" % x1) + x1 = 0.333 + + >>> print("x2 = %0.3f" % x2) + x2 = 0.667 + + >>> print("x3 = %0.3f" % x3) + x3 = -0.000 + + +Installing sIPOPT and k_aug +--------------------------- + +The sensitivity toolbox requires either sIPOPT or k_aug to be installed and available in your system PATH. See the sIPOPT and k_aug documentation for detailed instructions: + +* https://coin-or.github.io/Ipopt/INSTALL.html +* https://projects.coin-or.org/Ipopt/wiki/sIpopt +* https://coin-or.github.io/coinbrew/ +* https://github.com/dthierry/k_aug + +.. note:: + If you get an error that ``ipopt_sens`` or ``k_aug`` and ``dot_sens`` cannot be found, double check your installation and make sure the build directories containing the executables were added to your system PATH. + + +Sensitivity Toolbox Interface +----------------------------- + +.. autofunction:: pyomo.contrib.sensitivity_toolbox.sens.sensitivity_calculation diff --git a/doc/OnlineDocs/user_guide/contributed_packages/trustregion.rst b/doc/OnlineDocs/user_guide/contributed_packages/trustregion.rst new file mode 100644 index 00000000000..f477c905e33 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/trustregion.rst @@ -0,0 +1,189 @@ +#################################### +Trust Region Framework Method Solver +#################################### + +The Trust Region Framework (TRF) method solver allows users to solve hybrid +glass box/black box optimization problems in which parts of the system are +modeled with open, equation-based models and parts of the system are black +boxes. This method utilizes surrogate models that substitute high-fidelity +models with low-fidelity basis functions, thus avoiding the direct implementation +of the large, computationally expensive high-fidelity models. This is done +iteratively, resulting in fewer calls to the computationally expensive functions. + +This module implements the method from Yoshio & Biegler +[`Yoshio & Biegler, 2021`_] and represents a rewrite of the original 2018 +implementation of the algorithm from Eason & Biegler [`Eason & Biegler, 2018`_]. + +In the context of this updated module, black box functions are implemented as +Pyomo External Functions. + +This work was conducted as part of the Institute for the Design of Advanced +Energy Systems (`IDAES `_) with support through the +Simulation-Based Engineering, Crosscutting Research Program within the U.S. +Department of Energy’s Office of Fossil Energy and Carbon Management. + +.. _Eason & Biegler, 2018: https://doi.org/10.1002/aic.16364 +.. _Yoshio & Biegler, 2021: https://doi.org/10.1002/aic.17054 + +Methodology Overview +--------------------- + +The formulation of the original hybrid problem is: + +.. math:: + \begin{align*} + \displaystyle \min_{} & ~~ f\left(z, w, d\left(w\right)\right) & \\ + \displaystyle \text{s.t.} \quad \: & ~~ h\left(z, w, d\left(w\right)\right) = 0 \\ + \displaystyle & ~~ g\left(z, w, d\left(w\right)\right) \leq 0 + \end{align*} + +where: + +* :math:`w \in \mathbb{R}^m` are the inputs to the external functions +* :math:`z \in \mathbb{R}^n` are the remaining decision variables (i.e., degrees of freedom) +* :math:`d(w) : \mathbb{R}^m \to \mathbb{R}^p` are the outputs of the external functions as a function of :math:`w` +* :math:`f`, `h`, `g`, `d` are all assumed to be twice continuously differentiable + +This formulation is reworked to separate all external function information as +follows to enable the usage of the trust region method: + +.. math:: + \begin{align*} + \displaystyle \min_{x} & ~~ f\left(x\right) & \\ + \displaystyle \text{s.t.} \quad \: & ~~ h\left(x\right) = 0 \\ + \displaystyle & ~~ g\left(x\right) \leq 0 \\ + \displaystyle & ~~ y = d\left(w\right) + \end{align*} + +where: + +* :math:`y \in \mathbb{R}^p` are the outputs of the external functions +* :math:`x^T = [w^T, y^T, z^T]` is a set of all inputs and outputs + +Using this formulation and a user-supplied low-fidelity/ideal model basis function +:math:`b\left(w\right)`, the algorithm iteratively solves subproblems using +the surrogate model: + +.. math:: + \begin{align*} + r_k\left(w\right) = b\left(w\right) + \left( d\left(w_k\right) - b\left(w_k\right) \right) + \left( \nabla d\left(w_k\right) - \nabla b\left(w_k\right) \right)^T \left( w - w_k \right) + \end{align*} + +This acts similarly to Newton's method in that small, incremental steps are taken +towards an optimal solution. At each iteration, the current solution of the +subproblem is compared to the previous solution to ensure that +the iteration has moved in a direction towards an optimal solution. If not true, +the step is rejected. If true, the step is accepted and the surrogate +model is updated for the next iteration. + +When using TRF, please consider citing the above papers. + +TRF Inputs +----------- + +The required inputs to the TRF +:py:meth:`solve ` +method are the following: + +* The optimization model +* List of degree of freedom variables within the model + +The optional input to the TRF +:py:meth:`solve ` +method is the following: + +* The external function surrogate model rule ("basis function") + + +TRF Solver Interface +--------------------- + +.. note:: + The keyword arguments can be updated at solver instantiation or later when the ``solve`` method is called. + +.. autoclass:: pyomo.contrib.trustregion.TRF.TrustRegionSolver + :members: solve + +TRF Usage Example +------------------ +Two examples can be found in the examples_ subdirectory. One of them is +implemented below. + +.. _examples: https://github.com/Pyomo/pyomo/tree/main/pyomo/contrib/trustregion/examples + +Step 0: Import Pyomo +^^^^^^^^^^^^^^^^^^^^^ + +.. doctest:: + + >>> # === Required imports === + >>> import pyomo.environ as pyo + +Step 1: Define the external function and its gradient +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. doctest:: + + >>> # === Define a 'black box' function and its gradient === + >>> def ext_fcn(a, b): + ... return pyo.sin(a - b) + >>> def grad_ext_fcn(args, fixed): + ... a, b = args[:2] + ... return [ pyo.cos(a - b), -pyo.cos(a - b) ] + +Step 2: Create the model +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. doctest:: + + >>> # === Construct the Pyomo model object === + >>> def create_model(): + ... m = pyo.ConcreteModel() + ... m.name = 'Example 1: Eason' + ... m.z = pyo.Var(range(3), domain=pyo.Reals, initialize=2.) + ... m.x = pyo.Var(range(2), initialize=2.) + ... m.x[1] = 1.0 + ... + ... m.ext_fcn = pyo.ExternalFunction(ext_fcn, grad_ext_fcn) + ... + ... m.obj = pyo.Objective( + ... expr=(m.z[0]-1.0)**2 + (m.z[0]-m.z[1])**2 + (m.z[2]-1.0)**2 \ + ... + (m.x[0]-1.0)**4 + (m.x[1]-1.0)**6 + ... ) + ... + ... m.c1 = pyo.Constraint( + ... expr=m.x[0] * m.z[0]**2 + m.ext_fcn(m.x[0], m.x[1]) == 2*pyo.sqrt(2.0) + ... ) + ... m.c2 = pyo.Constraint(expr=m.z[2]**4 * m.z[1]**2 + m.z[1] == 8+pyo.sqrt(2.0)) + ... return m + >>> model = create_model() + +Step 3: Solve with TRF +^^^^^^^^^^^^^^^^^^^^^^^ + +.. note:: + Reminder from earlier that the ``solve`` method requires the user pass the model and a list of variables + which represent the degrees of freedom in the model. The user may also pass + a low-fidelity/ideal model (or "basis function") to this method to improve + convergence. + +.. doctest:: + :skipif: not ipopt_available + + >>> # === Instantiate the TRF solver object === + >>> trf_solver = pyo.SolverFactory('trustregion') + >>> # === Solve with TRF === + >>> result = trf_solver.solve(model, [model.z[0], model.z[1], model.z[2]]) + EXIT: Optimal solution found. + ... + +The :py:meth:`solve ` +method returns a clone of the original model which has been run +through TRF algorithm, thus leaving the original model intact. + + +.. warning:: + + TRF is still under a beta release. Please provide feedback and/or + report any problems by opening an issue on the Pyomo + `GitHub page `_. diff --git a/doc/OnlineDocs/user_guide/errors.rst b/doc/OnlineDocs/user_guide/errors.rst new file mode 100644 index 00000000000..162c2e10257 --- /dev/null +++ b/doc/OnlineDocs/user_guide/errors.rst @@ -0,0 +1,192 @@ +Common Warnings/Errors +====================== + +.. + NOTE to developers: as we use section links to direct users, it is + critical that the "IDs" are unique. When adding a new extended + warning / error description, DO NOT renumber existing entries. Also, + for backwards compatibility, DO NOT recycle old ID (no longer used) + numbers. + +.. doctest:: + :hide: + + >>> import pyomo.environ as pyo + +.. py:currentmodule:: pyomo.environ + + +.. =================================================================== +.. Extended descriptions for Pyomo warnings +.. =================================================================== + +Warnings +-------- + +.. _W1001: + +W1001: Setting Var value not in domain +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When setting :class:`Var` values (by either calling :meth:`Var.set_value()` +or setting the :attr:`value` attribute), Pyomo will validate the +incoming value by checking that the value is ``in`` the +:attr:`Var.domain`. Any values not in the domain will generate this +warning: + +.. doctest:: + + >>> m = pyo.ConcreteModel() + >>> m.x = pyo.Var(domain=pyo.Integers) + >>> m.x = 0.5 + WARNING (W1001): Setting Var 'x' to a value `0.5` (float) not in domain + Integers. + See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001 + >>> print(m.x.value) + 0.5 + + +Users can bypass all domain validation by setting the value using: + +.. doctest:: + + >>> m.x.set_value(0.75, skip_validation=True) + >>> print(m.x.value) + 0.75 + + + +.. _W1002: + +W1002: Setting Var value outside the bounds +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When setting :py:class:`Var` values (by either calling :meth:`set_value()` +or setting the :attr:`value` attribute), Pyomo will validate the +incoming value by checking that the value is within the range specified by +:attr:`Var.bounds`. Any values outside the bounds will generate this +warning: + +.. doctest:: + + >>> m = pyo.ConcreteModel() + >>> m.x = pyo.Var(domain=pyo.Integers, bounds=(1, 5)) + >>> m.x = 0 + WARNING (W1002): Setting Var 'x' to a numeric value `0` outside the bounds + (1, 5). + See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002 + >>> print(m.x.value) + 0 + +Users can bypass all domain validation by setting the value using: + +.. doctest:: + + >>> m.x.set_value(10, skip_validation=True) + >>> print(m.x.value) + 10 + + + +.. _W1003: + +W1003: Unexpected RecursionError walking an expression tree +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pyomo leverages a recursive walker (the +:py:class:`~pyomo.core.expr.visitor.StreamBasedExpressionVisitor`) to +traverse (walk) expression trees. For most expressions, this recursive +walker is the most efficient. However, Python has a relatively shallow +recursion limit (generally, 1000 frames). The recursive walker is +designed to monitor the stack depth and cleanly switch to a nonrecursive +walker before hitting the stack limit. However, there are two (rare) +cases where the Python stack limit can still generate a +:py:exc:`RecursionError` exception: + +#. Starting the walker with fewer than + :py:data:`pyomo.core.expr.visitor.RECURSION_LIMIT` available frames. +#. Callbacks that require more than 2 * + :py:data:`pyomo.core.expr.visitor.RECURSION_LIMIT` frames. + +The (default) recursive walker will catch the exception and restart the +walker from the beginning in non-recursive mode, issuing this warning. +The caution is that any partial work done by the walker before the +exception was raised will be lost, potentially leaving the walker in an +inconsistent state. Users can avoid this by + +- avoiding recursive callbacks +- restructuring the system design to avoid triggering the walker with + few available stack frames +- directly calling the + :py:meth:`~pyomo.core.expr.visitor.StreamBasedExpressionVisitor.walk_expression_nonrecursive()` + walker method + +.. doctest:: + :skipif: (on_github_actions and system_info[0].startswith('win')) \ + or system_info[2] == 'PyPy' + + >>> import sys + >>> import pyomo.core.expr.visitor as visitor + >>> from pyomo.core.tests.unit.test_visitor import fill_stack + >>> expression_depth = visitor.StreamBasedExpressionVisitor( + ... exitNode=lambda node, data: max(data) + 1 if data else 1) + >>> m = pyo.ConcreteModel() + >>> m.x = pyo.Var() + >>> @m.Expression(range(35)) + ... def e(m, i): + ... return m.e[i-1] if i else m.x + >>> expression_depth.walk_expression(m.e[34]) + 36 + >>> fill_stack(sys.getrecursionlimit() - visitor.get_stack_depth() - 30, + ... expression_depth.walk_expression, + ... m.e[34]) + WARNING (W1003): Unexpected RecursionError walking an expression tree. + See also https://pyomo.readthedocs.io/en/stable/errors.html#w1003 + 36 + >>> fill_stack(sys.getrecursionlimit() - visitor.get_stack_depth() - 30, + ... expression_depth.walk_expression_nonrecursive, + ... m.e[34]) + 36 + + +.. =================================================================== +.. Extended descriptions for Pyomo errors +.. =================================================================== + +Errors +------ + +.. _E2001: + +E2001: Variable domains must be an instance of a Pyomo Set +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Variable domains are always Pyomo :class:`Set` or :class:`RangeSet` +objects. This includes global sets like ``Reals``, ``Integers``, +``Binary``, ``NonNegativeReals``, etc., as well as model-specific +:class:`Set` instances. The :attr:`Var.domain` setter will attempt to +convert assigned values to a Pyomo `Set`, with any failures leading to +this warning (and an exception from the converter): + +.. doctest:: + + >>> m = pyo.ConcreteModel() + >>> m.x = pyo.Var() + >>> m.x.domain = 5 + Traceback (most recent call last): + ... + TypeError: Cannot create a Set from data that does not support __contains__... + ERROR (E2001): 5 is not a valid domain. Variable domains must be an instance + of a Pyomo Set or convertible to a Pyomo Set. + See also https://pyomo.readthedocs.io/en/stable/errors.html#e2001 + + + +.. =================================================================== +.. Extended descriptions for Pyomo exceptions +.. =================================================================== + +.. Exceptions +.. ---------- + +.. .. _X101: diff --git a/doc/OnlineDocs/user_guide/external_tutorials.rst b/doc/OnlineDocs/user_guide/external_tutorials.rst new file mode 100644 index 00000000000..a18f9d77d42 --- /dev/null +++ b/doc/OnlineDocs/user_guide/external_tutorials.rst @@ -0,0 +1,20 @@ +Pyomo Tutorial Examples +======================= + +Additional Pyomo tutorials and examples can be found at the following links: + +* `Pyomo — Optimization Modeling in Python + `_ ([PyomoBookIII]_) + +* `Pyomo Workshop Slides and Exercises + `_ + +* `Prof. Jeffrey Kantor's Pyomo Cookbook + `_ + +* The `companion notebooks `_ + for *Hands-On Mathematical Optimization with Python* + +* `Pyomo Gallery `_ + + diff --git a/doc/OnlineDocs/user_guide/flattener/index.rst b/doc/OnlineDocs/user_guide/flattener/index.rst new file mode 100644 index 00000000000..f9dd8ea6abb --- /dev/null +++ b/doc/OnlineDocs/user_guide/flattener/index.rst @@ -0,0 +1,65 @@ +"Flattening" a Pyomo model +========================== + +.. autosummary:: + + pyomo.dae.flatten + +.. toctree:: + :maxdepth: 1 + + motivation.rst + reference.rst + +What does it mean to flatten a model? +------------------------------------- +When accessing components in a block-structured model, we use +``component_objects`` or ``component_data_objects`` to access all objects +of a specific ``Component`` or ``ComponentData`` type. +The generated objects may be thought of as a "flattened" representation +of the model, as they may be accessed without any knowledge of the model's +block structure. +These methods are very useful, but it is still challenging to use them +to access specific components. +Specifically, we often want to access "all components indexed by some set," +or "all component data at a particular index of this set." +In addition, we often want to generate the components in a block that +is indexed by our particular set, as these components may be thought of as +"implicitly indexed" by this set. +The ``pyomo.dae.flatten`` module aims to address this use case by providing +utilities to generate all components indexed, explicitly or implicitly, by +user-provided sets. + +**When we say "flatten a model," we mean "recursively generate all components in +the model," where a component can be indexed only by user-specified indexing +sets (or is not indexed at all)**. + +Data structures +--------------- +The components returned are either ``ComponentData`` objects, for components +not indexed by any of the provided sets, or references-to-slices, for +components indexed, explicitly or implicitly, by the provided sets. +Slices are necessary as they can encode "implicit indexing" -- where a +component is contained in an indexed block. It is natural to return references +to these slices, so they may be accessed and manipulated like any other +component. + +Citation +-------- +If you use the ``pyomo.dae.flatten`` module in your research, we would appreciate +you citing the following paper, which gives more detail about the motivation for +and examples of using this functinoality. + +.. code-block:: bibtex + + @article{parker2023mpc, + title = {Model predictive control simulations with block-hierarchical differential-algebraic process models}, + journal = {Journal of Process Control}, + volume = {132}, + pages = {103113}, + year = {2023}, + issn = {0959-1524}, + doi = {https://doi.org/10.1016/j.jprocont.2023.103113}, + url = {https://www.sciencedirect.com/science/article/pii/S0959152423002007}, + author = {Robert B. Parker and Bethany L. Nicholson and John D. Siirola and Lorenz T. Biegler}, + } diff --git a/doc/OnlineDocs/user_guide/flattener/motivation.rst b/doc/OnlineDocs/user_guide/flattener/motivation.rst new file mode 100644 index 00000000000..046d888a215 --- /dev/null +++ b/doc/OnlineDocs/user_guide/flattener/motivation.rst @@ -0,0 +1,26 @@ +Motivation +========== + +The ``pyomo.dae.flatten`` module was originally developed to assist with +dynamic optimization. A very common operation in dynamic or multi-period +optimization is to initialize all time-indexed variables to their values +at a specific time point. However, for variables indexed by time and +arbitrary other indexing sets, this is difficult to do in a way that does +does not depend on the variable we are initializing. Things get worse +when we consider that a time index can exist on a parent block rather +than the component itself. + +By "reshaping" time-indexed variables in a model into references indexed +only by time, the ``flatten_dae_components`` function allows us to perform +operations that depend on knowledge of time indices without knowing +anything about the variables that we are operating on. + +This "flattened representation" of a model turns out to be useful for +dynamic optimization in a variety of other contexts. Examples include +constructing a tracking objective function and plotting results. +This representation is also useful in cases where we want to preserve +indexing along more than one set, as in PDE-constrained optimization. +The ``flatten_components_along_sets`` function allows partitioning +components while preserving multiple indexing sets. +In such a case, time and space-indexed data for a given variable is useful +for purposes such as initialization, visualization, and stability analysis. diff --git a/doc/OnlineDocs/user_guide/flattener/reference.rst b/doc/OnlineDocs/user_guide/flattener/reference.rst new file mode 100644 index 00000000000..22c7b67e1f6 --- /dev/null +++ b/doc/OnlineDocs/user_guide/flattener/reference.rst @@ -0,0 +1,14 @@ +API reference +============= + +.. autosummary:: + + pyomo.dae.flatten.slice_component_along_sets + pyomo.dae.flatten.flatten_components_along_sets + pyomo.dae.flatten.flatten_dae_components + +.. autofunction:: pyomo.dae.flatten.slice_component_along_sets + +.. autofunction:: pyomo.dae.flatten.flatten_components_along_sets + +.. autofunction:: pyomo.dae.flatten.flatten_dae_components diff --git a/doc/OnlineDocs/user_guide/index.rst b/doc/OnlineDocs/user_guide/index.rst new file mode 100644 index 00000000000..d2fa7377d39 --- /dev/null +++ b/doc/OnlineDocs/user_guide/index.rst @@ -0,0 +1,6 @@ +User Guide +========== + +:doc:`Common Warnings/Errors ` +:doc:`External Pyomo Tutorials ` + diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/__init__.py b/doc/OnlineDocs/user_guide/modeling_extensions/__init__.py new file mode 100644 index 00000000000..a4a626013c4 --- /dev/null +++ b/doc/OnlineDocs/user_guide/modeling_extensions/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/bilevel.rst b/doc/OnlineDocs/user_guide/modeling_extensions/bilevel.rst new file mode 100644 index 00000000000..5e9ee9b0a7c --- /dev/null +++ b/doc/OnlineDocs/user_guide/modeling_extensions/bilevel.rst @@ -0,0 +1,6 @@ +Bilevel Programming +=================== + +``pyomo.bilevel`` provides extensions supporting modeling of multi-level +optimization problems. + diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/dae.rst b/doc/OnlineDocs/user_guide/modeling_extensions/dae.rst new file mode 100644 index 00000000000..ff0fb75e610 --- /dev/null +++ b/doc/OnlineDocs/user_guide/modeling_extensions/dae.rst @@ -0,0 +1,933 @@ +Dynamic Optimization with pyomo.DAE +=================================== + +.. image:: /../logos/dae/Pyomo-DAE-150.png + :scale: 35% + :align: right + +The pyomo.DAE modeling extension [PyomoDAE]_ allows users to incorporate systems of +differential algebraic equations (DAE)s in a Pyomo model. The modeling +components in this extension are able to represent ordinary or partial +differential equations. The differential equations do not have to be +written in a particular format and the components are flexible enough to +represent higher-order derivatives or mixed partial +derivatives. Pyomo.DAE also includes model transformations which use +simultaneous discretization approaches to transform a DAE model into an +algebraic model. Finally, pyomo.DAE includes utilities for simulating +DAE models and initializing dynamic optimization problems. + + + +Modeling Components +------------------- + +.. (Replace these definitions with in-code documentation) + +Pyomo.DAE introduces three new modeling components to Pyomo: + +.. autosummary:: + :nosignatures: + + pyomo.dae.ContinuousSet + pyomo.dae.DerivativeVar + pyomo.dae.Integral + +As will be shown later, differential equations can be declared using +using these new modeling components along with the standard Pyomo +:py:class:`Var ` and +:py:class:`Constraint ` components. + +ContinuousSet +************* + +This component is used to define continuous bounded domains (for example +'spatial' or 'time' domains). It is similar to a Pyomo +:py:class:`Set ` component and can be used to index things +like variables and constraints. Any number of +:py:class:`ContinuousSets ` can be used to index a +component and components can be indexed by both +:py:class:`Sets ` and +:py:class:`ContinuousSets ` in arbitrary order. + +In the current implementation, models with +:py:class:`ContinuousSet` components may not be solved +until every :py:class:`ContinuousSet` has been +discretized. Minimally, a :py:class:`ContinuousSet` +must be initialized with two numeric values representing the upper and lower +bounds of the continuous domain. A user may also specify additional points in +the domain to be used as finite element points in the discretization. + +.. autoclass:: pyomo.dae.ContinuousSet + :members: + +The following code snippet shows examples of declaring a +:py:class:`ContinuousSet ` component on a +concrete Pyomo model: + +.. doctest:: + + Required imports + >>> from pyomo.environ import * + >>> from pyomo.dae import * + + >>> model = ConcreteModel() + + Declaration by providing bounds + >>> model.t = ContinuousSet(bounds=(0,5)) + + Declaration by initializing with desired discretization points + >>> model.x = ContinuousSet(initialize=[0,1,2,5]) + +.. note:: + A :py:class:`ContinuousSet ` may not be + constructed unless at least two numeric points are provided to bound the + continuous domain. + +The following code snippet shows an example of declaring a +:py:class:`ContinuousSet ` component on an +abstract Pyomo model using the example data file. + +.. code-block:: ampl + + set t := 0 0.5 2.25 3.75 5; + +.. doctest:: + + Required imports + >>> from pyomo.environ import * + >>> from pyomo.dae import * + + >>> model = AbstractModel() + + The ContinuousSet below will be initialized using the points + in the data file when a model instance is created. + >>> model.t = ContinuousSet() + +.. note:: + If a separate data file is used to initialize a + :py:class:`ContinuousSet `, it is done using + the 'set' command and not 'continuousset' + +.. note:: + Most valid ways to declare and initialize a + :py:class:`Set ` can be used to + declare and initialize a :py:class:`ContinuousSet`. + See the documentation for :py:class:`Set ` for additional + options. + +.. warning:: + Be careful using a :py:class:`ContinuousSet + ` as an implicit index in an expression, + i.e. ``sum(m.v[i] for i in m.myContinuousSet)``. The expression will + be generated using the discretization points contained in the + :py:class:`ContinuousSet ` at the time the + expression was constructed and will not be updated if additional + points are added to the set during discretization. + +.. note:: + :py:class:`ContinuousSet ` components are + always ordered (sorted) therefore the ``first()`` and ``last()`` + :py:class:`Set ` methods can be used to access the lower + and upper boundaries of the + :py:class:`ContinuousSet ` respectively + +DerivativeVar +************* + +.. autoclass:: pyomo.dae.DerivativeVar + :members: + +The code snippet below shows examples of declaring +:py:class:`DerivativeVar ` components on a +Pyomo model. In each case, the variable being differentiated is supplied +as the only positional argument and the type of derivative is specified +using the 'wrt' (or the more verbose 'withrespectto') keyword +argument. Any keyword argument that is valid for a Pyomo +:py:class:`Var ` component may also be specified. + +.. doctest:: + + Required imports + >>> from pyomo.environ import * + >>> from pyomo.dae import * + + >>> model = ConcreteModel() + >>> model.s = Set(initialize=['a','b']) + >>> model.t = ContinuousSet(bounds=(0,5)) + >>> model.l = ContinuousSet(bounds=(-10,10)) + + >>> model.x = Var(model.t) + >>> model.y = Var(model.s,model.t) + >>> model.z = Var(model.t,model.l) + + Declare the first derivative of model.x with respect to model.t + >>> model.dxdt = DerivativeVar(model.x, withrespectto=model.t) + + Declare the second derivative of model.y with respect to model.t + Note that this DerivativeVar will be indexed by both model.s and model.t + >>> model.dydt2 = DerivativeVar(model.y, wrt=(model.t,model.t)) + + Declare the partial derivative of model.z with respect to model.l + Note that this DerivativeVar will be indexed by both model.t and model.l + >>> model.dzdl = DerivativeVar(model.z, wrt=(model.l), initialize=0) + + Declare the mixed second order partial derivative of model.z with respect + to model.t and model.l and set bounds + >>> model.dz2 = DerivativeVar(model.z, wrt=(model.t, model.l), bounds=(-10, 10)) + +.. note:: + The 'initialize' keyword argument will initialize the value of a + derivative and is **not** the same as specifying an initial + condition. Initial or boundary conditions should be specified using a + :py:class:`Constraint` or + :py:class:`ConstraintList` or + by fixing the value of a :py:class:`Var` at a boundary + point. + +Declaring Differential Equations +-------------------------------- + +A differential equations is declared as a standard Pyomo +:py:class:`Constraint` and is not required to have +any particular form. The following code snippet shows how one might declare +an ordinary or partial differential equation. + +.. doctest:: + + Required imports + >>> from pyomo.environ import * + >>> from pyomo.dae import * + + >>> model = ConcreteModel() + >>> model.s = Set(initialize=['a', 'b']) + >>> model.t = ContinuousSet(bounds=(0, 5)) + >>> model.l = ContinuousSet(bounds=(-10, 10)) + + >>> model.x = Var(model.s, model.t) + >>> model.y = Var(model.t, model.l) + >>> model.dxdt = DerivativeVar(model.x, wrt=model.t) + >>> model.dydt = DerivativeVar(model.y, wrt=model.t) + >>> model.dydl2 = DerivativeVar(model.y, wrt=(model.l, model.l)) + + An ordinary differential equation + >>> def _ode_rule(m, s, t): + ... if t == 0: + ... return Constraint.Skip + ... return m.dxdt[s, t] == m.x[s, t]**2 + >>> model.ode = Constraint(model.s, model.t, rule=_ode_rule) + + A partial differential equation + >>> def _pde_rule(m, t, l): + ... if t == 0 or l == m.l.first() or l == m.l.last(): + ... return Constraint.Skip + ... return m.dydt[t, l] == m.dydl2[t, l] + >>> model.pde = Constraint(model.t, model.l, rule=_pde_rule) + +By default, a :py:class:`Constraint` declared over a +:py:class:`ContinuousSet` will be applied at every +discretization point contained in the set. Often a modeler does not want to +enforce a differential equation at one or both boundaries of a continuous +domain. This may be addressed explicitly in the +:py:class:`Constraint` declaration using +``Constraint.Skip`` as shown above. Alternatively, the desired constraints can +be deactivated just before the model is sent to a solver as shown below. + +.. doctest:: + :hide: + + >>> model.del_component('ode_index') + >>> model.del_component('pde_index') + >>> model.del_component('ode') + >>> model.del_component('pde') + +.. doctest:: + + >>> def _ode_rule(m, s, t): + ... return m.dxdt[s, t] == m.x[s, t]**2 + >>> model.ode = Constraint(model.s, model.t, rule=_ode_rule) + + >>> def _pde_rule(m, t, l): + ... return m.dydt[t, l] == m.dydl2[t, l] + >>> model.pde = Constraint(model.t, model.l, rule=_pde_rule) + + Declare other model components and apply a discretization transformation + ... + + Deactivate the differential equations at certain boundary points + >>> for con in model.ode[:, model.t.first()]: + ... con.deactivate() + + >>> for con in model.pde[0, :]: + ... con.deactivate() + + >>> for con in model.pde[:, model.l.first()]: + ... con.deactivate() + + >>> for con in model.pde[:, model.l.last()]: + ... con.deactivate() + + Solve the model + ... + +.. note:: + If you intend to use the pyomo.DAE + :py:class:`Simulator` on your model then you + **must** use **constraint deactivation** instead of **constraint + skipping** in the differential equation rule. + +Declaring Integrals +------------------- + +.. warning:: + The :py:class:`Integral` component is still under + development and considered a prototype. It currently includes only basic + functionality for simple integrals. We welcome feedback on the interface + and functionality but **we do not recommend using it** on general + models. Instead, integrals should be reformulated as differential + equations. + +.. autoclass:: pyomo.dae.Integral + :members: + +Declaring an :py:class:`Integral` component is similar to +declaring an :py:class:`Expression` component. A +simple example is shown below: + +.. doctest:: + + >>> model = ConcreteModel() + >>> model.time = ContinuousSet(bounds=(0,10)) + >>> model.X = Var(model.time) + >>> model.scale = Param(initialize=1E-3) + + >>> def _intX(m,t): + ... return m.X[t] + >>> model.intX = Integral(model.time,wrt=model.time,rule=_intX) + + >>> def _obj(m): + ... return m.scale*m.intX + >>> model.obj = Objective(rule=_obj) + +Notice that the positional arguments supplied to the +:py:class:`Integral` declaration must include all indices +needed to evaluate the integral expression. The integral expression is defined +in a function and supplied to the 'rule' keyword argument. Finally, a user must +specify a :py:class:`ContinuousSet` that the integral +is being evaluated over. This is done using the 'wrt' keyword argument. + +.. note:: + The :py:class:`ContinuousSet` specified using the + 'wrt' keyword argument must be explicitly specified as one of the indexing + sets (meaning it must be supplied as a positional argument). This is to + ensure consistency in the ordering and dimension of the indexing sets + +After an :py:class:`Integral` has been declared, it can be +used just like a Pyomo :py:class:`Expression` +component and can be included in constraints or the objective function as shown +above. + +If an :py:class:`Integral` is specified with multiple +positional arguments, i.e. multiple indexing sets, the final component will be +indexed by all of those sets except for the +:py:class:`ContinuousSet` that the integral was +taken over. In other words, the +:py:class:`ContinuousSet` specified with the +'wrt' keyword argument is removed from the indexing sets of the +:py:class:`Integral` even though it must be specified as a +positional argument. This should become more clear with the following example +showing a double integral over the +:py:class:`ContinuousSet` components ``model.t1`` and +``model.t2``. In addition, the expression is also indexed by the +:py:class:`Set` ``model.s``. The mathematical representation +and implementation in Pyomo are shown below: + +.. math:: + \sum_{s} \int_{t_2} \int_{t_1} \! X(t_1, t_2, s) \, dt_1 \, dt_2 + +.. doctest:: + + >>> model = ConcreteModel() + >>> model.t1 = ContinuousSet(bounds=(0, 10)) + >>> model.t2 = ContinuousSet(bounds=(-1, 1)) + >>> model.s = Set(initialize=['A', 'B', 'C']) + + >>> model.X = Var(model.t1, model.t2, model.s) + + >>> def _intX1(m, t1, t2, s): + ... return m.X[t1, t2, s] + >>> model.intX1 = Integral(model.t1, model.t2, model.s, wrt=model.t1, + ... rule=_intX1) + + >>> def _intX2(m, t2, s): + ... return m.intX1[t2, s] + >>> model.intX2 = Integral(model.t2, model.s, wrt=model.t2, rule=_intX2) + + >>> def _obj(m): + ... return sum(m.intX2[k] for k in m.s) + >>> model.obj = Objective(rule=_obj) + +Discretization Transformations +------------------------------ + +Before a Pyomo model with :py:class:`DerivativeVar` +or :py:class:`Integral` components can be sent to a +solver it must first be sent through a discretization transformation. These +transformations approximate any derivatives or integrals in the model by +using a numerical method. The numerical methods currently included in pyomo.DAE +discretize the continuous domains in the problem and introduce equality +constraints which approximate the derivatives and integrals at the +discretization points. Two families of discretization schemes have been +implemented in pyomo.DAE, Finite Difference and Collocation. These schemes are +described in more detail below. + +.. note:: + The schemes described here are for derivatives only. All integrals will + be transformed using the trapezoid rule. + +The user must write a Python script in order to use these discretizations, +they have not been tested on the pyomo command line. Example scripts are +shown below for each of the discretization schemes. The transformations are +applied to Pyomo model objects which can be further manipulated before being +sent to a solver. Examples of this are also shown below. + +Finite Difference Transformation +******************************** + +This transformation includes implementations of several finite +difference methods. For example, the Backward Difference method (also +called Implicit or Backward Euler) has been implemented. The +discretization equations for this method are shown below: + +.. math:: + \begin{array}{l} + \mathrm{Given: } \\ + \frac{dx}{dt} = f(t, x) , \quad x(t_0) = x_{0} \\ + \text{discretize $t$ and $x$ such that } \\ + x(t_0 + kh) = x_{k} \\ + x_{k + 1} = x_{k} + h * f(t_{k + 1}, x_{k + 1}) \\ + t_{k + 1} = t_{k} + h + \end{array} + +where :math:`h` is the step size between discretization points or the size of +each finite element. These equations are generated automatically as +:py:class:`Constraints` when the backward +difference method is applied to a Pyomo model. + +There are several discretization options available to a +``dae.finite_difference`` transformation which can be specified as keyword +arguments to the ``.apply_to()`` function of the transformation object. These +keywords are summarized below: + +.. Replace with in-code documentation. The autoclass that works with the +.. plugins: pyomo.dae.plugins.finitedifference.Finite_Difference_Transformation + + +Keyword arguments for applying a finite difference transformation: + +'nfe' + The desired number of finite element points to be included in the + discretization. The default value is 10. + +'wrt' + Indicates which :py:class:`ContinuousSet` the + transformation should be applied to. If this keyword argument is not + specified then the same scheme will be applied to every + :py:class:`ContinuousSet` . + +'scheme' + Indicates which finite difference method to apply. Options are + 'BACKWARD', 'CENTRAL', or 'FORWARD'. The default scheme is the backward + difference method. + +If the existing number of finite element points in a +:py:class:`ContinuousSet` is less than the desired +number, new discretization points will be added to the set. If a user specifies +a number of finite element points which is less than the number of points +already included in the :py:class:`ContinuousSet` then +the transformation will ignore the specified number and proceed with the larger +set of points. Discretization points will never be removed from a +:py:class:`ContinuousSet` during the discretization. + +The following code is a Python script applying the backward difference +method. The code also shows how to add a constraint to a discretized model. + +.. doctest:: + :hide: + + >>> model = ConcreteModel() + >>> model.time = ContinuousSet(bounds=(0, 10)) + >>> model.x1 = Var(model.time, bounds=(-10, 10)) + >>> model.dx1 = DerivativeVar(model.x1) + +.. doctest:: + :skipif: not ipopt_available + + Discretize model using Backward Difference method + >>> discretizer = TransformationFactory('dae.finite_difference') + >>> discretizer.apply_to(model,nfe=20,wrt=model.time,scheme='BACKWARD') + + Add another constraint to discretized model + >>> def _sum_limit(m): + ... return sum(m.x1[i] for i in m.time) <= 50 + >>> model.con_sum_limit = Constraint(rule=_sum_limit) + + Solve discretized model + >>> solver = SolverFactory('ipopt') + >>> results = solver.solve(model) + +Collocation Transformation +************************** + +This transformation uses orthogonal collocation to discretize the +differential equations in the model. Currently, two types of collocation +have been implemented. They both use Lagrange polynomials with either +Gauss-Radau roots or Gauss-Legendre roots. For more information on +orthogonal collocation and the discretization equations associated with this +method please see chapter 10 of the book "Nonlinear Programming: Concepts, +Algorithms, and Applications to Chemical Processes" by L.T. Biegler. + +The discretization options available to a ``dae.collocation`` transformation +are the same as those described above for the finite difference transformation +with different available schemes and the addition of the 'ncp' option. + +.. Replace with in-code documentation. The autoclass that works with the +.. plugins: pyomo.dae.plugins.finitedifference.Finite_Difference_Transformation + +Additional keyword arguments for collocation discretizations: + +'scheme' + The desired collocation scheme, either 'LAGRANGE-RADAU' or + 'LAGRANGE-LEGENDRE'. The default is 'LAGRANGE-RADAU'. + +'ncp' + The number of collocation points within each finite element. The + default value is 3. + +.. note:: + If the user's version of Python has access to the package Numpy then any + number of collocation points may be specified, otherwise the maximum number + is 10. + +.. note:: + Any points that exist in a + :py:class:`ContinuousSet` before discretization + will be used as finite element boundaries and not as collocation points. + The locations of the collocation points cannot be specified by the user, + they must be generated by the transformation. + +The following code is a Python script applying collocation with Lagrange +polynomials and Radau roots. The code also shows how to add an objective +function to a discretized model. + +.. doctest:: + :hide: + + >>> model = ConcreteModel() + >>> model.time = ContinuousSet(bounds=(0, 10)) + >>> model.x = Var(model.time, bounds=(-10, 10)) + >>> model.dx = DerivativeVar(model.x) + >>> model.x_ref = Param(initialize=5) + +.. doctest:: + :skipif: not ipopt_available + + Discretize model using Radau Collocation + >>> discretizer = TransformationFactory('dae.collocation') + >>> discretizer.apply_to(model,nfe=20,ncp=6,scheme='LAGRANGE-RADAU') + + Add objective function after model has been discretized + >>> def obj_rule(m): + ... return sum((m.x[i]-m.x_ref)**2 for i in m.time) + >>> model.obj = Objective(rule=obj_rule) + + Solve discretized model + >>> solver = SolverFactory('ipopt') + >>> results = solver.solve(model) + +Restricting Optimal Control Profiles +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When solving an optimal control problem a user may want to restrict the +number of degrees of freedom for the control input by forcing, for example, +a piecewise constant profile. Pyomo.DAE provides the +``reduce_collocation_points`` function to address this use-case. This function +is used in conjunction with the ``dae.collocation`` discretization +transformation to reduce the number of free collocation points within a finite +element for a particular variable. + +.. autoclass:: pyomo.dae.plugins.colloc.Collocation_Discretization_Transformation + :members: reduce_collocation_points + +An example of using this function is shown below: + +.. doctest:: + :hide: + + >>> model = ConcreteModel() + >>> model.time = ContinuousSet(bounds=(0, 10)) + >>> model.x = Var(model.time, bounds=(-10, 10)) + >>> model.dx = DerivativeVar(model.x) + >>> model.x_ref = Param(initialize=5) + >>> model.u = Var(model.time) + +.. doctest:: + + >>> discretizer = TransformationFactory('dae.collocation') + >>> discretizer.apply_to(model, nfe=10, ncp=6) + >>> model = discretizer.reduce_collocation_points(model, + ... var=model.u, + ... ncp=1, + ... contset=model.time) + +In the above example, the ``reduce_collocation_points`` function restricts +the variable ``model.u`` to have only **1** free collocation point per +finite element, thereby enforcing a piecewise constant profile. +:numref:`Fig. %s ` shows the solution profile before and +after applying +the ``reduce_collocation_points`` function. + +.. _reduce_points_fig: +.. figure:: reduce_points_demo.png + :scale: 100 % + :align: center + + (left) Profile before applying the ``reduce_collocation_points`` + function (right) Profile after applying the function, restricting + ``model.u`` to have a piecewise constant profile. + + +Applying Multiple Discretization Transformations +************************************************ + +Discretizations can be applied independently to each +:py:class:`ContinuousSet` in a model. This allows the +user great flexibility in discretizing their model. For example the same +numerical method can be applied with different resolutions: + +.. doctest:: + :hide: + + >>> model = ConcreteModel() + >>> model.t1 = ContinuousSet(bounds=(0, 10)) + >>> model.t2 = ContinuousSet(bounds=(-2, 2)) + +.. doctest:: + + >>> discretizer = TransformationFactory('dae.finite_difference') + >>> discretizer.apply_to(model,wrt=model.t1,nfe=10) + >>> discretizer.apply_to(model,wrt=model.t2,nfe=100) + +This also allows the user to combine different methods. For example, applying +the forward difference method to one +:py:class:`ContinuousSet` and the central finite +difference method to another +:py:class:`ContinuousSet`: + +.. doctest:: + :hide: + + >>> model = ConcreteModel() + >>> model.t1 = ContinuousSet(bounds=(0, 10)) + >>> model.t2 = ContinuousSet(bounds=(-2, 2)) + +.. doctest:: + + >>> discretizer = TransformationFactory('dae.finite_difference') + >>> discretizer.apply_to(model,wrt=model.t1,scheme='FORWARD') + >>> discretizer.apply_to(model,wrt=model.t2,scheme='CENTRAL') + +In addition, the user may combine finite difference and collocation +discretizations. For example: + +.. doctest:: + :hide: + + >>> model = ConcreteModel() + >>> model.t1 = ContinuousSet(bounds=(0, 10)) + >>> model.t2 = ContinuousSet(bounds=(-2, 2)) + +.. doctest:: + + >>> disc_fe = TransformationFactory('dae.finite_difference') + >>> disc_fe.apply_to(model,wrt=model.t1,nfe=10) + >>> disc_col = TransformationFactory('dae.collocation') + >>> disc_col.apply_to(model,wrt=model.t2,nfe=10,ncp=5) + +If the user would like to apply the same discretization to all +:py:class:`ContinuousSet` components in a model, just +specify the discretization once without the 'wrt' keyword argument. This will +apply that scheme to all :py:class:`ContinuousSet` +components in the model that haven't already been discretized. + +Custom Discretization Schemes +***************************** + +A transformation framework along with certain utility functions has been +created so that advanced users may easily implement custom discretization +schemes other than those listed above. The transformation framework consists of +the following steps: + + 1. Specify Discretization Options + 2. Discretize the ContinuousSet(s) + 3. Update Model Components + 4. Add Discretization Equations + 5. Return Discretized Model + +If a user would like to create a custom finite difference scheme then they only +have to worry about step (4) in the framework. The discretization equations for +a particular scheme have been isolated from of the rest of the code for +implementing the transformation. The function containing these discretization +equations can be found at the top of the source code file for the +transformation. For example, below is the function for the forward +difference method: + +.. code-block:: python + + def _forward_transform(v,s): + """ + Applies the Forward Difference formula of order O(h) for first derivatives + """ + def _fwd_fun(i): + tmp = sorted(s) + idx = tmp.index(i) + return 1/(tmp[idx+1]-tmp[idx])*(v(tmp[idx+1])-v(tmp[idx])) + return _fwd_fun + +In this function, 'v' represents the continuous variable or function that the +method is being applied to. 's' represents the set of discrete points in the +continuous domain. In order to implement a custom finite difference method, a +user would have to copy the above function and just replace the equation next +to the first return statement with their method. + +After implementing a custom finite difference method using the above function +template, the only other change that must be made is to add the custom method +to the 'all_schemes' dictionary in the ``dae.finite_difference`` +class. + +In the case of a custom collocation method, changes will have to be made in +steps (2) and (4) of the transformation framework. In addition to implementing +the discretization equations, the user would also have to ensure that the +desired collocation points are added to the ContinuousSet being discretized. + +Dynamic Model Simulation +------------------------ + +The pyomo.dae Simulator class can be used to simulate systems of ODEs and +DAEs. It provides an interface to integrators available in other Python +packages. + +.. note:: + The pyomo.dae Simulator does not include integrators directly. The user + must have at least one of the supported Python packages installed in + order to use this class. + +.. autoclass:: pyomo.dae.Simulator + :members: + +.. note:: + Any keyword options supported by the integrator may be specified as + keyword options to the simulate function and will be passed to the + integrator. + +Supported Simulator Packages +**************************** + +The Simulator currently includes interfaces to SciPy and CasADi. ODE +simulation is supported in both packages however, DAE simulation is only +supported by CasADi. A list of available integrators for each package is +given below. Please refer to the `SciPy +`_ +and `CasADi +`_ documentation directly for the most up-to-date information about +these packages and for more information about the various integrators and +options. + +SciPy Integrators: + - **'vode'** : Real-valued Variable-coefficient ODE solver, options for + non-stiff and stiff systems + - **'zvode'** : Complex-values Variable-coefficient ODE solver, options for + non-stiff and stiff systems + - **'lsoda'** : Real-values Variable-coefficient ODE solver, automatic + switching of algorithms for non-stiff or stiff systems + - **'dopri5'** : Explicit runge-kutta method of order (4)5 ODE solver + - **'dop853'** : Explicit runge-kutta method of order 8(5,3) ODE solver + +CasADi Integrators: + - **'cvodes'** : CVodes from the Sundials suite, solver for stiff or + non-stiff ODE systems + - **'idas'** : IDAS from the Sundials suite, DAE solver + - **'collocation'** : Fixed-step implicit runge-kutta method, ODE/DAE + solver + - **'rk'** : Fixed-step explicit runge-kutta method, ODE solver + +Using the Simulator +******************* + +We now show how to use the Simulator to simulate the following system of ODEs: + +.. math:: + \begin{array}{l} + \frac{d\theta}{dt} = \omega \\ + \frac{d\omega}{dt} = -b*\omega -c*sin(\theta) + \end{array} + +We begin by formulating the model using pyomo.DAE + +.. doctest:: + + >>> m = ConcreteModel() + + >>> m.t = ContinuousSet(bounds=(0.0, 10.0)) + + >>> m.b = Param(initialize=0.25) + >>> m.c = Param(initialize=5.0) + + >>> m.omega = Var(m.t) + >>> m.theta = Var(m.t) + + >>> m.domegadt = DerivativeVar(m.omega, wrt=m.t) + >>> m.dthetadt = DerivativeVar(m.theta, wrt=m.t) + + Setting the initial conditions + >>> m.omega[0].fix(0.0) + >>> m.theta[0].fix(3.14 - 0.1) + + >>> def _diffeq1(m, t): + ... return m.domegadt[t] == -m.b * m.omega[t] - m.c * sin(m.theta[t]) + >>> m.diffeq1 = Constraint(m.t, rule=_diffeq1) + + >>> def _diffeq2(m, t): + ... return m.dthetadt[t] == m.omega[t] + >>> m.diffeq2 = Constraint(m.t, rule=_diffeq2) + +Notice that the initial conditions are set by `fixing` the values of +``m.omega`` and ``m.theta`` at t=0 instead of being specified as extra +equality constraints. Also notice that the differential equations are +specified without using ``Constraint.Skip`` to skip enforcement at t=0. The +Simulator cannot simulate any constraints that contain if-statements in +their construction rules. + +To simulate the model you must first create a Simulator object. Building +this object prepares the Pyomo model for simulation with a particular Python +package and performs several checks on the model to ensure compatibility +with the Simulator. Be sure to read through the list of limitations at the +end of this section to understand the types of models supported by the +Simulator. + +.. doctest:: + + >>> sim = Simulator(m, package='scipy') # doctest: +SKIP + +After creating a Simulator object, the model can be simulated by calling the +simulate function. Please see the API documentation for the +:py:class:`Simulator` for more information about the +valid keyword arguments for this function. + +.. doctest:: + + >>> tsim, profiles = sim.simulate(numpoints=100, integrator='vode') # doctest: +SKIP + +The ``simulate`` function returns numpy arrays containing time points and +the corresponding values for the dynamic variable profiles. + +`Simulator Limitations`: + - Differential equations must be first-order and separable + - Model can only contain a single ContinuousSet + - Can't simulate constraints with if-statements in the construction rules + - Need to provide initial conditions for dynamic states by setting the + value or using fix() + +Specifying Time-Varying Inputs +****************************** +The :py:class:`Simulator` supports simulation of a system +of ODE's or DAE's with time-varying parameters or control inputs. Time-varying +inputs can be specified using a Pyomo ``Suffix``. We currently only support +piecewise constant profiles. For more complex inputs defined by a continuous +function of time we recommend adding an algebraic variable and constraint to +your model. + +The profile for a time-varying input should be specified +using a Python dictionary where the keys correspond to the switching times +and the values correspond to the value of the input at a time point. A +``Suffix`` is then used to associate this dictionary with the appropriate +``Var`` or ``Param`` and pass the information to the +:py:class:`Simulator`. The code snippet below shows an +example. + +.. doctest:: + + >>> m = ConcreteModel() + + >>> m.t = ContinuousSet(bounds=(0.0, 20.0)) + + Time-varying inputs + >>> m.b = Var(m.t) + >>> m.c = Param(m.t, default=5.0) + + >>> m.omega = Var(m.t) + >>> m.theta = Var(m.t) + + >>> m.domegadt = DerivativeVar(m.omega, wrt=m.t) + >>> m.dthetadt = DerivativeVar(m.theta, wrt=m.t) + + Setting the initial conditions + >>> m.omega[0] = 0.0 + >>> m.theta[0] = 3.14 - 0.1 + + >>> def _diffeq1(m, t): + ... return m.domegadt[t] == -m.b[t] * m.omega[t] - \ + ... m.c[t] * sin(m.theta[t]) + >>> m.diffeq1 = Constraint(m.t, rule=_diffeq1) + + >>> def _diffeq2(m, t): + ... return m.dthetadt[t] == m.omega[t] + >>> m.diffeq2 = Constraint(m.t, rule=_diffeq2) + + Specifying the piecewise constant inputs + >>> b_profile = {0: 0.25, 15: 0.025} + >>> c_profile = {0: 5.0, 7: 50} + + Declaring a Pyomo Suffix to pass the time-varying inputs to the Simulator + >>> m.var_input = Suffix(direction=Suffix.LOCAL) + >>> m.var_input[m.b] = b_profile + >>> m.var_input[m.c] = c_profile + + Simulate the model using scipy + >>> sim = Simulator(m, package='scipy') # doctest: +SKIP + >>> tsim, profiles = sim.simulate(numpoints=100, + ... integrator='vode', + ... varying_inputs=m.var_input) # doctest: +SKIP + +.. note:: + The Simulator does not support multi-indexed inputs (i.e. if ``m.b`` in + the above example was indexed by another set besides ``m.t``) + +Dynamic Model Initialization +---------------------------- +Providing a good initial guess is an important factor in solving dynamic +optimization problems. There are several model initialization tools under +development in pyomo.DAE to help users initialize their models. These tools +will be documented here as they become available. + +From Simulation +*************** +The :py:class:`Simulator` includes a function for +initializing discretized dynamic optimization models using the profiles +returned from the simulator. An example using this function is shown below + +.. doctest:: + + Simulate the model using scipy + >>> sim = Simulator(m, package='scipy') # doctest: +SKIP + >>> tsim, profiles = sim.simulate(numpoints=100, integrator='vode', + ... varying_inputs=m.var_input) # doctest: +SKIP + + Discretize the model using Orthogonal Collocation + >>> discretizer = TransformationFactory('dae.collocation') + >>> discretizer.apply_to(m, nfe=10, ncp=3) + + Initialize the discretized model using the simulator profiles + >>> sim.initialize_model() # doctest: +SKIP + +.. note:: + A model must be simulated before it can be initialized using this function diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/gdp/concepts.rst b/doc/OnlineDocs/user_guide/modeling_extensions/gdp/concepts.rst new file mode 100644 index 00000000000..95629bc48fd --- /dev/null +++ b/doc/OnlineDocs/user_guide/modeling_extensions/gdp/concepts.rst @@ -0,0 +1,151 @@ +.. image:: /../logos/gdp/Pyomo-GDP-150.png + :scale: 20% + :class: no-scaled-link + :align: right + +************ +Key Concepts +************ + +Generalized Disjunctive Programming (GDP) provides a way to bridge high-level propositional logic and algebraic constraints. +The GDP standard form from the :ref:`index page ` is repeated below. + +.. math:: + + \min\ obj = &\ f(x, z) \\ + \text{s.t.} \quad &\ Ax+Bz \leq d\\ + &\ g(x,z) \leq 0\\ + &\ \bigvee_{i\in D_k} \left[ + \begin{gathered} + Y_{ik} \\ + M_{ik} x + N_{ik} z \leq e_{ik} \\ + r_{ik}(x,z)\leq 0\\ + \end{gathered} + \right] \quad k \in K\\ + &\ \Omega(Y) = True \\ + &\ x \in X \subseteq \mathbb{R}^n\\ + &\ Y \in \{True, False\}^{p}\\ + &\ z \in Z \subseteq \mathbb{Z}^m + +Original support in Pyomo.GDP focused on the disjuncts and disjunctions, allowing the modelers to group relational expressions in disjuncts, with disjunctions describing logical-OR relationships between the groupings. +As a result, we implemented the ``Disjunct`` and ``Disjunction`` objects before ``BooleanVar`` and the rest of the logical expression system. +Accordingly, we also describe the disjuncts and disjunctions first below. + +Disjuncts +========= + +Disjuncts represent groupings of relational expressions (e.g. algebraic constraints) summarized by a Boolean indicator variable :math:`Y` through implication: + +.. math:: + + \left. + \begin{aligned} + & Y_{ik} \Rightarrow & M_{ik} x + N_{ik} z &\leq e_{ik}\\ + & Y_{ik} \Rightarrow & r_{ik}(x,z) &\leq 0 + \end{aligned} + \right.\qquad \forall i \in D_k, \forall k \in K + + +Logically, this means that if :math:`Y_{ik} = True`, then the constraints :math:`M_{ik} x + N_{ik} z \leq e_{ik}` and :math:`r_{ik}(x,z) \leq 0` must be satisfied. +However, if :math:`Y_{ik} = False`, then the corresponding constraints are ignored. +Note that :math:`Y_{ik} = False` does **not** imply that the corresponding constraints are *violated*. + +.. _gdp-disjunctions-concept: + +Disjunctions +============ + +Disjunctions describe a logical *OR* relationship between two or more Disjuncts. +The simplest and most common case is a 2-term disjunction: + +.. math:: + + \left[\begin{gathered} + Y_1 \\ + \exp(x_2) - 1 = x_1 \\ + x_3 = x_4 = 0 + \end{gathered} + \right] \bigvee \left[\begin{gathered} + Y_2 \\ + \exp\left(\frac{x_4}{1.2}\right) - 1 = x_3 \\ + x_1 = x_2 = 0 + \end{gathered} + \right] + + +The disjunction above describes the selection between two units in a process network. +:math:`Y_1` and :math:`Y_2` are the Boolean variables corresponding to the selection of process units 1 and 2, respectively. +The continuous variables :math:`x_1, x_2, x_3, x_4` describe flow in and out of the first and second units, respectively. +If a unit is selected, the nonlinear equality in the corresponding disjunct enforces the input/output relationship in the selected unit. +The final equality in each disjunct forces flows for the absent unit to zero. + +Boolean Variables +================= + +Boolean variables are decision variables that may take a value of ``True`` or ``False``. +These are most often encountered as the indicator variables of disjuncts. +However, they can also be independently defined to represent other problem decisions. + +.. note:: + + Boolean variables are not intended to participate in algebraic expressions. + That is, :math:`3 \times \text{True}` does not make sense; hence, :math:`x = 3 Y_1` does not make sense. + Instead, you may have the disjunction + + .. math:: + + \left[\begin{gathered} + Y_1 \\ + x = 3 + \end{gathered} + \right] \bigvee \left[\begin{gathered} + \neg Y_1 \\ + x = 0 + \end{gathered} + \right] + +Logical Propositions +==================== + +Logical propositions are constraints describing relationships between the Boolean variables in the model. + +These logical propositions can include: + +.. |neg| replace:: :math:`\neg Y_1` +.. |equiv| replace:: :math:`Y_1 \Leftrightarrow Y_2` +.. |land| replace:: :math:`Y_1 \land Y_2` +.. |lor| replace:: :math:`Y_1 \lor Y_2` +.. |xor| replace:: :math:`Y_1 \veebar Y_2` +.. |impl| replace:: :math:`Y_1 \Rightarrow Y_2` + ++-----------------+---------+-------------+-------------+-------------+ +| Operator | Example | :math:`Y_1` | :math:`Y_2` | Result | ++=================+=========+=============+=============+=============+ +| Negation | |neg| | | ``True`` | | | ``False`` | +| | | | ``False`` | | | ``True`` | ++-----------------+---------+-------------+-------------+-------------+ +| Equivalence | |equiv| | | ``True`` | | ``True`` | | ``True`` | +| | | | ``True`` | | ``False`` | | ``False`` | +| | | | ``False`` | | ``True`` | | ``False`` | +| | | | ``False`` | | ``False`` | | ``True`` | ++-----------------+---------+-------------+-------------+-------------+ +| Conjunction | |land| | | ``True`` | | ``True`` | | ``True`` | +| | | | ``True`` | | ``False`` | | ``False`` | +| | | | ``False`` | | ``True`` | | ``False`` | +| | | | ``False`` | | ``False`` | | ``False`` | ++-----------------+---------+-------------+-------------+-------------+ +| Disjunction | |lor| | | ``True`` | | ``True`` | | ``True`` | +| | | | ``True`` | | ``False`` | | ``True`` | +| | | | ``False`` | | ``True`` | | ``True`` | +| | | | ``False`` | | ``False`` | | ``False`` | ++-----------------+---------+-------------+-------------+-------------+ +| Exclusive OR | |xor| | | ``True`` | | ``True`` | | ``False`` | +| | | | ``True`` | | ``False`` | | ``True`` | +| | | | ``False`` | | ``True`` | | ``True`` | +| | | | ``False`` | | ``False`` | | ``False`` | ++-----------------+---------+-------------+-------------+-------------+ +| Implication | |impl| | | ``True`` | | ``True`` | | ``True`` | +| | | | ``True`` | | ``False`` | | ``False`` | +| | | | ``False`` | | ``True`` | | ``True`` | +| | | | ``False`` | | ``False`` | | ``True`` | ++-----------------+---------+-------------+-------------+-------------+ diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/gdp/index.rst b/doc/OnlineDocs/user_guide/modeling_extensions/gdp/index.rst new file mode 100644 index 00000000000..0c8529c60cb --- /dev/null +++ b/doc/OnlineDocs/user_guide/modeling_extensions/gdp/index.rst @@ -0,0 +1,79 @@ +.. _gdp-main-page: + +*********************************** +Generalized Disjunctive Programming +*********************************** + +.. image:: /../logos/gdp/Pyomo-GDP-150.png + :scale: 35% + :align: right + :class: no-scaled-link + +The Pyomo.GDP modeling extension\ [#gdp-main-paper]_ provides support for Generalized Disjunctive Programming (GDP)\ [#gdp]_, an extension of Disjunctive Programming\ [#dp]_ from the operations research community to include nonlinear relationships. The classic form for a GDP is given by: + +.. math:: + + \min\ obj = &\ f(x, z) \\ + \text{s.t.} \quad &\ Ax+Bz \leq d\\ + &\ g(x,z) \leq 0\\ + &\ \bigvee_{i\in D_k} \left[ + \begin{gathered} + Y_{ik} \\ + M_{ik} x + N_{ik} z \leq e_{ik} \\ + r_{ik}(x,z)\leq 0\\ + \end{gathered} + \right] \quad k \in K\\ + &\ \Omega(Y) = True \\ + &\ x \in X \subseteq \mathbb{R}^n\\ + &\ Y \in \{True, False\}^{p}\\ + &\ z \in Z \subseteq \mathbb{Z}^m + +Here, we have the minimization of an objective :math:`obj` subject to global linear constraints :math:`Ax+Bz \leq d` and nonlinear constraints :math:`g(x,z) \leq 0`, with conditional linear constraints :math:`M_{ik} x + N_{ik} z \leq e_{ik}` and nonlinear constraints :math:`r_{ik}(x,z)\leq 0`. +These conditional constraints are collected into disjuncts :math:`D_k`, organized into disjunctions :math:`K`. Finally, there are logical propositions :math:`\Omega(Y) = True`. +Decision/state variables can be continuous :math:`x`, Boolean :math:`Y`, and/or integer :math:`z`. + +GDP is useful to model discrete decisions that have implications on the system behavior\ [#gdpreview]_. +For example, in process design, a disjunction may model the choice between processes A and B. +If A is selected, then its associated equations and inequalities will apply; otherwise, if B is selected, then its respective constraints should be enforced. + +Modelers often ask to model if-then-else relationships. +These can be expressed as a disjunction as follows: + +.. math:: + :nowrap: + + \begin{gather*} + \left[\begin{gathered} + Y_1 \\ + \text{constraints} \\ + \text{for }\textit{then} + \end{gathered}\right] + \vee + \left[\begin{gathered} + Y_2 \\ + \text{constraints} \\ + \text{for }\textit{else} + \end{gathered}\right] \\ + Y_1 \veebar Y_2 + \end{gather*} + +Here, if the Boolean :math:`Y_1` is ``True``, then the constraints in the first disjunct are enforced; otherwise, the constraints in the second disjunct are enforced. +The following sections describe the key concepts, modeling, and solution approaches available for Generalized Disjunctive Programming. + +.. toctree:: + :caption: Pyomo.GDP Contents + :maxdepth: 2 + + concepts + modeling + solving + +Literature References +===================== +.. [#gdp-main-paper] Chen, Q., Johnson, E. S., Bernal, D. E., Valentin, R., Kale, S., Bates, J., Siirola, J. D. and Grossmann, I. E. (2021). Pyomo.GDP: an ecosystem for logic based modeling and optimization development, *Optimization and Engineering* (pp. 1-36).https://doi.org/10.1007/s11081-021-09601-7 + +.. [#gdp] Raman, R., & Grossmann, I. E. (1994). Modelling and computational techniques for logic based integer programming. *Computers & Chemical Engineering*, 18(7), 563–578. https://doi.org/10.1016/0098-1354(93)E0010-7 + +.. [#dp] Balas, E. (1985). Disjunctive Programming and a Hierarchy of Relaxations for Discrete Optimization Problems. *SIAM Journal on Algebraic Discrete Methods*, 6(3), 466–486. https://doi.org/10.1137/0606047 + +.. [#gdpreview] Grossmann, I. E., & Trespalacios, F. (2013). Systematic modeling of discrete-continuous optimization models through generalized disjunctive programming. *AIChE Journal*, 59(9), 3276–3295. https://doi.org/10.1002/aic.14088 diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/gdp/modeling.rst b/doc/OnlineDocs/user_guide/modeling_extensions/gdp/modeling.rst new file mode 100644 index 00000000000..996ebcb0366 --- /dev/null +++ b/doc/OnlineDocs/user_guide/modeling_extensions/gdp/modeling.rst @@ -0,0 +1,419 @@ +.. image:: /../logos/gdp/Pyomo-GDP-150.png + :scale: 20% + :class: no-scaled-link + :align: right + +********************* +Modeling in Pyomo.GDP +********************* + +.. testsetup:: + + from pyomo.environ import ( + ConcreteModel, RangeSet, BooleanVar, LogicalConstraint, + TransformationFactory, atleast, SolverFactory, Objective, + Constraint, Var, land, Reference + ) + from pyomo.gdp import Disjunct, Disjunction + from pyomo.core.plugins.transform.logical_to_linear import update_boolean_vars_from_binary + + # This is to make unicode comparison work in python 2.7. + import sys + if sys.version[0] == '2': + reload(sys) + sys.setdefaultencoding("utf-8") + +Disjunctions +============ + +To demonstrate modeling with disjunctions in Pyomo.GDP, we revisit the small example from :ref:`the previous page `. + +.. math:: + + \left[\begin{gathered} + Y_1 \\ + \exp(x_2) - 1 = x_1 \\ + x_3 = x_4 = 0 + \end{gathered} + \right] \bigvee \left[\begin{gathered} + Y_2 \\ + \exp\left(\frac{x_4}{1.2}\right) - 1 = x_3 \\ + x_1 = x_2 = 0 + \end{gathered} + \right] + +Explicit syntax: more descriptive +--------------------------------- + +Pyomo.GDP explicit syntax (see below) provides more clarity in the declaration of each modeling object, and gives the user explicit control over the ``Disjunct`` names. +Assuming the ``ConcreteModel`` object :code:`m` and variables have been defined, lines 1 and 5 declare the ``Disjunct`` objects corresponding to selection of unit 1 and 2, respectively. +Lines 2 and 6 define the input-output relations for each unit, and lines 3-4 and 7-8 enforce zero flow through the unit that is not selected. +Finally, line 9 declares the logical disjunction between the two disjunctive terms. + +.. code-block:: python + :linenos: + + m.unit1 = Disjunct() + m.unit1.inout = Constraint(expr=exp(m.x[2]) - 1 == m.x[1]) + m.unit1.no_unit2_flow1 = Constraint(expr=m.x[3] == 0) + m.unit1.no_unit2_flow2 = Constraint(expr=m.x[4] == 0) + m.unit2 = Disjunct() + m.unit2.inout = Constraint(expr=exp(m.x[4] / 1.2) - 1 == m.x[3]) + m.unit2.no_unit1_flow1 = Constraint(expr=m.x[1] == 0) + m.unit2.no_unit1_flow2 = Constraint(expr=m.x[2] == 0) + m.use_unit1or2 = Disjunction(expr=[m.unit1, m.unit2]) + +The indicator variables for each disjunct :math:`Y_1` and :math:`Y_2` are automatically generated by Pyomo.GDP, accessible via :code:`m.unit1.indicator_var` and :code:`m.unit2.indicator_var`. + +Compact syntax: more concise +---------------------------- + +For more advanced users, a compact syntax is also available below, taking advantage of the ability to declare disjuncts and constraints implicitly. +When the ``Disjunction`` object constructor is passed a list of lists, the outer list defines the disjuncts and the inner list defines the constraint expressions associated with the respective disjunct. + +.. code-block:: python + :linenos: + + m.use1or2 = Disjunction(expr=[ + # First disjunct + [exp(m.x[2])-1 == m.x[1], + m.x[3] == 0, m.x[4] == 0], + # Second disjunct + [exp(m.x[4]/1.2)-1 == m.x[3], + m.x[1] == 0, m.x[2] == 0]]) + +.. note:: + + By default, Pyomo.GDP ``Disjunction`` objects enforce an implicit "exactly one" relationship among the selection of the disjuncts (generalization of exclusive-OR). + That is, exactly one of the ``Disjunct`` indicator variables should take a ``True`` value. + This can be seen as an implicit logical proposition, in our example, :math:`Y_1 \veebar Y_2`. + +Logical Propositions +==================== + +Pyomo.GDP also supports the use of logical propositions through the use of the ``BooleanVar`` and ``LogicalConstraint`` objects. +The ``BooleanVar`` object in Pyomo represents Boolean variables, analogous to ``Var`` for numeric variables. +``BooleanVar`` can be indexed over a Pyomo ``Set``, as below: + +.. doctest:: + + >>> m = ConcreteModel() + >>> m.my_set = RangeSet(4) + >>> m.Y = BooleanVar(m.my_set) + >>> m.Y.display() + Y : Size=4, Index=my_set + Key : Value : Fixed : Stale + 1 : None : False : True + 2 : None : False : True + 3 : None : False : True + 4 : None : False : True + +Using these Boolean variables, we can define ``LogicalConstraint`` objects, analogous to algebraic ``Constraint`` objects. + +.. doctest:: + + >>> m.p = LogicalConstraint(expr=m.Y[1].implies(m.Y[2] & m.Y[3]) | m.Y[4]) + >>> m.p.pprint() + p : Size=1, Index=None, Active=True + Key : Body : Active + None : (Y[1] --> Y[2] ∧ Y[3]) ∨ Y[4] : True + +Supported Logical Operators +--------------------------- + +Pyomo.GDP logical expression system supported operators and their usage are listed in the table below. + ++--------------+------------------------+-----------------------------------+--------------------------------+ +| Operator | Operator | Method | Function | ++==============+========================+===================================+================================+ +| Negation | :code:`~Y[1]` | | :code:`lnot(Y[1])` | ++--------------+------------------------+-----------------------------------+--------------------------------+ +| Conjunction | :code:`Y[1] & Y[2]` | :code:`Y[1].land(Y[2])` | :code:`land(Y[1],Y[2])` | ++--------------+------------------------+-----------------------------------+--------------------------------+ +| Disjunction | :code:`Y[1] | Y[2]` | :code:`Y[1].lor(Y[2])` | :code:`lor(Y[1],Y[2])` | ++--------------+------------------------+-----------------------------------+--------------------------------+ +| Exclusive OR | :code:`Y[1] ^ Y[2]` | :code:`Y[1].xor(Y[2])` | :code:`xor(Y[1], Y[2])` | ++--------------+------------------------+-----------------------------------+--------------------------------+ +| Implication | | :code:`Y[1].implies(Y[2])` | :code:`implies(Y[1], Y[2])` | ++--------------+------------------------+-----------------------------------+--------------------------------+ +| Equivalence | | :code:`Y[1].equivalent_to(Y[2])` | :code:`equivalent(Y[1], Y[2])` | ++--------------+------------------------+-----------------------------------+--------------------------------+ + +.. note:: + + We omit support for some infix operators, e.g. :code:`Y[1] >> Y[2]`, due to concerns about non-intuitive Python operator precedence. + That is :code:`Y[1] | Y[2] >> Y[3]` would translate to :math:`Y_1 \lor (Y_2 \Rightarrow Y_3)` rather than :math:`(Y_1 \lor Y_2) \Rightarrow Y_3` + +In addition, the following constraint-programming-inspired operators are provided: ``exactly``, ``atmost``, and ``atleast``. +These predicates enforce, respectively, that exactly, at most, or at least N of their ``BooleanVar`` arguments are ``True``. + +Usage: + +- :code:`atleast(3, Y[1], Y[2], Y[3])` +- :code:`atmost(3, Y)` +- :code:`exactly(3, Y)` + +.. doctest:: + + >>> m = ConcreteModel() + >>> m.my_set = RangeSet(4) + >>> m.Y = BooleanVar(m.my_set) + >>> m.p = LogicalConstraint(expr=atleast(3, m.Y)) + >>> m.p.pprint() + p : Size=1, Index=None, Active=True + Key : Body : Active + None : atleast(3: [Y[1], Y[2], Y[3], Y[4]]) : True + >>> TransformationFactory('core.logical_to_linear').apply_to(m) + >>> # constraint auto-generated by transformation + >>> m.logic_to_linear.transformed_constraints.pprint() + transformed_constraints : Size=1, Index={1}, Active=True + Key : Lower : Body : Upper : Active + 1 : 3.0 : Y_asbinary[1] + Y_asbinary[2] + Y_asbinary[3] + Y_asbinary[4] : +Inf : True + +We elaborate on the ``logical_to_linear`` transformation :ref:`on the next page `. + +Indexed logical constraints +--------------------------- + +Like ``Constraint`` objects for algebraic expressions, ``LogicalConstraint`` objects can be indexed. +An example of this usage may be found below for the expression: + +.. math:: + + Y_{i+1} \Rightarrow Y_{i}, \quad i \in \{1, 2, \dots, n-1\} + +.. doctest:: + + >>> m = ConcreteModel() + >>> n = 5 + >>> m.I = RangeSet(n) + >>> m.Y = BooleanVar(m.I) + + >>> @m.LogicalConstraint(m.I) + ... def p(m, i): + ... return m.Y[i+1].implies(m.Y[i]) if i < n else Constraint.Skip + + >>> m.p.pprint() + p : Size=4, Index=I, Active=True + Key : Body : Active + 1 : Y[2] --> Y[1] : True + 2 : Y[3] --> Y[2] : True + 3 : Y[4] --> Y[3] : True + 4 : Y[5] --> Y[4] : True + +Integration with Disjunctions +----------------------------- + +.. note:: + + Historically, the ``indicator_var`` on ``Disjunct`` objects was + implemented as a binary ``Var``. Beginning in Pyomo 6.0, that has + been changed to the more mathematically correct ``BooleanVar``, with + the associated binary variable available as + ``binary_indicator_var``. + +The logical expression system is designed to augment the previously +introduced ``Disjunct`` and ``Disjunction`` components. Mathematically, +the disjunct indicator variable is Boolean, and can be used directly in +logical propositions. + +Here, we demonstrate this capability with a toy example: + +.. math:: + + \min~&x\\ + \text{s.t.}~&\left[\begin{gathered}Y_1\\x \geq 2\end{gathered}\right] \vee \left[\begin{gathered}Y_2\\x \geq 3\end{gathered}\right]\\ + &\left[\begin{gathered}Y_3\\x \leq 8\end{gathered}\right] \vee \left[\begin{gathered}Y_4\\x = 2.5\end{gathered}\right] \\ + &Y_1 \veebar Y_2\\ + &Y_3 \veebar Y_4\\ + &Y_1 \Rightarrow Y_4 + +.. doctest:: + :skipif: not glpk_available + + >>> m = ConcreteModel() + >>> m.s = RangeSet(4) + >>> m.ds = RangeSet(2) + >>> m.d = Disjunct(m.s) + >>> m.djn = Disjunction(m.ds) + >>> m.djn[1] = [m.d[1], m.d[2]] + >>> m.djn[2] = [m.d[3], m.d[4]] + >>> m.x = Var(bounds=(-2, 10)) + >>> m.d[1].c = Constraint(expr=m.x >= 2) + >>> m.d[2].c = Constraint(expr=m.x >= 3) + >>> m.d[3].c = Constraint(expr=m.x <= 8) + >>> m.d[4].c = Constraint(expr=m.x == 2.5) + >>> m.o = Objective(expr=m.x) + + >>> # Add the logical proposition + >>> m.p = LogicalConstraint( + ... expr=m.d[1].indicator_var.implies(m.d[4].indicator_var)) + >>> # Note: the implicit XOR enforced by m.djn[1] and m.djn[2] still apply + + >>> # Apply the Big-M reformulation: It will convert the logical + >>> # propositions to algebraic expressions. + >>> TransformationFactory('gdp.bigm').apply_to(m) + + >>> # Before solve, Boolean vars have no value + >>> Reference(m.d[:].indicator_var).display() + IndexedBooleanVar : Size=4, Index=s, ReferenceTo=d[:].indicator_var + Key : Value : Fixed : Stale + 1 : None : False : True + 2 : None : False : True + 3 : None : False : True + 4 : None : False : True + + >>> # Solve the reformulated model + >>> run_data = SolverFactory('glpk').solve(m) + >>> Reference(m.d[:].indicator_var).display() + IndexedBooleanVar : Size=4, Index=s, ReferenceTo=d[:].indicator_var + Key : Value : Fixed : Stale + 1 : True : False : False + 2 : False : False : False + 3 : False : False : False + 4 : True : False : False + +.. _gdp-advanced-examples: + +Advanced LogicalConstraint Examples +=================================== + +Support for complex nested expressions is a key benefit of the logical expression system. +Below are examples of expressions that we support, and with some, an explanation of their implementation. + +Composition of standard operators +--------------------------------- + +.. math:: + Y_1 \vee Y_2 \implies Y_3 \wedge \neg Y_4 \wedge (Y_5 \vee Y_6) + +.. code:: + + m.p = LogicalConstraint(expr=(m.Y[1] | m.Y[2]).implies( + m.Y[3] & ~m.Y[4] & (m.Y[5] | m.Y[6])) + ) + +Expressions within CP-type operators +------------------------------------ + +.. math:: + \text{atleast}(3, Y_1, Y_2 \vee Y_3, Y_4 \Rightarrow Y_5, Y_6) + +Here, augmented variables may be automatically added to the model as follows: + +.. math:: + \text{atleast}(3, &Y_1, Y_A, Y_B, Y_6)\\ + &Y_A \Leftrightarrow Y_2 \vee Y_3\\ + &Y_B \Leftrightarrow (Y_4 \Rightarrow Y_5) + +.. code:: + + m.p = LogicalConstraint( + expr=atleast(3, m.Y[1], Or(m.Y[2], m.Y[3]), m.Y[4].implies(m.Y[5]), m.Y[6])) + +Nested CP-style operators +------------------------- + +.. math:: + \text{atleast}(2, Y_1, \text{exactly}(2, Y_2, Y_3, Y_4), Y_5, Y_6) + +Here, we again need to add augmented variables: + +.. math:: + \text{atleast}(2, Y_1, Y_A, Y_5, Y_6)\\ + Y_A \Leftrightarrow \text{exactly}(2, Y_2, Y_3, Y_4) + +However, we also need to further interpret the second statement as a disjunction: + +.. math:: + :nowrap: + + \begin{gather*} + \text{atleast}(2, Y_1, Y_A, Y_5, Y_6)\\ + \left[\begin{gathered}Y_A\\\text{exactly}(2, Y_2, Y_3, Y_4)\end{gathered}\right] + \vee + \left[\begin{gathered}\neg Y_A\\ + \left[\begin{gathered}Y_B\\\text{atleast}(3, Y_2, Y_3, Y_4)\end{gathered}\right] \vee \left[\begin{gathered}Y_C\\\text{atmost}(1, Y_2, Y_3, Y_4)\end{gathered}\right] + \end{gathered}\right] + \end{gather*} + +or equivalently, + +.. math:: + :nowrap: + + \begin{gather*} + \text{atleast}(2, Y_1, Y_A, Y_5, Y_6)\\ + \text{exactly}(1, Y_A, Y_B, Y_C)\\ + \left[\begin{gathered}Y_A\\\text{exactly}(2, Y_2, Y_3, Y_4)\end{gathered}\right] + \vee + \left[\begin{gathered}Y_B\\\text{atleast}(3, Y_2, Y_3, Y_4)\end{gathered}\right] \vee \left[\begin{gathered}Y_C\\\text{atmost}(1, Y_2, Y_3, Y_4)\end{gathered}\right] + \end{gather*} + +.. code:: + + m.p = LogicalConstraint( + expr=atleast(2, m.Y[1], exactly(2, m.Y[2], m.Y[3], m.Y[4]), m.Y[5], m.Y[6])) + +In the ``logical_to_linear`` transformation, we automatically convert these special disjunctions to linear form using a Big M reformulation. + +Additional Examples +=================== + +The following models all work and are equivalent for :math:`\left[x = 0\right] \veebar \left[y = 0\right]`: + +.. doctest:: + + Option 1: Rule-based construction + + >>> from pyomo.environ import * + >>> from pyomo.gdp import * + >>> model = ConcreteModel() + + >>> model.x = Var() + >>> model.y = Var() + + >>> # Two conditions + >>> def _d(disjunct, flag): + ... model = disjunct.model() + ... if flag: + ... # x == 0 + ... disjunct.c = Constraint(expr=model.x == 0) + ... else: + ... # y == 0 + ... disjunct.c = Constraint(expr=model.y == 0) + >>> model.d = Disjunct([0,1], rule=_d) + + >>> # Define the disjunction + >>> def _c(model): + ... return [model.d[0], model.d[1]] + >>> model.c = Disjunction(rule=_c) + + Option 2: Explicit disjuncts + + >>> from pyomo.environ import * + >>> from pyomo.gdp import * + >>> model = ConcreteModel() + + >>> model.x = Var() + >>> model.y = Var() + + >>> model.fix_x = Disjunct() + >>> model.fix_x.c = Constraint(expr=model.x == 0) + + >>> model.fix_y = Disjunct() + >>> model.fix_y.c = Constraint(expr=model.y == 0) + + >>> model.c = Disjunction(expr=[model.fix_x, model.fix_y]) + + Option 3: Implicit disjuncts (disjunction rule returns a list of + expressions or a list of lists of expressions) + + >>> from pyomo.environ import * + >>> from pyomo.gdp import * + >>> model = ConcreteModel() + + >>> model.x = Var() + >>> model.y = Var() + + >>> model.c = Disjunction(expr=[model.x == 0, model.y == 0]) diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/gdp/solving.rst b/doc/OnlineDocs/user_guide/modeling_extensions/gdp/solving.rst new file mode 100644 index 00000000000..9fea90ebf5f --- /dev/null +++ b/doc/OnlineDocs/user_guide/modeling_extensions/gdp/solving.rst @@ -0,0 +1,201 @@ +.. image:: /../logos/gdp/Pyomo-GDP-150.png + :scale: 20% + :class: no-scaled-link + :align: right + +***************************************** +Solving Logic-based Models with Pyomo.GDP +***************************************** + + +Flexible Solution Suite +======================= + +Once a model is formulated as a GDP model, a range of solution +strategies are available to manipulate and solve it. + +The traditional approach is reformulation to a MI(N)LP, but various +other techniques are possible, including direct solution via the +:ref:`GDPopt solver `. Below, we describe some of +these capabilities. + +.. _gdp-reformulations: + +Reformulations +============== + +Logical constraints +------------------- + +.. note:: + + Historically users needed to explicitly convert logical propositions + to algebraic form prior to invoking the GDP MI(N)LP reformulations + or the GDPopt solver. However, this is mathematically incorrect + since the GDP MI(N)LP reformulations themselves convert logical + formulations to algebraic formulations. The current recommended + practice is to pass the entire (mixed logical / algebraic) model to + the MI(N)LP reformulations or GDPopt directly. + +There are several approaches to convert logical constraints into +algebraic form. + +Conjunctive Normal Form +^^^^^^^^^^^^^^^^^^^^^^^ + +The first transformation (`core.logical_to_linear`) leverages the +`sympy` package to generate the conjunctive normal form of the logical +constraints and then adds the equivalent as a list algebraic +constraints. The following transforms logical propositions on the model +to algebraic form: + +.. code:: + + TransformationFactory('core.logical_to_linear').apply_to(model) + +The transformation creates a constraint list with a unique name starting +with ``logic_to_linear``, within which the algebraic equivalents of the +logical constraints are placed. If not already associated with a binary +variable, each ``BooleanVar`` object will receive a generated binary +counterpart. These associated binary variables may be accessed via the +``get_associated_binary()`` method. + +.. code:: + + m.Y[1].get_associated_binary() + +Additional augmented variables and their corresponding constraints may +also be created, as described in :ref:`gdp-advanced-examples`. + +Following solution of the GDP model, values of the Boolean variables may be updated from their algebraic binary counterparts using the ``update_boolean_vars_from_binary()`` function. + +.. autofunction:: pyomo.core.plugins.transform.logical_to_linear.update_boolean_vars_from_binary + +Factorable Programming +^^^^^^^^^^^^^^^^^^^^^^ + +The second transformation (`contrib.logical_to_disjunctive`) leverages +ideas from factorable programming to first generate an equivalent set of +"factored" logical constraints form by traversing each logical +proposition and replacing each logical operator with an additional +Boolean variable and then adding the "simple" logical constraint that +equates the new Boolean variable with the single logical operator. + +The resulting "simple" logical constraints are converted to either MIP +or GDP form: if the constraint contains only Boolean variables, then +then MIP representation is emitted. Logical constraints with mixed +integer-Boolean arguments (e.g., `atmost`, `atleast`, `exactly`, etc.) +are converted to a disjunctive representation. + +As this transformation both avoids the conversion into `sympy` and only +requires a single traversal of each logical constraint, +`contrib.logical_to_disjunctive` is significantly faster than +`core.logical_to_linear` at the cost of a larger model. In practice, +the cost of the larger model is negated by the effectiveness of the MIP +presolve in most solvers. + +Reformulation to MI(N)LP +------------------------ + +To use standard commercial solvers, you must convert the disjunctive +model to a standard MILP/MINLP model. The two classical strategies for +doing so are the (included) Big-M and Hull reformulations. + + +Big-M (BM) Reformulation +^^^^^^^^^^^^^^^^^^^^^^^^ + +The Big-M reformulation\ [#gdp-bm]_ results in a smaller transformed model, avoiding the need to add extra variables; however, it yields a looser continuous relaxation. +By default, the BM transformation will estimate reasonably tight M values for you if variables are bounded. +For nonlinear models where finite expression bounds may be inferred from variable bounds, the BM transformation may also be able to automatically compute M values for you. +For all other models, you will need to provide the M values through a "BigM" Suffix, or through the `bigM` argument to the transformation. +We will raise a ``GDP_Error`` for missing M values. + +To apply the BM reformulation within a python script, use: + +.. code:: + + TransformationFactory('gdp.bigm').apply_to(model) + +From the Pyomo command line, include the ``--transform pyomo.gdp.bigm`` option. + +Multiple Big-M (MBM) Reformulation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We also implement the multiple-parameter Big-M (MBM) approach described in literature\ [#gdp-mbm]_. +By default, the MBM transformation will solve continuous subproblems in order to calculate M values. +This process can be time-consuming, so the transformation also provides a method to export the M values used as a dictionary and allows for M values to be provided through the `bigM` argument. + +For example, to apply the transformation and store the M values, use: + +.. code:: + + mbigm = TransformationFactory('gdp.mbigm') + mbigm.apply_to(model) + + # These can be stored... + M_values = mbigm.get_all_M_values(model) + # ...so that in future runs, you can write: + mbigm.apply_to(m, bigM=M_values) + +From the Pyomo command line, include the ``--transform pyomo.gdp.mbigm`` option. + +.. warning:: + The Multiple Big-M transformation does not currently support Suffixes and will + ignore "BigM" Suffixes. + +Hull Reformulation (HR) +^^^^^^^^^^^^^^^^^^^^^^^ + +The Hull Reformulation requires a lifting into a higher-dimensional space and consequently introduces disaggregated variables and their corresponding constraints. + +.. note:: + + - All variables that appear in disjuncts need upper and lower bounds. + + - The hull reformulation is an exact reformulation at the solution + points even for nonconvex GDP models, but the resulting MINLP will + also be nonconvex. + +To apply the Hull reformulation within a python script, use: + +.. code:: + + TransformationFactory('gdp.hull').apply_to(model) + +From the Pyomo command line, include the ``--transform pyomo.gdp.hull`` option. + +Hybrid BM/HR Reformulation +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +An experimental (for now) implementation of the cutting plane approach described in literature\ [#gdp-cuttingplanes]_ is provided for linear GDP models. +The transformation augments the BM reformulation by a set of cutting planes generated from the HR model by solving separation problems. +This gives a model that is not as large as the HR, but with a stronger continuous relaxation than the BM. + +This transformation is accessible via: + +.. code:: + + TransformationFactory('gdp.cuttingplane').apply_to(model) + +Direct GDP solvers +================== + +Pyomo includes the contributed GDPopt solver, which can directly solve +GDP models. Its usage is described within the :ref:`contributed +packages documentation `. + +References +========== + +.. [#gdp-pse-paper] Chen, Q., Johnson, E. S., Siirola, J. D., & Grossmann, I. E. (2018). Pyomo.GDP: Disjunctive Models in Python. In M. R. Eden, M. G. Ierapetritou, & G. P. Towler (Eds.), *Proceedings of the 13th International Symposium on Process Systems Engineering* (pp. 889–894). San Diego: Elsevier B.V. https://doi.org/10.1016/B978-0-444-64241-7.50143-9 + +.. [#gdp-main-paper] Chen, Q., Johnson, E. S., Bernal, D. E., Valentin, R., Kale, S., Bates, J., Siirola, J. D. and Grossmann, I. E. (2021). Pyomo.GDP: an ecosystem for logic based modeling and optimization development, *Optimization and Engineering* (pp. 1-36).https://doi.org/10.1007/s11081-021-09601-7 + +.. [#gdp-review-2013] Grossmann, I. E., & Trespalacios, F. (2013). Systematic modeling of discrete-continuous optimization models through generalized disjunctive programming. *AIChE Journal*, 59(9), 3276–3295. https://doi.org/10.1002/aic.14088 + +.. [#gdp-mbm] Trespalacios, F., & Grossmann, I. E. (2015). Improved Big-M reformulation for generalized disjunctive programs. *Computers and Chemical Engineering*, 76, 98–103. https://doi.org/10.1016/j.compchemeng.2015.02.013 + +.. [#gdp-bm] Nemhauser, G. L., & Wolsey, L. A. (1988). *Integer and combinatorial optimization*. New York: Wiley. + +.. [#gdp-cuttingplanes] Sawaya, N. W., & Grossmann, I. E. (2003). A cutting plane method for solving linear generalized disjunctive programming problems. *Computer Aided Chemical Engineering*, 15(C), 1032–1037. https://doi.org/10.1016/S1570-7946(03)80444-3 diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/index.rst b/doc/OnlineDocs/user_guide/modeling_extensions/index.rst new file mode 100644 index 00000000000..3a3370e510a --- /dev/null +++ b/doc/OnlineDocs/user_guide/modeling_extensions/index.rst @@ -0,0 +1,12 @@ +Modeling Extensions +=================== + +.. toctree:: + :maxdepth: 1 + + bilevel.rst + dae.rst + gdp/index.rst + mpec.rst + stochastic_programming.rst + network.rst diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/mpec.rst b/doc/OnlineDocs/user_guide/modeling_extensions/mpec.rst new file mode 100644 index 00000000000..b7ba19712ca --- /dev/null +++ b/doc/OnlineDocs/user_guide/modeling_extensions/mpec.rst @@ -0,0 +1,6 @@ +MPEC +==== + +``pyomo.mpec`` supports modeling complementarity conditions and +optimization problems with equilibrium constraints. + diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/network.rst b/doc/OnlineDocs/user_guide/modeling_extensions/network.rst new file mode 100644 index 00000000000..3fce9448997 --- /dev/null +++ b/doc/OnlineDocs/user_guide/modeling_extensions/network.rst @@ -0,0 +1,331 @@ +Pyomo Network +============= + +Pyomo Network is a package that allows users to easily represent their model +as a connected network of units. Units are blocks that contain ports, which +contain variables, that are connected to other ports via arcs. The connection +of two ports to each other via an arc typically represents a set of constraints +equating each member of each port to each other, however there exist other +connection rules as well, in addition to support for custom rules. Pyomo +Network also includes a model transformation that will automatically expand +the arcs and generate the appropriate constraints to produce an algebraic +model that a solver can handle. Furthermore, the package also introduces a +generic sequential decomposition tool that can leverage the modeling +components to decompose a model and compute each unit in the model in a +logically ordered sequence. + +Modeling Components +------------------- + +Pyomo Network introduces two new modeling components to Pyomo: + +.. autosummary:: + :nosignatures: + + pyomo.network.Port + pyomo.network.Arc + +Port +**** + +.. autoclass:: pyomo.network.Port + :members: + :exclude-members: construct, display + +.. autoclass:: pyomo.network.port._PortData + :members: + :special-members: __getattr__ + :exclude-members: set_value + +The following code snippet shows examples of declaring and using a +:py:class:`Port ` component on a +concrete Pyomo model: + +.. doctest:: + + >>> from pyomo.environ import * + >>> from pyomo.network import * + >>> m = ConcreteModel() + >>> m.x = Var() + >>> m.y = Var(['a', 'b']) # can be indexed + >>> m.z = Var() + >>> m.e = 5 * m.z # you can add Pyomo expressions too + >>> m.w = Var() + + >>> m.p = Port() + >>> m.p.add(m.x) # implicitly name the port member "x" + >>> m.p.add(m.y, "foo") # name the member "foo" + >>> m.p.add(m.e, rule=Port.Extensive) # specify a rule + >>> m.p.add(m.w, rule=Port.Extensive, write_var_sum=False) # keyword arg + +Arc +*** + +.. autoclass:: pyomo.network.Arc + :members: + :exclude-members: construct + +.. autoclass:: pyomo.network.arc._ArcData + :members: + :special-members: __getattr__ + +The following code snippet shows examples of declaring and using an +:py:class:`Arc ` component on a +concrete Pyomo model: + +.. doctest:: + + >>> from pyomo.environ import * + >>> from pyomo.network import * + >>> m = ConcreteModel() + >>> m.x = Var() + >>> m.y = Var(['a', 'b']) + >>> m.u = Var() + >>> m.v = Var(['a', 'b']) + >>> m.w = Var() + >>> m.z = Var(['a', 'b']) # indexes need to match + + >>> m.p = Port(initialize=[m.x, m.y]) + >>> m.q = Port(initialize={"x": m.u, "y": m.v}) + >>> m.r = Port(initialize={"x": m.w, "y": m.z}) # names need to match + >>> m.a = Arc(source=m.p, destination=m.q) # directed + >>> m.b = Arc(ports=(m.p, m.q)) # undirected + >>> m.c = Arc(ports=(m.p, m.q), directed=True) # directed + >>> m.d = Arc(src=m.p, dest=m.q) # aliases work + >>> m.e = Arc(source=m.r, dest=m.p) # ports can have both in and out + +Arc Expansion Transformation +---------------------------- + +The examples above show how to declare and instantiate a +:py:class:`Port ` and an +:py:class:`Arc `. These two components form the basis of +the higher level representation of a connected network with sets of related +variable quantities. Once a network model has been constructed, Pyomo Network +implements a transformation that will expand all (active) arcs on the model +and automatically generate the appropriate constraints. The constraints +created for each port member will be indexed by the same indexing set as +the port member itself. + +During transformation, a new block is created on the model for each arc +(located on the arc's parent block), which serves to contain all of the +auto generated constraints for that arc. At the end of the +transformation, a reference is created on the arc that points to this +new block, available via the arc property `arc.expanded_block`. + +The constraints produced by this transformation depend on the rule assigned +for each port member and can be different between members on the same port. +For example, you can have two different members on a port where one member's +rule is :py:func:`Port.Equality ` and the other +member's rule is :py:func:`Port.Extensive `. + +:py:func:`Port.Equality ` is the default rule +for port members. This rule simply generates equality constraints on the +expanded block between the source port's member and the destination port's +member. Another implemented expansion method is +:py:func:`Port.Extensive `, which essentially +represents implied splitting and mixing of certain variable quantities. +Users can refer to the documentation of the static method itself for more +details on how this implicit splitting and mixing is implemented. +Additionally, should users desire, the expansion API supports custom rules +that can be implemented to generate whatever is needed for special cases. + +The following code demonstrates how to call the transformation to expand +the arcs on a model: + +.. doctest:: + + >>> from pyomo.environ import * + >>> from pyomo.network import * + >>> m = ConcreteModel() + >>> m.x = Var() + >>> m.y = Var(['a', 'b']) + >>> m.u = Var() + >>> m.v = Var(['a', 'b']) + + >>> m.p = Port(initialize=[m.x, (m.y, Port.Extensive)]) # rules must match + >>> m.q = Port(initialize={"x": m.u, "y": (m.v, Port.Extensive)}) + >>> m.a = Arc(source=m.p, destination=m.q) + + >>> TransformationFactory("network.expand_arcs").apply_to(m) + +Sequential Decomposition +------------------------ + +Pyomo Network implements a generic +:py:class:`SequentialDecomposition ` +tool that can be used to compute each unit in a network model in a logically +ordered sequence. + +The sequential decomposition procedure is commenced via the +:py:func:`run ` method. + +Creating a Graph +**************** + +To begin this procedure, the Pyomo Network model is first utilized to create +a networkx `MultiDiGraph` by adding edges to the graph for every arc on the +model, where the nodes of the graph are the parent blocks of the source and +destination ports. This is done via the +:py:func:`create_graph ` +method, which requires all arcs on the model to be both directed and already +expanded. The `MultiDiGraph` class of networkx supports both direccted edges +as well as having multiple edges between the same two nodes, so users can +feel free to connect as many ports as desired between the same two units. + +Computation Order +***************** + +The order of computation is then determined by treating the resulting graph +as a tree, starting at the roots of the tree, and making sure by the time +each node is reached, all of its predecessors have already been computed. +This is implemented through the :py:func:`calculation_order +` and +:py:func:`tree_order ` +methods. Before this, however, the procedure will first select a set of tear +edges, if necessary, such that every loop in the graph is torn, while +minimizing both the number of times any single loop is torn as well as the +total number of tears. + +Tear Selection +************** + +A set of tear edges can be selected in one of two ways. By default, a Pyomo +MIP model is created and optimized resulting in an optimal set of tear edges. +The implementation of this MIP model is based on a set of binary "torn" +variables for every edge in the graph, and constraints on every loop in the +graph that dictate that there must be at least one tear on the loop. Then +there are two objectives (represented by a doubly weighted objective). The +primary objective is to minimize the number of times any single loop is torn, +and then secondary to that is to minimize the total number of tears. This +process is implemented in the :py:func:`select_tear_mip +` method, which uses +the model returned from the :py:func:`select_tear_mip_model +` method. + +Alternatively, there is the :py:func:`select_tear_heuristic +` method. This +uses a heuristic procedure that walks back and forth on the graph to find +every optimal tear set, and returns each equally optimal tear set it finds. +This method is much slower than the MIP method on larger models, but it +maintains some use in the fact that it returns every possible optimal tear set. + +A custom tear set can be assigned before calling the +:py:func:`run ` method. This is +useful so users can know what their tear set will be and thus what arcs will +require guesses for uninitialized values. See the +:py:func:`set_tear_set ` +method for details. + +Running the Sequential Decomposition Procedure +********************************************** + +After all of this computational order preparation, the sequential +decomposition procedure will then run through the graph in the order it +has determined. Thus, the `function` that was passed to the +:py:func:`run ` method will be +called on every unit in sequence. This function can perform any arbitrary +operations the user desires. The only thing that +:py:class:`SequentialDecomposition ` +expects from the function is that after returning from it, every variable +on every outgoing port of the unit will be specified (i.e. it will have a +set current value). Furthermore, the procedure guarantees to the user that +for every unit, before the function is called, every variable on every +incoming port of the unit will be fixed. + +In between computing each of these units, port member values are passed +across existing arcs involving the unit currently being computed. This means +that after computing a unit, the expanded constraints from each arc coming +out of this unit will be satisfied, and the values on the respective +destination ports will be fixed at these new values. While running the +computational order, values are not passed across tear edges, as tear edges +represent locations in loops to stop computations (during iterations). This +process continues until all units in the network have been computed. This +concludes the "first pass run" of the network. + +Guesses and Fixing Variables +**************************** + +When passing values across arcs while running the computational order, +values at the destinations of each of these arcs will be fixed at the +appropriate values. This is important to the fact that the procedure +guarantees every inlet variable will be fixed before calling the function. +However, since values are not passed across torn arcs, there is a need for +user-supplied guesses for those values. See the :py:func:`set_guesses_for +` method for details +on how to supply these values. + +In addition to passing dictionaries of guesses for certain ports, users can +also assign current values to the variables themselves and the procedure +will pick these up and fix the variables in place. Alternatively, users can +utilize the `default_guess` option to specify a value to use as a default +guess for all free variables if they have no guess or current value. If a +free variable has no guess or current value and there is no default guess +option, then an error will be raised. + +Similarly, if the procedure attempts to pass a value to a destination port +member but that port member is already fixed and its fixed value is different +from what is trying to be passed to it (by a tolerance specified by the +`almost_equal_tol` option), then an error will be raised. Lastly, if there +is more than one free variable in a constraint while trying to pass values +across an arc, an error will be raised asking the user to fix more variables +by the time values are passed across said arc. + +Tear Convergence +**************** + +After completing the first pass run of the network, the sequential +decomposition procedure will proceed to converge all tear edges in the +network (unless the user specifies not to, or if there are no tears). +This process occurs separately for every strongly connected component (SCC) +in the graph, and the SCCs are computed in a logical order such that each +SCC is computed before other SCCs downstream of it (much like +:py:func:`tree_order `). + +There are two implemented methods for converging tear edges: direct +substitution and Wegstein acceleration. Both of these will iteratively run +the computation order until every value in every tear arc has converged to +within the specified tolerance. See the +:py:class:`SequentialDecomposition ` +parameter documentation for details on what can be controlled about this +procedure. + +The following code demonstrates basic usage of the +:py:class:`SequentialDecomposition ` +class: + +.. doctest:: + :skipif: not __import__("pyomo.network").network.decomposition.imports_available + + >>> from pyomo.environ import * + >>> from pyomo.network import * + >>> m = ConcreteModel() + >>> m.unit1 = Block() + >>> m.unit1.x = Var() + >>> m.unit1.y = Var(['a', 'b']) + >>> m.unit2 = Block() + >>> m.unit2.x = Var() + >>> m.unit2.y = Var(['a', 'b']) + >>> m.unit1.port = Port(initialize=[m.unit1.x, (m.unit1.y, Port.Extensive)]) + >>> m.unit2.port = Port(initialize=[m.unit2.x, (m.unit2.y, Port.Extensive)]) + >>> m.a = Arc(source=m.unit1.port, destination=m.unit2.port) + >>> TransformationFactory("network.expand_arcs").apply_to(m) + + >>> m.unit1.x.fix(10) + >>> m.unit1.y['a'].fix(15) + >>> m.unit1.y['b'].fix(20) + + >>> seq = SequentialDecomposition(tol=1.0E-3) # options can go to init + >>> seq.options.select_tear_method = "heuristic" # or set them like so + >>> # seq.set_tear_set([...]) # assign a custom tear set + >>> # seq.set_guesses_for(m.unit.inlet, {...}) # choose guesses + >>> def initialize(b): + ... # b.initialize() + ... pass + ... + >>> seq.run(m, initialize) + +.. autoclass:: pyomo.network.SequentialDecomposition + :members: set_guesses_for, set_tear_set, tear_set_arcs, indexes_to_arcs, + run, create_graph, select_tear_mip, select_tear_mip_model, + select_tear_heuristic, calculation_order, tree_order diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/reduce_points_demo.png b/doc/OnlineDocs/user_guide/modeling_extensions/reduce_points_demo.png new file mode 100644 index 0000000000000000000000000000000000000000..00195f26dd7b6af03315c11372b3094a3aebf7ef GIT binary patch literal 29803 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 O_)=5R!W1i6KKu_f`Icb- literal 0 HcmV?d00001 diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/stochastic_programming.rst b/doc/OnlineDocs/user_guide/modeling_extensions/stochastic_programming.rst new file mode 100644 index 00000000000..227a8d9aa8d --- /dev/null +++ b/doc/OnlineDocs/user_guide/modeling_extensions/stochastic_programming.rst @@ -0,0 +1,17 @@ +Stochastic Programming in Pyomo +=============================== + +There are two extensions for modeling and solving Stochastic Programs in +Pyomo. Both are currently distributed as independent Python packages. +PySP was the original extension (and up through Pyomo 5.7.3 was +distributed as part of Pyomo). You can find the documentation here: + + `https://pysp.readthedocs.io `_ + +In 2020, the PySP developers released the mpi-sppy package, which +reimplemented much of the functionality from PySP in a new scalable +framework built on top of MPI and the mpi4py package. Future +development of stochastic programming capabilities is occurring in +mpi-sppy. The documentation is available here: + + `https://mpi-sppy.readthedocs.io `_ diff --git a/doc/OnlineDocs/user_guide/persistent_solvers.rst b/doc/OnlineDocs/user_guide/persistent_solvers.rst new file mode 100644 index 00000000000..aebb0545dd0 --- /dev/null +++ b/doc/OnlineDocs/user_guide/persistent_solvers.rst @@ -0,0 +1,188 @@ +Persistent Solvers +================== + +The purpose of the persistent solver interfaces is to efficiently +notify the solver of incremental changes to a Pyomo model. The +persistent solver interfaces create and store model instances from the +Python API for the corresponding solver. For example, the +:class:`GurobiPersistent` +class maintaints a pointer to a gurobipy Model object. Thus, we can +make small changes to the model and notify the solver rather than +recreating the entire model using the solver Python API (or rewriting +an entire model file - e.g., an lp file) every time the model is +solved. + +.. warning:: Users are responsible for notifying persistent solver + interfaces when changes to a model are made! + + +Using Persistent Solvers +------------------------ + +The first step in using a persistent solver is to create a Pyomo model +as usual. + +>>> import pyomo.environ as pe +>>> m = pe.ConcreteModel() +>>> m.x = pe.Var() +>>> m.y = pe.Var() +>>> m.obj = pe.Objective(expr=m.x**2 + m.y**2) +>>> m.c = pe.Constraint(expr=m.y >= -2*m.x + 5) + +You can create an instance of a persistent solver through the SolverFactory. + +>>> opt = pe.SolverFactory('gurobi_persistent') # doctest: +SKIP + +This returns an instance of :py:class:`GurobiPersistent`. Now we need +to tell the solver about our model. + +>>> opt.set_instance(m) # doctest: +SKIP + +This will create a gurobipy Model object and include the appropriate +variables and constraints. We can now solve the model. + +>>> results = opt.solve() # doctest: +SKIP + +We can also add or remove variables, constraints, blocks, and +objectives. For example, + +>>> m.c2 = pe.Constraint(expr=m.y >= m.x) # doctest: +SKIP +>>> opt.add_constraint(m.c2) # doctest: +SKIP + +This tells the solver to add one new constraint but otherwise leave +the model unchanged. We can now resolve the model. + +>>> results = opt.solve() # doctest: +SKIP + +To remove a component, simply call the corresponding remove method. + +>>> opt.remove_constraint(m.c2) # doctest: +SKIP +>>> del m.c2 # doctest: +SKIP +>>> results = opt.solve() # doctest: +SKIP + +If a pyomo component is replaced with another component with the same +name, the first component must be removed from the solver. Otherwise, +the solver will have multiple components. For example, the following +code will run without error, but the solver will have an extra +constraint. The solver will have both y >= -2*x + 5 and y <= x, which +is not what was intended! + +>>> m = pe.ConcreteModel() # doctest: +SKIP +>>> m.x = pe.Var() # doctest: +SKIP +>>> m.y = pe.Var() # doctest: +SKIP +>>> m.c = pe.Constraint(expr=m.y >= -2*m.x + 5) # doctest: +SKIP +>>> opt = pe.SolverFactory('gurobi_persistent') # doctest: +SKIP +>>> opt.set_instance(m) # doctest: +SKIP +>>> # WRONG: +>>> del m.c # doctest: +SKIP +>>> m.c = pe.Constraint(expr=m.y <= m.x) # doctest: +SKIP +>>> opt.add_constraint(m.c) # doctest: +SKIP + +The correct way to do this is: + +>>> m = pe.ConcreteModel() # doctest: +SKIP +>>> m.x = pe.Var() # doctest: +SKIP +>>> m.y = pe.Var() # doctest: +SKIP +>>> m.c = pe.Constraint(expr=m.y >= -2*m.x + 5) # doctest: +SKIP +>>> opt = pe.SolverFactory('gurobi_persistent') # doctest: +SKIP +>>> opt.set_instance(m) # doctest: +SKIP +>>> # Correct: +>>> opt.remove_constraint(m.c) # doctest: +SKIP +>>> del m.c # doctest: +SKIP +>>> m.c = pe.Constraint(expr=m.y <= m.x) # doctest: +SKIP +>>> opt.add_constraint(m.c) # doctest: +SKIP + +.. warning:: Components removed from a pyomo model must be removed + from the solver instance by the user. + +Additionally, unexpected behavior may result if a component is +modified before being removed. + +>>> m = pe.ConcreteModel() # doctest: +SKIP +>>> m.b = pe.Block() # doctest: +SKIP +>>> m.b.x = pe.Var() # doctest: +SKIP +>>> m.b.y = pe.Var() # doctest: +SKIP +>>> m.b.c = pe.Constraint(expr=m.b.y >= -2*m.b.x + 5) # doctest: +SKIP +>>> opt = pe.SolverFactory('gurobi_persistent') # doctest: +SKIP +>>> opt.set_instance(m) # doctest: +SKIP +>>> m.b.c2 = pe.Constraint(expr=m.b.y <= m.b.x) # doctest: +SKIP +>>> # ERROR: The constraint referenced by m.b.c2 does not +>>> # exist in the solver model. +>>> opt.remove_block(m.b) # doctest: +SKIP + +In most cases, the only way to modify a component is to remove it from +the solver instance, modify it with Pyomo, and then add it back to the +solver instance. The only exception is with variables. Variables may +be modified and then updated with with solver: + +>>> m = pe.ConcreteModel() # doctest: +SKIP +>>> m.x = pe.Var() # doctest: +SKIP +>>> m.y = pe.Var() # doctest: +SKIP +>>> m.obj = pe.Objective(expr=m.x**2 + m.y**2) # doctest: +SKIP +>>> m.c = pe.Constraint(expr=m.y >= -2*m.x + 5) # doctest: +SKIP +>>> opt = pe.SolverFactory('gurobi_persistent') # doctest: +SKIP +>>> opt.set_instance(m) # doctest: +SKIP +>>> m.x.setlb(1.0) # doctest: +SKIP +>>> opt.update_var(m.x) # doctest: +SKIP + +Working with Indexed Variables and Constraints +---------------------------------------------- + +The examples above all used simple variables and constraints; in order to use +indexed variables and/or constraints, the code must be slightly adapted: + +>>> for v in indexed_var.values(): # doctest: +SKIP +... opt.add_var(v) +>>> for v in indexed_con.values(): # doctest: +SKIP +... opt.add_constraint(v) + +This must be done when removing variables/constraints, too. Not doing this would +result in AttributeError exceptions, for example: + +>>> opt.add_var(indexed_var) # doctest: +SKIP +>>> # ERROR: AttributeError: 'IndexedVar' object has no attribute 'is_binary' +>>> opt.add_constraint(indexed_con) # doctest: +SKIP +>>> # ERROR: AttributeError: 'IndexedConstraint' object has no attribute 'body' + +The method "is_indexed" can be used to automate the process, for example: + +>>> def add_variable(opt, variable): # doctest: +SKIP +... if variable.is_indexed(): +... for v in variable.values(): +... opt.add_var(v) +... else: +... opt.add_var(v) + +Persistent Solver Performance +----------------------------- +In order to get the best performance out of the persistent solvers, use the +"save_results" flag: + +>>> import pyomo.environ as pe +>>> m = pe.ConcreteModel() +>>> m.x = pe.Var() +>>> m.y = pe.Var() +>>> m.obj = pe.Objective(expr=m.x**2 + m.y**2) +>>> m.c = pe.Constraint(expr=m.y >= -2*m.x + 5) +>>> opt = pe.SolverFactory('gurobi_persistent') # doctest: +SKIP +>>> opt.set_instance(m) # doctest: +SKIP +>>> results = opt.solve(save_results=False) # doctest: +SKIP + +Note that if the "save_results" flag is set to False, then the following +is not supported. + +>>> results = opt.solve(save_results=False, load_solutions=False) # doctest: +SKIP +>>> if results.solver.termination_condition == TerminationCondition.optimal: +... m.solutions.load_from(results) # doctest: +SKIP + +However, the following will work: + +>>> results = opt.solve(save_results=False, load_solutions=False) # doctest: +SKIP +>>> if results.solver.termination_condition == TerminationCondition.optimal: +... opt.load_vars() # doctest: +SKIP + +Additionally, a subset of variable values may be loaded back into the model: + +>>> results = opt.solve(save_results=False, load_solutions=False) # doctest: +SKIP +>>> if results.solver.termination_condition == TerminationCondition.optimal: +... opt.load_vars(m.x) # doctest: +SKIP diff --git a/doc/OnlineDocs/user_guide/pyomo_modeling_components/Constraints.rst b/doc/OnlineDocs/user_guide/pyomo_modeling_components/Constraints.rst new file mode 100644 index 00000000000..0cc42cb2abe --- /dev/null +++ b/doc/OnlineDocs/user_guide/pyomo_modeling_components/Constraints.rst @@ -0,0 +1,39 @@ +Constraints +=========== + +Most constraints are specified using equality or inequality expressions +that are created using a rule, which is a Python function. For example, +if the variable ``model.x`` has the indexes 'butter' and 'scones', then +this constraint limits the sum over these indexes to be exactly three: + +.. literalinclude:: ../src/scripting/spy4Constraints_Constraint_example.spy + :language: python + +Instead of expressions involving equality (==) or inequalities (`<=` or +`>=`), constraints can also be expressed using a 3-tuple if the form +(lb, expr, ub) where lb and ub can be ``None``, which is interpreted as +lb `<=` expr `<=` ub. Variables can appear only in the middle expr. For +example, the following two constraint declarations have the same +meaning: + +.. literalinclude:: ../src/scripting/spy4Constraints_Inequality_constraints_2expressions.spy + :language: python + +For this simple example, it would also be possible to declare +``model.x`` with a ``bounds`` option to accomplish the same thing. + +Constraints (and objectives) can be indexed by lists or sets. When the +declaration contains lists or sets as arguments, the elements are +iteratively passed to the rule function. If there is more than one, then +the cross product is sent. For example the following constraint could be +interpreted as placing a budget of :math:`i` on the +:math:`i^{\mbox{th}}` item to buy where the cost per item is given by +the parameter ``model.a``: + +.. literalinclude:: ../src/scripting/spy4Constraints_Passing_elements_crossproduct.spy + :language: python + +.. note:: + + Python and Pyomo are case sensitive so ``model.a`` is not the same as + ``model.A``. diff --git a/doc/OnlineDocs/user_guide/pyomo_modeling_components/Expressions.rst b/doc/OnlineDocs/user_guide/pyomo_modeling_components/Expressions.rst new file mode 100644 index 00000000000..16c206e2fe8 --- /dev/null +++ b/doc/OnlineDocs/user_guide/pyomo_modeling_components/Expressions.rst @@ -0,0 +1,218 @@ +Expressions +=========== + +In this section, we use the word "expression" in two ways: first in the +general sense of the word and second to describe a class of Pyomo objects +that have the name ``Expression`` as described in the subsection on +expression objects. + +Rules to Generate Expressions +----------------------------- + +Both objectives and constraints make use of rules to generate +expressions. These are Python functions that return the appropriate +expression. These are first-class functions that can access +global data as well as data passed in, including the model object. + +Operations on model elements results in expressions, which seems natural +in expressions like the constraints we have seen so far. It is also +possible to build up expressions. The following example illustrates +this, along with a reference to global Python data in the form of a +Python variable called ``switch``: + +.. literalinclude:: ../src/scripting/spy4Expressions_Buildup_expression_switch.spy + :language: python + +In this example, the constraint that is generated depends on the value +of the Python variable called ``switch``. If the value is 2 or greater, +then the constraint is ``summation(model.c, model.x) - model.d >= 0.5``; +otherwise, the ``model.d`` term is not present. + +.. warning:: + + Because model elements result in expressions, not values, the + following does not work as expected in an abstract model! + + .. literalinclude:: ../src/scripting/spy4Expressions_Abstract_wrong_usage.spy + :language: python + + The trouble is that ``model.d >= 2`` results in an expression, not + its evaluated value. Instead use ``if value(model.d) >= 2`` + +.. note:: + + Pyomo supports non-linear expressions and can call non-linear solvers such as Ipopt. + +.. _piecewise: + +.. _abstract2piece.py: + +Piecewise Linear Expressions +---------------------------- + +Pyomo has facilities to add piecewise constraints of the form y=f(x) for +a variety of forms of the function f. + +The piecewise types other than SOS2, BIGM_SOS1, BIGM_BIN are implement +as described in the paper [Vielma_et_al]_. + +There are two basic forms for the declaration of the constraint: + +.. literalinclude:: ../src/scripting/spy4Expressions_Declare_piecewise_constraints.spy + :language: python + +where ``pwconst`` can be replaced by a name appropriate for the +application. The choice depends on whether the x and y variables are +indexed. If so, they must have the same index sets and these sets are +give as the first arguments. + +Keywords: +********* + +* **pw_pts={ },[ ],( )** + + A dictionary of lists (where keys are the index set) or a single list + (for the non-indexed case or when an identical set of breakpoints is + used across all indices) defining the set of domain breakpoints for + the piecewise linear function. + + .. note:: + + pw_pts is always required. These give the breakpoints for the + piecewise function and are expected to fully span the bounds for + the independent variable(s). + +* **pw_repn=

" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "D_vals_rand_from_ind_FIM = []\n", + "running_FIM = np.zeros((n_para, n_para))\n", + "print(FIM_rand)\n", + "for i in range(20):\n", + " running_FIM += FIM_rand[i]\n", + " D_vals_rand_from_ind_FIM.append(np.log10(np.linalg.det(running_FIM)))\n", + "\n", + "plt.plot(range(20), D_vals_rand_from_ind_FIM, color='green')\n", + "plt.plot(range(20), D_vals_rand, color='orange')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0bf5a03-4f8a-4c7b-9ee3-57577550689e", + "metadata": {}, + "outputs": [], + "source": [ + "# mobel based design" + ] + }, + { + "cell_type": "code", + "execution_count": 106, + "id": "be09a59b-5783-4cdd-a023-380be8f2935a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.47e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 2.64e+01 3.85e+02 -1.0 1.47e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.71e+00 5.99e+01 -1.0 3.11e+01 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 3.98e-02 2.06e+00 -1.0 3.19e+00 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 7.87e-07 2.39e-04 -1.0 2.80e-02 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (287125)\n", + " 5 0.0000000e+00 2.27e-13 1.50e-09 -3.8 5.42e-07 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.3691704763652764e-13 2.2737367544323206e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.3691704763652764e-13 2.2737367544323206e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.078\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.0\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 8.41e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 5.97e+02 3.85e+02 -1.0 8.40e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 9.58e+01 5.99e+01 -1.0 8.73e+02 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.25e+00 2.06e+00 -1.0 7.86e+01 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.23e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (394005)\n", + " 5 0.0000000e+00 2.52e-13 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.3691704763652764e-13 2.5224267119483557e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.3691704763652764e-13 2.5224267119483557e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.097\n", + "Total CPU secs in NLP function evaluations = 0.015\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -7.7248744e+00 8.42e+02 3.03e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303416)\n", + " 1 -9.5185054e+00 3.55e+02 1.97e+00 -1.0 2.49e+01 - 6.64e-01 5.10e-01h 1\n", + " 2 -1.0459243e+01 2.21e+01 1.06e+00 -1.0 1.03e+01 - 9.89e-01 1.00e+00f 1\n", + " 3 -1.0005982e+01 2.37e+00 1.82e+01 -1.0 6.69e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -9.4350217e+00 6.84e+00 2.90e+01 -1.0 1.66e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -9.9270718e+00 3.91e+00 4.23e+00 -1.0 7.85e+01 - 1.00e+00 1.00e+00h 1\n", + " 6 -9.8988295e+00 8.63e-02 7.95e-01 -1.0 4.47e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -9.9256371e+00 1.12e-02 8.03e-02 -1.7 1.69e+00 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.0115145e+01 6.09e-01 3.86e+00 -2.5 1.24e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.0716463e+01 1.17e+01 7.10e+01 -2.5 4.55e+02 - 2.86e-01 2.24e-01h 2\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.1139383e+01 3.90e+00 1.50e+01 -2.5 3.96e+01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (326891)\n", + " 11 -1.1160539e+01 7.96e-02 1.12e-01 -2.5 2.01e+01 - 1.00e+00 1.00e+00h 1\n", + " 12 -1.1161798e+01 1.04e-03 2.28e-03 -2.5 3.39e+00 - 1.00e+00 1.00e+00h 1\n", + " 13 -1.1804938e+01 5.61e+00 3.57e+00 -3.8 7.07e+01 - 7.08e-01 1.00e+00f 1\n", + " 14 -1.2138004e+01 2.90e+00 2.94e+00 -3.8 6.85e+01 - 9.69e-01 1.00e+00h 1\n", + " 15 -1.2260171e+01 8.52e-01 5.77e-01 -3.8 3.63e+01 - 1.00e+00 1.00e+00h 1\n", + " 16 -1.2249771e+01 6.32e-03 2.20e-02 -3.8 9.24e+00 - 1.00e+00 1.00e+00h 1\n", + " 17 -1.2249799e+01 6.45e-06 1.24e-05 -3.8 3.70e-01 - 1.00e+00 1.00e+00h 1\n", + " 18 -1.2341110e+01 2.89e-01 1.08e-01 -5.7 2.25e+01 - 9.19e-01 9.86e-01f 1\n", + " 19 -1.2347950e+01 6.26e-03 1.40e-03 -5.7 2.74e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.2348059e+01 2.15e-06 1.44e-06 -5.7 4.38e-02 - 1.00e+00 1.00e+00h 1\n", + " 21 -1.2349341e+01 7.29e-05 2.04e-05 -8.6 3.46e-01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (355282)\n", + " 22 -1.2349342e+01 2.92e-10 5.48e-08 -8.6 5.48e-04 -4.0 1.00e+00 1.00e+00h 1\n", + " 23 -1.2349342e+01 4.55e-13 2.15e-11 -8.6 6.44e-07 -4.5 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 23\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.2349342292918536e+01 -1.2349342292918536e+01\n", + "Dual infeasibility......: 2.1473493827409733e-11 2.1473493827409733e-11\n", + "Constraint violation....: 1.7053025658242404e-13 4.5474735088646412e-13\n", + "Complementarity.........: 2.5059035849180921e-09 2.5059035849180921e-09\n", + "Overall NLP error.......: 2.5059035849180921e-09 2.5059035849180921e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 27\n", + "Number of objective gradient evaluations = 24\n", + "Number of equality constraint evaluations = 27\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 24\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 23\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.487\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.7\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.94e+01 3.85e+02 -1.0 3.97e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.83e+01 4.31e+01 -1.0 4.78e+01 - 4.76e-01 9.84e-01h 1\n", + " 3 0.0000000e+00 2.63e-01 4.48e+02 -1.0 2.89e+01 - 4.02e-02 9.90e-01h 1\n", + " 4 0.0000000e+00 5.47e-04 1.17e+02 -1.0 1.35e-01 - 9.90e-01 9.98e-01h 1\n", + "Reallocating memory for MA57: lfact (284905)\n", + " 5 0.0000000e+00 6.26e-11 1.00e-06 -1.0 2.79e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 6.2641447584610432e-11 6.2641447584610432e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 6.2641447584610432e-11 6.2641447584610432e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.078\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.1\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 2.19e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 6.10e+02 3.85e+02 -1.0 2.19e+03 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 9.60e+01 5.99e+01 -1.0 8.86e+02 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.25e+00 2.06e+00 -1.0 7.87e+01 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.23e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (394385)\n", + " 5 0.0000000e+00 2.27e-12 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 4.5474735088646412e-13 2.2737367544323206e-12\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 4.5474735088646412e-13 2.2737367544323206e-12\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.113\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.3841270e+01 8.42e+02 1.61e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303016)\n", + " 1 -1.4404335e+01 3.97e+02 3.77e+00 -1.0 2.54e+01 - 8.41e-01 5.01e-01h 1\n", + " 2 -1.4812683e+01 1.50e+01 5.74e-01 -1.0 7.17e+00 - 9.88e-01 1.00e+00f 1\n", + " 3 -1.4522399e+01 4.44e+00 1.61e+01 -1.0 8.94e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -1.4388879e+01 1.20e+01 1.37e+01 -1.0 1.49e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.4491792e+01 1.25e+00 1.12e+00 -1.0 3.05e+01 - 1.00e+00 1.00e+00h 1\n", + " 6 -1.4495171e+01 5.96e-04 9.48e-03 -1.0 1.09e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -1.4495521e+01 8.41e-05 5.34e-03 -2.5 3.10e-01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.4516522e+01 7.21e-01 1.81e+00 -3.8 1.75e+01 - 7.92e-01 1.00e+00h 1\n", + " 9 -1.4530830e+01 4.10e-03 1.85e-02 -3.8 1.06e+00 -4.0 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.4533751e+01 1.58e-03 4.43e-03 -3.8 1.26e+00 -4.5 1.00e+00 1.00e+00h 1\n", + " 11 -1.4540008e+01 1.21e-02 4.52e-02 -3.8 2.29e+00 -5.0 1.00e+00 1.00e+00h 1\n", + " 12 -1.4566257e+01 2.27e-01 7.87e-01 -3.8 9.29e+00 -5.4 1.00e+00 1.00e+00h 1\n", + " 13 -1.4585780e+01 3.91e-02 1.61e-01 -3.8 3.90e+00 -5.0 1.00e+00 1.00e+00h 1\n", + " 14 -1.4657627e+01 8.05e-01 2.88e+00 -3.8 1.70e+01 -5.5 1.00e+00 1.00e+00h 1\n", + " 15 -1.5563505e+01 8.18e+01 2.82e+02 -3.8 4.19e+02 -6.0 2.05e-01 5.14e-01h 1\n", + " 16 -1.5523849e+01 2.68e+01 2.80e+01 -3.8 6.91e+01 -5.5 1.00e+00 1.00e+00h 1\n", + " 17 -1.5802664e+01 2.51e+01 5.48e+00 -3.8 1.72e+02 - 3.49e-01 4.82e-01h 1\n", + " 18 -1.5795220e+01 1.64e+01 1.24e+01 -3.8 1.44e+02 - 1.00e+00 1.00e+00f 1\n", + " 19 -1.5802973e+01 1.78e+00 6.66e-01 -3.8 5.80e+01 - 9.66e-01 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.5792518e+01 6.29e-02 1.87e-02 -3.8 2.10e+01 - 1.00e+00 1.00e+00h 1\n", + " 21 -1.5792562e+01 6.62e-05 7.16e-05 -3.8 6.47e-01 - 1.00e+00 1.00e+00h 1\n", + " 22 -1.5863869e+01 1.39e+00 8.36e-01 -5.7 8.31e+01 - 7.11e-01 1.00e+00f 1\n", + " 23 -1.5877663e+01 1.08e-01 5.90e-02 -5.7 2.81e+01 - 9.75e-01 1.00e+00h 1\n", + " 24 -1.5878910e+01 9.88e-03 7.73e-04 -5.7 8.80e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (330048)\n", + " 25 -1.5878920e+01 2.19e-04 2.60e-06 -5.7 1.33e+00 - 1.00e+00 1.00e+00h 1\n", + " 26 -1.5878920e+01 4.59e-08 3.36e-10 -5.7 1.93e-02 - 1.00e+00 1.00e+00h 1\n", + " 27 -1.5880196e+01 1.15e-03 2.24e-04 -8.6 3.06e+00 - 9.93e-01 1.00e+00f 1\n", + "Reallocating memory for MA57: lfact (362150)\n", + " 28 -1.5880200e+01 1.55e-06 3.86e-08 -8.6 1.13e-01 - 1.00e+00 1.00e+00h 1\n", + " 29 -1.5880200e+01 4.40e-12 1.85e-10 -8.6 1.90e-04 -6.0 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 29\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.5880200473415972e+01 -1.5880200473415972e+01\n", + "Dual infeasibility......: 1.8546108990308775e-10 1.8546108990308775e-10\n", + "Constraint violation....: 4.3998138465894954e-12 4.3998138465894954e-12\n", + "Complementarity.........: 2.5059390926675284e-09 2.5059390926675284e-09\n", + "Overall NLP error.......: 2.5059390926675284e-09 2.5059390926675284e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 30\n", + "Number of objective gradient evaluations = 30\n", + "Number of equality constraint evaluations = 30\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 30\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 29\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.620\n", + "Total CPU secs in NLP function evaluations = 0.028\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.4\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.19e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 5.67e+01 3.85e+02 -1.0 3.19e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 2.60e+01 4.39e+02 -1.0 7.08e+01 - 5.65e-02 9.90e-01h 1\n", + " 3 0.0000000e+00 1.79e-01 1.22e+02 -1.0 1.49e+01 - 5.20e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 7.30e-06 2.37e+02 -1.0 1.13e-01 - 9.91e-01 1.00e+00h 1\n", + " 5 0.0000000e+00 3.41e-13 1.00e-06 -1.0 4.00e-06 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.7701825750354685e-13 3.4106051316484809e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.7701825750354685e-13 3.4106051316484809e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.065\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.9\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 4.70e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 6.35e+02 3.85e+02 -1.0 4.70e+03 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 9.62e+01 5.99e+01 -1.0 9.11e+02 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.26e+00 2.06e+00 -1.0 7.87e+01 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.24e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393774)\n", + " 5 0.0000000e+00 4.55e-13 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.3691704763652764e-13 4.5474735088646412e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.3691704763652764e-13 4.5474735088646412e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.119\n", + "Total CPU secs in NLP function evaluations = 0.002\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.7948727e+01 8.42e+02 1.33e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303016)\n", + " 1 -1.8165832e+01 4.09e+02 4.51e+00 -1.0 2.53e+01 - 9.19e-01 5.02e-01h 1\n", + " 2 -1.8347575e+01 8.18e+00 2.41e-01 -1.0 3.98e+00 - 9.87e-01 1.00e+00f 1\n", + " 3 -1.8279037e+01 6.26e+00 5.74e+00 -1.0 1.03e+02 - 1.00e+00 1.00e+00f 1\n", + " 4 -1.8238859e+01 1.69e+01 3.91e+00 -1.0 1.64e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.8269310e+01 2.31e-01 2.75e-01 -1.0 2.89e+00 - 1.00e+00 1.00e+00h 1\n", + " 6 -1.8270555e+01 5.19e-04 4.57e-04 -1.7 6.60e-01 - 1.00e+00 1.00e+00h 1\n", + " 7 -1.8270805e+01 7.88e-04 6.63e-04 -3.8 5.00e-01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.8271500e+01 9.45e-04 5.28e-04 -5.7 1.71e+00 -4.0 1.00e+00 1.00e+00h 1\n", + " 9 -1.8273229e+01 5.92e-03 1.56e-04 -5.7 4.65e+00 -4.5 1.00e+00 9.14e-01h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.8273631e+01 2.13e-03 1.40e-03 -5.7 3.86e-01 -5.0 1.00e+00 1.00e+00h 1\n", + " 11 -1.8275013e+01 2.11e-02 2.82e-03 -5.7 1.28e+00 -5.4 1.00e+00 1.00e+00h 1\n", + " 12 -1.8275591e+01 3.07e-03 4.00e-04 -5.7 5.07e-01 -5.0 1.00e+00 1.00e+00h 1\n", + " 13 -1.8277354e+01 3.13e-02 4.47e-03 -5.7 1.63e+00 -5.5 1.00e+00 1.00e+00h 1\n", + " 14 -1.8284188e+01 4.35e-01 7.97e-02 -5.7 6.56e+00 -6.0 1.00e+00 1.00e+00h 1\n", + " 15 -1.8339799e+01 2.22e+01 6.67e+00 -5.7 9.02e+01 -6.4 9.31e-01 6.26e-01h 1\n", + "Reallocating memory for MA57: lfact (332105)\n", + " 16 -1.8410872e+01 1.03e+01 2.06e+00 -5.7 1.03e+02 -6.0 1.00e+00 7.09e-01f 1\n", + " 17 -1.8453249e+01 9.48e+00 2.01e+00 -5.7 1.70e+02 -6.5 1.00e+00 1.89e-01f 1\n", + " 18 -1.8469379e+01 8.31e+00 1.68e+00 -5.7 8.57e+01 -7.0 4.45e-01 1.63e-01h 1\n", + " 19 -1.8493566e+01 3.77e+00 6.76e-01 -5.7 3.26e+01 -6.5 1.00e+00 5.98e-01h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.8501400e+01 3.15e+00 5.56e-01 -5.7 5.39e+01 -7.0 1.00e+00 1.78e-01h 1\n", + " 21 -1.8519701e+01 2.67e-01 7.89e-02 -5.7 2.04e+01 -6.6 1.00e+00 1.00e+00f 1\n", + " 22 -1.8524363e+01 3.95e-01 2.25e-01 -5.7 6.41e+01 -7.1 1.00e+00 2.07e-01h 1\n", + " 23 -1.8531234e+01 2.84e-01 2.21e-01 -5.7 1.59e+01 -6.6 1.00e+00 1.00e+00f 1\n", + " 24 -1.8538783e+01 6.75e-01 4.74e-01 -5.7 5.12e+01 -7.1 1.00e+00 3.67e-01h 1\n", + " 25 -1.8551386e+01 4.55e+00 1.63e+00 -5.7 8.00e+01 -7.6 1.00e+00 1.00e+00f 1\n", + " 26 -1.8545090e+01 1.46e-01 1.88e-01 -5.7 2.34e+01 -7.2 1.00e+00 1.00e+00h 1\n", + " 27 -1.8546477e+01 1.85e-01 1.29e-01 -5.7 9.39e+01 -7.6 1.00e+00 5.95e-01h 1\n", + " 28 -1.8546576e+01 2.34e-02 6.92e-03 -5.7 2.28e+01 - 1.00e+00 1.00e+00f 1\n", + " 29 -1.8546954e+01 2.55e-02 9.41e-03 -5.7 2.37e+01 - 1.00e+00 9.61e-01h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -1.8546953e+01 3.64e-05 1.38e-05 -5.7 9.21e-01 - 1.00e+00 1.00e+00h 1\n", + " 31 -1.8546953e+01 3.38e-09 1.41e-09 -5.7 8.86e-03 - 1.00e+00 1.00e+00h 1\n", + " 32 -1.8548230e+01 1.41e-03 1.43e-04 -8.6 1.62e+00 - 9.94e-01 9.96e-01f 1\n", + " 33 -1.8548236e+01 9.75e-08 1.46e-08 -8.6 9.43e-03 - 1.00e+00 1.00e+00h 1\n", + " 34 -1.8548236e+01 9.09e-13 1.54e-13 -8.6 8.26e-07 -8.1 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 34\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.8548235998334103e+01 -1.8548235998334103e+01\n", + "Dual infeasibility......: 1.5371362792196526e-13 1.5371362792196526e-13\n", + "Constraint violation....: 9.0949470177292824e-13 9.0949470177292824e-13\n", + "Complementarity.........: 2.5059035679086560e-09 2.5059035679086560e-09\n", + "Overall NLP error.......: 2.5059035679086560e-09 2.5059035679086560e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 35\n", + "Number of objective gradient evaluations = 35\n", + "Number of equality constraint evaluations = 35\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 35\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 34\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.987\n", + "Total CPU secs in NLP function evaluations = 0.017\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.9\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.94e+01 3.85e+02 -1.0 3.97e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.54e+01 4.20e+01 -1.0 4.92e+01 - 4.94e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 3.23e-01 3.50e+02 -1.0 2.84e+01 - 2.68e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 9.11e-06 2.34e+02 -1.0 1.90e-01 - 9.91e-01 1.00e+00h 1\n", + " 5 0.0000000e+00 3.13e-13 1.00e-06 -1.0 6.34e-06 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.2971319830862150e-13 3.1263880373444408e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.2971319830862150e-13 3.1263880373444408e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.056\n", + "Total CPU secs in NLP function evaluations = 0.001\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.0\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 9.91e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 6.88e+02 3.85e+02 -1.0 9.91e+03 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 9.67e+01 5.99e+01 -1.0 9.63e+02 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.26e+00 2.06e+00 -1.0 7.88e+01 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.24e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393643)\n", + " 5 0.0000000e+00 1.82e-12 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.8189894035458565e-12 1.8189894035458565e-12\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.8189894035458565e-12 1.8189894035458565e-12\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.117\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -2.1041036e+01 8.42e+02 1.22e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303234)\n", + " 1 -2.1145524e+01 4.14e+02 4.86e+00 -1.0 2.53e+01 - 9.57e-01 5.02e-01h 1\n", + " 2 -2.1235982e+01 4.29e+00 1.97e-01 -1.0 3.69e+00 - 9.85e-01 1.00e+00f 1\n", + " 3 -2.1196637e+01 7.15e+00 3.48e+00 -1.0 1.06e+02 - 1.00e+00 1.00e+00f 1\n", + " 4 -2.1182737e+01 1.81e+01 1.49e+00 -1.0 1.80e+02 - 1.00e+00 1.00e+00h 1\n", + " 5 -2.1196659e+01 9.63e-02 8.45e-02 -1.0 1.10e+01 - 1.00e+00 1.00e+00h 1\n", + " 6 -2.1196935e+01 1.22e-04 1.64e-03 -2.5 3.09e-01 - 1.00e+00 1.00e+00h 1\n", + " 7 -2.1197254e+01 5.64e-03 3.71e-04 -3.8 1.98e+00 - 1.00e+00 1.00e+00h 1\n", + " 8 -2.1197442e+01 2.71e-04 1.49e-04 -3.8 9.31e-01 -4.0 1.00e+00 1.00e+00h 1\n", + " 9 -2.1197814e+01 1.12e-03 1.02e-04 -5.7 1.87e+00 -4.5 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -2.1198435e+01 3.31e-03 1.47e-04 -5.7 3.66e+00 -5.0 1.00e+00 8.46e-01h 1\n", + " 11 -2.1198609e+01 2.27e-03 4.45e-04 -5.7 5.23e-01 -5.4 1.00e+00 1.00e+00f 1\n", + " 12 -2.1199224e+01 2.27e-02 1.60e-03 -5.7 1.87e+00 -5.9 1.00e+00 1.00e+00h 1\n", + " 13 -2.1201389e+01 2.97e-01 1.99e-02 -5.7 4.95e+00 -6.4 1.00e+00 1.00e+00h 1\n", + " 14 -2.1215482e+01 2.18e+01 3.79e+00 -5.7 1.77e+02 -6.9 4.71e-01 3.13e-01h 1\n", + " 15r-2.1215482e+01 2.18e+01 9.99e+02 1.3 0.00e+00 -7.3 0.00e+00 2.37e-07R 5\n", + " 16r-2.1215529e+01 2.18e+01 9.65e+03 1.3 7.99e+05 - 1.23e-05 4.88e-07f 1\n", + " 17r-2.1332007e+01 5.50e+00 9.67e+03 1.3 2.14e+04 - 7.52e-05 1.01e-03f 1\n", + " 18 -2.1341179e+01 6.06e+00 2.11e-01 -5.7 1.51e+03 - 9.35e-02 1.40e-02h 1\n", + " 19 -2.1348895e+01 6.45e+00 1.98e-01 -5.7 2.96e+03 - 8.78e-02 6.18e-02h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -2.1348057e+01 6.10e+00 1.44e+00 -5.7 3.35e+02 -7.8 1.00e+00 8.82e-02h 1\n", + " 21 -2.1337687e+01 5.02e+00 8.30e-01 -5.7 2.16e+02 -8.3 1.00e+00 3.48e-01h 1\n", + " 22 -2.1256596e+01 2.16e+01 7.80e+03 -5.7 6.01e+02 -7.9 7.05e-05 3.63e-01h 1\n", + " 23 -2.1229440e+01 6.42e+00 1.69e+03 -5.7 5.09e+01 -3.8 3.31e-01 7.83e-01h 1\n", + " 24 -2.1225416e+01 7.91e-02 1.07e+00 -5.7 4.92e+00 -4.3 1.85e-04 1.00e+00h 1\n", + " 25 -2.1234904e+01 2.01e-01 1.93e-02 -5.7 1.80e+01 -4.8 1.00e+00 1.00e+00h 1\n", + " 26 -2.1254412e+01 9.46e-01 1.46e-02 -5.7 5.59e+01 -5.3 1.00e+00 6.93e-01h 1\n", + " 27 -2.1288915e+01 3.38e+00 4.31e-02 -5.7 1.68e+02 -5.7 1.00e+00 4.35e-01f 1\n", + " 28 -2.1310182e+01 3.91e+00 4.77e-02 -5.7 3.62e+02 -6.2 1.00e+00 1.26e-01f 1\n", + " 29 -2.1313014e+01 6.05e-02 3.51e-02 -5.7 7.03e+00 -6.7 8.65e-02 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (332877)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -2.1314539e+01 1.81e-01 7.17e-02 -5.7 2.17e+01 -7.2 1.00e+00 4.80e-01h 1\n", + " 31 -2.1322536e+01 5.42e+00 6.40e+01 -5.7 1.14e+03 - 4.87e-05 5.01e-02h 2\n", + " 32 -2.1318925e+01 6.40e-02 1.01e+00 -5.7 1.52e+00 - 6.09e-02 1.00e+00h 1\n", + " 33 -2.1319551e+01 1.97e-03 3.61e-04 -5.7 1.05e+00 - 1.00e+00 1.00e+00h 1\n", + " 34 -2.1319541e+01 3.42e-06 6.83e-07 -5.7 2.81e-01 - 1.00e+00 1.00e+00h 1\n", + " 35 -2.1320810e+01 5.55e-03 3.24e-04 -8.6 3.28e+00 - 9.87e-01 9.90e-01h 1\n", + " 36 -2.1320824e+01 1.71e-06 1.42e-07 -8.6 4.12e-02 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (355912)\n", + "Reallocating memory for MA57: lfact (403977)\n", + " 37 -2.1320824e+01 4.55e-13 1.22e-13 -8.6 1.29e-05 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 37\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -2.1320823847268578e+01 -2.1320823847268578e+01\n", + "Dual infeasibility......: 1.2172728103276853e-13 1.2172728103276853e-13\n", + "Constraint violation....: 4.5474735088646412e-13 4.5474735088646412e-13\n", + "Complementarity.........: 2.5059035644843673e-09 2.5059035644843673e-09\n", + "Overall NLP error.......: 2.5059035644843673e-09 2.5059035644843673e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 45\n", + "Number of objective gradient evaluations = 37\n", + "Number of equality constraint evaluations = 45\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 39\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 37\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.000\n", + "Total CPU secs in NLP function evaluations = 0.051\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.1\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.94e+01 3.85e+02 -1.0 3.97e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.54e+01 4.20e+01 -1.0 4.92e+01 - 4.94e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 3.11e-01 3.50e+02 -1.0 2.84e+01 - 2.68e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 8.56e-06 2.34e+02 -1.0 1.82e-01 - 9.91e-01 1.00e+00h 1\n", + " 5 0.0000000e+00 2.97e-13 1.00e-06 -1.0 5.95e-06 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.2308013703147609e-13 2.9665159217984183e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.2308013703147609e-13 2.9665159217984183e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.069\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.1\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 2.03e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 7.92e+02 3.85e+02 -1.0 2.03e+04 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 9.78e+01 5.99e+01 -1.0 1.07e+03 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.27e+00 2.06e+00 -1.0 7.91e+01 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.25e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393908)\n", + " 5 0.0000000e+00 4.55e-13 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.2737367544323206e-13 4.5474735088646412e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.2737367544323206e-13 4.5474735088646412e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.118\n", + "Total CPU secs in NLP function evaluations = 0.001\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -2.3957786e+01 8.42e+02 1.15e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303016)\n", + " 1 -2.4009165e+01 4.16e+02 5.03e+00 -1.0 2.52e+01 - 9.74e-01 5.03e-01h 1\n", + " 2 -2.4053676e+01 2.23e+00 1.57e-01 -1.0 6.82e+00 - 9.83e-01 1.00e+00f 1\n", + " 3 -2.4049598e+01 2.63e-01 1.04e-01 -1.7 2.03e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -2.4031404e+01 5.51e+00 8.43e-01 -1.7 1.66e+02 - 1.00e+00 9.62e-01f 1\n", + " 5 -2.4031794e+01 9.63e+00 1.11e-01 -1.7 1.31e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -2.4034753e+01 9.12e-03 4.93e-03 -1.7 9.57e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -2.4034764e+01 5.23e-06 2.40e-04 -3.8 5.16e-02 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (320596)\n", + " 8 -2.4036325e+01 4.61e-01 4.50e-02 -5.7 2.11e+01 - 8.48e-01 1.00e+00h 1\n", + " 9 -2.4036710e+01 3.52e-04 5.33e-04 -5.7 3.85e-01 -4.0 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -2.4036850e+01 6.00e-04 7.85e-05 -5.7 1.39e+00 -4.5 1.00e+00 1.00e+00h 1\n", + " 11 -2.4036956e+01 2.95e-04 4.05e-04 -5.7 1.52e+00 -5.0 1.00e+00 6.87e-01H 1\n", + " 12 -2.4036981e+01 2.11e-04 1.51e-04 -5.7 1.55e-01 -5.4 1.00e+00 1.00e+00f 1\n", + " 13 -2.4037122e+01 2.08e-03 9.42e-05 -5.7 8.97e-01 -5.9 1.00e+00 1.00e+00h 1\n", + " 14 -2.4037545e+01 1.84e-02 9.37e-04 -5.7 2.52e+00 -6.4 1.00e+00 1.00e+00h 1\n", + " 15 -2.4038911e+01 1.53e-01 1.30e-02 -5.7 7.03e+00 -6.9 1.00e+00 1.00e+00h 1\n", + " 16 -2.4039513e+01 1.71e-02 2.17e-03 -5.7 2.31e+00 -6.4 1.00e+00 1.00e+00h 1\n", + " 17 -2.4041456e+01 2.25e-01 5.40e-02 -5.7 1.12e+01 -6.9 1.00e+00 1.00e+00h 1\n", + " 18 -2.4042770e+01 5.07e-02 2.20e-02 -5.7 5.24e+00 -6.5 1.00e+00 1.00e+00h 1\n", + " 19 -2.4050544e+01 4.39e+00 1.42e+00 -5.7 4.13e+01 -7.0 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -2.4066468e+01 2.01e+00 5.80e-01 -5.7 6.98e+01 -6.5 1.00e+00 1.00e+00h 1\n", + " 21 -2.4093450e+01 6.18e+00 2.10e+00 -5.7 3.11e+02 -7.0 6.69e-01 3.36e-01h 1\n", + " 22 -2.4106017e+01 6.61e+00 2.16e+00 -5.7 7.31e+02 -7.5 1.00e+00 5.80e-02h 1\n", + " 23 -2.4143250e+01 1.35e+01 2.72e+00 -5.7 4.43e+02 -8.0 6.35e-01 3.17e-01h 1\n", + " 24 -2.4148932e+01 1.23e+01 2.04e+00 -5.7 2.31e+02 -8.4 7.03e-01 2.32e-01h 1\n", + " 25 -2.4151239e+01 1.14e+01 1.84e+00 -5.7 1.07e+03 -8.9 2.31e-01 9.52e-02h 1\n", + " 26 -2.4155618e+01 9.19e+00 6.95e-01 -5.7 1.24e+02 - 1.00e+00 7.89e-01f 1\n", + " 27 -2.4153220e+01 4.92e-01 8.71e-02 -5.7 2.88e+01 - 1.00e+00 1.00e+00h 1\n", + " 28 -2.4153102e+01 1.90e-02 1.51e-04 -5.7 1.19e+01 - 1.00e+00 1.00e+00h 1\n", + " 29 -2.4153101e+01 3.02e-05 6.13e-08 -5.7 4.80e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -2.4153101e+01 6.65e-10 1.93e-11 -5.7 2.26e-03 - 1.00e+00 1.00e+00h 1\n", + " 31 -2.4154362e+01 2.69e-02 4.53e-03 -8.6 1.45e+01 - 9.56e-01 1.00e+00f 1\n", + "Reallocating memory for MA57: lfact (345644)\n", + " 32 -2.4154374e+01 4.15e-04 2.66e-06 -8.6 1.84e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (384901)\n", + " 33 -2.4154374e+01 4.16e-07 6.54e-10 -8.6 5.84e-02 - 1.00e+00 1.00e+00h 1\n", + " 34 -2.4154374e+01 9.09e-13 1.27e-13 -8.6 3.70e-05 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 34\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -2.4154373723270826e+01 -2.4154373723270826e+01\n", + "Dual infeasibility......: 1.2724980870831129e-13 1.2724980870831129e-13\n", + "Constraint violation....: 6.9129384604076763e-13 9.0949470177292824e-13\n", + "Complementarity.........: 2.5059035618469615e-09 2.5059035618469615e-09\n", + "Overall NLP error.......: 2.5059035618469615e-09 2.5059035618469615e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 36\n", + "Number of objective gradient evaluations = 35\n", + "Number of equality constraint evaluations = 36\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 35\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 34\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.007\n", + "Total CPU secs in NLP function evaluations = 0.010\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.9\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.24e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 5.76e+01 3.85e+02 -1.0 3.24e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 2.68e+01 4.57e+02 -1.0 7.19e+01 - 5.57e-02 9.90e-01h 1\n", + " 3 0.0000000e+00 1.69e-01 1.26e+02 -1.0 1.53e+01 - 4.90e-01 9.91e-01f 1\n", + " 4 0.0000000e+00 6.46e-06 2.38e+02 -1.0 1.08e-01 - 9.91e-01 1.00e+00h 1\n", + " 5 0.0000000e+00 4.55e-13 1.00e-06 -1.0 3.54e-06 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.9311082636750565e-13 4.5474735088646412e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.9311082636750565e-13 4.5474735088646412e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.071\n", + "Total CPU secs in NLP function evaluations = 0.001\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.0\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 4.10e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 9.98e+02 3.85e+02 -1.0 4.10e+04 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 9.98e+01 5.99e+01 -1.0 1.27e+03 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.29e+00 2.06e+00 -1.0 7.95e+01 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.27e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393985)\n", + " 5 0.0000000e+00 7.28e-12 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 7.2759576141834259e-12 7.2759576141834259e-12\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 7.2759576141834259e-12 7.2759576141834259e-12\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.117\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -2.6831720e+01 8.42e+02 1.11e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303196)\n", + " 1 -2.6856995e+01 4.17e+02 5.11e+00 -1.0 2.52e+01 - 9.83e-01 5.03e-01h 1\n", + " 2 -2.6878754e+01 1.15e+00 1.70e-01 -1.0 1.35e+01 - 9.82e-01 1.00e+00f 1\n", + " 3 -2.6876993e+01 2.41e-01 8.47e-02 -1.7 2.32e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -2.6868640e+01 5.70e+00 3.70e-01 -1.7 1.61e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -2.6869023e+01 8.10e+00 4.89e-02 -1.7 1.19e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -2.6870266e+01 5.51e-03 2.48e-03 -1.7 8.49e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -2.6870267e+01 1.82e-06 9.94e-05 -3.8 3.28e-02 - 1.00e+00 1.00e+00h 1\n", + " 8 -2.6870637e+01 1.33e-01 5.56e-03 -5.7 1.03e+01 - 9.45e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (320847)\n", + " 9 -2.6870673e+01 2.67e-05 3.18e-05 -5.7 3.18e-01 -4.0 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -2.6870728e+01 3.86e-04 3.70e-05 -5.7 1.11e+00 -4.5 1.00e+00 1.00e+00h 1\n", + " 11 -2.6870877e+01 2.81e-03 3.33e-05 -5.7 3.00e+00 -5.0 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (337659)\n", + " 12 -2.6870878e+01 1.08e-04 7.14e-05 -5.7 8.84e-02 -5.4 1.00e+00 1.00e+00h 1\n", + " 13 -2.6870919e+01 1.06e-03 1.84e-05 -5.7 6.15e-01 -5.9 1.00e+00 1.00e+00h 1\n", + " 14 -2.6871022e+01 9.59e-03 1.61e-04 -5.7 1.37e+00 -6.4 1.00e+00 1.00e+00h 1\n", + " 15 -2.6871350e+01 1.01e-01 1.67e-03 -5.7 3.88e+00 -6.9 1.00e+00 1.00e+00h 1\n", + " 16 -2.6872711e+01 2.39e+00 4.78e-02 -5.7 1.44e+01 -7.3 1.00e+00 1.00e+00h 1\n", + " 17 -2.6874054e+01 5.83e-01 1.31e-02 -5.7 1.62e+01 -6.9 1.00e+00 1.00e+00h 1\n", + " 18 -2.6879134e+01 1.19e+01 4.46e-01 -5.7 1.13e+02 -7.4 1.00e+00 5.07e-01h 1\n", + " 19 -2.6887284e+01 4.11e+00 1.69e-01 -5.7 7.00e+01 -7.0 1.00e+00 9.59e-01f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -2.6892052e+01 4.46e+00 1.97e-01 -5.7 1.88e+02 -7.4 1.00e+00 1.65e-01f 1\n", + " 21 -2.6894657e+01 1.59e+00 4.78e-02 -5.7 2.29e+01 -7.9 9.98e-01 8.13e-01f 1\n", + " 22 -2.6895685e+01 1.03e-01 2.03e-03 -5.7 1.09e+01 -7.5 1.00e+00 1.00e+00f 1\n", + " 23 -2.6896013e+01 3.73e-02 4.31e-03 -5.7 8.81e+00 -8.0 1.00e+00 1.00e+00h 1\n", + " 24 -2.6896032e+01 1.65e-03 4.30e-04 -5.7 1.77e+00 -7.5 1.00e+00 1.00e+00h 1\n", + " 25 -2.6896510e+01 5.88e+00 1.25e+00 -5.7 9.18e+01 -8.0 1.00e+00 1.00e+00h 1\n", + " 26 -2.6897324e+01 2.35e+00 1.70e-01 -5.7 8.48e+01 - 1.00e+00 1.00e+00h 1\n", + " 27 -2.6899093e+01 2.49e-01 4.48e-02 -5.7 3.94e+01 - 1.00e+00 9.34e-01h 1\n", + " 28 -2.6898971e+01 7.46e-02 2.34e-03 -5.7 2.43e+01 - 1.00e+00 1.00e+00f 1\n", + " 29 -2.6898969e+01 4.09e-03 9.27e-06 -5.7 5.54e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -2.6898969e+01 2.54e-05 4.67e-08 -5.7 4.39e-01 - 1.00e+00 1.00e+00h 1\n", + " 31 -2.6898969e+01 8.92e-10 1.97e-11 -5.7 2.60e-03 - 1.00e+00 1.00e+00h 1\n", + " 32 -2.6900308e+01 1.16e-01 2.77e-03 -8.6 1.79e+01 - 9.13e-01 9.53e-01f 1\n", + " 33 -2.6900393e+01 3.09e-03 1.29e-04 -8.6 4.98e+00 - 1.00e+00 1.00e+00h 1\n", + " 34 -2.6900395e+01 2.18e-04 1.36e-06 -8.6 1.34e+00 - 1.00e+00 1.00e+00h 1\n", + " 35 -2.6900395e+01 9.33e-07 2.55e-09 -8.6 8.75e-02 - 1.00e+00 1.00e+00h 1\n", + " 36 -2.6900395e+01 1.48e-11 5.99e-14 -8.6 3.48e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 36\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -2.6900395275199973e+01 -2.6900395275199973e+01\n", + "Dual infeasibility......: 5.9903245874163696e-14 5.9903245874163696e-14\n", + "Constraint violation....: 1.4777956636180534e-11 1.4777956636180534e-11\n", + "Complementarity.........: 2.5059038382096498e-09 2.5059038382096498e-09\n", + "Overall NLP error.......: 2.5059038382096498e-09 2.5059038382096498e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 37\n", + "Number of objective gradient evaluations = 37\n", + "Number of equality constraint evaluations = 37\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 37\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 36\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.085\n", + "Total CPU secs in NLP function evaluations = 0.056\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.9\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.53e+00 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.54e-02 3.84e+00 -1.0 1.38e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.22e-04 3.52e+00 -1.0 1.38e-02 - 1.00e+00 9.92e-01h 1\n", + " 4 0.0000000e+00 7.74e-13 1.01e-06 -1.0 1.10e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 7.7449158197850920e-13 7.7449158197850920e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 7.7449158197850920e-13 7.7449158197850920e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.049\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.9\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 8.26e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 1.41e+03 3.85e+02 -1.0 8.26e+04 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.04e+02 5.99e+01 -1.0 1.69e+03 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.33e+00 2.06e+00 -1.0 8.05e+01 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.31e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393607)\n", + " 5 0.0000000e+00 9.09e-13 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0949470177292824e-13 9.0949470177292824e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0949470177292824e-13 9.0949470177292824e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.117\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -2.9638946e+01 8.42e+02 1.08e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303234)\n", + " 1 -2.9651536e+01 4.17e+02 5.15e+00 -1.0 2.52e+01 - 9.87e-01 5.03e-01h 1\n", + " 2 -2.9662204e+01 5.88e-01 1.69e-01 -1.0 1.90e+01 - 9.82e-01 1.00e+00f 1\n", + " 3 -2.9661372e+01 2.28e-01 8.72e-02 -1.7 2.54e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -2.9657366e+01 5.59e+00 1.74e-01 -1.7 1.57e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -2.9657597e+01 7.45e+00 2.43e-02 -1.7 1.12e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -2.9658155e+01 9.80e-03 5.94e-04 -1.7 9.51e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -2.9658157e+01 2.42e-07 4.73e-05 -3.8 1.06e-02 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (324542)\n", + " 8 -2.9658248e+01 2.96e-02 2.07e-03 -5.7 5.06e+00 - 9.80e-01 1.00e+00h 1\n", + " 9 -2.9658255e+01 8.09e-06 1.82e-05 -5.7 1.82e-01 -4.0 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -2.9658269e+01 1.10e-04 1.97e-05 -8.6 5.90e-01 -4.5 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (353780)\n", + " 11 -2.9658314e+01 1.01e-03 1.99e-05 -8.6 1.79e+00 -5.0 1.00e+00 1.00e+00h 1\n", + " 12 -2.9658384e+01 2.94e-03 1.98e-05 -8.6 5.34e+00 -5.4 1.00e+00 5.24e-01h 1\n", + " 13 -2.9658390e+01 2.14e-04 6.05e-05 -8.6 1.92e-01 -5.9 1.00e+00 1.00e+00f 1\n", + " 14 -2.9658414e+01 2.03e-03 1.72e-05 -8.6 7.84e-01 -6.4 1.00e+00 1.00e+00h 1\n", + " 15 -2.9658485e+01 1.95e-02 1.65e-04 -8.6 2.16e+00 -6.9 1.00e+00 1.00e+00h 1\n", + " 16 -2.9658723e+01 2.33e-01 1.94e-03 -8.6 6.13e+00 -7.3 1.00e+00 1.00e+00h 1\n", + " 17 -2.9659707e+01 2.00e+01 4.71e-01 -8.6 4.15e+02 -7.8 2.10e-01 1.32e-01h 1\n", + " 18 -2.9661723e+01 1.08e+01 6.12e+00 -8.6 7.98e+01 -7.4 1.00e+00 5.36e-01h 1\n", + " 19 -2.9662944e+01 1.03e+01 8.86e+00 -8.6 2.03e+02 -7.9 7.79e-01 1.08e-01f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -2.9664685e+01 1.03e+01 1.59e+01 -8.6 1.87e+02 -8.3 1.00e+00 1.08e-01h 1\n", + " 21 -2.9668199e+01 7.67e+00 4.66e+00 -8.6 1.09e+02 -7.9 1.00e+00 5.18e-01h 1\n", + " 22 -2.9669825e+01 7.05e+00 2.58e+01 -8.6 2.15e+02 -8.4 1.00e+00 1.50e-01h 1\n", + " 23 -2.9670730e+01 3.22e+00 2.25e+01 -8.6 3.00e+01 -8.0 4.59e-01 5.76e-01h 1\n", + " 24 -2.9672420e+01 2.48e+01 1.63e+01 -8.6 4.62e+02 -8.4 3.30e-01 2.77e-01f 1\n", + " 25 -2.9671141e+01 6.69e+00 4.35e+00 -8.6 2.59e+01 -8.0 1.00e+00 7.33e-01h 1\n", + " 26 -2.9672562e+01 4.65e-01 2.66e-02 -8.6 2.69e+01 -8.5 1.00e+00 1.00e+00h 1\n", + " 27 -2.9672664e+01 4.46e-01 1.65e-02 -8.6 1.35e+02 -9.0 1.00e+00 4.39e-01h 1\n", + " 28 -2.9672722e+01 1.68e-01 1.61e-03 -8.6 1.93e+01 - 1.00e+00 1.00e+00h 1\n", + " 29 -2.9672724e+01 1.86e-02 5.75e-04 -8.6 2.08e+01 - 1.00e+00 9.29e-01h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -2.9672723e+01 3.34e-06 7.50e-08 -8.6 2.03e-02 - 1.00e+00 1.00e+00h 1\n", + " 31 -2.9672723e+01 1.46e-11 9.19e-14 -8.6 1.60e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 31\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -2.9672723137182857e+01 -2.9672723137182857e+01\n", + "Dual infeasibility......: 9.1912354482113998e-14 9.1912354482113998e-14\n", + "Constraint violation....: 3.6379788070917130e-12 1.4551915228366852e-11\n", + "Complementarity.........: 2.5059038164605147e-09 2.5059038164605147e-09\n", + "Overall NLP error.......: 2.5059038164605147e-09 2.5059038164605147e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 32\n", + "Number of objective gradient evaluations = 32\n", + "Number of equality constraint evaluations = 32\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 32\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 31\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.978\n", + "Total CPU secs in NLP function evaluations = 0.022\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.1\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.94e+01 3.85e+02 -1.0 3.97e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.51e+01 4.19e+01 -1.0 4.93e+01 - 4.96e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.44e-01 3.41e+02 -1.0 2.83e+01 - 2.90e-01 9.94e-01h 1\n", + " 4 0.0000000e+00 2.63e-06 2.33e+02 -1.0 7.93e-02 - 9.91e-01 1.00e+00h 1\n", + " 5 0.0000000e+00 2.49e-13 1.00e-06 -1.0 1.80e-06 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.0318095320003983e-13 2.4868995751603507e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.0318095320003983e-13 2.4868995751603507e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.056\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.9\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.66e+05 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 2.25e+03 3.85e+02 -1.0 1.66e+05 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.12e+02 5.99e+01 -1.0 2.52e+03 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.41e+00 2.06e+00 -1.0 8.23e+01 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.39e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393985)\n", + " 5 0.0000000e+00 2.91e-11 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.9103830456733704e-11 2.9103830456733704e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.9103830456733704e-11 2.9103830456733704e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.109\n", + "Total CPU secs in NLP function evaluations = 0.010\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -3.2428486e+01 8.42e+02 1.05e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303234)\n", + " 1 -3.2434768e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.03e-01h 1\n", + " 2 -3.2440069e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", + " 3 -3.2439664e+01 2.27e-01 8.67e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -3.2437689e+01 5.57e+00 1.18e-01 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -3.2437810e+01 7.15e+00 1.20e-02 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -3.2438076e+01 1.07e-02 4.29e-04 -1.7 9.35e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -3.2438077e+01 3.69e-07 2.81e-05 -3.8 6.32e-03 - 1.00e+00 1.00e+00h 1\n", + " 8 -3.2438078e+01 1.83e-06 7.60e-06 -5.7 7.60e-02 -4.0 1.00e+00 1.00e+00h 1\n", + " 9 -3.2438082e+01 2.75e-05 9.82e-06 -8.6 2.95e-01 -4.5 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -3.2438093e+01 2.57e-04 1.00e-05 -8.6 9.02e-01 -5.0 1.00e+00 1.00e+00h 1\n", + " 11 -3.2438127e+01 2.32e-03 1.00e-05 -8.6 2.71e+00 -5.4 1.00e+00 1.00e+00h 1\n", + " 12 -3.2438152e+01 3.01e-03 1.00e-05 -8.6 8.06e+00 -5.9 1.00e+00 2.49e-01h 1\n", + " 13 -3.2438155e+01 4.60e-04 4.53e-05 -8.6 2.32e-01 -6.4 1.00e+00 1.00e+00f 1\n", + " 14 -3.2438173e+01 4.53e-03 1.89e-05 -8.6 1.27e+00 -6.9 1.00e+00 1.00e+00h 1\n", + " 15 -3.2438225e+01 4.49e-02 1.89e-04 -8.6 3.33e+00 -7.3 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (333035)\n", + " 16 -3.2438415e+01 6.88e-01 2.78e-03 -8.6 9.07e+00 -7.8 1.00e+00 1.00e+00h 1\n", + " 17 -3.2438526e+01 1.13e-01 3.98e-04 -8.6 3.08e+00 -7.4 1.00e+00 1.00e+00h 1\n", + " 18 -3.2438959e+01 3.03e+00 2.02e-02 -8.6 1.89e+01 -7.9 1.00e+00 1.00e+00h 1\n", + " 19 -3.2439451e+01 9.31e-01 4.43e-03 -8.6 2.65e+01 -7.4 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -3.2440256e+01 3.61e+00 2.46e-02 -8.6 2.05e+02 -7.9 9.03e-01 1.92e-01h 1\n", + " 21 -3.2440820e+01 2.54e+00 1.86e-02 -8.6 4.61e+01 -7.5 1.00e+00 4.20e-01f 1\n", + " 22 -3.2443038e+01 8.60e+00 1.12e-01 -8.6 2.61e+02 -8.0 8.80e-01 2.74e-01f 1\n", + " 23 -3.2443317e+01 8.60e+00 1.12e-01 -8.6 1.27e+03 -8.4 5.04e-01 5.61e-03h 1\n", + " 24 -3.2444082e+01 2.43e+00 2.51e-02 -8.6 1.87e+01 -8.0 1.42e-01 7.76e-01h 1\n", + " 25 -3.2444206e+01 2.00e+00 2.04e-02 -8.6 2.84e+01 -8.5 8.77e-01 1.87e-01f 1\n", + " 26 -3.2444524e+01 1.50e-01 2.01e-03 -8.6 1.06e+01 -8.1 1.00e+00 1.00e+00h 1\n", + " 27 -3.2444530e+01 1.48e-01 1.97e-03 -8.6 3.53e+01 -8.5 1.00e+00 1.95e-02h 1\n", + " 28 -3.2444603e+01 2.35e-02 9.67e-04 -8.6 9.31e+00 -8.1 1.00e+00 1.00e+00f 1\n", + " 29 -3.2444869e+01 3.57e+00 1.39e-01 -8.6 8.64e+01 -8.6 1.00e+00 6.85e-01h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -3.2445180e+01 2.78e+00 5.43e-02 -8.6 2.44e+02 -9.1 1.00e+00 6.18e-01f 1\n", + " 31 -3.2445337e+01 1.07e+00 1.03e-02 -8.6 5.66e+01 - 1.00e+00 1.00e+00h 1\n", + " 32 -3.2445329e+01 4.67e-01 4.82e-03 -8.6 9.94e+01 - 1.00e+00 5.69e-01h 1\n", + " 33 -3.2445311e+01 1.99e-03 3.70e-05 -8.6 9.19e-01 - 1.00e+00 1.00e+00h 1\n", + " 34 -3.2445311e+01 1.62e-07 1.29e-09 -8.6 1.08e-02 - 1.00e+00 1.00e+00h 1\n", + " 35 -3.2445311e+01 5.82e-11 2.86e-14 -8.6 7.59e-07 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 35\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -3.2445310986118486e+01 -3.2445310986118486e+01\n", + "Dual infeasibility......: 2.8629516867220174e-14 2.8629516867220174e-14\n", + "Constraint violation....: 2.9103830456733704e-11 5.8207660913467407e-11\n", + "Complementarity.........: 2.5059035597432385e-09 2.5059035597432385e-09\n", + "Overall NLP error.......: 2.5059035597432385e-09 2.5059035597432385e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 36\n", + "Number of objective gradient evaluations = 36\n", + "Number of equality constraint evaluations = 36\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 36\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 35\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.090\n", + "Total CPU secs in NLP function evaluations = 0.019\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.9\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.94e+01 3.85e+02 -1.0 3.97e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.51e+01 4.19e+01 -1.0 4.93e+01 - 4.96e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.70e-02 3.43e+02 -1.0 2.83e+01 - 2.90e-01 9.98e-01h 1\n", + " 4 0.0000000e+00 7.99e-08 2.33e+02 -1.0 4.00e-02 - 9.91e-01 1.00e+00h 1\n", + " 5 0.0000000e+00 2.63e-13 1.00e-06 -1.0 7.80e-08 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.2874055091167042e-13 2.6290081223123707e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.2874055091167042e-13 2.6290081223123707e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.048\n", + "Total CPU secs in NLP function evaluations = 0.031\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.0\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.32e+05 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 3.91e+03 3.85e+02 -1.0 3.32e+05 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.29e+02 5.99e+01 -1.0 4.18e+03 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.58e+00 2.06e+00 -1.0 8.61e+01 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.56e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393774)\n", + " 5 0.0000000e+00 1.82e-12 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.8189894035458565e-12 1.8189894035458565e-12\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.8189894035458565e-12 1.8189894035458565e-12\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.100\n", + "Total CPU secs in NLP function evaluations = 0.017\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -3.5209502e+01 8.42e+02 1.04e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303016)\n", + " 1 -3.5212641e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.03e-01h 1\n", + " 2 -3.5215292e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", + " 3 -3.5215089e+01 2.27e-01 8.53e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -3.5214101e+01 5.57e+00 9.59e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -3.5214161e+01 7.15e+00 6.15e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -3.5214294e+01 1.06e-02 4.14e-04 -1.7 9.34e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -3.5214295e+01 7.51e-07 1.89e-05 -3.8 1.17e-02 - 1.00e+00 1.00e+00h 1\n", + " 8 -3.5214300e+01 1.80e-03 1.13e-05 -5.7 1.27e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (327848)\n", + " 9 -3.5214301e+01 6.97e-07 4.87e-06 -5.7 4.87e-02 -4.0 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -3.5214302e+01 7.02e-06 4.97e-06 -8.6 1.49e-01 -4.5 1.00e+00 1.00e+00h 1\n", + " 11 -3.5214305e+01 6.46e-05 5.02e-06 -8.6 4.52e-01 -5.0 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (348486)\n", + " 12 -3.5214313e+01 5.82e-04 5.03e-06 -8.6 1.36e+00 -5.4 1.00e+00 1.00e+00h 1\n", + " 13 -3.5214337e+01 4.65e-03 5.02e-06 -8.6 4.07e+00 -5.9 1.00e+00 9.40e-01h 1\n", + " 14 -3.5214337e+01 4.50e-03 2.33e-04 -8.6 1.47e+00 -6.4 1.00e+00 3.12e-02f 6\n", + " 15 -3.5214341e+01 1.09e-03 3.28e-05 -8.6 6.15e-01 -6.9 1.00e+00 1.00e+00h 1\n", + " 16 -3.5214353e+01 9.95e-03 2.09e-05 -8.6 1.69e+00 -7.3 1.00e+00 1.00e+00h 1\n", + " 17 -3.5214394e+01 1.07e-01 2.26e-04 -8.6 4.92e+00 -7.8 1.00e+00 1.00e+00h 1\n", + " 18 -3.5214572e+01 3.42e+00 7.71e-03 -8.6 1.68e+01 -8.3 1.00e+00 1.00e+00h 1\n", + " 19 -3.5214789e+01 1.03e+00 2.63e-03 -8.6 2.34e+01 -7.9 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -3.5215305e+01 8.47e+00 3.52e-02 -8.6 5.34e+02 -8.3 2.91e-01 1.01e-01h 1\n", + " 21 -3.5215730e+01 6.31e+00 2.51e-02 -8.6 7.99e+01 -7.9 1.00e+00 3.57e-01f 1\n", + " 22 -3.5216694e+01 1.02e+01 5.63e-02 -8.6 7.73e+02 -8.4 3.55e-01 7.78e-02h 1\n", + " 23 -3.5216797e+01 9.76e+00 5.36e-02 -8.6 1.42e+02 -8.9 1.04e-01 4.84e-02h 1\n", + " 24 -3.5216885e+01 8.53e+00 4.68e-02 -8.6 2.74e+01 -8.4 5.30e-01 1.28e-01h 1\n", + " 25 -3.5217124e+01 6.99e+00 3.66e-02 -8.6 5.63e+01 -8.9 3.25e-01 2.16e-01h 1\n", + " 26 -3.5217494e+01 4.51e-01 1.53e-03 -8.6 1.69e+01 -8.5 1.00e+00 9.89e-01f 1\n", + " 27 -3.5217762e+01 3.34e+01 5.83e-01 -8.6 2.98e+02 -9.0 3.79e-01 5.84e-01f 1\n", + " 28 -3.5217782e+01 3.08e+01 5.37e-01 -8.6 9.68e+01 -8.5 5.35e-01 8.04e-02h 1\n", + " 29 -3.5215828e+01 2.37e+01 4.05e-01 -8.6 4.07e+01 -8.1 2.51e-02 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -3.5216161e+01 1.84e+01 3.12e-01 -8.6 2.64e+01 -8.6 8.03e-01 2.30e-01h 1\n", + " 31 -3.5217419e+01 3.16e+00 2.96e-02 -8.6 2.66e+01 -9.1 1.20e-04 1.00e+00f 1\n", + " 32 -3.5217481e+01 3.12e+00 2.90e-02 -8.6 1.92e+02 -9.6 4.18e-01 3.25e-02h 1\n", + " 33 -3.5217730e+01 3.96e+00 6.86e-02 -8.6 1.96e+02 -10.0 2.65e-07 1.67e-01h 1\n", + " 34 -3.5218174e+01 1.05e+01 5.09e-02 -8.6 6.76e+01 - 4.79e-01 1.00e+00f 1\n", + " 35 -3.5218144e+01 9.56e+00 4.62e-02 -8.6 5.60e+02 - 6.27e-01 9.14e-02h 1\n", + " 36 -3.5217875e+01 1.29e-01 6.80e-03 -8.6 1.14e+01 - 1.00e+00 1.00e+00h 1\n", + " 37 -3.5217899e+01 1.43e-02 9.14e-05 -8.6 2.93e+00 - 1.00e+00 1.00e+00h 1\n", + " 38 -3.5217899e+01 1.12e-05 9.11e-08 -8.6 7.12e-02 - 1.00e+00 1.00e+00h 1\n", + " 39 -3.5217899e+01 5.82e-11 8.02e-14 -8.6 6.30e-05 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 39\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -3.5217898835055578e+01 -3.5217898835055578e+01\n", + "Dual infeasibility......: 8.0154814536833473e-14 8.0154814536833473e-14\n", + "Constraint violation....: 5.8207660913467407e-11 5.8207660913467407e-11\n", + "Complementarity.........: 2.5059035610674620e-09 2.5059035610674620e-09\n", + "Overall NLP error.......: 2.5059035610674620e-09 2.5059035610674620e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 45\n", + "Number of objective gradient evaluations = 40\n", + "Number of equality constraint evaluations = 45\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 40\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 39\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.271\n", + "Total CPU secs in NLP function evaluations = 0.044\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 4.2\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.93e+01 3.85e+02 -1.0 3.97e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.51e+01 4.19e+01 -1.0 4.93e+01 - 4.96e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.18e-01 3.44e+02 -1.0 2.83e+01 - 2.89e-01 1.00e+00h 1\n", + " 4 0.0000000e+00 7.85e-11 1.38e+02 -1.0 8.41e-02 - 9.91e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 7.8493656019418268e-11 7.8493656019418268e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 7.8493656019418268e-11 7.8493656019418268e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.062\n", + "Total CPU secs in NLP function evaluations = 0.003\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.0\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.64e+05 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 7.23e+03 3.85e+02 -1.0 6.64e+05 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.62e+02 5.99e+01 -1.0 7.51e+03 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.91e+00 2.06e+00 -1.0 9.36e+01 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.89e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393697)\n", + " 5 0.0000000e+00 7.28e-12 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 7.2759576141834259e-12 7.2759576141834259e-12\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 7.2759576141834259e-12 7.2759576141834259e-12\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.107\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -3.7986293e+01 8.42e+02 1.03e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303122)\n", + " 1 -3.7987862e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.03e-01h 1\n", + " 2 -3.7989187e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", + " 3 -3.7989086e+01 2.27e-01 8.49e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -3.7988591e+01 5.57e+00 8.46e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -3.7988622e+01 7.15e+00 3.66e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -3.7988688e+01 1.06e-02 4.07e-04 -1.7 9.34e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -3.7988689e+01 1.02e-06 1.41e-05 -3.8 1.65e-02 - 1.00e+00 1.00e+00h 1\n", + " 8 -3.7988690e+01 4.48e-04 5.42e-06 -5.7 6.35e-01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (324149)\n", + " 9 -3.7988690e+01 1.84e-07 2.46e-06 -5.7 2.46e-02 -4.0 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -3.7988690e+01 1.76e-06 2.49e-06 -8.6 7.46e-02 -4.5 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (343489)\n", + " 11 -3.7988691e+01 1.62e-05 2.51e-06 -8.6 2.26e-01 -5.0 1.00e+00 1.00e+00h 1\n", + " 12 -3.7988693e+01 1.46e-04 2.51e-06 -8.6 6.79e-01 -5.4 1.00e+00 1.00e+00h 1\n", + " 13 -3.7988699e+01 1.31e-03 2.51e-06 -8.6 2.04e+00 -5.9 1.00e+00 1.00e+00h 1\n", + " 14 -3.7988708e+01 3.29e-03 2.51e-06 -8.6 6.08e+00 -6.4 1.00e+00 4.72e-01h 1\n", + " 15 -3.7988708e+01 2.88e-03 9.75e-05 -8.6 7.36e-01 -6.9 1.00e+00 1.25e-01f 4\n", + " 16 -3.7988711e+01 2.52e-03 2.24e-05 -8.6 9.76e-01 -7.3 1.00e+00 1.00e+00h 1\n", + " 17 -3.7988721e+01 2.30e-02 2.42e-05 -8.6 2.53e+00 -7.8 1.00e+00 1.00e+00h 1\n", + " 18 -3.7988753e+01 2.83e-01 2.94e-04 -8.6 7.23e+00 -8.3 1.00e+00 1.00e+00h 1\n", + " 19 -3.7988768e+01 4.05e-02 4.12e-05 -8.6 1.67e+00 -7.9 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -3.7988818e+01 6.38e-01 6.02e-04 -8.6 6.87e+00 -8.3 1.00e+00 1.00e+00h 1\n", + " 21 -3.7988847e+01 1.12e-01 1.12e-04 -8.6 4.05e+00 -7.9 1.00e+00 1.00e+00h 1\n", + " 22 -3.7988962e+01 2.54e+00 4.68e-03 -8.6 1.86e+01 -8.4 1.00e+00 1.00e+00h 1\n", + " 23 -3.7989078e+01 7.34e-01 8.67e-04 -8.6 2.48e+01 -8.0 1.00e+00 1.00e+00h 1\n", + " 24 -3.7989232e+01 2.08e+00 3.41e-03 -8.6 1.45e+02 -8.4 1.00e+00 2.09e-01h 1\n", + " 25 -3.7989337e+01 1.43e+00 2.58e-03 -8.6 3.34e+01 -8.0 1.00e+00 4.49e-01f 1\n", + " 26 -3.7989784e+01 5.92e+00 1.95e-02 -8.6 1.61e+02 -8.5 1.00e+00 3.75e-01f 1\n", + " 27 -3.7990124e+01 2.81e+00 9.84e-03 -8.6 4.53e+01 -8.1 1.00e+00 6.70e-01f 1\n", + " 28 -3.7990143e+01 1.18e+00 3.83e-03 -8.6 1.17e+01 -8.5 1.00e+00 6.36e-01f 1\n", + " 29 -3.7990260e+01 5.81e-01 1.51e-03 -8.6 2.24e+01 -9.0 1.00e+00 6.81e-01f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -3.7990293e+01 3.41e-02 1.10e-04 -8.6 6.65e+00 -8.6 1.00e+00 1.00e+00f 1\n", + " 31 -3.7990329e+01 1.38e-01 1.24e-03 -8.6 1.50e+01 -9.1 1.00e+00 1.00e+00h 1\n", + " 32 -3.7990335e+01 1.01e-01 8.90e-04 -8.6 9.80e+00 -8.7 1.00e+00 3.33e-01h 1\n", + " 33 -3.7990393e+01 2.39e+00 2.02e-02 -8.6 5.51e+01 -9.1 1.00e+00 7.80e-01f 1\n", + " 34 -3.7990507e+01 4.27e+00 1.27e-02 -8.6 7.38e+01 -9.6 1.00e+00 7.82e-01h 1\n", + " 35 -3.7990474e+01 4.87e-02 8.37e-04 -8.6 1.92e+01 -9.2 1.00e+00 1.00e+00h 1\n", + " 36 -3.7990485e+01 2.85e-01 3.77e-04 -8.6 6.89e+01 -9.7 1.00e+00 1.00e+00h 1\n", + " 37 -3.7990486e+01 2.82e-01 3.75e-04 -8.6 4.06e+02 -10.1 1.00e+00 1.75e-02h 1\n", + " 38 -3.7990487e+01 5.77e-04 7.14e-07 -8.6 6.52e-01 - 1.00e+00 1.00e+00h 1\n", + " 39 -3.7990487e+01 1.03e-05 3.01e-08 -8.6 4.89e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 40 -3.7990487e+01 1.16e-10 1.03e-13 -8.6 7.76e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 40\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -3.7990486683995655e+01 -3.7990486683995655e+01\n", + "Dual infeasibility......: 1.0349462628516265e-13 1.0349462628516265e-13\n", + "Constraint violation....: 2.5821123017522041e-11 1.1641532182693481e-10\n", + "Complementarity.........: 2.5059035596820578e-09 2.5059035596820578e-09\n", + "Overall NLP error.......: 2.5059035596820578e-09 2.5059035596820578e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 44\n", + "Number of objective gradient evaluations = 41\n", + "Number of equality constraint evaluations = 44\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 41\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 40\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.453\n", + "Total CPU secs in NLP function evaluations = 0.019\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 4.5\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.93e+01 3.85e+02 -1.0 3.97e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.51e+01 4.19e+01 -1.0 4.93e+01 - 4.96e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.16e-01 3.45e+02 -1.0 2.83e+01 - 2.89e-01 1.00e+00h 1\n", + " 4 0.0000000e+00 7.46e-11 6.85e+01 -1.0 8.27e-02 - 9.91e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 7.4614092682168121e-11 7.4614092682168121e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 7.4614092682168121e-11 7.4614092682168121e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.064\n", + "Total CPU secs in NLP function evaluations = 0.001\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.3\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.33e+06 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 1.39e+04 3.85e+02 -1.0 1.33e+06 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 2.29e+02 5.99e+01 -1.0 1.42e+04 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 5.56e+00 2.06e+00 -1.0 1.16e+02 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 4.54e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (394246)\n", + " 5 0.0000000e+00 2.33e-10 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.3283064365386963e-10 2.3283064365386963e-10\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.3283064365386963e-10 2.3283064365386963e-10\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.108\n", + "Total CPU secs in NLP function evaluations = 0.001\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -4.0760980e+01 8.42e+02 1.02e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303234)\n", + " 1 -4.0761764e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.03e-01h 1\n", + " 2 -4.0762427e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", + " 3 -4.0762376e+01 2.27e-01 8.49e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -4.0762129e+01 5.57e+00 7.90e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -4.0762144e+01 7.15e+00 3.34e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -4.0762177e+01 1.06e-02 4.09e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -4.0762177e+01 1.17e-06 1.27e-05 -3.8 1.89e-02 - 1.00e+00 1.00e+00h 1\n", + " 8 -4.0762177e+01 1.11e-04 2.70e-06 -5.7 3.17e-01 - 1.00e+00 1.00e+00h 1\n", + " 9 -4.0762222e+01 5.53e-01 1.52e-03 -5.7 2.86e+01 - 1.00e+00 1.00e+00H 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -4.0762239e+01 1.77e-01 2.60e-03 -5.7 5.61e+01 - 1.00e+00 2.50e-01h 3\n", + " 11 -4.0762241e+01 3.40e-04 3.60e-05 -5.7 3.60e-01 -4.0 1.00e+00 1.00e+00h 1\n", + " 12 -4.0762241e+01 2.26e-07 8.96e-07 -5.7 2.69e-02 -4.5 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (319066)\n", + " 13 -4.0762540e+01 3.15e+01 1.26e-01 -8.6 1.14e+02 - 3.56e-01 1.00e+00h 1\n", + " 14 -4.0763467e+01 6.08e+01 2.43e-01 -8.6 1.29e+04 - 2.22e-02 2.26e-02h 1\n", + " 15 -4.0763784e+01 2.63e+01 3.51e-02 -8.6 1.44e+02 - 4.76e-01 1.00e+00h 1\n", + " 16 -4.0763958e+01 8.50e-01 4.67e-03 -8.6 7.67e+01 - 6.65e-01 1.00e+00h 1\n", + " 17 -4.0764005e+01 2.35e+00 2.00e-03 -8.6 1.84e+02 - 6.15e-01 6.58e-01h 1\n", + " 18 -4.0764023e+01 3.56e-02 1.49e-04 -8.6 5.80e+00 - 1.00e+00 1.00e+00h 1\n", + " 19 -4.0764023e+01 3.49e-05 5.34e-08 -8.6 5.34e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -4.0764023e+01 3.10e-09 4.31e-12 -8.6 5.03e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 20\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -4.0764023387128162e+01 -4.0764023387128162e+01\n", + "Dual infeasibility......: 4.3057665671855009e-12 4.3057665671855009e-12\n", + "Constraint violation....: 3.0985215504486519e-09 3.0985215504486519e-09\n", + "Complementarity.........: 2.5059046339081401e-09 2.5059046339081401e-09\n", + "Overall NLP error.......: 3.0985215504486519e-09 3.0985215504486519e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 25\n", + "Number of objective gradient evaluations = 21\n", + "Number of equality constraint evaluations = 25\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 21\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 20\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.467\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.4\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.25e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 5.78e+01 3.85e+02 -1.0 3.25e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 2.70e+01 4.60e+02 -1.0 7.21e+01 - 5.56e-02 9.90e-01h 1\n", + " 3 0.0000000e+00 1.18e-01 1.30e+02 -1.0 1.54e+01 - 4.83e-01 1.00e+00f 1\n", + " 4 0.0000000e+00 2.62e-10 5.01e+01 -1.0 5.82e-02 - 9.91e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.6183499812759692e-10 2.6183499812759692e-10\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.6183499812759692e-10 2.6183499812759692e-10\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.041\n", + "Total CPU secs in NLP function evaluations = 0.028\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.9\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 2.66e+06 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 2.72e+04 3.85e+02 -1.0 2.66e+06 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 3.62e+02 5.99e+01 -1.0 2.75e+04 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 6.88e+00 2.06e+00 -1.0 2.49e+02 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 5.86e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393945)\n", + " 5 0.0000000e+00 4.66e-10 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 4.6566128730773926e-10 4.6566128730773926e-10\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 4.6566128730773926e-10 4.6566128730773926e-10\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.118\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -4.3535091e+01 8.42e+02 1.01e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303234)\n", + " 1 -4.3535483e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", + " 2 -4.3535814e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", + " 3 -4.3535789e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -4.3535665e+01 5.57e+00 7.62e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -4.3535673e+01 7.15e+00 3.19e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -4.3535690e+01 1.06e-02 4.10e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -4.3535690e+01 1.25e-06 1.39e-05 -3.8 2.01e-02 - 1.00e+00 1.00e+00h 1\n", + " 8 -4.3535690e+01 7.45e-09 4.81e-07 -5.7 4.81e-03 -4.0 1.00e+00 1.00e+00h 1\n", + " 9 -4.3535690e+01 1.10e-07 6.22e-07 -8.6 1.86e-02 -4.5 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -4.3535690e+01 1.02e-06 6.29e-07 -8.6 5.66e-02 -5.0 1.00e+00 1.00e+00h 1\n", + " 11 -4.3535690e+01 9.13e-06 6.29e-07 -8.6 1.70e-01 -5.4 1.00e+00 1.00e+00h 1\n", + " 12 -4.3535690e+01 8.21e-05 6.29e-07 -8.6 5.09e-01 -5.9 1.00e+00 1.00e+00h 1\n", + " 13 -4.3535691e+01 7.36e-04 6.28e-07 -8.6 1.53e+00 -6.4 1.00e+00 1.00e+00h 1\n", + " 14 -4.3535694e+01 4.46e-03 6.23e-07 -8.6 4.53e+00 -6.9 1.00e+00 8.17e-01h 1\n", + " 15 -4.3535694e+01 3.40e-03 2.22e-05 -8.6 1.51e+00 -7.3 1.00e+00 2.50e-01f 3\n", + " 16 -4.3535694e+01 6.98e-05 3.89e-07 -8.6 4.14e-01 -6.9 1.00e+00 1.00e+00h 1\n", + " 17 -4.3535695e+01 1.95e-04 1.16e-07 -8.6 3.13e-01 -7.4 1.00e+00 1.00e+00h 1\n", + " 18 -4.3535695e+01 1.68e-03 4.37e-07 -8.6 7.09e-01 -7.9 1.00e+00 1.00e+00h 1\n", + " 19 -4.3535696e+01 2.35e-04 6.20e-08 -8.6 2.55e-01 -7.4 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (335313)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -4.3535696e+01 2.19e-03 5.77e-07 -8.6 7.85e-01 -7.9 1.00e+00 1.00e+00h 1\n", + " 21 -4.3535699e+01 2.12e-02 5.60e-06 -8.6 2.33e+00 -8.4 1.00e+00 1.00e+00h 1\n", + " 22 -4.3535706e+01 2.57e-01 6.66e-05 -8.6 6.62e+00 -8.9 1.00e+00 1.00e+00h 1\n", + " 23 -4.3535710e+01 3.67e-02 9.31e-06 -8.6 1.58e+00 -8.4 1.00e+00 1.00e+00h 1\n", + " 24 -4.3535722e+01 5.54e-01 1.31e-04 -8.6 6.37e+00 -8.9 1.00e+00 1.00e+00h 1\n", + " 25 -4.3535728e+01 9.45e-02 2.27e-05 -8.6 3.44e+00 -8.5 1.00e+00 1.00e+00h 1\n", + " 26 -4.3535753e+01 1.95e+00 8.61e-04 -8.6 1.60e+01 -9.0 1.00e+00 1.00e+00h 1\n", + " 27 -4.3535776e+01 5.09e-01 1.62e-04 -8.6 1.94e+01 -8.5 1.00e+00 1.00e+00h 1\n", + " 28 -4.3535823e+01 2.99e+00 1.35e-03 -8.6 9.76e+01 -9.0 1.00e+00 3.84e-01h 1\n", + " 29 -4.3535854e+01 1.72e+00 8.11e-04 -8.6 3.16e+01 -8.6 1.00e+00 5.32e-01f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -4.3535966e+01 5.97e+00 4.83e-03 -8.6 1.46e+02 -9.1 1.00e+00 4.18e-01f 1\n", + " 31 -4.3536027e+01 6.62e+00 5.59e-03 -8.6 4.78e+03 -9.6 8.09e-02 5.49e-03h 1\n", + " 32 -4.3536055e+01 5.60e+00 4.51e-03 -8.6 8.68e+01 -10.0 3.39e-01 1.92e-01h 1\n", + " 33 -4.3536080e+01 2.83e+00 2.13e-03 -8.6 2.89e+01 -9.6 8.95e-01 5.28e-01h 1\n", + " 34 -4.3536087e+01 1.67e+00 1.24e-03 -8.6 1.23e+01 -9.2 1.00e+00 4.18e-01f 1\n", + " 35 -4.3536112e+01 1.12e+00 2.09e-03 -8.6 3.20e+01 -9.7 1.00e+00 1.00e+00f 1\n", + " 36 -4.3536116e+01 1.13e+00 2.04e-03 -8.6 9.56e+01 -10.1 1.00e+00 7.04e-02h 1\n", + " 37 -4.3536133e+01 1.45e+00 1.17e-03 -8.6 2.53e+01 -9.7 1.00e+00 1.00e+00f 1\n", + " 38 -4.3536137e+01 7.69e-01 2.36e-04 -8.6 5.00e+01 -10.2 1.00e+00 1.00e+00h 1\n", + " 39 -4.3536139e+01 6.87e-01 2.41e-04 -8.6 5.07e+02 -10.7 6.28e-01 1.78e-01h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 40 -4.3536137e+01 4.16e-02 2.05e-05 -8.6 2.23e+01 - 1.00e+00 1.00e+00h 1\n", + " 41 -4.3536138e+01 2.47e-02 1.88e-05 -8.6 2.36e+01 - 1.00e+00 9.45e-01h 1\n", + " 42 -4.3536138e+01 1.80e-05 1.27e-08 -8.6 6.48e-01 - 1.00e+00 1.00e+00h 1\n", + " 43 -4.3536138e+01 1.79e-09 1.37e-12 -8.6 6.46e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 43\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -4.3536137623477664e+01 -4.3536137623477664e+01\n", + "Dual infeasibility......: 1.3656458789760815e-12 1.3656458789760815e-12\n", + "Constraint violation....: 1.7926176099081204e-09 1.7926176099081204e-09\n", + "Complementarity.........: 2.5060411067492794e-09 2.5060411067492794e-09\n", + "Overall NLP error.......: 2.5060411067492794e-09 2.5060411067492794e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 46\n", + "Number of objective gradient evaluations = 44\n", + "Number of equality constraint evaluations = 46\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 44\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 43\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.281\n", + "Total CPU secs in NLP function evaluations = 0.049\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 4.2\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.96e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.93e+01 3.85e+02 -1.0 3.96e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.51e+01 4.20e+01 -1.0 4.92e+01 - 4.96e-01 9.91e-01h 1\n", + " 3 0.0000000e+00 1.04e-01 3.48e+02 -1.0 2.82e+01 - 2.86e-01 1.00e+00h 1\n", + " 4 0.0000000e+00 5.40e-11 1.66e+01 -1.0 7.42e-02 - 9.91e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.3969273494658410e-11 5.3969273494658410e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.3969273494658410e-11 5.3969273494658410e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.043\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.3\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 5.32e+06 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 5.38e+04 3.85e+02 -1.0 5.32e+06 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 6.28e+02 5.99e+01 -1.0 5.40e+04 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 9.50e+00 2.06e+00 -1.0 5.15e+02 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 8.48e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393530)\n", + " 5 0.0000000e+00 1.16e-10 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.1641532182693481e-10 1.1641532182693481e-10\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.1641532182693481e-10 1.1641532182693481e-10\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", + "Total CPU secs in NLP function evaluations = 0.016\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -4.6308203e+01 8.42e+02 1.01e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303016)\n", + " 1 -4.6308399e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", + " 2 -4.6308565e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", + " 3 -4.6308552e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -4.6308490e+01 5.57e+00 7.50e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -4.6308494e+01 7.15e+00 3.11e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -4.6308502e+01 1.06e-02 4.10e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -4.6308502e+01 1.29e-06 1.45e-05 -3.8 2.07e-02 - 1.00e+00 1.00e+00h 1\n", + " 8 -4.6308502e+01 2.79e-09 2.41e-07 -5.7 2.41e-03 -4.0 1.00e+00 1.00e+00h 1\n", + " 9 -4.6308502e+01 2.70e-08 3.11e-07 -8.6 9.33e-03 -4.5 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -4.6308502e+01 2.54e-07 3.15e-07 -8.6 2.83e-02 -5.0 1.00e+00 1.00e+00h 1\n", + " 11 -4.6308502e+01 2.28e-06 3.14e-07 -8.6 8.49e-02 -5.4 1.00e+00 1.00e+00h 1\n", + " 12 -4.6308503e+01 2.05e-05 3.14e-07 -8.6 2.55e-01 -5.9 1.00e+00 1.00e+00h 1\n", + " 13 -4.6308503e+01 1.84e-04 3.14e-07 -8.6 7.63e-01 -6.4 1.00e+00 1.00e+00h 1\n", + " 14 -4.6308504e+01 1.64e-03 3.12e-07 -8.6 2.28e+00 -6.9 1.00e+00 1.00e+00h 1\n", + " 15 -4.6308505e+01 3.08e-03 3.98e-07 -8.6 6.49e+00 -7.3 1.00e+00 3.96e-01h 1\n", + " 16 -4.6308505e+01 4.06e-04 1.16e-06 -8.6 5.34e-01 -7.8 1.00e+00 1.00e+00f 1\n", + "Reallocating memory for MA57: lfact (332229)\n", + " 17 -4.6308505e+01 3.58e-03 4.28e-07 -8.6 1.71e+00 -8.3 1.00e+00 1.00e+00h 1\n", + " 18 -4.6308507e+01 2.97e-02 3.87e-06 -8.6 2.87e+00 -8.8 1.00e+00 1.00e+00h 1\n", + " 19 -4.6308511e+01 3.82e-01 4.92e-05 -8.6 7.99e+00 -9.2 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -4.6308514e+01 5.59e-02 6.88e-06 -8.6 1.91e+00 -8.8 1.00e+00 1.00e+00h 1\n", + " 21 -4.6308521e+01 9.99e-01 1.26e-04 -8.6 9.30e+00 -9.3 1.00e+00 1.00e+00h 1\n", + " 22 -4.6308527e+01 1.97e-01 3.02e-05 -8.6 7.51e+00 -8.9 1.00e+00 1.00e+00h 1\n", + " 23 -4.6308550e+01 5.83e+00 1.53e-03 -8.6 3.22e+01 -9.4 1.00e+00 1.00e+00h 1\n", + " 24 -4.6308572e+01 3.03e+00 6.36e-04 -8.6 5.12e+01 -8.9 1.00e+00 6.43e-01h 1\n", + " 25 -4.6308591e+01 3.33e+00 7.08e-04 -8.6 1.77e+02 -9.4 1.00e+00 1.29e-01f 1\n", + " 26 -4.6308634e+01 1.98e+00 7.94e-04 -8.6 4.29e+01 -9.0 1.00e+00 1.00e+00f 1\n", + " 27 -4.6308670e+01 2.73e+00 1.19e-03 -8.6 1.96e+02 -9.5 1.00e+00 1.61e-01h 1\n", + " 28 -4.6308683e+01 2.71e+00 1.18e-03 -8.6 2.33e+02 -9.9 1.00e+00 4.70e-02h 1\n", + " 29 -4.6308696e+01 2.50e-01 9.00e-05 -8.6 9.45e+00 -9.5 8.17e-01 9.93e-01h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -4.6308703e+01 2.04e-01 1.41e-04 -8.6 2.34e+01 -10.0 1.00e+00 5.79e-01f 1\n", + " 31 -4.6308703e+01 2.45e-02 1.55e-05 -8.6 6.94e+00 -9.6 1.00e+00 1.00e+00f 1\n", + " 32 -4.6308709e+01 8.74e-01 1.10e-03 -8.6 3.06e+01 -10.0 1.00e+00 9.86e-01h 1\n", + " 33 -4.6308713e+01 4.19e-01 1.63e-04 -8.6 1.23e+01 -9.6 1.00e+00 1.00e+00f 1\n", + " 34 -4.6308722e+01 1.83e+00 9.77e-04 -8.6 3.09e+01 -10.1 1.00e+00 1.00e+00h 1\n", + " 35 -4.6308726e+01 9.33e-01 1.76e-04 -8.6 6.82e+01 -10.6 1.00e+00 1.00e+00h 1\n", + " 36 -4.6308726e+01 8.58e-01 1.24e-04 -8.6 8.39e+02 -11.0 4.07e-01 8.08e-02h 1\n", + " 37 -4.6308725e+01 2.75e-02 9.18e-06 -8.6 1.48e+01 - 1.00e+00 1.00e+00h 1\n", + " 38 -4.6308725e+01 9.21e-03 3.64e-06 -8.6 1.42e+01 - 1.00e+00 1.00e+00h 1\n", + " 39 -4.6308725e+01 2.12e-05 8.21e-09 -8.6 7.01e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 40 -4.6308725e+01 1.86e-09 1.31e-13 -8.6 2.53e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 40\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -4.6308725472442092e+01 -4.6308725472442092e+01\n", + "Dual infeasibility......: 1.3063844228700835e-13 1.3063844228700835e-13\n", + "Constraint violation....: 9.3132257461547852e-10 1.8626451492309570e-09\n", + "Complementarity.........: 2.5059035596850455e-09 2.5059035596850455e-09\n", + "Overall NLP error.......: 2.5059035596850455e-09 2.5059035596850455e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 41\n", + "Number of objective gradient evaluations = 41\n", + "Number of equality constraint evaluations = 41\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 41\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 40\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.242\n", + "Total CPU secs in NLP function evaluations = 0.010\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 4.3\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.96e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.92e+01 3.85e+02 -1.0 3.96e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.51e+01 4.22e+01 -1.0 4.90e+01 - 4.96e-01 9.93e-01h 1\n", + " 3 0.0000000e+00 8.83e-02 3.52e+02 -1.0 2.82e+01 - 2.81e-01 1.00e+00h 1\n", + " 4 0.0000000e+00 3.29e-11 8.00e+00 -1.0 6.29e-02 - 9.91e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 3.2869706956262235e-11 3.2869706956262235e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 3.2869706956262235e-11 3.2869706956262235e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.046\n", + "Total CPU secs in NLP function evaluations = 0.016\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.8\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.06e+07 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 1.07e+05 3.85e+02 -1.0 1.06e+07 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.16e+03 5.99e+01 -1.0 1.07e+05 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.48e+01 2.06e+00 -1.0 1.05e+03 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 1.37e+01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393774)\n", + " 5 0.0000000e+00 2.33e-10 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.3283064365386963e-10 2.3283064365386963e-10\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.3283064365386963e-10 2.3283064365386963e-10\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.100\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -4.9081053e+01 8.42e+02 1.01e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303140)\n", + " 1 -4.9081151e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", + " 2 -4.9081234e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", + " 3 -4.9081228e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -4.9081197e+01 5.57e+00 7.50e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -4.9081199e+01 7.15e+00 3.08e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -4.9081203e+01 1.06e-02 4.10e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -4.9081203e+01 1.31e-06 1.47e-05 -3.8 2.10e-02 - 1.00e+00 1.00e+00h 1\n", + " 8 -4.9081203e+01 1.70e-06 3.33e-07 -5.7 3.92e-02 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (322057)\n", + " 9 -4.9081203e+01 1.16e-02 2.45e-05 -8.6 3.24e+00 - 9.81e-01 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -4.9081203e+01 2.03e-07 2.70e-07 -8.6 2.70e-03 -4.0 1.00e+00 1.00e+00h 1\n", + " 11 -4.9081203e+01 7.45e-09 1.57e-07 -8.6 4.71e-03 -4.5 1.00e+00 1.00e+00h 1\n", + " 12 -4.9081203e+01 6.33e-08 1.57e-07 -8.6 1.41e-02 -5.0 1.00e+00 1.00e+00h 1\n", + " 13 -4.9081203e+01 5.66e-07 1.57e-07 -8.6 4.24e-02 -5.4 1.00e+00 1.00e+00h 1\n", + " 14 -4.9081203e+01 5.09e-06 1.57e-07 -8.6 1.27e-01 -5.9 1.00e+00 1.00e+00h 1\n", + " 15 -4.9081203e+01 4.57e-05 1.57e-07 -8.6 3.81e-01 -6.4 1.00e+00 1.00e+00h 1\n", + " 16 -4.9081203e+01 4.07e-04 1.56e-07 -8.6 1.14e+00 -6.9 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (342868)\n", + " 17 -4.9081204e+01 3.47e-03 1.52e-07 -8.6 3.32e+00 -7.3 1.00e+00 1.00e+00h 1\n", + " 18 -4.9081204e+01 3.11e-03 3.19e-06 -8.6 4.30e+00 -7.8 1.00e+00 1.35e-01h 1\n", + " 19 -4.9081204e+01 7.01e-04 7.88e-07 -8.6 2.87e-01 -8.3 1.00e+00 1.00e+00f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -4.9081205e+01 6.81e-03 4.46e-07 -8.6 1.48e+00 -8.8 1.00e+00 1.00e+00h 1\n", + " 21 -4.9081206e+01 6.91e-02 4.53e-06 -8.6 4.12e+00 -9.2 1.00e+00 1.00e+00h 1\n", + " 22 -4.9081210e+01 1.25e+00 7.64e-05 -8.6 1.05e+01 -9.7 1.00e+00 1.00e+00h 1\n", + " 23 -4.9081212e+01 2.32e-01 1.39e-05 -8.6 6.13e+00 -9.3 1.00e+00 1.00e+00h 1\n", + " 24 -4.9081226e+01 1.28e+01 1.87e-03 -8.6 4.40e+01 -9.8 1.00e+00 1.00e+00h 1\n", + " 25 -4.9081236e+01 9.38e+00 1.30e-03 -8.6 9.23e+01 -9.4 1.00e+00 3.14e-01h 1\n", + " 26 -4.9081256e+01 1.10e+01 1.39e-03 -8.6 4.30e+02 -9.8 6.97e-01 1.11e-01f 1\n", + " 27 -4.9081286e+01 6.05e+00 8.65e-04 -8.6 8.29e+01 -9.4 1.00e+00 6.91e-01f 1\n", + " 28 -4.9081293e+01 1.88e+00 2.33e-04 -8.6 1.25e+01 -9.9 8.83e-01 7.46e-01f 1\n", + " 29 -4.9081301e+01 8.63e-01 1.23e-04 -8.6 2.66e+01 -10.4 1.00e+00 6.71e-01f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -4.9081302e+01 4.34e-02 7.27e-06 -8.6 6.80e+00 -9.9 1.00e+00 1.00e+00f 1\n", + " 31 -4.9081305e+01 2.53e+00 1.66e-03 -8.6 5.25e+01 -10.4 1.00e+00 1.00e+00h 1\n", + " 32 -4.9081319e+01 1.27e+01 2.66e-03 -8.6 1.00e+02 -10.9 1.00e+00 8.30e-01h 1\n", + " 33 -4.9081310e+01 4.58e-01 2.81e-04 -8.6 2.59e+01 -10.5 1.00e+00 1.00e+00h 1\n", + " 34 -4.9081312e+01 3.50e-01 1.29e-04 -8.6 7.29e+01 -10.9 1.00e+00 6.14e-01h 1\n", + " 35 -4.9081313e+01 1.07e-01 8.13e-06 -8.6 1.93e+01 - 1.00e+00 1.00e+00f 1\n", + " 36 -4.9081313e+01 1.48e-02 3.65e-06 -8.6 1.78e+01 - 1.00e+00 1.00e+00h 1\n", + " 37 -4.9081313e+01 5.02e-05 1.04e-08 -8.6 1.08e+00 - 1.00e+00 1.00e+00h 1\n", + " 38 -4.9081313e+01 1.86e-09 3.39e-13 -8.6 6.17e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 38\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -4.9081313321398035e+01 -4.9081313321398035e+01\n", + "Dual infeasibility......: 3.3870588295113704e-13 3.3870588295113704e-13\n", + "Constraint violation....: 1.6578169947933930e-09 1.8626451492309570e-09\n", + "Complementarity.........: 2.5059035596847262e-09 2.5059035596847262e-09\n", + "Overall NLP error.......: 2.5059035596847262e-09 2.5059035596847262e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 39\n", + "Number of objective gradient evaluations = 39\n", + "Number of equality constraint evaluations = 39\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 39\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 38\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.199\n", + "Total CPU secs in NLP function evaluations = 0.015\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 4.1\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.94e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.91e+01 3.85e+02 -1.0 3.94e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.52e+01 4.24e+01 -1.0 4.87e+01 - 4.96e-01 9.95e-01h 1\n", + " 3 0.0000000e+00 5.64e-02 3.60e+02 -1.0 2.82e+01 - 2.71e-01 1.00e+00h 1\n", + " 4 0.0000000e+00 8.55e-12 3.70e+00 -1.0 4.01e-02 - 9.91e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 8.5549345385516062e-12 8.5549345385516062e-12\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 8.5549345385516062e-12 8.5549345385516062e-12\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.053\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.0\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 2.13e+07 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 2.13e+05 3.85e+02 -1.0 2.13e+07 - 2.54e-03 9.90e-01h 1\n", + " 2 0.0000000e+00 2.22e+03 5.99e+01 -1.0 2.14e+05 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 2.53e+01 2.06e+00 -1.0 2.11e+03 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 2.42e+01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393985)\n", + " 5 0.0000000e+00 4.66e-10 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 4.6566128730773926e-10 4.6566128730773926e-10\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 4.6566128730773926e-10 4.6566128730773926e-10\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.110\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -5.1853772e+01 8.42e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303252)\n", + " 1 -5.1853821e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", + " 2 -5.1853862e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", + " 3 -5.1853859e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -5.1853844e+01 5.57e+00 7.49e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -5.1853845e+01 7.15e+00 3.09e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -5.1853847e+01 1.06e-02 4.11e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -5.1853847e+01 1.32e-06 1.49e-05 -3.8 2.12e-02 - 1.00e+00 1.00e+00h 1\n", + " 8 -5.1853847e+01 7.45e-09 6.02e-08 -5.7 6.02e-04 -4.0 1.00e+00 1.00e+00h 1\n", + " 9 -5.1853847e+01 7.45e-09 7.78e-08 -8.6 2.33e-03 -4.5 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -5.1853847e+01 1.86e-08 7.86e-08 -8.6 7.08e-03 -5.0 1.00e+00 1.00e+00h 1\n", + " 11 -5.1853847e+01 1.42e-07 7.86e-08 -8.6 2.12e-02 -5.4 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (327115)\n", + " 12 -5.1853847e+01 1.28e-06 7.86e-08 -8.6 6.37e-02 -5.9 1.00e+00 1.00e+00h 1\n", + " 13 -5.1853847e+01 1.15e-05 7.85e-08 -8.6 1.91e-01 -6.4 1.00e+00 1.00e+00h 1\n", + " 14 -5.1853847e+01 1.03e-04 7.82e-08 -8.6 5.70e-01 -6.9 1.00e+00 1.00e+00h 1\n", + " 15 -5.1853847e+01 8.97e-04 7.70e-08 -8.6 1.68e+00 -7.3 1.00e+00 1.00e+00h 1\n", + " 16 -5.1853847e+01 1.50e-03 1.82e-07 -8.6 4.60e+00 -7.8 1.00e+00 3.75e-01h 2\n", + " 17 -5.1853847e+01 1.53e-03 1.58e-06 -8.6 6.12e+00 -8.3 1.00e+00 1.44e-01h 2\n", + " 18 -5.1853847e+01 1.32e-03 1.57e-06 -8.6 1.69e+00 -8.8 1.00e+00 4.38e-01h 2\n", + "Reallocating memory for MA57: lfact (345657)\n", + " 19 -5.1853848e+01 1.70e-02 1.12e-06 -8.6 2.40e+00 -9.2 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -5.1853848e+01 1.75e-01 5.67e-06 -8.6 6.21e+00 -9.7 1.00e+00 1.00e+00h 1\n", + " 21 -5.1853853e+01 1.96e+01 1.30e-03 -8.6 4.77e+01 -10.2 1.00e+00 1.00e+00h 1\n", + " 22 -5.1853862e+01 1.44e+01 8.05e-04 -8.6 1.50e+02 -9.8 1.00e+00 4.12e-01h 1\n", + " 23 -5.1853873e+01 1.77e+01 9.19e-04 -8.6 4.63e+03 -10.3 6.40e-02 1.19e-02h 1\n", + " 24 -5.1853883e+01 1.46e+01 4.64e-02 -8.6 1.25e+02 -10.7 1.00e+00 2.45e-01h 1\n", + " 25 -5.1853894e+01 1.55e+00 9.50e-01 -8.6 3.86e+01 -10.3 6.08e-02 1.00e+00h 1\n", + " 26 -5.1853897e+01 3.12e-01 2.79e-01 -8.6 5.18e+01 -10.8 1.00e+00 7.06e-01H 1\n", + " 27 -5.1853898e+01 3.16e-01 5.81e-05 -8.6 1.47e+01 -10.4 1.00e+00 1.00e+00f 1\n", + " 28 -5.1853901e+01 2.65e+00 3.38e-04 -8.6 3.87e+01 -10.8 1.00e+00 1.00e+00h 1\n", + " 29 -5.1853901e+01 1.04e+00 9.14e-05 -8.6 1.20e+02 -11.3 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -5.1853901e+01 1.15e-02 1.39e-06 -8.6 1.55e+00 - 1.00e+00 1.00e+00h 1\n", + " 31 -5.1853901e+01 4.45e-06 6.79e-10 -8.6 1.25e-01 - 1.00e+00 1.00e+00h 1\n", + " 32 -5.1853901e+01 3.73e-09 2.52e-14 -8.6 2.14e-05 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 32\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -5.1853901170234600e+01 -5.1853901170234600e+01\n", + "Dual infeasibility......: 2.5166233628543340e-14 2.5166233628543340e-14\n", + "Constraint violation....: 4.6566128730773926e-10 3.7252902984619141e-09\n", + "Complementarity.........: 2.5059035596808703e-09 2.5059035596808703e-09\n", + "Overall NLP error.......: 2.5059035596808703e-09 3.7252902984619141e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 44\n", + "Number of objective gradient evaluations = 33\n", + "Number of equality constraint evaluations = 44\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 33\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 32\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.946\n", + "Total CPU secs in NLP function evaluations = 0.023\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.8\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.92e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.89e+01 3.85e+02 -1.0 3.92e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.52e+01 4.30e+01 -1.0 4.82e+01 - 4.96e-01 1.00e+00h 1\n", + " 3 0.0000000e+00 4.26e-04 3.75e+02 -1.0 2.80e+01 - 2.55e-01 1.00e+00h 1\n", + " 4 0.0000000e+00 2.52e-13 3.50e+00 -1.0 1.87e-04 - 9.91e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.0465496681718326e-13 2.5224267119483557e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.0465496681718326e-13 2.5224267119483557e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.047\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.8\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 4.26e+07 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 4.26e+05 3.85e+02 -1.0 4.26e+07 - 2.54e-03 9.90e-01h 1\n", + " 2 0.0000000e+00 4.35e+03 5.99e+01 -1.0 4.26e+05 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.63e+01 2.06e+00 -1.0 4.24e+03 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 4.52e+01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393567)\n", + " 5 0.0000000e+00 9.31e-10 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.3132257461547852e-10 9.3132257461547852e-10\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.3132257461547852e-10 9.3132257461547852e-10\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.113\n", + "Total CPU secs in NLP function evaluations = 0.002\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -5.4626425e+01 8.42e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303140)\n", + " 1 -5.4626450e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", + " 2 -5.4626470e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", + " 3 -5.4626469e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -5.4626461e+01 5.57e+00 7.49e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -5.4626462e+01 7.15e+00 3.09e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -5.4626463e+01 1.06e-02 4.11e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -5.4626463e+01 1.33e-06 1.50e-05 -3.8 2.12e-02 - 1.00e+00 1.00e+00h 1\n", + " 8 -5.4626463e+01 7.45e-09 3.01e-08 -5.7 3.01e-04 -4.0 1.00e+00 1.00e+00h 1\n", + " 9 -5.4626463e+01 7.31e-04 4.97e-06 -8.6 8.12e-01 - 9.96e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (323556)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -5.4626463e+01 7.45e-09 3.51e-08 -8.6 1.05e-03 -4.5 1.00e+00 1.00e+00h 1\n", + " 11 -5.4626463e+01 7.45e-09 3.93e-08 -8.6 3.53e-03 -5.0 1.00e+00 1.00e+00h 1\n", + " 12 -5.4626463e+01 3.73e-08 3.93e-08 -8.6 1.06e-02 -5.4 1.00e+00 1.00e+00h 1\n", + " 13 -5.4626463e+01 3.20e-07 3.92e-08 -8.6 3.18e-02 -5.9 1.00e+00 1.00e+00h 1\n", + " 14 -5.4626463e+01 2.87e-06 3.92e-08 -8.6 9.52e-02 -6.4 1.00e+00 1.00e+00h 1\n", + " 15 -5.4626463e+01 2.56e-05 3.90e-08 -8.6 2.84e-01 -6.9 1.00e+00 1.00e+00h 1\n", + " 16 -5.4626463e+01 2.24e-04 3.85e-08 -8.6 8.42e-01 -7.3 1.00e+00 1.00e+00h 1\n", + " 17 -5.4626463e+01 1.80e-03 3.64e-08 -8.6 2.39e+00 -7.8 1.00e+00 1.00e+00h 1\n", + " 18 -5.4626463e+01 1.75e-03 4.42e-07 -8.6 4.44e+00 -8.3 1.00e+00 2.55e-01h 2\n", + " 19 -5.4626463e+01 1.22e-03 7.37e-07 -8.6 1.65e+00 -8.8 1.00e+00 4.26e-01h 2\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -5.4626463e+01 3.93e-03 2.60e-07 -8.6 1.20e+00 -9.2 1.00e+00 1.00e+00h 1\n", + " 21 -5.4626463e+01 3.61e-02 5.90e-07 -8.6 3.14e+00 -9.7 1.00e+00 1.00e+00h 1\n", + " 22 -5.4626464e+01 4.44e-01 7.03e-06 -8.6 8.61e+00 -10.2 1.00e+00 1.00e+00h 1\n", + " 23 -5.4626464e+01 6.25e-02 9.33e-07 -8.6 2.01e+00 -9.8 1.00e+00 1.00e+00h 1\n", + " 24 -5.4626465e+01 1.08e+00 1.99e-05 -8.6 9.96e+00 -10.3 1.00e+00 1.00e+00h 1\n", + " 25 -5.4626466e+01 2.09e-01 4.63e-06 -8.6 8.06e+00 -9.8 1.00e+00 1.00e+00h 1\n", + " 26 -5.4626469e+01 6.30e+00 2.33e-04 -8.6 3.45e+01 -10.3 1.00e+00 1.00e+00h 1\n", + " 27 -5.4626473e+01 1.97e+00 5.73e-05 -8.6 4.80e+01 -9.9 1.00e+00 1.00e+00h 1\n", + " 28 -5.4626482e+01 8.48e+00 4.36e-04 -8.6 1.83e+02 -10.4 1.00e+00 4.10e-01h 1\n", + " 29 -5.4626483e+01 7.19e+00 3.68e-04 -8.6 3.53e+01 -10.8 1.00e+00 1.56e-01f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -5.4626486e+01 2.22e-01 2.33e-05 -8.6 6.24e+00 -10.4 5.58e-01 1.00e+00f 1\n", + " 31 -5.4626486e+01 1.20e-01 1.81e-05 -8.6 1.55e+01 -10.9 1.00e+00 1.00e+00h 1\n", + " 32 -5.4626486e+01 1.29e-02 4.92e-07 -8.6 3.51e+00 -10.5 1.00e+00 1.00e+00h 1\n", + " 33 -5.4626487e+01 2.08e-01 3.17e-05 -8.6 1.49e+01 -10.9 1.00e+00 1.00e+00h 1\n", + " 34 -5.4626487e+01 8.32e-02 7.55e-06 -8.6 7.39e+00 -10.5 1.00e+00 1.00e+00h 1\n", + " 35 -5.4626488e+01 2.61e+00 2.60e-04 -8.6 4.11e+01 -11.0 1.00e+00 1.00e+00h 1\n", + " 36 -5.4626489e+01 2.50e+00 5.32e-05 -8.6 7.17e+01 -11.5 1.00e+00 1.00e+00h 1\n", + " 37 -5.4626489e+01 1.89e+00 4.09e-05 -8.6 2.88e+02 -11.9 1.00e+00 2.43e-01h 1\n", + " 38 -5.4626489e+01 1.35e-02 1.91e-06 -8.6 1.74e+01 - 1.00e+00 1.00e+00h 1\n", + " 39 -5.4626489e+01 2.25e-03 9.41e-08 -8.6 6.88e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 40 -5.4626489e+01 1.34e-06 5.72e-11 -8.6 1.71e-01 - 1.00e+00 1.00e+00h 1\n", + " 41 -5.4626489e+01 1.49e-08 2.51e-14 -8.6 1.61e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 41\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -5.4626489018506717e+01 -5.4626489018506717e+01\n", + "Dual infeasibility......: 2.5104508813183050e-14 2.5104508813183050e-14\n", + "Constraint violation....: 9.3132257461547852e-10 1.4901161193847656e-08\n", + "Complementarity.........: 2.5059035596801395e-09 2.5059035596801395e-09\n", + "Overall NLP error.......: 2.5059035596801395e-09 1.4901161193847656e-08\n", + "\n", + "\n", + "Number of objective function evaluations = 48\n", + "Number of objective gradient evaluations = 42\n", + "Number of equality constraint evaluations = 48\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 42\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 41\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.229\n", + "Total CPU secs in NLP function evaluations = 0.069\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.5\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.86e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.85e+01 3.85e+02 -1.0 3.86e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.41e+01 4.32e+01 -1.0 4.70e+01 - 4.96e-01 1.00e+00h 1\n", + " 3 0.0000000e+00 4.19e-04 3.70e+02 -1.0 2.74e+01 - 2.54e-01 1.00e+00h 1\n", + " 4 0.0000000e+00 3.41e-13 3.45e+00 -1.0 1.89e-04 - 9.91e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.4150530724576891e-13 3.4106051316484809e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.4150530724576891e-13 3.4106051316484809e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.047\n", + "Total CPU secs in NLP function evaluations = 0.002\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.0\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 8.51e+07 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 8.52e+05 3.85e+02 -1.0 8.51e+07 - 2.54e-03 9.90e-01h 1\n", + " 2 0.0000000e+00 8.61e+03 5.99e+01 -1.0 8.52e+05 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 8.83e+01 2.06e+00 -1.0 8.49e+03 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 8.73e+01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (394288)\n", + " 5 0.0000000e+00 4.66e-10 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 4.6566128730773926e-10 4.6566128730773926e-10\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 4.6566128730773926e-10 4.6566128730773926e-10\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.090\n", + "Total CPU secs in NLP function evaluations = 0.015\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -5.7399046e+01 8.42e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303434)\n", + " 1 -5.7399058e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", + " 2 -5.7399068e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", + " 3 -5.7399068e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -5.7399064e+01 5.57e+00 7.49e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -5.7399064e+01 7.15e+00 3.09e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -5.7399065e+01 1.06e-02 4.11e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -5.7399065e+01 1.33e-06 1.50e-05 -3.8 2.13e-02 - 1.00e+00 1.00e+00h 1\n", + " 8 -5.7399065e+01 1.49e-08 1.51e-08 -5.7 1.51e-04 -4.0 1.00e+00 1.00e+00h 1\n", + " 9 -5.7399065e+01 2.98e-08 1.94e-08 -8.6 5.83e-04 -4.5 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -5.7399065e+01 1.49e-08 1.97e-08 -9.0 1.77e-03 -5.0 1.00e+00 1.00e+00h 1\n", + " 11 -5.7399065e+01 2.98e-08 1.97e-08 -9.0 5.31e-03 -5.4 1.00e+00 1.00e+00h 1\n", + " 12 -5.7399065e+01 8.94e-08 1.97e-08 -9.0 1.59e-02 -5.9 1.00e+00 1.00e+00h 1\n", + " 13 -5.7399065e+01 7.15e-07 1.96e-08 -9.0 4.77e-02 -6.4 1.00e+00 1.00e+00h 1\n", + " 14 -5.7399065e+01 6.48e-06 1.96e-08 -9.0 1.43e-01 -6.9 1.00e+00 1.00e+00h 1\n", + " 15 -5.7399065e+01 5.78e-05 1.95e-08 -9.0 4.27e-01 -7.3 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (333906)\n", + " 16 -5.7399065e+01 5.04e-04 1.92e-08 -9.0 1.26e+00 -7.8 1.00e+00 1.00e+00h 1\n", + " 17 -5.7399065e+01 3.89e-03 1.78e-08 -9.0 3.51e+00 -8.3 1.00e+00 1.00e+00h 1\n", + " 18 -5.7399065e+01 3.09e-03 5.12e-07 -9.0 1.50e+00 -8.8 1.00e+00 2.16e-01h 2\n", + " 19 -5.7399065e+01 9.36e-04 9.20e-08 -9.0 5.07e-01 -9.2 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -5.7399065e+01 8.67e-03 7.11e-08 -9.0 1.60e+00 -9.7 1.00e+00 1.00e+00h 1\n", + " 21 -5.7399065e+01 8.90e-02 7.27e-07 -9.0 4.66e+00 -10.2 1.00e+00 1.00e+00h 1\n", + " 22 -5.7399065e+01 1.83e+00 1.34e-05 -9.0 1.12e+01 -10.7 1.00e+00 1.00e+00h 1\n", + " 23 -5.7399066e+01 3.81e-01 3.61e-06 -9.0 1.00e+01 -10.3 1.00e+00 1.00e+00h 1\n", + " 24 -5.7399069e+01 8.09e+00 2.59e-04 -9.0 7.69e+01 -10.7 9.52e-01 6.16e-01H 1\n", + " 25 -5.7399072e+01 3.81e+00 1.19e-04 -9.0 5.43e+01 -10.3 1.00e+00 7.55e-01f 1\n", + " 26 -5.7399075e+01 4.33e+00 1.32e-04 -9.0 2.22e+02 -10.8 1.00e+00 1.41e-01f 1\n", + " 27 -5.7399076e+01 1.97e+00 3.29e-05 -9.0 2.73e+01 -11.3 8.89e-01 7.54e-01f 1\n", + " 28 -5.7399077e+01 1.75e-01 1.41e-06 -9.0 1.54e+01 -10.8 1.00e+00 1.00e+00f 1\n", + " 29 -5.7399077e+01 9.22e-01 7.02e-05 -9.0 3.11e+01 -11.3 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -5.7399077e+01 4.41e-01 1.26e-05 -9.0 1.65e+01 -10.9 1.00e+00 1.00e+00h 1\n", + " 31 -5.7399078e+01 2.23e+00 7.37e-05 -9.0 3.40e+01 -11.4 1.00e+00 1.00e+00h 1\n", + " 32 -5.7399078e+01 6.05e-01 8.47e-06 -9.0 8.14e+01 -11.8 1.00e+00 1.00e+00h 1\n", + " 33 -5.7399078e+01 5.73e-01 8.79e-06 -9.0 3.08e+02 -12.3 1.00e+00 1.64e-01h 1\n", + " 34 -5.7399078e+01 1.59e-02 1.89e-07 -9.0 1.07e+01 - 1.00e+00 1.00e+00h 1\n", + " 35 -5.7399078e+01 4.00e-04 1.26e-08 -9.0 2.96e+00 - 1.00e+00 1.00e+00h 1\n", + " 36 -5.7399078e+01 4.81e-08 1.16e-12 -9.0 3.27e-02 - 1.00e+00 1.00e+00h 1\n", + " 37 -5.7399078e+01 1.49e-08 9.10e-15 -9.0 5.66e-06 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 37\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -5.7399077980658021e+01 -5.7399077980658021e+01\n", + "Dual infeasibility......: 9.1002942970123576e-15 9.1002942970123576e-15\n", + "Constraint violation....: 1.8626451492309570e-09 1.4901161193847656e-08\n", + "Complementarity.........: 9.0909090909093753e-10 9.0909090909093753e-10\n", + "Overall NLP error.......: 1.8626451492309570e-09 1.4901161193847656e-08\n", + "\n", + "\n", + "Number of objective function evaluations = 40\n", + "Number of objective gradient evaluations = 38\n", + "Number of equality constraint evaluations = 40\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 38\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 37\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.064\n", + "Total CPU secs in NLP function evaluations = 0.031\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.8\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.89e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.87e+01 3.85e+02 -1.0 3.89e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.47e+01 4.31e+01 -1.0 4.76e+01 - 4.96e-01 1.00e+00h 1\n", + " 3 0.0000000e+00 4.23e-04 3.73e+02 -1.0 2.77e+01 - 2.54e-01 1.00e+00h 1\n", + " 4 0.0000000e+00 2.79e-13 3.48e+00 -1.0 1.88e-04 - 9.91e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.2337636129035084e-13 2.7888802378583932e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.2337636129035084e-13 2.7888802378583932e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.063\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.8\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.70e+08 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 1.70e+06 3.85e+02 -1.0 1.70e+08 - 2.54e-03 9.90e-01h 1\n", + " 2 0.0000000e+00 1.71e+04 5.99e+01 -1.0 1.70e+06 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.72e+02 2.06e+00 -1.0 1.70e+04 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 1.71e+02 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393530)\n", + " 5 0.0000000e+00 4.55e-13 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 5\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.3691704763652764e-13 4.5474735088646412e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.3691704763652764e-13 4.5474735088646412e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 6\n", + "Number of objective gradient evaluations = 6\n", + "Number of equality constraint evaluations = 6\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 6\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 5\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.115\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -6.0171651e+01 8.42e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303252)\n", + " 1 -6.0171657e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", + " 2 -6.0171662e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", + " 3 -6.0171662e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -6.0171660e+01 5.57e+00 7.49e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -6.0171660e+01 7.15e+00 3.09e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -6.0171660e+01 1.06e-02 4.11e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -6.0171660e+01 1.34e-06 1.50e-05 -3.8 2.13e-02 - 1.00e+00 1.00e+00h 1\n", + " 8 -6.0171660e+01 2.98e-08 2.02e-08 -5.7 1.92e-03 - 1.00e+00 1.00e+00h 1\n", + " 9 -6.0171660e+01 4.53e-05 2.16e-08 -8.6 2.02e-01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (326464)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -6.0171660e+01 2.98e-08 9.06e-09 -8.6 9.06e-05 -4.0 1.00e+00 1.00e+00h 1\n", + " 11 -6.0171660e+01 3.73e-09 9.82e-09 -8.6 2.94e-04 -4.5 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 11\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -6.0171659998267266e+01 -6.0171659998267266e+01\n", + "Dual infeasibility......: 9.8158985796691289e-09 9.8158985796691289e-09\n", + "Constraint violation....: 3.7252902984619141e-09 3.7252902984619141e-09\n", + "Complementarity.........: 2.5059035655454891e-09 2.5059035655454891e-09\n", + "Overall NLP error.......: 9.8158985796691289e-09 9.8158985796691289e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 12\n", + "Number of objective gradient evaluations = 12\n", + "Number of equality constraint evaluations = 12\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 12\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 11\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.265\n", + "Total CPU secs in NLP function evaluations = 0.001\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.1\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.92e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.55e+01 3.85e+02 -1.0 1.92e+02 - 2.55e-03 9.95e-01f 1\n", + " 2 0.0000000e+00 1.07e+01 4.90e+01 -1.0 1.71e+01 - 2.60e-01 1.00e+00h 1\n", + " 3 0.0000000e+00 4.19e-05 2.46e+01 -1.0 7.37e+00 - 5.90e-01 1.00e+00h 1\n", + " 4 0.0000000e+00 2.27e-13 1.48e-01 -1.0 3.16e-05 - 9.91e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.1202303897534080e-13 2.2737367544323206e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.1202303897534080e-13 2.2737367544323206e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.057\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.9\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.40e+08 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 3.40e+06 3.85e+02 -1.0 3.40e+08 - 2.54e-03 9.90e-01h 1\n", + " 2 0.0000000e+00 3.41e+04 5.99e+01 -1.0 3.41e+06 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 3.40e+02 2.06e+00 -1.0 3.40e+04 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.39e+02 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393908)\n", + " 5 0.0000000e+00 1.49e-08 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", + " 6 0.0000000e+00 4.55e-13 1.84e-11 -5.7 1.49e-08 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 6\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.8851038199681022e-14 4.5474735088646412e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.8851038199681022e-14 4.5474735088646412e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 7\n", + "Number of objective gradient evaluations = 7\n", + "Number of equality constraint evaluations = 7\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 7\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 6\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.120\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -6.2944244e+01 8.42e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303034)\n", + " 1 -6.2944247e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", + " 2 -6.2944250e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", + " 3 -6.2944249e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -6.2944249e+01 5.57e+00 7.49e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -6.2944249e+01 7.15e+00 3.09e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -6.2944249e+01 1.06e-02 4.11e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -6.2944249e+01 1.31e-06 1.50e-05 -3.8 2.13e-02 - 1.00e+00 1.00e+00h 1\n", + " 8 -6.2944249e+01 5.96e-08 1.04e-08 -5.7 6.73e-04 - 1.00e+00 1.00e+00h 1\n", + " 9 -6.2944249e+01 1.13e-05 1.08e-08 -8.6 1.01e-01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (326761)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -6.2944249e+01 5.96e-08 4.72e-09 -8.6 4.72e-05 -4.0 1.00e+00 1.00e+00h 1\n", + " 11 -6.2944249e+01 5.96e-08 4.91e-09 -9.0 1.47e-04 -4.5 1.00e+00 1.00e+00h 1\n", + " 12 -6.2944249e+01 1.49e-08 4.91e-09 -9.0 4.42e-04 -5.0 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 12\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -6.2944248720059186e+01 -6.2944248720059186e+01\n", + "Dual infeasibility......: 4.9123733168040214e-09 4.9123733168040214e-09\n", + "Constraint violation....: 7.4505805969238281e-09 1.4901161193847656e-08\n", + "Complementarity.........: 9.0909090909090920e-10 9.0909090909090920e-10\n", + "Overall NLP error.......: 7.4505805969238281e-09 1.4901161193847656e-08\n", + "\n", + "\n", + "Number of objective function evaluations = 13\n", + "Number of objective gradient evaluations = 13\n", + "Number of equality constraint evaluations = 13\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 13\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 12\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.286\n", + "Total CPU secs in NLP function evaluations = 0.017\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.3\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.92e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.55e+01 3.85e+02 -1.0 1.92e+02 - 2.55e-03 9.95e-01f 1\n", + " 2 0.0000000e+00 1.07e+01 4.90e+01 -1.0 1.71e+01 - 2.60e-01 1.00e+00h 1\n", + " 3 0.0000000e+00 4.17e-05 2.47e+01 -1.0 7.37e+00 - 5.89e-01 1.00e+00h 1\n", + " 4 0.0000000e+00 3.41e-13 1.49e-01 -1.0 3.15e-05 - 9.91e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.6092568863958805e-13 3.4106051316484809e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.6092568863958805e-13 3.4106051316484809e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.064\n", + "Total CPU secs in NLP function evaluations = 0.003\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.1\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26424\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7092\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7092\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.81e+08 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (295725)\n", + " 1 0.0000000e+00 6.81e+06 3.85e+02 -1.0 6.81e+08 - 2.54e-03 9.90e-01h 1\n", + " 2 0.0000000e+00 6.82e+04 5.99e+01 -1.0 6.81e+06 - 1.29e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 6.77e+02 2.06e+00 -1.0 6.81e+04 - 9.84e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 3.11e-04 2.39e-04 -1.0 6.76e+02 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (393607)\n", + " 5 0.0000000e+00 2.98e-08 1.50e-09 -3.8 3.15e-04 - 1.00e+00 1.00e+00h 1\n", + " 6 0.0000000e+00 4.55e-13 1.84e-11 -5.7 2.98e-08 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 6\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.7123678492091068e-14 4.5474735088646412e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.7123678492091068e-14 4.5474735088646412e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 7\n", + "Number of objective gradient evaluations = 7\n", + "Number of equality constraint evaluations = 7\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 7\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 6\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.117\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26544\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (273960)\n", + "Total number of variables............................: 7112\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7102\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -6.5716835e+01 8.42e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303450)\n", + " 1 -6.5716837e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", + " 2 -6.5716838e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", + " 3 -6.5716838e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", + " 4 -6.5716837e+01 5.57e+00 7.49e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -6.5716837e+01 7.15e+00 3.09e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", + " 6 -6.5716837e+01 1.06e-02 4.11e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", + " 7 -6.5716837e+01 1.43e-06 1.50e-05 -3.8 2.13e-02 - 1.00e+00 1.00e+00h 1\n", + " 8 -6.5716837e+01 1.19e-07 6.81e-09 -5.7 2.30e-04 - 1.00e+00 1.00e+00h 1\n", + " 9 -6.5716837e+01 2.74e-06 5.39e-09 -8.6 5.05e-02 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (321428)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -6.5716837e+01 1.19e-07 2.41e-09 -8.6 2.41e-05 -4.0 1.00e+00 1.00e+00h 1\n", + " 11 -6.5716837e+01 1.19e-07 2.45e-09 -8.6 7.36e-05 -4.5 1.00e+00 1.00e+00h 1\n", + " 12 -6.5716837e+01 1.19e-07 2.45e-09 -8.6 2.21e-04 -5.0 1.00e+00 1.00e+00h 1\n", + " 13 -6.5716837e+01 1.49e-08 2.45e-09 -8.6 6.62e-04 -5.4 1.00e+00 1.00e+00h 1\n", + " 14 -6.5716837e+01 1.19e-07 2.45e-09 -9.0 1.99e-03 -5.9 1.00e+00 1.00e+00h 1\n", + " 15 -6.5716837e+01 1.19e-07 2.45e-09 -9.0 5.96e-03 -6.4 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 15\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -6.5716837442211869e+01 -6.5716837442211869e+01\n", + "Dual infeasibility......: 2.4545255939761519e-09 2.4545255939761519e-09\n", + "Constraint violation....: 2.4652589872487225e-10 1.1920928955078125e-07\n", + "Complementarity.........: 9.0909090909090920e-10 9.0909090909090920e-10\n", + "Overall NLP error.......: 2.4545255939761519e-09 1.1920928955078125e-07\n", + "\n", + "\n", + "Number of objective function evaluations = 16\n", + "Number of objective gradient evaluations = 16\n", + "Number of equality constraint evaluations = 16\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 16\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 15\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.401\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.1\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.92e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 1.55e+01 3.85e+02 -1.0 1.92e+02 - 2.55e-03 9.95e-01f 1\n", + " 2 0.0000000e+00 1.07e+01 4.90e+01 -1.0 1.71e+01 - 2.60e-01 1.00e+00h 1\n", + " 3 0.0000000e+00 4.18e-05 2.47e+01 -1.0 7.37e+00 - 5.89e-01 1.00e+00h 1\n", + " 4 0.0000000e+00 2.57e-13 1.49e-01 -1.0 3.16e-05 - 9.91e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.0672779846629121e-13 2.5723867480564877e-13\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.0672779846629121e-13 2.5723867480564877e-13\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.067\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.9\n" + ] + } + ], + "source": [ + "n_para = len(parameter_dict)\n", + "\n", + "t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]\n", + "parameter_dict = {\"A1\": 85, \"A2\": 370, \"E1\": 8, \"E2\": 15}\n", + " \n", + "measurements = MeasurementVariables()\n", + "measurements.add_variables(\"C\", indices={0: [\"CA\", \"CB\", \"CC\"], 1: t_control}, time_index_position=1)\n", + " \n", + "exp_design = DesignVariables()\n", + "exp_design.add_variables(\n", + " \"CA0\",\n", + " indices={0: [0]},\n", + " time_index_position=0,\n", + " lower_bounds=1,\n", + " upper_bounds=5,\n", + " )\n", + "exp_design.add_variables(\n", + " \"T\",\n", + " indices={0: t_control},\n", + " time_index_position=0,\n", + " lower_bounds=300,\n", + " upper_bounds=700,\n", + " )\n", + "\n", + "exp_design.update_values({\"CA0[0]\": 5, \"T[0]\": 450, \"T[0.125]\": 300, \"T[0.25]\": 300, \"T[0.375]\": 300, \"T[0.5]\": 300, \"T[0.625]\": 300,\n", + " \"T[0.75]\": 300, \"T[0.875]\": 300, \"T[1]\": 300})\n", + " \n", + "doe_object = DesignOfExperiments(\n", + " parameter_dict,\n", + " exp_design, \n", + " measurements, \n", + " create_model,\n", + " discretize_model=disc_for_measure,\n", + " )\n", + "\n", + "result = doe_object.compute_FIM(\n", + " mode=\"sequential_finite\",\n", + " scale_nominal_param_value=True,\n", + " formula=\"central\",\n", + " )\n", + "\n", + "result.result_analysis()\n", + "\n", + "FIM_prior = result.FIM\n", + "FIM_new = np.zeros((n_para, n_para))\n", + "A_vals = []\n", + "D_vals = []\n", + "exp_conds = []\n", + "FIM_opt = None\n", + "\n", + "def get_exp_conds(m):\n", + " return [pyo.value(m.CA0[0]), pyo.value(m.T[0]), pyo.value(m.T[0.125]), pyo.value(m.T[0.25]), pyo.value(m.T[0.375]), pyo.value(m.T[0.5]), pyo.value(m.T[0.625]), pyo.value(m.T[0.75]), pyo.value(m.T[0.875]), pyo.value(m.T[1])]\n", + "\n", + "for i in range(20):\n", + " FIM_prior += FIM_new\n", + " \n", + " doe_object = DesignOfExperiments(\n", + " parameter_dict,\n", + " exp_design, \n", + " measurements, \n", + " create_model,\n", + " prior_FIM=FIM_prior,\n", + " discretize_model=disc_for_measure,\n", + " )\n", + " \n", + " square_result, optimize_result = doe_object.stochastic_program(\n", + " if_optimize=True,\n", + " if_Cholesky=True,\n", + " scale_nominal_param_value=True,\n", + " objective_option=\"det\",\n", + " L_initial=np.linalg.cholesky(FIM_prior),\n", + " )\n", + " FIM_new = optimize_result.FIM\n", + " \n", + " new_exp_conds = get_exp_conds(optimize_result.model)\n", + " result = new_doe_object2(new_exp_conds[0], new_exp_conds[1:], FIM_new)\n", + "\n", + " if FIM_opt is None:\n", + " FIM_opt = [result.FIM, ]\n", + " else:\n", + " FIM_opt.append(result.FIM)\n", + " \n", + " D_opt = np.linalg.det(FIM_new)\n", + " # A_vals.append(np.log10(A_opt))\n", + " D_vals.append(np.log10(D_opt))\n", + " exp_conds.append(new_exp_conds)" + ] + }, + { + "cell_type": "code", + "execution_count": 109, + "id": "8d7175ef-92b3-44f4-8bbb-5af12527e668", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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d3V1xdvbu8ggIiIi1VeF3F595MiRPP3009x+++107tyZjz76iEOHDjFkyJCK+DoRERGxIRUSPh5//HFSUlJ49913OX78OK1atWLJkiUEBQVVxNeJiIiIDamQ+3zcCN3nQ0RExPaU5fe7yj3V9mIW0iW3IiIituPi7/a19DSqXPhIT08H0CW3IiIiNig9PR0fH58rLlPlDrsUFBRw7NgxvLy8SrwvyI1IS0ujYcOGHD58uNof0rGnbQX72l5ta/VlT9urba1+DMMgPT2dwMBAHByufDFtlet8ODg40KBBgwr9Dnu6pNeethXsa3u1rdWXPW2vtrV6uVrH46IKu8OpiIiISEkUPkRERKRS2VX4cHV15e2338bV1dXapVQ4e9pWsK/t1bZWX/a0vdpW+1blTjgVERGR6s2uOh8iIiJifQofIiIiUqkUPkRERKRSKXyIiIhIpap24SMqKorg4GDc3Nxo3749q1evvuLysbGxtG/fHjc3N26++WZiYmIqqdLrN378eDp06ICXlxe+vr48/PDDJCYmXvEzK1euxGQyFRsSEhIqqerrN3bs2GJ1+/v7X/EztrhfAW666aYS91NERESJy9vSfl21ahV9+vQhMDAQk8nEokWLisw3DIOxY8cSGBiIu7s7d911Fzt27Ljqer///ntatGiBq6srLVq0YOHChRW0BWVzpe3Nzc3l1VdfpXXr1nh6ehIYGMgzzzzDsWPHrrjOTz/9tMT9ff78+Qremiu72r4dNGhQsZpDQ0Ovut6quG+vtq0l7R+TycTkyZNLXWdV3a8VqVqFj/nz5zN8+HDeeOMN4uPj6dq1K7179+bQoUMlLn/gwAHuv/9+unbtSnx8PK+//jovvfQS33//fSVXXjaxsbFERESwfv16li5dSl5eHj179iQjI+Oqn01MTOT48eOWoUmTJpVQ8Y1r2bJlkbq3bdtW6rK2ul8BNm3aVGQ7ly5dCsDf//73K37OFvZrRkYGbdq0YdasWSXOnzRpEtOmTWPWrFls2rQJf39/7r33Xsvznkqybt06Hn/8cZ5++mn+/PNPnn76afr168eGDRsqajOu2ZW2NzMzkz/++IO33nqLP/74gwULFrB7924efPDBq67X29u7yL4+fvw4bm5uFbEJ1+xq+xagV69eRWpesmTJFddZVfft1bb18n3zySefYDKZePTRR6+43qq4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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "D_vals_opt_from_ind_FIM = []\n", + "running_FIM = np.zeros((n_para, n_para))\n", + "for i in range(20):\n", + " running_FIM += FIM_opt[i]\n", + " D_vals_opt_from_ind_FIM.append(np.log10(np.linalg.det(running_FIM)))\n", + "\n", + "plt.plot(range(20), D_vals, color='black')\n", + "plt.plot(range(20), D_vals_opt_from_ind_FIM, color='green')\n", + "plt.plot(range(20), D_vals_rand_from_ind_FIM, color='orange')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 104, + "id": "2e53d948-0c13-41be-b417-3a771e09b3c4", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAigAAAGdCAYAAAA44ojeAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/NK7nSAAAACXBIWXMAAA9hAAAPYQGoP6dpAABifElEQVR4nO3dfZRU1Z0v/O/peu1uuqsbGrq6Y6chBk20MSGQK2AyGEGUEckVRzS+DM4wLr06JB1gmaDPrHTmzoB6L+IEZ0x0ERHRwTVXyZNnNCJEIWGQXCQ406BRZngRtNsOWF3Vr/V6nj+6z6mqpt+q6rzsfer7WauWUnW66lSdqrN/Z+/fb29FVVUVRERERAIpsXsHiIiIiIZigEJERETCYYBCREREwmGAQkRERMJhgEJERETCYYBCREREwmGAQkRERMJhgEJERETCcdu9A/lIpVL45JNPUFFRAUVR7N4dIiIiGgdVVdHV1YX6+nqUlIzeRyJlgPLJJ5+goaHB7t0gIiKiPJw5cwYXXXTRqNtIGaBUVFQAGHiDlZWVNu8NERERjUckEkFDQ4Pejo9GygBFG9aprKxkgEJERCSZ8aRnMEmWiIiIhMMAhYiIiITDAIWIiIiEwwCFiIiIhMMAhYiIiITDAIWIiIiEwwCFiIiIhMMAhYiIiITDAIWIiIiEk1OAMnXqVCiKcsHtgQceADCwCFBLSwvq6+tRWlqKq6++GseOHct6jmg0ilWrVqGmpgbl5eVYunQpzp49a9w7IiIiIunlFKAcOnQIbW1t+m337t0AgFtuuQUA8Nhjj+Hxxx/Hk08+iUOHDiEYDOLaa69FV1eX/hzNzc3YuXMnduzYgf3796O7uxtLlixBMpk08G0RERGRzBRVVdV8/7i5uRn/+q//iuPHjwMA6uvr0dzcjB/84AcABnpLamtr8eijj+Lee+9FOBzG5MmT8fzzz+PWW28FkF6Z+LXXXsN11103rteNRCIIBAIIh8Nci4eIiEgSubTfeS8WGIvFsH37dqxevRqKouDEiRNob2/HokWL9G18Ph/mz5+PAwcO4N5778Xhw4cRj8eztqmvr0dTUxMOHDgwYoASjUYRjUaz3qAZPo304+f7TwIKsG7xl015DTOd+GM3Xjp0BrFkyrTXuLw+gD+bNfoS2cXow0+78PLvzyKVGj7eH2lhrBGXyxrhAWXwgcyn0/43+770P7T7s54yY+Ohf69AgaJk36/t/6jbDN6Xfon0XlT43bjhijqUeeVbn1RVVagqUFIy9uJmRGScvM8Wv/jFL9DZ2Ym7774bANDe3g4AqK2tzdqutrYWp0+f1rfxer2orq6+YBvt74ezYcMG/PjHP853V8etO5rAz35zAhV+t5QBysbdH+LV/2gz/XWu+uIk1AVKTX8dmfzPf30Pvz1+zu7dENpnPTHcO/9iu3cjJ4lkCjc/dQD/fjYMd4kCj6sEHpcCr7tk8P8H/j30/9OPD/5be9w95N+D95V5XCjzuVHudaPM5xr4r9eFcp8b5V4XSr0ulHndcDFIoiKSd4CyZcsWLF68GPX19Vn3D71SVFV1zGWVx9pm3bp1WL16tf7vSCSChoaGPPZ6dFWlHgBAV38CiWQKbpdcRU4dkX4AwOKmIL4wudzw59/29ml09Sfwx64oA5Qh2sMDn/3Sr9SjrsqffmCYDpWhdw03yjrcwKs6zGNqxrONNFib+fxq1v0XPo+qDmyTfmyg90D7t6r9O+M5VKTvUAdfL/PxDz/twh/au3Am1Dv8Dgrs484+/PvZMAAgkVKRSCXRF7dvf/yekmGDmDKva8j9bpT7XPp/J0/wYXLFwC1Q6hnXUvdEdssrQDl9+jT27NmDV155Rb8vGAwCGOglqaur0+/v6OjQe1WCwSBisRhCoVBWL0pHRwfmzZs34uv5fD74fL58djUngcEABQAi/QlMLPea/ppG6uwdOHPeNacR875YY/jz//r9DvyhvUt/HUrrHGy17pt/MS6rZ15Upmf/7SR+/P+9J+X3Rtvn2kof/t8HvoF4MoVYMoV4MoV4Qk3/v35T0/9/weMqYokh/06mEEuk0BdPojeaQE8sid5YAr3RJHpiCfTGkuiJJqCNHPbHU+iPx3C+J//35HWVYHKFDzUVvqzAZXKFD1O0/x+83+9xGfApEuUnrwDl2WefxZQpU3DDDTfo902bNg3BYBC7d+/GzJkzAQzkqezbtw+PPvooAGDWrFnweDzYvXs3li9fDgBoa2vD0aNH8dhjjxX6XgrmdpWgwu9GV38Cnb0x+QKUwUYyUOYZY8v8VA0+b6edl5ACUlUV4cGGrMqkz15m2mcSlvB7o33XJ5b7EAz4x9jaHKqqIppI6cFKb2wweNGDmAR6osns/8bSAU9XfxznumP4Y1cU4b44YskUPu7sw8edfWO+doXfnRG4+IcNaOqrSrMu7oiMknOAkkql8Oyzz2LFihVwu9N/rigKmpubsX79ekyfPh3Tp0/H+vXrUVZWhttvvx0AEAgEsHLlSqxZswaTJk3CxIkTsXbtWsyYMQMLFy407l0VoKrMMxCgSHYyVVUVnb0xAEBVmTmBVVXpwPNqr0MD+uJJPTGZAcqF0t8buX5TQPq7XmVjA6woCvweF/weV8EXTdFEUg9WOiL9+GN3FH/sSt86tP/vjiKWSKGrP4Gu/gRO/HH0LpvPVZXi8vpKNH0ugMvrK3F5fQC1lT4OJVFBcg5Q9uzZg48++gh/+Zd/ecFjDz74IPr6+nD//fcjFArhyiuvxBtvvIGKigp9m02bNsHtdmP58uXo6+vDggULsHXrVrhcYnQlVpV6cQZ9+hWxLHpjScSTA/3AZp1M9R4UyT4bs2mfh9dVglJ2iV9A69ELSRjYar0+Tgk8fW4XPldVis9VjZ5DpqoqIoP5ZgOBS78euAwNaD7riek9Mm+896n+HJPKvbhsMFjRgpfGiWXCVENpPVMcxhJXzgHKokWLhk3qAwYi/ZaWFrS0tIz4936/H5s3b8bmzZtzfWlLVEl6MtV6fLyuEpR5zfnBaT0zDFCyaZ9HoIzJh8PRAmbZgn4gfWzN6pUUlaIoCJR6ECj14ItTJoy6bbgvjvc+ieDYJ2G890kERz8J47/+2IPzPTH89vi5rOq2CT43vlxXgcvrA4PBSyWmT6mA121sQUI8mUJHVxTt4X58GunX/9sW7kd7JH1fLJlCy42XY8W8qYa+PhlDvkkJTKaNpcrWCGtd0WY2kukcFLmCN7OJMAwgsurBxr0rmkA8mYJHouq4kD5symM7kkCpB3MvnoS5F0/S7+uPJ/GH9i4c+ySMY59EcOyTCP7QFkF3NIFDp0I4dCqkb+t1leCS4ARcXhfA5Z8bCFq+XFc54pw5Xf1xtA8GGnoAEulHeziq//+57uiIVW1DvfFeOwMUQTFAGULWRFA9SdPERlLmK2EzdTpsGMBolZnVcX1xTJpgfkWeUaz4XTmR3+PCVxuq8NWGKv2+RDKF//pjT0bQMvDfrv4Ejn4cwdGPI8A7A9sqCjCtphyX1wfgKVEGApBIPz4N96MnNr5lUdwlCmor/ait9KEuUIraSj+CAd/Afyv96OiKYtU/H8Gpc/KVvxcLBihDaAl9YUmHeMxsJGUN3symD/GUFtcwwHi5ShRU+t2IDCafyxSgMPg0jttVgkuDFbg0WIFlXxu4T1VVnA314dgnYRz9OB20dHRFceKPPSMm51b43QhW+hEM+PWAIxjwZ903qdw7ar7LH7sGZif/JNyH/niSuSgCYoAyhKyNsBWNZIBVPMPShryq2YiNqKrMOxCgSNb7pg+dMvg0haIoaJhYhoaJZbi+KT1/1h+7ogM5LW0Dy5oEK7ODj3Jf4U1XzQQvJvjc6I4mcDbUiy9OqRj7j8hSDFCGkDYHpc/8sXKZ57MwE+dAGVtVmQcffSZfcMseFHtMrvDh6kun4OpLp5j2GoqioHFSGY59EsHJcwxQRCRPtppF9EoVyRphS3JQMsqMC1gE23GKtdIjF9IG/oP7W81j60hTawaWBDl1roCpeck0DFCG0HsJZLvSs+AqXsvPSaTUcSeqFQOt94qzaY6sWsLAP5XKnPyQx9aJpk4qAwCcKmTtADINA5QhtB4ImU6kQEYjaeKVnt9Tos9XIFtXvZmsCA5lJ2Pg3x1Lr4HD4NOZpk4a7EFhgCIkBihDaN304b44Uil5hjE6LRjiURQlHcBJ1lVvJn22USZSjkjGwF8bNvV7Sljh4VDT9CEelhqLiAHKENqVkqoCXf0Jm/dm/Ky6iud09xdiD8rYAhLOQpwO+hl4OlXjYA+KVmpMYmGAMoTXXYLywaniZZoxVa/iMflkqi/8JtFnYzbmoIxN60GRaQkJKyrjyF5aqbGqAmc+Yy+KaBigDEPGNWfYg2KP/ngS/XGuZDwWGUvUQ+wZczyt1BgATp1ngCIaBijDCEh2tdcfTyKasKaRlLGhMZMWqLlKFEwwYPIop5Ix6A/3WtMrSfZiqbG4GKAMQ7ZG2MpGMt3QyBG8mS09tMaVjEeT7nmT53vD3KLiMI2VPMJigDIM2YYxrGwkZZ1wyyz6EgNsxEal5aBE+hNISlIdp1Uc8dg6WyPnQhEWA5RhpNeckaMRtrKRlHWtIrNwptHxCQxZ0VgGrOIpDiw1FhcDlGGkG2E5uqOtmANFk17tWY5GxmzhjN4rGpnbVYKKweFHWYLbMBeBLAosNRYXA5RhaI2NLI2w3khacBUvW/BmNg7xjJ/2GcmSfM4qnuLAUmNxMUAZhmzDGFb2oDAHJVsnZ5Edt/R093J8d7SE3gCPraMpioKpNSw1FhEDlGGkc1DkuNKzMpkvM3jjisas9MhFesFAOX5X+hIGPLaOpw3zsNRYLAxQhlEtbQ+KFUM8A68RS6T0CcqKWZizjY6bTL1vqqoy+CwiWqnxSVbyCIUByjD0BQMlOJEC1jaS5V4X3CUDpcyy5BKYSc9BYZLsmGQq3++JJZEYLIfm8J3zaaXGpxmgCIUByjBkG8YI9Vh3pacoilQNjdnSV9lsxMaiV4BJ0DMZ6hkIvn3uEpR6uZKx07HUWEwMUIahXQ0nUyq6o+KvaKznoFh0FV8lWS6BmTp7WWY8XlUSVfEw/6S4aNPds9RYLAxQhuH3uOD3DHw0MvQSaGuGWDVZmGxl2GbqZEM2bjLloHCStuIyqZylxiJigDKCKolmk7W6kZStDNss0UQSvbGBqy02ZGNLV/GI/73Regc5v01xyCw1PslKHmEwQBmBLBOS2dFIyrYUgFm0YYASBajwcyXjsaTnQRH7NwVYO7cQiUErNT7NuVCEwQBlBLJ0R2uNpGJhIylL8Ga2cEYFT0kJVzIei0w9b9rvimssFQ+WGouHAcoIZDmZ2tFIMgdlQHpojY3YeAQyqnhSgq9orFXxMLeoeGiJsiw1FgcDlBGkF8UTu5cgPdW6dSdSlhkP4BwoudE+J1UFIv1if3esnJ2ZxDB1cC4UlhqLgwHKCGRphNOL1Vl3FR9gmTGAjBJjNmLj4nWXoHxwThFZfldMfi4eLDUWT84Byscff4w777wTkyZNQllZGb761a/i8OHD+uN33303FEXJus2ZMyfrOaLRKFatWoWamhqUl5dj6dKlOHv2bOHvxkABSYZ47JiHo0qS/ByzhW3ovZJdlSSVPFzCoPiw1Fg8OQUooVAIV111FTweD371q1/hvffew8aNG1FVVZW13fXXX4+2tjb99tprr2U93tzcjJ07d2LHjh3Yv38/uru7sWTJEiST4kStspQZ2zGhlF6NIXgjYzbOIpu7dM+k2L1vrOIpPiw1Fk9OZR+PPvooGhoa8Oyzz+r3TZ069YLtfD4fgsHgsM8RDoexZcsWPP/881i4cCEAYPv27WhoaMCePXtw3XXX5bJLpqnWG2GeSIfSgjcZZgQ1kz5XBhuxcZMluA0x+CxKjZPKcfTjCEuNBZFTD8ovf/lLzJ49G7fccgumTJmCmTNn4plnnrlgu71792LKlCm45JJLcM8996Cjo0N/7PDhw4jH41i0aJF+X319PZqamnDgwIFhXzcajSISiWTdzBaQJAdFCxKszUEZ+Gz646miHqvlare5k6FnUlVVDvEUKZYaiyWnAOXEiRN46qmnMH36dOzatQv33Xcfvvvd72Lbtm36NosXL8YLL7yAN998Exs3bsShQ4dwzTXXIBqNAgDa29vh9XpRXV2d9dy1tbVob28f9nU3bNiAQCCg3xoaGnJ9nznTT6SCX+nZUcVT6XfDNVjSLPqVsJkYoOQuIMF6PL2xJOLJwZWMeWyLylR90UAGKCLIaYgnlUph9uzZWL9+PQBg5syZOHbsGJ566in8+Z//OQDg1ltv1bdvamrC7Nmz0djYiFdffRXLli0b8blVVYWiDD+Px7p167B69Wr935FIxPQgJXOsfLR9s5s2F0l1uXUnUkVRECj14LOeGDp746it9Fv22iLRhnhY6TF+MiRYa0G/11WCUg9XMi4mWqkxh3jEkFMPSl1dHS677LKs+7785S/jo48+GvVvGhsbcfz4cQBAMBhELBZDKBTK2q6jowO1tbXDPofP50NlZWXWzWxagBJPqvpU8iKyq5FMNzTiXgmbLV3izavs8ZIhB6WzN70Oj6gXJmQOlhqLJacA5aqrrsIHH3yQdd+HH36IxsbGEf/m/PnzOHPmDOrq6gAAs2bNgsfjwe7du/Vt2tracPToUcybNy+X3TFVqccFr2twRWOhT6b2NJKylGGbKcxKj5zpZcYCB7Z6ryQDz6LDUmOx5BSgfP/738fBgwexfv16/Od//idefPFFPP3003jggQcAAN3d3Vi7di3efvttnDp1Cnv37sWNN96Impoa3HTTTQCAQCCAlStXYs2aNfj1r3+NI0eO4M4778SMGTP0qh4RKIqSkSgr/snU6kay2Ke7jydT6IomALDSIxd6z5vAgW2Ik7QVLZYaiyWnHJSvf/3r2LlzJ9atW4e//du/xbRp0/DEE0/gjjvuAAC4XC60trZi27Zt6OzsRF1dHb71rW/hpZdeQkVFhf48mzZtgtvtxvLly9HX14cFCxZg69atcLnEGu+tKvXgj11RYRthOxvJqiKfTTaS0cCyzHj8tO+NqL8pIKN8nD0oRWnqYKnxKVby2C7n5W+XLFmCJUuWDPtYaWkpdu3aNeZz+P1+bN68GZs3b8715S0l+oKBmY1kpUUrGWtkWe3ZLNp3IrOiicYm+m8K4CRtxW7qYKnxKSbK2o5r8YwiIPicDdpJvsLvhttl7aGUoaExE2eRzU9mcrWoKxrbMTsziYOlxuJggDKKdCMs5jCGnfNwFHsOCifyyo82bJJSoQ9Piia9CCSDz2LEUmNxMEAZheiNcNjGeTiKPQdFr57iMEBOfG4XygZXNBb1dxXiBHxFjaXG4mCAMorqcsGHeGw8keozgvaI+dmYjUM8+UtX8ogZ3IZZxVPUJpV7UTFYavwRS41txQBlFAHBT6QhG6/i9d6lYs1BsWGJAacIlAke+HP4rqgpioLGwVJj5qHYiwHKKKoEXzAw3GvfibRaggm3zNRp42cvO9HnQuHwHaUreRig2IkByii0Ll5Rewm0E3y1DcMMWsPcE0silkhZ/vp2YyOWvyqBJ0BUVTXdO8bgs2ix1FgMDFBGUSX4yqt2NpIVfg+0ZUpEDeDMlG7EmKeQK5F7JvvjKT3g5rEtXiw1FgMDlFGIPhmZnY2kq0RBpV/LQxEzgDOTPrzGHpScVQmcg6JdjHhcCsq9Ys1sTdaZxhwUITBAGYV2pRdNpIQsN7O7kRT5SthsHAbIn8hVPOleSS9XMi5ijZO0UuN+Ic/9xYIByigm+NLTmIvYCNvdSFYJ3sNkJpYZ50/7voo4DworeAhIlxoDLDW2EwOUUSiKIsXVnl0nU71ctMhyUJIpFZF+9qDkS19CQsDvjV2rg5NYWGosBgYoYwgIOoyR2UgGbJpQKnNdlWLS1R+HOriMDKt4cidy8rndvZIkDpYa248ByhhEHcYQoZHUu+oFvBI2k/ZdmOBzw2PxIo1OIPIQT4jr8NCgaYOVPCfPcYjHLjy7jkE7UYlWqaI1kuVeF7xuew6jqMGb2bSrbPae5KcqY4hHVcVa0ZhDPKTREmVPswfFNgxQxiBqpYoI83BoOSgidtWbibPIFkb73JIpFd2CrWhsd14XiYOlxvZjgDKGKkET+rRG0s6r+GJdjyfMPIWC+D0u+D0Dpx7xAv/B3xWHeIoeS43txwBlDML2oAhwpSfqZ2O2UI82/wwbsXyJuoxEJ4d4aBBLje3HAGUM6URQsYYxtB4UO9bh0egzggr22ZhNz0FhD0reRK3kYe8YaRRF0ae8P8lhHlswQBmDNoQS6hHsSk+ARrJYe1B4lV04UZeRCAkQ+JM4GicN5KEwUdYeDFDGUCXoZGQiNJLaa3f1J5BIFs+KxrzKLpwe3Ar6u2KFFgEsNbYbA5Qx6Img7Iq+QOZJPNIvVjWGmfQqHuag5E3roRDpd9UfTyKqr2TMAIVYamw3BihjEPdKz/5G0u0q0ZPIimk2Wc42WjgRZ2jW9sVVomDC4PeaihtLje3FAGUMWgDQG0simhCn1EyEHJTM1xctgDNTmAsFFkzE8n19ocBSD1cyJgDp6e5ZamwPBihjqPC7oZ2rRCqJFGXGS5GnLTcLe1AKJ2KCtQil+ySWiSw1thUDlDGUlCh6roVIjbAIM8kCmVfCxTHEk0qpGcNrbMjyJeJCk51ch4eGYKmxvRigjIN+MhWkByWrkRRliEeg4M1M3bEEUoPLx1QyQMmbiEODIlTGkXhYamwfBijjoE17LUojnNlI2l0OqZ3MQ4J8NmbTetFKPS74PS6b90Ze1YL9pgBx8rpILCw1tg8DlHGoLhOrO1prJP2eEtsbyXQOihifjdmYp2CMzBmaRVnRON2DwiEeStMSZVnJYz0GKOMg2qJ4IQFKjDUiVmOYKSTAIo1OoH1v4kkVvTExqiO05SwYfFKmqTUc4rELA5RxqBKsO1qkq3gRqzHMxAoeY/g9JfC6B1c0FiS41ZazqOaxpQwsNbZPzgHKxx9/jDvvvBOTJk1CWVkZvvrVr+Lw4cP646qqoqWlBfX19SgtLcXVV1+NY8eOZT1HNBrFqlWrUFNTg/LycixduhRnz54t/N2YRF+PR5BhDJEaSVGXAjBLWKDeK5kpipLOX+oR5Xc12DvGKh7KwFJj++QUoIRCIVx11VXweDz41a9+hffeew8bN25EVVWVvs1jjz2Gxx9/HE8++SQOHTqEYDCIa6+9Fl1dXfo2zc3N2LlzJ3bs2IH9+/eju7sbS5YsQTIpZnQq2myyIjWSzEGhfKXzUMT4XbGKh4bDUmP75DSf86OPPoqGhgY8++yz+n1Tp07V/19VVTzxxBN4+OGHsWzZMgDAc889h9raWrz44ou49957EQ6HsWXLFjz//PNYuHAhAGD79u1oaGjAnj17cN111xnwtowl2mRkIjWSopVgm42VHsbR85cE+V2JsL4ViWlqTTlaPw4zUdZiOfWg/PKXv8Ts2bNxyy23YMqUKZg5cyaeeeYZ/fGTJ0+ivb0dixYt0u/z+XyYP38+Dhw4AAA4fPgw4vF41jb19fVoamrStxkqGo0iEolk3awk2mRkIjWSgYyr4FRKjGoMM2mNaTWHAQqW7pkU5HfFKh4awdTBuVBOnecQj5VyClBOnDiBp556CtOnT8euXbtw33334bvf/S62bdsGAGhvbwcA1NbWZv1dbW2t/lh7ezu8Xi+qq6tH3GaoDRs2IBAI6LeGhoZcdrtgok1GJtKJVMvPUVWgqwhWNA73cRZZo4iUYN0fT6JvMAGyqpzHlrKx1NgeOQUoqVQKX/va17B+/XrMnDkT9957L+655x489dRTWdsNXWhLVdUxF98abZt169YhHA7rtzNnzuSy2wWrEmyqe5HKIX1uF8q8A3OxiHIlbCaRhtdkpyVYi5CDou2Dq0TREyKJNCw1tkdOAUpdXR0uu+yyrPu+/OUv46OPPgIABINBALigJ6Sjo0PvVQkGg4jFYgiFQiNuM5TP50NlZWXWzUraibQrmkA8mbL0tYcjWjJfel0V+xsas+nDawL0XskuIFAVj/bdDXAlYxoGS43tkVOActVVV+GDDz7Iuu/DDz9EY2MjAGDatGkIBoPYvXu3/ngsFsO+ffswb948AMCsWbPg8Xiytmlra8PRo0f1bURT6U9fUUUEuNoTKQcFyFgKQIDPxmzsQTGOSNVxXACSRjOx3IuKwXbgNPNQLJNTgPL9738fBw8exPr16/Gf//mfePHFF/H000/jgQceADAwtNPc3Iz169dj586dOHr0KO6++26UlZXh9ttvBwAEAgGsXLkSa9aswa9//WscOXIEd955J2bMmKFX9YjG7SrRv5xinEzFyUEBxFyZ1gyqqgo1vCY77fsrwtCpaEE/iUVRlHQeCod5LJPTYOvXv/517Ny5E+vWrcPf/u3fYtq0aXjiiSdwxx136Ns8+OCD6Ovrw/33349QKIQrr7wSb7zxBioqKvRtNm3aBLfbjeXLl6Ovrw8LFizA1q1b4XKJu/hadZkXXf0J24cxRGwkRUp2NFNPLIl4cqBSSZTgUGbVAlXxhAUbNiXxsNTYejlngy1ZsgRLliwZ8XFFUdDS0oKWlpYRt/H7/di8eTM2b96c68vbpqrMg48+Syeo2qU3s5FkgGIprYfI6y6B38NVIgolUnWcNks0y8dpJCw1th7PsuMUECQRVDuRel0lKLV5JWNNerp7+6+EzZSZnMxEysJlLpNg94rGHOKhsbDU2HoMUMZJlAUDM5M0RWkkRSvDNgtnGjWW9r2JJVL6HCR2ES2vi8SjTXfPHBTrMEAZJ1ESQUVsJEWqxjATGzFjlXld8LgGgmy7A3/R8rpIPNoQTxtLjS3DAGWcRGmERWwkA/qaKg4f4tFXu2UjZgRFUTK+O4L8rnhsaQQsNbYeA5RxEiUHRcRGUpTgzWyiTZDnBKJU8mRO1EY0HJYaW48ByjhVCTIZmYiNpGirPZtFG16rLhen90p2onx3OlnFQ+Og56EwUdYSDFDGKZ0IyhyUodKrPdtfjWEmrRHjVbZxAqWCBP4C/q5IPNP0UmMGKFZggDJOogxj6FNyC3Slp302yZSK7qhzVzRmnoLxRJhDJ5pIojc2uJKxQLldJJ5GvdSYOShWYIAyTiKcSDNfX6SreL/HBZ974Ktk9+djJv0qm42YYUSojtN6JRUFehIk0XBYamwtBijjpHVFR/rjSKbsG8YQtStalADOTGH2oBhOhO9NOCPoLykRY24hEhNLja3FAGWctBOpqgJd/fafTEW7ik/noTi31DjEHBTDiTALcWjwN8UEWRoLS42txQBlnDyuEkzwDa5obOPVXqegE0qJtK6KGVRVFbb3SmYi9KAw+ZnGS1EUTBsc5jnJSh7TMUDJgT4Xik2Jsqqq6ld7op1MqwVJIjZLfzyFWCIFQKwEZdlpPW9hG783DDwpF1qi7GnmoZiOAUoO0ld79nRHZzaSos3FoTc0Dp1NVuu5cpcoKPeKsUijE4jQgxIWcG4hEhdLja3DACUHdp9MRW4k7f5szCbiIo1OoPUEhmwMbNPDpmIF/SQmlhpbhwFKDqpsXnNG5EYy4PAhHhHLu51AC2yjiZRtVRE8tpQLlhpbhwFKDuxuhEU+kVYJsuibWcK8yjbFBJ8b7hJ7VzTu1Kt4xPtdkXi0JNm2cD/6Yiw1NhMDlBxU2bxgoMiNpL6mikPLjNmImUNRlIxZmm3qmRT4d0XiqS7z6KXGH33GYR4zMUDJQboRtvdKT8RkPruDN7NpvWYBweafcQK7VwrXeyYZfNI4sNTYOgxQcmB7DkqfuCdSu4e/zMZ1eMyjT9Zmc4AiYuBPYmKpsTUYoOTA7ka4U9BZZIF0IxPudeaKxvrwGhsxw9m9Hk96hXDxflckJpYaW4MBSg60E2nY9hwU8RpJ7bOJJVP6yrBOwh4U89gZ+McSKX0FbuYX0XhN5RCPJRig5CC9bojNPSgCnkjLvC54XIPVGA4c5tHX4eFVtuHsrADLXslYvN8ViSk9xMMkWTMxQMlBdcZMsikbVjQWucxYURQ9gdSurnozMU/BPNU2VoBpr1np98DFlYxpnFhqbA0GKDmoHGycUirQHUtY/vqdgo+V6w2NAyt5wlyvxTR2zkIscq8kiYulxtZggJIDv8eFUs/AFPN2NMJaz4SoY+VVDq7kETlBWXYBG6t42DNG+WCpsTUYoORIa4TtWDtE9EYy4NDZZPvjSfQNTsMuYom37KpsXCU8Xbov5m+KxDVVW5OHlTymYYCSI7smlZKhkbR7RlCzRAYbsRIFqPC5bd4b57FzlXDReyVJXFMHS405F4p5GKDkyK5hDBkaSbvLsM2SnkXWgxImUhrOzioeDvFQvlhqbD4GKDnSTqZhi6/2ZGgk7Ux2NFN6HR4OA5ihqnzge9MXT1q+orHW28chHsoVS43NxwAlR3Y1wulqA3FPpHqyo8OGeDr1OVB4lW2GCp9bL/GNWNwzyR4UyhdLjc2XU4DS0tICRVGybsFgUH/87rvvvuDxOXPmZD1HNBrFqlWrUFNTg/LycixduhRnz5415t1YwK5ZL/VGUuATqVMXDNTLuwX+7GU2MIeOPb8rlo9TvqrLPKgcLDU+/RmHecyQcw/K5Zdfjra2Nv3W2tqa9fj111+f9fhrr72W9XhzczN27tyJHTt2YP/+/eju7saSJUuQTMoRgdo1Xt4pwYnU7tWezRKWoPdKdnYFtxy+o3wpiqLnoZw6x2EeM+Scbel2u7N6TYby+XwjPh4Oh7FlyxY8//zzWLhwIQBg+/btaGhowJ49e3DdddflujuWq7Jp1suwBF3RWvBmRwm2mfQ8BYE/e9kFbCrfD3H4jgowdVI5/uNsmKXGJsm5B+X48eOor6/HtGnTcNttt+HEiRNZj+/duxdTpkzBJZdcgnvuuQcdHR36Y4cPH0Y8HseiRYv0++rr69HU1IQDBw6M+JrRaBSRSCTrZhfbrvT0hQLFvdJzapJsiLONms6uCjAZAn8SF0uNzZVTgHLllVdi27Zt2LVrF5555hm0t7dj3rx5OH/+PABg8eLFeOGFF/Dmm29i48aNOHToEK655hpEo1EAQHt7O7xeL6qrq7Oet7a2Fu3t7SO+7oYNGxAIBPRbQ0NDru/TMHYtGCjyOjwa7So0mkhZXo1hJjZi5quyIcE6nkyha3AlY5EDfxIXS43NldMQz+LFi/X/nzFjBubOnYuLL74Yzz33HFavXo1bb71Vf7ypqQmzZ89GY2MjXn31VSxbtmzE51VVFYoycunsunXrsHr1av3fkUjEtiDFtioeCXJQtGqMZEpFZ28cwYDL7l0yhAy9V7Kz43eVWTGkJTsS5YI5KOYqqMy4vLwcM2bMwPHjx4d9vK6uDo2NjfrjwWAQsVgMoVAoa7uOjg7U1taO+Do+nw+VlZVZN7tk5qCoqnUrGqdnvBS3kVQUJWPacufkoei9VwIHh7LTk88t7JnUXqvC74bbxRkXKHfadPftEZYam6GgX2U0GsX777+Purq6YR8/f/48zpw5oz8+a9YseDwe7N69W9+mra0NR48exbx58wrZFctoJ9J4UkWvhV9IWRrJgAPzUDhXhvn0wN/C740MQT+JjaXG5sopQFm7di327duHkydP4ne/+x3+7M/+DJFIBCtWrEB3dzfWrl2Lt99+G6dOncLevXtx4403oqamBjfddBMAIBAIYOXKlVizZg1+/etf48iRI7jzzjsxY8YMvapHdH5PCbzugY/NyooDWRpJJ86Fkp4rgw2ZWexYhLOTyc9UIJYamyungdezZ8/iO9/5Ds6dO4fJkydjzpw5OHjwIBobG9HX14fW1lZs27YNnZ2dqKurw7e+9S289NJLqKio0J9j06ZNcLvdWL58Ofr6+rBgwQJs3boVLpcc+QraMEZHVxSdvXFcVD323xhBlkZS2z+ry7DNEk+m0K0lUgoeHMrMjkU4ZUg8J/Gx1Ng8OQUoO3bsGPGx0tJS7Nq1a8zn8Pv92Lx5MzZv3pzLSwulqmwgQLFqQjKZGkmn9aBox1hRgErBP3uZpQNb63NQRA/6SWzpHhQGKEZjZlgerJ5NNvOkLXojaddSAGbRjnGl36OvF0PGq9Zzl6zredMW/BQ96CexaXOhsAfFeAxQ8pBuhK05maYbSbfwjaRdSwGYJayXGLMRM5P2vemJJRFLpCx5TS2IruaxpQIwB8U8DFDyYPUwRliieTjsWgrALLIkJ8uuwu+GNhWSVcM8Ib0yTvzfFYmLpcbmYYCSB6sXxZOp2kCvxuhxRg9KJxsxS5SUZKxobNEwTyeHeMgALDU2DwOUPOjTclt2IpWn2kBvZBySgxJiI2aZKou/O2EJZmcm8SmKgmlMlDUFA5Q8WD0tt0zVBno1hkNWNGYjZp1AmbX5SzL1TJLYGgeHeU6dZx6KkRig5MHqabllqjaodmgVjwyfveysruTRXidQKn7gT2JjqbE5GKDkweppuUMSXelpwVtvLIloQv6EMS3QYg6K+bQg0IrcrkQyhUj/wNxCrOKhQrHU2BwMUPIQsHhBPJmGeOyoxjATEymtU2XhEI8WnABy5HaR2FhqbA4GKHlIrxsSt2RFY5kaycxqDCsXfjMLc1CsY2Xgr/2mKnxcyZgKN42lxqbgLzMP2pVeLJFCf9z8SaVkayStrsYwExMprZMZ+JstPXTH40qFq2KpsSkYoOSh3OuCe3BGV2uu9uRqJK2uxjATEymtY2VuV1iy3xSJjaXG5mCAkgdFUSwtNZatkayyeMItsyRTKhMpLZSujrMg6NdmZ5bkN0XiY6mx8Rig5Mmq5eEzG0lZrvasnmnXLJGM/WcipfmsDPq1mY5l+U2R+FhqbDwGKHlKLw9v7tWejI2k1WsVmUXLU2AipTXSk/xZl4PCAIWMMq1moNT4JAMUw/CsmyerGmHtRDrB54ZHkkZSy0EJST7Eo+0/Eymtof2muqIJxJPmJp+nJz/kEA8ZQxviOc0hHsPI0eIJKGDRjKnp/BN5GkmnVPEwkdJalRnfcbOHB9mDQkZjqbHxGKDkSU/os6gHRaYTqdUz7ZqFiZTWcpUoeqmm6b8riRbgJDmw1Nh4DFDyVK0ngpo7jCHjVXx1mXXVGGbSGzGJPnvZWZXbpfVMVkswOzPJgaXGxmOAkierKg46JRwrD1hYjWEmLhRovWqrflcS9kyS+LRKnpOc8t4QDFDyZNVkZDItFKipcshU97LN4OsEVv2uZJv8kOSQTpRlD4oRGKDkSWuEza5UkbGR1LrprajGMJOMvVeysyLBemBuIS0HhceWjMNSY2MxQMmTVZORydhIaoliQPY8LrLhei3WSw+dmhf4d/XHoa3xySRZMhJLjY3FACVPVlfxyNRIul0lqNCqMWQOUJiDYjkr5hfSnrvc64LXzVMgGYelxsbirzNPWsDQF0+iP27eF1HWRtLKacvNovWOVZfL03slOz0HxcTAVhuWrWIFDxkss9T4FPNQCsYAJU8VPjcGFzQ2dRgjnYMi18lU62Eyu1zUTOnhNbmCQ5lVWzDEwwoeMktmqTETZQvHACVPJSVKesFAEwMUvZGU7GQqew9KKqXqwaFMw2uysyK3S8a5hUgeLDU2DgOUAlSZXBKZ2UjKdhUf0Kuc5AxQuvoTSDGR0nIBC3K7ZEw8J3mw1Ng4DFAKoPegmNQd3RVNN5KVkjWS6enu5Rzi0WbBLfO64HO7bN6b4qF9b8ws35cx8ZzkwVJj4zBAKYA+jGFSd7TWFV3qccHvkauR1KucJK3ikTU5WXb6isb9CSRMmkNHO7bVDFDIBFoPCpNkC8cApQDaOh5mzZiqL1Yn4YlU9hyU9FU2hwGslDmcFulPmPIaHOIhM2mlxp9GouiNmfMdLhY5BSgtLS1QFCXrFgwG9cdVVUVLSwvq6+tRWlqKq6++GseOHct6jmg0ilWrVqGmpgbl5eVYunQpzp49a8y7sVg6Sdac7miZV1ytsqBc1Eys4LGH21WCCp+2orFJvysO8ZCJqsu9+jmbE7YVJucelMsvvxxtbW36rbW1VX/ssccew+OPP44nn3wShw4dQjAYxLXXXouuri59m+bmZuzcuRM7duzA/v370d3djSVLliCZlG9SG7N7CUISr7iaXo9HzhwUGZcYcIqqcnOHTjl8R2abOmkgD4WJsoXJOUBxu90IBoP6bfLkyQAGek+eeOIJPPzww1i2bBmamprw3HPPobe3Fy+++CIAIBwOY8uWLdi4cSMWLlyImTNnYvv27WhtbcWePXuMfWcWMHvdEJkbSbPzc8zGxeTso8+hY1LgL+vcQiQPlhobI+cA5fjx46ivr8e0adNw22234cSJEwCAkydPor29HYsWLdK39fl8mD9/Pg4cOAAAOHz4MOLxeNY29fX1aGpq0rcZTjQaRSQSybqJIF1mbO4Qj4yNpPQ5KPrwGhsxq6WDW7N+V/LmdpEc9ERZVvIUJKcA5corr8S2bduwa9cuPPPMM2hvb8e8efNw/vx5tLe3AwBqa2uz/qa2tlZ/rL29HV6vF9XV1SNuM5wNGzYgEAjot4aGhlx22zQBkxthmRtJbZ8j/XEktVppicicoCw7fQ6dHuN/V1lzC/HYkkm0UmNW8hQmpwBl8eLFuPnmmzFjxgwsXLgQr776KgDgueee07dRFCXrb1RVveC+ocbaZt26dQiHw/rtzJkzuey2acxe2EzmRlJrZFR1YPVY2YRZimobM4cHOQEfWWEqS40NUVCZcXl5OWbMmIHjx4/r1TxDe0I6Ojr0XpVgMIhYLIZQKDTiNsPx+XyorKzMuolAG+Ixa1rusMTJfF53Ccq9A3O3yDjMo1d6SNh7Jbt0DorxQzycgI+sMJWlxoZwF/LH0WgU77//Pr75zW9i2rRpCAaD2L17N2bOnAkAiMVi2LdvHx599FEAwKxZs+DxeLB7924sX74cANDW1oajR4/iscceK/CtWE8LHLqjCcSTKXhcxk4rI/uiZlVlXvTE+qRMlGWegn3M7EFhBQ9ZQSs1DvfF8d4nEUyfUjH+Px59wOHCzXPcPhclioIJvoLChILk9Mpr167FjTfeiM9//vPo6OjA3/3d3yESiWDFihVQFAXNzc1Yv349pk+fjunTp2P9+vUoKyvD7bffDgAIBAJYuXIl1qxZg0mTJmHixIlYu3atPmQkm8zp58N9cdRM8Bn6/FojKetVfKDUg487+0ydttwsMicoy87MNa44AR9ZZeqkMvz72TD+7Kdv270refvC5HK8ueZq214/pwDl7Nmz+M53voNz585h8uTJmDNnDg4ePIjGxkYAwIMPPoi+vj7cf//9CIVCuPLKK/HGG2+goiIdPW7atAlutxvLly9HX18fFixYgK1bt8Llkq+71VWioNLvRqQ/gc5e4wMU2ZP50uvxyNWDoqpquvdK0uBQZmaW73fqcwvJ+Zsiedz4lXoc/SQiZZGAKHIKUHbs2DHq44qioKWlBS0tLSNu4/f7sXnzZmzevDmXlxZWVZkXkf4EwgaXRKqqKv1VfLrUWK4elO5oQj+pyPrZy8zM743svymSx1998wv4i6umQVXHH6DkGsrk8NRSsm9wySGqyzz46DPju6N7YkkktEZS0qt4Wae7146lz10i3SKNTmDmHDoyl+6TfFwlCnJOKiEdFwssUMCk8XLt6tHrLoHfI+dhMrsM2yyyD63Jzsw5dGQu3ScqNnK2fAIxa7w8c0n4seaREZWegyJpD4qsPVey0743ZsyhI3PpPlGxYYBSoHQiqLHj5U5oJLV9ly0HRbvK5mq39vC4SvTSRsN7Jtk7RiQNBigF0q7EQoafSOVvJAMmzmdhJs6VYb+AST2TIX1+G3kDf6JiwQClQAGTEkGd0Ehq+y5bmTFzUOynffZGz6HDIR4ieTBAKVA6EdTgE6kDGkl5q3i0uTJ4lW0Xs+bQSQ/x8NgSiY4BSoHMSgTtdEBXdOZ8FimJJivSS1ElDg5lZ0b+UiqlcgkDIokwQCmQWXM2pOdrkPdEqu17SgW6JVowi7PI2s+M9Xi6Y1zJmEgmDFAKFDCpUsUJ1QZ+j0ufw0WmPBReZdvPjMBf+w76PZyAj0gGDFAKpJ1II/0JQyeVCjugzBhI779MCwY6IUFZdtr3xsih0xBzi4ikwgClQJldxREDT6ZOmfHSzGnLzZJe8Vbuz15mAROqeJwwbEpUTBigFChrUikjAxSHnEzNyCUwk6qq6d4rXmnbxoxlEpwwbEpUTBigGMDo1VdVVXXMyVTvqpdkiKcvnkQsmQLAIR47acGhkUM82ndQ9mFTomLBAMUARvcS9MdTiCUGGknZx8tlG+LR9tPjUlDmZSKlXYwO+geeyxlBP1GxYIBigHQvgTGNsDbu7oRGUrbp7tNDa15pF2l0gsz5hYyaQyfEoTsiqTBAMYDRCX1OaiTTE25JEqA4JDlZdplz6HRFjZlDh8eWSC4MUAxgdEKfk06k6SthOXJQuFaLGHxul957aFTPJI8tkVwYoBjA6OnunXQiNaMaw0xcq0Uc6ZXCDeqZdEjiOVGxYIBiAKPXDXHSiVTWHBQnfPayM3qlcO33GWAVD5EUGKAYwOhGODMHRXbS5aDopagMUOxm9ErhTlghnKiYMEAxAHNQRpaZg6Kq4q9ozB4UcVSXGzd0qqqqfmxlL90nKhYMUAxg9KRSjspBGWzo40kVPbGkzXszNi04DLARs13AwN637mgCicFyZQafRHJggGIAoyeVctJVfKnHBa9r4Gtm9IrPZuBCgeIwcpI/7Tl8bq5kTCQLBigG0BozoyaVctJVvKIoUs0myzwFcRiZg8LjSiQfBigG0JJkjZpUymlX8UaXYZsp/dnLHxzKzsglJHhcieTDAMUARk8q5bSrPZkqeZyUoCy7gIHl+9pcKjyuRPJggGIQvTvagBlTnVZtkC7DFjsHpT+eRH98YJHGABsy21Ub2YPisKCfqBgwQDGIli8SKrCXoD+eRF88OficzjiZyjKbrNZz5SpRUOFz27w3pFfHGdErqc9v44ygn6gYMEAxiFEJfU5sJGXJQUlPkOeRfpFGJ8jMQSl0Dh0nVcYRFQsGKAYxqhF2YiOpXQmLXmbMWWTFoq1onEypBSefa0M8TumVJCoGBQUoGzZsgKIoaG5u1u+7++67oShK1m3OnDlZfxeNRrFq1SrU1NSgvLwcS5cuxdmzZwvZFdsZVUrrxEYyIMkQD/MUxOL3uOD3DJyiCh3mYRUPkXzyDlAOHTqEp59+GldcccUFj11//fVoa2vTb6+99lrW483Nzdi5cyd27NiB/fv3o7u7G0uWLEEyKf5MoyMxatZLJ17pGVkuaiY9OHRIcrITGFUBph3bagf9roicLq8Apbu7G3fccQeeeeYZVFdXX/C4z+dDMBjUbxMnTtQfC4fD2LJlCzZu3IiFCxdi5syZ2L59O1pbW7Fnz57834nNqgyqVHHSNPcarZExItnRTE6bf8YJjPpdOTHwJ3K6vAKUBx54ADfccAMWLlw47ON79+7FlClTcMkll+Cee+5BR0eH/tjhw4cRj8exaNEi/b76+no0NTXhwIEDwz5fNBpFJBLJuolGn0224B4U513Fa41MSPQcFDZiwjFu6JRDPESyyblMZMeOHTh8+DDeeeedYR9fvHgxbrnlFjQ2NuLkyZP4m7/5G1xzzTU4fPgwfD4f2tvb4fV6L+h5qa2tRXt7+7DPuWHDBvz4xz/OdVctZdQwRmaSrFPoOSiD1RiiJv+yEROPPsRTwO9KVVWEOQEfkXRyClDOnDmD733ve3jjjTfg9/uH3ebWW2/V/7+pqQmzZ89GY2MjXn31VSxbtmzE5x6t4Vq3bh1Wr16t/zsSiaChoSGXXTedUbNeOjFRs7p84LOJJVLoj6dQ6hVzsTY2YuLRA/+e/H9XvbEk4kmuZEwkm5yGeA4fPoyOjg7MmjULbrcbbrcb+/btw09+8hO43e5hk1zr6urQ2NiI48ePAwCCwSBisRhCoVDWdh0dHaitrR32dX0+HyorK7NuojGqzNiJOSjlXhfcJQPBp8izyXKuDPEEDOiZ1IYWve4SlHIlYyJp5BSgLFiwAK2trXj33Xf12+zZs3HHHXfg3Xffhct14Y///PnzOHPmDOrq6gAAs2bNgsfjwe7du/Vt2tracPToUcybN6/At2Of6rJ0tUEhk0o5MQdFlhWNnTi8Jjsjqngyk59FHV4kogvlNMRTUVGBpqamrPvKy8sxadIkNDU1obu7Gy0tLbj55ptRV1eHU6dO4aGHHkJNTQ1uuukmAEAgEMDKlSuxZs0aTJo0CRMnTsTatWsxY8aMEZNuZaA1wImUip5YEhPynAXWqVfxgVIPznXHhA5Q0os0Oic4lF26ZzL/njenLb5JVCwMnUvd5XKhtbUV27ZtQ2dnJ+rq6vCtb30LL730EioqKvTtNm3aBLfbjeXLl6Ovrw8LFizA1q1bh+2BkYXf44LPXYJoIoXO3pgBAYqzGsmB99NTUENjNidOkie7agN63pj8TCSnggOUvXv36v9fWlqKXbt2jfk3fr8fmzdvxubNmwt9eaFUlXnwaSSKzt44LrpwephxcWojKfqCgbFECj2xgRwqXmmLI2BAFY82bMrycSK5cC0eAxU6Xu7kRtKIZEczacMAigJU+J312cvMiNwlTsBHJCcGKAYKFDjrpZMbSaOmLDeLNvQUKPXAVcJESlGkA5RY3snn+jT35RziIZIJAxQDFTqMoTWSlX7nNZJGJDuaKcSrbCFpga2WfJ4PVmcRyYkBioEKnQvFqRU8gHFTlptFb8QclpwsO7+nBF73wGkq30kQnTj5IVExYIBioKqywmaTdfJYuXb1Kup6PE5NTpadoigFV/KEWcVDJCUGKAYKFDjEk16sznkn0qoy0XNQeJUtKn017Hx7JrmEAZGUGKAYqNAFA518FV9t0FIAZnFy75XsAgX2oDAHhUhODFAMpF/p5Z0k69yreNGreNJzZTiv90p2VQUMD6qqqn/nWMVDJBcGKAaqLrDM2MlX8dpVcF88if54ftUYZnLyZy+7QpLP++JJxJKpgefhsSWSCgMUAxXcFe3gtWAqfG5oldMRAYd5nNx7JbtCks+136LHpaDMK+9SGkTFiAGKgfQTaV9+KxrrOSgObCRLSpR0ErGAAYqTS7xlV0jyeTr/xMuVjIkkwwDFQFoXciyRQl8ewxhObyRFruTRc1BYiiqc6rL81+NhBQ+RvBigGKjM64LHNXCVltfVnsMbyfSVsHhzoTg9OJSZnoNSQA9KNY8rkXQYoBhIUZT06qsFnEyd2kgWWoZtlkQyha7+BID01TqJo5AqnswhHiKSCwMUg1XlWcmT2Ug6tdpAe1/5lmGbJbM6pNLvtnFPaDiFrITNIR4ieTFAMVi+jXBkMDgBnDuhVDqJWKwhHq3hq/C74XbxJyEa7XsT7s09+TzM8nEiafFsbLB8hzG0vIwKn3MbyfR6PGL1oDh9aE12evJ5Mvfkcx5bInk5syW0Ub45KOl1eJx7Ii0k2dFMYW0YgHkKQirzuuB1aSsa5/q74gzBRLJigGKwfHNQwkVwpVct6hBPEXz2MlMUJe9JEEOs4iGSFgMUg+Wbg9JZBFfxhc60axYuJie+qjxL1NM5KM79XRE5FQMUg1WV5znE01sEQzwFzAhqpk5Ocy+8vHO7WMVDJC0GKAbTG+EchzGKYUIpvRpDsHlQwr3O772SXd65XewdI5IWAxSDVeU5jKEvVufgRlIL3rqjCcQHV5gVAXtQxJdPbld/PIloIpX190QkDwYoBqvK80ov5OCFAjWVGVexIvWi8CpbfNV5VIBpvyl3iYIJPk7ARyQbBigGy7eKpxgaSVeJos/UKlIeSroHxbm9V7LLZ6HJzOosrmRMJB8GKAbTklz74yn05zCpVLE0kuk8FHFKjbUcFCfn/8gukEduVzEE/UROxgDFYBU+N1wlA1druQxjhItgiAfIP0fHTCHOgyI87djkMguxPgGfw4N+IqdigGKwgRWNc2+E9R4Uh1/t5fPZmCmZUhHp54q3otNyu3LJQdGHeBz+myJyKgYoJsh1UqlUStV7W5w8DwqQvpoN5Tjhllm6+uPQ1p/jUIC48sntKoblI4icjAGKCXJdHr6rP1E0jaQ+064gVTzaVXa51wWvmz8HUeXT8xbSc4vYM0YkI56RTZDrdPfaVWGZ1wWf22XafomgWrAclGJJTpZd9eAMzdHE+JPPwxziIZJaQQHKhg0boCgKmpub9ftUVUVLSwvq6+tRWlqKq6++GseOHcv6u2g0ilWrVqGmpgbl5eVYunQpzp49W8iuCKUqx0XximmsPKB/NoIEKINX2U7vuZJdudcF92Dy+XiDWy4CSSS3vAOUQ4cO4emnn8YVV1yRdf9jjz2Gxx9/HE8++SQOHTqEYDCIa6+9Fl1dXfo2zc3N2LlzJ3bs2IH9+/eju7sbS5YsQTI5/rJckeXaHZ0eK3f+VXy+i76ZJcxZZKWgKEpGJc84A//BC4Ri+F0ROVFeAUp3dzfuuOMOPPPMM6iurtbvV1UVTzzxBB5++GEsW7YMTU1NeO6559Db24sXX3wRABAOh7FlyxZs3LgRCxcuxMyZM7F9+3a0trZiz549xrwrm1Xn2EvQqa8F4/xGUmtkRMtBYYAivpwD/yLqmSRyorwClAceeAA33HADFi5cmHX/yZMn0d7ejkWLFun3+Xw+zJ8/HwcOHAAAHD58GPF4PGub+vp6NDU16dsMFY1GEYlEsm4iq8pxWm6tsa4ud/6JVLR5UNKTefEqW3S5TvKXXoCTx5ZIRjkvULFjxw4cPnwY77zzzgWPtbe3AwBqa2uz7q+trcXp06f1bbxeb1bPi7aN9vdDbdiwAT/+8Y9z3VXb5FoSGeopnkYyvSqtGEM8nX3FMUGeE1TlPHTKY0sks5x6UM6cOYPvfe97eOGFF+D3+0fcbui6F6qqjrkWxmjbrFu3DuFwWL+dOXMml922nNYVrQUeYymmE6n2HiP9CSRTqs17w0oPmVTlMHTaH0+iPz6wkjHnQSGSU04ByuHDh9HR0YFZs2bB7XbD7XZj3759+MlPfgK32633nAztCeno6NAfCwaDiMViCIVCI24zlM/nQ2VlZdZNZOmu6HEO8RRRI5lZLRMRIA+lGFaRdopchge1356rREEFVzImklJOAcqCBQvQ2tqKd999V7/Nnj0bd9xxB95991184QtfQDAYxO7du/W/icVi2LdvH+bNmwcAmDVrFjweT9Y2bW1tOHr0qL6N7HKtVOksokoSj6sEEwYbDBFKjTkPijxy+V1lLhTIlYyJ5JTTpUVFRQWampqy7isvL8ekSZP0+5ubm7F+/XpMnz4d06dPx/r161FWVobbb78dABAIBLBy5UqsWbMGkyZNwsSJE7F27VrMmDHjgqRbWWmBRk8siVgiNeYMpem5OIqjkQyUetAdTQy+73Jb96WYeq9kl0sPSjFVxhE5leF9nw8++CD6+vpw//33IxQK4corr8Qbb7yBiooKfZtNmzbB7XZj+fLl6Ovrw4IFC7B161a4XM6YRbXC74GiAKo60NU8ucI36vbF1IMCDLzPjzv7hKjkYQ+KPAI5TIDIFaqJ5FdwgLJ3796sfyuKgpaWFrS0tIz4N36/H5s3b8bmzZsLfXkhuUoUVPo9CPfFEe6LjRmghIvsZJrPwm9mSKXU9JV2kXz2MsuliiesJ54z8CSSFdfiMcl4u6NVVU1fxRfJEI9ejWFzD0p3LIFUkSzS6ATVOSSfc5I2IvkxQDHJeK/2uqPpcttiuYrPdT4Ls2g9V35PCfweZwwvOllOOSj68hHF8ZsiciIGKCYZ76J42snW5y6eRlKU6e7TV9nF0XMlOy3Y6Isnx1zRmMeWSH4MUExSXTa+kshiXKyuSpDZZItpgjwnqPC54Rpc0Xis4Ja5RUTyY4BiEm0YY+wTafGtFxLQk2TF6EFh/okcFEUZ94KBXASSSH4MUEwSGGciqL4kfBE1kqLkoBRbebcTjHeyNpaPE8mPAYpJtBNpaIwTaTHO15DrUgBmCeuTebERk0XVOHvfwpyojUh6DFBMMt5E0GJsJKvGmZ9jtmIMDmWnB7dj9kzy2BLJjgGKScZbElmMY+WZ+TkpG1c0Tn/2xRMcyk4f4hllkr9oIoneWHJwex5bIlkxQDGJtq7OWLOlFuN8DZWDjUxKBbqiCdv2I8wqHulov5PQKIG/1rtSogAVfq5kTCQrBigmybkHpYiu9PweF0oH53yxc5iHs43KJ12iPvLvSg/6Sz0oKeFKxkSyYoBiEq3R6+pPIJFMjbhdsV7F5zIrqFmKsfdKduncrpEDWw7dETkDAxSTZJYNR/pHHsYo1qv4qnHOtGumYuy9kt14AlutV66YSveJnIgBikncrhJU+AbGv0cbxijWq/jxzmdhFlVVi7b3SmbjWWiSFTxEzsAAxURjzZiqqqqe0Fds3dF2r8fTG0siniyuRRqdYDwzNIeLtFeSyGkYoJioeow5G/riScQG81OK7WRqdw6KFjR6XSV6wi6Jr0qv4hm55y2kr8NTXEE/kdMwQDFRetbL4U+mWuPsdZWgzFtcjWRgHNUYZtLzFMo8UBRWeshCyxfqjSURTQy/ojGHeIicgQGKicZa2ExfrK4IG8mxgjezcRhAThV+N7SfykjDPDy2RM7AAMVEVWNMKtVZxOuF6LkENg/x8CpbLiUl6RWNR/rudPZxiIfICRigmEjrjg6PMF5ezI3keBd9M0tIL0VlIyab6jFK1DN7JolIXgxQTDRWI6yfSIuwkUznoNgzxKN99tVsxKQz3qHTavagEEmNAYqJxjyRFvE8HHaXGYeLuPdKdmNV8hTz0CmRkzBAMdFYs6UWczJfZpmxqlq/onEnS1GlNVr+UiyRQo+2kjGDTyKpMUAxkd5LMOKVXvFexWv5OYmUim4bVjROD68V32cvu3Tgf+HvSusZUxSgws9jSyQzBigm0qdzHykHpU+bi6P4ruL9nhJ43QNfPzvmQinmBGXZjTZ0qi1fUOn3wMWVjImkxgDFRIGMPItU6sJhjGJdKBAAFEXRE1TtyEMJc6FAaVWPknxezL2SRE7DAMVE2pWeqgJdw6xoXOyJmlU2ziZbzAnKsqsaZQmJUJGubUXkRAxQTORzu/Qp7IcbL+8s8qv4gI2zyTIHRV6BUap4WMFD5BwMUExWPcry8MV+FV81Rhm2WfrjSUQTg4s0FulnL7PRvjfF3itJ5CQMUEwWGCFRtj+eRH+8uBtJu+ZC0Ro2V4mCCT63pa9NhdOHeEbLQWEPCpH0GKCYLD3fR3Z3NBvJjHJRi2eT1XuuSotvkUYn0IKP7mgC8WQq67FirowjcpqcApSnnnoKV1xxBSorK1FZWYm5c+fiV7/6lf743XffDUVRsm5z5szJeo5oNIpVq1ahpqYG5eXlWLp0Kc6ePWvMuxFQ5oRkmdhIjj3TrllCPVyrRWaVpZ4RVzQOcQkDIsfIKUC56KKL8Mgjj+Cdd97BO++8g2uuuQbf/va3cezYMX2b66+/Hm1tbfrttddey3qO5uZm7Ny5Ezt27MD+/fvR3d2NJUuWIJlMGvOOBBMYoVKFC5rZt2CgNlcG12qRk6tEQaV/+OA2zDJjIsfIaWzhxhtvzPr33//93+Opp57CwYMHcfnllwMAfD4fgsHgsH8fDoexZcsWPP/881i4cCEAYPv27WhoaMCePXtw3XXX5fMehFY1QqUKx8ozV3u2JwelmD972VWVeRDui+vBpibdM8ngk0h2eeegJJNJ7NixAz09PZg7d65+/969ezFlyhRccskluOeee9DR0aE/dvjwYcTjcSxatEi/r76+Hk1NTThw4MCIrxWNRhGJRLJushhp3ZBwH9eCGWvRN7NoPTbF3HslO+13pQ3XadgzSeQcOQcora2tmDBhAnw+H+677z7s3LkTl112GQBg8eLFeOGFF/Dmm29i48aNOHToEK655hpEo1EAQHt7O7xeL6qrq7Oes7a2Fu3t7SO+5oYNGxAIBPRbQ0NDrrttm5GGMXgVP3KFk9mKff4ZJwiMsBBnMS/ASeQ0OZePXHrppXj33XfR2dmJl19+GStWrMC+fftw2WWX4dZbb9W3a2pqwuzZs9HY2IhXX30Vy5YtG/E5VVUdNVF03bp1WL16tf7vSCQiTZCSzkEZ2hXNK730YorxMb8DRgoX+fwzTpCeCyX9u4onU+gaXHiymHsmiZwi5wDF6/Xii1/8IgBg9uzZOHToEP7hH/4BP/vZzy7Ytq6uDo2NjTh+/DgAIBgMIhaLIRQKZfWidHR0YN68eSO+ps/ng8/ny3VXhTB2D0rxnki1JNVYMoW+eBJlXmvKrblei/yGm0Mn8/85QzCR/AqeB0VVVX0IZ6jz58/jzJkzqKurAwDMmjULHo8Hu3fv1rdpa2vD0aNHRw1QZJbZS5CJV/FAmdcFj2ug18TKUmNOcy+/qmFmaNb+v9Lv5krGRA6Q0yXrQw89hMWLF6OhoQFdXV3YsWMH9u7di9dffx3d3d1oaWnBzTffjLq6Opw6dQoPPfQQampqcNNNNwEAAoEAVq5ciTVr1mDSpEmYOHEi1q5dixkzZuhVPU6jL4jXlz2Mwav4gRWNA6VenOuOorM3jvqqUktet1OfDr14e69kVzVM/hITz4mcJacA5dNPP8Vdd92FtrY2BAIBXHHFFXj99ddx7bXXoq+vD62trdi2bRs6OztRV1eHb33rW3jppZdQUVGhP8emTZvgdruxfPly9PX1YcGCBdi6dStcLpfhb04EWgCSTKnojiZQMWT+hmI/mVaVeQYCFAsXDAxzQTnpDTdDM4N+ImfJKUDZsmXLiI+VlpZi165dYz6H3+/H5s2bsXnz5lxeWlp+jwt+Twn64yl09sb1AEVf1KzIG8mRyrDN1MkF5aQ33AzNHLojchauxWMBfUKyvsyTKXNQAOtnk40mkuiNDcxaXMwJyrLTq+Myet5CvRziIXISBigWGDohWSyRQg8bSQAjLwVgFq2nRlGACn9xLtLoBMP1oGgXAFyHh8gZGKBYYOiieNpVHxvJkZcCMIs+/0ypByWs9JCWVqLe1Z9AYnBFY05+SOQsDFAsMHQYI9zLRlJjdQ6K1ohxoUC5VWYE9pH+gcnZ0pMf8tgSOQEDFAukF8Ub6CXoZIKszur1eLTcHyZSys3tKtF7H7XvTiers4gchQGKBYaOl6cXNOOVXmCYCbfMxAoe5xj6uwrz2BI5CgMUCwSGDPHwSi9NH+KxqIqHi8k5R7o6buD3xCoeImdhgGKBqiGVKrzSS6u2vAeFjZhTjNQzyd8VkTMwQLFAemEzbaycV/Eay6t4OJmXY2RWxyWSKXQNJsvyd0XkDAxQLFA1Qpkxc1DSw1/98RT640nTX485KM6h9771xfVKHoDBJ5FTMECxQFXGiRRgD0qmCl965Vkr8lDCHAZwjPRK4TE9r6vC54bbxdMakRPwl2yB9Il0YEVjfcbLcjaSAysaXzgrqFn0HJQin8HXCbTvTag3jpBeGcffFJFTMECxgBagxJIp9MWTGT0obCSBzCEw8/NQQj1syJwis2dSy+/iBHxEzsEAxQKlHhe8g93OA1d7Wg4KG0ngwjJsM3EVaedIz0IcYwUPkQMxQLGAoijpRrg3xrk4hrBquvt4MoXu6GClB6+0pZe5hASrs4ichwGKRbRG+Fx3DF1sJLOku+rNHeLJTMJlQya/qow5dFidReQ8DFAsop04P/qsV7+vsshXMtZkJjuaSbvKrvSnK4dIXtpvKtIfx2c90YH7mNdF5BgMUCwSGDxxnj7XAwCo8LMcUjN0RlCzhDmLrKNoga2qAh991geAPShETsIW0iLaifPU+d6sf1PmejzmDvEwkdJZPK4STPAN9EKeGgz8GXwSOQcDFItojfDp84MnUnZF66rLrVmPh4mUzqMdy487B3tQeGyJHIMBikW0q/bTn7EHZSirJmpLJ1IyOHQK7XeUTKlZ/yYi+TFAsYi27k4skRr4N6/0dFrAYPZU9+FebRZZfvZOMXRiNgYoRM7BAMUiQxtFnkjTrJpJlqWozjN0ssMAh06JHIMBikWGXulxSu40LWDoiSX1HiYzMAfFeRj4EzkXAxSLDD1xspFMq/B7oAxOS2LmMI+2xABzUJwj83c1weeGh6X7RI7BX7NFhgYkbCTTXCUKKv3mlxpzHR7nyayGY9BP5CwMUCwytAeFjWQ2KyZr4zwozpOZg8LjSuQsDFAsMsGXPb06T6bZqiwoNe7kEI/jZAb6/E0ROQsDFIsoisKT6Si0MuyQSZU8yZSKSL+2SCM/e6fQJvkDGHgSOQ0DFAtldkezHDJberp7c3pQIlzJ2JGygn4eVyJHYYBiocwTKBvJbGbnoGhzoLDSw1mYg0LkXDmdqZ966ilcccUVqKysRGVlJebOnYtf/epX+uOqqqKlpQX19fUoLS3F1VdfjWPHjmU9RzQaxapVq1BTU4Py8nIsXboUZ8+eNebdCE7rgi73uuB1s5HMpH02nSZV8Wj5JwwMnSWQ1YPCXkkiJ8mplbzooovwyCOP4J133sE777yDa665Bt/+9rf1IOSxxx7D448/jieffBKHDh1CMBjEtddei66uLv05mpubsXPnTuzYsQP79+9Hd3c3lixZgmQyaew7E5DWg8Kx8guZnSTLWWSdyed2oczrAnDhrLJEJLecApQbb7wRf/qnf4pLLrkEl1xyCf7+7/8eEyZMwMGDB6GqKp544gk8/PDDWLZsGZqamvDcc8+ht7cXL774IgAgHA5jy5Yt2LhxIxYuXIiZM2di+/btaG1txZ49e0x5gyLRTqC8ir+QFjiYlYMSZomxY+mBP39XRI7izvcPk8kk/uVf/gU9PT2YO3cuTp48ifb2dixatEjfxufzYf78+Thw4ADuvfdeHD58GPF4PGub+vp6NDU14cCBA7juuuuGfa1oNIpoNKr/OxKJ5LvbttK6oNlIXkj7TN5vi+D/+UVr1mOqmr3tkH9e8PhwWx3/tHvgdTgM4DhfrqtEe6Qfl9RW2L0rRGSgnAOU1tZWzJ07F/39/ZgwYQJ27tyJyy67DAcOHAAA1NbWZm1fW1uL06dPAwDa29vh9XpRXV19wTbt7e0jvuaGDRvw4x//ONddFU5dlR8AEAz4bd4T8dQFSgEA57pj2H7wI9Nep76Kn73T/OMdX8NnPTHUV5XavStEZKCcA5RLL70U7777Ljo7O/Hyyy9jxYoV2Ldvn/64oihZ26uqesF9Q421zbp167B69Wr935FIBA0NDbnuuu1uvKIe0XgS13y5duyNi8yX6yqx6dav4PT53mEfVzD892Okr81wd5d6XVj2tYvy3EMSld/jYnBC5EA5Byherxdf/OIXAQCzZ8/GoUOH8A//8A/4wQ9+AGCgl6Surk7fvqOjQ+9VCQaDiMViCIVCWb0oHR0dmDdv3oiv6fP54PP5ct1V4ZR6Xbhr7lS7d0NYN81k8EBERAMKrnVVVRXRaBTTpk1DMBjE7t279cdisRj27dunBx+zZs2Cx+PJ2qatrQ1Hjx4dNUAhIiKi4pJTD8pDDz2ExYsXo6GhAV1dXdixYwf27t2L119/HYqioLm5GevXr8f06dMxffp0rF+/HmVlZbj99tsBAIFAACtXrsSaNWswadIkTJw4EWvXrsWMGTOwcOFCU94gERERySenAOXTTz/FXXfdhba2NgQCAVxxxRV4/fXXce211wIAHnzwQfT19eH+++9HKBTClVdeiTfeeAMVFens+k2bNsHtdmP58uXo6+vDggULsHXrVrhcLmPfGREREUlLUdXhizRFFolEEAgEEA6HUVlZaffuEBER0Tjk0n5zvnUiIiISDgMUIiIiEg4DFCIiIhIOAxQiIiISDgMUIiIiEg4DFCIiIhIOAxQiIiISDgMUIiIiEg4DFCIiIhJOzqsZi0Cb/DYSidi8J0RERDReWrs9nknspQxQurq6AAANDQ027wkRERHlqqurC4FAYNRtpFyLJ5VK4ZNPPkFFRQUURTH0uSORCBoaGnDmzBnHr/NTTO8VKK73y/fqXMX0fvlenUdVVXR1daG+vh4lJaNnmUjZg1JSUoKLLrrI1NeorKx09JckUzG9V6C43i/fq3MV0/vle3WWsXpONEySJSIiIuEwQCEiIiLhMEAZwufz4Uc/+hF8Pp/du2K6YnqvQHG9X75X5yqm98v3WtykTJIlIiIiZ2MPChEREQmHAQoREREJhwEKERERCYcBChEREQmnKAOUf/qnf8K0adPg9/sxa9Ys/Pa3vx11+3379mHWrFnw+/34whe+gJ/+9KcW7Wn+NmzYgK9//euoqKjAlClT8N//+3/HBx98MOrf7N27F4qiXHD7wx/+YNFe56+lpeWC/Q4Gg6P+jYzHFQCmTp067HF64IEHht1epuP6m9/8BjfeeCPq6+uhKAp+8YtfZD2uqipaWlpQX1+P0tJSXH311Th27NiYz/vyyy/jsssug8/nw2WXXYadO3ea9A5yM9r7jcfj+MEPfoAZM2agvLwc9fX1+PM//3N88sknoz7n1q1bhz3e/f39Jr+b0Y11bO++++4L9nnOnDljPq+Ix3as9zrc8VEUBf/rf/2vEZ9T1ONqpqILUF566SU0Nzfj4YcfxpEjR/DNb34TixcvxkcffTTs9idPnsSf/umf4pvf/CaOHDmChx56CN/97nfx8ssvW7znudm3bx8eeOABHDx4ELt370YikcCiRYvQ09Mz5t9+8MEHaGtr02/Tp0+3YI8Ld/nll2ftd2tr64jbynpcAeDQoUNZ73P37t0AgFtuuWXUv5PhuPb09OArX/kKnnzyyWEff+yxx/D444/jySefxKFDhxAMBnHttdfq63MN5+2338att96Ku+66C//+7/+Ou+66C8uXL8fvfvc7s97GuI32fnt7e/H73/8ef/M3f4Pf//73eOWVV/Dhhx9i6dKlYz5vZWVl1rFua2uD3+834y2M21jHFgCuv/76rH1+7bXXRn1OUY/tWO916LH5+c9/DkVRcPPNN4/6vCIeV1OpRea//bf/pt53331Z933pS19Sf/jDHw67/YMPPqh+6Utfyrrv3nvvVefMmWPaPpqho6NDBaDu27dvxG3eeustFYAaCoWs2zGD/OhHP1K/8pWvjHt7pxxXVVXV733ve+rFF1+splKpYR+X9bgCUHfu3Kn/O5VKqcFgUH3kkUf0+/r7+9VAIKD+9Kc/HfF5li9frl5//fVZ91133XXqbbfdZvg+F2Lo+x3O//2//1cFoJ4+fXrEbZ599lk1EAgYu3MGG+69rlixQv32t7+d0/PIcGzHc1y//e1vq9dcc82o28hwXI1WVD0osVgMhw8fxqJFi7LuX7RoEQ4cODDs37z99tsXbH/dddfhnXfeQTweN21fjRYOhwEAEydOHHPbmTNnoq6uDgsWLMBbb71l9q4Z5vjx46ivr8e0adNw22234cSJEyNu65TjGovFsH37dvzlX/7lmAtnynpcNSdPnkR7e3vWcfP5fJg/f/6Iv19g5GM92t+IKhwOQ1EUVFVVjbpdd3c3GhsbcdFFF2HJkiU4cuSINTtYoL1792LKlCm45JJLcM8996Cjo2PU7Z1wbD/99FO8+uqrWLly5Zjbynpc81VUAcq5c+eQTCZRW1ubdX9tbS3a29uH/Zv29vZht08kEjh37pxp+2okVVWxevVqfOMb30BTU9OI29XV1eHpp5/Gyy+/jFdeeQWXXnopFixYgN/85jcW7m1+rrzySmzbtg27du3CM888g/b2dsybNw/nz58fdnsnHFcA+MUvfoHOzk7cfffdI24j83HNpP1Gc/n9an+X69+IqL+/Hz/84Q9x++23j7qY3Je+9CVs3boVv/zlL/HP//zP8Pv9uOqqq3D8+HEL9zZ3ixcvxgsvvIA333wTGzduxKFDh3DNNdcgGo2O+DdOOLbPPfccKioqsGzZslG3k/W4FkLK1YwLNfRKU1XVUa8+h9t+uPtF9dd//df4j//4D+zfv3/U7S699FJceuml+r/nzp2LM2fO4H//7/+NP/mTPzF7NwuyePFi/f9nzJiBuXPn4uKLL8Zzzz2H1atXD/s3sh9XANiyZQsWL16M+vr6EbeR+bgOJ9ffb75/I5J4PI7bbrsNqVQK//RP/zTqtnPmzMlKLr3qqqvwta99DZs3b8ZPfvITs3c1b7feeqv+/01NTZg9ezYaGxvx6quvjtp4y35sf/7zn+OOO+4YM5dE1uNaiKLqQampqYHL5boguu7o6LggCtcEg8Fht3e73Zg0aZJp+2qUVatW4Ze//CXeeustXHTRRTn//Zw5c6SM0MvLyzFjxowR91324woAp0+fxp49e/BXf/VXOf+tjMdVq8rK5fer/V2ufyOSeDyO5cuX4+TJk9i9e/eovSfDKSkpwde//nXpjnddXR0aGxtH3W/Zj+1vf/tbfPDBB3n9hmU9rrkoqgDF6/Vi1qxZetWDZvfu3Zg3b96wfzN37twLtn/jjTcwe/ZseDwe0/a1UKqq4q//+q/xyiuv4M0338S0adPyep4jR46grq7O4L0zXzQaxfvvvz/ivst6XDM9++yzmDJlCm644Yac/1bG4zpt2jQEg8Gs4xaLxbBv374Rf7/AyMd6tL8RhRacHD9+HHv27MkreFZVFe+++650x/v8+fM4c+bMqPst87EFBnpAZ82aha985Ss5/62sxzUndmXn2mXHjh2qx+NRt2zZor733ntqc3OzWl5erp46dUpVVVX94Q9/qN5111369idOnFDLysrU73//++p7772nbtmyRfV4POr/+T//x663MC7/43/8DzUQCKh79+5V29ra9Ftvb6++zdD3umnTJnXnzp3qhx9+qB49elT94Q9/qAJQX375ZTveQk7WrFmj7t27Vz1x4oR68OBBdcmSJWpFRYXjjqsmmUyqn//859Uf/OAHFzwm83Ht6upSjxw5oh45ckQFoD7++OPqkSNH9KqVRx55RA0EAuorr7yitra2qt/5znfUuro6NRKJ6M9x1113ZVXl/du//ZvqcrnURx55RH3//ffVRx55RHW73erBgwctf39DjfZ+4/G4unTpUvWiiy5S33333azfcTQa1Z9j6PttaWlRX3/9dfW//uu/1CNHjqh/8Rd/obrdbvV3v/udHW9RN9p77erqUtesWaMeOHBAPXnypPrWW2+pc+fOVT/3uc9JeWzH+h6rqqqGw2G1rKxMfeqpp4Z9DlmOq5mKLkBRVVX9x3/8R7WxsVH1er3q1772tazS2xUrVqjz58/P2n7v3r3qzJkzVa/Xq06dOnXEL5RIAAx7e/bZZ/Vthr7XRx99VL344otVv9+vVldXq9/4xjfUV1991fqdz8Ott96q1tXVqR6PR62vr1eXLVumHjt2TH/cKcdVs2vXLhWA+sEHH1zwmMzHVSuJHnpbsWKFqqoDpcY/+tGP1GAwqPp8PvVP/uRP1NbW1qznmD9/vr695l/+5V/USy+9VPV4POqXvvQlYYKz0d7vyZMnR/wdv/XWW/pzDH2/zc3N6uc//3nV6/WqkydPVhctWqQeOHDA+jc3xGjvtbe3V120aJE6efJk1ePxqJ///OfVFStWqB999FHWc8hybMf6Hquqqv7sZz9TS0tL1c7OzmGfQ5bjaiZFVQczA4mIiIgEUVQ5KERERCQHBihEREQkHAYoREREJBwGKERERCQcBihEREQkHAYoREREJBwGKERERCQcBihEREQkHAYoREREJBwGKERERCQcBihEREQkHAYoREREJJz/H0t8KKeI27v/AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "exp_conds_array = np.array(exp_conds)\n", + "plt.plot(range(20), exp_conds_array[:, 9])\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "id": "a2243b52-d5fe-4a52-8280-fb0ce4436ed6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: =======Iteration Number: 1 =====\n", + "INFO: elapsed time: 2.2\n", + "INFO: This is run 1 out of 9.\n", + "INFO: The code has run 2.19064669997897 seconds.\n", + "INFO: Estimated remaining time: 7.667263449926395 seconds\n", + "INFO: =======Iteration Number: 2 =====\n", + "INFO: elapsed time: 1.0\n", + "INFO: This is run 2 out of 9.\n", + "INFO: The code has run 3.2144447999307886 seconds.\n", + "INFO: Estimated remaining time: 6.428889599861577 seconds\n", + "INFO: =======Iteration Number: 3 =====\n", + "INFO: elapsed time: 1.3\n", + "INFO: This is run 3 out of 9.\n", + "INFO: The code has run 4.496953599969856 seconds.\n", + "INFO: Estimated remaining time: 5.6211919999623206 seconds\n", + "INFO: =======Iteration Number: 4 =====\n", + "INFO: elapsed time: 1.0\n", + "INFO: This is run 4 out of 9.\n", + "INFO: The code has run 5.520735299913213 seconds.\n", + "INFO: Estimated remaining time: 4.4165882399305705 seconds\n", + "INFO: =======Iteration Number: 5 =====\n", + "INFO: elapsed time: 1.2\n", + "INFO: This is run 5 out of 9.\n", + "INFO: The code has run 6.751729399897158 seconds.\n", + "INFO: Estimated remaining time: 3.375864699948579 seconds\n", + "INFO: =======Iteration Number: 6 =====\n", + "INFO: elapsed time: 1.2\n", + "INFO: This is run 6 out of 9.\n", + "INFO: The code has run 7.984732399811037 seconds.\n", + "INFO: Estimated remaining time: 2.281352114231725 seconds\n", + "INFO: =======Iteration Number: 7 =====\n", + "INFO: elapsed time: 1.1\n", + "INFO: This is run 7 out of 9.\n", + "INFO: The code has run 9.039862399804406 seconds.\n", + "INFO: Estimated remaining time: 1.1299827999755507 seconds\n", + "INFO: =======Iteration Number: 8 =====\n", + "INFO: elapsed time: 2.8\n", + "INFO: This is run 8 out of 9.\n", + "INFO: The code has run 11.875191299826838 seconds.\n", + "INFO: Estimated remaining time: 0.0 seconds\n", + "INFO: =======Iteration Number: 9 =====\n", + "INFO: elapsed time: 0.9\n", + "INFO: This is run 9 out of 9.\n", + "INFO: The code has run 12.811318899854086 seconds.\n", + "INFO: Estimated remaining time: -1.2811318899854087 seconds\n", + "INFO: Overall wall clock time [s]: 12.811318899854086\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAlEAAAHcCAYAAAD2uv9FAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/NK7nSAAAACXBIWXMAAA9hAAAPYQGoP6dpAABmxElEQVR4nO3dd1RUV9cG8GdoQ0d0VFApFgQ7omIPYDeJJWqixAZYotFEY4xieQOaKMY0LIkxVuwtMYkag4kKKhY0Kq81YqFEBVQ6iFLu94ffzOvIgNPgMvL81ror4ZZz9lxczvacc/eVCIIggIiIiIg0YiR2AERERESGiEkUERERkRaYRBERERFpgUkUERERkRaYRBERERFpgUkUERERkRaYRBERERFpgUkUERERkRaYRBERERFpgUkUEVUroaGhkEgkCA0NFTuUckkkEkgkklL7fX19IZFIEBUVVflBEZESJlFUoVxdXRVfBvLN3NwcDRs2xKhRo3D27FmxQ9RYZmYmQkNDER4eLnYoepWQkFDqd1XWlpCQIHa4KiUkJCA0NBQbN24UO5RKFxUVhdDQUCZXRJXIROwAqHpwc3NDnTp1AABZWVm4efMmtm7dih07dmDDhg0YPXq0yBGqLzMzEwsWLICLiwumT58udjgVon379pBKpWUeNzc3r8Ro1JeQkIAFCxbAx8cHAQEBKs+RyWRwd3eHTCar3OD0xNnZGe7u7rC0tFTaHxUVhQULFgB4NlpFRBWPSRRVirlz5yp9qWVkZGDixInYs2cPpkyZgjfffBP29vbiBUhKdu/eDVdXV7HDqBBTp07F1KlTxQ5Da5s2bRI7BCL6f5zOI1HY29tj3bp1sLKyQk5ODg4dOiR2SERERBphEkWisbW1RdOmTQGgzDU2kZGRGDhwIOrWrQupVIoGDRogMDAQt27dUnn+6dOnMWvWLLRv3x516tSBVCqFk5MTRo8ejStXrpQbzz///IOJEyeiSZMmsLCwQK1atdCuXTuEhITg/v37AICAgAA0bNgQAJCYmFhqrdCLDhw4gH79+kEmk0EqlaJhw4Z4//33kZycrDIG+RqyhIQEHD16FP3794dMJjP4hcRJSUmYPHkyGjZsCKlUCplMhv79++PgwYMqz39+8XdKSgrGjRuHevXqwdzcHM2aNcNXX32FoqIipWt8fX3h5+cHAIiOjlb6vTw/qlbWwvKNGzdCIpEgICAAjx8/xpw5c9CoUSNYWFjA3d0dK1asUJz76NEjTJs2DS4uLjA3N0eLFi3KXIeVkpKCFStWoG/fvnB1dYW5uTns7e3h4+ODzZs3a3wvVS0sl0gkiqm8BQsWKH32gIAAZGZmwsLCAqampkhNTS2z7TfffBMSiQTfffedxnERVUsCUQVycXERAAgbNmxQedzd3V0AICxfvrzUsWnTpgkABABCnTp1hLZt2wq2trYCAMHW1laIiYkpdU3jxo0FAEKtWrWEli1bCm3atBHs7OwEAIKFhYVw9OhRlXFs2bJFMDMzU5zn5eUleHh4CFKpVCn+RYsWCe3btxcACFKpVOjatavS9rzg4GBF/A0aNBDatWsnWFpaCgAEe3t74ezZs2Xer8WLFwtGRkaCvb290KFDB6FBgwZlxq4vd+7cUcR7584dvbV7+vRpoUaNGgIAwcrKSmjXrp3QoEEDRV//+c9/Sl0TEhIiABCmTp0qODk5CcbGxoKnp6fQtGlTxXWDBw8WiouLFddMnTpVaNmypeLPx/O/l2HDhpVqOyQkRKnPDRs2CAAEf39/oXPnzoKxsbHQunVrwdXVVdHnggULhNTUVMHNzU0wMzMT2rZtK9SrV09xfP369aU+y2effab4c9W4cWOhffv2grOzs+KaSZMmqbxv8uMv8vHxEQAo/Xno2rWr4OTkJAAQnJyclD77okWLBEEQBH9/fwGA8PXXX6vsLyUlRTAxMRHMzMyER48eqTyHiJQxiaIKVV4SdePGDcHExEQAIBw7dkzp2A8//CAAEBo2bKj0ZVFUVCR8/vnnisTk8ePHStdFREQIt27dUtpXWFgorF27VjAxMREaNWqk9MUrCIJw9uxZwdTUVAAgzJo1S8jNzVUce/r0qbB9+3bh+PHjin3yZMPFxaXMz71v3z4BgGBiYiJs2bJFsT8rK0t46623BACCq6urkJ+fr/J+GRsbCwsWLBAKCwsFQRCEkpISoaCgoMz+9KEikqi8vDxFwvDOO+8I2dnZimMbN24UjI2NBQDC77//rnSdPNExMTERWrVqpRRPdHS0IjFeuXKl0nVHjx4VAAg+Pj5lxvSyJMrU1FRo1aqVcPv2bcWx7du3KxKhPn36CH5+fkJqaqri+KJFiwQAgqOjo1BUVKTU7vHjx4UjR46U2h8XFyc0a9ZMACBERUWVilOTJKq8zyX3559/CgCE1q1bqzz+9ddfCwCUEk4iKh+TKKpQqpKorKws4c8//xSaN28uACg1gvPkyRPBwcFBMDY2Fs6fP6+y3aFDhwoAhE2bNqkdy6hRowQApUawXn/9dQGAEBQUpFY76iRRXbt2FQAI06ZNK3UsLy9PkMlkAgBh3bp1Ssfk92vAgAFqxaJPzydR5W1t2rRRu801a9YIAIS6deuWSngFQRDef/99AYDQvXt3pf3yhACA8Pfff5e6bvny5YpEtKSkRLFfH0mURCJR+eeuc+fOikTq7t27SseKioqE+vXrCwDK/DOryl9//SUAECZMmFDqmL6TqJKSEsWo2oULF0odb926tQBA2L9/v9rxE1V3XBNFlSIwMFCxRsPOzg69e/fG9evXMXz4cOzbt0/p3FOnTiElJQVeXl5o27atyvYGDhwI4Nnalxddv34dISEhGDJkCHx9fdGtWzd069ZNcW5cXJzi3MePH+PPP/8EAMyaNUsvnzU3NxenTp0CAHzwwQeljltaWmLChAkAUOaC+jFjxuglFm21b98eXbt2VbmV9TtRRf75JkyYoLIswrRp0wAAJ0+eRF5eXqnjnTt3hpeXV6n9QUFBMDc3R0JCAv755x+141FH27ZtVX5GT09PAED//v1Rr149pWPGxsZo3bo1AOD27dulrs3JycGaNWswduxY9OnTB927d0e3bt0QHBwMQPnPZEWRSCQYO3YsACAiIkLp2MWLF/Hf//4XDg4O6NevX4XHQvSqYIkDqhTyOlGCICAlJQW3b9+GqakpOnToUKq0waVLlwA8W2zerVs3le1lZmYCAO7evau0PywsDPPnz0dJSUmZsaSnpyv+/+bNmygsLESNGjXg7u6uzUcr5ebNmygpKYFUKkWjRo1UntOiRQsAwI0bN1Qeb9asmV5i0Za+ShzIP1/z5s1VHndzc4OZmRmePn2KW7duKRIRubLug5WVFZycnBAfH48bN27Aw8ND51jlGjdurHJ/7dq11Tqem5urtP/ChQt48803ce/evTL7fP7PZEUKDAzEwoULsW3bNnz55ZcwMXn2FSBPqkaNGgVjY+NKiYXoVcCRKKoUc+fOxYkTJxATE4Nbt27hxIkTsLGxwcyZM7Flyxalc7OysgAADx48QExMjMpN/qTd48ePFdcdO3YMc+fOhUQiQVhYGK5cuYLc3FyUlJRAEATMmzcPAFBYWKi4Jjs7GwBQo0YNvX1W+Zdo7dq1VT6xBwB169YF8GyEQhUrKyuN+z148KBi1O35bf369Rq3pS/yeyEvtPoiiUSiSD5U3YuyrgNefg+19WIRSzn57/JlxwVBUOwrLi7GO++8g3v37uH1119HdHQ0Hj58iKKiIgiCgPj4eADKfyYrkouLC3r06IG0tDTFk5FFRUXYtm0bAJRZoJSIVONIFImia9euWLNmDd566y1MmzYNAwcOhK2tLQDA2toaADBy5MhSCVZ5tm7dCgD45JNPFNMkz1NVVsDGxgbA/0a29EEe/4MHDyAIgspESv6Yubx/fUhNTUVMTEyp/b169dJbH5qS34u0tDSVxwVBwIMHDwCovhfyY6rI29TnPdS32NhY3Lx5Ey4uLvj5559LVYEvq9RFRQoKCsLhw4cRERGBAQMG4ODBg0hLS0P79u0VI6REpB6ORJFoBg8ejE6dOiE9PR3ffPONYr986ufy5csatSevNdWlSxeVx1WtO5FPJ2VmZqq9tqas0SW5Jk2awMjICE+ePFG5PgaAYiRNXidLHwICAiA8e1hEaRPzRbvyz3f16lWVx+Pj4/H06VMYGxurnCa7du2ayuvy8/ORlJSk1Afw8t9NZZP/mWzXrp3K1+jocy2Uup99yJAhqFGjBvbt24f09HRFfSuOQhFpjkkUiUo+YrR8+XLF1E/37t0hk8kQFxenUYFJCwsLAFBZTPDQoUMqv7AsLCzQp08fAMBXX32lUT/PTyU+z9raWpHIPV+gUe7x48dYu3YtAKBv375q9Wmo5J9vzZo1KCgoKHV8+fLlAJ6NTKqawjx58iQuXrxYav/69etRUFAAFxcXpbVsL/vdVLby/kwWFhbq9SXW6n52c3Nz+Pv74+nTp1i5ciX2798PMzMz+Pv76y0WouqCSRSJauDAgWjWrBkyMjKwatUqAM/+kl+4cCEA4O2338bevXuV1pkAz0apZs+erTR9JV+EvmTJEty5c0ex/+zZs4qnuVQJCQmBqakp1q5di7lz5yI/P19xrLCwEDt37sSJEycU+2rXrg0bGxukpaWVOVIye/ZsAMD333+vWG8CPFu/M2bMGDx48ACurq4YMWLEy2+SAfP394ezszNSU1MREBCgtOh6y5YtWL16NQConH4FABMTEwQEBCAxMVGx78SJE/j0008BADNnzlQagZFXk7969Wq5U4GVpVOnTjAxMUFMTIzSO++ysrIwcuTIcquHa0r+EMPJkydLVXN/UVBQEADgs88+w9OnTzFw4EDUrFlTb7EQVRviVFag6uJlFcsFQRDWrVsnABAcHByUagk9X/G7Zs2aQocOHQQvLy+hZs2aiv0HDx5UnJ+VlSU0atRIACCYmZkJrVq1UlREb968uTBjxowy6+hs3rxZUXDT0tJS8PLyEpo1ayaYm5urjD8oKEgAIJibmwvt27cXfHx8StUmej5+JycnoX379oKVlZWiYnlsbGyZ90ufFcPV9XydqPbt25eqxv789mJx1PKcPn1aURzTyspKaN++vaK6NgBh/vz5pa6R1zyaMmWK4OTkJJiYmAienp6K3yf+v5bWi4VTBUEQevToIQAQbGxshI4dOwo+Pj7C8OHDS7VdVp2osWPHqvwcL6vDNHbsWJV/VmbOnKmI2dnZWWjXrp1gYWEhmJqaCqtWrSqz5pj8mheVVScqKytLsLe3VxT97Nq1q+Dj4yOEhYWpjFdeFwqsDUWkNY5EkehGjRqFevXqISUlRelJsrCwMMTExODdd9+FlZUV4uLikJCQgAYNGiAoKAgHDhxAz549Fefb2trixIkTGDNmDGxtbfHPP//g6dOnmDFjBk6dOlXuAuRRo0bh4sWLCAwMhEwmw+XLl/HgwQO0aNECoaGhpWrnLFu2DNOmTYODgwPi4uIQHR1dqmZVWFgY9u3bh969eyM3Nxf//e9/IZPJMGnSJMTFxaFDhw56uoP6d+7cuTKfjIyJicGjR4/Ubqtjx46Ii4vDe++9B5lMhv/+97/Izc1Fnz59cODAAXz22WdlXiuTyRAbG4sxY8YgNTUVd+7cgbu7O7744gv8/PPPMDIq/VfYtm3bEBAQAFtbW/z999+Ijo7G6dOntboP+rB06VKEh4fDw8MDKSkpSExMRK9evXD8+HG91mSytbXFoUOH0L9/fzx58gSnTp1CdHQ0rl+/rvJ8+Roo1oYi0p5EEF6YJyEiElloaCgWLFiAkJAQURfGv8qCg4PxxRdfYObMmfjyyy/FDofIIHEkioiomiksLFSs0QoMDBQ5GiLDxSSKiKiaWb58Oe7fvw8fH58yq8kT0cux2CYRUTWQkpKCESNG4NGjR7h8+TKMjIywaNEiscMiMmgciSIiqgYKCgoQHR2Nf/75By1atMCuXbvQtWtXscMiMmhcWE5ERESkBY5EEREREWmBa6L0rKSkBPfu3YONjU2Ve48XERG9nCAIyMnJQb169VTWItOXgoICPH36VOd2zMzMynwjA1UsJlF6du/ePTg5OYkdBhER6Sg5ORkNGjSokLYLCgpgaWEBfayncXBwwJ07d5hIiYBJlJ7Jq2In2wO2HIh65b2XLnYEVJl+FTsAqhQCgAKg3Lcc6Orp06cQAFgA0OWrQsCzJy+fPn3KJEoETKL0TD6FZysBbLni7JVnJnYAVKn476LqpTKWZBhD9ySKxMMkioiISCRMogwbx0qIiIiItMCRKCIiIpEYgSNRhoxJFBERkUiMoNuUUIm+AiGtMIkiIiISiTF0S6L4sIO4uCaKiIiISAsciSIiIhKJrtN5JC4mUURERCLhdJ5hYwJMREREpAWORBEREYmEI1GGjUkUERGRSLgmyrDxd0dERESkBY5EERERicQIz6b0yDAxiSIiIhKJrtN5fO2LuDidR0RERKQFjkQRERGJxBiczjNkTKKIiIhEwiTKsDGJIiIiEgnXRBk2rokiIiIi0gJHooiIiETC6TzDxiSKiIhIJEyiDBun84iIiIi0wJEoIiIikUig22hGib4CIa0wiSIiIhKJrtN5fDpPXJzOIyIiItICR6KIiIhEomudKI6EiIv3n4iISCTGetgq07FjxzBz5kz4+fnBzs4OEokEAQEBWrUlkUjK3JYsWaLfwCsIR6KIiIhILevXr0dERAQsLS3h7OyM7OxsndpzcXFRmYR169ZNp3YrC5MoIiIikRjawvKpU6fik08+gYeHB86ePYvOnTvr1J6rqytCQ0P1E5wImEQRERGJxNDWRLVv376Se6zamEQRERGJxNBGovQtMzMTa9euRVpaGmrXrg1fX1+4ubmJHZbamEQREREZuBfXJkmlUkilUpGiUV9cXBwmTJig+FkikWDkyJFYvXo1LC0tRYxMPXw6j4iISCRG0O3JPPmXuJOTE+zs7BRbWFhYpX4ObcycORNnzpxBeno6MjIycOTIEXTs2BFbtmzBuHHjxA5PLRyJIiIiEom+1kQlJyfD1tZWsb+8USiZTIZHjx6p3cfRo0fh6+urZYRl+/LLL5V+9vPzw+HDh9GmTRvs2LED8+fPR4sWLfTerz4xiSIiIjJwtra2SklUefz9/ZGTk6N22w4ODtqGpTFLS0v4+/vjs88+Q0xMDJMoIiIiUk3XheXavIB4xYoVOvRY8WQyGQAgPz9f5EhejkkUERGRSAytxEFlOHPmDIBnNaSqulfx/hMREVEVkJ+fj+vXryMpKUlp/4ULF1SONO3evRvbt2+HTCZDr169KitMrXEkioiISCRiTOfp4sSJE1i7di0A4MGDB4p98le3eHh4IDg4WHF+bGws/Pz84OPjg6ioKMX+ZcuW4ZdffkHPnj3h7OwMQRBw/vx5HD9+HObm5oiIiIC1tXWlfS5tMYkiIiISiaElUTdv3kRERITSvlu3buHWrVsAAB8fH6UkqiyDBg1CZmYmzp8/jz/++ANFRUWoX78+xo0bh5kzZ8LDw6NC4tc3iSAIhl7wtErJzs6GnZ0dsmoCtpwsfeWNfSh2BFSZ9ogdAFUKAcBjAFlZWWo/8aYp+XfFEACmOrRTCOBnVGysVDaORBEREYmEC8sNG5MoIiIikcgrlmurWF+BkFaYRBEREYlE1zVRulxLuuNIIBEREZEWOBJFREQkEq6JMmxMooiIiETC6TzDxiSWiIiISAsciSIiIhIJp/MMG5MoIiIikXA6z7AxiSUiIiLSAkeiiIiIRMKRKMNW5UeiMjMz8eGHH6Jz585wcHCAVCpF/fr10aNHD/z0009Q9eq/7OxszJgxAy4uLpBKpXBxccGMGTOQnZ1dZj/btm2Dt7c3rKysYG9vj9dffx3nzp2ryI9GRETVnAT/WxelzSap/JCrLEEQcOLECSxevBivv/46WrRogTp16sDGxgYNGzaEt7c3Jk2ahK1btyIlJUUvfVb5FxDfvHkTnp6e6NSpE5o0aYKaNWsiLS0N+/btQ1paGiZMmIAff/xRcX5eXh66deuGixcvonfv3vDy8kJcXBz++OMPeHp64sSJE7CyslLqY/HixZg3bx6cnZ0xbNgw5ObmYseOHSgoKEBkZCR8fX3VjpcvIK5e+ALi6oUvIK4eKvMFxO8BkOrQzhMAq1G9X0D877//Ys2aNdi4cSP+/fdfAFA5wCInkUhgbGyMfv36YcKECRgwYIDWfVf5JKq4uBiCIMDERHnmMScnB506dcLVq1dx+fJltGjRAgAQEhKChQsXYtasWfjiiy8U58v3f/rpp1iwYIFif3x8PJo3b45GjRohNjYWdnZ2AIArV67A29sbjo6OuH79eqn+y8IkqnphElW9MImqHioziXofuidR36N6JlEZGRn4/PPP8f333+PJkycwMTFBx44d4e3tjQ4dOsDR0RE1a9aEhYUF0tPTkZ6ejqtXryI2NhYnT57Ev//+C4lEgtatW2PJkiXo27evxjFU+SSqPDNmzMC3336LX375BYMGDYIgCGjQoAGys7ORkpKiNOJUUFCAevXqwdLSEsnJyZBIng2Czp07F2FhYYiIiMCYMWOU2p88eTJ++OEHREZGok+fPmrFxCSqemESVb0wiaoeKjOJ+gC6J1ErUD2TKHt7e2RlZaFTp04YO3Yshg0bhlq1aql9/cmTJ7Ft2zZs3boV2dnZ+OabbzBt2jSNYjDYr/mCggIcOXIEEokEzZs3B/BsVOnevXvo2rVrqSk7c3NzvPbaa7h79y5u3ryp2B8VFQUAKpMkeVYaHR1dQZ+CiIiqM13WQ+laY8rQeXl54ciRIzh58iTee+89jRIoAOjSpQtWrlyJhIQEfPrppzA21nyZvsE8nZeZmYnw8HCUlJQgLS0Nv//+O5KTkxESEgI3NzcAz5IoAIqfX/T8ec//v7W1NRwcHMo9n4iIiKqOw4cP66UdOzs7hISEaHWtQSVRz69lMjU1xZdffomPP/5YsS8rKwsAFOuaXiQf6pSfJ///OnXqqH3+i548eYInT54ofi7vCUAiIqLnscSBYTOYkUBXV1cIgoCioiLcuXMHCxcuxLx58zB06FAUFRWJFldYWBjs7OwUm5OTk2ixEBGRYeF0nmEzmJEoOWNjY7i6uiI4OBjGxsaYNWsW1qxZg8mTJytGoMoaOZKPEj0/UmVnZ6fR+S+aM2cOZsyYoXQNEykiIiLxpKWlITExEQ8ePMDjx48hk8lQu3ZtuLu7a7X2qSwGl0Q9r0+fPpg1axaioqIwefLkl65hUrVmys3NDadOnUJKSkqpdVEvW2MFAFKpFFKpLs9WEBFRdcXpPP35888/sXPnThw7dgy3bt1SeY6lpSU6deqEvn37YvTo0ahbt65OfRr0SOC9e/cAQFHDyc3NDfXq1UNMTAzy8vKUzi0oKMCxY8dQr149NGnSRLHfx8cHAHDo0KFS7UdGRiqdQ0REpE9G+F8ipc1m0F/ielBQUIAvv/wSjRo1Qr9+/bB+/XrcvHkT5ubmcHZ2hqenJzp37gx3d3fUrl0beXl5OHz4MGbPng1nZ2cMHToUf//9t9b9V/n7f/HiRZXTbenp6Zg7dy4AoH///gCeVSEdP348cnNzsXDhQqXzw8LCkJGRgfHjxytqRAFAYGAgTExMsGjRIqV+rly5gk2bNqFx48bo0aNHRXw0IiIi0tL69evh5uaG2bNn4/79+xg4cCDWrFmDuLg45OTk4M6dO/j7779x4sQJXL16FSkpKXj48CF+//13zJkzBy4uLti7dy+8vb3h7++PxMREjWOo8sU2p0+fjrVr18LPzw8uLi6wsrJCYmIiDhw4gNzcXAwdOhS7du2CkdGzfPDF1760a9cOcXFxOHjwYJmvfVm0aBHmz5+veO1LXl4etm/fjsePHyMyMhJ+fn5qx8tim9ULi21WLyy2WT1UZrHNeQDMdWinAMAiVM9im0ZGRmjUqBFmzZqFESNGaPX5//77byxfvhzbt2/H/Pnz8emnn2p0fZVPok6cOIF169bh9OnTuHfvHvLz81GzZk14eXlhzJgxGDFihNLIEvDsD9OCBQuwZ88exVqnYcOGISQkpMxF4lu3bkV4eDiuXLkCMzMzdO7cGQsXLkSHDh00ipdJVPXCJKp6YRJVPVRmEvUpdE+iFqJ6JlGbN2/Gu+++q5eF4nfu3MG///6L7t27a3RdlU+iDA2TqOqFSVT1wiSqemASReoy6KfziIiIDBmfzjNsTKKIiIhEomvBTE54iItJFBERkUg4EqWbF5/E14ami8mfxySKiIiIDFJoaKji4TJBEEo9aFYe+flMooiIiAwQp/P0w93dHV26dNEoidIHJlFEREQikVcs1+X66kwmk+Hhw4f4559/8PTpU4wcORKjRo0q93Vt+lTd7z8REREZqPv372P//v14++23cf/+fXz22Wfw8PBAly5d8P333+PRo0cV2j+TKCIiIpHo8t48XRelvwqMjY3x+uuvY8eOHUhNTcW6devg6+uL2NhYfPDBB6hXrx4GDx6MPXv24MmTJ3rvn0kUERGRSIz0sNEz1tbWCAwMxOHDh5GYmIjFixejadOm+O233zB8+HA4ODhgwoQJOHPmjN765P0nIiKiV0r9+vUxe/ZsXLp0CRcuXMCMGTNgbm6O9evX6/Q03ou4sJyIiEgkrBNVsYqLi5GUlISkpCRkZmZCEATo8213HIkiIiISiSGticrLy8OWLVvwzjvvoGnTprCwsECNGjXg4+OD7du3a9VmZGQkfH19YWtrCxsbG/j6+iIyMlLnWM+cOYOpU6fC0dERgwcPxu7du+Hs7IzQ0FCsXr1a5/blOBJFREREL3X8+HGMHj0atWrVQs+ePTF06FCkpaXh559/xrvvvouTJ09ixYoVare3detWjBo1CjKZDGPHjoVEIsGuXbvQr18/bNmyBSNHjtQovtu3b2PLli3YunUrbt68CUEQIJPJMHnyZIwePRodO3bU9CO/lETQ57gWKd7MnVUTsOU43ytv7EOxI6DKtEfsAKhSCAAeA8jKyoKtrW2F9CH/rlgFwEKHdh4DmIyKjVUuLi4OV65cwdtvvw1TU1PF/tTUVHTs2BGJiYmIjY1Fhw4dXtpWRkYGGjVqBBMTE5w/fx5OTk4AnpUs8PLyQkFBAW7fvg17e/uXtrNz505s3rwZp0+fhiAIMDc3x4ABAzBq1Cj0798fJiYVN17Er3kiIiKRGNJ0Xps2bfDuu+8qJVAAULduXbz33nsAgOjoaLXa2r17NzIzM/HBBx8oEigAcHR0xPTp05GZmYndu3e/tB0HBwdMmTIFZ86cwWuvvYa1a9ciNTUVO3fuxIABAyo0gQI4nUdERCQaCXQbzajcl5yUTZ5YqZu0REVFAQD69OlT6ljfvn0RHByM6OhoTJw4sdx2CgsLIZFI0KRJE5iammLHjh3YsWOH2nFLJBKd1mAxiSIiIiKtFRcXY9OmTZBIJOjVq5da18THxwOAytezyPfJz3kZQRBw48YN3LhxQ82I/0fXd+0xiSIiIhKJvkocZGdnK+2XSqWQSqU6tKy+//znP7h06RKCgoLQsmVLta7JysoCANjZ2ZU6ZmVlBWNjY8U55dmwYYNmweoZkygiIiKR6CuJen5dEQCEhIQgNDRU5TUymUyjd8odPXoUvr6+Ko/9+OOPCAsLQ9u2bbFs2TK129SXsWPHVnqfz2MSRUREZOCSk5OVns4rbxTK398fOTk5arft4OCgcv+GDRswadIktGrVCn/++Sesra3VblM+ApWVlYVatWopHcvLy0NxcbHKUaqqhkkUERGRSHR9/538WltbW7VLHGhSy6ks69evx4QJE9C8eXMcPny4VCL0Mm5ubjh37hzi4+NLXVveeqmqhkkUERGRSAzxtS/r16/H+PHj0axZMxw5cgS1a9fWuA15lfNDhw6hU6dOSsfkT8v5+Pi8tJ1NmzZp3PeLxowZo/W1LLapZyy2Wb2w2Gb1wmKb1UNlFtvcBsBSh3byAbyLyim2CQDr1q3DhAkT4OHhgaNHj6Ju3brlx5efj6SkJFhaWsLZ2VmxPyMjAw0bNoSpqalOxTaNjIx0esJOIpGgqKhI6+s5EkVERCQSQxqJOnLkCCZMmABBEPDaa69h1apVpc7x9PTE4MGDFT/HxsbCz88PPj4+itpQAGBvb4+VK1di9OjR8PLywogRI2BkZISdO3ciNTUVmzdvfmkCBQDOzs46lynQBZMoIiIikehrTVRlSEpKgnzyqqyX+I4dO1YpiSqP/L15YWFh2LhxIwDAy8sLERER6Nu3r1ptJCQkqHVeReF0np5xOq964XRe9cLpvOqhMqfzdkP36by3UXnTeaSMI1FEREQiMYJuU3L8t7q4eP+JiIhEYqSHrTobMmQI/vOf/4jWf3W//0RERKIx1sNWnf3yyy+Ijo5WeczY2FitMgm6YBJFRERErxxBEFDRy765JoqIiEgkhlTigEpjEkVERCQSQypxQKXx/hMRERFpgSNRREREIuF0nmFjEkVERCQSJlG6i4+PR1BQkMbHgGfvzlu3bp3WfbNiuZ6xYnn1worl1QsrllcPlVmx/DAAKx3ayQPQE9W3Yrn8BcSapjLyayQSCYqLi7XunyNRREREIpFAt8XJ4r16t2oYO3asqP0ziSIiIhIJp/N0s2HDBlH754QTERERkRY4EkVERCQS1okybLz/REREIuG787QXGxurt7by8/Nx9epVja9jEkVERCQSJlHa69SpE/r3748TJ05o3UZGRgYWL14MFxcX7Nmj+fO3TKKIiIjI4MycORPR0dHw8fFB48aNMX/+fJw8eRIFBQXlXpeUlIRt27Zh0KBBcHR0xPz58+Hi4oIBAwZoHAPrROkZ60RVL6wTVb2wTlT1UJl1os4CsNahnVwAHVB960T9+++/CAkJwfbt21FQUACJRAJjY2M0a9YMjo6OqFmzJqRSKTIzM5Geno7r16/j4cNnf3ELgoBmzZph/vz58Pf316p/JlF6xiSqemESVb0wiaoeKjOJOg/dkygvVN8kSi4zMxMRERHYuXMn/v77bxQWFpZ5bv369dG7d2+MGzcOXbt21alfPp1HREREBq1GjRqYNm0apk2bhoKCApw9exaJiYl4+PAhCgoKULNmTdSpUweenp5wdXXVW79MooiIiERiBN0Wh3PCozRzc3N0794d3bt3r/C+mEQRERGJhHWiDBvvPxEREZEWOBJFREQkEr47T7+CgoLUPtfY2Bg2NjZwdXVF165d0a5dO437YxJFREQkEk7n6dfGjRsBABKJBMCzMgYvevGY/Od27dohIiICzZo1U7s/JlFEREQi4UiUfm3YsAG3bt3CF198ASsrKwwePBitW7eGjY0NcnJycOnSJfzyyy/Iy8vDrFmz4ODggGvXruGnn37CuXPn4OfnhwsXLsDR0VGt/lgnSs9YJ6p6YZ2o6oV1oqqHyqwTdQOAjQ7t5ABoCtaJkrtz5w7at28Pb29vbN++HTVq1Ch1TnZ2NoYPH46zZ88iNjYWjRo1Ql5eHoYMGYK//voL06ZNwzfffKNWf0yi9EyRRB0HbHWpoEaG4Q2xA6DKdPye2BFQZcgD0B+Vk0Tdgu5JVGMwiZIbOXIkfvnlF9y9e1dlAiWXkZGBBg0aYNCgQdi2bRsA4O7du3BxcUGTJk1w/fp1tfrjdB4REZFIuCZKvw4fPowWLVqUm0ABgL29PVq0aIEjR44o9tWvXx8eHh64c+eO2v3x/hMREdErITs7G+np6Wqdm56ejuzsbKV9UqlUsdBcHUyiiIiIRCKvWK7txi9xZW5ubrhz5w72799f7nn79+/H7du30bRpU6X9t2/fRu3atdXuj/efiIhIJLokULo+2fcqmjx5MgRBwDvvvIMlS5YgJSVF6Xhqaiq++OILjBgxAhKJBJMnT1Yci4uLQ1ZWFry8vNTuj2uiiIiI6JUwadIknD17Fhs2bMC8efMwb9481KpVCzY2NsjNzcXDh88eqRYEAePGjcN7772nuDYqKgo+Pj4YM2aM2v3x6Tw949N51QyfzqtW+HRe9VCZT+fdA6BLD9kA6oFP571oz549+PrrrxEbG6tUcNPIyAgdO3bEjBkzMHToUJ374UgUERGRSFhss2IMGzYMw4YNQ25uLm7evIm8vDxYWVmhSZMmsLbW3wgHkygiIiJ6JVlbW8PT07PC2mcSRUREJBLWiTJsTKKIiIhEwuk87W3atAkAYGdnh0GDBint04QmC8lfxIXlesaF5dUMF5ZXK1xYXj1U5sLyLOi+sNwOlbOwPC8vD3v37sVvv/2GixcvIjk5GVKpFG3atMGkSZPg7++vUXvlFbUMCwtDcHBwudcbGRlBIpHA3d0dV69eVdqnieLiYo3Ofx5HooiIiOiljh8/jtGjR6NWrVro2bMnhg4dirS0NPz888949913cfLkSaxYsUKjNl1cXBAQEFBqf7du3V567ZgxYyCRSODo6FhqX2XhSJSecSSqmuFIVLXCkajqoVJHoiSArQ7f+dkCYCdUzkhUXFwcrly5grfffhumpqaK/ampqejYsSMSExMRGxuLDh06qNWeRCKBj48PoqKiKijiisc1aURERGIxoJLlbdq0wbvvvquUQAFA3bp1FUUro6OjKy+gKoDTeURERKQTeWJlYqJZWpGZmYm1a9ciLS0NtWvXhq+vL9zc3PQWV0lJCR49eoTHjx/D2dlZb+3KMYkiIiISizEAXZbwCACKnk0PPk8qlUIqleoSmdqKi4uxadMmSCQS9OrVS6Nr4+LiMGHCBMXPEokEI0eOxOrVq2Fpaal1TL///ju+/fZbnDx5EgUFBZBIJCgqKlIcX7RoEa5cuYJly5Zp9MLhF3E6j4iISCxGetgAODk5wc7OTrGFhYVV2kf4z3/+g0uXLiEwMBAtW7ZU+7qZM2fizJkzSE9PR0ZGBo4cOYKOHTtiy5YtGDdunNbxzJo1CwMGDMDhw4dRXFwMU1NTvLj829HRETt37sTevXu17gfgwnK948LyaoYLy6sVLiyvHip1YbmFHhaWPwaSk5OVYi1vJEomk+HRo0dq93H06FH4+vqqPPbjjz/ivffeQ9u2bXHs2DGdX6mSn5+PNm3a4ObNm7h8+TJatGih0fU//fQT3n77bdSvXx+rV69G37594evri5MnTyqVMsjIyIBMJkP//v2xf/9+rePldB4REZFY9DGdB8DW1lbthM/f3x85OTlqd+Hg4KBy/4YNGzBp0iS0atUKf/75p17eSWdpaQl/f3989tlniImJ0TiJ+u677yCRSLB792506tSpzPPs7e3RsGFDxMfH6xQvkygiIiKx6CmJ0oSmtZxUWb9+PSZMmIDmzZvj8OHDqFWrls5tyslkMgDPRqU0deHCBTg5OZWbQMnVrl0bly5d0riP53FNFBEREalt/fr1GD9+PDw8PHDkyBGdFmarcubMGQCAq6urxtc+efIENWrUUOvc/Px8GBvrViOCSRQREZFY9LSwvLKsW7dOKYGqU6dOuefn5+fj+vXrSEpKUtp/4cIFlSNNu3fvxvbt2yGTyTR+0g94tsD+5s2bKCwsLPe8rKwsXL9+HY0bN9a4j+dxOo+IiEgsuiZCJfoK5OWOHDmCCRMmQBAEvPbaa1i1alWpczw9PTF48GDFz7GxsfDz8ytVmXzZsmX45Zdf0LNnTzg7O0MQBJw/fx7Hjx+Hubk5IiIitFpj1bdvX3z33Xf49ttvMWvWrDLPW7hwIYqKivDmm29q3MfzmEQRERGJRYTRJG0lJSUpSgWsXr1a5Tljx45VSqLKMmjQIGRmZuL8+fP4448/UFRUhPr162PcuHGYOXMmPDw8tIpx9uzZ2LRpE+bOnYsHDx4olUooKSnB5cuXER4ejo0bN6J27dqYNm2aVv3IscSBnrHEQTXDEgfVCkscVA+VWuKgNmCrQxKVXQLYPaicd+cZiujoaAwZMgSZmZkqjwuCgJo1a+K3335Dly5ddOrLQPJfIiKiV5ABvTvPUPj4+ODy5cuYPn06XFxcIAiCYnN0dMTUqVMRFxencwIFcDqPiIhIPMbQbThDl/IIrzBHR0d8/fXX+Prrr5GXl4esrCxYW1vrfbSOSRQRERG9sqysrGBlZVUhbTOJIiIiEosBLSyn0phEERERiYXTeQaN+S8RERGRFjgSRUREJBYj8Ak7A8YkioiISCy6rolipUdRcTqPiIiISAsciSIiIhILC2YaNCZRREREYuF0nkFjEkVERCQWjkRpbdOmTXppZ8yYMVpfyySKiIiIDE5AQAAkEt0LZVV4EtWoUSOtO1BFIpHg1q1bem2TiIjI4HAkSmtjxozRSxKlC7WSqISEBL12KvaHJiIiqhK4JkprGzduFDsE9afzOnTogF27dunc4dtvv42///5b53aIiIiIxKR2EiWVSuHi4qJzh1KpVOc2iIiIXgm6ViyvxiNRVYFaSdTAgQPRsmVLvXTYvXt3yGQyvbRFRERk0HRdE8UkqkwlJSWIj49Heno6CgsLyzzvtdde07oPtZKoX375ResOXrR48WK9tUVERET0vAcPHiA4OBi7du1Cfn5+uedKJBIUFRVp3VellTi4ceMGmjZtWlndERERVX26Lizny9uUPHr0CB07dkRiYiIaNGgAY2Nj5OTkoEuXLkhOTsbdu3dRXFwMCwsLeHt769yf2rf/q6++0rqT//73v/Dx8dH6eiIioleSsR42Uli6dCkSEhIwdepUJCYmolWrVgCA48ePIyEhAampqQgODkZRURFcXFxw9OhRnfpTO4maPXs2li1bpnEHsbGx8PPzQ1pamsbXEhEREalr3759sLCwwGeffabyeM2aNbF48WKsWbMGmzdvxvfff69TfxoNBM6YMQPfffed2udHR0ejd+/eyMjIQOfOnTUOjoiI6JVmpIeNFBITE+Hq6gpbW1sAgJHRsxv04sLyMWPGwNHREevWrdOpP7Vv//r16yGRSPDhhx9i9erVLz3/jz/+wOuvv46cnBz07NkThw4d0ilQIiKiVw6n8/TK1NQUlpaWip9tbGwAACkpKaXOdXR0RHx8vE79qZ1EjR07Fj/++CMAYMqUKVi7dm2Z5/78888YPHgwHj9+jAEDBmD//v1KH4qIiIjAJErPGjRogPv37yt+lj/Qdvz4caXz8vLyEB8fr/MbVDQaCAwKCsLq1ashCAImTZqksuT6pk2bMGLECDx9+hTDhw/HTz/9xAKbREREVOG8vb2RmpqKzMxMAMCAAQMgCAI++eQT/PXXX8jLy8Pt27cxatQo5OTk6LzUSOPZ1PHjx+P777+HIAgYP348Nm/erDi2atUqBAUFoaioCEFBQdi2bRtMTCqtigIREZFhkUC39VB8Fa2SQYMGobi4GPv27QMA+Pn5YdCgQbh//z769u0LW1tbuLm54ddff4WZmRk+//xznfrTKsN57733UFJSgilTpiAoKAgmJiZITk7GnDlzIAgCPvzwQ4SHh+sUGBER0StP1ym5En0F8moYMGAAkpOTFWuhAGDXrl0ICwvDtm3bkJCQAAsLC3Tr1g0LFiyAl5eXTv1JBEHQumj8ypUr8eGHH8LIyAiCIEAQBMyZMweLFi3SKShDlp2dDTs7O2QdB2ytxY6GKtwbYgdAlen4PbEjoMqQB6A/gKysLMVTXvqm+K7oC9ia6tBOIWAXWbGxUtl0mmubOnUqBEHAtGnTIJFIEBYWhtmzZ+srNiIiolcbR6IMmtproho1aqRy+/bbb2FqagpjY2OsXr26zPMaN26sdZCurq6QSCQqt0mTJpU6Pzs7GzNmzICLiwukUilcXFwwY8YMZGdnl9nHtm3b4O3tDSsrK9jb2+P111/HuXPntI6ZiIjopVgnyqCpPRKVkJCg0zm6PkZoZ2eH6dOnl9rfvn17pZ/z8vLg4+ODixcvonfv3vD390dcXBy+/fZbHD16FCdOnICVlZXSNYsXL8a8efPg7OyMSZMmITc3Fzt27EDXrl0RGRkJX19fnWInIiKiyhMZGYk//vgDt2/fRm5uLspauSSRSHD48GGt+1E7idqwYYPWnehDjRo1EBoa+tLzli5diosXL2LWrFn44osvFPtDQkKwcOFCLF26FAsWLFDsj4+PR0hICJo2bYrY2FjY2dkBAD788EN4e3tj/PjxuH79Op8yJCIi/eN0nl5lZ2dj8ODBiI6OLjNxep6uAzw6LSyvLK6urgBePhomCAIaNGiA7OxspKSkKI04FRQUoF69erC0tERycrLixs2dOxdhYWGIiIjAmDFjlNqbPHkyfvjhB0RGRqJPnz5qxcqF5dUMF5ZXK1xYXj1U6sLyt/SwsHwvF5bLTZ48GatXr0bNmjUxceJEtG3bFrVr1y43WfLx8dG6P4MZXnny5AkiIiJw9+5d2Nvbo0uXLmjTpo3SOfHx8bh37x769u1basrO3Nwcr732Gn799VfcvHkTbm5uAICoqCgAUJkk9e3bFz/88AOio6PVTqKIiIhIHD///DNMTU0RHR2NFi1aVHh/BpNEpaSkICAgQGlfv379sHnzZshkMgBQvANHniC9SL4/Pj5e6f+tra3h4OBQ7vllefLkCZ48eaL4ubzF60REREo4nadXeXl5cHd3r5QEClBzXf+mTZsQGRmplw4jIyOxadMmja4JCgpCVFQUHjx4gOzsbJw+fRr9+/fHH3/8gYEDByrmPbOysgBAsa7pRfKhTvl58v/X5PwXhYWFwc7OTrE5OTlp9NmIiKgaM4Ju783j03lKPDw88Pjx40rrT63bHxAQoLcCmp9//jkCAwM1uubTTz+Fj48PZDIZbGxs0LFjR+zfvx/dunXDqVOn8Pvvv+slNm3MmTMHWVlZii05OVm0WIiIyMAYWImDJUuWoE+fPnBycoKFhQVq1aqF9u3b45tvvkF+fr7G7cmfgLe1tYWNjQ18fX11GrSZMmUKbt26pViqU9EMNoc1MjJSJGMxMTEA/jcCVdbIkXyq7fmRJzs7O43Of5FUKoWtra3SRkRE9CpavXo1MjIy0Lt3b0ybNg3+/v4oKCjAxx9/jC5dumiUSG3duhX9+vXDlStXMHbsWAQGBuL69evo168ftm7dqlV8gYGB+OCDDzBkyBCsWLECubm5WrWjLrXXRF26dAk9evTQucNLly7p3IacfC2U/Jf2sjVMqtZMubm54dSpU0hJSSm1Lupla6yIiIh0ouuaKF2u1cK1a9dgbm5eav+YMWOwefNmbNiwAVOmTHlpOxkZGZg6dSpkMhnOnz+vWAozZ84ceHl5YerUqXj99ddhb2+vcYxLly5FcnIypk+fjunTp6N27dqwtLRUea5EIsGtW7c07kNO7SQqKytLb8NjutZlkDtz5gyA/5VAcHNzQ7169RATE4O8vLxSJQ6OHTuGevXqoUmTJor9Pj4+OHXqFA4dOlSqxIF8SFGXxx+JiIjKZGBJlKoECgCGDRuGzZs34+bNm2q1s3v3bmRmZmLBggVKa4kdHR0xffp0BAcHY/fu3Zg4caJG8aWmpqJXr164evWqYr10Wlpamefrmo+olUQdPXpUp050cfXqVdSrVw81atRQ2n/ixAl88803kEqlGDJkCIBnN2P8+PFYuHAhFi5cqFRsMywsDBkZGfjggw+UblpgYCC++uorLFq0CIMGDVJM3V25cgWbNm1C48aN9TICR0RE9Ko6cOAAAKBly5Zqnf+y8kLBwcGIjo7WOImaPXs2rly5giZNmuCTTz6Bp6fnS+tE6UKtJErMkZhdu3Zh6dKl6NmzJ1xdXSGVSnH58mUcOnQIRkZG+OGHH+Ds7Kw4f9asWfjtt9+wdOlSXLhwAe3atUNcXBwOHjwIT09PzJo1S6n9pk2bIjQ0FPPnz0fr1q0xbNgw5OXlYfv27SgsLMSaNWtYrZyIiCqGrovDRVrZHB4ejszMTGRmZiImJgbnzp1Dnz59Ss3olKW85TLqlBcqyx9//AFzc3NERUWhXr16Gl+vqSqfHfj5+eHatWs4f/48oqOjUVBQgLp162L48OH46KOP4O3trXS+lZUVoqKisGDBAuzZswdRUVFwcHDARx99hJCQkFJFOAFg3rx5cHV1RXh4OFatWgUzMzN06dIFCxcuRIcOHSrroxIRUXWjp+m8F2sUSqVSSKVSHRouX3h4OBITExU/jxo1CqtWrYKpqXrl18srSWRlZQVjY+NyywuVJS8vDx4eHpWSQAEG8toXQ8LXvlQzfO1LtcLXvlQPlfral3GArZkO7TwF7NaV3h8SElLm+2ZlMhkePXqkdh9Hjx6Fr6+vymMpKSk4evQoZs2aBVtbW0RGRqJBgwYvbbNp06aIj49HYWGhytkeExMTNG7cGP/884/acQJAly5dcPfuXaUEryJV+ZEoIiKiV5aepvOSk5OVEr7yRqH8/f2Rk5Ojdheq3ujx/DF/f380adIE3t7e+Pjjj7Fz586Xtvl8SaJatWopHcvLy0NxcXG55YXK8sknn2Do0KHYtWsX3nnnHY2v1xSTKCIiIrHIK5brcj2gUZ3CFStW6NChah06dIC9vb3aT/G7ubnh3LlziI+PL5VE6VJe6K233sLy5csxfvx4nDlzBkFBQWjcuHGZTxXqymCLbRIREVHVkJubi6ysLLUfxJI/sHbo0KFSx3QpL2RsbIxp06YhLy8P4eHhaN26tWKNlapN1wfHmEQRERGJRZf35um6KF1DiYmJSEhIKLW/sLAQ06dPR0lJCfr37690LD8/H9evX0dSUpLS/nfeeQd2dnZYsWKF0uvS7t+/j/DwcNSoUQNvv/22xjEKgqDRVlKi2xucOZ1HREQkFgMqcXDhwgUMHToU3bt3h5ubG2QyGVJTU/HXX38hOTkZ7u7upd6zGxsbCz8/P/j4+ChN9dnb22PlypUYPXo0vLy8MGLECBgZGWHnzp1ITU3F5s2btapWrmtSpCm1k6gePXqgdevWCA8Pr8BwiIiIqhEDqlju5eWFadOm4dixY9i7dy8yMzNhbW2NZs2aYerUqZgyZYrKMkJlGTVqFGQyGcLCwrBx40ZFHxEREejbt28FfQr9UjuJioqKQlFRUUXGQkRERFWUs7MzvvnmG42u8fX1RXmVlPr164d+/frpGppoOJ1HREQkFgMaiaLSmEQRERGJxYDWRFU1jRo1AgA0adJE8ZSffJ+6JBIJbt26pXUMTKKIiIjI4MifFHy+BpSqpwfLo+uLiZlEERERiYXTeVq7c+cOACi9r0++r7JolETFxMTA2Fi735hEIuHCdCIioudJoNuUnG4DKQbNxcVFrX0VSaMkiu8qJiIiInpGoySqVatWWL58eUXFQkREVL1wOs+gaZRE2dnZafUuGyIiIlKBSZTeFRYWYsOGDTh48CBu376N3NzcMmfS+HQeEREREYCHDx+iR48euHLlilpLkPh0HhERkaFinSi9Cg4OxuXLl9GgQQPMmjULHTp0QJ06dWBkVDE3ikkUERGRWDidp1f79++Hqakpjhw5giZNmlR4f0yiiIiIxMIkSq+ysrLg7u5eKQkUoEESVVJSUpFxEBEREemkSZMmePr0aaX1x9lUIiIisRjpYSOF8ePHIz4+Hn///Xel9MfbT0REJBYj/G9KT5uN3+JKPvzwQ/j7+2Pw4MH49ddfK7w/rokiIiKiV0LPnj0BAGlpaRgyZAjs7e3RuHFjWFlZqTxfIpHg8OHDWvfHJIqIiEgsLHGgV1FRUUo/p6enIz09vczzWSeKiIjIUPHpPL06evRopfbHJIqIiIheCZX9ajomUURERGLhSJRBYxJFREQkFq6JMmhMooiIiMjgBAUFAQAcHR2xaNEipX3qkkgkWLdundYxSAR1XnNMasvOzoadnR2yjgO21mJHQxXuDbEDoMp0/J7YEVBlyAPQH89eIWJra1shfSi+K1YBthY6tPMYsJtcsbFWVfKXCnt4eODq1atK+9QlkUhQXFysdQwciSIiIhILp/O0tmHDBgCAnZ1dqX2VhUkUERGRWOQVy3W5vpoaO3asWvsqUjW+/URERETa40gUERGRWFjiwKAxiSIiIhIL10RViOvXryMyMhK3b99Gbm4uynqGTten85hEERER0SuhsLAQEydOxKZNmwCgzORJjkkUERGRoeJ0nl59+umniIiIgJmZGYYMGYK2bduidu3aOr9ouCxMooiIiMTCJEqvtmzZAiMjIxw6dAivvfZahffH2VQiIiJ6JTx69AhNmzatlAQK4EgUERGReLiwXK8aNWpUqf3x9hMREYnFWA8bKQQGBuLatWu4dOlSpfTHJIqIiIheCR999BEGDhyIN998E/v27avw/jidR0REJBYJdBvOqJiHzgyWkZERfv75ZwwdOhSDBw9GzZo10bhxY1haWqo8XyKR4PDhw1r3xySKiIhILHw6T69yc3Px1ltv4ciRIxAEAY8ePcKjR4/KPF/X0gdMooiIiMTCJEqv5s2bh8OHD6NWrVqYOHEiPD09WSeKiIiIxLdkyRIcOXIE165dw8OHD2FpaYmGDRvi3XffxaRJk8qcNlOlvMQmLCwMwcHBGsf3008/wdTUFNHR0WjevLnG12uKSRQREZFYDKzEwerVqyGTydC7d2/UqVMHubm5iIqKwscff4xNmzbh5MmTGiVSLi4uCAgIKLW/W7duWsWXkZEBDw+PSkmgACZRRERE4jGw6bxr167B3Ny81P4xY8Zg8+bN2LBhA6ZMmaJ2e66urggNDdVbfO7u7sjNzdVbey/DEgdERESkFlUJFAAMGzYMAHDz5s3KDKeU999/Hzdv3kRUVFSl9MeRKCIiIrEY2EhUWQ4cOAAAaNmypUbXZWZmYu3atUhLS0Pt2rXh6+sLNzc3reMYP348rl+/jiFDhmDBggUIDAyEtbW11u29jEQQBKHCWq+GsrOzYWdnh6zjgG3F/d6oqnhD7ACoMh2/J3YEVBnyAPQHkJWVBVtb2wrpQ/Fd8Rdga6VDO3mAXS8gOTlZKVapVAqpVKqHSFULDw9HZmYmMjMzERMTg3PnzqFPnz7Yv38/TE1N1WpD1cJyiUSCkSNHYvXq1RqtrZKTv/bl33//RXFxMQCgdu3a5daJunXrlsb9yHEkioiIyMA5OTkp/RwSEqLXtUYvCg8PR2JiouLnUaNGYdWqVWonUAAwc+ZMvP3223Bzc4NEIsGFCxcwd+5cbNmyBUVFRdi+fbvGcSUkJJTal5aWVub5upY+4EiUnin+dVGB/4KhKuQxywVXKzvFDoAqQ/ZjwO79ShqJOqLbrEV2LmDXQ7ORKJlMVm4ByhcdPXoUvr6+Ko+lpKTg6NGjmDVrFmxtbREZGYkGDRpo9Bmel5+fjzZt2uDmzZu4fPkyWrRoodH1zyd26nJxcdH4GjmORBEREYlFTyUObG1t1U74/P39kZOTo3YXDg4O5R7z9/dHkyZN4O3tjY8//hg7d2r/rw1LS0v4+/vjs88+Q0xMjMZJlC4JkTaYRBEREVUjK1as0HubHTp0gL29vV6eipPJZACejUpVdSxxQEREJBZjPWxVQG5uLrKysmBiovvYzJkzZwA8qyFV1TGJIiIiEosBJVGJiYkqF24XFhZi+vTpKCkpQf/+/ZWO5efn4/r160hKSlLaf+HCBZUjTbt378b27dshk8nQq1evcuNp2bIldu7cCV2XdiclJWHSpEn44osvNL6W03lERERiMaDXvly4cAFDhw5F9+7d4ebmBplMhtTUVPz1119ITk6Gu7s7Fi1apHRNbGws/Pz84OPjozTVt2zZMvzyyy/o2bMnnJ2dIQgCzp8/j+PHj8Pc3BwREREvre+Uk5ODd999F/Pnz8eYMWMwYsQItWtMPX36FAcOHMDWrVuxb98+FBcXY82aNRrfEyZRRERE9FJeXl6YNm0ajh07hr179yIzMxPW1tZo1qwZpk6diilTpsDKSr2iV4MGDUJmZibOnz+PP/74A0VFRahfvz7GjRuHmTNnwsPD46Vt3LhxA8uXL8eSJUsUJR0aN24Mb29vtGvXDo6OjqhZsyakUikyMzORnp6Oa9eu4dy5czh37hzy8vIgCAJ69+6NL774Ap6enhrfE5Y40DOWOKhmWOKgemGJg2qhUkscnNVDiYMOFRtrVZeTk4MtW7ZgzZo1uHjxIoCy6z/JUx4rKyuMGDECEydORIcOHbTumyNRREREYnlFXvsiJhsbG0yePBmTJ09GfHw8jh07hpMnTyIxMREPHz5EQUEBatasiTp16sDT0xPdunVDly5dtKqI/iImUURERPRKcHNzg5ubG8aNG1cp/TGJIiIiEosEui0O54oCUTGJIiIiEgun8wwakygiIiIyeA8ePMCvv/6KM2fOID4+HhkZGXj8+DEsLCxgb28PNzc3dOzYEQMHDkSdOnX00ieTKCIiIrEYUJ2oqqqgoACzZs3Cjz/+iMLCwjKLbx47dgzr16/H1KlTMWHCBCxduhQWFhY69c0kioiISCycztPJkydP4Ovri7Nnz0IQBHh4eKBr165o1KgR7O3tIZVK8eTJE2RkZOD27duIiYnB9evX8f333yM2NhbHjx+HmZmZ1v0ziSIiIiKD9OWXXyI2Nhbu7u5Yv349Onfu/NJrTp48iaCgIJw7dw5Lly7F/Pnzte6fA4FERERiMaB351VF27dvh5mZGQ4dOqRWAgUAXbp0QWRkJExMTLBt2zad+udIFBERkVi4Jkond+7cQcuWLeHk5KTRdS4uLmjZsiWuXbumU/9MooiIiMTCNVE6sba2RlpamlbXpqWlqf2uv7JU8xyWiIiIDFXnzp1x9+5dfPPNNxpd99VXX+Hu3bvo0qWLTv0ziSIiIhKLEXRbD1XNv8WDg4NhZGSETz75BK+//jr27NmD+/fvqzz3/v372LNnD/r374/Zs2fD2NgYc+bM0al/TucRERGJhWuidNK5c2ds3LgR48ePxx9//IHIyEgAgFQqRY0aNWBmZoanT58iMzMTT548AQAIggAzMzOsWbMGnTp10qn/an77iYiIyJCNHDkS169fx+TJk+Hg4ABBEFBQUICUlBQkJSUhJSUFBQUFEAQBdevWxeTJk3H9+nWMHj1a5745EkVERCQWLizXCxcXF3z33Xf47rvvkJSUpHjtS0FBAczNzRWvfXF2dtZrv0yiiIiIxMLpPL1zdnbWe7JUFt5+IiIiIi1wJIqIiEgsnM4Tzd27d1FcXKzTqBWTKCIiIrEwiRKNp6cnMjIyUFRUpHUbnM4jIiKiakkQBJ2u50gUERGRWLiw3KAxiSIiIhKLxAiQSHS4XgBQordwDM3ixYu1vvbx48c6988kioiISDQmAHRIoiAAeKqnWAzP/PnzIdEyCRUEQetr5ZhEERERkUEyNjZGSUkJhgwZAmtra42u3bFjB54+1S0BZRJFREQkGo5E6aJFixa4dOkSJkyYgD59+mh07f79+5Genq5T/1ySRkREJBoTPWzVl7e3NwDg3LlzovTPJIqIiIgMkre3NwRBwJkzZzS+VtfyBkB1T2GJiIhEZQzdxjOq75N5ANCrVy9MmzYNMplM42t/++03FBYW6tQ/kygiIiLRmIBJlPZcXV3x7bffanVtly5ddO6f03lEREREWuBIFBERkWg4EmXImEQRERGJhkmUIWMSRURERK8EY2Njtc81MjKCjY0NXF1d0a1bN4wfPx6tW7fWqD+uiSIiIhKNsR42khMEQe2tuLgYmZmZuHjxIlauXIl27drhyy+/1Kg/JlFERESiMYZuhTaZRD2vpKQE33zzDaRSKcaOHYuoqCikp6ejsLAQ6enpiI6ORkBAAKRSKb755hvk5ubi3LlzeP/99yEIAoKDg3H48GG1++N0HhERkWh0TYR0e4Huq+ann37Cxx9/jJUrV2Ly5MlKx2rUqIHu3buje/fu6NChA6ZOnYr69evj7bffhpeXFxo1aoSZM2di5cqV6Nmzp1r9cSSKiIiItHL69GkYGxtDIpFgyZIlGl8fGRkJX19f2NrawsbGBr6+voiMjNQ6nq+++gqOjo6lEqgXTZ48GY6Ojvj6668V+z788EPY2tri9OnTavfHJIqIiEg0hvvuvMePHyMgIAAWFhZaXb9161b069cPV65cwdixYxEYGIjr16+jX79+2Lp1q1ZtXr58GfXr11fr3Pr16+Pq1auKn01MTNC0aVONXkrMJIqIiEg0hptEzZs3D/fv30dwcLDG12ZkZGDq1KmQyWQ4f/48VqxYgeXLl+PChQtwcHDA1KlTkZGRoXG7pqamuHHjBp48eVLueU+ePMGNGzdgYqJ8/7Kzs2FjY6N2f0yiiIiISCMxMTFYtmwZvvrqKzRo0EDj63fv3o3MzEx88MEHcHJyUux3dHTE9OnTkZmZid27d2vcbteuXZGdnY2pU6eipER1DS1BEPDBBx8gKysL3bp1U+x/+vQp7ty5g3r16qndH5MoIiIi0RjeSFR+fj4CAgLg6+uLCRMmaNVGVFQUAKBPnz6ljvXt2xcAEB0drXG7CxcuhJmZGdavX49WrVphyZIl+P3333H8+HEcPHgQX3zxBVq3bo1169ZBKpVi4cKFimv37t2LwsJC+Pn5qd0fn84jIiISjbzEgeEIDg7G/fv3cejQIa3biI+PBwC4ubmVOibfJz9HE23btsW+ffswevRoXLt2DfPmzSt1jiAIcHBwwObNm+Hp6anYX7duXWzYsAHdu3dXuz/D+s0RERFRKdnZ2Uo/S6VSSKVSvfcTHR2NlStXIjw8HA0bNtS6naysLACAnZ1dqWNWVlYwNjZWnKOpXr16IT4+Htu2bcOff/6J+Ph45OXlwcrKCk2bNkXv3r3h7+8Pa2trpet8fX017otJFBERkWj0MyX3/LoiAAgJCUFoaKjKc2UyGR49eqR220ePHoWvry/y8vIQFBSEzp07Y+rUqbqEW+Gsra0xceJETJw4sUL7YRJFREQkGv0kUcnJybC1tVX8XN4olL+/P3JyctRu28HBAcCzp/Hu3buH33//HUZGui2plo9AZWVloVatWkrH8vLyUFxcrHKUqqphEkVERGTgbG1tlZKo8qxYsUKrPi5evIiCggJ4eHioPD5nzhzMmTMH06ZNQ3h4eLltubm54dy5c4iPjy+VRJW3XkoTd+7cwZ9//okbN24gJycHNjY2iuk8XaYin8ckioiISDTi1nrSxBtvvIEmTZqU2h8fH49jx46hQ4cOaN26NTp37vzStnx8fLB9+3YcOnQInTp1Ujomr1ju4+OjVZwZGRl4//33sXv3bgiCAODZYnKJ5NkrciQSCYYPH46VK1fC3t5eqz7kJIK8hypq48aNCAwMLPecHj16KL0wMDs7G6Ghofjpp5+QkpICBwcHDB06FKGhoWVm6tu2bUN4eDiuXLkCMzMzdO7cGQsXLkT79u01ijc7Oxt2dnbIyspS+18FZMAe871V1cpOsQOgypD9GLB7HxX69/j/vit6w9bWVId2CmFn96eo3zny7+mwsLBShTfz8/ORlJQES0tLODs7K/ZnZGSgYcOGMDU1xfnz5xVruu7fvw8vLy8UFBTg9u3bGic5jx8/RteuXREXFwdBENC5c2e0aNECdevWRWpqKq5cuYJTp05BIpHA09MTMTExMDc31/qzV/n019PTEyEhISqP7dmzB1euXFHUlACezaX6+Pjg4sWLihX4cXFx+Pbbb3H06FGcOHECVlZWSu0sXrwY8+bNg7OzMyZNmoTc3Fzs2LEDXbt2VbzXh4iISP90HYmq0uMgiI2NhZ+fH3x8fBS1oQDA3t4eK1euxOjRo+Hl5YURI0bAyMgIO3fuRGpqKjZv3qzVKNG3336LixcvwsPDA5s2bVI5EHLu3DmMHTsWFy9eRHh4uFYV1+UMIol6vo6D3NOnT7Fy5UqYmJhg7Nixiv1Lly7FxYsXMWvWLHzxxReK/SEhIVi4cCGWLl2KBQsWKPbHx8cjJCQETZs2RWxsrGIh24cffghvb2+MHz8e169fL1UanoiIiLQ3atQoyGQyhIWFYePGjQAALy8vREREKA2OaGLXrl0wNjbG/v370ahRI5XntG/fHr/99hs8PDywY8cOnZKoKj+dV5adO3dixIgRGDx4MPbu3Qvg2ZxngwYNkJ2djZSUFKURp4KCAtSrVw+WlpZITk5WzI3OnTsXYWFhiIiIwJgxY5T6mDx5Mn744QdERkaqrKqqCqfzqhlO51UvnM6rFip3Om+AHqbz9vE75/9ZW1vDzc0NFy5ceOm5bdu2RXx8PHJzc7Xuz2Bf+7Ju3ToAwPjx4xX74uPjce/ePXTt2rXUlJ25uTlee+013L17Fzdv3lTsr6jS80RERC9neK99qcqMjY1RWFio1rmFhYU6l2owyCQqMTERhw8fRv369dGvXz/F/pc9FqmqlHx8fDysra0VdTBedv6Lnjx5guzsbKWNiIiIKp+7uzuuXbuGuLi4cs+7ePEirl69imbNmunUn0EmURs2bEBJSQkCAwNhbGys2F9eGXkAiqHO50vJZ2VlaXT+i8LCwmBnZ6fYXqwaS0REVDaOROnT6NGjIQgC3nzzTezbt0/lOb/99hsGDhwIiUSC0aNH69Sfwd39kpISbNiwARKJBEFBQWKHgzlz5mDGjBmKn7Ozs5lIERGRmnR9AXGJvgJ5JUyePBm//PILjh49isGDB8PZ2RkeHh6oU6cO0tLScO3aNSQnJ0MQBPTo0QOTJ0/WqT+DS6L+/PNPJCUloWfPnqUqjj5fRl4V+VTb8yNP8kXg6p7/oop6ySMRERFpxsTEBAcOHMD8+fPxww8/IDExEYmJiUrnWFpaYvLkyfjss8+UZrO06k+nq0WgakG53MvWMKlaM+Xm5oZTp04pinK+7HwiIiL9Mf7/TZfr6Xnm5ub46quvEBISghMnTuDGjRvIzc2FtbU1mjZtim7dusHGxkYvfRlUEvXo0SP8+uuvqFmzJt56661Sx93c3FCvXj3ExMQgLy+vVImDY8eOoV69ekpl6318fHDq1CkcOnSoVIkDXUvPExERlU/XdU2cziuLjY0N+vfvj/79+1dYHwa1sHzz5s14+vQpRo0apXIKTSKRYPz48cjNzcXChQuVjoWFhSEjIwPjx49X1IgCgMDAQJiYmGDRokVK03pXrlzBpk2b0LhxY/To0aPiPhQREREZJIMaiSpvKk9u1qxZ+O2337B06VJcuHAB7dq1Q1xcHA4ePAhPT0/MmjVL6fymTZsiNDQU8+fPR+vWrTFs2DDk5eVh+/btKCwsxJo1a1itnIiIKghHorSVlJSkl3aef6efpgwmO4iNjcXly5fh7e2NVq1alXmelZUVoqKisGDBAuzZswdRUVFwcHDARx99hJCQkFJFOAFg3rx5cHV1RXh4OFatWgUzMzN06dIFCxcuRIcOHSryYxERUbXGJEpbrq6uSjNL2pBIJCgqKtL+ekN97UtVxde+VDN87Uv1wte+VAuV+9qX92Frq/0T3tnZT2Bn9321/M7RRxIFAHfu3NH6WoMZiSIiIiKSS0hIEDsEJlFERETi0XU6r1hfgZAWmEQRERGJhkmUITOoEgdEREREVQVHooiIiETDkShDxiSKiIhINLq+gFj7x/NJd5zOIyIiItICR6KIiIhEo+t0Hr/GxcS7T0REJBomUYaM03lEREREWmAKS0REJBqORBky3n0iIiLRMIkyZLz7REREotG1xIGxvgIhLXBNFBEREZEWOBJFREQkGk7nGTLefSIiItEwiTJknM4jIiIi0gJTWCIiItEYQ7fF4VxYLiYmUURERKLh03mGjNN5RERERFrgSBQREZFouLDckPHuExERiYZJlCHjdB4RERGRFpjCEhERiYYjUYaMd5+IiEg0TKIMGe8+ERGRaFjiwJBxTRQRERGRFphEERERicZED5t4Tp8+DWNjY0gkEixZskSjayUSSZmbpm2JhdN5REREojHcNVGPHz9GQEAALCwskJeXp1UbLi4uCAgIKLW/W7duOkZXOZhEERERkcbmzZuH+/fvIzg4GP/5z3+0asPV1RWhoaH6DawSMYkiIiISjWGORMXExGDZsmX44YcfYGpqKkoMVQGTKCIiItEYXhKVn5+PgIAA+Pr6YsKECdi4caPWbWVmZmLt2rVIS0tD7dq14evrCzc3N/0FW8GYRBERERm47OxspZ+lUimkUmmF9BUcHIz79+/j0KFDOrcVFxeHCRMmKH6WSCQYOXIkVq9eDUtLS53br2h8Oo+IiEg08jpR2m7P6kQ5OTnBzs5OsYWFhVVItNHR0Vi5ciUWL16Mhg0b6tTWzJkzcebMGaSnpyMjIwNHjhxBx44dsWXLFowbN05PEVcsjkQRERGJRj/TecnJybC1tVXsLW8USiaT4dGjR2r3cPToUfj6+iIvLw9BQUHo3Lkzpk6dqn3I/+/LL79U+tnPzw+HDx9GmzZtsGPHDsyfPx8tWrTQuZ+KxCSKiIhINPpJomxtbZWSqPL4+/sjJydH7R4cHBwAPHsa7969e/j9999hZFQxE1mWlpbw9/fHZ599hpiYGCZRREREVHWsWLFCq+suXryIgoICeHh4qDw+Z84czJkzB9OmTUN4eLjW8clkMgDPFrBXdUyiiIiIRGM4T+e98cYbaNKkSan98fHxOHbsGDp06IDWrVujc+fOOvVz5swZAM9qSFV1TKKIiIhEYzgvIP7kk09U7t+4cSOOHTuGIUOGIDg4WOlYfn4+kpKSYGlpCWdnZ8X+CxcuwN3dvdQTeLt378b27dshk8nQq1cv/X8IPWMSRURERBUiNjYWfn5+8PHxQVRUlGL/smXL8Msvv6Bnz55wdnaGIAg4f/48jh8/DnNzc0RERMDa2lq8wNXEJIqIiEg0xtBtNKnyRqL0adCgQcjMzMT58+fxxx9/oKioCPXr18e4ceMwc+bMMtddVTUSQRAEsYN4lWRnZ8POzg5ZWVlqPylBBuyxROwIqDLtFDsAqgzZjwG791Ghf4//77viKmxtbXRoJwd2ds35nSMSFtskIiIi0gKn84iIiERjOE/nUWm8+0RERKJhEmXIOJ1HREREpAWmsERERKIxnDpRVBqTKCIiItFwOs+Q8e4TERGJhkmUIeOaKCIiIiItMIUlIiISDUeiDBnvPhERkWiYRBky3n09k79FJzs7W+RIqFI8FjsAqlT8fVcL2f//e66Mt6Lp+l3B7xpxMYnSs5ycHACAk5OTyJEQEZEucnJyYGdnVyFtm5mZwcHBQS/fFQ4ODjAzM9NDVKQpvoBYz0pKSnDv3j3Y2NhAIqk+L6fNzs6Gk5MTkpOT+RLMVxx/19VHdf1dC4KAnJwc1KtXD0ZGFff8VUFBAZ4+fapzO2ZmZjA3N9dDRKQpjkTpmZGRERo0aCB2GKKxtbWtVn/ZVmf8XVcf1fF3XVEjUM8zNzdn8mPgWOKAiIiISAtMooiIiIi0wCSK9EIqlSIkJARSqVTsUKiC8XddffB3TVQ+LiwnIiIi0gJHooiIiIi0wCSKiIiISAtMooiIiIi0wCSKiIiISAtMokhrW7ZswXvvvYf27dtDKpVCIpFg48aNYodFepaZmYkPP/wQnTt3hoODA6RSKerXr48ePXrgp59+qpT3i1HlcnV1hUQiUblNmjRJ7PCIqgxWLCetzZ8/H4mJiZDJZHB0dERiYqLYIVEFePjwIdavX49OnTph8ODBqFmzJtLS0rBv3z4MGzYMEyZMwI8//ih2mKRndnZ2mD59eqn97du3r/xgiKooljggrf31119wc3ODi4sLlixZgjlz5mDDhg0ICAgQOzTSo+LiYgiCABMT5X9z5eTkoFOnTrh69SouX76MFi1aiBQh6ZurqysAICEhQdQ4iKo6TueR1nr16gUXFxexw6AKZmxsXCqBAgAbGxv07dsXAHDz5s3KDouISHScziMirRQUFODIkSOQSCRo3ry52OGQnj158gQRERG4e/cu7O3t0aVLF7Rp00bssIiqFCZRRKSWzMxMhIeHo6SkBGlpafj999+RnJyMkJAQuLm5iR0e6VlKSkqpqfl+/fph8+bNkMlk4gRFVMUwiSIitWRmZmLBggWKn01NTfHll1/i448/FjEqqghBQUHw8fFBixYtIJVKcfXqVSxYsAAHDx7EwIEDERMTA4lEInaYRKLjmigiUourqysEQUBRURHu3LmDhQsXYt68eRg6dCiKiorEDo/06NNPP4WPjw9kMhlsbGzQsWNH7N+/H926dcOpU6fw+++/ix0iUZXAJIqINGJsbAxXV1cEBwfj888/x969e7FmzRqxw6IKZmRkhMDAQABATEyMyNEQVQ1MoohIa3369AEAREVFiRsIVQr5Wqj8/HyRIyGqGphEEZHW7t27BwAqSyDQq+fMmTMA/ldHiqi6YxJFROW6ePEisrKySu1PT0/H3LlzAQD9+/ev7LCogly9ehWZmZml9p84cQLffPMNpFIphgwZUvmBEVVB/OcjaW3t2rU4ceIEAODSpUuKffKpncGDB2Pw4MEiRUf6snHjRqxduxZ+fn5wcXGBlZUVEhMTceDAAeTm5mLo0KF49913xQ6T9GTXrl1YunQpevbsCVdXV0ilUly+fBmHDh2CkZERfvjhBzg7O4sdJlGVwCSKtHbixAlEREQo7YuJiVEsOnV1dWUS9QoYNmwYsrKycPr0aRw7dgz5+fmoWbMmunXrhjFjxmDEiBF83P0V4ufnh2vXruH8+fOIjo5GQUEB6tati+HDh+Ojjz6Ct7e32CESVRl8dx4RERGRFrgmioiIiEgLTKKIiIiItMAkioiIiEgLTKKIiIiItMAkioiIiEgLTKKIiIiItMAkioiIiEgLTKKIiIiItMAkioiIiEgLTKKIiIiItMAkioj0IiEhARKJRGkLDQ2t0D49PT2V+vP19a3Q/oiInsckisiAxMTEYOLEifDw8ICdnR2kUinq16+PN998E2vXrkVeXp7YIUIqlaJr167o2rUrnJ2dSx13dXVVJD0ff/xxuW0tW7ZMKUl6Udu2bdG1a1e0bNlSb/ETEamLLyAmMgD5+fkIDAzErl27AADm5uZo3LgxLCwscPfuXdy/fx8A4OjoiMjISLRq1arSY0xISEDDhg3h4uKChISEMs9zdXVFYmIiAMDBwQH//vsvjI2NVZ7boUMHnDt3TvFzWX9dRUVFwc/PDz4+PoiKitL6MxARaYIjUURVXGFhIfr06YNdu3bBwcEBERERSE9Px+XLl3H27Fncu3cPV65cwXvvvYcHDx7g1q1bYoesFnd3d6SkpOCvv/5Sefyff/7BuXPn4O7uXsmRERGph0kUURW3YMECxMTEoG7dujh16hTGjBkDCwsLpXOaN2+OH374AUePHkWdOnVEilQzo0aNAgBs2bJF5fHNmzcDAEaPHl1pMRERaYJJFFEVlpWVheXLlwMAwsPD4erqWu753bp1Q5cuXSohMt35+PjAyckJe/fuLbWWSxAEbN26FRYWFhgyZIhIERIRlY9JFFEVduDAAeTk5KB27doYNmyY2OHolUQiwciRI5GXl4e9e/cqHTtx4gQSEhIwePBg2NjYiBQhEVH5mEQRVWEnT54EAHTt2hUmJiYiR6N/8qk6+dSdHKfyiMgQMIkiqsLu3r0LAGjYsKHIkVSM5s2bo23btjh8+LDiCcMnT55g9+7dqFOnDnr37i1yhEREZWMSRVSF5eTkAACsrKx0aqd3796QSCSlRnyel5CQgEGDBsHGxgb29vYYPXo0Hj58qFO/6hg9ejSKi4uxfft2AMD+/fuRmZkJf3//V3L0jYheHUyiiKow+XogXYpo3r9/H0eOHAFQ9pNwubm58PPzw927d7F9+3b8+OOPOHnyJN544w2UlJRo3bc6/P39YWxsrEjw5P+VP71HRFRV8Z95RFVY/fr1AQB37tzRuo1t27ahpKQEvXv3xuHDh5GSkgIHBwelc1avXo379+/j5MmTcHR0BPCsKKa3tzd+/fVXvPXWW9p/iJdwcHBAr169EBkZiWPHjuHgwYPw8PBA+/btK6xPIiJ94EgUURUmL1dw8uRJFBUVadXG5s2b0bp1ayxZskRp2ux5+/fvh5+fnyKBAp5VC2/atCn27dunXfAakC8gHz16NJ4+fcoF5URkEJhEEVVhr7/+OqytrZGWloY9e/ZofP2VK1cQFxeHkSNHwsvLC82bN1c5pXf16lW0aNGi1P4WLVrg2rVrWsWuibfeegvW1tZISkpSlD4gIqrqmEQRVWE1atTABx98AACYPn16ue+kA569oFheFgF4NgolkUjw7rvvAni2zuj8+fOlEqOMjAzUqFGjVHs1a9ZEenq6bh9CDZaWlvj444/Rs2dPvPfee3BxcanwPomIdMUkiqiKCw0NRefOnZGamorOnTtj8+bNKCgoUDrnxo0bmDJlCnx9fZGWlgbgWdXvbdu2wcfHBw0aNAAAjBw5EhKJROVolEQiKbWvMt9PHhoair/++gurVq2qtD6JiHTBJIqoijMzM8OhQ4cwdOhQpKSkYMyYMahZsyZatWoFb29vNGjQAO7u7vj+++/h4OCAJk2aAACioqKQnJyMQYMGITMzE5mZmbC1tUXHjh2xdetWpQTJ3t4eGRkZpfrOyMhAzZo1K+2zEhEZEiZRRAbA2toae/bswbFjxzBu3Dg4OTkhISEBcXFxEAQBb7zxBtatW4cbN26gZcuWAP5XzuCjjz6Cvb29Yjt9+jQSExNx4sQJRfstWrTA1atXS/V79epVNGvWrHI+JBGRgWGJAyID0r17d3Tv3v2l5xUUFGDPnj3o168fZs+erXSssLAQAwcOxJYtWxRtvfnmm5g3b55S+YO///4b//zzD8LCwvT6GV62rutFDRo0qNRpRSIidUkE/u1E9MrZtWsXhg8fjv379+ONN94odXz48OH4888/kZKSAjMzM+Tk5KB169aoXbs2QkJCUFBQgNmzZ6NWrVo4deoUjIxePmidkJCAhg0bQiqVKmo8BQUFISgoSO+fTy4wMBDx8fHIysrC5cuX4ePjg6ioqArrj4joeZzOI3oFbdmyBQ4ODujXr5/K44GBgcjIyMCBAwcAPKuMfuTIETg4OGD48OEYN24cOnXqhP3796uVQD3vyZMniImJQUxMDJKSknT+LOW5cOECYmJicPny5Qrth4hIFY5EEREREWmBI1FEREREWmASRURERKQFJlFEREREWmASRURERKQFJlFEREREWmASRURERKQFJlFEREREWmASRURERKQFJlFEREREWmASRURERKQFJlFEREREWmASRURERKSF/wOJlcL9geJOrwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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unTpolPdERER6NWrl8Z2a2trhIWFYd68efpdVCWqdrPDiIiISJ2bm5vawr0rV64ssez777+PuLg4bN68GbVq6f/w1jp16mD16tVISEhATk4Obt26hR07dsDJyQnz58/H+vXr9T53Zak23WFEREQ1j6HdYUUAoLUlSJu4uDi89957eOedd9C+fXsD6n3yoPGnu9Jq166NsWPHok2bNujQoQNCQkIwdepUmJlV3faWqhsZERFRjWecx2bY29urvUpKgiZOnAhvb+8Knc3VqlUrdO7cGXfv3lV7YHlVxJYgIiIiExEXFwcAsLKy0rq/a9euAID9+/dj6NChetejHBj96NEjvc9RGZgEERERScY43WG6eu2117Ruj4qKQlJSEoYMGYI6derA09NT74gKCwtx4cIFyGQyuLu7632eysAkiIiISDLKB6jqq7BcpTdu3Kh1+6RJk5CUlIRFixZpzA5LT09Heno6nJ2d1aa+K2eSPb0AcWFhIebNm4eUlBT0798fTk5O5YqvsjEJIiIiohKFh4cjLCwMISEhamOJxowZA5lMhm7dusHV1RUZGRmIiorC1atX4e7ujnXr1kkXtI6YBBEREUnG0O4w6b7GZ8yYgaNHjyIiIgLp6emwsLBA48aNsWTJErz99ttwdHSULDZdcbFEI+NiiaaFiyWaFi6WaBoqd7HEMNjbax+krNt5cqFQhFRorDUZp8gTERGRSWJ3GBERkWSqb3dYTcC7R0REJBkmQVLi3SMiIpKMoVPkzY0ViEnimCAiIiIySWwJIiIikgy7w6TEu0dERCQZJkFSYncYERERmSSmkERERJIxh2GDmzkw2hBMgoiIiCTD2WFSYncYERERmSS2BBEREUmGA6OlxLtHREQkGSZBUmJ3GBEREZkkppBERESSYUuQlHj3iIiIJMMkSEq8e0RERJLhFHkpcUwQERERmSS2BBEREUmG3WFS4t0jIiKSDJMgKbE7jIiIiEwSU0giIiLJsCVISrx7REREkmESJCV2hxEREZFJYgpJREQkGa4TJCUmQURERJJhd5iUePeIiIgkwyRIShwTRERERCaJKSQREZFk2BIkJd49IiIiyXBgtJTYHUZEREQmiS1BREREkjGHYa05bAkyBJMgIiIiyXBMkJTYHUZEREQmiSkkERGRZNgSJCXePSIiIskwCZISu8OIiIjIJDGFJCIi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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "### Define inputs\n", + "# Control time set [h]\n", + "t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]\n", + "# Define parameter nominal value\n", + "parameter_dict = {\"A1\": 85, \"A2\": 372, \"E1\": 8, \"E2\": 15}\n", + "\n", + "# measurement object\n", + "measurements = MeasurementVariables()\n", + "measurements.add_variables(\n", + " \"C\", # variable name\n", + " indices={0: [\"CA\", \"CB\", \"CC\"], 1: t_control}, # indices\n", + " time_index_position=1,\n", + ") # position of time index\n", + "\n", + "# design object\n", + "exp_design = DesignVariables()\n", + "\n", + "# add CAO as design variable\n", + "exp_design.add_variables(\n", + " \"CA0\", # variable name\n", + " indices={0: [0]}, # indices\n", + " time_index_position=0, # position of time index\n", + " values=[5], # nominal value\n", + " lower_bounds=1, # lower bound\n", + " upper_bounds=5, # upper bound\n", + ")\n", + "\n", + "# add T as design variable\n", + "exp_design.add_variables(\n", + " \"T\", # variable name\n", + " indices={0: t_control}, # indices\n", + " time_index_position=0, # position of time index\n", + " values=[470, 300, 300, 300, 300, 300, 300, 300, 300], # nominal value\n", + " lower_bounds=300, # lower bound\n", + " upper_bounds=700, # upper bound\n", + ")\n", + "\n", + "# For each variable, we define a list of possible values that are used\n", + "# in the sensitivity analysis\n", + "\n", + "design_ranges = {\n", + " \"CA0[0]\": [1, 3, 5],\n", + " (\n", + " \"T[0]\",\n", + " \"T[0.125]\",\n", + " \"T[0.25]\",\n", + " \"T[0.375]\",\n", + " \"T[0.5]\",\n", + " \"T[0.625]\",\n", + " \"T[0.75]\",\n", + " \"T[0.875]\",\n", + " \"T[1]\",\n", + " ): [300, 500, 700],\n", + "}\n", + "## choose from \"sequential_finite\", \"direct_kaug\"\n", + "sensi_opt = \"direct_kaug\"\n", + "\n", + "prior_pass = [\n", + " [22.52943024, 1.84034314, -70.23273336, -11.09432962],\n", + " [1.84034314, 18.09848116, -5.73565034, -109.15866135],\n", + " [-70.23273336, -5.73565034, 218.94192843, 34.57680848],\n", + " [-11.09432962, -109.15866135, 34.57680848, 658.37644634],\n", + "]\n", + "\n", + "doe_object = DesignOfExperiments(\n", + " parameter_dict, # parameter dictionary\n", + " exp_design, # design variables\n", + " measurements, # measurement variables\n", + " create_model, # model function\n", + " prior_FIM = prior_pass, \n", + " discretize_model=disc_for_measure, # discretization function\n", + ")\n", + "# run full factorial grid search\n", + "all_fim = doe_object.run_grid_search(design_ranges, mode=sensi_opt)\n", + "\n", + "all_fim.extract_criteria()\n", + "\n", + "for i in all_fim.store_all_results_dataframe.columns:\n", + " all_fim.store_all_results_dataframe[i] = all_fim.store_all_results_dataframe[i].values.real\n", + "\n", + "\n", + "fixed = {}\n", + "all_fim.figure_drawing(\n", + " fixed, \n", + " [\"CA0[0]\",\"T[0]\"],\n", + " \"Reactor\",\n", + " \"$C_{A0}$ [M]\",\n", + " \"T [K]\"\n", + ")\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "b805fb89-45a9-4ca2-8c72-325747d3fd25", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: =======Iteration Number: 1 =====\n", + "INFO: elapsed time: 0.7\n", + "INFO: This is run 1 out of 8.\n", + "INFO: The code has run 0.7312748000040301 seconds.\n", + "INFO: Estimated remaining time: 2.1938244000120903 seconds\n", + "INFO: =======Iteration Number: 2 =====\n", + "INFO: elapsed time: 0.8\n", + "INFO: This is run 2 out of 8.\n", + "INFO: The code has run 1.5368452000038815 seconds.\n", + "INFO: Estimated remaining time: 2.5614086666731355 seconds\n", + "INFO: =======Iteration Number: 3 =====\n", + "INFO: elapsed time: 0.8\n", + "INFO: This is run 3 out of 8.\n", + "INFO: The code has run 2.300336200009042 seconds.\n", + "INFO: Estimated remaining time: 2.300336200009042 seconds\n", + "INFO: =======Iteration Number: 4 =====\n", + "INFO: elapsed time: 1.0\n", + "INFO: This is run 4 out of 8.\n", + "INFO: The code has run 3.2964527000076487 seconds.\n", + "INFO: Estimated remaining time: 1.9778716200045894 seconds\n", + "INFO: =======Iteration Number: 5 =====\n", + "INFO: elapsed time: 0.7\n", + "INFO: This is run 5 out of 8.\n", + "INFO: The code has run 3.9811715000105323 seconds.\n", + "INFO: Estimated remaining time: 1.3270571666701774 seconds\n", + "INFO: =======Iteration Number: 6 =====\n", + "INFO: elapsed time: 0.7\n", + "INFO: This is run 6 out of 8.\n", + "INFO: The code has run 4.726026500014996 seconds.\n", + "INFO: Estimated remaining time: 0.675146642859285 seconds\n", + "INFO: =======Iteration Number: 7 =====\n", + "INFO: elapsed time: 0.8\n", + "INFO: This is run 7 out of 8.\n", + "INFO: The code has run 5.568790400015132 seconds.\n", + "INFO: Estimated remaining time: 0.0 seconds\n", + "INFO: =======Iteration Number: 8 =====\n", + "INFO: elapsed time: 0.7\n", + "INFO: This is run 8 out of 8.\n", + "INFO: The code has run 6.320087100015371 seconds.\n", + "INFO: Estimated remaining time: -0.7022319000017079 seconds\n", + "INFO: Overall wall clock time [s]: 6.320087100015371\n" + ] + }, + { + "data": { + "image/png": "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4eTkBE9PTxgZGVW5njod8JWUlGDLli2QSCQYPXp0TTdHrWbNmoVp06bJ/s7JyYGVlVUNtoiIiIiqi5GREXr16oVevXpVS/l1OuA7cuQIUlNT0b17d4WDhaW9a6X1okmHSF/thTMxMalQ+tJUpu7X6ejoQEdHp9y6iIiI5LGHrzZKTU1VSznW1taVylenAz5lizWkyptzp2yenaOjI06fPo2HDx8qzOMrb15eVesmIiJSD3UFfKROtra2kFRxbqVEIkFxcXGl8tbZgO/x48f47bffYGZmho8//ljhuaOjIywtLXHq1Ck8ffpUbrVsQUEBoqOjYWlpiebNm8vue3t74/Tp0zh8+DBGjBghV15kZKQsTXkqUzcRERGJl7W1dZUDvqqos9uybNu2Dc+fP8ewYcOUDn1KJBIEBAQgLy8P8+bNk3u2cOFCPHnyBAEBAXJvvr+/P7S0tLBgwQK54dhr167hp59+goODA7p16yZXVmpqKuLj4/Hs2bMq1U1ERKQemnjZn1OVizsvq1tycjJu375d5auyJEId3f23bdu2uHr1Kq5cuYK2bdsqTfP06VN4enrKjjfr0KEDLl++jD/++ANubm5KjzdbsGAB5syZA2trawwYMABPnz7Fjh07kJ+fj8jISPj4+Mil79q1K44fP45jx47JHbtWmbrLkpOTI5tjKF3lSyQ+Y2u6AUTVJifnOUxMtlbbf8f/9zvRCMbGVevPyckpgYnJI/7miEid7OGLjY3F1atX0alTp1KDPQAwMDBAVFQUpk6divj4eCxbtgxXr17F1KlTERUVpTTgmj17NsLDw2Fubo61a9di586d6NKlC06dOqUQ7JWlMnUTERERVYc628P3tmEPH70d2MNH4vXmeviaqKmH74HKbb137x527dqFgwcPIj4+Hg8fPoSZmRk8PDwwY8YMvPPOOyrXfffuXXzzzTf4448/8PDhQzRs2BA9e/bEvHnzytyebO/evVizZg0uXryIZ8+ewcLCAu+++y6WLFkil2/jxo3Yt28frl69ivT0dGhpacHW1hYfffQRvvjiC5iZmcmVm5ycrLATyKt27NiBIUOGqPz6akqdXbRBREREymih6gN4FTtlY9WqVVi8eDEcHBzg6+sLc3NzJCQkICIiAhEREdixYwcGDRpUbjlJSUno0qUL0tPT4evri8GDByMhIQFbt27FwYMH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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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1aVLF8XExCgkJETp6en6/PPPlZOTo7vvvlv/+te/5Od36avw++1fWrVqpS1btujLL78sdfuXqVOn6rnnnrNtPXP27FktWrRIubm5WrFihbp06WJ3fefOnbVmzRqtXr3ablsaV+q+EraeQUXA1jMws/LaeuZTSY7/dinZWUl9xNYzzvCpZRP69eunzMxMff/991q7dq3OnTunyMhIJSYmavDgwerfv79dr09ISIhSUlI0efJkffTRR0pJSVFUVJSefvppJSUllRhoJk6cqNjYWL355pt67733FBgYqPbt22vKlClq3bq1w211pW4AAOCbfKqHqiKjhwoVAT1UMLPy6qH6XJ7pofqj6KFyhk/1UAEAgCvzxBwo5lA5j88MAADATfRQAQBgIuzl5x0EKgAATIRA5R0EKgAA4JbDhw9ryZIl+uKLL7Rz504dPXpUkZGRSkhI0NixY3XLLbd4rW0XLlzQpk2btG7dOqWnp+vEiRPKzc1V1apVdcMNN6hly5bq0KGDatas6VY9BCoAAEzEIvcnSDu77PTf/vY3vfLKK6pXr5569OihatWqac+ePfr444/18ccfa9GiRbr33nvdbJVzVq9erZkzZ+rjjz9WXl6eJKmkhQ2Klltq3Lixhg0bpsGDB6tq1apO18eyCT6CZRNQEbBsAsysvJZNWCMp1M2yciR1kuNt/fe//60bbrhBHTp0sDv+3XffqVu3bgoLC9ORI0dktVrdbNnVLV++XOPHj9eOHTtkGIYCAgLUvHlztW7dWtWrV1dkZKSCg4N16tQpnTp1Stu3b9emTZt07NgxSVJgYKAefvhhPf/887rhhhscrpdA5SMIVKgICFQws/IKVN/JM4GqgzzT1l69emnlypXatGmT4uPj3WzZlXXs2FGpqakKDg7WHXfcof79+6tXr14KCrr6L859+/Zp8eLFWrRokbZv366wsDB98MEH+tOf/uRQ3SybAAAAykylSpUkSQEBZT/LaOvWrXr++ed16NAhLVq0SH/6058cClOSVK9ePU2cOFFbt27VN998o1atWunnn392uG7mUAEAYCLX0lN+Bw8e1Ndff62oqCg1b97cQ6WWLj09XWFhYW6X06VLF3Xp0kXZ2dkO30OgAgDARDwZqLKysuyOW61Wh+dBXbhwQYMGDVJ+fr6mT58uf/+yX4zBE2HK1fIY8gMAACWKjo5WRESE7TVt2jSH7issLNSwYcO0du1ajRgxQoMGDSrjlnofPVQAAJiIJ/fyy8jIsJuU7kjvlGEYGjFihJKTkzVw4EC9//77brbGs86dO6fc3FxFRkbalkzwBAIVAAAm4skhv/DwcKee8issLNTw4cM1Z84cDRgwQHPnzpWfn/cGw7KysvTpp59q7dq1toU9i9akslgsioyMtC3s2bNnT7Vu3drlulg2wUewbAIqApZNgJmV17IJP8ozyya0lHNtvTxM3XfffVqwYEG5zJsqycaNG/Xuu+9q6dKlys3NLXFBz8sV9VQ1a9ZMw4cP10MPPaTKlSs7VSc9VAAAmIif3O+hcrZPqbCwUA899JDmzp2re+65R8nJyV4JU7t379b48eP18ccfyzAMVa1aVX379lWbNm2uuLDnxo0blZqaqvXr1+upp57SSy+9pEmTJmnEiBEO97ARqAAAMBFPzqFy1JQpUzR37lyFhoaqQYMGevHFF4tdc+edd6pFixZutuzKmjZtKkm677779OCDD6p79+6lBrtq1aqpWrVqatSoke666y5Jl/YkXLRokd577z099thj+u233zRhwgSH6iZQAQAAtxw4cECSlJOTo6lTp5Z4TWxsbJkHqsGDB2vChAmqV6+eS/fXrFlTzzzzjJ5++mktWLDAqUnrzKHyEcyhQkXAHCqYWXnNodomyd3VmLIlNVXZtdWM6KECAMBEvDHkBwIVAACmci1tPVOREKgAAIBprF271u0yOnbs6PQ9BCoAAEykovdQde7c2a0V0C0WiwoKCpy+j0AFAICJMIfqkurVqys4OLjc6iNQAQAAUzEMQzk5OerVq5cGDhyoLl26lHmdZgihAADg/xStlO7Oy5fDwZYtWzRmzBiFhoZqzpw56t69u2JiYjRhwgRt3769zOr15c8MAAD8jrthyhNzsLypefPmevXVV5WRkaGVK1dq4MCBOnPmjF5++WU1b95cLVu21BtvvKGjR496tF4CFQAAMB2LxaLu3btr3rx5Onr0qJKTk9WzZ09t3bpVY8aMUXR0tG699VYtWLBA586dc7s+AhUAACbi56GXmQQHB+v+++/Xl19+qUOHDun1119XixYttHLlSg0ePFj9+vVzuw4mpQMAYCIVfdmEq6lWrZoGDx6swMBAnThxQgcPHnRpmYTfI1ABAADTO3/+vD799FMlJyfrq6++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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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aYIaNWqovF4QBISHh2tVJ5MzIiIiY8LZmpLKycnBG2+8gZMnT0IURTx+/BiPHz8u83oplthgckZERGRMmJxJatGiRQgPD0ft2rUxdepUtG/fnuucEREREenKd999B3Nzc0RGRqJly5ZVUieTMyIiImPCpTQklZ6eDk9PzypLzAAmZ0RERMaF3ZqSat68OXJycqq0TubGREREpFMrV65Ev3794OLiAmtra9SuXRve3t5YvXo18vLyNC4vLCwM/v7+kMlksLOzg7+/P8LCwioU2zvvvIObN28iIiKiQvdXBJMzIiIiY2KAi9Bu3LgR6enp6Nu3L2bNmoWAgADk5+fj/fffR/fu3TVK0Hbt2oUBAwbg6tWrGDduHCZMmIC4uDgMGDAAu3bt0ji2yZMnY86cORg6dCjWrVtXJa1ogiiKYqXXQlrLysqCvb09MjMzIZPJdB0OUeUYypUvyXhlFQL2R1Bp/x1X/E78CshstCwrF7DvU3mxvig/Px9WVlaljo8dOxY7d+7E+vXr8e677760nPT0dDRu3BhmZma4ePEiXFxcAAD379+Hl5cX8vPzcfv2bTg4OKgdm3z7prt376K4uBgA4OTkVO46Z7du3VK7fFXYckZEREQ6pSoxA4Dhw4cDAG7evKlWOQcOHEBGRgZmzpypSMwAoF69epg9ezYyMjJw4MABjWJLSEhAQkICioqKIIoiRFFEamqq4riql7Y4IYCIiMiYmED7bkk9abo5evQoAKB169ZqXS8fF9avX79S5/r374/58+cjMjISU6dOVTuGO3fuqH2tVJicERERGRMDXkpjzZo1yMjIQEZGBqKjo3HhwgX069cPY8eOVev++Ph4AICHh0epc/Jj8mvU5ebmptH1UmByRkREZEwkXEojKytL6bClpSUsLS21LLxsa9asQWJiouL9mDFjsGHDBpibm6t1f2ZmJgDA3t6+1DkbGxuYmpoqrtFnetJwSURERPrGxcUF9vb2iteKFSvKvNbR0RGCIKj9UrU0RUJCAkRRxP3797F7925ERESgS5cuuHv3biV+Sv3DljMiIiJjImHLWXJystJszfJazQICApCdna12Fc7OzuWeCwgIQNOmTdG5c2e8//772Ldv30vLlLeYZWZmonbt2krncnNzUVxcrLJVTa5169b46KOPMHLkSK32zkxKSsLy5cvRqFEjfPjhhxrf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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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iMomC5leChMZGYmOHTtizZo1RR63evVqdOzYEdHR0Ua3Jfvp27p1K3r06IEaNWpg2rRpyMjIwMKFC3HlyhXs27cPQgi9k3gRERERyWHdunU4duwYWrduXeRxrVu3RkREBDZs2GB0W7JPC44YMQKSJKFLly5YsmQJmjRpotmXmpoqd3NERET0NE4L6nTq1Ck4OzvrzG/1tMaNG6NSpUov1shVp06dIEkSDh8+jDFjxmDZsmW4efOm3M0QERGRLhJMnxJUYM6fGzduwMvLS69jvby8cOPGDaPbkj24Onz4MP79918sXboUABAUFAQPDw907twZmzdvhiQp8BMjIiKiF5q1tTUePnyo17EPHz6EhYXxIVKxLFlzc3PDe++9h7/++guxsbGYMWMGEhMT8e6770IIgY8//hgHDhwA85cSERHJzNTnCsoxrfgCqlu3LhITE5GQkFDkcQkJCUhISICvr6/RbRX7/QD169fHkiVLcPXqVRw5cgRjxoxBdHQ0evbsCXd39+JunoiIqGxhcKXTgAEDIITAyJEjkZKSovOYlJQUjBo1CpIkYdCgQUa3ZZbH32RlZeHnn3/G1q1b8fPPP5d086USH39DZQEff0NKVmKPvwkBHGxMrCsTcAxR1uNvMjIy0KxZM1y8eBGurq5444030LJlS1SsWBEpKSk4deoUNmzYgDt37qBu3br4888/S/bZgufPn8elS5fg6uqKVq1aPff4kydP4t69e6hVqxbq169vVEfLOgZXVBYwuCIlY3BlftevX0e/fv1w5swZnWvAhRDw8/PDDz/8YNLsmsGpGB4/foyuXbsiOTkZR48e1auMEAIDBw5EtWrVcPHiRahUJj5RkoiIiHRjKoZCubu74/Tp0/jxxx/x888/Iy4uDmlpaahQoQIaNGiAvn37om/fviYtZgeMCK62b9+OW7duYeLEiWjTpo1eZdq0aYPx48dj9erV2LFjB0aNGmVwR4mIiEgPDK6KZGFhgYEDB2LgwIHF14ahBX766SdIkoQpU6YYVE59p+APP/xgaJNEREREpYbBI1d//fUXqlatirp16xpUrnbt2qhevTr++usvQ5skIiIifcnxbEAFPluwJBl8+pKTk1G9enWjGqtWrRqSk5ONKktERER6sIDpaRhKeXDVsGFDfPvttybn07x27RomTpyIjz/+2KByBp8+GxsbZGRkGFoMwJPbIK2trY0qS0RERKSPhw8f4vXXX4evry8++ugjJCYm6l02Ozsbu3btwsCBA1G7dm2sW7cOrq6uBrVv8LRg1apVcenSJWRlZRl0119WVhYuXboEDw8PQ5skIiIifXFaEAkJCVixYgWWLFmC4OBghISEwMfHBy1atECzZs1QtWpVODs7Q6VSISUlBffv30dcXBxiYmIQExOD9PR0CCHQpUsXfPzxx2jSpIlB7RscXLVt2xbr16/Hzp07MWzYML3Lff/998jIyEDbtm0NbZKIiIj0xbsFoVKpEBQUhIkTJ2LLli1Yu3Ytzp49i6SkJGzfvl1nGfUUop2dHcaOHYsJEyagefPmRrVvcBLREydOICAgANWqVcPJkyf1SrJ17do1tGrVCnfu3EFkZCT8/f2N6mxZxiSiVBYwiSgpWYklEf0McDAusfj/6soAHKcpK4loYmIiIiMjceLECVy9ehXJycnIzMyEs7MzXF1d0aRJEwQEBKBNmzYoX768SW0ZPHLVpk0bDBo0CN9//z1atmyJL774AgMGDNCZcCs/Px87d+7Eu+++izt37mDAgAEMrIiIiIoTR650ql27NmrXro033ij+v+IMDq4AYNOmTbhx4wZOnDiBIUOGoHLlyvD390fNmjVhZ2eH9PR0XL58GSdOnMDdu3chhEDr1q2xadMmmbtPREREWrjmyuyMCq5sbW0RERGBkJAQrFy5Enfv3sWuXbu0ntOjnm20t7fH5MmTERISAisrK3l6TURERLpx5MrsjAquAKBcuXJYsGABZs6cib179+LEiRO4ceMGHj58iAoVKqB69epo06YNevbsCUdHRzn7TERERKS3e/fu4eeff8bvv/+OxMREPHjwABkZGbC1tYWTkxNq166Nli1bok+fPganXdDF4AXtZB5c0E5lARe0k5KV2IL2NTItaH+z9C9oz8zMxMyZM/H1118jJyenyKSikiTBysoK48ePx9KlS2Fra/xJNHrkioiIiF5A6gztptZRymVlZaF9+/b4448/IIRA3bp14e/vD29vbzg5OUGlUiErKwsPHjzAP//8g+joaMTHx+Orr77C6dOncfz4caMTnzO4IiIiIsX55JNPcPr0adSpUwcbNmxA69atn1vmxIkTGDt2LGJiYrB06VLMnTvXqLYVEJsSERGRhqnPFZRjQfwLYPv27bC2tsahQ4f0CqyAJ+mmDh48iHLlymHbtm1Gt83gioiISEksZHoZyMvLC5Ik6XxNnDhR73ry8/MRFhaGRo0awdbWFpUrV8bgwYMNej4gAFy+fBkNGzbUK9n50zw9PdGwYUNcuXLFoHJP47QgERERycLR0RHvvvtuge1+fn561zFx4kSsXbsW9evXx+TJk3Hnzh18++23OHToEE6cOIH69evrVY+9vT3u3r2rd7tPu3v3Luzs7IwqCzC4IiIiUhYz5rmqWLEiQkJCjG726NGjWLt2Ldq2bYvDhw9DpVIBAEaOHIkuXbpg0qRJOHbsmF51tW7dGnv27MHy5csxffp0vfvw6aef4saNG+jdu7dR7wHgtCAREZGylOI1V2vXrgUALFiwQBNYAUCnTp3QrVs3REZGIiEhQa+6Zs2aBQsLCwQFBaFnz57YuXMnbt26pfPYW7duYefOnejRowfef/99WFpaYvbs2Ua/D45cERERkSyysrKwefNm3LhxA05OTmjTpg0aN26sd/mIiAjY2dnpfA5xt27dcODAARw7dgy+vr7PrUv92L1x48bhwIEDOHjwIABApVKhYsWKsLa2RnZ2NlJSUpCVlQXgydNlrK2tsXbtWrRq1Urvfj+LwRUREZGSyPhswbS0NK3NKpVKa0TpWbdv38bo0aO1tnXv3h3h4eFwcXEpssn09HTcunULDRs2hKVlwaGz2rVrA4BBC9uHDRuGgIAALF26FD/99BNu3bqFzMxM3L59u8Cxbm5u6NevH4KCguDl5aV3G7owuCIiIlISGddcPXunXXBwcKFrqsaOHYvAwEA0aNAAKpUKFy5cQGhoKPbv348+ffogOjpa6xnEz0pNTQWAQh+Zp84Urz5OX56envjyyy/x5Zdf4tq1a5rH32RmZsLGxkbz+BsPDw+D6i0KgysiIiIlkWD6yNV/Y6Dr169rPf6mqFGrefPmaf27ZcuW2LNnDwIDAxEVFYV9+/bhlVdeMbFjpvHw8JA1iCoMF7QTERGRTg4ODlqvooIrXSwsLDBmzBgAQHR0dJHHqkesChuZUk9RFjay9SLhyBUREZGSmDEVgy7qtVaPHz8u8jg7OztUrVoVly9fRl5eXoF1V+q1Vuq1V8Xpxo0byMvLM3qUiyNXRERESvKCpWL4/fffAUCvReKBgYFIT0/XOcqlvtsvMDBQvs4VokmTJvD29ja6PIMrIiIiMsmFCxeQkpJSYHtUVBSWL18OlUqF/v37a7YnJycjPj4eycnJWsdPmDABADB37lxkZ2drth85cgQHDx5Eu3bt9ErDIAchhNFlGVwREREpiRmeLfjdd9+hWrVq6N27NyZPnowZM2age/fuaNeuHXJychAWFqY1xRYWFoZ69eohLCxMq54OHTpg3LhxOH78OF5++WXMnDkTo0aNwiuvvAIHBwesWrXKiBNS8rjmioiISEnMsOaqQ4cOiIuLw5kzZ3Ds2DFkZmaiSpUqeO211zBt2jS0aNFC77rWrFmDRo0aYc2aNVixYgXs7e3Ru3dvLFy40KBRq0WLFhn2Jp6SkZFhdFkAkIQp415UYtLS0uDo6AhbaO6QJVKc9DfM3QOi4pOWDTiGP7kb7un0BrLV/9/fE6n7AQfjnzn8pK50wLFH8fW1JFhYWBSZV6soQghIkoS8vDyjynPkioiISElesLsFzcXS0hL5+fno378/7O3tDSq7Y8cOrTVfhmJwRUREpCQyPv6mNGvQoAH+/vtvjB8/Hl27djWo7J49e3D//n2j21bA6SMiIiLSpl7nFRMTU+JtM7giIiJSEguYnuNKAdFBixYtIITQ5NkyhKnL0TktSEREpCScFgQAdO7cGVOnTtVkiDfEL7/8gpycHKPbZnBFRESkJFzQDuBJRvjPPvvMqLJt2rQxqW0FxKZERERELw6OXBERESkJR67MjsEVERGRknDNldkxuCIiIiLFs7TUfzjOwsICFSpUgJeXFwICAjBu3Dg0atRI//LGdJCIiIheUKamYZBjWvEFJITQ+5WXl4eUlBScPXsWYWFhaNasGT755BO922JwRUREpCQMrnTKz8/H8uXLoVKpMGrUKEREROD+/fvIycnB/fv3cezYMYwePRoqlQrLly/Ho0ePEBMTg7feegtCCMyaNQtHjhzRqy1OCxIREZHi/fDDD3jvvfcQFhaGSZMmae2rWLEi2rZti7Zt26J58+Z45513UL16dQwaNAhNmzaFt7c3ZsyYgbCwMHTq1Om5bUnC1DSkVCLUTzu3BWDcM76JXnzpb5i7B0TFJy0bcAwHUlNT4eDgIH/9//09kfoX4FDBxLoeAo4vF19fzaF169a4fv06/v333+ceW6NGDdSoUQOnTp0CAOTm5sLFxQW2tra4devWc8tzWpCIiEhJOC2oU2xsLKpXr67XsdWrV8eFCxc0/y5Xrhx8fX31fpgzgysiIiJSPCsrKyQkJCArK6vI47KyspCQkIBy5bRXTqWlpaFCBf2GBBlcERERKYmFTC+F8ff3R1paGt555x3k5+frPEYIgcmTJyM1NRUBAQGa7dnZ2bh8+TKqVaumV1tc0E5ERKQkzNCu0/z58/Hrr79iw4YNOHHiBEaMGIFGjRqhQoUKePToEf7v//4PW7ZswYULF6BSqTB//nxN2V27diEnJwcdOnTQqy0GV0RERErC4Eqnl19+Gbt378aIESMQFxeHOXPmFDhGCAE3NzeEh4ejSZMmmu1VqlTBxo0b0bZtW73aYnBFREREZULnzp2RmJiIbdu24fDhw0hMTER6ejrs7Ozg6+uLLl26YOjQobC3t9cq1759e4PaYXBFRESkJHy2YJHs7e0xYcIETJgwodjaYHBFRESkJJwWNDsGV0RERFSmXL58GYcPH0ZCQgIePnyIChUqaKYFa9asaXL9DK6IiIiUxAKmjzwpdFrwwYMHeOutt/D9999D/YAaIQQk6cmzTyRJwmuvvYawsDA4OTkZ3Q6DKyIiIiXhmiudMjIy0KlTJ5w7dw5CCLRu3RoNGjRAlSpVcOfOHZw/fx4nT57Ejh07EB8fj+joaNjY2BjVFoMrIiIiUrzPPvsMZ8+eRd26dfHNN9/Az8+vwDExMTEYNWoUzp49i88//xyzZs0yqi0FxqZERERlGJ8tqNN3330HS0tL7NmzR2dgBQB+fn745ZdfYGFhgR07dhjdFkeuiIiIlITTgjolJSWhYcOG8Pb2LvI4Hx8fNGzYEImJiUa3VapO36ZNmyBJUpGvTp06aZVJS0vD9OnT4enpCZVKBU9PT0yfPh1paWmFtrNt2za0aNECdnZ2cHJyQs+ePRETE2Nwf41pm4iIiORnaWmJnJwcvY7NycmBhYXxIVKpGrlq0qQJgoODde7buXMnzp8/j27dumm2paenIzAwEGfPntVkXT137hw+++wzHD16FFFRUbCzs9OqZ9GiRZgzZw48PDwwceJEPHr0CDt27IC/vz8OHjyod5ZWY9omIiIyGfNc6VSnTh38+eefOHfuHBo3blzocWfPnsWFCxfQvHlzo9sqdcHV08/6UcvOzkZYWBjKlSuHUaNGabYvXboUZ8+excyZM/Hxxx9rtgcHB2P+/PlYunQpQkNDNdsTExMRHBwMX19fnD59Go6OjgCAKVOmoEWLFhg3bhzi4+NRrtzzT5uhbRMREcmCwZVOI0aMQExMDHr16oWvvvoKvXv3LnDML7/8gnfeeQeSJGHEiBFGtyUJdaKHUuzbb7/FkCFD0LdvX+zatQvAk7wVNWrUQFpaGm7fvq01SpSZmYlq1aqhfPnyuH79uia/xQcffIDFixdj8+bNGDlypFYbkyZNwurVq3Hw4EF07dq1yP4Y0/bzpKWlwdHREbYA9CtBVPqkv2HuHhAVn7RswDEcSE1NhYODg/z1//f3RGoKYGr1aWmAY8Xi66s55Obmolu3bjh69CgkSYKHhwfq1q0LV1dX3L17F3Fxcbh+/TqEEOjYsSMOHjwIS0vjosxSteaqMOvXrwcAjBs3TrMtMTERN2/ehL+/f4HpNxsbG7Rr1w43btxAUlKSZntERAQA6Aye1NONx44de25/jGmbiIiIik+5cuWwd+9eTJ8+Hba2trh69SoOHjyI8PBwHDx4ENeuXYOtrS3ee+897Nmzx+jACihl04K6XL16FUeOHEH16tXRvXt3zXb1Kv/atWvrLKfenpiYqPX/9vb2cHNzK/L45zGm7WdlZWUhKytL828ugiciIr1IFoCesyKF1yEA5MvSnReJjY0NPv30UwQHByMqKgoJCQl49OgR7O3t4evri4CAAFSoUMHkdkp9cLVx40bk5+djzJgxWlFmamoqAGjWTT1LPcypPk79/66urnofXxhj2n7W4sWLuSaLiIiMUA6mLyARALJl6MuLqUKFCujRowd69OhRLPWX6uAqPz8fGzduhCRJGDt2rLm7I6vZs2dj+vTpmn+npaXB3d3djD0iIiIqHa5duyZLPR4eHkaVK9XB1eHDh3Ht2jV06tSpwFOs1aNGhY0OqafZnh5dcnR0NOj4whjT9rNUKhVUKtVz2yIiItLGkSsvLy+9bxgrjCRJyM3NNapsqQ6udC1kV3veGild66Jq166NkydP4vbt2wXWXT1vHZWpbRMREclDruCq9PLw8DA5uDJFqQ2u/vOf/+Dnn3+Gs7Mz+vXrV2B/7dq1Ua1aNURHRyM9Pb1AOoTIyEhUq1YNtWrV0mwPDAzEyZMncejQoQKpGA4ePKg55nmMaZuIiIjkceXKFbO2X2pTMYSHhyM7OxvDhw/XOX0mSRLGjRuHR48eYf78+Vr7Fi9ejAcPHmDcuHFake2YMWNQrlw5LFy4UGtK7/z58/jmm2/g4+ODjh07atV17do1xMfH4/Hjxya1TUREJA9LPBk7MeWlwCyiJajUJhF96aWXEBsbi//7v//DSy+9pPOY9PR0BAQEaB5B06xZM5w7dw779+9HkyZNdD6CZuHChZg7dy48PDwwcOBApKenY/v27cjIyMDBgwfRoUMHrePbt2+PY8eO4ejRo1qPxjGm7aIwiSiVBUwiSkpWYklEUyvDwcG0sZO0tHw4Ot5TVBLRklQqR65Onz6N2NhYtGjRotDACgDs7OwQERGBadOmIT4+HsuWLUNsbCymTZuGiIgIncHNnDlzsGXLFri6umLVqlXYsWMH2rRpg+jo6AKBVVGMaZuIiIhKv1I7clXWcOSKygKOXJGSldzIVVWZRq5uceTKSKV2QTsRERHpUg6mT0wpLzt7SWJwRUREpCiWMD244hyJKUrlmisiIiKiFxVHroiIiBTFEqanUsiToyNlFoMrIiIiRZEjTxWnBU3BaUEiIiIiGXHkioiISFE4cmVuDK6IiIgUhcGVuXFakIiIiEhGHLkiIiJSFI5cmRtHroiIiBTFEk8CLFNepgZnwNKlSyFJEiRJwqlTp/QuFxERoSmn62VIXebCkSsiIiKSVVxcHObNmwc7Ozukp6cbVUdgYCDat29fYHuNGjVM7F3xY3BFRESkKOrRJ/PIy8vDqFGj0LhxY/j6+mLLli1G1dO+fXuEhITI27kSwmlBIiIiRTF1StC04Ozjjz/GuXPnsGHDBlhamj69WBpx5IqIiEhRzDdyFRsbi9DQUMydOxcNGjQwqa7ExESsWLECjx8/hqenJ7p06QIXFxeZelq8GFwRERGRTmlpaVr/VqlUUKlUOo/Nzc3F6NGjUa9ePcyaNcvktrdt24Zt27Zp/m1ra4vQ0FAEBQWZXHdx47QgERGRosh3t6C7uzscHR01r8WLFxfa6qJFizTTgVZWVkb3vnLlyvjkk08QFxeH9PR03LhxA1u2bIGzszNmzpyJNWvWGF13SeHIFRERkaLIMS0oAADXr1+Hg4ODZmtho1bnzp3DggULMGPGDDRt2tSklhs0aKA1pVi+fHkMGzYMjRs3RrNmzRAcHIzx48fDwuLFHR96cXtGREREZuXg4KD1Kiy4GjVqFHx8fIr17r6GDRuiZcuWuHPnDpKSkoqtHTlw5IqIiEhR5Bu50te5c+cAADY2Njr3t27dGgCwa9cu9O3b1+heqRe0P3782Og6SgKDKyIiIkUp+eDqjTfe0Lk9MjISiYmJ6NOnDypXrgwvLy+je5Sbm4szZ85AkiR4eHgYXU9JYHBFREREJlm3bp3O7aNHj0ZiYiJmz56NVq1aae1LTk5GcnIyXFxctFIsnDx5Eq1atYIk/e/5hrm5uQgKCsLVq1fRvXt3ODs7F88bkQmDKyIiIkUp+ZErY4SFhSE0NBTBwcFaa7WGDh0KSZLQpk0bVK9eHSkpKYiMjMTFixfh4eGB1atXF3vfTMXgioiISFHUqRhMkS9HR4wyadIkHDhwABEREUhOTka5cuVQq1YtzJkzB++99x6cnJzM1jd9SUKI4g9PyWRpaWlwdHSELQDpuUcTlU7pupdtEClCWjbgGA6kpqZqpTeQrf7//p5ITR0CBwdrE+vKhqPjjmLrq9Jx5IqIiEhRLKFOAmpaHWQsBldERESKIseaK/NNCyoBgysiIiJFYXBlbszQTkRERCQjjlwREREpCkeuzI3BFRERkaLIkYohT46OlFmcFiQiIiKSEUeuiIiIFEWOaUGOXJmCwRUREZGiMLgyN04LEhEREcmII1dERESKwpErc2NwRUREpChy3C2YK0dHyixOCxIRERHJiCNXREREiiLHtCDDA1Pw7BERESkKgytz49kjIiJSFAZX5sY1V0REREQyYmhKRESkKBy5MjeePSIiIkWRIxWDpRwdKbM4LUhEREQkI45cERERKQqnBc2NZ4+IiEhRGFyZG6cFiYiIiGTE0JSIiEhRLGH6gnQuaDcFgysiIiJF4d2C5sZpQSIiIiIZceSKiIhIUbig3dx49oiIiBSFwZW58ewREREpCoMrc+OaKyIiIiIZMTQlIiJSFI5cmRvPHhERkaIwFYO5cVqQiIiISEYcuSIiIlIUTguaG88eERGRojC4MjdOCxIRERHJiKEpERGRonDkytx49oiIiBSFwZW5cVqQiIiISEYMTYmIiBSFea7MjcEVERGRonBa0Nx49oiIiBSFwZW5cc0VERERkYwYmhIRESkKR67MjWePiIhIUbig3dw4LUhEREQkI45cERERKYolTB954siVKRhcERERKQrXXJkbpwWJiIiIZMTQlIiISFE4cmVuPHtERESKwuDK3DgtSERERCQjhqZERESKwjxX5sbgioiISFE4LWhuPHtERESKwuDK3LjmioiIiEhGDE2JiIgUhSNX5sazR0REpCgMrsyNZ6+UEEI8+a+Z+0FUnNKyzd0DouKjvr7V3+fF1k5a2gtRR1nG4KqUePjwIQAg08z9ICpOjuHm7gFR8Xv48CEcHR1lr9fa2hpubm5wd3eXpT43NzdYW1vLUldZI4niDqFJFvn5+bh58yYqVKgASZLM3R3FS0tLg7u7O65fvw4HBwdzd4dIdrzGS54QAg8fPkS1atVgYVE895NlZmYiO1ueIWBra2vY2NjIUldZw5GrUsLCwgI1atQwdzfKHAcHB/7iIUXjNV6yimPE6mk2NjYMiF4ATMVAREREJCMGV0REREQyYnBFpINKpUJwcDBUKpW5u0JULHiNExUfLmgnIiIikhFHroiIiIhkxOCKiIiISEYMroiIiIhkxOCKiIiISEYMrqhM2LJlC9588034+flBpVJBkiRs2rTJ4Hry8/MRFhaGRo0awdbWFpUrV8bgwYORmJgof6eJDODl5QVJknS+Jk6cqHc9vMaJTMcM7VQmzJ07F1evXoWLiwuqVq2Kq1evGlXPxIkTsXbtWtSvXx+TJ0/GnTt38O233+LQoUM4ceIE6tevL3PPifTn6OiId999t8B2Pz8/vevgNU4kA0FUBhw+fFhcuXJFCCHE4sWLBQCxceNGg+r47bffBADRtm1bkZmZqdn+66+/CkmSRLt27eTsMpFBPD09haenp0l18BonkgenBalM6Ny5Mzw9PU2qY+3atQCABQsWaCVe7NSpE7p164bIyEgkJCSY1AaROfEaJ5IHgysiPUVERMDOzg7+/v4F9nXr1g0AcOzYsZLuFpFGVlYWNm/ejEWLFmHVqlU4d+6cQeV5jRPJg2uuiPSQnp6OW7duoWHDhrC0tCywv3bt2gDARb9kVrdv38bo0aO1tnXv3h3h4eFwcXEpsiyvcSL5cOSKSA+pqakAniwY1sXBwUHrOKKSNnbsWERERODevXtIS0vDqVOn0KNHDxw4cAB9+vSBeM6TzniNE8mHI1dERAowb948rX+3bNkSe/bsQWBgIKKiorBv3z688sorZuodUdnCkSsiPaj/mi/sr/a0tDSt44heBBYWFhgzZgwAIDo6ushjeY0TyYfBFZEe7OzsULVqVVy+fBl5eXkF9qvXoajXpRC9KNRrrR4/flzkcbzGieTD4IpIT4GBgUhPT9c5AnDw4EHNMUQvkt9//x3Akwzuz8NrnEgeDK6InpGcnIz4+HgkJydrbZ8wYQKAJ9nes7OzNduPHDmCgwcPol27dvD19S3RvhIBwIULF5CSklJge1RUFJYvXw6VSoX+/ftrtvMaJypeknjeLSRECrBu3TpERUUBAP7++2+cOXMG/v7+qFWrFgCgb9++6Nu3LwAgJCQEoaGhCA4ORkhIiFY948ePx7p161C/fn288sormkeD2NjY8NEgZDYhISFYunQpOnXqBC8vL6hUKsTGxuLQoUOwsLDA6tWrMW7cOK3jeY0TFR/eLUhlQlRUFDZv3qy1LTo6WjP94eXlpQmuirJmzRo0atQIa9aswYoVK2Bvb4/evXtj4cKF/IuezKZDhw6Ii4vDmTNncOzYMWRmZqJKlSp47bXXMG3aNLRo0ULvuniNE5mOI1dEREREMuKaKyIiIiIZMbgiIiIikhGDKyIiIiIZMbgiIiIikhGDKyIiIiIZMbgiIiIikhGDKyIiIiIZMbgiIiIikhGDKyIyi4iICEiSpPXatGmTbPX37dtXq259HlxMRCQHBldEVKRnAyB9Xu3bt9e7fgcHB/j7+8Pf3x9VqlTR2rdp06bnBkabN2+GpaUlJEnC0qVLNdvr168Pf39/+Pn5GfqWiYhMwmcLElGR/P39C2xLTU1FbGxsoftfeuklvet/+eWXERERYVTfNmzYgPHjxyM/Px/Lli3D9OnTNfsWLVoEALhy5Qpq1qxpVP1ERMZgcEVERYqKiiqwLSIiAh06dCh0f0lYt24dJkyYACEEvvjiC0yZMsUs/SAiehaDKyIqddasWYNJkyYBAL788ku89dZbZu4REdH/MLgiolJl1apVePvttzX//+abb5q5R0RE2rignYhKjbCwMM0o1dq1axlYEdELicEVEZUKK1aswOTJk2FhYYENGzbgjTfeMHeXiIh04rQgEb3wbty4galTp0KSJGzevBnDhw83d5eIiArFkSsieuEJITT//ffff83cGyKiojG4IqIXXo0aNTR5q2bPno0vv/zSzD0iIiocgysiKhVmz56N2bNnAwAmT54s66NyiIjkxOCKiEqNRYsWYfLkyRBCYNy4cdi5c6e5u0REVACDKyIqVb744guMGTMGeXl5eP3117Fv3z5zd4mISAuDKyIqVSRJwrp16zB48GDk5ORgwIABOHr0qLm7RUSkweCKiEodCwsLbNmyBb169UJmZib69OmDU6dOmbtbREQAGFwRUSllZWWF77//Hh07dsSjR4/Qs2dPnDt3ztzdIiJicEVEpZeNjQ1++eUXtG7dGg8ePEDXrl0RHx9v7m4RURnHDO1EZLD27dtrEnsWp9GjR2P06NFFHmNnZ4cTJ04Ue1+IiPTF4IqIzOqvv/5CQEAAAGDOnDno0aOHLPV+8MEHiIyMRFZWliz1ERHpi8EVEZlVWloaoqOjAQB37tyRrd4LFy5o6iUiKkmSKImxfSIiIqIyggvaiYiIiGTE4IqIiIhIRgyuiIiIiGTE4IqIiIhIRgyuiIiIiGTE4IqIiIhIRgyuiIiIiGTE4IqIiIhIRgyuiIiIiGTE4IqIiIhIRgyuiIiIiGT0/+FePadvrsQHAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + " ### 3 design variable example\n", + "# Define design ranges\n", + "design_ranges = {\n", + " \"CA0[0]\": list(np.linspace(1, 5, 2)),\n", + " \"T[0]\": list(np.linspace(300, 700, 2)),\n", + " (\n", + " \"T[0.125]\",\n", + " \"T[0.25]\",\n", + " \"T[0.375]\",\n", + " \"T[0.5]\",\n", + " \"T[0.625]\",\n", + " \"T[0.75]\",\n", + " \"T[0.875]\",\n", + " \"T[1]\",\n", + " ): [300, 500],\n", + "}\n", + "\n", + "sensi_opt = \"direct_kaug\"\n", + "\n", + "doe_object = DesignOfExperiments(\n", + " parameter_dict, # parameter dictionary\n", + " exp_design, # design variables\n", + " measurements, # measurement variables\n", + " create_model, # model function\n", + " prior_FIM = prior_pass, \n", + " discretize_model=disc_for_measure, # discretization function\n", + ")\n", + "# run the grid search for 3 dimensional case\n", + "all_fim = doe_object.run_grid_search(design_ranges, mode=sensi_opt)\n", + "\n", + "all_fim.extract_criteria()\n", + "\n", + "# see the criteria values\n", + "all_fim.store_all_results_dataframe\n", + "\n", + "\n", + "\n", + "fixed = {\"('T[0.125]', 'T[0.25]', 'T[0.375]', 'T[0.5]', 'T[0.625]', 'T[0.75]', 'T[0.875]','T[1]')\": 300}\n", + "\n", + "all_fim.figure_drawing(\n", + " fixed, \n", + " [\"CA0[0]\",\"T[0]\"],\n", + " \"Reactor\", \n", + " \"T [K]\", \n", + " \"$C_{A0}$ [M]\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10c928cf-bc40-4a62-a176-3e3eb19ec78e", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyomo/contrib/doe/examples/debugging_compute_FIM.ipynb b/pyomo/contrib/doe/examples/debugging_compute_FIM.ipynb new file mode 100644 index 00000000000..1c2f5383e58 --- /dev/null +++ b/pyomo/contrib/doe/examples/debugging_compute_FIM.ipynb @@ -0,0 +1,10011 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "59659c7f-5ced-42da-b7dd-de22f614db9f", + "metadata": {}, + "source": [ + "# Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "f875a1ff-c70d-4f94-a65c-495fbf8faa0c", + "metadata": {}, + "outputs": [], + "source": [ + "import pyomo.environ as pyo\n", + "from pyomo.dae import ContinuousSet, DerivativeVar\n", + "from pyomo.contrib.doe import DesignOfExperiments, MeasurementVariables, DesignVariables, ModelOptionLib\n", + "import copy\n", + "import numpy as np\n", + "from random import sample\n", + "from matplotlib import pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "id": "3b4d475b-bcf6-4e73-ba3b-0393bb65da30", + "metadata": {}, + "source": [ + "## Model function" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7cf05f64-c3b1-4905-b5c1-b54dcb3aacea", + "metadata": {}, + "outputs": [], + "source": [ + "def create_model(\n", + " mod=None,\n", + " model_option=\"stage2\",\n", + " control_time=[0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1],\n", + " control_val=None,\n", + " t_range=[0.0, 1],\n", + " CA_init=1,\n", + " C_init=0.1,\n", + "):\n", + " \"\"\"\n", + " This is an example user model provided to DoE library.\n", + " It is a dynamic problem solved by Pyomo.DAE.\n", + "\n", + " Arguments\n", + " ---------\n", + " mod: Pyomo model. If None, a Pyomo concrete model is created\n", + " model_option: choose from the 3 options in model_option\n", + " if ModelOptionLib.parmest, create a process model.\n", + " if ModelOptionLib.stage1, create the global model.\n", + " if ModelOptionLib.stage2, add model variables and constraints for block.\n", + " control_time: a list of control timepoints\n", + " control_val: control design variable values T at corresponding timepoints\n", + " t_range: time range, h\n", + " CA_init: time-independent design (control) variable, an initial value for CA\n", + " C_init: An initial value for C\n", + "\n", + " Return\n", + " ------\n", + " m: a Pyomo.DAE model\n", + " \"\"\"\n", + "\n", + " theta = {\"A1\": 84.79, \"A2\": 371.72, \"E1\": 7.78, \"E2\": 15.05}\n", + "\n", + " model_option = ModelOptionLib(model_option)\n", + "\n", + " if model_option == ModelOptionLib.parmest:\n", + " mod = pyo.ConcreteModel()\n", + " return_m = True\n", + " elif model_option == ModelOptionLib.stage1 or model_option == ModelOptionLib.stage2:\n", + " if not mod:\n", + " raise ValueError(\n", + " \"If model option is stage1 or stage2, a created model needs to be provided.\"\n", + " )\n", + " return_m = False\n", + " else:\n", + " raise ValueError(\n", + " \"model_option needs to be defined as parmest,stage1, or stage2.\"\n", + " )\n", + "\n", + " if not control_val:\n", + " control_val = [300] * 9\n", + "\n", + " controls = {}\n", + " for i, t in enumerate(control_time):\n", + " controls[t] = control_val[i]\n", + "\n", + " mod.t0 = pyo.Set(initialize=[0])\n", + " mod.t_con = pyo.Set(initialize=control_time)\n", + " mod.CA0 = pyo.Var(\n", + " mod.t0, initialize=CA_init, bounds=(1.0, 5.0), within=pyo.NonNegativeReals\n", + " ) # mol/L\n", + "\n", + " # check if control_time is in time range\n", + " assert (\n", + " control_time[0] >= t_range[0] and control_time[-1] <= t_range[1]\n", + " ), \"control time is outside time range.\"\n", + "\n", + " if model_option == ModelOptionLib.stage1:\n", + " mod.T = pyo.Var(\n", + " mod.t_con,\n", + " initialize=controls,\n", + " bounds=(300, 700),\n", + " within=pyo.NonNegativeReals,\n", + " )\n", + " return\n", + "\n", + " else:\n", + " para_list = [\"A1\", \"A2\", \"E1\", \"E2\"]\n", + "\n", + " ### Add variables\n", + " mod.CA_init = CA_init\n", + " mod.para_list = para_list\n", + "\n", + " # timepoints\n", + " mod.t = ContinuousSet(bounds=t_range, initialize=control_time)\n", + "\n", + " # time-dependent design variable, initialized with the first control value\n", + " def T_initial(m, t):\n", + " if t in m.t_con:\n", + " return controls[t]\n", + " else:\n", + " # count how many control points are before the current t;\n", + " # locate the nearest neighbouring control point before this t\n", + " neighbour_t = max(tc for tc in control_time if tc < t)\n", + " return controls[neighbour_t]\n", + "\n", + " mod.T = pyo.Var(\n", + " mod.t, initialize=T_initial, bounds=(300, 700), within=pyo.NonNegativeReals\n", + " )\n", + "\n", + " mod.R = 8.31446261815324 # J / K / mole\n", + "\n", + " # Define parameters as Param\n", + " mod.A1 = pyo.Var(initialize=theta[\"A1\"])\n", + " mod.A2 = pyo.Var(initialize=theta[\"A2\"])\n", + " mod.E1 = pyo.Var(initialize=theta[\"E1\"])\n", + " mod.E2 = pyo.Var(initialize=theta[\"E2\"])\n", + "\n", + " # Concentration variables under perturbation\n", + " mod.C_set = pyo.Set(initialize=[\"CA\", \"CB\", \"CC\"])\n", + " mod.C = pyo.Var(\n", + " mod.C_set, mod.t, initialize=C_init, within=pyo.NonNegativeReals\n", + " )\n", + "\n", + " # time derivative of C\n", + " mod.dCdt = DerivativeVar(mod.C, wrt=mod.t)\n", + "\n", + " # kinetic parameters\n", + " def kp1_init(m, t):\n", + " return m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t]))\n", + "\n", + " def kp2_init(m, t):\n", + " return m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t]))\n", + "\n", + " mod.kp1 = pyo.Var(mod.t, initialize=kp1_init)\n", + " mod.kp2 = pyo.Var(mod.t, initialize=kp2_init)\n", + "\n", + " def T_control(m, t):\n", + " \"\"\"\n", + " T at interval timepoint equal to the T of the control time point at the beginning of this interval\n", + " Count how many control points are before the current t;\n", + " locate the nearest neighbouring control point before this t\n", + " \"\"\"\n", + " if t in m.t_con:\n", + " return pyo.Constraint.Skip\n", + " else:\n", + " neighbour_t = max(tc for tc in control_time if tc < t)\n", + " return m.T[t] == m.T[neighbour_t]\n", + "\n", + " def cal_kp1(m, t):\n", + " \"\"\"\n", + " Create the perturbation parameter sets\n", + " m: model\n", + " t: time\n", + " \"\"\"\n", + " # LHS: 1/h\n", + " # RHS: 1/h*(kJ/mol *1000J/kJ / (J/mol/K) / K)\n", + " return m.kp1[t] == m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t]))\n", + "\n", + " def cal_kp2(m, t):\n", + " \"\"\"\n", + " Create the perturbation parameter sets\n", + " m: model\n", + " t: time\n", + " \"\"\"\n", + " # LHS: 1/h\n", + " # RHS: 1/h*(kJ/mol *1000J/kJ / (J/mol/K) / K)\n", + " return m.kp2[t] == m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t]))\n", + "\n", + " def dCdt_control(m, y, t):\n", + " \"\"\"\n", + " Calculate CA in Jacobian matrix analytically\n", + " y: CA, CB, CC\n", + " t: timepoints\n", + " \"\"\"\n", + " if y == \"CA\":\n", + " return m.dCdt[y, t] == -m.kp1[t] * m.C[\"CA\", t]\n", + " elif y == \"CB\":\n", + " return m.dCdt[y, t] == m.kp1[t] * m.C[\"CA\", t] - m.kp2[t] * m.C[\"CB\", t]\n", + " elif y == \"CC\":\n", + " return pyo.Constraint.Skip\n", + "\n", + " def alge(m, t):\n", + " \"\"\"\n", + " The algebraic equation for mole balance\n", + " z: m.pert\n", + " t: time\n", + " \"\"\"\n", + " return m.C[\"CA\", t] + m.C[\"CB\", t] + m.C[\"CC\", t] == m.CA0[0]\n", + "\n", + " # Control time\n", + " mod.T_rule = pyo.Constraint(mod.t, rule=T_control)\n", + "\n", + " # calculating C, Jacobian, FIM\n", + " mod.k1_pert_rule = pyo.Constraint(mod.t, rule=cal_kp1)\n", + " mod.k2_pert_rule = pyo.Constraint(mod.t, rule=cal_kp2)\n", + " mod.dCdt_rule = pyo.Constraint(mod.C_set, mod.t, rule=dCdt_control)\n", + "\n", + " mod.alge_rule = pyo.Constraint(mod.t, rule=alge)\n", + "\n", + " # B.C.\n", + " mod.C[\"CB\", 0.0].fix(0.0)\n", + " mod.C[\"CC\", 0.0].fix(0.0)\n", + "\n", + " if return_m:\n", + " return mod\n", + "\n", + "\n", + "def disc_for_measure(m, nfe=32, block=True):\n", + " \"\"\"Pyomo.DAE discretization\n", + "\n", + " Arguments\n", + " ---------\n", + " m: Pyomo model\n", + " nfe: number of finite elements b\n", + " block: if True, the input model has blocks\n", + " \"\"\"\n", + " discretizer = pyo.TransformationFactory(\"dae.collocation\")\n", + " if block:\n", + " for s in range(len(m.block)):\n", + " discretizer.apply_to(m.block[s], nfe=nfe, ncp=3, wrt=m.block[s].t)\n", + " else:\n", + " discretizer.apply_to(m, nfe=nfe, ncp=3, wrt=m.t)\n", + " return m" + ] + }, + { + "cell_type": "markdown", + "id": "ac9e76cb-cbf5-44fb-aa53-bbb5dca1bcb0", + "metadata": {}, + "source": [ + "## Helper Functions" + ] + }, + { + "cell_type": "markdown", + "id": "2814a5e6-e4b0-4fa1-948c-a8671c52fb4b", + "metadata": {}, + "source": [ + "### Create a doe object" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "24628fdb-317d-4121-b4ea-9909298a757e", + "metadata": {}, + "outputs": [], + "source": [ + "def create_doe_object(Ca_val, T_vals, prior_FIM=None):\n", + " t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]\n", + " parameter_dict = {\"A1\": 85, \"A2\": 370, \"E1\": 8, \"E2\": 15}\n", + " \n", + " measurements = MeasurementVariables()\n", + " measurements.add_variables(\"C\", indices={0: [\"CA\", \"CB\", \"CC\"], 1: t_control}, time_index_position=1)\n", + " \n", + " exp_design = DesignVariables()\n", + " exp_design.add_variables(\n", + " \"CA0\",\n", + " time_index_position=0,\n", + " values=[Ca_val, ],\n", + " lower_bounds=1,\n", + " indices={0: [0]},\n", + " upper_bounds=5,\n", + " )\n", + " exp_design.add_variables(\n", + " \"T\",\n", + " indices={0: t_control},\n", + " time_index_position=0,\n", + " values=T_vals,\n", + " lower_bounds=300,\n", + " upper_bounds=700,\n", + " )\n", + " \n", + " doe_object = DesignOfExperiments(\n", + " parameter_dict,\n", + " exp_design, \n", + " measurements, \n", + " create_model,\n", + " prior_FIM=prior_FIM,\n", + " discretize_model=disc_for_measure,\n", + " )\n", + " return doe_object" + ] + }, + { + "cell_type": "markdown", + "id": "35346c15-408b-4f32-bbfa-b4de9da2eaae", + "metadata": {}, + "source": [ + "### Compute FIM using the compute_FIM function" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "0a2d84d7-9989-43d3-84d0-df01a95c5a66", + "metadata": {}, + "outputs": [], + "source": [ + "def compute_specific_FIM(Ca_val, T_vals, prior_FIM=None, scale_param=True):\n", + " doe_object = create_doe_object(Ca_val, T_vals, prior_FIM)\n", + "\n", + " result = doe_object.compute_FIM(\n", + " mode=\"sequential_finite\",\n", + " scale_nominal_param_value=scale_param,\n", + " formula=\"central\",\n", + " )\n", + " result.result_analysis()\n", + " \n", + " return result" + ] + }, + { + "cell_type": "markdown", + "id": "1a6f71b5-f09c-40d3-9793-b830f05aae54", + "metadata": {}, + "source": [ + "### Rescale FIM" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "d000abc6-9140-4634-a180-d1c2b6df9e0c", + "metadata": {}, + "outputs": [], + "source": [ + "def rescale_FIM(FIM, param_vals):\n", + " param_scaling_mat = (1 / param_vals).transpose().dot(1 / param_vals)\n", + " unscaled_FIM = np.multiply(FIM, param_scaling_mat)\n", + " return unscaled_FIM" + ] + }, + { + "cell_type": "markdown", + "id": "679466b7-5299-4489-a3fb-cbfdd74cfc27", + "metadata": {}, + "source": [ + "### Translate Jacobian to numpy array" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "dff13661-d577-44a3-a171-b3fa1e6ff367", + "metadata": {}, + "outputs": [], + "source": [ + "def translate_jac(jac_dict):\n", + " param_names = ['A1', 'A2', 'E1', 'E2']\n", + " Q_all = np.array(list(jac_dict for p in param_names)).T\n", + " return Q_all" + ] + }, + { + "cell_type": "markdown", + "id": "60fcc29c-dad9-4d85-8d2c-81daeb08deb3", + "metadata": {}, + "source": [ + "### Get experimental conditions from solved model" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "d6923520-83b5-4a64-9040-f19e1fa663f2", + "metadata": {}, + "outputs": [], + "source": [ + "def get_exp_conds(m):\n", + " return [pyo.value(m.CA0[0]),\n", + " pyo.value(m.T[0]),\n", + " pyo.value(m.T[0.125]),\n", + " pyo.value(m.T[0.25]),\n", + " pyo.value(m.T[0.375]),\n", + " pyo.value(m.T[0.5]),\n", + " pyo.value(m.T[0.625]),\n", + " pyo.value(m.T[0.75]),\n", + " pyo.value(m.T[0.875]),\n", + " pyo.value(m.T[1])]" + ] + }, + { + "cell_type": "markdown", + "id": "ed69f2f3-0517-4bdd-ae43-9566560e7e73", + "metadata": {}, + "source": [ + "### Run optimal experiment" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "989b0b0d-6cca-4c56-8088-8c95c9c1773a", + "metadata": {}, + "outputs": [], + "source": [ + "def run_optimal_exp(Ca, Ta_vals, prior_FIM=None, scale_param=True):\n", + " doe_object = create_doe_object(Ca, Ta_vals, prior_FIM)\n", + "\n", + " if prior_FIM is None:\n", + " prior_FIM = np.eye(4)\n", + " \n", + " square_result, optimize_result = doe_object.stochastic_program(\n", + " if_optimize=True,\n", + " if_Cholesky=True,\n", + " scale_nominal_param_value=scale_param,\n", + " objective_option=\"det\",\n", + " L_initial=np.linalg.cholesky(prior_FIM),\n", + " )\n", + " \n", + " return optimize_result" + ] + }, + { + "cell_type": "markdown", + "id": "950f0e98-74b7-42bb-8781-a94da863eac6", + "metadata": {}, + "source": [ + "# Perform the analysis" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "e5013f1e-0e77-4697-a9de-8c137d930452", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 5.77e+02 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 6.70e+00 3.85e+00 -1.0 6.23e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 6.62e-02 4.39e+00 -1.0 7.18e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.10e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.131\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -7.8073036e+00 1.25e+00 1.21e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -7.8354856e+00 6.29e-01 5.10e+00 -1.0 4.68e+01 - 9.58e-01 4.97e-01h 1\n", + " 2 -7.8368689e+00 8.10e-01 1.42e+00 -1.0 3.23e+01 - 9.25e-01 1.00e+00f 1\n", + " 3 -7.9348649e+00 7.30e+01 1.59e+02 -1.0 4.72e+02 - 2.16e-01 7.26e-01f 1\n", + " 4 -7.7429992e+00 1.53e+01 2.05e+00 -1.0 1.61e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -7.7606510e+00 3.51e+00 2.02e+00 -1.0 1.39e+02 - 1.00e+00 1.00e+00f 1\n", + " 6 -7.6711598e+00 5.13e+00 3.33e-01 -1.0 4.03e+01 - 1.00e+00 1.00e+00h 1\n", + " 7 -7.6659485e+00 4.95e-02 7.71e-03 -1.0 2.74e+00 - 1.00e+00 1.00e+00h 1\n", + " 8 -7.6685762e+00 2.86e-03 1.24e-02 -2.5 8.68e-01 - 1.00e+00 1.00e+00h 1\n", + " 9 -7.7508739e+00 3.08e+00 3.55e-01 -3.8 3.14e+01 - 8.03e-01 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -7.8600596e+00 6.29e+00 2.01e-01 -3.8 6.87e+01 - 1.00e+00 1.00e+00h 1\n", + " 11 -7.9182984e+00 4.14e+00 1.52e-01 -3.8 2.68e+01 - 1.00e+00 1.00e+00h 1\n", + " 12 -7.9098461e+00 6.13e-02 2.12e-03 -3.8 8.74e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (325287)\n", + " 13 -7.9098458e+00 4.63e-05 4.17e-06 -3.8 2.49e-01 - 1.00e+00 1.00e+00h 1\n", + " 14 -7.9395810e+00 1.47e+00 5.37e-02 -5.7 2.73e+01 - 7.73e-01 1.00e+00h 1\n", + " 15 -7.9504201e+00 6.12e-01 7.82e-02 -5.7 2.47e+01 - 9.84e-01 1.00e+00h 1\n", + " 16 -7.9503130e+00 1.96e-02 5.10e-03 -5.7 1.22e+01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (347931)\n", + " 17 -7.9503760e+00 2.90e-03 7.32e-04 -5.7 4.78e+00 - 1.00e+00 1.00e+00h 1\n", + " 18 -7.9503861e+00 5.47e-05 1.53e-05 -5.7 6.60e-01 - 1.00e+00 1.00e+00h 1\n", + " 19 -7.9503862e+00 1.43e-08 5.92e-09 -5.7 1.07e-02 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -7.9510641e+00 2.59e-03 7.06e-04 -8.6 4.57e+00 - 9.84e-01 1.00e+00h 1\n", + " 21 -7.9510903e+00 1.70e-04 5.47e-05 -8.6 1.18e+00 - 1.00e+00 1.00e+00h 1\n", + " 22 -7.9510917e+00 4.69e-07 2.65e-07 -8.6 6.21e-02 - 1.00e+00 1.00e+00h 1\n", + " 23 -7.9510917e+00 4.83e-12 5.34e-12 -8.6 1.99e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 23\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -7.9510917256426819e+00 -7.9510917256426819e+00\n", + "Dual infeasibility......: 5.3406483518369473e-12 5.3406483518369473e-12\n", + "Constraint violation....: 4.8324677592859189e-12 4.8324677592859189e-12\n", + "Complementarity.........: 2.5059057458010459e-09 2.5059057458010459e-09\n", + "Overall NLP error.......: 2.5059057458010459e-09 2.5059057458010459e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 24\n", + "Number of objective gradient evaluations = 24\n", + "Number of equality constraint evaluations = 24\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 24\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 23\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.827\n", + "Total CPU secs in NLP function evaluations = 0.027\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.5 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 3.83e+00 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.42e-02 3.85e+00 -1.0 4.12e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.37e-04 4.39e+00 -1.0 4.72e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.67e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8338667230373176e-11 5.8338667230373176e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8338667230373176e-11 5.8338667230373176e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.129\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 5.3463002e+00 1.25e+00 7.84e+01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 5.3181206e+00 6.32e-01 4.24e+01 -1.0 7.13e-01 - 9.72e-01 4.95e-01h 1\n", + " 2 5.3083839e+00 2.63e-02 1.90e+00 -1.0 3.89e+00 - 9.00e-01 1.00e+00f 1\n", + " 3 5.2237497e+00 1.10e+01 1.32e+02 -1.0 1.82e+02 - 1.89e-01 6.62e-01f 1\n", + " 4 5.4304758e+00 5.04e-01 2.27e+00 -1.0 3.13e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 5.3767095e+00 5.59e-01 3.10e+01 -1.0 1.35e+02 - 1.00e+00 4.26e-01f 2\n", + " 6 5.4552152e+00 8.41e-01 3.31e+00 -1.0 8.84e+01 - 9.40e-01 3.64e-01f 2\n", + " 7 5.5170724e+00 4.57e-01 4.85e-01 -1.0 4.86e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 5.5110643e+00 5.10e-03 2.84e-02 -1.7 1.44e+00 - 1.00e+00 1.00e+00h 1\n", + " 9 5.4931986e+00 9.03e-03 1.34e-02 -2.5 4.22e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 5.4082326e+00 3.49e-01 3.95e-01 -3.8 2.69e+01 - 7.84e-01 1.00e+00h 1\n", + " 11 5.3009072e+00 1.28e+00 1.94e-01 -3.8 6.45e+01 - 1.00e+00 1.00e+00h 1\n", + " 12 5.2328311e+00 3.94e-01 2.25e-01 -3.8 2.80e+01 - 1.00e+00 1.00e+00h 1\n", + " 13 5.2437917e+00 5.42e-03 2.96e-03 -3.8 2.67e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (320786)\n", + " 14 5.2437575e+00 2.67e-05 8.17e-06 -3.8 2.13e-01 - 1.00e+00 1.00e+00h 1\n", + " 15 5.2424530e+00 7.65e-06 1.35e-05 -5.7 9.87e-02 -4.0 1.00e+00 1.00e+00h 1\n", + " 16 5.2417146e+00 5.83e-05 6.61e-05 -8.6 3.00e-01 -4.5 9.97e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (344430)\n", + " 17 5.2399653e+00 4.96e-04 6.20e-04 -8.6 8.80e-01 -5.0 1.00e+00 1.00e+00h 1\n", + " 18 5.2353444e+00 3.84e-03 4.47e-03 -8.6 2.48e+00 -5.4 1.00e+00 1.00e+00h 1\n", + " 19 5.2251746e+00 2.39e-02 2.25e-02 -8.6 6.35e+00 -5.9 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 5.2221058e+00 2.25e-02 2.03e-02 -8.6 1.32e+01 -6.4 1.00e+00 1.69e-01h 1\n", + " 21 5.2103493e+00 7.19e-02 5.06e-02 -8.6 2.54e+01 -6.9 1.00e+00 6.04e-01f 1\n", + " 22 5.1989953e+00 2.17e-01 1.44e-01 -8.6 5.35e+01 - 1.00e+00 7.30e-01f 1\n", + " 23 5.1994362e+00 1.93e-01 1.25e-01 -8.6 4.05e+01 - 1.00e+00 1.27e-01h 1\n", + " 24 5.2008740e+00 1.40e-01 6.63e-02 -8.6 3.68e+01 - 1.00e+00 4.77e-01h 1\n", + " 25 5.2025347e+00 1.75e-03 2.05e-03 -8.6 3.78e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (363451)\n", + " 26 5.2025125e+00 1.38e-03 1.06e-05 -8.6 3.34e+00 - 1.00e+00 1.00e+00h 1\n", + " 27 5.2025121e+00 2.22e-05 1.37e-07 -8.6 4.27e-01 - 1.00e+00 1.00e+00h 1\n", + " 28 5.2025121e+00 3.85e-09 2.27e-11 -8.6 5.62e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 28\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 5.2025120660150401e+00 5.2025120660150401e+00\n", + "Dual infeasibility......: 2.2651314864147046e-11 2.2651314864147046e-11\n", + "Constraint violation....: 3.8472891539242937e-09 3.8472891539242937e-09\n", + "Complementarity.........: 2.5061201652512559e-09 2.5061201652512559e-09\n", + "Overall NLP error.......: 3.8472891539242937e-09 3.8472891539242937e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 35\n", + "Number of objective gradient evaluations = 29\n", + "Number of equality constraint evaluations = 35\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 29\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 28\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.155\n", + "Total CPU secs in NLP function evaluations = 0.043\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.9 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.43e-03 1.52e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 2.33e-09 1.52e-06 -1.0 1.82e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.3276043314979233e-09 2.3276043314979233e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.3276043314979233e-09 2.3276043314979233e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.099\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.43e-03 1.52e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 2.33e-09 1.52e-06 -1.0 1.82e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.3276034433195036e-09 2.3276034433195036e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.3276034433195036e-09 2.3276034433195036e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.100\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 7.65e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 5.84e+02 3.85e+02 -1.0 7.65e+02 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 6.77e+00 3.85e+00 -1.0 6.31e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 6.70e-02 4.39e+00 -1.0 7.25e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.17e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0383309725439176e-09 9.0383309725439176e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0383309725439176e-09 9.0383309725439176e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.127\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -9.0980672e+00 1.25e+00 1.13e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -9.1088823e+00 6.23e-01 5.20e+00 -1.0 4.67e+01 - 9.73e-01 5.02e-01h 1\n", + " 2 -9.1070281e+00 1.28e+00 1.94e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -9.1458221e+00 7.33e+01 1.31e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -9.0628162e+00 1.67e+01 2.33e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -9.0789051e+00 1.17e+01 3.14e+01 -1.0 1.36e+02 - 1.00e+00 4.20e-01f 2\n", + " 6 -9.0469561e+00 1.20e+01 3.32e+00 -1.0 9.02e+01 - 9.08e-01 3.50e-01f 2\n", + " 7 -9.0194008e+00 3.33e+00 5.06e-01 -1.0 5.04e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -9.0315738e+00 3.90e-01 6.40e-02 -1.0 9.89e+00 - 1.00e+00 1.00e+00h 1\n", + " 9 -9.0319624e+00 5.08e-04 4.68e-03 -1.7 3.22e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -9.0344881e+00 1.57e-02 2.20e-01 -3.8 2.67e+00 - 9.84e-01 1.00e+00h 1\n", + " 11 -9.0929490e+00 9.49e+00 2.02e-01 -3.8 7.71e+01 - 1.00e+00 1.00e+00h 1\n", + " 12 -9.1265794e+00 4.04e+00 1.08e-01 -3.8 2.61e+01 - 1.00e+00 1.00e+00h 1\n", + " 13 -9.1216817e+00 7.85e-02 1.20e-03 -3.8 1.12e+01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (325095)\n", + " 14 -9.1216910e+00 9.57e-05 3.07e-06 -3.8 2.80e-01 - 1.00e+00 1.00e+00h 1\n", + " 15 -9.1419843e+00 2.21e+00 4.13e-02 -5.7 3.31e+01 - 7.83e-01 1.00e+00h 1\n", + " 16 -9.1523277e+00 3.23e+00 1.07e-01 -5.7 5.50e+01 - 5.77e-01 8.28e-01h 1\n", + " 17 -9.1542206e+00 1.66e-01 4.37e-03 -5.7 1.73e+01 - 1.00e+00 1.00e+00h 1\n", + " 18 -9.1545142e+00 1.36e-02 1.84e-03 -5.7 1.01e+01 - 1.00e+00 1.00e+00h 1\n", + " 19 -9.1545212e+00 1.02e-03 1.25e-04 -5.7 2.81e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (343319)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -9.1545223e+00 4.18e-06 5.47e-07 -5.7 1.81e-01 - 1.00e+00 1.00e+00h 1\n", + " 21 -9.1551820e+00 9.84e-03 7.80e-04 -8.6 6.83e+00 - 9.73e-01 1.00e+00h 1\n", + " 22 -9.1552078e+00 8.18e-04 1.19e-04 -8.6 2.58e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (362498)\n", + " 23 -9.1552112e+00 1.16e-05 2.50e-06 -8.6 3.08e-01 - 1.00e+00 1.00e+00h 1\n", + " 24 -9.1552112e+00 1.62e-09 9.16e-10 -8.6 3.65e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 24\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -9.1552111997823289e+00 -9.1552111997823289e+00\n", + "Dual infeasibility......: 9.1614693867759918e-10 9.1614693867759918e-10\n", + "Constraint violation....: 1.6240718769822138e-09 1.6240718769822138e-09\n", + "Complementarity.........: 2.5062713088537995e-09 2.5062713088537995e-09\n", + "Overall NLP error.......: 2.5062713088537995e-09 2.5062713088537995e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 31\n", + "Number of objective gradient evaluations = 25\n", + "Number of equality constraint evaluations = 31\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 25\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 24\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.818\n", + "Total CPU secs in NLP function evaluations = 0.029\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.3 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 3.86e+00 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.45e-02 3.85e+00 -1.0 4.15e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.40e-04 4.39e+00 -1.0 4.75e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.70e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8336890873533775e-11 5.8336890873533775e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8336890873533775e-11 5.8336890873533775e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.134\n", + "Total CPU secs in NLP function evaluations = 0.007\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 4.0555365e+00 1.25e+00 4.96e+01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 4.0447179e+00 6.24e-01 2.67e+01 -1.0 7.09e-01 - 9.72e-01 5.01e-01h 1\n", + " 2 4.0465757e+00 2.64e-02 1.93e+00 -1.0 3.91e+00 - 8.99e-01 1.00e+00f 1\n", + " 3 4.0077802e+00 1.10e+01 1.31e+02 -1.0 1.82e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 4.0907795e+00 5.11e-01 2.33e+00 -1.0 3.13e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 4.0746958e+00 5.49e-01 3.14e+01 -1.0 1.36e+02 - 1.00e+00 4.20e-01f 2\n", + " 6 4.1066414e+00 8.29e-01 3.32e+00 -1.0 9.02e+01 - 9.08e-01 3.50e-01f 2\n", + " 7 4.1341891e+00 4.99e-01 5.06e-01 -1.0 5.03e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 4.1322707e+00 5.59e-03 2.41e-02 -1.7 1.21e+00 - 1.00e+00 1.00e+00h 1\n", + " 9 4.1294448e+00 1.74e-03 1.90e-01 -3.8 1.80e+00 - 9.85e-01 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 4.0577304e+00 2.51e+00 3.18e-01 -3.8 8.71e+01 - 1.00e+00 1.00e+00h 1\n", + " 11 4.0619968e+00 6.23e-02 7.22e-03 -3.8 1.27e+00 -4.0 1.00e+00 1.00e+00h 1\n", + " 12 4.0610820e+00 2.11e-05 1.57e-05 -3.8 1.71e-01 -4.5 1.00e+00 1.00e+00h 1\n", + " 13 4.0025145e+00 1.12e+00 2.83e-01 -5.7 5.61e+01 - 6.15e-01 1.00e+00h 1\n", + " 14 4.0028658e+00 3.34e-01 8.43e-02 -5.7 2.46e+01 - 6.14e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (328499)\n", + " 15 3.9993729e+00 8.26e-02 2.36e-02 -5.7 1.29e+01 - 1.00e+00 8.29e-01h 1\n", + " 16 3.9990307e+00 1.69e-02 2.80e-03 -5.7 1.13e+01 - 1.00e+00 1.00e+00f 1\n", + " 17 3.9990819e+00 1.12e-03 1.43e-04 -5.7 2.96e+00 - 1.00e+00 1.00e+00h 1\n", + " 18 3.9990815e+00 4.62e-06 7.31e-07 -5.7 1.90e-01 - 1.00e+00 1.00e+00h 1\n", + " 19 3.9984217e+00 5.89e-03 7.80e-04 -8.6 6.83e+00 - 9.73e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (356713)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 3.9983959e+00 8.18e-04 1.19e-04 -8.6 2.58e+00 - 1.00e+00 1.00e+00h 1\n", + " 21 3.9983926e+00 1.16e-05 2.50e-06 -8.6 3.08e-01 - 1.00e+00 1.00e+00h 1\n", + " 22 3.9983926e+00 1.62e-09 9.16e-10 -8.6 3.65e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 22\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 3.9983925918754277e+00 3.9983925918754277e+00\n", + "Dual infeasibility......: 9.1638497048149078e-10 9.1638497048149078e-10\n", + "Constraint violation....: 1.6245825795735414e-09 1.6245825795735414e-09\n", + "Complementarity.........: 2.5062714319510496e-09 2.5062714319510496e-09\n", + "Overall NLP error.......: 2.5062714319510496e-09 2.5062714319510496e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 29\n", + "Number of objective gradient evaluations = 23\n", + "Number of equality constraint evaluations = 29\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 23\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 22\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.757\n", + "Total CPU secs in NLP function evaluations = 0.035\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.5 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.43e-03 1.62e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 2.31e-09 1.52e-06 -1.0 1.81e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.3096600187955119e-09 2.3096600187955119e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.3096600187955119e-09 2.3096600187955119e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", + "Total CPU secs in NLP function evaluations = 0.008\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.43e-03 1.62e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 2.31e-09 1.52e-06 -1.0 1.81e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.3096582424386725e-09 2.3096582424386725e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.3096582424386725e-09 2.3096582424386725e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", + "Total CPU secs in NLP function evaluations = 0.002\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.53e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 5.92e+02 3.85e+02 -1.0 1.53e+03 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 6.85e+00 3.85e+00 -1.0 6.39e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 6.78e-02 4.39e+00 -1.0 7.33e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.25e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0381035988684744e-09 9.0381035988684744e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0381035988684744e-09 9.0381035988684744e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.129\n", + "Total CPU secs in NLP function evaluations = 0.003\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -9.8243678e+00 1.25e+00 1.11e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -9.8309752e+00 6.21e-01 5.18e+00 -1.0 4.66e+01 - 9.73e-01 5.03e-01h 1\n", + " 2 -9.8284057e+00 1.28e+00 1.94e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -9.8536224e+00 7.33e+01 1.31e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -9.8018928e+00 1.67e+01 2.30e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -9.8108490e+00 1.17e+01 3.16e+01 -1.0 1.36e+02 - 1.00e+00 4.18e-01f 2\n", + " 6 -9.7907700e+00 1.21e+01 3.32e+00 -1.0 9.09e+01 - 8.96e-01 3.45e-01f 2\n", + " 7 -9.7727931e+00 3.44e+00 5.16e-01 -1.0 5.11e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -9.7806557e+00 4.10e-01 6.89e-02 -1.0 1.03e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -9.7808078e+00 3.15e-04 3.35e-03 -1.7 2.38e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -9.7818625e+00 6.70e-03 1.10e-01 -3.8 1.81e+00 - 9.92e-01 1.00e+00h 1\n", + " 11 -9.8219871e+00 1.08e+01 1.59e-01 -3.8 8.04e+01 - 1.00e+00 1.00e+00h 1\n", + " 12 -9.8318869e+00 1.12e+00 2.36e-02 -3.8 1.46e+01 - 1.00e+00 1.00e+00h 1\n", + " 13 -9.8315331e+00 8.02e-03 5.09e-05 -3.8 3.56e+00 - 1.00e+00 1.00e+00h 1\n", + " 14 -9.8315167e+00 4.91e-06 6.23e-08 -3.8 2.45e-02 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (319727)\n", + " 15 -9.8470261e+00 2.31e+00 2.95e-02 -5.7 3.68e+01 - 7.66e-01 1.00e+00f 1\n", + " 16 -9.8550043e+00 3.01e+00 3.76e-02 -5.7 4.15e+01 - 6.44e-01 1.00e+00h 1\n", + " 17 -9.8580603e+00 2.13e+00 3.03e-02 -5.7 5.38e+01 - 9.08e-01 5.97e-01h 1\n", + "Reallocating memory for MA57: lfact (343560)\n", + " 18 -9.8590178e+00 5.44e-02 3.07e-03 -5.7 1.49e+01 - 1.00e+00 1.00e+00h 1\n", + " 19 -9.8588956e+00 2.88e-03 2.14e-04 -5.7 4.70e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -9.8588964e+00 2.30e-05 1.59e-06 -5.7 4.22e-01 - 1.00e+00 1.00e+00h 1\n", + " 21 -9.8588964e+00 1.08e-09 9.88e-11 -5.7 2.90e-03 - 1.00e+00 1.00e+00h 1\n", + " 22 -9.8595479e+00 2.23e-02 7.77e-04 -8.6 8.37e+00 - 9.64e-01 1.00e+00f 1\n", + " 23 -9.8595710e+00 1.73e-03 1.60e-04 -8.6 3.74e+00 - 1.00e+00 1.00e+00h 1\n", + " 24 -9.8595756e+00 5.35e-05 6.70e-06 -8.6 6.62e-01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (362351)\n", + " 25 -9.8595757e+00 3.41e-08 9.69e-09 -8.6 1.67e-02 - 1.00e+00 1.00e+00h 1\n", + " 26 -9.8595757e+00 4.55e-13 5.52e-14 -8.6 2.20e-05 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 26\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -9.8595757275986511e+00 -9.8595757275986511e+00\n", + "Dual infeasibility......: 5.5219338836918813e-14 5.5219338836918813e-14\n", + "Constraint violation....: 1.1824654114063750e-13 4.5474735088646412e-13\n", + "Complementarity.........: 2.5059035684619852e-09 2.5059035684619852e-09\n", + "Overall NLP error.......: 2.5059035684619852e-09 2.5059035684619852e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 33\n", + "Number of objective gradient evaluations = 27\n", + "Number of equality constraint evaluations = 33\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 27\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 26\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.850\n", + "Total CPU secs in NLP function evaluations = 0.040\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.5 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 3.89e+00 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.48e-02 3.85e+00 -1.0 4.18e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.43e-04 4.39e+00 -1.0 4.79e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.73e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8336446784323925e-11 5.8336446784323925e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8336446784323925e-11 5.8336446784323925e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.129\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 3.3292360e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 3.3241914e+00 1.69e-01 1.13e+00 -1.0 5.96e-01 - 9.79e-01 8.65e-01h 1\n", + " 2 3.3248864e+00 3.48e-02 1.06e+01 -1.0 6.02e+00 - 8.34e-01 1.00e+00f 1\n", + " 3 3.3013605e+00 1.26e+01 5.70e+01 -1.0 1.80e+02 - 1.92e-01 7.12e-01f 1\n", + " 4 3.3560475e+00 7.19e-01 2.65e+00 -1.0 2.65e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 3.3417261e+00 5.86e-01 4.46e+01 -1.0 3.82e+02 - 4.35e-01 8.06e-02f 2\n", + " 6 3.3757459e+00 1.35e-01 4.87e+00 -1.0 2.40e+01 -4.0 8.21e-01 1.00e+00F 1\n", + " 7 3.3696371e+00 4.25e-01 3.04e-01 -1.0 5.43e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 3.3719932e+00 4.00e-02 5.66e-02 -1.7 1.71e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 3.3709820e+00 4.47e-03 3.86e-03 -2.5 5.77e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 3.3640190e+00 1.64e-02 7.98e-02 -3.8 6.30e+00 - 9.57e-01 1.00e+00h 1\n", + " 11 3.3273550e+00 8.10e-01 8.71e-02 -3.8 4.15e+01 - 1.00e+00 1.00e+00h 1\n", + " 12 3.3219891e+00 1.09e-01 1.42e-02 -3.8 1.16e+01 - 1.00e+00 1.00e+00h 1\n", + " 13 3.3220787e+00 1.81e-04 1.97e-05 -3.8 5.23e-01 - 1.00e+00 1.00e+00h 1\n", + " 14 3.3065783e+00 3.88e-01 2.95e-02 -5.7 3.68e+01 - 7.66e-01 1.00e+00h 1\n", + " 15 3.3088038e+00 6.58e-04 2.85e-03 -5.7 1.95e-01 -4.5 9.73e-01 1.00e+00h 1\n", + " 16 3.3086408e+00 2.78e-05 4.37e-06 -5.7 2.06e-01 -5.0 1.00e+00 1.00e+00h 1\n", + " 17 3.3082900e+00 2.44e-04 3.75e-05 -5.7 6.13e-01 -5.4 1.00e+00 1.00e+00h 1\n", + " 18 3.3073216e+00 2.02e-03 3.10e-04 -5.7 1.78e+00 -5.9 1.00e+00 1.00e+00h 1\n", + " 19 3.3049420e+00 1.48e-02 2.24e-03 -5.7 4.93e+00 -6.4 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 3.3006609e+00 6.58e-02 9.79e-03 -5.7 1.19e+01 -6.9 1.00e+00 8.79e-01h 1\n", + " 21 3.2971545e+00 1.04e-01 2.91e-02 -5.7 1.96e+01 -7.3 1.00e+00 1.00e+00f 1\n", + " 22 3.2963467e+00 1.07e-01 2.86e-02 -5.7 9.98e+01 - 9.97e-01 5.49e-02h 1\n", + " 23 3.2940284e+00 2.58e-01 2.86e-02 -5.7 4.31e+01 - 1.00e+00 1.00e+00f 1\n", + "Reallocating memory for MA57: lfact (328243)\n", + " 24 3.2947114e+00 6.39e-03 2.75e-03 -5.7 5.48e+00 - 1.00e+00 1.00e+00h 1\n", + " 25 3.2947076e+00 2.82e-04 1.21e-05 -5.7 1.48e+00 - 1.00e+00 1.00e+00h 1\n", + " 26 3.2947074e+00 3.77e-07 7.87e-09 -5.7 5.41e-02 - 1.00e+00 1.00e+00h 1\n", + " 27 3.2940558e+00 8.94e-03 7.77e-04 -8.6 8.37e+00 - 9.64e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (351796)\n", + " 28 3.2940328e+00 1.73e-03 1.60e-04 -8.6 3.74e+00 - 1.00e+00 1.00e+00h 1\n", + " 29 3.2940282e+00 5.35e-05 6.70e-06 -8.6 6.62e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 3.2940281e+00 3.41e-08 9.69e-09 -8.6 1.67e-02 - 1.00e+00 1.00e+00h 1\n", + " 31 3.2940281e+00 3.59e-13 6.26e-14 -8.6 2.20e-05 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 31\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 3.2940280640592707e+00 3.2940280640592707e+00\n", + "Dual infeasibility......: 6.2616567740385095e-14 6.2616567740385095e-14\n", + "Constraint violation....: 1.5287940420760863e-13 3.5882408155885059e-13\n", + "Complementarity.........: 2.5059035684585384e-09 2.5059035684585384e-09\n", + "Overall NLP error.......: 2.5059035684585384e-09 2.5059035684585384e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 36\n", + "Number of objective gradient evaluations = 32\n", + "Number of equality constraint evaluations = 36\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 32\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 31\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.195\n", + "Total CPU secs in NLP function evaluations = 0.048\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.9 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.42e-03 1.85e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 2.27e-09 1.51e-06 -1.0 1.79e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.2713546599106849e-09 2.2713546599106849e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.2713546599106849e-09 2.2713546599106849e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.104\n", + "Total CPU secs in NLP function evaluations = 0.007\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.42e-03 1.85e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 2.27e-09 1.51e-06 -1.0 1.79e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.2713533276430553e-09 2.2713533276430553e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.2713533276430553e-09 2.2713533276430553e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.105\n", + "Total CPU secs in NLP function evaluations = 0.002\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 2.30e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 6.00e+02 3.85e+02 -1.0 2.30e+03 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 6.93e+00 3.85e+00 -1.0 6.46e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 6.85e-02 4.39e+00 -1.0 7.41e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.33e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.131\n", + "Total CPU secs in NLP function evaluations = 0.008\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.0333966e+01 1.25e+00 1.09e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.0338708e+01 6.20e-01 5.17e+00 -1.0 4.65e+01 - 9.73e-01 5.04e-01h 1\n", + " 2 -1.0336296e+01 1.28e+00 1.95e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.0354984e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.0317441e+01 1.67e+01 2.27e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.0323555e+01 1.17e+01 3.18e+01 -1.0 1.36e+02 - 1.00e+00 4.16e-01f 2\n", + " 6 -1.0308913e+01 1.22e+01 3.32e+00 -1.0 9.13e+01 - 8.90e-01 3.42e-01f 2\n", + " 7 -1.0295537e+01 3.50e+00 5.22e-01 -1.0 5.14e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.0301363e+01 4.21e-01 7.17e-02 -1.0 1.05e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.0301441e+01 2.63e-04 2.69e-03 -1.7 2.08e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.0302019e+01 3.71e-03 5.57e-02 -3.8 1.37e+00 - 9.96e-01 1.00e+00h 1\n", + " 11 -1.0333647e+01 1.23e+01 1.35e-01 -3.8 8.29e+01 - 1.00e+00 1.00e+00h 1\n", + " 12 -1.0334471e+01 3.28e-01 4.31e-03 -3.8 1.15e+01 - 1.00e+00 1.00e+00h 1\n", + " 13 -1.0335026e+01 4.18e-03 1.95e-05 -3.8 6.62e-01 - 1.00e+00 1.00e+00h 1\n", + " 14 -1.0335021e+01 3.25e-07 2.67e-09 -3.8 1.60e-02 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (324090)\n", + " 15 -1.0347510e+01 2.31e+00 2.80e-02 -5.7 3.87e+01 - 7.60e-01 1.00e+00f 1\n", + " 16 -1.0354058e+01 2.41e+00 2.21e-02 -5.7 3.02e+01 - 7.30e-01 1.00e+00h 1\n", + " 17 -1.0356712e+01 3.68e+00 4.66e-02 -5.7 1.52e+02 - 5.86e-01 2.47e-01h 1\n", + " 18 -1.0358821e+01 2.88e-01 4.06e-03 -5.7 1.96e+01 - 1.00e+00 1.00e+00h 1\n", + " 19 -1.0358654e+01 8.94e-03 4.59e-04 -5.7 8.19e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.0358657e+01 2.20e-04 8.10e-06 -5.7 1.30e+00 - 1.00e+00 1.00e+00h 1\n", + " 21 -1.0358657e+01 9.03e-08 2.32e-09 -5.7 2.63e-02 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (347964)\n", + " 22 -1.0359304e+01 3.93e-02 7.56e-04 -8.6 9.55e+00 - 9.56e-01 1.00e+00h 1\n", + " 23 -1.0359325e+01 2.75e-03 1.87e-04 -8.6 4.70e+00 - 1.00e+00 1.00e+00h 1\n", + " 24 -1.0359330e+01 1.37e-04 1.18e-05 -8.6 1.06e+00 - 1.00e+00 1.00e+00h 1\n", + " 25 -1.0359330e+01 2.28e-07 3.96e-08 -8.6 4.33e-02 - 1.00e+00 1.00e+00h 1\n", + " 26 -1.0359330e+01 1.70e-12 4.97e-13 -8.6 1.18e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 26\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.0359330165707208e+01 -1.0359330165707208e+01\n", + "Dual infeasibility......: 4.9706657073264839e-13 4.9706657073264839e-13\n", + "Constraint violation....: 1.7005286068183523e-12 1.7005286068183523e-12\n", + "Complementarity.........: 2.5059037505427129e-09 2.5059037505427129e-09\n", + "Overall NLP error.......: 2.5059037505427129e-09 2.5059037505427129e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 33\n", + "Number of objective gradient evaluations = 27\n", + "Number of equality constraint evaluations = 33\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 27\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 26\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.860\n", + "Total CPU secs in NLP function evaluations = 0.024\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.6 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 3.92e+00 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.51e-02 3.85e+00 -1.0 4.21e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.46e-04 4.39e+00 -1.0 4.82e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.76e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8337334962743626e-11 5.8337334962743626e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8337334962743626e-11 5.8337334962743626e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.125\n", + "Total CPU secs in NLP function evaluations = 0.010\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 2.8196379e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 2.8162115e+00 1.65e-01 1.10e+00 -1.0 5.96e-01 - 9.79e-01 8.68e-01h 1\n", + " 2 2.8171449e+00 3.48e-02 1.08e+01 -1.0 6.03e+00 - 8.34e-01 1.00e+00f 1\n", + " 3 2.7994163e+00 1.25e+01 5.72e+01 -1.0 1.80e+02 - 1.92e-01 7.12e-01f 1\n", + " 4 2.8392046e+00 7.17e-01 2.60e+00 -1.0 2.66e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 2.8292019e+00 5.86e-01 4.54e+01 -1.0 3.71e+02 - 4.48e-01 8.32e-02f 2\n", + " 6 2.8540376e+00 1.35e-01 5.27e+00 -1.0 2.40e+01 -4.0 8.21e-01 1.00e+00F 1\n", + " 7 2.8496419e+00 4.28e-01 2.99e-01 -1.0 5.45e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 2.8514802e+00 3.95e-02 5.69e-02 -1.7 1.69e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 2.8509342e+00 4.75e-03 3.32e-03 -2.5 5.95e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 2.8470769e+00 9.20e-03 3.76e-02 -3.8 4.69e+00 - 9.80e-01 1.00e+00h 1\n", + " 11 2.8179475e+00 8.07e-01 7.45e-02 -3.8 3.88e+01 - 1.00e+00 1.00e+00h 1\n", + " 12 2.8189649e+00 2.78e-02 1.87e-03 -3.8 5.72e+00 - 1.00e+00 1.00e+00h 1\n", + " 13 2.8185808e+00 5.85e-05 6.97e-06 -3.8 3.19e-01 - 1.00e+00 1.00e+00h 1\n", + " 14 2.8060943e+00 4.50e-01 2.80e-02 -5.7 3.87e+01 - 7.60e-01 1.00e+00h 1\n", + " 15 2.8076988e+00 1.27e-03 1.40e-03 -5.7 2.15e-01 -4.5 9.84e-01 1.00e+00h 1\n", + " 16 2.8075717e+00 1.61e-05 2.02e-06 -5.7 1.49e-01 -5.0 1.00e+00 1.00e+00h 1\n", + " 17 2.8073034e+00 1.50e-04 2.56e-05 -8.6 4.56e-01 -5.4 9.91e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (332552)\n", + " 18 2.7996057e+00 1.17e+00 3.50e-01 -8.6 1.18e+02 - 2.14e-01 7.03e-01h 1\n", + " 19 2.7992999e+00 1.00e+00 2.99e-01 -8.6 1.21e+02 - 1.40e-01 1.38e-01h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 2.7992903e+00 9.94e-01 2.96e-01 -8.6 3.79e+01 - 7.17e-01 9.13e-03h 1\n", + " 21 2.7990482e+00 6.68e-01 1.97e-01 -8.6 3.87e+01 - 9.68e-01 3.74e-01h 1\n", + " 22 2.7989147e+00 5.66e-01 1.67e-01 -8.6 3.55e+01 - 1.00e+00 1.65e-01f 1\n", + " 23 2.7981330e+00 2.94e-01 8.73e-02 -8.6 3.53e+01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (356955)\n", + " 24 2.7982525e+00 9.54e-03 2.30e-03 -8.6 6.04e-01 -5.9 1.00e+00 1.00e+00h 1\n", + " 25 2.7953817e+00 8.37e-01 2.60e-01 -8.6 8.53e+01 - 7.77e-02 6.80e-01h 1\n", + " 26 2.7970874e+00 5.84e-01 1.64e-01 -8.6 5.21e+01 - 1.00e+00 5.00e-01h 2\n", + " 27 2.7963533e+00 2.95e-02 5.91e-03 -8.6 2.18e+00 -6.4 1.00e+00 1.00e+00h 1\n", + " 28 2.7958325e+00 8.11e-03 1.49e-03 -8.6 5.48e+00 -6.9 1.00e+00 1.00e+00h 1\n", + " 29 2.7945535e+00 7.95e-02 1.40e-02 -8.6 1.74e+01 -7.3 6.23e-01 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 2.7944657e+00 7.83e-02 1.38e-02 -8.6 5.44e+01 -7.8 8.23e-01 1.76e-02h 1\n", + " 31 2.7944608e+00 7.71e-02 1.36e-02 -8.6 7.54e+00 - 8.32e-01 1.59e-02h 1\n", + " 32 2.7941818e+00 6.39e-03 2.32e-03 -8.6 7.11e+00 - 1.00e+00 1.00e+00f 1\n", + " 33 2.7942732e+00 7.14e-05 1.97e-05 -8.6 7.45e-01 - 1.00e+00 1.00e+00h 1\n", + " 34 2.7942736e+00 7.61e-09 1.93e-09 -8.6 7.70e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 34\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 2.7942736259106784e+00 2.7942736259106784e+00\n", + "Dual infeasibility......: 1.9311658899047611e-09 1.9311658899047611e-09\n", + "Constraint violation....: 7.6102699697599974e-09 7.6102699697599974e-09\n", + "Complementarity.........: 2.5060093311148399e-09 2.5060093311148399e-09\n", + "Overall NLP error.......: 7.6102699697599974e-09 7.6102699697599974e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 40\n", + "Number of objective gradient evaluations = 35\n", + "Number of equality constraint evaluations = 40\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 35\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 34\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.244\n", + "Total CPU secs in NLP function evaluations = 0.050\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.41e-03 2.08e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 2.23e-09 1.50e-06 -1.0 1.78e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.2333721538814189e-09 2.2333721538814189e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.2333721538814189e-09 2.2333721538814189e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.099\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.41e-03 2.08e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 2.23e-09 1.50e-06 -1.0 1.78e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.2333717097922090e-09 2.2333717097922090e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.2333717097922090e-09 2.2333717097922090e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.103\n", + "Total CPU secs in NLP function evaluations = 0.003\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 0.8 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.06e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 6.07e+02 3.85e+02 -1.0 3.06e+03 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 7.00e+00 3.85e+00 -1.0 6.54e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 6.93e-02 4.39e+00 -1.0 7.48e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.40e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0381035988684744e-09 9.0381035988684744e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0381035988684744e-09 9.0381035988684744e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.127\n", + "Total CPU secs in NLP function evaluations = 0.007\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.0727169e+01 1.25e+00 1.08e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.0730862e+01 6.19e-01 5.16e+00 -1.0 4.65e+01 - 9.73e-01 5.05e-01h 1\n", + " 2 -1.0728705e+01 1.28e+00 1.95e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.0743551e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.0714096e+01 1.67e+01 2.25e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.0718710e+01 1.18e+01 3.18e+01 -1.0 1.36e+02 - 1.00e+00 4.15e-01f 2\n", + " 6 -1.0707188e+01 1.22e+01 3.32e+00 -1.0 9.16e+01 - 8.86e-01 3.40e-01f 2\n", + " 7 -1.0696529e+01 3.54e+00 5.25e-01 -1.0 5.17e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.0701162e+01 4.28e-01 7.36e-02 -1.0 1.06e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.0701207e+01 2.45e-04 2.30e-03 -1.7 2.53e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.0701573e+01 2.36e-03 2.36e-02 -3.8 1.10e+00 - 9.98e-01 1.00e+00h 1\n", + " 11 -1.0728466e+01 1.41e+01 1.22e-01 -3.8 8.53e+01 - 1.00e+00 1.00e+00h 1\n", + " 12 -1.0724859e+01 5.12e-01 5.81e-03 -3.8 1.89e+01 - 1.00e+00 1.00e+00h 1\n", + " 13 -1.0725400e+01 2.98e-03 1.26e-05 -3.8 1.22e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (319443)\n", + " 14 -1.0725400e+01 2.16e-08 1.65e-09 -3.8 6.85e-03 - 1.00e+00 1.00e+00h 1\n", + " 15 -1.0735769e+01 2.29e+00 2.70e-02 -5.7 3.95e+01 - 7.59e-01 1.00e+00f 1\n", + " 16 -1.0741697e+01 2.18e+00 1.82e-02 -5.7 3.17e+01 - 7.93e-01 1.00e+00h 1\n", + " 17 -1.0742738e+01 2.63e+00 1.88e-02 -5.7 3.36e+02 - 3.31e-01 5.69e-02h 2\n", + " 18 -1.0746014e+01 1.33e+00 1.30e-02 -5.7 3.62e+01 - 1.00e+00 1.00e+00h 1\n", + " 19 -1.0746312e+01 1.77e-01 1.01e-03 -5.7 1.56e+01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (336826)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.0746301e+01 6.01e-04 1.57e-05 -5.7 2.13e+00 - 1.00e+00 1.00e+00h 1\n", + " 21 -1.0746301e+01 5.82e-07 8.09e-09 -5.7 6.66e-02 - 1.00e+00 1.00e+00h 1\n", + " 22 -1.0746946e+01 6.09e-02 7.31e-04 -8.6 1.05e+01 - 9.50e-01 1.00e+00h 1\n", + " 23 -1.0746963e+01 3.80e-03 2.04e-04 -8.6 5.52e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (359506)\n", + " 24 -1.0746969e+01 2.64e-04 1.71e-05 -8.6 1.47e+00 - 1.00e+00 1.00e+00h 1\n", + " 25 -1.0746970e+01 8.56e-07 1.03e-07 -8.6 8.39e-02 - 1.00e+00 1.00e+00h 1\n", + " 26 -1.0746970e+01 1.75e-11 3.92e-12 -8.6 3.79e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 26\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.0746969709499885e+01 -1.0746969709499885e+01\n", + "Dual infeasibility......: 3.9211112624388169e-12 3.9211112624388169e-12\n", + "Constraint violation....: 1.7519652395492358e-11 1.7519652395492358e-11\n", + "Complementarity.........: 2.5059051337529310e-09 2.5059051337529310e-09\n", + "Overall NLP error.......: 2.5059051337529310e-09 2.5059051337529310e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 36\n", + "Number of objective gradient evaluations = 27\n", + "Number of equality constraint evaluations = 36\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 27\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 26\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.940\n", + "Total CPU secs in NLP function evaluations = 0.036\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.7 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 3.95e+00 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.55e-02 3.85e+00 -1.0 4.25e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.50e-04 4.39e+00 -1.0 4.85e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.80e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.130\n", + "Total CPU secs in NLP function evaluations = 0.008\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 2.4264349e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 2.4238621e+00 1.63e-01 1.09e+00 -1.0 5.95e-01 - 9.79e-01 8.70e-01h 1\n", + " 2 2.4248028e+00 3.48e-02 1.10e+01 -1.0 6.03e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 2.4105783e+00 1.25e+01 5.73e+01 -1.0 1.80e+02 - 1.92e-01 7.12e-01f 1\n", + " 4 2.4418472e+00 7.15e-01 2.57e+00 -1.0 2.66e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 2.4341825e+00 5.85e-01 4.59e+01 -1.0 3.64e+02 - 4.56e-01 8.47e-02f 2\n", + " 6 2.4537211e+00 1.36e-01 5.47e+00 -1.0 2.40e+01 -4.0 8.21e-01 1.00e+00F 1\n", + " 7 2.4503021e+00 4.30e-01 2.96e-01 -1.0 5.46e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 2.4518079e+00 3.92e-02 5.71e-02 -1.7 1.69e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 2.4514688e+00 4.92e-03 3.01e-03 -2.5 6.06e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 2.4490209e+00 5.79e-03 1.11e-02 -3.8 3.70e+00 - 9.94e-01 1.00e+00h 1\n", + " 11 2.4240878e+00 7.98e-01 6.68e-02 -3.8 3.99e+01 - 1.00e+00 1.00e+00h 1\n", + " 12 2.4284887e+00 8.05e-03 3.01e-03 -3.8 7.54e+00 - 1.00e+00 1.00e+00h 1\n", + " 13 2.4282037e+00 3.91e-05 3.61e-06 -3.8 2.51e-01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (321514)\n", + " 14 2.4178352e+00 4.81e-01 2.70e-02 -5.7 3.95e+01 - 7.59e-01 1.00e+00h 1\n", + " 15 2.4119071e+00 2.15e-01 1.82e-02 -5.7 3.17e+01 - 7.93e-01 1.00e+00h 1\n", + " 16 2.4108659e+00 2.12e-01 1.88e-02 -5.7 2.81e+02 - 3.31e-01 5.69e-02h 2\n", + " 17 2.4075899e+00 1.15e-01 1.30e-02 -5.7 2.13e+01 - 1.00e+00 1.00e+00h 1\n", + " 18 2.4072917e+00 1.38e-02 1.01e-03 -5.7 1.01e+01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (337602)\n", + " 19 2.4073026e+00 6.01e-04 1.57e-05 -5.7 2.13e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 2.4073027e+00 5.82e-07 8.09e-09 -5.7 6.66e-02 - 1.00e+00 1.00e+00h 1\n", + " 21 2.4066582e+00 1.43e-02 7.31e-04 -8.6 1.05e+01 - 9.50e-01 1.00e+00h 1\n", + " 22 2.4066404e+00 3.80e-03 2.04e-04 -8.6 5.52e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (360986)\n", + " 23 2.4066344e+00 2.64e-04 1.71e-05 -8.6 1.47e+00 - 1.00e+00 1.00e+00h 1\n", + " 24 2.4066341e+00 8.56e-07 1.03e-07 -8.6 8.39e-02 - 1.00e+00 1.00e+00h 1\n", + " 25 2.4066341e+00 1.75e-11 3.92e-12 -8.6 3.79e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 25\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 2.4066340821203402e+00 2.4066340821203402e+00\n", + "Dual infeasibility......: 3.9211113364714817e-12 3.9211113364714817e-12\n", + "Constraint violation....: 1.7518986261677583e-11 1.7518986261677583e-11\n", + "Complementarity.........: 2.5059051337162099e-09 2.5059051337162099e-09\n", + "Overall NLP error.......: 2.5059051337162099e-09 2.5059051337162099e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 33\n", + "Number of objective gradient evaluations = 26\n", + "Number of equality constraint evaluations = 33\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 26\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 25\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.855\n", + "Total CPU secs in NLP function evaluations = 0.033\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.6 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.39e-03 2.31e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 2.20e-09 1.49e-06 -1.0 1.76e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.1957125007077138e-09 2.1957125007077138e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.1957125007077138e-09 2.1957125007077138e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.102\n", + "Total CPU secs in NLP function evaluations = 0.003\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.39e-03 2.31e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 2.20e-09 1.49e-06 -1.0 1.76e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.1957116125292941e-09 2.1957116125292941e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.1957116125292941e-09 2.1957116125292941e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.099\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.83e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 6.15e+02 3.85e+02 -1.0 3.83e+03 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 7.08e+00 3.85e+00 -1.0 6.62e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 7.00e-02 4.39e+00 -1.0 7.56e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.48e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0390130935702473e-09 9.0390130935702473e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0390130935702473e-09 9.0390130935702473e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.128\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.1047463e+01 1.25e+00 1.08e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.1050486e+01 6.19e-01 5.16e+00 -1.0 4.65e+01 - 9.73e-01 5.05e-01h 1\n", + " 2 -1.1048562e+01 1.28e+00 1.95e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.1060878e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.1036646e+01 1.67e+01 2.24e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.1040342e+01 1.18e+01 3.19e+01 -1.0 1.36e+02 - 1.00e+00 4.15e-01f 2\n", + " 6 -1.1030843e+01 1.22e+01 3.32e+00 -1.0 9.18e+01 - 8.84e-01 3.39e-01f 2\n", + " 7 -1.1021982e+01 3.57e+00 5.28e-01 -1.0 5.19e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.1025828e+01 4.33e-01 7.49e-02 -1.0 1.07e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.1025857e+01 2.38e-04 2.03e-03 -1.7 2.83e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.1026109e+01 1.63e-03 2.25e-03 -3.8 9.21e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 -1.1026632e+01 7.86e-04 4.02e-04 -3.8 1.57e+00 -4.0 1.00e+00 1.00e+00h 1\n", + " 12 -1.1027550e+01 2.46e-03 3.00e-04 -5.7 2.75e+00 -4.5 1.00e+00 1.00e+00h 1\n", + " 13 -1.1028272e+01 2.47e-03 5.48e-04 -5.7 2.72e+00 -5.0 1.00e+00 7.82e-01h 1\n", + " 14 -1.1028813e+01 8.65e-03 7.74e-04 -5.7 1.44e+00 -5.4 1.00e+00 1.00e+00f 1\n", + " 15r-1.1028813e+01 8.65e-03 9.99e+02 -2.1 0.00e+00 - 0.00e+00 3.69e-07R 16\n", + " 16r-1.1028744e+01 1.97e-03 2.91e+02 -2.1 2.14e+02 - 1.00e+00 9.90e-04f 1\n", + " 17 -1.1028797e+01 1.97e-03 7.77e-02 -5.7 7.31e+03 - 6.04e-02 2.22e-05h 2\n", + " 18 -1.1070945e+01 5.57e+01 7.49e-02 -5.7 4.61e+03 - 3.70e-02 3.66e-02h 1\n", + " 19 -1.1077425e+01 5.37e+01 7.00e-02 -5.7 7.35e+02 - 7.21e-02 6.45e-02h 1\n", + "Reallocating memory for MA57: lfact (323295)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.1075942e+01 4.63e+01 6.82e-02 -5.7 2.71e+02 - 1.00e+00 1.71e-01h 1\n", + " 21 -1.1056650e+01 7.71e+00 7.12e-01 -5.7 9.20e+01 - 7.89e-01 1.00e+00h 1\n", + " 22 -1.1061268e+01 4.91e-01 3.85e-02 -5.7 2.95e+01 -5.9 1.00e+00 1.00e+00h 1\n", + " 23 -1.1062198e+01 6.67e-01 2.40e-02 -5.7 3.95e+01 - 1.00e+00 3.17e-01h 2\n", + " 24 -1.1062691e+01 5.52e-01 1.59e-02 -5.7 1.91e+01 - 1.00e+00 3.26e-01h 2\n", + " 25 -1.1063214e+01 1.71e-01 4.96e-03 -5.7 7.52e+00 - 1.00e+00 1.00e+00h 1\n", + " 26 -1.1063041e+01 1.76e-02 8.52e-04 -5.7 4.14e+00 - 1.00e+00 1.00e+00h 1\n", + " 27 -1.1063029e+01 1.30e-04 4.18e-06 -5.7 3.18e-01 - 1.00e+00 1.00e+00h 1\n", + " 28 -1.1063029e+01 3.22e-09 1.10e-10 -5.7 1.63e-03 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (352737)\n", + " 29 -1.1063672e+01 8.69e-02 7.05e-04 -8.6 1.13e+01 - 9.43e-01 1.00e+00f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -1.1063687e+01 4.85e-03 2.15e-04 -8.6 6.22e+00 - 1.00e+00 1.00e+00h 1\n", + " 31 -1.1063694e+01 4.30e-04 2.22e-05 -8.6 1.87e+00 - 1.00e+00 1.00e+00h 1\n", + " 32 -1.1063694e+01 2.31e-06 2.07e-07 -8.6 1.38e-01 - 1.00e+00 1.00e+00h 1\n", + " 33 -1.1063694e+01 1.00e-10 1.87e-11 -8.6 9.08e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 33\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.1063694185536061e+01 -1.1063694185536061e+01\n", + "Dual infeasibility......: 1.8708625160522883e-11 1.8708625160522883e-11\n", + "Constraint violation....: 1.0035383635198514e-10 1.0035383635198514e-10\n", + "Complementarity.........: 2.5059110801775927e-09 2.5059110801775927e-09\n", + "Overall NLP error.......: 2.5059110801775927e-09 2.5059110801775927e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 66\n", + "Number of objective gradient evaluations = 34\n", + "Number of equality constraint evaluations = 66\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 35\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 33\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.097\n", + "Total CPU secs in NLP function evaluations = 0.054\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.8 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.63e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 3.99e+00 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.58e-02 3.85e+00 -1.0 4.28e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.53e-04 4.39e+00 -1.0 4.88e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.83e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8337334962743626e-11 5.8337334962743626e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8337334962743626e-11 5.8337334962743626e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.135\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 2.1061408e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 2.1040894e+00 1.61e-01 1.07e+00 -1.0 5.95e-01 - 9.79e-01 8.71e-01h 1\n", + " 2 2.1049797e+00 3.48e-02 1.10e+01 -1.0 6.04e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 2.0931025e+00 1.25e+01 5.73e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", + " 4 2.1188567e+00 7.15e-01 2.55e+00 -1.0 2.66e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 2.1126515e+00 5.85e-01 4.62e+01 -1.0 3.59e+02 - 4.61e-01 8.58e-02f 2\n", + " 6 2.1287303e+00 1.37e-01 5.55e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", + " 7 2.1259503e+00 4.30e-01 2.96e-01 -1.0 5.46e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 2.1272270e+00 3.91e-02 5.74e-02 -1.7 1.69e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 2.1269980e+00 5.05e-03 3.05e-03 -2.5 6.14e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 2.1253079e+00 3.93e-03 1.72e-03 -3.8 3.02e+00 - 1.00e+00 1.00e+00h 1\n", + " 11 2.1243236e+00 3.24e-05 7.52e-06 -3.8 8.12e-02 -4.5 1.00e+00 1.00e+00h 1\n", + " 12 2.1000010e+00 1.90e+00 1.12e-01 -5.7 6.66e+01 - 5.63e-01 1.00e+00h 1\n", + " 13 2.0975393e+00 2.59e-01 1.56e-02 -5.7 2.55e+01 - 7.81e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (319517)\n", + " 14 2.0918091e+00 4.08e-01 3.59e-02 -5.7 2.58e+01 - 6.49e-01 9.22e-01h 1\n", + " 15 2.0912149e+00 4.07e-01 3.58e-02 -5.7 3.86e+03 - 3.37e-02 2.90e-03h 2\n", + " 16 2.0907195e+00 2.80e-02 2.35e-03 -5.7 1.32e+01 - 1.00e+00 1.00e+00h 1\n", + " 17 2.0905779e+00 7.03e-03 8.20e-04 -5.7 6.05e+00 - 1.00e+00 1.00e+00h 1\n", + " 18 2.0905748e+00 5.81e-05 1.08e-06 -5.7 6.63e-01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (342692)\n", + " 19 2.0905749e+00 3.67e-09 1.14e-10 -5.7 5.27e-03 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 2.0899322e+00 1.67e-02 7.05e-04 -8.6 1.13e+01 - 9.43e-01 1.00e+00h 1\n", + " 21 2.0899164e+00 4.85e-03 2.15e-04 -8.6 6.22e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (360246)\n", + " 22 2.0899101e+00 4.30e-04 2.22e-05 -8.6 1.87e+00 - 1.00e+00 1.00e+00h 1\n", + " 23 2.0899096e+00 2.31e-06 2.07e-07 -8.6 1.38e-01 - 1.00e+00 1.00e+00h 1\n", + " 24 2.0899096e+00 1.00e-10 1.87e-11 -8.6 9.08e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 24\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 2.0899096060902953e+00 2.0899096060902953e+00\n", + "Dual infeasibility......: 1.8708625206653092e-11 1.8708625206653092e-11\n", + "Constraint violation....: 1.0035450248579991e-10 1.0035450248579991e-10\n", + "Complementarity.........: 2.5059110801021708e-09 2.5059110801021708e-09\n", + "Overall NLP error.......: 2.5059110801021708e-09 2.5059110801021708e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 31\n", + "Number of objective gradient evaluations = 25\n", + "Number of equality constraint evaluations = 31\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 25\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 24\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.848\n", + "Total CPU secs in NLP function evaluations = 0.031\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.6 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.38e-03 2.54e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 2.16e-09 1.48e-06 -1.0 1.75e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.1583757003895698e-09 2.1583757003895698e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.1583757003895698e-09 2.1583757003895698e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.103\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.2 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.38e-03 2.54e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 2.16e-09 1.48e-06 -1.0 1.75e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.1583757003895698e-09 2.1583757003895698e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.1583757003895698e-09 2.1583757003895698e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.105\n", + "Total CPU secs in NLP function evaluations = 0.005\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 4.60e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 6.23e+02 3.85e+02 -1.0 4.60e+03 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 7.16e+00 3.85e+00 -1.0 6.69e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 7.08e-02 4.39e+00 -1.0 7.64e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.55e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.122\n", + "Total CPU secs in NLP function evaluations = 0.009\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.1317732e+01 1.25e+00 1.07e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.1320290e+01 6.18e-01 5.16e+00 -1.0 4.65e+01 - 9.73e-01 5.05e-01h 1\n", + " 2 -1.1318564e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.1329088e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.1308507e+01 1.67e+01 2.23e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.1311584e+01 1.18e+01 3.19e+01 -1.0 1.36e+02 - 1.00e+00 4.14e-01f 2\n", + " 6 -1.1303505e+01 1.23e+01 3.32e+00 -1.0 9.19e+01 - 8.82e-01 3.38e-01f 2\n", + " 7 -1.1295920e+01 3.59e+00 5.30e-01 -1.0 5.20e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.1299210e+01 4.37e-01 7.59e-02 -1.0 1.08e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.1299229e+01 2.36e-04 1.85e-03 -1.7 3.04e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.1299413e+01 1.20e-03 1.91e-03 -3.8 7.92e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 -1.1299812e+01 6.26e-04 3.20e-04 -3.8 1.40e+00 -4.0 1.00e+00 1.00e+00h 1\n", + " 12 -1.1322678e+01 1.98e+01 1.23e-01 -5.7 8.86e+01 - 5.46e-01 1.00e+00h 1\n", + " 13 -1.1325322e+01 1.20e+00 1.52e-02 -5.7 6.84e+01 - 7.61e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (323203)\n", + " 14 -1.1329152e+01 3.27e+00 1.89e-02 -5.7 3.81e+01 - 7.29e-01 1.00e+00h 1\n", + " 15 -1.1329720e+01 3.61e+00 1.87e-02 -5.7 8.73e+02 - 1.61e-01 1.97e-02h 2\n", + " 16 -1.1330564e+01 3.94e-01 2.15e-03 -5.7 2.39e+01 - 1.00e+00 1.00e+00h 1\n", + " 17 -1.1330828e+01 1.37e-01 1.02e-03 -5.7 1.22e+01 - 1.00e+00 1.00e+00h 1\n", + " 18 -1.1330818e+01 1.25e-04 4.23e-06 -5.7 5.25e-01 - 1.00e+00 1.00e+00h 1\n", + " 19 -1.1330818e+01 4.23e-09 1.18e-10 -5.7 2.97e-03 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.1330839e+01 4.74e-06 1.48e-06 -8.6 4.43e-02 -4.5 1.00e+00 1.00e+00h 1\n", + " 21 -1.1330851e+01 4.21e-05 2.23e-06 -8.6 1.69e-01 -5.0 1.00e+00 1.00e+00h 1\n", + " 22 -1.1330889e+01 3.75e-04 3.31e-06 -8.6 5.08e-01 -5.4 1.00e+00 1.00e+00h 1\n", + " 23 -1.1330999e+01 3.26e-03 2.88e-05 -8.6 1.52e+00 -5.9 1.00e+00 1.00e+00h 1\n", + " 24 -1.1331064e+01 3.74e-03 3.29e-05 -8.6 4.44e+00 -6.4 1.00e+00 2.10e-01h 1\n", + " 25 -1.1331228e+01 1.65e-02 2.12e-04 -8.6 4.71e+00 -6.9 1.00e+00 7.65e-01f 1\n", + " 26 -1.1331449e+01 1.71e-02 7.80e-04 -8.6 2.93e+01 - 1.00e+00 3.63e-01f 1\n", + " 27 -1.1331483e+01 1.98e-02 6.89e-04 -8.6 2.97e+01 - 1.00e+00 2.88e-01f 1\n", + " 28 -1.1331484e+01 1.83e-02 6.23e-04 -8.6 1.76e+01 - 1.00e+00 9.54e-02f 1\n", + " 29 -1.1331489e+01 7.02e-03 1.41e-04 -8.6 3.15e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (355308)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -1.1331481e+01 2.07e-06 4.96e-08 -8.6 1.21e-01 - 1.00e+00 1.00e+00h 1\n", + " 31 -1.1331481e+01 4.19e-11 6.10e-14 -8.6 5.87e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 31\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.1331480835989968e+01 -1.1331480835989968e+01\n", + "Dual infeasibility......: 6.1028774360629264e-14 6.1028774360629264e-14\n", + "Constraint violation....: 4.1914582915580922e-11 4.1914582915580922e-11\n", + "Complementarity.........: 2.5059039158044294e-09 2.5059039158044294e-09\n", + "Overall NLP error.......: 2.5059039158044294e-09 2.5059039158044294e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 41\n", + "Number of objective gradient evaluations = 32\n", + "Number of equality constraint evaluations = 41\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 32\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 31\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.143\n", + "Total CPU secs in NLP function evaluations = 0.040\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.8 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.97e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 4.02e+00 3.85e+02 -1.0 1.92e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.61e-02 3.85e+00 -1.0 4.31e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.56e-04 4.39e+00 -1.0 4.91e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.86e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8332005892225425e-11 5.8332005892225425e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8332005892225425e-11 5.8332005892225425e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.128\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 1.8358716e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 1.8341696e+00 1.60e-01 1.07e+00 -1.0 5.95e-01 - 9.79e-01 8.72e-01h 1\n", + " 2 1.8349973e+00 3.48e-02 1.11e+01 -1.0 6.04e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 1.8248024e+00 1.25e+01 5.74e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", + " 4 1.8466954e+00 7.14e-01 2.53e+00 -1.0 2.66e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 1.8414860e+00 5.85e-01 4.65e+01 -1.0 3.56e+02 - 4.65e-01 8.65e-02f 2\n", + " 6 1.8551941e+00 1.36e-01 5.72e+00 -1.0 2.40e+01 -4.0 8.22e-01 1.00e+00F 1\n", + " 7 1.8528188e+00 4.32e-01 2.96e-01 -1.0 5.47e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 1.8539217e+00 3.88e-02 5.74e-02 -1.7 1.68e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 1.8537565e+00 5.12e-03 3.24e-03 -2.5 6.19e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 1.8525196e+00 2.82e-03 1.42e-03 -3.8 2.54e+00 - 1.00e+00 1.00e+00h 1\n", + " 11 1.8366692e+00 5.04e-01 3.65e-02 -3.8 3.44e+01 - 1.00e+00 1.00e+00h 1\n", + " 12 1.8399503e+00 1.30e-02 1.92e-03 -3.8 6.01e+00 - 1.00e+00 1.00e+00h 1\n", + " 13 1.8398900e+00 1.11e-05 6.33e-07 -3.8 2.77e-01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (320183)\n", + " 14 1.8323517e+00 4.84e-01 2.54e-02 -5.7 3.88e+01 - 7.65e-01 1.00e+00h 1\n", + " 15 1.8270480e+00 3.24e-01 1.26e-02 -5.7 3.83e+01 - 8.75e-01 1.00e+00h 1\n", + " 16 1.8263301e+00 3.14e-01 1.52e-02 -5.7 5.98e+02 - 1.79e-01 2.87e-02h 2\n", + " 17 1.8227920e+00 2.59e-01 1.45e-02 -5.7 2.36e+01 - 1.00e+00 1.00e+00h 1\n", + " 18 1.8227886e+00 1.83e-02 5.07e-04 -5.7 1.16e+01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (338938)\n", + " 19 1.8227858e+00 8.79e-04 1.36e-05 -5.7 2.56e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 1.8227857e+00 9.53e-07 6.96e-09 -5.7 8.47e-02 - 1.00e+00 1.00e+00h 1\n", + " 21 1.8221441e+00 1.90e-02 6.81e-04 -8.6 1.20e+01 - 9.37e-01 1.00e+00h 1\n", + " 22 1.8221300e+00 5.89e-03 2.23e-04 -8.6 6.84e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (356229)\n", + " 23 1.8221235e+00 6.31e-04 2.70e-05 -8.6 2.27e+00 - 1.00e+00 1.00e+00h 1\n", + " 24 1.8221230e+00 5.04e-06 3.54e-07 -8.6 2.03e-01 - 1.00e+00 1.00e+00h 1\n", + " 25 1.8221230e+00 3.93e-10 6.30e-11 -8.6 1.80e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 25\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 1.8221229556418135e+00 1.8221229556418135e+00\n", + "Dual infeasibility......: 6.2966011365859705e-11 6.2966011365859705e-11\n", + "Constraint violation....: 3.9269965057542322e-10 3.9269965057542322e-10\n", + "Complementarity.........: 2.5059288062561410e-09 2.5059288062561410e-09\n", + "Overall NLP error.......: 2.5059288062561410e-09 2.5059288062561410e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 33\n", + "Number of objective gradient evaluations = 26\n", + "Number of equality constraint evaluations = 33\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 26\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 25\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.855\n", + "Total CPU secs in NLP function evaluations = 0.037\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.7 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.37e-03 2.77e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 2.12e-09 1.47e-06 -1.0 1.73e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.1213626411054065e-09 2.1213626411054065e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.1213626411054065e-09 2.1213626411054065e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.107\n", + "Total CPU secs in NLP function evaluations = 0.001\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.37e-03 2.77e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 2.12e-09 1.47e-06 -1.0 1.73e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.1213617529269868e-09 2.1213617529269868e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.1213617529269868e-09 2.1213617529269868e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", + "Total CPU secs in NLP function evaluations = 0.007\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 0.9 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 5.36e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 6.30e+02 3.85e+02 -1.0 5.36e+03 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 7.23e+00 3.85e+00 -1.0 6.77e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 7.15e-02 4.39e+00 -1.0 7.71e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.63e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.134\n", + "Total CPU secs in NLP function evaluations = 0.002\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.1551525e+01 1.25e+00 1.07e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.1553742e+01 6.18e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", + " 2 -1.1552181e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.1561368e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.1543482e+01 1.67e+01 2.22e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.1546117e+01 1.18e+01 3.19e+01 -1.0 1.36e+02 - 1.00e+00 4.14e-01f 2\n", + " 6 -1.1539087e+01 1.23e+01 3.32e+00 -1.0 9.20e+01 - 8.80e-01 3.37e-01f 2\n", + " 7 -1.1532457e+01 3.60e+00 5.31e-01 -1.0 5.21e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.1535331e+01 4.40e-01 7.66e-02 -1.0 1.08e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.1535344e+01 2.36e-04 1.71e-03 -1.7 3.20e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.1535485e+01 9.17e-04 1.68e-03 -3.8 6.95e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 -1.1550192e+01 1.09e+01 5.80e-02 -3.8 6.98e+01 - 1.00e+00 1.00e+00h 1\n", + " 12 -1.1547006e+01 6.44e-01 3.84e-03 -3.8 1.74e+01 - 1.00e+00 1.00e+00h 1\n", + " 13 -1.1547077e+01 6.58e-05 2.09e-06 -3.8 5.97e-01 - 1.00e+00 1.00e+00h 1\n", + " 14 -1.1553620e+01 2.12e+00 2.46e-02 -5.7 3.77e+01 - 7.69e-01 1.00e+00h 1\n", + " 15 -1.1558721e+01 2.04e+00 1.06e-02 -5.7 4.14e+01 - 9.03e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (322351)\n", + " 16 -1.1559371e+01 2.71e+00 1.30e-02 -5.7 6.60e+02 - 1.85e-01 3.02e-02h 2\n", + " 17 -1.1562122e+01 1.66e+00 1.35e-02 -5.7 6.81e+01 - 1.00e+00 7.45e-01H 1\n", + " 18 -1.1562895e+01 1.70e-01 1.76e-03 -5.7 1.75e+01 - 1.00e+00 1.00e+00f 1\n", + " 19 -1.1562787e+01 1.94e-03 2.65e-05 -5.7 3.56e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.1562787e+01 3.18e-06 1.64e-08 -5.7 1.32e-01 - 1.00e+00 1.00e+00h 1\n", + " 21 -1.1563428e+01 1.52e-01 7.00e-04 -8.6 1.27e+01 - 9.31e-01 9.99e-01h 1\n", + "Reallocating memory for MA57: lfact (365434)\n", + " 22 -1.1563441e+01 6.92e-03 2.29e-04 -8.6 7.40e+00 - 1.00e+00 1.00e+00h 1\n", + " 23 -1.1563447e+01 8.62e-04 3.14e-05 -8.6 2.65e+00 - 1.00e+00 1.00e+00h 1\n", + " 24 -1.1563448e+01 9.50e-06 5.44e-07 -8.6 2.79e-01 - 1.00e+00 1.00e+00h 1\n", + " 25 -1.1563448e+01 1.19e-09 1.68e-10 -8.6 3.13e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 25\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.1563448115914573e+01 -1.1563448115914573e+01\n", + "Dual infeasibility......: 1.6756190666721757e-10 1.6756190666721757e-10\n", + "Constraint violation....: 1.1895755491764248e-09 1.1895755491764248e-09\n", + "Complementarity.........: 2.5059705354210223e-09 2.5059705354210223e-09\n", + "Overall NLP error.......: 2.5059705354210223e-09 2.5059705354210223e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 36\n", + "Number of objective gradient evaluations = 26\n", + "Number of equality constraint evaluations = 36\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 26\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 25\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.877\n", + "Total CPU secs in NLP function evaluations = 0.040\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.7 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 2.31e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 4.05e+00 3.85e+02 -1.0 2.26e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.64e-02 3.85e+00 -1.0 4.34e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.59e-04 4.39e+00 -1.0 4.95e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.89e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8339111319583026e-11 5.8339111319583026e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8339111319583026e-11 5.8339111319583026e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.130\n", + "Total CPU secs in NLP function evaluations = 0.005\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 1.6020788e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 1.6006263e+00 1.59e-01 1.06e+00 -1.0 5.95e-01 - 9.79e-01 8.73e-01h 1\n", + " 2 1.6013928e+00 3.48e-02 1.11e+01 -1.0 6.04e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 1.5924626e+00 1.25e+01 5.74e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", + " 4 1.6115012e+00 7.14e-01 2.52e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 1.6070137e+00 5.85e-01 4.67e+01 -1.0 3.54e+02 - 4.67e-01 8.70e-02f 2\n", + " 6 1.6189485e+00 1.36e-01 5.81e+00 -1.0 2.40e+01 -4.0 8.22e-01 1.00e+00F 1\n", + " 7 1.6168842e+00 4.32e-01 2.97e-01 -1.0 5.47e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 1.6178560e+00 3.87e-02 5.74e-02 -1.7 1.68e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 1.6177317e+00 5.19e-03 3.40e-03 -2.5 6.23e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 1.6167878e+00 2.10e-03 1.20e-03 -3.8 2.17e+00 - 1.00e+00 1.00e+00h 1\n", + " 11 1.6042274e+00 4.07e-01 2.56e-02 -3.8 3.07e+01 - 1.00e+00 1.00e+00h 1\n", + " 12 1.6065666e+00 8.43e-03 1.13e-03 -3.8 4.88e+00 - 1.00e+00 1.00e+00h 1\n", + " 13 1.6065263e+00 4.74e-06 3.38e-07 -3.8 1.65e-01 - 1.00e+00 1.00e+00h 1\n", + " 14 1.6002214e+00 9.01e-01 7.69e-02 -5.7 9.37e+03 - 1.90e-02 7.73e-03f 1\n", + "Reallocating memory for MA57: lfact (349533)\n", + " 15 1.5979843e+00 1.46e-01 1.53e-02 -5.7 3.06e+01 - 8.53e-01 1.00e+00h 1\n", + " 16 1.5934679e+00 3.30e-01 2.00e-02 -5.7 4.37e+01 - 8.47e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (378883)\n", + " 17 1.5941729e+00 2.50e-03 1.57e-04 -5.7 3.14e-01 -4.5 1.00e+00 1.00e+00h 1\n", + " 18 1.5941556e+00 1.90e-06 8.52e-07 -5.7 7.65e-02 -5.0 1.00e+00 1.00e+00h 1\n", + " 19 1.5941055e+00 2.45e-05 6.85e-06 -8.6 1.93e-01 -5.4 9.98e-01 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 1.5940068e+00 2.13e-04 1.22e-05 -8.6 5.73e-01 -5.9 1.00e+00 1.00e+00h 1\n", + " 21 1.5937364e+00 1.77e-03 1.01e-04 -8.6 1.67e+00 -6.4 1.00e+00 1.00e+00h 1\n", + " 22 1.5931017e+00 1.24e-02 7.08e-04 -8.6 4.73e+00 -6.9 1.00e+00 9.59e-01h 1\n", + " 23 1.5922166e+00 4.10e-02 2.42e-03 -8.6 1.30e+01 -7.3 1.00e+00 6.17e-01f 1\n", + " 24 1.5911477e+00 1.11e-01 1.25e-02 -8.6 1.93e+01 -7.8 1.00e+00 1.00e+00f 1\n", + " 25 1.5905350e+00 3.48e-02 2.72e-03 -8.6 9.72e+00 -7.4 1.00e+00 1.00e+00h 1\n", + " 26 1.5903800e+00 3.48e-02 2.76e-03 -8.6 4.21e+01 -7.9 1.00e+00 7.61e-02h 1\n", + " 27 1.5901978e+00 7.47e-02 1.56e-03 -8.6 5.28e+01 - 1.00e+00 4.36e-01f 1\n", + " 28 1.5901521e+00 1.15e-01 6.10e-04 -8.6 4.20e+01 - 1.00e+00 6.10e-01f 1\n", + " 29 1.5901546e+00 9.61e-03 5.06e-05 -8.6 8.82e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (415680)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 1.5901559e+00 6.03e-03 5.58e-06 -8.6 6.93e+00 - 1.00e+00 1.00e+00h 1\n", + " 31 1.5901557e+00 3.61e-04 2.83e-07 -8.6 1.72e+00 - 1.00e+00 1.00e+00h 1\n", + " 32 1.5901557e+00 1.01e-06 7.50e-10 -8.6 9.08e-02 - 1.00e+00 1.00e+00h 1\n", + " 33 1.5901557e+00 6.89e-12 4.35e-14 -8.6 2.38e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 33\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 1.5901556757175646e+00 1.5901556757175646e+00\n", + "Dual infeasibility......: 4.3539707029773698e-14 4.3539707029773698e-14\n", + "Constraint violation....: 6.8930416929902094e-12 6.8930416929902094e-12\n", + "Complementarity.........: 2.5059036071950574e-09 2.5059036071950574e-09\n", + "Overall NLP error.......: 2.5059036071950574e-09 2.5059036071950574e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 38\n", + "Number of objective gradient evaluations = 34\n", + "Number of equality constraint evaluations = 38\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 34\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 33\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.558\n", + "Total CPU secs in NLP function evaluations = 0.050\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.2 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.36e-03 3.00e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 2.08e-09 1.46e-06 -1.0 1.72e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.0846711024091746e-09 2.0846711024091746e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.0846711024091746e-09 2.0846711024091746e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", + "Total CPU secs in NLP function evaluations = 0.003\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.36e-03 3.00e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 2.08e-09 1.46e-06 -1.0 1.72e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.0846711024091746e-09 2.0846711024091746e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.0846711024091746e-09 2.0846711024091746e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.115\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.13e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 6.38e+02 3.85e+02 -1.0 6.13e+03 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 7.31e+00 3.85e+00 -1.0 6.85e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 7.23e-02 4.39e+00 -1.0 7.79e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.70e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.134\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.1757533e+01 1.25e+00 1.06e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.1759489e+01 6.18e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", + " 2 -1.1758067e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.1766218e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.1750403e+01 1.67e+01 2.21e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.1752706e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.13e-01f 2\n", + " 6 -1.1746485e+01 1.23e+01 3.32e+00 -1.0 9.21e+01 - 8.79e-01 3.37e-01f 2\n", + " 7 -1.1740596e+01 3.61e+00 5.32e-01 -1.0 5.22e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.1743147e+01 4.42e-01 7.72e-02 -1.0 1.09e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.1743157e+01 2.36e-04 1.60e-03 -1.7 3.33e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.1743267e+01 7.25e-04 1.49e-03 -3.8 6.20e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 -1.1755095e+01 8.91e+00 4.24e-02 -5.7 6.38e+01 - 6.52e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (321581)\n", + " 12 -1.1761884e+01 2.85e+00 1.21e-02 -5.7 5.80e+01 - 8.37e-01 1.00e+00h 1\n", + " 13 -1.1766537e+01 6.45e+00 3.35e-02 -5.7 4.38e+01 - 7.40e-01 1.00e+00h 1\n", + " 14 -1.1766408e+01 5.42e+00 4.34e-02 -5.7 1.45e+01 -4.0 9.76e-01 1.60e-01h 1\n", + " 15 -1.1766359e+01 4.32e-01 3.46e-02 -5.7 2.48e+01 - 1.00e+00 1.00e+00h 1\n", + " 16 -1.1766255e+01 4.65e-04 1.06e-04 -5.7 2.81e-01 -4.5 1.00e+00 1.00e+00h 1\n", + " 17 -1.1766639e+01 3.37e-01 3.56e-03 -5.7 2.92e+02 - 4.58e-01 5.22e-02h 2\n", + " 18 -1.1767572e+01 9.44e-02 2.21e-03 -5.7 1.59e+01 - 1.00e+00 1.00e+00h 1\n", + " 19 -1.1767397e+01 5.01e-03 9.03e-05 -5.7 3.57e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.1767399e+01 1.54e-05 1.56e-06 -5.7 2.76e-01 - 1.00e+00 1.00e+00h 1\n", + " 21 -1.1768035e+01 1.89e-01 7.56e-04 -8.6 1.38e+01 - 9.26e-01 9.95e-01h 1\n", + "Reallocating memory for MA57: lfact (343029)\n", + " 22 -1.1768050e+01 8.01e-03 2.35e-04 -8.6 7.95e+00 - 1.00e+00 1.00e+00h 1\n", + " 23 -1.1768057e+01 1.12e-03 3.57e-05 -8.6 3.02e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (360465)\n", + " 24 -1.1768058e+01 1.63e-05 7.80e-07 -8.6 3.65e-01 - 1.00e+00 1.00e+00h 1\n", + " 25 -1.1768058e+01 3.05e-09 3.84e-10 -8.6 5.01e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 25\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.1768057697799625e+01 -1.1768057697799625e+01\n", + "Dual infeasibility......: 3.8359587305625804e-10 3.8359587305625804e-10\n", + "Constraint violation....: 3.0509063053685281e-09 3.0509063053685281e-09\n", + "Complementarity.........: 2.5060563776015687e-09 2.5060563776015687e-09\n", + "Overall NLP error.......: 3.0509063053685281e-09 3.0509063053685281e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 35\n", + "Number of objective gradient evaluations = 26\n", + "Number of equality constraint evaluations = 35\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 26\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 25\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.901\n", + "Total CPU secs in NLP function evaluations = 0.037\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.7 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 2.65e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 4.08e+00 3.85e+02 -1.0 2.60e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.67e-02 3.85e+00 -1.0 4.37e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.62e-04 4.39e+00 -1.0 4.98e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.92e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.133\n", + "Total CPU secs in NLP function evaluations = 0.005\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 1.3960708e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 1.3948050e+00 1.58e-01 1.05e+00 -1.0 5.95e-01 - 9.79e-01 8.73e-01h 1\n", + " 2 1.3955154e+00 3.48e-02 1.12e+01 -1.0 6.04e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 1.3875708e+00 1.25e+01 5.74e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", + " 4 1.4044133e+00 7.13e-01 2.51e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 1.4004729e+00 5.85e-01 4.68e+01 -1.0 3.52e+02 - 4.69e-01 8.75e-02f 2\n", + " 6 1.4110288e+00 1.36e-01 5.85e+00 -1.0 2.40e+01 -4.0 8.22e-01 1.00e+00F 1\n", + " 7 1.4092120e+00 4.33e-01 2.97e-01 -1.0 5.48e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 1.4100813e+00 3.86e-02 5.75e-02 -1.7 1.68e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 1.4099850e+00 5.24e-03 3.52e-03 -2.5 6.26e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 1.4092414e+00 1.61e-03 1.03e-03 -3.8 1.89e+00 - 1.00e+00 1.00e+00h 1\n", + " 11 1.4087035e+00 1.89e-05 1.98e-06 -3.8 5.64e-02 -4.5 1.00e+00 1.00e+00h 1\n", + " 12 1.3969745e+00 9.17e-01 3.82e-02 -5.7 4.48e+01 - 6.43e-01 1.00e+00h 1\n", + " 13 1.3917215e+00 4.19e-01 9.20e-03 -5.7 3.84e+01 - 8.02e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (324236)\n", + " 14 1.3869756e+00 5.56e-01 3.16e-02 -5.7 2.67e+01 - 7.65e-01 1.00e+00h 1\n", + " 15 1.3879570e+00 9.86e-03 5.26e-04 -5.7 5.35e-01 -5.0 1.00e+00 1.00e+00h 1\n", + " 16 1.3879136e+00 3.18e-06 4.27e-07 -5.7 6.95e-02 -5.4 1.00e+00 1.00e+00h 1\n", + " 17 1.3878380e+00 9.80e-05 2.86e-05 -8.6 3.89e-01 -5.9 9.93e-01 1.00e+00h 1\n", + " 18 1.3876686e+00 9.18e-04 5.19e-05 -8.6 1.23e+00 -6.4 1.00e+00 1.00e+00h 1\n", + " 19 1.3875598e+00 9.24e-04 5.14e-05 -8.6 1.98e+00 -6.9 1.00e+00 3.25e-01h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 1.3870548e+00 1.12e-02 6.48e-04 -8.6 4.76e+00 -7.3 1.00e+00 1.00e+00f 1\n", + " 21 1.3861210e+00 1.22e-01 1.05e-02 -8.6 2.04e+01 -7.8 1.00e+00 1.00e+00h 1\n", + " 22 1.3856832e+00 2.64e-02 2.12e-03 -8.6 9.07e+00 -7.4 1.00e+00 9.62e-01h 1\n", + " 23 1.3856413e+00 2.12e-02 1.73e-03 -8.6 3.86e+01 - 1.00e+00 2.02e-01f 1\n", + "Reallocating memory for MA57: lfact (344436)\n", + " 24 1.3856015e+00 1.68e-02 1.37e-03 -8.6 4.03e+01 - 1.00e+00 2.09e-01f 1\n", + " 25 1.3855581e+00 5.06e-02 6.97e-04 -8.6 3.68e+01 - 1.00e+00 4.94e-01f 1\n", + " 26 1.3855412e+00 1.19e-03 6.42e-05 -8.6 3.12e+00 - 1.00e+00 1.00e+00h 1\n", + " 27 1.3855461e+00 9.23e-04 7.59e-07 -8.6 2.74e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (362997)\n", + " 28 1.3855461e+00 9.77e-06 6.77e-09 -8.6 2.83e-01 - 1.00e+00 1.00e+00h 1\n", + " 29 1.3855461e+00 7.29e-10 5.06e-13 -8.6 2.45e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 29\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 1.3855460938286592e+00 1.3855460938286592e+00\n", + "Dual infeasibility......: 5.0636527904345562e-13 5.0636527904345562e-13\n", + "Constraint violation....: 7.2855699251306305e-10 7.2855699251306305e-10\n", + "Complementarity.........: 2.5059081084119374e-09 2.5059081084119374e-09\n", + "Overall NLP error.......: 2.5059081084119374e-09 2.5059081084119374e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 34\n", + "Number of objective gradient evaluations = 30\n", + "Number of equality constraint evaluations = 34\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 30\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 29\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.307\n", + "Total CPU secs in NLP function evaluations = 0.036\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.35e-03 3.23e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 2.05e-09 1.46e-06 -1.0 1.70e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.0483019724792939e-09 2.0483019724792939e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.0483019724792939e-09 2.0483019724792939e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.099\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.35e-03 3.23e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 2.05e-09 1.46e-06 -1.0 1.70e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.0483028606577136e-09 2.0483028606577136e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.0483028606577136e-09 2.0483028606577136e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.099\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.89e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 6.46e+02 3.85e+02 -1.0 6.89e+03 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 7.39e+00 3.85e+00 -1.0 6.92e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 7.30e-02 4.39e+00 -1.0 7.87e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.78e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0394678409211338e-09 9.0394678409211338e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0394678409211338e-09 9.0394678409211338e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.126\n", + "Total CPU secs in NLP function evaluations = 0.005\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.1941668e+01 1.25e+00 1.06e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.1943417e+01 6.18e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", + " 2 -1.1942113e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.1949438e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.1935265e+01 1.67e+01 2.21e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.1937309e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.13e-01f 2\n", + " 6 -1.1931730e+01 1.23e+01 3.32e+00 -1.0 9.21e+01 - 8.78e-01 3.37e-01f 2\n", + " 7 -1.1926433e+01 3.62e+00 5.33e-01 -1.0 5.22e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.1928727e+01 4.44e-01 7.77e-02 -1.0 1.09e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.1928733e+01 2.37e-04 1.51e-03 -1.7 3.43e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.1928823e+01 5.88e-04 1.35e-03 -3.8 5.59e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 -1.1929034e+01 3.59e-04 1.83e-04 -5.7 1.06e+00 -4.0 1.00e+00 1.00e+00h 1\n", + " 12 -1.1929754e+01 4.19e-03 1.21e-04 -5.7 3.63e+00 -4.5 1.00e+00 1.00e+00h 1\n", + " 13 -1.1930039e+01 3.14e-03 1.97e-03 -5.7 3.45e+00 -5.0 1.00e+00 4.14e-01h 1\n", + " 14 -1.1930215e+01 3.02e-03 9.79e-04 -5.7 8.14e-01 -5.4 1.00e+00 1.00e+00f 1\n", + " 15 -1.1930791e+01 2.61e-02 4.88e-05 -5.7 2.66e+00 -5.9 1.00e+00 1.00e+00h 1\n", + " 16 -1.1939335e+01 1.10e+01 4.69e-02 -5.7 8.17e+03 - 1.91e-02 9.26e-03h 1\n", + " 17 -1.1943119e+01 1.42e+01 5.92e-02 -5.7 4.18e+02 - 4.56e-01 7.84e-02h 1\n", + " 18 -1.1956668e+01 1.38e+01 5.53e-02 -5.7 1.09e+02 - 1.00e+00 8.15e-01h 1\n", + " 19 -1.1949881e+01 3.43e+00 1.16e-02 -5.7 5.57e+01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (321695)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.1950414e+01 8.23e-03 6.43e-04 -5.7 6.89e+00 - 1.00e+00 1.00e+00h 1\n", + " 21 -1.1950429e+01 3.89e-04 2.65e-05 -5.7 1.74e+00 - 1.00e+00 1.00e+00h 1\n", + " 22 -1.1950430e+01 6.41e-07 3.85e-08 -5.7 7.08e-02 - 1.00e+00 1.00e+00h 1\n", + " 23 -1.1951063e+01 2.31e-01 8.26e-04 -8.6 1.51e+01 - 9.21e-01 9.90e-01h 1\n", + "Reallocating memory for MA57: lfact (348619)\n", + " 24 -1.1951080e+01 9.09e-03 2.41e-04 -8.6 8.45e+00 - 1.00e+00 1.00e+00h 1\n", + " 25 -1.1951086e+01 1.41e-03 3.96e-05 -8.6 3.38e+00 - 1.00e+00 1.00e+00h 1\n", + " 26 -1.1951087e+01 2.57e-05 1.05e-06 -8.6 4.59e-01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (367172)\n", + " 27 -1.1951087e+01 7.10e-09 7.68e-10 -8.6 7.64e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 27\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.1951087152217010e+01 -1.1951087152217010e+01\n", + "Dual infeasibility......: 7.6785714050494160e-10 7.6785714050494160e-10\n", + "Constraint violation....: 7.1010632929358053e-09 7.1010632929358053e-09\n", + "Complementarity.........: 2.5062084307979245e-09 2.5062084307979245e-09\n", + "Overall NLP error.......: 7.1010632929358053e-09 7.1010632929358053e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 34\n", + "Number of objective gradient evaluations = 28\n", + "Number of equality constraint evaluations = 34\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 28\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 27\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.951\n", + "Total CPU secs in NLP function evaluations = 0.031\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.7 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 2.99e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 4.11e+00 3.85e+02 -1.0 2.94e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.71e-02 3.85e+00 -1.0 4.41e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.65e-04 4.39e+00 -1.0 5.01e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.95e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.135\n", + "Total CPU secs in NLP function evaluations = 0.002\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 1.2119360e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 1.2108149e+00 1.58e-01 1.05e+00 -1.0 5.95e-01 - 9.79e-01 8.74e-01h 1\n", + " 2 1.2114751e+00 3.48e-02 1.12e+01 -1.0 6.04e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 1.2043202e+00 1.25e+01 5.74e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", + " 4 1.2194210e+00 7.13e-01 2.50e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 1.2159091e+00 5.85e-01 4.69e+01 -1.0 3.51e+02 - 4.71e-01 8.78e-02f 2\n", + " 6 1.2253787e+00 1.36e-01 5.90e+00 -1.0 2.40e+01 -4.0 8.22e-01 1.00e+00F 1\n", + " 7 1.2237509e+00 4.33e-01 2.97e-01 -1.0 5.48e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 1.2245368e+00 3.86e-02 5.75e-02 -1.7 1.68e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 1.2244603e+00 5.28e-03 3.61e-03 -2.5 6.28e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 1.2238595e+00 1.27e-03 8.99e-04 -3.8 1.66e+00 - 1.00e+00 1.00e+00h 1\n", + " 11 1.2147814e+00 6.50e-01 3.09e-02 -5.7 3.86e+01 - 6.91e-01 1.00e+00h 1\n", + " 12 1.2082820e+00 6.40e-01 9.84e-03 -5.7 4.61e+01 - 8.50e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (319971)\n", + " 13 1.2037280e+00 6.91e-01 3.73e-02 -5.7 2.97e+01 - 7.52e-01 9.98e-01h 1\n", + " 14 1.2048711e+00 1.91e-02 1.61e-03 -5.7 3.27e+00 -4.5 9.31e-01 1.00e+00h 1\n", + " 15 1.2048031e+00 2.24e-06 5.66e-07 -5.7 2.40e-02 -5.0 1.00e+00 1.00e+00h 1\n", + " 16 1.2047671e+00 1.36e-05 6.26e-06 -8.6 1.52e-01 -5.4 9.99e-01 1.00e+00h 1\n", + " 17 1.2047082e+00 1.26e-04 6.76e-06 -8.6 4.70e-01 -5.9 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (336051)\n", + " 18 1.2045445e+00 1.05e-03 5.65e-05 -8.6 1.37e+00 -6.4 1.00e+00 1.00e+00h 1\n", + " 19 1.2044052e+00 1.58e-03 8.51e-05 -8.6 3.73e+00 -6.9 1.00e+00 3.42e-01h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 1.2039397e+00 9.51e-03 5.22e-04 -8.6 4.63e+00 -7.3 1.00e+00 1.00e+00f 1\n", + " 21 1.2031551e+00 8.32e-02 6.07e-03 -8.6 1.92e+01 -7.8 1.00e+00 8.71e-01h 1\n", + " 22 1.2028414e+00 1.08e-01 7.48e-03 -8.6 1.19e+03 -8.3 7.23e-02 8.23e-03h 1\n", + " 23 1.2025270e+00 5.89e-02 4.04e-03 -8.6 3.92e+01 - 1.00e+00 4.53e-01f 1\n", + " 24 1.2025096e+00 4.44e-02 3.04e-03 -8.6 3.68e+01 - 5.41e-01 2.48e-01f 1\n", + " 25 1.2025058e+00 4.33e-02 2.05e-03 -8.6 3.17e+01 - 1.00e+00 3.26e-01f 1\n", + " 26 1.2025106e+00 1.21e-03 1.32e-04 -8.6 2.37e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (356718)\n", + " 27 1.2025167e+00 5.23e-04 3.85e-07 -8.6 2.06e+00 - 1.00e+00 1.00e+00h 1\n", + " 28 1.2025166e+00 3.31e-06 2.06e-09 -8.6 1.65e-01 - 1.00e+00 1.00e+00h 1\n", + " 29 1.2025166e+00 8.35e-11 7.43e-14 -8.6 8.28e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 29\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 1.2025166394012419e+00 1.2025166394012419e+00\n", + "Dual infeasibility......: 7.4296667615744834e-14 7.4296667615744834e-14\n", + "Constraint violation....: 8.3521300986433289e-11 8.3521300986433289e-11\n", + "Complementarity.........: 2.5059040285569865e-09 2.5059040285569865e-09\n", + "Overall NLP error.......: 2.5059040285569865e-09 2.5059040285569865e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 34\n", + "Number of objective gradient evaluations = 30\n", + "Number of equality constraint evaluations = 34\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 30\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 29\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.243\n", + "Total CPU secs in NLP function evaluations = 0.037\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.33e-03 3.46e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 2.01e-09 1.45e-06 -1.0 1.69e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.0122556954049742e-09 2.0122556954049742e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.0122556954049742e-09 2.0122556954049742e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", + "Total CPU secs in NLP function evaluations = 0.003\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.2 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.33e-03 3.46e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 2.01e-09 1.45e-06 -1.0 1.69e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.0122561394941840e-09 2.0122561394941840e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.0122561394941840e-09 2.0122561394941840e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 0.9 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 7.66e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 6.53e+02 3.85e+02 -1.0 7.66e+03 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 7.46e+00 3.85e+00 -1.0 7.00e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 7.38e-02 4.39e+00 -1.0 7.94e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.85e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.127\n", + "Total CPU secs in NLP function evaluations = 0.005\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.2108134e+01 1.25e+00 1.06e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.2109716e+01 6.18e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", + " 2 -1.2108512e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.2115163e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.2102323e+01 1.67e+01 2.21e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.2104160e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.13e-01f 2\n", + " 6 -1.2099103e+01 1.23e+01 3.32e+00 -1.0 9.22e+01 - 8.77e-01 3.36e-01f 2\n", + " 7 -1.2094289e+01 3.63e+00 5.34e-01 -1.0 5.23e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.2096373e+01 4.45e-01 7.81e-02 -1.0 1.09e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.2096378e+01 2.38e-04 1.44e-03 -1.7 3.51e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.2096452e+01 4.87e-04 1.23e-03 -3.8 5.09e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 -1.2104600e+01 6.30e+00 3.11e-02 -5.7 5.44e+01 - 6.88e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (319533)\n", + " 12 -1.2111071e+01 3.68e+00 1.30e-02 -5.7 6.59e+01 - 8.37e-01 1.00e+00h 1\n", + " 13 -1.2115222e+01 6.14e+00 2.84e-02 -5.7 3.51e+01 - 8.03e-01 1.00e+00h 1\n", + " 14 -1.2115127e+01 5.38e+00 4.94e-02 -5.7 1.50e+01 -4.0 1.00e+00 1.24e-01h 1\n", + " 15 -1.2114999e+01 4.47e-01 2.64e-02 -5.7 2.58e+01 - 1.00e+00 1.00e+00h 1\n", + " 16 -1.2114900e+01 4.96e-04 8.86e-05 -5.7 3.02e-01 -4.5 1.00e+00 1.00e+00h 1\n", + " 17 -1.2115229e+01 5.02e-01 4.88e-03 -5.7 6.65e+02 - 1.86e-01 2.67e-02h 2\n", + " 18 -1.2116201e+01 2.32e-01 2.86e-03 -5.7 1.63e+01 - 1.00e+00 1.00e+00h 1\n", + " 19 -1.2115996e+01 6.36e-03 8.56e-05 -5.7 2.06e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (337748)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.2116001e+01 1.11e-05 1.53e-06 -5.7 2.94e-01 - 1.00e+00 1.00e+00h 1\n", + " 21 -1.2116631e+01 2.76e-01 8.95e-04 -8.6 1.65e+01 - 9.16e-01 9.85e-01h 1\n", + " 22 -1.2116650e+01 1.02e-02 2.45e-04 -8.6 8.93e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (355121)\n", + " 23 -1.2116656e+01 1.72e-03 4.32e-05 -8.6 3.73e+00 - 1.00e+00 1.00e+00h 1\n", + " 24 -1.2116657e+01 3.81e-05 1.36e-06 -8.6 5.59e-01 - 1.00e+00 1.00e+00h 1\n", + " 25 -1.2116657e+01 1.56e-08 1.40e-09 -8.6 1.13e-02 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (375561)\n", + " 26 -1.2116658e+01 6.53e-07 1.86e-08 -9.0 7.32e-02 - 1.00e+00 1.00e+00h 1\n", + " 27 -1.2116658e+01 1.00e-11 4.32e-13 -9.0 2.87e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 27\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.2116658032211422e+01 -1.2116658032211422e+01\n", + "Dual infeasibility......: 4.3248756227463823e-13 4.3248756227463823e-13\n", + "Constraint violation....: 9.9974473144470721e-12 9.9974473144470721e-12\n", + "Complementarity.........: 9.0909106880328588e-10 9.0909106880328588e-10\n", + "Overall NLP error.......: 9.0909106880328588e-10 9.0909106880328588e-10\n", + "\n", + "\n", + "Number of objective function evaluations = 37\n", + "Number of objective gradient evaluations = 28\n", + "Number of equality constraint evaluations = 37\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 28\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 27\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.055\n", + "Total CPU secs in NLP function evaluations = 0.036\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.8 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.33e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 4.15e+00 3.85e+02 -1.0 3.28e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.74e-02 3.85e+00 -1.0 4.44e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.69e-04 4.39e+00 -1.0 5.04e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.98e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8339111319583026e-11 5.8339111319583026e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8339111319583026e-11 5.8339111319583026e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.131\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 1.0454702e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 1.0444646e+00 1.58e-01 1.05e+00 -1.0 5.95e-01 - 9.79e-01 8.74e-01h 1\n", + " 2 1.0450802e+00 3.48e-02 1.12e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 1.0385721e+00 1.25e+01 5.74e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", + " 4 1.0522576e+00 7.13e-01 2.50e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 1.0490906e+00 5.85e-01 4.70e+01 -1.0 3.50e+02 - 4.73e-01 8.81e-02f 2\n", + " 6 1.0576821e+00 1.36e-01 5.97e+00 -1.0 2.40e+01 -4.0 8.22e-01 1.00e+00F 1\n", + " 7 1.0562026e+00 4.33e-01 2.97e-01 -1.0 5.48e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 1.0569195e+00 3.85e-02 5.75e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 1.0568573e+00 5.31e-03 3.69e-03 -2.5 6.30e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 1.0563619e+00 1.01e-03 7.93e-04 -3.8 1.47e+00 - 1.00e+00 1.00e+00h 1\n", + " 11 1.0487134e+00 5.52e-01 2.96e-02 -5.7 3.53e+01 - 7.06e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (319271)\n", + " 12 1.0424427e+00 7.03e-01 1.05e-02 -5.7 4.80e+01 - 8.51e-01 1.00e+00h 1\n", + " 13 1.0381157e+00 7.10e-01 3.48e-02 -5.7 3.01e+01 - 7.81e-01 1.00e+00h 1\n", + " 14 1.0390532e+00 1.28e-02 1.27e-03 -5.7 6.87e-01 -4.5 9.40e-01 1.00e+00h 1\n", + " 15 1.0390112e+00 4.50e-06 9.50e-07 -5.7 8.32e-02 -5.0 1.00e+00 1.00e+00h 1\n", + " 16 1.0389810e+00 7.21e-06 4.11e-06 -8.6 1.03e-01 -5.4 9.99e-01 1.00e+00h 1\n", + " 17 1.0389364e+00 8.39e-05 3.92e-06 -8.6 3.70e-01 -5.9 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (335399)\n", + " 18 1.0388452e+00 3.85e-04 1.79e-05 -8.6 1.04e+00 -6.4 1.00e+00 7.44e-01h 1\n", + " 19 1.0386655e+00 1.98e-03 6.30e-05 -8.6 1.81e+00 -6.9 1.00e+00 1.00e+00f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 1.0382979e+00 7.70e-03 3.44e-04 -8.6 3.97e+00 -7.3 1.00e+00 1.00e+00h 1\n", + " 21 1.0375873e+00 7.24e-02 5.06e-03 -8.6 1.56e+01 -7.8 1.00e+00 1.00e+00h 1\n", + " 22 1.0372258e+00 1.16e-01 7.43e-03 -8.6 5.45e+02 -8.3 1.62e-01 2.38e-02h 1\n", + " 23 1.0369759e+00 6.56e-02 4.17e-03 -8.6 3.95e+01 - 1.00e+00 4.33e-01f 1\n", + " 24 1.0369555e+00 5.05e-02 3.21e-03 -8.6 3.72e+01 - 1.00e+00 2.30e-01f 1\n", + " 25 1.0369460e+00 4.41e-02 2.04e-03 -8.6 3.24e+01 - 1.00e+00 3.64e-01f 1\n", + " 26 1.0369409e+00 1.26e-03 1.02e-04 -8.6 2.22e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (358185)\n", + " 27 1.0369464e+00 3.96e-04 2.65e-07 -8.6 1.80e+00 - 1.00e+00 1.00e+00h 1\n", + " 28 1.0369464e+00 1.87e-06 1.06e-09 -8.6 1.24e-01 - 1.00e+00 1.00e+00h 1\n", + " 29 1.0369464e+00 2.65e-11 3.91e-14 -8.6 4.66e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 29\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 1.0369464064985126e+00 1.0369464064985126e+00\n", + "Dual infeasibility......: 3.9074655964464579e-14 3.9074655964464579e-14\n", + "Constraint violation....: 2.6508573114369938e-11 2.6508573114369938e-11\n", + "Complementarity.........: 2.5059036949607426e-09 2.5059036949607426e-09\n", + "Overall NLP error.......: 2.5059036949607426e-09 2.5059036949607426e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 34\n", + "Number of objective gradient evaluations = 30\n", + "Number of equality constraint evaluations = 34\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 30\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 29\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.435\n", + "Total CPU secs in NLP function evaluations = 0.045\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.41e-03 2.08e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 2.23e-09 1.50e-06 -1.0 1.78e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.2337816041329006e-09 2.2337816041329006e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.2337816041329006e-09 2.2337816041329006e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.104\n", + "Total CPU secs in NLP function evaluations = 0.005\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.32e-03 3.69e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 1.98e-09 1.44e-06 -1.0 1.67e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.9765331593646351e-09 1.9765331593646351e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.9765331593646351e-09 1.9765331593646351e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.107\n", + "Total CPU secs in NLP function evaluations = 0.003\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.2 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 8.43e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 6.61e+02 3.85e+02 -1.0 8.43e+03 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 7.54e+00 3.85e+00 -1.0 7.08e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 7.46e-02 4.39e+00 -1.0 8.02e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.93e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.128\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.2260028e+01 1.25e+00 1.05e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.2261473e+01 6.18e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", + " 2 -1.2260355e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.2266446e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.2254710e+01 1.67e+01 2.20e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.2256378e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.13e-01f 2\n", + " 6 -1.2251753e+01 1.23e+01 3.32e+00 -1.0 9.22e+01 - 8.77e-01 3.36e-01f 2\n", + " 7 -1.2247342e+01 3.64e+00 5.35e-01 -1.0 5.23e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.2249251e+01 4.47e-01 7.84e-02 -1.0 1.10e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.2249255e+01 2.39e-04 1.38e-03 -1.7 3.57e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.2249317e+01 4.10e-04 1.13e-03 -3.8 4.68e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 -1.2249467e+01 2.64e-04 1.34e-04 -5.7 9.10e-01 -4.0 1.00e+00 1.00e+00h 1\n", + " 12 -1.2250000e+01 3.31e-03 1.08e-04 -5.7 3.23e+00 -4.5 1.00e+00 1.00e+00h 1\n", + " 13 -1.2250322e+01 3.13e-03 1.32e-03 -5.7 4.61e+00 -5.0 1.00e+00 4.23e-01h 1\n", + " 14 -1.2250433e+01 2.06e-03 7.85e-04 -5.7 6.18e-01 -5.4 1.00e+00 1.00e+00f 1\n", + " 15 -1.2261629e+01 2.19e+01 8.47e-02 -5.7 6.04e+03 - 2.88e-02 2.27e-02h 1\n", + " 16 -1.2265527e+01 2.15e+01 6.31e-02 -5.7 1.28e+02 - 1.00e+00 2.59e-01f 1\n", + " 17 -1.2268702e+01 9.75e-01 1.54e-02 -5.7 2.61e+01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (320847)\n", + " 18 -1.2267146e+01 3.13e-01 1.00e-03 -5.7 1.77e+01 - 1.00e+00 1.00e+00h 1\n", + " 19 -1.2267156e+01 4.84e-04 4.02e-06 -5.7 5.89e-01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (338500)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.2267156e+01 3.56e-08 2.75e-09 -5.7 1.30e-02 - 1.00e+00 1.00e+00h 1\n", + " 21 -1.2267782e+01 3.24e-01 9.64e-04 -8.6 1.78e+01 - 9.11e-01 9.81e-01h 1\n", + " 22 -1.2267804e+01 1.12e-02 2.49e-04 -8.6 9.37e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (357003)\n", + " 23 -1.2267811e+01 2.04e-03 4.64e-05 -8.6 4.06e+00 - 1.00e+00 1.00e+00h 1\n", + " 24 -1.2267812e+01 5.38e-05 1.69e-06 -8.6 6.64e-01 - 1.00e+00 1.00e+00h 1\n", + " 25 -1.2267812e+01 3.11e-08 2.34e-09 -8.6 1.60e-02 - 1.00e+00 1.00e+00h 1\n", + " 26 -1.2267812e+01 4.55e-13 3.29e-14 -8.6 2.09e-05 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 26\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.2267811714197844e+01 -1.2267811714197844e+01\n", + "Dual infeasibility......: 3.2899955311301634e-14 3.2899955311301634e-14\n", + "Constraint violation....: 1.2096124245683242e-13 4.5474735088646412e-13\n", + "Complementarity.........: 2.5059035616651650e-09 2.5059035616651650e-09\n", + "Overall NLP error.......: 2.5059035616651650e-09 2.5059035616651650e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 33\n", + "Number of objective gradient evaluations = 27\n", + "Number of equality constraint evaluations = 33\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 27\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 26\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.945\n", + "Total CPU secs in NLP function evaluations = 0.030\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.7 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 3.67e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 4.18e+00 3.85e+02 -1.0 3.62e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.77e-02 3.85e+00 -1.0 4.47e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.72e-04 4.39e+00 -1.0 5.07e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.02e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.132\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 8.9357618e-01 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 8.9266467e-01 1.57e-01 1.04e+00 -1.0 5.95e-01 - 9.79e-01 8.74e-01h 1\n", + " 2 8.9324077e-01 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 8.8727234e-01 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", + " 4 8.9978503e-01 7.13e-01 2.49e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 8.9690135e-01 5.85e-01 4.71e+01 -1.0 3.49e+02 - 4.74e-01 8.83e-02f 2\n", + " 6 9.0474858e-01 1.37e-01 5.96e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", + " 7 9.0340741e-01 4.33e-01 2.97e-01 -1.0 5.48e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 9.0406676e-01 3.85e-02 5.76e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 9.0401537e-01 5.34e-03 3.76e-03 -2.5 6.32e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 9.0360000e-01 8.24e-04 7.04e-04 -3.8 1.31e+00 - 1.00e+00 1.00e+00h 1\n", + " 11 8.9706762e-01 4.73e-01 2.82e-02 -5.7 3.26e+01 - 7.21e-01 1.00e+00h 1\n", + " 12 8.9104362e-01 7.55e-01 1.10e-02 -5.7 4.96e+01 - 8.53e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (326956)\n", + " 13 8.8691901e-01 7.24e-01 3.26e-02 -5.7 3.05e+01 - 8.07e-01 1.00e+00h 1\n", + " 14 8.8783634e-01 1.23e-02 9.48e-04 -5.7 7.02e-01 -4.5 9.51e-01 1.00e+00h 1\n", + " 15 8.8779529e-01 1.18e-06 3.71e-07 -5.7 3.26e-02 -5.0 1.00e+00 1.00e+00h 1\n", + " 16 8.8776642e-01 7.96e-06 3.75e-06 -8.6 1.12e-01 -5.4 9.99e-01 1.00e+00h 1\n", + " 17 8.8772695e-01 7.85e-05 3.35e-06 -8.6 3.61e-01 -5.9 1.00e+00 1.00e+00h 1\n", + " 18 8.8761634e-01 6.66e-04 2.84e-05 -8.6 1.06e+00 -6.4 1.00e+00 1.00e+00h 1\n", + " 19 8.8755498e-01 7.42e-04 3.16e-05 -8.6 2.76e+00 -6.9 1.00e+00 2.24e-01h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 8.8717865e-01 9.48e-03 3.05e-04 -8.6 4.13e+00 -7.3 1.00e+00 1.00e+00f 1\n", + " 21 8.8657032e-01 4.66e-02 2.97e-03 -8.6 1.24e+01 -7.8 1.00e+00 9.75e-01h 1\n", + " 22 8.8612174e-01 1.44e-01 8.55e-03 -8.6 3.02e+02 -8.3 3.51e-01 6.41e-02h 1\n", + " 23 8.8583006e-01 7.53e-02 4.42e-03 -8.6 4.03e+01 - 1.00e+00 4.78e-01f 1\n", + " 24 8.8580769e-01 5.71e-02 3.34e-03 -8.6 3.65e+01 - 1.00e+00 2.44e-01f 1\n", + " 25 8.8579801e-01 4.72e-02 2.21e-03 -8.6 3.13e+01 - 1.00e+00 3.38e-01f 1\n", + "Reallocating memory for MA57: lfact (344590)\n", + " 26 8.8578794e-01 1.54e-03 9.57e-05 -8.6 2.23e+00 - 1.00e+00 1.00e+00h 1\n", + " 27 8.8579268e-01 3.27e-04 1.98e-07 -8.6 1.63e+00 - 1.00e+00 1.00e+00h 1\n", + " 28 8.8579267e-01 1.28e-06 6.66e-10 -8.6 1.03e-01 - 1.00e+00 1.00e+00h 1\n", + " 29 8.8579267e-01 4.37e-12 1.51e-10 -8.6 1.89e-04 -7.0 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 29\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 8.8579267060918165e-01 8.8579267060918165e-01\n", + "Dual infeasibility......: 1.5144484733052913e-10 1.5144484733052913e-10\n", + "Constraint violation....: 4.3728354270911041e-12 4.3728354270911041e-12\n", + "Complementarity.........: 2.5059074743708337e-09 2.5059074743708337e-09\n", + "Overall NLP error.......: 2.5059074743708337e-09 2.5059074743708337e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 34\n", + "Number of objective gradient evaluations = 30\n", + "Number of equality constraint evaluations = 34\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 30\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 29\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.345\n", + "Total CPU secs in NLP function evaluations = 0.035\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.31e-03 3.92e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 1.94e-09 1.43e-06 -1.0 1.66e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.9411232621280305e-09 1.9411232621280305e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.9411232621280305e-09 1.9411232621280305e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.31e-03 3.92e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 1.94e-09 1.43e-06 -1.0 1.66e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.9411330320906472e-09 1.9411330320906472e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.9411330320906472e-09 1.9411330320906472e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.099\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 9.19e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 6.69e+02 3.85e+02 -1.0 9.19e+03 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 7.62e+00 3.85e+00 -1.0 7.15e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 7.53e-02 4.39e+00 -1.0 8.10e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.01e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.130\n", + "Total CPU secs in NLP function evaluations = 0.003\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.2399698e+01 1.25e+00 1.05e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.2401027e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", + " 2 -1.2399984e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.2405602e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.2394795e+01 1.67e+01 2.20e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.2396322e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.13e-01f 2\n", + " 6 -1.2392061e+01 1.23e+01 3.32e+00 -1.0 9.23e+01 - 8.76e-01 3.36e-01f 2\n", + " 7 -1.2387991e+01 3.64e+00 5.35e-01 -1.0 5.23e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.2389753e+01 4.48e-01 7.87e-02 -1.0 1.10e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.2389755e+01 2.40e-04 1.33e-03 -1.7 3.63e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.2389808e+01 3.50e-04 1.04e-03 -3.8 4.33e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 -1.2389937e+01 2.30e-04 1.17e-04 -5.7 8.50e-01 -4.0 1.00e+00 1.00e+00h 1\n", + " 12 -1.2390402e+01 2.96e-03 1.02e-04 -5.7 3.06e+00 -4.5 1.00e+00 1.00e+00h 1\n", + " 13 -1.2390734e+01 3.18e-03 1.06e-03 -5.7 4.98e+00 -5.0 1.00e+00 4.36e-01h 1\n", + " 14 -1.2390822e+01 1.73e-03 7.04e-04 -5.7 5.30e-01 -5.4 1.00e+00 1.00e+00f 1\n", + " 15 -1.2391170e+01 1.55e-02 2.13e-05 -5.7 2.13e+00 -5.9 1.00e+00 1.00e+00h 1\n", + " 16 -1.2392122e+01 1.25e-01 1.80e-04 -5.7 5.69e+00 -6.4 1.00e+00 1.00e+00h 1\n", + " 17 -1.2394535e+01 8.51e-01 1.24e-03 -5.7 1.34e+01 -6.9 1.00e+00 1.00e+00h 1\n", + " 18 -1.2399248e+01 3.83e+00 7.22e-03 -5.7 1.90e+01 -7.3 1.00e+00 1.00e+00h 1\n", + " 19 -1.2404443e+01 8.34e+00 2.26e-02 -5.7 3.55e+01 -7.8 1.00e+00 9.24e-01h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.2403887e+01 1.53e-01 1.60e-03 -5.7 1.03e+01 -7.4 1.00e+00 1.00e+00f 1\n", + " 21 -1.2405117e+01 4.79e-01 2.18e-03 -5.7 1.71e+01 -7.9 1.00e+00 1.00e+00h 1\n", + " 22 -1.2405374e+01 7.00e-02 1.88e-04 -5.7 8.45e+00 -7.4 1.00e+00 1.00e+00h 1\n", + " 23 -1.2405756e+01 2.12e-01 1.22e-03 -5.7 1.03e+02 - 1.00e+00 2.08e-01h 2\n", + " 24 -1.2406358e+01 1.03e+00 2.17e-03 -5.7 8.46e+01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (322545)\n", + " 25 -1.2406204e+01 1.75e-02 7.25e-05 -5.7 4.61e+00 - 1.00e+00 1.00e+00h 1\n", + " 26 -1.2406205e+01 2.37e-04 2.08e-07 -5.7 1.32e+00 - 1.00e+00 1.00e+00h 1\n", + " 27 -1.2406205e+01 7.23e-08 5.15e-11 -5.7 2.30e-02 - 1.00e+00 1.00e+00h 1\n", + " 28 -1.2406828e+01 3.77e-01 1.03e-03 -8.6 1.92e+01 - 9.07e-01 9.76e-01h 1\n", + "Reallocating memory for MA57: lfact (354103)\n", + " 29 -1.2406852e+01 1.23e-02 2.52e-04 -8.6 9.79e+00 - 9.97e-01 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -1.2406858e+01 2.37e-03 4.93e-05 -8.6 4.37e+00 - 1.00e+00 1.00e+00h 1\n", + " 31 -1.2406860e+01 7.39e-05 2.07e-06 -8.6 7.78e-01 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (376176)\n", + " 32 -1.2406860e+01 5.90e-08 3.77e-09 -8.6 2.20e-02 - 1.00e+00 1.00e+00h 1\n", + " 33 -1.2406860e+01 1.82e-12 3.89e-14 -8.6 3.63e-05 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 33\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.2406859586044352e+01 -1.2406859586044352e+01\n", + "Dual infeasibility......: 3.8887996860705895e-14 3.8887996860705895e-14\n", + "Constraint violation....: 9.1789600112649688e-13 1.8189894035458565e-12\n", + "Complementarity.........: 2.5059035652168643e-09 2.5059035652168643e-09\n", + "Overall NLP error.......: 2.5059035652168643e-09 2.5059035652168643e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 43\n", + "Number of objective gradient evaluations = 34\n", + "Number of equality constraint evaluations = 43\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 34\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 33\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.368\n", + "Total CPU secs in NLP function evaluations = 0.051\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 4.01e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 4.21e+00 3.85e+02 -1.0 3.96e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.80e-02 3.85e+00 -1.0 4.50e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.75e-04 4.39e+00 -1.0 5.11e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.05e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.131\n", + "Total CPU secs in NLP function evaluations = 0.005\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 7.5390617e-01 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 7.5307280e-01 1.57e-01 1.04e+00 -1.0 5.95e-01 - 9.79e-01 8.74e-01h 1\n", + " 2 7.5361378e-01 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 7.4810233e-01 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", + " 4 7.5962740e-01 7.12e-01 2.49e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 7.5698063e-01 5.85e-01 4.71e+01 -1.0 3.48e+02 - 4.75e-01 8.85e-02f 2\n", + " 6 7.6420980e-01 1.37e-01 5.99e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", + " 7 7.6297451e-01 4.34e-01 2.97e-01 -1.0 5.48e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 7.6358498e-01 3.85e-02 5.77e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 7.6354212e-01 5.37e-03 3.81e-03 -2.5 6.34e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 7.6350175e-01 2.77e-06 2.26e-05 -3.8 3.72e-02 -4.5 1.00e+00 1.00e+00h 1\n", + " 11 7.5733271e-01 4.48e-01 2.29e-02 -5.7 3.01e+01 - 7.11e-01 1.00e+00h 1\n", + " 12 7.5196585e-01 6.78e-01 8.94e-03 -5.7 4.61e+01 - 8.18e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (327108)\n", + " 13 7.4797841e-01 6.41e-01 2.55e-02 -5.7 2.96e+01 - 8.64e-01 1.00e+00h 1\n", + " 14 7.4890109e-01 8.10e-03 4.06e-04 -5.7 6.20e-01 -5.0 1.00e+00 1.00e+00h 1\n", + " 15 7.4886319e-01 1.99e-06 2.82e-07 -5.7 5.38e-02 -5.4 1.00e+00 1.00e+00h 1\n", + " 16 7.4881137e-01 6.86e-05 1.98e-05 -8.6 3.31e-01 -5.9 9.95e-01 1.00e+00h 1\n", + " 17 7.4870952e-01 6.03e-04 2.31e-05 -8.6 9.93e-01 -6.4 1.00e+00 1.00e+00h 1\n", + " 18 7.4845170e-01 4.66e-03 1.77e-04 -8.6 2.80e+00 -6.9 1.00e+00 1.00e+00h 1\n", + " 19 7.4825676e-01 6.71e-03 2.55e-04 -8.6 7.30e+00 -7.3 1.00e+00 3.51e-01h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 7.4767097e-01 3.32e-02 1.65e-03 -8.6 8.46e+00 -7.8 1.00e+00 1.00e+00f 1\n", + " 21 7.4711216e-01 2.44e-01 1.54e-02 -8.6 1.14e+02 -8.3 9.80e-01 2.52e-01h 1\n", + " 22 7.4688518e-01 1.38e-01 8.67e-03 -8.6 4.16e+01 - 1.00e+00 4.39e-01f 1\n", + " 23 7.4683046e-01 1.02e-01 6.42e-03 -8.6 3.76e+01 - 1.00e+00 2.59e-01f 1\n", + " 24 7.4679078e-01 6.26e-02 3.91e-03 -8.6 3.27e+01 - 1.00e+00 3.90e-01f 1\n", + " 25 7.4674071e-01 1.91e-03 7.64e-05 -8.6 2.40e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (350967)\n", + " 26 7.4674477e-01 5.39e-04 3.03e-07 -8.6 2.09e+00 - 1.00e+00 1.00e+00h 1\n", + " 27 7.4674475e-01 3.40e-06 1.63e-09 -8.6 1.67e-01 - 1.00e+00 1.00e+00h 1\n", + " 28 7.4674475e-01 8.78e-11 6.54e-14 -8.6 8.49e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 28\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 7.4674475313131294e-01 7.4674475313131294e-01\n", + "Dual infeasibility......: 6.5380653010782577e-14 6.5380653010782577e-14\n", + "Constraint violation....: 8.7840845708342385e-11 8.7840845708342385e-11\n", + "Complementarity.........: 2.5059039393135571e-09 2.5059039393135571e-09\n", + "Overall NLP error.......: 2.5059039393135571e-09 2.5059039393135571e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 33\n", + "Number of objective gradient evaluations = 29\n", + "Number of equality constraint evaluations = 33\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 29\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 28\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.211\n", + "Total CPU secs in NLP function evaluations = 0.043\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.9 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.30e-03 4.15e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 1.91e-09 1.42e-06 -1.0 1.64e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.9060446554419741e-09 1.9060446554419741e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.9060446554419741e-09 1.9060446554419741e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.103\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.30e-03 4.15e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 1.91e-09 1.42e-06 -1.0 1.64e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.9060530931369613e-09 1.9060530931369613e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.9060530931369613e-09 1.9060530931369613e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.100\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 0.8 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 9.96e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 6.76e+02 3.85e+02 -1.0 9.96e+03 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 7.69e+00 3.85e+00 -1.0 7.23e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 7.61e-02 4.39e+00 -1.0 8.17e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.08e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0403773356229067e-09 9.0403773356229067e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0403773356229067e-09 9.0403773356229067e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.132\n", + "Total CPU secs in NLP function evaluations = 0.005\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.2528966e+01 1.25e+00 1.05e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.2530197e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", + " 2 -1.2529220e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.2534432e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.2524418e+01 1.67e+01 2.20e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.2525826e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.13e-01f 2\n", + " 6 -1.2521877e+01 1.23e+01 3.32e+00 -1.0 9.23e+01 - 8.76e-01 3.35e-01f 2\n", + " 7 -1.2518098e+01 3.65e+00 5.36e-01 -1.0 5.24e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.2519733e+01 4.49e-01 7.90e-02 -1.0 1.10e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.2519735e+01 2.41e-04 1.29e-03 -1.7 3.68e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.2519781e+01 3.02e-04 9.67e-04 -3.8 4.03e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 -1.2519893e+01 2.02e-04 1.02e-04 -5.7 7.97e-01 -4.0 1.00e+00 1.00e+00h 1\n", + " 12 -1.2520302e+01 2.66e-03 9.65e-05 -5.7 2.89e+00 -4.5 1.00e+00 1.00e+00h 1\n", + " 13 -1.2520638e+01 3.26e-03 8.46e-04 -5.7 5.24e+00 -5.0 1.00e+00 4.54e-01h 1\n", + " 14 -1.2520708e+01 1.48e-03 6.33e-04 -5.7 4.47e-01 -5.4 1.00e+00 1.00e+00f 1\n", + " 15 -1.2521012e+01 1.34e-02 1.66e-05 -5.7 2.02e+00 -5.9 1.00e+00 1.00e+00h 1\n", + " 16 -1.2521836e+01 1.09e-01 1.45e-04 -5.7 5.34e+00 -6.4 1.00e+00 1.00e+00h 1\n", + " 17 -1.2523945e+01 7.51e-01 9.99e-04 -5.7 1.28e+01 -6.9 1.00e+00 1.00e+00h 1\n", + " 18r-1.2523945e+01 7.51e-01 9.99e+02 -0.1 0.00e+00 - 0.00e+00 3.08e-07R 18\n", + " 19r-1.2523451e+01 1.72e-01 9.81e+02 -0.1 3.12e+03 - 1.04e-01 9.90e-04f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.2524823e+01 1.77e-01 5.03e-02 -5.7 1.23e+04 - 7.38e-02 7.99e-04h 1\n", + " 21 -1.2539231e+01 3.95e+01 4.75e-02 -5.7 2.75e+03 - 3.37e-04 5.44e-02h 1\n", + "Reallocating memory for MA57: lfact (323127)\n", + " 22 -1.2541247e+01 3.61e+01 4.11e-02 -5.7 3.01e+02 - 1.46e-01 1.36e-01f 1\n", + " 23 -1.2539740e+01 2.50e+01 3.16e-02 -5.7 1.12e+02 - 1.19e-01 3.51e-01h 1\n", + " 24 -1.2536325e+01 1.39e+01 5.47e-02 -5.7 9.93e+01 - 1.00e+00 5.51e-01h 1\n", + " 25 -1.2536505e+01 1.38e+01 2.22e-01 -5.7 5.62e+02 - 1.93e-03 4.60e-02h 1\n", + " 26 -1.2534286e+01 1.59e+00 1.76e-01 -5.7 3.49e+01 - 1.24e-01 1.00e+00f 1\n", + " 27 -1.2534443e+01 5.57e-02 3.13e-04 -5.7 1.15e+01 -7.3 1.00e+00 1.00e+00h 1\n", + " 28 -1.2534551e+01 9.81e-02 7.65e-04 -5.7 7.58e+00 -7.8 1.00e+00 1.00e+00h 1\n", + " 29 -1.2534950e+01 1.03e+00 6.91e-03 -5.7 2.80e+01 -8.3 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -1.2534831e+01 1.41e-01 1.08e-03 -5.7 8.88e+00 - 1.00e+00 1.00e+00h 1\n", + " 31 -1.2534949e+01 4.97e-02 3.05e-04 -5.7 6.88e+00 - 1.00e+00 1.00e+00h 1\n", + " 32 -1.2534944e+01 2.69e-04 2.11e-06 -5.7 4.40e-01 - 1.00e+00 1.00e+00h 1\n", + " 33 -1.2534944e+01 1.07e-08 1.03e-10 -5.7 2.72e-03 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (344149)\n", + " 34 -1.2535563e+01 4.32e-01 1.10e-03 -8.6 2.06e+01 - 9.03e-01 9.71e-01h 1\n", + "Reallocating memory for MA57: lfact (363431)\n", + " 35 -1.2535591e+01 1.34e-02 2.54e-04 -8.6 1.02e+01 - 9.95e-01 1.00e+00h 1\n", + " 36 -1.2535597e+01 2.72e-03 5.20e-05 -8.6 4.68e+00 - 1.00e+00 1.00e+00h 1\n", + " 37 -1.2535598e+01 9.81e-05 2.47e-06 -8.6 8.96e-01 - 1.00e+00 1.00e+00h 1\n", + " 38 -1.2535598e+01 1.05e-07 5.76e-09 -8.6 2.93e-02 - 1.00e+00 1.00e+00h 1\n", + " 39 -1.2535598e+01 1.82e-12 5.88e-14 -8.6 5.95e-05 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 39\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.2535597772942342e+01 -1.2535597772942342e+01\n", + "Dual infeasibility......: 5.8821618000241020e-14 5.8821618000241020e-14\n", + "Constraint violation....: 1.8189894035458565e-12 1.8189894035458565e-12\n", + "Complementarity.........: 2.5059035734822043e-09 2.5059035734822043e-09\n", + "Overall NLP error.......: 2.5059035734822043e-09 2.5059035734822043e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 66\n", + "Number of objective gradient evaluations = 40\n", + "Number of equality constraint evaluations = 66\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 41\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 39\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.508\n", + "Total CPU secs in NLP function evaluations = 0.067\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.3 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 4.35e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 4.24e+00 3.85e+02 -1.0 4.30e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.83e-02 3.85e+00 -1.0 4.53e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.78e-04 4.39e+00 -1.0 5.14e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.08e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.128\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 6.2463783e-01 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 6.2387037e-01 1.57e-01 1.04e+00 -1.0 5.95e-01 - 9.79e-01 8.75e-01h 1\n", + " 2 6.2438002e-01 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 6.1926056e-01 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", + " 4 6.2994249e-01 7.12e-01 2.48e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 6.2749675e-01 5.85e-01 4.72e+01 -1.0 3.48e+02 - 4.75e-01 8.87e-02f 2\n", + " 6 6.3421863e-01 1.35e-01 6.11e+00 -1.0 2.40e+01 -4.0 8.21e-01 1.00e+00F 1\n", + " 7 6.3306025e-01 4.35e-01 2.98e-01 -1.0 5.49e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 6.3362659e-01 3.83e-02 5.75e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 6.3358982e-01 5.38e-03 3.85e-03 -2.5 6.34e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 6.3355519e-01 2.58e-06 2.12e-05 -3.8 3.51e-02 -4.5 1.00e+00 1.00e+00h 1\n", + " 11 6.3322338e-01 2.85e-05 3.51e-06 -5.7 9.23e-02 -5.0 1.00e+00 1.00e+00h 1\n", + " 12 6.3264654e-01 1.33e-04 5.05e-06 -5.7 2.72e-01 -5.4 1.00e+00 1.00e+00h 1\n", + " 13 6.3233614e-01 4.22e-04 2.05e-05 -5.7 7.80e-01 -5.9 1.00e+00 1.00e+00h 1\n", + " 14 6.3145086e-01 3.55e-03 1.66e-04 -5.7 2.26e+00 -6.4 1.00e+00 1.00e+00h 1\n", + " 15 6.2329402e-01 1.79e+00 4.47e-02 -5.7 7.85e+02 - 1.56e-01 9.99e-02h 1\n", + " 16 6.2001136e-01 1.37e+00 4.66e-02 -5.7 1.24e+02 - 1.00e+00 2.70e-01f 1\n", + " 17 6.1736350e-01 1.24e-01 1.13e-02 -5.7 2.91e+01 - 1.00e+00 1.00e+00f 1\n", + "Reallocating memory for MA57: lfact (322135)\n", + " 18 6.1867566e-01 7.27e-03 6.10e-04 -5.7 4.56e+00 - 1.00e+00 1.00e+00h 1\n", + " 19 6.1866009e-01 5.67e-05 3.42e-06 -5.7 6.62e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 6.1866005e-01 2.03e-08 1.12e-09 -5.7 1.25e-02 - 1.00e+00 1.00e+00h 1\n", + " 21 6.1804128e-01 3.11e-02 1.10e-03 -8.6 1.54e+01 - 9.03e-01 9.71e-01h 1\n", + "Reallocating memory for MA57: lfact (347921)\n", + " 22 6.1801354e-01 1.34e-02 2.54e-04 -8.6 1.02e+01 - 9.95e-01 1.00e+00h 1\n", + " 23 6.1800773e-01 2.72e-03 5.20e-05 -8.6 4.68e+00 - 1.00e+00 1.00e+00h 1\n", + " 24 6.1800657e-01 9.81e-05 2.47e-06 -8.6 8.96e-01 - 1.00e+00 1.00e+00h 1\n", + " 25 6.1800653e-01 1.05e-07 5.76e-09 -8.6 2.93e-02 - 1.00e+00 1.00e+00h 1\n", + " 26 6.1800653e-01 4.32e-13 5.93e-14 -8.6 5.95e-05 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 26\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 6.1800652712078263e-01 6.1800652712078263e-01\n", + "Dual infeasibility......: 5.9277911925848335e-14 5.9277911925848335e-14\n", + "Constraint violation....: 4.3220982348657344e-13 4.3220982348657344e-13\n", + "Complementarity.........: 2.5059035734816410e-09 2.5059035734816410e-09\n", + "Overall NLP error.......: 2.5059035734816410e-09 2.5059035734816410e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 31\n", + "Number of objective gradient evaluations = 27\n", + "Number of equality constraint evaluations = 31\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 27\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 26\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.926\n", + "Total CPU secs in NLP function evaluations = 0.031\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.7 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.29e-03 4.38e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 1.87e-09 1.41e-06 -1.0 1.63e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.8712897897898984e-09 1.8712897897898984e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.8712897897898984e-09 1.8712897897898984e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.103\n", + "Total CPU secs in NLP function evaluations = 0.002\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.29e-03 4.38e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 1.87e-09 1.41e-06 -1.0 1.63e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.8712982274848855e-09 1.8712982274848855e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.8712982274848855e-09 1.8712982274848855e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.098\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.07e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 6.84e+02 3.85e+02 -1.0 1.07e+04 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 7.77e+00 3.85e+00 -1.0 7.31e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 7.68e-02 4.39e+00 -1.0 8.25e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.16e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.129\n", + "Total CPU secs in NLP function evaluations = 0.003\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.2649276e+01 1.25e+00 1.05e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.2650421e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", + " 2 -1.2649502e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.2654364e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.2645035e+01 1.67e+01 2.20e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.2646341e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.13e-01f 2\n", + " 6 -1.2642660e+01 1.23e+01 3.32e+00 -1.0 9.23e+01 - 8.75e-01 3.35e-01f 2\n", + " 7 -1.2639134e+01 3.65e+00 5.36e-01 -1.0 5.24e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.2640660e+01 4.49e-01 7.92e-02 -1.0 1.10e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.2640661e+01 2.42e-04 1.25e-03 -1.7 3.72e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.2640701e+01 2.64e-04 9.04e-04 -3.8 3.77e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 -1.2645246e+01 3.62e+00 2.60e-02 -5.7 4.21e+01 - 7.44e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (319822)\n", + " 12 -1.2650749e+01 4.98e+00 1.49e-02 -5.7 7.45e+01 - 8.19e-01 1.00e+00h 1\n", + " 13 -1.2654062e+01 5.16e+00 1.87e-02 -5.7 3.19e+01 - 9.04e-01 1.00e+00h 1\n", + " 14 -1.2654011e+01 4.69e+00 5.08e-02 -5.7 1.46e+01 -4.0 1.00e+00 9.14e-02h 1\n", + " 15 -1.2654035e+01 7.16e-01 2.01e-02 -5.7 3.14e+01 - 1.00e+00 1.00e+00h 1\n", + " 16 -1.2653941e+01 1.00e-03 9.08e-05 -5.7 3.69e-01 -4.5 1.00e+00 1.00e+00h 1\n", + " 17 -1.2654241e+01 6.33e-01 5.16e-03 -5.7 1.06e+02 - 1.00e+00 1.70e-01h 2\n", + " 18 -1.2654880e+01 3.29e-01 2.15e-03 -5.7 1.63e+01 - 1.00e+00 1.00e+00h 1\n", + " 19 -1.2654796e+01 6.21e-03 3.76e-05 -5.7 1.47e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.2654797e+01 9.53e-06 4.39e-08 -5.7 1.04e-01 - 1.00e+00 1.00e+00h 1\n", + " 21 -1.2655412e+01 4.92e-01 1.17e-03 -8.6 2.19e+01 - 8.97e-01 9.66e-01h 1\n", + "Reallocating memory for MA57: lfact (343005)\n", + " 22 -1.2655443e+01 1.44e-02 2.56e-04 -8.6 1.06e+01 - 9.92e-01 1.00e+00h 1\n", + " 23 -1.2655449e+01 3.08e-03 5.46e-05 -8.6 4.98e+00 - 1.00e+00 1.00e+00h 1\n", + " 24 -1.2655450e+01 1.28e-04 2.92e-06 -8.6 1.02e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (360209)\n", + " 25 -1.2655450e+01 1.79e-07 8.58e-09 -8.6 3.83e-02 - 1.00e+00 1.00e+00h 1\n", + " 26 -1.2655450e+01 1.82e-12 1.06e-13 -8.6 9.45e-05 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 26\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.2655450125163370e+01 -1.2655450125163370e+01\n", + "Dual infeasibility......: 1.0562293264999832e-13 1.0562293264999832e-13\n", + "Constraint violation....: 1.0876854972252659e-12 1.8189894035458565e-12\n", + "Complementarity.........: 2.5059035921766028e-09 2.5059035921766028e-09\n", + "Overall NLP error.......: 2.5059035921766028e-09 2.5059035921766028e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 37\n", + "Number of objective gradient evaluations = 27\n", + "Number of equality constraint evaluations = 37\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 27\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 26\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.992\n", + "Total CPU secs in NLP function evaluations = 0.043\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.7 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 4.69e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 4.27e+00 3.85e+02 -1.0 4.64e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.87e-02 3.85e+00 -1.0 4.57e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.81e-04 4.39e+00 -1.0 5.17e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.11e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.134\n", + "Total CPU secs in NLP function evaluations = 0.005\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 5.0432833e-01 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 5.0361720e-01 1.56e-01 1.04e+00 -1.0 5.95e-01 - 9.79e-01 8.75e-01h 1\n", + " 2 5.0409878e-01 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 4.9931925e-01 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", + " 4 5.0927300e-01 7.12e-01 2.48e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 5.0699996e-01 5.85e-01 4.72e+01 -1.0 3.47e+02 - 4.76e-01 8.88e-02f 2\n", + " 6 5.1324183e-01 1.37e-01 6.01e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", + " 7 5.1217900e-01 4.34e-01 2.96e-01 -1.0 5.48e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 5.1271052e-01 3.84e-02 5.77e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 5.1267962e-01 5.41e-03 3.90e-03 -2.5 6.36e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 5.1241548e-01 5.08e-04 5.15e-04 -3.8 9.59e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 5.0807961e-01 3.17e-01 2.48e-02 -5.7 2.63e+01 - 7.57e-01 1.00e+00h 1\n", + " 12 5.0278293e-01 8.71e-01 1.15e-02 -5.7 5.29e+01 - 8.62e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (326106)\n", + " 13 4.9913241e-01 7.64e-01 2.78e-02 -5.7 3.15e+01 - 8.71e-01 1.00e+00h 1\n", + " 14 4.9997874e-01 1.14e-02 5.09e-04 -5.7 7.44e-01 -4.5 9.85e-01 1.00e+00h 1\n", + " 15 4.9994319e-01 6.15e-07 2.09e-07 -5.7 1.61e-02 -5.0 1.00e+00 1.00e+00h 1\n", + " 16 4.9991846e-01 5.82e-06 2.33e-06 -8.6 9.55e-02 -5.4 9.99e-01 1.00e+00h 1\n", + " 17 4.9989124e-01 5.24e-05 1.77e-06 -8.6 2.92e-01 -5.9 1.00e+00 1.00e+00h 1\n", + " 18 4.9981470e-01 4.48e-04 1.51e-05 -8.6 8.57e-01 -6.4 1.00e+00 1.00e+00h 1\n", + " 19 4.9961545e-01 3.50e-03 1.17e-04 -8.6 2.43e+00 -6.9 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 4.9957792e-01 3.35e-03 1.12e-04 -8.6 6.02e+00 -7.3 1.00e+00 8.63e-02h 1\n", + " 21 4.9902524e-01 3.02e-02 1.14e-03 -8.6 7.74e+00 -7.8 1.00e+00 1.00e+00f 1\n", + " 22 4.9848008e-01 2.40e-01 1.30e-02 -8.6 7.13e+01 -8.3 1.00e+00 4.02e-01h 1\n", + " 23 4.9828290e-01 1.29e-01 7.02e-03 -8.6 4.11e+01 - 1.00e+00 4.70e-01f 1\n", + " 24 4.9823261e-01 9.68e-02 5.22e-03 -8.6 3.63e+01 - 1.00e+00 2.57e-01f 1\n", + " 25 4.9820105e-01 6.31e-02 3.38e-03 -8.6 3.10e+01 - 1.00e+00 3.52e-01f 1\n", + " 26 4.9815087e-01 2.36e-03 6.18e-05 -8.6 2.04e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (361644)\n", + " 27 4.9815415e-01 3.57e-04 1.71e-07 -8.6 1.71e+00 - 1.00e+00 1.00e+00h 1\n", + " 28 4.9815414e-01 1.50e-06 6.26e-10 -8.6 1.11e-01 - 1.00e+00 1.00e+00h 1\n", + " 29 4.9815414e-01 1.71e-11 3.34e-14 -8.6 3.75e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 29\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 4.9815414100226940e-01 4.9815414100226940e-01\n", + "Dual infeasibility......: 3.3405414623680940e-14 3.3405414623680940e-14\n", + "Constraint violation....: 1.7140733277187792e-11 1.7140733277187792e-11\n", + "Complementarity.........: 2.5059036238671544e-09 2.5059036238671544e-09\n", + "Overall NLP error.......: 2.5059036238671544e-09 2.5059036238671544e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 34\n", + "Number of objective gradient evaluations = 30\n", + "Number of equality constraint evaluations = 34\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 30\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 29\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.235\n", + "Total CPU secs in NLP function evaluations = 0.038\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.27e-03 4.61e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 1.84e-09 1.40e-06 -1.0 1.61e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.8368555565473343e-09 1.8368555565473343e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.8368555565473343e-09 1.8368555565473343e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.105\n", + "Total CPU secs in NLP function evaluations = 0.003\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.27e-03 4.61e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 1.84e-09 1.40e-06 -1.0 1.61e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.8368631060639018e-09 1.8368631060639018e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.8368631060639018e-09 1.8368631060639018e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.102\n", + "Total CPU secs in NLP function evaluations = 0.003\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.15e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 6.92e+02 3.85e+02 -1.0 1.15e+04 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 7.85e+00 3.85e+00 -1.0 7.38e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 7.76e-02 4.39e+00 -1.0 8.33e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.23e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0367393568158150e-09 9.0367393568158150e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0367393568158150e-09 9.0367393568158150e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.126\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.2761788e+01 1.25e+00 1.05e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.2762860e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", + " 2 -1.2761992e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.2766548e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.2757815e+01 1.67e+01 2.19e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.2759034e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.12e-01f 2\n", + " 6 -1.2755587e+01 1.23e+01 3.32e+00 -1.0 9.23e+01 - 8.75e-01 3.35e-01f 2\n", + " 7 -1.2752282e+01 3.66e+00 5.36e-01 -1.0 5.24e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.2753712e+01 4.50e-01 7.94e-02 -1.0 1.10e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.2753713e+01 2.43e-04 1.22e-03 -1.7 3.76e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.2753748e+01 2.32e-04 8.49e-04 -3.8 3.54e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 -1.2753835e+01 1.60e-04 8.09e-05 -5.7 7.08e-01 -4.0 1.00e+00 1.00e+00h 1\n", + " 12 -1.2754158e+01 2.17e-03 8.72e-05 -5.7 2.61e+00 -4.5 1.00e+00 1.00e+00h 1\n", + " 13 -1.2754496e+01 3.46e-03 5.33e-04 -5.7 5.48e+00 -5.0 1.00e+00 4.99e-01h 1\n", + " 14 -1.2754536e+01 1.11e-03 5.18e-04 -5.7 2.85e-01 -5.4 1.00e+00 1.00e+00f 1\n", + " 15 -1.2754778e+01 1.03e-02 2.28e-05 -5.7 1.85e+00 -5.9 1.00e+00 1.00e+00h 1\n", + " 16 -1.2755412e+01 8.41e-02 9.87e-05 -5.7 4.74e+00 -6.4 1.00e+00 1.00e+00h 1\n", + " 17 -1.2757064e+01 5.97e-01 6.84e-04 -5.7 1.17e+01 -6.9 1.00e+00 1.00e+00h 1\n", + " 18 -1.2760500e+01 2.94e+00 4.15e-03 -5.7 1.95e+01 -7.3 1.00e+00 1.00e+00h 1\n", + " 19 -1.2761019e+01 9.59e+00 2.25e-02 -5.7 6.29e+02 - 1.13e-01 1.41e-01h 2\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.2764864e+01 1.90e+01 9.25e-02 -5.7 1.73e+02 - 3.33e-01 3.01e-01h 1\n", + " 21 -1.2764680e+01 1.16e+01 1.82e-01 -5.7 2.07e+02 - 3.79e-01 7.21e-01H 1\n", + " 22 -1.2767567e+01 1.81e+01 2.45e-01 -5.7 1.01e+02 - 1.00e+00 1.00e+00f 1\n", + " 23 -1.2767317e+01 1.23e+01 1.12e-01 -5.7 1.91e+02 - 1.00e+00 5.53e-01h 1\n", + "Reallocating memory for MA57: lfact (319005)\n", + " 24 -1.2767311e+01 8.83e+00 7.94e-02 -5.7 8.23e+01 - 8.73e-01 9.30e-01f 1\n", + " 25 -1.2766311e+01 1.99e+00 2.60e-02 -5.7 3.16e+01 - 1.00e+00 1.00e+00f 1\n", + " 26 -1.2766561e+01 2.73e-01 7.90e-04 -5.7 2.07e+01 -7.8 1.00e+00 1.00e+00h 1\n", + " 27 -1.2766765e+01 6.11e-01 2.75e-03 -5.7 3.40e+02 - 4.24e-01 4.80e-02h 2\n", + " 28 -1.2766866e+01 3.52e-02 1.15e-04 -5.7 5.75e+00 - 1.00e+00 1.00e+00h 1\n", + " 29 -1.2766913e+01 1.08e-02 5.42e-05 -5.7 3.27e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 30 -1.2766912e+01 5.95e-06 4.85e-08 -5.7 5.76e-02 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (340818)\n", + " 31 -1.2767524e+01 5.55e-01 1.25e-03 -8.6 2.33e+01 - 8.91e-01 9.61e-01h 1\n", + " 32 -1.2767558e+01 1.54e-02 2.57e-04 -8.6 1.09e+01 - 9.90e-01 1.00e+00h 1\n", + " 33 -1.2767563e+01 3.45e-03 5.70e-05 -8.6 5.26e+00 - 1.00e+00 1.00e+00h 1\n", + " 34 -1.2767564e+01 1.62e-04 3.39e-06 -8.6 1.15e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (358770)\n", + " 35 -1.2767564e+01 2.90e-07 1.23e-08 -8.6 4.88e-02 - 1.00e+00 1.00e+00h 1\n", + " 36 -1.2767564e+01 2.51e-12 1.99e-13 -8.6 1.44e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 36\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.2767564482596377e+01 -1.2767564482596377e+01\n", + "Dual infeasibility......: 1.9925310697126672e-13 1.9925310697126672e-13\n", + "Constraint violation....: 2.5131008385415043e-12 2.5131008385415043e-12\n", + "Complementarity.........: 2.5059036300337093e-09 2.5059036300337093e-09\n", + "Overall NLP error.......: 2.5059036300337093e-09 2.5059036300337093e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 51\n", + "Number of objective gradient evaluations = 37\n", + "Number of equality constraint evaluations = 51\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 37\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 36\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.271\n", + "Total CPU secs in NLP function evaluations = 0.053\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 5.03e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 4.31e+00 3.85e+02 -1.0 4.98e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.90e-02 3.85e+00 -1.0 4.60e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.84e-04 4.39e+00 -1.0 5.20e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.14e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8342664033261826e-11 5.8342664033261826e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8342664033261826e-11 5.8342664033261826e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.128\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 3.9181573e-01 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 3.9115328e-01 1.56e-01 1.04e+00 -1.0 5.95e-01 - 9.79e-01 8.75e-01h 1\n", + " 2 3.9160960e-01 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 3.8712768e-01 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", + " 4 3.9644619e-01 7.12e-01 2.48e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 3.9432311e-01 5.85e-01 4.73e+01 -1.0 3.46e+02 - 4.77e-01 8.89e-02f 2\n", + " 6 4.0017008e-01 1.36e-01 6.05e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", + " 7 3.9917425e-01 4.34e-01 2.97e-01 -1.0 5.49e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 3.9967295e-01 3.84e-02 5.77e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 3.9964620e-01 5.42e-03 3.94e-03 -2.5 6.37e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 3.9941445e-01 5.23e-04 4.68e-04 -3.8 8.72e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 3.9556789e-01 2.81e-01 2.38e-02 -5.7 2.46e+01 - 7.68e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (320660)\n", + " 12 3.9049239e-01 8.99e-01 1.14e-02 -5.7 5.36e+01 - 8.66e-01 1.00e+00h 1\n", + " 13 3.8696936e-01 7.77e-01 2.66e-02 -5.7 3.18e+01 - 8.89e-01 1.00e+00h 1\n", + " 14 3.8779264e-01 1.12e-02 4.89e-04 -5.7 7.59e-01 -4.5 9.97e-01 1.00e+00h 1\n", + " 15 3.8775852e-01 6.21e-07 1.98e-07 -5.7 1.51e-02 -5.0 1.00e+00 1.00e+00h 1\n", + " 16 3.8730660e-01 3.80e-01 2.43e-02 -8.6 1.44e+04 - 7.13e-03 2.64e-03h 1\n", + " 17 3.8571434e-01 1.57e-01 4.82e-03 -8.6 3.21e+01 - 8.01e-01 1.00e+00h 1\n", + " 18 3.8604273e-01 1.97e-02 3.18e-04 -8.6 1.23e+01 - 9.78e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (352373)\n", + " 19 3.8603975e-01 2.68e-03 8.27e-06 -8.6 4.65e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 3.8603975e-01 4.52e-05 4.68e-08 -8.6 6.09e-01 - 1.00e+00 1.00e+00h 1\n", + " 21 3.8603975e-01 1.34e-08 4.74e-12 -8.6 1.05e-02 - 1.00e+00 1.00e+00h 1\n", + " 22 3.8603911e-01 1.37e-06 2.67e-08 -9.0 1.06e-01 - 1.00e+00 1.00e+00h 1\n", + " 23 3.8603911e-01 4.37e-11 1.28e-12 -9.0 5.99e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 23\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 3.8603910736158609e-01 3.8603910736158609e-01\n", + "Dual infeasibility......: 1.2773062897591488e-12 1.2773062897591488e-12\n", + "Constraint violation....: 4.3745562727792731e-11 4.3745562727792731e-11\n", + "Complementarity.........: 9.0909138582673236e-10 9.0909138582673236e-10\n", + "Overall NLP error.......: 9.0909138582673236e-10 9.0909138582673236e-10\n", + "\n", + "\n", + "Number of objective function evaluations = 28\n", + "Number of objective gradient evaluations = 24\n", + "Number of equality constraint evaluations = 28\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 24\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 23\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.951\n", + "Total CPU secs in NLP function evaluations = 0.031\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.7 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.26e-03 4.84e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 1.80e-09 1.40e-06 -1.0 1.60e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.8027450643387510e-09 1.8027450643387510e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.8027450643387510e-09 1.8027450643387510e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.38e-03 2.49e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 2.17e-09 1.48e-06 -1.0 1.75e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.1657315940615263e-09 2.1657315940615263e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.1657315940615263e-09 2.1657315940615263e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.106\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 0.9 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.23e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 6.99e+02 3.85e+02 -1.0 1.23e+04 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 7.92e+00 3.85e+00 -1.0 7.46e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 7.83e-02 4.39e+00 -1.0 8.40e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.31e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.130\n", + "Total CPU secs in NLP function evaluations = 0.008\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.2867454e+01 1.25e+00 1.05e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.2868460e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", + " 2 -1.2867639e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.2871924e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.2863717e+01 1.67e+01 2.19e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.2864858e+01 1.18e+01 3.21e+01 -1.0 1.36e+02 - 1.00e+00 4.12e-01f 2\n", + " 6 -1.2861618e+01 1.23e+01 3.32e+00 -1.0 9.24e+01 - 8.74e-01 3.35e-01f 2\n", + " 7 -1.2858508e+01 3.66e+00 5.37e-01 -1.0 5.24e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.2859854e+01 4.51e-01 7.96e-02 -1.0 1.10e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.2859854e+01 2.44e-04 1.19e-03 -1.7 3.79e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.2859885e+01 2.06e-04 8.00e-04 -3.8 3.33e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 -1.2863468e+01 2.88e+00 2.39e-02 -5.7 3.78e+01 - 7.65e-01 1.00e+00h 1\n", + " 12 -1.2868524e+01 5.45e+00 1.49e-02 -5.7 7.69e+01 - 8.12e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (324125)\n", + " 13 -1.2871542e+01 4.87e+00 1.57e-02 -5.7 3.27e+01 - 9.43e-01 1.00e+00h 1\n", + " 14 -1.2871505e+01 4.47e+00 5.03e-02 -5.7 1.42e+01 -4.0 1.00e+00 8.24e-02h 1\n", + " 15 -1.2871544e+01 7.88e-01 1.77e-02 -5.7 3.24e+01 - 1.00e+00 1.00e+00h 1\n", + " 16 -1.2871459e+01 1.19e-03 1.13e-04 -5.7 4.55e-01 -4.5 1.00e+00 1.00e+00h 1\n", + " 17 -1.2871817e+01 6.63e-01 4.65e-03 -5.7 4.38e+01 - 1.00e+00 4.24e-01h 2\n", + " 18 -1.2872068e+01 6.22e-01 3.65e-03 -5.7 3.60e+01 - 1.00e+00 3.97e-01h 2\n", + " 19 -1.2872235e+01 1.80e-02 4.42e-04 -5.7 5.48e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.2872228e+01 2.81e-04 1.04e-06 -5.7 7.92e-01 - 1.00e+00 1.00e+00h 1\n", + " 21 -1.2872228e+01 6.84e-09 4.81e-11 -5.7 2.27e-03 - 1.00e+00 1.00e+00h 1\n", + " 22 -1.2872836e+01 6.22e-01 1.32e-03 -8.6 2.46e+01 - 8.84e-01 9.57e-01h 1\n", + "Reallocating memory for MA57: lfact (352404)\n", + " 23 -1.2872873e+01 1.64e-02 2.58e-04 -8.6 1.12e+01 - 9.88e-01 1.00e+00h 1\n", + " 24 -1.2872878e+01 3.83e-03 5.93e-05 -8.6 5.54e+00 - 1.00e+00 1.00e+00h 1\n", + " 25 -1.2872880e+01 2.02e-04 3.88e-06 -8.6 1.28e+00 - 1.00e+00 1.00e+00h 1\n", + " 26 -1.2872880e+01 4.51e-07 1.70e-08 -8.6 6.08e-02 - 1.00e+00 1.00e+00h 1\n", + " 27 -1.2872880e+01 5.38e-12 3.77e-13 -8.6 2.10e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 27\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.2872879704090735e+01 -1.2872879704090735e+01\n", + "Dual infeasibility......: 3.7744241620007363e-13 3.7744241620007363e-13\n", + "Constraint violation....: 5.3804738442408961e-12 5.3804738442408961e-12\n", + "Complementarity.........: 2.5059037015571048e-09 2.5059037015571048e-09\n", + "Overall NLP error.......: 2.5059037015571048e-09 2.5059037015571048e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 43\n", + "Number of objective gradient evaluations = 28\n", + "Number of equality constraint evaluations = 43\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 28\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 27\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.074\n", + "Total CPU secs in NLP function evaluations = 0.067\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 3.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 5.38e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 4.34e+00 3.85e+02 -1.0 5.32e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.93e-02 3.85e+00 -1.0 4.63e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.87e-04 4.39e+00 -1.0 5.23e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.17e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.127\n", + "Total CPU secs in NLP function evaluations = 0.007\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 2.8614991e-01 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 2.8552993e-01 1.56e-01 1.04e+00 -1.0 5.94e-01 - 9.79e-01 8.75e-01h 1\n", + " 2 2.8596341e-01 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 2.8174414e-01 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", + " 4 2.9050364e-01 7.12e-01 2.47e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 2.8851199e-01 5.85e-01 4.73e+01 -1.0 3.46e+02 - 4.77e-01 8.91e-02f 2\n", + " 6 2.9401108e-01 1.36e-01 6.08e+00 -1.0 2.40e+01 -4.0 8.22e-01 1.00e+00F 1\n", + " 7 2.9307524e-01 4.34e-01 2.97e-01 -1.0 5.49e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 2.9354482e-01 3.83e-02 5.77e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 2.9352137e-01 5.43e-03 3.97e-03 -2.5 6.38e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 2.9331638e-01 5.29e-04 4.27e-04 -3.8 8.79e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 2.8988055e-01 2.51e-01 2.28e-02 -5.7 2.32e+01 - 7.77e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (319389)\n", + " 12 2.8501317e-01 9.24e-01 1.13e-02 -5.7 5.42e+01 - 8.69e-01 1.00e+00h 1\n", + " 13 2.8160648e-01 7.91e-01 2.55e-02 -5.7 3.22e+01 - 9.07e-01 1.00e+00h 1\n", + " 14 2.8240769e-01 1.11e-02 4.72e-04 -5.7 7.74e-01 -4.5 1.00e+00 1.00e+00h 1\n", + " 15 2.8237479e-01 6.27e-07 1.90e-07 -5.7 1.42e-02 -5.0 1.00e+00 1.00e+00h 1\n", + " 16 2.8187382e-01 3.93e-01 2.36e-02 -8.6 6.25e+02 - 1.42e-01 6.14e-02h 1\n", + " 17 2.8044338e-01 1.41e-01 4.08e-03 -8.6 3.11e+01 - 8.06e-01 1.00e+00h 1\n", + " 18 2.8072577e-01 1.89e-02 2.94e-04 -8.6 1.21e+01 - 9.80e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (348083)\n", + " 19 2.8072395e-01 2.58e-03 1.02e-05 -8.6 4.56e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 2.8072390e-01 4.67e-05 1.13e-07 -8.6 6.18e-01 - 1.00e+00 1.00e+00h 1\n", + " 21 2.8072390e-01 1.53e-08 1.88e-11 -8.6 1.12e-02 - 1.00e+00 1.00e+00h 1\n", + " 22 2.8072325e-01 1.54e-06 2.83e-08 -9.0 1.12e-01 - 1.00e+00 1.00e+00h 1\n", + " 23 2.8072325e-01 5.55e-11 1.52e-12 -9.0 6.75e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 23\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 2.8072325129350517e-01 2.8072325129350517e-01\n", + "Dual infeasibility......: 1.5217814371823493e-12 1.5217814371823493e-12\n", + "Constraint violation....: 5.5502491491665751e-11 5.5502491491665751e-11\n", + "Complementarity.........: 9.0909147761315399e-10 9.0909147761315399e-10\n", + "Overall NLP error.......: 9.0909147761315399e-10 9.0909147761315399e-10\n", + "\n", + "\n", + "Number of objective function evaluations = 28\n", + "Number of objective gradient evaluations = 24\n", + "Number of equality constraint evaluations = 28\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 24\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 23\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.822\n", + "Total CPU secs in NLP function evaluations = 0.040\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.6 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.25e-03 5.07e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 1.77e-09 1.39e-06 -1.0 1.58e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.7689547604504696e-09 1.7689547604504696e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.7689547604504696e-09 1.7689547604504696e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", + "Total CPU secs in NLP function evaluations = 0.005\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.38e-03 2.57e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", + " 4 0.0000000e+00 2.15e-09 1.48e-06 -1.0 1.75e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.1522383875094420e-09 2.1522383875094420e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.1522383875094420e-09 2.1522383875094420e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.0 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.30e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 7.07e+02 3.85e+02 -1.0 1.30e+04 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 8.00e+00 3.85e+00 -1.0 7.54e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 7.91e-02 4.39e+00 -1.0 8.48e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.38e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.129\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.2967058e+01 1.25e+00 1.04e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.2968006e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", + " 2 -1.2967227e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.2971272e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.2963530e+01 1.67e+01 2.19e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.2964604e+01 1.18e+01 3.21e+01 -1.0 1.36e+02 - 1.00e+00 4.12e-01f 2\n", + " 6 -1.2961547e+01 1.23e+01 3.32e+00 -1.0 9.24e+01 - 8.74e-01 3.35e-01f 2\n", + " 7 -1.2958610e+01 3.66e+00 5.37e-01 -1.0 5.25e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.2959880e+01 4.51e-01 7.97e-02 -1.0 1.11e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.2959880e+01 2.45e-04 1.16e-03 -1.7 3.82e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.2959908e+01 1.85e-04 7.57e-04 -3.8 3.15e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 -1.2963121e+01 2.60e+00 2.30e-02 -5.7 3.59e+01 - 7.75e-01 1.00e+00h 1\n", + " 12 -1.2967974e+01 5.66e+00 1.48e-02 -5.7 7.78e+01 - 8.10e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (319946)\n", + " 13 -1.2970867e+01 4.77e+00 1.46e-02 -5.7 3.30e+01 - 9.60e-01 1.00e+00h 1\n", + " 14 -1.2970834e+01 4.39e+00 4.99e-02 -5.7 1.40e+01 -4.0 1.00e+00 7.87e-02h 1\n", + " 15 -1.2970871e+01 8.04e-01 1.66e-02 -5.7 3.26e+01 - 1.00e+00 1.00e+00h 1\n", + " 16 -1.2970791e+01 1.23e-03 1.24e-04 -5.7 4.78e-01 -4.5 1.00e+00 1.00e+00h 1\n", + " 17 -1.2971470e+01 2.07e+00 1.31e-02 -5.7 3.26e+01 - 1.00e+00 1.00e+00h 1\n", + " 18 -1.2971390e+01 2.65e-01 2.02e-03 -5.7 1.09e+01 - 1.00e+00 1.00e+00h 1\n", + " 19 -1.2971539e+01 1.75e-01 7.49e-04 -5.7 1.26e+01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.2971523e+01 4.44e-03 2.61e-05 -5.7 1.85e+00 - 1.00e+00 1.00e+00h 1\n", + " 21 -1.2971523e+01 2.35e-06 1.59e-08 -5.7 3.77e-02 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (342795)\n", + " 22 -1.2972127e+01 6.92e-01 1.39e-03 -8.6 2.60e+01 - 8.78e-01 9.52e-01h 1\n", + " 23 -1.2972167e+01 1.73e-02 2.59e-04 -8.6 1.15e+01 - 9.86e-01 1.00e+00h 1\n", + " 24 -1.2972172e+01 4.22e-03 6.14e-05 -8.6 5.81e+00 - 1.00e+00 1.00e+00h 1\n", + " 25 -1.2972173e+01 2.46e-04 4.38e-06 -8.6 1.42e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (361301)\n", + " 26 -1.2972174e+01 6.76e-07 2.28e-08 -8.6 7.45e-02 - 1.00e+00 1.00e+00h 1\n", + " 27 -1.2972174e+01 1.08e-11 6.93e-13 -8.6 2.98e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 27\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.2972173508586897e+01 -1.2972173508586897e+01\n", + "Dual infeasibility......: 6.9302411901920606e-13 6.9302411901920606e-13\n", + "Constraint violation....: 1.0807355010911124e-11 1.0807355010911124e-11\n", + "Complementarity.........: 2.5059038289136340e-09 2.5059038289136340e-09\n", + "Overall NLP error.......: 2.5059038289136340e-09 2.5059038289136340e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 34\n", + "Number of objective gradient evaluations = 28\n", + "Number of equality constraint evaluations = 34\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 28\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 27\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.039\n", + "Total CPU secs in NLP function evaluations = 0.034\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.9 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 5.72e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 4.37e+00 3.85e+02 -1.0 5.66e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.96e-02 3.85e+00 -1.0 4.66e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.91e-04 4.39e+00 -1.0 5.27e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.21e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.130\n", + "Total CPU secs in NLP function evaluations = 0.005\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 1.8654549e-01 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 1.8596289e-01 1.56e-01 1.03e+00 -1.0 5.94e-01 - 9.79e-01 8.75e-01h 1\n", + " 2 1.8637565e-01 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 1.8238996e-01 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", + " 4 1.9065372e-01 7.12e-01 2.47e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 1.8877820e-01 5.85e-01 4.73e+01 -1.0 3.46e+02 - 4.78e-01 8.92e-02f 2\n", + " 6 1.9396149e-01 1.37e-01 6.06e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", + " 7 1.9308197e-01 4.34e-01 2.96e-01 -1.0 5.49e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 1.9352656e-01 3.83e-02 5.78e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 1.9350624e-01 5.45e-03 4.00e-03 -2.5 6.39e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 1.9332380e-01 5.30e-04 3.89e-04 -3.8 9.48e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 1.9023661e-01 2.26e-01 2.20e-02 -5.7 2.19e+01 - 7.86e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (320076)\n", + " 12 1.8556544e-01 9.45e-01 1.12e-02 -5.7 5.48e+01 - 8.73e-01 1.00e+00h 1\n", + " 13 1.8226712e-01 8.03e-01 2.46e-02 -5.7 3.25e+01 - 9.25e-01 1.00e+00h 1\n", + " 14 1.8304698e-01 1.11e-02 4.57e-04 -5.7 7.87e-01 -4.5 1.00e+00 1.00e+00h 1\n", + " 15 1.8301518e-01 6.34e-07 1.83e-07 -5.7 1.33e-02 -5.0 1.00e+00 1.00e+00h 1\n", + " 16 1.8246962e-01 4.04e-01 2.28e-02 -8.6 3.22e+02 - 2.45e-01 1.20e-01h 1\n", + " 17 1.8118955e-01 1.25e-01 3.47e-03 -8.6 2.99e+01 - 8.12e-01 1.00e+00h 1\n", + " 18 1.8143090e-01 1.83e-02 2.70e-04 -8.6 1.19e+01 - 9.83e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (352539)\n", + " 19 1.8142961e-01 2.51e-03 1.26e-05 -8.6 4.50e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 1.8142950e-01 4.81e-05 1.90e-07 -8.6 6.28e-01 - 1.00e+00 1.00e+00h 1\n", + " 21 1.8142949e-01 1.74e-08 5.29e-11 -8.6 1.19e-02 - 1.00e+00 1.00e+00h 1\n", + " 22 1.8142885e-01 1.72e-06 2.99e-08 -9.0 1.19e-01 - 1.00e+00 1.00e+00h 1\n", + " 23 1.8142885e-01 6.95e-11 1.80e-12 -9.0 7.55e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 23\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 1.8142884693002581e-01 1.8142884693002581e-01\n", + "Dual infeasibility......: 1.7962352785824499e-12 1.7962352785824499e-12\n", + "Constraint violation....: 6.9450334372334055e-11 6.9450334372334055e-11\n", + "Complementarity.........: 9.0909158006744088e-10 9.0909158006744088e-10\n", + "Overall NLP error.......: 9.0909158006744088e-10 9.0909158006744088e-10\n", + "\n", + "\n", + "Number of objective function evaluations = 28\n", + "Number of objective gradient evaluations = 24\n", + "Number of equality constraint evaluations = 28\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 24\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 23\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.945\n", + "Total CPU secs in NLP function evaluations = 0.037\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.7 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.24e-03 5.30e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 1.74e-09 1.38e-06 -1.0 1.57e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.7354877535069591e-09 1.7354877535069591e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.7354877535069591e-09 1.7354877535069591e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.107\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.38e-03 2.66e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 2.14e-09 1.48e-06 -1.0 1.74e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.1387873694322934e-09 2.1387873694322934e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.1387873694322934e-09 2.1387873694322934e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.107\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.3 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.38e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 7.15e+02 3.85e+02 -1.0 1.38e+04 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 8.08e+00 3.85e+00 -1.0 7.61e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 7.99e-02 4.39e+00 -1.0 8.56e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.46e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0367393568158150e-09 9.0367393568158150e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0367393568158150e-09 9.0367393568158150e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.129\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.3061258e+01 1.25e+00 1.04e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.3062155e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.07e-01h 1\n", + " 2 -1.3061413e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.3065245e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.3057918e+01 1.67e+01 2.19e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.3058931e+01 1.18e+01 3.21e+01 -1.0 1.36e+02 - 1.00e+00 4.12e-01f 2\n", + " 6 -1.3056038e+01 1.23e+01 3.32e+00 -1.0 9.24e+01 - 8.74e-01 3.35e-01f 2\n", + " 7 -1.3053256e+01 3.67e+00 5.37e-01 -1.0 5.25e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.3054459e+01 4.52e-01 7.98e-02 -1.0 1.11e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.3054459e+01 2.46e-04 1.14e-03 -1.7 3.85e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.3054484e+01 1.66e-04 7.18e-04 -3.8 2.99e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 -1.3057381e+01 2.35e+00 2.21e-02 -5.7 3.43e+01 - 7.84e-01 1.00e+00h 1\n", + " 12 -1.3062042e+01 5.84e+00 1.46e-02 -5.7 7.86e+01 - 8.08e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (325047)\n", + " 13 -1.3064822e+01 4.69e+00 1.36e-02 -5.7 3.33e+01 - 9.76e-01 1.00e+00h 1\n", + " 14 -1.3064793e+01 4.33e+00 4.95e-02 -5.7 1.38e+01 -4.0 1.00e+00 7.55e-02h 1\n", + " 15 -1.3064825e+01 8.10e-01 1.55e-02 -5.7 3.27e+01 - 1.00e+00 1.00e+00h 1\n", + " 16 -1.3064750e+01 1.24e-03 1.35e-04 -5.7 4.92e-01 -4.5 1.00e+00 1.00e+00h 1\n", + " 17 -1.3065327e+01 1.32e+00 8.45e-03 -5.7 2.57e+01 - 1.00e+00 1.00e+00h 1\n", + " 18 -1.3065563e+01 3.66e-01 9.07e-04 -5.7 2.45e+01 - 1.00e+00 1.00e+00h 1\n", + " 19 -1.3065455e+01 8.75e-02 3.46e-04 -5.7 9.07e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.3065447e+01 1.22e-03 6.52e-06 -5.7 9.79e-01 - 1.00e+00 1.00e+00h 1\n", + " 21 -1.3065447e+01 1.73e-07 1.14e-09 -5.7 1.04e-02 - 1.00e+00 1.00e+00h 1\n", + " 22 -1.3066047e+01 7.66e-01 1.47e-03 -8.6 2.73e+01 - 8.72e-01 9.48e-01h 1\n", + "Reallocating memory for MA57: lfact (349926)\n", + " 23 -1.3066091e+01 1.83e-02 2.59e-04 -8.6 1.18e+01 - 9.84e-01 1.00e+00h 1\n", + " 24 -1.3066096e+01 4.62e-03 6.34e-05 -8.6 6.08e+00 - 1.00e+00 1.00e+00h 1\n", + " 25 -1.3066097e+01 2.96e-04 4.89e-06 -8.6 1.55e+00 - 1.00e+00 1.00e+00h 1\n", + " 26 -1.3066097e+01 9.81e-07 2.99e-08 -8.6 8.97e-02 - 1.00e+00 1.00e+00h 1\n", + " 27 -1.3066097e+01 2.05e-11 1.23e-12 -8.6 4.11e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 27\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.3066097364112752e+01 -1.3066097364112752e+01\n", + "Dual infeasibility......: 1.2294764961011457e-12 1.2294764961011457e-12\n", + "Constraint violation....: 2.0529578037553620e-11 2.0529578037553620e-11\n", + "Complementarity.........: 2.5059040443903054e-09 2.5059040443903054e-09\n", + "Overall NLP error.......: 2.5059040443903054e-09 2.5059040443903054e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 34\n", + "Number of objective gradient evaluations = 28\n", + "Number of equality constraint evaluations = 34\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 28\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 27\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.154\n", + "Total CPU secs in NLP function evaluations = 0.047\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.8 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.06e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 4.40e+00 3.85e+02 -1.0 6.00e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 4.99e-02 3.85e+00 -1.0 4.69e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.94e-04 4.39e+00 -1.0 5.30e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.24e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8342664033261826e-11 5.8342664033261826e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8342664033261826e-11 5.8342664033261826e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.145\n", + "Total CPU secs in NLP function evaluations = 0.007\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 9.2344539e-02 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 9.1795096e-02 1.56e-01 1.03e+00 -1.0 5.94e-01 - 9.79e-01 8.75e-01h 1\n", + " 2 9.2188972e-02 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 8.8412352e-02 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", + " 4 9.6233482e-02 7.12e-01 2.47e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 9.4461313e-02 5.85e-01 4.74e+01 -1.0 3.45e+02 - 4.78e-01 8.92e-02f 2\n", + " 6 9.9368726e-02 1.36e-01 6.08e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", + " 7 9.8535727e-02 4.34e-01 2.97e-01 -1.0 5.49e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 9.8957264e-02 3.83e-02 5.78e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 9.8939356e-02 5.46e-03 4.02e-03 -2.5 6.39e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 9.8775901e-02 5.26e-04 3.62e-04 -3.8 1.01e+00 - 1.00e+00 1.00e+00h 1\n", + " 11 9.5986597e-02 2.04e-01 2.11e-02 -5.7 2.08e+01 - 7.94e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (319905)\n", + " 12 9.1498624e-02 9.64e-01 1.11e-02 -5.7 5.52e+01 - 8.77e-01 1.00e+00h 1\n", + " 13 8.8299544e-02 8.17e-01 2.37e-02 -5.7 3.28e+01 - 9.41e-01 1.00e+00h 1\n", + " 14 8.9059447e-02 1.11e-02 4.46e-04 -5.7 8.02e-01 -4.5 1.00e+00 1.00e+00h 1\n", + " 15 8.9028548e-02 6.44e-07 1.79e-07 -5.7 1.25e-02 -5.0 1.00e+00 1.00e+00h 1\n", + " 16 8.8442722e-02 4.14e-01 2.20e-02 -8.6 2.18e+02 - 3.15e-01 1.78e-01h 1\n", + " 17 8.7303204e-02 1.14e-01 2.94e-03 -8.6 2.87e+01 - 8.18e-01 1.00e+00h 1\n", + " 18 8.7506248e-02 1.77e-02 2.54e-04 -8.6 1.17e+01 - 9.86e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (349182)\n", + " 19 8.7505239e-02 2.49e-03 1.53e-05 -8.6 4.48e+00 - 1.00e+00 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (366797)\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 8.7505073e-02 5.20e-05 3.04e-07 -8.6 6.52e-01 - 1.00e+00 1.00e+00h 1\n", + " 21 8.7505067e-02 2.17e-08 1.39e-10 -8.6 1.33e-02 - 1.00e+00 1.00e+00h 1\n", + " 22 8.7504422e-02 1.91e-06 3.15e-08 -9.0 1.25e-01 - 1.00e+00 1.00e+00h 1\n", + " 23 8.7504422e-02 1.03e-12 3.41e-10 -9.0 9.20e-05 -5.4 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 23\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 8.7504422324047759e-02 8.7504422324047759e-02\n", + "Dual infeasibility......: 3.4067176250603508e-10 3.4067176250603508e-10\n", + "Constraint violation....: 1.0300649222472202e-12 1.0300649222472202e-12\n", + "Complementarity.........: 9.0912236625038064e-10 9.0912236625038064e-10\n", + "Overall NLP error.......: 9.0912236625038064e-10 9.0912236625038064e-10\n", + "\n", + "\n", + "Number of objective function evaluations = 28\n", + "Number of objective gradient evaluations = 24\n", + "Number of equality constraint evaluations = 28\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 24\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 23\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.014\n", + "Total CPU secs in NLP function evaluations = 0.041\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.7 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.23e-03 5.54e-01 -1.0 1.85e-01 - 9.91e-01 9.92e-01h 1\n", + " 4 0.0000000e+00 1.70e-09 1.37e-06 -1.0 1.55e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.7023418230621701e-09 1.7023418230621701e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.7023418230621701e-09 1.7023418230621701e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.118\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.4 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.37e-03 2.74e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", + " 4 0.0000000e+00 2.13e-09 1.47e-06 -1.0 1.73e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.1253634407969457e-09 2.1253634407969457e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.1253634407969457e-09 2.1253634407969457e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.100\n", + "Total CPU secs in NLP function evaluations = 0.005\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.1 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.46e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (298604)\n", + " 1 0.0000000e+00 7.22e+02 3.85e+02 -1.0 1.46e+04 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 8.15e+00 3.85e+00 -1.0 7.69e+02 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 8.06e-02 4.39e+00 -1.0 8.63e+00 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.54e-02 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.131\n", + "Total CPU secs in NLP function evaluations = 0.009\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 -1.3150611e+01 1.25e+00 1.04e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 -1.3151462e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.07e-01h 1\n", + " 2 -1.3150755e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", + " 3 -1.3154393e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", + " 4 -1.3147440e+01 1.67e+01 2.19e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", + " 5 -1.3148399e+01 1.18e+01 3.21e+01 -1.0 1.36e+02 - 1.00e+00 4.12e-01f 2\n", + " 6 -1.3145653e+01 1.23e+01 3.32e+00 -1.0 9.24e+01 - 8.74e-01 3.35e-01f 2\n", + " 7 -1.3143010e+01 3.67e+00 5.37e-01 -1.0 5.25e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 -1.3144153e+01 4.52e-01 8.00e-02 -1.0 1.11e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 -1.3144153e+01 2.47e-04 1.12e-03 -1.7 3.87e-01 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 -1.3144175e+01 1.50e-04 6.83e-04 -3.8 2.85e-01 - 1.00e+00 1.00e+00h 1\n", + " 11 -1.3144232e+01 1.07e-04 5.80e-05 -5.7 5.80e-01 -4.0 1.00e+00 1.00e+00h 1\n", + " 12 -1.3144447e+01 1.51e-03 7.27e-05 -5.7 2.18e+00 -4.5 1.00e+00 1.00e+00h 1\n", + " 13 -1.3144772e+01 4.00e-03 2.07e-04 -5.7 5.35e+00 -5.0 1.00e+00 6.14e-01h 1\n", + " 14 -1.3144770e+01 6.95e-04 3.62e-04 -5.7 1.73e-01 -5.4 1.00e+00 1.00e+00f 1\n", + " 15 -1.3144945e+01 6.78e-03 3.62e-05 -5.7 1.70e+00 -5.9 1.00e+00 1.00e+00h 1\n", + " 16 -1.3153640e+01 3.49e+01 9.15e-02 -5.7 2.83e+03 - 6.17e-02 6.37e-02h 1\n", + " 17 -1.3154824e+01 1.98e+01 3.84e-02 -5.7 8.27e+01 - 1.00e+00 5.97e-01f 1\n", + " 18 -1.3154428e+01 6.27e-01 2.75e-03 -5.7 1.22e+01 - 1.00e+00 1.00e+00f 1\n", + "Reallocating memory for MA57: lfact (319597)\n", + " 19 -1.3154555e+01 6.10e-02 9.67e-05 -5.7 8.95e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.3154553e+01 3.35e-04 1.10e-06 -5.7 5.85e-01 - 1.00e+00 1.00e+00h 1\n", + " 21 -1.3154553e+01 1.64e-08 9.42e-11 -5.7 3.56e-03 - 1.00e+00 1.00e+00h 1\n", + " 22 -1.3155149e+01 8.44e-01 1.54e-03 -8.6 2.87e+01 - 8.65e-01 9.44e-01h 1\n", + "Reallocating memory for MA57: lfact (350471)\n", + " 23 -1.3155196e+01 1.92e-02 2.59e-04 -8.6 1.21e+01 - 9.82e-01 1.00e+00h 1\n", + " 24 -1.3155201e+01 5.02e-03 6.53e-05 -8.6 6.33e+00 - 1.00e+00 1.00e+00h 1\n", + " 25 -1.3155202e+01 3.50e-04 5.41e-06 -8.6 1.69e+00 - 1.00e+00 1.00e+00h 1\n", + " 26 -1.3155202e+01 1.38e-06 3.84e-08 -8.6 1.06e-01 - 1.00e+00 1.00e+00h 1\n", + " 27 -1.3155202e+01 3.71e-11 2.10e-12 -8.6 5.52e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 27\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.3155202417249349e+01 -1.3155202417249349e+01\n", + "Dual infeasibility......: 2.0969252789257937e-12 2.0969252789257937e-12\n", + "Constraint violation....: 3.7140956976600137e-11 3.7140956976600137e-11\n", + "Complementarity.........: 2.5059043930329935e-09 2.5059043930329935e-09\n", + "Overall NLP error.......: 2.5059043930329935e-09 2.5059043930329935e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 34\n", + "Number of objective gradient evaluations = 28\n", + "Number of equality constraint evaluations = 34\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 28\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 27\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 1.008\n", + "Total CPU secs in NLP function evaluations = 0.042\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.8 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26094\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2590\n", + "\n", + "Total number of variables............................: 7086\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7086\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.40e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (299488)\n", + " 1 0.0000000e+00 4.43e+00 3.85e+02 -1.0 6.34e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 5.03e-02 3.85e+00 -1.0 4.73e+00 - 9.90e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 4.97e-04 4.39e+00 -1.0 5.33e-02 - 1.00e+00 9.90e-01h 1\n", + " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.27e-04 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 5.8342664033261826e-11 5.8342664033261826e-11\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 5.8342664033261826e-11 5.8342664033261826e-11\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.133\n", + "Total CPU secs in NLP function evaluations = 0.006\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 26208\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2610\n", + "\n", + "Reallocating memory for MA57: lfact (275736)\n", + "Total number of variables............................: 7106\n", + " variables with only lower bounds: 2316\n", + " variables with lower and upper bounds: 794\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 7096\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 2.9907566e-03 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (303702)\n", + " 1 2.4709143e-03 1.56e-01 1.03e+00 -1.0 5.94e-01 - 9.79e-01 8.75e-01h 1\n", + " 2 2.8475281e-03 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", + " 3 -7.4086018e-04 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", + " 4 6.6826430e-03 7.12e-01 2.47e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", + " 5 5.0030420e-03 5.85e-01 4.74e+01 -1.0 3.45e+02 - 4.79e-01 8.93e-02f 2\n", + " 6 9.6602861e-03 1.37e-01 6.09e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", + " 7 8.8703660e-03 4.34e-01 2.96e-01 -1.0 5.49e+01 - 1.00e+00 1.00e+00h 1\n", + " 8 9.2713861e-03 3.83e-02 5.78e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", + " 9 9.2556032e-03 5.47e-03 4.05e-03 -2.5 6.40e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 9.1083712e-03 5.20e-04 3.49e-04 -3.8 1.06e+00 - 1.00e+00 1.00e+00h 1\n", + " 11 6.5758713e-03 1.85e-01 2.04e-02 -5.7 1.97e+01 - 8.02e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (319911)\n", + " 12 2.2595047e-03 9.80e-01 1.09e-02 -5.7 5.56e+01 - 8.79e-01 1.00e+00h 1\n", + " 13 -8.3311568e-04 8.21e-01 2.27e-02 -5.7 3.29e+01 - 9.59e-01 1.00e+00h 1\n", + " 14 -9.6435386e-05 1.08e-02 4.25e-04 -5.7 8.07e-01 -4.5 1.00e+00 1.00e+00h 1\n", + " 15 -1.2586985e-04 6.29e-07 1.66e-07 -5.7 1.19e-02 -5.0 1.00e+00 1.00e+00h 1\n", + " 16 -7.5509198e-04 4.26e-01 2.13e-02 -8.6 1.63e+02 - 3.64e-01 2.39e-01h 1\n", + " 17 -1.7704572e-03 1.04e-01 2.48e-03 -8.6 2.75e+01 - 8.25e-01 1.00e+00h 1\n", + " 18 -1.5994877e-03 1.71e-02 2.40e-04 -8.6 1.15e+01 - 9.87e-01 1.00e+00h 1\n", + "Reallocating memory for MA57: lfact (351858)\n", + " 19 -1.6003069e-03 2.49e-03 1.75e-05 -8.6 4.48e+00 - 1.00e+00 1.00e+00h 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 20 -1.6005224e-03 5.64e-05 4.27e-07 -8.6 6.79e-01 - 1.00e+00 1.00e+00h 1\n", + " 21 -1.6005305e-03 2.71e-08 2.85e-10 -8.6 1.49e-02 - 1.00e+00 1.00e+00h 1\n", + " 22 -1.6005305e-03 2.86e-13 2.91e-14 -8.6 7.06e-06 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 22\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: -1.6005305371233902e-03 -1.6005305371233902e-03\n", + "Dual infeasibility......: 2.9139664390433465e-14 2.9139664390433465e-14\n", + "Constraint violation....: 1.1865809618004581e-13 2.8599345114344032e-13\n", + "Complementarity.........: 2.5059035600370655e-09 2.5059035600370655e-09\n", + "Overall NLP error.......: 2.5059035600370655e-09 2.5059035600370655e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 27\n", + "Number of objective gradient evaluations = 23\n", + "Number of equality constraint evaluations = 27\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 23\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 22\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.929\n", + "Total CPU secs in NLP function evaluations = 0.041\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 2.8 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.22e-03 5.77e-01 -1.0 1.85e-01 - 9.91e-01 9.92e-01h 1\n", + " 4 0.0000000e+00 1.67e-09 1.36e-06 -1.0 1.54e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.6695191895621520e-09 1.6695191895621520e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.6695191895621520e-09 1.6695191895621520e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.104\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.2 seconds\n", + "Ipopt 3.13.2: linear_solver=ma57\n", + "halt_on_ampl_error=yes\n", + "max_iter=3000\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma57.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 25344\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 2320\n", + "\n", + "Total number of variables............................: 6968\n", + " variables with only lower bounds: 2312\n", + " variables with lower and upper bounds: 784\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6968\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + "Reallocating memory for MA57: lfact (239448)\n", + " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", + " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", + " 3 0.0000000e+00 1.22e-03 5.77e-01 -1.0 1.85e-01 - 9.91e-01 9.92e-01h 1\n", + " 4 0.0000000e+00 1.67e-09 1.36e-06 -1.0 1.54e-03 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 4\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 1.6694885474066723e-09 1.6694885474066723e-09\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 1.6694885474066723e-09 1.6694885474066723e-09\n", + "\n", + "\n", + "Number of objective function evaluations = 5\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 5\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 5\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 4\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.105\n", + "Total CPU secs in NLP function evaluations = 0.004\n", + "\n", + "EXIT: Optimal Solution Found.\n", + "INFO: elapsed time: 1.1 seconds\n" + ] + } + ], + "source": [ + "D_sc = []\n", + "D_sc_2 = []\n", + "D_unsc = []\n", + "D_unsc_2 = []\n", + "exp_conds_sc = []\n", + "exp_conds_unsc = []\n", + "FIM_opt_sc = []\n", + "FIM_opt_sc_2 = []\n", + "FIM_opt_unsc = []\n", + "FIM_opt_unsc_2 = []\n", + "jac_opt_sc = []\n", + "jac_opt_sc_2 = []\n", + "jac_opt_unsc = []\n", + "jac_opt_unsc_2 = []\n", + "standard_Ca = 5\n", + "standard_T = [300, 300, 300, 300, 300, 300, 300, 300, 300, ]\n", + "\n", + "FIM_new = None\n", + "FIM_new_unsc = None\n", + "\n", + "FIM_running = np.zeros((4, 4))\n", + "FIM_running_unsc = np.zeros((4, 4))\n", + "\n", + "for i in range(20):\n", + " # Optimize experiment (scaled)\n", + " sc_res = run_optimal_exp(standard_Ca, standard_T, FIM_new, True)\n", + " # sc_res.result_analysis()\n", + " sc_exp = get_exp_conds(sc_res.model)\n", + " FIM_new = sc_res.FIM\n", + "\n", + " # Optimize experiment (unscaled)\n", + " unsc_res = run_optimal_exp(standard_Ca, standard_T, FIM_new_unsc, False)\n", + " # unsc_res.result_analysis()\n", + " unsc_exp = get_exp_conds(unsc_res.model)\n", + " FIM_new_unsc = unsc_res.FIM\n", + "\n", + " # Compute FIM in isolation (scaled)\n", + " res_sc = compute_specific_FIM(sc_exp[0], sc_exp[1:], prior_FIM=None, scale_param=True)\n", + "\n", + " # Compute FIM in isolation (unscaled)\n", + " res_unsc = compute_specific_FIM(unsc_exp[0], unsc_exp[1:], prior_FIM=None, scale_param=False)\n", + "\n", + " # Computing running isolation FIM\n", + " FIM_running += res_sc.FIM\n", + " FIM_running_unsc += res_unsc.FIM\n", + "\n", + " # Compute objectives (D-optimality)\n", + " D_sc.append(np.log10(np.linalg.det(sc_res.FIM)))\n", + " D_sc_2.append(np.log10(np.linalg.det(FIM_running)))\n", + " D_unsc.append(np.log10(np.linalg.det(unsc_res.FIM)))\n", + " D_unsc_2.append(np.log10(np.linalg.det(FIM_running_unsc)))\n", + "\n", + " # Append experimental results\n", + " exp_conds_sc.append(sc_exp)\n", + " exp_conds_unsc.append(unsc_exp)\n", + "\n", + " # Append FIM information\n", + " FIM_opt_sc.append(FIM_new)\n", + " FIM_opt_sc_2.append(copy.deepcopy(FIM_running))\n", + " FIM_opt_unsc.append(FIM_new_unsc)\n", + " FIM_opt_unsc_2.append(copy.deepcopy(FIM_running_unsc))\n", + "\n", + " # Append jacobian information\n", + " jac_opt_sc.append(translate_jac(sc_res.jaco_information))\n", + " jac_opt_sc_2.append(translate_jac(res_sc.jaco_information))\n", + " jac_opt_unsc.append(translate_jac(unsc_res.jaco_information))\n", + " jac_opt_unsc_2.append(translate_jac(res_unsc.jaco_information))" + ] + }, + { + "cell_type": "markdown", + "id": "17763935-a215-4020-a6b4-9b4425bec1f2", + "metadata": {}, + "source": [ + "## Plotting the results" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "4d9a37ac-1d7e-452a-ad55-e87191ff7d5c", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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woQJ7N69m59//plRo0axbNky+vfvzxtvvMEnn3zCM888Q2xsLFqtFr1eT7169fj111/x8PBg+/btvPDCC/j6+vL444+X+HrLlVJJYmJilOeee04JCgpS7O3tlfr16ytvv/22kpOTU+hxnTp1UgCTZeTIkSU6d0pKigIoKSkppbkEIYSoNFlZWcrx48eVrKwsw4a8dEX5kcpZ8tKLHXenTp2UsWPHFti+YMECxcXFxbg+dOhQJTAwUMnPzzduGzhwoDJo0CBFURTl999/V5ydnZXU1NQCdaWmpip2dnbKt99+W+y4PvzwQ6V169bG9SlTpijh4eHG9QYNGiiLFy82OWb69OlKZGSkoiiK8s033ygeHh63Pg9FUb766isFUA4cOFDsONq3b6+MGDHCZNvAgQOVBx980LgOKC+++KJJmYiICGXUqFGKohi+X82dd+jQoUrfvn2N6506dVLuv/9+43p+fr7i4OCgPPPMM8ZtcXFxCqDs2LHDYswvvfSSMmDAAIvnKakCv9u3Kcn3d6W14Jw4cQK9Xs8333xDw4YNOXr0KCNGjCAjI4PZs2cXeuyIESN45513jOtarba8wxVCCFHBmjVrhpWVlXHd19eXI0eOANCjRw8CAwOpX78+vXr1olevXvTv3x+tVktUVBQ5OTl069bNYt0///wzn376KdHR0aSnp5Ofn4+zs7PZshkZGURHRzN8+HBGjBhh3J6fn2/s1xIVFUVYWBj29vbG/ZGRkSW+5qioKF544QWTbffdd1+B20B31h0ZGXlXT2uFhYUZX1tZWeHh4UHz5s2N27y9vQFISEgwbvviiy+YP38+sbGxZGVlkZubW+onzspDpSU4N38hb6pfvz4nT57kq6++KjLB0Wq1+Pj4lHeIQghRfVhp4fH0yjt3MTk7O5OSklJge3JycoFOsDY2NibrKpUKvV4PGG4x7d+/n82bN/PXX3/x9ttvM3XqVPbs2YNGoyk0hh07dvDUU08xbdo0evbsiYuLC0uWLOGjjz4yWz493fC+fvvtt0RERJjsuz0Bq47Mvce3b1PduPV4831fsmQJr776Kh999BGRkZE4OTnx4YcfsmvXrooLupiqVCfjlJQU3N3diyz3448/4unpyT333MOkSZPIzCz8vnNOTg6pqakmixBC1CgqFVg7VM5Sgv43ISEh7N+/v8D2/fv307hx4xJd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/MLUqVN5//332bt3L76+vnz5ZeFJ4KVLl3jwwQdp27Ythw4d4quvvmLevHm8++67JuW+++47rK2t2b17N3PnzuXjjz/m//7v/wBYunQp9erV45133iEuLo64uDiL54uOjmbNmjWsXbuWn376iXnz5tGnTx8uXrzIli1b+OCDD3jrrbeMiSCAWq3m008/5dixY3z33Xf8/fffTJgwodDrqkrkFpUQQogKtXLlShwdHcnPzycnJwe1Ws3nn38OQE5ODu+//z4bNmwgMjISgPr16/Pvv//yzTff0KlTJ2JjY2nZsiVt2rQBDP1Qblq8eDFXr15lz549uLu7A9CwYUPj/vDwcMLDw43r06dPZ9myZaxYsYLRo0cXiPXff/9l9+7dJCQkYGdnB8Ds2bNZvnw5v/32Gy+88AJz5sxh+PDhDB8+HIB3332XDRs2FNqK8+WXX+Lv78/nn3+OSqWiSZMmXL58mYkTJ/L222+jVhvaH/z9/fnkk09QqVSEhIRw5MgRPvnkE0aMGIG7uztWVlY4OTnh4+NT6Huu1+uZP38+Tk5ONG3alC5dunDy5ElWr16NWq0mJCSEDz74gE2bNhEREQHAuHHjjMcHBQXx7rvv8uKLLxaZvFUVkuAIIYSoUF26dOGrr74iIyODTz75BGtrawYMGADAmTNnyMzMpEePHibH5Obm0rJlSwBGjRrFgAED2L9/Pw888AD9+vWjffv2ABw8eJCWLVsak5s7paenM3XqVFatWkVcXBz5+flkZWVZbME5dOgQ6enpeHh4mGzPysoiOjoagKioKF588UWT/ZGRkWzatMniexAVFUVkZKTxthLAfffdR3p6OhcvXiQgIACAe++916RMZGQkH330ETqdDisrK4v13ykoKAgnJyfjure3N1ZWVsZE6ua2hIQE4/qGDRuYMWMGJ06cIDU1lfz8fLKzs8nMzESr1Rb73JVFEhwhhBAVysHBwdiqMn/+fMLDw5k3bx7Dhw839vFYtWoVdevWNTnuZgtK7969OX/+PKtXr2b9+vV069aNl156idmzZxfZufbVV19l/fr1zJ49m4YNG6LRaHjsscfIzc01Wz49PR1fX182b95cYF9VfkT6TjY2NibrN59iu3PbzVt5586d46GHHmLUqFG89957uLu78++//zJ8+HByc3OrRYIjfXCEEEJUGrVazRtvvMFbb71F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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Rescaling the scaled results\n", + "D_rescaled = []\n", + "param_vals = np.array([[85, 370, 8, 15], ])\n", + "for i in range(20):\n", + " resc_FIM = rescale_FIM(FIM_opt_sc_2[i], param_vals)\n", + " D_rescaled.append(np.log10(np.linalg.det(resc_FIM)))\n", + "\n", + "plt.plot(range(20), D_sc, color='green', ls='-', label='Scaled, optimal')\n", + "plt.scatter(range(20), D_sc_2, color='green', label='Scaled, compute')\n", + "plt.plot(range(20), D_unsc, color='orange', ls='-', label='Unscaled, optimal')\n", + "plt.scatter(range(20), D_unsc_2, color='orange', ls='--', label='Unscaled, compute')\n", + "plt.plot(range(20), D_rescaled, color='purple', ls=':', lw=5, label='Rescaled optimal')\n", + "\n", + "plt.legend()\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyomo/contrib/doe/examples/reactor_optimize_doe_DJL.py b/pyomo/contrib/doe/examples/reactor_optimize_doe_DJL.py new file mode 100644 index 00000000000..719983069f0 --- /dev/null +++ b/pyomo/contrib/doe/examples/reactor_optimize_doe_DJL.py @@ -0,0 +1,180 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# +# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation +# Initiative (CCSI), and is copyright (c) 2022 by the software owners: +# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., +# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, +# Battelle Memorial Institute, University of Notre Dame, +# The University of Pittsburgh, The University of Texas at Austin, +# University of Toledo, West Virginia University, et al. All rights reserved. +# +# NOTICE. This Software was developed under funding from the +# U.S. Department of Energy and the U.S. Government consequently retains +# certain rights. As such, the U.S. Government has been granted for itself +# and others acting on its behalf a paid-up, nonexclusive, irrevocable, +# worldwide license in the Software to reproduce, distribute copies to the +# public, prepare derivative works, and perform publicly and display +# publicly, and to permit other to do so. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import numpy as np +from pyomo.contrib.doe.examples.reactor_kinetics import create_model, disc_for_measure +from pyomo.contrib.doe import DesignOfExperiments, MeasurementVariables, DesignVariables +import pyomo.environ as aml + +def get_exp_results(m): + vals = [aml.value(m.CA0[0]), ] + for i in [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]: + vals.append(aml.value(m.T[i])) + return vals + + +def get_FIM_from_exp(CA_0=None, T_0=None, prior=None): + if CA_0 is None: + CA_0 = 5 + if T_0 is None: + T_0 = [570, 300, 300, 300, 300, 300, 300, 300, 300] + ### Define inputs + # Control time set [h] + t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] + # Define parameter nominal value + parameter_dict = {"A1": 85, "A2": 372, "E1": 8, "E2": 15} + + # measurement object + measurements = MeasurementVariables() + measurements.add_variables( + "C", # name of measurement + indices={0: ["CA", "CB", "CC"], 1: t_control}, # indices of measurement + time_index_position=1, + ) # position of time index + + # design object + exp_design = DesignVariables() + + # add CAO as design variable + exp_design.add_variables( + "CA0", # name of design variable + indices={0: [0]}, # indices of design variable + time_index_position=0, # position of time index + values=[CA_0], # nominal value of design variable + lower_bounds=1, # lower bound of design variable + upper_bounds=5, # upper bound of design variable + ) + + # add T as design variable + exp_design.add_variables( + "T", # name of design variable + indices={0: t_control}, # indices of design variable + time_index_position=0, # position of time index + values=list(T_0), # nominal value of design variable + lower_bounds=300, # lower bound of design variable + upper_bounds=700, # upper bound of design variable + ) + + doe_object2 = DesignOfExperiments( + parameter_dict, # dictionary of parameters + exp_design, # design variables + measurements, # measurement variables + create_model, # function to create model + prior_FIM=prior, + discretize_model=disc_for_measure, # function to discretize model + ) + + result = doe_object2.compute_FIM( + mode='sequential_finite', + formula = 'central', + ) + + result.result_analysis() + + return result.FIM + + +def main(CA_0=None, T_0=None, prior=None): + if CA_0 is None: + CA_0 = 5 + if T_0 is None: + T_0 = [570, 300, 300, 300, 300, 300, 300, 300, 300] + ### Define inputs + # Control time set [h] + t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] + # Define parameter nominal value + parameter_dict = {"A1": 85, "A2": 372, "E1": 8, "E2": 15} + + # measurement object + measurements = MeasurementVariables() + measurements.add_variables( + "C", # name of measurement + indices={0: ["CA", "CB", "CC"], 1: t_control}, # indices of measurement + time_index_position=1, + ) # position of time index + + # design object + exp_design = DesignVariables() + + # add CAO as design variable + exp_design.add_variables( + "CA0", # name of design variable + indices={0: [0]}, # indices of design variable + time_index_position=0, # position of time index + values=[CA_0], # nominal value of design variable + lower_bounds=1, # lower bound of design variable + upper_bounds=5, # upper bound of design variable + ) + + # add T as design variable + exp_design.add_variables( + "T", # name of design variable + indices={0: t_control}, # indices of design variable + time_index_position=0, # position of time index + values=list(T_0), # nominal value of design variable + lower_bounds=300, # lower bound of design variable + upper_bounds=700, # upper bound of design variable + ) + + design_names = exp_design.variable_names + # exp1 = [5, 570, 300, 300, 300, 300, 300, 300, 300, 300] + # exp1_design_dict = dict(zip(design_names, exp1)) + # exp_design.update_values(exp1_design_dict) + + # add a prior information (scaled FIM with T=500 and T=300 experiments) + if prior is None: + prior = np.asarray( + [ + [28.67892806, 5.41249739, -81.73674601, -24.02377324], + [5.41249739, 26.40935036, -12.41816477, -139.23992532], + [-81.73674601, -12.41816477, 240.46276004, 58.76422806], + [-24.02377324, -139.23992532, 58.76422806, 767.25584508], + ] + ) + + doe_object2 = DesignOfExperiments( + parameter_dict, # dictionary of parameters + exp_design, # design variables + measurements, # measurement variables + create_model, # function to create model + prior_FIM=prior, # prior information + discretize_model=disc_for_measure, # function to discretize model + ) + + square_result, optimize_result = doe_object2.stochastic_program( + if_optimize=True, # if optimize + if_Cholesky=True, # if use Cholesky decomposition + # scale_nominal_param_value=True, # if scale nominal parameter value + objective_option="det", # objective option + L_initial=np.linalg.cholesky(prior), # initial Cholesky decomposition + ) + + return prior, optimize_result, square_result + + +if __name__ == "__main__": + main() diff --git a/pyomo/contrib/doe/result.py b/pyomo/contrib/doe/result.py deleted file mode 100644 index f7145ae2a46..00000000000 --- a/pyomo/contrib/doe/result.py +++ /dev/null @@ -1,758 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# -# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation -# Initiative (CCSI), and is copyright (c) 2022 by the software owners: -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, -# University of Toledo, West Virginia University, et al. All rights reserved. -# -# NOTICE. This Software was developed under funding from the -# U.S. Department of Energy and the U.S. Government consequently retains -# certain rights. As such, the U.S. Government has been granted for itself -# and others acting on its behalf a paid-up, nonexclusive, irrevocable, -# worldwide license in the Software to reproduce, distribute copies to the -# public, prepare derivative works, and perform publicly and display -# publicly, and to permit other to do so. -# ___________________________________________________________________________ - - -from pyomo.common.dependencies import numpy as np, pandas as pd, matplotlib as plt -from pyomo.core.expr.numvalue import value - -from itertools import product -import logging -from pyomo.opt import SolverStatus, TerminationCondition - - -class FisherResults: - def __init__( - self, - parameter_names, - measurements, - jacobian_info=None, - all_jacobian_info=None, - prior_FIM=None, - store_FIM=None, - scale_constant_value=1, - max_condition_number=1.0e12, - ): - """Analyze the FIM result for a single run - - Parameters - ---------- - parameter_names: - A ``list`` of parameter names - measurements: - A ``MeasurementVariables`` which contains the Pyomo variable names and their corresponding indices and - bounds for experimental measurements - jacobian_info: - the jacobian for this measurement object - all_jacobian_info: - the overall jacobian - prior_FIM: - if there's prior FIM to be added - store_FIM: - if storing the FIM in a .csv or .txt, give the file name here as a string - scale_constant_value: - scale all elements in Jacobian matrix, default is 1. - max_condition_number: - max condition number - """ - self.parameter_names = parameter_names - self.measurements = measurements - self.measurement_variables = measurements.variable_names - - if jacobian_info is None: - self.jaco_information = all_jacobian_info - else: - self.jaco_information = jacobian_info - self.all_jacobian_info = all_jacobian_info - - self.prior_FIM = prior_FIM - self.store_FIM = store_FIM - self.scale_constant_value = scale_constant_value - self.fim_scale_constant_value = scale_constant_value**2 - self.max_condition_number = max_condition_number - self.logger = logging.getLogger(__name__) - self.logger.setLevel(level=logging.WARN) - - def result_analysis(self, result=None): - """Calculate FIM from Jacobian information. This is for grid search (combined models) results - - Parameters - ---------- - result: - solver status returned by IPOPT - """ - self.result = result - self.doe_result = None - - # get number of parameters - no_param = len(self.parameter_names) - - fim = np.zeros((no_param, no_param)) - - # convert dictionary to a numpy array - Q_all = [] - for par in self.parameter_names: - Q_all.append(self.jaco_information[par]) - n = len(self.parameter_names) - - Q_all = np.array(list(self.jaco_information[p] for p in self.parameter_names)).T - # add the FIM for each measurement variables together - for i, mea_name in enumerate(self.measurement_variables): - fim += ( - 1 - / self.measurements.variance[str(mea_name)] # variance of measurement - * ( - Q_all[i, :].reshape(n, 1) @ Q_all[i, :].reshape(n, 1).T - ) # Q.T @ Q for each measurement variable - ) - - # add prior information - if self.prior_FIM is not None: - try: - fim = fim + self.prior_FIM - self.logger.info("Existed information has been added.") - except: - raise ValueError("Check the shape of prior FIM.") - - if np.linalg.cond(fim) > self.max_condition_number: - self.logger.info( - "Warning: FIM is near singular. The condition number is: %s ;", - np.linalg.cond(fim), - ) - self.logger.info( - "A condition number bigger than %s is considered near singular.", - self.max_condition_number, - ) - - # call private methods - self._print_FIM_info(fim) - if self.result is not None: - self._get_solver_info() - - # if given store file name, store the FIM - if self.store_FIM is not None: - self._store_FIM() - - def subset(self, measurement_subset): - """Create new FisherResults object corresponding to provided measurement_subset. - This requires that measurement_subset is a true subset of the original measurement object. - - Parameters - ---------- - measurement_subset: Instance of Measurements class - - Returns - ------- - new_result: New instance of FisherResults - """ - - # Check that measurement_subset is a valid subset of self.measurement - self.measurements.check_subset(measurement_subset) - - # Split Jacobian (should already be 3D) - small_jac = self._split_jacobian(measurement_subset) - - # create a new subject - FIM_subset = FisherResults( - self.parameter_names, - measurement_subset, - jacobian_info=small_jac, - prior_FIM=self.prior_FIM, - store_FIM=self.store_FIM, - scale_constant_value=self.scale_constant_value, - max_condition_number=self.max_condition_number, - ) - - return FIM_subset - - def _split_jacobian(self, measurement_subset): - """ - Split jacobian - - Parameters - ---------- - measurement_subset: the object of the measurement subsets - - Returns - ------- - jaco_info: split Jacobian - """ - # create a dict for FIM. It has the same keys as the Jacobian dict. - jaco_info = {} - - # reorganize the jacobian subset with the same form of the jacobian - # loop over parameters - for par in self.parameter_names: - jaco_info[par] = [] - # loop over measurements - for name in measurement_subset.variable_names: - try: - n_all_measure = self.measurement_variables.index(name) - jaco_info[par].append(self.all_jacobian_info[par][n_all_measure]) - except: - raise ValueError( - "Measurement ", name, " is not in original measurement set." - ) - - return jaco_info - - def _print_FIM_info(self, FIM): - """ - using a dictionary to store all FIM information - - Parameters - ---------- - FIM: the Fisher Information Matrix, needs to be P.D. and symmetric - - Returns - ------- - fim_info: a FIM dictionary containing the following key:value pairs - ~['FIM']: a list of FIM itself - ~[design variable name]: a list of design variable values at each time point - ~['Trace']: a scalar number of Trace - ~['Determinant']: a scalar number of determinant - ~['Condition number:']: a scalar number of condition number - ~['Minimal eigen value:']: a scalar number of minimal eigen value - ~['Eigen values:']: a list of all eigen values - ~['Eigen vectors:']: a list of all eigen vectors - """ - eig = np.linalg.eigvals(FIM) - self.FIM = FIM - self.trace = np.trace(FIM) - self.det = np.linalg.det(FIM) - self.min_eig = min(eig) - self.cond = max(eig) / min(eig) - self.eig_vals = eig - self.eig_vecs = np.linalg.eig(FIM)[1] - - self.logger.info( - "FIM: %s; \n Trace: %s; \n Determinant: %s;", self.FIM, self.trace, self.det - ) - self.logger.info( - "Condition number: %s; \n Min eigenvalue: %s.", self.cond, self.min_eig - ) - - def _solution_info(self, m, dv_set): - """ - Solution information. Only for optimization problem - - Parameters - ---------- - m: model - dv_set: design variable dictionary - - Returns - ------- - model_info: model solutions dictionary containing the following key:value pairs - -['obj']: a scalar number of objective function value - -['det']: a scalar number of determinant calculated by the model (different from FIM_info['det'] which - is calculated by numpy) - -['trace']: a scalar number of trace calculated by the model - -[design variable name]: a list of design variable solution - """ - self.obj_value = value(m.obj) - - # When scaled with constant values, the effect of the scaling factors are removed here - # For determinant, the scaling factor to determinant is scaling factor ** (Dim of FIM) - # For trace, the scaling factor to trace is the scaling factor. - if self.obj == "det": - self.obj_det = np.exp(value(m.obj)) / (self.fim_scale_constant_value) ** ( - len(self.parameter_names) - ) - elif self.obj == "trace": - self.obj_trace = np.exp(value(m.obj)) / (self.fim_scale_constant_value) - - design_variable_names = list(dv_set.keys()) - dv_times = list(dv_set.values()) - - solution = {} - for d, dname in enumerate(design_variable_names): - sol = [] - if dv_times[d] is not None: - for t, time in enumerate(dv_times[d]): - newvar = getattr(m, dname)[time] - sol.append(value(newvar)) - else: - newvar = getattr(m, dname) - sol.append(value(newvar)) - - solution[dname] = sol - self.solution = solution - - def _store_FIM(self): - # if given store file name, store the FIM - store_dict = {} - for i, name in enumerate(self.parameter_names): - store_dict[name] = self.FIM[i] - FIM_store = pd.DataFrame(store_dict) - FIM_store.to_csv(self.store_FIM, index=False) - - def _get_solver_info(self): - """ - Solver information dictionary - - Return: - ------ - solver_status: a solver information dictionary containing the following key:value pairs - -['square']: a string of square result solver status - -['doe']: a string of doe result solver status - """ - - if (self.result.solver.status == SolverStatus.ok) and ( - self.result.solver.termination_condition == TerminationCondition.optimal - ): - self.status = "converged" - elif ( - self.result.solver.termination_condition == TerminationCondition.infeasible - ): - self.status = "infeasible" - else: - self.status = self.result.solver.status - - -class GridSearchResult: - def __init__( - self, - design_ranges, - design_dimension_names, - FIM_result_list, - store_optimality_name=None, - ): - """ - This class deals with the FIM results from grid search, providing A, D, E, ME-criteria results for each design variable. - Can choose to draw 1D sensitivity curves and 2D heatmaps. - - Parameters - ---------- - design_ranges: - a ``dict`` whose keys are design variable names, values are a list of design variable values to go over - design_dimension_names: - a ``list`` of design variables names - FIM_result_list: - a ``dict`` containing FIM results, keys are a tuple of design variable values, values are FIM result objects - store_optimality_name: - a .csv file name containing all four optimalities value - """ - # design variables - self.design_names = design_dimension_names - self.design_ranges = design_ranges - self.FIM_result_list = FIM_result_list - - self.store_optimality_name = store_optimality_name - - def extract_criteria(self): - """ - Extract design criteria values for every 'grid' (design variable combination) searched. - - Returns - ------- - self.store_all_results_dataframe: a pandas dataframe with columns as design variable names and A, D, E, ME-criteria names. - Each row contains the design variable value for this 'grid', and the 4 design criteria value for this 'grid'. - """ - - # a list store all results - store_all_results = [] - - # generate combinations of design variable values to go over - search_design_set = product(*self.design_ranges) - - # loop over deign value combinations - for design_set_iter in search_design_set: - # locate this grid in the dictionary of combined results - result_object_asdict = { - k: v for k, v in self.FIM_result_list.items() if k == design_set_iter - } - # an result object is identified by a tuple of the design variable value it uses - result_object_iter = result_object_asdict[design_set_iter] - - # store results as a row in the dataframe - store_iteration_result = list(design_set_iter) - store_iteration_result.append(result_object_iter.trace) - store_iteration_result.append(result_object_iter.det) - store_iteration_result.append(result_object_iter.min_eig) - store_iteration_result.append(result_object_iter.cond) - - # add this row to the dataframe - store_all_results.append(store_iteration_result) - - # generate column names for the dataframe - column_names = [] - # this count is for repeated design variable names which can happen in dynamic problems - for i in self.design_names: - # if design variables share the same value, use the first name as the column name - if type(i) is list: - column_names.append(i[0]) - else: - column_names.append(i) - - # Each design criteria has a column to store values - column_names.append("A") - column_names.append("D") - column_names.append("E") - column_names.append("ME") - # generate the dataframe - store_all_results = np.asarray(store_all_results) - self.store_all_results_dataframe = pd.DataFrame( - store_all_results, columns=column_names - ) - # if needs to store the values - if self.store_optimality_name is not None: - self.store_all_results_dataframe.to_csv( - self.store_optimality_name, index=False - ) - - def figure_drawing( - self, - fixed_design_dimensions, - sensitivity_dimension, - title_text, - xlabel_text, - ylabel_text, - font_axes=16, - font_tick=14, - log_scale=True, - ): - """ - Extract results needed for drawing figures from the overall result dataframe. - Draw 1D sensitivity curve or 2D heatmap. - It can be applied to results of any dimensions, but requires design variable values in other dimensions be fixed. - - Parameters - ---------- - fixed_design_dimensions: a dictionary, keys are the design variable names to be fixed, values are the value of it to be fixed. - sensitivity_dimension: a list of design variable names to draw figures. - If only one name is given, a 1D sensitivity curve is drawn - if two names are given, a 2D heatmap is drawn. - title_text: name of the figure, a string - xlabel_text: x label title, a string. - In a 1D sensitivity curve, it is the design variable by which the curve is drawn. - In a 2D heatmap, it should be the second design variable in the design_ranges - ylabel_text: y label title, a string. - A 1D sensitivity curve does not need it. In a 2D heatmap, it should be the first design variable in the dv_ranges - font_axes: axes label font size - font_tick: tick label font size - log_scale: if True, the result matrix will be scaled by log10 - - Returns - -------- - None - """ - self.fixed_design_names = list(fixed_design_dimensions.keys()) - self.fixed_design_values = list(fixed_design_dimensions.values()) - self.sensitivity_dimension = sensitivity_dimension - - if len(self.fixed_design_names) + len(self.sensitivity_dimension) != len( - self.design_names - ): - raise ValueError( - "Error: All dimensions except for those the figures are drawn by should be fixed." - ) - - if len(self.sensitivity_dimension) not in [1, 2]: - raise ValueError("Error: Either 1D or 2D figures can be drawn.") - - # generate a combination of logic sentences to filter the results of the DOF needed. - # an example filter: (self.store_all_results_dataframe["CA0"]==5). - if len(self.fixed_design_names) != 0: - filter = "" - for i in range(len(self.fixed_design_names)): - filter += "(self.store_all_results_dataframe[" - filter += str(self.fixed_design_names[i]) - filter += "]==" - filter += str(self.fixed_design_values[i]) - filter += ")" - if i != (len(self.fixed_design_names) - 1): - filter += "&" - # extract results with other dimensions fixed - figure_result_data = self.store_all_results_dataframe.loc[eval(filter)] - # if there is no other fixed dimensions - else: - figure_result_data = self.store_all_results_dataframe - - # add results for figures - self.figure_result_data = figure_result_data - - # if one design variable name is given as DOF, draw 1D sensitivity curve - if len(sensitivity_dimension) == 1: - self._curve1D( - title_text, xlabel_text, font_axes=16, font_tick=14, log_scale=True - ) - # if two design variable names are given as DOF, draw 2D heatmaps - elif len(sensitivity_dimension) == 2: - self._heatmap( - title_text, - xlabel_text, - ylabel_text, - font_axes=16, - font_tick=14, - log_scale=True, - ) - - def _curve1D( - self, title_text, xlabel_text, font_axes=16, font_tick=14, log_scale=True - ): - """ - Draw 1D sensitivity curves for all design criteria - - Parameters - ---------- - title_text: name of the figure, a string - xlabel_text: x label title, a string. - In a 1D sensitivity curve, it is the design variable by which the curve is drawn. - font_axes: axes label font size - font_tick: tick label font size - log_scale: if True, the result matrix will be scaled by log10 - - Returns - -------- - 4 Figures of 1D sensitivity curves for each criteria - """ - - # extract the range of the DOF design variable - x_range = self.figure_result_data[self.sensitivity_dimension[0]].values.tolist() - - # decide if the results are log scaled - if log_scale: - y_range_A = np.log10(self.figure_result_data["A"].values.tolist()) - y_range_D = np.log10(self.figure_result_data["D"].values.tolist()) - y_range_E = np.log10(self.figure_result_data["E"].values.tolist()) - y_range_ME = np.log10(self.figure_result_data["ME"].values.tolist()) - else: - y_range_A = self.figure_result_data["A"].values.tolist() - y_range_D = self.figure_result_data["D"].values.tolist() - y_range_E = self.figure_result_data["E"].values.tolist() - y_range_ME = self.figure_result_data["ME"].values.tolist() - - # Draw A-optimality - fig = plt.pyplot.figure() - plt.pyplot.rc("axes", titlesize=font_axes) - plt.pyplot.rc("axes", labelsize=font_axes) - plt.pyplot.rc("xtick", labelsize=font_tick) - plt.pyplot.rc("ytick", labelsize=font_tick) - ax = fig.add_subplot(111) - params = {"mathtext.default": "regular"} - # plt.rcParams.update(params) - ax.plot(x_range, y_range_A) - ax.scatter(x_range, y_range_A) - ax.set_ylabel("$log_{10}$ Trace") - ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ": A-optimality") - plt.pyplot.show() - - # Draw D-optimality - fig = plt.pyplot.figure() - plt.pyplot.rc("axes", titlesize=font_axes) - plt.pyplot.rc("axes", labelsize=font_axes) - plt.pyplot.rc("xtick", labelsize=font_tick) - plt.pyplot.rc("ytick", labelsize=font_tick) - ax = fig.add_subplot(111) - params = {"mathtext.default": "regular"} - # plt.rcParams.update(params) - ax.plot(x_range, y_range_D) - ax.scatter(x_range, y_range_D) - ax.set_ylabel("$log_{10}$ Determinant") - ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ": D-optimality") - plt.pyplot.show() - - # Draw E-optimality - fig = plt.pyplot.figure() - plt.pyplot.rc("axes", titlesize=font_axes) - plt.pyplot.rc("axes", labelsize=font_axes) - plt.pyplot.rc("xtick", labelsize=font_tick) - plt.pyplot.rc("ytick", labelsize=font_tick) - ax = fig.add_subplot(111) - params = {"mathtext.default": "regular"} - # plt.rcParams.update(params) - ax.plot(x_range, y_range_E) - ax.scatter(x_range, y_range_E) - ax.set_ylabel("$log_{10}$ Minimal eigenvalue") - ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ": E-optimality") - plt.pyplot.show() - - # Draw Modified E-optimality - fig = plt.pyplot.figure() - plt.pyplot.rc("axes", titlesize=font_axes) - plt.pyplot.rc("axes", labelsize=font_axes) - plt.pyplot.rc("xtick", labelsize=font_tick) - plt.pyplot.rc("ytick", labelsize=font_tick) - ax = fig.add_subplot(111) - params = {"mathtext.default": "regular"} - # plt.rcParams.update(params) - ax.plot(x_range, y_range_ME) - ax.scatter(x_range, y_range_ME) - ax.set_ylabel("$log_{10}$ Condition number") - ax.set_xlabel(xlabel_text) - plt.pyplot.title(title_text + ": Modified E-optimality") - plt.pyplot.show() - - def _heatmap( - self, - title_text, - xlabel_text, - ylabel_text, - font_axes=16, - font_tick=14, - log_scale=True, - ): - """ - Draw 2D heatmaps for all design criteria - - Parameters - ---------- - title_text: name of the figure, a string - xlabel_text: x label title, a string. - In a 2D heatmap, it should be the second design variable in the design_ranges - ylabel_text: y label title, a string. - In a 2D heatmap, it should be the first design variable in the dv_ranges - font_axes: axes label font size - font_tick: tick label font size - log_scale: if True, the result matrix will be scaled by log10 - - Returns - -------- - 4 Figures of 2D heatmap for each criteria - """ - - # achieve the design variable ranges this figure needs - # create a dictionary for sensitivity dimensions - sensitivity_dict = {} - for i, name in enumerate(self.design_names): - if name in self.sensitivity_dimension: - sensitivity_dict[name] = self.design_ranges[i] - elif name[0] in self.sensitivity_dimension: - sensitivity_dict[name[0]] = self.design_ranges[i] - - x_range = sensitivity_dict[self.sensitivity_dimension[0]] - y_range = sensitivity_dict[self.sensitivity_dimension[1]] - - # extract the design criteria values - A_range = self.figure_result_data["A"].values.tolist() - D_range = self.figure_result_data["D"].values.tolist() - E_range = self.figure_result_data["E"].values.tolist() - ME_range = self.figure_result_data["ME"].values.tolist() - - # reshape the design criteria values for heatmaps - cri_a = np.asarray(A_range).reshape(len(x_range), len(y_range)) - cri_d = np.asarray(D_range).reshape(len(x_range), len(y_range)) - cri_e = np.asarray(E_range).reshape(len(x_range), len(y_range)) - cri_e_cond = np.asarray(ME_range).reshape(len(x_range), len(y_range)) - - self.cri_a = cri_a - self.cri_d = cri_d - self.cri_e = cri_e - self.cri_e_cond = cri_e_cond - - # decide if log scaled - if log_scale: - hes_a = np.log10(self.cri_a) - hes_e = np.log10(self.cri_e) - hes_d = np.log10(self.cri_d) - hes_e2 = np.log10(self.cri_e_cond) - else: - hes_a = self.cri_a - hes_e = self.cri_e - hes_d = self.cri_d - hes_e2 = self.cri_e_cond - - # set heatmap x,y ranges - xLabel = x_range - yLabel = y_range - - # A-optimality - fig = plt.pyplot.figure() - plt.pyplot.rc("axes", titlesize=font_axes) - plt.pyplot.rc("axes", labelsize=font_axes) - plt.pyplot.rc("xtick", labelsize=font_tick) - plt.pyplot.rc("ytick", labelsize=font_tick) - ax = fig.add_subplot(111) - params = {"mathtext.default": "regular"} - plt.pyplot.rcParams.update(params) - ax.set_yticks(range(len(yLabel))) - ax.set_yticklabels(yLabel) - ax.set_ylabel(ylabel_text) - ax.set_xticks(range(len(xLabel))) - ax.set_xticklabels(xLabel) - ax.set_xlabel(xlabel_text) - im = ax.imshow(hes_a.T, cmap=plt.pyplot.cm.hot_r) - ba = plt.pyplot.colorbar(im) - ba.set_label("log10(trace(FIM))") - plt.pyplot.title(title_text + ": A-optimality") - plt.pyplot.show() - - # D-optimality - fig = plt.pyplot.figure() - plt.pyplot.rc("axes", titlesize=font_axes) - plt.pyplot.rc("axes", labelsize=font_axes) - plt.pyplot.rc("xtick", labelsize=font_tick) - plt.pyplot.rc("ytick", labelsize=font_tick) - ax = fig.add_subplot(111) - params = {"mathtext.default": "regular"} - plt.pyplot.rcParams.update(params) - ax.set_yticks(range(len(yLabel))) - ax.set_yticklabels(yLabel) - ax.set_ylabel(ylabel_text) - ax.set_xticks(range(len(xLabel))) - ax.set_xticklabels(xLabel) - ax.set_xlabel(xlabel_text) - im = ax.imshow(hes_d.T, cmap=plt.pyplot.cm.hot_r) - ba = plt.pyplot.colorbar(im) - ba.set_label("log10(det(FIM))") - plt.pyplot.title(title_text + ": D-optimality") - plt.pyplot.show() - - # E-optimality - fig = plt.pyplot.figure() - plt.pyplot.rc("axes", titlesize=font_axes) - plt.pyplot.rc("axes", labelsize=font_axes) - plt.pyplot.rc("xtick", labelsize=font_tick) - plt.pyplot.rc("ytick", labelsize=font_tick) - ax = fig.add_subplot(111) - params = {"mathtext.default": "regular"} - plt.pyplot.rcParams.update(params) - ax.set_yticks(range(len(yLabel))) - ax.set_yticklabels(yLabel) - ax.set_ylabel(ylabel_text) - ax.set_xticks(range(len(xLabel))) - ax.set_xticklabels(xLabel) - ax.set_xlabel(xlabel_text) - im = ax.imshow(hes_e.T, cmap=plt.pyplot.cm.hot_r) - ba = plt.pyplot.colorbar(im) - ba.set_label("log10(minimal eig(FIM))") - plt.pyplot.title(title_text + ": E-optimality") - plt.pyplot.show() - - # modified E-optimality - fig = plt.pyplot.figure() - plt.pyplot.rc("axes", titlesize=font_axes) - plt.pyplot.rc("axes", labelsize=font_axes) - plt.pyplot.rc("xtick", labelsize=font_tick) - plt.pyplot.rc("ytick", labelsize=font_tick) - ax = fig.add_subplot(111) - params = {"mathtext.default": "regular"} - plt.pyplot.rcParams.update(params) - ax.set_yticks(range(len(yLabel))) - ax.set_yticklabels(yLabel) - ax.set_ylabel(ylabel_text) - ax.set_xticks(range(len(xLabel))) - ax.set_xticklabels(xLabel) - ax.set_xlabel(xlabel_text) - im = ax.imshow(hes_e2.T, cmap=plt.pyplot.cm.hot_r) - ba = plt.pyplot.colorbar(im) - ba.set_label("log10(cond(FIM))") - plt.pyplot.title(title_text + ": Modified E-optimality") - plt.pyplot.show() diff --git a/pyomo/contrib/doe/scenario.py b/pyomo/contrib/doe/scenario.py deleted file mode 100644 index 3faeb78dd10..00000000000 --- a/pyomo/contrib/doe/scenario.py +++ /dev/null @@ -1,181 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# -# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation -# Initiative (CCSI), and is copyright (c) 2022 by the software owners: -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, -# University of Toledo, West Virginia University, et al. All rights reserved. -# -# NOTICE. This Software was developed under funding from the -# U.S. Department of Energy and the U.S. Government consequently retains -# certain rights. As such, the U.S. Government has been granted for itself -# and others acting on its behalf a paid-up, nonexclusive, irrevocable, -# worldwide license in the Software to reproduce, distribute copies to the -# public, prepare derivative works, and perform publicly and display -# publicly, and to permit other to do so. -# ___________________________________________________________________________ - -import pickle -from enum import Enum -from collections import namedtuple -import copy - - -class FiniteDifferenceStep(Enum): - forward = "forward" - central = "central" - backward = "backward" - - -# namedtuple for scenario data -ScenarioData = namedtuple( - "ScenarioData", ["scenario", "scena_num", "eps_abs", "scenario_indices"] -) - - -class ScenarioGenerator: - def __init__(self, parameter_dict=None, formula="central", step=0.001, store=False): - """Generate scenarios. - DoE library first calls this function to generate scenarios. - - Parameters - ----------- - parameter_dict: - a ``dict`` of parameter, keys are names of ''string'', values are their nominal value of ''float''. - for e.g., {'A1': 84.79, 'A2': 371.72, 'E1': 7.78, 'E2': 15.05} - formula: - choose from 'central', 'forward', 'backward', None. - step: - Sensitivity perturbation step size, a fraction between [0,1]. default is 0.001 - store: - if True, store results. - """ - # get info from parameter dictionary - self.parameters = parameter_dict - self.parameter_dict = parameter_dict - self.para_names = list(parameter_dict.keys()) - self.no_para = len(self.para_names) - # self.formula = FiniteDifferenceStep(formula) - self.formula = formula - self.step = step - self.store = store - self.scenario_nominal = [parameter_dict[d] for d in self.para_names] - - # generate scenarios - self.generate_scenario() - - def generate_scenario(self): - """ - Generate scenario data for the given parameter dictionary. - - Returns: - ------- - ScenarioData: a namedtuple containing scenarios information. - ScenarioData.scenario: a list of dictionaries, each dictionary contains a perturbed scenario - ScenarioData.scena_num: a dict of scenario number related to one parameter - ScenarioData.eps_abs: keys are parameter name, values are the step it is perturbed - ScenarioData.scenario_indices: a list of scenario indices - - - For e.g., if a dict {'P':100, 'D':20} is given, step=0.1, formula='central', it will return: - self.ScenarioData.scenario: [{'P':101, 'D':20}, {'P':99, 'D':20}, {'P':100, 'D':20.2}, {'P':100, 'D':19.8}], - self.ScenarioData.scena_num: {'P':[0,1], 'D':[2,3]}} - self.ScenarioData.eps_abs: {'P': 2.0, 'D': 0.4} - self.ScenarioData.scenario_indices: [0,1,2,3] - if formula ='forward', it will return: - self.ScenarioData.scenario:[{'P':101, 'D':20}, {'P':100, 'D':20.2}, {'P':100, 'D':20}], - self.ScenarioData.scena_num: {'P':[0,2], 'D':[1,2]}} - self.ScenarioData.eps_abs: {'P': 2.0, 'D': 0.4} - self.ScenarioData.scenario_indices: [0,1,2] - """ - # dict for parameter perturbation step size - eps_abs = {} - # scenario dict for block - scenario = [] - # number of scenario - scena_num = {} - - # count_scens = 0 - # for k, v in self.parameters.items(): - # if self.formula == FiniteDifferenceStep.central: - # scena_num[k.name] = [2 * count_scens, 2 * count_scens + 1] - # scena_dict_hi = {k.name: v * (1 + self.step)} - # scena_dict_lo = {k.name: v * (1 + self.step)} - - # eps_abs[k.name] = 2 * self.step * v - - # scenario.append(scena_dict_hi) - # scenario.append(scena_dict_lo) - - # self.ScenarioData = ScenarioData( - # scenario, scena_num, eps_abs, list(range(len(scenario))) - # ) - - ############################## - # Below is deprecated code # - ############################## - - # loop over parameter name - for p, para in enumerate(self.para_names): - ## get scenario dictionary - if self.formula == FiniteDifferenceStep.central: - if isinstance(para, str): - name = para - else: - name = para.name - scena_num[name] = [2 * p, 2 * p + 1] - scena_dict_up, scena_dict_lo = ( - copy.deepcopy(self.parameter_dict), - copy.deepcopy(self.parameter_dict), - ) - # corresponding parameter dictionary for the scenario - scena_dict_up[name] *= 1 + self.step - scena_dict_lo[name] *= 1 - self.step - - scenario.append(scena_dict_up) - scenario.append(scena_dict_lo) - - elif self.formula in [ - FiniteDifferenceStep.forward, - FiniteDifferenceStep.backward, - ]: - # the base case is added as the last one - scena_num[para] = [p, len(self.param_names)] - scena_dict_up, scena_dict_lo = ( - self.parameter_dict.copy(), - self.parameter_dict.copy(), - ) - if self.formula == FiniteDifferenceStep.forward: - scena_dict_up[para] *= 1 + self.step - - elif self.formula == FiniteDifferenceStep.backward: - scena_dict_lo[para] *= 1 - self.step - - scenario.append(scena_dict_up) - scenario.append(scena_dict_lo) - - ## get perturbation sizes - # for central difference scheme, perturbation size is two times the step size - if self.formula == FiniteDifferenceStep.central: - eps_abs[para] = 2 * self.step * self.parameter_dict[para] - else: - eps_abs[para] = self.step * self.parameter_dict[para] - - self.ScenarioData = ScenarioData( - scenario, scena_num, eps_abs, list(range(len(scenario))) - ) - - # store scenario - if self.store: - with open("scenario_simultaneous.pickle", "wb") as f: - pickle.dump(self.scenario_data, f) From 936cc4cda7840d39190f3a4dfc7167c9576bc177 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 11 Jul 2024 14:31:39 -0400 Subject: [PATCH 1878/3044] Removing more files --- pyomo/contrib/doe/measurements.py | 357 ------------------ .../doe/redesign/simple_reaction_example | 188 --------- 2 files changed, 545 deletions(-) delete mode 100644 pyomo/contrib/doe/measurements.py delete mode 100644 pyomo/contrib/doe/redesign/simple_reaction_example diff --git a/pyomo/contrib/doe/measurements.py b/pyomo/contrib/doe/measurements.py deleted file mode 100644 index 31a9dc19dbb..00000000000 --- a/pyomo/contrib/doe/measurements.py +++ /dev/null @@ -1,357 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# -# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation -# Initiative (CCSI), and is copyright (c) 2022 by the software owners: -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, -# University of Toledo, West Virginia University, et al. All rights reserved. -# -# NOTICE. This Software was developed under funding from the -# U.S. Department of Energy and the U.S. Government consequently retains -# certain rights. As such, the U.S. Government has been granted for itself -# and others acting on its behalf a paid-up, nonexclusive, irrevocable, -# worldwide license in the Software to reproduce, distribute copies to the -# public, prepare derivative works, and perform publicly and display -# publicly, and to permit other to do so. -# ___________________________________________________________________________ - -import itertools -import collections.abc -from pyomo.common.numeric_types import native_numeric_types - - -class VariablesWithIndices: - def __init__(self): - """This class provides utility methods for DesignVariables and MeasurementVariables to create - lists of Pyomo variable names with an arbitrary number of indices. - """ - self.variable_names = [] - self.variable_names_value = {} - self.lower_bounds = {} - self.upper_bounds = {} - - def set_variable_name_list(self, variable_name_list): - """ - Specify variable names with its full name. - - Parameters - ---------- - variable_name_list: a ``list`` of ``string``, containing the variable names with indices, - for e.g. "C['CA', 23, 0]". - """ - self.variable_names.extend(variable_name_list) - - def add_variables( - self, - var_name, - indices=None, - time_index_position=None, - values=None, - lower_bounds=None, - upper_bounds=None, - ): - """ - Used for generating string names with indices. - - Parameters - ---------- - var_name: variable name in ``string`` - indices: a ``dict`` containing indices - if default (None), no extra indices needed for all var in var_name - for e.g., {0:["CA", "CB", "CC"], 1: [1,2,3]}. - time_index_position: an integer indicates which index is the time index - for e.g., 1 is the time index position in the indices example. - values: a ``list`` containing values which has the same shape of flattened variables - default choice is None, means there is no give nvalues - lower_bounds: a ``list `` of lower bounds. If given a scalar number, it is set as the lower bounds for all variables. - upper_bounds: a ``list`` of upper bounds. If given a scalar number, it is set as the upper bounds for all variables. - - Returns - ------- - if not defining values, return a set of variable names - if defining values, return a dictionary of variable names and its value - """ - added_names = self._generate_variable_names_with_indices( - var_name, indices=indices, time_index_position=time_index_position - ) - - self._check_valid_input( - len(added_names), - var_name, - indices, - time_index_position, - values, - lower_bounds, - upper_bounds, - ) - - if values is not None: - # if a scalar (int or float) is given, set it as the value for all variables - if type(values) in native_numeric_types: - values = [values] * len(added_names) - # this dictionary keys are special set, values are its value - self.variable_names_value.update(zip(added_names, values)) - - if lower_bounds is not None: - # if a scalar (int or float) is given, set it as the lower bound for all variables - if type(lower_bounds) in native_numeric_types: - lower_bounds = [lower_bounds] * len(added_names) - self.lower_bounds.update(zip(added_names, lower_bounds)) - - if upper_bounds is not None: - # if a scalar (int or float) is given, set it as the upper bound for all variables - if type(upper_bounds) in native_numeric_types: - upper_bounds = [upper_bounds] * len(added_names) - self.upper_bounds.update(zip(added_names, upper_bounds)) - - return added_names - - def _generate_variable_names_with_indices( - self, var_name, indices=None, time_index_position=None - ): - """ - Used for generating string names with indices. - - Parameters - ---------- - var_name: a ``list`` of var names - indices: a ``dict`` containing indices - if default (None), no extra indices needed for all var in var_name - for e.g., {0:["CA", "CB", "CC"], 1: [1,2,3]}. - time_index_position: an integer indicates which index is the time index - for e.g., 1 is the time index position in the indices example. - """ - # first combine all indices into a list - all_index_list = [] # contains all index lists - if indices is not None: - for index_pointer in indices: - all_index_list.append(indices[index_pointer]) - - # all index list for one variable, such as ["CA", 10, 1] - # exhaustively enumerate over the full product of indices. For e.g., - # {0:["CA", "CB", "CC"], 1: [1,2,3]} - # becomes ["CA", 1], ["CA", 2], ..., ["CC", 2], ["CC", 3] - all_variable_indices = list(itertools.product(*all_index_list)) - - # list store all names added this time - added_names = [] - # iterate over index combinations ["CA", 1], ["CA", 2], ..., ["CC", 2], ["CC", 3] - for index_instance in all_variable_indices: - var_name_index_string = var_name - # - # Suggestion from JS: "Can you re-use name_repr and index_repr from pyomo.core.base.component_namer here?" - # - for i, idx in enumerate(index_instance): - # if i is the first index, open the [] - if i == 0: - var_name_index_string += "[" - # use repr() is different from using str() - # with repr(), "CA" is "CA", with str(), "CA" is CA. The first is not valid in our interface. - var_name_index_string += str(idx) - - # if i is the last index, close the []. if not, add a "," for the next index. - if i == len(index_instance) - 1: - var_name_index_string += "]" - else: - var_name_index_string += "," - - self.variable_names.append(var_name_index_string) - added_names.append(var_name_index_string) - - return added_names - - def _check_valid_input( - self, - len_indices, - var_name, - indices, - time_index_position, - values, - lower_bounds, - upper_bounds, - ): - """ - Check if the measurement information provided are valid to use. - """ - if not isinstance(var_name, str): - raise TypeError("Variable name must be a string.") - - # debugging note: what is an integer versus a list versus a dictionary here? - # check if time_index_position is in indices - if ( - indices is not None # ensure not None - and time_index_position is not None # ensure not None - and time_index_position - not in indices.keys() # ensure time_index_position is in indices - ): - raise ValueError("time index cannot be found in indices.") - - # if given a list, check if values have the same length with flattened variable - if ( - values is not None # ensure not None - and not type(values) - in native_numeric_types # skip this test if scalar (int or float) - and len(values) != len_indices - ): - raise ValueError("Values is of different length with indices.") - - if ( - lower_bounds is not None # ensure not None - and not type(lower_bounds) - in native_numeric_types # skip this test if scalar (int or float) - and isinstance(lower_bounds, collections.abc.Sequence) # ensure list-like - and len(lower_bounds) != len_indices # ensure same length - ): - raise ValueError("Lowerbounds have a different length with indices.") - - if ( - upper_bounds is not None # ensure not None - and not type(upper_bounds) - in native_numeric_types # skip this test if scalar (int or float) - and isinstance(upper_bounds, collections.abc.Sequence) # ensure list-like - and len(upper_bounds) != len_indices # ensure same length - ): - raise ValueError("Upperbounds have a different length with indices.") - - -class MeasurementVariables(VariablesWithIndices): - def __init__(self): - """ - This class stores information on which algebraic and differential variables in the Pyomo model are considered measurements. - """ - super().__init__() - self.variance = {} - - def set_variable_name_list(self, variable_name_list, variance=1): - """ - Specify variable names if given strings containing names and indices. - - Parameters - ---------- - variable_name_list: a ``list`` of ``string``, containing the variable names with indices, - for e.g. "C['CA', 23, 0]". - variance: a ``list`` of scalar numbers , which is the variance for this measurement. - """ - super().set_variable_name_list(variable_name_list) - - # add variance - if variance is not list: - variance = [variance] * len(variable_name_list) - - self.variance.update(zip(variable_name_list, variance)) - - def add_variables( - self, var_name, indices=None, time_index_position=None, variance=1 - ): - """ - Parameters - ----------- - var_name: a ``list`` of var names - indices: a ``dict`` containing indices - if default (None), no extra indices needed for all var in var_name - for e.g., {0:["CA", "CB", "CC"], 1: [1,2,3]}. - time_index_position: an integer indicates which index is the time index - for e.g., 1 is the time index position in the indices example. - variance: a scalar number, which is the variance for this measurement. - """ - added_names = super().add_variables( - var_name=var_name, indices=indices, time_index_position=time_index_position - ) - - # store variance - # if variance is a scalar number, repeat it for all added names - if variance is not list: - variance = [variance] * len(added_names) - self.variance.update(zip(added_names, variance)) - - def check_subset(self, subset_object): - """ - Check if subset_object is a subset of the current measurement object - - Parameters - ---------- - subset_object: a measurement object - """ - for name in subset_object.variable_names: - if name not in self.variable_names: - raise ValueError("Measurement not in the set: ", name) - - return True - - -class DesignVariables(VariablesWithIndices): - """ - Define design variables - """ - - def __init__(self): - super().__init__() - - def set_variable_name_list(self, variable_name_list): - """ - Specify variable names with its full name. - - Parameters - ---------- - variable_name_list: a ``list`` of ``string``, containing the variable names with indices, - for e.g. "C['CA', 23, 0]". - """ - super().set_variable_name_list(variable_name_list) - - def add_variables( - self, - var_name, - indices=None, - time_index_position=None, - values=None, - lower_bounds=None, - upper_bounds=None, - ): - """ - - Parameters - ---------- - var_name: a ``list`` of var names - indices: a ``dict`` containing indices - if default (None), no extra indices needed for all var in var_name - for e.g., {0:["CA", "CB", "CC"], 1: [1,2,3]}. - time_index_position: an integer indicates which index is the time index - for e.g., 1 is the time index position in the indices example. - values: a ``list`` containing values which has the same shape of flattened variables - default choice is None, means there is no give nvalues - lower_bounds: a ``list`` of lower bounds. If given a scalar number, it is set as the lower bounds for all variables. - upper_bounds: a ``list`` of upper bounds. If given a scalar number, it is set as the upper bounds for all variables. - """ - super().add_variables( - var_name=var_name, - indices=indices, - time_index_position=time_index_position, - values=values, - lower_bounds=lower_bounds, - upper_bounds=upper_bounds, - ) - - def update_values(self, new_value_dict): - """ - Update values of variables. Used for defining values for design variables of different experiments. - - Parameters - ---------- - new_value_dict: a ``dict`` containing the new values for the variables. - for e.g., {"C['CA', 23, 0]": 0.5, "C['CA', 24, 0]": 0.6} - """ - for key in new_value_dict: - if key not in self.variable_names: - raise ValueError("Variable not in the set: ", key) - - self.variable_names_value[key] = new_value_dict[key] diff --git a/pyomo/contrib/doe/redesign/simple_reaction_example b/pyomo/contrib/doe/redesign/simple_reaction_example deleted file mode 100644 index 416cbff1c13..00000000000 --- a/pyomo/contrib/doe/redesign/simple_reaction_example +++ /dev/null @@ -1,188 +0,0 @@ -def expand_model_components(m, base_components, index_sets): - """ - Takes model components and index sets and returns the - model component labels. - - Arguments - --------- - m: Pyomo model - base_components: list of variables from model 'm' - index_sets: list, same length as base_components, where each - element is a list of index sets, or None - """ - for val, indexes in itertools.zip_longest(base_components, index_sets): - # If the variable has no index, - # add just the model component - if not val.is_indexed(): - yield val - # If the component is indexed but no - # index supplied, add all indices - elif indexes is None: - yield from val.values() - else: - for j in itertools.product(*indexes): - yield val[j] - -class Experiment(object): - def __init__(self): - self.model = None - - def get_labeled_model(self): - raise NotImplementedError( - "Derived experiment class failed to implement get_labeled_model" - ) - - -class SimpleReactorExperiment(object): - def __init__(self, data, nfe, ncp): - self.data = data - self.nfe = nfe - self.ncp = ncp - self.model = None - - def get_labeled_model(self): - if self.model is None: - self.create_model() - self.finalize_model() - self.label_experiment_impl() - return self.model - - def create_model(self): - """ - This is an example user model provided to DoE library. - It is a dynamic problem solved by Pyomo.DAE. - - Return - ------ - m: a Pyomo.DAE model - """ - - m = self.model = pyo.ConcreteModel() - - # Model parameters - m.R = pyo.Param(mutable=False, initialize=8.314) - - # Define model variables - ######################## - # time - m.t = ContinuousSet(bounds=[0, 1]) - - # Concentrations - m.CA = pyo.Var(m.t, within=pyo.NonNegativeReals) - m.CB = pyo.Var(m.t, within=pyo.NonNegativeReals) - - # Arrhenius rate law equations - m.A1 = pyo.Var(within=pyo.NonNegativeReals) - - # Differential variables (Conc.) - m.dCAdt = DerivativeVar(m.CA, wrt=m.t) - - ######################## - # End variable def. - - # Equation def'n - ######################## - - # Expression for rate constants - @m.Expression(m.t) - def k1(m, t): - return m.A1 - - # Concentration odes - @m.Constraint(m.t) - def CA_rxn_ode(m, t): - return m.dCAdt[t] == -m.k1[t] * m.CA[t] - - # algebraic balance for concentration of B - # Valid because the reaction system (A --> B) is equimolar - @m.Constraint(m.t) - def CB_balance(m, t): - return m.CA[0] == m.CA[t] + m.CB[t] - - ######################## - # End equation def'n - - def finalize_model(self): - """ - Example finalize model function. There are two main tasks - here: - 1. Extracting useful information for the model to align - with the experiment. (Here: CA0, t_final, t_control) - 2. Discretizing the model subject to this information. - - Arguments - --------- - m: Pyomo model - data: object containing vital experimental information - nfe: number of finite elements - ncp: number of collocation points for the finite elements - """ - m = self.model - - # Unpacking data before simulation - control_points = self.data['control_points'] - - m.CA[0].fix(self.data['CA0']) - m.CB[0].fix(self.data['CB0']) - # m.A1 = self.data['A1'] - m.A1.fix(self.data['A1']) - - # TODO: add simulation for initialization????? - # Call the simulator (optional) - # sim = Simulator(m, package='casadi') - # tsim, profiles = sim.simulate(numpoints=100, integrator='idas') - - # Discretizing the model - discr = pyo.TransformationFactory("dae.collocation") - discr.apply_to(m, nfe=self.nfe, ncp=self.ncp, wrt=m.t) - - # sim.initialize_model() - - - def label_experiment_impl(self, index_sets_meas): - """ - Example for annotating (labeling) the model with a - full experiment. - - Arguments - --------- - - """ - m = self.model - - # Grab measurement labels - base_comp_meas = [m.CA, m.CB, ] - m.experiment_outputs = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) - m.experiment_outputs.update((k, None) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) - - # Adding no error for measurements currently - m.measurement_error = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) - m.measurement_error.update((k, 1e-2) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) - - # Grab design variables - base_comp_des = [m.CA, ] - index_sets_des = [[[m.t.first()]], ] - m.experiment_inputs = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) - m.experiment_inputs.update((k, pyo.ComponentUID(k)) for k in expand_model_components(m, base_comp_des, index_sets_des)) - - m.unknown_parameters = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) - m.unknown_parameters.update((k, pyo.value(k)) for k in [m.A1,]) - -f = open('result.json') -data_ex = json.load(f) -data_ex['control_points'] = {float(k): v for k, v in data_ex['control_points'].items()} - -experiments_simple = [ - SimpleReactorExperiment(data_ex, 32, 3), -] - -# in parmest / DoE: -expanded_experiments_simple = [e.get_labeled_model() for e in experiments_simple] \ No newline at end of file From 8bcb1963194209506d6d213e16a3a1ed18058a4f Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 14:45:12 -0400 Subject: [PATCH 1879/3044] Remove unused preprocessing functions --- pyomo/contrib/pyros/tests/test_grcs.py | 686 +------------------------ pyomo/contrib/pyros/util.py | 543 +++---------------- 2 files changed, 59 insertions(+), 1170 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 3a68e93429a..4414e76964a 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -23,16 +23,13 @@ from pyomo.common.collections import Bunch, ComponentSet, ComponentMap from pyomo.common.config import ConfigBlock, ConfigValue from pyomo.core.base.set_types import NonNegativeIntegers -from pyomo.core.base.var import VarData from pyomo.core.expr import ( identify_mutable_parameters, identify_variables, - MonomialTermExpression, - SumExpression, ) from pyomo.repn.plugins import nl_writer as pyomo_nl_writer from pyomo.common.dependencies import numpy as np, numpy_available -from pyomo.common.dependencies import scipy as sp, scipy_available +from pyomo.common.dependencies import scipy as scipy_available from pyomo.common.errors import ApplicationError, InfeasibleConstraintException from pyomo.environ import maximize as pyo_max from pyomo.opt import ( @@ -46,7 +43,6 @@ Reals, Set, Block, - ConstraintList, ConcreteModel, Constraint, Expression, @@ -70,28 +66,20 @@ ) from pyomo.contrib.pyros.solve_data import MasterProblemData, ROSolveResults from pyomo.contrib.pyros.uncertainty_sets import ( - UncertaintySet, BoxSet, AxisAlignedEllipsoidalSet, FactorModelSet, IntersectionSet, DiscreteScenarioSet, - Geometry, ) from pyomo.contrib.pyros.util import ( - add_decision_rule_variables, - add_decision_rule_constraints, - get_vars_from_component, identify_objective_functions, IterationLogRecord, ObjectiveType, pyrosTerminationCondition, - replace_uncertain_bounds_with_constraints, selective_clone, time_code, TimingData, - turn_bounds_to_constraints, - transform_to_standard_form, ) logger = logging.getLogger(__name__) @@ -269,678 +257,6 @@ def test_cloning_positive_case(self): ) -class testAddDecisionRuleVars(unittest.TestCase): - """ - Test method for adding decision rule variables to working model. - The number of decision rule variables per control variable - should depend on: - - - the number of uncertain parameters in the model - - the decision rule order specified by the user. - """ - - def make_simple_test_model(self): - """ - Make simple test model for DR variable - declaration testing. - """ - m = ConcreteModel() - - # uncertain parameters - m.p = Param(range(3), initialize=0, mutable=True) - - # second-stage variables - m.z = Var([0, 1], initialize=0) - - # util block - m.util = Block() - m.util.first_stage_variables = [] - m.util.second_stage_variables = list(m.z.values()) - m.util.uncertain_params = list(m.p.values()) - - return m - - @unittest.skipIf(not scipy_available, 'Scipy is not available.') - def test_correct_num_dr_vars_static(self): - """ - Test DR variable setup routines declare the correct - number of DR coefficient variables, static DR case. - """ - model_data = ROSolveResults() - model_data.working_model = m = self.make_simple_test_model() - - config = Bunch() - config.decision_rule_order = 0 - - add_decision_rule_variables(model_data=model_data, config=config) - - for indexed_dr_var in m.util.decision_rule_vars: - self.assertEqual( - len(indexed_dr_var), - 1, - msg=( - "Number of decision rule coefficient variables " - f"in indexed Var object {indexed_dr_var.name!r}" - "does not match correct value." - ), - ) - - self.assertEqual( - len(ComponentSet(m.util.decision_rule_vars)), - len(m.util.second_stage_variables), - msg=( - "Number of unique indexed DR variable components should equal " - "number of second-stage variables." - ), - ) - - @unittest.skipIf(not scipy_available, 'Scipy is not available.') - def test_correct_num_dr_vars_affine(self): - """ - Test DR variable setup routines declare the correct - number of DR coefficient variables, affine DR case. - """ - model_data = ROSolveResults() - model_data.working_model = m = self.make_simple_test_model() - - config = Bunch() - config.decision_rule_order = 1 - - add_decision_rule_variables(model_data=model_data, config=config) - - for indexed_dr_var in m.util.decision_rule_vars: - self.assertEqual( - len(indexed_dr_var), - 1 + len(m.util.uncertain_params), - msg=( - "Number of decision rule coefficient variables " - f"in indexed Var object {indexed_dr_var.name!r}" - "does not match correct value." - ), - ) - - self.assertEqual( - len(ComponentSet(m.util.decision_rule_vars)), - len(m.util.second_stage_variables), - msg=( - "Number of unique indexed DR variable components should equal " - "number of second-stage variables." - ), - ) - - @unittest.skipIf(not scipy_available, 'Scipy is not available.') - def test_correct_num_dr_vars_quadratic(self): - """ - Test DR variable setup routines declare the correct - number of DR coefficient variables, quadratic DR case. - """ - model_data = ROSolveResults() - model_data.working_model = m = self.make_simple_test_model() - - config = Bunch() - config.decision_rule_order = 2 - - add_decision_rule_variables(model_data=model_data, config=config) - - num_params = len(m.util.uncertain_params) - correct_num_dr_vars = ( - 1 # static term - + num_params # affine terms - + sp.special.comb(num_params, 2, repetition=True, exact=True) - # quadratic terms - ) - for indexed_dr_var in m.util.decision_rule_vars: - self.assertEqual( - len(indexed_dr_var), - correct_num_dr_vars, - msg=( - "Number of decision rule coefficient variables " - f"in indexed Var object {indexed_dr_var.name!r}" - "does not match correct value." - ), - ) - - self.assertEqual( - len(ComponentSet(m.util.decision_rule_vars)), - len(m.util.second_stage_variables), - msg=( - "Number of unique indexed DR variable components should equal " - "number of second-stage variables." - ), - ) - - -class testAddDecisionRuleConstraints(unittest.TestCase): - """ - Test method for adding decision rule equality constraints - to the working model. There should be as many decision - rule equality constraints as there are second-stage - variables, and each constraint should relate a second-stage - variable to the uncertain parameters and corresponding - decision rule variables. - """ - - def make_simple_test_model(self): - """ - Make simple model for DR constraint testing. - """ - m = ConcreteModel() - - # uncertain parameters - m.p = Param(range(3), initialize=0, mutable=True) - - # second-stage variables - m.z = Var([0, 1], initialize=0) - - # util block - m.util = Block() - m.util.first_stage_variables = [] - m.util.second_stage_variables = list(m.z.values()) - m.util.uncertain_params = list(m.p.values()) - - return m - - @unittest.skipIf(not scipy_available, 'Scipy is not available.') - def test_num_dr_eqns_added_correct(self): - """ - Check that number of DR equality constraints added - by constraint declaration routines matches the number - of second-stage variables in the model. - """ - model_data = ROSolveResults() - model_data.working_model = m = self.make_simple_test_model() - - # === Decision rule vars have been added - m.decision_rule_var_0 = Var([0], initialize=0) - m.decision_rule_var_1 = Var([0], initialize=0) - m.util.decision_rule_vars = [m.decision_rule_var_0, m.decision_rule_var_1] - - # set up simple config-like object - config = Bunch() - config.decision_rule_order = 0 - - add_decision_rule_constraints(model_data=model_data, config=config) - - self.assertEqual( - len(m.util.decision_rule_eqns), - len(m.util.second_stage_variables), - msg="The number of decision rule constraints added to model should equal" - "the number of control variables in the model.", - ) - - @unittest.skipIf(not scipy_available, 'Scipy is not available.') - def test_dr_eqns_form_correct(self): - """ - Check that form of decision rule equality constraints - is as expected. - - Decision rule equations should be of the standard form: - (sum of DR monomial terms) - (second-stage variable) == 0 - where each monomial term should be of form: - (product of uncertain parameters) * (decision rule variable) - - This test checks that the equality constraints are of this - standard form. - """ - # set up simple model data like object - model_data = ROSolveResults() - model_data.working_model = m = self.make_simple_test_model() - - # set up simple config-like object - config = Bunch() - config.decision_rule_order = 2 - - # add DR variables and constraints - add_decision_rule_variables(model_data, config) - add_decision_rule_constraints(model_data, config) - - # DR polynomial terms and order in which they should - # appear depends on number of uncertain parameters - # and order in which the parameters are listed. - # so uncertain parameters participating in each term - # of the monomial is known, and listed out here. - dr_monomial_param_combos = [ - (1,), - (m.p[0],), - (m.p[1],), - (m.p[2],), - (m.p[0], m.p[0]), - (m.p[0], m.p[1]), - (m.p[0], m.p[2]), - (m.p[1], m.p[1]), - (m.p[1], m.p[2]), - (m.p[2], m.p[2]), - ] - - dr_zip = zip( - m.util.second_stage_variables, - m.util.decision_rule_vars, - m.util.decision_rule_eqns, - ) - for ss_var, indexed_dr_var, dr_eq in dr_zip: - dr_eq_terms = dr_eq.body.args - - # check constraint body is sum expression - self.assertTrue( - isinstance(dr_eq.body, SumExpression), - msg=( - f"Body of DR constraint {dr_eq.name!r} is not of type " - f"{SumExpression.__name__}." - ), - ) - - # ensure DR equation has correct number of (additive) terms - self.assertEqual( - len(dr_eq_terms), - len(dr_monomial_param_combos) + 1, - msg=( - "Number of additive terms in the DR expression of " - f"DR constraint with name {dr_eq.name!r} does not match " - "expected value." - ), - ) - - # check last term is negative of second-stage variable - second_stage_var_term = dr_eq_terms[-1] - last_term_is_neg_ss_var = ( - isinstance(second_stage_var_term, MonomialTermExpression) - and (second_stage_var_term.args[0] == -1) - and (second_stage_var_term.args[1] is ss_var) - and len(second_stage_var_term.args) == 2 - ) - self.assertTrue( - last_term_is_neg_ss_var, - msg=( - "Last argument of last term in second-stage variable" - f"term of DR constraint with name {dr_eq.name!r} " - "is not the negative corresponding second-stage variable " - f"{ss_var.name!r}" - ), - ) - - # now we check the other terms. - # these should comprise the DR polynomial expression - dr_polynomial_terms = dr_eq_terms[:-1] - dr_polynomial_zip = zip( - dr_polynomial_terms, indexed_dr_var.values(), dr_monomial_param_combos - ) - for idx, (term, dr_var, param_combo) in enumerate(dr_polynomial_zip): - # term should be either a monomial expression or scalar variable - if isinstance(term, MonomialTermExpression): - # should be of form (uncertain parameter product) * - # (decision rule variable) so length of expression - # object should be 2 - self.assertEqual( - len(term.args), - 2, - msg=( - f"Length of `args` attribute of term {str(term)} " - f"of DR equation {dr_eq.name!r} is not as expected. " - f"Args: {term.args}" - ), - ) - - # check that uncertain parameters participating in - # the monomial are as expected - param_product_multiplicand = term.args[0] - dr_var_multiplicand = term.args[1] - else: - self.assertIsInstance(term, VarData) - param_product_multiplicand = 1 - dr_var_multiplicand = term - - if idx == 0: - # static DR term - param_combo_found_in_term = (param_product_multiplicand,) - param_names = (str(param) for param in param_combo) - elif len(param_combo) == 1: - # affine DR terms - param_combo_found_in_term = (param_product_multiplicand,) - param_names = (param.name for param in param_combo) - else: - # higher-order DR terms - param_combo_found_in_term = param_product_multiplicand.args - param_names = (param.name for param in param_combo) - - self.assertEqual( - param_combo_found_in_term, - param_combo, - msg=( - f"All but last multiplicand of DR monomial {str(term)} " - f"is not the uncertain parameter tuple " - f"({', '.join(param_names)})." - ), - ) - - # check that DR variable participating in the monomial - # is as expected - self.assertIs( - dr_var_multiplicand, - dr_var, - msg=( - f"Last multiplicand of DR monomial {str(term)} " - f"is not the DR variable {dr_var.name!r}." - ), - ) - - -class TestTurnVarBoundsToConstraints(unittest.TestCase): - """ - Tests for reformulating variable bounds to explicit - inequality/equality constraints. - """ - - def test_bounds_to_constraints(self): - m = ConcreteModel() - m.x = Var(initialize=1, bounds=(0, 1)) - m.y = Var(initialize=0, bounds=(None, 1)) - m.w = Var(initialize=0, bounds=(1, None)) - m.z = Var(initialize=0, bounds=(None, None)) - turn_bounds_to_constraints(m.z, m) - self.assertEqual( - len(list(m.component_data_objects(Constraint))), - 0, - msg="Inequality constraints were written for bounds on a variable with no bounds.", - ) - turn_bounds_to_constraints(m.y, m) - self.assertEqual( - len(list(m.component_data_objects(Constraint))), - 1, - msg="Inequality constraints were not " - "written correctly for a variable with an upper bound and no lower bound.", - ) - turn_bounds_to_constraints(m.w, m) - self.assertEqual( - len(list(m.component_data_objects(Constraint))), - 2, - msg="Inequality constraints were not " - "written correctly for a variable with a lower bound and no upper bound.", - ) - turn_bounds_to_constraints(m.x, m) - self.assertEqual( - len(list(m.component_data_objects(Constraint))), - 4, - msg="Inequality constraints were not " - "written correctly for a variable with both lower and upper bound.", - ) - - def test_uncertain_bounds_to_constraints(self): - # test model - m = ConcreteModel() - # parameters - m.p = Param(initialize=8, mutable=True) - m.r = Param(initialize=-5, mutable=True) - m.q = Param(initialize=1, mutable=False) - m.s = Param(initialize=1, mutable=True) - m.n = Param(initialize=1, mutable=True) - - # variables, with bounds contingent on params - m.u = Var(initialize=0, bounds=(0, m.p)) - m.v = Var(initialize=1, bounds=(m.r, m.p)) - m.w = Var(initialize=1, bounds=(None, None)) - m.x = Var(initialize=1, bounds=(0, exp(-1 * m.p / 8) * m.q * m.s)) - m.y = Var(initialize=-1, bounds=(m.r * m.p, 0)) - m.z = Var(initialize=1, bounds=(0, m.s)) - m.t = Var(initialize=1, bounds=(0, m.p**2)) - - # objective - m.obj = Objective(sense=maximize, expr=m.x**2 - m.y + m.t**2 + m.v) - - # clone model - mod = m.clone() - uncertain_params = [mod.n, mod.p, mod.r] - - # check variable replacement without any active objective - # or active performance constraints - mod.obj.deactivate() - replace_uncertain_bounds_with_constraints(mod, uncertain_params) - self.assertTrue( - hasattr(mod, 'uncertain_var_bound_cons'), - msg='Uncertain variable bounds erroneously added. ' - 'Check only variables participating in active ' - 'objective and constraints are added.', - ) - self.assertFalse(mod.uncertain_var_bound_cons) - mod.obj.activate() - - # add performance constraints - constraints_m = ConstraintList() - m.add_component('perf_constraints', constraints_m) - constraints_m.add(m.w == 2 * m.x + m.y) - constraints_m.add(m.v + m.x + m.y >= 0) - constraints_m.add(m.y**2 + m.z >= 0) - constraints_m.add(m.x**2 + m.u <= 1) - constraints_m[4].deactivate() - - # clone model with constraints added - mod_2 = m.clone() - - # manually replace uncertain parameter bounds with explicit constraints - uncertain_cons = ConstraintList() - m.add_component('uncertain_var_bound_cons', uncertain_cons) - uncertain_cons.add(m.x - m.x.upper <= 0) - uncertain_cons.add(m.y.lower - m.y <= 0) - uncertain_cons.add(m.v - m.v._ub <= 0) - uncertain_cons.add(m.v.lower - m.v <= 0) - uncertain_cons.add(m.t - m.t.upper <= 0) - - # remove corresponding variable bounds - m.x.setub(None) - m.y.setlb(None) - m.v.setlb(None) - m.v.setub(None) - m.t.setub(None) - - # check that vars participating in - # active objective and activated constraints correctly determined - svars_con = ComponentSet(get_vars_from_component(mod_2, Constraint)) - svars_obj = ComponentSet(get_vars_from_component(mod_2, Objective)) - vars_in_active_cons = ComponentSet( - [mod_2.z, mod_2.w, mod_2.y, mod_2.x, mod_2.v] - ) - vars_in_active_obj = ComponentSet([mod_2.x, mod_2.y, mod_2.t, mod_2.v]) - self.assertEqual( - svars_con, - vars_in_active_cons, - msg='Mismatch of variables participating in activated constraints.', - ) - self.assertEqual( - svars_obj, - vars_in_active_obj, - msg='Mismatch of variables participating in activated objectives.', - ) - - # replace bounds in model with performance constraints - uncertain_params = [mod_2.p, mod_2.r] - replace_uncertain_bounds_with_constraints(mod_2, uncertain_params) - - # check that same number of constraints added to model - self.assertEqual( - len(list(m.component_data_objects(Constraint))), - len(list(mod_2.component_data_objects(Constraint))), - msg='Mismatch between number of explicit variable ' - 'bound inequality constraints added ' - 'automatically and added manually.', - ) - - # check that explicit constraints contain correct vars and params - vars_in_cons = ComponentSet() - params_in_cons = ComponentSet() - - # get variables, mutable params in the explicit constraints - cons = mod_2.uncertain_var_bound_cons - for idx in cons: - for p in identify_mutable_parameters(cons[idx].expr): - params_in_cons.add(p) - for v in identify_variables(cons[idx].expr): - vars_in_cons.add(v) - # reduce only to uncertain mutable params found - params_in_cons = params_in_cons & uncertain_params - - # expected participating variables - vars_with_bounds_removed = ComponentSet([mod_2.x, mod_2.y, mod_2.v, mod_2.t]) - # complete the check - self.assertEqual( - params_in_cons, - ComponentSet([mod_2.p, mod_2.r]), - msg='Mismatch of parameters added to explicit inequality constraints.', - ) - self.assertEqual( - vars_in_cons, - vars_with_bounds_removed, - msg='Mismatch of variables added to explicit inequality constraints.', - ) - - -class testTransformToStandardForm(unittest.TestCase): - def test_transform_to_std_form(self): - """Check that `pyros.util.transform_to_standard_form` works - correctly for an example model. That is: - - all Constraints with a finite `upper` or `lower` attribute - are either equality constraints, or inequalities - of the standard form `expression(vars) <= upper`; - - every inequality Constraint for which the `upper` and `lower` - attribute are identical is converted to an equality constraint; - - every inequality Constraint with distinct finite `upper` and - `lower` attributes is split into two standard form inequality - Constraints. - """ - - m = ConcreteModel() - - m.p = Param(initialize=1, mutable=True) - - m.x = Var(initialize=0) - m.y = Var(initialize=1) - m.z = Var(initialize=1) - - # example constraints - m.c1 = Constraint(expr=m.x >= 1) - m.c2 = Constraint(expr=-m.y <= 0) - m.c3 = Constraint(rule=(None, m.x + m.y, None)) - m.c4 = Constraint(rule=(1, m.x + m.y, 2)) - m.c5 = Constraint(rule=(m.p, m.x, m.p)) - m.c6 = Constraint(rule=(1.0000, m.z, 1.0)) - - # example ConstraintList - clist = ConstraintList() - m.add_component('clist', clist) - clist.add(m.y <= 0) - clist.add(m.x >= 1) - clist.add((0, m.x, 1)) - - num_orig_cons = len( - [ - con - for con in m.component_data_objects( - Constraint, active=True, descend_into=True - ) - ] - ) - # constraints with finite, distinct lower & upper bounds - num_lbub_cons = len( - [ - con - for con in m.component_data_objects( - Constraint, active=True, descend_into=True - ) - if con.lower is not None - and con.upper is not None - and con.lower is not con.upper - ] - ) - - # count constraints with no bounds - num_nobound_cons = len( - [ - con - for con in m.component_data_objects( - Constraint, active=True, descend_into=True - ) - if con.lower is None and con.upper is None - ] - ) - - transform_to_standard_form(m) - cons = [ - con - for con in m.component_data_objects( - Constraint, active=True, descend_into=True - ) - ] - for con in cons: - has_lb_or_ub = not (con.lower is None and con.upper is None) - if has_lb_or_ub and not con.equality: - self.assertTrue( - con.lower is None, - msg="Constraint %s not in standard form" % con.name, - ) - lb_is_ub = con.lower is con.upper - self.assertFalse( - lb_is_ub, - msg="Constraint %s should be converted to equality" % con.name, - ) - if con is not m.c3: - self.assertTrue( - has_lb_or_ub, - msg="Constraint %s should have" - " a lower or upper bound" % con.name, - ) - - self.assertEqual( - len( - [ - con - for con in m.component_data_objects( - Constraint, active=True, descend_into=True - ) - ] - ), - num_orig_cons + num_lbub_cons - num_nobound_cons, - msg="Expected number of constraints after\n " - "standardizing constraints not matched. " - "Number of constraints after\n " - "transformation" - " should be (number constraints in original " - "model) \n + (number of constraints with " - "distinct finite lower and upper bounds).", - ) - - def test_transform_does_not_alter_num_of_constraints(self): - """ - Check that if model does not contain any constraints - for which both the `lower` and `upper` attributes are - distinct and not None, then number of constraints remains the same - after constraint standardization. - Standard form for the purpose of PyROS is all inequality constraints - as `g(.)<=0`. - """ - m = ConcreteModel() - m.x = Var(initialize=1, bounds=(0, 1)) - m.y = Var(initialize=0, bounds=(None, 1)) - m.con1 = Constraint(expr=m.x >= 1 + m.y) - m.con2 = Constraint(expr=m.x**2 + m.y**2 >= 9) - original_num_constraints = len(list(m.component_data_objects(Constraint))) - transform_to_standard_form(m) - final_num_constraints = len(list(m.component_data_objects(Constraint))) - self.assertEqual( - original_num_constraints, - final_num_constraints, - msg="Transform to standard form function led to a " - "different number of constraints than in the original model.", - ) - number_of_non_standard_form_inequalities = len( - list( - c for c in list(m.component_data_objects(Constraint)) if c.lower != None - ) - ) - self.assertEqual( - number_of_non_standard_form_inequalities, - 0, - msg="All inequality constraints were not transformed to standard form.", - ) - - class TestPyROSSolveFactorModelSet(unittest.TestCase): """ Test PyROS successfully solves model with factor model uncertainty. diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 4c4900b61b9..5d23774a873 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -544,70 +544,6 @@ class ObjectiveType(Enum): nominal = auto() -def recast_to_min_obj(model, obj): - """ - Recast model objective to a minimization objective, as necessary. - - Parameters - ---------- - model : ConcreteModel - Model of interest. - obj : ScalarObjective - Objective of interest. - """ - if obj.sense is not minimize: - if isinstance(obj.expr, SumExpression): - # ensure additive terms in objective - # are split in accordance with user declaration - obj.expr = sum(-term for term in obj.expr.args) - else: - obj.expr = -obj.expr - obj.sense = minimize - - -def turn_bounds_to_constraints(variable, model, config=None): - ''' - Turn the variable in question's "bounds" into direct inequality constraints on the model. - :param variable: the variable with bounds to be turned to None and made into constraints. - :param model: the model in which the variable resides - :param config: solver config - :return: the list of inequality constraints that are the bounds - ''' - lb, ub = variable.lower, variable.upper - if variable.domain is not Reals: - variable.domain = Reals - - if isinstance(lb, NPV_MaxExpression): - lb_args = lb.args - else: - lb_args = (lb,) - - if isinstance(ub, NPV_MinExpression): - ub_args = ub.args - else: - ub_args = (ub,) - - count = 0 - for arg in lb_args: - if arg is not None: - name = unique_component_name( - model, variable.name + f"_lower_bound_con_{count}" - ) - model.add_component(name, Constraint(expr=arg - variable <= 0)) - count += 1 - variable.setlb(None) - - count = 0 - for arg in ub_args: - if arg is not None: - name = unique_component_name( - model, variable.name + f"_upper_bound_con_{count}" - ) - model.add_component(name, Constraint(expr=variable - arg <= 0)) - count += 1 - variable.setub(None) - - def get_time_from_solver(results): """ Obtain solver time from a Pyomo `SolverResults` object. @@ -648,125 +584,6 @@ def get_time_from_solver(results): return float("nan") if solve_time is None else solve_time -def transform_to_standard_form(model): - """ - Recast all model inequality constraints of the form `a <= g(v)` (`<= b`) - to the 'standard' form `a - g(v) <= 0` (and `g(v) - b <= 0`), - in which `v` denotes all model variables and `a` and `b` are - contingent on model parameters. - - Parameters - ---------- - model : ConcreteModel - The model to search for constraints. This will descend into all - active Blocks and sub-Blocks as well. - - Note - ---- - If `a` and `b` are identical and the constraint is not classified as an - equality (i.e. the `equality` attribute of the constraint object - is `False`), then the constraint is recast to the equality `g(v) == a`. - """ - # Note: because we will be adding / modifying the number of - # constraints, we want to resolve the generator to a list before - # starting. - cons = list( - model.component_data_objects(Constraint, descend_into=True, active=True) - ) - for con in cons: - if not con.equality: - has_lb = con.lower is not None - has_ub = con.upper is not None - - if has_lb and has_ub: - if con.lower is con.upper: - # recast as equality Constraint - con.set_value(con.lower == con.body) - else: - # range inequality; split into two Constraints. - uniq_name = unique_component_name(model, con.name + '_lb') - model.add_component( - uniq_name, Constraint(expr=con.lower - con.body <= 0) - ) - con.set_value(con.body - con.upper <= 0) - elif has_lb: - # not in standard form; recast. - con.set_value(con.lower - con.body <= 0) - elif has_ub: - # move upper bound to body. - con.set_value(con.body - con.upper <= 0) - else: - # unbounded constraint: deactivate - con.deactivate() - - -def get_vars_from_component(block, ctype): - """Determine all variables used in active components within a block. - - Parameters - ---------- - block: Block - The block to search for components. This is a recursive - generator and will descend into any active sub-Blocks as well. - ctype: class - The component type (typically either :py:class:`Constraint` or - :py:class:`Objective` to search for). - - """ - - return get_vars_from_components(block, ctype, active=True, descend_into=True) - - -def replace_uncertain_bounds_with_constraints(model, uncertain_params): - """ - For variables of which the bounds are dependent on the parameters - in the list `uncertain_params`, remove the bounds and add - explicit variable bound inequality constraints. - - :param model: Model in which to make the bounds/constraint replacements - :type model: class:`pyomo.core.base.PyomoModel.ConcreteModel` - :param uncertain_params: List of uncertain model parameters - :type uncertain_params: list - """ - uncertain_param_set = ComponentSet(uncertain_params) - - # component for explicit inequality constraints - uncertain_var_bound_constrs = ConstraintList() - model.add_component( - unique_component_name(model, 'uncertain_var_bound_cons'), - uncertain_var_bound_constrs, - ) - - # get all variables in active objective and constraint expression(s) - vars_in_cons = ComponentSet(get_vars_from_component(model, Constraint)) - vars_in_obj = ComponentSet(get_vars_from_component(model, Objective)) - - for v in vars_in_cons | vars_in_obj: - # get mutable parameters in variable bounds expressions - ub = v.upper - mutable_params_ub = ComponentSet(identify_mutable_parameters(ub)) - lb = v.lower - mutable_params_lb = ComponentSet(identify_mutable_parameters(lb)) - - # add explicit inequality constraint(s), remove variable bound(s) - if mutable_params_ub & uncertain_param_set: - if type(ub) is NPV_MinExpression: - upper_bounds = ub.args - else: - upper_bounds = (ub,) - for u_bnd in upper_bounds: - uncertain_var_bound_constrs.add(v - u_bnd <= 0) - v.setub(None) - if mutable_params_lb & uncertain_param_set: - if type(ub) is NPV_MaxExpression: - lower_bounds = lb.args - else: - lower_bounds = (lb,) - for l_bnd in lower_bounds: - uncertain_var_bound_constrs.add(l_bnd - v <= 0) - v.setlb(None) - - def standardize_component_data( obj, valid_ctype, @@ -2190,139 +2007,6 @@ def standardize_active_objective(model_data, config): ) -def new_add_decision_rule_variables(model_data, config): - """ - Add variables parameterizing the (polynomial) - decision rules to the working model. - - Parameters - ---------- - model_data : model data object - Model data. - config : ConfigDict - PyROS solver options. - - Notes - ----- - 1. One set of decision rule variables is added for each - effective second-stage variable. - 2. As an efficiency, no decision rule variables - are added for the nonadjustable, user-defined second-stage - variables, since the decision rules for such variables - are necessarily nonstatic. - """ - effective_second_stage_vars = ( - model_data.working_model.effective_var_partitioning.second_stage_variables - ) - model_data.working_model.decision_rule_vars = decision_rule_vars = [] - - # facilitate matching of effective second-stage vars to DR vars later - model_data.working_model.eff_ss_var_to_dr_var_map = eff_ss_var_to_dr_var_map = ( - ComponentMap() - ) - - # since DR expression is a general polynomial in the uncertain - # parameters, the exact number of DR variables - # per effective second-stage variable - # depends only on the DR order and uncertainty set dimension - degree = config.decision_rule_order - num_uncertain_params = len(model_data.working_model.uncertain_params) - num_dr_vars = sp.special.comb( - N=num_uncertain_params + degree, k=degree, exact=True, repetition=False - ) - - for idx, eff_ss_var in enumerate(effective_second_stage_vars): - indexed_dr_var = Var( - range(num_dr_vars), initialize=0, bounds=(None, None), domain=Reals - ) - model_data.working_model.add_component( - f"decision_rule_var_{idx}", indexed_dr_var - ) - - # index 0 entry of the IndexedVar is the static - # DR term. initialize to user-provided value of - # the corresponding second-stage variable. - # all other entries remain initialized to 0. - indexed_dr_var[0].set_value(value(eff_ss_var, exception=False)) - - # update attributes - decision_rule_vars.append(indexed_dr_var) - eff_ss_var_to_dr_var_map[eff_ss_var] = indexed_dr_var - - -def new_add_decision_rule_constraints(model_data, config): - """ - Add decision rule equality constraints to the working model. - - Parameters - ---------- - model_data : model data object - Main model data object. - config : ConfigDict - PyROS solver options. - """ - - effective_second_stage_vars = ( - model_data.working_model.effective_var_partitioning.second_stage_variables - ) - indexed_dr_var_list = model_data.working_model.decision_rule_vars - uncertain_params = model_data.working_model.uncertain_params - degree = config.decision_rule_order - - model_data.working_model.decision_rule_eqns = decision_rule_eqns = [] - - # keeping track of degree of monomial - # (in terms of the uncertain parameters) - # in which each DR coefficient participates will be useful for - # later - model_data.working_model.dr_var_to_exponent_map = dr_var_to_exponent_map = ( - ComponentMap() - ) - - # facilitate retrieval of DR equation for a given - # effective second-stage variable later - model_data.working_model.eff_ss_var_to_dr_eqn_map = eff_ss_var_to_dr_eqn_map = ( - ComponentMap() - ) - - # set up uncertain parameter combinations for - # construction of the monomials of the DR expressions - monomial_param_combos = [] - for power in range(degree + 1): - power_combos = it.combinations_with_replacement(uncertain_params, power) - monomial_param_combos.extend(power_combos) - - # now construct DR equations and declare them on the working model - second_stage_dr_var_zip = zip(effective_second_stage_vars, indexed_dr_var_list) - for idx, (eff_ss_var, indexed_dr_var) in enumerate(second_stage_dr_var_zip): - # for each DR equation, the number of coefficients should match - # the number of monomial terms exactly - if len(monomial_param_combos) != len(indexed_dr_var.index_set()): - raise ValueError( - f"Mismatch between number of DR coefficient variables " - f"and number of DR monomials for DR equation index {idx}, " - "corresponding to effective second-stage variable " - f"{eff_ss_var.name!r}. " - f"({len(indexed_dr_var.index_set())}!= {len(monomial_param_combos)})" - ) - - # construct the DR polynomial - dr_expression = 0 - for dr_var, param_combo in zip(indexed_dr_var.values(), monomial_param_combos): - dr_expression += dr_var * prod(param_combo) - - # map decision rule var to degree (exponent) of the - # associated monomial with respect to the uncertain params - dr_var_to_exponent_map[dr_var] = len(param_combo) - - # declare constraint on model - dr_eqn = Constraint(expr=dr_expression - eff_ss_var == 0) - model_data.working_model.add_component(f"decision_rule_eqn_{idx}", dr_eqn) - - decision_rule_eqns.append(dr_eqn) - eff_ss_var_to_dr_eqn_map[eff_ss_var] = dr_eqn - - def get_all_nonadjustable_variables(working_model): """ Get all nonadjustable variables of the working model. @@ -2806,133 +2490,6 @@ def log_model_statistics(model_data, config): info_log_func(f" Performance inequalities : {num_performance_ineq_cons}") -def preprocess_model_data(model_data, config, var_partitioning): - """ - Preprocess model data. - """ - original_model = model_data.original_model - - # new_preprocess_model_data(model_data, config, var_partitioning) - - # temporary block to track variable partitioning - # and uncertain parameters after cloning. - # TODO: model may already have an attribute called `util`; - # fix that edge case - original_model.util = Block(concrete=True) - original_model.util.first_stage_variables = var_partitioning.first_stage_variables - original_model.util.second_stage_variables = var_partitioning.second_stage_variables - original_model.util.state_vars = var_partitioning.state_variables - original_model.util.uncertain_params = config.uncertain_params - - model_data.util_block = original_model.util - - # keep track of variables after cloning - cname = unique_component_name(model_data.original_model, 'tmp_var_list') - src_vars = list(model_data.original_model.component_data_objects(Var)) - setattr(model_data.original_model, cname, src_vars) - model_data.working_model = model_data.original_model.clone() - - # identify active objective function. - # (there should only be one at this point) - # recast to minimization if necessary - active_objs = list( - model_data.working_model.component_data_objects( - Objective, active=True, descend_into=True - ) - ) - assert len(active_objs) == 1 - active_obj = active_objs[0] - model_data.active_obj_original_sense = active_obj.sense - recast_to_min_obj(model_data.working_model, active_obj) - - # === Determine first and second-stage objectives - identify_objective_functions(model_data.working_model, active_obj) - active_obj.deactivate() - - # === Put model in standard form - transform_to_standard_form(model_data.working_model) - - # === Replace variable bounds depending on uncertain params with - # explicit inequality constraints - replace_uncertain_bounds_with_constraints( - model_data.working_model, model_data.working_model.util.uncertain_params - ) - - # === Add decision rule information - add_decision_rule_variables(model_data, config) - add_decision_rule_constraints(model_data, config) - - # === Move bounds on control variables to explicit ineq constraints - wm_util = model_data.working_model - - # cast bounds on second-stage and state variables to - # explicit constraints for separation objectives - for c in model_data.working_model.util.second_stage_variables: - turn_bounds_to_constraints(c, wm_util, config) - for c in model_data.working_model.util.state_vars: - turn_bounds_to_constraints(c, wm_util, config) - - # === Make control_variable_bounds array - wm_util.ssv_bounds = [] - for c in model_data.working_model.component_data_objects( - Constraint, descend_into=True - ): - if "bound_con" in c.name: - wm_util.ssv_bounds.append(c) - - -def substitute_ssv_in_dr_constraints(model, constraint): - ''' - Generate the standard_repn for the dr constraints. Generate new expression with replace_expression to ignore - the ssv component. - Then, replace_expression with substitution_map between ssv and the new expression. - Deactivate or del_component the original dr equation. - Then, return modified model and do coefficient matching as normal. - :param model: the working_model - :param constraint: an equality constraint from the working model identified to be of the form h(x,z,q) = 0. - :return: - ''' - dr_eqns = model.util.decision_rule_eqns - fsv = ComponentSet(model.util.first_stage_variables) - if not hasattr(model, "dr_substituted_constraints"): - model.dr_substituted_constraints = ConstraintList() - - substitution_map = {} - for eqn in dr_eqns: - repn = generate_standard_repn(eqn.body, compute_values=False) - new_expression = 0 - map_linear_coeff_to_var = [ - x - for x in zip(repn.linear_coefs, repn.linear_vars) - if x[1] in ComponentSet(fsv) - ] - map_quad_coeff_to_var = [ - x - for x in zip(repn.quadratic_coefs, repn.quadratic_vars) - if x[1] in ComponentSet(fsv) - ] - if repn.linear_coefs: - for coeff, var in map_linear_coeff_to_var: - new_expression += coeff * var - if repn.quadratic_coefs: - for coeff, var in map_quad_coeff_to_var: - new_expression += coeff * var[0] * var[1] # var here is a 2-tuple - - substitution_map[id(repn.linear_vars[-1])] = new_expression - - model.dr_substituted_constraints.add( - replace_expressions(expr=constraint.lower, substitution_map=substitution_map) - == replace_expressions(expr=constraint.body, substitution_map=substitution_map) - ) - - # === Delete the original constraint - model.del_component(constraint.name) - - return model.dr_substituted_constraints[ - max(model.dr_substituted_constraints.keys()) - ] - - def selective_clone(block, first_stage_vars): """ Clone everything in a base_model except for the first-stage variables @@ -2949,40 +2506,48 @@ def selective_clone(block, first_stage_vars): return new_block -def add_decision_rule_variables(model_data, config): +def new_add_decision_rule_variables(model_data, config): """ - Add variables for polynomial decision rules to the working - model. + Add variables parameterizing the (polynomial) + decision rules to the working model. Parameters ---------- - model_data : ROSolveResults + model_data : model data object Model data. - config : config_dict + config : ConfigDict PyROS solver options. - Note - ---- - Decision rule variables are considered first-stage decision - variables which do not get copied at each iteration. - PyROS currently supports static (zeroth order), - affine (first-order), and quadratic DR. + Notes + ----- + 1. One set of decision rule variables is added for each + effective second-stage variable. + 2. As an efficiency, no decision rule variables + are added for the nonadjustable, user-defined second-stage + variables, since the decision rules for such variables + are necessarily nonstatic. """ - second_stage_variables = model_data.working_model.util.second_stage_variables - first_stage_variables = model_data.working_model.util.first_stage_variables - decision_rule_vars = [] + effective_second_stage_vars = ( + model_data.working_model.effective_var_partitioning.second_stage_variables + ) + model_data.working_model.decision_rule_vars = decision_rule_vars = [] + + # facilitate matching of effective second-stage vars to DR vars later + model_data.working_model.eff_ss_var_to_dr_var_map = eff_ss_var_to_dr_var_map = ( + ComponentMap() + ) # since DR expression is a general polynomial in the uncertain - # parameters, the exact number of DR variables per second-stage - # variable depends on DR order and uncertainty set dimension + # parameters, the exact number of DR variables + # per effective second-stage variable + # depends only on the DR order and uncertainty set dimension degree = config.decision_rule_order - num_uncertain_params = len(model_data.working_model.util.uncertain_params) + num_uncertain_params = len(model_data.working_model.uncertain_params) num_dr_vars = sp.special.comb( N=num_uncertain_params + degree, k=degree, exact=True, repetition=False ) - for idx, ss_var in enumerate(second_stage_variables): - # declare DR coefficients for current second-stage variable + for idx, eff_ss_var in enumerate(effective_second_stage_vars): indexed_dr_var = Var( range(num_dr_vars), initialize=0, bounds=(None, None), domain=Reals ) @@ -2994,36 +2559,47 @@ def add_decision_rule_variables(model_data, config): # DR term. initialize to user-provided value of # the corresponding second-stage variable. # all other entries remain initialized to 0. - indexed_dr_var[0].set_value(value(ss_var, exception=False)) + indexed_dr_var[0].set_value(value(eff_ss_var, exception=False)) # update attributes - first_stage_variables.extend(indexed_dr_var.values()) decision_rule_vars.append(indexed_dr_var) - - model_data.working_model.util.decision_rule_vars = decision_rule_vars + eff_ss_var_to_dr_var_map[eff_ss_var] = indexed_dr_var -def add_decision_rule_constraints(model_data, config): +def new_add_decision_rule_constraints(model_data, config): """ Add decision rule equality constraints to the working model. Parameters ---------- - model_data : ROSolveResults - Model data. + model_data : model data object + Main model data object. config : ConfigDict PyROS solver options. """ - second_stage_variables = model_data.working_model.util.second_stage_variables - uncertain_params = model_data.working_model.util.uncertain_params - decision_rule_eqns = [] - decision_rule_vars_list = model_data.working_model.util.decision_rule_vars + effective_second_stage_vars = ( + model_data.working_model.effective_var_partitioning.second_stage_variables + ) + indexed_dr_var_list = model_data.working_model.decision_rule_vars + uncertain_params = model_data.working_model.uncertain_params degree = config.decision_rule_order - # keeping track of degree of monomial in which each - # DR coefficient participates will be useful for later - dr_var_to_exponent_map = ComponentMap() + model_data.working_model.decision_rule_eqns = decision_rule_eqns = [] + + # keeping track of degree of monomial + # (in terms of the uncertain parameters) + # in which each DR coefficient participates will be useful for + # later + model_data.working_model.dr_var_to_exponent_map = dr_var_to_exponent_map = ( + ComponentMap() + ) + + # facilitate retrieval of DR equation for a given + # effective second-stage variable later + model_data.working_model.eff_ss_var_to_dr_eqn_map = eff_ss_var_to_dr_eqn_map = ( + ComponentMap() + ) # set up uncertain parameter combinations for # construction of the monomials of the DR expressions @@ -3033,15 +2609,16 @@ def add_decision_rule_constraints(model_data, config): monomial_param_combos.extend(power_combos) # now construct DR equations and declare them on the working model - second_stage_dr_var_zip = zip(second_stage_variables, decision_rule_vars_list) - for idx, (ss_var, indexed_dr_var) in enumerate(second_stage_dr_var_zip): + second_stage_dr_var_zip = zip(effective_second_stage_vars, indexed_dr_var_list) + for idx, (eff_ss_var, indexed_dr_var) in enumerate(second_stage_dr_var_zip): # for each DR equation, the number of coefficients should match # the number of monomial terms exactly if len(monomial_param_combos) != len(indexed_dr_var.index_set()): raise ValueError( f"Mismatch between number of DR coefficient variables " f"and number of DR monomials for DR equation index {idx}, " - f"corresponding to second-stage variable {ss_var.name!r}. " + "corresponding to effective second-stage variable " + f"{eff_ss_var.name!r}. " f"({len(indexed_dr_var.index_set())}!= {len(monomial_param_combos)})" ) @@ -3055,15 +2632,11 @@ def add_decision_rule_constraints(model_data, config): dr_var_to_exponent_map[dr_var] = len(param_combo) # declare constraint on model - dr_eqn = Constraint(expr=dr_expression - ss_var == 0) + dr_eqn = Constraint(expr=dr_expression - eff_ss_var == 0) model_data.working_model.add_component(f"decision_rule_eqn_{idx}", dr_eqn) - # append to list of DR equality constraints decision_rule_eqns.append(dr_eqn) - - # finally, add attributes to util block - model_data.working_model.util.decision_rule_eqns = decision_rule_eqns - model_data.working_model.util.dr_var_to_exponent_map = dr_var_to_exponent_map + eff_ss_var_to_dr_eqn_map[eff_ss_var] = dr_eqn def enforce_dr_degree(blk, config, degree): From 14008142ceebd93ff9b2750c7b1dd947cf96ac96 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 15:30:38 -0400 Subject: [PATCH 1880/3044] Fix testing for method `solve_master` --- pyomo/contrib/pyros/tests/test_grcs.py | 78 +--------------- pyomo/contrib/pyros/tests/test_master.py | 113 +++++++++++++++++++++++ 2 files changed, 114 insertions(+), 77 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 4414e76964a..c8cba038578 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -20,8 +20,7 @@ import pyomo.common.unittest as unittest from pyomo.common.log import LoggingIntercept -from pyomo.common.collections import Bunch, ComponentSet, ComponentMap -from pyomo.common.config import ConfigBlock, ConfigValue +from pyomo.common.collections import Bunch, ComponentSet from pyomo.core.base.set_types import NonNegativeIntegers from pyomo.core.expr import ( identify_mutable_parameters, @@ -61,7 +60,6 @@ ) from pyomo.contrib.pyros.master_problem_methods import ( - solve_master, minimize_dr_vars, ) from pyomo.contrib.pyros.solve_data import MasterProblemData, ROSolveResults @@ -504,80 +502,6 @@ def test_two_stg_model_discrete_set(self): global_solver = "baron" -class testSolveMaster(unittest.TestCase): - @unittest.skipUnless(baron_available, "Global NLP solver is not available.") - def test_solve_master(self): - working_model = m = ConcreteModel() - m.x = Var(initialize=0.5, bounds=(0, 10)) - m.y = Var(initialize=1.0, bounds=(0, 5)) - m.z = Var(initialize=0, bounds=(None, None)) - m.p = Param(initialize=1, mutable=True) - m.obj = Objective(expr=m.x) - m.con = Constraint(expr=m.x + m.y + m.z <= 3) - model_data = MasterProblemData() - model_data.working_model = working_model - model_data.timing = None - model_data.iteration = 0 - master_data = initial_construct_master(model_data) - master_data.master_model.scenarios[0, 0].transfer_attributes_from( - working_model.clone() - ) - master_data.master_model.scenarios[0, 0].util = Block() - master_data.master_model.scenarios[0, 0].util.first_stage_variables = [ - master_data.master_model.scenarios[0, 0].x - ] - master_data.master_model.scenarios[0, 0].util.decision_rule_vars = [] - master_data.master_model.scenarios[0, 0].util.second_stage_variables = [] - master_data.master_model.scenarios[0, 0].util.uncertain_params = [ - master_data.master_model.scenarios[0, 0].p - ] - master_data.master_model.scenarios[0, 0].first_stage_objective = 0 - master_data.master_model.scenarios[0, 0].second_stage_objective = Expression( - expr=master_data.master_model.scenarios[0, 0].x - ) - master_data.master_model.scenarios[0, 0].util.dr_var_to_exponent_map = ( - ComponentMap() - ) - master_data.iteration = 0 - master_data.timing = TimingData() - - box_set = BoxSet(bounds=[(0, 2)]) - solver = SolverFactory(global_solver) - config = ConfigBlock() - config.declare("backup_global_solvers", ConfigValue(default=[])) - config.declare("backup_local_solvers", ConfigValue(default=[])) - config.declare("solve_master_globally", ConfigValue(default=True)) - config.declare("global_solver", ConfigValue(default=solver)) - config.declare("tee", ConfigValue(default=False)) - config.declare("decision_rule_order", ConfigValue(default=1)) - config.declare("objective_focus", ConfigValue(default=ObjectiveType.worst_case)) - config.declare( - "second_stage_variables", - ConfigValue( - default=master_data.master_model.scenarios[ - 0, 0 - ].util.second_stage_variables - ), - ) - config.declare("subproblem_file_directory", ConfigValue(default=None)) - config.declare("time_limit", ConfigValue(default=None)) - config.declare( - "progress_logger", ConfigValue(default=logging.getLogger(__name__)) - ) - config.declare("symbolic_solver_labels", ConfigValue(default=False)) - - with time_code(master_data.timing, "main", is_main_timer=True): - master_soln = solve_master(master_data, config) - self.assertEqual( - master_soln.termination_condition, - TerminationCondition.optimal, - msg=( - "Could not solve simple master problem with solve_master " - "function." - ), - ) - - # === regression test for the solver @unittest.skipUnless(baron_available, "Global NLP solver is not available.") class RegressionTest(unittest.TestCase): diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index 3ba0b49fea0..d4ac1f4b3a6 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -5,6 +5,7 @@ import logging +import time import unittest from pyomo.common.collections import Bunch @@ -18,23 +19,34 @@ ) from pyomo.core.expr import exp from pyomo.core.expr.compare import assertExpressionsEqual +from pyomo.environ import SolverFactory +from pyomo.opt import TerminationCondition from pyomo.contrib.pyros.master_problem_methods import ( add_scenario_block_to_master_problem, construct_initial_master_problem, new_construct_master_feasibility_problem, new_construct_dr_polishing_problem, + NewMasterProblemData, + solve_master, ) from pyomo.contrib.pyros.util import ( new_preprocess_model_data, ObjectiveType, + time_code, + TimingData, VariablePartitioning, + pyrosTerminationCondition, ) if not (numpy_available and scipy_available): raise unittest.SkipTest("Packages numpy and scipy must both be available.") +_baron = SolverFactory("baron") +baron_available = _baron.available() +baron_license_is_valid = _baron.license_is_valid() + logger = logging.getLogger(__name__) @@ -404,5 +416,106 @@ def test_construct_dr_polishing_problem_objectives(self): self.assertTrue(polishing_model.polishing_obj.active) +class TestSolveMaster(unittest.TestCase): + """ + Test method for solving master problem + """ + @unittest.skipUnless(baron_available, "Global NLP solver is not available.") + def test_solve_master(self): + model_data, config = build_simple_model_data() + model_data.timing = TimingData() + baron = SolverFactory("baron") + config.update(dict( + local_solver=baron, + global_solver=baron, + backup_local_solvers=[], + backup_global_solvers=[], + tee=False, + )) + master_data = NewMasterProblemData(model_data, config) + with time_code(master_data.timing, "main", is_main_timer=True): + master_soln = solve_master(master_data, config) + self.assertEqual( + master_soln.termination_condition, + TerminationCondition.optimal, + msg=( + "Could not solve simple master problem with solve_master " + "function." + ), + ) + + @unittest.skipUnless(baron_available, "Global NLP solver is not available") + def test_solve_master_timeout_on_master(self): + """ + Test method for solution of master problems times out + on feasibility problem. + """ + model_data, config = build_simple_model_data() + model_data.timing = TimingData() + baron = SolverFactory("baron") + config.update(dict( + local_solver=baron, + global_solver=baron, + backup_local_solvers=[], + backup_global_solvers=[], + tee=False, + time_limit=1, + )) + master_data = NewMasterProblemData(model_data, config) + with time_code(master_data.timing, "main", is_main_timer=True): + time.sleep(1) + master_soln = solve_master(master_data, config) + self.assertEqual( + master_soln.termination_condition, + TerminationCondition.optimal, + msg=( + "Could not solve simple master problem with solve_master " + "function." + ), + ) + self.assertEqual( + master_soln.master_subsolver_results, + (None, pyrosTerminationCondition.time_out), + ) + + @unittest.skipUnless(baron_available, "Global NLP solver is not available") + def test_solve_master_timeout_on_master_feasibility(self): + """ + Test method for solution of master problems times out + on feasibility problem. + """ + model_data, config = build_simple_model_data() + model_data.timing = TimingData() + baron = SolverFactory("baron") + config.update(dict( + local_solver=baron, + global_solver=baron, + backup_local_solvers=[], + backup_global_solvers=[], + tee=False, + time_limit=1, + )) + master_data = NewMasterProblemData(model_data, config) + add_scenario_block_to_master_problem( + master_data.master_model, + scenario_idx=[1, 0], + param_realization=[0.6], + from_block=master_data.master_model.scenarios[0, 0], + clone_first_stage_components=False, + ) + master_data.iteration = 1 + with time_code(master_data.timing, "main", is_main_timer=True): + time.sleep(1) + master_soln = solve_master(master_data, config) + self.assertEqual( + master_soln.pyros_termination_condition, + pyrosTerminationCondition.time_out, + ) + self.assertEqual( + master_soln.master_subsolver_results, + (None, pyrosTerminationCondition.time_out), + ) + + if __name__ == "__main__": unittest.main() From 036d74d3ecfe3210512678326a138a7398843873 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 15:47:27 -0400 Subject: [PATCH 1881/3044] Fix DR polishing tests --- pyomo/contrib/pyros/tests/test_grcs.py | 60 ------------------------ pyomo/contrib/pyros/tests/test_master.py | 54 +++++++++++++++++++-- 2 files changed, 50 insertions(+), 64 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index c8cba038578..81decb1ba95 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -625,66 +625,6 @@ def regression_test_quad_drs(self): pyrosTerminationCondition.robust_feasible, ) - @unittest.skipUnless( - baron_license_is_valid, "Global NLP solver is not available and licensed." - ) - def test_minimize_dr_norm(self): - m = ConcreteModel() - m.p1 = Param(initialize=0, mutable=True) - m.p2 = Param(initialize=0, mutable=True) - m.z1 = Var(initialize=0, bounds=(0, 1)) - m.z2 = Var(initialize=0, bounds=(0, 1)) - - m.working_model = ConcreteModel() - m.working_model.util = Block() - - m.working_model.util.second_stage_variables = [m.z1, m.z2] - m.working_model.util.uncertain_params = [m.p1, m.p2] - m.working_model.util.first_stage_variables = [] - m.working_model.util.state_vars = [] - - m.working_model.util.first_stage_variables = [] - config = Bunch() - config.decision_rule_order = 1 - config.objective_focus = ObjectiveType.nominal - config.global_solver = SolverFactory('baron') - config.uncertain_params = m.working_model.util.uncertain_params - config.tee = False - config.solve_master_globally = True - config.time_limit = None - config.progress_logger = logging.getLogger(__name__) - - add_decision_rule_variables(model_data=m, config=config) - add_decision_rule_constraints(model_data=m, config=config) - - # === Make master_type model - master = ConcreteModel() - master.scenarios = Block(NonNegativeIntegers, NonNegativeIntegers) - master.scenarios[0, 0].transfer_attributes_from(m.working_model.clone()) - master.scenarios[0, 0].first_stage_objective = 0 - master.scenarios[0, 0].second_stage_objective = Expression( - expr=(master.scenarios[0, 0].util.second_stage_variables[0] - 1) ** 2 - + (master.scenarios[0, 0].util.second_stage_variables[1] - 1) ** 2 - ) - master.obj = Objective(expr=master.scenarios[0, 0].second_stage_objective) - master_data = MasterProblemData() - master_data.master_model = master - master_data.master_model.const_efficiency_applied = False - master_data.master_model.linear_efficiency_applied = False - master_data.iteration = 0 - - master_data.timing = TimingData() - with time_code(master_data.timing, "main", is_main_timer=True): - results, success = minimize_dr_vars(model_data=master_data, config=config) - self.assertEqual( - results.solver.termination_condition, - TerminationCondition.optimal, - msg="Minimize dr norm did not solve to optimality.", - ) - self.assertTrue( - success, msg=f"DR polishing success {success}, expected True." - ) - @unittest.skipUnless( baron_license_is_valid, "Global NLP solver is not available and licensed." ) diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index d4ac1f4b3a6..3a14fdfac72 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -28,7 +28,6 @@ new_construct_master_feasibility_problem, new_construct_dr_polishing_problem, NewMasterProblemData, - solve_master, ) from pyomo.contrib.pyros.util import ( new_preprocess_model_data, @@ -434,7 +433,7 @@ def test_solve_master(self): )) master_data = NewMasterProblemData(model_data, config) with time_code(master_data.timing, "main", is_main_timer=True): - master_soln = solve_master(master_data, config) + master_soln = master_data.solve_master() self.assertEqual( master_soln.termination_condition, TerminationCondition.optimal, @@ -464,7 +463,7 @@ def test_solve_master_timeout_on_master(self): master_data = NewMasterProblemData(model_data, config) with time_code(master_data.timing, "main", is_main_timer=True): time.sleep(1) - master_soln = solve_master(master_data, config) + master_soln = master_data.solve_master() self.assertEqual( master_soln.termination_condition, TerminationCondition.optimal, @@ -506,7 +505,7 @@ def test_solve_master_timeout_on_master_feasibility(self): master_data.iteration = 1 with time_code(master_data.timing, "main", is_main_timer=True): time.sleep(1) - master_soln = solve_master(master_data, config) + master_soln = master_data.solve_master() self.assertEqual( master_soln.pyros_termination_condition, pyrosTerminationCondition.time_out, @@ -517,5 +516,52 @@ def test_solve_master_timeout_on_master_feasibility(self): ) +class TestPolishDRVars(unittest.TestCase): + """ + Test DR polishing subroutine. + """ + @unittest.skipUnless( + baron_license_is_valid, "Global NLP solver is not available and licensed." + ) + def test_polish_dr_vars(self): + model_data, config = build_simple_model_data() + model_data.timing = TimingData() + baron = SolverFactory("baron") + config.update(dict( + local_solver=baron, + global_solver=baron, + backup_local_solvers=[], + backup_global_solvers=[], + tee=False, + )) + master_data = NewMasterProblemData(model_data, config) + add_scenario_block_to_master_problem( + master_data.master_model, + scenario_idx=[1, 0], + param_realization=[0.6], + from_block=master_data.master_model.scenarios[0, 0], + clone_first_stage_components=False, + ) + master_data.iteration = 1 + + master_data.timing = TimingData() + with time_code(master_data.timing, "main", is_main_timer=True): + master_soln = master_data.solve_master() + self.assertEqual( + master_soln.termination_condition, + TerminationCondition.optimal, + ) + + results, success = master_data.solve_dr_polishing() + self.assertEqual( + results.solver.termination_condition, + TerminationCondition.optimal, + msg="Minimize dr norm did not solve to optimality.", + ) + self.assertTrue( + success, msg=f"DR polishing success {success}, expected True." + ) + + if __name__ == "__main__": unittest.main() From 74170f3d1ba42658ebc3770089f0a9e38aa0ec9d Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 16:02:14 -0400 Subject: [PATCH 1882/3044] Remove unused method for identifying objective expressions --- pyomo/contrib/pyros/tests/test_grcs.py | 180 +------------------------ pyomo/contrib/pyros/util.py | 53 -------- 2 files changed, 2 insertions(+), 231 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 81decb1ba95..2971f8ec649 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -20,12 +20,8 @@ import pyomo.common.unittest as unittest from pyomo.common.log import LoggingIntercept -from pyomo.common.collections import Bunch, ComponentSet +from pyomo.common.collections import Bunch from pyomo.core.base.set_types import NonNegativeIntegers -from pyomo.core.expr import ( - identify_mutable_parameters, - identify_variables, -) from pyomo.repn.plugins import nl_writer as pyomo_nl_writer from pyomo.common.dependencies import numpy as np, numpy_available from pyomo.common.dependencies import scipy as scipy_available @@ -44,25 +40,19 @@ Block, ConcreteModel, Constraint, - Expression, Objective, Param, SolverFactory, Var, - cos, exp, log, - sin, sqrt, value, maximize, minimize, ) -from pyomo.contrib.pyros.master_problem_methods import ( - minimize_dr_vars, -) -from pyomo.contrib.pyros.solve_data import MasterProblemData, ROSolveResults +from pyomo.contrib.pyros.solve_data import ROSolveResults from pyomo.contrib.pyros.uncertainty_sets import ( BoxSet, AxisAlignedEllipsoidalSet, @@ -71,13 +61,10 @@ DiscreteScenarioSet, ) from pyomo.contrib.pyros.util import ( - identify_objective_functions, IterationLogRecord, ObjectiveType, pyrosTerminationCondition, selective_clone, - time_code, - TimingData, ) logger = logging.getLogger(__name__) @@ -1973,169 +1960,6 @@ def test_multiple_objs(self): ) -class testModelIdentifyObjectives(unittest.TestCase): - """ - This class contains tests for validating routines used to - determine the first-stage and second-stage portions of a - two-stage expression. - """ - - def test_identify_objectives(self): - """ - Test first and second-stage objective identification - for a simple two-stage model. - """ - # model - m = ConcreteModel() - - # parameters - m.p = Param(range(4), initialize=1, mutable=True) - m.q = Param(initialize=1) - - # variables - m.x = Var(range(4)) - m.z = Var() - m.y = Var(initialize=2) - - # objective - m.obj = Objective( - expr=( - (m.x[0] + m.y) - * ( - sum(m.x[idx] * m.p[idx] for idx in range(3)) - + m.q * m.z - + m.x[0] * m.q - ) - + sin(m.x[0] + m.q) - + cos(m.x[2] + m.z) - ) - ) - - # util block for specifying DOF and uncertainty - m.util = Block() - m.util.first_stage_variables = list(m.x.values()) - m.util.second_stage_variables = [m.z] - m.util.uncertain_params = [m.p[0], m.p[1]] - - identify_objective_functions(m, m.obj) - - fsv_set = ComponentSet(m.util.first_stage_variables) - uncertain_param_set = ComponentSet(m.util.uncertain_params) - - # determine vars and uncertain params participating in - # objective - fsv_in_obj = ComponentSet( - var for var in identify_variables(m.obj) if var in fsv_set - ) - ssv_in_obj = ComponentSet( - var for var in identify_variables(m.obj) if var not in fsv_set - ) - uncertain_params_in_obj = ComponentSet( - param - for param in identify_mutable_parameters(m.obj) - if param in uncertain_param_set - ) - - # determine vars and uncertain params participating in - # first-stage objective - fsv_in_first_stg_cost = ComponentSet( - var for var in identify_variables(m.first_stage_objective) if var in fsv_set - ) - ssv_in_first_stg_cost = ComponentSet( - var - for var in identify_variables(m.first_stage_objective) - if var not in fsv_set - ) - uncertain_params_in_first_stg_cost = ComponentSet( - param - for param in identify_mutable_parameters(m.first_stage_objective) - if param in uncertain_param_set - ) - - # determine vars and uncertain params participating in - # second-stage objective - fsv_in_second_stg_cost = ComponentSet( - var - for var in identify_variables(m.second_stage_objective) - if var in fsv_set - ) - ssv_in_second_stg_cost = ComponentSet( - var - for var in identify_variables(m.second_stage_objective) - if var not in fsv_set - ) - uncertain_params_in_second_stg_cost = ComponentSet( - param - for param in identify_mutable_parameters(m.second_stage_objective) - if param in uncertain_param_set - ) - - # now perform checks - self.assertTrue( - fsv_in_first_stg_cost | fsv_in_second_stg_cost == fsv_in_obj, - f"{{var.name for var in fsv_in_first_stg_cost | fsv_in_second_stg_cost}} " - f"is not {{var.name for var in fsv_in_obj}}", - ) - self.assertFalse( - ssv_in_first_stg_cost, - f"First-stage expression {str(m.first_stage_objective.expr)}" - f" consists of non first-stage variables " - f"{{var.name for var in fsv_in_second_stg_cost}}", - ) - self.assertTrue( - ssv_in_second_stg_cost == ssv_in_obj, - f"{[var.name for var in ssv_in_second_stg_cost]} is not" - f"{{var.name for var in ssv_in_obj}}", - ) - self.assertFalse( - uncertain_params_in_first_stg_cost, - f"First-stage expression {str(m.first_stage_objective.expr)}" - " consists of uncertain params" - f" {{p.name for p in uncertain_params_in_first_stg_cost}}", - ) - self.assertTrue( - uncertain_params_in_second_stg_cost == uncertain_params_in_obj, - f"{{p.name for p in uncertain_params_in_second_stg_cost}} is not " - f"{{p.name for p in uncertain_params_in_obj}}", - ) - - def test_identify_objectives_var_expr(self): - """ - Test first and second-stage objective identification - for an objective expression consisting only of a Var. - """ - # model - m = ConcreteModel() - - # parameters - m.p = Param(range(4), initialize=1, mutable=True) - m.q = Param(initialize=1) - - # variables - m.x = Var(range(4)) - - # objective - m.obj = Objective(expr=m.x[1]) - - # util block for specifying DOF and uncertainty - m.util = Block() - m.util.first_stage_variables = list(m.x.values()) - m.util.second_stage_variables = list() - m.util.uncertain_params = list() - - identify_objective_functions(m, m.obj) - fsv_in_second_stg_obj = list( - v.name for v in identify_variables(m.second_stage_objective) - ) - - # perform checks - self.assertTrue(list(identify_variables(m.first_stage_objective)) == [m.x[1]]) - self.assertFalse( - fsv_in_second_stg_obj, - "Second stage objective contains variable(s) " f"{fsv_in_second_stg_obj}", - ) - - class testMasterFeasibilityUnitConsistency(unittest.TestCase): """ Test cases for models with unit-laden model components. diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 5d23774a873..2ca4c0be71e 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -2663,59 +2663,6 @@ def enforce_dr_degree(blk, config, degree): dr_var.unfix() -def identify_objective_functions(model, objective): - """ - Identify the first and second-stage portions of an Objective - expression, subject to user-provided variable partitioning and - uncertain parameter choice. In doing so, the first and second-stage - objective expressions are added to the model as `Expression` - attributes. - - Parameters - ---------- - model : ConcreteModel - Model of interest. - objective : Objective - Objective to be resolved into first and second-stage parts. - """ - expr_to_split = objective.expr - - has_args = hasattr(expr_to_split, "args") - is_sum = isinstance(expr_to_split, SumExpression) - - # determine additive terms of the objective expression - # additive terms are in accordance with user declaration - if has_args and is_sum: - obj_args = expr_to_split.args - else: - obj_args = [expr_to_split] - - # initialize first and second-stage summand expressions - first_stage_cost_expr = 0 - second_stage_cost_expr = 0 - - first_stage_var_set = ComponentSet(model.util.first_stage_variables) - uncertain_param_set = ComponentSet(model.util.uncertain_params) - - for term in obj_args: - non_first_stage_vars_in_term = ComponentSet( - v for v in identify_variables(term) if v not in first_stage_var_set - ) - uncertain_params_in_term = ComponentSet( - param - for param in identify_mutable_parameters(term) - if param in uncertain_param_set - ) - - if non_first_stage_vars_in_term or uncertain_params_in_term: - second_stage_cost_expr += term - else: - first_stage_cost_expr += term - - model.first_stage_objective = Expression(expr=first_stage_cost_expr) - model.second_stage_objective = Expression(expr=second_stage_cost_expr) - - def load_final_solution( model_data, master_soln, From d6b4e9cf834d605920e110986d30aca8f373f68e Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 16:05:23 -0400 Subject: [PATCH 1883/3044] Remove unused selective cloning method --- pyomo/contrib/pyros/master_problem_methods.py | 5 +- pyomo/contrib/pyros/tests/test_grcs.py | 63 ------------------- pyomo/contrib/pyros/util.py | 16 ----- 3 files changed, 1 insertion(+), 83 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index cade881e2ab..0d79eb6fd69 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -26,7 +26,6 @@ Constraint, ) from pyomo.core.base.set_types import NonNegativeIntegers, NonNegativeReals -from pyomo.core.expr.visitor import replace_expressions from pyomo.core.expr import value from pyomo.opt import ( check_optimal_termination, @@ -35,17 +34,15 @@ ) from pyomo.repn.standard_repn import generate_standard_repn -from pyomo.contrib.pyros.solve_data import MasterProblemData, MasterResult +from pyomo.contrib.pyros.solve_data import MasterResult from pyomo.contrib.pyros.util import ( call_solver, enforce_dr_degree, get_dr_expression, - get_main_elapsed_time, check_time_limit_reached, ObjectiveType, process_termination_condition_master_problem, pyrosTerminationCondition, - selective_clone, TIC_TOC_SOLVE_TIME_ATTR, ) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 2971f8ec649..507bbbff6dd 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -64,7 +64,6 @@ IterationLogRecord, ObjectiveType, pyrosTerminationCondition, - selective_clone, ) logger = logging.getLogger(__name__) @@ -180,68 +179,6 @@ def solve(self, model, **kwargs): return results -# === util.py -class testSelectiveClone(unittest.TestCase): - ''' - Testing for the selective_clone function. This function takes as input a Pyomo model object - and a list of variables objects "first_stage_vars" in that Pyomo model which should *not* be cloned. - It returns a clone of the original Pyomo model object wherein the "first_stage_vars" members are unchanged, - i.e. all cloned model expressions still reference the "first_stage_vars" of the original model object. - ''' - - def test_cloning_negative_case(self): - ''' - Testing correct behavior if incorrect first_stage_vars list object is passed to selective_clone - ''' - m = ConcreteModel() - m.x = Var(initialize=2) - m.y = Var(initialize=2) - m.p = Param(initialize=1) - m.con = Constraint(expr=m.x * m.p + m.y <= 0) - - n = ConcreteModel() - n.x = Var() - m.first_stage_vars = [n.x] - - cloned_model = selective_clone(block=m, first_stage_vars=m.first_stage_vars) - - self.assertNotEqual( - id(m.first_stage_vars), - id(cloned_model.first_stage_vars), - msg="First stage variables should not be equal.", - ) - - def test_cloning_positive_case(self): - ''' - Testing if selective_clone works correctly for correct first_stage_var object definition. - ''' - m = ConcreteModel() - m.x = Var(initialize=2) - m.y = Var(initialize=2) - m.p = Param(initialize=1) - m.con = Constraint(expr=m.x * m.p + m.y <= 0) - m.first_stage_vars = [m.x] - - cloned_model = selective_clone(block=m, first_stage_vars=m.first_stage_vars) - - self.assertEqual( - id(m.x), id(cloned_model.x), msg="First stage variables should be equal." - ) - self.assertNotEqual( - id(m.y), - id(cloned_model.y), - msg="Non-first-stage variables should not be equal.", - ) - self.assertNotEqual( - id(m.p), id(cloned_model.p), msg="Params should not be equal." - ) - self.assertNotEqual( - id(m.con), - id(cloned_model.con), - msg="Constraint objects should not be equal.", - ) - - class TestPyROSSolveFactorModelSet(unittest.TestCase): """ Test PyROS successfully solves model with factor model uncertainty. diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 2ca4c0be71e..f3cb340b485 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -2490,22 +2490,6 @@ def log_model_statistics(model_data, config): info_log_func(f" Performance inequalities : {num_performance_ineq_cons}") -def selective_clone(block, first_stage_vars): - """ - Clone everything in a base_model except for the first-stage variables - :param block: the block of the model to be clones - :param first_stage_vars: the variables which should not be cloned - :return: - """ - memo = {'__block_scope__': {id(block): True, id(None): False}} - for v in first_stage_vars: - memo[id(v)] = v - new_block = copy.deepcopy(block, memo) - new_block._parent = None - - return new_block - - def new_add_decision_rule_variables(model_data, config): """ Add variables parameterizing the (polynomial) From cc476cfc86eded9f80d902a1a557b2e58c9798de Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 16:17:14 -0400 Subject: [PATCH 1884/3044] Name uncertainty modeling components more carefully --- pyomo/contrib/pyros/uncertainty_sets.py | 26 +++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index a4e65d8d4c1..5746326f900 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -64,6 +64,7 @@ from enum import Enum from pyomo.common.dependencies import numpy as np, scipy as sp +from pyomo.common.modeling import unique_component_name from pyomo.core.base import ( Block, ConstraintList, @@ -121,20 +122,30 @@ def _setup_standard_uncertainty_set_constraint_block( block = Block(concrete=True) if uncertain_param_vars is None: - block.uncertain_param_indexed_var = Var(range(dim)) - param_var_data_list = list(block.uncertain_param_indexed_var.values()) + uncertain_param_indexed_var = Var(range(dim)) + block.add_component( + unique_component_name(block, "uncertain_param_indexed_var"), + uncertain_param_indexed_var, + ) + param_var_data_list = list(uncertain_param_indexed_var.values()) else: # resolve arguments param_var_data_list = standardize_uncertain_param_vars( uncertain_param_vars, dim=dim, ) - block.uncertainty_set_conlist = conlist = ConstraintList() + conlist = ConstraintList() + block.add_component( + unique_component_name(block, "uncertainty_set_conlist"), conlist + ) auxiliary_var_list = [] if num_auxiliary_vars is not None: - block.auxiliary_param_var = Var(range(num_auxiliary_vars)) - auxiliary_var_list = list(block.auxiliary_param_var.values()) + auxiliary_param_var = Var(range(num_auxiliary_vars)) + block.add_component( + unique_component_name(block, "auxiliary_param_var"), auxiliary_param_var + ) + auxiliary_var_list = list(auxiliary_param_var.values()) return block, param_var_data_list, conlist, auxiliary_var_list @@ -2917,7 +2928,10 @@ def set_as_constraint(self, uncertain_params=None, block=None): all_cons, all_aux_vars = [], [] for idx, unc_set in enumerate(intersection_set.all_sets): sub_block = Block() - block.add_component(f"sub_block_{idx}", sub_block) + block.add_component( + unique_component_name(block, f"sub_block_{idx}"), + sub_block, + ) set_quantification = unc_set.set_as_constraint( block=sub_block, uncertain_params=param_var_data_list, From 3e8c5e0039a3738804f4228e85a42880ec207c92 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 11 Jul 2024 16:21:50 -0400 Subject: [PATCH 1885/3044] Wrote solving tests Covering all different forms of DoE model (i.e., trace, det; central, forward, backward; full factorial FIM; compute FIM). --- pyomo/contrib/doe/doe.py | 16 +- .../doe/redesign/experiment_class_example.py | 29 -- pyomo/contrib/doe/redesign/test_build.py | 51 +--- .../doe/tests/experiment_class_example.py | 249 +++++++++++++++++ pyomo/contrib/doe/tests/result.json | 1 + pyomo/contrib/doe/tests/test_doe_solve.py | 252 ++++++++++++++++++ 6 files changed, 526 insertions(+), 72 deletions(-) create mode 100644 pyomo/contrib/doe/tests/experiment_class_example.py create mode 100644 pyomo/contrib/doe/tests/result.json create mode 100644 pyomo/contrib/doe/tests/test_doe_solve.py diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index fafff8f1954..70f08ffc1ff 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -26,6 +26,7 @@ # ___________________________________________________________________________ import pyomo.environ as pyo +from pyomo.opt import SolverStatus from pyomo.common import DeveloperError from pyomo.common.timing import TicTocTimer from pyomo.contrib.sensitivity_toolbox.sens import get_dsdp @@ -301,8 +302,8 @@ def run_doe(self, model=None, results_file=None): model.L[c, d].value = L_vals_sq[i, j] # Solve the full model, which has now been initialized with the square solve - self.solver.solve(model, tee=self.tee) - + res = self.solver.solve(model, tee=self.tee) + # Track time used to solve the DoE model solve_time = sp_timer.toc(msg=None) @@ -319,6 +320,9 @@ def run_doe(self, model=None, results_file=None): # Make sure stale results don't follow the DoE object instance self.results = {} + + self.results['Solver Status'] = res.solver.status + self.results['Termination Condition'] = res.solver.termination_condition # Important quantities for optimal design self.results["FIM"] = fim_local @@ -396,6 +400,12 @@ def compute_FIM(self, model=None, method="sequential"): model = self.compute_FIM_model self.check_model_labels(model=model) + + # Set length values for the model features + self.n_parameters = len(model.unknown_parameters) + self.n_measurement_error = len(model.measurement_error) + self.n_experiment_inputs = len(model.experiment_inputs) + self.n_experiment_outputs = len(model.experiment_outputs) # Check FIM input, if it exists. Otherwise, set the prior_FIM attribute if self.prior_FIM is None: @@ -438,6 +448,8 @@ def _sequential_FIM(self, model=None): model = self.compute_FIM_model # Create suffix to keep track of parameter scenarios + if hasattr(model, 'parameter_scenarios'): + model.del_component(model.parameter_scenarios) model.parameter_scenarios = pyo.Suffix( direction=pyo.Suffix.LOCAL, ) diff --git a/pyomo/contrib/doe/redesign/experiment_class_example.py b/pyomo/contrib/doe/redesign/experiment_class_example.py index 7855e32d2fc..eae355ebc89 100644 --- a/pyomo/contrib/doe/redesign/experiment_class_example.py +++ b/pyomo/contrib/doe/redesign/experiment_class_example.py @@ -153,16 +153,10 @@ def finalize_model(self): # Unpacking data before simulation control_points = self.data['control_points'] - # m.CA[0].fix(self.data['CA0']) m.CA[0].value = self.data['CA0'] m.CB[0].fix(self.data['CB0']) - # m.CC[0].fix(self.data['CC0']) m.t.update(self.data['t_range']) m.t.update(control_points) - # m.A1 = self.data['A1'] - # m.A2 = self.data['A2'] - # m.E1 = self.data['E1'] - # m.E2 = self.data['E2'] m.A1.fix(self.data['A1']) m.A2.fix(self.data['A2']) m.E1.fix(self.data['E1']) @@ -171,15 +165,8 @@ def finalize_model(self): m.CA[0].setlb(self.data['CA_bounds'][0]) m.CA[0].setub(self.data['CA_bounds'][1]) - # m.T[0].fix(control_points[0]) - m.t_control = control_points - # TODO: add simulation for initialization????? - # Call the simulator (optional) - # sim = Simulator(m, package='casadi') - # tsim, profiles = sim.simulate(numpoints=100, integrator='idas') - # Discretizing the model discr = pyo.TransformationFactory("dae.collocation") discr.apply_to(m, nfe=self.nfe, ncp=self.ncp, wrt=m.t) @@ -189,13 +176,10 @@ def finalize_model(self): for t in m.t: if t in control_points: cv = control_points[t] - # m.T[t].fix(cv) m.T[t].setlb(self.data['T_bounds'][0]) m.T[t].setub(self.data['T_bounds'][1]) m.T[t] = cv - # m.extra_con = pyo.Constraint(expr=m.T[0.0] == m.T[1.0]) - @m.Constraint(m.t - control_points) def T_control(m, t): """ @@ -263,16 +247,3 @@ def label_experiment(self): """ m = self.model return self.label_experiment_impl([[m.t_control], [[m.t.last()]], [[m.t.last()]]]) - - -f = open('result.json') -data_ex = json.load(f) -data_ex['control_points'] = {float(k): v for k, v in data_ex['control_points'].items()} - -experiments = [ - FullReactorExperiment(data_ex, 10, 3), - PartialReactorExperiment(data_ex, 10, 3), -] - -# in parmest / DoE: -expanded_experiments = [e.get_labeled_model() for e in experiments] \ No newline at end of file diff --git a/pyomo/contrib/doe/redesign/test_build.py b/pyomo/contrib/doe/redesign/test_build.py index 0d1d84b217b..7d3bc398f70 100644 --- a/pyomo/contrib/doe/redesign/test_build.py +++ b/pyomo/contrib/doe/redesign/test_build.py @@ -1,15 +1,15 @@ from experiment_class_example import * from pyomo.contrib.doe import * -from pyomo.contrib.doe import * - -from simple_reaction_example import * import numpy as np import logging +f = open('result.json') +data_ex = json.load(f) +data_ex['control_points'] = {float(k): v for k, v in data_ex['control_points'].items()} + doe_obj = [0, 0, 0, 0,] obj = ['trace', 'det', 'det'] -file_name = ['trash1.json', 'trash2.json', 'trash3.json'] for ind, fd in enumerate(['central', 'backward', 'forward']): experiment = FullReactorExperiment(data_ex, 10, 3) @@ -32,50 +32,19 @@ _only_compute_fim_lower=True, logger_level=logging.INFO, ) - doe_obj[ind].run_doe(results_file=file_name[ind]) - - -ind = 3 - -doe_obj[ind] = DesignOfExperiments( - experiment, - fd_formula='central', - step=1e-3, - objective_option=ObjectiveLib.det, - scale_constant_value=1, - scale_nominal_param_value=(True and (ind != 2)), - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_initial=None, - L_LB=1e-7, - solver=None, - tee=False, - args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - logger_level=logging.INFO, - ) -doe_obj[ind].model.set_blocks = pyo.Set(initialize=[0, 1, 2]) -doe_obj[ind].model.block_instances = pyo.Block(doe_obj[ind].model.set_blocks) -doe_obj[ind].create_doe_model(model=doe_obj[ind].model.block_instances[0]) -doe_obj[ind].create_doe_model(model=doe_obj[ind].model.block_instances[1]) -doe_obj[ind].create_doe_model(model=doe_obj[ind].model.block_instances[2]) - -print('Multi-block build complete') + doe_obj[ind].run_doe() # add a prior information (scaled FIM with T=500 and T=300 experiments) # prior = np.asarray( # [ - # [28.67892806, 5.41249739, -81.73674601, -24.02377324], - # [5.41249739, 26.40935036, -12.41816477, -139.23992532], - # [-81.73674601, -12.41816477, 240.46276004, 58.76422806], - # [-24.02377324, -139.23992532, 58.76422806, 767.25584508], + # [ 1745.81343391 1599.21859987 -3512.47892155 -7589.26220445] + # [ 1599.21859987 3525.63856364 -2900.94638673 -16465.46338508] + # [ -3512.47892155 -2900.94638673 7190.13048958 13849.96839993] + # [ -7589.26220445 -16465.46338508 13849.96839993 77674.03976715]] # ] # ) prior = None - design_ranges = { 'CA[0]': [1, 5, 3], 'T[0]': [300, 700, 3], @@ -88,7 +57,6 @@ print(doe_obj[0].kaug_FIM) - # Optimal values print("Optimal values for determinant optimized experimental design:") print("New formulation, scaled: {}".format(pyo.value(doe_obj[1].model.objective))) @@ -121,6 +89,7 @@ print("Results from using compute FIM (first old, then new)") print(doe_obj[0].kaug_FIM) print(doe_obj[0].seq_FIM) +print(np.isclose(doe_obj[0].kaug_FIM, doe_obj[0].seq_FIM, 1e-2)) print(np.log10(np.linalg.det(doe_obj[0].kaug_FIM))) print(np.log10(np.linalg.det(doe_obj[0].seq_FIM))) A = doe_obj[0].kaug_jac diff --git a/pyomo/contrib/doe/tests/experiment_class_example.py b/pyomo/contrib/doe/tests/experiment_class_example.py new file mode 100644 index 00000000000..eae355ebc89 --- /dev/null +++ b/pyomo/contrib/doe/tests/experiment_class_example.py @@ -0,0 +1,249 @@ +# === Required imports === +import pyomo.environ as pyo +from pyomo.dae import ContinuousSet, DerivativeVar, Simulator + +import itertools +import json +# ======================== + + +def expand_model_components(m, base_components, index_sets): + """ + Takes model components and index sets and returns the + model component labels. + + Arguments + --------- + m: Pyomo model + base_components: list of variables from model 'm' + index_sets: list, same length as base_components, where each + element is a list of index sets, or None + """ + for val, indexes in itertools.zip_longest(base_components, index_sets): + # If the variable has no index, + # add just the model component + if not val.is_indexed(): + yield val + # If the component is indexed but no + # index supplied, add all indices + elif indexes is None: + yield from val.values() + else: + for j in itertools.product(*indexes): + yield val[j] + + +class Experiment(object): + def __init__(self): + self.model = None + + def get_labeled_model(self): + raise NotImplementedError( + "Derived experiment class failed to implement get_labeled_model" + ) + + +class ReactorExperiment(object): + def __init__(self, data, nfe, ncp): + self.data = data + self.nfe = nfe + self.ncp = ncp + self.model = None + + def get_labeled_model(self): + if self.model is None: + self.create_model() + self.finalize_model() + self.label_experiment() + return self.model + + def create_model(self): + """ + This is an example user model provided to DoE library. + It is a dynamic problem solved by Pyomo.DAE. + + Return + ------ + m: a Pyomo.DAE model + """ + + m = self.model = pyo.ConcreteModel() + + # Model parameters + m.R = pyo.Param(mutable=False, initialize=8.314) + + # m.A1 = pyo.Param(mutable=True) + # m.E1 = pyo.Param(mutable=True) + # m.A2 = pyo.Param(mutable=True) + # m.E2 = pyo.Param(mutable=True) + + # Define model variables + ######################## + # time + m.t = ContinuousSet(bounds=[0, 1]) + + # Concentrations + m.CA = pyo.Var(m.t, within=pyo.NonNegativeReals) + m.CB = pyo.Var(m.t, within=pyo.NonNegativeReals) + m.CC = pyo.Var(m.t, within=pyo.NonNegativeReals) + + # Temperature + m.T = pyo.Var(m.t, within=pyo.NonNegativeReals) + + # Arrhenius rate law equations + m.A1 = pyo.Var(within=pyo.NonNegativeReals) + m.E1 = pyo.Var(within=pyo.NonNegativeReals) + m.A2 = pyo.Var(within=pyo.NonNegativeReals) + m.E2 = pyo.Var(within=pyo.NonNegativeReals) + + # Differential variables (Conc.) + m.dCAdt = DerivativeVar(m.CA, wrt=m.t) + m.dCBdt = DerivativeVar(m.CB, wrt=m.t) + + ######################## + # End variable def. + + # Equation def'n + ######################## + + # Expression for rate constants + @m.Expression(m.t) + def k1(m, t): + return m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t])) + + @m.Expression(m.t) + def k2(m, t): + return m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t])) + + # Concentration odes + @m.Constraint(m.t) + def CA_rxn_ode(m, t): + return m.dCAdt[t] == -m.k1[t] * m.CA[t] + + @m.Constraint(m.t) + def CB_rxn_ode(m, t): + return m.dCBdt[t] == m.k1[t] * m.CA[t] - m.k2[t] * m.CB[t] + + # algebraic balance for concentration of C + # Valid because the reaction system (A --> B --> C) is equimolar + @m.Constraint(m.t) + def CC_balance(m, t): + return m.CA[0] == m.CA[t] + m.CB[t] + m.CC[t] + + ######################## + # End equation def'n + + def finalize_model(self): + """ + Example finalize model function. There are two main tasks + here: + 1. Extracting useful information for the model to align + with the experiment. (Here: CA0, t_final, t_control) + 2. Discretizing the model subject to this information. + + Arguments + --------- + m: Pyomo model + data: object containing vital experimental information + nfe: number of finite elements + ncp: number of collocation points for the finite elements + """ + m = self.model + + # Unpacking data before simulation + control_points = self.data['control_points'] + + m.CA[0].value = self.data['CA0'] + m.CB[0].fix(self.data['CB0']) + m.t.update(self.data['t_range']) + m.t.update(control_points) + m.A1.fix(self.data['A1']) + m.A2.fix(self.data['A2']) + m.E1.fix(self.data['E1']) + m.E2.fix(self.data['E2']) + + m.CA[0].setlb(self.data['CA_bounds'][0]) + m.CA[0].setub(self.data['CA_bounds'][1]) + + m.t_control = control_points + + # Discretizing the model + discr = pyo.TransformationFactory("dae.collocation") + discr.apply_to(m, nfe=self.nfe, ncp=self.ncp, wrt=m.t) + + # Initializing Temperature in the model + cv = None + for t in m.t: + if t in control_points: + cv = control_points[t] + m.T[t].setlb(self.data['T_bounds'][0]) + m.T[t].setub(self.data['T_bounds'][1]) + m.T[t] = cv + + @m.Constraint(m.t - control_points) + def T_control(m, t): + """ + Piecewise constant Temperature between control points + """ + neighbour_t = max(tc for tc in control_points if tc < t) + return m.T[t] == m.T[neighbour_t] + + # sim.initialize_model() + + + def label_experiment_impl(self, index_sets_meas): + """ + Example for annotating (labeling) the model with a + full experiment. + + Arguments + --------- + + """ + m = self.model + + # Grab measurement labels + base_comp_meas = [m.CA, m.CB, m.CC] + m.experiment_outputs = pyo.Suffix( + direction=pyo.Suffix.LOCAL, + ) + m.experiment_outputs.update((k, None) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) + + # Adding no error for measurements currently + m.measurement_error = pyo.Suffix( + direction=pyo.Suffix.LOCAL, + ) + m.measurement_error.update((k, 1e-2) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) + + # Grab design variables + base_comp_des = [m.CA, m.T] + index_sets_des = [[[m.t.first()]], [m.t_control]] + m.experiment_inputs = pyo.Suffix( + direction=pyo.Suffix.LOCAL, + ) + m.experiment_inputs.update((k, pyo.ComponentUID(k)) for k in expand_model_components(m, base_comp_des, index_sets_des)) + + m.unknown_parameters = pyo.Suffix( + direction=pyo.Suffix.LOCAL, + ) + m.unknown_parameters.update((k, pyo.value(k)) for k in [m.A1, m.A2, m.E1, m.E2]) + + +class FullReactorExperiment(ReactorExperiment): + def label_experiment(self): + m = self.model + return self.label_experiment_impl([[m.t_control], [m.t_control], [m.t_control]]) + + +class PartialReactorExperiment(ReactorExperiment): + def label_experiment(self): + """ + Example for annotating (labeling) the model with a + "partial" experiment. + + Arguments + --------- + + """ + m = self.model + return self.label_experiment_impl([[m.t_control], [[m.t.last()]], [[m.t.last()]]]) diff --git a/pyomo/contrib/doe/tests/result.json b/pyomo/contrib/doe/tests/result.json new file mode 100644 index 00000000000..7e1b1a79a1b --- /dev/null +++ b/pyomo/contrib/doe/tests/result.json @@ -0,0 +1 @@ +{"CA0": 5.0, "CA_bounds": [1.0, 5.0], "CB0": 0.0, "CC0": 0.0, "t_range": [0, 1], "control_points": {"0": 500, "0.125": 300, "0.25": 300, "0.375": 300, "0.5": 300, "0.625": 300, "0.75": 300, "0.875": 300, "1": 300}, "T_bounds": [300, 700], "A1": 84.79, "A2": 371.72, "E1": 7.78, "E2": 15.05} \ No newline at end of file diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py new file mode 100644 index 00000000000..4102727fce5 --- /dev/null +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -0,0 +1,252 @@ +from pyomo.common.dependencies import ( + numpy as np, + numpy_available, + pandas as pd, + pandas_available, +) + +from experiment_class_example import * +from pyomo.contrib.doe import * + + +import pyomo.common.unittest as unittest + +from pyomo.opt import SolverFactory + +ipopt_available = SolverFactory("ipopt").available() + +f = open('result.json') +data_ex = json.load(f) +data_ex['control_points'] = {float(k): v for k, v in data_ex['control_points'].items()} + +class TestReactorExamples(unittest.TestCase): + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_fd_central_solve(self): + fd_method = "central" + obj_used = "trace" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.run_doe() + + assert (doe_obj.results['Solver Status'] == "ok") + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_fd_forward_solve(self): + fd_method = "forward" + obj_used = "trace" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.run_doe() + + assert (doe_obj.results['Solver Status'] == "ok") + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_fd_backward_solve(self): + fd_method = "backward" + obj_used = "trace" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.run_doe() + + assert (doe_obj.results['Solver Status'] == "ok") + + # TODO: Fix determinant objective code, something is awry + # Should only be using Cholesky=True + # @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + # @unittest.skipIf(not numpy_available, "Numpy is not available") + # def test_reactor_obj_det_solve(self): + # fd_method = "central" + # obj_used = "det" + + # experiment = FullReactorExperiment(data_ex, 10, 3) + + # doe_obj = DesignOfExperiments( + # experiment, + # fd_formula=fd_method, + # step=1e-3, + # objective_option=obj_used, + # scale_constant_value=1, + # scale_nominal_param_value=True, + # prior_FIM=None, + # jac_initial=None, + # fim_initial=None, + # L_initial=None, + # L_LB=1e-7, + # solver=None, + # tee=False, + # args=None, + # _Cholesky_option=False, + # _only_compute_fim_lower=False, + # ) + + # doe_obj.run_doe() + + # assert (doe_obj.results['Solver Status'] == "ok") + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_obj_cholesky_solve(self): + fd_method = "central" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.run_doe() + + assert (doe_obj.results['Solver Status'] == "ok") + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_compute_FIM_seq(self): + fd_method = "central" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + design_ranges = { + 'CA[0]': [1, 5, 3], + 'T[0]': [300, 700, 3], + } + + doe_obj.compute_FIM(method='sequential') + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not pandas_available, "pandas is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_grid_search(self): + fd_method = "central" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + design_ranges = { + 'CA[0]': [1, 5, 3], + 'T[0]': [300, 700, 3], + } + + doe_obj.compute_FIM_full_factorial(design_ranges=design_ranges, method='sequential') + + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From 528506e13c1807b06640fbac5f99f6a0c464db97 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 16:31:14 -0400 Subject: [PATCH 1886/3044] Unify logging of master and DR polishing results --- pyomo/contrib/pyros/master_problem_methods.py | 36 +++---------------- 1 file changed, 5 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 0d79eb6fd69..63e8162cebc 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -574,31 +574,7 @@ def minimize_dr_vars(master_data, config): mvar.set_value(value(pvar), skip_validation=True) config.progress_logger.debug(f" Optimized DR norm: {value(polishing_obj)}") - config.progress_logger.debug(" Polished master objective:") - - # print breakdown of objective value of polished master solution - if config.objective_focus == ObjectiveType.worst_case: - eval_obj_blk_idx = max( - master_data.master_model.scenarios.keys(), - key=lambda idx: value( - master_data.master_model.scenarios[idx].second_stage_objective - ), - ) - else: - eval_obj_blk_idx = (0, 0) - - # debugging: summarize objective breakdown - eval_obj_blk = master_data.master_model.scenarios[eval_obj_blk_idx] - config.progress_logger.debug( - " First-stage objective: " f"{value(eval_obj_blk.first_stage_objective)}" - ) - config.progress_logger.debug( - " Second-stage objective: " f"{value(eval_obj_blk.second_stage_objective)}" - ) - polished_master_obj = value( - eval_obj_blk.first_stage_objective + eval_obj_blk.second_stage_objective - ) - config.progress_logger.debug(f" Objective: {polished_master_obj}") + log_master_solve_results(polishing_model, config, results, desc="polished") return results, True @@ -684,7 +660,7 @@ def higher_order_decision_rule_efficiency(master_data, config): ) -def log_master_solve_results(master_model, config, results): +def log_master_solve_results(master_model, config, results, desc="Optimized"): """ Log master problem solve results. """ @@ -699,17 +675,15 @@ def log_master_solve_results(master_model, config, results): eval_obj_blk_idx = (0, 0) eval_obj_blk = master_model.scenarios[eval_obj_blk_idx] - config.progress_logger.debug(" Optimized master objective breakdown:") + config.progress_logger.debug(f" {desc.capitalize()} master objective breakdown:") config.progress_logger.debug( f" First-stage objective: {value(eval_obj_blk.first_stage_objective)}" ) config.progress_logger.debug( f" Second-stage objective: {value(eval_obj_blk.second_stage_objective)}" ) - master_obj = ( - eval_obj_blk.first_stage_objective + eval_obj_blk.second_stage_objective - ) - config.progress_logger.debug(f" Objective: {value(master_obj)}") + master_obj = eval_obj_blk.full_objective + config.progress_logger.debug(f" Overall Objective: {value(master_obj)}") config.progress_logger.debug( f" Termination condition: {results.solver.termination_condition}" ) From a09e701540e79514ee5b5fa4767fa8b54b497fae Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 16:32:45 -0400 Subject: [PATCH 1887/3044] Remove unused imports --- pyomo/contrib/pyros/util.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index f3cb340b485..2af82de2fd1 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -16,7 +16,6 @@ from collections import namedtuple from collections.abc import Iterable from contextlib import contextmanager -import copy from enum import Enum, auto import functools import itertools as it @@ -44,11 +43,7 @@ Var, value, ) -from pyomo.core.expr.numeric_expr import ( - NPV_MaxExpression, - NPV_MinExpression, - SumExpression, -) +from pyomo.core.expr.numeric_expr import SumExpression from pyomo.core.expr.numvalue import native_types from pyomo.core.expr.visitor import ( identify_variables, From 6d62eea9f98aada00d6a71ca1ff34b728e1a3fb1 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 16:53:04 -0400 Subject: [PATCH 1888/3044] Add tests for backup solver mechanism --- pyomo/contrib/pyros/tests/test_grcs.py | 61 ++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 507bbbff6dd..e84bbbc1d2d 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -724,6 +724,67 @@ def test_terminate_with_time_limit(self): ), ) + @unittest.skipUnless(baron_license_is_valid, "BARON not available and licensed") + def test_pyros_backup_solvers(self): + m = ConcreteModel() + m.name = "s381" + + class BadSolver: + def __init__(self, max_num_calls): + self.max_num_calls = max_num_calls + self.num_calls = 0 + + def available(self, exception_flag=True): + return True + + def solve(self, *args, **kwargs): + if self.num_calls < self.max_num_calls: + self.num_calls += 1 + return SolverFactory("baron").solve(*args, **kwargs) + res = SolverResults() + res.solver.termination_condition = TerminationCondition.maxIterations + res.solver.status = SolverStatus.warning + return res + + m.x1 = Var(within=Reals, bounds=(0, None), initialize=0.1) + m.x2 = Var(within=Reals, bounds=(0, None), initialize=0.1) + m.x3 = Var(within=Reals, bounds=(0, None), initialize=0.1) + + # === State Vars = [x13] + # === Decision Vars === + m.decision_vars = [m.x1, m.x2, m.x3] + + # === Uncertain Params === + m.set_params = Set(initialize=list(range(4))) + m.p = Param(m.set_params, initialize=2, mutable=True) + m.uncertain_params = [m.p] + + m.obj = Objective(expr=(m.x1 - 1) * 2, sense=minimize) + m.con1 = Constraint(expr=m.p[1] * m.x1 + m.x2 + m.x3 <= 2) + + box_set = BoxSet(bounds=[(1.8, 2.2)]) + pyros = SolverFactory("pyros") + results = pyros.solve( + model=m, + first_stage_variables=m.decision_vars, + second_stage_variables=[], + uncertain_params=[m.p[1]], + uncertainty_set=box_set, + # note: allow 4 calls to work normally + # to permit successful solution of uncertainty + # bounding problems + local_solver=BadSolver(4), + global_solver=BadSolver(4), + backup_local_solvers=[SolverFactory("baron")], + backup_global_solvers=[SolverFactory("baron")], + options={"objective_focus": ObjectiveType.nominal}, + solve_master_globally=True, + ) + self.assertTrue( + results.pyros_termination_condition, + pyrosTerminationCondition.robust_feasible, + ) + @unittest.skipUnless( SolverFactory('baron').license_is_valid(), "Global NLP solver is not available and licensed.", From 2d26caff25f62190b3a04e06e29aa0d2585c5ae8 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 16:56:45 -0400 Subject: [PATCH 1889/3044] Fix naming of missed regression tests --- pyomo/contrib/pyros/tests/test_grcs.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index e84bbbc1d2d..7a357b71b0e 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -429,8 +429,11 @@ def test_two_stg_model_discrete_set(self): # === regression test for the solver @unittest.skipUnless(baron_available, "Global NLP solver is not available.") class RegressionTest(unittest.TestCase): - def regression_test_constant_drs(self): - model = m = ConcreteModel() + @unittest.skipUnless( + baron_license_is_valid, "Global NLP solver is not available and licensed." + ) + def test_regression_constant_drs(self): + m = ConcreteModel() m.name = "s381" m.x1 = Var(within=Reals, bounds=(0, None), initialize=0.1) @@ -467,8 +470,11 @@ def regression_test_constant_drs(self): pyrosTerminationCondition.robust_feasible, ) - def regression_test_affine_drs(self): - model = m = ConcreteModel() + @unittest.skipUnless( + baron_license_is_valid, "Global NLP solver is not available and licensed." + ) + def test_regression_affine_drs(self): + m = ConcreteModel() m.name = "s381" m.x1 = Var(within=Reals, bounds=(0, None), initialize=0.1) @@ -508,8 +514,11 @@ def regression_test_affine_drs(self): pyrosTerminationCondition.robust_feasible, ) - def regression_test_quad_drs(self): - model = m = ConcreteModel() + @unittest.skipUnless( + baron_license_is_valid, "Global NLP solver is not available and licensed." + ) + def test_regression_quadratic_drs(self): + m = ConcreteModel() m.name = "s381" m.x1 = Var(within=Reals, bounds=(0, None), initialize=0.1) From 53384ae385e5c3a5db9e74cc061489252fa6c795 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 11 Jul 2024 17:33:01 -0400 Subject: [PATCH 1890/3044] Adding placeholder files for other tests --- pyomo/contrib/doe/tests/test_doe_build.py | 0 pyomo/contrib/doe/tests/test_doe_errors.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 pyomo/contrib/doe/tests/test_doe_build.py create mode 100644 pyomo/contrib/doe/tests/test_doe_errors.py diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py new file mode 100644 index 00000000000..e69de29bb2d From cf52e5d8093f6e31826625718f6e59871f5986b1 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 19:08:54 -0400 Subject: [PATCH 1891/3044] Deactivate DR polishing constraints in fixed Vars --- pyomo/contrib/pyros/master_problem_methods.py | 20 ++++++++++++++++++- pyomo/contrib/pyros/tests/test_master.py | 8 ++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 63e8162cebc..63f3692bbeb 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -26,7 +26,7 @@ Constraint, ) from pyomo.core.base.set_types import NonNegativeIntegers, NonNegativeReals -from pyomo.core.expr import value +from pyomo.core.expr import identify_variables, value from pyomo.opt import ( check_optimal_termination, SolverResults, @@ -40,6 +40,7 @@ enforce_dr_degree, get_dr_expression, check_time_limit_reached, + generate_all_decision_rule_var_data_objects, ObjectiveType, process_termination_condition_master_problem, pyrosTerminationCondition, @@ -386,6 +387,23 @@ def new_construct_dr_polishing_problem(master_data, config): for var in nondr_nonadjustable_vars: var.fix() + # deactivate original constraints that involved + # only vars that have been fixed. + # we do this mostly to ensure the number of active + # equality constraints does not outnumber the number of + # unfixed Vars + fixed_dr_vars = [ + var + for var in generate_all_decision_rule_var_data_objects(nominal_polishing_block) + if var.fixed + ] + fixed_nonadjustable_vars = ComponentSet(nondr_nonadjustable_vars + fixed_dr_vars) + for blk in polishing_model.scenarios.values(): + for con in blk.component_data_objects(Constraint, active=True): + vars_in_con = ComponentSet(identify_variables(con.body)) + if not (vars_in_con - fixed_nonadjustable_vars): + con.deactivate() + # we will add the polishing objective later polishing_model.epigraph_obj.deactivate() diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index 3a14fdfac72..debbb5b9d9b 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -370,6 +370,14 @@ def test_construct_dr_polishing_problem_nonadj_components(self): self.assertFalse(nom_polishing_block.decision_rule_vars[0][0].fixed) self.assertFalse(nom_polishing_block.decision_rule_vars[0][1].fixed) + # ensure constraints in fixed vars were deactivated + self.assertFalse(nom_polishing_block.user_model.eq_con.active) + + # these have either unfixed DR or adjustable variables, + # so they should remain active + self.assertTrue(nom_polishing_block.user_model.con.active) + self.assertTrue(nom_polishing_block.decision_rule_eqns[0].active) + def test_construct_dr_polishing_problem_polishing_components(self): """ Test auxiliary Var/Constraint components of the DR polishing From ba3fa3bea0b41528bf53e74c57fa4f757e87a6cb Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 19:17:35 -0400 Subject: [PATCH 1892/3044] Simplify names of revised methods and classes --- pyomo/contrib/pyros/master_problem_methods.py | 10 ++--- pyomo/contrib/pyros/pyros.py | 4 +- .../contrib/pyros/pyros_algorithm_methods.py | 4 +- .../pyros/separation_problem_methods.py | 2 +- pyomo/contrib/pyros/tests/test_master.py | 30 +++++++-------- .../contrib/pyros/tests/test_preprocessor.py | 38 +++++++++---------- pyomo/contrib/pyros/tests/test_separation.py | 4 +- pyomo/contrib/pyros/util.py | 10 ++--- 8 files changed, 51 insertions(+), 51 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 63f3692bbeb..fcffada53e4 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -156,7 +156,7 @@ def add_scenario_block_to_master_problem( con.deactivate() -def new_construct_master_feasibility_problem(master_data, config): +def construct_master_feasibility_problem(master_data, config): """ Construct slack variable minimization problem from the master model. @@ -287,7 +287,7 @@ def solve_master_feasibility_problem(master_data, config): results : SolverResults Solver results. """ - model = new_construct_master_feasibility_problem(master_data, config) + model = construct_master_feasibility_problem(master_data, config) active_obj = next(model.component_data_objects(Objective, active=True)) @@ -348,7 +348,7 @@ def solve_master_feasibility_problem(master_data, config): return results -def new_construct_dr_polishing_problem(master_data, config): +def construct_dr_polishing_problem(master_data, config): """ Construct DR polishing problem from the master problem. @@ -517,7 +517,7 @@ def minimize_dr_vars(master_data, config): False otherwise. """ # create polishing NLP - polishing_model = new_construct_dr_polishing_problem(master_data, config) + polishing_model = construct_dr_polishing_problem(master_data, config) if config.solve_master_globally: solver = config.global_solver @@ -904,7 +904,7 @@ def solve_master(master_data, config): ) -class NewMasterProblemData: +class MasterProblemData: """ Container for objects pertaining to the PyROS master problem. """ diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index e65608bd781..c87f64f65ff 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -23,7 +23,7 @@ ObjectiveType, validate_pyros_inputs, log_model_statistics, - new_preprocess_model_data, + preprocess_model_data, IterationLogRecord, setup_pyros_logger, TimingData, @@ -371,7 +371,7 @@ def solve( config.progress_logger.info("Preprocessing...") model_data.timing.start_timer("main.preprocessing") - robust_infeasible = new_preprocess_model_data( + robust_infeasible = preprocess_model_data( model_data, config, user_var_partitioning, diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 7fb6bea5707..5eed6af1098 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -145,8 +145,8 @@ def ROSolver_iterative_solve(model_data, config): ------- ... """ - master_data = mp_methods.NewMasterProblemData(model_data, config) - separation_data = sp_methods.NewSeparationProblemData(model_data, config) + master_data = mp_methods.MasterProblemData(model_data, config) + separation_data = sp_methods.SeparationProblemData(model_data, config) # === Nominal information nominal_data = Block() diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index 1dfe2bb158c..15214efd7da 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -1196,7 +1196,7 @@ def discrete_solve( ) -class NewSeparationProblemData: +class SeparationProblemData: """ Container for objects related to the PyROS separation problem. """ diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index debbb5b9d9b..7b2ba994b93 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -25,12 +25,12 @@ from pyomo.contrib.pyros.master_problem_methods import ( add_scenario_block_to_master_problem, construct_initial_master_problem, - new_construct_master_feasibility_problem, - new_construct_dr_polishing_problem, - NewMasterProblemData, + construct_master_feasibility_problem, + construct_dr_polishing_problem, + MasterProblemData, ) from pyomo.contrib.pyros.util import ( - new_preprocess_model_data, + preprocess_model_data, ObjectiveType, time_code, TimingData, @@ -80,7 +80,7 @@ def build_simple_model_data(objective_focus="worst_case"): state_variables=[], ) - new_preprocess_model_data(model_data, config, user_var_partitioning) + preprocess_model_data(model_data, config, user_var_partitioning) return model_data, config @@ -243,7 +243,7 @@ def test_construct_master_feasibility_problem_var_map(self): Test construction of feasibility problem var map. """ master_data, config = self.build_simple_master_data() - slack_model = new_construct_master_feasibility_problem(master_data, config) + slack_model = construct_master_feasibility_problem(master_data, config) self.assertTrue(master_data.feasibility_problem_varmap) for mvar, feasvar in master_data.feasibility_problem_varmap: @@ -263,7 +263,7 @@ def test_construct_master_feasibility_problem_slack_vars(self): Check master feasibility slack variables. """ master_data, config = self.build_simple_master_data() - slack_model = new_construct_master_feasibility_problem(master_data, config) + slack_model = construct_master_feasibility_problem(master_data, config) slack_var_blk = slack_model._core_add_slack_variables scenario_10_blk = slack_model.scenarios[1, 0] @@ -312,7 +312,7 @@ def test_construct_master_feasibility_problem_obj(self): Check master feasibility slack variables. """ master_data, config = self.build_simple_master_data() - slack_model = new_construct_master_feasibility_problem(master_data, config) + slack_model = construct_master_feasibility_problem(master_data, config) self.assertFalse(slack_model.epigraph_obj.active) self.assertTrue( @@ -348,7 +348,7 @@ def test_construct_dr_polishing_problem_nonadj_components(self): of the DR polishing problem. """ master_data, config = self.build_simple_master_data() - polishing_model = new_construct_dr_polishing_problem(master_data, config) + polishing_model = construct_dr_polishing_problem(master_data, config) eff_first_stage_vars = ( polishing_model .scenarios[0, 0] @@ -387,7 +387,7 @@ def test_construct_dr_polishing_problem_polishing_components(self): # DR order is 1, and x3 is second-stage. # to test fixing efficiency, fix the affine DR variable master_data.master_model.scenarios[0, 0].decision_rule_vars[0][1].fix() - polishing_model = new_construct_dr_polishing_problem(master_data, config) + polishing_model = construct_dr_polishing_problem(master_data, config) nom_polishing_block = polishing_model.scenarios[0, 0] self.assertFalse(nom_polishing_block.decision_rule_vars[0][0].fixed) @@ -418,7 +418,7 @@ def test_construct_dr_polishing_problem_objectives(self): polishing model. """ master_data, config = self.build_simple_master_data() - polishing_model = new_construct_dr_polishing_problem(master_data, config) + polishing_model = construct_dr_polishing_problem(master_data, config) self.assertFalse(polishing_model.epigraph_obj.active) self.assertTrue(polishing_model.polishing_obj.active) @@ -439,7 +439,7 @@ def test_solve_master(self): backup_global_solvers=[], tee=False, )) - master_data = NewMasterProblemData(model_data, config) + master_data = MasterProblemData(model_data, config) with time_code(master_data.timing, "main", is_main_timer=True): master_soln = master_data.solve_master() self.assertEqual( @@ -468,7 +468,7 @@ def test_solve_master_timeout_on_master(self): tee=False, time_limit=1, )) - master_data = NewMasterProblemData(model_data, config) + master_data = MasterProblemData(model_data, config) with time_code(master_data.timing, "main", is_main_timer=True): time.sleep(1) master_soln = master_data.solve_master() @@ -502,7 +502,7 @@ def test_solve_master_timeout_on_master_feasibility(self): tee=False, time_limit=1, )) - master_data = NewMasterProblemData(model_data, config) + master_data = MasterProblemData(model_data, config) add_scenario_block_to_master_problem( master_data.master_model, scenario_idx=[1, 0], @@ -542,7 +542,7 @@ def test_polish_dr_vars(self): backup_global_solvers=[], tee=False, )) - master_data = NewMasterProblemData(model_data, config) + master_data = MasterProblemData(model_data, config) add_scenario_block_to_master_problem( master_data.master_model, scenario_idx=[1, 0], diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index df34d127864..b1172812dbc 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -48,12 +48,12 @@ standardize_equality_constraints, standardize_active_objective, declare_objective_expressions, - new_add_decision_rule_constraints, - new_add_decision_rule_variables, + add_decision_rule_constraints, + add_decision_rule_variables, perform_coefficient_matching, setup_working_model, VariablePartitioning, - new_preprocess_model_data, + preprocess_model_data, log_model_statistics, ) parameterized, param_available = attempt_import('parameterized') @@ -1676,7 +1676,7 @@ def test_correct_num_dr_vars_static(self): config = Bunch() config.decision_rule_order = 0 - new_add_decision_rule_variables(model_data=model_data, config=config) + add_decision_rule_variables(model_data=model_data, config=config) for indexed_dr_var in model_data.working_model.decision_rule_vars: self.assertEqual( @@ -1728,7 +1728,7 @@ def test_correct_num_dr_vars_affine(self): config = Bunch() config.decision_rule_order = 1 - new_add_decision_rule_variables(model_data=model_data, config=config) + add_decision_rule_variables(model_data=model_data, config=config) for indexed_dr_var in model_data.working_model.decision_rule_vars: self.assertEqual( @@ -1780,7 +1780,7 @@ def test_correct_num_dr_vars_quadratic(self): config = Bunch() config.decision_rule_order = 2 - new_add_decision_rule_variables(model_data=model_data, config=config) + add_decision_rule_variables(model_data=model_data, config=config) num_params = len(model_data.working_model.uncertain_params) @@ -1882,8 +1882,8 @@ def test_num_dr_eqns_added_correct(self): config = Bunch() config.decision_rule_order = 0 - new_add_decision_rule_variables(model_data, config) - new_add_decision_rule_constraints(model_data, config) + add_decision_rule_variables(model_data, config) + add_decision_rule_constraints(model_data, config) effective_second_stage_vars = ( model_data.working_model.effective_var_partitioning.second_stage_variables @@ -1936,8 +1936,8 @@ def test_dr_eqns_form_correct(self): config.decision_rule_order = 2 # add DR variables and constraints - new_add_decision_rule_variables(model_data, config) - new_add_decision_rule_constraints(model_data, config) + add_decision_rule_variables(model_data, config) + add_decision_rule_constraints(model_data, config) dr_zip = zip( model_data.working_model.effective_var_partitioning.second_stage_variables, @@ -2101,8 +2101,8 @@ def test_coefficient_matching_nonlinear(self): ) config.progress_logger.setLevel(logging.DEBUG) - new_add_decision_rule_variables(model_data=model_data, config=config) - new_add_decision_rule_constraints(model_data=model_data, config=config) + add_decision_rule_variables(model_data=model_data, config=config) + add_decision_rule_constraints(model_data=model_data, config=config) ep = model_data.working_model.effective_var_partitioning model_data.working_model.all_nonadjustable_variables = list( @@ -2303,7 +2303,7 @@ def test_preprocessor_effective_var_partitioning_static_dr(self): decision_rule_order=0, progress_logger=logger, ) - new_preprocess_model_data( + preprocess_model_data( model_data, config, user_var_partitioning, ) ep = model_data.working_model.effective_var_partitioning @@ -2366,7 +2366,7 @@ def test_preprocessor_effective_var_partitioning_nonstatic_dr(self, name, dr_ord decision_rule_order=dr_order, progress_logger=logger, ) - new_preprocess_model_data( + preprocess_model_data( model_data, config, user_var_partitioning, ) ep = model_data.working_model.effective_var_partitioning @@ -2436,7 +2436,7 @@ def test_preprocessor_constraint_partitioning_nonstatic_dr( decision_rule_order=dr_order, progress_logger=logger, ) - new_preprocess_model_data( + preprocess_model_data( model_data, config, user_var_partitioning, ) @@ -2608,7 +2608,7 @@ def test_preprocessor_coefficient_matching( # static DR, problem should be robust infeasible # due to the coefficient matching constraints derived # from bounds on z5 - robust_infeasible = new_preprocess_model_data( + robust_infeasible = preprocess_model_data( model_data, config, user_var_partitioning, ) self.assertIsInstance(robust_infeasible, bool) @@ -2683,7 +2683,7 @@ def test_preprocessor_objective_standardization(self, name, dr_order): decision_rule_order=dr_order, progress_logger=logger, ) - new_preprocess_model_data( + preprocess_model_data( model_data, config, user_var_partitioning, ) @@ -2735,7 +2735,7 @@ def test_preprocessor_log_model_statistics_affine_dr(self, obj_focus): decision_rule_order=1, progress_logger=logger, ) - new_preprocess_model_data( + preprocess_model_data( model_data, config, user_var_partitioning, ) @@ -2787,7 +2787,7 @@ def test_preprocessor_log_model_statistics_quadratic_dr(self, obj_focus): decision_rule_order=2, progress_logger=logger, ) - new_preprocess_model_data( + preprocess_model_data( model_data, config, user_var_partitioning, ) diff --git a/pyomo/contrib/pyros/tests/test_separation.py b/pyomo/contrib/pyros/tests/test_separation.py index 42b0fe39a64..ce484dfdf9b 100644 --- a/pyomo/contrib/pyros/tests/test_separation.py +++ b/pyomo/contrib/pyros/tests/test_separation.py @@ -21,7 +21,7 @@ from pyomo.contrib.pyros.separation_problem_methods import construct_separation_problem from pyomo.contrib.pyros.uncertainty_sets import BoxSet, FactorModelSet from pyomo.contrib.pyros.util import ( - new_preprocess_model_data, + preprocess_model_data, ObjectiveType, VariablePartitioning, ) @@ -68,7 +68,7 @@ def build_simple_model_data(objective_focus="worst_case"): state_variables=[], ) - new_preprocess_model_data(model_data, config, user_var_partitioning) + preprocess_model_data(model_data, config, user_var_partitioning) return model_data, config diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 2af82de2fd1..d5d757be543 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -2331,7 +2331,7 @@ def perform_coefficient_matching(model_data, config): return False -def new_preprocess_model_data(model_data, config, user_var_partitioning): +def preprocess_model_data(model_data, config, user_var_partitioning): """ Preprocess user inputs to modeling objects from which PyROS subproblems can be efficiently constructed. @@ -2375,8 +2375,8 @@ def new_preprocess_model_data(model_data, config, user_var_partitioning): # DR components are added only per effective second-stage variable config.progress_logger.debug("Adding decision rule components...") - new_add_decision_rule_variables(model_data, config) - new_add_decision_rule_constraints(model_data, config) + add_decision_rule_variables(model_data, config) + add_decision_rule_constraints(model_data, config) # the epigraph and DR variables are also first-stage config.progress_logger.debug("Finalizing nonadjustable variables...") @@ -2485,7 +2485,7 @@ def log_model_statistics(model_data, config): info_log_func(f" Performance inequalities : {num_performance_ineq_cons}") -def new_add_decision_rule_variables(model_data, config): +def add_decision_rule_variables(model_data, config): """ Add variables parameterizing the (polynomial) decision rules to the working model. @@ -2545,7 +2545,7 @@ def new_add_decision_rule_variables(model_data, config): eff_ss_var_to_dr_var_map[eff_ss_var] = indexed_dr_var -def new_add_decision_rule_constraints(model_data, config): +def add_decision_rule_constraints(model_data, config): """ Add decision rule equality constraints to the working model. From 3c6bbd8aab8aed75518a1fa64ef347bac43d8cec Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 19:23:24 -0400 Subject: [PATCH 1893/3044] Remove unused module imports --- pyomo/contrib/pyros/__init__.py | 2 +- pyomo/contrib/pyros/pyros.py | 14 ++++++-------- pyomo/contrib/pyros/pyros_algorithm_methods.py | 12 ++---------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/pyros/__init__.py b/pyomo/contrib/pyros/__init__.py index 4e134ef1166..54f3d1623c6 100644 --- a/pyomo/contrib/pyros/__init__.py +++ b/pyomo/contrib/pyros/__init__.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ from pyomo.contrib.pyros.pyros import PyROS -from pyomo.contrib.pyros.pyros import ObjectiveType, pyrosTerminationCondition +from pyomo.contrib.pyros.util import ObjectiveType, pyrosTerminationCondition from pyomo.contrib.pyros.uncertainty_sets import ( UncertaintySet, EllipsoidalSet, diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index c87f64f65ff..1898e6c19b2 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -10,29 +10,27 @@ # ___________________________________________________________________________ # pyros.py: Generalized Robust Cutting-Set Algorithm for Pyomo +from datetime import datetime import logging + from pyomo.common.config import document_kwargs_from_configdict -from pyomo.core.base.block import Block from pyomo.core.expr import value -from pyomo.contrib.pyros.util import time_code from pyomo.opt import SolverFactory + from pyomo.contrib.pyros.config import pyros_config, logger_domain +from pyomo.contrib.pyros.pyros_algorithm_methods import ROSolver_iterative_solve +from pyomo.contrib.pyros.solve_data import ROSolveResults from pyomo.contrib.pyros.util import ( load_final_solution, pyrosTerminationCondition, - ObjectiveType, validate_pyros_inputs, log_model_statistics, preprocess_model_data, IterationLogRecord, setup_pyros_logger, + time_code, TimingData, ) -from pyomo.contrib.pyros.solve_data import ROSolveResults -from pyomo.contrib.pyros.pyros_algorithm_methods import ROSolver_iterative_solve -from pyomo.core.base import Constraint - -from datetime import datetime __version__ = "1.2.11" diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 5eed6af1098..b6f34070419 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -13,31 +13,23 @@ Methods for execution of the main PyROS cutting set algorithm. """ -from itertools import chain - from pyomo.common.dependencies import numpy as np -from pyomo.common.collections import ComponentSet, ComponentMap -from pyomo.core.base import Constraint, Block, value, VarData -from pyomo.core.expr import MonomialTermExpression +from pyomo.common.collections import ComponentMap +from pyomo.core.base import Block, value -from pyomo.contrib.pyros import master_problem_methods, separation_problem_methods import pyomo.contrib.pyros.master_problem_methods as mp_methods import pyomo.contrib.pyros.separation_problem_methods as sp_methods -from pyomo.contrib.pyros.solve_data import SeparationProblemData -from pyomo.contrib.pyros.uncertainty_sets import Geometry from pyomo.contrib.pyros.util import ( check_time_limit_reached, ObjectiveType, get_time_from_solver, pyrosTerminationCondition, IterationLogRecord, - generate_all_decision_rule_var_data_objects, get_main_elapsed_time, get_dr_var_to_monomial_map, ) - def update_grcs_solve_data( pyros_soln, term_cond, nominal_data, timing_data, separation_data, master_soln, k ): From 793bf57cfc9f35d0f802995af07e9141d9ed3a46 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 19:39:14 -0400 Subject: [PATCH 1894/3044] Remove unused subproblem data containers --- pyomo/contrib/pyros/solve_data.py | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index 382590bac70..1a1deb09931 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ """ -Objects to contain all model data and solve results for the ROSolver +Containers for PyROS subproblem solve results. """ @@ -83,32 +83,6 @@ def __str__(self): return "\n".join(lines) -class MasterProblemData(object): - """ - Container for the grcs master problem - - Attributes: - :master_model: master problem model object - :base_model: block representing the original model object - :iteration: current iteration of the algorithm - """ - - -class SeparationProblemData(object): - """ - Container for the grcs separation problem - - Attributes: - :separation_model: separation problem model object - :points_added_to_master: list of parameter violations added to the master problem over the course of the algorithm - :separation_problem_subsolver_statuses: list of subordinate sub-solver statuses throughout separations - :total_global_separation_solvers: Counter for number of times global solvers were employed in separation - :constraint_violations: List of constraint violations identified in separation - """ - - pass - - class MasterResult(object): """Data class for master problem results data. From 1ea438f7b7031e64ba059167a1e663bd09df1a8c Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 20:00:12 -0400 Subject: [PATCH 1895/3044] Simplify master results object --- pyomo/contrib/pyros/master_problem_methods.py | 32 +++++++------------ pyomo/contrib/pyros/solve_data.py | 16 +++------- 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index fcffada53e4..f0e6c84d346 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -34,7 +34,7 @@ ) from pyomo.repn.standard_repn import generate_standard_repn -from pyomo.contrib.pyros.solve_data import MasterResult +from pyomo.contrib.pyros.solve_data import MasterResults from pyomo.contrib.pyros.util import ( call_solver, enforce_dr_degree, @@ -710,9 +710,10 @@ def log_master_solve_results(master_model, config, results, desc="Optimized"): ) -def solver_call_master(master_data, config, solve_data): +def solver_call_master(master_data, config, master_soln): """ - Invoke subsolver(s) on PyROS master problem. + Invoke subsolver(s) on PyROS master problem, + and update the MasterResults object accordingly. Parameters ---------- @@ -720,22 +721,11 @@ def solver_call_master(master_data, config, solve_data): Container for current master problem and related data. config : ConfigDict PyROS solver settings. - solver : solver type - Primary subordinate optimizer with which to solve - the master problem. This may be a local or global - NLP solver. - solve_data : MasterResult + master_soln : MasterResults Master problem results object. May be empty or contain master feasibility problem results. - - Returns - ------- - master_soln : MasterResult - Master problem results object, containing master - model and subsolver results. """ master_model = master_data.master_model - master_soln = solve_data solver_term_cond_dict = {} if config.solve_master_globally: @@ -812,7 +802,7 @@ def solver_call_master(master_data, config, solve_data): ) if not try_backup: - return master_soln + return # all solvers have failed to return an acceptable status. # we will terminate PyROS with subsolver error status. @@ -864,14 +854,12 @@ def solver_call_master(master_data, config, solve_data): f"{serialization_msg}" ) - return master_soln - def solve_master(master_data, config): """ Solve the master problem """ - master_soln = MasterResult() + master_soln = MasterResults() # no master feas problem for iteration 0 if master_data.iteration > 0: @@ -899,10 +887,12 @@ def solve_master(master_data, config): ) return master_soln - return solver_call_master( - master_data=master_data, config=config, solve_data=master_soln + solver_call_master( + master_data=master_data, config=config, master_soln=master_soln ) + return master_soln + class MasterProblemData: """ diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index 1a1deb09931..2e7b506f40b 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -83,19 +83,11 @@ def __str__(self): return "\n".join(lines) -class MasterResult(object): - """Data class for master problem results data. - - Attributes: - - termination_condition: Solver termination condition - - fsv_values: list of design variable values - - ssv_values: list of control variable values - - first_stage_objective: objective contribution due to first-stage degrees of freedom - - second_stage_objective: objective contribution due to second-stage degrees of freedom - - grcs_termination_condition: the conditions under which the grcs terminated - (max_iter, robust_optimal, error) - - pyomo_results: results object from solve() statement +class MasterResults(object): + """ + Container for master problem solve results. + TODO: Formalize this class. """ From 06ad83009ee354282a0bfcbb16c8bd07ef779a1b Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 20:07:27 -0400 Subject: [PATCH 1896/3044] Simplify master problem constructor --- pyomo/contrib/pyros/master_problem_methods.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index f0e6c84d346..8c270a76c11 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -68,8 +68,8 @@ def construct_initial_master_problem(model_data, config): Contains a single scenario block fully cloned from the working model. """ - master_model = m = ConcreteModel() - m.scenarios = Block(NonNegativeIntegers, NonNegativeIntegers) + master_model = ConcreteModel() + master_model.scenarios = Block(NonNegativeIntegers, NonNegativeIntegers) add_scenario_block_to_master_problem( master_model=master_model, scenario_idx=(0, 0), From 2bb5d1a4415278c784546147d2f27d5aedf437cc Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 11 Jul 2024 20:21:23 -0400 Subject: [PATCH 1897/3044] Reorder model variable stats breakdown --- pyomo/contrib/pyros/tests/test_preprocessor.py | 4 ++-- pyomo/contrib/pyros/util.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index b1172812dbc..3c75816aa87 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -2744,10 +2744,10 @@ def test_preprocessor_log_model_statistics_affine_dr(self, obj_focus): f""" Model Statistics: Number of variables : 14 + Epigraph variable : 1 First-stage variables : 2 Second-stage variables : 5 (2 adj.) State variables : 2 (1 adj.) - Epigraph variable : 1 Decision rule variables : 4 Number of uncertain parameters : 1 Number of constraints : 23 @@ -2796,10 +2796,10 @@ def test_preprocessor_log_model_statistics_quadratic_dr(self, obj_focus): f""" Model Statistics: Number of variables : 16 + Epigraph variable : 1 First-stage variables : 2 Second-stage variables : 5 (2 adj.) State variables : 2 (1 adj.) - Epigraph variable : 1 Decision rule variables : 6 Number of uncertain parameters : 1 Number of constraints : 23 diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index d5d757be543..e65f03a1a0f 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -2460,6 +2460,7 @@ def log_model_statistics(model_data, config): info_log_func("Model Statistics:") info_log_func(f" Number of variables : {num_vars}") + info_log_func(f" Epigraph variable : {num_epigraph_vars}") info_log_func(f" First-stage variables : {num_first_stage_vars}") info_log_func( f" Second-stage variables : {num_second_stage_vars} " @@ -2469,7 +2470,6 @@ def log_model_statistics(model_data, config): f" State variables : {num_state_vars} " f"({num_eff_state_vars} adj.)" ) - info_log_func(f" Epigraph variable : {num_epigraph_vars}") info_log_func(f" Decision rule variables : {num_dr_vars}") info_log_func(f" Number of uncertain parameters : {num_uncertain_params}") From 25753d4d8041d506b5dc1e53ce4a46606924fb29 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 08:34:13 -0400 Subject: [PATCH 1898/3044] Added checks that FIM, Q, and L are consistent after solve --- pyomo/contrib/doe/redesign/test_build.py | 5 ++ pyomo/contrib/doe/tests/test_doe_build.py | 48 ++++++++++++ pyomo/contrib/doe/tests/test_doe_solve.py | 93 +++++++++++++++++++++-- 3 files changed, 140 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/doe/redesign/test_build.py b/pyomo/contrib/doe/redesign/test_build.py index 7d3bc398f70..05162179763 100644 --- a/pyomo/contrib/doe/redesign/test_build.py +++ b/pyomo/contrib/doe/redesign/test_build.py @@ -83,6 +83,11 @@ for i in range(27): sigma_inv_new_np[i, i] = sigma_inv_new[i] +# Check cholesky factorization +print("Cholesky Checking") +print(L_vals_new_np @ L_vals_new_np.T) +print(FIM_vals_new_np) + rescaled_FIM = rescale_FIM(FIM=FIM_vals_new_np, param_vals=param_vals) # Comparing values from compute FIM diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index e69de29bb2d..6f14f98b62a 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -0,0 +1,48 @@ +from pyomo.common.dependencies import ( + numpy as np, + numpy_available, + pandas as pd, + pandas_available, +) + +from experiment_class_example import * +from pyomo.contrib.doe import * + + +import pyomo.common.unittest as unittest + +from pyomo.opt import SolverFactory + +ipopt_available = SolverFactory("ipopt").available() + +f = open('result.json') +data_ex = json.load(f) +data_ex['control_points'] = {float(k): v for k, v in data_ex['control_points'].items()} + +class TestReactorExampleModel(unittest.TestCase): + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_fd_central_solve(self): + fd_method = "central" + obj_used = "trace" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) \ No newline at end of file diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 4102727fce5..0bc6196d7c1 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -19,7 +19,45 @@ data_ex = json.load(f) data_ex['control_points'] = {float(k): v for k, v in data_ex['control_points'].items()} -class TestReactorExamples(unittest.TestCase): +def get_FIM_Q_L(doe_obj=None,): + """ + Helper function to retreive results to compare. + + """ + model = doe_obj.model + + n_param = doe_obj.n_parameters + n_y = doe_obj.n_experiment_outputs + + FIM_vals = [pyo.value(model.fim[i, j]) for i in model.parameter_names for j in model.parameter_names] + if hasattr(model, 'L'): + L_vals = [pyo.value(model.L[i, j]) for i in model.parameter_names for j in model.parameter_names] + else: + L_vals = [[0] * n_param] * n_param + Q_vals = [pyo.value(model.sensitivity_jacobian[i, j]) for i in model.output_names for j in model.parameter_names] + sigma_inv = [1 / v for k,v in model.scenario_blocks[0].measurement_error.items()] + param_vals = np.array([[v for k, v in model.scenario_blocks[0].unknown_parameters.items()], ]) + + FIM_vals_np = np.array(FIM_vals).reshape((n_param, n_param)) + + for i in range(n_param): + for j in range(n_param): + if j < i: + FIM_vals_np[j, i] = FIM_vals_np[i, j] + + L_vals_np = np.array(L_vals).reshape((n_param, n_param)) + Q_vals_np = np.array(Q_vals).reshape((n_y, n_param)) + + sigma_inv_np = np.zeros((n_y, n_y)) + + for ind, v in enumerate(sigma_inv): + sigma_inv_np[ind, ind] = v + + return FIM_vals_np, Q_vals_np, L_vals_np, sigma_inv_np + + + +class TestReactorExampleSolving(unittest.TestCase): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_central_solve(self): @@ -49,7 +87,19 @@ def test_reactor_fd_central_solve(self): doe_obj.run_doe() + # Assert model solves assert (doe_obj.results['Solver Status'] == "ok") + + # Assert that Q, F, and L are the same. + FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj,) + + # Since Trace is used, no comparison for FIM and L.T @ L + + # Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM) + assert (np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q))) + + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") @@ -81,6 +131,14 @@ def test_reactor_fd_forward_solve(self): doe_obj.run_doe() assert (doe_obj.results['Solver Status'] == "ok") + + # Assert that Q, F, and L are the same. + FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj,) + + # Since Trace is used, no comparison for FIM and L.T @ L + + # Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM) + assert (np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q))) @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") @@ -112,6 +170,14 @@ def test_reactor_fd_backward_solve(self): doe_obj.run_doe() assert (doe_obj.results['Solver Status'] == "ok") + + # Assert that Q, F, and L are the same. + FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj,) + + # Since Trace is used, no comparison for FIM and L.T @ L + + # Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM) + assert (np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q))) # TODO: Fix determinant objective code, something is awry # Should only be using Cholesky=True @@ -176,6 +242,15 @@ def test_reactor_obj_cholesky_solve(self): doe_obj.run_doe() assert (doe_obj.results['Solver Status'] == "ok") + + # Assert that Q, F, and L are the same. + FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj,) + + # Since Cholesky is used, there is comparison for FIM and L.T @ L + assert (np.all(np.isclose(FIM, L @ L.T))) + + # Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM) + assert (np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q))) @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") @@ -204,11 +279,6 @@ def test_compute_FIM_seq(self): _only_compute_fim_lower=True, ) - design_ranges = { - 'CA[0]': [1, 5, 3], - 'T[0]': [300, 700, 3], - } - doe_obj.compute_FIM(method='sequential') @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @@ -245,6 +315,17 @@ def test_reactor_grid_search(self): } doe_obj.compute_FIM_full_factorial(design_ranges=design_ranges, method='sequential') + + # Check to make sure the lengths of the inputs in results object are indeed correct + CA_vals = doe_obj.fim_factorial_results['CA[0]'] + T_vals = doe_obj.fim_factorial_results['T[0]'] + + # Assert length is correct + assert (len(CA_vals) == 9) and (len(T_vals) == 9) + assert (len(set(CA_vals)) == 3) and (len(set(T_vals)) == 3) + + # Assert unique values are correct + assert (set(CA_vals).issuperset(set([1, 3, 5]))) and (set(T_vals).issuperset(set([300, 500, 700]))) From cdebfe9c94cdfc7273cc541222c881fce14bc5ab Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:38:25 -0400 Subject: [PATCH 1899/3044] Added build testing functions Added functions to make sure the finite differencing scheme is working properly and that design variable fixing is working properly for all currently available finite difference types (i.e., central, backward, forward). --- pyomo/contrib/doe/tests/test_doe_build.py | 293 +++++++++++++++++++++- 1 file changed, 290 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 6f14f98b62a..d9125f83a50 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -19,10 +19,10 @@ data_ex = json.load(f) data_ex['control_points'] = {float(k): v for k, v in data_ex['control_points'].items()} -class TestReactorExampleModel(unittest.TestCase): +class TestReactorExampleBuild(unittest.TestCase): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_reactor_fd_central_solve(self): + def test_reactor_fd_central_check_fd_eqns(self): fd_method = "central" obj_used = "trace" @@ -45,4 +45,291 @@ def test_reactor_fd_central_solve(self): args=None, _Cholesky_option=True, _only_compute_fim_lower=True, - ) \ No newline at end of file + ) + + doe_obj.create_doe_model() + + model = doe_obj.model + + # Check that the parameter values are correct + for s in model.scenarios: + param = model.parameter_scenarios[s] + + diff = (-1) ** s * doe_obj.step + + param_val = pyo.value(pyo.ComponentUID(param).find_component_on(model.scenario_blocks[s])) + + param_val_from_step = model.scenario_blocks[0].unknown_parameters[pyo.ComponentUID(param).find_component_on(model.scenario_blocks[0])] * (1 + diff) + + for k, v in model.scenario_blocks[s].unknown_parameters.items(): + name_ind = k.name.split(".").index("scenario_blocks[" + str(s) + "]") + + if ".".join(k.name.split(".")[name_ind + 1:]) == param.name: + continue + + other_param_val = pyo.value(k) + assert(np.isclose(other_param_val, v)) + + assert(np.isclose(param_val, param_val_from_step)) + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_fd_backward_check_fd_eqns(self): + fd_method = "backward" + obj_used = "trace" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.create_doe_model() + + model = doe_obj.model + + # Check that the parameter values are correct + for s in model.scenarios: + diff = -doe_obj.step * (s != 0) + if s != 0: + param = model.parameter_scenarios[s] + + param_val = pyo.value(pyo.ComponentUID(param).find_component_on(model.scenario_blocks[s])) + + param_val_from_step = model.scenario_blocks[0].unknown_parameters[pyo.ComponentUID(param).find_component_on(model.scenario_blocks[0])] * (1 + diff) + assert(np.isclose(param_val, param_val_from_step)) + + for k, v in model.scenario_blocks[s].unknown_parameters.items(): + name_ind = k.name.split(".").index("scenario_blocks[" + str(s) + "]") + + if not (s == 0) and ".".join(k.name.split(".")[name_ind + 1:]) == param.name: + continue + + other_param_val = pyo.value(k) + assert(np.isclose(other_param_val, v)) + + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_fd_forward_check_fd_eqns(self): + fd_method = "forward" + obj_used = "trace" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.create_doe_model() + + model = doe_obj.model + + # Check that the parameter values are correct + for s in model.scenarios: + diff = doe_obj.step * (s != 0) + if s != 0: + param = model.parameter_scenarios[s] + + param_val = pyo.value(pyo.ComponentUID(param).find_component_on(model.scenario_blocks[s])) + + param_val_from_step = model.scenario_blocks[0].unknown_parameters[pyo.ComponentUID(param).find_component_on(model.scenario_blocks[0])] * (1 + diff) + assert(np.isclose(param_val, param_val_from_step)) + + for k, v in model.scenario_blocks[s].unknown_parameters.items(): + name_ind = k.name.split(".").index("scenario_blocks[" + str(s) + "]") + + if not (s == 0) and ".".join(k.name.split(".")[name_ind + 1:]) == param.name: + continue + + other_param_val = pyo.value(k) + assert(np.isclose(other_param_val, v)) + + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_fd_central_design_fixing(self): + fd_method = "central" + obj_used = "trace" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.create_doe_model() + + model = doe_obj.model + + # Check that the design fixing constraints are generated + design_vars = [k for k, v in model.scenario_blocks[0].experiment_inputs.items()] + + con_name_base = "global_design_eq_con_" + + # Ensure that + for ind, d in enumerate(design_vars): + if ind == 0: + continue + + con_name = con_name_base + str(ind) + assert hasattr(model, con_name) + + # Ensure that each set of constraints has all blocks pairs with scenario 0 + # i.e., (0, 1), (0, 2), ..., (0, N) --> N - 1 constraints + assert len(getattr(model, con_name)) == (len(model.scenarios) - 1) + + # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) + assert not hasattr(model, con_name_base + str(len(design_vars))) + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_fd_backward_design_fixing(self): + fd_method = "backward" + obj_used = "trace" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.create_doe_model() + + model = doe_obj.model + + # Check that the design fixing constraints are generated + design_vars = [k for k, v in model.scenario_blocks[0].experiment_inputs.items()] + + con_name_base = "global_design_eq_con_" + + # Ensure that + for ind, d in enumerate(design_vars): + if ind == 0: + continue + + con_name = con_name_base + str(ind) + assert hasattr(model, con_name) + + # Ensure that each set of constraints has all blocks pairs with scenario 0 + # i.e., (0, 1), (0, 2), ..., (0, N) --> N - 1 constraints + assert len(getattr(model, con_name)) == (len(model.scenarios) - 1) + + # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) + assert not hasattr(model, con_name_base + str(len(design_vars))) + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_fd_forward_design_fixing(self): + fd_method = "forward" + obj_used = "trace" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.create_doe_model() + + model = doe_obj.model + + # Check that the design fixing constraints are generated + design_vars = [k for k, v in model.scenario_blocks[0].experiment_inputs.items()] + + con_name_base = "global_design_eq_con_" + + # Ensure that + for ind, d in enumerate(design_vars): + if ind == 0: + continue + + con_name = con_name_base + str(ind) + assert hasattr(model, con_name) + + # Ensure that each set of constraints has all blocks pairs with scenario 0 + # i.e., (0, 1), (0, 2), ..., (0, N) --> N - 1 constraints + assert len(getattr(model, con_name)) == (len(model.scenarios) - 1) + + # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) + assert not hasattr(model, con_name_base + str(len(design_vars))) + + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From a751f962e04d67fb3eb8ab02f1356af66739eb97 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:39:48 -0400 Subject: [PATCH 1900/3044] Ran Black --- pyomo/contrib/doe/doe.py | 12 +- pyomo/contrib/doe/tests/test_doe_build.py | 126 +++++++------ pyomo/contrib/doe/tests/test_doe_solve.py | 218 ++++++++++++---------- 3 files changed, 199 insertions(+), 157 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 70f08ffc1ff..9ec562ba562 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -303,7 +303,7 @@ def run_doe(self, model=None, results_file=None): # Solve the full model, which has now been initialized with the square solve res = self.solver.solve(model, tee=self.tee) - + # Track time used to solve the DoE model solve_time = sp_timer.toc(msg=None) @@ -320,9 +320,9 @@ def run_doe(self, model=None, results_file=None): # Make sure stale results don't follow the DoE object instance self.results = {} - - self.results['Solver Status'] = res.solver.status - self.results['Termination Condition'] = res.solver.termination_condition + + self.results["Solver Status"] = res.solver.status + self.results["Termination Condition"] = res.solver.termination_condition # Important quantities for optimal design self.results["FIM"] = fim_local @@ -400,7 +400,7 @@ def compute_FIM(self, model=None, method="sequential"): model = self.compute_FIM_model self.check_model_labels(model=model) - + # Set length values for the model features self.n_parameters = len(model.unknown_parameters) self.n_measurement_error = len(model.measurement_error) @@ -448,7 +448,7 @@ def _sequential_FIM(self, model=None): model = self.compute_FIM_model # Create suffix to keep track of parameter scenarios - if hasattr(model, 'parameter_scenarios'): + if hasattr(model, "parameter_scenarios"): model.del_component(model.parameter_scenarios) model.parameter_scenarios = pyo.Suffix( direction=pyo.Suffix.LOCAL, diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index d9125f83a50..f038a961bc2 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -15,9 +15,10 @@ ipopt_available = SolverFactory("ipopt").available() -f = open('result.json') +f = open("result.json") data_ex = json.load(f) -data_ex['control_points'] = {float(k): v for k, v in data_ex['control_points'].items()} +data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} + class TestReactorExampleBuild(unittest.TestCase): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @@ -25,11 +26,11 @@ class TestReactorExampleBuild(unittest.TestCase): def test_reactor_fd_central_check_fd_eqns(self): fd_method = "central" obj_used = "trace" - + experiment = FullReactorExperiment(data_ex, 10, 3) - + doe_obj = DesignOfExperiments( - experiment, + experiment, fd_formula=fd_method, step=1e-3, objective_option=obj_used, @@ -57,31 +58,35 @@ def test_reactor_fd_central_check_fd_eqns(self): diff = (-1) ** s * doe_obj.step - param_val = pyo.value(pyo.ComponentUID(param).find_component_on(model.scenario_blocks[s])) + param_val = pyo.value( + pyo.ComponentUID(param).find_component_on(model.scenario_blocks[s]) + ) - param_val_from_step = model.scenario_blocks[0].unknown_parameters[pyo.ComponentUID(param).find_component_on(model.scenario_blocks[0])] * (1 + diff) + param_val_from_step = model.scenario_blocks[0].unknown_parameters[ + pyo.ComponentUID(param).find_component_on(model.scenario_blocks[0]) + ] * (1 + diff) for k, v in model.scenario_blocks[s].unknown_parameters.items(): name_ind = k.name.split(".").index("scenario_blocks[" + str(s) + "]") - if ".".join(k.name.split(".")[name_ind + 1:]) == param.name: + if ".".join(k.name.split(".")[name_ind + 1 :]) == param.name: continue - + other_param_val = pyo.value(k) - assert(np.isclose(other_param_val, v)) + assert np.isclose(other_param_val, v) + + assert np.isclose(param_val, param_val_from_step) - assert(np.isclose(param_val, param_val_from_step)) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_backward_check_fd_eqns(self): fd_method = "backward" obj_used = "trace" - + experiment = FullReactorExperiment(data_ex, 10, 3) - + doe_obj = DesignOfExperiments( - experiment, + experiment, fd_formula=fd_method, step=1e-3, objective_option=obj_used, @@ -109,31 +114,37 @@ def test_reactor_fd_backward_check_fd_eqns(self): if s != 0: param = model.parameter_scenarios[s] - param_val = pyo.value(pyo.ComponentUID(param).find_component_on(model.scenario_blocks[s])) + param_val = pyo.value( + pyo.ComponentUID(param).find_component_on(model.scenario_blocks[s]) + ) - param_val_from_step = model.scenario_blocks[0].unknown_parameters[pyo.ComponentUID(param).find_component_on(model.scenario_blocks[0])] * (1 + diff) - assert(np.isclose(param_val, param_val_from_step)) + param_val_from_step = model.scenario_blocks[0].unknown_parameters[ + pyo.ComponentUID(param).find_component_on(model.scenario_blocks[0]) + ] * (1 + diff) + assert np.isclose(param_val, param_val_from_step) for k, v in model.scenario_blocks[s].unknown_parameters.items(): name_ind = k.name.split(".").index("scenario_blocks[" + str(s) + "]") - - if not (s == 0) and ".".join(k.name.split(".")[name_ind + 1:]) == param.name: + + if ( + not (s == 0) + and ".".join(k.name.split(".")[name_ind + 1 :]) == param.name + ): continue - + other_param_val = pyo.value(k) - assert(np.isclose(other_param_val, v)) - + assert np.isclose(other_param_val, v) @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_forward_check_fd_eqns(self): fd_method = "forward" obj_used = "trace" - + experiment = FullReactorExperiment(data_ex, 10, 3) - + doe_obj = DesignOfExperiments( - experiment, + experiment, fd_formula=fd_method, step=1e-3, objective_option=obj_used, @@ -161,31 +172,37 @@ def test_reactor_fd_forward_check_fd_eqns(self): if s != 0: param = model.parameter_scenarios[s] - param_val = pyo.value(pyo.ComponentUID(param).find_component_on(model.scenario_blocks[s])) + param_val = pyo.value( + pyo.ComponentUID(param).find_component_on(model.scenario_blocks[s]) + ) - param_val_from_step = model.scenario_blocks[0].unknown_parameters[pyo.ComponentUID(param).find_component_on(model.scenario_blocks[0])] * (1 + diff) - assert(np.isclose(param_val, param_val_from_step)) + param_val_from_step = model.scenario_blocks[0].unknown_parameters[ + pyo.ComponentUID(param).find_component_on(model.scenario_blocks[0]) + ] * (1 + diff) + assert np.isclose(param_val, param_val_from_step) for k, v in model.scenario_blocks[s].unknown_parameters.items(): name_ind = k.name.split(".").index("scenario_blocks[" + str(s) + "]") - - if not (s == 0) and ".".join(k.name.split(".")[name_ind + 1:]) == param.name: + + if ( + not (s == 0) + and ".".join(k.name.split(".")[name_ind + 1 :]) == param.name + ): continue - + other_param_val = pyo.value(k) - assert(np.isclose(other_param_val, v)) + assert np.isclose(other_param_val, v) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_central_design_fixing(self): fd_method = "central" obj_used = "trace" - + experiment = FullReactorExperiment(data_ex, 10, 3) - + doe_obj = DesignOfExperiments( - experiment, + experiment, fd_formula=fd_method, step=1e-3, objective_option=obj_used, @@ -212,31 +229,31 @@ def test_reactor_fd_central_design_fixing(self): con_name_base = "global_design_eq_con_" - # Ensure that + # Ensure that for ind, d in enumerate(design_vars): if ind == 0: continue - + con_name = con_name_base + str(ind) assert hasattr(model, con_name) # Ensure that each set of constraints has all blocks pairs with scenario 0 # i.e., (0, 1), (0, 2), ..., (0, N) --> N - 1 constraints assert len(getattr(model, con_name)) == (len(model.scenarios) - 1) - + # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) assert not hasattr(model, con_name_base + str(len(design_vars))) - + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_backward_design_fixing(self): fd_method = "backward" obj_used = "trace" - + experiment = FullReactorExperiment(data_ex, 10, 3) - + doe_obj = DesignOfExperiments( - experiment, + experiment, fd_formula=fd_method, step=1e-3, objective_option=obj_used, @@ -263,18 +280,18 @@ def test_reactor_fd_backward_design_fixing(self): con_name_base = "global_design_eq_con_" - # Ensure that + # Ensure that for ind, d in enumerate(design_vars): if ind == 0: continue - + con_name = con_name_base + str(ind) assert hasattr(model, con_name) # Ensure that each set of constraints has all blocks pairs with scenario 0 # i.e., (0, 1), (0, 2), ..., (0, N) --> N - 1 constraints assert len(getattr(model, con_name)) == (len(model.scenarios) - 1) - + # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) assert not hasattr(model, con_name_base + str(len(design_vars))) @@ -283,11 +300,11 @@ def test_reactor_fd_backward_design_fixing(self): def test_reactor_fd_forward_design_fixing(self): fd_method = "forward" obj_used = "trace" - + experiment = FullReactorExperiment(data_ex, 10, 3) - + doe_obj = DesignOfExperiments( - experiment, + experiment, fd_formula=fd_method, step=1e-3, objective_option=obj_used, @@ -314,22 +331,21 @@ def test_reactor_fd_forward_design_fixing(self): con_name_base = "global_design_eq_con_" - # Ensure that + # Ensure that for ind, d in enumerate(design_vars): if ind == 0: continue - + con_name = con_name_base + str(ind) assert hasattr(model, con_name) # Ensure that each set of constraints has all blocks pairs with scenario 0 # i.e., (0, 1), (0, 2), ..., (0, N) --> N - 1 constraints assert len(getattr(model, con_name)) == (len(model.scenarios) - 1) - + # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) assert not hasattr(model, con_name_base + str(len(design_vars))) - if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 0bc6196d7c1..b88a471e43a 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -15,11 +15,14 @@ ipopt_available = SolverFactory("ipopt").available() -f = open('result.json') +f = open("result.json") data_ex = json.load(f) -data_ex['control_points'] = {float(k): v for k, v in data_ex['control_points'].items()} +data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} -def get_FIM_Q_L(doe_obj=None,): + +def get_FIM_Q_L( + doe_obj=None, +): """ Helper function to retreive results to compare. @@ -29,14 +32,30 @@ def get_FIM_Q_L(doe_obj=None,): n_param = doe_obj.n_parameters n_y = doe_obj.n_experiment_outputs - FIM_vals = [pyo.value(model.fim[i, j]) for i in model.parameter_names for j in model.parameter_names] - if hasattr(model, 'L'): - L_vals = [pyo.value(model.L[i, j]) for i in model.parameter_names for j in model.parameter_names] + FIM_vals = [ + pyo.value(model.fim[i, j]) + for i in model.parameter_names + for j in model.parameter_names + ] + if hasattr(model, "L"): + L_vals = [ + pyo.value(model.L[i, j]) + for i in model.parameter_names + for j in model.parameter_names + ] else: L_vals = [[0] * n_param] * n_param - Q_vals = [pyo.value(model.sensitivity_jacobian[i, j]) for i in model.output_names for j in model.parameter_names] - sigma_inv = [1 / v for k,v in model.scenario_blocks[0].measurement_error.items()] - param_vals = np.array([[v for k, v in model.scenario_blocks[0].unknown_parameters.items()], ]) + Q_vals = [ + pyo.value(model.sensitivity_jacobian[i, j]) + for i in model.output_names + for j in model.parameter_names + ] + sigma_inv = [1 / v for k, v in model.scenario_blocks[0].measurement_error.items()] + param_vals = np.array( + [ + [v for k, v in model.scenario_blocks[0].unknown_parameters.items()], + ] + ) FIM_vals_np = np.array(FIM_vals).reshape((n_param, n_param)) @@ -56,18 +75,17 @@ def get_FIM_Q_L(doe_obj=None,): return FIM_vals_np, Q_vals_np, L_vals_np, sigma_inv_np - class TestReactorExampleSolving(unittest.TestCase): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_central_solve(self): fd_method = "central" obj_used = "trace" - + experiment = FullReactorExperiment(data_ex, 10, 3) - + doe_obj = DesignOfExperiments( - experiment, + experiment, fd_formula=fd_method, step=1e-3, objective_option=obj_used, @@ -84,33 +102,32 @@ def test_reactor_fd_central_solve(self): _Cholesky_option=True, _only_compute_fim_lower=True, ) - + doe_obj.run_doe() - + # Assert model solves - assert (doe_obj.results['Solver Status'] == "ok") + assert doe_obj.results["Solver Status"] == "ok" # Assert that Q, F, and L are the same. - FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj,) + FIM, Q, L, sigma_inv = get_FIM_Q_L( + doe_obj=doe_obj, + ) # Since Trace is used, no comparison for FIM and L.T @ L # Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM) - assert (np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q))) - + assert np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q)) - - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_forward_solve(self): fd_method = "forward" obj_used = "trace" - + experiment = FullReactorExperiment(data_ex, 10, 3) - + doe_obj = DesignOfExperiments( - experiment, + experiment, fd_formula=fd_method, step=1e-3, objective_option=obj_used, @@ -127,29 +144,31 @@ def test_reactor_fd_forward_solve(self): _Cholesky_option=True, _only_compute_fim_lower=True, ) - + doe_obj.run_doe() - - assert (doe_obj.results['Solver Status'] == "ok") + + assert doe_obj.results["Solver Status"] == "ok" # Assert that Q, F, and L are the same. - FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj,) + FIM, Q, L, sigma_inv = get_FIM_Q_L( + doe_obj=doe_obj, + ) # Since Trace is used, no comparison for FIM and L.T @ L # Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM) - assert (np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q))) - + assert np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q)) + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_backward_solve(self): fd_method = "backward" obj_used = "trace" - + experiment = FullReactorExperiment(data_ex, 10, 3) - + doe_obj = DesignOfExperiments( - experiment, + experiment, fd_formula=fd_method, step=1e-3, objective_option=obj_used, @@ -166,62 +185,64 @@ def test_reactor_fd_backward_solve(self): _Cholesky_option=True, _only_compute_fim_lower=True, ) - + doe_obj.run_doe() - - assert (doe_obj.results['Solver Status'] == "ok") + + assert doe_obj.results["Solver Status"] == "ok" # Assert that Q, F, and L are the same. - FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj,) + FIM, Q, L, sigma_inv = get_FIM_Q_L( + doe_obj=doe_obj, + ) # Since Trace is used, no comparison for FIM and L.T @ L # Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM) - assert (np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q))) - + assert np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q)) + # TODO: Fix determinant objective code, something is awry # Should only be using Cholesky=True # @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") # @unittest.skipIf(not numpy_available, "Numpy is not available") # def test_reactor_obj_det_solve(self): - # fd_method = "central" - # obj_used = "det" - - # experiment = FullReactorExperiment(data_ex, 10, 3) - - # doe_obj = DesignOfExperiments( - # experiment, - # fd_formula=fd_method, - # step=1e-3, - # objective_option=obj_used, - # scale_constant_value=1, - # scale_nominal_param_value=True, - # prior_FIM=None, - # jac_initial=None, - # fim_initial=None, - # L_initial=None, - # L_LB=1e-7, - # solver=None, - # tee=False, - # args=None, - # _Cholesky_option=False, - # _only_compute_fim_lower=False, - # ) - - # doe_obj.run_doe() - - # assert (doe_obj.results['Solver Status'] == "ok") - + # fd_method = "central" + # obj_used = "det" + + # experiment = FullReactorExperiment(data_ex, 10, 3) + + # doe_obj = DesignOfExperiments( + # experiment, + # fd_formula=fd_method, + # step=1e-3, + # objective_option=obj_used, + # scale_constant_value=1, + # scale_nominal_param_value=True, + # prior_FIM=None, + # jac_initial=None, + # fim_initial=None, + # L_initial=None, + # L_LB=1e-7, + # solver=None, + # tee=False, + # args=None, + # _Cholesky_option=False, + # _only_compute_fim_lower=False, + # ) + + # doe_obj.run_doe() + + # assert (doe_obj.results['Solver Status'] == "ok") + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_obj_cholesky_solve(self): fd_method = "central" obj_used = "det" - + experiment = FullReactorExperiment(data_ex, 10, 3) - + doe_obj = DesignOfExperiments( - experiment, + experiment, fd_formula=fd_method, step=1e-3, objective_option=obj_used, @@ -238,30 +259,32 @@ def test_reactor_obj_cholesky_solve(self): _Cholesky_option=True, _only_compute_fim_lower=True, ) - + doe_obj.run_doe() - - assert (doe_obj.results['Solver Status'] == "ok") + + assert doe_obj.results["Solver Status"] == "ok" # Assert that Q, F, and L are the same. - FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj,) + FIM, Q, L, sigma_inv = get_FIM_Q_L( + doe_obj=doe_obj, + ) # Since Cholesky is used, there is comparison for FIM and L.T @ L - assert (np.all(np.isclose(FIM, L @ L.T))) + assert np.all(np.isclose(FIM, L @ L.T)) # Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM) - assert (np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q))) - + assert np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q)) + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_compute_FIM_seq(self): fd_method = "central" obj_used = "det" - + experiment = FullReactorExperiment(data_ex, 10, 3) - + doe_obj = DesignOfExperiments( - experiment, + experiment, fd_formula=fd_method, step=1e-3, objective_option=obj_used, @@ -278,8 +301,8 @@ def test_compute_FIM_seq(self): _Cholesky_option=True, _only_compute_fim_lower=True, ) - - doe_obj.compute_FIM(method='sequential') + + doe_obj.compute_FIM(method="sequential") @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") @@ -287,11 +310,11 @@ def test_compute_FIM_seq(self): def test_reactor_grid_search(self): fd_method = "central" obj_used = "det" - + experiment = FullReactorExperiment(data_ex, 10, 3) - + doe_obj = DesignOfExperiments( - experiment, + experiment, fd_formula=fd_method, step=1e-3, objective_option=obj_used, @@ -308,26 +331,29 @@ def test_reactor_grid_search(self): _Cholesky_option=True, _only_compute_fim_lower=True, ) - + design_ranges = { - 'CA[0]': [1, 5, 3], - 'T[0]': [300, 700, 3], + "CA[0]": [1, 5, 3], + "T[0]": [300, 700, 3], } - - doe_obj.compute_FIM_full_factorial(design_ranges=design_ranges, method='sequential') + + doe_obj.compute_FIM_full_factorial( + design_ranges=design_ranges, method="sequential" + ) # Check to make sure the lengths of the inputs in results object are indeed correct - CA_vals = doe_obj.fim_factorial_results['CA[0]'] - T_vals = doe_obj.fim_factorial_results['T[0]'] + CA_vals = doe_obj.fim_factorial_results["CA[0]"] + T_vals = doe_obj.fim_factorial_results["T[0]"] # Assert length is correct assert (len(CA_vals) == 9) and (len(T_vals) == 9) assert (len(set(CA_vals)) == 3) and (len(set(T_vals)) == 3) # Assert unique values are correct - assert (set(CA_vals).issuperset(set([1, 3, 5]))) and (set(T_vals).issuperset(set([300, 500, 700]))) - - + assert (set(CA_vals).issuperset(set([1, 3, 5]))) and ( + set(T_vals).issuperset(set([300, 500, 700])) + ) + if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() From 73e1e9f9e7a8d308e998badbdeff7edd160e2c01 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 12 Jul 2024 09:51:44 -0400 Subject: [PATCH 1901/3044] Account for case where quadratic is None --- pyomo/repn/parameterized_quadratic.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 599add407ef..7528b438d20 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -391,14 +391,15 @@ def _factor_multiplier_into_quadratic_terms(self, ans, mult): del linear[vid] quadratic = ans.quadratic - quad_zeros = [] - for vid_pair, coef in ans.quadratic.items(): - if not is_zero(coef): - ans.quadratic[vid_pair] = mult * coef - else: - quad_zeros.append(vid_pair) - for vid_pair in quad_zeros: - del quadratic[vid_pair] + if quadratic is not None: + quad_zeros = [] + for vid_pair, coef in ans.quadratic.items(): + if not is_zero(coef): + ans.quadratic[vid_pair] = mult * coef + else: + quad_zeros.append(vid_pair) + for vid_pair in quad_zeros: + del quadratic[vid_pair] if ans.nonlinear is not None: ans.nonlinear *= mult From ea528ebd999a199cf7e497f34e25466181bbf92b Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:52:56 -0400 Subject: [PATCH 1902/3044] Added user initialization check, Ran Black --- pyomo/contrib/doe/tests/test_doe_build.py | 112 ++++++++++++++++++++-- 1 file changed, 106 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index f038a961bc2..79d21fe6f6f 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -20,8 +20,68 @@ data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} +def get_FIM_FIMPrior_Q_L( + doe_obj=None, +): + """ + Helper function to retreive results to compare. + + """ + model = doe_obj.model + + n_param = doe_obj.n_parameters + n_y = doe_obj.n_experiment_outputs + + FIM_vals = [ + pyo.value(model.fim[i, j]) + for i in model.parameter_names + for j in model.parameter_names + ] + FIM_prior_vals = [ + pyo.value(model.priorFIM[i, j]) + for i in model.parameter_names + for j in model.parameter_names + ] + if hasattr(model, "L"): + L_vals = [ + pyo.value(model.L[i, j]) + for i in model.parameter_names + for j in model.parameter_names + ] + else: + L_vals = [[0] * n_param] * n_param + Q_vals = [ + pyo.value(model.sensitivity_jacobian[i, j]) + for i in model.output_names + for j in model.parameter_names + ] + sigma_inv = [1 / v for k, v in model.scenario_blocks[0].measurement_error.items()] + param_vals = np.array( + [ + [v for k, v in model.scenario_blocks[0].unknown_parameters.items()], + ] + ) + + FIM_vals_np = np.array(FIM_vals).reshape((n_param, n_param)) + FIM_prior_vals_np = np.array(FIM_prior_vals).reshape((n_param, n_param)) + + for i in range(n_param): + for j in range(n_param): + if j < i: + FIM_vals_np[j, i] = FIM_vals_np[i, j] + + L_vals_np = np.array(L_vals).reshape((n_param, n_param)) + Q_vals_np = np.array(Q_vals).reshape((n_y, n_param)) + + sigma_inv_np = np.zeros((n_y, n_y)) + + for ind, v in enumerate(sigma_inv): + sigma_inv_np[ind, ind] = v + + return FIM_vals_np, FIM_prior_vals_np, Q_vals_np, L_vals_np, sigma_inv_np + + class TestReactorExampleBuild(unittest.TestCase): - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_central_check_fd_eqns(self): fd_method = "central" @@ -77,7 +137,6 @@ def test_reactor_fd_central_check_fd_eqns(self): assert np.isclose(param_val, param_val_from_step) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_backward_check_fd_eqns(self): fd_method = "backward" @@ -135,7 +194,6 @@ def test_reactor_fd_backward_check_fd_eqns(self): other_param_val = pyo.value(k) assert np.isclose(other_param_val, v) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_forward_check_fd_eqns(self): fd_method = "forward" @@ -193,7 +251,6 @@ def test_reactor_fd_forward_check_fd_eqns(self): other_param_val = pyo.value(k) assert np.isclose(other_param_val, v) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_central_design_fixing(self): fd_method = "central" @@ -244,7 +301,6 @@ def test_reactor_fd_central_design_fixing(self): # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) assert not hasattr(model, con_name_base + str(len(design_vars))) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_backward_design_fixing(self): fd_method = "backward" @@ -295,7 +351,6 @@ def test_reactor_fd_backward_design_fixing(self): # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) assert not hasattr(model, con_name_base + str(len(design_vars))) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_forward_design_fixing(self): fd_method = "forward" @@ -346,6 +401,51 @@ def test_reactor_fd_forward_design_fixing(self): # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) assert not hasattr(model, con_name_base + str(len(design_vars))) + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_user_initialization(self): + fd_method = "central" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + FIM_prior = np.ones((4, 4)) + FIM_initial = np.eye(4) + FIM_prior + JAC_initial = np.ones((27, 4)) * 2 + L_initial = np.tril( + np.ones((4, 4)) * 3 + ) # Must input lower triangular to get equality + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=FIM_prior, + jac_initial=JAC_initial, + fim_initial=FIM_initial, + L_initial=L_initial, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.create_doe_model() + + # Grab the matrix values on the model + FIM, FIM_prior_model, Q, L, sigma = get_FIM_FIMPrior_Q_L(doe_obj) + + # Make sure they match the inputs we gave + assert np.array_equal(FIM, FIM_initial) + assert np.array_equal(FIM_prior, FIM_prior_model) + assert np.array_equal(L_initial, L) + assert np.array_equal(JAC_initial, Q) + if __name__ == "__main__": unittest.main() From 1113b677adaa557324acff7810007b9434d81f66 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:56:52 -0400 Subject: [PATCH 1903/3044] Delete pyomo/contrib/doe/tests/test_reactor_example.py Removing old test files. --- .../contrib/doe/tests/test_reactor_example.py | 232 ------------------ 1 file changed, 232 deletions(-) delete mode 100644 pyomo/contrib/doe/tests/test_reactor_example.py diff --git a/pyomo/contrib/doe/tests/test_reactor_example.py b/pyomo/contrib/doe/tests/test_reactor_example.py deleted file mode 100644 index 19fb4e61820..00000000000 --- a/pyomo/contrib/doe/tests/test_reactor_example.py +++ /dev/null @@ -1,232 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# -# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation -# Initiative (CCSI), and is copyright (c) 2022 by the software owners: -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, -# University of Toledo, West Virginia University, et al. All rights reserved. -# -# NOTICE. This Software was developed under funding from the -# U.S. Department of Energy and the U.S. Government consequently retains -# certain rights. As such, the U.S. Government has been granted for itself -# and others acting on its behalf a paid-up, nonexclusive, irrevocable, -# worldwide license in the Software to reproduce, distribute copies to the -# public, prepare derivative works, and perform publicly and display -# publicly, and to permit other to do so. -# ___________________________________________________________________________ - - -# import libraries -from pyomo.common.dependencies import numpy as np, numpy_available, pandas_available -import pyomo.common.unittest as unittest -from pyomo.contrib.doe import DesignOfExperiments, MeasurementVariables, DesignVariables -from pyomo.environ import value, ConcreteModel -from pyomo.contrib.doe.examples.reactor_kinetics import create_model, disc_for_measure -from pyomo.opt import SolverFactory - -ipopt_available = SolverFactory("ipopt").available() - - -class Test_Reaction_Kinetics_Example(unittest.TestCase): - def test_reaction_kinetics_create_model(self): - """Test the three options in the kinetics example.""" - # parmest option - mod = create_model(model_option="parmest") - - # global and block option - mod = ConcreteModel() - create_model(mod, model_option="stage1") - create_model(mod, model_option="stage2") - # both options need a given model, or raise errors - with self.assertRaises(ValueError): - create_model(model_option="stage1") - - with self.assertRaises(ValueError): - create_model(model_option="stage2") - - with self.assertRaises(ValueError): - create_model(model_option="NotDefined") - - @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") - @unittest.skipIf(not pandas_available, "Pandas is not available") - def test_kinetics_example_sequential_finite_then_optimize(self): - """Test the kinetics example with sequential_finite mode and then optimization""" - doe_object = self.specify_reaction_kinetics() - - # Test FIM calculation at nominal values - sensi_opt = "sequential_finite" - result = doe_object.compute_FIM( - mode=sensi_opt, scale_nominal_param_value=True, formula="central" - ) - result.result_analysis() - self.assertAlmostEqual(np.log10(result.trace), 2.7885, places=2) - self.assertAlmostEqual(np.log10(result.det), 2.8218, places=2) - self.assertAlmostEqual(np.log10(result.min_eig), -1.0123, places=2) - - ### check subset feature - sub_name = "C" - sub_indices = {0: ["CB", "CC"], 1: [0.125, 0.25, 0.5, 0.75, 0.875]} - - measure_subset = MeasurementVariables() - measure_subset.add_variables( - sub_name, indices=sub_indices, time_index_position=1 - ) - sub_result = result.subset(measure_subset) - sub_result.result_analysis() - - self.assertAlmostEqual(np.log10(sub_result.trace), 2.5535, places=2) - self.assertAlmostEqual(np.log10(sub_result.det), 1.3464, places=2) - self.assertAlmostEqual(np.log10(sub_result.min_eig), -1.5386, places=2) - - ### Test stochastic_program mode - # Prior information (scaled FIM with T=500 and T=300 experiments) - prior = np.asarray( - [ - [28.67892806, 5.41249739, -81.73674601, -24.02377324], - [5.41249739, 26.40935036, -12.41816477, -139.23992532], - [-81.73674601, -12.41816477, 240.46276004, 58.76422806], - [-24.02377324, -139.23992532, 58.76422806, 767.25584508], - ] - ) - doe_object2 = self.specify_reaction_kinetics(prior=prior) - - square_result, optimize_result = doe_object2.stochastic_program( - if_optimize=True, - if_Cholesky=True, - scale_nominal_param_value=True, - objective_option="det", - L_initial=np.linalg.cholesky(prior), - jac_initial=result.jaco_information.copy(), - tee_opt=True, - ) - - optimize_result.result_analysis() - ## 2024-May-26: changing this to test the objective instead of the optimal solution - ## It's possible the objective is flat and the optimal solution is not unique - # self.assertAlmostEqual(value(optimize_result.model.CA0[0]), 5.0, places=2) - # self.assertAlmostEqual(value(optimize_result.model.T[0.5]), 300, places=2) - self.assertAlmostEqual(np.log10(optimize_result.det), 5.744, places=2) - - square_result, optimize_result = doe_object2.stochastic_program( - if_optimize=True, - scale_nominal_param_value=True, - objective_option="trace", - jac_initial=result.jaco_information.copy(), - tee_opt=True, - ) - - optimize_result.result_analysis() - ## 2024-May-26: changing this to test the objective instead of the optimal solution - ## It's possible the objective is flat and the optimal solution is not unique - # self.assertAlmostEqual(value(optimize_result.model.CA0[0]), 5.0, places=2) - # self.assertAlmostEqual(value(optimize_result.model.T[0.5]), 300, places=2) - self.assertAlmostEqual(np.log10(optimize_result.trace), 3.340, places=2) - - @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") - @unittest.skipIf(not pandas_available, "Pandas is not available") - def test_kinetics_example_direct_k_aug(self): - doe_object = self.specify_reaction_kinetics() - - # Test FIM calculation at nominal values - sensi_opt = "direct_kaug" - result = doe_object.compute_FIM( - mode=sensi_opt, scale_nominal_param_value=True, formula="central" - ) - result.result_analysis() - self.assertAlmostEqual(np.log10(result.trace), 2.789, places=2) - self.assertAlmostEqual(np.log10(result.det), 2.8247, places=2) - self.assertAlmostEqual(np.log10(result.min_eig), -1.0112, places=2) - - ### check subset feature - sub_name = "C" - sub_indices = {0: ["CB", "CC"], 1: [0.125, 0.25, 0.5, 0.75, 0.875]} - - measure_subset = MeasurementVariables() - measure_subset.add_variables( - sub_name, indices=sub_indices, time_index_position=1 - ) - sub_result = result.subset(measure_subset) - sub_result.result_analysis() - - self.assertAlmostEqual(np.log10(sub_result.trace), 2.5535, places=2) - self.assertAlmostEqual(np.log10(sub_result.det), 1.3464, places=2) - self.assertAlmostEqual(np.log10(sub_result.min_eig), -1.5386, places=2) - - def specify_reaction_kinetics(self, prior=None): - ### Define inputs - # Control time set [h] - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - # Define parameter nominal value - parameter_dict = {"A1": 84.79, "A2": 371.72, "E1": 7.78, "E2": 15.05} - - # measurement object - variable_name = "C" - indices = {0: ["CA", "CB", "CC"], 1: t_control} - - measurements = MeasurementVariables() - measurements.add_variables( - variable_name, indices=indices, time_index_position=1 - ) - - # design object - exp_design = DesignVariables() - - # add CAO as design variable - var_C = "CA0" - indices_C = {0: [0]} - exp1_C = [5] - exp_design.add_variables( - var_C, - indices=indices_C, - time_index_position=0, - values=exp1_C, - lower_bounds=1, - upper_bounds=5, - ) - - # add T as design variable - var_T = "T" - indices_T = {0: t_control} - exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] - - exp_design.add_variables( - var_T, - indices=indices_T, - time_index_position=0, - values=exp1_T, - lower_bounds=300, - upper_bounds=700, - ) - - design_names = exp_design.variable_names - exp1 = [5, 570, 300, 300, 300, 300, 300, 300, 300, 300] - exp1_design_dict = dict(zip(design_names, exp1)) - - exp_design.update_values(exp1_design_dict) - - doe_object = DesignOfExperiments( - parameter_dict, - exp_design, - measurements, - create_model, - discretize_model=disc_for_measure, - prior_FIM=prior, - ) - - return doe_object - - -if __name__ == "__main__": - unittest.main() From 7d5c562dab7b6dc71269a0e9285ce2734e8cdcbe Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:57:07 -0400 Subject: [PATCH 1904/3044] Delete pyomo/contrib/doe/tests/test_example.py Removing old test files. --- pyomo/contrib/doe/tests/test_example.py | 86 ------------------------- 1 file changed, 86 deletions(-) delete mode 100644 pyomo/contrib/doe/tests/test_example.py diff --git a/pyomo/contrib/doe/tests/test_example.py b/pyomo/contrib/doe/tests/test_example.py deleted file mode 100644 index e4ffbe89142..00000000000 --- a/pyomo/contrib/doe/tests/test_example.py +++ /dev/null @@ -1,86 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# -# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation -# Initiative (CCSI), and is copyright (c) 2022 by the software owners: -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, -# University of Toledo, West Virginia University, et al. All rights reserved. -# -# NOTICE. This Software was developed under funding from the -# U.S. Department of Energy and the U.S. Government consequently retains -# certain rights. As such, the U.S. Government has been granted for itself -# and others acting on its behalf a paid-up, nonexclusive, irrevocable, -# worldwide license in the Software to reproduce, distribute copies to the -# public, prepare derivative works, and perform publicly and display -# publicly, and to permit other to do so. -# ___________________________________________________________________________ - - -from pyomo.common.dependencies import ( - numpy as np, - numpy_available, - pandas as pd, - pandas_available, - scipy_available, -) - -import pyomo.common.unittest as unittest - -from pyomo.opt import SolverFactory - -ipopt_available = SolverFactory("ipopt").available() - - -class TestReactorExamples(unittest.TestCase): - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not scipy_available, "scipy is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_reactor_compute_FIM(self): - from pyomo.contrib.doe.examples import reactor_compute_FIM - - reactor_compute_FIM.main() - - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_reactor_optimize_doe(self): - from pyomo.contrib.doe.examples import reactor_optimize_doe - - reactor_optimize_doe.main() - - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not pandas_available, "pandas is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_reactor_grid_search(self): - from pyomo.contrib.doe.examples import reactor_grid_search - - reactor_grid_search.main() - - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not pandas_available, "pandas is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_reactor_design_slim_create_model_interface(self): - from pyomo.contrib.doe.examples import reactor_design - - reactor_design.main(legacy_create_model_interface=False) - - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not pandas_available, "pandas is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_reactor_design_legacy_create_model_interface(self): - from pyomo.contrib.doe.examples import reactor_design - - reactor_design.main(legacy_create_model_interface=True) - - -if __name__ == "__main__": - unittest.main() From 7a9e0c9ecd400ca74e426c4b54a653e02291bee8 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:57:20 -0400 Subject: [PATCH 1905/3044] Delete pyomo/contrib/doe/tests/test_fim_doe.py Removing old test files. --- pyomo/contrib/doe/tests/test_fim_doe.py | 468 ------------------------ 1 file changed, 468 deletions(-) delete mode 100644 pyomo/contrib/doe/tests/test_fim_doe.py diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py deleted file mode 100644 index d9a8d60fdb4..00000000000 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ /dev/null @@ -1,468 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# -# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation -# Initiative (CCSI), and is copyright (c) 2022 by the software owners: -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, -# University of Toledo, West Virginia University, et al. All rights reserved. -# -# NOTICE. This Software was developed under funding from the -# U.S. Department of Energy and the U.S. Government consequently retains -# certain rights. As such, the U.S. Government has been granted for itself -# and others acting on its behalf a paid-up, nonexclusive, irrevocable, -# worldwide license in the Software to reproduce, distribute copies to the -# public, prepare derivative works, and perform publicly and display -# publicly, and to permit other to do so. -# ___________________________________________________________________________ - -from pyomo.common.dependencies import numpy as np, numpy_available -import pyomo.common.unittest as unittest -from pyomo.contrib.doe import ( - MeasurementVariables, - DesignVariables, - ScenarioGenerator, - DesignOfExperiments, - VariablesWithIndices, -) -from pyomo.contrib.doe.examples.reactor_kinetics import create_model, disc_for_measure - - -class TestMeasurementError(unittest.TestCase): - - def test_with_time_plus_one_extra_index(self): - """This tests confirms the typical usage with a time index plus one extra index. - - This test should execute without throwing any errors. - - """ - - MeasurementVariables().add_variables( - "C", indices={0: ["A", "B", "C"], 1: [0, 0.5, 1.0]}, time_index_position=1 - ) - - def test_with_time_plus_two_extra_indices(self): - """This tests confirms the typical usage with a time index plus two extra indices. - - This test should execute without throwing any errors. - - """ - - MeasurementVariables().add_variables( - "C", - indices={ - 0: ["A", "B", "C"], # species - 1: [0, 0.5, 1.0], # time - 2: [1, 2, 3], - }, # position - time_index_position=1, - ) - - def test_time_index_position_out_of_bounds(self): - """This test confirms that an error is thrown when the time index position is out of bounds.""" - - # if time index is not in indices, an value error is thrown. - with self.assertRaises(ValueError): - MeasurementVariables().add_variables( - "C", - indices={0: ["CA", "CB", "CC"], 1: [0, 0.5, 1.0]}, # species # time - time_index_position=2, # this is out of bounds - ) - - def test_single_measurement_variable(self): - """This test confirms we can specify a single measurement variable without - specifying the indices. - - The test should execute with no errors. - """ - measurements = MeasurementVariables() - measurements.add_variables("HelloWorld", indices=None, time_index_position=None) - - def test_without_time_index(self): - """This test confirms we can add a measurement variable without specifying the time index. - - The test should execute with no errors. - - """ - - MeasurementVariables().add_variables( - "C", - indices={0: ["CA", "CB", "CC"]}, # species as only index - time_index_position=None, # no time index - ) - - def test_only_time_index(self): - """This test confirms we can add a measurement variable without specifying the variable name. - - The test should execute with no errors. - - """ - - MeasurementVariables().add_variables( - "HelloWorld", # name of the variable - indices={0: [0, 0.5, 1.0]}, - time_index_position=0, - ) - - def test_with_no_measurement_name(self): - """This test confirms that an error is thrown when None is used as the measurement name.""" - - with self.assertRaises(TypeError): - MeasurementVariables().add_variables( - None, indices={0: [0, 0.5, 1.0]}, time_index_position=0 - ) - - def test_with_non_string_measurement_name(self): - """This test confirms that an error is thrown when a non-string is used as the measurement name.""" - - with self.assertRaises(TypeError): - MeasurementVariables().add_variables( - 1, indices={0: [0, 0.5, 1.0]}, time_index_position=0 - ) - - def test_non_integer_index_keys(self): - """This test confirms that strings can be used as keys for specifying the indices. - - Warning: it is possible this usage breaks something else in Pyomo.DoE. - There may be an implicit assumption that the order of the keys must match the order - of the indices in the Pyomo model. - - """ - - MeasurementVariables().add_variables( - "C", - indices={"species": ["CA", "CB", "CC"], "time": [0, 0.5, 1.0]}, - time_index_position="time", - ) - - def test_no_measurements(self): - """This test confirms that an error is thrown when the user forgets to add any measurements. - - It's okay to have no decision variables. With no measurement variables, the FIM is the zero matrix. - This (no measurements) is a common user mistake. - """ - - with self.assertRaises(ValueError): - decisions = DesignVariables() - measurements = MeasurementVariables() - DesignOfExperiments( - {}, decisions, measurements, create_model, disc_for_measure - ) - - -class TestDesignError(unittest.TestCase): - def test(self): - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - # design object - exp_design = DesignVariables() - - # add T as design variable - var_T = "T" - indices_T = {0: t_control} - exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] - - upper_bound = [ - 700, - 700, - 700, - 700, - 700, - 700, - 700, - 700, - 700, - 800, - ] # wrong upper bound since it has more elements than the length of variable names - lower_bound = [300, 300, 300, 300, 300, 300, 300, 300, 300] - - with self.assertRaises(ValueError): - exp_design.add_variables( - var_T, - indices=indices_T, - time_index_position=0, - values=exp1_T, - lower_bounds=lower_bound, - upper_bounds=upper_bound, - ) - - -@unittest.skipIf(not numpy_available, "Numpy is not available") -class TestPriorFIMError(unittest.TestCase): - def test(self): - # Control time set [h] - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - # measurement object - variable_name = "C" - indices = {0: ["CA", "CB", "CC"], 1: t_control} - - measurements = MeasurementVariables() - measurements.add_variables( - variable_name, indices=indices, time_index_position=1 - ) - - # design object - exp_design = DesignVariables() - - # add CAO as design variable - var_C = "CA0" - indices_C = {0: [0]} - exp1_C = [5] - exp_design.add_variables( - var_C, - indices=indices_C, - time_index_position=0, - values=exp1_C, - lower_bounds=1, - upper_bounds=5, - ) - - # add T as design variable - var_T = "T" - indices_T = {0: t_control} - exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] - - exp_design.add_variables( - var_T, - indices=indices_T, - time_index_position=0, - values=exp1_T, - lower_bounds=300, - upper_bounds=700, - ) - - parameter_dict = {"A1": 1, "A2": 1, "E1": 1} - - # empty prior - prior_right = [[0] * 3 for i in range(3)] - prior_pass = [[0] * 5 for i in range(10)] - - # check if the error can be thrown when given a wrong shape of FIM prior - with self.assertRaises(ValueError): - doe_object = DesignOfExperiments( - parameter_dict, - exp_design, - measurements, - create_model, - prior_FIM=prior_pass, - discretize_model=disc_for_measure, - ) - - -class TestMeasurement(unittest.TestCase): - """Test the MeasurementVariables class, specify, add_element, update_variance, check_subset functions.""" - - def test_setup(self): - ### add_element function - - # control time for C [h] - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - # control time for T [h] - t_control2 = [0.2, 0.4, 0.6, 0.8] - - # measurement object - measurements = MeasurementVariables() - - # add variable C - variable_name = "C" - indices = {0: ["CA", "CB", "CC"], 1: t_control} - measurements.add_variables( - variable_name, indices=indices, time_index_position=1 - ) - - # add variable T - variable_name2 = "T" - indices2 = {0: [1, 3, 5], 1: t_control2} - measurements.add_variables( - variable_name2, indices=indices2, time_index_position=1, variance=10 - ) - - # check variable names - self.assertEqual(measurements.variable_names[0], "C[CA,0]") - self.assertEqual(measurements.variable_names[1], "C[CA,0.125]") - self.assertEqual(measurements.variable_names[-1], "T[5,0.8]") - self.assertEqual(measurements.variable_names[-2], "T[5,0.6]") - self.assertEqual(measurements.variance["T[5,0.4]"], 10) - self.assertEqual(measurements.variance["T[5,0.6]"], 10) - self.assertEqual(measurements.variance["T[5,0.4]"], 10) - self.assertEqual(measurements.variance["T[5,0.6]"], 10) - - ### specify function - var_names = [ - "C[CA,0]", - "C[CA,0.125]", - "C[CA,0.875]", - "C[CA,1]", - "C[CB,0]", - "C[CB,0.125]", - "C[CB,0.25]", - "C[CB,0.375]", - "C[CC,0]", - "C[CC,0.125]", - "C[CC,0.25]", - "C[CC,0.375]", - ] - - measurements2 = MeasurementVariables() - measurements2.set_variable_name_list(var_names) - - self.assertEqual(measurements2.variable_names[1], "C[CA,0.125]") - self.assertEqual(measurements2.variable_names[-1], "C[CC,0.375]") - - ### check_subset function - self.assertTrue(measurements.check_subset(measurements2)) - - -class TestDesignVariable(unittest.TestCase): - """Test the DesignVariable class, specify, add_element, add_bounds, update_values.""" - - def test_setup(self): - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - - # design object - exp_design = DesignVariables() - - # add CAO as design variable - var_C = "CA0" - indices_C = {0: [0]} - exp1_C = [5] - exp_design.add_variables( - var_C, - indices=indices_C, - time_index_position=0, - values=exp1_C, - lower_bounds=1, - upper_bounds=5, - ) - - # add T as design variable - var_T = "T" - indices_T = {0: t_control} - exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] - - exp_design.add_variables( - var_T, - indices=indices_T, - time_index_position=0, - values=exp1_T, - lower_bounds=300, - upper_bounds=700, - ) - - self.assertEqual( - exp_design.variable_names, - [ - "CA0[0]", - "T[0]", - "T[0.125]", - "T[0.25]", - "T[0.375]", - "T[0.5]", - "T[0.625]", - "T[0.75]", - "T[0.875]", - "T[1]", - ], - ) - self.assertEqual(exp_design.variable_names_value["CA0[0]"], 5) - self.assertEqual(exp_design.variable_names_value["T[0]"], 470) - self.assertEqual(exp_design.upper_bounds["CA0[0]"], 5) - self.assertEqual(exp_design.upper_bounds["T[0]"], 700) - self.assertEqual(exp_design.lower_bounds["CA0[0]"], 1) - self.assertEqual(exp_design.lower_bounds["T[0]"], 300) - - design_names = exp_design.variable_names - exp1 = [4, 600, 300, 300, 300, 300, 300, 300, 300, 300] - exp1_design_dict = dict(zip(design_names, exp1)) - exp_design.update_values(exp1_design_dict) - self.assertEqual(exp_design.variable_names_value["CA0[0]"], 4) - self.assertEqual(exp_design.variable_names_value["T[0]"], 600) - - -class TestParameter(unittest.TestCase): - """Test the ScenarioGenerator class, generate_scenario function.""" - - def test_setup(self): - # set up parameter class - param_dict = {"A1": 84.79, "A2": 371.72, "E1": 7.78, "E2": 15.05} - - scenario_gene = ScenarioGenerator(param_dict, formula="central", step=0.1) - parameter_set = scenario_gene.ScenarioData - - self.assertAlmostEqual(parameter_set.eps_abs["A1"], 16.9582, places=1) - self.assertAlmostEqual(parameter_set.eps_abs["E1"], 1.5554, places=1) - self.assertEqual(parameter_set.scena_num["A2"], [2, 3]) - self.assertEqual(parameter_set.scena_num["E1"], [4, 5]) - self.assertAlmostEqual(parameter_set.scenario[0]["A1"], 93.2699, places=1) - self.assertAlmostEqual(parameter_set.scenario[2]["A2"], 408.8895, places=1) - self.assertAlmostEqual(parameter_set.scenario[-1]["E2"], 13.54, places=1) - self.assertAlmostEqual(parameter_set.scenario[-2]["E2"], 16.55, places=1) - - -class TestVariablesWithIndices(unittest.TestCase): - """Test the DesignVariable class, specify, add_element, add_bounds, update_values.""" - - def test_setup(self): - special = VariablesWithIndices() - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - ### add_element function - # add CAO as design variable - var_C = "CA0" - indices_C = {0: [0]} - exp1_C = [5] - special.add_variables( - var_C, - indices=indices_C, - time_index_position=0, - values=exp1_C, - lower_bounds=1, - upper_bounds=5, - ) - - # add T as design variable - var_T = "T" - indices_T = {0: t_control} - exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] - - special.add_variables( - var_T, - indices=indices_T, - time_index_position=0, - values=exp1_T, - lower_bounds=300, - upper_bounds=700, - ) - - self.assertEqual( - special.variable_names, - [ - "CA0[0]", - "T[0]", - "T[0.125]", - "T[0.25]", - "T[0.375]", - "T[0.5]", - "T[0.625]", - "T[0.75]", - "T[0.875]", - "T[1]", - ], - ) - self.assertEqual(special.variable_names_value["CA0[0]"], 5) - self.assertEqual(special.variable_names_value["T[0]"], 470) - self.assertEqual(special.upper_bounds["CA0[0]"], 5) - self.assertEqual(special.upper_bounds["T[0]"], 700) - self.assertEqual(special.lower_bounds["CA0[0]"], 1) - self.assertEqual(special.lower_bounds["T[0]"], 300) - - -if __name__ == "__main__": - unittest.main() From ccc0be665853599cbb6ebd562d6846919e32cd5a Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:40:42 -0400 Subject: [PATCH 1906/3044] Corrected flags and added new test file for model building --- pyomo/contrib/doe/__init__.py | 1 + pyomo/contrib/doe/doe.py | 8 +- .../tests/experiment_class_example_flags.py | 241 ++++++++++++++++++ 3 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 pyomo/contrib/doe/tests/experiment_class_example_flags.py diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index 2a667660056..6ce3ada420e 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -15,4 +15,5 @@ ModelOptionLib, FiniteDifferenceStep, ) +from .tests import experiment_class_example, experiment_class_example_flags from .utils import rescale_FIM diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 9ec562ba562..8d1263e9fe2 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1267,7 +1267,7 @@ def check_model_labels(self, model=None): try: outputs = [k.name for k, v in model.experiment_outputs.items()] except: - RuntimeError( + raise RuntimeError( "Experiment model does not have suffix " + '"experiment_outputs".' ) @@ -1275,7 +1275,7 @@ def check_model_labels(self, model=None): try: outputs = [k.name for k, v in model.experiment_inputs.items()] except: - RuntimeError( + raise RuntimeError( "Experiment model does not have suffix " + '"experiment_inputs".' ) @@ -1283,7 +1283,7 @@ def check_model_labels(self, model=None): try: outputs = [k.name for k, v in model.unknown_parameters.items()] except: - RuntimeError( + raise RuntimeError( "Experiment model does not have suffix " + '"unknown_parameters".' ) @@ -1291,7 +1291,7 @@ def check_model_labels(self, model=None): try: outputs = [k.name for k, v in model.measurement_error.items()] except: - RuntimeError( + raise RuntimeError( "Experiment model does not have suffix " + '"measurement_error".' ) diff --git a/pyomo/contrib/doe/tests/experiment_class_example_flags.py b/pyomo/contrib/doe/tests/experiment_class_example_flags.py new file mode 100644 index 00000000000..2a555dd61d4 --- /dev/null +++ b/pyomo/contrib/doe/tests/experiment_class_example_flags.py @@ -0,0 +1,241 @@ +# === Required imports === +import pyomo.environ as pyo +from pyomo.dae import ContinuousSet, DerivativeVar, Simulator + +import itertools +import json +# ======================== + + +def expand_model_components(m, base_components, index_sets): + """ + Takes model components and index sets and returns the + model component labels. + + Arguments + --------- + m: Pyomo model + base_components: list of variables from model 'm' + index_sets: list, same length as base_components, where each + element is a list of index sets, or None + """ + for val, indexes in itertools.zip_longest(base_components, index_sets): + # If the variable has no index, + # add just the model component + if not val.is_indexed(): + yield val + # If the component is indexed but no + # index supplied, add all indices + elif indexes is None: + yield from val.values() + else: + for j in itertools.product(*indexes): + yield val[j] + + +class Experiment(object): + def __init__(self): + self.model = None + + def get_labeled_model(self): + raise NotImplementedError( + "Derived experiment class failed to implement get_labeled_model" + ) + + +class ReactorExperiment(object): + def __init__(self, data, nfe, ncp): + self.data = data + self.nfe = nfe + self.ncp = ncp + self.model = None + + def get_labeled_model(self, flag=0): + if self.model is None: + self.create_model() + self.finalize_model() + self.label_experiment(flag=flag) + return self.model + + def create_model(self): + """ + This is an example user model provided to DoE library. + It is a dynamic problem solved by Pyomo.DAE. + + Return + ------ + m: a Pyomo.DAE model + """ + + m = self.model = pyo.ConcreteModel() + + # Model parameters + m.R = pyo.Param(mutable=False, initialize=8.314) + + # m.A1 = pyo.Param(mutable=True) + # m.E1 = pyo.Param(mutable=True) + # m.A2 = pyo.Param(mutable=True) + # m.E2 = pyo.Param(mutable=True) + + # Define model variables + ######################## + # time + m.t = ContinuousSet(bounds=[0, 1]) + + # Concentrations + m.CA = pyo.Var(m.t, within=pyo.NonNegativeReals) + m.CB = pyo.Var(m.t, within=pyo.NonNegativeReals) + m.CC = pyo.Var(m.t, within=pyo.NonNegativeReals) + + # Temperature + m.T = pyo.Var(m.t, within=pyo.NonNegativeReals) + + # Arrhenius rate law equations + m.A1 = pyo.Var(within=pyo.NonNegativeReals) + m.E1 = pyo.Var(within=pyo.NonNegativeReals) + m.A2 = pyo.Var(within=pyo.NonNegativeReals) + m.E2 = pyo.Var(within=pyo.NonNegativeReals) + + # Differential variables (Conc.) + m.dCAdt = DerivativeVar(m.CA, wrt=m.t) + m.dCBdt = DerivativeVar(m.CB, wrt=m.t) + + ######################## + # End variable def. + + # Equation def'n + ######################## + + # Expression for rate constants + @m.Expression(m.t) + def k1(m, t): + return m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t])) + + @m.Expression(m.t) + def k2(m, t): + return m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t])) + + # Concentration odes + @m.Constraint(m.t) + def CA_rxn_ode(m, t): + return m.dCAdt[t] == -m.k1[t] * m.CA[t] + + @m.Constraint(m.t) + def CB_rxn_ode(m, t): + return m.dCBdt[t] == m.k1[t] * m.CA[t] - m.k2[t] * m.CB[t] + + # algebraic balance for concentration of C + # Valid because the reaction system (A --> B --> C) is equimolar + @m.Constraint(m.t) + def CC_balance(m, t): + return m.CA[0] == m.CA[t] + m.CB[t] + m.CC[t] + + ######################## + # End equation def'n + + def finalize_model(self): + """ + Example finalize model function. There are two main tasks + here: + 1. Extracting useful information for the model to align + with the experiment. (Here: CA0, t_final, t_control) + 2. Discretizing the model subject to this information. + + Arguments + --------- + m: Pyomo model + data: object containing vital experimental information + nfe: number of finite elements + ncp: number of collocation points for the finite elements + """ + m = self.model + + # Unpacking data before simulation + control_points = self.data['control_points'] + + m.CA[0].value = self.data['CA0'] + m.CB[0].fix(self.data['CB0']) + m.t.update(self.data['t_range']) + m.t.update(control_points) + m.A1.fix(self.data['A1']) + m.A2.fix(self.data['A2']) + m.E1.fix(self.data['E1']) + m.E2.fix(self.data['E2']) + + m.CA[0].setlb(self.data['CA_bounds'][0]) + m.CA[0].setub(self.data['CA_bounds'][1]) + + m.t_control = control_points + + # Discretizing the model + discr = pyo.TransformationFactory("dae.collocation") + discr.apply_to(m, nfe=self.nfe, ncp=self.ncp, wrt=m.t) + + # Initializing Temperature in the model + cv = None + for t in m.t: + if t in control_points: + cv = control_points[t] + m.T[t].setlb(self.data['T_bounds'][0]) + m.T[t].setub(self.data['T_bounds'][1]) + m.T[t] = cv + + @m.Constraint(m.t - control_points) + def T_control(m, t): + """ + Piecewise constant Temperature between control points + """ + neighbour_t = max(tc for tc in control_points if tc < t) + return m.T[t] == m.T[neighbour_t] + + # sim.initialize_model() + + + def label_experiment_impl(self, index_sets_meas, flag=0): + """ + Example for annotating (labeling) the model with a + full experiment. + + Arguments + --------- + + """ + m = self.model + base_comp_meas = [m.CA, m.CB, m.CC] + + if flag != 1: + # Grab measurement labels + print("Made experimental outputs!") + m.experiment_outputs = pyo.Suffix( + direction=pyo.Suffix.LOCAL, + ) + m.experiment_outputs.update((k, None) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) + + if flag != 2: + # Adding no error for measurements currently + m.measurement_error = pyo.Suffix( + direction=pyo.Suffix.LOCAL, + ) + m.measurement_error.update((k, 1e-2) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) + + if flag != 3: + # Grab design variables + base_comp_des = [m.CA, m.T] + index_sets_des = [[[m.t.first()]], [m.t_control]] + m.experiment_inputs = pyo.Suffix( + direction=pyo.Suffix.LOCAL, + ) + m.experiment_inputs.update((k, pyo.ComponentUID(k)) for k in expand_model_components(m, base_comp_des, index_sets_des)) + + if flag != 4: + m.unknown_parameters = pyo.Suffix( + direction=pyo.Suffix.LOCAL, + ) + m.unknown_parameters.update((k, pyo.value(k)) for k in [m.A1, m.A2, m.E1, m.E2]) + + +class FullReactorExperiment(ReactorExperiment): + def label_experiment(self, flag=0): + m = self.model + return self.label_experiment_impl([[m.t_control], [m.t_control], [m.t_control]], flag=flag) + From 31ede58b1513ba618a7e3073bac864d011eb44d5 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:46:23 -0400 Subject: [PATCH 1907/3044] Added some error tests Caught a bug while adding tests for checking model in the generate scenarios function. --- pyomo/contrib/doe/doe.py | 2 +- .../tests/experiment_class_example_flags.py | 1 - pyomo/contrib/doe/tests/test_doe_errors.py | 157 ++++++++++++++++++ 3 files changed, 158 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 8d1263e9fe2..264e9de94ae 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -954,7 +954,7 @@ def _generate_scenario_blocks(self, model=None): model.base_model = self.experiment.get_labeled_model(**self.args).clone() # Check the model that labels are correct - self.check_model_labels(model=model) + self.check_model_labels(model=model.base_model) # Gather lengths of label structures for later use in the model build process self.n_parameters = len(model.base_model.unknown_parameters) diff --git a/pyomo/contrib/doe/tests/experiment_class_example_flags.py b/pyomo/contrib/doe/tests/experiment_class_example_flags.py index 2a555dd61d4..731ffa577e4 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example_flags.py +++ b/pyomo/contrib/doe/tests/experiment_class_example_flags.py @@ -205,7 +205,6 @@ def label_experiment_impl(self, index_sets_meas, flag=0): if flag != 1: # Grab measurement labels - print("Made experimental outputs!") m.experiment_outputs = pyo.Suffix( direction=pyo.Suffix.LOCAL, ) diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index e69de29bb2d..f68788a8fe2 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -0,0 +1,157 @@ +from pyomo.common.dependencies import ( + numpy as np, + numpy_available, + pandas as pd, + pandas_available, +) + +from pyomo.contrib.doe.tests.experiment_class_example_flags import * +from pyomo.contrib.doe import * + + +import pyomo.common.unittest as unittest + +from pyomo.opt import SolverFactory + +from pathlib import Path + +ipopt_available = SolverFactory("ipopt").available() + +DATA_DIR = Path(__file__).parent +file_path = DATA_DIR / "result.json" + +f = open(file_path) +data_ex = json.load(f) +data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} + +class TestReactorExampleErrors(unittest.TestCase): + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_no_experiment_outputs(self): + fd_method = "central" + obj_used = "trace" + flag_val = 1 # Value for faulty model build mode - 1: No exp outputs + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={'flag': flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + RuntimeError, "Experiment model does not have suffix " + '"experiment_outputs".' + ): + doe_obj.create_doe_model() + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_no_measurement_error(self): + fd_method = "central" + obj_used = "trace" + flag_val = 2 # Value for faulty model build mode - 2: No meas error + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={'flag': flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + RuntimeError, "Experiment model does not have suffix " + '"measurement_error".' + ): + doe_obj.create_doe_model() + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_no_experiment_inputs(self): + fd_method = "central" + obj_used = "trace" + flag_val = 3 # Value for faulty model build mode - 3: No exp inputs/design vars + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={'flag': flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + RuntimeError, "Experiment model does not have suffix " + '"experiment_inputs".' + ): + doe_obj.create_doe_model() + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_no_unknown_parameters(self): + fd_method = "central" + obj_used = "trace" + flag_val = 4 # Value for faulty model build mode - 4: No unknown params + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={'flag': flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + RuntimeError, "Experiment model does not have suffix " + '"unknown_parameters".' + ): + doe_obj.create_doe_model() + +if __name__ == "__main__": + unittest.main() From a66bd6242d1d4ed6d664a821adddcb5f3c19c9b1 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:46:50 -0400 Subject: [PATCH 1908/3044] Updated model to have good imports And updated the file call to be robust. --- pyomo/contrib/doe/tests/test_doe_build.py | 9 +++++++-- pyomo/contrib/doe/tests/test_doe_solve.py | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 79d21fe6f6f..9365b289399 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -5,7 +5,7 @@ pandas_available, ) -from experiment_class_example import * +from pyomo.contrib.doe.tests.experiment_class_example import * from pyomo.contrib.doe import * @@ -13,9 +13,14 @@ from pyomo.opt import SolverFactory +from pathlib import Path + ipopt_available = SolverFactory("ipopt").available() -f = open("result.json") +DATA_DIR = Path(__file__).parent +file_path = DATA_DIR / "result.json" + +f = open(file_path) data_ex = json.load(f) data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index b88a471e43a..5577a56f055 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -5,7 +5,7 @@ pandas_available, ) -from experiment_class_example import * +from pyomo.contrib.doe.tests.experiment_class_example import * from pyomo.contrib.doe import * @@ -13,9 +13,14 @@ from pyomo.opt import SolverFactory +from pathlib import Path + ipopt_available = SolverFactory("ipopt").available() -f = open("result.json") +DATA_DIR = Path(__file__).parent +file_path = DATA_DIR / "result.json" + +f = open(file_path) data_ex = json.load(f) data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} From 5a5c51a2b5bdcfde3a77048b05194137fcec8916 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 12 Jul 2024 12:01:04 -0400 Subject: [PATCH 1909/3044] Tweak factor model set aux params calculations --- pyomo/contrib/pyros/uncertainty_sets.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 5746326f900..6f5474af026 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -2095,16 +2095,14 @@ def compute_auxiliary_param_vals(self, point, solver=None): if np.allclose(point_arr, self.origin): return np.zeros(self.number_of_factors), True - is_pinv_applicable = ( - self.dim > self.number_of_factors + is_psi_full_column_rank = ( + self.dim >= self.number_of_factors and np.linalg.matrix_rank(self.psi_mat) == self.number_of_factors ) - if is_pinv_applicable: - # full-rank skinny matrix. - # pseudoinverse uniquely determines the values of the - # auxiliary parameters - inv_psi = np.linalg.pinv(self.psi_mat) - aux_space_pt = inv_psi @ (point_arr - self.origin) + if is_psi_full_column_rank: + # pseudoinverse uniquely determines the auxiliary values + pinv_psi = np.linalg.pinv(self.psi_mat) + aux_space_pt = pinv_psi @ (point_arr - self.origin) tol = 1e-8 is_aux_pt_feasible = ( abs(aux_space_pt.sum()) <= self.beta * self.number_of_factors + tol @@ -2112,8 +2110,8 @@ def compute_auxiliary_param_vals(self, point, solver=None): ) return aux_space_pt, is_aux_pt_feasible else: - # check existence of point in auxiliary variable - # space using LPs + # there may be multiple feasible values or no feasible + # values. check with LP res = sp.optimize.linprog( c=np.zeros(self.number_of_factors), A_eq=self.psi_mat, @@ -2152,7 +2150,7 @@ def point_in_set(self, point): : bool True if the point lies in the set, False otherwise. """ - aux_space_pt, is_aux_pt_feasible = self.compute_auxiliary_param_vals(point) + _, is_aux_pt_feasible = self.compute_auxiliary_param_vals(point) return is_aux_pt_feasible From 09127298e94a5eade540941a5eec5028e1ebff0c Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 12 Jul 2024 13:33:24 -0400 Subject: [PATCH 1910/3044] Apply black --- pyomo/contrib/pyros/uncertainty_sets.py | 57 +++++++++---------------- 1 file changed, 19 insertions(+), 38 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 6f5474af026..7c95bef4a2c 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -109,11 +109,8 @@ def standardize_uncertain_param_vars(obj, dim): def _setup_standard_uncertainty_set_constraint_block( - block, - uncertain_param_vars, - dim, - num_auxiliary_vars=None, - ): + block, uncertain_param_vars, dim, num_auxiliary_vars=None +): """ Set up block to prepare for declaration of uncertainty set constraints. @@ -131,8 +128,7 @@ def _setup_standard_uncertainty_set_constraint_block( else: # resolve arguments param_var_data_list = standardize_uncertain_param_vars( - uncertain_param_vars, - dim=dim, + uncertain_param_vars, dim=dim ) conlist = ConstraintList() block.add_component( @@ -459,9 +455,7 @@ def _create_bounding_model(self): model.param_vars = Var(range(self.dim)) # add constraints - self.set_as_constraint( - uncertain_params=model.param_vars, block=model, - ) + self.set_as_constraint(uncertain_params=model.param_vars, block=model) @model.Objective(range(self.dim)) def param_var_objectives(self, idx): @@ -612,10 +606,7 @@ def _compute_parameter_bounds(self, solver): # solve should be successful for sense in (minimize, maximize): obj.sense = sense - res = solver.solve( - bounding_model, - load_solutions=False, - ) + res = solver.solve(bounding_model, load_solutions=False) if check_optimal_termination(res): bounding_model.solutions.load_from(res) else: @@ -637,10 +628,8 @@ def _compute_parameter_bounds(self, solver): return param_bounds def _add_bounds_on_uncertain_parameters( - self, - uncertain_param_vars, - global_solver=None, - ): + self, uncertain_param_vars, global_solver=None + ): """ Specify declared bounds for Vars representing the uncertain parameters constrained to an uncertainty set. @@ -661,7 +650,7 @@ def _add_bounds_on_uncertain_parameters( subproblem. """ uncertain_param_vars = standardize_uncertain_param_vars( - uncertain_param_vars, self.dim, + uncertain_param_vars, self.dim ) parameter_bounds = self.parameter_bounds @@ -1223,10 +1212,7 @@ def set_as_constraint(self, uncertain_params=None, block=None): ) cardinality_zip = zip( - self.origin, - self.positive_deviation, - aux_var_list, - param_var_data_list, + self.origin, self.positive_deviation, aux_var_list, param_var_data_list ) for orig_val, pos_dev, auxvar, param_var in cardinality_zip: conlist.add(orig_val + pos_dev * auxvar == param_var) @@ -1253,8 +1239,7 @@ def compute_auxiliary_param_vals(self, point, solver=None): aux_space_pt = np.empty(self.dim) is_zero_deviation_off_origin = np.logical_and( - self.positive_deviation == 0, - point_arr != self.origin, + self.positive_deviation == 0, point_arr != self.origin ) if np.any(is_zero_deviation_off_origin): return np.full(self.dim, np.nan), False @@ -1496,9 +1481,7 @@ def parameter_bounds(self): def set_as_constraint(self, uncertain_params=None, block=None): block, param_var_data_list, conlist, aux_var_list = ( _setup_standard_uncertainty_set_constraint_block( - block=block, - uncertain_param_vars=uncertain_params, - dim=self.dim, + block=block, uncertain_param_vars=uncertain_params, dim=self.dim ) ) @@ -2104,9 +2087,10 @@ def compute_auxiliary_param_vals(self, point, solver=None): pinv_psi = np.linalg.pinv(self.psi_mat) aux_space_pt = pinv_psi @ (point_arr - self.origin) tol = 1e-8 - is_aux_pt_feasible = ( - abs(aux_space_pt.sum()) <= self.beta * self.number_of_factors + tol - and np.all(np.abs(aux_space_pt) <= 1 + tol) + is_aux_pt_feasible = abs( + aux_space_pt.sum() + ) <= self.beta * self.number_of_factors + tol and np.all( + np.abs(aux_space_pt) <= 1 + tol ) return aux_space_pt, is_aux_pt_feasible else: @@ -2899,7 +2883,7 @@ def intersect(Q1, Q2): for set1, set2 in zip((Q1, Q2), (Q2, Q1)): if isinstance(set1, DiscreteScenarioSet): return DiscreteScenarioSet( - scenarios=[pt for pt in set1.scenarios if set1.point_in_set(pt)], + scenarios=[pt for pt in set1.scenarios if set1.point_in_set(pt)] ) # === This case is if both sets are continuous @@ -2919,20 +2903,17 @@ def set_as_constraint(self, uncertain_params=None, block=None): intersection_set = functools.reduce(self.intersect, self.all_sets) if isinstance(intersection_set, DiscreteScenarioSet): return intersection_set.set_as_constraint( - uncertain_params=uncertain_params, - block=block, + uncertain_params=uncertain_params, block=block ) all_cons, all_aux_vars = [], [] for idx, unc_set in enumerate(intersection_set.all_sets): sub_block = Block() block.add_component( - unique_component_name(block, f"sub_block_{idx}"), - sub_block, + unique_component_name(block, f"sub_block_{idx}"), sub_block ) set_quantification = unc_set.set_as_constraint( - block=sub_block, - uncertain_params=param_var_data_list, + block=sub_block, uncertain_params=param_var_data_list ) all_cons.extend(set_quantification.uncertainty_cons) all_aux_vars.extend(set_quantification.auxiliary_vars) From fe74e73f1edab89893e968a6c3d4a2371a2d5f7f Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:42:31 -0400 Subject: [PATCH 1911/3044] Updated all asserts to be raises; Added tests --- pyomo/contrib/doe/doe.py | 117 ++- .../tests/experiment_class_example_flags.py | 14 +- pyomo/contrib/doe/tests/test_doe_errors.py | 689 ++++++++++++++++++ 3 files changed, 748 insertions(+), 72 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 264e9de94ae..4b795062f7c 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -155,10 +155,9 @@ def __init__( logger_level: Specify the level of the logger. Change to logging.DEBUG for all messages. """ - # Assert that the Experiment object has callable ``get_labeled_model`` function - assert callable( - getattr(experiment, "get_labeled_model") - ), "The experiment object must have a ``get_labeled_model`` function" + # Check if the Experiment object has callable ``get_labeled_model`` function + if not hasattr(experiment, "get_labeled_model"): + raise ValueError("The experiment object must have a ``get_labeled_model`` function") # Set the experiment object from the user self.experiment = experiment @@ -234,6 +233,11 @@ def run_doe(self, model=None, results_file=None): default: None --> don't save """ + # Check results file name + if results_file is not None: + if type(results_file) not in [Path, str, ]: + raise ValueError("``results_file`` must be either a Path object or a string.") + # Start timer sp_timer = TicTocTimer() sp_timer.tic(msg=None) @@ -357,10 +361,6 @@ def run_doe(self, model=None, results_file=None): # If the user specifies to save the file, do it here as a json if results_file is not None: - assert type(results_file) in [ - Path, - str, - ], "`results_file` must be either a Path object or a string." with open(results_file, "w") as file: json.dump(self.results, file) @@ -962,11 +962,10 @@ def _generate_scenario_blocks(self, model=None): self.n_experiment_inputs = len(model.base_model.experiment_inputs) self.n_experiment_outputs = len(model.base_model.experiment_outputs) - assert ( - self.n_measurement_error == self.n_experiment_outputs - ), "Number of experiment outputs, {}, and length of measurement error, {}, do not match. Please check model labeling.".format( + if self.n_measurement_error != self.n_experiment_outputs: + raise ValueError("Number of experiment outputs, {}, and length of measurement error, {}, do not match. Please check model labeling.".format( self.n_experiment_outputs, self.n_measurement_error - ) + )) self.logger.info("Experiment output and measurement error lengths match.") @@ -1310,23 +1309,19 @@ def check_model_FIM(self, FIM=None): ---------- model: model for suffix checking, Default: None, (self.model) """ - assert FIM.shape == ( - self.n_parameters, - self.n_parameters, - ), "Shape of FIM provided should be n_parameters x n_parameters, or {}, FIM provided has shape: {}".format( - (self.n_parameters, self.n_parameters), FIM.shape - ) + if FIM.shape != (self.n_parameters, self.n_parameters): + raise ValueError("Shape of FIM provided should be n parameters by n parameters, or {} by {}, FIM provided has shape {} by {}".format( + self.n_parameters, self.n_parameters, FIM.shape[0], FIM.shape[1] + )) self.logger.info("FIM provided matches expected dimensions from model.") # Check the jacobian shape against what is expected from the model. def check_model_jac(self, jac=None): - assert jac.shape == ( - self.n_experiment_outputs, - self.n_parameters, - ), "Shape of Jacobian provided should be n_experiment_outputs x n_parameters, or {}, Jacobian provided has shape: {}".format( - (self.n_experiment_outputs, self.n_parameters), jac.shape - ) + if jac.shape != (self.n_experiment_outputs, self.n_parameters): + raise ValueError("Shape of Jacobian provided should be n experiment outputs by n parameters, or {} by {}, Jacobian provided has shape {} by {}".format( + self.n_experiment_outputs, self.n_parameters, jac.shape[0], jac.shape[1] + )) self.logger.info("Jacobian provided matches expected dimensions from model.") @@ -1351,9 +1346,8 @@ def update_FIM_prior(self, model=None, FIM=None): "FIM input for update_FIM_prior must be a 2D, square numpy array." ) - assert hasattr( - model, "fim" - ), "``fim`` is not defined on the model provided. Please build the model first." + if not hasattr(model, "fim"): + raise RuntimeError("``fim`` is not defined on the model provided. Please build the model first.") self.check_model_FIM(model, FIM) @@ -1560,47 +1554,38 @@ def draw_factorial_figure( """ if results is None: - assert hasattr( - self, "fim_factorial_results" - ), "Results must be provided or the compute_FIM_full_factorial function must be run." + if not hasattr(self, "fim_factorial_results"): + raise RuntimeError("Results must be provided or the compute_FIM_full_factorial function must be run.") results = self.fim_factorial_results full_design_variable_names = [ k.name for k, v in self.factorial_model.experiment_inputs.items() ] else: - assert ( - full_design_variable_names is not None - ), "If results object is provided, you must include all the design variable names." + if full_design_variable_names is None: + raise ValueError("If results object is provided, you must include all the design variable names.") des_names = full_design_variable_names # Inputs must exist for the function to do anything # ToDo: Put in a default value function????? - assert ( - sensitivity_design_variables is not None - ), "``sensitivity_design_variables`` must be included." - assert ( - fixed_design_variables is not None - ), "``sensitivity_design_variables`` must be included." + if sensitivity_design_variables is None: + raise ValueError("``sensitivity_design_variables`` must be included.") + + if fixed_design_variables is None: + raise ValueError("``fixed_design_variables`` must be included.") # Check that the provided design variables are within the results object check_des_vars = True for k, v in fixed_design_variables.items(): - check_des_vars *= k in [k for k, v in results.items()] + check_des_vars *= k in ([k2 for k2, v2 in results.items()]) check_sens_vars = True for k in sensitivity_design_variables: - check_sens_vars *= k in [k for k, v in results.items()] + check_sens_vars *= (k in [k2 for k2, v2 in results.items()]) - assert ( - check_des_vars - ), "Fixed design variables {} do not all appear in the results object keys {}.".format( - fixed_design_variables.keys(), results.keys() - ) - assert ( - check_sens_vars - ), "Sensitivity design variables {} do not all appear in the results object keys {}.".format( - sensitivity_design_variables.keys(), results.keys() - ) + if not check_des_vars: + raise ValueError("Fixed design variables do not all appear in the results object keys.") + if not check_sens_vars: + raise ValueError("Sensitivity design variables do not all appear in the results object keys.") # ToDo: Make it possible to plot pair-wise sensitivities for all variables # e.g. a curve like low-dimensional posterior distributions @@ -1953,9 +1938,8 @@ def get_FIM(self, model=None): if model is None: model = self.model - assert hasattr( - model, "fim" - ), "Model provided does not have variable `fim`. Please make sure the model is built properly before calling `get_FIM`" + if not hasattr(model, "fim"): + raise RuntimeError("Model provided does not have variable `fim`. Please make sure the model is built properly before calling `get_FIM`") fim_vals = [ pyo.value(model.fim[i, j]) @@ -1992,9 +1976,8 @@ def get_sensitivity_matrix(self, model=None): if model is None: model = self.model - assert hasattr( - model, "sensitivity_jacobian" - ), "Model provided does not have variable `sensitivity_jacobian`. Please make sure the model is built properly before calling `get_sensitivity_matrix`" + if not hasattr(model, "sensitivity_jacboian"): + raise RuntimeError("Model provided does not have variable `sensitivity_jacobian`. Please make sure the model is built properly before calling `get_sensitivity_matrix`") Q_vals = [ pyo.value(model.sensitivity_jacobian[i, j]) @@ -2027,9 +2010,8 @@ def get_experiment_input_values(self, model=None): model = self.model if not hasattr(model, "experiment_inputs"): - assert hasattr( - model, "scenario_blocks" - ), "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + if not hasattr(model, "scenario_blocks"): + raise RuntimeError("Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`") d_vals = [ pyo.value(k) @@ -2060,9 +2042,8 @@ def get_unknown_parameter_values(self, model=None): model = self.model if not hasattr(model, "unknown_parameters"): - assert hasattr( - model, "scenario_blocks" - ), "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + if not hasattr(model, "scenario_blocks"): + raise RuntimeError("Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`") theta_vals = [ pyo.value(k) @@ -2093,9 +2074,8 @@ def get_experiment_output_values(self, model=None): model = self.model if not hasattr(model, "experiment_outputs"): - assert hasattr( - model, "scenario_blocks" - ), "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + if not hasattr(model, "scenario_blocks"): + raise RuntimeError("Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`") y_hat_vals = [ pyo.value(k) @@ -2128,9 +2108,8 @@ def get_measurement_error_values(self, model=None): model = self.model if not hasattr(model, "measurement_error"): - assert hasattr( - model, "scenario_blocks" - ), "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + if not hasattr(model, "scenario_blocks"): + raise RuntimeError("Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`") sigma_vals = [ pyo.value(k) diff --git a/pyomo/contrib/doe/tests/experiment_class_example_flags.py b/pyomo/contrib/doe/tests/experiment_class_example_flags.py index 731ffa577e4..4b217ef691a 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example_flags.py +++ b/pyomo/contrib/doe/tests/experiment_class_example_flags.py @@ -33,6 +33,11 @@ def expand_model_components(m, base_components, index_sets): yield val[j] +class BadExperiment(object): + def __init__(self): + self.model = None + + class Experiment(object): def __init__(self): self.model = None @@ -213,9 +218,12 @@ def label_experiment_impl(self, index_sets_meas, flag=0): if flag != 2: # Adding no error for measurements currently m.measurement_error = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) - m.measurement_error.update((k, 1e-2) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) + direction=pyo.Suffix.LOCAL, + ) + if flag == 5: + m.measurement_error.update((m.CA[0], 1e-2) for k in range(1)) + else: + m.measurement_error.update((k, 1e-2) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) if flag != 3: # Grab design variables diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index f68788a8fe2..ba797a0fe59 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -25,6 +25,36 @@ data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} class TestReactorExampleErrors(unittest.TestCase): + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_no_get_labeled_model(self): + fd_method = "central" + obj_used = "trace" + flag_val = 1 # Value for faulty model build mode - 1: No exp outputs + + experiment = BadExperiment() + + with self.assertRaisesRegex( + ValueError, "The experiment object must have a ``get_labeled_model`` function" + ): + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_no_experiment_outputs(self): fd_method = "central" @@ -152,6 +182,665 @@ def test_reactor_check_no_unknown_parameters(self): RuntimeError, "Experiment model does not have suffix " + '"unknown_parameters".' ): doe_obj.create_doe_model() + + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_bad_prior_size(self): + fd_method = "central" + obj_used = "trace" + flag_val = 0 # Value for faulty model build mode - 0: full model + + prior_FIM = np.ones((5, 5)) + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=prior_FIM, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={'flag': flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + ValueError, "Shape of FIM provided should be n parameters by n parameters, or {} by {}, FIM provided has shape {} by {}".format( + 4, 4, prior_FIM.shape[0], prior_FIM.shape[1]) + ): + doe_obj.create_doe_model() + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_bad_jacobian_init_size(self): + fd_method = "central" + obj_used = "trace" + flag_val = 0 # Value for faulty model build mode - 0: full model + + jac_init = np.ones((5, 5)) + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=jac_init, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={'flag': flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + ValueError, "Shape of Jacobian provided should be n experiment outputs by n parameters, or {} by {}, Jacobian provided has shape {} by {}".format( + 27, 4, jac_init.shape[0], jac_init.shape[1]) + ): + doe_obj.create_doe_model() + + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_unbuilt_update_FIM(self): + fd_method = "central" + obj_used = "trace" + flag_val = 0 # Value for faulty model build mode - 0: full model + + FIM_update = np.ones((4, 4)) + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={'flag': flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + RuntimeError, "``fim`` is not defined on the model provided. Please build the model first." + ): + doe_obj.update_FIM_prior(FIM=FIM_update) + + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_results_file_name(self): + fd_method = "central" + obj_used = "trace" + flag_val = 0 # Value for faulty model build mode - 0: Full model + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={'flag': flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + ValueError, "``results_file`` must be either a Path object or a string." + ): + doe_obj.run_doe(results_file=int(15)) + + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_measurement_and_output_length_match(self): + fd_method = "central" + obj_used = "trace" + flag_val = 5 # Value for faulty model build mode - 5: Mismatch error and output length + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={'flag': flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + ValueError, "Number of experiment outputs, {}, and length of measurement error, {}, do not match. Please check model labeling.".format( + 27, 1 + )): + doe_obj.create_doe_model() + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not pandas_available, "pandas is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_grid_search_des_range_inputs(self): + fd_method = "central" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + design_ranges = { + "not": [1, 5, 3], + "correct": [300, 700, 3], + } + + with self.assertRaisesRegex( + ValueError, "Design ranges keys must be a subset of experimental design names." + ): + doe_obj.compute_FIM_full_factorial( + design_ranges=design_ranges, method="sequential" + ) + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not pandas_available, "pandas is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_premature_figure_drawing(self): + fd_method = "central" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + RuntimeError, "Results must be provided or the compute_FIM_full_factorial function must be run." + ): + doe_obj.draw_factorial_figure() + + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not pandas_available, "pandas is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_figure_drawing_no_des_var_names(self): + fd_method = "central" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + design_ranges = { + "CA[0]": [1, 5, 2], + "T[0]": [300, 700, 2], + } + + doe_obj.compute_FIM_full_factorial( + design_ranges=design_ranges, method="sequential" + ) + + with self.assertRaisesRegex( + ValueError, "If results object is provided, you must include all the design variable names." + ): + doe_obj.draw_factorial_figure(results=doe_obj.fim_factorial_results) + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not pandas_available, "pandas is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_figure_drawing_no_sens_names(self): + fd_method = "central" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + design_ranges = { + "CA[0]": [1, 5, 2], + "T[0]": [300, 700, 2], + } + + doe_obj.compute_FIM_full_factorial( + design_ranges=design_ranges, method="sequential" + ) + + with self.assertRaisesRegex( + ValueError, "``sensitivity_design_variables`` must be included." + ): + doe_obj.draw_factorial_figure() + + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not pandas_available, "pandas is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_figure_drawing_no_fixed_names(self): + fd_method = "central" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + design_ranges = { + "CA[0]": [1, 5, 2], + "T[0]": [300, 700, 2], + } + + doe_obj.compute_FIM_full_factorial( + design_ranges=design_ranges, method="sequential" + ) + + with self.assertRaisesRegex( + ValueError, "``fixed_design_variables`` must be included." + ): + doe_obj.draw_factorial_figure(sensitivity_design_variables={"dummy": "var"}) + + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not pandas_available, "pandas is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_figure_drawing_bad_fixed_names(self): + fd_method = "central" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + design_ranges = { + "CA[0]": [1, 5, 2], + "T[0]": [300, 700, 2], + } + + doe_obj.compute_FIM_full_factorial( + design_ranges=design_ranges, method="sequential" + ) + + with self.assertRaisesRegex( + ValueError, "Fixed design variables do not all appear in the results object keys." + ): + doe_obj.draw_factorial_figure(sensitivity_design_variables={"CA[0]": 1}, fixed_design_variables={"bad": "entry"}) + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not pandas_available, "pandas is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_figure_drawing_bad_sens_names(self): + fd_method = "central" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + design_ranges = { + "CA[0]": [1, 5, 2], + "T[0]": [300, 700, 2], + } + + doe_obj.compute_FIM_full_factorial( + design_ranges=design_ranges, method="sequential" + ) + + with self.assertRaisesRegex( + ValueError, "Sensitivity design variables do not all appear in the results object keys." + ): + doe_obj.draw_factorial_figure(sensitivity_design_variables={"bad": "entry"}, fixed_design_variables={"CA[0]": 1}) + + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_get_FIM_without_FIM(self): + fd_method = "central" + obj_used = "trace" + flag_val = 0 # Value for faulty model build mode - 5: Mismatch error and output length + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={'flag': flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + RuntimeError, "Model provided does not have variable `fim`. Please make sure the model is built properly before calling `get_FIM`" + ): + doe_obj.get_FIM() + + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_get_sens_mat_without_model(self): + fd_method = "central" + obj_used = "trace" + flag_val = 0 # Value for faulty model build mode - 5: Mismatch error and output length + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={'flag': flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + RuntimeError, "Model provided does not have variable `sensitivity_jacobian`. Please make sure the model is built properly before calling `get_sensitivity_matrix`" + ): + doe_obj.get_sensitivity_matrix() + + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_get_exp_inputs_without_model(self): + fd_method = "central" + obj_used = "trace" + flag_val = 0 # Value for faulty model build mode - 5: Mismatch error and output length + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={'flag': flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + RuntimeError, "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + ): + doe_obj.get_experiment_input_values() + + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_get_exp_outputs_without_model(self): + fd_method = "central" + obj_used = "trace" + flag_val = 0 # Value for faulty model build mode - 5: Mismatch error and output length + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={'flag': flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + RuntimeError, "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + ): + doe_obj.get_experiment_output_values() + + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_get_unknown_params_without_model(self): + fd_method = "central" + obj_used = "trace" + flag_val = 0 # Value for faulty model build mode - 5: Mismatch error and output length + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={'flag': flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + RuntimeError, "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + ): + doe_obj.get_unknown_parameter_values() + + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_get_meas_error_without_model(self): + fd_method = "central" + obj_used = "trace" + flag_val = 0 # Value for faulty model build mode - 5: Mismatch error and output length + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={'flag': flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + RuntimeError, "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + ): + doe_obj.get_measurement_error_values() + + if __name__ == "__main__": unittest.main() From a41efb574b9f50ffc3498f15d18d45ddbe0e1d94 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 12 Jul 2024 11:46:27 -0600 Subject: [PATCH 1912/3044] test for proper structure of changed as_domain() methods --- pyomo/core/tests/unit/kernel/test_conic.py | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/pyomo/core/tests/unit/kernel/test_conic.py b/pyomo/core/tests/unit/kernel/test_conic.py index ccfbcca7e1f..fc70b421060 100644 --- a/pyomo/core/tests/unit/kernel/test_conic.py +++ b/pyomo/core/tests/unit/kernel/test_conic.py @@ -35,6 +35,8 @@ primal_power, dual_exponential, dual_power, + primal_geomean, + dual_geomean, ) @@ -784,6 +786,40 @@ def test_as_domain(self): x[1].value = None +# these mosek 10 constraints are really anemic and can't be evaluated, pprinted, +# checked for convexity, pickled, etc. +class Test_primal_geomean(unittest.TestCase): + def test_as_domain(self): + b = primal_geomean.as_domain(r=[2, 3], x=6) + self.assertIs(type(b), block) + self.assertIs(type(b.q), primal_geomean) + self.assertIs(type(b.r), variable_tuple) + self.assertIs(type(b.x), variable) + self.assertIs(type(b.c), constraint_tuple) + self.assertExpressionsEqual(b.c[0].body, b.r[0]) + self.assertExpressionsEqual(b.c[0].rhs, 2) + self.assertExpressionsEqual(b.c[1].body, b.r[1]) + self.assertExpressionsEqual(b.c[1].rhs, 3) + self.assertExpressionsEqual(b.c[2].body, b.x) + self.assertExpressionsEqual(b.c[2].rhs, 6) + + +class Test_dual_geomean(unittest.TestCase): + def test_as_domain(self): + b = dual_geomean.as_domain(r=[2, 3], x=6) + self.assertIs(type(b), block) + self.assertIs(type(b.q), dual_geomean) + self.assertIs(type(b.r), variable_tuple) + self.assertIs(type(b.x), variable) + self.assertIs(type(b.c), constraint_tuple) + self.assertExpressionsEqual(b.c[0].body, b.r[0]) + self.assertExpressionsEqual(b.c[0].rhs, 2) + self.assertExpressionsEqual(b.c[1].body, b.r[1]) + self.assertExpressionsEqual(b.c[1].rhs, 3) + self.assertExpressionsEqual(b.c[2].body, b.x) + self.assertExpressionsEqual(b.c[2].rhs, 6) + + class TestMisc(unittest.TestCase): def test_build_linking_constraints(self): c = _build_linking_constraints([], []) From da285a9580e379301bbf0f8b9bd05584246d2db6 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:49:01 -0400 Subject: [PATCH 1913/3044] Delete pyomo/contrib/doe/redesign directory Removing temporary space for testing as build was ongoing. --- .../doe/redesign/experiment_class_example.py | 249 ------------------ pyomo/contrib/doe/redesign/result.json | 1 - .../doe/redesign/simple_reaction_example.py | 214 --------------- pyomo/contrib/doe/redesign/test_build.py | 140 ---------- 4 files changed, 604 deletions(-) delete mode 100644 pyomo/contrib/doe/redesign/experiment_class_example.py delete mode 100644 pyomo/contrib/doe/redesign/result.json delete mode 100644 pyomo/contrib/doe/redesign/simple_reaction_example.py delete mode 100644 pyomo/contrib/doe/redesign/test_build.py diff --git a/pyomo/contrib/doe/redesign/experiment_class_example.py b/pyomo/contrib/doe/redesign/experiment_class_example.py deleted file mode 100644 index eae355ebc89..00000000000 --- a/pyomo/contrib/doe/redesign/experiment_class_example.py +++ /dev/null @@ -1,249 +0,0 @@ -# === Required imports === -import pyomo.environ as pyo -from pyomo.dae import ContinuousSet, DerivativeVar, Simulator - -import itertools -import json -# ======================== - - -def expand_model_components(m, base_components, index_sets): - """ - Takes model components and index sets and returns the - model component labels. - - Arguments - --------- - m: Pyomo model - base_components: list of variables from model 'm' - index_sets: list, same length as base_components, where each - element is a list of index sets, or None - """ - for val, indexes in itertools.zip_longest(base_components, index_sets): - # If the variable has no index, - # add just the model component - if not val.is_indexed(): - yield val - # If the component is indexed but no - # index supplied, add all indices - elif indexes is None: - yield from val.values() - else: - for j in itertools.product(*indexes): - yield val[j] - - -class Experiment(object): - def __init__(self): - self.model = None - - def get_labeled_model(self): - raise NotImplementedError( - "Derived experiment class failed to implement get_labeled_model" - ) - - -class ReactorExperiment(object): - def __init__(self, data, nfe, ncp): - self.data = data - self.nfe = nfe - self.ncp = ncp - self.model = None - - def get_labeled_model(self): - if self.model is None: - self.create_model() - self.finalize_model() - self.label_experiment() - return self.model - - def create_model(self): - """ - This is an example user model provided to DoE library. - It is a dynamic problem solved by Pyomo.DAE. - - Return - ------ - m: a Pyomo.DAE model - """ - - m = self.model = pyo.ConcreteModel() - - # Model parameters - m.R = pyo.Param(mutable=False, initialize=8.314) - - # m.A1 = pyo.Param(mutable=True) - # m.E1 = pyo.Param(mutable=True) - # m.A2 = pyo.Param(mutable=True) - # m.E2 = pyo.Param(mutable=True) - - # Define model variables - ######################## - # time - m.t = ContinuousSet(bounds=[0, 1]) - - # Concentrations - m.CA = pyo.Var(m.t, within=pyo.NonNegativeReals) - m.CB = pyo.Var(m.t, within=pyo.NonNegativeReals) - m.CC = pyo.Var(m.t, within=pyo.NonNegativeReals) - - # Temperature - m.T = pyo.Var(m.t, within=pyo.NonNegativeReals) - - # Arrhenius rate law equations - m.A1 = pyo.Var(within=pyo.NonNegativeReals) - m.E1 = pyo.Var(within=pyo.NonNegativeReals) - m.A2 = pyo.Var(within=pyo.NonNegativeReals) - m.E2 = pyo.Var(within=pyo.NonNegativeReals) - - # Differential variables (Conc.) - m.dCAdt = DerivativeVar(m.CA, wrt=m.t) - m.dCBdt = DerivativeVar(m.CB, wrt=m.t) - - ######################## - # End variable def. - - # Equation def'n - ######################## - - # Expression for rate constants - @m.Expression(m.t) - def k1(m, t): - return m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t])) - - @m.Expression(m.t) - def k2(m, t): - return m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t])) - - # Concentration odes - @m.Constraint(m.t) - def CA_rxn_ode(m, t): - return m.dCAdt[t] == -m.k1[t] * m.CA[t] - - @m.Constraint(m.t) - def CB_rxn_ode(m, t): - return m.dCBdt[t] == m.k1[t] * m.CA[t] - m.k2[t] * m.CB[t] - - # algebraic balance for concentration of C - # Valid because the reaction system (A --> B --> C) is equimolar - @m.Constraint(m.t) - def CC_balance(m, t): - return m.CA[0] == m.CA[t] + m.CB[t] + m.CC[t] - - ######################## - # End equation def'n - - def finalize_model(self): - """ - Example finalize model function. There are two main tasks - here: - 1. Extracting useful information for the model to align - with the experiment. (Here: CA0, t_final, t_control) - 2. Discretizing the model subject to this information. - - Arguments - --------- - m: Pyomo model - data: object containing vital experimental information - nfe: number of finite elements - ncp: number of collocation points for the finite elements - """ - m = self.model - - # Unpacking data before simulation - control_points = self.data['control_points'] - - m.CA[0].value = self.data['CA0'] - m.CB[0].fix(self.data['CB0']) - m.t.update(self.data['t_range']) - m.t.update(control_points) - m.A1.fix(self.data['A1']) - m.A2.fix(self.data['A2']) - m.E1.fix(self.data['E1']) - m.E2.fix(self.data['E2']) - - m.CA[0].setlb(self.data['CA_bounds'][0]) - m.CA[0].setub(self.data['CA_bounds'][1]) - - m.t_control = control_points - - # Discretizing the model - discr = pyo.TransformationFactory("dae.collocation") - discr.apply_to(m, nfe=self.nfe, ncp=self.ncp, wrt=m.t) - - # Initializing Temperature in the model - cv = None - for t in m.t: - if t in control_points: - cv = control_points[t] - m.T[t].setlb(self.data['T_bounds'][0]) - m.T[t].setub(self.data['T_bounds'][1]) - m.T[t] = cv - - @m.Constraint(m.t - control_points) - def T_control(m, t): - """ - Piecewise constant Temperature between control points - """ - neighbour_t = max(tc for tc in control_points if tc < t) - return m.T[t] == m.T[neighbour_t] - - # sim.initialize_model() - - - def label_experiment_impl(self, index_sets_meas): - """ - Example for annotating (labeling) the model with a - full experiment. - - Arguments - --------- - - """ - m = self.model - - # Grab measurement labels - base_comp_meas = [m.CA, m.CB, m.CC] - m.experiment_outputs = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) - m.experiment_outputs.update((k, None) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) - - # Adding no error for measurements currently - m.measurement_error = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) - m.measurement_error.update((k, 1e-2) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) - - # Grab design variables - base_comp_des = [m.CA, m.T] - index_sets_des = [[[m.t.first()]], [m.t_control]] - m.experiment_inputs = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) - m.experiment_inputs.update((k, pyo.ComponentUID(k)) for k in expand_model_components(m, base_comp_des, index_sets_des)) - - m.unknown_parameters = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) - m.unknown_parameters.update((k, pyo.value(k)) for k in [m.A1, m.A2, m.E1, m.E2]) - - -class FullReactorExperiment(ReactorExperiment): - def label_experiment(self): - m = self.model - return self.label_experiment_impl([[m.t_control], [m.t_control], [m.t_control]]) - - -class PartialReactorExperiment(ReactorExperiment): - def label_experiment(self): - """ - Example for annotating (labeling) the model with a - "partial" experiment. - - Arguments - --------- - - """ - m = self.model - return self.label_experiment_impl([[m.t_control], [[m.t.last()]], [[m.t.last()]]]) diff --git a/pyomo/contrib/doe/redesign/result.json b/pyomo/contrib/doe/redesign/result.json deleted file mode 100644 index 7e1b1a79a1b..00000000000 --- a/pyomo/contrib/doe/redesign/result.json +++ /dev/null @@ -1 +0,0 @@ -{"CA0": 5.0, "CA_bounds": [1.0, 5.0], "CB0": 0.0, "CC0": 0.0, "t_range": [0, 1], "control_points": {"0": 500, "0.125": 300, "0.25": 300, "0.375": 300, "0.5": 300, "0.625": 300, "0.75": 300, "0.875": 300, "1": 300}, "T_bounds": [300, 700], "A1": 84.79, "A2": 371.72, "E1": 7.78, "E2": 15.05} \ No newline at end of file diff --git a/pyomo/contrib/doe/redesign/simple_reaction_example.py b/pyomo/contrib/doe/redesign/simple_reaction_example.py deleted file mode 100644 index 3bf4ac8ef74..00000000000 --- a/pyomo/contrib/doe/redesign/simple_reaction_example.py +++ /dev/null @@ -1,214 +0,0 @@ -# === Required imports === -import pyomo.environ as pyo -from pyomo.dae import ContinuousSet, DerivativeVar, Simulator - -import itertools -import json -# ======================== - -def expand_model_components(m, base_components, index_sets): - """ - Takes model components and index sets and returns the - model component labels. - - Arguments - --------- - m: Pyomo model - base_components: list of variables from model 'm' - index_sets: list, same length as base_components, where each - element is a list of index sets, or None - """ - for val, indexes in itertools.zip_longest(base_components, index_sets): - # If the variable has no index, - # add just the model component - if not val.is_indexed(): - yield val - # If the component is indexed but no - # index supplied, add all indices - elif indexes is None: - yield from val.values() - else: - for j in itertools.product(*indexes): - yield val[j] - -class Experiment(object): - def __init__(self): - self.model = None - - def get_labeled_model(self): - raise NotImplementedError( - "Derived experiment class failed to implement get_labeled_model" - ) - - -class SimpleReactorExperiment(object): - def __init__(self, data, nfe, ncp): - self.data = data - self.nfe = nfe - self.ncp = ncp - self.model = None - - def get_labeled_model(self): - if self.model is None: - self.create_model() - self.finalize_model() - self.label_experiment_impl(index_sets_meas=[[self.model.t_control], [self.model.t_control],]) - return self.model - - def create_model(self): - """ - This is an example user model provided to DoE library. - It is a dynamic problem solved by Pyomo.DAE. - - Return - ------ - m: a Pyomo.DAE model - """ - - m = self.model = pyo.ConcreteModel() - - # Model parameters - m.R = pyo.Param(mutable=False, initialize=8.314) - - # Define model variables - ######################## - # time - m.t = ContinuousSet(bounds=[0, 1]) - - # Concentrations - m.CA = pyo.Var(m.t, within=pyo.NonNegativeReals) - m.CB = pyo.Var(m.t, within=pyo.NonNegativeReals) - - # Arrhenius rate law equations - # m.A1 = pyo.Var(within=pyo.NonNegativeReals) - m.A1 = pyo.Param(initialize=0, mutable=True) - - # Differential variables (Conc.) - m.dCAdt = DerivativeVar(m.CA, wrt=m.t) - # m.dCBdt = DerivativeVar(m.CB, wrt=m.t) - - ######################## - # End variable def. - - # Equation def'n - ######################## - - m.k1 = pyo.Var(m.t, initialize=0) - - @m.Constraint(m.t) - def k1_con(m, t): - return m.k1[t] == m.A1 - - # @m.Expression(m.t) - # def k1(m, t): - # return m.A1 - - # Concentration odes - @m.Constraint(m.t) - def CA_rxn_ode(m, t): - return m.dCAdt[t] == -m.k1[t] * m.CA[t] - - # @m.Constraint(m.t) - # def CB_rxn_ode(m, t): - # return m.dCBdt[t] == m.A1 * m.CB[t] - - # algebraic balance for concentration of B - # Valid because the reaction system (A --> B) is equimolar - @m.Constraint(m.t) - def CB_balance(m, t): - return m.CA[0] == m.CA[t] + m.CB[t] - - ######################## - # End equation def'n - - def finalize_model(self): - """ - Example finalize model function. There are two main tasks - here: - 1. Extracting useful information for the model to align - with the experiment. (Here: CA0, t_final, t_control) - 2. Discretizing the model subject to this information. - - Arguments - --------- - m: Pyomo model - data: object containing vital experimental information - nfe: number of finite elements - ncp: number of collocation points for the finite elements - """ - m = self.model - - # Unpacking data before simulation - control_points = self.data['control_points'] - - m.CA[0].fix(self.data['CA0']) - # m.CB[0].fix(self.data['CB0']) - m.A1.value = self.data['A1'] - # m.A1.fix(self.data['A1']) - - m.k1.pprint() - - m.t_control = control_points - - print('SIMULATING MODEL.') - - # TODO: add simulation for initialization????? - # Call the simulator (optional) - sim = Simulator(m, package='casadi') - tsim, profiles = sim.simulate(integrator='idas') - - print('SIMULATION COMPLETE.') - - # Discretizing the model - discr = pyo.TransformationFactory("dae.collocation") - discr.apply_to(m, nfe=self.nfe, ncp=self.ncp, wrt=m.t) - - # sim.initialize_model() - - - def label_experiment_impl(self, index_sets_meas): - """ - Example for annotating (labeling) the model with a - full experiment. - - Arguments - --------- - - """ - m = self.model - - # Grab measurement labels - base_comp_meas = [m.CA, m.CB, ] - m.experiment_outputs = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) - m.experiment_outputs.update((k, None) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) - - # Adding no error for measurements currently - m.measurement_error = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) - m.measurement_error.update((k, 1e-2) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) - - # Grab design variables - base_comp_des = [m.CA, ] - index_sets_des = [[[m.t.first()]], ] - m.experiment_inputs = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) - m.experiment_inputs.update((k, pyo.ComponentUID(k)) for k in expand_model_components(m, base_comp_des, index_sets_des)) - - m.unknown_parameters = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) - m.unknown_parameters.update((k, pyo.value(k)) for k in [m.A1,]) - -f = open('result.json') -data_ex = json.load(f) -data_ex['control_points'] = {float(k): v for k, v in data_ex['control_points'].items()} - -experiments_simple = [ - SimpleReactorExperiment(data_ex, 32, 3), -] - -expanded_experiments_simple = [e.get_labeled_model() for e in experiments_simple] \ No newline at end of file diff --git a/pyomo/contrib/doe/redesign/test_build.py b/pyomo/contrib/doe/redesign/test_build.py deleted file mode 100644 index 05162179763..00000000000 --- a/pyomo/contrib/doe/redesign/test_build.py +++ /dev/null @@ -1,140 +0,0 @@ -from experiment_class_example import * -from pyomo.contrib.doe import * - -import numpy as np -import logging - -f = open('result.json') -data_ex = json.load(f) -data_ex['control_points'] = {float(k): v for k, v in data_ex['control_points'].items()} - -doe_obj = [0, 0, 0, 0,] -obj = ['trace', 'det', 'det'] - -for ind, fd in enumerate(['central', 'backward', 'forward']): - experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj[ind] = DesignOfExperiments( - experiment, - fd_formula='central', - step=1e-3, - objective_option=ObjectiveLib(obj[ind]), - scale_constant_value=1, - scale_nominal_param_value=(True and (ind != 2)), - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_initial=None, - L_LB=1e-7, - solver=None, - tee=False, - args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - logger_level=logging.INFO, - ) - doe_obj[ind].run_doe() - -# add a prior information (scaled FIM with T=500 and T=300 experiments) -# prior = np.asarray( - # [ - # [ 1745.81343391 1599.21859987 -3512.47892155 -7589.26220445] - # [ 1599.21859987 3525.63856364 -2900.94638673 -16465.46338508] - # [ -3512.47892155 -2900.94638673 7190.13048958 13849.96839993] - # [ -7589.26220445 -16465.46338508 13849.96839993 77674.03976715]] - # ] -# ) - -prior = None -design_ranges = { - 'CA[0]': [1, 5, 3], - 'T[0]': [300, 700, 3], -} -doe_obj[0].compute_FIM_full_factorial(design_ranges=design_ranges, method='kaug') - -doe_obj[0].compute_FIM(method='kaug') - -doe_obj[0].compute_FIM(method='sequential') - -print(doe_obj[0].kaug_FIM) - -# Optimal values -print("Optimal values for determinant optimized experimental design:") -print("New formulation, scaled: {}".format(pyo.value(doe_obj[1].model.objective))) -print("New formulation, unscaled: {}".format(pyo.value(doe_obj[2].model.objective))) - -# New values -FIM_vals_new = [pyo.value(doe_obj[1].model.fim[i, j]) for i in doe_obj[1].model.parameter_names for j in doe_obj[1].model.parameter_names] -L_vals_new = [pyo.value(doe_obj[1].model.L[i, j]) for i in doe_obj[1].model.parameter_names for j in doe_obj[1].model.parameter_names] -Q_vals_new = [pyo.value(doe_obj[1].model.sensitivity_jacobian[i, j]) for i in doe_obj[1].model.output_names for j in doe_obj[1].model.parameter_names] -sigma_inv_new = [1 / v for k,v in doe_obj[1].model.scenario_blocks[0].measurement_error.items()] -param_vals = np.array([[v for k, v in doe_obj[1].model.scenario_blocks[0].unknown_parameters.items()], ]) - -FIM_vals_new_np = np.array(FIM_vals_new).reshape((4, 4)) - -for i in range(4): - for j in range(4): - if j < i: - FIM_vals_new_np[j, i] = FIM_vals_new_np[i, j] - -L_vals_new_np = np.array(L_vals_new).reshape((4, 4)) -Q_vals_new_np = np.array(Q_vals_new).reshape((27, 4)) - -sigma_inv_new_np = np.zeros((27, 27)) -for i in range(27): - sigma_inv_new_np[i, i] = sigma_inv_new[i] - -# Check cholesky factorization -print("Cholesky Checking") -print(L_vals_new_np @ L_vals_new_np.T) -print(FIM_vals_new_np) - -rescaled_FIM = rescale_FIM(FIM=FIM_vals_new_np, param_vals=param_vals) - -# Comparing values from compute FIM -print("Results from using compute FIM (first old, then new)") -print(doe_obj[0].kaug_FIM) -print(doe_obj[0].seq_FIM) -print(np.isclose(doe_obj[0].kaug_FIM, doe_obj[0].seq_FIM, 1e-2)) -print(np.log10(np.linalg.det(doe_obj[0].kaug_FIM))) -print(np.log10(np.linalg.det(doe_obj[0].seq_FIM))) -A = doe_obj[0].kaug_jac -B = doe_obj[0].seq_jac -print(np.sum((A - B) ** 2)) - -measurement_vals_model = [] -meas_from_model = [] -mod = doe_obj[0].model -for p in mod.parameter_names: - fd_step_mult = 1 - param_ind = mod.parameter_names.data().index(p) - - # Different FD schemes lead to different scenarios for the computation - if doe_obj[0].fd_formula == FiniteDifferenceStep.central: - s1 = param_ind * 2 - s2 = param_ind * 2 + 1 - fd_step_mult = 2 - elif doe_obj[0].fd_formula == FiniteDifferenceStep.forward: - s1 = param_ind + 1 - s2 = 0 - elif doe_obj[0].fd_formula == FiniteDifferenceStep.backward: - s1 = 0 - s2 = param_ind + 1 - - var_up = [pyo.value(k) for k, v in mod.scenario_blocks[s1].experiment_outputs.items()] - var_lo = [pyo.value(k) for k, v in mod.scenario_blocks[s2].experiment_outputs.items()] - - meas_from_model.append(var_up) - meas_from_model.append(var_lo) - - -# Optimal values -print("Optimal values for determinant optimized experimental design:") -print("New formulation, scaled: {}".format(pyo.value(doe_obj[1].model.objective))) -print("New formulation, unscaled: {}".format(pyo.value(doe_obj[2].model.objective))) -print("New formulation, rescaled: {}".format(np.log10(np.linalg.det(rescaled_FIM)))) - -# Draw figures -sens_vars = ['CA[0]', 'T[0]'] -des_vars_fixed = {'T[' + str((i + 1) / 8) + ']': 300 for i in range(7)} -des_vars_fixed['T[1]'] = 300 -doe_obj[0].draw_factorial_figure(title_text='', xlabel_text='', ylabel_text='', sensitivity_design_variables=sens_vars, fixed_design_variables=des_vars_fixed,) \ No newline at end of file From fa1615dd3e0bcffd5cabe7c0621dc8f81a6b3fd2 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:49:54 -0400 Subject: [PATCH 1914/3044] Ran Black --- pyomo/contrib/doe/doe.py | 90 +++++++---- pyomo/contrib/doe/tests/test_doe_errors.py | 172 ++++++++++++--------- 2 files changed, 165 insertions(+), 97 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 4b795062f7c..3686dafdd51 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -157,7 +157,9 @@ def __init__( """ # Check if the Experiment object has callable ``get_labeled_model`` function if not hasattr(experiment, "get_labeled_model"): - raise ValueError("The experiment object must have a ``get_labeled_model`` function") + raise ValueError( + "The experiment object must have a ``get_labeled_model`` function" + ) # Set the experiment object from the user self.experiment = experiment @@ -235,9 +237,14 @@ def run_doe(self, model=None, results_file=None): """ # Check results file name if results_file is not None: - if type(results_file) not in [Path, str, ]: - raise ValueError("``results_file`` must be either a Path object or a string.") - + if type(results_file) not in [ + Path, + str, + ]: + raise ValueError( + "``results_file`` must be either a Path object or a string." + ) + # Start timer sp_timer = TicTocTimer() sp_timer.tic(msg=None) @@ -963,9 +970,11 @@ def _generate_scenario_blocks(self, model=None): self.n_experiment_outputs = len(model.base_model.experiment_outputs) if self.n_measurement_error != self.n_experiment_outputs: - raise ValueError("Number of experiment outputs, {}, and length of measurement error, {}, do not match. Please check model labeling.".format( - self.n_experiment_outputs, self.n_measurement_error - )) + raise ValueError( + "Number of experiment outputs, {}, and length of measurement error, {}, do not match. Please check model labeling.".format( + self.n_experiment_outputs, self.n_measurement_error + ) + ) self.logger.info("Experiment output and measurement error lengths match.") @@ -1310,18 +1319,25 @@ def check_model_FIM(self, FIM=None): model: model for suffix checking, Default: None, (self.model) """ if FIM.shape != (self.n_parameters, self.n_parameters): - raise ValueError("Shape of FIM provided should be n parameters by n parameters, or {} by {}, FIM provided has shape {} by {}".format( - self.n_parameters, self.n_parameters, FIM.shape[0], FIM.shape[1] - )) + raise ValueError( + "Shape of FIM provided should be n parameters by n parameters, or {} by {}, FIM provided has shape {} by {}".format( + self.n_parameters, self.n_parameters, FIM.shape[0], FIM.shape[1] + ) + ) self.logger.info("FIM provided matches expected dimensions from model.") # Check the jacobian shape against what is expected from the model. def check_model_jac(self, jac=None): if jac.shape != (self.n_experiment_outputs, self.n_parameters): - raise ValueError("Shape of Jacobian provided should be n experiment outputs by n parameters, or {} by {}, Jacobian provided has shape {} by {}".format( - self.n_experiment_outputs, self.n_parameters, jac.shape[0], jac.shape[1] - )) + raise ValueError( + "Shape of Jacobian provided should be n experiment outputs by n parameters, or {} by {}, Jacobian provided has shape {} by {}".format( + self.n_experiment_outputs, + self.n_parameters, + jac.shape[0], + jac.shape[1], + ) + ) self.logger.info("Jacobian provided matches expected dimensions from model.") @@ -1347,7 +1363,9 @@ def update_FIM_prior(self, model=None, FIM=None): ) if not hasattr(model, "fim"): - raise RuntimeError("``fim`` is not defined on the model provided. Please build the model first.") + raise RuntimeError( + "``fim`` is not defined on the model provided. Please build the model first." + ) self.check_model_FIM(model, FIM) @@ -1555,14 +1573,18 @@ def draw_factorial_figure( """ if results is None: if not hasattr(self, "fim_factorial_results"): - raise RuntimeError("Results must be provided or the compute_FIM_full_factorial function must be run.") + raise RuntimeError( + "Results must be provided or the compute_FIM_full_factorial function must be run." + ) results = self.fim_factorial_results full_design_variable_names = [ k.name for k, v in self.factorial_model.experiment_inputs.items() ] else: if full_design_variable_names is None: - raise ValueError("If results object is provided, you must include all the design variable names.") + raise ValueError( + "If results object is provided, you must include all the design variable names." + ) des_names = full_design_variable_names @@ -1570,7 +1592,7 @@ def draw_factorial_figure( # ToDo: Put in a default value function????? if sensitivity_design_variables is None: raise ValueError("``sensitivity_design_variables`` must be included.") - + if fixed_design_variables is None: raise ValueError("``fixed_design_variables`` must be included.") @@ -1580,12 +1602,16 @@ def draw_factorial_figure( check_des_vars *= k in ([k2 for k2, v2 in results.items()]) check_sens_vars = True for k in sensitivity_design_variables: - check_sens_vars *= (k in [k2 for k2, v2 in results.items()]) + check_sens_vars *= k in [k2 for k2, v2 in results.items()] if not check_des_vars: - raise ValueError("Fixed design variables do not all appear in the results object keys.") + raise ValueError( + "Fixed design variables do not all appear in the results object keys." + ) if not check_sens_vars: - raise ValueError("Sensitivity design variables do not all appear in the results object keys.") + raise ValueError( + "Sensitivity design variables do not all appear in the results object keys." + ) # ToDo: Make it possible to plot pair-wise sensitivities for all variables # e.g. a curve like low-dimensional posterior distributions @@ -1939,7 +1965,9 @@ def get_FIM(self, model=None): model = self.model if not hasattr(model, "fim"): - raise RuntimeError("Model provided does not have variable `fim`. Please make sure the model is built properly before calling `get_FIM`") + raise RuntimeError( + "Model provided does not have variable `fim`. Please make sure the model is built properly before calling `get_FIM`" + ) fim_vals = [ pyo.value(model.fim[i, j]) @@ -1977,7 +2005,9 @@ def get_sensitivity_matrix(self, model=None): model = self.model if not hasattr(model, "sensitivity_jacboian"): - raise RuntimeError("Model provided does not have variable `sensitivity_jacobian`. Please make sure the model is built properly before calling `get_sensitivity_matrix`") + raise RuntimeError( + "Model provided does not have variable `sensitivity_jacobian`. Please make sure the model is built properly before calling `get_sensitivity_matrix`" + ) Q_vals = [ pyo.value(model.sensitivity_jacobian[i, j]) @@ -2011,7 +2041,9 @@ def get_experiment_input_values(self, model=None): if not hasattr(model, "experiment_inputs"): if not hasattr(model, "scenario_blocks"): - raise RuntimeError("Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`") + raise RuntimeError( + "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + ) d_vals = [ pyo.value(k) @@ -2043,7 +2075,9 @@ def get_unknown_parameter_values(self, model=None): if not hasattr(model, "unknown_parameters"): if not hasattr(model, "scenario_blocks"): - raise RuntimeError("Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`") + raise RuntimeError( + "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + ) theta_vals = [ pyo.value(k) @@ -2075,7 +2109,9 @@ def get_experiment_output_values(self, model=None): if not hasattr(model, "experiment_outputs"): if not hasattr(model, "scenario_blocks"): - raise RuntimeError("Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`") + raise RuntimeError( + "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + ) y_hat_vals = [ pyo.value(k) @@ -2109,7 +2145,9 @@ def get_measurement_error_values(self, model=None): if not hasattr(model, "measurement_error"): if not hasattr(model, "scenario_blocks"): - raise RuntimeError("Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`") + raise RuntimeError( + "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + ) sigma_vals = [ pyo.value(k) diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index ba797a0fe59..4074415c9fd 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -24,6 +24,7 @@ data_ex = json.load(f) data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} + class TestReactorExampleErrors(unittest.TestCase): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_no_get_labeled_model(self): @@ -34,7 +35,8 @@ def test_reactor_check_no_get_labeled_model(self): experiment = BadExperiment() with self.assertRaisesRegex( - ValueError, "The experiment object must have a ``get_labeled_model`` function" + ValueError, + "The experiment object must have a ``get_labeled_model`` function", ): doe_obj = DesignOfExperiments( experiment, @@ -54,7 +56,7 @@ def test_reactor_check_no_get_labeled_model(self): _Cholesky_option=True, _only_compute_fim_lower=True, ) - + @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_no_experiment_outputs(self): fd_method = "central" @@ -77,16 +79,17 @@ def test_reactor_check_no_experiment_outputs(self): L_LB=1e-7, solver=None, tee=False, - args={'flag': flag_val}, + args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) with self.assertRaisesRegex( - RuntimeError, "Experiment model does not have suffix " + '"experiment_outputs".' + RuntimeError, + "Experiment model does not have suffix " + '"experiment_outputs".', ): doe_obj.create_doe_model() - + @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_no_measurement_error(self): fd_method = "central" @@ -109,16 +112,17 @@ def test_reactor_check_no_measurement_error(self): L_LB=1e-7, solver=None, tee=False, - args={'flag': flag_val}, + args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) with self.assertRaisesRegex( - RuntimeError, "Experiment model does not have suffix " + '"measurement_error".' + RuntimeError, + "Experiment model does not have suffix " + '"measurement_error".', ): doe_obj.create_doe_model() - + @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_no_experiment_inputs(self): fd_method = "central" @@ -141,16 +145,17 @@ def test_reactor_check_no_experiment_inputs(self): L_LB=1e-7, solver=None, tee=False, - args={'flag': flag_val}, + args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) with self.assertRaisesRegex( - RuntimeError, "Experiment model does not have suffix " + '"experiment_inputs".' + RuntimeError, + "Experiment model does not have suffix " + '"experiment_inputs".', ): doe_obj.create_doe_model() - + @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_no_unknown_parameters(self): fd_method = "central" @@ -173,16 +178,16 @@ def test_reactor_check_no_unknown_parameters(self): L_LB=1e-7, solver=None, tee=False, - args={'flag': flag_val}, + args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) with self.assertRaisesRegex( - RuntimeError, "Experiment model does not have suffix " + '"unknown_parameters".' + RuntimeError, + "Experiment model does not have suffix " + '"unknown_parameters".', ): doe_obj.create_doe_model() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_bad_prior_size(self): @@ -208,17 +213,19 @@ def test_reactor_check_bad_prior_size(self): L_LB=1e-7, solver=None, tee=False, - args={'flag': flag_val}, + args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) with self.assertRaisesRegex( - ValueError, "Shape of FIM provided should be n parameters by n parameters, or {} by {}, FIM provided has shape {} by {}".format( - 4, 4, prior_FIM.shape[0], prior_FIM.shape[1]) + ValueError, + "Shape of FIM provided should be n parameters by n parameters, or {} by {}, FIM provided has shape {} by {}".format( + 4, 4, prior_FIM.shape[0], prior_FIM.shape[1] + ), ): doe_obj.create_doe_model() - + @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_bad_jacobian_init_size(self): fd_method = "central" @@ -243,17 +250,18 @@ def test_reactor_check_bad_jacobian_init_size(self): L_LB=1e-7, solver=None, tee=False, - args={'flag': flag_val}, + args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) with self.assertRaisesRegex( - ValueError, "Shape of Jacobian provided should be n experiment outputs by n parameters, or {} by {}, Jacobian provided has shape {} by {}".format( - 27, 4, jac_init.shape[0], jac_init.shape[1]) + ValueError, + "Shape of Jacobian provided should be n experiment outputs by n parameters, or {} by {}, Jacobian provided has shape {} by {}".format( + 27, 4, jac_init.shape[0], jac_init.shape[1] + ), ): doe_obj.create_doe_model() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_unbuilt_update_FIM(self): @@ -279,17 +287,17 @@ def test_reactor_check_unbuilt_update_FIM(self): L_LB=1e-7, solver=None, tee=False, - args={'flag': flag_val}, + args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) with self.assertRaisesRegex( - RuntimeError, "``fim`` is not defined on the model provided. Please build the model first." + RuntimeError, + "``fim`` is not defined on the model provided. Please build the model first.", ): doe_obj.update_FIM_prior(FIM=FIM_update) - - + @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_results_file_name(self): fd_method = "central" @@ -312,7 +320,7 @@ def test_reactor_check_results_file_name(self): L_LB=1e-7, solver=None, tee=False, - args={'flag': flag_val}, + args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -321,13 +329,14 @@ def test_reactor_check_results_file_name(self): ValueError, "``results_file`` must be either a Path object or a string." ): doe_obj.run_doe(results_file=int(15)) - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_measurement_and_output_length_match(self): fd_method = "central" obj_used = "trace" - flag_val = 5 # Value for faulty model build mode - 5: Mismatch error and output length + flag_val = ( + 5 # Value for faulty model build mode - 5: Mismatch error and output length + ) experiment = FullReactorExperiment(data_ex, 10, 3) @@ -345,17 +354,19 @@ def test_reactor_check_measurement_and_output_length_match(self): L_LB=1e-7, solver=None, tee=False, - args={'flag': flag_val}, + args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) with self.assertRaisesRegex( - ValueError, "Number of experiment outputs, {}, and length of measurement error, {}, do not match. Please check model labeling.".format( - 27, 1 - )): + ValueError, + "Number of experiment outputs, {}, and length of measurement error, {}, do not match. Please check model labeling.".format( + 27, 1 + ), + ): doe_obj.create_doe_model() - + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") @@ -390,7 +401,8 @@ def test_reactor_grid_search_des_range_inputs(self): } with self.assertRaisesRegex( - ValueError, "Design ranges keys must be a subset of experimental design names." + ValueError, + "Design ranges keys must be a subset of experimental design names.", ): doe_obj.compute_FIM_full_factorial( design_ranges=design_ranges, method="sequential" @@ -425,10 +437,10 @@ def test_reactor_premature_figure_drawing(self): ) with self.assertRaisesRegex( - RuntimeError, "Results must be provided or the compute_FIM_full_factorial function must be run." + RuntimeError, + "Results must be provided or the compute_FIM_full_factorial function must be run.", ): doe_obj.draw_factorial_figure() - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") @@ -468,10 +480,11 @@ def test_reactor_figure_drawing_no_des_var_names(self): ) with self.assertRaisesRegex( - ValueError, "If results object is provided, you must include all the design variable names." + ValueError, + "If results object is provided, you must include all the design variable names.", ): doe_obj.draw_factorial_figure(results=doe_obj.fim_factorial_results) - + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") @@ -513,7 +526,6 @@ def test_reactor_figure_drawing_no_sens_names(self): ValueError, "``sensitivity_design_variables`` must be included." ): doe_obj.draw_factorial_figure() - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") @@ -556,7 +568,6 @@ def test_reactor_figure_drawing_no_fixed_names(self): ValueError, "``fixed_design_variables`` must be included." ): doe_obj.draw_factorial_figure(sensitivity_design_variables={"dummy": "var"}) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") @@ -596,10 +607,14 @@ def test_reactor_figure_drawing_bad_fixed_names(self): ) with self.assertRaisesRegex( - ValueError, "Fixed design variables do not all appear in the results object keys." + ValueError, + "Fixed design variables do not all appear in the results object keys.", ): - doe_obj.draw_factorial_figure(sensitivity_design_variables={"CA[0]": 1}, fixed_design_variables={"bad": "entry"}) - + doe_obj.draw_factorial_figure( + sensitivity_design_variables={"CA[0]": 1}, + fixed_design_variables={"bad": "entry"}, + ) + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") @@ -638,16 +653,21 @@ def test_reactor_figure_drawing_bad_sens_names(self): ) with self.assertRaisesRegex( - ValueError, "Sensitivity design variables do not all appear in the results object keys." + ValueError, + "Sensitivity design variables do not all appear in the results object keys.", ): - doe_obj.draw_factorial_figure(sensitivity_design_variables={"bad": "entry"}, fixed_design_variables={"CA[0]": 1}) - + doe_obj.draw_factorial_figure( + sensitivity_design_variables={"bad": "entry"}, + fixed_design_variables={"CA[0]": 1}, + ) @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_get_FIM_without_FIM(self): fd_method = "central" obj_used = "trace" - flag_val = 0 # Value for faulty model build mode - 5: Mismatch error and output length + flag_val = ( + 0 # Value for faulty model build mode - 5: Mismatch error and output length + ) experiment = FullReactorExperiment(data_ex, 10, 3) @@ -665,22 +685,24 @@ def test_reactor_check_get_FIM_without_FIM(self): L_LB=1e-7, solver=None, tee=False, - args={'flag': flag_val}, + args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) with self.assertRaisesRegex( - RuntimeError, "Model provided does not have variable `fim`. Please make sure the model is built properly before calling `get_FIM`" + RuntimeError, + "Model provided does not have variable `fim`. Please make sure the model is built properly before calling `get_FIM`", ): doe_obj.get_FIM() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_get_sens_mat_without_model(self): fd_method = "central" obj_used = "trace" - flag_val = 0 # Value for faulty model build mode - 5: Mismatch error and output length + flag_val = ( + 0 # Value for faulty model build mode - 5: Mismatch error and output length + ) experiment = FullReactorExperiment(data_ex, 10, 3) @@ -698,22 +720,24 @@ def test_reactor_check_get_sens_mat_without_model(self): L_LB=1e-7, solver=None, tee=False, - args={'flag': flag_val}, + args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) with self.assertRaisesRegex( - RuntimeError, "Model provided does not have variable `sensitivity_jacobian`. Please make sure the model is built properly before calling `get_sensitivity_matrix`" + RuntimeError, + "Model provided does not have variable `sensitivity_jacobian`. Please make sure the model is built properly before calling `get_sensitivity_matrix`", ): doe_obj.get_sensitivity_matrix() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_get_exp_inputs_without_model(self): fd_method = "central" obj_used = "trace" - flag_val = 0 # Value for faulty model build mode - 5: Mismatch error and output length + flag_val = ( + 0 # Value for faulty model build mode - 5: Mismatch error and output length + ) experiment = FullReactorExperiment(data_ex, 10, 3) @@ -731,22 +755,24 @@ def test_reactor_check_get_exp_inputs_without_model(self): L_LB=1e-7, solver=None, tee=False, - args={'flag': flag_val}, + args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) with self.assertRaisesRegex( - RuntimeError, "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + RuntimeError, + "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`", ): doe_obj.get_experiment_input_values() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_get_exp_outputs_without_model(self): fd_method = "central" obj_used = "trace" - flag_val = 0 # Value for faulty model build mode - 5: Mismatch error and output length + flag_val = ( + 0 # Value for faulty model build mode - 5: Mismatch error and output length + ) experiment = FullReactorExperiment(data_ex, 10, 3) @@ -764,22 +790,24 @@ def test_reactor_check_get_exp_outputs_without_model(self): L_LB=1e-7, solver=None, tee=False, - args={'flag': flag_val}, + args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) with self.assertRaisesRegex( - RuntimeError, "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + RuntimeError, + "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`", ): doe_obj.get_experiment_output_values() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_get_unknown_params_without_model(self): fd_method = "central" obj_used = "trace" - flag_val = 0 # Value for faulty model build mode - 5: Mismatch error and output length + flag_val = ( + 0 # Value for faulty model build mode - 5: Mismatch error and output length + ) experiment = FullReactorExperiment(data_ex, 10, 3) @@ -797,22 +825,24 @@ def test_reactor_check_get_unknown_params_without_model(self): L_LB=1e-7, solver=None, tee=False, - args={'flag': flag_val}, + args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) with self.assertRaisesRegex( - RuntimeError, "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + RuntimeError, + "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`", ): doe_obj.get_unknown_parameter_values() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_get_meas_error_without_model(self): fd_method = "central" obj_used = "trace" - flag_val = 0 # Value for faulty model build mode - 5: Mismatch error and output length + flag_val = ( + 0 # Value for faulty model build mode - 5: Mismatch error and output length + ) experiment = FullReactorExperiment(data_ex, 10, 3) @@ -830,17 +860,17 @@ def test_reactor_check_get_meas_error_without_model(self): L_LB=1e-7, solver=None, tee=False, - args={'flag': flag_val}, + args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) with self.assertRaisesRegex( - RuntimeError, "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`" + RuntimeError, + "Model provided does not have expected structure. Please make sure model is built properly before calling `get_experiment_input_values`", ): doe_obj.get_measurement_error_values() - if __name__ == "__main__": unittest.main() From 846d8589c4df4c3d564900097690edb20d3fc5c8 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 12 Jul 2024 13:51:21 -0400 Subject: [PATCH 1915/3044] Avoid use of deprecated method for `scipy.linprog` --- pyomo/contrib/pyros/uncertainty_sets.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 7c95bef4a2c..84b6752991e 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1336,7 +1336,7 @@ def _validate(self): c=np.zeros(self.coefficients_mat.shape[1]), A_ub=self.coefficients_mat, b_ub=self.rhs_vec, - method="simplex", + method="highs", bounds=(None, None), ) @@ -1344,7 +1344,7 @@ def _validate(self): if res.status == 1 or res.status == 4: raise ValueError( "Could not verify nonemptiness of the " - "polyhedral set (`scipy.optimize.linprog(method=simplex)` " + "polyhedral set (`scipy.optimize.linprog(method='highs')` " f" status {res.status}) " ) elif res.status == 2: @@ -2105,7 +2105,7 @@ def compute_auxiliary_param_vals(self, point, solver=None): ), b_ub=np.full(2, self.beta * self.number_of_factors), bounds=(-1, 1), - method="simplex", + method="highs", ) # check termination From 6871ba619508ae6b9aea587ab8bf8f80d2959c71 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:52:59 -0400 Subject: [PATCH 1916/3044] Fixed small typo --- pyomo/contrib/doe/doe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 3686dafdd51..d8f773879ee 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -2004,7 +2004,7 @@ def get_sensitivity_matrix(self, model=None): if model is None: model = self.model - if not hasattr(model, "sensitivity_jacboian"): + if not hasattr(model, "sensitivity_jacobian"): raise RuntimeError( "Model provided does not have variable `sensitivity_jacobian`. Please make sure the model is built properly before calling `get_sensitivity_matrix`" ) From 4a341f67e1ab0fe3b4e5b5cf5e6d95968040ec90 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:55:58 -0400 Subject: [PATCH 1917/3044] Delete pyomo/contrib/doe/examples/PyomoDoE-plots.ipynb Removed old file --- .../contrib/doe/examples/PyomoDoE-plots.ipynb | 9116 ----------------- 1 file changed, 9116 deletions(-) delete mode 100644 pyomo/contrib/doe/examples/PyomoDoE-plots.ipynb diff --git a/pyomo/contrib/doe/examples/PyomoDoE-plots.ipynb b/pyomo/contrib/doe/examples/PyomoDoE-plots.ipynb deleted file mode 100644 index f2d83550458..00000000000 --- a/pyomo/contrib/doe/examples/PyomoDoE-plots.ipynb +++ /dev/null @@ -1,9116 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 60, - "id": "22f7850b-5a09-4b71-a9cf-64555f21f7f5", - "metadata": {}, - "outputs": [], - "source": [ - "import pyomo.environ as pyo\n", - "from pyomo.dae import ContinuousSet, DerivativeVar\n", - "from pyomo.contrib.doe import DesignOfExperiments, MeasurementVariables, DesignVariables, ModelOptionLib\n", - "import numpy as np\n", - "from random import sample\n", - "from matplotlib import pyplot as plt" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "2607b8c0-527c-4a6e-b73d-989b1471aa8f", - "metadata": {}, - "outputs": [], - "source": [ - "def create_model(\n", - " mod=None,\n", - " model_option=\"stage2\",\n", - " control_time=[0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1],\n", - " control_val=None,\n", - " t_range=[0.0, 1],\n", - " CA_init=1,\n", - " C_init=0.1,\n", - "):\n", - " \"\"\"\n", - " This is an example user model provided to DoE library.\n", - " It is a dynamic problem solved by Pyomo.DAE.\n", - "\n", - " Arguments\n", - " ---------\n", - " mod: Pyomo model. If None, a Pyomo concrete model is created\n", - " model_option: choose from the 3 options in model_option\n", - " if ModelOptionLib.parmest, create a process model.\n", - " if ModelOptionLib.stage1, create the global model.\n", - " if ModelOptionLib.stage2, add model variables and constraints for block.\n", - " control_time: a list of control timepoints\n", - " control_val: control design variable values T at corresponding timepoints\n", - " t_range: time range, h\n", - " CA_init: time-independent design (control) variable, an initial value for CA\n", - " C_init: An initial value for C\n", - "\n", - " Return\n", - " ------\n", - " m: a Pyomo.DAE model\n", - " \"\"\"\n", - "\n", - " theta = {\"A1\": 84.79, \"A2\": 371.72, \"E1\": 7.78, \"E2\": 15.05}\n", - "\n", - " model_option = ModelOptionLib(model_option)\n", - "\n", - " if model_option == ModelOptionLib.parmest:\n", - " mod = pyo.ConcreteModel()\n", - " return_m = True\n", - " elif model_option == ModelOptionLib.stage1 or model_option == ModelOptionLib.stage2:\n", - " if not mod:\n", - " raise ValueError(\n", - " \"If model option is stage1 or stage2, a created model needs to be provided.\"\n", - " )\n", - " return_m = False\n", - " else:\n", - " raise ValueError(\n", - " \"model_option needs to be defined as parmest,stage1, or stage2.\"\n", - " )\n", - "\n", - " if not control_val:\n", - " control_val = [300] * 9\n", - "\n", - " controls = {}\n", - " for i, t in enumerate(control_time):\n", - " controls[t] = control_val[i]\n", - "\n", - " mod.t0 = pyo.Set(initialize=[0])\n", - " mod.t_con = pyo.Set(initialize=control_time)\n", - " mod.CA0 = pyo.Var(\n", - " mod.t0, initialize=CA_init, bounds=(1.0, 5.0), within=pyo.NonNegativeReals\n", - " ) # mol/L\n", - "\n", - " # check if control_time is in time range\n", - " assert (\n", - " control_time[0] >= t_range[0] and control_time[-1] <= t_range[1]\n", - " ), \"control time is outside time range.\"\n", - "\n", - " if model_option == ModelOptionLib.stage1:\n", - " mod.T = pyo.Var(\n", - " mod.t_con,\n", - " initialize=controls,\n", - " bounds=(300, 700),\n", - " within=pyo.NonNegativeReals,\n", - " )\n", - " return\n", - "\n", - " else:\n", - " para_list = [\"A1\", \"A2\", \"E1\", \"E2\"]\n", - "\n", - " ### Add variables\n", - " mod.CA_init = CA_init\n", - " mod.para_list = para_list\n", - "\n", - " # timepoints\n", - " mod.t = ContinuousSet(bounds=t_range, initialize=control_time)\n", - "\n", - " # time-dependent design variable, initialized with the first control value\n", - " def T_initial(m, t):\n", - " if t in m.t_con:\n", - " return controls[t]\n", - " else:\n", - " # count how many control points are before the current t;\n", - " # locate the nearest neighbouring control point before this t\n", - " neighbour_t = max(tc for tc in control_time if tc < t)\n", - " return controls[neighbour_t]\n", - "\n", - " mod.T = pyo.Var(\n", - " mod.t, initialize=T_initial, bounds=(300, 700), within=pyo.NonNegativeReals\n", - " )\n", - "\n", - " mod.R = 8.31446261815324 # J / K / mole\n", - "\n", - " # Define parameters as Param\n", - " mod.A1 = pyo.Var(initialize=theta[\"A1\"])\n", - " mod.A2 = pyo.Var(initialize=theta[\"A2\"])\n", - " mod.E1 = pyo.Var(initialize=theta[\"E1\"])\n", - " mod.E2 = pyo.Var(initialize=theta[\"E2\"])\n", - "\n", - " # Concentration variables under perturbation\n", - " mod.C_set = pyo.Set(initialize=[\"CA\", \"CB\", \"CC\"])\n", - " mod.C = pyo.Var(\n", - " mod.C_set, mod.t, initialize=C_init, within=pyo.NonNegativeReals\n", - " )\n", - "\n", - " # time derivative of C\n", - " mod.dCdt = DerivativeVar(mod.C, wrt=mod.t)\n", - "\n", - " # kinetic parameters\n", - " def kp1_init(m, t):\n", - " return m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t]))\n", - "\n", - " def kp2_init(m, t):\n", - " return m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t]))\n", - "\n", - " mod.kp1 = pyo.Var(mod.t, initialize=kp1_init)\n", - " mod.kp2 = pyo.Var(mod.t, initialize=kp2_init)\n", - "\n", - " def T_control(m, t):\n", - " \"\"\"\n", - " T at interval timepoint equal to the T of the control time point at the beginning of this interval\n", - " Count how many control points are before the current t;\n", - " locate the nearest neighbouring control point before this t\n", - " \"\"\"\n", - " if t in m.t_con:\n", - " return pyo.Constraint.Skip\n", - " else:\n", - " neighbour_t = max(tc for tc in control_time if tc < t)\n", - " return m.T[t] == m.T[neighbour_t]\n", - "\n", - " def cal_kp1(m, t):\n", - " \"\"\"\n", - " Create the perturbation parameter sets\n", - " m: model\n", - " t: time\n", - " \"\"\"\n", - " # LHS: 1/h\n", - " # RHS: 1/h*(kJ/mol *1000J/kJ / (J/mol/K) / K)\n", - " return m.kp1[t] == m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t]))\n", - "\n", - " def cal_kp2(m, t):\n", - " \"\"\"\n", - " Create the perturbation parameter sets\n", - " m: model\n", - " t: time\n", - " \"\"\"\n", - " # LHS: 1/h\n", - " # RHS: 1/h*(kJ/mol *1000J/kJ / (J/mol/K) / K)\n", - " return m.kp2[t] == m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t]))\n", - "\n", - " def dCdt_control(m, y, t):\n", - " \"\"\"\n", - " Calculate CA in Jacobian matrix analytically\n", - " y: CA, CB, CC\n", - " t: timepoints\n", - " \"\"\"\n", - " if y == \"CA\":\n", - " return m.dCdt[y, t] == -m.kp1[t] * m.C[\"CA\", t]\n", - " elif y == \"CB\":\n", - " return m.dCdt[y, t] == m.kp1[t] * m.C[\"CA\", t] - m.kp2[t] * m.C[\"CB\", t]\n", - " elif y == \"CC\":\n", - " return pyo.Constraint.Skip\n", - "\n", - " def alge(m, t):\n", - " \"\"\"\n", - " The algebraic equation for mole balance\n", - " z: m.pert\n", - " t: time\n", - " \"\"\"\n", - " return m.C[\"CA\", t] + m.C[\"CB\", t] + m.C[\"CC\", t] == m.CA0[0]\n", - "\n", - " # Control time\n", - " mod.T_rule = pyo.Constraint(mod.t, rule=T_control)\n", - "\n", - " # calculating C, Jacobian, FIM\n", - " mod.k1_pert_rule = pyo.Constraint(mod.t, rule=cal_kp1)\n", - " mod.k2_pert_rule = pyo.Constraint(mod.t, rule=cal_kp2)\n", - " mod.dCdt_rule = pyo.Constraint(mod.C_set, mod.t, rule=dCdt_control)\n", - "\n", - " mod.alge_rule = pyo.Constraint(mod.t, rule=alge)\n", - "\n", - " # B.C.\n", - " mod.C[\"CB\", 0.0].fix(0.0)\n", - " mod.C[\"CC\", 0.0].fix(0.0)\n", - "\n", - " if return_m:\n", - " return mod" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "e76ef4de-7319-4047-9cc8-b61c8ce2c57f", - "metadata": {}, - "outputs": [], - "source": [ - "def disc_for_measure(m, nfe=32, block=True):\n", - " \"\"\"Pyomo.DAE discretization\n", - "\n", - " Arguments\n", - " ---------\n", - " m: Pyomo model\n", - " nfe: number of finite elements b\n", - " block: if True, the input model has blocks\n", - " \"\"\"\n", - " discretizer = pyo.TransformationFactory(\"dae.collocation\")\n", - " if block:\n", - " for s in range(len(m.block)):\n", - " discretizer.apply_to(m.block[s], nfe=nfe, ncp=3, wrt=m.block[s].t)\n", - " else:\n", - " discretizer.apply_to(m, nfe=nfe, ncp=3, wrt=m.t)\n", - " return m" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "42186821-ccaf-4a93-bf0e-86d8cd6098c0", - "metadata": {}, - "outputs": [], - "source": [ - " # Control time set [h]\n", - " t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]\n", - " # Define parameter nominal value\n", - " parameter_dict = {\"A1\": 85, \"A2\": 370, \"E1\": 8, \"E2\": 15}\n", - "\n", - " # Define measurement object\n", - " measurements = MeasurementVariables()\n", - " measurements.add_variables(\n", - " \"C\", # measurement variable name\n", - " indices={\n", - " 0: [\"CA\", \"CB\", \"CC\"],\n", - " 1: t_control,\n", - " }, # 0,1 are indices of the index sets\n", - " time_index_position=1,\n", - " )\n", - "\n", - " # design object\n", - " exp_design = DesignVariables()\n", - "\n", - " # add CAO as design variable\n", - " exp_design.add_variables(\n", - " \"CA0\", # design variable name\n", - " indices={0: [0]}, # index dictionary\n", - " time_index_position=0, # time index position\n", - " values=[5], # design variable values\n", - " lower_bounds=1, # design variable lower bounds\n", - " upper_bounds=5, # design variable upper bounds\n", - " )\n", - "\n", - " # add T as design variable\n", - " exp_design.add_variables(\n", - " \"T\", # design variable name\n", - " indices={0: t_control}, # index dictionary\n", - " time_index_position=0, # time index position\n", - " values=[\n", - " 570,\n", - " 300,\n", - " 300,\n", - " 300,\n", - " 300,\n", - " 300,\n", - " 300,\n", - " 300,\n", - " 300,\n", - " ], # same length with t_control\n", - " lower_bounds=300, # design variable lower bounds\n", - " upper_bounds=700, # design variable upper bounds\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "70a60f53-bee2-401b-8c23-5d3998447946", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 2.67e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 4.76e+01 3.85e+02 -1.0 2.67e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.83e+01 2.78e+02 -1.0 5.91e+01 - 6.65e-02 9.90e-01h 1\n", - " 3 0.0000000e+00 1.33e-01 7.65e+01 -1.0 1.09e+01 - 7.59e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 4.86e-06 2.23e+02 -1.0 8.75e-02 - 9.91e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (287125)\n", - " 5 0.0000000e+00 3.41e-13 1.00e-06 -1.0 2.73e-06 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.4215102496496944e-13 3.4106051316484809e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.4215102496496944e-13 3.4106051316484809e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.173\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.1\n" - ] - } - ], - "source": [ - " doe_object = DesignOfExperiments(\n", - " parameter_dict, # parameter dictionary\n", - " exp_design, # DesignVariables object\n", - " measurements, # MeasurementVariables object\n", - " create_model, # create model function\n", - " discretize_model=disc_for_measure, # discretize model function\n", - " )\n", - "\n", - " result = doe_object.compute_FIM(\n", - " mode=\"sequential_finite\", # calculation mode\n", - " scale_nominal_param_value=True, # scale nominal parameter value\n", - " formula=\"central\", # formula for finite difference\n", - " )\n", - "\n", - " result.result_analysis()\n" - ] - }, - { - "cell_type": "code", - "execution_count": 54, - "id": "dd45bb25-e3e4-4438-9f5e-b1544346d262", - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 2.78e-01 9.66e+00 -1.0 3.72e+00 - 1.10e-01 9.90e-01h 1\n", - " 2 0.0000000e+00 2.78e-03 7.85e-02 -1.0 2.50e-01 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 2.23e-09 1.61e-04 -1.0 2.50e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 3\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.2346848815857356e-09 2.2346848815857356e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.2346848815857356e-09 2.2346848815857356e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 4\n", - "Number of objective gradient evaluations = 4\n", - "Number of equality constraint evaluations = 4\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 4\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 3\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.083\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 0.7\n", - "[[ 23.4919678 1.90035398 -73.31983907 -11.45520905]\n", - " [ 1.90035398 18.78885651 -5.92812017 -113.31030691]\n", - " [ -73.31983907 -5.92812017 228.84307107 35.73423936]\n", - " [ -11.45520905 -113.31030691 35.73423936 683.34280812]]\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.27e+02 2.27e+01 -1.0 1.97e+02 - 1.49e-02 3.57e-01f 1\n", - " 2 0.0000000e+00 1.18e+02 2.13e+01 -1.0 1.27e+02 - 2.20e-01 6.59e-02h 1\n", - " 3 0.0000000e+00 1.18e+02 1.57e+04 -1.0 1.18e+02 - 4.58e-01 1.07e-03h 1\n", - " 4 0.0000000e+00 1.18e+02 8.18e+08 -1.0 1.18e+02 - 5.67e-01 1.08e-05h 1\n", - " 5r 0.0000000e+00 1.18e+02 1.00e+03 2.1 0.00e+00 - 0.00e+00 5.40e-08R 2\n", - " 6r 0.0000000e+00 1.08e+02 8.37e+03 2.1 2.06e+04 - 5.73e-02 5.68e-03f 1\n", - " 7r 0.0000000e+00 9.81e+01 9.52e+03 2.1 1.83e+03 - 1.09e-01 5.87e-03f 1\n", - " 8r 0.0000000e+00 3.98e+01 5.75e+03 2.1 1.58e+03 - 8.18e-02 4.13e-02f 1\n", - " 9r 0.0000000e+00 2.03e+01 6.57e+03 1.4 9.62e+02 - 1.41e-01 2.07e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 3.00e+00 8.02e+03 1.4 3.63e+02 - 3.32e-01 5.56e-02f 1\n", - " 11r 0.0000000e+00 1.44e+00 5.13e+03 1.4 9.12e+00 - 6.26e-01 2.87e-01f 1\n", - " 12r 0.0000000e+00 8.25e-01 3.35e+03 1.4 6.02e+00 - 3.97e-01 3.54e-01f 1\n", - " 13r 0.0000000e+00 3.34e-01 1.92e+03 1.4 1.38e+00 - 1.00e+00 5.65e-01f 1\n", - " 14r 0.0000000e+00 8.90e-02 2.52e+01 1.4 4.37e-01 - 1.00e+00 1.00e+00f 1\n", - " 15r 0.0000000e+00 6.06e-02 6.19e+02 -0.7 3.41e+00 - 9.23e-01 2.41e-01f 1\n", - " 16r 0.0000000e+00 2.81e-02 7.76e+02 -0.7 5.14e+00 - 9.06e-01 4.55e-01f 1\n", - " 17r 0.0000000e+00 7.09e-03 4.75e+02 -0.7 2.70e+00 - 1.00e+00 7.43e-01f 1\n", - " 18r 0.0000000e+00 2.33e-03 8.38e+02 -0.7 5.62e-01 - 1.00e+00 6.77e-01f 1\n", - " 19r 0.0000000e+00 4.95e-04 3.26e+02 -0.7 3.01e-01 - 1.00e+00 8.74e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 3.22e-04 7.71e-03 -0.7 3.84e-02 - 1.00e+00 1.00e+00f 1\n", - " 21r 0.0000000e+00 5.09e-05 3.36e+02 -3.2 4.74e-02 - 1.00e+00 7.57e-01f 1\n", - " 22r 0.0000000e+00 2.83e-05 9.25e+02 -3.2 2.60e-02 - 9.52e-01 6.49e-01f 1\n", - " 23r 0.0000000e+00 8.26e-06 8.54e+02 -3.2 4.25e-03 - 9.08e-01 7.50e-01f 1\n", - " 24r 0.0000000e+00 1.07e-06 3.61e+01 -3.2 1.20e-03 - 9.68e-01 1.00e+00f 1\n", - " 25r 0.0000000e+00 1.06e-06 1.08e-07 -3.2 3.22e-06 - 1.00e+00 1.00e+00f 1\n", - " 26r 0.0000000e+00 1.25e-08 1.36e+00 -7.2 1.30e-05 - 1.00e+00 9.91e-01f 1\n", - " 27r 0.0000000e+00 4.44e-09 9.66e+01 -7.2 1.80e-07 - 1.00e+00 6.43e-01f 1\n", - "\n", - "Number of Iterations....: 27\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 3.8353336372415511e-09 4.4417719505051396e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 3.8353336372415511e-09 4.4417719505051396e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 30\n", - "Number of objective gradient evaluations = 7\n", - "Number of equality constraint evaluations = 30\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 29\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 27\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.415\n", - "Total CPU secs in NLP function evaluations = 0.011\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.7\n", - "[[ 22.79600957 1.98478998 -70.74572684 -11.61551788]\n", - " [ 1.98478998 18.42336506 -6.01361754 -110.33089639]\n", - " [ -70.74572684 -6.01361754 219.92911039 35.57976045]\n", - " [ -11.61551788 -110.33089639 35.57976045 662.60606496]]\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.16e+02 2.62e+01 -1.0 3.97e+02 - 7.43e-03 2.04e-01f 1\n", - " 2 0.0000000e+00 3.03e+02 2.51e+01 -1.0 3.16e+02 - 1.45e-01 4.23e-02h 1\n", - " 3 0.0000000e+00 3.03e+02 8.97e+03 -1.0 3.03e+02 - 2.61e-01 7.62e-04h 1\n", - " 4r 0.0000000e+00 3.03e+02 1.00e+03 2.5 0.00e+00 - 0.00e+00 4.81e-07R 5\n", - " 5r 0.0000000e+00 2.80e+02 2.51e+04 2.5 1.49e+04 - 8.36e-04 2.01e-02f 1\n", - " 6r 0.0000000e+00 2.77e+02 4.72e+04 2.5 1.15e+03 - 4.92e-01 2.99e-03f 1\n", - " 7r 0.0000000e+00 1.44e+02 3.94e+04 2.5 8.87e+02 - 1.66e-01 1.77e-01f 1\n", - " 8r 0.0000000e+00 8.40e+01 2.88e+04 2.5 7.02e+02 - 5.33e-01 1.02e-01f 1\n", - " 9r 0.0000000e+00 7.98e+00 2.05e+04 2.5 2.25e+02 - 6.82e-01 3.66e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 2.42e+00 1.01e+04 1.8 1.19e+01 - 1.00e+00 5.27e-01f 1\n", - " 11r 0.0000000e+00 8.94e-01 1.27e+03 1.8 3.82e+00 - 1.00e+00 8.53e-01f 1\n", - " 12r 0.0000000e+00 5.02e-01 1.87e+03 1.1 1.61e+00 - 1.00e+00 5.52e-01f 1\n", - " 13r 0.0000000e+00 2.92e-01 1.58e+03 1.1 3.89e+00 - 1.00e+00 5.66e-01f 1\n", - " 14r 0.0000000e+00 2.10e-01 3.14e+00 1.1 1.42e+00 - 1.00e+00 1.00e+00f 1\n", - " 15r 0.0000000e+00 2.28e-01 5.05e+02 -1.0 9.32e-01 - 9.06e-01 6.86e-01f 1\n", - " 16r 0.0000000e+00 1.61e-01 3.01e+03 -1.0 4.16e+01 - 4.10e-01 6.33e-02f 1\n", - " 17r 0.0000000e+00 3.96e-02 2.90e+03 -1.0 3.34e+01 - 4.84e-01 2.04e-01f 1\n", - " 18r 0.0000000e+00 3.80e-02 2.77e+03 -1.0 1.90e+01 - 2.00e-02 3.94e-02f 1\n", - " 19r 0.0000000e+00 3.76e-02 3.37e+03 -1.0 1.24e+01 - 1.00e+00 1.20e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 3.18e-02 3.17e+03 -1.0 6.34e+00 - 1.00e+00 1.59e-01f 1\n", - " 21r 0.0000000e+00 1.30e-02 1.27e+03 -1.0 2.32e+00 - 1.00e+00 6.13e-01f 1\n", - " 22r 0.0000000e+00 2.75e-03 2.56e+02 -1.0 7.79e-01 - 1.00e+00 8.72e-01f 1\n", - " 23r 0.0000000e+00 1.24e-03 1.45e-02 -1.0 8.66e-02 - 1.00e+00 1.00e+00f 1\n", - " 24r 0.0000000e+00 4.49e-04 9.60e+02 -3.9 4.13e-02 - 1.00e+00 6.37e-01f 1\n", - " 25r 0.0000000e+00 2.39e-04 1.95e+03 -3.9 3.96e-02 - 9.64e-01 4.59e-01f 1\n", - " 26r 0.0000000e+00 6.90e-05 1.04e+03 -3.9 3.37e-03 - 9.83e-01 6.98e-01f 1\n", - " 27r 0.0000000e+00 2.36e-05 1.29e+03 -3.9 1.13e-03 - 9.70e-01 6.16e-01f 1\n", - " 28r 0.0000000e+00 4.53e-06 1.22e+03 -3.9 4.33e-04 - 1.00e+00 6.79e-01f 1\n", - " 29r 0.0000000e+00 1.29e-06 1.43e+03 -3.9 1.39e-04 - 1.00e+00 5.77e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 4.96e-07 7.07e-06 -3.9 5.89e-05 - 1.00e+00 1.00e+00f 1\n", - " 31r 0.0000000e+00 1.34e-07 1.42e+03 -5.8 4.88e-06 - 1.00e+00 5.68e-01f 1\n", - " 32r 0.0000000e+00 4.98e-08 1.83e+03 -5.8 2.01e-06 - 9.73e-01 6.09e-01f 1\n", - " 33r 0.0000000e+00 1.06e-08 2.39e+02 -5.8 8.73e-07 - 9.85e-01 9.28e-01f 1\n", - "\n", - "Number of Iterations....: 33\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 6.2369178856385201e-09 1.0648229654397684e-08\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 6.2369178856385201e-09 1.0648229654397684e-08\n", - "\n", - "\n", - "Number of objective function evaluations = 39\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 39\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 35\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 33\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.459\n", - "Total CPU secs in NLP function evaluations = 0.090\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.0\n", - "[[ 22.63335364 1.90230787 -70.37558022 -11.25402903]\n", - " [ 1.90230787 18.17182952 -5.82082335 -109.34769934]\n", - " [ -70.37558022 -5.82082335 219.13827714 34.79632173]\n", - " [ -11.25402903 -109.34769934 34.79632173 658.86364692]]\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 9.05e-01 1.87e+02 -1.0 1.12e+01 - 5.21e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 9.03e-03 1.85e+00 -1.0 8.13e-01 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 7.20e-09 5.47e-05 -1.0 8.12e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 3\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 7.2006525186907311e-09 7.2006525186907311e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 7.2006525186907311e-09 7.2006525186907311e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 4\n", - "Number of objective gradient evaluations = 4\n", - "Number of equality constraint evaluations = 4\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 4\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 3\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.079\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 0.7\n", - "[[ 31.19226828 2.38044072 -98.01668477 -14.34224445]\n", - " [ 2.38044072 24.31185934 -7.46787881 -146.52347138]\n", - " [ -98.01668477 -7.46787881 308.05221217 44.99368637]\n", - " [ -14.34224445 -146.52347138 44.99368637 883.07370235]]\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.06e+02 8.67e+01 -1.0 1.97e+02 - 5.21e-03 4.63e-01f 1\n", - " 2 0.0000000e+00 1.03e+02 8.41e+01 -1.0 1.06e+02 - 1.23e-01 3.00e-02h 1\n", - " 3 0.0000000e+00 1.03e+02 1.14e+04 -1.0 1.03e+02 - 2.02e-01 3.66e-04h 1\n", - " 4r 0.0000000e+00 1.03e+02 1.00e+03 2.0 0.00e+00 - 0.00e+00 4.59e-07R 4\n", - " 5r 0.0000000e+00 9.98e+01 3.34e+03 2.0 1.78e+04 - 5.49e-03 2.23e-03f 1\n", - " 6r 0.0000000e+00 9.58e+01 1.77e+04 1.3 1.22e+04 - 2.58e-02 2.86e-03f 1\n", - " 7r 0.0000000e+00 7.91e+01 4.28e+04 1.3 3.59e+03 - 3.01e-02 5.36e-03f 1\n", - " 8r 0.0000000e+00 3.41e+01 4.38e+04 1.3 3.03e+03 - 3.45e-02 1.54e-02f 1\n", - " 9r 0.0000000e+00 2.48e+01 3.96e+04 1.3 1.85e+03 - 2.78e-01 5.04e-03f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 5.64e+00 3.44e+04 1.3 2.01e+02 - 1.86e-01 1.23e-01f 1\n", - " 11r 0.0000000e+00 2.42e+00 1.91e+04 1.3 1.70e+01 - 5.49e-01 2.58e-01f 1\n", - " 12r 0.0000000e+00 1.24e+00 8.44e+03 1.3 7.64e+00 - 6.61e-01 4.56e-01f 1\n", - " 13r 0.0000000e+00 1.19e+00 2.99e+03 1.3 2.61e+00 - 5.40e-01 3.71e-01f 1\n", - " 14r 0.0000000e+00 1.15e+00 4.69e+03 1.3 1.51e+00 - 8.83e-01 6.33e-01f 1\n", - " 15r 0.0000000e+00 1.13e+00 4.99e+03 1.3 6.01e-01 - 5.32e-01 7.72e-01f 1\n", - " 16r 0.0000000e+00 1.13e+00 1.09e+00 1.3 1.75e-01 - 1.00e+00 1.00e+00f 1\n", - " 17r 0.0000000e+00 1.62e+00 1.19e+02 -0.8 7.16e+00 - 6.51e-01 8.04e-01f 1\n", - " 18r 0.0000000e+00 1.29e+00 5.12e+02 -0.8 8.63e+00 - 3.56e-01 4.94e-01f 1\n", - " 19r 0.0000000e+00 7.07e-01 1.78e+02 -0.8 1.47e+01 - 5.56e-01 4.96e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 9.00e-03 8.30e+02 -0.8 2.77e+01 - 4.78e-01 3.14e-01f 1\n", - " 21r 0.0000000e+00 4.40e-03 2.34e+03 -0.8 1.62e+01 - 8.28e-01 5.79e-03f 1\n", - " 22r 0.0000000e+00 2.91e-03 1.69e+03 -0.8 2.80e+00 - 1.00e+00 2.71e-01f 1\n", - " 23r 0.0000000e+00 8.18e-04 5.53e+02 -0.8 1.23e+00 - 1.00e+00 6.90e-01f 1\n", - " 24r 0.0000000e+00 1.16e-03 1.32e-03 -0.8 2.40e-01 - 1.00e+00 1.00e+00f 1\n", - " 25r 0.0000000e+00 4.27e-04 4.03e+02 -3.3 2.88e-01 - 1.00e+00 7.55e-01f 1\n", - " 26r 0.0000000e+00 4.49e-05 3.83e+03 -3.3 7.00e-01 - 9.34e-01 1.68e-01f 1\n", - " 27r 0.0000000e+00 2.16e-06 1.67e+02 -3.3 2.07e-02 - 9.48e-01 9.55e-01f 1\n", - " 28r 0.0000000e+00 3.64e-08 1.90e-06 -3.3 8.95e-04 - 1.00e+00 1.00e+00f 1\n", - " 29r 0.0000000e+00 2.25e-09 2.47e-01 -7.5 1.53e-05 - 1.00e+00 9.98e-01f 1\n", - "\n", - "Number of Iterations....: 29\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.9395965331428192e-09 2.2481533234965274e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.9395965331428192e-09 2.2481533234965274e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 34\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 34\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 31\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 29\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.614\n", - "Total CPU secs in NLP function evaluations = 0.081\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.1\n", - "[[ 24.92864423 3.14036468 -74.84967466 -15.78502396]\n", - " [ 3.14036468 21.02243624 -8.23735511 -119.70877671]\n", - " [ -74.84967466 -8.23735511 227.82656607 43.60337618]\n", - " [ -15.78502396 -119.70877671 43.60337618 696.44301405]]\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.04e+02 4.34e+01 -1.0 3.97e+02 - 5.21e-03 2.34e-01f 1\n", - " 2 0.0000000e+00 2.98e+02 4.26e+01 -1.0 3.04e+02 - 7.70e-02 2.01e-02h 1\n", - " 3 0.0000000e+00 2.98e+02 7.36e+03 -1.0 2.98e+02 - 1.28e-01 2.60e-04h 1\n", - " 4r 0.0000000e+00 2.98e+02 1.00e+03 2.5 0.00e+00 - 0.00e+00 3.26e-07R 4\n", - " 5r 0.0000000e+00 2.77e+02 1.21e+04 2.5 1.42e+04 - 1.92e-03 2.08e-02f 1\n", - " 6r 0.0000000e+00 2.74e+02 1.33e+04 1.8 1.29e+03 - 1.28e-01 2.52e-03f 1\n", - " 7r 0.0000000e+00 2.26e+02 1.74e+04 1.8 2.17e+03 - 1.75e-01 2.27e-02f 1\n", - " 8r 0.0000000e+00 1.70e+02 1.94e+04 1.8 1.50e+03 - 3.05e-01 4.00e-02f 1\n", - " 9r 0.0000000e+00 8.81e+01 1.78e+04 1.8 7.79e+02 - 2.82e-01 1.08e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 2.94e+01 1.40e+04 1.8 2.94e+02 - 5.10e-01 2.10e-01f 1\n", - " 11r 0.0000000e+00 2.15e+01 1.17e+04 1.8 4.85e+01 - 1.00e+00 1.67e-01f 1\n", - " 12r 0.0000000e+00 5.21e+00 2.64e+03 1.8 2.09e+01 - 1.00e+00 7.79e-01f 1\n", - " 13r 0.0000000e+00 2.67e+00 3.16e+03 1.1 5.47e+00 - 9.78e-01 5.06e-01f 1\n", - " 14r 0.0000000e+00 1.98e+00 9.35e+02 1.1 2.78e+00 - 1.00e+00 8.27e-01f 1\n", - " 15r 0.0000000e+00 2.00e+00 2.79e+03 0.4 1.09e+00 - 9.98e-01 4.40e-01f 1\n", - " 16r 0.0000000e+00 2.00e+00 1.21e+03 0.4 3.21e+00 - 9.19e-01 6.28e-01f 1\n", - " 17r 0.0000000e+00 1.99e+00 1.22e+00 0.4 8.67e-01 - 1.00e+00 1.00e+00f 1\n", - " 18r 0.0000000e+00 2.00e+00 1.65e+03 -1.7 5.96e-01 - 9.91e-01 4.10e-01f 1\n", - " 19r 0.0000000e+00 2.00e+00 1.83e+03 -1.7 2.35e+01 - 4.40e-01 1.42e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 2.00e+00 1.60e+03 -1.7 1.13e+01 - 7.62e-03 9.12e-02f 1\n", - " 21r 0.0000000e+00 1.88e+00 1.11e+03 -1.7 7.12e+01 - 4.10e-04 1.86e-01f 1\n", - " 22r 0.0000000e+00 7.03e-01 3.48e+03 -1.7 8.30e+01 - 3.09e-02 3.12e-01f 1\n", - " 23r 0.0000000e+00 4.00e-01 3.15e+03 -1.7 6.39e+01 - 9.85e-02 1.02e-01f 1\n", - " 24r 0.0000000e+00 3.97e-01 3.03e+03 -1.7 5.91e+01 - 2.08e-02 1.19e-03f 1\n", - " 25r 0.0000000e+00 2.62e-01 2.91e+03 -1.7 5.95e+01 - 4.23e-02 4.88e-02f 1\n", - " 26r 0.0000000e+00 1.40e-01 2.63e+03 -1.7 5.67e+01 - 9.35e-02 4.62e-02f 1\n", - " 27r 0.0000000e+00 1.38e-01 1.95e+03 -1.7 5.44e+01 - 2.47e-01 7.95e-04f 1\n", - " 28r 0.0000000e+00 9.04e-02 1.90e+03 -1.7 5.42e+01 - 1.88e-01 1.90e-02f 1\n", - " 29r 0.0000000e+00 3.77e-02 3.31e+03 -1.7 4.98e+01 - 5.30e-01 2.27e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 6.02e-03 3.61e+03 -1.7 1.34e+01 - 9.70e-01 6.02e-02f 1\n", - " 31r 0.0000000e+00 5.14e-03 3.30e+03 -1.7 6.99e+00 - 1.00e+00 1.53e-01f 1\n", - " 32r 0.0000000e+00 3.66e-03 2.50e+03 -1.7 5.55e-01 - 1.00e+00 3.01e-01f 1\n", - " 33r 0.0000000e+00 5.42e-04 2.44e+02 -1.7 3.24e-01 - 1.00e+00 9.11e-01f 1\n", - " 34r 0.0000000e+00 2.39e-04 3.00e-03 -1.7 2.78e-02 - 1.00e+00 1.00e+00f 1\n", - " 35r 0.0000000e+00 8.49e-05 1.01e+03 -3.9 1.83e-02 - 1.00e+00 6.33e-01f 1\n", - " 36r 0.0000000e+00 6.04e-05 2.69e+03 -3.9 2.68e-02 - 9.77e-01 2.72e-01f 1\n", - " 37r 0.0000000e+00 1.04e-05 8.23e+02 -3.9 1.42e-03 - 1.00e+00 7.69e-01f 1\n", - " 38r 0.0000000e+00 1.46e-06 1.11e+03 -3.9 2.73e-04 - 1.00e+00 6.80e-01f 1\n", - " 39r 0.0000000e+00 9.63e-07 2.09e+03 -3.9 8.74e-05 - 1.00e+00 3.68e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 40r 0.0000000e+00 1.97e-07 3.12e-06 -3.9 5.52e-05 - 1.00e+00 1.00e+00f 1\n", - " 41r 0.0000000e+00 5.22e-08 1.13e+03 -5.8 5.75e-06 - 1.00e+00 6.12e-01f 1\n", - " 42r 0.0000000e+00 2.38e-08 1.61e+03 -5.8 4.28e-06 - 9.81e-01 6.72e-01f 1\n", - " 43r 0.0000000e+00 2.62e-09 1.78e-07 -5.8 5.81e-07 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 43\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.5727324704138637e-09 2.6229877388222538e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.5727324704138637e-09 2.6229877388222538e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 48\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 48\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 45\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 43\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.628\n", - "Total CPU secs in NLP function evaluations = 0.025\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.7\n", - "[[ 23.46474083 2.39802572 -71.51835509 -12.53162432]\n", - " [ 2.39802572 18.75861639 -6.50220745 -110.86000329]\n", - " [ -71.51835509 -6.50220745 220.70906678 36.55242777]\n", - " [ -12.53162432 -110.86000329 36.55242777 662.76125156]]\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.53e+00 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.54e-02 3.85e+00 -1.0 1.38e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.52e-04 4.39e+00 -1.0 1.39e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 1.21e-12 1.02e-06 -1.0 1.37e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.2079226507921703e-12 1.2079226507921703e-12\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.2079226507921703e-12 1.2079226507921703e-12\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.108\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.4\n", - "[[ 46.59286925 3.34061422 -147.41037617 -20.11631538]\n", - " [ 3.34061422 35.357865 -10.54739617 -212.94980026]\n", - " [-147.41037617 -10.54739617 466.47049433 63.51258081]\n", - " [ -20.11631538 -212.94980026 63.51258081 1282.53549019]]\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 6.79e+01 2.55e+02 -1.0 1.97e+02 - 2.54e-03 6.55e-01f 1\n", - " 2 0.0000000e+00 6.61e+01 2.48e+02 -1.0 6.79e+01 - 9.64e-02 2.70e-02h 1\n", - " 3 0.0000000e+00 6.61e+01 8.37e+03 -1.0 6.61e+01 - 1.53e-01 3.12e-04h 1\n", - " 4r 0.0000000e+00 6.61e+01 1.00e+03 1.8 0.00e+00 - 0.00e+00 3.91e-07R 4\n", - " 5r 0.0000000e+00 6.67e+01 5.63e+03 1.8 1.73e+04 - 7.67e-03 9.94e-04f 1\n", - " 6r 0.0000000e+00 6.61e+01 9.66e+03 1.8 1.05e+04 - 7.18e-03 1.92e-03f 1\n", - " 7r 0.0000000e+00 5.73e+01 2.19e+04 1.8 3.17e+03 - 2.31e-02 8.98e-03f 1\n", - " 8r 0.0000000e+00 4.55e+01 4.36e+04 1.8 2.06e+03 - 3.06e-02 6.44e-03f 1\n", - " 9r 0.0000000e+00 1.70e+01 4.20e+04 1.8 1.76e+03 - 2.61e-02 1.63e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 3.30e+00 3.66e+04 1.8 1.16e+03 - 1.46e-01 1.44e-02f 1\n", - " 11r 0.0000000e+00 3.25e+00 3.00e+04 1.8 3.01e+01 - 2.44e-01 4.88e-02f 1\n", - " 12r 0.0000000e+00 2.82e+00 3.21e+04 1.8 2.66e+01 - 1.32e-01 2.76e-01f 1\n", - " 13r 0.0000000e+00 2.37e+00 3.59e+04 1.8 1.50e+01 - 2.84e-01 4.41e-01f 1\n", - " 14r 0.0000000e+00 2.16e+00 1.80e+04 1.8 5.42e+00 - 4.72e-01 4.96e-01f 1\n", - " 15r 0.0000000e+00 2.03e+00 1.32e+04 1.8 2.30e+00 - 3.74e-01 7.02e-01f 1\n", - " 16r 0.0000000e+00 1.98e+00 1.17e+03 1.8 7.23e-01 - 8.25e-01 1.00e+00f 1\n", - " 17r 0.0000000e+00 2.82e+00 1.50e+02 1.1 1.05e+01 - 8.15e-01 1.00e+00f 1\n", - " 18r 0.0000000e+00 2.91e+00 7.78e+02 -0.3 1.91e+00 - 1.00e+00 6.05e-01f 1\n", - " 19r 0.0000000e+00 1.90e+00 4.96e+02 -0.3 2.51e+01 - 3.68e-01 5.07e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 8.26e-01 1.70e+02 -0.3 2.16e+01 - 6.29e-01 6.19e-01f 1\n", - " 21r 0.0000000e+00 2.72e-02 5.46e+01 -0.3 1.07e+01 - 9.66e-01 9.45e-01f 1\n", - " 22r 0.0000000e+00 2.41e-01 9.04e-02 -0.3 2.89e+00 - 1.00e+00 1.00e+00h 1\n", - " 23r 0.0000000e+00 1.84e-01 2.23e+02 -1.7 8.95e-01 - 9.77e-01 8.00e-01f 1\n", - " 24r 0.0000000e+00 9.24e-02 3.27e+03 -1.7 7.88e+01 - 4.30e-01 2.01e-02f 1\n", - " 25r 0.0000000e+00 6.91e-02 4.42e+03 -1.7 2.78e+01 - 1.00e+00 8.35e-02f 1\n", - " 26r 0.0000000e+00 6.19e-02 4.27e+03 -1.7 2.15e+01 - 1.00e+00 1.01e-01f 1\n", - " 27r 0.0000000e+00 5.33e-02 3.71e+03 -1.7 1.63e+01 - 1.00e+00 1.38e-01f 1\n", - " 28r 0.0000000e+00 4.09e-02 2.86e+03 -1.7 1.70e+00 - 1.00e+00 2.31e-01f 1\n", - " 29r 0.0000000e+00 5.26e-04 3.44e+01 -1.7 6.11e-01 - 1.00e+00 9.88e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 4.79e-05 1.18e-05 -1.7 7.08e-03 - 1.00e+00 1.00e+00f 1\n", - " 31r 0.0000000e+00 2.78e-06 4.99e+01 -3.8 9.31e-03 - 1.00e+00 9.51e-01f 1\n", - " 32r 0.0000000e+00 9.04e-07 1.47e+02 -3.8 1.14e-03 - 1.00e+00 6.33e-01f 1\n", - " 33r 0.0000000e+00 2.71e-07 1.48e-07 -3.8 1.85e-04 - 1.00e+00 1.00e+00f 1\n", - " 34r 0.0000000e+00 9.86e-09 2.31e+00 -8.5 6.77e-06 - 1.00e+00 9.30e-01f 1\n", - "\n", - "Number of Iterations....: 34\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.8581836027733516e-09 9.8581836027733516e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.8581836027733516e-09 9.8581836027733516e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 39\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 39\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 36\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 34\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.544\n", - "Total CPU secs in NLP function evaluations = 0.064\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.1\n", - "[[ 29.19391386 5.4515142 -83.05757061 -24.12403549]\n", - " [ 5.4515142 26.22057856 -12.68483021 -138.46453604]\n", - " [ -83.05757061 -12.68483021 243.6214775 59.65060539]\n", - " [ -24.12403549 -138.46453604 59.65060539 764.11690335]]\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 2.88e+02 1.06e+02 -1.0 3.97e+02 - 2.54e-03 2.75e-01f 1\n", - " 2 0.0000000e+00 2.84e+02 1.05e+02 -1.0 2.88e+02 - 5.84e-02 1.38e-02h 1\n", - " 3 0.0000000e+00 2.84e+02 6.74e+03 -1.0 2.84e+02 - 8.78e-02 1.63e-04h 1\n", - " 4r 0.0000000e+00 2.84e+02 1.00e+03 2.5 0.00e+00 - 0.00e+00 4.09e-07R 3\n", - " 5r 0.0000000e+00 2.65e+02 4.54e+03 2.5 1.27e+04 - 9.57e-03 2.21e-02f 1\n", - " 6r 0.0000000e+00 2.62e+02 5.22e+03 1.8 1.16e+03 - 1.22e-01 2.99e-03f 1\n", - " 7r 0.0000000e+00 2.17e+02 1.22e+04 1.8 2.24e+03 - 1.37e-01 2.14e-02f 1\n", - " 8r 0.0000000e+00 1.98e+02 2.11e+04 1.8 1.52e+03 - 3.43e-01 1.19e-02f 1\n", - " 9r 0.0000000e+00 7.11e+01 1.83e+04 1.8 8.57e+02 - 1.81e-01 1.62e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 3.55e+01 2.82e+04 1.8 3.09e+02 - 5.74e-01 1.19e-01f 1\n", - " 11r 0.0000000e+00 2.71e+01 2.37e+04 1.8 5.61e+01 - 2.77e-01 1.52e-01f 1\n", - " 12r 0.0000000e+00 1.86e+01 1.20e+04 1.8 3.65e+01 - 1.00e+00 2.34e-01f 1\n", - " 13r 0.0000000e+00 4.96e+00 1.82e+03 1.8 1.80e+01 - 1.00e+00 7.59e-01f 1\n", - " 14r 0.0000000e+00 3.88e+00 2.00e+03 1.1 8.16e+00 - 1.00e+00 6.28e-01f 1\n", - " 15r 0.0000000e+00 3.88e+00 2.12e+03 1.1 1.91e+00 - 1.00e+00 6.37e-01f 1\n", - " 16r 0.0000000e+00 3.89e+00 2.53e+01 1.1 5.29e-01 - 1.00e+00 1.00e+00f 1\n", - " 17r 0.0000000e+00 4.00e+00 9.24e+02 -1.0 6.41e+00 - 9.23e-01 3.65e-01f 1\n", - " 18r 0.0000000e+00 4.00e+00 6.06e+02 -1.0 8.54e+00 - 2.38e-01 6.12e-01f 1\n", - " 19r 0.0000000e+00 3.82e+00 2.67e+03 -1.0 1.38e+01 - 8.45e-03 4.34e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 3.49e+00 2.91e+03 -1.0 2.16e+01 - 1.81e-01 3.33e-01f 1\n", - " 21r 0.0000000e+00 3.04e+00 2.45e+03 -1.0 2.44e+01 - 2.81e-01 4.00e-01f 1\n", - " 22r 0.0000000e+00 2.49e+00 2.47e+03 -1.0 2.48e+01 - 1.11e-01 4.77e-01f 1\n", - " 23r 0.0000000e+00 2.02e+00 9.87e+02 -1.0 1.69e+01 - 5.96e-01 5.88e-01f 1\n", - " 24r 0.0000000e+00 1.58e+00 6.84e+02 -1.0 1.01e+01 - 7.58e-01 9.54e-01f 1\n", - " 25r 0.0000000e+00 1.54e+00 2.82e-03 -1.0 8.65e-01 - 1.00e+00 1.00e+00f 1\n", - " 26r 0.0000000e+00 1.55e+00 4.05e+02 -3.9 4.34e-01 - 9.88e-01 7.49e-01f 1\n", - " 27r 0.0000000e+00 6.78e-02 1.12e+03 -3.9 1.80e+03 - 2.85e-02 1.85e-02f 1\n", - " 28r 0.0000000e+00 6.78e-02 3.06e+03 -3.9 1.77e+03 - 5.48e-02 2.33e-06f 1\n", - " 29r 0.0000000e+00 6.78e-02 3.68e+03 -3.9 1.06e+03 - 9.19e-02 1.15e-04f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 4.53e-02 4.22e+03 -3.9 3.54e+02 - 1.00e+00 1.57e-02f 1\n", - " 31r 0.0000000e+00 3.59e-02 4.41e+03 -3.9 3.39e+02 - 1.00e+00 6.72e-03f 1\n", - " 32r 0.0000000e+00 4.50e-03 4.37e+03 -3.9 3.23e+02 - 1.00e+00 2.67e-02f 1\n", - " 33r 0.0000000e+00 3.36e-03 4.21e+03 -3.9 6.29e+00 - 1.00e+00 4.40e-02f 1\n", - " 34r 0.0000000e+00 1.89e-04 3.05e+02 -3.9 8.22e-01 - 1.00e+00 9.29e-01f 1\n", - " 35r 0.0000000e+00 6.15e-05 1.17e+03 -3.9 4.55e-02 - 1.00e+00 6.75e-01f 1\n", - " 36r 0.0000000e+00 3.79e-05 2.05e+03 -3.9 1.48e-02 - 1.00e+00 3.85e-01f 1\n", - " 37r 0.0000000e+00 1.94e-07 3.86e-06 -3.9 9.09e-03 - 1.00e+00 1.00e+00f 1\n", - " 38r 0.0000000e+00 6.81e-08 1.50e+03 -5.9 1.01e-05 - 1.00e+00 5.73e-01f 1\n", - " 39r 0.0000000e+00 2.35e-08 1.26e+03 -5.9 7.54e-06 - 9.51e-01 7.37e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 40r 0.0000000e+00 6.36e-09 4.69e+02 -5.9 1.34e-06 - 1.00e+00 7.63e-01f 1\n", - "\n", - "Number of Iterations....: 40\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 6.3620335666314531e-09 6.3620335666314531e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 6.3620335666314531e-09 6.3620335666314531e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 44\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 44\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 42\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 40\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.718\n", - "Total CPU secs in NLP function evaluations = 0.045\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.3\n", - "[[ 25.12751441 3.3894616 -73.80390505 -15.08681458]\n", - " [ 3.3894616 19.93219076 -7.86497679 -113.88461226]\n", - " [ -73.80390505 -7.86497679 223.85064824 40.06464174]\n", - " [ -15.08681458 -113.88461226 40.06464174 670.5564622 ]]\n" - ] - } - ], - "source": [ - "# Make a function that takes in experimental design and gives you a new doe object\n", - "def new_doe_object(Ca, T0):\n", - " t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]\n", - " parameter_dict = {\"A1\": 85, \"A2\": 370, \"E1\": 8, \"E2\": 15}\n", - " \n", - " measurements = MeasurementVariables()\n", - " measurements.add_variables(\"C\", indices={0: [\"CA\", \"CB\", \"CC\"], 1: t_control}, time_index_position=1)\n", - " \n", - " exp_design = DesignVariables()\n", - " exp_design.add_variables(\n", - " \"CA0\",\n", - " time_index_position=0,\n", - " # values=[Ca],\n", - " lower_bounds=1,\n", - " indices={0: [0]},\n", - " upper_bounds=5,\n", - " )\n", - " exp_design.add_variables(\n", - " \"T\",\n", - " indices={0: t_control},\n", - " time_index_position=0,\n", - " # values=[T0]*9,\n", - " lower_bounds=300,\n", - " upper_bounds=700,\n", - " )\n", - "\n", - " exp_design.update_values({\"CA0[0]\": Ca, \"T[0]\": T0, \"T[0.125]\": T0, \"T[0.25]\": T0, \"T[0.375]\": T0, \"T[0.5]\": T0, \"T[0.625]\": T0,\n", - " \"T[0.75]\": T0, \"T[0.875]\": T0, \"T[1]\": T0})\n", - "\n", - " prior_pass = [\n", - " [22.52943024, 1.84034314, -70.23273336, -11.09432962],\n", - " [1.84034314, 18.09848116, -5.73565034, -109.15866135],\n", - " [-70.23273336, -5.73565034, 218.94192843, 34.57680848],\n", - " [-11.09432962, -109.15866135, 34.57680848, 658.37644634]\n", - " ]\n", - " \n", - " doe_object = DesignOfExperiments(\n", - " parameter_dict,\n", - " exp_design, \n", - " measurements, \n", - " create_model,\n", - " prior_FIM=prior_pass,\n", - " discretize_model=disc_for_measure,\n", - " )\n", - "\n", - " result = doe_object.compute_FIM(\n", - " mode=\"sequential_finite\",\n", - " scale_nominal_param_value=True,\n", - " formula=\"central\",\n", - " )\n", - "\n", - " result.result_analysis()\n", - " \n", - " return result\n", - "\n", - "\n", - "# Discretize the experimental design space (make a linspace) for sensitivity analysis\n", - "T_vals = np.linspace(300, 700, 3)\n", - "C_vals = np.linspace(1, 5, 3)\n", - "\n", - "A_opt_vals = []\n", - "D_opt_vals = []\n", - "E_opt_vals = []\n", - "ME_opt_vals = []\n", - "for i in C_vals:\n", - " for j in T_vals:\n", - " fims = new_doe_object(i, j)\n", - " print(fims.FIM)\n", - " A_opt = np.trace(fims.FIM)\n", - " D_opt = np.linalg.det(fims.FIM)\n", - " E_opt = min(np.linalg.eigvals(fims.FIM))\n", - " ME_opt = np.linalg.cond(fims.FIM)\n", - " \n", - " A_opt_vals.append(np.log10(A_opt))\n", - " D_opt_vals.append(np.log10(D_opt))\n", - " E_opt_vals.append(np.log10(E_opt))\n", - " ME_opt_vals.append(np.log10(ME_opt))" - ] - }, - { - "cell_type": "code", - "execution_count": 93, - "id": "f05a1814-1b73-4eef-853b-58594589635f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([4.10820024])" - ] - }, - "execution_count": 93, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# random experiment loop\n", - "np.random.rand(1) * 4 + 1" - ] - }, - { - "cell_type": "code", - "execution_count": 94, - "id": "3127f8a2-798e-474e-a90d-89eb1c2aca06", - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.47e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 2.64e+01 3.85e+02 -1.0 1.47e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.71e+00 5.99e+01 -1.0 3.11e+01 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 3.98e-02 2.06e+00 -1.0 3.19e+00 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 7.87e-07 2.39e-04 -1.0 2.80e-02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (287125)\n", - " 5 0.0000000e+00 2.27e-13 1.50e-09 -3.8 5.42e-07 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.3691704763652764e-13 2.2737367544323206e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.3691704763652764e-13 2.2737367544323206e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.072\n", - "Total CPU secs in NLP function evaluations = 0.001\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.4\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.79e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 9.63e+01 2.70e+02 -1.0 3.79e+02 - 2.73e-03 7.46e-01f 1\n", - " 2 0.0000000e+00 9.32e+01 2.61e+02 -1.0 9.63e+01 - 6.76e-02 3.18e-02h 1\n", - " 3 0.0000000e+00 9.32e+01 4.47e+03 -1.0 9.32e+01 - 1.23e-01 3.69e-04h 1\n", - " 4r 0.0000000e+00 9.32e+01 1.00e+03 2.0 0.00e+00 - 0.00e+00 4.62e-07R 4\n", - " 5r 0.0000000e+00 9.47e+01 3.38e+03 2.0 1.88e+04 - 7.23e-03 1.52e-03f 1\n", - " 6r 0.0000000e+00 9.56e+01 8.77e+03 1.3 9.69e+03 - 1.49e-02 2.29e-03f 1\n", - " 7r 0.0000000e+00 9.02e+01 1.81e+04 1.3 2.12e+03 - 1.81e-02 5.67e-03f 1\n", - " 8r 0.0000000e+00 7.27e+01 4.84e+04 1.3 2.78e+03 - 4.24e-02 8.77e-03f 1\n", - " 9r 0.0000000e+00 7.18e+01 4.87e+04 1.3 1.96e+03 - 1.12e-02 4.83e-04f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 3.41e+01 4.77e+04 1.3 1.67e+03 - 1.59e-02 2.26e-02f 1\n", - " 11r 0.0000000e+00 2.89e+01 4.49e+04 1.3 1.04e+03 - 1.79e-01 4.94e-03f 1\n", - " 12r 0.0000000e+00 2.20e+01 3.81e+04 1.3 2.29e+02 - 2.37e-01 3.01e-02f 1\n", - " 13r 0.0000000e+00 7.18e+00 3.39e+04 1.3 9.62e+01 - 3.56e-01 1.55e-01f 1\n", - " 14r 0.0000000e+00 3.48e+00 3.59e+04 1.3 2.78e+01 - 6.26e-01 2.52e-01f 1\n", - " 15r 0.0000000e+00 2.86e+00 2.83e+04 1.3 1.68e+01 - 3.20e-02 1.85e-01f 1\n", - " 16r 0.0000000e+00 2.68e+00 2.65e+04 1.3 1.50e+01 - 1.58e-01 6.45e-02f 1\n", - " 17r 0.0000000e+00 2.20e+00 2.12e+04 1.3 6.49e+00 - 5.44e-01 1.84e-01f 1\n", - " 18r 0.0000000e+00 1.93e+00 9.46e+03 1.3 4.48e+00 - 7.25e-01 5.25e-01f 1\n", - " 19r 0.0000000e+00 1.89e+00 6.10e+03 1.3 1.60e+00 - 1.00e+00 3.45e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 1.87e+00 3.70e+03 1.3 7.92e-01 - 3.59e-01 4.16e-01f 1\n", - " 21r 0.0000000e+00 1.87e+00 3.40e+04 1.3 4.32e-01 - 6.73e-01 2.10e-01f 1\n", - " 22r 0.0000000e+00 1.87e+00 1.96e+04 1.3 3.07e-01 - 2.16e-01 3.56e-01f 1\n", - " 23r 0.0000000e+00 1.88e+00 5.64e+04 1.3 2.13e-01 - 2.36e-01 1.00e+00f 1\n", - " 24r 0.0000000e+00 1.86e+00 2.34e+00 1.3 2.78e-01 - 1.00e+00 1.00e+00f 1\n", - " 25r 0.0000000e+00 2.46e+00 3.10e+01 -0.8 8.15e+00 - 6.90e-01 7.24e-01f 1\n", - " 26r 0.0000000e+00 1.88e+00 3.41e+02 -0.8 2.98e+01 - 4.36e-01 3.54e-01f 1\n", - " 27r 0.0000000e+00 5.36e-01 3.71e+02 -0.8 3.60e+01 - 4.50e-01 4.02e-01f 1\n", - " 28r 0.0000000e+00 9.73e-02 1.52e+03 -0.8 4.27e+01 - 3.79e-01 1.29e-01f 1\n", - " 29r 0.0000000e+00 9.74e-02 4.95e+03 -0.8 3.18e+01 - 6.93e-01 3.32e-03f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 9.31e-02 5.14e+03 -0.8 8.75e+00 - 1.00e+00 1.62e-01f 1\n", - " 31r 0.0000000e+00 7.82e-02 1.83e+03 -0.8 6.58e+00 - 1.00e+00 6.63e-01f 1\n", - " 32r 0.0000000e+00 7.12e-02 1.65e+02 -0.8 2.21e+00 - 1.00e+00 9.14e-01f 1\n", - " 33r 0.0000000e+00 7.05e-02 2.39e-05 -0.8 2.63e-01 - 1.00e+00 1.00e+00f 1\n", - " 34r 0.0000000e+00 7.07e-02 1.56e+02 -3.4 4.12e-01 - 1.00e+00 7.89e-01f 1\n", - " 35r 0.0000000e+00 3.58e-04 8.04e+02 -3.4 5.07e+02 - 9.62e-01 3.36e-02f 1\n", - " 36r 0.0000000e+00 3.58e-04 8.39e+02 -3.4 2.26e+02 - 1.00e+00 8.40e-05f 1\n", - " 37r 0.0000000e+00 1.64e-04 4.20e+02 -3.4 9.66e-02 - 1.00e+00 5.43e-01f 1\n", - " 38r 0.0000000e+00 6.08e-08 1.58e-05 -3.4 4.52e-02 - 1.00e+00 1.00e+00f 1\n", - " 39r 0.0000000e+00 4.61e-10 3.50e-01 -7.7 2.55e-05 - 1.00e+00 9.99e-01f 1\n", - "\n", - "Number of Iterations....: 39\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 4.6097259343014230e-10 4.6097259343014230e-10\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 4.6097259343014230e-10 4.6097259343014230e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 44\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 44\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 41\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 39\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.332\n", - "Total CPU secs in NLP function evaluations = 0.028\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.3\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.47e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.21e+02 1.90e+02 -1.0 3.47e+02 - 3.38e-03 6.51e-01f 1\n", - " 2 0.0000000e+00 1.16e+02 1.83e+02 -1.0 1.21e+02 - 8.04e-02 3.68e-02h 1\n", - " 3 0.0000000e+00 1.16e+02 5.05e+03 -1.0 1.16e+02 - 1.48e-01 4.53e-04h 1\n", - " 4r 0.0000000e+00 1.16e+02 1.00e+03 2.1 0.00e+00 - 0.00e+00 2.84e-07R 5\n", - " 5r 0.0000000e+00 1.14e+02 2.71e+03 2.1 7.90e+03 - 1.08e-02 5.20e-03f 1\n", - " 6r 0.0000000e+00 1.10e+02 8.74e+03 1.4 1.44e+03 - 2.83e-02 5.26e-03f 1\n", - " 7r 0.0000000e+00 8.03e+01 1.59e+04 1.4 3.04e+03 - 4.49e-02 1.19e-02f 1\n", - " 8r 0.0000000e+00 4.23e+01 1.55e+04 1.4 2.42e+03 - 6.77e-02 1.57e-02f 1\n", - " 9r 0.0000000e+00 2.55e+01 9.59e+03 1.4 1.06e+03 - 3.03e-01 1.60e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 2.97e+00 6.56e+03 1.4 1.85e+02 - 1.66e-01 1.30e-01f 1\n", - " 11r 0.0000000e+00 2.46e+00 6.17e+03 1.4 2.18e+01 - 2.39e-01 1.35e-01f 1\n", - " 12r 0.0000000e+00 1.69e+00 8.83e+03 1.4 1.49e+01 - 2.90e-01 5.14e-01f 1\n", - " 13r 0.0000000e+00 1.63e+00 5.65e+03 1.4 2.04e+00 - 4.09e-01 3.77e-01f 1\n", - " 14r 0.0000000e+00 1.63e+00 4.72e+03 1.4 1.76e+00 - 1.17e-01 4.02e-01f 1\n", - " 15r 0.0000000e+00 1.63e+00 4.34e+03 1.4 1.14e+00 - 3.39e-01 2.20e-01f 1\n", - " 16r 0.0000000e+00 1.63e+00 5.17e+03 1.4 5.39e-01 - 2.44e-01 3.70e-01f 1\n", - " 17r 0.0000000e+00 1.61e+00 1.25e+04 1.4 2.67e-01 - 3.33e-01 1.00e+00f 1\n", - " 18r 0.0000000e+00 1.57e+00 6.37e-01 1.4 3.12e-01 - 1.00e+00 1.00e+00f 1\n", - " 19r 0.0000000e+00 2.14e+00 7.01e+01 -0.7 5.85e+00 - 6.75e-01 7.41e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 1.66e+00 6.97e+01 -0.7 1.81e+01 - 4.31e-01 4.08e-01f 1\n", - " 21r 0.0000000e+00 7.63e-01 5.00e+02 -0.7 2.19e+01 - 5.02e-01 3.90e-01f 1\n", - " 22r 0.0000000e+00 7.13e-02 6.33e+02 -0.7 2.85e+01 - 4.13e-01 2.39e-01f 1\n", - " 23r 0.0000000e+00 7.15e-02 1.44e+03 -0.7 2.04e+01 - 4.36e-01 4.86e-03f 1\n", - " 24r 0.0000000e+00 7.02e-02 2.07e+03 -0.7 5.19e+00 - 1.00e+00 8.72e-02f 1\n", - " 25r 0.0000000e+00 5.96e-02 3.86e+02 -0.7 4.18e+00 - 1.00e+00 8.42e-01f 1\n", - " 26r 0.0000000e+00 5.77e-02 2.81e-03 -0.7 6.30e-01 - 1.00e+00 1.00e+00f 1\n", - " 27r 0.0000000e+00 5.79e-02 6.37e+02 -3.2 5.03e-01 - 9.99e-01 6.84e-01f 1\n", - " 28r 0.0000000e+00 3.93e-02 2.66e+03 -3.2 2.99e+02 - 9.51e-01 1.50e-02f 1\n", - " 29r 0.0000000e+00 4.06e-04 3.64e+03 -3.2 2.94e+02 - 1.00e+00 3.22e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 4.05e-04 3.79e+03 -3.2 2.60e+01 - 1.00e+00 1.82e-03f 1\n", - " 31r 0.0000000e+00 2.92e-05 2.72e+02 -3.2 1.69e-01 - 1.00e+00 9.28e-01f 1\n", - " 32r 0.0000000e+00 1.25e-07 7.91e-06 -3.2 1.22e-02 - 1.00e+00 1.00e+00f 1\n", - " 33r 0.0000000e+00 5.10e-09 4.43e-01 -7.2 5.28e-05 - 1.00e+00 9.97e-01f 1\n", - "\n", - "Number of Iterations....: 33\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 4.1933425768594618e-09 5.0964019530138488e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 4.1933425768594618e-09 5.0964019530138488e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 39\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 39\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 35\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 33\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.281\n", - "Total CPU secs in NLP function evaluations = 0.043\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.3\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.75e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.27e+02 8.39e+01 -1.0 3.75e+02 - 7.66e-03 6.60e-01f 1\n", - " 2 0.0000000e+00 1.21e+02 7.95e+01 -1.0 1.27e+02 - 8.92e-02 5.26e-02h 1\n", - " 3 0.0000000e+00 1.21e+02 5.06e+03 -1.0 1.21e+02 - 1.90e-01 6.85e-04h 1\n", - " 4r 0.0000000e+00 1.21e+02 1.00e+03 2.1 0.00e+00 - 0.00e+00 4.29e-07R 5\n", - " 5r 0.0000000e+00 1.18e+02 3.76e+03 2.1 1.32e+04 - 1.10e-02 4.67e-03f 1\n", - " 6r 0.0000000e+00 1.13e+02 1.04e+04 1.4 1.51e+03 - 2.60e-02 6.04e-03f 1\n", - " 7r 0.0000000e+00 7.22e+01 1.49e+04 1.4 2.88e+03 - 2.82e-02 1.57e-02f 1\n", - " 8r 0.0000000e+00 4.91e+01 1.55e+04 1.4 2.25e+03 - 1.34e-01 1.11e-02f 1\n", - " 9r 0.0000000e+00 4.46e+01 1.03e+04 1.4 6.00e+02 - 1.39e-01 7.47e-03f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 1.28e+01 2.43e+04 1.4 2.89e+02 - 3.16e-01 1.10e-01f 1\n", - " 11r 0.0000000e+00 2.22e+00 2.04e+04 1.4 6.20e+01 - 2.94e-01 2.05e-01f 1\n", - " 12r 0.0000000e+00 1.78e+00 1.86e+04 1.4 1.16e+01 - 6.29e-01 1.84e-01f 1\n", - " 13r 0.0000000e+00 1.60e+00 1.47e+04 1.4 1.15e+01 - 2.26e-01 1.37e-01f 1\n", - " 14r 0.0000000e+00 1.18e+00 1.20e+04 1.4 5.45e+00 - 1.92e-01 2.67e-01f 1\n", - " 15r 0.0000000e+00 8.27e-01 7.10e+03 1.4 4.24e+00 - 3.39e-01 2.57e-01f 1\n", - " 16r 0.0000000e+00 7.55e-01 4.82e+03 1.4 2.29e+00 - 4.12e-01 1.42e-01f 1\n", - " 17r 0.0000000e+00 5.13e-01 1.07e+04 1.4 1.12e+00 - 2.45e-01 4.22e-01f 1\n", - " 18r 0.0000000e+00 5.10e-01 8.28e+03 1.4 6.72e-01 - 2.21e-01 2.17e-01f 1\n", - " 19r 0.0000000e+00 5.06e-01 9.33e+03 1.4 5.23e-01 - 2.44e-01 5.04e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 4.54e-01 9.33e+01 1.4 5.96e-01 - 9.69e-01 1.00e+00f 1\n", - " 21r 0.0000000e+00 9.44e-01 4.24e+02 -0.0 4.97e+00 - 4.34e-01 7.87e-01f 1\n", - " 22r 0.0000000e+00 7.53e-01 9.03e+01 -0.0 7.82e+00 - 5.74e-01 5.18e-01f 1\n", - " 23r 0.0000000e+00 3.96e-01 3.26e+02 -0.0 6.53e+00 - 7.93e-01 6.49e-01f 1\n", - " 24r 0.0000000e+00 6.22e-02 1.69e+02 -0.0 5.24e+00 - 1.00e+00 8.48e-01f 1\n", - " 25r 0.0000000e+00 1.21e-01 5.96e-02 -0.0 1.33e+00 - 1.00e+00 1.00e+00h 1\n", - " 26r 0.0000000e+00 1.23e-01 2.48e+01 -1.4 6.66e-01 - 7.98e-01 7.68e-01f 1\n", - " 27r 0.0000000e+00 8.31e-02 1.67e+03 -1.4 6.15e+01 - 1.89e-01 1.54e-02f 1\n", - " 28r 0.0000000e+00 7.45e-02 3.08e+03 -1.4 2.92e+01 - 8.39e-01 1.52e-02f 1\n", - " 29r 0.0000000e+00 5.63e-02 2.74e+03 -1.4 2.79e+01 - 1.00e+00 1.93e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 3.32e-02 1.62e+03 -1.4 2.20e+01 - 1.00e+00 4.05e-01f 1\n", - " 31r 0.0000000e+00 3.05e-02 1.50e+03 -1.4 4.31e+00 - 1.00e+00 7.84e-02f 1\n", - " 32r 0.0000000e+00 2.85e-03 1.41e+02 -1.4 5.52e-01 - 1.00e+00 9.06e-01f 1\n", - " 33r 0.0000000e+00 8.12e-05 5.53e-04 -1.4 4.22e-02 - 1.00e+00 1.00e+00f 1\n", - " 34r 0.0000000e+00 5.19e-06 2.99e+01 -4.8 3.09e-02 - 1.00e+00 9.39e-01f 1\n", - " 35r 0.0000000e+00 2.12e-06 3.88e+02 -4.8 5.79e-03 - 1.00e+00 3.73e-01f 1\n", - " 36r 0.0000000e+00 4.48e-10 1.79e-06 -4.8 5.54e-04 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 36\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 4.4780146257750175e-10 4.4780146257750175e-10\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 4.4780146257750175e-10 4.4780146257750175e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 42\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 42\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 38\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 36\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.329\n", - "Total CPU secs in NLP function evaluations = 0.023\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.8\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.15e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 5.48e+01 2.03e+02 -1.0 3.15e+02 - 4.01e-03 8.26e-01f 1\n", - " 2 0.0000000e+00 5.18e+01 1.92e+02 -1.0 5.48e+01 - 8.17e-02 5.52e-02h 1\n", - " 3 0.0000000e+00 5.17e+01 4.29e+03 -1.0 5.18e+01 - 1.73e-01 6.99e-04h 1\n", - " 4r 0.0000000e+00 5.17e+01 1.00e+03 1.7 0.00e+00 - 0.00e+00 4.38e-07R 5\n", - " 5r 0.0000000e+00 5.21e+01 3.90e+03 1.7 2.34e+03 - 4.16e-03 6.39e-04f 1\n", - " 6r 0.0000000e+00 5.22e+01 7.89e+03 1.7 1.59e+03 - 5.68e-03 1.06e-03f 1\n", - " 7r 0.0000000e+00 5.14e+01 4.08e+04 1.7 1.33e+03 - 4.31e-02 4.03e-03f 1\n", - " 8r 0.0000000e+00 3.35e+01 4.38e+04 1.7 1.29e+03 - 2.87e-02 1.99e-02f 1\n", - " 9r 0.0000000e+00 2.33e+01 3.77e+04 1.7 2.06e+03 - 8.82e-02 4.96e-03f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 5.31e+00 2.32e+04 1.7 8.19e+02 - 4.10e-01 2.82e-02f 1\n", - " 11r 0.0000000e+00 3.31e+00 9.35e+03 1.7 2.79e+01 - 4.57e-01 2.18e-01f 1\n", - " 12r 0.0000000e+00 1.54e+00 1.11e+04 1.7 1.44e+01 - 3.37e-01 4.76e-01f 1\n", - " 13r 0.0000000e+00 9.67e-01 5.00e+03 1.7 3.61e+00 - 6.85e-01 6.15e-01f 1\n", - " 14r 0.0000000e+00 7.59e-01 7.16e+00 1.7 2.56e+00 - 1.00e+00 1.00e+00f 1\n", - " 15r 0.0000000e+00 1.45e+00 2.49e+02 -0.4 9.91e+00 - 4.68e-01 7.63e-01f 1\n", - " 16r 0.0000000e+00 1.16e+00 3.68e+02 -0.4 1.08e+01 - 6.33e-01 4.50e-01f 1\n", - " 17r 0.0000000e+00 3.39e-01 2.33e+02 -0.4 1.75e+01 - 6.02e-01 5.35e-01f 1\n", - " 18r 0.0000000e+00 2.54e-02 1.04e+03 -0.4 2.08e+01 - 6.33e-01 1.79e-01f 1\n", - " 19r 0.0000000e+00 2.53e-02 1.45e+03 -0.4 6.72e+00 - 1.00e+00 1.86e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 1.60e-02 1.72e+02 -0.4 4.41e+00 - 1.00e+00 8.92e-01f 1\n", - " 21r 0.0000000e+00 1.49e-02 3.13e-03 -0.4 4.38e-01 - 1.00e+00 1.00e+00f 1\n", - " 22r 0.0000000e+00 1.46e-02 4.88e+02 -2.7 5.10e-01 - 1.00e+00 7.79e-01f 1\n", - " 23r 0.0000000e+00 1.62e-03 1.95e+03 -2.7 1.98e+02 - 8.89e-01 1.78e-02f 1\n", - " 24r 0.0000000e+00 1.33e-03 1.81e+03 -2.7 1.39e+00 - 1.00e+00 6.22e-02f 1\n", - " 25r 0.0000000e+00 7.19e-06 1.11e+01 -2.7 4.06e-01 - 1.00e+00 9.94e-01f 1\n", - " 26r 0.0000000e+00 3.77e-07 6.55e-08 -2.7 2.13e-03 - 1.00e+00 1.00e+00f 1\n", - " 27r 0.0000000e+00 7.89e-09 4.38e-01 -6.0 1.65e-04 - 1.00e+00 9.99e-01f 1\n", - "\n", - "Number of Iterations....: 27\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 6.8976688608700370e-09 7.8893271863306191e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 6.8976688608700370e-09 7.8893271863306191e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 33\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 33\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 29\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 27\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.279\n", - "Total CPU secs in NLP function evaluations = 0.003\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.3\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.96e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 2.45e+02 4.98e+01 -1.0 3.96e+02 - 7.44e-03 3.82e-01f 1\n", - " 2 0.0000000e+00 2.36e+02 4.81e+01 -1.0 2.45e+02 - 1.33e-01 3.44e-02h 1\n", - " 3 0.0000000e+00 2.36e+02 1.20e+04 -1.0 2.36e+02 - 2.37e-01 4.38e-04h 1\n", - " 4r 0.0000000e+00 2.36e+02 1.00e+03 2.4 0.00e+00 - 0.00e+00 2.75e-07R 5\n", - " 5r 0.0000000e+00 2.26e+02 1.13e+03 2.4 1.23e+04 - 7.14e-03 8.92e-03f 1\n", - " 6r 0.0000000e+00 2.22e+02 2.63e+03 1.0 1.35e+03 - 4.20e-02 3.84e-03f 1\n", - " 7r 0.0000000e+00 1.66e+02 4.09e+03 1.0 4.68e+03 - 2.15e-02 1.19e-02f 1\n", - " 8r 0.0000000e+00 1.54e+02 7.04e+03 1.0 3.41e+03 - 6.48e-02 3.70e-03f 1\n", - " 9r 0.0000000e+00 1.00e+02 9.04e+03 1.0 2.10e+03 - 1.47e-01 2.81e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 5.77e+01 9.14e+03 1.0 6.36e+02 - 2.78e-01 6.69e-02f 1\n", - " 11r 0.0000000e+00 2.10e+01 7.43e+03 1.0 1.64e+02 - 2.45e-01 2.24e-01f 1\n", - " 12r 0.0000000e+00 6.07e+00 7.89e+03 1.0 4.33e+01 - 6.44e-01 3.46e-01f 1\n", - " 13r 0.0000000e+00 2.19e+00 4.02e+03 1.0 8.39e+00 - 5.64e-01 4.67e-01f 1\n", - " 14r 0.0000000e+00 9.56e-01 1.95e+03 1.0 3.19e+00 - 4.50e-01 3.89e-01f 1\n", - " 15r 0.0000000e+00 5.36e-01 9.74e+02 1.0 1.71e+00 - 3.50e-01 3.26e-01f 1\n", - " 16r 0.0000000e+00 5.38e-01 2.58e+03 1.0 9.75e-01 - 3.56e-01 4.28e-01f 1\n", - " 17r 0.0000000e+00 5.42e-01 1.63e+03 1.0 6.88e-01 - 6.24e-01 5.31e-01f 1\n", - " 18r 0.0000000e+00 5.46e-01 7.82e+00 1.0 5.18e-01 - 1.00e+00 1.00e+00f 1\n", - " 19r 0.0000000e+00 9.22e-01 4.37e+02 -1.1 5.33e+00 - 5.40e-01 8.04e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 5.65e-01 1.82e+03 -1.1 1.08e+01 - 1.64e-01 4.12e-01f 1\n", - " 21r 0.0000000e+00 4.50e-02 1.28e+03 -1.1 1.65e+01 - 3.58e-01 3.90e-01f 1\n", - " 22r 0.0000000e+00 2.31e-03 2.20e+03 -1.1 2.78e+01 - 4.49e-01 1.90e-02f 1\n", - " 23r 0.0000000e+00 2.27e-03 2.78e+03 -1.1 2.10e+00 - 1.00e+00 1.41e-02f 1\n", - " 24r 0.0000000e+00 1.39e-04 2.09e+02 -1.1 9.81e-01 - 1.00e+00 9.24e-01f 1\n", - " 25r 0.0000000e+00 6.99e-05 1.01e-03 -1.1 2.85e-02 - 1.00e+00 1.00e+00f 1\n", - " 26r 0.0000000e+00 2.43e-05 6.84e+02 -4.1 2.96e-02 - 1.00e+00 7.85e-01f 1\n", - " 27r 0.0000000e+00 1.04e-05 1.23e+03 -4.1 1.49e-02 - 8.56e-01 6.16e-01f 1\n", - " 28r 0.0000000e+00 4.47e-07 1.96e+01 -4.1 2.40e-03 - 1.00e+00 9.84e-01f 1\n", - " 29r 0.0000000e+00 2.05e-09 1.48e-08 -4.1 2.07e-04 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 29\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.0521482846369565e-09 2.0521482846369565e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.0521482846369565e-09 2.0521482846369565e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 35\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 35\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 31\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 29\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.239\n", - "Total CPU secs in NLP function evaluations = 0.039\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.1\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.72e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 2.61e+02 3.60e+01 -1.0 3.72e+02 - 7.93e-03 2.96e-01f 1\n", - " 2 0.0000000e+00 2.53e+02 3.49e+01 -1.0 2.61e+02 - 1.58e-01 3.06e-02h 1\n", - " 3 0.0000000e+00 2.53e+02 2.01e+04 -1.0 2.53e+02 - 3.10e-01 3.94e-04h 1\n", - " 4r 0.0000000e+00 2.53e+02 1.00e+03 2.4 0.00e+00 - 0.00e+00 4.95e-07R 4\n", - " 5r 0.0000000e+00 2.37e+02 1.05e+04 2.4 1.51e+04 - 1.17e-03 1.25e-02f 1\n", - " 6r 0.0000000e+00 2.35e+02 1.18e+04 1.7 1.30e+03 - 8.34e-02 1.70e-03f 1\n", - " 7r 0.0000000e+00 1.83e+02 1.27e+04 1.7 2.44e+03 - 6.29e-02 2.12e-02f 1\n", - " 8r 0.0000000e+00 1.24e+02 1.00e+04 1.7 1.85e+03 - 1.40e-01 3.23e-02f 1\n", - " 9r 0.0000000e+00 6.63e+01 1.21e+04 1.7 1.16e+03 - 2.28e-01 5.10e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 3.68e+01 1.07e+04 1.7 4.32e+02 - 2.77e-01 6.82e-02f 1\n", - " 11r 0.0000000e+00 1.57e+01 9.36e+03 1.7 1.56e+02 - 2.53e-01 1.35e-01f 1\n", - " 12r 0.0000000e+00 2.87e+00 8.33e+03 1.7 6.40e+01 - 5.80e-01 2.43e-01f 1\n", - " 13r 0.0000000e+00 1.93e+00 5.21e+03 1.7 3.30e+00 - 7.93e-01 3.21e-01f 1\n", - " 14r 0.0000000e+00 2.80e-01 2.68e+03 1.7 1.93e+00 - 7.69e-01 1.00e+00f 1\n", - " 15r 0.0000000e+00 1.98e-01 2.17e+03 1.0 2.62e+00 - 1.00e+00 3.68e-01f 1\n", - " 16r 0.0000000e+00 9.94e-02 2.32e+02 1.0 2.49e+00 - 1.00e+00 9.35e-01f 1\n", - " 17r 0.0000000e+00 1.07e-01 2.05e+02 0.3 1.61e+00 - 3.29e-01 4.41e-01f 1\n", - " 18r 0.0000000e+00 1.04e-01 1.05e+03 0.3 3.32e+00 - 1.00e+00 5.33e-01f 1\n", - " 19r 0.0000000e+00 6.40e-02 4.29e-01 0.3 1.54e+00 - 1.00e+00 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 1.07e-01 1.10e+02 -1.8 1.41e+00 - 3.80e-01 4.37e-01f 1\n", - " 21r 0.0000000e+00 1.07e-01 2.01e+02 -1.8 1.95e+01 - 1.74e-01 2.31e-01f 1\n", - " 22r 0.0000000e+00 6.49e-02 5.91e+02 -1.8 9.96e+00 - 2.51e-03 1.11e-01f 1\n", - " 23r 0.0000000e+00 6.33e-02 4.58e+02 -1.8 1.93e+01 - 2.98e-02 7.77e-04f 1\n", - " 24r 0.0000000e+00 5.10e-02 3.76e+02 -1.8 1.93e+01 - 4.61e-02 5.71e-03f 1\n", - " 25r 0.0000000e+00 2.14e-02 2.76e+02 -1.8 1.72e+01 - 6.59e-02 1.56e-02f 1\n", - " 26r 0.0000000e+00 2.87e-03 1.64e+03 -1.8 7.05e+00 - 6.11e-01 3.14e-02f 1\n", - " 27r 0.0000000e+00 1.38e-03 1.02e+03 -1.8 7.30e-01 - 1.00e+00 5.15e-01f 1\n", - " 28r 0.0000000e+00 2.95e-04 3.93e+02 -1.8 1.31e-01 - 1.00e+00 8.08e-01f 1\n", - " 29r 0.0000000e+00 9.46e-05 7.73e+02 -1.8 2.27e-02 - 1.00e+00 7.75e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 3.57e-05 3.29e-04 -1.8 5.02e-03 - 1.00e+00 1.00e+00f 1\n", - " 31r 0.0000000e+00 6.02e-06 6.92e+02 -4.0 3.94e-03 - 1.00e+00 6.62e-01f 1\n", - " 32r 0.0000000e+00 2.81e-06 1.20e+03 -4.0 2.72e-03 - 9.73e-01 6.04e-01f 1\n", - " 33r 0.0000000e+00 9.98e-07 5.98e+02 -4.0 2.90e-04 - 9.64e-01 7.94e-01f 1\n", - " 34r 0.0000000e+00 2.20e-08 6.50e-06 -4.0 7.03e-05 - 1.00e+00 1.00e+00f 1\n", - " 35r 0.0000000e+00 2.23e-09 8.19e-02 -6.0 1.55e-06 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 35\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.8426819152568400e-09 2.2255034382400569e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.8426819152568400e-09 2.2255034382400569e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 40\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 40\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 37\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 35\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.274\n", - "Total CPU secs in NLP function evaluations = 0.058\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.3\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.87e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.68e+02 8.00e+01 -1.0 3.87e+02 - 6.89e-03 5.66e-01f 1\n", - " 2 0.0000000e+00 1.63e+02 7.76e+01 -1.0 1.68e+02 - 7.67e-02 2.95e-02h 1\n", - " 3 0.0000000e+00 1.63e+02 5.80e+03 -1.0 1.63e+02 - 1.40e-01 3.53e-04h 1\n", - " 4r 0.0000000e+00 1.63e+02 1.00e+03 2.2 0.00e+00 - 0.00e+00 4.43e-07R 4\n", - " 5r 0.0000000e+00 1.59e+02 6.51e+03 2.2 1.63e+04 - 1.20e-03 7.89e-03f 1\n", - " 6r 0.0000000e+00 1.58e+02 7.34e+03 1.5 1.51e+03 - 3.35e-02 1.55e-03f 1\n", - " 7r 0.0000000e+00 1.25e+02 9.10e+03 1.5 2.75e+03 - 3.40e-02 1.20e-02f 1\n", - " 8r 0.0000000e+00 7.96e+01 1.06e+04 1.5 2.37e+03 - 1.07e-01 2.00e-02f 1\n", - " 9r 0.0000000e+00 5.15e+01 1.07e+04 1.5 1.06e+03 - 1.37e-01 2.93e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 2.98e+01 9.15e+03 1.5 3.66e+02 - 1.10e-01 6.31e-02f 1\n", - " 11r 0.0000000e+00 1.42e+01 7.49e+03 1.5 1.56e+02 - 1.92e-01 1.00e-01f 1\n", - " 12r 0.0000000e+00 5.99e+00 6.19e+03 1.5 5.89e+01 - 2.77e-01 1.39e-01f 1\n", - " 13r 0.0000000e+00 1.73e+00 4.52e+03 1.5 2.35e+01 - 3.82e-01 2.55e-01f 1\n", - " 14r 0.0000000e+00 1.29e+00 2.91e+03 1.5 6.53e+00 - 4.45e-01 3.67e-01f 1\n", - " 15r 0.0000000e+00 1.23e+00 5.95e+03 1.5 2.69e+00 - 2.74e-01 5.38e-01f 1\n", - " 16r 0.0000000e+00 1.17e+00 1.13e+03 1.5 2.10e+00 - 7.15e-01 9.68e-01f 1\n", - " 17r 0.0000000e+00 1.51e+00 1.07e+02 0.8 7.88e+00 - 8.98e-01 9.05e-01f 1\n", - " 18r 0.0000000e+00 1.52e+00 5.98e+02 0.1 1.35e+00 - 1.00e+00 6.47e-01f 1\n", - " 19r 0.0000000e+00 1.45e+00 8.89e+02 0.1 2.43e+00 - 1.94e-01 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 1.11e+00 1.71e+02 0.1 7.15e+00 - 9.26e-01 1.00e+00f 1\n", - " 21r 0.0000000e+00 1.05e+00 4.83e-02 0.1 1.29e+00 - 1.00e+00 1.00e+00f 1\n", - " 22r 0.0000000e+00 1.19e+00 1.23e+02 -2.0 3.72e+00 - 8.52e-01 7.58e-01f 1\n", - " 23r 0.0000000e+00 4.52e-02 1.95e+03 -2.0 1.34e+02 - 5.22e-02 1.81e-01f 1\n", - " 24r 0.0000000e+00 2.87e-02 1.09e+03 -2.0 1.48e+02 - 1.91e-01 5.32e-03f 1\n", - " 25r 0.0000000e+00 2.87e-02 2.29e+03 -2.0 9.93e+01 - 3.30e-01 8.99e-04f 1\n", - " 26r 0.0000000e+00 1.33e-02 2.25e+03 -2.0 2.85e+01 - 1.00e+00 1.34e-01f 1\n", - " 27r 0.0000000e+00 3.43e-03 2.10e+03 -2.0 2.20e+01 - 1.00e+00 1.39e-01f 1\n", - " 28r 0.0000000e+00 1.99e-03 1.83e+03 -2.0 2.20e+00 - 1.00e+00 1.64e-01f 1\n", - " 29r 0.0000000e+00 1.11e-04 1.19e+02 -2.0 6.96e-01 - 1.00e+00 9.38e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 1.14e-05 2.14e-04 -2.0 4.33e-02 - 1.00e+00 1.00e+00f 1\n", - " 31r 0.0000000e+00 3.06e-06 1.72e+02 -4.5 3.76e-03 - 1.00e+00 8.67e-01f 1\n", - " 32r 0.0000000e+00 1.29e-06 1.73e+02 -4.5 1.18e-03 - 1.00e+00 5.83e-01f 1\n", - " 33r 0.0000000e+00 1.76e-09 1.28e-06 -4.5 1.67e-04 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 33\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.7635167770535531e-09 1.7635167770535531e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.7635167770535531e-09 1.7635167770535531e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 38\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 38\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 35\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 33\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.309\n", - "Total CPU secs in NLP function evaluations = 0.009\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.5\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.79e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 2.32e+02 8.73e+01 -1.0 3.79e+02 - 4.33e-03 3.87e-01f 1\n", - " 2 0.0000000e+00 2.27e+02 8.51e+01 -1.0 2.32e+02 - 8.32e-02 2.51e-02h 1\n", - " 3 0.0000000e+00 2.26e+02 6.87e+03 -1.0 2.27e+02 - 1.38e-01 3.13e-04h 1\n", - " 4r 0.0000000e+00 2.26e+02 1.00e+03 2.4 0.00e+00 - 0.00e+00 3.92e-07R 4\n", - " 5r 0.0000000e+00 2.18e+02 5.24e+03 2.4 6.72e+03 - 1.53e-03 8.08e-03f 1\n", - " 6r 0.0000000e+00 2.11e+02 6.39e+03 1.7 1.35e+03 - 5.23e-02 5.22e-03f 1\n", - " 7r 0.0000000e+00 1.63e+02 7.61e+03 1.7 2.61e+03 - 4.63e-02 1.85e-02f 1\n", - " 8r 0.0000000e+00 1.52e+02 8.34e+03 1.7 2.11e+03 - 1.43e-01 5.53e-03f 1\n", - " 9r 0.0000000e+00 8.62e+01 6.88e+03 1.7 1.35e+03 - 2.57e-01 5.01e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 4.41e+01 7.80e+03 1.7 4.99e+02 - 3.92e-01 8.96e-02f 1\n", - " 11r 0.0000000e+00 1.76e+01 6.21e+03 1.7 1.36e+02 - 1.98e-01 2.04e-01f 1\n", - " 12r 0.0000000e+00 7.94e+00 6.72e+03 1.7 5.67e+01 - 7.95e-01 1.70e-01f 1\n", - " 13r 0.0000000e+00 2.01e+00 4.62e+03 1.7 1.94e+01 - 1.00e+00 4.08e-01f 1\n", - " 14r 0.0000000e+00 1.95e+00 5.95e+02 1.7 3.44e+00 - 9.75e-01 8.56e-01f 1\n", - " 15r 0.0000000e+00 2.34e+00 1.49e+02 0.3 9.78e+00 - 8.14e-01 7.50e-01f 1\n", - " 16r 0.0000000e+00 2.29e+00 1.06e+03 0.3 2.00e+00 - 3.58e-01 6.35e-01f 1\n", - " 17r 0.0000000e+00 2.20e+00 4.59e+02 0.3 2.88e+00 - 8.54e-01 6.97e-01f 1\n", - " 18r 0.0000000e+00 2.04e+00 3.75e+02 0.3 3.28e+00 - 8.25e-01 1.00e+00f 1\n", - " 19r 0.0000000e+00 2.03e+00 4.69e-03 0.3 2.66e-01 - 1.00e+00 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 2.19e+00 1.23e+02 -1.8 4.02e+00 - 9.06e-01 7.70e-01f 1\n", - " 21r 0.0000000e+00 9.38e-01 2.90e+03 -1.8 9.16e+01 - 4.10e-02 2.74e-01f 1\n", - " 22r 0.0000000e+00 2.46e-02 1.94e+03 -1.8 1.08e+02 - 2.21e-01 1.72e-01f 1\n", - " 23r 0.0000000e+00 2.47e-02 2.02e+03 -1.8 1.09e+02 - 2.16e-01 1.72e-03f 1\n", - " 24r 0.0000000e+00 2.45e-02 3.75e+03 -1.8 2.59e+01 - 7.11e-01 4.29e-03f 1\n", - " 25r 0.0000000e+00 1.61e-02 3.51e+03 -1.8 1.76e+01 - 1.00e+00 1.16e-01f 1\n", - " 26r 0.0000000e+00 1.08e-02 3.50e+03 -1.8 1.47e+01 - 1.00e+00 8.73e-02f 1\n", - " 27r 0.0000000e+00 2.06e-03 2.80e+03 -1.8 1.05e+01 - 1.00e+00 2.47e-01f 1\n", - " 28r 0.0000000e+00 1.24e-03 1.74e+03 -1.8 8.12e-01 - 1.00e+00 3.99e-01f 1\n", - " 29r 0.0000000e+00 5.90e-05 5.81e+01 -1.8 2.34e-01 - 1.00e+00 9.68e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 3.22e-05 1.22e-04 -1.8 7.57e-03 - 1.00e+00 1.00e+00f 1\n", - " 31r 0.0000000e+00 8.59e-06 4.05e+02 -4.1 9.74e-03 - 1.00e+00 7.37e-01f 1\n", - " 32r 0.0000000e+00 2.82e-06 1.88e+03 -4.1 7.91e-03 - 9.72e-01 3.73e-01f 1\n", - " 33r 0.0000000e+00 6.13e-07 5.39e+02 -4.1 3.14e-04 - 1.00e+00 8.82e-01f 1\n", - " 34r 0.0000000e+00 7.85e-09 2.16e-06 -4.1 5.11e-05 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 34\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 6.6954687905074195e-09 7.8509231044920165e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 6.6954687905074195e-09 7.8509231044920165e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 39\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 39\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 36\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 34\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.295\n", - "Total CPU secs in NLP function evaluations = 0.035\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.3\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.76e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 2.67e+02 3.55e+01 -1.0 3.76e+02 - 7.84e-03 2.89e-01f 1\n", - " 2 0.0000000e+00 2.58e+02 3.43e+01 -1.0 2.67e+02 - 1.36e-01 3.43e-02h 1\n", - " 3 0.0000000e+00 2.58e+02 1.25e+04 -1.0 2.58e+02 - 2.56e-01 4.71e-04h 1\n", - " 4r 0.0000000e+00 2.58e+02 1.00e+03 2.4 0.00e+00 - 0.00e+00 2.96e-07R 5\n", - " 5r 0.0000000e+00 2.45e+02 7.55e+03 2.4 1.31e+04 - 1.36e-03 1.03e-02f 1\n", - " 6r 0.0000000e+00 2.41e+02 8.88e+03 1.7 1.32e+03 - 7.60e-02 2.33e-03f 1\n", - " 7r 0.0000000e+00 1.91e+02 1.08e+04 1.7 2.50e+03 - 8.74e-02 2.02e-02f 1\n", - " 8r 0.0000000e+00 1.66e+02 1.34e+04 1.7 1.82e+03 - 3.14e-01 1.41e-02f 1\n", - " 9r 0.0000000e+00 6.77e+01 1.15e+04 1.7 9.90e+02 - 1.96e-01 9.89e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 4.22e+01 8.73e+03 1.7 4.00e+02 - 6.92e-01 7.58e-02f 1\n", - " 11r 0.0000000e+00 1.28e+01 7.30e+03 1.7 9.32e+01 - 1.00e+00 3.35e-01f 1\n", - " 12r 0.0000000e+00 3.55e+00 4.88e+03 1.7 2.64e+01 - 1.00e+00 3.50e-01f 1\n", - " 13r 0.0000000e+00 1.69e+00 2.88e+03 1.7 7.90e+00 - 1.00e+00 4.53e-01f 1\n", - " 14r 0.0000000e+00 6.03e-01 8.23e+02 1.0 2.33e+00 - 4.43e-01 8.26e-01f 1\n", - " 15r 0.0000000e+00 5.44e-01 6.26e+00 1.0 1.57e+00 - 1.00e+00 9.98e-01f 1\n", - " 16r 0.0000000e+00 7.15e-01 2.12e+02 -1.1 3.16e+00 - 5.99e-01 4.65e-01f 1\n", - " 17r 0.0000000e+00 7.08e-01 4.60e+02 -1.1 7.26e+00 - 7.72e-02 3.24e-01f 1\n", - " 18r 0.0000000e+00 5.24e-01 1.33e+03 -1.1 7.74e+00 - 1.41e-02 2.59e-01f 1\n", - " 19r 0.0000000e+00 2.34e-01 1.24e+03 -1.1 7.27e+00 - 2.80e-01 3.71e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 9.07e-03 1.06e+03 -1.1 9.86e+00 - 5.50e-01 2.08e-01f 1\n", - " 21r 0.0000000e+00 7.28e-03 1.75e+03 -1.1 1.31e+01 - 3.75e-01 2.95e-03f 1\n", - " 22r 0.0000000e+00 6.06e-03 2.39e+03 -1.1 4.91e+00 - 1.00e+00 1.03e-01f 1\n", - " 23r 0.0000000e+00 2.62e-03 1.50e+03 -1.1 4.00e+00 - 1.00e+00 4.71e-01f 1\n", - " 24r 0.0000000e+00 1.40e-03 3.97e+02 -1.1 1.08e+00 - 1.00e+00 7.99e-01f 1\n", - " 25r 0.0000000e+00 5.02e-04 1.55e-03 -1.1 1.68e-01 - 1.00e+00 1.00e+00f 1\n", - " 26r 0.0000000e+00 2.02e-04 1.03e+03 -4.0 1.53e-01 - 1.00e+00 6.42e-01f 1\n", - " 27r 0.0000000e+00 4.23e-05 3.32e+03 -4.0 3.68e-01 - 9.45e-01 1.64e-01f 1\n", - " 28r 0.0000000e+00 6.59e-06 8.73e+02 -4.0 6.96e-03 - 9.73e-01 7.76e-01f 1\n", - " 29r 0.0000000e+00 2.70e-06 1.28e+03 -4.0 1.13e-03 - 9.39e-01 6.61e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 6.39e-07 2.14e+02 -4.0 3.79e-04 - 8.88e-01 9.54e-01f 1\n", - " 31r 0.0000000e+00 5.66e-09 3.83e-07 -4.0 1.73e-05 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 31\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.6641896728493180e-09 5.6641896728493180e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.6641896728493180e-09 5.6641896728493180e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 37\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 37\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 33\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 31\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.299\n", - "Total CPU secs in NLP function evaluations = 0.020\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.4\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.12e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.42e+02 7.75e+01 -1.0 3.12e+02 - 6.86e-03 5.47e-01f 1\n", - " 2 0.0000000e+00 1.36e+02 7.44e+01 -1.0 1.42e+02 - 1.36e-01 4.13e-02h 1\n", - " 3 0.0000000e+00 1.36e+02 1.08e+04 -1.0 1.36e+02 - 2.46e-01 5.18e-04h 1\n", - " 4r 0.0000000e+00 1.36e+02 1.00e+03 2.1 0.00e+00 - 0.00e+00 3.25e-07R 5\n", - " 5r 0.0000000e+00 9.30e+01 4.20e+03 2.1 1.83e+04 - 1.01e-02 4.22e-03f 1\n", - " 6r 0.0000000e+00 8.60e+01 6.51e+03 1.4 1.12e+04 - 1.11e-02 5.18e-03f 1\n", - " 7r 0.0000000e+00 6.37e+01 2.08e+04 1.4 2.99e+03 - 3.09e-02 8.06e-03f 1\n", - " 8r 0.0000000e+00 6.32e+01 2.86e+04 1.4 2.43e+03 - 7.46e-02 2.34e-04f 1\n", - " 9r 0.0000000e+00 8.20e+00 2.07e+04 1.4 1.52e+03 - 3.82e-01 4.12e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 4.76e+00 1.27e+04 1.4 2.31e+01 - 3.36e-01 1.49e-01f 1\n", - " 11r 0.0000000e+00 9.35e-01 4.92e+03 1.4 1.05e+01 - 1.00e+00 4.51e-01f 1\n", - " 12r 0.0000000e+00 4.23e-01 6.26e+03 1.4 3.59e+00 - 6.30e-01 8.74e-01f 1\n", - " 13r 0.0000000e+00 4.02e-01 2.59e+00 1.4 5.34e-01 - 1.00e+00 1.00e+00f 1\n", - " 14r 0.0000000e+00 9.60e-01 2.09e+02 -0.7 5.84e+00 - 3.71e-01 7.86e-01f 1\n", - " 15r 0.0000000e+00 6.90e-01 1.17e+02 -0.7 5.80e+00 - 5.49e-01 5.65e-01f 1\n", - " 16r 0.0000000e+00 1.01e-01 1.58e+02 -0.7 1.02e+01 - 5.69e-01 5.29e-01f 1\n", - " 17r 0.0000000e+00 1.53e-02 1.59e+03 -0.7 1.68e+01 - 4.32e-01 5.29e-02f 1\n", - " 18r 0.0000000e+00 1.52e-02 2.33e+03 -0.7 2.54e+00 - 1.00e+00 2.41e-02f 1\n", - " 19r 0.0000000e+00 1.15e-02 1.53e+02 -0.7 2.10e+00 - 1.00e+00 9.43e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 1.14e-02 1.06e-03 -0.7 1.03e-01 - 1.00e+00 1.00e+00f 1\n", - " 21r 0.0000000e+00 1.13e-02 2.33e+02 -3.1 3.47e-01 - 1.00e+00 8.62e-01f 1\n", - " 22r 0.0000000e+00 3.48e-04 7.69e+02 -3.1 1.30e+02 - 9.77e-01 2.08e-02f 1\n", - " 23r 0.0000000e+00 3.41e-04 8.97e+02 -3.1 1.87e+00 - 1.00e+00 2.00e-02f 1\n", - " 24r 0.0000000e+00 2.53e-06 6.41e+00 -3.1 1.20e-01 - 1.00e+00 9.93e-01f 1\n", - " 25r 0.0000000e+00 1.31e-07 8.18e-09 -3.1 8.77e-04 - 1.00e+00 1.00e+00f 1\n", - " 26r 0.0000000e+00 9.78e-10 3.71e-01 -7.0 5.48e-05 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 26\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.1057295037444419e-10 9.7818515353703672e-10\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.1057295037444419e-10 9.7818515353703672e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 32\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 32\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 28\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 26\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.258\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.5\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.85e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 2.56e+02 9.16e+01 -1.0 3.85e+02 - 3.57e-03 3.35e-01f 1\n", - " 2 0.0000000e+00 2.51e+02 8.99e+01 -1.0 2.56e+02 - 7.05e-02 1.89e-02h 1\n", - " 3 0.0000000e+00 2.51e+02 6.86e+03 -1.0 2.51e+02 - 1.12e-01 2.27e-04h 1\n", - " 4r 0.0000000e+00 2.51e+02 1.00e+03 2.4 0.00e+00 - 0.00e+00 2.84e-07R 4\n", - " 5r 0.0000000e+00 2.35e+02 2.10e+03 2.4 1.31e+04 - 8.56e-03 1.49e-02f 1\n", - " 6r 0.0000000e+00 2.33e+02 3.45e+03 1.0 1.24e+03 - 5.51e-02 2.07e-03f 1\n", - " 7r 0.0000000e+00 1.83e+02 5.32e+03 1.0 5.02e+03 - 2.27e-02 1.09e-02f 1\n", - " 8r 0.0000000e+00 1.70e+02 7.52e+03 1.0 3.56e+03 - 5.25e-02 4.04e-03f 1\n", - " 9r 0.0000000e+00 1.27e+02 1.02e+04 1.0 2.46e+03 - 1.06e-01 1.87e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 6.56e+01 2.01e+04 1.0 1.12e+03 - 3.63e-01 5.60e-02f 1\n", - " 11r 0.0000000e+00 3.55e+01 2.18e+04 1.0 2.21e+02 - 3.03e-01 1.42e-01f 1\n", - " 12r 0.0000000e+00 1.70e+01 2.29e+04 1.0 8.53e+01 - 5.79e-01 2.17e-01f 1\n", - " 13r 0.0000000e+00 1.39e+01 2.07e+04 1.0 3.36e+01 - 4.90e-02 9.28e-02f 1\n", - " 14r 0.0000000e+00 1.09e+01 1.86e+04 1.0 2.91e+01 - 2.30e-01 1.02e-01f 1\n", - " 15r 0.0000000e+00 3.48e+00 1.19e+04 1.0 2.33e+01 - 7.44e-01 3.19e-01f 1\n", - " 16r 0.0000000e+00 2.42e+00 8.14e+03 1.0 8.84e+00 - 2.88e-01 3.68e-01f 1\n", - " 17r 0.0000000e+00 2.45e+00 6.21e+03 1.0 1.85e+00 - 2.52e-01 2.81e-01f 1\n", - " 18r 0.0000000e+00 2.48e+00 4.36e+03 1.0 1.26e+00 - 2.84e-01 2.74e-01f 1\n", - " 19r 0.0000000e+00 2.51e+00 3.06e+03 1.0 6.66e-01 - 2.68e-01 2.60e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 2.53e+00 3.28e+03 1.0 7.04e-01 - 3.07e-01 3.65e-01f 1\n", - " 21r 0.0000000e+00 2.55e+00 2.35e+03 1.0 6.09e-01 - 3.82e-01 3.99e-01f 1\n", - " 22r 0.0000000e+00 2.57e+00 3.23e+03 1.0 4.76e-01 - 6.82e-01 1.00e+00f 1\n", - " 23r 0.0000000e+00 2.57e+00 1.41e-01 1.0 1.60e-01 - 1.00e+00 1.00e+00f 1\n", - " 24r 0.0000000e+00 2.87e+00 1.18e+02 -1.1 5.95e+00 - 8.69e-01 7.73e-01f 1\n", - " 25r 0.0000000e+00 2.84e+00 1.05e+03 -1.1 6.87e+00 - 4.86e-02 4.92e-01f 1\n", - " 26r 0.0000000e+00 2.47e+00 2.21e+03 -1.1 1.78e+01 - 3.48e-02 3.34e-01f 1\n", - " 27r 0.0000000e+00 2.00e+00 7.63e+02 -1.1 1.99e+01 - 5.19e-01 3.89e-01f 1\n", - " 28r 0.0000000e+00 7.56e-01 1.03e+03 -1.1 4.01e+01 - 3.41e-01 5.09e-01f 1\n", - " 29r 0.0000000e+00 1.13e-02 2.08e+03 -1.1 2.16e+01 - 1.78e-01 5.64e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 5.30e-03 1.16e+03 -1.1 9.51e+00 - 3.26e-01 1.34e-02f 1\n", - " 31r 0.0000000e+00 5.52e-03 1.97e+03 -1.1 1.86e+00 - 1.00e+00 3.77e-02f 1\n", - " 32r 0.0000000e+00 1.14e-02 1.04e+02 -1.1 1.87e+00 - 7.66e-01 1.00e+00f 1\n", - " 33r 0.0000000e+00 1.15e-02 1.58e-05 -1.1 1.75e-01 - 1.00e+00 1.00e+00f 1\n", - " 34r 0.0000000e+00 1.15e-02 3.84e+02 -4.0 3.45e-01 - 9.63e-01 7.35e-01f 1\n", - " 35r 0.0000000e+00 1.39e-03 3.05e+03 -4.0 4.05e+02 - 9.71e-01 6.73e-03f 1\n", - " 36r 0.0000000e+00 1.39e-03 3.64e+03 -4.0 2.50e+02 - 1.00e+00 2.55e-04f 1\n", - " 37r 0.0000000e+00 7.73e-04 2.45e+03 -4.0 4.49e-01 - 1.00e+00 3.79e-01f 1\n", - " 38r 0.0000000e+00 1.11e-04 3.84e+02 -4.0 1.97e-01 - 1.00e+00 8.57e-01f 1\n", - " 39r 0.0000000e+00 3.89e-05 1.28e+03 -4.0 2.82e-02 - 1.00e+00 6.49e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 40r 0.0000000e+00 5.46e-06 8.75e+02 -4.0 9.91e-03 - 1.00e+00 8.60e-01f 1\n", - " 41r 0.0000000e+00 7.94e-09 8.92e-07 -4.0 1.39e-03 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 41\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 7.5392754261827122e-09 7.9367750923575655e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 7.5392754261827122e-09 7.9367750923575655e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 46\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 46\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 43\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 41\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.370\n", - "Total CPU secs in NLP function evaluations = 0.031\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.4\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.92e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 2.63e+02 4.22e+01 -1.0 3.92e+02 - 7.52e-03 3.28e-01f 1\n", - " 2 0.0000000e+00 2.56e+02 4.09e+01 -1.0 2.63e+02 - 1.51e-01 2.93e-02h 1\n", - " 3 0.0000000e+00 2.56e+02 1.88e+04 -1.0 2.56e+02 - 2.78e-01 3.64e-04h 1\n", - " 4r 0.0000000e+00 2.56e+02 1.00e+03 2.4 0.00e+00 - 0.00e+00 4.56e-07R 4\n", - " 5r 0.0000000e+00 2.11e+02 9.30e+03 2.4 1.35e+03 - 5.28e-03 3.32e-02f 1\n", - " 6r 0.0000000e+00 1.91e+02 8.43e+03 1.7 1.19e+03 - 4.36e-02 1.76e-02f 1\n", - " 7r 0.0000000e+00 1.47e+02 6.95e+03 1.7 2.30e+03 - 4.86e-02 2.14e-02f 1\n", - " 8r 0.0000000e+00 1.17e+02 9.39e+03 1.7 1.63e+03 - 2.42e-01 1.83e-02f 1\n", - " 9r 0.0000000e+00 6.91e+01 1.17e+04 1.7 9.11e+02 - 3.84e-01 5.68e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 3.46e+01 1.09e+04 1.7 2.42e+02 - 1.00e+00 1.53e-01f 1\n", - " 11r 0.0000000e+00 2.02e+01 7.14e+03 1.7 4.19e+01 - 6.28e-01 3.44e-01f 1\n", - " 12r 0.0000000e+00 3.51e-01 1.44e+03 1.7 2.48e+01 - 1.00e+00 8.12e-01f 1\n", - " 13r 0.0000000e+00 4.10e-01 1.23e+03 1.0 4.59e+00 - 6.08e-01 1.61e-01f 1\n", - " 14r 0.0000000e+00 4.75e-01 1.49e+02 1.0 2.07e+00 - 1.00e+00 8.77e-01f 1\n", - " 15r 0.0000000e+00 5.52e-01 6.94e+02 0.3 3.21e+00 - 8.25e-01 3.97e-01f 1\n", - " 16r 0.0000000e+00 5.50e-01 3.86e+02 0.3 2.58e+00 - 1.00e+00 7.86e-01f 1\n", - " 17r 0.0000000e+00 5.27e-01 1.24e-01 0.3 7.99e-01 - 1.00e+00 1.00e+00f 1\n", - " 18r 0.0000000e+00 5.52e-01 7.33e+02 -1.8 1.28e+00 - 8.78e-01 3.61e-01f 1\n", - " 19r 0.0000000e+00 5.52e-01 7.69e+02 -1.8 1.91e+01 - 3.11e-01 1.63e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 5.43e-01 5.05e+02 -1.8 3.60e+00 - 4.54e-03 1.68e-01f 1\n", - " 21r 0.0000000e+00 2.60e-01 6.69e+02 -1.8 6.84e+01 - 4.13e-03 1.48e-01f 1\n", - " 22r 0.0000000e+00 2.55e-01 3.52e+02 -1.8 6.82e+01 - 7.60e-02 1.55e-03f 1\n", - " 23r 0.0000000e+00 2.37e-01 5.27e+02 -1.8 7.29e+01 - 8.64e-02 4.59e-03f 1\n", - " 24r 0.0000000e+00 2.03e-01 1.07e+03 -1.8 7.70e+01 - 1.27e-01 8.57e-03f 1\n", - " 25r 0.0000000e+00 1.31e-01 2.01e+03 -1.8 8.06e+01 - 2.26e-01 1.70e-02f 1\n", - " 26r 0.0000000e+00 2.55e-02 2.65e+03 -1.8 7.48e+01 - 3.10e-01 2.71e-02f 1\n", - " 27r 0.0000000e+00 3.51e-03 3.18e+03 -1.8 9.82e+00 - 9.46e-01 4.99e-02f 1\n", - " 28r 0.0000000e+00 2.39e-04 4.89e+02 -1.8 3.27e-01 - 1.00e+00 8.56e-01f 1\n", - " 29r 0.0000000e+00 5.43e-05 8.27e+02 -1.8 4.57e-02 - 1.00e+00 7.04e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 1.52e-05 6.90e-04 -1.8 1.36e-02 - 1.00e+00 1.00e+00f 1\n", - " 31r 0.0000000e+00 8.56e-06 1.25e+03 -4.0 3.76e-03 - 1.00e+00 7.07e-01f 1\n", - " 32r 0.0000000e+00 2.49e-06 9.69e+02 -4.0 2.26e-03 - 8.51e-01 7.40e-01f 1\n", - " 33r 0.0000000e+00 5.33e-08 6.71e-06 -4.0 5.23e-04 - 1.00e+00 1.00e+00f 1\n", - " 34r 0.0000000e+00 5.37e-10 1.01e-01 -6.0 2.49e-05 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 34\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 4.4637958782706658e-10 5.3720153514778086e-10\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 4.4637958782706658e-10 5.3720153514778086e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 39\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 39\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 36\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 34\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.319\n", - "Total CPU secs in NLP function evaluations = 0.007\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.3\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.51e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 2.10e+02 4.62e+01 -1.0 3.51e+02 - 8.40e-03 4.00e-01f 1\n", - " 2 0.0000000e+00 2.04e+02 4.48e+01 -1.0 2.10e+02 - 2.17e-01 2.95e-02h 1\n", - " 3 0.0000000e+00 2.04e+02 3.94e+04 -1.0 2.04e+02 - 4.22e-01 3.55e-04h 1\n", - " 4r 0.0000000e+00 2.04e+02 1.00e+03 2.3 0.00e+00 - 0.00e+00 4.44e-07R 4\n", - " 5r 0.0000000e+00 1.81e+02 3.64e+03 2.3 1.78e+04 - 3.08e-03 1.14e-02f 1\n", - " 6r 0.0000000e+00 1.79e+02 5.13e+03 1.6 1.42e+03 - 8.45e-02 1.49e-03f 1\n", - " 7r 0.0000000e+00 1.19e+02 1.08e+04 1.6 2.58e+03 - 7.51e-02 2.40e-02f 1\n", - " 8r 0.0000000e+00 6.76e+01 6.25e+03 1.6 1.81e+03 - 1.70e-01 2.92e-02f 1\n", - " 9r 0.0000000e+00 1.88e+01 6.76e+03 1.6 8.62e+02 - 3.07e-01 5.65e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 5.08e+00 6.06e+03 1.6 1.67e+02 - 2.49e-01 1.12e-01f 1\n", - " 11r 0.0000000e+00 3.81e+00 4.70e+03 1.6 1.06e+01 - 3.13e-01 2.13e-01f 1\n", - " 12r 0.0000000e+00 2.12e+00 1.83e+03 1.6 6.54e+00 - 7.68e-01 4.05e-01f 1\n", - " 13r 0.0000000e+00 1.94e+00 1.61e+03 0.9 2.15e+00 - 6.59e-01 8.52e-02f 1\n", - " 14r 0.0000000e+00 2.67e-01 2.64e+02 0.9 2.93e+00 - 1.00e+00 8.54e-01f 1\n", - " 15r 0.0000000e+00 1.91e-01 8.59e+02 0.2 8.87e-01 - 8.22e-01 2.83e-01f 1\n", - " 16r 0.0000000e+00 6.01e-02 8.76e+02 0.2 2.97e+00 - 1.00e+00 6.84e-01f 1\n", - " 17r 0.0000000e+00 2.69e-02 2.67e+00 0.2 1.04e+00 - 1.00e+00 1.00e+00f 1\n", - " 18r 0.0000000e+00 3.87e-02 1.69e+02 -1.9 6.90e-01 - 4.97e-01 4.22e-01f 1\n", - " 19r 0.0000000e+00 3.87e-02 3.21e+02 -1.9 1.56e+01 - 2.54e-01 1.72e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 2.81e-02 3.53e+02 -1.9 3.29e+00 - 3.47e-03 2.20e-01f 1\n", - " 21r 0.0000000e+00 1.46e-02 3.77e+02 -1.9 4.69e+01 - 5.08e-03 8.11e-03f 1\n", - " 22r 0.0000000e+00 1.26e-02 3.42e+02 -1.9 3.88e+01 - 1.21e-02 1.00e-03f 1\n", - " 23r 0.0000000e+00 3.16e-03 2.94e+02 -1.9 2.90e+01 - 2.40e-02 6.20e-03f 1\n", - " 24r 0.0000000e+00 1.80e-03 2.20e+03 -1.9 4.26e+00 - 8.01e-01 1.54e-02f 1\n", - " 25r 0.0000000e+00 2.09e-04 6.36e+02 -1.9 5.92e-01 - 1.00e+00 7.90e-01f 1\n", - " 26r 0.0000000e+00 4.97e-05 7.73e+02 -1.9 4.07e-02 - 1.00e+00 7.06e-01f 1\n", - " 27r 0.0000000e+00 1.73e-05 2.67e+01 -1.9 1.17e-02 - 1.00e+00 9.81e-01f 1\n", - " 28r 0.0000000e+00 4.43e-06 1.78e-04 -1.9 2.17e-04 - 1.00e+00 1.00e+00f 1\n", - " 29r 0.0000000e+00 2.35e-06 5.22e+01 -4.2 1.68e-03 - 1.00e+00 9.10e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 2.92e-07 1.85e+01 -4.2 3.04e-04 - 1.00e+00 8.79e-01f 1\n", - " 31r 0.0000000e+00 1.26e-09 7.00e-08 -4.2 1.09e-04 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 31\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.2612473843098826e-09 1.2612473843098826e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.2612473843098826e-09 1.2612473843098826e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 36\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 36\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 33\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 31\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.271\n", - "Total CPU secs in NLP function evaluations = 0.023\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.1\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.83e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 6.16e+01 3.98e+01 -1.0 1.83e+02 - 1.60e-02 6.64e-01f 1\n", - " 2 0.0000000e+00 5.16e+01 3.35e+01 -1.0 6.16e+01 - 3.45e-01 1.63e-01h 1\n", - " 3 0.0000000e+00 5.14e+01 1.26e+04 -1.0 5.16e+01 - 7.21e-01 4.84e-03h 1\n", - " 4 0.0000000e+00 5.14e+01 2.29e+08 -1.0 5.14e+01 - 9.11e-01 5.02e-05h 1\n", - " 5r 0.0000000e+00 5.14e+01 1.00e+03 1.7 0.00e+00 - 0.00e+00 2.52e-07R 2\n", - " 6r 0.0000000e+00 4.50e+01 1.23e+04 1.7 1.84e+04 - 2.52e-02 2.32e-03f 1\n", - " 7r 0.0000000e+00 3.54e+01 4.07e+04 1.7 2.49e+03 - 6.96e-02 3.85e-03f 1\n", - " 8r 0.0000000e+00 5.85e+00 4.93e+04 1.7 1.43e+03 - 8.64e-02 2.45e-02f 1\n", - " 9r 0.0000000e+00 3.35e+00 1.48e+04 1.7 5.73e+01 - 7.58e-01 5.86e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 2.86e+00 1.09e+04 1.7 4.88e+00 - 1.57e-01 1.13e-01f 1\n", - " 11r 0.0000000e+00 2.68e-01 2.71e+04 1.7 4.02e+00 - 1.46e-01 8.12e-01f 1\n", - " 12r 0.0000000e+00 1.04e+00 2.76e+03 1.7 4.27e+00 - 8.58e-01 1.00e+00f 1\n", - " 13r 0.0000000e+00 2.25e-01 1.74e+02 1.0 4.50e+00 - 8.78e-01 9.22e-01f 1\n", - " 14r 0.0000000e+00 3.86e-02 9.86e+02 0.3 2.51e+00 - 1.00e+00 4.78e-01f 1\n", - " 15r 0.0000000e+00 2.63e-02 1.70e+03 0.3 1.78e+00 - 1.00e+00 4.16e-01f 1\n", - " 16r 0.0000000e+00 8.78e-03 1.77e+02 0.3 1.30e+00 - 1.00e+00 9.07e-01f 1\n", - " 17r 0.0000000e+00 8.83e-03 5.64e-04 0.3 3.49e-01 - 1.00e+00 1.00e+00f 1\n", - " 18r 0.0000000e+00 2.39e-03 2.14e+02 -1.8 1.08e+00 - 9.93e-01 7.08e-01f 1\n", - " 19r 0.0000000e+00 6.59e-04 3.82e+02 -1.8 2.55e+00 - 1.00e+00 3.14e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 1.07e-04 7.78e+01 -1.8 1.63e-01 - 1.00e+00 8.17e-01f 1\n", - " 21r 0.0000000e+00 2.42e-05 4.73e+01 -1.8 2.45e-02 - 1.00e+00 8.44e-01f 1\n", - " 22r 0.0000000e+00 2.70e-06 4.22e-07 -1.8 4.46e-03 - 1.00e+00 1.00e+00f 1\n", - " 23r 0.0000000e+00 1.78e-06 3.08e+00 -4.0 1.12e-03 - 1.00e+00 9.46e-01f 1\n", - " 24r 0.0000000e+00 1.34e-07 4.68e-07 -4.0 4.21e-04 - 1.00e+00 1.00e+00f 1\n", - " 25r 0.0000000e+00 9.50e-11 2.65e-02 -6.0 5.53e-05 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 25\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.5036423175542950e-11 9.5036423175542950e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.5036423175542950e-11 9.5036423175542950e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 28\n", - "Number of objective gradient evaluations = 7\n", - "Number of equality constraint evaluations = 28\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 27\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 25\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.235\n", - "Total CPU secs in NLP function evaluations = 0.015\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.5\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.85e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 2.08e+02 1.71e+02 -1.0 3.85e+02 - 2.65e-03 4.60e-01f 1\n", - " 2 0.0000000e+00 2.04e+02 1.68e+02 -1.0 2.08e+02 - 7.77e-02 1.78e-02h 1\n", - " 3 0.0000000e+00 2.04e+02 8.66e+03 -1.0 2.04e+02 - 1.18e-01 2.03e-04h 1\n", - " 4r 0.0000000e+00 2.04e+02 1.00e+03 2.3 0.00e+00 - 0.00e+00 2.54e-07R 4\n", - " 5r 0.0000000e+00 1.94e+02 9.86e+02 2.3 1.29e+04 - 1.19e-02 1.28e-02f 1\n", - " 6r 0.0000000e+00 1.91e+02 1.69e+04 0.9 1.49e+03 - 5.89e-02 2.58e-03f 1\n", - " 7r 0.0000000e+00 1.47e+02 3.49e+04 0.9 4.77e+03 - 2.90e-02 9.30e-03f 1\n", - " 8r 0.0000000e+00 1.09e+02 3.82e+04 0.9 3.42e+03 - 5.74e-02 1.25e-02f 1\n", - " 9r 0.0000000e+00 1.03e+02 3.57e+04 0.9 1.67e+03 - 2.11e-01 3.92e-03f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 3.97e+01 3.04e+04 0.9 5.25e+02 - 1.76e-01 1.26e-01f 1\n", - " 11r 0.0000000e+00 2.44e+01 3.83e+04 0.9 1.50e+02 - 5.49e-01 1.02e-01f 1\n", - " 12r 0.0000000e+00 7.22e+00 3.16e+04 0.9 5.01e+01 - 7.21e-01 3.44e-01f 1\n", - " 13r 0.0000000e+00 2.66e+00 1.79e+04 0.9 1.69e+01 - 6.25e-01 4.25e-01f 1\n", - " 14r 0.0000000e+00 2.70e+00 1.27e+04 0.9 1.52e+00 - 3.28e-01 2.87e-01f 1\n", - " 15r 0.0000000e+00 2.72e+00 8.79e+03 0.9 1.07e+00 - 3.59e-01 2.85e-01f 1\n", - " 16r 0.0000000e+00 2.79e+00 6.00e+03 0.9 1.91e+00 - 3.34e-01 2.96e-01f 1\n", - " 17r 0.0000000e+00 2.81e+00 4.53e+03 0.9 1.14e+00 - 2.48e-01 2.63e-01f 1\n", - " 18r 0.0000000e+00 2.86e+00 2.90e+03 0.9 1.45e+00 - 3.42e-01 3.27e-01f 1\n", - " 19r 0.0000000e+00 2.88e+00 1.22e+03 0.9 7.48e-01 - 3.63e-01 3.24e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 2.92e+00 3.75e+03 0.9 6.52e-01 - 5.04e-01 5.89e-01f 1\n", - " 21r 0.0000000e+00 2.93e+00 4.25e+04 0.9 2.49e-01 - 4.20e-01 9.81e-01f 1\n", - " 22r 0.0000000e+00 2.93e+00 1.50e+03 0.9 5.14e-02 - 8.63e-01 1.00e+00f 1\n", - " 23r 0.0000000e+00 2.93e+00 7.38e-03 0.9 6.94e-03 - 1.00e+00 1.00e+00f 1\n", - " 24r 0.0000000e+00 3.29e+00 1.59e+02 -1.2 4.49e+00 - 8.54e-01 7.15e-01f 1\n", - " 25r 0.0000000e+00 2.68e+00 3.13e+02 -1.2 3.24e+01 - 1.92e-01 2.63e-01f 1\n", - " 26r 0.0000000e+00 1.50e+00 3.12e+02 -1.2 3.25e+01 - 3.28e-01 3.55e-01f 1\n", - " 27r 0.0000000e+00 1.07e-01 3.03e+02 -1.2 3.79e+01 - 3.23e-01 3.74e-01f 1\n", - " 28r 0.0000000e+00 1.07e-01 9.17e+02 -1.2 3.15e+01 - 2.55e-01 5.49e-03f 1\n", - " 29r 0.0000000e+00 1.07e-01 2.86e+03 -1.2 1.92e+01 - 7.85e-01 1.14e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 1.01e-01 2.05e+03 -1.2 5.56e+00 - 1.00e+00 3.41e-01f 1\n", - " 31r 0.0000000e+00 9.73e-02 1.58e+03 -1.2 3.30e+00 - 1.00e+00 3.01e-01f 1\n", - " 32r 0.0000000e+00 8.92e-02 4.60e+01 -1.2 2.31e+00 - 1.00e+00 9.73e-01f 1\n", - " 33r 0.0000000e+00 8.90e-02 8.87e-04 -1.2 1.20e-01 - 1.00e+00 1.00e+00f 1\n", - " 34r 0.0000000e+00 8.91e-02 3.61e+02 -4.2 1.72e-01 - 9.81e-01 7.79e-01f 1\n", - " 35r 0.0000000e+00 7.41e-02 1.37e+03 -4.2 6.60e+02 - 1.61e-01 5.45e-03f 1\n", - " 36r 0.0000000e+00 7.35e-02 6.49e+03 -4.2 6.55e+02 - 9.03e-01 2.46e-04f 1\n", - " 37r 0.0000000e+00 3.03e-04 6.68e+03 -4.2 5.94e+02 - 8.93e-01 2.98e-02f 1\n", - " 38r 0.0000000e+00 2.75e-04 6.08e+03 -4.2 2.39e-01 - 1.00e+00 8.74e-02f 1\n", - " 39r 0.0000000e+00 1.08e-08 2.67e-04 -4.2 8.74e-02 - 1.00e+00 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 40r 0.0000000e+00 8.78e-11 2.37e-01 -6.4 2.18e-06 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 40\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 7.3066333062577876e-11 8.7764854222018352e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 7.3066333062577876e-11 8.7764854222018352e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 45\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 45\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 42\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 40\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.307\n", - "Total CPU secs in NLP function evaluations = 0.049\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.1\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.96e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 2.36e+02 1.43e+02 -1.0 3.96e+02 - 2.78e-03 4.05e-01f 1\n", - " 2 0.0000000e+00 2.32e+02 1.41e+02 -1.0 2.36e+02 - 1.32e-01 1.71e-02h 1\n", - " 3 0.0000000e+00 2.32e+02 2.28e+04 -1.0 2.32e+02 - 1.88e-01 1.93e-04h 1\n", - " 4r 0.0000000e+00 2.32e+02 1.00e+03 2.4 0.00e+00 - 0.00e+00 4.83e-07R 3\n", - " 5r 0.0000000e+00 2.17e+02 9.85e+02 2.4 1.19e+04 - 1.06e-02 1.36e-02f 1\n", - " 6r 0.0000000e+00 2.14e+02 1.30e+04 1.0 1.27e+03 - 5.82e-02 2.44e-03f 1\n", - " 7r 0.0000000e+00 1.62e+02 2.34e+04 1.0 4.91e+03 - 2.38e-02 1.16e-02f 1\n", - " 8r 0.0000000e+00 1.57e+02 2.45e+04 1.0 3.32e+03 - 6.10e-02 1.67e-03f 1\n", - " 9r 0.0000000e+00 1.32e+02 2.26e+04 1.0 2.02e+03 - 1.26e-01 1.32e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 6.58e+01 2.03e+04 1.0 9.37e+02 - 1.95e-01 7.75e-02f 1\n", - " 11r 0.0000000e+00 4.25e+01 3.75e+04 1.0 2.34e+02 - 5.24e-01 9.91e-02f 1\n", - " 12r 0.0000000e+00 9.87e+00 1.92e+04 1.0 6.76e+01 - 4.88e-01 4.85e-01f 1\n", - " 13r 0.0000000e+00 8.03e+00 1.63e+04 1.0 1.29e+01 - 1.21e-01 1.43e-01f 1\n", - " 14r 0.0000000e+00 6.78e+00 1.54e+04 1.0 1.03e+01 - 4.81e-01 1.22e-01f 1\n", - " 15r 0.0000000e+00 2.80e+00 7.26e+03 1.0 8.05e+00 - 6.67e-01 4.96e-01f 1\n", - " 16r 0.0000000e+00 2.51e+00 5.18e+03 1.0 3.44e+00 - 2.86e-01 2.98e-01f 1\n", - " 17r 0.0000000e+00 2.53e+00 3.39e+03 1.0 2.35e+00 - 2.85e-01 1.95e-01f 1\n", - " 18r 0.0000000e+00 2.56e+00 2.71e+03 1.0 1.86e+00 - 2.75e-01 3.16e-01f 1\n", - " 19r 0.0000000e+00 2.58e+00 2.08e+03 1.0 1.22e+00 - 2.65e-01 2.72e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 2.61e+00 2.39e+03 1.0 8.14e-01 - 3.60e-01 4.17e-01f 1\n", - " 21r 0.0000000e+00 2.62e+00 1.38e+03 1.0 3.95e-01 - 4.37e-01 3.67e-01f 1\n", - " 22r 0.0000000e+00 2.63e+00 9.07e+03 1.0 2.59e-01 - 7.30e-01 1.00e+00f 1\n", - " 23r 0.0000000e+00 2.63e+00 2.12e-01 1.0 7.58e-02 - 1.00e+00 1.00e+00f 1\n", - " 24r 0.0000000e+00 3.06e+00 6.51e+01 -1.1 8.23e+00 - 8.22e-01 7.61e-01f 1\n", - " 25r 0.0000000e+00 2.19e+00 1.34e+03 -1.1 2.73e+01 - 2.45e-01 4.74e-01f 1\n", - " 26r 0.0000000e+00 9.32e-01 7.84e+02 -1.1 4.34e+01 - 4.27e-01 4.32e-01f 1\n", - " 27r 0.0000000e+00 1.88e-02 5.84e+02 -1.1 5.35e+01 - 2.86e-01 2.55e-01f 1\n", - " 28r 0.0000000e+00 1.89e-02 2.49e+03 -1.1 3.90e+01 - 3.89e-01 3.63e-03f 1\n", - " 29r 0.0000000e+00 1.87e-02 5.16e+03 -1.1 2.92e+00 - 1.00e+00 3.09e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 1.28e-02 9.16e-03 -1.1 2.56e+00 - 1.00e+00 1.00e+00f 1\n", - " 31r 0.0000000e+00 1.27e-02 1.52e+02 -4.1 1.56e-01 - 9.63e-01 9.13e-01f 1\n", - " 32r 0.0000000e+00 5.45e-03 5.50e+02 -4.1 4.28e+02 - 1.00e+00 4.09e-03f 1\n", - " 33r 0.0000000e+00 3.87e-04 9.26e+02 -4.1 3.38e+01 - 1.00e+00 3.76e-02f 1\n", - " 34r 0.0000000e+00 1.37e-05 6.68e+01 -4.1 1.08e-01 - 1.00e+00 9.28e-01f 1\n", - " 35r 0.0000000e+00 5.96e-09 2.75e-07 -4.1 3.78e-03 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 35\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.9644783556578318e-09 5.9644783556578318e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.9644783556578318e-09 5.9644783556578318e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 39\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 39\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 37\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 35\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.282\n", - "Total CPU secs in NLP function evaluations = 0.018\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.1\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.91e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 2.24e+02 8.48e+01 -1.0 3.91e+02 - 4.93e-03 4.28e-01f 1\n", - " 2 0.0000000e+00 2.19e+02 8.30e+01 -1.0 2.24e+02 - 1.02e-01 2.13e-02h 1\n", - " 3 0.0000000e+00 2.19e+02 1.20e+04 -1.0 2.19e+02 - 1.62e-01 2.46e-04h 1\n", - " 4r 0.0000000e+00 2.19e+02 1.00e+03 2.3 0.00e+00 - 0.00e+00 3.08e-07R 4\n", - " 5r 0.0000000e+00 2.10e+02 9.91e+02 2.3 9.42e+03 - 6.94e-03 7.96e-03f 1\n", - " 6r 0.0000000e+00 2.03e+02 8.13e+03 0.9 1.28e+03 - 5.02e-02 5.28e-03f 1\n", - " 7r 0.0000000e+00 1.48e+02 1.44e+04 0.9 5.00e+03 - 2.26e-02 1.21e-02f 1\n", - " 8r 0.0000000e+00 1.39e+02 1.73e+04 0.9 3.27e+03 - 5.92e-02 2.98e-03f 1\n", - " 9r 0.0000000e+00 1.03e+02 1.60e+04 0.9 1.87e+03 - 1.25e-01 1.91e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 4.88e+01 2.24e+04 0.9 7.38e+02 - 4.01e-01 7.65e-02f 1\n", - " 11r 0.0000000e+00 1.43e+01 1.91e+04 0.9 1.24e+02 - 4.48e-01 2.85e-01f 1\n", - " 12r 0.0000000e+00 1.09e+01 2.06e+04 0.9 2.83e+01 - 5.27e-01 1.19e-01f 1\n", - " 13r 0.0000000e+00 1.73e+00 8.52e+03 0.9 1.86e+01 - 3.55e-01 5.53e-01f 1\n", - " 14r 0.0000000e+00 1.74e+00 6.65e+03 0.9 3.33e+00 - 6.71e-01 2.03e-01f 1\n", - " 15r 0.0000000e+00 1.77e+00 3.19e+03 0.9 6.45e-01 - 5.58e-01 5.19e-01f 1\n", - " 16r 0.0000000e+00 1.81e+00 1.87e+03 0.9 7.55e-01 - 4.69e-01 4.19e-01f 1\n", - " 17r 0.0000000e+00 1.84e+00 8.90e+02 0.9 6.66e-01 - 5.24e-01 5.24e-01f 1\n", - " 18r 0.0000000e+00 1.86e+00 7.28e+03 0.9 3.80e-01 - 6.38e-01 9.45e-01f 1\n", - " 19r 0.0000000e+00 1.86e+00 2.29e-01 0.9 1.35e-01 - 1.00e+00 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 2.11e+00 1.47e+02 -1.2 7.42e+00 - 8.33e-01 6.69e-01f 1\n", - " 21r 0.0000000e+00 2.09e+00 8.04e+02 -1.2 9.84e+00 - 5.60e-02 5.23e-01f 1\n", - " 22r 0.0000000e+00 1.67e+00 2.28e+03 -1.2 2.04e+01 - 2.82e-02 4.16e-01f 1\n", - " 23r 0.0000000e+00 1.29e+00 1.28e+03 -1.2 2.24e+01 - 3.92e-01 3.52e-01f 1\n", - " 24r 0.0000000e+00 4.93e-01 1.35e+03 -1.2 3.49e+01 - 1.31e-01 4.69e-01f 1\n", - " 25r 0.0000000e+00 1.86e-02 1.21e+03 -1.2 2.81e+01 - 2.80e-01 3.53e-01f 1\n", - " 26r 0.0000000e+00 1.87e-02 7.43e+02 -1.2 2.09e+01 - 2.66e-01 5.16e-03f 1\n", - " 27r 0.0000000e+00 1.89e-02 1.38e+03 -1.2 5.45e+00 - 9.74e-01 2.34e-02f 1\n", - " 28r 0.0000000e+00 2.27e-02 6.45e-03 -1.2 8.25e-01 - 1.00e+00 1.00e+00f 1\n", - " 29r 0.0000000e+00 2.26e-02 9.20e+02 -4.2 1.72e-01 - 9.80e-01 7.59e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 1.75e-02 5.71e+03 -4.2 4.73e+02 - 8.52e-01 2.65e-03f 1\n", - " 31r 0.0000000e+00 1.72e-02 5.75e+03 -4.2 2.67e+02 - 9.30e-01 2.46e-04f 1\n", - " 32r 0.0000000e+00 4.12e-04 3.38e+03 -4.2 1.00e+01 - 1.00e+00 4.12e-01f 1\n", - " 33r 0.0000000e+00 5.00e-06 4.10e+01 -4.2 9.28e-02 - 1.00e+00 9.88e-01f 1\n", - " 34r 0.0000000e+00 5.72e-09 1.95e-08 -4.2 1.14e-03 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 34\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.7186637647532734e-09 5.7186637647532734e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.7186637647532734e-09 5.7186637647532734e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 39\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 39\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 36\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 34\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.280\n", - "Total CPU secs in NLP function evaluations = 0.016\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.2\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.20e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.98e+02 4.01e+01 -1.0 3.20e+02 - 9.18e-03 3.81e-01f 1\n", - " 2 0.0000000e+00 1.89e+02 3.82e+01 -1.0 1.98e+02 - 1.50e-01 4.65e-02h 1\n", - " 3 0.0000000e+00 1.89e+02 1.15e+04 -1.0 1.89e+02 - 2.92e-01 6.46e-04h 1\n", - " 4r 0.0000000e+00 1.89e+02 1.00e+03 2.3 0.00e+00 - 0.00e+00 4.05e-07R 5\n", - " 5r 0.0000000e+00 1.48e+02 9.55e+03 2.3 1.59e+03 - 3.15e-03 2.97e-02f 1\n", - " 6r 0.0000000e+00 1.35e+02 9.39e+03 1.6 1.41e+03 - 5.41e-02 8.85e-03f 1\n", - " 7r 0.0000000e+00 1.07e+02 9.99e+03 1.6 2.53e+03 - 6.98e-02 1.11e-02f 1\n", - " 8r 0.0000000e+00 5.51e+01 1.07e+04 1.6 1.83e+03 - 1.60e-01 2.86e-02f 1\n", - " 9r 0.0000000e+00 1.52e+01 1.04e+04 1.6 8.87e+02 - 3.72e-01 4.50e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 2.13e+00 9.47e+03 1.6 1.20e+02 - 5.89e-01 1.26e-01f 1\n", - " 11r 0.0000000e+00 1.13e+00 6.09e+03 1.6 5.27e+00 - 4.85e-01 3.78e-01f 1\n", - " 12r 0.0000000e+00 4.39e-01 3.50e+03 1.6 2.77e+00 - 9.96e-01 5.47e-01f 1\n", - " 13r 0.0000000e+00 1.67e-01 5.99e+01 1.6 7.56e-01 - 1.00e+00 1.00e+00f 1\n", - " 14r 0.0000000e+00 1.43e-01 6.08e+02 -0.5 2.07e+00 - 4.41e-01 2.19e-01f 1\n", - " 15r 0.0000000e+00 1.14e-01 9.82e+02 -0.5 5.71e+00 - 6.67e-01 2.18e-01f 1\n", - " 16r 0.0000000e+00 1.01e-01 1.95e+03 -0.5 6.61e+00 - 9.73e-01 1.02e-01f 1\n", - " 17r 0.0000000e+00 2.24e-02 6.60e+02 -0.5 4.47e+00 - 1.00e+00 7.58e-01f 1\n", - " 18r 0.0000000e+00 5.86e-03 6.65e+02 -0.5 2.81e+00 - 1.00e+00 7.35e-01f 1\n", - " 19r 0.0000000e+00 1.87e-03 5.67e+02 -0.5 8.99e-01 - 1.00e+00 8.08e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 9.39e-04 7.94e-03 -0.5 3.57e-01 - 1.00e+00 1.00e+00f 1\n", - " 21r 0.0000000e+00 3.11e-04 4.65e+02 -2.9 2.41e-01 - 1.00e+00 6.65e-01f 1\n", - " 22r 0.0000000e+00 7.23e-05 1.37e+03 -2.9 3.38e-01 - 9.96e-01 3.40e-01f 1\n", - " 23r 0.0000000e+00 1.78e-05 6.46e+02 -2.9 1.48e-02 - 1.00e+00 8.26e-01f 1\n", - " 24r 0.0000000e+00 2.87e-06 2.62e+02 -2.9 3.33e-03 - 9.07e-01 1.00e+00f 1\n", - " 25r 0.0000000e+00 1.20e-07 2.54e-06 -2.9 2.24e-04 - 1.00e+00 1.00e+00f 1\n", - " 26r 0.0000000e+00 2.61e-08 1.39e+00 -6.5 4.87e-05 - 1.00e+00 9.92e-01f 1\n", - " 27r 0.0000000e+00 9.46e-10 7.24e-01 -6.5 3.19e-06 - 1.00e+00 9.84e-01f 1\n", - "\n", - "Number of Iterations....: 27\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.4556971974579795e-10 9.4556971974579795e-10\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.4556971974579795e-10 9.4556971974579795e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 33\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 33\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 29\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 27\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.236\n", - "Total CPU secs in NLP function evaluations = 0.016\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.4\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.79e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.95e+02 1.42e+02 -1.0 3.79e+02 - 3.35e-03 4.85e-01f 1\n", - " 2 0.0000000e+00 1.90e+02 1.39e+02 -1.0 1.95e+02 - 6.27e-02 2.35e-02h 1\n", - " 3 0.0000000e+00 1.90e+02 4.85e+03 -1.0 1.90e+02 - 1.07e-01 2.78e-04h 1\n", - " 4r 0.0000000e+00 1.90e+02 1.00e+03 2.3 0.00e+00 - 0.00e+00 3.48e-07R 4\n", - " 5r 0.0000000e+00 1.83e+02 1.25e+03 2.3 1.20e+04 - 9.88e-03 9.54e-03f 1\n", - " 6r 0.0000000e+00 1.80e+02 1.08e+04 0.9 1.40e+03 - 5.06e-02 3.21e-03f 1\n", - " 7r 0.0000000e+00 1.29e+02 1.89e+04 0.9 5.14e+03 - 2.68e-02 1.17e-02f 1\n", - " 8r 0.0000000e+00 1.20e+02 1.97e+04 0.9 3.41e+03 - 6.44e-02 2.90e-03f 1\n", - " 9r 0.0000000e+00 7.81e+01 1.73e+04 0.9 1.72e+03 - 1.46e-01 2.61e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 4.04e+01 1.34e+04 0.9 5.31e+02 - 1.94e-01 7.10e-02f 1\n", - " 11r 0.0000000e+00 1.84e+01 2.35e+04 0.9 1.78e+02 - 4.56e-01 1.24e-01f 1\n", - " 12r 0.0000000e+00 2.31e+00 1.73e+04 0.9 5.28e+01 - 4.92e-01 3.46e-01f 1\n", - " 13r 0.0000000e+00 2.32e+00 1.46e+04 0.9 2.67e+00 - 2.76e-01 1.71e-01f 1\n", - " 14r 0.0000000e+00 2.38e+00 8.69e+03 0.9 2.34e+00 - 1.52e-01 4.16e-01f 1\n", - " 15r 0.0000000e+00 2.46e+00 5.56e+03 0.9 2.11e+00 - 3.20e-01 4.13e-01f 1\n", - " 16r 0.0000000e+00 2.49e+00 2.92e+03 0.9 7.46e-01 - 5.68e-01 6.55e-01f 1\n", - " 17r 0.0000000e+00 2.50e+00 6.54e+03 0.9 6.35e-01 - 6.89e-01 4.64e-01f 1\n", - " 18r 0.0000000e+00 2.50e+00 1.87e+03 0.9 4.61e-01 - 3.88e-01 4.89e-01f 1\n", - " 19r 0.0000000e+00 2.51e+00 4.78e+03 0.9 3.13e-01 - 5.43e-01 9.57e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 2.50e+00 1.46e-01 0.9 1.10e-01 - 1.00e+00 1.00e+00f 1\n", - " 21r 0.0000000e+00 2.82e+00 5.49e+01 -1.2 7.01e+00 - 8.58e-01 7.83e-01f 1\n", - " 22r 0.0000000e+00 2.11e+00 1.56e+03 -1.2 2.69e+01 - 1.33e-01 4.64e-01f 1\n", - " 23r 0.0000000e+00 1.24e+00 1.21e+03 -1.2 3.79e+01 - 3.54e-01 4.06e-01f 1\n", - " 24r 0.0000000e+00 2.10e-02 1.01e+03 -1.2 4.93e+01 - 2.92e-01 4.34e-01f 1\n", - " 25r 0.0000000e+00 2.13e-02 1.06e+03 -1.2 3.44e+01 - 3.13e-01 6.32e-03f 1\n", - " 26r 0.0000000e+00 2.12e-02 2.85e+03 -1.2 8.72e+00 - 9.89e-01 1.30e-02f 1\n", - " 27r 0.0000000e+00 1.80e-02 1.32e+03 -1.2 1.90e+00 - 1.00e+00 5.52e-01f 1\n", - " 28r 0.0000000e+00 1.54e-02 1.66e-03 -1.2 6.86e-01 - 1.00e+00 1.00e+00f 1\n", - " 29r 0.0000000e+00 1.52e-02 6.58e+02 -4.3 1.27e-01 - 9.97e-01 6.89e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30r 0.0000000e+00 4.21e-03 4.72e+03 -4.3 5.52e+02 - 9.32e-01 4.85e-03f 1\n", - " 31r 0.0000000e+00 2.80e-03 5.53e+03 -4.3 7.54e+01 - 9.19e-01 4.50e-03f 1\n", - " 32r 0.0000000e+00 4.43e-05 1.13e+03 -4.3 8.54e-01 - 1.00e+00 7.96e-01f 1\n", - " 33r 0.0000000e+00 1.04e-08 2.00e-05 -4.3 9.88e-03 - 1.00e+00 1.00e+00f 1\n", - " 34r 0.0000000e+00 6.20e-11 1.70e-01 -6.5 2.71e-06 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 34\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.3625672114701202e-11 6.1975749971474384e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.3625672114701202e-11 6.1975749971474384e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 39\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 39\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 36\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 34\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.303\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.3\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.92e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 2.91e+02 3.29e+01 -1.0 3.92e+02 - 7.53e-03 2.58e-01f 1\n", - " 2 0.0000000e+00 2.82e+02 3.20e+01 -1.0 2.91e+02 - 1.86e-01 2.98e-02h 1\n", - " 3 0.0000000e+00 2.82e+02 2.19e+04 -1.0 2.82e+02 - 2.86e-01 3.83e-04h 1\n", - " 4r 0.0000000e+00 2.82e+02 1.00e+03 2.4 0.00e+00 - 0.00e+00 4.80e-07R 4\n", - " 5r 0.0000000e+00 2.69e+02 2.38e+03 2.4 1.17e+04 - 4.31e-03 9.93e-03f 1\n", - " 6r 0.0000000e+00 2.64e+02 2.51e+03 1.1 1.25e+03 - 4.37e-02 3.56e-03f 1\n", - " 7r 0.0000000e+00 2.09e+02 3.67e+03 1.1 4.56e+03 - 2.42e-02 1.23e-02f 1\n", - " 8r 0.0000000e+00 2.03e+02 7.82e+03 1.1 3.40e+03 - 1.53e-01 1.74e-03f 1\n", - " 9r 0.0000000e+00 9.66e+01 7.79e+03 1.1 1.93e+03 - 7.98e-02 5.71e-02f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 6.20e+01 8.90e+03 1.1 8.06e+02 - 1.95e-01 4.86e-02f 1\n", - " 11r 0.0000000e+00 3.84e+01 1.20e+04 1.1 2.44e+02 - 7.62e-01 9.71e-02f 1\n", - " 12r 0.0000000e+00 1.60e+01 6.06e+03 1.1 5.46e+01 - 1.00e+00 4.10e-01f 1\n", - " 13r 0.0000000e+00 5.82e-01 1.27e+03 1.1 2.06e+01 - 1.00e+00 7.70e-01f 1\n", - " 14r 0.0000000e+00 4.84e-01 3.32e+03 1.1 9.52e-01 - 1.00e+00 1.69e-01f 1\n", - " 15r 0.0000000e+00 9.97e-02 2.79e+01 1.1 1.14e+00 - 1.00e+00 1.00e+00f 1\n", - " 16r 0.0000000e+00 8.91e-02 4.40e+02 -1.0 1.27e+00 - 5.29e-01 3.11e-01f 1\n", - " 17r 0.0000000e+00 7.90e-02 4.60e+02 -1.0 8.04e+00 - 2.34e-01 8.48e-02f 1\n", - " 18r 0.0000000e+00 7.13e-02 1.09e+03 -1.0 6.71e+00 - 7.40e-01 7.36e-02f 1\n", - " 19r 0.0000000e+00 2.65e-02 6.12e+02 -1.0 5.05e+00 - 1.00e+00 5.80e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20r 0.0000000e+00 9.27e-03 5.14e+02 -1.0 3.33e+00 - 1.00e+00 7.03e-01f 1\n", - " 21r 0.0000000e+00 6.56e-04 5.07e+02 -1.0 1.64e+00 - 1.00e+00 7.90e-01f 1\n", - " 22r 0.0000000e+00 1.79e-04 7.46e+01 -1.0 2.19e-01 - 1.00e+00 9.76e-01f 1\n", - " 23r 0.0000000e+00 2.84e-04 8.65e+02 -1.0 5.80e-02 - 4.80e-01 1.00e+00f 1\n", - " 24r 0.0000000e+00 3.74e-04 9.40e-06 -1.0 2.16e-02 - 1.00e+00 1.00e+00f 1\n", - " 25r 0.0000000e+00 3.06e-05 5.52e+02 -3.9 6.97e-02 - 1.00e+00 7.26e-01f 1\n", - " 26r 0.0000000e+00 1.41e-05 2.27e+03 -3.9 2.89e-02 - 9.71e-01 4.66e-01f 1\n", - " 27r 0.0000000e+00 4.15e-06 1.11e+03 -3.9 1.25e-03 - 9.53e-01 8.10e-01f 1\n", - " 28r 0.0000000e+00 1.67e-07 1.22e+02 -3.9 7.64e-04 - 9.19e-01 1.00e+00f 1\n", - " 29r 0.0000000e+00 4.10e-09 2.54e-08 -3.9 3.49e-06 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 29\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 4.0956430746863504e-09 4.0956430746863504e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 4.0956430746863504e-09 4.0956430746863504e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 34\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 34\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 31\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 29\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.216\n", - "Total CPU secs in NLP function evaluations = 0.049\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.0\n" - ] - } - ], - "source": [ - "def new_doe_object2(Ca, T0, FIM_prior):\n", - " t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]\n", - " parameter_dict = {\"A1\": 85, \"A2\": 370, \"E1\": 8, \"E2\": 15}\n", - " \n", - " measurements = MeasurementVariables()\n", - " measurements.add_variables(\"C\", indices={0: [\"CA\", \"CB\", \"CC\"], 1: t_control}, time_index_position=1)\n", - " \n", - " exp_design = DesignVariables()\n", - " exp_design.add_variables(\n", - " \"CA0\",\n", - " time_index_position=0,\n", - " values=[Ca, ],\n", - " lower_bounds=1,\n", - " indices={0: [0]},\n", - " upper_bounds=5,\n", - " )\n", - " exp_design.add_variables(\n", - " \"T\",\n", - " indices={0: t_control},\n", - " time_index_position=0,\n", - " values=list(T0),\n", - " lower_bounds=300,\n", - " upper_bounds=700,\n", - " )\n", - "\n", - " # exp_design.update_values({\"CA0[0]\": Ca, \"T[0]\": T0[0], \"T[0.125]\": T0[1], \"T[0.25]\": T0[2], \"T[0.375]\": T0[3], \"T[0.5]\": T0[4], \"T[0.625]\": T0[5],\n", - " # \"T[0.75]\": T0[6], \"T[0.875]\": T0[7], \"T[1]\": T0[8]})\n", - " \n", - " doe_object = DesignOfExperiments(\n", - " parameter_dict,\n", - " exp_design, \n", - " measurements, \n", - " create_model,\n", - " # prior_FIM=FIM_prior,\n", - " discretize_model=disc_for_measure,\n", - " )\n", - "\n", - " result = doe_object.compute_FIM(\n", - " mode=\"sequential_finite\",\n", - " scale_nominal_param_value=True,\n", - " formula=\"central\",\n", - " )\n", - "\n", - " result.result_analysis()\n", - " \n", - " return result\n", - "\n", - "n_para = len(parameter_dict)\n", - "\n", - "\n", - "t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]\n", - "parameter_dict = {\"A1\": 85, \"A2\": 370, \"E1\": 8, \"E2\": 15}\n", - " \n", - "measurements = MeasurementVariables()\n", - "measurements.add_variables(\"C\", indices={0: [\"CA\", \"CB\", \"CC\"], 1: t_control}, time_index_position=1)\n", - " \n", - "exp_design = DesignVariables()\n", - "exp_design.add_variables(\n", - " \"CA0\",\n", - " indices={0: [0]},\n", - " time_index_position=0,\n", - " lower_bounds=1,\n", - " upper_bounds=5,\n", - " )\n", - "exp_design.add_variables(\n", - " \"T\",\n", - " indices={0: t_control},\n", - " time_index_position=0,\n", - " lower_bounds=300,\n", - " upper_bounds=700,\n", - " )\n", - "\n", - "exp_design.update_values({\"CA0[0]\": 5, \"T[0]\": 450, \"T[0.125]\": 300, \"T[0.25]\": 300, \"T[0.375]\": 300, \"T[0.5]\": 300, \"T[0.625]\": 300,\n", - " \"T[0.75]\": 300, \"T[0.875]\": 300, \"T[1]\": 300})\n", - " \n", - "doe_object = DesignOfExperiments(\n", - " parameter_dict,\n", - " exp_design, \n", - " measurements, \n", - " create_model,\n", - " discretize_model=disc_for_measure,\n", - " )\n", - "\n", - "result = doe_object.compute_FIM(\n", - " mode=\"sequential_finite\",\n", - " scale_nominal_param_value=True,\n", - " formula=\"central\",\n", - " )\n", - "\n", - "result.result_analysis()\n", - "\n", - "FIM_prior = result.FIM\n", - "FIM_new = np.zeros((n_para, n_para))\n", - "A_vals_rand = []\n", - "D_vals_rand = []\n", - "exp_conds_rand = []\n", - "FIM_rand = None\n", - "\n", - "for i in range(20):\n", - " FIM_prior += FIM_new\n", - " # T_val = sample(range(300, 750, 50), 1)\n", - " T_val = np.random.rand(9)\n", - " T_val = 300 + (700 - 300) * T_val\n", - " # C_val = sample([1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0], 1)\n", - " C_val = np.random.rand(1)\n", - " C_val = 1.0 + (5.0 - 1.0) * C_val\n", - " \n", - " new_exp = new_doe_object2(C_val[0], T_val, FIM_prior)\n", - " FIM_new = new_exp.FIM\n", - " if FIM_rand is None:\n", - " FIM_rand = [FIM_new, ]\n", - " else:\n", - " FIM_rand.append(FIM_new)\n", - " A_opt = np.trace(FIM_new)\n", - " D_opt = np.linalg.det(FIM_new)\n", - " A_vals_rand.append(np.log10(A_opt))\n", - " D_vals_rand.append(np.log10(D_opt))" - ] - }, - { - "cell_type": "code", - "execution_count": 99, - "id": "883c7a88-c484-4d30-a240-0f59b7851641", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[array([[ 7.41191972, 1.91892715, -17.16569693, -5.13978557],\n", - " [ 1.91892715, 3.00288018, -3.30481097, -9.22942421],\n", - " [-17.16569693, -3.30481097, 40.83791667, 8.43170603],\n", - " [ -5.13978557, -9.22942421, 8.43170603, 29.03347955]]), array([[ 4.85545081, 0.91633779, -11.26700706, -2.3725946 ],\n", - " [ 0.91633779, 2.07545769, -1.67466172, -6.76212137],\n", - " [-11.26700706, -1.67466172, 26.48743701, 4.09255167],\n", - " [ -2.3725946 , -6.76212137, 4.09255167, 22.59510839]]), array([[ 1.63712282, 0.26253947, -3.61049017, -0.64461976],\n", - " [ 0.26253947, 0.65918137, -0.44757963, -2.18785091],\n", - " [-3.61049017, -0.44757963, 8.05695621, 1.03503989],\n", - " [-0.64461976, -2.18785091, 1.03503989, 7.4873944 ]]), array([[ 2.97488904, 0.93745651, -5.81666255, -3.06492446],\n", - " [ 0.93745651, 2.37956777, -1.78533978, -8.23677851],\n", - " [-5.81666255, -1.78533978, 11.39473346, 5.858635 ],\n", - " [-3.06492446, -8.23677851, 5.858635 , 28.72717236]]), array([[ 1.45973057, 0.98386612, -2.86168557, -3.83501583],\n", - " [ 0.98386612, 2.15443774, -1.91504893, -8.31274632],\n", - " [-2.86168557, -1.91504893, 5.61339247, 7.46485661],\n", - " [-3.83501583, -8.31274632, 7.46485661, 32.13147505]]), array([[ 0.32942076, 0.05277751, -0.72206457, -0.14075814],\n", - " [ 0.05277751, 0.16812912, -0.10170988, -0.57890579],\n", - " [-0.72206457, -0.10170988, 1.59135893, 0.26682263],\n", - " [-0.14075814, -0.57890579, 0.26682263, 2.03945911]]), array([[ 0.76173934, 0.46813848, -1.06469869, -1.24519301],\n", - " [ 0.46813848, 0.55668808, -0.65355734, -1.47375114],\n", - " [-1.06469869, -0.65355734, 1.48829363, 1.73857997],\n", - " [-1.24519301, -1.47375114, 1.73857997, 3.90772047]]), array([[ 1.58568372, 1.03312929, -2.30508585, -2.88710352],\n", - " [ 1.03312929, 1.33624454, -1.49728349, -3.71846817],\n", - " [-2.30508585, -1.49728349, 3.35175044, 4.18651955],\n", - " [-2.88710352, -3.71846817, 4.18651955, 10.36697694]]), array([[ 0.9146211 , 0.2098628 , -2.02667857, -0.78736555],\n", - " [ 0.2098628 , 0.76055504, -0.46593176, -2.99906867],\n", - " [-2.02667857, -0.46593176, 4.49422093, 1.7552855 ],\n", - " [-0.78736555, -2.99906867, 1.7552855 , 11.87833482]]), array([[ 2.26254966, 1.09103039, -4.86569687, -3.9775466 ],\n", - " [ 1.09103039, 2.48711336, -2.27302328, -9.29606811],\n", - " [-4.86569687, -2.27302328, 10.48284989, 8.27766951],\n", - " [-3.9775466 , -9.29606811, 8.27766951, 34.86643729]]), array([[ 2.37963826, 1.31101885, -3.90017864, -3.96581534],\n", - " [ 1.31101885, 2.32934987, -2.15691544, -7.08276537],\n", - " [-3.90017864, -2.15691544, 6.39343664, 6.52737831],\n", - " [-3.96581534, -7.08276537, 6.52737831, 21.54981334]]), array([[ 0.39317634, 0.27099406, -0.59222429, -0.7984167 ],\n", - " [ 0.27099406, 0.37061251, -0.40707655, -1.08492437],\n", - " [-0.59222429, -0.40707655, 0.8924423 , 1.20061859],\n", - " [-0.7984167 , -1.08492437, 1.20061859, 3.18892252]]), array([[ 0.22495 , 0.17358708, -0.34404272, -0.54730493],\n", - " [ 0.17358708, 0.2354521 , -0.2639738 , -0.73582788],\n", - " [-0.34404272, -0.2639738 , 0.52689608, 0.83381041],\n", - " [-0.54730493, -0.73582788, 0.83381041, 2.32359059]]), array([[ 1.46276597, 0.62662469, -3.9557259 , -2.91076272],\n", - " [ 0.62662469, 2.25275237, -1.67394427, -10.7119166 ],\n", - " [ -3.9557259 , -1.67394427, 10.70199128, 7.76623662],\n", - " [ -2.91076272, -10.7119166 , 7.76623662, 51.07349918]]), array([[ 6.54061161, 1.02433269, -16.03397716, -2.356947 ],\n", - " [ 1.02433269, 2.12804573, -1.76843941, -6.46991952],\n", - " [-16.03397716, -1.76843941, 40.14490486, 3.56281626],\n", - " [ -2.356947 , -6.46991952, 3.56281626, 20.19787514]]), array([[ 9.75348357, 7.80718051, -17.89422681, -30.05800918],\n", - " [ 7.80718051, 14.92544475, -13.33572462, -56.99505773],\n", - " [-17.89422681, -13.33572462, 33.23087006, 51.41850532],\n", - " [-30.05800918, -56.99505773, 51.41850532, 219.15103769]]), array([[ 1.77917606, 1.29540968, -2.57471389, -3.86435101],\n", - " [ 1.29540968, 1.60079675, -1.84799977, -4.7469565 ],\n", - " [-2.57471389, -1.84799977, 3.73604891, 5.52039717],\n", - " [-3.86435101, -4.7469565 , 5.52039717, 14.22683014]]), array([[ 1.0426306 , 0.31074057, -2.58096954, -1.12614861],\n", - " [ 0.31074057, 0.89576898, -0.74096629, -3.4507674 ],\n", - " [-2.58096954, -0.74096629, 6.42529421, 2.68122134],\n", - " [-1.12614861, -3.4507674 , 2.68122134, 13.34402829]]), array([[ 2.62318377, 1.58532964, -4.11611868, -4.65267119],\n", - " [ 1.58532964, 2.43243152, -2.4907991 , -7.1402936 ],\n", - " [-4.11611868, -2.4907991 , 6.46038325, 7.31016341],\n", - " [-4.65267119, -7.1402936 , 7.31016341, 20.97349451]]), array([[ 0.45863109, 0.06430434, -1.28081215, -0.18713209],\n", - " [ 0.06430434, 0.24416895, -0.16066722, -0.93320429],\n", - " [-1.28081215, -0.16066722, 3.61921878, 0.46801022],\n", - " [-0.18713209, -0.93320429, 0.46801022, 3.64970397]])]\n" - ] - }, - { - "data": { - "image/png": "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ypQp9O/fn8TERJo3b85Pf/pT1q5dW+tz5syZg8vlOu62Zs0aH1XdMPfee+9xNaemptb6nED8TN0yMzNr/JwmTpxY4/GB9LnOmzePn/zkJ6SlpeFyuXjvvfeqfd1xHO69917S0tKIjY3l7LPPZuXKlSd93enTp9OtWzeio6Pp1q0b7777rpfeQd3V9l5LS0v5/e9/T48ePYiPjyctLY1rr72W7OzsWl/z5ZdfrvGzLioq8vK7ObmTfbbXX3/9cXUPHDjwpK8baJ8tUONn5HK5eOihh074mv782XpL0AaRN998k9tvv50//vGPLF26lKFDh3LhhReybdu2Go/fvHkzo0aNYujQoSxdupQ//OEP3HbbbUyfPt3HldfP3LlzmThxIgsXLmTmzJmUlZUxYsQICgoKTvrctWvXsnPnzspbx44dfVDxqTnttNOq1bx8+fITHhuon6nbokWLqr3XmTNnAnDFFVfU+rxA+FwLCgro2bMnTz75ZI1f/8c//sEjjzzCk08+yaJFi0hNTeX888+v3H+qJt988w1XXXUV48aNY9myZYwbN44rr7ySb7/91ltvo05qe6+FhYV8//33/PnPf+b777/nnXfeYd26dVxyySUnfd2kpKRqn/POnTuJiYnxxluol5N9tgAjR46sVvcnn3xS62sG4mcLHPf5vPjii7hcLi6//PJaX9dfP1uvcYLUGWec4UyYMKHaY126dHHuuuuuGo//3e9+53Tp0qXaY7/85S+dgQMHeq1Gb9izZ48DOHPnzj3hMbNnz3YA58CBA74rzAPuuecep2fPnnU+Plg+U7df//rXTvv27Z2Kiooavx6onyvgvPvuu5X/rqiocFJTU50HH3yw8rGioiInOTnZefbZZ0/4OldeeaUzcuTIao9dcMEFzpgxYzxec0Md+15r8t133zmAs3Xr1hMe89JLLznJycmeLc4Lanq/1113nXPppZfW63WC5bO99NJLnXPOOafWYwLls/WkoOwRKSkpYcmSJYwYMaLa4yNGjGDBggU1Puebb7457vgLLriAxYsXU1pa6rVaPS03NxeAxo0bn/TY3r1707JlS84991xmz57t7dI8Yv369aSlpdG2bVvGjBnDpk2bTnhssHymYP6ffvXVV7nxxhtPugFkIH6uR9u8eTO7du2q9tlFR0czbNiwE37/wok/79qe449yc3NxuVw0atSo1uMOHTpEmzZtSE9P5+KLL2bp0qW+KdAD5syZQ/PmzenUqRM333wze/bsqfX4YPhsd+/ezccff8z48eNPemwgf7YNEZRBJCcnh/Lyclq0aFHt8RYtWrBr164an7Nr164ajy8rKyMnJ8drtXqS4zhMnjyZIUOG0L179xMe17JlS55//nmmT5/OO++8Q+fOnTn33HOZN2+eD6utvwEDBvCf//yHzz//nBdeeIFdu3YxePBg9u3bV+PxwfCZur333nscPHiQ66+//oTHBOrneiz392h9vn/dz6vvc/xNUVERd911F2PHjq11Q7QuXbrw8ssv88EHH/D6668TExPDmWeeyfr1631YbcNceOGFvPbaa8yaNYt//vOfLFq0iHPOOYfi4uITPicYPttXXnmFxMRERo8eXetxgfzZNpRf7757qo79y9FxnFr/mqzp+Joe91eTJk3ixx9/5Ouvv671uM6dO9O5c+fKfw8aNIisrCwefvhhzjrrLG+X2WAXXnhhZbtHjx4MGjSI9u3b88orrzB58uQanxPon6nb1KlTufDCC0lLSzvhMYH6uZ5Ifb9/G/ocf1FaWsqYMWOoqKjg6aefrvXYgQMHVhvgeeaZZ9KnTx+eeOIJHn/8cW+Xekquuuqqynb37t3p168fbdq04eOPP671l3Qgf7YAL774Itdcc81Jx3oE8mfbUEHZI9K0aVPCw8OPS8t79uw5LlW7paam1nh8REQETZo08VqtnnLrrbfywQcfMHv2bNLT0+v9/IEDBwZc4o6Pj6dHjx4nrDvQP1O3rVu38sUXX3DTTTfV+7mB+Lm6Z0LV5/vX/bz6PsdflJaWcuWVV7J582ZmzpxZ7+3hw8LC6N+/f8B91mB68tq0aVNr7YH82QJ89dVXrF27tkHfw4H82dZVUAaRqKgo+vbtWznLwG3mzJkMHjy4xucMGjTouONnzJhBv379iIyM9Fqtp8pxHCZNmsQ777zDrFmzaNu2bYNeZ+nSpbRs2dLD1XlXcXExq1evPmHdgfqZHuull16iefPmXHTRRfV+biB+rm3btiU1NbXaZ1dSUsLcuXNP+P0LJ/68a3uOP3CHkPXr1/PFF180KCQ7jsMPP/wQcJ81wL59+8jKyqq19kD9bN2mTp1K37596dmzZ72fG8ifbZ3ZGiXrbW+88YYTGRnpTJ061Vm1apVz++23O/Hx8c6WLVscx3Gcu+66yxk3blzl8Zs2bXLi4uKcO+64w1m1apUzdepUJzIy0nn77bdtvYU6+dWvfuUkJyc7c+bMcXbu3Fl5KywsrDzm2Pf66KOPOu+++66zbt06Z8WKFc5dd93lAM706dNtvIU6u/POO505c+Y4mzZtchYuXOhcfPHFTmJiYtB9pkcrLy93Wrdu7fz+978/7muB/Lnm5+c7S5cudZYuXeoAziOPPOIsXbq0cqbIgw8+6CQnJzvvvPOOs3z5cufqq692WrZs6eTl5VW+xrhx46rNgps/f74THh7uPPjgg87q1audBx980ImIiHAWLlzo8/d3tNrea2lpqXPJJZc46enpzg8//FDte7i4uLjyNY59r/fee6/z2WefORs3bnSWLl3q3HDDDU5ERITz7bff2niL1dT2fvPz850777zTWbBggbN582Zn9uzZzqBBg5xWrVoF3Wfrlpub68TFxTnPPPNMja8RSJ+ttwRtEHEcx3nqqaecNm3aOFFRUU6fPn2qTWm97rrrnGHDhlU7fs6cOU7v3r2dqKgoJzMz84T/4/gToMbbSy+9VHnMse/173//u9O+fXsnJibGSUlJcYYMGeJ8/PHHvi++nq666iqnZcuWTmRkpJOWluaMHj3aWblyZeXXg+UzPdrnn3/uAM7atWuP+1ogf67uqcbH3q677jrHccwU3nvuucdJTU11oqOjnbPOOstZvnx5tdcYNmxY5fFub731ltO5c2cnMjLS6dKli1+EsNre6+bNm0/4PTx79uzK1zj2vd5+++1O69atnaioKKdZs2bOiBEjnAULFvj+zdWgtvdbWFjojBgxwmnWrJkTGRnptG7d2rnuuuucbdu2VXuNYPhs3Z577jknNjbWOXjwYI2vEUifrbe4HOfI6D0RERERHwvKMSIiIiISGBRERERExBoFEREREbFGQURERESsURARERERaxRERERExBoFEREREbFGQURERESsURARERERaxRERERExBoFEREREbFGQURERESs+X8Eavm372QE9AAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "D_vals_rand_from_ind_FIM = []\n", - "running_FIM = np.zeros((n_para, n_para))\n", - "print(FIM_rand)\n", - "for i in range(20):\n", - " running_FIM += FIM_rand[i]\n", - " D_vals_rand_from_ind_FIM.append(np.log10(np.linalg.det(running_FIM)))\n", - "\n", - "plt.plot(range(20), D_vals_rand_from_ind_FIM, color='green')\n", - "plt.plot(range(20), D_vals_rand, color='orange')\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d0bf5a03-4f8a-4c7b-9ee3-57577550689e", - "metadata": {}, - "outputs": [], - "source": [ - "# mobel based design" - ] - }, - { - "cell_type": "code", - "execution_count": 106, - "id": "be09a59b-5783-4cdd-a023-380be8f2935a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.47e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 2.64e+01 3.85e+02 -1.0 1.47e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.71e+00 5.99e+01 -1.0 3.11e+01 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 3.98e-02 2.06e+00 -1.0 3.19e+00 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 7.87e-07 2.39e-04 -1.0 2.80e-02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (287125)\n", - " 5 0.0000000e+00 2.27e-13 1.50e-09 -3.8 5.42e-07 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.3691704763652764e-13 2.2737367544323206e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.3691704763652764e-13 2.2737367544323206e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.078\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.0\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 8.41e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 5.97e+02 3.85e+02 -1.0 8.40e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 9.58e+01 5.99e+01 -1.0 8.73e+02 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.25e+00 2.06e+00 -1.0 7.86e+01 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.23e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (394005)\n", - " 5 0.0000000e+00 2.52e-13 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.3691704763652764e-13 2.5224267119483557e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.3691704763652764e-13 2.5224267119483557e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.097\n", - "Total CPU secs in NLP function evaluations = 0.015\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -7.7248744e+00 8.42e+02 3.03e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303416)\n", - " 1 -9.5185054e+00 3.55e+02 1.97e+00 -1.0 2.49e+01 - 6.64e-01 5.10e-01h 1\n", - " 2 -1.0459243e+01 2.21e+01 1.06e+00 -1.0 1.03e+01 - 9.89e-01 1.00e+00f 1\n", - " 3 -1.0005982e+01 2.37e+00 1.82e+01 -1.0 6.69e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -9.4350217e+00 6.84e+00 2.90e+01 -1.0 1.66e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -9.9270718e+00 3.91e+00 4.23e+00 -1.0 7.85e+01 - 1.00e+00 1.00e+00h 1\n", - " 6 -9.8988295e+00 8.63e-02 7.95e-01 -1.0 4.47e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -9.9256371e+00 1.12e-02 8.03e-02 -1.7 1.69e+00 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.0115145e+01 6.09e-01 3.86e+00 -2.5 1.24e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.0716463e+01 1.17e+01 7.10e+01 -2.5 4.55e+02 - 2.86e-01 2.24e-01h 2\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.1139383e+01 3.90e+00 1.50e+01 -2.5 3.96e+01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (326891)\n", - " 11 -1.1160539e+01 7.96e-02 1.12e-01 -2.5 2.01e+01 - 1.00e+00 1.00e+00h 1\n", - " 12 -1.1161798e+01 1.04e-03 2.28e-03 -2.5 3.39e+00 - 1.00e+00 1.00e+00h 1\n", - " 13 -1.1804938e+01 5.61e+00 3.57e+00 -3.8 7.07e+01 - 7.08e-01 1.00e+00f 1\n", - " 14 -1.2138004e+01 2.90e+00 2.94e+00 -3.8 6.85e+01 - 9.69e-01 1.00e+00h 1\n", - " 15 -1.2260171e+01 8.52e-01 5.77e-01 -3.8 3.63e+01 - 1.00e+00 1.00e+00h 1\n", - " 16 -1.2249771e+01 6.32e-03 2.20e-02 -3.8 9.24e+00 - 1.00e+00 1.00e+00h 1\n", - " 17 -1.2249799e+01 6.45e-06 1.24e-05 -3.8 3.70e-01 - 1.00e+00 1.00e+00h 1\n", - " 18 -1.2341110e+01 2.89e-01 1.08e-01 -5.7 2.25e+01 - 9.19e-01 9.86e-01f 1\n", - " 19 -1.2347950e+01 6.26e-03 1.40e-03 -5.7 2.74e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.2348059e+01 2.15e-06 1.44e-06 -5.7 4.38e-02 - 1.00e+00 1.00e+00h 1\n", - " 21 -1.2349341e+01 7.29e-05 2.04e-05 -8.6 3.46e-01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (355282)\n", - " 22 -1.2349342e+01 2.92e-10 5.48e-08 -8.6 5.48e-04 -4.0 1.00e+00 1.00e+00h 1\n", - " 23 -1.2349342e+01 4.55e-13 2.15e-11 -8.6 6.44e-07 -4.5 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 23\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.2349342292918536e+01 -1.2349342292918536e+01\n", - "Dual infeasibility......: 2.1473493827409733e-11 2.1473493827409733e-11\n", - "Constraint violation....: 1.7053025658242404e-13 4.5474735088646412e-13\n", - "Complementarity.........: 2.5059035849180921e-09 2.5059035849180921e-09\n", - "Overall NLP error.......: 2.5059035849180921e-09 2.5059035849180921e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 27\n", - "Number of objective gradient evaluations = 24\n", - "Number of equality constraint evaluations = 27\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 24\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 23\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.487\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.7\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.94e+01 3.85e+02 -1.0 3.97e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.83e+01 4.31e+01 -1.0 4.78e+01 - 4.76e-01 9.84e-01h 1\n", - " 3 0.0000000e+00 2.63e-01 4.48e+02 -1.0 2.89e+01 - 4.02e-02 9.90e-01h 1\n", - " 4 0.0000000e+00 5.47e-04 1.17e+02 -1.0 1.35e-01 - 9.90e-01 9.98e-01h 1\n", - "Reallocating memory for MA57: lfact (284905)\n", - " 5 0.0000000e+00 6.26e-11 1.00e-06 -1.0 2.79e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 6.2641447584610432e-11 6.2641447584610432e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 6.2641447584610432e-11 6.2641447584610432e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.078\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.1\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 2.19e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 6.10e+02 3.85e+02 -1.0 2.19e+03 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 9.60e+01 5.99e+01 -1.0 8.86e+02 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.25e+00 2.06e+00 -1.0 7.87e+01 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.23e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (394385)\n", - " 5 0.0000000e+00 2.27e-12 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 4.5474735088646412e-13 2.2737367544323206e-12\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 4.5474735088646412e-13 2.2737367544323206e-12\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.113\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.3841270e+01 8.42e+02 1.61e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303016)\n", - " 1 -1.4404335e+01 3.97e+02 3.77e+00 -1.0 2.54e+01 - 8.41e-01 5.01e-01h 1\n", - " 2 -1.4812683e+01 1.50e+01 5.74e-01 -1.0 7.17e+00 - 9.88e-01 1.00e+00f 1\n", - " 3 -1.4522399e+01 4.44e+00 1.61e+01 -1.0 8.94e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -1.4388879e+01 1.20e+01 1.37e+01 -1.0 1.49e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.4491792e+01 1.25e+00 1.12e+00 -1.0 3.05e+01 - 1.00e+00 1.00e+00h 1\n", - " 6 -1.4495171e+01 5.96e-04 9.48e-03 -1.0 1.09e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -1.4495521e+01 8.41e-05 5.34e-03 -2.5 3.10e-01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.4516522e+01 7.21e-01 1.81e+00 -3.8 1.75e+01 - 7.92e-01 1.00e+00h 1\n", - " 9 -1.4530830e+01 4.10e-03 1.85e-02 -3.8 1.06e+00 -4.0 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.4533751e+01 1.58e-03 4.43e-03 -3.8 1.26e+00 -4.5 1.00e+00 1.00e+00h 1\n", - " 11 -1.4540008e+01 1.21e-02 4.52e-02 -3.8 2.29e+00 -5.0 1.00e+00 1.00e+00h 1\n", - " 12 -1.4566257e+01 2.27e-01 7.87e-01 -3.8 9.29e+00 -5.4 1.00e+00 1.00e+00h 1\n", - " 13 -1.4585780e+01 3.91e-02 1.61e-01 -3.8 3.90e+00 -5.0 1.00e+00 1.00e+00h 1\n", - " 14 -1.4657627e+01 8.05e-01 2.88e+00 -3.8 1.70e+01 -5.5 1.00e+00 1.00e+00h 1\n", - " 15 -1.5563505e+01 8.18e+01 2.82e+02 -3.8 4.19e+02 -6.0 2.05e-01 5.14e-01h 1\n", - " 16 -1.5523849e+01 2.68e+01 2.80e+01 -3.8 6.91e+01 -5.5 1.00e+00 1.00e+00h 1\n", - " 17 -1.5802664e+01 2.51e+01 5.48e+00 -3.8 1.72e+02 - 3.49e-01 4.82e-01h 1\n", - " 18 -1.5795220e+01 1.64e+01 1.24e+01 -3.8 1.44e+02 - 1.00e+00 1.00e+00f 1\n", - " 19 -1.5802973e+01 1.78e+00 6.66e-01 -3.8 5.80e+01 - 9.66e-01 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.5792518e+01 6.29e-02 1.87e-02 -3.8 2.10e+01 - 1.00e+00 1.00e+00h 1\n", - " 21 -1.5792562e+01 6.62e-05 7.16e-05 -3.8 6.47e-01 - 1.00e+00 1.00e+00h 1\n", - " 22 -1.5863869e+01 1.39e+00 8.36e-01 -5.7 8.31e+01 - 7.11e-01 1.00e+00f 1\n", - " 23 -1.5877663e+01 1.08e-01 5.90e-02 -5.7 2.81e+01 - 9.75e-01 1.00e+00h 1\n", - " 24 -1.5878910e+01 9.88e-03 7.73e-04 -5.7 8.80e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (330048)\n", - " 25 -1.5878920e+01 2.19e-04 2.60e-06 -5.7 1.33e+00 - 1.00e+00 1.00e+00h 1\n", - " 26 -1.5878920e+01 4.59e-08 3.36e-10 -5.7 1.93e-02 - 1.00e+00 1.00e+00h 1\n", - " 27 -1.5880196e+01 1.15e-03 2.24e-04 -8.6 3.06e+00 - 9.93e-01 1.00e+00f 1\n", - "Reallocating memory for MA57: lfact (362150)\n", - " 28 -1.5880200e+01 1.55e-06 3.86e-08 -8.6 1.13e-01 - 1.00e+00 1.00e+00h 1\n", - " 29 -1.5880200e+01 4.40e-12 1.85e-10 -8.6 1.90e-04 -6.0 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 29\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.5880200473415972e+01 -1.5880200473415972e+01\n", - "Dual infeasibility......: 1.8546108990308775e-10 1.8546108990308775e-10\n", - "Constraint violation....: 4.3998138465894954e-12 4.3998138465894954e-12\n", - "Complementarity.........: 2.5059390926675284e-09 2.5059390926675284e-09\n", - "Overall NLP error.......: 2.5059390926675284e-09 2.5059390926675284e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 30\n", - "Number of objective gradient evaluations = 30\n", - "Number of equality constraint evaluations = 30\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 30\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 29\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.620\n", - "Total CPU secs in NLP function evaluations = 0.028\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.4\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.19e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 5.67e+01 3.85e+02 -1.0 3.19e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 2.60e+01 4.39e+02 -1.0 7.08e+01 - 5.65e-02 9.90e-01h 1\n", - " 3 0.0000000e+00 1.79e-01 1.22e+02 -1.0 1.49e+01 - 5.20e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 7.30e-06 2.37e+02 -1.0 1.13e-01 - 9.91e-01 1.00e+00h 1\n", - " 5 0.0000000e+00 3.41e-13 1.00e-06 -1.0 4.00e-06 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.7701825750354685e-13 3.4106051316484809e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.7701825750354685e-13 3.4106051316484809e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.065\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.9\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 4.70e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 6.35e+02 3.85e+02 -1.0 4.70e+03 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 9.62e+01 5.99e+01 -1.0 9.11e+02 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.26e+00 2.06e+00 -1.0 7.87e+01 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.24e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393774)\n", - " 5 0.0000000e+00 4.55e-13 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.3691704763652764e-13 4.5474735088646412e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.3691704763652764e-13 4.5474735088646412e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.119\n", - "Total CPU secs in NLP function evaluations = 0.002\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.7948727e+01 8.42e+02 1.33e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303016)\n", - " 1 -1.8165832e+01 4.09e+02 4.51e+00 -1.0 2.53e+01 - 9.19e-01 5.02e-01h 1\n", - " 2 -1.8347575e+01 8.18e+00 2.41e-01 -1.0 3.98e+00 - 9.87e-01 1.00e+00f 1\n", - " 3 -1.8279037e+01 6.26e+00 5.74e+00 -1.0 1.03e+02 - 1.00e+00 1.00e+00f 1\n", - " 4 -1.8238859e+01 1.69e+01 3.91e+00 -1.0 1.64e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.8269310e+01 2.31e-01 2.75e-01 -1.0 2.89e+00 - 1.00e+00 1.00e+00h 1\n", - " 6 -1.8270555e+01 5.19e-04 4.57e-04 -1.7 6.60e-01 - 1.00e+00 1.00e+00h 1\n", - " 7 -1.8270805e+01 7.88e-04 6.63e-04 -3.8 5.00e-01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.8271500e+01 9.45e-04 5.28e-04 -5.7 1.71e+00 -4.0 1.00e+00 1.00e+00h 1\n", - " 9 -1.8273229e+01 5.92e-03 1.56e-04 -5.7 4.65e+00 -4.5 1.00e+00 9.14e-01h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.8273631e+01 2.13e-03 1.40e-03 -5.7 3.86e-01 -5.0 1.00e+00 1.00e+00h 1\n", - " 11 -1.8275013e+01 2.11e-02 2.82e-03 -5.7 1.28e+00 -5.4 1.00e+00 1.00e+00h 1\n", - " 12 -1.8275591e+01 3.07e-03 4.00e-04 -5.7 5.07e-01 -5.0 1.00e+00 1.00e+00h 1\n", - " 13 -1.8277354e+01 3.13e-02 4.47e-03 -5.7 1.63e+00 -5.5 1.00e+00 1.00e+00h 1\n", - " 14 -1.8284188e+01 4.35e-01 7.97e-02 -5.7 6.56e+00 -6.0 1.00e+00 1.00e+00h 1\n", - " 15 -1.8339799e+01 2.22e+01 6.67e+00 -5.7 9.02e+01 -6.4 9.31e-01 6.26e-01h 1\n", - "Reallocating memory for MA57: lfact (332105)\n", - " 16 -1.8410872e+01 1.03e+01 2.06e+00 -5.7 1.03e+02 -6.0 1.00e+00 7.09e-01f 1\n", - " 17 -1.8453249e+01 9.48e+00 2.01e+00 -5.7 1.70e+02 -6.5 1.00e+00 1.89e-01f 1\n", - " 18 -1.8469379e+01 8.31e+00 1.68e+00 -5.7 8.57e+01 -7.0 4.45e-01 1.63e-01h 1\n", - " 19 -1.8493566e+01 3.77e+00 6.76e-01 -5.7 3.26e+01 -6.5 1.00e+00 5.98e-01h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.8501400e+01 3.15e+00 5.56e-01 -5.7 5.39e+01 -7.0 1.00e+00 1.78e-01h 1\n", - " 21 -1.8519701e+01 2.67e-01 7.89e-02 -5.7 2.04e+01 -6.6 1.00e+00 1.00e+00f 1\n", - " 22 -1.8524363e+01 3.95e-01 2.25e-01 -5.7 6.41e+01 -7.1 1.00e+00 2.07e-01h 1\n", - " 23 -1.8531234e+01 2.84e-01 2.21e-01 -5.7 1.59e+01 -6.6 1.00e+00 1.00e+00f 1\n", - " 24 -1.8538783e+01 6.75e-01 4.74e-01 -5.7 5.12e+01 -7.1 1.00e+00 3.67e-01h 1\n", - " 25 -1.8551386e+01 4.55e+00 1.63e+00 -5.7 8.00e+01 -7.6 1.00e+00 1.00e+00f 1\n", - " 26 -1.8545090e+01 1.46e-01 1.88e-01 -5.7 2.34e+01 -7.2 1.00e+00 1.00e+00h 1\n", - " 27 -1.8546477e+01 1.85e-01 1.29e-01 -5.7 9.39e+01 -7.6 1.00e+00 5.95e-01h 1\n", - " 28 -1.8546576e+01 2.34e-02 6.92e-03 -5.7 2.28e+01 - 1.00e+00 1.00e+00f 1\n", - " 29 -1.8546954e+01 2.55e-02 9.41e-03 -5.7 2.37e+01 - 1.00e+00 9.61e-01h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -1.8546953e+01 3.64e-05 1.38e-05 -5.7 9.21e-01 - 1.00e+00 1.00e+00h 1\n", - " 31 -1.8546953e+01 3.38e-09 1.41e-09 -5.7 8.86e-03 - 1.00e+00 1.00e+00h 1\n", - " 32 -1.8548230e+01 1.41e-03 1.43e-04 -8.6 1.62e+00 - 9.94e-01 9.96e-01f 1\n", - " 33 -1.8548236e+01 9.75e-08 1.46e-08 -8.6 9.43e-03 - 1.00e+00 1.00e+00h 1\n", - " 34 -1.8548236e+01 9.09e-13 1.54e-13 -8.6 8.26e-07 -8.1 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 34\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.8548235998334103e+01 -1.8548235998334103e+01\n", - "Dual infeasibility......: 1.5371362792196526e-13 1.5371362792196526e-13\n", - "Constraint violation....: 9.0949470177292824e-13 9.0949470177292824e-13\n", - "Complementarity.........: 2.5059035679086560e-09 2.5059035679086560e-09\n", - "Overall NLP error.......: 2.5059035679086560e-09 2.5059035679086560e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 35\n", - "Number of objective gradient evaluations = 35\n", - "Number of equality constraint evaluations = 35\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 35\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 34\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.987\n", - "Total CPU secs in NLP function evaluations = 0.017\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.9\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.94e+01 3.85e+02 -1.0 3.97e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.54e+01 4.20e+01 -1.0 4.92e+01 - 4.94e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 3.23e-01 3.50e+02 -1.0 2.84e+01 - 2.68e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 9.11e-06 2.34e+02 -1.0 1.90e-01 - 9.91e-01 1.00e+00h 1\n", - " 5 0.0000000e+00 3.13e-13 1.00e-06 -1.0 6.34e-06 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.2971319830862150e-13 3.1263880373444408e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.2971319830862150e-13 3.1263880373444408e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.056\n", - "Total CPU secs in NLP function evaluations = 0.001\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.0\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 9.91e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 6.88e+02 3.85e+02 -1.0 9.91e+03 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 9.67e+01 5.99e+01 -1.0 9.63e+02 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.26e+00 2.06e+00 -1.0 7.88e+01 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.24e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393643)\n", - " 5 0.0000000e+00 1.82e-12 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.8189894035458565e-12 1.8189894035458565e-12\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.8189894035458565e-12 1.8189894035458565e-12\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.117\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -2.1041036e+01 8.42e+02 1.22e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303234)\n", - " 1 -2.1145524e+01 4.14e+02 4.86e+00 -1.0 2.53e+01 - 9.57e-01 5.02e-01h 1\n", - " 2 -2.1235982e+01 4.29e+00 1.97e-01 -1.0 3.69e+00 - 9.85e-01 1.00e+00f 1\n", - " 3 -2.1196637e+01 7.15e+00 3.48e+00 -1.0 1.06e+02 - 1.00e+00 1.00e+00f 1\n", - " 4 -2.1182737e+01 1.81e+01 1.49e+00 -1.0 1.80e+02 - 1.00e+00 1.00e+00h 1\n", - " 5 -2.1196659e+01 9.63e-02 8.45e-02 -1.0 1.10e+01 - 1.00e+00 1.00e+00h 1\n", - " 6 -2.1196935e+01 1.22e-04 1.64e-03 -2.5 3.09e-01 - 1.00e+00 1.00e+00h 1\n", - " 7 -2.1197254e+01 5.64e-03 3.71e-04 -3.8 1.98e+00 - 1.00e+00 1.00e+00h 1\n", - " 8 -2.1197442e+01 2.71e-04 1.49e-04 -3.8 9.31e-01 -4.0 1.00e+00 1.00e+00h 1\n", - " 9 -2.1197814e+01 1.12e-03 1.02e-04 -5.7 1.87e+00 -4.5 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -2.1198435e+01 3.31e-03 1.47e-04 -5.7 3.66e+00 -5.0 1.00e+00 8.46e-01h 1\n", - " 11 -2.1198609e+01 2.27e-03 4.45e-04 -5.7 5.23e-01 -5.4 1.00e+00 1.00e+00f 1\n", - " 12 -2.1199224e+01 2.27e-02 1.60e-03 -5.7 1.87e+00 -5.9 1.00e+00 1.00e+00h 1\n", - " 13 -2.1201389e+01 2.97e-01 1.99e-02 -5.7 4.95e+00 -6.4 1.00e+00 1.00e+00h 1\n", - " 14 -2.1215482e+01 2.18e+01 3.79e+00 -5.7 1.77e+02 -6.9 4.71e-01 3.13e-01h 1\n", - " 15r-2.1215482e+01 2.18e+01 9.99e+02 1.3 0.00e+00 -7.3 0.00e+00 2.37e-07R 5\n", - " 16r-2.1215529e+01 2.18e+01 9.65e+03 1.3 7.99e+05 - 1.23e-05 4.88e-07f 1\n", - " 17r-2.1332007e+01 5.50e+00 9.67e+03 1.3 2.14e+04 - 7.52e-05 1.01e-03f 1\n", - " 18 -2.1341179e+01 6.06e+00 2.11e-01 -5.7 1.51e+03 - 9.35e-02 1.40e-02h 1\n", - " 19 -2.1348895e+01 6.45e+00 1.98e-01 -5.7 2.96e+03 - 8.78e-02 6.18e-02h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -2.1348057e+01 6.10e+00 1.44e+00 -5.7 3.35e+02 -7.8 1.00e+00 8.82e-02h 1\n", - " 21 -2.1337687e+01 5.02e+00 8.30e-01 -5.7 2.16e+02 -8.3 1.00e+00 3.48e-01h 1\n", - " 22 -2.1256596e+01 2.16e+01 7.80e+03 -5.7 6.01e+02 -7.9 7.05e-05 3.63e-01h 1\n", - " 23 -2.1229440e+01 6.42e+00 1.69e+03 -5.7 5.09e+01 -3.8 3.31e-01 7.83e-01h 1\n", - " 24 -2.1225416e+01 7.91e-02 1.07e+00 -5.7 4.92e+00 -4.3 1.85e-04 1.00e+00h 1\n", - " 25 -2.1234904e+01 2.01e-01 1.93e-02 -5.7 1.80e+01 -4.8 1.00e+00 1.00e+00h 1\n", - " 26 -2.1254412e+01 9.46e-01 1.46e-02 -5.7 5.59e+01 -5.3 1.00e+00 6.93e-01h 1\n", - " 27 -2.1288915e+01 3.38e+00 4.31e-02 -5.7 1.68e+02 -5.7 1.00e+00 4.35e-01f 1\n", - " 28 -2.1310182e+01 3.91e+00 4.77e-02 -5.7 3.62e+02 -6.2 1.00e+00 1.26e-01f 1\n", - " 29 -2.1313014e+01 6.05e-02 3.51e-02 -5.7 7.03e+00 -6.7 8.65e-02 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (332877)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -2.1314539e+01 1.81e-01 7.17e-02 -5.7 2.17e+01 -7.2 1.00e+00 4.80e-01h 1\n", - " 31 -2.1322536e+01 5.42e+00 6.40e+01 -5.7 1.14e+03 - 4.87e-05 5.01e-02h 2\n", - " 32 -2.1318925e+01 6.40e-02 1.01e+00 -5.7 1.52e+00 - 6.09e-02 1.00e+00h 1\n", - " 33 -2.1319551e+01 1.97e-03 3.61e-04 -5.7 1.05e+00 - 1.00e+00 1.00e+00h 1\n", - " 34 -2.1319541e+01 3.42e-06 6.83e-07 -5.7 2.81e-01 - 1.00e+00 1.00e+00h 1\n", - " 35 -2.1320810e+01 5.55e-03 3.24e-04 -8.6 3.28e+00 - 9.87e-01 9.90e-01h 1\n", - " 36 -2.1320824e+01 1.71e-06 1.42e-07 -8.6 4.12e-02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (355912)\n", - "Reallocating memory for MA57: lfact (403977)\n", - " 37 -2.1320824e+01 4.55e-13 1.22e-13 -8.6 1.29e-05 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 37\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -2.1320823847268578e+01 -2.1320823847268578e+01\n", - "Dual infeasibility......: 1.2172728103276853e-13 1.2172728103276853e-13\n", - "Constraint violation....: 4.5474735088646412e-13 4.5474735088646412e-13\n", - "Complementarity.........: 2.5059035644843673e-09 2.5059035644843673e-09\n", - "Overall NLP error.......: 2.5059035644843673e-09 2.5059035644843673e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 45\n", - "Number of objective gradient evaluations = 37\n", - "Number of equality constraint evaluations = 45\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 39\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 37\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.000\n", - "Total CPU secs in NLP function evaluations = 0.051\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.1\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.94e+01 3.85e+02 -1.0 3.97e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.54e+01 4.20e+01 -1.0 4.92e+01 - 4.94e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 3.11e-01 3.50e+02 -1.0 2.84e+01 - 2.68e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 8.56e-06 2.34e+02 -1.0 1.82e-01 - 9.91e-01 1.00e+00h 1\n", - " 5 0.0000000e+00 2.97e-13 1.00e-06 -1.0 5.95e-06 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.2308013703147609e-13 2.9665159217984183e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.2308013703147609e-13 2.9665159217984183e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.069\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.1\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 2.03e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 7.92e+02 3.85e+02 -1.0 2.03e+04 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 9.78e+01 5.99e+01 -1.0 1.07e+03 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.27e+00 2.06e+00 -1.0 7.91e+01 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.25e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393908)\n", - " 5 0.0000000e+00 4.55e-13 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.2737367544323206e-13 4.5474735088646412e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.2737367544323206e-13 4.5474735088646412e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.118\n", - "Total CPU secs in NLP function evaluations = 0.001\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -2.3957786e+01 8.42e+02 1.15e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303016)\n", - " 1 -2.4009165e+01 4.16e+02 5.03e+00 -1.0 2.52e+01 - 9.74e-01 5.03e-01h 1\n", - " 2 -2.4053676e+01 2.23e+00 1.57e-01 -1.0 6.82e+00 - 9.83e-01 1.00e+00f 1\n", - " 3 -2.4049598e+01 2.63e-01 1.04e-01 -1.7 2.03e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -2.4031404e+01 5.51e+00 8.43e-01 -1.7 1.66e+02 - 1.00e+00 9.62e-01f 1\n", - " 5 -2.4031794e+01 9.63e+00 1.11e-01 -1.7 1.31e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -2.4034753e+01 9.12e-03 4.93e-03 -1.7 9.57e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -2.4034764e+01 5.23e-06 2.40e-04 -3.8 5.16e-02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (320596)\n", - " 8 -2.4036325e+01 4.61e-01 4.50e-02 -5.7 2.11e+01 - 8.48e-01 1.00e+00h 1\n", - " 9 -2.4036710e+01 3.52e-04 5.33e-04 -5.7 3.85e-01 -4.0 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -2.4036850e+01 6.00e-04 7.85e-05 -5.7 1.39e+00 -4.5 1.00e+00 1.00e+00h 1\n", - " 11 -2.4036956e+01 2.95e-04 4.05e-04 -5.7 1.52e+00 -5.0 1.00e+00 6.87e-01H 1\n", - " 12 -2.4036981e+01 2.11e-04 1.51e-04 -5.7 1.55e-01 -5.4 1.00e+00 1.00e+00f 1\n", - " 13 -2.4037122e+01 2.08e-03 9.42e-05 -5.7 8.97e-01 -5.9 1.00e+00 1.00e+00h 1\n", - " 14 -2.4037545e+01 1.84e-02 9.37e-04 -5.7 2.52e+00 -6.4 1.00e+00 1.00e+00h 1\n", - " 15 -2.4038911e+01 1.53e-01 1.30e-02 -5.7 7.03e+00 -6.9 1.00e+00 1.00e+00h 1\n", - " 16 -2.4039513e+01 1.71e-02 2.17e-03 -5.7 2.31e+00 -6.4 1.00e+00 1.00e+00h 1\n", - " 17 -2.4041456e+01 2.25e-01 5.40e-02 -5.7 1.12e+01 -6.9 1.00e+00 1.00e+00h 1\n", - " 18 -2.4042770e+01 5.07e-02 2.20e-02 -5.7 5.24e+00 -6.5 1.00e+00 1.00e+00h 1\n", - " 19 -2.4050544e+01 4.39e+00 1.42e+00 -5.7 4.13e+01 -7.0 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -2.4066468e+01 2.01e+00 5.80e-01 -5.7 6.98e+01 -6.5 1.00e+00 1.00e+00h 1\n", - " 21 -2.4093450e+01 6.18e+00 2.10e+00 -5.7 3.11e+02 -7.0 6.69e-01 3.36e-01h 1\n", - " 22 -2.4106017e+01 6.61e+00 2.16e+00 -5.7 7.31e+02 -7.5 1.00e+00 5.80e-02h 1\n", - " 23 -2.4143250e+01 1.35e+01 2.72e+00 -5.7 4.43e+02 -8.0 6.35e-01 3.17e-01h 1\n", - " 24 -2.4148932e+01 1.23e+01 2.04e+00 -5.7 2.31e+02 -8.4 7.03e-01 2.32e-01h 1\n", - " 25 -2.4151239e+01 1.14e+01 1.84e+00 -5.7 1.07e+03 -8.9 2.31e-01 9.52e-02h 1\n", - " 26 -2.4155618e+01 9.19e+00 6.95e-01 -5.7 1.24e+02 - 1.00e+00 7.89e-01f 1\n", - " 27 -2.4153220e+01 4.92e-01 8.71e-02 -5.7 2.88e+01 - 1.00e+00 1.00e+00h 1\n", - " 28 -2.4153102e+01 1.90e-02 1.51e-04 -5.7 1.19e+01 - 1.00e+00 1.00e+00h 1\n", - " 29 -2.4153101e+01 3.02e-05 6.13e-08 -5.7 4.80e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -2.4153101e+01 6.65e-10 1.93e-11 -5.7 2.26e-03 - 1.00e+00 1.00e+00h 1\n", - " 31 -2.4154362e+01 2.69e-02 4.53e-03 -8.6 1.45e+01 - 9.56e-01 1.00e+00f 1\n", - "Reallocating memory for MA57: lfact (345644)\n", - " 32 -2.4154374e+01 4.15e-04 2.66e-06 -8.6 1.84e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (384901)\n", - " 33 -2.4154374e+01 4.16e-07 6.54e-10 -8.6 5.84e-02 - 1.00e+00 1.00e+00h 1\n", - " 34 -2.4154374e+01 9.09e-13 1.27e-13 -8.6 3.70e-05 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 34\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -2.4154373723270826e+01 -2.4154373723270826e+01\n", - "Dual infeasibility......: 1.2724980870831129e-13 1.2724980870831129e-13\n", - "Constraint violation....: 6.9129384604076763e-13 9.0949470177292824e-13\n", - "Complementarity.........: 2.5059035618469615e-09 2.5059035618469615e-09\n", - "Overall NLP error.......: 2.5059035618469615e-09 2.5059035618469615e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 36\n", - "Number of objective gradient evaluations = 35\n", - "Number of equality constraint evaluations = 36\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 35\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 34\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.007\n", - "Total CPU secs in NLP function evaluations = 0.010\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.9\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.24e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 5.76e+01 3.85e+02 -1.0 3.24e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 2.68e+01 4.57e+02 -1.0 7.19e+01 - 5.57e-02 9.90e-01h 1\n", - " 3 0.0000000e+00 1.69e-01 1.26e+02 -1.0 1.53e+01 - 4.90e-01 9.91e-01f 1\n", - " 4 0.0000000e+00 6.46e-06 2.38e+02 -1.0 1.08e-01 - 9.91e-01 1.00e+00h 1\n", - " 5 0.0000000e+00 4.55e-13 1.00e-06 -1.0 3.54e-06 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.9311082636750565e-13 4.5474735088646412e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.9311082636750565e-13 4.5474735088646412e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.071\n", - "Total CPU secs in NLP function evaluations = 0.001\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.0\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 4.10e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 9.98e+02 3.85e+02 -1.0 4.10e+04 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 9.98e+01 5.99e+01 -1.0 1.27e+03 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.29e+00 2.06e+00 -1.0 7.95e+01 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.27e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393985)\n", - " 5 0.0000000e+00 7.28e-12 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 7.2759576141834259e-12 7.2759576141834259e-12\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 7.2759576141834259e-12 7.2759576141834259e-12\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.117\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -2.6831720e+01 8.42e+02 1.11e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303196)\n", - " 1 -2.6856995e+01 4.17e+02 5.11e+00 -1.0 2.52e+01 - 9.83e-01 5.03e-01h 1\n", - " 2 -2.6878754e+01 1.15e+00 1.70e-01 -1.0 1.35e+01 - 9.82e-01 1.00e+00f 1\n", - " 3 -2.6876993e+01 2.41e-01 8.47e-02 -1.7 2.32e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -2.6868640e+01 5.70e+00 3.70e-01 -1.7 1.61e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -2.6869023e+01 8.10e+00 4.89e-02 -1.7 1.19e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -2.6870266e+01 5.51e-03 2.48e-03 -1.7 8.49e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -2.6870267e+01 1.82e-06 9.94e-05 -3.8 3.28e-02 - 1.00e+00 1.00e+00h 1\n", - " 8 -2.6870637e+01 1.33e-01 5.56e-03 -5.7 1.03e+01 - 9.45e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (320847)\n", - " 9 -2.6870673e+01 2.67e-05 3.18e-05 -5.7 3.18e-01 -4.0 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -2.6870728e+01 3.86e-04 3.70e-05 -5.7 1.11e+00 -4.5 1.00e+00 1.00e+00h 1\n", - " 11 -2.6870877e+01 2.81e-03 3.33e-05 -5.7 3.00e+00 -5.0 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (337659)\n", - " 12 -2.6870878e+01 1.08e-04 7.14e-05 -5.7 8.84e-02 -5.4 1.00e+00 1.00e+00h 1\n", - " 13 -2.6870919e+01 1.06e-03 1.84e-05 -5.7 6.15e-01 -5.9 1.00e+00 1.00e+00h 1\n", - " 14 -2.6871022e+01 9.59e-03 1.61e-04 -5.7 1.37e+00 -6.4 1.00e+00 1.00e+00h 1\n", - " 15 -2.6871350e+01 1.01e-01 1.67e-03 -5.7 3.88e+00 -6.9 1.00e+00 1.00e+00h 1\n", - " 16 -2.6872711e+01 2.39e+00 4.78e-02 -5.7 1.44e+01 -7.3 1.00e+00 1.00e+00h 1\n", - " 17 -2.6874054e+01 5.83e-01 1.31e-02 -5.7 1.62e+01 -6.9 1.00e+00 1.00e+00h 1\n", - " 18 -2.6879134e+01 1.19e+01 4.46e-01 -5.7 1.13e+02 -7.4 1.00e+00 5.07e-01h 1\n", - " 19 -2.6887284e+01 4.11e+00 1.69e-01 -5.7 7.00e+01 -7.0 1.00e+00 9.59e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -2.6892052e+01 4.46e+00 1.97e-01 -5.7 1.88e+02 -7.4 1.00e+00 1.65e-01f 1\n", - " 21 -2.6894657e+01 1.59e+00 4.78e-02 -5.7 2.29e+01 -7.9 9.98e-01 8.13e-01f 1\n", - " 22 -2.6895685e+01 1.03e-01 2.03e-03 -5.7 1.09e+01 -7.5 1.00e+00 1.00e+00f 1\n", - " 23 -2.6896013e+01 3.73e-02 4.31e-03 -5.7 8.81e+00 -8.0 1.00e+00 1.00e+00h 1\n", - " 24 -2.6896032e+01 1.65e-03 4.30e-04 -5.7 1.77e+00 -7.5 1.00e+00 1.00e+00h 1\n", - " 25 -2.6896510e+01 5.88e+00 1.25e+00 -5.7 9.18e+01 -8.0 1.00e+00 1.00e+00h 1\n", - " 26 -2.6897324e+01 2.35e+00 1.70e-01 -5.7 8.48e+01 - 1.00e+00 1.00e+00h 1\n", - " 27 -2.6899093e+01 2.49e-01 4.48e-02 -5.7 3.94e+01 - 1.00e+00 9.34e-01h 1\n", - " 28 -2.6898971e+01 7.46e-02 2.34e-03 -5.7 2.43e+01 - 1.00e+00 1.00e+00f 1\n", - " 29 -2.6898969e+01 4.09e-03 9.27e-06 -5.7 5.54e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -2.6898969e+01 2.54e-05 4.67e-08 -5.7 4.39e-01 - 1.00e+00 1.00e+00h 1\n", - " 31 -2.6898969e+01 8.92e-10 1.97e-11 -5.7 2.60e-03 - 1.00e+00 1.00e+00h 1\n", - " 32 -2.6900308e+01 1.16e-01 2.77e-03 -8.6 1.79e+01 - 9.13e-01 9.53e-01f 1\n", - " 33 -2.6900393e+01 3.09e-03 1.29e-04 -8.6 4.98e+00 - 1.00e+00 1.00e+00h 1\n", - " 34 -2.6900395e+01 2.18e-04 1.36e-06 -8.6 1.34e+00 - 1.00e+00 1.00e+00h 1\n", - " 35 -2.6900395e+01 9.33e-07 2.55e-09 -8.6 8.75e-02 - 1.00e+00 1.00e+00h 1\n", - " 36 -2.6900395e+01 1.48e-11 5.99e-14 -8.6 3.48e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 36\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -2.6900395275199973e+01 -2.6900395275199973e+01\n", - "Dual infeasibility......: 5.9903245874163696e-14 5.9903245874163696e-14\n", - "Constraint violation....: 1.4777956636180534e-11 1.4777956636180534e-11\n", - "Complementarity.........: 2.5059038382096498e-09 2.5059038382096498e-09\n", - "Overall NLP error.......: 2.5059038382096498e-09 2.5059038382096498e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 37\n", - "Number of objective gradient evaluations = 37\n", - "Number of equality constraint evaluations = 37\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 37\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 36\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.085\n", - "Total CPU secs in NLP function evaluations = 0.056\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.9\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.53e+00 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.54e-02 3.84e+00 -1.0 1.38e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.22e-04 3.52e+00 -1.0 1.38e-02 - 1.00e+00 9.92e-01h 1\n", - " 4 0.0000000e+00 7.74e-13 1.01e-06 -1.0 1.10e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 7.7449158197850920e-13 7.7449158197850920e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 7.7449158197850920e-13 7.7449158197850920e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.049\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.9\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 8.26e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 1.41e+03 3.85e+02 -1.0 8.26e+04 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.04e+02 5.99e+01 -1.0 1.69e+03 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.33e+00 2.06e+00 -1.0 8.05e+01 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.31e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393607)\n", - " 5 0.0000000e+00 9.09e-13 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0949470177292824e-13 9.0949470177292824e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0949470177292824e-13 9.0949470177292824e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.117\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -2.9638946e+01 8.42e+02 1.08e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303234)\n", - " 1 -2.9651536e+01 4.17e+02 5.15e+00 -1.0 2.52e+01 - 9.87e-01 5.03e-01h 1\n", - " 2 -2.9662204e+01 5.88e-01 1.69e-01 -1.0 1.90e+01 - 9.82e-01 1.00e+00f 1\n", - " 3 -2.9661372e+01 2.28e-01 8.72e-02 -1.7 2.54e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -2.9657366e+01 5.59e+00 1.74e-01 -1.7 1.57e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -2.9657597e+01 7.45e+00 2.43e-02 -1.7 1.12e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -2.9658155e+01 9.80e-03 5.94e-04 -1.7 9.51e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -2.9658157e+01 2.42e-07 4.73e-05 -3.8 1.06e-02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (324542)\n", - " 8 -2.9658248e+01 2.96e-02 2.07e-03 -5.7 5.06e+00 - 9.80e-01 1.00e+00h 1\n", - " 9 -2.9658255e+01 8.09e-06 1.82e-05 -5.7 1.82e-01 -4.0 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -2.9658269e+01 1.10e-04 1.97e-05 -8.6 5.90e-01 -4.5 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (353780)\n", - " 11 -2.9658314e+01 1.01e-03 1.99e-05 -8.6 1.79e+00 -5.0 1.00e+00 1.00e+00h 1\n", - " 12 -2.9658384e+01 2.94e-03 1.98e-05 -8.6 5.34e+00 -5.4 1.00e+00 5.24e-01h 1\n", - " 13 -2.9658390e+01 2.14e-04 6.05e-05 -8.6 1.92e-01 -5.9 1.00e+00 1.00e+00f 1\n", - " 14 -2.9658414e+01 2.03e-03 1.72e-05 -8.6 7.84e-01 -6.4 1.00e+00 1.00e+00h 1\n", - " 15 -2.9658485e+01 1.95e-02 1.65e-04 -8.6 2.16e+00 -6.9 1.00e+00 1.00e+00h 1\n", - " 16 -2.9658723e+01 2.33e-01 1.94e-03 -8.6 6.13e+00 -7.3 1.00e+00 1.00e+00h 1\n", - " 17 -2.9659707e+01 2.00e+01 4.71e-01 -8.6 4.15e+02 -7.8 2.10e-01 1.32e-01h 1\n", - " 18 -2.9661723e+01 1.08e+01 6.12e+00 -8.6 7.98e+01 -7.4 1.00e+00 5.36e-01h 1\n", - " 19 -2.9662944e+01 1.03e+01 8.86e+00 -8.6 2.03e+02 -7.9 7.79e-01 1.08e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -2.9664685e+01 1.03e+01 1.59e+01 -8.6 1.87e+02 -8.3 1.00e+00 1.08e-01h 1\n", - " 21 -2.9668199e+01 7.67e+00 4.66e+00 -8.6 1.09e+02 -7.9 1.00e+00 5.18e-01h 1\n", - " 22 -2.9669825e+01 7.05e+00 2.58e+01 -8.6 2.15e+02 -8.4 1.00e+00 1.50e-01h 1\n", - " 23 -2.9670730e+01 3.22e+00 2.25e+01 -8.6 3.00e+01 -8.0 4.59e-01 5.76e-01h 1\n", - " 24 -2.9672420e+01 2.48e+01 1.63e+01 -8.6 4.62e+02 -8.4 3.30e-01 2.77e-01f 1\n", - " 25 -2.9671141e+01 6.69e+00 4.35e+00 -8.6 2.59e+01 -8.0 1.00e+00 7.33e-01h 1\n", - " 26 -2.9672562e+01 4.65e-01 2.66e-02 -8.6 2.69e+01 -8.5 1.00e+00 1.00e+00h 1\n", - " 27 -2.9672664e+01 4.46e-01 1.65e-02 -8.6 1.35e+02 -9.0 1.00e+00 4.39e-01h 1\n", - " 28 -2.9672722e+01 1.68e-01 1.61e-03 -8.6 1.93e+01 - 1.00e+00 1.00e+00h 1\n", - " 29 -2.9672724e+01 1.86e-02 5.75e-04 -8.6 2.08e+01 - 1.00e+00 9.29e-01h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -2.9672723e+01 3.34e-06 7.50e-08 -8.6 2.03e-02 - 1.00e+00 1.00e+00h 1\n", - " 31 -2.9672723e+01 1.46e-11 9.19e-14 -8.6 1.60e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 31\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -2.9672723137182857e+01 -2.9672723137182857e+01\n", - "Dual infeasibility......: 9.1912354482113998e-14 9.1912354482113998e-14\n", - "Constraint violation....: 3.6379788070917130e-12 1.4551915228366852e-11\n", - "Complementarity.........: 2.5059038164605147e-09 2.5059038164605147e-09\n", - "Overall NLP error.......: 2.5059038164605147e-09 2.5059038164605147e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 32\n", - "Number of objective gradient evaluations = 32\n", - "Number of equality constraint evaluations = 32\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 32\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 31\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.978\n", - "Total CPU secs in NLP function evaluations = 0.022\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.1\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.94e+01 3.85e+02 -1.0 3.97e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.51e+01 4.19e+01 -1.0 4.93e+01 - 4.96e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.44e-01 3.41e+02 -1.0 2.83e+01 - 2.90e-01 9.94e-01h 1\n", - " 4 0.0000000e+00 2.63e-06 2.33e+02 -1.0 7.93e-02 - 9.91e-01 1.00e+00h 1\n", - " 5 0.0000000e+00 2.49e-13 1.00e-06 -1.0 1.80e-06 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.0318095320003983e-13 2.4868995751603507e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.0318095320003983e-13 2.4868995751603507e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.056\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.9\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.66e+05 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 2.25e+03 3.85e+02 -1.0 1.66e+05 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.12e+02 5.99e+01 -1.0 2.52e+03 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.41e+00 2.06e+00 -1.0 8.23e+01 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.39e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393985)\n", - " 5 0.0000000e+00 2.91e-11 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.9103830456733704e-11 2.9103830456733704e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.9103830456733704e-11 2.9103830456733704e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.109\n", - "Total CPU secs in NLP function evaluations = 0.010\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -3.2428486e+01 8.42e+02 1.05e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303234)\n", - " 1 -3.2434768e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.03e-01h 1\n", - " 2 -3.2440069e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", - " 3 -3.2439664e+01 2.27e-01 8.67e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -3.2437689e+01 5.57e+00 1.18e-01 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -3.2437810e+01 7.15e+00 1.20e-02 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -3.2438076e+01 1.07e-02 4.29e-04 -1.7 9.35e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -3.2438077e+01 3.69e-07 2.81e-05 -3.8 6.32e-03 - 1.00e+00 1.00e+00h 1\n", - " 8 -3.2438078e+01 1.83e-06 7.60e-06 -5.7 7.60e-02 -4.0 1.00e+00 1.00e+00h 1\n", - " 9 -3.2438082e+01 2.75e-05 9.82e-06 -8.6 2.95e-01 -4.5 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -3.2438093e+01 2.57e-04 1.00e-05 -8.6 9.02e-01 -5.0 1.00e+00 1.00e+00h 1\n", - " 11 -3.2438127e+01 2.32e-03 1.00e-05 -8.6 2.71e+00 -5.4 1.00e+00 1.00e+00h 1\n", - " 12 -3.2438152e+01 3.01e-03 1.00e-05 -8.6 8.06e+00 -5.9 1.00e+00 2.49e-01h 1\n", - " 13 -3.2438155e+01 4.60e-04 4.53e-05 -8.6 2.32e-01 -6.4 1.00e+00 1.00e+00f 1\n", - " 14 -3.2438173e+01 4.53e-03 1.89e-05 -8.6 1.27e+00 -6.9 1.00e+00 1.00e+00h 1\n", - " 15 -3.2438225e+01 4.49e-02 1.89e-04 -8.6 3.33e+00 -7.3 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (333035)\n", - " 16 -3.2438415e+01 6.88e-01 2.78e-03 -8.6 9.07e+00 -7.8 1.00e+00 1.00e+00h 1\n", - " 17 -3.2438526e+01 1.13e-01 3.98e-04 -8.6 3.08e+00 -7.4 1.00e+00 1.00e+00h 1\n", - " 18 -3.2438959e+01 3.03e+00 2.02e-02 -8.6 1.89e+01 -7.9 1.00e+00 1.00e+00h 1\n", - " 19 -3.2439451e+01 9.31e-01 4.43e-03 -8.6 2.65e+01 -7.4 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -3.2440256e+01 3.61e+00 2.46e-02 -8.6 2.05e+02 -7.9 9.03e-01 1.92e-01h 1\n", - " 21 -3.2440820e+01 2.54e+00 1.86e-02 -8.6 4.61e+01 -7.5 1.00e+00 4.20e-01f 1\n", - " 22 -3.2443038e+01 8.60e+00 1.12e-01 -8.6 2.61e+02 -8.0 8.80e-01 2.74e-01f 1\n", - " 23 -3.2443317e+01 8.60e+00 1.12e-01 -8.6 1.27e+03 -8.4 5.04e-01 5.61e-03h 1\n", - " 24 -3.2444082e+01 2.43e+00 2.51e-02 -8.6 1.87e+01 -8.0 1.42e-01 7.76e-01h 1\n", - " 25 -3.2444206e+01 2.00e+00 2.04e-02 -8.6 2.84e+01 -8.5 8.77e-01 1.87e-01f 1\n", - " 26 -3.2444524e+01 1.50e-01 2.01e-03 -8.6 1.06e+01 -8.1 1.00e+00 1.00e+00h 1\n", - " 27 -3.2444530e+01 1.48e-01 1.97e-03 -8.6 3.53e+01 -8.5 1.00e+00 1.95e-02h 1\n", - " 28 -3.2444603e+01 2.35e-02 9.67e-04 -8.6 9.31e+00 -8.1 1.00e+00 1.00e+00f 1\n", - " 29 -3.2444869e+01 3.57e+00 1.39e-01 -8.6 8.64e+01 -8.6 1.00e+00 6.85e-01h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -3.2445180e+01 2.78e+00 5.43e-02 -8.6 2.44e+02 -9.1 1.00e+00 6.18e-01f 1\n", - " 31 -3.2445337e+01 1.07e+00 1.03e-02 -8.6 5.66e+01 - 1.00e+00 1.00e+00h 1\n", - " 32 -3.2445329e+01 4.67e-01 4.82e-03 -8.6 9.94e+01 - 1.00e+00 5.69e-01h 1\n", - " 33 -3.2445311e+01 1.99e-03 3.70e-05 -8.6 9.19e-01 - 1.00e+00 1.00e+00h 1\n", - " 34 -3.2445311e+01 1.62e-07 1.29e-09 -8.6 1.08e-02 - 1.00e+00 1.00e+00h 1\n", - " 35 -3.2445311e+01 5.82e-11 2.86e-14 -8.6 7.59e-07 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 35\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -3.2445310986118486e+01 -3.2445310986118486e+01\n", - "Dual infeasibility......: 2.8629516867220174e-14 2.8629516867220174e-14\n", - "Constraint violation....: 2.9103830456733704e-11 5.8207660913467407e-11\n", - "Complementarity.........: 2.5059035597432385e-09 2.5059035597432385e-09\n", - "Overall NLP error.......: 2.5059035597432385e-09 2.5059035597432385e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 36\n", - "Number of objective gradient evaluations = 36\n", - "Number of equality constraint evaluations = 36\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 36\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 35\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.090\n", - "Total CPU secs in NLP function evaluations = 0.019\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.9\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.94e+01 3.85e+02 -1.0 3.97e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.51e+01 4.19e+01 -1.0 4.93e+01 - 4.96e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.70e-02 3.43e+02 -1.0 2.83e+01 - 2.90e-01 9.98e-01h 1\n", - " 4 0.0000000e+00 7.99e-08 2.33e+02 -1.0 4.00e-02 - 9.91e-01 1.00e+00h 1\n", - " 5 0.0000000e+00 2.63e-13 1.00e-06 -1.0 7.80e-08 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.2874055091167042e-13 2.6290081223123707e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.2874055091167042e-13 2.6290081223123707e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.048\n", - "Total CPU secs in NLP function evaluations = 0.031\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.0\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.32e+05 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 3.91e+03 3.85e+02 -1.0 3.32e+05 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.29e+02 5.99e+01 -1.0 4.18e+03 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.58e+00 2.06e+00 -1.0 8.61e+01 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.56e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393774)\n", - " 5 0.0000000e+00 1.82e-12 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.8189894035458565e-12 1.8189894035458565e-12\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.8189894035458565e-12 1.8189894035458565e-12\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.100\n", - "Total CPU secs in NLP function evaluations = 0.017\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -3.5209502e+01 8.42e+02 1.04e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303016)\n", - " 1 -3.5212641e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.03e-01h 1\n", - " 2 -3.5215292e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", - " 3 -3.5215089e+01 2.27e-01 8.53e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -3.5214101e+01 5.57e+00 9.59e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -3.5214161e+01 7.15e+00 6.15e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -3.5214294e+01 1.06e-02 4.14e-04 -1.7 9.34e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -3.5214295e+01 7.51e-07 1.89e-05 -3.8 1.17e-02 - 1.00e+00 1.00e+00h 1\n", - " 8 -3.5214300e+01 1.80e-03 1.13e-05 -5.7 1.27e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (327848)\n", - " 9 -3.5214301e+01 6.97e-07 4.87e-06 -5.7 4.87e-02 -4.0 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -3.5214302e+01 7.02e-06 4.97e-06 -8.6 1.49e-01 -4.5 1.00e+00 1.00e+00h 1\n", - " 11 -3.5214305e+01 6.46e-05 5.02e-06 -8.6 4.52e-01 -5.0 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (348486)\n", - " 12 -3.5214313e+01 5.82e-04 5.03e-06 -8.6 1.36e+00 -5.4 1.00e+00 1.00e+00h 1\n", - " 13 -3.5214337e+01 4.65e-03 5.02e-06 -8.6 4.07e+00 -5.9 1.00e+00 9.40e-01h 1\n", - " 14 -3.5214337e+01 4.50e-03 2.33e-04 -8.6 1.47e+00 -6.4 1.00e+00 3.12e-02f 6\n", - " 15 -3.5214341e+01 1.09e-03 3.28e-05 -8.6 6.15e-01 -6.9 1.00e+00 1.00e+00h 1\n", - " 16 -3.5214353e+01 9.95e-03 2.09e-05 -8.6 1.69e+00 -7.3 1.00e+00 1.00e+00h 1\n", - " 17 -3.5214394e+01 1.07e-01 2.26e-04 -8.6 4.92e+00 -7.8 1.00e+00 1.00e+00h 1\n", - " 18 -3.5214572e+01 3.42e+00 7.71e-03 -8.6 1.68e+01 -8.3 1.00e+00 1.00e+00h 1\n", - " 19 -3.5214789e+01 1.03e+00 2.63e-03 -8.6 2.34e+01 -7.9 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -3.5215305e+01 8.47e+00 3.52e-02 -8.6 5.34e+02 -8.3 2.91e-01 1.01e-01h 1\n", - " 21 -3.5215730e+01 6.31e+00 2.51e-02 -8.6 7.99e+01 -7.9 1.00e+00 3.57e-01f 1\n", - " 22 -3.5216694e+01 1.02e+01 5.63e-02 -8.6 7.73e+02 -8.4 3.55e-01 7.78e-02h 1\n", - " 23 -3.5216797e+01 9.76e+00 5.36e-02 -8.6 1.42e+02 -8.9 1.04e-01 4.84e-02h 1\n", - " 24 -3.5216885e+01 8.53e+00 4.68e-02 -8.6 2.74e+01 -8.4 5.30e-01 1.28e-01h 1\n", - " 25 -3.5217124e+01 6.99e+00 3.66e-02 -8.6 5.63e+01 -8.9 3.25e-01 2.16e-01h 1\n", - " 26 -3.5217494e+01 4.51e-01 1.53e-03 -8.6 1.69e+01 -8.5 1.00e+00 9.89e-01f 1\n", - " 27 -3.5217762e+01 3.34e+01 5.83e-01 -8.6 2.98e+02 -9.0 3.79e-01 5.84e-01f 1\n", - " 28 -3.5217782e+01 3.08e+01 5.37e-01 -8.6 9.68e+01 -8.5 5.35e-01 8.04e-02h 1\n", - " 29 -3.5215828e+01 2.37e+01 4.05e-01 -8.6 4.07e+01 -8.1 2.51e-02 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -3.5216161e+01 1.84e+01 3.12e-01 -8.6 2.64e+01 -8.6 8.03e-01 2.30e-01h 1\n", - " 31 -3.5217419e+01 3.16e+00 2.96e-02 -8.6 2.66e+01 -9.1 1.20e-04 1.00e+00f 1\n", - " 32 -3.5217481e+01 3.12e+00 2.90e-02 -8.6 1.92e+02 -9.6 4.18e-01 3.25e-02h 1\n", - " 33 -3.5217730e+01 3.96e+00 6.86e-02 -8.6 1.96e+02 -10.0 2.65e-07 1.67e-01h 1\n", - " 34 -3.5218174e+01 1.05e+01 5.09e-02 -8.6 6.76e+01 - 4.79e-01 1.00e+00f 1\n", - " 35 -3.5218144e+01 9.56e+00 4.62e-02 -8.6 5.60e+02 - 6.27e-01 9.14e-02h 1\n", - " 36 -3.5217875e+01 1.29e-01 6.80e-03 -8.6 1.14e+01 - 1.00e+00 1.00e+00h 1\n", - " 37 -3.5217899e+01 1.43e-02 9.14e-05 -8.6 2.93e+00 - 1.00e+00 1.00e+00h 1\n", - " 38 -3.5217899e+01 1.12e-05 9.11e-08 -8.6 7.12e-02 - 1.00e+00 1.00e+00h 1\n", - " 39 -3.5217899e+01 5.82e-11 8.02e-14 -8.6 6.30e-05 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 39\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -3.5217898835055578e+01 -3.5217898835055578e+01\n", - "Dual infeasibility......: 8.0154814536833473e-14 8.0154814536833473e-14\n", - "Constraint violation....: 5.8207660913467407e-11 5.8207660913467407e-11\n", - "Complementarity.........: 2.5059035610674620e-09 2.5059035610674620e-09\n", - "Overall NLP error.......: 2.5059035610674620e-09 2.5059035610674620e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 45\n", - "Number of objective gradient evaluations = 40\n", - "Number of equality constraint evaluations = 45\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 40\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 39\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.271\n", - "Total CPU secs in NLP function evaluations = 0.044\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 4.2\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.93e+01 3.85e+02 -1.0 3.97e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.51e+01 4.19e+01 -1.0 4.93e+01 - 4.96e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.18e-01 3.44e+02 -1.0 2.83e+01 - 2.89e-01 1.00e+00h 1\n", - " 4 0.0000000e+00 7.85e-11 1.38e+02 -1.0 8.41e-02 - 9.91e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 7.8493656019418268e-11 7.8493656019418268e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 7.8493656019418268e-11 7.8493656019418268e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.062\n", - "Total CPU secs in NLP function evaluations = 0.003\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.0\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.64e+05 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 7.23e+03 3.85e+02 -1.0 6.64e+05 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.62e+02 5.99e+01 -1.0 7.51e+03 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.91e+00 2.06e+00 -1.0 9.36e+01 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.89e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393697)\n", - " 5 0.0000000e+00 7.28e-12 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 7.2759576141834259e-12 7.2759576141834259e-12\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 7.2759576141834259e-12 7.2759576141834259e-12\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.107\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -3.7986293e+01 8.42e+02 1.03e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303122)\n", - " 1 -3.7987862e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.03e-01h 1\n", - " 2 -3.7989187e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", - " 3 -3.7989086e+01 2.27e-01 8.49e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -3.7988591e+01 5.57e+00 8.46e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -3.7988622e+01 7.15e+00 3.66e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -3.7988688e+01 1.06e-02 4.07e-04 -1.7 9.34e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -3.7988689e+01 1.02e-06 1.41e-05 -3.8 1.65e-02 - 1.00e+00 1.00e+00h 1\n", - " 8 -3.7988690e+01 4.48e-04 5.42e-06 -5.7 6.35e-01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (324149)\n", - " 9 -3.7988690e+01 1.84e-07 2.46e-06 -5.7 2.46e-02 -4.0 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -3.7988690e+01 1.76e-06 2.49e-06 -8.6 7.46e-02 -4.5 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (343489)\n", - " 11 -3.7988691e+01 1.62e-05 2.51e-06 -8.6 2.26e-01 -5.0 1.00e+00 1.00e+00h 1\n", - " 12 -3.7988693e+01 1.46e-04 2.51e-06 -8.6 6.79e-01 -5.4 1.00e+00 1.00e+00h 1\n", - " 13 -3.7988699e+01 1.31e-03 2.51e-06 -8.6 2.04e+00 -5.9 1.00e+00 1.00e+00h 1\n", - " 14 -3.7988708e+01 3.29e-03 2.51e-06 -8.6 6.08e+00 -6.4 1.00e+00 4.72e-01h 1\n", - " 15 -3.7988708e+01 2.88e-03 9.75e-05 -8.6 7.36e-01 -6.9 1.00e+00 1.25e-01f 4\n", - " 16 -3.7988711e+01 2.52e-03 2.24e-05 -8.6 9.76e-01 -7.3 1.00e+00 1.00e+00h 1\n", - " 17 -3.7988721e+01 2.30e-02 2.42e-05 -8.6 2.53e+00 -7.8 1.00e+00 1.00e+00h 1\n", - " 18 -3.7988753e+01 2.83e-01 2.94e-04 -8.6 7.23e+00 -8.3 1.00e+00 1.00e+00h 1\n", - " 19 -3.7988768e+01 4.05e-02 4.12e-05 -8.6 1.67e+00 -7.9 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -3.7988818e+01 6.38e-01 6.02e-04 -8.6 6.87e+00 -8.3 1.00e+00 1.00e+00h 1\n", - " 21 -3.7988847e+01 1.12e-01 1.12e-04 -8.6 4.05e+00 -7.9 1.00e+00 1.00e+00h 1\n", - " 22 -3.7988962e+01 2.54e+00 4.68e-03 -8.6 1.86e+01 -8.4 1.00e+00 1.00e+00h 1\n", - " 23 -3.7989078e+01 7.34e-01 8.67e-04 -8.6 2.48e+01 -8.0 1.00e+00 1.00e+00h 1\n", - " 24 -3.7989232e+01 2.08e+00 3.41e-03 -8.6 1.45e+02 -8.4 1.00e+00 2.09e-01h 1\n", - " 25 -3.7989337e+01 1.43e+00 2.58e-03 -8.6 3.34e+01 -8.0 1.00e+00 4.49e-01f 1\n", - " 26 -3.7989784e+01 5.92e+00 1.95e-02 -8.6 1.61e+02 -8.5 1.00e+00 3.75e-01f 1\n", - " 27 -3.7990124e+01 2.81e+00 9.84e-03 -8.6 4.53e+01 -8.1 1.00e+00 6.70e-01f 1\n", - " 28 -3.7990143e+01 1.18e+00 3.83e-03 -8.6 1.17e+01 -8.5 1.00e+00 6.36e-01f 1\n", - " 29 -3.7990260e+01 5.81e-01 1.51e-03 -8.6 2.24e+01 -9.0 1.00e+00 6.81e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -3.7990293e+01 3.41e-02 1.10e-04 -8.6 6.65e+00 -8.6 1.00e+00 1.00e+00f 1\n", - " 31 -3.7990329e+01 1.38e-01 1.24e-03 -8.6 1.50e+01 -9.1 1.00e+00 1.00e+00h 1\n", - " 32 -3.7990335e+01 1.01e-01 8.90e-04 -8.6 9.80e+00 -8.7 1.00e+00 3.33e-01h 1\n", - " 33 -3.7990393e+01 2.39e+00 2.02e-02 -8.6 5.51e+01 -9.1 1.00e+00 7.80e-01f 1\n", - " 34 -3.7990507e+01 4.27e+00 1.27e-02 -8.6 7.38e+01 -9.6 1.00e+00 7.82e-01h 1\n", - " 35 -3.7990474e+01 4.87e-02 8.37e-04 -8.6 1.92e+01 -9.2 1.00e+00 1.00e+00h 1\n", - " 36 -3.7990485e+01 2.85e-01 3.77e-04 -8.6 6.89e+01 -9.7 1.00e+00 1.00e+00h 1\n", - " 37 -3.7990486e+01 2.82e-01 3.75e-04 -8.6 4.06e+02 -10.1 1.00e+00 1.75e-02h 1\n", - " 38 -3.7990487e+01 5.77e-04 7.14e-07 -8.6 6.52e-01 - 1.00e+00 1.00e+00h 1\n", - " 39 -3.7990487e+01 1.03e-05 3.01e-08 -8.6 4.89e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 40 -3.7990487e+01 1.16e-10 1.03e-13 -8.6 7.76e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 40\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -3.7990486683995655e+01 -3.7990486683995655e+01\n", - "Dual infeasibility......: 1.0349462628516265e-13 1.0349462628516265e-13\n", - "Constraint violation....: 2.5821123017522041e-11 1.1641532182693481e-10\n", - "Complementarity.........: 2.5059035596820578e-09 2.5059035596820578e-09\n", - "Overall NLP error.......: 2.5059035596820578e-09 2.5059035596820578e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 44\n", - "Number of objective gradient evaluations = 41\n", - "Number of equality constraint evaluations = 44\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 41\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 40\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.453\n", - "Total CPU secs in NLP function evaluations = 0.019\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 4.5\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.97e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.93e+01 3.85e+02 -1.0 3.97e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.51e+01 4.19e+01 -1.0 4.93e+01 - 4.96e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.16e-01 3.45e+02 -1.0 2.83e+01 - 2.89e-01 1.00e+00h 1\n", - " 4 0.0000000e+00 7.46e-11 6.85e+01 -1.0 8.27e-02 - 9.91e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 7.4614092682168121e-11 7.4614092682168121e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 7.4614092682168121e-11 7.4614092682168121e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.064\n", - "Total CPU secs in NLP function evaluations = 0.001\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.3\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.33e+06 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 1.39e+04 3.85e+02 -1.0 1.33e+06 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 2.29e+02 5.99e+01 -1.0 1.42e+04 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 5.56e+00 2.06e+00 -1.0 1.16e+02 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 4.54e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (394246)\n", - " 5 0.0000000e+00 2.33e-10 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.3283064365386963e-10 2.3283064365386963e-10\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.3283064365386963e-10 2.3283064365386963e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.108\n", - "Total CPU secs in NLP function evaluations = 0.001\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -4.0760980e+01 8.42e+02 1.02e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303234)\n", - " 1 -4.0761764e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.03e-01h 1\n", - " 2 -4.0762427e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", - " 3 -4.0762376e+01 2.27e-01 8.49e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -4.0762129e+01 5.57e+00 7.90e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -4.0762144e+01 7.15e+00 3.34e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -4.0762177e+01 1.06e-02 4.09e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -4.0762177e+01 1.17e-06 1.27e-05 -3.8 1.89e-02 - 1.00e+00 1.00e+00h 1\n", - " 8 -4.0762177e+01 1.11e-04 2.70e-06 -5.7 3.17e-01 - 1.00e+00 1.00e+00h 1\n", - " 9 -4.0762222e+01 5.53e-01 1.52e-03 -5.7 2.86e+01 - 1.00e+00 1.00e+00H 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -4.0762239e+01 1.77e-01 2.60e-03 -5.7 5.61e+01 - 1.00e+00 2.50e-01h 3\n", - " 11 -4.0762241e+01 3.40e-04 3.60e-05 -5.7 3.60e-01 -4.0 1.00e+00 1.00e+00h 1\n", - " 12 -4.0762241e+01 2.26e-07 8.96e-07 -5.7 2.69e-02 -4.5 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (319066)\n", - " 13 -4.0762540e+01 3.15e+01 1.26e-01 -8.6 1.14e+02 - 3.56e-01 1.00e+00h 1\n", - " 14 -4.0763467e+01 6.08e+01 2.43e-01 -8.6 1.29e+04 - 2.22e-02 2.26e-02h 1\n", - " 15 -4.0763784e+01 2.63e+01 3.51e-02 -8.6 1.44e+02 - 4.76e-01 1.00e+00h 1\n", - " 16 -4.0763958e+01 8.50e-01 4.67e-03 -8.6 7.67e+01 - 6.65e-01 1.00e+00h 1\n", - " 17 -4.0764005e+01 2.35e+00 2.00e-03 -8.6 1.84e+02 - 6.15e-01 6.58e-01h 1\n", - " 18 -4.0764023e+01 3.56e-02 1.49e-04 -8.6 5.80e+00 - 1.00e+00 1.00e+00h 1\n", - " 19 -4.0764023e+01 3.49e-05 5.34e-08 -8.6 5.34e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -4.0764023e+01 3.10e-09 4.31e-12 -8.6 5.03e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 20\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -4.0764023387128162e+01 -4.0764023387128162e+01\n", - "Dual infeasibility......: 4.3057665671855009e-12 4.3057665671855009e-12\n", - "Constraint violation....: 3.0985215504486519e-09 3.0985215504486519e-09\n", - "Complementarity.........: 2.5059046339081401e-09 2.5059046339081401e-09\n", - "Overall NLP error.......: 3.0985215504486519e-09 3.0985215504486519e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 25\n", - "Number of objective gradient evaluations = 21\n", - "Number of equality constraint evaluations = 25\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 21\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 20\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.467\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.4\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.25e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 5.78e+01 3.85e+02 -1.0 3.25e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 2.70e+01 4.60e+02 -1.0 7.21e+01 - 5.56e-02 9.90e-01h 1\n", - " 3 0.0000000e+00 1.18e-01 1.30e+02 -1.0 1.54e+01 - 4.83e-01 1.00e+00f 1\n", - " 4 0.0000000e+00 2.62e-10 5.01e+01 -1.0 5.82e-02 - 9.91e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.6183499812759692e-10 2.6183499812759692e-10\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.6183499812759692e-10 2.6183499812759692e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.041\n", - "Total CPU secs in NLP function evaluations = 0.028\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.9\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 2.66e+06 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 2.72e+04 3.85e+02 -1.0 2.66e+06 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 3.62e+02 5.99e+01 -1.0 2.75e+04 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 6.88e+00 2.06e+00 -1.0 2.49e+02 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 5.86e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393945)\n", - " 5 0.0000000e+00 4.66e-10 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 4.6566128730773926e-10 4.6566128730773926e-10\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 4.6566128730773926e-10 4.6566128730773926e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.118\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -4.3535091e+01 8.42e+02 1.01e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303234)\n", - " 1 -4.3535483e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", - " 2 -4.3535814e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", - " 3 -4.3535789e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -4.3535665e+01 5.57e+00 7.62e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -4.3535673e+01 7.15e+00 3.19e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -4.3535690e+01 1.06e-02 4.10e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -4.3535690e+01 1.25e-06 1.39e-05 -3.8 2.01e-02 - 1.00e+00 1.00e+00h 1\n", - " 8 -4.3535690e+01 7.45e-09 4.81e-07 -5.7 4.81e-03 -4.0 1.00e+00 1.00e+00h 1\n", - " 9 -4.3535690e+01 1.10e-07 6.22e-07 -8.6 1.86e-02 -4.5 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -4.3535690e+01 1.02e-06 6.29e-07 -8.6 5.66e-02 -5.0 1.00e+00 1.00e+00h 1\n", - " 11 -4.3535690e+01 9.13e-06 6.29e-07 -8.6 1.70e-01 -5.4 1.00e+00 1.00e+00h 1\n", - " 12 -4.3535690e+01 8.21e-05 6.29e-07 -8.6 5.09e-01 -5.9 1.00e+00 1.00e+00h 1\n", - " 13 -4.3535691e+01 7.36e-04 6.28e-07 -8.6 1.53e+00 -6.4 1.00e+00 1.00e+00h 1\n", - " 14 -4.3535694e+01 4.46e-03 6.23e-07 -8.6 4.53e+00 -6.9 1.00e+00 8.17e-01h 1\n", - " 15 -4.3535694e+01 3.40e-03 2.22e-05 -8.6 1.51e+00 -7.3 1.00e+00 2.50e-01f 3\n", - " 16 -4.3535694e+01 6.98e-05 3.89e-07 -8.6 4.14e-01 -6.9 1.00e+00 1.00e+00h 1\n", - " 17 -4.3535695e+01 1.95e-04 1.16e-07 -8.6 3.13e-01 -7.4 1.00e+00 1.00e+00h 1\n", - " 18 -4.3535695e+01 1.68e-03 4.37e-07 -8.6 7.09e-01 -7.9 1.00e+00 1.00e+00h 1\n", - " 19 -4.3535696e+01 2.35e-04 6.20e-08 -8.6 2.55e-01 -7.4 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (335313)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -4.3535696e+01 2.19e-03 5.77e-07 -8.6 7.85e-01 -7.9 1.00e+00 1.00e+00h 1\n", - " 21 -4.3535699e+01 2.12e-02 5.60e-06 -8.6 2.33e+00 -8.4 1.00e+00 1.00e+00h 1\n", - " 22 -4.3535706e+01 2.57e-01 6.66e-05 -8.6 6.62e+00 -8.9 1.00e+00 1.00e+00h 1\n", - " 23 -4.3535710e+01 3.67e-02 9.31e-06 -8.6 1.58e+00 -8.4 1.00e+00 1.00e+00h 1\n", - " 24 -4.3535722e+01 5.54e-01 1.31e-04 -8.6 6.37e+00 -8.9 1.00e+00 1.00e+00h 1\n", - " 25 -4.3535728e+01 9.45e-02 2.27e-05 -8.6 3.44e+00 -8.5 1.00e+00 1.00e+00h 1\n", - " 26 -4.3535753e+01 1.95e+00 8.61e-04 -8.6 1.60e+01 -9.0 1.00e+00 1.00e+00h 1\n", - " 27 -4.3535776e+01 5.09e-01 1.62e-04 -8.6 1.94e+01 -8.5 1.00e+00 1.00e+00h 1\n", - " 28 -4.3535823e+01 2.99e+00 1.35e-03 -8.6 9.76e+01 -9.0 1.00e+00 3.84e-01h 1\n", - " 29 -4.3535854e+01 1.72e+00 8.11e-04 -8.6 3.16e+01 -8.6 1.00e+00 5.32e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -4.3535966e+01 5.97e+00 4.83e-03 -8.6 1.46e+02 -9.1 1.00e+00 4.18e-01f 1\n", - " 31 -4.3536027e+01 6.62e+00 5.59e-03 -8.6 4.78e+03 -9.6 8.09e-02 5.49e-03h 1\n", - " 32 -4.3536055e+01 5.60e+00 4.51e-03 -8.6 8.68e+01 -10.0 3.39e-01 1.92e-01h 1\n", - " 33 -4.3536080e+01 2.83e+00 2.13e-03 -8.6 2.89e+01 -9.6 8.95e-01 5.28e-01h 1\n", - " 34 -4.3536087e+01 1.67e+00 1.24e-03 -8.6 1.23e+01 -9.2 1.00e+00 4.18e-01f 1\n", - " 35 -4.3536112e+01 1.12e+00 2.09e-03 -8.6 3.20e+01 -9.7 1.00e+00 1.00e+00f 1\n", - " 36 -4.3536116e+01 1.13e+00 2.04e-03 -8.6 9.56e+01 -10.1 1.00e+00 7.04e-02h 1\n", - " 37 -4.3536133e+01 1.45e+00 1.17e-03 -8.6 2.53e+01 -9.7 1.00e+00 1.00e+00f 1\n", - " 38 -4.3536137e+01 7.69e-01 2.36e-04 -8.6 5.00e+01 -10.2 1.00e+00 1.00e+00h 1\n", - " 39 -4.3536139e+01 6.87e-01 2.41e-04 -8.6 5.07e+02 -10.7 6.28e-01 1.78e-01h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 40 -4.3536137e+01 4.16e-02 2.05e-05 -8.6 2.23e+01 - 1.00e+00 1.00e+00h 1\n", - " 41 -4.3536138e+01 2.47e-02 1.88e-05 -8.6 2.36e+01 - 1.00e+00 9.45e-01h 1\n", - " 42 -4.3536138e+01 1.80e-05 1.27e-08 -8.6 6.48e-01 - 1.00e+00 1.00e+00h 1\n", - " 43 -4.3536138e+01 1.79e-09 1.37e-12 -8.6 6.46e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 43\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -4.3536137623477664e+01 -4.3536137623477664e+01\n", - "Dual infeasibility......: 1.3656458789760815e-12 1.3656458789760815e-12\n", - "Constraint violation....: 1.7926176099081204e-09 1.7926176099081204e-09\n", - "Complementarity.........: 2.5060411067492794e-09 2.5060411067492794e-09\n", - "Overall NLP error.......: 2.5060411067492794e-09 2.5060411067492794e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 46\n", - "Number of objective gradient evaluations = 44\n", - "Number of equality constraint evaluations = 46\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 44\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 43\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.281\n", - "Total CPU secs in NLP function evaluations = 0.049\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 4.2\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.96e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.93e+01 3.85e+02 -1.0 3.96e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.51e+01 4.20e+01 -1.0 4.92e+01 - 4.96e-01 9.91e-01h 1\n", - " 3 0.0000000e+00 1.04e-01 3.48e+02 -1.0 2.82e+01 - 2.86e-01 1.00e+00h 1\n", - " 4 0.0000000e+00 5.40e-11 1.66e+01 -1.0 7.42e-02 - 9.91e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.3969273494658410e-11 5.3969273494658410e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.3969273494658410e-11 5.3969273494658410e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.043\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.3\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 5.32e+06 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 5.38e+04 3.85e+02 -1.0 5.32e+06 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 6.28e+02 5.99e+01 -1.0 5.40e+04 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 9.50e+00 2.06e+00 -1.0 5.15e+02 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 8.48e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393530)\n", - " 5 0.0000000e+00 1.16e-10 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.1641532182693481e-10 1.1641532182693481e-10\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.1641532182693481e-10 1.1641532182693481e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", - "Total CPU secs in NLP function evaluations = 0.016\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -4.6308203e+01 8.42e+02 1.01e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303016)\n", - " 1 -4.6308399e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", - " 2 -4.6308565e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", - " 3 -4.6308552e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -4.6308490e+01 5.57e+00 7.50e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -4.6308494e+01 7.15e+00 3.11e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -4.6308502e+01 1.06e-02 4.10e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -4.6308502e+01 1.29e-06 1.45e-05 -3.8 2.07e-02 - 1.00e+00 1.00e+00h 1\n", - " 8 -4.6308502e+01 2.79e-09 2.41e-07 -5.7 2.41e-03 -4.0 1.00e+00 1.00e+00h 1\n", - " 9 -4.6308502e+01 2.70e-08 3.11e-07 -8.6 9.33e-03 -4.5 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -4.6308502e+01 2.54e-07 3.15e-07 -8.6 2.83e-02 -5.0 1.00e+00 1.00e+00h 1\n", - " 11 -4.6308502e+01 2.28e-06 3.14e-07 -8.6 8.49e-02 -5.4 1.00e+00 1.00e+00h 1\n", - " 12 -4.6308503e+01 2.05e-05 3.14e-07 -8.6 2.55e-01 -5.9 1.00e+00 1.00e+00h 1\n", - " 13 -4.6308503e+01 1.84e-04 3.14e-07 -8.6 7.63e-01 -6.4 1.00e+00 1.00e+00h 1\n", - " 14 -4.6308504e+01 1.64e-03 3.12e-07 -8.6 2.28e+00 -6.9 1.00e+00 1.00e+00h 1\n", - " 15 -4.6308505e+01 3.08e-03 3.98e-07 -8.6 6.49e+00 -7.3 1.00e+00 3.96e-01h 1\n", - " 16 -4.6308505e+01 4.06e-04 1.16e-06 -8.6 5.34e-01 -7.8 1.00e+00 1.00e+00f 1\n", - "Reallocating memory for MA57: lfact (332229)\n", - " 17 -4.6308505e+01 3.58e-03 4.28e-07 -8.6 1.71e+00 -8.3 1.00e+00 1.00e+00h 1\n", - " 18 -4.6308507e+01 2.97e-02 3.87e-06 -8.6 2.87e+00 -8.8 1.00e+00 1.00e+00h 1\n", - " 19 -4.6308511e+01 3.82e-01 4.92e-05 -8.6 7.99e+00 -9.2 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -4.6308514e+01 5.59e-02 6.88e-06 -8.6 1.91e+00 -8.8 1.00e+00 1.00e+00h 1\n", - " 21 -4.6308521e+01 9.99e-01 1.26e-04 -8.6 9.30e+00 -9.3 1.00e+00 1.00e+00h 1\n", - " 22 -4.6308527e+01 1.97e-01 3.02e-05 -8.6 7.51e+00 -8.9 1.00e+00 1.00e+00h 1\n", - " 23 -4.6308550e+01 5.83e+00 1.53e-03 -8.6 3.22e+01 -9.4 1.00e+00 1.00e+00h 1\n", - " 24 -4.6308572e+01 3.03e+00 6.36e-04 -8.6 5.12e+01 -8.9 1.00e+00 6.43e-01h 1\n", - " 25 -4.6308591e+01 3.33e+00 7.08e-04 -8.6 1.77e+02 -9.4 1.00e+00 1.29e-01f 1\n", - " 26 -4.6308634e+01 1.98e+00 7.94e-04 -8.6 4.29e+01 -9.0 1.00e+00 1.00e+00f 1\n", - " 27 -4.6308670e+01 2.73e+00 1.19e-03 -8.6 1.96e+02 -9.5 1.00e+00 1.61e-01h 1\n", - " 28 -4.6308683e+01 2.71e+00 1.18e-03 -8.6 2.33e+02 -9.9 1.00e+00 4.70e-02h 1\n", - " 29 -4.6308696e+01 2.50e-01 9.00e-05 -8.6 9.45e+00 -9.5 8.17e-01 9.93e-01h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -4.6308703e+01 2.04e-01 1.41e-04 -8.6 2.34e+01 -10.0 1.00e+00 5.79e-01f 1\n", - " 31 -4.6308703e+01 2.45e-02 1.55e-05 -8.6 6.94e+00 -9.6 1.00e+00 1.00e+00f 1\n", - " 32 -4.6308709e+01 8.74e-01 1.10e-03 -8.6 3.06e+01 -10.0 1.00e+00 9.86e-01h 1\n", - " 33 -4.6308713e+01 4.19e-01 1.63e-04 -8.6 1.23e+01 -9.6 1.00e+00 1.00e+00f 1\n", - " 34 -4.6308722e+01 1.83e+00 9.77e-04 -8.6 3.09e+01 -10.1 1.00e+00 1.00e+00h 1\n", - " 35 -4.6308726e+01 9.33e-01 1.76e-04 -8.6 6.82e+01 -10.6 1.00e+00 1.00e+00h 1\n", - " 36 -4.6308726e+01 8.58e-01 1.24e-04 -8.6 8.39e+02 -11.0 4.07e-01 8.08e-02h 1\n", - " 37 -4.6308725e+01 2.75e-02 9.18e-06 -8.6 1.48e+01 - 1.00e+00 1.00e+00h 1\n", - " 38 -4.6308725e+01 9.21e-03 3.64e-06 -8.6 1.42e+01 - 1.00e+00 1.00e+00h 1\n", - " 39 -4.6308725e+01 2.12e-05 8.21e-09 -8.6 7.01e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 40 -4.6308725e+01 1.86e-09 1.31e-13 -8.6 2.53e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 40\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -4.6308725472442092e+01 -4.6308725472442092e+01\n", - "Dual infeasibility......: 1.3063844228700835e-13 1.3063844228700835e-13\n", - "Constraint violation....: 9.3132257461547852e-10 1.8626451492309570e-09\n", - "Complementarity.........: 2.5059035596850455e-09 2.5059035596850455e-09\n", - "Overall NLP error.......: 2.5059035596850455e-09 2.5059035596850455e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 41\n", - "Number of objective gradient evaluations = 41\n", - "Number of equality constraint evaluations = 41\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 41\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 40\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.242\n", - "Total CPU secs in NLP function evaluations = 0.010\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 4.3\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.96e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.92e+01 3.85e+02 -1.0 3.96e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.51e+01 4.22e+01 -1.0 4.90e+01 - 4.96e-01 9.93e-01h 1\n", - " 3 0.0000000e+00 8.83e-02 3.52e+02 -1.0 2.82e+01 - 2.81e-01 1.00e+00h 1\n", - " 4 0.0000000e+00 3.29e-11 8.00e+00 -1.0 6.29e-02 - 9.91e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 3.2869706956262235e-11 3.2869706956262235e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 3.2869706956262235e-11 3.2869706956262235e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.046\n", - "Total CPU secs in NLP function evaluations = 0.016\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.8\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.06e+07 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 1.07e+05 3.85e+02 -1.0 1.06e+07 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.16e+03 5.99e+01 -1.0 1.07e+05 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.48e+01 2.06e+00 -1.0 1.05e+03 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 1.37e+01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393774)\n", - " 5 0.0000000e+00 2.33e-10 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.3283064365386963e-10 2.3283064365386963e-10\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.3283064365386963e-10 2.3283064365386963e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.100\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -4.9081053e+01 8.42e+02 1.01e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303140)\n", - " 1 -4.9081151e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", - " 2 -4.9081234e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", - " 3 -4.9081228e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -4.9081197e+01 5.57e+00 7.50e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -4.9081199e+01 7.15e+00 3.08e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -4.9081203e+01 1.06e-02 4.10e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -4.9081203e+01 1.31e-06 1.47e-05 -3.8 2.10e-02 - 1.00e+00 1.00e+00h 1\n", - " 8 -4.9081203e+01 1.70e-06 3.33e-07 -5.7 3.92e-02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (322057)\n", - " 9 -4.9081203e+01 1.16e-02 2.45e-05 -8.6 3.24e+00 - 9.81e-01 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -4.9081203e+01 2.03e-07 2.70e-07 -8.6 2.70e-03 -4.0 1.00e+00 1.00e+00h 1\n", - " 11 -4.9081203e+01 7.45e-09 1.57e-07 -8.6 4.71e-03 -4.5 1.00e+00 1.00e+00h 1\n", - " 12 -4.9081203e+01 6.33e-08 1.57e-07 -8.6 1.41e-02 -5.0 1.00e+00 1.00e+00h 1\n", - " 13 -4.9081203e+01 5.66e-07 1.57e-07 -8.6 4.24e-02 -5.4 1.00e+00 1.00e+00h 1\n", - " 14 -4.9081203e+01 5.09e-06 1.57e-07 -8.6 1.27e-01 -5.9 1.00e+00 1.00e+00h 1\n", - " 15 -4.9081203e+01 4.57e-05 1.57e-07 -8.6 3.81e-01 -6.4 1.00e+00 1.00e+00h 1\n", - " 16 -4.9081203e+01 4.07e-04 1.56e-07 -8.6 1.14e+00 -6.9 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (342868)\n", - " 17 -4.9081204e+01 3.47e-03 1.52e-07 -8.6 3.32e+00 -7.3 1.00e+00 1.00e+00h 1\n", - " 18 -4.9081204e+01 3.11e-03 3.19e-06 -8.6 4.30e+00 -7.8 1.00e+00 1.35e-01h 1\n", - " 19 -4.9081204e+01 7.01e-04 7.88e-07 -8.6 2.87e-01 -8.3 1.00e+00 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -4.9081205e+01 6.81e-03 4.46e-07 -8.6 1.48e+00 -8.8 1.00e+00 1.00e+00h 1\n", - " 21 -4.9081206e+01 6.91e-02 4.53e-06 -8.6 4.12e+00 -9.2 1.00e+00 1.00e+00h 1\n", - " 22 -4.9081210e+01 1.25e+00 7.64e-05 -8.6 1.05e+01 -9.7 1.00e+00 1.00e+00h 1\n", - " 23 -4.9081212e+01 2.32e-01 1.39e-05 -8.6 6.13e+00 -9.3 1.00e+00 1.00e+00h 1\n", - " 24 -4.9081226e+01 1.28e+01 1.87e-03 -8.6 4.40e+01 -9.8 1.00e+00 1.00e+00h 1\n", - " 25 -4.9081236e+01 9.38e+00 1.30e-03 -8.6 9.23e+01 -9.4 1.00e+00 3.14e-01h 1\n", - " 26 -4.9081256e+01 1.10e+01 1.39e-03 -8.6 4.30e+02 -9.8 6.97e-01 1.11e-01f 1\n", - " 27 -4.9081286e+01 6.05e+00 8.65e-04 -8.6 8.29e+01 -9.4 1.00e+00 6.91e-01f 1\n", - " 28 -4.9081293e+01 1.88e+00 2.33e-04 -8.6 1.25e+01 -9.9 8.83e-01 7.46e-01f 1\n", - " 29 -4.9081301e+01 8.63e-01 1.23e-04 -8.6 2.66e+01 -10.4 1.00e+00 6.71e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -4.9081302e+01 4.34e-02 7.27e-06 -8.6 6.80e+00 -9.9 1.00e+00 1.00e+00f 1\n", - " 31 -4.9081305e+01 2.53e+00 1.66e-03 -8.6 5.25e+01 -10.4 1.00e+00 1.00e+00h 1\n", - " 32 -4.9081319e+01 1.27e+01 2.66e-03 -8.6 1.00e+02 -10.9 1.00e+00 8.30e-01h 1\n", - " 33 -4.9081310e+01 4.58e-01 2.81e-04 -8.6 2.59e+01 -10.5 1.00e+00 1.00e+00h 1\n", - " 34 -4.9081312e+01 3.50e-01 1.29e-04 -8.6 7.29e+01 -10.9 1.00e+00 6.14e-01h 1\n", - " 35 -4.9081313e+01 1.07e-01 8.13e-06 -8.6 1.93e+01 - 1.00e+00 1.00e+00f 1\n", - " 36 -4.9081313e+01 1.48e-02 3.65e-06 -8.6 1.78e+01 - 1.00e+00 1.00e+00h 1\n", - " 37 -4.9081313e+01 5.02e-05 1.04e-08 -8.6 1.08e+00 - 1.00e+00 1.00e+00h 1\n", - " 38 -4.9081313e+01 1.86e-09 3.39e-13 -8.6 6.17e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 38\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -4.9081313321398035e+01 -4.9081313321398035e+01\n", - "Dual infeasibility......: 3.3870588295113704e-13 3.3870588295113704e-13\n", - "Constraint violation....: 1.6578169947933930e-09 1.8626451492309570e-09\n", - "Complementarity.........: 2.5059035596847262e-09 2.5059035596847262e-09\n", - "Overall NLP error.......: 2.5059035596847262e-09 2.5059035596847262e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 39\n", - "Number of objective gradient evaluations = 39\n", - "Number of equality constraint evaluations = 39\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 39\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 38\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.199\n", - "Total CPU secs in NLP function evaluations = 0.015\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 4.1\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.94e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.91e+01 3.85e+02 -1.0 3.94e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.52e+01 4.24e+01 -1.0 4.87e+01 - 4.96e-01 9.95e-01h 1\n", - " 3 0.0000000e+00 5.64e-02 3.60e+02 -1.0 2.82e+01 - 2.71e-01 1.00e+00h 1\n", - " 4 0.0000000e+00 8.55e-12 3.70e+00 -1.0 4.01e-02 - 9.91e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 8.5549345385516062e-12 8.5549345385516062e-12\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 8.5549345385516062e-12 8.5549345385516062e-12\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.053\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.0\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 2.13e+07 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 2.13e+05 3.85e+02 -1.0 2.13e+07 - 2.54e-03 9.90e-01h 1\n", - " 2 0.0000000e+00 2.22e+03 5.99e+01 -1.0 2.14e+05 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 2.53e+01 2.06e+00 -1.0 2.11e+03 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 2.42e+01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393985)\n", - " 5 0.0000000e+00 4.66e-10 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 4.6566128730773926e-10 4.6566128730773926e-10\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 4.6566128730773926e-10 4.6566128730773926e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.110\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -5.1853772e+01 8.42e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303252)\n", - " 1 -5.1853821e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", - " 2 -5.1853862e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", - " 3 -5.1853859e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -5.1853844e+01 5.57e+00 7.49e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -5.1853845e+01 7.15e+00 3.09e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -5.1853847e+01 1.06e-02 4.11e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -5.1853847e+01 1.32e-06 1.49e-05 -3.8 2.12e-02 - 1.00e+00 1.00e+00h 1\n", - " 8 -5.1853847e+01 7.45e-09 6.02e-08 -5.7 6.02e-04 -4.0 1.00e+00 1.00e+00h 1\n", - " 9 -5.1853847e+01 7.45e-09 7.78e-08 -8.6 2.33e-03 -4.5 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -5.1853847e+01 1.86e-08 7.86e-08 -8.6 7.08e-03 -5.0 1.00e+00 1.00e+00h 1\n", - " 11 -5.1853847e+01 1.42e-07 7.86e-08 -8.6 2.12e-02 -5.4 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (327115)\n", - " 12 -5.1853847e+01 1.28e-06 7.86e-08 -8.6 6.37e-02 -5.9 1.00e+00 1.00e+00h 1\n", - " 13 -5.1853847e+01 1.15e-05 7.85e-08 -8.6 1.91e-01 -6.4 1.00e+00 1.00e+00h 1\n", - " 14 -5.1853847e+01 1.03e-04 7.82e-08 -8.6 5.70e-01 -6.9 1.00e+00 1.00e+00h 1\n", - " 15 -5.1853847e+01 8.97e-04 7.70e-08 -8.6 1.68e+00 -7.3 1.00e+00 1.00e+00h 1\n", - " 16 -5.1853847e+01 1.50e-03 1.82e-07 -8.6 4.60e+00 -7.8 1.00e+00 3.75e-01h 2\n", - " 17 -5.1853847e+01 1.53e-03 1.58e-06 -8.6 6.12e+00 -8.3 1.00e+00 1.44e-01h 2\n", - " 18 -5.1853847e+01 1.32e-03 1.57e-06 -8.6 1.69e+00 -8.8 1.00e+00 4.38e-01h 2\n", - "Reallocating memory for MA57: lfact (345657)\n", - " 19 -5.1853848e+01 1.70e-02 1.12e-06 -8.6 2.40e+00 -9.2 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -5.1853848e+01 1.75e-01 5.67e-06 -8.6 6.21e+00 -9.7 1.00e+00 1.00e+00h 1\n", - " 21 -5.1853853e+01 1.96e+01 1.30e-03 -8.6 4.77e+01 -10.2 1.00e+00 1.00e+00h 1\n", - " 22 -5.1853862e+01 1.44e+01 8.05e-04 -8.6 1.50e+02 -9.8 1.00e+00 4.12e-01h 1\n", - " 23 -5.1853873e+01 1.77e+01 9.19e-04 -8.6 4.63e+03 -10.3 6.40e-02 1.19e-02h 1\n", - " 24 -5.1853883e+01 1.46e+01 4.64e-02 -8.6 1.25e+02 -10.7 1.00e+00 2.45e-01h 1\n", - " 25 -5.1853894e+01 1.55e+00 9.50e-01 -8.6 3.86e+01 -10.3 6.08e-02 1.00e+00h 1\n", - " 26 -5.1853897e+01 3.12e-01 2.79e-01 -8.6 5.18e+01 -10.8 1.00e+00 7.06e-01H 1\n", - " 27 -5.1853898e+01 3.16e-01 5.81e-05 -8.6 1.47e+01 -10.4 1.00e+00 1.00e+00f 1\n", - " 28 -5.1853901e+01 2.65e+00 3.38e-04 -8.6 3.87e+01 -10.8 1.00e+00 1.00e+00h 1\n", - " 29 -5.1853901e+01 1.04e+00 9.14e-05 -8.6 1.20e+02 -11.3 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -5.1853901e+01 1.15e-02 1.39e-06 -8.6 1.55e+00 - 1.00e+00 1.00e+00h 1\n", - " 31 -5.1853901e+01 4.45e-06 6.79e-10 -8.6 1.25e-01 - 1.00e+00 1.00e+00h 1\n", - " 32 -5.1853901e+01 3.73e-09 2.52e-14 -8.6 2.14e-05 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 32\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -5.1853901170234600e+01 -5.1853901170234600e+01\n", - "Dual infeasibility......: 2.5166233628543340e-14 2.5166233628543340e-14\n", - "Constraint violation....: 4.6566128730773926e-10 3.7252902984619141e-09\n", - "Complementarity.........: 2.5059035596808703e-09 2.5059035596808703e-09\n", - "Overall NLP error.......: 2.5059035596808703e-09 3.7252902984619141e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 44\n", - "Number of objective gradient evaluations = 33\n", - "Number of equality constraint evaluations = 44\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 33\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 32\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.946\n", - "Total CPU secs in NLP function evaluations = 0.023\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.8\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.92e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.89e+01 3.85e+02 -1.0 3.92e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.52e+01 4.30e+01 -1.0 4.82e+01 - 4.96e-01 1.00e+00h 1\n", - " 3 0.0000000e+00 4.26e-04 3.75e+02 -1.0 2.80e+01 - 2.55e-01 1.00e+00h 1\n", - " 4 0.0000000e+00 2.52e-13 3.50e+00 -1.0 1.87e-04 - 9.91e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.0465496681718326e-13 2.5224267119483557e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.0465496681718326e-13 2.5224267119483557e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.047\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.8\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 4.26e+07 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 4.26e+05 3.85e+02 -1.0 4.26e+07 - 2.54e-03 9.90e-01h 1\n", - " 2 0.0000000e+00 4.35e+03 5.99e+01 -1.0 4.26e+05 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.63e+01 2.06e+00 -1.0 4.24e+03 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 4.52e+01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393567)\n", - " 5 0.0000000e+00 9.31e-10 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.3132257461547852e-10 9.3132257461547852e-10\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.3132257461547852e-10 9.3132257461547852e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.113\n", - "Total CPU secs in NLP function evaluations = 0.002\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -5.4626425e+01 8.42e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303140)\n", - " 1 -5.4626450e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", - " 2 -5.4626470e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", - " 3 -5.4626469e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -5.4626461e+01 5.57e+00 7.49e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -5.4626462e+01 7.15e+00 3.09e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -5.4626463e+01 1.06e-02 4.11e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -5.4626463e+01 1.33e-06 1.50e-05 -3.8 2.12e-02 - 1.00e+00 1.00e+00h 1\n", - " 8 -5.4626463e+01 7.45e-09 3.01e-08 -5.7 3.01e-04 -4.0 1.00e+00 1.00e+00h 1\n", - " 9 -5.4626463e+01 7.31e-04 4.97e-06 -8.6 8.12e-01 - 9.96e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (323556)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -5.4626463e+01 7.45e-09 3.51e-08 -8.6 1.05e-03 -4.5 1.00e+00 1.00e+00h 1\n", - " 11 -5.4626463e+01 7.45e-09 3.93e-08 -8.6 3.53e-03 -5.0 1.00e+00 1.00e+00h 1\n", - " 12 -5.4626463e+01 3.73e-08 3.93e-08 -8.6 1.06e-02 -5.4 1.00e+00 1.00e+00h 1\n", - " 13 -5.4626463e+01 3.20e-07 3.92e-08 -8.6 3.18e-02 -5.9 1.00e+00 1.00e+00h 1\n", - " 14 -5.4626463e+01 2.87e-06 3.92e-08 -8.6 9.52e-02 -6.4 1.00e+00 1.00e+00h 1\n", - " 15 -5.4626463e+01 2.56e-05 3.90e-08 -8.6 2.84e-01 -6.9 1.00e+00 1.00e+00h 1\n", - " 16 -5.4626463e+01 2.24e-04 3.85e-08 -8.6 8.42e-01 -7.3 1.00e+00 1.00e+00h 1\n", - " 17 -5.4626463e+01 1.80e-03 3.64e-08 -8.6 2.39e+00 -7.8 1.00e+00 1.00e+00h 1\n", - " 18 -5.4626463e+01 1.75e-03 4.42e-07 -8.6 4.44e+00 -8.3 1.00e+00 2.55e-01h 2\n", - " 19 -5.4626463e+01 1.22e-03 7.37e-07 -8.6 1.65e+00 -8.8 1.00e+00 4.26e-01h 2\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -5.4626463e+01 3.93e-03 2.60e-07 -8.6 1.20e+00 -9.2 1.00e+00 1.00e+00h 1\n", - " 21 -5.4626463e+01 3.61e-02 5.90e-07 -8.6 3.14e+00 -9.7 1.00e+00 1.00e+00h 1\n", - " 22 -5.4626464e+01 4.44e-01 7.03e-06 -8.6 8.61e+00 -10.2 1.00e+00 1.00e+00h 1\n", - " 23 -5.4626464e+01 6.25e-02 9.33e-07 -8.6 2.01e+00 -9.8 1.00e+00 1.00e+00h 1\n", - " 24 -5.4626465e+01 1.08e+00 1.99e-05 -8.6 9.96e+00 -10.3 1.00e+00 1.00e+00h 1\n", - " 25 -5.4626466e+01 2.09e-01 4.63e-06 -8.6 8.06e+00 -9.8 1.00e+00 1.00e+00h 1\n", - " 26 -5.4626469e+01 6.30e+00 2.33e-04 -8.6 3.45e+01 -10.3 1.00e+00 1.00e+00h 1\n", - " 27 -5.4626473e+01 1.97e+00 5.73e-05 -8.6 4.80e+01 -9.9 1.00e+00 1.00e+00h 1\n", - " 28 -5.4626482e+01 8.48e+00 4.36e-04 -8.6 1.83e+02 -10.4 1.00e+00 4.10e-01h 1\n", - " 29 -5.4626483e+01 7.19e+00 3.68e-04 -8.6 3.53e+01 -10.8 1.00e+00 1.56e-01f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -5.4626486e+01 2.22e-01 2.33e-05 -8.6 6.24e+00 -10.4 5.58e-01 1.00e+00f 1\n", - " 31 -5.4626486e+01 1.20e-01 1.81e-05 -8.6 1.55e+01 -10.9 1.00e+00 1.00e+00h 1\n", - " 32 -5.4626486e+01 1.29e-02 4.92e-07 -8.6 3.51e+00 -10.5 1.00e+00 1.00e+00h 1\n", - " 33 -5.4626487e+01 2.08e-01 3.17e-05 -8.6 1.49e+01 -10.9 1.00e+00 1.00e+00h 1\n", - " 34 -5.4626487e+01 8.32e-02 7.55e-06 -8.6 7.39e+00 -10.5 1.00e+00 1.00e+00h 1\n", - " 35 -5.4626488e+01 2.61e+00 2.60e-04 -8.6 4.11e+01 -11.0 1.00e+00 1.00e+00h 1\n", - " 36 -5.4626489e+01 2.50e+00 5.32e-05 -8.6 7.17e+01 -11.5 1.00e+00 1.00e+00h 1\n", - " 37 -5.4626489e+01 1.89e+00 4.09e-05 -8.6 2.88e+02 -11.9 1.00e+00 2.43e-01h 1\n", - " 38 -5.4626489e+01 1.35e-02 1.91e-06 -8.6 1.74e+01 - 1.00e+00 1.00e+00h 1\n", - " 39 -5.4626489e+01 2.25e-03 9.41e-08 -8.6 6.88e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 40 -5.4626489e+01 1.34e-06 5.72e-11 -8.6 1.71e-01 - 1.00e+00 1.00e+00h 1\n", - " 41 -5.4626489e+01 1.49e-08 2.51e-14 -8.6 1.61e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 41\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -5.4626489018506717e+01 -5.4626489018506717e+01\n", - "Dual infeasibility......: 2.5104508813183050e-14 2.5104508813183050e-14\n", - "Constraint violation....: 9.3132257461547852e-10 1.4901161193847656e-08\n", - "Complementarity.........: 2.5059035596801395e-09 2.5059035596801395e-09\n", - "Overall NLP error.......: 2.5059035596801395e-09 1.4901161193847656e-08\n", - "\n", - "\n", - "Number of objective function evaluations = 48\n", - "Number of objective gradient evaluations = 42\n", - "Number of equality constraint evaluations = 48\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 42\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 41\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.229\n", - "Total CPU secs in NLP function evaluations = 0.069\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.5\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.86e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.85e+01 3.85e+02 -1.0 3.86e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.41e+01 4.32e+01 -1.0 4.70e+01 - 4.96e-01 1.00e+00h 1\n", - " 3 0.0000000e+00 4.19e-04 3.70e+02 -1.0 2.74e+01 - 2.54e-01 1.00e+00h 1\n", - " 4 0.0000000e+00 3.41e-13 3.45e+00 -1.0 1.89e-04 - 9.91e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.4150530724576891e-13 3.4106051316484809e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.4150530724576891e-13 3.4106051316484809e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.047\n", - "Total CPU secs in NLP function evaluations = 0.002\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.0\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 8.51e+07 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 8.52e+05 3.85e+02 -1.0 8.51e+07 - 2.54e-03 9.90e-01h 1\n", - " 2 0.0000000e+00 8.61e+03 5.99e+01 -1.0 8.52e+05 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 8.83e+01 2.06e+00 -1.0 8.49e+03 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 8.73e+01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (394288)\n", - " 5 0.0000000e+00 4.66e-10 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 4.6566128730773926e-10 4.6566128730773926e-10\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 4.6566128730773926e-10 4.6566128730773926e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.090\n", - "Total CPU secs in NLP function evaluations = 0.015\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -5.7399046e+01 8.42e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303434)\n", - " 1 -5.7399058e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", - " 2 -5.7399068e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", - " 3 -5.7399068e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -5.7399064e+01 5.57e+00 7.49e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -5.7399064e+01 7.15e+00 3.09e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -5.7399065e+01 1.06e-02 4.11e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -5.7399065e+01 1.33e-06 1.50e-05 -3.8 2.13e-02 - 1.00e+00 1.00e+00h 1\n", - " 8 -5.7399065e+01 1.49e-08 1.51e-08 -5.7 1.51e-04 -4.0 1.00e+00 1.00e+00h 1\n", - " 9 -5.7399065e+01 2.98e-08 1.94e-08 -8.6 5.83e-04 -4.5 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -5.7399065e+01 1.49e-08 1.97e-08 -9.0 1.77e-03 -5.0 1.00e+00 1.00e+00h 1\n", - " 11 -5.7399065e+01 2.98e-08 1.97e-08 -9.0 5.31e-03 -5.4 1.00e+00 1.00e+00h 1\n", - " 12 -5.7399065e+01 8.94e-08 1.97e-08 -9.0 1.59e-02 -5.9 1.00e+00 1.00e+00h 1\n", - " 13 -5.7399065e+01 7.15e-07 1.96e-08 -9.0 4.77e-02 -6.4 1.00e+00 1.00e+00h 1\n", - " 14 -5.7399065e+01 6.48e-06 1.96e-08 -9.0 1.43e-01 -6.9 1.00e+00 1.00e+00h 1\n", - " 15 -5.7399065e+01 5.78e-05 1.95e-08 -9.0 4.27e-01 -7.3 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (333906)\n", - " 16 -5.7399065e+01 5.04e-04 1.92e-08 -9.0 1.26e+00 -7.8 1.00e+00 1.00e+00h 1\n", - " 17 -5.7399065e+01 3.89e-03 1.78e-08 -9.0 3.51e+00 -8.3 1.00e+00 1.00e+00h 1\n", - " 18 -5.7399065e+01 3.09e-03 5.12e-07 -9.0 1.50e+00 -8.8 1.00e+00 2.16e-01h 2\n", - " 19 -5.7399065e+01 9.36e-04 9.20e-08 -9.0 5.07e-01 -9.2 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -5.7399065e+01 8.67e-03 7.11e-08 -9.0 1.60e+00 -9.7 1.00e+00 1.00e+00h 1\n", - " 21 -5.7399065e+01 8.90e-02 7.27e-07 -9.0 4.66e+00 -10.2 1.00e+00 1.00e+00h 1\n", - " 22 -5.7399065e+01 1.83e+00 1.34e-05 -9.0 1.12e+01 -10.7 1.00e+00 1.00e+00h 1\n", - " 23 -5.7399066e+01 3.81e-01 3.61e-06 -9.0 1.00e+01 -10.3 1.00e+00 1.00e+00h 1\n", - " 24 -5.7399069e+01 8.09e+00 2.59e-04 -9.0 7.69e+01 -10.7 9.52e-01 6.16e-01H 1\n", - " 25 -5.7399072e+01 3.81e+00 1.19e-04 -9.0 5.43e+01 -10.3 1.00e+00 7.55e-01f 1\n", - " 26 -5.7399075e+01 4.33e+00 1.32e-04 -9.0 2.22e+02 -10.8 1.00e+00 1.41e-01f 1\n", - " 27 -5.7399076e+01 1.97e+00 3.29e-05 -9.0 2.73e+01 -11.3 8.89e-01 7.54e-01f 1\n", - " 28 -5.7399077e+01 1.75e-01 1.41e-06 -9.0 1.54e+01 -10.8 1.00e+00 1.00e+00f 1\n", - " 29 -5.7399077e+01 9.22e-01 7.02e-05 -9.0 3.11e+01 -11.3 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -5.7399077e+01 4.41e-01 1.26e-05 -9.0 1.65e+01 -10.9 1.00e+00 1.00e+00h 1\n", - " 31 -5.7399078e+01 2.23e+00 7.37e-05 -9.0 3.40e+01 -11.4 1.00e+00 1.00e+00h 1\n", - " 32 -5.7399078e+01 6.05e-01 8.47e-06 -9.0 8.14e+01 -11.8 1.00e+00 1.00e+00h 1\n", - " 33 -5.7399078e+01 5.73e-01 8.79e-06 -9.0 3.08e+02 -12.3 1.00e+00 1.64e-01h 1\n", - " 34 -5.7399078e+01 1.59e-02 1.89e-07 -9.0 1.07e+01 - 1.00e+00 1.00e+00h 1\n", - " 35 -5.7399078e+01 4.00e-04 1.26e-08 -9.0 2.96e+00 - 1.00e+00 1.00e+00h 1\n", - " 36 -5.7399078e+01 4.81e-08 1.16e-12 -9.0 3.27e-02 - 1.00e+00 1.00e+00h 1\n", - " 37 -5.7399078e+01 1.49e-08 9.10e-15 -9.0 5.66e-06 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 37\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -5.7399077980658021e+01 -5.7399077980658021e+01\n", - "Dual infeasibility......: 9.1002942970123576e-15 9.1002942970123576e-15\n", - "Constraint violation....: 1.8626451492309570e-09 1.4901161193847656e-08\n", - "Complementarity.........: 9.0909090909093753e-10 9.0909090909093753e-10\n", - "Overall NLP error.......: 1.8626451492309570e-09 1.4901161193847656e-08\n", - "\n", - "\n", - "Number of objective function evaluations = 40\n", - "Number of objective gradient evaluations = 38\n", - "Number of equality constraint evaluations = 40\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 38\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 37\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.064\n", - "Total CPU secs in NLP function evaluations = 0.031\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.8\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.89e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.87e+01 3.85e+02 -1.0 3.89e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.47e+01 4.31e+01 -1.0 4.76e+01 - 4.96e-01 1.00e+00h 1\n", - " 3 0.0000000e+00 4.23e-04 3.73e+02 -1.0 2.77e+01 - 2.54e-01 1.00e+00h 1\n", - " 4 0.0000000e+00 2.79e-13 3.48e+00 -1.0 1.88e-04 - 9.91e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.2337636129035084e-13 2.7888802378583932e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.2337636129035084e-13 2.7888802378583932e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.063\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.8\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.70e+08 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 1.70e+06 3.85e+02 -1.0 1.70e+08 - 2.54e-03 9.90e-01h 1\n", - " 2 0.0000000e+00 1.71e+04 5.99e+01 -1.0 1.70e+06 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.72e+02 2.06e+00 -1.0 1.70e+04 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 1.71e+02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393530)\n", - " 5 0.0000000e+00 4.55e-13 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.3691704763652764e-13 4.5474735088646412e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.3691704763652764e-13 4.5474735088646412e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.115\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -6.0171651e+01 8.42e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303252)\n", - " 1 -6.0171657e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", - " 2 -6.0171662e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", - " 3 -6.0171662e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -6.0171660e+01 5.57e+00 7.49e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -6.0171660e+01 7.15e+00 3.09e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -6.0171660e+01 1.06e-02 4.11e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -6.0171660e+01 1.34e-06 1.50e-05 -3.8 2.13e-02 - 1.00e+00 1.00e+00h 1\n", - " 8 -6.0171660e+01 2.98e-08 2.02e-08 -5.7 1.92e-03 - 1.00e+00 1.00e+00h 1\n", - " 9 -6.0171660e+01 4.53e-05 2.16e-08 -8.6 2.02e-01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (326464)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -6.0171660e+01 2.98e-08 9.06e-09 -8.6 9.06e-05 -4.0 1.00e+00 1.00e+00h 1\n", - " 11 -6.0171660e+01 3.73e-09 9.82e-09 -8.6 2.94e-04 -4.5 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 11\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -6.0171659998267266e+01 -6.0171659998267266e+01\n", - "Dual infeasibility......: 9.8158985796691289e-09 9.8158985796691289e-09\n", - "Constraint violation....: 3.7252902984619141e-09 3.7252902984619141e-09\n", - "Complementarity.........: 2.5059035655454891e-09 2.5059035655454891e-09\n", - "Overall NLP error.......: 9.8158985796691289e-09 9.8158985796691289e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 12\n", - "Number of objective gradient evaluations = 12\n", - "Number of equality constraint evaluations = 12\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 12\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 11\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.265\n", - "Total CPU secs in NLP function evaluations = 0.001\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.1\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.92e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.55e+01 3.85e+02 -1.0 1.92e+02 - 2.55e-03 9.95e-01f 1\n", - " 2 0.0000000e+00 1.07e+01 4.90e+01 -1.0 1.71e+01 - 2.60e-01 1.00e+00h 1\n", - " 3 0.0000000e+00 4.19e-05 2.46e+01 -1.0 7.37e+00 - 5.90e-01 1.00e+00h 1\n", - " 4 0.0000000e+00 2.27e-13 1.48e-01 -1.0 3.16e-05 - 9.91e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.1202303897534080e-13 2.2737367544323206e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.1202303897534080e-13 2.2737367544323206e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.057\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.9\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.40e+08 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 3.40e+06 3.85e+02 -1.0 3.40e+08 - 2.54e-03 9.90e-01h 1\n", - " 2 0.0000000e+00 3.41e+04 5.99e+01 -1.0 3.41e+06 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 3.40e+02 2.06e+00 -1.0 3.40e+04 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.10e-04 2.39e-04 -1.0 3.39e+02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393908)\n", - " 5 0.0000000e+00 1.49e-08 1.50e-09 -3.8 3.14e-04 - 1.00e+00 1.00e+00h 1\n", - " 6 0.0000000e+00 4.55e-13 1.84e-11 -5.7 1.49e-08 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 6\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.8851038199681022e-14 4.5474735088646412e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.8851038199681022e-14 4.5474735088646412e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 7\n", - "Number of objective gradient evaluations = 7\n", - "Number of equality constraint evaluations = 7\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 7\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 6\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.120\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -6.2944244e+01 8.42e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303034)\n", - " 1 -6.2944247e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", - " 2 -6.2944250e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", - " 3 -6.2944249e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -6.2944249e+01 5.57e+00 7.49e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -6.2944249e+01 7.15e+00 3.09e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -6.2944249e+01 1.06e-02 4.11e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -6.2944249e+01 1.31e-06 1.50e-05 -3.8 2.13e-02 - 1.00e+00 1.00e+00h 1\n", - " 8 -6.2944249e+01 5.96e-08 1.04e-08 -5.7 6.73e-04 - 1.00e+00 1.00e+00h 1\n", - " 9 -6.2944249e+01 1.13e-05 1.08e-08 -8.6 1.01e-01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (326761)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -6.2944249e+01 5.96e-08 4.72e-09 -8.6 4.72e-05 -4.0 1.00e+00 1.00e+00h 1\n", - " 11 -6.2944249e+01 5.96e-08 4.91e-09 -9.0 1.47e-04 -4.5 1.00e+00 1.00e+00h 1\n", - " 12 -6.2944249e+01 1.49e-08 4.91e-09 -9.0 4.42e-04 -5.0 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 12\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -6.2944248720059186e+01 -6.2944248720059186e+01\n", - "Dual infeasibility......: 4.9123733168040214e-09 4.9123733168040214e-09\n", - "Constraint violation....: 7.4505805969238281e-09 1.4901161193847656e-08\n", - "Complementarity.........: 9.0909090909090920e-10 9.0909090909090920e-10\n", - "Overall NLP error.......: 7.4505805969238281e-09 1.4901161193847656e-08\n", - "\n", - "\n", - "Number of objective function evaluations = 13\n", - "Number of objective gradient evaluations = 13\n", - "Number of equality constraint evaluations = 13\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 13\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 12\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.286\n", - "Total CPU secs in NLP function evaluations = 0.017\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.3\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.92e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.55e+01 3.85e+02 -1.0 1.92e+02 - 2.55e-03 9.95e-01f 1\n", - " 2 0.0000000e+00 1.07e+01 4.90e+01 -1.0 1.71e+01 - 2.60e-01 1.00e+00h 1\n", - " 3 0.0000000e+00 4.17e-05 2.47e+01 -1.0 7.37e+00 - 5.89e-01 1.00e+00h 1\n", - " 4 0.0000000e+00 3.41e-13 1.49e-01 -1.0 3.15e-05 - 9.91e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.6092568863958805e-13 3.4106051316484809e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.6092568863958805e-13 3.4106051316484809e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.064\n", - "Total CPU secs in NLP function evaluations = 0.003\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.1\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.81e+08 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (295725)\n", - " 1 0.0000000e+00 6.81e+06 3.85e+02 -1.0 6.81e+08 - 2.54e-03 9.90e-01h 1\n", - " 2 0.0000000e+00 6.82e+04 5.99e+01 -1.0 6.81e+06 - 1.29e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 6.77e+02 2.06e+00 -1.0 6.81e+04 - 9.84e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 3.11e-04 2.39e-04 -1.0 6.76e+02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393607)\n", - " 5 0.0000000e+00 2.98e-08 1.50e-09 -3.8 3.15e-04 - 1.00e+00 1.00e+00h 1\n", - " 6 0.0000000e+00 4.55e-13 1.84e-11 -5.7 2.98e-08 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 6\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.7123678492091068e-14 4.5474735088646412e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.7123678492091068e-14 4.5474735088646412e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 7\n", - "Number of objective gradient evaluations = 7\n", - "Number of equality constraint evaluations = 7\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 7\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 6\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.117\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -6.5716835e+01 8.42e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303450)\n", - " 1 -6.5716837e+01 4.18e+02 5.15e+00 -1.0 2.52e+01 - 9.88e-01 5.04e-01h 1\n", - " 2 -6.5716838e+01 3.73e-01 1.66e-01 -1.0 2.07e+01 - 9.82e-01 1.00e+00f 1\n", - " 3 -6.5716838e+01 2.27e-01 8.50e-02 -1.7 2.61e+01 - 1.00e+00 1.00e+00f 1\n", - " 4 -6.5716837e+01 5.57e+00 7.49e-02 -1.7 1.56e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -6.5716837e+01 7.15e+00 3.09e-03 -1.7 1.09e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -6.5716837e+01 1.06e-02 4.11e-04 -1.7 9.33e+00 - 1.00e+00 1.00e+00h 1\n", - " 7 -6.5716837e+01 1.43e-06 1.50e-05 -3.8 2.13e-02 - 1.00e+00 1.00e+00h 1\n", - " 8 -6.5716837e+01 1.19e-07 6.81e-09 -5.7 2.30e-04 - 1.00e+00 1.00e+00h 1\n", - " 9 -6.5716837e+01 2.74e-06 5.39e-09 -8.6 5.05e-02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (321428)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -6.5716837e+01 1.19e-07 2.41e-09 -8.6 2.41e-05 -4.0 1.00e+00 1.00e+00h 1\n", - " 11 -6.5716837e+01 1.19e-07 2.45e-09 -8.6 7.36e-05 -4.5 1.00e+00 1.00e+00h 1\n", - " 12 -6.5716837e+01 1.19e-07 2.45e-09 -8.6 2.21e-04 -5.0 1.00e+00 1.00e+00h 1\n", - " 13 -6.5716837e+01 1.49e-08 2.45e-09 -8.6 6.62e-04 -5.4 1.00e+00 1.00e+00h 1\n", - " 14 -6.5716837e+01 1.19e-07 2.45e-09 -9.0 1.99e-03 -5.9 1.00e+00 1.00e+00h 1\n", - " 15 -6.5716837e+01 1.19e-07 2.45e-09 -9.0 5.96e-03 -6.4 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 15\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -6.5716837442211869e+01 -6.5716837442211869e+01\n", - "Dual infeasibility......: 2.4545255939761519e-09 2.4545255939761519e-09\n", - "Constraint violation....: 2.4652589872487225e-10 1.1920928955078125e-07\n", - "Complementarity.........: 9.0909090909090920e-10 9.0909090909090920e-10\n", - "Overall NLP error.......: 2.4545255939761519e-09 1.1920928955078125e-07\n", - "\n", - "\n", - "Number of objective function evaluations = 16\n", - "Number of objective gradient evaluations = 16\n", - "Number of equality constraint evaluations = 16\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 16\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 15\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.401\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.1\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.92e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 1.55e+01 3.85e+02 -1.0 1.92e+02 - 2.55e-03 9.95e-01f 1\n", - " 2 0.0000000e+00 1.07e+01 4.90e+01 -1.0 1.71e+01 - 2.60e-01 1.00e+00h 1\n", - " 3 0.0000000e+00 4.18e-05 2.47e+01 -1.0 7.37e+00 - 5.89e-01 1.00e+00h 1\n", - " 4 0.0000000e+00 2.57e-13 1.49e-01 -1.0 3.16e-05 - 9.91e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.0672779846629121e-13 2.5723867480564877e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.0672779846629121e-13 2.5723867480564877e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.067\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.9\n" - ] - } - ], - "source": [ - "n_para = len(parameter_dict)\n", - "\n", - "t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]\n", - "parameter_dict = {\"A1\": 85, \"A2\": 370, \"E1\": 8, \"E2\": 15}\n", - " \n", - "measurements = MeasurementVariables()\n", - "measurements.add_variables(\"C\", indices={0: [\"CA\", \"CB\", \"CC\"], 1: t_control}, time_index_position=1)\n", - " \n", - "exp_design = DesignVariables()\n", - "exp_design.add_variables(\n", - " \"CA0\",\n", - " indices={0: [0]},\n", - " time_index_position=0,\n", - " lower_bounds=1,\n", - " upper_bounds=5,\n", - " )\n", - "exp_design.add_variables(\n", - " \"T\",\n", - " indices={0: t_control},\n", - " time_index_position=0,\n", - " lower_bounds=300,\n", - " upper_bounds=700,\n", - " )\n", - "\n", - "exp_design.update_values({\"CA0[0]\": 5, \"T[0]\": 450, \"T[0.125]\": 300, \"T[0.25]\": 300, \"T[0.375]\": 300, \"T[0.5]\": 300, \"T[0.625]\": 300,\n", - " \"T[0.75]\": 300, \"T[0.875]\": 300, \"T[1]\": 300})\n", - " \n", - "doe_object = DesignOfExperiments(\n", - " parameter_dict,\n", - " exp_design, \n", - " measurements, \n", - " create_model,\n", - " discretize_model=disc_for_measure,\n", - " )\n", - "\n", - "result = doe_object.compute_FIM(\n", - " mode=\"sequential_finite\",\n", - " scale_nominal_param_value=True,\n", - " formula=\"central\",\n", - " )\n", - "\n", - "result.result_analysis()\n", - "\n", - "FIM_prior = result.FIM\n", - "FIM_new = np.zeros((n_para, n_para))\n", - "A_vals = []\n", - "D_vals = []\n", - "exp_conds = []\n", - "FIM_opt = None\n", - "\n", - "def get_exp_conds(m):\n", - " return [pyo.value(m.CA0[0]), pyo.value(m.T[0]), pyo.value(m.T[0.125]), pyo.value(m.T[0.25]), pyo.value(m.T[0.375]), pyo.value(m.T[0.5]), pyo.value(m.T[0.625]), pyo.value(m.T[0.75]), pyo.value(m.T[0.875]), pyo.value(m.T[1])]\n", - "\n", - "for i in range(20):\n", - " FIM_prior += FIM_new\n", - " \n", - " doe_object = DesignOfExperiments(\n", - " parameter_dict,\n", - " exp_design, \n", - " measurements, \n", - " create_model,\n", - " prior_FIM=FIM_prior,\n", - " discretize_model=disc_for_measure,\n", - " )\n", - " \n", - " square_result, optimize_result = doe_object.stochastic_program(\n", - " if_optimize=True,\n", - " if_Cholesky=True,\n", - " scale_nominal_param_value=True,\n", - " objective_option=\"det\",\n", - " L_initial=np.linalg.cholesky(FIM_prior),\n", - " )\n", - " FIM_new = optimize_result.FIM\n", - " \n", - " new_exp_conds = get_exp_conds(optimize_result.model)\n", - " result = new_doe_object2(new_exp_conds[0], new_exp_conds[1:], FIM_new)\n", - "\n", - " if FIM_opt is None:\n", - " FIM_opt = [result.FIM, ]\n", - " else:\n", - " FIM_opt.append(result.FIM)\n", - " \n", - " D_opt = np.linalg.det(FIM_new)\n", - " # A_vals.append(np.log10(A_opt))\n", - " D_vals.append(np.log10(D_opt))\n", - " exp_conds.append(new_exp_conds)" - ] - }, - { - "cell_type": "code", - "execution_count": 109, - "id": "8d7175ef-92b3-44f4-8bbb-5af12527e668", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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d3V1xdvbu8ggIiIi1VeF3F595MiRPP3009x+++107tyZjz76iEOHDjFkyJCK+DoRERGxIRUSPh5//HFSUlJ49913OX78OK1atWLJkiUEBQVVxNeJiIiIDamQ+3zcCN3nQ0RExPaU5fe7yj3V9mIW0iW3IiIituPi7/a19DSqXPhIT08H0CW3IiIiNig9PR0fH58rLlPlDrsUFBRw7NgxvLy8SrwvyI1IS0ujYcOGHD58uNof0rGnbQX72l5ta/VlT9urba1+DMMgPT2dwMBAHByufDFtlet8ODg40KBBgwr9Dnu6pNeethXsa3u1rdWXPW2vtrV6uVrH46IKu8OpiIiISEkUPkRERKRS2VX4cHV15e2338bV1dXapVQ4e9pWsK/t1bZWX/a0vdpW+1blTjgVERGR6s2uOh8iIiJifQofIiIiUqkUPkRERKRSKXyIiIhIpap24SMqKorg4GDc3Nxo3749q1evvuLysbGxtG/fHjc3N26++WZiYmIqqdLrN378eDp06ICXlxe+vr48/PDDJCYmXvEzK1euxGQyFRsSEhIqqerrN3bs2GJ1+/v7X/EztrhfAW666aYS91NERESJy9vSfl21ahV9+vQhMDAQk8nEokWLisw3DIOxY8cSGBiIu7s7d911Fzt27Ljqer///ntatGiBq6srLVq0YOHChRW0BWVzpe3Nzc3l1VdfpXXr1nh6ehIYGMgzzzzDsWPHrrjOTz/9tMT9ff78+Qremiu72r4dNGhQsZpDQ0Ovut6quG+vtq0l7R+TycTkyZNLXWdV3a8VqVqFj/nz5zN8+HDeeOMN4uPj6dq1K7179+bQoUMlLn/gwAHuv/9+unbtSnx8PK+//jovvfQS33//fSVXXjaxsbFERESwfv16li5dSl5eHj179iQjI+Oqn01MTOT48eOWoUmTJpVQ8Y1r2bJlkbq3bdtW6rK2ul8BNm3aVGQ7ly5dCsDf//73K37OFvZrRkYGbdq0YdasWSXOnzRpEtOmTWPWrFls2rQJf39/7r33Xsvznkqybt06Hn/8cZ5++mn+/PNPnn76afr168eGDRsqajOu2ZW2NzMzkz/++IO33nqLP/74gwULFrB7924efPDBq67X29u7yL4+fvw4bm5uFbEJ1+xq+xagV69eRWpesmTJFddZVfft1bb18n3zySefYDKZePTRR6+43qq4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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "D_vals_opt_from_ind_FIM = []\n", - "running_FIM = np.zeros((n_para, n_para))\n", - "for i in range(20):\n", - " running_FIM += FIM_opt[i]\n", - " D_vals_opt_from_ind_FIM.append(np.log10(np.linalg.det(running_FIM)))\n", - "\n", - "plt.plot(range(20), D_vals, color='black')\n", - "plt.plot(range(20), D_vals_opt_from_ind_FIM, color='green')\n", - "plt.plot(range(20), D_vals_rand_from_ind_FIM, color='orange')\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 104, - "id": "2e53d948-0c13-41be-b417-3a771e09b3c4", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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k9i9e/eovSfDKSkpwde//nXpjnddXR0aGxtH3W/Zj+1vf/tbfPDBB3n9hmU9rrkoqgDF6/Vi1qxZetWDZvfu3Zg3b96wfzN37twLtn/jjTcwe/ZseDwe0/a1UKqq4q//+q/xyiuv4M0338S0adPyep4jR46grq7O4L0zXzQaxfvvvz/ivst6XDM9++yzmDJlCm644Yac/1bG4zpt2jQEg8Gs4xaLxbBv374Rf7/AyMd6tL8RhRacHD9+HHv27MkreFZVFe+++650x/v8+fM4c+bMqPst87EFBnpAZ82aha985Ss5/62sxzUndmXn2mXHjh2qx+NRt2zZor733ntqc3OzWl5erp46dUpVVVX94Q9/qN5111369idOnFDLysrU73//++p7772nbtmyRfV4POr/+T//x663MC7/43/8DzUQCKh79+5V29ra9Ftvb6++zdD3umnTJnXnzp3qhx9+qB49elT94Q9/qAJQX375ZTveQk7WrFmj7t27Vz1x4oR68OBBdcmSJWpFRYXjjqsmmUyqn//859Uf/OAHFzwm83Ht6upSjxw5oh45ckQFoD7++OPqkSNH9KqVRx55RA0EAuorr7yitra2qt/5znfUuro6NRKJ6M9x1113ZVXl/du//ZvqcrnURx55RH3//ffVRx55RHW73erBgwctf39DjfZ+4/G4unTpUvWiiy5S33333azfcTQa1Z9j6PttaWlRX3/9dfW//uu/1CNHjqh/8Rd/obrdbvV3v/udHW9RN9p77erqUtesWaMeOHBAPXnypPrWW2+pc+fOVT/3uc9JeWzH+h6rqqqGw2G1rKxMfeqpp4Z9DlmOq5mKLkBRVVX9x3/8R7WxsVH1er3q1772tazS2xUrVqjz58/P2n7v3r3qzJkzVa/Xq06dOnXEL5RIAAx7e/bZZ/Vthr7XRx99VL344otVv9+vVldXq9/4xjfUV1991fqdz8Ott96q1tXVqR6PR62vr1eXLVumHjt2TH/cKcdVs2vXLhWA+sEHH1zwmMzHVSuJHnpbsWKFqqoDpcY/+tGP1GAwqPp8PvVP/uRP1NbW1qznmD9/vr695l/+5V/USy+9VPV4POqXvvQlYYKz0d7vyZMnR/wdv/XWW/pzDH2/zc3N6uc//3nV6/WqkydPVhctWqQeOHDA+jc3xGjvtbe3V120aJE6efJk1ePxqJ///OfVFStWqB999FHWc8hybMf6Hquqqv7sZz9TS0tL1c7OzmGfQ5bjaiZFVQczA4mIiIgEUVQ5KERERCQHBihEREQkHAYoREREJBwGKERERCQcBihEREQkHAYoREREJBwGKERERCQcBihEREQkHAYoREREJBwGKERERCQcBihEREQkHAYoREREJJz/H0t8KKeI27v/AAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "exp_conds_array = np.array(exp_conds)\n", - "plt.plot(range(20), exp_conds_array[:, 9])\n", - "plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 79, - "id": "a2243b52-d5fe-4a52-8280-fb0ce4436ed6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: =======Iteration Number: 1 =====\n", - "INFO: elapsed time: 2.2\n", - "INFO: This is run 1 out of 9.\n", - "INFO: The code has run 2.19064669997897 seconds.\n", - "INFO: Estimated remaining time: 7.667263449926395 seconds\n", - "INFO: =======Iteration Number: 2 =====\n", - "INFO: elapsed time: 1.0\n", - "INFO: This is run 2 out of 9.\n", - "INFO: The code has run 3.2144447999307886 seconds.\n", - "INFO: Estimated remaining time: 6.428889599861577 seconds\n", - "INFO: =======Iteration Number: 3 =====\n", - "INFO: elapsed time: 1.3\n", - "INFO: This is run 3 out of 9.\n", - "INFO: The code has run 4.496953599969856 seconds.\n", - "INFO: Estimated remaining time: 5.6211919999623206 seconds\n", - "INFO: =======Iteration Number: 4 =====\n", - "INFO: elapsed time: 1.0\n", - "INFO: This is run 4 out of 9.\n", - "INFO: The code has run 5.520735299913213 seconds.\n", - "INFO: Estimated remaining time: 4.4165882399305705 seconds\n", - "INFO: =======Iteration Number: 5 =====\n", - "INFO: elapsed time: 1.2\n", - "INFO: This is run 5 out of 9.\n", - "INFO: The code has run 6.751729399897158 seconds.\n", - "INFO: Estimated remaining time: 3.375864699948579 seconds\n", - "INFO: =======Iteration Number: 6 =====\n", - "INFO: elapsed time: 1.2\n", - "INFO: This is run 6 out of 9.\n", - "INFO: The code has run 7.984732399811037 seconds.\n", - "INFO: Estimated remaining time: 2.281352114231725 seconds\n", - "INFO: =======Iteration Number: 7 =====\n", - "INFO: elapsed time: 1.1\n", - "INFO: This is run 7 out of 9.\n", - "INFO: The code has run 9.039862399804406 seconds.\n", - "INFO: Estimated remaining time: 1.1299827999755507 seconds\n", - "INFO: =======Iteration Number: 8 =====\n", - "INFO: elapsed time: 2.8\n", - "INFO: This is run 8 out of 9.\n", - "INFO: The code has run 11.875191299826838 seconds.\n", - "INFO: Estimated remaining time: 0.0 seconds\n", - "INFO: =======Iteration Number: 9 =====\n", - "INFO: elapsed time: 0.9\n", - "INFO: This is run 9 out of 9.\n", - "INFO: The code has run 12.811318899854086 seconds.\n", - "INFO: Estimated remaining time: -1.2811318899854087 seconds\n", - "INFO: Overall wall clock time [s]: 12.811318899854086\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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o23btjh8+LDiCcMnT55g9+7dqFOnDnr37i1yhEREZWMSRVSF5eTkAACsrKx0aqd3796QSCSlRnyel5CQgEGDBsHGxgb29vYYPXo0Hj58qFO/6hg9ejSKi4uxfft2AMD+/fuRmZkJf3//V3L0jYheHUyiiKow+XogXYpo3r9/H0eOHAFQ9pNwubm58PPzw927d7F9+3b8+OOPOHnyJN544w2UlJRo3bc6/P39YWxsrEjw5P+VP71HRFRV8Z95RFVY/fr1AQB37tzRuo1t27ahpKQEvXv3xuHDh5GSkgIHBwelc1avXo379+/j5MmTcHR0BPCsKKa3tzd+/fVXvPXWW9p/iJdwcHBAr169EBkZiWPHjuHgwYPw8PBA+/btK6xPIiJ94EgUURUmL1dw8uRJFBUVadXG5s2b0bp1ayxZskRp2ux5+/fvh5+fnyKBAp5VC2/atCn27dunXfAakC8gHz16NJ4+fcoF5URkEJhEEVVhr7/+OqytrZGWloY9e/ZofP2VK1cQFxeHkSNHwsvLC82bN1c5pXf16lW0aNGi1P4WLVrg2rVrWsWuibfeegvW1tZISkpSlD4gIqrqmEQRVWE1atTABx98AACYPn16ue+kA569oFheFgF4NgolkUjw7rvvAni2zuj8+fOlEqOMjAzUqFGjVHs1a9ZEenq6bh9CDZaWlvj444/Rs2dPvPfee3BxcanwPomIdMUkiqiKCw0NRefOnZGamorOnTtj8+bNKCgoUDrnxo0bmDJlCnx9fZGWlgbgWdXvbdu2wcfHBw0aNAAAjBw5EhKJROVolEQiKbWvMt9PHhoair/++gurVq2qtD6JiHTBJIqoijMzM8OhQ4cwdOhQpKSkYMyYMahZsyZatWoFb29vNGjQAO7u7vj+++/h4OCAJk2aAACioqKQnJyMQYMGITMzE5mZmbC1tUXHjh2xdetWpQTJ3t4eGRkZpfrOyMhAzZo1K+2zEhEZEiZRRAbA2toae/bswbFjxzBu3Dg4OTkhISEBcXFxEAQBb7zxBtatW4cbN26gZcuWAP5XzuCjjz6Cvb29Yjt9+jQSExNx4sQJRfstWrTA1atXS/V79epVNGvWrHI+JBGRgWGJAyID0r17d3Tv3v2l5xUUFGDPnj3o168fZs+erXSssLAQAwcOxJYtWxRtvfnmm5g3b55S+YO///4b//zzD8LCwvT6GV62rutFDRo0qNRpRSIidUkE/u1E9MrZtWsXhg8fjv379+ONN94odXz48OH4888/kZKSAjMzM+Tk5KB169aoXbs2QkJCUFBQgNmzZ6NWrVo4deoUjIxePmidkJCAhg0bQiqVKmo8BQUFISgoSO+fTy4wMBDx8fHIysrC5cuX4ePjg6ioqArrj4joeZzOI3oFbdmyBQ4ODujXr5/K44GBgcjIyMCBAwcAPKuMfuTIETg4OGD48OEYN24cOnXqhP3796uVQD3vyZMniImJQUxMDJKSknT+LOW5cOECYmJicPny5Qrth4hIFY5EEREREWmBI1FEREREWmASRURERKQFJlFEREREWmASRURERKQFJlFEREREWmASRURERKQFJlFEREREWmASRURERKQFJlFEREREWmASRURERKQFJlFEREREWmASRURERKSF/wOJlcL9geJOrwAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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unTpolPdERER6NWrl8Z2a2trhIWFYd68efpdVCWqdrPDiIiISJ2bm5vawr0rV64ssez777+PuLg4bN68GbVq6f/w1jp16mD16tVISEhATk4Obt26hR07dsDJyQnz58/H+vXr9T53Zak23WFEREQ1j6HdYUUAoLUlSJu4uDi89957eOedd9C+fXsD6n3yoPGnu9Jq166NsWPHok2bNujQoQNCQkIwdepUmJlV3faWqhsZERFRjWecx2bY29urvUpKgiZOnAhvb+8Knc3VqlUrdO7cGXfv3lV7YHlVxJYgIiIiExEXFwcAsLKy0rq/a9euAID9+/dj6NChetejHBj96NEjvc9RGZgEERERScY43WG6eu2117Ruj4qKQlJSEoYMGYI6derA09NT74gKCwtx4cIFyGQyuLu7632eysAkiIiISDLKB6jqq7BcpTdu3Kh1+6RJk5CUlIRFixZpzA5LT09Heno6nJ2d1aa+K2eSPb0AcWFhIebNm4eUlBT0798fTk5O5YqvsjEJIiIiohKFh4cjLCwMISEhamOJxowZA5lMhm7dusHV1RUZGRmIiorC1atX4e7ujnXr1kkXtI6YBBEREUnG0O4w6b7GZ8yYgaNHjyIiIgLp6emwsLBA48aNsWTJErz99ttwdHSULDZdcbFEI+NiiaaFiyWaFi6WaBoqd7HEMNjbax+krNt5cqFQhFRorDUZp8gTERGRSWJ3GBERkWSqb3dYTcC7R0REJBkmQVLi3SMiIpKMoVPkzY0ViEnimCAiIiIySWwJIiIikgy7w6TEu0dERCQZJkFSYncYERERmSSmkERERJIxh2GDmzkw2hBMgoiIiCTD2WFSYncYERERmSS2BBEREUmGA6OlxLtHREQkGSZBUmJ3GBEREZkkppBERESSYUuQlHj3iIiIJMMkSEq8e0RERJLhFHkpcUwQERERmSS2BBEREUmG3WFS4t0jIiKSDJMgKbE7jIiIiEwSU0giIiLJsCVISrx7REREkmESJCV2hxEREZFJYgpJREQkGa4TJCUmQURERJJhd5iUePeIiIgkwyRIShwTRERERCaJKSQREZFk2BIkJd49IiIiyXBgtJTYHUZEREQmiS1BREREkjGHYa05bAkyBJMgIiIiyXBMkJTYHUZEREQmiSkkERGRZNgSJCXePSIiIskwCZISu8OIiIjIJDGFJCIi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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "### Define inputs\n", - "# Control time set [h]\n", - "t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]\n", - "# Define parameter nominal value\n", - "parameter_dict = {\"A1\": 85, \"A2\": 372, \"E1\": 8, \"E2\": 15}\n", - "\n", - "# measurement object\n", - "measurements = MeasurementVariables()\n", - "measurements.add_variables(\n", - " \"C\", # variable name\n", - " indices={0: [\"CA\", \"CB\", \"CC\"], 1: t_control}, # indices\n", - " time_index_position=1,\n", - ") # position of time index\n", - "\n", - "# design object\n", - "exp_design = DesignVariables()\n", - "\n", - "# add CAO as design variable\n", - "exp_design.add_variables(\n", - " \"CA0\", # variable name\n", - " indices={0: [0]}, # indices\n", - " time_index_position=0, # position of time index\n", - " values=[5], # nominal value\n", - " lower_bounds=1, # lower bound\n", - " upper_bounds=5, # upper bound\n", - ")\n", - "\n", - "# add T as design variable\n", - "exp_design.add_variables(\n", - " \"T\", # variable name\n", - " indices={0: t_control}, # indices\n", - " time_index_position=0, # position of time index\n", - " values=[470, 300, 300, 300, 300, 300, 300, 300, 300], # nominal value\n", - " lower_bounds=300, # lower bound\n", - " upper_bounds=700, # upper bound\n", - ")\n", - "\n", - "# For each variable, we define a list of possible values that are used\n", - "# in the sensitivity analysis\n", - "\n", - "design_ranges = {\n", - " \"CA0[0]\": [1, 3, 5],\n", - " (\n", - " \"T[0]\",\n", - " \"T[0.125]\",\n", - " \"T[0.25]\",\n", - " \"T[0.375]\",\n", - " \"T[0.5]\",\n", - " \"T[0.625]\",\n", - " \"T[0.75]\",\n", - " \"T[0.875]\",\n", - " \"T[1]\",\n", - " ): [300, 500, 700],\n", - "}\n", - "## choose from \"sequential_finite\", \"direct_kaug\"\n", - "sensi_opt = \"direct_kaug\"\n", - "\n", - "prior_pass = [\n", - " [22.52943024, 1.84034314, -70.23273336, -11.09432962],\n", - " [1.84034314, 18.09848116, -5.73565034, -109.15866135],\n", - " [-70.23273336, -5.73565034, 218.94192843, 34.57680848],\n", - " [-11.09432962, -109.15866135, 34.57680848, 658.37644634],\n", - "]\n", - "\n", - "doe_object = DesignOfExperiments(\n", - " parameter_dict, # parameter dictionary\n", - " exp_design, # design variables\n", - " measurements, # measurement variables\n", - " create_model, # model function\n", - " prior_FIM = prior_pass, \n", - " discretize_model=disc_for_measure, # discretization function\n", - ")\n", - "# run full factorial grid search\n", - "all_fim = doe_object.run_grid_search(design_ranges, mode=sensi_opt)\n", - "\n", - "all_fim.extract_criteria()\n", - "\n", - "for i in all_fim.store_all_results_dataframe.columns:\n", - " all_fim.store_all_results_dataframe[i] = all_fim.store_all_results_dataframe[i].values.real\n", - "\n", - "\n", - "fixed = {}\n", - "all_fim.figure_drawing(\n", - " fixed, \n", - " [\"CA0[0]\",\"T[0]\"],\n", - " \"Reactor\",\n", - " \"$C_{A0}$ [M]\",\n", - " \"T [K]\"\n", - ")\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "id": "b805fb89-45a9-4ca2-8c72-325747d3fd25", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: =======Iteration Number: 1 =====\n", - "INFO: elapsed time: 0.7\n", - "INFO: This is run 1 out of 8.\n", - "INFO: The code has run 0.7312748000040301 seconds.\n", - "INFO: Estimated remaining time: 2.1938244000120903 seconds\n", - "INFO: =======Iteration Number: 2 =====\n", - "INFO: elapsed time: 0.8\n", - "INFO: This is run 2 out of 8.\n", - "INFO: The code has run 1.5368452000038815 seconds.\n", - "INFO: Estimated remaining time: 2.5614086666731355 seconds\n", - "INFO: =======Iteration Number: 3 =====\n", - "INFO: elapsed time: 0.8\n", - "INFO: This is run 3 out of 8.\n", - "INFO: The code has run 2.300336200009042 seconds.\n", - "INFO: Estimated remaining time: 2.300336200009042 seconds\n", - "INFO: =======Iteration Number: 4 =====\n", - "INFO: elapsed time: 1.0\n", - "INFO: This is run 4 out of 8.\n", - "INFO: The code has run 3.2964527000076487 seconds.\n", - "INFO: Estimated remaining time: 1.9778716200045894 seconds\n", - "INFO: =======Iteration Number: 5 =====\n", - "INFO: elapsed time: 0.7\n", - "INFO: This is run 5 out of 8.\n", - "INFO: The code has run 3.9811715000105323 seconds.\n", - "INFO: Estimated remaining time: 1.3270571666701774 seconds\n", - "INFO: =======Iteration Number: 6 =====\n", - "INFO: elapsed time: 0.7\n", - "INFO: This is run 6 out of 8.\n", - "INFO: The code has run 4.726026500014996 seconds.\n", - "INFO: Estimated remaining time: 0.675146642859285 seconds\n", - "INFO: =======Iteration Number: 7 =====\n", - "INFO: elapsed time: 0.8\n", - "INFO: This is run 7 out of 8.\n", - "INFO: The code has run 5.568790400015132 seconds.\n", - "INFO: Estimated remaining time: 0.0 seconds\n", - "INFO: =======Iteration Number: 8 =====\n", - "INFO: elapsed time: 0.7\n", - "INFO: This is run 8 out of 8.\n", - "INFO: The code has run 6.320087100015371 seconds.\n", - "INFO: Estimated remaining time: -0.7022319000017079 seconds\n", - "INFO: Overall wall clock time [s]: 6.320087100015371\n" - ] - }, - { - "data": { - "image/png": "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4eTkBE9PTxgZGVW5njod8JWUlGDLli2QSCQYPXp0TTdHrWbNmoVp06bJ/s7JyYGVlVUNtoiIiIiqi5GREXr16oVevXpVS/l1OuA7cuQIUlNT0b17d4WDhaW9a6X1okmHSF/thTMxMalQ+tJUpu7X6ejoQEdHp9y6iIiI5LGHrzZKTU1VSznW1taVylenAz5lizWkyptzp2yenaOjI06fPo2HDx8qzOMrb15eVesmIiJSD3UFfKROtra2kFRxbqVEIkFxcXGl8tbZgO/x48f47bffYGZmho8//ljhuaOjIywtLXHq1Ck8ffpUbrVsQUEBoqOjYWlpiebNm8vue3t74/Tp0zh8+DBGjBghV15kZKQsTXkqUzcRERGJl7W1dZUDvqqos9uybNu2Dc+fP8ewYcOUDn1KJBIEBAQgLy8P8+bNk3u2cOFCPHnyBAEBAXJvvr+/P7S0tLBgwQK54dhr167hp59+goODA7p16yZXVmpqKuLj4/Hs2bMq1U1ERKQemnjZn1OVizsvq1tycjJu375d5auyJEId3f23bdu2uHr1Kq5cuYK2bdsqTfP06VN4enrKjjfr0KEDLl++jD/++ANubm5KjzdbsGAB5syZA2trawwYMABPnz7Fjh07kJ+fj8jISPj4+Mil79q1K44fP45jx47JHbtWmbrLkpOTI5tjKF3lSyQ+Y2u6AUTVJifnOUxMtlbbf8f/9zvRCMbGVevPyckpgYnJI/7miEid7OGLjY3F1atX0alTp1KDPQAwMDBAVFQUpk6divj4eCxbtgxXr17F1KlTERUVpTTgmj17NsLDw2Fubo61a9di586d6NKlC06dOqUQ7JWlMnUTERERVYc628P3tmEPH70d2MNH4vXmeviaqKmH74HKbb137x527dqFgwcPIj4+Hg8fPoSZmRk8PDwwY8YMvPPOOyrXfffuXXzzzTf4448/8PDhQzRs2BA9e/bEvHnzytyebO/evVizZg0uXryIZ8+ewcLCAu+++y6WLFkil2/jxo3Yt28frl69ivT0dGhpacHW1hYfffQRvvjiC5iZmcmVm5ycrLATyKt27NiBIUOGqPz6akqdXbRBREREymih6gN4FTtlY9WqVVi8eDEcHBzg6+sLc3NzJCQkICIiAhEREdixYwcGDRpUbjlJSUno0qUL0tPT4evri8GDByMhIQFbt27FwYMH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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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1aVLF8XExCgkJETp6en6/PPPlZOTo7vvvlv/+te/5Od36avw++1fWrVqpS1btujLL78sdfuXqVOn6rnnnrNtPXP27FktWrRIubm5WrFihbp06WJ3fefOnbVmzRqtXr3ablsaV+q+EraeQUXA1jMws/LaeuZTSY7/dinZWUl9xNYzzvCpZRP69eunzMxMff/991q7dq3OnTunyMhIJSYmavDgwerfv79dr09ISIhSUlI0efJkffTRR0pJSVFUVJSefvppJSUllRhoJk6cqNjYWL355pt67733FBgYqPbt22vKlClq3bq1w211pW4AAOCbfKqHqiKjhwoVAT1UMLPy6qH6XJ7pofqj6KFyhk/1UAEAgCvzxBwo5lA5j88MAADATfRQAQBgIuzl5x0EKgAATIRA5R0EKgAA4JbDhw9ryZIl+uKLL7Rz504dPXpUkZGRSkhI0NixY3XLLbd4rW0XLlzQpk2btG7dOqWnp+vEiRPKzc1V1apVdcMNN6hly5bq0KGDatas6VY9BCoAAEzEIvcnSDu77PTf/vY3vfLKK6pXr5569OihatWqac+ePfr444/18ccfa9GiRbr33nvdbJVzVq9erZkzZ+rjjz9WXl6eJKmkhQ2Klltq3Lixhg0bpsGDB6tq1apO18eyCT6CZRNQEbBsAsysvJZNWCMp1M2yciR1kuNt/fe//60bbrhBHTp0sDv+3XffqVu3bgoLC9ORI0dktVrdbNnVLV++XOPHj9eOHTtkGIYCAgLUvHlztW7dWtWrV1dkZKSCg4N16tQpnTp1Stu3b9emTZt07NgxSVJgYKAefvhhPf/887rhhhscrpdA5SMIVKgICFQws/IKVN/JM4GqgzzT1l69emnlypXatGmT4uPj3WzZlXXs2FGpqakKDg7WHXfcof79+6tXr14KCrr6L859+/Zp8eLFWrRokbZv366wsDB98MEH+tOf/uRQ3SybAAAAykylSpUkSQEBZT/LaOvWrXr++ed16NAhLVq0SH/6058cClOSVK9ePU2cOFFbt27VN998o1atWunnn392uG7mUAEAYCLX0lN+Bw8e1Ndff62oqCg1b97cQ6WWLj09XWFhYW6X06VLF3Xp0kXZ2dkO30OgAgDARDwZqLKysuyOW61Wh+dBXbhwQYMGDVJ+fr6mT58uf/+yX4zBE2HK1fIY8gMAACWKjo5WRESE7TVt2jSH7issLNSwYcO0du1ajRgxQoMGDSrjlnofPVQAAJiIJ/fyy8jIsJuU7kjvlGEYGjFihJKTkzVw4EC9//77brbGs86dO6fc3FxFRkbalkzwBAIVAAAm4skhv/DwcKee8issLNTw4cM1Z84cDRgwQHPnzpWfn/cGw7KysvTpp59q7dq1toU9i9akslgsioyMtC3s2bNnT7Vu3drlulg2wUewbAIqApZNgJmV17IJP8ozyya0lHNtvTxM3XfffVqwYEG5zJsqycaNG/Xuu+9q6dKlys3NLXFBz8sV9VQ1a9ZMw4cP10MPPaTKlSs7VSc9VAAAmIif3O+hcrZPqbCwUA899JDmzp2re+65R8nJyV4JU7t379b48eP18ccfyzAMVa1aVX379lWbNm2uuLDnxo0blZqaqvXr1+upp57SSy+9pEmTJmnEiBEO97ARqAAAMBFPzqFy1JQpUzR37lyFhoaqQYMGevHFF4tdc+edd6pFixZutuzKmjZtKkm677779OCDD6p79+6lBrtq1aqpWrVqatSoke666y5Jl/YkXLRokd577z099thj+u233zRhwgSH6iZQAQAAtxw4cECSlJOTo6lTp5Z4TWxsbJkHqsGDB2vChAmqV6+eS/fXrFlTzzzzjJ5++mktWLDAqUnrzKHyEcyhQkXAHCqYWXnNodomyd3VmLIlNVXZtdWM6KECAMBEvDHkBwIVAACmci1tPVOREKgAAIBprF271u0yOnbs6PQ9BCoAAEykovdQde7c2a0V0C0WiwoKCpy+j0AFAICJMIfqkurVqys4OLjc6iNQAQAAUzEMQzk5OerVq5cGDhyoLl26lHmdZgihAADg/xStlO7Oy5fDwZYtWzRmzBiFhoZqzpw56t69u2JiYjRhwgRt3769zOr15c8MAAD8jrthyhNzsLypefPmevXVV5WRkaGVK1dq4MCBOnPmjF5++WU1b95cLVu21BtvvKGjR496tF4CFQAAMB2LxaLu3btr3rx5Onr0qJKTk9WzZ09t3bpVY8aMUXR0tG699VYtWLBA586dc7s+AhUAACbi56GXmQQHB+v+++/Xl19+qUOHDun1119XixYttHLlSg0ePFj9+vVzuw4mpQMAYCIVfdmEq6lWrZoGDx6swMBAnThxQgcPHnRpmYTfI1ABAADTO3/+vD799FMlJyfrq6++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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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aYIaNWqovF4QBISHh2tVJ5MzIiIiY8LZmpLKycnBG2+8gZMnT0IURTx+/BiPHz8u83oplthgckZERGRMmJxJatGiRQgPD0ft2rUxdepUtG/fnuucEREREenKd999B3Nzc0RGRqJly5ZVUieTMyIiImPCpTQklZ6eDk9PzypLzAAmZ0RERMaF3ZqSat68OXJycqq0TubGREREpFMrV65Ev3794OLiAmtra9SuXRve3t5YvXo18vLyNC4vLCwM/v7+kMlksLOzg7+/P8LCwioU2zvvvIObN28iIiKiQvdXBJMzIiIiY2KAi9Bu3LgR6enp6Nu3L2bNmoWAgADk5+fj/fffR/fu3TVK0Hbt2oUBAwbg6tWrGDduHCZMmIC4uDgMGDAAu3bt0ji2yZMnY86cORg6dCjWrVtXJa1ogiiKYqXXQlrLysqCvb09MjMzIZPJdB0OUeUYypUvyXhlFQL2R1Bp/x1X/E78CshstCwrF7DvU3mxvig/Px9WVlaljo8dOxY7d+7E+vXr8e677760nPT0dDRu3BhmZma4ePEiXFxcAAD379+Hl5cX8vPzcfv2bTg4OKgdm3z7prt376K4uBgA4OTkVO46Z7du3VK7fFXYckZEREQ6pSoxA4Dhw4cDAG7evKlWOQcOHEBGRgZmzpypSMwAoF69epg9ezYyMjJw4MABjWJLSEhAQkICioqKIIoiRFFEamqq4riql7Y4IYCIiMiYmED7bkk9abo5evQoAKB169ZqXS8fF9avX79S5/r374/58+cjMjISU6dOVTuGO3fuqH2tVJicERERGRMDXkpjzZo1yMjIQEZGBqKjo3HhwgX069cPY8eOVev++Ph4AICHh0epc/Jj8mvU5ebmptH1UmByRkREZEwkXEojKytL6bClpSUsLS21LLxsa9asQWJiouL9mDFjsGHDBpibm6t1f2ZmJgDA3t6+1DkbGxuYmpoqrtFnetJwSURERPrGxcUF9vb2iteKFSvKvNbR0RGCIKj9UrU0RUJCAkRRxP3797F7925ERESgS5cuuHv3biV+Sv3DljMiIiJjImHLWXJystJszfJazQICApCdna12Fc7OzuWeCwgIQNOmTdG5c2e8//772Ldv30vLlLeYZWZmonbt2krncnNzUVxcrLJVTa5169b46KOPMHLkSK32zkxKSsLy5cvRqFEjfPjhhxrf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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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iMomC5leChMZGYmOHTtizZo1RR63evVqdOzYEdHR0Ua3Jfvp27p1K3r06IEaNWpg2rRpyMjIwMKFC3HlyhXs27cPQgi9k3gRERERyWHdunU4duwYWrduXeRxrVu3RkREBDZs2GB0W7JPC44YMQKSJKFLly5YsmQJmjRpotmXmpoqd3NERET0NE4L6nTq1Ck4OzvrzG/1tMaNG6NSpUov1shVp06dIEkSDh8+jDFjxmDZsmW4efOm3M0QERGRLhJMnxJUYM6fGzduwMvLS69jvby8cOPGDaPbkj24Onz4MP79918sXboUABAUFAQPDw907twZmzdvhiQp8BMjIiKiF5q1tTUePnyo17EPHz6EhYXxIVKxLFlzc3PDe++9h7/++guxsbGYMWMGEhMT8e6770IIgY8//hgHDhwA85cSERHJzNTnCsoxrfgCqlu3LhITE5GQkFDkcQkJCUhISICvr6/RbRX7/QD169fHkiVLcPXqVRw5cgRjxoxBdHQ0evbsCXd39+JunoiIqGxhcKXTgAEDIITAyJEjkZKSovOYlJQUjBo1CpIkYdCgQUa3ZZbH32RlZeHnn3/G1q1b8fPPP5d086USH39DZQEff0NKVmKPvwkBHGxMrCsTcAxR1uNvMjIy0KxZM1y8eBGurq5444030LJlS1SsWBEpKSk4deoUNmzYgDt37qBu3br4888/S/bZgufPn8elS5fg6uqKVq1aPff4kydP4t69e6hVqxbq169vVEfLOgZXVBYwuCIlY3BlftevX0e/fv1w5swZnWvAhRDw8/PDDz/8YNLsmsGpGB4/foyuXbsiOTkZR48e1auMEAIDBw5EtWrVcPHiRahUJj5RkoiIiHRjKoZCubu74/Tp0/jxxx/x888/Iy4uDmlpaahQoQIaNGiAvn37om/fviYtZgeMCK62b9+OW7duYeLEiWjTpo1eZdq0aYPx48dj9erV2LFjB0aNGmVwR4mIiEgPDK6KZGFhgYEDB2LgwIHF14ahBX766SdIkoQpU6YYVE59p+APP/xgaJNEREREpYbBI1d//fUXqlatirp16xpUrnbt2qhevTr++usvQ5skIiIifcnxbEAFPluwJBl8+pKTk1G9enWjGqtWrRqSk5ONKktERER6sIDpaRhKeXDVsGFDfPvttybn07x27RomTpyIjz/+2KByBp8+GxsbZGRkGFoMwJPbIK2trY0qS0RERKSPhw8f4vXXX4evry8++ugjJCYm6l02Ozsbu3btwsCBA1G7dm2sW7cOrq6uBrVv8LRg1apVcenSJWRlZRl0119WVhYuXboEDw8PQ5skIiIifXFaEAkJCVixYgWWLFmC4OBghISEwMfHBy1atECzZs1QtWpVODs7Q6VSISUlBffv30dcXBxiYmIQExOD9PR0CCHQpUsXfPzxx2jSpIlB7RscXLVt2xbr16/Hzp07MWzYML3Lff/998jIyEDbtm0NbZKIiIj0xbsFoVKpEBQUhIkTJ2LLli1Yu3Ytzp49i6SkJGzfvl1nGfUUop2dHcaOHYsJEyagefPmRrVvcBLREydOICAgANWqVcPJkyf1SrJ17do1tGrVCnfu3EFkZCT8/f2N6mxZxiSiVBYwiSgpWYklEf0McDAusfj/6soAHKcpK4loYmIiIiMjceLECVy9ehXJycnIzMyEs7MzXF1d0aRJEwQEBKBNmzYoX768SW0ZPHLVpk0bDBo0CN9//z1atmyJL774AgMGDNCZcCs/Px87d+7Eu+++izt37mDAgAEMrIiIiIoTR650ql27NmrXro033ij+v+IMDq4AYNOmTbhx4wZOnDiBIUOGoHLlyvD390fNmjVhZ2eH9PR0XL58GSdOnMDdu3chhEDr1q2xadMmmbtPREREWrjmyuyMCq5sbW0RERGBkJAQrFy5Enfv3sWuXbu0ntOjnm20t7fH5MmTERISAisrK3l6TURERLpx5MrsjAquAKBcuXJYsGABZs6cib179+LEiRO4ceMGHj58iAoVKqB69epo06YNevbsCUdHRzn7TERERKS3e/fu4eeff8bvv/+OxMREPHjwABkZGbC1tYWTkxNq166Nli1bok+fPganXdDF4AXtZB5c0E5lARe0k5KV2IL2NTItaH+z9C9oz8zMxMyZM/H1118jJyenyKSikiTBysoK48ePx9KlS2Fra/xJNHrkioiIiF5A6gztptZRymVlZaF9+/b4448/IIRA3bp14e/vD29vbzg5OUGlUiErKwsPHjzAP//8g+joaMTHx+Orr77C6dOncfz4caMTnzO4IiIiIsX55JNPcPr0adSpUwcbNmxA69atn1vmxIkTGDt2LGJiYrB06VLMnTvXqLYVEJsSERGRhqnPFZRjQfwLYPv27bC2tsahQ4f0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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - " ### 3 design variable example\n", - "# Define design ranges\n", - "design_ranges = {\n", - " \"CA0[0]\": list(np.linspace(1, 5, 2)),\n", - " \"T[0]\": list(np.linspace(300, 700, 2)),\n", - " (\n", - " \"T[0.125]\",\n", - " \"T[0.25]\",\n", - " \"T[0.375]\",\n", - " \"T[0.5]\",\n", - " \"T[0.625]\",\n", - " \"T[0.75]\",\n", - " \"T[0.875]\",\n", - " \"T[1]\",\n", - " ): [300, 500],\n", - "}\n", - "\n", - "sensi_opt = \"direct_kaug\"\n", - "\n", - "doe_object = DesignOfExperiments(\n", - " parameter_dict, # parameter dictionary\n", - " exp_design, # design variables\n", - " measurements, # measurement variables\n", - " create_model, # model function\n", - " prior_FIM = prior_pass, \n", - " discretize_model=disc_for_measure, # discretization function\n", - ")\n", - "# run the grid search for 3 dimensional case\n", - "all_fim = doe_object.run_grid_search(design_ranges, mode=sensi_opt)\n", - "\n", - "all_fim.extract_criteria()\n", - "\n", - "# see the criteria values\n", - "all_fim.store_all_results_dataframe\n", - "\n", - "\n", - "\n", - "fixed = {\"('T[0.125]', 'T[0.25]', 'T[0.375]', 'T[0.5]', 'T[0.625]', 'T[0.75]', 'T[0.875]','T[1]')\": 300}\n", - "\n", - "all_fim.figure_drawing(\n", - " fixed, \n", - " [\"CA0[0]\",\"T[0]\"],\n", - " \"Reactor\", \n", - " \"T [K]\", \n", - " \"$C_{A0}$ [M]\"\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "10c928cf-bc40-4a62-a176-3e3eb19ec78e", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.14" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 3ec88c087f7e6748a72422429e35e98c7536ce4d Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:56:16 -0400 Subject: [PATCH 1918/3044] Delete pyomo/contrib/doe/examples/debugging_compute_FIM.ipynb Removing old file --- .../doe/examples/debugging_compute_FIM.ipynb | 10011 ---------------- 1 file changed, 10011 deletions(-) delete mode 100644 pyomo/contrib/doe/examples/debugging_compute_FIM.ipynb diff --git a/pyomo/contrib/doe/examples/debugging_compute_FIM.ipynb b/pyomo/contrib/doe/examples/debugging_compute_FIM.ipynb deleted file mode 100644 index 1c2f5383e58..00000000000 --- a/pyomo/contrib/doe/examples/debugging_compute_FIM.ipynb +++ /dev/null @@ -1,10011 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "59659c7f-5ced-42da-b7dd-de22f614db9f", - "metadata": {}, - "source": [ - "# Imports" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "id": "f875a1ff-c70d-4f94-a65c-495fbf8faa0c", - "metadata": {}, - "outputs": [], - "source": [ - "import pyomo.environ as pyo\n", - "from pyomo.dae import ContinuousSet, DerivativeVar\n", - "from pyomo.contrib.doe import DesignOfExperiments, MeasurementVariables, DesignVariables, ModelOptionLib\n", - "import copy\n", - "import numpy as np\n", - "from random import sample\n", - "from matplotlib import pyplot as plt" - ] - }, - { - "cell_type": "markdown", - "id": "3b4d475b-bcf6-4e73-ba3b-0393bb65da30", - "metadata": {}, - "source": [ - "## Model function" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "7cf05f64-c3b1-4905-b5c1-b54dcb3aacea", - "metadata": {}, - "outputs": [], - "source": [ - "def create_model(\n", - " mod=None,\n", - " model_option=\"stage2\",\n", - " control_time=[0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1],\n", - " control_val=None,\n", - " t_range=[0.0, 1],\n", - " CA_init=1,\n", - " C_init=0.1,\n", - "):\n", - " \"\"\"\n", - " This is an example user model provided to DoE library.\n", - " It is a dynamic problem solved by Pyomo.DAE.\n", - "\n", - " Arguments\n", - " ---------\n", - " mod: Pyomo model. If None, a Pyomo concrete model is created\n", - " model_option: choose from the 3 options in model_option\n", - " if ModelOptionLib.parmest, create a process model.\n", - " if ModelOptionLib.stage1, create the global model.\n", - " if ModelOptionLib.stage2, add model variables and constraints for block.\n", - " control_time: a list of control timepoints\n", - " control_val: control design variable values T at corresponding timepoints\n", - " t_range: time range, h\n", - " CA_init: time-independent design (control) variable, an initial value for CA\n", - " C_init: An initial value for C\n", - "\n", - " Return\n", - " ------\n", - " m: a Pyomo.DAE model\n", - " \"\"\"\n", - "\n", - " theta = {\"A1\": 84.79, \"A2\": 371.72, \"E1\": 7.78, \"E2\": 15.05}\n", - "\n", - " model_option = ModelOptionLib(model_option)\n", - "\n", - " if model_option == ModelOptionLib.parmest:\n", - " mod = pyo.ConcreteModel()\n", - " return_m = True\n", - " elif model_option == ModelOptionLib.stage1 or model_option == ModelOptionLib.stage2:\n", - " if not mod:\n", - " raise ValueError(\n", - " \"If model option is stage1 or stage2, a created model needs to be provided.\"\n", - " )\n", - " return_m = False\n", - " else:\n", - " raise ValueError(\n", - " \"model_option needs to be defined as parmest,stage1, or stage2.\"\n", - " )\n", - "\n", - " if not control_val:\n", - " control_val = [300] * 9\n", - "\n", - " controls = {}\n", - " for i, t in enumerate(control_time):\n", - " controls[t] = control_val[i]\n", - "\n", - " mod.t0 = pyo.Set(initialize=[0])\n", - " mod.t_con = pyo.Set(initialize=control_time)\n", - " mod.CA0 = pyo.Var(\n", - " mod.t0, initialize=CA_init, bounds=(1.0, 5.0), within=pyo.NonNegativeReals\n", - " ) # mol/L\n", - "\n", - " # check if control_time is in time range\n", - " assert (\n", - " control_time[0] >= t_range[0] and control_time[-1] <= t_range[1]\n", - " ), \"control time is outside time range.\"\n", - "\n", - " if model_option == ModelOptionLib.stage1:\n", - " mod.T = pyo.Var(\n", - " mod.t_con,\n", - " initialize=controls,\n", - " bounds=(300, 700),\n", - " within=pyo.NonNegativeReals,\n", - " )\n", - " return\n", - "\n", - " else:\n", - " para_list = [\"A1\", \"A2\", \"E1\", \"E2\"]\n", - "\n", - " ### Add variables\n", - " mod.CA_init = CA_init\n", - " mod.para_list = para_list\n", - "\n", - " # timepoints\n", - " mod.t = ContinuousSet(bounds=t_range, initialize=control_time)\n", - "\n", - " # time-dependent design variable, initialized with the first control value\n", - " def T_initial(m, t):\n", - " if t in m.t_con:\n", - " return controls[t]\n", - " else:\n", - " # count how many control points are before the current t;\n", - " # locate the nearest neighbouring control point before this t\n", - " neighbour_t = max(tc for tc in control_time if tc < t)\n", - " return controls[neighbour_t]\n", - "\n", - " mod.T = pyo.Var(\n", - " mod.t, initialize=T_initial, bounds=(300, 700), within=pyo.NonNegativeReals\n", - " )\n", - "\n", - " mod.R = 8.31446261815324 # J / K / mole\n", - "\n", - " # Define parameters as Param\n", - " mod.A1 = pyo.Var(initialize=theta[\"A1\"])\n", - " mod.A2 = pyo.Var(initialize=theta[\"A2\"])\n", - " mod.E1 = pyo.Var(initialize=theta[\"E1\"])\n", - " mod.E2 = pyo.Var(initialize=theta[\"E2\"])\n", - "\n", - " # Concentration variables under perturbation\n", - " mod.C_set = pyo.Set(initialize=[\"CA\", \"CB\", \"CC\"])\n", - " mod.C = pyo.Var(\n", - " mod.C_set, mod.t, initialize=C_init, within=pyo.NonNegativeReals\n", - " )\n", - "\n", - " # time derivative of C\n", - " mod.dCdt = DerivativeVar(mod.C, wrt=mod.t)\n", - "\n", - " # kinetic parameters\n", - " def kp1_init(m, t):\n", - " return m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t]))\n", - "\n", - " def kp2_init(m, t):\n", - " return m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t]))\n", - "\n", - " mod.kp1 = pyo.Var(mod.t, initialize=kp1_init)\n", - " mod.kp2 = pyo.Var(mod.t, initialize=kp2_init)\n", - "\n", - " def T_control(m, t):\n", - " \"\"\"\n", - " T at interval timepoint equal to the T of the control time point at the beginning of this interval\n", - " Count how many control points are before the current t;\n", - " locate the nearest neighbouring control point before this t\n", - " \"\"\"\n", - " if t in m.t_con:\n", - " return pyo.Constraint.Skip\n", - " else:\n", - " neighbour_t = max(tc for tc in control_time if tc < t)\n", - " return m.T[t] == m.T[neighbour_t]\n", - "\n", - " def cal_kp1(m, t):\n", - " \"\"\"\n", - " Create the perturbation parameter sets\n", - " m: model\n", - " t: time\n", - " \"\"\"\n", - " # LHS: 1/h\n", - " # RHS: 1/h*(kJ/mol *1000J/kJ / (J/mol/K) / K)\n", - " return m.kp1[t] == m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t]))\n", - "\n", - " def cal_kp2(m, t):\n", - " \"\"\"\n", - " Create the perturbation parameter sets\n", - " m: model\n", - " t: time\n", - " \"\"\"\n", - " # LHS: 1/h\n", - " # RHS: 1/h*(kJ/mol *1000J/kJ / (J/mol/K) / K)\n", - " return m.kp2[t] == m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t]))\n", - "\n", - " def dCdt_control(m, y, t):\n", - " \"\"\"\n", - " Calculate CA in Jacobian matrix analytically\n", - " y: CA, CB, CC\n", - " t: timepoints\n", - " \"\"\"\n", - " if y == \"CA\":\n", - " return m.dCdt[y, t] == -m.kp1[t] * m.C[\"CA\", t]\n", - " elif y == \"CB\":\n", - " return m.dCdt[y, t] == m.kp1[t] * m.C[\"CA\", t] - m.kp2[t] * m.C[\"CB\", t]\n", - " elif y == \"CC\":\n", - " return pyo.Constraint.Skip\n", - "\n", - " def alge(m, t):\n", - " \"\"\"\n", - " The algebraic equation for mole balance\n", - " z: m.pert\n", - " t: time\n", - " \"\"\"\n", - " return m.C[\"CA\", t] + m.C[\"CB\", t] + m.C[\"CC\", t] == m.CA0[0]\n", - "\n", - " # Control time\n", - " mod.T_rule = pyo.Constraint(mod.t, rule=T_control)\n", - "\n", - " # calculating C, Jacobian, FIM\n", - " mod.k1_pert_rule = pyo.Constraint(mod.t, rule=cal_kp1)\n", - " mod.k2_pert_rule = pyo.Constraint(mod.t, rule=cal_kp2)\n", - " mod.dCdt_rule = pyo.Constraint(mod.C_set, mod.t, rule=dCdt_control)\n", - "\n", - " mod.alge_rule = pyo.Constraint(mod.t, rule=alge)\n", - "\n", - " # B.C.\n", - " mod.C[\"CB\", 0.0].fix(0.0)\n", - " mod.C[\"CC\", 0.0].fix(0.0)\n", - "\n", - " if return_m:\n", - " return mod\n", - "\n", - "\n", - "def disc_for_measure(m, nfe=32, block=True):\n", - " \"\"\"Pyomo.DAE discretization\n", - "\n", - " Arguments\n", - " ---------\n", - " m: Pyomo model\n", - " nfe: number of finite elements b\n", - " block: if True, the input model has blocks\n", - " \"\"\"\n", - " discretizer = pyo.TransformationFactory(\"dae.collocation\")\n", - " if block:\n", - " for s in range(len(m.block)):\n", - " discretizer.apply_to(m.block[s], nfe=nfe, ncp=3, wrt=m.block[s].t)\n", - " else:\n", - " discretizer.apply_to(m, nfe=nfe, ncp=3, wrt=m.t)\n", - " return m" - ] - }, - { - "cell_type": "markdown", - "id": "ac9e76cb-cbf5-44fb-aa53-bbb5dca1bcb0", - "metadata": {}, - "source": [ - "## Helper Functions" - ] - }, - { - "cell_type": "markdown", - "id": "2814a5e6-e4b0-4fa1-948c-a8671c52fb4b", - "metadata": {}, - "source": [ - "### Create a doe object" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "24628fdb-317d-4121-b4ea-9909298a757e", - "metadata": {}, - "outputs": [], - "source": [ - "def create_doe_object(Ca_val, T_vals, prior_FIM=None):\n", - " t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]\n", - " parameter_dict = {\"A1\": 85, \"A2\": 370, \"E1\": 8, \"E2\": 15}\n", - " \n", - " measurements = MeasurementVariables()\n", - " measurements.add_variables(\"C\", indices={0: [\"CA\", \"CB\", \"CC\"], 1: t_control}, time_index_position=1)\n", - " \n", - " exp_design = DesignVariables()\n", - " exp_design.add_variables(\n", - " \"CA0\",\n", - " time_index_position=0,\n", - " values=[Ca_val, ],\n", - " lower_bounds=1,\n", - " indices={0: [0]},\n", - " upper_bounds=5,\n", - " )\n", - " exp_design.add_variables(\n", - " \"T\",\n", - " indices={0: t_control},\n", - " time_index_position=0,\n", - " values=T_vals,\n", - " lower_bounds=300,\n", - " upper_bounds=700,\n", - " )\n", - " \n", - " doe_object = DesignOfExperiments(\n", - " parameter_dict,\n", - " exp_design, \n", - " measurements, \n", - " create_model,\n", - " prior_FIM=prior_FIM,\n", - " discretize_model=disc_for_measure,\n", - " )\n", - " return doe_object" - ] - }, - { - "cell_type": "markdown", - "id": "35346c15-408b-4f32-bbfa-b4de9da2eaae", - "metadata": {}, - "source": [ - "### Compute FIM using the compute_FIM function" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "0a2d84d7-9989-43d3-84d0-df01a95c5a66", - "metadata": {}, - "outputs": [], - "source": [ - "def compute_specific_FIM(Ca_val, T_vals, prior_FIM=None, scale_param=True):\n", - " doe_object = create_doe_object(Ca_val, T_vals, prior_FIM)\n", - "\n", - " result = doe_object.compute_FIM(\n", - " mode=\"sequential_finite\",\n", - " scale_nominal_param_value=scale_param,\n", - " formula=\"central\",\n", - " )\n", - " result.result_analysis()\n", - " \n", - " return result" - ] - }, - { - "cell_type": "markdown", - "id": "1a6f71b5-f09c-40d3-9793-b830f05aae54", - "metadata": {}, - "source": [ - "### Rescale FIM" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "d000abc6-9140-4634-a180-d1c2b6df9e0c", - "metadata": {}, - "outputs": [], - "source": [ - "def rescale_FIM(FIM, param_vals):\n", - " param_scaling_mat = (1 / param_vals).transpose().dot(1 / param_vals)\n", - " unscaled_FIM = np.multiply(FIM, param_scaling_mat)\n", - " return unscaled_FIM" - ] - }, - { - "cell_type": "markdown", - "id": "679466b7-5299-4489-a3fb-cbfdd74cfc27", - "metadata": {}, - "source": [ - "### Translate Jacobian to numpy array" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "dff13661-d577-44a3-a171-b3fa1e6ff367", - "metadata": {}, - "outputs": [], - "source": [ - "def translate_jac(jac_dict):\n", - " param_names = ['A1', 'A2', 'E1', 'E2']\n", - " Q_all = np.array(list(jac_dict for p in param_names)).T\n", - " return Q_all" - ] - }, - { - "cell_type": "markdown", - "id": "60fcc29c-dad9-4d85-8d2c-81daeb08deb3", - "metadata": {}, - "source": [ - "### Get experimental conditions from solved model" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "d6923520-83b5-4a64-9040-f19e1fa663f2", - "metadata": {}, - "outputs": [], - "source": [ - "def get_exp_conds(m):\n", - " return [pyo.value(m.CA0[0]),\n", - " pyo.value(m.T[0]),\n", - " pyo.value(m.T[0.125]),\n", - " pyo.value(m.T[0.25]),\n", - " pyo.value(m.T[0.375]),\n", - " pyo.value(m.T[0.5]),\n", - " pyo.value(m.T[0.625]),\n", - " pyo.value(m.T[0.75]),\n", - " pyo.value(m.T[0.875]),\n", - " pyo.value(m.T[1])]" - ] - }, - { - "cell_type": "markdown", - "id": "ed69f2f3-0517-4bdd-ae43-9566560e7e73", - "metadata": {}, - "source": [ - "### Run optimal experiment" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "id": "989b0b0d-6cca-4c56-8088-8c95c9c1773a", - "metadata": {}, - "outputs": [], - "source": [ - "def run_optimal_exp(Ca, Ta_vals, prior_FIM=None, scale_param=True):\n", - " doe_object = create_doe_object(Ca, Ta_vals, prior_FIM)\n", - "\n", - " if prior_FIM is None:\n", - " prior_FIM = np.eye(4)\n", - " \n", - " square_result, optimize_result = doe_object.stochastic_program(\n", - " if_optimize=True,\n", - " if_Cholesky=True,\n", - " scale_nominal_param_value=scale_param,\n", - " objective_option=\"det\",\n", - " L_initial=np.linalg.cholesky(prior_FIM),\n", - " )\n", - " \n", - " return optimize_result" - ] - }, - { - "cell_type": "markdown", - "id": "950f0e98-74b7-42bb-8781-a94da863eac6", - "metadata": {}, - "source": [ - "# Perform the analysis" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "id": "e5013f1e-0e77-4697-a9de-8c137d930452", - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 5.77e+02 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 6.70e+00 3.85e+00 -1.0 6.23e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 6.62e-02 4.39e+00 -1.0 7.18e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.10e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.131\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -7.8073036e+00 1.25e+00 1.21e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -7.8354856e+00 6.29e-01 5.10e+00 -1.0 4.68e+01 - 9.58e-01 4.97e-01h 1\n", - " 2 -7.8368689e+00 8.10e-01 1.42e+00 -1.0 3.23e+01 - 9.25e-01 1.00e+00f 1\n", - " 3 -7.9348649e+00 7.30e+01 1.59e+02 -1.0 4.72e+02 - 2.16e-01 7.26e-01f 1\n", - " 4 -7.7429992e+00 1.53e+01 2.05e+00 -1.0 1.61e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -7.7606510e+00 3.51e+00 2.02e+00 -1.0 1.39e+02 - 1.00e+00 1.00e+00f 1\n", - " 6 -7.6711598e+00 5.13e+00 3.33e-01 -1.0 4.03e+01 - 1.00e+00 1.00e+00h 1\n", - " 7 -7.6659485e+00 4.95e-02 7.71e-03 -1.0 2.74e+00 - 1.00e+00 1.00e+00h 1\n", - " 8 -7.6685762e+00 2.86e-03 1.24e-02 -2.5 8.68e-01 - 1.00e+00 1.00e+00h 1\n", - " 9 -7.7508739e+00 3.08e+00 3.55e-01 -3.8 3.14e+01 - 8.03e-01 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -7.8600596e+00 6.29e+00 2.01e-01 -3.8 6.87e+01 - 1.00e+00 1.00e+00h 1\n", - " 11 -7.9182984e+00 4.14e+00 1.52e-01 -3.8 2.68e+01 - 1.00e+00 1.00e+00h 1\n", - " 12 -7.9098461e+00 6.13e-02 2.12e-03 -3.8 8.74e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (325287)\n", - " 13 -7.9098458e+00 4.63e-05 4.17e-06 -3.8 2.49e-01 - 1.00e+00 1.00e+00h 1\n", - " 14 -7.9395810e+00 1.47e+00 5.37e-02 -5.7 2.73e+01 - 7.73e-01 1.00e+00h 1\n", - " 15 -7.9504201e+00 6.12e-01 7.82e-02 -5.7 2.47e+01 - 9.84e-01 1.00e+00h 1\n", - " 16 -7.9503130e+00 1.96e-02 5.10e-03 -5.7 1.22e+01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (347931)\n", - " 17 -7.9503760e+00 2.90e-03 7.32e-04 -5.7 4.78e+00 - 1.00e+00 1.00e+00h 1\n", - " 18 -7.9503861e+00 5.47e-05 1.53e-05 -5.7 6.60e-01 - 1.00e+00 1.00e+00h 1\n", - " 19 -7.9503862e+00 1.43e-08 5.92e-09 -5.7 1.07e-02 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -7.9510641e+00 2.59e-03 7.06e-04 -8.6 4.57e+00 - 9.84e-01 1.00e+00h 1\n", - " 21 -7.9510903e+00 1.70e-04 5.47e-05 -8.6 1.18e+00 - 1.00e+00 1.00e+00h 1\n", - " 22 -7.9510917e+00 4.69e-07 2.65e-07 -8.6 6.21e-02 - 1.00e+00 1.00e+00h 1\n", - " 23 -7.9510917e+00 4.83e-12 5.34e-12 -8.6 1.99e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 23\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -7.9510917256426819e+00 -7.9510917256426819e+00\n", - "Dual infeasibility......: 5.3406483518369473e-12 5.3406483518369473e-12\n", - "Constraint violation....: 4.8324677592859189e-12 4.8324677592859189e-12\n", - "Complementarity.........: 2.5059057458010459e-09 2.5059057458010459e-09\n", - "Overall NLP error.......: 2.5059057458010459e-09 2.5059057458010459e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 24\n", - "Number of objective gradient evaluations = 24\n", - "Number of equality constraint evaluations = 24\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 24\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 23\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.827\n", - "Total CPU secs in NLP function evaluations = 0.027\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.5 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 3.83e+00 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.42e-02 3.85e+00 -1.0 4.12e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.37e-04 4.39e+00 -1.0 4.72e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.67e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8338667230373176e-11 5.8338667230373176e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8338667230373176e-11 5.8338667230373176e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.129\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 5.3463002e+00 1.25e+00 7.84e+01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 5.3181206e+00 6.32e-01 4.24e+01 -1.0 7.13e-01 - 9.72e-01 4.95e-01h 1\n", - " 2 5.3083839e+00 2.63e-02 1.90e+00 -1.0 3.89e+00 - 9.00e-01 1.00e+00f 1\n", - " 3 5.2237497e+00 1.10e+01 1.32e+02 -1.0 1.82e+02 - 1.89e-01 6.62e-01f 1\n", - " 4 5.4304758e+00 5.04e-01 2.27e+00 -1.0 3.13e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 5.3767095e+00 5.59e-01 3.10e+01 -1.0 1.35e+02 - 1.00e+00 4.26e-01f 2\n", - " 6 5.4552152e+00 8.41e-01 3.31e+00 -1.0 8.84e+01 - 9.40e-01 3.64e-01f 2\n", - " 7 5.5170724e+00 4.57e-01 4.85e-01 -1.0 4.86e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 5.5110643e+00 5.10e-03 2.84e-02 -1.7 1.44e+00 - 1.00e+00 1.00e+00h 1\n", - " 9 5.4931986e+00 9.03e-03 1.34e-02 -2.5 4.22e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 5.4082326e+00 3.49e-01 3.95e-01 -3.8 2.69e+01 - 7.84e-01 1.00e+00h 1\n", - " 11 5.3009072e+00 1.28e+00 1.94e-01 -3.8 6.45e+01 - 1.00e+00 1.00e+00h 1\n", - " 12 5.2328311e+00 3.94e-01 2.25e-01 -3.8 2.80e+01 - 1.00e+00 1.00e+00h 1\n", - " 13 5.2437917e+00 5.42e-03 2.96e-03 -3.8 2.67e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (320786)\n", - " 14 5.2437575e+00 2.67e-05 8.17e-06 -3.8 2.13e-01 - 1.00e+00 1.00e+00h 1\n", - " 15 5.2424530e+00 7.65e-06 1.35e-05 -5.7 9.87e-02 -4.0 1.00e+00 1.00e+00h 1\n", - " 16 5.2417146e+00 5.83e-05 6.61e-05 -8.6 3.00e-01 -4.5 9.97e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (344430)\n", - " 17 5.2399653e+00 4.96e-04 6.20e-04 -8.6 8.80e-01 -5.0 1.00e+00 1.00e+00h 1\n", - " 18 5.2353444e+00 3.84e-03 4.47e-03 -8.6 2.48e+00 -5.4 1.00e+00 1.00e+00h 1\n", - " 19 5.2251746e+00 2.39e-02 2.25e-02 -8.6 6.35e+00 -5.9 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 5.2221058e+00 2.25e-02 2.03e-02 -8.6 1.32e+01 -6.4 1.00e+00 1.69e-01h 1\n", - " 21 5.2103493e+00 7.19e-02 5.06e-02 -8.6 2.54e+01 -6.9 1.00e+00 6.04e-01f 1\n", - " 22 5.1989953e+00 2.17e-01 1.44e-01 -8.6 5.35e+01 - 1.00e+00 7.30e-01f 1\n", - " 23 5.1994362e+00 1.93e-01 1.25e-01 -8.6 4.05e+01 - 1.00e+00 1.27e-01h 1\n", - " 24 5.2008740e+00 1.40e-01 6.63e-02 -8.6 3.68e+01 - 1.00e+00 4.77e-01h 1\n", - " 25 5.2025347e+00 1.75e-03 2.05e-03 -8.6 3.78e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (363451)\n", - " 26 5.2025125e+00 1.38e-03 1.06e-05 -8.6 3.34e+00 - 1.00e+00 1.00e+00h 1\n", - " 27 5.2025121e+00 2.22e-05 1.37e-07 -8.6 4.27e-01 - 1.00e+00 1.00e+00h 1\n", - " 28 5.2025121e+00 3.85e-09 2.27e-11 -8.6 5.62e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 28\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 5.2025120660150401e+00 5.2025120660150401e+00\n", - "Dual infeasibility......: 2.2651314864147046e-11 2.2651314864147046e-11\n", - "Constraint violation....: 3.8472891539242937e-09 3.8472891539242937e-09\n", - "Complementarity.........: 2.5061201652512559e-09 2.5061201652512559e-09\n", - "Overall NLP error.......: 3.8472891539242937e-09 3.8472891539242937e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 35\n", - "Number of objective gradient evaluations = 29\n", - "Number of equality constraint evaluations = 35\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 29\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 28\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.155\n", - "Total CPU secs in NLP function evaluations = 0.043\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.9 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.43e-03 1.52e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 2.33e-09 1.52e-06 -1.0 1.82e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.3276043314979233e-09 2.3276043314979233e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.3276043314979233e-09 2.3276043314979233e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.099\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.43e-03 1.52e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 2.33e-09 1.52e-06 -1.0 1.82e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.3276034433195036e-09 2.3276034433195036e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.3276034433195036e-09 2.3276034433195036e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.100\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 7.65e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 5.84e+02 3.85e+02 -1.0 7.65e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 6.77e+00 3.85e+00 -1.0 6.31e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 6.70e-02 4.39e+00 -1.0 7.25e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.17e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0383309725439176e-09 9.0383309725439176e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0383309725439176e-09 9.0383309725439176e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.127\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -9.0980672e+00 1.25e+00 1.13e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -9.1088823e+00 6.23e-01 5.20e+00 -1.0 4.67e+01 - 9.73e-01 5.02e-01h 1\n", - " 2 -9.1070281e+00 1.28e+00 1.94e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -9.1458221e+00 7.33e+01 1.31e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -9.0628162e+00 1.67e+01 2.33e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -9.0789051e+00 1.17e+01 3.14e+01 -1.0 1.36e+02 - 1.00e+00 4.20e-01f 2\n", - " 6 -9.0469561e+00 1.20e+01 3.32e+00 -1.0 9.02e+01 - 9.08e-01 3.50e-01f 2\n", - " 7 -9.0194008e+00 3.33e+00 5.06e-01 -1.0 5.04e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -9.0315738e+00 3.90e-01 6.40e-02 -1.0 9.89e+00 - 1.00e+00 1.00e+00h 1\n", - " 9 -9.0319624e+00 5.08e-04 4.68e-03 -1.7 3.22e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -9.0344881e+00 1.57e-02 2.20e-01 -3.8 2.67e+00 - 9.84e-01 1.00e+00h 1\n", - " 11 -9.0929490e+00 9.49e+00 2.02e-01 -3.8 7.71e+01 - 1.00e+00 1.00e+00h 1\n", - " 12 -9.1265794e+00 4.04e+00 1.08e-01 -3.8 2.61e+01 - 1.00e+00 1.00e+00h 1\n", - " 13 -9.1216817e+00 7.85e-02 1.20e-03 -3.8 1.12e+01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (325095)\n", - " 14 -9.1216910e+00 9.57e-05 3.07e-06 -3.8 2.80e-01 - 1.00e+00 1.00e+00h 1\n", - " 15 -9.1419843e+00 2.21e+00 4.13e-02 -5.7 3.31e+01 - 7.83e-01 1.00e+00h 1\n", - " 16 -9.1523277e+00 3.23e+00 1.07e-01 -5.7 5.50e+01 - 5.77e-01 8.28e-01h 1\n", - " 17 -9.1542206e+00 1.66e-01 4.37e-03 -5.7 1.73e+01 - 1.00e+00 1.00e+00h 1\n", - " 18 -9.1545142e+00 1.36e-02 1.84e-03 -5.7 1.01e+01 - 1.00e+00 1.00e+00h 1\n", - " 19 -9.1545212e+00 1.02e-03 1.25e-04 -5.7 2.81e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (343319)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -9.1545223e+00 4.18e-06 5.47e-07 -5.7 1.81e-01 - 1.00e+00 1.00e+00h 1\n", - " 21 -9.1551820e+00 9.84e-03 7.80e-04 -8.6 6.83e+00 - 9.73e-01 1.00e+00h 1\n", - " 22 -9.1552078e+00 8.18e-04 1.19e-04 -8.6 2.58e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (362498)\n", - " 23 -9.1552112e+00 1.16e-05 2.50e-06 -8.6 3.08e-01 - 1.00e+00 1.00e+00h 1\n", - " 24 -9.1552112e+00 1.62e-09 9.16e-10 -8.6 3.65e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 24\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -9.1552111997823289e+00 -9.1552111997823289e+00\n", - "Dual infeasibility......: 9.1614693867759918e-10 9.1614693867759918e-10\n", - "Constraint violation....: 1.6240718769822138e-09 1.6240718769822138e-09\n", - "Complementarity.........: 2.5062713088537995e-09 2.5062713088537995e-09\n", - "Overall NLP error.......: 2.5062713088537995e-09 2.5062713088537995e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 31\n", - "Number of objective gradient evaluations = 25\n", - "Number of equality constraint evaluations = 31\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 25\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 24\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.818\n", - "Total CPU secs in NLP function evaluations = 0.029\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.3 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 3.86e+00 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.45e-02 3.85e+00 -1.0 4.15e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.40e-04 4.39e+00 -1.0 4.75e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.70e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8336890873533775e-11 5.8336890873533775e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8336890873533775e-11 5.8336890873533775e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.134\n", - "Total CPU secs in NLP function evaluations = 0.007\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 4.0555365e+00 1.25e+00 4.96e+01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 4.0447179e+00 6.24e-01 2.67e+01 -1.0 7.09e-01 - 9.72e-01 5.01e-01h 1\n", - " 2 4.0465757e+00 2.64e-02 1.93e+00 -1.0 3.91e+00 - 8.99e-01 1.00e+00f 1\n", - " 3 4.0077802e+00 1.10e+01 1.31e+02 -1.0 1.82e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 4.0907795e+00 5.11e-01 2.33e+00 -1.0 3.13e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 4.0746958e+00 5.49e-01 3.14e+01 -1.0 1.36e+02 - 1.00e+00 4.20e-01f 2\n", - " 6 4.1066414e+00 8.29e-01 3.32e+00 -1.0 9.02e+01 - 9.08e-01 3.50e-01f 2\n", - " 7 4.1341891e+00 4.99e-01 5.06e-01 -1.0 5.03e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 4.1322707e+00 5.59e-03 2.41e-02 -1.7 1.21e+00 - 1.00e+00 1.00e+00h 1\n", - " 9 4.1294448e+00 1.74e-03 1.90e-01 -3.8 1.80e+00 - 9.85e-01 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 4.0577304e+00 2.51e+00 3.18e-01 -3.8 8.71e+01 - 1.00e+00 1.00e+00h 1\n", - " 11 4.0619968e+00 6.23e-02 7.22e-03 -3.8 1.27e+00 -4.0 1.00e+00 1.00e+00h 1\n", - " 12 4.0610820e+00 2.11e-05 1.57e-05 -3.8 1.71e-01 -4.5 1.00e+00 1.00e+00h 1\n", - " 13 4.0025145e+00 1.12e+00 2.83e-01 -5.7 5.61e+01 - 6.15e-01 1.00e+00h 1\n", - " 14 4.0028658e+00 3.34e-01 8.43e-02 -5.7 2.46e+01 - 6.14e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (328499)\n", - " 15 3.9993729e+00 8.26e-02 2.36e-02 -5.7 1.29e+01 - 1.00e+00 8.29e-01h 1\n", - " 16 3.9990307e+00 1.69e-02 2.80e-03 -5.7 1.13e+01 - 1.00e+00 1.00e+00f 1\n", - " 17 3.9990819e+00 1.12e-03 1.43e-04 -5.7 2.96e+00 - 1.00e+00 1.00e+00h 1\n", - " 18 3.9990815e+00 4.62e-06 7.31e-07 -5.7 1.90e-01 - 1.00e+00 1.00e+00h 1\n", - " 19 3.9984217e+00 5.89e-03 7.80e-04 -8.6 6.83e+00 - 9.73e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (356713)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 3.9983959e+00 8.18e-04 1.19e-04 -8.6 2.58e+00 - 1.00e+00 1.00e+00h 1\n", - " 21 3.9983926e+00 1.16e-05 2.50e-06 -8.6 3.08e-01 - 1.00e+00 1.00e+00h 1\n", - " 22 3.9983926e+00 1.62e-09 9.16e-10 -8.6 3.65e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 22\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 3.9983925918754277e+00 3.9983925918754277e+00\n", - "Dual infeasibility......: 9.1638497048149078e-10 9.1638497048149078e-10\n", - "Constraint violation....: 1.6245825795735414e-09 1.6245825795735414e-09\n", - "Complementarity.........: 2.5062714319510496e-09 2.5062714319510496e-09\n", - "Overall NLP error.......: 2.5062714319510496e-09 2.5062714319510496e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 29\n", - "Number of objective gradient evaluations = 23\n", - "Number of equality constraint evaluations = 29\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 23\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 22\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.757\n", - "Total CPU secs in NLP function evaluations = 0.035\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.5 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.43e-03 1.62e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 2.31e-09 1.52e-06 -1.0 1.81e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.3096600187955119e-09 2.3096600187955119e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.3096600187955119e-09 2.3096600187955119e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", - "Total CPU secs in NLP function evaluations = 0.008\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.43e-03 1.62e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 2.31e-09 1.52e-06 -1.0 1.81e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.3096582424386725e-09 2.3096582424386725e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.3096582424386725e-09 2.3096582424386725e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", - "Total CPU secs in NLP function evaluations = 0.002\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.53e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 5.92e+02 3.85e+02 -1.0 1.53e+03 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 6.85e+00 3.85e+00 -1.0 6.39e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 6.78e-02 4.39e+00 -1.0 7.33e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.25e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0381035988684744e-09 9.0381035988684744e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0381035988684744e-09 9.0381035988684744e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.129\n", - "Total CPU secs in NLP function evaluations = 0.003\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -9.8243678e+00 1.25e+00 1.11e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -9.8309752e+00 6.21e-01 5.18e+00 -1.0 4.66e+01 - 9.73e-01 5.03e-01h 1\n", - " 2 -9.8284057e+00 1.28e+00 1.94e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -9.8536224e+00 7.33e+01 1.31e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -9.8018928e+00 1.67e+01 2.30e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -9.8108490e+00 1.17e+01 3.16e+01 -1.0 1.36e+02 - 1.00e+00 4.18e-01f 2\n", - " 6 -9.7907700e+00 1.21e+01 3.32e+00 -1.0 9.09e+01 - 8.96e-01 3.45e-01f 2\n", - " 7 -9.7727931e+00 3.44e+00 5.16e-01 -1.0 5.11e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -9.7806557e+00 4.10e-01 6.89e-02 -1.0 1.03e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -9.7808078e+00 3.15e-04 3.35e-03 -1.7 2.38e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -9.7818625e+00 6.70e-03 1.10e-01 -3.8 1.81e+00 - 9.92e-01 1.00e+00h 1\n", - " 11 -9.8219871e+00 1.08e+01 1.59e-01 -3.8 8.04e+01 - 1.00e+00 1.00e+00h 1\n", - " 12 -9.8318869e+00 1.12e+00 2.36e-02 -3.8 1.46e+01 - 1.00e+00 1.00e+00h 1\n", - " 13 -9.8315331e+00 8.02e-03 5.09e-05 -3.8 3.56e+00 - 1.00e+00 1.00e+00h 1\n", - " 14 -9.8315167e+00 4.91e-06 6.23e-08 -3.8 2.45e-02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (319727)\n", - " 15 -9.8470261e+00 2.31e+00 2.95e-02 -5.7 3.68e+01 - 7.66e-01 1.00e+00f 1\n", - " 16 -9.8550043e+00 3.01e+00 3.76e-02 -5.7 4.15e+01 - 6.44e-01 1.00e+00h 1\n", - " 17 -9.8580603e+00 2.13e+00 3.03e-02 -5.7 5.38e+01 - 9.08e-01 5.97e-01h 1\n", - "Reallocating memory for MA57: lfact (343560)\n", - " 18 -9.8590178e+00 5.44e-02 3.07e-03 -5.7 1.49e+01 - 1.00e+00 1.00e+00h 1\n", - " 19 -9.8588956e+00 2.88e-03 2.14e-04 -5.7 4.70e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -9.8588964e+00 2.30e-05 1.59e-06 -5.7 4.22e-01 - 1.00e+00 1.00e+00h 1\n", - " 21 -9.8588964e+00 1.08e-09 9.88e-11 -5.7 2.90e-03 - 1.00e+00 1.00e+00h 1\n", - " 22 -9.8595479e+00 2.23e-02 7.77e-04 -8.6 8.37e+00 - 9.64e-01 1.00e+00f 1\n", - " 23 -9.8595710e+00 1.73e-03 1.60e-04 -8.6 3.74e+00 - 1.00e+00 1.00e+00h 1\n", - " 24 -9.8595756e+00 5.35e-05 6.70e-06 -8.6 6.62e-01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (362351)\n", - " 25 -9.8595757e+00 3.41e-08 9.69e-09 -8.6 1.67e-02 - 1.00e+00 1.00e+00h 1\n", - " 26 -9.8595757e+00 4.55e-13 5.52e-14 -8.6 2.20e-05 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 26\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -9.8595757275986511e+00 -9.8595757275986511e+00\n", - "Dual infeasibility......: 5.5219338836918813e-14 5.5219338836918813e-14\n", - "Constraint violation....: 1.1824654114063750e-13 4.5474735088646412e-13\n", - "Complementarity.........: 2.5059035684619852e-09 2.5059035684619852e-09\n", - "Overall NLP error.......: 2.5059035684619852e-09 2.5059035684619852e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 33\n", - "Number of objective gradient evaluations = 27\n", - "Number of equality constraint evaluations = 33\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 27\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 26\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.850\n", - "Total CPU secs in NLP function evaluations = 0.040\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.5 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 3.89e+00 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.48e-02 3.85e+00 -1.0 4.18e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.43e-04 4.39e+00 -1.0 4.79e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.73e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8336446784323925e-11 5.8336446784323925e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8336446784323925e-11 5.8336446784323925e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.129\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 3.3292360e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 3.3241914e+00 1.69e-01 1.13e+00 -1.0 5.96e-01 - 9.79e-01 8.65e-01h 1\n", - " 2 3.3248864e+00 3.48e-02 1.06e+01 -1.0 6.02e+00 - 8.34e-01 1.00e+00f 1\n", - " 3 3.3013605e+00 1.26e+01 5.70e+01 -1.0 1.80e+02 - 1.92e-01 7.12e-01f 1\n", - " 4 3.3560475e+00 7.19e-01 2.65e+00 -1.0 2.65e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 3.3417261e+00 5.86e-01 4.46e+01 -1.0 3.82e+02 - 4.35e-01 8.06e-02f 2\n", - " 6 3.3757459e+00 1.35e-01 4.87e+00 -1.0 2.40e+01 -4.0 8.21e-01 1.00e+00F 1\n", - " 7 3.3696371e+00 4.25e-01 3.04e-01 -1.0 5.43e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 3.3719932e+00 4.00e-02 5.66e-02 -1.7 1.71e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 3.3709820e+00 4.47e-03 3.86e-03 -2.5 5.77e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 3.3640190e+00 1.64e-02 7.98e-02 -3.8 6.30e+00 - 9.57e-01 1.00e+00h 1\n", - " 11 3.3273550e+00 8.10e-01 8.71e-02 -3.8 4.15e+01 - 1.00e+00 1.00e+00h 1\n", - " 12 3.3219891e+00 1.09e-01 1.42e-02 -3.8 1.16e+01 - 1.00e+00 1.00e+00h 1\n", - " 13 3.3220787e+00 1.81e-04 1.97e-05 -3.8 5.23e-01 - 1.00e+00 1.00e+00h 1\n", - " 14 3.3065783e+00 3.88e-01 2.95e-02 -5.7 3.68e+01 - 7.66e-01 1.00e+00h 1\n", - " 15 3.3088038e+00 6.58e-04 2.85e-03 -5.7 1.95e-01 -4.5 9.73e-01 1.00e+00h 1\n", - " 16 3.3086408e+00 2.78e-05 4.37e-06 -5.7 2.06e-01 -5.0 1.00e+00 1.00e+00h 1\n", - " 17 3.3082900e+00 2.44e-04 3.75e-05 -5.7 6.13e-01 -5.4 1.00e+00 1.00e+00h 1\n", - " 18 3.3073216e+00 2.02e-03 3.10e-04 -5.7 1.78e+00 -5.9 1.00e+00 1.00e+00h 1\n", - " 19 3.3049420e+00 1.48e-02 2.24e-03 -5.7 4.93e+00 -6.4 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 3.3006609e+00 6.58e-02 9.79e-03 -5.7 1.19e+01 -6.9 1.00e+00 8.79e-01h 1\n", - " 21 3.2971545e+00 1.04e-01 2.91e-02 -5.7 1.96e+01 -7.3 1.00e+00 1.00e+00f 1\n", - " 22 3.2963467e+00 1.07e-01 2.86e-02 -5.7 9.98e+01 - 9.97e-01 5.49e-02h 1\n", - " 23 3.2940284e+00 2.58e-01 2.86e-02 -5.7 4.31e+01 - 1.00e+00 1.00e+00f 1\n", - "Reallocating memory for MA57: lfact (328243)\n", - " 24 3.2947114e+00 6.39e-03 2.75e-03 -5.7 5.48e+00 - 1.00e+00 1.00e+00h 1\n", - " 25 3.2947076e+00 2.82e-04 1.21e-05 -5.7 1.48e+00 - 1.00e+00 1.00e+00h 1\n", - " 26 3.2947074e+00 3.77e-07 7.87e-09 -5.7 5.41e-02 - 1.00e+00 1.00e+00h 1\n", - " 27 3.2940558e+00 8.94e-03 7.77e-04 -8.6 8.37e+00 - 9.64e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (351796)\n", - " 28 3.2940328e+00 1.73e-03 1.60e-04 -8.6 3.74e+00 - 1.00e+00 1.00e+00h 1\n", - " 29 3.2940282e+00 5.35e-05 6.70e-06 -8.6 6.62e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 3.2940281e+00 3.41e-08 9.69e-09 -8.6 1.67e-02 - 1.00e+00 1.00e+00h 1\n", - " 31 3.2940281e+00 3.59e-13 6.26e-14 -8.6 2.20e-05 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 31\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 3.2940280640592707e+00 3.2940280640592707e+00\n", - "Dual infeasibility......: 6.2616567740385095e-14 6.2616567740385095e-14\n", - "Constraint violation....: 1.5287940420760863e-13 3.5882408155885059e-13\n", - "Complementarity.........: 2.5059035684585384e-09 2.5059035684585384e-09\n", - "Overall NLP error.......: 2.5059035684585384e-09 2.5059035684585384e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 36\n", - "Number of objective gradient evaluations = 32\n", - "Number of equality constraint evaluations = 36\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 32\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 31\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.195\n", - "Total CPU secs in NLP function evaluations = 0.048\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.9 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.42e-03 1.85e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 2.27e-09 1.51e-06 -1.0 1.79e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.2713546599106849e-09 2.2713546599106849e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.2713546599106849e-09 2.2713546599106849e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.104\n", - "Total CPU secs in NLP function evaluations = 0.007\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.42e-03 1.85e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 2.27e-09 1.51e-06 -1.0 1.79e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.2713533276430553e-09 2.2713533276430553e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.2713533276430553e-09 2.2713533276430553e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.105\n", - "Total CPU secs in NLP function evaluations = 0.002\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 2.30e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 6.00e+02 3.85e+02 -1.0 2.30e+03 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 6.93e+00 3.85e+00 -1.0 6.46e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 6.85e-02 4.39e+00 -1.0 7.41e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.33e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.131\n", - "Total CPU secs in NLP function evaluations = 0.008\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.0333966e+01 1.25e+00 1.09e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.0338708e+01 6.20e-01 5.17e+00 -1.0 4.65e+01 - 9.73e-01 5.04e-01h 1\n", - " 2 -1.0336296e+01 1.28e+00 1.95e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.0354984e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.0317441e+01 1.67e+01 2.27e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.0323555e+01 1.17e+01 3.18e+01 -1.0 1.36e+02 - 1.00e+00 4.16e-01f 2\n", - " 6 -1.0308913e+01 1.22e+01 3.32e+00 -1.0 9.13e+01 - 8.90e-01 3.42e-01f 2\n", - " 7 -1.0295537e+01 3.50e+00 5.22e-01 -1.0 5.14e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.0301363e+01 4.21e-01 7.17e-02 -1.0 1.05e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.0301441e+01 2.63e-04 2.69e-03 -1.7 2.08e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.0302019e+01 3.71e-03 5.57e-02 -3.8 1.37e+00 - 9.96e-01 1.00e+00h 1\n", - " 11 -1.0333647e+01 1.23e+01 1.35e-01 -3.8 8.29e+01 - 1.00e+00 1.00e+00h 1\n", - " 12 -1.0334471e+01 3.28e-01 4.31e-03 -3.8 1.15e+01 - 1.00e+00 1.00e+00h 1\n", - " 13 -1.0335026e+01 4.18e-03 1.95e-05 -3.8 6.62e-01 - 1.00e+00 1.00e+00h 1\n", - " 14 -1.0335021e+01 3.25e-07 2.67e-09 -3.8 1.60e-02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (324090)\n", - " 15 -1.0347510e+01 2.31e+00 2.80e-02 -5.7 3.87e+01 - 7.60e-01 1.00e+00f 1\n", - " 16 -1.0354058e+01 2.41e+00 2.21e-02 -5.7 3.02e+01 - 7.30e-01 1.00e+00h 1\n", - " 17 -1.0356712e+01 3.68e+00 4.66e-02 -5.7 1.52e+02 - 5.86e-01 2.47e-01h 1\n", - " 18 -1.0358821e+01 2.88e-01 4.06e-03 -5.7 1.96e+01 - 1.00e+00 1.00e+00h 1\n", - " 19 -1.0358654e+01 8.94e-03 4.59e-04 -5.7 8.19e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.0358657e+01 2.20e-04 8.10e-06 -5.7 1.30e+00 - 1.00e+00 1.00e+00h 1\n", - " 21 -1.0358657e+01 9.03e-08 2.32e-09 -5.7 2.63e-02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (347964)\n", - " 22 -1.0359304e+01 3.93e-02 7.56e-04 -8.6 9.55e+00 - 9.56e-01 1.00e+00h 1\n", - " 23 -1.0359325e+01 2.75e-03 1.87e-04 -8.6 4.70e+00 - 1.00e+00 1.00e+00h 1\n", - " 24 -1.0359330e+01 1.37e-04 1.18e-05 -8.6 1.06e+00 - 1.00e+00 1.00e+00h 1\n", - " 25 -1.0359330e+01 2.28e-07 3.96e-08 -8.6 4.33e-02 - 1.00e+00 1.00e+00h 1\n", - " 26 -1.0359330e+01 1.70e-12 4.97e-13 -8.6 1.18e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 26\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.0359330165707208e+01 -1.0359330165707208e+01\n", - "Dual infeasibility......: 4.9706657073264839e-13 4.9706657073264839e-13\n", - "Constraint violation....: 1.7005286068183523e-12 1.7005286068183523e-12\n", - "Complementarity.........: 2.5059037505427129e-09 2.5059037505427129e-09\n", - "Overall NLP error.......: 2.5059037505427129e-09 2.5059037505427129e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 33\n", - "Number of objective gradient evaluations = 27\n", - "Number of equality constraint evaluations = 33\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 27\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 26\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.860\n", - "Total CPU secs in NLP function evaluations = 0.024\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.6 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 3.92e+00 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.51e-02 3.85e+00 -1.0 4.21e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.46e-04 4.39e+00 -1.0 4.82e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.76e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8337334962743626e-11 5.8337334962743626e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8337334962743626e-11 5.8337334962743626e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.125\n", - "Total CPU secs in NLP function evaluations = 0.010\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 2.8196379e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 2.8162115e+00 1.65e-01 1.10e+00 -1.0 5.96e-01 - 9.79e-01 8.68e-01h 1\n", - " 2 2.8171449e+00 3.48e-02 1.08e+01 -1.0 6.03e+00 - 8.34e-01 1.00e+00f 1\n", - " 3 2.7994163e+00 1.25e+01 5.72e+01 -1.0 1.80e+02 - 1.92e-01 7.12e-01f 1\n", - " 4 2.8392046e+00 7.17e-01 2.60e+00 -1.0 2.66e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 2.8292019e+00 5.86e-01 4.54e+01 -1.0 3.71e+02 - 4.48e-01 8.32e-02f 2\n", - " 6 2.8540376e+00 1.35e-01 5.27e+00 -1.0 2.40e+01 -4.0 8.21e-01 1.00e+00F 1\n", - " 7 2.8496419e+00 4.28e-01 2.99e-01 -1.0 5.45e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 2.8514802e+00 3.95e-02 5.69e-02 -1.7 1.69e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 2.8509342e+00 4.75e-03 3.32e-03 -2.5 5.95e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 2.8470769e+00 9.20e-03 3.76e-02 -3.8 4.69e+00 - 9.80e-01 1.00e+00h 1\n", - " 11 2.8179475e+00 8.07e-01 7.45e-02 -3.8 3.88e+01 - 1.00e+00 1.00e+00h 1\n", - " 12 2.8189649e+00 2.78e-02 1.87e-03 -3.8 5.72e+00 - 1.00e+00 1.00e+00h 1\n", - " 13 2.8185808e+00 5.85e-05 6.97e-06 -3.8 3.19e-01 - 1.00e+00 1.00e+00h 1\n", - " 14 2.8060943e+00 4.50e-01 2.80e-02 -5.7 3.87e+01 - 7.60e-01 1.00e+00h 1\n", - " 15 2.8076988e+00 1.27e-03 1.40e-03 -5.7 2.15e-01 -4.5 9.84e-01 1.00e+00h 1\n", - " 16 2.8075717e+00 1.61e-05 2.02e-06 -5.7 1.49e-01 -5.0 1.00e+00 1.00e+00h 1\n", - " 17 2.8073034e+00 1.50e-04 2.56e-05 -8.6 4.56e-01 -5.4 9.91e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (332552)\n", - " 18 2.7996057e+00 1.17e+00 3.50e-01 -8.6 1.18e+02 - 2.14e-01 7.03e-01h 1\n", - " 19 2.7992999e+00 1.00e+00 2.99e-01 -8.6 1.21e+02 - 1.40e-01 1.38e-01h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 2.7992903e+00 9.94e-01 2.96e-01 -8.6 3.79e+01 - 7.17e-01 9.13e-03h 1\n", - " 21 2.7990482e+00 6.68e-01 1.97e-01 -8.6 3.87e+01 - 9.68e-01 3.74e-01h 1\n", - " 22 2.7989147e+00 5.66e-01 1.67e-01 -8.6 3.55e+01 - 1.00e+00 1.65e-01f 1\n", - " 23 2.7981330e+00 2.94e-01 8.73e-02 -8.6 3.53e+01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (356955)\n", - " 24 2.7982525e+00 9.54e-03 2.30e-03 -8.6 6.04e-01 -5.9 1.00e+00 1.00e+00h 1\n", - " 25 2.7953817e+00 8.37e-01 2.60e-01 -8.6 8.53e+01 - 7.77e-02 6.80e-01h 1\n", - " 26 2.7970874e+00 5.84e-01 1.64e-01 -8.6 5.21e+01 - 1.00e+00 5.00e-01h 2\n", - " 27 2.7963533e+00 2.95e-02 5.91e-03 -8.6 2.18e+00 -6.4 1.00e+00 1.00e+00h 1\n", - " 28 2.7958325e+00 8.11e-03 1.49e-03 -8.6 5.48e+00 -6.9 1.00e+00 1.00e+00h 1\n", - " 29 2.7945535e+00 7.95e-02 1.40e-02 -8.6 1.74e+01 -7.3 6.23e-01 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 2.7944657e+00 7.83e-02 1.38e-02 -8.6 5.44e+01 -7.8 8.23e-01 1.76e-02h 1\n", - " 31 2.7944608e+00 7.71e-02 1.36e-02 -8.6 7.54e+00 - 8.32e-01 1.59e-02h 1\n", - " 32 2.7941818e+00 6.39e-03 2.32e-03 -8.6 7.11e+00 - 1.00e+00 1.00e+00f 1\n", - " 33 2.7942732e+00 7.14e-05 1.97e-05 -8.6 7.45e-01 - 1.00e+00 1.00e+00h 1\n", - " 34 2.7942736e+00 7.61e-09 1.93e-09 -8.6 7.70e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 34\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 2.7942736259106784e+00 2.7942736259106784e+00\n", - "Dual infeasibility......: 1.9311658899047611e-09 1.9311658899047611e-09\n", - "Constraint violation....: 7.6102699697599974e-09 7.6102699697599974e-09\n", - "Complementarity.........: 2.5060093311148399e-09 2.5060093311148399e-09\n", - "Overall NLP error.......: 7.6102699697599974e-09 7.6102699697599974e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 40\n", - "Number of objective gradient evaluations = 35\n", - "Number of equality constraint evaluations = 40\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 35\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 34\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.244\n", - "Total CPU secs in NLP function evaluations = 0.050\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.41e-03 2.08e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 2.23e-09 1.50e-06 -1.0 1.78e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.2333721538814189e-09 2.2333721538814189e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.2333721538814189e-09 2.2333721538814189e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.099\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.41e-03 2.08e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 2.23e-09 1.50e-06 -1.0 1.78e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.2333717097922090e-09 2.2333717097922090e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.2333717097922090e-09 2.2333717097922090e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.103\n", - "Total CPU secs in NLP function evaluations = 0.003\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 0.8 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.06e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 6.07e+02 3.85e+02 -1.0 3.06e+03 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 7.00e+00 3.85e+00 -1.0 6.54e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 6.93e-02 4.39e+00 -1.0 7.48e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.40e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0381035988684744e-09 9.0381035988684744e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0381035988684744e-09 9.0381035988684744e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.127\n", - "Total CPU secs in NLP function evaluations = 0.007\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.0727169e+01 1.25e+00 1.08e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.0730862e+01 6.19e-01 5.16e+00 -1.0 4.65e+01 - 9.73e-01 5.05e-01h 1\n", - " 2 -1.0728705e+01 1.28e+00 1.95e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.0743551e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.0714096e+01 1.67e+01 2.25e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.0718710e+01 1.18e+01 3.18e+01 -1.0 1.36e+02 - 1.00e+00 4.15e-01f 2\n", - " 6 -1.0707188e+01 1.22e+01 3.32e+00 -1.0 9.16e+01 - 8.86e-01 3.40e-01f 2\n", - " 7 -1.0696529e+01 3.54e+00 5.25e-01 -1.0 5.17e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.0701162e+01 4.28e-01 7.36e-02 -1.0 1.06e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.0701207e+01 2.45e-04 2.30e-03 -1.7 2.53e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.0701573e+01 2.36e-03 2.36e-02 -3.8 1.10e+00 - 9.98e-01 1.00e+00h 1\n", - " 11 -1.0728466e+01 1.41e+01 1.22e-01 -3.8 8.53e+01 - 1.00e+00 1.00e+00h 1\n", - " 12 -1.0724859e+01 5.12e-01 5.81e-03 -3.8 1.89e+01 - 1.00e+00 1.00e+00h 1\n", - " 13 -1.0725400e+01 2.98e-03 1.26e-05 -3.8 1.22e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (319443)\n", - " 14 -1.0725400e+01 2.16e-08 1.65e-09 -3.8 6.85e-03 - 1.00e+00 1.00e+00h 1\n", - " 15 -1.0735769e+01 2.29e+00 2.70e-02 -5.7 3.95e+01 - 7.59e-01 1.00e+00f 1\n", - " 16 -1.0741697e+01 2.18e+00 1.82e-02 -5.7 3.17e+01 - 7.93e-01 1.00e+00h 1\n", - " 17 -1.0742738e+01 2.63e+00 1.88e-02 -5.7 3.36e+02 - 3.31e-01 5.69e-02h 2\n", - " 18 -1.0746014e+01 1.33e+00 1.30e-02 -5.7 3.62e+01 - 1.00e+00 1.00e+00h 1\n", - " 19 -1.0746312e+01 1.77e-01 1.01e-03 -5.7 1.56e+01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (336826)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.0746301e+01 6.01e-04 1.57e-05 -5.7 2.13e+00 - 1.00e+00 1.00e+00h 1\n", - " 21 -1.0746301e+01 5.82e-07 8.09e-09 -5.7 6.66e-02 - 1.00e+00 1.00e+00h 1\n", - " 22 -1.0746946e+01 6.09e-02 7.31e-04 -8.6 1.05e+01 - 9.50e-01 1.00e+00h 1\n", - " 23 -1.0746963e+01 3.80e-03 2.04e-04 -8.6 5.52e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (359506)\n", - " 24 -1.0746969e+01 2.64e-04 1.71e-05 -8.6 1.47e+00 - 1.00e+00 1.00e+00h 1\n", - " 25 -1.0746970e+01 8.56e-07 1.03e-07 -8.6 8.39e-02 - 1.00e+00 1.00e+00h 1\n", - " 26 -1.0746970e+01 1.75e-11 3.92e-12 -8.6 3.79e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 26\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.0746969709499885e+01 -1.0746969709499885e+01\n", - "Dual infeasibility......: 3.9211112624388169e-12 3.9211112624388169e-12\n", - "Constraint violation....: 1.7519652395492358e-11 1.7519652395492358e-11\n", - "Complementarity.........: 2.5059051337529310e-09 2.5059051337529310e-09\n", - "Overall NLP error.......: 2.5059051337529310e-09 2.5059051337529310e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 36\n", - "Number of objective gradient evaluations = 27\n", - "Number of equality constraint evaluations = 36\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 27\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 26\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.940\n", - "Total CPU secs in NLP function evaluations = 0.036\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.7 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.32e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 3.95e+00 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.55e-02 3.85e+00 -1.0 4.25e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.50e-04 4.39e+00 -1.0 4.85e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.80e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.130\n", - "Total CPU secs in NLP function evaluations = 0.008\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 2.4264349e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 2.4238621e+00 1.63e-01 1.09e+00 -1.0 5.95e-01 - 9.79e-01 8.70e-01h 1\n", - " 2 2.4248028e+00 3.48e-02 1.10e+01 -1.0 6.03e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 2.4105783e+00 1.25e+01 5.73e+01 -1.0 1.80e+02 - 1.92e-01 7.12e-01f 1\n", - " 4 2.4418472e+00 7.15e-01 2.57e+00 -1.0 2.66e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 2.4341825e+00 5.85e-01 4.59e+01 -1.0 3.64e+02 - 4.56e-01 8.47e-02f 2\n", - " 6 2.4537211e+00 1.36e-01 5.47e+00 -1.0 2.40e+01 -4.0 8.21e-01 1.00e+00F 1\n", - " 7 2.4503021e+00 4.30e-01 2.96e-01 -1.0 5.46e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 2.4518079e+00 3.92e-02 5.71e-02 -1.7 1.69e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 2.4514688e+00 4.92e-03 3.01e-03 -2.5 6.06e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 2.4490209e+00 5.79e-03 1.11e-02 -3.8 3.70e+00 - 9.94e-01 1.00e+00h 1\n", - " 11 2.4240878e+00 7.98e-01 6.68e-02 -3.8 3.99e+01 - 1.00e+00 1.00e+00h 1\n", - " 12 2.4284887e+00 8.05e-03 3.01e-03 -3.8 7.54e+00 - 1.00e+00 1.00e+00h 1\n", - " 13 2.4282037e+00 3.91e-05 3.61e-06 -3.8 2.51e-01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (321514)\n", - " 14 2.4178352e+00 4.81e-01 2.70e-02 -5.7 3.95e+01 - 7.59e-01 1.00e+00h 1\n", - " 15 2.4119071e+00 2.15e-01 1.82e-02 -5.7 3.17e+01 - 7.93e-01 1.00e+00h 1\n", - " 16 2.4108659e+00 2.12e-01 1.88e-02 -5.7 2.81e+02 - 3.31e-01 5.69e-02h 2\n", - " 17 2.4075899e+00 1.15e-01 1.30e-02 -5.7 2.13e+01 - 1.00e+00 1.00e+00h 1\n", - " 18 2.4072917e+00 1.38e-02 1.01e-03 -5.7 1.01e+01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (337602)\n", - " 19 2.4073026e+00 6.01e-04 1.57e-05 -5.7 2.13e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 2.4073027e+00 5.82e-07 8.09e-09 -5.7 6.66e-02 - 1.00e+00 1.00e+00h 1\n", - " 21 2.4066582e+00 1.43e-02 7.31e-04 -8.6 1.05e+01 - 9.50e-01 1.00e+00h 1\n", - " 22 2.4066404e+00 3.80e-03 2.04e-04 -8.6 5.52e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (360986)\n", - " 23 2.4066344e+00 2.64e-04 1.71e-05 -8.6 1.47e+00 - 1.00e+00 1.00e+00h 1\n", - " 24 2.4066341e+00 8.56e-07 1.03e-07 -8.6 8.39e-02 - 1.00e+00 1.00e+00h 1\n", - " 25 2.4066341e+00 1.75e-11 3.92e-12 -8.6 3.79e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 25\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 2.4066340821203402e+00 2.4066340821203402e+00\n", - "Dual infeasibility......: 3.9211113364714817e-12 3.9211113364714817e-12\n", - "Constraint violation....: 1.7518986261677583e-11 1.7518986261677583e-11\n", - "Complementarity.........: 2.5059051337162099e-09 2.5059051337162099e-09\n", - "Overall NLP error.......: 2.5059051337162099e-09 2.5059051337162099e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 33\n", - "Number of objective gradient evaluations = 26\n", - "Number of equality constraint evaluations = 33\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 26\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 25\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.855\n", - "Total CPU secs in NLP function evaluations = 0.033\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.6 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.39e-03 2.31e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 2.20e-09 1.49e-06 -1.0 1.76e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.1957125007077138e-09 2.1957125007077138e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.1957125007077138e-09 2.1957125007077138e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.102\n", - "Total CPU secs in NLP function evaluations = 0.003\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.39e-03 2.31e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 2.20e-09 1.49e-06 -1.0 1.76e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.1957116125292941e-09 2.1957116125292941e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.1957116125292941e-09 2.1957116125292941e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.099\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.83e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 6.15e+02 3.85e+02 -1.0 3.83e+03 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 7.08e+00 3.85e+00 -1.0 6.62e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 7.00e-02 4.39e+00 -1.0 7.56e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.48e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0390130935702473e-09 9.0390130935702473e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0390130935702473e-09 9.0390130935702473e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.128\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.1047463e+01 1.25e+00 1.08e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.1050486e+01 6.19e-01 5.16e+00 -1.0 4.65e+01 - 9.73e-01 5.05e-01h 1\n", - " 2 -1.1048562e+01 1.28e+00 1.95e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.1060878e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.1036646e+01 1.67e+01 2.24e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.1040342e+01 1.18e+01 3.19e+01 -1.0 1.36e+02 - 1.00e+00 4.15e-01f 2\n", - " 6 -1.1030843e+01 1.22e+01 3.32e+00 -1.0 9.18e+01 - 8.84e-01 3.39e-01f 2\n", - " 7 -1.1021982e+01 3.57e+00 5.28e-01 -1.0 5.19e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.1025828e+01 4.33e-01 7.49e-02 -1.0 1.07e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.1025857e+01 2.38e-04 2.03e-03 -1.7 2.83e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.1026109e+01 1.63e-03 2.25e-03 -3.8 9.21e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 -1.1026632e+01 7.86e-04 4.02e-04 -3.8 1.57e+00 -4.0 1.00e+00 1.00e+00h 1\n", - " 12 -1.1027550e+01 2.46e-03 3.00e-04 -5.7 2.75e+00 -4.5 1.00e+00 1.00e+00h 1\n", - " 13 -1.1028272e+01 2.47e-03 5.48e-04 -5.7 2.72e+00 -5.0 1.00e+00 7.82e-01h 1\n", - " 14 -1.1028813e+01 8.65e-03 7.74e-04 -5.7 1.44e+00 -5.4 1.00e+00 1.00e+00f 1\n", - " 15r-1.1028813e+01 8.65e-03 9.99e+02 -2.1 0.00e+00 - 0.00e+00 3.69e-07R 16\n", - " 16r-1.1028744e+01 1.97e-03 2.91e+02 -2.1 2.14e+02 - 1.00e+00 9.90e-04f 1\n", - " 17 -1.1028797e+01 1.97e-03 7.77e-02 -5.7 7.31e+03 - 6.04e-02 2.22e-05h 2\n", - " 18 -1.1070945e+01 5.57e+01 7.49e-02 -5.7 4.61e+03 - 3.70e-02 3.66e-02h 1\n", - " 19 -1.1077425e+01 5.37e+01 7.00e-02 -5.7 7.35e+02 - 7.21e-02 6.45e-02h 1\n", - "Reallocating memory for MA57: lfact (323295)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.1075942e+01 4.63e+01 6.82e-02 -5.7 2.71e+02 - 1.00e+00 1.71e-01h 1\n", - " 21 -1.1056650e+01 7.71e+00 7.12e-01 -5.7 9.20e+01 - 7.89e-01 1.00e+00h 1\n", - " 22 -1.1061268e+01 4.91e-01 3.85e-02 -5.7 2.95e+01 -5.9 1.00e+00 1.00e+00h 1\n", - " 23 -1.1062198e+01 6.67e-01 2.40e-02 -5.7 3.95e+01 - 1.00e+00 3.17e-01h 2\n", - " 24 -1.1062691e+01 5.52e-01 1.59e-02 -5.7 1.91e+01 - 1.00e+00 3.26e-01h 2\n", - " 25 -1.1063214e+01 1.71e-01 4.96e-03 -5.7 7.52e+00 - 1.00e+00 1.00e+00h 1\n", - " 26 -1.1063041e+01 1.76e-02 8.52e-04 -5.7 4.14e+00 - 1.00e+00 1.00e+00h 1\n", - " 27 -1.1063029e+01 1.30e-04 4.18e-06 -5.7 3.18e-01 - 1.00e+00 1.00e+00h 1\n", - " 28 -1.1063029e+01 3.22e-09 1.10e-10 -5.7 1.63e-03 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (352737)\n", - " 29 -1.1063672e+01 8.69e-02 7.05e-04 -8.6 1.13e+01 - 9.43e-01 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -1.1063687e+01 4.85e-03 2.15e-04 -8.6 6.22e+00 - 1.00e+00 1.00e+00h 1\n", - " 31 -1.1063694e+01 4.30e-04 2.22e-05 -8.6 1.87e+00 - 1.00e+00 1.00e+00h 1\n", - " 32 -1.1063694e+01 2.31e-06 2.07e-07 -8.6 1.38e-01 - 1.00e+00 1.00e+00h 1\n", - " 33 -1.1063694e+01 1.00e-10 1.87e-11 -8.6 9.08e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 33\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.1063694185536061e+01 -1.1063694185536061e+01\n", - "Dual infeasibility......: 1.8708625160522883e-11 1.8708625160522883e-11\n", - "Constraint violation....: 1.0035383635198514e-10 1.0035383635198514e-10\n", - "Complementarity.........: 2.5059110801775927e-09 2.5059110801775927e-09\n", - "Overall NLP error.......: 2.5059110801775927e-09 2.5059110801775927e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 66\n", - "Number of objective gradient evaluations = 34\n", - "Number of equality constraint evaluations = 66\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 35\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 33\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.097\n", - "Total CPU secs in NLP function evaluations = 0.054\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.8 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.63e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 3.99e+00 3.85e+02 -1.0 1.87e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.58e-02 3.85e+00 -1.0 4.28e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.53e-04 4.39e+00 -1.0 4.88e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.83e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8337334962743626e-11 5.8337334962743626e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8337334962743626e-11 5.8337334962743626e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.135\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 2.1061408e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 2.1040894e+00 1.61e-01 1.07e+00 -1.0 5.95e-01 - 9.79e-01 8.71e-01h 1\n", - " 2 2.1049797e+00 3.48e-02 1.10e+01 -1.0 6.04e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 2.0931025e+00 1.25e+01 5.73e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", - " 4 2.1188567e+00 7.15e-01 2.55e+00 -1.0 2.66e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 2.1126515e+00 5.85e-01 4.62e+01 -1.0 3.59e+02 - 4.61e-01 8.58e-02f 2\n", - " 6 2.1287303e+00 1.37e-01 5.55e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", - " 7 2.1259503e+00 4.30e-01 2.96e-01 -1.0 5.46e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 2.1272270e+00 3.91e-02 5.74e-02 -1.7 1.69e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 2.1269980e+00 5.05e-03 3.05e-03 -2.5 6.14e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 2.1253079e+00 3.93e-03 1.72e-03 -3.8 3.02e+00 - 1.00e+00 1.00e+00h 1\n", - " 11 2.1243236e+00 3.24e-05 7.52e-06 -3.8 8.12e-02 -4.5 1.00e+00 1.00e+00h 1\n", - " 12 2.1000010e+00 1.90e+00 1.12e-01 -5.7 6.66e+01 - 5.63e-01 1.00e+00h 1\n", - " 13 2.0975393e+00 2.59e-01 1.56e-02 -5.7 2.55e+01 - 7.81e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (319517)\n", - " 14 2.0918091e+00 4.08e-01 3.59e-02 -5.7 2.58e+01 - 6.49e-01 9.22e-01h 1\n", - " 15 2.0912149e+00 4.07e-01 3.58e-02 -5.7 3.86e+03 - 3.37e-02 2.90e-03h 2\n", - " 16 2.0907195e+00 2.80e-02 2.35e-03 -5.7 1.32e+01 - 1.00e+00 1.00e+00h 1\n", - " 17 2.0905779e+00 7.03e-03 8.20e-04 -5.7 6.05e+00 - 1.00e+00 1.00e+00h 1\n", - " 18 2.0905748e+00 5.81e-05 1.08e-06 -5.7 6.63e-01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (342692)\n", - " 19 2.0905749e+00 3.67e-09 1.14e-10 -5.7 5.27e-03 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 2.0899322e+00 1.67e-02 7.05e-04 -8.6 1.13e+01 - 9.43e-01 1.00e+00h 1\n", - " 21 2.0899164e+00 4.85e-03 2.15e-04 -8.6 6.22e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (360246)\n", - " 22 2.0899101e+00 4.30e-04 2.22e-05 -8.6 1.87e+00 - 1.00e+00 1.00e+00h 1\n", - " 23 2.0899096e+00 2.31e-06 2.07e-07 -8.6 1.38e-01 - 1.00e+00 1.00e+00h 1\n", - " 24 2.0899096e+00 1.00e-10 1.87e-11 -8.6 9.08e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 24\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 2.0899096060902953e+00 2.0899096060902953e+00\n", - "Dual infeasibility......: 1.8708625206653092e-11 1.8708625206653092e-11\n", - "Constraint violation....: 1.0035450248579991e-10 1.0035450248579991e-10\n", - "Complementarity.........: 2.5059110801021708e-09 2.5059110801021708e-09\n", - "Overall NLP error.......: 2.5059110801021708e-09 2.5059110801021708e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 31\n", - "Number of objective gradient evaluations = 25\n", - "Number of equality constraint evaluations = 31\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 25\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 24\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.848\n", - "Total CPU secs in NLP function evaluations = 0.031\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.6 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.38e-03 2.54e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 2.16e-09 1.48e-06 -1.0 1.75e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.1583757003895698e-09 2.1583757003895698e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.1583757003895698e-09 2.1583757003895698e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.103\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.2 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.38e-03 2.54e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 2.16e-09 1.48e-06 -1.0 1.75e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.1583757003895698e-09 2.1583757003895698e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.1583757003895698e-09 2.1583757003895698e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.105\n", - "Total CPU secs in NLP function evaluations = 0.005\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 4.60e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 6.23e+02 3.85e+02 -1.0 4.60e+03 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 7.16e+00 3.85e+00 -1.0 6.69e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 7.08e-02 4.39e+00 -1.0 7.64e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.55e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.122\n", - "Total CPU secs in NLP function evaluations = 0.009\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.1317732e+01 1.25e+00 1.07e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.1320290e+01 6.18e-01 5.16e+00 -1.0 4.65e+01 - 9.73e-01 5.05e-01h 1\n", - " 2 -1.1318564e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.1329088e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.1308507e+01 1.67e+01 2.23e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.1311584e+01 1.18e+01 3.19e+01 -1.0 1.36e+02 - 1.00e+00 4.14e-01f 2\n", - " 6 -1.1303505e+01 1.23e+01 3.32e+00 -1.0 9.19e+01 - 8.82e-01 3.38e-01f 2\n", - " 7 -1.1295920e+01 3.59e+00 5.30e-01 -1.0 5.20e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.1299210e+01 4.37e-01 7.59e-02 -1.0 1.08e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.1299229e+01 2.36e-04 1.85e-03 -1.7 3.04e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.1299413e+01 1.20e-03 1.91e-03 -3.8 7.92e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 -1.1299812e+01 6.26e-04 3.20e-04 -3.8 1.40e+00 -4.0 1.00e+00 1.00e+00h 1\n", - " 12 -1.1322678e+01 1.98e+01 1.23e-01 -5.7 8.86e+01 - 5.46e-01 1.00e+00h 1\n", - " 13 -1.1325322e+01 1.20e+00 1.52e-02 -5.7 6.84e+01 - 7.61e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (323203)\n", - " 14 -1.1329152e+01 3.27e+00 1.89e-02 -5.7 3.81e+01 - 7.29e-01 1.00e+00h 1\n", - " 15 -1.1329720e+01 3.61e+00 1.87e-02 -5.7 8.73e+02 - 1.61e-01 1.97e-02h 2\n", - " 16 -1.1330564e+01 3.94e-01 2.15e-03 -5.7 2.39e+01 - 1.00e+00 1.00e+00h 1\n", - " 17 -1.1330828e+01 1.37e-01 1.02e-03 -5.7 1.22e+01 - 1.00e+00 1.00e+00h 1\n", - " 18 -1.1330818e+01 1.25e-04 4.23e-06 -5.7 5.25e-01 - 1.00e+00 1.00e+00h 1\n", - " 19 -1.1330818e+01 4.23e-09 1.18e-10 -5.7 2.97e-03 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.1330839e+01 4.74e-06 1.48e-06 -8.6 4.43e-02 -4.5 1.00e+00 1.00e+00h 1\n", - " 21 -1.1330851e+01 4.21e-05 2.23e-06 -8.6 1.69e-01 -5.0 1.00e+00 1.00e+00h 1\n", - " 22 -1.1330889e+01 3.75e-04 3.31e-06 -8.6 5.08e-01 -5.4 1.00e+00 1.00e+00h 1\n", - " 23 -1.1330999e+01 3.26e-03 2.88e-05 -8.6 1.52e+00 -5.9 1.00e+00 1.00e+00h 1\n", - " 24 -1.1331064e+01 3.74e-03 3.29e-05 -8.6 4.44e+00 -6.4 1.00e+00 2.10e-01h 1\n", - " 25 -1.1331228e+01 1.65e-02 2.12e-04 -8.6 4.71e+00 -6.9 1.00e+00 7.65e-01f 1\n", - " 26 -1.1331449e+01 1.71e-02 7.80e-04 -8.6 2.93e+01 - 1.00e+00 3.63e-01f 1\n", - " 27 -1.1331483e+01 1.98e-02 6.89e-04 -8.6 2.97e+01 - 1.00e+00 2.88e-01f 1\n", - " 28 -1.1331484e+01 1.83e-02 6.23e-04 -8.6 1.76e+01 - 1.00e+00 9.54e-02f 1\n", - " 29 -1.1331489e+01 7.02e-03 1.41e-04 -8.6 3.15e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (355308)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -1.1331481e+01 2.07e-06 4.96e-08 -8.6 1.21e-01 - 1.00e+00 1.00e+00h 1\n", - " 31 -1.1331481e+01 4.19e-11 6.10e-14 -8.6 5.87e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 31\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.1331480835989968e+01 -1.1331480835989968e+01\n", - "Dual infeasibility......: 6.1028774360629264e-14 6.1028774360629264e-14\n", - "Constraint violation....: 4.1914582915580922e-11 4.1914582915580922e-11\n", - "Complementarity.........: 2.5059039158044294e-09 2.5059039158044294e-09\n", - "Overall NLP error.......: 2.5059039158044294e-09 2.5059039158044294e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 41\n", - "Number of objective gradient evaluations = 32\n", - "Number of equality constraint evaluations = 41\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 32\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 31\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.143\n", - "Total CPU secs in NLP function evaluations = 0.040\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.8 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.97e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 4.02e+00 3.85e+02 -1.0 1.92e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.61e-02 3.85e+00 -1.0 4.31e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.56e-04 4.39e+00 -1.0 4.91e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.86e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8332005892225425e-11 5.8332005892225425e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8332005892225425e-11 5.8332005892225425e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.128\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 1.8358716e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 1.8341696e+00 1.60e-01 1.07e+00 -1.0 5.95e-01 - 9.79e-01 8.72e-01h 1\n", - " 2 1.8349973e+00 3.48e-02 1.11e+01 -1.0 6.04e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 1.8248024e+00 1.25e+01 5.74e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", - " 4 1.8466954e+00 7.14e-01 2.53e+00 -1.0 2.66e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 1.8414860e+00 5.85e-01 4.65e+01 -1.0 3.56e+02 - 4.65e-01 8.65e-02f 2\n", - " 6 1.8551941e+00 1.36e-01 5.72e+00 -1.0 2.40e+01 -4.0 8.22e-01 1.00e+00F 1\n", - " 7 1.8528188e+00 4.32e-01 2.96e-01 -1.0 5.47e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 1.8539217e+00 3.88e-02 5.74e-02 -1.7 1.68e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 1.8537565e+00 5.12e-03 3.24e-03 -2.5 6.19e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 1.8525196e+00 2.82e-03 1.42e-03 -3.8 2.54e+00 - 1.00e+00 1.00e+00h 1\n", - " 11 1.8366692e+00 5.04e-01 3.65e-02 -3.8 3.44e+01 - 1.00e+00 1.00e+00h 1\n", - " 12 1.8399503e+00 1.30e-02 1.92e-03 -3.8 6.01e+00 - 1.00e+00 1.00e+00h 1\n", - " 13 1.8398900e+00 1.11e-05 6.33e-07 -3.8 2.77e-01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (320183)\n", - " 14 1.8323517e+00 4.84e-01 2.54e-02 -5.7 3.88e+01 - 7.65e-01 1.00e+00h 1\n", - " 15 1.8270480e+00 3.24e-01 1.26e-02 -5.7 3.83e+01 - 8.75e-01 1.00e+00h 1\n", - " 16 1.8263301e+00 3.14e-01 1.52e-02 -5.7 5.98e+02 - 1.79e-01 2.87e-02h 2\n", - " 17 1.8227920e+00 2.59e-01 1.45e-02 -5.7 2.36e+01 - 1.00e+00 1.00e+00h 1\n", - " 18 1.8227886e+00 1.83e-02 5.07e-04 -5.7 1.16e+01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (338938)\n", - " 19 1.8227858e+00 8.79e-04 1.36e-05 -5.7 2.56e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 1.8227857e+00 9.53e-07 6.96e-09 -5.7 8.47e-02 - 1.00e+00 1.00e+00h 1\n", - " 21 1.8221441e+00 1.90e-02 6.81e-04 -8.6 1.20e+01 - 9.37e-01 1.00e+00h 1\n", - " 22 1.8221300e+00 5.89e-03 2.23e-04 -8.6 6.84e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (356229)\n", - " 23 1.8221235e+00 6.31e-04 2.70e-05 -8.6 2.27e+00 - 1.00e+00 1.00e+00h 1\n", - " 24 1.8221230e+00 5.04e-06 3.54e-07 -8.6 2.03e-01 - 1.00e+00 1.00e+00h 1\n", - " 25 1.8221230e+00 3.93e-10 6.30e-11 -8.6 1.80e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 25\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 1.8221229556418135e+00 1.8221229556418135e+00\n", - "Dual infeasibility......: 6.2966011365859705e-11 6.2966011365859705e-11\n", - "Constraint violation....: 3.9269965057542322e-10 3.9269965057542322e-10\n", - "Complementarity.........: 2.5059288062561410e-09 2.5059288062561410e-09\n", - "Overall NLP error.......: 2.5059288062561410e-09 2.5059288062561410e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 33\n", - "Number of objective gradient evaluations = 26\n", - "Number of equality constraint evaluations = 33\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 26\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 25\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.855\n", - "Total CPU secs in NLP function evaluations = 0.037\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.7 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.37e-03 2.77e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 2.12e-09 1.47e-06 -1.0 1.73e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.1213626411054065e-09 2.1213626411054065e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.1213626411054065e-09 2.1213626411054065e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.107\n", - "Total CPU secs in NLP function evaluations = 0.001\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.37e-03 2.77e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 2.12e-09 1.47e-06 -1.0 1.73e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.1213617529269868e-09 2.1213617529269868e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.1213617529269868e-09 2.1213617529269868e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", - "Total CPU secs in NLP function evaluations = 0.007\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 0.9 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 5.36e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 6.30e+02 3.85e+02 -1.0 5.36e+03 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 7.23e+00 3.85e+00 -1.0 6.77e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 7.15e-02 4.39e+00 -1.0 7.71e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.63e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.134\n", - "Total CPU secs in NLP function evaluations = 0.002\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.1551525e+01 1.25e+00 1.07e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.1553742e+01 6.18e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", - " 2 -1.1552181e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.1561368e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.1543482e+01 1.67e+01 2.22e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.1546117e+01 1.18e+01 3.19e+01 -1.0 1.36e+02 - 1.00e+00 4.14e-01f 2\n", - " 6 -1.1539087e+01 1.23e+01 3.32e+00 -1.0 9.20e+01 - 8.80e-01 3.37e-01f 2\n", - " 7 -1.1532457e+01 3.60e+00 5.31e-01 -1.0 5.21e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.1535331e+01 4.40e-01 7.66e-02 -1.0 1.08e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.1535344e+01 2.36e-04 1.71e-03 -1.7 3.20e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.1535485e+01 9.17e-04 1.68e-03 -3.8 6.95e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 -1.1550192e+01 1.09e+01 5.80e-02 -3.8 6.98e+01 - 1.00e+00 1.00e+00h 1\n", - " 12 -1.1547006e+01 6.44e-01 3.84e-03 -3.8 1.74e+01 - 1.00e+00 1.00e+00h 1\n", - " 13 -1.1547077e+01 6.58e-05 2.09e-06 -3.8 5.97e-01 - 1.00e+00 1.00e+00h 1\n", - " 14 -1.1553620e+01 2.12e+00 2.46e-02 -5.7 3.77e+01 - 7.69e-01 1.00e+00h 1\n", - " 15 -1.1558721e+01 2.04e+00 1.06e-02 -5.7 4.14e+01 - 9.03e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (322351)\n", - " 16 -1.1559371e+01 2.71e+00 1.30e-02 -5.7 6.60e+02 - 1.85e-01 3.02e-02h 2\n", - " 17 -1.1562122e+01 1.66e+00 1.35e-02 -5.7 6.81e+01 - 1.00e+00 7.45e-01H 1\n", - " 18 -1.1562895e+01 1.70e-01 1.76e-03 -5.7 1.75e+01 - 1.00e+00 1.00e+00f 1\n", - " 19 -1.1562787e+01 1.94e-03 2.65e-05 -5.7 3.56e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.1562787e+01 3.18e-06 1.64e-08 -5.7 1.32e-01 - 1.00e+00 1.00e+00h 1\n", - " 21 -1.1563428e+01 1.52e-01 7.00e-04 -8.6 1.27e+01 - 9.31e-01 9.99e-01h 1\n", - "Reallocating memory for MA57: lfact (365434)\n", - " 22 -1.1563441e+01 6.92e-03 2.29e-04 -8.6 7.40e+00 - 1.00e+00 1.00e+00h 1\n", - " 23 -1.1563447e+01 8.62e-04 3.14e-05 -8.6 2.65e+00 - 1.00e+00 1.00e+00h 1\n", - " 24 -1.1563448e+01 9.50e-06 5.44e-07 -8.6 2.79e-01 - 1.00e+00 1.00e+00h 1\n", - " 25 -1.1563448e+01 1.19e-09 1.68e-10 -8.6 3.13e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 25\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.1563448115914573e+01 -1.1563448115914573e+01\n", - "Dual infeasibility......: 1.6756190666721757e-10 1.6756190666721757e-10\n", - "Constraint violation....: 1.1895755491764248e-09 1.1895755491764248e-09\n", - "Complementarity.........: 2.5059705354210223e-09 2.5059705354210223e-09\n", - "Overall NLP error.......: 2.5059705354210223e-09 2.5059705354210223e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 36\n", - "Number of objective gradient evaluations = 26\n", - "Number of equality constraint evaluations = 36\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 26\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 25\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.877\n", - "Total CPU secs in NLP function evaluations = 0.040\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.7 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 2.31e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 4.05e+00 3.85e+02 -1.0 2.26e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.64e-02 3.85e+00 -1.0 4.34e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.59e-04 4.39e+00 -1.0 4.95e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.89e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8339111319583026e-11 5.8339111319583026e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8339111319583026e-11 5.8339111319583026e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.130\n", - "Total CPU secs in NLP function evaluations = 0.005\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 1.6020788e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 1.6006263e+00 1.59e-01 1.06e+00 -1.0 5.95e-01 - 9.79e-01 8.73e-01h 1\n", - " 2 1.6013928e+00 3.48e-02 1.11e+01 -1.0 6.04e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 1.5924626e+00 1.25e+01 5.74e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", - " 4 1.6115012e+00 7.14e-01 2.52e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 1.6070137e+00 5.85e-01 4.67e+01 -1.0 3.54e+02 - 4.67e-01 8.70e-02f 2\n", - " 6 1.6189485e+00 1.36e-01 5.81e+00 -1.0 2.40e+01 -4.0 8.22e-01 1.00e+00F 1\n", - " 7 1.6168842e+00 4.32e-01 2.97e-01 -1.0 5.47e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 1.6178560e+00 3.87e-02 5.74e-02 -1.7 1.68e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 1.6177317e+00 5.19e-03 3.40e-03 -2.5 6.23e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 1.6167878e+00 2.10e-03 1.20e-03 -3.8 2.17e+00 - 1.00e+00 1.00e+00h 1\n", - " 11 1.6042274e+00 4.07e-01 2.56e-02 -3.8 3.07e+01 - 1.00e+00 1.00e+00h 1\n", - " 12 1.6065666e+00 8.43e-03 1.13e-03 -3.8 4.88e+00 - 1.00e+00 1.00e+00h 1\n", - " 13 1.6065263e+00 4.74e-06 3.38e-07 -3.8 1.65e-01 - 1.00e+00 1.00e+00h 1\n", - " 14 1.6002214e+00 9.01e-01 7.69e-02 -5.7 9.37e+03 - 1.90e-02 7.73e-03f 1\n", - "Reallocating memory for MA57: lfact (349533)\n", - " 15 1.5979843e+00 1.46e-01 1.53e-02 -5.7 3.06e+01 - 8.53e-01 1.00e+00h 1\n", - " 16 1.5934679e+00 3.30e-01 2.00e-02 -5.7 4.37e+01 - 8.47e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (378883)\n", - " 17 1.5941729e+00 2.50e-03 1.57e-04 -5.7 3.14e-01 -4.5 1.00e+00 1.00e+00h 1\n", - " 18 1.5941556e+00 1.90e-06 8.52e-07 -5.7 7.65e-02 -5.0 1.00e+00 1.00e+00h 1\n", - " 19 1.5941055e+00 2.45e-05 6.85e-06 -8.6 1.93e-01 -5.4 9.98e-01 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 1.5940068e+00 2.13e-04 1.22e-05 -8.6 5.73e-01 -5.9 1.00e+00 1.00e+00h 1\n", - " 21 1.5937364e+00 1.77e-03 1.01e-04 -8.6 1.67e+00 -6.4 1.00e+00 1.00e+00h 1\n", - " 22 1.5931017e+00 1.24e-02 7.08e-04 -8.6 4.73e+00 -6.9 1.00e+00 9.59e-01h 1\n", - " 23 1.5922166e+00 4.10e-02 2.42e-03 -8.6 1.30e+01 -7.3 1.00e+00 6.17e-01f 1\n", - " 24 1.5911477e+00 1.11e-01 1.25e-02 -8.6 1.93e+01 -7.8 1.00e+00 1.00e+00f 1\n", - " 25 1.5905350e+00 3.48e-02 2.72e-03 -8.6 9.72e+00 -7.4 1.00e+00 1.00e+00h 1\n", - " 26 1.5903800e+00 3.48e-02 2.76e-03 -8.6 4.21e+01 -7.9 1.00e+00 7.61e-02h 1\n", - " 27 1.5901978e+00 7.47e-02 1.56e-03 -8.6 5.28e+01 - 1.00e+00 4.36e-01f 1\n", - " 28 1.5901521e+00 1.15e-01 6.10e-04 -8.6 4.20e+01 - 1.00e+00 6.10e-01f 1\n", - " 29 1.5901546e+00 9.61e-03 5.06e-05 -8.6 8.82e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (415680)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 1.5901559e+00 6.03e-03 5.58e-06 -8.6 6.93e+00 - 1.00e+00 1.00e+00h 1\n", - " 31 1.5901557e+00 3.61e-04 2.83e-07 -8.6 1.72e+00 - 1.00e+00 1.00e+00h 1\n", - " 32 1.5901557e+00 1.01e-06 7.50e-10 -8.6 9.08e-02 - 1.00e+00 1.00e+00h 1\n", - " 33 1.5901557e+00 6.89e-12 4.35e-14 -8.6 2.38e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 33\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 1.5901556757175646e+00 1.5901556757175646e+00\n", - "Dual infeasibility......: 4.3539707029773698e-14 4.3539707029773698e-14\n", - "Constraint violation....: 6.8930416929902094e-12 6.8930416929902094e-12\n", - "Complementarity.........: 2.5059036071950574e-09 2.5059036071950574e-09\n", - "Overall NLP error.......: 2.5059036071950574e-09 2.5059036071950574e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 38\n", - "Number of objective gradient evaluations = 34\n", - "Number of equality constraint evaluations = 38\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 34\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 33\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.558\n", - "Total CPU secs in NLP function evaluations = 0.050\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.2 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.36e-03 3.00e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 2.08e-09 1.46e-06 -1.0 1.72e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.0846711024091746e-09 2.0846711024091746e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.0846711024091746e-09 2.0846711024091746e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", - "Total CPU secs in NLP function evaluations = 0.003\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.36e-03 3.00e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 2.08e-09 1.46e-06 -1.0 1.72e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.0846711024091746e-09 2.0846711024091746e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.0846711024091746e-09 2.0846711024091746e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.115\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.13e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 6.38e+02 3.85e+02 -1.0 6.13e+03 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 7.31e+00 3.85e+00 -1.0 6.85e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 7.23e-02 4.39e+00 -1.0 7.79e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.70e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.134\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.1757533e+01 1.25e+00 1.06e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.1759489e+01 6.18e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", - " 2 -1.1758067e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.1766218e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.1750403e+01 1.67e+01 2.21e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.1752706e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.13e-01f 2\n", - " 6 -1.1746485e+01 1.23e+01 3.32e+00 -1.0 9.21e+01 - 8.79e-01 3.37e-01f 2\n", - " 7 -1.1740596e+01 3.61e+00 5.32e-01 -1.0 5.22e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.1743147e+01 4.42e-01 7.72e-02 -1.0 1.09e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.1743157e+01 2.36e-04 1.60e-03 -1.7 3.33e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.1743267e+01 7.25e-04 1.49e-03 -3.8 6.20e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 -1.1755095e+01 8.91e+00 4.24e-02 -5.7 6.38e+01 - 6.52e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (321581)\n", - " 12 -1.1761884e+01 2.85e+00 1.21e-02 -5.7 5.80e+01 - 8.37e-01 1.00e+00h 1\n", - " 13 -1.1766537e+01 6.45e+00 3.35e-02 -5.7 4.38e+01 - 7.40e-01 1.00e+00h 1\n", - " 14 -1.1766408e+01 5.42e+00 4.34e-02 -5.7 1.45e+01 -4.0 9.76e-01 1.60e-01h 1\n", - " 15 -1.1766359e+01 4.32e-01 3.46e-02 -5.7 2.48e+01 - 1.00e+00 1.00e+00h 1\n", - " 16 -1.1766255e+01 4.65e-04 1.06e-04 -5.7 2.81e-01 -4.5 1.00e+00 1.00e+00h 1\n", - " 17 -1.1766639e+01 3.37e-01 3.56e-03 -5.7 2.92e+02 - 4.58e-01 5.22e-02h 2\n", - " 18 -1.1767572e+01 9.44e-02 2.21e-03 -5.7 1.59e+01 - 1.00e+00 1.00e+00h 1\n", - " 19 -1.1767397e+01 5.01e-03 9.03e-05 -5.7 3.57e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.1767399e+01 1.54e-05 1.56e-06 -5.7 2.76e-01 - 1.00e+00 1.00e+00h 1\n", - " 21 -1.1768035e+01 1.89e-01 7.56e-04 -8.6 1.38e+01 - 9.26e-01 9.95e-01h 1\n", - "Reallocating memory for MA57: lfact (343029)\n", - " 22 -1.1768050e+01 8.01e-03 2.35e-04 -8.6 7.95e+00 - 1.00e+00 1.00e+00h 1\n", - " 23 -1.1768057e+01 1.12e-03 3.57e-05 -8.6 3.02e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (360465)\n", - " 24 -1.1768058e+01 1.63e-05 7.80e-07 -8.6 3.65e-01 - 1.00e+00 1.00e+00h 1\n", - " 25 -1.1768058e+01 3.05e-09 3.84e-10 -8.6 5.01e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 25\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.1768057697799625e+01 -1.1768057697799625e+01\n", - "Dual infeasibility......: 3.8359587305625804e-10 3.8359587305625804e-10\n", - "Constraint violation....: 3.0509063053685281e-09 3.0509063053685281e-09\n", - "Complementarity.........: 2.5060563776015687e-09 2.5060563776015687e-09\n", - "Overall NLP error.......: 3.0509063053685281e-09 3.0509063053685281e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 35\n", - "Number of objective gradient evaluations = 26\n", - "Number of equality constraint evaluations = 35\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 26\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 25\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.901\n", - "Total CPU secs in NLP function evaluations = 0.037\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.7 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 2.65e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 4.08e+00 3.85e+02 -1.0 2.60e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.67e-02 3.85e+00 -1.0 4.37e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.62e-04 4.39e+00 -1.0 4.98e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.92e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.133\n", - "Total CPU secs in NLP function evaluations = 0.005\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 1.3960708e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 1.3948050e+00 1.58e-01 1.05e+00 -1.0 5.95e-01 - 9.79e-01 8.73e-01h 1\n", - " 2 1.3955154e+00 3.48e-02 1.12e+01 -1.0 6.04e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 1.3875708e+00 1.25e+01 5.74e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", - " 4 1.4044133e+00 7.13e-01 2.51e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 1.4004729e+00 5.85e-01 4.68e+01 -1.0 3.52e+02 - 4.69e-01 8.75e-02f 2\n", - " 6 1.4110288e+00 1.36e-01 5.85e+00 -1.0 2.40e+01 -4.0 8.22e-01 1.00e+00F 1\n", - " 7 1.4092120e+00 4.33e-01 2.97e-01 -1.0 5.48e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 1.4100813e+00 3.86e-02 5.75e-02 -1.7 1.68e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 1.4099850e+00 5.24e-03 3.52e-03 -2.5 6.26e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 1.4092414e+00 1.61e-03 1.03e-03 -3.8 1.89e+00 - 1.00e+00 1.00e+00h 1\n", - " 11 1.4087035e+00 1.89e-05 1.98e-06 -3.8 5.64e-02 -4.5 1.00e+00 1.00e+00h 1\n", - " 12 1.3969745e+00 9.17e-01 3.82e-02 -5.7 4.48e+01 - 6.43e-01 1.00e+00h 1\n", - " 13 1.3917215e+00 4.19e-01 9.20e-03 -5.7 3.84e+01 - 8.02e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (324236)\n", - " 14 1.3869756e+00 5.56e-01 3.16e-02 -5.7 2.67e+01 - 7.65e-01 1.00e+00h 1\n", - " 15 1.3879570e+00 9.86e-03 5.26e-04 -5.7 5.35e-01 -5.0 1.00e+00 1.00e+00h 1\n", - " 16 1.3879136e+00 3.18e-06 4.27e-07 -5.7 6.95e-02 -5.4 1.00e+00 1.00e+00h 1\n", - " 17 1.3878380e+00 9.80e-05 2.86e-05 -8.6 3.89e-01 -5.9 9.93e-01 1.00e+00h 1\n", - " 18 1.3876686e+00 9.18e-04 5.19e-05 -8.6 1.23e+00 -6.4 1.00e+00 1.00e+00h 1\n", - " 19 1.3875598e+00 9.24e-04 5.14e-05 -8.6 1.98e+00 -6.9 1.00e+00 3.25e-01h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 1.3870548e+00 1.12e-02 6.48e-04 -8.6 4.76e+00 -7.3 1.00e+00 1.00e+00f 1\n", - " 21 1.3861210e+00 1.22e-01 1.05e-02 -8.6 2.04e+01 -7.8 1.00e+00 1.00e+00h 1\n", - " 22 1.3856832e+00 2.64e-02 2.12e-03 -8.6 9.07e+00 -7.4 1.00e+00 9.62e-01h 1\n", - " 23 1.3856413e+00 2.12e-02 1.73e-03 -8.6 3.86e+01 - 1.00e+00 2.02e-01f 1\n", - "Reallocating memory for MA57: lfact (344436)\n", - " 24 1.3856015e+00 1.68e-02 1.37e-03 -8.6 4.03e+01 - 1.00e+00 2.09e-01f 1\n", - " 25 1.3855581e+00 5.06e-02 6.97e-04 -8.6 3.68e+01 - 1.00e+00 4.94e-01f 1\n", - " 26 1.3855412e+00 1.19e-03 6.42e-05 -8.6 3.12e+00 - 1.00e+00 1.00e+00h 1\n", - " 27 1.3855461e+00 9.23e-04 7.59e-07 -8.6 2.74e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (362997)\n", - " 28 1.3855461e+00 9.77e-06 6.77e-09 -8.6 2.83e-01 - 1.00e+00 1.00e+00h 1\n", - " 29 1.3855461e+00 7.29e-10 5.06e-13 -8.6 2.45e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 29\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 1.3855460938286592e+00 1.3855460938286592e+00\n", - "Dual infeasibility......: 5.0636527904345562e-13 5.0636527904345562e-13\n", - "Constraint violation....: 7.2855699251306305e-10 7.2855699251306305e-10\n", - "Complementarity.........: 2.5059081084119374e-09 2.5059081084119374e-09\n", - "Overall NLP error.......: 2.5059081084119374e-09 2.5059081084119374e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 34\n", - "Number of objective gradient evaluations = 30\n", - "Number of equality constraint evaluations = 34\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 30\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 29\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.307\n", - "Total CPU secs in NLP function evaluations = 0.036\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.35e-03 3.23e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 2.05e-09 1.46e-06 -1.0 1.70e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.0483019724792939e-09 2.0483019724792939e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.0483019724792939e-09 2.0483019724792939e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.099\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.35e-03 3.23e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 2.05e-09 1.46e-06 -1.0 1.70e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.0483028606577136e-09 2.0483028606577136e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.0483028606577136e-09 2.0483028606577136e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.099\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.89e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 6.46e+02 3.85e+02 -1.0 6.89e+03 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 7.39e+00 3.85e+00 -1.0 6.92e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 7.30e-02 4.39e+00 -1.0 7.87e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.78e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0394678409211338e-09 9.0394678409211338e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0394678409211338e-09 9.0394678409211338e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.126\n", - "Total CPU secs in NLP function evaluations = 0.005\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.1941668e+01 1.25e+00 1.06e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.1943417e+01 6.18e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", - " 2 -1.1942113e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.1949438e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.1935265e+01 1.67e+01 2.21e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.1937309e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.13e-01f 2\n", - " 6 -1.1931730e+01 1.23e+01 3.32e+00 -1.0 9.21e+01 - 8.78e-01 3.37e-01f 2\n", - " 7 -1.1926433e+01 3.62e+00 5.33e-01 -1.0 5.22e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.1928727e+01 4.44e-01 7.77e-02 -1.0 1.09e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.1928733e+01 2.37e-04 1.51e-03 -1.7 3.43e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.1928823e+01 5.88e-04 1.35e-03 -3.8 5.59e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 -1.1929034e+01 3.59e-04 1.83e-04 -5.7 1.06e+00 -4.0 1.00e+00 1.00e+00h 1\n", - " 12 -1.1929754e+01 4.19e-03 1.21e-04 -5.7 3.63e+00 -4.5 1.00e+00 1.00e+00h 1\n", - " 13 -1.1930039e+01 3.14e-03 1.97e-03 -5.7 3.45e+00 -5.0 1.00e+00 4.14e-01h 1\n", - " 14 -1.1930215e+01 3.02e-03 9.79e-04 -5.7 8.14e-01 -5.4 1.00e+00 1.00e+00f 1\n", - " 15 -1.1930791e+01 2.61e-02 4.88e-05 -5.7 2.66e+00 -5.9 1.00e+00 1.00e+00h 1\n", - " 16 -1.1939335e+01 1.10e+01 4.69e-02 -5.7 8.17e+03 - 1.91e-02 9.26e-03h 1\n", - " 17 -1.1943119e+01 1.42e+01 5.92e-02 -5.7 4.18e+02 - 4.56e-01 7.84e-02h 1\n", - " 18 -1.1956668e+01 1.38e+01 5.53e-02 -5.7 1.09e+02 - 1.00e+00 8.15e-01h 1\n", - " 19 -1.1949881e+01 3.43e+00 1.16e-02 -5.7 5.57e+01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (321695)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.1950414e+01 8.23e-03 6.43e-04 -5.7 6.89e+00 - 1.00e+00 1.00e+00h 1\n", - " 21 -1.1950429e+01 3.89e-04 2.65e-05 -5.7 1.74e+00 - 1.00e+00 1.00e+00h 1\n", - " 22 -1.1950430e+01 6.41e-07 3.85e-08 -5.7 7.08e-02 - 1.00e+00 1.00e+00h 1\n", - " 23 -1.1951063e+01 2.31e-01 8.26e-04 -8.6 1.51e+01 - 9.21e-01 9.90e-01h 1\n", - "Reallocating memory for MA57: lfact (348619)\n", - " 24 -1.1951080e+01 9.09e-03 2.41e-04 -8.6 8.45e+00 - 1.00e+00 1.00e+00h 1\n", - " 25 -1.1951086e+01 1.41e-03 3.96e-05 -8.6 3.38e+00 - 1.00e+00 1.00e+00h 1\n", - " 26 -1.1951087e+01 2.57e-05 1.05e-06 -8.6 4.59e-01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (367172)\n", - " 27 -1.1951087e+01 7.10e-09 7.68e-10 -8.6 7.64e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 27\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.1951087152217010e+01 -1.1951087152217010e+01\n", - "Dual infeasibility......: 7.6785714050494160e-10 7.6785714050494160e-10\n", - "Constraint violation....: 7.1010632929358053e-09 7.1010632929358053e-09\n", - "Complementarity.........: 2.5062084307979245e-09 2.5062084307979245e-09\n", - "Overall NLP error.......: 7.1010632929358053e-09 7.1010632929358053e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 34\n", - "Number of objective gradient evaluations = 28\n", - "Number of equality constraint evaluations = 34\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 28\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 27\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.951\n", - "Total CPU secs in NLP function evaluations = 0.031\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.7 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 2.99e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 4.11e+00 3.85e+02 -1.0 2.94e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.71e-02 3.85e+00 -1.0 4.41e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.65e-04 4.39e+00 -1.0 5.01e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.95e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.135\n", - "Total CPU secs in NLP function evaluations = 0.002\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 1.2119360e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 1.2108149e+00 1.58e-01 1.05e+00 -1.0 5.95e-01 - 9.79e-01 8.74e-01h 1\n", - " 2 1.2114751e+00 3.48e-02 1.12e+01 -1.0 6.04e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 1.2043202e+00 1.25e+01 5.74e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", - " 4 1.2194210e+00 7.13e-01 2.50e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 1.2159091e+00 5.85e-01 4.69e+01 -1.0 3.51e+02 - 4.71e-01 8.78e-02f 2\n", - " 6 1.2253787e+00 1.36e-01 5.90e+00 -1.0 2.40e+01 -4.0 8.22e-01 1.00e+00F 1\n", - " 7 1.2237509e+00 4.33e-01 2.97e-01 -1.0 5.48e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 1.2245368e+00 3.86e-02 5.75e-02 -1.7 1.68e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 1.2244603e+00 5.28e-03 3.61e-03 -2.5 6.28e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 1.2238595e+00 1.27e-03 8.99e-04 -3.8 1.66e+00 - 1.00e+00 1.00e+00h 1\n", - " 11 1.2147814e+00 6.50e-01 3.09e-02 -5.7 3.86e+01 - 6.91e-01 1.00e+00h 1\n", - " 12 1.2082820e+00 6.40e-01 9.84e-03 -5.7 4.61e+01 - 8.50e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (319971)\n", - " 13 1.2037280e+00 6.91e-01 3.73e-02 -5.7 2.97e+01 - 7.52e-01 9.98e-01h 1\n", - " 14 1.2048711e+00 1.91e-02 1.61e-03 -5.7 3.27e+00 -4.5 9.31e-01 1.00e+00h 1\n", - " 15 1.2048031e+00 2.24e-06 5.66e-07 -5.7 2.40e-02 -5.0 1.00e+00 1.00e+00h 1\n", - " 16 1.2047671e+00 1.36e-05 6.26e-06 -8.6 1.52e-01 -5.4 9.99e-01 1.00e+00h 1\n", - " 17 1.2047082e+00 1.26e-04 6.76e-06 -8.6 4.70e-01 -5.9 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (336051)\n", - " 18 1.2045445e+00 1.05e-03 5.65e-05 -8.6 1.37e+00 -6.4 1.00e+00 1.00e+00h 1\n", - " 19 1.2044052e+00 1.58e-03 8.51e-05 -8.6 3.73e+00 -6.9 1.00e+00 3.42e-01h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 1.2039397e+00 9.51e-03 5.22e-04 -8.6 4.63e+00 -7.3 1.00e+00 1.00e+00f 1\n", - " 21 1.2031551e+00 8.32e-02 6.07e-03 -8.6 1.92e+01 -7.8 1.00e+00 8.71e-01h 1\n", - " 22 1.2028414e+00 1.08e-01 7.48e-03 -8.6 1.19e+03 -8.3 7.23e-02 8.23e-03h 1\n", - " 23 1.2025270e+00 5.89e-02 4.04e-03 -8.6 3.92e+01 - 1.00e+00 4.53e-01f 1\n", - " 24 1.2025096e+00 4.44e-02 3.04e-03 -8.6 3.68e+01 - 5.41e-01 2.48e-01f 1\n", - " 25 1.2025058e+00 4.33e-02 2.05e-03 -8.6 3.17e+01 - 1.00e+00 3.26e-01f 1\n", - " 26 1.2025106e+00 1.21e-03 1.32e-04 -8.6 2.37e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (356718)\n", - " 27 1.2025167e+00 5.23e-04 3.85e-07 -8.6 2.06e+00 - 1.00e+00 1.00e+00h 1\n", - " 28 1.2025166e+00 3.31e-06 2.06e-09 -8.6 1.65e-01 - 1.00e+00 1.00e+00h 1\n", - " 29 1.2025166e+00 8.35e-11 7.43e-14 -8.6 8.28e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 29\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 1.2025166394012419e+00 1.2025166394012419e+00\n", - "Dual infeasibility......: 7.4296667615744834e-14 7.4296667615744834e-14\n", - "Constraint violation....: 8.3521300986433289e-11 8.3521300986433289e-11\n", - "Complementarity.........: 2.5059040285569865e-09 2.5059040285569865e-09\n", - "Overall NLP error.......: 2.5059040285569865e-09 2.5059040285569865e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 34\n", - "Number of objective gradient evaluations = 30\n", - "Number of equality constraint evaluations = 34\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 30\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 29\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.243\n", - "Total CPU secs in NLP function evaluations = 0.037\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.33e-03 3.46e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 2.01e-09 1.45e-06 -1.0 1.69e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.0122556954049742e-09 2.0122556954049742e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.0122556954049742e-09 2.0122556954049742e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", - "Total CPU secs in NLP function evaluations = 0.003\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.2 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.33e-03 3.46e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 2.01e-09 1.45e-06 -1.0 1.69e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.0122561394941840e-09 2.0122561394941840e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.0122561394941840e-09 2.0122561394941840e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 0.9 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 7.66e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 6.53e+02 3.85e+02 -1.0 7.66e+03 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 7.46e+00 3.85e+00 -1.0 7.00e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 7.38e-02 4.39e+00 -1.0 7.94e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.85e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.127\n", - "Total CPU secs in NLP function evaluations = 0.005\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.2108134e+01 1.25e+00 1.06e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.2109716e+01 6.18e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", - " 2 -1.2108512e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.2115163e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.2102323e+01 1.67e+01 2.21e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.2104160e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.13e-01f 2\n", - " 6 -1.2099103e+01 1.23e+01 3.32e+00 -1.0 9.22e+01 - 8.77e-01 3.36e-01f 2\n", - " 7 -1.2094289e+01 3.63e+00 5.34e-01 -1.0 5.23e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.2096373e+01 4.45e-01 7.81e-02 -1.0 1.09e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.2096378e+01 2.38e-04 1.44e-03 -1.7 3.51e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.2096452e+01 4.87e-04 1.23e-03 -3.8 5.09e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 -1.2104600e+01 6.30e+00 3.11e-02 -5.7 5.44e+01 - 6.88e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (319533)\n", - " 12 -1.2111071e+01 3.68e+00 1.30e-02 -5.7 6.59e+01 - 8.37e-01 1.00e+00h 1\n", - " 13 -1.2115222e+01 6.14e+00 2.84e-02 -5.7 3.51e+01 - 8.03e-01 1.00e+00h 1\n", - " 14 -1.2115127e+01 5.38e+00 4.94e-02 -5.7 1.50e+01 -4.0 1.00e+00 1.24e-01h 1\n", - " 15 -1.2114999e+01 4.47e-01 2.64e-02 -5.7 2.58e+01 - 1.00e+00 1.00e+00h 1\n", - " 16 -1.2114900e+01 4.96e-04 8.86e-05 -5.7 3.02e-01 -4.5 1.00e+00 1.00e+00h 1\n", - " 17 -1.2115229e+01 5.02e-01 4.88e-03 -5.7 6.65e+02 - 1.86e-01 2.67e-02h 2\n", - " 18 -1.2116201e+01 2.32e-01 2.86e-03 -5.7 1.63e+01 - 1.00e+00 1.00e+00h 1\n", - " 19 -1.2115996e+01 6.36e-03 8.56e-05 -5.7 2.06e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (337748)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.2116001e+01 1.11e-05 1.53e-06 -5.7 2.94e-01 - 1.00e+00 1.00e+00h 1\n", - " 21 -1.2116631e+01 2.76e-01 8.95e-04 -8.6 1.65e+01 - 9.16e-01 9.85e-01h 1\n", - " 22 -1.2116650e+01 1.02e-02 2.45e-04 -8.6 8.93e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (355121)\n", - " 23 -1.2116656e+01 1.72e-03 4.32e-05 -8.6 3.73e+00 - 1.00e+00 1.00e+00h 1\n", - " 24 -1.2116657e+01 3.81e-05 1.36e-06 -8.6 5.59e-01 - 1.00e+00 1.00e+00h 1\n", - " 25 -1.2116657e+01 1.56e-08 1.40e-09 -8.6 1.13e-02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (375561)\n", - " 26 -1.2116658e+01 6.53e-07 1.86e-08 -9.0 7.32e-02 - 1.00e+00 1.00e+00h 1\n", - " 27 -1.2116658e+01 1.00e-11 4.32e-13 -9.0 2.87e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 27\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.2116658032211422e+01 -1.2116658032211422e+01\n", - "Dual infeasibility......: 4.3248756227463823e-13 4.3248756227463823e-13\n", - "Constraint violation....: 9.9974473144470721e-12 9.9974473144470721e-12\n", - "Complementarity.........: 9.0909106880328588e-10 9.0909106880328588e-10\n", - "Overall NLP error.......: 9.0909106880328588e-10 9.0909106880328588e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 37\n", - "Number of objective gradient evaluations = 28\n", - "Number of equality constraint evaluations = 37\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 28\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 27\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.055\n", - "Total CPU secs in NLP function evaluations = 0.036\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.8 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.33e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 4.15e+00 3.85e+02 -1.0 3.28e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.74e-02 3.85e+00 -1.0 4.44e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.69e-04 4.39e+00 -1.0 5.04e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 4.98e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8339111319583026e-11 5.8339111319583026e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8339111319583026e-11 5.8339111319583026e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.131\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 1.0454702e+00 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 1.0444646e+00 1.58e-01 1.05e+00 -1.0 5.95e-01 - 9.79e-01 8.74e-01h 1\n", - " 2 1.0450802e+00 3.48e-02 1.12e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 1.0385721e+00 1.25e+01 5.74e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", - " 4 1.0522576e+00 7.13e-01 2.50e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 1.0490906e+00 5.85e-01 4.70e+01 -1.0 3.50e+02 - 4.73e-01 8.81e-02f 2\n", - " 6 1.0576821e+00 1.36e-01 5.97e+00 -1.0 2.40e+01 -4.0 8.22e-01 1.00e+00F 1\n", - " 7 1.0562026e+00 4.33e-01 2.97e-01 -1.0 5.48e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 1.0569195e+00 3.85e-02 5.75e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 1.0568573e+00 5.31e-03 3.69e-03 -2.5 6.30e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 1.0563619e+00 1.01e-03 7.93e-04 -3.8 1.47e+00 - 1.00e+00 1.00e+00h 1\n", - " 11 1.0487134e+00 5.52e-01 2.96e-02 -5.7 3.53e+01 - 7.06e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (319271)\n", - " 12 1.0424427e+00 7.03e-01 1.05e-02 -5.7 4.80e+01 - 8.51e-01 1.00e+00h 1\n", - " 13 1.0381157e+00 7.10e-01 3.48e-02 -5.7 3.01e+01 - 7.81e-01 1.00e+00h 1\n", - " 14 1.0390532e+00 1.28e-02 1.27e-03 -5.7 6.87e-01 -4.5 9.40e-01 1.00e+00h 1\n", - " 15 1.0390112e+00 4.50e-06 9.50e-07 -5.7 8.32e-02 -5.0 1.00e+00 1.00e+00h 1\n", - " 16 1.0389810e+00 7.21e-06 4.11e-06 -8.6 1.03e-01 -5.4 9.99e-01 1.00e+00h 1\n", - " 17 1.0389364e+00 8.39e-05 3.92e-06 -8.6 3.70e-01 -5.9 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (335399)\n", - " 18 1.0388452e+00 3.85e-04 1.79e-05 -8.6 1.04e+00 -6.4 1.00e+00 7.44e-01h 1\n", - " 19 1.0386655e+00 1.98e-03 6.30e-05 -8.6 1.81e+00 -6.9 1.00e+00 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 1.0382979e+00 7.70e-03 3.44e-04 -8.6 3.97e+00 -7.3 1.00e+00 1.00e+00h 1\n", - " 21 1.0375873e+00 7.24e-02 5.06e-03 -8.6 1.56e+01 -7.8 1.00e+00 1.00e+00h 1\n", - " 22 1.0372258e+00 1.16e-01 7.43e-03 -8.6 5.45e+02 -8.3 1.62e-01 2.38e-02h 1\n", - " 23 1.0369759e+00 6.56e-02 4.17e-03 -8.6 3.95e+01 - 1.00e+00 4.33e-01f 1\n", - " 24 1.0369555e+00 5.05e-02 3.21e-03 -8.6 3.72e+01 - 1.00e+00 2.30e-01f 1\n", - " 25 1.0369460e+00 4.41e-02 2.04e-03 -8.6 3.24e+01 - 1.00e+00 3.64e-01f 1\n", - " 26 1.0369409e+00 1.26e-03 1.02e-04 -8.6 2.22e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (358185)\n", - " 27 1.0369464e+00 3.96e-04 2.65e-07 -8.6 1.80e+00 - 1.00e+00 1.00e+00h 1\n", - " 28 1.0369464e+00 1.87e-06 1.06e-09 -8.6 1.24e-01 - 1.00e+00 1.00e+00h 1\n", - " 29 1.0369464e+00 2.65e-11 3.91e-14 -8.6 4.66e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 29\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 1.0369464064985126e+00 1.0369464064985126e+00\n", - "Dual infeasibility......: 3.9074655964464579e-14 3.9074655964464579e-14\n", - "Constraint violation....: 2.6508573114369938e-11 2.6508573114369938e-11\n", - "Complementarity.........: 2.5059036949607426e-09 2.5059036949607426e-09\n", - "Overall NLP error.......: 2.5059036949607426e-09 2.5059036949607426e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 34\n", - "Number of objective gradient evaluations = 30\n", - "Number of equality constraint evaluations = 34\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 30\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 29\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.435\n", - "Total CPU secs in NLP function evaluations = 0.045\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.89e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.41e-03 2.08e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 2.23e-09 1.50e-06 -1.0 1.78e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.2337816041329006e-09 2.2337816041329006e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.2337816041329006e-09 2.2337816041329006e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.104\n", - "Total CPU secs in NLP function evaluations = 0.005\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.32e-03 3.69e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 1.98e-09 1.44e-06 -1.0 1.67e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.9765331593646351e-09 1.9765331593646351e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.9765331593646351e-09 1.9765331593646351e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.107\n", - "Total CPU secs in NLP function evaluations = 0.003\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.2 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 8.43e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 6.61e+02 3.85e+02 -1.0 8.43e+03 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 7.54e+00 3.85e+00 -1.0 7.08e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 7.46e-02 4.39e+00 -1.0 8.02e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 7.93e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.128\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.2260028e+01 1.25e+00 1.05e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.2261473e+01 6.18e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", - " 2 -1.2260355e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.2266446e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.2254710e+01 1.67e+01 2.20e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.2256378e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.13e-01f 2\n", - " 6 -1.2251753e+01 1.23e+01 3.32e+00 -1.0 9.22e+01 - 8.77e-01 3.36e-01f 2\n", - " 7 -1.2247342e+01 3.64e+00 5.35e-01 -1.0 5.23e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.2249251e+01 4.47e-01 7.84e-02 -1.0 1.10e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.2249255e+01 2.39e-04 1.38e-03 -1.7 3.57e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.2249317e+01 4.10e-04 1.13e-03 -3.8 4.68e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 -1.2249467e+01 2.64e-04 1.34e-04 -5.7 9.10e-01 -4.0 1.00e+00 1.00e+00h 1\n", - " 12 -1.2250000e+01 3.31e-03 1.08e-04 -5.7 3.23e+00 -4.5 1.00e+00 1.00e+00h 1\n", - " 13 -1.2250322e+01 3.13e-03 1.32e-03 -5.7 4.61e+00 -5.0 1.00e+00 4.23e-01h 1\n", - " 14 -1.2250433e+01 2.06e-03 7.85e-04 -5.7 6.18e-01 -5.4 1.00e+00 1.00e+00f 1\n", - " 15 -1.2261629e+01 2.19e+01 8.47e-02 -5.7 6.04e+03 - 2.88e-02 2.27e-02h 1\n", - " 16 -1.2265527e+01 2.15e+01 6.31e-02 -5.7 1.28e+02 - 1.00e+00 2.59e-01f 1\n", - " 17 -1.2268702e+01 9.75e-01 1.54e-02 -5.7 2.61e+01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (320847)\n", - " 18 -1.2267146e+01 3.13e-01 1.00e-03 -5.7 1.77e+01 - 1.00e+00 1.00e+00h 1\n", - " 19 -1.2267156e+01 4.84e-04 4.02e-06 -5.7 5.89e-01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (338500)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.2267156e+01 3.56e-08 2.75e-09 -5.7 1.30e-02 - 1.00e+00 1.00e+00h 1\n", - " 21 -1.2267782e+01 3.24e-01 9.64e-04 -8.6 1.78e+01 - 9.11e-01 9.81e-01h 1\n", - " 22 -1.2267804e+01 1.12e-02 2.49e-04 -8.6 9.37e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (357003)\n", - " 23 -1.2267811e+01 2.04e-03 4.64e-05 -8.6 4.06e+00 - 1.00e+00 1.00e+00h 1\n", - " 24 -1.2267812e+01 5.38e-05 1.69e-06 -8.6 6.64e-01 - 1.00e+00 1.00e+00h 1\n", - " 25 -1.2267812e+01 3.11e-08 2.34e-09 -8.6 1.60e-02 - 1.00e+00 1.00e+00h 1\n", - " 26 -1.2267812e+01 4.55e-13 3.29e-14 -8.6 2.09e-05 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 26\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.2267811714197844e+01 -1.2267811714197844e+01\n", - "Dual infeasibility......: 3.2899955311301634e-14 3.2899955311301634e-14\n", - "Constraint violation....: 1.2096124245683242e-13 4.5474735088646412e-13\n", - "Complementarity.........: 2.5059035616651650e-09 2.5059035616651650e-09\n", - "Overall NLP error.......: 2.5059035616651650e-09 2.5059035616651650e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 33\n", - "Number of objective gradient evaluations = 27\n", - "Number of equality constraint evaluations = 33\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 27\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 26\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.945\n", - "Total CPU secs in NLP function evaluations = 0.030\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.7 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 3.67e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 4.18e+00 3.85e+02 -1.0 3.62e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.77e-02 3.85e+00 -1.0 4.47e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.72e-04 4.39e+00 -1.0 5.07e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.02e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.132\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 8.9357618e-01 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 8.9266467e-01 1.57e-01 1.04e+00 -1.0 5.95e-01 - 9.79e-01 8.74e-01h 1\n", - " 2 8.9324077e-01 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 8.8727234e-01 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", - " 4 8.9978503e-01 7.13e-01 2.49e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 8.9690135e-01 5.85e-01 4.71e+01 -1.0 3.49e+02 - 4.74e-01 8.83e-02f 2\n", - " 6 9.0474858e-01 1.37e-01 5.96e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", - " 7 9.0340741e-01 4.33e-01 2.97e-01 -1.0 5.48e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 9.0406676e-01 3.85e-02 5.76e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 9.0401537e-01 5.34e-03 3.76e-03 -2.5 6.32e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 9.0360000e-01 8.24e-04 7.04e-04 -3.8 1.31e+00 - 1.00e+00 1.00e+00h 1\n", - " 11 8.9706762e-01 4.73e-01 2.82e-02 -5.7 3.26e+01 - 7.21e-01 1.00e+00h 1\n", - " 12 8.9104362e-01 7.55e-01 1.10e-02 -5.7 4.96e+01 - 8.53e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (326956)\n", - " 13 8.8691901e-01 7.24e-01 3.26e-02 -5.7 3.05e+01 - 8.07e-01 1.00e+00h 1\n", - " 14 8.8783634e-01 1.23e-02 9.48e-04 -5.7 7.02e-01 -4.5 9.51e-01 1.00e+00h 1\n", - " 15 8.8779529e-01 1.18e-06 3.71e-07 -5.7 3.26e-02 -5.0 1.00e+00 1.00e+00h 1\n", - " 16 8.8776642e-01 7.96e-06 3.75e-06 -8.6 1.12e-01 -5.4 9.99e-01 1.00e+00h 1\n", - " 17 8.8772695e-01 7.85e-05 3.35e-06 -8.6 3.61e-01 -5.9 1.00e+00 1.00e+00h 1\n", - " 18 8.8761634e-01 6.66e-04 2.84e-05 -8.6 1.06e+00 -6.4 1.00e+00 1.00e+00h 1\n", - " 19 8.8755498e-01 7.42e-04 3.16e-05 -8.6 2.76e+00 -6.9 1.00e+00 2.24e-01h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 8.8717865e-01 9.48e-03 3.05e-04 -8.6 4.13e+00 -7.3 1.00e+00 1.00e+00f 1\n", - " 21 8.8657032e-01 4.66e-02 2.97e-03 -8.6 1.24e+01 -7.8 1.00e+00 9.75e-01h 1\n", - " 22 8.8612174e-01 1.44e-01 8.55e-03 -8.6 3.02e+02 -8.3 3.51e-01 6.41e-02h 1\n", - " 23 8.8583006e-01 7.53e-02 4.42e-03 -8.6 4.03e+01 - 1.00e+00 4.78e-01f 1\n", - " 24 8.8580769e-01 5.71e-02 3.34e-03 -8.6 3.65e+01 - 1.00e+00 2.44e-01f 1\n", - " 25 8.8579801e-01 4.72e-02 2.21e-03 -8.6 3.13e+01 - 1.00e+00 3.38e-01f 1\n", - "Reallocating memory for MA57: lfact (344590)\n", - " 26 8.8578794e-01 1.54e-03 9.57e-05 -8.6 2.23e+00 - 1.00e+00 1.00e+00h 1\n", - " 27 8.8579268e-01 3.27e-04 1.98e-07 -8.6 1.63e+00 - 1.00e+00 1.00e+00h 1\n", - " 28 8.8579267e-01 1.28e-06 6.66e-10 -8.6 1.03e-01 - 1.00e+00 1.00e+00h 1\n", - " 29 8.8579267e-01 4.37e-12 1.51e-10 -8.6 1.89e-04 -7.0 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 29\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 8.8579267060918165e-01 8.8579267060918165e-01\n", - "Dual infeasibility......: 1.5144484733052913e-10 1.5144484733052913e-10\n", - "Constraint violation....: 4.3728354270911041e-12 4.3728354270911041e-12\n", - "Complementarity.........: 2.5059074743708337e-09 2.5059074743708337e-09\n", - "Overall NLP error.......: 2.5059074743708337e-09 2.5059074743708337e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 34\n", - "Number of objective gradient evaluations = 30\n", - "Number of equality constraint evaluations = 34\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 30\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 29\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.345\n", - "Total CPU secs in NLP function evaluations = 0.035\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.31e-03 3.92e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 1.94e-09 1.43e-06 -1.0 1.66e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.9411232621280305e-09 1.9411232621280305e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.9411232621280305e-09 1.9411232621280305e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.31e-03 3.92e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 1.94e-09 1.43e-06 -1.0 1.66e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.9411330320906472e-09 1.9411330320906472e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.9411330320906472e-09 1.9411330320906472e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.099\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 9.19e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 6.69e+02 3.85e+02 -1.0 9.19e+03 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 7.62e+00 3.85e+00 -1.0 7.15e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 7.53e-02 4.39e+00 -1.0 8.10e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.01e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.130\n", - "Total CPU secs in NLP function evaluations = 0.003\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.2399698e+01 1.25e+00 1.05e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.2401027e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", - " 2 -1.2399984e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.2405602e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.2394795e+01 1.67e+01 2.20e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.2396322e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.13e-01f 2\n", - " 6 -1.2392061e+01 1.23e+01 3.32e+00 -1.0 9.23e+01 - 8.76e-01 3.36e-01f 2\n", - " 7 -1.2387991e+01 3.64e+00 5.35e-01 -1.0 5.23e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.2389753e+01 4.48e-01 7.87e-02 -1.0 1.10e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.2389755e+01 2.40e-04 1.33e-03 -1.7 3.63e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.2389808e+01 3.50e-04 1.04e-03 -3.8 4.33e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 -1.2389937e+01 2.30e-04 1.17e-04 -5.7 8.50e-01 -4.0 1.00e+00 1.00e+00h 1\n", - " 12 -1.2390402e+01 2.96e-03 1.02e-04 -5.7 3.06e+00 -4.5 1.00e+00 1.00e+00h 1\n", - " 13 -1.2390734e+01 3.18e-03 1.06e-03 -5.7 4.98e+00 -5.0 1.00e+00 4.36e-01h 1\n", - " 14 -1.2390822e+01 1.73e-03 7.04e-04 -5.7 5.30e-01 -5.4 1.00e+00 1.00e+00f 1\n", - " 15 -1.2391170e+01 1.55e-02 2.13e-05 -5.7 2.13e+00 -5.9 1.00e+00 1.00e+00h 1\n", - " 16 -1.2392122e+01 1.25e-01 1.80e-04 -5.7 5.69e+00 -6.4 1.00e+00 1.00e+00h 1\n", - " 17 -1.2394535e+01 8.51e-01 1.24e-03 -5.7 1.34e+01 -6.9 1.00e+00 1.00e+00h 1\n", - " 18 -1.2399248e+01 3.83e+00 7.22e-03 -5.7 1.90e+01 -7.3 1.00e+00 1.00e+00h 1\n", - " 19 -1.2404443e+01 8.34e+00 2.26e-02 -5.7 3.55e+01 -7.8 1.00e+00 9.24e-01h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.2403887e+01 1.53e-01 1.60e-03 -5.7 1.03e+01 -7.4 1.00e+00 1.00e+00f 1\n", - " 21 -1.2405117e+01 4.79e-01 2.18e-03 -5.7 1.71e+01 -7.9 1.00e+00 1.00e+00h 1\n", - " 22 -1.2405374e+01 7.00e-02 1.88e-04 -5.7 8.45e+00 -7.4 1.00e+00 1.00e+00h 1\n", - " 23 -1.2405756e+01 2.12e-01 1.22e-03 -5.7 1.03e+02 - 1.00e+00 2.08e-01h 2\n", - " 24 -1.2406358e+01 1.03e+00 2.17e-03 -5.7 8.46e+01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (322545)\n", - " 25 -1.2406204e+01 1.75e-02 7.25e-05 -5.7 4.61e+00 - 1.00e+00 1.00e+00h 1\n", - " 26 -1.2406205e+01 2.37e-04 2.08e-07 -5.7 1.32e+00 - 1.00e+00 1.00e+00h 1\n", - " 27 -1.2406205e+01 7.23e-08 5.15e-11 -5.7 2.30e-02 - 1.00e+00 1.00e+00h 1\n", - " 28 -1.2406828e+01 3.77e-01 1.03e-03 -8.6 1.92e+01 - 9.07e-01 9.76e-01h 1\n", - "Reallocating memory for MA57: lfact (354103)\n", - " 29 -1.2406852e+01 1.23e-02 2.52e-04 -8.6 9.79e+00 - 9.97e-01 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -1.2406858e+01 2.37e-03 4.93e-05 -8.6 4.37e+00 - 1.00e+00 1.00e+00h 1\n", - " 31 -1.2406860e+01 7.39e-05 2.07e-06 -8.6 7.78e-01 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (376176)\n", - " 32 -1.2406860e+01 5.90e-08 3.77e-09 -8.6 2.20e-02 - 1.00e+00 1.00e+00h 1\n", - " 33 -1.2406860e+01 1.82e-12 3.89e-14 -8.6 3.63e-05 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 33\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.2406859586044352e+01 -1.2406859586044352e+01\n", - "Dual infeasibility......: 3.8887996860705895e-14 3.8887996860705895e-14\n", - "Constraint violation....: 9.1789600112649688e-13 1.8189894035458565e-12\n", - "Complementarity.........: 2.5059035652168643e-09 2.5059035652168643e-09\n", - "Overall NLP error.......: 2.5059035652168643e-09 2.5059035652168643e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 43\n", - "Number of objective gradient evaluations = 34\n", - "Number of equality constraint evaluations = 43\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 34\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 33\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.368\n", - "Total CPU secs in NLP function evaluations = 0.051\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 4.01e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 4.21e+00 3.85e+02 -1.0 3.96e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.80e-02 3.85e+00 -1.0 4.50e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.75e-04 4.39e+00 -1.0 5.11e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.05e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.131\n", - "Total CPU secs in NLP function evaluations = 0.005\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 7.5390617e-01 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 7.5307280e-01 1.57e-01 1.04e+00 -1.0 5.95e-01 - 9.79e-01 8.74e-01h 1\n", - " 2 7.5361378e-01 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 7.4810233e-01 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", - " 4 7.5962740e-01 7.12e-01 2.49e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 7.5698063e-01 5.85e-01 4.71e+01 -1.0 3.48e+02 - 4.75e-01 8.85e-02f 2\n", - " 6 7.6420980e-01 1.37e-01 5.99e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", - " 7 7.6297451e-01 4.34e-01 2.97e-01 -1.0 5.48e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 7.6358498e-01 3.85e-02 5.77e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 7.6354212e-01 5.37e-03 3.81e-03 -2.5 6.34e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 7.6350175e-01 2.77e-06 2.26e-05 -3.8 3.72e-02 -4.5 1.00e+00 1.00e+00h 1\n", - " 11 7.5733271e-01 4.48e-01 2.29e-02 -5.7 3.01e+01 - 7.11e-01 1.00e+00h 1\n", - " 12 7.5196585e-01 6.78e-01 8.94e-03 -5.7 4.61e+01 - 8.18e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (327108)\n", - " 13 7.4797841e-01 6.41e-01 2.55e-02 -5.7 2.96e+01 - 8.64e-01 1.00e+00h 1\n", - " 14 7.4890109e-01 8.10e-03 4.06e-04 -5.7 6.20e-01 -5.0 1.00e+00 1.00e+00h 1\n", - " 15 7.4886319e-01 1.99e-06 2.82e-07 -5.7 5.38e-02 -5.4 1.00e+00 1.00e+00h 1\n", - " 16 7.4881137e-01 6.86e-05 1.98e-05 -8.6 3.31e-01 -5.9 9.95e-01 1.00e+00h 1\n", - " 17 7.4870952e-01 6.03e-04 2.31e-05 -8.6 9.93e-01 -6.4 1.00e+00 1.00e+00h 1\n", - " 18 7.4845170e-01 4.66e-03 1.77e-04 -8.6 2.80e+00 -6.9 1.00e+00 1.00e+00h 1\n", - " 19 7.4825676e-01 6.71e-03 2.55e-04 -8.6 7.30e+00 -7.3 1.00e+00 3.51e-01h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 7.4767097e-01 3.32e-02 1.65e-03 -8.6 8.46e+00 -7.8 1.00e+00 1.00e+00f 1\n", - " 21 7.4711216e-01 2.44e-01 1.54e-02 -8.6 1.14e+02 -8.3 9.80e-01 2.52e-01h 1\n", - " 22 7.4688518e-01 1.38e-01 8.67e-03 -8.6 4.16e+01 - 1.00e+00 4.39e-01f 1\n", - " 23 7.4683046e-01 1.02e-01 6.42e-03 -8.6 3.76e+01 - 1.00e+00 2.59e-01f 1\n", - " 24 7.4679078e-01 6.26e-02 3.91e-03 -8.6 3.27e+01 - 1.00e+00 3.90e-01f 1\n", - " 25 7.4674071e-01 1.91e-03 7.64e-05 -8.6 2.40e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (350967)\n", - " 26 7.4674477e-01 5.39e-04 3.03e-07 -8.6 2.09e+00 - 1.00e+00 1.00e+00h 1\n", - " 27 7.4674475e-01 3.40e-06 1.63e-09 -8.6 1.67e-01 - 1.00e+00 1.00e+00h 1\n", - " 28 7.4674475e-01 8.78e-11 6.54e-14 -8.6 8.49e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 28\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 7.4674475313131294e-01 7.4674475313131294e-01\n", - "Dual infeasibility......: 6.5380653010782577e-14 6.5380653010782577e-14\n", - "Constraint violation....: 8.7840845708342385e-11 8.7840845708342385e-11\n", - "Complementarity.........: 2.5059039393135571e-09 2.5059039393135571e-09\n", - "Overall NLP error.......: 2.5059039393135571e-09 2.5059039393135571e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 33\n", - "Number of objective gradient evaluations = 29\n", - "Number of equality constraint evaluations = 33\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 29\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 28\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.211\n", - "Total CPU secs in NLP function evaluations = 0.043\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.9 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.30e-03 4.15e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 1.91e-09 1.42e-06 -1.0 1.64e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.9060446554419741e-09 1.9060446554419741e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.9060446554419741e-09 1.9060446554419741e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.103\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.30e-03 4.15e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 1.91e-09 1.42e-06 -1.0 1.64e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.9060530931369613e-09 1.9060530931369613e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.9060530931369613e-09 1.9060530931369613e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.100\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 0.8 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 9.96e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 6.76e+02 3.85e+02 -1.0 9.96e+03 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 7.69e+00 3.85e+00 -1.0 7.23e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 7.61e-02 4.39e+00 -1.0 8.17e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.08e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0403773356229067e-09 9.0403773356229067e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0403773356229067e-09 9.0403773356229067e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.132\n", - "Total CPU secs in NLP function evaluations = 0.005\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.2528966e+01 1.25e+00 1.05e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.2530197e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", - " 2 -1.2529220e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.2534432e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.2524418e+01 1.67e+01 2.20e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.2525826e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.13e-01f 2\n", - " 6 -1.2521877e+01 1.23e+01 3.32e+00 -1.0 9.23e+01 - 8.76e-01 3.35e-01f 2\n", - " 7 -1.2518098e+01 3.65e+00 5.36e-01 -1.0 5.24e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.2519733e+01 4.49e-01 7.90e-02 -1.0 1.10e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.2519735e+01 2.41e-04 1.29e-03 -1.7 3.68e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.2519781e+01 3.02e-04 9.67e-04 -3.8 4.03e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 -1.2519893e+01 2.02e-04 1.02e-04 -5.7 7.97e-01 -4.0 1.00e+00 1.00e+00h 1\n", - " 12 -1.2520302e+01 2.66e-03 9.65e-05 -5.7 2.89e+00 -4.5 1.00e+00 1.00e+00h 1\n", - " 13 -1.2520638e+01 3.26e-03 8.46e-04 -5.7 5.24e+00 -5.0 1.00e+00 4.54e-01h 1\n", - " 14 -1.2520708e+01 1.48e-03 6.33e-04 -5.7 4.47e-01 -5.4 1.00e+00 1.00e+00f 1\n", - " 15 -1.2521012e+01 1.34e-02 1.66e-05 -5.7 2.02e+00 -5.9 1.00e+00 1.00e+00h 1\n", - " 16 -1.2521836e+01 1.09e-01 1.45e-04 -5.7 5.34e+00 -6.4 1.00e+00 1.00e+00h 1\n", - " 17 -1.2523945e+01 7.51e-01 9.99e-04 -5.7 1.28e+01 -6.9 1.00e+00 1.00e+00h 1\n", - " 18r-1.2523945e+01 7.51e-01 9.99e+02 -0.1 0.00e+00 - 0.00e+00 3.08e-07R 18\n", - " 19r-1.2523451e+01 1.72e-01 9.81e+02 -0.1 3.12e+03 - 1.04e-01 9.90e-04f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.2524823e+01 1.77e-01 5.03e-02 -5.7 1.23e+04 - 7.38e-02 7.99e-04h 1\n", - " 21 -1.2539231e+01 3.95e+01 4.75e-02 -5.7 2.75e+03 - 3.37e-04 5.44e-02h 1\n", - "Reallocating memory for MA57: lfact (323127)\n", - " 22 -1.2541247e+01 3.61e+01 4.11e-02 -5.7 3.01e+02 - 1.46e-01 1.36e-01f 1\n", - " 23 -1.2539740e+01 2.50e+01 3.16e-02 -5.7 1.12e+02 - 1.19e-01 3.51e-01h 1\n", - " 24 -1.2536325e+01 1.39e+01 5.47e-02 -5.7 9.93e+01 - 1.00e+00 5.51e-01h 1\n", - " 25 -1.2536505e+01 1.38e+01 2.22e-01 -5.7 5.62e+02 - 1.93e-03 4.60e-02h 1\n", - " 26 -1.2534286e+01 1.59e+00 1.76e-01 -5.7 3.49e+01 - 1.24e-01 1.00e+00f 1\n", - " 27 -1.2534443e+01 5.57e-02 3.13e-04 -5.7 1.15e+01 -7.3 1.00e+00 1.00e+00h 1\n", - " 28 -1.2534551e+01 9.81e-02 7.65e-04 -5.7 7.58e+00 -7.8 1.00e+00 1.00e+00h 1\n", - " 29 -1.2534950e+01 1.03e+00 6.91e-03 -5.7 2.80e+01 -8.3 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -1.2534831e+01 1.41e-01 1.08e-03 -5.7 8.88e+00 - 1.00e+00 1.00e+00h 1\n", - " 31 -1.2534949e+01 4.97e-02 3.05e-04 -5.7 6.88e+00 - 1.00e+00 1.00e+00h 1\n", - " 32 -1.2534944e+01 2.69e-04 2.11e-06 -5.7 4.40e-01 - 1.00e+00 1.00e+00h 1\n", - " 33 -1.2534944e+01 1.07e-08 1.03e-10 -5.7 2.72e-03 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (344149)\n", - " 34 -1.2535563e+01 4.32e-01 1.10e-03 -8.6 2.06e+01 - 9.03e-01 9.71e-01h 1\n", - "Reallocating memory for MA57: lfact (363431)\n", - " 35 -1.2535591e+01 1.34e-02 2.54e-04 -8.6 1.02e+01 - 9.95e-01 1.00e+00h 1\n", - " 36 -1.2535597e+01 2.72e-03 5.20e-05 -8.6 4.68e+00 - 1.00e+00 1.00e+00h 1\n", - " 37 -1.2535598e+01 9.81e-05 2.47e-06 -8.6 8.96e-01 - 1.00e+00 1.00e+00h 1\n", - " 38 -1.2535598e+01 1.05e-07 5.76e-09 -8.6 2.93e-02 - 1.00e+00 1.00e+00h 1\n", - " 39 -1.2535598e+01 1.82e-12 5.88e-14 -8.6 5.95e-05 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 39\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.2535597772942342e+01 -1.2535597772942342e+01\n", - "Dual infeasibility......: 5.8821618000241020e-14 5.8821618000241020e-14\n", - "Constraint violation....: 1.8189894035458565e-12 1.8189894035458565e-12\n", - "Complementarity.........: 2.5059035734822043e-09 2.5059035734822043e-09\n", - "Overall NLP error.......: 2.5059035734822043e-09 2.5059035734822043e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 66\n", - "Number of objective gradient evaluations = 40\n", - "Number of equality constraint evaluations = 66\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 41\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 39\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.508\n", - "Total CPU secs in NLP function evaluations = 0.067\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.3 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 4.35e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 4.24e+00 3.85e+02 -1.0 4.30e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.83e-02 3.85e+00 -1.0 4.53e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.78e-04 4.39e+00 -1.0 5.14e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.08e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.128\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 6.2463783e-01 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 6.2387037e-01 1.57e-01 1.04e+00 -1.0 5.95e-01 - 9.79e-01 8.75e-01h 1\n", - " 2 6.2438002e-01 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 6.1926056e-01 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", - " 4 6.2994249e-01 7.12e-01 2.48e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 6.2749675e-01 5.85e-01 4.72e+01 -1.0 3.48e+02 - 4.75e-01 8.87e-02f 2\n", - " 6 6.3421863e-01 1.35e-01 6.11e+00 -1.0 2.40e+01 -4.0 8.21e-01 1.00e+00F 1\n", - " 7 6.3306025e-01 4.35e-01 2.98e-01 -1.0 5.49e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 6.3362659e-01 3.83e-02 5.75e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 6.3358982e-01 5.38e-03 3.85e-03 -2.5 6.34e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 6.3355519e-01 2.58e-06 2.12e-05 -3.8 3.51e-02 -4.5 1.00e+00 1.00e+00h 1\n", - " 11 6.3322338e-01 2.85e-05 3.51e-06 -5.7 9.23e-02 -5.0 1.00e+00 1.00e+00h 1\n", - " 12 6.3264654e-01 1.33e-04 5.05e-06 -5.7 2.72e-01 -5.4 1.00e+00 1.00e+00h 1\n", - " 13 6.3233614e-01 4.22e-04 2.05e-05 -5.7 7.80e-01 -5.9 1.00e+00 1.00e+00h 1\n", - " 14 6.3145086e-01 3.55e-03 1.66e-04 -5.7 2.26e+00 -6.4 1.00e+00 1.00e+00h 1\n", - " 15 6.2329402e-01 1.79e+00 4.47e-02 -5.7 7.85e+02 - 1.56e-01 9.99e-02h 1\n", - " 16 6.2001136e-01 1.37e+00 4.66e-02 -5.7 1.24e+02 - 1.00e+00 2.70e-01f 1\n", - " 17 6.1736350e-01 1.24e-01 1.13e-02 -5.7 2.91e+01 - 1.00e+00 1.00e+00f 1\n", - "Reallocating memory for MA57: lfact (322135)\n", - " 18 6.1867566e-01 7.27e-03 6.10e-04 -5.7 4.56e+00 - 1.00e+00 1.00e+00h 1\n", - " 19 6.1866009e-01 5.67e-05 3.42e-06 -5.7 6.62e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 6.1866005e-01 2.03e-08 1.12e-09 -5.7 1.25e-02 - 1.00e+00 1.00e+00h 1\n", - " 21 6.1804128e-01 3.11e-02 1.10e-03 -8.6 1.54e+01 - 9.03e-01 9.71e-01h 1\n", - "Reallocating memory for MA57: lfact (347921)\n", - " 22 6.1801354e-01 1.34e-02 2.54e-04 -8.6 1.02e+01 - 9.95e-01 1.00e+00h 1\n", - " 23 6.1800773e-01 2.72e-03 5.20e-05 -8.6 4.68e+00 - 1.00e+00 1.00e+00h 1\n", - " 24 6.1800657e-01 9.81e-05 2.47e-06 -8.6 8.96e-01 - 1.00e+00 1.00e+00h 1\n", - " 25 6.1800653e-01 1.05e-07 5.76e-09 -8.6 2.93e-02 - 1.00e+00 1.00e+00h 1\n", - " 26 6.1800653e-01 4.32e-13 5.93e-14 -8.6 5.95e-05 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 26\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 6.1800652712078263e-01 6.1800652712078263e-01\n", - "Dual infeasibility......: 5.9277911925848335e-14 5.9277911925848335e-14\n", - "Constraint violation....: 4.3220982348657344e-13 4.3220982348657344e-13\n", - "Complementarity.........: 2.5059035734816410e-09 2.5059035734816410e-09\n", - "Overall NLP error.......: 2.5059035734816410e-09 2.5059035734816410e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 31\n", - "Number of objective gradient evaluations = 27\n", - "Number of equality constraint evaluations = 31\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 27\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 26\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.926\n", - "Total CPU secs in NLP function evaluations = 0.031\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.7 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.29e-03 4.38e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 1.87e-09 1.41e-06 -1.0 1.63e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.8712897897898984e-09 1.8712897897898984e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.8712897897898984e-09 1.8712897897898984e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.103\n", - "Total CPU secs in NLP function evaluations = 0.002\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.29e-03 4.38e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 1.87e-09 1.41e-06 -1.0 1.63e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.8712982274848855e-09 1.8712982274848855e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.8712982274848855e-09 1.8712982274848855e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.098\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.07e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 6.84e+02 3.85e+02 -1.0 1.07e+04 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 7.77e+00 3.85e+00 -1.0 7.31e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 7.68e-02 4.39e+00 -1.0 8.25e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.16e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.129\n", - "Total CPU secs in NLP function evaluations = 0.003\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.2649276e+01 1.25e+00 1.05e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.2650421e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", - " 2 -1.2649502e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.2654364e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.2645035e+01 1.67e+01 2.20e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.2646341e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.13e-01f 2\n", - " 6 -1.2642660e+01 1.23e+01 3.32e+00 -1.0 9.23e+01 - 8.75e-01 3.35e-01f 2\n", - " 7 -1.2639134e+01 3.65e+00 5.36e-01 -1.0 5.24e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.2640660e+01 4.49e-01 7.92e-02 -1.0 1.10e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.2640661e+01 2.42e-04 1.25e-03 -1.7 3.72e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.2640701e+01 2.64e-04 9.04e-04 -3.8 3.77e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 -1.2645246e+01 3.62e+00 2.60e-02 -5.7 4.21e+01 - 7.44e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (319822)\n", - " 12 -1.2650749e+01 4.98e+00 1.49e-02 -5.7 7.45e+01 - 8.19e-01 1.00e+00h 1\n", - " 13 -1.2654062e+01 5.16e+00 1.87e-02 -5.7 3.19e+01 - 9.04e-01 1.00e+00h 1\n", - " 14 -1.2654011e+01 4.69e+00 5.08e-02 -5.7 1.46e+01 -4.0 1.00e+00 9.14e-02h 1\n", - " 15 -1.2654035e+01 7.16e-01 2.01e-02 -5.7 3.14e+01 - 1.00e+00 1.00e+00h 1\n", - " 16 -1.2653941e+01 1.00e-03 9.08e-05 -5.7 3.69e-01 -4.5 1.00e+00 1.00e+00h 1\n", - " 17 -1.2654241e+01 6.33e-01 5.16e-03 -5.7 1.06e+02 - 1.00e+00 1.70e-01h 2\n", - " 18 -1.2654880e+01 3.29e-01 2.15e-03 -5.7 1.63e+01 - 1.00e+00 1.00e+00h 1\n", - " 19 -1.2654796e+01 6.21e-03 3.76e-05 -5.7 1.47e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.2654797e+01 9.53e-06 4.39e-08 -5.7 1.04e-01 - 1.00e+00 1.00e+00h 1\n", - " 21 -1.2655412e+01 4.92e-01 1.17e-03 -8.6 2.19e+01 - 8.97e-01 9.66e-01h 1\n", - "Reallocating memory for MA57: lfact (343005)\n", - " 22 -1.2655443e+01 1.44e-02 2.56e-04 -8.6 1.06e+01 - 9.92e-01 1.00e+00h 1\n", - " 23 -1.2655449e+01 3.08e-03 5.46e-05 -8.6 4.98e+00 - 1.00e+00 1.00e+00h 1\n", - " 24 -1.2655450e+01 1.28e-04 2.92e-06 -8.6 1.02e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (360209)\n", - " 25 -1.2655450e+01 1.79e-07 8.58e-09 -8.6 3.83e-02 - 1.00e+00 1.00e+00h 1\n", - " 26 -1.2655450e+01 1.82e-12 1.06e-13 -8.6 9.45e-05 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 26\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.2655450125163370e+01 -1.2655450125163370e+01\n", - "Dual infeasibility......: 1.0562293264999832e-13 1.0562293264999832e-13\n", - "Constraint violation....: 1.0876854972252659e-12 1.8189894035458565e-12\n", - "Complementarity.........: 2.5059035921766028e-09 2.5059035921766028e-09\n", - "Overall NLP error.......: 2.5059035921766028e-09 2.5059035921766028e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 37\n", - "Number of objective gradient evaluations = 27\n", - "Number of equality constraint evaluations = 37\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 27\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 26\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.992\n", - "Total CPU secs in NLP function evaluations = 0.043\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.7 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 4.69e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 4.27e+00 3.85e+02 -1.0 4.64e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.87e-02 3.85e+00 -1.0 4.57e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.81e-04 4.39e+00 -1.0 5.17e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.11e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.134\n", - "Total CPU secs in NLP function evaluations = 0.005\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 5.0432833e-01 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 5.0361720e-01 1.56e-01 1.04e+00 -1.0 5.95e-01 - 9.79e-01 8.75e-01h 1\n", - " 2 5.0409878e-01 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 4.9931925e-01 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", - " 4 5.0927300e-01 7.12e-01 2.48e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 5.0699996e-01 5.85e-01 4.72e+01 -1.0 3.47e+02 - 4.76e-01 8.88e-02f 2\n", - " 6 5.1324183e-01 1.37e-01 6.01e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", - " 7 5.1217900e-01 4.34e-01 2.96e-01 -1.0 5.48e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 5.1271052e-01 3.84e-02 5.77e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 5.1267962e-01 5.41e-03 3.90e-03 -2.5 6.36e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 5.1241548e-01 5.08e-04 5.15e-04 -3.8 9.59e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 5.0807961e-01 3.17e-01 2.48e-02 -5.7 2.63e+01 - 7.57e-01 1.00e+00h 1\n", - " 12 5.0278293e-01 8.71e-01 1.15e-02 -5.7 5.29e+01 - 8.62e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (326106)\n", - " 13 4.9913241e-01 7.64e-01 2.78e-02 -5.7 3.15e+01 - 8.71e-01 1.00e+00h 1\n", - " 14 4.9997874e-01 1.14e-02 5.09e-04 -5.7 7.44e-01 -4.5 9.85e-01 1.00e+00h 1\n", - " 15 4.9994319e-01 6.15e-07 2.09e-07 -5.7 1.61e-02 -5.0 1.00e+00 1.00e+00h 1\n", - " 16 4.9991846e-01 5.82e-06 2.33e-06 -8.6 9.55e-02 -5.4 9.99e-01 1.00e+00h 1\n", - " 17 4.9989124e-01 5.24e-05 1.77e-06 -8.6 2.92e-01 -5.9 1.00e+00 1.00e+00h 1\n", - " 18 4.9981470e-01 4.48e-04 1.51e-05 -8.6 8.57e-01 -6.4 1.00e+00 1.00e+00h 1\n", - " 19 4.9961545e-01 3.50e-03 1.17e-04 -8.6 2.43e+00 -6.9 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 4.9957792e-01 3.35e-03 1.12e-04 -8.6 6.02e+00 -7.3 1.00e+00 8.63e-02h 1\n", - " 21 4.9902524e-01 3.02e-02 1.14e-03 -8.6 7.74e+00 -7.8 1.00e+00 1.00e+00f 1\n", - " 22 4.9848008e-01 2.40e-01 1.30e-02 -8.6 7.13e+01 -8.3 1.00e+00 4.02e-01h 1\n", - " 23 4.9828290e-01 1.29e-01 7.02e-03 -8.6 4.11e+01 - 1.00e+00 4.70e-01f 1\n", - " 24 4.9823261e-01 9.68e-02 5.22e-03 -8.6 3.63e+01 - 1.00e+00 2.57e-01f 1\n", - " 25 4.9820105e-01 6.31e-02 3.38e-03 -8.6 3.10e+01 - 1.00e+00 3.52e-01f 1\n", - " 26 4.9815087e-01 2.36e-03 6.18e-05 -8.6 2.04e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (361644)\n", - " 27 4.9815415e-01 3.57e-04 1.71e-07 -8.6 1.71e+00 - 1.00e+00 1.00e+00h 1\n", - " 28 4.9815414e-01 1.50e-06 6.26e-10 -8.6 1.11e-01 - 1.00e+00 1.00e+00h 1\n", - " 29 4.9815414e-01 1.71e-11 3.34e-14 -8.6 3.75e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 29\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 4.9815414100226940e-01 4.9815414100226940e-01\n", - "Dual infeasibility......: 3.3405414623680940e-14 3.3405414623680940e-14\n", - "Constraint violation....: 1.7140733277187792e-11 1.7140733277187792e-11\n", - "Complementarity.........: 2.5059036238671544e-09 2.5059036238671544e-09\n", - "Overall NLP error.......: 2.5059036238671544e-09 2.5059036238671544e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 34\n", - "Number of objective gradient evaluations = 30\n", - "Number of equality constraint evaluations = 34\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 30\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 29\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.235\n", - "Total CPU secs in NLP function evaluations = 0.038\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.27e-03 4.61e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 1.84e-09 1.40e-06 -1.0 1.61e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.8368555565473343e-09 1.8368555565473343e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.8368555565473343e-09 1.8368555565473343e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.105\n", - "Total CPU secs in NLP function evaluations = 0.003\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.27e-03 4.61e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 1.84e-09 1.40e-06 -1.0 1.61e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.8368631060639018e-09 1.8368631060639018e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.8368631060639018e-09 1.8368631060639018e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.102\n", - "Total CPU secs in NLP function evaluations = 0.003\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.15e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 6.92e+02 3.85e+02 -1.0 1.15e+04 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 7.85e+00 3.85e+00 -1.0 7.38e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 7.76e-02 4.39e+00 -1.0 8.33e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.23e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0367393568158150e-09 9.0367393568158150e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0367393568158150e-09 9.0367393568158150e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.126\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.2761788e+01 1.25e+00 1.05e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.2762860e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", - " 2 -1.2761992e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.2766548e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.2757815e+01 1.67e+01 2.19e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.2759034e+01 1.18e+01 3.20e+01 -1.0 1.36e+02 - 1.00e+00 4.12e-01f 2\n", - " 6 -1.2755587e+01 1.23e+01 3.32e+00 -1.0 9.23e+01 - 8.75e-01 3.35e-01f 2\n", - " 7 -1.2752282e+01 3.66e+00 5.36e-01 -1.0 5.24e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.2753712e+01 4.50e-01 7.94e-02 -1.0 1.10e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.2753713e+01 2.43e-04 1.22e-03 -1.7 3.76e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.2753748e+01 2.32e-04 8.49e-04 -3.8 3.54e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 -1.2753835e+01 1.60e-04 8.09e-05 -5.7 7.08e-01 -4.0 1.00e+00 1.00e+00h 1\n", - " 12 -1.2754158e+01 2.17e-03 8.72e-05 -5.7 2.61e+00 -4.5 1.00e+00 1.00e+00h 1\n", - " 13 -1.2754496e+01 3.46e-03 5.33e-04 -5.7 5.48e+00 -5.0 1.00e+00 4.99e-01h 1\n", - " 14 -1.2754536e+01 1.11e-03 5.18e-04 -5.7 2.85e-01 -5.4 1.00e+00 1.00e+00f 1\n", - " 15 -1.2754778e+01 1.03e-02 2.28e-05 -5.7 1.85e+00 -5.9 1.00e+00 1.00e+00h 1\n", - " 16 -1.2755412e+01 8.41e-02 9.87e-05 -5.7 4.74e+00 -6.4 1.00e+00 1.00e+00h 1\n", - " 17 -1.2757064e+01 5.97e-01 6.84e-04 -5.7 1.17e+01 -6.9 1.00e+00 1.00e+00h 1\n", - " 18 -1.2760500e+01 2.94e+00 4.15e-03 -5.7 1.95e+01 -7.3 1.00e+00 1.00e+00h 1\n", - " 19 -1.2761019e+01 9.59e+00 2.25e-02 -5.7 6.29e+02 - 1.13e-01 1.41e-01h 2\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.2764864e+01 1.90e+01 9.25e-02 -5.7 1.73e+02 - 3.33e-01 3.01e-01h 1\n", - " 21 -1.2764680e+01 1.16e+01 1.82e-01 -5.7 2.07e+02 - 3.79e-01 7.21e-01H 1\n", - " 22 -1.2767567e+01 1.81e+01 2.45e-01 -5.7 1.01e+02 - 1.00e+00 1.00e+00f 1\n", - " 23 -1.2767317e+01 1.23e+01 1.12e-01 -5.7 1.91e+02 - 1.00e+00 5.53e-01h 1\n", - "Reallocating memory for MA57: lfact (319005)\n", - " 24 -1.2767311e+01 8.83e+00 7.94e-02 -5.7 8.23e+01 - 8.73e-01 9.30e-01f 1\n", - " 25 -1.2766311e+01 1.99e+00 2.60e-02 -5.7 3.16e+01 - 1.00e+00 1.00e+00f 1\n", - " 26 -1.2766561e+01 2.73e-01 7.90e-04 -5.7 2.07e+01 -7.8 1.00e+00 1.00e+00h 1\n", - " 27 -1.2766765e+01 6.11e-01 2.75e-03 -5.7 3.40e+02 - 4.24e-01 4.80e-02h 2\n", - " 28 -1.2766866e+01 3.52e-02 1.15e-04 -5.7 5.75e+00 - 1.00e+00 1.00e+00h 1\n", - " 29 -1.2766913e+01 1.08e-02 5.42e-05 -5.7 3.27e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 30 -1.2766912e+01 5.95e-06 4.85e-08 -5.7 5.76e-02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (340818)\n", - " 31 -1.2767524e+01 5.55e-01 1.25e-03 -8.6 2.33e+01 - 8.91e-01 9.61e-01h 1\n", - " 32 -1.2767558e+01 1.54e-02 2.57e-04 -8.6 1.09e+01 - 9.90e-01 1.00e+00h 1\n", - " 33 -1.2767563e+01 3.45e-03 5.70e-05 -8.6 5.26e+00 - 1.00e+00 1.00e+00h 1\n", - " 34 -1.2767564e+01 1.62e-04 3.39e-06 -8.6 1.15e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (358770)\n", - " 35 -1.2767564e+01 2.90e-07 1.23e-08 -8.6 4.88e-02 - 1.00e+00 1.00e+00h 1\n", - " 36 -1.2767564e+01 2.51e-12 1.99e-13 -8.6 1.44e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 36\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.2767564482596377e+01 -1.2767564482596377e+01\n", - "Dual infeasibility......: 1.9925310697126672e-13 1.9925310697126672e-13\n", - "Constraint violation....: 2.5131008385415043e-12 2.5131008385415043e-12\n", - "Complementarity.........: 2.5059036300337093e-09 2.5059036300337093e-09\n", - "Overall NLP error.......: 2.5059036300337093e-09 2.5059036300337093e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 51\n", - "Number of objective gradient evaluations = 37\n", - "Number of equality constraint evaluations = 51\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 37\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 36\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.271\n", - "Total CPU secs in NLP function evaluations = 0.053\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 5.03e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 4.31e+00 3.85e+02 -1.0 4.98e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.90e-02 3.85e+00 -1.0 4.60e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.84e-04 4.39e+00 -1.0 5.20e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.14e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8342664033261826e-11 5.8342664033261826e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8342664033261826e-11 5.8342664033261826e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.128\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 3.9181573e-01 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 3.9115328e-01 1.56e-01 1.04e+00 -1.0 5.95e-01 - 9.79e-01 8.75e-01h 1\n", - " 2 3.9160960e-01 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 3.8712768e-01 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", - " 4 3.9644619e-01 7.12e-01 2.48e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 3.9432311e-01 5.85e-01 4.73e+01 -1.0 3.46e+02 - 4.77e-01 8.89e-02f 2\n", - " 6 4.0017008e-01 1.36e-01 6.05e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", - " 7 3.9917425e-01 4.34e-01 2.97e-01 -1.0 5.49e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 3.9967295e-01 3.84e-02 5.77e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 3.9964620e-01 5.42e-03 3.94e-03 -2.5 6.37e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 3.9941445e-01 5.23e-04 4.68e-04 -3.8 8.72e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 3.9556789e-01 2.81e-01 2.38e-02 -5.7 2.46e+01 - 7.68e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (320660)\n", - " 12 3.9049239e-01 8.99e-01 1.14e-02 -5.7 5.36e+01 - 8.66e-01 1.00e+00h 1\n", - " 13 3.8696936e-01 7.77e-01 2.66e-02 -5.7 3.18e+01 - 8.89e-01 1.00e+00h 1\n", - " 14 3.8779264e-01 1.12e-02 4.89e-04 -5.7 7.59e-01 -4.5 9.97e-01 1.00e+00h 1\n", - " 15 3.8775852e-01 6.21e-07 1.98e-07 -5.7 1.51e-02 -5.0 1.00e+00 1.00e+00h 1\n", - " 16 3.8730660e-01 3.80e-01 2.43e-02 -8.6 1.44e+04 - 7.13e-03 2.64e-03h 1\n", - " 17 3.8571434e-01 1.57e-01 4.82e-03 -8.6 3.21e+01 - 8.01e-01 1.00e+00h 1\n", - " 18 3.8604273e-01 1.97e-02 3.18e-04 -8.6 1.23e+01 - 9.78e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (352373)\n", - " 19 3.8603975e-01 2.68e-03 8.27e-06 -8.6 4.65e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 3.8603975e-01 4.52e-05 4.68e-08 -8.6 6.09e-01 - 1.00e+00 1.00e+00h 1\n", - " 21 3.8603975e-01 1.34e-08 4.74e-12 -8.6 1.05e-02 - 1.00e+00 1.00e+00h 1\n", - " 22 3.8603911e-01 1.37e-06 2.67e-08 -9.0 1.06e-01 - 1.00e+00 1.00e+00h 1\n", - " 23 3.8603911e-01 4.37e-11 1.28e-12 -9.0 5.99e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 23\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 3.8603910736158609e-01 3.8603910736158609e-01\n", - "Dual infeasibility......: 1.2773062897591488e-12 1.2773062897591488e-12\n", - "Constraint violation....: 4.3745562727792731e-11 4.3745562727792731e-11\n", - "Complementarity.........: 9.0909138582673236e-10 9.0909138582673236e-10\n", - "Overall NLP error.......: 9.0909138582673236e-10 9.0909138582673236e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 28\n", - "Number of objective gradient evaluations = 24\n", - "Number of equality constraint evaluations = 28\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 24\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 23\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.951\n", - "Total CPU secs in NLP function evaluations = 0.031\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.7 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.26e-03 4.84e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 1.80e-09 1.40e-06 -1.0 1.60e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.8027450643387510e-09 1.8027450643387510e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.8027450643387510e-09 1.8027450643387510e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.38e-03 2.49e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 2.17e-09 1.48e-06 -1.0 1.75e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.1657315940615263e-09 2.1657315940615263e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.1657315940615263e-09 2.1657315940615263e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.106\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 0.9 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.23e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 6.99e+02 3.85e+02 -1.0 1.23e+04 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 7.92e+00 3.85e+00 -1.0 7.46e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 7.83e-02 4.39e+00 -1.0 8.40e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.31e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.130\n", - "Total CPU secs in NLP function evaluations = 0.008\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.2867454e+01 1.25e+00 1.05e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.2868460e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", - " 2 -1.2867639e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.2871924e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.2863717e+01 1.67e+01 2.19e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.2864858e+01 1.18e+01 3.21e+01 -1.0 1.36e+02 - 1.00e+00 4.12e-01f 2\n", - " 6 -1.2861618e+01 1.23e+01 3.32e+00 -1.0 9.24e+01 - 8.74e-01 3.35e-01f 2\n", - " 7 -1.2858508e+01 3.66e+00 5.37e-01 -1.0 5.24e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.2859854e+01 4.51e-01 7.96e-02 -1.0 1.10e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.2859854e+01 2.44e-04 1.19e-03 -1.7 3.79e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.2859885e+01 2.06e-04 8.00e-04 -3.8 3.33e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 -1.2863468e+01 2.88e+00 2.39e-02 -5.7 3.78e+01 - 7.65e-01 1.00e+00h 1\n", - " 12 -1.2868524e+01 5.45e+00 1.49e-02 -5.7 7.69e+01 - 8.12e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (324125)\n", - " 13 -1.2871542e+01 4.87e+00 1.57e-02 -5.7 3.27e+01 - 9.43e-01 1.00e+00h 1\n", - " 14 -1.2871505e+01 4.47e+00 5.03e-02 -5.7 1.42e+01 -4.0 1.00e+00 8.24e-02h 1\n", - " 15 -1.2871544e+01 7.88e-01 1.77e-02 -5.7 3.24e+01 - 1.00e+00 1.00e+00h 1\n", - " 16 -1.2871459e+01 1.19e-03 1.13e-04 -5.7 4.55e-01 -4.5 1.00e+00 1.00e+00h 1\n", - " 17 -1.2871817e+01 6.63e-01 4.65e-03 -5.7 4.38e+01 - 1.00e+00 4.24e-01h 2\n", - " 18 -1.2872068e+01 6.22e-01 3.65e-03 -5.7 3.60e+01 - 1.00e+00 3.97e-01h 2\n", - " 19 -1.2872235e+01 1.80e-02 4.42e-04 -5.7 5.48e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.2872228e+01 2.81e-04 1.04e-06 -5.7 7.92e-01 - 1.00e+00 1.00e+00h 1\n", - " 21 -1.2872228e+01 6.84e-09 4.81e-11 -5.7 2.27e-03 - 1.00e+00 1.00e+00h 1\n", - " 22 -1.2872836e+01 6.22e-01 1.32e-03 -8.6 2.46e+01 - 8.84e-01 9.57e-01h 1\n", - "Reallocating memory for MA57: lfact (352404)\n", - " 23 -1.2872873e+01 1.64e-02 2.58e-04 -8.6 1.12e+01 - 9.88e-01 1.00e+00h 1\n", - " 24 -1.2872878e+01 3.83e-03 5.93e-05 -8.6 5.54e+00 - 1.00e+00 1.00e+00h 1\n", - " 25 -1.2872880e+01 2.02e-04 3.88e-06 -8.6 1.28e+00 - 1.00e+00 1.00e+00h 1\n", - " 26 -1.2872880e+01 4.51e-07 1.70e-08 -8.6 6.08e-02 - 1.00e+00 1.00e+00h 1\n", - " 27 -1.2872880e+01 5.38e-12 3.77e-13 -8.6 2.10e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 27\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.2872879704090735e+01 -1.2872879704090735e+01\n", - "Dual infeasibility......: 3.7744241620007363e-13 3.7744241620007363e-13\n", - "Constraint violation....: 5.3804738442408961e-12 5.3804738442408961e-12\n", - "Complementarity.........: 2.5059037015571048e-09 2.5059037015571048e-09\n", - "Overall NLP error.......: 2.5059037015571048e-09 2.5059037015571048e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 43\n", - "Number of objective gradient evaluations = 28\n", - "Number of equality constraint evaluations = 43\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 28\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 27\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.074\n", - "Total CPU secs in NLP function evaluations = 0.067\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 3.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 5.38e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 4.34e+00 3.85e+02 -1.0 5.32e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.93e-02 3.85e+00 -1.0 4.63e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.87e-04 4.39e+00 -1.0 5.23e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.17e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.127\n", - "Total CPU secs in NLP function evaluations = 0.007\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 2.8614991e-01 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 2.8552993e-01 1.56e-01 1.04e+00 -1.0 5.94e-01 - 9.79e-01 8.75e-01h 1\n", - " 2 2.8596341e-01 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 2.8174414e-01 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", - " 4 2.9050364e-01 7.12e-01 2.47e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 2.8851199e-01 5.85e-01 4.73e+01 -1.0 3.46e+02 - 4.77e-01 8.91e-02f 2\n", - " 6 2.9401108e-01 1.36e-01 6.08e+00 -1.0 2.40e+01 -4.0 8.22e-01 1.00e+00F 1\n", - " 7 2.9307524e-01 4.34e-01 2.97e-01 -1.0 5.49e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 2.9354482e-01 3.83e-02 5.77e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 2.9352137e-01 5.43e-03 3.97e-03 -2.5 6.38e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 2.9331638e-01 5.29e-04 4.27e-04 -3.8 8.79e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 2.8988055e-01 2.51e-01 2.28e-02 -5.7 2.32e+01 - 7.77e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (319389)\n", - " 12 2.8501317e-01 9.24e-01 1.13e-02 -5.7 5.42e+01 - 8.69e-01 1.00e+00h 1\n", - " 13 2.8160648e-01 7.91e-01 2.55e-02 -5.7 3.22e+01 - 9.07e-01 1.00e+00h 1\n", - " 14 2.8240769e-01 1.11e-02 4.72e-04 -5.7 7.74e-01 -4.5 1.00e+00 1.00e+00h 1\n", - " 15 2.8237479e-01 6.27e-07 1.90e-07 -5.7 1.42e-02 -5.0 1.00e+00 1.00e+00h 1\n", - " 16 2.8187382e-01 3.93e-01 2.36e-02 -8.6 6.25e+02 - 1.42e-01 6.14e-02h 1\n", - " 17 2.8044338e-01 1.41e-01 4.08e-03 -8.6 3.11e+01 - 8.06e-01 1.00e+00h 1\n", - " 18 2.8072577e-01 1.89e-02 2.94e-04 -8.6 1.21e+01 - 9.80e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (348083)\n", - " 19 2.8072395e-01 2.58e-03 1.02e-05 -8.6 4.56e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 2.8072390e-01 4.67e-05 1.13e-07 -8.6 6.18e-01 - 1.00e+00 1.00e+00h 1\n", - " 21 2.8072390e-01 1.53e-08 1.88e-11 -8.6 1.12e-02 - 1.00e+00 1.00e+00h 1\n", - " 22 2.8072325e-01 1.54e-06 2.83e-08 -9.0 1.12e-01 - 1.00e+00 1.00e+00h 1\n", - " 23 2.8072325e-01 5.55e-11 1.52e-12 -9.0 6.75e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 23\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 2.8072325129350517e-01 2.8072325129350517e-01\n", - "Dual infeasibility......: 1.5217814371823493e-12 1.5217814371823493e-12\n", - "Constraint violation....: 5.5502491491665751e-11 5.5502491491665751e-11\n", - "Complementarity.........: 9.0909147761315399e-10 9.0909147761315399e-10\n", - "Overall NLP error.......: 9.0909147761315399e-10 9.0909147761315399e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 28\n", - "Number of objective gradient evaluations = 24\n", - "Number of equality constraint evaluations = 28\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 24\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 23\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.822\n", - "Total CPU secs in NLP function evaluations = 0.040\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.6 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.25e-03 5.07e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 1.77e-09 1.39e-06 -1.0 1.58e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.7689547604504696e-09 1.7689547604504696e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.7689547604504696e-09 1.7689547604504696e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", - "Total CPU secs in NLP function evaluations = 0.005\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.38e-03 2.57e-01 -1.0 1.85e-01 - 9.91e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 2.15e-09 1.48e-06 -1.0 1.75e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.1522383875094420e-09 2.1522383875094420e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.1522383875094420e-09 2.1522383875094420e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.101\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.0 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.30e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 7.07e+02 3.85e+02 -1.0 1.30e+04 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 8.00e+00 3.85e+00 -1.0 7.54e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 7.91e-02 4.39e+00 -1.0 8.48e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.38e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.129\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.2967058e+01 1.25e+00 1.04e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.2968006e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.06e-01h 1\n", - " 2 -1.2967227e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.2971272e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.2963530e+01 1.67e+01 2.19e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.2964604e+01 1.18e+01 3.21e+01 -1.0 1.36e+02 - 1.00e+00 4.12e-01f 2\n", - " 6 -1.2961547e+01 1.23e+01 3.32e+00 -1.0 9.24e+01 - 8.74e-01 3.35e-01f 2\n", - " 7 -1.2958610e+01 3.66e+00 5.37e-01 -1.0 5.25e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.2959880e+01 4.51e-01 7.97e-02 -1.0 1.11e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.2959880e+01 2.45e-04 1.16e-03 -1.7 3.82e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.2959908e+01 1.85e-04 7.57e-04 -3.8 3.15e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 -1.2963121e+01 2.60e+00 2.30e-02 -5.7 3.59e+01 - 7.75e-01 1.00e+00h 1\n", - " 12 -1.2967974e+01 5.66e+00 1.48e-02 -5.7 7.78e+01 - 8.10e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (319946)\n", - " 13 -1.2970867e+01 4.77e+00 1.46e-02 -5.7 3.30e+01 - 9.60e-01 1.00e+00h 1\n", - " 14 -1.2970834e+01 4.39e+00 4.99e-02 -5.7 1.40e+01 -4.0 1.00e+00 7.87e-02h 1\n", - " 15 -1.2970871e+01 8.04e-01 1.66e-02 -5.7 3.26e+01 - 1.00e+00 1.00e+00h 1\n", - " 16 -1.2970791e+01 1.23e-03 1.24e-04 -5.7 4.78e-01 -4.5 1.00e+00 1.00e+00h 1\n", - " 17 -1.2971470e+01 2.07e+00 1.31e-02 -5.7 3.26e+01 - 1.00e+00 1.00e+00h 1\n", - " 18 -1.2971390e+01 2.65e-01 2.02e-03 -5.7 1.09e+01 - 1.00e+00 1.00e+00h 1\n", - " 19 -1.2971539e+01 1.75e-01 7.49e-04 -5.7 1.26e+01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.2971523e+01 4.44e-03 2.61e-05 -5.7 1.85e+00 - 1.00e+00 1.00e+00h 1\n", - " 21 -1.2971523e+01 2.35e-06 1.59e-08 -5.7 3.77e-02 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (342795)\n", - " 22 -1.2972127e+01 6.92e-01 1.39e-03 -8.6 2.60e+01 - 8.78e-01 9.52e-01h 1\n", - " 23 -1.2972167e+01 1.73e-02 2.59e-04 -8.6 1.15e+01 - 9.86e-01 1.00e+00h 1\n", - " 24 -1.2972172e+01 4.22e-03 6.14e-05 -8.6 5.81e+00 - 1.00e+00 1.00e+00h 1\n", - " 25 -1.2972173e+01 2.46e-04 4.38e-06 -8.6 1.42e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (361301)\n", - " 26 -1.2972174e+01 6.76e-07 2.28e-08 -8.6 7.45e-02 - 1.00e+00 1.00e+00h 1\n", - " 27 -1.2972174e+01 1.08e-11 6.93e-13 -8.6 2.98e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 27\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.2972173508586897e+01 -1.2972173508586897e+01\n", - "Dual infeasibility......: 6.9302411901920606e-13 6.9302411901920606e-13\n", - "Constraint violation....: 1.0807355010911124e-11 1.0807355010911124e-11\n", - "Complementarity.........: 2.5059038289136340e-09 2.5059038289136340e-09\n", - "Overall NLP error.......: 2.5059038289136340e-09 2.5059038289136340e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 34\n", - "Number of objective gradient evaluations = 28\n", - "Number of equality constraint evaluations = 34\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 28\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 27\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.039\n", - "Total CPU secs in NLP function evaluations = 0.034\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.9 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 5.72e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 4.37e+00 3.85e+02 -1.0 5.66e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.96e-02 3.85e+00 -1.0 4.66e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.91e-04 4.39e+00 -1.0 5.27e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.21e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8335558605904225e-11 5.8335558605904225e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.130\n", - "Total CPU secs in NLP function evaluations = 0.005\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 1.8654549e-01 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 1.8596289e-01 1.56e-01 1.03e+00 -1.0 5.94e-01 - 9.79e-01 8.75e-01h 1\n", - " 2 1.8637565e-01 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 1.8238996e-01 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", - " 4 1.9065372e-01 7.12e-01 2.47e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 1.8877820e-01 5.85e-01 4.73e+01 -1.0 3.46e+02 - 4.78e-01 8.92e-02f 2\n", - " 6 1.9396149e-01 1.37e-01 6.06e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", - " 7 1.9308197e-01 4.34e-01 2.96e-01 -1.0 5.49e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 1.9352656e-01 3.83e-02 5.78e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 1.9350624e-01 5.45e-03 4.00e-03 -2.5 6.39e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 1.9332380e-01 5.30e-04 3.89e-04 -3.8 9.48e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 1.9023661e-01 2.26e-01 2.20e-02 -5.7 2.19e+01 - 7.86e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (320076)\n", - " 12 1.8556544e-01 9.45e-01 1.12e-02 -5.7 5.48e+01 - 8.73e-01 1.00e+00h 1\n", - " 13 1.8226712e-01 8.03e-01 2.46e-02 -5.7 3.25e+01 - 9.25e-01 1.00e+00h 1\n", - " 14 1.8304698e-01 1.11e-02 4.57e-04 -5.7 7.87e-01 -4.5 1.00e+00 1.00e+00h 1\n", - " 15 1.8301518e-01 6.34e-07 1.83e-07 -5.7 1.33e-02 -5.0 1.00e+00 1.00e+00h 1\n", - " 16 1.8246962e-01 4.04e-01 2.28e-02 -8.6 3.22e+02 - 2.45e-01 1.20e-01h 1\n", - " 17 1.8118955e-01 1.25e-01 3.47e-03 -8.6 2.99e+01 - 8.12e-01 1.00e+00h 1\n", - " 18 1.8143090e-01 1.83e-02 2.70e-04 -8.6 1.19e+01 - 9.83e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (352539)\n", - " 19 1.8142961e-01 2.51e-03 1.26e-05 -8.6 4.50e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 1.8142950e-01 4.81e-05 1.90e-07 -8.6 6.28e-01 - 1.00e+00 1.00e+00h 1\n", - " 21 1.8142949e-01 1.74e-08 5.29e-11 -8.6 1.19e-02 - 1.00e+00 1.00e+00h 1\n", - " 22 1.8142885e-01 1.72e-06 2.99e-08 -9.0 1.19e-01 - 1.00e+00 1.00e+00h 1\n", - " 23 1.8142885e-01 6.95e-11 1.80e-12 -9.0 7.55e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 23\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 1.8142884693002581e-01 1.8142884693002581e-01\n", - "Dual infeasibility......: 1.7962352785824499e-12 1.7962352785824499e-12\n", - "Constraint violation....: 6.9450334372334055e-11 6.9450334372334055e-11\n", - "Complementarity.........: 9.0909158006744088e-10 9.0909158006744088e-10\n", - "Overall NLP error.......: 9.0909158006744088e-10 9.0909158006744088e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 28\n", - "Number of objective gradient evaluations = 24\n", - "Number of equality constraint evaluations = 28\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 24\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 23\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.945\n", - "Total CPU secs in NLP function evaluations = 0.037\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.7 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.24e-03 5.30e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 1.74e-09 1.38e-06 -1.0 1.57e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.7354877535069591e-09 1.7354877535069591e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.7354877535069591e-09 1.7354877535069591e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.107\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.38e-03 2.66e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 2.14e-09 1.48e-06 -1.0 1.74e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.1387873694322934e-09 2.1387873694322934e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.1387873694322934e-09 2.1387873694322934e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.107\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.3 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.38e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 7.15e+02 3.85e+02 -1.0 1.38e+04 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 8.08e+00 3.85e+00 -1.0 7.61e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 7.99e-02 4.39e+00 -1.0 8.56e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.46e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0367393568158150e-09 9.0367393568158150e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0367393568158150e-09 9.0367393568158150e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.129\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.3061258e+01 1.25e+00 1.04e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.3062155e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.07e-01h 1\n", - " 2 -1.3061413e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.3065245e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.3057918e+01 1.67e+01 2.19e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.3058931e+01 1.18e+01 3.21e+01 -1.0 1.36e+02 - 1.00e+00 4.12e-01f 2\n", - " 6 -1.3056038e+01 1.23e+01 3.32e+00 -1.0 9.24e+01 - 8.74e-01 3.35e-01f 2\n", - " 7 -1.3053256e+01 3.67e+00 5.37e-01 -1.0 5.25e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.3054459e+01 4.52e-01 7.98e-02 -1.0 1.11e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.3054459e+01 2.46e-04 1.14e-03 -1.7 3.85e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.3054484e+01 1.66e-04 7.18e-04 -3.8 2.99e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 -1.3057381e+01 2.35e+00 2.21e-02 -5.7 3.43e+01 - 7.84e-01 1.00e+00h 1\n", - " 12 -1.3062042e+01 5.84e+00 1.46e-02 -5.7 7.86e+01 - 8.08e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (325047)\n", - " 13 -1.3064822e+01 4.69e+00 1.36e-02 -5.7 3.33e+01 - 9.76e-01 1.00e+00h 1\n", - " 14 -1.3064793e+01 4.33e+00 4.95e-02 -5.7 1.38e+01 -4.0 1.00e+00 7.55e-02h 1\n", - " 15 -1.3064825e+01 8.10e-01 1.55e-02 -5.7 3.27e+01 - 1.00e+00 1.00e+00h 1\n", - " 16 -1.3064750e+01 1.24e-03 1.35e-04 -5.7 4.92e-01 -4.5 1.00e+00 1.00e+00h 1\n", - " 17 -1.3065327e+01 1.32e+00 8.45e-03 -5.7 2.57e+01 - 1.00e+00 1.00e+00h 1\n", - " 18 -1.3065563e+01 3.66e-01 9.07e-04 -5.7 2.45e+01 - 1.00e+00 1.00e+00h 1\n", - " 19 -1.3065455e+01 8.75e-02 3.46e-04 -5.7 9.07e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.3065447e+01 1.22e-03 6.52e-06 -5.7 9.79e-01 - 1.00e+00 1.00e+00h 1\n", - " 21 -1.3065447e+01 1.73e-07 1.14e-09 -5.7 1.04e-02 - 1.00e+00 1.00e+00h 1\n", - " 22 -1.3066047e+01 7.66e-01 1.47e-03 -8.6 2.73e+01 - 8.72e-01 9.48e-01h 1\n", - "Reallocating memory for MA57: lfact (349926)\n", - " 23 -1.3066091e+01 1.83e-02 2.59e-04 -8.6 1.18e+01 - 9.84e-01 1.00e+00h 1\n", - " 24 -1.3066096e+01 4.62e-03 6.34e-05 -8.6 6.08e+00 - 1.00e+00 1.00e+00h 1\n", - " 25 -1.3066097e+01 2.96e-04 4.89e-06 -8.6 1.55e+00 - 1.00e+00 1.00e+00h 1\n", - " 26 -1.3066097e+01 9.81e-07 2.99e-08 -8.6 8.97e-02 - 1.00e+00 1.00e+00h 1\n", - " 27 -1.3066097e+01 2.05e-11 1.23e-12 -8.6 4.11e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 27\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.3066097364112752e+01 -1.3066097364112752e+01\n", - "Dual infeasibility......: 1.2294764961011457e-12 1.2294764961011457e-12\n", - "Constraint violation....: 2.0529578037553620e-11 2.0529578037553620e-11\n", - "Complementarity.........: 2.5059040443903054e-09 2.5059040443903054e-09\n", - "Overall NLP error.......: 2.5059040443903054e-09 2.5059040443903054e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 34\n", - "Number of objective gradient evaluations = 28\n", - "Number of equality constraint evaluations = 34\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 28\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 27\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.154\n", - "Total CPU secs in NLP function evaluations = 0.047\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.8 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.06e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 4.40e+00 3.85e+02 -1.0 6.00e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 4.99e-02 3.85e+00 -1.0 4.69e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.94e-04 4.39e+00 -1.0 5.30e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.24e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8342664033261826e-11 5.8342664033261826e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8342664033261826e-11 5.8342664033261826e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.145\n", - "Total CPU secs in NLP function evaluations = 0.007\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 9.2344539e-02 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 9.1795096e-02 1.56e-01 1.03e+00 -1.0 5.94e-01 - 9.79e-01 8.75e-01h 1\n", - " 2 9.2188972e-02 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 8.8412352e-02 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", - " 4 9.6233482e-02 7.12e-01 2.47e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 9.4461313e-02 5.85e-01 4.74e+01 -1.0 3.45e+02 - 4.78e-01 8.92e-02f 2\n", - " 6 9.9368726e-02 1.36e-01 6.08e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", - " 7 9.8535727e-02 4.34e-01 2.97e-01 -1.0 5.49e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 9.8957264e-02 3.83e-02 5.78e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 9.8939356e-02 5.46e-03 4.02e-03 -2.5 6.39e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 9.8775901e-02 5.26e-04 3.62e-04 -3.8 1.01e+00 - 1.00e+00 1.00e+00h 1\n", - " 11 9.5986597e-02 2.04e-01 2.11e-02 -5.7 2.08e+01 - 7.94e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (319905)\n", - " 12 9.1498624e-02 9.64e-01 1.11e-02 -5.7 5.52e+01 - 8.77e-01 1.00e+00h 1\n", - " 13 8.8299544e-02 8.17e-01 2.37e-02 -5.7 3.28e+01 - 9.41e-01 1.00e+00h 1\n", - " 14 8.9059447e-02 1.11e-02 4.46e-04 -5.7 8.02e-01 -4.5 1.00e+00 1.00e+00h 1\n", - " 15 8.9028548e-02 6.44e-07 1.79e-07 -5.7 1.25e-02 -5.0 1.00e+00 1.00e+00h 1\n", - " 16 8.8442722e-02 4.14e-01 2.20e-02 -8.6 2.18e+02 - 3.15e-01 1.78e-01h 1\n", - " 17 8.7303204e-02 1.14e-01 2.94e-03 -8.6 2.87e+01 - 8.18e-01 1.00e+00h 1\n", - " 18 8.7506248e-02 1.77e-02 2.54e-04 -8.6 1.17e+01 - 9.86e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (349182)\n", - " 19 8.7505239e-02 2.49e-03 1.53e-05 -8.6 4.48e+00 - 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (366797)\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 8.7505073e-02 5.20e-05 3.04e-07 -8.6 6.52e-01 - 1.00e+00 1.00e+00h 1\n", - " 21 8.7505067e-02 2.17e-08 1.39e-10 -8.6 1.33e-02 - 1.00e+00 1.00e+00h 1\n", - " 22 8.7504422e-02 1.91e-06 3.15e-08 -9.0 1.25e-01 - 1.00e+00 1.00e+00h 1\n", - " 23 8.7504422e-02 1.03e-12 3.41e-10 -9.0 9.20e-05 -5.4 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 23\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 8.7504422324047759e-02 8.7504422324047759e-02\n", - "Dual infeasibility......: 3.4067176250603508e-10 3.4067176250603508e-10\n", - "Constraint violation....: 1.0300649222472202e-12 1.0300649222472202e-12\n", - "Complementarity.........: 9.0912236625038064e-10 9.0912236625038064e-10\n", - "Overall NLP error.......: 9.0912236625038064e-10 9.0912236625038064e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 28\n", - "Number of objective gradient evaluations = 24\n", - "Number of equality constraint evaluations = 28\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 24\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 23\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.014\n", - "Total CPU secs in NLP function evaluations = 0.041\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.7 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.23e-03 5.54e-01 -1.0 1.85e-01 - 9.91e-01 9.92e-01h 1\n", - " 4 0.0000000e+00 1.70e-09 1.37e-06 -1.0 1.55e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.7023418230621701e-09 1.7023418230621701e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.7023418230621701e-09 1.7023418230621701e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.118\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.4 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.37e-03 2.74e-01 -1.0 1.85e-01 - 9.91e-01 9.91e-01h 1\n", - " 4 0.0000000e+00 2.13e-09 1.47e-06 -1.0 1.73e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.1253634407969457e-09 2.1253634407969457e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.1253634407969457e-09 2.1253634407969457e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.100\n", - "Total CPU secs in NLP function evaluations = 0.005\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.1 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.46e+04 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (298604)\n", - " 1 0.0000000e+00 7.22e+02 3.85e+02 -1.0 1.46e+04 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 8.15e+00 3.85e+00 -1.0 7.69e+02 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 8.06e-02 4.39e+00 -1.0 8.63e+00 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 9.04e-09 1.02e-06 -1.0 8.54e-02 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.0385583462193608e-09 9.0385583462193608e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.131\n", - "Total CPU secs in NLP function evaluations = 0.009\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.3150611e+01 1.25e+00 1.04e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 -1.3151462e+01 6.17e-01 5.15e+00 -1.0 4.65e+01 - 9.73e-01 5.07e-01h 1\n", - " 2 -1.3150755e+01 1.28e+00 1.96e+00 -1.0 4.46e+01 - 8.99e-01 1.00e+00f 1\n", - " 3 -1.3154393e+01 7.33e+01 1.30e+02 -1.0 5.12e+02 - 1.88e-01 6.60e-01f 1\n", - " 4 -1.3147440e+01 1.67e+01 2.19e+00 -1.0 1.72e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.3148399e+01 1.18e+01 3.21e+01 -1.0 1.36e+02 - 1.00e+00 4.12e-01f 2\n", - " 6 -1.3145653e+01 1.23e+01 3.32e+00 -1.0 9.24e+01 - 8.74e-01 3.35e-01f 2\n", - " 7 -1.3143010e+01 3.67e+00 5.37e-01 -1.0 5.25e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.3144153e+01 4.52e-01 8.00e-02 -1.0 1.11e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 -1.3144153e+01 2.47e-04 1.12e-03 -1.7 3.87e-01 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.3144175e+01 1.50e-04 6.83e-04 -3.8 2.85e-01 - 1.00e+00 1.00e+00h 1\n", - " 11 -1.3144232e+01 1.07e-04 5.80e-05 -5.7 5.80e-01 -4.0 1.00e+00 1.00e+00h 1\n", - " 12 -1.3144447e+01 1.51e-03 7.27e-05 -5.7 2.18e+00 -4.5 1.00e+00 1.00e+00h 1\n", - " 13 -1.3144772e+01 4.00e-03 2.07e-04 -5.7 5.35e+00 -5.0 1.00e+00 6.14e-01h 1\n", - " 14 -1.3144770e+01 6.95e-04 3.62e-04 -5.7 1.73e-01 -5.4 1.00e+00 1.00e+00f 1\n", - " 15 -1.3144945e+01 6.78e-03 3.62e-05 -5.7 1.70e+00 -5.9 1.00e+00 1.00e+00h 1\n", - " 16 -1.3153640e+01 3.49e+01 9.15e-02 -5.7 2.83e+03 - 6.17e-02 6.37e-02h 1\n", - " 17 -1.3154824e+01 1.98e+01 3.84e-02 -5.7 8.27e+01 - 1.00e+00 5.97e-01f 1\n", - " 18 -1.3154428e+01 6.27e-01 2.75e-03 -5.7 1.22e+01 - 1.00e+00 1.00e+00f 1\n", - "Reallocating memory for MA57: lfact (319597)\n", - " 19 -1.3154555e+01 6.10e-02 9.67e-05 -5.7 8.95e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.3154553e+01 3.35e-04 1.10e-06 -5.7 5.85e-01 - 1.00e+00 1.00e+00h 1\n", - " 21 -1.3154553e+01 1.64e-08 9.42e-11 -5.7 3.56e-03 - 1.00e+00 1.00e+00h 1\n", - " 22 -1.3155149e+01 8.44e-01 1.54e-03 -8.6 2.87e+01 - 8.65e-01 9.44e-01h 1\n", - "Reallocating memory for MA57: lfact (350471)\n", - " 23 -1.3155196e+01 1.92e-02 2.59e-04 -8.6 1.21e+01 - 9.82e-01 1.00e+00h 1\n", - " 24 -1.3155201e+01 5.02e-03 6.53e-05 -8.6 6.33e+00 - 1.00e+00 1.00e+00h 1\n", - " 25 -1.3155202e+01 3.50e-04 5.41e-06 -8.6 1.69e+00 - 1.00e+00 1.00e+00h 1\n", - " 26 -1.3155202e+01 1.38e-06 3.84e-08 -8.6 1.06e-01 - 1.00e+00 1.00e+00h 1\n", - " 27 -1.3155202e+01 3.71e-11 2.10e-12 -8.6 5.52e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 27\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.3155202417249349e+01 -1.3155202417249349e+01\n", - "Dual infeasibility......: 2.0969252789257937e-12 2.0969252789257937e-12\n", - "Constraint violation....: 3.7140956976600137e-11 3.7140956976600137e-11\n", - "Complementarity.........: 2.5059043930329935e-09 2.5059043930329935e-09\n", - "Overall NLP error.......: 2.5059043930329935e-09 2.5059043930329935e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 34\n", - "Number of objective gradient evaluations = 28\n", - "Number of equality constraint evaluations = 34\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 28\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 27\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 1.008\n", - "Total CPU secs in NLP function evaluations = 0.042\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.8 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26094\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7086\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7086\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.40e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (299488)\n", - " 1 0.0000000e+00 4.43e+00 3.85e+02 -1.0 6.34e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 5.03e-02 3.85e+00 -1.0 4.73e+00 - 9.90e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 4.97e-04 4.39e+00 -1.0 5.33e-02 - 1.00e+00 9.90e-01h 1\n", - " 4 0.0000000e+00 5.83e-11 1.02e-06 -1.0 5.27e-04 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 5.8342664033261826e-11 5.8342664033261826e-11\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 5.8342664033261826e-11 5.8342664033261826e-11\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.133\n", - "Total CPU secs in NLP function evaluations = 0.006\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26208\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (275736)\n", - "Total number of variables............................: 7106\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7096\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 2.9907566e-03 1.25e+00 8.80e-01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303702)\n", - " 1 2.4709143e-03 1.56e-01 1.03e+00 -1.0 5.94e-01 - 9.79e-01 8.75e-01h 1\n", - " 2 2.8475281e-03 3.48e-02 1.13e+01 -1.0 6.05e+00 - 8.35e-01 1.00e+00f 1\n", - " 3 -7.4086018e-04 1.25e+01 5.75e+01 -1.0 1.80e+02 - 1.92e-01 7.11e-01f 1\n", - " 4 6.6826430e-03 7.12e-01 2.47e+00 -1.0 2.67e+01 - 1.00e+00 1.00e+00f 1\n", - " 5 5.0030420e-03 5.85e-01 4.74e+01 -1.0 3.45e+02 - 4.79e-01 8.93e-02f 2\n", - " 6 9.6602861e-03 1.37e-01 6.09e+00 -1.0 2.40e+01 -4.0 8.23e-01 1.00e+00F 1\n", - " 7 8.8703660e-03 4.34e-01 2.96e-01 -1.0 5.49e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 9.2713861e-03 3.83e-02 5.78e-02 -1.7 1.67e+01 - 1.00e+00 1.00e+00h 1\n", - " 9 9.2556032e-03 5.47e-03 4.05e-03 -2.5 6.40e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 9.1083712e-03 5.20e-04 3.49e-04 -3.8 1.06e+00 - 1.00e+00 1.00e+00h 1\n", - " 11 6.5758713e-03 1.85e-01 2.04e-02 -5.7 1.97e+01 - 8.02e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (319911)\n", - " 12 2.2595047e-03 9.80e-01 1.09e-02 -5.7 5.56e+01 - 8.79e-01 1.00e+00h 1\n", - " 13 -8.3311568e-04 8.21e-01 2.27e-02 -5.7 3.29e+01 - 9.59e-01 1.00e+00h 1\n", - " 14 -9.6435386e-05 1.08e-02 4.25e-04 -5.7 8.07e-01 -4.5 1.00e+00 1.00e+00h 1\n", - " 15 -1.2586985e-04 6.29e-07 1.66e-07 -5.7 1.19e-02 -5.0 1.00e+00 1.00e+00h 1\n", - " 16 -7.5509198e-04 4.26e-01 2.13e-02 -8.6 1.63e+02 - 3.64e-01 2.39e-01h 1\n", - " 17 -1.7704572e-03 1.04e-01 2.48e-03 -8.6 2.75e+01 - 8.25e-01 1.00e+00h 1\n", - " 18 -1.5994877e-03 1.71e-02 2.40e-04 -8.6 1.15e+01 - 9.87e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (351858)\n", - " 19 -1.6003069e-03 2.49e-03 1.75e-05 -8.6 4.48e+00 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.6005224e-03 5.64e-05 4.27e-07 -8.6 6.79e-01 - 1.00e+00 1.00e+00h 1\n", - " 21 -1.6005305e-03 2.71e-08 2.85e-10 -8.6 1.49e-02 - 1.00e+00 1.00e+00h 1\n", - " 22 -1.6005305e-03 2.86e-13 2.91e-14 -8.6 7.06e-06 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 22\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.6005305371233902e-03 -1.6005305371233902e-03\n", - "Dual infeasibility......: 2.9139664390433465e-14 2.9139664390433465e-14\n", - "Constraint violation....: 1.1865809618004581e-13 2.8599345114344032e-13\n", - "Complementarity.........: 2.5059035600370655e-09 2.5059035600370655e-09\n", - "Overall NLP error.......: 2.5059035600370655e-09 2.5059035600370655e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 27\n", - "Number of objective gradient evaluations = 23\n", - "Number of equality constraint evaluations = 27\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 23\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 22\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.929\n", - "Total CPU secs in NLP function evaluations = 0.041\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.8 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.22e-03 5.77e-01 -1.0 1.85e-01 - 9.91e-01 9.92e-01h 1\n", - " 4 0.0000000e+00 1.67e-09 1.36e-06 -1.0 1.54e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.6695191895621520e-09 1.6695191895621520e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.6695191895621520e-09 1.6695191895621520e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.104\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.2 seconds\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 6.90e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.28e+00 3.85e+02 -1.0 6.90e+01 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.46e-01 3.07e+01 -1.0 5.88e+00 - 5.91e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 1.22e-03 5.77e-01 -1.0 1.85e-01 - 9.91e-01 9.92e-01h 1\n", - " 4 0.0000000e+00 1.67e-09 1.36e-06 -1.0 1.54e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 4\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.6694885474066723e-09 1.6694885474066723e-09\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.6694885474066723e-09 1.6694885474066723e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 5\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 5\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 5\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 4\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.105\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 1.1 seconds\n" - ] - } - ], - "source": [ - "D_sc = []\n", - "D_sc_2 = []\n", - "D_unsc = []\n", - "D_unsc_2 = []\n", - "exp_conds_sc = []\n", - "exp_conds_unsc = []\n", - "FIM_opt_sc = []\n", - "FIM_opt_sc_2 = []\n", - "FIM_opt_unsc = []\n", - "FIM_opt_unsc_2 = []\n", - "jac_opt_sc = []\n", - "jac_opt_sc_2 = []\n", - "jac_opt_unsc = []\n", - "jac_opt_unsc_2 = []\n", - "standard_Ca = 5\n", - "standard_T = [300, 300, 300, 300, 300, 300, 300, 300, 300, ]\n", - "\n", - "FIM_new = None\n", - "FIM_new_unsc = None\n", - "\n", - "FIM_running = np.zeros((4, 4))\n", - "FIM_running_unsc = np.zeros((4, 4))\n", - "\n", - "for i in range(20):\n", - " # Optimize experiment (scaled)\n", - " sc_res = run_optimal_exp(standard_Ca, standard_T, FIM_new, True)\n", - " # sc_res.result_analysis()\n", - " sc_exp = get_exp_conds(sc_res.model)\n", - " FIM_new = sc_res.FIM\n", - "\n", - " # Optimize experiment (unscaled)\n", - " unsc_res = run_optimal_exp(standard_Ca, standard_T, FIM_new_unsc, False)\n", - " # unsc_res.result_analysis()\n", - " unsc_exp = get_exp_conds(unsc_res.model)\n", - " FIM_new_unsc = unsc_res.FIM\n", - "\n", - " # Compute FIM in isolation (scaled)\n", - " res_sc = compute_specific_FIM(sc_exp[0], sc_exp[1:], prior_FIM=None, scale_param=True)\n", - "\n", - " # Compute FIM in isolation (unscaled)\n", - " res_unsc = compute_specific_FIM(unsc_exp[0], unsc_exp[1:], prior_FIM=None, scale_param=False)\n", - "\n", - " # Computing running isolation FIM\n", - " FIM_running += res_sc.FIM\n", - " FIM_running_unsc += res_unsc.FIM\n", - "\n", - " # Compute objectives (D-optimality)\n", - " D_sc.append(np.log10(np.linalg.det(sc_res.FIM)))\n", - " D_sc_2.append(np.log10(np.linalg.det(FIM_running)))\n", - " D_unsc.append(np.log10(np.linalg.det(unsc_res.FIM)))\n", - " D_unsc_2.append(np.log10(np.linalg.det(FIM_running_unsc)))\n", - "\n", - " # Append experimental results\n", - " exp_conds_sc.append(sc_exp)\n", - " exp_conds_unsc.append(unsc_exp)\n", - "\n", - " # Append FIM information\n", - " FIM_opt_sc.append(FIM_new)\n", - " FIM_opt_sc_2.append(copy.deepcopy(FIM_running))\n", - " FIM_opt_unsc.append(FIM_new_unsc)\n", - " FIM_opt_unsc_2.append(copy.deepcopy(FIM_running_unsc))\n", - "\n", - " # Append jacobian information\n", - " jac_opt_sc.append(translate_jac(sc_res.jaco_information))\n", - " jac_opt_sc_2.append(translate_jac(res_sc.jaco_information))\n", - " jac_opt_unsc.append(translate_jac(unsc_res.jaco_information))\n", - " jac_opt_unsc_2.append(translate_jac(res_unsc.jaco_information))" - ] - }, - { - "cell_type": "markdown", - "id": "17763935-a215-4020-a6b4-9b4425bec1f2", - "metadata": {}, - "source": [ - "## Plotting the results" - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "id": "4d9a37ac-1d7e-452a-ad55-e87191ff7d5c", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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woQJ7N69m59//plRo0axbNky+vfvzxtvvMEnn3zCM888Q2xsLFqtFr1eT7169fj111/x8PBg+/btvPDCC/j6+vL444+X+HrLlVJJYmJilOeee04JCgpS7O3tlfr16ytvv/22kpOTU+hxnTp1UgCTZeTIkSU6d0pKigIoKSkppbkEIYSoNFlZWcrx48eVrKwsw4a8dEX5kcpZ8tKLHXenTp2UsWPHFti+YMECxcXFxbg+dOhQJTAwUMnPzzduGzhwoDJo0CBFURTl999/V5ydnZXU1NQCdaWmpip2dnbKt99+W+y4PvzwQ6V169bG9SlTpijh4eHG9QYNGiiLFy82OWb69OlKZGSkoiiK8s033ygeHh63Pg9FUb766isFUA4cOFDsONq3b6+MGDHCZNvAgQOVBx980LgOKC+++KJJmYiICGXUqFGKohi+X82dd+jQoUrfvn2N6506dVLuv/9+43p+fr7i4OCgPPPMM8ZtcXFxCqDs2LHDYswvvfSSMmDAAIvnKakCv9u3Kcn3d6W14Jw4cQK9Xs8333xDw4YNOXr0KCNGjCAjI4PZs2cXeuyIESN45513jOtarba8wxVCCFHBmjVrhpWVlXHd19eXI0eOANCjRw8CAwOpX78+vXr1olevXvTv3x+tVktUVBQ5OTl069bNYt0///wzn376KdHR0aSnp5Ofn4+zs7PZshkZGURHRzN8+HBGjBhh3J6fn2/s1xIVFUVYWBj29vbG/ZGRkSW+5qioKF544QWTbffdd1+B20B31h0ZGXlXT2uFhYUZX1tZWeHh4UHz5s2N27y9vQFISEgwbvviiy+YP38+sbGxZGVlkZubW+onzspDpSU4N38hb6pfvz4nT57kq6++KjLB0Wq1+Pj4lHeIQghRfVhp4fH0yjt3MTk7O5OSklJge3JycoFOsDY2NibrKpUKvV4PGG4x7d+/n82bN/PXX3/x9ttvM3XqVPbs2YNGoyk0hh07dvDUU08xbdo0evbsiYuLC0uWLOGjjz4yWz493fC+fvvtt0RERJjsuz0Bq47Mvce3b1PduPV4831fsmQJr776Kh999BGRkZE4OTnx4YcfsmvXrooLupiqVCfjlJQU3N3diyz3448/4unpyT333MOkSZPIzCz8vnNOTg6pqakmixBC1CgqFVg7VM5Sgv43ISEh7N+/v8D2/fv307hx4xJd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/MLUqVN5//332bt3L76+vnz5ZeFJ4KVLl3jwwQdp27Ythw4d4quvvmLevHm8++67JuW+++47rK2t2b17N3PnzuXjjz/m//7v/wBYunQp9erV45133iEuLo64uDiL54uOjmbNmjWsXbuWn376iXnz5tGnTx8uXrzIli1b+OCDD3jrrbeMiSCAWq3m008/5dixY3z33Xf8/fffTJgwodDrqkrkFpUQQogKtXLlShwdHcnPzycnJwe1Ws3nn38OQE5ODu+//z4bNmwgMjISgPr16/Pvv//yzTff0KlTJ2JjY2nZsiVt2rQBDP1Qblq8eDFXr15lz549uLu7A9CwYUPj/vDwcMLDw43r06dPZ9myZaxYsYLRo0cXiPXff/9l9+7dJCQkYGdnB8Ds2bNZvnw5v/32Gy+88AJz5sxh+PDhDB8+HIB3332XDRs2FNqK8+WXX+Lv78/nn3+OSqWiSZMmXL58mYkTJ/L222+jVhvaH/z9/fnkk09QqVSEhIRw5MgRPvnkE0aMGIG7uztWVlY4OTnh4+NT6Huu1+uZP38+Tk5ONG3alC5dunDy5ElWr16NWq0mJCSEDz74gE2bNhEREQHAuHHjjMcHBQXx7rvv8uKLLxaZvFUVkuAIIYSoUF26dOGrr74iIyODTz75BGtrawYMGADAmTNnyMzMpEePHibH5Obm0rJlSwBGjRrFgAED2L9/Pw888AD9+vWjffv2ABw8eJCWLVsak5s7paenM3XqVFatWkVcXBz5+flkZWVZbME5dOgQ6enpeHh4mGzPysoiOjoagKioKF588UWT/ZGRkWzatMniexAVFUVkZKTxthLAfffdR3p6OhcvXiQgIACAe++916RMZGQkH330ETqdDisrK4v13ykoKAgnJyfjure3N1ZWVsZE6ua2hIQE4/qGDRuYMWMGJ06cIDU1lfz8fLKzs8nMzESr1Rb73JVFEhwhhBAVysHBwdiqMn/+fMLDw5k3bx7Dhw839vFYtWoVdevWNTnuZgtK7969OX/+PKtXr2b9+vV069aNl156idmzZxfZufbVV19l/fr1zJ49m4YNG6LRaHjsscfIzc01Wz49PR1fX182b95cYF9VfkT6TjY2NibrN59iu3PbzVt5586d46GHHmLUqFG89957uLu78++//zJ8+HByc3OrRYIjfXCEEEJUGrVazRtvvMFbb71F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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Rescaling the scaled results\n", - "D_rescaled = []\n", - "param_vals = np.array([[85, 370, 8, 15], ])\n", - "for i in range(20):\n", - " resc_FIM = rescale_FIM(FIM_opt_sc_2[i], param_vals)\n", - " D_rescaled.append(np.log10(np.linalg.det(resc_FIM)))\n", - "\n", - "plt.plot(range(20), D_sc, color='green', ls='-', label='Scaled, optimal')\n", - "plt.scatter(range(20), D_sc_2, color='green', label='Scaled, compute')\n", - "plt.plot(range(20), D_unsc, color='orange', ls='-', label='Unscaled, optimal')\n", - "plt.scatter(range(20), D_unsc_2, color='orange', ls='--', label='Unscaled, compute')\n", - "plt.plot(range(20), D_rescaled, color='purple', ls=':', lw=5, label='Rescaled optimal')\n", - "\n", - "plt.legend()\n", - "plt.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.14" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From de535f49306158d7d5c52ba6e5fe9ebb72a7c997 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:56:31 -0400 Subject: [PATCH 1919/3044] Delete pyomo/contrib/doe/examples/reactor_optimize_doe_DJL.py Removing old file --- .../doe/examples/reactor_optimize_doe_DJL.py | 180 ------------------ 1 file changed, 180 deletions(-) delete mode 100644 pyomo/contrib/doe/examples/reactor_optimize_doe_DJL.py diff --git a/pyomo/contrib/doe/examples/reactor_optimize_doe_DJL.py b/pyomo/contrib/doe/examples/reactor_optimize_doe_DJL.py deleted file mode 100644 index 719983069f0..00000000000 --- a/pyomo/contrib/doe/examples/reactor_optimize_doe_DJL.py +++ /dev/null @@ -1,180 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# -# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation -# Initiative (CCSI), and is copyright (c) 2022 by the software owners: -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, -# University of Toledo, West Virginia University, et al. All rights reserved. -# -# NOTICE. This Software was developed under funding from the -# U.S. Department of Energy and the U.S. Government consequently retains -# certain rights. As such, the U.S. Government has been granted for itself -# and others acting on its behalf a paid-up, nonexclusive, irrevocable, -# worldwide license in the Software to reproduce, distribute copies to the -# public, prepare derivative works, and perform publicly and display -# publicly, and to permit other to do so. -# ___________________________________________________________________________ - -from pyomo.common.dependencies import numpy as np -from pyomo.contrib.doe.examples.reactor_kinetics import create_model, disc_for_measure -from pyomo.contrib.doe import DesignOfExperiments, MeasurementVariables, DesignVariables -import pyomo.environ as aml - -def get_exp_results(m): - vals = [aml.value(m.CA0[0]), ] - for i in [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]: - vals.append(aml.value(m.T[i])) - return vals - - -def get_FIM_from_exp(CA_0=None, T_0=None, prior=None): - if CA_0 is None: - CA_0 = 5 - if T_0 is None: - T_0 = [570, 300, 300, 300, 300, 300, 300, 300, 300] - ### Define inputs - # Control time set [h] - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - # Define parameter nominal value - parameter_dict = {"A1": 85, "A2": 372, "E1": 8, "E2": 15} - - # measurement object - measurements = MeasurementVariables() - measurements.add_variables( - "C", # name of measurement - indices={0: ["CA", "CB", "CC"], 1: t_control}, # indices of measurement - time_index_position=1, - ) # position of time index - - # design object - exp_design = DesignVariables() - - # add CAO as design variable - exp_design.add_variables( - "CA0", # name of design variable - indices={0: [0]}, # indices of design variable - time_index_position=0, # position of time index - values=[CA_0], # nominal value of design variable - lower_bounds=1, # lower bound of design variable - upper_bounds=5, # upper bound of design variable - ) - - # add T as design variable - exp_design.add_variables( - "T", # name of design variable - indices={0: t_control}, # indices of design variable - time_index_position=0, # position of time index - values=list(T_0), # nominal value of design variable - lower_bounds=300, # lower bound of design variable - upper_bounds=700, # upper bound of design variable - ) - - doe_object2 = DesignOfExperiments( - parameter_dict, # dictionary of parameters - exp_design, # design variables - measurements, # measurement variables - create_model, # function to create model - prior_FIM=prior, - discretize_model=disc_for_measure, # function to discretize model - ) - - result = doe_object2.compute_FIM( - mode='sequential_finite', - formula = 'central', - ) - - result.result_analysis() - - return result.FIM - - -def main(CA_0=None, T_0=None, prior=None): - if CA_0 is None: - CA_0 = 5 - if T_0 is None: - T_0 = [570, 300, 300, 300, 300, 300, 300, 300, 300] - ### Define inputs - # Control time set [h] - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - # Define parameter nominal value - parameter_dict = {"A1": 85, "A2": 372, "E1": 8, "E2": 15} - - # measurement object - measurements = MeasurementVariables() - measurements.add_variables( - "C", # name of measurement - indices={0: ["CA", "CB", "CC"], 1: t_control}, # indices of measurement - time_index_position=1, - ) # position of time index - - # design object - exp_design = DesignVariables() - - # add CAO as design variable - exp_design.add_variables( - "CA0", # name of design variable - indices={0: [0]}, # indices of design variable - time_index_position=0, # position of time index - values=[CA_0], # nominal value of design variable - lower_bounds=1, # lower bound of design variable - upper_bounds=5, # upper bound of design variable - ) - - # add T as design variable - exp_design.add_variables( - "T", # name of design variable - indices={0: t_control}, # indices of design variable - time_index_position=0, # position of time index - values=list(T_0), # nominal value of design variable - lower_bounds=300, # lower bound of design variable - upper_bounds=700, # upper bound of design variable - ) - - design_names = exp_design.variable_names - # exp1 = [5, 570, 300, 300, 300, 300, 300, 300, 300, 300] - # exp1_design_dict = dict(zip(design_names, exp1)) - # exp_design.update_values(exp1_design_dict) - - # add a prior information (scaled FIM with T=500 and T=300 experiments) - if prior is None: - prior = np.asarray( - [ - [28.67892806, 5.41249739, -81.73674601, -24.02377324], - [5.41249739, 26.40935036, -12.41816477, -139.23992532], - [-81.73674601, -12.41816477, 240.46276004, 58.76422806], - [-24.02377324, -139.23992532, 58.76422806, 767.25584508], - ] - ) - - doe_object2 = DesignOfExperiments( - parameter_dict, # dictionary of parameters - exp_design, # design variables - measurements, # measurement variables - create_model, # function to create model - prior_FIM=prior, # prior information - discretize_model=disc_for_measure, # function to discretize model - ) - - square_result, optimize_result = doe_object2.stochastic_program( - if_optimize=True, # if optimize - if_Cholesky=True, # if use Cholesky decomposition - # scale_nominal_param_value=True, # if scale nominal parameter value - objective_option="det", # objective option - L_initial=np.linalg.cholesky(prior), # initial Cholesky decomposition - ) - - return prior, optimize_result, square_result - - -if __name__ == "__main__": - main() From c8ef2dd2e4d3cd0e1cfca006b64af9031857cf27 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:16:29 -0400 Subject: [PATCH 1920/3044] Ran Black --- .../doe/tests/experiment_class_example.py | 59 +++++++++------- .../tests/experiment_class_example_flags.py | 68 +++++++++++-------- 2 files changed, 75 insertions(+), 52 deletions(-) diff --git a/pyomo/contrib/doe/tests/experiment_class_example.py b/pyomo/contrib/doe/tests/experiment_class_example.py index eae355ebc89..08d205df39b 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example.py +++ b/pyomo/contrib/doe/tests/experiment_class_example.py @@ -4,6 +4,7 @@ import itertools import json + # ======================== @@ -59,12 +60,12 @@ def get_labeled_model(self): def create_model(self): """ - This is an example user model provided to DoE library. - It is a dynamic problem solved by Pyomo.DAE. + This is an example user model provided to DoE library. + It is a dynamic problem solved by Pyomo.DAE. - Return - ------ - m: a Pyomo.DAE model + Return + ------ + m: a Pyomo.DAE model """ m = self.model = pyo.ConcreteModel() @@ -151,19 +152,19 @@ def finalize_model(self): m = self.model # Unpacking data before simulation - control_points = self.data['control_points'] + control_points = self.data["control_points"] - m.CA[0].value = self.data['CA0'] - m.CB[0].fix(self.data['CB0']) - m.t.update(self.data['t_range']) + m.CA[0].value = self.data["CA0"] + m.CB[0].fix(self.data["CB0"]) + m.t.update(self.data["t_range"]) m.t.update(control_points) - m.A1.fix(self.data['A1']) - m.A2.fix(self.data['A2']) - m.E1.fix(self.data['E1']) - m.E2.fix(self.data['E2']) + m.A1.fix(self.data["A1"]) + m.A2.fix(self.data["A2"]) + m.E1.fix(self.data["E1"]) + m.E2.fix(self.data["E2"]) - m.CA[0].setlb(self.data['CA_bounds'][0]) - m.CA[0].setub(self.data['CA_bounds'][1]) + m.CA[0].setlb(self.data["CA_bounds"][0]) + m.CA[0].setub(self.data["CA_bounds"][1]) m.t_control = control_points @@ -176,8 +177,8 @@ def finalize_model(self): for t in m.t: if t in control_points: cv = control_points[t] - m.T[t].setlb(self.data['T_bounds'][0]) - m.T[t].setub(self.data['T_bounds'][1]) + m.T[t].setlb(self.data["T_bounds"][0]) + m.T[t].setub(self.data["T_bounds"][1]) m.T[t] = cv @m.Constraint(m.t - control_points) @@ -190,7 +191,6 @@ def T_control(m, t): # sim.initialize_model() - def label_experiment_impl(self, index_sets_meas): """ Example for annotating (labeling) the model with a @@ -207,13 +207,19 @@ def label_experiment_impl(self, index_sets_meas): m.experiment_outputs = pyo.Suffix( direction=pyo.Suffix.LOCAL, ) - m.experiment_outputs.update((k, None) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) - + m.experiment_outputs.update( + (k, None) + for k in expand_model_components(m, base_comp_meas, index_sets_meas) + ) + # Adding no error for measurements currently m.measurement_error = pyo.Suffix( direction=pyo.Suffix.LOCAL, ) - m.measurement_error.update((k, 1e-2) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) + m.measurement_error.update( + (k, 1e-2) + for k in expand_model_components(m, base_comp_meas, index_sets_meas) + ) # Grab design variables base_comp_des = [m.CA, m.T] @@ -221,8 +227,11 @@ def label_experiment_impl(self, index_sets_meas): m.experiment_inputs = pyo.Suffix( direction=pyo.Suffix.LOCAL, ) - m.experiment_inputs.update((k, pyo.ComponentUID(k)) for k in expand_model_components(m, base_comp_des, index_sets_des)) - + m.experiment_inputs.update( + (k, pyo.ComponentUID(k)) + for k in expand_model_components(m, base_comp_des, index_sets_des) + ) + m.unknown_parameters = pyo.Suffix( direction=pyo.Suffix.LOCAL, ) @@ -246,4 +255,6 @@ def label_experiment(self): """ m = self.model - return self.label_experiment_impl([[m.t_control], [[m.t.last()]], [[m.t.last()]]]) + return self.label_experiment_impl( + [[m.t_control], [[m.t.last()]], [[m.t.last()]]] + ) diff --git a/pyomo/contrib/doe/tests/experiment_class_example_flags.py b/pyomo/contrib/doe/tests/experiment_class_example_flags.py index 4b217ef691a..0e4ee7c816f 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example_flags.py +++ b/pyomo/contrib/doe/tests/experiment_class_example_flags.py @@ -4,6 +4,7 @@ import itertools import json + # ======================== @@ -64,12 +65,12 @@ def get_labeled_model(self, flag=0): def create_model(self): """ - This is an example user model provided to DoE library. - It is a dynamic problem solved by Pyomo.DAE. + This is an example user model provided to DoE library. + It is a dynamic problem solved by Pyomo.DAE. - Return - ------ - m: a Pyomo.DAE model + Return + ------ + m: a Pyomo.DAE model """ m = self.model = pyo.ConcreteModel() @@ -156,19 +157,19 @@ def finalize_model(self): m = self.model # Unpacking data before simulation - control_points = self.data['control_points'] + control_points = self.data["control_points"] - m.CA[0].value = self.data['CA0'] - m.CB[0].fix(self.data['CB0']) - m.t.update(self.data['t_range']) + m.CA[0].value = self.data["CA0"] + m.CB[0].fix(self.data["CB0"]) + m.t.update(self.data["t_range"]) m.t.update(control_points) - m.A1.fix(self.data['A1']) - m.A2.fix(self.data['A2']) - m.E1.fix(self.data['E1']) - m.E2.fix(self.data['E2']) + m.A1.fix(self.data["A1"]) + m.A2.fix(self.data["A2"]) + m.E1.fix(self.data["E1"]) + m.E2.fix(self.data["E2"]) - m.CA[0].setlb(self.data['CA_bounds'][0]) - m.CA[0].setub(self.data['CA_bounds'][1]) + m.CA[0].setlb(self.data["CA_bounds"][0]) + m.CA[0].setub(self.data["CA_bounds"][1]) m.t_control = control_points @@ -181,8 +182,8 @@ def finalize_model(self): for t in m.t: if t in control_points: cv = control_points[t] - m.T[t].setlb(self.data['T_bounds'][0]) - m.T[t].setub(self.data['T_bounds'][1]) + m.T[t].setlb(self.data["T_bounds"][0]) + m.T[t].setub(self.data["T_bounds"][1]) m.T[t] = cv @m.Constraint(m.t - control_points) @@ -195,7 +196,6 @@ def T_control(m, t): # sim.initialize_model() - def label_experiment_impl(self, index_sets_meas, flag=0): """ Example for annotating (labeling) the model with a @@ -213,17 +213,23 @@ def label_experiment_impl(self, index_sets_meas, flag=0): m.experiment_outputs = pyo.Suffix( direction=pyo.Suffix.LOCAL, ) - m.experiment_outputs.update((k, None) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) - + m.experiment_outputs.update( + (k, None) + for k in expand_model_components(m, base_comp_meas, index_sets_meas) + ) + if flag != 2: # Adding no error for measurements currently m.measurement_error = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) + direction=pyo.Suffix.LOCAL, + ) if flag == 5: m.measurement_error.update((m.CA[0], 1e-2) for k in range(1)) else: - m.measurement_error.update((k, 1e-2) for k in expand_model_components(m, base_comp_meas, index_sets_meas)) + m.measurement_error.update( + (k, 1e-2) + for k in expand_model_components(m, base_comp_meas, index_sets_meas) + ) if flag != 3: # Grab design variables @@ -232,17 +238,23 @@ def label_experiment_impl(self, index_sets_meas, flag=0): m.experiment_inputs = pyo.Suffix( direction=pyo.Suffix.LOCAL, ) - m.experiment_inputs.update((k, pyo.ComponentUID(k)) for k in expand_model_components(m, base_comp_des, index_sets_des)) - + m.experiment_inputs.update( + (k, pyo.ComponentUID(k)) + for k in expand_model_components(m, base_comp_des, index_sets_des) + ) + if flag != 4: m.unknown_parameters = pyo.Suffix( direction=pyo.Suffix.LOCAL, ) - m.unknown_parameters.update((k, pyo.value(k)) for k in [m.A1, m.A2, m.E1, m.E2]) + m.unknown_parameters.update( + (k, pyo.value(k)) for k in [m.A1, m.A2, m.E1, m.E2] + ) class FullReactorExperiment(ReactorExperiment): def label_experiment(self, flag=0): m = self.model - return self.label_experiment_impl([[m.t_control], [m.t_control], [m.t_control]], flag=flag) - + return self.label_experiment_impl( + [[m.t_control], [m.t_control], [m.t_control]], flag=flag + ) From 402784bb186ddcb41875ae875bc735d49ccf6159 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Fri, 12 Jul 2024 14:30:40 -0600 Subject: [PATCH 1921/3044] fixing bug in _load_slacks --- pyomo/solvers/plugins/solvers/xpress_direct.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/xpress_direct.py b/pyomo/solvers/plugins/solvers/xpress_direct.py index c62f76d85ce..33a3c8d0282 100644 --- a/pyomo/solvers/plugins/solvers/xpress_direct.py +++ b/pyomo/solvers/plugins/solvers/xpress_direct.py @@ -1036,10 +1036,8 @@ def _load_slacks(self, cons_to_load=None): if xpress_con in self._range_constraints: ## for xpress, the slack on a range constraint ## is based on the upper bound - ## FIXME: This looks like a bug - there is no variable named - ## `con` - there is, however, `xpress_con` and `pyomo_con` - lb = con.lb - ub = con.ub + lb = xpress_con.lb + ub = xpress_con.ub ub_s = val expr_val = ub - ub_s lb_s = lb - expr_val From 06a0bf8691436941b2c77fce4ac87d1e16bfcb49 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 12 Jul 2024 22:43:27 -0400 Subject: [PATCH 1922/3044] Simplify main solve and subproblem results objects --- pyomo/contrib/pyros/master_problem_methods.py | 4 + pyomo/contrib/pyros/pyros.py | 12 +- .../contrib/pyros/pyros_algorithm_methods.py | 241 ++++++------------ .../pyros/separation_problem_methods.py | 3 - 4 files changed, 84 insertions(+), 176 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 8c270a76c11..41aa9dc0edd 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -802,6 +802,10 @@ def solver_call_master(master_data, config, master_soln): ) if not try_backup: + if infeasible: + master_soln.pyrosTerminationCondition = ( + pyrosTerminationCondition.robust_infeasible + ) return # all solvers have failed to return an acceptable status. diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 1898e6c19b2..db2714266ae 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -386,9 +386,7 @@ def solve( # === Solve and load solution into model return_soln = ROSolveResults() if not robust_infeasible: - pyros_soln, final_iter_separation_solns = ROSolver_iterative_solve( - model_data, config - ) + pyros_soln = ROSolver_iterative_solve(model_data, config) IterationLogRecord.log_header_rule(config.progress_logger.info) termination_acceptable = ( @@ -401,7 +399,7 @@ def solve( if termination_acceptable: load_final_solution( model_data=model_data, - master_soln=pyros_soln.master_soln, + master_soln=pyros_soln.master_results, config=config, original_user_var_partitioning=user_var_partitioning, ) @@ -409,7 +407,7 @@ def solve( # get the most recent master objective, if available return_soln.final_objective_value = None master_epigraph_obj_value = value( - pyros_soln.master_soln.master_model.epigraph_obj, + pyros_soln.master_results.master_model.epigraph_obj, exception=False, ) if master_epigraph_obj_value is not None: @@ -424,9 +422,7 @@ def solve( return_soln.pyros_termination_condition = ( pyros_soln.pyros_termination_condition ) - return_soln.iterations = pyros_soln.total_iters + 1 - - del pyros_soln.working_model + return_soln.iterations = pyros_soln.iterations else: return_soln.final_objective_value = None return_soln.pyros_termination_condition = ( diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index b6f34070419..bafd6f87e5a 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -15,14 +15,13 @@ from pyomo.common.dependencies import numpy as np from pyomo.common.collections import ComponentMap -from pyomo.core.base import Block, value +from pyomo.core.base import value import pyomo.contrib.pyros.master_problem_methods as mp_methods import pyomo.contrib.pyros.separation_problem_methods as sp_methods from pyomo.contrib.pyros.util import ( check_time_limit_reached, ObjectiveType, - get_time_from_solver, pyrosTerminationCondition, IterationLogRecord, get_main_elapsed_time, @@ -30,30 +29,34 @@ ) -def update_grcs_solve_data( - pyros_soln, term_cond, nominal_data, timing_data, separation_data, master_soln, k -): - ''' - This function updates the results data container object to return to the user so that they have all pertinent - information from the PyROS run. - :param grcs_soln: PyROS solution data container object - :param term_cond: PyROS termination condition - :param nominal_data: Contains information on all nominal data (var values, objective) - :param timing_data: Contains timing information on subsolver calls in PyROS - :param separation_data: Separation model data container - :param master_problem_subsolver_statuses: All master problem sub-solver termination conditions from the PyROS run - :param separation_problem_subsolver_statuses: All separation problem sub-solver termination conditions from the PyROS run - :param k: Iteration counter - :return: None - ''' - pyros_soln.pyros_termination_condition = term_cond - pyros_soln.total_iters = k - pyros_soln.nominal_data = nominal_data - pyros_soln.timing_data = timing_data - pyros_soln.separation_data = separation_data - pyros_soln.master_soln = master_soln - - return +class GRCSResults: + """ + Cutting set RO algorithm solve results. + + Attributes + ---------- + master_results : MasterResults + Solve results for most recent master problem. + separation_results : SeparationResults or None + Solve results for separation problem(s) of last iteration. + If the separation subroutine was not invoked in the last + iteration, then None. + pyros_termination_condition : pyrosTerminationCondition + PyROS termination condition. + iterations : int + Number of iterations required. + """ + def __init__( + self, + master_results, + separation_results, + pyros_termination_condition, + iterations, + ): + self.master_results = master_results + self.separation_results = separation_results + self.pyros_termination_condition = pyros_termination_condition + self.iterations = iterations def _evaluate_shift(current, prev, initial, norm=None): @@ -135,26 +138,12 @@ def ROSolver_iterative_solve(model_data, config): Returns ------- - ... + GRCSResults + Iterative solve results. """ master_data = mp_methods.MasterProblemData(model_data, config) separation_data = sp_methods.SeparationProblemData(model_data, config) - # === Nominal information - nominal_data = Block() - nominal_data.nom_fsv_vals = [] - nominal_data.nom_ssv_vals = [] - nominal_data.nom_first_stage_cost = 0 - nominal_data.nom_second_stage_cost = 0 - nominal_data.nom_obj = 0 - - # === Time information - timing_data = Block() - timing_data.total_master_solve_time = 0 - timing_data.total_separation_local_time = 0 - timing_data.total_separation_global_time = 0 - timing_data.total_dr_polish_time = 0 - # set up first-stage variable and DR variable sets nominal_master_blk = master_data.master_model.scenarios[0, 0] dr_var_monomial_map = get_dr_var_to_monomial_map(nominal_master_blk) @@ -179,42 +168,21 @@ def ROSolver_iterative_solve(model_data, config): config.progress_logger.debug(f"PyROS working on iteration {k}...") master_soln = master_data.solve_master() - # === Keep track of total time and subsolver termination conditions - timing_data.total_master_solve_time += get_time_from_solver(master_soln.results) - - if k > 0: # master feas problem not solved for iteration 0 - timing_data.total_master_solve_time += get_time_from_solver( - master_soln.feasibility_problem_results - ) - master_statuses.append(master_soln.results.solver.termination_condition) master_soln.master_problem_subsolver_statuses = master_statuses # check master solve status # to determine whether to terminate here - if ( - master_soln.master_subsolver_results[1] - is pyrosTerminationCondition.robust_infeasible - ): - term_cond = pyrosTerminationCondition.robust_infeasible - elif ( - master_soln.pyros_termination_condition - is pyrosTerminationCondition.subsolver_error - ): - term_cond = pyrosTerminationCondition.subsolver_error - elif ( + master_termination_not_acceptable = ( master_soln.pyros_termination_condition - is pyrosTerminationCondition.time_out - ): - term_cond = pyrosTerminationCondition.time_out - else: - term_cond = None - if term_cond in { - pyrosTerminationCondition.subsolver_error, - pyrosTerminationCondition.time_out, - pyrosTerminationCondition.robust_infeasible, - }: - log_record = IterationLogRecord( + in { + pyrosTerminationCondition.robust_infeasible, + pyrosTerminationCondition.time_out, + pyrosTerminationCondition.subsolver_error, + } + ) + if master_termination_not_acceptable: + iter_log_record = IterationLogRecord( iteration=k, objective=None, first_stage_var_shift=None, @@ -227,17 +195,13 @@ def ROSolver_iterative_solve(model_data, config): global_separation=None, elapsed_time=get_main_elapsed_time(model_data.timing), ) - log_record.log(config.progress_logger.info) - update_grcs_solve_data( - pyros_soln=model_data, - k=k, - term_cond=term_cond, - nominal_data=nominal_data, - timing_data=timing_data, - separation_data=separation_data, - master_soln=master_soln, + iter_log_record.log(config.progress_logger.info) + return GRCSResults( + master_results=master_soln, + separation_results=None, + pyros_termination_condition=master_soln.pyros_termination_condition, + iterations=k + 1, ) - return model_data, [] polishing_successful = True polish_master_solution = ( @@ -246,8 +210,7 @@ def ROSolver_iterative_solve(model_data, config): and k != 0 ) if polish_master_solution: - polishing_results, polishing_successful = master_data.solve_dr_polishing() - timing_data.total_dr_polish_time += get_time_from_solver(polishing_results) + master_data.solve_dr_polishing() # track variable values current_iter_var_data = get_variable_value_data( @@ -265,7 +228,7 @@ def ROSolver_iterative_solve(model_data, config): ) # === Check if time limit reached after polishing - if check_time_limit_reached(master_data.timing, config): + if check_time_limit_reached(model_data.timing, config): iter_log_record = IterationLogRecord( iteration=k, objective=value(master_data.master_model.epigraph_obj), @@ -277,49 +240,20 @@ def ROSolver_iterative_solve(model_data, config): dr_polishing_success=polishing_successful, all_sep_problems_solved=None, global_separation=None, - elapsed_time=master_data.timing.get_main_elapsed_time(), - ) - update_grcs_solve_data( - pyros_soln=model_data, - k=k, - term_cond=pyrosTerminationCondition.time_out, - nominal_data=nominal_data, - timing_data=timing_data, - separation_data=separation_data, - master_soln=master_soln, + elapsed_time=model_data.timing.get_main_elapsed_time(), ) iter_log_record.log(config.progress_logger.info) - return model_data, [] + return GRCSResults( + master_results=master_soln, + separation_results=None, + pyros_termination_condition=pyrosTerminationCondition.time_out, + iterations=k + 1, + ) # === Solve Separation Problem separation_data.iteration = k separation_data.master_model = master_data.master_model separation_results = separation_data.solve_separation(master_data) - separation_data.separation_problem_subsolver_statuses.extend( - [ - res.solver.termination_condition - for res in separation_results.generate_subsolver_results() - ] - ) - if separation_results.solved_globally: - separation_data.total_global_separation_solves += 1 - - # make updates based on separation results - timing_data.total_separation_local_time += ( - separation_results.evaluate_local_solve_time(get_time_from_solver) - ) - timing_data.total_separation_global_time += ( - separation_results.evaluate_global_solve_time(get_time_from_solver) - ) - if separation_results.found_violation: - scaled_violations = separation_results.scaled_violations - if scaled_violations is not None: - # can be None if time out or subsolver error - # reported in separation - separation_data.constraint_violations.append(scaled_violations.values()) - separation_data.points_separated = ( - separation_results.violating_param_realization - ) scaled_violations = [ solve_call_res.scaled_violations[con] @@ -353,34 +287,19 @@ def ROSolver_iterative_solve(model_data, config): ) # terminate on time limit - if separation_results.time_out: - termination_condition = pyrosTerminationCondition.time_out - update_grcs_solve_data( - pyros_soln=model_data, - k=k, - term_cond=termination_condition, - nominal_data=nominal_data, - timing_data=timing_data, - separation_data=separation_data, - master_soln=master_soln, + if separation_results.time_out or separation_results.subsolver_error: + pyros_term_cond = ( + pyrosTerminationCondition.time_out + if separation_results.time_out + else pyrosTerminationCondition.subsolver_error ) iter_log_record.log(config.progress_logger.info) - return model_data, separation_results - - # terminate on separation subsolver error - if separation_results.subsolver_error: - termination_condition = pyrosTerminationCondition.subsolver_error - update_grcs_solve_data( - pyros_soln=model_data, - k=k, - term_cond=termination_condition, - nominal_data=nominal_data, - timing_data=timing_data, - separation_data=separation_data, - master_soln=master_soln, + return GRCSResults( + master_results=master_soln, + separation_results=separation_results, + pyros_termination_condition=pyros_term_cond, + iterations=k + 1, ) - iter_log_record.log(config.progress_logger.info) - return model_data, separation_results # === Check if we terminate due to robust optimality or feasibility, # or in the event of bypassing global separation, no violations @@ -400,17 +319,13 @@ def ROSolver_iterative_solve(model_data, config): termination_condition = pyrosTerminationCondition.robust_optimal else: termination_condition = pyrosTerminationCondition.robust_feasible - update_grcs_solve_data( - pyros_soln=model_data, - k=k, - term_cond=termination_condition, - nominal_data=nominal_data, - timing_data=timing_data, - separation_data=separation_data, - master_soln=master_soln, - ) iter_log_record.log(config.progress_logger.info) - return model_data, separation_results + return GRCSResults( + master_results=master_soln, + separation_results=separation_results, + pyros_termination_condition=termination_condition, + iterations=k + 1, + ) # === Add block to master at violation mp_methods.add_scenario_block_to_master_problem( @@ -448,13 +363,9 @@ def ROSolver_iterative_solve(model_data, config): previous_iter_var_data = current_iter_var_data # Iteration limit reached - update_grcs_solve_data( - pyros_soln=model_data, - k=k - 1, # remove last increment to fix iteration count - term_cond=pyrosTerminationCondition.max_iter, - nominal_data=nominal_data, - timing_data=timing_data, - separation_data=separation_data, - master_soln=master_soln, + return GRCSResults( + master_results=master_soln, + separation_results=separation_results, + pyros_termination_condition=pyrosTerminationCondition.max_iter, + iterations=k, # iteration count was already incremented ) - return model_data, separation_results diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index 15214efd7da..bea637c0d56 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -1215,9 +1215,6 @@ def __init__(self, model_data, config): aux_var.value for aux_var in self.separation_model.uncertainty.auxiliary_var_list ]} - self.constraint_violations = [] - self.total_global_separation_solves = 0 - self.separation_problem_subsolver_statuses = [] if config.uncertainty_set.geometry == Geometry.DISCRETE_SCENARIOS: self.idxs_of_master_scenarios = [ From fc95ac3759bf6314e7f16629c35a6340346f8f1e Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 13 Jul 2024 01:13:45 -0400 Subject: [PATCH 1923/3044] Fix testing error messages --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 26fcdcf4fd1..a156eb96820 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -1592,16 +1592,12 @@ def test_normal_discrete_set_construction_and_update(self): dset = DiscreteScenarioSet(scenarios) # check scenarios added appropriately - np.testing.assert_allclose( - scenarios, dset.scenarios, err_msg="BoxSet bounds not as expected" - ) + np.testing.assert_allclose(scenarios, dset.scenarios) # check scenarios updated appropriately new_scenarios = [[0, 1, 2], [1, 2, 0], [3, 5, 4]] dset.scenarios = new_scenarios - np.testing.assert_allclose( - new_scenarios, dset.scenarios, err_msg="BoxSet bounds not as expected" - ) + np.testing.assert_allclose(new_scenarios, dset.scenarios) def test_error_on_discrete_set_dim_change(self): """ From 7ecd1496ead31957d30c6c50e9720ad912a95b71 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 13 Jul 2024 14:11:01 -0400 Subject: [PATCH 1924/3044] Update and more thoroughly test `EllipsoidalSet.point_in_set` --- .../pyros/tests/test_uncertainty_sets.py | 24 +++++++++++++++++-- pyomo/contrib/pyros/uncertainty_sets.py | 9 +++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index a156eb96820..a8e46c6efb2 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -2085,8 +2085,28 @@ def test_point_in_set(self): shape_matrix=[[1, 0.5], [0.5, 1]], scale=2.5, ) + sqrt_mat = np.linalg.cholesky(eset.shape_matrix) + sqrt_scale = eset.scale ** 0.5 + center = eset.center self.assertTrue(eset.point_in_set(eset.center)) + # some boundary points + self.assertTrue(eset.point_in_set(center + sqrt_mat @ [0, sqrt_scale])) + self.assertTrue(eset.point_in_set(center + sqrt_mat @ [sqrt_scale, 0])) + self.assertTrue(eset.point_in_set(center + sqrt_mat @ [0, -sqrt_scale])) + self.assertTrue(eset.point_in_set(center + sqrt_mat @ [-sqrt_scale, 0])) + + self.assertFalse(eset.point_in_set(center + sqrt_mat @ [0, sqrt_scale * 2])) + self.assertFalse(eset.point_in_set(center + sqrt_mat @ [sqrt_scale * 2, 0])) + self.assertFalse(eset.point_in_set(center + sqrt_mat @ [0, -sqrt_scale * 2])) + self.assertFalse(eset.point_in_set(center + sqrt_mat @ [-sqrt_scale * 2, 0])) + + # test singleton + eset.scale = 0 + self.assertTrue(eset.point_in_set(eset.center)) + self.assertTrue(eset.point_in_set(eset.center + [5e-9, 0])) + self.assertFalse(eset.point_in_set(eset.center + [1e-4, 1e-4])) + @unittest.skipUnless(baron_available, "BARON is not available.") def test_compute_parameter_bounds(self): """ @@ -2109,8 +2129,8 @@ def test_compute_parameter_bounds(self): ) computed_bounds_2 = eset2._compute_parameter_bounds(baron) - # add absolute tolerance to account for (im)precision - # from matrix inversion and roundoff + # add absolute tolerance to account from + # matrix inversion and roundoff errors np.testing.assert_allclose(computed_bounds_2, [[-0.5, 2.5], [0, 3]], atol=1e-8) np.testing.assert_allclose(computed_bounds_2, eset2.parameter_bounds, atol=1e-8) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 84b6752991e..ef03bc6de3b 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -2554,6 +2554,15 @@ def parameter_bounds(self): ] return parameter_bounds + @copy_docstring(UncertaintySet.point_in_set) + def point_in_set(self, point): + off_center = point - self.center + normalized_pt_radius = np.sqrt( + off_center @ np.linalg.inv(self.shape_matrix) @ off_center + ) + normalized_boundary_radius = np.sqrt(self.scale) + return normalized_pt_radius <= normalized_boundary_radius + 1e-8 + @copy_docstring(UncertaintySet.set_as_constraint) def set_as_constraint(self, uncertain_params=None, block=None): block, param_var_data_list, uncertainty_conlist, aux_var_list = ( From e720fd837f4cae5aa1c17587da9503b3cf17890e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 14 Jul 2024 15:50:51 -0600 Subject: [PATCH 1925/3044] NLv2: support models with expressions with nested external functions --- pyomo/repn/plugins/nl_writer.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index a8966e44f71..8fc82d21d30 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1988,6 +1988,18 @@ def _record_named_expression_usage(self, named_exprs, src, comp_type): elif info[comp_type] != src: info[comp_type] = 0 + def _resolve_subexpression_args(self, nl, args): + final_args = [] + for arg in args: + if arg in self.var_id_to_nl_map: + final_args.append(self.var_id_to_nl_map[arg]) + else: + _nl, _ids, _ = self.subexpression_cache[arg][1].compile_repn( + self.visitor + ) + final_args.append(self._resolve_subexpression_args(_nl, _ids)) + return nl % tuple(final_args) + def _write_nl_expression(self, repn, include_const): # Note that repn.mult should always be 1 (the AMPLRepn was # compiled before this point). Omitting the assertion for @@ -2007,18 +2019,7 @@ def _write_nl_expression(self, repn, include_const): nl % tuple(map(self.var_id_to_nl_map.__getitem__, args)) ) except KeyError: - final_args = [] - for arg in args: - if arg in self.var_id_to_nl_map: - final_args.append(self.var_id_to_nl_map[arg]) - else: - _nl, _ids, _ = self.subexpression_cache[arg][1].compile_repn( - self.visitor - ) - final_args.append( - _nl % tuple(map(self.var_id_to_nl_map.__getitem__, _ids)) - ) - self.ostream.write(nl % tuple(final_args)) + self.ostream.write(self._resolve_subexpression_args(nl, args)) elif include_const: self.ostream.write(self.template.const % repn.const) From 4464b99c969660be7309097f59a05c6aee8b60e9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 14 Jul 2024 15:51:32 -0600 Subject: [PATCH 1926/3044] Add test --- pyomo/repn/tests/ampl/test_nlv2.py | 69 ++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index b6bb5f6c074..4d7b5d9ab6c 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2703,3 +2703,72 @@ def test_presolve_check_invalid_monomial_constraints(self): r"\(fixed body value 5.0 outside bounds \[10, None\]\)\.", ): nl_writer.NLWriter().write(m, OUT, linear_presolve=True) + + def test_nested_external_expressions(self): + # This tests nested external functions in a single expression + DLL = find_GSL() + if not DLL: + self.skipTest("Could not find the amplgsl.dll library") + + m = ConcreteModel() + m.hypot = ExternalFunction(library=DLL, function="gsl_hypot") + m.p = Param(initialize=1, mutable=True) + m.x = Var(bounds=(None, 3)) + m.y = Var(bounds=(3, None)) + m.z = Var(initialize=1) + m.o = Objective(expr=m.z**2 * m.hypot(m.z, m.hypot(m.x, m.y)) ** 2) + m.c = Constraint(expr=m.x == m.y) + + OUT = io.StringIO() + nl_writer.NLWriter().write( + m, OUT, symbolic_solver_labels=True, linear_presolve=False + ) + self.assertEqual( + *nl_diff( + """g3 1 1 0 #problem unknown + 3 1 1 0 1 #vars, constraints, objectives, ranges, eqns + 0 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 3 0 #nonlinear vars in constraints, objectives, both + 0 1 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 2 3 #nonzeros in Jacobian, obj. gradient + 1 1 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +F0 1 -1 gsl_hypot +C0 #c +n0 +O0 0 #o +o2 #* +o5 #^ +v0 #z +n2 +o5 #^ +f0 2 #hypot +v0 #z +f0 2 #hypot +v1 #x +v2 #y +n2 +x1 #initial guess +0 1 #z +r #1 ranges (rhs's) +4 0 #c +b #3 bounds (on variables) +3 #z +1 3 #x +2 3 #y +k2 #intermediate Jacobian column lengths +0 +1 +J0 2 #c +1 1 +2 -1 +G0 3 #o +0 0 +1 0 +2 0 +""", + OUT.getvalue(), + ) + ) From 5c89e4a89eabf757aa4e0045b01287df7c67bbd0 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 15 Jul 2024 08:24:56 -0600 Subject: [PATCH 1927/3044] Ran Black with correct command with flags --- pyomo/contrib/doe/doe.py | 29 +++++------------- .../doe/tests/experiment_class_example.py | 16 +++------- .../tests/experiment_class_example_flags.py | 16 +++------- pyomo/contrib/doe/tests/test_doe_build.py | 8 ++--- pyomo/contrib/doe/tests/test_doe_errors.py | 30 ++++--------------- pyomo/contrib/doe/tests/test_doe_solve.py | 29 +++++------------- pyomo/contrib/doe/utils.py | 12 ++------ 7 files changed, 32 insertions(+), 108 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index d8f773879ee..403d0c8cd4c 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -237,10 +237,7 @@ def run_doe(self, model=None, results_file=None): """ # Check results file name if results_file is not None: - if type(results_file) not in [ - Path, - str, - ]: + if type(results_file) not in [Path, str]: raise ValueError( "``results_file`` must be either a Path object or a string." ) @@ -457,9 +454,7 @@ def _sequential_FIM(self, model=None): # Create suffix to keep track of parameter scenarios if hasattr(model, "parameter_scenarios"): model.del_component(model.parameter_scenarios) - model.parameter_scenarios = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) + model.parameter_scenarios = pyo.Suffix(direction=pyo.Suffix.LOCAL) # Populate parameter scenarios, and scenario inds based on finite difference scheme if self.fd_formula == FiniteDifferenceStep.central: @@ -752,9 +747,7 @@ def initialize_jac(m, i, j): return 0.1 model.sensitivity_jacobian = pyo.Var( - model.output_names, - model.parameter_names, - initialize=initialize_jac, + model.output_names, model.parameter_names, initialize=initialize_jac ) # Initialize the FIM @@ -770,15 +763,11 @@ def initialize_fim(m, j, d): if self.fim_initial is not None: model.fim = pyo.Var( - model.parameter_names, - model.parameter_names, - initialize=initialize_fim, + model.parameter_names, model.parameter_names, initialize=initialize_fim ) else: model.fim = pyo.Var( - model.parameter_names, - model.parameter_names, - initialize=identity_matrix, + model.parameter_names, model.parameter_names, initialize=identity_matrix ) # To-Do: Look into this functionality..... @@ -801,9 +790,7 @@ def init_cho(m, i, j): # Initialize with L in L_initial if self.L_initial is not None: model.L = pyo.Var( - model.parameter_names, - model.parameter_names, - initialize=init_cho, + model.parameter_names, model.parameter_names, initialize=init_cho ) # or initialize with the identity matrix else: @@ -993,9 +980,7 @@ def _generate_scenario_blocks(self, model=None): self.jac_initial = np.eye(self.n_experiment_outputs, self.n_parameters) # Make a new Suffix to hold which scenarios are associated with parameters - model.parameter_scenarios = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) + model.parameter_scenarios = pyo.Suffix(direction=pyo.Suffix.LOCAL) # Populate parameter scenarios, and scenario inds based on finite difference scheme if self.fd_formula == FiniteDifferenceStep.central: diff --git a/pyomo/contrib/doe/tests/experiment_class_example.py b/pyomo/contrib/doe/tests/experiment_class_example.py index 08d205df39b..56fdda6a4a9 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example.py +++ b/pyomo/contrib/doe/tests/experiment_class_example.py @@ -204,18 +204,14 @@ def label_experiment_impl(self, index_sets_meas): # Grab measurement labels base_comp_meas = [m.CA, m.CB, m.CC] - m.experiment_outputs = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.experiment_outputs.update( (k, None) for k in expand_model_components(m, base_comp_meas, index_sets_meas) ) # Adding no error for measurements currently - m.measurement_error = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) + m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.measurement_error.update( (k, 1e-2) for k in expand_model_components(m, base_comp_meas, index_sets_meas) @@ -224,17 +220,13 @@ def label_experiment_impl(self, index_sets_meas): # Grab design variables base_comp_des = [m.CA, m.T] index_sets_des = [[[m.t.first()]], [m.t_control]] - m.experiment_inputs = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) + m.experiment_inputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.experiment_inputs.update( (k, pyo.ComponentUID(k)) for k in expand_model_components(m, base_comp_des, index_sets_des) ) - m.unknown_parameters = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update((k, pyo.value(k)) for k in [m.A1, m.A2, m.E1, m.E2]) diff --git a/pyomo/contrib/doe/tests/experiment_class_example_flags.py b/pyomo/contrib/doe/tests/experiment_class_example_flags.py index 0e4ee7c816f..9b89d19d759 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example_flags.py +++ b/pyomo/contrib/doe/tests/experiment_class_example_flags.py @@ -210,9 +210,7 @@ def label_experiment_impl(self, index_sets_meas, flag=0): if flag != 1: # Grab measurement labels - m.experiment_outputs = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.experiment_outputs.update( (k, None) for k in expand_model_components(m, base_comp_meas, index_sets_meas) @@ -220,9 +218,7 @@ def label_experiment_impl(self, index_sets_meas, flag=0): if flag != 2: # Adding no error for measurements currently - m.measurement_error = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) + m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) if flag == 5: m.measurement_error.update((m.CA[0], 1e-2) for k in range(1)) else: @@ -235,18 +231,14 @@ def label_experiment_impl(self, index_sets_meas, flag=0): # Grab design variables base_comp_des = [m.CA, m.T] index_sets_des = [[[m.t.first()]], [m.t_control]] - m.experiment_inputs = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) + m.experiment_inputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.experiment_inputs.update( (k, pyo.ComponentUID(k)) for k in expand_model_components(m, base_comp_des, index_sets_des) ) if flag != 4: - m.unknown_parameters = pyo.Suffix( - direction=pyo.Suffix.LOCAL, - ) + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.unknown_parameters.update( (k, pyo.value(k)) for k in [m.A1, m.A2, m.E1, m.E2] ) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 9365b289399..065b8cce711 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -25,9 +25,7 @@ data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} -def get_FIM_FIMPrior_Q_L( - doe_obj=None, -): +def get_FIM_FIMPrior_Q_L(doe_obj=None): """ Helper function to retreive results to compare. @@ -62,9 +60,7 @@ def get_FIM_FIMPrior_Q_L( ] sigma_inv = [1 / v for k, v in model.scenario_blocks[0].measurement_error.items()] param_vals = np.array( - [ - [v for k, v in model.scenario_blocks[0].unknown_parameters.items()], - ] + [[v for k, v in model.scenario_blocks[0].unknown_parameters.items()]] ) FIM_vals_np = np.array(FIM_vals).reshape((n_param, n_param)) diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 4074415c9fd..9977bffe94e 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -395,10 +395,7 @@ def test_reactor_grid_search_des_range_inputs(self): _only_compute_fim_lower=True, ) - design_ranges = { - "not": [1, 5, 3], - "correct": [300, 700, 3], - } + design_ranges = {"not": [1, 5, 3], "correct": [300, 700, 3]} with self.assertRaisesRegex( ValueError, @@ -470,10 +467,7 @@ def test_reactor_figure_drawing_no_des_var_names(self): _only_compute_fim_lower=True, ) - design_ranges = { - "CA[0]": [1, 5, 2], - "T[0]": [300, 700, 2], - } + design_ranges = {"CA[0]": [1, 5, 2], "T[0]": [300, 700, 2]} doe_obj.compute_FIM_full_factorial( design_ranges=design_ranges, method="sequential" @@ -513,10 +507,7 @@ def test_reactor_figure_drawing_no_sens_names(self): _only_compute_fim_lower=True, ) - design_ranges = { - "CA[0]": [1, 5, 2], - "T[0]": [300, 700, 2], - } + design_ranges = {"CA[0]": [1, 5, 2], "T[0]": [300, 700, 2]} doe_obj.compute_FIM_full_factorial( design_ranges=design_ranges, method="sequential" @@ -555,10 +546,7 @@ def test_reactor_figure_drawing_no_fixed_names(self): _only_compute_fim_lower=True, ) - design_ranges = { - "CA[0]": [1, 5, 2], - "T[0]": [300, 700, 2], - } + design_ranges = {"CA[0]": [1, 5, 2], "T[0]": [300, 700, 2]} doe_obj.compute_FIM_full_factorial( design_ranges=design_ranges, method="sequential" @@ -597,10 +585,7 @@ def test_reactor_figure_drawing_bad_fixed_names(self): _only_compute_fim_lower=True, ) - design_ranges = { - "CA[0]": [1, 5, 2], - "T[0]": [300, 700, 2], - } + design_ranges = {"CA[0]": [1, 5, 2], "T[0]": [300, 700, 2]} doe_obj.compute_FIM_full_factorial( design_ranges=design_ranges, method="sequential" @@ -643,10 +628,7 @@ def test_reactor_figure_drawing_bad_sens_names(self): _only_compute_fim_lower=True, ) - design_ranges = { - "CA[0]": [1, 5, 2], - "T[0]": [300, 700, 2], - } + design_ranges = {"CA[0]": [1, 5, 2], "T[0]": [300, 700, 2]} doe_obj.compute_FIM_full_factorial( design_ranges=design_ranges, method="sequential" diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 5577a56f055..5ac2c879a42 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -25,9 +25,7 @@ data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} -def get_FIM_Q_L( - doe_obj=None, -): +def get_FIM_Q_L(doe_obj=None): """ Helper function to retreive results to compare. @@ -57,9 +55,7 @@ def get_FIM_Q_L( ] sigma_inv = [1 / v for k, v in model.scenario_blocks[0].measurement_error.items()] param_vals = np.array( - [ - [v for k, v in model.scenario_blocks[0].unknown_parameters.items()], - ] + [[v for k, v in model.scenario_blocks[0].unknown_parameters.items()]] ) FIM_vals_np = np.array(FIM_vals).reshape((n_param, n_param)) @@ -114,9 +110,7 @@ def test_reactor_fd_central_solve(self): assert doe_obj.results["Solver Status"] == "ok" # Assert that Q, F, and L are the same. - FIM, Q, L, sigma_inv = get_FIM_Q_L( - doe_obj=doe_obj, - ) + FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) # Since Trace is used, no comparison for FIM and L.T @ L @@ -155,9 +149,7 @@ def test_reactor_fd_forward_solve(self): assert doe_obj.results["Solver Status"] == "ok" # Assert that Q, F, and L are the same. - FIM, Q, L, sigma_inv = get_FIM_Q_L( - doe_obj=doe_obj, - ) + FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) # Since Trace is used, no comparison for FIM and L.T @ L @@ -196,9 +188,7 @@ def test_reactor_fd_backward_solve(self): assert doe_obj.results["Solver Status"] == "ok" # Assert that Q, F, and L are the same. - FIM, Q, L, sigma_inv = get_FIM_Q_L( - doe_obj=doe_obj, - ) + FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) # Since Trace is used, no comparison for FIM and L.T @ L @@ -270,9 +260,7 @@ def test_reactor_obj_cholesky_solve(self): assert doe_obj.results["Solver Status"] == "ok" # Assert that Q, F, and L are the same. - FIM, Q, L, sigma_inv = get_FIM_Q_L( - doe_obj=doe_obj, - ) + FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) # Since Cholesky is used, there is comparison for FIM and L.T @ L assert np.all(np.isclose(FIM, L @ L.T)) @@ -337,10 +325,7 @@ def test_reactor_grid_search(self): _only_compute_fim_lower=True, ) - design_ranges = { - "CA[0]": [1, 5, 3], - "T[0]": [300, 700, 3], - } + design_ranges = {"CA[0]": [1, 5, 3], "T[0]": [300, 700, 3]} doe_obj.compute_FIM_full_factorial( design_ranges=design_ranges, method="sequential" diff --git a/pyomo/contrib/doe/utils.py b/pyomo/contrib/doe/utils.py index ed0569e461f..cb00dfcd67f 100644 --- a/pyomo/contrib/doe/utils.py +++ b/pyomo/contrib/doe/utils.py @@ -43,11 +43,7 @@ def rescale_FIM(FIM, param_vals): """ if isinstance(param_vals, list): - param_vals = np.array( - [ - param_vals, - ] - ) + param_vals = np.array([param_vals]) elif isinstance(param_vals, np.ndarray): if len(param_vals.shape) > 2 or ( (len(param_vals.shape) == 2) and (param_vals.shape[0] != 1) @@ -58,11 +54,7 @@ def rescale_FIM(FIM, param_vals): ) ) if len(param_vals.shape) == 1: - param_vals = np.array( - [ - param_vals, - ] - ) + param_vals = np.array([param_vals]) scaling_mat = (1 / param_vals).transpose().dot((1 / param_vals)) scaled_FIM = np.multiply(FIM, scaling_mat) return scaled_FIM From 8bbf084207d57efaaa6817d265a6c436b1aa2379 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 15 Jul 2024 08:32:43 -0600 Subject: [PATCH 1928/3044] Fixed typos --- pyomo/contrib/doe/doe.py | 20 ++++++++++---------- pyomo/contrib/doe/tests/test_doe_build.py | 2 +- pyomo/contrib/doe/tests/test_doe_solve.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 403d0c8cd4c..1a83598c1f5 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -104,7 +104,7 @@ def __init__( should have a ``get_labeled_model`` where a model is returned with the following labeled sets: ``unknown_parameters``, ``experimental_inputs``, ``experimental_outputs`` fd_formula: - Finite difference formula for computing the sensitivy matrix. Must be one of + Finite difference formula for computing the sensitivity matrix. Must be one of [``central``, ``forward``, ``backward``], default: ``central`` step: Relative step size for the finite difference formula. @@ -120,7 +120,7 @@ def __init__( scale_nominal_param_value: Boolean for whether or not to scale the sensitivity matrix by the nominal parameter values. Every column of the sensitivity matrix will be divided by the respective - nominal paramter value. + nominal parameter value. default: False prior_FIM: 2D numpy array representing information from prior experiments. If no value is given, @@ -262,7 +262,7 @@ def run_doe(self, model=None, results_file=None): # Track time required to build the DoE model build_time = sp_timer.toc(msg=None) self.logger.info( - "Succesfully built the DoE model.\nBuild time: %0.1f seconds" % build_time + "Successfully built the DoE model.\nBuild time: %0.1f seconds" % build_time ) # Solve the square problem first to initialize the fim and @@ -279,7 +279,7 @@ def run_doe(self, model=None, results_file=None): # Track time to initialize the DoE model initialization_time = sp_timer.toc(msg=None) self.logger.info( - "Succesfully initialized the DoE model.\nInitialization time: %0.1f seconds" + "Successfully initialized the DoE model.\nInitialization time: %0.1f seconds" % initialization_time ) @@ -316,7 +316,7 @@ def run_doe(self, model=None, results_file=None): solve_time = sp_timer.toc(msg=None) self.logger.info( - "Succesfully optimized experiment.\nSolve time: %0.1f seconds" % solve_time + "Successfully optimized experiment.\nSolve time: %0.1f seconds" % solve_time ) self.logger.info( "Total time for build, initialization, and solve: %0.1f seconds" @@ -371,13 +371,13 @@ def run_doe(self, model=None, results_file=None): # Perform multi-experiment doe (sequential, or ``greedy`` approach) def run_multi_doe_sequential(self, N_exp=1): raise NotImplementedError( - "Multipled experiment optimization not yet supported." + "Multiple experiment optimization not yet supported." ) # Perform multi-experiment doe (simultaneous, optimal approach) def run_multi_doe_simultaneous(self, N_exp=1): raise NotImplementedError( - "Multipled experiment optimization not yet supported." + "Multiple experiment optimization not yet supported." ) # Compute FIM for the DoE object @@ -1154,7 +1154,7 @@ def cholesky_imp(m, c, d): Calculate Cholesky L matrix using algebraic constraints """ # If the row is greater than or equal to the column, we are in the - # lower traingle region of the L and FIM matrices. + # lower triangle region of the L and FIM matrices. # This region is where our equations are well-defined. if list(model.parameter_names).index(c) >= list( model.parameter_names @@ -1363,7 +1363,7 @@ def update_FIM_prior(self, model=None, FIM=None): # ToDo: Add an update function for the parameter values? --> closed loop parameter estimation? # Or leave this to the user????? - def udpate_unknown_parameter_values(self, model=None, param_vals=None): + def update_unknown_parameter_values(self, model=None, param_vals=None): return # Evaluates FIM and statistics for a full factorial space (same as run_grid_search) @@ -1391,7 +1391,7 @@ def compute_FIM_full_factorial(self, design_ranges=None, method="sequential"): self.factorial_model = self.experiment.get_labeled_model(**self.args).clone() model = self.factorial_model - # Permute the inputs to be aligned with the experiment input indicies + # Permute the inputs to be aligned with the experiment input indices design_ranges_enum = {k: np.linspace(*v) for k, v in design_ranges.items()} design_map = { ind: (k[0].name, k[0]) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 065b8cce711..cfadecb867c 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -27,7 +27,7 @@ def get_FIM_FIMPrior_Q_L(doe_obj=None): """ - Helper function to retreive results to compare. + Helper function to retrieve results to compare. """ model = doe_obj.model diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 5ac2c879a42..78abb857717 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -27,7 +27,7 @@ def get_FIM_Q_L(doe_obj=None): """ - Helper function to retreive results to compare. + Helper function to retrieve results to compare. """ model = doe_obj.model From 615329d448435796f534fb2bc3281cdb942bf99e Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 15 Jul 2024 08:35:05 -0600 Subject: [PATCH 1929/3044] Ran Black again --- pyomo/contrib/doe/doe.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 1a83598c1f5..ceed2fe422a 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -370,15 +370,11 @@ def run_doe(self, model=None, results_file=None): # Perform multi-experiment doe (sequential, or ``greedy`` approach) def run_multi_doe_sequential(self, N_exp=1): - raise NotImplementedError( - "Multiple experiment optimization not yet supported." - ) + raise NotImplementedError("Multiple experiment optimization not yet supported.") # Perform multi-experiment doe (simultaneous, optimal approach) def run_multi_doe_simultaneous(self, N_exp=1): - raise NotImplementedError( - "Multiple experiment optimization not yet supported." - ) + raise NotImplementedError("Multiple experiment optimization not yet supported.") # Compute FIM for the DoE object def compute_FIM(self, model=None, method="sequential"): From 5c82a5741ed0e9cb6fdd121f0c510838365b0131 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 15 Jul 2024 08:37:33 -0600 Subject: [PATCH 1930/3044] Fixed another typo --- pyomo/contrib/doe/doe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index ceed2fe422a..01f4edd8603 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -114,7 +114,7 @@ def __init__( ``det`` (for determinant, or D-optimality) and ``trace`` (for trace or A-optimality) scale_constant_value: - Constant scaling for the sensitivty matrix. Every element will be multiplied by this + Constant scaling for the sensitivity matrix. Every element will be multiplied by this scaling factor. default: 1 scale_nominal_param_value: From d13f94bd9c7c462c7cfe898accc81ea76a930bd9 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 15 Jul 2024 09:26:47 -0600 Subject: [PATCH 1931/3044] Removing commented lines from example files --- pyomo/contrib/doe/tests/experiment_class_example.py | 5 ----- pyomo/contrib/doe/tests/experiment_class_example_flags.py | 5 ----- 2 files changed, 10 deletions(-) diff --git a/pyomo/contrib/doe/tests/experiment_class_example.py b/pyomo/contrib/doe/tests/experiment_class_example.py index 56fdda6a4a9..57bc7b424a1 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example.py +++ b/pyomo/contrib/doe/tests/experiment_class_example.py @@ -73,11 +73,6 @@ def create_model(self): # Model parameters m.R = pyo.Param(mutable=False, initialize=8.314) - # m.A1 = pyo.Param(mutable=True) - # m.E1 = pyo.Param(mutable=True) - # m.A2 = pyo.Param(mutable=True) - # m.E2 = pyo.Param(mutable=True) - # Define model variables ######################## # time diff --git a/pyomo/contrib/doe/tests/experiment_class_example_flags.py b/pyomo/contrib/doe/tests/experiment_class_example_flags.py index 9b89d19d759..0869dee1570 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example_flags.py +++ b/pyomo/contrib/doe/tests/experiment_class_example_flags.py @@ -78,11 +78,6 @@ def create_model(self): # Model parameters m.R = pyo.Param(mutable=False, initialize=8.314) - # m.A1 = pyo.Param(mutable=True) - # m.E1 = pyo.Param(mutable=True) - # m.A2 = pyo.Param(mutable=True) - # m.E2 = pyo.Param(mutable=True) - # Define model variables ######################## # time From 0625cec5cdd62bf0aa24a9034eed65fee70f3809 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 15 Jul 2024 09:50:53 -0600 Subject: [PATCH 1932/3044] Added ipopt check for build, which initializes the model using ipopt --- pyomo/contrib/doe/tests/test_doe_build.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index cfadecb867c..db009f32742 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -83,6 +83,7 @@ def get_FIM_FIMPrior_Q_L(doe_obj=None): class TestReactorExampleBuild(unittest.TestCase): + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_central_check_fd_eqns(self): fd_method = "central" @@ -138,6 +139,7 @@ def test_reactor_fd_central_check_fd_eqns(self): assert np.isclose(param_val, param_val_from_step) + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_backward_check_fd_eqns(self): fd_method = "backward" @@ -195,6 +197,7 @@ def test_reactor_fd_backward_check_fd_eqns(self): other_param_val = pyo.value(k) assert np.isclose(other_param_val, v) + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_forward_check_fd_eqns(self): fd_method = "forward" @@ -252,6 +255,7 @@ def test_reactor_fd_forward_check_fd_eqns(self): other_param_val = pyo.value(k) assert np.isclose(other_param_val, v) + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_central_design_fixing(self): fd_method = "central" @@ -302,6 +306,7 @@ def test_reactor_fd_central_design_fixing(self): # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) assert not hasattr(model, con_name_base + str(len(design_vars))) + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_backward_design_fixing(self): fd_method = "backward" @@ -352,6 +357,7 @@ def test_reactor_fd_backward_design_fixing(self): # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) assert not hasattr(model, con_name_base + str(len(design_vars))) + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_forward_design_fixing(self): fd_method = "forward" From c6ddbc97ee5a08b465c875f86f6b6f09e7734084 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 15 Jul 2024 10:33:34 -0600 Subject: [PATCH 1933/3044] allow None to override variable value, but log warning --- pyomo/core/plugins/transform/scaling.py | 14 +++++++++++--- pyomo/core/tests/transform/test_scaling.py | 6 +++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index 654903773bd..4c427e72b92 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import logging from pyomo.common.collections import ComponentMap from pyomo.core.base import Block, Var, Constraint, Objective, Suffix, value from pyomo.core.plugins.transform.hierarchy import Transformation @@ -17,6 +18,8 @@ from pyomo.core.expr import replace_expressions from pyomo.util.components import rename_components +logger = logging.getLogger("pyomo.core.plugins.transform.scaling") + @TransformationFactory.register( 'core.scale_model', doc="Scale model variables, constraints, and objectives." @@ -313,9 +316,14 @@ def propagate_solution(self, scaled_model, original_model): original_v = original_model.find_component(original_v_path) for k in scaled_v: - if scaled_v[k].value is not None: - # NOTE: if the variable is set to None in the scaled model, - # we don't attempt to change its value in the original model + if scaled_v[k].value is None and original_v[k].value is not None: + logger.warning( + "Variable with value None in the scaled model is replacing" + f" value of variable {original_v[k].name} in the original" + f" model with None (was {original_v[k].value})." + ) + original_v[k].set_value(None, skip_validation=True) + elif scaled_v[k].value is not None: original_v[k].set_value( value(scaled_v[k]) / component_scaling_factor_map[scaled_v[k]], skip_validation=True, diff --git a/pyomo/core/tests/transform/test_scaling.py b/pyomo/core/tests/transform/test_scaling.py index e354916c309..cb31aaa33ec 100644 --- a/pyomo/core/tests/transform/test_scaling.py +++ b/pyomo/core/tests/transform/test_scaling.py @@ -703,9 +703,9 @@ def test_propagate_solution_uninitialized_variable(self): scaled_model, m ) self.assertAlmostEqual(m.x[1].value, 2.0, delta=1e-8) - # Note that because x[2] was None in the scaled model, its value is unchanged - # (and has not been overridden and set to None). - self.assertEqual(m.x[2].value, 1.0) + # Note that value of x[2] in original model *has* been overriddeen to None. + # In this case, a warning has been raised. + self.assertIs(m.x[2].value, None) if __name__ == "__main__": From 6d25c370393d34454d79711b4290c64aca6472ed Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 16 Jul 2024 08:01:29 -0600 Subject: [PATCH 1934/3044] Disable interface/testing for NEOS/octeract --- pyomo/neos/__init__.py | 2 +- pyomo/neos/tests/test_neos.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pyomo/neos/__init__.py b/pyomo/neos/__init__.py index 7d18535e753..9f910f4a302 100644 --- a/pyomo/neos/__init__.py +++ b/pyomo/neos/__init__.py @@ -30,7 +30,7 @@ 'minos': 'SLC NLP solver', 'minto': 'MILP solver', 'mosek': 'Interior point NLP solver', - 'octeract': 'Deterministic global MINLP solver', + #'octeract': 'Deterministic global MINLP solver', 'ooqp': 'Convex QP solver', 'path': 'Nonlinear MCP solver', 'snopt': 'SQP NLP solver', diff --git a/pyomo/neos/tests/test_neos.py b/pyomo/neos/tests/test_neos.py index a4c4e9e6367..01b19a76b15 100644 --- a/pyomo/neos/tests/test_neos.py +++ b/pyomo/neos/tests/test_neos.py @@ -149,8 +149,11 @@ def test_minto(self): def test_mosek(self): self._run('mosek') - def test_octeract(self): - self._run('octeract') + # [16 Jul 24] Octeract is erroring. We will disable the interface + # (and testing) until we have time to resolve #3321 + # + # def test_octeract(self): + # self._run('octeract') def test_ooqp(self): if self.sense == pyo.maximize: From 5a8ea16c9965d6dbaca5a93fbea81ed2d10335b5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 16 Jul 2024 08:31:01 -0600 Subject: [PATCH 1935/3044] Remove octeract as an expected solver interface --- pyomo/neos/tests/test_neos.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/neos/tests/test_neos.py b/pyomo/neos/tests/test_neos.py index 01b19a76b15..363368cd616 100644 --- a/pyomo/neos/tests/test_neos.py +++ b/pyomo/neos/tests/test_neos.py @@ -79,6 +79,9 @@ def test_doc(self): doc = pyomo.neos.doc dockeys = set(doc.keys()) + # Octeract interface is disabled, see #3321 + amplsolvers.pop('octeract') + self.assertEqual(amplsolvers, dockeys) # gamssolvers = set(v[0].lower() for v in tmp if v[1]=='GAMS') From e7a3711475bc905f6a4a608b4cf8cbcfb178b59e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 16 Jul 2024 09:05:52 -0600 Subject: [PATCH 1936/3044] bugfix: use correct set api --- pyomo/neos/tests/test_neos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/neos/tests/test_neos.py b/pyomo/neos/tests/test_neos.py index 363368cd616..681856781be 100644 --- a/pyomo/neos/tests/test_neos.py +++ b/pyomo/neos/tests/test_neos.py @@ -80,7 +80,7 @@ def test_doc(self): dockeys = set(doc.keys()) # Octeract interface is disabled, see #3321 - amplsolvers.pop('octeract') + amplsolvers.remove('octeract') self.assertEqual(amplsolvers, dockeys) From 8e942de294e1fbfa3471ac73ad3e486cac10d27c Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 16 Jul 2024 09:54:14 -0600 Subject: [PATCH 1937/3044] Ensure DR polishing success status properly updated --- pyomo/contrib/pyros/pyros_algorithm_methods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index bafd6f87e5a..5d66ff98125 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -210,7 +210,7 @@ def ROSolver_iterative_solve(model_data, config): and k != 0 ) if polish_master_solution: - master_data.solve_dr_polishing() + _, polishing_successful = master_data.solve_dr_polishing() # track variable values current_iter_var_data = get_variable_value_data( From c7518020317ce25bc316c261bc0b7fb29d8b5acd Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 16 Jul 2024 09:58:06 -0600 Subject: [PATCH 1938/3044] Simplify namedtuple import --- pyomo/contrib/pyros/pyros_algorithm_methods.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 5d66ff98125..058b25e374e 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -13,6 +13,8 @@ Methods for execution of the main PyROS cutting set algorithm. """ +from collections import namedtuple + from pyomo.common.dependencies import numpy as np from pyomo.common.collections import ComponentMap from pyomo.core.base import value @@ -74,8 +76,6 @@ def get_variable_value_data(working_blk, dr_var_to_monomial_map): """ Get variable value data. """ - from collections import namedtuple - VariableValueData = namedtuple( "VariableValueData", ("first_stage_variables", "second_stage_variables", "decision_rule_monomials"), From 93e64f813fe17ac5bd5896ff0ec25ba2ebb38b16 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 16 Jul 2024 16:17:42 -0600 Subject: [PATCH 1939/3044] Tweak variable names and some comments --- pyomo/repn/parameterized_quadratic.py | 31 +++++++++++++++------------ 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 7528b438d20..3694bbde0df 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -236,9 +236,8 @@ def _handle_product_linear_linear(visitor, node, arg1, arg2): arg1.linear = {} elif not is_equal_to(arg2.constant, 1): c = arg2.constant - _linear = arg1.linear - for vid, coef in _linear.items(): - _linear[vid] = c * coef + for vid, coef in arg1.linear.items(): + arg1.linear[vid] = c * coef if not is_zero(arg1.constant): _merge_dict(arg1.linear, arg1.constant, arg2.linear) @@ -258,33 +257,35 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): ans.nonlinear = to_expression(visitor, arg1) * to_expression(visitor, arg2) return _GENERAL, ans - # We are multiplying (A + Bx + Cx^2 + D(x)) * (A + Bx + Cx^2 + Dx)) + # multiplying (A1 + B1x + C1x^2 + D1(x)) * (A2 + B2x + C2x^2 + D2x)) _, x1 = arg1 _, x2 = arg2 ans.multiplier = x1.multiplier * x2.multiplier x1.multiplier = x2.multiplier = 1 - # x1.const * x2.const [AA] + + # constant term [A1A2] # TODO: what if either constant is NaN? if is_zero(x1.constant) or is_zero(x2.constant): ans.constant = 0 else: ans.constant = x1.constant * x2.constant + # linear & quadratic terms if not is_zero(x2.constant): - # [BA], [CA] - c = x2.constant - if is_equal_to(c, 1): + # [B1A2], [C1A2] + x2_c = x2.constant + if is_equal_to(x2_c, 1): ans.linear = dict(x1.linear) if x1.quadratic: ans.quadratic = dict(x1.quadratic) else: - ans.linear = {vid: c * coef for vid, coef in x1.linear.items()} + ans.linear = {vid: x2_c * coef for vid, coef in x1.linear.items()} if x1.quadratic: - ans.quadratic = {k: c * coef for k, coef in x1.quadratic.items()} + ans.quadratic = {k: x2_c * coef for k, coef in x1.quadratic.items()} if not is_zero(x1.constant): - # [AB] + # [A1B2] _merge_dict(ans.linear, x1.constant, x2.linear) - # [AC] + # [A1C2] if x2.quadratic: if ans.quadratic: _merge_dict(ans.quadratic, x1.constant, x2.quadratic) @@ -293,14 +294,16 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): else: c = x1.constant ans.quadratic = {k: c * coef for k, coef in x2.quadratic.items()} - # [BB] + # [B1B2] if x1.linear and x2.linear: quad = _mul_linear_linear(visitor.var_order.__getitem__, x1.linear, x2.linear) if ans.quadratic: _merge_dict(ans.quadratic, 1, quad) else: ans.quadratic = quad - # [DA] + [DB] + [DC] + [DD] + + # nonlinear portion + # [D1A2] + [D1B2] + [D1C2] + [D1D2] ans.nonlinear = 0 if x1.nonlinear is not None: ans.nonlinear += x1.nonlinear * x2.to_expression(visitor) From 06ab631ec0b5d877fee983d32297093b8bdf21df Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 16 Jul 2024 16:27:43 -0600 Subject: [PATCH 1940/3044] Tweak variable names and some comments --- pyomo/repn/parameterized_quadratic.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 3694bbde0df..ac9e9247586 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -434,19 +434,26 @@ def finalizeResult(self, result): for vidpair, coef in quadratic_zeros: del ans.quadratic[vidpair] elif not mult: - # the multiplier has cleared out the entire expression. Check - # if this is suppressing a NaN because we can't clear everything - # out if it is - if ans.constant != ans.constant or any( - c != c for c in ans.linear.values() - ): + # the multiplier has cleared out the entire expression. + # check if this is suppressing a NaN because we can't + # clear everything out if it is + has_nan_coefficient = ( + ans.constant != ans.constant + or any(lcoeff != lcoeff for lcoeff in ans.linear.values()) + or ( + ans.quadratic is not None + and any(qcoeff != qcoeff for qcoeff in ans.quadratic.values()) + ) + ) + if has_nan_coefficient: # There's a nan in here, so we distribute the 0 self._factor_multiplier_into_quadratic_terms(ans, mult) return ans return self.Result() else: # mult not in {0, 1}: factor it into the constant, - # linear coefficients, and nonlinear term + # linear coefficients, quadratic coefficients, + # and nonlinear term self._factor_multiplier_into_quadratic_terms(ans, mult) return ans From bb0199429c6f7909105b9923ce49981960a6a526 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 16 Jul 2024 18:28:10 -0600 Subject: [PATCH 1941/3044] Remove unused `beforeChild` dispatcher --- pyomo/repn/parameterized_quadratic.py | 30 --------------------------- 1 file changed, 30 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index ac9e9247586..356fc10c3f8 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -185,36 +185,6 @@ def append(self, other): self.nonlinear += nl -class ParameterizedQuadraticBeforeChildDispatcher( - ParameterizedLinearBeforeChildDispatcher -): - @staticmethod - def _before_linear(visitor, child): - return True, None - - @staticmethod - def _before_var(visitor, child): - _id = id(child) - if _id not in visitor.var_map: - if child.fixed: - return False, (_CONSTANT, visitor.check_constant(child.value, child)) - if child in visitor.wrt: - # pseudo-constant - # We aren't treating this Var as a Var for the purposes of this walker - return False, (_FIXED, child) - # This is a normal situation - ParameterizedLinearBeforeChildDispatcher._record_var(visitor, child) - ans = visitor.Result() - ans.linear[_id] = 1 - return False, (ExprType.LINEAR, ans) - - @staticmethod - def _before_param(visitor, child): - ans = visitor.Result() - ans.constant = child - return False, (_CONSTANT, ans) - - def is_zero(obj): """Return true if expression/constant is zero, False otherwise.""" return obj.__class__ in native_numeric_types and not obj From 3675202dd93f96a272bda8932a34e1da703644e8 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 10:26:12 -0600 Subject: [PATCH 1942/3044] Account for zeros more carefully --- pyomo/repn/parameterized_quadratic.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 356fc10c3f8..af1c4de78ab 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -212,11 +212,7 @@ def _handle_product_linear_linear(visitor, node, arg1, arg2): _merge_dict(arg1.linear, arg1.constant, arg2.linear) # Finally, the constant and multipliers - # TODO: what if arg1.constant or arg2.constant is nan? - if is_zero(arg1.constant) or is_zero(arg2.constant): - arg1.constant = 0 - else: - arg1.constant *= arg2.constant + arg1.constant *= arg2.constant arg1.multiplier *= arg2.multiplier return _QUADRATIC, arg1 @@ -234,14 +230,13 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): x1.multiplier = x2.multiplier = 1 # constant term [A1A2] - # TODO: what if either constant is NaN? - if is_zero(x1.constant) or is_zero(x2.constant): + if not x1.constant and not x2.constant: ans.constant = 0 else: ans.constant = x1.constant * x2.constant # linear & quadratic terms - if not is_zero(x2.constant): + if x2.constant: # [B1A2], [C1A2] x2_c = x2.constant if is_equal_to(x2_c, 1): @@ -252,7 +247,7 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): ans.linear = {vid: x2_c * coef for vid, coef in x1.linear.items()} if x1.quadratic: ans.quadratic = {k: x2_c * coef for k, coef in x1.quadratic.items()} - if not is_zero(x1.constant): + if x1.constant: # [A1B2] _merge_dict(ans.linear, x1.constant, x2.linear) # [A1C2] @@ -293,7 +288,8 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): x1.linear = x1_lin ans.nonlinear += x1.to_expression(visitor) * x2.to_expression(visitor) # [AD] - if not is_zero(x1_c) and x2.nonlinear is not None: + if x1_c and x2.nonlinear is not None: + # TODO: what if nonlinear contains nan? ans.nonlinear += x1_c * x2.nonlinear return _GENERAL, ans From 0cf9c7ce88a3909397ef43f1657eadc366532588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Wed, 17 Jul 2024 18:38:43 +0200 Subject: [PATCH 1943/3044] Applied black. --- pyomo/core/base/set.py | 3 +- pyomo/core/tests/unit/test_set.py | 1 + pyomo/core/tests/unit/test_sets.py | 56 ++++++++++++++++++++---------- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 33737b130e8..c4a1866f13c 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -506,6 +506,7 @@ def _tuplize(self, _val, parent, index): class _NotFound(object): "Internal type flag used to indicate if an object is not found in a set" + pass @@ -1420,7 +1421,7 @@ def add(self, *values): # _value is not a tuple: no need to unpack it for the method arguments' tuple flag = self._validate(_block, (_value, self._index)) else: - # non-indexed set: only the tentative member is given + # non-indexed set: only the tentative member is given flag = self._validate(_block, _value) except: logger.error( diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 70dfeb26f74..e920493fcb9 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4349,6 +4349,7 @@ def _validate_I(model, i, j): # validot when it is called for the index. def _validate_J(model, i, j, index): return _validate_I(model, i, j) + m.J = Set([(0, 0), (2, 2)], validate=_validate_J) output = StringIO() with LoggingIntercept(output, 'pyomo.core'): diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index 7595e1a2fe7..5167d55defc 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2816,13 +2816,15 @@ def test_validation2(self): self.fail("fail test_within2") else: pass - + def test_validation3_pass(self): # # Create data file to test a successful validation using indexed sets # OUTPUT = open(currdir + "setsAB.dat", "w") - OUTPUT.write("data; set Z := A C; set A[A] := 1 3 5 5.5; set B[A] := 1 3 5; end;") + OUTPUT.write( + "data; set Z := A C; set A[A] := 1 3 5 5.5; set B[A] := 1 3 5; end;" + ) OUTPUT.close() # # Create A with an error @@ -2831,13 +2833,15 @@ def test_validation3_pass(self): self.model.A = Set(self.model.Z, validate=lambda model, x, i: x < 6) self.model.B = Set(self.model.Z, validate=lambda model, x, i: x in model.A[i]) self.instance = self.model.create_instance(currdir + "setsAB.dat") - + def test_validation3_fail(self): # # Create data file to test a failed validation using indexed sets # OUTPUT = open(currdir + "setsAB.dat", "w") - OUTPUT.write("data; set Z := A C; set A[A] := 1 3 5 5.5; set B[A] := 1 3 5 6; end;") + OUTPUT.write( + "data; set Z := A C; set A[A] := 1 3 5 5.5; set B[A] := 1 3 5 6; end;" + ) OUTPUT.close() # # Create A with an error @@ -2851,32 +2855,36 @@ def test_validation3_fail(self): except ValueError: error_raised = True assert error_raised - + def test_validation4_pass(self): # # Test a successful validation using indexed sets and tuple entries # - self.model.Z = Set(initialize=['A','B']) - self.model.A = Set(self.model.Z, dimen=2, initialize={'A': [(1, 2), (3, 4)], 'B': [(5, 6)]}) + self.model.Z = Set(initialize=['A', 'B']) + self.model.A = Set( + self.model.Z, dimen=2, initialize={'A': [(1, 2), (3, 4)], 'B': [(5, 6)]} + ) self.model.B = Set( self.model.Z, dimen=2, initialize={'A': [(1, 2), (3, 4)]}, - validate=lambda model, x, y, i: (x,y) in model.A[i], + validate=lambda model, x, y, i: (x, y) in model.A[i], ) self.instance = self.model.create_instance() - + def test_validation4_fail(self): # # Test a failed validation using indexed sets and tuple entries # - self.model.Z = Set(initialize=['A','B']) - self.model.A = Set(self.model.Z, dimen=2, initialize={'A': [(1, 2), (3, 4)], 'B': [(5, 6)]}) + self.model.Z = Set(initialize=['A', 'B']) + self.model.A = Set( + self.model.Z, dimen=2, initialize={'A': [(1, 2), (3, 4)], 'B': [(5, 6)]} + ) self.model.B = Set( self.model.Z, dimen=2, initialize={'A': [(1, 2), (3, 4), (5, 6)]}, - validate=lambda model, x, y, i: (x,y) in model.A[i], + validate=lambda model, x, y, i: (x, y) in model.A[i], ) error_raised = False try: @@ -2884,15 +2892,19 @@ def test_validation4_fail(self): except ValueError: error_raised = True assert error_raised - + def test_validation5_pass(self): # # Test a successful validation using indexed sets and tuple entries # - self.model.Z = Set(initialize=['A','B']) - self.model.A = Set(self.model.Z, dimen=2, initialize={'A': [(1, 2), (3, 4)], 'B': [(5, 6)]}) + self.model.Z = Set(initialize=['A', 'B']) + self.model.A = Set( + self.model.Z, dimen=2, initialize={'A': [(1, 2), (3, 4)], 'B': [(5, 6)]} + ) + def validate_B(m, e1, e2, i): return (e1, e2) in m.A[i] + self.model.B = Set( self.model.Z, dimen=2, @@ -2900,15 +2912,19 @@ def validate_B(m, e1, e2, i): validate=validate_B, ) self.instance = self.model.create_instance() - + def test_validation5_fail(self): # # Test a failed validation using indexed sets and tuple entries # - self.model.Z = Set(initialize=['A','B']) - self.model.A = Set(self.model.Z, dimen=2, initialize={'A': [(1, 2), (3, 4)], 'B': [(5, 6)]}) + self.model.Z = Set(initialize=['A', 'B']) + self.model.A = Set( + self.model.Z, dimen=2, initialize={'A': [(1, 2), (3, 4)], 'B': [(5, 6)]} + ) + def validate_B(m, e1, e2, i): return (e1, e2) in m.A[i] + self.model.B = Set( self.model.Z, dimen=2, @@ -2958,7 +2974,9 @@ def tmp_init(model, i): self.model.n = Param(initialize=5) self.model.Z = Set(initialize=['A']) self.model.A = Set( - self.model.Z, initialize=tmp_init, validate=lambda model, x, i: x in Integers + self.model.Z, + initialize=tmp_init, + validate=lambda model, x, i: x in Integers, ) try: self.instance = self.model.create_instance() From a8a78a78e9f5e68f811ee416aeb6154beade4122 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 17 Jul 2024 10:38:46 -0600 Subject: [PATCH 1944/3044] Be more direct with my comments --- pyomo/core/tests/unit/kernel/test_conic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/tests/unit/kernel/test_conic.py b/pyomo/core/tests/unit/kernel/test_conic.py index fc70b421060..bd97c13fc2e 100644 --- a/pyomo/core/tests/unit/kernel/test_conic.py +++ b/pyomo/core/tests/unit/kernel/test_conic.py @@ -786,8 +786,8 @@ def test_as_domain(self): x[1].value = None -# these mosek 10 constraints are really anemic and can't be evaluated, pprinted, -# checked for convexity, pickled, etc. +# These mosek 10 constraints can't be evaluated, pprinted, checked for convexity, +# pickled, etc., so I won't use the _conic_tester_base for them class Test_primal_geomean(unittest.TestCase): def test_as_domain(self): b = primal_geomean.as_domain(r=[2, 3], x=6) From bc54e01ef87106b9e2c578e1ff66c041d4322b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Wed, 17 Jul 2024 19:23:13 +0200 Subject: [PATCH 1945/3044] Revised test statements. --- pyomo/core/tests/unit/test_sets.py | 185 +++++++---------------------- 1 file changed, 45 insertions(+), 140 deletions(-) diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index 5167d55defc..e557a9c3487 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2396,24 +2396,19 @@ def test_dimen1(self): self.model.A = Set(initialize=[1, 2, 3], dimen=1) self.instance = self.model.create_instance() # - try: + with self.assertRaisesRegex(ValueError, ".*Cannot tuplize list data for set"): self.model.A = Set(initialize=[4, 5, 6], dimen=2) self.instance = self.model.create_instance() - except ValueError: - pass - else: - self.fail("test_dimen") - # + self.model.A = Set(initialize=[(1, 2), (2, 3), (3, 4)], dimen=2) self.instance = self.model.create_instance() # - try: + with self.assertRaisesRegex( + ValueError, + ".*has dimension 2 and is not valid for " + ): self.model.A = Set(initialize=[(1, 2), (2, 3), (3, 4)], dimen=1) self.instance = self.model.create_instance() - except ValueError: - pass - else: - self.fail("test_dimen") # def f(model): @@ -2422,22 +2417,19 @@ def f(model): self.model.A = Set(initialize=f, dimen=2) self.instance = self.model.create_instance() # - try: + with self.assertRaisesRegex( + ValueError, ".*has dimension 2 and is not valid for " + ): self.model.A = Set(initialize=f, dimen=3) self.instance = self.model.create_instance() - except ValueError: - pass - else: - self.fail("test_dimen") def test_dimen2(self): - try: + with self.assertRaisesRegex( + ValueError, ".*has dimension 2 and is not valid for " + ): self.model.A = Set(initialize=[1, 2, (3, 4)]) self.instance = self.model.create_instance() - except ValueError: - pass - else: - self.fail("test_dimen2") + self.model.A = Set(dimen=None, initialize=[1, 2, (3, 4)]) self.instance = self.model.create_instance() @@ -2496,7 +2488,7 @@ def tmp_init(model, z): self.instance = self.model.create_instance(currdir + "setA.dat") self.assertEqual(len(self.instance.A), 5) - def test_within1(self): + def test_within_fail(self): # # Create Set 'A' data file # @@ -2507,14 +2499,10 @@ def test_within1(self): # Create A with an error # self.model.A = Set(within=Integers) - try: + with self.assertRaisesRegex(ValueError, ".*Cannot add value "): self.instance = self.model.create_instance(currdir + "setA.dat") - except ValueError: - pass - else: - self.fail("fail test_within1") - def test_within2(self): + def test_within_pass(self): # # Create Set 'A' data file # @@ -2522,17 +2510,12 @@ def test_within2(self): OUTPUT.write("data; set A := 1 3 5 7.5; end;") OUTPUT.close() # - # Create A with an error + # Create A without an error # self.model.A = Set(within=Reals) - try: - self.instance = self.model.create_instance(currdir + "setA.dat") - except ValueError: - self.fail("fail test_within2") - else: - pass + self.instance = self.model.create_instance(currdir + "setA.dat") - def test_validation1(self): + def test_validation_fail(self): # # Create Set 'A' data file # @@ -2543,14 +2526,10 @@ def test_validation1(self): # Create A with an error # self.model.A = Set(validate=lambda model, x: x < 6) - try: + with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance(currdir + "setA.dat") - except ValueError: - pass - else: - self.fail("fail test_validation1") - def test_validation2(self): + def test_validation_pass(self): # # Create Set 'A' data file # @@ -2558,35 +2537,22 @@ def test_validation2(self): OUTPUT.write("data; set A := 1 3 5 5.5; end;") OUTPUT.close() # - # Create A with an error + # Create A without an error # self.model.A = Set(validate=lambda model, x: x < 6) - try: - self.instance = self.model.create_instance(currdir + "setA.dat") - except ValueError: - self.fail("fail test_validation2") - else: - pass + self.instance = self.model.create_instance(currdir + "setA.dat") def test_other1(self): self.model.A = Set( initialize=[1, 2, 3, 'A'], validate=lambda model, x: x in Integers ) - try: + with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance() - except ValueError: - pass - else: - self.fail("fail test_other1") def test_other2(self): self.model.A = Set(initialize=[1, 2, 3, 'A'], within=Integers) - try: + with self.assertRaisesRegex(ValueError, ".*Cannot add value"): self.instance = self.model.create_instance() - except ValueError: - pass - else: - self.fail("fail test_other1") def test_other3(self): OUTPUT = open(currdir + "setA.dat", "w") @@ -2601,12 +2567,8 @@ def tmp_init(model): self.model.n = Param() self.model.A = Set(initialize=tmp_init, validate=lambda model, x: x in Integers) - try: + with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance(currdir + "setA.dat") - except ValueError: - pass - else: - self.fail("fail test_other1") def test_other4(self): OUTPUT = open(currdir + "setA.dat", "w") @@ -2621,13 +2583,8 @@ def tmp_init(model): self.model.n = Param() self.model.A = Set(initialize=tmp_init, within=Integers) - try: + with self.assertRaisesRegex(ValueError, ".*Cannot add value "): self.instance = self.model.create_instance(currdir + "setA.dat") - except ValueError: - pass - else: - self.fail("fail test_other1") - class TestSetArgs2(PyomoModel): def setUp(self): @@ -2666,24 +2623,18 @@ def test_dimen(self): self.model.Z = Set(initialize=[1, 2]) self.model.A = Set(self.model.Z, initialize=[1, 2, 3], dimen=1) self.instance = self.model.create_instance() - try: + with self.assertRaisesRegex(ValueError, ".*Cannot tuplize list data for set"): self.model.A = Set(self.model.Z, initialize=[4, 5, 6], dimen=2) self.instance = self.model.create_instance() - except ValueError: - pass - else: - self.fail("test_dimen") self.model.A = Set(self.model.Z, initialize=[(1, 2), (2, 3), (3, 4)], dimen=2) self.instance = self.model.create_instance() - try: + with self.assertRaisesRegex( + ValueError, ".*has dimension 2 and is not valid for" + ): self.model.A = Set( self.model.Z, initialize=[(1, 2), (2, 3), (3, 4)], dimen=1 ) self.instance = self.model.create_instance() - except ValueError: - pass - else: - self.fail("test_dimen") def test_rule(self): # @@ -2753,12 +2704,8 @@ def test_within1(self): # self.model.Z = Set() self.model.A = Set(self.model.Z, within=Integers) - try: + with self.assertRaisesRegex(ValueError, ".*Cannot add value "): self.instance = self.model.create_instance(currdir + "setA.dat") - except ValueError: - pass - else: - self.fail("fail test_within1") def test_within2(self): # @@ -2768,16 +2715,11 @@ def test_within2(self): OUTPUT.write("data; set Z := A C; set A[A] := 1 3 5 7.5; end;") OUTPUT.close() # - # Create A with an error + # Create A without an error # self.model.Z = Set() self.model.A = Set(self.model.Z, within=Reals) - try: - self.instance = self.model.create_instance(currdir + "setA.dat") - except ValueError: - self.fail("fail test_within2") - else: - pass + self.instance = self.model.create_instance(currdir + "setA.dat") def test_validation1(self): # @@ -2791,12 +2733,8 @@ def test_validation1(self): # self.model.Z = Set() self.model.A = Set(self.model.Z, validate=lambda model, x, i: x < 6) - try: + with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance(currdir + "setA.dat") - except ValueError: - pass - else: - self.fail("fail test_within1") def test_validation2(self): # @@ -2806,16 +2744,11 @@ def test_validation2(self): OUTPUT.write("data; set Z := A C; set A[A] := 1 3 5 5.5; end;") OUTPUT.close() # - # Create A with an error + # Create A without an error # self.model.Z = Set() self.model.A = Set(self.model.Z, validate=lambda model, x, i: x < 6) - try: - self.instance = self.model.create_instance(currdir + "setA.dat") - except ValueError: - self.fail("fail test_within2") - else: - pass + self.instance = self.model.create_instance(currdir + "setA.dat") def test_validation3_pass(self): # @@ -2849,12 +2782,8 @@ def test_validation3_fail(self): self.model.Z = Set() self.model.A = Set(self.model.Z, validate=lambda model, x, i: x < 6) self.model.B = Set(self.model.Z, validate=lambda model, x, i: x in model.A[i]) - error_raised = False - try: + with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance(currdir + "setsAB.dat") - except ValueError: - error_raised = True - assert error_raised def test_validation4_pass(self): # @@ -2886,12 +2815,8 @@ def test_validation4_fail(self): initialize={'A': [(1, 2), (3, 4), (5, 6)]}, validate=lambda model, x, y, i: (x, y) in model.A[i], ) - error_raised = False - try: + with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance() - except ValueError: - error_raised = True - assert error_raised def test_validation5_pass(self): # @@ -2931,12 +2856,8 @@ def validate_B(m, e1, e2, i): initialize={'A': [(1, 2), (3, 4), (5, 6)]}, validate=validate_B, ) - error_raised = False - try: + with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance() - except ValueError: - error_raised = True - assert error_raised def test_other1(self): self.model.Z = Set(initialize=['A']) @@ -2945,24 +2866,16 @@ def test_other1(self): initialize={'A': [1, 2, 3, 'A']}, validate=lambda model, x, i: x in Integers, ) - try: + with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance() - except ValueError: - pass - else: - self.fail("fail test_other1") - + def test_other2(self): self.model.Z = Set(initialize=['A']) self.model.A = Set( self.model.Z, initialize={'A': [1, 2, 3, 'A']}, within=Integers ) - try: + with self.assertRaisesRegex(ValueError, ".*Cannot add value "): self.instance = self.model.create_instance() - except ValueError: - pass - else: - self.fail("fail test_other1") def test_other3(self): def tmp_init(model, i): @@ -2978,12 +2891,8 @@ def tmp_init(model, i): initialize=tmp_init, validate=lambda model, x, i: x in Integers, ) - try: + with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance() - except ValueError: - pass - else: - self.fail("fail test_other1") def test_other4(self): def tmp_init(model, i): @@ -2996,12 +2905,8 @@ def tmp_init(model, i): self.model.Z = Set(initialize=['A']) self.model.A = Set(self.model.Z, initialize=tmp_init, within=Integers) self.model.B = Set(self.model.Z, initialize=tmp_init, within=Integers) - try: + with self.assertRaisesRegex(ValueError, ".*Cannot add value "): self.instance = self.model.create_instance() - except ValueError: - pass - else: - self.fail("fail test_other1") class TestMisc(PyomoModel): From 080cddd4550024ca18c032201575f590639d41d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Wed, 17 Jul 2024 19:33:58 +0200 Subject: [PATCH 1946/3044] Applied black again. --- pyomo/core/tests/unit/test_sets.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index e557a9c3487..bd168c7c279 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2399,14 +2399,13 @@ def test_dimen1(self): with self.assertRaisesRegex(ValueError, ".*Cannot tuplize list data for set"): self.model.A = Set(initialize=[4, 5, 6], dimen=2) self.instance = self.model.create_instance() - + self.model.A = Set(initialize=[(1, 2), (2, 3), (3, 4)], dimen=2) self.instance = self.model.create_instance() # with self.assertRaisesRegex( - ValueError, - ".*has dimension 2 and is not valid for " - ): + ValueError, ".*has dimension 2 and is not valid for " + ): self.model.A = Set(initialize=[(1, 2), (2, 3), (3, 4)], dimen=1) self.instance = self.model.create_instance() @@ -2419,17 +2418,17 @@ def f(model): # with self.assertRaisesRegex( ValueError, ".*has dimension 2 and is not valid for " - ): + ): self.model.A = Set(initialize=f, dimen=3) self.instance = self.model.create_instance() def test_dimen2(self): with self.assertRaisesRegex( ValueError, ".*has dimension 2 and is not valid for " - ): + ): self.model.A = Set(initialize=[1, 2, (3, 4)]) self.instance = self.model.create_instance() - + self.model.A = Set(dimen=None, initialize=[1, 2, (3, 4)]) self.instance = self.model.create_instance() @@ -2586,6 +2585,7 @@ def tmp_init(model): with self.assertRaisesRegex(ValueError, ".*Cannot add value "): self.instance = self.model.create_instance(currdir + "setA.dat") + class TestSetArgs2(PyomoModel): def setUp(self): # @@ -2630,7 +2630,7 @@ def test_dimen(self): self.instance = self.model.create_instance() with self.assertRaisesRegex( ValueError, ".*has dimension 2 and is not valid for" - ): + ): self.model.A = Set( self.model.Z, initialize=[(1, 2), (2, 3), (3, 4)], dimen=1 ) @@ -2868,7 +2868,7 @@ def test_other1(self): ) with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance() - + def test_other2(self): self.model.Z = Set(initialize=['A']) self.model.A = Set( From 56f22d6154489679c94da9c4e353b0a3733ef954 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 13:43:53 -0600 Subject: [PATCH 1947/3044] Fix zero coefficient checks --- pyomo/repn/parameterized_quadratic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index af1c4de78ab..f8c6aacfc6d 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -230,13 +230,13 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): x1.multiplier = x2.multiplier = 1 # constant term [A1A2] - if not x1.constant and not x2.constant: + if is_zero(x1.constant) and is_zero(x2.constant): ans.constant = 0 else: ans.constant = x1.constant * x2.constant # linear & quadratic terms - if x2.constant: + if not is_zero(x2.constant): # [B1A2], [C1A2] x2_c = x2.constant if is_equal_to(x2_c, 1): @@ -247,7 +247,7 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): ans.linear = {vid: x2_c * coef for vid, coef in x1.linear.items()} if x1.quadratic: ans.quadratic = {k: x2_c * coef for k, coef in x1.quadratic.items()} - if x1.constant: + if not is_zero(x1.constant): # [A1B2] _merge_dict(ans.linear, x1.constant, x2.linear) # [A1C2] @@ -288,7 +288,7 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): x1.linear = x1_lin ans.nonlinear += x1.to_expression(visitor) * x2.to_expression(visitor) # [AD] - if x1_c and x2.nonlinear is not None: + if not is_zero(x1_c) and x2.nonlinear is not None: # TODO: what if nonlinear contains nan? ans.nonlinear += x1_c * x2.nonlinear return _GENERAL, ans From 8767c8b8bb4cdb79f473f24233443affbc570ba8 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 13:53:35 -0600 Subject: [PATCH 1948/3044] Implement `ParameterizedQuadraticRepn.__repn__` --- pyomo/repn/parameterized_quadratic.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index f8c6aacfc6d..2053b45bb74 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -74,6 +74,9 @@ def __str__(self): f"nonlinear={self.nonlinear})" ) + def __repr__(self): + return str(self) + def walker_exitNode(self): if self.nonlinear is not None: return _GENERAL, self From 57203c62187820637fe3514caf02dc9b55b4f5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Wed, 17 Jul 2024 22:01:35 +0200 Subject: [PATCH 1949/3044] Nothing. --- pyomo/core/tests/unit/test_sets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index bd168c7c279..d8f0a7ee4de 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2908,7 +2908,6 @@ def tmp_init(model, i): with self.assertRaisesRegex(ValueError, ".*Cannot add value "): self.instance = self.model.create_instance() - class TestMisc(PyomoModel): def setUp(self): # From 7fdc2b5db3023c87dc5aa0869e9f43ade21e1007 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 14:15:28 -0600 Subject: [PATCH 1950/3044] Add comprehensive test suite --- .../tests/test_parameterized_quadratic.py | 1288 +++++++++++++++++ 1 file changed, 1288 insertions(+) create mode 100644 pyomo/repn/tests/test_parameterized_quadratic.py diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py new file mode 100644 index 00000000000..9480070d69b --- /dev/null +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -0,0 +1,1288 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +from math import isnan +import unittest + +from pyomo.core.expr import SumExpression, MonomialTermExpression +from pyomo.core.expr.compare import assertExpressionsEqual +from pyomo.environ import Any, ConcreteModel, log, Param, Var +from pyomo.repn.parameterized_quadratic import ParameterizedQuadraticRepnVisitor +from pyomo.repn.tests.test_linear import VisitorConfig +from pyomo.repn.util import InvalidNumber + + +def build_test_model(): + m = ConcreteModel() + m.x = Var() + m.y = Var() + m.z = Var() + m.p = Param(initialize=1, mutable=True) + + return m + + +class TestParameterizedQuadratic(unittest.TestCase): + def test_constant_literal(self): + """ + Ensure ParameterizedQuadraticRepnVisitor(*args, wrt=[]) works + like QuadraticRepnVisitor. + """ + expr = 2 + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {}) + self.assertEqual(cfg.var_order, {}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 2) + self.assertEqual(repn.linear, {}) + self.assertIsNone(repn.quadratic) + self.assertIsNone(repn.nonlinear) + self.assertEqual(repn.to_expression(visitor), 2) + + def test_constant_param(self): + m = build_test_model() + m.p.set_value(2) + expr = 2 + m.p + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {}) + self.assertEqual(cfg.var_order, {}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, 4) + self.assertEqual(repn.linear, {}) + self.assertIsNone(repn.quadratic) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual(self, repn.to_expression(visitor), 4) + + def test_binary_sum_identical_terms(self): + m = build_test_model() + expr = m.x + m.x + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.y, m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {id(m.x): 2}) + self.assertIsNone(repn.quadratic) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual(self, repn.to_expression(visitor), 2 * m.x) + + def test_binary_sum_identical_terms_wrt_x(self): + m = build_test_model() + expr = m.x + m.x + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.x]) + # note: covers walker_exitNode for case where + # constant is a fixed expression + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {}) + self.assertEqual(cfg.var_order, {}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, m.x + m.x) + self.assertEqual(repn.linear, {}) + self.assertIsNone(repn.quadratic) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual(self, repn.to_expression(visitor), m.x + m.x) + + def test_binary_sum_nonidentical_terms(self): + m = build_test_model() + expr = m.x + m.y + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {id(m.x): 1, id(m.y): 1}) + self.assertIsNone(repn.quadratic) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual(self, repn.to_expression(visitor), m.x + m.y) + + def test_binary_sum_nonidentical_terms_wrt_x(self): + m = build_test_model() + expr = m.x + m.y + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.x]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.y): 0}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, m.x) + self.assertEqual(repn.linear, {id(m.y): 1}) + self.assertIsNone(repn.quadratic) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual(self, repn.to_expression(visitor), m.y + m.x) + + def test_ternary_sum_with_product(self): + m = build_test_model() + e = m.x + m.z * m.y + m.z + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + repn = visitor.walk_expression(e) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.z): m.z, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.z): 1, id(m.y): 2}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(len(repn.linear), 2) + self.assertEqual(repn.linear[id(m.x)], 1) + self.assertEqual(repn.linear[id(m.z)], 1) + self.assertEqual(len(repn.quadratic), 1) + self.assertEqual(repn.quadratic[(id(m.z), id(m.y))], 1) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, repn.to_expression(visitor), m.z * m.y + (m.x + m.z) + ) + + def test_ternary_sum_with_product_wrt_z(self): + m = build_test_model() + e = m.x + m.z * m.y + m.z + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.z]) + repn = visitor.walk_expression(e) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertIs(repn.constant, m.z) + self.assertEqual(len(repn.linear), 2) + self.assertEqual(repn.linear[id(m.x)], 1) + self.assertIs(repn.linear[id(m.y)], m.z) + self.assertIsNone(repn.quadratic) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual(self, repn.to_expression(visitor), m.x + m.z * m.y + m.z) + + def test_nonlinear_wrt_x(self): + m = build_test_model() + expr = log(m.x) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.x]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {}) + self.assertEqual(cfg.var_order, {}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, log(m.x)) + self.assertEqual(repn.linear, {}) + self.assertIsNone(repn.quadratic) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual(self, repn.to_expression(visitor), log(m.x)) + + def test_linear_constant_coeffs(self): + m = build_test_model() + e = 2 + 3 * m.x + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor.expand_nonlinear_products = True + repn = visitor.walk_expression(e) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 2) + self.assertEqual(repn.linear, {id(m.x): 3}) + self.assertIsNone(repn.quadratic) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual(self, repn.to_expression(visitor), 3 * m.x + 2) + + def test_linear_constant_coeffs_wrt_x(self): + m = build_test_model() + e = 2 + 3 * m.x + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.x]) + visitor.expand_nonlinear_products = True + repn = visitor.walk_expression(e) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {}) + self.assertEqual(cfg.var_order, {}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, 2 + 3 * m.x) + self.assertEqual(repn.linear, {}) + self.assertIsNone(repn.quadratic) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual(self, repn.to_expression(visitor), 2 + 3 * m.x) + + def test_quadratic(self): + m = build_test_model() + e = 2 + 3 * m.x + 4 * m.x**2 + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor.expand_nonlinear_products = True + repn = visitor.walk_expression(e) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 2) + self.assertEqual(repn.linear, {id(m.x): 3}) + self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 4}) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, repn.to_expression(visitor), 4 * m.x ** 2 + 3 * m.x + 2 + ) + + def test_product_quadratic_quadratic(self): + m = build_test_model() + e = (2 + 3 * m.x + 4 * m.x**2) * (5 + 6 * m.x + 7 * m.x**2) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor.expand_nonlinear_products = True + repn = visitor.walk_expression(e) + + QE4 = SumExpression([4 * m.x**2]) + QE7 = SumExpression([7 * m.x**2]) + LE3 = MonomialTermExpression((3, m.x)) + LE6 = MonomialTermExpression((6, m.x)) + NL = +QE4 * (QE7 + LE6) + (LE3) * (QE7) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 10) + self.assertEqual(repn.linear, {id(m.x): 27}) + self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 52}) + assertExpressionsEqual(self, repn.nonlinear, NL) + assertExpressionsEqual( + self, repn.to_expression(visitor), NL + 52 * m.x ** 2 + 27 * m.x + 10 + ) + + def test_product_quadratic_quadratic_2(self): + m = build_test_model() + e = (2 + 3 * m.x + 4 * m.x**2) * (5 + 6 * m.x + 7 * m.x**2) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor.expand_nonlinear_products = False + repn = visitor.walk_expression(e) + + NL = (4 * m.x**2 + 3 * m.x + 2) * (7 * m.x**2 + 6 * m.x + 5) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {}) + self.assertIsNone(repn.quadratic) + assertExpressionsEqual(self, repn.nonlinear, NL) + assertExpressionsEqual(self, repn.to_expression(visitor), NL) + + def test_product_linear_linear(self): + m = build_test_model() + e = (1 + 2 * m.x + 3 * m.y) * (4 + 5 * m.x + 6 * m.y) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + repn = visitor.walk_expression(e) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 4) + self.assertEqual(repn.linear, {id(m.x): 13, id(m.y): 18}) + self.assertEqual( + repn.quadratic, + {(id(m.x), id(m.x)): 10, (id(m.y), id(m.y)): 18, (id(m.x), id(m.y)): 27}, + ) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + ( + 10 * m.x ** 2 + 27 * (m.x * m.y) + 18 * m.y ** 2 + + (13 * m.x + 18 * m.y) + 4 + ), + ) + + def test_product_linear_linear_wrt_y(self): + m = build_test_model() + e = (1 + 2 * m.x + 3 * m.y) * (4 + 5 * m.x + 6 * m.y) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.y, m.z]) + repn = visitor.walk_expression(e) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual( + self, repn.constant, (1 + 3 * m.y) * (4 + 6 * m.y) + ) + self.assertEqual(len(repn.linear), 1) + assertExpressionsEqual( + self, repn.linear[id(m.x)], (4 + 6 * m.y) * 2 + (1 + 3 * m.y) * 5 + ) + self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 10}) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + ( + 10 * m.x ** 2 + + ((4 + 6 * m.y) * 2 + (1 + 3 * m.y) * 5) * m.x + + (1 + 3 * m.y) * (4 + 6 * m.y) + ) + ) + + def test_product_linear_linear_const_0(self): + m = build_test_model() + expr = (0 + 3 * m.x + 4 * m.y) * (5 + 3 * m.x + 7 * m.y) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {id(m.x): 15, id(m.y): 20}) + self.assertEqual( + repn.quadratic, + {(id(m.x), id(m.x)): 9, (id(m.x), id(m.y)): 33, (id(m.y), id(m.y)): 28}, + ) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + 9 * m.x ** 2 + 33 * (m.x * m.y) + 28 * m.y ** 2 + (15 * m.x + 20 * m.y) + ) + + def test_product_linear_quadratic(self): + m = build_test_model() + expr = (5 + 3 * m.x + 7 * m.y) * (1 + 3 * m.x + 4 * m.y + 8 * m.y * m.x) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 5) + self.assertEqual(repn.linear, {id(m.x): 18, id(m.y): 27}) + self.assertEqual( + repn.quadratic, + {(id(m.x), id(m.y)): 73, (id(m.x), id(m.x)): 9, (id(m.y), id(m.y)): 28}, + ) + assertExpressionsEqual( + self, + repn.nonlinear, + (3 * m.x + 7 * m.y) * SumExpression([8 * (m.x * m.y)]) + ) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + ( + 73 * (m.x * m.y) + 9 * m.x ** 2 + 28 * m.y ** 2 + + (3 * m.x + 7 * m.y) * SumExpression([8 * (m.x * m.y)]) + + (18 * m.x + 27 * m.y) + + 5 + ), + ) + + def test_product_linear_quadratic_wrt_x(self): + m = build_test_model() + expr = (0 + 3 * m.x + 4 * m.y + 8 * m.y * m.x) * (5 + 3 * m.x + 7 * m.y) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.x]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.y): 0}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, 3 * m.x * (5 + 3 * m.x)) + self.assertEqual(len(repn.linear), 1) + assertExpressionsEqual( + self, + repn.linear[id(m.y)], + (5 + 3 * m.x) * (4 + 8 * m.x) + 21 * m.x, + ) + self.assertEqual(len(repn.quadratic), 1) + assertExpressionsEqual( + self, + repn.quadratic[id(m.y), id(m.y)], + (4 + 8 * m.x) * 7, + ) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + (4 + 8 * m.x) * 7 * m.y ** 2 + + ((5 + 3 * m.x) * (4 + 8 * m.x) + 21 * m.x) * m.y + + 3 * m.x * (5 + 3 * m.x) + ) + + def test_product_nonlinear_var_expand_false(self): + m = build_test_model() + e = (m.x + m.y + log(m.x)) * m.x + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor.expand_nonlinear_products = False + repn = visitor.walk_expression(e) + + NL = (log(m.x) + (m.x + m.y)) * m.x + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {}) + self.assertIsNone(repn.quadratic) + assertExpressionsEqual(self, repn.nonlinear, NL) + assertExpressionsEqual(self, repn.to_expression(visitor), NL) + + def test_product_nonlinear_var_expand_true(self): + m = build_test_model() + e = (m.x + m.y + log(m.x)) * m.x + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor.expand_nonlinear_products = True + repn = visitor.walk_expression(e) + + NL = log(m.x) * m.x + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 1, (id(m.x), id(m.y)): 1}) + assertExpressionsEqual(self, repn.nonlinear, NL) + + def test_product_nonlinear_var_2_expand_false(self): + m = build_test_model() + e = m.x * (m.x + m.y + log(m.x) + 2) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor.expand_nonlinear_products = False + repn = visitor.walk_expression(e) + + NL = m.x * (log(m.x) + (m.x + m.y) + 2) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {}) + self.assertIsNone(repn.quadratic) + assertExpressionsEqual(self, repn.nonlinear, NL) + assertExpressionsEqual(self, repn.to_expression(visitor), NL) + + def test_product_nonlinear_var_2_expand_true(self): + m = build_test_model() + e = m.x * (m.x + m.y + log(m.x) + 2) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor.expand_nonlinear_products = True + repn = visitor.walk_expression(e) + + NL = m.x * log(m.x) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {id(m.x): 2}) + self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 1, (id(m.x), id(m.y)): 1}) + assertExpressionsEqual(self, repn.nonlinear, NL) + assertExpressionsEqual( + self, repn.to_expression(visitor), m.x ** 2 + m.x * m.y + NL + 2 * m.x + ) + + def test_zero_elimination(self): + m = ConcreteModel() + m.x = Var(range(4)) + e = 0 * m.x[0] + 0 * m.x[1] * m.x[2] + 0 * log(m.x[3]) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + repn = visitor.walk_expression(e) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual( + cfg.var_map, + { + id(m.x[0]): m.x[0], + id(m.x[1]): m.x[1], + id(m.x[2]): m.x[2], + id(m.x[3]): m.x[3], + }, + ) + self.assertEqual( + cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1, id(m.x[2]): 2, id(m.x[3]): 3} + ) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {}) + self.assertIsNone(repn.quadratic) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual(self, repn.to_expression(visitor), 0) + + def test_uninitialized_param_expansion(self): + m = ConcreteModel() + m.x = Var(range(4)) + m.p = Param(mutable=True, within=Any, initialize=None) + e = m.p * m.x[0] + m.p * m.x[1] * m.x[2] + m.p * log(m.x[3]) + + cfg = VisitorConfig() + repn = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]).walk_expression(e) + self.assertEqual(cfg.subexpr, {}) + self.assertEqual( + cfg.var_map, + { + id(m.x[0]): m.x[0], + id(m.x[1]): m.x[1], + id(m.x[2]): m.x[2], + id(m.x[3]): m.x[3], + }, + ) + self.assertEqual( + cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1, id(m.x[2]): 2, id(m.x[3]): 3} + ) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {id(m.x[0]): InvalidNumber(None)}) + self.assertEqual( + repn.quadratic, {(id(m.x[1]), id(m.x[2])): InvalidNumber(None)} + ) + self.assertEqual(repn.nonlinear, InvalidNumber(None)) + + def test_zero_times_var(self): + m = build_test_model() + e = 0 * m.x + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) + repn = visitor.walk_expression(e) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {}) + self.assertIsNone(repn.quadratic) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual(self, repn.to_expression(visitor), 0) + + def test_square_linear(self): + m = build_test_model() + expr = (1 + 3 * m.x + 4 * m.y) ** 2 + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 1) + self.assertEqual(repn.linear, {id(m.x): 6, id(m.y): 8}) + self.assertEqual( + repn.quadratic, + {(id(m.x), id(m.x)): 9, (id(m.y), id(m.y)): 16, (id(m.x), id(m.y)): 24}, + ) + self.assertEqual(repn.nonlinear, None) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + 9 * m.x ** 2 + 24 * (m.x * m.y) + 16 * m.y ** 2 + (6 * m.x + 8 * m.y) + 1 + ) + + def test_square_linear_wrt_y(self): + m = build_test_model() + expr = (1 + 3 * m.x + 4 * m.y) ** 2 + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.y, m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, (1 + 4 * m.y) * (1 + 4 * m.y)) + self.assertEqual(len(repn.linear), 1) + assertExpressionsEqual( + self, repn.linear[id(m.x)], (1 + 4 * m.y) * 3 + (1 + 4 * m.y) * 3 + ) + self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 9}) + self.assertEqual(repn.nonlinear, None) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + ( + 9 * m.x ** 2 + ((1 + 4 * m.y) * 3 + (1 + 4 * m.y) * 3) * m.x + + ((1 + 4 * m.y) * (1 + 4 * m.y)) + ), + ) + + def test_square_linear_float(self): + m = build_test_model() + expr = (1 + 3 * m.x + 4 * m.y) ** 2.0 + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 1) + self.assertEqual(repn.linear, {id(m.x): 6, id(m.y): 8}) + self.assertEqual( + repn.quadratic, + {(id(m.x), id(m.x)): 9, (id(m.y), id(m.y)): 16, (id(m.x), id(m.y)): 24}, + ) + self.assertEqual(repn.nonlinear, None) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + 9 * m.x ** 2 + 24 * (m.x * m.y) + 16 * m.y ** 2 + (6 * m.x + 8 * m.y) + 1 + ) + + def test_division_quadratic_nonlinear(self): + m = build_test_model() + expr = (1 + 3 * m.x + 4 * log(m.x) * m.y + 4 * m.y ** 2) / (2 * m.x) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertIsNone(repn.quadratic) + assertExpressionsEqual( + self, + repn.nonlinear, + (4 * m.y ** 2 + 4 * (log(m.x) * m.y) + 3 * m.x + 1) / (2 * m.x), + ) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + repn.nonlinear, + ) + + def test_division_quadratic_nonlinear_wrt_x(self): + m = build_test_model() + expr = (1 + 3 * m.x + 4 * log(m.x) * m.y + 4 * m.y ** 2) / (2 * m.x) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.x]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.y): 0}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, (1 + 3 * m.x) * (1 / (2 * m.x))) + self.assertEqual(len(repn.linear), 1) + assertExpressionsEqual( + self, repn.linear[id(m.y)], (1 / (2 * m.x)) * (4 * log(m.x)) + ) + self.assertEqual(len(repn.quadratic), 1) + assertExpressionsEqual( + self, + repn.quadratic[id(m.y), id(m.y)], + (1 / (2 * m.x)) * 4, + ) + self.assertEqual(repn.nonlinear, None) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + ((1 / (2 * m.x)) * 4) * m.y ** 2 + + ((1 / (2 * m.x)) * (4 * log(m.x))) * m.y + + (1 + 3 * m.x) * (1 / (2 * m.x)) + ) + + def test_constant_expr_multiplier(self): + m = build_test_model() + expr = 5 * (2 * m.x + m.x ** 2) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {id(m.x): 10}) + self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 5}) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, repn.to_expression(visitor), 5 * m.x ** 2 + 10 * m.x + ) + + def test_0_mult_nan_linear_coeff(self): + m = build_test_model() + expr = 0 * (float("nan") * m.x + m.y + log(m.x) + m.y * m.x ** 2 + 2 * m.x) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, 0 * m.y) + self.assertEqual(len(repn.linear), 1) + assertExpressionsEqual(self, repn.linear[id(m.x)], float("nan")) + self.assertEqual(len(repn.quadratic), 1) + assertExpressionsEqual(self, repn.quadratic[id(m.x), id(m.x)], 0 * m.y) + assertExpressionsEqual(self, repn.nonlinear, (log(m.x)) * 0) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + 0 * m.y * m.x ** 2 + (log(m.x)) * 0 + float("nan") * m.x + 0 * m.y + ) + + def test_0_mult_nan_quadratic_coeff(self): + m = build_test_model() + expr = 0 * (m.x + m.y + log(m.x) + float("nan") * m.x ** 2 + 2 * m.x) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, 0 * m.y) + self.assertEqual(repn.linear, {id(m.x): 0}) + self.assertEqual(len(repn.quadratic), 1) + assertExpressionsEqual(self, repn.quadratic[id(m.x), id(m.x)], float("nan")) + assertExpressionsEqual(self, repn.nonlinear, (log(m.x)) * 0) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + float("nan") * m.x ** 2 + (log(m.x)) * 0 + 0 * m.y + ) + + def test_square_quadratic(self): + m = build_test_model() + expr = (1 + m.x + m.y + m.x ** 2 + m.x * m.y) ** 2.0 + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) + repn = visitor.walk_expression(expr) + + NL = ( + (m.x ** 2 + m.x * m.y) * (m.x ** 2 + m.x * m.y + (m.x + m.y)) + + (m.x + m.y) * (m.x ** 2 + m.x * m.y) + ) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 1) + self.assertEqual(repn.linear, {id(m.x): 2, id(m.y): 2}) + self.assertEqual( + repn.quadratic, + { + (id(m.x), id(m.x)): 3, + (id(m.x), id(m.y)): 4, + (id(m.y), id(m.y)): 1, + }, + ) + assertExpressionsEqual(self, repn.nonlinear, NL) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + NL + 3 * m.x ** 2 + 4 * (m.x * m.y) + m.y ** 2 + (2 * m.x + 2 * m.y) + 1 + ) + + def test_square_quadratic_wrt_y(self): + m = build_test_model() + expr = (1 + m.x + m.y + m.x ** 2 + m.x * m.y) ** 2.0 + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + repn = visitor.walk_expression(expr) + + NL = ( + SumExpression([m.x ** 2]) * (m.x ** 2 + (1 + m.y) * m.x) + + ((1 + m.y) * m.x) * SumExpression([m.x ** 2]) + ) + QC = 1 + m.y + 1 + m.y + (1 + m.y) * (1 + m.y) + LC = (1 + m.y) * (1 + m.y) + (1 + m.y) * (1 + m.y) + CON = (1 + m.y) * (1 + m.y) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, (1 + m.y) * (1 + m.y)) + self.assertEqual(len(repn.linear), 1) + assertExpressionsEqual( + self, + repn.linear[id(m.x)], + (1 + m.y) * (1 + m.y) + (1 + m.y) * (1 + m.y), + ) + self.assertEqual(len(repn.quadratic), 1) + assertExpressionsEqual( + self, + repn.quadratic[id(m.x), id(m.x)], + 1 + m.y + 1 + m.y + (1 + m.y) * (1 + m.y), + ) + assertExpressionsEqual(self, repn.nonlinear, NL) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + NL + QC * m.x ** 2 + LC * m.x + CON, + ) + + def test_cube_linear(self): + m = build_test_model() + expr = (1 + m.x + m.y) ** 3 + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {}) + self.assertIsNone(repn.quadratic) + # cubic expansion not supported + assertExpressionsEqual(self, repn.nonlinear, (m.x + m.y + 1) ** 3) + assertExpressionsEqual(self, repn.to_expression(visitor), (m.x + m.y + 1) ** 3) + + def test_nonlinear_product_with_constant_terms(self): + m = build_test_model() + # test product of nonlinear expressions where one + # multiplicand has constant of value 1 + expr = (1 + log(m.x)) * (log(m.x) + m.y ** 2) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.quadratic, {(id(m.y), id(m.y)): 1}) + assertExpressionsEqual( + self, + repn.nonlinear, + log(m.x) * (m.y ** 2 + log(m.x)) + log(m.x), + ) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + log(m.x) * (m.y ** 2 + log(m.x)) + log(m.x) + m.y ** 2, + ) + + def test_finalize_simplify_coefficients(self): + m = build_test_model() + expr = m.x + m.p * m.x ** 2 + 2 * m.y ** 2 - m.x - m.p * m.x ** 2 - m.p * m.z + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.z): m.z}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.z): 1}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, 2 * m.y ** 2) + self.assertEqual(repn.linear, {id(m.z): -1}) + self.assertEqual(repn.quadratic, {}) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + -1 * m.z + 2 * m.y ** 2, + ) + + def test_factor_multiplier_simplify_coefficients(self): + m = build_test_model() + expr = 2 * (m.x + m.x ** 2 + 2 * m.y ** 2 - m.x - m.x ** 2 - m.p * m.z) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + # this tests case where there are zeros in the `linear` + # and `quadratic` dicts of the unfinalized repn + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.z): m.z}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.z): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertIsNone(repn.nonlinear) + self.assertEqual(repn.quadratic, {}) + self.assertEqual(repn.linear, {id(m.z): -2}) + assertExpressionsEqual(self, repn.constant, (2 * m.y ** 2) * 2) + assertExpressionsEqual( + self, repn.to_expression(visitor), -2 * m.z + (2 * m.y ** 2) * 2 + ) + + def test_sum_nonlinear_custom_multiplier(self): + m = build_test_model() + expr = 2 * (1 + log(m.x)) + (2 * (m.y + m.y ** 2 + log(m.x))) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual( + self, + repn.constant, + 2 + 2 * (m.y + m.y ** 2), + ) + self.assertEqual(repn.linear, {}) + self.assertIsNone(repn.quadratic) + assertExpressionsEqual( + self, + repn.nonlinear, + 2 * log(m.x) + 2 * log(m.x), + ) + + def test_negation_linear(self): + m = build_test_model() + expr = - (2 + 3 * m.x + 5 * m.x * m.y) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, -2) + self.assertEqual(len(repn.linear), 1) + assertExpressionsEqual(self, repn.linear[id(m.x)], -1 * (3 + 5 * m.y)) + self.assertIsNone(repn.quadratic) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, repn.to_expression(visitor), -1 * (3 + 5 * m.y) * m.x - 2 + ) + + def test_negation_nonlinear_wrt_y_fix_z(self): + m = build_test_model() + m.z.fix(2) + expr = - ( + 2 + 3 * m.x + 4 * m.y * m.z + 5 * m.x ** 2 * m.y + + 6 * m.x * (m.z - 2) + m.z ** 2 + + m.z * log(m.x) + ) + + cfg = VisitorConfig() + # note: variable fixing takes precedence over inclusion in + # the `wrt` list; that is tested here + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.y, m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, (2 + 8 * m.y + 4) * -1) + self.assertEqual(repn.linear, {id(m.x): -3}) + self.assertEqual(len(repn.quadratic), 1) + assertExpressionsEqual(self, repn.quadratic[(id(m.x), id(m.x))], -5 * m.y) + assertExpressionsEqual(self, repn.nonlinear, 2 * log(m.x) * -1) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + + (-5 * m.y) * (m.x ** 2) + + 2 * log(m.x) * -1 + + (-3) * m.x + + (2 + 8 * m.y + 4) * (-1) + ) + + def test_negation_product_linear_linear(self): + m = build_test_model() + expr = -(1 + 2 * m.x + 3 * m.y) * (4 + 5 * m.x + 6 * m.y * 7 * m.z) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.y, m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual( + self, repn.constant, (1 + 3 * m.y) * (4 + 42 * m.y * m.z) * (-1) + ) + self.assertEqual(len(repn.linear), 1) + assertExpressionsEqual( + self, + repn.linear[id(m.x)], + (-1) * ((4 + 42 * m.y * m.z) * 2 + (1 + 3 * m.y) * 5), + ) + self.assertEqual(len(repn.quadratic), 1) + assertExpressionsEqual( + self, repn.quadratic[id(m.x), id(m.x)], -10, + ) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + ( + -10 * m.x ** 2 + + (-1) * ((4 + 42 * m.y * m.z) * 2 + (1 + 3 * m.y) * 5) * m.x + + (1 + 3 * m.y) * (4 + 42 * m.y * m.z) * (-1) + ), + ) + + def test_sum_bilinear_terms_commute_product(self): + m = build_test_model() + expr = m.x * m.y + m.y * m.x + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.quadratic, {(id(m.x), id(m.y)): 2}) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, repn.to_expression(visitor), SumExpression([2 * (m.x * m.y)]) + ) + + def test_sum_nonlinear(self): + m = build_test_model() + expr = (1 + log(m.x)) + (m.x + m.y + m.y ** 2 + log(m.x)) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + # tests special case of `repn.append` where multiplier + # is 1 and both summands have a nonlinear term + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, 1 + m.y + m.y ** 2) + self.assertEqual(repn.linear, {id(m.x): 1}) + self.assertIsNone(repn.quadratic) + assertExpressionsEqual(self, repn.nonlinear, log(m.x) + log(m.x)) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + log(m.x) + log(m.x) + m.x + (1 + m.y) + m.y ** 2, + ) + + def test_product_linear_linear_0_nan(self): + m = build_test_model() + m.p.set_value(0) + expr = (m.p + 0 * m.x) * (float("nan") + float("nan") * m.x) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertTrue(isnan(repn.constant)) + self.assertEqual(len(repn.linear), 1) + self.assertTrue(isnan(repn.linear[id(m.x)])) + self.assertIsNone(repn.quadratic) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + float("nan") * m.x + float("nan"), + ) + + def test_product_quadratic_quadratic_nan_0(self): + m = build_test_model() + m.p.set_value(0) + expr = ( + (float("nan") + float("nan") * m.x + float("nan") * m.x ** 2) + * (m.p + 0 * m.x + 0 * m.x ** 2) + ) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertTrue(isnan(repn.constant)) + self.assertEqual(len(repn.linear), 1) + self.assertTrue(isnan(repn.linear[id(m.x)])) + self.assertEqual(len(repn.quadratic), 1) + self.assertTrue(isnan(repn.quadratic[id(m.x), id(m.x)])) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + float("nan") * m.x ** 2 + float("nan") * m.x + float("nan"), + ) + + def test_product_quadratic_quadratic_0_nan(self): + m = build_test_model() + m.p.set_value(0) + expr = ( + (m.p + 0 * m.x + 0 * m.x ** 2) + * (float("nan") + float("nan") * m.x + float("nan") * m.x ** 2) + ) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertTrue(isnan(repn.constant)) + self.assertEqual(len(repn.linear), 1) + self.assertTrue(isnan(repn.linear[id(m.x)])) + self.assertEqual(len(repn.quadratic), 1) + self.assertTrue(isnan(repn.quadratic[id(m.x), id(m.x)])) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + float("nan") * m.x ** 2 + float("nan") * m.x + float("nan"), + ) + + def test_nary_sum_products(self): + m = build_test_model() + expr = ( + m.x ** 2 * (m.z - 1) + + m.x * (m.y ** 4 + 0.8) + - 5 * m.x * m.y * m.z + + m.x * (m.y + 2) + ) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(len(repn.linear), 1) + assertExpressionsEqual( + self, + repn.linear[id(m.x)], + m.y ** 4 + 0.8 + 5 * m.y * m.z * (-1) + (m.y + 2), + ) + assertExpressionsEqual(self, repn.quadratic[id(m.x), id(m.x)], m.z - 1) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + (m.z - 1) * m.x ** 2 + + (m.y ** 4 + 0.8 + 5 * m.y * m.z * (-1) + (m.y + 2)) * m.x + ) + + def test_repr_parameterized_quadratic_repn(self): + m = build_test_model() + expr = 2 + m.x + m.x ** 2 + log(m.x) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + repn = visitor.walk_expression(expr) + + linear_dict = {id(m.x): 1} + quad_dict = {(id(m.x), id(m.x)): 1} + expected_repn_str = ( + "ParameterizedQuadraticRepn(" + "mult=1, " + "const=2, " + f"linear={linear_dict}, " + f"quadratic={quad_dict}, " + "nonlinear=log(x))" + ) + self.assertEqual(repr(repn), expected_repn_str) + self.assertEqual(str(repn), expected_repn_str) From bee1f566b6641c5f28b8799d65cf6385ede5f8c2 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 14:16:17 -0600 Subject: [PATCH 1951/3044] Apply black to new testing module --- .../tests/test_parameterized_quadratic.py | 214 +++++++----------- 1 file changed, 85 insertions(+), 129 deletions(-) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index 9480070d69b..50d76771172 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -263,7 +263,7 @@ def test_quadratic(self): self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 4}) self.assertIsNone(repn.nonlinear) assertExpressionsEqual( - self, repn.to_expression(visitor), 4 * m.x ** 2 + 3 * m.x + 2 + self, repn.to_expression(visitor), 4 * m.x**2 + 3 * m.x + 2 ) def test_product_quadratic_quadratic(self): @@ -290,7 +290,7 @@ def test_product_quadratic_quadratic(self): self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 52}) assertExpressionsEqual(self, repn.nonlinear, NL) assertExpressionsEqual( - self, repn.to_expression(visitor), NL + 52 * m.x ** 2 + 27 * m.x + 10 + self, repn.to_expression(visitor), NL + 52 * m.x**2 + 27 * m.x + 10 ) def test_product_quadratic_quadratic_2(self): @@ -336,10 +336,7 @@ def test_product_linear_linear(self): assertExpressionsEqual( self, repn.to_expression(visitor), - ( - 10 * m.x ** 2 + 27 * (m.x * m.y) + 18 * m.y ** 2 - + (13 * m.x + 18 * m.y) + 4 - ), + (10 * m.x**2 + 27 * (m.x * m.y) + 18 * m.y**2 + (13 * m.x + 18 * m.y) + 4), ) def test_product_linear_linear_wrt_y(self): @@ -354,9 +351,7 @@ def test_product_linear_linear_wrt_y(self): self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) self.assertEqual(repn.multiplier, 1) - assertExpressionsEqual( - self, repn.constant, (1 + 3 * m.y) * (4 + 6 * m.y) - ) + assertExpressionsEqual(self, repn.constant, (1 + 3 * m.y) * (4 + 6 * m.y)) self.assertEqual(len(repn.linear), 1) assertExpressionsEqual( self, repn.linear[id(m.x)], (4 + 6 * m.y) * 2 + (1 + 3 * m.y) * 5 @@ -367,10 +362,10 @@ def test_product_linear_linear_wrt_y(self): self, repn.to_expression(visitor), ( - 10 * m.x ** 2 + 10 * m.x**2 + ((4 + 6 * m.y) * 2 + (1 + 3 * m.y) * 5) * m.x + (1 + 3 * m.y) * (4 + 6 * m.y) - ) + ), ) def test_product_linear_linear_const_0(self): @@ -395,7 +390,7 @@ def test_product_linear_linear_const_0(self): assertExpressionsEqual( self, repn.to_expression(visitor), - 9 * m.x ** 2 + 33 * (m.x * m.y) + 28 * m.y ** 2 + (15 * m.x + 20 * m.y) + 9 * m.x**2 + 33 * (m.x * m.y) + 28 * m.y**2 + (15 * m.x + 20 * m.y), ) def test_product_linear_quadratic(self): @@ -417,15 +412,15 @@ def test_product_linear_quadratic(self): {(id(m.x), id(m.y)): 73, (id(m.x), id(m.x)): 9, (id(m.y), id(m.y)): 28}, ) assertExpressionsEqual( - self, - repn.nonlinear, - (3 * m.x + 7 * m.y) * SumExpression([8 * (m.x * m.y)]) + self, repn.nonlinear, (3 * m.x + 7 * m.y) * SumExpression([8 * (m.x * m.y)]) ) assertExpressionsEqual( self, repn.to_expression(visitor), ( - 73 * (m.x * m.y) + 9 * m.x ** 2 + 28 * m.y ** 2 + 73 * (m.x * m.y) + + 9 * m.x**2 + + 28 * m.y**2 + (3 * m.x + 7 * m.y) * SumExpression([8 * (m.x * m.y)]) + (18 * m.x + 27 * m.y) + 5 @@ -447,23 +442,19 @@ def test_product_linear_quadratic_wrt_x(self): assertExpressionsEqual(self, repn.constant, 3 * m.x * (5 + 3 * m.x)) self.assertEqual(len(repn.linear), 1) assertExpressionsEqual( - self, - repn.linear[id(m.y)], - (5 + 3 * m.x) * (4 + 8 * m.x) + 21 * m.x, + self, repn.linear[id(m.y)], (5 + 3 * m.x) * (4 + 8 * m.x) + 21 * m.x ) self.assertEqual(len(repn.quadratic), 1) assertExpressionsEqual( - self, - repn.quadratic[id(m.y), id(m.y)], - (4 + 8 * m.x) * 7, + self, repn.quadratic[id(m.y), id(m.y)], (4 + 8 * m.x) * 7 ) self.assertIsNone(repn.nonlinear) assertExpressionsEqual( self, repn.to_expression(visitor), - (4 + 8 * m.x) * 7 * m.y ** 2 + (4 + 8 * m.x) * 7 * m.y**2 + ((5 + 3 * m.x) * (4 + 8 * m.x) + 21 * m.x) * m.y - + 3 * m.x * (5 + 3 * m.x) + + 3 * m.x * (5 + 3 * m.x), ) def test_product_nonlinear_var_expand_false(self): @@ -548,7 +539,7 @@ def test_product_nonlinear_var_2_expand_true(self): self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 1, (id(m.x), id(m.y)): 1}) assertExpressionsEqual(self, repn.nonlinear, NL) assertExpressionsEqual( - self, repn.to_expression(visitor), m.x ** 2 + m.x * m.y + NL + 2 * m.x + self, repn.to_expression(visitor), m.x**2 + m.x * m.y + NL + 2 * m.x ) def test_zero_elimination(self): @@ -649,7 +640,7 @@ def test_square_linear(self): assertExpressionsEqual( self, repn.to_expression(visitor), - 9 * m.x ** 2 + 24 * (m.x * m.y) + 16 * m.y ** 2 + (6 * m.x + 8 * m.y) + 1 + 9 * m.x**2 + 24 * (m.x * m.y) + 16 * m.y**2 + (6 * m.x + 8 * m.y) + 1, ) def test_square_linear_wrt_y(self): @@ -675,7 +666,8 @@ def test_square_linear_wrt_y(self): self, repn.to_expression(visitor), ( - 9 * m.x ** 2 + ((1 + 4 * m.y) * 3 + (1 + 4 * m.y) * 3) * m.x + 9 * m.x**2 + + ((1 + 4 * m.y) * 3 + (1 + 4 * m.y) * 3) * m.x + ((1 + 4 * m.y) * (1 + 4 * m.y)) ), ) @@ -702,12 +694,12 @@ def test_square_linear_float(self): assertExpressionsEqual( self, repn.to_expression(visitor), - 9 * m.x ** 2 + 24 * (m.x * m.y) + 16 * m.y ** 2 + (6 * m.x + 8 * m.y) + 1 + 9 * m.x**2 + 24 * (m.x * m.y) + 16 * m.y**2 + (6 * m.x + 8 * m.y) + 1, ) def test_division_quadratic_nonlinear(self): m = build_test_model() - expr = (1 + 3 * m.x + 4 * log(m.x) * m.y + 4 * m.y ** 2) / (2 * m.x) + expr = (1 + 3 * m.x + 4 * log(m.x) * m.y + 4 * m.y**2) / (2 * m.x) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) @@ -722,17 +714,13 @@ def test_division_quadratic_nonlinear(self): assertExpressionsEqual( self, repn.nonlinear, - (4 * m.y ** 2 + 4 * (log(m.x) * m.y) + 3 * m.x + 1) / (2 * m.x), - ) - assertExpressionsEqual( - self, - repn.to_expression(visitor), - repn.nonlinear, + (4 * m.y**2 + 4 * (log(m.x) * m.y) + 3 * m.x + 1) / (2 * m.x), ) + assertExpressionsEqual(self, repn.to_expression(visitor), repn.nonlinear) def test_division_quadratic_nonlinear_wrt_x(self): m = build_test_model() - expr = (1 + 3 * m.x + 4 * log(m.x) * m.y + 4 * m.y ** 2) / (2 * m.x) + expr = (1 + 3 * m.x + 4 * log(m.x) * m.y + 4 * m.y**2) / (2 * m.x) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.x]) @@ -749,22 +737,20 @@ def test_division_quadratic_nonlinear_wrt_x(self): ) self.assertEqual(len(repn.quadratic), 1) assertExpressionsEqual( - self, - repn.quadratic[id(m.y), id(m.y)], - (1 / (2 * m.x)) * 4, + self, repn.quadratic[id(m.y), id(m.y)], (1 / (2 * m.x)) * 4 ) self.assertEqual(repn.nonlinear, None) assertExpressionsEqual( self, repn.to_expression(visitor), - ((1 / (2 * m.x)) * 4) * m.y ** 2 + ((1 / (2 * m.x)) * 4) * m.y**2 + ((1 / (2 * m.x)) * (4 * log(m.x))) * m.y - + (1 + 3 * m.x) * (1 / (2 * m.x)) + + (1 + 3 * m.x) * (1 / (2 * m.x)), ) def test_constant_expr_multiplier(self): m = build_test_model() - expr = 5 * (2 * m.x + m.x ** 2) + expr = 5 * (2 * m.x + m.x**2) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) @@ -778,13 +764,11 @@ def test_constant_expr_multiplier(self): self.assertEqual(repn.linear, {id(m.x): 10}) self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 5}) self.assertIsNone(repn.nonlinear) - assertExpressionsEqual( - self, repn.to_expression(visitor), 5 * m.x ** 2 + 10 * m.x - ) + assertExpressionsEqual(self, repn.to_expression(visitor), 5 * m.x**2 + 10 * m.x) def test_0_mult_nan_linear_coeff(self): m = build_test_model() - expr = 0 * (float("nan") * m.x + m.y + log(m.x) + m.y * m.x ** 2 + 2 * m.x) + expr = 0 * (float("nan") * m.x + m.y + log(m.x) + m.y * m.x**2 + 2 * m.x) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) @@ -803,12 +787,12 @@ def test_0_mult_nan_linear_coeff(self): assertExpressionsEqual( self, repn.to_expression(visitor), - 0 * m.y * m.x ** 2 + (log(m.x)) * 0 + float("nan") * m.x + 0 * m.y + 0 * m.y * m.x**2 + (log(m.x)) * 0 + float("nan") * m.x + 0 * m.y, ) def test_0_mult_nan_quadratic_coeff(self): m = build_test_model() - expr = 0 * (m.x + m.y + log(m.x) + float("nan") * m.x ** 2 + 2 * m.x) + expr = 0 * (m.x + m.y + log(m.x) + float("nan") * m.x**2 + 2 * m.x) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) @@ -826,21 +810,20 @@ def test_0_mult_nan_quadratic_coeff(self): assertExpressionsEqual( self, repn.to_expression(visitor), - float("nan") * m.x ** 2 + (log(m.x)) * 0 + 0 * m.y + float("nan") * m.x**2 + (log(m.x)) * 0 + 0 * m.y, ) def test_square_quadratic(self): m = build_test_model() - expr = (1 + m.x + m.y + m.x ** 2 + m.x * m.y) ** 2.0 + expr = (1 + m.x + m.y + m.x**2 + m.x * m.y) ** 2.0 cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) repn = visitor.walk_expression(expr) - NL = ( - (m.x ** 2 + m.x * m.y) * (m.x ** 2 + m.x * m.y + (m.x + m.y)) - + (m.x + m.y) * (m.x ** 2 + m.x * m.y) - ) + NL = (m.x**2 + m.x * m.y) * (m.x**2 + m.x * m.y + (m.x + m.y)) + ( + m.x + m.y + ) * (m.x**2 + m.x * m.y) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) @@ -850,31 +833,26 @@ def test_square_quadratic(self): self.assertEqual(repn.linear, {id(m.x): 2, id(m.y): 2}) self.assertEqual( repn.quadratic, - { - (id(m.x), id(m.x)): 3, - (id(m.x), id(m.y)): 4, - (id(m.y), id(m.y)): 1, - }, + {(id(m.x), id(m.x)): 3, (id(m.x), id(m.y)): 4, (id(m.y), id(m.y)): 1}, ) assertExpressionsEqual(self, repn.nonlinear, NL) assertExpressionsEqual( self, repn.to_expression(visitor), - NL + 3 * m.x ** 2 + 4 * (m.x * m.y) + m.y ** 2 + (2 * m.x + 2 * m.y) + 1 + NL + 3 * m.x**2 + 4 * (m.x * m.y) + m.y**2 + (2 * m.x + 2 * m.y) + 1, ) def test_square_quadratic_wrt_y(self): m = build_test_model() - expr = (1 + m.x + m.y + m.x ** 2 + m.x * m.y) ** 2.0 + expr = (1 + m.x + m.y + m.x**2 + m.x * m.y) ** 2.0 cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) repn = visitor.walk_expression(expr) - NL = ( - SumExpression([m.x ** 2]) * (m.x ** 2 + (1 + m.y) * m.x) - + ((1 + m.y) * m.x) * SumExpression([m.x ** 2]) - ) + NL = SumExpression([m.x**2]) * (m.x**2 + (1 + m.y) * m.x) + ( + (1 + m.y) * m.x + ) * SumExpression([m.x**2]) QC = 1 + m.y + 1 + m.y + (1 + m.y) * (1 + m.y) LC = (1 + m.y) * (1 + m.y) + (1 + m.y) * (1 + m.y) CON = (1 + m.y) * (1 + m.y) @@ -886,9 +864,7 @@ def test_square_quadratic_wrt_y(self): assertExpressionsEqual(self, repn.constant, (1 + m.y) * (1 + m.y)) self.assertEqual(len(repn.linear), 1) assertExpressionsEqual( - self, - repn.linear[id(m.x)], - (1 + m.y) * (1 + m.y) + (1 + m.y) * (1 + m.y), + self, repn.linear[id(m.x)], (1 + m.y) * (1 + m.y) + (1 + m.y) * (1 + m.y) ) self.assertEqual(len(repn.quadratic), 1) assertExpressionsEqual( @@ -898,9 +874,7 @@ def test_square_quadratic_wrt_y(self): ) assertExpressionsEqual(self, repn.nonlinear, NL) assertExpressionsEqual( - self, - repn.to_expression(visitor), - NL + QC * m.x ** 2 + LC * m.x + CON, + self, repn.to_expression(visitor), NL + QC * m.x**2 + LC * m.x + CON ) def test_cube_linear(self): @@ -926,7 +900,7 @@ def test_nonlinear_product_with_constant_terms(self): m = build_test_model() # test product of nonlinear expressions where one # multiplicand has constant of value 1 - expr = (1 + log(m.x)) * (log(m.x) + m.y ** 2) + expr = (1 + log(m.x)) * (log(m.x) + m.y**2) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.z]) @@ -940,19 +914,17 @@ def test_nonlinear_product_with_constant_terms(self): self.assertEqual(repn.linear, {}) self.assertEqual(repn.quadratic, {(id(m.y), id(m.y)): 1}) assertExpressionsEqual( - self, - repn.nonlinear, - log(m.x) * (m.y ** 2 + log(m.x)) + log(m.x), + self, repn.nonlinear, log(m.x) * (m.y**2 + log(m.x)) + log(m.x) ) assertExpressionsEqual( self, repn.to_expression(visitor), - log(m.x) * (m.y ** 2 + log(m.x)) + log(m.x) + m.y ** 2, + log(m.x) * (m.y**2 + log(m.x)) + log(m.x) + m.y**2, ) def test_finalize_simplify_coefficients(self): m = build_test_model() - expr = m.x + m.p * m.x ** 2 + 2 * m.y ** 2 - m.x - m.p * m.x ** 2 - m.p * m.z + expr = m.x + m.p * m.x**2 + 2 * m.y**2 - m.x - m.p * m.x**2 - m.p * m.z cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) @@ -962,19 +934,15 @@ def test_finalize_simplify_coefficients(self): self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.z): m.z}) self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.z): 1}) self.assertEqual(repn.multiplier, 1) - assertExpressionsEqual(self, repn.constant, 2 * m.y ** 2) + assertExpressionsEqual(self, repn.constant, 2 * m.y**2) self.assertEqual(repn.linear, {id(m.z): -1}) self.assertEqual(repn.quadratic, {}) self.assertIsNone(repn.nonlinear) - assertExpressionsEqual( - self, - repn.to_expression(visitor), - -1 * m.z + 2 * m.y ** 2, - ) + assertExpressionsEqual(self, repn.to_expression(visitor), -1 * m.z + 2 * m.y**2) def test_factor_multiplier_simplify_coefficients(self): m = build_test_model() - expr = 2 * (m.x + m.x ** 2 + 2 * m.y ** 2 - m.x - m.x ** 2 - m.p * m.z) + expr = 2 * (m.x + m.x**2 + 2 * m.y**2 - m.x - m.x**2 - m.p * m.z) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) @@ -989,14 +957,14 @@ def test_factor_multiplier_simplify_coefficients(self): self.assertIsNone(repn.nonlinear) self.assertEqual(repn.quadratic, {}) self.assertEqual(repn.linear, {id(m.z): -2}) - assertExpressionsEqual(self, repn.constant, (2 * m.y ** 2) * 2) + assertExpressionsEqual(self, repn.constant, (2 * m.y**2) * 2) assertExpressionsEqual( - self, repn.to_expression(visitor), -2 * m.z + (2 * m.y ** 2) * 2 + self, repn.to_expression(visitor), -2 * m.z + (2 * m.y**2) * 2 ) def test_sum_nonlinear_custom_multiplier(self): m = build_test_model() - expr = 2 * (1 + log(m.x)) + (2 * (m.y + m.y ** 2 + log(m.x))) + expr = 2 * (1 + log(m.x)) + (2 * (m.y + m.y**2 + log(m.x))) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) @@ -1006,22 +974,14 @@ def test_sum_nonlinear_custom_multiplier(self): self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) self.assertEqual(repn.multiplier, 1) - assertExpressionsEqual( - self, - repn.constant, - 2 + 2 * (m.y + m.y ** 2), - ) + assertExpressionsEqual(self, repn.constant, 2 + 2 * (m.y + m.y**2)) self.assertEqual(repn.linear, {}) self.assertIsNone(repn.quadratic) - assertExpressionsEqual( - self, - repn.nonlinear, - 2 * log(m.x) + 2 * log(m.x), - ) + assertExpressionsEqual(self, repn.nonlinear, 2 * log(m.x) + 2 * log(m.x)) def test_negation_linear(self): m = build_test_model() - expr = - (2 + 3 * m.x + 5 * m.x * m.y) + expr = -(2 + 3 * m.x + 5 * m.x * m.y) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) @@ -1043,9 +1003,13 @@ def test_negation_linear(self): def test_negation_nonlinear_wrt_y_fix_z(self): m = build_test_model() m.z.fix(2) - expr = - ( - 2 + 3 * m.x + 4 * m.y * m.z + 5 * m.x ** 2 * m.y - + 6 * m.x * (m.z - 2) + m.z ** 2 + expr = -( + 2 + + 3 * m.x + + 4 * m.y * m.z + + 5 * m.x**2 * m.y + + 6 * m.x * (m.z - 2) + + m.z**2 + m.z * log(m.x) ) @@ -1067,10 +1031,10 @@ def test_negation_nonlinear_wrt_y_fix_z(self): assertExpressionsEqual( self, repn.to_expression(visitor), - + (-5 * m.y) * (m.x ** 2) + +(-5 * m.y) * (m.x**2) + 2 * log(m.x) * -1 + (-3) * m.x - + (2 + 8 * m.y + 4) * (-1) + + (2 + 8 * m.y + 4) * (-1), ) def test_negation_product_linear_linear(self): @@ -1095,15 +1059,13 @@ def test_negation_product_linear_linear(self): (-1) * ((4 + 42 * m.y * m.z) * 2 + (1 + 3 * m.y) * 5), ) self.assertEqual(len(repn.quadratic), 1) - assertExpressionsEqual( - self, repn.quadratic[id(m.x), id(m.x)], -10, - ) + assertExpressionsEqual(self, repn.quadratic[id(m.x), id(m.x)], -10) self.assertIsNone(repn.nonlinear) assertExpressionsEqual( self, repn.to_expression(visitor), ( - -10 * m.x ** 2 + -10 * m.x**2 + (-1) * ((4 + 42 * m.y * m.z) * 2 + (1 + 3 * m.y) * 5) * m.x + (1 + 3 * m.y) * (4 + 42 * m.y * m.z) * (-1) ), @@ -1131,7 +1093,7 @@ def test_sum_bilinear_terms_commute_product(self): def test_sum_nonlinear(self): m = build_test_model() - expr = (1 + log(m.x)) + (m.x + m.y + m.y ** 2 + log(m.x)) + expr = (1 + log(m.x)) + (m.x + m.y + m.y**2 + log(m.x)) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) @@ -1143,14 +1105,14 @@ def test_sum_nonlinear(self): self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) self.assertEqual(repn.multiplier, 1) - assertExpressionsEqual(self, repn.constant, 1 + m.y + m.y ** 2) + assertExpressionsEqual(self, repn.constant, 1 + m.y + m.y**2) self.assertEqual(repn.linear, {id(m.x): 1}) self.assertIsNone(repn.quadratic) assertExpressionsEqual(self, repn.nonlinear, log(m.x) + log(m.x)) assertExpressionsEqual( self, repn.to_expression(visitor), - log(m.x) + log(m.x) + m.x + (1 + m.y) + m.y ** 2, + log(m.x) + log(m.x) + m.x + (1 + m.y) + m.y**2, ) def test_product_linear_linear_0_nan(self): @@ -1172,17 +1134,14 @@ def test_product_linear_linear_0_nan(self): self.assertIsNone(repn.quadratic) self.assertIsNone(repn.nonlinear) assertExpressionsEqual( - self, - repn.to_expression(visitor), - float("nan") * m.x + float("nan"), + self, repn.to_expression(visitor), float("nan") * m.x + float("nan") ) def test_product_quadratic_quadratic_nan_0(self): m = build_test_model() m.p.set_value(0) - expr = ( - (float("nan") + float("nan") * m.x + float("nan") * m.x ** 2) - * (m.p + 0 * m.x + 0 * m.x ** 2) + expr = (float("nan") + float("nan") * m.x + float("nan") * m.x**2) * ( + m.p + 0 * m.x + 0 * m.x**2 ) cfg = VisitorConfig() @@ -1202,15 +1161,14 @@ def test_product_quadratic_quadratic_nan_0(self): assertExpressionsEqual( self, repn.to_expression(visitor), - float("nan") * m.x ** 2 + float("nan") * m.x + float("nan"), + float("nan") * m.x**2 + float("nan") * m.x + float("nan"), ) def test_product_quadratic_quadratic_0_nan(self): m = build_test_model() m.p.set_value(0) - expr = ( - (m.p + 0 * m.x + 0 * m.x ** 2) - * (float("nan") + float("nan") * m.x + float("nan") * m.x ** 2) + expr = (m.p + 0 * m.x + 0 * m.x**2) * ( + float("nan") + float("nan") * m.x + float("nan") * m.x**2 ) cfg = VisitorConfig() @@ -1230,14 +1188,14 @@ def test_product_quadratic_quadratic_0_nan(self): assertExpressionsEqual( self, repn.to_expression(visitor), - float("nan") * m.x ** 2 + float("nan") * m.x + float("nan"), + float("nan") * m.x**2 + float("nan") * m.x + float("nan"), ) def test_nary_sum_products(self): m = build_test_model() expr = ( - m.x ** 2 * (m.z - 1) - + m.x * (m.y ** 4 + 0.8) + m.x**2 * (m.z - 1) + + m.x * (m.y**4 + 0.8) - 5 * m.x * m.y * m.z + m.x * (m.y + 2) ) @@ -1253,22 +1211,20 @@ def test_nary_sum_products(self): self.assertEqual(repn.constant, 0) self.assertEqual(len(repn.linear), 1) assertExpressionsEqual( - self, - repn.linear[id(m.x)], - m.y ** 4 + 0.8 + 5 * m.y * m.z * (-1) + (m.y + 2), + self, repn.linear[id(m.x)], m.y**4 + 0.8 + 5 * m.y * m.z * (-1) + (m.y + 2) ) assertExpressionsEqual(self, repn.quadratic[id(m.x), id(m.x)], m.z - 1) self.assertIsNone(repn.nonlinear) assertExpressionsEqual( self, repn.to_expression(visitor), - (m.z - 1) * m.x ** 2 - + (m.y ** 4 + 0.8 + 5 * m.y * m.z * (-1) + (m.y + 2)) * m.x + (m.z - 1) * m.x**2 + + (m.y**4 + 0.8 + 5 * m.y * m.z * (-1) + (m.y + 2)) * m.x, ) def test_repr_parameterized_quadratic_repn(self): m = build_test_model() - expr = 2 + m.x + m.x ** 2 + log(m.x) + expr = 2 + m.x + m.x**2 + log(m.x) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) From cf7af78b147dafb805ea4d19390847113e5d3e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Wed, 17 Jul 2024 22:42:14 +0200 Subject: [PATCH 1952/3044] Applied black yet again. --- pyomo/core/tests/unit/test_sets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index d8f0a7ee4de..bd168c7c279 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2908,6 +2908,7 @@ def tmp_init(model, i): with self.assertRaisesRegex(ValueError, ".*Cannot add value "): self.instance = self.model.create_instance() + class TestMisc(PyomoModel): def setUp(self): # From 330cac8e13c5768784f1d134a6eb3b55ef63f787 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 14:48:40 -0600 Subject: [PATCH 1953/3044] Fix comments --- pyomo/repn/parameterized_quadratic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 2053b45bb74..dcd6a9e5364 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -281,16 +281,16 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): x1.constant = 0 x1_lin = x1.linear x1.linear = {} - # [CB] + [CC] + [CD] + # [C1B2] + [C1C2] + [C1D2] if x1.quadratic: ans.nonlinear += x1.to_expression(visitor) * x2.to_expression(visitor) x1.quadratic = None x2.linear = {} - # [BC] + [BD] + # [B1C2] + [B1D2] if x1_lin and (x2.nonlinear is not None or x2.quadratic): x1.linear = x1_lin ans.nonlinear += x1.to_expression(visitor) * x2.to_expression(visitor) - # [AD] + # [A1D2] if not is_zero(x1_c) and x2.nonlinear is not None: # TODO: what if nonlinear contains nan? ans.nonlinear += x1_c * x2.nonlinear From 21df3d48a43ac802e55f7f88fd27455ffbfa22d3 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 15:11:02 -0600 Subject: [PATCH 1954/3044] Test ternary product of linear expressions --- .../tests/test_parameterized_quadratic.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index 50d76771172..d96cfe112c5 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1222,6 +1222,38 @@ def test_nary_sum_products(self): + (m.y**4 + 0.8 + 5 * m.y * m.z * (-1) + (m.y + 2)) * m.x, ) + def test_ternary_product_linear(self): + m = build_test_model() + expr = (1 + 2 * m.x) * (3 + 4 * m.y) * (5 + 6 * m.z) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.z): m.z}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.z): 1}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, 5 * (3 + 4 * m.y)) + self.assertEqual(len(repn.linear), 2) + assertExpressionsEqual(self, repn.linear[id(m.x)], (3 + 4 * m.y) * 10) + assertExpressionsEqual(self, repn.linear[id(m.z)], (3 + 4 * m.y) * 6) + self.assertEqual(len(repn.quadratic), 1) + assertExpressionsEqual( + self, repn.quadratic[id(m.x), id(m.z)], (3 + 4 * m.y) * 12 + ) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + ( + (3 + 4 * m.y) * 12 * (m.x * m.z) + + (3 + 4 * m.y) * 10 * m.x + + (3 + 4 * m.y) * 6 * m.z + + 5 * (3 + 4 * m.y) + ), + ) + def test_repr_parameterized_quadratic_repn(self): m = build_test_model() expr = 2 + m.x + m.x**2 + log(m.x) From d82b9f373e8ca6f305bcd43f7c8f34154c15ed91 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 15:15:39 -0600 Subject: [PATCH 1955/3044] Add test for noninteger power of linear expression --- .../tests/test_parameterized_quadratic.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index d96cfe112c5..ebe3e21d502 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1254,6 +1254,26 @@ def test_ternary_product_linear(self): ), ) + def test_noninteger_pow_linear(self): + m = build_test_model() + expr = (1 + 2 * m.x + 3 * m.y) ** 1.5 + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {}) + self.assertIsNone(repn.quadratic) + assertExpressionsEqual(self, repn.nonlinear, (1 + 3 * m.y + 2 * m.x) ** 1.5) + assertExpressionsEqual( + self, repn.to_expression(visitor), (1 + 3 * m.y + 2 * m.x) ** 1.5 + ) + def test_repr_parameterized_quadratic_repn(self): m = build_test_model() expr = 2 + m.x + m.x**2 + log(m.x) From e4fca5776bf8a8cbaefe1876c00245f71b0e8b23 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 15:24:07 -0600 Subject: [PATCH 1956/3044] Test linear expression raised to fixed integer power --- .../tests/test_parameterized_quadratic.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index ebe3e21d502..edc1d7729a2 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1274,6 +1274,56 @@ def test_noninteger_pow_linear(self): self, repn.to_expression(visitor), (1 + 3 * m.y + 2 * m.x) ** 1.5 ) + def test_variable_pow_linear(self): + m = build_test_model() + expr = (1 + 2 * m.x + 3 * m.y) ** (m.y) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {}) + self.assertIsNone(repn.quadratic) + assertExpressionsEqual(self, repn.nonlinear, (1 + 3 * m.y + 2 * m.x) ** m.y) + assertExpressionsEqual( + self, repn.to_expression(visitor), (1 + 3 * m.y + 2 * m.x) ** m.y + ) + + def test_pow_integer_fixed_var(self): + m = build_test_model() + m.z.fix(2) + expr = (1 + 2 * m.x + 3 * m.y) ** (m.z) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, (1 + 3 * m.y) * (1 + 3 * m.y)) + self.assertEqual(len(repn.linear), 1) + assertExpressionsEqual( + self, repn.linear[id(m.x)], (1 + 3 * m.y) * 2 + (1 + 3 * m.y) * 2 + ) + self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 4}) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + ( + 4 * m.x ** 2 + + ((1 + 3 * m.y) * 2 + (1 + 3 * m.y) * 2) * m.x + + (1 + 3 * m.y) * (1 + 3 * m.y) + ) + ) + def test_repr_parameterized_quadratic_repn(self): m = build_test_model() expr = 2 + m.x + m.x**2 + log(m.x) From 7cca746a8884bd91f53a42c5782224d36f501d5e Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 15:28:01 -0600 Subject: [PATCH 1957/3044] Apply black --- pyomo/repn/tests/test_parameterized_quadratic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index edc1d7729a2..b0173416cd5 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1318,10 +1318,10 @@ def test_pow_integer_fixed_var(self): self, repn.to_expression(visitor), ( - 4 * m.x ** 2 + 4 * m.x**2 + ((1 + 3 * m.y) * 2 + (1 + 3 * m.y) * 2) * m.x + (1 + 3 * m.y) * (1 + 3 * m.y) - ) + ), ) def test_repr_parameterized_quadratic_repn(self): From ada07b088ac512c2b2601b059229767f58264fde Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 17:37:30 -0600 Subject: [PATCH 1958/3044] Complete test of nonlinear sum --- pyomo/repn/tests/test_parameterized_quadratic.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index b0173416cd5..2f89720dfe6 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -978,6 +978,11 @@ def test_sum_nonlinear_custom_multiplier(self): self.assertEqual(repn.linear, {}) self.assertIsNone(repn.quadratic) assertExpressionsEqual(self, repn.nonlinear, 2 * log(m.x) + 2 * log(m.x)) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + 2 * log(m.x) + 2 * log(m.x) + 2 + 2 * (m.y + m.y ** 2) + ) def test_negation_linear(self): m = build_test_model() From dbbb0f109e7592dafb5de9a7020d47b1d93f0baf Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 17:40:22 -0600 Subject: [PATCH 1959/3044] Apply black --- pyomo/repn/tests/test_parameterized_quadratic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index 2f89720dfe6..0fedd1b3260 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -981,7 +981,7 @@ def test_sum_nonlinear_custom_multiplier(self): assertExpressionsEqual( self, repn.to_expression(visitor), - 2 * log(m.x) + 2 * log(m.x) + 2 + 2 * (m.y + m.y ** 2) + 2 * log(m.x) + 2 * log(m.x) + 2 + 2 * (m.y + m.y**2), ) def test_negation_linear(self): From 3eee189135a20ff03d26c08d57cc6c4110ce61b2 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 17:53:20 -0600 Subject: [PATCH 1960/3044] Test simple expanded square monomial --- .../tests/test_parameterized_quadratic.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index 0fedd1b3260..42fc58b5b6c 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1076,6 +1076,28 @@ def test_negation_product_linear_linear(self): ), ) + def test_expanded_monomial_square_term(self): + m = build_test_model() + expr = m.x * m.x * m.p + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.z]) + # ensure overcomplication issues with standard repn + # are not repeated by quadratic repn + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + self.assertEqual(repn.constant, 0) + self.assertEqual(repn.linear, {}) + self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 1}) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, repn.to_expression(visitor), SumExpression([m.x ** 2]) + ) + def test_sum_bilinear_terms_commute_product(self): m = build_test_model() expr = m.x * m.y + m.y * m.x From 416287b39b6da4c6830042eb0d6219f250abe68d Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 17:53:51 -0600 Subject: [PATCH 1961/3044] Blacken new monomial square test --- pyomo/repn/tests/test_parameterized_quadratic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index 42fc58b5b6c..a9e1aebd9b2 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1095,7 +1095,7 @@ def test_expanded_monomial_square_term(self): self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 1}) self.assertIsNone(repn.nonlinear) assertExpressionsEqual( - self, repn.to_expression(visitor), SumExpression([m.x ** 2]) + self, repn.to_expression(visitor), SumExpression([m.x**2]) ) def test_sum_bilinear_terms_commute_product(self): From 767006305f97062a360a69aba3f71686a0ae9ae2 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 18 Jul 2024 09:20:43 -0600 Subject: [PATCH 1962/3044] Added test for rescale FIM function --- pyomo/contrib/doe/tests/test_doe_solve.py | 67 ++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 78abb857717..bf24815fe50 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -7,9 +7,10 @@ from pyomo.contrib.doe.tests.experiment_class_example import * from pyomo.contrib.doe import * - +from pyomo.contrib.doe.utils import * import pyomo.common.unittest as unittest +import pyomo.environ as pyo from pyomo.opt import SolverFactory @@ -344,6 +345,70 @@ def test_reactor_grid_search(self): set(T_vals).issuperset(set([300, 500, 700])) ) + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_rescale_FIM(self): + fd_method = "central" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + # With parameter scaling + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + # Without parameter scaling + doe_obj2 = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=False, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + # Run both problems + doe_obj.run_doe() + doe_obj2.run_doe() + + # Extract FIM values + FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) + FIM2, Q2, L2, sigma_inv2 = get_FIM_Q_L(doe_obj=doe_obj2) + + # Get rescaled FIM from the scaled version + param_vals = np.array([[v for k, v in doe_obj.model.scenario_blocks[0].unknown_parameters.items()], ]) + + resc_FIM = rescale_FIM(FIM, param_vals) + + # Compare scaled and rescaled values + assert np.all(np.isclose(FIM2, resc_FIM)) + if __name__ == "__main__": unittest.main() From 7ad1005381baac066315cd4083e96681f598df1f Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 18 Jul 2024 09:29:56 -0600 Subject: [PATCH 1963/3044] Fixed bug and added test for compute FIM seq with back/forw --- pyomo/contrib/doe/doe.py | 12 +++-- pyomo/contrib/doe/tests/test_doe_solve.py | 60 ++++++++++++++++++++++- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 01f4edd8603..476d1fffb71 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -482,8 +482,6 @@ def _sequential_FIM(self, model=None): # In a loop..... # Calculate measurement values for each scenario for s in model.scenarios: - param = model.parameter_scenarios[s] - # Perturbation to be (1 + diff) * param_value if self.fd_formula == FiniteDifferenceStep.central: diff = self.step * ( @@ -502,8 +500,14 @@ def _sequential_FIM(self, model=None): diff = 0 pass - # Update parameter values for the given finite difference scenario - param.set_value(model.unknown_parameters[param] * (1 + diff)) + # If we are doing forward/backward, no change for s=0 + skip_param_update = (self.fd_formula in [FiniteDifferenceStep.forward, FiniteDifferenceStep.backward]) and (s == 0) + if not skip_param_update: + param = model.parameter_scenarios[s] + # Update parameter values for the given finite difference scenario + param.set_value(model.unknown_parameters[param] * (1 + diff)) + else: + continue # Simulate the model self.solver.solve(model) diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index bf24815fe50..6667418b13b 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -271,7 +271,7 @@ def test_reactor_obj_cholesky_solve(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_compute_FIM_seq(self): + def test_compute_FIM_seq_centr(self): fd_method = "central" obj_used = "det" @@ -298,6 +298,64 @@ def test_compute_FIM_seq(self): doe_obj.compute_FIM(method="sequential") + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_compute_FIM_seq_forward(self): + fd_method = "forward" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.compute_FIM(method="sequential") + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_compute_FIM_seq_backward(self): + fd_method = "backward" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.compute_FIM(method="sequential") + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") From 2b645c70793eb71a2fb1946173babad70c357c3f Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 18 Jul 2024 10:14:23 -0600 Subject: [PATCH 1964/3044] Adding test for model that is poorly posed --- pyomo/contrib/doe/doe.py | 20 +++-- .../tests/experiment_class_example_flags.py | 13 +++ pyomo/contrib/doe/tests/test_doe_solve.py | 84 +++++++++++++++++++ 3 files changed, 112 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 476d1fffb71..5a25c065dfb 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -510,7 +510,13 @@ def _sequential_FIM(self, model=None): continue # Simulate the model - self.solver.solve(model) + try: + res = self.solver.solve(model) + assert (res.solver.termination_condition == "optimal") + except: + raise RuntimeError( + "Model from experiment did not solve appropriately. Make sure the model is well-posed." + ) # Extract the measurement values for the scenario and append measurement_vals.append( @@ -1014,7 +1020,8 @@ def _generate_scenario_blocks(self, model=None): comp.fix() try: - self.solver.solve(model.base_model, tee=self.tee) + res = self.solver.solve(model.base_model, tee=self.tee) + assert (res.solver.termination_condition == "optimal") self.logger.info("Model from experiment solved.") except: raise RuntimeError( @@ -1479,7 +1486,10 @@ def compute_FIM_full_factorial(self, design_ranges=None, method="sequential"): self.logger.warning("failed count:", failures) self._computed_FIM = np.zeros(self.prior_FIM.shape) - iter_timer.tic(msg=None) + + iter_t = iter_timer.toc(msg=None) + time_set.append(iter_t) + FIM = self._computed_FIM @@ -1494,9 +1504,9 @@ def compute_FIM_full_factorial(self, design_ranges=None, method="sequential"): "Eigenvalue has imaginary component greater than 1e-6, contact developers if this issue persists." ) - # If the real value is less than or equal to zero, set the E_opt value to np.NaN + # If the real value is less than or equal to zero, set the E_opt value to nan if E_vals.real[E_ind] <= 0: - E_opt = np.NaN + E_opt = np.nan else: E_opt = np.log10(E_vals.real[E_ind]) diff --git a/pyomo/contrib/doe/tests/experiment_class_example_flags.py b/pyomo/contrib/doe/tests/experiment_class_example_flags.py index 0869dee1570..4d060328d8f 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example_flags.py +++ b/pyomo/contrib/doe/tests/experiment_class_example_flags.py @@ -245,3 +245,16 @@ def label_experiment(self, flag=0): return self.label_experiment_impl( [[m.t_control], [m.t_control], [m.t_control]], flag=flag ) + +class FullReactorExperimentBad(ReactorExperiment): + def label_experiment(self, flag=0): + m = self.model + + self.label_experiment_impl( + [[m.t_control], [m.t_control], [m.t_control]], flag=flag + ) + + m.bad_con_1 = pyo.Constraint(expr=m.CA[0] >= 1.0) + m.bad_con_2 = pyo.Constraint(expr=m.CA[0] <= 0.0) + + return m \ No newline at end of file diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 6667418b13b..132df056822 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -6,6 +6,7 @@ ) from pyomo.contrib.doe.tests.experiment_class_example import * +from pyomo.contrib.doe.tests.experiment_class_example_flags import FullReactorExperimentBad from pyomo.contrib.doe import * from pyomo.contrib.doe.utils import * @@ -16,6 +17,8 @@ from pathlib import Path +import logging + ipopt_available = SolverFactory("ipopt").available() DATA_DIR = Path(__file__).parent @@ -467,6 +470,87 @@ def test_rescale_FIM(self): # Compare scaled and rescaled values assert np.all(np.isclose(FIM2, resc_FIM)) + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_solve_bad_model(self): + fd_method = "central" + obj_used = "det" + + experiment = FullReactorExperimentBad(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + RuntimeError, + "Model from experiment did not solve appropriately. Make sure the model is well-posed.", + ): + doe_obj.run_doe() + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not pandas_available, "pandas is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_grid_search_bad_model(self): + fd_method = "central" + obj_used = "det" + + experiment = FullReactorExperimentBad(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + logger_level=logging.ERROR, + ) + + design_ranges = {"CA[0]": [1, 5, 3], "T[0]": [300, 700, 3]} + + doe_obj.compute_FIM_full_factorial( + design_ranges=design_ranges, method="sequential" + ) + + # Check to make sure the lengths of the inputs in results object are indeed correct + CA_vals = doe_obj.fim_factorial_results["CA[0]"] + T_vals = doe_obj.fim_factorial_results["T[0]"] + + # Assert length is correct + assert (len(CA_vals) == 9) and (len(T_vals) == 9) + assert (len(set(CA_vals)) == 3) and (len(set(T_vals)) == 3) + + # Assert unique values are correct + assert (set(CA_vals).issuperset(set([1, 3, 5]))) and ( + set(T_vals).issuperset(set([300, 500, 700])) + ) + if __name__ == "__main__": unittest.main() From f1ae5df9469e1c7fea38eaa9219e0b00607f9224 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 18 Jul 2024 10:15:42 -0600 Subject: [PATCH 1965/3044] Ran Black --- pyomo/contrib/doe/doe.py | 10 ++++++---- .../tests/experiment_class_example_flags.py | 3 ++- pyomo/contrib/doe/tests/test_doe_solve.py | 19 +++++++++++++++---- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 5a25c065dfb..20fac470441 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -501,7 +501,10 @@ def _sequential_FIM(self, model=None): pass # If we are doing forward/backward, no change for s=0 - skip_param_update = (self.fd_formula in [FiniteDifferenceStep.forward, FiniteDifferenceStep.backward]) and (s == 0) + skip_param_update = ( + self.fd_formula + in [FiniteDifferenceStep.forward, FiniteDifferenceStep.backward] + ) and (s == 0) if not skip_param_update: param = model.parameter_scenarios[s] # Update parameter values for the given finite difference scenario @@ -512,7 +515,7 @@ def _sequential_FIM(self, model=None): # Simulate the model try: res = self.solver.solve(model) - assert (res.solver.termination_condition == "optimal") + assert res.solver.termination_condition == "optimal" except: raise RuntimeError( "Model from experiment did not solve appropriately. Make sure the model is well-posed." @@ -1021,7 +1024,7 @@ def _generate_scenario_blocks(self, model=None): try: res = self.solver.solve(model.base_model, tee=self.tee) - assert (res.solver.termination_condition == "optimal") + assert res.solver.termination_condition == "optimal" self.logger.info("Model from experiment solved.") except: raise RuntimeError( @@ -1490,7 +1493,6 @@ def compute_FIM_full_factorial(self, design_ranges=None, method="sequential"): iter_t = iter_timer.toc(msg=None) time_set.append(iter_t) - FIM = self._computed_FIM # Compute and record metrics on FIM diff --git a/pyomo/contrib/doe/tests/experiment_class_example_flags.py b/pyomo/contrib/doe/tests/experiment_class_example_flags.py index 4d060328d8f..2a282023a0e 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example_flags.py +++ b/pyomo/contrib/doe/tests/experiment_class_example_flags.py @@ -246,6 +246,7 @@ def label_experiment(self, flag=0): [[m.t_control], [m.t_control], [m.t_control]], flag=flag ) + class FullReactorExperimentBad(ReactorExperiment): def label_experiment(self, flag=0): m = self.model @@ -257,4 +258,4 @@ def label_experiment(self, flag=0): m.bad_con_1 = pyo.Constraint(expr=m.CA[0] >= 1.0) m.bad_con_2 = pyo.Constraint(expr=m.CA[0] <= 0.0) - return m \ No newline at end of file + return m diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 132df056822..e93e656f19d 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -6,7 +6,9 @@ ) from pyomo.contrib.doe.tests.experiment_class_example import * -from pyomo.contrib.doe.tests.experiment_class_example_flags import FullReactorExperimentBad +from pyomo.contrib.doe.tests.experiment_class_example_flags import ( + FullReactorExperimentBad, +) from pyomo.contrib.doe import * from pyomo.contrib.doe.utils import * @@ -463,7 +465,16 @@ def test_rescale_FIM(self): FIM2, Q2, L2, sigma_inv2 = get_FIM_Q_L(doe_obj=doe_obj2) # Get rescaled FIM from the scaled version - param_vals = np.array([[v for k, v in doe_obj.model.scenario_blocks[0].unknown_parameters.items()], ]) + param_vals = np.array( + [ + [ + v + for k, v in doe_obj.model.scenario_blocks[ + 0 + ].unknown_parameters.items() + ] + ] + ) resc_FIM = rescale_FIM(FIM, param_vals) @@ -498,8 +509,8 @@ def test_reactor_solve_bad_model(self): ) with self.assertRaisesRegex( - RuntimeError, - "Model from experiment did not solve appropriately. Make sure the model is well-posed.", + RuntimeError, + "Model from experiment did not solve appropriately. Make sure the model is well-posed.", ): doe_obj.run_doe() From 858aafcf74db4f598ff053ff03dbf6296ac805ca Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 18 Jul 2024 10:36:00 -0600 Subject: [PATCH 1966/3044] Added tests for update_FIM --- pyomo/contrib/doe/doe.py | 16 +++++---- pyomo/contrib/doe/tests/test_doe_build.py | 41 ++++++++++++++++++++++- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 20fac470441..8ab014b24e5 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -878,7 +878,7 @@ def jacobian_rule(m, n, p): def read_prior(m, i, j): return fim_initial_dict[(i, j)] - model.priorFIM = pyo.Expression( + model.prior_FIM = pyo.Expression( model.parameter_names, model.parameter_names, rule=read_prior ) @@ -914,7 +914,7 @@ def fim_rule(m, p, q): * m.sensitivity_jacobian[n, q] for n in model.output_names ) - + m.priorFIM[p, q] + + m.prior_FIM[p, q] ) model.jacobian_constraint = pyo.Constraint( @@ -976,11 +976,11 @@ def _generate_scenario_blocks(self, model=None): # Check that the user input FIM and Jacobian are the correct dimension if self.prior_FIM is not None: - self.check_model_FIM(self.prior_FIM) + self.check_model_FIM(FIM=self.prior_FIM) else: self.prior_FIM = np.zeros((self.n_parameters, self.n_parameters)) if self.fim_initial is not None: - self.check_model_FIM(self.fim_initial) + self.check_model_FIM(FIM=self.fim_initial) else: self.fim_initial = np.eye(self.n_parameters) + self.prior_FIM if self.jac_initial is not None: @@ -1301,7 +1301,7 @@ def check_model_labels(self, model=None): self.logger.info("Model has expected labels.") # Check the FIM shape against what is expected from the model. - def check_model_FIM(self, FIM=None): + def check_model_FIM(self, model=None, FIM=None): """ Checks if the specified matrix, FIM, matches the shape expected from the model. This method should only be called after the @@ -1312,7 +1312,11 @@ def check_model_FIM(self, FIM=None): Parameters ---------- model: model for suffix checking, Default: None, (self.model) + FIM: FIM value to check on the model """ + if model is None: + model = self.model + if FIM.shape != (self.n_parameters, self.n_parameters): raise ValueError( "Shape of FIM provided should be n parameters by n parameters, or {} by {}, FIM provided has shape {} by {}".format( @@ -1362,7 +1366,7 @@ def update_FIM_prior(self, model=None, FIM=None): "``fim`` is not defined on the model provided. Please build the model first." ) - self.check_model_FIM(model, FIM) + self.check_model_FIM(model=model, FIM=FIM) # Update FIM prior for ind1, p1 in enumerate(model.parameter_names): diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index db009f32742..cfa4304fbe9 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -41,7 +41,7 @@ def get_FIM_FIMPrior_Q_L(doe_obj=None): for j in model.parameter_names ] FIM_prior_vals = [ - pyo.value(model.priorFIM[i, j]) + pyo.value(model.prior_FIM[i, j]) for i in model.parameter_names for j in model.parameter_names ] @@ -453,6 +453,45 @@ def test_reactor_check_user_initialization(self): assert np.array_equal(L_initial, L) assert np.array_equal(JAC_initial, Q) + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_update_FIM(self): + fd_method = "forward" + obj_used = "trace" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + FIM_update = np.ones((4, 4)) * 10 + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.create_doe_model() + + doe_obj.update_FIM_prior(FIM=FIM_update) + + # Grab values to ensure we set the correct piece + FIM, FIM_prior_model, Q, L, sigma = get_FIM_FIMPrior_Q_L(doe_obj) + + # Make sure they match the inputs we gave + assert np.array_equal(FIM_update, FIM_prior_model) + if __name__ == "__main__": unittest.main() From 2555900285896e584b62725651e6357d21b82205 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 18 Jul 2024 10:40:15 -0600 Subject: [PATCH 1967/3044] Add update_FIM error check --- pyomo/contrib/doe/tests/test_doe_errors.py | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 9977bffe94e..4289f2a9112 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -298,6 +298,41 @@ def test_reactor_check_unbuilt_update_FIM(self): ): doe_obj.update_FIM_prior(FIM=FIM_update) + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_check_none_update_FIM(self): + fd_method = "central" + obj_used = "trace" + flag_val = 0 # Value for faulty model build mode - 0: full model + + FIM_update = None + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={"flag": flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + ValueError, + "FIM input for update_FIM_prior must be a 2D, square numpy array.", + ): + doe_obj.update_FIM_prior(FIM=FIM_update) + @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_results_file_name(self): fd_method = "central" From ca32fc5d69be234d58b65a0b9b2061fd5ccc9b52 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 18 Jul 2024 10:52:25 -0600 Subject: [PATCH 1968/3044] Added tests for get values functions without blocks --- pyomo/contrib/doe/tests/test_doe_build.py | 149 ++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index cfa4304fbe9..c5b76f2b1d3 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -492,6 +492,155 @@ def test_update_FIM(self): # Make sure they match the inputs we gave assert np.array_equal(FIM_update, FIM_prior_model) + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_get_experiment_inputs_without_blocks(self): + fd_method = "forward" + obj_used = "trace" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + FIM_update = np.ones((4, 4)) * 10 + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.compute_FIM(method="sequential") + + stuff = doe_obj.get_experiment_input_values(model=doe_obj.compute_FIM_model) + + assert len(stuff) == len( + [k.name for k, v in doe_obj.compute_FIM_model.experiment_inputs.items()] + ) + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_get_experiment_outputs_without_blocks(self): + fd_method = "forward" + obj_used = "trace" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + FIM_update = np.ones((4, 4)) * 10 + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.compute_FIM(method="sequential") + + stuff = doe_obj.get_experiment_output_values(model=doe_obj.compute_FIM_model) + + assert len(stuff) == len( + [k.name for k, v in doe_obj.compute_FIM_model.experiment_outputs.items()] + ) + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_get_measurement_error_without_blocks(self): + fd_method = "forward" + obj_used = "trace" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + FIM_update = np.ones((4, 4)) * 10 + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.compute_FIM(method="sequential") + + stuff = doe_obj.get_measurement_error_values(model=doe_obj.compute_FIM_model) + + assert len(stuff) == len( + [k.name for k, v in doe_obj.compute_FIM_model.measurement_error.items()] + ) + + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_get_unknown_parameters_without_blocks(self): + fd_method = "forward" + obj_used = "trace" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + FIM_update = np.ones((4, 4)) * 10 + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.compute_FIM(method="sequential") + + # Make sure the values can be retrieved + stuff = doe_obj.get_unknown_parameter_values(model=doe_obj.compute_FIM_model) + + assert len(stuff) == len( + [k.name for k, v in doe_obj.compute_FIM_model.unknown_parameters.items()] + ) + if __name__ == "__main__": unittest.main() From 5864e68182b6c51e3d65563712bc3d59a05ce9a9 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 18 Jul 2024 11:04:22 -0600 Subject: [PATCH 1969/3044] Adding tests for multi experiment not implemented and scenario building withoutmodel --- pyomo/contrib/doe/tests/test_doe_build.py | 37 +++++++++--- pyomo/contrib/doe/tests/test_doe_errors.py | 68 ++++++++++++++++++++++ 2 files changed, 97 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index c5b76f2b1d3..4c1541f088c 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -500,8 +500,6 @@ def test_get_experiment_inputs_without_blocks(self): experiment = FullReactorExperiment(data_ex, 10, 3) - FIM_update = np.ones((4, 4)) * 10 - doe_obj = DesignOfExperiments( experiment, fd_formula=fd_method, @@ -537,8 +535,6 @@ def test_get_experiment_outputs_without_blocks(self): experiment = FullReactorExperiment(data_ex, 10, 3) - FIM_update = np.ones((4, 4)) * 10 - doe_obj = DesignOfExperiments( experiment, fd_formula=fd_method, @@ -574,8 +570,6 @@ def test_get_measurement_error_without_blocks(self): experiment = FullReactorExperiment(data_ex, 10, 3) - FIM_update = np.ones((4, 4)) * 10 - doe_obj = DesignOfExperiments( experiment, fd_formula=fd_method, @@ -611,8 +605,6 @@ def test_get_unknown_parameters_without_blocks(self): experiment = FullReactorExperiment(data_ex, 10, 3) - FIM_update = np.ones((4, 4)) * 10 - doe_obj = DesignOfExperiments( experiment, fd_formula=fd_method, @@ -641,6 +633,35 @@ def test_get_unknown_parameters_without_blocks(self): [k.name for k, v in doe_obj.compute_FIM_model.unknown_parameters.items()] ) + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_generate_blocks_without_model(self): + fd_method = "forward" + obj_used = "trace" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj._generate_scenario_blocks() + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 4289f2a9112..639e4c690e4 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -888,6 +888,74 @@ def test_reactor_check_get_meas_error_without_model(self): ): doe_obj.get_measurement_error_values() + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_multiple_exp_not_implemented_seq(self): + fd_method = "central" + obj_used = "trace" + flag_val = ( + 0 # Value for faulty model build mode - 5: Mismatch error and output length + ) + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={"flag": flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + NotImplementedError, "Multiple experiment optimization not yet supported." + ): + doe_obj.run_multi_doe_sequential(N_exp=1) + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_multiple_exp_not_implemented_sim(self): + fd_method = "central" + obj_used = "trace" + flag_val = ( + 0 # Value for faulty model build mode - 5: Mismatch error and output length + ) + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={"flag": flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + NotImplementedError, "Multiple experiment optimization not yet supported." + ): + doe_obj.run_multi_doe_simultaneous(N_exp=1) + if __name__ == "__main__": unittest.main() From 48118832f1e286b3be40c1347807d629c340384e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Jul 2024 11:07:15 -0600 Subject: [PATCH 1970/3044] whoops, fixing typo from last commit --- pyomo/contrib/piecewise/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index e5a0a541657..67596a709e3 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -35,6 +35,7 @@ ) from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( NonlinearToPWL, +) from pyomo.contrib.piecewise.transform.nested_inner_repn import ( NestedInnerRepresentationGDPTransformation, ) From 02cef2d00b68b07be4bfae9090b07745363a6070 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Jul 2024 11:23:55 -0600 Subject: [PATCH 1971/3044] Putting common part of log x tests into helper function for tests --- .../piecewise/tests/test_nonlinear_to_pwl.py | 86 ++++++++++++------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 020edee3924..d0f5acc2ff1 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -37,35 +37,14 @@ def make_model(self): return m - def check_pw_linear_log_x(self, m, points): - x1 = points[0][0] - x2 = points - - def test_log_constraint_uniform_grid(self): - m = self.make_model() - + def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') - n_to_pwl.apply_to( - m, - num_points=3, - domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, - ) - - # cons is transformed - self.assertFalse(m.cons.active) - pwlf = list(m.component_data_objects(PiecewiseLinearFunction, - descend_into=True)) - self.assertEqual(len(pwlf), 1) - pwlf = pwlf[0] - - points = [(1.0009,), (5.5,), (9.9991,)] + points = [(x1,), (x2,), (x3,)] self.assertEqual(pwlf._simplices, [(0, 1), (1, 2)]) self.assertEqual(pwlf._points, points) self.assertEqual(len(pwlf._linear_functions), 2) - x1 = 1.0009 - x2 = 5.5 assertExpressionsStructurallyEqual( self, pwlf._linear_functions[0](m.x), @@ -73,13 +52,11 @@ def test_log_constraint_uniform_grid(self): (log(x2) - ((log(x2) - log(x1))/(x2 - x1))*x2), places=7 ) - x1 = 5.5 - x2 = 9.9991 assertExpressionsStructurallyEqual( self, pwlf._linear_functions[1](m.x), - ((log(x2) - log(x1))/(x2 - x1))*m.x + - (log(x2) - ((log(x2) - log(x1))/(x2 - x1))*x2), + ((log(x3) - log(x2))/(x3 - x2))*m.x + + (log(x3) - ((log(x3) - log(x2))/(x3 - x2))*x3), places=7 ) @@ -90,6 +67,28 @@ def test_log_constraint_uniform_grid(self): self.assertIsNone(new_cons.ub) self.assertEqual(new_cons.lb, 0.35) self.assertIs(n_to_pwl.get_src_component(new_cons), m.cons) + + def test_log_constraint_uniform_grid(self): + m = self.make_model() + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, + num_points=3, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + ) + + # cons is transformed + self.assertFalse(m.cons.active) + + pwlf = list(m.component_data_objects(PiecewiseLinearFunction, + descend_into=True)) + self.assertEqual(len(pwlf), 1) + pwlf = pwlf[0] + + points = [(1.0009,), (5.5,), (9.9991,)] + (x1, x2, x3) = 1.0009, 5.5, 9.9991 + self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) def test_log_constraint_random_grid(self): m = self.make_model() @@ -110,9 +109,34 @@ def test_log_constraint_random_grid(self): descend_into=True)) self.assertEqual(len(pwlf), 1) pwlf = pwlf[0] + + x1 = 4.370861069626263 + x2 = 7.587945476302646 + x3 = 9.556428757689245 + self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) + + + def test_log_constraint_lmt_uniform_sample(self): + m = self.make_model() + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, + num_points=3, + domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_UNIFORM, + ) + + # cons is transformed + self.assertFalse(m.cons.active) + + pwlf = list(m.component_data_objects(PiecewiseLinearFunction, + descend_into=True)) + self.assertEqual(len(pwlf), 1) + pwlf = pwlf[0] + set_trace() - points = [(4.370861069626263,), (7.587945476302646,), (9.556428757689245,)] - self.assertEqual(pwlf._simplices, [(0, 1), (1, 2)]) - self.assertEqual(pwlf._points, points) - self.assertEqual(len(pwlf._linear_functions), 2) + + x1 = 4.370861069626263 + x2 = 7.587945476302646 + x3 = 9.556428757689245 + self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) From 11af0059c9f8a23afa2a3273c47a00a37fba42ae Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 18 Jul 2024 11:28:40 -0600 Subject: [PATCH 1972/3044] Adding tests for bad FD values --- pyomo/contrib/doe/doe.py | 18 ++---- pyomo/contrib/doe/tests/test_doe_errors.py | 73 +++++++++++++++++++++- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 8ab014b24e5..61bac7e6eb4 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -27,7 +27,6 @@ import pyomo.environ as pyo from pyomo.opt import SolverStatus -from pyomo.common import DeveloperError from pyomo.common.timing import TicTocTimer from pyomo.contrib.sensitivity_toolbox.sens import get_dsdp @@ -415,7 +414,6 @@ def compute_FIM(self, model=None, method="sequential"): else: self.check_model_FIM(FIM=self.prior_FIM) - # ToDo: Decide where the FIM should be saved. if method == "sequential": self._sequential_FIM(model=model) self._computed_FIM = self.seq_FIM @@ -471,8 +469,9 @@ def _sequential_FIM(self, model=None): ) model.scenarios = range(len(model.unknown_parameters) + 1) else: - # To-Do: add an error message for this as not being implemented yet - pass + raise AttributeError( + "Finite difference option not recognized. Please contact the developers as you should not see this error." + ) # Fix design variables for comp, _ in model.experiment_inputs.items(): @@ -493,12 +492,6 @@ def _sequential_FIM(self, model=None): ) # Backward always negative perturbation; 0 at s = 0 elif self.fd_formula == FiniteDifferenceStep.forward: diff = self.step * (s != 0) # Forward always positive; 0 at s = 0 - else: - raise DeveloperError( - "Finite difference option not recognized. Please contact the developers as you should not see this error." - ) - diff = 0 - pass # If we are doing forward/backward, no change for s=0 skip_param_update = ( @@ -1012,7 +1005,7 @@ def _generate_scenario_blocks(self, model=None): ) model.scenarios = range(len(model.base_model.unknown_parameters) + 1) else: - raise DeveloperError( + raise AttributeError( "Finite difference option not recognized. Please contact the developers as you should not see this error." ) @@ -1247,8 +1240,7 @@ def det_general(m): # add dummy objective function model.objective = pyo.Objective(expr=0) else: - # something went wrong! - raise DeveloperError( + raise AttributeError( "Objective option not recognized. Please contact the developers as you should not see this error." ) diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 639e4c690e4..15cf6b6373e 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -8,7 +8,6 @@ from pyomo.contrib.doe.tests.experiment_class_example_flags import * from pyomo.contrib.doe import * - import pyomo.common.unittest as unittest from pyomo.opt import SolverFactory @@ -956,6 +955,78 @@ def test_multiple_exp_not_implemented_sim(self): ): doe_obj.run_multi_doe_simultaneous(N_exp=1) + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_bad_FD_generate_scens(self): + fd_method = "central" + obj_used = "trace" + flag_val = ( + 0 # Value for faulty model build mode - 5: Mismatch error and output length + ) + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={"flag": flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + AttributeError, + "Finite difference option not recognized. Please contact the developers as you should not see this error.", + ): + doe_obj.fd_formula = "bad things" + doe_obj._generate_scenario_blocks() + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_bad_FD_seq_compute_FIM(self): + fd_method = "central" + obj_used = "trace" + flag_val = ( + 0 # Value for faulty model build mode - 5: Mismatch error and output length + ) + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={"flag": flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + AttributeError, + "Finite difference option not recognized. Please contact the developers as you should not see this error.", + ): + doe_obj.fd_formula = "bad things" + doe_obj.compute_FIM(method="sequential") + if __name__ == "__main__": unittest.main() From ae9d288099807c0e2431646eefa8dd6b4bc45bca Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 18 Jul 2024 11:37:24 -0600 Subject: [PATCH 1973/3044] Added tests for bad objective builds --- pyomo/contrib/doe/doe.py | 18 ++++-- pyomo/contrib/doe/tests/test_doe_errors.py | 71 ++++++++++++++++++++++ 2 files changed, 85 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 61bac7e6eb4..e5f5c41bcb9 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1116,6 +1116,20 @@ def create_objective_function(self, model=None): if model is None: model = self.model + if self.objective_option not in [ + ObjectiveLib.det, + ObjectiveLib.trace, + ObjectiveLib.zero, + ]: + raise AttributeError( + "Objective option not recognized. Please contact the developers as you should not see this error." + ) + + if not hasattr(model, "fim"): + raise RuntimeError( + "Model provided does not have variable `fim`. Please make sure the model is built properly before creating the objective." + ) + small_number = 1e-10 # Make objective block for constraints connected to objective @@ -1239,10 +1253,6 @@ def det_general(m): elif self.objective_option == ObjectiveLib.zero: # add dummy objective function model.objective = pyo.Objective(expr=0) - else: - raise AttributeError( - "Objective option not recognized. Please contact the developers as you should not see this error." - ) # Check to see if the model has all the required suffixes def check_model_labels(self, model=None): diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 15cf6b6373e..e2030162330 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -1027,6 +1027,77 @@ def test_bad_FD_seq_compute_FIM(self): doe_obj.fd_formula = "bad things" doe_obj.compute_FIM(method="sequential") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_bad_objective(self): + fd_method = "central" + obj_used = "trace" + flag_val = ( + 0 # Value for faulty model build mode - 5: Mismatch error and output length + ) + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={"flag": flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + AttributeError, + "Objective option not recognized. Please contact the developers as you should not see this error.", + ): + doe_obj.objective_option = "bad things" + doe_obj.create_objective_function() + + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_no_model_for_objective(self): + fd_method = "central" + obj_used = "trace" + flag_val = ( + 0 # Value for faulty model build mode - 5: Mismatch error and output length + ) + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={"flag": flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + RuntimeError, + "Model provided does not have variable `fim`. Please make sure the model is built properly before creating the objective.", + ): + doe_obj.create_objective_function() + if __name__ == "__main__": unittest.main() From 06a0f50eb58d55ea1fa1727404200646675b1de7 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 18 Jul 2024 11:40:00 -0600 Subject: [PATCH 1974/3044] Changed one run to use objective type --- pyomo/contrib/doe/tests/test_doe_solve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index e93e656f19d..4ab9e275c7e 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -127,7 +127,7 @@ def test_reactor_fd_central_solve(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_forward_solve(self): fd_method = "forward" - obj_used = "trace" + obj_used = "zero" experiment = FullReactorExperiment(data_ex, 10, 3) From cc4eec0df1a89deb789ff2e3b0e78c87cfd94d23 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 18 Jul 2024 11:45:57 -0600 Subject: [PATCH 1975/3044] Added not implemented error for unknown parameter value updating --- pyomo/contrib/doe/doe.py | 4 ++- pyomo/contrib/doe/tests/test_doe_errors.py | 34 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index e5f5c41bcb9..9f31fdeb010 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1380,7 +1380,9 @@ def update_FIM_prior(self, model=None, FIM=None): # ToDo: Add an update function for the parameter values? --> closed loop parameter estimation? # Or leave this to the user????? def update_unknown_parameter_values(self, model=None, param_vals=None): - return + raise NotImplementedError( + "Updating unknown parameter values not yet supported." + ) # Evaluates FIM and statistics for a full factorial space (same as run_grid_search) def compute_FIM_full_factorial(self, design_ranges=None, method="sequential"): diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index e2030162330..3df8355e528 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -955,6 +955,40 @@ def test_multiple_exp_not_implemented_sim(self): ): doe_obj.run_multi_doe_simultaneous(N_exp=1) + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_update_unknown_parameter_values_not_implemented_seq(self): + fd_method = "central" + obj_used = "trace" + flag_val = ( + 0 # Value for faulty model build mode - 5: Mismatch error and output length + ) + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={"flag": flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + NotImplementedError, "Updating unknown parameter values not yet supported." + ): + doe_obj.update_unknown_parameter_values() + @unittest.skipIf(not numpy_available, "Numpy is not available") def test_bad_FD_generate_scens(self): fd_method = "central" From a389d4f91881b72e6b051134ef88cd1f82c11682 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:48:09 -0400 Subject: [PATCH 1976/3044] Delete pyomo/contrib/doe/examples directory Old files removed to make room for new examples directory --- pyomo/contrib/doe/examples/__init__.py | 10 - pyomo/contrib/doe/examples/dynamic.csv | 5 - .../doe/examples/fim_5_300_500_scale.csv | 5 - .../contrib/doe/examples/fim_5_300_scale.csv | 5 - .../doe/examples/fim_doe_tutorial.ipynb | 1868 ----------------- .../doe/examples/reactor_compute_FIM.py | 111 - pyomo/contrib/doe/examples/reactor_design.py | 236 --- .../doe/examples/reactor_grid_search.py | 140 -- .../contrib/doe/examples/reactor_kinetics.py | 247 --- .../doe/examples/reactor_optimize_doe.py | 123 -- 10 files changed, 2750 deletions(-) delete mode 100644 pyomo/contrib/doe/examples/__init__.py delete mode 100644 pyomo/contrib/doe/examples/dynamic.csv delete mode 100644 pyomo/contrib/doe/examples/fim_5_300_500_scale.csv delete mode 100644 pyomo/contrib/doe/examples/fim_5_300_scale.csv delete mode 100644 pyomo/contrib/doe/examples/fim_doe_tutorial.ipynb delete mode 100644 pyomo/contrib/doe/examples/reactor_compute_FIM.py delete mode 100644 pyomo/contrib/doe/examples/reactor_design.py delete mode 100644 pyomo/contrib/doe/examples/reactor_grid_search.py delete mode 100644 pyomo/contrib/doe/examples/reactor_kinetics.py delete mode 100644 pyomo/contrib/doe/examples/reactor_optimize_doe.py diff --git a/pyomo/contrib/doe/examples/__init__.py b/pyomo/contrib/doe/examples/__init__.py deleted file mode 100644 index a4a626013c4..00000000000 --- a/pyomo/contrib/doe/examples/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/pyomo/contrib/doe/examples/dynamic.csv b/pyomo/contrib/doe/examples/dynamic.csv deleted file mode 100644 index f54d798bda3..00000000000 --- a/pyomo/contrib/doe/examples/dynamic.csv +++ /dev/null @@ -1,5 +0,0 @@ -A1,A2,E1,E2 -6.209381770954067,7.719297166025923,-12.835153102965977,-38.540492455469554 -7.719297166025923,20.53859118830565,-14.829065563786362,-99.2962499942191 --12.835153102965977,-14.829065563786362,26.869188945470434,74.5001011185848 --38.540492455469554,-99.2962499942191,74.5001011185848,484.97578893372025 diff --git a/pyomo/contrib/doe/examples/fim_5_300_500_scale.csv b/pyomo/contrib/doe/examples/fim_5_300_500_scale.csv deleted file mode 100644 index 77c0424aa13..00000000000 --- a/pyomo/contrib/doe/examples/fim_5_300_500_scale.csv +++ /dev/null @@ -1,5 +0,0 @@ -A1,A2,E1,E2 -28.678928056936364,5.412497388906993,-81.73674601413501,-24.023773235011475 -5.412497388906993,26.409350356572013,-12.418164773953235,-139.2399253159117 --81.73674601413501,-12.418164773953235,240.46276003997696,58.764228064029076 --24.023773235011475,-139.2399253159117,58.764228064029076,767.255845082616 diff --git a/pyomo/contrib/doe/examples/fim_5_300_scale.csv b/pyomo/contrib/doe/examples/fim_5_300_scale.csv deleted file mode 100644 index 381e916b9d4..00000000000 --- a/pyomo/contrib/doe/examples/fim_5_300_scale.csv +++ /dev/null @@ -1,5 +0,0 @@ -A1,A2,E1,E2 -22.529430237938822,1.8403431417002734,-70.23273336318343,-11.094329617631416 -1.8403431417002734,18.098481155262718,-5.7356503398877745,-109.15866135211135 --70.23273336318343,-5.7356503398877745,218.9419284259853,34.576808479575064 --11.094329617631416,-109.15866135211135,34.576808479575064,658.3764463408718 diff --git a/pyomo/contrib/doe/examples/fim_doe_tutorial.ipynb b/pyomo/contrib/doe/examples/fim_doe_tutorial.ipynb deleted file mode 100644 index 36ec42fbe49..00000000000 --- a/pyomo/contrib/doe/examples/fim_doe_tutorial.ipynb +++ /dev/null @@ -1,1868 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Pyomo.DoE Tutorial: Reaction Kinetics Example \n", - "\n", - "Jialu Wang (jwang44@nd.edu), Alex Dowling (adowling@nd.edu), and Hailey Lynch (hlynch@nd.edu)\n", - "\n", - "University of Notre Dame\n", - "\n", - "This notebook demonstrates the main features of Pyomo.DoE (model-based design of experiments) using a reaction kinetics example. See [Wang and Dowling (2022), AIChE J.](https://aiche.onlinelibrary.wiley.com/doi/full/10.1002/aic.17813), for more information.\n", - "\n", - "The user will be able to learn concepts involved with model-based design of experiments (MBDoE) and practice using Pyomo.DoE from methodology in the notebook. Results will be interpreted throughout the notebook to connect the material with the Pyomo implementation.\n", - " " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The general process that will follow throughout this notebook:\n", - "\n", - "Import Modules\n", - "\n", - "* Step 0: Import Pyomo and Pyomo.DoE Module\n", - "\n", - "Problem Statement\n", - "\n", - "* Step 1: Import Reaction Kinetics Example Mathematical Model\n", - "\n", - "Implementation in Pyomo\n", - "\n", - "* Step 2: Implement Mathematical Model in Pyomo\n", - "* Step 3: Define Inputs for the Model\n", - "\n", - "Methodology\n", - "\n", - "* Step 4: Method for Computing FIM\n", - "\n", - "* Step 5: Method for Optimization\n", - "\n", - "* Step 6: Method for Exploratory Analysis through Enumeration\n", - "\n", - "Visualizing Results\n", - "\n", - "* Step 7: Results through Heatmaps and Sensitivity Curves\n", - "\n", - "Key Takeaways\n", - "* MBDoE maximizes the information gained from experiments which reduces uncertainty (technical risk) and facilitates better decision-making.\n", - "\n", - "* FIM quantifies the information contained in a set of experiments (data) with respect to a mathematical model\n", - "\n", - "* MBDoE optimality criteria (e.g., A, D, E-optimal designs) compress the FIM into a scalar. The \"correct\" criterion depends on the DoE goal and model context.\n", - "\n", - "* Heatmaps provide visualizations of the most informative parameters using the MBDoE optimality criteria." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 0: Import Pyomo and Pyomo.DoE module" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Successfully loaded IDAES.\n" - ] - } - ], - "source": [ - "# Ipopt installer\n", - "import sys\n", - "\n", - "# If running on Google Colab, install Ipopt via IDAES\n", - "if \"google.colab\" in sys.modules:\n", - " !wget \"https://raw.githubusercontent.com/IDAES/idaes-pse/main/scripts/colab_helper.py\"\n", - " import colab_helper\n", - "\n", - " colab_helper.install_idaes()\n", - " colab_helper.install_ipopt()\n", - "\n", - "# Otherwise, attempt to load IDAES which should include Ipopt and k_aug\n", - "# See https://idaes-pse.readthedocs.io/en/stable/tutorials/getting_started/index.html\n", - "# for instructions on running IDAES get-extensions\n", - "else:\n", - " try:\n", - " import idaes\n", - "\n", - " # Provided IDAES extensions are installed, importing IDAES provides access to\n", - " # Ipopt with HSL and k_aug which are needed for this example\n", - " print(\"Successfully loaded IDAES.\")\n", - " except:\n", - " print(\n", - " \"IDAES is not installed. Make sure you have independently installed Ipopt with HSL and k_aug.\"\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Imports\n", - "import numpy as np\n", - "import pyomo.environ as pyo\n", - "from pyomo.dae import ContinuousSet, DerivativeVar\n", - "from pyomo.contrib.doe import (\n", - " ModelOptionLib,\n", - " DesignOfExperiments,\n", - " MeasurementVariables,\n", - " DesignVariables,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Check if Ipopt is available\n", - "ipopt_available = pyo.SolverFactory(\"ipopt\").available()\n", - "if not (ipopt_available):\n", - " raise RuntimeError(\"This Pyomo.DoE example requires Ipopt.\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 1: Import Reaction Kinetics Example Mathematical Model" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Consider two chemical reactions that convert molecule $A$ to desired product $B$ and a less valuable side-product $C$.\n", - "\n", - "$$A \\overset{k_1}{\\rightarrow} B \\overset{k_2}{\\rightarrow} C$$\n", - "\n", - "Our ultimate goal is to design a large-scale continuous reactor that maximizes the production of $B$. This general sequential reactions problem is widely applicable to CO$_2$ capture and industry more broadly (petrochemicals, pharmaceuticals, etc.).\n", - "\n", - "The rate laws for these two chemical reactions are:\n", - "\n", - "$$r_A = -k_1 C_A$$\n", - "\n", - "$$r_B = k_1 C_A - k_2 C_B$$\n", - "\n", - "$$r_C = k_2 C_B$$\n", - "\n", - "Here, $C_A$, $C_B$, and $C_C$ are the concentrations of each species. \n", - "\n", - "The rate constants $k_1$ and $k_2$ depend on temperature as follows:\n", - "\n", - "$$k_1 = A_1 \\exp{\\frac{-E_1}{R T}}$$\n", - "\n", - "$$k_2 = A_2 \\exp{\\frac{-E_2}{R T}}$$\n", - "\n", - "where:\n", - "* $A_1$ [$s^{-1}$], $A_2$ [$s^{-1}$] , $E_1$ [kJ/mol], and $E_2$ [kJ/mol] are fitted model parameters\n", - "* $R$ [J/molK] is the ideal-gas constant\n", - "* $T$ [K] is absolute temperature\n", - "\n", - "Using the Pyomo ecosystem, we would like to perform **uncertainty quantification** and **design of experiments** on a small-scale batch reactor to infer parameters $A_1$, $A_2$, $E_1$, and $E_2$." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Batch Reactor\n", - "\n", - "The concentrations in a batch reactor evolve with time and are modeled by the following differential equations:\n", - "\n", - "$$ \\frac{d C_A}{dt} = r_A = -k_1 C_A $$\n", - "\n", - "$$ \\frac{d C_B}{dt} = r_B = k_1 C_A - k_2 C_B $$\n", - "\n", - "$$ \\frac{d C_C}{dt} = r_C = k_2 C_B $$\n", - "\n", - "We have now established a linear system of differential equations. Next, we can write the initial conditions assuming the feed is only species $A$ such that:\n", - "\n", - "$$C_A(t=0) = C_{A0}, \\quad C_B(t=0) = 0, \\quad C_C(t=0) = 0$$\n", - "\n", - "When $k_1$ and $k_2$ are at constant temperature, it leads to the following analytic solution:\n", - "\n", - "$$C_A(t) = C_{A0} \\exp(-k_1 t)$$\n", - "\n", - "$$C_B(t) = \\frac{k_1}{k_2 - k_1} C_{A0} \\left[\\exp(-k_1 t) - \\exp(-k_2 t) \\right]$$\n", - "\n", - "$$C_C(t) = C_{A0} - \\frac{k_2}{k_2 - k_1} C_{A0} \\exp(-k_1 t) + \\frac{k_1}{k_2 - k_1} \\exp(-k_2 t) C_{A0} = C_{A0} - C_{A}(t) - C_{B}(t)$$" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Implement Mathematical Model in Pyomo" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The mathematical model is comprised of a system of differential-algebraic equations (DAEs) which will be solved using Pyomo.DAE." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "# Discretize using Pyomo.DAE\n", - "def disc_for_measure(m, nfe=32, block=True):\n", - " \"\"\"\n", - " Pyomo.DAE discretization\n", - "\n", - " Arguments\n", - " ---------\n", - " m: Pyomo model\n", - " nfe: number of finite elements b\n", - " block: if True, the input model has blocks\n", - " \"\"\"\n", - " # Discretization using collocation\n", - " discretizer = pyo.TransformationFactory(\"dae.collocation\")\n", - " if block:\n", - " for s in range(len(m.block)):\n", - " discretizer.apply_to(m.block[s], nfe=nfe, ncp=3, wrt=m.block[s].t)\n", - " else:\n", - " discretizer.apply_to(m, nfe=nfe, ncp=3, wrt=m.t)\n", - " return m" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Next, create the model." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "# Create model\n", - "def create_model(\n", - " mod=None,\n", - " model_option=\"stage2\",\n", - " control_time=[0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1],\n", - " control_val=None,\n", - " t_range=[0.0, 1],\n", - " CA_init=1,\n", - " C_init=0.1,\n", - "):\n", - " \"\"\"\n", - " This is an example user model provided to the DoE library.\n", - " It is a dynamic problem solved by Pyomo.DAE.\n", - "\n", - " Arguments\n", - " ---------\n", - " mod: Pyomo model. If None, a Pyomo concrete model is created\n", - " model_option: choose from the 3 options in model_option\n", - " if ModelOptionLib.parmest, create a process model.\n", - " if ModelOptionLib.stage1, create the global model.\n", - " if ModelOptionLib.stage2, add model variables and constraints for block.\n", - " control_time: a list of control timepoints\n", - " control_val: control design variable values T at corresponding timepoints\n", - " t_range: time range, hours\n", - " CA_init: time-independent design (control) variable, an initial value for CA\n", - " C_init: An initial value for C\n", - "\n", - " Return\n", - " ------\n", - " m: a Pyomo.DAE model\n", - " \"\"\"\n", - " # Parameter initialization; results from parameter estimation\n", - " theta = {\"A1\": 84.79, \"A2\": 371.72, \"E1\": 7.78, \"E2\": 15.05}\n", - "\n", - " # Model option\n", - " model_option = ModelOptionLib(model_option)\n", - "\n", - " if model_option == ModelOptionLib.parmest:\n", - " mod = pyo.ConcreteModel()\n", - " return_m = True\n", - " elif model_option == ModelOptionLib.stage1 or model_option == ModelOptionLib.stage2:\n", - " if not mod:\n", - " raise ValueError(\n", - " \"If model option is stage1 or stage2, a created model needs to be provided.\"\n", - " )\n", - " return_m = False\n", - " else:\n", - " raise ValueError(\n", - " \"model_option needs to be defined as parmest, stage1, or stage2.\"\n", - " )\n", - "\n", - " # Control value\n", - " if not control_val:\n", - " control_val = [300] * 9\n", - "\n", - " # Control time\n", - " controls = {}\n", - " for i, t in enumerate(control_time):\n", - " controls[t] = control_val[i]\n", - "\n", - " mod.t0 = pyo.Set(initialize=[0])\n", - " mod.t_con = pyo.Set(initialize=control_time)\n", - " mod.CA0 = pyo.Var(\n", - " mod.t0, initialize=CA_init, bounds=(1.0, 5.0), within=pyo.NonNegativeReals\n", - " ) # mol/L\n", - "\n", - " # Check if control_time is in time range\n", - " assert (\n", - " control_time[0] >= t_range[0] and control_time[-1] <= t_range[1]\n", - " ), \"control time is outside time range.\"\n", - "\n", - " if model_option == ModelOptionLib.stage1:\n", - " mod.T = pyo.Var(\n", - " mod.t_con,\n", - " initialize=controls,\n", - " bounds=(300, 700),\n", - " within=pyo.NonNegativeReals,\n", - " )\n", - " return\n", - "\n", - " else:\n", - " para_list = [\"A1\", \"A2\", \"E1\", \"E2\"]\n", - "\n", - " # Add variables\n", - " mod.CA_init = CA_init\n", - " mod.para_list = para_list\n", - "\n", - " # Timepoints\n", - " mod.t = ContinuousSet(bounds=t_range, initialize=control_time)\n", - "\n", - " # Time-dependent design variable; initialized with the first control value\n", - " def T_initial(m, t):\n", - " if t in m.t_con:\n", - " return controls[t]\n", - " else:\n", - " # Count how many control points are before the current t;\n", - " # Locate the nearest neighbouring control point before this t\n", - " neighbour_t = max(tc for tc in control_time if tc < t)\n", - " return controls[neighbour_t]\n", - "\n", - " mod.T = pyo.Var(\n", - " mod.t, initialize=T_initial, bounds=(300, 700), within=pyo.NonNegativeReals\n", - " )\n", - "\n", - " # Gas constant\n", - " mod.R = 8.31446261815324 # J / K / mole\n", - "\n", - " # Define variables as Var\n", - " mod.A1 = pyo.Var(initialize=theta[\"A1\"])\n", - " mod.A2 = pyo.Var(initialize=theta[\"A2\"])\n", - " mod.E1 = pyo.Var(initialize=theta[\"E1\"])\n", - " mod.E2 = pyo.Var(initialize=theta[\"E2\"])\n", - "\n", - " # Concentration variables under perturbation\n", - " mod.C_set = pyo.Set(initialize=[\"CA\", \"CB\", \"CC\"])\n", - " mod.C = pyo.Var(\n", - " mod.C_set, mod.t, initialize=C_init, within=pyo.NonNegativeReals\n", - " )\n", - "\n", - " # Time derivative of C\n", - " mod.dCdt = DerivativeVar(mod.C, wrt=mod.t)\n", - "\n", - " # Kinetic parameters\n", - " def kp1_init(m, t):\n", - " return m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t]))\n", - "\n", - " def kp2_init(m, t):\n", - " return m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t]))\n", - "\n", - " mod.kp1 = pyo.Var(mod.t, initialize=kp1_init)\n", - " mod.kp2 = pyo.Var(mod.t, initialize=kp2_init)\n", - "\n", - " def T_control(m, t):\n", - " \"\"\"\n", - " Time is discretized for numeric integration. A subset of these time points are control time points.\n", - " Temperature is constant within each control time point.\n", - "\n", - " TODO: replace this function with reduce_collocation_points\n", - " https://pyomo.readthedocs.io/en/stable/modeling_extensions/dae.html\n", - "\n", - " \"\"\"\n", - " if t in m.t_con:\n", - " return pyo.Constraint.Skip\n", - " else:\n", - " neighbour_t = max(tc for tc in control_time if tc < t)\n", - " return m.T[t] == m.T[neighbour_t]\n", - "\n", - " def cal_kp1(m, t):\n", - " \"\"\"\n", - " Create the perturbation parameter sets for A --> B reaction\n", - "\n", - " m: model\n", - " t: time\n", - " \"\"\"\n", - " # LHS: 1/h\n", - " # RHS: 1/h*(kJ/mol *1000J/kJ / (J/mol/K) / K)\n", - " return m.kp1[t] == m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t]))\n", - "\n", - " def cal_kp2(m, t):\n", - " \"\"\"\n", - " Create the perturbation parameter sets for B --> C reaction\n", - "\n", - " m: model\n", - " t: time\n", - " \"\"\"\n", - " # LHS: 1/h\n", - " # RHS: 1/h*(kJ/mol *1000J/kJ / (J/mol/K) / K)\n", - " return m.kp2[t] == m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t]))\n", - "\n", - " def dCdt_control(m, y, t):\n", - " \"\"\"\n", - " Calculate CA in Jacobian matrix analytically\n", - "\n", - " y: CA, CB, CC\n", - " t: timepoints\n", - " \"\"\"\n", - " if y == \"CA\":\n", - " return m.dCdt[y, t] == -m.kp1[t] * m.C[\"CA\", t]\n", - " elif y == \"CB\":\n", - " return m.dCdt[y, t] == m.kp1[t] * m.C[\"CA\", t] - m.kp2[t] * m.C[\"CB\", t]\n", - " elif y == \"CC\":\n", - " return pyo.Constraint.Skip\n", - "\n", - " def alge(m, t):\n", - " \"\"\"\n", - " The algebraic equation for mole balance\n", - "\n", - " z: m.pert\n", - " t: time\n", - " \"\"\"\n", - " return m.C[\"CA\", t] + m.C[\"CB\", t] + m.C[\"CC\", t] == m.CA0[0]\n", - "\n", - " # Control time\n", - " mod.T_rule = pyo.Constraint(mod.t, rule=T_control)\n", - "\n", - " # Calculating C, Jacobian, FIM\n", - " mod.k1_pert_rule = pyo.Constraint(mod.t, rule=cal_kp1)\n", - " mod.k2_pert_rule = pyo.Constraint(mod.t, rule=cal_kp2)\n", - " mod.dCdt_rule = pyo.Constraint(mod.C_set, mod.t, rule=dCdt_control)\n", - "\n", - " mod.alge_rule = pyo.Constraint(mod.t, rule=alge)\n", - "\n", - " # Boundary conditions\n", - " mod.C[\"CB\", 0.0].fix(0.0)\n", - " mod.C[\"CC\", 0.0].fix(0.0)\n", - "\n", - " if return_m:\n", - " return mod" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# The above models are alternately available in the examples folder:\n", - "# from pyomo.contrib.doe.examples.reactor_kinetics import create_model, disc_for_measure" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 3: Define Inputs for the Model" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "scrolled": true, - "tags": [] - }, - "outputs": [], - "source": [ - "# Control time set [h]\n", - "t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1]\n", - "# Define parameter nominal value\n", - "parameter_dict = {\"A1\": 84.79, \"A2\": 371.72, \"E1\": 7.78, \"E2\": 15.05}" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "scrolled": true, - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "measurement names: ['C[CA,0]', 'C[CA,0.125]', 'C[CA,0.25]', 'C[CA,0.375]', 'C[CA,0.5]', 'C[CA,0.625]', 'C[CA,0.75]', 'C[CA,0.875]', 'C[CA,1]', 'C[CB,0]', 'C[CB,0.125]', 'C[CB,0.25]', 'C[CB,0.375]', 'C[CB,0.5]', 'C[CB,0.625]', 'C[CB,0.75]', 'C[CB,0.875]', 'C[CB,1]', 'C[CC,0]', 'C[CC,0.125]', 'C[CC,0.25]', 'C[CC,0.375]', 'C[CC,0.5]', 'C[CC,0.625]', 'C[CC,0.75]', 'C[CC,0.875]', 'C[CC,1]']\n" - ] - } - ], - "source": [ - "# Pyomo.DoE defines measurements\n", - "# Measurements have at most 1 index besides the time index\n", - "variable_name = \"C\"\n", - "indices = {0: [\"CA\", \"CB\", \"CC\"], 1: t_control}\n", - "\n", - "# Measurement class\n", - "measure_class = MeasurementVariables()\n", - "measure_class.add_variables(variable_name, indices=indices, time_index_position=1)\n", - "print(\"measurement names:\", measure_class.variable_names)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "scrolled": true, - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Design variable names: ['CA0[0]', 'T[0]', 'T[0.125]', 'T[0.25]', 'T[0.375]', 'T[0.5]', 'T[0.625]', 'T[0.75]', 'T[0.875]', 'T[1]']\n" - ] - } - ], - "source": [ - "# Design variables\n", - "design_gen = DesignVariables()\n", - "\n", - "var_C = \"CA0\"\n", - "indices_C = {0: [0]}\n", - "exp1_C = [5]\n", - "\n", - "# Add design variable\n", - "design_gen.add_variables(\n", - " var_C,\n", - " indices=indices_C,\n", - " time_index_position=0,\n", - " values=exp1_C,\n", - " lower_bounds=1,\n", - " upper_bounds=5,\n", - ")\n", - "\n", - "\n", - "var_T = \"T\"\n", - "indices_T = {0: t_control}\n", - "exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300]\n", - "\n", - "design_gen.add_variables(\n", - " var_T,\n", - " indices=indices_T,\n", - " time_index_position=0,\n", - " values=exp1_T,\n", - " lower_bounds=300,\n", - " upper_bounds=700,\n", - ")\n", - "print(\"Design variable names:\", design_gen.variable_names)" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Parameter dictionary\n", - "param_dict = {\"A1\": 84.79, \"A2\": 371.72, \"E1\": 7.78, \"E2\": 15.05}" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 4: Method for Computing FIM \n", - "\n", - "This method computes an FIM-based MBDoE optimization problem with zero degrees of freedom." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Fisher Information Matrix (FIM)\n", - "The FIM measures the information content for the unknown parameters $\\theta$ from the model output $y_i$ given that:\n", - "\n", - "$$ y_i = f(\\psi_i, \\theta) $$\n", - "\n", - "where $\\psi$ is a design vector from a DAE system.\n", - "\n", - "In order to quantify the uncertainty of the estimated parameters for parameter estimation, consider the covariance matrix for the parameters:\n", - "\n", - "$$V(\\hat{\\theta},\\psi) = \\left[\\sum_{r}^{N_{r}}\\sum_{r'}^{N_{r}} \\tilde{\\sigma}_{(r,r')}Q_{r}^{T}Q_{r'}+V_{\\theta}(\\hat{\\theta})^{-1}\\right]^{-1}$$\n", - "\n", - "where:\n", - "* $\\hat{\\theta}$: estimated parameters\n", - "* $\\tilde{\\sigma}$: element in the inverse of the observational covariance matrix\n", - "* $r,$ $r'$: measurements\n", - "* $Q$: dynamic sensitivity\n", - "* $V_{\\theta}$: prior information\n", - "* $N_r$: number of measurements\n", - "\n", - "The inverse of $V$ estimates the FIM such that:\n", - "\n", - "$$V(\\hat{\\theta},\\psi) \\approx [M(\\hat{\\theta},\\psi)]^{-1}$$\n", - "\n", - "For sequential design of experiments, consider prior information such that after $N_e$ dynamic experiments, the FIM is calculated by:\n", - "$$M= \\sum_{k=1}^{N_e-1}M_k+M_{N_e}(\\hat{\\theta},\\psi_{N_e}) = K+M_{N_e}(\\hat{\\theta},\\psi_{N_e})$$\n", - "\n", - "where:\n", - "* $N_e - 1$: previous experiments\n", - "* $K$: constant matrix encoding information from all $N_e - 1$\n", - "\n", - "**Key Takeaway**:\n", - "A **large** FIM value denotes **more** information about $\\theta$ is gained from the model" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Model-Based Design of Experiments\n", - "The objective of MBDoE changes the conditions of one or more experiments based on a specific purpose such as:\n", - "\n", - "1. Model identification\n", - " * Discriminates between possible models while omitting inadequate models\n", - "2. Parameter estimation\n", - " * Improves parameter estimation precision\n", - "\n", - "**Key Takeaways:**\n", - "Given an estimate for an unknown parameter and one or more mathematical models, MBDoE:\n", - "1. Determines a set of experimental conditions to maximize the precision of the unknown model parameters\n", - "2. Discriminates between the given models\n", - "3. Or both (1) and (2)\n", - "\n", - "Currently, Pyomo.DoE supports MBDoE for parameter precision." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "scrolled": true, - "tags": [] - }, - "outputs": [], - "source": [ - "# Choose from 'sequential_finite', 'direct_kaug'\n", - "# sensi_opt = \"direct_kaug\"\n", - "sensi_opt = \"sequential_finite\"\n", - "\n", - "# Define experiments\n", - "design_names = design_gen.variable_names\n", - "exp1 = [5, 470, 300, 300, 300, 300, 300, 300, 300, 300]\n", - "exp1_design_dict = dict(zip(design_names, exp1))\n", - "\n", - "# Update values\n", - "design_gen.update_values(exp1_design_dict)" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "scrolled": true, - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 25344\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2320\n", - "\n", - "Total number of variables............................: 6968\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 6968\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.67e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (239448)\n", - " 1 0.0000000e+00 3.23e+01 3.85e+02 -1.0 1.67e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 6.16e+00 8.61e+01 -1.0 3.87e+01 - 1.11e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 5.12e-02 9.97e+00 -1.0 4.16e+00 - 9.60e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 1.20e-06 7.52e+01 -1.0 3.62e-02 - 9.97e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (287125)\n", - " 5 0.0000000e+00 2.25e-13 1.00e-06 -1.0 7.68e-07 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 9.3507738837536098e-14 2.2537527399890678e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 9.3507738837536098e-14 2.2537527399890678e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.468\n", - "Total CPU secs in NLP function evaluations = 0.017\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "INFO: elapsed time: 2.9\n" - ] - } - ], - "source": [ - "# Create doe_object using DesignOfExperiments\n", - "doe_object = DesignOfExperiments(\n", - " parameter_dict, # dictionary of parameters\n", - " design_gen, # design variable\n", - " measure_class, # measurement variable\n", - " create_model, # model\n", - " discretize_model=disc_for_measure, # discretized model\n", - ")\n", - "\n", - "# Computing the FIM\n", - "result = doe_object.compute_FIM(\n", - " mode=sensi_opt, # solver option for sensitivity optimization\n", - " FIM_store_name=\"dynamic.csv\", # csv file that stores FIM data\n", - " read_output=None, # outputs from stored file; do not have to rerun since there are measurement values already\n", - " scale_nominal_param_value=True, # scale the Jacobian with the parameter values\n", - " formula=\"central\", # finite difference - central method\n", - ")\n", - "\n", - "# Results\n", - "result.result_analysis()" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": { - "scrolled": true, - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "======Results Summary======\n", - "Four design criteria log10() value:\n", - "A-optimality: 2.989724462425373\n", - "D-optimality: 3.3010989022733894\n", - "E-optimality: -0.9193349136200465\n", - "Modified E-optimality: 3.87680755495709\n", - "[[ 17.22096879 13.67125453 -37.1471375 -68.68858407]\n", - " [ 13.67125453 34.5737961 -26.37449298 -170.10871631]\n", - " [ -37.1471375 -26.37449298 81.32448107 133.30724227]\n", - " [ -68.68858407 -170.10871631 133.30724227 843.49816474]]\n" - ] - } - ], - "source": [ - "# Results summary\n", - "print(\"======Results Summary======\")\n", - "print(\"Four design criteria log10() value:\")\n", - "print(\"A-optimality:\", np.log10(result.trace))\n", - "print(\"D-optimality:\", np.log10(result.det))\n", - "print(\"E-optimality:\", np.log10(result.min_eig))\n", - "print(\"Modified E-optimality:\", np.log10(result.cond))\n", - "print(result.FIM)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Optimality Conditions\n", - "**D-Optimality:** Maximizes the determinant of $M$ or minimizes the determinant of $V$ \t\n", - "* Computation: Determinant\n", - "* Geometric interpretation: Minimizes the volume of the confidence ellipsoid \n", - "\n", - "**A-Optimality:** Maximizes the trace of $M$ or minimizes the trace of $V$ \t\n", - "* Computation: Trace \t\n", - "* Geometric interpretation: Minimizes the dimensions of the enclosing box around the confidence ellipsoid \t\n", - "\n", - "**E-Optimality:** Minimizes the variance of the most uncertain parameter \t \n", - "* Computation: Eigenvalue \t\n", - "* Geometric interpretiation: Minimizes the size of the major axis of the confidence ellipsoid \n", - "\n", - "**Modified E-Optimality:** Reduces the correlations between parameters \t \n", - "* Computation: Condition number \t \n", - "* Geometric interpretation: Transforms the confidence ellipsoid into a round sphere " - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['C[CB,0.125]', 'C[CB,0.25]', 'C[CB,0.5]', 'C[CB,0.75]', 'C[CB,0.875]', 'C[CC,0.125]', 'C[CC,0.25]', 'C[CC,0.5]', 'C[CC,0.75]', 'C[CC,0.875]']\n" - ] - } - ], - "source": [ - "# Choose a subset of measurements and get the results without resolving the model\n", - "sub_name = \"C\"\n", - "sub_indices = {0: [\"CB\", \"CC\"], 1: [0.125, 0.25, 0.5, 0.75, 0.875]}\n", - "\n", - "# Measurement subset\n", - "measure_subset = MeasurementVariables()\n", - "measure_subset.add_variables(sub_name, indices=sub_indices, time_index_position=1)\n", - "print(measure_subset.variable_names)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "======Subset Results Summary======\n", - "Four design criteria log10() value:\n", - "A-optimality: 2.7312606650205398\n", - "D-optimality: 1.8213450338458799\n", - "E-optimality: -1.430816119614162\n", - "Modified E-optimality: 4.147090377578492\n" - ] - } - ], - "source": [ - "# Subset results summary\n", - "sub_result = result.subset(measure_subset)\n", - "sub_result.result_analysis()\n", - "print(\"======Subset Results Summary======\")\n", - "print(\"Four design criteria log10() value:\")\n", - "print(\"A-optimality:\", np.log10(sub_result.trace))\n", - "print(\"D-optimality:\", np.log10(sub_result.det))\n", - "print(\"E-optimality:\", np.log10(sub_result.min_eig))\n", - "print(\"Modified E-optimality:\", np.log10(sub_result.cond))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 5: Method for Optimization\n", - "Gradient-based optimization with Ipopt using stochastic_program().\n", - "\n", - "We first fix the experiment design decisions and solve the simulation problem (zero degrees of freedom). This facilitates initialization.\n", - "\n", - "Next, we unfix the experiment design variables and resolve the optimization problem (positive number of degrees of freedom).\n", - "\n", - "This allows us to compute the best time-varying piecewise-constant temperature profile for the batch reactor experiment." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [], - "source": [ - "# Experiment\n", - "exp1 = [5, 500, 300, 300, 300, 300, 300, 300, 300, 300]\n", - "exp1_design_dict = dict(zip(design_names, exp1))\n", - "design_gen.update_values(exp1_design_dict)\n", - "\n", - "# Add prior information (scaled FIM with T=500 and T=300 experiments)\n", - "prior = np.asarray(\n", - " [\n", - " [28.67892806, 5.41249739, -81.73674601, -24.02377324],\n", - " [5.41249739, 26.40935036, -12.41816477, -139.23992532],\n", - " [-81.73674601, -12.41816477, 240.46276004, 58.76422806],\n", - " [-24.02377324, -139.23992532, 58.76422806, 767.25584508],\n", - " ]\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26424\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2590\n", - "\n", - "Total number of variables............................: 7092\n", - " variables with only lower bounds: 2312\n", - " variables with lower and upper bounds: 784\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7092\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 7.67e+02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (296034)\n", - " 1 0.0000000e+00 6.31e+02 3.85e+02 -1.0 7.66e+02 - 2.54e-03 9.90e-01f 1\n", - " 2 0.0000000e+00 1.42e+02 1.31e+02 -1.0 9.10e+02 - 9.17e-02 9.90e-01h 1\n", - " 3 0.0000000e+00 2.07e+01 1.75e+01 -1.0 1.43e+02 - 9.37e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 1.52e-03 1.41e+02 -1.0 1.86e+01 - 9.94e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (393945)\n", - " 5 0.0000000e+00 4.55e-13 1.00e-06 -1.0 1.55e-03 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.2737367544323206e-13 4.5474735088646412e-13\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.2737367544323206e-13 4.5474735088646412e-13\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.584\n", - "Total CPU secs in NLP function evaluations = 0.017\n", - "\n", - "EXIT: Optimal Solution Found.\n", - "Ipopt 3.13.2: linear_solver=ma57\n", - "halt_on_ampl_error=yes\n", - "max_iter=3000\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma57.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 26544\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 2610\n", - "\n", - "Reallocating memory for MA57: lfact (273960)\n", - "Total number of variables............................: 7112\n", - " variables with only lower bounds: 2316\n", - " variables with lower and upper bounds: 794\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7102\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -1.1850752e+01 7.70e+02 1.75e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "Reallocating memory for MA57: lfact (303416)\n", - " 1 -1.3675099e+01 2.82e+02 1.02e+00 -1.0 1.74e+01 - 5.78e-01 5.20e-01h 1\n", - " 2 -1.4089220e+01 2.17e+01 1.63e+00 -1.0 1.13e+01 - 9.61e-01 1.00e+00f 1\n", - " 3 -1.3921907e+01 1.69e+00 3.69e+00 -1.0 6.63e+01 - 9.32e-01 1.00e+00f 1\n", - " 4 -1.3336947e+01 2.31e+01 1.65e+01 -1.0 1.77e+02 - 1.00e+00 1.00e+00f 1\n", - " 5 -1.3222146e+01 1.86e+01 1.00e+01 -1.0 1.99e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 -1.3243949e+01 2.13e-01 6.39e-01 -1.0 1.01e+01 - 1.00e+00 1.00e+00h 1\n", - " 7 -1.3252911e+01 1.32e-03 1.78e-02 -1.7 5.37e-01 - 1.00e+00 1.00e+00h 1\n", - " 8 -1.3275341e+01 5.50e-02 1.07e+00 -3.8 4.82e+00 - 9.24e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (319294)\n", - " 9 -1.3682468e+01 1.73e+01 2.63e+01 -3.8 8.88e+01 - 5.41e-01 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -1.4419541e+01 8.07e+01 9.14e+01 -3.8 5.70e+02 - 2.57e-01 5.52e-01h 1\n", - " 11 -1.4227603e+01 2.35e+01 2.67e+00 -3.8 7.85e+01 - 7.14e-01 1.00e+00h 1\n", - " 12 -1.4224985e+01 3.88e-01 5.16e-01 -3.8 2.90e+01 - 1.00e+00 1.00e+00h 1\n", - " 13 -1.4226481e+01 2.67e-02 5.57e-03 -3.8 6.57e+00 - 1.00e+00 1.00e+00h 1\n", - " 14 -1.4226282e+01 1.91e-05 5.93e-06 -3.8 1.11e-01 - 1.00e+00 1.00e+00h 1\n", - " 15 -1.4292847e+01 1.06e+00 7.22e-01 -5.7 4.62e+01 - 7.74e-01 1.00e+00f 1\n", - "Reallocating memory for MA57: lfact (339585)\n", - " 16 -1.4306021e+01 5.87e-02 4.67e-02 -5.7 2.09e+01 - 1.00e+00 1.00e+00h 1\n", - " 17 -1.4307820e+01 1.18e-02 2.10e-03 -5.7 9.64e+00 - 1.00e+00 1.00e+00h 1\n", - " 18 -1.4307833e+01 3.56e-04 4.99e-06 -5.7 1.70e+00 - 1.00e+00 1.00e+00h 1\n", - " 19 -1.4307833e+01 2.54e-07 3.05e-09 -5.7 4.55e-02 - 1.00e+00 1.00e+00h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 -1.4309105e+01 8.75e-04 2.56e-04 -8.6 2.68e+00 - 9.92e-01 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (358657)\n", - "Reallocating memory for MA57: lfact (392921)\n", - " 21 -1.4309111e+01 2.81e-06 7.65e-08 -8.6 1.52e-01 - 1.00e+00 1.00e+00h 1\n", - " 22 -1.4309111e+01 8.54e-12 2.66e-08 -8.6 2.66e-04 -4.0 1.00e+00 1.00e+00h 1\n", - "Reallocating memory for MA57: lfact (450838)\n", - "Reallocating memory for MA57: lfact (477335)\n", - " 23 -1.4309111e+01 3.31e-12 5.52e-09 -8.6 1.66e-04 -4.5 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 23\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -1.4309111333867460e+01 -1.4309111333867460e+01\n", - "Dual infeasibility......: 5.5189781291491592e-09 5.5189781291491592e-09\n", - "Constraint violation....: 3.3140157285060923e-12 3.3140157285060923e-12\n", - "Complementarity.........: 2.5059035851932608e-09 2.5059035851932608e-09\n", - "Overall NLP error.......: 5.5189781291491592e-09 5.5189781291491592e-09\n", - "\n", - "\n", - "Number of objective function evaluations = 24\n", - "Number of objective gradient evaluations = 24\n", - "Number of equality constraint evaluations = 24\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 24\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 23\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 2.472\n", - "Total CPU secs in NLP function evaluations = 0.059\n", - "\n", - "EXIT: Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: elapsed time: 5.6\n" - ] - } - ], - "source": [ - "# Create doe_object using DesignOfExperiments\n", - "doe_object = DesignOfExperiments(\n", - " parameter_dict, # dictionary of parameters\n", - " design_gen, # design variable\n", - " measure_class, # measurement variable\n", - " create_model, # model\n", - " prior_FIM=prior, # FIM of prior experiments\n", - " discretize_model=disc_for_measure, # discretized model\n", - ")\n", - "\n", - "# Stochastic programming for optimization; see above for how the function solves twice\n", - "square_result, optimize_result = doe_object.stochastic_program(\n", - " if_optimize=True, # optimize\n", - " if_Cholesky=True, # use Cholesky decomposition\n", - " scale_nominal_param_value=True, # scale model parameter value\n", - " objective_option=\"det\", # objective option\n", - " L_initial=np.linalg.cholesky(prior), # initial Cholesky decomposition\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "======Results Summary======\n", - "This optimization is solved with status: converged\n", - "C solution: 5.0\n", - "T solution:\n", - "579.3896781590472\n", - "300.00008825998646\n", - "300.0001449066281\n", - "300.0002011134464\n", - "300.00026910716224\n", - "300.00037503303196\n", - "300.00058304040493\n", - "300.00119444148544\n", - "300.00410830698996\n", - "The result FIM is: \n", - " [[ 46.26165475 24.02303687 -111.13766257 -98.84248628]\n", - " [ 24.02303687 56.00005105 -41.78107762 -257.31551935]\n", - " [-111.13766257 -41.78107762 290.39184707 177.30569633]\n", - " [ -98.84248628 -257.31551935 177.30569633 1245.5926873 ]]\n", - "Four design criteria log10() value:\n", - "A-optimality: 3.2143791799119263\n", - "D-optimality: 6.214368093237916\n", - "E-optimality: 0.007877626397731468\n", - "Modified E-optimality: 3.1198074131681715\n" - ] - } - ], - "source": [ - "# Results summary\n", - "print(\"======Results Summary======\")\n", - "print(\"This optimization is solved with status:\", optimize_result.status)\n", - "print(\"C solution:\", pyo.value(optimize_result.model.CA0[0]))\n", - "print(\"T solution:\")\n", - "for t in t_control:\n", - " print(pyo.value(optimize_result.model.T[t]))\n", - "\n", - "print(\"The result FIM is: \\n\", optimize_result.FIM)\n", - "print(\"Four design criteria log10() value:\")\n", - "print(\"A-optimality:\", np.log10(optimize_result.trace))\n", - "print(\"D-optimality:\", np.log10(optimize_result.det))\n", - "print(\"E-optimality:\", np.log10(optimize_result.min_eig))\n", - "print(\"Modified E-optimality:\", np.log10(optimize_result.cond))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 6: Method for Exploratory Analysis through Enumeration\n", - "\n", - "This method conducts exploratory analysis using enumeration. \n", - "It allows a user to define any number (dimensions) of design variables." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Specify user inputs" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [], - "source": [ - "# Design variable ranges as lists\n", - "design_ranges = {\n", - " \"CA0[0]\": [1, 3, 5],\n", - " (\n", - " \"T[0]\",\n", - " \"T[0.125]\",\n", - " \"T[0.25]\",\n", - " \"T[0.375]\",\n", - " \"T[0.5]\",\n", - " \"T[0.625]\",\n", - " \"T[0.75]\",\n", - " \"T[0.875]\",\n", - " \"T[1]\",\n", - " ): [300, 500, 700],\n", - "}\n", - "\n", - "# Choose from 'sequential_finite', 'direct_kaug'\n", - "# sensi_opt = \"sequential_finite\"\n", - "sensi_opt = \"direct_kaug\"" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The prior information FIM:\n", - " [[22.52943024, 1.84034314, -70.23273336, -11.09432962], [1.84034314, 18.09848116, -5.73565034, -109.15866135], [-70.23273336, -5.73565034, 218.94192843, 34.57680848], [-11.09432962, -109.15866135, 34.57680848, 658.37644634]]\n", - "Prior Det: 1.9558434494323278e-08\n" - ] - } - ], - "source": [ - "# Add prior information\n", - "prior_pass = [\n", - " [22.52943024, 1.84034314, -70.23273336, -11.09432962],\n", - " [1.84034314, 18.09848116, -5.73565034, -109.15866135],\n", - " [-70.23273336, -5.73565034, 218.94192843, 34.57680848],\n", - " [-11.09432962, -109.15866135, 34.57680848, 658.37644634],\n", - "]\n", - "\n", - "# Print prior information\n", - "print(\"The prior information FIM:\\n\", prior_pass)\n", - "print(\"Prior Det:\", np.linalg.det(prior_pass))" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: =======Iteration Number: 1 =====\n", - "INFO: elapsed time: 1.5\n", - "INFO: This is run 1 out of 9.\n", - "INFO: The code has run 1.4800095079999664 seconds.\n", - "INFO: Estimated remaining time: 5.180033277999883 seconds\n", - "INFO: =======Iteration Number: 2 =====\n", - "INFO: elapsed time: 0.8\n", - "INFO: This is run 2 out of 9.\n", - "INFO: The code has run 2.2786834119997366 seconds.\n", - "INFO: Estimated remaining time: 4.557366823999473 seconds\n", - "INFO: =======Iteration Number: 3 =====\n", - "INFO: elapsed time: 0.8\n", - "INFO: This is run 3 out of 9.\n", - "INFO: The code has run 3.0774706169995625 seconds.\n", - "INFO: Estimated remaining time: 3.846838271249453 seconds\n", - "INFO: =======Iteration Number: 4 =====\n", - "INFO: elapsed time: 0.6\n", - "INFO: This is run 4 out of 9.\n", - "INFO: The code has run 3.6389742199989996 seconds.\n", - "INFO: Estimated remaining time: 2.9111793759991995 seconds\n", - "INFO: =======Iteration Number: 5 =====\n", - "INFO: elapsed time: 0.8\n", - "INFO: This is run 5 out of 9.\n", - "INFO: The code has run 4.463020823998704 seconds.\n", - "INFO: Estimated remaining time: 2.231510411999352 seconds\n", - "INFO: =======Iteration Number: 6 =====\n", - "INFO: elapsed time: 0.8\n", - "INFO: This is run 6 out of 9.\n", - "INFO: The code has run 5.244051230999503 seconds.\n", - "INFO: Estimated remaining time: 1.4983003517141438 seconds\n", - "INFO: =======Iteration Number: 7 =====\n", - "INFO: elapsed time: 0.6\n", - "INFO: This is run 7 out of 9.\n", - "INFO: The code has run 5.848174373999427 seconds.\n", - "INFO: Estimated remaining time: 0.7310217967499284 seconds\n", - "INFO: =======Iteration Number: 8 =====\n", - "INFO: elapsed time: 0.7\n", - "INFO: This is run 8 out of 9.\n", - "INFO: The code has run 6.5564294039986635 seconds.\n", - "INFO: Estimated remaining time: 0.0 seconds\n", - "INFO: =======Iteration Number: 9 =====\n", - "INFO: elapsed time: 0.9\n", - "INFO: This is run 9 out of 9.\n", - "INFO: The code has run 7.479818032998082 seconds.\n", - "INFO: Estimated remaining time: -0.7479818032998082 seconds\n", - "INFO: Overall wall clock time [s]: 7.479818032998082\n" - ] - } - ], - "source": [ - "# Create doe_object using DesignOfExperiments\n", - "doe_object = DesignOfExperiments(\n", - " parameter_dict, # dictionary of parameters\n", - " design_gen, # design variable\n", - " measure_class, # measurement variable\n", - " create_model, # model\n", - " prior_FIM=prior_pass, # FIM of prior experiments\n", - " discretize_model=disc_for_measure, # discretized model\n", - ")\n", - "# Grid search\n", - "all_fim = doe_object.run_grid_search(\n", - " design_ranges, # range of design variables\n", - " mode=sensi_opt, # solver option for sensitivity\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 7: Results through Sensitivity Curves and Heatmaps" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 1D Sensitivity Curve\n", - "\n", - "1D sensitivity curves can be drawn by one design variable and fixing other design variables." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " CA0[0] \\\n", - "0 1.0 \n", - "1 1.0 \n", - "2 1.0 \n", - "3 3.0 \n", - "4 3.0 \n", - "5 3.0 \n", - "6 5.0 \n", - "7 5.0 \n", - "8 5.0 \n", - "\n", - " (T[0], T[0.125], T[0.25], T[0.375], T[0.5], T[0.625], T[0.75], T[0.875], T[1]) \\\n", - "0 300.0 \n", - "1 500.0 \n", - "2 700.0 \n", - "3 300.0 \n", - "4 500.0 \n", - "5 700.0 \n", - "6 300.0 \n", - "7 500.0 \n", - "8 700.0 \n", - "\n", - " A D E ME \n", - "0 918.207526 5.129865 0.002829 2.402240e+05 \n", - "1 917.979819 0.052028 0.000288 2.358410e+06 \n", - "2 917.951300 0.000610 0.000020 3.457451e+07 \n", - "3 920.297448 415.446336 0.025426 2.676791e+04 \n", - "4 918.248082 4.208215 0.002590 2.624381e+05 \n", - "5 917.991412 0.048511 0.000174 3.907348e+06 \n", - "6 924.477291 3205.559576 0.070438 9.688727e+03 \n", - "7 918.784607 32.467061 0.007192 9.455956e+04 \n", - "8 918.071634 0.373747 0.000482 1.408681e+06 \n" - ] - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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KbZ169Zh4sSJUCgU+O677/DRRx8p93311VcAgAcPHsDV1bVM9RNRxWLSRUTV2smTJwttCw4ORteuXYvdXxHWrFmDSZMmQQiB5cuXY8aMGVqJg4g0h0kXEZGOWbVqFaZMmQIA+OGHH/DBBx9oOSIi0gQmXUREOuSnn37C1KlTlX9///33tRwREWkKB9ITEemIlStXKlu1Vq9ezYSLqIph0kVEpAMCAwMxffp0yGQyrFu3DuPHj9d2SESkYexeJCLSstjYWMycOROSJGHjxo0YPXq0tkMionLAli4iIi0TQij/fPjwoZajIaLywqSLiEjL6tatq5x3a+7cufjhhx+0HBERlQcmXUREOmDu3LmYO3cuAGD69OkaXTKIiHQDky4iIh3x1VdfYfr06RBCYMKECdixY4e2QyIiDWLSRUSkQ5YvX45x48YhLy8PI0eOxIEDB7QdEhFpCJMuIiIdIkkS1qxZg2HDhiEnJweDBw/G8ePHtR0WEWkAky4iIh0jk8kQFBSEfv36ITMzEwMGDMDp06e1HRYRqYlJFxGRDjIwMMD27dvRrVs3pKWloU+fPoiIiNB2WESkBiZdREQ6ysjICH/88Qc6dOiA58+fo0ePHoiMjNR2WERURpyRnojoH7p06aKcsLQ8jR07FmPHji2xjKmpKU6dOlXusRBR+WPSRURUji5evAgfHx8AwPz589G7d2+N1Dtv3jyEhIQgKytLI/URUflj0kVEVI5SUlIQFhYGAHjy5InG6r1+/bqyXiKqHCRREW3oRERERNUcB9ITERERVQAmXUREREQVgEkXERERUQVg0kVERERUAZh0EREREVUAJl1EREREFYBJFxEREVEFYNJFREREVAGYdBERERFVACZdRERERBWASRcRERFRBfg/49AlAORjCQEAAAAASUVORK5CYII=\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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IlJNlXYEB4CoqCiMGDECV65ceb7TF/ouFO2+TZs22L59O9zd3TU9nF5iaKLa5kh4PD757SZSsvJhKJXgw15NMTnQHYYGGjdgExFVmSrtCA4ATZo0wT///IO///4bR48eRUREBDIzM2FpaQkPDw/06dMHPXr00MahiEjH0nIKEHwwHL9djQMANKtnhW+G+8GrgUzHlRERVS6ttDQRW5qodjgTmYTZe27gSVoupBLg3UB3fNirKUwMDXRdGhFRhVR6S1N4eDju3bsHBwcHtG/f/qXbX7hwAUlJSWjSpAk8PT0rckgi0qHsfDm++Os2tl18CABwszXH8mF+8Hetef0TiYjKonZoys7ORu/evZGcnIwTJ06o9BghBN544w00aNAAd+/ehYmJidqFEpFuXH7wFLN2hyImJRsAMKaDK+b0aw5zY63N901EVC2o3WNzx44dePLkCSZMmICOHTuq9JiOHTti4sSJiI2Nxc6dO9UukoiqXm5BIZb8dRtD111ATEo2GshM8cs77RD8ujcDExHVSmqHpv3790MikeD9999X63EffvghhBDYu3evuockoioWFpeGAavPYt3p+xACGOrfEIdndEWnJmUPYEtEVNOp/efitWvXUL9+fTRv3lytxzVt2hROTk64du2auockoipSUKjAmhP38N3xSMgVAnaWJlgy2AdBnvV0XRoRkc6pHZqSk5Ph5+dXoYM1aNAAN27cqNBjiahyRSZkYNbuUNx4lAYAeMXHEYsH+sDGwljHlRER6Qe1Q5OpqSlycnIqdLCcnBwYG/MDmEifFCoENp6NxldH7yJfroDMzAifve6FAX4NOMkuEdEL1A5N9evXx71795CXl6fWXXB5eXm4d+8eXFxc1D0kEVWShynZ+Gh3KEIePAUAdGtmj6VDfFHP2lTHlRER6R+1O4J36dIFubm52LNnj1qP2717N3JyctClSxd1D0lEWiaEwC+XYtB35WmEPHgKC2MDfDnYB5vGtmFgIiIqg9qhaezYsRBCYM6cOYiNjVXpMQ8fPsTs2bMhkUgwZswYtYskIu2JT8vFmE3/4NN9YcjOL0S7RjY4/GFXvNnWhZfjiIjKoXZo6tixI4YOHYrHjx+jXbt22L17NxQKRanbKhQK7Nq1C+3bt0dCQgKGDBmCTp06aVw0EalPCIH91+LQe8UpnI5IgomhFP951RM7JraH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MHDgwDL3Y2Fhgfr16yM6OhqFhYUl+jUV9WUq6tv0byYmJsXuuNOmxIzyA5O62xEREZHmql1omjBhQqnLT58+jcjISAwYMAD29vZwc3N76b4CAwOxc+dOnDt3Dl27di227siRI8ptqpqDVemBsKLbERERkeYkQojyus5UG2PHjsXmzZtx4cIFtG/fvti65ORkJCcnw87ODnZ2dsrlJ06cQI8ePdClSxf873//g7GxMQDg77//RlBQELp06aLy4Jbp6emQyWRIS0tTdiKvqEKFQOelxxGflltqvyYJAEeZKc7O6QEDqUSjYxEREdVm6nx/V7s+TRWxevVqtGjRAqtXry62vHv37njnnXdw5swZtGrVCrNnz8aYMWPQv39/WFtb44cfftBJvQZSCRa+5gngeUB6UdG/F77mycBERERUhWpFaCrPunXrsGrVKkgkEqxatQp//vknXnvtNYSEhMDT01NndfX1ro8fRraGo6z4JThHmSl+GNkafb3r66gyIiKi2qnGXJ7TNW1enntRaSOCs4WJiIhIO9T5/q52HcFrGwOpBB3cbXVdBhERUa1X6y/PEREREamCoYmIiIhIBQxNRERERCpgaCIiIiJSAUMTERERkQoYmoiIiIhUwNBEREREpAKGJiIiIiIVMDQRERERqYAjgmtJ0Ww06enpOq6EiIiIVFX0va3KrHIMTVqSkZEBAHB2dtZxJURERKSujIwMyGSycrfhhL1aolAo8PjxY1hZWUEi0e6Euunp6XB2dkZsbKxWJwOmqsNzWL3x/FV/PIfVX2WdQyEEMjIy0KBBA0il5fdaYkuTlkilUjRs2LBSj2Ftbc03ezXHc1i98fxVfzyH1V9lnMOXtTAVYUdwIiIiIhUwNBERERGpgKGpGjAxMcHChQthYmKi61KogngOqzeev+qP57D604dzyI7gRERERCpgSxMRERGRChiaiIiIiFTA0ERERESkAoYmIiIiIhUwNFWB1NRUvP/+++jQoQMcHR1hYmICJycn9OjRA3v37i11vpv09HTMnDkTrq6uMDExgaurK2bOnFnu3Hbbt29H27ZtYWFhgbp16+KVV17B5cuXK/Op1VrLli2DRCKBRCLBxYsXS92G51C/uLm5Kc/Zv38mT55cYnueP/21b98+BAUFwdbWFmZmZmjUqBFGjBiB2NjYYtvxHOqXn3/+ucz3YNFP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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Extract criteria from FIM\n", - "all_fim.extract_criteria()\n", - "print(all_fim.store_all_results_dataframe)\n", - "\n", - "# Draw 1D sensitivity curve\n", - "# This problem has two degrees of freedom; to draw a 1D curve, it needs to fix one dimension\n", - "fixed = {\"'CA0[0]'\": 5.0}\n", - "\n", - "all_fim.figure_drawing(\n", - " fixed,\n", - " [\n", - " (\n", - " \"T[0]\",\n", - " \"T[0.125]\",\n", - " \"T[0.25]\",\n", - " \"T[0.375]\",\n", - " \"T[0.5]\",\n", - " \"T[0.625]\",\n", - " \"T[0.75]\",\n", - " \"T[0.875]\",\n", - " \"T[1]\",\n", - " )\n", - " ],\n", - " \"Reactor case\",\n", - " \"T [K]\",\n", - " \"$C_{A0}$ [M]\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Heatmaps\n", - "\n", - "Heatmaps can be drawn using two design variables and fixing other design variables." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Interpreting Heatmaps\n", - "\n", - "A heatmap shows the change of the objective function (the experimental information content) in the design region. \n", - "\n", - "Horizontal and vertical axes are two design variables, while the color of each grid shows the experimental information content.\n", - "\n", - "The color of each grid is based on a gradient of information. A darker color refers to an area with more information content whereas the lighter color refers to an area with less information content." - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "image/png": "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cvD333+jXbt26NevH0xNTRXK/vXXXwAePxDg6elZbX2yxeU3btxQ2L9kyRLMnz8flZWVNfblyWAiPT0d5eXlaN++fZPlekpPT0dlZSV0dHRgb29fbZkePXoAgPyHqqpnn322Qe3W91oEQcA777yDVatW1Vruyc+sf//+eP7553Hy5ElYWVnB19cXL774Iry9vdGnTx+lheay+7lz504cO3as2vozMzMBKN/P5nLjxg0cOnQIgHJg9vLLL8PU1BS5ubmIi4vDK6+8olKdsntpbW0NAwODass09L7L9td0XmM4ODgo7evYsaNKx4uLixX2N+T71FwMDQ0xZswYbNy4Edu3b8esWbMAAHfu3MHevXuhra2N8ePHN3s/iFoLjpi1MbI8ZomJibh69SqOHj0KIyMjfPjhh4iOjlYoW1BQAAD4559/kJiYWO0me8KypKREfl5CQgI++eQTSCQSLFmyBKmpqSguLkZlZSUEQUBwcDAAoLy8XH6O7GlAWQ6rpiD7serYsWONr8Lp1KkTAKCoqKja4zX9sNemIdeyZcsWrFq1CgYGBli1ahXS0tLw4MEDCI+XG8hHj578zDQ0NPD7779j9uzZ0NPTw6+//ooPPvgAffv2hZ2dncITgcC/9zM9Pb3G+3n9+nUAivezJjk5OfIRmCe32p7Aq2rr1q2orKxEnz59lIJYbW1tjBkzRv75qEp23y0sLGosU9d9r+ncus5rDH19faV9T35vazsuCILC/oZ8n5rTlClTAPw7QgY8fuq1vLwcw4YNg5mZWYv0g6g14IhZGzdgwACsW7cOr732GmbPno1hw4bB2NgYwON/6QKPp5SqBm21kaU4+Oijj/Dxxx8rHa8uRYVsaq269A4NJev/P//8A0EQqg3Obt++rdB+U2jItcg+s5UrV2LatGlKx2tK62FqaoqIiAh88cUXOHv2LBISEhATE4NDhw4hICAAhoaGGD16NIB/P49169bJp+Qao7S0FImJiUr7tbRU/2tFFnCdPn261vdI/vrrrygsLJR/N2sju87c3Nway9R13//55x906dJFab+szqb8vjSHhn6fmounpyecnJxw+vRpnD9/Hj179uQ0JlENOGJGGDFiBF544QXk5eUhPDxcvr979+4AoJS7qS6yXGgeHh7VHn9ybZmMo6MjtLW1kZ+fj8uXL6vUTl0vhO7atSs0NDRQ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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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hdb9ym455XFVGVuCiIiIqrn09HS1MUHaWoGAx9PLgcczrr7//ns0aNAAANC9e3fs3LkTfn5++OijjzBt2rSKD/r/NWnSxOgyZDIZ/vzzT73P0ykJMkWATzI0WCIiohrFRGOCHBwcdBoYrWy98ff3VyVASq1bt0aTJk2QmpqKzMzMUgcfK8soraVH2TVXWkvR065evarTcdrIZDIIISCTGfYQNZ2SIGMC1MbQYImIiGoUE40J0lXz5s0BoNQER7n90aNHpR5ja2uL+vXr48qVKyguLtYYF6QcC6QcG1SeK1euaN3+9ddf49///jdatmyJ119/HS1btoSrqyvu3r2LixcvYtWqVbh48SL+85//YOTIkTrV9TSdu8M6duyIHTt2GFTJk1566SX8+uuvRpdDRERE+unVqxcA4OLFixr7CgsLkZqaCltbW9StW7fMcoKCgrB9+3YkJiZqTJGPiYlRHaMLT09PjW0///wzFixYgJkzZ2oM1Pbx8UG3bt3w2muvYe7cuXjnnXfQvn17reWUR+ckSC6XG1SBtnKIiIgIxq8YrWdLkLe3N/r27YsDBw5g7dq1CAkJUe17//33kZmZibFjx6rWD8rIyEBGRgZcXFzg4uKiOnbKlCnYvn07Fi5ciJ9//hnW1tYAHi+WGBMTgx49ehg1Pf69996Do6MjPvjggzKPW7p0KTZs2ID33nsPffr00bsenZKgIUOGwNfXV+/CtenevbvajSQiIjJbxo4JMmC541WrVqFr16547bXX8P3336NFixb47bffcPjwYXh6emL58uWqY6OiohAZGYnw8HBERESotvfq1QshISFYu3Yt2rVrh4EDB6oem+Hg4FDuDLPynDlzBs2bN9c6Bf9JVlZW8Pb2NriHSack6PvvvzeocG3ee+89k5VFRERE+vH29sbp06exaNEi7N+/HwcOHICbmxveeOMNLFq0CPXq1dOpnDVr1sDPzw9r1qzBypUrYWdnh8GDB+Pdd981epFEIQSuXLmCkpISWFiUPmiquLgYV65cgaEPv6i0x2YkJycbfVOqAz42w7zwsRlmho/NMAuV+tiMzoCDEYvVZBcBihMVG6sUnnvuORw5cgTz58/HkiVLSj1u0aJFWLJkCXr37o2ff/5Z73p0HpNe3gqSZfn99991HiBFRERkNky0WGJN8+9//xsymQxLly5Fly5dsGnT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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# This problem has two degrees of freedom. Fixing dimensions are not necessary for drawing a heatmap\n", - "\n", - "fixed = {}\n", - "all_fim.figure_drawing(\n", - " fixed, [\"CA0[0]\", \"T[0]\"], \"Reactor case\", \"$C_{A0}$ [M]\", \"T [K]\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Grid Search for 3 Design Variables" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [], - "source": [ - "# Define design ranges\n", - "design_ranges = {\n", - " \"CA0[0]\": list(np.linspace(1, 5, 2)),\n", - " \"T[0]\": list(np.linspace(300, 700, 2)),\n", - " (\n", - " \"T[0.125]\",\n", - " \"T[0.25]\",\n", - " \"T[0.375]\",\n", - " \"T[0.5]\",\n", - " \"T[0.625]\",\n", - " \"T[0.75]\",\n", - " \"T[0.875]\",\n", - " \"T[1]\",\n", - " ): [300, 500],\n", - "}\n", - "\n", - "# Choose from 'sequential_finite', 'direct_kaug'\n", - "sensi_opt = \"direct_kaug\"" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: =======Iteration Number: 1 =====\n", - "INFO: elapsed time: 0.8\n", - "INFO: This is run 1 out of 8.\n", - "INFO: The code has run 0.8139118879998932 seconds.\n", - "INFO: Estimated remaining time: 2.4417356639996797 seconds\n", - "INFO: =======Iteration Number: 2 =====\n", - "INFO: elapsed time: 0.8\n", - "INFO: This is run 2 out of 8.\n", - "INFO: The code has run 1.6158038199992006 seconds.\n", - "INFO: Estimated remaining time: 2.693006366665334 seconds\n", - "INFO: =======Iteration Number: 3 =====\n", - "INFO: elapsed time: 0.6\n", - "INFO: This is run 3 out of 8.\n", - "INFO: The code has run 2.2149686929988093 seconds.\n", - "INFO: Estimated remaining time: 2.2149686929988093 seconds\n", - "INFO: =======Iteration Number: 4 =====\n", - "INFO: elapsed time: 1.0\n", - "INFO: This is run 4 out of 8.\n", - "INFO: The code has run 3.1933937759986293 seconds.\n", - "INFO: Estimated remaining time: 1.9160362655991774 seconds\n", - "INFO: =======Iteration Number: 5 =====\n", - "INFO: elapsed time: 0.6\n", - "INFO: This is run 5 out of 8.\n", - "INFO: The code has run 3.7590698399981193 seconds.\n", - "INFO: Estimated remaining time: 1.253023279999373 seconds\n", - "INFO: =======Iteration Number: 6 =====\n", - "INFO: elapsed time: 0.8\n", - "INFO: This is run 6 out of 8.\n", - "INFO: The code has run 4.590044279998438 seconds.\n", - "INFO: Estimated remaining time: 0.6557206114283483 seconds\n", - "INFO: =======Iteration Number: 7 =====\n", - "INFO: elapsed time: 0.7\n", - "INFO: This is run 7 out of 8.\n", - "INFO: The code has run 5.270455575998312 seconds.\n", - "INFO: Estimated remaining time: 0.0 seconds\n", - "INFO: =======Iteration Number: 8 =====\n", - "INFO: elapsed time: 0.7\n", - "INFO: This is run 8 out of 8.\n", - "INFO: The code has run 5.959630374997687 seconds.\n", - "INFO: Estimated remaining time: -0.6621811527775208 seconds\n", - "INFO: Overall wall clock time [s]: 5.959630374997687\n" - ] - } - ], - "source": [ - "# Create doe_object using DesignOfExperiments\n", - "doe_object = DesignOfExperiments(\n", - " parameter_dict, # dictionary of parameters\n", - " design_gen, # design variable\n", - " measure_class, # measurement variable\n", - " create_model, # model\n", - " prior_FIM=prior_pass, # FIM of prior experiments\n", - " discretize_model=disc_for_measure, # discretized model\n", - ")\n", - "\n", - "# Run grid search\n", - "all_fim = doe_object.run_grid_search(\n", - " design_ranges, # range of design variables\n", - " mode=sensi_opt, # solver option for sensitivity\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Draw 1D Sensitivity Curve" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "# FIM criteria\n", - "test = all_fim.extract_criteria()" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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2yIismLFCnnw4IFOWVJSkixevFgASKdOnXSWnTlzRgCIq6urXLlyRWdZbGysrFy5Uu7evatTPnz4cAEgXbt21Tk+T58+lcGDBwsAGTp0aJ7bV5zi4+O1ifipU6dERCQuLk7Mzc0FgJw5c6bAbS5btkwAiLW1tezfv19bHh4erk1W2rRpo7deVhJjYmIin3/+ufY7mpiYKK+88or2+/bP70vHjh0FQK6/FOSV+P2zz+joaGnbtq0AkD59+oiFhYVs2LBBu97du3elTp06AkC++eYbvXYL+n0SKVjil9d2Zcna31u3btVblpaWJg4ODgJALl26lGMbVDYx8aMSERUVJStWrJB+/fqJu7u7mJqaioODg/Ts2VP27t1b4PaePn0qM2fOlLp164qpqak4OjrKkCFD8vxP58CBA9K/f39xcnISU1NTqVmzpgwcOFDOnz+vraPRaGT37t3yxhtvSJMmTcTGxkbMzc2ladOmsnjx4mx/Ky8t2SV+sbGx8scff4iHh4f2N/bnpaamSvXq1cXIyEjOnj2bbbtZV5x++umnfMcyatQoAaB3pbB3794CQMaNG5evdvKT+GWddLKuJD0vMTFRHB0dBYCsXr1aZ1nW/urXr1++Yvmngm5LXnx9fQWA3L9/X1u2ceNGASDTp0/PVxvBwcHa/RUXF6e3PDExUVxdXUVRFLl9+3axxJ0fWVf36tWrp1P+0ksv5XjscqPRaMTV1VUAyJdffqm3/P79+9orf3/99ZfOsqwkpn///nrrZf08AJAffvhBZ1lxJH4DBgzQW7Zv3z7tetnth6xfzLKLNzfZfZ9ESibxW716dY7bt3XrVgEgLVu2LFD8VDYw8aMS8e233woAcXFxkdGjR8ucOXNk1KhR2qsBn376ab7bioqKkvr16wsA8fHxkRkzZsiIESPE1NRULCws5MSJE9mul3Vly9nZWSZOnChz586VcePGScOGDWXdunXaesnJydqhxx49esisWbPk7bff1vbZqlUrvSsFpSXr5JLdR6VSycsvvyxPnz7VWefgwYPauHOydu1aASDjx4/XW3b16lWZP3++DBo0SDp27KgdAso6KT9/lSIpKUl7hezatWv52qa8Er/4+HhRqVQCQG7cuJFtnblz5woAefnll3XKs/bXL7/8kq9YnleYbcly+vRpee+996Rfv37SoUMH7T6rWrWqANAZ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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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ixYtSi0UJ4QQX375pQAgXFxcxJQpU8S7774rrK2thZGRkTh48KA2T7fGeFbSxWuoX2bOnCnMzMzEq6++KsaPHy8++OAD0b17dyFJkjAwMCj1Jc7rp59u3rwpHBwcBADxyiuviA8++EB06dJFABCurq4iMTFRWZbXUL81a9ZMABCXLl0qt4w+XkMmXVVARESEGDFihPD29hbW1tbC0NBQODg4iB49eog1a9aUWFG3WFpamnj//feFs7OzMDIyEs7OzuL9998vs8es2KpVq0SrVq2EmZmZsLGxET169BCnT5+uzFOr0Z6VdAnBa6hPwsPDxaBBg4SHh4ewsrISRkZGwsnJSQwePFicOnWqzGN4/fRTfHy8GDFihKhXr57yuoSFhYmkpKRSZXkN9dOpU6cEANGmTZvnltW3a8gHXhMRERFpASfSExEREWkBky4iIiIiLWDSRURERKQFTLqIiIiItIBJFxEREZEWMOkiIiIi0gImXURERERawKSLiIiISAuYdBERVYLw8HBIklTi9eeff2qs/j59+pSou/iB20Skv5h0EVGN9t/ESJVXp06dVK7f2toaHTt2RMeOHVG3bt0S+/7888/nJkwrVqyAgYEBJEnCt99+q9zu4+ODjh07olWrVhU9ZSLSEUNdB0BEpEsdO3YstS09PR1RUVHl7vf19VW5fn9/f4SHh79QbMuXL8c777wDhUKBH374AVOmTFHumzNnDgAgNjYW7u7uL1Q/EWkXky4iqtGOHj1aalt4eDg6d+5c7n5t+P333zFmzBgIIbBgwQJMnDhRJ3EQkeYw6SIi0jO//vorxo0bBwBYsmQJ3nvvPR1HRESawKSLiEiP/PzzzwgLC1P++91339VxRESkKZxIT0SkJxYvXqzs1Vq6dCkTLqJqhkkXEZEeWLhwISZMmACZTIbly5fj7bff1nVIRKRhHF4kItKxe/fuYdKkSZAkCStWrEBoaKiuQyKiSsCeLiIiHRNCKP979+5dHUdDRJWFSRcRkY45OTkp192aPn06lixZouOIiKgyMOkiItID06dPx/Tp0wEAEyZM0Ogjg4hIPzDpIiLSE3PmzMGECRMghMDo0aOxadMmXYdERBrEpIuISI8sWLAAI0eOhFwux9ChQ7F7925dh0REGsKki4hIj0iShN9//x2DBg1CYWEh+vfvj0OHDuk6LCLSACZdRER6RiaTYdWqVXj11VeRl5eH3r174+TJk7oOi4jUxKSLiEgPGRkZYePGjejSpQuysrLQq1cvREZG6josIlIDky4iIj1lamqKv//+G+3bt0dqaiq6deuGmJgYXYdFRC+IK9ITEf1Hp06dlAuWVqYRI0ZgxIgRzyxjYWGB48ePV3osRFT5mHQREVWiCxcuIDAwEADwySefoGfPnhqpd8aMGThy5Ajy8/M1Uh8RVT4mXURElSgjIwPHjh0DACQlJWms3itXrijrJaKqQRLa6EMnIiIiquE4kZ6IiIhIC5h0EREREWkBky4iIiIiLWDSRURERKQFTLqIiIiItIBJFxEREZEWMOkiIiIi0gImXURERERawKSLiIiISAuYdBERERFpAZMuIiIiIi34f+QeRCpUXGt1AAAAAElFTkSuQmCC\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAk0AAAHZCAYAAACb5Q+QAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACCB0lEQVR4nO3dd1hUx/s28HuXsnQQAVGkCIoKKkXsGnuLxt5jEns02NM030QhMWqMGruxxRiNGjXRJBpb7L2BKKIiFkQsgEiHBXbn/cOX/UloC7uwlPtzXXslnjNn5jkcdvdhZs4ciRBCgIiIiIgKJdV1AEREREQVAZMmIiIiIjUwaSIiIiJSA5MmIiIiIjUwaSIiIiJSA5MmIiIiIjUwaSIiIiJSA5MmIiIiIjUwaSIiIiJSA5MmIiINPXr0CBKJBC4uLnn2SSQSSCSSAo8bNmwY7OzsIJVKIZFI8PPPPwMAXFxcIJFI8OjRo9ILXM04q7LCrm15MmrUqFy/Pzl+/vlnSCQSjBo1SidxVTZMmiiXnA/qN19GRkaoU6cORo4ciStXrug6xGJLSEhAQEAAli1bputQqITe/L38+OOPCy27fPnyXL+/5ZVcLkenTp3w22+/AQBatGiBNm3aoEaNGjqOTH0dOnTI83mR3ysgIEDXoRZo2bJlCAgIQEJCgq5DKVP8XCwZfV0HQOVTvXr1YGdnBwBITExEREQEfv31V+zcuRObN2/Ge++9p+MI1ZeQkIDAwEA4Oztj+vTpug6HNLR9+3YsWrQIenp6+e7ftm1bGUdUuPr16+e7/fDhw3j48CH8/Pxw9uxZyGSyXPvd3NxgZGQEAwODsghTI46OjnBycipwf2H7dG3ZsmWIjIzEqFGjYGVllWe/gYEB6tevDwcHh7IPTgssLS1Rv3591KxZM9d2fi6WDJMmytcXX3yRqzv31atXmDBhAvbs2QN/f3/07t0b1apV012AVCXVr18fd+/exb///ovu3bvn2X/37l1cvXpVVa48uHPnTqHbO3XqlCdhAoBjx46ValzaNGbMmHLdm6QJBweHAq9hRdC/f3/0799f12FUGhyeI7VUq1YNmzZtgqmpKZKTk3HkyBFdh0RV0MiRIwEU3Ju0detWAKgQPaHp6ekAAGNjYx1HQkTqYtJEarOwsIC7uzsAFDg59fDhw+jTpw9q1KgBmUyG2rVrY/To0bh//36+5S9evIjPPvsMfn5+sLOzg0wmg6OjI9577z3cunWr0Hju3r2LCRMmoG7dujA2Nkb16tXRtGlTzJ07F8+ePQPwenJknTp1AACRkZF55lr814EDB9CjRw/Y2NhAJpOhTp06+OijjxAVFZVvDG9O1j1x4gR69uwJGxsbSCQSnDx5stD4i3suOY4ePYrJkyfDy8sL1tbWMDIygpubGyZNmoTHjx/nW392djaWL1+O5s2bw9zcHDKZDLVq1ULr1q0xd+7cfOdzZGdn48cff0Tbtm1hZWUFIyMjNGjQAF9++SWSkpLUPjdtat++PRwdHbF3716kpqbm2ieEwK+//gpjY2MMGDCg0HpSU1Mxb948NGnSBKamprCwsECLFi2wevVqZGdnF3jcqVOn0KVLF1hYWMDS0hIdO3bE0aNHC23rv79rORNzc3pmAgMDVWXenGxc1ETw4r7XAODGjRvo27cvqlWrBjMzM7Ro0QI7d+4sNH5dyMrKwsqVK9G8eXNYWFjA1NQUXl5e+Pbbb5GWlpan/JuTtYUQWLlyJRo3bgwTExPY2dnhvffey/PeyLkOkZGRAIA6derk+mzIef+qO8l/7969aN26NczMzFCjRg188MEHeP78uars5s2b0bRpU5iamsLOzg4TJ05EYmJinjoVCgX+/PNPjBkzBp6enrC0tISJiQkaNmyIzz77DHFxccX6WeY3EVydz8Vhw4ZBIpFgyZIlBda9Z88eSCQSNGvWrFgxVWiC6A3Ozs4CgNi8eXO+++vXry8AiBUrVuTZN23aNAFAABB2dnbCx8dHWFhYCADCwsJCnDt3Ls8xbm5uAoCoXr26aNSokfDy8hKWlpYCgDA2NhYnTpzIN45t27YJQ0NDVTlfX1/RoEEDIZPJcsX/7bffCj8/PwFAyGQy0aZNm1yvN82aNUsVf+3atUXTpk2FiYmJACCqVasmrly5UuDPa/78+UIqlYpq1aqJZs2aidq1axcYe0nPJYeenp6QSCTCzs5OeHt7i0aNGglTU1PVz/HWrVt52hg4cKDq3Nzc3ESzZs2Eo6Oj0NPTEwBEcHBwrvKJiYnirbfeEgCEVCoVzs7OolGjRqo4GzZsKF68eKHW+WlDzs/5zJkzquu0devWXGVOnz4tAIjhw4eLqKgo1fn+V0xMjGjcuLHq3Jo0aSIaNmyoKt+1a1eRnp6e57gdO3YIqVSq+jn7+fkJa2trIZVKxcKFCwUA4ezsnOe4/8bxzz//iDZt2ghHR0cBQDg6Oqp+HwcNGpTnnB8+fJinzpK8106dOiWMjY1VZfz8/IS9vb0AIBYtWlTgz6sw7du3FwDE3Llzi3VcYdLS0kSnTp1U8TRs2FA0adJE9bP39vYWcXFxuY55+PCh6uc/adIkAUA4OTmJpk2bCiMjIwFA2Nraijt37qiOybkOOe8zPz+/XJ8NQUFBeer+r5wYV6xYofrc8PLyUtXp4eEh0tPTxdSpUwUA4erqKjw9PYW+vr4AINq3by+USmWuOnN+d6VSqahZs6bq8yDnPFxcXMTz58/zxPLBBx/k+3mxefNmAUB88MEHqm3qfC4ePnxYABCNGzcu8Fr17t1bABCrVq0qsExlw6SJciksaQoPD1e92U+fPp1r348//igAiDp16uRKFrKzs8W8efNUHyj//TLasmWLuH//fq5tWVlZYuPGjUJfX1+4uroKhUKRa/+VK1eEgYGBACA+++wzkZKSotqXmZkpduzYIc6cOaPaVtiHXo6///5bABD6+vpi27Ztqu2JiYmif//+qg+rtLS0fH9eenp6IjAwUGRlZQkhhFAqlSIjI6PA9kp6LkIIsW7dOhEdHZ1rW1pamvj2228FANGhQ4dc+65evar6cg4LC8u1LzExUWzYsEE8fvw41/Zhw4YJAKJz5865rk98fLwYMGCAAJDrC760vZk03bp1SwAQ3bp1y1Vm/PjxAoD4559/Ck2achJIT09PERERodp+5coVUaNGDdW1eNOTJ0+EmZmZACBmzZqlus6ZmZlixowZqmuoTtKUY+7cuYUmHAUlTSV5r6WkpIjatWsLAOL9998XqampQgghFAqFWLJkiSr+8pA0ffzxxwKAqFWrlrh27Zpq+71790SDBg0EADFkyJBcx+S8x/X19YWBgYHYsWOHal9cXJzo0qWLACCaN2+eJ0kpLDl9s+7Crq2pqanYvn27antUVJSoW7euACD69esnLC0txb///qvaf+PGDWFtba36fX1TQkKC+Pnnn8XLly9zbX/16pWYPHmyACBGjRqVJ5biJE1FnZcQr383nJycBABVAvmmFy9eCH19fWFoaJgn1sqMSRPlkl/SlJiYKI4ePSo8PDwEgDw9NHK5XNjb2ws9Pb1831xC/N8X1S+//KJ2LCNHjhQA8vzV/PbbbwsAYsyYMWrVo07S1KZNGwFATJs2Lc++1NRUYWNjIwCITZs25dqX8/N655131Irlv4p7LkVp27atACCePHmi2rZjxw4BQMyYMUOtOkJCQlQ/r6SkpDz7U1NThaOjo5BIJOLRo0daibsobyZNQgjh4+Mj9PT0xNOnT4UQQmRkZAgrKythZ2cnsrKyCkyawsPDhUQiKfCLYNeuXaovwTfP/csvvxQARLNmzfKNr0mTJmWSNJX0vbZx40YBQDg4OIjMzMw8x/Tp00ejpKmo1397MguSmJio6t3du3dvnv2XL18WAIREIsmV8Oa8xwGIqVOn5jnuxYsXqp6a48eP59qnjaQpv8+NdevWqfb/8MMPefbn9JjmF29hHB0dhYmJiSpxz6HtpEkIIb766qsCz2/p0qVl/sdTecA5TZSv0aNHq8a3LS0t0bVrV9y5cwdDhw7F33//navshQsX8Pz5c/j6+sLHxyff+vr06QPg9ZyQ/7pz5w7mzp2LAQMGoEOHDmjbti3atm2rKhsSEqIqm56erppD8tlnn2nlXFNSUnDhwgUAwJQpU/LsNzExwfjx4wGgwAnw77//frHb1eRcrl69ilmzZqFPnz5o37696mcWHh4O4PXclRyOjo4AXt+NFR8fX2Tde/fuBQAMGTIE5ubmefabmJigS5cuEELgzJkzxYpbW9577z0oFArs2LEDALB//34kJCRg+PDh0Ncv+Kbgo0ePQgiBtm3b5vu7OnDgQNSuXRupqak4d+6cavvhw4cBAJMmTcq33o8++kiT01FbSd9rOfGPHTs23yUMNI3f0dERbdq0KfBlZmamVj1nz55FWloanJyc0Ldv3zz7mzVrhlatWkEIUeBcMn9//zzb7OzsMGjQIAD/97PQprFjx+bZ5u3trfr/MWPG5Nmfc/0ePHiQb53Hjx/HjBkz0KtXL7z11luq93hiYiLS0tJw79497QRfiJzvge3btyMrKyvXvi1btgBAlVs0k0sOUL5y1mkSQuD58+d48OABDAwM0KxZszxLDdy8eRPA6wmTbdu2zbe+nInG0dHRubYvWLAAX375JZRKZYGxvPlFHxERgaysLFhZWRW4/k1xRUREQKlUQiaTwdXVNd8ynp6eAKBKSv6rYcOGJWq3uOcihMDkyZOxZs2aQsu9+TNr1aoVWrRogUuXLsHR0RFdu3bFW2+9hfbt28PX1zfPhPic67l3716cP38+3/pzJs/+93qWleHDh+PTTz/F1q1bMXPmTNVdczl31xUk5/p5eHjku18qlaJBgwZ48uQJwsPD0aNHj1zHFXSdS3L9S6Kk77XSjl9bSw7kxNmgQYMCFyb19PTEhQsX8n0vGhgYoG7duvkel3OOBb2HNeHm5pZnm62treq/FhYWBe5PSUnJtT0zMxNDhw7Fvn37Cm1TnT+ANFWnTh106NABJ06cwMGDB1UJeUhICEJCQmBvb696j1QVTJooX/9dp+ncuXPo168fPvnkE9SoUSPXl1POHSCxsbGIjY0ttN6c26wB4PTp0/jiiy+gp6eHBQsWoE+fPnB2doaJiQkkEgm+/PJLfPvtt7n+wsm5ayu/RehKKudDy9bWtsAP6pxVmpOTk/Pdb2pqWux2S3IuW7duxZo1a2Bqaorvv/8eXbt2hYODg+q29ZEjR+LXX3/N9TOTSqU4ePAgAgMDsW3bNvz555/4888/AQDOzs4ICAjIda1zrmdERAQiIiIKjefN61mQ58+fq/7Kf5OPjw9WrlxZ5PH5sbe3R5cuXXD48GGcPn0aBw8eRIMGDeDn51focTnXOmfh1vzkd63f/B0p7JjSVtL3WnmJvyglvT45qlevDqk0/wGUot7DmjAxMcmzLeezJL99b+4XQuTavnDhQuzbtw/29vZYtGgR3nrrLdjb26vW8mrbti3OnTuXp+entIwZMwYnTpzAli1bVElTTi/TyJEjC1xktrLi8ByppU2bNtiwYQMAYNq0abluOc/pen/33XchXs+TK/D15m34v/76KwDg008/xaxZs+Dh4QFTU1PVh0l+t/nnDBdp85EHOfHHxsbm+QDL8eLFi1zta0NJziXnZ7ZkyRJMmjRJtURBjoKWRqhWrRqWLVuG2NhYBAcHY/ny5ejYsSMiIyMxevRo7NmzR1U25+exYcOGIq+nOr0LGRkZOHfuXJ5XTq9JSeWsxfTee+8hMzNTrbWZcs4tJiamwDL5Xes3f0fyU1h92lTS91p5ib8oJb0+OV6+fFlgr3VOndp8D5eGnPf4zz//jPfeew/Ozs65Fj8t6D1eWgYOHAhLS0vs378fL1++RHZ2NrZv3w6g6g3NAUyaqBj69euHli1bIj4+HkuXLlVtzxnqCA0NLVZ9OevPtG7dOt/9b85lylGvXj0YGhoiISFB7RWfi3r+WN26dSGVSiGXywucX5CzZlTOOlXaUJJzKexnlpWVhdu3bxd6vEQigbe3N6ZOnYrjx49j1qxZAKBKiIGSX8+C5KydU9iXekn0798fZmZmePz4MSQSCd59990ij8m5fmFhYfnuVyqVqtWf37zWOf9f0MrQRf3ctaWk16a8xF+UnDhv375d4B8whb0Xs7KyClynKucc/3tceXs+YWHv8ZcvX2ptSFzd8zY2NsawYcOQmZmJHTt24ODBg3jx4gX8/PxU0xaqEiZNVCw5X7IrVqxQdaW3a9cONjY2CAkJKdYXYU4PSc5fjm86cuRIvkmTsbExunXrBgBYvHhxsdopaCjJzMxM9QGV33BReno6Nm7cCAD5PrqjpDQ5l/x+Zps3by5yyOa/WrZsCQB4+vSpalvOIxe2bduGly9fFqu+smRiYoKPP/4YnTt3xocffghnZ+cij+nWrRskEgnOnj2L4ODgPPv/+OMPPHnyBKampmjTpk2u4wDgxx9/zLfetWvXlvAsiqek77Wc+Ddt2pTvsE5Rc+TKStu2bWFiYoKoqCjVEPKbrl69igsXLkAikaBr16751pHfucTGxmL37t0A/u9nkaOoz4eyVth7fMmSJVAoFFptR53zzpnIvmXLlio7AVylVO/NowqnqMUtlUqlaiHARYsWqbavWbNGABA2Njbijz/+yLMWys2bN8Vnn30mzp49q9r2/fffC+D1YosPHjxQbb98+bJwcHBQ3SL831uy31zbaPbs2ao1Z4R4vW7Ozp07c61tpFQqhbm5uQCQZ52iHDnrNBkYGIhff/1VtT0pKUkMGjSoyHWaCrpduSjFPRd/f38BQLRo0ULExMSoth88eFBYWFiofmZvXr9t27aJr7/+Ok+McXFxqkUE33///Vz7hgwZIgAIHx+fPLe2Z2dnixMnTogRI0aotRaVNvx3yYGiqLNOU6NGjXKtQXXt2jVRs2ZNAUB8/vnneerLWUD0yy+/zLVO0yeffFKm6zSV5L2WkpIiHBwcBAAxevRo1e+xUqkUy5YtK5frNDk4OOT63YuIiFAtezJ06NBcx7y5TpOhoaHYtWuXat/Lly9Ft27dBPB6Acv//rx69eolAIi1a9fmG486Sw4U9zghhDhx4oQAXi9wmV88ffr0EcnJyUKI19dpy5YtwsDAQPUe/+/iucVdckCdz8U3NWrUKNfPuCqtzfQmJk2US1FJkxBCbNq0SQAQ9vb2uRbQe3NFbWtra9GsWTPh6+urWsQNgDh48KCqfGJionB1dRUAhKGhoWjcuLFqxXEPDw8xc+bMAj+Qt27dqvqgNzExEb6+vqJhw4b5Jg1CCDFmzBgBQBgZGQk/Pz/Rvn37PB9Wb8bv6Ogo/Pz8VF+U1apVE5cvXy7w51XSpKm45xIZGan6eRobGwtvb2/h4uIiAIiOHTuKd999N88xP/zwg+q8HBwcRLNmzXKt7u3g4CAiIyNzxZScnCy6du2qOs7JyUm0aNFCNG7cWLWqNIB8V84uDdpMmt5cEVxPT094eXmpvowBiC5duuR7Xtu2bVOt8WRjYyOaNWtWohXBc5Q0aRKi+O81IYQ4fvy4aqVqCwsL0axZM62tCP7mqub5vWbPnq12nWlpaaJjx46qeDw8PISXl5dq9XovLy+1VgR3dnYWfn5+qt/X6tWr55sc/PLLL6q2GjVqpPpsyFlbqqyTpqtXr+a6Tk2bNhW1atUSAMR7772n+plrmjQJod7nYo4lS5aozreqrc30JiZNlIs6SZNcLle9iVevXp1r37lz58SIESOEo6OjMDQ0FNbW1qJJkyZizJgx4sCBA3kW1nv69Kl4//33hY2NjTA0NBR16tQRM2fOFImJiUV+qdy6dUuMHj1aODk5CUNDQ2FjYyOaNm0qAgICxLNnz3KVTU5OFtOmTRMuLi6F/lX9999/i65du4pq1aoJQ0ND4ezsLCZOnJhnxez//rw0SZqKey53794VAwYMEJaWlsLIyEg0aNBABAYGCrlcnu8H5+PHj8V3330nunbtKpycnISRkZGoXr268PX1FfPmzROvXr3KNyaFQiF+/fVX0b17d2FjYyMMDAxEzZo1RYsWLcTnn3+ebxJZWrSZNAnxuufl66+/Fo0aNRLGxsbC1NRUNGvWTKxcuTLfxR9znDhxQnTs2FGYmZkJc3Nz0b59e3H48OESfbFqkjQJUfz3mhBCBAcHi3feeUdYWlqqzjln9WxNkqaiXn379i1WvZmZmWL58uWqP1yMjY1F48aNxbx583L1xuZ48+evVCrF8uXLRaNGjYSRkZGwsbER7777bqELsS5fvlw0adIk1x8EOUlJWSdNQghx6dIl0bVrV2FmZiZMTU2Ft7e3WLFihVAqlVpNmtT9XBTi9R8bOYnr/v378y1TFUiEKGC2HRERUQXw6NEj1KlTB87OzgU+4Jg0c+fOHTRs2BD29vZ48uRJlVtqIAcnghMREVGhNm3aBOD1Eh9VNWECmDQRERFRIR4+fIh169ZBT08PH374oa7D0SmuCE5ERER5TJ8+HZcvX0ZISAjS0tIwYcKEfB8ZU5Wwp4mIiIjyuH79Oi5cuABzc3NMnToVy5Yt03VIOseJ4ERERERqYE8TERERkRo4p0lLlEolnj59CnNz83L3LCMiIiLKnxACycnJqFWrFqTSwvuSmDRpydOnT+Ho6KjrMIiIiKgEoqKiULt27ULLMGnSEnNzcwCvf+gWFhY6joaIiIjUkZSUBEdHR9X3eGGYNGlJzpCchYUFkyYiIqIKRp2pNZwITkRERKQGJk1EREREamDSRERERKQGJk1EREREamDSRERERKQGJk1EREREamDSRERERKSGCp007d27F127dkX16tVhbGyMOnXqYPjw4YiKilLr+ISEBMyZMwdNmjSBubk5bGxs0KxZM6xatQoZGRmlHD0RERFVJBVycUshBCZOnIj169fDzc0Nw4YNg7m5OZ4+fYpTp04hMjKyyEeaJCQkoGnTpnjw4AHatm2LDz/8EHK5HAcPHsSUKVOwd+9eHD16tMjn0BAREVHVUCGTppUrV2L9+vXw9/fH8uXLoaenl2t/dnZ2kXWsX78eDx48wIwZM7B06VLV9szMTLRt2xbHjx/H2bNn8dZbb2k9fiIiIlKfQilw+WE8YpIzYGduhOZ1rKEnLXoFb22rcElTeno6AgMD4erqimXLluVJmABAX7/o03rw4AEA4O2338613dDQEF27dsWVK1cQExOjnaCJiIioRA6FPkPg32F4lvh/02ZqWhph7jse6NGoZpnGUuHGno4ePYr4+Hj069cPCoUCf/zxBxYuXIgff/wRERERatfj6ekJADh06FCu7VlZWfj3339hbGyMVq1aaTV2IiIiUt+h0GeYtC0oV8IEAM8TMzBpWxAOhT4r03gqXE/T1atXAbzuTfLy8sLdu3dV+6RSKWbMmIHFixcXWc+4ceOwdetWLFmyBFevXkWzZs0gl8tx6NAhvHr1Ctu3b4eDg0OBx8vlcsjlctW/k5KSNDgrIiIiepNCKRD4dxhEPvsEAAmAwL/D0NXDvsyG6ipcT1POkNmSJUtgYWGBy5cvIzk5GadPn4a7uzuWLFmCtWvXFlmPsbExTp48iZEjR+LUqVNYvHgxVq5cifv372PEiBFo27ZtoccvWLAAlpaWqldRE8+JiIhIfZcfxufpYXqTAPAsMQOXH8aXWUwVLmlSKpUAXs892rdvH5o1awYzMzO0a9cOe/bsgVQqxZIlS4qsJy4uDl27dsXFixdx4MABJCQk4Pnz5/jxxx+xefNmtGjRAq9evSrw+NmzZyMxMVH1UneZAyIiIipaTLJ6S/+oW04bKtzwnKWlJQDAz88PtWrVyrXP09MTrq6uiIiIQEJCAqysrAqsZ+bMmTh//jxCQkLQpEkTVd3jx4+HQqHApEmTsGzZMgQGBuZ7vEwmg0wm085JERERUS4vUzLVKmdnblTKkfyfCtfTVL9+fQAoMCHK2Z6enl5oPQcOHIC1tbUqYXpTp06dAADXrl0reaBERERUbAqlwMpj9zDvQFih5SR4fRdd8zrWZRMYKmBPU8eOHQEAt2/fzrMvKysLERERMDU1ha2tbaH1ZGZmIiMjA5mZmTA0NMy1LzY2FgDYk0RERFSGYpPlmPHbdZyNiAMAtKhjjUsP4yEBck0Iz5n2PfcdjzJdr6nC9TS5ubmhW7duiIiIwMaNG3PtW7hwIRISEtC/f3/VWk1xcXG4c+cO4uLicpVt06YNsrOz8c033+TaLpfLVdtyEjQiIiIqXecj4vD2ijM4GxEHIwMpvh/UBL992Ao/jvSFvWXuITh7SyOsHelb5us0SYQQ+d3NV67dv38frVu3RkxMDHr16oUGDRogODgYx48fh7OzMy5evAh7e3sAQEBAAAIDAzF37lwEBASo6rh+/TreeustJCcno3nz5mjTpg0yMjJw+PBhPHjwAE2bNsXZs2dhZKTeWGlSUhIsLS2RmJgICwuL0jhtIiKiSkehFFh+7B5WHr8HIQD3GmZYPcIX9WqY5ypTWiuCF+f7u8INzwGve5uuXr2KOXPm4NChQzhy5Ajs7e3h7++POXPmwM7Orsg6vL29ce3aNSxYsADHjh3DqlWroK+vj7p16yIwMBCffPKJ2gkTERERFd+LpAxM2xmMiw9eLxswxK82Avs0grFh7qd96EklaOVWXRch5lIhe5rKI/Y0ERERqe90eCxm/HYdL1MzYWKoh2/7N0J/n9plHkel72kiIiKiiilbocQP/4Zjzcn7EAJoYG+O1e/6ws3WTNehFYlJExEREZWJZ4npmLojGFcevV48+t0WTviqtweMDPSKOLJ8YNJEREREpe7EnRjM3HUdr9KyYCbTx4IBjfGOV62iDyxHmDQRERFRqclSKLH48F2sO/0AANDIwQKrhvvCxcZUx5EVH5MmIiIiKhVPXqVhyo5gBD9OAACMau2C2W83gEy/YgzH/ReTJiIiItK6I7ee49M9N5CYngVzI318P6hJmS9GqW1MmoiIiEhrMrOVWHDwNjafewQA8KptiVUjfOFobaLbwLSASRMRERFpxeOXaZi8Iwg3niQCAMa2rYPPezSAoX6Fe2pbvpg0ERERkcYO3nyGz/bcQLI8G5bGBlg82AtdPWroOiytYtJEREREJZaRpcD8f27jlwuRAABfJyusHOELBytjHUemfUyaiIiIqEQexqVi8vYg3HqaBAD4sL0rPulWHwZ6lWM47r+YNBEREVGx/RXyFF/8cRMp8mxYmxpiyRAvdKxvp+uwShWTJiIiIlJbRpYCgX+HYcflxwCA5i7WWDHcB/aWRjqOrPQxaSIiIiK1RMSkYPL2INx5ngyJBJjcsS6mda4H/Uo6HPdfTJqIiIioSH8EPcGX+0KRlqmAjZkhfhjqjXb1bHUdVpli0kREREQFSsvMxtw/b2H3tScAgFau1bF8mDfsLCr/cNx/MWkiIiKifIW/SIb/r0G4F5MCiQSY1rkepnSqBz2pRNeh6QSTJiIiIspFCIHdV59gzl+hyMhSwtZchuXDvNHazUbXoekUkyYiIiJSSZVn48t9odgbHA0AaFfPBj8M9YaNmUzHkekekyYiIiICANx+lgT/X4PwIC4VUgnwcbf6mNTeDdIqOhz3X0yaiIiIqjghBLZffozAv8OQma2EvYURVgz3QfM61roOrVxh0kRERFSFJWdkYfYfN7H/xjMAQMf6tlgyxBvWpoY6jqz8YdJERERURYVGJ2Ly9iA8epkGfakEn3avj/HtXDkcVwAmTURERFWMEAK/XIjEtwduI1OhhIOVMVYM90FT52q6Dq1cY9JERERUhSSmZ2HW7zdwMPQ5AKBLwxpYPLgJrEw4HFcUJk1ERERVxPWoBEzeHoQnr9JhoCfBrJ4NMaaNCyQSDsepg0kTERFRJSeEwKazD/HdoTvIUgg4Whtj1XBfeDla6Tq0CoVJExERUSWWkJaJT3bfwL+3XwAAejayx8KBTWBpbKDjyCoeJk1ERESV1LXIV5iyPQhPEzNgqCfFl70b4r2WzhyOKyEmTURERJWMUimw/swDfH/4LhRKAZfqJlg1wheNHCx1HVqFpnHS9PjxYwBA7dq1IZVKNQ6IiIiISi4+NRMzd13HybuxAIB3vGphfv9GMDficJymNE6aXFxcUKNGDURHR2sjHiIiIiqhyw/jMXVHMJ4nZUCmL8XcdzwxvLkjh+O0ROOuIUtLSzg7O+ukl2nv3r3o2rUrqlevDmNjY9SpUwfDhw9HVFSU2nUkJydj7ty5aNSoEUxMTGBlZQVfX18EBgaWYuRERETao1QKrDp+D8PWX8DzpAy42ppin38bjGjhxIRJizTuaWrcuDEiIiK0EYvahBCYOHEi1q9fDzc3NwwbNgzm5uZ4+vQpTp06hcjISDg6OhZZz+PHj9GpUyc8ePAAXbp0Qa9evSCXyxEREYHff/8dc+fOLYOzISIiKrnYZDlm7rqOM/fiAAADfBzwTb9GMJVx2rK2afwTnTZtGgYPHoyffvoJY8aM0UZMRVq5ciXWr18Pf39/LF++HHp6ern2Z2dnF1mHQqHAoEGD8PTpUxw7dgwdO3Ysdh1ERES6dP5+HKbtvI7YZDmMDKT4um8jDG5am71LpUTjpGngwIFYuHAh/P39cfPmTbz33nto2LAhjI2NtRFfHunp6QgMDISrqyuWLVuWJ2ECAH39ok9rz549uHLlCr766qs8CZO6dRAREemCQimw8vg9rDh2D0oB1LMzw+p3feFew1zXoVVqGmcGbyYtK1aswIoVKwotL5FINOrFOXr0KOLj4zFq1CgoFAr89ddfCA8Ph5WVFbp06YK6deuqVc9vv/0GABg8eDCioqJw4MABJCQkwM3NDT179oSZmVmJYyQiIiotMUkZmLbzOi48eAkAGOJXG4F9GsHYMG8nAmmXxkmTEKJUy//X1atXAbzuCfLy8sLdu3dV+6RSKWbMmIHFixerXc/Zs2cxY8YMyOVy1T5bW1vs2rULHTp0KPB4uVye65ikpKTingoREVGxnLkXixm/XUdcSiZMDPUwr18jDPCtreuwqgyNb3lTKpXFfmkiJiYGALBkyRJYWFjg8uXLSE5OxunTp+Hu7o4lS5Zg7dq1atczZcoUTJ8+HVFRUYiNjcWKFSuQmJiIfv364dmzZwUev2DBAlhaWqpe6kw8JyIiKolshRKLD9/F+z9dRlxKJhrYm+OvyW2ZMJUxidC066eMTZgwARs2bICxsTEiIiJQq1Yt1b5bt26hSZMmqFOnTpF39BkaGiIrKwt9+/bFvn37cu2bNWsWvvvuO3zzzTf48ssv8z0+v54mR0dHJCYmwsLCouQnSERE9IZniemYtuM6Lj+KBwCMaOGEOb09YGTA4ThtSEpKgqWlpVrf3xVuCW9Ly9dLwPv5+eVKmADA09MTrq6uuH//PhISEtSqp0+fPnn2vfPOOwD+bwgvPzKZDBYWFrleRERE2nTiTgzeXn4Glx/Fw0ymjxXDfTC/f2MmTDqi1VvEoqKicObMGURHRyM9PR1z5sxR7cvKyoIQAoaGhhq1Ub9+fQCAlZVVvvtztqenpxdYJqeeuLi4fMu8WQcREVFZy/r/w3HrTj8AAHjWssDqEb5wsTHVcWRVm1Z6muLi4jB06FDUqVMH7733HmbNmpVnRe3Ro0fD2NgY165d06itnOUBbt++nWdfVlYWIiIiYGpqCltb20Lr6dSpEwAgLCwsz76cbS4uLhrFSkREVFzRCekYuu6CKmH6oJUzfp/UmglTOaBx0pScnIz27dtj9+7dcHBwwKhRo+Dg4JCn3Lhx4yCEwB9//KFRe25ubujWrRsiIiKwcePGXPsWLlyIhIQE9O/fX7XOUlxcHO7cuYO4uLhcZUePHg2ZTIaVK1fmem5ecnIy5s+fDwAYMmSIRrESEREVx9GwF3h7+RkEPU6AuZE+1r7ri8C+jTgcV14IDX355ZdCIpGIQYMGibS0NCGEEG3bthVSqTRXOYVCIUxMTESrVq00bVJEREQIOzs7AUD06tVLfPzxx6JTp04CgHB2dhbPnj1TlZ07d64AIObOnZunnhUrVggAonr16mLcuHHC399fuLi4CABiwoQJxYopMTFRABCJiYmanh4REVUx8iyF+PrvW8L58/3C+fP9os/KMyIyLlXXYVUJxfn+1nhO0549eyCTybBx48ZCVwGXSqWoW7cuHj9+rGmTcHNzw9WrVzFnzhwcOnQIR44cgb29Pfz9/TFnzhzY2dmpVc+UKVPg4uKC77//Hjt37kR2djY8PT3xxRdfYPz48RrHSUREVJSo+DRM3h6EkCeJAIAxbepgVs8GMNSvcPdqVXoaLzlgbGwMd3d3hISEqLa1a9cO58+fh0KhyFW2VatWCA4ORkZGhiZNlkvFuWWRiIgIAA6FPsOne24gOSMblsYGWDzYC109aug6rCqlON/fGvc0GRkZITk5Wa2yz549U93qT0REVFVlZCmw4J/b2HIhEgDg62SFFcN9ULuaiY4jo8Jo3Pfn6emJqKgoREZGFlru+vXrePz4MZo2bappk0RERBXWo7hUDFx7XpUwfdjeFb992IoJUwWgcdI0cuRIKBQKTJgwAWlpafmWefXqFcaOHQuJRIL3339f0yaJiIgqpL9DnqL3yrO49TQJ1UwMsHlUM8zu2RAGepy/VBFoPDw3fvx47NixA0ePHkXjxo0xePBgvHjxAgDw008/ITQ0FNu2bUNcXBy6deuGYcOGaRw0ERFRRZKRpcDX+8Ow/dLrm6GauVTDiuE+qGlZ8A1UVP5o5dlzycnJmDBhAn777TdIJBLkVPnm/w8ZMgSbNm2CqWnlXJyLE8GJiCg/92NT4P9rEO48T4ZEAvh3qIvpXepBn71L5UJxvr+1+sDemzdvYu/evbh58yYSExNhZmYGDw8P9O/fv9LPZWLSRERE/7U3+An+tzcUaZkKVDc1xLJh3mhXr/AnVlDZKtO7597UuHFjNG7cWJtVEhERVTjpmQrM/SsUu64+AQC0cq2O5cO8YWdhpOPISBNaTZqIiIiqunsvkvHRr0G4F5MCiQSY2qkepnauBz2pRNehkYa0ljTJ5XLs3LkThw8fRnh4OJKTk2Fubg53d3fVBHAjI2bYRERUOQkhsPvaE8z5MxQZWUrYmsuwfKg3Wte10XVopCVamdN0/vx5jBw5EpGRkcivOolEAicnJ2zbtg1t2rTRtLlyiXOaiIiqrlR5Nr7aF4o/gl8/AL5dPRssHeINW3OZjiOjopTpnKZbt26ha9euSE9Ph729PcaNG4eGDRuiRo0aiImJwe3bt7Fp0yZERkaiW7duuHTpEho1aqRps0REROXC7WdJmLw9CPdjUyGVAB93q49J7d0g5XBcpaNxT1P//v3x559/YuTIkdi0aRMMDAzylMnKysK4ceOwdetW9OvXD3/88YcmTZZL7GkiIqpahBDYcTkKgX/fgjxbCXsLI6wY7oPmdax1HRoVQ5kuOVC9enUoFAo8f/680DlLGRkZsLe3h1QqRXx8vCZNlktMmoiIqo7kjCx8sTcUf4c8BQB0qG+LpUO8YW1qqOPIqLjKdHguMzMTHh4eRU7yNjIyQv369REWFqZpk0RERDoTGp2IyduD8OhlGvSkEnzWvT7Gt3PlcFwVoHHS1LBhQzx58kStslFRUfD09NS0SSIiojInhMDWi5GYt/82MhVK1LI0wsoRvmjqXE3XoVEZ0XgN9+nTp+PZs2dYvnx5oeVWrFiB58+fY/r06Zo2SUREVKYS07Pgvz0Ic/68hUyFEl0a1sA/09oxYapiNO5pGjFiBKKjo/H555/j1KlT+Oijj9CwYUPY2dkhNjYWt2/fxpo1a3DgwAEsWrSID+wlIqIKJSQqAZN3BCEqPh0GehJ83qMBxratA4mEw3FVTbEmguvp6WneoESC7OxsjespbzgRnIiochFC4Kdzj7Dw4G1kKQRqVzPGqhG+8Ha00nVopEWlNhFcG8/21eLzgYmIiEpFQlomPt1zA0fDXgAAenja47tBTWBpnHdZHao6ipU0KZXK0oqDiIioXAh6/ApTtgcjOiEdhnpSfNm7Id5r6czhOOIDe4mIiABAqRTYcOYBvj98F9lKAefqJlg9wheNHCx1HRqVE0yaiIioyotPzcQnu0Nw/E4MAKB3k5pYMKAxzI04HEf/h0kTERFVaZcfxmPqjmA8T8qAob4UAe94YnhzRw7HUR5aS5oOHz6MQ4cO4cGDB0hJSSlwwrdEIsGxY8e01SwREVGJKJUCa0/dx9Kj4VAoBVxtTLH6XV80rMk7oCl/GidNSUlJ6NevH06dOqXWnXHM3ImISNfiUuSY8dt1nLkXBwDo7+OAef0awVTGARgqmMa/HZ9//jlOnjwJa2trTJgwAT4+PrC1tWVyRERE5dKF+y8xbWcwYpLlMDKQ4us+jTDYrza/t6hIGidNf/zxBwwMDHDq1Ck+V46IiMothVJg5fF7WHHsHpQCqGdnhtXv+sK9hrmuQ6MKQuOkKTU1FfXr12fCRERE5VZMcgam77yO8/dfAgAGN62NwL6eMDHkcBypT+PflgYNGiAxMVEbsRAREWnd2XtxmP5bMOJSMmFiqId5/RphgG9tXYdFFZBU0wr8/f1x//59nDx5UgvhEBERaUe2QonFh+/ivZ8uIS4lEw3szfHX5LZMmKjENE6aRo8ejSlTpmDAgAFYuXIlUlJStBEXERFRiT1PzMCIjZew6kQEhACGN3fCPv82qGtnpuvQqAKTCC08QVcul2P48OH4888/AQC2trYwMTHJv0GJBPfv39e0yXKnOE9JJiKi0nPibgw+3hWC+NRMmBrqYcHAJujjVUvXYVE5VZzvb43nNL148QJdunRBWFiYap2mmJiYAsvzlk4iIioNWQolFh+5i3WnHgAAPGtZYNUIX9SxMdVxZFRZaGWdplu3bqFu3br49NNP4e3tXWbrNO3duxdr1qxBUFAQ0tLSYG9vj5YtW2LRokVwdHQsVl1ZWVlo1qwZQkJCUL9+fdy5c6eUoiYiIm2LTkjH1B3BuBb5CgDwfitnfPF2QxgZ6Ok4MqpMNE6aDh06BCMjI5w8eRK1apVN96cQAhMnTsT69evh5uaGYcOGwdzcHE+fPsWpU6cQGRlZ7KTpm2++QURERClFTEREpeXfsBf4eHcIEtOzYC7Tx3eDmuDtxjV1HRZVQlpZp6lBgwZlljABwMqVK7F+/Xr4+/tj+fLl0NPL/ZdEdnZ2seoLCgrCggULsHTpUkydOlWboRIRUSnJzFZi0aE72Hj2IQCgSW1LrBruC6fq+c+pJdKUxhPBW7dujejoaERGRmorpkKlp6ejdu3asLKywt27d6Gvr1nel5mZCT8/P1haWuL06dOQSqUlGp7jRHAiorITFZ+GyTuCERKVAAAY06YOZvVsAEN9jW8KpyqmTCeCf/rppxg4cCB27dqFIUOGaFpdkY4ePYr4+HiMGjUKCoUCf/31F8LDw2FlZYUuXbqgbt26xaovICAA9+7dQ0hICCepExFVAIdCn+HTPTeQnJENCyN9LB7shW6e9roOi6oAjZOm/v37Y8WKFRg3bhwuXbqEMWPGwM3NDUZGRtqIL4+rV68CAPT19eHl5YW7d++q9kmlUsyYMQOLFy9Wq64rV65g0aJFmD9/Ptzd3YsVh1wuh1wuV/07KSmpWMcTEVHxyLMVmH/gNrZceD2y4eNkhZXDfVC7GofjqGxoPDz33/lERTYokRR7ztGbJk6ciHXr1kFPTw++vr5YvXo1GjZsiODgYEyYMAF37tzBmjVrMGnSpELrkcvl8PX1hYmJCS5evKg6D4lEotbwXEBAAAIDA/Ns5/AcEZH2PYpLxeQdQQiNfv0H6odvueKT7vVhoMfhONJMcYbnNP5tE0IU66VUKjVqL+d4Q0ND7Nu3D82aNYOZmRnatWuHPXv2QCqVYsmSJUXW89VXX+HevXv46aefip34AcDs2bORmJioekVFRRW7DiIiKtr+G0/Re+VZhEYnoZqJAX4a5YfZbzdkwkRlTuPhOU2ToOKytLQEAPj5+eW5Y8/T0xOurq6IiIhAQkICrKys8q0jKCgIS5cuxVdffYXGjRuXKA6ZTAaZTFaiY4mIqGgZWQp8sz8Mv156DABo5lINK4b7oKalsY4jo6qqwqXp9evXB4ACE6Kc7enp6QXWcePGDSgUCgQEBEAikeR6AcDdu3chkUgKbIOIiErX/dgU9Ft9Dr9eegyJBPDv6IYd41syYSKd0rinqax17NgRAHD79u08+7KyshAREQFTU1PY2toWWIe7uzvGjh2b775NmzbB0tISgwYNKvD5eUREVHr2BUfji703kZapQHVTQ/ww1BtvuRf8mU5UVipc0uTm5oZu3brhyJEj2LhxI8aNG6fat3DhQiQkJGDkyJGq9Zvi4uIQFxcHGxsb2NjYAHi9tlTr1q3zrX/Tpk2wt7fHxo0bS/9kiIhIJT1TgYC/buG3q6/niLZ0tcbyYT6oYVE6d2MTFZfGw3N6enrFemm6GCUArFmzBnZ2dhg/fjx69+6NTz75BJ07d8acOXPg7OyM77//XlV21apVaNiwIVatWqVxu0REVDruvUhG39Vn8dvVKEgkwLTO9fDruJZMmKhcqXB3zwGve5uuXr2KUaNG4dq1a1ixYgXu3bsHf39/XL58Gfb2XOSMiKii2H01Cn1WnUP4ixTYmsvw69gWmNHVHXpSLjhM5YvG6zQVJi0tDREREdiwYQM2b96MpUuXYsKECaXVnE7xMSpERMWTKs/GV3+G4o+gaABA27o2+GGoN2zNeWcylZ0yfYxKYUxMTNCkSROsXLkSfn5+GDNmDBwdHdGzZ8/SbJaIiMq5O8+T4P9rEO7HpkIqAWZ2dcdHHepCyt4lKsdKtafpvxwcHODm5obTp0+XVZNlhj1NRERFE0Jg55UoBPx1C/JsJWpYyLBimA9auFbXdWhURZWbnqb/qlmzJq5fv16WTRIRUTmRIs/GF3/cxF8hTwEA7d1tsXSIF6qbcTiOKoYyS5pSU1Nx9+7dEj2yhIiIKrbQ6ERM3h6ERy/ToCeV4NPu9TGhnSuH46hCKZOk6fbt25g5cybS0tLQo0ePsmiSiIjKASEEtl2MxDcHbiMzW4lalkZYOcIHTZ2tdR0aUbFpnDS5uroWuE8IgdjYWKSnp0MIATMzM8yfP1/TJomIqAJIysjCrN9v4J+bzwEAXRra4ftBXqhmaqjjyIhKRuOk6dGjR0WWsbS0RPfu3REYGKh6dhwREVVeN54kwH97EKLi02GgJ8HnPRpgbNs6qmd8ElVEGidNDx8+LHCfRCKBqakpqlfnXRFERFWBEAKbzz3CgoO3kaUQqF3NGKtG+MLb0UrXoRFpTOOkydnZWRtxEBFRBZeYloVP94TgSNgLAEAPT3t8N6gJLI0NdBwZkXZUuAf2EhFR+RP0+BWmbA9GdEI6DPWk+F+vhni/lTOH46hS0XrS9OrVK6SkpKCwNTOdnJy03SwREemAUimw8ewDLDp0F9lKAefqJlg13BeNa1vqOjQirdNK0hQeHo6AgAAcOnQIiYmJhZaVSCTIzs7WRrNERKRDr1Iz8fHuEBy/EwMA6NWkJhYOaAxzIw7HUeWkcdJ0/fp1tG/fXtW7ZGRkBFtbW0ilUm3ER0RE5dCVR/GYuiMYzxIzYKgvxdx3PDCiuROH46hS0zhp+uKLL5CcnIzOnTvjhx9+QKNGjbQRFxERlUNKpcDaU/ex9Gg4FEoBVxtTrBrhC49afOYmVX4aJ03nz5+HmZkZ9u3bB1NTU23ERERE5VBcihwzd4XgdHgsAKCfdy3M698YZjLeU0RVg8a/6UqlEvXr12fCRERUiV188BJTdwQjJlkOIwMpvu7TCIP9anM4jqoUjZMmb29vPHjwQBuxEBFROaNQCqw6HoHlx8KhFEBdOzOsHuGL+vbmug6NqMxpPFt79uzZePbsGbZu3aqNeIiIqJyISc7Ae5su4Yd/XydMg5vWxl+T2zBhoipL456mnj17Ys2aNfjoo48QFBSEsWPHws3NDcbGxtqIj4iIdODsvThM/+064lLkMDbQw7f9G2GAb21dh0WkUxJR2CqUatDT0yteg5V0naakpCRYWloiMTERFha8i4SIKqZshRLLj93DqhMREAJoYG+OVSN8UdfOTNehEZWK4nx/a9zTVNycS8McjYiISsnzxAxM3RmMyw/jAQDDmzti7jueMDIo3h/HRJWVVu6eIyKiiu3k3RjM3BWC+NRMmBrqYf6Axujr7aDrsIjKFS6uQURUhWUplFh6NBxrT94HAHjUtMDqd31Rx4bLyBD9F5MmIqIq6mlCOqbsCMa1yFcAgPdaOuN/vRpyOI6oAEyaiIiqoGO3X+Dj3SFISMuCuUwf3w1qgrcb19R1WETlGpMmIqIqJDNbiUWH7mDj2YcAgCa1LbFquC+cqpvoODKi8o9JExFRFREVn4bJO4IREpUAABjTpg4+71kfMn0OxxGpg0kTEVEVcCj0OT7bE4KkjGxYGOlj8WAvdPO013VYRBUKkyYiokpMnq3Agn/u4OfzjwAAPk5WWDncB7WrcTiOqLiYNBERVVKRL1MxeXswbkYnAgAmvOWKT7vXh4Gexo8dJaqSmDQREVVCB248w6zfbyBZno1qJgZYMsQLnRrU0HVYRBWa1pOmV69eISUlpdDHpTg5OWm7WSIiApCRpcC8A2HYdvExAMDPuRpWjvBBTUs+RJ1IU1pJmsLDwxEQEIBDhw4hMTGx0LLafGDv3r17sWbNGgQFBSEtLQ329vZo2bIlFi1aBEdHx0KPPXv2LPbu3YuTJ0/i0aNHSE1NhYuLC/r27YvZs2fDyspKKzESEZWVB7Ep8N8ejNvPkgAAH3Vww8yu7tDncByRVmicNF2/fh3t27dX9S4ZGRnB1tYWUmnpvUmFEJg4cSLWr18PNzc3DBs2DObm5nj69ClOnTqFyMjIIpOmQYMGIS4uDm3btsX7778PiUSCkydPYtGiRfj9999x/vx52NnZldo5EBFp05/Xo/HFHzeRmqlAdVNDLB3qjfbutroOi6hS0Thp+uKLL5CcnIzOnTvjhx9+QKNGjbQRV6FWrlyJ9evXw9/fH8uXL4eeXu41RtTpyZoxYwbef/991Kz5fyvgCiHg7++PtWvXIjAwEKtXr9Z67ERE2pSeqUDg37ew80oUAKClqzWWD/NBDQsjHUdGVPlIRGGTj9RgZWUFpVKJZ8+ewdS09B/wmJ6ejtq1a8PKygp3796Fvr52p2U9e/YMtWrVgqenJ0JDQ9U+LikpCZaWlkhMTISFhYVWYyIiyk9ETDL8fw3G3RfJkEiAKZ3qYVrnetCTSnQdGlGFUZzvb40zDqVSifr165dJwgQAR48eRXx8PEaNGgWFQoG//voL4eHhsLKyQpcuXVC3bl2N6jcwMAAArSdjRETatOfaE3y1LxTpWQrYmMmwYpg3Wte10XVYRJWaxpmBt7c3Hjx4oI1Y1HL16lUAr5MaLy8v3L17V7VPKpVixowZWLx4cYnr/+mnnwAA3bp1K7ScXC6HXC5X/TspKanEbRIRqSstMxtf7gvFH0HRAIC2dW3ww1Bv2JrLdBwZUeWn8Wzt2bNn49mzZ9i6das24ilSTEwMAGDJkiWwsLDA5cuXkZycjNOnT8Pd3R1LlizB2rVrS1T39evXERgYCDs7O3z22WeFll2wYAEsLS1Vr6ImnhMRaerO8yS8s/Is/giKhlQCfNzVHVvGNGfCRFRGNJ7TBADr1q3DJ598gnHjxmHs2LFwc3ODsXHprAkyYcIEbNiwAcbGxoiIiECtWrVU+27duoUmTZqgTp06iIiIKFa9Dx8+RLt27RAXF4eDBw+iY8eOhZbPr6fJ0dGRc5qISOuEEPjtShTm/nUL8mwlaljIsHyYD1q6Vtd1aEQVXpnOaXrzzrUVK1ZgxYoVhZbXdJ0mS0tLAICfn1+uhAkAPD094erqioiICCQkJKi91lJkZCQ6duyI2NhY/P7770UmTAAgk8kgk/GvOyIqXSnybPxv7038ef0pAKC9uy2WDvFCdTN+/hCVNY2TpuJ2VGnasVW/fn0AKDAhytmenp6uVtL06NEjdOzYEU+fPsXu3bvRu3dvjeIjItKWW08TMXl7MB7GpUJPKsEn3erjw7dcIeXdcUQ6oZW758pSTi/Q7du38+zLyspCREQETE1NYWtb9KJujx49QocOHfD06VP89ttv6Nu3r9bjJSIqLiEEtl16jG/2hyEzW4malkZYOdwHfi7Wug6NqEqrcGvru7m5oVu3boiIiMDGjRtz7Vu4cCESEhLQv39/1ZIBcXFxuHPnDuLi4nKVzUmYoqOjsXPnTvTv37/MzoGIqCBJGVmYvD0YX+0LRWa2Ep0b2OGfqe2YMBGVA1qZCF7W7t+/j9atWyMmJga9evVCgwYNEBwcjOPHj8PZ2RkXL16Evb09ACAgIACBgYGYO3cuAgICVHW4uLggMjISLVu2RPfu3fNt583yReHilkSkqRtPEjB5ezAex6dBXyrBrJ4NMLZtHUgkHI4jKi1lOhH8v8LDwxEeHo7k5GSYm5vD3d0d7u7uWm3Dzc0NV69exZw5c3Do0CEcOXIE9vb28Pf3x5w5c9R6ZlxkZCQA4OLFi7h48WK+ZYqTNBERlZQQAj+ff4T5/9xGlkLAwcoYq0b4wMepmq5DI6I3aK2nad26dfjuu+9UycibXFxcMGvWLIwfP14bTZVL7GkiopJITMvCZ7+H4PCtFwCA7p41sGigFyxNDHQcGVHVUOY9TaNHj8Yvv/wCIQRkMhkcHR1Ro0YNvHjxAlFRUXj48CEmTpyI8+fPY/Pmzdpokoiowgt+/AqTtwcjOiEdhnpSfPF2A3zQ2oXDcUTllMYTwbdv344tW7bAxMQEixYtQmxsLMLDw3HmzBmEh4cjNjYWixYtgqmpKX755Rfs2LFDG3ETEVVYQghsOP0Ag3+8gOiEdDhZm+D3Sa0xqg3nLxGVZxoPz3Xs2BGnT5/GwYMHC31e25EjR9CjRw906NABx48f16TJconDc0SkjlepmfhkdwiO3Xn9SKheTWpiwYDGsDDicByRLhTn+1vjpMna2hrVq1fHvXv3iizr7u6O2NhYvHr1SpMmyyUmTURUlKuP4jFlRzCeJWbAUF+KOb098G4LJ/YuEelQmc5pysjIUPtxJRYWFnjy5ImmTRIRVShKpcCPp+9jyZFwKJQCdWxMsWqEDzxrWeo6NCIqBo2TJicnJ4SGhiIuLg42NjYFlouNjcWtW7fg7OysaZNERBXGyxQ5Zu4KwanwWABAX+9a+LZ/Y5jJtL7iCxGVMo0ngvfp0wdyuRxDhw5FbGxsvmViYmIwdOhQZGZm8lElRFRlXHzwEm+vOINT4bEwMpDiu4GNsWyoNxMmogpK4zlN8fHx8Pb2RnR0NGQyGQYPHgwPDw/Y2dkhJiYGYWFh2L17NzIyMuDo6Ijg4GBYW1e+xwFwThMR5VAoBVafiMCyf8OhFEBdOzOsHuGL+vbmug6NiP6jTCeCA0BERASGDx+Oa9euva70jUmNOdU3a9YM27dvh5ubm6bNlUtMmogIAGKSMzDjt+s4F/ESADCoaW183dcTJobsXSIqj8p8ccu6deviypUrOHbsGI4cOYLw8HCkpKTAzMwM7u7u6N69Ozp16qSNpoiIyq1zEXGYtvM64lLkMDbQw7x+jTCwaW1dh0VEWlIhH9hbHrGniajqUigFlh+7h5XH70EIoH4Nc6x+1wd17TgcR1Te6fSBvUREVcmLpAxM3RGMSw/jAQDDmzti7jueMDLQ03FkRKRtxUqaHj9+DAAwMDBAzZo1c20rDicnp2IfQ0RU3pwKj8WM364jPjUTpoZ6mD+gMfp6O+g6LCIqJcVKmlxcXj9IskGDBrh161aubeqSSCTIzs4uXpREROVItkKJJUfDsfbkfQBAw5oWWD3CB662ZjqOjIhKU7GSJien18v95/QyvbmNiKgqeJqQjqk7gnE18vXjoN5r6Yz/9WrI4TiiKqBYSdOjR4/U2kZEVBkdv/MCM3eFICEtC+YyfSwc2AS9mtQs+kAiqhQ0ngh++vRpWFpawsvLq8iyN27cQEJCAt566y1NmyUiKjNZCiUWHbqDDWceAgAaO1hi1QgfOFc31XFkRFSWNE6aOnTogHbt2uHUqVNFlp02bRpOnz4NhUKhabNERGUiKj4NU3YE43pUAgBgdBsXzOrZADJ9DscRVTVaWXKASz0RUWV0+NZzfLo7BEkZ2bAw0sf3g73Q3dNe12ERkY6U6TpNL1++hLGxcVk2SURUbPJsBRYevIPN5x4BALwdrbByuA8crU10GxgR6VSxk6akpCQkJCTk2iaXyxEVFVVgj1N6ejpOnTqF0NBQteY+ERHpyuOXafDfHoSb0YkAgPHt6uDT7g1gqC/VcWREpGvFTpp++OEHfP3117m2Xb16FS4uLmodP3bs2OI2SURUJv65+Qyf77mBZHk2rEwMsGSwFzo3rKHrsIionCh20mRlZZVrRe/Hjx/D0NAQ9vb5j/NLJBIYGxvD1dUVQ4cOxciRI0seLRFRKcjIUmDegTBsu/j6CQd+ztWwYrgPallxOgER/R+NH9grlUrRtm1bnD59WlsxVUh8YC9RxfQwLhX+vwYh7FkSAOCjDm6Y0dUdBnocjiOqCsr0gb2bN29GjRrsviaiiufP69H44o+bSM1UoLqpIZYO9UZ7d1tdh0VE5ZTGSdMHH3ygjTiIiMpMRpYCAX/dws4rUQCAFnWssWK4D2pYGOk4MiIqz8p0yQEiIl2LiEmG/6/BuPsiGRIJMKVTPUztVBf6HI4joiIUK2l6/Pj1JEkDAwPVQ3tzthXHmxPJiYjKyu/XnuDLfaFIz1LAxkyG5cO80aauja7DIqIKolhJk4uLCyQSCRo0aIBbt27l2qYuiUSC7Ozs4kVJRKSBtMxszPnzFvZcewIAaFO3On4Y6g07cw7HEZH6ipU0OTk5QSKRqHqZ3txGRFQe3X2eDP/tQYiISYFUAkzv4g7/jnWhJ+XnFhEVT7GSpkePHqm1jYhI14QQ2HU1CnP/uoWMLCVqWMiwfJgPWrpW13VoRFRBcSI4EVU6KfJsfLn3JvZdfwoAeMvdFj8M8UJ1M5mOIyOiioxJExFVKmFPkzB5exAexKVCTyrBx93cMfEtN0g5HEdEGirR3XOa0tbdc3v37sWaNWsQFBSEtLQ02Nvbo2XLlli0aBEcHR2LPF6pVGLNmjVYv3497t27BzMzM3Ts2BHffvst6tWrp5UYiahsCCHw66XH+Hp/GDKzlahpaYSVw33g52Kt69CIqJIo0d1zmtDG3XNCCEycOBHr16+Hm5sbhg0bBnNzczx9+hSnTp1CZGSkWknTxIkTsWHDBnh4eGDKlCl48eIFfvvtNxw5cgTnz5+Hh4eHRnESUdlIysjC7D9u4sCNZwCAzg3ssHiwF6qZGuo4MiKqTEp091x+oqOjVcmQvr4+bGxs8PLlS2RlZQF4vbZTrVq1NAz3tZUrV2L9+vXw9/fH8uXLoaenl2u/OknZiRMnsGHDBrRr1w5Hjx6FTPZ6rsP777+Prl27YtKkSTh16pRW4iWi0nPzSSIm7whC5Ms06EslmNWzAca2rcO7eolI64q1BO6jR4/w8OHDPK9evXpBIpFg6tSpuHPnDuRyOZ4+fYqMjAzcvXsXU6dOhUQiQe/evfHw4UONAk5PT0dgYCBcXV2xbNmyPAkT8DppK8qGDRsAAPPmzVMlTADQuXNndO/eHadPn0Z4eLhGsRJR6RFC4OdzDzFw7XlEvkyDg5Uxdk9shXHtXJkwEVGp0Hgi+Jo1a7B27Vrs2LEDQ4YMybVPIpGgXr16WLZsGVq3bo3hw4fDw8MDkyZNKnF7R48eRXx8PEaNGgWFQoG//voL4eHhsLKyQpcuXVC3bl216jl58iRMTU3Rpk2bPPu6d++OQ4cO4dSpU3B3dy9xrERUOhLTsvDZ7yE4fOsFAKCbRw18P8gLliYGOo6MiCozjZOmdevWwcnJKU/C9F9DhgzB559/jnXr1mmUNF29ehXA694kLy8v3L17V7VPKpVixowZWLx4caF1pKam4tmzZ2jUqFG+PVU5k8Dv3btXYB1yuRxyuVz176SkpGKdBxGVzPWoBEzeHoQnr9JhoCfBF283xKjWms+3JCIqisZPqIyIiICtra1aZW1tbQtNRNQRExMDAFiyZAksLCxw+fJlJCcn4/Tp03B3d8eSJUuwdu3aQutITEwEAFhaWua738LCIle5/CxYsACWlpaqlzoTz4mo5IQQ2HjmAQatPY8nr9LhZG2C3ye1xug2nL9ERGVD46TJzMwMt27dQkJCQqHlEhIScOvWLZiammrUnlKpBAAYGhpi3759aNasGczMzNCuXTvs2bMHUqkUS5Ys0agNdcyePRuJiYmqV1RUVKm3SVRVJaRlYvwvVzHvwG1kKwV6Na6J/VPbokltK12HRkRViMZJU9euXZGeno53330X8fHx+ZZ59eoV3n33XWRkZKB79+4atZfTO+Tn55fnbjxPT0+4urri/v37hSZxOXUU1JOUM9RWUE8UAMhkMlhYWOR6EZH2XYuMx9vLz+Df2zEw1Jfim36NsGqEDyyMOH+JiMqWxnOa5s+fj0OHDuHQoUNwcnLC4MGD0bBhQ9ja2iI2NhZ37tzB7t27kZqaiurVq2PevHkatVe/fn0AgJWVVb77c7anp6cXWMbU1BQ1a9bEw4cPoVAo8sxryhlC5AKXRLqjVAqsO/0Ai4/chUIpUMfGFKtG+MCzVsF/zBARlSaNkyYnJyecOXMGI0eORHBwMLZs2ZJrfoEQAgDg4+ODrVu3wtnZWaP2OnbsCAC4fft2nn1ZWVmIiIiAqalpkfOs2rdvj507d+LcuXN46623cu07fPiwqgwRlb2XKXLM3BWCU+GxAIC+3rXwbf/GMJPxyU9EpDta+QRq2LAhrl27huPHj+Pw4cMIDw9HSkoKzMzM4O7ujm7duqFz587aaApubm7o1q0bjhw5go0bN2LcuHGqfQsXLkRCQgJGjhypWqspLi4OcXFxsLGxgY2NjarshAkTsHPnTnz55Zf4999/YWj4euXgY8eO4fDhw3jrrbe43ACRDlx68BJTdwbjRZIcMn0pvu7riSF+jpzsTUQ6JxE5XUEVyP3799G6dWvExMSgV69eaNCgAYKDg3H8+HE4Ozvj4sWLsLe3BwAEBAQgMDAQc+fORUBAQK56xo8fj40bN8LDwwO9evVSPUbFyMio2I9RSUpKgqWlJRITEzm/iagEFEqBNSci8MO/4VAKwM3WFGvebYr69ua6Do2IKrHifH9rPBFcF9zc3HD16lWMGjUK165dw4oVK3Dv3j34+/vj8uXLqoSpKOvWrcOKFSsgkUiwYsUKHDhwAO+88w4uX77M584RlaHYZDk++Okylhx9nTAN9K2Nv6e0ZcJEROWK1nuaXr16hZSUFBRWrZOTkzabLBfY00RUMucj4jB153XEpchhbKCHb/o1wqCmtXUdFhFVEcX5/tbKnKbw8HAEBATg0KFDhS4ICbx+tIo6D9QlospNoRRYfuweVh6/ByGA+jXMsWqED+rVYO8SEZVPGidN169fR/v27VW9S0ZGRrC1tYVUWiFH/oioDLxIysC0ncG4+OD12m7Dmjli7jueMDbM+1gjIqLyQuOk6YsvvkBycjI6d+6MH374AY0aNdJGXERUSZ0Oj8WM367jZWomTA31MH9AY/T1dtB1WERERdI4aTp//jzMzMywb98+jR+RQkSVV7ZCiaVHw7Hm5H0AQMOaFlg9wgeutmY6joyISD0aJ01KpRL169dnwkREBXqWmI6pO4Jx5dErAMDIlk74spcHjAw4HEdEFYfGSZO3tzcePHigjViIqBI6fucFPt4VgldpWTCX6WPBwMbo3aRW0QcSEZUzGs/Wnj17Np49e4atW7dqIx4iqiSyFErM/+c2xvx8Fa/SstDYwRL7p7ZlwkREFZbGPU09e/bEmjVr8NFHHyEoKAhjx46Fm5sbjI2NtREfEVVAT16lYcqOYAQ/TgAAjGrtgtlvN4BMn8NxRFRxaby4pZ5e8T4EK+s6TVzckui1I7ee45PdIUjKyIaFkT4WDfJCj0bqrdJPRFTWynRxy+LmXBXwUXdEpIbMbCUWHLyNzeceAQC8HK2wargPHK1NdBsYEZGWaOXuOSKq2h6/TMPkHUG48eT1EwHGt6uDT7s3gKE+F7klospDK49RIaKq65+bz/D5nhtIlmfDysQAiwd5oYtHDV2HRUSkdVpPmsLDwxEeHo7k5GSYm5vD3d0d7u7u2m6GiHQsI0uBbw/cxtaLkQCAps7VsHK4D2pZ8SYQIqqctJY0rVu3Dt999x0iIyPz7HNxccGsWbMwfvx4bTVHRDr0MC4Vk7cH4dbTJADApA5umNnVHQZ6HI4jospLK0nT6NGj8csvv0AIAZlMBkdHR9SoUQMvXrxAVFQUHj58iIkTJ+L8+fPYvHmzNpokIh35K+QpZv9+A6mZClibGmLpEC90qG+n67CIiEqdxn8Wbt++HVu2bIGJiQkWLVqE2NhYhIeH48yZMwgPD0dsbCwWLVoEU1NT/PLLL9ixY4c24iaiMpaRpcDsP25i6o5gpGYq0LyONf6Z2o4JExFVGRqv09SxY0ecPn0aBw8eRLdu3Qosd+TIEfTo0QMdOnTA8ePHNWmyXOI6TVSZRcSkYPL2INx5ngyJBJjSsS6mdq4HfQ7HEVEFV5zvb42TJmtra1SvXh337t0rsqy7uztiY2Px6tUrTZosl5g0UWX1+7Un+HJfKNKzFLAxk2HZUG+0rWej67CIiLSiTBe3zMjIgJWVlVplLSws8OTJE02bJKIykJaZjTl/3sKea6/fs63dqmPZMG/YmRvpODIiIt3QOGlycnJCaGgo4uLiYGNT8F+fsbGxuHXrFpydnTVtkohKWfiLZPj/GoR7MSmQSoDpXdzh37Eu9KQSXYdGRKQzGk9I6NOnD+RyOYYOHYrY2Nh8y8TExGDo0KHIzMxE3759NW2SiEqJEAK7rkShz6qzuBeTAjtzGX4d1xJTO9djwkREVZ7Gc5ri4+Ph7e2N6OhoyGQyDB48GB4eHrCzs0NMTAzCwsKwe/duZGRkwNHREcHBwbC2ttZW/OUG5zRRRZcqz8b/9t7EvutPAQDt6tngh6HesDGT6TgyIqLSU6YTwQEgIiICw4cPx7Vr115XKvm/v0hzqm/WrBm2b98ONzc3TZsrl5g0UUUW9jQJk7cH4UFcKvSkEnzczR0T33KDlL1LRFTJlelEcACoW7curly5gmPHjuHIkSMIDw9HSkoKzMzM4O7uju7du6NTp07aaIqItEgIge2XHyPw7zBkZitR09IIK4b7oJlL5esNJiLSlFZ6mog9TVTxJGdkYfYfN7H/xjMAQKcGdlgy2AvVTA11HBkRUdkp854mIqpYQqMT4b89CJEv06AvleDzHg0wtm0dDscRERWiREnTrVu3cP/+fdjZ2aFly5ZFlr9w4QJiY2NRt25deHh4lKRJItICIQR+uRCJbw/cRqZCCQcrY6wc4QNfp2q6Do2IqNwrdtKUlpaGbt26IS4uDidOnFDrGCEEBg0ahFq1auHu3buQyXg3DlFZS0zPwud7buDQrecAgG4eNfD9IC9YmhjoODIiooqh2Os07dixA8+ePcPYsWPRunVrtY5p3bo1xo8fj6ioKOzcubPYQRKRZq5HJaDXijM4dOs5DPQkmPuOB9a915QJExFRMRQ7adq3bx8kEgmmTp1arOOmT58OIQR+//334jZJRCUkhMDGMw8w+MfzePIqHU7WJvh9UmuMblMn19IgRERUtGIPzwUHB6NmzZpo0KBBsY6rV68eHBwcEBwcXNwmiagEEtIy8cnuEPx7OwYA8HZjeywc2AQWRuxdIiIqiWInTXFxcfDy8ipRY7Vq1cKNGzdKdCwRqe9aZDymbA/G08QMGOpL8VVvD4xs4cTeJSIiDRR7eM7IyAjp6eklaiw9PR2GhpqvAePi4gKJRJLva+LEiWrXk5CQgDlz5qBJkyYwNzeHjY0NmjVrhlWrViEjI0PjOInKmlIp8OOp+xiy7iKeJmagjo0p9n7UGu+1dGbCRESkoWL3NNWsWRP379+HXC4v1l1wcrkc9+/fh5OTU3GbzJelpSWmT5+eZ7ufn59axyckJKBp06Z48OAB2rZtiw8//BByuRwHDx7ElClTsHfvXhw9ehRSqcbPNCYqEy9T5Ph4dwhO3n394Ow+XrUwf0BjmMm4HBsRkTYU+9O0Xbt22LRpE/bs2YN3331X7eN2796N9PR0tGvXrrhN5svKygoBAQElPn79+vV48OABZsyYgaVLl6q2Z2Zmom3btjh+/DjOnj2Lt956SwvREpWuyw/jMWVHEF4kySHTlyKwjyeGNnNk7xIRkRYVuxtl1KhREELg888/R1RUlFrHPH78GJ999hkkEgk++OCDYgdZGh48eAAAePvtt3NtNzQ0RNeuXQEAMTExZR4XUXEolQKrjt/DsPUX8CJJDjdbU/w5uQ2GNef8JSIibSt20tS6dWsMHjwYT58+RYsWLbB7924olcp8yyqVSuzatQstW7bEixcvMHDgQLRp00bjoIHXw31btmzB/PnzsXbtWoSEhBTreE9PTwDAoUOHcm3PysrCv//+C2NjY7Rq1UorsRKVhthkOT7YfBmLj4RDKYABvg74a3JbNLDnsw+JiEpDiR7Ym56ejq5du+L8+fOQSCSwtbVFmzZtUKdOHZiamiI1NRUPHz7E+fPnERMTAyEEWrVqhaNHj8LExETjoF1cXBAZGZlne48ePbB161bY2NiodQ7t27fHlStX0L59ezRr1gxyuRyHDh3Cq1evsGHDBvTr16/A4+VyOeRyuerfSUlJcHR05AN7qUycj4jDtN+uIzZZDmMDPXzd1xOD/Rx1HRYRUYVTnAf2lihpAoDs7GwEBARg5cqVSE5Ofl3ZG8MBOdWamZlhypQpCAgIgIGBdtaH+frrr9G+fXt4enpCJpMhLCwMgYGBOHjwIFq1aoVz586pNTSRlpaGDz/8ENu2bVNtk0qlmDx5Mr766qtCk6+AgAAEBgbm2c6kiUqTQimw4tg9rDh+D0IA7jXMsHqEL+rVMNd1aEREFVKZJE1vNnbgwAGcP38e0dHRSE5Ohrm5ORwcHNC6dWu8/fbbsLS01KQJtSiVSrRv3x5nz57F/v370atXr0LLx8XFoW/fvoiJicHy5cvRpk0bZGRk4K+//sLHH38MW1tbXL16FdWq5f8gU/Y0UVmLScrA1J3BuPggHgAw1M8RAX08YWyop+PIiIgqruIkTRrfi2xhYYHhw4dj+PDhmlalEalUitGjR+Ps2bM4d+5ckUnTzJkzcf78eYSEhKBJkyYAXi9jMH78eCgUCkyaNAnLli3LtzcJAGQyGR88TGXmdHgsZvx2HS9TM2FiqIf5/Rujn4+DrsMiIqpSKtUCLjnDaWlpaUWWPXDgAKytrVUJ05s6deoEALh27Zp2AyQqpmyFEj/8G441J+9DCKBhTQusHuEDV1szXYdGRFTlVKqk6dKlSwBeTxQvSmZmJjIyMpCZmZlnlfLY2NeLA7IniXTpWWI6pu24jsuPXg/HvdvCCV/19oCRAYfjiIh0ocItdx0WFoaEhIQ828+ePYulS5dCJpNhwIABqu1xcXG4c+cO4uLicpVv06YNsrOz8c033+TaLpfLVds6duyo/RMgUsOJOzF4e/kZXH4UDzOZPlaN8MG3/RszYSIi0qEK19O0a9cuLFq0CJ07d4aLiwtkMhlCQ0Nx5MgRSKVS/Pjjj7ke1bJq1SoEBgZi7ty5uVYQX7hwIc6fP4958+bhyJEjqonghw8fxoMHD9C0aVOMGzdOB2dIVVmWQonFh+9i3enXi682crDA6hG+cK5uquPIiIiowiVNHTt2xO3btxEUFIRTp04hIyMDNWrUwNChQzFjxgw0b95crXq8vb1x7do1LFiwAMeOHcOqVaugr6+PunXrIjAwEJ988gmMjIxK+WyI/k90QjqmbA9C0OMEAMCo1i6Y/XYDyPTZu0REVB5ovOQAvVacWxaJ/uto2At8sjsEielZMDfSx/eDmqBHo5q6DouIqNIr0yUHiKjkMrOVWHjwDn469xAA4OVohVXDfeBorfnK+UREpF1Mmoh0JCo+DZO3ByHkSSIAYFzbOvisRwMY6le4+zOIiKqEUkua/vzzT/z999+4ffs24uNf3zJtbW2Nhg0bok+fPujTp09pNU1U7h28+Qyf/X4DyRnZsDQ2wJLBXujiUUPXYRERUSG0njS9fPkSvXv3xqVLl+Du7g5PT094eHhACIFXr17h3Llz+Omnn9CyZUv8/fffqF69urZDICq3MrIUmP/Pbfxy4fUDp5s6V8OK4T5wsDLWcWRERFQUrSdNM2bMQGxsLC5fvgw/P798y1y7dg3Dhg3DzJkzsWXLFm2HQFQuPYpLhf/2INx6mgQAmNjeDR93c4eBHofjiIgqAq0nTfv378eGDRsKTJgAoGnTpli4cCHGjx+v7eaJyqW/Qp7iiz9uIkWeDWtTQywd4oUO9e10HRYRERWD1pOm7OxsmJgUfeePsbExsrOztd08UbmSkaVA4N9h2HH5MQCgeR1rrBjmA3tLrgFGRFTRaD1p6tixI+bOnYumTZvCzi7/v6RjYmIQGBioejAuUWV0PzYF/r8G4c7zZEgkwOSOdTGtcz3ocziOiKhC0nrStGLFCnTo0AEuLi7o2LEjPD09YWVlBYlEglevXiEsLAwnTpyAvb09du3ape3micqFvcFP8L+9oUjLVMDGzBDLhvqgbT0bXYdFREQaKJUVwVNTU/Hjjz/iwIEDCAsLw6tXrwAA1apVg6enJ3r37o3x48fDzMxM203rDFcEJwBIz1Rgzp+h2H3tCQCgtVt1LBvqDTsLDscREZVHxfn+5mNUtIRJE4W/SIb/r0G4F5MCqQSY1tkdkzvVhZ5UouvQiIioAHyMClEZEkJg97UnmPNnKDKylLAzl2H5MB+0cuMaZERElYnOZqTevn0bX3/9ta6aJ9KKVHk2Zu4KwWd7biAjS4l29Wzwz7R2TJiIiCohnSVNYWFhCAwM1FXzRBq7/SwJ76w6i73B0dCTSvBp9/rYMro5bMxkug6NiIhKAYfniIpJCIEdl6MQ8PctZGYrYW9hhJUjfNDMxVrXoRERUSnSetKkp6en7SqJyo3kjCx8sTcUf4c8BQB0amCHxYO9YG1qqOPIiIiotGk9aTI0NETLli3Ro0ePQsvdvHkTO3bs0HbzRKUmNDoRk7cH4dHLNOhLJfisR32Ma+sKKe+OIyKqErSeNHl5ecHCwgKff/55oeV+//13Jk1UIQghsPViJObtv41MhRIOVsZYOcIHvk7VdB0aERGVIa0nTc2aNcPvv/+uVlkuEUXlXWJ6Fmb9fgMHQ58DALp61MD3g5rAyoTDcUREVY3WF7eMjo5GREQE2rdvr81qyz0ubln5hEQlYPKOIETFp8NAT4LZPRtidBsXSCQcjiMiqix0urilg4MDHBwctF0tUZkRQuCnc4+w8OBtZCkEHK2NsWq4L7wcrXQdGhER6RCXHCB6Q0JaJj7ZfQP/3n4BAHi7sT0WDmwCCyMDHUdGRES6xqSJ6P+7FvkKU3cEIzohHYZ6UnzVuyFGtnTmcBwREQHQQtL0+PFjtcvq6enB3Nycc36oXFEqBTaceYDvD99FtlLApboJVo3wRSMHS12HRkRE5YjGSZOLS/EnxlpZWaFNmzaYOHEi3n77bU1DICqx+NRMfLzrOk7cjQUA9PGqhfkDGsNMxk5YIiLKTeNnzzk5OcHJyQn6+voQQkAIAXNzc9SqVQvm5uaqbfr6+nByckL16tXx6tUr7N+/H++88w78/f21cR5ExXb5YTzeXn4GJ+7GQqYvxYIBjbF8mDcTJiIiypfGSdOjR4/Qt29fSKVSzJ07F48ePUJCQgKioqKQkJCAyMhIBAQEQE9PD3379kVMTAzi4uKwaNEiyGQy/Pjjj9izZ482zoVILUqlwOoTERi+4SKeJ2XA1dYU+/zbYHhzJ85fIiKiAmm8TtO6devw0UcfYc+ePejfv3+B5fbt24eBAwdi9erVmDhxIgBg27ZteP/999G1a1ccPnxYkzB0jus0VQxxKXLM+O06ztyLAwAM8HHAN/0awZS9S0REVVJxvr81Tpp8fHyQmJiIBw8eFFnW1dUVFhYWuH79umqbra0tACA2NlaTMHSOSVP5d/5+HKbtvI7YZDmMDKT4pm8jDPZz1HVYRESkQ8X5/tZ4eC48PBw2NjZqlbWxscG9e/dybXN1dUVSUpKmYRAVSKEUWPZvOEZuvITYZDnca5jh78ltmTAREVGxaDwmYWpqirCwMCQmJsLSsuBbtBMTExEWFgZTU9Nc21++fFnocUSaiEnKwPTfruP8/ZcAgKF+jgjo4wljQz0dR0ZERBWNxj1NnTt3RlpaGkaOHInk5OR8y6SmpuK9995Deno6unbtmmt7ZGQkHB35Fz9p35l7sXh7xRmcv/8SJoZ6WDbUG98NasKEiYiISkTjnqZvv/0Whw8fxj///AM3NzcMGDAATZo0gbm5OVJSUnDjxg388ccfiI2NRbVq1TBv3jzVsdu3b4dCoUC3bt00DYNIJVuhxLJ/72H1yQgIATSwN8fqd33hZmum69CIiKgC03giOADcuHEDI0eORGho6OtK37htO6f6Jk2aYOvWrWjcuLFqX2hoKF6+fAkPDw/VhHB1uLi4IDIyMt99H374IX788Ue160pOTsbixYvx+++/48GDBzA0NISrqyv69u2LuXPnql0PJ4KXD88S0zFtx3VcfhQPAHi3hRO+6u0BIwP2LhERUV5levdcDiEEjh49iqNHj+LevXtITU2Fqakp3N3d0bVrV3Tp0kVra+C4uLggISEB06dPz7PPz88PvXv3Vquex48fo1OnTnjw4AG6dOkCHx8fyOVyRERE4PHjx7hx44baMTFp0r0Td2Mw87freJWWBTOZPhYMaIx3vGrpOiwiIirHdJI0lSUXFxcArxfWLCmFQoFWrVohNDQUBw4cQMeOHXPtz87Ohr6++qOXTJp0J0uhxOIjd7Hu1OtlLxo5WGDVcF+42JgWcSQREVV1xfn+1vqKfuHh4QgPD0dycjLMzc3h7u4Od3d3bTejsT179uDKlSv46quv8iRMAIqVMJHuRCekY8r2IAQ9TgAAjGrtgtlvN4BMn8NxRESkXVrLDNatW4fvvvsu37lGzs7OmD17NsaPH6+t5iCXy7FlyxZER0ejWrVqaN26Nby8vNQ+/rfffgMADB48GFFRUThw4AASEhLg5uaGnj17wsyMk4bLu6NhL/DJ7hAkpmfB3Egf3w9qgh6Nauo6LCIiqqS0kjSNHj0av/zyC4QQkMlkcHR0RI0aNfDixQtERUXh0aNHmDhxIs6fP4/Nmzdro0k8f/4co0aNyrWtR48e2Lp1q1qLbV69ehUAcPbsWcyYMQNyuVy1z9bWFrt27UKHDh0KPF4ul+c6hgt0lp3MbCW+O3QHm84+BAB41bbEqhG+cLQ20XFkRERUmWm8TtP27duxZcsWmJiYYNGiRYiNjUV4eDjOnDmD8PBwxMbGYtGiRTA1NcUvv/yCHTt2aBz0mDFjcPLkScTGxiIpKQkXL15Ez549cejQIfTp0wfqTNOKiYkBAEyZMgXTp09HVFQUYmNjsWLFCiQmJqJfv3549uxZgccvWLAAlpaWqhfXmiobUfFpGLzugiphGte2DnZPbM2EiYiISp3GE8E7duyI06dP4+DBg4Wut3TkyBH06NEDHTp0wPHjxzVpMl9KpRLt27fH2bNnsX//fvTq1avQ8oaGhsjKykLfvn2xb9++XPtmzZqF7777Dt988w2+/PLLfI/Pr6fJ0dGRE8FL0aHQZ/h0zw0kZ2TD0tgASwZ7oYtHDV2HRUREFViZPnsuJCQErq6uRS5Q2a1bN9StWxfBwcGaNpkvqVSK0aNHAwDOnTtXZPmcR7f06dMnz7533nkHwP8N4eVHJpPBwsIi14tKhzxbgbl/hmLitiAkZ2TD18kK/0xrx4SJiIjKlMZzmjIyMmBlZaVWWQsLCzx58kTTJguUM5cpLS2tyLL169dHXFxcvrHnbEtPT9dmeFQCj+JSMXlHEEKjX88Z+7C9Kz7pVh8Gehrn+0RERMWi8TePk5MTQkNDERcXV2i52NhY3Lp1C05OTpo2WaBLly4B+L91nArTqVMnAEBYWFiefTnb1KmHSs/fIU/Re+VZhEYnwdrUEJtHN8Psng2ZMBERkU5o/O3Tp08fyOVyDB06FLGxsfmWiYmJwdChQ5GZmYm+fftq1F5YWBgSEhLybD979iyWLl0KmUyGAQMGqLbHxcXhzp07eZK60aNHQyaTYeXKlYiOjlZtT05Oxvz58wEAQ4YM0ShWKpmMLAW+2HsTU3YEI0WejeYu1vhnajt0rG+n69CIiKgK03gieHx8PLy9vREdHQ2ZTIbBgwfDw8MDdnZ2iImJQVhYGHbv3o2MjAw4OjoiODgY1tbWJW4vICAAixYtQufOneHi4gKZTIbQ0FAcOXIEUqkUP/74I8aNG5erfGBgIObOnYuAgIBcda1cuRJTp05F9erV0b9/f8hkMhw4cACPHj3ChAkTsG7dOrXj4org2nE/NgX+vwbhzvNkSCTA5I51Ma1zPeizd4mIiEpBma4Ibm1tjePHj2P48OG4du0atm7dmu8De5s1a4bt27drlDABr+/Wu337NoKCgnDq1ClkZGSgRo0aGDp0KGbMmIHmzZurXdeUKVPg4uKC77//Hjt37kR2djY8PT3xxRdfaHUhTlLP3uAn+N/eUKRlKmBjZogfhnqjXT31H+RMRERUmrT67Lljx47hyJEjCA8PR0pKCszMzODu7o7u3bur5hBVVuxpKrn0TAXm/hWKXVdf3yTQyrU6lg/zhp2FkY4jIyKiyq7SP7C3PGLSVDL3XiTDf3sQwl+kQCIBpnWuhymd6kFPKin6YCIiIg3p9IG9ROrafTUKX/0ZiowsJWzNZVg+zBut3Yp+BA4REZEuFCtpevz4sVYaLc1lB6j8S5Vn46s/Q/FH0Ou7FtvVs8EPQ71hYybTcWREREQFK1bS5OLikmuSd0lIJBJkZ2drVAdVXHeeJ8H/1yDcj02FVAJ83K0+JrV3g5TDcUREVM4VK2lycnLSOGmiqkkIgZ1XohDw1y3Is5WwtzDCiuE+aF5Hs7spiYiIykqxkqZHjx6VUhhUmSVnZOGLvaH4O+QpAKBjfVssGeINa1NDHUdGRESkPk4Ep1IVGp2IyduD8OhlGvSlEnzavT7Gt3PlcBwREVU4TJqoVAghsO1iJL7ZfxuZCiUcrIyxYrgPmjpX03VoREREJcKkibQuKSMLs36/gX9uPgcAdGlYA4sHN4GVCYfjiIio4mLSRFoVEpWAyTuCEBWfDgM9CWb1bIgxbTS/65KIiEjXmDSRVgghsPncIyw4eBtZCgFHa2OsGu4LL0crXYdGRESkFUyaSGMJaZn4dM8NHA17AQDo2cgeCwc2gaWxgY4jIyIi0h4mTaSRoMevMGV7MKIT0mGoJ8WXvRvivZbOHI4jIqJKh0kTlYhSKbDx7AMsOnQX2UoBl+omWDXCF40cLHUdGhERUalg0kTFFp+aiU92h+D4nRgAwDtetTC/fyOYG3E4joiIKi8mTVQsVx7FY+qOYDxLzIBMX4q573hieHNHDscREVGlx6SJ1KJUCqw9dR9Lj4ZDoRRwtTXF6hG+aFjTQtehERERlQkmTVSkuBQ5Zvx2HWfuxQEABvg44Jt+jWAq468PERFVHfzWo0JduP8S03YGIyZZDiMDKb7u2wiDm9bmcBwREVU5TJooXwqlwKrjEVh+LBxKAdSzM8Pqd33hXsNc16ERERHpBJMmyiMmOQPTd17H+fsvAQBD/GojsE8jGBvq6TgyIiIi3WHSRLmcvReH6b8FIy4lEyaGevi2fyP096mt67CIiIh0jkkTAQCyFUosP3YPq05EQAiggb05Vo3wRV07M12HRkREVC4waSI8T8zA1J3BuPwwHgAwooUT5vT2gJEBh+OIiIhyMGmq4k7ejcHMXSGIT82EmUwf8wc0Rh+vWroOi4iIqNxh0lRFZSmUWHIkHD+eug8A8KxlgdUjfOFiY6rjyIiIiMonJk1VUHRCOqbuCMa1yFcAgA9aOWP22w05HEdERFQIJk1VzL9hL/DJnhAkpGXB3EgfiwY2Qc/GNXUdFhERUbnHpKmKyMxWYtGhO9h49iEAwKu2JVYO94VTdRMdR0ZERFQxMGmqAqLi0zB5RzBCohIAAGPb1sHnPRrAUF+q28CIiIgqECZNldyh0Of4dE8IkjOyYWlsgMWDvdDVo4auwyIiIqpwmDRVUvJsBRb8cwc/n38EAPB1ssKK4T6oXY3DcURERCVRIcdnXFxcIJFI8n1NnDixRHVmZWXB29sbEokEDRo00HLEZSvyZSoGrb2gSpg+bO+K3z5sxYSJiIhIAxW2p8nS0hLTp0/Ps93Pz69E9X3zzTeIiIjQMCrd23/jKWb9fhMp8mxUMzHA0iHe6NjATtdhERERVXgVNmmysrJCQECAVuoKCgrCggULsHTpUkydOlUrdZa1jCwFvtkfhl8vPQYANHOphhXDfVDT0ljHkREREVUOFXJ4TpsyMzMxatQotGzZEpMnT9Z1OCXyIDYF/decx6+XHkMiASZ3rIsd41syYSIiItKiCtvTJJfLsWXLFkRHR6NatWpo3bo1vLy8il1PQEAA7t27h5CQEEgkklKItHTtC47GF3tvIi1Tgeqmhlg2zBvt6tnqOiwiIqJKp8ImTc+fP8eoUaNybevRowe2bt0KGxsbteq4cuUKFi1ahPnz58Pd3b1Y7cvlcsjlctW/k5KSinW8ptIzFQj46xZ+uxoFAGjlWh3Lh3nDzsKoTOMgIiKqKirk8NyYMWNw8uRJxMbGIikpCRcvXkTPnj1x6NAh9OnTB0KIIuuQy+UYNWoUfHx88PHHHxc7hgULFsDS0lL1cnR0LMmpFEmhFLhw/yX+vB6NC/dfQqEUuPciGX1Xn8VvV6MgkQDTu9TDtnEtmDARERGVIolQJ8OoAJRKJdq3b4+zZ89i//796NWrV6HlP/vsMyxbtgzXrl1D48aNVdslEgnq16+PO3fuFHp8fj1Njo6OSExMhIWFhWYn8/8dCn2GwL/D8CwxQ7XN0lgf6ZlKZCqUsDWXYfkwb7R2U69njYiIiHJLSkqCpaWlWt/fFbKnKT9SqRSjR48GAJw7d67QskFBQVi6dCn+97//5UqYikMmk8HCwiLXS5sOhT7DpG1BuRImAEhMz0amQomGNc3xz9R2TJiIiIjKSKVJmgCo5jKlpaUVWu7GjRtQKBQICAjIszgmANy9excSiQRWVlalHXK+FEqBwL/DUFgX4KvUTFibGpZZTERERFVdhZ0Inp9Lly4BeL1ieGHc3d0xduzYfPdt2rQJlpaWGDRoEExMdLOC9uWH8Xl6mP7reZIclx/Go5Vb9TKKioiIqGqrcElTWFgYatWqlacX6OzZs1i6dClkMhkGDBig2h4XF4e4uDjY2NioeqJat26N1q1b51v/pk2bYG9vj40bN5baORQlJrnwhKm45YiIiEhzFW54bteuXahVqxbeeecdTJkyBZ988gl69OiBt956C1lZWVi1ahWcnJxU5VetWoWGDRti1apVOoy6eOzM1bsLTt1yREREpLkK19PUsWNH3L59G0FBQTh16hQyMjJQo0YNDB06FDNmzEDz5s11HaLGmtexRk1LIzxPzMh3XpMEgL2lEZrXsS7r0IiIiKqsSrPkgK4V55ZFdeTcPQcgV+KUs2b52pG+6NGopsbtEBERVWVVcsmByqZHo5pYO9IX9pa5h+DsLY2YMBEREelAhRueq0p6NKqJrh72uPwwHjHJGbAzfz0kpyeteM/IIyIiquiYNJVzelIJlxUgIiIqBzg8R0RERKQGJk1EREREamDSRERERKQGJk1EREREamDSRERERKQGJk1EREREamDSRERERKQGJk1EREREamDSRERERKQGrgiuJTnPPU5KStJxJERERKSunO/tnO/xwjBp0pLk5GQAgKOjo44jISIiouJKTk6GpaVloWUkQp3UioqkVCrx9OlTmJubQyLR7gN1k5KS4OjoiKioKFhYWGi1biobvIYVG69fxcdrWPGV1jUUQiA5ORm1atWCVFr4rCX2NGmJVCpF7dq1S7UNCwsLvtkrOF7Dio3Xr+LjNaz4SuMaFtXDlIMTwYmIiIjUwKSJiIiISA1MmioAmUyGuXPnQiaT6ToUKiFew4qN16/i4zWs+MrDNeREcCIiIiI1sKeJiIiISA1MmoiIiIjUwKSJiIiISA1MmoiIiIjUwKSpDCQkJGDq1Klo1aoV7O3tIZPJ4ODggE6dOuH333/P93k3SUlJmDlzJpydnSGTyeDs7IyZM2cW+my77du3o3nz5jA1NUW1atXw9ttv4+rVq6V5alXWokWLIJFIIJFIcPHixXzL8BqWLy4uLqpr9t/XxIkT85Tn9Su/9u7di65du6J69eowNjZGnTp1MHz4cERFReUqx2tYvvz8888FvgdzXp07d851THm7hrx7rgxERETA29sbLVu2RN26dWFtbY2YmBj8/fffiImJwfjx47F+/XpV+dTUVLRt2xbXr19H165d4evri5CQEBw6dAje3t44e/YsTE1Nc7Uxf/58/O9//4OTkxMG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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Draw 1D sensitivity curve\n", - "# This problem has three degrees of freedom. To draw the 1D curve, it needs to fix two dimensions\n", - "fixed = {\n", - " \"'CA0[0]'\": 1.0,\n", - " \"('T[0.125]','T[0.25]','T[0.375]','T[0.5]','T[0.625]','T[0.75]','T[0.875]','T[1]')\": 300,\n", - "}\n", - "\n", - "all_fim.figure_drawing(fixed, [\"T[0]\"], \"Reactor case\", \"T [K]\", \"$C_{A0}$ [M]\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Draw 2D Sensitivity Curve" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "image/png": "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i/Hw4UMcP34c165dg1gsxnfffSdX/qxZs3D16lX4+vrC3t4eBQUFOHXqFP78809YW1tj8+bNStf1q28Y5BEREWk96RIqtSctLQ0AkJ+fjwULFihN4+TkJAvyyuPg4IAePXrg+PHjePDgAUxNTdGxY0dERERgyJAhSvO4uroiKSkJ8+bNw4EDB3Dw4EGIxWJMmTIF8+bNg62trSyto6MjZs+ejbi4OOzfvx9ZWVkwNjaGm5sb5s6diw8//BCNGjWSK3/UqFH4+eefcfLkSVnPo6OjI6ZPn46ZM2dqzYLI3Lu2HuPetfRy4N61pLtqb+9ab1haVi/Iy80thZVVIn9zdAh78oiIiLRe1feeJd3HbwQREZHWY5BHivh2LREREZEOYthPRESk9diTR4r4jSAiItJ6mni7lu9h6hoGeURERFpPEz15DPJ0DefkEREREekg9uQRERFpPfbkkSIGeURERFqPQR4p4nAtERERkQ5iTx4REZHWY08eKWKQR0REpPU0sYSKRBMNoXqEw7VEREREOog9eURERFpP//+P6pZBuoRBHhERkdbTxJw8DtfqGg7XEhEREekg9uQRERFpPfbkkSIGeURERFqPQR4pYpBHRESk9TSxhMozTTSE6hHOySMiIiLSQezJIyIi0nqaGK5lT56uYZBHRESk9RjkkSIO1xIRERHpIPbkERERaT325JEiBnlERERaTxNv15ZqoiFUj3C4loiIiEgHsSePiIhI62liuJYhga7hEyUiItJ6DPJIEYdriYiIiHQQw3YiIiKtx548UsQnSkREpPUY5JEiPlEiIiKtp4klVPQ10RCqRzgnj4iIiEgHsSePiIhI63G4lhTxiRIREWk9BnmkiMO1RERERDqIYTsREZHW00f1X5zgixe6hkEeERGR1uPbtaSIw7VEREREOog9eURERFqPL16QIj5RIiIirccgjxRxuJaIiIhIBzFsJyIi0nrsySNFfKJERERaj0EeKeITJSIi0npcQoUUcU4eERERkQ5ikEdERKT1DDR0qO7OnTuIjIxEQEAAHBwcYGhoCLFYjCFDhuDMmTNVKuv27duYOHGirBw7OzsEBQXh1q1bFebbtWsX/P390ahRI5iYmMDZ2RkjR45UyLd27Vr85z//gbOzM8zMzGBlZQVPT0/MmzcPWVlZ5Za/detWeHt7w8zMDNbW1ujXrx+S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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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IiKStQcPHqBnz544fvw4hBBo2bIlQkJC0KRJE7i6ukKpVOLBgwe4d+8e/v77byQlJSE1NRWff/45jh07hl9//RU2NjYGl8sQRUREJHf1vDtv6dKlOHbsGFq0aIGvv/4aXbt2rfKYw4cPIzo6GsnJyViyZAnmzp1rcLkyvmVEREQE4H/fzjN2kanNmzfDxsYGe/bs0StAAUC3bt2we/duWFlZYdOmTZLKZYgiIiIiWbt8+TLatm0LHx8fg47z8/ND27ZtkZaWJqlcducRERHJXT2fJ8rR0RE3b96UdOzNmzfh4OAg6Vi2RBEREcmdhYkWmeratSuuXr2Kjz76yKDjPvzwQ1y9ehXdunWTVK6MbxkRERERMHPmTFhYWOCf//wnnnvuOWzduhXXr1/Xue/169exdetW9O/fH++88w4sLS0xa9YsSeWyO4+IiEju6nl3XteuXbF27VrExMTg559/xu7duwEASqUSTzzxBGxsbFBUVITs7Gw8ePAAACCEgI2NDVatWoVnnnlGUrlsiSIiIpK7et6dBwAjR45EamoqJk6cCE9PTwghUFhYiKysLGRkZCArKwuFhYUQQqBhw4aYOHEiUlNTERkZKblMtkQRERHJHWcsB/Dw23afffYZPvvsM2RkZGge+1JYWAhbW1vNY198fX1NUh5DFBEREdU5vr6+JgtLFWGIIiIikrt6PibKXBiiiIiI5K6eP/bFGFevXkVpaamkViuGKCIiIqq3goKCcO/ePZSUlBh8LEMUERGR3LE7zyhCCEnHMUQRERHJHUOUWTBEERERkay99957ko8tKCiQfCxDFBERkdzV84Hlc+fOhUKhkHSsEELysQxRREREclfPu/MsLS1RVlaGl156CY6OjgYd+80336CoqEhSuQxRREREJGtt2rTByZMnMW7cOPTt29egY3fu3Im7d+9KKlfGjXdEREQEAFDA+OfmSevRqhWCg4MBAMnJyTVaLkMUERGR3FmaaJGp4OBgCCHw22+/GXys1OkNAHbnERERyV89HxPVp08fTJkyBe7u7gYf+8MPP6C4uFhSuQxRREREJGv+/v74+OOPJR3brVs3yeUyRBEREcldPZ/iwFwYooiIiOSunnfnmQtzJxEREZEEbIkiIiKSO7ZEabG01P9iLCws4OTkBH9/f4SGhiImJgbt27fX71ipFSQiIqJawtg5okwx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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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PgGFKhYDsirs33//haurq767QUREWkpISICLi4vO683JyYGFuTl0tfbI0dERd+/eZVCmBwzIqjD58/ES+gM2xnruDFEFcdyj7x4QVRwBIAdQ+bxTXcjLy4MAYA5A23kUASAxMRF5eXkMyPSAAVkVJp+mtDFmQEbVFyfjqSao6GUnhtBNQEb6w4CMiIhI4hiQSR/vsiQiIiLSM46QERERSZwBOEImdQzIiIiIJM4A2k95FemiI1RuDMiIiIgkzhDaB2S8wUa/uIaMiIiISM84QkZERCRxupiyJP1iQEZERCRxnLKUPgbURERERHrGETIiIiKJ4wiZ9DEgIyIikjiuIZM+vn9EREREesYRMiIiIokzwLNpS5IuBmREREQSp4spSz46Sb84ZUlERESkZxwhIyIikjhDcMpS6hiQERERSRwDMuljQEZERCRxXEMmfVxDRkRERKRnHCEjIiKSOE5ZSh8DMiIiIoljQCZ9nLIkIiIi0jOOkBEREUmcDNqPsBTpoiNUbgzIiIiIJE4XU5a8y1K/OGVJREREpGccISMiIpI4XeQh4wiNfjEgIyIikjhOWUofA2IiIiIiPeMIGRERkcRxhEz6GJARERFJHNeQSR8DMiIiIonjCJn0MSAmIiIi0jOOkBEREUmcAbQfIWOmfv1iQEZERCRxXEMmfbz+RERERHrGETIiIiKJ08Wifk5Z6hcDMiIiIonjlKX08foTERFRpTtx4gRmzZqFgIAA2NraQiaTYcyYMeWqSyaTlfpatmyZbjteQThCRkREJHFSnLLcsGEDNm3aBAsLCzRo0ADp6ela1efm5qYyoPPz89Oq3srCgIyIiEjipBiQTZs2De+//z68vb1x7tw5dO7cWav63N3dERoaqpvO6QEDMiIiIqp07dq103cXqhQGZERERBLHRf1Aamoq1q1bh6SkJNSpUwc9evSAp6envrulNgZkREREEqeLTP2F///f59dymZqawtTUVMvaK97ly5cxceJExc8ymQwjR47EmjVrYGFhoceeqUfqATEREVGNZ6ijFwC4urrC1tZW8Vq6dGllnkq5zJo1C2fOnEFycjJSUlJw7NgxdOzYEVu3bsX48eP13T21cISMiIiIFBISEmBjY6P4uazRMQcHBzx58kTtuiMiItCjRw9tuqfS559/rvRzQEAAjh49itatW2PHjh1YsGABmjdv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\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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atQIc+fOxcyZM+Hk5KRHzyof0yZUYUybQDUC0yZQNVZZaROGA7A0sK58AFuBCutrdccRKiIiIpkzxb38SB0DKiIiIpkz1ZQf/T8GpEREREQG4ggVERGRzHHKz/QYUBEREckcp/xMjwEpERERkYE4QkVERCRzHKEyPQZUREREMifB8CknyRgdqcE45UdERERkII5QERERyRyn/EyPARUREZHMMaAyPQZUREREMsc8VKbH60dERERkII5QERERyRyn/EyPARUREZHMccrP9Hj9iIiIiAzEESoiIiKZ45Sf6TGgIiIikjkzGB4QccrKMLx+RERERAbiCBUREZHMcVG66TGgIiIikjmuoTI9BqREREREBuIIFRERkcxxhMr0GFARERHJHNdQmR4DKiIiIpnjCJXpMSAlIiIiMhBHqIiIiGSOU36mx4CKiIhI5pgp3fR4/YiIiIgMxBEqIiIimeOidNNjQEVERCRzXENlerx+RERERAbiCBUREZHMccrP9BhQERERyRwDKtPjlB8RERGRgThCRUREJHNclG56DKiIiIhkjlN+pseAioiISOYkGD7CJBmjIzVYlR/hy8jIwLRp09ClSxe4ublBoVCgfv366NmzJ/773/9CCFHimKysLMyYMQPe3t5QKBTw9vbGjBkzkJWVpbWdLVu2oGPHjrCxsYGTkxOeffZZxMfHl7u/+rRNRERE8iYJTRFJFXLhwgW0adMGnTt3RqNGjeDs7Izbt29j165duH37NiZMmIBvv/1WVT4nJwfdunXDmTNnEBoainbt2iEhIQH79u1DmzZtEBsbCxsbG7U2Fi1ahLlz58LLywuDBw/G/fv3sW3bNuTm5iIyMhLBwcE69VWftkuTlZUFBwcHZA4B7GvpfBiRvGyq0h9BRAZRfY5nZsLe3r7C6v8OQG0D63oAYBxQYX2t7qr8lF+DBg2QkZEBCwv1rmZnZ6Nz585YvXo1pk+fjhYtWgAAlixZgjNnzmD27Nn4+OOPVeXDwsIwf/58LFmyBBEREartqampCAsLg5+fH06dOgUHBwcAwLRp09CxY0eMHz8eycnJJdrXpLxtExERGQPXUJlelZ/yMzc31xjM2NnZoU+fPgAejWIBgBACa9asga2tLT744AO18u+++y6cnJywdu1atWnCdevWobCwEHPnzlUFUwDQokULjBo1ChcvXsThw4fL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hz08PBwHDx7EihUrKq1NIqrZGFAR1SCWlpbYv38/XnzxRdy8eROjRo2Cs7MzWrZsiY4dO8LDwwNNmjTB8uXL4ebmhkaNGgEAoqKicPXqVTz//PPIyMhARkYG7O3t0alTJ2zevFktWHJycsK9e/dKtH3v3j04OztX2rkSEVUmBlRENYytrS127NiBmJgYvPrqq/D09MSlS5eQkJAAIQT69euHtWvXIiUlBf7+/gD+P0XCW2+9BScnJ9XjxIkTuHz5MmJjY1X1t2jRAufOnSvR7rlz59CsWbPKOUkiokrGtAlENVT37t3RvXv3Msvl5uZix44deOaZZ/DOO++o7SsoKMCAAQOwadMmVV3PPfcc5s6dq5ZS4ffff8f58+exePFio55DWevAnuTh4VGpU49EVHNIgp8uRFSK7du3Y+jQodi9ezf69etXYv/QoUNx4MAB3Lx5E5aWlsjOzkarVq1Qp04dhIWFITc3F++88w6eeuopHD9+HGZmZQ+MX7p0CQ0aNIBCoVDlkBo3bhzGjRtn9PNTGjt2LFJTU5GZmYnExEQEBQUhKiqqwtojouqFU35EVKpNmzbBzc0NzzzzjMb9Y8eOxb1797Bnzx4AjzKyHz58GG5ubhg6dCheffVVdO7cGbt379YpmHpcXl4e4uLiEBcXhytXrhh8LqX5448/EBcXh8TExApth4iqJ45QERERERmII1REREREBmJARURERGQgBlREREREBmJARURERGQgBlREREREBmJARURERGQgBlREREREBmJARURERGQgBlREREREBmJARURERGQgBlREREREBmJARURERGSg/wNLJnUzYAtbewAAAABJRU5ErkJggg==\n", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# This problem has three degrees of freedom. To draw the heatmap, it needs to fix one dimension\n", - "fixed = {\n", - " \"('T[0.125]','T[0.25]','T[0.375]','T[0.5]','T[0.625]','T[0.75]','T[0.875]','T[1]')\": 300\n", - "}\n", - "\n", - "all_fim.figure_drawing(\n", - " fixed, [\"CA0[0]\", \"T[0]\"], \"Reactor case\", \"$C_{A0}$ [M]\", \"T [K]\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As seen in the Reactor Case - A optimality figure, A-optimality shows that the most informative region is around $C_{A0}=5.0$ M, $T=300.0$ K, while the least informative region is around $C_{A0}=1.0$ M, $T=700.0$ K.\n", - "\n", - "As seen in the Reactor Case - D optimality figure, D-optimality shows that the most informative region is around $C_{A0}=5.0$ M, $T=300.0$ K, while the least informative region is around $C_{A0}=1.0$ M, $T=700.0$ K.\n", - "\n", - "As seen in the Reactor Case - E optimality figure, E-optimality shows that the most informative region is around $C_{A0}=5.0$ M, $T=300.0$ K, while the least informative region is around $C_{A0}=1.0$ M, $T=700.0$ K.\n", - "\n", - "As seen in the Reactor Case - Modified E optimality figure, ME-optimality shows that the most informative region is around $C_{A0}=1.0$ M, $T=700.0$ K, while the least informative region is around $C_{A0}=5.0$ M, $T=300.0$ K." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Key Takeaways\n", - "\n", - "* MBDoE maximizes the information gained from experiments which reduces uncertainty (technical risk) and facilitates better decision-making.\n", - "\n", - "* FIM quantifies the information contained in a set of experiments (data) with respect to a mathematical model\n", - "\n", - "* MBDoE optimality criteria (e.g., A, D, E-optimal designs) compress the FIM into a scalar. The \"correct\" criterion depends on the DoE goal and model context.\n", - "\n", - "* Heatmaps provide visualizations of the most informative parameters using the MBDoE optimality criteria." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.11" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/pyomo/contrib/doe/examples/reactor_compute_FIM.py b/pyomo/contrib/doe/examples/reactor_compute_FIM.py deleted file mode 100644 index 108f5bd16a0..00000000000 --- a/pyomo/contrib/doe/examples/reactor_compute_FIM.py +++ /dev/null @@ -1,111 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# -# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation -# Initiative (CCSI), and is copyright (c) 2022 by the software owners: -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, -# University of Toledo, West Virginia University, et al. All rights reserved. -# -# NOTICE. This Software was developed under funding from the -# U.S. Department of Energy and the U.S. Government consequently retains -# certain rights. As such, the U.S. Government has been granted for itself -# and others acting on its behalf a paid-up, nonexclusive, irrevocable, -# worldwide license in the Software to reproduce, distribute copies to the -# public, prepare derivative works, and perform publicly and display -# publicly, and to permit other to do so. -# ___________________________________________________________________________ - - -from pyomo.common.dependencies import numpy as np -from pyomo.contrib.doe.examples.reactor_kinetics import create_model, disc_for_measure -from pyomo.contrib.doe import DesignOfExperiments, MeasurementVariables, DesignVariables - - -def main(): - ### Define inputs - # Control time set [h] - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - # Define parameter nominal value - parameter_dict = {"A1": 85, "A2": 370, "E1": 8, "E2": 15} - - # Define measurement object - measurements = MeasurementVariables() - measurements.add_variables( - "C", # measurement variable name - indices={ - 0: ["CA", "CB", "CC"], - 1: t_control, - }, # 0,1 are indices of the index sets - time_index_position=1, - ) - - # design object - exp_design = DesignVariables() - - # add CAO as design variable - exp_design.add_variables( - "CA0", # design variable name - indices={0: [0]}, # index dictionary - time_index_position=0, # time index position - values=[5], # design variable values - lower_bounds=1, # design variable lower bounds - upper_bounds=5, # design variable upper bounds - ) - - # add T as design variable - exp_design.add_variables( - "T", # design variable name - indices={0: t_control}, # index dictionary - time_index_position=0, # time index position - values=[ - 570, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - ], # same length with t_control - lower_bounds=300, # design variable lower bounds - upper_bounds=700, # design variable upper bounds - ) - - ### Compute the FIM of a square model-based Design of Experiments problem - doe_object = DesignOfExperiments( - parameter_dict, # parameter dictionary - exp_design, # DesignVariables object - measurements, # MeasurementVariables object - create_model, # create model function - discretize_model=disc_for_measure, # discretize model function - ) - - result = doe_object.compute_FIM( - mode="sequential_finite", # calculation mode - scale_nominal_param_value=True, # scale nominal parameter value - formula="central", # formula for finite difference - ) - - result.result_analysis() - - # test result - relative_error = abs(np.log10(result.trace) - 2.78) - assert relative_error < 0.01 - - relative_error = abs(np.log10(result.det) - 2.99) - assert relative_error < 0.01 - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/doe/examples/reactor_design.py b/pyomo/contrib/doe/examples/reactor_design.py deleted file mode 100644 index 67d6ff02fd2..00000000000 --- a/pyomo/contrib/doe/examples/reactor_design.py +++ /dev/null @@ -1,236 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# -# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation -# Initiative (CCSI), and is copyright (c) 2022 by the software owners: -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, -# University of Toledo, West Virginia University, et al. All rights reserved. -# -# NOTICE. This Software was developed under funding from the -# U.S. Department of Energy and the U.S. Government consequently retains -# certain rights. As such, the U.S. Government has been granted for itself -# and others acting on its behalf a paid-up, nonexclusive, irrevocable, -# worldwide license in the Software to reproduce, distribute copies to the -# public, prepare derivative works, and perform publicly and display -# publicly, and to permit other to do so. -# ___________________________________________________________________________ - -# from pyomo.contrib.parmest.examples.reactor_design import reactor_design_model -# if we refactor to use the same create_model function as parmest, -# we can just import instead of redefining the model - -import pyomo.environ as pyo -from pyomo.dae import ContinuousSet, DerivativeVar -from pyomo.contrib.doe import ( - ModelOptionLib, - DesignOfExperiments, - MeasurementVariables, - DesignVariables, -) -from pyomo.common.dependencies import numpy as np - - -def create_model_legacy(mod=None, model_option=None): - model_option = ModelOptionLib(model_option) - - model = mod - - if model_option == ModelOptionLib.parmest: - model = pyo.ConcreteModel() - return_m = True - elif model_option == ModelOptionLib.stage1 or model_option == ModelOptionLib.stage2: - if model is None: - raise ValueError( - "If model option is stage1 or stage2, a created model needs to be provided." - ) - return_m = False - else: - raise ValueError( - "model_option needs to be defined as parmest, stage1, or stage2." - ) - - model = _create_model_details(model) - - if return_m: - return model - - -def create_model(): - model = pyo.ConcreteModel() - return _create_model_details(model) - - -def _create_model_details(model): - - # Rate constants - model.k1 = pyo.Var(initialize=5.0 / 6.0, within=pyo.PositiveReals) # min^-1 - model.k2 = pyo.Var(initialize=5.0 / 3.0, within=pyo.PositiveReals) # min^-1 - model.k3 = pyo.Var( - initialize=1.0 / 6000.0, within=pyo.PositiveReals - ) # m^3/(gmol min) - - # Inlet concentration of A, gmol/m^3 - model.caf = pyo.Var(initialize=10000, within=pyo.PositiveReals) - - # Space velocity (flowrate/volume) - model.sv = pyo.Var(initialize=1.0, within=pyo.PositiveReals) - - # Outlet concentration of each component - model.ca = pyo.Var(initialize=5000.0, within=pyo.PositiveReals) - model.cb = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) - model.cc = pyo.Var(initialize=2000.0, within=pyo.PositiveReals) - model.cd = pyo.Var(initialize=1000.0, within=pyo.PositiveReals) - - # Constraints - model.ca_bal = pyo.Constraint( - expr=( - 0 - == model.sv * model.caf - - model.sv * model.ca - - model.k1 * model.ca - - 2.0 * model.k3 * model.ca**2.0 - ) - ) - - model.cb_bal = pyo.Constraint( - expr=(0 == -model.sv * model.cb + model.k1 * model.ca - model.k2 * model.cb) - ) - - model.cc_bal = pyo.Constraint( - expr=(0 == -model.sv * model.cc + model.k2 * model.cb) - ) - - model.cd_bal = pyo.Constraint( - expr=(0 == -model.sv * model.cd + model.k3 * model.ca**2.0) - ) - - return model - - -def main(legacy_create_model_interface=False): - - # measurement object - measurements = MeasurementVariables() - measurements.add_variables("ca", indices=None, time_index_position=None) - measurements.add_variables("cb", indices=None, time_index_position=None) - measurements.add_variables("cc", indices=None, time_index_position=None) - measurements.add_variables("cd", indices=None, time_index_position=None) - - # design object - exp_design = DesignVariables() - exp_design.add_variables( - "sv", - indices=None, - time_index_position=None, - values=1.0, - lower_bounds=0.1, - upper_bounds=10.0, - ) - exp_design.add_variables( - "caf", - indices=None, - time_index_position=None, - values=10000, - lower_bounds=5000, - upper_bounds=15000, - ) - - theta_values = {"k1": 5.0 / 6.0, "k2": 5.0 / 3.0, "k3": 1.0 / 6000.0} - - if legacy_create_model_interface: - create_model_ = create_model_legacy - else: - create_model_ = create_model - - doe1 = DesignOfExperiments( - theta_values, exp_design, measurements, create_model_, prior_FIM=None - ) - - result = doe1.compute_FIM( - mode="sequential_finite", # calculation mode - scale_nominal_param_value=True, # scale nominal parameter value - formula="central", # formula for finite difference - ) - - # doe1.model.pprint() - - result.result_analysis() - - # print("FIM =\n",result.FIM) - # print("jac =\n",result.jaco_information) - # print("log10 Trace of FIM: ", np.log10(result.trace)) - # print("log10 Determinant of FIM: ", np.log10(result.det)) - - # test result - expected_log10_trace = 6.815 - log10_trace = np.log10(result.trace) - relative_error_trace = abs(log10_trace - 6.815) - assert relative_error_trace < 0.01, ( - "log10(tr(FIM)) regression test failed, answer " - + str(round(log10_trace, 3)) - + " does not match expected answer of " - + str(expected_log10_trace) - ) - - expected_log10_det = 18.719 - log10_det = np.log10(result.det) - relative_error_det = abs(log10_det - 18.719) - assert relative_error_det < 0.01, ( - "log10(det(FIM)) regression test failed, answer " - + str(round(log10_det, 3)) - + " does not match expected answer of " - + str(expected_log10_det) - ) - - doe2 = DesignOfExperiments( - theta_values, exp_design, measurements, create_model_, prior_FIM=None - ) - - square_result2, optimize_result2 = doe2.stochastic_program( - if_optimize=True, - if_Cholesky=True, - scale_nominal_param_value=True, - objective_option="det", - jac_initial=result.jaco_information.copy(), - step=0.1, - ) - - optimize_result2.result_analysis() - log_det = np.log10(optimize_result2.det) - print("log(det) = ", round(log_det, 3)) - log_det_expected = 19.266 - assert abs(log_det - log_det_expected) < 0.01, "log(det) regression test failed" - - doe3 = DesignOfExperiments( - theta_values, exp_design, measurements, create_model_, prior_FIM=None - ) - - square_result3, optimize_result3 = doe3.stochastic_program( - if_optimize=True, - scale_nominal_param_value=True, - objective_option="trace", - jac_initial=result.jaco_information.copy(), - step=0.1, - ) - - optimize_result3.result_analysis() - log_trace = np.log10(optimize_result3.trace) - log_trace_expected = 7.509 - print("log(trace) = ", round(log_trace, 3)) - assert ( - abs(log_trace - log_trace_expected) < 0.01 - ), "log(trace) regression test failed" - - -if __name__ == "__main__": - main(legacy_create_model_interface=False) diff --git a/pyomo/contrib/doe/examples/reactor_grid_search.py b/pyomo/contrib/doe/examples/reactor_grid_search.py deleted file mode 100644 index 1f5aae77f85..00000000000 --- a/pyomo/contrib/doe/examples/reactor_grid_search.py +++ /dev/null @@ -1,140 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# -# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation -# Initiative (CCSI), and is copyright (c) 2022 by the software owners: -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, -# University of Toledo, West Virginia University, et al. All rights reserved. -# -# NOTICE. This Software was developed under funding from the -# U.S. Department of Energy and the U.S. Government consequently retains -# certain rights. As such, the U.S. Government has been granted for itself -# and others acting on its behalf a paid-up, nonexclusive, irrevocable, -# worldwide license in the Software to reproduce, distribute copies to the -# public, prepare derivative works, and perform publicly and display -# publicly, and to permit other to do so. -# ___________________________________________________________________________ - - -from pyomo.common.dependencies import numpy as np -from pyomo.contrib.doe.examples.reactor_kinetics import create_model, disc_for_measure -from pyomo.contrib.doe import DesignOfExperiments, MeasurementVariables, DesignVariables - - -def main(): - ### Define inputs - # Control time set [h] - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - # Define parameter nominal value - parameter_dict = {"A1": 85, "A2": 372, "E1": 8, "E2": 15} - - # measurement object - measurements = MeasurementVariables() - measurements.add_variables( - "C", # variable name - indices={0: ["CA", "CB", "CC"], 1: t_control}, # indices - time_index_position=1, - ) # position of time index - - # design object - exp_design = DesignVariables() - - # add CAO as design variable - exp_design.add_variables( - "CA0", # variable name - indices={0: [0]}, # indices - time_index_position=0, # position of time index - values=[5], # nominal value - lower_bounds=1, # lower bound - upper_bounds=5, # upper bound - ) - - # add T as design variable - exp_design.add_variables( - "T", # variable name - indices={0: t_control}, # indices - time_index_position=0, # position of time index - values=[470, 300, 300, 300, 300, 300, 300, 300, 300], # nominal value - lower_bounds=300, # lower bound - upper_bounds=700, # upper bound - ) - - # For each variable, we define a list of possible values that are used - # in the sensitivity analysis - - design_ranges = { - "CA0[0]": [1, 3, 5], - ( - "T[0]", - "T[0.125]", - "T[0.25]", - "T[0.375]", - "T[0.5]", - "T[0.625]", - "T[0.75]", - "T[0.875]", - "T[1]", - ): [300, 500, 700], - } - ## choose from "sequential_finite", "direct_kaug" - sensi_opt = "direct_kaug" - - doe_object = DesignOfExperiments( - parameter_dict, # parameter dictionary - exp_design, # design variables - measurements, # measurement variables - create_model, # model function - discretize_model=disc_for_measure, # discretization function - ) - # run full factorial grid search - all_fim = doe_object.run_grid_search(design_ranges, mode=sensi_opt) - - all_fim.extract_criteria() - - ### 3 design variable example - # Define design ranges - design_ranges = { - "CA0[0]": list(np.linspace(1, 5, 2)), - "T[0]": list(np.linspace(300, 700, 2)), - ( - "T[0.125]", - "T[0.25]", - "T[0.375]", - "T[0.5]", - "T[0.625]", - "T[0.75]", - "T[0.875]", - "T[1]", - ): [300, 500], - } - - sensi_opt = "direct_kaug" - - doe_object = DesignOfExperiments( - parameter_dict, # parameter dictionary - exp_design, # design variables - measurements, # measurement variables - create_model, # model function - discretize_model=disc_for_measure, # discretization function - ) - # run the grid search for 3 dimensional case - all_fim = doe_object.run_grid_search(design_ranges, mode=sensi_opt) - - all_fim.extract_criteria() - - # see the criteria values - all_fim.store_all_results_dataframe - - -if __name__ == "__main__": - main() diff --git a/pyomo/contrib/doe/examples/reactor_kinetics.py b/pyomo/contrib/doe/examples/reactor_kinetics.py deleted file mode 100644 index ed2175085f2..00000000000 --- a/pyomo/contrib/doe/examples/reactor_kinetics.py +++ /dev/null @@ -1,247 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# -# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation -# Initiative (CCSI), and is copyright (c) 2022 by the software owners: -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, -# University of Toledo, West Virginia University, et al. All rights reserved. -# -# NOTICE. This Software was developed under funding from the -# U.S. Department of Energy and the U.S. Government consequently retains -# certain rights. As such, the U.S. Government has been granted for itself -# and others acting on its behalf a paid-up, nonexclusive, irrevocable, -# worldwide license in the Software to reproduce, distribute copies to the -# public, prepare derivative works, and perform publicly and display -# publicly, and to permit other to do so. -# ___________________________________________________________________________ - - -import pyomo.environ as pyo -from pyomo.dae import ContinuousSet, DerivativeVar -from pyomo.contrib.doe import ModelOptionLib - - -def disc_for_measure(m, nfe=32, block=True): - """Pyomo.DAE discretization - - Arguments - --------- - m: Pyomo model - nfe: number of finite elements b - block: if True, the input model has blocks - """ - discretizer = pyo.TransformationFactory("dae.collocation") - if block: - for s in range(len(m.block)): - discretizer.apply_to(m.block[s], nfe=nfe, ncp=3, wrt=m.block[s].t) - else: - discretizer.apply_to(m, nfe=nfe, ncp=3, wrt=m.t) - return m - - -def create_model( - mod=None, - model_option="stage2", - control_time=[0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1], - control_val=None, - t_range=[0.0, 1], - CA_init=1, - C_init=0.1, -): - """ - This is an example user model provided to DoE library. - It is a dynamic problem solved by Pyomo.DAE. - - Arguments - --------- - mod: Pyomo model. If None, a Pyomo concrete model is created - model_option: choose from the 3 options in model_option - if ModelOptionLib.parmest, create a process model. - if ModelOptionLib.stage1, create the global model. - if ModelOptionLib.stage2, add model variables and constraints for block. - control_time: a list of control timepoints - control_val: control design variable values T at corresponding timepoints - t_range: time range, h - CA_init: time-independent design (control) variable, an initial value for CA - C_init: An initial value for C - - Return - ------ - m: a Pyomo.DAE model - """ - - theta = {"A1": 84.79, "A2": 371.72, "E1": 7.78, "E2": 15.05} - - model_option = ModelOptionLib(model_option) - - if model_option == ModelOptionLib.parmest: - mod = pyo.ConcreteModel() - return_m = True - elif model_option == ModelOptionLib.stage1 or model_option == ModelOptionLib.stage2: - if not mod: - raise ValueError( - "If model option is stage1 or stage2, a created model needs to be provided." - ) - return_m = False - else: - raise ValueError( - "model_option needs to be defined as parmest,stage1, or stage2." - ) - - if not control_val: - control_val = [300] * 9 - - controls = {} - for i, t in enumerate(control_time): - controls[t] = control_val[i] - - mod.t0 = pyo.Set(initialize=[0]) - mod.t_con = pyo.Set(initialize=control_time) - mod.CA0 = pyo.Var( - mod.t0, initialize=CA_init, bounds=(1.0, 5.0), within=pyo.NonNegativeReals - ) # mol/L - - # check if control_time is in time range - assert ( - control_time[0] >= t_range[0] and control_time[-1] <= t_range[1] - ), "control time is outside time range." - - if model_option == ModelOptionLib.stage1: - mod.T = pyo.Var( - mod.t_con, - initialize=controls, - bounds=(300, 700), - within=pyo.NonNegativeReals, - ) - return - - else: - para_list = ["A1", "A2", "E1", "E2"] - - ### Add variables - mod.CA_init = CA_init - mod.para_list = para_list - - # timepoints - mod.t = ContinuousSet(bounds=t_range, initialize=control_time) - - # time-dependent design variable, initialized with the first control value - def T_initial(m, t): - if t in m.t_con: - return controls[t] - else: - # count how many control points are before the current t; - # locate the nearest neighbouring control point before this t - neighbour_t = max(tc for tc in control_time if tc < t) - return controls[neighbour_t] - - mod.T = pyo.Var( - mod.t, initialize=T_initial, bounds=(300, 700), within=pyo.NonNegativeReals - ) - - mod.R = 8.31446261815324 # J / K / mole - - # Define parameters as Param - mod.A1 = pyo.Var(initialize=theta["A1"]) - mod.A2 = pyo.Var(initialize=theta["A2"]) - mod.E1 = pyo.Var(initialize=theta["E1"]) - mod.E2 = pyo.Var(initialize=theta["E2"]) - - # Concentration variables under perturbation - mod.C_set = pyo.Set(initialize=["CA", "CB", "CC"]) - mod.C = pyo.Var( - mod.C_set, mod.t, initialize=C_init, within=pyo.NonNegativeReals - ) - - # time derivative of C - mod.dCdt = DerivativeVar(mod.C, wrt=mod.t) - - # kinetic parameters - def kp1_init(m, t): - return m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t])) - - def kp2_init(m, t): - return m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t])) - - mod.kp1 = pyo.Var(mod.t, initialize=kp1_init) - mod.kp2 = pyo.Var(mod.t, initialize=kp2_init) - - def T_control(m, t): - """ - T at interval timepoint equal to the T of the control time point at the beginning of this interval - Count how many control points are before the current t; - locate the nearest neighbouring control point before this t - """ - if t in m.t_con: - return pyo.Constraint.Skip - else: - neighbour_t = max(tc for tc in control_time if tc < t) - return m.T[t] == m.T[neighbour_t] - - def cal_kp1(m, t): - """ - Create the perturbation parameter sets - m: model - t: time - """ - # LHS: 1/h - # RHS: 1/h*(kJ/mol *1000J/kJ / (J/mol/K) / K) - return m.kp1[t] == m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t])) - - def cal_kp2(m, t): - """ - Create the perturbation parameter sets - m: model - t: time - """ - # LHS: 1/h - # RHS: 1/h*(kJ/mol *1000J/kJ / (J/mol/K) / K) - return m.kp2[t] == m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t])) - - def dCdt_control(m, y, t): - """ - Calculate CA in Jacobian matrix analytically - y: CA, CB, CC - t: timepoints - """ - if y == "CA": - return m.dCdt[y, t] == -m.kp1[t] * m.C["CA", t] - elif y == "CB": - return m.dCdt[y, t] == m.kp1[t] * m.C["CA", t] - m.kp2[t] * m.C["CB", t] - elif y == "CC": - return pyo.Constraint.Skip - - def alge(m, t): - """ - The algebraic equation for mole balance - z: m.pert - t: time - """ - return m.C["CA", t] + m.C["CB", t] + m.C["CC", t] == m.CA0[0] - - # Control time - mod.T_rule = pyo.Constraint(mod.t, rule=T_control) - - # calculating C, Jacobian, FIM - mod.k1_pert_rule = pyo.Constraint(mod.t, rule=cal_kp1) - mod.k2_pert_rule = pyo.Constraint(mod.t, rule=cal_kp2) - mod.dCdt_rule = pyo.Constraint(mod.C_set, mod.t, rule=dCdt_control) - - mod.alge_rule = pyo.Constraint(mod.t, rule=alge) - - # B.C. - mod.C["CB", 0.0].fix(0.0) - mod.C["CC", 0.0].fix(0.0) - - if return_m: - return mod diff --git a/pyomo/contrib/doe/examples/reactor_optimize_doe.py b/pyomo/contrib/doe/examples/reactor_optimize_doe.py deleted file mode 100644 index f7b4a74c891..00000000000 --- a/pyomo/contrib/doe/examples/reactor_optimize_doe.py +++ /dev/null @@ -1,123 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# -# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation -# Initiative (CCSI), and is copyright (c) 2022 by the software owners: -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, -# University of Toledo, West Virginia University, et al. All rights reserved. -# -# NOTICE. This Software was developed under funding from the -# U.S. Department of Energy and the U.S. Government consequently retains -# certain rights. As such, the U.S. Government has been granted for itself -# and others acting on its behalf a paid-up, nonexclusive, irrevocable, -# worldwide license in the Software to reproduce, distribute copies to the -# public, prepare derivative works, and perform publicly and display -# publicly, and to permit other to do so. -# ___________________________________________________________________________ - -from pyomo.common.dependencies import numpy as np -from pyomo.contrib.doe.examples.reactor_kinetics import create_model, disc_for_measure -from pyomo.contrib.doe import DesignOfExperiments, MeasurementVariables, DesignVariables - - -def main(): - ### Define inputs - # Control time set [h] - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - # Define parameter nominal value - parameter_dict = {"A1": 85, "A2": 372, "E1": 8, "E2": 15} - - # measurement object - measurements = MeasurementVariables() - measurements.add_variables( - "C", # name of measurement - indices={0: ["CA", "CB", "CC"], 1: t_control}, # indices of measurement - time_index_position=1, - ) # position of time index - - # design object - exp_design = DesignVariables() - - # add CAO as design variable - exp_design.add_variables( - "CA0", # name of design variable - indices={0: [0]}, # indices of design variable - time_index_position=0, # position of time index - values=[5], # nominal value of design variable - lower_bounds=1, # lower bound of design variable - upper_bounds=5, # upper bound of design variable - ) - - # add T as design variable - exp_design.add_variables( - "T", # name of design variable - indices={0: t_control}, # indices of design variable - time_index_position=0, # position of time index - values=[ - 470, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - 300, - ], # nominal value of design variable - lower_bounds=300, # lower bound of design variable - upper_bounds=700, # upper bound of design variable - ) - - design_names = exp_design.variable_names - exp1 = [5, 570, 300, 300, 300, 300, 300, 300, 300, 300] - exp1_design_dict = dict(zip(design_names, exp1)) - exp_design.update_values(exp1_design_dict) - - # add a prior information (scaled FIM with T=500 and T=300 experiments) - prior = np.asarray( - [ - [28.67892806, 5.41249739, -81.73674601, -24.02377324], - [5.41249739, 26.40935036, -12.41816477, -139.23992532], - [-81.73674601, -12.41816477, 240.46276004, 58.76422806], - [-24.02377324, -139.23992532, 58.76422806, 767.25584508], - ] - ) - - doe_object2 = DesignOfExperiments( - parameter_dict, # dictionary of parameters - exp_design, # design variables - measurements, # measurement variables - create_model, # function to create model - prior_FIM=prior, # prior information - discretize_model=disc_for_measure, # function to discretize model - ) - - square_result, optimize_result = doe_object2.stochastic_program( - if_optimize=True, # if optimize - if_Cholesky=True, # if use Cholesky decomposition - scale_nominal_param_value=True, # if scale nominal parameter value - objective_option="det", # objective option - L_initial=np.linalg.cholesky(prior), # initial Cholesky decomposition - ) - - square_result, optimize_result = doe_object2.stochastic_program( - if_optimize=True, # if optimize - if_Cholesky=True, # if use Cholesky decomposition - scale_nominal_param_value=True, # if scale nominal parameter value - objective_option="trace", # objective option - L_initial=np.linalg.cholesky(prior), # initial Cholesky decomposition - ) - - -if __name__ == "__main__": - main() From e6feef6e20f8ff4acd03960efd435a2a2b526584 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 18 Jul 2024 11:54:33 -0600 Subject: [PATCH 1977/3044] Add test for bad option in compute FIM method --- pyomo/contrib/doe/tests/test_doe_errors.py | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 3df8355e528..00336ff0420 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -989,6 +989,7 @@ def test_update_unknown_parameter_values_not_implemented_seq(self): ): doe_obj.update_unknown_parameter_values() + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_bad_FD_generate_scens(self): fd_method = "central" @@ -1025,6 +1026,7 @@ def test_bad_FD_generate_scens(self): doe_obj.fd_formula = "bad things" doe_obj._generate_scenario_blocks() + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_bad_FD_seq_compute_FIM(self): fd_method = "central" @@ -1132,6 +1134,44 @@ def test_no_model_for_objective(self): ): doe_obj.create_objective_function() + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_bad_compute_FIM_option(self): + fd_method = "central" + obj_used = "trace" + flag_val = ( + 0 # Value for faulty model build mode - 5: Mismatch error and output length + ) + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args={"flag": flag_val}, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + with self.assertRaisesRegex( + ValueError, + "The method provided, {}, must be either `sequential` or `kaug`".format( + "Bad Method" + ), + ): + doe_obj.compute_FIM(method="Bad Method") + if __name__ == "__main__": unittest.main() From 6391b18cc5d028eb7a86600428c2c0b50f6d5d36 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 18 Jul 2024 12:04:20 -0600 Subject: [PATCH 1978/3044] Make pretriangularization optional --- pyomo/contrib/pyros/config.py | 14 ++++ pyomo/contrib/pyros/util.py | 146 ++++++++++++++++++---------------- 2 files changed, 91 insertions(+), 69 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index c02dcd7ed0f..f73095b9db0 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -814,6 +814,20 @@ def pyros_config(): # ================================================ # === Advanced Options # ================================================ + CONFIG.declare( + "skip_pretriangularization", + ConfigValue( + default=True, + domain=bool, + description=( + """ + True to skip pretriangularization of the equality + constraints to determine nonadjustable variables + during preprocessing, False otherwise. + """ + ), + ), + ) CONFIG.declare( "bypass_local_separation", ConfigValue( diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index e65f03a1a0f..a6020e798c8 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -1257,80 +1257,88 @@ def get_effective_var_partitioning(model_data, config): " the variable is fixed by domain/bounds" ) - uncertain_params_set = ComponentSet(working_model.uncertain_params) - - # determine constraints that are potentially applicable for - # pretriangularization - certain_eq_cons = ComponentSet() - for wcon in working_model.component_data_objects(Constraint, active=True): - if not wcon.equality: - continue - if ComponentSet(identify_mutable_parameters(wcon.expr)) & uncertain_params_set: - continue - certain_eq_cons.add(wcon) - - pretriangular_con_var_map = ComponentMap() - for num_passes in it.count(1): - config.progress_logger.debug( - f"Performing pass number {num_passes} over the certain constraints." - ) - new_pretriangular_con_var_map = ComponentMap() - for ccon in certain_eq_cons: - vars_in_con = ComponentSet(identify_variables(ccon.body - ccon.upper)) - adj_vars_in_con = vars_in_con - nonadjustable_var_set - - # conditions for pretriangularization of constraint - # with no uncertain params: - # - only one nonadjustable variable in the constraint - # - the nonadjustable variable appears only linearly, - # and the linear coefficient exceeds our specified - # tolerance. - if len(adj_vars_in_con) == 1: - adj_var_in_con = next(iter(adj_vars_in_con)) - ccon_expr_repn = generate_standard_repn( - expr=ccon.body - ccon.upper, - quadratic=False, - compute_values=True, - ) - adj_var_appears_linearly = ( - adj_var_in_con not in ComponentSet(ccon_expr_repn.nonlinear_vars) - and adj_var_in_con in ComponentSet(ccon_expr_repn.linear_vars) - ) - if adj_var_appears_linearly: - # get coefficient by summation just in case - # standard repn does not simplify completely - var_linear_coeff = sum( - lcoeff - for lvar, lcoeff - in zip(ccon_expr_repn.linear_vars, ccon_expr_repn.linear_coefs) - if lvar is adj_var_in_con - ) - if abs(var_linear_coeff) > PRETRIANGULAR_VAR_COEFF_TOL: - new_pretriangular_con_var_map[ccon] = adj_var_in_con - config.progress_logger.debug( - f" The variable {adj_var_in_con.name!r} is " - "made nonadjustable by the pretriangular constraint " - f"{ccon.name!r}." - ) + if not config.skip_pretriangularization: + uncertain_params_set = ComponentSet(working_model.uncertain_params) + + # determine constraints that are potentially applicable for + # pretriangularization + certain_eq_cons = ComponentSet() + for wcon in working_model.component_data_objects(Constraint, active=True): + if not wcon.equality: + continue + uncertain_params_in_expr = ( + ComponentSet(identify_mutable_parameters(wcon.expr)) + & uncertain_params_set + ) + if uncertain_params_in_expr: + continue + certain_eq_cons.add(wcon) - nonadjustable_var_set.update(new_pretriangular_con_var_map.values()) - pretriangular_con_var_map.update(new_pretriangular_con_var_map) - if not new_pretriangular_con_var_map: + pretriangular_con_var_map = ComponentMap() + for num_passes in it.count(1): config.progress_logger.debug( - "No new pretriangular constraint/variable pairs found. " - "Terminating pretriangularization loop." + f"Performing pass number {num_passes} over the certain constraints." ) - break + new_pretriangular_con_var_map = ComponentMap() + for ccon in certain_eq_cons: + vars_in_con = ComponentSet(identify_variables(ccon.body - ccon.upper)) + adj_vars_in_con = vars_in_con - nonadjustable_var_set + + # conditions for pretriangularization of constraint + # with no uncertain params: + # - only one nonadjustable variable in the constraint + # - the nonadjustable variable appears only linearly, + # and the linear coefficient exceeds our specified + # tolerance. + if len(adj_vars_in_con) == 1: + adj_var_in_con = next(iter(adj_vars_in_con)) + ccon_expr_repn = generate_standard_repn( + expr=ccon.body - ccon.upper, + quadratic=False, + compute_values=True, + ) + adj_var_appears_linearly = ( + adj_var_in_con + not in ComponentSet(ccon_expr_repn.nonlinear_vars) + and adj_var_in_con in ComponentSet(ccon_expr_repn.linear_vars) + ) + if adj_var_appears_linearly: + # get coefficient by summation just in case + # standard repn does not simplify completely + var_linear_coeff = sum( + lcoeff + for lvar, lcoeff + in zip( + ccon_expr_repn.linear_vars, ccon_expr_repn.linear_coefs + ) + if lvar is adj_var_in_con + ) + if abs(var_linear_coeff) > PRETRIANGULAR_VAR_COEFF_TOL: + new_pretriangular_con_var_map[ccon] = adj_var_in_con + config.progress_logger.debug( + f" The variable {adj_var_in_con.name!r} is " + "made nonadjustable by the pretriangular constraint " + f"{ccon.name!r}." + ) + + nonadjustable_var_set.update(new_pretriangular_con_var_map.values()) + pretriangular_con_var_map.update(new_pretriangular_con_var_map) + if not new_pretriangular_con_var_map: + config.progress_logger.debug( + "No new pretriangular constraint/variable pairs found. " + "Terminating pretriangularization loop." + ) + break - for pcon in new_pretriangular_con_var_map: - certain_eq_cons.remove(pcon) + for pcon in new_pretriangular_con_var_map: + certain_eq_cons.remove(pcon) - pretriangular_vars = ComponentSet(pretriangular_con_var_map.values()) - config.progress_logger.debug( - f"Identified {len(pretriangular_con_var_map)} pretriangular " - f"constraints and {len(pretriangular_vars)} pretriangular variables " - f"in {num_passes} passes over the certain constraints." - ) + pretriangular_vars = ComponentSet(pretriangular_con_var_map.values()) + config.progress_logger.debug( + f"Identified {len(pretriangular_con_var_map)} pretriangular " + f"constraints and {len(pretriangular_vars)} pretriangular variables " + f"in {num_passes} passes over the certain constraints." + ) effective_first_stage_vars = list(nonadjustable_var_set) effective_second_stage_vars = [ From f1df9be77bf5cac91c4bf50d65213dc514ce033f Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 18 Jul 2024 12:24:30 -0600 Subject: [PATCH 1979/3044] Removed bad files, have to rewrite OnlineDocs soon --- .../contributed_packages/doe/doe.rst | 53 +------------------ 1 file changed, 2 insertions(+), 51 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index 8c22ff7370d..ce48da3e516 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -151,28 +151,7 @@ Pyomo.DoE Solver Interface .. autoclass:: pyomo.contrib.doe.doe.DesignOfExperiments - :members: __init__, stochastic_program, compute_FIM, run_grid_search - -.. Note:: - ``stochastic_program()`` includes the following steps: - #. Build two-stage stochastic programming optimization model where scenarios correspond to finite difference approximations for the Jacobian of the response variables with respect to calibrated model parameters - #. Fix the experiment design decisions and solve a square (i.e., zero degrees of freedom) instance of the two-stage DOE problem. This step is for initialization. - #. Unfix the experiment design decisions and solve the two-stage DOE problem. - -.. autoclass:: pyomo.contrib.doe.measurements.MeasurementVariables - :members: __init__, add_variables - -.. autoclass:: pyomo.contrib.doe.measurements.DesignVariables - :members: __init__, add_variables - -.. autoclass:: pyomo.contrib.doe.scenario.ScenarioGenerator - :special-members: __init__ - -.. autoclass:: pyomo.contrib.doe.result.FisherResults - :members: __init__, result_analysis - -.. autoclass:: pyomo.contrib.doe.result.GridSearchResult - :special-members: __init__ + :members: __init__, create_doe_model, compute_FIM, run_doe, compute_FIM_full_factorial Pyomo.DoE Usage Example @@ -211,7 +190,7 @@ Step 0: Import Pyomo and the Pyomo.DoE module >>> # === Required import === >>> import pyomo.environ as pyo >>> from pyomo.dae import ContinuousSet, DerivativeVar - >>> from pyomo.contrib.doe import DesignOfExperiments, MeasurementVariables, DesignVariables + >>> from pyomo.contrib.doe import DesignOfExperiments >>> import numpy as np Step 1: Define the Pyomo process model @@ -219,26 +198,10 @@ Step 1: Define the Pyomo process model The process model for the reaction kinetics problem is shown below. -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_kinetics.py - :language: python - :pyobject: create_model - -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_kinetics.py - :language: python - :pyobject: disc_for_measure - -.. note:: - The model requires at least two options: "block" and "global". Both options requires the pass of a created empty Pyomo model. - With "global" option, only design variables and their time sets need to be defined; - With "block" option, a full model needs to be defined. Step 2: Define the inputs for Pyomo.DoE ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_compute_FIM.py - :language: python - :start-at: # Control time set - :end-before: ### Compute Step 3: Compute the FIM of a square MBDoE problem @@ -249,10 +212,6 @@ This method computes an MBDoE optimization problem with no degree of freedom. This method can be accomplished by two modes, ``direct_kaug`` and ``sequential_finite``. ``direct_kaug`` mode requires the installation of the solver `k_aug `_. -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_compute_FIM.py - :language: python - :start-after: ### Compute the FIM - :end-before: # test result Step 4: Exploratory analysis (Enumeration) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -266,14 +225,9 @@ It allows users to define any number of design decisions. Heatmaps can be drawn The function ``run_grid_search`` enumerates over the design space, each MBDoE problem accomplished by ``compute_FIM`` method. Therefore, ``run_grid_search`` supports only two modes: ``sequential_finite`` and ``direct_kaug``. -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_grid_search.py - :language: python - :pyobject: main Successful run of the above code shows the following figure: -.. figure:: grid-1.png - :scale: 35 % A heatmap shows the change of the objective function, a.k.a. the experimental information content, in the design region. Horizontal and vertical axes are two design variables, while the color of each grid shows the experimental information content. Taking the Fig. Reactor case - A optimality as example, A-optimality shows that the most informative region is around $C_{A0}=5.0$ M, $T=300.0$ K, while the least informative region is around $C_{A0}=1.0$ M, $T=700.0$ K. @@ -284,8 +238,5 @@ Pyomo.DoE accomplishes gradient-based optimization with the ``stochastic_program This function solves twice: It solves the square version of the MBDoE problem first, and then unfixes the design variables as degree of freedoms and solves again. In this way the optimization problem can be well initialized. -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_optimize_doe.py - :language: python - :pyobject: main From a619cf68d6523366c9bfd44f9ad02a091be852f1 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Jul 2024 14:05:38 -0600 Subject: [PATCH 1980/3044] Removing hard dependencies --- .../piecewise/tests/test_nonlinear_to_pwl.py | 47 ++++++++++--------- .../piecewise/transform/nonlinear_to_pwl.py | 28 ++++------- 2 files changed, 34 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index d0f5acc2ff1..6b1e3f9f89f 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -116,27 +116,28 @@ def test_log_constraint_random_grid(self): self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) - def test_log_constraint_lmt_uniform_sample(self): - m = self.make_model() + # def test_log_constraint_lmt_uniform_sample(self): + # m = self.make_model() - n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') - n_to_pwl.apply_to( - m, - num_points=3, - domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_UNIFORM, - ) - - # cons is transformed - self.assertFalse(m.cons.active) - - pwlf = list(m.component_data_objects(PiecewiseLinearFunction, - descend_into=True)) - self.assertEqual(len(pwlf), 1) - pwlf = pwlf[0] - - set_trace() - - x1 = 4.370861069626263 - x2 = 7.587945476302646 - x3 = 9.556428757689245 - self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) + # n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + # n_to_pwl.apply_to( + # m, + # num_points=3, + # domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_UNIFORM, + # ) + + # # cons is transformed + # self.assertFalse(m.cons.active) + + # pwlf = list(m.component_data_objects(PiecewiseLinearFunction, + # descend_into=True)) + # self.assertEqual(len(pwlf), 1) + # pwlf = pwlf[0] + + # set_trace() + + # # TODO + # x1 = 4.370861069626263 + # x2 = 7.587945476302646 + # x3 = 9.556428757689245 + # self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index d819e893d5f..408efdb4b32 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -12,10 +12,7 @@ import enum import itertools -from lineartree import LinearTreeRegressor -import lineartree import logging -import numpy as np from pyomo.environ import ( TransformationFactory, @@ -41,6 +38,8 @@ from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import ComponentMap, ComponentSet from pyomo.common.config import ConfigDict, ConfigValue, PositiveInt, InEnum +from pyomo.common.dependencies import attempt_import +from pyomo.common.dependencies import numpy as np from pyomo.common.modeling import unique_component_name from pyomo.core.expr.numeric_expr import SumExpression from pyomo.core.expr import identify_variables @@ -54,13 +53,12 @@ from pyomo.network import Port from pyomo.repn.quadratic import QuadraticRepnVisitor -from sklearn.linear_model import LinearRegression -import random -from sklearn.metrics import mean_squared_error -from sklearn.model_selection import train_test_split +lineartree, lineartree_available = attempt_import('lineartree') +sklearn_lm, sklearn_available = attempt_import('sklearn.linear_model') logger = logging.getLogger(__name__) + class DomainPartitioningMethod(enum.IntEnum): RANDOM_GRID = 1 UNIFORM_GRID = 2 @@ -117,22 +115,15 @@ def get_points_lmt(points, bounds, func, seed): y_list = [] for point in points: y_list.append(func(*point)) - regr = LinearTreeRegressor( - LinearRegression(), + # ESJ: Do we really need the sklearn dependency to get LinearRegression?? + regr = lineartree.LinearTreeRegressor( + sklearn_lm.LinearRegression(), criterion='mse', max_bins=120, min_samples_leaf=4, max_depth=5, ) - - # Using train_test_split is silly. TODO: remove this and just sample my own - # extra points if I want to estimate the error. - X_train, X_test, y_train, y_test = train_test_split( - x_list, y_list, test_size=0.2, random_state=seed - ) - regr.fit(X_train, y_train) - y_pred = regr.predict(X_test) - error = mean_squared_error(y_test, y_pred) + regr.fit(x_list, y_list) leaves, splits, ths = parse_linear_tree_regressor(regr, bounds) @@ -572,6 +563,7 @@ def _get_bounds_list(self, var_list, parent_component): bounds = [] for v in var_list: if None in v.bounds: + # ESJ TODO: Con is undefined--this is a bug! raise ValueError( "Cannot automatically approximate constraints with unbounded " "variables. Var '%s' appearining in component '%s' is missing " From 76e06c55158ce3842b47ac3e8bc2075d58bab1ab Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 18 Jul 2024 15:26:13 -0600 Subject: [PATCH 1981/3044] Fixed determinant obj without cholesky, added tests. --- pyomo/contrib/doe/doe.py | 19 +++-- pyomo/contrib/doe/tests/test_doe_solve.py | 93 +++++++++++++++-------- 2 files changed, 75 insertions(+), 37 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 9f31fdeb010..4f147afef31 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -43,6 +43,7 @@ import logging import json +import math class CalculationMode(Enum): @@ -290,10 +291,9 @@ def run_doe(self, model=None, results_file=None): model.objective.activate() model.obj_cons.activate() - # ToDo: add a ``get FIM from model`` function # If the model has L, initialize it with the solved FIM if hasattr(model, "L"): - # Get the FIM values --> ToDo: add this as a function + # Get the FIM values fim_vals = [ pyo.value(model.fim[i, j]) for i in model.parameter_names @@ -308,6 +308,9 @@ def run_doe(self, model=None, results_file=None): for j, d in enumerate(model.parameter_names): model.L[c, d].value = L_vals_sq[i, j] + if hasattr(model, "det"): + model.det.value = np.linalg.det(np.array(self.get_FIM())) + # Solve the full model, which has now been initialized with the square solve res = self.solver.solve(model, tee=self.tee) @@ -1211,13 +1214,15 @@ def det_general(m): for y, element in enumerate(model.parameter_names): if x_order[x] == y: name_order.append(element) - # det(A) = sum_{\sigma \in \S_n} (sgn(\sigma) * \Prod_{i=1}^n a_{i,\sigma_i}) det_perm = sum( self._sgn(list_p[d]) - * sum( - model.fim[each, name_order[b]] - for b, each in enumerate(model.parameter_names) + * math.prod( + model.fim[ + model.parameter_names.at(val + 1), + model.parameter_names.at(ind + 1), + ] + for ind, val in enumerate(list_p[d]) ) for d in range(len(list_p)) ) @@ -1239,7 +1244,7 @@ def det_general(m): ) model.obj_cons.det_rule = pyo.Constraint(rule=det_general) model.objective = pyo.Objective( - expr=pyo.log10(model.det), sense=pyo.maximize + expr=pyo.log10(model.det + 1e-6), sense=pyo.maximize ) elif self.objective_option == ObjectiveLib.trace: diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 4ab9e275c7e..c27e3946064 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -22,6 +22,7 @@ import logging ipopt_available = SolverFactory("ipopt").available() +k_aug_available = SolverFactory('k_aug', solver_io='nl', validate=False) DATA_DIR = Path(__file__).parent file_path = DATA_DIR / "result.json" @@ -203,36 +204,36 @@ def test_reactor_fd_backward_solve(self): # TODO: Fix determinant objective code, something is awry # Should only be using Cholesky=True - # @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - # @unittest.skipIf(not numpy_available, "Numpy is not available") - # def test_reactor_obj_det_solve(self): - # fd_method = "central" - # obj_used = "det" - - # experiment = FullReactorExperiment(data_ex, 10, 3) - - # doe_obj = DesignOfExperiments( - # experiment, - # fd_formula=fd_method, - # step=1e-3, - # objective_option=obj_used, - # scale_constant_value=1, - # scale_nominal_param_value=True, - # prior_FIM=None, - # jac_initial=None, - # fim_initial=None, - # L_initial=None, - # L_LB=1e-7, - # solver=None, - # tee=False, - # args=None, - # _Cholesky_option=False, - # _only_compute_fim_lower=False, - # ) - - # doe_obj.run_doe() - - # assert (doe_obj.results['Solver Status'] == "ok") + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_reactor_obj_det_solve(self): + fd_method = "central" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=False, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=False, + _only_compute_fim_lower=False, + ) + + doe_obj.run_doe() + + assert doe_obj.results['Solver Status'] == "ok" @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") @@ -332,6 +333,38 @@ def test_compute_FIM_seq_forward(self): doe_obj.compute_FIM(method="sequential") + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf( + not k_aug_available.available(False), "The 'k_aug' command is not available" + ) + @unittest.skipIf(not numpy_available, "Numpy is not available") + def test_compute_FIM_kaug(self): + fd_method = "forward" + obj_used = "det" + + experiment = FullReactorExperiment(data_ex, 10, 3) + + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_method, + step=1e-3, + objective_option=obj_used, + scale_constant_value=1, + scale_nominal_param_value=True, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.compute_FIM(method="kaug") + @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") def test_compute_FIM_seq_backward(self): From a94b7a4fa86f41358746a0ef2cc390cbe1754bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Mon, 22 Jul 2024 22:12:56 +0200 Subject: [PATCH 1982/3044] Removed unnecessary pass statement. --- pyomo/core/base/set.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 8a6e4767f6a..27dd3b7c51c 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1413,7 +1413,6 @@ def add(self, *values): # differentiate between indexed and non-indexed sets if self._index is not None: # indexed set: the value and the index are given - pass if type(_value) == tuple: # _value is a tuple: unpack it for the method arguments' tuple flag = self._validate(_block, (*_value, self._index)) From 930ac20fed8ba5b44131d8f73685b353aa0baf3b Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 23 Jul 2024 11:50:31 -0400 Subject: [PATCH 1983/3044] Added scipy availability flag for kaug test --- pyomo/contrib/doe/tests/test_doe_solve.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index c27e3946064..f4b5729d9ac 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -3,6 +3,7 @@ numpy_available, pandas as pd, pandas_available, + scipy_available ) from pyomo.contrib.doe.tests.experiment_class_example import * @@ -334,6 +335,7 @@ def test_compute_FIM_seq_forward(self): doe_obj.compute_FIM(method="sequential") @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") + @unittest.skipIf(not scipy_available, "Scipy is not available") @unittest.skipIf( not k_aug_available.available(False), "The 'k_aug' command is not available" ) From 33121af6b98d397c4e3428a1aad6e921baa9f2ae Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 23 Jul 2024 12:12:35 -0400 Subject: [PATCH 1984/3044] Fixed typo --- pyomo/contrib/doe/tests/test_doe_solve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index f4b5729d9ac..92787ee8abb 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -3,7 +3,7 @@ numpy_available, pandas as pd, pandas_available, - scipy_available + scipy_available, ) from pyomo.contrib.doe.tests.experiment_class_example import * From 74ef05b0f38ece19b27b6acb18c1a2374e7fe7d8 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 23 Jul 2024 11:14:23 -0600 Subject: [PATCH 1985/3044] Change BARON download URL --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 8ba04eec466..03894a1cb20 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -519,7 +519,7 @@ jobs: $BARON_DIR = "${env:TPL_DIR}/baron" echo "$BARON_DIR" | ` Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - $URL = "https://www.minlp.com/downloads/xecs/baron/current/" + $URL = "https://minlp.com/downloads/xecs/baron/current/" if ( "${{matrix.TARGET}}" -eq "win" ) { $INSTALLER = "${env:DOWNLOAD_DIR}/baron_install.exe" $URL += "baron-win64.exe" diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 0bfd12b998d..cc9760cbe5d 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -562,7 +562,7 @@ jobs: $BARON_DIR = "${env:TPL_DIR}/baron" echo "$BARON_DIR" | ` Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - $URL = "https://www.minlp.com/downloads/xecs/baron/current/" + $URL = "https://minlp.com/downloads/xecs/baron/current/" if ( "${{matrix.TARGET}}" -eq "win" ) { $INSTALLER = "${env:DOWNLOAD_DIR}/baron_install.exe" $URL += "baron-win64.exe" From 9b0d4161e043da834ea4c94bad6bc7cb3ada5980 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 23 Jul 2024 13:32:19 -0400 Subject: [PATCH 1986/3044] Started updating documentation (doe.rst). --- .../contributed_packages/doe/doe.rst | 30 ++++--------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index ce48da3e516..7ddddd64f13 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -116,32 +116,14 @@ In order to solve problems of the above, Pyomo.DoE implements the 2-stage stocha Pyomo.DoE Required Inputs -------------------------------- -The required inputs to the Pyomo.DoE solver are the following: +The required inputs to the Pyomo.DoE solver is an Experiment object. The experiment object must have a get_labeled_model function which returns a pyomo model with four special labeled components as suffixes on the pyomo model. This is in line with the convention used in the new interface for the ParmEst contribute packed to Pyomo. The four suffix components are: -* A function that creates the process model -* Dictionary of parameters and their nominal value -* A measurement object -* A design variables object -* A Numpy ``array`` containing the Prior FIM -* Optimization solver +* experiment_inputs - The experimental design decisions +* experiment_outputs - The values measured during the experiment +* measurement_error - The error associated with individual values measured during the experiment +* unknown_parameters - Those parameters in the model that are estimated using the measured values during the experiment -Below is a list of arguments that Pyomo.DoE expects the user to provide. - -parameter_dict : ``dictionary`` - A ``dictionary`` of parameter names and values. If they are an indexed variable, put the variable name and index in a nested ``Dictionary``. - -design_variables: ``DesignVariables`` - A ``DesignVariables`` of design variables, provided by the DesignVariables class. - If this design var is independent of time (constant), set the time to [0] - -measurement_variables : ``MeasurementVariables`` - A ``MeasurementVariables`` of the measurements, provided by the MeasurementVariables class. - -create_model : ``function`` - A ``function`` returning a deterministic process model. - -prior_FIM : ``array`` - An ``array`` defining the Fisher information matrix (FIM) for prior experiments, default is a zero matrix. +An example an Experiment object that builds and labels the model is shown below: Pyomo.DoE Solver Interface --------------------------- From 08884e31455990035e677f996eba22aa9b8cee8d Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 23 Jul 2024 13:51:07 -0400 Subject: [PATCH 1987/3044] Updated ParmEst reference --- doc/OnlineDocs/contributed_packages/doe/doe.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index 7ddddd64f13..3ccd637d948 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -116,7 +116,7 @@ In order to solve problems of the above, Pyomo.DoE implements the 2-stage stocha Pyomo.DoE Required Inputs -------------------------------- -The required inputs to the Pyomo.DoE solver is an Experiment object. The experiment object must have a get_labeled_model function which returns a pyomo model with four special labeled components as suffixes on the pyomo model. This is in line with the convention used in the new interface for the ParmEst contribute packed to Pyomo. The four suffix components are: +The required inputs to the Pyomo.DoE solver is an Experiment object. The experiment object must have a get_labeled_model function which returns a pyomo model with four special labeled components as suffixes on the pyomo model. This is in line with the convention used in the new interface in the contributed pacakge, `Parmest `_. The four suffix components are: * experiment_inputs - The experimental design decisions * experiment_outputs - The values measured during the experiment From 29d8d437b97d17285c123a8c4436a53d0658b49b Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 23 Jul 2024 14:38:21 -0400 Subject: [PATCH 1988/3044] Fixed typo in doe.rst --- doc/OnlineDocs/contributed_packages/doe/doe.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index 3ccd637d948..1a5c334394b 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -116,7 +116,7 @@ In order to solve problems of the above, Pyomo.DoE implements the 2-stage stocha Pyomo.DoE Required Inputs -------------------------------- -The required inputs to the Pyomo.DoE solver is an Experiment object. The experiment object must have a get_labeled_model function which returns a pyomo model with four special labeled components as suffixes on the pyomo model. This is in line with the convention used in the new interface in the contributed pacakge, `Parmest `_. The four suffix components are: +The required inputs to the Pyomo.DoE solver is an Experiment object. The experiment object must have a get_labeled_model function which returns a pyomo model with four special labeled components as suffixes on the pyomo model. This is in line with the convention used in the new interface in the contributed package, `Parmest `_. The four suffix components are: * experiment_inputs - The experimental design decisions * experiment_outputs - The values measured during the experiment From 5ccd890d7b449bbe79fe7bf7704a2f90357ea4c5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 23 Jul 2024 15:53:01 -0600 Subject: [PATCH 1989/3044] Generalizing log tests, adding chaos test that doesn't do anything yet --- pyomo/contrib/piecewise/__init__.py | 4 +- .../piecewise/tests/test_nonlinear_to_pwl.py | 108 +++++++++++++----- .../piecewise/transform/nonlinear_to_pwl.py | 95 ++++++++------- 3 files changed, 137 insertions(+), 70 deletions(-) diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 67596a709e3..5fc75dcd091 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -33,9 +33,7 @@ from pyomo.contrib.piecewise.transform.convex_combination import ( ConvexCombinationTransformation, ) -from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( - NonlinearToPWL, -) +from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import NonlinearToPWL from pyomo.contrib.piecewise.transform.nested_inner_repn import ( NestedInnerRepresentationGDPTransformation, ) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 6b1e3f9f89f..3a1b5270aea 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -13,22 +13,15 @@ from pyomo.contrib.piecewise import PiecewiseLinearFunction from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( NonlinearToPWL, - DomainPartitioningMethod -) -from pyomo.core.expr.compare import ( - assertExpressionsStructurallyEqual, -) -from pyomo.environ import ( - ConcreteModel, - Var, - Constraint, - TransformationFactory, - log, + DomainPartitioningMethod, ) +from pyomo.core.expr.compare import assertExpressionsStructurallyEqual +from pyomo.environ import ConcreteModel, Var, Constraint, TransformationFactory, log ## debug from pytest import set_trace + class TestNonlinearToPWL_1D(unittest.TestCase): def make_model(self): m = ConcreteModel() @@ -48,16 +41,16 @@ def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): assertExpressionsStructurallyEqual( self, pwlf._linear_functions[0](m.x), - ((log(x2) - log(x1))/(x2 - x1))*m.x + - (log(x2) - ((log(x2) - log(x1))/(x2 - x1))*x2), - places=7 + ((log(x2) - log(x1)) / (x2 - x1)) * m.x + + (log(x2) - ((log(x2) - log(x1)) / (x2 - x1)) * x2), + places=7, ) assertExpressionsStructurallyEqual( self, pwlf._linear_functions[1](m.x), - ((log(x3) - log(x2))/(x3 - x2))*m.x + - (log(x3) - ((log(x3) - log(x2))/(x3 - x2))*x3), - places=7 + ((log(x3) - log(x2)) / (x3 - x2)) * m.x + + (log(x3) - ((log(x3) - log(x2)) / (x3 - x2)) * x3), + places=7, ) self.assertEqual(len(pwlf._expressions), 1) @@ -67,7 +60,7 @@ def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): self.assertIsNone(new_cons.ub) self.assertEqual(new_cons.lb, 0.35) self.assertIs(n_to_pwl.get_src_component(new_cons), m.cons) - + def test_log_constraint_uniform_grid(self): m = self.make_model() @@ -81,18 +74,19 @@ def test_log_constraint_uniform_grid(self): # cons is transformed self.assertFalse(m.cons.active) - pwlf = list(m.component_data_objects(PiecewiseLinearFunction, - descend_into=True)) + pwlf = list( + m.component_data_objects(PiecewiseLinearFunction, descend_into=True) + ) self.assertEqual(len(pwlf), 1) pwlf = pwlf[0] - + points = [(1.0009,), (5.5,), (9.9991,)] (x1, x2, x3) = 1.0009, 5.5, 9.9991 self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) def test_log_constraint_random_grid(self): m = self.make_model() - + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') # [ESJ 3/30/24]: The seed is actually set in the function for getting # the points right now, so this will be deterministic. @@ -105,8 +99,9 @@ def test_log_constraint_random_grid(self): # cons is transformed self.assertFalse(m.cons.active) - pwlf = list(m.component_data_objects(PiecewiseLinearFunction, - descend_into=True)) + pwlf = list( + m.component_data_objects(PiecewiseLinearFunction, descend_into=True) + ) self.assertEqual(len(pwlf), 1) pwlf = pwlf[0] @@ -114,11 +109,10 @@ def test_log_constraint_random_grid(self): x2 = 7.587945476302646 x3 = 9.556428757689245 self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) - # def test_log_constraint_lmt_uniform_sample(self): # m = self.make_model() - + # n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') # n_to_pwl.apply_to( # m, @@ -141,3 +135,65 @@ def test_log_constraint_random_grid(self): # x2 = 7.587945476302646 # x3 = 9.556428757689245 # self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) + + +class TestNonlinearToPWLIntegration(unittest.TestCase): + def test_Ali_example(self): + m = ConcreteModel() + m.flow_super_heated_vapor = Var() + m.flow_super_heated_vapor.fix(0.4586949988166174) + m.super_heated_vapor_temperature = Var(bounds=(31, 200), initialize=45) + m.evaporator_condensate_temperature = Var( + bounds=(29, 120.8291392028045), initialize=30 + ) + m.LMTD = Var(bounds=(0, 130.61608989795093), initialize=1) + m.evaporator_condensate_enthalpy = Var( + bounds=(-15836.847, -15510.210751855624), initialize=100 + ) + m.evaporator_condensate_vapor_enthalpy = Var( + bounds=(-13416.64, -13247.674383866839), initialize=100 + ) + m.heat_transfer_coef = Var( + bounds=(1.9936854577372858, 5.995319594088982), initialize=0.1 + ) + m.evaporator_brine_temperature = Var( + bounds=(27, 118.82913920280366), initialize=35 + ) + m.each_evaporator_area = Var() + + m.c = Constraint( + expr=m.each_evaporator_area + == ( + 1.873 + * m.flow_super_heated_vapor + * ( + m.super_heated_vapor_temperature + - m.evaporator_condensate_temperature + ) + / (100 * m.LMTD) + + m.flow_super_heated_vapor + * ( + m.evaporator_condensate_vapor_enthalpy + - m.evaporator_condensate_enthalpy + ) + / ( + m.heat_transfer_coef + * ( + m.evaporator_condensate_temperature + - m.evaporator_brine_temperature + ) + ) + ) + ) + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, + num_points=3, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + ) + + m.pprint() + + from pyomo.environ import SolverFactory + SolverFactory('gurobi').solve(m, tee=True) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 408efdb4b32..798eaafdddf 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -33,7 +33,7 @@ Block, ExternalFunction, SortComponents, - LogicalConstraint + LogicalConstraint, ) from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import ComponentMap, ComponentSet @@ -45,10 +45,7 @@ from pyomo.core.expr import identify_variables from pyomo.core.expr import SumExpression from pyomo.core.util import target_list -from pyomo.contrib.piecewise import ( - PiecewiseLinearExpression, - PiecewiseLinearFunction -) +from pyomo.contrib.piecewise import PiecewiseLinearExpression, PiecewiseLinearFunction from pyomo.gdp import Disjunct, Disjunction from pyomo.network import Port from pyomo.repn.quadratic import QuadraticRepnVisitor @@ -65,23 +62,28 @@ class DomainPartitioningMethod(enum.IntEnum): LINEAR_MODEL_TREE_UNIFORM = 3 LINEAR_MODEL_TREE_RANDOM = 4 + # This should be safe to use many times; declare it globally _quadratic_repn_visitor = QuadraticRepnVisitor( subexpression_cache={}, var_map={}, var_order={}, sorter=None ) + class _NonlinearToPWLTransformationData(AutoSlots.Mixin): __slots__ = ('transformed_component', 'src_component') def __init__(self): self.transformed_component = ComponentMap() self.src_component = ComponentMap() + + Block.register_private_data_initializer(_NonlinearToPWLTransformationData) + def get_random_point_grid(bounds, n, func, seed=42): # Generate randomized grid of points linspaces = [] - for (lb, ub) in bounds: + for lb, ub in bounds: np.random.seed(seed) linspaces.append(np.random.uniform(lb, ub, n)) return list(itertools.product(*linspaces)) @@ -90,7 +92,7 @@ def get_random_point_grid(bounds, n, func, seed=42): def get_uniform_point_grid(bounds, n, func): # Generate non-randomized grid of points linspaces = [] - for (lb, ub) in bounds: + for lb, ub in bounds: # Issues happen when exactly using the boundary nudge = (ub - lb) * 1e-4 linspaces.append( @@ -137,6 +139,7 @@ def get_points_lmt(points, bounds, func, seed): # here? return bound_point_list + _partition_method_dispatcher = { DomainPartitioningMethod.RANDOM_GRID: get_random_point_grid, DomainPartitioningMethod.UNIFORM_GRID: get_uniform_point_grid, @@ -144,6 +147,7 @@ def get_points_lmt(points, bounds, func, seed): DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM: get_points_lmt_random_sample, } + def get_pwl_function_approximation(func, method, n, bounds): """ Get a piecewise-linear approximation of a function, given: @@ -163,8 +167,10 @@ def get_pwl_function_approximation(func, method, n, bounds): # After getting the points, construct PWLF using the # function-and-list-of-points constructor - logger.debug(f"Constructing PWLF with {len(points)} points, each of which " - f"are {dim}-dimensional") + logger.debug( + f"Constructing PWLF with {len(points)} points, each of which " + f"are {dim}-dimensional" + ) return PiecewiseLinearFunction(points=points, function=func) @@ -183,7 +189,7 @@ def generate_bound_points(leaves, bounds): for var_bound in leaf['bounds'].values(): lower_corner_list.append(var_bound[0]) upper_corner_list.append(var_bound[1]) - + # Duct tape to fix issues from unknown bugs for pt in [lower_corner_list, upper_corner_list]: for i in range(len(pt)): @@ -228,12 +234,12 @@ def parse_linear_tree_regressor(linear_tree_regressor, bounds): if left_child_node in leaves: # if left child is a leaf node node['left_leaves'].append(left_child_node) else: # traverse its left node by calling function to find all the - # leaves from its left node + # leaves from its left node node['left_leaves'] = find_leaves(splits, leaves, splits[left_child_node]) if right_child_node in leaves: # if right child is a leaf node node['right_leaves'].append(right_child_node) else: # traverse its right node by calling function to find all the - # leaves from its right node + # leaves from its right node node['right_leaves'] = find_leaves(splits, leaves, splits[right_child_node]) # For each feature in each leaf, initialize lower and upper bounds to None @@ -319,6 +325,7 @@ class NonlinearToPWL(Transformation): """ Convert nonlinear constraints and objectives to piecewise-linear approximations. """ + CONFIG = ConfigDict('contrib.piecewise.nonlinear_to_pwl') CONFIG.declare( 'targets', @@ -422,6 +429,7 @@ class NonlinearToPWL(Transformation): points in order to partition the function domain.""", ), ) + def __init__(self): super(Transformation).__init__() self._handlers = { @@ -454,7 +462,7 @@ def _apply_to(self, instance, **kwds): self._transformation_blocks.clear() self._transformation_block_set.clear() - def _apply_to_impl( self, model, **kwds): + def _apply_to_impl(self, model, **kwds): config = self.CONFIG(kwds.pop('options', {})) config.set_value(kwds) @@ -480,23 +488,19 @@ def _get_transformation_block(self, parent): if parent in self._transformation_blocks: return self._transformation_blocks[parent] - nm = unique_component_name( - parent, '_pyomo_contrib_nonlinear_to_pwl' - ) + nm = unique_component_name(parent, '_pyomo_contrib_nonlinear_to_pwl') self._transformation_blocks[parent] = transBlock = Block() parent.add_component(nm, transBlock) self._transformation_block_set.add(transBlock) transBlock._pwl_cons = Constraint(Any) return transBlock - + def _transform_block_components(self, block, config): blocks = block.values() if block.is_indexed() else (block,) for b in blocks: for obj in b.component_objects( - active=True, - descend_into=False, - sort=SortComponents.deterministic + active=True, descend_into=False, sort=SortComponents.deterministic ): if obj in self._transformation_block_set: # This is a Block we created--we know we don't need to look @@ -519,8 +523,8 @@ def _transform_constraint(self, cons, config): constraints = cons.values() if cons.is_indexed() else (cons,) for c in constraints: pw_approx = self._approximate_expression( - c.body, c, trans_block, config, - config.approximate_quadratic_constraints) + c.body, c, trans_block, config, config.approximate_quadratic_constraints + ) if pw_approx is None: # Didn't need approximated, nothing to do @@ -531,7 +535,7 @@ def _transform_constraint(self, cons, config): new_cons = trans_block._pwl_cons[c.name, idx] trans_data_dict.src_component[new_cons] = c src_data_dict.transformed_component[c] = new_cons - + # deactivate original c.deactivate() @@ -542,8 +546,12 @@ def _transform_objective(self, objective, config): src_data_dict = objective.parent_block().private_data() for obj in objectives: pw_approx = self._approximate_expression( - obj.expr, obj, trans_block, config, - config.approximate_quadratic_objectives) + obj.expr, + obj, + trans_block, + config, + config.approximate_quadratic_objectives, + ) if pw_approx is None: # Didn't need approximated, nothing to do @@ -551,12 +559,11 @@ def _transform_objective(self, objective, config): new_obj = Objective(expr=pw_approx, sense=obj.sense) trans_block.add_component( - unique_component_name(trans_block, obj.name), - new_obj + unique_component_name(trans_block, obj.name), new_obj ) trans_data_dict.src_component[new_obj] = obj src_data_dict.transformed_component[obj] = new_obj - + obj.deactivate() def _get_bounds_list(self, var_list, parent_component): @@ -567,7 +574,8 @@ def _get_bounds_list(self, var_list, parent_component): raise ValueError( "Cannot automatically approximate constraints with unbounded " "variables. Var '%s' appearining in component '%s' is missing " - "at least one bound" % (con.name, v.name)) + "at least one bound" % (con.name, v.name) + ) else: bounds.append(v.bounds) return bounds @@ -584,15 +592,17 @@ def _needs_approximating(self, expr, approximate_quadratic): return False return True - def _approximate_expression(self, obj, parent_component, trans_block, - config, approximate_quadratic): + def _approximate_expression( + self, obj, parent_component, trans_block, config, approximate_quadratic + ): if not self._needs_approximating(obj, approximate_quadratic): return - + # Additively decompose obj and work on the pieces pwl_func = 0 - for k, expr in enumerate(_additively_decompose_expr(obj) if - config.additively_decompose else (obj,)): + for k, expr in enumerate( + _additively_decompose_expr(obj) if config.additively_decompose else (obj,) + ): # First check is this is a good idea expr_vars = list(identify_variables(expr, include_fixed=False)) orig_values = ComponentMap((v, v.value) for v in expr_vars) @@ -603,7 +613,8 @@ def _approximate_expression(self, obj, parent_component, trans_block, "Not approximating expression for component '%s' as " "it exceeds the maximum dimension of %s. Try increasing " "'max_dimension' or additively separating the expression." - % (parent_component.name, config.max_dimension)) + % (parent_component.name, config.max_dimension) + ) pwl_func += expr continue elif not self._needs_approximating(expr, approximate_quadratic): @@ -616,13 +627,13 @@ def eval_expr(*args): return value(expr) pwlf = get_pwl_function_approximation( - eval_expr, config.domain_partitioning_method, + eval_expr, + config.domain_partitioning_method, config.num_points, - self._get_bounds_list(expr_vars, parent_component) + self._get_bounds_list(expr_vars, parent_component), ) name = unique_component_name( - trans_block, - parent_component.getname(fully_qualified=False) + trans_block, parent_component.getname(fully_qualified=False) ) trans_block.add_component(f"_pwle_{name}_{k}", pwlf) pwl_func += pwlf(*expr_vars) @@ -640,7 +651,8 @@ def get_src_component(self, cons): else: raise ValueError( "It does not appear that '%s' is a transformed Constraint " - "created by the 'nonlinear_to_pwl' transformation." % cons.name) + "created by the 'nonlinear_to_pwl' transformation." % cons.name + ) def get_transformed_component(self, cons): data = cons.parent_block().private_data().transformed_component @@ -649,4 +661,5 @@ def get_transformed_component(self, cons): else: raise ValueError( "It does not appear that '%s' is a Constraint that was " - "transformed by the 'nonlinear_to_pwl' transformation." % cons.name) + "transformed by the 'nonlinear_to_pwl' transformation." % cons.name + ) From 89be970cad522d9a35911f5b38c90ef0198f9cdb Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 23 Jul 2024 16:07:07 -0600 Subject: [PATCH 1990/3044] Checking bool inside of native types --- .../cp/transform/logical_to_disjunctive_walker.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index 47a85076ce1..4bce9c7d6af 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -243,14 +243,13 @@ def initializeWalker(self, expr): return True, expr def beforeChild(self, node, child, child_idx): - if child.__class__ is bool: - # If we encounter a bool, we are going to need to treat it as - # binary explicitly because we are finally pedantic enough in the - # expression system to not allow some of the mixing we will need - # (like summing a LinearExpression with a bool) - return False, int(child) - if child.__class__ in EXPR.native_types: + if child.__class__ is bool: + # If we encounter a bool, we are going to need to treat it as + # binary explicitly because we are finally pedantic enough in the + # expression system to not allow some of the mixing we will need + # (like summing a LinearExpression with a bool) + return False, int(child) return False, child if child.is_numeric_type(): From 507d828acc24f5b3b8b5edfb466f112655d4c227 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 23 Jul 2024 16:11:00 -0600 Subject: [PATCH 1991/3044] Removing the integrality check for Param because it isn't really future-proof and it is redundant with the same check in the expression nodes where it actually is required --- .../cp/transform/logical_to_disjunctive_walker.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py index 4bce9c7d6af..95cbaf57fa5 100644 --- a/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py +++ b/pyomo/contrib/cp/transform/logical_to_disjunctive_walker.py @@ -51,14 +51,7 @@ def _dispatch_var(visitor, node): def _dispatch_param(visitor, node): - if int(value(node)) == value(node): - return False, node - else: - raise ValueError( - "Found non-integer valued Param '%s' in a logical " - "expression. This cannot be written to a disjunctive " - "form." % node.name - ) + return False, node def _dispatch_expression(visitor, node): From f7b5f57649b7c398ddb1cba5d18b2ef6faa30a6f Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 23 Jul 2024 18:20:37 -0400 Subject: [PATCH 1992/3044] Added examples and figure saving capabilities --- pyomo/contrib/doe/doe.py | 75 +++++-- .../examples/reactor_compute_factorial_FIM.py | 88 ++++++++ pyomo/contrib/doe/examples/reactor_example.py | 84 ++++++++ .../doe/examples/reactor_experiment.py | 204 ++++++++++++++++++ 4 files changed, 436 insertions(+), 15 deletions(-) create mode 100644 pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py create mode 100644 pyomo/contrib/doe/examples/reactor_example.py create mode 100644 pyomo/contrib/doe/examples/reactor_experiment.py diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 4f147afef31..359b0c051cc 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1549,9 +1549,9 @@ def draw_factorial_figure( sensitivity_design_variables=None, fixed_design_variables=None, full_design_variable_names=None, - title_text=None, - xlabel_text=None, - ylabel_text=None, + title_text="", + xlabel_text="", + ylabel_text="", figure_file_name=None, font_axes=16, font_tick=14, @@ -1668,10 +1668,6 @@ def draw_factorial_figure( self.figure_sens_des_vars = sensitivity_design_variables self.figure_fixed_des_vars = fixed_design_variables - # ToDo: Add figure saving capabilities - if figure_file_name is not None: - self.logger.warning("File saving for drawing is not yet implemented.") - # if one design variable name is given as DOF, draw 1D sensitivity curve if len(self.figure_sens_des_vars) == 1: self._curve1D( @@ -1722,6 +1718,10 @@ def _curve1D( -------- 4 Figures of 1D sensitivity curves for each criteria """ + if figure_file_name is not None: + show_fig = False + else: + show_fig = True # extract the range of the DOF design variable x_range = self.figure_result_data[self.figure_sens_des_vars[0]].values.tolist() @@ -1754,7 +1754,12 @@ def _curve1D( ax.set_ylabel("$log_{10}$ Trace") ax.set_xlabel(xlabel_text) plt.pyplot.title(title_text + ": A-optimality") - plt.pyplot.show() + if show_fig: + plt.pyplot.show() + else: + plt.pyplot.savefig( + Path(figure_file_name + "_A_opt.png"), format="png", dpi=450 + ) # Draw D-optimality fig = plt.pyplot.figure() @@ -1770,7 +1775,12 @@ def _curve1D( ax.set_ylabel("$log_{10}$ Determinant") ax.set_xlabel(xlabel_text) plt.pyplot.title(title_text + ": D-optimality") - plt.pyplot.show() + if show_fig: + plt.pyplot.show() + else: + plt.pyplot.savefig( + Path(figure_file_name + "_D_opt.png"), format="png", dpi=450 + ) # Draw E-optimality fig = plt.pyplot.figure() @@ -1786,7 +1796,12 @@ def _curve1D( ax.set_ylabel("$log_{10}$ Minimal eigenvalue") ax.set_xlabel(xlabel_text) plt.pyplot.title(title_text + ": E-optimality") - plt.pyplot.show() + if show_fig: + plt.pyplot.show() + else: + plt.pyplot.savefig( + Path(figure_file_name + "_E_opt.png"), format="png", dpi=450 + ) # Draw Modified E-optimality fig = plt.pyplot.figure() @@ -1802,7 +1817,12 @@ def _curve1D( ax.set_ylabel("$log_{10}$ Condition number") ax.set_xlabel(xlabel_text) plt.pyplot.title(title_text + ": Modified E-optimality") - plt.pyplot.show() + if show_fig: + plt.pyplot.show() + else: + plt.pyplot.savefig( + Path(figure_file_name + "_ME_opt.png"), format="png", dpi=450 + ) def _heatmap( self, @@ -1832,6 +1852,11 @@ def _heatmap( -------- 4 Figures of 2D heatmap for each criteria """ + if figure_file_name is not None: + show_fig = False + else: + show_fig = True + des_names = [k for k, v in self.figure_fixed_des_vars.items()] sens_ranges = {} for i in self.figure_sens_des_vars: @@ -1892,7 +1917,12 @@ def _heatmap( ba = plt.pyplot.colorbar(im) ba.set_label("log10(trace(FIM))") plt.pyplot.title(title_text + ": A-optimality") - plt.pyplot.show() + if show_fig: + plt.pyplot.show() + else: + plt.pyplot.savefig( + Path(figure_file_name + "_A_opt.png"), format="png", dpi=450 + ) # D-optimality fig = plt.pyplot.figure() @@ -1913,7 +1943,12 @@ def _heatmap( ba = plt.pyplot.colorbar(im) ba.set_label("log10(det(FIM))") plt.pyplot.title(title_text + ": D-optimality") - plt.pyplot.show() + if show_fig: + plt.pyplot.show() + else: + plt.pyplot.savefig( + Path(figure_file_name + "_D_opt.png"), format="png", dpi=450 + ) # E-optimality fig = plt.pyplot.figure() @@ -1934,7 +1969,12 @@ def _heatmap( ba = plt.pyplot.colorbar(im) ba.set_label("log10(minimal eig(FIM))") plt.pyplot.title(title_text + ": E-optimality") - plt.pyplot.show() + if show_fig: + plt.pyplot.show() + else: + plt.pyplot.savefig( + Path(figure_file_name + "_E_opt.png"), format="png", dpi=450 + ) # Modified E-optimality fig = plt.pyplot.figure() @@ -1955,7 +1995,12 @@ def _heatmap( ba = plt.pyplot.colorbar(im) ba.set_label("log10(cond(FIM))") plt.pyplot.title(title_text + ": Modified E-optimality") - plt.pyplot.show() + if show_fig: + plt.pyplot.show() + else: + plt.pyplot.savefig( + Path(figure_file_name + "_ME_opt.png"), format="png", dpi=450 + ) # Gets the FIM from an existing model def get_FIM(self, model=None): diff --git a/pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py b/pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py new file mode 100644 index 00000000000..b482fc319f0 --- /dev/null +++ b/pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py @@ -0,0 +1,88 @@ +from pyomo.common.dependencies import numpy as np + +from pyomo.contrib.doe.examples.reactor_experiment import ReactorExperiment +from pyomo.contrib.doe import DesignOfExperiments + +import pyomo.environ as pyo + +import json +from pathlib import Path + + +# Example to run a DOE on the reactor +def run_reactor_doe(): + # Read in file + DATA_DIR = Path(__file__).parent + file_path = DATA_DIR / "result.json" + + f = open(file_path) + data_ex = json.load(f) + + # Process control data points into correct format for reactor experiment + data_ex["control_points"] = { + float(k): v for k, v in data_ex["control_points"].items() + } + + # Create a ReactorExperiment object; data and discretization information are part + # of the constructor of this object + experiment = ReactorExperiment(data=data_ex, nfe=10, ncp=3) + + # Use a central difference, with step size 1e-3 + fd_formula = "central" + step_size = 1e-3 + + # Use the determinant objective with scaled sensitivity matrix + objective_option = "det" + scale_nominal_param_value = True + + # Create the DesignOfExperiments object + # We will not be passing any prior information in this example + # and allow the experiment object and the DesignOfExperiments + # call of ``run_doe`` perform model initialization. + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_formula, + step=step_size, + objective_option=objective_option, + scale_constant_value=1, + scale_nominal_param_value=scale_nominal_param_value, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + # Make design ranges to compute the full factorial design + design_ranges = {"CA[0]": [1, 5, 9], "T[0]": [300, 700, 9]} + + # Compute the full factorial design with the sequential FIM calculation + doe_obj.compute_FIM_full_factorial(design_ranges=design_ranges, method="sequential") + + # Plot the results + doe_obj.draw_factorial_figure( + sensitivity_design_variables=["CA[0]", "T[0]"], + fixed_design_variables={ + "T[0.125]": 300, + "T[0.25]": 300, + "T[0.375]": 300, + "T[0.5]": 300, + "T[0.625]": 300, + "T[0.75]": 300, + "T[0.875]": 300, + "T[1]": 300, + }, + title_text="Reactor Example", + xlabel_text="Concentration of A (M)", + ylabel_text="Initial Temperature (K)", + figure_file_name="example_reactor_compute_FIM", + ) + + +if __name__ == "__main__": + run_reactor_doe() diff --git a/pyomo/contrib/doe/examples/reactor_example.py b/pyomo/contrib/doe/examples/reactor_example.py new file mode 100644 index 00000000000..f673d4a3750 --- /dev/null +++ b/pyomo/contrib/doe/examples/reactor_example.py @@ -0,0 +1,84 @@ +from pyomo.common.dependencies import numpy as np + +from pyomo.contrib.doe.examples.reactor_experiment import ReactorExperiment +from pyomo.contrib.doe import DesignOfExperiments + +import pyomo.environ as pyo + +import json +from pathlib import Path + + +# Example to run a DOE on the reactor +def run_reactor_doe(): + # Read in file + DATA_DIR = Path(__file__).parent + file_path = DATA_DIR / "result.json" + + f = open(file_path) + data_ex = json.load(f) + + # Process control data points into correct format for reactor experiment + data_ex["control_points"] = { + float(k): v for k, v in data_ex["control_points"].items() + } + + # Create a ReactorExperiment object; data and discretization information are part + # of the constructor of this object + experiment = ReactorExperiment(data=data_ex, nfe=10, ncp=3) + + # Use a central difference, with step size 1e-3 + fd_formula = "central" + step_size = 1e-3 + + # Use the determinant objective with scaled sensitivity matrix + objective_option = "det" + scale_nominal_param_value = True + + # Create the DesignOfExperiments object + # We will not be passing any prior information in this example + # and allow the experiment object and the DesignOfExperiments + # call of ``run_doe`` perform model initialization. + doe_obj = DesignOfExperiments( + experiment, + fd_formula=fd_formula, + step=step_size, + objective_option=objective_option, + scale_constant_value=1, + scale_nominal_param_value=scale_nominal_param_value, + prior_FIM=None, + jac_initial=None, + fim_initial=None, + L_initial=None, + L_LB=1e-7, + solver=None, + tee=False, + args=None, + _Cholesky_option=True, + _only_compute_fim_lower=True, + ) + + doe_obj.run_doe() + + # Print out a results summary + print("Optimal experiment values: ") + print( + "\tInitial concentration: {:.2f}".format( + doe_obj.results["Experiment Design"][0] + ) + ) + print( + ("\tTemperature values: [" + "{:.2f}, " * 8 + "{:.2f}]").format( + *doe_obj.results["Experiment Design"][1:] + ) + ) + print("FIM at optimal design:\n {}".format(np.array(doe_obj.results["FIM"]))) + print( + "Objective value at optimal design: {:.2f}".format( + pyo.value(doe_obj.model.objective) + ) + ) + + +if __name__ == "__main__": + run_reactor_doe() diff --git a/pyomo/contrib/doe/examples/reactor_experiment.py b/pyomo/contrib/doe/examples/reactor_experiment.py new file mode 100644 index 00000000000..912cecefc96 --- /dev/null +++ b/pyomo/contrib/doe/examples/reactor_experiment.py @@ -0,0 +1,204 @@ +# === Required imports === +import pyomo.environ as pyo +from pyomo.dae import ContinuousSet, DerivativeVar, Simulator + +# ======================== + + +class Experiment(object): + def __init__(self): + self.model = None + + def get_labeled_model(self): + raise NotImplementedError( + "Derived experiment class failed to implement get_labeled_model" + ) + + +class ReactorExperiment(object): + def __init__(self, data, nfe, ncp): + self.data = data + self.nfe = nfe + self.ncp = ncp + self.model = None + + def get_labeled_model(self): + if self.model is None: + self.create_model() + self.finalize_model() + self.label_experiment() + return self.model + + def create_model(self): + """ + This is an example user model provided to DoE library. + It is a dynamic problem solved by Pyomo.DAE. + + Return + ------ + m: a Pyomo.DAE model + """ + + m = self.model = pyo.ConcreteModel() + + # Model parameters + m.R = pyo.Param(mutable=False, initialize=8.314) + + # Define model variables + ######################## + # time + m.t = ContinuousSet(bounds=[0, 1]) + + # Concentrations + m.CA = pyo.Var(m.t, within=pyo.NonNegativeReals) + m.CB = pyo.Var(m.t, within=pyo.NonNegativeReals) + m.CC = pyo.Var(m.t, within=pyo.NonNegativeReals) + + # Temperature + m.T = pyo.Var(m.t, within=pyo.NonNegativeReals) + + # Arrhenius rate law equations + m.A1 = pyo.Var(within=pyo.NonNegativeReals) + m.E1 = pyo.Var(within=pyo.NonNegativeReals) + m.A2 = pyo.Var(within=pyo.NonNegativeReals) + m.E2 = pyo.Var(within=pyo.NonNegativeReals) + + # Differential variables (Conc.) + m.dCAdt = DerivativeVar(m.CA, wrt=m.t) + m.dCBdt = DerivativeVar(m.CB, wrt=m.t) + + ######################## + # End variable def. + + # Equation def'n + ######################## + + # Expression for rate constants + @m.Expression(m.t) + def k1(m, t): + return m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t])) + + @m.Expression(m.t) + def k2(m, t): + return m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t])) + + # Concentration odes + @m.Constraint(m.t) + def CA_rxn_ode(m, t): + return m.dCAdt[t] == -m.k1[t] * m.CA[t] + + @m.Constraint(m.t) + def CB_rxn_ode(m, t): + return m.dCBdt[t] == m.k1[t] * m.CA[t] - m.k2[t] * m.CB[t] + + # algebraic balance for concentration of C + # Valid because the reaction system (A --> B --> C) is equimolar + @m.Constraint(m.t) + def CC_balance(m, t): + return m.CA[0] == m.CA[t] + m.CB[t] + m.CC[t] + + ######################## + # End equation def'n + + def finalize_model(self): + """ + Example finalize model function. There are two main tasks + here: + 1. Extracting useful information for the model to align + with the experiment. (Here: CA0, t_final, t_control) + 2. Discretizing the model subject to this information. + + Arguments + --------- + m: Pyomo model + data: object containing vital experimental information + nfe: number of finite elements + ncp: number of collocation points for the finite elements + """ + m = self.model + + # Unpacking data before simulation + control_points = self.data["control_points"] + + m.CA[0].value = self.data["CA0"] + m.CB[0].fix(self.data["CB0"]) + m.t.update(self.data["t_range"]) + m.t.update(control_points) + m.A1.fix(self.data["A1"]) + m.A2.fix(self.data["A2"]) + m.E1.fix(self.data["E1"]) + m.E2.fix(self.data["E2"]) + + m.CA[0].setlb(self.data["CA_bounds"][0]) + m.CA[0].setub(self.data["CA_bounds"][1]) + + m.t_control = control_points + + # Discretizing the model + discr = pyo.TransformationFactory("dae.collocation") + discr.apply_to(m, nfe=self.nfe, ncp=self.ncp, wrt=m.t) + + # Initializing Temperature in the model + cv = None + for t in m.t: + if t in control_points: + cv = control_points[t] + m.T[t].setlb(self.data["T_bounds"][0]) + m.T[t].setub(self.data["T_bounds"][1]) + m.T[t] = cv + + @m.Constraint(m.t - control_points) + def T_control(m, t): + """ + Piecewise constant Temperature between control points + """ + neighbour_t = max(tc for tc in control_points if tc < t) + return m.T[t] == m.T[neighbour_t] + + # sim.initialize_model() + + def label_experiment(self): + """ + Example for annotating (labeling) the model with a + full experiment. + + Arguments + --------- + + """ + m = self.model + + # Grab measurement labels + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + # Add CA to experiment outputs + m.experiment_outputs.update((m.CA[t], None) for t in m.t) + # Add CB to experiment outputs + m.experiment_outputs.update((m.CB[t], None) for t in m.t) + # Add CC to experiment outputs + m.experiment_outputs.update((m.CC[t], None) for t in m.t) + + # Adding no error for measurements currently + m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) + concentration_error = 1e-2 # Error in concentration measurement + # Add measurement error for CA + m.measurement_error.update((m.CA[t], concentration_error) for t in m.t) + # Add measurement error for CB + m.measurement_error.update((m.CB[t], concentration_error) for t in m.t) + # Add measurement error for CC + m.measurement_error.update((m.CC[t], concentration_error) for t in m.t) + + # Grab design variables + m.experiment_inputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + # Add experimental input label for initial concentration + m.experiment_inputs.update( + (m.CA[t], pyo.ComponentUID(m.CA[t])) for t in [m.t.first()] + ) + # Add experimental input label for Temperature + m.experiment_inputs.update( + (m.T[t], pyo.ComponentUID(m.T[t])) for t in m.t_control + ) + + # Add unknown parameter labels + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + # Add labels to all unknown parameters with nominal value as the value + m.unknown_parameters.update((k, pyo.value(k)) for k in [m.A1, m.A2, m.E1, m.E2]) From 3211d4d0e260c38858dea6ddc5130d5cf7627e10 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 23 Jul 2024 18:54:28 -0400 Subject: [PATCH 1993/3044] Fixed documentation, cleaned example files --- .../doe/FIM_sensitivity.png | Bin 0 -> 194054 bytes .../contributed_packages/doe/doe.rst | 81 +++++++++++------- .../doe/examples/reactor_experiment.py | 2 - 3 files changed, 49 insertions(+), 34 deletions(-) create mode 100644 doc/OnlineDocs/contributed_packages/doe/FIM_sensitivity.png diff --git a/doc/OnlineDocs/contributed_packages/doe/FIM_sensitivity.png b/doc/OnlineDocs/contributed_packages/doe/FIM_sensitivity.png new file mode 100644 index 0000000000000000000000000000000000000000..af6b75cbbea900c72a1a2f289bed88f7b21dfa1e GIT binary patch literal 194054 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

8c*Ru>bK9l!!k2 zG|7IS(e`)G1;#NQTM{fmfo}_HYD-B{N4-~85y$>vLAW}!6vW=3#1^r{#-m7!B6LbMbT?=`TH8EVP*USD`*iDaZa1UO~ZQYmj?QivqB|K$$7F|iC!X(ESbwmAQCfr7_ zL5gumIIQJhsUWH7GF}C~y>Oa1w-f2~^jVP&HBtYl1H;->)Ys!IvAf&GQ^8h>-Y`3JIVdR3W{Twz`&0y!^@dB7hC#c8ms~-gpd>dYPw?w~pI=j`r`P5OiIe1s` zI1A%*noi{ zk-Prcv5Vjn!%URInECFQau1>re2e1vN+1)Y@LNoM#XldTot4vV^xCif#fi;mM1SJCwve#El}L3lg7*wVb-Y&U6JSP<8~ z@E3doASSU2tXKHu@pJ&Cbp;&hYtB%<2gU%ZC~6=HSTnX5UU}P%?X^`hSu~S&)0syB z%DW$y1x;yc)|7bpJ^U7>;}Ii%=8kTllUr79WVKiK()$)~g?8*aAkm4W=O4gsz2`_J zNGcY@N4Y2?Wr)`q%VT3uO_O>-xL{T>ZNPI43k%UXlpim{L7Xc4s7z(5J=eJQQ~FQ5 zjEvu#xlPPB`$TXi4d@aS?G|WNeEAOfm#>y%^2Bw zT?VFSo%Rn$FcpJ_h#gJ@QxHLoC4r*9mK4mgC4MyP{B%5FewW z;T^w&GujI#U^I~;8uZGm=>Hc310NzJAGG4u$P|>wjP>l^;aens>s^BLYXC4##Z(<( zWEK?YeYh0(0uRV|-o%>e)0Dja%BxRN{g?~BtJPy8qVj`F5Vpk2)# z?H=cDpfsmgnSwWpj{gbCf*(sJdwWkt%8yb=@YuR2$R+q&pLp&=HPPY^cxyPKPZ~jn zktad(0+3nQBu_WH0@Ph+X2YOiCriThU?i99lrEVM^YYKH8=#To!KB0-8fa%Bq1OOL z%+IgHh1@Z?5fY>H0R=XKsa?Nl=li>vd9u`a0g#A-(pMrEI%NQvWalg{N*Q{uy;Tf* zW#6Xc^JLoYWAZ=t1|PBNXNm#C{kN*o>pDQB&wp6ecoEuF+QZy1W$<**_CTT%2~l;r z+AbvW45@3e`o(I>C&mtBiXw%0A5-}~w~12Y5eF4`+wNRThzIq`z`=?xdX?Ai97o3) zoi{>2y4SPRsI0H{@l*~$GB5>SkPh`>#tr|z90hT>j|)Hy_fc?c)`B-EBP*;t9JvDc9DmkmpzPt zAuYb98xVeG(Eh@7jK#s3(!!24ORp=TFvk1isA-B-13j~L8JTB*k+Vc4Mm+u2Vx%`cv8ii|s9`>*J>Ba?Af8y52gf z%KeM>mfnEU4bmVYDN@oUt)irav?z_lCKTyzX+#AB5s?lF>5vwb?o=9-65(ABcz$<` zcij8u8RvN1dq44AYtFeo>o>&ettzwIg}Sf>v@W9867Hs%(}jAOy|c`9*Zwx?4I&do zpFn-I!Q#6vJ%H_Oe|x|MxQV4>u4|8)UN3EAy<} zRF~cac^S{72YD@CaX77Ia|p1rHk#!Z(rwhCXZ@rUMRH$WT1v(bF(|HX9mlPIEK;l1 zapu?6eGaNpTcX0(wf*hI5KL5WlSo2Lh}eUXf07y;oybq4)b2Uc==6ilFs8=5>FZr` zbCUrhpC8=Y2S=rRXQ6~AoRz-+^z`j`^Ec`#_v9g@+VjHKe*Ne-?n)CL2Px1VXG&#z zhD{aKb?)KS+5A{CPM&6sZ`V_fE@7>zo-3j88ly1Xx%f3eS`3)TqIC7s?H+Lth2cZ&M%&r_G%4PTBRm=~(d zZjGY9H<{Y0X|-aJl|oP%_>(FDx?;8q#P|fN`ZU%#S?oXFJkvPUfC6O&*yEVl0 z{Kx*x*JymhaHpF}yE4c7F}$hKQ5#0p#}QpyoGTb4jGlS@&6gG0RG(<3Ghd9s%%di) zd{;k=?vDGi8t+bWEHEp^cs3e=*RdrVa+(S4-=)zjD0w&}UaEQe5yL6`7=~`mxUs9t zPa!fLgSS&CRCrBVT-XY|>5`sBI-9E#HB}Q;suh%@dYfIJ*K+n2?&>ckAivNHOHwYm z+RDCb^agUJb~smmF`{8pLJX)V3YJxHSVY+*j9pFCDz%)d^UHty_h1boP^#bZzODuy z648rvn&*12))0<-p2IK#P`5ER8Oc(Z*OYe>l2(w$JyI>(1zS|ez~$Qu=hEMv^CSN zyd$_mSk{wcl1|B&GGwjNGVt#9r4>R4Q+Lunkbv*kcNsitp(iS|gBp4b!5xPN$r%<| zhS5u+3!Vj-pNAko5-%Y&pR0N@-#L-e9)=qiEd$%TL`FMjKD^6w0j+IBoST892!q%k zexS;P>sQ-b`g3(IZL=Z+y+{|Peckk|xKaA)v-@I*Wa=E@@E7;?(e%opGj1XsbiaBR z2?C~RP`*VoAUWUmDXw_#Wc&=dJk09wAQfZhiNP*Ji=L2*cy<0~`|g#2(>qImOf>U= zBjHo3(7hzW#`!T4@4zy^uXVaXkb;M2u*!rpW*wSKr{a4(?hvUdcu<4hA?+@1W%RB9 zE@E1*-ba&~xyCP#o{KtW;s>At*)7L{LSOJbBMQXXM1mr`a|;l(a0Y=+W%!)@1mupBTx28wGodaD!3RVU9Q&S*N$3}=H zE7U$ElN0@Qne>rma~K>Mtnzn#ip7{5j8BatYHYPwM>6NIhu^{8PoZ>KQqVX%bEXW$ ztb1;DWAuycE7QZ*G9{Y?+kSz5YU;T+6kD_@tD4}CXw>)j6z)z>8N{7&wO9n@3izT9Z< zhCk2n6~#~sgFe9po6p>eHji#BoN2fJC6x&!!IAUbtu9d|(b@fKTkVtQA&&%}bJe~` zD9F0C3|Z1ujeCf1K#netnJ=(K9N*iBxq&8#mAT_gw1 zJjrgR`QIyUCiuzmpYy`0f`eG92|BW8@;9Lxt%AtgQ_ZLK#BC7~OZ7Y2>pvlD^hr1D zu)L@N;wRh0q!Q~s#R5!tQFl&XlxiuC$TV2XzEkg)mZ?a^d%4^WWDlNzZWu$8dIZB_ z;}8p8BWb%9Nn@61m5`Rcr;O%irEj&iKv_^URY35rldwDF$mL;tR5)6@#D1)zFw7np zu~A6FT?Gu}aqtF~Ij?8vUppKQkqteQY-gKS7>w_YN;2WmaA@?{(V_^nz0rNX&5drLR2q2&@}_tw;F;s34FZ!=aT-5rr-pfnHgIry%tFyWptnWOsh9Vj|XSF ziX*A@MvOMDXly|;!85cY_2l8~(Hv0h6S=s{uT?wNT)S{u3nsGM^!FiuG!M4r`?IO> zBhQOlO(5BRR-g@6$4nH-{Zwp)u>u-+06dBh5+F+})co>aM3jS3xGgm-E!>|oVL^Qk z^Q0QC)ga^W=3hl?q%7WbGE8|wBUxMYx4x{tC-xg;R18r3Srgpv^U|CyK5G1XVCeqy zVZn_@RHw}e4KqkX-?sUFItx~;~tZ|#qlk7v$!>#CUky|f{q)R`0K~r;x zwO+m1rAX1`FG}Vkr0y8CVB#H6?89`FSWLXqhhB`x#5Sn|VkvhW& zj@~YWF8i%l9yt>=iB?avO_qby(@1YXmhPK%hpFq%xSP+zkN*I!}f zjNwk7m`GO|$>Yi<5_+Gf3m8kDJ*+%*|BNdtc|JhIWlk#W@dTm6*Z-1eSFq9Nls1QE zug(|hO*?GYBExvbW>q3jslc!GoJ-$jhxZIe)X~_}@1I$WKfotf3YNv+(sWH$P2fz$ zRrpl;;w3q0+>g?iQ?=$t8KP$VC^A`J7-Hfq!+20sc+SrOCUsONC=bq?5!c&67-21# z-a&v^vgvB>3>l^dMhqli26B6j0N#pP4Tvi7BVnDHHfvjiEJq4TB`(TxmX51VZmrr? ziJt`b$0BIVm!0{Ch!`t*rc?j+_`==8>{|CLWax^cE^tuCT=3fV1XU zUWj{Aao~N*z{1DJzg15k9P?;dz6`-+3q4jI?PiYf#z8F<(QC-8p7=Qkc9q_~M9WD} z%eT9#K$%FwWh|DQI9{&pOzy>*Eq%SCO#vPcSETe{k-VDTjvB%i(hX7Vk#KNAm;+#e*K#u;?fkb_CN zTO!_=ZO56s&6k?uGxzdwPIk1*c?ox(!L!8DO>)(95GtH^2ZX`sjXj=BpWn-a{CY2U zV6NN^SQ1e|tBfG|Sv}I)F9TSh!&2|dH$O8$yDwWuNYq$(L?w@Rz$Pr(L4^TdAbOtE zQuDWz;^zROt8LZzn_OeC6r%GtxfT*gymA)eqQ8oyf64uOv7Nz?XJgjXinN%&cAKaY z3<-(q!>poyz;2$7;zx3|=L$>C@Ab(^xisY{y7_3yEfu!z{Agp;{ER#_YtQ%n3EJyHW` zg*6!Ec0n=+QR^_l>so=1W)T1S?Y-W|Jn{W4m>srPtw0=0U1MsNZAh1TAgpoEbr#(2 zRH4?~jjz`e_r5R>oK%Xk_nBkW(F)pf1)*_PS;*FmlAJRQ{B1WPS|4!+w;maLj9U@D z6GQWN5RhYRYHoHl__zo^I4Kk;S(s~S3G8mN5!m0fq(|~0m<2X{tnjrYLX;VBcv0zA zas!Q;E|C8a>Jh6Rg?e!oW~27noM!ezk{K15K8ib1G}d9oNV*B#>n^!ztUU@`pKh05 z(|?og5Qc@qlWJ&U#-a6GL*hfzE>~5iX(8(!-xWyCOPWYT(RodkkjT+>E8_8-^~0GcS{< z_!m#Wi$7C1eROj1FZUQ%5h$!ie8s}`L!ul zMf{uE??E}qcb;w0<+{YQzJuxi&L)Jr*5K%SxMNbQ!sN-9<_M7u+tU2?28WPpsEK#x zy#M)882S|^KX5vuP^WOXecc4*UE-(QKpfEypH=HV70WtA$V7aYdnpQJ2iP3XPZ!dJ zeQa-hD?k&Va7=!Bf|^6^?Hp}!G4mxR-Non8$kfYMFOq2WBf~vh#@$%a>Rr-Z_J!Ah zA5A?%9jR76^w*t=+%LG1sxM4-7cqYBh_H$^L}uB(TWKP{@f3;&2J05vZ@KfF8Yxqt ztXpP!YJWzudAzQ{8C>3=C2NI=4Qck$^Xsg@^hT=F_H8KNSk($-sUs63rcIOi(hG$da!mz0fjn2R+yWF=GEf zsrSLDMX;5@Vzay6{Xe%`{zVQ+n-#8bvPA?z(=X}(CuNR5Fs9O;I~mWOno%1;#p|N| zMtJ}!hj`W}U_fd9TQXy}|L~<+1A%fitkZyjU+y(#>h6?|LY+s(KV>dq!!?c@U4YDV zp=DrKx+uSBDCEb)sZvjWF_d4t95Z!MQz3e_{VVGR2vmk&EnQar0@s$iV|JPNjzyGk z=WWNK0<$$_T?6l(l8veozMJrc34gWRANnV*2&10TO#a&cy+s-(=U{zsDOg^+QW6(d zrXsqhmggK2|13BD_e_G%#DFQSY{Iu3kHuR>XaX-43FxJGFijyT7d_XGBt1sOAtu7vVdOV7u|_Y+ z9BmRSGcgdi^5VX4`^08K`6^YOmi-DcDm9LOAp;iX+*4G^Vrr4mxO*w|OxK-%T70GY#h;KC%3%#i_mbe;#i1eO!CE?6a@T%-5r{m8dgX(OZ|55$TH} zJz7H|ZqdUXh^V9}uA&}7bl#{ZQ)(RAx{R|5U)JY3Ueh!~>tkz)l5+%Xr7Kd*Fu0)` zS$_5fKa5Lfw;_20J8)XgyR@;?cSTF05f2h-AUCAG!jDEY`YLFyj1o9Czm#^(b=m8 zVFI!r=5vg%OT2g29NGZ5oXrj8S*LV9T zl`~7lq6oEVZ;x%58>O_a#yD;s_$E#&UniMo*NVoMUCvfiX{MrjO=^?2H46bW%Qyx_ zFA}Tx56&igj!dYhva0E9@0c1xU=HmsN;IZ z0@h2ax$kUxYG9FR^89+)0ME{wpb3}N@-df)Alnk#K|W2#5go3R%X3{$XqszU?)Scw z9%s$w+n$&Ud=x`5=^l9NLM_dUVzeLWS}1=f-cB5BsF(?+-R+shv373giiz^YbN1?{ ziD>1%j^+rq5){#*;WM2A_8a<`iJSrKW)fUMXJh5?hZx2^>u0x9jzsN4M1%vNsJV~x49}-br@H| zh^!E+YA;!{_TaY6C1O9ZxD-nYn)JKzyB}88ott>Y-b^hfMo}()Oxk>LMp?(Nn1)#% z&%`$H4QE0wY|rTbY9Y|~9Tc>ZhGCcr{sHEt?aLxLE#hisB{SU0ZSy-rk3UCk&u%8K zj6-DZj{)W^6mW(|{>2bfxlKVUWw}^%gkZC?(NTA+r`_X7ow9t++8?bwmi**(pe0mU ze5E)V0+lkPoFR@ik@li?==T)qN1x`KY{Bwldv;cS8g7btWO85!#*e;yx>(Ni2tO|F z$`vPF$=~|%=ZNK8*6a)FadlmJJ$+exd(sO+zI-_sPbrQm`1d_+zZvCzGI%5#|b-%H2pHWRG7f-x;0UPEC{Ur zowABpE@Rc6+dLsBDXj<4*RXW@7m~(HK%8?Iw)YLj6FUO_j%q8GX9pSPai0*#`krSX zL{5(XtW9_5y zb}5@>+6iCGp22ClEb`79T9~1$dFK-PLSv!Fc8#3Adwig$U+h1g1eysY=9!ds;soth z+kZ1TC_jINGg30az96T{7Py2)@3siD* zdS+^;Yr_OYWIL;(3PQ=riwlwZGDuK%C?)JQDx^ofq3Zl}q(Kja0>&n`(Q|R>#QgOq z*Ndm;P=y+B%9Zb(j+_#n48@#C`nEQ%sq6)of-tn;^W#cES+!h${WUtlF-v*|R zBh{27*oXOSAK`1Buj>Z(lYT_<5O795f|L`wP%^B2{N{%vGlU;YT| zd^MAI#n)m!WKNWGLf@VJt6VERF-jsNxrx;D$Z%`Q(qNT`Bl$08XOf~fn(23ru3GEh zKdONmYz&Ir`;5MwW9Aw3ML}a6LU*U*Qc0bttemXwDr2h z8p}3WvJ`(-!C)#B=)Pr}(wsK}u059Ij9=_|?nqu5_~}g%(oCaS`dH)wE?p))Ti&UA zL7vO9q4L_faEd_sZtG0K3pe6Jn6&f_xA|7$W^r+LL3qA&`;POsS4E66*r2!ws+UC3 zTc&u(nbp7TBfvQA5mfwSiC$KG=HMg~9nG`Ox@_-JzRyWNiqFi8Ug4l$*KH%fypys& zS6`xTwX)3~o?AgNwXw=#N>Yg|Osw7{pDbg*KzmE3>SxdM2a#r@?=T}+fG>Fc6tQDkHQT6%ow*T#%GP@h2D-^4Yk*c`c4{o#*fbSLqZNoS^9g!D(ex( zjkZSeXaWl}1a55Db}|NDR%J(zA3{pp9zad8lH$i9cbRnrPBceI*Y+61N0rSPc!DZ1 z4*3te21bi)C!C1`X(LfI!|Z|&+&2zR4_`l5t<~V8;$FTb%a$1D9Wz6#!LtU>+Pk#- z-f+cg4P-^t{{^`Haut}BYbzhH%Fm5A6OKE#GA@6ASNKLLv~HfSa*mArr$5$(BFyu6 zW;~r!&ge1H)wu39JWanyApg`qxFTTC*~GtS#&|9~CyqIU z*EVBsYE;0_7@V=G{H#4tXNeoGZz(hcWdb{td;{6nC~Db8GP=-bzTk^MlHo_Jo_zIh zZVBZ#qwqx3E_nLfJRf6x5^8q-(HGxD+I4Les^UcSXVUPgb6UiL(U>&tLBc-tpIfRN zx--NnUG`X}_G5#zP5eL^3k{%|Bc9bq4V2vPXTZ74)kJ!Qj8W*wb^q9Z_3yowFDuKi z@-N=qdKZZ5#oYF_2Wc`U%ZS@iEU=z_jTiewg3t++CGy$!#drMtjjTFKn{)X?O_Fm@ z4y8pSr%z)(O^mdnRQ0m{EQSiH80RX|lZ(2{sX&B2p%;J7)OSOb(~fi{WyMN9wcVt8 zbE;QvR)TzN4r4AOQPUkShWBZYZ{ab6awjnX>JRuic3F($K^#(+-x)D)s9OB5ugy|A z;}^XV%C&IMR7fnTS6_0fJt#!a*y#07EIlq#M?4;)vlN zl;xOkEd%)Ioo-68@VpHq8g=NMbLm9aMEOaw#$rt#3X`(ls&bd|fN6^=O_5yvDIPI3 zPKVUIAidyWs2FVj^v_q$ihV7MOR|Smq50tCSs72ODjzL?-r5s)t$F)<5w0pz8_p`B zTixxjnYz9_&+P?OUiDeqf#(dAy=J(SyPeE&AA6U2&K=TBg#`UCKA^=qrAp;EoTE-v zmjQJwXSFf#BcmUfhdQH6jDeLFtyUs8qbSBR_XFD3658v&bWKQ)Djse^Qbyp+_6k$Z z0nF|-q|%J>9+|~Kd$7LcTa?epz7Z%AIt}vvgBjNSo$5GS51=KK&Cns|ZRtJtq>}TU zV>3F`x`KmDOl3O>A=1is_36u4eFv(NE`(%FtGm`nM( z;nvtpVF)$Tw+;boMF0S|7EPey9>I>D=t4@C^A3jChx85 z;Fq{=;i<~3jc#H|k*gr-LyO{_!-y#`ZW1A|#kvSD$?j_nT6SJZl&}!bcy-rEHYVmFVpjl<62uKkiRIDA0+XX z4Q1JRi!eqb_94%MGKJk% ztBWg}4s4Mx!OFPb)8@^qQXNmv)*Ht!`m7Jn^N)U!V4&hbe%`Uhfu(zrByS*Xg5GOz z|MPX;>n_NJiZW;Vpasdgok2%?&+5SWf=ZSiun!LLd)63|d30HbkoMv#$wsx{=)?Wl zqN(%?&s^j2U7#Z@)8mm*Ds7vxgvv0_61>%8GiKQ{6?!dHjo*17|o6+@P zDnol8I+N!TGoftqIL!r*OaA4R2pBLV*A8LUNVY^w_nplpB_c9a{S9QWf8AqZQ!b#S z&l=3J`mO^bM1cr*q}pT4n?4E9Z3R48Ov@`S4fWZEyVq&Z#QVb!A@CmsW2(VwnTT5y zw|QmB;u1LKQ@^1nxqdO)87kHxcAh>a4H~vjNIdR5EI>DqP(oCFY`L!`WAJ4h5LsBy z#PfAn>|>4hc}3w$Nx#7JVpVaT;pSX?4Q$WhACrd&y3uEZ#Eu@y2s$k>Z#^#;j-MKl?t#ighvkf;5Hiy_=QOXKdFH{(Cz1BM0yE(wd_o;}i z1u2H+w-oXeI=8R+i0=D_Vh|H}b9II~q3N=?E!gkQ$<56dkD#}Q*f+mf0UyJXYbu)W4MzWBJi?JUYfd%A>mQAZ$wx@ z;?EjQkMWuG#l+1An_aFq!go%BcMyh4Y!mfdFPJlbWuw}?n_=&(=n?N8#C49Lq&mo1 zR#Ei|M@CC^c8cTOrDg7Id|7;sA1p!(JUQ7WD_?L*OcQIg|HkU!%_o0(ET@pi7EFIb zK3>Qi5!!UzUk>463)&#d+gDpL+G{KK-$fE!YDz4=hCXYu2(5-0V~}>tK6Ko-++ME! zYrgDIXus~BSJs~v#Hb9uqhtkGLg&l~*<1}gaesE418f@HnDSzIUPl1U2*JBo4?K%H@gSW@1*&$9Q0 zNft@tEI{bBeJ!;ES5-;xn0dW{(d@l8+sn&5*HC=8KNvv^z)}oXB_UXrE5l7Z zBu-Yfo>nah%?Y7XZ%}HI2zQRXic|eLm>~^WO>YXcD8x6p#yyj9ERr zsq*&7Me?set-xjsiRlVxM)*X{(hqss8A*vMuSt@9z7a@5j>>B;4Rmt3f+WTo9 zI8$V48+#=>p+fJXi&F&1dIrvs5Q2ScF9Tpawf+aMR%InD5Hv+|`vP1yT@RgrfAP3T zr0fhJ5c~*S7+rw?F9Pl5*ItqdPbSo3?x0?oEcd<)5I0FYZ^X{D#l0d|lp_{_a)+bA zkWcdN-z4r*#0TfNiQNey5rbRH8cvF)Y#!anK*D^ybEzT4w|%Gg#|>RtfIj^WBIJe?zxKfP_)xS) zB(Y)|ZM7tZod4cHD&{3fD6y0B-su$oc$eXM0n|j=c|oQL6=}oU2=K;#moAH``O=x> z2Zf>DsWh)LzBcnrY_v$gZYfPar7>^#z0OnGk$xxnO0uFK-{n2w$r4lcYrPzAlx?q2 zD$$hMF2b~Jzd`;9$$CM-13~nXF?u?P^FVM1v3pAGy+i-(za6e$`)zP4KoH<#geJi| zwPiI(>hH|;FhWnPtQ&qW3(%e zVqzxr0Z5kCs+FQY1R18texe+@3U(xUIO)C&{P$uVaiTyi>1a;Q807m^cZI$^Rk>&d zj=QcqEJH9ft;`fbtiE9^aJ_oQXNlKylVrfhCf~7@&Mam%aNX<1=c&iXsk1*qY#Sc( z5Bp3WE~$tpDy@H8+GSxs%4d4WNc7=EhzbfjGvQ^jSb97CKNi4?jvaz5#^hB`f{MS8 zOS4Qc-ypEIZeNesG-=)#lYpHhno|}tF*CY|Nx0g7OhAoDr<+|mnZRSp1 zdkWBxuE;U_XT$Ng27rENAo!*_D`dRSc~&=~d@JevO+}_GT>K#UC*^na>%&bg0!8^P zSwiO@pzdc3>UAhdHWd@ES8}K)X)irS2sHTQ!(m<#d%F;QdHR=Y15ZnYwlH2~ZbjV+ zy3!YSl=QLsq@)T0p>#@?{uj9~WPMUL_i)9k%KA%`h2?$HCk299)MpFl zy+#+j#hRJD-52kX(ku|3=RBuv(sd_B1@aNAWMhUP;a-<; zg8{6-vBXlDmk@q=qf={zD`hNwwR(w*3Zt#I;95{sT#4UNeArb@lHZxo7>y2s_F9yk zCTwdhnlR=j#}+fbqS%HZ^e1r5H1HRrQyqqG%`P~mC=m~M*o6CHYin9F`Q7DojMtyF z6623BZ)m0nyfi{>z~9DzU!yv;RD<2|O!1$U`(lq|LouSRYY*(`L0we`*c$mNTNGQK) zBsuT3yS92=df(Cmn1`PL^Tc$d7qouE zxU7hPA3Or9S3tp|>gkl0x85-E2I)sLR6%8%(S@OvN22{K3!H;{gj76;t0~+g-r&IU zqqFWgAR;*rK7gEU6;k-w_xOk^(;G?4!uHxe>>5OS9f#czx&)PZ&NEmRj8a}D5`eE7 z?^9;77yonf@w;46)4C@FnDcOsFY8w)pLsMbE-NW2bD2bBKN_WCQiIII#&v4__sjU7 zv26GLY31FHqgGN>(`hs+^zvFxGXgOM6uQH@4}lc`F(<i2vigcCt?PxeOgL*lrzJy(-7&h<#ea};S?N2Us{ln$EQ zPyZUtUHR^8TpYt=p8qmjT1c9O!nvEtFss+<;V^pm0=;F-gNj(uHl`oqKfk?C9@Cg4 zq=9JKy@U}MVmKtRT{<~YeVC`jEy6Z6vxEak(_a}|kR@m3h{f7Nh~q{naFx98qVm2|r3CaZ&xQ;9BJFwSdjfmqaaE&XTWg zv=QGaBloMf_zm-zp^h!bjQ0aiUf?V@&%r;_wNYGpCSsrVT2Gzox4*|U*wMule6OOi z^W|NlYhhvepSyH<3e_hcniV_uohI zM{NXK`MFvZ@6D=_6xs2mj#bm#zhe0s0Vm#{^fj(IX{le4LQ74buWIOPMEXQoO|+s8 z03rP*=YG>A@fer~85V0YC6~?8taRFP@eN|#gg|r=K}BFYJ0bdv$|fO!Rqyrxjk$4= zhN`7V4x96;htwj6T2_fIc+=}y4`(!G+Wp_|s}r;Fvq0f!~a%ka`Mr`$)ENHX3W_a5wN5R2y5fI_Gqes*0&7>kezf0mv0nMRa3 zQj_L6gy8<01H6Rf94`er18=Dc-YoiQNnqA|0*Y!Y=aar?);*c4NM@RAWCYyHK{#9z zJzI0<#vm;9cyAi-L?lJ{e#ndY8Ejy4=+_iI$1#N7L>A8IxXArHd4}MHTBoKo&wGA+JfKPY1=W*iI&)9+o{iAx0pzk&~Y$QSvHJ zJhA7WA7?5};K4^S>+~Xa`f+vH9jCG{FiJ5n0;S~Hu0+sbRCf}Yv2H>ka?$n>$S_xv zw6C=(0jBTv>h~Q?JPp41PdrCTH}S~SCHww2QQ38LK7OjE1l9w&Ivv6f2HKE# zs?i_wgLqLh$8S1%!bZsQ0rLjs!GG)~d1i0y>K7pu)XichNmLlz&!vF=ru*WO&fbUh zDOj)e9Le<@=l);vOyF{0+}LB8F?nY&5m%+^gsF)Nn5pSMM@F-f|lf-W@`gB$;5{H=+jy5n>-KLiI92 zDEgIox?}I#k&@&jEv~GG`FT3?ry?*@<23 z_l67~|Libz3DknkWJcxoqau)73qT#RZCWKX-D0b7=6KBc^`N|ItEXw}j~avW(7S#0 zTC*9vvE(pe-&f|A$H{dM0zQ)Wavb3|{RnE5IdmlN;13J>{KHvL;6Sj@Ct3e-RL6@W z20V*dep>ZTMxj4%_Zy$jM)nhQ>VMyRc>ZnS=Dobb^mhc;7BSk&mUCLIyfZN!kAVxT zZ!!sArWZbOIH2e8SOEC*d;vgvi15u_1Skfc5_eV!wGO-Lci9hQu7xmQs0o!=i2Av zzaK%pG#;@au)5%DyKUYX?hAip4Ym{t64(6be=BjCvAF=|`_-`3L$WsoOpZUrXm|+Y zoV%K4zD0>eb{vyp@h7GHRXR4DIoDQD5%oAHEauWUGof=}r$y7y4A)9CrBH#nowLx| z1^E1=oueK6luJfF+DYw&M@$O2N2TK=FPL$$`JeUSZZEsqm+~X0(!x69zKpOl8Oh;u z&F?zW`|%u4k9t0}@^&=7FA+!YZ|eLAD_R+|m~TynA@oeM_ z1WRhLs>JRj9`O=N3k;h`wzg1~Tp7e9Ui$taee*NO&ZFZH) zxO{5oOb-lhohmE`FcT$_+9iuu)mAg?FZK%yqWkG%H#hJuiq$p^4(Xd49qG1MYlQis zD`*VIjxxQgX)H22EfxlyY15f*jBRY~?QQO_I~(YUn-ttl`jvM3f!IuU=Ixm3ksa9) zW{}4MNSKmO95%lEVoYuH0+D(3dbp&4BW@flRtZ@T<(Z0zf zBl|fLMO7hYAv)f}6KCb7YoOg%9qM^Ce*E`tb3M5<$7Q8zc~mdl2Xn<)-zR{V$PIZ;D_}|Jqn1nBC7LUD#<`n+IFn_@85sygW*NMJkEmg!@aw_&r&0ugbzUev zl?Ia`h!g2{pl-A1mB^HP!R}ey`rr!ybhi{EEJTL8k?E!>0Fz zAHF|&I>CnKqgeHXO@RImq(kmpH+D%ga_v*f2lsLf8EJvOrRw}KXjm1Hf-1G^)E4*y zQO*yOV8y@GNXf?dt1BWhsRf7yqvz6(7%oZyo5HrW6#iGWYmm`8`yn!zc^L4vEd!GLo?-^?wMfOe>J_R|LPK%OV;xtF3qXt1x4O zSYs`R-y9Wh5o1wIv6Hj%8=-n5*={!g$vMf7DV(W-Q8CR|Y;sOl8wq2af`J(RNzkI@ z>Gqbwm0wwmk4O%llutknqn+OYMq5XqVv{Jg{PSZXL6T9}ow~{}PxNJ`12m&GzUmEm z34bmO?unV#Dzy{__mSK$-L8S&zw&~X`DT}oPyp$N0nWh5eEq_PxXhre`*Q=0`*{>R zaQ{tTfwt~#z`$+yJxJ-7^guK_`Y#R2Usa!Y&IcH-`2S;~12#55mQN_hcgd9248yNp zg1A0_y+KZ5{^{*8$Y!h~?RGaT&?-qZyP%mihf1vh{jejcZZ+fm&j-fJzoJZ+dlpI=|CL*BU|51&v*PS6+`x|J0Ui!O+l(cbOc7O8U>>wpa6?mMH z-UkfbW^aRMi5Mk^kQMDoZ-T#6Hd`ka|2q4Trr|~@X{TNjo85)Zwi>LL$^YYTssTDo zT)X+Nq5ROF-^q8$_g9Z*zZ+Ktd)96{7{fax?}ZYHkV_Y7l3He!`&< zsp{WcR|FwulD=9Q$^4O6mnwFxJ z7z4{k1T?S7F!)}thgCFnpsOh*asmC3mV-$_5x4J|8IgH?88D%RS}#{`-6?%wRr#hb zJLOkU&rInqP#r6pBd8563CX{|$}>9z*qM+C-O9&anMJ?|{I;Yr`LCstMX{5F($U2+ z-+!OKFjl2b#rgG4#?*IxdrCGIln3-;E^uh@;_>@aW^TQ+W@1r^r5|~H*;<@*1a(g~ zgUu&77N^{0x`9_o zghnFs@JD3sJqj$A!z9{4AkL?xgxzD#GD{(img^%@?C8r@u7U}qy8M8`He_6?tD)MF z?q8xun-o)gqs#8$)rU9~+H5r)@f_+5I3EDacKecc@U9W3&VBEll^;+$Jtj;z$BId< zw854(3(Glj3uK0+cR274e0TscC6-d|x6E#87`7oQ)Y zJV*u|IEW~)h5xh*k!N;92?YF1wv*6g*j;w){x(unTQ#+1JHC*9)E_9-bZIM)o!+0i zI5X7BEciw1@u{MYNZ&Nrl7pvbkXW=fZ>Z(Q2MB0az!c^8u4fX1 zdz4E3h%%}N?Qw||w8-C@s+jqkBM_qa8VstG|KbM{K+5!Wi^u?*r%*Rv{*XhxVh)vS zF}^wa!kAO}xV!ofVH(N&o6>rz1Fv8^d;guBYG?IPCbOaNgS>fIL$UH;Fpp~ihnVvhhFoNCumBr2HD>Uj z(N%@LEMu3~hTP$T(Eum>BF`7E@EC@@H4&ivF5vFd^%zV%Q%4|av5Md##dq7Pl&7P# zTf8gnN6VruOg!f)cR`@3K%%(va)*K&ggwHCBHWuZer<&r>g(1q8qRsmqRpZav#Q^9 zPU01;NK&cuyG2Y14}~(!l-s4g(Ea@Q(d?4}LmENzKqbdlRb-O$@ip^P4(PA-N`2A( zS$FmCzH(>NIh#;5Ov4y#`=K<+6W68Nyr`PcyTUU57`HU3mszn02oY%oixCetm`P@< zAt+@X$nio3v_>kTro}1EjY?ls$EN=@64?{?-ivGOtPzc#RVLgVYL{`Mp)WoT+}oH9 zWO#z+%>YI7Xe09bVH+q=^gOL3`$2$2aC~{KUA0N*r$*H2oAiYMsVUW!2R2R5TQ`qp z#2isFVtgB9lu!H^E%3z89T;<=>D*SD}s`Edi*Y}>RE~8vSzR& zV7PNn`Wf9xp5{dw351bL;;dgd3u;B?V?Ea(dL#vDgfZZrP9cfDkfkI(Ot!PMoMD@1 zR)z{Kf}RSQt%g-I-wU@Kct96cn>LqXBucs6Szj{i|1f! zF}qUZdg|roXW;2P)*BJ_XHX@sbPi;}*+#_)Fo=G6G1RKi8UXRfyD68rO#EqN;y3J6 zj$Squo}i@+%k1ulQNPRZ*GD;5Lj}?w-LuYq7 zEBs|FL`6|$_4wj}S0sOA6fc~C9pEQGB=YyfORRC@h&SWm0ai%{`Nj0yIRV$wx?{z?g265thT|wh#{5QTgp@!%u?Cn2 z;>Nc}LpoqgkNMK~;4$M|NQp#5dtOEVvcRtRkeD$qpzEb(OC{(}pw|^LQVN4^PM?c8 z)BfuD<3PEF%LLICO7aPNSSYrT-%fY8; zTBKd`g3pC;mXY~Um8K}s^D=j;r*UL2w*cn(69O+NSIB+1osCnj{aEoOowrs@tqM`M z2w6bC%-?@ts26RdPfX*s*}q(huqcuw>T#=H<>KxG6b|{ein?6))vXSkhl4Di1ebr} zi1=RElZN2ZASA~Wg(t{AoWV_WNqsgik)SOtb3U;dnUyAYfbsaZuPZkG7F;7i_Plnf zKZpBU#oEGi@N%}=XY_#}ALmzQYpt`ykHRfw$HXoy$}_Vo69$RjswvOTP;YI*I=O+- zl3{c~NFT?Hi<)Z_J*GSWnT@?3B;ZD?LmL-YtM4Ct zt1AX8H-97BkKvkGi$ZixdkkwdKAq>|94du1mWHIunj17quEd#o|Ll5G4z?#^Ar;9V zoEg-U`2Dk^uYQpyY-4Juir-~4@n~nqcRoifX)Tm6X^S3?Vw4*BY9n9}6dNMSII$Br zkOa4~J z)qPPxB&r8~PL~q^aBkdD^LL@M~B|19+yOr5AjQ7Xx(#J*& zPCfjolxIsu41e{tuVg_akQ&?69aNIIq_F-;4?{j5_;@xg+*kRM$li6LgXW>d?`@%Dc z=T;$ivB`K_nHN)JU*+hP?WhJ@m#G(A%DkXg2Lm%ZUOX;8y%}hN9gT=%&Bgm|oXp+T zO;c}BbV{2KJopF7q5CmFmaB~Z$BeJ;K4oUuQ${qhwvm9~G1mByMHfIciC>96@kBH~ z^c>F}U<_T(VL(i7#f}wN?ox&1e*G+4#W}=b8u$DwPe`GNylOKdedi!M1V;;dug9!> z$_*l7E65;srgCx+G9H`T&w$CUi<6GzrW7t%xM-M8U8(Y-25R>vP#sDfDY({yKP(s| z3g)kJU^HS}u?(Sc5b4CB_UH;>p{5h}oMn;>YpSFi2=r%ppj)OuWEVAuG?=~f z6nZ=IO)CE>_Jcz%G*iC{XQGI7Mo8Ay`kkaRAHY0CwV;5+U9T=H)e(sbj0y|+FjV;q zaf0a(?-u-#j`Bk}NXz063)`t$l-yt19V;DbVVnc+EfL5G8Uchbtd6&i_j~$2r223! zd4)DOLtav9*db`}?e83=?tc0}Vb>rT^r!`o@U7>WFbp80lhN(P(YCe&mZxIff}qX%45EzXX$L1dL7wg8Kj7G#&~D zs`EnX=`ZXi**#j0ZWwVctBN0JeS}@V7Shs2#EwR(-{xNU8SpW9hs&9{5vFp))uj8! zJ&vStX*2i2=7`rGl&6awHpk=Brz3@@*h;693_7P^l6e$+nDr6CK@}OJgRF9o>`ev| z8nb4XexHH3qTs?0Fk8&`q9CZ5dsgS+iM~A@vM{~3fu5DOON+aFe6;t@J3D6U5TlOF zxWvKl?YHwTp1L`D?464_trP;?`p>U_Owa#L&_VK&$5dez6Y~Lgtx{&PaukLbMr5CCiJ(A4M-S=Fuec?myzuUbjJYcZD74xFG~Y)qeRQB>z?~i zP(hN^i%M4uJx!m>I36HXzL?pV^qVIGJL_Of{P|3TZ_4z^)u!etp2zoY& zU3^OL`;O;j!Nph^oKmBdA$jG!rJsV&#*C<`#r~joU1c6@ry^`iMWLMEYEnt?MWuyzO6*W*P<436rodE`j@T#kJyUZo!@sEctEeAr)=6mhrM@vAPOkFB{OU!`wZ8yd+9! zv*;c-Ur9m6fYotXp{ zwaP=H32R_`>FGB|$x3Ml0U20cTPH8(1Cv&-Q%|%XD2a7S`_LMA94|}f%vdy=LZ==ty zh#$=-e8>KP{V`8Wsf?TYxMEn<1^3e0t(i&atkohQ#(^JvXjmED8R%X#v!*N;Mvmwnni5Ml^Ui$MgRp^2q3_0>|lG@l1e^VVxQ`$^k z?`!t9&wGc&A8V2M6MPUX-9c{!CRB{U1(IBm^0@ivY1`=QiM+z1hmIvfKK-bKWBr`X z>SXq^x~3^AmE|)pb!srU*hD=9Iv7RwRxH0xTd2q zY%A{h7x2B)G)JqKIkaq>VDTxZC$vNUx_>;qdd?wpC3Qj$7|WZ5u-4g9`izE8mC9%9 zmMZo~i+gt^sQQq_=iBAP9n^EFqxY&7gBr;@h%{afGDzJRH_4SU_q2-6! zZ(Zl_G-j|=_;?yreQsWfsZtF3bXj)%9`IM_~CjR?p1p+8jmh88D z61wgvHa*`kCw1L+U;Ea_@zve0a7R|-GDcF-l27A_)e+7+SZm~u)EmNW;X`Yi7WPx2 z+nWmu7{8|jaVKSUu}5Nwm@hh5{T^tp!?gLE$P2f8PwLaeGgDiY?%#8mU#41L%9Hb} zXx7Z9c3*!mpr#5&Xo*GBYO43nlh6|hE;^V5*Dn@y1dFSMC$b1q#M{N#hnrk4=t->C zQ~#*ceg5~fSo{mz&A!YP?v1;?e~U3cKe`4UMQKz>$f{nJI@-U8>TE{z!As8PNppqP>*?B;HwkYF4m(nwuJ@7tpnSu8{>xlY>-uzO4;R6@YQwe_6zyZb;>clm z&DqE`6&HFbwyO#{A`f9cX&NmwiwLZiN4Q3DgejmQ(H>C@^VB<6(XH|{cTD~|}J4Fa3zE8Pj-yMBz(fE^|!N+f7WxLC>=3<`aP7O(87K#N8O9On76K2F;ihf(0doa814fwgs zYpGq|FWk+-R1Nqw!1SZ+k6<^?*jIzWJktB6@KvQjC-%qH+)>AttK~JBZ|=UXaUNS- z=qc2^M&M6RH?5-LTQs*l94OJmq?$5UND?($s6TlyTP@#F6~06)SYM|?68a;es#GyA z#;>w4F}ZlsFJJRjnVm-To44=Dr%pkKodUY+{&i^bFM=zA0F)}qgVupp1-L=p{y})P zAh#FsYPRhiY-1n5Qgse)r@H!}n!xjA!k& z`;7@6q%0HF#mXIGVTlv-#NUsM((|gkO-vH$b)ABnze1$%<40cAPHW@S(w;2$@HDf5 zLaoh_XnpQ$Z)b(XFZ@g}#`2DxwG`csx_87y=4YUB`pNTC4y6_D4NJNQF=DKpFgCYB zK=@ndrJ=i#+(A0N^TYB-+PuNd3mf{>&U@1NU4?5GF(4RE}^MDiURm1 zb;RRLs=b44Ya!v(EVl$x#&p7Tc>?hVNNUu#BZG!W@O4kXe}< zta9zqUT69dcNYC^{DRna{4ofKErI?kh?wFEgKgwh;oqcXY1jKAi*H0{MT|zy8Qyxm zKrvyw_pSXc0m;vh6TOG~A9)`6U6bsfZd|O(8~BLTn8_2r@Rn)Vw&osd2ytH{*4yH5 zl{cNF+u!XMPm$hqO8Bi!_gsi4P{Y8NX^@F^av*6vKtK4P3caScR4Ix4etOUqrtV;u zDC0scMIadw&~Ju)MjTrWq7!ZJSy`jiSAoNkVP$ARI(j0u|Fw;zT~R8MrI4**sn=B} z$2;Z96Ehn`wd52tr>0pZxUx3xg{#ummwEiFlbt?e8JYy!J9!zMnVCl|E3?%;OtAfomW3i0<`nZz|t+bDQ}wB^F=n&Gj*X-F8fv zhP25qIXIR*b9HNV%cAsgs^{B`iOgq~>r+WRxxO?aX7BgwnD(Vkl)9eXb99lmXCyE5 zGSoTjpYeSps6T$7W5)FP#^eV2l-jpBdZS{tkx;@Trip|dAN<1<+cnMrXSm%;FW`tt z8)A5AA%=|V9R(%miRAuODQpcHx3j92=d;j?2}O4Ks91ofZk)a^%~GsSc@U9n?-)^J zbn4r_aTGIGKJY}g_#wW7U`RC&>rzY+F;medqwDc(`X7jp=mQqerik+idsS08=lzP# z4VDi*;HR|ej<6C{%D!dax|F=gq}wp~jY0JA!X7jNWM7bzI&DP8ma z^Ot)bzE3}1wA@jw}gw1vbgdu<~x3&EPe=ZyA5z|B*Hl74F8pz3 zd}_u8iB4uYl*zd@A_l0lfb1+Ow<25#{$hZzs{m;2pHG;!grp*NDCsZ)Uj(v)0OD@! zvU>u;p3$dg9V=!mzqc{#J9X? zJS{8#U`0o22U8o~z$>@{NTEjOU&eTfYL3vR6=lw`5;^^}ot#8VgrvZ_6Ax!25W!Nw zT6|O`ziE2{@&Dd;fJ;&n*0<*o+CSb11LV&(4Ro0!@BQol0XaNIDXg{Ep4j#S6{@X9 ziF3SP_09i;H?NJAzxD!)Tm#?458v+hT&<-I<+?7}nHr#hL_oLnR)Lf*4Ggp#l+R92 z`jEJCWNMbcR_z>T%J!=#u3m5ZqdE!gIZ8-^tJco$L4T%6k%vE4s^gz2@DGJOMQ5j* z_PNDR^sze=J)VKa?TbbTr7aT8Oa`WOP1T`hF*oVpYs)1Axk5G236+7n(+l>kwus3k zpX;)^3ruuU!PuZ}9S-Ju`B`kl=Qt99l59)>5>cj*6Ai~;U26SS&Mu-I5%DS|jJ#La zTly1r-fG5RWx90sUdMKJrZ#MWqZbE;=MOpv-NgFHy!~w`e!9N~Wq{JaEiG~{TN^7d zkDyrW#jrq3_+<2>FTDdLO;W$P+IvyUYi3oKyp*)Tg*?bRzVR65;ftVs=e_Z+B0jB& z-OI1?N7slCY$D{A(zUCFMamoFJ$VN&4kv#Pv6G<#}kja31>EM zV2n+!n<1V?dUc+q6o(=}C?UBG()DDnkX>wMNae=rdkvLzH5asY8> zdoc9HPxE1o#}QM*kmw(Fm1FI$0Q^k0%SFPV3%xZ4}SH0pmf*+a_c~B0%wBBKGLM)e1VA4 zc!Z|l)3c!VJ9e^SH5Kv-rK;Y52qZimCh=Fu#s*11`UXqXLoNvKkU&*FbP7B!XZBmk zI@H6FJq~gos1d~JB*RF6)47dSi?Y+-(H)WCfSU^0i+{`uIK@AvM!N(bsIkLFAGKf? zr+Hqh@!!fO-{^Z$RcG6;D9@Eq6~)_h%JltC8Il&U2xjdrRNbS!O=C_8J;bv=>mZ^! z9hf-}tXLCj^C5+K5tNl4;`4|j;KV7oilxG?Ts`HyJ3CJ=dwDwmpVIrT-V^LvJ>R2s z&LojHqG{eYL}?~k()kR<$63xl|EH*q-A7Dsf8$3Z@yTn>eM9Yt5%p#iws@XO%OKr2 z=iS=K+$d|&kk1nEdSSG9;)*QO>Lh!t;(Nsm$C`dX%$iIWHG`~$qOQBrzRpmq<}qzX zO&*DMhHwU1ue4q~W9Cnvd`*b?j2nyCTO=FoZLyeJX$!gatRt)4b4WnRX!ntVg>~e< z?PbH=4h$c~jd9FN~bLJCHo6yTGJqDM^9dTtues zfy&CqSX6w68-jE2Qw6}7RS<^B)fCi;>vd)+?LYxQTW)3H(1{ zVQFB>(hwMu%6T@;FkKQCX2Hed5+OL>`Ux2D_;rjr=d7`8Jglw4gRn4Vt~!1~x)+PU z0#RNQGSlT~aSU%zN=oChK2}Z>=tBzV_kBMgL9<6qQSG{&xZMhRA?oerOYYU?(|d)VF<+$`?Xg;Ea%$B6BQ75#(`vmlbTr1kze?voMz@hth z0!c75rE15vJdZwEvmEi4&+yC(vI5HZ=u>U-_5hZCtL`IX5BR6|1h!3z{EqWj3S)@o z#@?cRBX|_tsnY6ImV_wTR^*%8VJ_B`k?>d+Tcx?_`oF%(MU>;oVXW1w`6XG{KR$E} zhFs3DNs7yT)*{t3^c7r^f*#1e!5E;^&qCi4VB_Kg zX6}^HuJi=&KEgRpYKwv`ftt^#DzQH(Q!X6b-5T|DUBwKRbp2(b$iF4sHFx+Hw0b1Q z;{F|iSC#|Hx8?H)QL2`Y^BxNbxYyLoR&qJu`|N+>`_hJMx{#3hNAm1=n0wsJo~AGfg}XGCwNc70%H`e6gNuB?=H256ZRpxA){<+?`1j?MFk@U zf>d0fGS;)=qp|+)G7B9#4weR8OZOJKWEYBOM~#&sLrUB5?5einK3RxzA^Od3Lh)#Z zXlF1XMyGyJ(vM17kp~B5ay76$zB}IQS5Xy7H%BZvC^jQFv72rehAh4j8KyW1-HURyFv8kNUrlZsRuqgI6y z7~K|Li0$+oSEPx1ZtzKss7ZaaJBZb1{87$Xn2018O!sP0#D$h4_95@D<`ed+2{j4F znM|y$SyHGfP#ru@TPJq{-pOQ$ICKWVMopZFXq_5cX|>vK_Zw20+5Idah$*ooca95l<@PL zUR1{@T`y?XfArtE8fgwOSu=j z0zJlUUMK0u+`$T2jMon6XQRB)z$sE0kH$hb+q2!(e5kchKP5Eje2vmQy~kMT{lUtw zU-+^0!10@eWM2!1_ARF*xxb(K0jhI}V1`TaD*xMEK5qJXB%VNmI-pK>`^r$YO2+}D z9n83nLSbfD4c#)WYSph#0w&)r=aG*9mq~b=wDOLu(VxSOt6uKQ2P_29WX|=m>o2Ab zY=h}!Wq-$I#fbS}e{2#~sGugii+Is2pyfzV`eL6obuH#zIQDWxoi)$JiS8)v=)vS7 ze?#CtmpH%hbjmM1ylJMPK~=)b=7EqS^wz$&5KeZ;v?$@l`86KzMp9G~O=dlO(uEZc z!lMVdc7N{#4|Vy(;KdEDiXro>H?2wL7yt7DFfc26KR#${bv2fM#P<1Nyv>^RkK^sp z`DR!18KT}q%>aQo@5)K%o**{vsLp5d6T)BRc6h#g^C&cB<*A@!?G=h79PoIq!Lq@R zzsv4hD|xxz<|psUQqSw(uUeuFp32^I#(AH->_z{(t7$1C>P__5%4T|6AHEOuT}mp} zKEoozglFDZYkIMje=l6W7~rgXXFy3)*Wg@o1>3{+yONxV=-nan^+X>4QtK4yMfGk?5OZ{ZqadZ{&t}cu2`3a}`d9GTaOf17-q{o}c=uf%PSJJ!flMYJ%khm8@ z{oARO;7Guka$}4n{=NNr8=|J@%X->~rcX$N^Ts&rZDc$z8ih+|ESj-}G6EC-Q(@__*Koo_LT;3q!P;xGGQ-^rJI9j&oV^8e4g_m>@IDw~76T0;C zFgH$9lKyT=j>?v>eH}&fTSkxZD;}+Do){Mg<=s^EP`}hW4FU~5hR1HGYcGxHQM$}p zTcW7<7EjECIeN%Q*^B>MOuDBpuQ}~Taf=tYk{%h|5ve&|-&d!|Kz-eNcJkvaNj_svxwXX1IRfDp7(Cu-dp~CntXq3@)C2CZP?B+bZwn}o?U%NZ zUI*QH#@xygz+3AA1>aRBNzNsvgUcekrP}HW(aO=rlu<%L;lHfp3>2N9vrpDMRpGa^ zw}I#WUmwR@=IP^}s2s+@RY*^A44s}9-Trhb<3#=n=h_NtW6$;1>9aW}h?;vuW4*_$ zV>Yns%kwihv+PMrBsqIoe91%#>SM0=mO61#jFnzd4r!)GMrJb3%TzzZF@_Cq_H(rh z@U62@nEt|xL%}{#+c9DAYx+~%z;J@n4j#G~22DG_ zMIq<{@ymRXn;@TA9p_|jqO4#6a;x!`i+e=j8ky9bOh7y#zwE?-n=`zf1)6t_gdRiS)GIMbZVppJ9ZW?^RdnQWsJG>-9d1pKM!)`5!tg>^SF&_>oY0!54`4vsoQ`!=Wi%a!Y zo{{!|uljOEZyRGA)!%{+tG;y(alEfGl|o85Qy&3oHAz89iWv+4V>ekcM$t9Lbo}c} zE+hEPW2I*DdxaUFQxtColz-uUbESG+A$t@?@E#A1o_fyE?zoqEb}(@%tWa}P+Z$=h zJtMc^mI}lmOMP`o-17~6v-LaWwd&T(( z&jiCWZOAXgzidBc!ItSbS#kSO>_@U%OusweX(j!M=#Bnzl}aU9phfCrKsJkJxjJzc zR(}l$DN-Sdm__&Ig2*Y0t)#8$w-)qh@o~*9QA`)x$)1ppuab@M!%J4`uP_S|O99r; zv_R?Irl2$(!;BUr;Eh*{8M6o!-wX=ajpdO=rz?v|CL^%fYrfTN%LV-An6#U7uM$+* zaJS~0CV>cG_*u>LrMY%If+*mxCmsq69I{7Zf#y9W|)ZB2MARX^kM_)Bzm93etRYGFE%Q`shG(D>_ zC*|~tCA-~>PLOT~AfQT#A;CU{y^=vyTqGp-P9c$v##%&=)0=mv>=lo|7{H}M;|l~t zO0;6VcoxL7=%rz@eh97cb^AjytMr+=iXf-iI))sa*Eu9c++mtkEBR>#H)(!6(WBoF zdx(xn=!_@))FrlNM}W`u$`(&`pin!OMj)t63ezlNUaHm6I&r*BxDXF1^5iTZ2h#)` zFx;_*!=Ub|(I?j)ws9h;@(A%B#2Si0saE}N)Y-5}^~qt}z8Ei=Ql(#8|O5#Jq}nO9o6xL_Gy z9%?q6q@O<<&yOt{Qp3v_7W&l4n|=X(rnOrBRGf=cQg+F-*9E()W(ce045%kXjz;wB z+oXI)mvQk&+5@`+Zzr0l52oo|#GuJjm< zFt7CQn(+CD9p+z6aclAmW zC&GKps@Ua3s@I|Jyx)|tj@w$&*u%WqE-X(?O&tX+)IZ;^Nvs^P#9(>(ZOicyUH{q= zbZG~KSsu}flI8DkiWI%(kGxa2f* zfpywawJBXiJ!vTWt(O^=+6I~yo`E;3jMx6&x>LqjnIP8ISW-KJU}8N`rnL66OjS){ zno371R#xpFl=FYn72g;_<;G{~P@m9hW-MG+JB&l^nH^<#ifKi?GF*2zh8MdnFPGSu zp!!(z=s|#+jd+~Y6s3=+y^}RQN3v>IMXmvNGpFH^)*lhq<=BTV-FEZMN~)VFW!c|5 z1D`+a^(+zNx-7evmjGor=|7T$)OEs=@2@}5N9VcOaL`lhOLy|nu8eI!$Z$_mB%Q)3 zNhRUXN9fzud?8DgdlI7Bc6U%7xaeb=?iV24sEm{c<>bwRa z>$6f=eAt53I8R8x9;}d8qV-uRVZcbJozev%4svxl_U@~tmV)_|e zerC|IF$E~8Dn{*v@R`8vYJHSphfeOM;DgiSBTh2<7CSOFd?IXzLq6B1W+LwEq?7Od zLvO6SC_TF+A&uOk0@4H#PercE%&=88!-Z@mV#{tZmgW0k&~WAq%V5HFfDs59%zEe1mwLr6K2LS2VVx1OPdMVguOD1=z?oCJ656EoYFAc5Sgw;Lm;xg=4(nTJ7I0?I8CIMJgV`;%&;9h^eKf2)+`1+JXs%J>B)+3>9 zO^oun+3h0bu)~&Q*_$*QVv&p@{^^uH>EC`R#FNbpwoJEUQGDHw_Ps8m`R{4Y%04X+ z{gFnl&2@X7i7;1QD5XHy%P)UxlsUc`nfy6!W!yafR0{5(@LM_jt`tr#YKdX&%8=@h zuFuAl*fDiX``6%+&Ay2-&r76J|NKoRnedXP#`Su;GC`%S%(aN~4iFmy{6t>fybAF$ zvn?1O@f6n|zKHAxDrI?k9WFJtx(QpHK`50 z+M5HWQ(!LSePC9Jc|I-co74( z$XTV(Xw+~a6s8rR9&~Sn_o%UOrE1ei%;v=3Ys2`RO@cK#%czXAaS%Z!_*vy>f{f(r z@w<6}_R_e+kMRRS7;k6u6a4aj?JF6i94$4h9>l%8cM#Zz=%k5!A`IMdUsUY*)Z zZByYA^88+yrYP3W;=@P%^QGknn1&J4;Yv@)G;j zZ&}x@)^5w)Mee5&`g}nn!{pv^d|>!Mm6sr~ZpLW^7NJ3ou|ezUY4Kq$&oj&M2}S+s zcg5u2a07iCzUFPxn-RCkvcv>USUIKYxra}fg`9b!)wZgb9HtpA0y`*1^5>RLt=7q3 z>JjRFYqE050M_IjGtiQtlnJ_^rp!40oUo;cO?t5F(!%wlQ9JqGV#gK(c0E`^$SC@4P~v0CT#=>@{N>Nf8wsusm@RtRTq9tJ5FtW;%#|&Kqt`bvf#&8 zcuEGN%H8Q+IC~}NoqdqyD4odo)z2G zK6A{Rzh5RlIWD>-boa~aV6(WYlWESOJ7>UqB*MS=hwq-KoFxB;cR!z!B$B?-@+}{_ z5zVDGdU5%7&x}59?fp4vEK+%vBMC!hYotM$5dneP0=vs+f|i5RRiscl0$q+cF@A z_t#{GH!yLlq4BUz^bitN9%lNTdhLO8$O}+9lSjQbhvB`SGDOt%0UD31Q~GhfWyofu zqtCCVM~?5^M_qQsppQj^9Z*k76aOEiM&9dA2gh&8$vPcVzjo&OVAt)1cFHu7^?1db z$X+bDO%AIB>zPr+C+XXr!tDQcS?Y_IKg;4fyt*x{-4Q0W104yy&Zvz%bZsMN`jelDyX4Zv%;TO6Rxe8hg z9NpEfQRqZC$ad{g{OZm5c6|@|EnkDM@=jqy+;Dd4gT%O%0|O)taU8%)WjQap@cchB zN>QXlx~R%HDeYAc-Lpcvtb}y`HbYh_c(s+5j9xc>h93j;9rMM3EC(cSQ25|8--yxq z%H%Ll-z}OOjpV(1RgOGb+%5Lnfmu#uq%5$67;Qxq&S?P4$djtkf6;=r&{qn+r!F$8 z^>D;-UU73R_-~;u`5XACyM!O!xa?bV8D9Zm%0A>A|68fit4F{t{Cw6>&8^RiVyztb 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%0A>dR22?P+=33 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

?Tw5^j2|F7UlNIcESP9uzpmkq?c1ue(<5C? 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 # === Required import === >>> import pyomo.environ as pyo - >>> from pyomo.dae import ContinuousSet, DerivativeVar >>> from pyomo.contrib.doe import DesignOfExperiments >>> import numpy as np +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py + :language: python + :linenos: 8-23 + Step 1: Define the Pyomo process model ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The process model for the reaction kinetics problem is shown below. +The process model for the reaction kinetics problem is shown below. We build the model in without any data or discretization. +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py + :language: python + :linenos: 32-101 +Step 2: Finalize the Pyomo process model +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Step 2: Define the inputs for Pyomo.DoE -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Here we add data to the model and finalize the discretization. This step is required before the model can be labeled. +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py + :language: python + :linenos: 103-156 -Step 3: Compute the FIM of a square MBDoE problem -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Step 3: Label the important information for model on the DoE object +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We label the four important groups as defined before. -This method computes an MBDoE optimization problem with no degree of freedom. +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py + :language: python + :linenos: 158-202 -This method can be accomplished by two modes, ``direct_kaug`` and ``sequential_finite``. -``direct_kaug`` mode requires the installation of the solver `k_aug `_. +Step 4: We give the experiment object a get_labeled_model function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +This function summarizes the previous 3 steps and is used by `Pyomo.DoE` to build the model to perform optimal experimental design. -Step 4: Exploratory analysis (Enumeration) +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py + :language: python + :linenos: 25-30 + +Step 5: Exploratory analysis (Enumeration) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Exploratory analysis is suggested to enumerate the design space to check if the problem is identifiable, i.e., ensure that D-, E-optimality metrics are not small numbers near zero, and Modified E-optimality is not a big number. -Pyomo.DoE accomplishes the exploratory analysis with the ``run_grid_search`` function. +Pyomo.DoE accomplishes the exploratory analysis with the ``compute_FIM_full_factorial`` function. It allows users to define any number of design decisions. Heatmaps can be drawn by two design variables, fixing other design variables. 1D curve can be drawn by one design variable, fixing all other variables. -The function ``run_grid_search`` enumerates over the design space, each MBDoE problem accomplished by ``compute_FIM`` method. -Therefore, ``run_grid_search`` supports only two modes: ``sequential_finite`` and ``direct_kaug``. +The function ``compute_FIM_full_factorial`` enumerates over the design space, each MBDoE problem accomplished by ``compute_FIM`` method. + +The following code executes the above problem description: +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py + :language: python + :linenos: 14-84 -Successful run of the above code shows the following figure: +An example output of the code above, a design exploration for the initial concentration and temperature as experimental design variables with 9 values, produces the four figures summarized below: +.. figure:: FIM_sensitivity.png + :scale: 35 % A heatmap shows the change of the objective function, a.k.a. the experimental information content, in the design region. Horizontal and vertical axes are two design variables, while the color of each grid shows the experimental information content. Taking the Fig. Reactor case - A optimality as example, A-optimality shows that the most informative region is around $C_{A0}=5.0$ M, $T=300.0$ K, while the least informative region is around $C_{A0}=1.0$ M, $T=700.0$ K. -Step 5: Gradient-based optimization -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Step 6: Performing an optimal experimental design +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Pyomo.DoE accomplishes gradient-based optimization with the ``stochastic_program`` function for A- and D-optimality design. +This is an example of running an experimental design to determine an optimal experiment for the reactor example. We utilize the determinant as the objective. -This function solves twice: It solves the square version of the MBDoE problem first, and then unfixes the design variables as degree of freedoms and solves again. In this way the optimization problem can be well initialized. +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_example.py + :language: python + :linenos: 14-80 +When run, the optimal design is an initial concentration of 5.0 mol/L and an initial temperature of 494 K with all other temperatures being 300 K. The corresponding log-10 determinant of the FIM is 13.75 diff --git a/pyomo/contrib/doe/examples/reactor_experiment.py b/pyomo/contrib/doe/examples/reactor_experiment.py index 912cecefc96..2909727751c 100644 --- a/pyomo/contrib/doe/examples/reactor_experiment.py +++ b/pyomo/contrib/doe/examples/reactor_experiment.py @@ -155,8 +155,6 @@ def T_control(m, t): neighbour_t = max(tc for tc in control_points if tc < t) return m.T[t] == m.T[neighbour_t] - # sim.initialize_model() - def label_experiment(self): """ Example for annotating (labeling) the model with a From 462a75a6b17cff49fb5e4993e3e3c0b56dacab33 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 23 Jul 2024 19:14:38 -0400 Subject: [PATCH 1994/3044] Fixing literalinclude statements --- doc/OnlineDocs/contributed_packages/doe/doe.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index a9afa35e32b..8148d707359 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -165,7 +165,7 @@ Step 0: Import Pyomo and the Pyomo.DoE module and create an Experiment object .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py :language: python - :linenos: 8-23 + :lines: 8-23 Step 1: Define the Pyomo process model ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -174,7 +174,7 @@ The process model for the reaction kinetics problem is shown below. We build the .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py :language: python - :linenos: 32-101 + :lines: 32-101 Step 2: Finalize the Pyomo process model ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -183,7 +183,7 @@ Here we add data to the model and finalize the discretization. This step is requ .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py :language: python - :linenos: 103-156 + :lines: 103-156 Step 3: Label the important information for model on the DoE object ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -192,7 +192,7 @@ We label the four important groups as defined before. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py :language: python - :linenos: 158-202 + :lines: 158-202 Step 4: We give the experiment object a get_labeled_model function ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -201,7 +201,7 @@ This function summarizes the previous 3 steps and is used by `Pyomo.DoE` to buil .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py :language: python - :linenos: 25-30 + :lines: 25-30 Step 5: Exploratory analysis (Enumeration) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -218,12 +218,12 @@ The following code executes the above problem description: .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py :language: python - :linenos: 14-84 + :lines: 14-84 An example output of the code above, a design exploration for the initial concentration and temperature as experimental design variables with 9 values, produces the four figures summarized below: .. figure:: FIM_sensitivity.png - :scale: 35 % + :scale: 50 % A heatmap shows the change of the objective function, a.k.a. the experimental information content, in the design region. Horizontal and vertical axes are two design variables, while the color of each grid shows the experimental information content. Taking the Fig. Reactor case - A optimality as example, A-optimality shows that the most informative region is around $C_{A0}=5.0$ M, $T=300.0$ K, while the least informative region is around $C_{A0}=1.0$ M, $T=700.0$ K. @@ -234,7 +234,7 @@ This is an example of running an experimental design to determine an optimal exp .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_example.py :language: python - :linenos: 14-80 + :lines: 14-80 When run, the optimal design is an initial concentration of 5.0 mol/L and an initial temperature of 494 K with all other temperatures being 300 K. The corresponding log-10 determinant of the FIM is 13.75 From 087eaad99b0758a3295394a2473706c209c731cc Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 23 Jul 2024 19:19:14 -0400 Subject: [PATCH 1995/3044] Fixing literalinclude statements again --- doc/OnlineDocs/contributed_packages/doe/doe.rst | 7 ------- 1 file changed, 7 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index 8148d707359..291fe99821e 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -164,7 +164,6 @@ Step 0: Import Pyomo and the Pyomo.DoE module and create an Experiment object >>> import numpy as np .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :language: python :lines: 8-23 Step 1: Define the Pyomo process model @@ -173,7 +172,6 @@ Step 1: Define the Pyomo process model The process model for the reaction kinetics problem is shown below. We build the model in without any data or discretization. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :language: python :lines: 32-101 Step 2: Finalize the Pyomo process model @@ -182,7 +180,6 @@ Step 2: Finalize the Pyomo process model Here we add data to the model and finalize the discretization. This step is required before the model can be labeled. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :language: python :lines: 103-156 Step 3: Label the important information for model on the DoE object @@ -191,7 +188,6 @@ Step 3: Label the important information for model on the DoE object We label the four important groups as defined before. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :language: python :lines: 158-202 Step 4: We give the experiment object a get_labeled_model function @@ -200,7 +196,6 @@ Step 4: We give the experiment object a get_labeled_model function This function summarizes the previous 3 steps and is used by `Pyomo.DoE` to build the model to perform optimal experimental design. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :language: python :lines: 25-30 Step 5: Exploratory analysis (Enumeration) @@ -217,7 +212,6 @@ The function ``compute_FIM_full_factorial`` enumerates over the design space, ea The following code executes the above problem description: .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py - :language: python :lines: 14-84 An example output of the code above, a design exploration for the initial concentration and temperature as experimental design variables with 9 values, produces the four figures summarized below: @@ -233,7 +227,6 @@ Step 6: Performing an optimal experimental design This is an example of running an experimental design to determine an optimal experiment for the reactor example. We utilize the determinant as the objective. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_example.py - :language: python :lines: 14-80 When run, the optimal design is an initial concentration of 5.0 mol/L and an initial temperature of 494 K with all other temperatures being 300 K. The corresponding log-10 determinant of the FIM is 13.75 From 328bc83c1e07e217e5b9ff2709c87b6808542fc8 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 23 Jul 2024 21:08:06 -0400 Subject: [PATCH 1996/3044] Added examples to init --- pyomo/contrib/doe/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index 6ce3ada420e..cbb1ee50f56 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -17,3 +17,4 @@ ) from .tests import experiment_class_example, experiment_class_example_flags from .utils import rescale_FIM +from .examples import reactor_experiment, reactor_example, reactor_compute_factorial_FIM From e3a00cfb627a8572ceccb9ed2b1f00094c923098 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 23 Jul 2024 21:30:18 -0400 Subject: [PATCH 1997/3044] Added missing init file --- pyomo/contrib/doe/examples/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 pyomo/contrib/doe/examples/__init__.py diff --git a/pyomo/contrib/doe/examples/__init__.py b/pyomo/contrib/doe/examples/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 08ccf404447c59d532fca8150f5b9233e0652abd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 24 Jul 2024 10:52:34 -0600 Subject: [PATCH 1998/3044] Rename normalize_constraint -> to_bounded_expression --- pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp | 2 +- pyomo/contrib/appsi/cmodel/src/lp_writer.cpp | 2 +- pyomo/contrib/appsi/cmodel/src/nl_writer.cpp | 2 +- pyomo/core/base/constraint.py | 45 +++++++++++++++---- pyomo/core/kernel/constraint.py | 2 +- pyomo/gdp/plugins/bilinear.py | 2 +- pyomo/gdp/plugins/cuttingplane.py | 4 +- .../plugins/solvers/persistent_solver.py | 2 +- 8 files changed, 44 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp b/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp index 708cfd9e073..ca865d429e2 100644 --- a/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp +++ b/pyomo/contrib/appsi/cmodel/src/fbbt_model.cpp @@ -205,7 +205,7 @@ void process_fbbt_constraints(FBBTModel *model, PyomoExprTypes &expr_types, py::handle con_body; for (py::handle c : cons) { - lower_body_upper = c.attr("normalize_constraint")(); + lower_body_upper = c.attr("to_bounded_expression")(); con_lb = lower_body_upper[0]; con_body = lower_body_upper[1]; con_ub = lower_body_upper[2]; diff --git a/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp b/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp index 996bb34f564..f33060ee523 100644 --- a/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp +++ b/pyomo/contrib/appsi/cmodel/src/lp_writer.cpp @@ -289,7 +289,7 @@ void process_lp_constraints(py::list cons, py::object writer) { py::object nonlinear_expr; PyomoExprTypes expr_types = PyomoExprTypes(); for (py::handle c : cons) { - lower_body_upper = c.attr("normalize_constraint")(); + lower_body_upper = c.attr("to_bounded_expression")(); cname = getSymbol(c, labeler); repn = generate_standard_repn( lower_body_upper[1], "compute_values"_a = false, "quadratic"_a = true); diff --git a/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp b/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp index 477bdd87aee..854262496ea 100644 --- a/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp +++ b/pyomo/contrib/appsi/cmodel/src/nl_writer.cpp @@ -527,7 +527,7 @@ void process_nl_constraints(NLWriter *nl_writer, PyomoExprTypes &expr_types, py::handle repn_nonlinear_expr; for (py::handle c : cons) { - lower_body_upper = c.attr("normalize_constraint")(); + lower_body_upper = c.attr("to_bounded_expression")(); repn = generate_standard_repn( lower_body_upper[1], "compute_values"_a = false, "quadratic"_a = false); _const = appsi_expr_from_pyomo_expr(repn.attr("constant"), var_map, diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index defaea99dff..5a9d1da5af1 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -174,12 +174,35 @@ def __init__(self, expr=None, component=None): def __call__(self, exception=True): """Compute the value of the body of this constraint.""" - body = self.normalize_constraint()[1] + body = self.to_bounded_expression()[1] if body.__class__ not in native_numeric_types: body = value(self.body, exception=exception) return body - def normalize_constraint(self): + def to_bounded_expression(self): + """Convert this constraint to a tuple of 3 expressions (lb, body, ub) + + This method "standardizes" the expression into a 3-tuple of + expressions: (`lower_bound`, `body`, `upper_bound`). Upon + conversion, `lower_bound` and `upper_bound` are guaranteed to be + `None`, numeric constants, or fixed (not necessarily constant) + expressions. + + Note + ---- + As this method operates on the *current state* of the + expression, the any required expression manipulations (and by + extension, the result) can change after fixing / unfixing + :py:class:`Var` objects. + + Raises + ------ + + ValueError: Raised if the expression cannot be mapped to this + form (i.e., :py:class:`RangedExpression` constraints with + variable lower of upper bounds. + + """ expr = self._expr if expr.__class__ is RangedExpression: lb, body, ub = ans = expr.args @@ -217,8 +240,12 @@ def normalize_constraint(self): def body(self): """Access the body of a constraint expression.""" try: - ans = self.normalize_constraint()[1] + ans = self.to_bounded_expression()[1] except ValueError: + # It is possible that the expression is not currently valid + # (i.e., a ranged expression with a non-fixed bound). We + # will catch that exception here and - if this actually *is* + # a RangedExpression - return the body. if self._expr.__class__ is RangedExpression: _, ans, _ = self._expr.args else: @@ -229,14 +256,14 @@ def body(self): # # [JDS 6/2024: it would be nice to remove this behavior, # although possibly unnecessary, as people should use - # normalize_constraint() instead] + # to_bounded_expression() instead] return as_numeric(ans) return ans @property def lower(self): """Access the lower bound of a constraint expression.""" - ans = self.normalize_constraint()[0] + ans = self.to_bounded_expression()[0] if ans.__class__ in native_types and ans is not None: # Historically, constraint.lower was guaranteed to return a type # derived from Pyomo NumericValue (or None). Replicate that @@ -250,7 +277,7 @@ def lower(self): @property def upper(self): """Access the upper bound of a constraint expression.""" - ans = self.normalize_constraint()[2] + ans = self.to_bounded_expression()[2] if ans.__class__ in native_types and ans is not None: # Historically, constraint.upper was guaranteed to return a type # derived from Pyomo NumericValue (or None). Replicate that @@ -264,7 +291,7 @@ def upper(self): @property def lb(self): """Access the value of the lower bound of a constraint expression.""" - bound = self.normalize_constraint()[0] + bound = self.to_bounded_expression()[0] if bound is None: return None if bound.__class__ not in native_numeric_types: @@ -282,7 +309,7 @@ def lb(self): @property def ub(self): """Access the value of the upper bound of a constraint expression.""" - bound = self.normalize_constraint()[2] + bound = self.to_bounded_expression()[2] if bound is None: return None if bound.__class__ not in native_numeric_types: @@ -824,7 +851,7 @@ class SimpleConstraint(metaclass=RenamedClass): { 'add', 'set_value', - 'normalize_constraint', + 'to_bounded_expression', 'body', 'lower', 'upper', diff --git a/pyomo/core/kernel/constraint.py b/pyomo/core/kernel/constraint.py index 6b8c4c619f5..fe8eb8b2c1f 100644 --- a/pyomo/core/kernel/constraint.py +++ b/pyomo/core/kernel/constraint.py @@ -177,7 +177,7 @@ class _MutableBoundsConstraintMixin(object): # Define some of the IConstraint abstract methods # - def normalize_constraint(self): + def to_bounded_expression(self): return self.lower, self.body, self.upper @property diff --git a/pyomo/gdp/plugins/bilinear.py b/pyomo/gdp/plugins/bilinear.py index 70b6e83b52f..bc91836ea9c 100644 --- a/pyomo/gdp/plugins/bilinear.py +++ b/pyomo/gdp/plugins/bilinear.py @@ -77,7 +77,7 @@ def _transformBlock(self, block, instance): for component in block.component_data_objects( Constraint, active=True, descend_into=False ): - lb, body, ub = component.normalize_constraint() + lb, body, ub = component.to_bounded_expression() expr = self._transformExpression(body, instance) instance.bilinear_data_.c_body[id(component)] = body component.set_value((lb, expr, ub)) diff --git a/pyomo/gdp/plugins/cuttingplane.py b/pyomo/gdp/plugins/cuttingplane.py index a757f23c826..4cef098eba9 100644 --- a/pyomo/gdp/plugins/cuttingplane.py +++ b/pyomo/gdp/plugins/cuttingplane.py @@ -400,7 +400,7 @@ def back_off_constraint_with_calculated_cut_violation( val = value(transBlock_rHull.infeasibility_objective) - TOL if val <= 0: logger.info("\tBacking off cut by %s" % val) - lb, body, ub = cut.normalize_constraint() + lb, body, ub = cut.to_bounded_expression() cut.set_value((lb, body + abs(val), ub)) # else there is nothing to do: restore the objective transBlock_rHull.del_component(transBlock_rHull.infeasibility_objective) @@ -425,7 +425,7 @@ def back_off_constraint_by_fixed_tolerance( this callback TOL: An absolute tolerance to be added to make cut more conservative. """ - lb, body, ub = cut.normalize_constraint() + lb, body, ub = cut.to_bounded_expression() cut.set_value((lb, body + TOL, ub)) diff --git a/pyomo/solvers/plugins/solvers/persistent_solver.py b/pyomo/solvers/plugins/solvers/persistent_solver.py index ef96bfa339f..ef883fe5496 100644 --- a/pyomo/solvers/plugins/solvers/persistent_solver.py +++ b/pyomo/solvers/plugins/solvers/persistent_solver.py @@ -262,7 +262,7 @@ def _add_and_collect_column_data(self, var, obj_coef, constraints, coefficients) coeff_list = list() constr_list = list() for val, c in zip(coefficients, constraints): - lb, body, ub = c.normalize_constraint() + lb, body, ub = c.to_bounded_expression() body += val * var c.set_value((lb, body, ub)) self._vars_referenced_by_con[c].add(var) From 2529557649b39abd0e963632f0cb608a2a2a6627 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 24 Jul 2024 11:23:51 -0600 Subject: [PATCH 1999/3044] NFC: fix comment typo --- pyomo/contrib/fbbt/tests/test_fbbt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/fbbt/tests/test_fbbt.py b/pyomo/contrib/fbbt/tests/test_fbbt.py index ff1cc8a5cfb..83e69233bb5 100644 --- a/pyomo/contrib/fbbt/tests/test_fbbt.py +++ b/pyomo/contrib/fbbt/tests/test_fbbt.py @@ -1339,7 +1339,7 @@ def setUp(self) -> None: def test_ranged_expression(self): # The python version of FBBT is slightly more flexible than # APPSI's cmodel (it allows - and correctly handles - - # RangedExpressions with variable lower / upper bounds. If we + # RangedExpressions with variable lower / upper bounds). If we # ever port that functionality into APPSI, then this test can be # moved into the base class. m = pyo.ConcreteModel() From 070ab312863820f0cf182bcff340180e2b4e16d8 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:25:12 -0400 Subject: [PATCH 2000/3044] Add experiment and result json file for examples --- pyomo/contrib/doe/examples/result.json | 1 + pyomo/contrib/doe/experiment.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 pyomo/contrib/doe/examples/result.json create mode 100644 pyomo/contrib/doe/experiment.py diff --git a/pyomo/contrib/doe/examples/result.json b/pyomo/contrib/doe/examples/result.json new file mode 100644 index 00000000000..7e1b1a79a1b --- /dev/null +++ b/pyomo/contrib/doe/examples/result.json @@ -0,0 +1 @@ +{"CA0": 5.0, "CA_bounds": [1.0, 5.0], "CB0": 0.0, "CC0": 0.0, "t_range": [0, 1], "control_points": {"0": 500, "0.125": 300, "0.25": 300, "0.375": 300, "0.5": 300, "0.625": 300, "0.75": 300, "0.875": 300, "1": 300}, "T_bounds": [300, 700], "A1": 84.79, "A2": 371.72, "E1": 7.78, "E2": 15.05} \ No newline at end of file diff --git a/pyomo/contrib/doe/experiment.py b/pyomo/contrib/doe/experiment.py new file mode 100644 index 00000000000..d75b20e36e1 --- /dev/null +++ b/pyomo/contrib/doe/experiment.py @@ -0,0 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +class Experiment(object): + def __init__(self): + self.model = None + + def get_labeled_model(self): + raise NotImplementedError( + "Derived experiment class failed to implement get_labeled_model" + ) \ No newline at end of file From 45b7f87a7d22a820c3ebd9d8459a5c08c8a593e0 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:28:21 -0400 Subject: [PATCH 2001/3044] Add Pyomo disclaimer and clean examples codes --- pyomo/contrib/doe/__init__.py | 1 + pyomo/contrib/doe/examples/__init__.py | 10 +++++ .../examples/reactor_compute_factorial_FIM.py | 13 +++++- pyomo/contrib/doe/examples/reactor_example.py | 12 +++++- .../doe/examples/reactor_experiment.py | 40 +++++++++++-------- 5 files changed, 57 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index cbb1ee50f56..fe71b7f5920 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -18,3 +18,4 @@ from .tests import experiment_class_example, experiment_class_example_flags from .utils import rescale_FIM from .examples import reactor_experiment, reactor_example, reactor_compute_factorial_FIM +from .experiment import Experiment diff --git a/pyomo/contrib/doe/examples/__init__.py b/pyomo/contrib/doe/examples/__init__.py index e69de29bb2d..dcaaeb3bde2 100644 --- a/pyomo/contrib/doe/examples/__init__.py +++ b/pyomo/contrib/doe/examples/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ \ No newline at end of file diff --git a/pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py b/pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py index b482fc319f0..c7f10d115b3 100644 --- a/pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py +++ b/pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py @@ -1,3 +1,13 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ from pyomo.common.dependencies import numpy as np from pyomo.contrib.doe.examples.reactor_experiment import ReactorExperiment @@ -18,7 +28,7 @@ def run_reactor_doe(): f = open(file_path) data_ex = json.load(f) - # Process control data points into correct format for reactor experiment + # Put temperature control time points into correct format for reactor experiment data_ex["control_points"] = { float(k): v for k, v in data_ex["control_points"].items() } @@ -81,6 +91,7 @@ def run_reactor_doe(): xlabel_text="Concentration of A (M)", ylabel_text="Initial Temperature (K)", figure_file_name="example_reactor_compute_FIM", + log_scale=False, ) diff --git a/pyomo/contrib/doe/examples/reactor_example.py b/pyomo/contrib/doe/examples/reactor_example.py index f673d4a3750..2f7cd7b43e3 100644 --- a/pyomo/contrib/doe/examples/reactor_example.py +++ b/pyomo/contrib/doe/examples/reactor_example.py @@ -1,3 +1,13 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ from pyomo.common.dependencies import numpy as np from pyomo.contrib.doe.examples.reactor_experiment import ReactorExperiment @@ -18,7 +28,7 @@ def run_reactor_doe(): f = open(file_path) data_ex = json.load(f) - # Process control data points into correct format for reactor experiment + # Put temperature control time points into correct format for reactor experiment data_ex["control_points"] = { float(k): v for k, v in data_ex["control_points"].items() } diff --git a/pyomo/contrib/doe/examples/reactor_experiment.py b/pyomo/contrib/doe/examples/reactor_experiment.py index 2909727751c..32d2342751f 100644 --- a/pyomo/contrib/doe/examples/reactor_experiment.py +++ b/pyomo/contrib/doe/examples/reactor_experiment.py @@ -1,21 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ # === Required imports === import pyomo.environ as pyo from pyomo.dae import ContinuousSet, DerivativeVar, Simulator +from pyomo.contrib.doe.experiment import Experiment # ======================== - - -class Experiment(object): - def __init__(self): - self.model = None - - def get_labeled_model(self): - raise NotImplementedError( - "Derived experiment class failed to implement get_labeled_model" - ) - - -class ReactorExperiment(object): +class ReactorExperiment(Experiment): def __init__(self, data, nfe, ncp): self.data = data self.nfe = nfe @@ -120,15 +119,21 @@ def finalize_model(self): # Unpacking data before simulation control_points = self.data["control_points"] + # Set initial concentration values for the experiment m.CA[0].value = self.data["CA0"] m.CB[0].fix(self.data["CB0"]) + + # Update model time `t` with time range and control time points m.t.update(self.data["t_range"]) m.t.update(control_points) + + # Fix the unknown parameter values m.A1.fix(self.data["A1"]) m.A2.fix(self.data["A2"]) m.E1.fix(self.data["E1"]) m.E2.fix(self.data["E2"]) + # Add upper and lower bounds to the design variable, CA[0] m.CA[0].setlb(self.data["CA_bounds"][0]) m.CA[0].setub(self.data["CA_bounds"][1]) @@ -147,10 +152,11 @@ def finalize_model(self): m.T[t].setub(self.data["T_bounds"][1]) m.T[t] = cv + # Make a constraint that holds temperature constant between control time points @m.Constraint(m.t - control_points) def T_control(m, t): """ - Piecewise constant Temperature between control points + Piecewise constant temperature between control points """ neighbour_t = max(tc for tc in control_points if tc < t) return m.T[t] == m.T[neighbour_t] @@ -166,7 +172,7 @@ def label_experiment(self): """ m = self.model - # Grab measurement labels + # Set measurement labels m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) # Add CA to experiment outputs m.experiment_outputs.update((m.CA[t], None) for t in m.t) @@ -175,7 +181,7 @@ def label_experiment(self): # Add CC to experiment outputs m.experiment_outputs.update((m.CC[t], None) for t in m.t) - # Adding no error for measurements currently + # Adding error for measurement values (assuming no covariance and constant error for all measurements) m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) concentration_error = 1e-2 # Error in concentration measurement # Add measurement error for CA @@ -185,7 +191,7 @@ def label_experiment(self): # Add measurement error for CC m.measurement_error.update((m.CC[t], concentration_error) for t in m.t) - # Grab design variables + # Identify design variables (experiment inputs) for the model m.experiment_inputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) # Add experimental input label for initial concentration m.experiment_inputs.update( From b203356cb5c52f7361e7ed1a9432c6722971aa1b Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:30:23 -0400 Subject: [PATCH 2002/3044] Added Pyomo statement to test files --- pyomo/contrib/doe/tests/experiment_class_example.py | 10 ++++++++++ .../doe/tests/experiment_class_example_flags.py | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/pyomo/contrib/doe/tests/experiment_class_example.py b/pyomo/contrib/doe/tests/experiment_class_example.py index 57bc7b424a1..97ba820880d 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example.py +++ b/pyomo/contrib/doe/tests/experiment_class_example.py @@ -1,3 +1,13 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ # === Required imports === import pyomo.environ as pyo from pyomo.dae import ContinuousSet, DerivativeVar, Simulator diff --git a/pyomo/contrib/doe/tests/experiment_class_example_flags.py b/pyomo/contrib/doe/tests/experiment_class_example_flags.py index 2a282023a0e..adffbe40f16 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example_flags.py +++ b/pyomo/contrib/doe/tests/experiment_class_example_flags.py @@ -1,3 +1,13 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ # === Required imports === import pyomo.environ as pyo from pyomo.dae import ContinuousSet, DerivativeVar, Simulator From 52557f2490de6232297ce54a3f9f2d706d797f00 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:31:10 -0400 Subject: [PATCH 2003/3044] Add more Pyomo statements to test files --- pyomo/contrib/doe/tests/test_doe_build.py | 10 ++++++++++ pyomo/contrib/doe/tests/test_doe_errors.py | 10 ++++++++++ pyomo/contrib/doe/tests/test_doe_solve.py | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 4c1541f088c..c38bdd54386 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -1,3 +1,13 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ from pyomo.common.dependencies import ( numpy as np, numpy_available, diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 00336ff0420..ec2024ac5a6 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -1,3 +1,13 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ from pyomo.common.dependencies import ( numpy as np, numpy_available, diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 92787ee8abb..bdef07c7fed 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -1,3 +1,13 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ from pyomo.common.dependencies import ( numpy as np, numpy_available, From 24c4d6ea8d33710b77cd1bdda46abea17fa14242 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:41:40 -0400 Subject: [PATCH 2004/3044] Updated test files to use experiment class, Ran Black --- pyomo/contrib/doe/examples/__init__.py | 2 +- .../doe/examples/reactor_experiment.py | 4 ++- pyomo/contrib/doe/experiment.py | 2 +- .../doe/tests/experiment_class_example.py | 30 ++----------------- .../tests/experiment_class_example_flags.py | 14 ++------- 5 files changed, 11 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/doe/examples/__init__.py b/pyomo/contrib/doe/examples/__init__.py index dcaaeb3bde2..a4a626013c4 100644 --- a/pyomo/contrib/doe/examples/__init__.py +++ b/pyomo/contrib/doe/examples/__init__.py @@ -7,4 +7,4 @@ # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain # rights in this software. # This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ \ No newline at end of file +# ___________________________________________________________________________ diff --git a/pyomo/contrib/doe/examples/reactor_experiment.py b/pyomo/contrib/doe/examples/reactor_experiment.py index 32d2342751f..68c33a4ca2b 100644 --- a/pyomo/contrib/doe/examples/reactor_experiment.py +++ b/pyomo/contrib/doe/examples/reactor_experiment.py @@ -13,6 +13,8 @@ from pyomo.dae import ContinuousSet, DerivativeVar, Simulator from pyomo.contrib.doe.experiment import Experiment + + # ======================== class ReactorExperiment(Experiment): def __init__(self, data, nfe, ncp): @@ -122,7 +124,7 @@ def finalize_model(self): # Set initial concentration values for the experiment m.CA[0].value = self.data["CA0"] m.CB[0].fix(self.data["CB0"]) - + # Update model time `t` with time range and control time points m.t.update(self.data["t_range"]) m.t.update(control_points) diff --git a/pyomo/contrib/doe/experiment.py b/pyomo/contrib/doe/experiment.py index d75b20e36e1..17a51cf667a 100644 --- a/pyomo/contrib/doe/experiment.py +++ b/pyomo/contrib/doe/experiment.py @@ -15,4 +15,4 @@ def __init__(self): def get_labeled_model(self): raise NotImplementedError( "Derived experiment class failed to implement get_labeled_model" - ) \ No newline at end of file + ) diff --git a/pyomo/contrib/doe/tests/experiment_class_example.py b/pyomo/contrib/doe/tests/experiment_class_example.py index 97ba820880d..b4cdd13b416 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example.py +++ b/pyomo/contrib/doe/tests/experiment_class_example.py @@ -12,6 +12,8 @@ import pyomo.environ as pyo from pyomo.dae import ContinuousSet, DerivativeVar, Simulator +from pyomo.contrib.doe.experiment import Experiment + import itertools import json @@ -44,17 +46,7 @@ def expand_model_components(m, base_components, index_sets): yield val[j] -class Experiment(object): - def __init__(self): - self.model = None - - def get_labeled_model(self): - raise NotImplementedError( - "Derived experiment class failed to implement get_labeled_model" - ) - - -class ReactorExperiment(object): +class ReactorExperiment(Experiment): def __init__(self, data, nfe, ncp): self.data = data self.nfe = nfe @@ -239,19 +231,3 @@ class FullReactorExperiment(ReactorExperiment): def label_experiment(self): m = self.model return self.label_experiment_impl([[m.t_control], [m.t_control], [m.t_control]]) - - -class PartialReactorExperiment(ReactorExperiment): - def label_experiment(self): - """ - Example for annotating (labeling) the model with a - "partial" experiment. - - Arguments - --------- - - """ - m = self.model - return self.label_experiment_impl( - [[m.t_control], [[m.t.last()]], [[m.t.last()]]] - ) diff --git a/pyomo/contrib/doe/tests/experiment_class_example_flags.py b/pyomo/contrib/doe/tests/experiment_class_example_flags.py index adffbe40f16..c014f9f3065 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example_flags.py +++ b/pyomo/contrib/doe/tests/experiment_class_example_flags.py @@ -12,6 +12,8 @@ import pyomo.environ as pyo from pyomo.dae import ContinuousSet, DerivativeVar, Simulator +from pyomo.contrib.doe.experiment import Experiment + import itertools import json @@ -49,17 +51,7 @@ def __init__(self): self.model = None -class Experiment(object): - def __init__(self): - self.model = None - - def get_labeled_model(self): - raise NotImplementedError( - "Derived experiment class failed to implement get_labeled_model" - ) - - -class ReactorExperiment(object): +class ReactorExperiment(Experiment): def __init__(self, data, nfe, ncp): self.data = data self.nfe = nfe From 834088f3e8ab34275b0903829d39594c7ddeb79c Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Wed, 24 Jul 2024 15:41:59 -0400 Subject: [PATCH 2005/3044] Updated documentation with recent changes --- doc/OnlineDocs/contributed_packages/doe/doe.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index 291fe99821e..e8b4d9ca2c7 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -164,7 +164,7 @@ Step 0: Import Pyomo and the Pyomo.DoE module and create an Experiment object >>> import numpy as np .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :lines: 8-23 + :lines: 19-24 Step 1: Define the Pyomo process model ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -172,7 +172,7 @@ Step 1: Define the Pyomo process model The process model for the reaction kinetics problem is shown below. We build the model in without any data or discretization. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :lines: 32-101 + :lines: 33-102 Step 2: Finalize the Pyomo process model ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -180,7 +180,7 @@ Step 2: Finalize the Pyomo process model Here we add data to the model and finalize the discretization. This step is required before the model can be labeled. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :lines: 103-156 + :lines: 104-157 Step 3: Label the important information for model on the DoE object ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -188,7 +188,7 @@ Step 3: Label the important information for model on the DoE object We label the four important groups as defined before. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :lines: 158-202 + :lines: 159-203 Step 4: We give the experiment object a get_labeled_model function ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -196,7 +196,7 @@ Step 4: We give the experiment object a get_labeled_model function This function summarizes the previous 3 steps and is used by `Pyomo.DoE` to build the model to perform optimal experimental design. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :lines: 25-30 + :lines: 26-31 Step 5: Exploratory analysis (Enumeration) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -212,7 +212,7 @@ The function ``compute_FIM_full_factorial`` enumerates over the design space, ea The following code executes the above problem description: .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py - :lines: 14-84 + :lines: 24-95 An example output of the code above, a design exploration for the initial concentration and temperature as experimental design variables with 9 values, produces the four figures summarized below: @@ -227,7 +227,7 @@ Step 6: Performing an optimal experimental design This is an example of running an experimental design to determine an optimal experiment for the reactor example. We utilize the determinant as the objective. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_example.py - :lines: 14-80 + :lines: 24-90 When run, the optimal design is an initial concentration of 5.0 mol/L and an initial temperature of 494 K with all other temperatures being 300 K. The corresponding log-10 determinant of the FIM is 13.75 From 5956843fca7353bb63f5ef0e7cba969f6eb4c5c4 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 24 Jul 2024 16:00:45 -0600 Subject: [PATCH 2006/3044] Remove unused imports --- .../piecewise/transform/incremental.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 59c9039fdc8..fb761c00ebb 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -9,7 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) @@ -17,26 +16,18 @@ from pyomo.core import ( Constraint, Binary, - NonNegativeIntegers, - Suffix, Var, RangeSet, Param, ) from pyomo.core.base import TransformationFactory -from pyomo.gdp import Disjunct, Disjunction -from pyomo.common.errors import DeveloperError -from pyomo.core.expr.visitor import SimpleExpressionVisitor -from pyomo.core.expr.current import identify_components -from math import ceil, log2 -import logging @TransformationFactory.register( "contrib.piecewise.incremental", doc=""" The incremental MIP formulation of a piecewise-linear function, as described - by [1]. To work in the multivariate case, the underlying triangulation must + by [1]. To work in the multivariate case, the underlying triangulation must satisfy these properties: (1) The simplices are ordered T_1, ..., T_N such that T_i has nonempty intersection with T_{i+1}. It doesn't have to be a whole face; just a vertex is enough. @@ -98,8 +89,8 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc num_simplices = len(simplices) transBlock.simplex_indices = RangeSet(0, num_simplices - 1) transBlock.simplex_indices_except_last = RangeSet(0, num_simplices - 2) - # Assumption: the simplices are really simplices and all have the same number of points, - # which is dimension + 1 + # Assumption: the simplices are really simplices and all have the same number of + # points, which is dimension + 1 transBlock.simplex_point_indices = RangeSet(0, dimension) transBlock.nonzero_simplex_point_indices = RangeSet(1, dimension) transBlock.last_simplex_point_index = Param(initialize=dimension) @@ -141,8 +132,8 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc transBlock.simplex_indices_except_last, domain=Binary ) - # If the delta for the final point in simplex i is not one, y_i must be zero. That is, - # y_i is one for and only for simplices that are completely "used" + # If the delta for the final point in simplex i is not one, y_i must be zero. + # That is, y_i is one for and only for simplices that are completely "used" @transBlock.Constraint(transBlock.simplex_indices_except_last) def y_below_delta(m, i): return ( From 79369ad1826b3299cf867d9844f8fe7ea9b1745a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 24 Jul 2024 16:16:33 -0600 Subject: [PATCH 2007/3044] Add copyright notices and expand comment --- ...generate_ordered_3d_j1_triangulation_data.py | 11 +++++++++++ .../ordered_3d_j1_triangulation_data.py | 17 ++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py index 93a087d883f..58c68051cdb 100644 --- a/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py +++ b/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import networkx as nx import itertools diff --git a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py index a4e921f42d2..631b0b3d4ef 100644 --- a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py +++ b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py @@ -1,4 +1,19 @@ -# Generated using generate_ordered_3d_j1_triangulation_data.py +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# This file was generated using generate_ordered_3d_j1_triangulation_data.py +# Data format: Keys are a pair of simplices specified as the direction they are facing, +# as a standard unit vector or negative of one, and a tag, 1 or 2, disambiguating which +# of the two simplices considered is used. Values are a list of simplices given as +# (sign_vector, permutation) pairs. hamiltonian_paths = { (((-1, 0, 0), 1), ((0, -1, 0), 1)): [ ((-1, -1, -1), (1, 2, 3)), From c20e41a6f38393a29b880dfec350a344d5b83125 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 24 Jul 2024 17:30:06 -0600 Subject: [PATCH 2008/3044] Change PiecewiseLinearFunction `triangulation` argument and remove override argument --- .../piecewise/piecewise_linear_function.py | 79 +++++++++++-------- .../piecewise/tests/test_incremental.py | 3 +- .../tests/test_piecewise_linear_function.py | 22 ------ 3 files changed, 47 insertions(+), 57 deletions(-) diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 7edba3d41d2..e1374a78668 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -24,7 +24,7 @@ get_ordered_j1_triangulation, Triangulation, ) -from pyomo.core import Any, NonNegativeIntegers, value, Var +from pyomo.core import Any, NonNegativeIntegers, value from pyomo.core.base.block import BlockData, Block from pyomo.core.base.component import ModelComponentFactory from pyomo.core.base.expression import Expression @@ -235,6 +235,19 @@ class PiecewiseLinearFunction(Block): expression for a linear function of the arguments. tabular_data: A dictionary mapping values of the nonlinear function to points in the domain + triangulation (optional): An enum value of type Triangulation specifying + how Pyomo should triangulate the function domain, or None. Behavior + depends on how this piecewise-linear function is constructed: + when constructed using methods (1) or (4) above, valid arguments + are the members of Triangulation except Unknown or AssumeValid, + and Pyomo will use that method to triangulate the domain and to tag + the resulting PWLF. If no argument or None is passed, the default + is Triangulation.Delaunay. When constructed using methods (2) or (3) + above, valid arguments are only Triangulation.Unknown and + Triangulation.AssumeValid. Pyomo will tag the constructed PWLF + as specified, trusting the user in the case of AssumeValid. + When no argument or None is passed, the default is + Triangulation.Unknown """ _ComponentDataClass = PiecewiseLinearFunctionData @@ -261,8 +274,7 @@ def __init__(self, *args, **kwargs): _linear_functions = kwargs.pop('linear_functions', None) _tabular_data_arg = kwargs.pop('tabular_data', None) _tabular_data_rule_arg = kwargs.pop('tabular_data_rule', None) - _triangulation_rule_arg = kwargs.pop('triangulation', Triangulation.Delaunay) - _triangulation_override_rule_arg = kwargs.pop('override_triangulation', None) + _triangulation_rule_arg = kwargs.pop('triangulation', None) kwargs.setdefault('ctype', PiecewiseLinearFunction) Block.__init__(self, *args, **kwargs) @@ -284,9 +296,6 @@ def __init__(self, *args, **kwargs): self._triangulation_rule = Initializer( _triangulation_rule_arg, treat_sequences_as_mappings=False ) - self._triangulation_override_rule = Initializer( - _triangulation_override_rule_arg, treat_sequences_as_mappings=False - ) def _get_dimension_from_points(self, points): if len(points) < 1: @@ -305,7 +314,13 @@ def _get_dimension_from_points(self, points): def _construct_simplices_from_multivariate_points( self, obj, parent, points, dimension ): - tri = self._triangulation_rule(parent, obj._index) + if self._triangulation_rule is None: + tri = Triangulation.Delaunay + else: + tri = self._triangulation_rule(parent, obj._index) + if tri is None: + tri = Triangulation.Delaunay + if tri == Triangulation.Delaunay: try: triangulation = spatial.Delaunay(points) @@ -321,7 +336,7 @@ def _construct_simplices_from_multivariate_points( obj._triangulation = tri else: raise ValueError( - "Unrecognized triangulation specified for '%s': %s" % (obj, tri) + "Invalid or unrecognized triangulation specified for '%s': %s" % (obj, tri) ) # Get the points for the triangulation because they might not all be @@ -341,7 +356,7 @@ def _construct_simplices_from_multivariate_points( # checking the determinant because matrix_rank will by default calculate a # tolerance based on the input to account for numerical errors in the # SVD computation. - if tri != Triangulation.Delaunay: + if tri in (Triangulation.J1, Triangulation.OrderedJ1): # Note: do not sort vertices from OrderedJ1, or it will break. # Non-ordered J1 is already sorted, though it doesn't matter. # Also, we don't need to check for degeneracy with simplices we @@ -365,6 +380,24 @@ def _construct_simplices_from_multivariate_points( "%s from the triangulation." % pt[0] ) + # Call when constructing from simplices to allow use of AssumeValid and + # ensure the user is not making mistakes + def _check_and_set_triangulation_from_user(self, parent, obj): + if self._triangulation_rule is None: + tri = None + else: + tri = self._triangulation_rule(parent, obj._index) + if tri is None or tri == Triangulation.Unknown: + obj._triangulation = Triangulation.Unknown + elif tri == Triangulation.AssumeValid: + obj._triangulation = Triangulation.AssumeValid + else: + raise ValueError( + f"Invalid or unrecognized triangulation tag specified for {obj} when" + f" giving simplices: {tri}. Valid arguments when giving simplices are" + " Triangulation.Unknown and Triangulation.AssumeValid." + ) + def _construct_one_dimensional_simplices_from_points(self, obj, points): points.sort() obj._simplices = [] @@ -401,7 +434,7 @@ def _construct_from_univariate_function_and_segments( ): # We can trust they are nicely ordered if we made them, otherwise anything goes. if segments_are_user_defined: - obj._triangulation = Triangulation.Unknown + self._check_and_set_triangulation_from_user(parent, obj) else: obj._triangulation = Triangulation.AssumeValid @@ -439,9 +472,9 @@ def _construct_from_function_and_simplices( ) # If we triangulated, then this tag was already set. If they provided it, - # then it should be unknown. + # then check their arguments and set. if simplices_are_user_defined: - obj._triangulation = Triangulation.Unknown + self._check_and_set_triangulation_from_user(parent, obj) # evaluate the function at each of the points and form the homogeneous # system of equations @@ -494,7 +527,7 @@ def _construct_from_linear_functions_and_simplices( # have been called. obj._get_simplices_from_arg(self._simplices_rule(parent, obj._index)) obj._linear_functions = [f for f in self._linear_funcs_rule(parent, obj._index)] - obj._triangulation = Triangulation.Unknown + self._check_and_set_triangulation_from_user(parent, obj) return obj @_define_handler(_handlers, False, False, False, False, True) @@ -543,17 +576,6 @@ def _getitem_when_not_present(self, index): elif self._func is not None: nonlinear_function = self._func - # If the user asked for a specific triangulation but passed simplices, - # warn them that we're going to use the simplices and ignore the triangulation. - if self._simplices_rule is not None: - tri = self._triangulation_rule(parent, obj._index) - if tri not in (None, Triangulation.Delaunay): - logger.warn( - f"Non-default triangulation request {tri} was ignored because the " - "simplices were provided. If you meant to override the tag, use " - "`override_triangulation` instead." - ) - handler = self._handlers.get( ( nonlinear_function is not None, @@ -575,15 +597,6 @@ def _getitem_when_not_present(self, index): ) obj = handler(self, obj, parent, nonlinear_function) - # If the user wanted to override the triangulation tag, do it after we - # are finished setting it ourselves. - if self._triangulation_override_rule is not None: - triangulation_override = self._triangulation_override_rule( - parent, obj._index - ) - if triangulation_override is not None: - obj._triangulation = triangulation_override - return obj diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index a3071982e6f..cfeab53ef0b 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -14,7 +14,6 @@ from pyomo.contrib.piecewise.triangulations import Triangulation from pyomo.core.base import TransformationFactory from pyomo.core.expr.compare import assertExpressionsEqual -from pyomo.gdp import Disjunct, Disjunction from pyomo.environ import ( Constraint, SolverFactory, @@ -236,7 +235,7 @@ def g2(x1, x2): m.pw_paraboloid = PiecewiseLinearFunction( simplices=simplices, linear_functions=[g1, g1, g2, g2], - override_triangulation=Triangulation.AssumeValid, + triangulation=Triangulation.AssumeValid, ) m.paraboloid_expr = m.pw_paraboloid(m.x1, m.x2) diff --git a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py index 022c82cb759..a49519ae25e 100644 --- a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py @@ -340,28 +340,6 @@ def test_pw_linear_approx_of_paraboloid_j1(self): self.assertEqual(len(m.pw._simplices), 8) self.assertEqual(m.pw.triangulation, Triangulation.OrderedJ1) - @unittest.skipUnless(numpy_available, "numpy is not available") - def test_triangulation_override(self): - m = self.make_model() - m.pw = PiecewiseLinearFunction( - points=[ - (0, 1), - (0, 4), - (0, 7), - (3, 1), - (3, 4), - (3, 7), - (4, 1), - (4, 4), - (4, 7), - ], - function=m.g, - triangulation=Triangulation.OrderedJ1, - override_triangulation=Triangulation.AssumeValid, - ) - self.assertEqual(len(m.pw._simplices), 8) - self.assertEqual(m.pw.triangulation, Triangulation.AssumeValid) - @unittest.skipUnless(scipy_available, "scipy is not available") def test_pw_linear_approx_tabular_data(self): m = self.make_model() From caa8cb58bb9040d1da347539e5a8e45df295a31b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 25 Jul 2024 08:16:31 -0600 Subject: [PATCH 2009/3044] Allow maingo_solvermodel to be imported without maingopy --- pyomo/contrib/appsi/solvers/maingo.py | 40 +++++-------------- .../appsi/solvers/maingo_solvermodel.py | 12 +----- 2 files changed, 13 insertions(+), 39 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index e52130061f7..c5860b42ce7 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -57,33 +57,13 @@ from pyomo.repn.util import valid_expr_ctypes_minlp -def _import_SolverModel(): - try: - from . import maingo_solvermodel - except ImportError: - raise - return maingo_solvermodel - - -maingo_solvermodel, solvermodel_available = attempt_import( - "maingo_solvermodel", importer=_import_SolverModel -) - -MaingoVar = namedtuple("MaingoVar", "type name lb ub init") - logger = logging.getLogger(__name__) - - -def _import_maingopy(): - try: - import maingopy - except ImportError: - MAiNGO._available = MAiNGO.Availability.NotFound - raise - return maingopy - - -maingopy, maingopy_available = attempt_import("maingopy", importer=_import_maingopy) +MaingoVar = namedtuple("MaingoVar", "type name lb ub init") +maingopy, maingopy_available = attempt_import("maingopy") +# Note that importing maingo_solvermodel will trigger the import of +# maingopy, so we defer that import using attempt_import (which will +# always succeed, even if maingopy is not available) +maingo_solvermodel = attempt_import("pyomo.contrib.appsi.solvers.maingo_solvermodel")[0] class MAiNGOConfig(MIPSolverConfig): @@ -185,9 +165,11 @@ def __init__(self, only_child_vars=False): self._last_results_object: Optional[MAiNGOResults] = None def available(self): - if not maingopy_available: - return self.Availability.NotFound - self._available = True + if self._available is None: + if maingopy_available: + MAiNGO._available = True + else: + MAiNGO._available = MAiNGO.Availability.NotFound return self._available def version(self): diff --git a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py index ca746c4a9b7..b12a386284c 100644 --- a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py +++ b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py @@ -28,15 +28,7 @@ from pyomo.repn.util import valid_expr_ctypes_minlp -def _import_maingopy(): - try: - import maingopy - except ImportError: - raise - return maingopy - - -maingopy, maingopy_available = attempt_import("maingopy", importer=_import_maingopy) +maingopy, maingopy_available = attempt_import("maingopy") _plusMinusOne = {1, -1} @@ -219,7 +211,7 @@ def _linear_to_maingo(self, node): return sum(values) -class SolverModel(maingopy.MAiNGOmodel): +class SolverModel(maingopy.MAiNGOmodel if maingopy_available else object): def __init__(self, var_list, objective, con_list, idmap, logger): maingopy.MAiNGOmodel.__init__(self) self._var_list = var_list From 8bc8d8663a6b7ee7f5131a1792826d8b94d32b9c Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 25 Jul 2024 09:21:53 -0600 Subject: [PATCH 2010/3044] Deduplicate test file --- pyomo/contrib/piecewise/tests/models.py | 22 +++-- .../piecewise/tests/test_incremental.py | 96 +++++-------------- 2 files changed, 38 insertions(+), 80 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/models.py b/pyomo/contrib/piecewise/tests/models.py index 1a8bef04ad7..e209b1ac879 100644 --- a/pyomo/contrib/piecewise/tests/models.py +++ b/pyomo/contrib/piecewise/tests/models.py @@ -9,11 +9,18 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.contrib.piecewise import PiecewiseLinearFunction +from pyomo.contrib.piecewise import PiecewiseLinearFunction, Triangulation from pyomo.environ import ConcreteModel, Constraint, log, Objective, Var +default_simplices = [ + [(0, 1), (0, 4), (3, 4)], + [(0, 1), (3, 4), (3, 1)], + [(3, 4), (3, 7), (0, 7)], + [(0, 7), (0, 4), (3, 4)], +] -def make_log_x_model(): + +def make_log_x_model(simplices=default_simplices): m = ConcreteModel() m.x = Var(bounds=(1, 10)) m.pw_log = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=log) @@ -50,14 +57,11 @@ def g2(x1, x2): return 3 * x1 + 11 * x2 - 28 m.g2 = g2 - simplices = [ - [(0, 1), (0, 4), (3, 4)], - [(0, 1), (3, 4), (3, 1)], - [(3, 4), (3, 7), (0, 7)], - [(0, 7), (0, 4), (3, 4)], - ] + m.pw_paraboloid = PiecewiseLinearFunction( - simplices=simplices, linear_functions=[g1, g1, g2, g2] + simplices=simplices, + linear_functions=[g1, g1, g2, g2], + triangulation=Triangulation.AssumeValid, ) m.paraboloid_expr = m.pw_paraboloid(m.x1, m.x2) diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index cfeab53ef0b..8ca43df20f3 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -11,6 +11,7 @@ import pyomo.common.unittest as unittest import pyomo.contrib.piecewise.tests.common_tests as ct +from pyomo.contrib.piecewise.tests.models import make_log_x_model from pyomo.contrib.piecewise.triangulations import Triangulation from pyomo.core.base import TransformationFactory from pyomo.core.expr.compare import assertExpressionsEqual @@ -132,42 +133,59 @@ def check_pw_paraboloid(self, m): self.assertIsInstance(paraboloid_block.set_substitute, Constraint) self.assertEqual(len(paraboloid_block.set_substitute), 1) + ordered_simplices = [ + [(0, 1), (3, 1), (3, 4)], + [(3, 4), (0, 1), (0, 4)], + [(0, 4), (0, 7), (3, 4)], + [(3, 4), (3, 7), (0, 7)], + ] + # Test methods using the common_tests.py code. def test_transformation_do_not_descend(self): ct.check_transformation_do_not_descend( - self, 'contrib.piecewise.incremental', make_log_x_model_ordered() + self, + 'contrib.piecewise.incremental', + make_log_x_model(simplices=self.ordered_simplices), ) def test_transformation_PiecewiseLinearFunction_targets(self): ct.check_transformation_PiecewiseLinearFunction_targets( - self, 'contrib.piecewise.incremental', make_log_x_model_ordered() + self, + 'contrib.piecewise.incremental', + make_log_x_model(simplices=self.ordered_simplices), ) def test_descend_into_expressions(self): ct.check_descend_into_expressions( - self, 'contrib.piecewise.incremental', make_log_x_model_ordered() + self, + 'contrib.piecewise.incremental', + make_log_x_model(simplices=self.ordered_simplices), ) def test_descend_into_expressions_constraint_target(self): ct.check_descend_into_expressions_constraint_target( - self, 'contrib.piecewise.incremental', make_log_x_model_ordered() + self, + 'contrib.piecewise.incremental', + make_log_x_model(simplices=self.ordered_simplices), ) def test_descend_into_expressions_objective_target(self): ct.check_descend_into_expressions_objective_target( - self, 'contrib.piecewise.incremental', make_log_x_model_ordered() + self, + 'contrib.piecewise.incremental', + make_log_x_model(simplices=self.ordered_simplices), ) @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available') @unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license') def test_solve_log_model(self): - m = make_log_x_model_ordered() + m = make_log_x_model(simplices=self.ordered_simplices) TransformationFactory('contrib.piecewise.incremental').apply_to(m) TransformationFactory('gdp.bigm').apply_to(m) SolverFactory('gurobi').solve(m) ct.check_log_x_model_soln(self, m) - # Failed during development when j1 vertex ordering got broken + # Failed during development when ordered j1 vertex ordering got broken @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available') @unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license') def test_solve_product_model(self): @@ -184,67 +202,3 @@ def test_solve_product_model(self): TransformationFactory("contrib.piecewise.incremental").apply_to(m) SolverFactory('gurobi').solve(m) self.assertAlmostEqual(0.45, value(m.obj)) - - -# Make a version of the log_x model with the simplices properly ordered for the -# incremental transform -def make_log_x_model_ordered(): - m = ConcreteModel() - m.x = Var(bounds=(1, 10)) - m.pw_log = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=log) - - # Here are the linear functions, for safe keeping. - def f1(x): - return (log(3) / 2) * x - log(3) / 2 - - m.f1 = f1 - - def f2(x): - return (log(2) / 3) * x + log(3 / 2) - - m.f2 = f2 - - def f3(x): - return (log(5 / 3) / 4) * x + log(6 / ((5 / 3) ** (3 / 2))) - - m.f3 = f3 - - m.log_expr = m.pw_log(m.x) - m.obj = Objective(expr=m.log_expr) - - m.x1 = Var(bounds=(0, 3)) - m.x2 = Var(bounds=(1, 7)) - - ## approximates paraboloid x1**2 + x2**2 - def g1(x1, x2): - return 3 * x1 + 5 * x2 - 4 - - m.g1 = g1 - - def g2(x1, x2): - return 3 * x1 + 11 * x2 - 28 - - m.g2 = g2 - # order for incremental transformation - simplices = [ - [(0, 1), (3, 1), (3, 4)], - [(3, 4), (0, 1), (0, 4)], - [(0, 4), (0, 7), (3, 4)], - [(3, 4), (3, 7), (0, 7)], - ] - m.pw_paraboloid = PiecewiseLinearFunction( - simplices=simplices, - linear_functions=[g1, g1, g2, g2], - triangulation=Triangulation.AssumeValid, - ) - m.paraboloid_expr = m.pw_paraboloid(m.x1, m.x2) - - def c_rule(m, i): - if i == 0: - return m.x >= m.paraboloid_expr - else: - return (1, m.x1, 2) - - m.indexed_c = Constraint([0, 1], rule=c_rule) - - return m From 2371d1bd5747d3d23609ed94c99280cbc7db785f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 25 Jul 2024 09:23:54 -0600 Subject: [PATCH 2011/3044] Remove NLv2 ActiveVisitor singleton --- pyomo/repn/plugins/nl_writer.py | 406 ++++++++++++++--------------- pyomo/repn/tests/ampl/test_nlv2.py | 6 +- 2 files changed, 201 insertions(+), 211 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 8fc82d21d30..bc0c44c93b6 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -122,6 +122,119 @@ ) +def _create_strict_inequality_map(vars_): + vars_['strict_inequality_map'] = { + True: vars_['less_than'], + False: vars_['less_equal'], + (True, True): (vars_['less_than'], vars_['less_than']), + (True, False): (vars_['less_than'], vars_['less_equal']), + (False, True): (vars_['less_equal'], vars_['less_than']), + (False, False): (vars_['less_equal'], vars_['less_equal']), + } + + +class text_nl_debug_template(object): + unary = { + 'log': 'o43\t#log\n', + 'log10': 'o42\t#log10\n', + 'sin': 'o41\t#sin\n', + 'cos': 'o46\t#cos\n', + 'tan': 'o38\t#tan\n', + 'sinh': 'o40\t#sinh\n', + 'cosh': 'o45\t#cosh\n', + 'tanh': 'o37\t#tanh\n', + 'asin': 'o51\t#asin\n', + 'acos': 'o53\t#acos\n', + 'atan': 'o49\t#atan\n', + 'exp': 'o44\t#exp\n', + 'sqrt': 'o39\t#sqrt\n', + 'asinh': 'o50\t#asinh\n', + 'acosh': 'o52\t#acosh\n', + 'atanh': 'o47\t#atanh\n', + 'ceil': 'o14\t#ceil\n', + 'floor': 'o13\t#floor\n', + } + + binary_sum = 'o0\t#+\n' + product = 'o2\t#*\n' + division = 'o3\t# /\n' + pow = 'o5\t#^\n' + abs = 'o15\t# abs\n' + negation = 'o16\t#-\n' + nary_sum = 'o54\t# sumlist\n%d\t# (n)\n' + exprif = 'o35\t# if\n' + and_expr = 'o21\t# and\n' + less_than = 'o22\t# lt\n' + less_equal = 'o23\t# le\n' + equality = 'o24\t# eq\n' + external_fcn = 'f%d %d%s\n' + # NOTE: to support scaling and substitutions, we do NOT include the + # 'v' or the EOL here: + var = '%s' + const = 'n%r\n' + string = 'h%d:%s\n' + monomial = product + const + var.replace('%', '%%') + multiplier = product + const + + _create_strict_inequality_map(vars()) + + +nl_operators = { + 0: (2, operator.add), + 2: (2, operator.mul), + 3: (2, operator.truediv), + 5: (2, operator.pow), + 15: (1, operator.abs), + 16: (1, operator.neg), + 54: (None, lambda *x: sum(x)), + 35: (3, lambda a, b, c: b if a else c), + 21: (2, operator.and_), + 22: (2, operator.lt), + 23: (2, operator.le), + 24: (2, operator.eq), + 43: (1, math.log), + 42: (1, math.log10), + 41: (1, math.sin), + 46: (1, math.cos), + 38: (1, math.tan), + 40: (1, math.sinh), + 45: (1, math.cosh), + 37: (1, math.tanh), + 51: (1, math.asin), + 53: (1, math.acos), + 49: (1, math.atan), + 44: (1, math.exp), + 39: (1, math.sqrt), + 50: (1, math.asinh), + 52: (1, math.acosh), + 47: (1, math.atanh), + 14: (1, math.ceil), + 13: (1, math.floor), +} + + +def _strip_template_comments(vars_, base_): + vars_['unary'] = { + k: v[: v.find('\t#')] + '\n' if v[-1] == '\n' else '' + for k, v in base_.unary.items() + } + for k, v in base_.__dict__.items(): + if type(v) is str and '\t#' in v: + v_lines = v.split('\n') + for i, l in enumerate(v_lines): + comment_start = l.find('\t#') + if comment_start >= 0: + v_lines[i] = l[:comment_start] + vars_[k] = '\n'.join(v_lines) + + +# The "standard" text mode template is the debugging template with the +# comments removed +class text_nl_template(text_nl_debug_template): + _strip_template_comments(vars(), text_nl_debug_template) + _create_strict_inequality_map(vars()) + + # TODO: make a proper base class class NLWriterInfo(object): """Return type for NLWriter.write() @@ -539,10 +652,6 @@ def __init__(self, ostream, rowstream, colstream, config): self.colstream = colstream self.config = config self.symbolic_solver_labels = config.symbolic_solver_labels - if self.symbolic_solver_labels: - self.template = text_nl_debug_template - else: - self.template = text_nl_template self.subexpression_cache = {} self.subexpression_order = None # set to [] later self.external_functions = {} @@ -551,7 +660,6 @@ def __init__(self, ostream, rowstream, colstream, config): self.var_id_to_nl_map = {} self.sorter = FileDeterminism_to_SortComponents(config.file_determinism) self.visitor = AMPLRepnVisitor( - self.template, self.subexpression_cache, self.external_functions, self.var_map, @@ -562,18 +670,15 @@ def __init__(self, ostream, rowstream, colstream, config): ) self.next_V_line_id = 0 self.pause_gc = None + self.template = self.visitor.Result.template def __enter__(self): - assert AMPLRepn.ActiveVisitor is None - AMPLRepn.ActiveVisitor = self.visitor self.pause_gc = PauseGC() self.pause_gc.__enter__() return self def __exit__(self, exc_type, exc_value, tb): self.pause_gc.__exit__(exc_type, exc_value, tb) - assert AMPLRepn.ActiveVisitor is self.visitor - AMPLRepn.ActiveVisitor = None def write(self, model): timing_logger = logging.getLogger('pyomo.common.timing.writer') @@ -1111,7 +1216,7 @@ def write(self, model): # Update any eliminated variables to point to the (potentially # scaled) substituted variables for _id, expr_info in list(eliminated_vars.items()): - nl, args, _ = expr_info.compile_repn(visitor) + nl, args, _ = expr_info.compile_repn() for _i in args: # It is possible that the eliminated variable could # reference another variable that is no longer part of @@ -1133,8 +1238,8 @@ def write(self, model): val = 0 else: val = lb if abs(lb) < abs(ub) else ub - eliminated_vars[_i] = AMPLRepn(val, {}, None) - nl_map[_i] = expr_info.compile_repn(visitor)[0] + eliminated_vars[_i] = visitor.Result(val, {}, None) + nl_map[_i] = expr_info.compile_repn()[0] logger.warning( "presolve identified an underdetermined independent " "linear subsystem that was removed from the model. " @@ -1792,7 +1897,7 @@ def _linear_presolve( a = x = None b, _ = var_bounds[_id] logger.debug("NL presolve: bounds fixed %s := %s", var_map[_id], b) - eliminated_vars[_id] = AMPLRepn(b, {}, None) + eliminated_vars[_id] = self.visitor.Result(b, {}, None) nl_map[_id] = template.const % b elif one_var: con_id, info = one_var.popitem() @@ -1994,9 +2099,7 @@ def _resolve_subexpression_args(self, nl, args): if arg in self.var_id_to_nl_map: final_args.append(self.var_id_to_nl_map[arg]) else: - _nl, _ids, _ = self.subexpression_cache[arg][1].compile_repn( - self.visitor - ) + _nl, _ids, _ = self.subexpression_cache[arg][1].compile_repn() final_args.append(self._resolve_subexpression_args(_nl, _ids)) return nl % tuple(final_args) @@ -2070,7 +2173,7 @@ def name(self): class AMPLRepn(object): __slots__ = ('nl', 'mult', 'const', 'linear', 'nonlinear', 'named_exprs') - ActiveVisitor = None + template = text_nl_template def __init__(self, const, linear, nonlinear): self.nl = None @@ -2094,7 +2197,7 @@ def __repr__(self): return str(self) def __eq__(self, other): - return other.__class__ is AMPLRepn and ( + return isinstance(other.__class__, AMPLRepn) and ( self.nl == other.nl and self.mult == other.mult and self.const == other.const @@ -2120,8 +2223,8 @@ def duplicate(self): ans.named_exprs = None if self.named_exprs is None else set(self.named_exprs) return ans - def compile_repn(self, visitor, prefix='', args=None, named_exprs=None): - template = visitor.template + def compile_repn(self, prefix='', args=None, named_exprs=None): + template = self.template if self.mult != 1: if self.mult == -1: prefix += template.negation @@ -2195,7 +2298,7 @@ def compile_repn(self, visitor, prefix='', args=None, named_exprs=None): else: # nterms == 0 return prefix + (template.const % 0), args, named_exprs - def compile_nonlinear_fragment(self, visitor): + def compile_nonlinear_fragment(self): if not self.nonlinear: self.nonlinear = None return @@ -2205,9 +2308,9 @@ def compile_nonlinear_fragment(self, visitor): deque(map(args.extend, map(itemgetter(1), self.nonlinear)), maxlen=0) if nterms > 2: - self.nonlinear = (visitor.template.nary_sum % nterms) + nl_sum, args + self.nonlinear = (self.template.nary_sum % nterms) + nl_sum, args elif nterms == 2: - self.nonlinear = visitor.template.binary_sum + nl_sum, args + self.nonlinear = self.template.binary_sum + nl_sum, args else: # nterms == 1: self.nonlinear = nl_sum, args @@ -2249,9 +2352,7 @@ def append(self, other): # it and append it (this both resolves the # multiplier, and marks the named expression as # having been used) - other = other.compile_repn( - self.ActiveVisitor, '', None, self.named_exprs - ) + other = other.compile_repn('', None, self.named_exprs) nl, nl_args, self.named_exprs = other self.nonlinear.append((nl, nl_args)) return @@ -2272,11 +2373,11 @@ def append(self, other): linear[v] = c * mult if other.nonlinear: if other.nonlinear.__class__ is list: - other.compile_nonlinear_fragment(self.ActiveVisitor) + other.compile_nonlinear_fragment() if mult == -1: - prefix = self.ActiveVisitor.template.negation + prefix = self.template.negation else: - prefix = self.ActiveVisitor.template.multiplier % mult + prefix = self.template.multiplier % mult self.nonlinear.append( (prefix + other.nonlinear[0], other.nonlinear[1]) ) @@ -2316,132 +2417,9 @@ def to_expr(self, var_map): return ans * self.mult -def _create_strict_inequality_map(vars_): - vars_['strict_inequality_map'] = { - True: vars_['less_than'], - False: vars_['less_equal'], - (True, True): (vars_['less_than'], vars_['less_than']), - (True, False): (vars_['less_than'], vars_['less_equal']), - (False, True): (vars_['less_equal'], vars_['less_than']), - (False, False): (vars_['less_equal'], vars_['less_equal']), - } - - -class text_nl_debug_template(object): - unary = { - 'log': 'o43\t#log\n', - 'log10': 'o42\t#log10\n', - 'sin': 'o41\t#sin\n', - 'cos': 'o46\t#cos\n', - 'tan': 'o38\t#tan\n', - 'sinh': 'o40\t#sinh\n', - 'cosh': 'o45\t#cosh\n', - 'tanh': 'o37\t#tanh\n', - 'asin': 'o51\t#asin\n', - 'acos': 'o53\t#acos\n', - 'atan': 'o49\t#atan\n', - 'exp': 'o44\t#exp\n', - 'sqrt': 'o39\t#sqrt\n', - 'asinh': 'o50\t#asinh\n', - 'acosh': 'o52\t#acosh\n', - 'atanh': 'o47\t#atanh\n', - 'ceil': 'o14\t#ceil\n', - 'floor': 'o13\t#floor\n', - } - - binary_sum = 'o0\t#+\n' - product = 'o2\t#*\n' - division = 'o3\t# /\n' - pow = 'o5\t#^\n' - abs = 'o15\t# abs\n' - negation = 'o16\t#-\n' - nary_sum = 'o54\t# sumlist\n%d\t# (n)\n' - exprif = 'o35\t# if\n' - and_expr = 'o21\t# and\n' - less_than = 'o22\t# lt\n' - less_equal = 'o23\t# le\n' - equality = 'o24\t# eq\n' - external_fcn = 'f%d %d%s\n' - # NOTE: to support scaling and substitutions, we do NOT include the - # 'v' or the EOL here: - var = '%s' - const = 'n%r\n' - string = 'h%d:%s\n' - monomial = product + const + var.replace('%', '%%') - multiplier = product + const - - _create_strict_inequality_map(vars()) - - -nl_operators = { - 0: (2, operator.add), - 2: (2, operator.mul), - 3: (2, operator.truediv), - 5: (2, operator.pow), - 15: (1, operator.abs), - 16: (1, operator.neg), - 54: (None, lambda *x: sum(x)), - 35: (3, lambda a, b, c: b if a else c), - 21: (2, operator.and_), - 22: (2, operator.lt), - 23: (2, operator.le), - 24: (2, operator.eq), - 43: (1, math.log), - 42: (1, math.log10), - 41: (1, math.sin), - 46: (1, math.cos), - 38: (1, math.tan), - 40: (1, math.sinh), - 45: (1, math.cosh), - 37: (1, math.tanh), - 51: (1, math.asin), - 53: (1, math.acos), - 49: (1, math.atan), - 44: (1, math.exp), - 39: (1, math.sqrt), - 50: (1, math.asinh), - 52: (1, math.acosh), - 47: (1, math.atanh), - 14: (1, math.ceil), - 13: (1, math.floor), -} - - -def _strip_template_comments(vars_, base_): - vars_['unary'] = { - k: v[: v.find('\t#')] + '\n' if v[-1] == '\n' else '' - for k, v in base_.unary.items() - } - for k, v in base_.__dict__.items(): - if type(v) is str and '\t#' in v: - v_lines = v.split('\n') - for i, l in enumerate(v_lines): - comment_start = l.find('\t#') - if comment_start >= 0: - v_lines[i] = l[:comment_start] - vars_[k] = '\n'.join(v_lines) - - -# The "standard" text mode template is the debugging template with the -# comments removed -class text_nl_template(text_nl_debug_template): - _strip_template_comments(vars(), text_nl_debug_template) - _create_strict_inequality_map(vars()) - - -def node_result_to_amplrepn(data): - if data[0] is _GENERAL: - return data[1] - elif data[0] is _MONOMIAL: - _, v, c = data - if c: - return AMPLRepn(0, {v: c}, None) - else: - return AMPLRepn(0, None, None) - elif data[0] is _CONSTANT: - return AMPLRepn(data[1], None, None) - else: - raise DeveloperError("unknown result type") +class DebugAMPLRepn(AMPLRepn): + __slots__ = () + template = text_nl_debug_template def handle_negation_node(visitor, node, arg1): @@ -2511,11 +2489,11 @@ def handle_product_node(visitor, node, arg1, arg2): _prod = 0 return (_CONSTANT, _prod) return (_CONSTANT, mult * arg2[1]) - nonlin = node_result_to_amplrepn(arg1).compile_repn( - visitor, visitor.template.product + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn( + visitor.template.product ) - nonlin = node_result_to_amplrepn(arg2).compile_repn(visitor, *nonlin) - return (_GENERAL, AMPLRepn(0, None, nonlin)) + nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) + return (_GENERAL, visitor.Result(0, None, nonlin)) def handle_division_node(visitor, node, arg1, arg2): @@ -2540,11 +2518,11 @@ def handle_division_node(visitor, node, arg1, arg2): return _CONSTANT, apply_node_operation(node, (arg1[1], div)) elif arg1[0] is _CONSTANT and not arg1[1]: return _CONSTANT, 0 - nonlin = node_result_to_amplrepn(arg1).compile_repn( - visitor, visitor.template.division + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn( + visitor.template.division ) - nonlin = node_result_to_amplrepn(arg2).compile_repn(visitor, *nonlin) - return (_GENERAL, AMPLRepn(0, None, nonlin)) + nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) + return (_GENERAL, visitor.Result(0, None, nonlin)) def handle_pow_node(visitor, node, arg1, arg2): @@ -2558,25 +2536,25 @@ def handle_pow_node(visitor, node, arg1, arg2): return _CONSTANT, 1 elif arg2[1] == 1: return arg1 - nonlin = node_result_to_amplrepn(arg1).compile_repn(visitor, visitor.template.pow) - nonlin = node_result_to_amplrepn(arg2).compile_repn(visitor, *nonlin) - return (_GENERAL, AMPLRepn(0, None, nonlin)) + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn(visitor.template.pow) + nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) + return (_GENERAL, visitor.Result(0, None, nonlin)) def handle_abs_node(visitor, node, arg1): if arg1[0] is _CONSTANT: return (_CONSTANT, abs(arg1[1])) - nonlin = node_result_to_amplrepn(arg1).compile_repn(visitor, visitor.template.abs) - return (_GENERAL, AMPLRepn(0, None, nonlin)) + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn(visitor.template.abs) + return (_GENERAL, visitor.Result(0, None, nonlin)) def handle_unary_node(visitor, node, arg1): if arg1[0] is _CONSTANT: return _CONSTANT, apply_node_operation(node, (arg1[1],)) - nonlin = node_result_to_amplrepn(arg1).compile_repn( - visitor, visitor.template.unary[node.name] + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn( + visitor.template.unary[node.name] ) - return (_GENERAL, AMPLRepn(0, None, nonlin)) + return (_GENERAL, visitor.Result(0, None, nonlin)) def handle_exprif_node(visitor, node, arg1, arg2, arg3): @@ -2585,49 +2563,47 @@ def handle_exprif_node(visitor, node, arg1, arg2, arg3): return arg2 else: return arg3 - nonlin = node_result_to_amplrepn(arg1).compile_repn( - visitor, visitor.template.exprif - ) - nonlin = node_result_to_amplrepn(arg2).compile_repn(visitor, *nonlin) - nonlin = node_result_to_amplrepn(arg3).compile_repn(visitor, *nonlin) - return (_GENERAL, AMPLRepn(0, None, nonlin)) + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn(visitor.template.exprif) + nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) + nonlin = visitor.node_result_to_amplrepn(arg3).compile_repn(*nonlin) + return (_GENERAL, visitor.Result(0, None, nonlin)) def handle_equality_node(visitor, node, arg1, arg2): if arg1[0] is _CONSTANT and arg2[0] is _CONSTANT: return (_CONSTANT, arg1[1] == arg2[1]) - nonlin = node_result_to_amplrepn(arg1).compile_repn( - visitor, visitor.template.equality + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn( + visitor.template.equality ) - nonlin = node_result_to_amplrepn(arg2).compile_repn(visitor, *nonlin) - return (_GENERAL, AMPLRepn(0, None, nonlin)) + nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) + return (_GENERAL, visitor.Result(0, None, nonlin)) def handle_inequality_node(visitor, node, arg1, arg2): if arg1[0] is _CONSTANT and arg2[0] is _CONSTANT: return (_CONSTANT, node._apply_operation((arg1[1], arg2[1]))) - nonlin = node_result_to_amplrepn(arg1).compile_repn( - visitor, visitor.template.strict_inequality_map[node.strict] + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn( + visitor.template.strict_inequality_map[node.strict] ) - nonlin = node_result_to_amplrepn(arg2).compile_repn(visitor, *nonlin) - return (_GENERAL, AMPLRepn(0, None, nonlin)) + nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) + return (_GENERAL, visitor.Result(0, None, nonlin)) def handle_ranged_inequality_node(visitor, node, arg1, arg2, arg3): if arg1[0] is _CONSTANT and arg2[0] is _CONSTANT and arg3[0] is _CONSTANT: return (_CONSTANT, node._apply_operation((arg1[1], arg2[1], arg3[1]))) op = visitor.template.strict_inequality_map[node.strict] - nl, args, named = node_result_to_amplrepn(arg1).compile_repn( - visitor, visitor.template.and_expr + op[0] + nl, args, named = visitor.node_result_to_amplrepn(arg1).compile_repn( + visitor.template.and_expr + op[0] ) - nl2, args2, named = node_result_to_amplrepn(arg2).compile_repn( - visitor, '', None, named + nl2, args2, named = visitor.node_result_to_amplrepn(arg2).compile_repn( + '', None, named ) nl += nl2 + op[1] + nl2 args.extend(args2) args.extend(args2) - nonlin = node_result_to_amplrepn(arg3).compile_repn(visitor, nl, args, named) - return (_GENERAL, AMPLRepn(0, None, nonlin)) + nonlin = visitor.node_result_to_amplrepn(arg3).compile_repn(nl, args, named) + return (_GENERAL, visitor.Result(0, None, nonlin)) def handle_named_expression_node(visitor, node, arg1): @@ -2637,7 +2613,7 @@ def handle_named_expression_node(visitor, node, arg1): # to appear in the 'linear' portion of a constraint / objective # definition. We will return this as a "var" template, but # wrapped in the nonlinear portion of the expression tree. - repn = node_result_to_amplrepn(arg1) + repn = visitor.node_result_to_amplrepn(arg1) # A local copy of the expression source list. This will be updated # later if the same Expression node is encountered in another @@ -2669,7 +2645,7 @@ def handle_named_expression_node(visitor, node, arg1): # original (linear + nonlinear) V line (which will not happen if # the V line is part of a larger linear operator). if repn.nonlinear.__class__ is list: - repn.compile_nonlinear_fragment(visitor) + repn.compile_nonlinear_fragment() if not visitor.use_named_exprs: return _GENERAL, repn.duplicate() @@ -2693,7 +2669,7 @@ def handle_named_expression_node(visitor, node, arg1): # named subexpressions when appropriate. sub_node = NLFragment(repn, node) sub_id = id(sub_node) - sub_repn = AMPLRepn(0, None, None) + sub_repn = visitor.Result(0, None, None) sub_repn.nonlinear = repn.nonlinear sub_repn.nl = (visitor.template.var, (sub_id,)) sub_repn.named_exprs = set(repn.named_exprs) @@ -2801,11 +2777,11 @@ def handle_external_function_node(visitor, node, *args): arg_ids.append(_id) visitor.subexpression_cache[_id] = ( arg, - AMPLRepn( + visitor.Result( 0, None, - node_result_to_amplrepn(arg).compile_repn( - visitor, named_exprs=named_exprs + visitor.node_result_to_amplrepn(arg).compile_repn( + named_exprs=named_exprs ), ), (None, None, True), @@ -2814,7 +2790,7 @@ def handle_external_function_node(visitor, node, *args): named_exprs = None return ( _GENERAL, - AMPLRepn(0, None, (nl + '%s' * len(arg_ids), arg_ids, named_exprs)), + visitor.Result(0, None, (nl + '%s' * len(arg_ids), arg_ids, named_exprs)), ) @@ -2875,7 +2851,7 @@ def _record_var(visitor, var): @staticmethod def _before_string(visitor, child): visitor.encountered_string_arguments = True - ans = AMPLRepn(child, None, None) + ans = visitor.Result(child, None, None) ans.nl = (visitor.template.string % (len(child), child), ()) return False, (_GENERAL, ans) @@ -2996,7 +2972,7 @@ def _before_linear(visitor, child): return True, None if linear: - return False, (_GENERAL, AMPLRepn(const, linear, None)) + return False, (_GENERAL, visitor.Result(const, linear, None)) else: return False, (_CONSTANT, const) @@ -3021,7 +2997,6 @@ def _before_named_expression(visitor, child): class AMPLRepnVisitor(StreamBasedExpressionVisitor): def __init__( self, - template, subexpression_cache, external_functions, var_map, @@ -3031,7 +3006,6 @@ def __init__( sorter, ): super().__init__() - self.template = template self.subexpression_cache = subexpression_cache self.external_functions = external_functions self.active_expression_source = None @@ -3045,6 +3019,12 @@ def __init__( self.evaluate = self._eval_expr_visitor.dfs_postorder_stack self.sorter = sorter + if symbolic_solver_labels: + self.Result = DebugAMPLRepn + else: + self.Result = AMPLRepn + self.template = self.Result.template + def check_constant(self, ans, obj): if ans.__class__ not in native_numeric_types: # None can be returned from uninitialized Var/Param objects @@ -3088,6 +3068,20 @@ def cache_fixed_var(self, _id, child): ) self.fixed_vars[_id] = self.check_constant(child.value, child) + def node_result_to_amplrepn(self, data): + if data[0] is _GENERAL: + return data[1] + elif data[0] is _MONOMIAL: + _, v, c = data + if c: + return self.Result(0, {v: c}, None) + else: + return self.Result(0, None, None) + elif data[0] is _CONSTANT: + return self.Result(data[1], None, None) + else: + raise DeveloperError("unknown result type") + def initializeWalker(self, expr): expr, src, src_idx, self.expression_scaling_factor = expr self.active_expression_source = (src_idx, id(src)) @@ -3103,14 +3097,14 @@ def enterNode(self, node): # SumExpression are potentially large nary operators. Directly # populate the result if node.__class__ in sum_like_expression_types: - data = AMPLRepn(0, {}, None) + data = self.Result(0, {}, None) data.nonlinear = [] return node.args, data else: return node.args, [] def exitNode(self, node, data): - if data.__class__ is AMPLRepn: + if data.__class__ is self.Result: # If the summation resulted in a constant, return the constant if data.linear or data.nonlinear or data.nl: return (_GENERAL, data) @@ -3122,7 +3116,7 @@ def exitNode(self, node, data): return _operator_handles[node.__class__](self, node, *data) def finalizeResult(self, result): - ans = node_result_to_amplrepn(result) + ans = self.node_result_to_amplrepn(result) # Multiply the expression by the scaling factor provided by the caller ans.mult *= self.expression_scaling_factor @@ -3161,7 +3155,7 @@ def finalizeResult(self, result): ans.nl = None if ans.nonlinear.__class__ is list: - ans.compile_nonlinear_fragment(self) + ans.compile_nonlinear_fragment() if not ans.linear: ans.linear = {} diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 4d7b5d9ab6c..336e96b93f3 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -64,7 +64,6 @@ def __init__(self, symbolic=False): self.symbolic_solver_labels = symbolic self.visitor = nl_writer.AMPLRepnVisitor( - self.template, self.subexpression_cache, self.external_functions, self.var_map, @@ -75,13 +74,10 @@ def __init__(self, symbolic=False): ) def __enter__(self): - assert nl_writer.AMPLRepn.ActiveVisitor is None - nl_writer.AMPLRepn.ActiveVisitor = self.visitor return self def __exit__(self, exc_type, exc_value, tb): - assert nl_writer.AMPLRepn.ActiveVisitor is self.visitor - nl_writer.AMPLRepn.ActiveVisitor = None + pass class Test_AMPLRepnVisitor(unittest.TestCase): From df55bd2118e7e8dc18552acd54ea86025f4f5591 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 25 Jul 2024 09:38:17 -0600 Subject: [PATCH 2012/3044] Make several functions private --- .../piecewise/tests/test_triangulations.py | 6 +- pyomo/contrib/piecewise/triangulations.py | 73 ++++++++++--------- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index 0576191fc7d..26b415f95c7 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -16,7 +16,7 @@ from pyomo.contrib.piecewise.triangulations import ( get_unordered_j1_triangulation, get_ordered_j1_triangulation, - get_Gn_hamiltonian, + _get_Gn_hamiltonian, get_grid_hamiltonian, ) from pyomo.common.dependencies import numpy as np, numpy_available @@ -67,7 +67,7 @@ def check_J1_ordered(self, points, num_points, dim): for idx, first_simplex in enumerate(ordered_triangulation): if idx != len(ordered_triangulation) - 1: second_simplex = ordered_triangulation[idx + 1] - # test property (2) which also guarantees property (1) + # test property (2) which also guarantees property (1) (from Vielma 2010) self.assertEqual( first_simplex[-1], second_simplex[0], @@ -166,7 +166,7 @@ def test_J1_ordered_4d_and_above(self): ) def check_Gn_hamiltonian_path(self, n, start_permutation, target_symbol, last): - path = get_Gn_hamiltonian(n, start_permutation, target_symbol, last) + path = _get_Gn_hamiltonian(n, start_permutation, target_symbol, last) self.assertEqual(len(path), factorial(n)) self.assertEqual(path[0], start_permutation) if last: diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 8696aa31144..c09417ee003 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -37,7 +37,7 @@ class Triangulation(Enum): def get_unordered_j1_triangulation(points, dimension): points_map, num_pts = _process_points_j1(points, dimension) simplices_list = _get_j1_triangulation(points_map, num_pts - 1, dimension) - return _FakeScipyTriangulation( + return _Triangulation( points=np.array(points), simplices=np.array(simplices_list), coplanar=np.array([]), @@ -67,7 +67,7 @@ def get_ordered_j1_triangulation(points, dimension): simplices_list = _get_ordered_j1_triangulation_4d_and_above( points_map, num_pts - 1, dimension ) - return _FakeScipyTriangulation( + return _Triangulation( points=np.array(points), simplices=np.array(simplices_list), coplanar=np.array([]), @@ -80,7 +80,7 @@ def get_ordered_j1_triangulation(points, dimension): # - simplices: list of M simplices as P x (n + 1) array of point _indices_ # - coplanar: list of N points omitted from triangulation as tuples of (point index, # nearest simplex index, nearest vertex index), stacked into an N x 3 array -class _FakeScipyTriangulation: +class _Triangulation: def __init__(self, points, simplices, coplanar): self.points = points self.simplices = simplices @@ -94,11 +94,13 @@ def _process_points_j1(points, dimension): num_pts = round(len(points) ** (1 / dimension)) if not len(points) == num_pts**dimension: raise ValueError( - "'points' must have points forming an n-dimensional grid with straight grid lines and the same odd number of points in each axis" + "'points' must have points forming an n-dimensional grid with straight grid" + " lines and the same odd number of points in each axis." ) if not num_pts % 2 == 1: raise ValueError( - "'points' must have points forming an n-dimensional grid with straight grid lines and the same odd number of points in each axis" + "'points' must have points forming an n-dimensional grid with straight grid" + " lines and the same odd number of points in each axis." ) # munge the points into an organized map from n-dimensional keys to original @@ -224,7 +226,7 @@ def add_top_left(): if is_turnaround(x, y): # finished; this case should always eventually be reached add_bottom_left() - fix_vertices_incremental_order(simplices) + _fix_vertices_incremental_order(simplices) return simplices else: if square_parity_tlbr(x, y): @@ -317,10 +319,13 @@ def add_top_left(): def _get_ordered_j1_triangulation_3d(points_map, num_pts): # To start, we need a hamiltonian path in the grid graph of *double* cubes # (2x2x2 cubes) - grid_hamiltonian = get_grid_hamiltonian(3, round(num_pts / 2)) # division is exact + grid_hamiltonian = _get_grid_hamiltonian(3, round(num_pts / 2)) # division is exact # We always start by going from [0, 0, 0] to [0, 0, 1], so we can safely - # start from the -x side + # start from the -x side. + # Data format: the first tuple is a basis vector or its negative, representing a + # face. The number afterwards is a 1 or 2 disambiguating which, of the two simplices + # on that face we consider, we are referring to. start_data = ((-1, 0, 0), 1) simplices = [] @@ -351,7 +356,7 @@ def _get_ordered_j1_triangulation_3d(points_map, num_pts): for simplex_data in current_cube_path: simplices.append( - get_one_j1_simplex( + _get_one_j1_simplex( current_v_0, simplex_data[1], simplex_data[0], 3, points_map ) ) @@ -374,19 +379,19 @@ def _get_ordered_j1_triangulation_3d(points_map, num_pts): for simplex_data in current_cube_path: simplices.append( - get_one_j1_simplex( + _get_one_j1_simplex( current_v_0, simplex_data[1], simplex_data[0], 3, points_map ) ) - fix_vertices_incremental_order(simplices) + _fix_vertices_incremental_order(simplices) return simplices def _get_ordered_j1_triangulation_4d_and_above(points_map, num_pts, dim): # step one: get a hamiltonian path in the appropriate grid graph (low-coordinate # corners of the grid squares) - grid_hamiltonian = get_grid_hamiltonian(dim, num_pts) + grid_hamiltonian = _get_grid_hamiltonian(dim, num_pts) # step 1.5: get a starting simplex. Anything that is *not* adjacent to the # second square is fine. Since we always go from [0, ..., 0] to [0, ..., 1], @@ -405,34 +410,34 @@ def _get_ordered_j1_triangulation_4d_and_above(points_map, num_pts, dim): j = [k + 1 for k in range(dim) if current_corner[k] != next_corner[k]][0] # border x_j value between this square and next c = max(current_corner[j - 1], next_corner[j - 1]) - v_0, sign = get_nearest_odd_and_sign_vec(current_corner) + v_0, sign = _get_nearest_odd_and_sign_vec(current_corner) # According to Todd, what we need is to end with a permutation where rho(n) = j # if c is odd, and end with one where rho(1) = j if c is even. I think this # is right -- basically the sign from the sign vector sometimes cancels # out the sign from whether we are entering in the +c or -c direction. if c % 2 == 0: - perm_sequence = get_Gn_hamiltonian(dim, start_perm, j, False) + perm_sequence = _get_Gn_hamiltonian(dim, start_perm, j, False) for pi in perm_sequence: - simplices.append(get_one_j1_simplex(v_0, pi, sign, dim, points_map)) + simplices.append(_get_one_j1_simplex(v_0, pi, sign, dim, points_map)) else: - perm_sequence = get_Gn_hamiltonian(dim, start_perm, j, True) + perm_sequence = _get_Gn_hamiltonian(dim, start_perm, j, True) for pi in perm_sequence: - simplices.append(get_one_j1_simplex(v_0, pi, sign, dim, points_map)) + simplices.append(_get_one_j1_simplex(v_0, pi, sign, dim, points_map)) # should be true regardless of odd or even start_perm = perm_sequence[-1] # step three: finish out the last square # Any final permutation is fine; we are going nowhere after this - v_0, sign = get_nearest_odd_and_sign_vec(grid_hamiltonian[-1]) - for pi in get_Gn_hamiltonian(dim, start_perm, 1, False): - simplices.append(get_one_j1_simplex(v_0, pi, sign, dim, points_map)) + v_0, sign = _get_nearest_odd_and_sign_vec(grid_hamiltonian[-1]) + for pi in _get_Gn_hamiltonian(dim, start_perm, 1, False): + simplices.append(_get_one_j1_simplex(v_0, pi, sign, dim, points_map)) # fix vertices and return - fix_vertices_incremental_order(simplices) + _fix_vertices_incremental_order(simplices) return simplices -def get_one_j1_simplex(v_0, pi, sign, dim, points_map): +def _get_one_j1_simplex(v_0, pi, sign, dim, points_map): simplex = [] current = list(v_0) simplex.append(points_map[tuple(current)]) @@ -444,7 +449,7 @@ def get_one_j1_simplex(v_0, pi, sign, dim, points_map): # get the v_0 and sign vectors corresponding to a given square, identified by its # low-coordinate corner -def get_nearest_odd_and_sign_vec(corner): +def _get_nearest_odd_and_sign_vec(corner): v_0 = [] sign = [] for x in corner: @@ -457,12 +462,12 @@ def get_nearest_odd_and_sign_vec(corner): return v_0, sign -def get_grid_hamiltonian(dim, length): +def _get_grid_hamiltonian(dim, length): if dim == 1: return [[n] for n in range(length)] else: ret = [] - prev = get_grid_hamiltonian(dim - 1, length) + prev = _get_grid_hamiltonian(dim - 1, length) for n in range(length): # if n is even, add the previous hamiltonian with n in its new first # coordinate. If odd, do the same with the previous hamiltonian in reverse. @@ -476,7 +481,7 @@ def get_grid_hamiltonian(dim, length): # Fix vertices (in place) when the simplices are right but vertices are not -def fix_vertices_incremental_order(simplices): +def _fix_vertices_incremental_order(simplices): last_vertex_index = len(simplices[0]) - 1 for i, simplex in enumerate(simplices): # Choose vertices like this: first is always the same as last @@ -496,7 +501,7 @@ def fix_vertices_incremental_order(simplices): if simplex[n] in simplices[i + 1] and n != first: last = n break - if first == None or last == None: + if first is None or last is None: raise DeveloperError("Couldn't fix vertex ordering for incremental.") # reorder the simplex with the desired first and last @@ -520,7 +525,7 @@ def fix_vertices_incremental_order(simplices): # starting permutation, such that a fixed target symbol is either the image # rho(1), or it is rho(n), depending on whether first or last is requested, # where rho is the final permutation. -def get_Gn_hamiltonian(n, start_permutation, target_symbol, last, _cache={}): +def _get_Gn_hamiltonian(n, start_permutation, target_symbol, last, _cache={}): if n < 4: raise ValueError("n must be at least 4 for this operation to be possible") if (n, start_permutation, target_symbol, last) in _cache: @@ -529,7 +534,7 @@ def get_Gn_hamiltonian(n, start_permutation, target_symbol, last, _cache={}): if last: ret = [ tuple(reversed(pi)) - for pi in get_Gn_hamiltonian( + for pi in _get_Gn_hamiltonian( n, tuple(reversed(start_permutation)), target_symbol, False ) ] @@ -544,19 +549,19 @@ def get_Gn_hamiltonian(n, start_permutation, target_symbol, last, _cache={}): ] # pi^-1(j) ret = [ tuple(start_permutation[pi[i] - 1] for i in range(n)) - for pi in _get_Gn_hamiltonian(n, new_target_symbol) + for pi in _get_Gn_hamiltonian_impl(n, new_target_symbol) ] _cache[(n, start_permutation, target_symbol, last)] = ret return ret else: - ret = _get_Gn_hamiltonian(n, target_symbol) + ret = _get_Gn_hamiltonian_impl(n, target_symbol) _cache[(n, start_permutation, target_symbol, last)] = ret return ret # Assume the starting permutation is (1, ..., n) and the target symbol needs to # be in the first position of the last permutation -def _get_Gn_hamiltonian(n, target_symbol): +def _get_Gn_hamiltonian_impl(n, target_symbol): # base case: proof by picture from Todd 79, Figure 2 # note: Figure 2 contains an error, careful! if n == 4: @@ -675,7 +680,7 @@ def _get_Gn_hamiltonian(n, target_symbol): idx = n - 1 facing = -1 ret = [] - for pi in _get_Gn_hamiltonian(n - 1, target_symbol): + for pi in _get_Gn_hamiltonian_impl(n - 1, target_symbol): for _ in range(n): l = list(pi) l.insert(idx, n) @@ -689,7 +694,7 @@ def _get_Gn_hamiltonian(n, target_symbol): idx = 0 facing = 1 ret = [] - for pi in _get_Gn_hamiltonian(n - 1, n - 1): + for pi in _get_Gn_hamiltonian_impl(n - 1, n - 1): for _ in range(n): l = [x + 1 for x in pi] l.insert(idx, 1) From c9215711f3527243f62b5ff4c2bbf9b21d8e80aa Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 25 Jul 2024 09:41:49 -0600 Subject: [PATCH 2013/3044] finish renaming symbol --- pyomo/contrib/piecewise/tests/test_triangulations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index 26b415f95c7..db17738b242 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -17,7 +17,7 @@ get_unordered_j1_triangulation, get_ordered_j1_triangulation, _get_Gn_hamiltonian, - get_grid_hamiltonian, + _get_grid_hamiltonian, ) from pyomo.common.dependencies import numpy as np, numpy_available from math import factorial @@ -203,7 +203,7 @@ def test_Gn_hamiltonian_paths(self): self.check_Gn_hamiltonian_path(7, (1, 2, 3, 4, 5, 6, 7), 7, False) def check_grid_hamiltonian(self, dim, length): - path = get_grid_hamiltonian(dim, length) + path = _get_grid_hamiltonian(dim, length) self.assertEqual(len(path), length**dim) for x in itertools.product(range(length), repeat=dim): self.assertTrue(list(x) in path) From 58462ae383a3a1a7167cf19bd3404a81d8a35ce7 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 25 Jul 2024 09:42:12 -0600 Subject: [PATCH 2014/3044] Any color you like --- pyomo/contrib/piecewise/piecewise_linear_function.py | 3 ++- pyomo/contrib/piecewise/transform/incremental.py | 8 +------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index e1374a78668..42538082d16 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -336,7 +336,8 @@ def _construct_simplices_from_multivariate_points( obj._triangulation = tri else: raise ValueError( - "Invalid or unrecognized triangulation specified for '%s': %s" % (obj, tri) + "Invalid or unrecognized triangulation specified for '%s': %s" + % (obj, tri) ) # Get the points for the triangulation because they might not all be diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index fb761c00ebb..4cfe16c7ac4 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -13,13 +13,7 @@ PiecewiseLinearTransformationBase, ) from pyomo.contrib.piecewise.triangulations import Triangulation -from pyomo.core import ( - Constraint, - Binary, - Var, - RangeSet, - Param, -) +from pyomo.core import Constraint, Binary, Var, RangeSet, Param from pyomo.core.base import TransformationFactory From 89366854d07f7293d4299232af30f8961341db71 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 25 Jul 2024 09:56:08 -0600 Subject: [PATCH 2015/3044] reword a comment due to changed PWLF constructor --- pyomo/contrib/piecewise/transform/incremental.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 4cfe16c7ac4..f7143676b58 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -57,7 +57,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc "would likely lead to incorrect results! The built-in " "Triangulation.OrderedJ1 triangulation has an appropriate ordering for " "this transformation. If you know what you are doing, you can also " - "suppress this error by overriding the triangulation tag to be " + "suppress this error by setting the triangulation tag to " "Triangulation.AssumeValid during PiecewiseLinearFunction construction." ) # Get a new Block() in transformation_block.transformed_functions, which From 2f4bc712885ada01fc73bf58a14e7970d945266e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 25 Jul 2024 10:31:46 -0600 Subject: [PATCH 2016/3044] Track changes to AMPLRepnVisitor API --- pyomo/contrib/incidence_analysis/config.py | 3 +-- pyomo/contrib/incidence_analysis/incidence.py | 8 +------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/incidence_analysis/config.py b/pyomo/contrib/incidence_analysis/config.py index 9fac48c8a26..10bfb9133f7 100644 --- a/pyomo/contrib/incidence_analysis/config.py +++ b/pyomo/contrib/incidence_analysis/config.py @@ -14,7 +14,7 @@ import enum from pyomo.common.config import ConfigDict, ConfigValue, InEnum from pyomo.common.modeling import NOTSET -from pyomo.repn.plugins.nl_writer import AMPLRepnVisitor, text_nl_template +from pyomo.repn.plugins.nl_writer import AMPLRepnVisitor from pyomo.repn.util import FileDeterminism, FileDeterminism_to_SortComponents @@ -140,7 +140,6 @@ def get_config_from_kwds(**kwds): export_defined_variables = False sorter = FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) amplvisitor = AMPLRepnVisitor( - text_nl_template, subexpression_cache, external_functions, var_map, diff --git a/pyomo/contrib/incidence_analysis/incidence.py b/pyomo/contrib/incidence_analysis/incidence.py index 030ee2b0f79..48160bc793e 100644 --- a/pyomo/contrib/incidence_analysis/incidence.py +++ b/pyomo/contrib/incidence_analysis/incidence.py @@ -17,7 +17,6 @@ from pyomo.core.expr.numvalue import value as pyo_value from pyomo.repn import generate_standard_repn from pyomo.util.subsystems import TemporarySubsystemManager -from pyomo.repn.plugins.nl_writer import AMPLRepn from pyomo.contrib.incidence_analysis.config import ( IncidenceMethod, get_config_from_kwds, @@ -95,12 +94,7 @@ def _nonlinear_var_id_collector(idlist): yield _id var_map = visitor.var_map - orig_activevisitor = AMPLRepn.ActiveVisitor - AMPLRepn.ActiveVisitor = visitor - try: - repn = visitor.walk_expression((expr, None, 0, 1.0)) - finally: - AMPLRepn.ActiveVisitor = orig_activevisitor + repn = visitor.walk_expression((expr, None, 0, 1.0)) nonlinear_var_id_set = set() unique_nonlinear_var_ids = [] From 89ef9606f6cc66fcd1d39ea54679d3a2597ad8e9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 25 Jul 2024 11:40:32 -0600 Subject: [PATCH 2017/3044] Split the AMPLRepnVisitor from the NLv2 writer module --- pyomo/contrib/incidence_analysis/config.py | 4 +- pyomo/repn/ampl.py | 1261 ++++++++++++++++++++ pyomo/repn/plugins/nl_writer.py | 1257 +------------------ pyomo/repn/tests/ampl/test_ampl_nl.py | 4 +- pyomo/repn/tests/ampl/test_nlv2.py | 5 +- pyomo/repn/tests/nl_diff.py | 4 +- 6 files changed, 1278 insertions(+), 1257 deletions(-) create mode 100644 pyomo/repn/ampl.py diff --git a/pyomo/contrib/incidence_analysis/config.py b/pyomo/contrib/incidence_analysis/config.py index 10bfb9133f7..50ad4e48f3e 100644 --- a/pyomo/contrib/incidence_analysis/config.py +++ b/pyomo/contrib/incidence_analysis/config.py @@ -14,7 +14,7 @@ import enum from pyomo.common.config import ConfigDict, ConfigValue, InEnum from pyomo.common.modeling import NOTSET -from pyomo.repn.plugins.nl_writer import AMPLRepnVisitor +from pyomo.repn.ampl import AMPLRepnVisitor from pyomo.repn.util import FileDeterminism, FileDeterminism_to_SortComponents @@ -33,7 +33,7 @@ class IncidenceMethod(enum.Enum): """ ampl_repn = 3 - """Use ``pyomo.repn.plugins.nl_writer.AMPLRepnVisitor``""" + """Use ``pyomo.repn.ampl.AMPLRepnVisitor``""" class IncidenceOrder(enum.Enum): diff --git a/pyomo/repn/ampl.py b/pyomo/repn/ampl.py new file mode 100644 index 00000000000..7e949e37088 --- /dev/null +++ b/pyomo/repn/ampl.py @@ -0,0 +1,1261 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import ctypes +import math +import operator + +from collections import deque +from operator import itemgetter + +from pyomo.common.deprecation import deprecation_warning +from pyomo.common.errors import DeveloperError, InfeasibleConstraintException, MouseTrap +from pyomo.common.numeric_types import ( + native_complex_types, + native_numeric_types, + native_types, + value, +) + + +from pyomo.core.base import Expression +from pyomo.core.expr import ( + NegationExpression, + ProductExpression, + DivisionExpression, + PowExpression, + AbsExpression, + UnaryFunctionExpression, + MonomialTermExpression, + LinearExpression, + SumExpression, + EqualityExpression, + InequalityExpression, + RangedExpression, + Expr_ifExpression, + ExternalFunctionExpression, +) +from pyomo.core.expr.visitor import StreamBasedExpressionVisitor, _EvaluationVisitor +from pyomo.repn.util import ( + BeforeChildDispatcher, + ExitNodeDispatcher, + ExprType, + InvalidNumber, + apply_node_operation, + complex_number_error, + nan, + sum_like_expression_types, +) + + +_CONSTANT = ExprType.CONSTANT +_MONOMIAL = ExprType.MONOMIAL +_GENERAL = ExprType.GENERAL + +# Feasibility tolerance for trivial (fixed) constraints +TOL = 1e-8 + + +def _create_strict_inequality_map(vars_): + vars_['strict_inequality_map'] = { + True: vars_['less_than'], + False: vars_['less_equal'], + (True, True): (vars_['less_than'], vars_['less_than']), + (True, False): (vars_['less_than'], vars_['less_equal']), + (False, True): (vars_['less_equal'], vars_['less_than']), + (False, False): (vars_['less_equal'], vars_['less_equal']), + } + + +class text_nl_debug_template(object): + unary = { + 'log': 'o43\t#log\n', + 'log10': 'o42\t#log10\n', + 'sin': 'o41\t#sin\n', + 'cos': 'o46\t#cos\n', + 'tan': 'o38\t#tan\n', + 'sinh': 'o40\t#sinh\n', + 'cosh': 'o45\t#cosh\n', + 'tanh': 'o37\t#tanh\n', + 'asin': 'o51\t#asin\n', + 'acos': 'o53\t#acos\n', + 'atan': 'o49\t#atan\n', + 'exp': 'o44\t#exp\n', + 'sqrt': 'o39\t#sqrt\n', + 'asinh': 'o50\t#asinh\n', + 'acosh': 'o52\t#acosh\n', + 'atanh': 'o47\t#atanh\n', + 'ceil': 'o14\t#ceil\n', + 'floor': 'o13\t#floor\n', + } + + binary_sum = 'o0\t#+\n' + product = 'o2\t#*\n' + division = 'o3\t# /\n' + pow = 'o5\t#^\n' + abs = 'o15\t# abs\n' + negation = 'o16\t#-\n' + nary_sum = 'o54\t# sumlist\n%d\t# (n)\n' + exprif = 'o35\t# if\n' + and_expr = 'o21\t# and\n' + less_than = 'o22\t# lt\n' + less_equal = 'o23\t# le\n' + equality = 'o24\t# eq\n' + external_fcn = 'f%d %d%s\n' + # NOTE: to support scaling and substitutions, we do NOT include the + # 'v' or the EOL here: + var = '%s' + const = 'n%r\n' + string = 'h%d:%s\n' + monomial = product + const + var.replace('%', '%%') + multiplier = product + const + + _create_strict_inequality_map(vars()) + + +nl_operators = { + 0: (2, operator.add), + 2: (2, operator.mul), + 3: (2, operator.truediv), + 5: (2, operator.pow), + 15: (1, operator.abs), + 16: (1, operator.neg), + 54: (None, lambda *x: sum(x)), + 35: (3, lambda a, b, c: b if a else c), + 21: (2, operator.and_), + 22: (2, operator.lt), + 23: (2, operator.le), + 24: (2, operator.eq), + 43: (1, math.log), + 42: (1, math.log10), + 41: (1, math.sin), + 46: (1, math.cos), + 38: (1, math.tan), + 40: (1, math.sinh), + 45: (1, math.cosh), + 37: (1, math.tanh), + 51: (1, math.asin), + 53: (1, math.acos), + 49: (1, math.atan), + 44: (1, math.exp), + 39: (1, math.sqrt), + 50: (1, math.asinh), + 52: (1, math.acosh), + 47: (1, math.atanh), + 14: (1, math.ceil), + 13: (1, math.floor), +} + + +def _strip_template_comments(vars_, base_): + vars_['unary'] = { + k: v[: v.find('\t#')] + '\n' if v[-1] == '\n' else '' + for k, v in base_.unary.items() + } + for k, v in base_.__dict__.items(): + if type(v) is str and '\t#' in v: + v_lines = v.split('\n') + for i, l in enumerate(v_lines): + comment_start = l.find('\t#') + if comment_start >= 0: + v_lines[i] = l[:comment_start] + vars_[k] = '\n'.join(v_lines) + + +# The "standard" text mode template is the debugging template with the +# comments removed +class text_nl_template(text_nl_debug_template): + _strip_template_comments(vars(), text_nl_debug_template) + _create_strict_inequality_map(vars()) + + +class NLFragment(object): + """This is a mock "component" for the nl portion of a named Expression. + + It is used internally in the writer when requesting symbolic solver + labels so that we can generate meaningful names for the nonlinear + portion of an Expression component. + + """ + + __slots__ = ('_repn', '_node') + + def __init__(self, repn, node): + self._repn = repn + self._node = node + + @property + def name(self): + return 'nl(' + self._node.name + ')' + + +class AMPLRepn(object): + __slots__ = ('nl', 'mult', 'const', 'linear', 'nonlinear', 'named_exprs') + + template = text_nl_template + + def __init__(self, const, linear, nonlinear): + self.nl = None + self.mult = 1 + self.const = const + self.linear = linear + if nonlinear is None: + self.nonlinear = self.named_exprs = None + else: + nl, nl_args, self.named_exprs = nonlinear + self.nonlinear = nl, nl_args + + def __str__(self): + return ( + f'AMPLRepn(mult={self.mult}, const={self.const}, ' + f'linear={self.linear}, nonlinear={self.nonlinear}, ' + f'nl={self.nl}, named_exprs={self.named_exprs})' + ) + + def __repr__(self): + return str(self) + + def __eq__(self, other): + return isinstance(other.__class__, AMPLRepn) and ( + self.nl == other.nl + and self.mult == other.mult + and self.const == other.const + and self.linear == other.linear + and self.nonlinear == other.nonlinear + and self.named_exprs == other.named_exprs + ) + + def __hash__(self): + # Approximation of the Python default object hash + # (4 LSB are rolled to the MSB to reduce hash collisions) + return id(self) // 16 + ( + (id(self) & 15) << 8 * ctypes.sizeof(ctypes.c_void_p) - 4 + ) + + def duplicate(self): + ans = self.__class__.__new__(self.__class__) + ans.nl = self.nl + ans.mult = self.mult + ans.const = self.const + ans.linear = None if self.linear is None else dict(self.linear) + ans.nonlinear = self.nonlinear + ans.named_exprs = None if self.named_exprs is None else set(self.named_exprs) + return ans + + def compile_repn(self, prefix='', args=None, named_exprs=None): + template = self.template + if self.mult != 1: + if self.mult == -1: + prefix += template.negation + else: + prefix += template.multiplier % self.mult + self.mult = 1 + if self.named_exprs is not None: + if named_exprs is None: + named_exprs = set(self.named_exprs) + else: + named_exprs.update(self.named_exprs) + if self.nl is not None: + # This handles both named subexpressions and embedded + # non-numeric (e.g., string) arguments. + nl, nl_args = self.nl + if prefix: + nl = prefix + nl + if args is not None: + assert args is not nl_args + args.extend(nl_args) + else: + args = list(nl_args) + if nl_args: + # For string arguments, nl_args is an empty tuple and + # self.named_exprs is None. For named subexpressions, + # we are guaranteed that named_exprs is NOT None. We + # need to ensure that the named subexpression that we + # are returning is added to the named_exprs set. + named_exprs.update(nl_args) + return nl, args, named_exprs + + if args is None: + args = [] + if self.linear: + nterms = -len(args) + _v_template = template.var + _m_template = template.monomial + # Because we are compiling this expression (into a NL + # expression), we will go ahead and filter the 0*x terms + # from the expression. Note that the args are accumulated + # by side-effect, which prevents iterating over the linear + # terms twice. + nl_sum = ''.join( + args.append(v) or (_v_template if c == 1 else _m_template % c) + for v, c in self.linear.items() + if c + ) + nterms += len(args) + else: + nterms = 0 + nl_sum = '' + if self.nonlinear: + if self.nonlinear.__class__ is list: + nterms += len(self.nonlinear) + nl_sum += ''.join(map(itemgetter(0), self.nonlinear)) + deque(map(args.extend, map(itemgetter(1), self.nonlinear)), maxlen=0) + else: + nterms += 1 + nl_sum += self.nonlinear[0] + args.extend(self.nonlinear[1]) + if self.const: + nterms += 1 + nl_sum += template.const % self.const + + if nterms > 2: + return (prefix + (template.nary_sum % nterms) + nl_sum, args, named_exprs) + elif nterms == 2: + return prefix + template.binary_sum + nl_sum, args, named_exprs + elif nterms == 1: + return prefix + nl_sum, args, named_exprs + else: # nterms == 0 + return prefix + (template.const % 0), args, named_exprs + + def compile_nonlinear_fragment(self): + if not self.nonlinear: + self.nonlinear = None + return + args = [] + nterms = len(self.nonlinear) + nl_sum = ''.join(map(itemgetter(0), self.nonlinear)) + deque(map(args.extend, map(itemgetter(1), self.nonlinear)), maxlen=0) + + if nterms > 2: + self.nonlinear = (self.template.nary_sum % nterms) + nl_sum, args + elif nterms == 2: + self.nonlinear = self.template.binary_sum + nl_sum, args + else: # nterms == 1: + self.nonlinear = nl_sum, args + + def append(self, other): + """Append a child result from acceptChildResult + + Notes + ----- + This method assumes that the operator was "+". It is implemented + so that we can directly use an AMPLRepn() as a data object in + the expression walker (thereby avoiding the function call for a + custom callback) + + """ + # Note that self.mult will always be 1 (we only call append() + # within a sum, so there is no opportunity for self.mult to + # change). Omitting the assertion for efficiency. + # assert self.mult == 1 + _type = other[0] + if _type is _MONOMIAL: + _, v, c = other + if v in self.linear: + self.linear[v] += c + else: + self.linear[v] = c + elif _type is _GENERAL: + _, other = other + if other.nl is not None and other.nl[1]: + if other.linear: + # This is a named expression with both a linear and + # nonlinear component. We want to merge it with + # this AMPLRepn, preserving the named expression for + # only the nonlinear component (merging the linear + # component with this AMPLRepn). + pass + else: + # This is a nonlinear-only named expression, + # possibly with a multiplier that is not 1. Compile + # it and append it (this both resolves the + # multiplier, and marks the named expression as + # having been used) + other = other.compile_repn('', None, self.named_exprs) + nl, nl_args, self.named_exprs = other + self.nonlinear.append((nl, nl_args)) + return + if other.named_exprs is not None: + if self.named_exprs is None: + self.named_exprs = set(other.named_exprs) + else: + self.named_exprs.update(other.named_exprs) + if other.mult != 1: + mult = other.mult + self.const += mult * other.const + if other.linear: + linear = self.linear + for v, c in other.linear.items(): + if v in linear: + linear[v] += c * mult + else: + linear[v] = c * mult + if other.nonlinear: + if other.nonlinear.__class__ is list: + other.compile_nonlinear_fragment() + if mult == -1: + prefix = self.template.negation + else: + prefix = self.template.multiplier % mult + self.nonlinear.append( + (prefix + other.nonlinear[0], other.nonlinear[1]) + ) + else: + self.const += other.const + if other.linear: + linear = self.linear + for v, c in other.linear.items(): + if v in linear: + linear[v] += c + else: + linear[v] = c + if other.nonlinear: + if other.nonlinear.__class__ is list: + self.nonlinear.extend(other.nonlinear) + else: + self.nonlinear.append(other.nonlinear) + elif _type is _CONSTANT: + self.const += other[1] + + def to_expr(self, var_map): + if self.nl is not None or self.nonlinear is not None: + # TODO: support converting general nonlinear expressiosn + # back to Pyomo expressions. This will require an AMPL + # parser. + raise MouseTrap("Cannot convert nonlinear AMPLRepn to Pyomo Expression") + if self.linear: + # Explicitly generate the LinearExpression. At time of + # writing, this is about 40% faster than standard operator + # overloading for O(1000) element sums + ans = LinearExpression( + [coef * var_map[vid] for vid, coef in self.linear.items()] + ) + ans += self.const + else: + ans = self.const + return ans * self.mult + + +class DebugAMPLRepn(AMPLRepn): + __slots__ = () + template = text_nl_debug_template + + +def handle_negation_node(visitor, node, arg1): + if arg1[0] is _MONOMIAL: + return (_MONOMIAL, arg1[1], -1 * arg1[2]) + elif arg1[0] is _GENERAL: + arg1[1].mult *= -1 + return arg1 + elif arg1[0] is _CONSTANT: + return (_CONSTANT, -1 * arg1[1]) + else: + raise RuntimeError("%s: %s" % (type(arg1[0]), arg1)) + + +def handle_product_node(visitor, node, arg1, arg2): + if arg2[0] is _CONSTANT: + arg2, arg1 = arg1, arg2 + if arg1[0] is _CONSTANT: + mult = arg1[1] + if not mult: + # simplify multiplication by 0 (if arg2 is zero, the + # simplification happens when we evaluate the constant + # below). Note that this is not IEEE-754 compliant, and + # will map 0*inf and 0*nan to 0 (and not to nan). We are + # including this for backwards compatibility with the NLv1 + # writer, but arguably we should deprecate/remove this + # "feature" in the future. + if arg2[0] is _CONSTANT: + _prod = mult * arg2[1] + if _prod: + deprecation_warning( + f"Encountered {mult}*{str(arg2[1])} in expression tree. " + "Mapping the NaN result to 0 for compatibility " + "with the nl_v1 writer. In the future, this NaN " + "will be preserved/emitted to comply with IEEE-754.", + version='6.4.3', + ) + _prod = 0 + return (_CONSTANT, _prod) + return arg1 + if mult == 1: + return arg2 + elif arg2[0] is _MONOMIAL: + if mult != mult: + # This catches mult (i.e., arg1) == nan + return arg1 + return (_MONOMIAL, arg2[1], mult * arg2[2]) + elif arg2[0] is _GENERAL: + if mult != mult: + # This catches mult (i.e., arg1) == nan + return arg1 + arg2[1].mult *= mult + return arg2 + elif arg2[0] is _CONSTANT: + if not arg2[1]: + # Simplify multiplication by 0; see note above about + # IEEE-754 incompatibility. + _prod = mult * arg2[1] + if _prod: + deprecation_warning( + f"Encountered {str(mult)}*{arg2[1]} in expression tree. " + "Mapping the NaN result to 0 for compatibility " + "with the nl_v1 writer. In the future, this NaN " + "will be preserved/emitted to comply with IEEE-754.", + version='6.4.3', + ) + _prod = 0 + return (_CONSTANT, _prod) + return (_CONSTANT, mult * arg2[1]) + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn( + visitor.template.product + ) + nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) + return (_GENERAL, visitor.Result(0, None, nonlin)) + + +def handle_division_node(visitor, node, arg1, arg2): + if arg2[0] is _CONSTANT: + div = arg2[1] + if div == 1: + return arg1 + if arg1[0] is _MONOMIAL: + tmp = apply_node_operation(node, (arg1[2], div)) + if tmp != tmp: + # This catches if the coefficient division results in nan + return _CONSTANT, tmp + return (_MONOMIAL, arg1[1], tmp) + elif arg1[0] is _GENERAL: + tmp = apply_node_operation(node, (arg1[1].mult, div)) + if tmp != tmp: + # This catches if the multiplier division results in nan + return _CONSTANT, tmp + arg1[1].mult = tmp + return arg1 + elif arg1[0] is _CONSTANT: + return _CONSTANT, apply_node_operation(node, (arg1[1], div)) + elif arg1[0] is _CONSTANT and not arg1[1]: + return _CONSTANT, 0 + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn( + visitor.template.division + ) + nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) + return (_GENERAL, visitor.Result(0, None, nonlin)) + + +def handle_pow_node(visitor, node, arg1, arg2): + if arg2[0] is _CONSTANT: + if arg1[0] is _CONSTANT: + ans = apply_node_operation(node, (arg1[1], arg2[1])) + if ans.__class__ in native_complex_types: + ans = complex_number_error(ans, visitor, node) + return _CONSTANT, ans + elif not arg2[1]: + return _CONSTANT, 1 + elif arg2[1] == 1: + return arg1 + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn(visitor.template.pow) + nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) + return (_GENERAL, visitor.Result(0, None, nonlin)) + + +def handle_abs_node(visitor, node, arg1): + if arg1[0] is _CONSTANT: + return (_CONSTANT, abs(arg1[1])) + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn(visitor.template.abs) + return (_GENERAL, visitor.Result(0, None, nonlin)) + + +def handle_unary_node(visitor, node, arg1): + if arg1[0] is _CONSTANT: + return _CONSTANT, apply_node_operation(node, (arg1[1],)) + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn( + visitor.template.unary[node.name] + ) + return (_GENERAL, visitor.Result(0, None, nonlin)) + + +def handle_exprif_node(visitor, node, arg1, arg2, arg3): + if arg1[0] is _CONSTANT: + if arg1[1]: + return arg2 + else: + return arg3 + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn(visitor.template.exprif) + nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) + nonlin = visitor.node_result_to_amplrepn(arg3).compile_repn(*nonlin) + return (_GENERAL, visitor.Result(0, None, nonlin)) + + +def handle_equality_node(visitor, node, arg1, arg2): + if arg1[0] is _CONSTANT and arg2[0] is _CONSTANT: + return (_CONSTANT, arg1[1] == arg2[1]) + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn( + visitor.template.equality + ) + nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) + return (_GENERAL, visitor.Result(0, None, nonlin)) + + +def handle_inequality_node(visitor, node, arg1, arg2): + if arg1[0] is _CONSTANT and arg2[0] is _CONSTANT: + return (_CONSTANT, node._apply_operation((arg1[1], arg2[1]))) + nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn( + visitor.template.strict_inequality_map[node.strict] + ) + nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) + return (_GENERAL, visitor.Result(0, None, nonlin)) + + +def handle_ranged_inequality_node(visitor, node, arg1, arg2, arg3): + if arg1[0] is _CONSTANT and arg2[0] is _CONSTANT and arg3[0] is _CONSTANT: + return (_CONSTANT, node._apply_operation((arg1[1], arg2[1], arg3[1]))) + op = visitor.template.strict_inequality_map[node.strict] + nl, args, named = visitor.node_result_to_amplrepn(arg1).compile_repn( + visitor.template.and_expr + op[0] + ) + nl2, args2, named = visitor.node_result_to_amplrepn(arg2).compile_repn( + '', None, named + ) + nl += nl2 + op[1] + nl2 + args.extend(args2) + args.extend(args2) + nonlin = visitor.node_result_to_amplrepn(arg3).compile_repn(nl, args, named) + return (_GENERAL, visitor.Result(0, None, nonlin)) + + +def handle_named_expression_node(visitor, node, arg1): + _id = id(node) + # Note that while named subexpressions ('defined variables' in the + # ASL NL file vernacular) look like variables, they are not allowed + # to appear in the 'linear' portion of a constraint / objective + # definition. We will return this as a "var" template, but + # wrapped in the nonlinear portion of the expression tree. + repn = visitor.node_result_to_amplrepn(arg1) + + # A local copy of the expression source list. This will be updated + # later if the same Expression node is encountered in another + # expression tree. + # + # This is a 3-tuple [con_id, obj_id, substitute_expression]. If the + # expression is used by more than 1 constraint / objective, then the + # id is set to 0. If it is not used by any, then it is None. + # substitute_expression is a bool indicating if this named + # subexpression tree should be directly substituted into any + # expression tree that references this node (i.e., do NOT emit the V + # line). + expression_source = [None, None, False] + # Record this common expression + visitor.subexpression_cache[_id] = ( + # 0: the "component" that generated this expression ID + node, + # 1: the common subexpression (to be written out) + repn, + # 2: the source usage information for this subexpression: + # [(con_id, obj_id, substitute); see above] + expression_source, + ) + + # As we will eventually need the compiled form of any nonlinear + # expression, we will go ahead and compile it here. We do not + # do the same for the linear component as we will only need the + # linear component compiled to a dict if we are emitting the + # original (linear + nonlinear) V line (which will not happen if + # the V line is part of a larger linear operator). + if repn.nonlinear.__class__ is list: + repn.compile_nonlinear_fragment() + + if not visitor.use_named_exprs: + return _GENERAL, repn.duplicate() + + mult, repn.mult = repn.mult, 1 + if repn.named_exprs is None: + repn.named_exprs = set() + + # When converting this shared subexpression to a (nonlinear) + # node, we want to just reference this subexpression: + repn.nl = (visitor.template.var, (_id,)) + + if repn.nonlinear: + if repn.linear: + # If this expression has both linear and nonlinear + # components, we will follow the ASL convention and break + # the named subexpression into two named subexpressions: one + # that is only the nonlinear component and one that has the + # const/linear component (and references the first). This + # will allow us to propagate linear coefficients up from + # named subexpressions when appropriate. + sub_node = NLFragment(repn, node) + sub_id = id(sub_node) + sub_repn = visitor.Result(0, None, None) + sub_repn.nonlinear = repn.nonlinear + sub_repn.nl = (visitor.template.var, (sub_id,)) + sub_repn.named_exprs = set(repn.named_exprs) + + repn.named_exprs.add(sub_id) + repn.nonlinear = sub_repn.nl + + # See above for the meaning of this source information + nl_info = list(expression_source) + visitor.subexpression_cache[sub_id] = (sub_node, sub_repn, nl_info) + # It is important that the NL subexpression comes before the + # main named expression: re-insert the original named + # expression (so that the nonlinear sub_node comes first + # when iterating over subexpression_cache) + visitor.subexpression_cache[_id] = visitor.subexpression_cache.pop(_id) + else: + nl_info = expression_source + else: + repn.nonlinear = None + if repn.linear: + if ( + not repn.const + and len(repn.linear) == 1 + and next(iter(repn.linear.values())) == 1 + ): + # This Expression holds only a variable (multiplied by + # 1). Do not emit this as a named variable and instead + # just inject the variable where this expression is + # used. + repn.nl = None + expression_source[2] = True + else: + # This Expression holds only a constant. Do not emit this + # as a named variable and instead just inject the constant + # where this expression is used. + repn.nl = None + expression_source[2] = True + + if mult != 1: + repn.const *= mult + if repn.linear: + _lin = repn.linear + for v in repn.linear: + _lin[v] *= mult + if repn.nonlinear: + if mult == -1: + prefix = visitor.template.negation + else: + prefix = visitor.template.multiplier % mult + repn.nonlinear = prefix + repn.nonlinear[0], repn.nonlinear[1] + + if expression_source[2]: + if repn.linear: + assert len(repn.linear) == 1 and not repn.const + return (_MONOMIAL,) + next(iter(repn.linear.items())) + else: + return (_CONSTANT, repn.const) + + return (_GENERAL, repn.duplicate()) + + +def handle_external_function_node(visitor, node, *args): + func = node._fcn._function + # There is a special case for external functions: these are the only + # expressions that can accept string arguments. As we currently pass + # these as 'precompiled' GENERAL AMPLRepns, the normal trap for + # constant subexpressions will miss string arguments. We will catch + # that case here by looking for NL fragments with no variable + # references. Note that the NL fragment is NOT the raw string + # argument that we want to evaluate: the raw string is in the + # `const` field. + if all( + arg[0] is _CONSTANT or (arg[0] is _GENERAL and arg[1].nl and not arg[1].nl[1]) + for arg in args + ): + arg_list = [arg[1] if arg[0] is _CONSTANT else arg[1].const for arg in args] + return _CONSTANT, apply_node_operation(node, arg_list) + if func in visitor.external_functions: + if node._fcn._library != visitor.external_functions[func][1]._library: + raise RuntimeError( + "The same external function name (%s) is associated " + "with two different libraries (%s through %s, and %s " + "through %s). The ASL solver will fail to link " + "correctly." + % ( + func, + visitor.external_functions[func]._library, + visitor.external_functions[func]._library.name, + node._fcn._library, + node._fcn.name, + ) + ) + else: + visitor.external_functions[func] = (len(visitor.external_functions), node._fcn) + comment = f'\t#{node.local_name}' if visitor.symbolic_solver_labels else '' + nl = visitor.template.external_fcn % ( + visitor.external_functions[func][0], + len(args), + comment, + ) + arg_ids = [] + named_exprs = set() + for arg in args: + _id = id(arg) + arg_ids.append(_id) + visitor.subexpression_cache[_id] = ( + arg, + visitor.Result( + 0, + None, + visitor.node_result_to_amplrepn(arg).compile_repn( + named_exprs=named_exprs + ), + ), + (None, None, True), + ) + if not named_exprs: + named_exprs = None + return ( + _GENERAL, + visitor.Result(0, None, (nl + '%s' * len(arg_ids), arg_ids, named_exprs)), + ) + + +_operator_handles = ExitNodeDispatcher( + { + NegationExpression: handle_negation_node, + ProductExpression: handle_product_node, + DivisionExpression: handle_division_node, + PowExpression: handle_pow_node, + AbsExpression: handle_abs_node, + UnaryFunctionExpression: handle_unary_node, + Expr_ifExpression: handle_exprif_node, + EqualityExpression: handle_equality_node, + InequalityExpression: handle_inequality_node, + RangedExpression: handle_ranged_inequality_node, + Expression: handle_named_expression_node, + ExternalFunctionExpression: handle_external_function_node, + # These are handled explicitly in beforeChild(): + # LinearExpression: handle_linear_expression, + # SumExpression: handle_sum_expression, + # + # Note: MonomialTermExpression is only hit when processing NPV + # subexpressions that raise errors (e.g., log(0) * m.x), so no + # special processing is needed [it is just a product expression] + MonomialTermExpression: handle_product_node, + } +) + + +class AMPLBeforeChildDispatcher(BeforeChildDispatcher): + __slots__ = () + + def __init__(self): + # Special linear / summation expressions + self[MonomialTermExpression] = self._before_monomial + self[LinearExpression] = self._before_linear + self[SumExpression] = self._before_general_expression + + @staticmethod + def _record_var(visitor, var): + # We always add all indices to the var_map at once so that + # we can honor deterministic ordering of unordered sets + # (because the user could have iterated over an unordered + # set when constructing an expression, thereby altering the + # order in which we would see the variables) + vm = visitor.var_map + try: + _iter = var.parent_component().values(visitor.sorter) + except AttributeError: + # Note that this only works for the AML, as kernel does not + # provide a parent_component() + _iter = (var,) + for v in _iter: + if v.fixed: + continue + vm[id(v)] = v + + @staticmethod + def _before_string(visitor, child): + visitor.encountered_string_arguments = True + ans = visitor.Result(child, None, None) + ans.nl = (visitor.template.string % (len(child), child), ()) + return False, (_GENERAL, ans) + + @staticmethod + def _before_var(visitor, child): + _id = id(child) + if _id not in visitor.var_map: + if child.fixed: + if _id not in visitor.fixed_vars: + visitor.cache_fixed_var(_id, child) + return False, (_CONSTANT, visitor.fixed_vars[_id]) + _before_child_handlers._record_var(visitor, child) + return False, (_MONOMIAL, _id, 1) + + @staticmethod + def _before_monomial(visitor, child): + # + # The following are performance optimizations for common + # situations (Monomial terms and Linear expressions) + # + arg1, arg2 = child._args_ + if arg1.__class__ not in native_types: + try: + arg1 = visitor.check_constant(visitor.evaluate(arg1), arg1) + except (ValueError, ArithmeticError): + return True, None + + # Trap multiplication by 0 and nan. + if not arg1: + if arg2.fixed: + _id = id(arg2) + if _id not in visitor.fixed_vars: + visitor.cache_fixed_var(id(arg2), arg2) + arg2 = visitor.fixed_vars[_id] + if arg2 != arg2: + deprecation_warning( + f"Encountered {arg1}*{arg2} in expression tree. " + "Mapping the NaN result to 0 for compatibility " + "with the nl_v1 writer. In the future, this NaN " + "will be preserved/emitted to comply with IEEE-754.", + version='6.4.3', + ) + return False, (_CONSTANT, arg1) + + _id = id(arg2) + if _id not in visitor.var_map: + if arg2.fixed: + if _id not in visitor.fixed_vars: + visitor.cache_fixed_var(_id, arg2) + return False, (_CONSTANT, arg1 * visitor.fixed_vars[_id]) + _before_child_handlers._record_var(visitor, arg2) + return False, (_MONOMIAL, _id, arg1) + + @staticmethod + def _before_linear(visitor, child): + # Because we are going to modify the LinearExpression in this + # walker, we need to make a copy of the arg list from the original + # expression tree. + var_map = visitor.var_map + const = 0 + linear = {} + for arg in child.args: + if arg.__class__ is MonomialTermExpression: + arg1, arg2 = arg._args_ + if arg1.__class__ not in native_types: + try: + arg1 = visitor.check_constant(visitor.evaluate(arg1), arg1) + except (ValueError, ArithmeticError): + return True, None + + # Trap multiplication by 0 and nan. + if not arg1: + if arg2.fixed: + arg2 = visitor.check_constant(arg2.value, arg2) + if arg2 != arg2: + deprecation_warning( + f"Encountered {arg1}*{str(arg2.value)} in expression " + "tree. Mapping the NaN result to 0 for compatibility " + "with the nl_v1 writer. In the future, this NaN " + "will be preserved/emitted to comply with IEEE-754.", + version='6.4.3', + ) + continue + + _id = id(arg2) + if _id not in var_map: + if arg2.fixed: + if _id not in visitor.fixed_vars: + visitor.cache_fixed_var(_id, arg2) + const += arg1 * visitor.fixed_vars[_id] + continue + _before_child_handlers._record_var(visitor, arg2) + linear[_id] = arg1 + elif _id in linear: + linear[_id] += arg1 + else: + linear[_id] = arg1 + elif arg.__class__ in native_types: + const += arg + elif arg.is_variable_type(): + _id = id(arg) + if _id not in var_map: + if arg.fixed: + if _id not in visitor.fixed_vars: + visitor.cache_fixed_var(_id, arg) + const += visitor.fixed_vars[_id] + continue + _before_child_handlers._record_var(visitor, arg) + linear[_id] = 1 + elif _id in linear: + linear[_id] += 1 + else: + linear[_id] = 1 + else: + try: + const += visitor.check_constant(visitor.evaluate(arg), arg) + except (ValueError, ArithmeticError): + return True, None + + if linear: + return False, (_GENERAL, visitor.Result(const, linear, None)) + else: + return False, (_CONSTANT, const) + + @staticmethod + def _before_named_expression(visitor, child): + _id = id(child) + if _id in visitor.subexpression_cache: + obj, repn, info = visitor.subexpression_cache[_id] + if info[2]: + if repn.linear: + return False, (_MONOMIAL, next(iter(repn.linear)), 1) + else: + return False, (_CONSTANT, repn.const) + return False, (_GENERAL, repn.duplicate()) + else: + return True, None + + +_before_child_handlers = AMPLBeforeChildDispatcher() + + +class AMPLRepnVisitor(StreamBasedExpressionVisitor): + def __init__( + self, + subexpression_cache, + external_functions, + var_map, + used_named_expressions, + symbolic_solver_labels, + use_named_exprs, + sorter, + ): + super().__init__() + self.subexpression_cache = subexpression_cache + self.external_functions = external_functions + self.active_expression_source = None + self.var_map = var_map + self.used_named_expressions = used_named_expressions + self.symbolic_solver_labels = symbolic_solver_labels + self.use_named_exprs = use_named_exprs + self.encountered_string_arguments = False + self.fixed_vars = {} + self._eval_expr_visitor = _EvaluationVisitor(True) + self.evaluate = self._eval_expr_visitor.dfs_postorder_stack + self.sorter = sorter + + if symbolic_solver_labels: + self.Result = DebugAMPLRepn + else: + self.Result = AMPLRepn + self.template = self.Result.template + + def check_constant(self, ans, obj): + if ans.__class__ not in native_numeric_types: + # None can be returned from uninitialized Var/Param objects + if ans is None: + return InvalidNumber( + None, f"'{obj}' evaluated to a nonnumeric value '{ans}'" + ) + if ans.__class__ is InvalidNumber: + return ans + elif ans.__class__ in native_complex_types: + return complex_number_error(ans, self, obj) + else: + # It is possible to get other non-numeric types. Most + # common are bool and 1-element numpy.array(). We will + # attempt to convert the value to a float before + # proceeding. + # + # TODO: we should check bool and warn/error (while bool is + # convertible to float in Python, they have very + # different semantic meanings in Pyomo). + try: + ans = float(ans) + except: + return InvalidNumber( + ans, f"'{obj}' evaluated to a nonnumeric value '{ans}'" + ) + if ans != ans: + return InvalidNumber( + nan, f"'{obj}' evaluated to a nonnumeric value '{ans}'" + ) + return ans + + def cache_fixed_var(self, _id, child): + val = self.check_constant(child.value, child) + lb, ub = child.bounds + if (lb is not None and lb - val > TOL) or (ub is not None and ub - val < -TOL): + raise InfeasibleConstraintException( + "model contains a trivially infeasible " + f"variable '{child.name}' (fixed value " + f"{val} outside bounds [{lb}, {ub}])." + ) + self.fixed_vars[_id] = self.check_constant(child.value, child) + + def node_result_to_amplrepn(self, data): + if data[0] is _GENERAL: + return data[1] + elif data[0] is _MONOMIAL: + _, v, c = data + if c: + return self.Result(0, {v: c}, None) + else: + return self.Result(0, None, None) + elif data[0] is _CONSTANT: + return self.Result(data[1], None, None) + else: + raise DeveloperError("unknown result type") + + def initializeWalker(self, expr): + expr, src, src_idx, self.expression_scaling_factor = expr + self.active_expression_source = (src_idx, id(src)) + walk, result = self.beforeChild(None, expr, 0) + if not walk: + return False, self.finalizeResult(result) + return True, expr + + def beforeChild(self, node, child, child_idx): + return _before_child_handlers[child.__class__](self, child) + + def enterNode(self, node): + # SumExpression are potentially large nary operators. Directly + # populate the result + if node.__class__ in sum_like_expression_types: + data = self.Result(0, {}, None) + data.nonlinear = [] + return node.args, data + else: + return node.args, [] + + def exitNode(self, node, data): + if data.__class__ is self.Result: + # If the summation resulted in a constant, return the constant + if data.linear or data.nonlinear or data.nl: + return (_GENERAL, data) + else: + return (_CONSTANT, data.const) + # + # General expressions... + # + return _operator_handles[node.__class__](self, node, *data) + + def finalizeResult(self, result): + ans = self.node_result_to_amplrepn(result) + + # Multiply the expression by the scaling factor provided by the caller + ans.mult *= self.expression_scaling_factor + + # If this was a nonlinear named expression, and that expression + # has no linear portion, then we will directly use this as a + # named expression. We need to mark that the expression was + # used and return it as a simple nonlinear expression pointing + # to this named expression. In all other cases, we will return + # the processed representation (which will reference the + # nonlinear-only named subexpression - if it exists - but not + # this outer named expression). This prevents accidentally + # recharacterizing variables that only appear linearly as + # nonlinear variables. + if ans.nl is not None: + if not ans.nl[1]: + raise ValueError("Numeric expression resolved to a string constant") + # This *is* a named subexpression. If there is no linear + # component, then replace this expression with the named + # expression. The mult will be handled later. We know that + # the const is built into the nonlinear expression, because + # it cannot be changed "in place" (only through addition, + # which would have "cleared" the nl attribute) + if not ans.linear: + ans.named_exprs.update(ans.nl[1]) + ans.nonlinear = ans.nl + ans.const = 0 + else: + # This named expression has both a linear and a + # nonlinear component, and possibly a multiplier and + # constant. We will not include this named expression + # and instead will expose the components so that linear + # variables are not accidentally re-characterized as + # nonlinear. + pass + ans.nl = None + + if ans.nonlinear.__class__ is list: + ans.compile_nonlinear_fragment() + + if not ans.linear: + ans.linear = {} + if ans.mult != 1: + linear = ans.linear + mult, ans.mult = ans.mult, 1 + ans.const *= mult + if linear: + for k in linear: + linear[k] *= mult + if ans.nonlinear: + if mult == -1: + prefix = self.template.negation + else: + prefix = self.template.multiplier % mult + ans.nonlinear = prefix + ans.nonlinear[0], ans.nonlinear[1] + # + self.active_expression_source = None + return ans + + +def evaluate_ampl_nl_expression(nl, external_functions): + expr = nl.splitlines() + stack = [] + while expr: + line = expr.pop() + tokens = line.split() + # remove tokens after the first comment + for i, t in enumerate(tokens): + if t.startswith('#'): + tokens = tokens[:i] + break + if len(tokens) != 1: + # skip blank lines + if not tokens: + continue + if tokens[0][0] == 'f': + # external function + fid, nargs = tokens + fid = int(fid[1:]) + nargs = int(nargs) + fcn_id, ef = external_functions[fid] + assert fid == fcn_id + stack.append(ef.evaluate(tuple(stack.pop() for i in range(nargs)))) + continue + raise DeveloperError( + f"Unsupported line format _evaluate_constant_nl() " + f"(we expect each line to contain a single token): '{line}'" + ) + term = tokens[0] + # the "command" can be determined by the first character on the line + cmd = term[0] + # Note that we will unpack the line into the expected number of + # explicit arguments as a form of error checking + if cmd == 'n': + # numeric constant + stack.append(float(term[1:])) + elif cmd == 'o': + # operator + nargs, fcn = nl_operators[int(term[1:])] + if nargs is None: + nargs = int(stack.pop()) + stack.append(fcn(*(stack.pop() for i in range(nargs)))) + elif cmd in '1234567890': + # this is either a single int (e.g., the nargs in a nary + # sum) or a string argument. Preserve it as-is until later + # when we know which we are expecting. + stack.append(term) + elif cmd == 'h': + stack.append(term.split(':', 1)[1]) + else: + raise DeveloperError( + f"Unsupported NL operator in _evaluate_constant_nl(): '{line}'" + ) + assert len(stack) == 1 + return stack[0] diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index bc0c44c93b6..2e5a5484657 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -9,52 +9,26 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import ctypes import logging -import math -import operator import os -from collections import deque, defaultdict, namedtuple +from collections import defaultdict, namedtuple from contextlib import nullcontext -from itertools import filterfalse, product, chain +from itertools import filterfalse, product from math import log10 as _log10 -from operator import itemgetter, attrgetter, setitem +from operator import itemgetter, attrgetter from pyomo.common.collections import ComponentMap, ComponentSet from pyomo.common.config import ( - ConfigBlock, + ConfigDict, ConfigValue, InEnum, document_kwargs_from_configdict, ) -from pyomo.common.deprecation import deprecation_warning -from pyomo.common.errors import DeveloperError, InfeasibleConstraintException, MouseTrap +from pyomo.common.deprecation import relocated_module_attribute +from pyomo.common.errors import DeveloperError, InfeasibleConstraintException from pyomo.common.gc_manager import PauseGC -from pyomo.common.numeric_types import ( - native_complex_types, - native_numeric_types, - native_types, - value, -) from pyomo.common.timing import TicTocTimer -from pyomo.core.expr import ( - NegationExpression, - ProductExpression, - DivisionExpression, - PowExpression, - AbsExpression, - UnaryFunctionExpression, - MonomialTermExpression, - LinearExpression, - SumExpression, - EqualityExpression, - InequalityExpression, - RangedExpression, - Expr_ifExpression, - ExternalFunctionExpression, -) -from pyomo.core.expr.visitor import StreamBasedExpressionVisitor, _EvaluationVisitor from pyomo.core.base import ( Block, Objective, @@ -80,21 +54,14 @@ from pyomo.core.pyomoobject import PyomoObject from pyomo.opt import WriterFactory +from pyomo.repn.ampl import AMPLRepnVisitor, evaluate_ampl_nl_expression, TOL from pyomo.repn.util import ( - BeforeChildDispatcher, - ExitNodeDispatcher, - ExprType, FileDeterminism, FileDeterminism_to_SortComponents, - InvalidNumber, - apply_node_operation, categorize_valid_components, - complex_number_error, initialize_var_map_from_column_order, int_float, ordered_active_constraints, - nan, - sum_like_expression_types, ) from pyomo.repn.plugins.ampl.ampl_ import set_pyomo_amplfunc_env @@ -107,134 +74,17 @@ logger = logging.getLogger(__name__) -# Feasibility tolerance for trivial (fixed) constraints -TOL = 1e-8 +relocated_module_attribute('AMPLRepn', 'pyomo.repn.ampl.AMPLRepn', version='6.4.0.dev0') + inf = float('inf') minus_inf = -inf allowable_binary_var_bounds = {(0, 0), (0, 1), (1, 1)} -_CONSTANT = ExprType.CONSTANT -_MONOMIAL = ExprType.MONOMIAL -_GENERAL = ExprType.GENERAL - ScalingFactors = namedtuple( 'ScalingFactors', ['variables', 'constraints', 'objectives'] ) -def _create_strict_inequality_map(vars_): - vars_['strict_inequality_map'] = { - True: vars_['less_than'], - False: vars_['less_equal'], - (True, True): (vars_['less_than'], vars_['less_than']), - (True, False): (vars_['less_than'], vars_['less_equal']), - (False, True): (vars_['less_equal'], vars_['less_than']), - (False, False): (vars_['less_equal'], vars_['less_equal']), - } - - -class text_nl_debug_template(object): - unary = { - 'log': 'o43\t#log\n', - 'log10': 'o42\t#log10\n', - 'sin': 'o41\t#sin\n', - 'cos': 'o46\t#cos\n', - 'tan': 'o38\t#tan\n', - 'sinh': 'o40\t#sinh\n', - 'cosh': 'o45\t#cosh\n', - 'tanh': 'o37\t#tanh\n', - 'asin': 'o51\t#asin\n', - 'acos': 'o53\t#acos\n', - 'atan': 'o49\t#atan\n', - 'exp': 'o44\t#exp\n', - 'sqrt': 'o39\t#sqrt\n', - 'asinh': 'o50\t#asinh\n', - 'acosh': 'o52\t#acosh\n', - 'atanh': 'o47\t#atanh\n', - 'ceil': 'o14\t#ceil\n', - 'floor': 'o13\t#floor\n', - } - - binary_sum = 'o0\t#+\n' - product = 'o2\t#*\n' - division = 'o3\t# /\n' - pow = 'o5\t#^\n' - abs = 'o15\t# abs\n' - negation = 'o16\t#-\n' - nary_sum = 'o54\t# sumlist\n%d\t# (n)\n' - exprif = 'o35\t# if\n' - and_expr = 'o21\t# and\n' - less_than = 'o22\t# lt\n' - less_equal = 'o23\t# le\n' - equality = 'o24\t# eq\n' - external_fcn = 'f%d %d%s\n' - # NOTE: to support scaling and substitutions, we do NOT include the - # 'v' or the EOL here: - var = '%s' - const = 'n%r\n' - string = 'h%d:%s\n' - monomial = product + const + var.replace('%', '%%') - multiplier = product + const - - _create_strict_inequality_map(vars()) - - -nl_operators = { - 0: (2, operator.add), - 2: (2, operator.mul), - 3: (2, operator.truediv), - 5: (2, operator.pow), - 15: (1, operator.abs), - 16: (1, operator.neg), - 54: (None, lambda *x: sum(x)), - 35: (3, lambda a, b, c: b if a else c), - 21: (2, operator.and_), - 22: (2, operator.lt), - 23: (2, operator.le), - 24: (2, operator.eq), - 43: (1, math.log), - 42: (1, math.log10), - 41: (1, math.sin), - 46: (1, math.cos), - 38: (1, math.tan), - 40: (1, math.sinh), - 45: (1, math.cosh), - 37: (1, math.tanh), - 51: (1, math.asin), - 53: (1, math.acos), - 49: (1, math.atan), - 44: (1, math.exp), - 39: (1, math.sqrt), - 50: (1, math.asinh), - 52: (1, math.acosh), - 47: (1, math.atanh), - 14: (1, math.ceil), - 13: (1, math.floor), -} - - -def _strip_template_comments(vars_, base_): - vars_['unary'] = { - k: v[: v.find('\t#')] + '\n' if v[-1] == '\n' else '' - for k, v in base_.unary.items() - } - for k, v in base_.__dict__.items(): - if type(v) is str and '\t#' in v: - v_lines = v.split('\n') - for i, l in enumerate(v_lines): - comment_start = l.find('\t#') - if comment_start >= 0: - v_lines[i] = l[:comment_start] - vars_[k] = '\n'.join(v_lines) - - -# The "standard" text mode template is the debugging template with the -# comments removed -class text_nl_template(text_nl_debug_template): - _strip_template_comments(vars(), text_nl_debug_template) - _create_strict_inequality_map(vars()) - - # TODO: make a proper base class class NLWriterInfo(object): """Return type for NLWriter.write() @@ -314,7 +164,7 @@ def __init__( @WriterFactory.register('nl_v2', 'Generate the corresponding AMPL NL file (version 2).') class NLWriter(object): - CONFIG = ConfigBlock('nlwriter') + CONFIG = ConfigDict('nlwriter') CONFIG.declare( 'show_section_timing', ConfigValue( @@ -894,7 +744,7 @@ def write(self, model): if any(vid not in nl_map for vid in args): constraints.append(info) continue - expr_info.const += _evaluate_constant_nl( + expr_info.const += evaluate_ampl_nl_expression( nl % tuple(nl_map[i] for i in args), self.external_functions ) expr_info.nonlinear = None @@ -2068,7 +1918,7 @@ def _linear_presolve( # variables. So, we will fall back on parsing the (now # constant) nonlinear fragment and evaluating it. info.nonlinear = None - info.const += _evaluate_constant_nl( + info.const += evaluate_ampl_nl_expression( nl % tuple(nl_map[i] for i in args), self.external_functions ) if not info.linear: @@ -2149,1086 +1999,3 @@ def _write_v_line(self, expr_id, k): self._write_nl_expression(info[1], True) self.next_V_line_id += 1 - -class NLFragment(object): - """This is a mock "component" for the nl portion of a named Expression. - - It is used internally in the writer when requesting symbolic solver - labels so that we can generate meaningful names for the nonlinear - portion of an Expression component. - - """ - - __slots__ = ('_repn', '_node') - - def __init__(self, repn, node): - self._repn = repn - self._node = node - - @property - def name(self): - return 'nl(' + self._node.name + ')' - - -class AMPLRepn(object): - __slots__ = ('nl', 'mult', 'const', 'linear', 'nonlinear', 'named_exprs') - - template = text_nl_template - - def __init__(self, const, linear, nonlinear): - self.nl = None - self.mult = 1 - self.const = const - self.linear = linear - if nonlinear is None: - self.nonlinear = self.named_exprs = None - else: - nl, nl_args, self.named_exprs = nonlinear - self.nonlinear = nl, nl_args - - def __str__(self): - return ( - f'AMPLRepn(mult={self.mult}, const={self.const}, ' - f'linear={self.linear}, nonlinear={self.nonlinear}, ' - f'nl={self.nl}, named_exprs={self.named_exprs})' - ) - - def __repr__(self): - return str(self) - - def __eq__(self, other): - return isinstance(other.__class__, AMPLRepn) and ( - self.nl == other.nl - and self.mult == other.mult - and self.const == other.const - and self.linear == other.linear - and self.nonlinear == other.nonlinear - and self.named_exprs == other.named_exprs - ) - - def __hash__(self): - # Approximation of the Python default object hash - # (4 LSB are rolled to the MSB to reduce hash collisions) - return id(self) // 16 + ( - (id(self) & 15) << 8 * ctypes.sizeof(ctypes.c_void_p) - 4 - ) - - def duplicate(self): - ans = self.__class__.__new__(self.__class__) - ans.nl = self.nl - ans.mult = self.mult - ans.const = self.const - ans.linear = None if self.linear is None else dict(self.linear) - ans.nonlinear = self.nonlinear - ans.named_exprs = None if self.named_exprs is None else set(self.named_exprs) - return ans - - def compile_repn(self, prefix='', args=None, named_exprs=None): - template = self.template - if self.mult != 1: - if self.mult == -1: - prefix += template.negation - else: - prefix += template.multiplier % self.mult - self.mult = 1 - if self.named_exprs is not None: - if named_exprs is None: - named_exprs = set(self.named_exprs) - else: - named_exprs.update(self.named_exprs) - if self.nl is not None: - # This handles both named subexpressions and embedded - # non-numeric (e.g., string) arguments. - nl, nl_args = self.nl - if prefix: - nl = prefix + nl - if args is not None: - assert args is not nl_args - args.extend(nl_args) - else: - args = list(nl_args) - if nl_args: - # For string arguments, nl_args is an empty tuple and - # self.named_exprs is None. For named subexpressions, - # we are guaranteed that named_exprs is NOT None. We - # need to ensure that the named subexpression that we - # are returning is added to the named_exprs set. - named_exprs.update(nl_args) - return nl, args, named_exprs - - if args is None: - args = [] - if self.linear: - nterms = -len(args) - _v_template = template.var - _m_template = template.monomial - # Because we are compiling this expression (into a NL - # expression), we will go ahead and filter the 0*x terms - # from the expression. Note that the args are accumulated - # by side-effect, which prevents iterating over the linear - # terms twice. - nl_sum = ''.join( - args.append(v) or (_v_template if c == 1 else _m_template % c) - for v, c in self.linear.items() - if c - ) - nterms += len(args) - else: - nterms = 0 - nl_sum = '' - if self.nonlinear: - if self.nonlinear.__class__ is list: - nterms += len(self.nonlinear) - nl_sum += ''.join(map(itemgetter(0), self.nonlinear)) - deque(map(args.extend, map(itemgetter(1), self.nonlinear)), maxlen=0) - else: - nterms += 1 - nl_sum += self.nonlinear[0] - args.extend(self.nonlinear[1]) - if self.const: - nterms += 1 - nl_sum += template.const % self.const - - if nterms > 2: - return (prefix + (template.nary_sum % nterms) + nl_sum, args, named_exprs) - elif nterms == 2: - return prefix + template.binary_sum + nl_sum, args, named_exprs - elif nterms == 1: - return prefix + nl_sum, args, named_exprs - else: # nterms == 0 - return prefix + (template.const % 0), args, named_exprs - - def compile_nonlinear_fragment(self): - if not self.nonlinear: - self.nonlinear = None - return - args = [] - nterms = len(self.nonlinear) - nl_sum = ''.join(map(itemgetter(0), self.nonlinear)) - deque(map(args.extend, map(itemgetter(1), self.nonlinear)), maxlen=0) - - if nterms > 2: - self.nonlinear = (self.template.nary_sum % nterms) + nl_sum, args - elif nterms == 2: - self.nonlinear = self.template.binary_sum + nl_sum, args - else: # nterms == 1: - self.nonlinear = nl_sum, args - - def append(self, other): - """Append a child result from acceptChildResult - - Notes - ----- - This method assumes that the operator was "+". It is implemented - so that we can directly use an AMPLRepn() as a data object in - the expression walker (thereby avoiding the function call for a - custom callback) - - """ - # Note that self.mult will always be 1 (we only call append() - # within a sum, so there is no opportunity for self.mult to - # change). Omitting the assertion for efficiency. - # assert self.mult == 1 - _type = other[0] - if _type is _MONOMIAL: - _, v, c = other - if v in self.linear: - self.linear[v] += c - else: - self.linear[v] = c - elif _type is _GENERAL: - _, other = other - if other.nl is not None and other.nl[1]: - if other.linear: - # This is a named expression with both a linear and - # nonlinear component. We want to merge it with - # this AMPLRepn, preserving the named expression for - # only the nonlinear component (merging the linear - # component with this AMPLRepn). - pass - else: - # This is a nonlinear-only named expression, - # possibly with a multiplier that is not 1. Compile - # it and append it (this both resolves the - # multiplier, and marks the named expression as - # having been used) - other = other.compile_repn('', None, self.named_exprs) - nl, nl_args, self.named_exprs = other - self.nonlinear.append((nl, nl_args)) - return - if other.named_exprs is not None: - if self.named_exprs is None: - self.named_exprs = set(other.named_exprs) - else: - self.named_exprs.update(other.named_exprs) - if other.mult != 1: - mult = other.mult - self.const += mult * other.const - if other.linear: - linear = self.linear - for v, c in other.linear.items(): - if v in linear: - linear[v] += c * mult - else: - linear[v] = c * mult - if other.nonlinear: - if other.nonlinear.__class__ is list: - other.compile_nonlinear_fragment() - if mult == -1: - prefix = self.template.negation - else: - prefix = self.template.multiplier % mult - self.nonlinear.append( - (prefix + other.nonlinear[0], other.nonlinear[1]) - ) - else: - self.const += other.const - if other.linear: - linear = self.linear - for v, c in other.linear.items(): - if v in linear: - linear[v] += c - else: - linear[v] = c - if other.nonlinear: - if other.nonlinear.__class__ is list: - self.nonlinear.extend(other.nonlinear) - else: - self.nonlinear.append(other.nonlinear) - elif _type is _CONSTANT: - self.const += other[1] - - def to_expr(self, var_map): - if self.nl is not None or self.nonlinear is not None: - # TODO: support converting general nonlinear expressiosn - # back to Pyomo expressions. This will require an AMPL - # parser. - raise MouseTrap("Cannot convert nonlinear AMPLRepn to Pyomo Expression") - if self.linear: - # Explicitly generate the LinearExpression. At time of - # writing, this is about 40% faster than standard operator - # overloading for O(1000) element sums - ans = LinearExpression( - [coef * var_map[vid] for vid, coef in self.linear.items()] - ) - ans += self.const - else: - ans = self.const - return ans * self.mult - - -class DebugAMPLRepn(AMPLRepn): - __slots__ = () - template = text_nl_debug_template - - -def handle_negation_node(visitor, node, arg1): - if arg1[0] is _MONOMIAL: - return (_MONOMIAL, arg1[1], -1 * arg1[2]) - elif arg1[0] is _GENERAL: - arg1[1].mult *= -1 - return arg1 - elif arg1[0] is _CONSTANT: - return (_CONSTANT, -1 * arg1[1]) - else: - raise RuntimeError("%s: %s" % (type(arg1[0]), arg1)) - - -def handle_product_node(visitor, node, arg1, arg2): - if arg2[0] is _CONSTANT: - arg2, arg1 = arg1, arg2 - if arg1[0] is _CONSTANT: - mult = arg1[1] - if not mult: - # simplify multiplication by 0 (if arg2 is zero, the - # simplification happens when we evaluate the constant - # below). Note that this is not IEEE-754 compliant, and - # will map 0*inf and 0*nan to 0 (and not to nan). We are - # including this for backwards compatibility with the NLv1 - # writer, but arguably we should deprecate/remove this - # "feature" in the future. - if arg2[0] is _CONSTANT: - _prod = mult * arg2[1] - if _prod: - deprecation_warning( - f"Encountered {mult}*{str(arg2[1])} in expression tree. " - "Mapping the NaN result to 0 for compatibility " - "with the nl_v1 writer. In the future, this NaN " - "will be preserved/emitted to comply with IEEE-754.", - version='6.4.3', - ) - _prod = 0 - return (_CONSTANT, _prod) - return arg1 - if mult == 1: - return arg2 - elif arg2[0] is _MONOMIAL: - if mult != mult: - # This catches mult (i.e., arg1) == nan - return arg1 - return (_MONOMIAL, arg2[1], mult * arg2[2]) - elif arg2[0] is _GENERAL: - if mult != mult: - # This catches mult (i.e., arg1) == nan - return arg1 - arg2[1].mult *= mult - return arg2 - elif arg2[0] is _CONSTANT: - if not arg2[1]: - # Simplify multiplication by 0; see note above about - # IEEE-754 incompatibility. - _prod = mult * arg2[1] - if _prod: - deprecation_warning( - f"Encountered {str(mult)}*{arg2[1]} in expression tree. " - "Mapping the NaN result to 0 for compatibility " - "with the nl_v1 writer. In the future, this NaN " - "will be preserved/emitted to comply with IEEE-754.", - version='6.4.3', - ) - _prod = 0 - return (_CONSTANT, _prod) - return (_CONSTANT, mult * arg2[1]) - nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn( - visitor.template.product - ) - nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) - return (_GENERAL, visitor.Result(0, None, nonlin)) - - -def handle_division_node(visitor, node, arg1, arg2): - if arg2[0] is _CONSTANT: - div = arg2[1] - if div == 1: - return arg1 - if arg1[0] is _MONOMIAL: - tmp = apply_node_operation(node, (arg1[2], div)) - if tmp != tmp: - # This catches if the coefficient division results in nan - return _CONSTANT, tmp - return (_MONOMIAL, arg1[1], tmp) - elif arg1[0] is _GENERAL: - tmp = apply_node_operation(node, (arg1[1].mult, div)) - if tmp != tmp: - # This catches if the multiplier division results in nan - return _CONSTANT, tmp - arg1[1].mult = tmp - return arg1 - elif arg1[0] is _CONSTANT: - return _CONSTANT, apply_node_operation(node, (arg1[1], div)) - elif arg1[0] is _CONSTANT and not arg1[1]: - return _CONSTANT, 0 - nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn( - visitor.template.division - ) - nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) - return (_GENERAL, visitor.Result(0, None, nonlin)) - - -def handle_pow_node(visitor, node, arg1, arg2): - if arg2[0] is _CONSTANT: - if arg1[0] is _CONSTANT: - ans = apply_node_operation(node, (arg1[1], arg2[1])) - if ans.__class__ in native_complex_types: - ans = complex_number_error(ans, visitor, node) - return _CONSTANT, ans - elif not arg2[1]: - return _CONSTANT, 1 - elif arg2[1] == 1: - return arg1 - nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn(visitor.template.pow) - nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) - return (_GENERAL, visitor.Result(0, None, nonlin)) - - -def handle_abs_node(visitor, node, arg1): - if arg1[0] is _CONSTANT: - return (_CONSTANT, abs(arg1[1])) - nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn(visitor.template.abs) - return (_GENERAL, visitor.Result(0, None, nonlin)) - - -def handle_unary_node(visitor, node, arg1): - if arg1[0] is _CONSTANT: - return _CONSTANT, apply_node_operation(node, (arg1[1],)) - nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn( - visitor.template.unary[node.name] - ) - return (_GENERAL, visitor.Result(0, None, nonlin)) - - -def handle_exprif_node(visitor, node, arg1, arg2, arg3): - if arg1[0] is _CONSTANT: - if arg1[1]: - return arg2 - else: - return arg3 - nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn(visitor.template.exprif) - nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) - nonlin = visitor.node_result_to_amplrepn(arg3).compile_repn(*nonlin) - return (_GENERAL, visitor.Result(0, None, nonlin)) - - -def handle_equality_node(visitor, node, arg1, arg2): - if arg1[0] is _CONSTANT and arg2[0] is _CONSTANT: - return (_CONSTANT, arg1[1] == arg2[1]) - nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn( - visitor.template.equality - ) - nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) - return (_GENERAL, visitor.Result(0, None, nonlin)) - - -def handle_inequality_node(visitor, node, arg1, arg2): - if arg1[0] is _CONSTANT and arg2[0] is _CONSTANT: - return (_CONSTANT, node._apply_operation((arg1[1], arg2[1]))) - nonlin = visitor.node_result_to_amplrepn(arg1).compile_repn( - visitor.template.strict_inequality_map[node.strict] - ) - nonlin = visitor.node_result_to_amplrepn(arg2).compile_repn(*nonlin) - return (_GENERAL, visitor.Result(0, None, nonlin)) - - -def handle_ranged_inequality_node(visitor, node, arg1, arg2, arg3): - if arg1[0] is _CONSTANT and arg2[0] is _CONSTANT and arg3[0] is _CONSTANT: - return (_CONSTANT, node._apply_operation((arg1[1], arg2[1], arg3[1]))) - op = visitor.template.strict_inequality_map[node.strict] - nl, args, named = visitor.node_result_to_amplrepn(arg1).compile_repn( - visitor.template.and_expr + op[0] - ) - nl2, args2, named = visitor.node_result_to_amplrepn(arg2).compile_repn( - '', None, named - ) - nl += nl2 + op[1] + nl2 - args.extend(args2) - args.extend(args2) - nonlin = visitor.node_result_to_amplrepn(arg3).compile_repn(nl, args, named) - return (_GENERAL, visitor.Result(0, None, nonlin)) - - -def handle_named_expression_node(visitor, node, arg1): - _id = id(node) - # Note that while named subexpressions ('defined variables' in the - # ASL NL file vernacular) look like variables, they are not allowed - # to appear in the 'linear' portion of a constraint / objective - # definition. We will return this as a "var" template, but - # wrapped in the nonlinear portion of the expression tree. - repn = visitor.node_result_to_amplrepn(arg1) - - # A local copy of the expression source list. This will be updated - # later if the same Expression node is encountered in another - # expression tree. - # - # This is a 3-tuple [con_id, obj_id, substitute_expression]. If the - # expression is used by more than 1 constraint / objective, then the - # id is set to 0. If it is not used by any, then it is None. - # substitute_expression is a bool indicating if this named - # subexpression tree should be directly substituted into any - # expression tree that references this node (i.e., do NOT emit the V - # line). - expression_source = [None, None, False] - # Record this common expression - visitor.subexpression_cache[_id] = ( - # 0: the "component" that generated this expression ID - node, - # 1: the common subexpression (to be written out) - repn, - # 2: the source usage information for this subexpression: - # [(con_id, obj_id, substitute); see above] - expression_source, - ) - - # As we will eventually need the compiled form of any nonlinear - # expression, we will go ahead and compile it here. We do not - # do the same for the linear component as we will only need the - # linear component compiled to a dict if we are emitting the - # original (linear + nonlinear) V line (which will not happen if - # the V line is part of a larger linear operator). - if repn.nonlinear.__class__ is list: - repn.compile_nonlinear_fragment() - - if not visitor.use_named_exprs: - return _GENERAL, repn.duplicate() - - mult, repn.mult = repn.mult, 1 - if repn.named_exprs is None: - repn.named_exprs = set() - - # When converting this shared subexpression to a (nonlinear) - # node, we want to just reference this subexpression: - repn.nl = (visitor.template.var, (_id,)) - - if repn.nonlinear: - if repn.linear: - # If this expression has both linear and nonlinear - # components, we will follow the ASL convention and break - # the named subexpression into two named subexpressions: one - # that is only the nonlinear component and one that has the - # const/linear component (and references the first). This - # will allow us to propagate linear coefficients up from - # named subexpressions when appropriate. - sub_node = NLFragment(repn, node) - sub_id = id(sub_node) - sub_repn = visitor.Result(0, None, None) - sub_repn.nonlinear = repn.nonlinear - sub_repn.nl = (visitor.template.var, (sub_id,)) - sub_repn.named_exprs = set(repn.named_exprs) - - repn.named_exprs.add(sub_id) - repn.nonlinear = sub_repn.nl - - # See above for the meaning of this source information - nl_info = list(expression_source) - visitor.subexpression_cache[sub_id] = (sub_node, sub_repn, nl_info) - # It is important that the NL subexpression comes before the - # main named expression: re-insert the original named - # expression (so that the nonlinear sub_node comes first - # when iterating over subexpression_cache) - visitor.subexpression_cache[_id] = visitor.subexpression_cache.pop(_id) - else: - nl_info = expression_source - else: - repn.nonlinear = None - if repn.linear: - if ( - not repn.const - and len(repn.linear) == 1 - and next(iter(repn.linear.values())) == 1 - ): - # This Expression holds only a variable (multiplied by - # 1). Do not emit this as a named variable and instead - # just inject the variable where this expression is - # used. - repn.nl = None - expression_source[2] = True - else: - # This Expression holds only a constant. Do not emit this - # as a named variable and instead just inject the constant - # where this expression is used. - repn.nl = None - expression_source[2] = True - - if mult != 1: - repn.const *= mult - if repn.linear: - _lin = repn.linear - for v in repn.linear: - _lin[v] *= mult - if repn.nonlinear: - if mult == -1: - prefix = visitor.template.negation - else: - prefix = visitor.template.multiplier % mult - repn.nonlinear = prefix + repn.nonlinear[0], repn.nonlinear[1] - - if expression_source[2]: - if repn.linear: - assert len(repn.linear) == 1 and not repn.const - return (_MONOMIAL,) + next(iter(repn.linear.items())) - else: - return (_CONSTANT, repn.const) - - return (_GENERAL, repn.duplicate()) - - -def handle_external_function_node(visitor, node, *args): - func = node._fcn._function - # There is a special case for external functions: these are the only - # expressions that can accept string arguments. As we currently pass - # these as 'precompiled' GENERAL AMPLRepns, the normal trap for - # constant subexpressions will miss string arguments. We will catch - # that case here by looking for NL fragments with no variable - # references. Note that the NL fragment is NOT the raw string - # argument that we want to evaluate: the raw string is in the - # `const` field. - if all( - arg[0] is _CONSTANT or (arg[0] is _GENERAL and arg[1].nl and not arg[1].nl[1]) - for arg in args - ): - arg_list = [arg[1] if arg[0] is _CONSTANT else arg[1].const for arg in args] - return _CONSTANT, apply_node_operation(node, arg_list) - if func in visitor.external_functions: - if node._fcn._library != visitor.external_functions[func][1]._library: - raise RuntimeError( - "The same external function name (%s) is associated " - "with two different libraries (%s through %s, and %s " - "through %s). The ASL solver will fail to link " - "correctly." - % ( - func, - visitor.external_functions[func]._library, - visitor.external_functions[func]._library.name, - node._fcn._library, - node._fcn.name, - ) - ) - else: - visitor.external_functions[func] = (len(visitor.external_functions), node._fcn) - comment = f'\t#{node.local_name}' if visitor.symbolic_solver_labels else '' - nl = visitor.template.external_fcn % ( - visitor.external_functions[func][0], - len(args), - comment, - ) - arg_ids = [] - named_exprs = set() - for arg in args: - _id = id(arg) - arg_ids.append(_id) - visitor.subexpression_cache[_id] = ( - arg, - visitor.Result( - 0, - None, - visitor.node_result_to_amplrepn(arg).compile_repn( - named_exprs=named_exprs - ), - ), - (None, None, True), - ) - if not named_exprs: - named_exprs = None - return ( - _GENERAL, - visitor.Result(0, None, (nl + '%s' * len(arg_ids), arg_ids, named_exprs)), - ) - - -_operator_handles = ExitNodeDispatcher( - { - NegationExpression: handle_negation_node, - ProductExpression: handle_product_node, - DivisionExpression: handle_division_node, - PowExpression: handle_pow_node, - AbsExpression: handle_abs_node, - UnaryFunctionExpression: handle_unary_node, - Expr_ifExpression: handle_exprif_node, - EqualityExpression: handle_equality_node, - InequalityExpression: handle_inequality_node, - RangedExpression: handle_ranged_inequality_node, - Expression: handle_named_expression_node, - ExternalFunctionExpression: handle_external_function_node, - # These are handled explicitly in beforeChild(): - # LinearExpression: handle_linear_expression, - # SumExpression: handle_sum_expression, - # - # Note: MonomialTermExpression is only hit when processing NPV - # subexpressions that raise errors (e.g., log(0) * m.x), so no - # special processing is needed [it is just a product expression] - MonomialTermExpression: handle_product_node, - } -) - - -class AMPLBeforeChildDispatcher(BeforeChildDispatcher): - __slots__ = () - - def __init__(self): - # Special linear / summation expressions - self[MonomialTermExpression] = self._before_monomial - self[LinearExpression] = self._before_linear - self[SumExpression] = self._before_general_expression - - @staticmethod - def _record_var(visitor, var): - # We always add all indices to the var_map at once so that - # we can honor deterministic ordering of unordered sets - # (because the user could have iterated over an unordered - # set when constructing an expression, thereby altering the - # order in which we would see the variables) - vm = visitor.var_map - try: - _iter = var.parent_component().values(visitor.sorter) - except AttributeError: - # Note that this only works for the AML, as kernel does not - # provide a parent_component() - _iter = (var,) - for v in _iter: - if v.fixed: - continue - vm[id(v)] = v - - @staticmethod - def _before_string(visitor, child): - visitor.encountered_string_arguments = True - ans = visitor.Result(child, None, None) - ans.nl = (visitor.template.string % (len(child), child), ()) - return False, (_GENERAL, ans) - - @staticmethod - def _before_var(visitor, child): - _id = id(child) - if _id not in visitor.var_map: - if child.fixed: - if _id not in visitor.fixed_vars: - visitor.cache_fixed_var(_id, child) - return False, (_CONSTANT, visitor.fixed_vars[_id]) - _before_child_handlers._record_var(visitor, child) - return False, (_MONOMIAL, _id, 1) - - @staticmethod - def _before_monomial(visitor, child): - # - # The following are performance optimizations for common - # situations (Monomial terms and Linear expressions) - # - arg1, arg2 = child._args_ - if arg1.__class__ not in native_types: - try: - arg1 = visitor.check_constant(visitor.evaluate(arg1), arg1) - except (ValueError, ArithmeticError): - return True, None - - # Trap multiplication by 0 and nan. - if not arg1: - if arg2.fixed: - _id = id(arg2) - if _id not in visitor.fixed_vars: - visitor.cache_fixed_var(id(arg2), arg2) - arg2 = visitor.fixed_vars[_id] - if arg2 != arg2: - deprecation_warning( - f"Encountered {arg1}*{arg2} in expression tree. " - "Mapping the NaN result to 0 for compatibility " - "with the nl_v1 writer. In the future, this NaN " - "will be preserved/emitted to comply with IEEE-754.", - version='6.4.3', - ) - return False, (_CONSTANT, arg1) - - _id = id(arg2) - if _id not in visitor.var_map: - if arg2.fixed: - if _id not in visitor.fixed_vars: - visitor.cache_fixed_var(_id, arg2) - return False, (_CONSTANT, arg1 * visitor.fixed_vars[_id]) - _before_child_handlers._record_var(visitor, arg2) - return False, (_MONOMIAL, _id, arg1) - - @staticmethod - def _before_linear(visitor, child): - # Because we are going to modify the LinearExpression in this - # walker, we need to make a copy of the arg list from the original - # expression tree. - var_map = visitor.var_map - const = 0 - linear = {} - for arg in child.args: - if arg.__class__ is MonomialTermExpression: - arg1, arg2 = arg._args_ - if arg1.__class__ not in native_types: - try: - arg1 = visitor.check_constant(visitor.evaluate(arg1), arg1) - except (ValueError, ArithmeticError): - return True, None - - # Trap multiplication by 0 and nan. - if not arg1: - if arg2.fixed: - arg2 = visitor.check_constant(arg2.value, arg2) - if arg2 != arg2: - deprecation_warning( - f"Encountered {arg1}*{str(arg2.value)} in expression " - "tree. Mapping the NaN result to 0 for compatibility " - "with the nl_v1 writer. In the future, this NaN " - "will be preserved/emitted to comply with IEEE-754.", - version='6.4.3', - ) - continue - - _id = id(arg2) - if _id not in var_map: - if arg2.fixed: - if _id not in visitor.fixed_vars: - visitor.cache_fixed_var(_id, arg2) - const += arg1 * visitor.fixed_vars[_id] - continue - _before_child_handlers._record_var(visitor, arg2) - linear[_id] = arg1 - elif _id in linear: - linear[_id] += arg1 - else: - linear[_id] = arg1 - elif arg.__class__ in native_types: - const += arg - elif arg.is_variable_type(): - _id = id(arg) - if _id not in var_map: - if arg.fixed: - if _id not in visitor.fixed_vars: - visitor.cache_fixed_var(_id, arg) - const += visitor.fixed_vars[_id] - continue - _before_child_handlers._record_var(visitor, arg) - linear[_id] = 1 - elif _id in linear: - linear[_id] += 1 - else: - linear[_id] = 1 - else: - try: - const += visitor.check_constant(visitor.evaluate(arg), arg) - except (ValueError, ArithmeticError): - return True, None - - if linear: - return False, (_GENERAL, visitor.Result(const, linear, None)) - else: - return False, (_CONSTANT, const) - - @staticmethod - def _before_named_expression(visitor, child): - _id = id(child) - if _id in visitor.subexpression_cache: - obj, repn, info = visitor.subexpression_cache[_id] - if info[2]: - if repn.linear: - return False, (_MONOMIAL, next(iter(repn.linear)), 1) - else: - return False, (_CONSTANT, repn.const) - return False, (_GENERAL, repn.duplicate()) - else: - return True, None - - -_before_child_handlers = AMPLBeforeChildDispatcher() - - -class AMPLRepnVisitor(StreamBasedExpressionVisitor): - def __init__( - self, - subexpression_cache, - external_functions, - var_map, - used_named_expressions, - symbolic_solver_labels, - use_named_exprs, - sorter, - ): - super().__init__() - self.subexpression_cache = subexpression_cache - self.external_functions = external_functions - self.active_expression_source = None - self.var_map = var_map - self.used_named_expressions = used_named_expressions - self.symbolic_solver_labels = symbolic_solver_labels - self.use_named_exprs = use_named_exprs - self.encountered_string_arguments = False - self.fixed_vars = {} - self._eval_expr_visitor = _EvaluationVisitor(True) - self.evaluate = self._eval_expr_visitor.dfs_postorder_stack - self.sorter = sorter - - if symbolic_solver_labels: - self.Result = DebugAMPLRepn - else: - self.Result = AMPLRepn - self.template = self.Result.template - - def check_constant(self, ans, obj): - if ans.__class__ not in native_numeric_types: - # None can be returned from uninitialized Var/Param objects - if ans is None: - return InvalidNumber( - None, f"'{obj}' evaluated to a nonnumeric value '{ans}'" - ) - if ans.__class__ is InvalidNumber: - return ans - elif ans.__class__ in native_complex_types: - return complex_number_error(ans, self, obj) - else: - # It is possible to get other non-numeric types. Most - # common are bool and 1-element numpy.array(). We will - # attempt to convert the value to a float before - # proceeding. - # - # TODO: we should check bool and warn/error (while bool is - # convertible to float in Python, they have very - # different semantic meanings in Pyomo). - try: - ans = float(ans) - except: - return InvalidNumber( - ans, f"'{obj}' evaluated to a nonnumeric value '{ans}'" - ) - if ans != ans: - return InvalidNumber( - nan, f"'{obj}' evaluated to a nonnumeric value '{ans}'" - ) - return ans - - def cache_fixed_var(self, _id, child): - val = self.check_constant(child.value, child) - lb, ub = child.bounds - if (lb is not None and lb - val > TOL) or (ub is not None and ub - val < -TOL): - raise InfeasibleConstraintException( - "model contains a trivially infeasible " - f"variable '{child.name}' (fixed value " - f"{val} outside bounds [{lb}, {ub}])." - ) - self.fixed_vars[_id] = self.check_constant(child.value, child) - - def node_result_to_amplrepn(self, data): - if data[0] is _GENERAL: - return data[1] - elif data[0] is _MONOMIAL: - _, v, c = data - if c: - return self.Result(0, {v: c}, None) - else: - return self.Result(0, None, None) - elif data[0] is _CONSTANT: - return self.Result(data[1], None, None) - else: - raise DeveloperError("unknown result type") - - def initializeWalker(self, expr): - expr, src, src_idx, self.expression_scaling_factor = expr - self.active_expression_source = (src_idx, id(src)) - walk, result = self.beforeChild(None, expr, 0) - if not walk: - return False, self.finalizeResult(result) - return True, expr - - def beforeChild(self, node, child, child_idx): - return _before_child_handlers[child.__class__](self, child) - - def enterNode(self, node): - # SumExpression are potentially large nary operators. Directly - # populate the result - if node.__class__ in sum_like_expression_types: - data = self.Result(0, {}, None) - data.nonlinear = [] - return node.args, data - else: - return node.args, [] - - def exitNode(self, node, data): - if data.__class__ is self.Result: - # If the summation resulted in a constant, return the constant - if data.linear or data.nonlinear or data.nl: - return (_GENERAL, data) - else: - return (_CONSTANT, data.const) - # - # General expressions... - # - return _operator_handles[node.__class__](self, node, *data) - - def finalizeResult(self, result): - ans = self.node_result_to_amplrepn(result) - - # Multiply the expression by the scaling factor provided by the caller - ans.mult *= self.expression_scaling_factor - - # If this was a nonlinear named expression, and that expression - # has no linear portion, then we will directly use this as a - # named expression. We need to mark that the expression was - # used and return it as a simple nonlinear expression pointing - # to this named expression. In all other cases, we will return - # the processed representation (which will reference the - # nonlinear-only named subexpression - if it exists - but not - # this outer named expression). This prevents accidentally - # recharacterizing variables that only appear linearly as - # nonlinear variables. - if ans.nl is not None: - if not ans.nl[1]: - raise ValueError("Numeric expression resolved to a string constant") - # This *is* a named subexpression. If there is no linear - # component, then replace this expression with the named - # expression. The mult will be handled later. We know that - # the const is built into the nonlinear expression, because - # it cannot be changed "in place" (only through addition, - # which would have "cleared" the nl attribute) - if not ans.linear: - ans.named_exprs.update(ans.nl[1]) - ans.nonlinear = ans.nl - ans.const = 0 - else: - # This named expression has both a linear and a - # nonlinear component, and possibly a multiplier and - # constant. We will not include this named expression - # and instead will expose the components so that linear - # variables are not accidentally re-characterized as - # nonlinear. - pass - ans.nl = None - - if ans.nonlinear.__class__ is list: - ans.compile_nonlinear_fragment() - - if not ans.linear: - ans.linear = {} - if ans.mult != 1: - linear = ans.linear - mult, ans.mult = ans.mult, 1 - ans.const *= mult - if linear: - for k in linear: - linear[k] *= mult - if ans.nonlinear: - if mult == -1: - prefix = self.template.negation - else: - prefix = self.template.multiplier % mult - ans.nonlinear = prefix + ans.nonlinear[0], ans.nonlinear[1] - # - self.active_expression_source = None - return ans - - -def _evaluate_constant_nl(nl, external_functions): - expr = nl.splitlines() - stack = [] - while expr: - line = expr.pop() - tokens = line.split() - # remove tokens after the first comment - for i, t in enumerate(tokens): - if t.startswith('#'): - tokens = tokens[:i] - break - if len(tokens) != 1: - # skip blank lines - if not tokens: - continue - if tokens[0][0] == 'f': - # external function - fid, nargs = tokens - fid = int(fid[1:]) - nargs = int(nargs) - fcn_id, ef = external_functions[fid] - assert fid == fcn_id - stack.append(ef.evaluate(tuple(stack.pop() for i in range(nargs)))) - continue - raise DeveloperError( - f"Unsupported line format _evaluate_constant_nl() " - f"(we expect each line to contain a single token): '{line}'" - ) - term = tokens[0] - # the "command" can be determined by the first character on the line - cmd = term[0] - # Note that we will unpack the line into the expected number of - # explicit arguments as a form of error checking - if cmd == 'n': - # numeric constant - stack.append(float(term[1:])) - elif cmd == 'o': - # operator - nargs, fcn = nl_operators[int(term[1:])] - if nargs is None: - nargs = int(stack.pop()) - stack.append(fcn(*(stack.pop() for i in range(nargs)))) - elif cmd in '1234567890': - # this is either a single int (e.g., the nargs in a nary - # sum) or a string argument. Preserve it as-is until later - # when we know which we are expecting. - stack.append(term) - elif cmd == 'h': - stack.append(term.split(':', 1)[1]) - else: - raise DeveloperError( - f"Unsupported NL operator in _evaluate_constant_nl(): '{line}'" - ) - assert len(stack) == 1 - return stack[0] diff --git a/pyomo/repn/tests/ampl/test_ampl_nl.py b/pyomo/repn/tests/ampl/test_ampl_nl.py index 53a2d3cda82..53c34c4c5db 100644 --- a/pyomo/repn/tests/ampl/test_ampl_nl.py +++ b/pyomo/repn/tests/ampl/test_ampl_nl.py @@ -31,11 +31,9 @@ from ..nl_diff import load_and_compare_nl_baseline import pyomo.repn.plugins.ampl.ampl_ as ampl_ -import pyomo.repn.plugins.nl_writer as nl_writer +from pyomo.repn.ampl import text_nl_debug_template as template gsr = ampl_.generate_standard_repn -template = nl_writer.text_nl_debug_template - thisdir = this_file_dir() diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 336e96b93f3..0bfdb22a2ba 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -53,10 +53,6 @@ class INFO(object): def __init__(self, symbolic=False): - if symbolic: - self.template = nl_writer.text_nl_debug_template - else: - self.template = nl_writer.text_nl_template self.subexpression_cache = {} self.external_functions = {} self.var_map = {} @@ -72,6 +68,7 @@ def __init__(self, symbolic=False): True, None, ) + self.template = self.visitor.template def __enter__(self): return self diff --git a/pyomo/repn/tests/nl_diff.py b/pyomo/repn/tests/nl_diff.py index aa2b4519db3..736be4f9606 100644 --- a/pyomo/repn/tests/nl_diff.py +++ b/pyomo/repn/tests/nl_diff.py @@ -15,9 +15,7 @@ from difflib import SequenceMatcher, unified_diff from pyomo.repn.tests.diffutils import compare_floats, load_baseline -import pyomo.repn.plugins.nl_writer as nl_writer - -template = nl_writer.text_nl_debug_template +from pyomo.repn.ampl import text_nl_debug_template as template _norm_whitespace = re.compile(r'[^\S\n]+') _norm_integers = re.compile(r'(?m)\.0+$') From 9c3f5bbb0efb1744c6ac53a18afbd2a14382dd50 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 26 Jul 2024 12:16:00 -0600 Subject: [PATCH 2018/3044] NFC: apply black --- pyomo/repn/plugins/nl_writer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 2e5a5484657..6f340cc887f 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1998,4 +1998,3 @@ def _write_v_line(self, expr_id, k): ostream.write(f'{column_order[_id]} {linear[_id]!s}\n') self._write_nl_expression(info[1], True) self.next_V_line_id += 1 - From 0ea60d254b433eb12ae8151410a31ede360b9830 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 26 Jul 2024 12:26:07 -0600 Subject: [PATCH 2019/3044] Promulgate pyros feasibility tolerance workaround to ampl repn visitor --- pyomo/contrib/pyros/util.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index ecabca8f115..5ade304c077 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -38,7 +38,8 @@ from pyomo.core.expr import value from pyomo.core.expr.numeric_expr import NPV_MaxExpression, NPV_MinExpression from pyomo.repn.standard_repn import generate_standard_repn -from pyomo.repn.plugins import nl_writer as pyomo_nl_writer +import pyomo.repn.plugins.nl_writer as pyomo_nl_writer +import pyomo.repn.ampl as pyomo_ampl_repn from pyomo.core.expr.visitor import ( identify_variables, identify_mutable_parameters, @@ -1824,8 +1825,9 @@ def call_solver(model, solver, config, timing_obj, timer_name, err_msg): # e.g., a Var fixed outside bounds beyond the Pyomo NL writer # tolerance, but still within the default IPOPT feasibility # tolerance - current_nl_writer_tol = pyomo_nl_writer.TOL + current_nl_writer_tol = pyomo_nl_writer.TOL, pyomo_ampl_repn.TOL pyomo_nl_writer.TOL = 1e-4 + pyomo_ampl_repn.TOL = 1e-4 try: results = solver.solve( @@ -1845,7 +1847,7 @@ def call_solver(model, solver, config, timing_obj, timer_name, err_msg): results.solver, TIC_TOC_SOLVE_TIME_ATTR, tt_timer.toc(msg=None, delta=True) ) finally: - pyomo_nl_writer.TOL = current_nl_writer_tol + pyomo_nl_writer.TOL, pyomo_ampl_repn.TOL = current_nl_writer_tol timing_obj.stop_timer(timer_name) revert_solver_max_time_adjustment( From c9956acc760404defee1182ef6ade8c5c826d389 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Fri, 26 Jul 2024 16:48:05 -0600 Subject: [PATCH 2020/3044] grammar --- pyomo/contrib/pynumero/interfaces/cyipopt_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py index 26b43c37d1c..5187efadac9 100644 --- a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py +++ b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py @@ -316,8 +316,8 @@ def __init__(self, nlp, intermediate_callback=None, halt_on_evaluation_error=Non # we support this by adding the Problem object to the args we pass to a user's # callback. To preserve backwards compatibility, we inspect the user's # callback to infer whether they want this argument. To preserve backwards - # if the user asked for variable-length *args, we only pass the Problem as - # an argument if their callback asks for exactly 13 arguments. + # compatibility if the user asked for variable-length *args, we do not pass + # the Problem object as an argument in this case. # A more maintainable solution may be to force users to accept **kwds if they # want "extra info." If we find ourselves continuing to augment this callback, # this may be worth considering. -RBP From ac479032cb52dc14708bcd8d499642b874e76332 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Jul 2024 05:36:58 -0600 Subject: [PATCH 2021/3044] Rework _initialize and avoid redundant call to iter() --- pyomo/core/base/set.py | 36 ++++++++++++++----------------- pyomo/core/tests/unit/test_set.py | 9 ++++++++ 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 87e9549ec8d..e8ac978c355 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1386,29 +1386,27 @@ def set_value(self, val): self.update(val) def _initialize(self, val): - self.update(val) + try: + self.update(val) + except TypeError as e: + if 'not iterable' in str(e): + logger.error( + "Initializer for Set %s returned non-iterable object " + "of type %s." + % ( + self.name, + (val if val.__class__ is type else type(val).__name__), + ) + ) + raise def update(self, values): - # _values was initialized above... - # # Special case: set operations that are not first attached # to the model must be constructed. if isinstance(values, SetOperator): values.construct() - try: - val_iter = iter(values) - except TypeError: - logger.error( - "Initializer for Set %s%s returned non-iterable object " - "of type %s." - % ( - self.name, - ("[%s]" % (index,) if self.is_indexed() else ""), - (values if values.__class__ is type else type(values).__name__), - ) - ) - raise - + # It is important that val_iter is an actual iterator + val_iter = iter(values) if self._dimen is not None: if normalize_index.flatten: val_iter = self._cb_normalized_dimen_verifier(self._dimen, val_iter) @@ -1472,8 +1470,6 @@ def _cb_validate(self, validate, block, val_iter): yield value def _cb_normalized_dimen_verifier(self, dimen, val_iter): - # It is important that the iterator is an actual iterator - val_iter = iter(val_iter) for value in val_iter: if value.__class__ is tuple: if dimen == len(value): @@ -1833,7 +1829,7 @@ def _initialize(self, val): "This WILL potentially lead to nondeterministic behavior " "in Pyomo" % (self.name, type(val).__name__) ) - super().update(val) + super()._initialize(val) def set_value(self, val): if type(val) in Set._UnorderedInitializers: diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index ff0aaa5600b..7fe7d1fd7ae 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -3811,6 +3811,7 @@ def I_init(m): self.assertEqual(m.I.data(), (4, 3, 2, 1)) self.assertEqual(m.I.dimen, 1) + def test_initialize_with_noniterable(self): output = StringIO() with LoggingIntercept(output, 'pyomo.core'): with self.assertRaisesRegex(TypeError, "'int' object is not iterable"): @@ -3819,6 +3820,14 @@ def I_init(m): ref = "Initializer for Set I returned non-iterable object of type int." self.assertIn(ref, output.getvalue()) + output = StringIO() + with LoggingIntercept(output, 'pyomo.core'): + with self.assertRaisesRegex(TypeError, "'int' object is not iterable"): + m = ConcreteModel() + m.I = Set([1,2], initialize=5) + ref = "Initializer for Set I[1] returned non-iterable object of type int." + self.assertIn(ref, output.getvalue()) + def test_scalar_indexed_api(self): m = ConcreteModel() m.I = Set(initialize=range(3)) From 9aab8c2e487cefd8f0ed05ef19351cf19ed6e88b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Jul 2024 05:37:35 -0600 Subject: [PATCH 2022/3044] Duplicate _update_impl workaround from ordered sets in sorted sets --- pyomo/core/base/set.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index e8ac978c355..b62f61f8557 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1897,8 +1897,12 @@ def __reversed__(self): def _update_impl(self, values): for val in values: + # Note that we reset _ordered_values within the loop because + # of an old example where the initializer rule makes + # reference to values previously inserted into the Set + # (which triggered the creation of the _ordered_values) + self._ordered_values = None self._values[val] = None - self._ordered_values = None # Note: removing data does not affect the sorted flag # def remove(self, val): From 9364337b9589d378a47ccd33774f789b78f55563 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 29 Jul 2024 08:19:02 -0400 Subject: [PATCH 2023/3044] Make pretriangularization mandatory again --- pyomo/contrib/pyros/config.py | 14 ---- pyomo/contrib/pyros/util.py | 153 +++++++++++++++++----------------- 2 files changed, 76 insertions(+), 91 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index f73095b9db0..c02dcd7ed0f 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -814,20 +814,6 @@ def pyros_config(): # ================================================ # === Advanced Options # ================================================ - CONFIG.declare( - "skip_pretriangularization", - ConfigValue( - default=True, - domain=bool, - description=( - """ - True to skip pretriangularization of the equality - constraints to determine nonadjustable variables - during preprocessing, False otherwise. - """ - ), - ), - ) CONFIG.declare( "bypass_local_separation", ConfigValue( diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index a6020e798c8..cc5d5e1b3bd 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -1257,88 +1257,87 @@ def get_effective_var_partitioning(model_data, config): " the variable is fixed by domain/bounds" ) - if not config.skip_pretriangularization: - uncertain_params_set = ComponentSet(working_model.uncertain_params) - - # determine constraints that are potentially applicable for - # pretriangularization - certain_eq_cons = ComponentSet() - for wcon in working_model.component_data_objects(Constraint, active=True): - if not wcon.equality: - continue - uncertain_params_in_expr = ( - ComponentSet(identify_mutable_parameters(wcon.expr)) - & uncertain_params_set - ) - if uncertain_params_in_expr: - continue - certain_eq_cons.add(wcon) + uncertain_params_set = ComponentSet(working_model.uncertain_params) - pretriangular_con_var_map = ComponentMap() - for num_passes in it.count(1): - config.progress_logger.debug( - f"Performing pass number {num_passes} over the certain constraints." - ) - new_pretriangular_con_var_map = ComponentMap() - for ccon in certain_eq_cons: - vars_in_con = ComponentSet(identify_variables(ccon.body - ccon.upper)) - adj_vars_in_con = vars_in_con - nonadjustable_var_set - - # conditions for pretriangularization of constraint - # with no uncertain params: - # - only one nonadjustable variable in the constraint - # - the nonadjustable variable appears only linearly, - # and the linear coefficient exceeds our specified - # tolerance. - if len(adj_vars_in_con) == 1: - adj_var_in_con = next(iter(adj_vars_in_con)) - ccon_expr_repn = generate_standard_repn( - expr=ccon.body - ccon.upper, - quadratic=False, - compute_values=True, - ) - adj_var_appears_linearly = ( - adj_var_in_con - not in ComponentSet(ccon_expr_repn.nonlinear_vars) - and adj_var_in_con in ComponentSet(ccon_expr_repn.linear_vars) + # determine constraints that are potentially applicable for + # pretriangularization + certain_eq_cons = ComponentSet() + for wcon in working_model.component_data_objects(Constraint, active=True): + if not wcon.equality: + continue + uncertain_params_in_expr = ( + ComponentSet(identify_mutable_parameters(wcon.expr)) + & uncertain_params_set + ) + if uncertain_params_in_expr: + continue + certain_eq_cons.add(wcon) + + pretriangular_con_var_map = ComponentMap() + for num_passes in it.count(1): + config.progress_logger.debug( + f"Performing pass number {num_passes} over the certain constraints." + ) + new_pretriangular_con_var_map = ComponentMap() + for ccon in certain_eq_cons: + vars_in_con = ComponentSet(identify_variables(ccon.body - ccon.upper)) + adj_vars_in_con = vars_in_con - nonadjustable_var_set + + # conditions for pretriangularization of constraint + # with no uncertain params: + # - only one nonadjustable variable in the constraint + # - the nonadjustable variable appears only linearly, + # and the linear coefficient exceeds our specified + # tolerance. + if len(adj_vars_in_con) == 1: + adj_var_in_con = next(iter(adj_vars_in_con)) + ccon_expr_repn = generate_standard_repn( + expr=ccon.body - ccon.upper, + quadratic=False, + compute_values=True, + ) + adj_var_appears_linearly = ( + adj_var_in_con + not in ComponentSet(ccon_expr_repn.nonlinear_vars) + and adj_var_in_con in ComponentSet(ccon_expr_repn.linear_vars) + ) + if adj_var_appears_linearly: + # get coefficient by summation just in case + # standard repn does not simplify completely + var_linear_coeff = sum( + lcoeff + for lvar, lcoeff + in zip( + ccon_expr_repn.linear_vars, ccon_expr_repn.linear_coefs + ) + if lvar is adj_var_in_con ) - if adj_var_appears_linearly: - # get coefficient by summation just in case - # standard repn does not simplify completely - var_linear_coeff = sum( - lcoeff - for lvar, lcoeff - in zip( - ccon_expr_repn.linear_vars, ccon_expr_repn.linear_coefs - ) - if lvar is adj_var_in_con + if abs(var_linear_coeff) > PRETRIANGULAR_VAR_COEFF_TOL: + new_pretriangular_con_var_map[ccon] = adj_var_in_con + config.progress_logger.debug( + f" The variable {adj_var_in_con.name!r} is " + "made nonadjustable by the pretriangular constraint " + f"{ccon.name!r}." ) - if abs(var_linear_coeff) > PRETRIANGULAR_VAR_COEFF_TOL: - new_pretriangular_con_var_map[ccon] = adj_var_in_con - config.progress_logger.debug( - f" The variable {adj_var_in_con.name!r} is " - "made nonadjustable by the pretriangular constraint " - f"{ccon.name!r}." - ) - - nonadjustable_var_set.update(new_pretriangular_con_var_map.values()) - pretriangular_con_var_map.update(new_pretriangular_con_var_map) - if not new_pretriangular_con_var_map: - config.progress_logger.debug( - "No new pretriangular constraint/variable pairs found. " - "Terminating pretriangularization loop." - ) - break - for pcon in new_pretriangular_con_var_map: - certain_eq_cons.remove(pcon) + nonadjustable_var_set.update(new_pretriangular_con_var_map.values()) + pretriangular_con_var_map.update(new_pretriangular_con_var_map) + if not new_pretriangular_con_var_map: + config.progress_logger.debug( + "No new pretriangular constraint/variable pairs found. " + "Terminating pretriangularization loop." + ) + break - pretriangular_vars = ComponentSet(pretriangular_con_var_map.values()) - config.progress_logger.debug( - f"Identified {len(pretriangular_con_var_map)} pretriangular " - f"constraints and {len(pretriangular_vars)} pretriangular variables " - f"in {num_passes} passes over the certain constraints." - ) + for pcon in new_pretriangular_con_var_map: + certain_eq_cons.remove(pcon) + + pretriangular_vars = ComponentSet(pretriangular_con_var_map.values()) + config.progress_logger.debug( + f"Identified {len(pretriangular_con_var_map)} pretriangular " + f"constraints and {len(pretriangular_vars)} pretriangular variables " + f"in {num_passes} passes over the certain constraints." + ) effective_first_stage_vars = list(nonadjustable_var_set) effective_second_stage_vars = [ From 6f0d5f431ec21b75ed9f2a21217ac056708fd5c4 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 29 Jul 2024 09:21:39 -0400 Subject: [PATCH 2024/3044] Modify online docs methodology section --- doc/OnlineDocs/contributed_packages/pyros.rst | 83 ++++++++++--------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index 95049eded8a..fbbeb220fc2 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -14,19 +14,17 @@ The developers gratefully acknowledge support from the U.S. Department of Energy Methodology Overview ----------------------------- -Below is an overview of the type of optimization models PyROS can accommodate. +PyROS can accommodate optimization models with: +* **continuous variables** only +* **nonlinearities** (including **nonconvexities**) in both the + variables and uncertain parameters +* **equality constraints** defining state variables, + including implicitly defined state variables that cannot be + eliminated from the model via reformulation +* **first-stage degrees of freedom** and **second-stage degrees of freedom** -* PyROS is suitable for optimization models of **continuous variables** - that may feature non-linearities (including **non-convexities**) in - both the variables and uncertain parameters. -* PyROS can handle **equality constraints** defining state variables, - including implicit state variables that cannot be eliminated via - reformulation. -* PyROS allows for **two-stage** optimization problems that may - feature both first-stage and second-stage degrees of freedom. - -PyROS is designed to operate on deterministic models of the general form +Supported deterministic models can be written in the general form .. _deterministic-model: @@ -39,20 +37,21 @@ PyROS is designed to operate on deterministic models of the general form where: -* :math:`x \in \mathcal{X}` are the "design" variables - (i.e., first-stage degrees of freedom), - where :math:`\mathcal{X} \subseteq \mathbb{R}^{n_x}` is the feasible space defined by the model constraints +* :math:`x \in \mathcal{X}` are the first-stage degrees of freedom, + (or "design" variables,) + of which the feasible space :math:`\mathcal{X} \subseteq \mathbb{R}^{n_x}` + is defined by the model constraints (including variable bounds specifications) referencing :math:`x` only. -* :math:`z \in \mathbb{R}^{n_z}` are the "control" variables - (i.e., second-stage degrees of freedom) +* :math:`z \in \mathbb{R}^{n_z}` are the second-stage degrees of freedom + (or "control" variables) * :math:`y \in \mathbb{R}^{n_y}` are the "state" variables * :math:`q \in \mathbb{R}^{n_q}` is the vector of model parameters considered uncertain, and :math:`q^{\text{nom}}` is the vector of nominal values - associated with those. -* :math:`f_1\left(x\right)` are the terms of the objective function that depend + associated with those +* :math:`f_1\left(x\right)` is the summand of the objective function that depends only on design variables -* :math:`f_2\left(x, z, y; q\right)` are the terms of the objective function - that depend on all variables and the uncertain parameters +* :math:`f_2\left(x, z, y; q\right)` is the summand of the objective function + that depends on all variables and the uncertain parameters * :math:`g_i\left(x, z, y; q\right)` is the :math:`i^\text{th}` inequality constraint function in set :math:`\mathcal{I}` (see :ref:`Note `) @@ -63,23 +62,13 @@ where: .. _var-bounds-to-ineqs: .. note:: - PyROS accepts models in which bounds are directly imposed on - ``Var`` objects representing components of the variables :math:`z` - and :math:`y`. These models are cast to - :ref:`the form above ` - by reformulating the bounds as inequality constraints. - -.. _unique-mapping: + PyROS accepts models in which there are: -.. note:: - A key requirement of PyROS is that each value of :math:`\left(x, z, q \right)` - maps to a unique value of :math:`y`, a property that is assumed to - be properly enforced by the system of equality constraints - :math:`\mathcal{J}`. - If the mapping is not unique, then the selection of 'state' - (i.e., not degree of freedom) variables :math:`y` is incorrect, - and one or more of the :math:`y` variables should be appropriately - redesignated to be part of either :math:`x` or :math:`z`. + 1. Bounds declared on the ``Var`` objects representing + components of the variable vectors :math:`z` and :math:`y`. + These bounds are reformulated to inequality constraints. + 2. Ranged inequality constraints. These are easily reformulated to + single inequality constraints. In order to cast the robust optimization counterpart of the :ref:`deterministic model `, @@ -89,7 +78,8 @@ any realization in a compact uncertainty set the nominal value :math:`q^{\text{nom}}`. The set :math:`\mathcal{Q}` may be **either continuous or discrete**. -Based on the above notation, the form of the robust counterpart addressed by PyROS is +Based on the above notation, +the form of the robust counterpart addressed by PyROS is .. math:: \begin{array}{ccclll} @@ -102,8 +92,25 @@ Based on the above notation, the form of the robust counterpart addressed by PyR PyROS solves problems of this form using the Generalized Robust Cutting-Set algorithm developed in [Isenberg_et_al]_. +When using PyROS, please consider citing that paper. -When using PyROS, please consider citing the above paper. +.. _unique-mapping: + +.. note:: + A key requirement of PyROS is that + for every + :math:`x \in \mathcal{X}`, + :math:`z \in \mathbb{R}^{n_z}`, + :math:`q \in \mathcal{Q}`, + there exists a unique :math:`y \in \mathbb{R}^{n_y}` + for which :math:`(x, z, y, q)` + satisfies the equality constraints + :math:`h_j(x, z, y, q) = 0\,\,\forall\, j \in \mathcal{J}`. + If this requirement is not met, + then the selection of 'state' + (i.e., not degree of freedom) variables :math:`y` is incorrect, + and one or more of the :math:`y` variables should be appropriately + redesignated to be part of either :math:`x` or :math:`z`. PyROS Required Inputs ----------------------------- From 26488f38c49961d5b8c6ac5b847096c720971edb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Jul 2024 07:26:58 -0600 Subject: [PATCH 2025/3044] Catch (and test) an edge case when flattening sets --- pyomo/core/base/set.py | 19 +++++++++++-------- pyomo/core/tests/unit/test_set.py | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index b62f61f8557..045fc134f74 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1471,16 +1471,19 @@ def _cb_validate(self, validate, block, val_iter): def _cb_normalized_dimen_verifier(self, dimen, val_iter): for value in val_iter: - if value.__class__ is tuple: - if dimen == len(value): - yield value[0] if dimen == 1 else value + if value.__class__ in native_types: + if dimen == 1: + yield value continue - elif dimen == 1 and value.__class__ in native_types: - yield value - continue + normalized_value = value + else: + normalized_value = normalize_index(value) + # Note: normalize_index() will never return a 1-tuple + if normalized_value.__class__ is tuple: + if dimen == len(normalized_value): + yield normalized_value[0] if dimen == 1 else normalized_value + continue - # Note: normalize_index() will never return a 1-tuple - normalized_value = normalize_index(value) _d = len(normalized_value) if normalized_value.__class__ is tuple else 1 if _d == dimen: yield normalized_value diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 7fe7d1fd7ae..2a2651ca554 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -5256,6 +5256,21 @@ def Bindex(m): self.assertIs(m.K.index_set()._domain, Integers) self.assertEqual(m.K.index_set(), [0, 1, 2, 3, 4]) + def test_normalize_index(self): + try: + _oldFlatten = normalize_index.flatten + normalize_index.flatten = True + + m = ConcreteModel() + with self.assertRaisesRegex( + ValueError, + r"The value=\(\(2, 3\),\) has dimension 2 and is not " + "valid for Set I which has dimen=1", + ): + m.I = Set(initialize=[1, ((2, 3),)]) + finally: + normalize_index.flatten = _oldFlatten + def test_no_normalize_index(self): try: _oldFlatten = normalize_index.flatten From 0fc78ad21925e647da1f58483e2ab37be48f716a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Jul 2024 07:27:25 -0600 Subject: [PATCH 2026/3044] NFC: apply black --- pyomo/core/tests/unit/test_set.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 2a2651ca554..8c0360d618b 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -3824,7 +3824,7 @@ def test_initialize_with_noniterable(self): with LoggingIntercept(output, 'pyomo.core'): with self.assertRaisesRegex(TypeError, "'int' object is not iterable"): m = ConcreteModel() - m.I = Set([1,2], initialize=5) + m.I = Set([1, 2], initialize=5) ref = "Initializer for Set I[1] returned non-iterable object of type int." self.assertIn(ref, output.getvalue()) From 49a53d89cef82445091afb352a59129914b73f0e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Jul 2024 07:46:49 -0600 Subject: [PATCH 2027/3044] NFC: fix doc typo Co-authored-by: Bethany Nicholson --- pyomo/core/base/constraint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 5a9d1da5af1..b79bc178e80 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -191,7 +191,7 @@ def to_bounded_expression(self): Note ---- As this method operates on the *current state* of the - expression, the any required expression manipulations (and by + expression, any required expression manipulations (and by extension, the result) can change after fixing / unfixing :py:class:`Var` objects. From 21502959923070e54076d943647fcaa723a224cc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Jul 2024 07:47:04 -0600 Subject: [PATCH 2028/3044] NFC: fix doc typo Co-authored-by: Bethany Nicholson --- pyomo/core/base/constraint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index b79bc178e80..bc9a32f5404 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -200,7 +200,7 @@ def to_bounded_expression(self): ValueError: Raised if the expression cannot be mapped to this form (i.e., :py:class:`RangedExpression` constraints with - variable lower of upper bounds. + variable lower or upper bounds. """ expr = self._expr From d8900b296b698b29d5b285dd115cd899014994c0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Jul 2024 08:10:54 -0600 Subject: [PATCH 2029/3044] Remove redundant header, simplify imports --- pyomo/contrib/sensitivity_toolbox/sens.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/pyomo/contrib/sensitivity_toolbox/sens.py b/pyomo/contrib/sensitivity_toolbox/sens.py index a3d69b2c7b1..818f13cb789 100644 --- a/pyomo/contrib/sensitivity_toolbox/sens.py +++ b/pyomo/contrib/sensitivity_toolbox/sens.py @@ -9,16 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -# ______________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License -# ______________________________________________________________________________ from pyomo.environ import ( Param, Var, @@ -36,6 +26,7 @@ from pyomo.core.expr import ExpressionReplacementVisitor from pyomo.common.modeling import unique_component_name +from pyomo.common.dependencies import numpy as np, scipy from pyomo.common.deprecation import deprecated from pyomo.common.tempfiles import TempfileManager from pyomo.opt import SolverFactory, SolverStatus @@ -44,8 +35,6 @@ import os import io import shutil -from pyomo.common.dependencies import numpy as np, numpy_available -from pyomo.common.dependencies import scipy, scipy_available logger = logging.getLogger('pyomo.contrib.sensitivity_toolbox') From bb17e9c9bc115238c34ff67fd9e3354eff63c782 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Jul 2024 08:11:41 -0600 Subject: [PATCH 2030/3044] Update constraint processing to leverage new Constraint internal storage --- pyomo/contrib/sensitivity_toolbox/sens.py | 35 +++++++++++++---------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/sensitivity_toolbox/sens.py b/pyomo/contrib/sensitivity_toolbox/sens.py index 818f13cb789..34fbb92327a 100644 --- a/pyomo/contrib/sensitivity_toolbox/sens.py +++ b/pyomo/contrib/sensitivity_toolbox/sens.py @@ -24,6 +24,7 @@ from pyomo.common.sorting import sorted_robust from pyomo.core.expr import ExpressionReplacementVisitor +from pyomo.core.expr.numvalue import is_potentially_variable from pyomo.common.modeling import unique_component_name from pyomo.common.dependencies import numpy as np, scipy @@ -673,25 +674,29 @@ def _replace_parameters_in_constraints(self, variableSubMap): ) last_idx = 0 for con in old_con_list: - if con.equality or con.lower is None or con.upper is None: - new_expr = param_replacer.walk_expression(con.expr) - block.constList.add(expr=new_expr) + new_expr = param_replacer.walk_expression(con.expr) + # TODO: We could only create new constraints for expressions + # where substitution actually happened, but that breaks some + # current tests: + # + # if new_expr is con.expr: + # # No params were substituted. We can ignore this constraint + # continue + if new_expr.nargs() == 3 and ( + is_potentially_variable(new_expr.arg(0)) + or is_potentially_variable(new_expr.arg(2)) + ): + # This is a potentially "invalid" range constraint: it + # may now have variables in the bounds. For safety, we + # will split it into two simple inequalities. + block.constList.add(expr=(new_expr.arg(0) <= new_expr.arg(1))) last_idx += 1 new_old_comp_map[block.constList[last_idx]] = con - else: - # Constraint must be a ranged inequality, break into - # separate constraints - new_body = param_replacer.walk_expression(con.body) - new_lower = param_replacer.walk_expression(con.lower) - new_upper = param_replacer.walk_expression(con.upper) - - # Add constraint for lower bound - block.constList.add(expr=(new_lower <= new_body)) + block.constList.add(expr=(new_expr.arg(1) <= new_expr.arg(2))) last_idx += 1 new_old_comp_map[block.constList[last_idx]] = con - - # Add constraint for upper bound - block.constList.add(expr=(new_body <= new_upper)) + else: + block.constList.add(expr=new_expr) last_idx += 1 new_old_comp_map[block.constList[last_idx]] = con con.deactivate() From 7df3801d9679d88e4f195fd38bc6f0a6750a08c2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 29 Jul 2024 08:28:11 -0600 Subject: [PATCH 2031/3044] Avoid duplicate logging of unordered Set data warning --- pyomo/core/base/set.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index fadd0c3e7d0..e4a6d13e96e 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1387,7 +1387,10 @@ def set_value(self, val): def _initialize(self, val): try: - self.update(val) + # We want to explicitly call the update() on *this class* to + # bypass potential double logging of the use of unordered + # data with ordered Sets + FiniteSetData.update(self, val) except TypeError as e: if 'not iterable' in str(e): logger.error( From a1c14abfbb1a0efc0e48362809091e40fafa2de0 Mon Sep 17 00:00:00 2001 From: whart222 Date: Mon, 29 Jul 2024 08:44:12 -0600 Subject: [PATCH 2032/3044] Resolving PR issues --- .../contrib/alternative_solutions/__init__.py | 11 ++++ .../alternative_solutions/aos_utils.py | 13 +++-- pyomo/contrib/alternative_solutions/balas.py | 11 ++++ .../contrib/alternative_solutions/lp_enum.py | 2 +- .../alternative_solutions/lp_enum_solnpool.py | 11 ++-- pyomo/contrib/alternative_solutions/obbt.py | 11 ++++ .../alternative_solutions/shifted_lp.py | 2 +- .../contrib/alternative_solutions/solnpool.py | 10 ++-- .../contrib/alternative_solutions/solution.py | 11 ++++ .../alternative_solutions/tests/__init__.py | 10 ++++ .../tests/test_aos_utils.py | 22 +++++--- .../alternative_solutions/tests/test_balas.py | 40 ++++++++------ .../alternative_solutions/tests/test_cases.py | 16 ++++-- .../tests/test_lp_enum.py | 38 ++++++++------ .../tests/test_lp_enum_solnpool.py | 39 +++++++++----- .../alternative_solutions/tests/test_obbt.py | 52 +++++++++++-------- .../tests/test_shifted_lp.py | 31 ++++++----- .../tests/test_solnpool.py | 36 ++++++++----- .../tests/test_solution.py | 11 ++++ 19 files changed, 251 insertions(+), 126 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/__init__.py b/pyomo/contrib/alternative_solutions/__init__.py index 2dc7e153117..fae0c7f79c0 100644 --- a/pyomo/contrib/alternative_solutions/__init__.py +++ b/pyomo/contrib/alternative_solutions/__init__.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from pyomo.contrib.alternative_solutions.solution import Solution from pyomo.contrib.alternative_solutions.solnpool import gurobi_generate_solutions from pyomo.contrib.alternative_solutions.balas import enumerate_binary_solutions diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index 3f5169f1037..6418931c440 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,14 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -try: +from pyomo.common.dependencies import numpy as numpy, numpy_available + +if numpy_available: import numpy.random from numpy.linalg import norm - numpy_available = True -except: - numpy_available = False - import pyomo.environ as pe from pyomo.common.modeling import unique_component_name from pyomo.common.collections import ComponentSet @@ -121,7 +119,6 @@ def _get_random_direction(num_dimensions): Get a unit vector of dimension num_dimensions by sampling from and normalizing a standard multivariate Gaussian distribution. """ - global rng iterations = 1000 min_norm = 1e-4 idx = 0 @@ -195,6 +192,8 @@ def get_model_variables( Boolean indicating that integer variables should be included. include_fixed : boolean Boolean indicating that fixed variables should be included. + quiet : boolean + Boolean that is True if all output is suppressed. Returns ------- diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index 3bd8675fca4..16a494cd067 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pe from pyomo.common.collections import ComponentSet from pyomo.contrib.alternative_solutions import Solution diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index 2c066d35cd6..9a85decae07 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py index 651ea57f4d2..b9ee63e9347 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,12 +9,9 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -try: - from gurobipy import GRB +from pyomo.common.dependencies import attempt_import - gurobi_available = True -except: - gurobi_available = False +gurobipy, gurobi_available = attempt_import("gurobipy") import pyomo.environ as pe from pyomo.contrib.alternative_solutions import aos_utils, shifted_lp, solution @@ -43,6 +40,8 @@ def __init__( self.num_solutions = num_solutions def cut_generator_callback(self, cb_m, cb_opt, cb_where): + from gurobipy import GRB + if cb_where == GRB.Callback.MIPSOL: cb_opt.cbGetSolution(vars=self.variables) print("***FOUND SOLUTION***") diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 5c611c3ab5d..99e66b2876d 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pe from pyomo.contrib.alternative_solutions import aos_utils from pyomo.contrib.alternative_solutions import Solution diff --git a/pyomo/contrib/alternative_solutions/shifted_lp.py b/pyomo/contrib/alternative_solutions/shifted_lp.py index 4014e151640..1575306a9e3 100644 --- a/pyomo/contrib/alternative_solutions/shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/shifted_lp.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 8e039da7e5d..929fed447f6 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# Copyright (c) 2008-2024 # National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain @@ -9,12 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -try: - import gurobipy +from pyomo.common.dependencies import attempt_import + +gurobipy, gurobipy_available = attempt_import("gurobipy") - gurobi_available = True -except: - gurobi_available = False import pyomo.environ as pe from pyomo.contrib import appsi import pyomo.contrib.alternative_solutions.aos_utils as aos_utils diff --git a/pyomo/contrib/alternative_solutions/solution.py b/pyomo/contrib/alternative_solutions/solution.py index c215880cc0e..82b6ce01d96 100644 --- a/pyomo/contrib/alternative_solutions/solution.py +++ b/pyomo/contrib/alternative_solutions/solution.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import json import pyomo.environ as pe from pyomo.common.collections import ComponentMap, ComponentSet diff --git a/pyomo/contrib/alternative_solutions/tests/__init__.py b/pyomo/contrib/alternative_solutions/tests/__init__.py index e69de29bb2d..a4a626013c4 100644 --- a/pyomo/contrib/alternative_solutions/tests/__init__.py +++ b/pyomo/contrib/alternative_solutions/tests/__init__.py @@ -0,0 +1,10 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ diff --git a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py index f49d91f4747..ca7edbe8f75 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py +++ b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py @@ -1,11 +1,17 @@ -import pytest +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ -try: - from numpy.linalg import norm +from pyomo.common import unittest - numpy_available = True -except: - numpy_available = False +from pyomo.common.dependencies import numpy as numpy, numpy_available import pyomo.environ as pe import pyomo.common.unittest as unittest @@ -144,11 +150,13 @@ def test_max_both_obj_constraint2(self): self.assertEqual(None, cons[1].upper) self.assertEqual(9, cons[1].lower) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_random_direction(self): """ Ensure that _get_random_direction returns a normal vector. """ + from numpy.linalg import norm + vector = au._get_random_direction(10) self.assertAlmostEqual(1.0, norm(vector)) diff --git a/pyomo/contrib/alternative_solutions/tests/test_balas.py b/pyomo/contrib/alternative_solutions/tests/test_balas.py index 8c30a3e3871..54dea42a2e7 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_balas.py +++ b/pyomo/contrib/alternative_solutions/tests/test_balas.py @@ -1,15 +1,23 @@ -import pytest +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ -try: - from numpy.testing import assert_array_almost_equal - - numpy_available = True -except: - numpy_available = False from collections import Counter +from pyomo.common.dependencies import numpy as numpy, numpy_available + +if numpy_available: + from numpy.testing import assert_array_almost_equal + import pyomo.environ as pe -import pyomo.common.unittest as unittest +from pyomo.common import unittest import pyomo.opt from pyomo.contrib.alternative_solutions import enumerate_binary_solutions @@ -17,7 +25,7 @@ solvers = list(pyomo.opt.check_available_solvers("glpk", "gurobi", "appsi_gurobi")) -pytestmark = pytest.mark.parametrize("mip_solver", solvers) +pytestmark = unittest.pytest.mark.parametrize("mip_solver", solvers) @unittest.pytest.mark.default @@ -32,7 +40,7 @@ def test_ip_feasibility(self, mip_solver): m = tc.get_triangle_ip() results = enumerate_binary_solutions(m, num_solutions=100, solver=mip_solver) assert len(results) == 1 - assert results[0].objective_value == pytest.approx(5) + assert results[0].objective_value == unittest.pytest.approx(5) def Xtest_no_time(self, mip_solver): """ @@ -41,12 +49,12 @@ def Xtest_no_time(self, mip_solver): Check that something sensible happens when the solver times out. """ m = tc.get_triangle_ip() - with pytest.raises(Exception): + with unittest.pytest.raises(Exception): results = enumerate_binary_solutions( m, num_solutions=100, solver=mip_solver, solver_options={"TimeLimit": 0} ) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_knapsack_all(self, mip_solver): """ Enumerate solutions for a binary problem: knapsack @@ -63,7 +71,7 @@ def test_knapsack_all(self, mip_solver): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, m.num_ranked_solns) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_knapsack_x0_x1(self, mip_solver): """ Enumerate solutions for a binary problem: knapsack @@ -83,7 +91,7 @@ def test_knapsack_x0_x1(self, mip_solver): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, [1, 1, 1, 1]) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_knapsack_optimal_3(self, mip_solver): """ Enumerate solutions for a binary problem: knapsack @@ -98,7 +106,7 @@ def test_knapsack_optimal_3(self, mip_solver): ) assert_array_almost_equal(objectives, m.ranked_solution_values[:3]) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_knapsack_hamming_3(self, mip_solver): """ Enumerate solutions for a binary problem: knapsack @@ -115,7 +123,7 @@ def test_knapsack_hamming_3(self, mip_solver): ) assert_array_almost_equal(objectives, [6, 3, 1]) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_knapsack_random_3(self, mip_solver): """ Enumerate solutions for a binary problem: knapsack diff --git a/pyomo/contrib/alternative_solutions/tests/test_cases.py b/pyomo/contrib/alternative_solutions/tests/test_cases.py index 0ad6be85f11..2cac807ca7e 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_cases.py +++ b/pyomo/contrib/alternative_solutions/tests/test_cases.py @@ -1,11 +1,19 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from itertools import product from math import ceil, floor from collections import Counter -try: - import numpy as np -except: - pass +from pyomo.common.dependencies import numpy as np import pyomo.environ as pe diff --git a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py index 66f75a70654..6357de0828a 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py +++ b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py @@ -1,14 +1,18 @@ -import pytest - -try: - import numpy +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ - numpy_available = True -except: - numpy_available = False +from pyomo.common.dependencies import numpy as numpy, numpy_available import pyomo.environ as pe -import pyomo.common.unittest as unittest +from pyomo.common import unittest import pyomo.opt import pyomo.contrib.alternative_solutions.tests.test_cases as tc @@ -20,7 +24,7 @@ solvers = list( pyomo.opt.check_available_solvers("glpk", "gurobi") ) # , "appsi_gurobi")) -pytestmark = pytest.mark.parametrize("mip_solver", solvers) +pytestmark = unittest.pytest.mark.parametrize("mip_solver", solvers) timelimit = {"gurobi": "TimeLimit", "appsi_gurobi": "TimeLimit", "glpk": "tmlim"} @@ -34,7 +38,7 @@ def Xtest_no_time(self, mip_solver): more restrictive bounds are implied by the constraints. """ m = tc.get_3d_polyhedron_problem() - with pytest.raises(Exception): + with unittest.pytest.raises(Exception): lp_enum.enumerate_linear_solutions( m, solver=mip_solver, solver_options={timelimit[mip_solver]: 0} ) @@ -47,7 +51,7 @@ def test_3d_polyhedron(self, mip_solver): sols = lp_enum.enumerate_linear_solutions(m, solver=mip_solver) assert len(sols) == 2 for s in sols: - assert s.objective_value == pytest.approx(4) + assert s.objective_value == unittest.pytest.approx(4) def test_3d_polyhedron(self, mip_solver): m = tc.get_3d_polyhedron_problem() @@ -57,9 +61,9 @@ def test_3d_polyhedron(self, mip_solver): sols = lp_enum.enumerate_linear_solutions(m, solver=mip_solver) assert len(sols) == 2 for s in sols: - assert s.objective_value == pytest.approx( + assert s.objective_value == unittest.pytest.approx( 9 - ) or s.objective_value == pytest.approx(10) + ) or s.objective_value == unittest.pytest.approx(10) def test_2d_diamond_problem(self, mip_solver): m = tc.get_2d_diamond_problem() @@ -67,10 +71,10 @@ def test_2d_diamond_problem(self, mip_solver): assert len(sols) == 2 for s in sols: print(s) - assert sols[0].objective_value == pytest.approx(6.789473684210527) - assert sols[1].objective_value == pytest.approx(3.6923076923076916) + assert sols[0].objective_value == unittest.pytest.approx(6.789473684210527) + assert sols[1].objective_value == unittest.pytest.approx(3.6923076923076916) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_pentagonal_pyramid(self, mip_solver): n = tc.get_pentagonal_pyramid_mip() n.o.sense = pe.minimize @@ -84,7 +88,7 @@ def test_pentagonal_pyramid(self, mip_solver): print(s) assert len(sols) == 6 - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_pentagon(self, mip_solver): n = tc.get_pentagonal_lp() diff --git a/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py index 013cb6e2be2..be6e92d399b 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.environ as pe import pyomo.opt @@ -5,20 +16,22 @@ from pyomo.contrib.alternative_solutions import lp_enum from pyomo.contrib.alternative_solutions import lp_enum_solnpool -try: - import numpy as np +from pyomo.common.dependencies import attempt_import + +numpy, numpy_available = attempt_import("numpy") + +# +# TODO: Setup detailed tests here +# - numpy_available = True -except: - numpy_available = False -if numpy_available: - n = tc.get_pentagonal_pyramid_mip() - n.x.domain = pe.Reals - n.y.domain = pe.Reals +def test_here(): + if numpy_available: + n = tc.get_pentagonal_pyramid_mip() + n.x.domain = pe.Reals + n.y.domain = pe.Reals - sols = lp_enum_solnpool.enumerate_linear_solutions_soln_pool(n, tee=True) + sols = lp_enum_solnpool.enumerate_linear_solutions_soln_pool(n, tee=True) - # for s in sols: - # print(s) - # assert len(sols) == 6 + # TODO - Confirm how solnpools deal with duplicate solutions + assert len(sols) == 7 diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index 0d6b75d0848..65f457e7dd5 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -1,15 +1,23 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import math -import pytest -try: - from numpy.testing import assert_array_almost_equal +from pyomo.common.dependencies import numpy as numpy, numpy_available - numpy_available = True -except: - numpy_available = False +if numpy_available: + from numpy.testing import assert_array_almost_equal import pyomo.environ as pe -import pyomo.common.unittest as unittest +from pyomo.common import unittest import pyomo.opt from pyomo.contrib.alternative_solutions import ( @@ -19,7 +27,7 @@ import pyomo.contrib.alternative_solutions.tests.test_cases as tc solvers = list(pyomo.opt.check_available_solvers("glpk", "gurobi", "appsi_gurobi")) -pytestmark = pytest.mark.parametrize("mip_solver", solvers) +pytestmark = unittest.pytest.mark.parametrize("mip_solver", solvers) timelimit = {"gurobi": "TimeLimit", "appsi_gurobi": "TimeLimit", "glpk": "tmlim"} @@ -27,7 +35,7 @@ @unittest.pytest.mark.default class TestOBBTUnit: - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_obbt_analysis(self, mip_solver): """ Check that the correct bounds are found for a continuous problem. @@ -40,10 +48,10 @@ def test_obbt_analysis(self, mip_solver): def test_obbt_error1(self, mip_solver): m = tc.get_2d_diamond_problem() - with pytest.raises(AssertionError): + with unittest.pytest.raises(AssertionError): obbt_analysis_bounds_and_solutions(m, variables=[m.x], solver=mip_solver) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_obbt_some_vars(self, mip_solver): """ Check that the correct bounds are found for a continuous problem. @@ -57,7 +65,7 @@ def test_obbt_some_vars(self, mip_solver): for var, bounds in all_bounds.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_obbt_continuous(self, mip_solver): """ Check that the correct bounds are found for a continuous problem. @@ -69,7 +77,7 @@ def test_obbt_continuous(self, mip_solver): for var, bounds in all_bounds.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_mip_rel_objective(self, mip_solver): """ Check that relative mip gap constraints are added for a mip with indexed vars and constraints @@ -79,9 +87,9 @@ def test_mip_rel_objective(self, mip_solver): m, rel_opt_gap=0.5, solver=mip_solver ) assert len(solns) == 2 * len(all_bounds) + 1 - assert m._obbt.optimality_tol_rel.lb == pytest.approx(2.5) + assert m._obbt.optimality_tol_rel.lb == unittest.pytest.approx(2.5) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_mip_abs_objective(self, mip_solver): """ Check that absolute mip gap constraints are added @@ -91,9 +99,9 @@ def test_mip_abs_objective(self, mip_solver): m, abs_opt_gap=1.99, solver=mip_solver ) assert len(solns) == 2 * len(all_bounds) + 1 - assert m._obbt.optimality_tol_abs.lb == pytest.approx(3.01) + assert m._obbt.optimality_tol_abs.lb == unittest.pytest.approx(3.01) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_obbt_warmstart(self, mip_solver): """ Check that warmstarting works. @@ -109,7 +117,7 @@ def test_obbt_warmstart(self, mip_solver): for var, bounds in all_bounds.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_obbt_mip(self, mip_solver): """ Check that bound tightening only occurs for continuous variables @@ -134,7 +142,7 @@ def test_obbt_mip(self, mip_solver): assert bounds_tightened assert bounds_not_tightened - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_obbt_unbounded(self, mip_solver): """ Check that the correct bounds are found for an unbounded problem. @@ -151,7 +159,7 @@ def test_obbt_unbounded(self, mip_solver): assert_array_almost_equal(bounds, m.continuous_bounds[var]) assert len(solns) == num - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_bound_tightening(self, mip_solver): """ Check that the correct bounds are found for a discrete problem where @@ -170,7 +178,7 @@ def Xtest_no_time(self, mip_solver): more restrictive bounds are implied by the constraints. """ m = tc.get_implied_bound_ip() - with pytest.raises(RuntimeError): + with unittest.pytest.raises(RuntimeError): obbt_analysis_bounds_and_solutions( m, solver=mip_solver, solver_options={timelimit[mip_solver]: 0} ) @@ -198,7 +206,7 @@ def test_obbt_infeasible(self, mip_solver): """ m = tc.get_2d_diamond_problem() m.infeasible_constraint = pe.Constraint(expr=m.x >= 10) - with pytest.raises(Exception): + with unittest.pytest.raises(Exception): obbt_analysis_bounds_and_solutions(m, solver=mip_solver) diff --git a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py index 3dbd7e82696..ec49d5bfb3e 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py @@ -1,15 +1,22 @@ -import pytest - -try: +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import numpy as numpy, numpy_available + +if numpy_available: from numpy.testing import assert_array_almost_equal - numpy_available = True -except: - numpy_available = False - import pyomo.environ as pe import pyomo.opt -import pyomo.common.unittest as unittest +from pyomo.common import unittest import pyomo.contrib.alternative_solutions.tests.test_cases as tc from pyomo.contrib.alternative_solutions import shifted_lp @@ -22,13 +29,13 @@ solvers = list(pyomo.opt.check_available_solvers("glpk", "gurobi")) if "glpk" in solvers: solver = ["glpk"] -pytestmark = pytest.mark.parametrize("lp_solver", solvers) +pytestmark = unittest.pytest.mark.parametrize("lp_solver", solvers) @unittest.pytest.mark.default class TestShiftedIP: - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_mip_abs_objective(self, lp_solver): m = tc.get_indexed_pentagonal_pyramid_mip() m.x.domain = pe.Reals @@ -41,7 +48,7 @@ def test_mip_abs_objective(self, lp_solver): new_results = opt.solve(new_model, tee=False) new_obj = pe.value(new_model.objective) - assert old_obj == pytest.approx(new_obj) + assert old_obj == unittest.pytest.approx(new_obj) def test_polyhedron(self, lp_solver): m = tc.get_3d_polyhedron_problem() @@ -54,7 +61,7 @@ def test_polyhedron(self, lp_solver): new_results = opt.solve(new_model, tee=False) new_obj = pe.value(new_model.objective) - assert old_obj == pytest.approx(new_obj) + assert old_obj == unittest.pytest.approx(new_obj) if __name__ == "__main__": diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index 3e2558dca69..4bba864207c 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -1,15 +1,23 @@ -import pytest - -try: +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common.dependencies import numpy as numpy, numpy_available + +if numpy_available: from numpy.testing import assert_array_almost_equal - numpy_available = True -except: - numpy_available = False from collections import Counter import pyomo.environ as pe -import pyomo.common.unittest as unittest +from pyomo.common import unittest from pyomo.contrib.alternative_solutions import gurobi_generate_solutions import pyomo.contrib.alternative_solutions.tests.test_cases as tc @@ -31,7 +39,7 @@ class TestSolnPoolUnit(unittest.TestCase): Maybe this should be an AOS utility since it may be a thing we will want to do often. """ - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_ip_feasibility(self): """ Enumerate all solutions for an ip: triangle_ip. @@ -45,7 +53,7 @@ def test_ip_feasibility(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_ip_num_solutions(self): """ Enumerate 8 solutions for an ip: triangle_ip. @@ -60,7 +68,7 @@ def test_ip_num_solutions(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_mip_feasibility(self): """ Enumerate all solutions for a mip: indexed_pentagonal_pyramid_mip. @@ -74,7 +82,7 @@ def test_mip_feasibility(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_mip_rel_feasibility(self): """ Enumerate solutions for a mip: indexed_pentagonal_pyramid_mip. @@ -89,7 +97,7 @@ def test_mip_rel_feasibility(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_mip_rel_feasibility_options(self): """ Enumerate solutions for a mip: indexed_pentagonal_pyramid_mip. @@ -106,7 +114,7 @@ def test_mip_rel_feasibility_options(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def test_mip_abs_feasibility(self): """ Enumerate solutions for a mip: indexed_pentagonal_pyramid_mip. @@ -121,7 +129,7 @@ def test_mip_abs_feasibility(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) - @pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") def Xtest_mip_no_time(self): """ Enumerate solutions for a mip: indexed_pentagonal_pyramid_mip. diff --git a/pyomo/contrib/alternative_solutions/tests/test_solution.py b/pyomo/contrib/alternative_solutions/tests/test_solution.py index 547e8add4e3..9dbddcd3baf 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solution.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solution.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import pyomo.opt import pyomo.environ as pe import pyomo.common.unittest as unittest From a28b7b40ed53a0968e1cef7493a8e2b358ad3858 Mon Sep 17 00:00:00 2001 From: whart222 Date: Mon, 29 Jul 2024 09:21:28 -0600 Subject: [PATCH 2033/3044] Further updates for the PR --- .../contrib/alternative_solutions/lp_enum.py | 9 +++++--- pyomo/contrib/alternative_solutions/obbt.py | 2 -- .../contrib/alternative_solutions/solnpool.py | 4 +++- .../contrib/alternative_solutions/solution.py | 2 +- .../tests/test_solnpool.py | 23 ++++++++++--------- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index 9a85decae07..d6e815b3ec2 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -102,7 +102,7 @@ def enumerate_linear_solutions( "random", "norm", ], 'search mode must be "optimal", "random", or "norm".' - # TODO: Implement thethe random and norm objectives. I think it is sufficient + # TODO: Implement the random and norm objectives. I think it is sufficient # to only consider the cb.var_lower variables in the objective for these two # cases. The cb.var_upper variables are directly linked to these to diversity # in one implies diversity in the other. Diversity in the cb.basic_slack @@ -236,8 +236,6 @@ def enumerate_linear_solutions( if debug: model.pprint() - # print("Writing test{}.lp".format(solution_number)) - # cb.write("test{}.lp".format(solution_number)) if use_appsi: results = opt.solve(model) condition = results.termination_condition @@ -288,8 +286,13 @@ def enumerate_linear_solutions( cb.basic_last_slack, ] + # Number of variables with non-zero values num_non_zero = 0 + # This expression is used to ensure that at least one of the non-zero basic + # variables in the previous solution is selected. force_out_expr = -1 + # This expression is used to ensure that at most (# non-zero basic variables)-1 + # binary choice variables can be selected. non_zero_basic_expr = 1 for idx in range(len(variable_groups)): continuous_var, binary_var, constraint = variable_groups[idx] diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 99e66b2876d..ca4b54d7495 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -72,8 +72,6 @@ def obbt_analysis( A Pyomo ComponentMap containing the bounds for each variable. {variable: (lower_bound, upper_bound)}. An exception is raised when the solver encountered an issue. - solutions - [Solution] """ bounds, solns = obbt_analysis_bounds_and_solutions( model, diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 929fed447f6..b7575a8194f 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -55,6 +55,8 @@ def gurobi_generate_solutions( Solver option-value pairs to be passed to the Gurobi solver. tee : boolean Boolean indicating that the solver output should be displayed. + quiet : boolean + Boolean indicating whether to suppress all output. Returns ------- @@ -64,7 +66,7 @@ def gurobi_generate_solutions( # # Setup gurobi # - if not gurobi_available: + if not gurobipy_available: return [] opt = appsi.solvers.Gurobi() if not opt.available(): # pragma: no cover diff --git a/pyomo/contrib/alternative_solutions/solution.py b/pyomo/contrib/alternative_solutions/solution.py index 82b6ce01d96..777b006e8de 100644 --- a/pyomo/contrib/alternative_solutions/solution.py +++ b/pyomo/contrib/alternative_solutions/solution.py @@ -36,7 +36,7 @@ class Solution: Get a dictionary of variable name-variable value pairs. get_fixed_variable_names(self): Get a list of fixed-variable names. - def get_objective_name_values(self): + get_objective_name_values(self): Get a dictionary of objective name-objective value pairs. """ diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index 4bba864207c..add402097a0 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -13,6 +13,9 @@ if numpy_available: from numpy.testing import assert_array_almost_equal +from pyomo.common.dependencies import attempt_import + +gurobipy, gurobipy_available = attempt_import("gurobipy") from collections import Counter @@ -23,10 +26,8 @@ import pyomo.contrib.alternative_solutions.tests.test_cases as tc -@unittest.skipUnless( - pe.SolverFactory("gurobi").available(), "Gurobi MIP solver not available" -) -@unittest.pytest.mark.solver("gurobi") +@unittest.skipUnless(gurobipy_available, "Gurobi MIP solver not available") +# @unittest.pytest.mark.skipif(not gurobipy_available, reason="Gurobi MIP solver not available") class TestSolnPoolUnit(unittest.TestCase): """ Cases to cover: @@ -39,7 +40,7 @@ class TestSolnPoolUnit(unittest.TestCase): Maybe this should be an AOS utility since it may be a thing we will want to do often. """ - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_ip_feasibility(self): """ Enumerate all solutions for an ip: triangle_ip. @@ -53,7 +54,7 @@ def test_ip_feasibility(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_ip_num_solutions(self): """ Enumerate 8 solutions for an ip: triangle_ip. @@ -68,7 +69,7 @@ def test_ip_num_solutions(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_mip_feasibility(self): """ Enumerate all solutions for a mip: indexed_pentagonal_pyramid_mip. @@ -82,7 +83,7 @@ def test_mip_feasibility(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_mip_rel_feasibility(self): """ Enumerate solutions for a mip: indexed_pentagonal_pyramid_mip. @@ -97,7 +98,7 @@ def test_mip_rel_feasibility(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_mip_rel_feasibility_options(self): """ Enumerate solutions for a mip: indexed_pentagonal_pyramid_mip. @@ -114,7 +115,7 @@ def test_mip_rel_feasibility_options(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_mip_abs_feasibility(self): """ Enumerate solutions for a mip: indexed_pentagonal_pyramid_mip. @@ -129,7 +130,7 @@ def test_mip_abs_feasibility(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def Xtest_mip_no_time(self): """ Enumerate solutions for a mip: indexed_pentagonal_pyramid_mip. From 1ab6d2a07f2ced8e88deff4a940003772868eecd Mon Sep 17 00:00:00 2001 From: whart222 Date: Mon, 29 Jul 2024 10:38:30 -0600 Subject: [PATCH 2034/3044] Fix test errors when gurobi not installed --- .../alternative_solutions/tests/test_lp_enum_solnpool.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py index be6e92d399b..fccac026cf5 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py @@ -19,6 +19,7 @@ from pyomo.common.dependencies import attempt_import numpy, numpy_available = attempt_import("numpy") +gurobipy, gurobi_available = attempt_import("gurobipy") # # TODO: Setup detailed tests here @@ -34,4 +35,7 @@ def test_here(): sols = lp_enum_solnpool.enumerate_linear_solutions_soln_pool(n, tee=True) # TODO - Confirm how solnpools deal with duplicate solutions - assert len(sols) == 7 + if gurobi_available: + assert len(sols) == 7 + else: + assert len(sols) == 0 From 4d1e3460bcd2b5e3d2a5cb83b43e456825b3d1eb Mon Sep 17 00:00:00 2001 From: whart222 Date: Mon, 29 Jul 2024 13:57:08 -0600 Subject: [PATCH 2035/3044] Removing pytest.mark.skipif logic --- .../tests/test_aos_utils.py | 2 +- .../alternative_solutions/tests/test_balas.py | 10 +++++----- .../tests/test_lp_enum.py | 4 ++-- .../alternative_solutions/tests/test_obbt.py | 18 +++++++++--------- .../tests/test_shifted_lp.py | 2 +- .../tests/test_solnpool.py | 3 +-- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py index ca7edbe8f75..625104fa56a 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py +++ b/pyomo/contrib/alternative_solutions/tests/test_aos_utils.py @@ -150,7 +150,7 @@ def test_max_both_obj_constraint2(self): self.assertEqual(None, cons[1].upper) self.assertEqual(9, cons[1].lower) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_random_direction(self): """ Ensure that _get_random_direction returns a normal vector. diff --git a/pyomo/contrib/alternative_solutions/tests/test_balas.py b/pyomo/contrib/alternative_solutions/tests/test_balas.py index 54dea42a2e7..10e2bb8ae65 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_balas.py +++ b/pyomo/contrib/alternative_solutions/tests/test_balas.py @@ -54,7 +54,7 @@ def Xtest_no_time(self, mip_solver): m, num_solutions=100, solver=mip_solver, solver_options={"TimeLimit": 0} ) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_knapsack_all(self, mip_solver): """ Enumerate solutions for a binary problem: knapsack @@ -71,7 +71,7 @@ def test_knapsack_all(self, mip_solver): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, m.num_ranked_solns) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_knapsack_x0_x1(self, mip_solver): """ Enumerate solutions for a binary problem: knapsack @@ -91,7 +91,7 @@ def test_knapsack_x0_x1(self, mip_solver): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, [1, 1, 1, 1]) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_knapsack_optimal_3(self, mip_solver): """ Enumerate solutions for a binary problem: knapsack @@ -106,7 +106,7 @@ def test_knapsack_optimal_3(self, mip_solver): ) assert_array_almost_equal(objectives, m.ranked_solution_values[:3]) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_knapsack_hamming_3(self, mip_solver): """ Enumerate solutions for a binary problem: knapsack @@ -123,7 +123,7 @@ def test_knapsack_hamming_3(self, mip_solver): ) assert_array_almost_equal(objectives, [6, 3, 1]) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_knapsack_random_3(self, mip_solver): """ Enumerate solutions for a binary problem: knapsack diff --git a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py index 6357de0828a..f8bad28e552 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py +++ b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py @@ -74,7 +74,7 @@ def test_2d_diamond_problem(self, mip_solver): assert sols[0].objective_value == unittest.pytest.approx(6.789473684210527) assert sols[1].objective_value == unittest.pytest.approx(3.6923076923076916) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_pentagonal_pyramid(self, mip_solver): n = tc.get_pentagonal_pyramid_mip() n.o.sense = pe.minimize @@ -88,7 +88,7 @@ def test_pentagonal_pyramid(self, mip_solver): print(s) assert len(sols) == 6 - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_pentagon(self, mip_solver): n = tc.get_pentagonal_lp() diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index 65f457e7dd5..91e702a20d9 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -35,7 +35,7 @@ @unittest.pytest.mark.default class TestOBBTUnit: - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_obbt_analysis(self, mip_solver): """ Check that the correct bounds are found for a continuous problem. @@ -51,7 +51,7 @@ def test_obbt_error1(self, mip_solver): with unittest.pytest.raises(AssertionError): obbt_analysis_bounds_and_solutions(m, variables=[m.x], solver=mip_solver) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_obbt_some_vars(self, mip_solver): """ Check that the correct bounds are found for a continuous problem. @@ -65,7 +65,7 @@ def test_obbt_some_vars(self, mip_solver): for var, bounds in all_bounds.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_obbt_continuous(self, mip_solver): """ Check that the correct bounds are found for a continuous problem. @@ -77,7 +77,7 @@ def test_obbt_continuous(self, mip_solver): for var, bounds in all_bounds.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_mip_rel_objective(self, mip_solver): """ Check that relative mip gap constraints are added for a mip with indexed vars and constraints @@ -89,7 +89,7 @@ def test_mip_rel_objective(self, mip_solver): assert len(solns) == 2 * len(all_bounds) + 1 assert m._obbt.optimality_tol_rel.lb == unittest.pytest.approx(2.5) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_mip_abs_objective(self, mip_solver): """ Check that absolute mip gap constraints are added @@ -101,7 +101,7 @@ def test_mip_abs_objective(self, mip_solver): assert len(solns) == 2 * len(all_bounds) + 1 assert m._obbt.optimality_tol_abs.lb == unittest.pytest.approx(3.01) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_obbt_warmstart(self, mip_solver): """ Check that warmstarting works. @@ -117,7 +117,7 @@ def test_obbt_warmstart(self, mip_solver): for var, bounds in all_bounds.items(): assert_array_almost_equal(bounds, m.continuous_bounds[var]) - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_obbt_mip(self, mip_solver): """ Check that bound tightening only occurs for continuous variables @@ -142,7 +142,7 @@ def test_obbt_mip(self, mip_solver): assert bounds_tightened assert bounds_not_tightened - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_obbt_unbounded(self, mip_solver): """ Check that the correct bounds are found for an unbounded problem. @@ -159,7 +159,7 @@ def test_obbt_unbounded(self, mip_solver): assert_array_almost_equal(bounds, m.continuous_bounds[var]) assert len(solns) == num - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_bound_tightening(self, mip_solver): """ Check that the correct bounds are found for a discrete problem where diff --git a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py index ec49d5bfb3e..da17e537914 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/tests/test_shifted_lp.py @@ -35,7 +35,7 @@ @unittest.pytest.mark.default class TestShiftedIP: - @unittest.pytest.mark.skipif(not numpy_available, reason="Numpy not installed") + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_mip_abs_objective(self, lp_solver): m = tc.get_indexed_pentagonal_pyramid_mip() m.x.domain = pe.Reals diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index add402097a0..48727cba121 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -26,8 +26,7 @@ import pyomo.contrib.alternative_solutions.tests.test_cases as tc -@unittest.skipUnless(gurobipy_available, "Gurobi MIP solver not available") -# @unittest.pytest.mark.skipif(not gurobipy_available, reason="Gurobi MIP solver not available") +@unittest.skipIf(not gurobipy_available, "Gurobi MIP solver not available") class TestSolnPoolUnit(unittest.TestCase): """ Cases to cover: From dbf8408083ff23bc1d551c35707a791696929a96 Mon Sep 17 00:00:00 2001 From: whart222 Date: Mon, 29 Jul 2024 14:50:08 -0600 Subject: [PATCH 2036/3044] Fixing doc --- pyomo/contrib/alternative_solutions/solution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/alternative_solutions/solution.py b/pyomo/contrib/alternative_solutions/solution.py index 777b006e8de..7b224e3089b 100644 --- a/pyomo/contrib/alternative_solutions/solution.py +++ b/pyomo/contrib/alternative_solutions/solution.py @@ -25,7 +25,7 @@ class Solution: A map between Pyomo variables and their values for a solution. fixed_vars : ComponentSet The set of Pyomo variables that are fixed in a solution. - objectives : ComponentMap + objective : ComponentMap A map between Pyomo objectives and their values for a solution. Methods From 9f843b4fe7f60e584abf53c80b06ca5f1a8971f8 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 30 Jul 2024 13:27:06 -0600 Subject: [PATCH 2037/3044] correct docstring --- pyomo/core/plugins/transform/scaling.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index 4c427e72b92..c15993524f4 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -29,7 +29,7 @@ class ScaleModel(Transformation): Transformation to scale a model. This plugin performs variable, constraint, and objective scaling on - a model based on the scaling factors in the suffix 'scaling_parameter' + a model based on the scaling factors in the suffix 'scaling_factor' set for the variables, constraints, and/or objective. This is typically done to scale the problem for improved numerical properties. @@ -38,6 +38,10 @@ class ScaleModel(Transformation): * :py:meth:`create_using ` * :py:meth:`propagate_solution ` + By default, scaling components are renamed with the prefix ``scaled_``. To disable + this behavior and scale variables in-place (or keep the same names in a new model), + use the ``rename=False`` argument to ``apply_to`` or ``create_using``. + Examples -------- @@ -70,8 +74,6 @@ class ScaleModel(Transformation): >>> print(value(scaled_model.scaled_obj)) 101.0 - .. todo:: Implement an option to change the variables names or not - """ def __init__(self, **kwds): From 6b377da7611eb1f7bbe5c13bc138092111a89ed1 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 30 Jul 2024 13:31:59 -0600 Subject: [PATCH 2038/3044] test warning --- pyomo/core/tests/transform/test_scaling.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pyomo/core/tests/transform/test_scaling.py b/pyomo/core/tests/transform/test_scaling.py index cb31aaa33ec..c1c8e36904d 100644 --- a/pyomo/core/tests/transform/test_scaling.py +++ b/pyomo/core/tests/transform/test_scaling.py @@ -10,10 +10,12 @@ # ___________________________________________________________________________ # +import io import pyomo.common.unittest as unittest import pyomo.environ as pyo from pyomo.opt.base.solvers import UnknownSolver from pyomo.core.plugins.transform.scaling import ScaleModel +from pyomo.common.log import LoggingIntercept class TestScaleModelTransformation(unittest.TestCase): @@ -699,11 +701,15 @@ def test_propagate_solution_uninitialized_variable(self): scaled_model = pyo.TransformationFactory("core.scale_model").create_using(m) scaled_model.scaled_x[1] = 20.0 scaled_model.scaled_x[2] = None - pyo.TransformationFactory("core.scale_model").propagate_solution( - scaled_model, m - ) + + OUTPUT = io.StringIO() + with LoggingIntercept(OUTPUT, "pyomo.core.plugins.transform.scaling"): + pyo.TransformationFactory("core.scale_model").propagate_solution( + scaled_model, m + ) + self.assertIn("replacing value of variable", OUTPUT.getvalue()) self.assertAlmostEqual(m.x[1].value, 2.0, delta=1e-8) - # Note that value of x[2] in original model *has* been overriddeen to None. + # Note that value of x[2] in original model *has* been overridden to None. # In this case, a warning has been raised. self.assertIs(m.x[2].value, None) From 2c6b76072150f344fc27089fdfd15c367e5dba2c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 30 Jul 2024 15:04:24 -0600 Subject: [PATCH 2039/3044] Adding in an API to get the transformed linear and general nonlinear constraints --- .../piecewise/tests/test_nonlinear_to_pwl.py | 6 +++ .../piecewise/transform/nonlinear_to_pwl.py | 50 ++++++++++++------- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 3a1b5270aea..a06c74b2a51 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -61,6 +61,12 @@ def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): self.assertEqual(new_cons.lb, 0.35) self.assertIs(n_to_pwl.get_src_component(new_cons), m.cons) + quadratic = n_to_pwl.get_transformed_quadratic_constraints(m) + self.assertEqual(len(quadratic), 0) + nonlinear = n_to_pwl.get_transformed_nonlinear_constraints(m) + self.assertEqual(len(nonlinear), 1) + self.assertIn(m.cons, nonlinear) + def test_log_constraint_uniform_grid(self): m = self.make_model() diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 798eaafdddf..bc6ea3a1f7d 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from collections import defaultdict import enum import itertools @@ -49,6 +50,7 @@ from pyomo.gdp import Disjunct, Disjunction from pyomo.network import Port from pyomo.repn.quadratic import QuadraticRepnVisitor +from pyomo.repn.util import ExprType lineartree, lineartree_available = attempt_import('lineartree') sklearn_lm, sklearn_available = attempt_import('sklearn.linear_model') @@ -70,11 +72,12 @@ class DomainPartitioningMethod(enum.IntEnum): class _NonlinearToPWLTransformationData(AutoSlots.Mixin): - __slots__ = ('transformed_component', 'src_component') + __slots__ = ('transformed_component', 'src_component', 'transformed_constraints') def __init__(self): self.transformed_component = ComponentMap() self.src_component = ComponentMap() + self.transformed_constraints = defaultdict(ComponentSet) Block.register_private_data_initializer(_NonlinearToPWLTransformationData) @@ -585,26 +588,33 @@ def _needs_approximating(self, expr, approximate_quadratic): if repn.nonlinear is None: if repn.quadratic is None: # Linear constraint. Always skip. - return False + return ExprType.LINEAR, False else: if not approximate_quadratic: # Didn't need approximated, nothing to do - return False - return True + return ExprType.QUADRATIC, False + return ExprType.QUADRATIC, True + return ExprType.GENERAL, True def _approximate_expression( - self, obj, parent_component, trans_block, config, approximate_quadratic + self, expr, obj, trans_block, config, approximate_quadratic ): - if not self._needs_approximating(obj, approximate_quadratic): + expr_type, needs_approximating = self._needs_approximating( + expr, + approximate_quadratic + ) + if not needs_approximating: return - # Additively decompose obj and work on the pieces + obj.model().private_data().transformed_constraints[expr_type].add(obj) + + # Additively decompose expr and work on the pieces pwl_func = 0 - for k, expr in enumerate( - _additively_decompose_expr(obj) if config.additively_decompose else (obj,) + for k, subexpr in enumerate( + _additively_decompose_expr(expr) if config.additively_decompose else (expr,) ): # First check is this is a good idea - expr_vars = list(identify_variables(expr, include_fixed=False)) + expr_vars = list(identify_variables(subexpr, include_fixed=False)) orig_values = ComponentMap((v, v.value) for v in expr_vars) dim = len(expr_vars) @@ -613,27 +623,27 @@ def _approximate_expression( "Not approximating expression for component '%s' as " "it exceeds the maximum dimension of %s. Try increasing " "'max_dimension' or additively separating the expression." - % (parent_component.name, config.max_dimension) + % (obj.name, config.max_dimension) ) - pwl_func += expr + pwl_func += subexpr continue - elif not self._needs_approximating(expr, approximate_quadratic): - pwl_func += expr + elif not self._needs_approximating(expr, approximate_quadratic)[1]: + pwl_func += subexpr continue def eval_expr(*args): for i, v in enumerate(expr_vars): v.value = args[i] - return value(expr) + return value(subexpr) pwlf = get_pwl_function_approximation( eval_expr, config.domain_partitioning_method, config.num_points, - self._get_bounds_list(expr_vars, parent_component), + self._get_bounds_list(expr_vars, obj), ) name = unique_component_name( - trans_block, parent_component.getname(fully_qualified=False) + trans_block, obj.getname(fully_qualified=False) ) trans_block.add_component(f"_pwle_{name}_{k}", pwlf) pwl_func += pwlf(*expr_vars) @@ -663,3 +673,9 @@ def get_transformed_component(self, cons): "It does not appear that '%s' is a Constraint that was " "transformed by the 'nonlinear_to_pwl' transformation." % cons.name ) + + def get_transformed_nonlinear_constraints(self, model): + return model.private_data().transformed_constraints[ExprType.GENERAL] + + def get_transformed_quadratic_constraints(self, model): + return model.private_data().transformed_constraints[ExprType.QUADRATIC] From aecea50f4092c2bb89c7f811fb637bd85778fcba Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 30 Jul 2024 21:47:19 -0400 Subject: [PATCH 2040/3044] Assert that `FactorModelSet` instances have matrices of full column rank --- pyomo/contrib/pyros/tests/test_separation.py | 18 +- .../pyros/tests/test_uncertainty_sets.py | 256 +++++++++--------- pyomo/contrib/pyros/uncertainty_sets.py | 89 +++--- 3 files changed, 175 insertions(+), 188 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_separation.py b/pyomo/contrib/pyros/tests/test_separation.py index ce484dfdf9b..7f44dc440ee 100644 --- a/pyomo/contrib/pyros/tests/test_separation.py +++ b/pyomo/contrib/pyros/tests/test_separation.py @@ -215,14 +215,14 @@ def test_construct_separation_problem_uncertain_factor_param_components(self): config.uncertainty_set = FactorModelSet( origin=[1, 0], beta=1, - number_of_factors=3, - psi_mat=[[1, 2.5, 1], [0, 1, 0.5]], + number_of_factors=2, + psi_mat=[[1, 2.5], [0, 1]], ) separation_model = construct_separation_problem(model_data, config) uncertainty_blk = separation_model.uncertainty *matrix_product_cons, aux_sum_con = uncertainty_blk.uncertainty_cons_list paramvar1, paramvar2 = uncertainty_blk.uncertain_param_var_list - auxvar1, auxvar2, auxvar3 = uncertainty_blk.auxiliary_var_list + auxvar1, auxvar2 = uncertainty_blk.auxiliary_var_list self.assertEqual(len(matrix_product_cons), 2) self.assertTrue(matrix_product_cons[0].active) @@ -231,17 +231,17 @@ def test_construct_separation_problem_uncertain_factor_param_components(self): assertExpressionsEqual( self, aux_sum_con.expr, - RangedExpression((-3, auxvar1 + auxvar2 + auxvar3, 3), False), + RangedExpression((-2, auxvar1 + auxvar2, 2), False), ) assertExpressionsEqual( self, matrix_product_cons[0].expr, - auxvar1 + 2.5 * auxvar2 + auxvar3 + 1 == paramvar1, + auxvar1 + 2.5 * auxvar2 + 1 == paramvar1, ) assertExpressionsEqual( self, matrix_product_cons[1].expr, - 0.0 * auxvar1 + auxvar2 + 0.5 * auxvar3 == paramvar2, + 0.0 * auxvar1 + auxvar2 == paramvar2, ) # none of the vars should be fixed @@ -249,16 +249,14 @@ def test_construct_separation_problem_uncertain_factor_param_components(self): self.assertFalse(paramvar2.fixed) self.assertFalse(auxvar1.fixed) self.assertFalse(auxvar2.fixed) - self.assertFalse(auxvar3.fixed) # factor set auxiliary variables self.assertEqual(auxvar1.bounds, (-1, 1)) self.assertEqual(auxvar2.bounds, (-1, 1)) - self.assertEqual(auxvar3.bounds, (-1, 1)) # factor set bounds are tighter - self.assertEqual(paramvar1.bounds, (-3.5, 5.5)) - self.assertEqual(paramvar2.bounds, (-1.5, 1.5)) + self.assertEqual(paramvar1.bounds, (-2.5, 4.5)) + self.assertEqual(paramvar2.bounds, (-1.0, 1.0)) if __name__ == "__main__": diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index a8e46c6efb2..e01cd5b2ee9 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -5,7 +5,12 @@ import itertools as it import unittest -from pyomo.common.dependencies import numpy as np, numpy_available, scipy_available +from pyomo.common.dependencies import ( + attempt_import, + numpy as np, + numpy_available, + scipy_available, +) from pyomo.environ import SolverFactory from pyomo.core.base import ( ConcreteModel, @@ -35,9 +40,13 @@ logger = logging.getLogger(__name__) +parameterized, param_available = attempt_import('parameterized') -if not (numpy_available and scipy_available): - raise unittest.SkipTest('PyROS unit tests require parameterized, numpy, and scipy') +if not (numpy_available and scipy_available and param_available): + raise unittest.SkipTest( + 'PyROS preprocessor unit tests require parameterized, numpy, and scipy' + ) +parameterized = parameterized.parameterized # === Config args for testing global_solver = 'baron' @@ -706,10 +715,12 @@ def test_error_on_invalid_number_of_factors(self): """ exc_str = r".*'number_of_factors' must be a positive int \(provided value -1\)" with self.assertRaisesRegex(ValueError, exc_str): - FactorModelSet(origin=[0], number_of_factors=-1, psi_mat=[[1, 1]], beta=0.1) + FactorModelSet( + origin=[0], number_of_factors=-1, psi_mat=[[1, 2], [1, 1]], beta=0.1 + ) fset = FactorModelSet( - origin=[0], number_of_factors=2, psi_mat=[[1, 1]], beta=0.1 + origin=[0, 1], number_of_factors=2, psi_mat=[[1, 2], [1, 1]], beta=0.1 ) exc_str = r".*'number_of_factors' is immutable" @@ -746,67 +757,86 @@ def test_error_on_invalid_beta(self): with self.assertRaisesRegex(ValueError, big_exc_str): fset.beta = big_beta + def test_error_on_rank_deficient_psi_mat(self): + """ + Test exception raised if factor loading matrix `psi_mat` + is rank-deficient. + """ + with self.assertRaisesRegex(ValueError, r"full column rank.*\(2, 3\)"): + # more columns than rows + FactorModelSet( + origin=[0, 0], + number_of_factors=3, + psi_mat=[[1, -1, 1], [1, 0.1, 1]], + beta=1 / 6, + ) + with self.assertRaisesRegex(ValueError, r"full column rank.*\(2, 2\)"): + # linearly dependent columns + FactorModelSet( + origin=[0, 0], + number_of_factors=2, + psi_mat=[[1, -1], [1, -1]], + beta=1 / 6, + ) + @unittest.skipUnless(baron_available, "BARON is not available") - def test_compute_parameter_bounds(self): + @parameterized.expand([ + # map beta to expected parameter bounds + ["beta0", 0, [(-2.0, 2.0), (0.1, 1.9), (-5.0, 9.0), (-4.0, 10.0)]], + ["beta1ov6", 1/6, [(-2.5, 2.5), (-0.4, 2.4), (-8.0, 12.0), (-7.0, 13.0)]], + ["beta1ov3", 1/3, [(-3.0, 3.0), (-0.9, 2.9), (-11.0, 15.0), (-10.0, 16.0)]], + ["beta1ov2", 1/2, [(-3.0, 3.0), (-0.95, 2.95), (-11.5, 15.5), (-10.5, 16.5)]], + ["beta2ov3", 2/3, [(-3.0, 3.0), (-1.0, 3.0), (-12.0, 16.0), (-11.0, 17.0)]], + ["beta7ov9", 7/9, [(-3.0, 3.0), (-31/30, 91/30), (-37/3, 49/3), (-34/3, 52/3)]], + ["beta1", 1, [(-3.0, 3.0), (-1.1, 3.1), (-13.0, 17.0), (-12.0, 18.0)]], + ]) + def test_compute_parameter_bounds(self, name, beta, expected_param_bounds): """ Test parameter bounds computations give expected results. """ solver = SolverFactory("baron") - # cases where prior parameter bounds - # approximations were probably too tight - fset1 = FactorModelSet( - origin=[0, 0], + fset = FactorModelSet( + origin=[0, 1, 2, 3], number_of_factors=3, - psi_mat=[[1, -1, 1], [1, 0.1, 1]], - beta=1 / 6, - ) - fset2 = FactorModelSet( - origin=[0], number_of_factors=3, psi_mat=[[1, 6, 8]], beta=1 / 2 - ) - fset3 = FactorModelSet( - origin=[1], number_of_factors=2, psi_mat=[[1, 2]], beta=1 / 4 - ) - fset4 = FactorModelSet( - origin=[1], number_of_factors=3, psi_mat=[[-1, -6, -8]], beta=1 / 2 - ) - fset5 = FactorModelSet( - origin=[0], number_of_factors=3, psi_mat=[[-1.5, 3, 4]], beta=7 / 9 + psi_mat=[[1, -1, 1], [1, 0.1, 1], [-1, -6, -8], [1, 6, 8]], + beta=beta, ) - # check against hand-calculated bounds - self.assertEqual(fset1.parameter_bounds, [(-2.5, 2.5), (-1.4, 1.4)]) - self.assertEqual(fset2.parameter_bounds, [(-13.5, 13.5)]) - self.assertEqual(fset3.parameter_bounds, [(-0.5, 2.5)]) - self.assertEqual(fset4.parameter_bounds, [(-12.5, 14.5)]) - self.assertEqual(fset5.parameter_bounds, [(-8.5, 8.5)]) + param_bounds = fset.parameter_bounds + # won't be exactly equal, + np.testing.assert_allclose( + param_bounds, + expected_param_bounds, + atol=1e-13, + ) # check parameter bounds matches LP results # exactly for each case - for fset in [fset1, fset2, fset3, fset4]: - param_bounds = fset.parameter_bounds - solver_param_bounds = fset._compute_parameter_bounds(solver) - np.testing.assert_allclose( - param_bounds, - solver_param_bounds, - err_msg=( - "Parameter bounds not consistent with LP values for " - "FactorModelSet with parameterization:\n" - f"F={fset.number_of_factors},\n" - f"beta={fset.beta},\n" - f"psi_mat={fset.psi_mat},\n" - f"origin={fset.origin}." - ), - ) + solver_param_bounds = fset._compute_parameter_bounds(solver) + np.testing.assert_allclose( + solver_param_bounds, + param_bounds, + err_msg=( + "Parameter bounds not consistent with LP values for " + "FactorModelSet with parameterization:\n" + f"F={fset.number_of_factors},\n" + f"beta={fset.beta},\n" + f"psi_mat={fset.psi_mat},\n" + f"origin={fset.origin}." + ), + # account for solver tolerances and numerical errors + atol=1e-4, + ) def test_set_as_constraint(self): """ Test method for setting up constraints works correctly. """ fset = FactorModelSet( - origin=[1, 2], + origin=[0, 1, 2, 3], number_of_factors=3, - psi_mat=[[1, -1, 1], [1, 0.1, 1]], + psi_mat=[[1, -1, 1], [1, 0.1, 1], [-1, -6, -8], [1, 6, 8]], beta=1 / 6, ) uq = fset.set_as_constraint(uncertain_params=None) @@ -818,7 +848,7 @@ def test_set_as_constraint(self): *factor_model_matrix_cons, betaf_abs_val_con = uq.uncertainty_cons - self.assertEqual(len(factor_model_matrix_cons), 2) + self.assertEqual(len(factor_model_matrix_cons), 4) assertExpressionsEqual( self, factor_model_matrix_cons[0].expr, @@ -826,7 +856,6 @@ def test_set_as_constraint(self): uq.auxiliary_vars[0] + (-1.0) * uq.auxiliary_vars[1] + uq.auxiliary_vars[2] - + 1 == uq.uncertain_param_vars[0] ), ) @@ -837,10 +866,32 @@ def test_set_as_constraint(self): uq.auxiliary_vars[0] + 0.1 * uq.auxiliary_vars[1] + uq.auxiliary_vars[2] - + 2 + + 1 == uq.uncertain_param_vars[1] ), ) + assertExpressionsEqual( + self, + factor_model_matrix_cons[2].expr, + ( + (-1.0) * uq.auxiliary_vars[0] + + (-6.0) * uq.auxiliary_vars[1] + + (-8.0) * uq.auxiliary_vars[2] + + 2 + == uq.uncertain_param_vars[2] + ), + ) + assertExpressionsEqual( + self, + factor_model_matrix_cons[3].expr, + ( + (1.0) * uq.auxiliary_vars[0] + + (6.0) * uq.auxiliary_vars[1] + + (8.0) * uq.auxiliary_vars[2] + + 3 + == uq.uncertain_param_vars[3] + ), + ) betaf_abs_val_con = uq.uncertainty_cons[-1] assertExpressionsEqual( @@ -874,7 +925,7 @@ def test_set_as_constraint_type_mismatch(self): with self.assertRaisesRegex(TypeError, ".*valid component type"): box_set.set_as_constraint(uncertain_params=m.p1, block=m) - def test_point_in_set_skinny_psi_matrix(self): + def test_point_in_set(self): """ Test point in set check works if psi matrix is skinny. """ @@ -915,64 +966,24 @@ def test_point_in_set_skinny_psi_matrix(self): self.assertFalse(fset.point_in_set(fset.origin + fset.psi_mat @ [1, -1, -1])) self.assertFalse(fset.point_in_set(fset.origin + fset.psi_mat @ [-1, -1, -1])) - def test_point_in_set_nonskinny_psi_matrix(self): - """ - Test point in set check works if psi matrix is not - skinny. - """ - fset = FactorModelSet( - origin=[0, 0], - number_of_factors=3, - psi_mat=[[1, -1, 1], [1, 0.1, 1]], - beta=1 / 6, - ) - - self.assertTrue(fset.point_in_set(fset.origin)) - - for aux_space_pt in it.permutations([1, 0.5, -1]): - fset_pt_from_crit = fset.origin + fset.psi_mat @ aux_space_pt - self.assertTrue( - fset.point_in_set(fset_pt_from_crit), - msg=( - f"Point {fset_pt_from_crit} generated from critical point " - f"{aux_space_pt} of the auxiliary variable space " - "is not in the set." - ), - ) - - fset_pt_from_neg_crit = fset.origin - fset.psi_mat @ aux_space_pt - self.assertTrue( - fset.point_in_set(fset_pt_from_neg_crit), - msg=( - f"Point {fset_pt_from_neg_crit} generated from critical point " - f"{aux_space_pt} of the auxiliary variable space " - "is not in the set." - ), - ) - - # some points transformed from hypercube vertices. - # no such point should be in this instance of the set - self.assertFalse(fset.point_in_set(fset.origin + fset.psi_mat @ [1, 1, 1])) - self.assertFalse(fset.point_in_set(fset.origin + fset.psi_mat @ [1, 1, -1])) - self.assertFalse(fset.point_in_set(fset.origin + fset.psi_mat @ [1, -1, -1])) - self.assertFalse(fset.point_in_set(fset.origin + fset.psi_mat @ [-1, -1, -1])) - def test_add_bounds_on_uncertain_parameters(self): m = ConcreteModel() - m.uncertain_param_vars = Var([0, 1], initialize=0) + m.uncertain_param_vars = Var(range(4), initialize=0) fset = FactorModelSet( - origin=[0, 0], + origin=[0, 1, 2, 3], number_of_factors=3, - psi_mat=[[1, -1, 1], [1, 0.1, 1]], - beta=1 / 6, + psi_mat=[[1, -1, 1], [1, 0.1, 1], [-1, -6, -8], [1, 6, 8]], + beta=1, ) fset._add_bounds_on_uncertain_parameters( global_solver=None, uncertain_param_vars=m.uncertain_param_vars, ) - self.assertEqual(m.uncertain_param_vars[0].bounds, (-2.5, 2.5)) - self.assertEqual(m.uncertain_param_vars[1].bounds, (-1.4, 1.4)) + self.assertEqual(m.uncertain_param_vars[0].bounds, (-3.0, 3.0)) + self.assertEqual(m.uncertain_param_vars[1].bounds, (-1.1, 3.1)) + self.assertEqual(m.uncertain_param_vars[2].bounds, (-13.0, 17.0)) + self.assertEqual(m.uncertain_param_vars[3].bounds, (-12.0, 18.0)) class TestIntersectionSet(unittest.TestCase): @@ -1160,14 +1171,12 @@ def test_set_as_constraint(self): i_set = IntersectionSet( set1=BoxSet([(-0.5, 0.5), (-0.5, 0.5)]), - # this is just an origin-centered square set2=FactorModelSet( origin=[0, 0], - number_of_factors=3, + number_of_factors=2, beta=0.75, - psi_mat=[[1, 1, 0], [0, 1, 1]], + psi_mat=[[1, 1], [1, 2]], ), - # another origin-centered square set3=CardinalitySet([-0.5, -0.5], [2, 2], 2), # ellipsoid. this is enclosed in all the other sets set4=AxisAlignedEllipsoidalSet([0, 0], [0.25, 0.25]), @@ -1177,7 +1186,7 @@ def test_set_as_constraint(self): self.assertIs(uq.block, m) self.assertEqual(uq.uncertain_param_vars, [m.v1, m.v2]) - self.assertEqual(len(uq.auxiliary_vars), 5) + self.assertEqual(len(uq.auxiliary_vars), 4) self.assertEqual(len(uq.uncertainty_cons), 9) # box set constraints @@ -1197,43 +1206,42 @@ def test_set_as_constraint(self): assertExpressionsEqual( self, uq.uncertainty_cons[2].expr, - aux_vars[0] + aux_vars[1] + 0 * aux_vars[2] == m.v1, + aux_vars[0] + aux_vars[1] == m.v1, ) assertExpressionsEqual( self, uq.uncertainty_cons[3].expr, - 0 * aux_vars[0] + aux_vars[1] + aux_vars[2] == m.v2, + aux_vars[0] + 2 * aux_vars[1] == m.v2, ) assertExpressionsEqual( self, uq.uncertainty_cons[4].expr, RangedExpression( - (-2.25, aux_vars[0] + aux_vars[1] + aux_vars[2], 2.25), + (-1.5, aux_vars[0] + aux_vars[1], 1.5), False, ), ) self.assertEqual(aux_vars[0].bounds, (-1, 1)) self.assertEqual(aux_vars[1].bounds, (-1, 1)) - self.assertEqual(aux_vars[2].bounds, (-1, 1)) # cardinality set constraints assertExpressionsEqual( self, uq.uncertainty_cons[5].expr, - -0.5 + 2 * aux_vars[3] == m.v1, + -0.5 + 2 * aux_vars[2] == m.v1, ) assertExpressionsEqual( self, uq.uncertainty_cons[6].expr, - -0.5 + 2 * aux_vars[4] == m.v2, + -0.5 + 2 * aux_vars[3] == m.v2, ) assertExpressionsEqual( self, uq.uncertainty_cons[7].expr, - sum(aux_vars[3:5]) <= 2, + sum(aux_vars[2:4]) <= 2, ) - self.assertEqual(aux_vars[3].bounds, (0, 1)) - self.assertEqual(uq.auxiliary_vars[4].bounds, (0, 1)) + self.assertEqual(aux_vars[2].bounds, (0, 1)) + self.assertEqual(uq.auxiliary_vars[3].bounds, (0, 1)) # axis-aligned ellipsoid constraint assertExpressionsEqual( @@ -1280,12 +1288,11 @@ def test_compute_parameter_bounds(self): """ i_set = IntersectionSet( set1=BoxSet([(-0.5, 0.5), (-0.5, 0.5)]), - # this is just an origin-centered square set2=FactorModelSet( origin=[0, 0], - number_of_factors=3, + number_of_factors=2, beta=0.75, - psi_mat=[[1, 1, 0], [0, 1, 1]], + psi_mat=[[1, 1], [1, 2]], ), # another origin-centered square set3=CardinalitySet([-0.5, -0.5], [2, 2], 2), @@ -1310,11 +1317,10 @@ def test_point_in_set(self): # this is just an origin-centered square set2=FactorModelSet( origin=[0, 0], - number_of_factors=3, + number_of_factors=2, beta=0.75, - psi_mat=[[1, 1, 0], [0, 1, 1]], + psi_mat=[[1, 1], [1, 2]], ), - # another origin-centered square set3=CardinalitySet([-0.5, -0.5], [2, 2], 2), # ellipsoid. this is enclosed in all the other sets set4=AxisAlignedEllipsoidalSet([0, 0], [0.25, 0.25]), @@ -1339,14 +1345,12 @@ def test_add_bounds_on_uncertain_parameters(self): m.uncertain_param_vars = Var([0, 1], initialize=0) iset = IntersectionSet( set1=BoxSet([(-0.5, 0.5), (-0.5, 0.5)]), - # this is just an origin-centered square set2=FactorModelSet( origin=[0, 0], - number_of_factors=3, + number_of_factors=2, beta=0.75, - psi_mat=[[1, 1, 0], [0, 1, 1]], + psi_mat=[[1, 1], [1, 2]], ), - # another origin-centered square set3=CardinalitySet([-0.5, -0.5], [2, 2], 2), # ellipsoid. this is enclosed in all the other sets set4=AxisAlignedEllipsoidalSet([0, 0], [0.25, 0.25]), @@ -1356,8 +1360,10 @@ def test_add_bounds_on_uncertain_parameters(self): global_solver=SolverFactory("baron"), uncertain_param_vars=m.uncertain_param_vars, ) - self.assertEqual(m.uncertain_param_vars[0].bounds, (-0.25, 0.25)) - self.assertEqual(m.uncertain_param_vars[1].bounds, (-0.25, 0.25)) + + # account for imprecision + np.testing.assert_allclose(m.uncertain_param_vars[0].bounds, (-0.25, 0.25)) + np.testing.assert_allclose(m.uncertain_param_vars[1].bounds, (-0.25, 0.25)) class TestCardinalitySet(unittest.TestCase): diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index ef03bc6de3b..9e81dea5afb 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1791,11 +1791,12 @@ class FactorModelSet(UncertaintySet): Uncertain parameter values around which deviations are restrained. number_of_factors : int - Natural number representing the dimensionality of the + Natural number representing the dimension of the space to which the set projects. psi_mat : (N, F) array_like - Matrix designating each uncertain parameter's contribution to - each factor. Each row is associated with a separate uncertain + Matrix, of full column rank, designating each uncertain + parameter's contribution to each factor. + Each row is associated with a separate uncertain parameter. Each column is associated with a separate factor. Number of columns `F` of `psi_mat` should be equal to `number_of_factors`. @@ -1813,7 +1814,7 @@ class FactorModelSet(UncertaintySet): >>> fset = FactorModelSet( ... origin=np.zeros(4), ... number_of_factors=2, - ... psi_mat=np.full(shape=(4, 2), fill_value=0.1), + ... psi_mat=[[0, 0.1], [0, 0.1], [0.1, 0], [0.1, 0]], ... beta=0.5, ... ) >>> fset.origin @@ -1821,10 +1822,10 @@ class FactorModelSet(UncertaintySet): >>> fset.number_of_factors 2 >>> fset.psi_mat - array([[0.1, 0.1], - [0.1, 0.1], - [0.1, 0.1], - [0.1, 0.1]]) + array([[0. , 0.1], + [0. , 0.1], + [0.1, 0. ], + [0.1, 0. ]]) >>> fset.beta 0.5 """ @@ -1876,13 +1877,15 @@ def origin(self, val): @property def number_of_factors(self): """ - int : Natural number representing the dimensionality `F` + int : Natural number representing the dimension `F` of the space to which the set projects. - This attribute is immutable, and may only be set at - object construction. Typically, the number of factors - is significantly less than the set dimension, but no - restriction to that end is imposed here. + This attribute is immutable, may only be set at + object construction, and must be equal to the number of + columns of the factor loading matrix ``self.psi_mat``. + Therefore, since we also require that ``self.psi_mat`` + be full column rank, `number_of_factors` + must not exceed the set dimension. """ return self._number_of_factors @@ -1903,10 +1906,10 @@ def number_of_factors(self, val): @property def psi_mat(self): """ - (N, F) numpy.ndarray : Matrix designating each - uncertain parameter's contribution to each factor. Each row is - associated with a separate uncertain parameter. Each column with - a separate factor. + (N, F) numpy.ndarray : Factor loading matrix, i.e., a full + column rank matrix for which each entry indicates how strongly + the factor corresponding to the entry's column is related + to the uncertain parameter corresponding to the entry's row. """ return self._psi_mat @@ -1933,13 +1936,13 @@ def psi_mat(self, val): f"(provided shape {psi_mat_arr.shape})" ) - # check values acceptable - for column in psi_mat_arr.T: - if np.allclose(column, 0): - raise ValueError( - "Each column of attribute 'psi_mat' should have at least " - "one nonzero entry" - ) + psi_mat_rank = np.linalg.matrix_rank(psi_mat_arr) + is_full_column_rank = psi_mat_rank == self.number_of_factors + if not is_full_column_rank: + raise ValueError( + "Attribute 'psi_mat' should be full column rank. " + f"(Got a matrix of shape {psi_mat_arr.shape} and rank {psi_mat_rank}.)" + ) self._psi_mat = psi_mat_arr @@ -1954,7 +1957,7 @@ def beta(self): that as many factors will be above 0 as there will be below 0 (i.e., "zero-net-alpha" model). If ``beta = 1``, then the set is numerically equivalent to a `BoxSet` with bounds - ``[origin - psi @ np.ones(F), origin + psi @ np.ones(F)].T``. + ``[self.origin - psi @ np.ones(F), self.origin + psi @ np.ones(F)].T``. """ return self._beta @@ -2078,10 +2081,8 @@ def compute_auxiliary_param_vals(self, point, solver=None): if np.allclose(point_arr, self.origin): return np.zeros(self.number_of_factors), True - is_psi_full_column_rank = ( - self.dim >= self.number_of_factors - and np.linalg.matrix_rank(self.psi_mat) == self.number_of_factors - ) + psi_mat_rank = np.linalg.matrix_rank(self.psi_mat) + is_psi_full_column_rank = psi_mat_rank == self.number_of_factors if is_psi_full_column_rank: # pseudoinverse uniquely determines the auxiliary values pinv_psi = np.linalg.pinv(self.psi_mat) @@ -2094,32 +2095,14 @@ def compute_auxiliary_param_vals(self, point, solver=None): ) return aux_space_pt, is_aux_pt_feasible else: - # there may be multiple feasible values or no feasible - # values. check with LP - res = sp.optimize.linprog( - c=np.zeros(self.number_of_factors), - A_eq=self.psi_mat, - b_eq=point_arr - self.origin, - A_ub=np.vstack( - [np.ones(self.number_of_factors), -np.ones(self.number_of_factors)] - ), - b_ub=np.full(2, self.beta * self.number_of_factors), - bounds=(-1, 1), - method="highs", + # guard against possible changes to individual entries, + # rows, or columns after `psi_mat` setter invoked + raise ValueError( + "Factor loading matrix `psi_mat` must be full column rank. " + f"(There are {self.number_of_factors} factors/columns, but" + f"the matrix is of rank {psi_mat_rank}.)" ) - # check termination - if res.success and res.status == 0: - return res.x, True - elif res.status == 2: - return res.x, False - else: - raise ValueError( - f"Could not conclude whether a solution exists " - "for the feasibility problem." - f" Linprog results:\n {res} " - ) - def point_in_set(self, point): """ Determine whether a given point lies in the factor model set. From 20c2cf00c374294bc4fd75d7a067850f297c2317 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 30 Jul 2024 22:02:44 -0400 Subject: [PATCH 2041/3044] Simplify methods for calculating auxiliary param values --- .../pyros/separation_problem_methods.py | 9 +-- pyomo/contrib/pyros/uncertainty_sets.py | 73 +++++++------------ 2 files changed, 29 insertions(+), 53 deletions(-) diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index bea637c0d56..9ea3fbf2556 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -85,15 +85,14 @@ def add_uncertainty_set_constraints(separation_model, config): global_solver=config.global_solver, ) if aux_vars: - aux_var_vals, aux_var_vals_feasible = ( - config.uncertainty_set.compute_auxiliary_param_vals( + aux_var_vals = ( + config.uncertainty_set.compute_auxiliary_uncertain_param_vals( point=config.nominal_uncertain_param_vals, solver=config.global_solver, ) ) - if aux_var_vals_feasible: - for auxvar, auxval in zip(aux_vars, aux_var_vals): - auxvar.set_value(auxval) + for auxvar, auxval in zip(aux_vars, aux_var_vals): + auxvar.set_value(auxval) # preprocess uncertain parameters which have been fixed by bounds # in order to simplify the separation problems diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 9e81dea5afb..0bab44c1d2d 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -661,9 +661,10 @@ def _add_bounds_on_uncertain_parameters( param_var.setlb(lb) param_var.setub(ub) - def compute_auxiliary_param_vals(self, point, solver=None): + def compute_auxiliary_uncertain_param_vals(self, point, solver=None): """ - Compute auxiliary parameter values for a given point. + Compute auxiliary uncertain parameter values for a given point. + The point need not be in the uncertainty set. Parameters ---------- @@ -676,16 +677,7 @@ def compute_auxiliary_param_vals(self, point, solver=None): Returns ------- aux_space_pt : numpy.ndarray - Computed auxiliary parameter values. - aux_space_pt_feasible : bool - True if conclusion made that auxiliary values are - feasible, False otherwise. - - Raises - ------ - ValueError - If conclusion on feasibility of auxiliary values - cannot be made. + Computed auxiliary uncertain parameter values. """ raise NotImplementedError( f"Auxiliary parameter computation not supported for {type(self).__name__}." @@ -1230,33 +1222,18 @@ def set_as_constraint(self, uncertain_params=None, block=None): auxiliary_vars=aux_var_list, ) - @copy_docstring(UncertaintySet.compute_auxiliary_param_vals) - def compute_auxiliary_param_vals(self, point, solver=None): + @copy_docstring(UncertaintySet.compute_auxiliary_uncertain_param_vals) + def compute_auxiliary_uncertain_param_vals(self, point, solver=None): point_arr = np.array(point) - if np.allclose(point_arr, self.origin): - return np.zeros(self.dim), True - - aux_space_pt = np.empty(self.dim) - is_zero_deviation_off_origin = np.logical_and( - self.positive_deviation == 0, point_arr != self.origin - ) - if np.any(is_zero_deviation_off_origin): - return np.full(self.dim, np.nan), False - is_dev_nonzero = self.positive_deviation != 0 + aux_space_pt = np.empty(self.dim) aux_space_pt[is_dev_nonzero] = ( point_arr[is_dev_nonzero] - self.origin[is_dev_nonzero] ) / self.positive_deviation[is_dev_nonzero] aux_space_pt[self.positive_deviation == 0] = 0 - aux_space_pt_feasible = ( - aux_space_pt.sum() <= self.gamma - and np.all(0 <= aux_space_pt) - and np.all(aux_space_pt <= 1) - ) - - return aux_space_pt, aux_space_pt_feasible + return aux_space_pt def point_in_set(self, point): """ @@ -1272,8 +1249,13 @@ def point_in_set(self, point): : bool True if the point lies in the set, False otherwise. """ - _, aux_space_pt_feasible = self.compute_auxiliary_param_vals(point) - return aux_space_pt_feasible + aux_space_pt = self.compute_auxiliary_uncertain_param_vals(point) + return ( + np.all(point == self.origin + self.positive_deviation * aux_space_pt) + and aux_space_pt.sum() <= self.gamma + and np.all(0 <= aux_space_pt) + and np.all(aux_space_pt <= 1) + ) class PolyhedralSet(UncertaintySet): @@ -2075,28 +2057,19 @@ def set_as_constraint(self, uncertain_params=None, block=None): auxiliary_vars=aux_var_list, ) - @copy_docstring(UncertaintySet.compute_auxiliary_param_vals) - def compute_auxiliary_param_vals(self, point, solver=None): + @copy_docstring(UncertaintySet.compute_auxiliary_uncertain_param_vals) + def compute_auxiliary_uncertain_param_vals(self, point, solver=None): point_arr = np.array(point) - if np.allclose(point_arr, self.origin): - return np.zeros(self.number_of_factors), True psi_mat_rank = np.linalg.matrix_rank(self.psi_mat) is_psi_full_column_rank = psi_mat_rank == self.number_of_factors if is_psi_full_column_rank: # pseudoinverse uniquely determines the auxiliary values - pinv_psi = np.linalg.pinv(self.psi_mat) - aux_space_pt = pinv_psi @ (point_arr - self.origin) - tol = 1e-8 - is_aux_pt_feasible = abs( - aux_space_pt.sum() - ) <= self.beta * self.number_of_factors + tol and np.all( - np.abs(aux_space_pt) <= 1 + tol - ) - return aux_space_pt, is_aux_pt_feasible + return np.linalg.pinv(self.psi_mat) @ (point_arr - self.origin) else: # guard against possible changes to individual entries, # rows, or columns after `psi_mat` setter invoked + # that may render `psi_mat` rank deficient raise ValueError( "Factor loading matrix `psi_mat` must be full column rank. " f"(There are {self.number_of_factors} factors/columns, but" @@ -2117,8 +2090,12 @@ def point_in_set(self, point): : bool True if the point lies in the set, False otherwise. """ - _, is_aux_pt_feasible = self.compute_auxiliary_param_vals(point) - return is_aux_pt_feasible + aux_space_pt = self.compute_auxiliary_uncertain_param_vals(point) + tol = 1e-8 + return ( + abs(aux_space_pt.sum()) <= self.beta * self.number_of_factors + tol + and np.all(np.abs(aux_space_pt) <= 1 + tol) + ) class AxisAlignedEllipsoidalSet(UncertaintySet): From 3a8be0aebca7c812ee2e5146980cf79f92e46f3b Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 30 Jul 2024 22:19:52 -0400 Subject: [PATCH 2042/3044] Standardize validation of arguments to `point_in_set` --- .../pyros/tests/test_uncertainty_sets.py | 36 +++++++++++ pyomo/contrib/pyros/uncertainty_sets.py | 64 ++++++++++++++++--- 2 files changed, 91 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index e01cd5b2ee9..bd0d4121ddb 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -339,6 +339,10 @@ def test_point_in_set(self): msg=f"Point {point} should not be in uncertainty set {box_set}." ) + # check what happens if dimensions are off + with self.assertRaisesRegex(ValueError, ".*to match the set dimension.*"): + box_set.point_in_set([1, 2, 3]) + def test_add_bounds_on_uncertain_parameters(self): m = ConcreteModel() m.uncertain_param_vars = Var([0, 1], initialize=0) @@ -627,6 +631,10 @@ def test_point_in_set(self): self.assertFalse(buset.point_in_set([0, 3])) self.assertFalse(buset.point_in_set([4, 2])) + # check what happens if dimensions are off + with self.assertRaisesRegex(ValueError, ".*to match the set dimension.*"): + buset.point_in_set([1, 2, 3, 4]) + def test_add_bounds_on_uncertain_parameters(self): """ Test method for adding bounds on uncertain params @@ -966,6 +974,10 @@ def test_point_in_set(self): self.assertFalse(fset.point_in_set(fset.origin + fset.psi_mat @ [1, -1, -1])) self.assertFalse(fset.point_in_set(fset.origin + fset.psi_mat @ [-1, -1, -1])) + # check what happens if dimensions are off + with self.assertRaisesRegex(ValueError, ".*to match the set dimension.*"): + fset.point_in_set([1, 2, 3]) + def test_add_bounds_on_uncertain_parameters(self): m = ConcreteModel() m.uncertain_param_vars = Var(range(4), initialize=0) @@ -1365,6 +1377,10 @@ def test_add_bounds_on_uncertain_parameters(self): np.testing.assert_allclose(m.uncertain_param_vars[0].bounds, (-0.25, 0.25)) np.testing.assert_allclose(m.uncertain_param_vars[1].bounds, (-0.25, 0.25)) + # check what happens if dimensions are off + with self.assertRaisesRegex(ValueError, ".*to match the set dimension.*"): + iset.point_in_set([1, 2, 3]) + class TestCardinalitySet(unittest.TestCase): """ @@ -1550,6 +1566,10 @@ def test_point_in_set(self): # deviation in dimension that has been fixed self.assertFalse(cset.point_in_set([-0.25, 4, 2.01])) + # check what happens if dimensions are off + with self.assertRaisesRegex(ValueError, ".*to match the set dimension.*"): + cset.point_in_set([1, 2, 3, 4]) + @unittest.skipUnless(baron_available, "BARON is not available.") def test_compute_parameter_bounds(self): """ @@ -1672,6 +1692,10 @@ def test_point_in_set(self): self.assertFalse(dset.point_in_set([5.1e-9, 5.1e-9])) self.assertFalse(dset.point_in_set([1e-7, 1e-7])) + # check what happens if dimensions are off + with self.assertRaisesRegex(ValueError, ".*to match the set dimension.*"): + dset.point_in_set([1, 2, 3]) + def test_add_bounds_on_uncertain_parameters(self): m = ConcreteModel() m.uncertain_param_vars = Var([0, 1], initialize=0) @@ -1849,6 +1873,10 @@ def test_point_in_set(self): self.assertFalse(aeset.point_in_set([1.505, 0, 1])) self.assertFalse(aeset.point_in_set([0, 2.05, 1])) + # check what happens if dimensions are off + with self.assertRaisesRegex(ValueError, ".*to match the set dimension.*"): + aeset.point_in_set([1, 2, 3, 4]) + def test_add_bounds_on_uncertain_parameters(self): m = ConcreteModel() m.uncertain_param_vars = Var([0, 1, 2], initialize=0) @@ -2113,6 +2141,10 @@ def test_point_in_set(self): self.assertTrue(eset.point_in_set(eset.center + [5e-9, 0])) self.assertFalse(eset.point_in_set(eset.center + [1e-4, 1e-4])) + # check what happens if dimensions are off + with self.assertRaisesRegex(ValueError, ".*to match the set dimension.*"): + eset.point_in_set([1, 2, 3, 4]) + @unittest.skipUnless(baron_available, "BARON is not available.") def test_compute_parameter_bounds(self): """ @@ -2355,6 +2387,10 @@ def test_point_in_set(self): self.assertFalse(pset.point_in_set([-1, 0])) self.assertFalse(pset.point_in_set([0, 0])) + # check what happens if dimensions are off + with self.assertRaisesRegex(ValueError, ".*to match the set dimension.*"): + pset.point_in_set([1, 2, 3, 4]) + @unittest.skipUnless(baron_available, "Global NLP solver is not available.") def test_add_bounds_on_uncertain_parameters(self): m = ConcreteModel() diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 0bab44c1d2d..d3c98197c4e 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -321,7 +321,8 @@ def validate_dimensions(arr_name, arr, dim, display_value=False): def validate_array( - arr, arr_name, dim, valid_types, valid_type_desc=None, required_shape=None + arr, arr_name, dim, valid_types, valid_type_desc=None, required_shape=None, + required_shape_qual="", ): """ Validate shape and entry types of an array-like object. @@ -348,6 +349,9 @@ def validate_array( corresponding to the position of the entry or `None` (meaning no requirement for the length in the corresponding dimension). + required_shape_qual : str, optional + Clause/phrase expressing reason `arr` should be of shape + `required_shape`, e.g. "to match the set dimension". """ np_arr = np.array(arr, dtype=object) validate_dimensions(arr_name, np_arr, dim, display_value=False) @@ -371,9 +375,13 @@ def generate_shape_str(shape, required_shape): if size is not None and size != np_arr.shape[idx]: req_shape_str = generate_shape_str(required_shape, required_shape) actual_shape_str = generate_shape_str(np_arr.shape, required_shape) + required_shape_qual = ( + # add a preceding space, if needed + f" {required_shape_qual}" if required_shape_qual else "" + ) raise ValueError( f"Attribute '{arr_name}' should be of shape " - f"{req_shape_str}, but detected shape " + f"{req_shape_str}{required_shape_qual}, but detected shape " f"{actual_shape_str}" ) @@ -566,13 +574,15 @@ def point_in_set(self, point): determine whether a user-specified nominal parameter realization lies in the uncertainty set. """ - - # === Ensure point is of correct dimensionality as the uncertain parameters - if len(point) != self.dim: - raise AttributeError( - f"Point has {len(point)} entries, but the dimension " - f"of the uncertainty set is {self.dim}." - ) + validate_array( + arr=point, + arr_name="point", + dim=1, + valid_types=valid_num_types, + valid_type_desc="numeric type", + required_shape=[self.dim], + required_shape_qual="to match the set dimension" + ) m = ConcreteModel() uncertainty_quantification = self.set_as_constraint(block=m) @@ -1224,6 +1234,15 @@ def set_as_constraint(self, uncertain_params=None, block=None): @copy_docstring(UncertaintySet.compute_auxiliary_uncertain_param_vals) def compute_auxiliary_uncertain_param_vals(self, point, solver=None): + validate_array( + arr=point, + arr_name="point", + dim=1, + valid_types=valid_num_types, + valid_type_desc="numeric type", + required_shape=[self.dim], + required_shape_qual="to match the set dimension" + ) point_arr = np.array(point) is_dev_nonzero = self.positive_deviation != 0 @@ -2059,6 +2078,15 @@ def set_as_constraint(self, uncertain_params=None, block=None): @copy_docstring(UncertaintySet.compute_auxiliary_uncertain_param_vals) def compute_auxiliary_uncertain_param_vals(self, point, solver=None): + validate_array( + arr=point, + arr_name="point", + dim=1, + valid_types=valid_num_types, + valid_type_desc="numeric type", + required_shape=[self.dim], + required_shape_qual="to match the set dimension" + ) point_arr = np.array(point) psi_mat_rank = np.linalg.matrix_rank(self.psi_mat) @@ -2516,6 +2544,15 @@ def parameter_bounds(self): @copy_docstring(UncertaintySet.point_in_set) def point_in_set(self, point): + validate_array( + arr=point, + arr_name="point", + dim=1, + valid_types=valid_num_types, + valid_type_desc="numeric type", + required_shape=[self.dim], + required_shape_qual="to match the set dimension" + ) off_center = point - self.center normalized_pt_radius = np.sqrt( off_center @ np.linalg.inv(self.shape_matrix) @ off_center @@ -2702,6 +2739,15 @@ def point_in_set(self, point): : bool True if the point lies in the set, False otherwise. """ + validate_array( + arr=point, + arr_name="point", + dim=1, + valid_types=valid_num_types, + valid_type_desc="numeric type", + required_shape=[self.dim], + required_shape_qual="to match the set dimension" + ) # Round all double precision to a tolerance rounded_scenarios = np.round(self.scenarios, decimals=8) rounded_point = np.round(point, decimals=8) From b5992eca0cca6195d037b5eb037f184e0f7da1f8 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Wed, 31 Jul 2024 10:22:27 -0400 Subject: [PATCH 2043/3044] Updated documentation typo --- doc/OnlineDocs/contributed_packages/doe/doe.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index e8b4d9ca2c7..4be5d0ad8dc 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -219,7 +219,7 @@ An example output of the code above, a design exploration for the initial concen .. figure:: FIM_sensitivity.png :scale: 50 % -A heatmap shows the change of the objective function, a.k.a. the experimental information content, in the design region. Horizontal and vertical axes are two design variables, while the color of each grid shows the experimental information content. Taking the Fig. Reactor case - A optimality as example, A-optimality shows that the most informative region is around $C_{A0}=5.0$ M, $T=300.0$ K, while the least informative region is around $C_{A0}=1.0$ M, $T=700.0$ K. +A heatmap shows the change of the objective function, a.k.a. the experimental information content, in the design space. Horizontal and vertical axes are the two experimental design variables, while the color of each grid shows the experimental information content. For A optimality (top left subfigure), the figure shows that the most informative region is around :math:`C_{A0}=5.0` M, :math:`T=300.0` K, while the least informative region is around :math:`C_{A0}=1.0` M, :math:`T=700.0` K. Step 6: Performing an optimal experimental design ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ From e3992dbda8cc858ad9b16351ff92ff078ed3b14b Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 31 Jul 2024 14:51:39 -0600 Subject: [PATCH 2044/3044] Porting recent changes in docs to equivalent sections in the doc-reorg branch --- doc/OnlineDocs/user_guide/contributed_packages/gdpopt.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/OnlineDocs/user_guide/contributed_packages/gdpopt.rst b/doc/OnlineDocs/user_guide/contributed_packages/gdpopt.rst index d550b0ced76..670d7633f6d 100644 --- a/doc/OnlineDocs/user_guide/contributed_packages/gdpopt.rst +++ b/doc/OnlineDocs/user_guide/contributed_packages/gdpopt.rst @@ -93,10 +93,10 @@ An example that includes the modeling approach may be found below. Variables: x : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain - None : -1.2 : 0.0 : 2 : False : False : Reals + None : -1.2 : 0 : 2 : False : False : Reals y : Size=1, Index=None Key : Lower : Value : Upper : Fixed : Stale : Domain - None : -10 : 1.0 : 10 : False : False : Reals + None : -10 : 1 : 10 : False : False : Reals Objectives: objective : Size=1, Index=None, Active=True @@ -106,7 +106,7 @@ An example that includes the modeling approach may be found below. Constraints: c : Size=1 Key : Lower : Body : Upper - None : 1.0 : 1.0 : 1.0 + None : 1.0 : 1 : 1.0 .. note:: From a31f0ff14e3f7b66416cbe0bbc62da97639023c6 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Wed, 31 Jul 2024 15:00:47 -0600 Subject: [PATCH 2045/3044] Fixing typo --- doc/OnlineDocs/getting_started/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/getting_started/index.rst b/doc/OnlineDocs/getting_started/index.rst index 2cdea1bdf8f..9a6a251e7a3 100644 --- a/doc/OnlineDocs/getting_started/index.rst +++ b/doc/OnlineDocs/getting_started/index.rst @@ -1,5 +1,5 @@ Getting Started =============== -TOOO +TODO From 5b85af7000b5a12b4c6153b6e16fd32d54df84d5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 31 Jul 2024 15:21:05 -0600 Subject: [PATCH 2046/3044] Adding APIs for getting transformed nonlinear and quadratic Constraints and Objectives, fixing a few bugs --- .../piecewise/transform/nonlinear_to_pwl.py | 57 ++++++++++++++----- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index bc6ea3a1f7d..0adca4c8d80 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -72,12 +72,14 @@ class DomainPartitioningMethod(enum.IntEnum): class _NonlinearToPWLTransformationData(AutoSlots.Mixin): - __slots__ = ('transformed_component', 'src_component', 'transformed_constraints') + __slots__ = ('transformed_component', 'src_component', 'transformed_constraints', + 'transformed_objectives') def __init__(self): self.transformed_component = ComponentMap() self.src_component = ComponentMap() self.transformed_constraints = defaultdict(ComponentSet) + self.transformed_objectives = defaultdict(ComponentSet) Block.register_private_data_initializer(_NonlinearToPWLTransformationData) @@ -432,6 +434,10 @@ class NonlinearToPWL(Transformation): points in order to partition the function domain.""", ), ) + # TODO: Minimum dimension to additively decompose--(Only decompose if the + # dimension exceeds this.) + + # TODO: incorporate Bashar's linear tree changes. def __init__(self): super(Transformation).__init__() @@ -484,7 +490,7 @@ def _apply_to_impl(self, model, **kwds): raise ValueError( "Target '%s' is not a Block, Constraint, or Objective. It " "is of type '%s' and cannot be transformed." - % (target.name, type(t)) + % (target.name, type(target)) ) def _get_transformation_block(self, parent): @@ -525,13 +531,14 @@ def _transform_constraint(self, cons, config): src_data_dict = cons.parent_block().private_data() constraints = cons.values() if cons.is_indexed() else (cons,) for c in constraints: - pw_approx = self._approximate_expression( + pw_approx, expr_type = self._approximate_expression( c.body, c, trans_block, config, config.approximate_quadratic_constraints ) if pw_approx is None: # Didn't need approximated, nothing to do continue + c.model().private_data().transformed_constraints[expr_type].add(c) idx = len(trans_block._pwl_cons) trans_block._pwl_cons[c.name, idx] = (c.lower, pw_approx, c.upper) @@ -548,7 +555,7 @@ def _transform_objective(self, objective, config): objectives = objective.values() if objective.is_indexed() else (objective,) src_data_dict = objective.parent_block().private_data() for obj in objectives: - pw_approx = self._approximate_expression( + pw_approx, expr_type = self._approximate_expression( obj.expr, obj, trans_block, @@ -559,6 +566,7 @@ def _transform_objective(self, objective, config): if pw_approx is None: # Didn't need approximated, nothing to do continue + obj.model().private_data().transformed_objectives[expr_type].add(obj) new_obj = Objective(expr=pw_approx, sense=obj.sense) trans_block.add_component( @@ -569,15 +577,14 @@ def _transform_objective(self, objective, config): obj.deactivate() - def _get_bounds_list(self, var_list, parent_component): + def _get_bounds_list(self, var_list, obj): bounds = [] for v in var_list: if None in v.bounds: - # ESJ TODO: Con is undefined--this is a bug! raise ValueError( "Cannot automatically approximate constraints with unbounded " - "variables. Var '%s' appearining in component '%s' is missing " - "at least one bound" % (con.name, v.name) + "variables. Var '%s' appearing in component '%s' is missing " + "at least one bound" % (v.name, obj.name) ) else: bounds.append(v.bounds) @@ -604,16 +611,14 @@ def _approximate_expression( approximate_quadratic ) if not needs_approximating: - return - - obj.model().private_data().transformed_constraints[expr_type].add(obj) + return None, expr_type # Additively decompose expr and work on the pieces pwl_func = 0 for k, subexpr in enumerate( _additively_decompose_expr(expr) if config.additively_decompose else (expr,) ): - # First check is this is a good idea + # First check if this is a good idea expr_vars = list(identify_variables(subexpr, include_fixed=False)) orig_values = ComponentMap((v, v.value) for v in expr_vars) @@ -652,7 +657,7 @@ def eval_expr(*args): for v, val in orig_values.items(): v.value = val - return pwl_func + return pwl_func, expr_type def get_src_component(self, cons): data = cons.parent_block().private_data().src_component @@ -675,7 +680,33 @@ def get_transformed_component(self, cons): ) def get_transformed_nonlinear_constraints(self, model): + """ + Given a model that has been transformed with contrib.piecewise.nonlinear_to_pwl, + return the list of general (not quadratic) nonlinear Constraints that were + approximated with PiecewiseLinearFunctions + """ return model.private_data().transformed_constraints[ExprType.GENERAL] def get_transformed_quadratic_constraints(self, model): + """ + Given a model that has been transformed with contrib.piecewise.nonlinear_to_pwl, + return the list of quadratic Constraints that were approximated with + PiecewiseLinearFunctions + """ return model.private_data().transformed_constraints[ExprType.QUADRATIC] + + def get_transformed_nonlinear_objectives(self, model): + """ + Given a model that has been transformed with contrib.piecewise.nonlinear_to_pwl, + return the list of general (not quadratic) nonlinear Constraints that were + approximated with PiecewiseLinearFunctions + """ + return model.private_data().transformed_objectives[ExprType.GENERAL] + + def get_transformed_quadratic_objectives(self, model): + """ + Given a model that has been transformed with contrib.piecewise.nonlinear_to_pwl, + return the list of quadratic Constraints that were approximated with + PiecewiseLinearFunctions + """ + return model.private_data().transformed_objectives[ExprType.QUADRATIC] From 1cc71bda35d1505d16a5b7c119326ecfeb956758 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 31 Jul 2024 15:21:20 -0600 Subject: [PATCH 2047/3044] Adding a lot of tests, including 3D --- .../piecewise/tests/test_nonlinear_to_pwl.py | 324 +++++++++++++++--- 1 file changed, 274 insertions(+), 50 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index a06c74b2a51..ef1a5a4e096 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -16,7 +16,9 @@ DomainPartitioningMethod, ) from pyomo.core.expr.compare import assertExpressionsStructurallyEqual -from pyomo.environ import ConcreteModel, Var, Constraint, TransformationFactory, log +from pyomo.environ import ( + ConcreteModel, Var, Constraint, TransformationFactory, log, Objective +) ## debug from pytest import set_trace @@ -116,6 +118,101 @@ def test_log_constraint_random_grid(self): x3 = 9.556428757689245 self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) + def test_do_not_transform_quadratic_constraint(self): + m = self.make_model() + m.quad = Constraint(expr=m.x ** 2 <= 9) + m.lin = Constraint(expr=m.x >= 2) + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, + num_points=3, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + approximate_quadratic_constraints=False + ) + + # cons is transformed + self.assertFalse(m.cons.active) + + pwlf = list( + m.component_data_objects(PiecewiseLinearFunction, descend_into=True) + ) + self.assertEqual(len(pwlf), 1) + pwlf = pwlf[0] + + points = [(1.0009,), (5.5,), (9.9991,)] + (x1, x2, x3) = 1.0009, 5.5, 9.9991 + self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) + + # quad is not + self.assertTrue(m.quad.active) + # neither is the linear one + self.assertTrue(m.lin.active) + + def test_constraint_target(self): + m = self.make_model() + m.quad = Constraint(expr=m.x ** 2 <= 9) + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, + num_points=3, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + targets=[m.cons] + ) + + # cons is transformed + self.assertFalse(m.cons.active) + + pwlf = list( + m.component_data_objects(PiecewiseLinearFunction, descend_into=True) + ) + self.assertEqual(len(pwlf), 1) + pwlf = pwlf[0] + + points = [(1.0009,), (5.5,), (9.9991,)] + (x1, x2, x3) = 1.0009, 5.5, 9.9991 + self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) + + # quad is not + self.assertTrue(m.quad.active) + + def test_crazy_target_error(self): + m = self.make_model() + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + with self.assertRaisesRegex( + ValueError, + "Target 'x' is not a Block, Constraint, or Objective. It " + "is of type '' and cannot " + "be transformed." + ): + n_to_pwl.apply_to( + m, + num_points=3, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + targets=[m.x] + ) + + def test_cannot_approximate_constraints_with_unbounded_vars(self): + m = ConcreteModel() + m.x = Var() + m.quad = Constraint(expr=m.x ** 2 <= 9) + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + with self.assertRaisesRegex( + ValueError, + "Cannot automatically approximate constraints with unbounded " + "variables. Var 'x' appearing in component 'quad' is missing " + "at least one bound" + ): + n_to_pwl.apply_to( + m, + num_points=3, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + ) + + # def test_log_constraint_lmt_uniform_sample(self): # m = self.make_model() @@ -143,63 +240,190 @@ def test_log_constraint_random_grid(self): # self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) -class TestNonlinearToPWLIntegration(unittest.TestCase): - def test_Ali_example(self): +class TestNonlinearToPWL_2D(unittest.TestCase): + def make_paraboloid_model(self): m = ConcreteModel() - m.flow_super_heated_vapor = Var() - m.flow_super_heated_vapor.fix(0.4586949988166174) - m.super_heated_vapor_temperature = Var(bounds=(31, 200), initialize=45) - m.evaporator_condensate_temperature = Var( - bounds=(29, 120.8291392028045), initialize=30 - ) - m.LMTD = Var(bounds=(0, 130.61608989795093), initialize=1) - m.evaporator_condensate_enthalpy = Var( - bounds=(-15836.847, -15510.210751855624), initialize=100 - ) - m.evaporator_condensate_vapor_enthalpy = Var( - bounds=(-13416.64, -13247.674383866839), initialize=100 + m.x1 = Var(bounds=(0, 3)) + m.x2 = Var(bounds=(1, 7)) + m.obj = Objective(expr=m.x1 ** 2 + m.x2 ** 2) + + return m + + def check_pw_linear_paraboloid(self, m, pwlf, x1, x2, y1, y2): + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + points = [ + (x1, y1), + (x1, y2), + (x2, y1), + (x2, y2), + ] + self.assertEqual(pwlf._points, points) + self.assertEqual(pwlf._simplices, [(0, 1, 3), (0, 2, 3)]) + self.assertEqual(len(pwlf._linear_functions), 2) + + # just check that the linear functions make sense--they intersect the + # paraboloid at the vertices of the simplices. + self.assertAlmostEqual(pwlf._linear_functions[0](x1, y1), x1 **2 + y1 ** 2) + self.assertAlmostEqual(pwlf._linear_functions[0](x1, y2), x1 **2 + y2 ** 2) + self.assertAlmostEqual(pwlf._linear_functions[0](x2, y2), x2 **2 + y2 ** 2) + + self.assertAlmostEqual(pwlf._linear_functions[1](x1, y1), x1 ** 2 + y1 ** 2) + self.assertAlmostEqual(pwlf._linear_functions[1](x2, y1), x2 ** 2 + y1 ** 2) + self.assertAlmostEqual(pwlf._linear_functions[1](x2, y2), x2 ** 2 + y2 ** 2) + + self.assertEqual(len(pwlf._expressions), 1) + new_obj = n_to_pwl.get_transformed_component(m.obj) + self.assertTrue(new_obj.active) + self.assertIs(new_obj.expr, pwlf._expressions[id(new_obj.expr.expr)]) + self.assertIs(n_to_pwl.get_src_component(new_obj), m.obj) + + quadratic = n_to_pwl.get_transformed_quadratic_constraints(m) + self.assertEqual(len(quadratic), 0) + nonlinear = n_to_pwl.get_transformed_nonlinear_constraints(m) + self.assertEqual(len(nonlinear), 0) + quadratic = n_to_pwl.get_transformed_quadratic_objectives(m) + self.assertEqual(len(quadratic), 1) + self.assertIn(m.obj, quadratic) + nonlinear = n_to_pwl.get_transformed_nonlinear_objectives(m) + self.assertEqual(len(nonlinear), 0) + + def test_paraboloid_objective_uniform_grid(self): + m = self.make_paraboloid_model() + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, num_points=2, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID ) - m.heat_transfer_coef = Var( - bounds=(1.9936854577372858, 5.995319594088982), initialize=0.1 + + # check obj is transformed + self.assertFalse(m.obj.active) + + pwlf = list( + m.component_data_objects(PiecewiseLinearFunction, descend_into=True) ) - m.evaporator_brine_temperature = Var( - bounds=(27, 118.82913920280366), initialize=35 + self.assertEqual(len(pwlf), 1) + pwlf = pwlf[0] + + x1 = 0.00030000000000000003 + x2 = 2.9997 + y1 = 1.0006 + y2 = 6.9994 + + self.check_pw_linear_paraboloid(m, pwlf, x1, x2, y1, y2) + + def test_objective_target(self): + m = self.make_paraboloid_model() + + m.some_other_nonlinear_constraint = Constraint(expr=m.x1 ** 3 + m.x2 <= 6) + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, num_points=2, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + targets=[m.obj] ) - m.each_evaporator_area = Var() - - m.c = Constraint( - expr=m.each_evaporator_area - == ( - 1.873 - * m.flow_super_heated_vapor - * ( - m.super_heated_vapor_temperature - - m.evaporator_condensate_temperature - ) - / (100 * m.LMTD) - + m.flow_super_heated_vapor - * ( - m.evaporator_condensate_vapor_enthalpy - - m.evaporator_condensate_enthalpy - ) - / ( - m.heat_transfer_coef - * ( - m.evaporator_condensate_temperature - - m.evaporator_brine_temperature - ) - ) - ) + + + # check obj is transformed + self.assertFalse(m.obj.active) + + pwlf = list( + m.component_data_objects(PiecewiseLinearFunction, descend_into=True) ) + self.assertEqual(len(pwlf), 1) + pwlf = pwlf[0] + + x1 = 0.00030000000000000003 + x2 = 2.9997 + y1 = 1.0006 + y2 = 6.9994 + + self.check_pw_linear_paraboloid(m, pwlf, x1, x2, y1, y2) + + # and check that the constraint isn't transformed + self.assertTrue(m.some_other_nonlinear_constraint.active) + + def test_do_not_transform_quadratic_objective(self): + m = self.make_paraboloid_model() n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') n_to_pwl.apply_to( - m, - num_points=3, + m, num_points=2, domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + approximate_quadratic_objectives=False ) + + # check obj is *not* transformed + self.assertTrue(m.obj.active) - m.pprint() - - from pyomo.environ import SolverFactory - SolverFactory('gurobi').solve(m, tee=True) + quadratic = n_to_pwl.get_transformed_quadratic_constraints(m) + self.assertEqual(len(quadratic), 0) + nonlinear = n_to_pwl.get_transformed_nonlinear_constraints(m) + self.assertEqual(len(nonlinear), 0) + quadratic = n_to_pwl.get_transformed_quadratic_objectives(m) + self.assertEqual(len(quadratic), 0) + nonlinear = n_to_pwl.get_transformed_nonlinear_objectives(m) + self.assertEqual(len(nonlinear), 0) + + +# class TestNonlinearToPWLIntegration(unittest.TestCase): +# def test_Ali_example(self): +# m = ConcreteModel() +# m.flow_super_heated_vapor = Var() +# m.flow_super_heated_vapor.fix(0.4586949988166174) +# m.super_heated_vapor_temperature = Var(bounds=(31, 200), initialize=45) +# m.evaporator_condensate_temperature = Var( +# bounds=(29, 120.8291392028045), initialize=30 +# ) +# m.LMTD = Var(bounds=(0, 130.61608989795093), initialize=1) +# m.evaporator_condensate_enthalpy = Var( +# bounds=(-15836.847, -15510.210751855624), initialize=100 +# ) +# m.evaporator_condensate_vapor_enthalpy = Var( +# bounds=(-13416.64, -13247.674383866839), initialize=100 +# ) +# m.heat_transfer_coef = Var( +# bounds=(1.9936854577372858, 5.995319594088982), initialize=0.1 +# ) +# m.evaporator_brine_temperature = Var( +# bounds=(27, 118.82913920280366), initialize=35 +# ) +# m.each_evaporator_area = Var() + +# m.c = Constraint( +# expr=m.each_evaporator_area +# == ( +# 1.873 +# * m.flow_super_heated_vapor +# * ( +# m.super_heated_vapor_temperature +# - m.evaporator_condensate_temperature +# ) +# / (100 * m.LMTD) +# + m.flow_super_heated_vapor +# * ( +# m.evaporator_condensate_vapor_enthalpy +# - m.evaporator_condensate_enthalpy +# ) +# / ( +# m.heat_transfer_coef +# * ( +# m.evaporator_condensate_temperature +# - m.evaporator_brine_temperature +# ) +# ) +# ) +# ) + +# n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') +# n_to_pwl.apply_to( +# m, +# num_points=3, +# domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, +# ) + +# m.pprint() + +# from pyomo.environ import SolverFactory +# SolverFactory('gurobi').solve(m, tee=True) From 2a6ad4603ff6e3baf611dc96ddc09f42c43f13ae Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 31 Jul 2024 15:22:15 -0600 Subject: [PATCH 2048/3044] Incorporating black's opinions --- .../piecewise/tests/test_nonlinear_to_pwl.py | 81 ++++++++++--------- .../piecewise/transform/nonlinear_to_pwl.py | 15 ++-- 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index ef1a5a4e096..c9edabd00bd 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -17,7 +17,12 @@ ) from pyomo.core.expr.compare import assertExpressionsStructurallyEqual from pyomo.environ import ( - ConcreteModel, Var, Constraint, TransformationFactory, log, Objective + ConcreteModel, + Var, + Constraint, + TransformationFactory, + log, + Objective, ) ## debug @@ -120,7 +125,7 @@ def test_log_constraint_random_grid(self): def test_do_not_transform_quadratic_constraint(self): m = self.make_model() - m.quad = Constraint(expr=m.x ** 2 <= 9) + m.quad = Constraint(expr=m.x**2 <= 9) m.lin = Constraint(expr=m.x >= 2) n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') @@ -128,7 +133,7 @@ def test_do_not_transform_quadratic_constraint(self): m, num_points=3, domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, - approximate_quadratic_constraints=False + approximate_quadratic_constraints=False, ) # cons is transformed @@ -151,14 +156,14 @@ def test_do_not_transform_quadratic_constraint(self): def test_constraint_target(self): m = self.make_model() - m.quad = Constraint(expr=m.x ** 2 <= 9) + m.quad = Constraint(expr=m.x**2 <= 9) n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') n_to_pwl.apply_to( m, num_points=3, domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, - targets=[m.cons] + targets=[m.cons], ) # cons is transformed @@ -179,32 +184,32 @@ def test_constraint_target(self): def test_crazy_target_error(self): m = self.make_model() - + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') with self.assertRaisesRegex( - ValueError, - "Target 'x' is not a Block, Constraint, or Objective. It " - "is of type '' and cannot " - "be transformed." + ValueError, + "Target 'x' is not a Block, Constraint, or Objective. It " + "is of type '' and cannot " + "be transformed.", ): n_to_pwl.apply_to( m, num_points=3, domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, - targets=[m.x] + targets=[m.x], ) def test_cannot_approximate_constraints_with_unbounded_vars(self): m = ConcreteModel() m.x = Var() - m.quad = Constraint(expr=m.x ** 2 <= 9) + m.quad = Constraint(expr=m.x**2 <= 9) n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') with self.assertRaisesRegex( - ValueError, - "Cannot automatically approximate constraints with unbounded " - "variables. Var 'x' appearing in component 'quad' is missing " - "at least one bound" + ValueError, + "Cannot automatically approximate constraints with unbounded " + "variables. Var 'x' appearing in component 'quad' is missing " + "at least one bound", ): n_to_pwl.apply_to( m, @@ -212,7 +217,6 @@ def test_cannot_approximate_constraints_with_unbounded_vars(self): domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, ) - # def test_log_constraint_lmt_uniform_sample(self): # m = self.make_model() @@ -245,31 +249,26 @@ def make_paraboloid_model(self): m = ConcreteModel() m.x1 = Var(bounds=(0, 3)) m.x2 = Var(bounds=(1, 7)) - m.obj = Objective(expr=m.x1 ** 2 + m.x2 ** 2) + m.obj = Objective(expr=m.x1**2 + m.x2**2) return m def check_pw_linear_paraboloid(self, m, pwlf, x1, x2, y1, y2): n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') - points = [ - (x1, y1), - (x1, y2), - (x2, y1), - (x2, y2), - ] + points = [(x1, y1), (x1, y2), (x2, y1), (x2, y2)] self.assertEqual(pwlf._points, points) self.assertEqual(pwlf._simplices, [(0, 1, 3), (0, 2, 3)]) self.assertEqual(len(pwlf._linear_functions), 2) # just check that the linear functions make sense--they intersect the # paraboloid at the vertices of the simplices. - self.assertAlmostEqual(pwlf._linear_functions[0](x1, y1), x1 **2 + y1 ** 2) - self.assertAlmostEqual(pwlf._linear_functions[0](x1, y2), x1 **2 + y2 ** 2) - self.assertAlmostEqual(pwlf._linear_functions[0](x2, y2), x2 **2 + y2 ** 2) + self.assertAlmostEqual(pwlf._linear_functions[0](x1, y1), x1**2 + y1**2) + self.assertAlmostEqual(pwlf._linear_functions[0](x1, y2), x1**2 + y2**2) + self.assertAlmostEqual(pwlf._linear_functions[0](x2, y2), x2**2 + y2**2) - self.assertAlmostEqual(pwlf._linear_functions[1](x1, y1), x1 ** 2 + y1 ** 2) - self.assertAlmostEqual(pwlf._linear_functions[1](x2, y1), x2 ** 2 + y1 ** 2) - self.assertAlmostEqual(pwlf._linear_functions[1](x2, y2), x2 ** 2 + y2 ** 2) + self.assertAlmostEqual(pwlf._linear_functions[1](x1, y1), x1**2 + y1**2) + self.assertAlmostEqual(pwlf._linear_functions[1](x2, y1), x2**2 + y1**2) + self.assertAlmostEqual(pwlf._linear_functions[1](x2, y2), x2**2 + y2**2) self.assertEqual(len(pwlf._expressions), 1) new_obj = n_to_pwl.get_transformed_component(m.obj) @@ -292,8 +291,9 @@ def test_paraboloid_objective_uniform_grid(self): n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') n_to_pwl.apply_to( - m, num_points=2, - domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID + m, + num_points=2, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, ) # check obj is transformed @@ -315,16 +315,16 @@ def test_paraboloid_objective_uniform_grid(self): def test_objective_target(self): m = self.make_paraboloid_model() - m.some_other_nonlinear_constraint = Constraint(expr=m.x1 ** 3 + m.x2 <= 6) + m.some_other_nonlinear_constraint = Constraint(expr=m.x1**3 + m.x2 <= 6) n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') n_to_pwl.apply_to( - m, num_points=2, + m, + num_points=2, domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, - targets=[m.obj] + targets=[m.obj], ) - # check obj is transformed self.assertFalse(m.obj.active) @@ -349,11 +349,12 @@ def test_do_not_transform_quadratic_objective(self): n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') n_to_pwl.apply_to( - m, num_points=2, + m, + num_points=2, domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, - approximate_quadratic_objectives=False + approximate_quadratic_objectives=False, ) - + # check obj is *not* transformed self.assertTrue(m.obj.active) @@ -365,7 +366,7 @@ def test_do_not_transform_quadratic_objective(self): self.assertEqual(len(quadratic), 0) nonlinear = n_to_pwl.get_transformed_nonlinear_objectives(m) self.assertEqual(len(nonlinear), 0) - + # class TestNonlinearToPWLIntegration(unittest.TestCase): # def test_Ali_example(self): diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 0adca4c8d80..fd11c67a17b 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -72,8 +72,12 @@ class DomainPartitioningMethod(enum.IntEnum): class _NonlinearToPWLTransformationData(AutoSlots.Mixin): - __slots__ = ('transformed_component', 'src_component', 'transformed_constraints', - 'transformed_objectives') + __slots__ = ( + 'transformed_component', + 'src_component', + 'transformed_constraints', + 'transformed_objectives', + ) def __init__(self): self.transformed_component = ComponentMap() @@ -607,8 +611,7 @@ def _approximate_expression( self, expr, obj, trans_block, config, approximate_quadratic ): expr_type, needs_approximating = self._needs_approximating( - expr, - approximate_quadratic + expr, approximate_quadratic ) if not needs_approximating: return None, expr_type @@ -690,7 +693,7 @@ def get_transformed_nonlinear_constraints(self, model): def get_transformed_quadratic_constraints(self, model): """ Given a model that has been transformed with contrib.piecewise.nonlinear_to_pwl, - return the list of quadratic Constraints that were approximated with + return the list of quadratic Constraints that were approximated with PiecewiseLinearFunctions """ return model.private_data().transformed_constraints[ExprType.QUADRATIC] @@ -706,7 +709,7 @@ def get_transformed_nonlinear_objectives(self, model): def get_transformed_quadratic_objectives(self, model): """ Given a model that has been transformed with contrib.piecewise.nonlinear_to_pwl, - return the list of quadratic Constraints that were approximated with + return the list of quadratic Constraints that were approximated with PiecewiseLinearFunctions """ return model.private_data().transformed_objectives[ExprType.QUADRATIC] From 3e870d857572d71d19e9bd9e326ead9a0e7c137c Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 31 Jul 2024 18:52:35 -0400 Subject: [PATCH 2049/3044] Adjust polishing model component declarations --- pyomo/contrib/pyros/master_problem_methods.py | 6 +++--- pyomo/contrib/pyros/tests/test_master.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 41aa9dc0edd..be3017d2034 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -408,7 +408,7 @@ def construct_dr_polishing_problem(master_data, config): polishing_model.epigraph_obj.deactivate() decision_rule_vars = nominal_polishing_block.decision_rule_vars - nominal_polishing_block.polishing_vars = polishing_vars = [] + polishing_model.polishing_vars = polishing_vars = [] for idx, indexed_dr_var in enumerate(decision_rule_vars): # declare auxiliary 'polishing' variables. # these are meant to represent the absolute values @@ -443,11 +443,11 @@ def construct_dr_polishing_problem(master_data, config): polishing_absolute_value_ub_cons = Constraint(indexed_polishing_var.index_set()) # add indexed constraints to polishing model - nominal_polishing_block.add_component( + polishing_model.add_component( unique_component_name(polishing_model, f"polishing_abs_val_lb_con_{idx}"), polishing_absolute_value_lb_cons, ) - nominal_polishing_block.add_component( + polishing_model.add_component( unique_component_name(polishing_model, f"polishing_abs_val_ub_con_{idx}"), polishing_absolute_value_ub_cons, ) diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index 7b2ba994b93..5823379b8c8 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -391,24 +391,24 @@ def test_construct_dr_polishing_problem_polishing_components(self): nom_polishing_block = polishing_model.scenarios[0, 0] self.assertFalse(nom_polishing_block.decision_rule_vars[0][0].fixed) - self.assertFalse(nom_polishing_block.polishing_vars[0][0].fixed) - self.assertTrue(nom_polishing_block.polishing_abs_val_lb_con_0[0].active) - self.assertTrue(nom_polishing_block.polishing_abs_val_ub_con_0[0].active) + self.assertFalse(polishing_model.polishing_vars[0][0].fixed) + self.assertTrue(polishing_model.polishing_abs_val_lb_con_0[0].active) + self.assertTrue(polishing_model.polishing_abs_val_ub_con_0[0].active) # polishing components for the affine DR term should be # fixed/deactivated since the DR variable was fixed self.assertTrue(nom_polishing_block.decision_rule_vars[0][1].fixed) - self.assertTrue(nom_polishing_block.polishing_vars[0][1].fixed) - self.assertFalse(nom_polishing_block.polishing_abs_val_lb_con_0[1].active) - self.assertFalse(nom_polishing_block.polishing_abs_val_ub_con_0[1].active) + self.assertTrue(polishing_model.polishing_vars[0][1].fixed) + self.assertFalse(polishing_model.polishing_abs_val_lb_con_0[1].active) + self.assertFalse(polishing_model.polishing_abs_val_ub_con_0[1].active) # check initialization of polishing vars self.assertEqual( - nom_polishing_block.polishing_vars[0][0].value, + polishing_model.polishing_vars[0][0].value, abs(nom_polishing_block.decision_rule_vars[0][0].value), ) self.assertEqual( - nom_polishing_block.polishing_vars[0][1].value, + polishing_model.polishing_vars[0][1].value, abs(nom_polishing_block.decision_rule_vars[0][1].value), ) From 3046d266502f359f8b346a7c22a417f358605266 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 31 Jul 2024 19:02:56 -0400 Subject: [PATCH 2050/3044] Further adjust DR polishing component declarations --- pyomo/contrib/pyros/master_problem_methods.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index be3017d2034..bd06cee5cce 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -417,8 +417,8 @@ def construct_dr_polishing_problem(master_data, config): indexed_polishing_var = Var( list(indexed_dr_var.keys()), domain=NonNegativeReals ) - nominal_polishing_block.add_component( - unique_component_name(nominal_polishing_block, f"dr_polishing_var_{idx}"), + polishing_model.add_component( + unique_component_name(polishing_model, f"dr_polishing_var_{idx}"), indexed_polishing_var, ) polishing_vars.append(indexed_polishing_var) @@ -435,8 +435,8 @@ def construct_dr_polishing_problem(master_data, config): polishing_vars, eff_ss_var_to_dr_expr_pairs, ) - nominal_polishing_block.polishing_abs_val_lb_cons = all_lb_cons = [] - nominal_polishing_block.polishing_abs_val_ub_cons = all_ub_cons = [] + polishing_model.polishing_abs_val_lb_cons = all_lb_cons = [] + polishing_model.polishing_abs_val_ub_cons = all_ub_cons = [] for idx, (indexed_polishing_var, (ss_var, dr_expr)) in enumerate(dr_eq_var_zip): # set up absolute value constraint components polishing_absolute_value_lb_cons = Constraint(indexed_polishing_var.index_set()) From e3d3fd63a7b0368704fc08fe06b1d96a68982451 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 31 Jul 2024 17:51:31 -0600 Subject: [PATCH 2051/3044] Fixing some typos --- pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index fd11c67a17b..f88c83af17f 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -136,7 +136,8 @@ def get_points_lmt(points, bounds, func, seed): ) regr.fit(x_list, y_list) - leaves, splits, ths = parse_linear_tree_regressor(regr, bounds) + # ESJ TODO: we actually only needs leaves from here... + leaves, splits, thresholds = parse_linear_tree_regressor(regr, bounds) # This was originally part of the LMT_Model_component and used to calculate # avg_leaves for the output data. TODO: get this back @@ -419,7 +420,7 @@ class NonlinearToPWL(Transformation): It is recommended to leave this False as long as no nonlinear constraint involves more than about 5-6 variables. For constraints with higher- dimmensional nonlinear functions, additive decomposition will improve - the scalability of the approximation (since paritioning the domain is + the scalability of the approximation (since partitioning the domain is subject to the curse of dimensionality).""", ), ) @@ -433,9 +434,9 @@ class NonlinearToPWL(Transformation): Specifies the maximum dimension function the transformation should attempt to approximate. If a nonlinear function dimension exceeds 'max_dimension' the transformation will log a warning and leave the - expression as-is. For functions with dimension significantly the default - (5), it is likely that this transformation will stall triangulating the - points in order to partition the function domain.""", + expression as-is. For functions with dimension significantly above the + default (5), it is likely that this transformation will stall + triangulating the points in order to partition the function domain.""", ), ) # TODO: Minimum dimension to additively decompose--(Only decompose if the From a6d6da08cfe555676a5d290fc87cd1afc7ed9455 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 31 Jul 2024 21:14:02 -0400 Subject: [PATCH 2052/3044] Switch DR polishing obj to nonstatic infinity norm --- pyomo/contrib/pyros/master_problem_methods.py | 83 ++++++++++--------- pyomo/contrib/pyros/tests/test_master.py | 19 ++--- 2 files changed, 54 insertions(+), 48 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index bd06cee5cce..0c33c236f37 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -27,6 +27,7 @@ ) from pyomo.core.base.set_types import NonNegativeIntegers, NonNegativeReals from pyomo.core.expr import identify_variables, value +from pyomo.core.util import prod from pyomo.opt import ( check_optimal_termination, SolverResults, @@ -407,21 +408,10 @@ def construct_dr_polishing_problem(master_data, config): # we will add the polishing objective later polishing_model.epigraph_obj.deactivate() - decision_rule_vars = nominal_polishing_block.decision_rule_vars - polishing_model.polishing_vars = polishing_vars = [] - for idx, indexed_dr_var in enumerate(decision_rule_vars): - # declare auxiliary 'polishing' variables. - # these are meant to represent the absolute values - # of the terms of DR polynomial; we need these for the - # L1-norm - indexed_polishing_var = Var( - list(indexed_dr_var.keys()), domain=NonNegativeReals - ) - polishing_model.add_component( - unique_component_name(polishing_model, f"dr_polishing_var_{idx}"), - indexed_polishing_var, - ) - polishing_vars.append(indexed_polishing_var) + polishing_model.infinity_norm_var = infinity_norm_var = Var( + domain=NonNegativeReals, + initialize=0, + ) # we need the DR expressions to set up the # absolute value constraints and initialize the @@ -431,16 +421,12 @@ def construct_dr_polishing_problem(master_data, config): for ss_var in nominal_eff_var_partitioning.second_stage_variables ] - dr_eq_var_zip = zip( - polishing_vars, - eff_ss_var_to_dr_expr_pairs, - ) polishing_model.polishing_abs_val_lb_cons = all_lb_cons = [] polishing_model.polishing_abs_val_ub_cons = all_ub_cons = [] - for idx, (indexed_polishing_var, (ss_var, dr_expr)) in enumerate(dr_eq_var_zip): + for idx, (ss_var, dr_expr) in enumerate(eff_ss_var_to_dr_expr_pairs): # set up absolute value constraint components - polishing_absolute_value_lb_cons = Constraint(indexed_polishing_var.index_set()) - polishing_absolute_value_ub_cons = Constraint(indexed_polishing_var.index_set()) + polishing_absolute_value_lb_cons = Constraint(NonNegativeReals) + polishing_absolute_value_ub_cons = Constraint(NonNegativeReals) # add indexed constraints to polishing model polishing_model.add_component( @@ -458,41 +444,62 @@ def construct_dr_polishing_problem(master_data, config): for dr_monomial in dr_expr.args: if dr_monomial.is_expression_type(): - # degree > 1 monomial expression of form + # degree >= 1 monomial expression of form # (product of uncertain params) * dr variable dr_var_in_term = dr_monomial.args[-1] else: - # the static term (intercept) - dr_var_in_term = dr_monomial + # the static term (intercept); + # we do not polish this term + # continue + continue # we want the DR variable and corresponding polishing - # variable to have the same index + # constraints to have the same index in the indexed + # components dr_var_in_term_idx = dr_var_in_term.index() - polishing_var = indexed_polishing_var[dr_var_in_term_idx] + + # Fix DR variable if: + # (1) it has already been fixed from master due to + # DR efficiencies (already done) + # (2) coefficient of term + # (i.e. product of uncertain parameter values) + # in DR expression is 0 + # across all master blocks + dr_term_copies = [ + scenario_blk.decision_rule_eqns[idx].body.args[dr_var_in_term_idx] + for scenario_blk in master_model.scenarios.values() + ] + all_copy_coeffs_zero = all( + abs(value(prod(term.args[:-1]))) <= 1e-10 + # if not expression type, then it's the static DR + # term, which is just a Var + if term.is_expression_type() else 1 + for term in dr_term_copies + ) + if all_copy_coeffs_zero: + dr_var_in_term.fix(0) # add polishing constraints polishing_absolute_value_lb_cons[dr_var_in_term_idx] = ( - -polishing_var - dr_monomial <= 0 + -infinity_norm_var - dr_monomial <= 0 ) polishing_absolute_value_ub_cons[dr_var_in_term_idx] = ( - dr_monomial - polishing_var <= 0 + dr_monomial - infinity_norm_var <= 0 ) # some DR variables may be fixed in the earlier # PyROS iterations for efficiency purposes if dr_var_in_term.fixed: - polishing_var.fix() polishing_absolute_value_lb_cons[dr_var_in_term_idx].deactivate() polishing_absolute_value_ub_cons[dr_var_in_term_idx].deactivate() + else: + # ensure infinity norm var properly initialized + abs_monomial_val = abs(value(dr_monomial)) + if abs_monomial_val > infinity_norm_var.value: + infinity_norm_var.set_value(abs_monomial_val) - # ensure the polishing constraints - # are satisfied (to equality) at the initial point - polishing_var.set_value(abs(value(dr_monomial))) - - # finally, the 1-norm objective - polishing_model.polishing_obj = Objective( - expr=sum(sum(polishing_var.values()) for polishing_var in polishing_vars) - ) + # finally, the infinity-norm objective + polishing_model.polishing_obj = Objective(expr=infinity_norm_var) return polishing_model diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index 5823379b8c8..4802163991a 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -391,25 +391,24 @@ def test_construct_dr_polishing_problem_polishing_components(self): nom_polishing_block = polishing_model.scenarios[0, 0] self.assertFalse(nom_polishing_block.decision_rule_vars[0][0].fixed) - self.assertFalse(polishing_model.polishing_vars[0][0].fixed) - self.assertTrue(polishing_model.polishing_abs_val_lb_con_0[0].active) - self.assertTrue(polishing_model.polishing_abs_val_ub_con_0[0].active) + self.assertEqual(list(polishing_model.polishing_abs_val_lb_con_0.keys()), [1]) + self.assertEqual(list(polishing_model.polishing_abs_val_ub_con_0.keys()), [1]) # polishing components for the affine DR term should be # fixed/deactivated since the DR variable was fixed self.assertTrue(nom_polishing_block.decision_rule_vars[0][1].fixed) - self.assertTrue(polishing_model.polishing_vars[0][1].fixed) self.assertFalse(polishing_model.polishing_abs_val_lb_con_0[1].active) self.assertFalse(polishing_model.polishing_abs_val_ub_con_0[1].active) - # check initialization of polishing vars + # check initialization of infinity norm var self.assertEqual( - polishing_model.polishing_vars[0][0].value, - abs(nom_polishing_block.decision_rule_vars[0][0].value), + polishing_model.infinity_norm_var.value, + 0, ) - self.assertEqual( - polishing_model.polishing_vars[0][1].value, - abs(nom_polishing_block.decision_rule_vars[0][1].value), + assertExpressionsEqual( + self, + polishing_model.polishing_obj.expr, + polishing_model.infinity_norm_var, ) def test_construct_dr_polishing_problem_objectives(self): From 6aff08b68f4e0cbf516f9c48b8aba81a75ee7931 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 31 Jul 2024 21:33:08 -0400 Subject: [PATCH 2053/3044] Resolve ambiguity in new DR polishing efficiency --- pyomo/contrib/pyros/master_problem_methods.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 0c33c236f37..b0178a65fcd 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -443,7 +443,8 @@ def construct_dr_polishing_problem(master_data, config): all_ub_cons.append(polishing_absolute_value_ub_cons) for dr_monomial in dr_expr.args: - if dr_monomial.is_expression_type(): + is_a_nonstatic_dr_term = dr_monomial.is_expression_type() + if is_a_nonstatic_dr_term: # degree >= 1 monomial expression of form # (product of uncertain params) * dr variable dr_var_in_term = dr_monomial.args[-1] @@ -469,11 +470,8 @@ def construct_dr_polishing_problem(master_data, config): scenario_blk.decision_rule_eqns[idx].body.args[dr_var_in_term_idx] for scenario_blk in master_model.scenarios.values() ] - all_copy_coeffs_zero = all( + all_copy_coeffs_zero = is_a_nonstatic_dr_term and all( abs(value(prod(term.args[:-1]))) <= 1e-10 - # if not expression type, then it's the static DR - # term, which is just a Var - if term.is_expression_type() else 1 for term in dr_term_copies ) if all_copy_coeffs_zero: From 36b146f55a1cfb1f9c909bd44e50ee5c3e6d2d9e Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 31 Jul 2024 21:45:10 -0400 Subject: [PATCH 2054/3044] Include static DR term in polishing norm --- pyomo/contrib/pyros/master_problem_methods.py | 2 +- pyomo/contrib/pyros/tests/test_master.py | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index b0178a65fcd..d7dd83d991b 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -452,7 +452,7 @@ def construct_dr_polishing_problem(master_data, config): # the static term (intercept); # we do not polish this term # continue - continue + dr_var_in_term = dr_monomial # we want the DR variable and corresponding polishing # constraints to have the same index in the indexed diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index 4802163991a..0365fc0a3cb 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -391,8 +391,17 @@ def test_construct_dr_polishing_problem_polishing_components(self): nom_polishing_block = polishing_model.scenarios[0, 0] self.assertFalse(nom_polishing_block.decision_rule_vars[0][0].fixed) - self.assertEqual(list(polishing_model.polishing_abs_val_lb_con_0.keys()), [1]) - self.assertEqual(list(polishing_model.polishing_abs_val_ub_con_0.keys()), [1]) + self.assertEqual( + list(polishing_model.polishing_abs_val_lb_con_0.keys()), [0, 1] + ) + self.assertEqual( + list(polishing_model.polishing_abs_val_ub_con_0.keys()), [0, 1] + ) + + # static term unfixed; ensure polishing components active + self.assertFalse(nom_polishing_block.decision_rule_vars[0][0].fixed) + self.assertTrue(polishing_model.polishing_abs_val_lb_con_0[0].active) + self.assertTrue(polishing_model.polishing_abs_val_ub_con_0[0].active) # polishing components for the affine DR term should be # fixed/deactivated since the DR variable was fixed @@ -403,7 +412,8 @@ def test_construct_dr_polishing_problem_polishing_components(self): # check initialization of infinity norm var self.assertEqual( polishing_model.infinity_norm_var.value, - 0, + # static term has higher abs value + abs(nom_polishing_block.decision_rule_vars[0][0].value), ) assertExpressionsEqual( self, From 56b6bd3ed349ac4f4f7deb72fa3d8e0119dd43be Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 1 Aug 2024 12:11:52 -0400 Subject: [PATCH 2055/3044] Remove nonstatic terms from DR polishing norm --- pyomo/contrib/pyros/master_problem_methods.py | 2 +- pyomo/contrib/pyros/tests/test_master.py | 16 +++------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index d7dd83d991b..b0178a65fcd 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -452,7 +452,7 @@ def construct_dr_polishing_problem(master_data, config): # the static term (intercept); # we do not polish this term # continue - dr_var_in_term = dr_monomial + continue # we want the DR variable and corresponding polishing # constraints to have the same index in the indexed diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index 0365fc0a3cb..7bd8bd4d90c 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -391,17 +391,11 @@ def test_construct_dr_polishing_problem_polishing_components(self): nom_polishing_block = polishing_model.scenarios[0, 0] self.assertFalse(nom_polishing_block.decision_rule_vars[0][0].fixed) - self.assertEqual( - list(polishing_model.polishing_abs_val_lb_con_0.keys()), [0, 1] - ) - self.assertEqual( - list(polishing_model.polishing_abs_val_ub_con_0.keys()), [0, 1] - ) + self.assertEqual(list(polishing_model.polishing_abs_val_lb_con_0.keys()), [1]) + self.assertEqual(list(polishing_model.polishing_abs_val_ub_con_0.keys()), [1]) # static term unfixed; ensure polishing components active self.assertFalse(nom_polishing_block.decision_rule_vars[0][0].fixed) - self.assertTrue(polishing_model.polishing_abs_val_lb_con_0[0].active) - self.assertTrue(polishing_model.polishing_abs_val_ub_con_0[0].active) # polishing components for the affine DR term should be # fixed/deactivated since the DR variable was fixed @@ -410,11 +404,7 @@ def test_construct_dr_polishing_problem_polishing_components(self): self.assertFalse(polishing_model.polishing_abs_val_ub_con_0[1].active) # check initialization of infinity norm var - self.assertEqual( - polishing_model.infinity_norm_var.value, - # static term has higher abs value - abs(nom_polishing_block.decision_rule_vars[0][0].value), - ) + self.assertEqual(polishing_model.infinity_norm_var.value, 0) assertExpressionsEqual( self, polishing_model.polishing_obj.expr, From c10b5146015f995b20a3aa6e12bd9e468bd82a03 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 1 Aug 2024 12:53:30 -0400 Subject: [PATCH 2056/3044] Restore 1-norm as DR polishing objective --- pyomo/contrib/pyros/master_problem_methods.py | 58 ++++++++++++------- pyomo/contrib/pyros/tests/test_master.py | 25 +++++--- 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index b0178a65fcd..b97cbeff2ab 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -408,10 +408,20 @@ def construct_dr_polishing_problem(master_data, config): # we will add the polishing objective later polishing_model.epigraph_obj.deactivate() - polishing_model.infinity_norm_var = infinity_norm_var = Var( - domain=NonNegativeReals, - initialize=0, - ) + polishing_model.polishing_vars = polishing_vars = [] + for idx, indexed_dr_var in enumerate(nominal_polishing_block.decision_rule_vars): + # declare auxiliary 'polishing' variables. + # these are meant to represent the absolute values + # of the terms of DR polynomial; we need these for the + # L1-norm + indexed_polishing_var = Var( + list(indexed_dr_var.keys()), domain=NonNegativeReals + ) + polishing_model.add_component( + unique_component_name(polishing_model, f"dr_polishing_var_{idx}"), + indexed_polishing_var, + ) + polishing_vars.append(indexed_polishing_var) # we need the DR expressions to set up the # absolute value constraints and initialize the @@ -421,12 +431,16 @@ def construct_dr_polishing_problem(master_data, config): for ss_var in nominal_eff_var_partitioning.second_stage_variables ] + dr_eq_var_zip = zip( + polishing_vars, + eff_ss_var_to_dr_expr_pairs, + ) polishing_model.polishing_abs_val_lb_cons = all_lb_cons = [] polishing_model.polishing_abs_val_ub_cons = all_ub_cons = [] - for idx, (ss_var, dr_expr) in enumerate(eff_ss_var_to_dr_expr_pairs): + for idx, (indexed_polishing_var, (ss_var, dr_expr)) in enumerate(dr_eq_var_zip): # set up absolute value constraint components - polishing_absolute_value_lb_cons = Constraint(NonNegativeReals) - polishing_absolute_value_ub_cons = Constraint(NonNegativeReals) + polishing_absolute_value_lb_cons = Constraint(indexed_polishing_var.index_set()) + polishing_absolute_value_ub_cons = Constraint(indexed_polishing_var.index_set()) # add indexed constraints to polishing model polishing_model.add_component( @@ -438,7 +452,7 @@ def construct_dr_polishing_problem(master_data, config): polishing_absolute_value_ub_cons, ) - # update list of absolute value cons + # update list of absolute value (i.e., polishing) cons all_lb_cons.append(polishing_absolute_value_lb_cons) all_ub_cons.append(polishing_absolute_value_ub_cons) @@ -449,15 +463,14 @@ def construct_dr_polishing_problem(master_data, config): # (product of uncertain params) * dr variable dr_var_in_term = dr_monomial.args[-1] else: - # the static term (intercept); - # we do not polish this term - # continue - continue + # the static term (intercept) + dr_var_in_term = dr_monomial # we want the DR variable and corresponding polishing # constraints to have the same index in the indexed # components dr_var_in_term_idx = dr_var_in_term.index() + polishing_var = indexed_polishing_var[dr_var_in_term_idx] # Fix DR variable if: # (1) it has already been fixed from master due to @@ -475,29 +488,30 @@ def construct_dr_polishing_problem(master_data, config): for term in dr_term_copies ) if all_copy_coeffs_zero: - dr_var_in_term.fix(0) + dr_var_in_term.fix() # add polishing constraints polishing_absolute_value_lb_cons[dr_var_in_term_idx] = ( - -infinity_norm_var - dr_monomial <= 0 + -polishing_var - dr_monomial <= 0 ) polishing_absolute_value_ub_cons[dr_var_in_term_idx] = ( - dr_monomial - infinity_norm_var <= 0 + dr_monomial - polishing_var <= 0 ) # some DR variables may be fixed in the earlier # PyROS iterations for efficiency purposes if dr_var_in_term.fixed: + polishing_var.fix() polishing_absolute_value_lb_cons[dr_var_in_term_idx].deactivate() polishing_absolute_value_ub_cons[dr_var_in_term_idx].deactivate() - else: - # ensure infinity norm var properly initialized - abs_monomial_val = abs(value(dr_monomial)) - if abs_monomial_val > infinity_norm_var.value: - infinity_norm_var.set_value(abs_monomial_val) - # finally, the infinity-norm objective - polishing_model.polishing_obj = Objective(expr=infinity_norm_var) + # ensure polishing var properly initialized + polishing_var.set_value(abs(value(dr_monomial))) + + # L1-norm objective + polishing_model.polishing_obj = Objective( + expr=sum(sum(polishing_var.values()) for polishing_var in polishing_vars) + ) return polishing_model diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index 7bd8bd4d90c..ec6bd8ca1a6 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -13,6 +13,7 @@ from pyomo.core.base import ( ConcreteModel, Constraint, + minimize, Objective, Param, Var, @@ -391,25 +392,33 @@ def test_construct_dr_polishing_problem_polishing_components(self): nom_polishing_block = polishing_model.scenarios[0, 0] self.assertFalse(nom_polishing_block.decision_rule_vars[0][0].fixed) - self.assertEqual(list(polishing_model.polishing_abs_val_lb_con_0.keys()), [1]) - self.assertEqual(list(polishing_model.polishing_abs_val_ub_con_0.keys()), [1]) - - # static term unfixed; ensure polishing components active - self.assertFalse(nom_polishing_block.decision_rule_vars[0][0].fixed) + self.assertFalse(polishing_model.polishing_vars[0][0].fixed) + self.assertTrue(polishing_model.polishing_abs_val_lb_con_0[0].active) + self.assertTrue(polishing_model.polishing_abs_val_ub_con_0[0].active) # polishing components for the affine DR term should be # fixed/deactivated since the DR variable was fixed self.assertTrue(nom_polishing_block.decision_rule_vars[0][1].fixed) + self.assertTrue(polishing_model.polishing_vars[0][1].fixed) self.assertFalse(polishing_model.polishing_abs_val_lb_con_0[1].active) self.assertFalse(polishing_model.polishing_abs_val_ub_con_0[1].active) - # check initialization of infinity norm var - self.assertEqual(polishing_model.infinity_norm_var.value, 0) + # check initialization of polishing vars + self.assertEqual( + polishing_model.polishing_vars[0][0].value, + abs(nom_polishing_block.decision_rule_vars[0][0].value), + ) + self.assertEqual( + polishing_model.polishing_vars[0][1].value, + abs(nom_polishing_block.decision_rule_vars[0][1].value), + ) + assertExpressionsEqual( self, polishing_model.polishing_obj.expr, - polishing_model.infinity_norm_var, + polishing_model.polishing_vars[0][0] + polishing_model.polishing_vars[0][1], ) + self.assertEqual(polishing_model.polishing_obj.sense, minimize) def test_construct_dr_polishing_problem_objectives(self): """ From 3d3820f060df9862a16b2b8b66113bcce2c4316f Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 1 Aug 2024 13:10:43 -0400 Subject: [PATCH 2057/3044] Tweak polishing problem comments --- pyomo/contrib/pyros/master_problem_methods.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index b97cbeff2ab..f5d3594e833 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -390,9 +390,8 @@ def construct_dr_polishing_problem(master_data, config): # deactivate original constraints that involved # only vars that have been fixed. - # we do this mostly to ensure the number of active - # equality constraints does not outnumber the number of - # unfixed Vars + # we do this mostly to ensure that the active equality constraints + # do not grossly outnumber the unfixed Vars fixed_dr_vars = [ var for var in generate_all_decision_rule_var_data_objects(nominal_polishing_block) @@ -410,10 +409,10 @@ def construct_dr_polishing_problem(master_data, config): polishing_model.polishing_vars = polishing_vars = [] for idx, indexed_dr_var in enumerate(nominal_polishing_block.decision_rule_vars): - # declare auxiliary 'polishing' variables. + # auxiliary 'polishing' variables. # these are meant to represent the absolute values - # of the terms of DR polynomial; we need these for the - # L1-norm + # of the terms of DR polynomial; + # we need these for the L1-norm indexed_polishing_var = Var( list(indexed_dr_var.keys()), domain=NonNegativeReals ) @@ -498,8 +497,10 @@ def construct_dr_polishing_problem(master_data, config): dr_monomial - polishing_var <= 0 ) - # some DR variables may be fixed in the earlier - # PyROS iterations for efficiency purposes + # some DR variables may be fixed, + # due to the PyROS DR order efficiency instituted + # in the first few iterations. + # these need not be polished if dr_var_in_term.fixed: polishing_var.fix() polishing_absolute_value_lb_cons[dr_var_in_term_idx].deactivate() From 1a2ab176efecaf5daf77397c2e5b97c21cc53019 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 1 Aug 2024 14:05:57 -0400 Subject: [PATCH 2058/3044] Rephrase text of online docs methodology section --- doc/OnlineDocs/contributed_packages/pyros.rst | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index fbbeb220fc2..1fb0c77c4a1 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -16,13 +16,14 @@ Methodology Overview PyROS can accommodate optimization models with: -* **continuous variables** only -* **nonlinearities** (including **nonconvexities**) in both the +* **Continuous variables** only +* **Nonlinearities** (including **nonconvexities**) in both the variables and uncertain parameters -* **equality constraints** defining state variables, +* **First-stage degrees of freedom** and **second-stage degrees of freedom** +* **Equality constraints** defining state variables, including implicitly defined state variables that cannot be eliminated from the model via reformulation -* **first-stage degrees of freedom** and **second-stage degrees of freedom** +* **Inequality constraints** in the degree-of-freedom and/or state variables Supported deterministic models can be written in the general form @@ -41,7 +42,7 @@ where: (or "design" variables,) of which the feasible space :math:`\mathcal{X} \subseteq \mathbb{R}^{n_x}` is defined by the model constraints - (including variable bounds specifications) referencing :math:`x` only. + (including variable bounds specifications) referencing :math:`x` only * :math:`z \in \mathbb{R}^{n_z}` are the second-stage degrees of freedom (or "control" variables) * :math:`y \in \mathbb{R}^{n_y}` are the "state" variables @@ -90,14 +91,16 @@ the form of the robust counterpart addressed by PyROS is & & & \displaystyle ~~ h_j\left(x, z, y, q\right) = 0 & & \forall\,j \in \mathcal{J} \end{array} -PyROS solves problems of this form using the -Generalized Robust Cutting-Set algorithm developed in [Isenberg_et_al]_. -When using PyROS, please consider citing that paper. +PyROS accepts a deterministic model and accompanying uncertainty set +and then, +using the Generalized Robust Cutting-Set algorithm developed in [Isenberg_et_al]_, +seeks a solution to the robust counterpart. +When using PyROS, please consider citing [Isenberg_et_al]_. .. _unique-mapping: .. note:: - A key requirement of PyROS is that + A key assumption of PyROS is that for every :math:`x \in \mathcal{X}`, :math:`z \in \mathbb{R}^{n_z}`, @@ -106,7 +109,7 @@ When using PyROS, please consider citing that paper. for which :math:`(x, z, y, q)` satisfies the equality constraints :math:`h_j(x, z, y, q) = 0\,\,\forall\, j \in \mathcal{J}`. - If this requirement is not met, + If this assumption is not met, then the selection of 'state' (i.e., not degree of freedom) variables :math:`y` is incorrect, and one or more of the :math:`y` variables should be appropriately From 1556123802eb0c7cbb926dc78d15b5cff94b5049 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 1 Aug 2024 14:12:50 -0400 Subject: [PATCH 2059/3044] Add copyright statement to testing modules --- pyomo/contrib/pyros/tests/test_config.py | 11 +++++++++++ pyomo/contrib/pyros/tests/test_master.py | 11 +++++++++++ pyomo/contrib/pyros/tests/test_preprocessor.py | 11 +++++++++++ pyomo/contrib/pyros/tests/test_separation.py | 11 +++++++++++ pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 11 +++++++++++ 5 files changed, 55 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_config.py b/pyomo/contrib/pyros/tests/test_config.py index 166fbada4ff..b30db03c3e8 100644 --- a/pyomo/contrib/pyros/tests/test_config.py +++ b/pyomo/contrib/pyros/tests/test_config.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Test objects for construction of PyROS ConfigDict. """ diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index ec6bd8ca1a6..b4c3d79dfd1 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Test methods for construction and solution of master problem objects. diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index 3c75816aa87..c5384e8b49f 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Tests for the PyROS preprocessor. """ diff --git a/pyomo/contrib/pyros/tests/test_separation.py b/pyomo/contrib/pyros/tests/test_separation.py index 7f44dc440ee..d61285bfcc1 100644 --- a/pyomo/contrib/pyros/tests/test_separation.py +++ b/pyomo/contrib/pyros/tests/test_separation.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Test separation problem construction methods. """ diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index bd0d4121ddb..076cca4dea0 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + """ Tests for the PyROS UncertaintySet class and subclasses. """ From 6901f43c2e4a0026b5bac646ffdbae6332431db5 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 1 Aug 2024 14:14:58 -0400 Subject: [PATCH 2060/3044] Modify PyROS solver test module docstring --- pyomo/contrib/pyros/tests/test_grcs.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 7a357b71b0e..cc6e927156e 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -9,10 +9,9 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -''' -Unit tests for the grcs API -One class per function being tested, minimum one test per class -''' +""" +Tests for the PyROS solver. +""" import logging import math From 361ae9335d52ac2d18b8d88a5b67c9f48e689ca3 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 1 Aug 2024 19:35:32 -0400 Subject: [PATCH 2061/3044] Drop static DR terms from polishing norm --- pyomo/contrib/pyros/master_problem_methods.py | 2 +- pyomo/contrib/pyros/tests/test_master.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index f5d3594e833..db55061303f 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -501,7 +501,7 @@ def construct_dr_polishing_problem(master_data, config): # due to the PyROS DR order efficiency instituted # in the first few iterations. # these need not be polished - if dr_var_in_term.fixed: + if dr_var_in_term.fixed or not is_a_nonstatic_dr_term: polishing_var.fix() polishing_absolute_value_lb_cons[dr_var_in_term_idx].deactivate() polishing_absolute_value_ub_cons[dr_var_in_term_idx].deactivate() diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index b4c3d79dfd1..ced00f5925f 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -403,9 +403,9 @@ def test_construct_dr_polishing_problem_polishing_components(self): nom_polishing_block = polishing_model.scenarios[0, 0] self.assertFalse(nom_polishing_block.decision_rule_vars[0][0].fixed) - self.assertFalse(polishing_model.polishing_vars[0][0].fixed) - self.assertTrue(polishing_model.polishing_abs_val_lb_con_0[0].active) - self.assertTrue(polishing_model.polishing_abs_val_ub_con_0[0].active) + self.assertTrue(polishing_model.polishing_vars[0][0].fixed) + self.assertFalse(polishing_model.polishing_abs_val_lb_con_0[0].active) + self.assertFalse(polishing_model.polishing_abs_val_ub_con_0[0].active) # polishing components for the affine DR term should be # fixed/deactivated since the DR variable was fixed From 43e71280a3d833449c37b3df30dfbfa31ceb08e7 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 1 Aug 2024 20:16:20 -0400 Subject: [PATCH 2062/3044] Add todo comment on DR polishing obj --- pyomo/contrib/pyros/master_problem_methods.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index db55061303f..dcfac1aa378 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -510,6 +510,9 @@ def construct_dr_polishing_problem(master_data, config): polishing_var.set_value(abs(value(dr_monomial))) # L1-norm objective + # TODO: if dropping nonstatic terms, ensure the + # corresponding polishing variables are excluded + # from this expression polishing_model.polishing_obj = Objective( expr=sum(sum(polishing_var.values()) for polishing_var in polishing_vars) ) From 67f0cf9417a627f00a780864c24e7ab93c782df8 Mon Sep 17 00:00:00 2001 From: whart222 Date: Fri, 2 Aug 2024 05:07:17 -0600 Subject: [PATCH 2063/3044] Adding online documentation --- .../alternative_solutions.rst | 100 ++++++++++++++++++ doc/OnlineDocs/contributed_packages/index.rst | 1 + pyomo/contrib/alternative_solutions/balas.py | 78 +++++++------- .../alternative_solutions/lp_enum_solnpool.py | 62 +++++------ pyomo/contrib/alternative_solutions/obbt.py | 76 ++++++------- .../alternative_solutions/shifted_lp.py | 22 ++-- .../contrib/alternative_solutions/solnpool.py | 54 +++++----- 7 files changed, 247 insertions(+), 146 deletions(-) create mode 100644 doc/OnlineDocs/contributed_packages/alternative_solutions.rst diff --git a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst new file mode 100644 index 00000000000..e6fa3655a5d --- /dev/null +++ b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst @@ -0,0 +1,100 @@ +############################################### +Generating Alternative (Near-)Optimal Solutions +############################################### + +Optimization solvers are generally designed to return a feasible solution +to the user. However, there are many applications where a users needs +more context than this result. For example, + +* alternative solutions can support an assessment of trade-offs between competing objectives; +* if the optimization formulation may be inaccurate or untrustworthy, then comparisons amongst alternative solutions provides additional insights into the reliability of these model predictions; or +* the user may have unexpressed objectives or constraints, which only are realized in later stages of model analysis. + +The *alternative-solutions library* provides a variety of functions that +can be used to generate optimal or near-optimal solutions for a pyomo +model. Conceptually, these functions are like pyomo solvers. They can +be configured with solver names and options, and they return a list of +solutions for the pyomo model. However, these functions are independent +of pyomo's solver interface because they return a custom solution object. + +The following functions are defined in the alternative-solutions library: + +* enumerate_binary_solutions + + * Finds alternative optimal solutions for a binary problem using no-good cuts. + +* enumerate_linear_solutions + + * Finds alternative optimal solutions a (mixed-integer) linear program. + +* enumerate_linear_solutions_soln_pool + + * Finds alternative optimal solutions for a (mixed-binary) linear program using Gurobi's solution pool feature. + +* gurobi_generate_solutions + + * Finds alternative optimal solutions for discrete variables using Gurobi's built-in Solution Pool capability. + +* obbt_analysis_bounds_and_solutions + + * Calculates the bounds on each variable by solving a series of min and max optimization problems where each variable is used as the objective function. This can be applied to any class of problem supported by the selected solver. + + +Usage Example +------------- + +Many of functions in the alternative-solutions library have similar options, so we simply illustrate the ``enumerate_binary_solutions`` function. We define a simple model whose feasible space is a isosceles right triangle. The optimal solutiosn fall along the hypotenuse, where :math:`x + y == 5`. Alternative near-optimal feasible points have integer objective values ranging from 0 to 4. + +.. doctest:: + + >>> import pyomo.environ as pyo + + >>> m = pyo.ConcreteModel() + >>> m.x = pyo.Var(within=pyo.NonNegativeIntegers, bounds=(0, 5)) + >>> m.y = pyo.Var(within=pyo.NonNegativeIntegers, bounds=(0, 5)) + >>> m.o = pyo.Objective(expr=m.x + m.y, sense=pyo.maximize) + >>> m.c = pyo.Constraint(expr=m.x + m.y <= 5) + +We can execute the ``enumerate_binary_solutions`` function to generate a list of ``Solution`` objects that represent alternative optimal solutions: + +.. doctest:: + :skipif: not gurobi_available + + >>> import pyomo.contrib.alternative_solutions as aos + >>> solns = aos.enumerate_binary_solutions(m, num_solutions=100, solver="gurobi") + >>> assert len(solns) == 1 + +Each ``Solution`` object constains information about the objective and variables, and it includes various methods to access this information. For example: + +.. doctest:: + :skipif: not gurobi_available + + >>> print(solns[0]) +{ + "fixed_variables": [], + "objective": "o", + "objective_value": 5.0, + "solution": { + "x": 5, + "y": 0 + } +} + + +Interface Documentation +----------------------- + +.. currentmodule:: pyomo.contrib.alternative_solutions + +.. autofunction:: enumerate_binary_solutions + +.. autofunction:: enumerate_linear_solutions + +.. autofunction:: enumerate_linear_solutions_soln_pool + +.. autofunction:: gurobi_generate_solutions + +.. autofunction:: obbt_analysis_bounds_and_solutions + +.. autoclass: Solution + diff --git a/doc/OnlineDocs/contributed_packages/index.rst b/doc/OnlineDocs/contributed_packages/index.rst index b1d9cbbad3b..65c14a721df 100644 --- a/doc/OnlineDocs/contributed_packages/index.rst +++ b/doc/OnlineDocs/contributed_packages/index.rst @@ -15,6 +15,7 @@ Contributed packages distributed with Pyomo: .. toctree:: :maxdepth: 1 + alternative_solutions.rst community.rst doe/doe.rst gdpopt.rst diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index 16a494cd067..dbcc155cbbc 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -33,46 +33,46 @@ def enumerate_binary_solutions( Finds alternative optimal solutions for a binary problem using no-good cuts. - Parameters - ---------- - model : ConcreteModel - A concrete Pyomo model - num_solutions : int - The maximum number of solutions to generate. - variables: 'all' or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. 'all' indicates - that all variables will be included. Alternatively, a collection of - _GenereralVarData variables can be provided. - rel_opt_gap : float or None - The relative optimality gap for the original objective for which - variable bounds will be found. None indicates that a relative gap - constraint will not be added to the model. - abs_opt_gap : float or None - The absolute optimality gap for the original objective for which - variable bounds will be found. None indicates that an absolute gap - constraint will not be added to the model. - search_mode : 'optimal', 'random', or 'hamming' - Indicates the mode that is used to generate alternative solutions. - The optimal mode finds the next best solution. The random mode - finds an alternative solution in the direction of a random ray. The - hamming mode iteratively finds solution that maximize the hamming - distance from previously discovered solutions. - solver : string - The solver to be used. - solver_options : dict - Solver option-value pairs to be passed to the solver. - tee : boolean - Boolean indicating that the solver output should be displayed. - quiet : boolean - Boolean indicating whether to suppress all output. - seed : int - Optional integer seed for the numpy random number generator + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model + num_solutions : int + The maximum number of solutions to generate. + variables: 'all' or a collection of Pyomo _GeneralVarData variables + The variables for which bounds will be generated. 'all' indicates + that all variables will be included. Alternatively, a collection of + _GenereralVarData variables can be provided. + rel_opt_gap : float or None + The relative optimality gap for the original objective for which + variable bounds will be found. None indicates that a relative gap + constraint will not be added to the model. + abs_opt_gap : float or None + The absolute optimality gap for the original objective for which + variable bounds will be found. None indicates that an absolute gap + constraint will not be added to the model. + search_mode : 'optimal', 'random', or 'hamming' + Indicates the mode that is used to generate alternative solutions. + The optimal mode finds the next best solution. The random mode + finds an alternative solution in the direction of a random ray. The + hamming mode iteratively finds solution that maximize the hamming + distance from previously discovered solutions. + solver : string + The solver to be used. + solver_options : dict + Solver option-value pairs to be passed to the solver. + tee : boolean + Boolean indicating that the solver output should be displayed. + quiet : boolean + Boolean indicating whether to suppress all output. + seed : int + Optional integer seed for the numpy random number generator - Returns - ------- - solutions - A list of Solution objects. - [Solution] + Returns + ------- + solutions + A list of Solution objects. + [Solution] """ if not quiet: # pragma: no cover print("STARTING NO-GOOD CUT ANALYSIS") diff --git a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py index b9ee63e9347..cc584dd848d 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py @@ -91,37 +91,37 @@ def enumerate_linear_solutions_soln_pool( Finds alternative optimal solutions for a (mixed-binary) linear program using Gurobi's solution pool feature. - Parameters - ---------- - model : ConcreteModel - A concrete Pyomo model - num_solutions : int - The maximum number of solutions to generate. - variables: 'all' or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. 'all' indicates - that all variables will be included. Alternatively, a collection of - _GenereralVarData variables can be provided. - rel_opt_gap : float or None - The relative optimality gap for the original objective for which - variable bounds will be found. None indicates that a relative gap - constraint will not be added to the model. - abs_opt_gap : float or None - The absolute optimality gap for the original objective for which - variable bounds will be found. None indicates that an absolute gap - constraint will not be added to the model. - zero_threshold: float - The threshold for which a continuous variables' value is considered - to be equal to zero. - solver_options : dict - Solver option-value pairs to be passed to the solver. - tee : boolean - Boolean indicating that the solver output should be displayed. - - Returns - ------- - solutions - A list of Solution objects. - [Solution] + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model + num_solutions : int + The maximum number of solutions to generate. + variables: 'all' or a collection of Pyomo _GeneralVarData variables + The variables for which bounds will be generated. 'all' indicates + that all variables will be included. Alternatively, a collection of + _GenereralVarData variables can be provided. + rel_opt_gap : float or None + The relative optimality gap for the original objective for which + variable bounds will be found. None indicates that a relative gap + constraint will not be added to the model. + abs_opt_gap : float or None + The absolute optimality gap for the original objective for which + variable bounds will be found. None indicates that an absolute gap + constraint will not be added to the model. + zero_threshold: float + The threshold for which a continuous variables' value is considered + to be equal to zero. + solver_options : dict + Solver option-value pairs to be passed to the solver. + tee : boolean + Boolean indicating that the solver output should be displayed. + + Returns + ------- + solutions + A list of Solution objects. + [Solution] """ print("STARTING LP ENUMERATION ANALYSIS USING GUROBI SOLUTION POOL") # diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index ca4b54d7495..ea0ed44c574 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -34,44 +34,44 @@ def obbt_analysis( This can be applied to any class of problem supported by the selected solver. - Parameters - ---------- - model : ConcreteModel - A concrete Pyomo model. - variables: 'all' or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. 'all' indicates - that all variables will be included. Alternatively, a collection of - _GenereralVarData variables can be provided. - rel_opt_gap : float or None - The relative optimality gap for the original objective for which - variable bounds will be found. None indicates that a relative gap - constraint will not be added to the model. - abs_opt_gap : float or None - The absolute optimality gap for the original objective for which - variable bounds will be found. None indicates that an absolute gap - constraint will not be added to the model. - refine_discrete_bounds : boolean - Boolean indicating that new constraints should be added to the - model at each iteration to tighten the bounds for discrete - variables. - warmstart : boolean - Boolean indicating that the solver should be warmstarted from the - best previously discovered solution. - solver : string - The solver to be used. - solver_options : dict - Solver option-value pairs to be passed to the solver. - tee : boolean - Boolean indicating that the solver output should be displayed. - quiet : boolean - Boolean indicating whether to suppress all output. - - Returns - ------- - variable_ranges - A Pyomo ComponentMap containing the bounds for each variable. - {variable: (lower_bound, upper_bound)}. An exception is raised when - the solver encountered an issue. + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model. + variables: 'all' or a collection of Pyomo _GeneralVarData variables + The variables for which bounds will be generated. 'all' indicates + that all variables will be included. Alternatively, a collection of + _GenereralVarData variables can be provided. + rel_opt_gap : float or None + The relative optimality gap for the original objective for which + variable bounds will be found. None indicates that a relative gap + constraint will not be added to the model. + abs_opt_gap : float or None + The absolute optimality gap for the original objective for which + variable bounds will be found. None indicates that an absolute gap + constraint will not be added to the model. + refine_discrete_bounds : boolean + Boolean indicating that new constraints should be added to the + model at each iteration to tighten the bounds for discrete + variables. + warmstart : boolean + Boolean indicating that the solver should be warmstarted from the + best previously discovered solution. + solver : string + The solver to be used. + solver_options : dict + Solver option-value pairs to be passed to the solver. + tee : boolean + Boolean indicating that the solver output should be displayed. + quiet : boolean + Boolean indicating whether to suppress all output. + + Returns + ------- + variable_ranges + A Pyomo ComponentMap containing the bounds for each variable. + {variable: (lower_bound, upper_bound)}. An exception is raised when + the solver encountered an issue. """ bounds, solns = obbt_analysis_bounds_and_solutions( model, diff --git a/pyomo/contrib/alternative_solutions/shifted_lp.py b/pyomo/contrib/alternative_solutions/shifted_lp.py index 1575306a9e3..3e3a8a3f3f8 100644 --- a/pyomo/contrib/alternative_solutions/shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/shifted_lp.py @@ -67,17 +67,17 @@ def get_shifted_linear_model(model, block=None): networks, Computers & Chemical Engineering, Volume 24, Issues 2–7, 2000, page 712 for additional details. - Parameters - ---------- - model : ConcreteModel - A concrete Pyomo model - block : Block - The Pyomo block that the new model should be added to. - - Returns - ------- - block - The block that holds the reformulated model. + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model + block : Block + The Pyomo block that the new model should be added to. + + Returns + ------- + block + The block that holds the reformulated model. """ # Gather all variables and confirm the model is bounded diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index b7575a8194f..97d949f65c2 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -34,34 +34,34 @@ def gurobi_generate_solutions( built-in Solution Pool capability. See the Gurobi Solution Pool documentation for additional details. - Parameters - ---------- - model : ConcreteModel - A concrete Pyomo model. - num_solutions : int - The maximum number of solutions to generate. This parameter maps to - the PoolSolutions parameter in Gurobi. - rel_opt_gap : non-negative float or None - The relative optimality gap for allowable alternative solutions. - None implies that there is no limit on the relative optimality gap - (i.e. that any feasible solution can be considered by Gurobi). - This parameter maps to the PoolGap parameter in Gurobi. - abs_opt_gap : non-negative float or None - The absolute optimality gap for allowable alternative solutions. - None implies that there is no limit on the absolute optimality gap - (i.e. that any feasible solution can be considered by Gurobi). - This parameter maps to the PoolGapAbs parameter in Gurobi. - solver_options : dict - Solver option-value pairs to be passed to the Gurobi solver. - tee : boolean - Boolean indicating that the solver output should be displayed. - quiet : boolean - Boolean indicating whether to suppress all output. + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model. + num_solutions : int + The maximum number of solutions to generate. This parameter maps to + the PoolSolutions parameter in Gurobi. + rel_opt_gap : non-negative float or None + The relative optimality gap for allowable alternative solutions. + None implies that there is no limit on the relative optimality gap + (i.e. that any feasible solution can be considered by Gurobi). + This parameter maps to the PoolGap parameter in Gurobi. + abs_opt_gap : non-negative float or None + The absolute optimality gap for allowable alternative solutions. + None implies that there is no limit on the absolute optimality gap + (i.e. that any feasible solution can be considered by Gurobi). + This parameter maps to the PoolGapAbs parameter in Gurobi. + solver_options : dict + Solver option-value pairs to be passed to the Gurobi solver. + tee : boolean + Boolean indicating that the solver output should be displayed. + quiet : boolean + Boolean indicating whether to suppress all output. - Returns - ------- - solutions - A list of Solution objects. [Solution] + Returns + ------- + solutions + A list of Solution objects. [Solution] """ # # Setup gurobi From a2639e57320b85d675b7de97eae9c2d864776642 Mon Sep 17 00:00:00 2001 From: whart222 Date: Fri, 2 Aug 2024 05:15:06 -0600 Subject: [PATCH 2064/3044] Fixing docs --- .../alternative_solutions.rst | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst index e6fa3655a5d..793673882d4 100644 --- a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst +++ b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst @@ -7,7 +7,9 @@ to the user. However, there are many applications where a users needs more context than this result. For example, * alternative solutions can support an assessment of trade-offs between competing objectives; + * if the optimization formulation may be inaccurate or untrustworthy, then comparisons amongst alternative solutions provides additional insights into the reliability of these model predictions; or + * the user may have unexpressed objectives or constraints, which only are realized in later stages of model analysis. The *alternative-solutions library* provides a variety of functions that @@ -19,23 +21,23 @@ of pyomo's solver interface because they return a custom solution object. The following functions are defined in the alternative-solutions library: -* enumerate_binary_solutions +* ``enumerate_binary_solutions`` * Finds alternative optimal solutions for a binary problem using no-good cuts. -* enumerate_linear_solutions +* ``enumerate_linear_solutions`` * Finds alternative optimal solutions a (mixed-integer) linear program. -* enumerate_linear_solutions_soln_pool +* ``enumerate_linear_solutions_soln_pool`` * Finds alternative optimal solutions for a (mixed-binary) linear program using Gurobi's solution pool feature. -* gurobi_generate_solutions +* ``gurobi_generate_solutions`` * Finds alternative optimal solutions for discrete variables using Gurobi's built-in Solution Pool capability. -* obbt_analysis_bounds_and_solutions +* ``obbt_analysis_bounds_and_solutions`` * Calculates the bounds on each variable by solving a series of min and max optimization problems where each variable is used as the objective function. This can be applied to any class of problem supported by the selected solver. @@ -70,15 +72,15 @@ Each ``Solution`` object constains information about the objective and variables :skipif: not gurobi_available >>> print(solns[0]) -{ - "fixed_variables": [], - "objective": "o", - "objective_value": 5.0, - "solution": { - "x": 5, - "y": 0 - } -} + { + "fixed_variables": [], + "objective": "o", + "objective_value": 5.0, + "solution": { + "x": 5, + "y": 0 + } + } Interface Documentation @@ -96,5 +98,5 @@ Interface Documentation .. autofunction:: obbt_analysis_bounds_and_solutions -.. autoclass: Solution +.. autoclass:: Solution From 3d6031c51c521bf5bf9a836c46645f1783e266ee Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 2 Aug 2024 06:10:48 -0600 Subject: [PATCH 2065/3044] Track rename of normalize_constraint to to_bounded_expression --- pyomo/core/base/constraint.py | 4 ++-- pyomo/repn/linear_template.py | 3 +-- pyomo/repn/plugins/standard_form.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 4fbf61a5d6c..b661fec59e8 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -556,10 +556,10 @@ def set_value(self, expr): self.__class__ = ConstraintData return self.set_value(expr) - def normalize_constraint(self): + def to_bounded_expression(self): tmp, self._expr = self._expr, self._expr[0] try: - return super().normalize_constraint() + return super().to_bounded_expression() finally: self._expr = tmp diff --git a/pyomo/repn/linear_template.py b/pyomo/repn/linear_template.py index 7a00a1d5cf3..c0377958d49 100644 --- a/pyomo/repn/linear_template.py +++ b/pyomo/repn/linear_template.py @@ -266,7 +266,6 @@ def _before_named_expression(self, visitor, child): raise NotImplementedError() - def _handle_getitem(visitor, node, comp, *args): expr = comp[1][tuple(arg[1] for arg in args)] if comp[0] is _CONSTANT: @@ -339,7 +338,7 @@ def expand_expression(self, obj, template_info): expr, indices = template_info args = [smap.getSymbol(i) for i in indices] if expr.is_expression_type(ExpressionType.RELATIONAL): - lb, body, ub = obj.normalize_constraint() + lb, body, ub = obj.to_bounded_expression() if body is not None: body = self.walk_expression(body).compile( env, smap, self.expr_cache, args, False diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index ef2f8ba9332..dda64eaa84b 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -405,7 +405,7 @@ def write(self, model): linear_index = map(var_order.__getitem__, linear_index) else: # Note: lb and ub could be a number, expression, or None - lb, body, ub = con.normalize_constraint() + lb, body, ub = con.to_bounded_expression() if lb.__class__ not in native_types: lb = value(lb) if ub.__class__ not in native_types: From e918f18d1b882e465956e5c9e2c9b444a29a6974 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 2 Aug 2024 06:27:22 -0600 Subject: [PATCH 2066/3044] Abstract var recording into separate class; directly generate index from var in template expressions --- pyomo/repn/linear.py | 66 ++--- pyomo/repn/linear_template.py | 20 +- pyomo/repn/parameterized_linear.py | 14 +- pyomo/repn/plugins/lp_writer.py | 20 +- pyomo/repn/plugins/standard_form.py | 21 +- pyomo/repn/quadratic.py | 13 +- pyomo/repn/tests/test_linear.py | 259 +++++++++--------- pyomo/repn/tests/test_parameterized_linear.py | 72 ++--- pyomo/repn/tests/test_quadratic.py | 76 ++--- pyomo/repn/util.py | 102 +++++++ 10 files changed, 395 insertions(+), 268 deletions(-) diff --git a/pyomo/repn/linear.py b/pyomo/repn/linear.py index 8dd9fbf139b..e5aece86556 100644 --- a/pyomo/repn/linear.py +++ b/pyomo/repn/linear.py @@ -47,7 +47,11 @@ BeforeChildDispatcher, ExitNodeDispatcher, ExprType, + FileDeterminism, + FileDeterminism_to_SortComponents, InvalidNumber, + OrderedVarRecorder, + VarRecorder, apply_node_operation, complex_number_error, initialize_exit_node_dispatcher, @@ -533,36 +537,13 @@ def __init__(self): self[LinearExpression] = self._before_linear self[SumExpression] = self._before_general_expression - def record_var(self, visitor, var): - # We always add all indices to the var_map at once so that - # we can honor deterministic ordering of unordered sets - # (because the user could have iterated over an unordered - # set when constructing an expression, thereby altering the - # order in which we would see the variables) - vm = visitor.var_map - vo = visitor.var_order - l = len(vo) - try: - _iter = var.parent_component().values(visitor.sorter) - except AttributeError: - # Note that this only works for the AML, as kernel does not - # provide a parent_component() - _iter = (var,) - for v in _iter: - if v.fixed: - continue - vid = id(v) - vm[vid] = v - vo[vid] = l - l += 1 - @staticmethod def _before_var(visitor, child): _id = id(child) if _id not in visitor.var_map: if child.fixed: return False, (_CONSTANT, visitor.check_constant(child.value, child)) - visitor.before_child_dispatcher.record_var(visitor, child) + visitor.var_recorder.add(child) ans = visitor.Result() ans.linear[_id] = 1 return False, (_LINEAR, ans) @@ -591,7 +572,7 @@ def _before_monomial(visitor, child): _CONSTANT, arg1 * visitor.check_constant(arg2.value, arg2), ) - visitor.before_child_dispatcher.record_var(visitor, arg2) + visitor.var_recorder.add(arg2) # Trap multiplication by 0 and nan. if not arg1: @@ -614,7 +595,6 @@ def _before_monomial(visitor, child): @staticmethod def _before_linear(visitor, child): var_map = visitor.var_map - var_order = visitor.var_order ans = visitor.Result() const = 0 linear = ans.linear @@ -646,7 +626,7 @@ def _before_linear(visitor, child): if arg2.fixed: const += arg1 * visitor.check_constant(arg2.value, arg2) continue - visitor.before_child_dispatcher.record_var(visitor, arg2) + visitor.var_recorder.add(arg2) linear[_id] = arg1 elif _id in linear: linear[_id] += arg1 @@ -660,7 +640,7 @@ def _before_linear(visitor, child): if arg.fixed: const += visitor.check_constant(arg.value, arg) continue - visitor.before_child_dispatcher.record_var(visitor, arg) + visitor.var_recorder.add(arg) linear[_id] = 1 elif _id in linear: linear[_id] += 1 @@ -711,12 +691,34 @@ class LinearRepnVisitor(StreamBasedExpressionVisitor): expand_nonlinear_products = False max_exponential_expansion = 1 - def __init__(self, subexpression_cache, var_map, var_order, sorter): + def __init__( + self, + subexpression_cache, + var_map=None, + var_order=None, + sorter=None, + var_recorder=None, + ): super().__init__() self.subexpression_cache = subexpression_cache - self.var_map = var_map - self.var_order = var_order - self.sorter = sorter + if any(_ is not None for _ in (var_map, var_order, sorter)): + if var_recorder is not None: + raise ValueError( + "LinearRepnVisitor: cannot specify any of var_map, " + "var_order, or sorter with var_recorder" + ) + deprecation_warning( + "var_map, var_order, and sorter are deprecated arguments to " + "LinearRepnVisitor(). Please pass the VarRecorder object directly.", + version='6.7.4.dev0', + ) + var_recorder = OrderedVarRecorder(var_map, var_order, sorter) + if var_recorder is None: + var_recorder = VarRecorder( + {}, FileDeterminism_to_SortComponents(FileDeterminism.ORDERED) + ) + self.var_recorder = var_recorder + self.var_map = var_recorder.var_map self._eval_expr_visitor = _EvaluationVisitor(True) self.evaluate = self._eval_expr_visitor.dfs_postorder_stack diff --git a/pyomo/repn/linear_template.py b/pyomo/repn/linear_template.py index c0377958d49..7521274dcee 100644 --- a/pyomo/repn/linear_template.py +++ b/pyomo/repn/linear_template.py @@ -18,7 +18,6 @@ import pyomo.repn.linear as linear import pyomo.repn.util as util -from pyomo.core.base import NumericLabeler from pyomo.core.expr import ExpressionType from pyomo.repn.linear import LinearRepn @@ -227,17 +226,16 @@ def record_var(self, visitor, var): # order in which we would see the variables) vm = visitor.var_map _iter = var_comp.items(visitor.sorter) - for idx, v in _iter: + for i, (idx, v) in enumerate(_iter, start=len(ve)): # if v.fixed: # ve[idx] = (v.value,) # continue - vid = id(v) - vm[vid] = v - ve[idx] = vid + vm[id(v)] = v + ve[idx] = 0 if v.fixed else i def _before_indexed_var(self, visitor, child): if child not in visitor.indexed_vars: - visitor.before_child_dispatcher.record_var(visitor, child) + visitor.var_recorder(child) visitor.indexed_vars.add(child) return False, (_VARIABLE, child) @@ -307,15 +305,13 @@ class LinearTemplateRepnVisitor(linear.LinearRepnVisitor): util.initialize_exit_node_dispatcher(define_exit_node_handlers()) ) - def __init__( - self, subexpression_cache, var_map, var_order, sorter, remove_fixed_vars=False - ): - super().__init__(subexpression_cache, var_map, var_order, sorter) + def __init__(self, subexpression_cache, var_recorder, remove_fixed_vars=False): + super().__init__(subexpression_cache, var_recorder=var_recorder) self.indexed_vars = set() self.indexed_params = set() self.expr_cache = {} - self.env = {} - self.symbolmap = expr.SymbolMap(NumericLabeler('x')) + self.env = var_recorder.env + self.symbolmap = var_recorder.symbolmap self.expanded_templates = {} self.remove_fixed_vars = remove_fixed_vars diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index 3e8f712f562..e4c7a4ce628 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -199,7 +199,7 @@ def _before_var(visitor, child): # We aren't treating this Var as a Var for the purposes of this walker return False, (_FIXED, child) # This is a normal situation - visitor.before_child_dispatcher.record_var(visitor, child) + visitor.var_recorder.add(child) ans = visitor.Result() ans.linear[_id] = 1 return False, (ExprType.LINEAR, ans) @@ -333,8 +333,16 @@ class ParameterizedLinearRepnVisitor(LinearRepnVisitor): initialize_exit_node_dispatcher(define_exit_node_handlers()) ) - def __init__(self, subexpression_cache, var_map, var_order, sorter, wrt): - super().__init__(subexpression_cache, var_map, var_order, sorter) + def __init__( + self, + subexpression_cache, + var_map=None, + var_order=None, + sorter=None, + wrt=None, + var_recorder=None, + ): + super().__init__(subexpression_cache, var_map, var_order, sorter, var_recorder) self.wrt = ComponentSet(_flattened(wrt)) def beforeChild(self, node, child, child_idx): diff --git a/pyomo/repn/plugins/lp_writer.py b/pyomo/repn/plugins/lp_writer.py index 814f79a4eb9..fe1671cf865 100644 --- a/pyomo/repn/plugins/lp_writer.py +++ b/pyomo/repn/plugins/lp_writer.py @@ -43,6 +43,7 @@ from pyomo.repn.util import ( FileDeterminism, FileDeterminism_to_SortComponents, + OrderedVarRecorder, categorize_valid_components, initialize_var_map_from_column_order, int_float, @@ -267,7 +268,9 @@ def write(self, model): aliasSymbol = self.symbol_map.alias getSymbol = self.symbol_map.getSymbol - sorter = FileDeterminism_to_SortComponents(self.config.file_determinism) + self.sorter = sorter = FileDeterminism_to_SortComponents( + self.config.file_determinism + ) component_map, unknown = categorize_valid_components( model, active=True, @@ -303,20 +306,19 @@ def write(self, model): ONE_VAR_CONSTANT = Var(name='ONE_VAR_CONSTANT', bounds=(1, 1)) ONE_VAR_CONSTANT.construct() - self.var_map = var_map = {id(ONE_VAR_CONSTANT): ONE_VAR_CONSTANT} - initialize_var_map_from_column_order(model, self.config, var_map) - self.var_order = {_id: i for i, _id in enumerate(var_map)} + self.var_map = {id(ONE_VAR_CONSTANT): ONE_VAR_CONSTANT} + initialize_var_map_from_column_order(model, self.config, self.var_map) + self.var_order = {_id: i for i, _id in enumerate(self.var_map)} + self.var_recorder = OrderedVarRecorder(self.var_map, self.var_order, sorter) _qp = self.config.allow_quadratic_objective _qc = self.config.allow_quadratic_constraint objective_visitor = (QuadraticRepnVisitor if _qp else LinearRepnVisitor)( - {}, var_map, self.var_order, sorter + {}, var_recorder=self.var_recorder ) constraint_visitor = (QuadraticRepnVisitor if _qc else LinearRepnVisitor)( objective_visitor.subexpression_cache if _qp == _qc else {}, - var_map, - self.var_order, - sorter, + var_recorder=self.var_recorder, ) timer.toc('Initialized column order', level=logging.DEBUG) @@ -511,7 +513,7 @@ def write(self, model): integer_vars = [] binary_vars = [] getSymbolByObjectID = self.symbol_map.byObject.get - for vid, v in var_map.items(): + for vid, v in self.var_map.items(): # Some variables in the var_map may not actually have been # written out to the LP file (e.g., added from col_order, or # multiplied by 0 in the expressions). Check to see that diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index dda64eaa84b..273d192d7bf 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -43,6 +43,7 @@ from pyomo.repn.util import ( FileDeterminism, FileDeterminism_to_SortComponents, + TemplateVarRecorder, categorize_valid_components, initialize_var_map_from_column_order, ordered_active_constraints, @@ -300,10 +301,10 @@ def write(self, model): self.var_map = var_map = {} initialize_var_map_from_column_order(model, self.config, var_map) - var_order = {_id: i for i, _id in enumerate(var_map)} - visitor = self._get_visitor({}, var_map, var_order, sorter) - template_visitor = LinearTemplateRepnVisitor({}, var_map, var_order, sorter) + var_recorder = TemplateVarRecorder(var_map, None, sorter) + visitor = self._get_visitor({}, var_recorder=var_recorder) + template_visitor = LinearTemplateRepnVisitor({}, var_recorder=var_recorder) timer.toc('Initialized column order', level=logging.DEBUG) @@ -353,13 +354,13 @@ def write(self, model): template_visitor.expand_expression(obj, obj.template_expr()) ) N = len(linear_index) - obj_index.append(map(var_order.__getitem__, linear_index)) + obj_index.append(linear_index) obj_data.append(linear_data) obj_offset.append(offset) else: repn = visitor.walk_expression(obj.expr) N = len(repn.linear) - obj_index.append(map(var_order.__getitem__, repn.linear)) + obj_index.append(map(var_recorder.var_order.__getitem__, repn.linear)) obj_data.append(repn.linear.values()) obj_offset.append(repn.constant) @@ -402,7 +403,6 @@ def write(self, model): template_visitor.expand_expression(con, con.template_expr()) ) N = len(linear_data) - linear_index = map(var_order.__getitem__, linear_index) else: # Note: lb and ub could be a number, expression, or None lb, body, ub = con.to_bounded_expression() @@ -420,7 +420,7 @@ def write(self, model): N = len(repn.linear) # Pull out the constant: we will move it to the bounds offset = repn.constant - linear_index = map(var_order.__getitem__, repn.linear) + linear_index = map(var_recorder.var_order.__getitem__, repn.linear) linear_data = repn.linear.values() if lb is None and ub is None: @@ -480,7 +480,10 @@ def write(self, model): if ub is not None: v.lb = lb - ub var_map[id(v)] = v - var_order[id(v)] = slack_col = len(var_order) + if var_recorder.var_order is not None: + var_recorder.var_order[id(v)] = slack_col = len( + var_recorder.var_order + ) linear_data = list(linear_data) linear_data.append(1) linear_index = list(linear_index) @@ -513,7 +516,7 @@ def write(self, model): timer.toc('Constraint %s', last_parent(), level=logging.DEBUG) # Get the variable list - var_order.update({_id: i for i, _id in enumerate(var_map)}) + # var_order.update({_id: i for i, _id in enumerate(var_map)}) columns = list(var_map.values()) nCol = len(columns) diff --git a/pyomo/repn/quadratic.py b/pyomo/repn/quadratic.py index 1568f721eb5..a3659a6157e 100644 --- a/pyomo/repn/quadratic.py +++ b/pyomo/repn/quadratic.py @@ -158,11 +158,14 @@ def append(self, other): self.nonlinear += nl -def _mul_linear_linear(varOrder, linear1, linear2): +def _mul_linear_linear(linear1, linear2): quadratic = {} for vid1, coef1 in linear1.items(): for vid2, coef2 in linear2.items(): - if varOrder(vid1) < varOrder(vid2): + # Note that this is random. If the client cares about + # determinism, it may need to reverse the keys based on + # something more deterministic than vid + if vid1 < vid2: key = vid1, vid2 else: key = vid2, vid1 @@ -177,9 +180,7 @@ def _handle_product_linear_linear(visitor, node, arg1, arg2): _, arg1 = arg1 _, arg2 = arg2 # Quadratic first, because we will update linear in a minute - arg1.quadratic = _mul_linear_linear( - visitor.var_order.__getitem__, arg1.linear, arg2.linear - ) + arg1.quadratic = _mul_linear_linear(arg1.linear, arg2.linear) # Linear second, as this relies on knowing the original constants if not arg2.constant: arg1.linear = {} @@ -235,7 +236,7 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): ans.quadratic = {k: c * coef for k, coef in x2.quadratic.items()} # [BB] if x1.linear and x2.linear: - quad = _mul_linear_linear(visitor.var_order.__getitem__, x1.linear, x2.linear) + quad = _mul_linear_linear(x1.linear, x2.linear) if ans.quadratic: _merge_dict(ans.quadratic, 1, quad) else: diff --git a/pyomo/repn/tests/test_linear.py b/pyomo/repn/tests/test_linear.py index d6b738b880f..04e1bdb6584 100644 --- a/pyomo/repn/tests/test_linear.py +++ b/pyomo/repn/tests/test_linear.py @@ -19,7 +19,7 @@ from pyomo.core.expr import Expr_if, inequality, LinearExpression, NPV_SumExpression import pyomo.repn.linear as linear from pyomo.repn.linear import LinearRepn, LinearRepnVisitor -from pyomo.repn.util import InvalidNumber +from pyomo.repn.util import InvalidNumber, OrderedVarRecorder from pyomo.environ import ( Any, @@ -35,15 +35,28 @@ nan = float('nan') -class VisitorConfig(object): +class VisitorConfig(dict): def __init__(self): self.subexpr = {} self.var_map = {} self.var_order = {} self.sorter = None + self.var_recorder = OrderedVarRecorder( + self.var_map, self.var_order, self.sorter + ) + super().__init__( + subexpression_cache=self.subexpr, var_recorder=self.var_recorder + ) - def __iter__(self): - return iter((self.subexpr, self.var_map, self.var_order, self.sorter)) + def order_quadratic(self, quad): + return { + ( + (vid1, vid2) + if self.var_order[vid1] <= self.var_order[vid2] + else (vid2, vid1) + ): val + for (vid1, vid2), val in quad.items() + } def sum_sq(args, fixed, fgh): @@ -63,7 +76,7 @@ def test_finalize(self): e = m.x + 2 * m.y - m.x - m.z cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y, id(m.z): m.z}) self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1, id(m.z): 2}) @@ -75,7 +88,7 @@ def test_finalize(self): e *= 5 cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y, id(m.z): m.z}) self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1, id(m.z): 2}) @@ -87,7 +100,7 @@ def test_finalize(self): e = 5 * (m.y + m.z**2 + 3 * m.y**3) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.y): m.y, id(m.z): m.z}) self.assertEqual(cfg.var_order, {id(m.y): 0, id(m.z): 1}) @@ -102,7 +115,7 @@ def test_scalars(self): m.p = Param(mutable=True, initialize=2) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(3) + repn = LinearRepnVisitor(**cfg).walk_expression(3) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -112,7 +125,7 @@ def test_scalars(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression((-1) ** 0.5) + repn = LinearRepnVisitor(**cfg).walk_expression((-1) ** 0.5) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -122,7 +135,7 @@ def test_scalars(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(m.p) + repn = LinearRepnVisitor(**cfg).walk_expression(m.p) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -133,7 +146,7 @@ def test_scalars(self): m.p.set_value(None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(m.p) + repn = LinearRepnVisitor(**cfg).walk_expression(m.p) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -144,7 +157,7 @@ def test_scalars(self): m.p.set_value(nan) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(m.p) + repn = LinearRepnVisitor(**cfg).walk_expression(m.p) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -155,7 +168,7 @@ def test_scalars(self): m.p.set_value(1j) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(m.p) + repn = LinearRepnVisitor(**cfg).walk_expression(m.p) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -165,7 +178,7 @@ def test_scalars(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(m.x) + repn = LinearRepnVisitor(**cfg).walk_expression(m.x) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -176,7 +189,7 @@ def test_scalars(self): m.x.fix(1) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(m.x) + repn = LinearRepnVisitor(**cfg).walk_expression(m.x) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -187,7 +200,7 @@ def test_scalars(self): m.x.fix(None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(m.x) + repn = LinearRepnVisitor(**cfg).walk_expression(m.x) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -198,7 +211,7 @@ def test_scalars(self): m.x.fix(nan) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(m.x) + repn = LinearRepnVisitor(**cfg).walk_expression(m.x) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -209,7 +222,7 @@ def test_scalars(self): m.x.fix(1j) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(m.x) + repn = LinearRepnVisitor(**cfg).walk_expression(m.x) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -226,7 +239,7 @@ def test_npv(self): pow_expr = m.p ** (0.5) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(nested_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(nested_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -236,7 +249,7 @@ def test_npv(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(pow_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(pow_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -248,7 +261,7 @@ def test_npv(self): m.p = 0 cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(nested_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(nested_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -258,7 +271,7 @@ def test_npv(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(pow_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(pow_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -270,7 +283,7 @@ def test_npv(self): m.p = -1 cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(nested_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(nested_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -280,7 +293,7 @@ def test_npv(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(pow_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(pow_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -292,7 +305,7 @@ def test_npv(self): m.p = None cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(nested_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(nested_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -302,7 +315,7 @@ def test_npv(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(pow_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(pow_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -322,7 +335,7 @@ def test_monomial(self): pow_expr = (m.p ** (0.5)) * m.x cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(const_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(const_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -332,7 +345,7 @@ def test_monomial(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(param_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(param_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -342,7 +355,7 @@ def test_monomial(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(nested_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(nested_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -352,7 +365,7 @@ def test_monomial(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(pow_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(pow_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -364,7 +377,7 @@ def test_monomial(self): m.p = -1.0 cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(param_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(param_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -374,7 +387,7 @@ def test_monomial(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(nested_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(nested_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -384,7 +397,7 @@ def test_monomial(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(pow_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(pow_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -396,7 +409,7 @@ def test_monomial(self): m.p = float('nan') cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(param_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(param_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -406,7 +419,7 @@ def test_monomial(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(nested_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(nested_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -416,7 +429,7 @@ def test_monomial(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(pow_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(pow_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -428,7 +441,7 @@ def test_monomial(self): m.p.set_value(None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(param_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(param_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -441,7 +454,7 @@ def test_monomial(self): m.x.fix(10) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(const_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(const_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -451,7 +464,7 @@ def test_monomial(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(param_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(param_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -461,7 +474,7 @@ def test_monomial(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(nested_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(nested_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -471,7 +484,7 @@ def test_monomial(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(pow_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(pow_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -483,7 +496,7 @@ def test_monomial(self): m.p = float('nan') cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(param_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(param_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -493,7 +506,7 @@ def test_monomial(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(nested_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(nested_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -503,7 +516,7 @@ def test_monomial(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(pow_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(pow_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -515,7 +528,7 @@ def test_monomial(self): m.p.set_value(None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(param_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(param_expr) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -529,7 +542,7 @@ def test_monomial(self): cfg = VisitorConfig() with LoggingIntercept() as LOG: - repn = LinearRepnVisitor(*cfg).walk_expression(param_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(param_expr) self.assertEqual(LOG.getvalue(), "") self.assertEqual(cfg.subexpr, {}) @@ -544,7 +557,7 @@ def test_monomial(self): cfg = VisitorConfig() with LoggingIntercept() as LOG: - repn = LinearRepnVisitor(*cfg).walk_expression(param_expr) + repn = LinearRepnVisitor(**cfg).walk_expression(param_expr) self.assertEqual(LOG.getvalue(), "") self.assertEqual(cfg.subexpr, {}) @@ -563,7 +576,7 @@ def test_linear(self): e = LinearExpression() cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -575,7 +588,7 @@ def test_linear(self): e += m.x[0] cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual( cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} @@ -589,7 +602,7 @@ def test_linear(self): e += 2 * m.x[0] cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual( cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} @@ -603,7 +616,7 @@ def test_linear(self): e += m.p * m.x[1] cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual( cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} @@ -617,7 +630,7 @@ def test_linear(self): e += (m.p**0.5) * m.x[1] cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual( cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} @@ -631,7 +644,7 @@ def test_linear(self): e += 10 cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual( cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} @@ -645,7 +658,7 @@ def test_linear(self): e += 10 * m.p cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual( cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} @@ -659,7 +672,7 @@ def test_linear(self): m.p = -1 cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual( cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} @@ -676,7 +689,7 @@ def test_linear(self): e += (1 / m.p) * m.x[1] cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual( cfg.var_map, {id(m.x[0]): m.x[0], id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]} @@ -692,10 +705,10 @@ def test_linear(self): m.x[0].fix(10) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x[1]): m.x[1], id(m.x[2]): m.x[2]}) - self.assertEqual(cfg.var_order, {id(m.x[1]): 0, id(m.x[2]): 1}) + self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1, id(m.x[2]): 2}) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 40) self.assertStructuredAlmostEqual(repn.linear, {id(m.x[1]): InvalidNumber(nan)}) @@ -704,7 +717,7 @@ def test_linear(self): m.x[1].fix(10) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -720,10 +733,10 @@ def test_linear(self): e += m.x[2] + (1 / m.p) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x[2]): m.x[2]}) - self.assertEqual(cfg.var_order, {id(m.x[2]): 0}) + self.assertEqual(cfg.var_order, {id(m.x[0]): 0, id(m.x[1]): 1, id(m.x[2]): 2}) self.assertEqual(repn.multiplier, 1) self.assertStructuredAlmostEqual(repn.constant, InvalidNumber(nan)) self.assertEqual(repn.linear, {id(m.x[2]): 1}) @@ -734,7 +747,7 @@ def test_linear(self): cfg.var_map[id(m.x[0])] = m.x[0] cfg.var_order[id(m.x[2])] = 0 cfg.var_order[id(m.x[0])] = 1 - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x[2]): m.x[2], id(m.x[0]): m.x[0]}) self.assertEqual(cfg.var_order, {id(m.x[2]): 0, id(m.x[0]): 1}) @@ -748,7 +761,7 @@ def test_linear(self): cfg = VisitorConfig() with LoggingIntercept() as LOG: - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(LOG.getvalue(), "") self.assertEqual(cfg.subexpr, {}) @@ -763,7 +776,7 @@ def test_linear(self): cfg = VisitorConfig() with LoggingIntercept() as LOG: - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertIn( "DEPRECATED: Encountered 0*nan in expression tree.", LOG.getvalue() ) @@ -783,7 +796,7 @@ def test_trig(self): e = cos(m.x) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -795,7 +808,7 @@ def test_trig(self): m.x.fix(0) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -812,7 +825,7 @@ def test_named_expr(self): e = m.e * 2 cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(len(cfg.subexpr), 1) self.assertEqual(cfg.subexpr[id(m.e)][1].multiplier, 1) self.assertEqual(cfg.subexpr[id(m.e)][1].constant, 0) @@ -834,7 +847,7 @@ def test_named_expr(self): e = m.e * 2 + 3 * m.e cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(len(cfg.subexpr), 1) self.assertEqual(cfg.subexpr[id(m.e)][1].multiplier, 1) self.assertEqual(cfg.subexpr[id(m.e)][1].constant, 0) @@ -859,7 +872,7 @@ def test_named_expr(self): e = m.e * 2 cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(len(cfg.subexpr), 1) self.assertEqual(cfg.subexpr[id(m.e)][1], 10) @@ -873,7 +886,7 @@ def test_named_expr(self): e = m.e * 2 + 3 * m.e cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(len(cfg.subexpr), 1) self.assertEqual(cfg.subexpr[id(m.e)][1], 10) @@ -887,7 +900,7 @@ def test_named_expr(self): m.e = None cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(m.e) + repn = LinearRepnVisitor(**cfg).walk_expression(m.e) self.assertEqual( cfg.subexpr, {id(m.e): (linear._CONSTANT, InvalidNumber(None))} ) @@ -899,7 +912,7 @@ def test_named_expr(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(2 * m.e) + repn = LinearRepnVisitor(**cfg).walk_expression(2 * m.e) self.assertEqual( cfg.subexpr, {id(m.e): (linear._CONSTANT, InvalidNumber(None))} ) @@ -918,7 +931,7 @@ def test_pow_expr(self): e = m.x**m.p cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -930,7 +943,7 @@ def test_pow_expr(self): m.p = 0 cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -942,7 +955,7 @@ def test_pow_expr(self): m.p = 2 cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -954,7 +967,7 @@ def test_pow_expr(self): m.x.fix(2) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -967,7 +980,7 @@ def test_pow_expr(self): m.x = -1 cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -980,7 +993,7 @@ def test_pow_expr(self): e = (1 + m.x) ** 2 cfg = VisitorConfig() - visitor = LinearRepnVisitor(*cfg) + visitor = LinearRepnVisitor(**cfg) visitor.max_exponential_expansion = 2 repn = visitor.walk_expression(e) @@ -993,7 +1006,7 @@ def test_pow_expr(self): assertExpressionsEqual(self, repn.nonlinear, (m.x + 1) * (m.x + 1)) cfg = VisitorConfig() - visitor = LinearRepnVisitor(*cfg) + visitor = LinearRepnVisitor(**cfg) visitor.max_exponential_expansion = 2 visitor.expand_nonlinear_products = True repn = visitor.walk_expression(e) @@ -1015,7 +1028,7 @@ def test_product(self): e = (2 + 3 * m.x + 4 * m.x**2) * (5 + 6 * m.x + 7 * m.x**2) cfg = VisitorConfig() - visitor = LinearRepnVisitor(*cfg) + visitor = LinearRepnVisitor(**cfg) visitor.expand_nonlinear_products = True repn = visitor.walk_expression(e) @@ -1040,7 +1053,7 @@ def test_product(self): e = m.x * m.y cfg = VisitorConfig() - visitor = LinearRepnVisitor(*cfg) + visitor = LinearRepnVisitor(**cfg) visitor.expand_nonlinear_products = True with LoggingIntercept() as LOG: repn = visitor.walk_expression(e) @@ -1059,7 +1072,7 @@ def test_product(self): e = m.x * (m.y + 2 + m.z) cfg = VisitorConfig() - visitor = LinearRepnVisitor(*cfg) + visitor = LinearRepnVisitor(**cfg) visitor.expand_nonlinear_products = True with LoggingIntercept() as LOG: repn = visitor.walk_expression(e) @@ -1085,7 +1098,7 @@ def test_expr_if(self): m.y.fix(2) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -1095,7 +1108,7 @@ def test_expr_if(self): assertExpressionsEqual(self, repn.nonlinear, m.x**2) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(f) + repn = LinearRepnVisitor(**cfg).walk_expression(f) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -1105,7 +1118,7 @@ def test_expr_if(self): assertExpressionsEqual(self, repn.nonlinear, m.x**2) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(g) + repn = LinearRepnVisitor(**cfg).walk_expression(g) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -1117,7 +1130,7 @@ def test_expr_if(self): m.y.fix(5) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -1127,7 +1140,7 @@ def test_expr_if(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(f) + repn = LinearRepnVisitor(**cfg).walk_expression(f) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -1137,7 +1150,7 @@ def test_expr_if(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(g) + repn = LinearRepnVisitor(**cfg).walk_expression(g) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -1150,7 +1163,7 @@ def test_expr_if(self): m.x.fix(3) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -1160,7 +1173,7 @@ def test_expr_if(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(f) + repn = LinearRepnVisitor(**cfg).walk_expression(f) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -1170,7 +1183,7 @@ def test_expr_if(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(g) + repn = LinearRepnVisitor(**cfg).walk_expression(g) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -1183,7 +1196,7 @@ def test_expr_if(self): m.x.fix(6) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -1193,7 +1206,7 @@ def test_expr_if(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(f) + repn = LinearRepnVisitor(**cfg).walk_expression(f) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -1203,7 +1216,7 @@ def test_expr_if(self): self.assertEqual(repn.nonlinear, None) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(g) + repn = LinearRepnVisitor(**cfg).walk_expression(g) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -1216,7 +1229,7 @@ def test_expr_if(self): m.x.unfix() cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -1230,7 +1243,7 @@ def test_expr_if(self): ) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(f) + repn = LinearRepnVisitor(**cfg).walk_expression(f) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -1244,7 +1257,7 @@ def test_expr_if(self): ) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(g) + repn = LinearRepnVisitor(**cfg).walk_expression(g) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -1260,7 +1273,7 @@ def test_expr_if(self): m.y.unfix() cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.y): m.y, id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.y): 0, id(m.x): 1}) @@ -1272,7 +1285,7 @@ def test_expr_if(self): ) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(f) + repn = LinearRepnVisitor(**cfg).walk_expression(f) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.y): m.y, id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.y): 0, id(m.x): 1}) @@ -1284,7 +1297,7 @@ def test_expr_if(self): ) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(g) + repn = LinearRepnVisitor(**cfg).walk_expression(g) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.y): m.y, id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.y): 0, id(m.x): 1}) @@ -1300,7 +1313,7 @@ def test_expr_if(self): h = Expr_if(1 / m.y >= 1, m.x, m.x**2) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(h) + repn = LinearRepnVisitor(**cfg).walk_expression(h) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) self.assertEqual(cfg.var_order, {id(m.y): 0, id(m.x): 1}) @@ -1313,7 +1326,7 @@ def test_expr_if(self): m.y.fix(0) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(h) + repn = LinearRepnVisitor(**cfg).walk_expression(h) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -1335,7 +1348,7 @@ def test_division(self): m.y.fix(2) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -1347,7 +1360,7 @@ def test_division(self): e = m.y / (m.x + 1) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -1364,7 +1377,7 @@ def test_negation(self): e = -(m.x + 2) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -1376,7 +1389,7 @@ def test_negation(self): m.x.fix(3) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -1394,7 +1407,7 @@ def test_external(self): e = m.sq(2 / m.x, 2 * m.y) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -1407,7 +1420,7 @@ def test_external(self): m.y.fix(3) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -1419,7 +1432,7 @@ def test_external(self): m.x.fix(0) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -1439,7 +1452,7 @@ def test_errors_propagate_nan(self): expr = (m.x + 1) / m.p cfg = VisitorConfig() with LoggingIntercept() as LOG: - repn = LinearRepnVisitor(*cfg).walk_expression(expr) + repn = LinearRepnVisitor(**cfg).walk_expression(expr) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'div(1, 0)'\n" @@ -1455,7 +1468,7 @@ def test_errors_propagate_nan(self): expr = m.y + m.x + m.z + ((3 * m.x) / m.p) / m.y cfg = VisitorConfig() with LoggingIntercept() as LOG: - repn = LinearRepnVisitor(*cfg).walk_expression(expr) + repn = LinearRepnVisitor(**cfg).walk_expression(expr) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'div(3, 0)'\n" @@ -1471,14 +1484,14 @@ def test_errors_propagate_nan(self): m.y.fix(None) expr = log(m.y) + 3 - repn = LinearRepnVisitor(*cfg).walk_expression(expr) + repn = LinearRepnVisitor(**cfg).walk_expression(expr) self.assertEqual(repn.multiplier, 1) self.assertEqual(str(repn.constant), 'InvalidNumber(nan)') self.assertEqual(repn.linear, {}) self.assertEqual(repn.nonlinear, None) expr = 3 * m.y - repn = LinearRepnVisitor(*cfg).walk_expression(expr) + repn = LinearRepnVisitor(**cfg).walk_expression(expr) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, InvalidNumber(None)) self.assertEqual(repn.linear, {}) @@ -1486,7 +1499,7 @@ def test_errors_propagate_nan(self): m.p.value = None expr = 5 * (m.p * m.x + 2 * m.z) - repn = LinearRepnVisitor(*cfg).walk_expression(expr) + repn = LinearRepnVisitor(**cfg).walk_expression(expr) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) self.assertEqual(len(repn.linear), 2) @@ -1495,7 +1508,7 @@ def test_errors_propagate_nan(self): self.assertEqual(repn.nonlinear, None) expr = m.y * m.x - repn = LinearRepnVisitor(*cfg).walk_expression(expr) + repn = LinearRepnVisitor(**cfg).walk_expression(expr) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) self.assertEqual(len(repn.linear), 1) @@ -1505,14 +1518,14 @@ def test_errors_propagate_nan(self): m.z = Var([1, 2, 3, 4], initialize=lambda m, i: i - 1) m.z[1].fix(None) expr = m.z[1] - ((m.z[2] * m.z[3]) * m.z[4]) - repn = LinearRepnVisitor(*cfg).walk_expression(expr) + repn = LinearRepnVisitor(**cfg).walk_expression(expr) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, InvalidNumber(None)) self.assertEqual(repn.linear, {}) self.assertIsNotNone(repn.nonlinear) m.z[3].fix(float('nan')) - repn = LinearRepnVisitor(*cfg).walk_expression(expr) + repn = LinearRepnVisitor(**cfg).walk_expression(expr) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, InvalidNumber(None)) self.assertEqual(repn.linear, {}) @@ -1522,7 +1535,7 @@ def test_type_registrations(self): m = ConcreteModel() cfg = VisitorConfig() - visitor = LinearRepnVisitor(*cfg) + visitor = LinearRepnVisitor(**cfg) _orig_dispatcher = visitor.before_child_dispatcher linear._before_child_dispatcher = bcd = _orig_dispatcher.__class__() @@ -1577,7 +1590,7 @@ def test_to_expression(self): m.y = Var() cfg = VisitorConfig() - visitor = LinearRepnVisitor(*cfg) + visitor = LinearRepnVisitor(**cfg) # prepopulate the visitor's var_map visitor.walk_expression(m.x + m.y) @@ -1614,7 +1627,7 @@ def test_nonnumeric(self): m.e = Expression() cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(m.p) + repn = LinearRepnVisitor(**cfg).walk_expression(m.p) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -1626,7 +1639,7 @@ def test_nonnumeric(self): m.p = numpy.array([3, 4]) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(m.p) + repn = LinearRepnVisitor(**cfg).walk_expression(m.p) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -1642,7 +1655,7 @@ def test_zero_elimination(self): e = 0 * m.x[0] + 0 * m.x[1] * m.x[2] + 0 * log(m.x[3]) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual( cfg.var_map, @@ -1665,7 +1678,7 @@ def test_zero_elimination(self): e = m.p * m.x[0] + m.p * m.x[1] * m.x[2] + m.p * log(m.x[3]) cfg = VisitorConfig() - repn = LinearRepnVisitor(*cfg).walk_expression(e) + repn = LinearRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual( cfg.var_map, diff --git a/pyomo/repn/tests/test_parameterized_linear.py b/pyomo/repn/tests/test_parameterized_linear.py index 624f8390d16..d2bde4845ff 100644 --- a/pyomo/repn/tests/test_parameterized_linear.py +++ b/pyomo/repn/tests/test_parameterized_linear.py @@ -31,7 +31,7 @@ def test_walk_sum(self): m = self.make_model() e = m.x + m.y cfg = VisitorConfig() - visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) + visitor = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) @@ -48,7 +48,7 @@ def test_walk_triple_sum(self): e = m.x + m.z * m.y + m.z cfg = VisitorConfig() - visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.z]) + visitor = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.z]) repn = visitor.walk_expression(e) @@ -67,7 +67,7 @@ def test_sum_two_of_the_same(self): m = self.make_model() e = m.x + m.x cfg = VisitorConfig() - visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.z]) + visitor = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.z]) repn = visitor.walk_expression(e) @@ -84,7 +84,7 @@ def test_sum_with_mult_0(self): e = 0 * m.x + m.x - m.y cfg = VisitorConfig() - visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) + visitor = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) self.assertIsNone(repn.nonlinear) @@ -100,7 +100,7 @@ def test_sum_nonlinear_to_linear(self): e = m.y * m.x**2 + m.y * m.x - 3 cfg = VisitorConfig() - visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) + visitor = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) assertExpressionsEqual(self, repn.nonlinear, m.y * m.x**2) @@ -118,7 +118,7 @@ def test_sum_nonlinear_to_nonlinear(self): e = m.x**3 + 3 + m.x**2 cfg = VisitorConfig() - visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) + visitor = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) assertExpressionsEqual(self, repn.nonlinear, m.x**3 + m.x**2) @@ -131,7 +131,7 @@ def test_sum_to_linear_expr(self): e = m.x + m.y * (m.x + 5) cfg = VisitorConfig() - visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) + visitor = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) self.assertEqual(len(repn.linear), 1) @@ -147,7 +147,7 @@ def test_bilinear_term(self): m = self.make_model() e = m.x * m.y cfg = VisitorConfig() - visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) + visitor = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) @@ -163,7 +163,7 @@ def test_distributed_bilinear_term(self): m = self.make_model() e = m.y * (m.x + 7) cfg = VisitorConfig() - visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) + visitor = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) @@ -179,7 +179,7 @@ def test_monomial(self): m = self.make_model() e = 45 * m.y cfg = VisitorConfig() - visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.x, m.z]) + visitor = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.x, m.z]) repn = visitor.walk_expression(e) @@ -195,7 +195,7 @@ def test_constant(self): m = self.make_model() e = 45 * m.y cfg = VisitorConfig() - visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) + visitor = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) @@ -211,7 +211,7 @@ def test_fixed_var(self): e = (m.y**2) * (m.x + m.x**2) cfg = VisitorConfig() - visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) + visitor = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) @@ -226,7 +226,7 @@ def test_nonlinear(self): e = (m.y * log(m.x)) * (m.y + 2) / m.x cfg = VisitorConfig() - visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]) + visitor = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) @@ -244,7 +244,7 @@ def test_finalize(self): e = m.x + 2 * m.w**2 * m.y - m.x - m.w * m.z cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.w]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.w]).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y, id(m.z): m.z}) self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1, id(m.z): 2}) @@ -260,7 +260,7 @@ def test_finalize(self): e *= 5 cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.w]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.w]).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y, id(m.z): m.z}) self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1, id(m.z): 2}) @@ -276,7 +276,7 @@ def test_finalize(self): e = 5 * (m.w * m.y + m.z**2 + 3 * m.w * m.y**3) cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.w]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.w]).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.y): m.y, id(m.z): m.z}) self.assertEqual(cfg.var_order, {id(m.y): 0, id(m.z): 1}) @@ -299,7 +299,7 @@ def test_ANY_over_constant_division(self): expr = m.y + m.x + m.z + ((3 * m.z * m.x) / m.p) / m.y cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]).walk_expression( + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y, m.z]).walk_expression( expr ) @@ -320,9 +320,9 @@ def test_errors_propagate_nan(self): expr = m.y + m.x + m.z + ((3 * m.z * m.x) / m.p) / m.y cfg = VisitorConfig() with LoggingIntercept() as LOG: - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]).walk_expression( - expr - ) + repn = ParameterizedLinearRepnVisitor( + **cfg, wrt=[m.y, m.z] + ).walk_expression(expr) self.assertEqual( LOG.getvalue(), "Exception encountered evaluating expression 'div(3*z, 0)'\n" @@ -338,7 +338,7 @@ def test_errors_propagate_nan(self): m.y.fix(None) expr = m.z * log(m.y) + 3 - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]).walk_expression( + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y, m.z]).walk_expression( expr ) self.assertEqual(repn.multiplier, 1) @@ -352,7 +352,7 @@ def test_negation_constant(self): e = -(m.y * m.z + 17) cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y, m.z]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y, m.z]).walk_expression(e) self.assertEqual(len(repn.linear), 0) self.assertEqual(repn.multiplier, 1) @@ -363,7 +363,7 @@ def test_product_nonlinear(self): m = self.make_model() e = (m.x**2) * (log(m.y) * m.z**4) * m.y cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y]).walk_expression(e) self.assertEqual(len(repn.linear), 0) self.assertEqual(repn.multiplier, 1) @@ -377,7 +377,7 @@ def test_division_pseudo_constant_constant(self): e = m.x / 4 + m.y cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.x]).walk_expression(e) self.assertEqual(len(repn.linear), 1) self.assertIn(id(m.y), repn.linear) @@ -388,7 +388,7 @@ def test_division_pseudo_constant_constant(self): e = 4 / m.x + m.y cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.x]).walk_expression(e) self.assertEqual(len(repn.linear), 1) self.assertIn(id(m.y), repn.linear) @@ -399,7 +399,7 @@ def test_division_pseudo_constant_constant(self): e = m.z / m.x + m.y cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.x, m.z]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.x, m.z]).walk_expression(e) self.assertEqual(len(repn.linear), 1) self.assertIn(id(m.y), repn.linear) @@ -413,7 +413,7 @@ def test_division_ANY_pseudo_constant(self): e = (m.x + 3 * m.z) / m.y cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y]).walk_expression(e) self.assertEqual(len(repn.linear), 2) self.assertIn(id(m.x), repn.linear) @@ -429,7 +429,7 @@ def test_duplicate(self): e = (1 + m.x) ** 2 + m.y cfg = VisitorConfig() - visitor = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]) + visitor = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y]) visitor.max_exponential_expansion = 2 repn = visitor.walk_expression(e) @@ -443,7 +443,7 @@ def test_pow_ANY_pseudo_constant(self): e = (m.x**2 + 3 * m.z) ** m.y cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y]).walk_expression(e) self.assertEqual(len(repn.linear), 0) self.assertEqual(repn.multiplier, 1) @@ -455,7 +455,7 @@ def test_pow_pseudo_constant_ANY(self): e = m.y ** (m.x**2 + 3 * m.z) cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y]).walk_expression(e) self.assertEqual(len(repn.linear), 0) self.assertEqual(repn.multiplier, 1) @@ -467,7 +467,7 @@ def test_pow_linear_pseudo_constant(self): e = (m.x + 3 * m.z) ** m.y cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y]).walk_expression(e) self.assertEqual(len(repn.linear), 0) self.assertEqual(repn.multiplier, 1) @@ -479,7 +479,7 @@ def test_pow_pseudo_constant_linear(self): e = m.y ** (m.x + 3 * m.z) cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y]).walk_expression(e) self.assertEqual(len(repn.linear), 0) self.assertEqual(repn.multiplier, 1) @@ -493,7 +493,7 @@ def test_0_mult(self): e = m.p * (m.y**2 + m.z) cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.z]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.z]).walk_expression(e) self.assertEqual(len(repn.linear), 0) self.assertEqual(repn.multiplier, 1) @@ -508,7 +508,7 @@ def test_0_mult_nan(self): e = m.p * (m.y**2 + m.x) cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.x]).walk_expression(e) self.assertEqual(len(repn.linear), 0) self.assertEqual(repn.multiplier, 1) @@ -523,7 +523,7 @@ def test_0_mult_nan_param(self): e = m.p * (m.y**2) cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.y]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.y]).walk_expression(e) self.assertEqual(len(repn.linear), 0) self.assertEqual(repn.multiplier, 1) @@ -539,7 +539,7 @@ def test_0_mult_linear_with_nan(self): e = m.p * (3 * m.x * m.y + m.z) cfg = VisitorConfig() - repn = ParameterizedLinearRepnVisitor(*cfg, wrt=[m.x]).walk_expression(e) + repn = ParameterizedLinearRepnVisitor(**cfg, wrt=[m.x]).walk_expression(e) self.assertEqual(len(repn.linear), 2) self.assertIn(id(m.y), repn.linear) diff --git a/pyomo/repn/tests/test_quadratic.py b/pyomo/repn/tests/test_quadratic.py index 2d2e4022037..137954dc1d0 100644 --- a/pyomo/repn/tests/test_quadratic.py +++ b/pyomo/repn/tests/test_quadratic.py @@ -19,22 +19,12 @@ SumExpression, ) from pyomo.repn.quadratic import QuadraticRepnVisitor +from pyomo.repn.tests.test_linear import VisitorConfig from pyomo.repn.util import InvalidNumber from pyomo.environ import ConcreteModel, Var, Param, Any, log -class VisitorConfig(object): - def __init__(self): - self.subexpr = {} - self.var_map = {} - self.var_order = {} - self.sorter = None - - def __iter__(self): - return iter((self.subexpr, self.var_map, self.var_order, self.sorter)) - - class TestQuadratic(unittest.TestCase): def test_product(self): m = ConcreteModel() @@ -44,7 +34,7 @@ def test_product(self): e = 2 cfg = VisitorConfig() - visitor = QuadraticRepnVisitor(*cfg) + visitor = QuadraticRepnVisitor(**cfg) visitor.expand_nonlinear_products = True repn = visitor.walk_expression(e) @@ -60,7 +50,7 @@ def test_product(self): e = 2 + 3 * m.x cfg = VisitorConfig() - visitor = QuadraticRepnVisitor(*cfg) + visitor = QuadraticRepnVisitor(**cfg) visitor.expand_nonlinear_products = True repn = visitor.walk_expression(e) @@ -76,7 +66,7 @@ def test_product(self): e = 2 + 3 * m.x + 4 * m.x**2 cfg = VisitorConfig() - visitor = QuadraticRepnVisitor(*cfg) + visitor = QuadraticRepnVisitor(**cfg) visitor.expand_nonlinear_products = True repn = visitor.walk_expression(e) @@ -92,7 +82,7 @@ def test_product(self): e = (2 + 3 * m.x + 4 * m.x**2) * (5 + 6 * m.x + 7 * m.x**2) cfg = VisitorConfig() - visitor = QuadraticRepnVisitor(*cfg) + visitor = QuadraticRepnVisitor(**cfg) visitor.expand_nonlinear_products = True repn = visitor.walk_expression(e) @@ -114,7 +104,7 @@ def test_product(self): e = (2 + 3 * m.x + 4 * m.x**2) * (5 + 6 * m.x + 7 * m.x**2) cfg = VisitorConfig() - visitor = QuadraticRepnVisitor(*cfg) + visitor = QuadraticRepnVisitor(**cfg) visitor.expand_nonlinear_products = False repn = visitor.walk_expression(e) @@ -132,7 +122,7 @@ def test_product(self): e = (1 + 2 * m.x + 3 * m.y) * (4 + 5 * m.x + 6 * m.y) cfg = VisitorConfig() - repn = QuadraticRepnVisitor(*cfg).walk_expression(e) + repn = QuadraticRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) @@ -141,7 +131,7 @@ def test_product(self): self.assertEqual(repn.constant, 4) self.assertEqual(repn.linear, {id(m.x): 13, id(m.y): 18}) self.assertEqual( - repn.quadratic, + cfg.order_quadratic(repn.quadratic), {(id(m.x), id(m.x)): 10, (id(m.y), id(m.y)): 18, (id(m.x), id(m.y)): 27}, ) assertExpressionsEqual(self, repn.nonlinear, None) @@ -149,7 +139,7 @@ def test_product(self): e = (m.x + m.y + log(m.x)) * m.x cfg = VisitorConfig() - visitor = QuadraticRepnVisitor(*cfg) + visitor = QuadraticRepnVisitor(**cfg) visitor.expand_nonlinear_products = False repn = visitor.walk_expression(e) @@ -175,13 +165,16 @@ def test_product(self): self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) self.assertEqual(repn.linear, {}) - self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 1, (id(m.x), id(m.y)): 1}) + self.assertEqual( + cfg.order_quadratic(repn.quadratic), + {(id(m.x), id(m.x)): 1, (id(m.x), id(m.y)): 1}, + ) assertExpressionsEqual(self, repn.nonlinear, NL) e = m.x * (m.x + m.y + log(m.x) + 2) cfg = VisitorConfig() - visitor = QuadraticRepnVisitor(*cfg) + visitor = QuadraticRepnVisitor(**cfg) visitor.expand_nonlinear_products = False repn = visitor.walk_expression(e) @@ -207,7 +200,10 @@ def test_product(self): self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 0) self.assertEqual(repn.linear, {id(m.x): 2}) - self.assertEqual(repn.quadratic, {(id(m.x), id(m.x)): 1, (id(m.x), id(m.y)): 1}) + self.assertEqual( + cfg.order_quadratic(repn.quadratic), + {(id(m.x), id(m.x)): 1, (id(m.x), id(m.y)): 1}, + ) assertExpressionsEqual(self, repn.nonlinear, NL) def test_sum(self): @@ -218,7 +214,7 @@ def test_sum(self): e = SumExpression([]) cfg = VisitorConfig() - repn = QuadraticRepnVisitor(*cfg).walk_expression(e) + repn = QuadraticRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -231,7 +227,7 @@ def test_sum(self): e += 5 cfg = VisitorConfig() - repn = QuadraticRepnVisitor(*cfg).walk_expression(e) + repn = QuadraticRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {}) self.assertEqual(cfg.var_order, {}) @@ -244,7 +240,7 @@ def test_sum(self): e += m.x cfg = VisitorConfig() - repn = QuadraticRepnVisitor(*cfg).walk_expression(e) + repn = QuadraticRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x}) self.assertEqual(cfg.var_order, {id(m.x): 0}) @@ -257,7 +253,7 @@ def test_sum(self): e += m.y**2 cfg = VisitorConfig() - repn = QuadraticRepnVisitor(*cfg).walk_expression(e) + repn = QuadraticRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) @@ -270,7 +266,7 @@ def test_sum(self): e += m.y**3 cfg = VisitorConfig() - repn = QuadraticRepnVisitor(*cfg).walk_expression(e) + repn = QuadraticRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) @@ -283,7 +279,7 @@ def test_sum(self): e += 2 * m.x**4 cfg = VisitorConfig() - repn = QuadraticRepnVisitor(*cfg).walk_expression(e) + repn = QuadraticRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) @@ -296,7 +292,7 @@ def test_sum(self): e += 2 * m.y cfg = VisitorConfig() - repn = QuadraticRepnVisitor(*cfg).walk_expression(e) + repn = QuadraticRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) @@ -309,14 +305,17 @@ def test_sum(self): e += 3 * m.x * m.y cfg = VisitorConfig() - repn = QuadraticRepnVisitor(*cfg).walk_expression(e) + repn = QuadraticRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) self.assertEqual(repn.multiplier, 1) self.assertEqual(repn.constant, 5) self.assertEqual(repn.linear, {id(m.x): 1, id(m.y): 2}) - self.assertEqual(repn.quadratic, {(id(m.y), id(m.y)): 1, (id(m.x), id(m.y)): 3}) + self.assertEqual( + cfg.order_quadratic(repn.quadratic), + {(id(m.y), id(m.y)): 1, (id(m.x), id(m.y)): 3}, + ) assertExpressionsEqual(self, repn.nonlinear, m.y**3 + 2 * m.x**4) def test_pow(self): @@ -326,7 +325,7 @@ def test_pow(self): # Check **{int} cfg = VisitorConfig() - repn = QuadraticRepnVisitor(*cfg).walk_expression((1 + 3 * m.x + 4 * m.y) ** 2) + repn = QuadraticRepnVisitor(**cfg).walk_expression((1 + 3 * m.x + 4 * m.y) ** 2) self.assertEqual(cfg.subexpr, {}) self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) @@ -334,14 +333,14 @@ def test_pow(self): self.assertEqual(repn.constant, 1) self.assertEqual(repn.linear, {id(m.x): 6, id(m.y): 8}) self.assertEqual( - repn.quadratic, + cfg.order_quadratic(repn.quadratic), {(id(m.x), id(m.x)): 9, (id(m.y), id(m.y)): 16, (id(m.x), id(m.y)): 24}, ) self.assertEqual(repn.nonlinear, None) # Check **{int} cfg = VisitorConfig() - repn = QuadraticRepnVisitor(*cfg).walk_expression( + repn = QuadraticRepnVisitor(**cfg).walk_expression( (1 + 3 * m.x + 4 * m.y) ** 2.0 ) self.assertEqual(cfg.subexpr, {}) @@ -351,7 +350,7 @@ def test_pow(self): self.assertEqual(repn.constant, 1) self.assertEqual(repn.linear, {id(m.x): 6, id(m.y): 8}) self.assertEqual( - repn.quadratic, + cfg.order_quadratic(repn.quadratic), {(id(m.x), id(m.x)): 9, (id(m.y), id(m.y)): 16, (id(m.x), id(m.y)): 24}, ) self.assertEqual(repn.nonlinear, None) @@ -363,7 +362,7 @@ def test_zero_elimination(self): e = 0 * m.x[0] + 0 * m.x[1] * m.x[2] + 0 * log(m.x[3]) cfg = VisitorConfig() - repn = QuadraticRepnVisitor(*cfg).walk_expression(e) + repn = QuadraticRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual( cfg.var_map, @@ -387,7 +386,7 @@ def test_zero_elimination(self): e = m.p * m.x[0] + m.p * m.x[1] * m.x[2] + m.p * log(m.x[3]) cfg = VisitorConfig() - repn = QuadraticRepnVisitor(*cfg).walk_expression(e) + repn = QuadraticRepnVisitor(**cfg).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual( cfg.var_map, @@ -405,6 +404,7 @@ def test_zero_elimination(self): self.assertEqual(repn.constant, 0) self.assertEqual(repn.linear, {id(m.x[0]): InvalidNumber(None)}) self.assertEqual( - repn.quadratic, {(id(m.x[1]), id(m.x[2])): InvalidNumber(None)} + cfg.order_quadratic(repn.quadratic), + {(id(m.x[1]), id(m.x[2])): InvalidNumber(None)}, ) self.assertEqual(repn.nonlinear, InvalidNumber(None)) diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index fcbc0d37d33..fd51eaacea3 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -36,6 +36,7 @@ Block, Constraint, Expression, + NumericLabeler, Suffix, SortComponents, ) @@ -732,6 +733,107 @@ def ordered_active_constraints(model, config): return sorted(constraints, key=lambda x: _row_getter(id(x), _n)) +class VarRecorder(object): + def __init__(self, var_map, sorter): + self.var_map = var_map + self.sorter = sorter + + def add(self, var): + # We always add all indices to the var_map at once so that + # we can honor deterministic ordering of unordered sets + # (because the user could have iterated over an unordered + # set when constructing an expression, thereby altering the + # order in which we would see the variables) + vm = self.var_map + try: + _iter = var.parent_component().values(self.sorter) + except AttributeError: + # Note that this only works for the AML, as kernel does not + # provide a parent_component() + _iter = (var,) + for v in _iter: + if not v.fixed: + vm[id(v)] = v + + +class OrderedVarRecorder(object): + def __init__(self, var_map, var_order, sorter): + self.var_map = var_map + self.var_order = var_order + self.sorter = sorter + + def add(self, var): + # We always add all indices to the var_map at once so that + # we can honor deterministic ordering of unordered sets + # (because the user could have iterated over an unordered + # set when constructing an expression, thereby altering the + # order in which we would see the variables) + vm = self.var_map + vo = self.var_order + try: + _iter = var.parent_component().values(self.sorter) + except AttributeError: + # Note that this only works for the AML, as kernel does not + # provide a parent_component() + _iter = (var,) + for i, v in enumerate(_iter, start=len(vo)): + vid = id(v) + vo[vid] = i + if not v.fixed: + vm[vid] = v + + +class TemplateVarRecorder(object): + def __init__(self, var_map, var_order, sorter): + self.var_map = var_map + self._var_order = var_order + self.sorter = sorter + self.env = {None: 0} + self.symbolmap = EXPR.SymbolMap(NumericLabeler('x')) + + @property + def var_order(self): + if self._var_order is None: + self._var_order = {vid: i for i, vid in enumerate(self.var_map)} + return self._var_order + + def add(self, var): + # Note: the following is mostly a copy of + # LinearBeforeChildDispatcher.record_var, but with extra + # hanlding to update the env in the same loop + var_comp = var.parent_component() + # Double-check that the component has not already been processed + # (through an individual var data) + name = self.symbolmap.getSymbol(var_comp) + if name in self.env: + return + + # We always add all indices to the var_map at once so that + # we can honor deterministic ordering of unordered sets + # (because the user could have iterated over an unordered + # set when constructing an expression, thereby altering the + # order in which we would see the variables) + vm = self.var_map + ve = self.env[name] = {} + vo = self._var_order + try: + _iter = var_comp.items(self.sorter) + except AttributeError: + # Note that this only works for the AML, as kernel does not + # provide a parent_component() + _iter = (var,) + if vo is None: + for i, (idx, v) in enumerate(_iter, start=len(vm)): + vm[id(v)] = v + ve[idx] = i + else: + for i, (idx, v) in enumerate(_iter, start=len(vm)): + vid = id(v) + vm[vid] = v + ve[idx] = i + vo[vid] = i + + # Copied from cpxlp.py: # Keven Hunter made a nice point about using %.16g in his attachment # to ticket #4319. I am adjusting this to %.17g as this mocks the From 9a0d0542efc6b4b615f704a0ebbbfdfe23e8f608 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 2 Aug 2024 06:36:49 -0600 Subject: [PATCH 2067/3044] fix incorrect reference to VarRecorder.add, remove old code --- pyomo/repn/linear_template.py | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/pyomo/repn/linear_template.py b/pyomo/repn/linear_template.py index 7521274dcee..334f8db64a3 100644 --- a/pyomo/repn/linear_template.py +++ b/pyomo/repn/linear_template.py @@ -207,35 +207,10 @@ def compile( class LinearTemplateBeforeChildDispatcher(linear.LinearBeforeChildDispatcher): - def record_var(self, visitor, var): - # Note: the following is mostly a copy of - # LinearBeforeChildDispatcher.record_var, but with extra - # hanlding to update the env in the same loop - var_comp = var.parent_component() - # Double-check that the component has not already been processed - # (through an individual var data) - name = visitor.symbolmap.getSymbol(var_comp) - if name in visitor.env: - return - ve = visitor.env[name] = {} - - # We always add all indices to the var_map at once so that - # we can honor deterministic ordering of unordered sets - # (because the user could have iterated over an unordered - # set when constructing an expression, thereby altering the - # order in which we would see the variables) - vm = visitor.var_map - _iter = var_comp.items(visitor.sorter) - for i, (idx, v) in enumerate(_iter, start=len(ve)): - # if v.fixed: - # ve[idx] = (v.value,) - # continue - vm[id(v)] = v - ve[idx] = 0 if v.fixed else i def _before_indexed_var(self, visitor, child): if child not in visitor.indexed_vars: - visitor.var_recorder(child) + visitor.var_recorder.add(child) visitor.indexed_vars.add(child) return False, (_VARIABLE, child) From 83c326a64009dfc76763a789017f8ba21ee381cb Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 2 Aug 2024 09:18:32 -0400 Subject: [PATCH 2068/3044] Fix private method docstring --- pyomo/repn/parameterized_quadratic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index dcd6a9e5364..7400985db51 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -46,8 +46,8 @@ def _merge_dict(dest_dict, mult, src_dict): """ - Slightly different from `merge_dict` of - from the parameterized module. + Slightly different from `merge_dict` + in the `parameterized_linear` module. """ if not is_equal_to(mult, 1): for vid, coef in src_dict.items(): From 228ec3e2bc13ec04d0909c7dd168bb5bc1ece55a Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 2 Aug 2024 09:21:22 -0400 Subject: [PATCH 2069/3044] Simplify implementation of `beforeChild` --- pyomo/repn/parameterized_quadratic.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 7400985db51..1dd93955825 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -28,7 +28,6 @@ ) from pyomo.repn.parameterized_linear import ( define_exit_node_handlers as _param_linear_def_exit_node_handlers, - ParameterizedLinearBeforeChildDispatcher, ParameterizedLinearRepnVisitor, to_expression, _handle_division_ANY_pseudo_constant, @@ -297,9 +296,6 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): return _GENERAL, ans -_before_child_dispatcher = ParameterizedLinearBeforeChildDispatcher() - - def define_exit_node_handlers(exit_node_handlers=None): if exit_node_handlers is None: exit_node_handlers = {} @@ -348,9 +344,6 @@ class ParameterizedQuadraticRepnVisitor(ParameterizedLinearRepnVisitor): max_exponential_expansion = 2 expand_nonlinear_products = True - def beforeChild(self, node, child, child_idx): - return _before_child_dispatcher[child.__class__](self, child) - def _factor_multiplier_into_quadratic_terms(self, ans, mult): linear = ans.linear zeros = [] From 5f3d9fc7faa4d88e3e6baf76518e733051d96d32 Mon Sep 17 00:00:00 2001 From: whart222 Date: Fri, 2 Aug 2024 08:01:10 -0600 Subject: [PATCH 2070/3044] Typo fix --- doc/OnlineDocs/contributed_packages/alternative_solutions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst index 793673882d4..cfabf66a6b9 100644 --- a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst +++ b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst @@ -66,7 +66,7 @@ We can execute the ``enumerate_binary_solutions`` function to generate a list of >>> solns = aos.enumerate_binary_solutions(m, num_solutions=100, solver="gurobi") >>> assert len(solns) == 1 -Each ``Solution`` object constains information about the objective and variables, and it includes various methods to access this information. For example: +Each ``Solution`` object contains information about the objective and variables, and it includes various methods to access this information. For example: .. doctest:: :skipif: not gurobi_available From 735c9326e4fb51dcb21667a4881cfaeb7da98296 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 2 Aug 2024 10:59:14 -0400 Subject: [PATCH 2071/3044] Adding functionality to use Params instead of Vars --- pyomo/contrib/doe/__init__.py | 2 +- pyomo/contrib/doe/doe.py | 16 ++++++++- .../doe/examples/reactor_experiment.py | 26 +++++++++----- pyomo/contrib/doe/utils.py | 36 +++++++++++++++++++ 4 files changed, 70 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index fe71b7f5920..aaca4db12fb 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -16,6 +16,6 @@ FiniteDifferenceStep, ) from .tests import experiment_class_example, experiment_class_example_flags -from .utils import rescale_FIM +from .utils import rescale_FIM, get_parameters_from_suffix from .examples import reactor_experiment, reactor_example, reactor_compute_factorial_FIM from .experiment import Experiment diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 359b0c051cc..550c5ab447e 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -30,6 +30,10 @@ from pyomo.common.timing import TicTocTimer from pyomo.contrib.sensitivity_toolbox.sens import get_dsdp +from pyomo.contrib.parmest.utils.model_utils import convert_params_to_vars + +from pyomo.contrib.doe.utils import get_parameters_from_suffix + from pyomo.common.dependencies import ( numpy as np, numpy_available, @@ -1012,7 +1016,17 @@ def _generate_scenario_blocks(self, model=None): "Finite difference option not recognized. Please contact the developers as you should not see this error." ) - # To-Do: Fix parameter values if they are not Params? + # Search for unknown parameters that are Params, or Vars. Params are updated to be fixed Vars + unknown_parameter_Params = get_parameters_from_suffix(model.base_model.unknown_parameters, fix_vars=True) + print(unknown_parameter_Params) + + # Change the unknown parameters that are Params to be Vars and fix them + if len(unknown_parameter_Params) > 0: + print("GOT HERE!!!!") + model.base_model = convert_params_to_vars(model.base_model, unknown_parameter_Params, fix_vars=True) + + # Search for experiment inputs that are Params, or Vars. Params are updated to be unfixed vars + experiment_inputs_Params = get_parameters_from_suffix(model.base_model.experiment_inputs, fix_vars=False) # Run base model to get initialized model and check model function for comp, _ in model.base_model.experiment_inputs.items(): diff --git a/pyomo/contrib/doe/examples/reactor_experiment.py b/pyomo/contrib/doe/examples/reactor_experiment.py index 68c33a4ca2b..9b4ddae2bf5 100644 --- a/pyomo/contrib/doe/examples/reactor_experiment.py +++ b/pyomo/contrib/doe/examples/reactor_experiment.py @@ -59,10 +59,15 @@ def create_model(self): m.T = pyo.Var(m.t, within=pyo.NonNegativeReals) # Arrhenius rate law equations - m.A1 = pyo.Var(within=pyo.NonNegativeReals) - m.E1 = pyo.Var(within=pyo.NonNegativeReals) - m.A2 = pyo.Var(within=pyo.NonNegativeReals) - m.E2 = pyo.Var(within=pyo.NonNegativeReals) + # m.A1 = pyo.Var(within=pyo.NonNegativeReals) + # m.E1 = pyo.Var(within=pyo.NonNegativeReals) + # m.A2 = pyo.Var(within=pyo.NonNegativeReals) + # m.E2 = pyo.Var(within=pyo.NonNegativeReals) + + m.A1 = pyo.Param(mutable=True) + m.E1 = pyo.Param(mutable=True) + m.A2 = pyo.Param(mutable=True) + m.E2 = pyo.Param(mutable=True) # Differential variables (Conc.) m.dCAdt = DerivativeVar(m.CA, wrt=m.t) @@ -130,10 +135,15 @@ def finalize_model(self): m.t.update(control_points) # Fix the unknown parameter values - m.A1.fix(self.data["A1"]) - m.A2.fix(self.data["A2"]) - m.E1.fix(self.data["E1"]) - m.E2.fix(self.data["E2"]) + # m.A1.fix(self.data["A1"]) + # m.A2.fix(self.data["A2"]) + # m.E1.fix(self.data["E1"]) + # m.E2.fix(self.data["E2"]) + + m.A1.set_value(self.data["A1"]) + m.A2.set_value(self.data["A2"]) + m.E1.set_value(self.data["E1"]) + m.E2.set_value(self.data["E2"]) # Add upper and lower bounds to the design variable, CA[0] m.CA[0].setlb(self.data["CA_bounds"][0]) diff --git a/pyomo/contrib/doe/utils.py b/pyomo/contrib/doe/utils.py index cb00dfcd67f..c7ec6f9d879 100644 --- a/pyomo/contrib/doe/utils.py +++ b/pyomo/contrib/doe/utils.py @@ -25,8 +25,13 @@ # publicly, and to permit other to do so. # ___________________________________________________________________________ +import pyomo.environ as pyo + from pyomo.common.dependencies import numpy as np, numpy_available +from pyomo.core.base.param import ParamData +from pyomo.core.base.var import VarData + # Rescale FIM (a scaling function to help rescale FIM from parameter values) def rescale_FIM(FIM, param_vals): @@ -58,3 +63,34 @@ def rescale_FIM(FIM, param_vals): scaling_mat = (1 / param_vals).transpose().dot((1 / param_vals)) scaled_FIM = np.multiply(FIM, scaling_mat) return scaled_FIM + +def get_parameters_from_suffix(suffix, fix_vars=False): + """ + Finds the Params within the suffix provided. It will also check to see + if there are Vars in the suffix provided. ``fix_vars`` will indicate + if we should fix all the Vars in the set or not. + + Parameters + ---------- + suffix: pyomo Suffix object, contains the components to be checked + as keys + fix_vars: boolean, whether or not to fix the Vars, default = False + + Returns + ------- + param_list: list of Param + """ + param_list = [] + + # FIX THE MODEL TREE ISSUE WHERE I GET base_model. INSTEAD OF + # Check keys if they are Param or Var. Fix the vars if ``fix_vars`` is True + for k, v in suffix.items(): + if isinstance(k, ParamData): + param_list.append(k.name) + elif isinstance(k, VarData): + if fix_vars: + k.fix() + else: + pass # ToDo: Write error for suffix keys that aren't ParamData or VarData + + return param_list \ No newline at end of file From f038bc22057dfbb09cd9050b4b75b43989f0e4ef Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Fri, 2 Aug 2024 14:09:22 -0400 Subject: [PATCH 2072/3044] Commented out new Param to Var code Will discuss at meeting later today (8/2/2024). --- pyomo/contrib/doe/doe.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 550c5ab447e..566afe78853 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1016,17 +1016,27 @@ def _generate_scenario_blocks(self, model=None): "Finite difference option not recognized. Please contact the developers as you should not see this error." ) - # Search for unknown parameters that are Params, or Vars. Params are updated to be fixed Vars - unknown_parameter_Params = get_parameters_from_suffix(model.base_model.unknown_parameters, fix_vars=True) - print(unknown_parameter_Params) + # TODO: Allow Params for `unknown_parameters` and `experiment_inputs` + # May need to make a new converter Param to Var that allows non-string names/references to be passed - # Change the unknown parameters that are Params to be Vars and fix them - if len(unknown_parameter_Params) > 0: - print("GOT HERE!!!!") - model.base_model = convert_params_to_vars(model.base_model, unknown_parameter_Params, fix_vars=True) + # # Search for unknown parameters that are Params, or Vars. Params are updated to be fixed Vars + # unknown_parameter_Params = get_parameters_from_suffix(model.base_model.unknown_parameters, fix_vars=True) + + # # # Remove ``base_model`` precursor name on parameters + # # for ind, val in enumerate(unknown_parameter_Params): + # # base_model_ind = val.split(".").index("base_model") + # # unknown_parameter_Params[ind] = ".".join(val.split(".")[(base_model_ind + 1) :]) - # Search for experiment inputs that are Params, or Vars. Params are updated to be unfixed vars - experiment_inputs_Params = get_parameters_from_suffix(model.base_model.experiment_inputs, fix_vars=False) + # print(unknown_parameter_Params) + + # # Change the unknown parameters that are Params to be Vars and fix them + # if len(unknown_parameter_Params) > 0: + # model.base_model = convert_params_to_vars(model.base_model, unknown_parameter_Params, fix_vars=True) + + # model.base_model.unknown_parameters.pprint() + + # # Search for experiment inputs that are Params, or Vars. Params are updated to be unfixed vars + # experiment_inputs_Params = get_parameters_from_suffix(model.base_model.experiment_inputs, fix_vars=False) # Run base model to get initialized model and check model function for comp, _ in model.base_model.experiment_inputs.items(): From 087e7be754a9118e57c62d0468cc4d99fc83d7b2 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Fri, 2 Aug 2024 14:15:22 -0400 Subject: [PATCH 2073/3044] ran black and typos --- pyomo/contrib/doe/doe.py | 4 ++-- pyomo/contrib/doe/utils.py | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 566afe78853..597c058d728 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1019,9 +1019,9 @@ def _generate_scenario_blocks(self, model=None): # TODO: Allow Params for `unknown_parameters` and `experiment_inputs` # May need to make a new converter Param to Var that allows non-string names/references to be passed - # # Search for unknown parameters that are Params, or Vars. Params are updated to be fixed Vars + # # Search for unknown parameters that are Params, or Vars. Params are updated to be fixed Vars # unknown_parameter_Params = get_parameters_from_suffix(model.base_model.unknown_parameters, fix_vars=True) - + # # # Remove ``base_model`` precursor name on parameters # # for ind, val in enumerate(unknown_parameter_Params): # # base_model_ind = val.split(".").index("base_model") diff --git a/pyomo/contrib/doe/utils.py b/pyomo/contrib/doe/utils.py index c7ec6f9d879..889bebbea33 100644 --- a/pyomo/contrib/doe/utils.py +++ b/pyomo/contrib/doe/utils.py @@ -64,15 +64,16 @@ def rescale_FIM(FIM, param_vals): scaled_FIM = np.multiply(FIM, scaling_mat) return scaled_FIM + def get_parameters_from_suffix(suffix, fix_vars=False): """ Finds the Params within the suffix provided. It will also check to see - if there are Vars in the suffix provided. ``fix_vars`` will indicate + if there are Vars in the suffix provided. ``fix_vars`` will indicate if we should fix all the Vars in the set or not. Parameters ---------- - suffix: pyomo Suffix object, contains the components to be checked + suffix: pyomo Suffix object, contains the components to be checked as keys fix_vars: boolean, whether or not to fix the Vars, default = False @@ -92,5 +93,5 @@ def get_parameters_from_suffix(suffix, fix_vars=False): k.fix() else: pass # ToDo: Write error for suffix keys that aren't ParamData or VarData - - return param_list \ No newline at end of file + + return param_list From 1fe0f000415e14bd791d7592594605a702127e70 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 2 Aug 2024 14:17:01 -0600 Subject: [PATCH 2074/3044] Fixing a couple bugs with additive decomposition in nonlinear-to-pwl --- .../piecewise/transform/nonlinear_to_pwl.py | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index f88c83af17f..4daf69e14ea 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -52,6 +52,7 @@ from pyomo.repn.quadratic import QuadraticRepnVisitor from pyomo.repn.util import ExprType + lineartree, lineartree_available = attempt_import('lineartree') sklearn_lm, sklearn_available = attempt_import('sklearn.linear_model') @@ -124,6 +125,7 @@ def get_points_lmt_uniform_sample(bounds, n, func, seed=42): def get_points_lmt(points, bounds, func, seed): x_list = np.array(points) y_list = [] + for point in points: y_list.append(func(*point)) # ESJ: Do we really need the sklearn dependency to get LinearRegression?? @@ -132,7 +134,9 @@ def get_points_lmt(points, bounds, func, seed): criterion='mse', max_bins=120, min_samples_leaf=4, - max_depth=5, + max_depth=max(4, int(np.log2(len(points) / 4))), # Want the tree to grow + # with increasing points + # but not get too large. ) regr.fit(x_list, y_list) @@ -439,6 +443,20 @@ class NonlinearToPWL(Transformation): triangulating the points in order to partition the function domain.""", ), ) + CONFIG.declare( + 'min_additive_decomposition_dimension', + ConfigValue( + default=1, + domain=PositiveInt, + description="The minimum dimension of functions that will be additively decomposed.", + doc=""" + Specifies the minimum dimension of a function that the transformation should + attempt to additively decompose. If a nonlinear function dimension exceeds + 'min_additive_decomposition_dimension' the transformation will additively decompose + If a the dimension of an expression is less than the "min_additive_decomposition_dimension" + then, it will not be additively decomposed""", + ), + ) # TODO: Minimum dimension to additively decompose--(Only decompose if the # dimension exceeds this.) @@ -634,11 +652,12 @@ def _approximate_expression( "'max_dimension' or additively separating the expression." % (obj.name, config.max_dimension) ) - pwl_func += subexpr + pwl_func = pwl_func + subexpr continue - elif not self._needs_approximating(expr, approximate_quadratic)[1]: - pwl_func += subexpr + elif not self._needs_approximating(subexpr, approximate_quadratic)[1]: + pwl_func = pwl_func + subexpr continue + # else we approximate subexpr def eval_expr(*args): for i, v in enumerate(expr_vars): @@ -655,7 +674,11 @@ def eval_expr(*args): trans_block, obj.getname(fully_qualified=False) ) trans_block.add_component(f"_pwle_{name}_{k}", pwlf) - pwl_func += pwlf(*expr_vars) + # NOTE: We are *not* using += because it will hit the NamedExpression + # implementation of iadd and dereference the ExpressionData holding + # the PiecewiseLinearExpression that we later transform my remapping + # it to a Var... + pwl_func = pwl_func + pwlf(*expr_vars) # restore var values for v, val in orig_values.items(): From 9ae91fbcc514aeeec2ffec88c545e2dbfc9384cb Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 2 Aug 2024 14:17:38 -0600 Subject: [PATCH 2075/3044] Adding partial test of additive decomposition --- .../piecewise/tests/test_nonlinear_to_pwl.py | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index c9edabd00bd..5c352bb3052 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -15,7 +15,11 @@ NonlinearToPWL, DomainPartitioningMethod, ) -from pyomo.core.expr.compare import assertExpressionsStructurallyEqual +from pyomo.core.expr.compare import ( + assertExpressionsEqual, + assertExpressionsStructurallyEqual, +) +from pyomo.core.expr.numeric_expr import SumExpression from pyomo.environ import ( ConcreteModel, Var, @@ -23,6 +27,8 @@ TransformationFactory, log, Objective, + Reals, + SolverFactory, ) ## debug @@ -368,7 +374,53 @@ def test_do_not_transform_quadratic_objective(self): self.assertEqual(len(nonlinear), 0) -# class TestNonlinearToPWLIntegration(unittest.TestCase): +class TestNonlinearToPWLIntegration(unittest.TestCase): + def test_additively_decompose(self): + m = ConcreteModel() + m.x1 = Var(within=Reals, bounds=(0, 2), initialize=1.745) + m.x4 = Var(within=Reals, bounds=(0, 5), initialize=3.048) + m.x7 = Var(within=Reals, bounds=(0.9, 0.95), initialize=0.928) + m.obj = Objective(expr=-6.3 * m.x4 * m.x7 + 5.04 * m.x1) + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, + num_points=4, + domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM, + additively_decompose=True, + ) + + self.assertFalse(m.obj.active) + new_obj = n_to_pwl.get_transformed_component(m.obj) + self.assertIs(n_to_pwl.get_src_component(new_obj), m.obj) + self.assertTrue(new_obj.active) + # two terms + self.assertIsInstance(new_obj.expr, SumExpression) + self.assertEqual(len(new_obj.expr.args), 2) + first = new_obj.expr.args[0] + pwlf = first.expr.pw_linear_function + all_pwlf = list( + m.component_data_objects(PiecewiseLinearFunction, descend_into=True) + ) + self.assertEqual(len(all_pwlf), 1) + # It is on the active tree. + self.assertIs(pwlf, all_pwlf[0]) + + second = new_obj.expr.args[1] + assertExpressionsEqual(self, second, 5.04 * m.x1) + + objs = n_to_pwl.get_transformed_nonlinear_objectives(m) + self.assertEqual(len(objs), 0) + objs = n_to_pwl.get_transformed_quadratic_objectives(m) + self.assertEqual(len(objs), 1) + self.assertIn(m.obj, objs) + self.assertEqual(len(n_to_pwl.get_transformed_nonlinear_constraints(m)), 0) + self.assertEqual(len(n_to_pwl.get_transformed_quadratic_constraints(m)), 0) + + TransformationFactory('contrib.piecewise.outer_repn_gdp').apply_to(m) + TransformationFactory('gdp.bigm').apply_to(m) + SolverFactory('gurobi').solve(m) + + # def test_Ali_example(self): # m = ConcreteModel() # m.flow_super_heated_vapor = Var() From 6345ecec6d45da5e93e5a05cc28f2f028f0eea8b Mon Sep 17 00:00:00 2001 From: whart222 Date: Mon, 5 Aug 2024 07:04:26 -0600 Subject: [PATCH 2076/3044] Using glpk for doctests --- .../contributed_packages/alternative_solutions.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst index cfabf66a6b9..06bde85423d 100644 --- a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst +++ b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst @@ -60,16 +60,16 @@ Many of functions in the alternative-solutions library have similar options, so We can execute the ``enumerate_binary_solutions`` function to generate a list of ``Solution`` objects that represent alternative optimal solutions: .. doctest:: - :skipif: not gurobi_available + :skipif: not glpk_available >>> import pyomo.contrib.alternative_solutions as aos - >>> solns = aos.enumerate_binary_solutions(m, num_solutions=100, solver="gurobi") + >>> solns = aos.enumerate_binary_solutions(m, num_solutions=100, solver="glpk") >>> assert len(solns) == 1 Each ``Solution`` object contains information about the objective and variables, and it includes various methods to access this information. For example: .. doctest:: - :skipif: not gurobi_available + :skipif: not glpk_available >>> print(solns[0]) { From e2e469bc32fcffaa3ec7698efa29d4aeda536f56 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:42:03 -0400 Subject: [PATCH 2077/3044] Revert reactor experiment for update --- .../doe/examples/reactor_experiment.py | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_experiment.py b/pyomo/contrib/doe/examples/reactor_experiment.py index 9b4ddae2bf5..68c33a4ca2b 100644 --- a/pyomo/contrib/doe/examples/reactor_experiment.py +++ b/pyomo/contrib/doe/examples/reactor_experiment.py @@ -59,15 +59,10 @@ def create_model(self): m.T = pyo.Var(m.t, within=pyo.NonNegativeReals) # Arrhenius rate law equations - # m.A1 = pyo.Var(within=pyo.NonNegativeReals) - # m.E1 = pyo.Var(within=pyo.NonNegativeReals) - # m.A2 = pyo.Var(within=pyo.NonNegativeReals) - # m.E2 = pyo.Var(within=pyo.NonNegativeReals) - - m.A1 = pyo.Param(mutable=True) - m.E1 = pyo.Param(mutable=True) - m.A2 = pyo.Param(mutable=True) - m.E2 = pyo.Param(mutable=True) + m.A1 = pyo.Var(within=pyo.NonNegativeReals) + m.E1 = pyo.Var(within=pyo.NonNegativeReals) + m.A2 = pyo.Var(within=pyo.NonNegativeReals) + m.E2 = pyo.Var(within=pyo.NonNegativeReals) # Differential variables (Conc.) m.dCAdt = DerivativeVar(m.CA, wrt=m.t) @@ -135,15 +130,10 @@ def finalize_model(self): m.t.update(control_points) # Fix the unknown parameter values - # m.A1.fix(self.data["A1"]) - # m.A2.fix(self.data["A2"]) - # m.E1.fix(self.data["E1"]) - # m.E2.fix(self.data["E2"]) - - m.A1.set_value(self.data["A1"]) - m.A2.set_value(self.data["A2"]) - m.E1.set_value(self.data["E1"]) - m.E2.set_value(self.data["E2"]) + m.A1.fix(self.data["A1"]) + m.A2.fix(self.data["A2"]) + m.E1.fix(self.data["E1"]) + m.E2.fix(self.data["E2"]) # Add upper and lower bounds to the design variable, CA[0] m.CA[0].setlb(self.data["CA_bounds"][0]) From ce6d92ea0e21ceb7e683dd6d9cb73559ae0d2c49 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:55:09 -0400 Subject: [PATCH 2078/3044] Updated experiment to be inherited from `parmest` --- pyomo/contrib/doe/examples/reactor_experiment.py | 2 +- pyomo/contrib/doe/tests/experiment_class_example.py | 2 +- pyomo/contrib/doe/tests/experiment_class_example_flags.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_experiment.py b/pyomo/contrib/doe/examples/reactor_experiment.py index 68c33a4ca2b..0eb9dcd4833 100644 --- a/pyomo/contrib/doe/examples/reactor_experiment.py +++ b/pyomo/contrib/doe/examples/reactor_experiment.py @@ -12,7 +12,7 @@ import pyomo.environ as pyo from pyomo.dae import ContinuousSet, DerivativeVar, Simulator -from pyomo.contrib.doe.experiment import Experiment +from pyomo.contrib.parmest.experiment import Experiment # ======================== diff --git a/pyomo/contrib/doe/tests/experiment_class_example.py b/pyomo/contrib/doe/tests/experiment_class_example.py index b4cdd13b416..2c555a732a8 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example.py +++ b/pyomo/contrib/doe/tests/experiment_class_example.py @@ -12,7 +12,7 @@ import pyomo.environ as pyo from pyomo.dae import ContinuousSet, DerivativeVar, Simulator -from pyomo.contrib.doe.experiment import Experiment +from pyomo.contrib.parmest.experiment import Experiment import itertools import json diff --git a/pyomo/contrib/doe/tests/experiment_class_example_flags.py b/pyomo/contrib/doe/tests/experiment_class_example_flags.py index c014f9f3065..324ab7461d0 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example_flags.py +++ b/pyomo/contrib/doe/tests/experiment_class_example_flags.py @@ -12,7 +12,7 @@ import pyomo.environ as pyo from pyomo.dae import ContinuousSet, DerivativeVar, Simulator -from pyomo.contrib.doe.experiment import Experiment +from pyomo.contrib.parmest.experiment import Experiment import itertools import json From 4d23cd88b784986b77dbd80899fe0d07890364d9 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:16:32 -0400 Subject: [PATCH 2079/3044] Update experiment inputs key as None --- pyomo/contrib/doe/examples/reactor_experiment.py | 9 +++++---- pyomo/contrib/doe/tests/experiment_class_example.py | 2 +- .../contrib/doe/tests/experiment_class_example_flags.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_experiment.py b/pyomo/contrib/doe/examples/reactor_experiment.py index 0eb9dcd4833..981c4a2c306 100644 --- a/pyomo/contrib/doe/examples/reactor_experiment.py +++ b/pyomo/contrib/doe/examples/reactor_experiment.py @@ -196,12 +196,13 @@ def label_experiment(self): # Identify design variables (experiment inputs) for the model m.experiment_inputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) # Add experimental input label for initial concentration - m.experiment_inputs.update( - (m.CA[t], pyo.ComponentUID(m.CA[t])) for t in [m.t.first()] - ) + # m.experiment_inputs.update( + # (m.CA[t], None) for t in [m.t.first()] + # ) + m.experiment_inputs[m.CA[m.t.first()]] = None # Add experimental input label for Temperature m.experiment_inputs.update( - (m.T[t], pyo.ComponentUID(m.T[t])) for t in m.t_control + (m.T[t], None) for t in m.t_control ) # Add unknown parameter labels diff --git a/pyomo/contrib/doe/tests/experiment_class_example.py b/pyomo/contrib/doe/tests/experiment_class_example.py index 2c555a732a8..54ac4f32d3c 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example.py +++ b/pyomo/contrib/doe/tests/experiment_class_example.py @@ -219,7 +219,7 @@ def label_experiment_impl(self, index_sets_meas): index_sets_des = [[[m.t.first()]], [m.t_control]] m.experiment_inputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.experiment_inputs.update( - (k, pyo.ComponentUID(k)) + (k, None) for k in expand_model_components(m, base_comp_des, index_sets_des) ) diff --git a/pyomo/contrib/doe/tests/experiment_class_example_flags.py b/pyomo/contrib/doe/tests/experiment_class_example_flags.py index 324ab7461d0..e0d8363b20a 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example_flags.py +++ b/pyomo/contrib/doe/tests/experiment_class_example_flags.py @@ -230,7 +230,7 @@ def label_experiment_impl(self, index_sets_meas, flag=0): index_sets_des = [[[m.t.first()]], [m.t_control]] m.experiment_inputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.experiment_inputs.update( - (k, pyo.ComponentUID(k)) + (k, None) for k in expand_model_components(m, base_comp_des, index_sets_des) ) From 19251d5e7e3c2b044f8f6fc8c14a93d1b87aed10 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:17:32 -0400 Subject: [PATCH 2080/3044] Simplify component identification in doe Per advice, using the ``context`` keyword to find parameters without using their string trees as before. --- pyomo/contrib/doe/doe.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 597c058d728..848da7d4986 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1069,10 +1069,6 @@ def build_block_scenarios(b, s): param = model.parameter_scenarios[s] - # Grabbing the index of the parameter without the "base_model" precursor - base_model_ind = param.name.split(".").index("base_model") - param_loc = ".".join(param.name.split(".")[(base_model_ind + 1) :]) - # Perturbation to be (1 + diff) * param_value if self.fd_formula == FiniteDifferenceStep.central: diff = self.step * ( @@ -1088,7 +1084,7 @@ def build_block_scenarios(b, s): pass # Update parameter values for the given finite difference scenario - pyo.ComponentUID(param_loc).find_component_on(b).set_value( + pyo.ComponentUID(param, context=model.base_model).find_component_on(b).set_value( model.base_model.unknown_parameters[param] * (1 + diff) ) @@ -1106,11 +1102,7 @@ def build_block_scenarios(b, s): def global_design_fixing(m, s): if s == 0: return pyo.Constraint.Skip - ref_design_var = model.scenario_blocks[0].experiment_inputs[d] - ref_design_var_loc = ".".join(ref_design_var.get_repr().split(".")[0:]) - block_design_var = pyo.ComponentUID( - ref_design_var_loc - ).find_component_on(model.scenario_blocks[s]) + block_design_var = pyo.ComponentUID(d, context=model.scenario_blocks[0]).find_component_on(model.scenario_blocks[s]) return d == block_design_var setattr( From e0978114332940d475b6b1df63deb9b5540d9467 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:23:04 -0400 Subject: [PATCH 2081/3044] Update variable fixing readability in doe --- pyomo/contrib/doe/doe.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 848da7d4986..b7aad640ffd 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -274,7 +274,7 @@ def run_doe(self, model=None, results_file=None): # Deactivate objective expression and objective constraints (on a block), and fix design variables model.objective.deactivate() model.obj_cons.deactivate() - for comp, _ in model.scenario_blocks[0].experiment_inputs.items(): + for comp in model.scenario_blocks[0].experiment_inputs: comp.fix() model.dummy_obj = pyo.Objective(expr=0, sense=pyo.minimize) @@ -290,7 +290,7 @@ def run_doe(self, model=None, results_file=None): model.dummy_obj.deactivate() # Reactivate objective and unfix experimental design decisions - for comp, _ in model.scenario_blocks[0].experiment_inputs.items(): + for comp in model.scenario_blocks[0].experiment_inputs: comp.unfix() model.objective.activate() model.obj_cons.activate() @@ -481,7 +481,7 @@ def _sequential_FIM(self, model=None): ) # Fix design variables - for comp, _ in model.experiment_inputs.items(): + for comp in model.experiment_inputs: comp.fix() measurement_vals = [] @@ -605,7 +605,7 @@ def _kaug_FIM(self, model=None): # call k_aug get_dsdp function # Solve the square problem # Deactivate object and fix experimental design decisions to make square - for comp, _ in model.experiment_inputs.items(): + for comp in model.experiment_inputs: comp.fix() self.solver.solve(model, tee=self.tee) @@ -1039,7 +1039,7 @@ def _generate_scenario_blocks(self, model=None): # experiment_inputs_Params = get_parameters_from_suffix(model.base_model.experiment_inputs, fix_vars=False) # Run base model to get initialized model and check model function - for comp, _ in model.base_model.experiment_inputs.items(): + for comp in model.base_model.experiment_inputs: comp.fix() try: @@ -1051,7 +1051,7 @@ def _generate_scenario_blocks(self, model=None): "Model from experiment did not solve appropriately. Make sure the model is well-posed." ) - for comp, _ in model.base_model.experiment_inputs.items(): + for comp in model.base_model.experiment_inputs: comp.unfix() # Generate blocks for finite difference scenarios From 73140d8851e3199d84fcdb0e291a5e9a8f1fd6ec Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:26:32 -0400 Subject: [PATCH 2082/3044] Change setattr to add_component --- pyomo/contrib/doe/doe.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index b7aad640ffd..884f7f4df68 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1105,11 +1105,7 @@ def global_design_fixing(m, s): block_design_var = pyo.ComponentUID(d, context=model.scenario_blocks[0]).find_component_on(model.scenario_blocks[s]) return d == block_design_var - setattr( - model, - con_name, - pyo.Constraint(model.scenarios, rule=global_design_fixing), - ) + model.add_component(con_name, pyo.Constraint(model.scenarios, rule=global_design_fixing)) # Clean up the base model used to generate the scenarios model.del_component(model.base_model) From 96b0a99277e81a22cacdb5526a1e41c55dfbea1b Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:33:15 -0400 Subject: [PATCH 2083/3044] Update import order --- pyomo/contrib/doe/doe.py | 46 ++++++++++++---------------------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 884f7f4df68..311d7443dd5 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -25,14 +25,12 @@ # publicly, and to permit other to do so. # ___________________________________________________________________________ -import pyomo.environ as pyo -from pyomo.opt import SolverStatus -from pyomo.common.timing import TicTocTimer -from pyomo.contrib.sensitivity_toolbox.sens import get_dsdp - -from pyomo.contrib.parmest.utils.model_utils import convert_params_to_vars - -from pyomo.contrib.doe.utils import get_parameters_from_suffix +from enum import Enum +from itertools import permutations, product +import json +import logging +import math +from pathlib import Path from pyomo.common.dependencies import ( numpy as np, @@ -41,13 +39,13 @@ matplotlib as plt, ) -from itertools import permutations, product -from enum import Enum -from pathlib import Path +from pyomo.common.timing import TicTocTimer -import logging -import json -import math +from pyomo.contrib.sensitivity_toolbox.sens import get_dsdp + +import pyomo.environ as pyo + +from pyomo.opt import SolverStatus class CalculationMode(Enum): @@ -1018,25 +1016,7 @@ def _generate_scenario_blocks(self, model=None): # TODO: Allow Params for `unknown_parameters` and `experiment_inputs` # May need to make a new converter Param to Var that allows non-string names/references to be passed - - # # Search for unknown parameters that are Params, or Vars. Params are updated to be fixed Vars - # unknown_parameter_Params = get_parameters_from_suffix(model.base_model.unknown_parameters, fix_vars=True) - - # # # Remove ``base_model`` precursor name on parameters - # # for ind, val in enumerate(unknown_parameter_Params): - # # base_model_ind = val.split(".").index("base_model") - # # unknown_parameter_Params[ind] = ".".join(val.split(".")[(base_model_ind + 1) :]) - - # print(unknown_parameter_Params) - - # # Change the unknown parameters that are Params to be Vars and fix them - # if len(unknown_parameter_Params) > 0: - # model.base_model = convert_params_to_vars(model.base_model, unknown_parameter_Params, fix_vars=True) - - # model.base_model.unknown_parameters.pprint() - - # # Search for experiment inputs that are Params, or Vars. Params are updated to be unfixed vars - # experiment_inputs_Params = get_parameters_from_suffix(model.base_model.experiment_inputs, fix_vars=False) + # Waiting on updates to the parmest params_to_vars utility function..... # Run base model to get initialized model and check model function for comp in model.base_model.experiment_inputs: From 4679cb7a1c594d28665e2bf88c2da3135a489a0f Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:36:07 -0400 Subject: [PATCH 2084/3044] Added unique component naming if model is provided --- pyomo/contrib/doe/doe.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 311d7443dd5..e79ecf2baa9 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -27,9 +27,11 @@ from enum import Enum from itertools import permutations, product + import json import logging import math + from pathlib import Path from pyomo.common.dependencies import ( @@ -38,7 +40,7 @@ pandas as pd, matplotlib as plt, ) - +from pyomo.common.modeling import unique_component_name from pyomo.common.timing import TicTocTimer from pyomo.contrib.sensitivity_toolbox.sens import get_dsdp @@ -252,6 +254,8 @@ def run_doe(self, model=None, results_file=None): # Model is none, set it to self.model if model is None: model = self.model + else: + model = unique_component_name(model, "design_of_experiments_block") # ToDo: potentially work with this for more complicated models # Create the full DoE model (build scenarios for F.D. scheme) From 20e5ae61242c56490b70ddbc28e0395f2fdfd65d Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:44:40 -0400 Subject: [PATCH 2085/3044] Added unique block addition to user models for doe tasks --- pyomo/contrib/doe/doe.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index e79ecf2baa9..ad89004a1ff 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -255,7 +255,9 @@ def run_doe(self, model=None, results_file=None): if model is None: model = self.model else: - model = unique_component_name(model, "design_of_experiments_block") + doe_block = pyo.Block() + doe_block_name = unique_component_name(model, "design_of_experiments_block") + model.add_component(doe_block_name, doe_block) # ToDo: potentially work with this for more complicated models # Create the full DoE model (build scenarios for F.D. scheme) @@ -406,6 +408,11 @@ def compute_FIM(self, model=None, method="sequential"): **self.args ).clone() model = self.compute_FIM_model + else: + doe_block = pyo.Block() + doe_block_name = unique_component_name(model, "design_of_experiments_block") + model.add_component(doe_block_name, doe_block) + self.compute_FIM_model = model self.check_model_labels(model=model) @@ -696,6 +703,10 @@ def create_doe_model(self, model=None): """ if model is None: model = self.model + else: + doe_block = pyo.Block() + doe_block_name = unique_component_name(model, "design_of_experiments_block") + model.add_component(doe_block_name, doe_block) # Developer recommendation: use the Cholesky decomposition for D-optimality # The explicit formula is available for benchmarking purposes and is NOT recommended From c06bfa520a884b36eb237cfa89c2e2d8f96a491b Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:47:52 -0400 Subject: [PATCH 2086/3044] Fixed potential bug --- pyomo/contrib/doe/doe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index ad89004a1ff..9abbda91858 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -282,7 +282,7 @@ def run_doe(self, model=None, results_file=None): comp.fix() model.dummy_obj = pyo.Objective(expr=0, sense=pyo.minimize) - self.solver.solve(self.model, tee=self.tee) + self.solver.solve(model, tee=self.tee) # Track time to initialize the DoE model initialization_time = sp_timer.toc(msg=None) From cba2b476b17c631d23f4306785127c279ab40d0b Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:51:38 -0400 Subject: [PATCH 2087/3044] Made L_LB more verbose --- pyomo/contrib/doe/doe.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 9abbda91858..bfaffa0391d 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -86,7 +86,7 @@ def __init__( jac_initial=None, fim_initial=None, L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -137,7 +137,7 @@ def __init__( 2D numpy array as the initial values for the FIM. L_initial: 2D numpy array as the initial values for the Cholesky matrix. - L_LB: + L_diagonal_lower_bound: Lower bound for the values of the lower triangular Cholesky factorization matrix. default: 1e-7 solver: @@ -187,7 +187,7 @@ def __init__( self.L_initial = L_initial # Set the lower bound on the Cholesky lower triangular matrix - self.L_LB = L_LB + self.L_diagonal_lower_bound = L_diagonal_lower_bound # check if user-defined solver is given if solver: @@ -829,9 +829,9 @@ def init_cho(m, i, j): if i < j: model.L[c, d].fix(0.0) # Give LB to the diagonal entries - if self.L_LB: + if self.L_diagonal_lower_bound: if c == d: - model.L[c, d].setlb(self.L_LB) + model.L[c, d].setlb(self.L_diagonal_lower_bound) # jacobian rule def jacobian_rule(m, n, p): From 93706d03d3d21ba8ba6d2e9241dc6fac90d458fc Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:07:00 -0400 Subject: [PATCH 2088/3044] Updated ``det`` to be more verbose --- pyomo/contrib/doe/doe.py | 67 ++++------- pyomo/contrib/doe/tests/test_doe_build.py | 45 +++---- pyomo/contrib/doe/tests/test_doe_errors.py | 134 ++++++++++----------- pyomo/contrib/doe/tests/test_doe_solve.py | 76 ++++++------ 4 files changed, 135 insertions(+), 187 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index bfaffa0391d..7854fd243f7 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -56,7 +56,7 @@ class CalculationMode(Enum): class ObjectiveLib(Enum): - det = "det" + determinant = "determinant" trace = "trace" zero = "zero" @@ -79,13 +79,12 @@ def __init__( experiment, fd_formula="central", step=1e-3, - objective_option="det", + objective_option="determinant", scale_constant_value=1.0, scale_nominal_param_value=False, prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -115,7 +114,7 @@ def __init__( default: 1e-3 objective_option: String representation of the objective option. Current available options are: - ``det`` (for determinant, or D-optimality) and ``trace`` (for trace or + ``determinant`` (for determinant, or D-optimality) and ``trace`` (for trace or A-optimality) scale_constant_value: Constant scaling for the sensitivity matrix. Every element will be multiplied by this @@ -135,8 +134,6 @@ def __init__( 2D numpy array as the initial values for the sensitivity matrix. fim_initial: 2D numpy array as the initial values for the FIM. - L_initial: - 2D numpy array as the initial values for the Cholesky matrix. L_diagonal_lower_bound: Lower bound for the values of the lower triangular Cholesky factorization matrix. default: 1e-7 @@ -184,7 +181,6 @@ def __init__( # Set the initial values for the jacobian, fim, and L matrices self.jac_initial = jac_initial self.fim_initial = fim_initial - self.L_initial = L_initial # Set the lower bound on the Cholesky lower triangular matrix self.L_diagonal_lower_bound = L_diagonal_lower_bound @@ -316,8 +312,8 @@ def run_doe(self, model=None, results_file=None): for j, d in enumerate(model.parameter_names): model.L[c, d].value = L_vals_sq[i, j] - if hasattr(model, "det"): - model.det.value = np.linalg.det(np.array(self.get_FIM())) + if hasattr(model, "determinant"): + model.determinant.value = np.linalg.det(np.array(self.get_FIM())) # Solve the full model, which has now been initialized with the square solve res = self.solver.solve(model, tee=self.tee) @@ -712,7 +708,7 @@ def create_doe_model(self, model=None): # The explicit formula is available for benchmarking purposes and is NOT recommended if ( self.only_compute_fim_lower - and self.objective_option == ObjectiveLib.det + and self.objective_option == ObjectiveLib.determinant and not self.Cholesky_option ): raise ValueError( @@ -794,33 +790,12 @@ def initialize_fim(m, j, d): # To-Do: Look into this functionality..... # if cholesky, define L elements as variables - if self.Cholesky_option and self.objective_option == ObjectiveLib.det: - - # move the L matrix initial point to a dictionary - if self.L_initial is not None: - dict_cho = { - (bu, un): self.L_initial[i][j] - for i, bu in enumerate(model.parameter_names) - for j, un in enumerate(model.parameter_names) - } - - # use the L dictionary to initialize L matrix - def init_cho(m, i, j): - return dict_cho[(i, j)] - - # Define elements of Cholesky decomposition matrix as Pyomo variables and either - # Initialize with L in L_initial - if self.L_initial is not None: - model.L = pyo.Var( - model.parameter_names, model.parameter_names, initialize=init_cho - ) - # or initialize with the identity matrix - else: - model.L = pyo.Var( - model.parameter_names, - model.parameter_names, - initialize=identity_matrix, - ) + if self.Cholesky_option and self.objective_option == ObjectiveLib.determinant: + model.L = pyo.Var( + model.parameter_names, + model.parameter_names, + initialize=identity_matrix, + ) # loop over parameter name for i, c in enumerate(model.parameter_names): @@ -1127,7 +1102,7 @@ def create_objective_function(self, model=None): model = self.model if self.objective_option not in [ - ObjectiveLib.det, + ObjectiveLib.determinant, ObjectiveLib.trace, ObjectiveLib.zero, ]: @@ -1156,7 +1131,7 @@ def create_objective_function(self, model=None): ) ### Initialize the Cholesky decomposition matrix - if self.Cholesky_option and self.objective_option == ObjectiveLib.det: + if self.Cholesky_option and self.objective_option == ObjectiveLib.determinant: # Calculate the eigenvalues of the FIM matrix eig = np.linalg.eigvals(fim) @@ -1201,7 +1176,7 @@ def trace_calc(m): """ return model.trace == sum(model.fim[j, j] for j in model.parameter_names) - def det_general(m): + def determinant_general(m): r"""Calculate determinant. Can be applied to FIM of any size. det(A) = \sum_{\sigma in \S_n} (sgn(\sigma) * \Prod_{i=1}^n a_{i,\sigma_i}) Use permutation() to get permutations, sgn() to get signature @@ -1233,9 +1208,9 @@ def det_general(m): ) for d in range(len(list_p)) ) - return model.det == det_perm + return model.determinant == det_perm - if self.Cholesky_option and self.objective_option == ObjectiveLib.det: + if self.Cholesky_option and self.objective_option == ObjectiveLib.determinant: model.obj_cons.cholesky_cons = pyo.Constraint( model.parameter_names, model.parameter_names, rule=cholesky_imp ) @@ -1244,14 +1219,14 @@ def det_general(m): sense=pyo.maximize, ) - elif self.objective_option == ObjectiveLib.det: + elif self.objective_option == ObjectiveLib.determinant: # if not cholesky but determinant, calculating det and evaluate the OBJ with det - model.det = pyo.Var( + model.determinant = pyo.Var( initialize=np.linalg.det(fim), bounds=(small_number, None) ) - model.obj_cons.det_rule = pyo.Constraint(rule=det_general) + model.obj_cons.determinant_rule = pyo.Constraint(rule=determinant_general) model.objective = pyo.Objective( - expr=pyo.log10(model.det + 1e-6), sense=pyo.maximize + expr=pyo.log10(model.determinant + 1e-6), sense=pyo.maximize ) elif self.objective_option == ObjectiveLib.trace: diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index c38bdd54386..6611225036e 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -111,8 +111,7 @@ def test_reactor_fd_central_check_fd_eqns(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -167,8 +166,7 @@ def test_reactor_fd_backward_check_fd_eqns(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -225,8 +223,7 @@ def test_reactor_fd_forward_check_fd_eqns(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -283,8 +280,7 @@ def test_reactor_fd_central_design_fixing(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -334,8 +330,7 @@ def test_reactor_fd_backward_design_fixing(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -385,8 +380,7 @@ def test_reactor_fd_forward_design_fixing(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -422,16 +416,13 @@ def test_reactor_fd_forward_design_fixing(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_user_initialization(self): fd_method = "central" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) FIM_prior = np.ones((4, 4)) FIM_initial = np.eye(4) + FIM_prior JAC_initial = np.ones((27, 4)) * 2 - L_initial = np.tril( - np.ones((4, 4)) * 3 - ) # Must input lower triangular to get equality doe_obj = DesignOfExperiments( experiment, @@ -443,8 +434,7 @@ def test_reactor_check_user_initialization(self): prior_FIM=FIM_prior, jac_initial=JAC_initial, fim_initial=FIM_initial, - L_initial=L_initial, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -460,7 +450,6 @@ def test_reactor_check_user_initialization(self): # Make sure they match the inputs we gave assert np.array_equal(FIM, FIM_initial) assert np.array_equal(FIM_prior, FIM_prior_model) - assert np.array_equal(L_initial, L) assert np.array_equal(JAC_initial, Q) @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @@ -483,8 +472,7 @@ def test_update_FIM(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -520,8 +508,7 @@ def test_get_experiment_inputs_without_blocks(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -555,8 +542,7 @@ def test_get_experiment_outputs_without_blocks(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -590,8 +576,7 @@ def test_get_measurement_error_without_blocks(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -625,8 +610,7 @@ def test_get_unknown_parameters_without_blocks(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -661,8 +645,7 @@ def test_generate_blocks_without_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index ec2024ac5a6..49309c4d358 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -57,8 +57,7 @@ def test_reactor_check_no_get_labeled_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -84,8 +83,7 @@ def test_reactor_check_no_experiment_outputs(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -117,8 +115,7 @@ def test_reactor_check_no_measurement_error(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -150,8 +147,7 @@ def test_reactor_check_no_experiment_inputs(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -183,8 +179,7 @@ def test_reactor_check_no_unknown_parameters(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -218,8 +213,7 @@ def test_reactor_check_bad_prior_size(self): prior_FIM=prior_FIM, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -255,8 +249,7 @@ def test_reactor_check_bad_jacobian_init_size(self): prior_FIM=None, jac_initial=jac_init, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -292,8 +285,7 @@ def test_reactor_check_unbuilt_update_FIM(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -326,9 +318,8 @@ def test_reactor_check_none_update_FIM(self): scale_nominal_param_value=True, prior_FIM=None, jac_initial=None, - fim_initial=None, - L_initial=None, - L_LB=1e-7, + fim_initial=None, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -360,8 +351,8 @@ def test_reactor_check_results_file_name(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -394,8 +385,8 @@ def test_reactor_check_measurement_and_output_length_match(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -416,7 +407,7 @@ def test_reactor_check_measurement_and_output_length_match(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_grid_search_des_range_inputs(self): fd_method = "central" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) @@ -430,8 +421,8 @@ def test_reactor_grid_search_des_range_inputs(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -454,7 +445,7 @@ def test_reactor_grid_search_des_range_inputs(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_premature_figure_drawing(self): fd_method = "central" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) @@ -468,8 +459,8 @@ def test_reactor_premature_figure_drawing(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -488,7 +479,7 @@ def test_reactor_premature_figure_drawing(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_figure_drawing_no_des_var_names(self): fd_method = "central" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) @@ -502,8 +493,8 @@ def test_reactor_figure_drawing_no_des_var_names(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -528,7 +519,7 @@ def test_reactor_figure_drawing_no_des_var_names(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_figure_drawing_no_sens_names(self): fd_method = "central" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) @@ -542,8 +533,8 @@ def test_reactor_figure_drawing_no_sens_names(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -567,7 +558,7 @@ def test_reactor_figure_drawing_no_sens_names(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_figure_drawing_no_fixed_names(self): fd_method = "central" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) @@ -581,8 +572,8 @@ def test_reactor_figure_drawing_no_fixed_names(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -606,7 +597,7 @@ def test_reactor_figure_drawing_no_fixed_names(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_figure_drawing_bad_fixed_names(self): fd_method = "central" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) @@ -620,8 +611,8 @@ def test_reactor_figure_drawing_bad_fixed_names(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -649,7 +640,7 @@ def test_reactor_figure_drawing_bad_fixed_names(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_figure_drawing_bad_sens_names(self): fd_method = "central" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) @@ -663,8 +654,8 @@ def test_reactor_figure_drawing_bad_sens_names(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -707,8 +698,8 @@ def test_reactor_check_get_FIM_without_FIM(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -742,8 +733,8 @@ def test_reactor_check_get_sens_mat_without_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -777,8 +768,8 @@ def test_reactor_check_get_exp_inputs_without_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -812,8 +803,8 @@ def test_reactor_check_get_exp_outputs_without_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -847,8 +838,8 @@ def test_reactor_check_get_unknown_params_without_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -882,8 +873,8 @@ def test_reactor_check_get_meas_error_without_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -917,8 +908,8 @@ def test_multiple_exp_not_implemented_seq(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -951,8 +942,8 @@ def test_multiple_exp_not_implemented_sim(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -985,8 +976,8 @@ def test_update_unknown_parameter_values_not_implemented_seq(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -1020,8 +1011,8 @@ def test_bad_FD_generate_scens(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -1057,8 +1048,8 @@ def test_bad_FD_seq_compute_FIM(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -1093,8 +1084,8 @@ def test_bad_objective(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -1129,8 +1120,8 @@ def test_no_model_for_objective(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, @@ -1165,8 +1156,7 @@ def test_bad_compute_FIM_option(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args={"flag": flag_val}, diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index bdef07c7fed..1c79225536c 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -113,8 +113,8 @@ def test_reactor_fd_central_solve(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -153,8 +153,8 @@ def test_reactor_fd_forward_solve(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -192,8 +192,8 @@ def test_reactor_fd_backward_solve(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -219,7 +219,7 @@ def test_reactor_fd_backward_solve(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_obj_det_solve(self): fd_method = "central" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) @@ -233,8 +233,8 @@ def test_reactor_obj_det_solve(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -250,7 +250,7 @@ def test_reactor_obj_det_solve(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_obj_cholesky_solve(self): fd_method = "central" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) @@ -264,8 +264,8 @@ def test_reactor_obj_cholesky_solve(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -290,7 +290,7 @@ def test_reactor_obj_cholesky_solve(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_compute_FIM_seq_centr(self): fd_method = "central" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) @@ -304,8 +304,8 @@ def test_compute_FIM_seq_centr(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -319,7 +319,7 @@ def test_compute_FIM_seq_centr(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_compute_FIM_seq_forward(self): fd_method = "forward" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) @@ -333,8 +333,8 @@ def test_compute_FIM_seq_forward(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -352,7 +352,7 @@ def test_compute_FIM_seq_forward(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_compute_FIM_kaug(self): fd_method = "forward" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) @@ -366,8 +366,8 @@ def test_compute_FIM_kaug(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -381,7 +381,7 @@ def test_compute_FIM_kaug(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_compute_FIM_seq_backward(self): fd_method = "backward" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) @@ -395,8 +395,8 @@ def test_compute_FIM_seq_backward(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -411,7 +411,7 @@ def test_compute_FIM_seq_backward(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_grid_search(self): fd_method = "central" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) @@ -425,8 +425,8 @@ def test_reactor_grid_search(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -457,7 +457,7 @@ def test_reactor_grid_search(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_rescale_FIM(self): fd_method = "central" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperiment(data_ex, 10, 3) @@ -472,8 +472,8 @@ def test_rescale_FIM(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -492,8 +492,8 @@ def test_rescale_FIM(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -530,7 +530,7 @@ def test_rescale_FIM(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_solve_bad_model(self): fd_method = "central" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperimentBad(data_ex, 10, 3) @@ -544,8 +544,8 @@ def test_reactor_solve_bad_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, @@ -564,7 +564,7 @@ def test_reactor_solve_bad_model(self): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_grid_search_bad_model(self): fd_method = "central" - obj_used = "det" + obj_used = "determinant" experiment = FullReactorExperimentBad(data_ex, 10, 3) @@ -578,8 +578,8 @@ def test_reactor_grid_search_bad_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + + L_diagonal_lower_bound=1e-7, solver=None, tee=False, args=None, From c36cedbba72a72fa5f8464b3f9ba657b461ac3db Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:10:29 -0400 Subject: [PATCH 2089/3044] Made ``args`` more verbose ``args`` are additional arguments that go to the get_labeled_model function. So the name was changed to ``get_labeled_model_args``` --- pyomo/contrib/doe/doe.py | 22 ++++---- pyomo/contrib/doe/tests/test_doe_build.py | 26 ++++----- pyomo/contrib/doe/tests/test_doe_errors.py | 64 +++++++++++----------- pyomo/contrib/doe/tests/test_doe_solve.py | 28 +++++----- 4 files changed, 70 insertions(+), 70 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 7854fd243f7..361c4ccc51e 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -88,7 +88,7 @@ def __init__( L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, logger_level=logging.WARNING, _Cholesky_option=True, _only_compute_fim_lower=True, @@ -142,7 +142,7 @@ def __init__( If not specified, default solver is set to IPOPT with MA57. tee: Solver option to be passed for verbose output. - args: + get_labeled_model_args: Additional arguments for the ``get_labeled_model`` function on the Experiment object. _Cholesky_option: Boolean value of whether or not to use the choleskyn factorization to compute the @@ -198,10 +198,10 @@ def __init__( self.tee = tee - # Set args as an empty dict if no arguments are passed - if args is None: - args = {} - self.args = args + # Set get_labeled_model_args as an empty dict if no arguments are passed + if get_labeled_model_args is None: + get_labeled_model_args = {} + self.get_labeled_model_args = get_labeled_model_args # Revtrieve logger and set logging level self.logger = logging.getLogger(__name__) @@ -401,7 +401,7 @@ def compute_FIM(self, model=None, method="sequential"): """ if model is None: self.compute_FIM_model = self.experiment.get_labeled_model( - **self.args + **self.get_labeled_model_args ).clone() model = self.compute_FIM_model else: @@ -453,7 +453,7 @@ def _sequential_FIM(self, model=None): # Build a singular model instance if model is None: self.compute_FIM_model = self.experiment.get_labeled_model( - **self.args + **self.get_labeled_model_args ).clone() model = self.compute_FIM_model @@ -599,7 +599,7 @@ def _kaug_FIM(self, model=None): # compute_FIM_model needs to be the right version for function to work. if model is None: self.compute_FIM_model = self.experiment.get_labeled_model( - **self.args + **self.get_labeled_model_args ).clone() model = self.compute_FIM_model @@ -942,7 +942,7 @@ def _generate_scenario_blocks(self, model=None): model = self.model # Generate initial scenario to populate unknown parameter values - model.base_model = self.experiment.get_labeled_model(**self.args).clone() + model.base_model = self.experiment.get_labeled_model(**self.get_labeled_model_args).clone() # Check the model that labels are correct self.check_model_labels(model=model.base_model) @@ -1393,7 +1393,7 @@ def compute_FIM_full_factorial(self, design_ranges=None, method="sequential"): self.logger.info("Beginning Full Factorial Design.") # Make new model for factorial design - self.factorial_model = self.experiment.get_labeled_model(**self.args).clone() + self.factorial_model = self.experiment.get_labeled_model(**self.get_labeled_model_args).clone() model = self.factorial_model # Permute the inputs to be aligned with the experiment input indices diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 6611225036e..9751153aea8 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -114,7 +114,7 @@ def test_reactor_fd_central_check_fd_eqns(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -169,7 +169,7 @@ def test_reactor_fd_backward_check_fd_eqns(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -226,7 +226,7 @@ def test_reactor_fd_forward_check_fd_eqns(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -283,7 +283,7 @@ def test_reactor_fd_central_design_fixing(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -333,7 +333,7 @@ def test_reactor_fd_backward_design_fixing(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -383,7 +383,7 @@ def test_reactor_fd_forward_design_fixing(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -437,7 +437,7 @@ def test_reactor_check_user_initialization(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -475,7 +475,7 @@ def test_update_FIM(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -511,7 +511,7 @@ def test_get_experiment_inputs_without_blocks(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -545,7 +545,7 @@ def test_get_experiment_outputs_without_blocks(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -579,7 +579,7 @@ def test_get_measurement_error_without_blocks(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -613,7 +613,7 @@ def test_get_unknown_parameters_without_blocks(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -648,7 +648,7 @@ def test_generate_blocks_without_model(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 49309c4d358..9405a8eaf46 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -60,7 +60,7 @@ def test_reactor_check_no_get_labeled_model(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -86,7 +86,7 @@ def test_reactor_check_no_experiment_outputs(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -118,7 +118,7 @@ def test_reactor_check_no_measurement_error(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -150,7 +150,7 @@ def test_reactor_check_no_experiment_inputs(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -182,7 +182,7 @@ def test_reactor_check_no_unknown_parameters(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -216,7 +216,7 @@ def test_reactor_check_bad_prior_size(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -252,7 +252,7 @@ def test_reactor_check_bad_jacobian_init_size(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -288,7 +288,7 @@ def test_reactor_check_unbuilt_update_FIM(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -322,7 +322,7 @@ def test_reactor_check_none_update_FIM(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -355,7 +355,7 @@ def test_reactor_check_results_file_name(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -389,7 +389,7 @@ def test_reactor_check_measurement_and_output_length_match(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -425,7 +425,7 @@ def test_reactor_grid_search_des_range_inputs(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -463,7 +463,7 @@ def test_reactor_premature_figure_drawing(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -497,7 +497,7 @@ def test_reactor_figure_drawing_no_des_var_names(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -537,7 +537,7 @@ def test_reactor_figure_drawing_no_sens_names(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -576,7 +576,7 @@ def test_reactor_figure_drawing_no_fixed_names(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -615,7 +615,7 @@ def test_reactor_figure_drawing_bad_fixed_names(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -658,7 +658,7 @@ def test_reactor_figure_drawing_bad_sens_names(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -702,7 +702,7 @@ def test_reactor_check_get_FIM_without_FIM(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -737,7 +737,7 @@ def test_reactor_check_get_sens_mat_without_model(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -772,7 +772,7 @@ def test_reactor_check_get_exp_inputs_without_model(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -807,7 +807,7 @@ def test_reactor_check_get_exp_outputs_without_model(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -842,7 +842,7 @@ def test_reactor_check_get_unknown_params_without_model(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -877,7 +877,7 @@ def test_reactor_check_get_meas_error_without_model(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -912,7 +912,7 @@ def test_multiple_exp_not_implemented_seq(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -946,7 +946,7 @@ def test_multiple_exp_not_implemented_sim(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -980,7 +980,7 @@ def test_update_unknown_parameter_values_not_implemented_seq(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -1015,7 +1015,7 @@ def test_bad_FD_generate_scens(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -1052,7 +1052,7 @@ def test_bad_FD_seq_compute_FIM(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -1088,7 +1088,7 @@ def test_bad_objective(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -1124,7 +1124,7 @@ def test_no_model_for_objective(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -1159,7 +1159,7 @@ def test_bad_compute_FIM_option(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args={"flag": flag_val}, + get_labeled_model_args={"flag": flag_val}, _Cholesky_option=True, _only_compute_fim_lower=True, ) diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 1c79225536c..07627a3d2aa 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -117,7 +117,7 @@ def test_reactor_fd_central_solve(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -157,7 +157,7 @@ def test_reactor_fd_forward_solve(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -196,7 +196,7 @@ def test_reactor_fd_backward_solve(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -237,7 +237,7 @@ def test_reactor_obj_det_solve(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=False, _only_compute_fim_lower=False, ) @@ -268,7 +268,7 @@ def test_reactor_obj_cholesky_solve(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -308,7 +308,7 @@ def test_compute_FIM_seq_centr(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -337,7 +337,7 @@ def test_compute_FIM_seq_forward(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -370,7 +370,7 @@ def test_compute_FIM_kaug(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -399,7 +399,7 @@ def test_compute_FIM_seq_backward(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -429,7 +429,7 @@ def test_reactor_grid_search(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -476,7 +476,7 @@ def test_rescale_FIM(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -496,7 +496,7 @@ def test_rescale_FIM(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -548,7 +548,7 @@ def test_reactor_solve_bad_model(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -582,7 +582,7 @@ def test_reactor_grid_search_bad_model(self): L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, logger_level=logging.ERROR, From ddc8ec8d3b55d01e099ee94efd664d65a5ef55e7 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:21:44 -0400 Subject: [PATCH 2090/3044] Added naming map for results object --- pyomo/contrib/doe/doe.py | 4 ++++ .../doe/examples/reactor_compute_factorial_FIM.py | 7 +++---- pyomo/contrib/doe/examples/reactor_example.py | 9 +++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 361c4ccc51e..ec790a8651b 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -342,9 +342,13 @@ def run_doe(self, model=None, results_file=None): self.results["FIM"] = fim_local self.results["Sensitivity Matrix"] = self.get_sensitivity_matrix() self.results["Experiment Design"] = self.get_experiment_input_values() + self.results["Experiment Design Names"] = [str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) for k in self.model.scenario_blocks[0].experiment_inputs] self.results["Experiment Outputs"] = self.get_experiment_output_values() + self.results["Experiment Output Names"] = [str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) for k in self.model.scenario_blocks[0].experiment_outputs] self.results["Unknown Parameters"] = self.get_unknown_parameter_values() + self.results["Unknown Parameter Names"] = [str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) for k in self.model.scenario_blocks[0].unknown_parameters] self.results["Measurement Error"] = self.get_measurement_error_values() + self.results["Measurement Error Names"] = [str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) for k in self.model.scenario_blocks[0].measurement_error] self.results["Prior FIM"] = [list(row) for row in list(self.prior_FIM)] diff --git a/pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py b/pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py index c7f10d115b3..0826a131bab 100644 --- a/pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py +++ b/pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py @@ -42,7 +42,7 @@ def run_reactor_doe(): step_size = 1e-3 # Use the determinant objective with scaled sensitivity matrix - objective_option = "det" + objective_option = "determinant" scale_nominal_param_value = True # Create the DesignOfExperiments object @@ -59,11 +59,10 @@ def run_reactor_doe(): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) diff --git a/pyomo/contrib/doe/examples/reactor_example.py b/pyomo/contrib/doe/examples/reactor_example.py index 2f7cd7b43e3..c17bd097303 100644 --- a/pyomo/contrib/doe/examples/reactor_example.py +++ b/pyomo/contrib/doe/examples/reactor_example.py @@ -42,7 +42,7 @@ def run_reactor_doe(): step_size = 1e-3 # Use the determinant objective with scaled sensitivity matrix - objective_option = "det" + objective_option = "determinant" scale_nominal_param_value = True # Create the DesignOfExperiments object @@ -59,11 +59,10 @@ def run_reactor_doe(): prior_FIM=None, jac_initial=None, fim_initial=None, - L_initial=None, - L_LB=1e-7, + L_diagonal_lower_bound=1e-7, solver=None, tee=False, - args=None, + get_labeled_model_args=None, _Cholesky_option=True, _only_compute_fim_lower=True, ) @@ -89,6 +88,8 @@ def run_reactor_doe(): ) ) + print(doe_obj.results["Experiment Design Names"]) + if __name__ == "__main__": run_reactor_doe() From 2c22203a2d02eeb3ca9f4e60e840503882be99de Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:27:25 -0400 Subject: [PATCH 2091/3044] Updated import order for test files --- pyomo/contrib/doe/tests/test_doe_build.py | 10 ++++------ pyomo/contrib/doe/tests/test_doe_errors.py | 9 ++++----- pyomo/contrib/doe/tests/test_doe_solve.py | 10 +++++----- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 9751153aea8..6efe731c08b 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -8,23 +8,21 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from pathlib import Path + from pyomo.common.dependencies import ( numpy as np, numpy_available, pandas as pd, pandas_available, ) +import pyomo.common.unittest as unittest -from pyomo.contrib.doe.tests.experiment_class_example import * from pyomo.contrib.doe import * - - -import pyomo.common.unittest as unittest +from pyomo.contrib.doe.tests.experiment_class_example import * from pyomo.opt import SolverFactory -from pathlib import Path - ipopt_available = SolverFactory("ipopt").available() DATA_DIR = Path(__file__).parent diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 9405a8eaf46..025371ab8a7 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -8,22 +8,21 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from pathlib import Path + from pyomo.common.dependencies import ( numpy as np, numpy_available, pandas as pd, pandas_available, ) +import pyomo.common.unittest as unittest -from pyomo.contrib.doe.tests.experiment_class_example_flags import * from pyomo.contrib.doe import * - -import pyomo.common.unittest as unittest +from pyomo.contrib.doe.tests.experiment_class_example_flags import * from pyomo.opt import SolverFactory -from pathlib import Path - ipopt_available = SolverFactory("ipopt").available() DATA_DIR = Path(__file__).parent diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 07627a3d2aa..a3645fabbed 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -8,6 +8,9 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import logging +from pathlib import Path + from pyomo.common.dependencies import ( numpy as np, numpy_available, @@ -15,22 +18,19 @@ pandas_available, scipy_available, ) +import pyomo.common.unittest as unittest +from pyomo.contrib.doe import * from pyomo.contrib.doe.tests.experiment_class_example import * from pyomo.contrib.doe.tests.experiment_class_example_flags import ( FullReactorExperimentBad, ) -from pyomo.contrib.doe import * from pyomo.contrib.doe.utils import * -import pyomo.common.unittest as unittest import pyomo.environ as pyo from pyomo.opt import SolverFactory -from pathlib import Path - -import logging ipopt_available = SolverFactory("ipopt").available() k_aug_available = SolverFactory('k_aug', solver_io='nl', validate=False) From c2967cd3be355995e75a0b1e4c5274d75d41e16b Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:33:24 -0400 Subject: [PATCH 2092/3044] Remove stale Enum structures --- pyomo/contrib/doe/__init__.py | 2 -- pyomo/contrib/doe/doe.py | 11 ----------- 2 files changed, 13 deletions(-) diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index aaca4db12fb..c0dcce8596d 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -10,9 +10,7 @@ # ___________________________________________________________________________ from .doe import ( DesignOfExperiments, - CalculationMode, ObjectiveLib, - ModelOptionLib, FiniteDifferenceStep, ) from .tests import experiment_class_example, experiment_class_example_flags diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index ec790a8651b..f4ae4280ec6 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -50,23 +50,12 @@ from pyomo.opt import SolverStatus -class CalculationMode(Enum): - sequential_finite = "sequential_finite" - direct_kaug = "direct_kaug" - - class ObjectiveLib(Enum): determinant = "determinant" trace = "trace" zero = "zero" -class ModelOptionLib(Enum): - parmest = "parmest" - stage1 = "stage1" - stage2 = "stage2" - - class FiniteDifferenceStep(Enum): forward = "forward" central = "central" From 8074fe083b4325acc5f1dbd9831f288755ccccf2 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 11:53:11 -0600 Subject: [PATCH 2093/3044] NFC: Reformatting some docstrings --- .../piecewise/transform/nonlinear_to_pwl.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 4daf69e14ea..d1002930a26 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -413,8 +413,8 @@ class NonlinearToPWL(Transformation): ConfigValue( default=False, domain=bool, - description="Whether or not to additively decompose constraints and " - "approximate the summands separately.", + description="Whether or not to additively decompose constraint expressions " + "and approximate the summands separately.", doc=""" If False, each nonlinear constraint expression will be approximated by exactly one piecewise-linear function. If True, constraints will be @@ -444,17 +444,19 @@ class NonlinearToPWL(Transformation): ), ) CONFIG.declare( - 'min_additive_decomposition_dimension', + 'min_dimension_to_additively_decompose', ConfigValue( default=1, domain=PositiveInt, - description="The minimum dimension of functions that will be additively decomposed.", + description="The minimum dimension of functions that will be additively " + "decomposed.", doc=""" - Specifies the minimum dimension of a function that the transformation should - attempt to additively decompose. If a nonlinear function dimension exceeds - 'min_additive_decomposition_dimension' the transformation will additively decompose - If a the dimension of an expression is less than the "min_additive_decomposition_dimension" - then, it will not be additively decomposed""", + Specifies the minimum dimension of a function that the transformation + should attempt to additively decompose. If a nonlinear function dimension + exceeds 'min_dimension_to_additively_decompose' the transformation will + additively decompose. If a the dimension of an expression is less than + the 'min_dimension_to_additively_decompose' then it will not be additively + decomposed""", ), ) # TODO: Minimum dimension to additively decompose--(Only decompose if the From 383cbc7c5e663e4df0c709a17d8a64be0228f192 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 5 Aug 2024 13:31:44 -0500 Subject: [PATCH 2094/3044] Ran black and typos --- pyomo/contrib/doe/__init__.py | 6 +-- pyomo/contrib/doe/doe.py | 46 +++++++++++++------ .../doe/examples/reactor_experiment.py | 4 +- .../doe/tests/experiment_class_example.py | 3 +- pyomo/contrib/doe/tests/test_doe_errors.py | 24 +--------- pyomo/contrib/doe/tests/test_doe_solve.py | 14 ------ 6 files changed, 36 insertions(+), 61 deletions(-) diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index c0dcce8596d..72cad1cabdb 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -8,11 +8,7 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from .doe import ( - DesignOfExperiments, - ObjectiveLib, - FiniteDifferenceStep, -) +from .doe import DesignOfExperiments, ObjectiveLib, FiniteDifferenceStep from .tests import experiment_class_example, experiment_class_example_flags from .utils import rescale_FIM, get_parameters_from_suffix from .examples import reactor_experiment, reactor_example, reactor_compute_factorial_FIM diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index f4ae4280ec6..ab5d9df3faa 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -331,13 +331,25 @@ def run_doe(self, model=None, results_file=None): self.results["FIM"] = fim_local self.results["Sensitivity Matrix"] = self.get_sensitivity_matrix() self.results["Experiment Design"] = self.get_experiment_input_values() - self.results["Experiment Design Names"] = [str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) for k in self.model.scenario_blocks[0].experiment_inputs] + self.results["Experiment Design Names"] = [ + str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) + for k in self.model.scenario_blocks[0].experiment_inputs + ] self.results["Experiment Outputs"] = self.get_experiment_output_values() - self.results["Experiment Output Names"] = [str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) for k in self.model.scenario_blocks[0].experiment_outputs] + self.results["Experiment Output Names"] = [ + str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) + for k in self.model.scenario_blocks[0].experiment_outputs + ] self.results["Unknown Parameters"] = self.get_unknown_parameter_values() - self.results["Unknown Parameter Names"] = [str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) for k in self.model.scenario_blocks[0].unknown_parameters] + self.results["Unknown Parameter Names"] = [ + str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) + for k in self.model.scenario_blocks[0].unknown_parameters + ] self.results["Measurement Error"] = self.get_measurement_error_values() - self.results["Measurement Error Names"] = [str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) for k in self.model.scenario_blocks[0].measurement_error] + self.results["Measurement Error Names"] = [ + str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) + for k in self.model.scenario_blocks[0].measurement_error + ] self.results["Prior FIM"] = [list(row) for row in list(self.prior_FIM)] @@ -785,9 +797,7 @@ def initialize_fim(m, j, d): # if cholesky, define L elements as variables if self.Cholesky_option and self.objective_option == ObjectiveLib.determinant: model.L = pyo.Var( - model.parameter_names, - model.parameter_names, - initialize=identity_matrix, + model.parameter_names, model.parameter_names, initialize=identity_matrix ) # loop over parameter name @@ -935,7 +945,9 @@ def _generate_scenario_blocks(self, model=None): model = self.model # Generate initial scenario to populate unknown parameter values - model.base_model = self.experiment.get_labeled_model(**self.get_labeled_model_args).clone() + model.base_model = self.experiment.get_labeled_model( + **self.get_labeled_model_args + ).clone() # Check the model that labels are correct self.check_model_labels(model=model.base_model) @@ -1047,9 +1059,9 @@ def build_block_scenarios(b, s): pass # Update parameter values for the given finite difference scenario - pyo.ComponentUID(param, context=model.base_model).find_component_on(b).set_value( - model.base_model.unknown_parameters[param] * (1 + diff) - ) + pyo.ComponentUID(param, context=model.base_model).find_component_on( + b + ).set_value(model.base_model.unknown_parameters[param] * (1 + diff)) model.scenario_blocks = pyo.Block(model.scenarios, rule=build_block_scenarios) @@ -1065,10 +1077,14 @@ def build_block_scenarios(b, s): def global_design_fixing(m, s): if s == 0: return pyo.Constraint.Skip - block_design_var = pyo.ComponentUID(d, context=model.scenario_blocks[0]).find_component_on(model.scenario_blocks[s]) + block_design_var = pyo.ComponentUID( + d, context=model.scenario_blocks[0] + ).find_component_on(model.scenario_blocks[s]) return d == block_design_var - model.add_component(con_name, pyo.Constraint(model.scenarios, rule=global_design_fixing)) + model.add_component( + con_name, pyo.Constraint(model.scenarios, rule=global_design_fixing) + ) # Clean up the base model used to generate the scenarios model.del_component(model.base_model) @@ -1386,7 +1402,9 @@ def compute_FIM_full_factorial(self, design_ranges=None, method="sequential"): self.logger.info("Beginning Full Factorial Design.") # Make new model for factorial design - self.factorial_model = self.experiment.get_labeled_model(**self.get_labeled_model_args).clone() + self.factorial_model = self.experiment.get_labeled_model( + **self.get_labeled_model_args + ).clone() model = self.factorial_model # Permute the inputs to be aligned with the experiment input indices diff --git a/pyomo/contrib/doe/examples/reactor_experiment.py b/pyomo/contrib/doe/examples/reactor_experiment.py index 981c4a2c306..52056c01fa2 100644 --- a/pyomo/contrib/doe/examples/reactor_experiment.py +++ b/pyomo/contrib/doe/examples/reactor_experiment.py @@ -201,9 +201,7 @@ def label_experiment(self): # ) m.experiment_inputs[m.CA[m.t.first()]] = None # Add experimental input label for Temperature - m.experiment_inputs.update( - (m.T[t], None) for t in m.t_control - ) + m.experiment_inputs.update((m.T[t], None) for t in m.t_control) # Add unknown parameter labels m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) diff --git a/pyomo/contrib/doe/tests/experiment_class_example.py b/pyomo/contrib/doe/tests/experiment_class_example.py index 54ac4f32d3c..03c8ca3a784 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example.py +++ b/pyomo/contrib/doe/tests/experiment_class_example.py @@ -219,8 +219,7 @@ def label_experiment_impl(self, index_sets_meas): index_sets_des = [[[m.t.first()]], [m.t_control]] m.experiment_inputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) m.experiment_inputs.update( - (k, None) - for k in expand_model_components(m, base_comp_des, index_sets_des) + (k, None) for k in expand_model_components(m, base_comp_des, index_sets_des) ) m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 025371ab8a7..ee3b0cc27be 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -317,7 +317,7 @@ def test_reactor_check_none_update_FIM(self): scale_nominal_param_value=True, prior_FIM=None, jac_initial=None, - fim_initial=None, + fim_initial=None, L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -350,7 +350,6 @@ def test_reactor_check_results_file_name(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -384,7 +383,6 @@ def test_reactor_check_measurement_and_output_length_match(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -420,7 +418,6 @@ def test_reactor_grid_search_des_range_inputs(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -458,7 +455,6 @@ def test_reactor_premature_figure_drawing(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -492,7 +488,6 @@ def test_reactor_figure_drawing_no_des_var_names(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -532,7 +527,6 @@ def test_reactor_figure_drawing_no_sens_names(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -571,7 +565,6 @@ def test_reactor_figure_drawing_no_fixed_names(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -610,7 +603,6 @@ def test_reactor_figure_drawing_bad_fixed_names(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -653,7 +645,6 @@ def test_reactor_figure_drawing_bad_sens_names(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -697,7 +688,6 @@ def test_reactor_check_get_FIM_without_FIM(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -732,7 +722,6 @@ def test_reactor_check_get_sens_mat_without_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -767,7 +756,6 @@ def test_reactor_check_get_exp_inputs_without_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -802,7 +790,6 @@ def test_reactor_check_get_exp_outputs_without_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -837,7 +824,6 @@ def test_reactor_check_get_unknown_params_without_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -872,7 +858,6 @@ def test_reactor_check_get_meas_error_without_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -907,7 +892,6 @@ def test_multiple_exp_not_implemented_seq(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -941,7 +925,6 @@ def test_multiple_exp_not_implemented_sim(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -975,7 +958,6 @@ def test_update_unknown_parameter_values_not_implemented_seq(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -1010,7 +992,6 @@ def test_bad_FD_generate_scens(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -1047,7 +1028,6 @@ def test_bad_FD_seq_compute_FIM(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -1083,7 +1063,6 @@ def test_bad_objective(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -1119,7 +1098,6 @@ def test_no_model_for_objective(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index a3645fabbed..6439b9f8c37 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -113,7 +113,6 @@ def test_reactor_fd_central_solve(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -153,7 +152,6 @@ def test_reactor_fd_forward_solve(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -192,7 +190,6 @@ def test_reactor_fd_backward_solve(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -233,7 +230,6 @@ def test_reactor_obj_det_solve(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -264,7 +260,6 @@ def test_reactor_obj_cholesky_solve(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -304,7 +299,6 @@ def test_compute_FIM_seq_centr(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -333,7 +327,6 @@ def test_compute_FIM_seq_forward(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -366,7 +359,6 @@ def test_compute_FIM_kaug(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -395,7 +387,6 @@ def test_compute_FIM_seq_backward(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -425,7 +416,6 @@ def test_reactor_grid_search(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -472,7 +462,6 @@ def test_rescale_FIM(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -492,7 +481,6 @@ def test_rescale_FIM(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -544,7 +532,6 @@ def test_reactor_solve_bad_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, @@ -578,7 +565,6 @@ def test_reactor_grid_search_bad_model(self): prior_FIM=None, jac_initial=None, fim_initial=None, - L_diagonal_lower_bound=1e-7, solver=None, tee=False, From b78efe540752b2f15778568836f437fcc70875fa Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 13:36:40 -0600 Subject: [PATCH 2095/3044] Adding max depth argument to be used for the linear tree domain partitioning methods --- .../piecewise/transform/nonlinear_to_pwl.py | 81 +++++++++++-------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index d1002930a26..851c5a9e0f4 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -90,7 +90,7 @@ def __init__(self): Block.register_private_data_initializer(_NonlinearToPWLTransformationData) -def get_random_point_grid(bounds, n, func, seed=42): +def get_random_point_grid(bounds, n, func, config, seed=42): # Generate randomized grid of points linspaces = [] for lb, ub in bounds: @@ -99,7 +99,7 @@ def get_random_point_grid(bounds, n, func, seed=42): return list(itertools.product(*linspaces)) -def get_uniform_point_grid(bounds, n, func): +def get_uniform_point_grid(bounds, n, func, config): # Generate non-randomized grid of points linspaces = [] for lb, ub in bounds: @@ -112,31 +112,32 @@ def get_uniform_point_grid(bounds, n, func): return list(itertools.product(*linspaces)) -def get_points_lmt_random_sample(bounds, n, func, seed=42): +def get_points_lmt_random_sample(bounds, n, func, config, seed=42): points = get_random_point_grid(bounds, n, func, seed=seed) - return get_points_lmt(points, bounds, func, seed) + return get_points_lmt(points, bounds, func, config, seed) -def get_points_lmt_uniform_sample(bounds, n, func, seed=42): +def get_points_lmt_uniform_sample(bounds, n, func, config, seed=42): points = get_uniform_point_grid(bounds, n, func) - return get_points_lmt(points, bounds, func, seed) + return get_points_lmt(points, bounds, func, seed, config) -def get_points_lmt(points, bounds, func, seed): +def get_points_lmt(points, bounds, func, seed, config): x_list = np.array(points) y_list = [] for point in points: y_list.append(func(*point)) - # ESJ: Do we really need the sklearn dependency to get LinearRegression?? + max_depth = config.linear_tree_max_depth + if max_depth is None: + # Want the tree to grow with increasing points but not get too large. + max_depth = max(4, int(np.log2(len(points) / 4))) regr = lineartree.LinearTreeRegressor( sklearn_lm.LinearRegression(), criterion='mse', max_bins=120, min_samples_leaf=4, - max_depth=max(4, int(np.log2(len(points) / 4))), # Want the tree to grow - # with increasing points - # but not get too large. + max_depth=max_depth ) regr.fit(x_list, y_list) @@ -162,19 +163,21 @@ def get_points_lmt(points, bounds, func, seed): } -def get_pwl_function_approximation(func, method, n, bounds): +def _get_pwl_function_approximation(func, config, bounds): """ Get a piecewise-linear approximation of a function, given: func: function to approximate - method: method to use for the approximation, member of DomainPartitioningMethod - n: parameter controlling fineness of the approximation based on the specified method - bounds: list of tuples giving upper and lower bounds for each of func's arguments + config: ConfigDict for transformation, specifying domain_partitioning_method, + num_points, and max_depth (if using linear trees) + bounds: list of tuples giving upper and lower bounds for each of func's + arguments """ - points = _partition_method_dispatcher[method](bounds, n, func) + method = config.domain_partitioning_method + n = config.num_points + points = _partition_method_dispatcher[method](bounds, n, func, config) - # DUCT TAPE WARNING: work around deficiency in PiecewiseLinearFunction - # constructor. TODO + # Don't confuse PiecewiseLinearFunction constructor... dim = len(points[0]) if dim == 1: points = [pt[0] for pt in points] @@ -188,10 +191,6 @@ def get_pwl_function_approximation(func, method, n, bounds): return PiecewiseLinearFunction(points=points, function=func) -# TODO: this is still horrible. Maybe I should put these back together into -# a wrapper class again, but better this time? - - # Given a leaves dict (as generated by parse_tree) and a list of tuples # representing variable bounds, generate the set of vertices separating each # subset of the domain @@ -288,9 +287,11 @@ def parse_linear_tree_regressor(linear_tree_regressor, bounds): # This doesn't catch all additively separable expressions--we really need a # walker (as does gdp.partition_disjuncts) -def _additively_decompose_expr(input_expr): - if input_expr.__class__ is not SumExpression: - # This isn't separable, so we just have the one expression +def _additively_decompose_expr(input_expr, min_dimension): + dimension = len(list(identify_variables(input_expr))) + if input_expr.__class__ is not SumExpression or dimension < min_dimension: + # This isn't separable or we don't want to separate it, so we just have + # the one expression return [input_expr] # else, it was a SumExpression, and we will break it into the summands return list(input_expr.args) @@ -459,10 +460,22 @@ class NonlinearToPWL(Transformation): decomposed""", ), ) - # TODO: Minimum dimension to additively decompose--(Only decompose if the - # dimension exceeds this.) - - # TODO: incorporate Bashar's linear tree changes. + CONFIG.declare( + 'linear_tree_max_depth', + ConfigValue( + default=None, + domain=PositiveInt, + description="Maximum depth for linear tree training, used if using a " + "domain partitioning method based on linear model trees.", + doc=""" + Only used if 'domain_partitioning_method' is LINEAR_MODEL_TREE_UNIFORM or + LINEAR_MODEL_TREE_RANDOM: Specifies the maximum depth of the linear model + trees trained to determine the points to be triangulated to form the + domain of the piecewise-linear approximations. If None (the default), + the max depth will be given as max(4, ln(num_points / 4)). + """, + ), + ) def __init__(self): super(Transformation).__init__() @@ -640,7 +653,10 @@ def _approximate_expression( # Additively decompose expr and work on the pieces pwl_func = 0 for k, subexpr in enumerate( - _additively_decompose_expr(expr) if config.additively_decompose else (expr,) + _additively_decompose_expr( + expr, + config.min_dimension_to_additively_decompose) + if config.additively_decompose else (expr,) ): # First check if this is a good idea expr_vars = list(identify_variables(subexpr, include_fixed=False)) @@ -666,10 +682,9 @@ def eval_expr(*args): v.value = args[i] return value(subexpr) - pwlf = get_pwl_function_approximation( + pwlf = _get_pwl_function_approximation( eval_expr, - config.domain_partitioning_method, - config.num_points, + config, self._get_bounds_list(expr_vars, obj), ) name = unique_component_name( From 31a95bab3b3894bdcaa27c4a0191a1e6fe77b117 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 14:38:31 -0600 Subject: [PATCH 2096/3044] Adding tests with absolute value for linear model tree partitioning --- .../piecewise/tests/test_nonlinear_to_pwl.py | 99 ++++++++++++++++++- .../piecewise/transform/nonlinear_to_pwl.py | 8 +- 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 5c352bb3052..b779543cc9b 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from pyomo.common.dependencies import attempt_import, scipy_available import pyomo.common.unittest as unittest from pyomo.contrib.piecewise import PiecewiseLinearFunction from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( @@ -31,9 +32,15 @@ SolverFactory, ) -## debug -from pytest import set_trace +gurobi_available = ( + SolverFactory('gurobi').available(exception_flag=False) + and SolverFactory('gurobi').license_is_valid() +) +lineartree_available = attempt_import('lineartree')[1] +sklearn_available = attempt_import('sklearn.linear_model')[1] +## DEBUG +from pytest import set_trace class TestNonlinearToPWL_1D(unittest.TestCase): def make_model(self): @@ -374,8 +381,90 @@ def test_do_not_transform_quadratic_objective(self): self.assertEqual(len(nonlinear), 0) +@unittest.skipUnless(lineartree_available, "lineartree not available") +@unittest.skipUnless(sklearn_available, "sklearn not available") +class TestLinearTreeDomainPartitioning(unittest.TestCase): + def make_absolute_value_model(self): + m = ConcreteModel() + m.x = Var(bounds=(-10, 10)) + m.obj = Objective(expr=abs(m.x)) + + return m + + def test_linear_model_tree_uniform(self): + m = self.make_absolute_value_model() + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, + num_points=301, # sample a lot so we train a good tree + domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_UNIFORM, + linear_tree_max_depth=1, # force parsimony + ) + + transformed_obj = n_to_pwl.get_transformed_component(m.obj) + pwlf = transformed_obj.expr.expr.pw_linear_function + + self.assertEqual(len(pwlf._simplices), 2) + self.assertEqual(pwlf._simplices, [(0, 1), (1, 2)]) + self.assertEqual(pwlf._points, [(-10,), (-0.08402,), (10,)]) + self.assertEqual(len(pwlf._linear_functions), 2) + assertExpressionsEqual( + self, + pwlf._linear_functions[0](m.x), + - 1.0 * m.x + ) + assertExpressionsStructurallyEqual( + self, + pwlf._linear_functions[1](m.x), + # pretty close to m.x, but we're a bit off because we don't have 0 + # as a breakpoint. + 0.9833360108369479*m.x + 0.16663989163052034, + places=7 + ) + + def test_linear_model_tree_random(self): + m = self.make_absolute_value_model() + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, + num_points=300, # sample a lot so we train a good tree + domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM, + linear_tree_max_depth=1, # force parsimony + ) + + transformed_obj = n_to_pwl.get_transformed_component(m.obj) + pwlf = transformed_obj.expr.expr.pw_linear_function + + self.assertEqual(len(pwlf._simplices), 2) + self.assertEqual(pwlf._simplices, [(0, 1), (1, 2)]) + self.assertEqual(pwlf._points, [(-10,), (-0.03638,), (10,)]) + self.assertEqual(len(pwlf._linear_functions), 2) + assertExpressionsEqual( + self, + pwlf._linear_functions[0](m.x), + - 1.0 * m.x + ) + assertExpressionsStructurallyEqual( + self, + pwlf._linear_functions[1](m.x), + # pretty close to m.x, but we're a bit off because we don't have 0 + # as a breakpoint. + 0.9927503741388829*m.x + 0.07249625861117256, + places=7 + ) + + class TestNonlinearToPWLIntegration(unittest.TestCase): - def test_additively_decompose(self): + @unittest.skipUnless(gurobi_available, "Gurobi is not available") + @unittest.skipUnless(scipy_available, "Scipy is not available") + def test_transform_and_solve_additively_decomposes_model(self): + # A bit of an integration test to make sure that we build additively + # decomposed pw-linear approximations in such a way that they are + # transformed to MILP and solved correctly. (Largely because we have to + # be careful to make sure that we don't ever directly insert + # PiecewiseLinearExpression objects into expressions and are instead + # using the ExpressionData that points to them (and will eventually be + # replaced in transformation)) m = ConcreteModel() m.x1 = Var(within=Reals, bounds=(0, 2), initialize=1.745) m.x4 = Var(within=Reals, bounds=(0, 5), initialize=3.048) @@ -385,7 +474,7 @@ def test_additively_decompose(self): n_to_pwl.apply_to( m, num_points=4, - domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, additively_decompose=True, ) @@ -420,6 +509,8 @@ def test_additively_decompose(self): TransformationFactory('gdp.bigm').apply_to(m) SolverFactory('gurobi').solve(m) + # actually test the answer or something + self.assertTrue(False) # def test_Ali_example(self): # m = ConcreteModel() diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 851c5a9e0f4..59330a373eb 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -113,16 +113,16 @@ def get_uniform_point_grid(bounds, n, func, config): def get_points_lmt_random_sample(bounds, n, func, config, seed=42): - points = get_random_point_grid(bounds, n, func, seed=seed) + points = get_random_point_grid(bounds, n, func, config, seed=seed) return get_points_lmt(points, bounds, func, config, seed) def get_points_lmt_uniform_sample(bounds, n, func, config, seed=42): - points = get_uniform_point_grid(bounds, n, func) - return get_points_lmt(points, bounds, func, seed, config) + points = get_uniform_point_grid(bounds, n, func, config) + return get_points_lmt(points, bounds, func, config, seed) -def get_points_lmt(points, bounds, func, seed, config): +def get_points_lmt(points, bounds, func, config, seed): x_list = np.array(points) y_list = [] From c89ddbf454819acf4fef59d37696b858f09bf16c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 14:39:38 -0600 Subject: [PATCH 2097/3044] NFC: black --- .../piecewise/tests/test_nonlinear_to_pwl.py | 30 ++++++++----------- .../piecewise/transform/nonlinear_to_pwl.py | 13 ++++---- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index b779543cc9b..7e8669e209a 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -42,6 +42,7 @@ ## DEBUG from pytest import set_trace + class TestNonlinearToPWL_1D(unittest.TestCase): def make_model(self): m = ConcreteModel() @@ -396,9 +397,9 @@ def test_linear_model_tree_uniform(self): n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') n_to_pwl.apply_to( m, - num_points=301, # sample a lot so we train a good tree + num_points=301, # sample a lot so we train a good tree domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_UNIFORM, - linear_tree_max_depth=1, # force parsimony + linear_tree_max_depth=1, # force parsimony ) transformed_obj = n_to_pwl.get_transformed_component(m.obj) @@ -408,18 +409,14 @@ def test_linear_model_tree_uniform(self): self.assertEqual(pwlf._simplices, [(0, 1), (1, 2)]) self.assertEqual(pwlf._points, [(-10,), (-0.08402,), (10,)]) self.assertEqual(len(pwlf._linear_functions), 2) - assertExpressionsEqual( - self, - pwlf._linear_functions[0](m.x), - - 1.0 * m.x - ) + assertExpressionsEqual(self, pwlf._linear_functions[0](m.x), -1.0 * m.x) assertExpressionsStructurallyEqual( self, pwlf._linear_functions[1](m.x), # pretty close to m.x, but we're a bit off because we don't have 0 # as a breakpoint. - 0.9833360108369479*m.x + 0.16663989163052034, - places=7 + 0.9833360108369479 * m.x + 0.16663989163052034, + places=7, ) def test_linear_model_tree_random(self): @@ -427,9 +424,9 @@ def test_linear_model_tree_random(self): n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') n_to_pwl.apply_to( m, - num_points=300, # sample a lot so we train a good tree + num_points=300, # sample a lot so we train a good tree domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM, - linear_tree_max_depth=1, # force parsimony + linear_tree_max_depth=1, # force parsimony ) transformed_obj = n_to_pwl.get_transformed_component(m.obj) @@ -439,18 +436,14 @@ def test_linear_model_tree_random(self): self.assertEqual(pwlf._simplices, [(0, 1), (1, 2)]) self.assertEqual(pwlf._points, [(-10,), (-0.03638,), (10,)]) self.assertEqual(len(pwlf._linear_functions), 2) - assertExpressionsEqual( - self, - pwlf._linear_functions[0](m.x), - - 1.0 * m.x - ) + assertExpressionsEqual(self, pwlf._linear_functions[0](m.x), -1.0 * m.x) assertExpressionsStructurallyEqual( self, pwlf._linear_functions[1](m.x), # pretty close to m.x, but we're a bit off because we don't have 0 # as a breakpoint. - 0.9927503741388829*m.x + 0.07249625861117256, - places=7 + 0.9927503741388829 * m.x + 0.07249625861117256, + places=7, ) @@ -512,6 +505,7 @@ def test_transform_and_solve_additively_decomposes_model(self): # actually test the answer or something self.assertTrue(False) + # def test_Ali_example(self): # m = ConcreteModel() # m.flow_super_heated_vapor = Var() diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 59330a373eb..4b549c0be8d 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -137,7 +137,7 @@ def get_points_lmt(points, bounds, func, config, seed): criterion='mse', max_bins=120, min_samples_leaf=4, - max_depth=max_depth + max_depth=max_depth, ) regr.fit(x_list, y_list) @@ -654,9 +654,10 @@ def _approximate_expression( pwl_func = 0 for k, subexpr in enumerate( _additively_decompose_expr( - expr, - config.min_dimension_to_additively_decompose) - if config.additively_decompose else (expr,) + expr, config.min_dimension_to_additively_decompose + ) + if config.additively_decompose + else (expr,) ): # First check if this is a good idea expr_vars = list(identify_variables(subexpr, include_fixed=False)) @@ -683,9 +684,7 @@ def eval_expr(*args): return value(subexpr) pwlf = _get_pwl_function_approximation( - eval_expr, - config, - self._get_bounds_list(expr_vars, obj), + eval_expr, config, self._get_bounds_list(expr_vars, obj) ) name = unique_component_name( trans_block, obj.getname(fully_qualified=False) From e4bfca0af063f78e9aa50e0c3f758200b98fddbd Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 15:16:20 -0600 Subject: [PATCH 2098/3044] Finishing integration test, adding bigger linear model tree test --- .../piecewise/tests/test_nonlinear_to_pwl.py | 118 +++++++++++------- 1 file changed, 73 insertions(+), 45 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 7e8669e209a..074ef64b12d 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -30,6 +30,8 @@ Objective, Reals, SolverFactory, + TerminationCondition, + value ) gurobi_available = ( @@ -231,32 +233,6 @@ def test_cannot_approximate_constraints_with_unbounded_vars(self): domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, ) - # def test_log_constraint_lmt_uniform_sample(self): - # m = self.make_model() - - # n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') - # n_to_pwl.apply_to( - # m, - # num_points=3, - # domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_UNIFORM, - # ) - - # # cons is transformed - # self.assertFalse(m.cons.active) - - # pwlf = list(m.component_data_objects(PiecewiseLinearFunction, - # descend_into=True)) - # self.assertEqual(len(pwlf), 1) - # pwlf = pwlf[0] - - # set_trace() - - # # TODO - # x1 = 4.370861069626263 - # x2 = 7.587945476302646 - # x3 = 9.556428757689245 - # self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) - class TestNonlinearToPWL_2D(unittest.TestCase): def make_paraboloid_model(self): @@ -446,6 +422,46 @@ def test_linear_model_tree_random(self): places=7, ) + def test_linear_model_tree_random_auto_depth_tree(self): + m = self.make_absolute_value_model() + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, + num_points=100, # sample a lot but not too many because this one is + # more prone to overfitting + domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM, + ) + + transformed_obj = n_to_pwl.get_transformed_component(m.obj) + pwlf = transformed_obj.expr.expr.pw_linear_function + + print(pwlf._simplices) + print(pwlf._points) + for f in pwlf._linear_functions: + print(f(m.x)) + + # We end up with 8, which is just what happens, but it's not a terrible + # approximation + self.assertEqual(len(pwlf._simplices), 8) + self.assertEqual(pwlf._simplices, [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), + (5, 6), (6, 7), (7, 8)]) + self.assertEqual(pwlf._points, [(-10,), (-9.24119,), (-8.71428,), (-8.11135,), + (0.06048,), (0.70015,), (1.9285,), (2.15597,), + (10,)]) + self.assertEqual(len(pwlf._linear_functions), 8) + for i in range(3): + assertExpressionsEqual(self, pwlf._linear_functions[i](m.x), -1.0 * m.x) + assertExpressionsStructurallyEqual( + self, + pwlf._linear_functions[3](m.x), + # pretty close to - m.x, but we're a bit off because we don't have 0 + # as a breakpoint. + -0.9851979299618323*m.x + 0.12006477080409184, + places=7, + ) + for i in range(4, 8): + assertExpressionsEqual(self, pwlf._linear_functions[i](m.x), m.x) + class TestNonlinearToPWLIntegration(unittest.TestCase): @unittest.skipUnless(gurobi_available, "Gurobi is not available") @@ -464,16 +480,16 @@ def test_transform_and_solve_additively_decomposes_model(self): m.x7 = Var(within=Reals, bounds=(0.9, 0.95), initialize=0.928) m.obj = Objective(expr=-6.3 * m.x4 * m.x7 + 5.04 * m.x1) n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') - n_to_pwl.apply_to( + xm = n_to_pwl.create_using( m, num_points=4, domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, additively_decompose=True, ) - self.assertFalse(m.obj.active) - new_obj = n_to_pwl.get_transformed_component(m.obj) - self.assertIs(n_to_pwl.get_src_component(new_obj), m.obj) + self.assertFalse(xm.obj.active) + new_obj = n_to_pwl.get_transformed_component(xm.obj) + self.assertIs(n_to_pwl.get_src_component(new_obj), xm.obj) self.assertTrue(new_obj.active) # two terms self.assertIsInstance(new_obj.expr, SumExpression) @@ -481,30 +497,42 @@ def test_transform_and_solve_additively_decomposes_model(self): first = new_obj.expr.args[0] pwlf = first.expr.pw_linear_function all_pwlf = list( - m.component_data_objects(PiecewiseLinearFunction, descend_into=True) + xm.component_data_objects(PiecewiseLinearFunction, descend_into=True) ) self.assertEqual(len(all_pwlf), 1) # It is on the active tree. self.assertIs(pwlf, all_pwlf[0]) second = new_obj.expr.args[1] - assertExpressionsEqual(self, second, 5.04 * m.x1) + assertExpressionsEqual(self, second, 5.04 * xm.x1) - objs = n_to_pwl.get_transformed_nonlinear_objectives(m) + objs = n_to_pwl.get_transformed_nonlinear_objectives(xm) self.assertEqual(len(objs), 0) - objs = n_to_pwl.get_transformed_quadratic_objectives(m) + objs = n_to_pwl.get_transformed_quadratic_objectives(xm) self.assertEqual(len(objs), 1) - self.assertIn(m.obj, objs) - self.assertEqual(len(n_to_pwl.get_transformed_nonlinear_constraints(m)), 0) - self.assertEqual(len(n_to_pwl.get_transformed_quadratic_constraints(m)), 0) - - TransformationFactory('contrib.piecewise.outer_repn_gdp').apply_to(m) - TransformationFactory('gdp.bigm').apply_to(m) - SolverFactory('gurobi').solve(m) - - # actually test the answer or something - self.assertTrue(False) - + self.assertIn(xm.obj, objs) + self.assertEqual(len(n_to_pwl.get_transformed_nonlinear_constraints(xm)), 0) + self.assertEqual(len(n_to_pwl.get_transformed_quadratic_constraints(xm)), 0) + + TransformationFactory('contrib.piecewise.outer_repn_gdp').apply_to(xm) + TransformationFactory('gdp.bigm').apply_to(xm) + opt = SolverFactory('gurobi') + results = opt.solve(xm) + self.assertEqual(results.solver.termination_condition, + TerminationCondition.optimal) + + # solve the original + opt.options['NonConvex'] = 2 + results = opt.solve(m) + self.assertEqual(results.solver.termination_condition, + TerminationCondition.optimal) + + # Not a bad approximation: + self.assertAlmostEqual(value(xm.obj), value(m.obj), places=2) + + self.assertAlmostEqual(value(xm.x4), value(m.x4), places=3) + self.assertAlmostEqual(value(xm.x7), value(m.x7), places=4) + self.assertAlmostEqual(value(xm.x1), value(m.x1), places=7) # def test_Ali_example(self): # m = ConcreteModel() From 96b9aeecf37a9d3f954dc2c08b2f37b61ca84fa4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 15:16:59 -0600 Subject: [PATCH 2099/3044] NFC: black --- .../piecewise/tests/test_nonlinear_to_pwl.py | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 074ef64b12d..2a3a9821173 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -31,7 +31,7 @@ Reals, SolverFactory, TerminationCondition, - value + value, ) gurobi_available = ( @@ -428,7 +428,7 @@ def test_linear_model_tree_random_auto_depth_tree(self): n_to_pwl.apply_to( m, num_points=100, # sample a lot but not too many because this one is - # more prone to overfitting + # more prone to overfitting domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM, ) @@ -443,11 +443,24 @@ def test_linear_model_tree_random_auto_depth_tree(self): # We end up with 8, which is just what happens, but it's not a terrible # approximation self.assertEqual(len(pwlf._simplices), 8) - self.assertEqual(pwlf._simplices, [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), - (5, 6), (6, 7), (7, 8)]) - self.assertEqual(pwlf._points, [(-10,), (-9.24119,), (-8.71428,), (-8.11135,), - (0.06048,), (0.70015,), (1.9285,), (2.15597,), - (10,)]) + self.assertEqual( + pwlf._simplices, + [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8)], + ) + self.assertEqual( + pwlf._points, + [ + (-10,), + (-9.24119,), + (-8.71428,), + (-8.11135,), + (0.06048,), + (0.70015,), + (1.9285,), + (2.15597,), + (10,), + ], + ) self.assertEqual(len(pwlf._linear_functions), 8) for i in range(3): assertExpressionsEqual(self, pwlf._linear_functions[i](m.x), -1.0 * m.x) @@ -456,7 +469,7 @@ def test_linear_model_tree_random_auto_depth_tree(self): pwlf._linear_functions[3](m.x), # pretty close to - m.x, but we're a bit off because we don't have 0 # as a breakpoint. - -0.9851979299618323*m.x + 0.12006477080409184, + -0.9851979299618323 * m.x + 0.12006477080409184, places=7, ) for i in range(4, 8): @@ -518,14 +531,16 @@ def test_transform_and_solve_additively_decomposes_model(self): TransformationFactory('gdp.bigm').apply_to(xm) opt = SolverFactory('gurobi') results = opt.solve(xm) - self.assertEqual(results.solver.termination_condition, - TerminationCondition.optimal) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) # solve the original opt.options['NonConvex'] = 2 results = opt.solve(m) - self.assertEqual(results.solver.termination_condition, - TerminationCondition.optimal) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) # Not a bad approximation: self.assertAlmostEqual(value(xm.obj), value(m.obj), places=2) @@ -534,6 +549,7 @@ def test_transform_and_solve_additively_decomposes_model(self): self.assertAlmostEqual(value(xm.x7), value(m.x7), places=4) self.assertAlmostEqual(value(xm.x1), value(m.x1), places=7) + # def test_Ali_example(self): # m = ConcreteModel() # m.flow_super_heated_vapor = Var() From 76459a2ed5923a6ad83d48b6bbeeb0b1f82a0fc0 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 15:34:46 -0600 Subject: [PATCH 2100/3044] nonlinear_to_pwl.py --- .../piecewise/tests/test_nonlinear_to_pwl.py | 102 +++++++----------- .../piecewise/transform/nonlinear_to_pwl.py | 2 +- 2 files changed, 39 insertions(+), 65 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 2a3a9821173..36b646c6420 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -16,6 +16,7 @@ NonlinearToPWL, DomainPartitioningMethod, ) +from pyomo.core.base.expression import _ExpressionData from pyomo.core.expr.compare import ( assertExpressionsEqual, assertExpressionsStructurallyEqual, @@ -41,9 +42,6 @@ lineartree_available = attempt_import('lineartree')[1] sklearn_available = attempt_import('sklearn.linear_model')[1] -## DEBUG -from pytest import set_trace - class TestNonlinearToPWL_1D(unittest.TestCase): def make_model(self): @@ -233,6 +231,43 @@ def test_cannot_approximate_constraints_with_unbounded_vars(self): domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, ) + def test_error_for_non_separable_exceeding_max_dimension(self): + m = ConcreteModel() + m.x = Var([0, 1, 2, 3, 4], bounds=(-4, 5)) + m.ick = Constraint(expr=m.x[0] ** (m.x[1] * m.x[2] * m.x[3] * m.x[4]) <= 8) + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + with self.assertRaisesRegex( + ValueError, + "Not approximating expression for component 'ick' as " + "it exceeds the maximum dimension of 4. Try increasing " + "'max_dimension' or additively separating the expression." + ): + n_to_pwl.apply_to( + m, + num_points=3, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + max_dimension=4 + ) + + def test_do_not_additively_decompose_below_min_dimension(self): + m = ConcreteModel() + m.x = Var([0, 1, 2, 3, 4], bounds=(-4, 5)) + m.c = Constraint(expr=m.x[0] * m.x[1] + m.x[3] <= 4) + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, + num_points=3, + additively_decompose=True, + min_dimension_to_additively_decompose=4, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + ) + + transformed_c = n_to_pwl.get_transformed_component(m.c) + # This is only approximated by one pwlf: + self.assertIsInstance(transformed_c.body, _ExpressionData) + class TestNonlinearToPWL_2D(unittest.TestCase): def make_paraboloid_model(self): @@ -548,64 +583,3 @@ def test_transform_and_solve_additively_decomposes_model(self): self.assertAlmostEqual(value(xm.x4), value(m.x4), places=3) self.assertAlmostEqual(value(xm.x7), value(m.x7), places=4) self.assertAlmostEqual(value(xm.x1), value(m.x1), places=7) - - -# def test_Ali_example(self): -# m = ConcreteModel() -# m.flow_super_heated_vapor = Var() -# m.flow_super_heated_vapor.fix(0.4586949988166174) -# m.super_heated_vapor_temperature = Var(bounds=(31, 200), initialize=45) -# m.evaporator_condensate_temperature = Var( -# bounds=(29, 120.8291392028045), initialize=30 -# ) -# m.LMTD = Var(bounds=(0, 130.61608989795093), initialize=1) -# m.evaporator_condensate_enthalpy = Var( -# bounds=(-15836.847, -15510.210751855624), initialize=100 -# ) -# m.evaporator_condensate_vapor_enthalpy = Var( -# bounds=(-13416.64, -13247.674383866839), initialize=100 -# ) -# m.heat_transfer_coef = Var( -# bounds=(1.9936854577372858, 5.995319594088982), initialize=0.1 -# ) -# m.evaporator_brine_temperature = Var( -# bounds=(27, 118.82913920280366), initialize=35 -# ) -# m.each_evaporator_area = Var() - -# m.c = Constraint( -# expr=m.each_evaporator_area -# == ( -# 1.873 -# * m.flow_super_heated_vapor -# * ( -# m.super_heated_vapor_temperature -# - m.evaporator_condensate_temperature -# ) -# / (100 * m.LMTD) -# + m.flow_super_heated_vapor -# * ( -# m.evaporator_condensate_vapor_enthalpy -# - m.evaporator_condensate_enthalpy -# ) -# / ( -# m.heat_transfer_coef -# * ( -# m.evaporator_condensate_temperature -# - m.evaporator_brine_temperature -# ) -# ) -# ) -# ) - -# n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') -# n_to_pwl.apply_to( -# m, -# num_points=3, -# domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, -# ) - -# m.pprint() - -# from pyomo.environ import SolverFactory -# SolverFactory('gurobi').solve(m, tee=True) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 4b549c0be8d..30797b5318e 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -665,7 +665,7 @@ def _approximate_expression( dim = len(expr_vars) if dim > config.max_dimension: - logger.warning( + raise ValueError( "Not approximating expression for component '%s' as " "it exceeds the maximum dimension of %s. Try increasing " "'max_dimension' or additively separating the expression." From c61a7052d680a57d5e67e24500359fdcf9ce2163 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 15:36:06 -0600 Subject: [PATCH 2101/3044] Black --- .../contrib/piecewise/tests/test_nonlinear_to_pwl.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 36b646c6420..f5f4ebc8009 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -238,23 +238,23 @@ def test_error_for_non_separable_exceeding_max_dimension(self): n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') with self.assertRaisesRegex( - ValueError, - "Not approximating expression for component 'ick' as " - "it exceeds the maximum dimension of 4. Try increasing " - "'max_dimension' or additively separating the expression." + ValueError, + "Not approximating expression for component 'ick' as " + "it exceeds the maximum dimension of 4. Try increasing " + "'max_dimension' or additively separating the expression.", ): n_to_pwl.apply_to( m, num_points=3, domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, - max_dimension=4 + max_dimension=4, ) def test_do_not_additively_decompose_below_min_dimension(self): m = ConcreteModel() m.x = Var([0, 1, 2, 3, 4], bounds=(-4, 5)) m.c = Constraint(expr=m.x[0] * m.x[1] + m.x[3] <= 4) - + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') n_to_pwl.apply_to( m, From 4145590e1b80aeed282e8ab757b348bdb416a6f8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 15:56:21 -0600 Subject: [PATCH 2102/3044] Making a lot of helper functions private --- .../piecewise/transform/nonlinear_to_pwl.py | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 30797b5318e..8b73ec00678 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -90,7 +90,7 @@ def __init__(self): Block.register_private_data_initializer(_NonlinearToPWLTransformationData) -def get_random_point_grid(bounds, n, func, config, seed=42): +def _get_random_point_grid(bounds, n, func, config, seed=42): # Generate randomized grid of points linspaces = [] for lb, ub in bounds: @@ -99,7 +99,7 @@ def get_random_point_grid(bounds, n, func, config, seed=42): return list(itertools.product(*linspaces)) -def get_uniform_point_grid(bounds, n, func, config): +def _get_uniform_point_grid(bounds, n, func, config): # Generate non-randomized grid of points linspaces = [] for lb, ub in bounds: @@ -112,17 +112,17 @@ def get_uniform_point_grid(bounds, n, func, config): return list(itertools.product(*linspaces)) -def get_points_lmt_random_sample(bounds, n, func, config, seed=42): - points = get_random_point_grid(bounds, n, func, config, seed=seed) - return get_points_lmt(points, bounds, func, config, seed) +def _get_points_lmt_random_sample(bounds, n, func, config, seed=42): + points = _get_random_point_grid(bounds, n, func, config, seed=seed) + return _get_points_lmt(points, bounds, func, config, seed) -def get_points_lmt_uniform_sample(bounds, n, func, config, seed=42): - points = get_uniform_point_grid(bounds, n, func, config) - return get_points_lmt(points, bounds, func, config, seed) +def _get_points_lmt_uniform_sample(bounds, n, func, config, seed=42): + points = _get_uniform_point_grid(bounds, n, func, config) + return _get_points_lmt(points, bounds, func, config, seed) -def get_points_lmt(points, bounds, func, config, seed): +def _get_points_lmt(points, bounds, func, config, seed): x_list = np.array(points) y_list = [] @@ -142,24 +142,24 @@ def get_points_lmt(points, bounds, func, config, seed): regr.fit(x_list, y_list) # ESJ TODO: we actually only needs leaves from here... - leaves, splits, thresholds = parse_linear_tree_regressor(regr, bounds) + leaves, splits, thresholds = _parse_linear_tree_regressor(regr, bounds) # This was originally part of the LMT_Model_component and used to calculate # avg_leaves for the output data. TODO: get this back # self.total_leaves += len(leaves) # bound_point_list = lmt.generate_bound(leaves) - bound_point_list = generate_bound_points(leaves, bounds) + bound_point_list = _generate_bound_points(leaves, bounds) # duct tape to fix possible issues from unknown bugs. TODO should this go # here? return bound_point_list _partition_method_dispatcher = { - DomainPartitioningMethod.RANDOM_GRID: get_random_point_grid, - DomainPartitioningMethod.UNIFORM_GRID: get_uniform_point_grid, - DomainPartitioningMethod.LINEAR_MODEL_TREE_UNIFORM: get_points_lmt_uniform_sample, - DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM: get_points_lmt_random_sample, + DomainPartitioningMethod.RANDOM_GRID: _get_random_point_grid, + DomainPartitioningMethod.UNIFORM_GRID: _get_uniform_point_grid, + DomainPartitioningMethod.LINEAR_MODEL_TREE_UNIFORM: _get_points_lmt_uniform_sample, + DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM: _get_points_lmt_random_sample, } @@ -194,7 +194,7 @@ def _get_pwl_function_approximation(func, config, bounds): # Given a leaves dict (as generated by parse_tree) and a list of tuples # representing variable bounds, generate the set of vertices separating each # subset of the domain -def generate_bound_points(leaves, bounds): +def _generate_bound_points(leaves, bounds): bound_points = [] for leaf in leaves.values(): lower_corner_list = [] @@ -226,7 +226,7 @@ def generate_bound_points(leaves, bounds): # Parse a LinearTreeRegressor and identify features such as bounds, slope, and # intercept for leaves. Return some dicts. -def parse_linear_tree_regressor(linear_tree_regressor, bounds): +def _parse_linear_tree_regressor(linear_tree_regressor, bounds): leaves = linear_tree_regressor.summary(only_leaves=True) splits = linear_tree_regressor.summary() @@ -248,12 +248,14 @@ def parse_linear_tree_regressor(linear_tree_regressor, bounds): node['left_leaves'].append(left_child_node) else: # traverse its left node by calling function to find all the # leaves from its left node - node['left_leaves'] = find_leaves(splits, leaves, splits[left_child_node]) + node['left_leaves'] = _find_leaves(splits, leaves, splits[left_child_node]) if right_child_node in leaves: # if right child is a leaf node node['right_leaves'].append(right_child_node) else: # traverse its right node by calling function to find all the # leaves from its right node - node['right_leaves'] = find_leaves(splits, leaves, splits[right_child_node]) + node['right_leaves'] = _find_leaves( + splits, leaves, splits[right_child_node] + ) # For each feature in each leaf, initialize lower and upper bounds to None for th in features: @@ -267,7 +269,7 @@ def parse_linear_tree_regressor(linear_tree_regressor, bounds): for leaf in splits[split]['right_leaves']: leaves[leaf]['bounds'][var][0] = splits[split]['th'] - leaves_new = reassign_none_bounds(leaves, bounds) + leaves_new = _reassign_none_bounds(leaves, bounds) splitting_thresholds = {} for split in splits: var = splits[split]['col'] @@ -299,7 +301,7 @@ def _additively_decompose_expr(input_expr, min_dimension): # Populate the "None" bounds with the bounding box bounds for a leaves-dict-tree # amalgamation. -def reassign_none_bounds(leaves, input_bounds): +def _reassign_none_bounds(leaves, input_bounds): L = np.array(list(leaves.keys())) features = np.arange(0, len(leaves[L[0]]['slope'])) @@ -312,7 +314,7 @@ def reassign_none_bounds(leaves, input_bounds): return leaves -def find_leaves(splits, leaves, input_node): +def _find_leaves(splits, leaves, input_node): root_node = input_node leaves_list = [] queue = [root_node] From 9e56e1dfa63c9e4248dc82079a1ae9a8d21af97b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 15:58:10 -0600 Subject: [PATCH 2103/3044] NFC: removing some comments --- pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 8b73ec00678..8e5dd9d8219 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -106,7 +106,6 @@ def _get_uniform_point_grid(bounds, n, func, config): # Issues happen when exactly using the boundary nudge = (ub - lb) * 1e-4 linspaces.append( - # np.linspace(b[0], b[1], n) np.linspace(lb + nudge, ub - nudge, n) ) return list(itertools.product(*linspaces)) @@ -141,17 +140,9 @@ def _get_points_lmt(points, bounds, func, config, seed): ) regr.fit(x_list, y_list) - # ESJ TODO: we actually only needs leaves from here... leaves, splits, thresholds = _parse_linear_tree_regressor(regr, bounds) - # This was originally part of the LMT_Model_component and used to calculate - # avg_leaves for the output data. TODO: get this back - # self.total_leaves += len(leaves) - - # bound_point_list = lmt.generate_bound(leaves) bound_point_list = _generate_bound_points(leaves, bounds) - # duct tape to fix possible issues from unknown bugs. TODO should this go - # here? return bound_point_list @@ -203,7 +194,6 @@ def _generate_bound_points(leaves, bounds): lower_corner_list.append(var_bound[0]) upper_corner_list.append(var_bound[1]) - # Duct tape to fix issues from unknown bugs for pt in [lower_corner_list, upper_corner_list]: for i in range(len(pt)): # clamp within bounds range From 738a8f8fba874cac313a662927e95d752a3102a9 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 15:58:38 -0600 Subject: [PATCH 2104/3044] Apparently black has an opinion about that --- pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 8e5dd9d8219..5755286eabe 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -105,9 +105,7 @@ def _get_uniform_point_grid(bounds, n, func, config): for lb, ub in bounds: # Issues happen when exactly using the boundary nudge = (ub - lb) * 1e-4 - linspaces.append( - np.linspace(lb + nudge, ub - nudge, n) - ) + linspaces.append(np.linspace(lb + nudge, ub - nudge, n)) return list(itertools.product(*linspaces)) From 92b8ba1beefdf03db846df191927e21f759912e1 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 16:04:19 -0600 Subject: [PATCH 2105/3044] DomainPartitioningMethod imports with the module --- pyomo/contrib/piecewise/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 5fc75dcd091..1bdb7a70676 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -33,7 +33,10 @@ from pyomo.contrib.piecewise.transform.convex_combination import ( ConvexCombinationTransformation, ) -from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import NonlinearToPWL +from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( + DomainPartitioningMethod, + NonlinearToPWL, +) from pyomo.contrib.piecewise.transform.nested_inner_repn import ( NestedInnerRepresentationGDPTransformation, ) From bc4625a424a322390bb33facd512086a47d2945c Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Aug 2024 01:32:10 -0400 Subject: [PATCH 2106/3044] Reformulate all state variable independent equality cons --- .../contrib/pyros/tests/test_preprocessor.py | 114 ++++++++--- pyomo/contrib/pyros/util.py | 183 ++++++++++-------- 2 files changed, 194 insertions(+), 103 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index c5384e8b49f..a226c5c547d 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -61,7 +61,7 @@ declare_objective_expressions, add_decision_rule_constraints, add_decision_rule_variables, - perform_coefficient_matching, + reformulate_state_var_independent_eq_cons, setup_working_model, VariablePartitioning, preprocess_model_data, @@ -1991,14 +1991,15 @@ def test_dr_eqns_form_correct(self): ) -class TestCoefficientMatching(unittest.TestCase): +class TestReformulateStateVarIndependentEqCons(unittest.TestCase): """ - Unit tests for PyROS coefficient matching routine. + Unit tests for routine that reformulates + state variable-independent performance equality constraints. """ def setup_test_model_data(self): """ - Set up simple test model for coefficient matching - tests. + Set up simple test model for testing the reformulation + routine. """ model_data = Bunch() model_data.working_model = working_model = ConcreteModel() @@ -2025,6 +2026,7 @@ def setup_test_model_data(self): working_model.effective_first_stage_equality_cons = [] working_model.effective_performance_equality_cons = [m.eq_con, m.eq_con_2] + working_model.effective_performance_inequality_cons = [m.con] working_model.all_variables = [m.x1, m.x2] ep = working_model.effective_var_partitioning = Bunch() @@ -2057,7 +2059,9 @@ def test_coefficient_matching_correct_constraints_added(self): ep.first_stage_variables ) - robust_infeasible = perform_coefficient_matching(model_data, config) + robust_infeasible = reformulate_state_var_independent_eq_cons( + model_data, config + ) self.assertFalse( robust_infeasible, @@ -2098,17 +2102,18 @@ def test_coefficient_matching_correct_constraints_added(self): list(model_data.working_model.coefficient_matching_conlist.values()), ) - def test_coefficient_matching_nonlinear(self): + def test_reformulate_nonlinear_state_var_independent_eq_con(self): """ - Test coefficient matching raises exception in event - of encountering unsupported nonlinearities. + Test routine appropriately performs coefficient matching + of polynomial-like constraints, + and recasting of nonlinear constraints to opposing equalities. """ model_data = self.setup_test_model_data() config = Bunch() config.decision_rule_order = 1 config.progress_logger = logging.getLogger( - self.test_coefficient_matching_nonlinear.__name__ + self.test_reformulate_nonlinear_state_var_independent_eq_con.__name__ ) config.progress_logger.setLevel(logging.DEBUG) @@ -2121,6 +2126,7 @@ def test_coefficient_matching_nonlinear(self): + list(model_data.working_model.decision_rule_var_0.values()) ) + wm = model_data.working_model m = model_data.working_model.user_model # we want only one of the constraints to trigger the error @@ -2128,7 +2134,9 @@ def test_coefficient_matching_nonlinear(self): m.eq_con_2.set_value(m.u * (m.x1 - 1) == 0) with LoggingIntercept(level=logging.DEBUG) as LOG: - robust_infeasible = perform_coefficient_matching(model_data, config) + robust_infeasible = reformulate_state_var_independent_eq_cons( + model_data, config + ) err_msg = LOG.getvalue() self.assertRegex( @@ -2148,19 +2156,56 @@ def test_coefficient_matching_nonlinear(self): # check constraint partitioning updated as expected self.assertEqual( - model_data.working_model.effective_performance_equality_cons, - [model_data.working_model.user_model.eq_con], + wm.effective_performance_equality_cons, + [], ) self.assertEqual( - model_data.working_model.effective_first_stage_equality_cons, - [model_data.working_model.coefficient_matching_conlist[1]], + wm.effective_performance_inequality_cons, + [ + m.con, + m.con_eq_con_lower_bound_con, + m.con_eq_con_upper_bound_con, + ], + ) + self.assertEqual( + wm.effective_first_stage_equality_cons, + [wm.coefficient_matching_conlist[1]], ) + + # verify expressions assertExpressionsEqual( self, - model_data.working_model.coefficient_matching_conlist[1].expr, + m.con_eq_con_lower_bound_con.expr, + -( + m.u**2 * (m.x2 - 1) + + m.u * (m.x1**3 + 0.5) + - ((5 * m.u * m.x1) * m.x2) + - (-m.u) * (m.x1 + 2) + ) <= 0.0, + ) + assertExpressionsEqual( + self, + m.con_eq_con_upper_bound_con.expr, + ( + m.u**2 * (m.x2 - 1) + + m.u * (m.x1**3 + 0.5) + - ((5 * m.u * m.x1) * m.x2) + - (-m.u) * (m.x1 + 2) + <= 0.0 + ), + ) + assertExpressionsEqual( + self, + wm.coefficient_matching_conlist[1].expr, (-1) + m.x1 == 0, ) + # ensure the reformulated equality constraint was deactivated, + # and the added inequalities were activated + self.assertFalse(m.eq_con.active) + self.assertTrue(m.con_eq_con_upper_bound_con.active) + self.assertTrue(m.con_eq_con_lower_bound_con.active) + def test_coefficient_matching_robust_infeasible_proof(self): """ Test coefficient matching detects robust infeasibility @@ -2189,7 +2234,9 @@ def test_coefficient_matching_robust_infeasible_proof(self): ) with LoggingIntercept(level=logging.INFO) as LOG: - robust_infeasible = perform_coefficient_matching(model_data, config) + robust_infeasible = reformulate_state_var_independent_eq_cons( + model_data, config + ) self.assertTrue( robust_infeasible, @@ -2486,6 +2533,10 @@ def test_preprocessor_constraint_partitioning_nonstatic_dr( ublk.find_component("con_ineq3_upper_bound_con"), ublk.ineq4, ] + # eq1 gets reformulated to two inequality constraints + # since it is state variable independent and + # too nonlinear for coefficient matching + + ([ublk.con_eq1_lower_bound_con, ublk.con_eq1_upper_bound_con] if dr_order == 2 else []) + ([working_model.epigraph_con] if obj_focus == "worst_case" else []) ), ) @@ -2493,7 +2544,7 @@ def test_preprocessor_constraint_partitioning_nonstatic_dr( ComponentSet(working_model.effective_performance_equality_cons), # eq1 doesn't get reformulated in coefficient matching # when DR order is 2 as the polynomial degree is too high - ComponentSet([ublk.eq4] + ([ublk.eq1] if dr_order == 2 else [])), + ComponentSet([ublk.eq4]), ) # verify the constraints are active @@ -2653,13 +2704,22 @@ def test_preprocessor_coefficient_matching( working_model.decision_rule_vars[0][1] == 0, ) if config.decision_rule_order == 2: - # check the constraint expressions of eq1 and eq4 - self.assertTrue(m.eq1.active) + # eq1 should be deactivated and refomulated to 2 inequalities + self.assertFalse(m.eq1.active) + self.assertTrue(m.con_eq1_lower_bound_con.active) + self.assertTrue(m.con_eq1_upper_bound_con.active) + assertExpressionsEqual( + self, + m.con_eq1_lower_bound_con.expr, + -(m.q * (m.z3 + m.x2)) <= 0.0, + ) assertExpressionsEqual( self, - m.eq1.expr, - m.q * (m.z3 + m.x2) == 0, + m.con_eq1_upper_bound_con.expr, + m.q * (m.z3 + m.x2) <= 0.0, ) + + # check coefficient matching constraint expressions assertExpressionsEqual( self, working_model.coefficient_matching_conlist[1].expr, @@ -2813,15 +2873,15 @@ def test_preprocessor_log_model_statistics_quadratic_dr(self, obj_focus): State variables : 2 (1 adj.) Decision rule variables : 6 Number of uncertain parameters : 1 - Number of constraints : 23 - Equality constraints : 9 + Number of constraints : 24 + Equality constraints : 8 Coefficient matching constraints : 3 Other first-stage equations : 2 - Performance equations : 2 + Performance equations : 1 Decision rule equations : 2 - Inequality constraints : 14 + Inequality constraints : 16 First-stage inequalities : {3 if obj_focus == 'nominal' else 2} - Performance inequalities : {11 if obj_focus == 'nominal' else 12} + Performance inequalities : {13 if obj_focus == 'nominal' else 14} """ ) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index cc5d5e1b3bd..c93c17e8b4a 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -2136,16 +2136,25 @@ def check_time_limit_reached(timing_data, config): ) -def perform_coefficient_matching(model_data, config): - """ - Perform coefficient matching reformulation - of some performance equality constraints. - - Every performance equality constraint that is independent - of the state variables can potentially be simplified to a - set of first-stage equality constraints. - - In some cases, robust infeasibility can be detected. +def reformulate_state_var_independent_eq_cons(model_data, config): + """ + Reformulate performance equality constraints that are + independent of the state variables. + + The state variable-independent performance equality + constraints that can be rewritten as polynomials + in terms of the uncertain parameters + are reformulated to first-stage equalities + through matching of the polynomial coefficients. + Hence, this reformulation technique is referred to as + coefficient matching. + In some cases, matching of the coefficients may lead to + a certificate of robust infeasibility. + + All other state variable-independent performance equality + constraints are recast to pairs of opposing performance inequality + constraints, as they would otherwise over-constrain the uncertain + parameters in the separation subproblems. Parameters ---------- @@ -2256,78 +2265,95 @@ def perform_coefficient_matching(model_data, config): f"(decision_rule_order={config.decision_rule_order}). " "We are unable to write a coefficent matching reformulation " "of this constraint." + "Recasting to two inequality constraints." ) - # nothing we can do to reformulate this constraint, - # so move on - continue - - polynomial_repn_coeffs = ( - [expr_repn.constant] - + list(expr_repn.linear_coefs) - + list(expr_repn.quadratic_coefs) - ) - for coef_expr in polynomial_repn_coeffs: - simplified_coef_expr = generate_standard_repn( - expr=coef_expr, - compute_values=True, - ).to_expression() - - # for robust satisfaction of the original equality - # constraint, all polynomial coefficients must be - # equal to zero. so for each coefficient, - # we either check for trivial robust - # feasibility/infeasibility, or add a constraint - # restricting the coefficient expression to value 0 - if isinstance(simplified_coef_expr, tuple(native_types)): - # coefficient is a constant; - # check value to determine - # trivial feasibility/infeasibility - robust_infeasible = not math.isclose( - a=simplified_coef_expr, - b=0, - rel_tol=COEFF_MATCH_REL_TOL, - abs_tol=COEFF_MATCH_ABS_TOL, + # keeping this constraint as an equality is not appropriate, + # as it effectively constrains the uncertain parameters + # in the separation problems, since the effective DOF + # variables and DR variables are fixed. + # hence, we reformulate to inequalities + con_name = con.getname( + relative_to=working_model.user_model, + fully_qualified=True, + ) + for bound_type in ["lower", "upper"]: + std_con_expr = create_bound_constraint_expr( + expr=con.body, bound=con.upper, bound_type=bound_type ) - if robust_infeasible: - config.progress_logger.info( - "PyROS has determined that the model is " - "robust infeasible. " - "One reason for this is that " - f"the equality constraint {con.name!r} " - "cannot be satisfied against all realizations " - "of uncertainty, " - "given the current partitioning into " - "first-stage, second-stage, and state variables. " - "Consider editing this constraint to reference some " - "(additional) second-stage and/or state variable(s)." + new_con = Constraint(expr=std_con_expr) + working_model.user_model.add_component( + f"con_{con_name}_{bound_type}_bound_con", + new_con, + ) + working_model.effective_performance_inequality_cons.append(new_con) + else: + polynomial_repn_coeffs = ( + [expr_repn.constant] + + list(expr_repn.linear_coefs) + + list(expr_repn.quadratic_coefs) + ) + for coef_expr in polynomial_repn_coeffs: + simplified_coef_expr = generate_standard_repn( + expr=coef_expr, + compute_values=True, + ).to_expression() + + # for robust satisfaction of the original equality + # constraint, all polynomial coefficients must be + # equal to zero. so for each coefficient, + # we either check for trivial robust + # feasibility/infeasibility, or add a constraint + # restricting the coefficient expression to value 0 + if isinstance(simplified_coef_expr, tuple(native_types)): + # coefficient is a constant; + # check value to determine + # trivial feasibility/infeasibility + robust_infeasible = not math.isclose( + a=simplified_coef_expr, + b=0, + rel_tol=COEFF_MATCH_REL_TOL, + abs_tol=COEFF_MATCH_ABS_TOL, ) + if robust_infeasible: + config.progress_logger.info( + "PyROS has determined that the model is " + "robust infeasible. " + "One reason for this is that " + f"the equality constraint {con.name!r} " + "cannot be satisfied against all realizations " + "of uncertainty, " + "given the current partitioning into " + "first-stage, second-stage, and state variables. " + "Consider editing this constraint to reference some " + "(additional) second-stage and/or state variable(s)." + ) + + # robust infeasibility found; + # that is sufficient for termination of PyROS. + return robust_infeasible - # robust infeasibility found; - # that is sufficient for termination of PyROS. - return robust_infeasible - - else: - # coefficient is dependent on model first-stage - # and DR variables. add matching constraint - coeff_matching_conlist.add(simplified_coef_expr == 0) - - # matching constraint depends on nonadjustable - # variables only, so it is first-stage - last_idx = coeff_matching_conlist.index_set().last() - working_model.effective_first_stage_equality_cons.append( - coeff_matching_conlist[last_idx] - ) + else: + # coefficient is dependent on model first-stage + # and DR variables. add matching constraint + coeff_matching_conlist.add(simplified_coef_expr == 0) + + # matching constraint depends on nonadjustable + # variables only, so it is first-stage + last_idx = coeff_matching_conlist.index_set().last() + working_model.effective_first_stage_equality_cons.append( + coeff_matching_conlist[last_idx] + ) - config.progress_logger.debug( - f"Derived from constraint {con.name!r} a coefficient " - "matching constraint with expression: \n " - f"{coeff_matching_conlist[last_idx].expr}." - ) + config.progress_logger.info( + f"Derived from constraint {con.name!r} a coefficient " + "matching constraint with expression: \n " + f"{coeff_matching_conlist[last_idx].expr}." + ) # constraint has been reformulated out of the model, - # i.e., coefficients have all been matched or found - # to yield trivial satisfaction of the constraint + # either by coefficient matching + # or by casting to two inequalities con.deactivate() working_model.effective_performance_equality_cons.remove(con) @@ -2399,8 +2425,13 @@ def preprocess_model_data(model_data, config, user_var_partitioning): + model_data.working_model.effective_var_partitioning.state_variables ) - config.progress_logger.debug("Performing coefficient matching reformulation...") - robust_infeasible = perform_coefficient_matching(model_data, config) + config.progress_logger.debug( + "Reformulating state variable-independent performance equality constraints..." + ) + robust_infeasible = reformulate_state_var_independent_eq_cons( + model_data, + config, + ) return robust_infeasible From f4650e14dceaf66dc3300ef0b22c4af3f842ecf9 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Aug 2024 02:00:51 -0400 Subject: [PATCH 2107/3044] Add normalization of DR polishing variables --- pyomo/contrib/pyros/master_problem_methods.py | 16 ++++++++++++---- pyomo/contrib/pyros/tests/test_master.py | 5 +++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index dcfac1aa378..3b338640907 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -487,14 +487,22 @@ def construct_dr_polishing_problem(master_data, config): for term in dr_term_copies ) if all_copy_coeffs_zero: - dr_var_in_term.fix() + # increment static DR variable value + # to maintain feasibility of the initial point + # as much as possible + static_dr_var_in_expr = dr_expr.args[0] + static_dr_var_in_expr.set_value( + value(static_dr_var_in_expr) + value(dr_monomial) + ) + dr_var_in_term.fix(0) # add polishing constraints + scale_factor = 1 / max(1, abs(value(ss_var))) polishing_absolute_value_lb_cons[dr_var_in_term_idx] = ( - -polishing_var - dr_monomial <= 0 + -polishing_var - scale_factor * dr_monomial <= 0 ) polishing_absolute_value_ub_cons[dr_var_in_term_idx] = ( - dr_monomial - polishing_var <= 0 + scale_factor * dr_monomial - polishing_var <= 0 ) # some DR variables may be fixed, @@ -507,7 +515,7 @@ def construct_dr_polishing_problem(master_data, config): polishing_absolute_value_ub_cons[dr_var_in_term_idx].deactivate() # ensure polishing var properly initialized - polishing_var.set_value(abs(value(dr_monomial))) + polishing_var.set_value(abs(value(scale_factor * dr_monomial))) # L1-norm objective # TODO: if dropping nonstatic terms, ensure the diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index ced00f5925f..710bd9c9579 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -415,13 +415,14 @@ def test_construct_dr_polishing_problem_polishing_components(self): self.assertFalse(polishing_model.polishing_abs_val_ub_con_0[1].active) # check initialization of polishing vars + # scaling factor (for normalization) is 1 / 3 self.assertEqual( polishing_model.polishing_vars[0][0].value, - abs(nom_polishing_block.decision_rule_vars[0][0].value), + abs(nom_polishing_block.decision_rule_vars[0][0].value / 3), ) self.assertEqual( polishing_model.polishing_vars[0][1].value, - abs(nom_polishing_block.decision_rule_vars[0][1].value), + abs(nom_polishing_block.decision_rule_vars[0][1].value / 3), ) assertExpressionsEqual( From c3f12da05bf8c3dc894076e74b06a8cb8632bc31 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 6 Aug 2024 06:33:18 -0600 Subject: [PATCH 2108/3044] Resolving PR concerns --- .../alternative_solutions.rst | 6 ++--- .../alternative_solutions/aos_utils.py | 26 +++++++------------ pyomo/contrib/alternative_solutions/balas.py | 17 +++++------- .../contrib/alternative_solutions/lp_enum.py | 16 ++++++------ .../alternative_solutions/lp_enum_solnpool.py | 12 ++++----- pyomo/contrib/alternative_solutions/obbt.py | 18 ++++++------- .../alternative_solutions/shifted_lp.py | 2 +- .../contrib/alternative_solutions/solnpool.py | 2 +- .../alternative_solutions/tests/test_balas.py | 3 ++- .../tests/test_lp_enum.py | 3 ++- .../alternative_solutions/tests/test_obbt.py | 3 ++- .../tests/test_solnpool.py | 4 +-- 12 files changed, 52 insertions(+), 60 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst index 06bde85423d..19951b6a742 100644 --- a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst +++ b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst @@ -8,7 +8,7 @@ more context than this result. For example, * alternative solutions can support an assessment of trade-offs between competing objectives; -* if the optimization formulation may be inaccurate or untrustworthy, then comparisons amongst alternative solutions provides additional insights into the reliability of these model predictions; or +* if the optimization formulation may be inaccurate or untrustworthy, then comparisons amongst alternative solutions provide additional insights into the reliability of these model predictions; or * the user may have unexpressed objectives or constraints, which only are realized in later stages of model analysis. @@ -27,7 +27,7 @@ The following functions are defined in the alternative-solutions library: * ``enumerate_linear_solutions`` - * Finds alternative optimal solutions a (mixed-integer) linear program. + * Finds alternative optimal solutions for a (mixed-integer) linear program. * ``enumerate_linear_solutions_soln_pool`` @@ -45,7 +45,7 @@ The following functions are defined in the alternative-solutions library: Usage Example ------------- -Many of functions in the alternative-solutions library have similar options, so we simply illustrate the ``enumerate_binary_solutions`` function. We define a simple model whose feasible space is a isosceles right triangle. The optimal solutiosn fall along the hypotenuse, where :math:`x + y == 5`. Alternative near-optimal feasible points have integer objective values ranging from 0 to 4. +Many of functions in the alternative-solutions library have similar options, so we simply illustrate the ``enumerate_binary_solutions`` function. We define a simple model whose feasible space is an isosceles right triangle. The optimal solutiosn fall along the hypotenuse, where :math:`x + y == 5`. Alternative near-optimal feasible points have integer objective values ranging from 0 to 4. .. doctest:: diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index 6418931c440..cfc84ce9dc3 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -27,11 +27,7 @@ def get_active_objective(model): assume that there is exactly one active objective. """ - active_objs = [] - for o in model.component_data_objects(pe.Objective, active=True): - objs = o.values() if o.is_indexed() else (o,) - for obj in objs: - active_objs.append(obj) + active_objs = list(model.component_data_objects(pe.Objective, active=True)) assert ( len(active_objs) == 1 ), "Model has {} active objective functions, exactly one is required.".format( @@ -58,10 +54,10 @@ def _add_objective_constraint( assert ( rel_opt_gap is None or rel_opt_gap >= 0.0 - ), "rel_opt_gap must be None of >= 0.0" + ), "rel_opt_gap must be None or >= 0.0" assert ( abs_opt_gap is None or abs_opt_gap >= 0.0 - ), "abs_opt_gap must be None of >= 0.0" + ), "abs_opt_gap must be None or >= 0.0" objective_constraints = [] @@ -114,20 +110,16 @@ def _set_numpy_rng(seed): rng = numpy.random.default_rng(seed) -def _get_random_direction(num_dimensions): +def _get_random_direction(num_dimensions, iterations=1000, min_norm=1e-4): """ Get a unit vector of dimension num_dimensions by sampling from and normalizing a standard multivariate Gaussian distribution. """ - iterations = 1000 - min_norm = 1e-4 - idx = 0 - while idx < iterations: + for idx in range(iterations): samples = rng.normal(size=num_dimensions) samples_norm = norm(samples) if samples_norm > min_norm: return samples / samples_norm - idx += 1 # pragma: no cover raise Exception( # pragma: no cover ( "Generated {} sequential Gaussian draws with a norm of " @@ -160,7 +152,7 @@ def _filter_model_variables( def get_model_variables( model, - components="all", + components=None, include_continuous=True, include_binary=True, include_integer=True, @@ -175,8 +167,8 @@ def get_model_variables( ---------- model : ConcreteModel A concrete Pyomo model. - components: 'all' or a collection Pyomo components - The components from which variables should be collected. 'all' + components: None or a collection of Pyomo components + The components from which variables should be collected. None indicates that all variables will be included. Alternatively, a collection of Pyomo Blocks, Constraints, or Variables (indexed or non-indexed) from which variables will be gathered can be provided. @@ -203,7 +195,7 @@ def get_model_variables( component_list = (pe.Objective, pe.Constraint) variable_set = ComponentSet() - if components == "all": + if components == None: var_generator = vfe.get_vars_from_components( model, component_list, include_fixed=include_fixed ) diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index dbcc155cbbc..0bc2a3a995e 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -19,7 +19,7 @@ def enumerate_binary_solutions( model, *, num_solutions=10, - variables="all", + variables=None, rel_opt_gap=None, abs_opt_gap=None, search_mode="optimal", @@ -39,8 +39,8 @@ def enumerate_binary_solutions( A concrete Pyomo model num_solutions : int The maximum number of solutions to generate. - variables: 'all' or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. 'all' indicates + variables: None or a collection of Pyomo _GeneralVarData variables + The variables for which bounds will be generated. None indicates that all variables will be included. Alternatively, a collection of _GenereralVarData variables can be provided. rel_opt_gap : float or None @@ -86,10 +86,9 @@ def enumerate_binary_solutions( if seed is not None: aos_utils._set_numpy_rng(seed) - if variables == "all": - binary_variables = aos_utils.get_model_variables( - model, "all", include_continuous=False, include_integer=False - ) + all_variables = aos_utils.get_model_variables(model, include_fixed=True) + if variables == None: + binary_variables = [var for var in all_variables if var.is_binary() and not var.is_fixed()] else: binary_variables = ComponentSet() non_binary_variables = [] @@ -108,7 +107,6 @@ def enumerate_binary_solutions( ) print(", ".join(non_binary_variables)) - all_variables = aos_utils.get_model_variables(model, "all", include_fixed=True) orig_objective = aos_utils.get_active_objective(model) # @@ -149,8 +147,8 @@ def enumerate_binary_solutions( print("Performing initial solve of model.") results = opt.solve(model, tee=tee, load_solutions=False) status = results.solver.status - condition = results.solver.termination_condition if not pe.check_optimal_termination(results): + condition = results.solver.termination_condition raise Exception( ( "No-good cut analysis cannot be applied, " @@ -218,7 +216,6 @@ def enumerate_binary_solutions( if pe.check_optimal_termination(results): model.solutions.load_from(results) orig_obj_value = pe.value(orig_objective) - orig_obj_value = pe.value(orig_objective) if not quiet: # pragma: no cover print( "Iteration {}: objective = {}".format( diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index d6e815b3ec2..fda8799739d 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -23,7 +23,7 @@ def enumerate_linear_solutions( model, *, num_solutions=10, - variables="all", + variables=None, rel_opt_gap=None, abs_opt_gap=None, search_mode="optimal", @@ -50,8 +50,8 @@ def enumerate_linear_solutions( A concrete Pyomo model num_solutions : int The maximum number of solutions to generate. - variables: 'all' or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. 'all' indicates + variables: None or a collection of Pyomo _GeneralVarData variables + The variables for which bounds will be generated. None indicates that all variables will be included. Alternatively, a collection of _GenereralVarData variables can be provided. rel_opt_gap : float or None @@ -90,12 +90,12 @@ def enumerate_linear_solutions( if not quiet: # pragma: no cover print("STARTING LP ENUMERATION ANALYSIS") - # TODO: Make this a parameter + # TODO: Make this a parameter? zero_threshold = 1e-5 # For now keeping things simple # TODO: See if this can be relaxed, but for now just leave as all - assert variables == "all" + assert variables == None assert search_mode in [ "optimal", @@ -109,8 +109,8 @@ def enumerate_linear_solutions( # variables doesn't really matter since we only really care about diversity # in the original problem and not in the slack space (I think) - if variables == "all": - all_variables = aos_utils.get_model_variables(model, "all") + if variables == None: + all_variables = aos_utils.get_model_variables(model) # else: # binary_variables = ComponentSet() # non_binary_variables = [] @@ -123,7 +123,7 @@ def enumerate_linear_solutions( # print(('Warning: The following non-binary variables were included' # 'in the variable list and will be ignored:')) # print(", ".join(non_binary_variables)) - # all_variables = aos_utils.get_model_variables(model, 'all', + # all_variables = aos_utils.get_model_variables(model, None, # include_fixed=True) # TODO: Relax this if possible - Should allow for the mixed-binary case diff --git a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py index cc584dd848d..4d801082f7f 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py @@ -80,7 +80,7 @@ def cut_generator_callback(self, cb_m, cb_opt, cb_where): def enumerate_linear_solutions_soln_pool( model, num_solutions=10, - variables="all", + variables=None, rel_opt_gap=None, abs_opt_gap=None, zero_threshold=1e-5, @@ -97,8 +97,8 @@ def enumerate_linear_solutions_soln_pool( A concrete Pyomo model num_solutions : int The maximum number of solutions to generate. - variables: 'all' or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. 'all' indicates + variables: None or a collection of Pyomo _GeneralVarData variables + The variables for which bounds will be generated. None indicates that all variables will be included. Alternatively, a collection of _GenereralVarData variables can be provided. rel_opt_gap : float or None @@ -132,9 +132,9 @@ def enumerate_linear_solutions_soln_pool( # For now keeping things simple # TODO: See if this can be relaxed, but for now just leave as all - assert variables == "all" - if variables == "all": - all_variables = aos_utils.get_model_variables(model, "all") + assert variables == None + if variables == None: + all_variables = aos_utils.get_model_variables(model) # TODO: Check if problem is continuous or mixed binary diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index ea0ed44c574..182c6e9973a 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -18,7 +18,7 @@ def obbt_analysis( model, *, - variables="all", + variables=None, rel_opt_gap=None, abs_opt_gap=None, refine_discrete_bounds=False, @@ -38,8 +38,8 @@ def obbt_analysis( ---------- model : ConcreteModel A concrete Pyomo model. - variables: 'all' or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. 'all' indicates + variables: None or a collection of Pyomo _GeneralVarData variables + The variables for which bounds will be generated. None indicates that all variables will be included. Alternatively, a collection of _GenereralVarData variables can be provided. rel_opt_gap : float or None @@ -91,7 +91,7 @@ def obbt_analysis( def obbt_analysis_bounds_and_solutions( model, *, - variables="all", + variables=None, rel_opt_gap=None, abs_opt_gap=None, refine_discrete_bounds=False, @@ -111,8 +111,8 @@ def obbt_analysis_bounds_and_solutions( ---------- model : ConcreteModel A concrete Pyomo model. - variables: 'all' or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. 'all' indicates + variables: None or a collection of Pyomo _GeneralVarData variables + The variables for which bounds will be generated. None indicates that all variables will be included. Alternatively, a collection of _GenereralVarData variables can be provided. rel_opt_gap : float or None @@ -156,10 +156,10 @@ def obbt_analysis_bounds_and_solutions( if warmstart: assert ( - variables == "all" + variables == None ), "Cannot restrict variable list when warmstart is specified" - all_variables = aos_utils.get_model_variables(model, "all", include_fixed=False) - if variables == "all": + all_variables = aos_utils.get_model_variables(model, include_fixed=False) + if variables == None: variable_list = all_variables else: variable_list = list(variables) diff --git a/pyomo/contrib/alternative_solutions/shifted_lp.py b/pyomo/contrib/alternative_solutions/shifted_lp.py index 3e3a8a3f3f8..944651c96c5 100644 --- a/pyomo/contrib/alternative_solutions/shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/shifted_lp.py @@ -81,7 +81,7 @@ def get_shifted_linear_model(model, block=None): """ # Gather all variables and confirm the model is bounded - all_vars = aos_utils.get_model_variables(model, "all") + all_vars = aos_utils.get_model_variables(model) new_vars = {} all_vars_new = {} var_map = ComponentMap() diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 97d949f65c2..b18f5c82eee 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -99,7 +99,7 @@ def gurobi_generate_solutions( # Collect solutions # solution_count = opt.get_model_attr("SolCount") - variables = aos_utils.get_model_variables(model, "all", include_fixed=True) + variables = aos_utils.get_model_variables(model, include_fixed=True) solutions = [] for i in range(solution_count): # diff --git a/pyomo/contrib/alternative_solutions/tests/test_balas.py b/pyomo/contrib/alternative_solutions/tests/test_balas.py index 10e2bb8ae65..d706b5d389d 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_balas.py +++ b/pyomo/contrib/alternative_solutions/tests/test_balas.py @@ -42,7 +42,8 @@ def test_ip_feasibility(self, mip_solver): assert len(results) == 1 assert results[0].objective_value == unittest.pytest.approx(5) - def Xtest_no_time(self, mip_solver): + @unittest.skipIf(True, "Ignoring fragile test for solver timeout.") + def test_no_time(self, mip_solver): """ Enumerate solutions for an ip: triangle_ip. diff --git a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py index f8bad28e552..13fb33dd9d1 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py +++ b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py @@ -32,7 +32,8 @@ @unittest.pytest.mark.default class TestLPEnum: - def Xtest_no_time(self, mip_solver): + @unittest.skipIf(True, "Ignoring fragile test for solver timeout.") + def test_no_time(self, mip_solver): """ Check that the correct bounds are found for a discrete problem where more restrictive bounds are implied by the constraints. diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index 91e702a20d9..f4bef580074 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -172,7 +172,8 @@ def test_bound_tightening(self, mip_solver): for var, bounds in all_bounds.items(): assert_array_almost_equal(bounds, m.var_bounds[var]) - def Xtest_no_time(self, mip_solver): + @unittest.skipIf(True, "Ignoring fragile test for solver timeout.") + def test_no_time(self, mip_solver): """ Check that the correct bounds are found for a discrete problem where more restrictive bounds are implied by the constraints. diff --git a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py index 48727cba121..0b9914a86dd 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solnpool.py @@ -129,8 +129,8 @@ def test_mip_abs_feasibility(self): unique_solns_by_obj = [val for val in Counter(objectives).values()] assert_array_almost_equal(unique_solns_by_obj, actual_solns_by_obj) - @unittest.skipIf(not numpy_available, "Numpy not installed") - def Xtest_mip_no_time(self): + @unittest.skipIf(True, "Ignoring fragile test for solver timeout.") + def test_mip_no_time(self): """ Enumerate solutions for a mip: indexed_pentagonal_pyramid_mip. From 15b47bc27fbee615ff56019fce039b1a0a5a1064 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 6 Aug 2024 06:42:38 -0600 Subject: [PATCH 2109/3044] Reformatting --- pyomo/contrib/alternative_solutions/balas.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index 0bc2a3a995e..b28be674cf1 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -88,7 +88,9 @@ def enumerate_binary_solutions( all_variables = aos_utils.get_model_variables(model, include_fixed=True) if variables == None: - binary_variables = [var for var in all_variables if var.is_binary() and not var.is_fixed()] + binary_variables = [ + var for var in all_variables if var.is_binary() and not var.is_fixed() + ] else: binary_variables = ComponentSet() non_binary_variables = [] From 953a39dd0cd354f4d129d84ab3f385d47adcb897 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 6 Aug 2024 09:41:48 -0600 Subject: [PATCH 2110/3044] Skipping tests when numpy not available --- pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index f5f4ebc8009..ed3dfd2129f 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -88,6 +88,7 @@ def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): self.assertEqual(len(nonlinear), 1) self.assertIn(m.cons, nonlinear) + @skipUnless(numpy_available, "Numpy is not available") def test_log_constraint_uniform_grid(self): m = self.make_model() @@ -111,6 +112,7 @@ def test_log_constraint_uniform_grid(self): (x1, x2, x3) = 1.0009, 5.5, 9.9991 self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) + @skipUnless(numpy_available, "Numpy is not available") def test_log_constraint_random_grid(self): m = self.make_model() @@ -137,6 +139,7 @@ def test_log_constraint_random_grid(self): x3 = 9.556428757689245 self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) + @skipUnless(numpy_available, "Numpy is not available") def test_do_not_transform_quadratic_constraint(self): m = self.make_model() m.quad = Constraint(expr=m.x**2 <= 9) @@ -168,6 +171,7 @@ def test_do_not_transform_quadratic_constraint(self): # neither is the linear one self.assertTrue(m.lin.active) + @skipUnless(numpy_available, "Numpy is not available") def test_constraint_target(self): m = self.make_model() m.quad = Constraint(expr=m.x**2 <= 9) @@ -250,6 +254,7 @@ def test_error_for_non_separable_exceeding_max_dimension(self): max_dimension=4, ) + @skipUnless(numpy_available, "Numpy is not available") def test_do_not_additively_decompose_below_min_dimension(self): m = ConcreteModel() m.x = Var([0, 1, 2, 3, 4], bounds=(-4, 5)) @@ -311,6 +316,7 @@ def check_pw_linear_paraboloid(self, m, pwlf, x1, x2, y1, y2): nonlinear = n_to_pwl.get_transformed_nonlinear_objectives(m) self.assertEqual(len(nonlinear), 0) + @skipUnless(numpy_available, "Numpy is not available") def test_paraboloid_objective_uniform_grid(self): m = self.make_paraboloid_model() @@ -337,6 +343,7 @@ def test_paraboloid_objective_uniform_grid(self): self.check_pw_linear_paraboloid(m, pwlf, x1, x2, y1, y2) + @skipUnless(numpy_available, "Numpy is not available") def test_objective_target(self): m = self.make_paraboloid_model() From 68b15a5bafc65b224d6661704f9f9fe6b64e22cf Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 6 Aug 2024 09:43:23 -0600 Subject: [PATCH 2111/3044] HA, she can code... Fixing dumb typos in previous commit --- .../piecewise/tests/test_nonlinear_to_pwl.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index ed3dfd2129f..fec3245edd3 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.common.dependencies import attempt_import, scipy_available +from pyomo.common.dependencies import attempt_import, scipy_available, numpy_available import pyomo.common.unittest as unittest from pyomo.contrib.piecewise import PiecewiseLinearFunction from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( @@ -88,7 +88,7 @@ def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): self.assertEqual(len(nonlinear), 1) self.assertIn(m.cons, nonlinear) - @skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(numpy_available, "Numpy is not available") def test_log_constraint_uniform_grid(self): m = self.make_model() @@ -112,7 +112,7 @@ def test_log_constraint_uniform_grid(self): (x1, x2, x3) = 1.0009, 5.5, 9.9991 self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) - @skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(numpy_available, "Numpy is not available") def test_log_constraint_random_grid(self): m = self.make_model() @@ -139,7 +139,7 @@ def test_log_constraint_random_grid(self): x3 = 9.556428757689245 self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) - @skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(numpy_available, "Numpy is not available") def test_do_not_transform_quadratic_constraint(self): m = self.make_model() m.quad = Constraint(expr=m.x**2 <= 9) @@ -171,7 +171,7 @@ def test_do_not_transform_quadratic_constraint(self): # neither is the linear one self.assertTrue(m.lin.active) - @skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(numpy_available, "Numpy is not available") def test_constraint_target(self): m = self.make_model() m.quad = Constraint(expr=m.x**2 <= 9) @@ -254,7 +254,7 @@ def test_error_for_non_separable_exceeding_max_dimension(self): max_dimension=4, ) - @skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(numpy_available, "Numpy is not available") def test_do_not_additively_decompose_below_min_dimension(self): m = ConcreteModel() m.x = Var([0, 1, 2, 3, 4], bounds=(-4, 5)) @@ -316,7 +316,7 @@ def check_pw_linear_paraboloid(self, m, pwlf, x1, x2, y1, y2): nonlinear = n_to_pwl.get_transformed_nonlinear_objectives(m) self.assertEqual(len(nonlinear), 0) - @skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(numpy_available, "Numpy is not available") def test_paraboloid_objective_uniform_grid(self): m = self.make_paraboloid_model() @@ -343,7 +343,7 @@ def test_paraboloid_objective_uniform_grid(self): self.check_pw_linear_paraboloid(m, pwlf, x1, x2, y1, y2) - @skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(numpy_available, "Numpy is not available") def test_objective_target(self): m = self.make_paraboloid_model() From dabafbf4a08ab5f72157dd1488b39c81a5a0b0c1 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 6 Aug 2024 09:46:06 -0600 Subject: [PATCH 2112/3044] Skipping 3 more tests if scipy is not available --- pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index fec3245edd3..67d0bb9f098 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -255,6 +255,7 @@ def test_error_for_non_separable_exceeding_max_dimension(self): ) @unittest.skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(scipy_available, "Scipy is not available") def test_do_not_additively_decompose_below_min_dimension(self): m = ConcreteModel() m.x = Var([0, 1, 2, 3, 4], bounds=(-4, 5)) @@ -317,6 +318,7 @@ def check_pw_linear_paraboloid(self, m, pwlf, x1, x2, y1, y2): self.assertEqual(len(nonlinear), 0) @unittest.skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(scipy_available, "Scipy is not available") def test_paraboloid_objective_uniform_grid(self): m = self.make_paraboloid_model() @@ -344,6 +346,7 @@ def test_paraboloid_objective_uniform_grid(self): self.check_pw_linear_paraboloid(m, pwlf, x1, x2, y1, y2) @unittest.skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(scipy_available, "Scipy is not available") def test_objective_target(self): m = self.make_paraboloid_model() From d845e080c1e4ff62c30a0d208d9360eb389a604d Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Aug 2024 12:43:46 -0400 Subject: [PATCH 2113/3044] Make documentation of `FactorModelSet.psi_mat` more informative --- pyomo/contrib/pyros/uncertainty_sets.py | 30 ++++++++++++------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index d3c98197c4e..4e3c0c05df5 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -1801,6 +1801,8 @@ class FactorModelSet(UncertaintySet): parameter. Each column is associated with a separate factor. Number of columns `F` of `psi_mat` should be equal to `number_of_factors`. + Since `psi_mat` is expected to be full column rank, + we require `F <= N`. beta : numeric type Real value between 0 and 1 specifying the fraction of the independent factors that can simultaneously attain @@ -1911,6 +1913,8 @@ def psi_mat(self): column rank matrix for which each entry indicates how strongly the factor corresponding to the entry's column is related to the uncertain parameter corresponding to the entry's row. + Since `psi_mat` is expected to be full column rank, + we require `F <= N`. """ return self._psi_mat @@ -1942,7 +1946,9 @@ def psi_mat(self, val): if not is_full_column_rank: raise ValueError( "Attribute 'psi_mat' should be full column rank. " - f"(Got a matrix of shape {psi_mat_arr.shape} and rank {psi_mat_rank}.)" + f"(Got a matrix of shape {psi_mat_arr.shape} and rank {psi_mat_rank}.) " + "Ensure `psi_mat` does not have more columns than rows, " + "and the columns of `psi_mat` are linearly independent." ) self._psi_mat = psi_mat_arr @@ -2089,20 +2095,14 @@ def compute_auxiliary_uncertain_param_vals(self, point, solver=None): ) point_arr = np.array(point) - psi_mat_rank = np.linalg.matrix_rank(self.psi_mat) - is_psi_full_column_rank = psi_mat_rank == self.number_of_factors - if is_psi_full_column_rank: - # pseudoinverse uniquely determines the auxiliary values - return np.linalg.pinv(self.psi_mat) @ (point_arr - self.origin) - else: - # guard against possible changes to individual entries, - # rows, or columns after `psi_mat` setter invoked - # that may render `psi_mat` rank deficient - raise ValueError( - "Factor loading matrix `psi_mat` must be full column rank. " - f"(There are {self.number_of_factors} factors/columns, but" - f"the matrix is of rank {psi_mat_rank}.)" - ) + # protect against cases where + # `psi_mat` was recently modified entrywise + # to a matrix that is not full column rank + self.psi_mat = self.psi_mat + + # since `psi_mat` is full column rank, + # the pseudoinverse uniquely determines the auxiliary values + return np.linalg.pinv(self.psi_mat) @ (point_arr - self.origin) def point_in_set(self, point): """ From 4190f1885c7ddab6a1cd5f289eb00815d768b780 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 6 Aug 2024 13:36:21 -0400 Subject: [PATCH 2114/3044] Undo DR polishing variable normalization --- pyomo/contrib/pyros/master_problem_methods.py | 7 +++---- pyomo/contrib/pyros/tests/test_master.py | 5 ++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 3b338640907..15e41c279fc 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -497,12 +497,11 @@ def construct_dr_polishing_problem(master_data, config): dr_var_in_term.fix(0) # add polishing constraints - scale_factor = 1 / max(1, abs(value(ss_var))) polishing_absolute_value_lb_cons[dr_var_in_term_idx] = ( - -polishing_var - scale_factor * dr_monomial <= 0 + -polishing_var - dr_monomial <= 0 ) polishing_absolute_value_ub_cons[dr_var_in_term_idx] = ( - scale_factor * dr_monomial - polishing_var <= 0 + dr_monomial - polishing_var <= 0 ) # some DR variables may be fixed, @@ -515,7 +514,7 @@ def construct_dr_polishing_problem(master_data, config): polishing_absolute_value_ub_cons[dr_var_in_term_idx].deactivate() # ensure polishing var properly initialized - polishing_var.set_value(abs(value(scale_factor * dr_monomial))) + polishing_var.set_value(abs(value(dr_monomial))) # L1-norm objective # TODO: if dropping nonstatic terms, ensure the diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index 710bd9c9579..ced00f5925f 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -415,14 +415,13 @@ def test_construct_dr_polishing_problem_polishing_components(self): self.assertFalse(polishing_model.polishing_abs_val_ub_con_0[1].active) # check initialization of polishing vars - # scaling factor (for normalization) is 1 / 3 self.assertEqual( polishing_model.polishing_vars[0][0].value, - abs(nom_polishing_block.decision_rule_vars[0][0].value / 3), + abs(nom_polishing_block.decision_rule_vars[0][0].value), ) self.assertEqual( polishing_model.polishing_vars[0][1].value, - abs(nom_polishing_block.decision_rule_vars[0][1].value / 3), + abs(nom_polishing_block.decision_rule_vars[0][1].value), ) assertExpressionsEqual( From 017258dd593817f96ee9da4207e379807e5c8e52 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 12:33:02 -0600 Subject: [PATCH 2115/3044] Restore 'options' as an alias of the new config.solver_options --- pyomo/contrib/solver/base.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 98bf3836004..ac07bda4e67 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -353,15 +353,20 @@ def __init__(self, **kwargs): raise NotImplementedError('Still working on this') # There is no reason for a user to be trying to mix both old # and new options. That is silly. So we will yell at them. - self.options = kwargs.pop('options', None) + _options = kwargs.pop('options', None) if 'solver_options' in kwargs: - if self.options is not None: + if _options is not None: raise ValueError( "Both 'options' and 'solver_options' were requested. " "Please use one or the other, not both." ) - self.options = kwargs.pop('solver_options') + _options = kwargs.pop('solver_options') + if _options is not None: + kwargs['solver_options'] = _options super().__init__(**kwargs) + # Make the legacy 'options' attribute an alias of the new + # config.solver_options + self.options = self.config.solver_options # # Support "with" statements From 61bb290ee5a2b1c9600d9abc7b381202b52dc238 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 13:40:20 -0600 Subject: [PATCH 2116/3044] Update tests to not directly instantiate the LegacySolverWrapper mixin --- pyomo/contrib/solver/tests/unit/test_base.py | 35 ++++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index b52f96ba903..b7937d16af3 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -16,6 +16,10 @@ from pyomo.contrib.solver import base +class _LegacyWrappedSolverBase(base.LegacySolverWrapper, base.SolverBase): + pass + + class TestSolverBase(unittest.TestCase): def test_abstract_member_list(self): expected_list = ['solve', 'available', 'version'] @@ -192,11 +196,13 @@ def test_class_method_list(self): ] self.assertEqual(sorted(expected_list), sorted(method_list)) + @unittest.mock.patch.multiple(_LegacyWrappedSolverBase, __abstractmethods__=set()) def test_context_manager(self): - with base.LegacySolverWrapper() as instance: - with self.assertRaises(AttributeError): - instance.available() + with _LegacyWrappedSolverBase() as instance: + self.assertIsInstance(instance, _LegacyWrappedSolverBase) + self.assertFalse(instance.available(False)) + @unittest.mock.patch.multiple(_LegacyWrappedSolverBase, __abstractmethods__=set()) def test_map_config(self): # Create a fake/empty config structure that can be added to an empty # instance of LegacySolverWrapper @@ -205,7 +211,7 @@ def test_map_config(self): 'solver_options', ConfigDict(implicit=True, description="Options to pass to the solver."), ) - instance = base.LegacySolverWrapper() + instance = _LegacyWrappedSolverBase() instance.config = self.config instance._map_config( True, False, False, 20, True, False, None, None, None, False, None, None @@ -272,20 +278,21 @@ def test_map_config(self): with self.assertRaises(AttributeError): print(instance.config.keepfiles) + @unittest.mock.patch.multiple(_LegacyWrappedSolverBase, __abstractmethods__=set()) def test_solver_options_behavior(self): # options can work in multiple ways (set from instantiation, set # after instantiation, set during solve). # Test case 1: Set at instantiation - solver = base.LegacySolverWrapper(options={'max_iter': 6}) + solver = _LegacyWrappedSolverBase(options={'max_iter': 6}) self.assertEqual(solver.options, {'max_iter': 6}) # Test case 2: Set later - solver = base.LegacySolverWrapper() + solver = _LegacyWrappedSolverBase() solver.options = {'max_iter': 4, 'foo': 'bar'} self.assertEqual(solver.options, {'max_iter': 4, 'foo': 'bar'}) # Test case 3: pass some options to the mapping (aka, 'solve' command) - solver = base.LegacySolverWrapper() + solver = _LegacyWrappedSolverBase() config = ConfigDict(implicit=True) config.declare( 'solver_options', @@ -296,7 +303,7 @@ def test_solver_options_behavior(self): self.assertEqual(solver.config.solver_options, {'max_iter': 4}) # Test case 4: Set at instantiation and override during 'solve' call - solver = base.LegacySolverWrapper(options={'max_iter': 6}) + solver = _LegacyWrappedSolverBase(options={'max_iter': 6}) config = ConfigDict(implicit=True) config.declare( 'solver_options', @@ -309,11 +316,11 @@ def test_solver_options_behavior(self): # solver_options are also supported # Test case 1: set at instantiation - solver = base.LegacySolverWrapper(solver_options={'max_iter': 6}) + solver = _LegacyWrappedSolverBase(solver_options={'max_iter': 6}) self.assertEqual(solver.options, {'max_iter': 6}) # Test case 2: pass some solver_options to the mapping (aka, 'solve' command) - solver = base.LegacySolverWrapper() + solver = _LegacyWrappedSolverBase() config = ConfigDict(implicit=True) config.declare( 'solver_options', @@ -324,7 +331,7 @@ def test_solver_options_behavior(self): self.assertEqual(solver.config.solver_options, {'max_iter': 4}) # Test case 3: Set at instantiation and override during 'solve' call - solver = base.LegacySolverWrapper(solver_options={'max_iter': 6}) + solver = _LegacyWrappedSolverBase(solver_options={'max_iter': 6}) config = ConfigDict(implicit=True) config.declare( 'solver_options', @@ -337,7 +344,7 @@ def test_solver_options_behavior(self): # users can mix... sort of # Test case 1: Initialize with options, solve with solver_options - solver = base.LegacySolverWrapper(options={'max_iter': 6}) + solver = _LegacyWrappedSolverBase(options={'max_iter': 6}) config = ConfigDict(implicit=True) config.declare( 'solver_options', @@ -351,11 +358,11 @@ def test_solver_options_behavior(self): # do we know what to do with it then? # Test case 1: Class instance with self.assertRaises(ValueError): - solver = base.LegacySolverWrapper( + solver = _LegacyWrappedSolverBase( options={'max_iter': 6}, solver_options={'max_iter': 4} ) # Test case 2: Passing to `solve` - solver = base.LegacySolverWrapper() + solver = _LegacyWrappedSolverBase() config = ConfigDict(implicit=True) config.declare( 'solver_options', From 109c0e97352a669293ed97efc127fefe18f6ee66 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 6 Aug 2024 13:52:59 -0600 Subject: [PATCH 2117/3044] Moving the code to generate the hamiltonian paths data to a comment in its file, and wrapping it in a function so that it doesn't get built when pyomo.environ is imported --- ...nerate_ordered_3d_j1_triangulation_data.py | 93 - .../ordered_3d_j1_triangulation_data.py | 6095 +++++++++-------- pyomo/contrib/piecewise/triangulations.py | 3 +- 3 files changed, 3095 insertions(+), 3096 deletions(-) delete mode 100644 pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py diff --git a/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py deleted file mode 100644 index 58c68051cdb..00000000000 --- a/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py +++ /dev/null @@ -1,93 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import networkx as nx -import itertools - -# Get a list of 60 hamiltonian paths used in the 3d version of the ordered J1 -# triangulation, and dump it to stdout. -if __name__ == '__main__': - # Graph of a double cube - sign_vecs = list(itertools.product((-1, 1), repeat=3)) - permutations = itertools.permutations(range(1, 4)) - simplices = list(itertools.product(sign_vecs, permutations)) - - G = nx.Graph() - G.add_nodes_from(simplices) - for s in sign_vecs: - # interior connectivity of cubes - G.add_edges_from( - [ - ((s, (1, 2, 3)), (s, (1, 3, 2))), - ((s, (1, 3, 2)), (s, (3, 1, 2))), - ((s, (3, 1, 2)), (s, (3, 2, 1))), - ((s, (3, 2, 1)), (s, (2, 3, 1))), - ((s, (2, 3, 1)), (s, (2, 1, 3))), - ((s, (2, 1, 3)), (s, (1, 2, 3))), - ] - ) - # connectivity between cubes in double cube - for simplex in simplices: - neighbor_sign = list(simplex[0]) - neighbor_sign[simplex[1][2] - 1] *= -1 - neighbor_simplex = (tuple(neighbor_sign), simplex[1]) - G.add_edge(simplex, neighbor_simplex) - - # Each of these simplices has an outward face in the specified direction; also, - # the +x simplex of one cube is adjacent to the -x simplex of a cube adjacent in - # the x direction, and similarly for the others. - border_simplices = { - # simplices in low-coordinate cube - # -x - ((-1, 0, 0), 1): ((-1, -1, -1), (1, 2, 3)), - ((-1, 0, 0), 2): ((-1, -1, -1), (1, 3, 2)), - # -y - ((0, -1, 0), 1): ((-1, -1, -1), (2, 1, 3)), - ((0, -1, 0), 2): ((-1, -1, -1), (2, 3, 1)), - # -z - ((0, 0, -1), 1): ((-1, -1, -1), (3, 1, 2)), - ((0, 0, -1), 2): ((-1, -1, -1), (3, 2, 1)), - # simplices in one-high-coordinate cubes - # +x - ((1, 0, 0), 1): ((1, -1, -1), (1, 2, 3)), - ((1, 0, 0), 2): ((1, -1, -1), (1, 3, 2)), - # +y - ((0, 1, 0), 1): ((-1, 1, -1), (2, 1, 3)), - ((0, 1, 0), 2): ((-1, 1, -1), (2, 3, 1)), - # +z - ((0, 0, 1), 1): ((-1, -1, 1), (3, 1, 2)), - ((0, 0, 1), 2): ((-1, -1, 1), (3, 2, 1)), - } - - # Need: Hamiltonian paths from each input to some output in each direction - all_needed_hamiltonians = {} - for i, s1 in border_simplices.items(): - for j, s2 in border_simplices.items(): - # I could cut the number of these in half or less via symmetry but I don't care - if i[0] != j[0]: - if (i, (j[0], 1)) in all_needed_hamiltonians.keys() or ( - i, - (j[0], 2), - ) in all_needed_hamiltonians.keys(): - print( - f"skipping search for path from {i} to {j} because we have a path from {i} to {(j[0], 1) if (i, (j[0], 1)) in all_needed_hamiltonians.keys() else (j[0], 2)}" - ) - continue - print(f"searching for path from {i} to {j}") - for path in nx.all_simple_paths(G, s1, s2): - if len(path) == 48: - # it's hamiltonian! - print(f"found hamiltonian path from {i} to {j}") - all_needed_hamiltonians[(i, j)] = path - break - print(f"done looking for paths from {i} to {j}") - print() - print(all_needed_hamiltonians) diff --git a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py index 631b0b3d4ef..edc117df6d7 100644 --- a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py +++ b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py @@ -9,3010 +9,3101 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ + +""" +This code was used to generate the data structure in this file. It should never +need to be run again, but is here for the sake of documentation: + +import networkx as nx +import itertools + +# Get a list of 60 hamiltonian paths used in the 3d version of the ordered J1 +# triangulation, and dump it to stdout. +if __name__ == '__main__': + # Graph of a double cube + sign_vecs = list(itertools.product((-1, 1), repeat=3)) + permutations = itertools.permutations(range(1, 4)) + simplices = list(itertools.product(sign_vecs, permutations)) + + G = nx.Graph() + G.add_nodes_from(simplices) + for s in sign_vecs: + # interior connectivity of cubes + G.add_edges_from( + [ + ((s, (1, 2, 3)), (s, (1, 3, 2))), + ((s, (1, 3, 2)), (s, (3, 1, 2))), + ((s, (3, 1, 2)), (s, (3, 2, 1))), + ((s, (3, 2, 1)), (s, (2, 3, 1))), + ((s, (2, 3, 1)), (s, (2, 1, 3))), + ((s, (2, 1, 3)), (s, (1, 2, 3))), + ] + ) + # connectivity between cubes in double cube + for simplex in simplices: + neighbor_sign = list(simplex[0]) + neighbor_sign[simplex[1][2] - 1] *= -1 + neighbor_simplex = (tuple(neighbor_sign), simplex[1]) + G.add_edge(simplex, neighbor_simplex) + + # Each of these simplices has an outward face in the specified direction; also, + # the +x simplex of one cube is adjacent to the -x simplex of a cube adjacent in + # the x direction, and similarly for the others. + border_simplices = { + # simplices in low-coordinate cube + # -x + ((-1, 0, 0), 1): ((-1, -1, -1), (1, 2, 3)), + ((-1, 0, 0), 2): ((-1, -1, -1), (1, 3, 2)), + # -y + ((0, -1, 0), 1): ((-1, -1, -1), (2, 1, 3)), + ((0, -1, 0), 2): ((-1, -1, -1), (2, 3, 1)), + # -z + ((0, 0, -1), 1): ((-1, -1, -1), (3, 1, 2)), + ((0, 0, -1), 2): ((-1, -1, -1), (3, 2, 1)), + # simplices in one-high-coordinate cubes + # +x + ((1, 0, 0), 1): ((1, -1, -1), (1, 2, 3)), + ((1, 0, 0), 2): ((1, -1, -1), (1, 3, 2)), + # +y + ((0, 1, 0), 1): ((-1, 1, -1), (2, 1, 3)), + ((0, 1, 0), 2): ((-1, 1, -1), (2, 3, 1)), + # +z + ((0, 0, 1), 1): ((-1, -1, 1), (3, 1, 2)), + ((0, 0, 1), 2): ((-1, -1, 1), (3, 2, 1)), + } + + # Need: Hamiltonian paths from each input to some output in each direction + all_needed_hamiltonians = {} + for i, s1 in border_simplices.items(): + for j, s2 in border_simplices.items(): + # I could cut the number of these in half or less via symmetry but I don't care + if i[0] != j[0]: + if (i, (j[0], 1)) in all_needed_hamiltonians.keys() or ( + i, + (j[0], 2), + ) in all_needed_hamiltonians.keys(): + print( + f"skipping search for path from {i} to {j} because we have a path from {i} to {(j[0], 1) if (i, (j[0], 1)) in all_needed_hamiltonians.keys() else (j[0], 2)}" + ) + continue + print(f"searching for path from {i} to {j}") + for path in nx.all_simple_paths(G, s1, s2): + if len(path) == 48: + # it's hamiltonian! + print(f"found hamiltonian path from {i} to {j}") + all_needed_hamiltonians[(i, j)] = path + break + print(f"done looking for paths from {i} to {j}") + print() + print(all_needed_hamiltonians) + +""" + # This file was generated using generate_ordered_3d_j1_triangulation_data.py # Data format: Keys are a pair of simplices specified as the direction they are facing, # as a standard unit vector or negative of one, and a tag, 1 or 2, disambiguating which # of the two simplices considered is used. Values are a list of simplices given as # (sign_vector, permutation) pairs. -hamiltonian_paths = { - (((-1, 0, 0), 1), ((0, -1, 0), 1)): [ - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, -1), (2, 1, 3)), - ], - (((-1, 0, 0), 1), ((0, 0, -1), 2)): [ - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 2, 1)), - ], - (((-1, 0, 0), 1), ((1, 0, 0), 1)): [ - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ], - (((-1, 0, 0), 1), ((0, 1, 0), 2)): [ - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ], - (((-1, 0, 0), 1), ((0, 0, 1), 1)): [ - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 1, 2)), - ], - (((-1, 0, 0), 2), ((0, -1, 0), 2)): [ - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 3, 1)), - ], - (((-1, 0, 0), 2), ((0, 0, -1), 1)): [ - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 1, 2)), - ], - (((-1, 0, 0), 2), ((1, 0, 0), 2)): [ - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ], - (((-1, 0, 0), 2), ((0, 1, 0), 1)): [ - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ], - (((-1, 0, 0), 2), ((0, 0, 1), 2)): [ - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 2, 1)), - ], - (((0, -1, 0), 1), ((-1, 0, 0), 1)): [ - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, -1), (1, 2, 3)), - ], - (((0, -1, 0), 1), ((0, 0, -1), 1)): [ - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 1, 2)), - ], - (((0, -1, 0), 1), ((1, 0, 0), 2)): [ - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 3, 2)), - ], - (((0, -1, 0), 1), ((0, 1, 0), 1)): [ - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ], - (((0, -1, 0), 1), ((0, 0, 1), 2)): [ - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 2, 1)), - ], - (((0, -1, 0), 2), ((-1, 0, 0), 2)): [ - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 3, 2)), - ], - (((0, -1, 0), 2), ((0, 0, -1), 2)): [ - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 2, 1)), - ], - (((0, -1, 0), 2), ((1, 0, 0), 1)): [ - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ], - (((0, -1, 0), 2), ((0, 1, 0), 2)): [ - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ], - (((0, -1, 0), 2), ((0, 0, 1), 1)): [ - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 1, 2)), - ], - (((0, 0, -1), 1), ((-1, 0, 0), 2)): [ - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 3, 2)), - ], - (((0, 0, -1), 1), ((0, -1, 0), 1)): [ - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, -1), (2, 1, 3)), - ], - (((0, 0, -1), 1), ((1, 0, 0), 1)): [ - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, -1), (1, 2, 3)), - ], - (((0, 0, -1), 1), ((0, 1, 0), 2)): [ - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ], - (((0, 0, -1), 1), ((0, 0, 1), 1)): [ - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 1, 2)), - ], - (((0, 0, -1), 2), ((-1, 0, 0), 1)): [ - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, -1), (1, 2, 3)), - ], - (((0, 0, -1), 2), ((0, -1, 0), 2)): [ - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 3, 1)), - ], - (((0, 0, -1), 2), ((1, 0, 0), 2)): [ - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 3, 2)), - ], - (((0, 0, -1), 2), ((0, 1, 0), 1)): [ - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ], - (((0, 0, -1), 2), ((0, 0, 1), 2)): [ - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ], - (((1, 0, 0), 1), ((-1, 0, 0), 1)): [ - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ], - (((1, 0, 0), 1), ((0, -1, 0), 2)): [ - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (2, 3, 1)), - ], - (((1, 0, 0), 1), ((0, 0, -1), 1)): [ - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ], - (((1, 0, 0), 1), ((0, 1, 0), 1)): [ - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ], - (((1, 0, 0), 1), ((0, 0, 1), 2)): [ - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ], - (((1, 0, 0), 2), ((-1, 0, 0), 2)): [ - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ], - (((1, 0, 0), 2), ((0, -1, 0), 1)): [ - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ], - (((1, 0, 0), 2), ((0, 0, -1), 2)): [ - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ], - (((1, 0, 0), 2), ((0, 1, 0), 2)): [ - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ], - (((1, 0, 0), 2), ((0, 0, 1), 1)): [ - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ], - (((0, 1, 0), 1), ((-1, 0, 0), 2)): [ - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ], - (((0, 1, 0), 1), ((0, -1, 0), 1)): [ - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ], - (((0, 1, 0), 1), ((0, 0, -1), 2)): [ - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ], - (((0, 1, 0), 1), ((1, 0, 0), 1)): [ - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ], - (((0, 1, 0), 1), ((0, 0, 1), 1)): [ - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ], - (((0, 1, 0), 2), ((-1, 0, 0), 1)): [ - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ], - (((0, 1, 0), 2), ((0, -1, 0), 2)): [ - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 3, 1)), - ], - (((0, 1, 0), 2), ((0, 0, -1), 1)): [ - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ], - (((0, 1, 0), 2), ((1, 0, 0), 2)): [ - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 3, 2)), - ], - (((0, 1, 0), 2), ((0, 0, 1), 2)): [ - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (3, 2, 1)), - ], - (((0, 0, 1), 1), ((-1, 0, 0), 1)): [ - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ], - (((0, 0, 1), 1), ((0, -1, 0), 2)): [ - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ], - (((0, 0, 1), 1), ((0, 0, -1), 1)): [ - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 1, 2)), - ], - (((0, 0, 1), 1), ((1, 0, 0), 2)): [ - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ], - (((0, 0, 1), 1), ((0, 1, 0), 1)): [ - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((-1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ], - (((0, 0, 1), 2), ((-1, 0, 0), 2)): [ - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ], - (((0, 0, 1), 2), ((0, -1, 0), 1)): [ - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 3, 1)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 2, 1)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (2, 1, 3)), - ], - (((0, 0, 1), 2), ((0, 0, -1), 2)): [ - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ], - (((0, 0, 1), 2), ((1, 0, 0), 1)): [ - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (2, 3, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 2, 1)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (1, 2, 3)), - ], - (((0, 0, 1), 2), ((0, 1, 0), 2)): [ - ((-1, -1, 1), (3, 2, 1)), - ((-1, -1, 1), (3, 1, 2)), - ((-1, -1, 1), (1, 3, 2)), - ((-1, -1, 1), (1, 2, 3)), - ((-1, -1, 1), (2, 1, 3)), - ((-1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (2, 3, 1)), - ((1, -1, 1), (3, 2, 1)), - ((1, -1, 1), (3, 1, 2)), - ((1, -1, 1), (1, 3, 2)), - ((1, -1, 1), (1, 2, 3)), - ((1, -1, 1), (2, 1, 3)), - ((1, -1, -1), (2, 1, 3)), - ((1, -1, -1), (1, 2, 3)), - ((1, -1, -1), (1, 3, 2)), - ((1, -1, -1), (3, 1, 2)), - ((1, 1, -1), (3, 1, 2)), - ((1, 1, -1), (1, 3, 2)), - ((1, 1, -1), (1, 2, 3)), - ((1, 1, -1), (2, 1, 3)), - ((1, 1, -1), (2, 3, 1)), - ((1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 2, 1)), - ((-1, 1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 1, 2)), - ((-1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (3, 2, 1)), - ((1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 3, 1)), - ((-1, -1, -1), (2, 1, 3)), - ((-1, -1, -1), (1, 2, 3)), - ((-1, -1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 3, 2)), - ((-1, 1, -1), (1, 2, 3)), - ((-1, 1, 1), (1, 2, 3)), - ((-1, 1, 1), (1, 3, 2)), - ((-1, 1, 1), (3, 1, 2)), - ((-1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 2, 1)), - ((1, 1, 1), (3, 1, 2)), - ((1, 1, 1), (1, 3, 2)), - ((1, 1, 1), (1, 2, 3)), - ((1, 1, 1), (2, 1, 3)), - ((1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 3, 1)), - ((-1, 1, 1), (2, 1, 3)), - ((-1, 1, -1), (2, 1, 3)), - ((-1, 1, -1), (2, 3, 1)), - ], -} +def get_hamiltonian_paths(): + return { + (((-1, 0, 0), 1), ((0, -1, 0), 1)): [ + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, -1), (2, 1, 3)), + ], + (((-1, 0, 0), 1), ((0, 0, -1), 2)): [ + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 2, 1)), + ], + (((-1, 0, 0), 1), ((1, 0, 0), 1)): [ + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ], + (((-1, 0, 0), 1), ((0, 1, 0), 2)): [ + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ], + (((-1, 0, 0), 1), ((0, 0, 1), 1)): [ + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 1, 2)), + ], + (((-1, 0, 0), 2), ((0, -1, 0), 2)): [ + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 3, 1)), + ], + (((-1, 0, 0), 2), ((0, 0, -1), 1)): [ + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 1, 2)), + ], + (((-1, 0, 0), 2), ((1, 0, 0), 2)): [ + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ], + (((-1, 0, 0), 2), ((0, 1, 0), 1)): [ + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ], + (((-1, 0, 0), 2), ((0, 0, 1), 2)): [ + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 2, 1)), + ], + (((0, -1, 0), 1), ((-1, 0, 0), 1)): [ + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, -1), (1, 2, 3)), + ], + (((0, -1, 0), 1), ((0, 0, -1), 1)): [ + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 1, 2)), + ], + (((0, -1, 0), 1), ((1, 0, 0), 2)): [ + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 3, 2)), + ], + (((0, -1, 0), 1), ((0, 1, 0), 1)): [ + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ], + (((0, -1, 0), 1), ((0, 0, 1), 2)): [ + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 2, 1)), + ], + (((0, -1, 0), 2), ((-1, 0, 0), 2)): [ + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 3, 2)), + ], + (((0, -1, 0), 2), ((0, 0, -1), 2)): [ + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 2, 1)), + ], + (((0, -1, 0), 2), ((1, 0, 0), 1)): [ + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ], + (((0, -1, 0), 2), ((0, 1, 0), 2)): [ + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ], + (((0, -1, 0), 2), ((0, 0, 1), 1)): [ + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 1, 2)), + ], + (((0, 0, -1), 1), ((-1, 0, 0), 2)): [ + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 3, 2)), + ], + (((0, 0, -1), 1), ((0, -1, 0), 1)): [ + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, -1), (2, 1, 3)), + ], + (((0, 0, -1), 1), ((1, 0, 0), 1)): [ + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, -1), (1, 2, 3)), + ], + (((0, 0, -1), 1), ((0, 1, 0), 2)): [ + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ], + (((0, 0, -1), 1), ((0, 0, 1), 1)): [ + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 1, 2)), + ], + (((0, 0, -1), 2), ((-1, 0, 0), 1)): [ + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, -1), (1, 2, 3)), + ], + (((0, 0, -1), 2), ((0, -1, 0), 2)): [ + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 3, 1)), + ], + (((0, 0, -1), 2), ((1, 0, 0), 2)): [ + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 3, 2)), + ], + (((0, 0, -1), 2), ((0, 1, 0), 1)): [ + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ], + (((0, 0, -1), 2), ((0, 0, 1), 2)): [ + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ], + (((1, 0, 0), 1), ((-1, 0, 0), 1)): [ + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ], + (((1, 0, 0), 1), ((0, -1, 0), 2)): [ + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (2, 3, 1)), + ], + (((1, 0, 0), 1), ((0, 0, -1), 1)): [ + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ], + (((1, 0, 0), 1), ((0, 1, 0), 1)): [ + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ], + (((1, 0, 0), 1), ((0, 0, 1), 2)): [ + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ], + (((1, 0, 0), 2), ((-1, 0, 0), 2)): [ + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ], + (((1, 0, 0), 2), ((0, -1, 0), 1)): [ + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ], + (((1, 0, 0), 2), ((0, 0, -1), 2)): [ + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ], + (((1, 0, 0), 2), ((0, 1, 0), 2)): [ + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ], + (((1, 0, 0), 2), ((0, 0, 1), 1)): [ + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ], + (((0, 1, 0), 1), ((-1, 0, 0), 2)): [ + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ], + (((0, 1, 0), 1), ((0, -1, 0), 1)): [ + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ], + (((0, 1, 0), 1), ((0, 0, -1), 2)): [ + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ], + (((0, 1, 0), 1), ((1, 0, 0), 1)): [ + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ], + (((0, 1, 0), 1), ((0, 0, 1), 1)): [ + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ], + (((0, 1, 0), 2), ((-1, 0, 0), 1)): [ + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ], + (((0, 1, 0), 2), ((0, -1, 0), 2)): [ + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 3, 1)), + ], + (((0, 1, 0), 2), ((0, 0, -1), 1)): [ + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ], + (((0, 1, 0), 2), ((1, 0, 0), 2)): [ + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 3, 2)), + ], + (((0, 1, 0), 2), ((0, 0, 1), 2)): [ + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (3, 2, 1)), + ], + (((0, 0, 1), 1), ((-1, 0, 0), 1)): [ + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ], + (((0, 0, 1), 1), ((0, -1, 0), 2)): [ + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ], + (((0, 0, 1), 1), ((0, 0, -1), 1)): [ + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 1, 2)), + ], + (((0, 0, 1), 1), ((1, 0, 0), 2)): [ + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ], + (((0, 0, 1), 1), ((0, 1, 0), 1)): [ + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((-1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ], + (((0, 0, 1), 2), ((-1, 0, 0), 2)): [ + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ], + (((0, 0, 1), 2), ((0, -1, 0), 1)): [ + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 3, 1)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 2, 1)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (2, 1, 3)), + ], + (((0, 0, 1), 2), ((0, 0, -1), 2)): [ + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ], + (((0, 0, 1), 2), ((1, 0, 0), 1)): [ + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (2, 3, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 2, 1)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (1, 2, 3)), + ], + (((0, 0, 1), 2), ((0, 1, 0), 2)): [ + ((-1, -1, 1), (3, 2, 1)), + ((-1, -1, 1), (3, 1, 2)), + ((-1, -1, 1), (1, 3, 2)), + ((-1, -1, 1), (1, 2, 3)), + ((-1, -1, 1), (2, 1, 3)), + ((-1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (2, 3, 1)), + ((1, -1, 1), (3, 2, 1)), + ((1, -1, 1), (3, 1, 2)), + ((1, -1, 1), (1, 3, 2)), + ((1, -1, 1), (1, 2, 3)), + ((1, -1, 1), (2, 1, 3)), + ((1, -1, -1), (2, 1, 3)), + ((1, -1, -1), (1, 2, 3)), + ((1, -1, -1), (1, 3, 2)), + ((1, -1, -1), (3, 1, 2)), + ((1, 1, -1), (3, 1, 2)), + ((1, 1, -1), (1, 3, 2)), + ((1, 1, -1), (1, 2, 3)), + ((1, 1, -1), (2, 1, 3)), + ((1, 1, -1), (2, 3, 1)), + ((1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 2, 1)), + ((-1, 1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 1, 2)), + ((-1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (3, 2, 1)), + ((1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 3, 1)), + ((-1, -1, -1), (2, 1, 3)), + ((-1, -1, -1), (1, 2, 3)), + ((-1, -1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 3, 2)), + ((-1, 1, -1), (1, 2, 3)), + ((-1, 1, 1), (1, 2, 3)), + ((-1, 1, 1), (1, 3, 2)), + ((-1, 1, 1), (3, 1, 2)), + ((-1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 2, 1)), + ((1, 1, 1), (3, 1, 2)), + ((1, 1, 1), (1, 3, 2)), + ((1, 1, 1), (1, 2, 3)), + ((1, 1, 1), (2, 1, 3)), + ((1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 3, 1)), + ((-1, 1, 1), (2, 1, 3)), + ((-1, 1, -1), (2, 1, 3)), + ((-1, 1, -1), (2, 3, 1)), + ], + } diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index c09417ee003..7fba49d6e8d 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -14,7 +14,7 @@ from pyomo.common.errors import DeveloperError from pyomo.common.dependencies import numpy as np from pyomo.contrib.piecewise.ordered_3d_j1_triangulation_data import ( - hamiltonian_paths as incremental_3d_simplex_pair_to_path, + get_hamiltonian_paths#as incremental_3d_simplex_pair_to_path, ) @@ -317,6 +317,7 @@ def add_top_left(): def _get_ordered_j1_triangulation_3d(points_map, num_pts): + incremental_3d_simplex_pair_to_path = get_hamiltonian_paths() # To start, we need a hamiltonian path in the grid graph of *double* cubes # (2x2x2 cubes) grid_hamiltonian = _get_grid_hamiltonian(3, round(num_pts / 2)) # division is exact From 896eae16203d9b3ab8865ce68ae5f13db6057b23 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 14:07:31 -0600 Subject: [PATCH 2118/3044] LegacySolverWrapper: 'options' and 'config' should be singleton attributes --- pyomo/contrib/solver/base.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index ac07bda4e67..4fe8bee4e53 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -377,6 +377,14 @@ def __enter__(self): def __exit__(self, t, v, traceback): """Exit statement - enables `with` statements.""" + def __setattr__(self, attr, value): + # 'options' and 'config' are really singleton attributes. Map + # any assignment to set_value() + if attr in ('options', 'config') and attr in self.__dict__: + getattr(self, attr).set_value(value) + else: + super().__setattr__(attr, value) + def _map_config( self, tee=NOTSET, @@ -395,7 +403,6 @@ def _map_config( writer_config=NOTSET, ): """Map between legacy and new interface configuration options""" - self.config = self.config() if 'report_timing' not in self.config: self.config.declare( 'report_timing', ConfigValue(domain=bool, default=False) @@ -410,8 +417,6 @@ def _map_config( self.config.time_limit = timelimit if report_timing is not NOTSET: self.config.report_timing = report_timing - if self.options is not None: - self.config.solver_options.set_value(self.options) if (options is not NOTSET) and (solver_options is not NOTSET): # There is no reason for a user to be trying to mix both old # and new options. That is silly. So we will yell at them. From 6975cb6b068beda72f748396dd51644f03b895ce Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 14:08:02 -0600 Subject: [PATCH 2119/3044] Update 'options' tests to reflect new bas class --- pyomo/contrib/solver/tests/unit/test_base.py | 50 +++++++------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index b7937d16af3..ba62f97542a 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -285,73 +285,49 @@ def test_solver_options_behavior(self): # Test case 1: Set at instantiation solver = _LegacyWrappedSolverBase(options={'max_iter': 6}) self.assertEqual(solver.options, {'max_iter': 6}) + self.assertEqual(solver.config.solver_options, {'max_iter': 6}) # Test case 2: Set later solver = _LegacyWrappedSolverBase() solver.options = {'max_iter': 4, 'foo': 'bar'} self.assertEqual(solver.options, {'max_iter': 4, 'foo': 'bar'}) + self.assertEqual(solver.config.solver_options, {'max_iter': 4, 'foo': 'bar'}) # Test case 3: pass some options to the mapping (aka, 'solve' command) solver = _LegacyWrappedSolverBase() - config = ConfigDict(implicit=True) - config.declare( - 'solver_options', - ConfigDict(implicit=True, description="Options to pass to the solver."), - ) - solver.config = config solver._map_config(options={'max_iter': 4}) + self.assertEqual(solver.options, {'max_iter': 4}) self.assertEqual(solver.config.solver_options, {'max_iter': 4}) # Test case 4: Set at instantiation and override during 'solve' call solver = _LegacyWrappedSolverBase(options={'max_iter': 6}) - config = ConfigDict(implicit=True) - config.declare( - 'solver_options', - ConfigDict(implicit=True, description="Options to pass to the solver."), - ) - solver.config = config solver._map_config(options={'max_iter': 4}) + self.assertEqual(solver.options, {'max_iter': 4}) self.assertEqual(solver.config.solver_options, {'max_iter': 4}) - self.assertEqual(solver.options, {'max_iter': 6}) # solver_options are also supported # Test case 1: set at instantiation solver = _LegacyWrappedSolverBase(solver_options={'max_iter': 6}) self.assertEqual(solver.options, {'max_iter': 6}) + self.assertEqual(solver.config.solver_options, {'max_iter': 6}) # Test case 2: pass some solver_options to the mapping (aka, 'solve' command) solver = _LegacyWrappedSolverBase() - config = ConfigDict(implicit=True) - config.declare( - 'solver_options', - ConfigDict(implicit=True, description="Options to pass to the solver."), - ) - solver.config = config solver._map_config(solver_options={'max_iter': 4}) + self.assertEqual(solver.options, {'max_iter': 4}) self.assertEqual(solver.config.solver_options, {'max_iter': 4}) # Test case 3: Set at instantiation and override during 'solve' call solver = _LegacyWrappedSolverBase(solver_options={'max_iter': 6}) - config = ConfigDict(implicit=True) - config.declare( - 'solver_options', - ConfigDict(implicit=True, description="Options to pass to the solver."), - ) - solver.config = config solver._map_config(solver_options={'max_iter': 4}) + self.assertEqual(solver.options, {'max_iter': 4}) self.assertEqual(solver.config.solver_options, {'max_iter': 4}) - self.assertEqual(solver.options, {'max_iter': 6}) # users can mix... sort of # Test case 1: Initialize with options, solve with solver_options solver = _LegacyWrappedSolverBase(options={'max_iter': 6}) - config = ConfigDict(implicit=True) - config.declare( - 'solver_options', - ConfigDict(implicit=True, description="Options to pass to the solver."), - ) - solver.config = config solver._map_config(solver_options={'max_iter': 4}) + self.assertEqual(solver.options, {'max_iter': 4}) self.assertEqual(solver.config.solver_options, {'max_iter': 4}) # users CANNOT initialize both values at the same time, because how @@ -363,14 +339,20 @@ def test_solver_options_behavior(self): ) # Test case 2: Passing to `solve` solver = _LegacyWrappedSolverBase() + with self.assertRaises(ValueError): + solver._map_config(solver_options={'max_iter': 4}, options={'max_iter': 6}) + + # Test that assignment to maps to set_vlaue: + solver = _LegacyWrappedSolverBase() config = ConfigDict(implicit=True) config.declare( 'solver_options', ConfigDict(implicit=True, description="Options to pass to the solver."), ) solver.config = config - with self.assertRaises(ValueError): - solver._map_config(solver_options={'max_iter': 4}, options={'max_iter': 6}) + solver.config.solver_options.max_iter = 6 + self.assertEqual(solver.options, {'max_iter': 6}) + self.assertEqual(solver.config.solver_options, {'max_iter': 6}) def test_map_results(self): # Unclear how to test this From a61df0c68d98a60090bc884521769ced4caa5b46 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 14:12:52 -0600 Subject: [PATCH 2120/3044] Fix typo --- pyomo/contrib/solver/tests/unit/test_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/solver/tests/unit/test_base.py b/pyomo/contrib/solver/tests/unit/test_base.py index ba62f97542a..e9ea717593f 100644 --- a/pyomo/contrib/solver/tests/unit/test_base.py +++ b/pyomo/contrib/solver/tests/unit/test_base.py @@ -342,7 +342,7 @@ def test_solver_options_behavior(self): with self.assertRaises(ValueError): solver._map_config(solver_options={'max_iter': 4}, options={'max_iter': 6}) - # Test that assignment to maps to set_vlaue: + # Test that assignment to maps to set_value: solver = _LegacyWrappedSolverBase() config = ConfigDict(implicit=True) config.declare( From 7ade8ac7e7286a7d229ea35ad140060e972a03a1 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 6 Aug 2024 14:19:32 -0600 Subject: [PATCH 2121/3044] Raising exceptions when a solver is not available --- pyomo/contrib/alternative_solutions/balas.py | 1 + pyomo/contrib/alternative_solutions/lp_enum.py | 2 ++ .../contrib/alternative_solutions/lp_enum_solnpool.py | 5 ++++- pyomo/contrib/alternative_solutions/obbt.py | 2 ++ pyomo/contrib/alternative_solutions/solnpool.py | 5 +++-- .../contrib/alternative_solutions/tests/test_balas.py | 11 ++++++++++- .../alternative_solutions/tests/test_lp_enum.py | 10 ++++++++++ .../tests/test_lp_enum_solnpool.py | 5 ++++- .../contrib/alternative_solutions/tests/test_obbt.py | 11 +++++++++++ 9 files changed, 47 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index b28be674cf1..b5cee23da4f 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -115,6 +115,7 @@ def enumerate_binary_solutions( # Setup solver # opt = pe.SolverFactory(solver) + opt.available() for parameter, value in solver_options.items(): opt.options[parameter] = value # diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index fda8799739d..ad6478a9d76 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -159,6 +159,8 @@ def enumerate_linear_solutions( opt.gurobi_options[parameter] = value else: opt = pe.SolverFactory(solver) + if not opt.available(): + raise ValueError(solver + " is not available") for parameter, value in solver_options.items(): opt.options[parameter] = value if solver == "gurobi": diff --git a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py index 4d801082f7f..448c646d4fb 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py @@ -14,6 +14,7 @@ gurobipy, gurobi_available = attempt_import("gurobipy") import pyomo.environ as pe +import pyomo.common.errors from pyomo.contrib.alternative_solutions import aos_utils, shifted_lp, solution from pyomo.contrib import appsi @@ -128,7 +129,7 @@ def enumerate_linear_solutions_soln_pool( # Setup gurobi # if not gurobi_available: - return [] + raise pyomo.common.errors.ApplicationError(f"Solver (gurobi) not available") # For now keeping things simple # TODO: See if this can be relaxed, but for now just leave as all @@ -139,6 +140,8 @@ def enumerate_linear_solutions_soln_pool( # TODO: Check if problem is continuous or mixed binary opt = pe.SolverFactory("gurobi") + if not opt.available(): + raise ValueError(solver + " is not available") for parameter, value in solver_options.items(): opt.options[parameter] = value diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 182c6e9973a..b88b7bd5df4 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -190,6 +190,8 @@ def obbt_analysis_bounds_and_solutions( use_appsi = True else: opt = pe.SolverFactory(solver) + if not opt.available(): + raise ValueError(solver + " is not available") for parameter, value in solver_options.items(): opt.options[parameter] = value try: diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index b18f5c82eee..96e85677b9a 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -67,10 +67,11 @@ def gurobi_generate_solutions( # Setup gurobi # if not gurobipy_available: - return [] + raise pyomo.common.errors.ApplicationError("Solver (gurobi) not available") opt = appsi.solvers.Gurobi() + opt.available() if not opt.available(): # pragma: no cover - return [] + raise pyomo.common.errors.ApplicationError("Solver (gurobi) not available") opt.config.stream_solver = tee opt.config.load_solution = False diff --git a/pyomo/contrib/alternative_solutions/tests/test_balas.py b/pyomo/contrib/alternative_solutions/tests/test_balas.py index d706b5d389d..27c3c7b014d 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_balas.py +++ b/pyomo/contrib/alternative_solutions/tests/test_balas.py @@ -23,7 +23,6 @@ from pyomo.contrib.alternative_solutions import enumerate_binary_solutions import pyomo.contrib.alternative_solutions.tests.test_cases as tc - solvers = list(pyomo.opt.check_available_solvers("glpk", "gurobi", "appsi_gurobi")) pytestmark = unittest.pytest.mark.parametrize("mip_solver", solvers) @@ -31,6 +30,16 @@ @unittest.pytest.mark.default class TestBalasUnit: + def test_bad_solver(self, mip_solver): + """ + Confirm that an exception is thrown with a bad solver name. + """ + m = tc.get_triangle_ip() + try: + enumerate_binary_solutions(m, solver="unknown_solver") + except pyomo.common.errors.ApplicationError as e: + pass + def test_ip_feasibility(self, mip_solver): """ Enumerate solutions for an ip: triangle_ip. diff --git a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py index 13fb33dd9d1..18ab229ff1c 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py +++ b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py @@ -32,6 +32,16 @@ @unittest.pytest.mark.default class TestLPEnum: + def test_bad_solver(self, mip_solver): + """ + Confirm that an exception is thrown with a bad solver name. + """ + m = tc.get_3d_polyhedron_problem() + try: + lp_enum.enumerate_linear_solutions(m, solver="unknown_solver") + except pyomo.common.errors.ApplicationError as e: + pass + @unittest.skipIf(True, "Ignoring fragile test for solver timeout.") def test_no_time(self, mip_solver): """ diff --git a/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py index fccac026cf5..6d3b5211f9e 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/tests/test_lp_enum_solnpool.py @@ -32,7 +32,10 @@ def test_here(): n.x.domain = pe.Reals n.y.domain = pe.Reals - sols = lp_enum_solnpool.enumerate_linear_solutions_soln_pool(n, tee=True) + try: + sols = lp_enum_solnpool.enumerate_linear_solutions_soln_pool(n, tee=True) + except pyomo.common.errors.ApplicationError as e: + sols = [] # TODO - Confirm how solnpools deal with duplicate solutions if gurobi_available: diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index f4bef580074..884ada6e885 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -35,6 +35,17 @@ @unittest.pytest.mark.default class TestOBBTUnit: + @unittest.skipIf(not numpy_available, "Numpy not installed") + def test_bad_solver(self, mip_solver): + """ + Confirm that an exception is thrown with a bad solver name. + """ + m = tc.get_2d_diamond_problem() + try: + obbt_analysis(m, solver="unknown_solver") + except pyomo.common.errors.ApplicationError as e: + pass + @unittest.skipIf(not numpy_available, "Numpy not installed") def test_obbt_analysis(self, mip_solver): """ From 835733b34e9c5140ee9f804382bd09d0615e2765 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 6 Aug 2024 14:47:21 -0600 Subject: [PATCH 2122/3044] Using logging instead of quiet/debug --- .../alternative_solutions/aos_utils.py | 29 +++++--- pyomo/contrib/alternative_solutions/balas.py | 66 ++++++++---------- .../contrib/alternative_solutions/lp_enum.py | 68 ++++++++----------- .../alternative_solutions/lp_enum_solnpool.py | 16 +++-- pyomo/contrib/alternative_solutions/obbt.py | 44 +++++------- .../contrib/alternative_solutions/solnpool.py | 17 ++--- .../tests/test_lp_enum.py | 8 +-- 7 files changed, 113 insertions(+), 135 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index cfc84ce9dc3..1f23834aa8f 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -9,6 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import logging + +logger = logging.getLogger(__name__) + +from contextlib import contextmanager + from pyomo.common.dependencies import numpy as numpy, numpy_available if numpy_available: @@ -21,6 +27,17 @@ import pyomo.util.vars_from_expressions as vfe +@contextmanager +def logcontext(level): + logger = logging.getLogger() + current_level = logger.getEffectiveLevel() + logger.setLevel(level) + try: + yield + finally: + logger.setLevel(current_level) + + def get_active_objective(model): """ Finds and returns the active objective function for a model. Currently @@ -157,7 +174,6 @@ def get_model_variables( include_binary=True, include_integer=True, include_fixed=False, - quiet=True, ): """ Gathers and returns all variables or a subset of variables from a Pyomo @@ -184,8 +200,6 @@ def get_model_variables( Boolean indicating that integer variables should be included. include_fixed : boolean Boolean indicating that fixed variables should be included. - quiet : boolean - Boolean that is True if all output is suppressed. Returns ------- @@ -271,11 +285,8 @@ def get_model_variables( include_fixed, ) else: # pragma: no cover - if not quiet: - print( - ("No variables added for unrecognized component {}.").format( - comp - ) - ) + logger.info( + ("No variables added for unrecognized component {}.").format(comp) + ) return variable_set diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index b5cee23da4f..8cff6bde305 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -9,6 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import logging + +logger = logging.getLogger(__name__) + import pyomo.environ as pe from pyomo.common.collections import ComponentSet from pyomo.contrib.alternative_solutions import Solution @@ -26,7 +30,6 @@ def enumerate_binary_solutions( solver="gurobi", solver_options={}, tee=False, - quiet=True, seed=None, ): """ @@ -63,8 +66,6 @@ def enumerate_binary_solutions( Solver option-value pairs to be passed to the solver. tee : boolean Boolean indicating that the solver output should be displayed. - quiet : boolean - Boolean indicating whether to suppress all output. seed : int Optional integer seed for the numpy random number generator @@ -74,8 +75,7 @@ def enumerate_binary_solutions( A list of Solution objects. [Solution] """ - if not quiet: # pragma: no cover - print("STARTING NO-GOOD CUT ANALYSIS") + logger.info("STARTING NO-GOOD CUT ANALYSIS") assert search_mode in [ "optimal", @@ -100,14 +100,13 @@ def enumerate_binary_solutions( else: # pragma: no cover non_binary_variables.append(var.name) if len(non_binary_variables) > 0: - if not quiet: # pragma: no cover - print( - ( - "Warning: The following non-binary variables were included" - "in the variable list and will be ignored:" - ) + logger.warn( + ( + "Warning: The following non-binary variables were included" + "in the variable list and will be ignored:" ) - print(", ".join(non_binary_variables)) + ) + logger.warn(", ".join(non_binary_variables)) orig_objective = aos_utils.get_active_objective(model) @@ -146,8 +145,7 @@ def enumerate_binary_solutions( # # Initial solve of the model # - if not quiet: # pragma: no cover - print("Performing initial solve of model.") + logger.info("Performing initial solve of model.") results = opt.solve(model, tee=tee, load_solutions=False) status = results.solver.status if not pe.check_optimal_termination(results): @@ -162,8 +160,7 @@ def enumerate_binary_solutions( model.solutions.load_from(results) orig_objective_value = pe.value(orig_objective) - if not quiet: # pragma: no cover - print("Found optimal solution, value = {}.".format(orig_objective_value)) + logger.info("Found optimal solution, value = {}.".format(orig_objective_value)) solutions = [Solution(model, all_variables, objective=orig_objective)] # # Return just this solution if there are no binary variables @@ -172,8 +169,7 @@ def enumerate_binary_solutions( return solutions aos_block = aos_utils._add_aos_block(model, name="_balas") - if not quiet: # pragma: no cover - print("Added block {} to the model.".format(aos_block)) + logger.info("Added block {} to the model.".format(aos_block)) aos_block.no_good_cuts = pe.ConstraintList() aos_utils._add_objective_constraint( aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap @@ -219,39 +215,33 @@ def enumerate_binary_solutions( if pe.check_optimal_termination(results): model.solutions.load_from(results) orig_obj_value = pe.value(orig_objective) - if not quiet: # pragma: no cover - print( - "Iteration {}: objective = {}".format( - solution_number, orig_obj_value - ) - ) + logger.info( + "Iteration {}: objective = {}".format(solution_number, orig_obj_value) + ) solutions.append(Solution(model, all_variables, objective=orig_objective)) solution_number += 1 elif ( condition == pe.TerminationCondition.infeasibleOrUnbounded or condition == pe.TerminationCondition.infeasible ): - if not quiet: # pragma: no cover - print( - "Iteration {}: Infeasible, no additional binary solutions.".format( - solution_number - ) + logger.info( + "Iteration {}: Infeasible, no additional binary solutions.".format( + solution_number ) + ) break else: # pragma: no cover - if not quiet: - print( - ( - "Iteration {}: Unexpected condition, SolverStatus = {}, " - "TerminationCondition = {}" - ).format(solution_number, status.value, condition.value) - ) + logger.info( + ( + "Iteration {}: Unexpected condition, SolverStatus = {}, " + "TerminationCondition = {}" + ).format(solution_number, status.value, condition.value) + ) break aos_block.deactivate() orig_objective.activate() - if not quiet: # pragma: no cover - print("COMPLETED NO-GOOD CUT ANALYSIS") + logger.info("COMPLETED NO-GOOD CUT ANALYSIS") return solutions diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index ad6478a9d76..8e16728e256 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -9,6 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import logging + +logger = logging.getLogger(__name__) + import pyomo.environ as pe from pyomo.contrib.alternative_solutions import ( aos_utils, @@ -16,6 +20,7 @@ solution, solnpool, ) +from pyomo.contrib.alternative_solutions.aos_utils import logcontext from pyomo.contrib import appsi @@ -30,8 +35,6 @@ def enumerate_linear_solutions( solver="gurobi", solver_options={}, tee=False, - quiet=True, - debug=False, seed=None, ): """ @@ -74,10 +77,6 @@ def enumerate_linear_solutions( Solver option-value pairs to be passed to the solver. tee : boolean Boolean indicating that the solver output should be displayed. - quiet : boolean - Boolean indicating whether to suppress all output. - debug : boolean - Boolean indicating whether to include debugging output. seed : int Optional integer seed for the numpy random number generator @@ -87,8 +86,7 @@ def enumerate_linear_solutions( A list of Solution objects. [Solution] """ - if not quiet: # pragma: no cover - print("STARTING LP ENUMERATION ANALYSIS") + logger.info("STARTING LP ENUMERATION ANALYSIS") # TODO: Make this a parameter? zero_threshold = 1e-5 @@ -120,9 +118,9 @@ def enumerate_linear_solutions( # else: # non_binary_variables.append(var.name) # if len(non_binary_variables) > 0: - # print(('Warning: The following non-binary variables were included' + # logger.warn(('Warning: The following non-binary variables were included' # 'in the variable list and will be ignored:')) - # print(", ".join(non_binary_variables)) + # logger.warn(", ".join(non_binary_variables)) # all_variables = aos_utils.get_model_variables(model, None, # include_fixed=True) @@ -168,8 +166,7 @@ def enumerate_linear_solutions( # solutions not at a vertex opt.options["Heuristics"] = 0.0 - if not quiet: # pragma: no cover - print("Performing initial solve of model.") + logger.info("Performing initial solve of model.") if use_appsi: results = opt.solve(model) @@ -194,15 +191,13 @@ def enumerate_linear_solutions( orig_objective = aos_utils.get_active_objective(model) orig_objective_value = pe.value(orig_objective) - if not quiet: # pragma: no cover - print("Found optimal solution, value = {}.".format(orig_objective_value)) + logger.info("Found optimal solution, value = {}.".format(orig_objective_value)) aos_block = aos_utils._add_aos_block(model, name="_lp_enum") aos_utils._add_objective_constraint( aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap ) - if not quiet: # pragma: no cover - print("Added block {} to the model.".format(aos_block)) + logger.info("Added block {} to the model.".format(aos_block)) canon_block = shifted_lp.get_shifted_linear_model(model) cb = canon_block @@ -233,10 +228,9 @@ def enumerate_linear_solutions( solution_number = 1 solutions = [] while solution_number <= num_solutions: - if not quiet: # pragma: no cover - print("Solving Iteration {}: ".format(solution_number), end="") + logger.info("Solving Iteration {}: ".format(solution_number), end="") - if debug: + with logcontext(logging.DEBUG): model.pprint() if use_appsi: results = opt.solve(model) @@ -257,13 +251,13 @@ def enumerate_linear_solutions( solutions.append(sol) orig_objective_value = sol.objective[1] - if not quiet: # pragma: no cover - print("Solved, objective = {}".format(orig_objective_value)) + with logcontext(logging.INFO): + logger.info("Solved, objective = {}".format(orig_objective_value)) for var, index in cb.var_map.items(): - print( + logger.info( "{} = {}".format(var.name, var.lb + cb.var_lower[index].value) ) - if debug: + with logcontext(logging.DEBUG): model.display() if hasattr(cb, "force_out"): @@ -330,27 +324,23 @@ def enumerate_linear_solutions( condition == pe.TerminationCondition.infeasibleOrUnbounded or condition == pe.TerminationCondition.infeasible ): - if not quiet: # pragma: no cover - print("Infeasible, all alternative solutions have been found.") + logger.info("Infeasible, all alternative solutions have been found.") break else: - if not quiet: # pragma: no cover - status = results.solver.status - print( - ( - "Unexpected solver condition. Stopping LP enumeration. " - "SolverStatus = {}, TerminationCondition = {}" - ).format(status.value, condition.value) - ) + logger.info( + ( + "Unexpected solver condition. Stopping LP enumeration. " + "SolverStatus = {}, TerminationCondition = {}" + ).format(results.solver.status.value, condition.value) + ) break - if debug: - print("") - print("=" * 80) - print("") + with logcontext(logging.DEBUG): + logging.debug("") + logging.debug("=" * 80) + logging.debug("") model.del_component("aos_block") - if not quiet: # pragma: no cover - print("COMPLETED LP ENUMERATION ANALYSIS") + logger.info("COMPLETED LP ENUMERATION ANALYSIS") return solutions diff --git a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py index 448c646d4fb..6c707dbae53 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py @@ -9,6 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import logging + +logger = logging.getLogger(__name__) + from pyomo.common.dependencies import attempt_import gurobipy, gurobi_available = attempt_import("gurobipy") @@ -45,7 +49,7 @@ def cut_generator_callback(self, cb_m, cb_opt, cb_where): if cb_where == GRB.Callback.MIPSOL: cb_opt.cbGetSolution(vars=self.variables) - print("***FOUND SOLUTION***") + logger.info("***FOUND SOLUTION***") for var, index in self.model.var_map.items(): var.set_value(var.lb + self.model.var_lower[index].value) @@ -124,7 +128,7 @@ def enumerate_linear_solutions_soln_pool( A list of Solution objects. [Solution] """ - print("STARTING LP ENUMERATION ANALYSIS USING GUROBI SOLUTION POOL") + logger.info("STARTING LP ENUMERATION ANALYSIS USING GUROBI SOLUTION POOL") # # Setup gurobi # @@ -145,7 +149,7 @@ def enumerate_linear_solutions_soln_pool( for parameter, value in solver_options.items(): opt.options[parameter] = value - print("Performing initial solve of model.") + logger.info("Performing initial solve of model.") results = opt.solve(model, tee=tee) status = results.solver.status condition = results.solver.termination_condition @@ -160,10 +164,10 @@ def enumerate_linear_solutions_soln_pool( orig_objective = aos_utils.get_active_objective(model) orig_objective_value = pe.value(orig_objective) - print("Found optimal solution, value = {}.".format(orig_objective_value)) + logger.info("Found optimal solution, value = {}.".format(orig_objective_value)) aos_block = aos_utils._add_aos_block(model, name="_lp_enum") - print("Added block {} to the model.".format(aos_block)) + logger.info("Added block {} to the model.".format(aos_block)) aos_utils._add_objective_constraint( aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap ) @@ -232,6 +236,6 @@ def bound_slack_rule(m, var_index): opt.solve(cb) aos_block.deactivate() - print("COMPLETED LP ENUMERATION ANALYSIS") + logger.info("COMPLETED LP ENUMERATION ANALYSIS") return cut_generator.solutions diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index b88b7bd5df4..7cd64a70c09 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -9,6 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import logging + +logger = logging.getLogger(__name__) + import pyomo.environ as pe from pyomo.contrib.alternative_solutions import aos_utils from pyomo.contrib.alternative_solutions import Solution @@ -26,7 +30,6 @@ def obbt_analysis( solver="gurobi", solver_options={}, tee=False, - quiet=True, ): """ Calculates the bounds on each variable by solving a series of min and max @@ -63,8 +66,6 @@ def obbt_analysis( Solver option-value pairs to be passed to the solver. tee : boolean Boolean indicating that the solver output should be displayed. - quiet : boolean - Boolean indicating whether to suppress all output. Returns ------- @@ -83,7 +84,6 @@ def obbt_analysis( solver=solver, solver_options=solver_options, tee=tee, - quiet=quiet, ) return bounds @@ -99,7 +99,6 @@ def obbt_analysis_bounds_and_solutions( solver="gurobi", solver_options={}, tee=False, - quiet=True, ): """ Calculates the bounds on each variable by solving a series of min and max @@ -136,8 +135,6 @@ def obbt_analysis_bounds_and_solutions( Solver option-value pairs to be passed to the solver. tee : boolean Boolean indicating that the solver output should be displayed. - quiet : boolean - Boolean indicating whether to suppress all output. Returns ------- @@ -151,8 +148,7 @@ def obbt_analysis_bounds_and_solutions( # TODO - parallelization - if not quiet: # pragma: no cover - print("STARTING OBBT ANALYSIS") + logger.info("STARTING OBBT ANALYSIS") if warmstart: assert ( @@ -169,10 +165,9 @@ def obbt_analysis_bounds_and_solutions( solutions[var] = [] num_vars = len(variable_list) - if not quiet: # pragma: no cover - print( - "Analyzing {} variables ({} total solves).".format(num_vars, 2 * num_vars) - ) + logger.info( + "Analyzing {} variables ({} total solves).".format(num_vars, 2 * num_vars) + ) orig_objective = aos_utils.get_active_objective(model) use_appsi = False @@ -207,8 +202,7 @@ def obbt_analysis_bounds_and_solutions( optimal_tc = pe.TerminationCondition.optimal infeas_or_unbdd_tc = pe.TerminationCondition.infeasibleOrUnbounded unbdd_tc = pe.TerminationCondition.unbounded - if not quiet: # pragma: no cover - print("Performing initial solve of model.") + logger.info("Performing initial solve of model.") if condition != optimal_tc: raise RuntimeError( @@ -223,11 +217,9 @@ def obbt_analysis_bounds_and_solutions( if warmstart: _add_solution(solutions) orig_objective_value = pe.value(orig_objective) - if not quiet: # pragma: no cover - print("Found optimal solution, value = {}.".format(orig_objective_value)) + logger.info("Found optimal solution, value = {}.".format(orig_objective_value)) aos_block = aos_utils._add_aos_block(model, name="_obbt") - if not quiet: # pragma: no cover - print("Added block {} to the model.".format(aos_block)) + logger.info("Added block {} to the model.".format(aos_block)) obj_constraints = aos_utils._add_objective_constraint( aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap ) @@ -322,7 +314,7 @@ def obbt_analysis_bounds_and_solutions( else: variable_bounds[var][idx] = float("inf") else: # pragma: no cover - print( + logger.warn( ( "Unexpected condition for the variable {} {} problem." "TerminationCondition = {}" @@ -330,12 +322,11 @@ def obbt_analysis_bounds_and_solutions( ) var_value = variable_bounds[var][idx] - if not quiet: # pragma: no cover - print( - "Iteration {}/{}: {}_{} = {}".format( - iteration, total_iterations, var.name, bound_dir, var_value - ) + logger.info( + "Iteration {}/{}: {}_{} = {}".format( + iteration, total_iterations, var.name, bound_dir, var_value ) + ) if idx == 1: variable_bounds[var] = tuple(variable_bounds[var]) @@ -346,8 +337,7 @@ def obbt_analysis_bounds_and_solutions( aos_block.deactivate() orig_objective.activate() - if not quiet: # pragma: no cover - print("COMPLETED OBBT ANALYSIS") + logger.info("COMPLETED OBBT ANALYSIS") return variable_bounds, solns diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 96e85677b9a..9a5a728f963 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -9,6 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import logging + +logger = logging.getLogger(__name__) + from pyomo.common.dependencies import attempt_import gurobipy, gurobipy_available = attempt_import("gurobipy") @@ -27,7 +31,6 @@ def gurobi_generate_solutions( abs_opt_gap=None, solver_options={}, tee=False, - quiet=True, ): """ Finds alternative optimal solutions for discrete variables using Gurobi's @@ -55,8 +58,6 @@ def gurobi_generate_solutions( Solver option-value pairs to be passed to the Gurobi solver. tee : boolean Boolean indicating that the solver output should be displayed. - quiet : boolean - Boolean indicating whether to suppress all output. Returns ------- @@ -89,13 +90,9 @@ def gurobi_generate_solutions( results = opt.solve(model) condition = results.termination_condition if not (condition == appsi.base.TerminationCondition.optimal): - if not quiet: - print( - ("Model cannot be solved, " "TerminationCondition = {}").format( - condition.value - ) - ) - return [] + raise pyomo.common.errors.ApplicationError( + "Model cannot be solved, " "TerminationCondition = {}" + ).format(condition.value) # # Collect solutions # diff --git a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py index 18ab229ff1c..d761522b019 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py +++ b/pyomo/contrib/alternative_solutions/tests/test_lp_enum.py @@ -92,9 +92,7 @@ def test_pentagonal_pyramid(self, mip_solver): n.x.domain = pe.Reals n.y.domain = pe.Reals - sols = lp_enum.enumerate_linear_solutions( - n, solver=mip_solver, quiet=True, debug=False, tee=False - ) + sols = lp_enum.enumerate_linear_solutions(n, solver=mip_solver, tee=False) for s in sols: print(s) assert len(sols) == 6 @@ -103,9 +101,7 @@ def test_pentagonal_pyramid(self, mip_solver): def test_pentagon(self, mip_solver): n = tc.get_pentagonal_lp() - sols = lp_enum.enumerate_linear_solutions( - n, solver=mip_solver, quiet=True, debug=False - ) + sols = lp_enum.enumerate_linear_solutions(n, solver=mip_solver) for s in sols: print(s) assert len(sols) == 6 From d4c426660a604503c38bdcc768646683850202f5 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 6 Aug 2024 14:53:43 -0600 Subject: [PATCH 2123/3044] Changed objective re-initialization --- pyomo/contrib/alternative_solutions/obbt.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 7cd64a70c09..4b48e4c6efe 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -256,9 +256,10 @@ def obbt_analysis_bounds_and_solutions( variable_bounds[var] = [None, None] if hasattr(aos_block, "var_objective"): - aos_block.del_component("var_objective") - - aos_block.var_objective = pe.Objective(expr=var, sense=sense) + aos_block.var_objective.expr = var + aos_block.var_objective.sense = sense + else: + aos_block.var_objective = pe.Objective(expr=var, sense=sense) if warmstart: _update_values(var, bound_dir, solutions) From 73ca0fcab8bdac103b8445a7d102969e63030259 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:09:15 -0600 Subject: [PATCH 2124/3044] Guard additional tests for pynumero availability --- pyomo/contrib/parmest/tests/test_examples.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index 450863a08a4..552c568cc7c 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -44,6 +44,7 @@ def test_model_with_constraint(self): rooney_biegler_with_constraint.main() + @unittest.skipUnless(pynumero_ASL_available, "test requires libpynumero_ASL") @unittest.skipUnless(seaborn_available, "test requires seaborn") def test_parameter_estimation_example(self): from pyomo.contrib.parmest.examples.rooney_biegler import ( @@ -67,11 +68,12 @@ def test_likelihood_ratio_example(self): likelihood_ratio_example.main() -@unittest.skipIf( - not parmest.parmest_available, +@unittest.skipUnless(pynumero_ASL_available, "test requires libpynumero_ASL") +@unittest.skipUnless(ipopt_available, "The 'ipopt' solver is not available") +@unittest.skipUnless( + parmest.parmest_available, "Cannot test parmest: required dependencies are missing", ) -@unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") class TestReactionKineticsExamples(unittest.TestCase): @classmethod def setUpClass(self): @@ -141,6 +143,7 @@ def test_model(self): reactor_design.main() + @unittest.skipUnless(pynumero_ASL_available, "test requires libpynumero_ASL") def test_parameter_estimation_example(self): from pyomo.contrib.parmest.examples.reactor_design import ( parameter_estimation_example, From 31cb42014aeee6834da21945294165a3e529a612 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:09:53 -0600 Subject: [PATCH 2125/3044] Remove tests of k_aug functionality that is no longer supported --- pyomo/contrib/parmest/tests/test_parmest.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index 1ff42a38d9e..fbe4290dd10 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -35,7 +35,6 @@ is_osx = platform.mac_ver()[0] != "" ipopt_available = SolverFactory("ipopt").available() -k_aug_available = SolverFactory("k_aug").available(exception_flag=False) pynumero_ASL_available = AmplInterface.available() testdir = this_file_dir() @@ -199,12 +198,6 @@ def test_parallel_parmest(self): retcode = subprocess.call(rlist) self.assertEqual(retcode, 0) - @unittest.skipUnless(k_aug_available, "k_aug solver not found") - def test_theta_k_aug_for_Hessian(self): - # this will fail if k_aug is not installed - objval, thetavals, Hessian = self.pest.theta_est(solver="k_aug") - self.assertAlmostEqual(objval, 4.4675, places=2) - @unittest.skipIf(not pynumero_ASL_available, "pynumero_ASL is not available") def test_theta_est_cov(self): objval, thetavals, cov = self.pest.theta_est(calc_cov=True, cov_n=6) @@ -1205,12 +1198,6 @@ def test_parallel_parmest(self): retcode = subprocess.call(rlist) assert retcode == 0 - @unittest.skipUnless(k_aug_available, "k_aug solver not found") - def test_theta_k_aug_for_Hessian(self): - # this will fail if k_aug is not installed - objval, thetavals, Hessian = self.pest.theta_est(solver="k_aug") - self.assertAlmostEqual(objval, 4.4675, places=2) - @unittest.skipIf(not pynumero_ASL_available, "pynumero_ASL is not available") def test_theta_est_cov(self): objval, thetavals, cov = self.pest.theta_est(calc_cov=True, cov_n=6) From 3ba93e555fbcb1ea3221cc028c0e1642bb488e3c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:11:17 -0600 Subject: [PATCH 2126/3044] Fix GJH version() to return the expected tuple --- pyomo/solvers/plugins/solvers/ASL.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solvers/plugins/solvers/ASL.py b/pyomo/solvers/plugins/solvers/ASL.py index 7acd59936b1..c912f2a30ee 100644 --- a/pyomo/solvers/plugins/solvers/ASL.py +++ b/pyomo/solvers/plugins/solvers/ASL.py @@ -108,7 +108,7 @@ def _get_version(self): if ver is None: # Some ASL solvers do not export a version number if results.stdout.strip().split()[-1].startswith('ASL('): - return '0.0.0' + return (0, 0, 0) return ver except OSError: pass From d1c7952fa40cc340bb54bd87471e59a6d68a80ea Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:11:30 -0600 Subject: [PATCH 2127/3044] Update "pyomo help -s" - Silence all output generated when gathering solver availability / version information - Improve handling of version() not returning a tuple - Fix a bug where an exception raised when creating a solver caused an unhandled UnknownSolver error --- pyomo/scripting/driver_help.py | 64 ++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/pyomo/scripting/driver_help.py b/pyomo/scripting/driver_help.py index 38d1a4c16bf..ce7c36932ec 100644 --- a/pyomo/scripting/driver_help.py +++ b/pyomo/scripting/driver_help.py @@ -20,6 +20,7 @@ import pyomo.common from pyomo.common.collections import Bunch +from pyomo.common.tee import capture_output import pyomo.scripting.pyomo_parser logger = logging.getLogger('pyomo.solvers') @@ -235,33 +236,42 @@ def help_solvers(): try: # Disable warnings logging.disable(logging.WARNING) - for s in solver_list: - # Create a solver, and see if it is available - with pyomo.opt.SolverFactory(s) as opt: - ver = '' - if opt.available(False): - avail = '-' - if opt.license_is_valid(): - avail = '+' - try: - ver = opt.version() - if ver: - while len(ver) > 2 and ver[-1] == 0: - ver = ver[:-1] - ver = '.'.join(str(v) for v in ver) - else: - ver = '' - except (AttributeError, NameError): - pass - elif s == 'py' or (hasattr(opt, "_metasolver") and opt._metasolver): - # py is a metasolver, but since we don't specify a subsolver - # for this test, opt is actually an UnknownSolver, so we - # can't try to get the _metasolver attribute from it. - # Also, default to False if the attribute isn't implemented - avail = '*' - else: - avail = '' - _data.append((avail, s, ver, pyomo.opt.SolverFactory.doc(s))) + # suppress ALL output + with capture_output(capture_fd=True): + for s in solver_list: + # Create a solver, and see if it is available + with pyomo.opt.SolverFactory(s) as opt: + ver = '' + if opt.available(False): + avail = '-' + if opt.license_is_valid(): + avail = '+' + try: + ver = opt.version() + if isinstance(ver, str): + pass + elif ver: + while len(ver) > 2 and ver[-1] == 0: + ver = ver[:-1] + ver = '.'.join(str(v) for v in ver) + else: + ver = '' + except (AttributeError, NameError): + pass + elif isinstance(s, UnknownSolver): + # We can get here if creating a registered + # solver failed (i.e., an exception was raised + # in __init__) + avail = '' + elif s == 'py' or (hasattr(opt, "_metasolver") and opt._metasolver): + # py is a metasolver, but since we don't specify a subsolver + # for this test, opt is actually an UnknownSolver, so we + # can't try to get the _metasolver attribute from it. + # Also, default to False if the attribute isn't implemented + avail = '*' + else: + avail = '' + _data.append((avail, s, ver, pyomo.opt.SolverFactory.doc(s))) finally: # Reset logging level logging.disable(logging.NOTSET) From 8a4dcf1d1b32969681f877de3f1b3a8bb3d464d3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:17:01 -0600 Subject: [PATCH 2128/3044] Guard against platforms missing lsb_release --- pyomo/common/tests/test_download.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/common/tests/test_download.py b/pyomo/common/tests/test_download.py index 4fde029f1b1..4ee781d5738 100644 --- a/pyomo/common/tests/test_download.py +++ b/pyomo/common/tests/test_download.py @@ -22,7 +22,7 @@ import pyomo.common.envvar as envvar from pyomo.common import DeveloperError -from pyomo.common.fileutils import this_file +from pyomo.common.fileutils import this_file, Executable from pyomo.common.download import FileDownloader, distro_available from pyomo.common.log import LoggingIntercept from pyomo.common.tee import capture_output @@ -173,7 +173,8 @@ def test_get_os_version(self): self.assertTrue(v.replace('.', '').startswith(dist_ver)) if ( - subprocess.run( + Executable('lsb_release').available() + and subprocess.run( ['lsb_release'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, From 0a87a535ef1c07ca00077b904a1d631d7092cf00 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:18:59 -0600 Subject: [PATCH 2129/3044] Add missing import --- pyomo/repn/plugins/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/repn/plugins/__init__.py b/pyomo/repn/plugins/__init__.py index d3804c55106..ffe131b9b8b 100644 --- a/pyomo/repn/plugins/__init__.py +++ b/pyomo/repn/plugins/__init__.py @@ -37,6 +37,8 @@ def load(): def activate_writer_version(name, ver): """DEBUGGING TOOL to switch the "default" writer implementation""" + from pyomo.opt import WriterFactory + doc = WriterFactory.doc(name) WriterFactory.unregister(name) WriterFactory.register(name, doc)(WriterFactory.get_class(f'{name}_v{ver}')) From 353fb6c66cce5bfdb2bbded0801d5166f309ebb6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:19:41 -0600 Subject: [PATCH 2130/3044] NFC: apply black --- pyomo/contrib/appsi/solvers/ipopt.py | 3 ++- pyomo/contrib/parmest/tests/test_examples.py | 4 ++-- pyomo/contrib/parmest/tests/test_parmest.py | 8 ++------ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/ipopt.py b/pyomo/contrib/appsi/solvers/ipopt.py index 97e8122fe78..af40d2e88d2 100644 --- a/pyomo/contrib/appsi/solvers/ipopt.py +++ b/pyomo/contrib/appsi/solvers/ipopt.py @@ -571,9 +571,10 @@ def get_reduced_costs( def has_linear_solver(self, linear_solver): import pyomo.core as AML from pyomo.common.tee import capture_output + m = AML.ConcreteModel() m.x = AML.Var() - m.o = AML.Objective(expr=(m.x-2)**2) + m.o = AML.Objective(expr=(m.x - 2) ** 2) with capture_output() as OUT: solver = self.__class__() solver.config.stream_solver = True diff --git a/pyomo/contrib/parmest/tests/test_examples.py b/pyomo/contrib/parmest/tests/test_examples.py index 552c568cc7c..3b0c869affa 100644 --- a/pyomo/contrib/parmest/tests/test_examples.py +++ b/pyomo/contrib/parmest/tests/test_examples.py @@ -18,6 +18,7 @@ ipopt_available = SolverFactory("ipopt").available() pynumero_ASL_available = AmplInterface.available() + @unittest.skipIf( not parmest.parmest_available, "Cannot test parmest: required dependencies are missing", @@ -71,8 +72,7 @@ def test_likelihood_ratio_example(self): @unittest.skipUnless(pynumero_ASL_available, "test requires libpynumero_ASL") @unittest.skipUnless(ipopt_available, "The 'ipopt' solver is not available") @unittest.skipUnless( - parmest.parmest_available, - "Cannot test parmest: required dependencies are missing", + parmest.parmest_available, "Cannot test parmest: required dependencies are missing" ) class TestReactionKineticsExamples(unittest.TestCase): @classmethod diff --git a/pyomo/contrib/parmest/tests/test_parmest.py b/pyomo/contrib/parmest/tests/test_parmest.py index fbe4290dd10..52b7cd390e8 100644 --- a/pyomo/contrib/parmest/tests/test_parmest.py +++ b/pyomo/contrib/parmest/tests/test_parmest.py @@ -22,12 +22,7 @@ import pyomo.environ as pyo import pyomo.dae as dae -from pyomo.common.dependencies import ( - numpy as np, - pandas as pd, - scipy, - matplotlib, -) +from pyomo.common.dependencies import numpy as np, pandas as pd, scipy, matplotlib from pyomo.common.fileutils import this_file_dir from pyomo.contrib.parmest.experiment import Experiment from pyomo.contrib.pynumero.asl import AmplInterface @@ -38,6 +33,7 @@ pynumero_ASL_available = AmplInterface.available() testdir = this_file_dir() + @unittest.skipIf( not parmest.parmest_available, "Cannot test parmest: required dependencies are missing", From 7ae66e33d29236caf37413106632d0f954fcfe05 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 17:36:25 -0600 Subject: [PATCH 2131/3044] fix UnknownSolver logic, missing import --- pyomo/scripting/driver_help.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pyomo/scripting/driver_help.py b/pyomo/scripting/driver_help.py index ce7c36932ec..45fdc711137 100644 --- a/pyomo/scripting/driver_help.py +++ b/pyomo/scripting/driver_help.py @@ -258,16 +258,18 @@ def help_solvers(): ver = '' except (AttributeError, NameError): pass - elif isinstance(s, UnknownSolver): + elif s == 'py': + # py is a metasolver, but since we don't specify a subsolver + # for this test, opt is actually an UnknownSolver, so we + # can't try to get the _metasolver attribute from it. + avail = '*' + elif isinstance(s, pyomo.opt.solvers.UnknownSolver): # We can get here if creating a registered # solver failed (i.e., an exception was raised # in __init__) avail = '' - elif s == 'py' or (hasattr(opt, "_metasolver") and opt._metasolver): - # py is a metasolver, but since we don't specify a subsolver - # for this test, opt is actually an UnknownSolver, so we - # can't try to get the _metasolver attribute from it. - # Also, default to False if the attribute isn't implemented + elif getattr(opt, "_metasolver", False): + # Note: default to False if the attribute isn't implemented avail = '*' else: avail = '' From b86ec2db69a0e7da398d7fc07aad633eae6a5b06 Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 6 Aug 2024 18:40:24 -0600 Subject: [PATCH 2132/3044] Reverting a recent change ... and adding some documentation to an OBBT test --- pyomo/contrib/alternative_solutions/obbt.py | 8 ++++---- pyomo/contrib/alternative_solutions/tests/test_obbt.py | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 4b48e4c6efe..4a887d74a3e 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -255,11 +255,11 @@ def obbt_analysis_bounds_and_solutions( if idx == 0: variable_bounds[var] = [None, None] + # NOTE: Simply setting the expr/sense values works differently with the APPSI solver if hasattr(aos_block, "var_objective"): - aos_block.var_objective.expr = var - aos_block.var_objective.sense = sense - else: - aos_block.var_objective = pe.Objective(expr=var, sense=sense) + aos_block.del_component("var_objective") + + aos_block.var_objective = pe.Objective(expr=var, sense=sense) if warmstart: _update_values(var, bound_dir, solutions) diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index 884ada6e885..184aa22a310 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -58,6 +58,9 @@ def test_obbt_analysis(self, mip_solver): assert_array_almost_equal(bounds, m.continuous_bounds[var]) def test_obbt_error1(self, mip_solver): + """ + ERROR: Cannot restrict variable list when warmstart is specified + """ m = tc.get_2d_diamond_problem() with unittest.pytest.raises(AssertionError): obbt_analysis_bounds_and_solutions(m, variables=[m.x], solver=mip_solver) From c28d103fefc7937d22c48203722749eb5ed54106 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 6 Aug 2024 20:35:45 -0500 Subject: [PATCH 2133/3044] Clean up init file --- pyomo/contrib/doe/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index 72cad1cabdb..775fc6fb47d 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -9,7 +9,4 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ from .doe import DesignOfExperiments, ObjectiveLib, FiniteDifferenceStep -from .tests import experiment_class_example, experiment_class_example_flags from .utils import rescale_FIM, get_parameters_from_suffix -from .examples import reactor_experiment, reactor_example, reactor_compute_factorial_FIM -from .experiment import Experiment From 52433f43d52eb94ecfa0be4cfe000d72fb922fc3 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 6 Aug 2024 20:41:23 -0500 Subject: [PATCH 2134/3044] Updating example with feedback --- .../doe/examples/reactor_compute_factorial_FIM.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py b/pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py index 0826a131bab..b6703606781 100644 --- a/pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py +++ b/pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py @@ -19,14 +19,14 @@ from pathlib import Path -# Example to run a DOE on the reactor +# Example to run a DoE on the reactor def run_reactor_doe(): # Read in file DATA_DIR = Path(__file__).parent file_path = DATA_DIR / "result.json" - f = open(file_path) - data_ex = json.load(f) + with open(file_path) as f: + data_ex = json.load(f) # Put temperature control time points into correct format for reactor experiment data_ex["control_points"] = { @@ -46,9 +46,9 @@ def run_reactor_doe(): scale_nominal_param_value = True # Create the DesignOfExperiments object - # We will not be passing any prior information in this example - # and allow the experiment object and the DesignOfExperiments - # call of ``run_doe`` perform model initialization. + # We will not be passing any prior information in this example. + # We also will rely on the initialization routine within + # the DesignOfExperiments class. doe_obj = DesignOfExperiments( experiment, fd_formula=fd_formula, From 0f79ae044118e3e7607bf09a89ea9bacd42bb05c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 22:27:46 -0600 Subject: [PATCH 2135/3044] Updating baseline due to new ipopt has_linear_solver method --- pyomo/contrib/solver/tests/unit/test_ipopt.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/solver/tests/unit/test_ipopt.py b/pyomo/contrib/solver/tests/unit/test_ipopt.py index cc459245506..9769eadecae 100644 --- a/pyomo/contrib/solver/tests/unit/test_ipopt.py +++ b/pyomo/contrib/solver/tests/unit/test_ipopt.py @@ -84,6 +84,7 @@ def test_class_member_list(self): 'CONFIG', 'config', 'available', + 'has_linear_solver', 'is_persistent', 'solve', 'version', From 2629cfdcdef022e11fa9458db883865e065ff38c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 6 Aug 2024 23:41:22 -0600 Subject: [PATCH 2136/3044] Additional test guard for ipopt availability --- pyomo/contrib/doe/tests/test_fim_doe.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py index d9a8d60fdb4..8891bd072a4 100644 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ b/pyomo/contrib/doe/tests/test_fim_doe.py @@ -35,6 +35,9 @@ VariablesWithIndices, ) from pyomo.contrib.doe.examples.reactor_kinetics import create_model, disc_for_measure +from pyomo.environ import SolverFactory + +ipopt_available = SolverFactory("ipopt").available() class TestMeasurementError(unittest.TestCase): @@ -196,6 +199,7 @@ def test(self): @unittest.skipIf(not numpy_available, "Numpy is not available") +@unittest.skipIf(not ipopt_available, "Numpy is not available") class TestPriorFIMError(unittest.TestCase): def test(self): # Control time set [h] From 3a11b6cff5839ba3af998944640e4a6382f89a79 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 7 Aug 2024 10:21:15 -0600 Subject: [PATCH 2137/3044] Adding placeholder for hamiltonian paths tests --- pyomo/contrib/piecewise/tests/test_triangulations.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index db17738b242..a88630c17a2 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -13,6 +13,9 @@ import itertools from unittest import skipUnless import pyomo.common.unittest as unittest +from pyomo.contrib.piecewise.ordered_3d_j1_triangulation_data import ( + get_hamiltonian_paths +) from pyomo.contrib.piecewise.triangulations import ( get_unordered_j1_triangulation, get_ordered_j1_triangulation, @@ -220,3 +223,7 @@ def test_grid_hamiltonian_paths(self): self.check_grid_hamiltonian(2, 8) self.check_grid_hamiltonian(3, 5) self.check_grid_hamiltonian(4, 3) + +class TestHamiltonianPaths(unittest.TestCase): + def test_hamiltonian_paths(self): + paths = get_hamiltonian_paths() From 257d24f858d2e740bcb1b6892e200df2c15b0823 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 7 Aug 2024 10:59:44 -0600 Subject: [PATCH 2138/3044] reverting original change to Set validation --- pyomo/core/base/set.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 27dd3b7c51c..d297ba890fb 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1410,18 +1410,7 @@ def add(self, *values): if self._validate is not None: try: - # differentiate between indexed and non-indexed sets - if self._index is not None: - # indexed set: the value and the index are given - if type(_value) == tuple: - # _value is a tuple: unpack it for the method arguments' tuple - flag = self._validate(_block, (*_value, self._index)) - else: - # _value is not a tuple: no need to unpack it for the method arguments' tuple - flag = self._validate(_block, (_value, self._index)) - else: - # non-indexed set: only the tentative member is given - flag = self._validate(_block, _value) + flag = self._validate(_block, _value) except: logger.error( "Exception raised while validating element '%s' " From 4143140163a5e1c787e5dcc03d894340450f4dee Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Aug 2024 21:17:16 -0400 Subject: [PATCH 2139/3044] Add further simplification of zero times expr --- pyomo/repn/parameterized_quadratic.py | 21 ++++- .../tests/test_parameterized_quadratic.py | 94 +++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 1dd93955825..edd662ec4a5 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -192,6 +192,17 @@ def is_zero(obj): return obj.__class__ in native_numeric_types and not obj +def is_zero_product(e1, e2): + """ + Return True if e1 is zero and e2 is not known to be an indeterminate + (e.g., NaN, inf), or vice versa, False otherwise. + """ + return ( + (is_zero(e1) and e2 == e2) + or (e1 == e1 and is_zero(e2)) + ) + + def is_equal_to(obj, val): return obj.__class__ in native_numeric_types and obj == val @@ -211,10 +222,16 @@ def _handle_product_linear_linear(visitor, node, arg1, arg2): for vid, coef in arg1.linear.items(): arg1.linear[vid] = c * coef if not is_zero(arg1.constant): + # TODO: what if a linear coefficient is indeterminate (nan/inf)? + # might that also affect nonlinear product handler? _merge_dict(arg1.linear, arg1.constant, arg2.linear) # Finally, the constant and multipliers - arg1.constant *= arg2.constant + if is_zero_product(arg1.constant, arg2.constant): + arg1.constant = 0 + else: + arg1.constant *= arg2.constant + arg1.multiplier *= arg2.multiplier return _QUADRATIC, arg1 @@ -232,7 +249,7 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): x1.multiplier = x2.multiplier = 1 # constant term [A1A2] - if is_zero(x1.constant) and is_zero(x2.constant): + if is_zero_product(x1.constant, x2.constant): ans.constant = 0 else: ans.constant = x1.constant * x2.constant diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index a9e1aebd9b2..6a1f0ff0b92 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1371,3 +1371,97 @@ def test_repr_parameterized_quadratic_repn(self): ) self.assertEqual(repr(repn), expected_repn_str) self.assertEqual(str(repn), expected_repn_str) + + def test_product_var_linear_wrt_yz(self): + """ + Test product of Var and quadratic expression. + + Aimed at testing what happens when one multiplicand + of a product + has a constant term of 0, and the other has a + constant term that is an expression. + """ + m = build_test_model() + expr = m.x * (m.y + m.x * m.y + m.z) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, 0) + self.assertEqual(len(repn.linear), 1) + assertExpressionsEqual( + self, repn.linear[id(m.x)], m.y + m.z + ) + self.assertEqual(len(repn.quadratic), 1) + assertExpressionsEqual(self, repn.quadratic[id(m.x), id(m.x)], m.y) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, repn.to_expression(visitor), m.y * m.x ** 2 + (m.y + m.z) * m.x, + ) + + def test_product_linear_var_wrt_yz(self): + """ + Test product of Var and quadratic expression. + + Checks what happens when multiplicands of + `test_product_var_linear` are swapped/commuted. + """ + m = build_test_model() + expr = (m.y + m.x * m.y + m.z) * m.x + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x}) + self.assertEqual(cfg.var_order, {id(m.x): 0}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, 0) + self.assertEqual(len(repn.linear), 1) + assertExpressionsEqual( + self, repn.linear[id(m.x)], m.y + m.z + ) + self.assertEqual(len(repn.quadratic), 1) + assertExpressionsEqual(self, repn.quadratic[id(m.x), id(m.x)], m.y) + self.assertIsNone(repn.nonlinear) + assertExpressionsEqual( + self, repn.to_expression(visitor), m.y * m.x ** 2 + (m.y + m.z) * m.x, + ) + + def test_product_var_quadratic(self): + """ + Test product of Var and quadratic expression. + + Aimed at testing what happens when one multiplicand + of a product + has a constant term of 0, and the other has a + constant term that is an expression. + """ + m = build_test_model() + expr = m.x * (m.y + m.x * m.y + m.z) + + cfg = VisitorConfig() + visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.z]) + repn = visitor.walk_expression(expr) + + self.assertEqual(cfg.subexpr, {}) + self.assertEqual(cfg.var_map, {id(m.x): m.x, id(m.y): m.y}) + self.assertEqual(cfg.var_order, {id(m.x): 0, id(m.y): 1}) + self.assertEqual(repn.multiplier, 1) + assertExpressionsEqual(self, repn.constant, 0) + self.assertEqual(len(repn.linear), 1) + assertExpressionsEqual(self, repn.linear[id(m.x)], m.z) + self.assertEqual(len(repn.quadratic), 1) + self.assertEqual(repn.quadratic, {(id(m.x), id(m.y)): 1}) + assertExpressionsEqual(self, repn.nonlinear, m.x * SumExpression([m.x * m.y])) + assertExpressionsEqual( + self, + repn.to_expression(visitor), + m.x * m.y + m.x * SumExpression([m.x * m.y]) + m.z * m.x, + ) From 91c59ade86c107aeef08ec6c5fe46e0f215b5d4a Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Aug 2024 21:38:49 -0400 Subject: [PATCH 2140/3044] Apply black --- pyomo/repn/parameterized_quadratic.py | 5 +---- pyomo/repn/tests/test_parameterized_quadratic.py | 12 ++++-------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index edd662ec4a5..0face2702c7 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -197,10 +197,7 @@ def is_zero_product(e1, e2): Return True if e1 is zero and e2 is not known to be an indeterminate (e.g., NaN, inf), or vice versa, False otherwise. """ - return ( - (is_zero(e1) and e2 == e2) - or (e1 == e1 and is_zero(e2)) - ) + return (is_zero(e1) and e2 == e2) or (e1 == e1 and is_zero(e2)) def is_equal_to(obj, val): diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index 6a1f0ff0b92..38f5f8ec8ad 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1394,14 +1394,12 @@ def test_product_var_linear_wrt_yz(self): self.assertEqual(repn.multiplier, 1) assertExpressionsEqual(self, repn.constant, 0) self.assertEqual(len(repn.linear), 1) - assertExpressionsEqual( - self, repn.linear[id(m.x)], m.y + m.z - ) + assertExpressionsEqual(self, repn.linear[id(m.x)], m.y + m.z) self.assertEqual(len(repn.quadratic), 1) assertExpressionsEqual(self, repn.quadratic[id(m.x), id(m.x)], m.y) self.assertIsNone(repn.nonlinear) assertExpressionsEqual( - self, repn.to_expression(visitor), m.y * m.x ** 2 + (m.y + m.z) * m.x, + self, repn.to_expression(visitor), m.y * m.x**2 + (m.y + m.z) * m.x ) def test_product_linear_var_wrt_yz(self): @@ -1424,14 +1422,12 @@ def test_product_linear_var_wrt_yz(self): self.assertEqual(repn.multiplier, 1) assertExpressionsEqual(self, repn.constant, 0) self.assertEqual(len(repn.linear), 1) - assertExpressionsEqual( - self, repn.linear[id(m.x)], m.y + m.z - ) + assertExpressionsEqual(self, repn.linear[id(m.x)], m.y + m.z) self.assertEqual(len(repn.quadratic), 1) assertExpressionsEqual(self, repn.quadratic[id(m.x), id(m.x)], m.y) self.assertIsNone(repn.nonlinear) assertExpressionsEqual( - self, repn.to_expression(visitor), m.y * m.x ** 2 + (m.y + m.z) * m.x, + self, repn.to_expression(visitor), m.y * m.x**2 + (m.y + m.z) * m.x ) def test_product_var_quadratic(self): From 12930fe61af1f48ac5500db4fda2e207b4c88a32 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 8 Aug 2024 06:48:01 -0600 Subject: [PATCH 2141/3044] Bug fix: Issue 3336 --- pyomo/opt/base/solvers.py | 4 ++-- pyomo/opt/tests/base/test_solver.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/opt/base/solvers.py b/pyomo/opt/base/solvers.py index c0698165603..4ffef7e7cac 100644 --- a/pyomo/opt/base/solvers.py +++ b/pyomo/opt/base/solvers.py @@ -470,8 +470,8 @@ def set_results_format(self, format): Set the current results format (if it's valid for the current problem format). """ - if (self._problem_format in self._valid_results_formats) and ( - format in self._valid_results_formats[self._problem_format] + if (self._problem_format in self._valid_result_formats) and ( + format in self._valid_result_formats[self._problem_format] ): self._results_format = format else: diff --git a/pyomo/opt/tests/base/test_solver.py b/pyomo/opt/tests/base/test_solver.py index 8ffc647804d..919e9375f60 100644 --- a/pyomo/opt/tests/base/test_solver.py +++ b/pyomo/opt/tests/base/test_solver.py @@ -109,7 +109,7 @@ def test_set_problem_format(self): def test_set_results_format(self): opt = pyomo.opt.SolverFactory("stest1") opt._valid_problem_formats = ['a'] - opt._valid_results_formats = {'a': 'b'} + opt._valid_result_formats = {'a': 'b'} self.assertEqual(opt.problem_format(), None) try: opt.set_results_format('b') From ebb2296b86787854f927b06d3e7e6512b60b7501 Mon Sep 17 00:00:00 2001 From: whart222 Date: Thu, 8 Aug 2024 08:39:10 -0600 Subject: [PATCH 2142/3044] Several changes 1. Adding documentation to logcontext() 2. Removing mis-use of logcontext. --- pyomo/contrib/alternative_solutions/aos_utils.py | 11 +++++++++++ pyomo/contrib/alternative_solutions/lp_enum.py | 9 ++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index 1f23834aa8f..23fa8e3b4f7 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -29,6 +29,17 @@ @contextmanager def logcontext(level): + """ + This context manager is used to dynamically set the specified logging level + and then execute a block of code using that logging level. When the context is + deleted, the logging level is reset to the original value. + + Examples + -------- + >>> with logcontext(logging.INFO): + >>> logging.debug("This will not be printed") + >>> logging.info("This will be printed") + """ logger = logging.getLogger() current_level = logger.getEffectiveLevel() logger.setLevel(level) diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index 8e16728e256..0d74ea490c3 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -20,7 +20,6 @@ solution, solnpool, ) -from pyomo.contrib.alternative_solutions.aos_utils import logcontext from pyomo.contrib import appsi @@ -230,7 +229,7 @@ def enumerate_linear_solutions( while solution_number <= num_solutions: logger.info("Solving Iteration {}: ".format(solution_number), end="") - with logcontext(logging.DEBUG): + if logger.isEnabledFor(logging.DEBUG): model.pprint() if use_appsi: results = opt.solve(model) @@ -251,13 +250,13 @@ def enumerate_linear_solutions( solutions.append(sol) orig_objective_value = sol.objective[1] - with logcontext(logging.INFO): + if logger.isEnabledFor(logging.INFO): logger.info("Solved, objective = {}".format(orig_objective_value)) for var, index in cb.var_map.items(): logger.info( "{} = {}".format(var.name, var.lb + cb.var_lower[index].value) ) - with logcontext(logging.DEBUG): + if logger.isEnabledFor(logging.DEBUG): model.display() if hasattr(cb, "force_out"): @@ -334,7 +333,7 @@ def enumerate_linear_solutions( ).format(results.solver.status.value, condition.value) ) break - with logcontext(logging.DEBUG): + if logger.isEnabledFor(logging.DEBUG): logging.debug("") logging.debug("=" * 80) logging.debug("") From ead2c5924ee548852572c574e82653917f4b5b90 Mon Sep 17 00:00:00 2001 From: whart222 Date: Thu, 8 Aug 2024 12:08:53 -0600 Subject: [PATCH 2143/3044] Removing deprecated TODO --- pyomo/contrib/alternative_solutions/obbt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 4a887d74a3e..e9a8310be1a 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -334,7 +334,6 @@ def obbt_analysis_bounds_and_solutions( iteration += 1 - # TODO - Remove this block aos_block.deactivate() orig_objective.activate() From 8e221084c950d3de84b8673f8ad32356bc4e66d1 Mon Sep 17 00:00:00 2001 From: whart222 Date: Thu, 8 Aug 2024 12:13:18 -0600 Subject: [PATCH 2144/3044] Raise an exception for models with binary vars --- pyomo/contrib/alternative_solutions/lp_enum_solnpool.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py index 6c707dbae53..8947fb806ee 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py @@ -140,8 +140,11 @@ def enumerate_linear_solutions_soln_pool( assert variables == None if variables == None: all_variables = aos_utils.get_model_variables(model) - - # TODO: Check if problem is continuous or mixed binary + for var in all_variables: + if var.is_binary(): + raise pyomo.common.errors.ApplicationError( + f"The enumerate_linear_solutions_soln_pool() function cannot be used with models that contain binary variables" + ) opt = pe.SolverFactory("gurobi") if not opt.available(): From a4946df48905ce94f3d8fc840374a03d30f5a599 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Thu, 8 Aug 2024 12:53:45 -0600 Subject: [PATCH 2145/3044] Update pyomo/contrib/doe/tests/test_fim_doe.py --- pyomo/contrib/doe/tests/test_fim_doe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py index 8891bd072a4..9cae2fe6278 100644 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ b/pyomo/contrib/doe/tests/test_fim_doe.py @@ -199,7 +199,7 @@ def test(self): @unittest.skipIf(not numpy_available, "Numpy is not available") -@unittest.skipIf(not ipopt_available, "Numpy is not available") +@unittest.skipIf(not ipopt_available, "ipopt is not available") class TestPriorFIMError(unittest.TestCase): def test(self): # Control time set [h] From 678b28d28b266b6844aab37770a2eca50de9f839 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Thu, 8 Aug 2024 19:01:36 -0500 Subject: [PATCH 2146/3044] Made imports explicit --- pyomo/contrib/doe/tests/test_doe_solve.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 6439b9f8c37..140e65288fc 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -8,6 +8,7 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import json import logging from pathlib import Path @@ -20,12 +21,12 @@ ) import pyomo.common.unittest as unittest -from pyomo.contrib.doe import * -from pyomo.contrib.doe.tests.experiment_class_example import * +from pyomo.contrib.doe import DesignOfExperiments +from pyomo.contrib.doe.tests.experiment_class_example import FullReactorExperiment from pyomo.contrib.doe.tests.experiment_class_example_flags import ( FullReactorExperimentBad, ) -from pyomo.contrib.doe.utils import * +from pyomo.contrib.doe.utils import rescale_FIM import pyomo.environ as pyo From 1920965df6933c8530b0bc88ebfcb71d266e4a07 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Fri, 9 Aug 2024 08:38:12 -0500 Subject: [PATCH 2147/3044] Made imports explicit in other tests --- pyomo/contrib/doe/tests/test_doe_build.py | 7 +++++-- pyomo/contrib/doe/tests/test_doe_errors.py | 8 ++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 6efe731c08b..d1eb3c11412 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -8,6 +8,7 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import json from pathlib import Path from pyomo.common.dependencies import ( @@ -18,8 +19,10 @@ ) import pyomo.common.unittest as unittest -from pyomo.contrib.doe import * -from pyomo.contrib.doe.tests.experiment_class_example import * +from pyomo.contrib.doe import DesignOfExperiments +from pyomo.contrib.doe.tests.experiment_class_example import FullReactorExperiment + +import pyomo.environ as pyo from pyomo.opt import SolverFactory diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index ee3b0cc27be..986cdcb4f5c 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -8,6 +8,7 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import json from pathlib import Path from pyomo.common.dependencies import ( @@ -18,8 +19,11 @@ ) import pyomo.common.unittest as unittest -from pyomo.contrib.doe import * -from pyomo.contrib.doe.tests.experiment_class_example_flags import * +from pyomo.contrib.doe import DesignOfExperiments +from pyomo.contrib.doe.tests.experiment_class_example_flags import ( + BadExperiment, + FullReactorExperiment, +) from pyomo.opt import SolverFactory From afd01bb0f8d5d463778e60e88ce157a07a8992d2 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Fri, 9 Aug 2024 09:17:02 -0500 Subject: [PATCH 2148/3044] Added shortcut function for test arguments --- pyomo/contrib/doe/tests/test_doe_build.py | 283 ++------- pyomo/contrib/doe/tests/test_doe_errors.py | 662 ++++----------------- pyomo/contrib/doe/tests/test_doe_solve.py | 305 +++------- 3 files changed, 247 insertions(+), 1003 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index d1eb3c11412..e35dab1bddb 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -93,6 +93,26 @@ def get_FIM_FIMPrior_Q_L(doe_obj=None): return FIM_vals_np, FIM_prior_vals_np, Q_vals_np, L_vals_np, sigma_inv_np +def get_standard_args(experiment, fd_method, obj_used): + args = {} + args['experiment'] = experiment + args['fd_formula'] = fd_method + args['step'] = 1e-3 + args['objective_option'] = obj_used + args['scale_constant_value'] = 1 + args['scale_nominal_param_value'] = True + args['prior_FIM'] = None + args['jac_initial'] = None + args['fim_initial'] = None + args['L_diagonal_lower_bound'] = 1e-7 + args['solver'] = None + args['tee'] = False + args['get_labeled_model_args'] = None + args['_Cholesky_option'] = True + args['_only_compute_fim_lower'] = True + return args + + class TestReactorExampleBuild(unittest.TestCase): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") @@ -102,23 +122,9 @@ def test_reactor_fd_central_check_fd_eqns(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.create_doe_model() @@ -157,23 +163,9 @@ def test_reactor_fd_backward_check_fd_eqns(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.create_doe_model() @@ -214,23 +206,9 @@ def test_reactor_fd_forward_check_fd_eqns(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.create_doe_model() @@ -271,23 +249,9 @@ def test_reactor_fd_central_design_fixing(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.create_doe_model() @@ -321,23 +285,9 @@ def test_reactor_fd_backward_design_fixing(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.create_doe_model() @@ -371,23 +321,9 @@ def test_reactor_fd_forward_design_fixing(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.create_doe_model() @@ -425,23 +361,12 @@ def test_reactor_check_user_initialization(self): FIM_initial = np.eye(4) + FIM_prior JAC_initial = np.ones((27, 4)) * 2 - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=FIM_prior, - jac_initial=JAC_initial, - fim_initial=FIM_initial, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + DoE_args['prior_FIM'] = FIM_prior + DoE_args['fim_initial'] = FIM_initial + DoE_args['jac_initial'] = JAC_initial + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.create_doe_model() @@ -463,23 +388,9 @@ def test_update_FIM(self): FIM_update = np.ones((4, 4)) * 10 - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.create_doe_model() @@ -499,23 +410,9 @@ def test_get_experiment_inputs_without_blocks(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.compute_FIM(method="sequential") @@ -533,23 +430,9 @@ def test_get_experiment_outputs_without_blocks(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.compute_FIM(method="sequential") @@ -567,23 +450,9 @@ def test_get_measurement_error_without_blocks(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.compute_FIM(method="sequential") @@ -601,23 +470,9 @@ def test_get_unknown_parameters_without_blocks(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.compute_FIM(method="sequential") @@ -636,23 +491,9 @@ def test_generate_blocks_without_model(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj._generate_scenario_blocks() diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 986cdcb4f5c..2aa7886a65e 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -37,6 +37,26 @@ data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} +def get_standard_args(experiment, fd_method, obj_used, flag): + args = {} + args['experiment'] = experiment + args['fd_formula'] = fd_method + args['step'] = 1e-3 + args['objective_option'] = obj_used + args['scale_constant_value'] = 1 + args['scale_nominal_param_value'] = True + args['prior_FIM'] = None + args['jac_initial'] = None + args['fim_initial'] = None + args['L_diagonal_lower_bound'] = 1e-7 + args['solver'] = None + args['tee'] = False + args['get_labeled_model_args'] = {"flag": flag} + args['_Cholesky_option'] = True + args['_only_compute_fim_lower'] = True + return args + + class TestReactorExampleErrors(unittest.TestCase): @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_no_get_labeled_model(self): @@ -50,23 +70,9 @@ def test_reactor_check_no_get_labeled_model(self): ValueError, "The experiment object must have a ``get_labeled_model`` function", ): - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_no_experiment_outputs(self): @@ -76,23 +82,9 @@ def test_reactor_check_no_experiment_outputs(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( RuntimeError, @@ -108,23 +100,9 @@ def test_reactor_check_no_measurement_error(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( RuntimeError, @@ -140,23 +118,9 @@ def test_reactor_check_no_experiment_inputs(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( RuntimeError, @@ -172,23 +136,9 @@ def test_reactor_check_no_unknown_parameters(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( RuntimeError, @@ -206,23 +156,10 @@ def test_reactor_check_bad_prior_size(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=prior_FIM, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + DoE_args['prior_FIM'] = prior_FIM + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( ValueError, @@ -242,23 +179,10 @@ def test_reactor_check_bad_jacobian_init_size(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=jac_init, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + DoE_args['jac_initial'] = jac_init + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( ValueError, @@ -278,23 +202,9 @@ def test_reactor_check_unbuilt_update_FIM(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( RuntimeError, @@ -312,23 +222,9 @@ def test_reactor_check_none_update_FIM(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( ValueError, @@ -344,23 +240,9 @@ def test_reactor_check_results_file_name(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( ValueError, "``results_file`` must be either a Path object or a string." @@ -377,23 +259,9 @@ def test_reactor_check_measurement_and_output_length_match(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( ValueError, @@ -412,23 +280,9 @@ def test_reactor_grid_search_des_range_inputs(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag=0) + + doe_obj = DesignOfExperiments(**DoE_args) design_ranges = {"not": [1, 5, 3], "correct": [300, 700, 3]} @@ -449,23 +303,9 @@ def test_reactor_premature_figure_drawing(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag=0) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( RuntimeError, @@ -482,23 +322,9 @@ def test_reactor_figure_drawing_no_des_var_names(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag=0) + + doe_obj = DesignOfExperiments(**DoE_args) design_ranges = {"CA[0]": [1, 5, 2], "T[0]": [300, 700, 2]} @@ -521,23 +347,9 @@ def test_reactor_figure_drawing_no_sens_names(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag=0) + + doe_obj = DesignOfExperiments(**DoE_args) design_ranges = {"CA[0]": [1, 5, 2], "T[0]": [300, 700, 2]} @@ -559,23 +371,9 @@ def test_reactor_figure_drawing_no_fixed_names(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag=0) + + doe_obj = DesignOfExperiments(**DoE_args) design_ranges = {"CA[0]": [1, 5, 2], "T[0]": [300, 700, 2]} @@ -597,23 +395,9 @@ def test_reactor_figure_drawing_bad_fixed_names(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag=0) + + doe_obj = DesignOfExperiments(**DoE_args) design_ranges = {"CA[0]": [1, 5, 2], "T[0]": [300, 700, 2]} @@ -639,23 +423,9 @@ def test_reactor_figure_drawing_bad_sens_names(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag=0) + + doe_obj = DesignOfExperiments(**DoE_args) design_ranges = {"CA[0]": [1, 5, 2], "T[0]": [300, 700, 2]} @@ -682,23 +452,9 @@ def test_reactor_check_get_FIM_without_FIM(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( RuntimeError, @@ -716,23 +472,9 @@ def test_reactor_check_get_sens_mat_without_model(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( RuntimeError, @@ -750,23 +492,9 @@ def test_reactor_check_get_exp_inputs_without_model(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( RuntimeError, @@ -784,23 +512,9 @@ def test_reactor_check_get_exp_outputs_without_model(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( RuntimeError, @@ -818,23 +532,9 @@ def test_reactor_check_get_unknown_params_without_model(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( RuntimeError, @@ -852,23 +552,9 @@ def test_reactor_check_get_meas_error_without_model(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( RuntimeError, @@ -886,23 +572,9 @@ def test_multiple_exp_not_implemented_seq(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( NotImplementedError, "Multiple experiment optimization not yet supported." @@ -919,23 +591,9 @@ def test_multiple_exp_not_implemented_sim(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( NotImplementedError, "Multiple experiment optimization not yet supported." @@ -952,23 +610,9 @@ def test_update_unknown_parameter_values_not_implemented_seq(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( NotImplementedError, "Updating unknown parameter values not yet supported." @@ -986,23 +630,9 @@ def test_bad_FD_generate_scens(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( AttributeError, @@ -1022,23 +652,9 @@ def test_bad_FD_seq_compute_FIM(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( AttributeError, @@ -1057,23 +673,9 @@ def test_bad_objective(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( AttributeError, @@ -1092,23 +694,9 @@ def test_no_model_for_objective(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( RuntimeError, @@ -1127,23 +715,9 @@ def test_bad_compute_FIM_option(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args={"flag": flag_val}, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used, flag_val) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( ValueError, diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 140e65288fc..3870cd9344c 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -95,6 +95,26 @@ def get_FIM_Q_L(doe_obj=None): return FIM_vals_np, Q_vals_np, L_vals_np, sigma_inv_np +def get_standard_args(experiment, fd_method, obj_used): + args = {} + args['experiment'] = experiment + args['fd_formula'] = fd_method + args['step'] = 1e-3 + args['objective_option'] = obj_used + args['scale_constant_value'] = 1 + args['scale_nominal_param_value'] = True + args['prior_FIM'] = None + args['jac_initial'] = None + args['fim_initial'] = None + args['L_diagonal_lower_bound'] = 1e-7 + args['solver'] = None + args['tee'] = False + args['get_labeled_model_args'] = None + args['_Cholesky_option'] = True + args['_only_compute_fim_lower'] = True + return args + + class TestReactorExampleSolving(unittest.TestCase): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not numpy_available, "Numpy is not available") @@ -104,23 +124,9 @@ def test_reactor_fd_central_solve(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.run_doe() @@ -143,23 +149,9 @@ def test_reactor_fd_forward_solve(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.run_doe() @@ -181,23 +173,9 @@ def test_reactor_fd_backward_solve(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.run_doe() @@ -221,23 +199,14 @@ def test_reactor_obj_det_solve(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=False, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=False, - _only_compute_fim_lower=False, + DoE_args = get_standard_args(experiment, fd_method, obj_used) + DoE_args['scale_nominal_param_value'] = ( + False # Vanilla determinant solve needs this ) + DoE_args['_Cholesky_option'] = False + DoE_args['_only_compute_fim_lower'] = False + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.run_doe() @@ -251,23 +220,9 @@ def test_reactor_obj_cholesky_solve(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.run_doe() @@ -290,23 +245,9 @@ def test_compute_FIM_seq_centr(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.compute_FIM(method="sequential") @@ -318,23 +259,9 @@ def test_compute_FIM_seq_forward(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.compute_FIM(method="sequential") @@ -350,23 +277,9 @@ def test_compute_FIM_kaug(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.compute_FIM(method="kaug") @@ -378,23 +291,9 @@ def test_compute_FIM_seq_backward(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) doe_obj.compute_FIM(method="sequential") @@ -407,23 +306,9 @@ def test_reactor_grid_search(self): experiment = FullReactorExperiment(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) design_ranges = {"CA[0]": [1, 5, 3], "T[0]": [300, 700, 3]} @@ -453,43 +338,15 @@ def test_rescale_FIM(self): experiment = FullReactorExperiment(data_ex, 10, 3) # With parameter scaling - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) # Without parameter scaling - doe_obj2 = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=False, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args2 = get_standard_args(experiment, fd_method, obj_used) + DoE_args2['scale_nominal_param_value'] = False + doe_obj2 = DesignOfExperiments(**DoE_args2) # Run both problems doe_obj.run_doe() doe_obj2.run_doe() @@ -523,23 +380,9 @@ def test_reactor_solve_bad_model(self): experiment = FullReactorExperimentBad(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + + doe_obj = DesignOfExperiments(**DoE_args) with self.assertRaisesRegex( RuntimeError, @@ -556,24 +399,10 @@ def test_reactor_grid_search_bad_model(self): experiment = FullReactorExperimentBad(data_ex, 10, 3) - doe_obj = DesignOfExperiments( - experiment, - fd_formula=fd_method, - step=1e-3, - objective_option=obj_used, - scale_constant_value=1, - scale_nominal_param_value=True, - prior_FIM=None, - jac_initial=None, - fim_initial=None, - L_diagonal_lower_bound=1e-7, - solver=None, - tee=False, - get_labeled_model_args=None, - _Cholesky_option=True, - _only_compute_fim_lower=True, - logger_level=logging.ERROR, - ) + DoE_args = get_standard_args(experiment, fd_method, obj_used) + DoE_args['logger_level'] = logging.ERROR + + doe_obj = DesignOfExperiments(**DoE_args) design_ranges = {"CA[0]": [1, 5, 3], "T[0]": [300, 700, 3]} From b66a6c509964fe0964b0dd231a46f73960833460 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Fri, 9 Aug 2024 09:21:25 -0500 Subject: [PATCH 2149/3044] Simplified decorator to class --- pyomo/contrib/doe/tests/test_doe_build.py | 28 ++--------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index e35dab1bddb..50b7498a6d0 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -113,9 +113,9 @@ def get_standard_args(experiment, fd_method, obj_used): return args +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +@unittest.skipIf(not numpy_available, "Numpy is not available") class TestReactorExampleBuild(unittest.TestCase): - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_central_check_fd_eqns(self): fd_method = "central" obj_used = "trace" @@ -155,8 +155,6 @@ def test_reactor_fd_central_check_fd_eqns(self): assert np.isclose(param_val, param_val_from_step) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_backward_check_fd_eqns(self): fd_method = "backward" obj_used = "trace" @@ -198,8 +196,6 @@ def test_reactor_fd_backward_check_fd_eqns(self): other_param_val = pyo.value(k) assert np.isclose(other_param_val, v) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_forward_check_fd_eqns(self): fd_method = "forward" obj_used = "trace" @@ -241,8 +237,6 @@ def test_reactor_fd_forward_check_fd_eqns(self): other_param_val = pyo.value(k) assert np.isclose(other_param_val, v) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_central_design_fixing(self): fd_method = "central" obj_used = "trace" @@ -277,8 +271,6 @@ def test_reactor_fd_central_design_fixing(self): # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) assert not hasattr(model, con_name_base + str(len(design_vars))) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_backward_design_fixing(self): fd_method = "backward" obj_used = "trace" @@ -313,8 +305,6 @@ def test_reactor_fd_backward_design_fixing(self): # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) assert not hasattr(model, con_name_base + str(len(design_vars))) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_forward_design_fixing(self): fd_method = "forward" obj_used = "trace" @@ -349,8 +339,6 @@ def test_reactor_fd_forward_design_fixing(self): # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) assert not hasattr(model, con_name_base + str(len(design_vars))) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_user_initialization(self): fd_method = "central" obj_used = "determinant" @@ -378,8 +366,6 @@ def test_reactor_check_user_initialization(self): assert np.array_equal(FIM_prior, FIM_prior_model) assert np.array_equal(JAC_initial, Q) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_update_FIM(self): fd_method = "forward" obj_used = "trace" @@ -402,8 +388,6 @@ def test_update_FIM(self): # Make sure they match the inputs we gave assert np.array_equal(FIM_update, FIM_prior_model) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_get_experiment_inputs_without_blocks(self): fd_method = "forward" obj_used = "trace" @@ -422,8 +406,6 @@ def test_get_experiment_inputs_without_blocks(self): [k.name for k, v in doe_obj.compute_FIM_model.experiment_inputs.items()] ) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_get_experiment_outputs_without_blocks(self): fd_method = "forward" obj_used = "trace" @@ -442,8 +424,6 @@ def test_get_experiment_outputs_without_blocks(self): [k.name for k, v in doe_obj.compute_FIM_model.experiment_outputs.items()] ) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_get_measurement_error_without_blocks(self): fd_method = "forward" obj_used = "trace" @@ -462,8 +442,6 @@ def test_get_measurement_error_without_blocks(self): [k.name for k, v in doe_obj.compute_FIM_model.measurement_error.items()] ) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_get_unknown_parameters_without_blocks(self): fd_method = "forward" obj_used = "trace" @@ -483,8 +461,6 @@ def test_get_unknown_parameters_without_blocks(self): [k.name for k, v in doe_obj.compute_FIM_model.unknown_parameters.items()] ) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_generate_blocks_without_model(self): fd_method = "forward" obj_used = "trace" From 0855619fb68261e87ab3f03791032fa9b610e931 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 12:59:05 -0600 Subject: [PATCH 2150/3044] Add testing of ipopt.has_linear_solver() --- pyomo/contrib/appsi/tests/test_ipopt.py | 42 +++++++++++++++++++ pyomo/contrib/solver/tests/unit/test_ipopt.py | 23 ++++++++++ pyomo/solvers/plugins/solvers/IPOPT.py | 8 +++- pyomo/solvers/tests/checks/test_ipopt.py | 42 +++++++++++++++++++ 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 pyomo/contrib/appsi/tests/test_ipopt.py create mode 100644 pyomo/solvers/tests/checks/test_ipopt.py diff --git a/pyomo/contrib/appsi/tests/test_ipopt.py b/pyomo/contrib/appsi/tests/test_ipopt.py new file mode 100644 index 00000000000..b3697b9b233 --- /dev/null +++ b/pyomo/contrib/appsi/tests/test_ipopt.py @@ -0,0 +1,42 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.contrib.appsi.solvers import ipopt + + +ipopt_available = ipopt.Ipopt().available() + + +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestIpoptInterface(unittest.TestCase): + def test_has_linear_solver(self): + opt = ipopt.Ipopt() + self.assertTrue( + any( + map( + opt.has_linear_solver, + [ + 'mumps', + 'ma27', + 'ma57', + 'ma77', + 'ma86', + 'ma97', + 'pardiso', + 'pardisomkl', + 'spral', + 'wsmp', + ], + ) + ) + ) + self.assertFalse(opt.has_linear_solver('bogus_linear_solver')) diff --git a/pyomo/contrib/solver/tests/unit/test_ipopt.py b/pyomo/contrib/solver/tests/unit/test_ipopt.py index 9769eadecae..27a80feede0 100644 --- a/pyomo/contrib/solver/tests/unit/test_ipopt.py +++ b/pyomo/contrib/solver/tests/unit/test_ipopt.py @@ -168,6 +168,29 @@ def test_write_options_file(self): data = f.readlines() self.assertEqual(len(data), len(list(opt.config.solver_options.keys()))) + def test_has_linear_solver(self): + opt = ipopt.Ipopt() + self.assertTrue( + any( + map( + opt.has_linear_solver, + [ + 'mumps', + 'ma27', + 'ma57', + 'ma77', + 'ma86', + 'ma97', + 'pardiso', + 'pardisomkl', + 'spral', + 'wsmp', + ], + ) + ) + ) + self.assertFalse(opt.has_linear_solver('bogus_linear_solver')) + def test_create_command_line(self): opt = ipopt.Ipopt() # No custom options, no file created. Plain and simple. diff --git a/pyomo/solvers/plugins/solvers/IPOPT.py b/pyomo/solvers/plugins/solvers/IPOPT.py index be0f143ea46..82dcfdb75a0 100644 --- a/pyomo/solvers/plugins/solvers/IPOPT.py +++ b/pyomo/solvers/plugins/solvers/IPOPT.py @@ -14,6 +14,7 @@ from pyomo.common import Executable from pyomo.common.collections import Bunch +from pyomo.common.errors import ApplicationError from pyomo.common.tee import capture_output from pyomo.common.tempfiles import TempfileManager @@ -215,6 +216,9 @@ def has_linear_solver(self, linear_solver): m = AML.ConcreteModel() m.x = AML.Var() m.o = AML.Objective(expr=(m.x - 2) ** 2) - with capture_output() as OUT: - self.solve(m, tee=True, options={'linear_solver': linear_solver}) + try: + with capture_output() as OUT: + self.solve(m, tee=True, options={'linear_solver': linear_solver}) + except ApplicationError: + return False return 'running with linear solver' in OUT.getvalue() diff --git a/pyomo/solvers/tests/checks/test_ipopt.py b/pyomo/solvers/tests/checks/test_ipopt.py new file mode 100644 index 00000000000..b7d00c35a6f --- /dev/null +++ b/pyomo/solvers/tests/checks/test_ipopt.py @@ -0,0 +1,42 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest +from pyomo.solvers.plugins.solvers import IPOPT +import pyomo.environ + +ipopt_available = IPOPT.IPOPT().available() + + +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +class TestIpoptInterface(unittest.TestCase): + def test_has_linear_solver(self): + opt = IPOPT.IPOPT() + self.assertTrue( + any( + map( + opt.has_linear_solver, + [ + 'mumps', + 'ma27', + 'ma57', + 'ma77', + 'ma86', + 'ma97', + 'pardiso', + 'pardisomkl', + 'spral', + 'wsmp', + ], + ) + ) + ) + self.assertFalse(opt.has_linear_solver('bogus_linear_solver')) From cbd702dadd80e218a8e8597e511c57b2d0b449a9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 13:14:13 -0600 Subject: [PATCH 2151/3044] Add active_writer_version, test writer activation functions --- pyomo/repn/plugins/__init__.py | 15 ++++++++++ pyomo/repn/tests/test_plugins.py | 50 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 pyomo/repn/tests/test_plugins.py diff --git a/pyomo/repn/plugins/__init__.py b/pyomo/repn/plugins/__init__.py index ffe131b9b8b..4029f44a03d 100644 --- a/pyomo/repn/plugins/__init__.py +++ b/pyomo/repn/plugins/__init__.py @@ -42,3 +42,18 @@ def activate_writer_version(name, ver): doc = WriterFactory.doc(name) WriterFactory.unregister(name) WriterFactory.register(name, doc)(WriterFactory.get_class(f'{name}_v{ver}')) + + +def active_writer_version(name): + """DEBUGGING TOOL to switch the "default" writer implementation""" + from pyomo.opt import WriterFactory + + ref = WriterFactory.get_class(name) + ver = 1 + try: + while 1: + if WriterFactory.get_class(f'{name}_v{ver}') is ref: + return ver + ver += 1 + except KeyError: + return None diff --git a/pyomo/repn/tests/test_plugins.py b/pyomo/repn/tests/test_plugins.py new file mode 100644 index 00000000000..fa6026ea74e --- /dev/null +++ b/pyomo/repn/tests/test_plugins.py @@ -0,0 +1,50 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.common import unittest + +from pyomo.opt import WriterFactory +from pyomo.repn.plugins import activate_writer_version, active_writer_version + +import pyomo.environ + + +class TestPlugins(unittest.TestCase): + def test_active(self): + with self.assertRaises(KeyError): + active_writer_version('nonexistant_writer') + ver = active_writer_version('lp') + self.assertIs( + WriterFactory.get_class('lp'), WriterFactory.get_class(f'lp_v{ver}') + ) + + class TMP(object): + pass + + WriterFactory.register('test_writer')(TMP) + try: + self.assertIsNone(active_writer_version('test_writer')) + finally: + WriterFactory.unregister('test_writer') + + def test_activate(self): + ver = active_writer_version('lp') + try: + activate_writer_version('lp', 2) + self.assertIs( + WriterFactory.get_class('lp'), WriterFactory.get_class(f'lp_v2') + ) + activate_writer_version('lp', 1) + self.assertIs( + WriterFactory.get_class('lp'), WriterFactory.get_class(f'lp_v1') + ) + finally: + activate_writer_version('lp', ver) From 658dab36b72f3bd18759dad286f5e20b4ec936e7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 13:29:54 -0600 Subject: [PATCH 2152/3044] NFC: fix typo --- pyomo/repn/tests/test_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/tests/test_plugins.py b/pyomo/repn/tests/test_plugins.py index fa6026ea74e..1152131f6b6 100644 --- a/pyomo/repn/tests/test_plugins.py +++ b/pyomo/repn/tests/test_plugins.py @@ -20,7 +20,7 @@ class TestPlugins(unittest.TestCase): def test_active(self): with self.assertRaises(KeyError): - active_writer_version('nonexistant_writer') + active_writer_version('nonexistent_writer') ver = active_writer_version('lp') self.assertIs( WriterFactory.get_class('lp'), WriterFactory.get_class(f'lp_v{ver}') From 12227d012b0346c78e64b6d58f5c55855573b684 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 18:12:43 -0600 Subject: [PATCH 2153/3044] Move InitializerBase to leverage AutoSlots --- pyomo/core/base/initializer.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/pyomo/core/base/initializer.py b/pyomo/core/base/initializer.py index c87a4236abe..e498b711ce8 100644 --- a/pyomo/core/base/initializer.py +++ b/pyomo/core/base/initializer.py @@ -16,6 +16,7 @@ from collections.abc import Sequence from collections.abc import Mapping +from pyomo.common.autoslots import AutoSlots from pyomo.common.dependencies import numpy, numpy_available, pandas, pandas_available from pyomo.common.modeling import NOTSET from pyomo.core.pyomoobject import PyomoObject @@ -193,27 +194,13 @@ def Initializer( return ConstantInitializer(arg) -class InitializerBase(object): +class InitializerBase(AutoSlots.Mixin, object): """Base class for all Initializer objects""" __slots__ = () verified = False - def __getstate__(self): - """Class serializer - - This class must declare __getstate__ because it is slotized. - This implementation should be sufficient for simple derived - classes (where __slots__ are only declared on the most derived - class). - """ - return {k: getattr(self, k) for k in self.__slots__} - - def __setstate__(self, state): - for key, val in state.items(): - object.__setattr__(self, key, val) - def constant(self): """Return True if this initializer is constant across all indices""" return False From 52be7a7e6f550adf949cd3212ed649bcef99f412 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 18:13:23 -0600 Subject: [PATCH 2154/3044] Support parameterized initializer functions (that take additional arguments) --- pyomo/core/base/initializer.py | 92 ++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/pyomo/core/base/initializer.py b/pyomo/core/base/initializer.py index e498b711ce8..946d6fd3167 100644 --- a/pyomo/core/base/initializer.py +++ b/pyomo/core/base/initializer.py @@ -38,6 +38,7 @@ def Initializer( allow_generators=False, treat_sequences_as_mappings=True, arg_not_specified=None, + additional_args=0, ): """Standardized processing of Component keyword arguments @@ -70,9 +71,51 @@ def Initializer( If ``arg`` is ``arg_not_specified``, then the function will return None (and not an InitializerBase object). + additional_args: int + + The number of additional arguments that will be passed to any + function calls (provided *before* the index value). + """ if arg is arg_not_specified: return None + if additional_args: + if arg.__class__ in function_types: + if allow_generators or inspect.isgeneratorfunction(arg): + raise ValueError( + "Generator functions are not allowed when prassing additional args" + ) + _args = inspect.getfullargspec(arg) + _nargs = len(_args.args) + if inspect.ismethod(arg) and arg.__self__ is not None: + # Ignore 'self' for bound instance methods and 'cls' for + # @classmethods + _nargs -= 1 + if _nargs == 1 + additional_args and _args.varargs is None: + return ParameterizedScalarCallInitializer(arg, constant=True) + else: + return ParameterizedIndexedCallInitializer(arg) + else: + base_initializer = Initializer( + arg=arg, + allow_generators=allow_generators, + treat_sequences_as_mappings=treat_sequences_as_mappings, + arg_not_specified=arg_not_specified, + ) + if arg.__class__ in function_types: + # This is an edge case: if we are providing additional + # args, but this is the first time we are seeing a + # callable type, we will (potentially) incorrectly + # categorize this as an IndexedCallInitializer. Re-try + # now that we know this is a function_type. + return Initializer( + arg=arg, + allow_generators=allow_generators, + treat_sequences_as_mappings=treat_sequences_as_mappings, + arg_not_specified=arg_not_specified, + additional_args=additional_args, + ) + return ParameterizedInitializer(base_initializer) if arg.__class__ in initializer_map: return initializer_map[arg.__class__](arg) if arg.__class__ in sequence_types: @@ -303,6 +346,18 @@ def __call__(self, parent, idx): return self._fcn(parent, idx) +class ParameterizedIndexedCallInitializer(IndexedCallInitializer): + """IndexedCallCallInitializer that accepts additional arguments""" + + __slots__ = () + + def __call__(self, parent, idx, *args): + if idx.__class__ is tuple: + return self._fcn(parent, *args, *idx) + else: + return self._fcn(parent, *args, idx) + + class CountedCallGenerator(object): """Generator implementing the "counted call" initialization scheme @@ -429,6 +484,15 @@ def constant(self): return self._constant +class ParameterizedScalarCallInitializer(ScalarCallInitializer): + """ScalarCallInitializer that accepts additional arguments""" + + __slots__ = () + + def __call__(self, parent, idx, *args): + return self._fcn(parent, *args) + + class DefaultInitializer(InitializerBase): """Initializer wrapper that maps exceptions to default values. @@ -472,6 +536,34 @@ def indices(self): return self._initializer.indices() +class ParameterizedInitializer(InitializerBase): + """Base class for all Initializer objects""" + + __slots__ = ('_base_initializer',) + + def __init__(self, base): + self._base_initializer = base + + def constant(self): + """Return True if this initializer is constant across all indices""" + return self._base_initializer.constant() + + def contains_indices(self): + """Return True if this initializer contains embedded indices""" + return self._base_initializer.contains_indices() + + def indices(self): + """Return a generator over the embedded indices + + This will raise a RuntimeError if this initializer does not + contain embedded indices + """ + return self._base_initializer.indices() + + def __call__(self, parent, idx, *args): + return self._base_initializer(parent, idx)(*args) + + _bound_sequence_types = collections.defaultdict(None.__class__) From 8f6b9ba570613e7d3c1a92abd7df27f7050b4568 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 18:14:32 -0600 Subject: [PATCH 2155/3044] Revert changes to old tests/examples --- examples/pyomo/tutorials/set.py | 2 +- pyomo/core/tests/unit/test_set.py | 4 ++-- pyomo/core/tests/unit/test_sets.py | 8 +++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/examples/pyomo/tutorials/set.py b/examples/pyomo/tutorials/set.py index 78d5f78cda1..9acdf35460c 100644 --- a/examples/pyomo/tutorials/set.py +++ b/examples/pyomo/tutorials/set.py @@ -183,7 +183,7 @@ def P_init(model, i, j): # Validation of set arrays can also be performed with the _validate_ option. # This is applied to all sets in the array: # -def T_validate(model, value, index): +def T_validate(model, value): return value in model.A diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 37a78bc9a56..6b71201d287 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4328,7 +4328,7 @@ def _lt_3(model, i): m = ConcreteModel() - def _validate_I(model, i, j): + def _validate(model, i, j): self.assertIs(model, m) if i + j < 2: return True @@ -4336,7 +4336,7 @@ def _validate_I(model, i, j): return False raise RuntimeError("Bogus value") - m.I = Set(validate=_validate_I) + m.I = Set(validate=_validate) output = StringIO() with LoggingIntercept(output, 'pyomo.core'): self.assertTrue(m.I.add((0, 1))) diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index bd168c7c279..52c4523eaba 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2732,7 +2732,7 @@ def test_validation1(self): # Create A with an error # self.model.Z = Set() - self.model.A = Set(self.model.Z, validate=lambda model, x, i: x < 6) + self.model.A = Set(self.model.Z, validate=lambda model, x: x < 6) with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance(currdir + "setA.dat") @@ -2864,7 +2864,7 @@ def test_other1(self): self.model.A = Set( self.model.Z, initialize={'A': [1, 2, 3, 'A']}, - validate=lambda model, x, i: x in Integers, + validate=lambda model, x: x in Integers, ) with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance() @@ -2887,9 +2887,7 @@ def tmp_init(model, i): self.model.n = Param(initialize=5) self.model.Z = Set(initialize=['A']) self.model.A = Set( - self.model.Z, - initialize=tmp_init, - validate=lambda model, x, i: x in Integers, + self.model.Z, initialize=tmp_init, validate=lambda model, x: x in Integers ) with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance() From f2a67ee95c607f5fb9a45404d302a1dd61a3bc58 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 18:18:46 -0600 Subject: [PATCH 2156/3044] Switch Set to use Initializer to process validate callback --- pyomo/core/base/set.py | 164 +++++++++++++++++++++++++++-------------- 1 file changed, 109 insertions(+), 55 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 0b1aab643cd..1eccb6e3624 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -40,10 +40,13 @@ ) from pyomo.core.base.disable_methods import disable_methods from pyomo.core.base.initializer import ( - InitializerBase, - Initializer, CountedCallInitializer, IndexedCallInitializer, + Initializer, + InitializerBase, + ParameterizedIndexedCallInitializer, + ParameterizedInitializer, + ParameterizedScalarCallInitializer, ) from pyomo.core.base.range import ( NumericRange, @@ -1428,7 +1431,7 @@ def update(self, values): val_iter = filter(partial(self._filter, self.parent_block()), val_iter) if self._validate is not None: - val_iter = self._cb_validate(self._validate, self.parent_block(), val_iter) + val_iter = self._cb_validate_filter('validate', val_iter) # We wrap this check in a try-except because some values # (like lists) are not hashable and can raise exceptions. @@ -1456,22 +1459,82 @@ def _cb_check_set_end(self, val_iter): return yield value - def _cb_validate(self, validate, block, val_iter): + def _cb_validate_filter(self, mode, val_iter): + failFalse = mode == 'validate' + fcn = getattr(self, '_' + mode) + block = self.parent_block() + idx = self.index() for value in val_iter: try: - flag = validate(block, value) - except: - logger.error( - "Exception raised while validating element '%s' " - "for Set %s" % (value, self.name) - ) - raise - if not flag: - raise ValueError( - "The value=%s violates the validation rule of Set %s" - % (value, self.name) - ) - yield value + flag = fcn(block, idx, value) + if flag: + yield value + continue + except Exception as e: + flag = None + exc = e + + if isinstance(value, tuple): + vstar = value + else: + vstar = (value,) + + # First: try the old format: *values and no index + if fcn.__class__ is ParameterizedIndexedCallInitializer: + try: + flag = fcn(block, (), *vstar) + if flag: + deprecation_warning( + f"{self.__class__.__name__} {self.name}: {mode} " + "callback signature matched (block, *value). " + "Please update the callback to match the signature " + "(block, value, *index).", + version='6.7.4.dev0' + ) + orig_fcn = fcn._fcn + fcn = ParameterizedScalarCallInitializer( + lambda m, v: orig_fcn(m, *v), True + ) + setattr(self, '_' + mode, fcn) + yield value + continue + except TypeError: + pass + except Exception as e: + exc = e + + # Now try *values and index + try: + flag = fcn(block, idx, *value) + if flag: + deprecation_warning( + f"{self.__class__.__name__} {self.name}: {mode} " + "callback signature matched (block, *value, *index). " + "Please update the callback to match the signature " + "(block, value, *index).", + version='6.7.4.dev0' + ) + if fcn.__class__ is not ParameterizedInitializer: + orig_fcn = fcn._fcn + fcn._fcn = lambda m, v, *i: orig_fcn(m, *v, *i) + yield value + continue + except TypeError: + pass + except Exception as e: + exc = e + if flag is not None: + if failFalse: + raise ValueError( + "The value=%s violates the validation rule of Set %s" + % (value, self.name) + ) + continue + logger.error( + "Exception raised while validating element '%s' " + "for Set %s" % (value, self.name) + ) + raise exc from None def _cb_normalized_dimen_verifier(self, dimen, val_iter): for value in val_iter: @@ -2170,7 +2233,7 @@ def __init__(self, *args, **kwds): allow_generators=True, ) ) - self._init_validate = Initializer(kwds.pop('validate', None)) + self._validate = Initializer(kwds.pop('validate', None), additional_args=1) self._init_filter = Initializer(kwds.pop('filter', None)) if 'virtual' in kwds: @@ -2183,6 +2246,19 @@ def __init__(self, *args, **kwds): IndexedComponent.__init__(self, *args, **kwds) + if ( + self._validate.__class__ is ParameterizedIndexedCallInitializer + and not self.parent_component().is_indexed() + ): + # TBD [JDS: 8/2024]: should we deprecate the "expanded + # tuple" version of the validate callback for scalar sets? + # It is widely used and we can (reasonably reliably) map to + # the expected behavior. + orig_fcn = self._validate._fcn + self._validate = ParameterizedScalarCallInitializer( + lambda m, v: orig_fcn(m, *v), True + ) + # HACK to make the "counted call" syntax work. We wait until # after the base class is set up so that is_indexed() is # reliable. @@ -2291,17 +2367,8 @@ def _getitem_when_not_present(self, index): obj._domain = domain if _d is not UnknownSetDimen: obj._dimen = _d - if self._init_validate is not None: - try: - obj._validate = Initializer(self._init_validate(_block, index)) - if obj._validate.constant(): - # _init_validate was the actual validate function; use it. - obj._validate = self._init_validate - except: - # We will assume any exceptions raised when getting the - # validator for this index indicate that the function - # should have been passed directly to the underlying sets. - obj._validate = self._init_validate + if self._validate is not None: + obj._validate = self._validate if self._init_filter is not None: try: obj._filter = Initializer(self._init_filter(_block, index)) @@ -2997,7 +3064,7 @@ def __init__(self, *args, **kwds): ) kwds.pop('finite', None) self._init_data = (args, kwds.pop('ranges', ())) - self._init_validate = Initializer(kwds.pop('validate', None)) + self._validate = Initializer(kwds.pop('validate', None), additional_args=1) self._init_filter = Initializer(kwds.pop('filter', None)) self._init_bounds = kwds.pop('bounds', None) if self._init_bounds is not None: @@ -3192,38 +3259,25 @@ def construct(self, data=None): new_ranges.append(r) self._ranges = new_ranges - if self._init_validate is not None: + if self._validate is not None: if not self.isfinite(): raise ValueError( "The 'validate' keyword argument is not valid for " "non-finite RangeSet component (%s)" % (self.name,) ) - try: - _validate = Initializer(self._init_validate(_block, None)) - if _validate.constant(): - # _init_validate was the actual validate function; use it. - _validate = self._init_validate + for val in self: + if not self._validate(_block, None, val): + raise ValueError( + "The value=%s violates the validation rule of " + "Set %s" % (val, self.name) + ) except: - # We will assume any exceptions raised when getting the - # validator for this index indicate that the function - # should have been passed directly to the underlying set. - _validate = self._init_validate - - for val in self: - try: - flag = _validate(_block, val) - except: - logger.error( - "Exception raised while validating element '%s' " - "for Set %s" % (val, self.name) - ) - raise - if not flag: - raise ValueError( - "The value=%s violates the validation rule of " - "Set %s" % (val, self.name) - ) + logger.error( + "Exception raised while validating element '%s' " + "for Set %s" % (val, self.name) + ) + raise # Defer the warning about non-constant args until after the # component has been constructed, so that the conversion of the From 9402a8e27d1ec3b5ea57969b30d46f57b6b2a71a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 18:19:17 -0600 Subject: [PATCH 2157/3044] Expand tutorial, update tests --- examples/pyomo/tutorials/set.dat | 6 +- examples/pyomo/tutorials/set.py | 10 +++ pyomo/core/tests/unit/test_set.py | 102 +++++++++++++++++++++++++----- 3 files changed, 102 insertions(+), 16 deletions(-) diff --git a/examples/pyomo/tutorials/set.dat b/examples/pyomo/tutorials/set.dat index e2ad04122d8..16ad7ff9698 100644 --- a/examples/pyomo/tutorials/set.dat +++ b/examples/pyomo/tutorials/set.dat @@ -17,5 +17,9 @@ set S[5] := 2 3; set T[2] := 1 3; set T[5] := 2 3; +set T_indexed_validate[2] := 1; +set T_indexed_validate[3] := 1 2; +set T_indexed_validate[4] := 1 2 3; + set X[2] := 1; -set X[5] := 2 3; \ No newline at end of file +set X[5] := 2 3; diff --git a/examples/pyomo/tutorials/set.py b/examples/pyomo/tutorials/set.py index 9acdf35460c..220bfbc82da 100644 --- a/examples/pyomo/tutorials/set.py +++ b/examples/pyomo/tutorials/set.py @@ -190,6 +190,16 @@ def T_validate(model, value): model.T = Set(model.B, validate=T_validate) +# +# Validation also provides the index within the IndexedSet being validated: +# +def T_indexed_validate(model, value, i): + return value in model.A and value < i + + +model.T_indexed_validate = Set(model.B, validate=T_indexed_validate) + + ## ## Set options ## diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 6b71201d287..74847ad91e9 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4354,27 +4354,99 @@ def _validate(model, i, j): "Exception raised while validating element '(2, 2)' for Set I\n", ) - # Note: one of these indices will trigger the exception in the - # validot when it is called for the index. - def _validate_J(model, i, j, index): - return _validate_I(model, i, j) + m.J1 = Set([(0, 0), (2, 2)], validate=_validate) + with LoggingIntercept() as OUT: + self.assertTrue(m.J1[2, 2].add((0, 1))) + self.assertRegex( + OUT.getvalue().replace('\n', ' '), + r"DEPRECATED: InsertionOrderSetData J1\[2,2\]: validate callback " + r"signature matched \(block, \*value\). Please update the " + r"callback to match the signature \(block, value, \*index\)" + ) + with LoggingIntercept() as OUT: + with self.assertRaisesRegex( + ValueError, + r"The value=\(4, 1\) violates the validation rule of " r"Set J1\[0,0\]", + ): + m.J1[0, 0].add((4, 1)) + with self.assertRaisesRegex(RuntimeError, "Bogus value"): + m.J1[2, 2].add((2, 2)) + self.assertEqual( + OUT.getvalue(), + "Exception raised while validating element '(2, 2)' for Set J1[2,2]\n", + ) - m.J = Set([(0, 0), (2, 2)], validate=_validate_J) - output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): - self.assertTrue(m.J[2, 2].add((0, 1))) - self.assertEqual(output.getvalue(), "") + def _validate(model, i, j, ind1, ind2): + self.assertIs(model, m) + if i + j < ind1 + ind2: + return True + if i - j > ind1 + ind2: + return False + raise RuntimeError("Bogus value") + + m.J2 = Set([(0, 0), (2, 2)], validate=_validate) + with LoggingIntercept() as OUT: + self.assertTrue(m.J2[2, 2].add((0, 1))) + self.assertRegex( + OUT.getvalue().replace('\n', ' '), + r"DEPRECATED: InsertionOrderSetData J2\[2,2\]: validate callback " + r"signature matched \(block, \*value, \*index\). Please update the " + r"callback to match the signature \(block, value, \*index\)" + ) + + with LoggingIntercept() as OUT: + self.assertEqual(OUT.getvalue(), "") with self.assertRaisesRegex( ValueError, - r"The value=\(4, 1\) violates the validation rule of " r"Set J\[0,0\]", + r"The value=\(1, 0\) violates the validation rule of Set J2\[0,0\]", ): - m.J[0, 0].add((4, 1)) - self.assertEqual(output.getvalue(), "") + m.J2[0, 0].add((1, 0)) + with self.assertRaisesRegex( + ValueError, + r"The value=\(4, 1\) violates the validation rule of Set J2\[0,0\]", + ): + m.J2[0, 0].add((4, 1)) + self.assertEqual(OUT.getvalue(), "") with self.assertRaisesRegex(RuntimeError, "Bogus value"): - m.J[2, 2].add((2, 2)) + m.J2[2, 2].add((2, 2)) self.assertEqual( - output.getvalue(), - "Exception raised while validating element '(2, 2)' for Set J[2,2]\n", + OUT.getvalue(), + "Exception raised while validating element '(2, 2)' for Set J2[2,2]\n", + ) + + + def _validate(model, v, ind1, ind2): + self.assertIs(model, m) + i, j = v + if i + j < ind1 + ind2: + return True + if i - j > ind1 + ind2: + return False + raise RuntimeError("Bogus value") + + m.J3 = Set([(0, 0), (2, 2)], validate=_validate) + with LoggingIntercept() as OUT: + self.assertTrue(m.J3[2, 2].add((0, 1))) + self.assertEqual(OUT.getvalue(), "") + + with LoggingIntercept() as OUT: + self.assertEqual(OUT.getvalue(), "") + with self.assertRaisesRegex( + ValueError, + r"The value=\(1, 0\) violates the validation rule of Set J3\[0,0\]", + ): + m.J3[0, 0].add((1, 0)) + with self.assertRaisesRegex( + ValueError, + r"The value=\(4, 1\) violates the validation rule of Set J3\[0,0\]", + ): + m.J3[0, 0].add((4, 1)) + self.assertEqual(OUT.getvalue(), "") + with self.assertRaisesRegex(RuntimeError, "Bogus value"): + m.J3[2, 2].add((2, 2)) + self.assertEqual( + OUT.getvalue(), + "Exception raised while validating element '(2, 2)' for Set J3[2,2]\n", ) def test_domain(self): From fc1d8eec24739df65b6033a8230379f46201ae1c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 18:30:27 -0600 Subject: [PATCH 2158/3044] Move Set filter callback to leverage Initializer / validate handler logic --- pyomo/core/base/set.py | 37 +++++++------------------------ pyomo/core/tests/unit/test_set.py | 6 ++--- 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 1eccb6e3624..a66cc99c871 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1428,7 +1428,7 @@ def update(self, values): val_iter = self._cb_domain_verifier(self._domain, val_iter) if self._filter is not None: - val_iter = filter(partial(self._filter, self.parent_block()), val_iter) + val_iter = self._cb_validate_filter('filter', val_iter) if self._validate is not None: val_iter = self._cb_validate_filter('validate', val_iter) @@ -2234,7 +2234,7 @@ def __init__(self, *args, **kwds): ) ) self._validate = Initializer(kwds.pop('validate', None), additional_args=1) - self._init_filter = Initializer(kwds.pop('filter', None)) + self._filter = Initializer(kwds.pop('filter', None), additional_args=1) if 'virtual' in kwds: deprecation_warning( @@ -2369,19 +2369,8 @@ def _getitem_when_not_present(self, index): obj._dimen = _d if self._validate is not None: obj._validate = self._validate - if self._init_filter is not None: - try: - obj._filter = Initializer(self._init_filter(_block, index)) - if obj._filter.constant(): - # _init_filter was the actual filter function; use it. - obj._filter = self._init_filter - except: - # We will assume any exceptions raised when getting the - # filter for this index indicate that the function - # should have been passed directly to the underlying sets. - obj._filter = self._init_filter - else: - obj._filter = None + if self._filter is not None: + obj._filter = self._filter if self._init_values is not None: # record the user-provided dimen in the initializer self._init_values._dimen = _d @@ -3065,7 +3054,7 @@ def __init__(self, *args, **kwds): kwds.pop('finite', None) self._init_data = (args, kwds.pop('ranges', ())) self._validate = Initializer(kwds.pop('validate', None), additional_args=1) - self._init_filter = Initializer(kwds.pop('filter', None)) + self._filter = Initializer(kwds.pop('filter', None), additional_args=1) self._init_bounds = kwds.pop('bounds', None) if self._init_bounds is not None: self._init_bounds = BoundsInitializer(self._init_bounds) @@ -3216,23 +3205,13 @@ def construct(self, data=None): self._ranges = ranges - if self._init_filter is not None: + if self._filter is not None: if not self.isfinite(): raise ValueError( "The 'filter' keyword argument is not valid for " "non-finite RangeSet component (%s)" % (self.name,) ) - - try: - _filter = Initializer(self._init_filter(_block, None)) - if _filter.constant(): - # _init_filter was the actual filter function; use it. - _filter = self._init_filter - except: - # We will assume any exceptions raised when getting the - # filter for this index indicate that the function - # should have been passed directly to the underlying sets. - _filter = self._init_filter + _filter = self._filter # If this is a finite set, then we can go ahead and filter # all the ranges. This allows pprint and len to be correct, @@ -3243,7 +3222,7 @@ def construct(self, data=None): while old_ranges: r = old_ranges.pop() for i, val in enumerate(FiniteRangeSetData._range_gen(r)): - if not _filter(_block, val): + if not _filter(_block, (), val): split_r = r.range_difference((NumericRange(val, val, 0),)) if len(split_r) == 2: new_ranges.append(split_r[0]) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 74847ad91e9..41b88f3eb20 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -34,6 +34,7 @@ ConstantInitializer, ItemInitializer, IndexedCallInitializer, + ParameterizedScalarCallInitializer, ) from pyomo.core.base.set import ( NumericRange as NR, @@ -4293,8 +4294,7 @@ def _l_tri(model, i, j): return i >= j m.K = Set(initialize=RangeSet(3) * RangeSet(3), filter=_l_tri) - self.assertIsInstance(m.K.filter, IndexedCallInitializer) - self.assertIs(m.K.filter._fcn, _l_tri) + self.assertIsInstance(m.K.filter, ParameterizedScalarCallInitializer) self.assertEqual(list(m.K), [(1, 1), (2, 1), (2, 2), (3, 1), (3, 2), (3, 3)]) output = StringIO() @@ -5921,7 +5921,7 @@ def test_filter(self): output = StringIO() with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): - self.assertIsInstance(m.K.filter, IndexedCallInitializer) + self.assertIsInstance(m.K.filter, ParameterizedScalarCallInitializer) self.assertRegex( output.getvalue(), "^DEPRECATED: 'filter' is no longer a public attribute" ) From 7bb1f2ff3f16ddb8408510ec9d6f9733878d51a0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 18:35:27 -0600 Subject: [PATCH 2159/3044] Remove _validate and _filter from SetData (and only store on container) --- pyomo/core/base/set.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index a66cc99c871..6cbd9f5d5ee 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1301,7 +1301,7 @@ def ranges(self): class FiniteSetData(_FiniteSetMixin, SetData): """A general unordered iterable Set""" - __slots__ = ('_values', '_domain', '_validate', '_filter', '_dimen') + __slots__ = ('_values', '_domain', '_dimen') def __init__(self, component): SetData.__init__(self, component=component) @@ -1310,8 +1310,6 @@ def __init__(self, component): if not hasattr(self, '_values'): self._values = set() self._domain = Any - self._validate = None - self._filter = None self._dimen = UnknownSetDimen def get(self, value, default=None): @@ -1427,10 +1425,11 @@ def update(self, values): if self._domain is not Any: val_iter = self._cb_domain_verifier(self._domain, val_iter) - if self._filter is not None: + comp = self.parent_component() + if comp._filter is not None: val_iter = self._cb_validate_filter('filter', val_iter) - if self._validate is not None: + if comp._validate is not None: val_iter = self._cb_validate_filter('validate', val_iter) # We wrap this check in a try-except because some values @@ -1461,8 +1460,9 @@ def _cb_check_set_end(self, val_iter): def _cb_validate_filter(self, mode, val_iter): failFalse = mode == 'validate' - fcn = getattr(self, '_' + mode) - block = self.parent_block() + comp = self.parent_component() + fcn = getattr(comp, '_' + mode) + block = comp.parent_block() idx = self.index() for value in val_iter: try: @@ -1495,7 +1495,7 @@ def _cb_validate_filter(self, mode, val_iter): fcn = ParameterizedScalarCallInitializer( lambda m, v: orig_fcn(m, *v), True ) - setattr(self, '_' + mode, fcn) + setattr(comp, '_' + mode, fcn) yield value continue except TypeError: @@ -2367,10 +2367,6 @@ def _getitem_when_not_present(self, index): obj._domain = domain if _d is not UnknownSetDimen: obj._dimen = _d - if self._validate is not None: - obj._validate = self._validate - if self._filter is not None: - obj._filter = self._filter if self._init_values is not None: # record the user-provided dimen in the initializer self._init_values._dimen = _d From d58847af6a7f6ff0a687c9db8230b3ef6400f1bd Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 9 Aug 2024 21:46:15 -0400 Subject: [PATCH 2160/3044] Reorganize and rename working model stagewise components --- pyomo/contrib/pyros/master_problem_methods.py | 44 +- .../contrib/pyros/pyros_algorithm_methods.py | 10 +- .../pyros/separation_problem_methods.py | 379 ++++---- pyomo/contrib/pyros/solve_data.py | 94 +- pyomo/contrib/pyros/tests/test_grcs.py | 14 +- pyomo/contrib/pyros/tests/test_master.py | 62 +- .../contrib/pyros/tests/test_preprocessor.py | 901 ++++++++---------- pyomo/contrib/pyros/tests/test_separation.py | 64 +- pyomo/contrib/pyros/util.py | 353 ++++--- 9 files changed, 889 insertions(+), 1032 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 15e41c279fc..b232b3f07cd 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -84,7 +84,7 @@ def construct_initial_master_problem(model_data, config): # model rather than to the model to prevent # duplication across scenario sub-blocks master_model.epigraph_obj = Objective( - expr=master_model.scenarios[0, 0].epigraph_var, + expr=master_model.scenarios[0, 0].first_stage.epigraph_var, ) return master_model @@ -149,11 +149,9 @@ def add_scenario_block_to_master_problem( # deactivate the first-stage constraints: they are duplicate if scenario_idx != (0, 0): new_blk = master_model.scenarios[scenario_idx] - new_blk_first_stage_cons = ( - new_blk.effective_first_stage_inequality_cons - + new_blk.effective_first_stage_equality_cons - ) - for con in new_blk_first_stage_cons: + for con in new_blk.first_stage.inequality_cons.values(): + con.deactivate() + for con in new_blk.first_stage.equality_cons.values(): con.deactivate() @@ -162,8 +160,9 @@ def construct_master_feasibility_problem(master_data, config): Construct slack variable minimization problem from the master model. - Slack variables are added only to the performance constraints - of the blocks added for the current PyROS iteration. + Slack variables are added only to the seconds-stage + inequality constraints of the blocks added for the + current PyROS iteration. Parameters ---------- @@ -203,13 +202,14 @@ def construct_master_feasibility_problem(master_data, config): obj.deactivate() iteration = master_data.iteration - # add slacks only to performance inequality constraints for the newest - # master block. these should be the only constraints which + # add slacks only to second-stage inequality constraints for the + # newest master block(s). + # these should be the only constraints that # may have been violated by the previous master and separation # solution(s) targets = [] for blk in slack_model.scenarios[iteration, :]: - targets.extend(blk.effective_performance_inequality_cons) + targets.extend(blk.second_stage.inequality_cons.values()) # retain original constraint expressions before adding slacks # (to facilitate slack initialization and scaling) @@ -383,7 +383,7 @@ def construct_dr_polishing_problem(master_data, config): nominal_eff_var_partitioning.first_stage_variables # fixing epigraph variable constrains the problem # to the optimal master problem solution set - + [nominal_polishing_block.epigraph_var] + + [nominal_polishing_block.first_stage.epigraph_var] ) for var in nondr_nonadjustable_vars: var.fix() @@ -408,7 +408,8 @@ def construct_dr_polishing_problem(master_data, config): polishing_model.epigraph_obj.deactivate() polishing_model.polishing_vars = polishing_vars = [] - for idx, indexed_dr_var in enumerate(nominal_polishing_block.decision_rule_vars): + indexed_dr_var_list = nominal_polishing_block.first_stage.decision_rule_vars + for idx, indexed_dr_var in enumerate(indexed_dr_var_list): # auxiliary 'polishing' variables. # these are meant to represent the absolute values # of the terms of DR polynomial; @@ -417,7 +418,7 @@ def construct_dr_polishing_problem(master_data, config): list(indexed_dr_var.keys()), domain=NonNegativeReals ) polishing_model.add_component( - unique_component_name(polishing_model, f"dr_polishing_var_{idx}"), + f"dr_polishing_var_{idx}", indexed_polishing_var, ) polishing_vars.append(indexed_polishing_var) @@ -443,11 +444,11 @@ def construct_dr_polishing_problem(master_data, config): # add indexed constraints to polishing model polishing_model.add_component( - unique_component_name(polishing_model, f"polishing_abs_val_lb_con_{idx}"), + f"polishing_abs_val_lb_con_{idx}", polishing_absolute_value_lb_cons, ) polishing_model.add_component( - unique_component_name(polishing_model, f"polishing_abs_val_ub_con_{idx}"), + f"polishing_abs_val_ub_con_{idx}", polishing_absolute_value_ub_cons, ) @@ -479,7 +480,10 @@ def construct_dr_polishing_problem(master_data, config): # in DR expression is 0 # across all master blocks dr_term_copies = [ - scenario_blk.decision_rule_eqns[idx].body.args[dr_var_in_term_idx] + ( + scenario_blk.second_stage.decision_rule_eqns[idx] + .body.args[dr_var_in_term_idx] + ) for scenario_blk in master_model.scenarios.values() ] all_copy_coeffs_zero = is_a_nonstatic_dr_term and all( @@ -614,8 +618,8 @@ def minimize_dr_vars(master_data, config): for master_var, polish_var in adjustable_vars_zip: master_var.set_value(value(polish_var)) dr_var_zip = zip( - blk.decision_rule_vars, - polishing_model.scenarios[idx].decision_rule_vars, + blk.first_stage.decision_rule_vars, + polishing_model.scenarios[idx].first_stage.decision_rule_vars, ) for master_dr, polish_dr in dr_var_zip: for mvar, pvar in zip(master_dr.values(), polish_dr.values()): @@ -702,7 +706,7 @@ def higher_order_decision_rule_efficiency(master_data, config): """ order_to_enforce = get_master_dr_degree(master_data, config) enforce_dr_degree( - blk=master_data.master_model.scenarios[0, 0], + working_blk=master_data.master_model.scenarios[0, 0], config=config, degree=order_to_enforce, ) diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 058b25e374e..008fed2f4c5 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -153,8 +153,8 @@ def ROSolver_iterative_solve(model_data, config): previous_iter_var_data = None current_iter_var_data = None - num_performance_cons = len( - separation_data.separation_model.effective_performance_inequality_cons + num_second_stage_ineq_cons = len( + separation_data.separation_model.second_stage.inequality_cons ) IterationLogRecord.log_header(config.progress_logger.info) k = 0 @@ -206,7 +206,7 @@ def ROSolver_iterative_solve(model_data, config): polishing_successful = True polish_master_solution = ( config.decision_rule_order != 0 - and nominal_master_blk.decision_rule_vars + and nominal_master_blk.first_stage.decision_rule_vars and k != 0 ) if polish_master_solution: @@ -264,10 +264,10 @@ def ROSolver_iterative_solve(model_data, config): max_sep_con_violation = max(scaled_violations) else: max_sep_con_violation = None - num_violated_cons = len(separation_results.violated_performance_constraints) + num_violated_cons = len(separation_results.violated_second_stage_ineq_cons) all_sep_problems_solved = ( - len(scaled_violations) == num_performance_cons + len(scaled_violations) == num_second_stage_ineq_cons and not separation_results.subsolver_error and not separation_results.time_out ) or separation_results.all_discrete_scenarios_exhausted diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index 9ea3fbf2556..ac2a0be72ea 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -129,12 +129,10 @@ def construct_separation_problem(model_data, config): # fix/deactivate all nonadjustable components for var in separation_model.all_nonadjustable_variables: var.fix() - nonadjustable_cons = ( - separation_model.effective_first_stage_inequality_cons - + separation_model.effective_first_stage_equality_cons - ) - for nadjcon in nonadjustable_cons: - nadjcon.deactivate() + for fs_eqcon in separation_model.first_stage.equality_cons.values(): + fs_eqcon.deactivate() + for fs_ineqcon in separation_model.first_stage.inequality_cons.values(): + fs_ineqcon.deactivate() # add block for the uncertainty set quantification add_uncertainty_set_constraints(separation_model, config) @@ -151,9 +149,9 @@ def construct_separation_problem(model_data, config): } uncertain_params_set = ComponentSet(uncertain_params) adjustable_cons = ( - separation_model.effective_performance_inequality_cons - + separation_model.effective_performance_equality_cons - + separation_model.decision_rule_eqns + list(separation_model.second_stage.inequality_cons.values()) + + list(separation_model.second_stage.equality_cons.values()) + + list(separation_model.second_stage.decision_rule_eqns.values()) ) for adjcon in adjustable_cons: uncertain_params_in_con = ComponentSet( @@ -164,26 +162,26 @@ def construct_separation_problem(model_data, config): replace_expressions(adjcon.expr, substitution_map=param_id_to_var_map) ) - # performance inequality constraint expressions + # second-stage inequality constraint expressions # become maximization objectives in the separation problems - separation_model.perf_ineq_con_to_obj_map = ComponentMap() - perf_ineq_cons = separation_model.effective_performance_inequality_cons - for idx, perf_con in enumerate(perf_ineq_cons): - perf_con.deactivate() - separation_obj = Objective(expr=perf_con.body - perf_con.upper, sense=maximize) + separation_model.second_stage_ineq_con_to_obj_map = ComponentMap() + ss_ineq_cons = separation_model.second_stage.inequality_cons.values() + for idx, ss_ineq_con in enumerate(ss_ineq_cons): + ss_ineq_con.deactivate() + separation_obj = Objective(expr=ss_ineq_con.body - ss_ineq_con.upper, sense=maximize) separation_model.add_component( f"separation_obj_{idx}", separation_obj, ) - separation_model.perf_ineq_con_to_obj_map[perf_con] = separation_obj + separation_model.second_stage_ineq_con_to_obj_map[ss_ineq_con] = separation_obj separation_obj.deactivate() return separation_model -def get_sep_objective_values(separation_data, config, perf_cons): +def get_sep_objective_values(separation_data, config, ss_ineq_cons): """ - Evaluate performance constraint functions at current + Evaluate second-stage inequality constraint functions at current separation solution. Parameters @@ -192,35 +190,36 @@ def get_sep_objective_values(separation_data, config, perf_cons): Separation problem data. config : ConfigDict PyROS solver settings. - perf_cons : list of Constraint - Performance constraints to be evaluated. + ss_ineq_cons : list of Constraint + Second-stage inequality constraints to be evaluated. Returns ------- violations : ComponentMap - Mapping from performance constraints to violation values. + Mapping from second-stage inequality constraints + to violation values. """ - con_to_obj_map = separation_data.separation_model.perf_ineq_con_to_obj_map + con_to_obj_map = separation_data.separation_model.second_stage_ineq_con_to_obj_map violations = ComponentMap() user_var_partitioning = separation_data.separation_model.user_var_partitioning first_stage_variables = user_var_partitioning.first_stage_variables second_stage_variables = user_var_partitioning.second_stage_variables - for perf_con in perf_cons: - obj = con_to_obj_map[perf_con] + for ss_ineq_con in ss_ineq_cons: + obj = con_to_obj_map[ss_ineq_con] try: - violations[perf_con] = value(obj.expr) + violations[ss_ineq_con] = value(obj.expr) except ValueError: for v in first_stage_variables: config.progress_logger.info(v.name + " " + str(v.value)) for v in second_stage_variables: config.progress_logger.info(v.name + " " + str(v.value)) raise ArithmeticError( - f"Evaluation of performance constraint {perf_con.name} " + f"Evaluation of second-stage inequality constraint {ss_ineq_con.name} " f"(separation objective {obj.name}) " "led to a math domain error. " - "Does the performance constraint expression " + "Does the constraint expression " "contain log(x) or 1/x functions " "or others with tricky domains?" ) @@ -228,41 +227,43 @@ def get_sep_objective_values(separation_data, config, perf_cons): return violations -def get_argmax_sum_violations(solver_call_results_map, perf_cons_to_evaluate): +def get_argmax_sum_violations(solver_call_results_map, ss_ineq_cons_to_evaluate): """ Get key of entry of `solver_call_results_map` which contains - separation problem solution with maximal sum of performance - constraint violations over a specified sequence of performance - constraints. + separation problem solution with maximal sum of second-stage + inequality constraint violations over a specified sequence of + second-stage inequality constraints. Parameters ---------- solver_call_results : ComponentMap - Mapping from performance constraints to corresponding + Mapping from second-stage inequality constraints to corresponding separation solver call results. - perf_cons_to_evaluate : list of Constraints - Performance constraints to consider for evaluating + ss_ineq_cons_to_evaluate : list of Constraints + Second-stage inequality constraints to consider for evaluating maximal sum. Returns ------- - worst_perf_con : None or Constraint - Performance constraint corresponding to solver call + worst_ss_ineq_con : None or Constraint + Second-stage inequality constraint corresponding to solver call results object containing solution with maximal sum - of violations across all performance constraints. + of violations across all second-stage inequality constraints. If ``found_violation`` attribute of all value entries of `solver_call_results_map` is False, then `None` is - returned, as this means none of the performance constraints + returned, as this means + none of the second-stage inequality constraints were found to be violated. """ - # get indices of performance constraints for which violation found - idx_to_perf_con_map = { - idx: perf_con for idx, perf_con in enumerate(solver_call_results_map) + # get indices of second-stage ineq constraints + # for which violation found + idx_to_ss_ineq_con_map = { + idx: ss_ineq_con for idx, ss_ineq_con in enumerate(solver_call_results_map) } idxs_of_violated_cons = [ idx - for idx, perf_con in idx_to_perf_con_map.items() - if solver_call_results_map[perf_con].found_violation + for idx, ss_ineq_con in idx_to_ss_ineq_con_map.items() + if solver_call_results_map[ss_ineq_con].found_violation ] num_violated_cons = len(idxs_of_violated_cons) @@ -272,7 +273,7 @@ def get_argmax_sum_violations(solver_call_results_map, perf_cons_to_evaluate): # assemble square matrix (2D array) of constraint violations. # matrix size: number of constraints for which violation was found - # each row corresponds to a performance constraint + # each row corresponds to a second-stage inequality constraint # each column corresponds to a separation problem solution violations_arr = np.zeros(shape=(num_violated_cons, num_violated_cons)) idxs_product = product( @@ -282,19 +283,19 @@ def get_argmax_sum_violations(solver_call_results_map, perf_cons_to_evaluate): violations_arr[row_idx, col_idx] = max( 0, ( - # violation of this row's performance constraint + # violation of this row's second-stage inequality con # by this column's separation solution # if separation problems were solved globally, # then diagonal entries should be the largest in each row solver_call_results_map[ - idx_to_perf_con_map[viol_param_idx] - ].scaled_violations[idx_to_perf_con_map[viol_con_idx]] + idx_to_ss_ineq_con_map[viol_param_idx] + ].scaled_violations[idx_to_ss_ineq_con_map[viol_con_idx]] ), ) worst_col_idx = np.argmax(np.sum(violations_arr, axis=0)) - return idx_to_perf_con_map[idxs_of_violated_cons[worst_col_idx]] + return idx_to_ss_ineq_con_map[idxs_of_violated_cons[worst_col_idx]] def solve_separation_problem(separation_data, master_data, config): @@ -356,33 +357,34 @@ def solve_separation_problem(separation_data, master_data, config): def evaluate_violations_by_nominal_master( separation_data, master_data, - performance_cons, + ss_ineq_cons, ): """ - Evaluate violation of performance constraints by + Evaluate violation of second-stage inequality constraints by variables in nominal block of most recent master problem. Returns ------- - nom_perf_con_violations : dict - Mapping from performance constraint names + nom_ss_ineq_con_violations : dict + Mapping from second-stage inequality constraint names to floats equal to violations by nominal master problem variables. """ - nom_perf_con_violations = ComponentMap() - for perf_con in performance_cons: + nom_ss_ineq_con_violations = ComponentMap() + for ss_ineq_con in ss_ineq_cons: nom_violation = value( - master_data.master_model.scenarios[0, 0].find_component(perf_con) + master_data.master_model.scenarios[0, 0].find_component(ss_ineq_con) ) - nom_perf_con_violations[perf_con] = nom_violation + nom_ss_ineq_con_violations[ss_ineq_con] = nom_violation - return nom_perf_con_violations + return nom_ss_ineq_con_violations -def group_performance_constraints_by_priority(separation_data, config): +def group_ss_ineq_constraints_by_priority(separation_data, config): """ - Group model performance constraints by separation priority. + Group model second-stage inequality constraints + by separation priority. Parameters ---------- @@ -394,54 +396,55 @@ def group_performance_constraints_by_priority(separation_data, config): Returns ------- dict - Mapping from an int to a list of performance constraints + Mapping from an int to a list of second-stage + inequality constraints (Constraint objects), for which the int is equal to the specified priority. Keys are sorted in descending order (i.e. highest priority first). """ - all_perf_cons = ( - separation_data.separation_model.effective_performance_inequality_cons + all_ss_ineq_cons = list( + separation_data.separation_model.second_stage.inequality_cons.values() ) separation_priority_groups = dict() config_sep_priority_dict = config.separation_priority_order - for perf_con in all_perf_cons: + for ss_ineq_con in all_ss_ineq_cons: # by default, priority set to 0 - priority = config_sep_priority_dict.get(perf_con.name, 0) + priority = config_sep_priority_dict.get(ss_ineq_con.name, 0) cons_with_same_priority = separation_priority_groups.setdefault(priority, []) - cons_with_same_priority.append(perf_con) + cons_with_same_priority.append(ss_ineq_con) # sort separation priority groups return { - priority: perf_cons - for priority, perf_cons in sorted( + priority: ss_ineq_cons + for priority, ss_ineq_cons in sorted( separation_priority_groups.items(), reverse=True ) } def get_worst_discrete_separation_solution( - performance_constraint, + ss_ineq_con, config, - perf_cons_to_evaluate, + ss_ineq_cons_to_evaluate, discrete_solve_results, ): """ Determine separation solution (and therefore worst-case uncertain parameter realization) with maximum violation - of specified performance constraint. + of specified second-stage inequality constraint. Parameters ---------- - performance_constraint : Constraint - Performance constraint of interest. + ss_ineq_con : Constraint + Second-stage inequality constraint of interest. separation_data : SeparationProblemData Separation problem data. config : ConfigDict User-specified PyROS solver settings. - perf_cons_to_evaluate : list of Constraint - Performance constraints for which to report violations - by separation solution. + ss_ineq_cons_to_evaluate : list of Constraint + Second-stage inequality constraints for which to report + violations by separation solution. discrete_solve_results : DiscreteSeparationSolveCallResults Separation problem solutions corresponding to the uncertain parameter scenarios listed in @@ -450,42 +453,43 @@ def get_worst_discrete_separation_solution( Returns ------- SeparationSolveCallResult - Solver call result for performance constraint of interest. + Solver call result for second-stage inequality constraint of interest. """ - # violation of specified performance constraint by separation + # violation of specified second-stage inequality + # constraint by separation # problem solutions for all scenarios - violations_of_perf_con = [ - solve_call_res.scaled_violations[performance_constraint] + violations_of_ss_ineq_con = [ + solve_call_res.scaled_violations[ss_ineq_con] for solve_call_res in discrete_solve_results.solver_call_results.values() ] list_of_scenario_idxs = list(discrete_solve_results.solver_call_results.keys()) # determine separation solution for which scaled violation of this - # performance constraint is the worst + # second-stage inequality constraint is the worst worst_case_res = discrete_solve_results.solver_call_results[ - list_of_scenario_idxs[np.argmax(violations_of_perf_con)] + list_of_scenario_idxs[np.argmax(violations_of_ss_ineq_con)] ] - worst_case_violation = np.max(violations_of_perf_con) + worst_case_violation = np.max(violations_of_ss_ineq_con) assert worst_case_violation in worst_case_res.scaled_violations.values() - # evaluate violations for specified performance constraints - eval_perf_con_scaled_violations = ComponentMap( - (perf_con, worst_case_res.scaled_violations[perf_con]) - for perf_con in perf_cons_to_evaluate + # evaluate violations for specified second-stage inequality constraints + eval_ss_ineq_con_scaled_violations = ComponentMap( + (ss_ineq_con, worst_case_res.scaled_violations[ss_ineq_con]) + for ss_ineq_con in ss_ineq_cons_to_evaluate ) # discrete separation solutions were obtained by optimizing - # just one performance constraint, as an efficiency. + # just one second-stage inequality constraint, as an efficiency. # if the constraint passed to this routine is the same as the # constraint used to obtain the solutions, then we bundle # the separation solve call results into a single list. # otherwise, we return an empty list, as we did not need to call - # subsolvers for the other performance constraints - is_optimized_performance_con = ( - performance_constraint is discrete_solve_results.performance_constraint + # subsolvers for the other second-stage inequality constraints + is_optimized_ss_ineq_con = ( + ss_ineq_con is discrete_solve_results.second_stage_ineq_con ) - if is_optimized_performance_con: + if is_optimized_ss_ineq_con: results_list = [ res for solve_call_results @@ -498,7 +502,7 @@ def get_worst_discrete_separation_solution( return SeparationSolveCallResults( solved_globally=worst_case_res.solved_globally, results_list=results_list, - scaled_violations=eval_perf_con_scaled_violations, + scaled_violations=eval_ss_ineq_con_scaled_violations, violating_param_realization=worst_case_res.violating_param_realization, variable_values=worst_case_res.variable_values, found_violation=(worst_case_violation > config.robust_feasibility_tolerance), @@ -510,9 +514,8 @@ def get_worst_discrete_separation_solution( def get_con_name_repr(separation_model, con, with_obj_name=True): """ - Get string representation of performance constraint - and the objective to which it has - been mapped. + Get string representation of second-stage inequality constraint + and the objective to which it has been mapped. Parameters ---------- @@ -522,7 +525,7 @@ def get_con_name_repr(separation_model, con, with_obj_name=True): Constraint for which to get the representation. with_obj_name : bool, optional Include name of separation model objective to which - constraint is mapped. Applicable only to performance + constraint is mapped. Applicable only to second-stage inequality constraints of the separation problem. Returns @@ -532,11 +535,11 @@ def get_con_name_repr(separation_model, con, with_obj_name=True): """ qual_str = "" if with_obj_name: - objectives_map = separation_model.perf_ineq_con_to_obj_map + objectives_map = separation_model.second_stage_ineq_con_to_obj_map separation_obj = objectives_map[con] qual_str = f" (mapped to objective {separation_obj.name!r})" - return f"{con.name!r}{qual_str}" + return f"{con.index()!r}{qual_str}" def perform_separation_loop(separation_data, master_data, config, solve_globally): @@ -559,24 +562,24 @@ def perform_separation_loop(separation_data, master_data, config, solve_globally pyros.solve_data.SeparationLoopResults Separation problem solve results. """ - all_performance_constraints = ( - separation_data.separation_model.effective_performance_inequality_cons + all_ss_ineq_constraints = list( + separation_data.separation_model.second_stage.inequality_cons.values() ) - if not all_performance_constraints: + if not all_ss_ineq_constraints: # robustness certified: no separation problems to solve return SeparationLoopResults( solver_call_results=ComponentMap(), solved_globally=solve_globally, - worst_case_perf_con=None, + worst_case_ss_ineq_con=None, ) # needed for normalizing separation solution constraint violations - separation_data.nom_perf_con_violations = evaluate_violations_by_nominal_master( + separation_data.nom_ss_ineq_con_violations = evaluate_violations_by_nominal_master( separation_data=separation_data, master_data=master_data, - performance_cons=all_performance_constraints, + ss_ineq_cons=all_ss_ineq_constraints, ) - sorted_priority_groups = group_performance_constraints_by_priority( + sorted_priority_groups = group_ss_ineq_constraints_by_priority( separation_data, config ) uncertainty_set_is_discrete = ( @@ -593,11 +596,11 @@ def perform_separation_loop(separation_data, master_data, config, solve_globally return SeparationLoopResults( solver_call_results=ComponentMap(), solved_globally=solve_globally, - worst_case_perf_con=None, + worst_case_ss_ineq_con=None, all_discrete_scenarios_exhausted=True, ) - perf_con_to_maximize = sorted_priority_groups[ + ss_ineq_con_to_maximize = sorted_priority_groups[ max(sorted_priority_groups.keys()) ][0] @@ -608,8 +611,8 @@ def perform_separation_loop(separation_data, master_data, config, solve_globally master_data=master_data, config=config, solve_globally=solve_globally, - perf_con_to_maximize=perf_con_to_maximize, - perf_cons_to_evaluate=all_performance_constraints, + ss_ineq_con_to_maximize=ss_ineq_con_to_maximize, + ss_ineq_cons_to_evaluate=all_ss_ineq_constraints, ) termination_not_ok = ( @@ -623,7 +626,7 @@ def perform_separation_loop(separation_data, master_data, config, solve_globally in discrete_sep_results.solver_call_results.values() for res in solve_call_results.results_list ] - single_solver_call_res[perf_con_to_maximize] = ( + single_solver_call_res[ss_ineq_con_to_maximize] = ( # not the neatest assembly, # but should maintain accuracy of total solve times # and overall outcome @@ -637,33 +640,34 @@ def perform_separation_loop(separation_data, master_data, config, solve_globally return SeparationLoopResults( solver_call_results=single_solver_call_res, solved_globally=solve_globally, - worst_case_perf_con=None, + worst_case_ss_ineq_con=None, ) all_solve_call_results = ComponentMap() priority_groups_enum = enumerate(sorted_priority_groups.items()) - for group_idx, (priority, perf_constraints) in priority_groups_enum: + for group_idx, (priority, ss_ineq_constraints) in priority_groups_enum: priority_group_solve_call_results = ComponentMap() - for idx, perf_con in enumerate(perf_constraints): + for idx, ss_ineq_con in enumerate(ss_ineq_constraints): # log progress of separation loop solve_adverb = "Globally" if solve_globally else "Locally" config.progress_logger.debug( - f"{solve_adverb} separating performance constraint " - f"{get_con_name_repr(separation_data.separation_model, perf_con)} " + f"{solve_adverb} separating second-stage inequality constraint " + f"{get_con_name_repr(separation_data.separation_model, ss_ineq_con)} " f"(priority {priority}, priority group {group_idx + 1} of " f"{len(sorted_priority_groups)}, " - f"constraint {idx + 1} of {len(perf_constraints)} " + f"constraint {idx + 1} of {len(ss_ineq_constraints)} " "in priority group, " f"{len(all_solve_call_results) + idx + 1} of " - f"{len(all_performance_constraints)} total)" + f"{len(all_ss_ineq_constraints)} total)" ) - # solve separation problem for this performance constraint + # solve separation problem for + # this second-stage inequality constraint if uncertainty_set_is_discrete: solve_call_results = get_worst_discrete_separation_solution( - performance_constraint=perf_con, + ss_ineq_con=ss_ineq_con, config=config, - perf_cons_to_evaluate=all_performance_constraints, + ss_ineq_cons_to_evaluate=all_ss_ineq_constraints, discrete_solve_results=discrete_sep_results, ) else: @@ -672,11 +676,11 @@ def perform_separation_loop(separation_data, master_data, config, solve_globally master_data=master_data, config=config, solve_globally=solve_globally, - perf_con_to_maximize=perf_con, - perf_cons_to_evaluate=all_performance_constraints, + ss_ineq_con_to_maximize=ss_ineq_con, + ss_ineq_cons_to_evaluate=all_ss_ineq_constraints, ) - priority_group_solve_call_results[perf_con] = solve_call_results + priority_group_solve_call_results[ss_ineq_con] = solve_call_results termination_not_ok = ( solve_call_results.time_out or solve_call_results.subsolver_error @@ -686,21 +690,21 @@ def perform_separation_loop(separation_data, master_data, config, solve_globally return SeparationLoopResults( solver_call_results=all_solve_call_results, solved_globally=solve_globally, - worst_case_perf_con=None, + worst_case_ss_ineq_con=None, ) all_solve_call_results.update(priority_group_solve_call_results) # there may be multiple separation problem solutions - # found to have violated a performance constraint. + # found to have violated a second-stage inequality constraint. # we choose just one for master problem of next iteration - worst_case_perf_con = get_argmax_sum_violations( + worst_case_ss_ineq_con = get_argmax_sum_violations( solver_call_results_map=all_solve_call_results, - perf_cons_to_evaluate=perf_constraints, + ss_ineq_cons_to_evaluate=ss_ineq_constraints, ) - if worst_case_perf_con is not None: + if worst_case_ss_ineq_con is not None: # take note of chosen separation solution - worst_case_res = all_solve_call_results[worst_case_perf_con] + worst_case_res = all_solve_call_results[worst_case_ss_ineq_con] if uncertainty_set_is_discrete: separation_data.idxs_of_master_scenarios.append( worst_case_res.discrete_set_scenario_index @@ -717,13 +721,13 @@ def perform_separation_loop(separation_data, master_data, config, solve_globally ) config.progress_logger.debug( "Worst-case constraint: " - f"{get_con_name_repr(separation_data.separation_model, worst_case_perf_con)} " + f"{get_con_name_repr(separation_data.separation_model, worst_case_ss_ineq_con)} " "under realization " f"{worst_case_res.violating_param_realization}." ) config.progress_logger.debug( f"Maximal scaled violation " - f"{worst_case_res.scaled_violations[worst_case_perf_con]} " + f"{worst_case_res.scaled_violations[worst_case_ss_ineq_con]} " "from this constraint " "exceeds the robust feasibility tolerance " f"{config.robust_feasibility_tolerance}" @@ -733,17 +737,19 @@ def perform_separation_loop(separation_data, master_data, config, solve_globally # exit loop break else: - config.progress_logger.debug("No violated performance constraints found.") + config.progress_logger.debug( + "No violated second-stage inequality constraints found." + ) return SeparationLoopResults( solver_call_results=all_solve_call_results, solved_globally=solve_globally, - worst_case_perf_con=worst_case_perf_con, + worst_case_ss_ineq_con=worst_case_ss_ineq_con, ) -def evaluate_performance_constraint_violations( - separation_data, config, perf_con_to_maximize, perf_cons_to_evaluate +def evaluate_ss_ineq_con_violations( + separation_data, config, ss_ineq_con_to_maximize, ss_ineq_cons_to_evaluate ): """ Evaluate the inequality constraint function violations @@ -759,8 +765,11 @@ def evaluate_performance_constraint_violations( Object containing the separation model. config : ConfigDict PyROS solver settings. - perf_cons_to_evaluate : list of Constraint - Performance constraints whose expressions are to + ss_ineq_con_to_maximize : ConstraintData + Second-stage inequality constraint + to which the current solution is mapped. + ss_ineq_cons_to_evaluate : list of Constraint + Second-stage inequality constraints whose expressions are to be evaluated at the current separation problem solution. Exactly one of these constraints should be mapped @@ -772,17 +781,17 @@ def evaluate_performance_constraint_violations( Uncertain parameter realization corresponding to maximum constraint violation. scaled_violations : ComponentMap - Mapping from performance constraints to be evaluated + Mapping from second-stage inequality constraints to be evaluated to their violations by the separation problem solution. constraint_violated : bool - True if performance constraint mapped to active + True if second-stage inequality constraint mapped to active separation model Objective is violated (beyond tolerance), False otherwise Raises ------ ValueError - If `perf_cons_to_evaluate` does not contain exactly + If `ss_ineq_cons_to_evaluate` does not contain exactly 1 entry which can be mapped to an active Objective of ``model_data.separation_model``. """ @@ -794,21 +803,24 @@ def evaluate_performance_constraint_violations( param_var.value for param_var in uncertain_param_vars ) - # evaluate violations for all performance constraints provided + # evaluate violations for all second-stage inequality + # constraints provided violations_by_sep_solution = get_sep_objective_values( - separation_data=separation_data, config=config, perf_cons=perf_cons_to_evaluate + separation_data=separation_data, + config=config, + ss_ineq_cons=ss_ineq_cons_to_evaluate ) # normalize constraint violation: i.e. divide by # absolute value of constraint expression evaluated at # nominal master solution (if expression value is large enough) scaled_violations = ComponentMap() - for perf_con, sep_sol_violation in violations_by_sep_solution.items(): + for ss_ineq_con, sep_sol_violation in violations_by_sep_solution.items(): scaled_violation = sep_sol_violation / max( - 1, abs(separation_data.nom_perf_con_violations[perf_con]) + 1, abs(separation_data.nom_ss_ineq_con_violations[ss_ineq_con]) ) - scaled_violations[perf_con] = scaled_violation - if perf_con is perf_con_to_maximize: + scaled_violations[ss_ineq_con] = scaled_violation + if ss_ineq_con is ss_ineq_con_to_maximize: scaled_active_obj_violation = scaled_violation constraint_violated = ( @@ -818,15 +830,16 @@ def evaluate_performance_constraint_violations( return (violating_param_realization, scaled_violations, constraint_violated) -def initialize_separation(perf_con_to_maximize, separation_data, master_data, config): +def initialize_separation(ss_ineq_con_to_maximize, separation_data, master_data, config): """ Initialize separation problem variables using the solution to the most recent master problem. Parameters ---------- - perf_con_to_maximize : ConstraintData - Performance constraint whose violation is to be maximized + ss_ineq_con_to_maximize : ConstraintData + Second-stage inequality constraint + whose violation is to be maximized for the separation problem of interest. separation_data : SeparationProblemData Separation problem data. @@ -848,16 +861,16 @@ def initialize_separation(perf_con_to_maximize, separation_data, master_data, co def eval_master_violation(scenario_idx): """ - Evaluate violation of `perf_con` by variables of + Evaluate violation of `ss_ineq_con` by variables of specified master block. """ master_con = ( - master_model.scenarios[scenario_idx].find_component(perf_con_to_maximize) + master_model.scenarios[scenario_idx].find_component(ss_ineq_con_to_maximize) ) return value(master_con) # initialize from master block with max violation of the - # performance constraint of interest. This gives the best known + # second-stage ineq constraint of interest. Gives the best known # feasible solution (for case of non-discrete uncertainty sets). worst_master_block_idx = max( master_model.scenarios.keys(), @@ -894,9 +907,9 @@ def eval_master_violation(scenario_idx): # revisit initialization of auxiliary uncertainty set # variables later tol = ABS_CON_CHECK_FEAS_TOL - perf_con_name_repr = get_con_name_repr( + ss_ineq_con_name_repr = get_con_name_repr( separation_model=sep_model, - con=perf_con_to_maximize, + con=ss_ineq_con_to_maximize, with_obj_name=True, ) uncertainty_set_is_discrete = ( @@ -911,8 +924,8 @@ def eval_master_violation(scenario_idx): with_obj_name=False, ) config.progress_logger.debug( - f"Initial point for separation of performance constraint " - f"{perf_con_name_repr} violates the model constraint " + f"Initial point for separation of second-stage ineq constraint " + f"{ss_ineq_con_name_repr} violates the model constraint " f"{con_name_repr} by more than {tol}. " f"(lslack={con.lslack()}, uslack={con.uslack()})" ) @@ -924,7 +937,7 @@ def eval_master_violation(scenario_idx): def solver_call_separation( separation_data, master_data, config, - solve_globally, perf_con_to_maximize, perf_cons_to_evaluate + solve_globally, ss_ineq_con_to_maximize, ss_ineq_cons_to_evaluate ): """ Invoke subordinate solver(s) on separation problem. @@ -938,11 +951,12 @@ def solver_call_separation( solve_globally : bool True to solve separation problems globally, False to solve locally. - perf_con_to_maximize : Constraint - Performance constraint for which to solve separation problem. + ss_ineq_con_to_maximize : Constraint + Second-stage inequality constraint + for which to solve separation problem. Informs the objective (constraint violation) to maximize. - perf_cons_to_evaluate : list of Constraint - Performance constraints whose expressions are to be + ss_ineq_cons_to_evaluate : list of Constraint + Second-stage inequality constraints whose expressions are to be evaluated at the separation problem solution obtained. @@ -953,15 +967,15 @@ def solver_call_separation( """ # prepare the problem separation_model = separation_data.separation_model - objectives_map = separation_data.separation_model.perf_ineq_con_to_obj_map - separation_obj = objectives_map[perf_con_to_maximize] - initialize_separation(perf_con_to_maximize, separation_data, master_data, config) + objectives_map = separation_data.separation_model.second_stage_ineq_con_to_obj_map + separation_obj = objectives_map[ss_ineq_con_to_maximize] + initialize_separation(ss_ineq_con_to_maximize, separation_data, master_data, config) separation_obj.activate() - # get name of constraint for loggers + # get name (index) of constraint for loggers con_name_repr = get_con_name_repr( separation_model=separation_model, - con=perf_con_to_maximize, + con=ss_ineq_con_to_maximize, with_obj_name=True, ) @@ -986,7 +1000,7 @@ def solver_call_separation( config.progress_logger.warning( f"Invoking backup solver {opt!r} " f"(solver {idx + 1} of {len(solvers)}) for {solve_mode} " - f"separation of performance constraint {con_name_repr} " + f"separation of second-stage inequality constraint {con_name_repr} " f"in iteration {separation_data.iteration}." ) results = call_solver( @@ -1035,8 +1049,8 @@ def solver_call_separation( solve_call_results.violating_param_realization, solve_call_results.scaled_violations, solve_call_results.found_violation, - ) = evaluate_performance_constraint_violations( - separation_data, config, perf_con_to_maximize, perf_cons_to_evaluate + ) = evaluate_ss_ineq_con_violations( + separation_data, config, ss_ineq_con_to_maximize, ss_ineq_cons_to_evaluate ) solve_call_results.auxiliary_param_values = [ auxvar.value @@ -1049,7 +1063,7 @@ def solver_call_separation( else: config.progress_logger.debug( f"Solver {opt} ({idx + 1} of {len(solvers)}) " - f"failed for {solve_mode} separation of performance " + f"failed for {solve_mode} separation of second-stage inequality " f"constraint {con_name_repr} in iteration " f"{separation_data.iteration}. Termination condition: " f"{results.solver.termination_condition!r}." @@ -1087,7 +1101,7 @@ def solver_call_separation( solve_call_results.message = ( "Could not successfully solve separation problem of iteration " f"{separation_data.iteration} " - f"for performance constraint {con_name_repr} with any of the " + f"for second-stage inequality constraint {con_name_repr} with any of the " f"provided subordinate {solve_mode} optimizers. " f"(Termination statuses: " f"{[str(term_cond) for term_cond in solver_status_dict.values()]}.)" @@ -1102,7 +1116,7 @@ def solver_call_separation( def discrete_solve( separation_data, master_data, config, - solve_globally, perf_con_to_maximize, perf_cons_to_evaluate + solve_globally, ss_ineq_con_to_maximize, ss_ineq_cons_to_evaluate ): """ Obtain separation problem solution for each scenario @@ -1120,18 +1134,18 @@ def discrete_solve( the model. solve_globally : bool Is separation problem to be solved globally. - perf_con_to_maximize : Constraint - Performance constraint for which to solve separation + ss_ineq_con_to_maximize : Constraint + Second-stage inequality constraint for which to solve separation problem. - perf_cons_to_evaluate : list of Constraint - Performance constraints whose expressions are to be + ss_ineq_cons_to_evaluate : list of Constraint + Secnod-stage inequality constraints whose expressions are to be evaluated at the each of separation problem solutions obtained. Returns ------- discrete_separation_results : DiscreteSeparationSolveCallResults - Separation solver call results on performance constraint + Separation solver call results on second-stage inequality constraint of interest for every scenario considered. Notes @@ -1140,8 +1154,9 @@ def discrete_solve( variables and uncertain parameter values uniquely define the state variables, this method need be only be invoked once per separation loop. Subject to our assumption, the choice of objective - (``perf_con_to_maximize``) should not affect the solutions returned - beyond subsolver tolerances. For other performance constraints, the + (``ss_ineq_con_to_maximize``) should not affect the solutions returned + beyond subsolver tolerances. + For other second-stage inequality constraints, the optimal separation problem solution can then be evaluated by simple enumeration of the solutions returned by this function, since for discrete uncertainty sets, the number of feasible separation @@ -1175,8 +1190,8 @@ def discrete_solve( master_data=master_data, config=config, solve_globally=solve_globally, - perf_con_to_maximize=perf_con_to_maximize, - perf_cons_to_evaluate=perf_cons_to_evaluate, + ss_ineq_con_to_maximize=ss_ineq_con_to_maximize, + ss_ineq_cons_to_evaluate=ss_ineq_cons_to_evaluate, ) solve_call_results.discrete_set_scenario_index = scenario_idx solve_call_results_dict[scenario_idx] = solve_call_results @@ -1191,7 +1206,7 @@ def discrete_solve( return DiscreteSeparationSolveCallResults( solved_globally=solve_globally, solver_call_results=solve_call_results_dict, - performance_constraint=perf_con_to_maximize, + second_stage_ineq_con=ss_ineq_con_to_maximize, ) diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index 2e7b506f40b..9fae2eb32ef 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -113,7 +113,7 @@ class SeparationSolveCallResults: subordinate local/global solvers provided (including backup) and the number of scenarios in the uncertainty set. scaled_violations : ComponentMap, optional - Mapping from performance constraints to floats equal + Mapping from second-stage inequality constraints to floats equal to their scaled violations by separation problem solution stored in this result. violating_param_realization : list of float, optional @@ -126,9 +126,9 @@ class SeparationSolveCallResults: Second-stage DOF and state variable values for reported separation problem solution. found_violation : bool, optional - True if violation of performance constraint (i.e. constraint - expression value) by reported separation solution was found to - exceed tolerance, False otherwise. + True if violation of second-stage inequality constraint + (i.e. constraint expression value) by reported separation + solution was found to exceed tolerance, False otherwise. time_out : bool, optional True if PyROS time limit reached attempting to solve the separation problem, False otherwise. @@ -240,8 +240,8 @@ class DiscreteSeparationSolveCallResults: Mapping from discrete uncertainty set scenario list indexes to solver call results for separation problems subject to the scenarios. - performance_constraint : Constraint - Separation problem performance constraint for which + second_stage_ineq_con : Constraint + Separation problem second-stage inequality constraint for which `self` was generated. Attributes @@ -249,18 +249,18 @@ class DiscreteSeparationSolveCallResults: solved_globally scenario_indexes solver_call_results - performance_constraint + second_stage_ineq_con time_out subsolver_error """ def __init__( - self, solved_globally, solver_call_results=None, performance_constraint=None + self, solved_globally, solver_call_results=None, second_stage_ineq_con=None ): """Initialize self (see class docstring).""" self.solved_globally = solved_globally self.solver_call_results = solver_call_results - self.performance_constraint = performance_constraint + self.second_stage_ineq_con = second_stage_ineq_con @property def time_out(self): @@ -317,10 +317,11 @@ class SeparationLoopResults: True if separation problems were solved to global optimality, False otherwise. solver_call_results : ComponentMap - Mapping from performance constraints to corresponding + Mapping from second-stage inequality constraints to corresponding ``SeparationSolveCallResults`` objects. - worst_case_perf_con : None or Constraint - Performance constraint mapped to ``SeparationSolveCallResults`` + worst_case_ss_ineq_con : None or Constraint + Second-stage inequality constraint mapped to + ``SeparationSolveCallResults`` object in `self` corresponding to maximally violating separation problem solution. all_discrete_scenarios_exhausted : bool, optional @@ -334,7 +335,7 @@ class SeparationLoopResults: ---------- solver_call_results solved_globally - worst_case_perf_con + worst_case_ss_ineq_con all_discrete_scenarios_exhausted found_violation violating_param_realization @@ -349,13 +350,13 @@ def __init__( self, solved_globally, solver_call_results, - worst_case_perf_con, + worst_case_ss_ineq_con, all_discrete_scenarios_exhausted=False, ): """Initialize self (see class docstring).""" self.solver_call_results = solver_call_results self.solved_globally = solved_globally - self.worst_case_perf_con = worst_case_perf_con + self.worst_case_ss_ineq_con = worst_case_ss_ineq_con self.all_discrete_scenarios_exhausted = all_discrete_scenarios_exhausted @property @@ -363,8 +364,8 @@ def found_violation(self): """ bool : True if separation solution for at least one ``SeparationSolveCallResults`` object listed in self - was reported to violate its corresponding performance - constraint, False otherwise. + was reported to violate its corresponding second-stage + inequality constraint, False otherwise. """ return any( solver_call_res.found_violation @@ -377,13 +378,13 @@ def violating_param_realization(self): None or list of float : Uncertain parameter values for for maximally violating separation problem solution, specified according to solver call results object - listed in self at index ``self.worst_case_perf_con``. - If ``self.worst_case_perf_con`` is not specified, + listed in self at index ``self.worst_case_ss_ineq_con``. + If ``self.worst_case_ss_ineq_con`` is not specified, then None is returned. """ - if self.worst_case_perf_con is not None: + if self.worst_case_ss_ineq_con is not None: return self.solver_call_results[ - self.worst_case_perf_con + self.worst_case_ss_ineq_con ].violating_param_realization else: return None @@ -394,9 +395,9 @@ def auxiliary_param_values(self): None or list of float : Auxiliary parameter values for the maximially violating separation problem solution. """ - if self.worst_case_perf_con is not None: + if self.worst_case_ss_ineq_con is not None: return self.solver_call_results[ - self.worst_case_perf_con + self.worst_case_ss_ineq_con ].auxiliary_param_values else: return None @@ -404,15 +405,16 @@ def auxiliary_param_values(self): @property def scaled_violations(self): """ - None or ComponentMap : Scaled performance constraint violations + None or ComponentMap : Scaled second-stage inequality + constraint violations for maximally violating separation problem solution, specified according to solver call results object - listed in self at index ``self.worst_case_perf_con``. - If ``self.worst_case_perf_con`` is not specified, + listed in self at index ``self.worst_case_ss_ineq_con``. + If ``self.worst_case_ss_ineq_con`` is not specified, then None is returned. """ - if self.worst_case_perf_con is not None: - return self.solver_call_results[self.worst_case_perf_con].scaled_violations + if self.worst_case_ss_ineq_con is not None: + return self.solver_call_results[self.worst_case_ss_ineq_con].scaled_violations else: return None @@ -422,20 +424,20 @@ def violating_separation_variable_values(self): None or ComponentMap : Second-stage and state variable values for maximally violating separation problem solution, specified according to solver call results object - listed in self at index ``self.worst_case_perf_con``. - If ``self.worst_case_perf_con`` is not specified, + listed in self at index ``self.worst_case_ss_ineq_con``. + If ``self.worst_case_ss_ineq_con`` is not specified, then None is returned. """ - if self.worst_case_perf_con is not None: - return self.solver_call_results[self.worst_case_perf_con].variable_values + if self.worst_case_ss_ineq_con is not None: + return self.solver_call_results[self.worst_case_ss_ineq_con].variable_values else: return None @property - def violated_performance_constraints(self): + def violated_second_stage_ineq_cons(self): """ - list of Constraint : Performance constraints for which violation - found. + list of Constraint : Second-stage inequality constraints + for which violation found. """ return [ con @@ -612,12 +614,13 @@ def all_discrete_scenarios_exhausted(self): return self.get_violating_attr("all_discrete_scenarios_exhausted") @property - def worst_case_perf_con(self): + def worst_case_ss_ineq_con(self): """ - ConstraintData : Performance constraint corresponding to the + ConstraintData : Second-stage inequality constraint + corresponding to the separation solution chosen for the next master problem. """ - return self.get_violating_attr("worst_case_perf_con") + return self.get_violating_attr("worst_case_ss_ineq_con") @property def main_loop_results(self): @@ -648,7 +651,7 @@ def violating_param_realization(self): None or list of float : Uncertain parameter values for maximally violating separation problem solution reported in local or global separation loop results. - If no such solution found, (i.e. ``worst_case_perf_con`` + If no such solution found, (i.e. ``worst_case_ss_ineq_con`` set to None for both local and global loop results), then None is returned. """ @@ -665,10 +668,11 @@ def auxiliary_param_values(self): @property def scaled_violations(self): """ - None or ComponentMap : Scaled performance constraint violations + None or ComponentMap : + Scaled second-stage inequality constraint violations for maximally violating separation problem solution reported in local or global separation loop results. - If no such solution found, (i.e. ``worst_case_perf_con`` + If no such solution found, (i.e. ``worst_case_ss_ineq_con`` set to None for both local and global loop results), then None is returned. """ @@ -680,18 +684,18 @@ def violating_separation_variable_values(self): None or ComponentMap : Second-stage and state variable values for maximally violating separation problem solution reported in local or global separation loop results. - If no such solution found, (i.e. ``worst_case_perf_con`` + If no such solution found, (i.e. ``worst_case_ss_ineq_con`` set to None for both local and global loop results), then None is returned. """ return self.get_violating_attr("violating_separation_variable_values") @property - def violated_performance_constraints(self): + def violated_second_stage_ineq_cons(self): """ - Return list of violated performance constraints. + Return list of violated second-stage inequality constraints. """ - return self.get_violating_attr("violated_performance_constraints") + return self.get_violating_attr("violated_second_stage_ineq_cons") def evaluate_local_solve_time(self, evaluator_func, **evaluator_func_kwargs): """ diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index cc6e927156e..dac3d23f256 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -1197,7 +1197,7 @@ def test_pyros_math_domain_error(self): """ Test PyROS on a two-stage problem, discrete set type with a math domain error evaluating - performance constraint expressions in separation. + second-stage inequality constraint expressions in separation. """ m = ConcreteModel() m.q = Param(initialize=1, mutable=True) @@ -1214,7 +1214,7 @@ def test_pyros_math_domain_error(self): with self.assertRaisesRegex( expected_exception=ArithmeticError, expected_regex=( - "Evaluation of performance constraint.*math domain error.*" + "Evaluation of second-stage inequality constraint.*math domain error.*" ), msg="ValueError arising from math domain error not raised", ): @@ -1240,8 +1240,8 @@ def test_pyros_math_domain_error(self): def test_pyros_no_perf_cons(self): """ Ensure PyROS properly accommodates models with no - performance constraints (such as effectively deterministic - models). + second-stage inequality constraints + (such as effectively deterministic models). """ m = ConcreteModel() m.x = Var(bounds=(0, 1)) @@ -1711,7 +1711,7 @@ def test_coefficient_matching_nonlinear_expr(self): pyros_log = LOG.getvalue() self.assertRegex( pyros_log, - r".*Equality constraint 'user_model\.eq_con'.*cannot be written.*", + r".*Equality constraint '.*eq_con.*'.*cannot be written.*", ) # should still solve in spite of coefficient matching @@ -2361,8 +2361,8 @@ def test_log_iter_record_not_all_sep_solved(self): solver time limit was reached, or the user-provides subordinate optimizer(s) were unable to solve a separation subproblem to an acceptable level. - A '+' should be appended to the number of performance constraints - found to be violated. + A '+' should be appended to the number of second-stage + inequality constraints found to be violated. """ # for some fields, we choose floats with more than four # four decimal points to ensure rounding also matches diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index ced00f5925f..0240caeea99 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -116,7 +116,7 @@ def test_initial_construct_master(self): self.assertTrue(master_model.epigraph_obj.active) self.assertIs( master_model.epigraph_obj.expr, - master_model.scenarios[0, 0].epigraph_var, + master_model.scenarios[0, 0].first_stage.epigraph_var, ) # check all the variables (including first-stage ones) @@ -171,8 +171,8 @@ def test_add_scenario_block_to_master(self): # should be cloned. we do this to avoid issues with the solver # interfaces (such as issues with manipulating symbol maps) nadj_ineq_con_zip = zip( - master_model.scenarios[0, 0].effective_first_stage_inequality_cons, - master_model.scenarios[0, 1].effective_first_stage_inequality_cons, + master_model.scenarios[0, 0].first_stage.inequality_cons.values(), + master_model.scenarios[0, 1].first_stage.inequality_cons.values(), ) for ineq_con_00, ineq_con_01 in nadj_ineq_con_zip: self.assertIsNot( @@ -199,8 +199,8 @@ def test_add_scenario_block_to_master(self): ) nadj_eq_con_zip = zip( - master_model.scenarios[0, 0].effective_first_stage_equality_cons, - master_model.scenarios[0, 1].effective_first_stage_equality_cons, + master_model.scenarios[0, 0].first_stage.equality_cons.values(), + master_model.scenarios[0, 1].first_stage.equality_cons.values(), ) for eq_con_00, eq_con_01 in nadj_eq_con_zip: self.assertIsNot( @@ -281,12 +281,12 @@ def test_construct_master_feasibility_problem_slack_vars(self): scenario_10_blk = slack_model.scenarios[1, 0] # test a few of the constraints - slack_user_model_x3_lb_con = ( - scenario_10_blk.user_model.var_x3_certain_lower_bound_con - ) + slack_user_model_x3_lb_con = scenario_10_blk.second_stage.inequality_cons[ + "var_x3_certain_lower_bound_con" + ] slack_user_model_x3_lb_con_var = slack_var_blk.find_component( - "'_slack_minus_scenarios[1,0].user_model." - "var_x3_certain_lower_bound_con'" + "'_slack_minus_scenarios[1,0].second_stage.inequality_cons[" + "var_x3_certain_lower_bound_con]'" ) assertExpressionsEqual( self, @@ -295,12 +295,12 @@ def test_construct_master_feasibility_problem_slack_vars(self): ) self.assertEqual(slack_user_model_x3_lb_con_var.value, 0) - slack_user_model_x3_ub_con = ( - scenario_10_blk.user_model.var_x3_certain_upper_bound_con - ) + slack_user_model_x3_ub_con = scenario_10_blk.second_stage.inequality_cons[ + "var_x3_certain_upper_bound_con" + ] slack_user_model_x3_ub_con_var = slack_var_blk.find_component( - "'_slack_minus_scenarios[1,0].user_model." - "var_x3_certain_upper_bound_con'" + "'_slack_minus_scenarios[1,0].second_stage.inequality_cons[" + "var_x3_certain_upper_bound_con]'" ) assertExpressionsEqual( self, @@ -312,7 +312,8 @@ def test_construct_master_feasibility_problem_slack_vars(self): # constraint 'con' is violated when u = 0.8; # check slack initialization slack_user_model_con_var = slack_var_blk.find_component( - "'_slack_minus_scenarios[1,0].user_model.con'" + "'_slack_minus_scenarios[1,0].second_stage.inequality_cons" + "[ineq_con_con_upper_bound_con]'" ) self.assertEqual( slack_user_model_con_var.value, @@ -378,17 +379,21 @@ def test_construct_dr_polishing_problem_nonadj_components(self): ) nom_polishing_block = polishing_model.scenarios[0, 0] - self.assertTrue(nom_polishing_block.epigraph_var.fixed) - self.assertFalse(nom_polishing_block.decision_rule_vars[0][0].fixed) - self.assertFalse(nom_polishing_block.decision_rule_vars[0][1].fixed) + self.assertTrue(nom_polishing_block.first_stage.epigraph_var.fixed) + self.assertFalse(nom_polishing_block.first_stage.decision_rule_vars[0][0].fixed) + self.assertFalse(nom_polishing_block.first_stage.decision_rule_vars[0][1].fixed) # ensure constraints in fixed vars were deactivated self.assertFalse(nom_polishing_block.user_model.eq_con.active) # these have either unfixed DR or adjustable variables, # so they should remain active - self.assertTrue(nom_polishing_block.user_model.con.active) - self.assertTrue(nom_polishing_block.decision_rule_eqns[0].active) + # self.assertTrue(nom_polishing_block.user_model.con.active) + self.assertTrue( + nom_polishing_block + .second_stage.inequality_cons["ineq_con_con_upper_bound_con"].active + ) + self.assertTrue(nom_polishing_block.second_stage.decision_rule_eqns[0].active) def test_construct_dr_polishing_problem_polishing_components(self): """ @@ -398,18 +403,21 @@ def test_construct_dr_polishing_problem_polishing_components(self): master_data, config = self.build_simple_master_data() # DR order is 1, and x3 is second-stage. # to test fixing efficiency, fix the affine DR variable - master_data.master_model.scenarios[0, 0].decision_rule_vars[0][1].fix() + decision_rule_vars = ( + master_data.master_model.scenarios[0, 0].first_stage.decision_rule_vars + ) + decision_rule_vars[0][1].fix() polishing_model = construct_dr_polishing_problem(master_data, config) - nom_polishing_block = polishing_model.scenarios[0, 0] - self.assertFalse(nom_polishing_block.decision_rule_vars[0][0].fixed) + + self.assertFalse(decision_rule_vars[0][0].fixed) self.assertTrue(polishing_model.polishing_vars[0][0].fixed) self.assertFalse(polishing_model.polishing_abs_val_lb_con_0[0].active) self.assertFalse(polishing_model.polishing_abs_val_ub_con_0[0].active) # polishing components for the affine DR term should be # fixed/deactivated since the DR variable was fixed - self.assertTrue(nom_polishing_block.decision_rule_vars[0][1].fixed) + self.assertTrue(decision_rule_vars[0][1].fixed) self.assertTrue(polishing_model.polishing_vars[0][1].fixed) self.assertFalse(polishing_model.polishing_abs_val_lb_con_0[1].active) self.assertFalse(polishing_model.polishing_abs_val_ub_con_0[1].active) @@ -417,11 +425,11 @@ def test_construct_dr_polishing_problem_polishing_components(self): # check initialization of polishing vars self.assertEqual( polishing_model.polishing_vars[0][0].value, - abs(nom_polishing_block.decision_rule_vars[0][0].value), + abs(nom_polishing_block.first_stage.decision_rule_vars[0][0].value), ) self.assertEqual( polishing_model.polishing_vars[0][1].value, - abs(nom_polishing_block.decision_rule_vars[0][1].value), + abs(nom_polishing_block.first_stage.decision_rule_vars[0][1].value), ) assertExpressionsEqual( diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index a226c5c547d..673ab3509d6 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -24,6 +24,7 @@ from pyomo.common.dependencies import attempt_import from pyomo.common.log import LoggingIntercept from pyomo.core.base import ( + Any, Var, Constraint, Objective, @@ -39,8 +40,6 @@ Reals, ) from pyomo.core.expr import ( - identify_mutable_parameters, - identify_variables, log, sin, exp, @@ -474,10 +473,10 @@ def test_setup_working_model(self): ) # constraint partitioning initialization - self.assertEqual(working_model.effective_first_stage_inequality_cons, []) - self.assertEqual(working_model.effective_performance_inequality_cons, []) - self.assertEqual(working_model.effective_first_stage_equality_cons, []) - self.assertEqual(working_model.effective_performance_equality_cons, []) + self.assertFalse(working_model.first_stage.inequality_cons) + self.assertFalse(working_model.first_stage.equality_cons) + self.assertFalse(working_model.second_stage.inequality_cons) + self.assertFalse(working_model.second_stage.equality_cons) class TestResolveVarBounds(unittest.TestCase): @@ -613,8 +612,10 @@ def build_simple_test_model_data(self): m.z10 = Var(domain=RangeSet(0, 5, 0), bounds=[m.q1, m.p2]) model_data.working_model.uncertain_params = [m.q1, m.q2] - model_data.working_model.effective_performance_equality_cons = [] - model_data.working_model.effective_performance_inequality_cons = [] + + model_data.working_model.second_stage = Block() + model_data.working_model.second_stage.inequality_cons = Constraint(Any) + model_data.working_model.second_stage.equality_cons = Constraint(Any) return model_data @@ -720,72 +721,50 @@ def test_turn_nonadjustable_bounds_to_constraints(self): ), ) - for cbtype in con_bound_types: - # verify the bound constraints were added - # and are as expected - varname = var.getname( - relative_to=m, fully_qualified=True - ) - bound_con = model_data.working_model.user_model.find_component( - f"var_{varname}_uncertain_{cbtype}_bound_con" - ) - self.assertIsNotNone( - bound_con, - msg=f"Bound constraint for variable {var.name!r} not found." - ) - if cbtype == "eq": - self.assertIn( - bound_con, - working_model.effective_performance_equality_cons, - msg=( - "Bound constraint " - f"{bound_con.name!r} " - "not in first-stage equality constraint set." - ), - ) - else: - self.assertIn( - bound_con, - working_model.effective_performance_inequality_cons, - msg=( - "Bound constraint " - f"{bound_con.name!r} " - "not in first-stage inequality constraint set." - ), - ) + second_stage = working_model.second_stage # verify bound constraint expressions assertExpressionsEqual( self, - m.var_z4_uncertain_lower_bound_con.expr, -m.z4 <= -m.q1 + second_stage.inequality_cons["var_z4_uncertain_lower_bound_con"].expr, + -m.z4 <= -m.q1, ) assertExpressionsEqual( self, - m.var_z5_uncertain_upper_bound_con.expr, m.z5 <= m.q2 + second_stage.inequality_cons["var_z5_uncertain_upper_bound_con"].expr, + m.z5 <= m.q2, ) assertExpressionsEqual( - self, m.var_z6_uncertain_eq_bound_con.expr, m.z6 == m.q1 + self, + second_stage.equality_cons["var_z6_uncertain_eq_bound_con"].expr, + m.z6 == m.q1 ) assertExpressionsEqual( - self, m.var_z7_uncertain_eq_bound_con.expr, m.z7 == m.q1 + self, + second_stage.equality_cons["var_z7_uncertain_eq_bound_con"].expr, + m.z7 == m.q1, ) assertExpressionsEqual( - self, m.var_z8_uncertain_lower_bound_con.expr, -m.z8 <= -m.q1, + self, + second_stage.inequality_cons["var_z8_uncertain_lower_bound_con"].expr, + -m.z8 <= -m.q1, ) assertExpressionsEqual( - self, m.var_z8_uncertain_upper_bound_con.expr, m.z8 <= m.q2, + self, + second_stage.inequality_cons["var_z8_uncertain_upper_bound_con"].expr, + m.z8 <= m.q2, ) # check constraint partitioning self.assertEqual( - len(working_model.effective_performance_inequality_cons), + len(working_model.second_stage.inequality_cons), 4, - msg="Number of performance inequalities not as expected.," + msg="Number of second-stage inequalities not as expected." ) self.assertEqual( - len(working_model.effective_performance_equality_cons), + len(working_model.second_stage.equality_cons), 2, - msg="Number of performance equalities not as expected.," + msg="Number of second-stage equalities not as expected." ) def test_turn_adjustable_bounds_to_constraints(self): @@ -803,7 +782,6 @@ def test_turn_adjustable_bounds_to_constraints(self): model_data = self.build_simple_test_model_data() m = model_data.working_model.user_model - uncertain_params_set = ComponentSet(model_data.working_model.uncertain_params) # simple mock partitioning for the test ep = model_data.working_model.effective_var_partitioning = Bunch() @@ -817,24 +795,8 @@ def test_turn_adjustable_bounds_to_constraints(self): for var in model_data.working_model.user_model.component_data_objects(Var) ) - # for checking the correct bound constraints were - # added. - # - first list: types of certain bound constraints - # that should have been added - # - second list: types of uncertain bound constraints - # that should have been added - expected_cert_uncert_bound_con_types = ComponentMap(( - (m.z1, ([], [])), - (m.z2, (["eq"], [])), - (m.z3, (["lower", "upper"], [])), - (m.z4, (["eq"], ["lower"])), - (m.z5, (["eq"], ["upper"])), - (m.z6, (["lower"], ["eq"])), - (m.z7, (["upper"], ["eq"])), - (m.z8, (["lower", "upper"], ["lower", "upper"])), - )) - turn_adjustable_var_bounds_to_constraints(model_data) + for var, (orig_domain, orig_bounds) in original_var_domains_and_bounds.items(): _, (final_lb, final_ub) = get_var_bound_pairs(var) if var not in effective_first_stage_var_set: @@ -869,102 +831,6 @@ def test_turn_adjustable_bounds_to_constraints(self): f"{final_ub}." ), ) - - # check the constraints added are as expected: - # they are present, involve only the variable - # of interest, and where applicable, the - # uncertain parameters - varname = var.getname( - relative_to=m, fully_qualified=True - ) - cert_bound_con_types, uncert_bound_con_types = ( - expected_cert_uncert_bound_con_types[var] - ) - for ccbtype in cert_bound_con_types: - cert_bound_con_name = f"var_{varname}_certain_{ccbtype}_bound_con" - - cert_bound_con = model_data.working_model.user_model.find_component( - cert_bound_con_name - ) - self.assertIsNotNone( - cert_bound_con, - msg=( - f"Expected working model to contain a certain {ccbtype} " - f"bound constraint with name {cert_bound_con_name!r}, " - f"for the variable {var.name!r}, " - "but no such constraint was not found." - ) - ) - vars_in_bound_con = ComponentSet( - identify_variables(cert_bound_con.body) - ) - self.assertEqual( - vars_in_bound_con, - ComponentSet((var,)), - msg=( - f"Bound constraint {cert_bound_con.name} should involve " - f"only the variable with name {var.name!r}, but involves " - f"the variables {vars_in_bound_con}." - ), - ) - - uncertain_params_in_bound_con = ComponentSet( - identify_mutable_parameters(cert_bound_con.body) - & uncertain_params_set - ) - self.assertFalse( - uncertain_params_in_bound_con, - msg=( - f"Uncertain parameters were found in the expression " - "of the bound constraint with name" - f"{cert_bound_con.name!r}; expression is " - f"{cert_bound_con.expr}" - ), - ) - - for ucbtype in uncert_bound_con_types: - unc_bound_con_name = f"var_{varname}_uncertain_{ucbtype}_bound_con" - unc_bound_con = model_data.working_model.user_model.find_component( - unc_bound_con_name - ) - - self.assertIsNotNone( - unc_bound_con, - msg=( - f"Expected working model to contain an uncertain {ucbtype} " - f"bound constraint with name {unc_bound_con_name!r}, " - f"for the variable {var.name!r}, " - "but no such constraint was not found." - ), - ) - - vars_in_bound_con = ComponentSet( - identify_variables(unc_bound_con.body) - ) - self.assertEqual( - vars_in_bound_con, - ComponentSet((var,)), - msg=( - f"Bound constraint {unc_bound_con.name} should involve " - f"only the variable with name {var.name!r}, but involves " - f"the variables {vars_in_bound_con}." - ), - ) - - # we want to ensure that uncertain params, - # rather than their values, - # are used to create the bound constraints - uncertain_params_in_bound_con = ComponentSet( - identify_mutable_parameters(unc_bound_con.expr) - & uncertain_params_set - ) - self.assertTrue( - uncertain_params_in_bound_con, - msg=( - f"No uncertain parameters were found in the bound " - f"constraint with name {unc_bound_con.name!r}." - ), - ) else: # these are the nonadjustable variables. # domains and bounds should be left unchanged @@ -996,94 +862,87 @@ def test_turn_adjustable_bounds_to_constraints(self): ), ) + second_stage = model_data.working_model.second_stage + # verify bound constraint expressions assertExpressionsEqual( self, - m.var_z2_certain_eq_bound_con.expr, + second_stage.equality_cons["var_z2_certain_eq_bound_con"].expr, m.z2 == 1, ) assertExpressionsEqual( self, - m.var_z3_certain_lower_bound_con.expr, + second_stage.inequality_cons["var_z3_certain_lower_bound_con"].expr, -m.z3 <= -2, ) assertExpressionsEqual( self, - m.var_z3_certain_upper_bound_con.expr, + second_stage.inequality_cons["var_z3_certain_upper_bound_con"].expr, m.z3 <= m.p1, ) assertExpressionsEqual( self, - m.var_z4_certain_eq_bound_con.expr, + second_stage.equality_cons["var_z4_certain_eq_bound_con"].expr, m.z4 == 0, ) assertExpressionsEqual( self, - m.var_z4_uncertain_lower_bound_con.expr, + second_stage.inequality_cons["var_z4_uncertain_lower_bound_con"].expr, - m.z4 <= -m.q1, ) assertExpressionsEqual( self, - m.var_z5_certain_eq_bound_con.expr, + second_stage.equality_cons["var_z5_certain_eq_bound_con"].expr, m.z5 == 4, ) assertExpressionsEqual( self, - m.var_z5_uncertain_upper_bound_con.expr, + second_stage.inequality_cons["var_z5_uncertain_upper_bound_con"].expr, m.z5 <= m.q2, ) assertExpressionsEqual( self, - m.var_z6_certain_lower_bound_con.expr, + second_stage.inequality_cons["var_z6_certain_lower_bound_con"].expr, -m.z6 <= 0, ) assertExpressionsEqual( self, - m.var_z6_uncertain_eq_bound_con.expr, + second_stage.equality_cons["var_z6_uncertain_eq_bound_con"].expr, m.z6 == m.q1, ) assertExpressionsEqual( self, - m.var_z7_certain_upper_bound_con.expr, + second_stage.inequality_cons["var_z7_certain_upper_bound_con"].expr, m.z7 <= 0, ) assertExpressionsEqual( self, - m.var_z7_uncertain_eq_bound_con.expr, + second_stage.equality_cons["var_z7_uncertain_eq_bound_con"].expr, m.z7 == m.q1, ) assertExpressionsEqual( self, - m.var_z8_certain_lower_bound_con.expr, + second_stage.inequality_cons["var_z8_certain_lower_bound_con"].expr, -m.z8 <= 0, ) assertExpressionsEqual( self, - m.var_z8_certain_upper_bound_con.expr, + second_stage.inequality_cons["var_z8_certain_upper_bound_con"].expr, m.z8 <= 5, ) assertExpressionsEqual( self, - m.var_z8_uncertain_lower_bound_con.expr, + second_stage.inequality_cons["var_z8_uncertain_lower_bound_con"].expr, - m.z8 <= -m.q1, ) assertExpressionsEqual( self, - m.var_z8_uncertain_upper_bound_con.expr, + second_stage.inequality_cons["var_z8_uncertain_upper_bound_con"].expr, m.z8 <= m.q2, ) - working_model = model_data.working_model - self.assertEqual( - len(working_model.effective_performance_inequality_cons), - 10, - msg="Number of performance inequalty constraints not as expected.", - ) - self.assertEqual( - len(working_model.effective_performance_equality_cons), - 5, - msg="Number of performance equalty constraints not as expected.", - ) + self.assertEqual(len(second_stage.inequality_cons), 10) + self.assertEqual(len(second_stage.equality_cons), 5) class TestStandardizeInequalityConstraints(unittest.TestCase): @@ -1126,8 +985,10 @@ def build_simple_test_model_data(self): model_data.working_model.uncertain_params = [m.q] - model_data.working_model.effective_first_stage_inequality_cons = [] - model_data.working_model.effective_performance_inequality_cons = [] + model_data.working_model.first_stage = Block() + model_data.working_model.first_stage.inequality_cons = Constraint(Any) + model_data.working_model.second_stage = Block() + model_data.working_model.second_stage.inequality_cons = Constraint(Any) model_data.working_model.original_active_inequality_cons = [ m.c1, m.c2, m.c3, m.c4, m.c5, m.c6, m.c7, m.c8, m.c9, m.c10, m.c12, @@ -1149,116 +1010,109 @@ def test_standardize_inequality_constraints(self): m = working_model.user_model standardize_inequality_constraints(model_data) - m = working_model.user_model - self.assertEqual(len(working_model.effective_first_stage_inequality_cons), 4) - self.assertEqual(len(working_model.effective_performance_inequality_cons), 12) + fs_ineq_cons = working_model.first_stage.inequality_cons + ss_ineq_cons = working_model.second_stage.inequality_cons - self.assertTrue(m.c1.active) - self.assertIn(m.c1, working_model.effective_first_stage_inequality_cons) - assertExpressionsEqual(self, m.c1.expr, m.x1 <= 1) + self.assertEqual(len(fs_ineq_cons), 4) + self.assertEqual(len(ss_ineq_cons), 12) + + self.assertFalse(m.c1.active) + fs_ineq_cons.pprint() + new_c1_con = fs_ineq_cons["ineq_con_c1"] + self.assertTrue(new_c1_con.active) + assertExpressionsEqual(self, new_c1_con.expr, m.x1 <= 1) # 1 <= m.x1 <= 2; first-stage constraint. no modification - self.assertTrue(m.c2.active) - self.assertIn(m.c2, working_model.effective_first_stage_inequality_cons) + self.assertFalse(m.c2.active) + new_c2_con = fs_ineq_cons["ineq_con_c2"] + self.assertTrue(new_c2_con.active) assertExpressionsEqual( - self, m.c2.expr, RangedExpression((1, m.x1, 2), False) + self, new_c2_con.expr, RangedExpression((1, m.x1, 2), False) ) - # m.q <= m.x1; single performance inequality. modify in place - self.assertTrue(m.c3.active) - self.assertIn(m.c3, working_model.effective_performance_inequality_cons) - assertExpressionsEqual(self, m.c3.expr, - m.x1 <= -m.q) + # m.q <= m.x1; single second-stage inequality. modify in place + self.assertFalse(m.c3.active) + new_c3_con = ss_ineq_cons["ineq_con_c3_lower_bound_con"] + self.assertTrue(new_c3_con.active) + assertExpressionsEqual(self, new_c3_con.expr, - m.x1 <= -m.q) # log(m.p) <= m.x2 <= m.q - # log(m.p) <= m.x2 stays in place as first-stage inequality - # m.x2 - m.q <= 0 added as performance inequality - self.assertTrue(m.c4.active) - c4_upper_bound_con = m.find_component("con_c4_upper_bound_con") - self.assertIn(m.c4, working_model.effective_first_stage_inequality_cons) - self.assertIn( - c4_upper_bound_con, - working_model.effective_performance_inequality_cons, - ) - assertExpressionsEqual(self, m.c4.expr, m.c4.expr, log(m.p) <= m.x2) - assertExpressionsEqual(self, c4_upper_bound_con.expr, m.x2 <= m.q) + # lower bound is first-stage, upper bound second-stage + self.assertFalse(m.c4.active) + new_c4_lower_bound_con = fs_ineq_cons["ineq_con_c4_lower_bound_con"] + new_c4_upper_bound_con = ss_ineq_cons["ineq_con_c4_upper_bound_con"] + self.assertTrue(new_c4_lower_bound_con.active) + self.assertTrue(new_c4_upper_bound_con.active) + assertExpressionsEqual(self, new_c4_lower_bound_con.expr, log(m.p) <= m.x2) + assertExpressionsEqual(self, new_c4_upper_bound_con.expr, m.x2 <= m.q) # m.q <= m.x2 <= 2 * m.q - # two constraints, one for each bound. deactivate the original + # two second-stage constraints, one for each bound self.assertFalse(m.c5.active) - c5_lower_bound_con = m.find_component("con_c5_lower_bound_con") - c5_upper_bound_con = m.find_component("con_c5_upper_bound_con") - self.assertIn( - c5_lower_bound_con, - working_model.effective_performance_inequality_cons, - ) - self.assertIn( - c5_upper_bound_con, - working_model.effective_performance_inequality_cons, - ) - assertExpressionsEqual(self, c5_lower_bound_con.expr, - m.x2 <= -m.q) - assertExpressionsEqual(self, c5_upper_bound_con.expr, m.x2 <= 2 * m.q) - - # single performance inequality m.z1 - 1.0 <= 0 - self.assertTrue(m.c6.active) - self.assertIn(m.c6, working_model.effective_performance_inequality_cons) - assertExpressionsEqual(self, m.c6.expr, m.z1 <= 1.0) - - # two new performance inequalities: - # 0 - m.z2 <= 0 and m.z2 - 1 <= 0 - # the original should be deactivated + new_c5_lower_bound_con = ss_ineq_cons["ineq_con_c5_lower_bound_con"] + new_c5_upper_bound_con = ss_ineq_cons["ineq_con_c5_upper_bound_con"] + self.assertTrue(new_c5_lower_bound_con.active) + self.assertTrue(new_c5_lower_bound_con.active) + assertExpressionsEqual(self, new_c5_lower_bound_con.expr, - m.x2 <= -m.q) + assertExpressionsEqual(self, new_c5_upper_bound_con.expr, m.x2 <= 2 * m.q) + + # single second-stage inequality + self.assertFalse(m.c6.active) + new_c6_upper_bound_con = ss_ineq_cons["ineq_con_c6_upper_bound_con"] + self.assertTrue(new_c6_upper_bound_con.active) + assertExpressionsEqual(self, new_c6_upper_bound_con.expr, m.z1 <= 1.0) + + # two new second-stage inequalities self.assertFalse(m.c7.active) - c7_lower_bound_con = m.find_component("con_c7_lower_bound_con") - c7_upper_bound_con = m.find_component("con_c7_upper_bound_con") - self.assertIn( - c7_lower_bound_con, working_model.effective_performance_inequality_cons, - ) - self.assertIn( - c7_upper_bound_con, working_model.effective_performance_inequality_cons, - ) - assertExpressionsEqual(self, c7_lower_bound_con.expr, -m.z2 <= 0.0) - assertExpressionsEqual(self, c7_upper_bound_con.expr, m.z2 <= 1.0) + new_c7_lower_bound_con = ss_ineq_cons["ineq_con_c7_lower_bound_con"] + new_c7_upper_bound_con = ss_ineq_cons["ineq_con_c7_upper_bound_con"] + self.assertTrue(new_c7_lower_bound_con.active) + self.assertTrue(new_c7_upper_bound_con.active) + assertExpressionsEqual(self, new_c7_lower_bound_con.expr, -m.z2 <= 0.0) + assertExpressionsEqual(self, new_c7_upper_bound_con.expr, m.z2 <= 1.0) # m.p ** 0.5 <= m.y1 <= m.p - # two performance inequalities; deactivate the original + # two second-stage inequalities self.assertFalse(m.c8.active) - c8_lower_bound_con = m.find_component("con_c8_lower_bound_con") - c8_upper_bound_con = m.find_component("con_c8_upper_bound_con") - self.assertIn( - c8_lower_bound_con, working_model.effective_performance_inequality_cons, - ) - self.assertIn( - c8_upper_bound_con, working_model.effective_performance_inequality_cons, - ) - assertExpressionsEqual(self, c8_lower_bound_con.expr, - m.y1 <= -m.p ** 0.5) - assertExpressionsEqual(self, c8_upper_bound_con.expr, m.y1 <= m.p) + new_c8_lower_bound_con = ss_ineq_cons["ineq_con_c8_lower_bound_con"] + new_c8_upper_bound_con = ss_ineq_cons["ineq_con_c8_upper_bound_con"] + self.assertTrue(new_c8_lower_bound_con.active) + self.assertTrue(new_c8_upper_bound_con.active) + assertExpressionsEqual(self, new_c8_lower_bound_con.expr, - m.y1 <= -m.p ** 0.5) + assertExpressionsEqual(self, new_c8_upper_bound_con.expr, m.y1 <= m.p) # m.y1 - m.q <= 0 - # single performance inequality - self.assertTrue(m.c9.active) - self.assertIn(m.c9, working_model.effective_performance_inequality_cons) - assertExpressionsEqual(self, m.c9.expr, m.y1 - m.q <= 0.0) + # one second-stage inequality + self.assertFalse(m.c9.active) + new_c9_upper_bound_con = ss_ineq_cons["ineq_con_c9_upper_bound_con"] + self.assertTrue(new_c9_upper_bound_con.active) + assertExpressionsEqual(self, new_c9_upper_bound_con.expr, m.y1 - m.q <= 0.0) # m.y1 <= m.q ** 2 - # single performance inequality - self.assertTrue(m.c10.active) - self.assertIn(m.c10, working_model.effective_performance_inequality_cons) - assertExpressionsEqual(self, m.c10.expr, m.y1 <= m.q ** 2) + # single second-stage inequality + self.assertFalse(m.c10.active) + new_c10_upper_bound_con = ( + ss_ineq_cons["ineq_con_c10_upper_bound_con"] + ) + self.assertTrue(new_c10_upper_bound_con.active) + assertExpressionsEqual(self, new_c10_upper_bound_con.expr, m.y1 <= m.q ** 2) # originally deactivated; # no modification self.assertFalse(m.c11.active) assertExpressionsEqual(self, m.c11.expr, m.z2 <= m.q) - # lower bound performance; upper bound first-stage - self.assertTrue(m.c12.active) - c12_lower_bound_con = m.find_component("con_c12_lower_bound_con") - self.assertIn( - c12_lower_bound_con, working_model.effective_performance_inequality_cons + # lower bound second-stage; upper bound first-stage + self.assertFalse(m.c12.active) + new_c12_lower_bound_con = ( + ss_ineq_cons["ineq_con_c12_lower_bound_con"] ) - self.assertIn(m.c12, working_model.effective_first_stage_inequality_cons) - assertExpressionsEqual(self, m.c12.expr, m.x1 <= sin(m.p)) - assertExpressionsEqual(self, c12_lower_bound_con.expr, - m.x1 <= -m.q ** 2) + new_c12_upper_bound_con = fs_ineq_cons["ineq_con_c12_upper_bound_con"] + self.assertTrue(new_c12_lower_bound_con.active) + self.assertTrue(new_c12_upper_bound_con.active) + assertExpressionsEqual(self, new_c12_lower_bound_con.expr, - m.x1 <= -m.q ** 2) + assertExpressionsEqual(self, new_c12_upper_bound_con.expr, m.x1 <= sin(m.p)) def test_standardize_inequality_error(self): """ @@ -1304,26 +1158,29 @@ def build_simple_test_model_data(self): m.eq1 = Constraint(expr=m.x1 + log(m.p) == 1) m.eq2 = Constraint(expr=(1, m.x2, 1)) - # performance equalities + # second-stage equalities m.eq3 = Constraint(expr=m.x2 * m.q == 1) m.eq4 = Constraint(expr=m.x2 - m.z1 ** 2 == 0) m.eq5 = Constraint(expr=m.q == m.y1) m.eq6 = Constraint(expr=(m.q, m.y1, m.q)) m.eq7 = Constraint(expr=m.z2 == 0) + # make eq7 out of scope m.eq7.deactivate() model_data.working_model.uncertain_params = [m.q] - model_data.working_model.effective_first_stage_equality_cons = [] - model_data.working_model.effective_performance_equality_cons = [] + model_data.working_model.first_stage = Block() + model_data.working_model.first_stage.equality_cons = Constraint(Any) + model_data.working_model.second_stage = Block() + model_data.working_model.second_stage.equality_cons = Constraint(Any) model_data.working_model.original_active_equality_cons = [ m.eq1, m.eq2, m.eq3, m.eq4, m.eq5, m.eq6, ] ep = model_data.working_model.effective_var_partitioning = Bunch() - ep.first_stage_variables = [m.x1, m.x2] + ep.second_stage_variables = [m.x1, m.x2] ep.second_stage_variables = [m.z1, m.z2] ep.state_variables = [m.y1] @@ -1339,34 +1196,44 @@ def test_standardize_equality_constraints(self): standardize_equality_constraints(model_data) - self.assertEqual( - ComponentSet(working_model.effective_first_stage_equality_cons), - ComponentSet([m.eq1, m.eq2]), - ) - self.assertEqual( - ComponentSet(working_model.effective_performance_equality_cons), - ComponentSet([m.eq3, m.eq4, m.eq5, m.eq6]), - ) + first_stage_eq_cons = working_model.first_stage.equality_cons + second_stage_eq_cons = working_model.second_stage.equality_cons - # should be first-stage - self.assertTrue(m.eq1.active) - assertExpressionsEqual(self, m.eq1.expr, m.x1 + log(m.p) == 1) + self.assertEqual(len(first_stage_eq_cons), 2) + self.assertEqual(len(second_stage_eq_cons), 4) - self.assertTrue(m.eq2.active) - assertExpressionsEqual(self, m.eq2.expr, RangedExpression((1, m.x2, 1), False)) + self.assertFalse(m.eq1.active) + new_eq1_con = first_stage_eq_cons["eq_con_eq1"] + self.assertTrue(new_eq1_con.active) + assertExpressionsEqual(self, new_eq1_con.expr, m.x1 + log(m.p) == 1) + + self.assertFalse(m.eq2.active) + new_eq2_con = first_stage_eq_cons["eq_con_eq2"] + self.assertTrue(new_eq2_con.active) + assertExpressionsEqual( + self, new_eq2_con.expr, RangedExpression((1, m.x2, 1), False) + ) - self.assertTrue(m.eq3.active) - assertExpressionsEqual(self, m.eq3.expr, m.x2 * m.q == 1) + self.assertFalse(m.eq3.active) + new_eq3_con = second_stage_eq_cons["eq_con_eq3"] + self.assertTrue(new_eq3_con.active) + assertExpressionsEqual(self, new_eq3_con.expr, m.x2 * m.q == 1) - self.assertTrue(m.eq4.active) - assertExpressionsEqual(self, m.eq4.expr, m.x2 - m.z1 ** 2 == 0) + self.assertFalse(m.eq4.active) + new_eq4_con = second_stage_eq_cons["eq_con_eq4"] + self.assertTrue(new_eq4_con) + assertExpressionsEqual(self, new_eq4_con.expr, m.x2 - m.z1 ** 2 == 0) - self.assertTrue(m.eq5.active) - assertExpressionsEqual(self, m.eq5.expr, m.q == m.y1) + self.assertFalse(m.eq5.active) + new_eq5_con = second_stage_eq_cons["eq_con_eq5"] + self.assertTrue(new_eq5_con) + assertExpressionsEqual(self, new_eq5_con.expr, m.q == m.y1) - self.assertTrue(m.eq6.active) + self.assertFalse(m.eq6.active) + new_eq6_con = second_stage_eq_cons["eq_con_eq6"] + self.assertTrue(new_eq6_con.active) assertExpressionsEqual( - self, m.eq6.expr, RangedExpression((m.q, m.y1, m.q), False), + self, new_eq6_con.expr, RangedExpression((m.q, m.y1, m.q), False), ) # excluded from the list of active constraints; @@ -1417,8 +1284,10 @@ def build_simple_test_model_data(self): ep.second_stage_variables = [] ep.state_variables = [m.y] - model_data.working_model.effective_first_stage_inequality_cons = [] - model_data.working_model.effective_performance_inequality_cons = [] + model_data.working_model.first_stage = Block() + model_data.working_model.first_stage.inequality_cons = Constraint(Any) + model_data.working_model.second_stage = Block() + model_data.working_model.second_stage.inequality_cons = Constraint(Any) return model_data @@ -1497,26 +1366,10 @@ def test_standardize_active_obj_worst_case_focus(self): f"{standardize_active_objective}." ), ) - self.assertNotIn( - working_model.epigraph_con, - working_model.effective_first_stage_inequality_cons, - msg=( - f"Epigraph constraint {working_model.epigraph_con.name!r} " - "should not be in the list of effective first-stage inequalities." - ), - ) - self.assertIn( - working_model.epigraph_con, - working_model.effective_performance_inequality_cons, - msg=( - f"Epigraph constraint {working_model.epigraph_con.name!r} " - "should be in the list of effective performance inequalities." - ), - ) assertExpressionsEqual( self, - working_model.epigraph_con.expr, - m.obj1.expr - working_model.epigraph_var <= 0, + working_model.second_stage.inequality_cons["epigraph_con"].expr, + m.obj1.expr - working_model.first_stage.epigraph_var <= 0, ) def test_standardize_active_obj_nominal_focus(self): @@ -1541,26 +1394,10 @@ def test_standardize_active_obj_nominal_focus(self): f"{standardize_active_objective}." ), ) - self.assertIn( - working_model.epigraph_con, - working_model.effective_first_stage_inequality_cons, - msg=( - f"Epigraph constraint {working_model.epigraph_con.name!r} " - "should be in the list of effective first-stage inequalities." - ), - ) - self.assertNotIn( - working_model.epigraph_con, - working_model.effective_performance_inequality_cons, - msg=( - f"Epigraph constraint {working_model.epigraph_con.name!r} " - "should not be in the list of effective performance inequalities." - ), - ) assertExpressionsEqual( self, - working_model.epigraph_con.expr, - m.obj1.expr - working_model.epigraph_var <= 0, + working_model.first_stage.inequality_cons["epigraph_con"].expr, + m.obj1.expr - working_model.first_stage.epigraph_var <= 0, ) def test_standardize_active_obj_unsupported_focus(self): @@ -1609,27 +1446,11 @@ def test_standardize_active_obj_nonadjustable_max(self): f"{standardize_active_objective}." ), ) - self.assertIn( - working_model.epigraph_con, - working_model.effective_first_stage_inequality_cons, - msg=( - f"Epigraph constraint {working_model.epigraph_con.name!r} " - "should be in the list of effective first-stage inequalities." - ), - ) - self.assertNotIn( - working_model.epigraph_con, - working_model.effective_performance_inequality_cons, - msg=( - f"Epigraph constraint {working_model.epigraph_con.name!r} " - "should not be in the list of effective performance inequalities." - ), - ) assertExpressionsEqual( self, - working_model.epigraph_con.expr, - -m.obj2.expr - working_model.epigraph_var <= 0, + working_model.first_stage.inequality_cons["epigraph_con"].expr, + -m.obj2.expr - working_model.first_stage.epigraph_var <= 0, ) @@ -1675,6 +1496,8 @@ def build_simple_test_model_data(self): ep.second_stage_variables = [m.z2] ep.state_variables = [m.y] + model_data.working_model.first_stage = Block() + return model_data def test_correct_num_dr_vars_static(self): @@ -1689,7 +1512,7 @@ def test_correct_num_dr_vars_static(self): add_decision_rule_variables(model_data=model_data, config=config) - for indexed_dr_var in model_data.working_model.decision_rule_vars: + for indexed_dr_var in model_data.working_model.first_stage.decision_rule_vars: self.assertEqual( len(indexed_dr_var), 1, @@ -1704,7 +1527,7 @@ def test_correct_num_dr_vars_static(self): model_data.working_model.effective_var_partitioning.second_stage_variables ) self.assertEqual( - len(ComponentSet(model_data.working_model.decision_rule_vars)), + len(ComponentSet(model_data.working_model.first_stage.decision_rule_vars)), len(effective_second_stage_vars), msg=( "Number of unique indexed DR variable components should equal " @@ -1715,7 +1538,7 @@ def test_correct_num_dr_vars_static(self): # check mapping is as expected ess_dr_var_zip = zip( effective_second_stage_vars, - model_data.working_model.decision_rule_vars, + model_data.working_model.first_stage.decision_rule_vars, ) for ess_var, indexed_dr_var in ess_dr_var_zip: mapped_dr_var = model_data.working_model.eff_ss_var_to_dr_var_map[ess_var] @@ -1741,7 +1564,7 @@ def test_correct_num_dr_vars_affine(self): add_decision_rule_variables(model_data=model_data, config=config) - for indexed_dr_var in model_data.working_model.decision_rule_vars: + for indexed_dr_var in model_data.working_model.first_stage.decision_rule_vars: self.assertEqual( len(indexed_dr_var), 1 + len(model_data.working_model.uncertain_params), @@ -1756,7 +1579,7 @@ def test_correct_num_dr_vars_affine(self): model_data.working_model.effective_var_partitioning.second_stage_variables ) self.assertEqual( - len(ComponentSet(model_data.working_model.decision_rule_vars)), + len(ComponentSet(model_data.working_model.first_stage.decision_rule_vars)), len(effective_second_stage_vars), msg=( "Number of unique indexed DR variable components should equal " @@ -1767,7 +1590,7 @@ def test_correct_num_dr_vars_affine(self): # check mapping is as expected ess_dr_var_zip = zip( effective_second_stage_vars, - model_data.working_model.decision_rule_vars, + model_data.working_model.first_stage.decision_rule_vars, ) for ess_var, indexed_dr_var in ess_dr_var_zip: mapped_dr_var = model_data.working_model.eff_ss_var_to_dr_var_map[ess_var] @@ -1795,7 +1618,7 @@ def test_correct_num_dr_vars_quadratic(self): num_params = len(model_data.working_model.uncertain_params) - for indexed_dr_var in model_data.working_model.decision_rule_vars: + for indexed_dr_var in model_data.working_model.first_stage.decision_rule_vars: self.assertEqual( len(indexed_dr_var), 1 # static term @@ -1813,7 +1636,7 @@ def test_correct_num_dr_vars_quadratic(self): model_data.working_model.effective_var_partitioning.second_stage_variables ) self.assertEqual( - len(ComponentSet(model_data.working_model.decision_rule_vars)), + len(ComponentSet(model_data.working_model.first_stage.decision_rule_vars)), len(effective_second_stage_vars), msg=( "Number of unique indexed DR variable components should equal " @@ -1824,7 +1647,7 @@ def test_correct_num_dr_vars_quadratic(self): # check mapping is as expected ess_dr_var_zip = zip( effective_second_stage_vars, - model_data.working_model.decision_rule_vars, + model_data.working_model.first_stage.decision_rule_vars, ) for ess_var, indexed_dr_var in ess_dr_var_zip: mapped_dr_var = model_data.working_model.eff_ss_var_to_dr_var_map[ess_var] @@ -1879,6 +1702,9 @@ def build_simple_test_model_data(self): ep.second_stage_variables = [m.z2] ep.state_variables = [m.y] + model_data.working_model.first_stage = Block() + model_data.working_model.second_stage = Block() + return model_data def test_num_dr_eqns_added_correct(self): @@ -1900,7 +1726,7 @@ def test_num_dr_eqns_added_correct(self): model_data.working_model.effective_var_partitioning.second_stage_variables ) self.assertEqual( - len(model_data.working_model.decision_rule_eqns), + len(model_data.working_model.second_stage.decision_rule_eqns), len(effective_second_stage_vars), msg=( "Number of decision rule equations should match number of " @@ -1911,19 +1737,20 @@ def test_num_dr_eqns_added_correct(self): # check second-stage var to DR equation mapping is as expected ess_dr_var_zip = zip( effective_second_stage_vars, - model_data.working_model.decision_rule_eqns, + model_data.working_model.second_stage.decision_rule_eqns.values(), ) - for ess_var, indexed_dr_eqn in ess_dr_var_zip: + for ess_var, dr_eqn in ess_dr_var_zip: mapped_dr_eqn = model_data.working_model.eff_ss_var_to_dr_eqn_map[ess_var] self.assertIs( mapped_dr_eqn, - indexed_dr_eqn, + dr_eqn, msg=( f"Second-stage var {ess_var.name!r} " f"is mapped to DR equation {mapped_dr_eqn.name!r}, " - f"but expected mapping to DR equation {indexed_dr_eqn.name!r}." + f"but expected mapping to DR equation {dr_eqn.name!r}." ) ) + self.assertTrue(mapped_dr_eqn.active) def test_dr_eqns_form_correct(self): """ @@ -1952,8 +1779,8 @@ def test_dr_eqns_form_correct(self): dr_zip = zip( model_data.working_model.effective_var_partitioning.second_stage_variables, - model_data.working_model.decision_rule_vars, - model_data.working_model.decision_rule_eqns, + model_data.working_model.first_stage.decision_rule_vars, + model_data.working_model.second_stage.decision_rule_eqns.values(), ) for ss_var, indexed_dr_var, dr_eq in dr_zip: expected_dr_eq_expression = ( @@ -1994,7 +1821,7 @@ def test_dr_eqns_form_correct(self): class TestReformulateStateVarIndependentEqCons(unittest.TestCase): """ Unit tests for routine that reformulates - state variable-independent performance equality constraints. + state variable-independent second-stage equality constraints. """ def setup_test_model_data(self): """ @@ -2017,16 +1844,28 @@ def setup_test_model_data(self): == - m.u * (m.x1 + 2) ) - # redundant, but makes the tests more rigorous + # mathematically redundant, but makes the tests more rigorous # as we want to check that loops in the coefficient # matching routine are exited appropriately m.eq_con_2 = Constraint(expr=m.u * (m.x2 - 1) == 0) working_model.uncertain_params = [m.u] - working_model.effective_first_stage_equality_cons = [] - working_model.effective_performance_equality_cons = [m.eq_con, m.eq_con_2] - working_model.effective_performance_inequality_cons = [m.con] + working_model.first_stage = Block() + working_model.first_stage.equality_cons = Constraint(Any) + working_model.second_stage = Block() + working_model.second_stage.equality_cons = Constraint(Any) + working_model.second_stage.inequality_cons = Constraint(Any) + + working_model.second_stage.equality_cons["eq_con"] = m.eq_con.expr + working_model.second_stage.equality_cons["eq_con_2"] = m.eq_con_2.expr + working_model.second_stage.inequality_cons["con"] = m.con.expr + + # deactivate constraints on user model, as these are not + # what the reformulation routine actually processes + m.eq_con.deactivate() + m.eq_con_2.deactivate() + m.con.deactivate() working_model.all_variables = [m.x1, m.x2] ep = working_model.effective_var_partitioning = Bunch() @@ -2053,8 +1892,8 @@ def test_coefficient_matching_correct_constraints_added(self): config.decision_rule_order = 1 config.progress_logger = logger - model_data.working_model.decision_rule_vars = [] - model_data.working_model.decision_rule_eqns = [] + model_data.working_model.first_stage.decision_rule_vars = [] + model_data.working_model.second_stage.decision_rule_eqns = [] model_data.working_model.all_nonadjustable_variables = list( ep.first_stage_variables ) @@ -2070,38 +1909,33 @@ def test_coefficient_matching_correct_constraints_added(self): "a robust infeasible constraint" ), ) + + first_stage_eq_cons = model_data.working_model.first_stage.equality_cons self.assertEqual( - len(model_data.working_model.coefficient_matching_conlist), + len(first_stage_eq_cons), 3, msg="Number of coefficient matching constraints not as expected." ) + self.assertEqual(len(model_data.working_model.second_stage.equality_cons), 0) + # we originally declared an inequality constraint on the model + self.assertEqual(len(model_data.working_model.second_stage.inequality_cons), 1) assertExpressionsEqual( self, - model_data.working_model.coefficient_matching_conlist[1].expr, + first_stage_eq_cons["coeff_matching_eq_con_coeff_1"].expr, 2.5 + m.x1 + (-5) * (m.x1 * m.x2) + m.x1 ** 3 == 0, ) assertExpressionsEqual( self, - model_data.working_model.coefficient_matching_conlist[2].expr, + first_stage_eq_cons["coeff_matching_eq_con_coeff_2"].expr, (-1) + m.x2 == 0, ) assertExpressionsEqual( self, - model_data.working_model.coefficient_matching_conlist[3].expr, + first_stage_eq_cons["coeff_matching_eq_con_2_coeff_1"].expr, (-1) + m.x2 == 0, ) - # check constraint partitioning updated as expected - self.assertEqual( - model_data.working_model.effective_performance_equality_cons, - [], - ) - self.assertEqual( - model_data.working_model.effective_first_stage_equality_cons, - list(model_data.working_model.coefficient_matching_conlist.values()), - ) - def test_reformulate_nonlinear_state_var_independent_eq_con(self): """ Test routine appropriately performs coefficient matching @@ -2123,15 +1957,15 @@ def test_reformulate_nonlinear_state_var_independent_eq_con(self): ep = model_data.working_model.effective_var_partitioning model_data.working_model.all_nonadjustable_variables = list( ep.first_stage_variables - + list(model_data.working_model.decision_rule_var_0.values()) + + list(model_data.working_model.first_stage.decision_rule_var_0.values()) ) wm = model_data.working_model m = model_data.working_model.user_model - # we want only one of the constraints to trigger the error + # we want only one of the constraints to be 'nonlinear' # change eq_con_2 to give a valid matching constraint - m.eq_con_2.set_value(m.u * (m.x1 - 1) == 0) + wm.second_stage.equality_cons["eq_con_2"].set_value(m.u * (m.x1 - 1) == 0) with LoggingIntercept(level=logging.DEBUG) as LOG: robust_infeasible = reformulate_state_var_independent_eq_cons( @@ -2142,7 +1976,7 @@ def test_reformulate_nonlinear_state_var_independent_eq_con(self): self.assertRegex( text=err_msg, expected_regex=( - r".*Equality constraint 'user_model\.eq_con'.*cannot be written.*" + r".*Equality constraint '.*eq_con.*'.*cannot be written.*" ), ) @@ -2155,27 +1989,22 @@ def test_reformulate_nonlinear_state_var_independent_eq_con(self): ) # check constraint partitioning updated as expected - self.assertEqual( - wm.effective_performance_equality_cons, - [], - ) - self.assertEqual( - wm.effective_performance_inequality_cons, - [ - m.con, - m.con_eq_con_lower_bound_con, - m.con_eq_con_upper_bound_con, - ], - ) - self.assertEqual( - wm.effective_first_stage_equality_cons, - [wm.coefficient_matching_conlist[1]], + self.assertFalse(wm.second_stage.equality_cons) + self.assertEqual(len(wm.second_stage.inequality_cons), 3) + self.assertEqual(len(wm.first_stage.equality_cons), 1) + + second_stage_ineq_cons = wm.second_stage.inequality_cons + self.assertTrue(second_stage_ineq_cons["reform_lower_bound_from_eq_con"].active) + self.assertTrue(second_stage_ineq_cons["reform_upper_bound_from_eq_con"].active) + self.assertTrue( + wm.first_stage.equality_cons["coeff_matching_eq_con_2_coeff_1"].active ) - # verify expressions + # expressions for the new opposing inequalities + # and coefficient matching constraint assertExpressionsEqual( self, - m.con_eq_con_lower_bound_con.expr, + second_stage_ineq_cons["reform_lower_bound_from_eq_con"].expr, -( m.u**2 * (m.x2 - 1) + m.u * (m.x1**3 + 0.5) @@ -2185,7 +2014,7 @@ def test_reformulate_nonlinear_state_var_independent_eq_con(self): ) assertExpressionsEqual( self, - m.con_eq_con_upper_bound_con.expr, + second_stage_ineq_cons["reform_upper_bound_from_eq_con"].expr, ( m.u**2 * (m.x2 - 1) + m.u * (m.x1**3 + 0.5) @@ -2196,16 +2025,10 @@ def test_reformulate_nonlinear_state_var_independent_eq_con(self): ) assertExpressionsEqual( self, - wm.coefficient_matching_conlist[1].expr, + wm.first_stage.equality_cons["coeff_matching_eq_con_2_coeff_1"].expr, (-1) + m.x1 == 0, ) - # ensure the reformulated equality constraint was deactivated, - # and the added inequalities were activated - self.assertFalse(m.eq_con.active) - self.assertTrue(m.con_eq_con_upper_bound_con.active) - self.assertTrue(m.con_eq_con_lower_bound_con.active) - def test_coefficient_matching_robust_infeasible_proof(self): """ Test coefficient matching detects robust infeasibility @@ -2214,7 +2037,7 @@ def test_coefficient_matching_robust_infeasible_proof(self): # Write the deterministic Pyomo model model_data = self.setup_test_model_data() m = model_data.working_model.user_model - m.eq_con.set_value( + model_data.working_model.second_stage.equality_cons["eq_con"].set_value( expr=m.u * (m.x1**3 + 0.5) - 5 * m.u * m.x1 * m.x2 + m.u * (m.x1 + 2) @@ -2247,7 +2070,7 @@ def test_coefficient_matching_robust_infeasible_proof(self): text=robust_infeasible_msg, expected_regex=( r"PyROS has determined that the model is robust infeasible\. " - r"One reason for this.*equality constraint 'user_model\.eq_con'.*" + r"One reason for this.*equality constraint '.*eq_con.*'.*" ) ) @@ -2263,7 +2086,7 @@ def build_test_model_data(self): model_data = Bunch() model_data.original_model = m = ConcreteModel() - # PARAMS: one uncertain, one certain + # PARAMS: p uncertain, q certain m.p = Param(initialize=2, mutable=True) m.q = Param(initialize=4.5, mutable=True) @@ -2301,20 +2124,23 @@ def build_test_model_data(self): m.eq2 = Constraint(expr=m.x1 - m.z1 == 0) # pretriangular: makes z2 nonadjustable, so first-stage m.eq3 = Constraint(expr=m.x1 ** 2 + m.x2 + m.p * m.z2 == m.p) - # performance equality + # second-stage equality m.eq4 = Constraint(expr=m.z3 + m.y1 == m.q) # INEQUALITY CONSTRAINTS - # since x1, z1 nonadjustable, LB is first-stage. but UB is performance + # since x1, z1 nonadjustable, LB is first-stage, + # but UB second-stage due to uncertain param q m.ineq1 = Constraint(expr=(-m.p, m.x1 + m.z1, exp(m.q))) # two first-stage inequalities m.ineq2 = Constraint(expr=(0, m.x1 + m.x2, 10)) # though the bounds are structurally equal, they are not - # identical objects, so this constitutes two performance inequalities - # note: these inequalities redundant, as collectively these constraints + # identical objects, so this constitutes + # two second-stage inequalities + # note: these inequalities redundant, + # as collectively these constraints # are mathematically identical to eq4 m.ineq3 = Constraint(expr=(2 * m.q, 2 * (m.z3 + m.y1), 2 * m.q)) - # performance inequality. trivially satisfied/infeasible, + # second-stage inequality. trivially satisfied/infeasible, # since y2 is fixed m.ineq4 = Constraint(expr=-m.q <= m.y2 ** 2 + log(m.y2)) @@ -2386,7 +2212,7 @@ def test_preprocessor_effective_var_partitioning_static_dr(self): ublk.x1, ublk.x2, ublk.z1, ublk.z2, ublk.z3, ublk.z4, ublk.z5, ublk.y2, ] - + [working_model.epigraph_var] + + [working_model.first_stage.epigraph_var] ), ) self.assertEqual( @@ -2403,7 +2229,7 @@ def test_preprocessor_effective_var_partitioning_static_dr(self): ublk.y1, ublk.y2, ] - + [working_model.epigraph_var] + + [working_model.first_stage.epigraph_var] ), ) @@ -2446,9 +2272,9 @@ def test_preprocessor_effective_var_partitioning_nonstatic_dr(self, name, dr_ord ComponentSet(working_model.all_nonadjustable_variables), ComponentSet( [ublk.x1, ublk.x2, ublk.z1, ublk.z2, ublk.z4, ublk.y2] - + [working_model.epigraph_var] - + list(working_model.decision_rule_var_0.values()) - + list(working_model.decision_rule_var_1.values()) + + [working_model.first_stage.epigraph_var] + + list(working_model.first_stage.decision_rule_var_0.values()) + + list(working_model.first_stage.decision_rule_var_1.values()) ), ) self.assertEqual( @@ -2465,9 +2291,9 @@ def test_preprocessor_effective_var_partitioning_nonstatic_dr(self, name, dr_ord ublk.y1, ublk.y2, ] - + [working_model.epigraph_var] - + list(working_model.decision_rule_var_0.values()) - + list(working_model.decision_rule_var_1.values()) + + [working_model.first_stage.epigraph_var] + + list(working_model.first_stage.decision_rule_var_0.values()) + + list(working_model.first_stage.decision_rule_var_1.values()) ), ) @@ -2500,152 +2326,171 @@ def test_preprocessor_constraint_partitioning_nonstatic_dr( working_model = model_data.working_model ublk = working_model.user_model + + # list of expected coefficient matching constraint names + # equality bound constraint for z5 and/or eq1 are subject + # to reformulation + if dr_order == 1: + coeff_matching_con_names = [ + "coeff_matching_var_z5_uncertain_eq_bound_con_coeff_0", + "coeff_matching_var_z5_uncertain_eq_bound_con_coeff_1", + 'coeff_matching_eq_con_eq1_coeff_1', + 'coeff_matching_eq_con_eq1_coeff_2', + ] + else: + coeff_matching_con_names = [ + "coeff_matching_var_z5_uncertain_eq_bound_con_coeff_0", + "coeff_matching_var_z5_uncertain_eq_bound_con_coeff_1", + "coeff_matching_var_z5_uncertain_eq_bound_con_coeff_2", + ] + self.assertEqual( - ComponentSet(working_model.effective_first_stage_inequality_cons), - ComponentSet( - [ublk.ineq1, ublk.ineq2] - + ([working_model.epigraph_con] if obj_focus == "nominal" else []) - ), + list(working_model.first_stage.inequality_cons), + ( + ["ineq_con_ineq1_lower_bound_con", "ineq_con_ineq2"] + + (["epigraph_con"] if obj_focus == "nominal" else []) + ) ) self.assertEqual( - ComponentSet(working_model.effective_first_stage_equality_cons), - ComponentSet( - [ - ublk.eq2, - ublk.eq3, - *working_model.coefficient_matching_conlist.values(), - ] - ), + list(working_model.first_stage.equality_cons), + ["eq_con_eq2", "eq_con_eq3"] + coeff_matching_con_names, ) self.assertEqual( - ComponentSet(working_model.effective_performance_inequality_cons), - ComponentSet( + list(working_model.second_stage.inequality_cons), + ( [ - ublk.find_component("var_x1_uncertain_upper_bound_con"), - ublk.find_component("var_z1_uncertain_upper_bound_con"), - ublk.find_component("var_z2_uncertain_lower_bound_con"), - ublk.find_component("var_z3_certain_upper_bound_con"), - ublk.find_component("var_z3_uncertain_lower_bound_con"), - ublk.find_component("var_z5_certain_lower_bound_con"), - ublk.find_component("var_y1_certain_lower_bound_con"), - ublk.find_component("con_ineq1_upper_bound_con"), - ublk.find_component("con_ineq3_lower_bound_con"), - ublk.find_component("con_ineq3_upper_bound_con"), - ublk.ineq4, + "var_x1_uncertain_upper_bound_con", + "var_z1_uncertain_upper_bound_con", + "var_z2_uncertain_lower_bound_con", + "var_z3_certain_upper_bound_con", + "var_z3_uncertain_lower_bound_con", + "var_z5_certain_lower_bound_con", + "var_y1_certain_lower_bound_con", + "ineq_con_ineq1_upper_bound_con", + "ineq_con_ineq3_lower_bound_con", + "ineq_con_ineq3_upper_bound_con", + "ineq_con_ineq4_lower_bound_con", ] - # eq1 gets reformulated to two inequality constraints - # since it is state variable independent and - # too nonlinear for coefficient matching - + ([ublk.con_eq1_lower_bound_con, ublk.con_eq1_upper_bound_con] if dr_order == 2 else []) - + ([working_model.epigraph_con] if obj_focus == "worst_case" else []) + + (["epigraph_con"] if obj_focus == "worst_case" else []) + + ( + # for quadratic DR, + # eq1 gets reformulated to two inequality constraints + # since it is state variable independent and + # too nonlinear for coefficient matching + [ + "reform_lower_bound_from_eq_con_eq1", + "reform_upper_bound_from_eq_con_eq1", + ] + if dr_order == 2 else [] + ) ), ) self.assertEqual( - ComponentSet(working_model.effective_performance_equality_cons), + list(working_model.second_stage.equality_cons), # eq1 doesn't get reformulated in coefficient matching # when DR order is 2 as the polynomial degree is too high - ComponentSet([ublk.eq4]), + ["eq_con_eq4"], ) # verify the constraints are active - for fs_eq_con in working_model.effective_first_stage_equality_cons: + for fs_eq_con in working_model.first_stage.equality_cons.values(): self.assertTrue(fs_eq_con.active, msg=f"{fs_eq_con.name} inactive") - for fs_ineq_con in working_model.effective_first_stage_inequality_cons: + for fs_ineq_con in working_model.first_stage.inequality_cons.values(): self.assertTrue(fs_ineq_con.active, msg=f"{fs_ineq_con.name} inactive") - for perf_eq_con in working_model.effective_performance_equality_cons: + for perf_eq_con in working_model.second_stage.equality_cons.values(): self.assertTrue(perf_eq_con.active, msg=f"{perf_eq_con.name} inactive") - for perf_ineq_con in working_model.effective_performance_inequality_cons: + for perf_ineq_con in working_model.second_stage.inequality_cons.values(): self.assertTrue(perf_ineq_con.active, msg=f"{perf_ineq_con.name} inactive") # verify the constraint expressions m = ublk + fs = working_model.first_stage + ss = working_model.second_stage assertExpressionsEqual(self, m.x1.lower, 0) assertExpressionsEqual( self, - m.var_x1_uncertain_upper_bound_con.expr, m.x1 <= m.q, + ss.inequality_cons["var_x1_uncertain_upper_bound_con"].expr, + m.x1 <= m.q, ) assertExpressionsEqual( self, - m.var_z1_uncertain_upper_bound_con.expr, + ss.inequality_cons["var_z1_uncertain_upper_bound_con"].expr, m.z1 <= m.q, ) assertExpressionsEqual( self, - m.var_z2_uncertain_lower_bound_con.expr, + ss.inequality_cons["var_z2_uncertain_lower_bound_con"].expr, -m.z2 <= -(-2 * m.q ** 2), ) assertExpressionsEqual( self, - m.var_z3_uncertain_lower_bound_con.expr, + ss.inequality_cons["var_z3_uncertain_lower_bound_con"].expr, -m.z3 <= -(-m.q), ) assertExpressionsEqual( self, - m.var_z3_certain_upper_bound_con.expr, + ss.inequality_cons["var_z3_certain_upper_bound_con"].expr, m.z3 <= 0, ) assertExpressionsEqual( self, - m.var_z5_certain_lower_bound_con.expr, + ss.inequality_cons["var_z5_certain_lower_bound_con"].expr, -m.z5 <= 0, ) assertExpressionsEqual( self, - m.var_y1_certain_lower_bound_con.expr, + ss.inequality_cons["var_y1_certain_lower_bound_con"].expr, -m.y1 <= 0, ) assertExpressionsEqual( self, - m.ineq1.expr, + fs.inequality_cons["ineq_con_ineq1_lower_bound_con"].expr, -m.p <= m.x1 + m.z1, ) assertExpressionsEqual( self, - m.con_ineq1_upper_bound_con.expr, + ss.inequality_cons["ineq_con_ineq1_upper_bound_con"].expr, m.x1 + m.z1 <= exp(m.q), ) assertExpressionsEqual( self, - m.ineq2.expr, + fs.inequality_cons["ineq_con_ineq2"].expr, RangedExpression((0, m.x1 + m.x2, 10), False), ) assertExpressionsEqual( self, - m.con_ineq3_lower_bound_con.expr, + ss.inequality_cons["ineq_con_ineq3_lower_bound_con"].expr, -(2 * (m.z3 + m.y1)) <= -(2 * m.q), ) assertExpressionsEqual( self, - m.con_ineq3_upper_bound_con.expr, + ss.inequality_cons["ineq_con_ineq3_upper_bound_con"].expr, 2 * (m.z3 + m.y1) <= 2 * m.q, ) assertExpressionsEqual( self, - m.ineq3.upper, - None, - ) - self.assertFalse(m.ineq3.active) - assertExpressionsEqual( - self, - m.ineq4.expr, + ss.inequality_cons["ineq_con_ineq4_lower_bound_con"].expr, -(m.y2 ** 2 + log(m.y2)) <= -(-m.q), ) self.assertFalse(m.ineq5.active) assertExpressionsEqual( self, - m.eq2.expr, + fs.equality_cons["eq_con_eq2"].expr, m.x1 - m.z1 == 0, ) assertExpressionsEqual( self, - m.eq3.expr, + fs.equality_cons["eq_con_eq3"].expr, m.x1 ** 2 + m.x2 + m.p * m.z2 == m.p, ) if dr_order < 2: - # due to coefficient matching - self.assertFalse(m.eq1.active) + # due to coefficient matching, this should have been deleted + self.assertNotIn("eq_con_eq1", ss.equality_cons) + + # user model block should have no active constraints + self.assertFalse(list(m.component_data_objects(Constraint, active=True))) @parameterized.expand([ ["static", 0, True], @@ -2667,7 +2512,7 @@ def test_preprocessor_coefficient_matching( progress_logger=logger, ) - # static DR, problem should be robust infeasible + # for static DR, problem should be robust infeasible # due to the coefficient matching constraints derived # from bounds on z5 robust_infeasible = preprocess_model_data( @@ -2679,61 +2524,59 @@ def test_preprocessor_coefficient_matching( # check the coefficient matching constraint expressions working_model = model_data.working_model m = model_data.working_model.user_model - working_model.coefficient_matching_conlist.pprint() + fs = working_model.first_stage + fs_eqs = working_model.first_stage.equality_cons + ss_ineqs = working_model.second_stage.inequality_cons if config.decision_rule_order == 1: # check the constraint expressions of eq1 and z5 bound - self.assertFalse(m.eq1.active) assertExpressionsEqual( self, - working_model.coefficient_matching_conlist[1].expr, - working_model.decision_rule_vars[1][0] == 0, + fs_eqs["coeff_matching_var_z5_uncertain_eq_bound_con_coeff_0"].expr, + fs.decision_rule_vars[1][0] == 0, ) assertExpressionsEqual( self, - working_model.coefficient_matching_conlist[2].expr, - -1 + working_model.decision_rule_vars[1][1] == 0, + fs_eqs["coeff_matching_var_z5_uncertain_eq_bound_con_coeff_1"].expr, + -1 + fs.decision_rule_vars[1][1] == 0, ) assertExpressionsEqual( self, - working_model.coefficient_matching_conlist[3].expr, - working_model.decision_rule_vars[0][0] + m.x2 == 0, + fs_eqs["coeff_matching_eq_con_eq1_coeff_1"].expr, + fs.decision_rule_vars[0][0] + m.x2 == 0, ) assertExpressionsEqual( self, - working_model.coefficient_matching_conlist[4].expr, - working_model.decision_rule_vars[0][1] == 0, + fs_eqs["coeff_matching_eq_con_eq1_coeff_2"].expr, + fs.decision_rule_vars[0][1] == 0, ) if config.decision_rule_order == 2: # eq1 should be deactivated and refomulated to 2 inequalities - self.assertFalse(m.eq1.active) - self.assertTrue(m.con_eq1_lower_bound_con.active) - self.assertTrue(m.con_eq1_upper_bound_con.active) assertExpressionsEqual( self, - m.con_eq1_lower_bound_con.expr, + ss_ineqs["reform_lower_bound_from_eq_con_eq1"].expr, -(m.q * (m.z3 + m.x2)) <= 0.0, ) assertExpressionsEqual( self, - m.con_eq1_upper_bound_con.expr, + ss_ineqs["reform_upper_bound_from_eq_con_eq1"].expr, m.q * (m.z3 + m.x2) <= 0.0, ) # check coefficient matching constraint expressions assertExpressionsEqual( self, - working_model.coefficient_matching_conlist[1].expr, - working_model.decision_rule_vars[1][0] == 0, + fs_eqs["coeff_matching_var_z5_uncertain_eq_bound_con_coeff_0"].expr, + fs.decision_rule_vars[1][0] == 0, ) assertExpressionsEqual( self, - working_model.coefficient_matching_conlist[2].expr, - -1 + working_model.decision_rule_vars[1][1] == 0, + fs_eqs["coeff_matching_var_z5_uncertain_eq_bound_con_coeff_1"].expr, + -1 + fs.decision_rule_vars[1][1] == 0, ) assertExpressionsEqual( self, - working_model.coefficient_matching_conlist[3].expr, - working_model.decision_rule_vars[1][2] == 0, + fs_eqs["coeff_matching_var_z5_uncertain_eq_bound_con_coeff_2"].expr, + fs.decision_rule_vars[1][2] == 0, ) @parameterized.expand([ @@ -2760,10 +2603,11 @@ def test_preprocessor_objective_standardization(self, name, dr_order): ublk = model_data.working_model.user_model working_model = model_data.working_model + assertExpressionsEqual( self, - working_model.epigraph_con.expr, - ublk.obj.expr - working_model.epigraph_var <= 0 + working_model.second_stage.inequality_cons["epigraph_con"].expr, + ublk.obj.expr - working_model.first_stage.epigraph_var <= 0 ) assertExpressionsEqual( self, @@ -2773,8 +2617,9 @@ def test_preprocessor_objective_standardization(self, name, dr_order): # recall: objective summands are classified according # to dependence on uncertain parameters and variables - # the *user* considers adjustable + # the *user* considers adjustable, # so the summands should be independent of the DR order + # (which itself affects the effective var partitioning) assertExpressionsEqual( self, working_model.first_stage_objective.expr, @@ -2825,11 +2670,11 @@ def test_preprocessor_log_model_statistics_affine_dr(self, obj_focus): Equality constraints : 9 Coefficient matching constraints : 4 Other first-stage equations : 2 - Performance equations : 1 + Second-stage equations : 1 Decision rule equations : 2 Inequality constraints : 14 First-stage inequalities : {3 if obj_focus == 'nominal' else 2} - Performance inequalities : {11 if obj_focus == 'nominal' else 12} + Second-stage inequalities : {11 if obj_focus == 'nominal' else 12} """ ) @@ -2877,11 +2722,11 @@ def test_preprocessor_log_model_statistics_quadratic_dr(self, obj_focus): Equality constraints : 8 Coefficient matching constraints : 3 Other first-stage equations : 2 - Performance equations : 1 + Second-stage equations : 1 Decision rule equations : 2 Inequality constraints : 16 First-stage inequalities : {3 if obj_focus == 'nominal' else 2} - Performance inequalities : {13 if obj_focus == 'nominal' else 14} + Second-stage inequalities : {13 if obj_focus == 'nominal' else 14} """ ) diff --git a/pyomo/contrib/pyros/tests/test_separation.py b/pyomo/contrib/pyros/tests/test_separation.py index d61285bfcc1..f5a3d053945 100644 --- a/pyomo/contrib/pyros/tests/test_separation.py +++ b/pyomo/contrib/pyros/tests/test_separation.py @@ -99,33 +99,28 @@ def test_construct_separation_problem_nonadj_components(self): # check nonadjustable components fixed/deactivated self.assertTrue(separation_model.user_model.x1.fixed) - self.assertTrue(separation_model.epigraph_var.fixed) - for indexed_var in separation_model.decision_rule_vars: + self.assertTrue(separation_model.first_stage.epigraph_var.fixed) + for indexed_var in separation_model.first_stage.decision_rule_vars: for dr_var in indexed_var.values(): self.assertTrue(dr_var.fixed, msg=f"DR var {dr_var.name!r} not fixed") # first-stage equality constraints should be inactive self.assertFalse(separation_model.user_model.eq_con.active) - for coeff_con in separation_model.coefficient_matching_conlist.values(): + for coeff_con in separation_model.first_stage.coefficient_matching_cons: self.assertFalse( coeff_con.active, msg=f"Coefficient mathcing constraint {coeff_con.name!r} active." ) - def test_construct_separation_problem_perf_ineq_cons(self): + def test_construct_separation_problem_ss_ineq_cons(self): """ - Check performance inequality constraints are deactivated + Check second-stage inequality constraints are deactivated and replaced with objectives, as appropriate. """ model_data, config = build_simple_model_data(objective_focus="worst_case") separation_model = construct_separation_problem(model_data, config) - # check performance constraints deactivated - # check these individually - self.assertFalse(separation_model.epigraph_con.active) - self.assertFalse(separation_model.user_model.con.active) - - # check expression of performance cons correct + # check expression of second-stage ineq cons correct # check these individually # (i.e. uncertain params have been replaced) m = separation_model.user_model @@ -133,51 +128,62 @@ def test_construct_separation_problem_perf_ineq_cons(self): u2_var = separation_model.uncertainty.uncertain_param_var_list[1] assertExpressionsEqual( self, - separation_model.epigraph_con.expr, + separation_model.second_stage.inequality_cons["epigraph_con"].expr, ( m.x1 + m.x2 / 2 + m.x3 / 3 + u1_var + u2_var - - separation_model.epigraph_var <= 0 + - separation_model.first_stage.epigraph_var <= 0 ), ) - self.assertFalse(separation_model.epigraph_con.active) - self.assertFalse(m.con.active) - self.assertFalse(m.var_x3_certain_lower_bound_con.active) - self.assertFalse(m.var_x3_certain_upper_bound_con.active) + self.assertFalse( + separation_model.second_stage.inequality_cons["epigraph_con"].active + ) + self.assertFalse( + m.con.active, + separation_model.second_stage.inequality_cons["ineq_con_con_upper_bound_con"].active + ) + self.assertFalse( + m.con.active, + separation_model.second_stage.inequality_cons["var_x3_certain_lower_bound_con"].active + ) + self.assertFalse( + m.con.active, + separation_model.second_stage.inequality_cons["var_x3_certain_upper_bound_con"].active + ) - # check performance con expressions match obj expressions + # check second-stage ineq con expressions match obj expressions # (loop through the con to obj map) self.assertEqual( - len(separation_model.perf_ineq_con_to_obj_map), - len(separation_model.effective_performance_inequality_cons), + len(separation_model.second_stage_ineq_con_to_obj_map), + len(separation_model.second_stage.inequality_cons), ) - for perf_con, obj in separation_model.perf_ineq_con_to_obj_map.items(): + for ineq_con, obj in separation_model.second_stage_ineq_con_to_obj_map.items(): assertExpressionsEqual( self, - perf_con.body - perf_con.upper, + ineq_con.body - ineq_con.upper, obj.expr, ) - def test_construct_separation_problem_perf_eq_and_dr_cons(self): + def test_construct_separation_problem_ss_eq_and_dr_cons(self): """ - Check performance and DR equations are appropriately handled + Check second-stage and DR equations are appropriately handled by the separation problems. """ # check DR equation is active model_data, config = build_simple_model_data(objective_focus="worst_case") separation_model = construct_separation_problem(model_data, config) - self.assertTrue(separation_model.decision_rule_eqns[0].active) + self.assertTrue(separation_model.second_stage.decision_rule_eqns[0].active) u1_var = separation_model.uncertainty.uncertain_param_var_list[0] u2_var = separation_model.uncertainty.uncertain_param_var_list[1] assertExpressionsEqual( self, - separation_model.decision_rule_eqns[0].expr, + separation_model.second_stage.decision_rule_eqns[0].expr, ( - separation_model.decision_rule_vars[0][0] - + u1_var * separation_model.decision_rule_vars[0][1] - + u2_var * separation_model.decision_rule_vars[0][2] + separation_model.first_stage.decision_rule_vars[0][0] + + u1_var * separation_model.first_stage.decision_rule_vars[0][1] + + u2_var * separation_model.first_stage.decision_rule_vars[0][2] - separation_model.user_model.x3 == 0 ), diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index c93c17e8b4a..ca899c3b5a6 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -30,6 +30,7 @@ from pyomo.common.modeling import unique_component_name from pyomo.common.timing import HierarchicalTimer, TicTocTimer from pyomo.core.base import ( + Any, Block, Component, ConcreteModel, @@ -1400,12 +1401,12 @@ def add_effective_var_partitioning(model_data, config): ) -def create_bound_constraint_expr(expr, bound, bound_type): +def create_bound_constraint_expr(expr, bound, bound_type, standardize=True): """ Create a relational expression establishing a bound for a numeric expression of interest. - The expression is such that the bound appears on the + If desired, the expression is such that `bound` appears on the right-hand side of the relational (inequality/equality) operator. @@ -1420,6 +1421,9 @@ def create_bound_constraint_expr(expr, bound, bound_type): bound_type : {'lower', 'eq', 'upper'} Indicator for whether `expr` is to be lower bounded, equality bounded, or upper bounded, by `bound`. + standardize : bool, optional + True to ensure `expr` appears on the left-hand side of the + relational operator, False otherwise. Returns ------- @@ -1427,7 +1431,7 @@ def create_bound_constraint_expr(expr, bound, bound_type): Establishes a bound on `expr`. """ if bound_type == "lower": - return -expr <= -bound + return -expr <= -bound if standardize else bound <= expr elif bound_type == "eq": return expr == bound elif bound_type == "upper": @@ -1485,7 +1489,7 @@ def turn_nonadjustable_var_bounds_to_constraints(model_data): constraints, as these are the only bounds we need to reformulate to properly construct the subproblems. Consequently, all constraints added to the working model - in this method are considered performance constraints. + in this method are considered second-stage constraints. Parameters ---------- @@ -1495,9 +1499,6 @@ def turn_nonadjustable_var_bounds_to_constraints(model_data): PyROS solver settings. """ working_model = model_data.working_model - performance_eq_cons = working_model.effective_performance_equality_cons - performance_ineq_cons = working_model.effective_performance_inequality_cons - nonadjustable_vars = ( working_model.effective_var_partitioning.first_stage_variables ) @@ -1518,22 +1519,17 @@ def turn_nonadjustable_var_bounds_to_constraints(model_data): ) ) if is_bound_uncertain: - var_bound_con = Constraint( - expr=create_bound_constraint_expr(var, bound, btype), - ) - working_model.user_model.add_component( - unique_component_name( - working_model.user_model, - f"var_{var_name}_uncertain_{btype}_bound_con", - ), - var_bound_con, - ) + new_con_expr = create_bound_constraint_expr(var, bound, btype) + new_con_name = f"var_{var_name}_uncertain_{btype}_bound_con" remove_var_declared_bound(var, btype) - if btype == "eq": - performance_eq_cons.append(var_bound_con) + working_model.second_stage.equality_cons[new_con_name] = ( + new_con_expr + ) else: - performance_ineq_cons.append(var_bound_con) + working_model.second_stage.inequality_cons[new_con_name] = ( + new_con_expr + ) # for subsequent developments: return a mapping # from each variable to the corresponding binding constraints? @@ -1552,7 +1548,7 @@ def turn_adjustable_var_bounds_to_constraints(model_data): as this is required for appropriate construction of the subproblems later. Since these constraints depend on adjustable variables, - they are taken to be (effective) performance constraints. + they are taken to be (effective) second-stage constraints. Parameters ---------- @@ -1562,8 +1558,6 @@ def turn_adjustable_var_bounds_to_constraints(model_data): PyROS solver settings. """ working_model = model_data.working_model - performance_eq_cons = working_model.effective_performance_equality_cons - performance_ineq_cons = working_model.effective_performance_inequality_cons adjustable_vars = ( working_model.effective_var_partitioning.second_stage_variables @@ -1584,20 +1578,16 @@ def turn_adjustable_var_bounds_to_constraints(model_data): for certainty_desc, bound_triple in cert_uncert_bound_zip: for btype, bound in bound_triple._asdict().items(): if bound is not None: - var_bound_con = Constraint( - expr=create_bound_constraint_expr(var, bound, btype), - ) - working_model.user_model.add_component( - unique_component_name( - working_model.user_model, - f"var_{var_name}_{certainty_desc}_{btype}_bound_con", - ), - var_bound_con, - ) + new_con_name = f"var_{var_name}_{certainty_desc}_{btype}_bound_con" + new_con_expr = create_bound_constraint_expr(var, bound, btype) if btype == "eq": - performance_eq_cons.append(var_bound_con) + working_model.second_stage.equality_cons[new_con_name] = ( + new_con_expr + ) else: - performance_ineq_cons.append(var_bound_con) + working_model.second_stage.inequality_cons[new_con_name] = ( + new_con_expr + ) remove_all_var_bounds(var) @@ -1639,6 +1629,14 @@ def setup_working_model(model_data, config, user_var_partitioning): # now set up working model model_data.working_model = working_model = ConcreteModel() + # stagewise blocks for containing stagewise constraints + working_model.first_stage = Block() + working_model.first_stage.equality_cons = Constraint(Any) + working_model.first_stage.inequality_cons = Constraint(Any) + working_model.second_stage = Block() + working_model.second_stage.equality_cons = Constraint(Any) + working_model.second_stage.inequality_cons = Constraint(Any) + # original user model will be a sub-block of working model, # in order to avoid attribute name clashes later working_model.user_model = original_model.clone() @@ -1670,38 +1668,12 @@ def setup_working_model(model_data, config, user_var_partitioning): else: working_model.original_active_inequality_cons.append(con) - # partition the constraints according to their - # status as equality/inequality constraints - # and their dependence on adjustable variables or uncertain - # parameters. - # we will need this later for construction of the subproblems - working_model.effective_first_stage_equality_cons = [] - working_model.effective_first_stage_inequality_cons = [] - working_model.effective_performance_equality_cons = [] - working_model.effective_performance_inequality_cons = [] - - -def remove_con_declared_bound(con, bound_type): - """ - Remove a bound in a constraint data expression. - """ - if bound_type == "lower": - con.set_value((None, con.body, con.upper)) - elif bound_type == "eq": - con.set_value((None, con.body, None)) - elif bound_type == "upper": - con.set_value((con.lower, con.body, None)) - else: - raise ValueError( - f"Bound type {bound_type} not supported." - ) - def standardize_inequality_constraints(model_data): """ Standardize the inequality constraints of the working model, - and classify them as first-stage inequalities or performance - (i.e., second-stage) inequalities. + and classify them as first-stage inequalities or second-stage + inequalities. Parameters ---------- @@ -1723,6 +1695,10 @@ def standardize_inequality_constraints(model_data): ComponentSet(identify_variables(con.body)) & adjustable_vars_set ) + con_rel_name = con.getname( + relative_to=working_model.user_model, + fully_qualified=True, + ) if uncertain_params_in_con_expr | adjustable_vars_in_con_body: con_bounds_triple = rearrange_bound_pair_to_triple( @@ -1748,48 +1724,45 @@ def standardize_inequality_constraints(model_data): "Report this case to the Pyomo/PyROS developers." ) - std_con_expr = create_bound_constraint_expr(con.body, bound, btype) + std_con_expr = create_bound_constraint_expr( + expr=con.body, + bound=bound, + bound_type=btype, + standardize=True, + ) + new_con_name = f"ineq_con_{con_rel_name}_{btype}_bound_con" - uncertain_params_in_std_expr = ComponentSet( + uncertain_params_in_std_expr = uncertain_params_set & ComponentSet( identify_mutable_parameters(std_con_expr) - ) & uncertain_params_set + ) if adjustable_vars_in_con_body | uncertain_params_in_std_expr: - if len(finite_bounds) == 1: - # modify constraints with only a single inequality - # operator in place, for efficiency - con.set_value(std_con_expr) - new_con = con - else: - # ranged constraint: declare a new constraint for - # each of the performance inequalities; - # first-stage inequalities remain in place - new_con_name = con.getname( - relative_to=working_model.user_model, - fully_qualified=True, - ) - new_con = Constraint(expr=std_con_expr) - working_model.user_model.add_component( - f"con_{new_con_name}_{btype}_bound_con", - new_con, - ) - remove_con_declared_bound(con, btype) - working_model.effective_performance_inequality_cons.append(new_con) + working_model.second_stage.inequality_cons[new_con_name] = ( + std_con_expr + ) else: - # constraint has a first-stage inequality (bound) - # this inequality (bound) will not be modified - working_model.effective_first_stage_inequality_cons.append(con) - - if con.lower is None and con.upper is None: - # either the original constraint had no bounds, - # or the inequalities (bounds) have been stripped - # and used to declare performance constraints - con.deactivate() + # we do not want to modify the arrangement of + # lower bound for first-stage inequalities, so + # pass `standardize=False` + working_model.first_stage.inequality_cons[new_con_name] = ( + create_bound_constraint_expr( + expr=con.body, + bound=bound, + bound_type=btype, + standardize=False, + ) + ) + + # constraint has now been moved over to stagewise blocks + con.deactivate() else: # constraint depends on the nonadjustable variables only - working_model.effective_first_stage_inequality_cons.append(con) + working_model.first_stage.inequality_cons[f"ineq_con_{con_rel_name}"] = ( + con.expr + ) + con.deactivate() # for subsequent developments: map the original constraints - # to the derived performance inequalities? + # to the derived second-stage inequalities? # we will add this as needed when changes are made to # the interface for separation priority ordering @@ -1797,7 +1770,7 @@ def standardize_inequality_constraints(model_data): def standardize_equality_constraints(model_data): """ Classify the original active equality constraints of the - working model as first-stage or performance constraints. + working model as first-stage or second-stage constraints. Parameters ---------- @@ -1821,10 +1794,21 @@ def standardize_equality_constraints(model_data): ) # note: none of the equality constraint expressions are modified + con_rel_name = con.getname( + relative_to=working_model.user_model, + fully_qualified=True, + ) if uncertain_params_in_con_expr | adjustable_vars_in_con_body: - working_model.effective_performance_equality_cons.append(con) + working_model.second_stage.equality_cons[f"eq_con_{con_rel_name}"] = ( + con.expr + ) else: - working_model.effective_first_stage_equality_cons.append(con) + working_model.first_stage.equality_cons[f"eq_con_{con_rel_name}"] = ( + con.expr + ) + + # definitely don't want active duplicate + con.deactivate() def get_summands(expr): @@ -1964,10 +1948,9 @@ def standardize_active_objective(model_data, config): objective=active_obj, ) - # epigraph reformulation components will be useful for later - working_model.epigraph_var = Var(initialize=value(active_obj, exception=False)) - working_model.epigraph_con = Constraint( - expr=working_model.full_objective.expr - working_model.epigraph_var <= 0 + # useful for later + working_model.first_stage.epigraph_var = Var( + initialize=value(active_obj, exception=False) ) # we add the epigraph objective later, as needed, @@ -1975,7 +1958,7 @@ def standardize_active_objective(model_data, config): # doing so is more efficient than adding the objective now active_obj.deactivate() - # classify the epigraph constraint + # add the epigraph constraint adjustable_vars = ( working_model.effective_var_partitioning.second_stage_variables + working_model.effective_var_partitioning.state_variables @@ -1990,12 +1973,16 @@ def standardize_active_objective(model_data, config): ) if (uncertain_params_in_obj | adjustable_vars_in_obj): if config.objective_focus == ObjectiveType.worst_case: - working_model.effective_performance_inequality_cons.append( - working_model.epigraph_con + working_model.second_stage.inequality_cons["epigraph_con"] = ( + working_model.full_objective.expr + - working_model.first_stage.epigraph_var + <= 0 ) elif config.objective_focus == ObjectiveType.nominal: - working_model.effective_first_stage_inequality_cons.append( - working_model.epigraph_con + working_model.first_stage.inequality_cons["epigraph_con"] = ( + working_model.full_objective.expr + - working_model.first_stage.epigraph_var + <= 0 ) else: raise ValueError( @@ -2004,8 +1991,10 @@ def standardize_active_objective(model_data, config): f"for objective focus {config.objective_focus!r}." ) else: - working_model.effective_first_stage_inequality_cons.append( - working_model.epigraph_con + working_model.first_stage.inequality_cons["epigraph_con"] = ( + working_model.full_objective.expr + - working_model.first_stage.epigraph_var + <= 0 ) @@ -2019,7 +2008,7 @@ def get_all_nonadjustable_variables(working_model): - decision rule variables - effective first-stage variables """ - epigraph_var = working_model.epigraph_var + epigraph_var = working_model.first_stage.epigraph_var decision_rule_vars = list( generate_all_decision_rule_var_data_objects(working_model) ) @@ -2056,7 +2045,7 @@ def generate_all_decision_rule_var_data_objects(working_blk): VarData Decision rule variable. """ - for indexed_var in working_blk.decision_rule_vars: + for indexed_var in working_blk.first_stage.decision_rule_vars: yield from indexed_var.values() @@ -2064,8 +2053,7 @@ def generate_all_decision_rule_eqns(working_blk): """ Generate sequence of all decision rule equations. """ - for indexed_con in working_blk.decision_rule_eqns: - yield from indexed_con.values() + yield from working_blk.second_stage.decision_rule_eqns.values() def get_dr_expression(working_blk, second_stage_var): @@ -2138,10 +2126,10 @@ def check_time_limit_reached(timing_data, config): def reformulate_state_var_independent_eq_cons(model_data, config): """ - Reformulate performance equality constraints that are + Reformulate second-stage equality constraints that are independent of the state variables. - The state variable-independent performance equality + The state variable-independent second-stage equality constraints that can be rewritten as polynomials in terms of the uncertain parameters are reformulated to first-stage equalities @@ -2151,8 +2139,8 @@ def reformulate_state_var_independent_eq_cons(model_data, config): In some cases, matching of the coefficients may lead to a certificate of robust infeasibility. - All other state variable-independent performance equality - constraints are recast to pairs of opposing performance inequality + All other state variable-independent second-stage equality + constraints are recast to pairs of opposing second-stage inequality constraints, as they would otherwise over-constrain the uncertain parameters in the separation subproblems. @@ -2206,13 +2194,11 @@ def reformulate_state_var_independent_eq_cons(model_data, config): id(param): var for param, var in uncertain_param_to_temp_var_map.items() } - # constraints generated during the reformulation will be placed here - working_model.coefficient_matching_conlist = coeff_matching_conlist = ( - ConstraintList() - ) - - performance_eq_cons = working_model.effective_performance_equality_cons.copy() - for con in performance_eq_cons: + # copy the items iterable, + # as we will be modifying the constituents of the constraint + # in place + working_model.first_stage.coefficient_matching_cons = coefficient_matching_cons = [] + for con_idx, con in list(working_model.second_stage.equality_cons.items()): vars_in_con = ComponentSet(identify_variables(con.expr)) mutable_params_in_con = ComponentSet(identify_mutable_parameters(con.expr)) @@ -2273,29 +2259,22 @@ def reformulate_state_var_independent_eq_cons(model_data, config): # in the separation problems, since the effective DOF # variables and DR variables are fixed. # hence, we reformulate to inequalities - con_name = con.getname( - relative_to=working_model.user_model, - fully_qualified=True, - ) for bound_type in ["lower", "upper"]: std_con_expr = create_bound_constraint_expr( expr=con.body, bound=con.upper, bound_type=bound_type ) - new_con = Constraint(expr=std_con_expr) - working_model.user_model.add_component( - f"con_{con_name}_{bound_type}_bound_con", - new_con, - ) - working_model.effective_performance_inequality_cons.append(new_con) + working_model.second_stage.inequality_cons[ + f"reform_{bound_type}_bound_from_{con_idx}" + ] = std_con_expr else: polynomial_repn_coeffs = ( [expr_repn.constant] + list(expr_repn.linear_coefs) + list(expr_repn.quadratic_coefs) ) - for coef_expr in polynomial_repn_coeffs: - simplified_coef_expr = generate_standard_repn( - expr=coef_expr, + for coeff_idx, coeff_expr in enumerate(polynomial_repn_coeffs): + simplified_coeff_expr = generate_standard_repn( + expr=coeff_expr, compute_values=True, ).to_expression() @@ -2305,12 +2284,12 @@ def reformulate_state_var_independent_eq_cons(model_data, config): # we either check for trivial robust # feasibility/infeasibility, or add a constraint # restricting the coefficient expression to value 0 - if isinstance(simplified_coef_expr, tuple(native_types)): + if isinstance(simplified_coeff_expr, tuple(native_types)): # coefficient is a constant; # check value to determine # trivial feasibility/infeasibility robust_infeasible = not math.isclose( - a=simplified_coef_expr, + a=simplified_coeff_expr, b=0, rel_tol=COEFF_MATCH_REL_TOL, abs_tol=COEFF_MATCH_ABS_TOL, @@ -2336,26 +2315,24 @@ def reformulate_state_var_independent_eq_cons(model_data, config): else: # coefficient is dependent on model first-stage # and DR variables. add matching constraint - coeff_matching_conlist.add(simplified_coef_expr == 0) - - # matching constraint depends on nonadjustable - # variables only, so it is first-stage - last_idx = coeff_matching_conlist.index_set().last() - working_model.effective_first_stage_equality_cons.append( - coeff_matching_conlist[last_idx] + new_con_name = f"coeff_matching_{con_idx}_coeff_{coeff_idx}" + working_model.first_stage.equality_cons[new_con_name] = ( + simplified_coeff_expr == 0 ) + new_con = working_model.first_stage.equality_cons[new_con_name] + coefficient_matching_cons.append(new_con) config.progress_logger.info( f"Derived from constraint {con.name!r} a coefficient " - "matching constraint with expression: \n " - f"{coeff_matching_conlist[last_idx].expr}." + f"matching constraint named {new_con_name!r} " + "with expression: \n " + f"{new_con.expr}." ) - # constraint has been reformulated out of the model, - # either by coefficient matching - # or by casting to two inequalities - con.deactivate() - working_model.effective_performance_equality_cons.remove(con) + # remove rather than deactivate to facilitate: + # - we no longer need this constraint anywhere + # - faciliates accurate counting of active constraints + del working_model.second_stage.equality_cons[con_idx] # we no longer need these auxiliary components working_model.del_component(temp_param_vars) @@ -2421,12 +2398,11 @@ def preprocess_model_data(model_data, config, user_var_partitioning): ) model_data.working_model.all_variables = ( model_data.working_model.all_nonadjustable_variables - + model_data.working_model.effective_var_partitioning.second_stage_variables - + model_data.working_model.effective_var_partitioning.state_variables + + model_data.working_model.all_adjustable_variables ) config.progress_logger.debug( - "Reformulating state variable-independent performance equality constraints..." + "Reformulating state variable-independent second-stage equality constraints..." ) robust_infeasible = reformulate_state_var_independent_eq_cons( model_data, @@ -2473,24 +2449,24 @@ def log_model_statistics(model_data, config): ) # # equality constraints - num_eq_cons = len( - working_model.effective_first_stage_equality_cons - + working_model.effective_performance_equality_cons - + working_model.decision_rule_eqns + num_eq_cons = ( + len(working_model.first_stage.equality_cons) + + len(working_model.second_stage.equality_cons) + + len(working_model.second_stage.decision_rule_eqns) ) - num_first_stage_eq_cons = len(working_model.effective_first_stage_equality_cons) - num_coeff_matching_cons = len(working_model.coefficient_matching_conlist) + num_first_stage_eq_cons = len(working_model.first_stage.equality_cons) + num_coeff_matching_cons = len(working_model.first_stage.coefficient_matching_cons) num_other_first_stage_eqns = num_first_stage_eq_cons - num_coeff_matching_cons - num_performance_eq_cons = len(working_model.effective_performance_equality_cons) - num_dr_eq_cons = len(working_model.decision_rule_eqns) + num_second_stage_eq_cons = len(working_model.second_stage.equality_cons) + num_dr_eq_cons = len(working_model.second_stage.decision_rule_eqns) # # inequality constraints - num_ineq_cons = len( - working_model.effective_first_stage_inequality_cons - + working_model.effective_performance_inequality_cons + num_ineq_cons = ( + len(working_model.first_stage.inequality_cons) + + len(working_model.second_stage.inequality_cons) ) - num_first_stage_ineq_cons = len(working_model.effective_first_stage_inequality_cons) - num_performance_ineq_cons = len(working_model.effective_performance_inequality_cons) + num_first_stage_ineq_cons = len(working_model.first_stage.inequality_cons) + num_second_stage_ineq_cons = len(working_model.second_stage.inequality_cons) info_log_func = config.progress_logger.info @@ -2516,11 +2492,11 @@ def log_model_statistics(model_data, config): info_log_func(f" Equality constraints : {num_eq_cons}") info_log_func(f" Coefficient matching constraints : {num_coeff_matching_cons}") info_log_func(f" Other first-stage equations : {num_other_first_stage_eqns}") - info_log_func(f" Performance equations : {num_performance_eq_cons}") + info_log_func(f" Second-stage equations : {num_second_stage_eq_cons}") info_log_func(f" Decision rule equations : {num_dr_eq_cons}") info_log_func(f" Inequality constraints : {num_ineq_cons}") info_log_func(f" First-stage inequalities : {num_first_stage_ineq_cons}") - info_log_func(f" Performance inequalities : {num_performance_ineq_cons}") + info_log_func(f" Second-stage inequalities : {num_second_stage_ineq_cons}") def add_decision_rule_variables(model_data, config): @@ -2547,7 +2523,7 @@ def add_decision_rule_variables(model_data, config): effective_second_stage_vars = ( model_data.working_model.effective_var_partitioning.second_stage_variables ) - model_data.working_model.decision_rule_vars = decision_rule_vars = [] + model_data.working_model.first_stage.decision_rule_vars = decision_rule_vars = [] # facilitate matching of effective second-stage vars to DR vars later model_data.working_model.eff_ss_var_to_dr_var_map = eff_ss_var_to_dr_var_map = ( @@ -2568,7 +2544,7 @@ def add_decision_rule_variables(model_data, config): indexed_dr_var = Var( range(num_dr_vars), initialize=0, bounds=(None, None), domain=Reals ) - model_data.working_model.add_component( + model_data.working_model.first_stage.add_component( f"decision_rule_var_{idx}", indexed_dr_var ) @@ -2598,11 +2574,13 @@ def add_decision_rule_constraints(model_data, config): effective_second_stage_vars = ( model_data.working_model.effective_var_partitioning.second_stage_variables ) - indexed_dr_var_list = model_data.working_model.decision_rule_vars + indexed_dr_var_list = model_data.working_model.first_stage.decision_rule_vars uncertain_params = model_data.working_model.uncertain_params degree = config.decision_rule_order - model_data.working_model.decision_rule_eqns = decision_rule_eqns = [] + model_data.working_model.second_stage.decision_rule_eqns = decision_rule_eqns = ( + Constraint(range(len(effective_second_stage_vars))) + ) # keeping track of degree of monomial # (in terms of the uncertain parameters) @@ -2649,14 +2627,11 @@ def add_decision_rule_constraints(model_data, config): dr_var_to_exponent_map[dr_var] = len(param_combo) # declare constraint on model - dr_eqn = Constraint(expr=dr_expression - eff_ss_var == 0) - model_data.working_model.add_component(f"decision_rule_eqn_{idx}", dr_eqn) - - decision_rule_eqns.append(dr_eqn) - eff_ss_var_to_dr_eqn_map[eff_ss_var] = dr_eqn + decision_rule_eqns[idx] = dr_expression - eff_ss_var == 0 + eff_ss_var_to_dr_eqn_map[eff_ss_var] = decision_rule_eqns[idx] -def enforce_dr_degree(blk, config, degree): +def enforce_dr_degree(working_blk, config, degree): """ Make decision rule polynomials of a given degree by fixing value of the appropriate subset of the decision @@ -2671,9 +2646,9 @@ def enforce_dr_degree(blk, config, degree): degree : int Degree of the DR polynomials that is to be enforced. """ - for indexed_dr_var in blk.decision_rule_vars: + for indexed_dr_var in working_blk.first_stage.decision_rule_vars: for dr_var in indexed_dr_var.values(): - dr_var_degree = blk.dr_var_to_exponent_map[dr_var] + dr_var_degree = working_blk.dr_var_to_exponent_map[dr_var] if dr_var_degree > degree: dr_var.fix(0) else: @@ -2886,7 +2861,7 @@ class IterationLogRecord: dr_polishing_success : bool or None, optional True if DR polishing solved successfully, False otherwise. num_violated_cons : int or None, optional - Number of performance constraints found to be violated + Number of second-stage constraints found to be violated during separation step. all_sep_problems_solved : int or None, optional True if all separation problems were solved successfully, @@ -2897,7 +2872,7 @@ class IterationLogRecord: True if separation problems were solved with the subordinate global optimizer(s), False otherwise. max_violation : int or None - Maximum scaled violation of any performance constraint + Maximum scaled violation of any second-stage constraint found during separation step. elapsed_time : float, optional Total time elapsed up to the current iteration, in seconds. @@ -2925,7 +2900,7 @@ class IterationLogRecord: dr_polishing_success : bool or None True if DR polishing was solved successfully, False otherwise. num_violated_cons : int or None - Number of performance constraints found to be violated + Number of second-stage constraints found to be violated during separation step. all_sep_problems_solved : int or None True if all separation problems were solved successfully, @@ -2936,7 +2911,7 @@ class IterationLogRecord: True if separation problems were solved with the subordinate global optimizer(s), False otherwise. max_violation : int or None - Maximum scaled violation of any performance constraint + Maximum scaled violation of any second-stage constraint found during separation step. elapsed_time : float Total time elapsed up to the current iteration, in seconds. From cc0d3f53ca03c782dafe81741f0f861036b3f30f Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 9 Aug 2024 21:48:38 -0400 Subject: [PATCH 2161/3044] Remove unused solve time retrieval method --- pyomo/contrib/pyros/util.py | 40 ------------------------------------- 1 file changed, 40 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index ca899c3b5a6..97c0f1c6e3b 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -540,46 +540,6 @@ class ObjectiveType(Enum): nominal = auto() -def get_time_from_solver(results): - """ - Obtain solver time from a Pyomo `SolverResults` object. - - Returns - ------- - : float - Solver time. May be CPU time or elapsed time, - depending on the solver. If no time attribute - is found, then `float("nan")` is returned. - - NOTE - ---- - This method attempts to access solver time through the - attributes of `results.solver` in the following order - of precedence: - - 1) Attribute with name ``pyros.util.TIC_TOC_SOLVE_TIME_ATTR``. - This attribute is an estimate of the elapsed solve time - obtained using the Pyomo `TicTocTimer` at the point the - solver from which the results object is derived was invoked. - Preferred over other time attributes, as other attributes - may be in CPUs, and for purposes of evaluating overhead - time, we require wall s. - 2) `'user_time'` if the results object was returned by a GAMS - solver, `'time'` otherwise. - """ - solver_name = getattr(results.solver, "name", None) - - # is this sufficient to confirm GAMS solver used? - from_gams = solver_name is not None and str(solver_name).startswith("GAMS ") - time_attr_name = "user_time" if from_gams else "time" - for attr_name in [TIC_TOC_SOLVE_TIME_ATTR, time_attr_name]: - solve_time = getattr(results.solver, attr_name, None) - if solve_time is not None: - break - - return float("nan") if solve_time is None else solve_time - - def standardize_component_data( obj, valid_ctype, From 799d69426677aab02844503d3f76402a33edd299 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 9 Aug 2024 21:54:20 -0400 Subject: [PATCH 2162/3044] Unify component data arg type validators --- pyomo/contrib/pyros/config.py | 67 ++++++++--------------------------- 1 file changed, 14 insertions(+), 53 deletions(-) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index c02dcd7ed0f..4ce4aebea0c 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -19,7 +19,11 @@ from pyomo.core.base import Var, VarData from pyomo.core.base.param import Param, ParamData from pyomo.opt import SolverFactory -from pyomo.contrib.pyros.util import ObjectiveType, setup_pyros_logger +from pyomo.contrib.pyros.util import ( + ObjectiveType, + setup_pyros_logger, + standardize_component_data, +) from pyomo.contrib.pyros.uncertainty_sets import UncertaintySet @@ -131,24 +135,6 @@ def __init__( self.cdatatype_validator = cdatatype_validator self.allow_repeats = allow_repeats - def standardize_ctype_obj(self, obj): - """ - Standardize object of type ``self.ctype`` to list - of objects of type ``self.cdatatype``. - """ - if self.ctype_validator is not None: - self.ctype_validator(obj) - return list(obj.values()) - - def standardize_cdatatype_obj(self, obj): - """ - Standardize object of type ``self.cdatatype`` to - ``[obj]``. - """ - if self.cdatatype_validator is not None: - self.cdatatype_validator(obj) - return [obj] - def __call__(self, obj, from_iterable=None, allow_repeats=None): """ Cast object to a flat list of Pyomo component data type @@ -172,40 +158,15 @@ def __call__(self, obj, from_iterable=None, allow_repeats=None): ValueError If the resulting list contains duplicate entries. """ - if allow_repeats is None: - allow_repeats = self.allow_repeats - - if isinstance(obj, self.ctype): - ans = self.standardize_ctype_obj(obj) - elif isinstance(obj, self.cdatatype): - ans = self.standardize_cdatatype_obj(obj) - elif isinstance(obj, Iterable) and not isinstance(obj, str): - ans = [] - for item in obj: - ans.extend(self.__call__(item, from_iterable=obj)) - else: - from_iterable_qual = ( - f" (entry of iterable {from_iterable})" - if from_iterable is not None - else "" - ) - raise TypeError( - f"Input object {obj!r}{from_iterable_qual} " - "is not of valid component type " - f"{self.ctype.__name__} or component data type " - f"{self.cdatatype.__name__}." - ) - - # check for duplicates if desired - if not allow_repeats and len(ans) != len(ComponentSet(ans)): - comp_name_list = [comp.name for comp in ans] - raise ValueError( - f"Standardized component list {comp_name_list} " - f"derived from input {obj} " - "contains duplicate entries." - ) - - return ans + return standardize_component_data( + obj=obj, + valid_ctype=self.ctype, + valid_cdatatype=self.cdatatype, + ctype_validator=self.ctype_validator, + cdatatype_validator=self.cdatatype_validator, + allow_repeats=allow_repeats, + from_iterable=from_iterable, + ) def domain_name(self): """Return str briefly describing domain encompassed by self.""" From aa3ea48b6680cfa4e2eee4aa7da2a133ac7d6963 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 20:57:50 -0600 Subject: [PATCH 2163/3044] NF: apply black --- pyomo/core/base/set.py | 4 ++-- pyomo/core/tests/unit/test_set.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 6cbd9f5d5ee..26f44ffc08a 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1489,7 +1489,7 @@ def _cb_validate_filter(self, mode, val_iter): "callback signature matched (block, *value). " "Please update the callback to match the signature " "(block, value, *index).", - version='6.7.4.dev0' + version='6.7.4.dev0', ) orig_fcn = fcn._fcn fcn = ParameterizedScalarCallInitializer( @@ -1512,7 +1512,7 @@ def _cb_validate_filter(self, mode, val_iter): "callback signature matched (block, *value, *index). " "Please update the callback to match the signature " "(block, value, *index).", - version='6.7.4.dev0' + version='6.7.4.dev0', ) if fcn.__class__ is not ParameterizedInitializer: orig_fcn = fcn._fcn diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 41b88f3eb20..17a9e254a65 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4361,7 +4361,7 @@ def _validate(model, i, j): OUT.getvalue().replace('\n', ' '), r"DEPRECATED: InsertionOrderSetData J1\[2,2\]: validate callback " r"signature matched \(block, \*value\). Please update the " - r"callback to match the signature \(block, value, \*index\)" + r"callback to match the signature \(block, value, \*index\)", ) with LoggingIntercept() as OUT: with self.assertRaisesRegex( @@ -4391,7 +4391,7 @@ def _validate(model, i, j, ind1, ind2): OUT.getvalue().replace('\n', ' '), r"DEPRECATED: InsertionOrderSetData J2\[2,2\]: validate callback " r"signature matched \(block, \*value, \*index\). Please update the " - r"callback to match the signature \(block, value, \*index\)" + r"callback to match the signature \(block, value, \*index\)", ) with LoggingIntercept() as OUT: @@ -4414,7 +4414,6 @@ def _validate(model, i, j, ind1, ind2): "Exception raised while validating element '(2, 2)' for Set J2[2,2]\n", ) - def _validate(model, v, ind1, ind2): self.assertIs(model, m) i, j = v From 3aac6938f89a989dc4f25ac3ae8f494140480c52 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 10 Aug 2024 12:57:56 -0400 Subject: [PATCH 2164/3044] Change coeff matching logging level to DEBUG --- pyomo/contrib/pyros/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 97c0f1c6e3b..dedc3587eb8 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -2282,7 +2282,7 @@ def reformulate_state_var_independent_eq_cons(model_data, config): new_con = working_model.first_stage.equality_cons[new_con_name] coefficient_matching_cons.append(new_con) - config.progress_logger.info( + config.progress_logger.debug( f"Derived from constraint {con.name!r} a coefficient " f"matching constraint named {new_con_name!r} " "with expression: \n " From b24f0a4081d81304a9db9812ebfb644abc87e7d3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 10 Aug 2024 11:11:24 -0600 Subject: [PATCH 2165/3044] Update baseline with additional Set example --- examples/pyomo/tutorials/set.out | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/pyomo/tutorials/set.out b/examples/pyomo/tutorials/set.out index dd1ef2d4335..3f278a2f9b2 100644 --- a/examples/pyomo/tutorials/set.out +++ b/examples/pyomo/tutorials/set.out @@ -1,4 +1,4 @@ -24 Set Declarations +25 Set Declarations A : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 3 : {1, 2, 3} @@ -80,6 +80,11 @@ Key : Dimen : Domain : Size : Members 2 : 1 : Any : 2 : {1, 3} 5 : 1 : Any : 2 : {2, 3} + T_indexed_validate : Size=3, Index=B, Ordered=Insertion + Key : Dimen : Domain : Size : Members + 2 : 1 : Any : 1 : {1,} + 3 : 1 : Any : 2 : {1, 2} + 4 : 1 : Any : 3 : {1, 2, 3} U : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members None : 1 : Any : 5 : {1, 2, 6, 24, 120} @@ -94,4 +99,4 @@ 2 : 1 : S[2] : 1 : {1,} 5 : 1 : S[5] : 2 : {2, 3} -24 Declarations: A B C D E F G H Hsub I J K K_2 L M N O P R S X T U V +25 Declarations: A B C D E F G H Hsub I J K K_2 L M N O P R S X T T_indexed_validate U V From 71876938c38fb388879a8e2b40289cf82a532116 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 10 Aug 2024 20:02:29 -0400 Subject: [PATCH 2166/3044] Fix and test robust infeas detection from master problem --- pyomo/contrib/pyros/master_problem_methods.py | 2 +- pyomo/contrib/pyros/tests/test_grcs.py | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index b232b3f07cd..237000fb347 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -837,7 +837,7 @@ def solver_call_master(master_data, config, master_soln): if not try_backup: if infeasible: - master_soln.pyrosTerminationCondition = ( + master_soln.pyros_termination_condition = ( pyrosTerminationCondition.robust_infeasible ) return diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index dac3d23f256..ecb66f82622 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -422,6 +422,41 @@ def test_two_stg_model_discrete_set(self): ) +class TestPyROSRobustInfeasible(unittest.TestCase): + @unittest.skipUnless(baron_available, "BARON is not available and licensed") + def test_pyros_robust_infeasible(self): + """ + Test PyROS behavior when robust infeasibility detected + from a master problem. + """ + m = ConcreteModel() + m.q = Param(initialize=0.5, mutable=True) + m.x = Var(bounds=(m.q, 1)) + # makes model infeasible since 2 is outside bounds + m.con1 = Constraint(expr=m.x == 2) + m.obj = Objective(expr=m.x) + baron = SolverFactory("baron") + pyros = SolverFactory("pyros") + results = pyros.solve( + model=m, + first_stage_variables=[m.x], + second_stage_variables=[], + uncertain_params=m.q, + uncertainty_set=BoxSet([[0, 1]]), + local_solver=baron, + global_solver=baron, + solve_master_globally=True, + ) + + self.assertEqual( + results.pyros_termination_condition, + pyrosTerminationCondition.robust_infeasible, + ) + self.assertEqual(results.iterations, 1) + # since x was not initialized + self.assertEqual(results.final_objective_value, None) + + global_solver = "baron" From 649ecc8bd67d7639c948e6d3d935392e671d70f7 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 10 Aug 2024 20:04:46 -0400 Subject: [PATCH 2167/3044] Remove unused p-robustness subroutine --- pyomo/contrib/pyros/master_problem_methods.py | 21 ------------------- .../contrib/pyros/pyros_algorithm_methods.py | 2 -- 2 files changed, 23 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 237000fb347..bd3b4825f2f 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -631,27 +631,6 @@ def minimize_dr_vars(master_data, config): return results, True -def add_p_robust_constraint(master_data, config): - """ - p-robustness--adds constraints to the master problem ensuring that the - optimal k-th iteration solution is within (1+rho) of the nominal - objective. The parameter rho is specified by the user and should be between. - """ - rho = config.p_robustness['rho'] - model = master_data.master_model - block_0 = model.scenarios[0, 0] - frac_nom_cost = (1 + rho) * ( - block_0.first_stage_objective + block_0.second_stage_objective - ) - - for block_k in model.scenarios[master_data.iteration, :]: - model.p_robust_constraints.add( - block_k.first_stage_objective + block_k.second_stage_objective - <= frac_nom_cost - ) - return - - def get_master_dr_degree(master_data, config): """ Determine DR polynomial degree to enforce based on diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 008fed2f4c5..49e31ef8383 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -162,8 +162,6 @@ def ROSolver_iterative_solve(model_data, config): while config.max_iter == -1 or k < config.max_iter: master_data.iteration = k - # TODO: what about p-robustness? - # === Solve Master Problem config.progress_logger.debug(f"PyROS working on iteration {k}...") master_soln = master_data.solve_master() From 3c22d04800f491e2991720ea006756a91c9c039b Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 10 Aug 2024 20:59:21 -0400 Subject: [PATCH 2168/3044] Test DR polishing small param coefficients efficiency --- pyomo/contrib/pyros/tests/test_master.py | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index 0240caeea99..5eed6b1352f 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -449,6 +449,36 @@ def test_construct_dr_polishing_problem_objectives(self): self.assertFalse(polishing_model.epigraph_obj.active) self.assertTrue(polishing_model.polishing_obj.active) + def test_construct_dr_polishing_problem_params_zero(self): + """ + Check that DR polishing fixes/deactivates components + for DR expression terms where the product of uncertain + parameters is below tolerance. + """ + master_data, config = self.build_simple_master_data() + + # trigger fixing of the corresponding polishing vars + master_data.master_model.scenarios[0, 0].user_model.u.set_value(1e-10) + master_data.master_model.scenarios[1, 0].user_model.u.set_value(1e-11) + + polishing_model = construct_dr_polishing_problem(master_data, config) + + dr_vars = polishing_model.scenarios[0, 0].first_stage.decision_rule_vars + + # since static DR terms should not be polished + self.assertTrue(polishing_model.polishing_vars[0][0].fixed) + self.assertFalse(polishing_model.polishing_abs_val_lb_con_0[0].active) + self.assertFalse(polishing_model.polishing_abs_val_ub_con_0[0].active) + + # affine term should be fixed to 0, + # since the uncertain param values are small enough. + # polishing constraints are deactivated since we don't need them + self.assertTrue(dr_vars[0][1].fixed) + self.assertEqual(dr_vars[0][1].value, 0) + self.assertTrue(polishing_model.polishing_vars[0][1].fixed) + self.assertFalse(polishing_model.polishing_abs_val_lb_con_0[1].active) + self.assertFalse(polishing_model.polishing_abs_val_ub_con_0[1].active) + class TestSolveMaster(unittest.TestCase): """ From f4204f0f7d5dfb30f68adb88d2e2140e7f4b7303 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 10 Aug 2024 21:46:19 -0400 Subject: [PATCH 2169/3044] Update PyROS solver logging documentation --- doc/OnlineDocs/contributed_packages/pyros.rst | 82 ++++++++++--------- 1 file changed, 45 insertions(+), 37 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index 1fb0c77c4a1..8329d88c66c 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -850,7 +850,7 @@ for a basic tutorial, see the :doc:`logging HOWTO `. every master feasility, master, and DR polishing problem * Progress updates for the separation procedure * Separation subproblem initial point infeasibilities - * Summary of separation loop outcomes: performance constraints + * Summary of separation loop outcomes: second-stage inequality constraints violated, uncertain parameter scenario added to the master problem * Uncertain parameter scenarios added to the master problem @@ -871,12 +871,19 @@ Observe that the log contains the following information: * **Preprocessing information** (lines 39--41). Wall time required for preprocessing the deterministic model and associated components, - i.e. standardizing model components and adding the decision rule + i.e., standardizing model components and adding the decision rule variables and equations. * **Model component statistics** (lines 42--58). Breakdown of model component statistics. Includes components added by PyROS, such as the decision rule variables and equations. + The preprocessor may find that some second-stage variables + and state variables are mathematically + not adjustable to the uncertain parameters; + to this end, in the logs, the numbers of + adjustable second-stage variables and state variables + are included in parentheses, next to the total numbers + of second-stage variables and state variables, respectively. * **Iteration log table** (lines 59--69). Summary information on the problem iterates and subproblem outcomes. The constituent columns are defined in detail in @@ -914,20 +921,20 @@ Observe that the log contains the following information: ============================================================================== PyROS: The Pyomo Robust Optimization Solver, v1.2.11. - Pyomo version: 6.7.2 + Pyomo version: 6.7.4 Commit hash: unknown - Invoked at UTC 2024-03-28T00:00:00.000000 - + Invoked at UTC 2024-09-01T00:00:00.000000 + Developed by: Natalie M. Isenberg (1), Jason A. F. Sherman (1), John D. Siirola (2), Chrysanthos E. Gounaris (1) (1) Carnegie Mellon University, Department of Chemical Engineering (2) Sandia National Laboratories, Center for Computing Research - + The developers gratefully acknowledge support from the U.S. Department of Energy's Institute for the Design of Advanced Energy Systems (IDAES). ============================================================================== ================================= DISCLAIMER ================================= - PyROS is still under development. + PyROS is still under development. Please provide feedback and/or report any issues by creating a ticket at https://github.com/Pyomo/pyomo/issues/new/choose ============================================================================== @@ -953,55 +960,56 @@ Observe that the log contains the following information: p_robustness={} ------------------------------------------------------------------------------ Preprocessing... - Done preprocessing; required wall time of 0.175s. + Done preprocessing; required wall time of 0.018s. ------------------------------------------------------------------------------ - Model statistics: + Model Statistics: Number of variables : 62 Epigraph variable : 1 First-stage variables : 7 - Second-stage variables : 6 - State variables : 18 + Second-stage variables : 6 (6 adj.) + State variables : 18 (7 adj.) Decision rule variables : 30 Number of uncertain parameters : 4 - Number of constraints : 81 + Number of constraints : 52 Equality constraints : 24 Coefficient matching constraints : 0 + Other first-stage equations : 10 + Second-stage equations : 8 Decision rule equations : 6 - All other equality constraints : 18 - Inequality constraints : 57 - First-stage inequalities (incl. certain var bounds) : 10 - Performance constraints (incl. var bounds) : 47 + Inequality constraints : 28 + First-stage inequalities : 1 + Second-stage inequalities : 27 ------------------------------------------------------------------------------ Itn Objective 1-Stg Shift 2-Stg Shift #CViol Max Viol Wall Time (s) ------------------------------------------------------------------------------ - 0 3.5838e+07 - - 5 1.8832e+04 1.741 - 1 3.5838e+07 3.5184e-15 3.9404e-15 10 4.2516e+06 3.766 - 2 3.5993e+07 1.8105e-01 7.1406e-01 13 5.2004e+06 6.288 - 3 3.6285e+07 5.1968e-01 7.7753e-01 4 1.7892e+04 8.247 - 4 3.6285e+07 9.1166e-13 1.9702e-15 0 7.1157e-10g 11.456 + 0 3.5838e+07 - - 1 2.7000e+02 0.657 + 1 3.6087e+07 8.0199e-01 1.2807e-01 5 4.1852e+04 1.460 + 2 3.6125e+07 8.7068e-01 2.7098e-01 8 2.7711e+01 3.041 + 3 3.6174e+07 7.6526e-01 2.2357e-01 4 1.3893e+02 4.186 + 4 3.6285e+07 2.8923e-01 3.4064e-01 0 1.2670e-09g 7.162 ------------------------------------------------------------------------------ Robust optimal solution identified. ------------------------------------------------------------------------------ Timing breakdown: - + Identifier ncalls cumtime percall % ----------------------------------------------------------- - main 1 11.457 11.457 100.0 + main 1 7.163 7.163 100.0 ------------------------------------------------------ - dr_polishing 4 0.682 0.171 6.0 - global_separation 47 1.109 0.024 9.7 - local_separation 235 5.810 0.025 50.7 - master 5 1.353 0.271 11.8 - master_feasibility 4 0.247 0.062 2.2 - preprocessing 1 0.429 0.429 3.7 - other n/a 1.828 n/a 16.0 + dr_polishing 4 0.293 0.073 4.1 + global_separation 27 1.106 0.041 15.4 + local_separation 135 3.385 0.025 47.3 + master 5 1.396 0.279 19.5 + master_feasibility 4 0.155 0.039 2.2 + preprocessing 1 0.018 0.018 0.2 + other n/a 0.811 n/a 11.3 ====================================================== =========================================================== - + ------------------------------------------------------------------------------ Termination stats: Iterations : 5 - Solve time (wall s) : 11.457 + Solve time (wall s) : 7.163 Final objective value : 3.6285e+07 Termination condition : pyrosTerminationCondition.robust_optimal ------------------------------------------------------------------------------ @@ -1059,10 +1067,10 @@ The constituent columns are defined in the there are no second-stage variables, or the master problem of the current iteration is not solved successfully. * - #CViol - - Number of performance constraints found to be violated during + - Number of second-stage inequality constraints found to be violated during the separation step of the current iteration. - Unless a custom prioritization of the model's performance constraints - is specified (through the ``separation_priority_order`` argument), + Unless a custom prioritization of the model's second-stage inequality + constraints is specified (through the ``separation_priority_order`` argument), expect this number to trend downward as the iteration number increases. A "+" is appended if not all of the separation problems were solved successfully, either due to custom prioritization, a time out, @@ -1070,13 +1078,13 @@ The constituent columns are defined in the A dash ("-") is produced in lieu of a value if the separation routine is not invoked during the current iteration. * - Max Viol - - Maximum scaled performance constraint violation. + - Maximum scaled second-stage inequality constraint violation. Expect this value to trend downward as the iteration number increases. A 'g' is appended to the value if the separation problems were solved globally during the current iteration. A dash ("-") is produced in lieu of a value if the separation routine is not invoked during the current iteration, or if there are - no performance constraints. + no second-stage inequality constraints. * - Wall time (s) - Total time elapsed by the solver, in seconds, up to the end of the current iteration. From 33ff4d8a593f1921465c80432a7b1ac3b5ec3d5f Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 10 Aug 2024 21:59:40 -0400 Subject: [PATCH 2170/3044] Add note on 'adj.' abbreviation in docs --- doc/OnlineDocs/contributed_packages/pyros.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index 8329d88c66c..0c40eb456b4 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -879,11 +879,12 @@ Observe that the log contains the following information: and equations. The preprocessor may find that some second-stage variables and state variables are mathematically - not adjustable to the uncertain parameters; - to this end, in the logs, the numbers of + not adjustable to the uncertain parameters. + To this end, in the logs, the numbers of adjustable second-stage variables and state variables are included in parentheses, next to the total numbers - of second-stage variables and state variables, respectively. + of second-stage variables and state variables, respectively; + note that 'adjustable' has been abbreviated as 'adj.'. * **Iteration log table** (lines 59--69). Summary information on the problem iterates and subproblem outcomes. The constituent columns are defined in detail in From 3bb1d9d5f3fe015b557eebb1ea6e3369c1235673 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 10 Aug 2024 22:02:44 -0400 Subject: [PATCH 2171/3044] Apply black --- pyomo/contrib/pyros/master_problem_methods.py | 86 ++-- pyomo/contrib/pyros/pyros.py | 21 +- .../contrib/pyros/pyros_algorithm_methods.py | 35 +- .../pyros/separation_problem_methods.py | 130 +++--- pyomo/contrib/pyros/solve_data.py | 4 +- pyomo/contrib/pyros/tests/test_grcs.py | 19 +- pyomo/contrib/pyros/tests/test_master.py | 115 +++-- .../contrib/pyros/tests/test_preprocessor.py | 431 ++++++++---------- pyomo/contrib/pyros/tests/test_separation.py | 59 +-- .../pyros/tests/test_uncertainty_sets.py | 285 +++++------- pyomo/contrib/pyros/uncertainty_sets.py | 28 +- pyomo/contrib/pyros/util.py | 243 ++++------ 12 files changed, 592 insertions(+), 864 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index bd3b4825f2f..eb51afdf08a 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -18,13 +18,7 @@ from pyomo.common.collections import ComponentMap, ComponentSet from pyomo.common.modeling import unique_component_name from pyomo.core import TransformationFactory -from pyomo.core.base import ( - ConcreteModel, - Block, - Var, - Objective, - Constraint, -) +from pyomo.core.base import ConcreteModel, Block, Var, Objective, Constraint from pyomo.core.base.set_types import NonNegativeIntegers, NonNegativeReals from pyomo.core.expr import identify_variables, value from pyomo.core.util import prod @@ -84,19 +78,19 @@ def construct_initial_master_problem(model_data, config): # model rather than to the model to prevent # duplication across scenario sub-blocks master_model.epigraph_obj = Objective( - expr=master_model.scenarios[0, 0].first_stage.epigraph_var, + expr=master_model.scenarios[0, 0].first_stage.epigraph_var ) return master_model def add_scenario_block_to_master_problem( - master_model, - scenario_idx, - param_realization, - from_block, - clone_first_stage_components, - ): + master_model, + scenario_idx, + param_realization, + from_block, + clone_first_stage_components, +): """ Add new scenario block to the master model. @@ -140,9 +134,7 @@ def add_scenario_block_to_master_problem( master_model.scenarios[scenario_idx].transfer_attributes_from(new_block) # update uncertain parameter values in new block - new_uncertain_params = ( - master_model.scenarios[scenario_idx].uncertain_params - ) + new_uncertain_params = master_model.scenarios[scenario_idx].uncertain_params for param, val in zip(new_uncertain_params, param_realization): param.set_value(val) @@ -218,13 +210,12 @@ def construct_master_feasibility_problem(master_data, config): # add slack variables and objective # inequalities g(v) <= b become g(v) - s^- <= b TransformationFactory("core.add_slack_variables").apply_to( - slack_model, - targets=targets, + slack_model, targets=targets ) slack_vars = ComponentSet( - slack_model - ._core_add_slack_variables - .component_data_objects(Var, descend_into=True) + slack_model._core_add_slack_variables.component_data_objects( + Var, descend_into=True + ) ) # initialize slack variables @@ -417,10 +408,7 @@ def construct_dr_polishing_problem(master_data, config): indexed_polishing_var = Var( list(indexed_dr_var.keys()), domain=NonNegativeReals ) - polishing_model.add_component( - f"dr_polishing_var_{idx}", - indexed_polishing_var, - ) + polishing_model.add_component(f"dr_polishing_var_{idx}", indexed_polishing_var) polishing_vars.append(indexed_polishing_var) # we need the DR expressions to set up the @@ -431,10 +419,7 @@ def construct_dr_polishing_problem(master_data, config): for ss_var in nominal_eff_var_partitioning.second_stage_variables ] - dr_eq_var_zip = zip( - polishing_vars, - eff_ss_var_to_dr_expr_pairs, - ) + dr_eq_var_zip = zip(polishing_vars, eff_ss_var_to_dr_expr_pairs) polishing_model.polishing_abs_val_lb_cons = all_lb_cons = [] polishing_model.polishing_abs_val_ub_cons = all_ub_cons = [] for idx, (indexed_polishing_var, (ss_var, dr_expr)) in enumerate(dr_eq_var_zip): @@ -444,12 +429,10 @@ def construct_dr_polishing_problem(master_data, config): # add indexed constraints to polishing model polishing_model.add_component( - f"polishing_abs_val_lb_con_{idx}", - polishing_absolute_value_lb_cons, + f"polishing_abs_val_lb_con_{idx}", polishing_absolute_value_lb_cons ) polishing_model.add_component( - f"polishing_abs_val_ub_con_{idx}", - polishing_absolute_value_ub_cons, + f"polishing_abs_val_ub_con_{idx}", polishing_absolute_value_ub_cons ) # update list of absolute value (i.e., polishing) cons @@ -481,14 +464,14 @@ def construct_dr_polishing_problem(master_data, config): # across all master blocks dr_term_copies = [ ( - scenario_blk.second_stage.decision_rule_eqns[idx] - .body.args[dr_var_in_term_idx] + scenario_blk.second_stage.decision_rule_eqns[idx].body.args[ + dr_var_in_term_idx + ] ) for scenario_blk in master_model.scenarios.values() ] all_copy_coeffs_zero = is_a_nonstatic_dr_term and all( - abs(value(prod(term.args[:-1]))) <= 1e-10 - for term in dr_term_copies + abs(value(prod(term.args[:-1]))) <= 1e-10 for term in dr_term_copies ) if all_copy_coeffs_zero: # increment static DR variable value @@ -611,9 +594,9 @@ def minimize_dr_vars(master_data, config): # update master problem variable values for idx, blk in master_data.master_model.scenarios.items(): master_adjustable_vars = blk.all_adjustable_variables - polishing_adjustable_vars = ( - polishing_model.scenarios[idx].all_adjustable_variables - ) + polishing_adjustable_vars = polishing_model.scenarios[ + idx + ].all_adjustable_variables adjustable_vars_zip = zip(master_adjustable_vars, polishing_adjustable_vars) for master_var, polish_var in adjustable_vars_zip: master_var.set_value(value(polish_var)) @@ -698,9 +681,7 @@ def log_master_solve_results(master_model, config, results, desc="Optimized"): if config.objective_focus == ObjectiveType.worst_case: eval_obj_blk_idx = max( master_model.scenarios.keys(), - key=lambda idx: value( - master_model.scenarios[idx].second_stage_objective - ), + key=lambda idx: value(master_model.scenarios[idx].second_stage_objective), ) else: eval_obj_blk_idx = (0, 0) @@ -810,9 +791,7 @@ def solver_call_master(master_data, config, master_soln): None, pyrosTerminationCondition.time_out, ) - master_soln.pyros_termination_condition = ( - pyrosTerminationCondition.time_out - ) + master_soln.pyros_termination_condition = pyrosTerminationCondition.time_out if not try_backup: if infeasible: @@ -895,18 +874,14 @@ def solve_master(master_data, config): setattr(master_soln.results.solver, TIC_TOC_SOLVE_TIME_ATTR, 0) # PyROS time out status - master_soln.pyros_termination_condition = ( - pyrosTerminationCondition.time_out - ) + master_soln.pyros_termination_condition = pyrosTerminationCondition.time_out master_soln.master_subsolver_results = ( None, pyrosTerminationCondition.time_out, ) return master_soln - solver_call_master( - master_data=master_data, config=config, master_soln=master_soln - ) + solver_call_master(master_data=master_data, config=config, master_soln=master_soln) return master_soln @@ -915,10 +890,9 @@ class MasterProblemData: """ Container for objects pertaining to the PyROS master problem. """ - def __init__(self, model_data, config): - """Initialize self (see docstring). - """ + def __init__(self, model_data, config): + """Initialize self (see docstring).""" self.master_model = construct_initial_master_problem(model_data, config) # we track the original model name for serialization purposes self.original_model_name = model_data.original_model.name diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index db2714266ae..616191e66b8 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -357,8 +357,7 @@ def solve( self._log_disclaimer(logger=progress_logger, level=logging.INFO) config, user_var_partitioning = self._resolve_and_validate_pyros_args( - model, - **kwds, + model, **kwds ) self._log_config( logger=config.progress_logger, @@ -370,9 +369,7 @@ def solve( config.progress_logger.info("Preprocessing...") model_data.timing.start_timer("main.preprocessing") robust_infeasible = preprocess_model_data( - model_data, - config, - user_var_partitioning, + model_data, config, user_var_partitioning ) model_data.timing.stop_timer("main.preprocessing") preprocessing_time = model_data.timing.get_total_time("main.preprocessing") @@ -389,13 +386,10 @@ def solve( pyros_soln = ROSolver_iterative_solve(model_data, config) IterationLogRecord.log_header_rule(config.progress_logger.info) - termination_acceptable = ( - pyros_soln.pyros_termination_condition - in { - pyrosTerminationCondition.robust_optimal, - pyrosTerminationCondition.robust_feasible - } - ) + termination_acceptable = pyros_soln.pyros_termination_condition in { + pyrosTerminationCondition.robust_optimal, + pyrosTerminationCondition.robust_feasible, + } if termination_acceptable: load_final_solution( model_data=model_data, @@ -407,8 +401,7 @@ def solve( # get the most recent master objective, if available return_soln.final_objective_value = None master_epigraph_obj_value = value( - pyros_soln.master_results.master_model.epigraph_obj, - exception=False, + pyros_soln.master_results.master_model.epigraph_obj, exception=False ) if master_epigraph_obj_value is not None: # account for sense of the original model objective diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 49e31ef8383..913e81267e7 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -48,13 +48,14 @@ class GRCSResults: iterations : int Number of iterations required. """ + def __init__( - self, - master_results, - separation_results, - pyros_termination_condition, - iterations, - ): + self, + master_results, + separation_results, + pyros_termination_condition, + iterations, + ): self.master_results = master_results self.separation_results = separation_results self.pyros_termination_condition = pyros_termination_condition @@ -66,8 +67,7 @@ def _evaluate_shift(current, prev, initial, norm=None): return None else: normalizers = np.max( - np.vstack((np.ones(initial.size), np.abs(initial))), - axis=0, + np.vstack((np.ones(initial.size), np.abs(initial))), axis=0 ) return np.max(np.abs(current - prev) / normalizers) @@ -91,8 +91,7 @@ def get_variable_value_data(working_blk, dr_var_to_monomial_map): ) dr_term_data = ComponentMap( (dr_var, value(monomial)) - for dr_var, monomial - in get_dr_var_to_monomial_map(working_blk).items() + for dr_var, monomial in get_dr_var_to_monomial_map(working_blk).items() ) return VariableValueData( @@ -171,14 +170,11 @@ def ROSolver_iterative_solve(model_data, config): # check master solve status # to determine whether to terminate here - master_termination_not_acceptable = ( - master_soln.pyros_termination_condition - in { - pyrosTerminationCondition.robust_infeasible, - pyrosTerminationCondition.time_out, - pyrosTerminationCondition.subsolver_error, - } - ) + master_termination_not_acceptable = master_soln.pyros_termination_condition in { + pyrosTerminationCondition.robust_infeasible, + pyrosTerminationCondition.time_out, + pyrosTerminationCondition.subsolver_error, + } if master_termination_not_acceptable: iter_log_record = IterationLogRecord( iteration=k, @@ -212,8 +208,7 @@ def ROSolver_iterative_solve(model_data, config): # track variable values current_iter_var_data = get_variable_value_data( - nominal_master_blk, - dr_var_monomial_map, + nominal_master_blk, dr_var_monomial_map ) if k == 0: first_iter_var_data = current_iter_var_data diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index ac2a0be72ea..ca21ff24a9c 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -20,19 +20,9 @@ from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.common.dependencies import numpy as np -from pyomo.core.base import ( - Block, - Constraint, - maximize, - Objective, - value, - Var, -) +from pyomo.core.base import Block, Constraint, maximize, Objective, value, Var from pyomo.opt import TerminationCondition as tc -from pyomo.core.expr import ( - replace_expressions, - identify_mutable_parameters, -) +from pyomo.core.expr import replace_expressions, identify_mutable_parameters from pyomo.contrib.pyros.solve_data import ( DiscreteSeparationSolveCallResults, @@ -67,11 +57,8 @@ def add_uncertainty_set_constraints(separation_model, config): }, ) indexed_param_var = separation_model.uncertainty.uncertain_param_indexed_var - uncertainty_quantification = ( - config.uncertainty_set.set_as_constraint( - uncertain_params=indexed_param_var, - block=separation_model.uncertainty, - ) + uncertainty_quantification = config.uncertainty_set.set_as_constraint( + uncertain_params=indexed_param_var, block=separation_model.uncertainty ) # facilitate retrieval later @@ -81,15 +68,11 @@ def add_uncertainty_set_constraints(separation_model, config): separation_model.uncertainty.uncertainty_cons_list = uncertainty_cons config.uncertainty_set._add_bounds_on_uncertain_parameters( - uncertain_param_vars=param_var_list, - global_solver=config.global_solver, + uncertain_param_vars=param_var_list, global_solver=config.global_solver ) if aux_vars: - aux_var_vals = ( - config.uncertainty_set.compute_auxiliary_uncertain_param_vals( - point=config.nominal_uncertain_param_vals, - solver=config.global_solver, - ) + aux_var_vals = config.uncertainty_set.compute_auxiliary_uncertain_param_vals( + point=config.nominal_uncertain_param_vals, solver=config.global_solver ) for auxvar, auxval in zip(aux_vars, aux_var_vals): auxvar.set_value(auxval) @@ -144,8 +127,7 @@ def construct_separation_problem(model_data, config): uncertain_params = separation_model.uncertain_params uncertain_param_vars = separation_model.uncertainty.uncertain_param_var_list param_id_to_var_map = { - id(param): var - for param, var in zip(uncertain_params, uncertain_param_vars) + id(param): var for param, var in zip(uncertain_params, uncertain_param_vars) } uncertain_params_set = ComponentSet(uncertain_params) adjustable_cons = ( @@ -154,9 +136,10 @@ def construct_separation_problem(model_data, config): + list(separation_model.second_stage.decision_rule_eqns.values()) ) for adjcon in adjustable_cons: - uncertain_params_in_con = ComponentSet( - identify_mutable_parameters(adjcon.expr) - ) & uncertain_params_set + uncertain_params_in_con = ( + ComponentSet(identify_mutable_parameters(adjcon.expr)) + & uncertain_params_set + ) if uncertain_params_in_con: adjcon.set_value( replace_expressions(adjcon.expr, substitution_map=param_id_to_var_map) @@ -168,11 +151,10 @@ def construct_separation_problem(model_data, config): ss_ineq_cons = separation_model.second_stage.inequality_cons.values() for idx, ss_ineq_con in enumerate(ss_ineq_cons): ss_ineq_con.deactivate() - separation_obj = Objective(expr=ss_ineq_con.body - ss_ineq_con.upper, sense=maximize) - separation_model.add_component( - f"separation_obj_{idx}", - separation_obj, + separation_obj = Objective( + expr=ss_ineq_con.body - ss_ineq_con.upper, sense=maximize ) + separation_model.add_component(f"separation_obj_{idx}", separation_obj) separation_model.second_stage_ineq_con_to_obj_map[ss_ineq_con] = separation_obj separation_obj.deactivate() @@ -354,11 +336,7 @@ def solve_separation_problem(separation_data, master_data, config): ) -def evaluate_violations_by_nominal_master( - separation_data, - master_data, - ss_ineq_cons, - ): +def evaluate_violations_by_nominal_master(separation_data, master_data, ss_ineq_cons): """ Evaluate violation of second-stage inequality constraints by variables in nominal block of most recent master @@ -424,10 +402,7 @@ def group_ss_ineq_constraints_by_priority(separation_data, config): def get_worst_discrete_separation_solution( - ss_ineq_con, - config, - ss_ineq_cons_to_evaluate, - discrete_solve_results, + ss_ineq_con, config, ss_ineq_cons_to_evaluate, discrete_solve_results ): """ Determine separation solution (and therefore worst-case @@ -492,8 +467,7 @@ def get_worst_discrete_separation_solution( if is_optimized_ss_ineq_con: results_list = [ res - for solve_call_results - in discrete_solve_results.solver_call_results.values() + for solve_call_results in discrete_solve_results.solver_call_results.values() for res in solve_call_results.results_list ] else: @@ -622,8 +596,7 @@ def perform_separation_loop(separation_data, master_data, config, solve_globally single_solver_call_res = ComponentMap() results_list = [ res - for solve_call_results - in discrete_sep_results.solver_call_results.values() + for solve_call_results in discrete_sep_results.solver_call_results.values() for res in solve_call_results.results_list ] single_solver_call_res[ss_ineq_con_to_maximize] = ( @@ -808,7 +781,7 @@ def evaluate_ss_ineq_con_violations( violations_by_sep_solution = get_sep_objective_values( separation_data=separation_data, config=config, - ss_ineq_cons=ss_ineq_cons_to_evaluate + ss_ineq_cons=ss_ineq_cons_to_evaluate, ) # normalize constraint violation: i.e. divide by @@ -830,7 +803,9 @@ def evaluate_ss_ineq_con_violations( return (violating_param_realization, scaled_violations, constraint_violated) -def initialize_separation(ss_ineq_con_to_maximize, separation_data, master_data, config): +def initialize_separation( + ss_ineq_con_to_maximize, separation_data, master_data, config +): """ Initialize separation problem variables using the solution to the most recent master problem. @@ -864,8 +839,8 @@ def eval_master_violation(scenario_idx): Evaluate violation of `ss_ineq_con` by variables of specified master block. """ - master_con = ( - master_model.scenarios[scenario_idx].find_component(ss_ineq_con_to_maximize) + master_con = master_model.scenarios[scenario_idx].find_component( + ss_ineq_con_to_maximize ) return value(master_con) @@ -873,8 +848,7 @@ def eval_master_violation(scenario_idx): # second-stage ineq constraint of interest. Gives the best known # feasible solution (for case of non-discrete uncertainty sets). worst_master_block_idx = max( - master_model.scenarios.keys(), - key=eval_master_violation, + master_model.scenarios.keys(), key=eval_master_violation ) worst_case_master_blk = master_model.scenarios[worst_master_block_idx] for sep_var in sep_model.all_variables: @@ -885,9 +859,7 @@ def eval_master_violation(scenario_idx): # have already been addressed if config.uncertainty_set.geometry != Geometry.DISCRETE_SCENARIOS: param_vars = sep_model.uncertainty.uncertain_param_var_list - param_values = separation_data.points_added_to_master[ - worst_master_block_idx - ] + param_values = separation_data.points_added_to_master[worst_master_block_idx] for param_var, val in zip(param_vars, param_values): param_var.set_value(val) @@ -908,9 +880,7 @@ def eval_master_violation(scenario_idx): # variables later tol = ABS_CON_CHECK_FEAS_TOL ss_ineq_con_name_repr = get_con_name_repr( - separation_model=sep_model, - con=ss_ineq_con_to_maximize, - with_obj_name=True, + separation_model=sep_model, con=ss_ineq_con_to_maximize, with_obj_name=True ) uncertainty_set_is_discrete = ( config.uncertainty_set.geometry is Geometry.DISCRETE_SCENARIOS @@ -919,9 +889,7 @@ def eval_master_violation(scenario_idx): lslack, uslack = con.lslack(), con.uslack() if (lslack < -tol or uslack < -tol) and not uncertainty_set_is_discrete: con_name_repr = get_con_name_repr( - separation_model=sep_model, - con=con, - with_obj_name=False, + separation_model=sep_model, con=con, with_obj_name=False ) config.progress_logger.debug( f"Initial point for separation of second-stage ineq constraint " @@ -936,8 +904,12 @@ def eval_master_violation(scenario_idx): def solver_call_separation( - separation_data, master_data, config, - solve_globally, ss_ineq_con_to_maximize, ss_ineq_cons_to_evaluate + separation_data, + master_data, + config, + solve_globally, + ss_ineq_con_to_maximize, + ss_ineq_cons_to_evaluate, ): """ Invoke subordinate solver(s) on separation problem. @@ -1050,7 +1022,10 @@ def solver_call_separation( solve_call_results.scaled_violations, solve_call_results.found_violation, ) = evaluate_ss_ineq_con_violations( - separation_data, config, ss_ineq_con_to_maximize, ss_ineq_cons_to_evaluate + separation_data, + config, + ss_ineq_con_to_maximize, + ss_ineq_cons_to_evaluate, ) solve_call_results.auxiliary_param_values = [ auxvar.value @@ -1115,8 +1090,12 @@ def solver_call_separation( def discrete_solve( - separation_data, master_data, config, - solve_globally, ss_ineq_con_to_maximize, ss_ineq_cons_to_evaluate + separation_data, + master_data, + config, + solve_globally, + ss_ineq_con_to_maximize, + ss_ineq_cons_to_evaluate, ): """ Obtain separation problem solution for each scenario @@ -1214,21 +1193,22 @@ class SeparationProblemData: """ Container for objects related to the PyROS separation problem. """ - def __init__(self, model_data, config): - """Initialize self (see class docstring). - """ + def __init__(self, model_data, config): + """Initialize self (see class docstring).""" self.separation_model = construct_separation_problem(model_data, config) self.timing = model_data.timing self.iteration = 0 self.config = config self.points_added_to_master = {(0, 0): config.nominal_uncertain_param_vals} - self.auxiliary_values_for_master_points = {(0, 0): [ - # auxiliary variable values for nominal point have already - # been computed and loaded into separation model - aux_var.value - for aux_var in self.separation_model.uncertainty.auxiliary_var_list - ]} + self.auxiliary_values_for_master_points = { + (0, 0): [ + # auxiliary variable values for nominal point have already + # been computed and loaded into separation model + aux_var.value + for aux_var in self.separation_model.uncertainty.auxiliary_var_list + ] + } if config.uncertainty_set.geometry == Geometry.DISCRETE_SCENARIOS: self.idxs_of_master_scenarios = [ diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index 9fae2eb32ef..2c41c1ea579 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -414,7 +414,9 @@ def scaled_violations(self): then None is returned. """ if self.worst_case_ss_ineq_con is not None: - return self.solver_call_results[self.worst_case_ss_ineq_con].scaled_violations + return self.solver_call_results[ + self.worst_case_ss_ineq_con + ].scaled_violations else: return None diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index ecb66f82622..2841910ece5 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -207,10 +207,7 @@ def test_two_stg_mod_with_factor_model_set(self): # Define the uncertainty set # we take the parameter `u2` to be 'fixed' fset = FactorModelSet( - origin=[1.125, 1], - beta=1, - number_of_factors=1, - psi_mat=[[0.5], [0.5]], + origin=[1.125, 1], beta=1, number_of_factors=1, psi_mat=[[0.5], [0.5]] ) # Instantiate the PyROS solver @@ -312,6 +309,7 @@ class TestPyROSSolveDiscreteSet(unittest.TestCase): """ Test PyROS solves models with discrete uncertainty sets. """ + @unittest.skipUnless( baron_license_is_valid, "Global NLP solver is not available and licensed." ) @@ -1539,7 +1537,7 @@ def create_mitsos_4_3(self): ) @unittest.skipIf( (24, 1, 5) <= baron_version and baron_version <= (24, 5, 8), - f"Test expected to fail for BARON version {baron_version}" + f"Test expected to fail for BARON version {baron_version}", ) def test_coeff_matching_solver_insensitive(self): """ @@ -1745,8 +1743,7 @@ def test_coefficient_matching_nonlinear_expr(self): pyros_log = LOG.getvalue() self.assertRegex( - pyros_log, - r".*Equality constraint '.*eq_con.*'.*cannot be written.*", + pyros_log, r".*Equality constraint '.*eq_con.*'.*cannot be written.*" ) # should still solve in spite of coefficient matching @@ -3063,10 +3060,7 @@ def test_pyros_vars_not_in_model(self): # now perform checks with LoggingIntercept(level=logging.ERROR) as LOG: - exc_str = ( - "Found Vars.*active.*" - "not descended from.*model.*" - ) + exc_str = "Found Vars.*active.*" "not descended from.*model.*" with self.assertRaisesRegex(ValueError, exc_str): pyros.solve( model=mdl, @@ -3086,8 +3080,7 @@ def test_pyros_vars_not_in_model(self): msg="Number of lines referencing name of invalid Vars not as expected.", ) self.assertRegex( - text=invalid_vars_strs_list[0], - expected_regex=f"{mdl2.x2.name!r}", + text=invalid_vars_strs_list[0], expected_regex=f"{mdl2.x2.name!r}" ) def test_pyros_non_continuous_vars(self): diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index 5eed6b1352f..f23a4aec2dd 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -21,14 +21,7 @@ from pyomo.common.collections import Bunch from pyomo.common.dependencies import numpy_available, scipy_available -from pyomo.core.base import ( - ConcreteModel, - Constraint, - minimize, - Objective, - Param, - Var, -) +from pyomo.core.base import ConcreteModel, Constraint, minimize, Objective, Param, Var from pyomo.core.expr import exp from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.environ import SolverFactory @@ -71,9 +64,7 @@ def build_simple_model_data(objective_focus="worst_case"): m.x1 = Var(bounds=[-1000, 1000], initialize=1) m.x2 = Var(bounds=[-1000, 1000], initialize=1) m.x3 = Var(bounds=[-1000, 1000], initialize=-3) - m.con = Constraint( - expr=exp(m.u - 1) - m.x1 - m.x2 * m.u - m.x3 * m.u**2 <= 0, - ) + m.con = Constraint(expr=exp(m.u - 1) - m.x1 - m.x2 * m.u - m.x3 * m.u**2 <= 0) m.eq_con = Constraint(expr=m.x2 - 1 == 0) m.obj = Objective(expr=m.x1 + m.x2 / 2 + m.x3 / 3) @@ -291,7 +282,7 @@ def test_construct_master_feasibility_problem_slack_vars(self): assertExpressionsEqual( self, slack_user_model_x3_lb_con.body <= slack_user_model_x3_lb_con.upper, - -scenario_10_blk.user_model.x3 - slack_user_model_x3_lb_con_var <= 1000.0 + -scenario_10_blk.user_model.x3 - slack_user_model_x3_lb_con_var <= 1000.0, ) self.assertEqual(slack_user_model_x3_lb_con_var.value, 0) @@ -305,7 +296,7 @@ def test_construct_master_feasibility_problem_slack_vars(self): assertExpressionsEqual( self, slack_user_model_x3_ub_con.body <= slack_user_model_x3_ub_con.upper, - scenario_10_blk.user_model.x3 - slack_user_model_x3_ub_con_var <= 1000.0 + scenario_10_blk.user_model.x3 - slack_user_model_x3_ub_con_var <= 1000.0, ) self.assertEqual(slack_user_model_x3_lb_con_var.value, 0) @@ -328,15 +319,14 @@ def test_construct_master_feasibility_problem_obj(self): slack_model = construct_master_feasibility_problem(master_data, config) self.assertFalse(slack_model.epigraph_obj.active) - self.assertTrue( - slack_model._core_add_slack_variables._slack_objective.active - ) + self.assertTrue(slack_model._core_add_slack_variables._slack_objective.active) class TestDRPolishingProblem(unittest.TestCase): """ Tests for the PyROS DR polishing problem. """ + def build_simple_master_data(self): """ Construct master data-like object for feasibility problem @@ -362,12 +352,9 @@ def test_construct_dr_polishing_problem_nonadj_components(self): """ master_data, config = self.build_simple_master_data() polishing_model = construct_dr_polishing_problem(master_data, config) - eff_first_stage_vars = ( - polishing_model - .scenarios[0, 0] - .effective_var_partitioning - .first_stage_variables - ) + eff_first_stage_vars = polishing_model.scenarios[ + 0, 0 + ].effective_var_partitioning.first_stage_variables for effective_first_stage_var in eff_first_stage_vars: self.assertTrue( effective_first_stage_var.fixed, @@ -390,8 +377,9 @@ def test_construct_dr_polishing_problem_nonadj_components(self): # so they should remain active # self.assertTrue(nom_polishing_block.user_model.con.active) self.assertTrue( - nom_polishing_block - .second_stage.inequality_cons["ineq_con_con_upper_bound_con"].active + nom_polishing_block.second_stage.inequality_cons[ + "ineq_con_con_upper_bound_con" + ].active ) self.assertTrue(nom_polishing_block.second_stage.decision_rule_eqns[0].active) @@ -403,9 +391,9 @@ def test_construct_dr_polishing_problem_polishing_components(self): master_data, config = self.build_simple_master_data() # DR order is 1, and x3 is second-stage. # to test fixing efficiency, fix the affine DR variable - decision_rule_vars = ( - master_data.master_model.scenarios[0, 0].first_stage.decision_rule_vars - ) + decision_rule_vars = master_data.master_model.scenarios[ + 0, 0 + ].first_stage.decision_rule_vars decision_rule_vars[0][1].fix() polishing_model = construct_dr_polishing_problem(master_data, config) nom_polishing_block = polishing_model.scenarios[0, 0] @@ -484,18 +472,21 @@ class TestSolveMaster(unittest.TestCase): """ Test method for solving master problem """ + @unittest.skipUnless(baron_available, "Global NLP solver is not available.") def test_solve_master(self): model_data, config = build_simple_model_data() model_data.timing = TimingData() baron = SolverFactory("baron") - config.update(dict( - local_solver=baron, - global_solver=baron, - backup_local_solvers=[], - backup_global_solvers=[], - tee=False, - )) + config.update( + dict( + local_solver=baron, + global_solver=baron, + backup_local_solvers=[], + backup_global_solvers=[], + tee=False, + ) + ) master_data = MasterProblemData(model_data, config) with time_code(master_data.timing, "main", is_main_timer=True): master_soln = master_data.solve_master() @@ -517,14 +508,16 @@ def test_solve_master_timeout_on_master(self): model_data, config = build_simple_model_data() model_data.timing = TimingData() baron = SolverFactory("baron") - config.update(dict( - local_solver=baron, - global_solver=baron, - backup_local_solvers=[], - backup_global_solvers=[], - tee=False, - time_limit=1, - )) + config.update( + dict( + local_solver=baron, + global_solver=baron, + backup_local_solvers=[], + backup_global_solvers=[], + tee=False, + time_limit=1, + ) + ) master_data = MasterProblemData(model_data, config) with time_code(master_data.timing, "main", is_main_timer=True): time.sleep(1) @@ -551,14 +544,16 @@ def test_solve_master_timeout_on_master_feasibility(self): model_data, config = build_simple_model_data() model_data.timing = TimingData() baron = SolverFactory("baron") - config.update(dict( - local_solver=baron, - global_solver=baron, - backup_local_solvers=[], - backup_global_solvers=[], - tee=False, - time_limit=1, - )) + config.update( + dict( + local_solver=baron, + global_solver=baron, + backup_local_solvers=[], + backup_global_solvers=[], + tee=False, + time_limit=1, + ) + ) master_data = MasterProblemData(model_data, config) add_scenario_block_to_master_problem( master_data.master_model, @@ -585,6 +580,7 @@ class TestPolishDRVars(unittest.TestCase): """ Test DR polishing subroutine. """ + @unittest.skipUnless( baron_license_is_valid, "Global NLP solver is not available and licensed." ) @@ -592,13 +588,15 @@ def test_polish_dr_vars(self): model_data, config = build_simple_model_data() model_data.timing = TimingData() baron = SolverFactory("baron") - config.update(dict( - local_solver=baron, - global_solver=baron, - backup_local_solvers=[], - backup_global_solvers=[], - tee=False, - )) + config.update( + dict( + local_solver=baron, + global_solver=baron, + backup_local_solvers=[], + backup_global_solvers=[], + tee=False, + ) + ) master_data = MasterProblemData(model_data, config) add_scenario_block_to_master_problem( master_data.master_model, @@ -613,8 +611,7 @@ def test_polish_dr_vars(self): with time_code(master_data.timing, "main", is_main_timer=True): master_soln = master_data.solve_master() self.assertEqual( - master_soln.termination_condition, - TerminationCondition.optimal, + master_soln.termination_condition, TerminationCondition.optimal ) results, success = master_data.solve_dr_polishing() diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index 673ab3509d6..e3f28ecd313 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -34,17 +34,8 @@ maximize, Block, ) -from pyomo.core.base.set_types import ( - NonNegativeReals, - NonPositiveReals, - Reals, -) -from pyomo.core.expr import ( - log, - sin, - exp, - RangedExpression, -) +from pyomo.core.base.set_types import NonNegativeReals, NonPositiveReals, Reals +from pyomo.core.expr import log, sin, exp, RangedExpression from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.contrib.pyros.util import ( @@ -66,6 +57,7 @@ preprocess_model_data, log_model_statistics, ) + parameterized, param_available = attempt_import('parameterized') if not (numpy_available and scipy_available and param_available): @@ -98,15 +90,11 @@ def build_simple_test_model_data(self): m.c0 = Constraint(expr=m.q + m.x1 + m.z == 0) m.c1 = Constraint(expr=(0, m.x1 - m.z, 0)) - m.c2 = Constraint(expr=m.x1 ** 2 - m.z + m.y[1] == 0) - m.c2_dupl = Constraint(expr=m.x1 ** 2 - m.z + m.y[1] == 0) - m.c3 = Constraint(expr=m.x1 ** 3 + m.y[1] + 2 * m.y[2] == 0) - m.c4 = Constraint( - expr=m.x2 ** 2 + m.y[1] + m.y[2] + m.y[3] + m.y[4] == 0 - ) - m.c5 = Constraint( - expr=m.x2 + 2 * m.y[2] + m.y[3] + 2 * m.y[4] == 0 - ) + m.c2 = Constraint(expr=m.x1**2 - m.z + m.y[1] == 0) + m.c2_dupl = Constraint(expr=m.x1**2 - m.z + m.y[1] == 0) + m.c3 = Constraint(expr=m.x1**3 + m.y[1] + 2 * m.y[2] == 0) + m.c4 = Constraint(expr=m.x2**2 + m.y[1] + m.y[2] + m.y[3] + m.y[4] == 0) + m.c5 = Constraint(expr=m.x2 + 2 * m.y[2] + m.y[3] + 2 * m.y[4] == 0) model_data = Bunch() model_data.working_model = ConcreteModel() @@ -140,8 +128,7 @@ def test_effective_partitioning_system(self): for dr_order in [0, 1, 2]: config.decision_rule_order = dr_order actual_partitioning = get_effective_var_partitioning( - model_data=model_data, - config=config, + model_data=model_data, config=config ) for vartype, expected_vars in expected_partitioning.items(): actual_vars = getattr(actual_partitioning, vartype) @@ -160,8 +147,8 @@ def test_effective_partitioning_system(self): # linear coefficient below tolerance; # that should prevent pretriangularization - m.c2.set_value(m.x1 ** 2 + m.z + 1e-10 * m.y[1] == 0) - m.c2_dupl.set_value(m.x1 ** 2 + m.z + 1e-10 * m.y[1] == 0) + m.c2.set_value(m.x1**2 + m.z + 1e-10 * m.y[1] == 0) + m.c2_dupl.set_value(m.x1**2 + m.z + 1e-10 * m.y[1] == 0) expected_partitioning = { "first_stage_variables": [m.x1, m.x2, m.z], "second_stage_variables": [], @@ -170,8 +157,7 @@ def test_effective_partitioning_system(self): for dr_order in [0, 1, 2]: config.decision_rule_order = dr_order actual_partitioning = get_effective_var_partitioning( - model_data=model_data, - config=config, + model_data=model_data, config=config ) for vartype, expected_vars in expected_partitioning.items(): actual_vars = getattr(actual_partitioning, vartype) @@ -190,8 +176,8 @@ def test_effective_partitioning_system(self): # put linear coefs above tolerance again: # original behavior expected - m.c2.set_value(1e-6 * m.y[1] + m.x1 ** 2 + m.z + 1e-10 * m.y[1] == 0) - m.c2_dupl.set_value(1e-6 * m.y[1] + m.x1 ** 2 + m.z + 1e-10 * m.y[1] == 0) + m.c2.set_value(1e-6 * m.y[1] + m.x1**2 + m.z + 1e-10 * m.y[1] == 0) + m.c2_dupl.set_value(1e-6 * m.y[1] + m.x1**2 + m.z + 1e-10 * m.y[1] == 0) expected_partitioning = { "first_stage_variables": [m.x1, m.x2, m.z, m.y[1], m.y[2]], "second_stage_variables": [], @@ -200,8 +186,7 @@ def test_effective_partitioning_system(self): for dr_order in [0, 1, 2]: config.decision_rule_order = dr_order actual_partitioning = get_effective_var_partitioning( - model_data=model_data, - config=config, + model_data=model_data, config=config ) for vartype, expected_vars in expected_partitioning.items(): actual_vars = getattr(actual_partitioning, vartype) @@ -225,12 +210,11 @@ def test_effective_partitioning_system(self): "second_stage_variables": [], "state_variables": [m.y[2], m.y[3], m.y[4]], } - m.c3.set_value(m.x1 ** 3 + m.y[1] + 2 * m.y[1] * m.y[2] == 0) + m.c3.set_value(m.x1**3 + m.y[1] + 2 * m.y[1] * m.y[2] == 0) for dr_order in [0, 1, 2]: config.decision_rule_order = dr_order actual_partitioning = get_effective_var_partitioning( - model_data=model_data, - config=config, + model_data=model_data, config=config ) for vartype, expected_vars in expected_partitioning.items(): actual_vars = getattr(actual_partitioning, vartype) @@ -257,8 +241,7 @@ def test_effective_partitioning_system(self): for dr_order in [0, 1, 2]: config.decision_rule_order = dr_order actual_partitioning = get_effective_var_partitioning( - model_data=model_data, - config=config, + model_data=model_data, config=config ) for vartype, expected_vars in expected_partitioning.items(): actual_vars = getattr(actual_partitioning, vartype) @@ -285,7 +268,7 @@ def test_effective_partitioning_modified_linear_system(self): # now the second-stage variable can't be determined uniquely; # can't pretriangularize this unless z already known to be # nonadjustable - m.c1.set_value((0, m.x1 + m.z ** 2, 0)) + m.c1.set_value((0, m.x1 + m.z**2, 0)) config = Bunch() config.decision_rule_order = 0 @@ -297,8 +280,7 @@ def test_effective_partitioning_modified_linear_system(self): "state_variables": [m.y[3], m.y[4]], } actual_partitioning_static_dr = get_effective_var_partitioning( - model_data=model_data, - config=config, + model_data=model_data, config=config ) for vartype, expected_vars in expected_partitioning_static_dr.items(): actual_vars = getattr(actual_partitioning_static_dr, vartype) @@ -323,8 +305,7 @@ def test_effective_partitioning_modified_linear_system(self): } for dr_order in [1, 2]: actual_partitioning_nonstatic_dr = get_effective_var_partitioning( - model_data=model_data, - config=config, + model_data=model_data, config=config ) for vartype, expected_vars in expected_partitioning_nonstatic_dr.items(): actual_vars = getattr(actual_partitioning_nonstatic_dr, vartype) @@ -346,6 +327,7 @@ class TestSetupModelData(unittest.TestCase): """ Test method for setting up the working model works as expected. """ + def build_test_model_data(self): """ Build model data object for the preprocessor. @@ -363,7 +345,7 @@ def build_test_model_data(self): # second-stage variables m.z1 = Var(domain=RangeSet(2, 4, 0), bounds=[-m.p, m.q], initialize=2) - m.z2 = Var(bounds=(-2 * m.q ** 2, None), initialize=1) + m.z2 = Var(bounds=(-2 * m.q**2, None), initialize=1) m.z3 = Var(bounds=(-m.q, 0), initialize=0) m.z4 = Var(initialize=5) m.z5 = Var(domain=NonNegativeReals, bounds=(m.q, m.q)) @@ -382,14 +364,14 @@ def build_test_model_data(self): # EQUALITY CONSTRAINTS m.eq1 = Constraint(expr=m.q * (m.z3 + m.x2) == 0) m.eq2 = Constraint(expr=m.x1 - m.z1 == 0) - m.eq3 = Constraint(expr=m.x1 ** 2 + m.x2 + m.p * m.z2 == m.p) + m.eq3 = Constraint(expr=m.x1**2 + m.x2 + m.p * m.z2 == m.p) m.eq4 = Constraint(expr=m.z3 + m.y1 == m.q) # INEQUALITY CONSTRAINTS m.ineq1 = Constraint(expr=(-m.p, m.x1 + m.z1, exp(m.q))) m.ineq2 = Constraint(expr=(0, m.x1 + m.x2, 10)) m.ineq3 = Constraint(expr=(2 * m.q, 2 * (m.z3 + m.y1), 2 * m.q)) - m.ineq4 = Constraint(expr=-m.q <= m.y2 ** 2 + log(m.y2)) + m.ineq4 = Constraint(expr=-m.q <= m.y2**2 + log(m.y2)) # out of scope: deactivated m.ineq5 = Constraint(expr=m.y3 <= m.q) @@ -399,15 +381,15 @@ def build_test_model_data(self): # contains a rich combination of first-stage and second-stage terms m.obj = Objective( expr=( - m.p ** 2 + m.p**2 + 2 * m.p * m.q + log(m.x1) + 2 * m.p * m.x1 - + m.q ** 2 * m.x1 - + m.p ** 3 * (m.z1 + m.z2 + m.y1) + + m.q**2 * m.x1 + + m.p**3 * (m.z1 + m.z2 + m.y1) + m.z4 + m.z5 - ), + ) ) # set up the var partitioning @@ -448,28 +430,22 @@ def test_setup_working_model(self): # user var partitioning up = working_model.user_var_partitioning self.assertEqual( - ComponentSet(up.first_stage_variables), - ComponentSet([m.x1, m.x2]), + ComponentSet(up.first_stage_variables), ComponentSet([m.x1, m.x2]) ) self.assertEqual( ComponentSet(up.second_stage_variables), ComponentSet([m.z1, m.z2, m.z3, m.z4, m.z5]), ) - self.assertEqual( - ComponentSet(up.state_variables), - ComponentSet([m.y1, m.y2]), - ) + self.assertEqual(ComponentSet(up.state_variables), ComponentSet([m.y1, m.y2])) # uncertain params self.assertEqual( - ComponentSet(working_model.uncertain_params), - ComponentSet([m.q]), + ComponentSet(working_model.uncertain_params), ComponentSet([m.q]) ) # ensure original model unchanged self.assertFalse( - hasattr(om, "util"), - msg="Original model still has temporary util block", + hasattr(om, "util"), msg="Original model still has temporary util block" ) # constraint partitioning initialization @@ -483,6 +459,7 @@ class TestResolveVarBounds(unittest.TestCase): """ Tests for resolution of variable bounds. """ + def test_resolve_var_bounds(self): """ Test resolve variable bounds. @@ -504,10 +481,12 @@ def test_resolve_var_bounds(self): m.z10 = Var(domain=RangeSet(0, 5, 0), bounds=[m.q1, m.p2]) # useful for checking domains later - original_var_domains = ComponentMap(( - (var, var.domain) for var in - (m.z1, m.z2, m.z3, m.z4, m.z5, m.z6, m.z7, m.z8, m.z9, m.z10) - )) + original_var_domains = ComponentMap( + ( + (var, var.domain) + for var in (m.z1, m.z2, m.z3, m.z4, m.z5, m.z6, m.z7, m.z8, m.z9, m.z10) + ) + ) expected_bounds = ( (m.z1, (0, None, 1), (None, None, None)), @@ -575,7 +554,7 @@ def test_resolve_var_bounds(self): f"from {orig_domain} to {var.domain} " "by the bounds resolution method " f"{get_var_certain_uncertain_bounds.__name__!r}." - ) + ), ) @@ -636,9 +615,7 @@ def test_turn_nonadjustable_bounds_to_constraints(self): # mock effective partitioning for testing ep = model_data.working_model.effective_var_partitioning = Bunch() - ep.first_stage_variables = [ - m.z1, m.z2, m.z3, m.z4, m.z5, m.z6, m.z7, m.z8 - ] + ep.first_stage_variables = [m.z1, m.z2, m.z3, m.z4, m.z5, m.z6, m.z7, m.z8] ep.second_stage_variables = [m.z9] ep.state_variables = [m.z10] effective_first_stage_var_set = ComponentSet(ep.first_stage_variables) @@ -649,16 +626,18 @@ def test_turn_nonadjustable_bounds_to_constraints(self): ) # expected final bounds and bound constraint types - expected_final_nonadj_var_bounds = ComponentMap(( - (m.z1, (get_var_bound_pairs(m.z1)[1], [])), - (m.z2, (get_var_bound_pairs(m.z2)[1], [])), - (m.z3, (get_var_bound_pairs(m.z3)[1], [])), - (m.z4, ((None, 0), ["lower"])), - (m.z5, ((4, None), ["upper"])), - (m.z6, ((None, None), ["eq"])), - (m.z7, ((None, None), ["eq"])), - (m.z8, ((None, None), ["lower", "upper"])), - )) + expected_final_nonadj_var_bounds = ComponentMap( + ( + (m.z1, (get_var_bound_pairs(m.z1)[1], [])), + (m.z2, (get_var_bound_pairs(m.z2)[1], [])), + (m.z3, (get_var_bound_pairs(m.z3)[1], [])), + (m.z4, ((None, 0), ["lower"])), + (m.z5, ((4, None), ["upper"])), + (m.z6, ((None, None), ["eq"])), + (m.z7, ((None, None), ["eq"])), + (m.z8, ((None, None), ["lower", "upper"])), + ) + ) turn_nonadjustable_var_bounds_to_constraints(model_data) @@ -737,7 +716,7 @@ def test_turn_nonadjustable_bounds_to_constraints(self): assertExpressionsEqual( self, second_stage.equality_cons["var_z6_uncertain_eq_bound_con"].expr, - m.z6 == m.q1 + m.z6 == m.q1, ) assertExpressionsEqual( self, @@ -759,12 +738,12 @@ def test_turn_nonadjustable_bounds_to_constraints(self): self.assertEqual( len(working_model.second_stage.inequality_cons), 4, - msg="Number of second-stage inequalities not as expected." + msg="Number of second-stage inequalities not as expected.", ) self.assertEqual( len(working_model.second_stage.equality_cons), 2, - msg="Number of second-stage equalities not as expected." + msg="Number of second-stage equalities not as expected.", ) def test_turn_adjustable_bounds_to_constraints(self): @@ -888,7 +867,7 @@ def test_turn_adjustable_bounds_to_constraints(self): assertExpressionsEqual( self, second_stage.inequality_cons["var_z4_uncertain_lower_bound_con"].expr, - - m.z4 <= -m.q1, + -m.z4 <= -m.q1, ) assertExpressionsEqual( self, @@ -933,7 +912,7 @@ def test_turn_adjustable_bounds_to_constraints(self): assertExpressionsEqual( self, second_stage.inequality_cons["var_z8_uncertain_lower_bound_con"].expr, - - m.z8 <= -m.q1, + -m.z8 <= -m.q1, ) assertExpressionsEqual( self, @@ -975,11 +954,11 @@ def build_simple_test_model_data(self): m.c5 = Constraint(expr=(m.q, m.x2, 2 * m.q)) m.c6 = Constraint(expr=m.z1 <= 1) m.c7 = Constraint(expr=(0, m.z2, 1)) - m.c8 = Constraint(expr=(m.p ** 0.5, m.y1, m.p)) + m.c8 = Constraint(expr=(m.p**0.5, m.y1, m.p)) m.c9 = Constraint(expr=m.y1 - m.q <= 0) - m.c10 = Constraint(expr=m.y1 <= m.q ** 2) + m.c10 = Constraint(expr=m.y1 <= m.q**2) m.c11 = Constraint(expr=m.z2 <= m.q) - m.c12 = Constraint(expr=(m.q ** 2, m.x1, sin(m.p))) + m.c12 = Constraint(expr=(m.q**2, m.x1, sin(m.p))) m.c11.deactivate() @@ -991,7 +970,17 @@ def build_simple_test_model_data(self): model_data.working_model.second_stage.inequality_cons = Constraint(Any) model_data.working_model.original_active_inequality_cons = [ - m.c1, m.c2, m.c3, m.c4, m.c5, m.c6, m.c7, m.c8, m.c9, m.c10, m.c12, + m.c1, + m.c2, + m.c3, + m.c4, + m.c5, + m.c6, + m.c7, + m.c8, + m.c9, + m.c10, + m.c12, ] ep = model_data.working_model.effective_var_partitioning = Bunch() @@ -1035,7 +1024,7 @@ def test_standardize_inequality_constraints(self): self.assertFalse(m.c3.active) new_c3_con = ss_ineq_cons["ineq_con_c3_lower_bound_con"] self.assertTrue(new_c3_con.active) - assertExpressionsEqual(self, new_c3_con.expr, - m.x1 <= -m.q) + assertExpressionsEqual(self, new_c3_con.expr, -m.x1 <= -m.q) # log(m.p) <= m.x2 <= m.q # lower bound is first-stage, upper bound second-stage @@ -1054,7 +1043,7 @@ def test_standardize_inequality_constraints(self): new_c5_upper_bound_con = ss_ineq_cons["ineq_con_c5_upper_bound_con"] self.assertTrue(new_c5_lower_bound_con.active) self.assertTrue(new_c5_lower_bound_con.active) - assertExpressionsEqual(self, new_c5_lower_bound_con.expr, - m.x2 <= -m.q) + assertExpressionsEqual(self, new_c5_lower_bound_con.expr, -m.x2 <= -m.q) assertExpressionsEqual(self, new_c5_upper_bound_con.expr, m.x2 <= 2 * m.q) # single second-stage inequality @@ -1079,7 +1068,7 @@ def test_standardize_inequality_constraints(self): new_c8_upper_bound_con = ss_ineq_cons["ineq_con_c8_upper_bound_con"] self.assertTrue(new_c8_lower_bound_con.active) self.assertTrue(new_c8_upper_bound_con.active) - assertExpressionsEqual(self, new_c8_lower_bound_con.expr, - m.y1 <= -m.p ** 0.5) + assertExpressionsEqual(self, new_c8_lower_bound_con.expr, -m.y1 <= -m.p**0.5) assertExpressionsEqual(self, new_c8_upper_bound_con.expr, m.y1 <= m.p) # m.y1 - m.q <= 0 @@ -1092,11 +1081,9 @@ def test_standardize_inequality_constraints(self): # m.y1 <= m.q ** 2 # single second-stage inequality self.assertFalse(m.c10.active) - new_c10_upper_bound_con = ( - ss_ineq_cons["ineq_con_c10_upper_bound_con"] - ) + new_c10_upper_bound_con = ss_ineq_cons["ineq_con_c10_upper_bound_con"] self.assertTrue(new_c10_upper_bound_con.active) - assertExpressionsEqual(self, new_c10_upper_bound_con.expr, m.y1 <= m.q ** 2) + assertExpressionsEqual(self, new_c10_upper_bound_con.expr, m.y1 <= m.q**2) # originally deactivated; # no modification @@ -1105,13 +1092,11 @@ def test_standardize_inequality_constraints(self): # lower bound second-stage; upper bound first-stage self.assertFalse(m.c12.active) - new_c12_lower_bound_con = ( - ss_ineq_cons["ineq_con_c12_lower_bound_con"] - ) + new_c12_lower_bound_con = ss_ineq_cons["ineq_con_c12_lower_bound_con"] new_c12_upper_bound_con = fs_ineq_cons["ineq_con_c12_upper_bound_con"] self.assertTrue(new_c12_lower_bound_con.active) self.assertTrue(new_c12_upper_bound_con.active) - assertExpressionsEqual(self, new_c12_lower_bound_con.expr, - m.x1 <= -m.q ** 2) + assertExpressionsEqual(self, new_c12_lower_bound_con.expr, -m.x1 <= -m.q**2) assertExpressionsEqual(self, new_c12_upper_bound_con.expr, m.x1 <= sin(m.p)) def test_standardize_inequality_error(self): @@ -1160,7 +1145,7 @@ def build_simple_test_model_data(self): # second-stage equalities m.eq3 = Constraint(expr=m.x2 * m.q == 1) - m.eq4 = Constraint(expr=m.x2 - m.z1 ** 2 == 0) + m.eq4 = Constraint(expr=m.x2 - m.z1**2 == 0) m.eq5 = Constraint(expr=m.q == m.y1) m.eq6 = Constraint(expr=(m.q, m.y1, m.q)) m.eq7 = Constraint(expr=m.z2 == 0) @@ -1176,7 +1161,12 @@ def build_simple_test_model_data(self): model_data.working_model.second_stage.equality_cons = Constraint(Any) model_data.working_model.original_active_equality_cons = [ - m.eq1, m.eq2, m.eq3, m.eq4, m.eq5, m.eq6, + m.eq1, + m.eq2, + m.eq3, + m.eq4, + m.eq5, + m.eq6, ] ep = model_data.working_model.effective_var_partitioning = Bunch() @@ -1222,7 +1212,7 @@ def test_standardize_equality_constraints(self): self.assertFalse(m.eq4.active) new_eq4_con = second_stage_eq_cons["eq_con_eq4"] self.assertTrue(new_eq4_con) - assertExpressionsEqual(self, new_eq4_con.expr, m.x2 - m.z1 ** 2 == 0) + assertExpressionsEqual(self, new_eq4_con.expr, m.x2 - m.z1**2 == 0) self.assertFalse(m.eq5.active) new_eq5_con = second_stage_eq_cons["eq_con_eq5"] @@ -1233,7 +1223,7 @@ def test_standardize_equality_constraints(self): new_eq6_con = second_stage_eq_cons["eq_con_eq6"] self.assertTrue(new_eq6_con.active) assertExpressionsEqual( - self, new_eq6_con.expr, RangedExpression((m.q, m.y1, m.q), False), + self, new_eq6_con.expr, RangedExpression((m.q, m.y1, m.q), False) ) # excluded from the list of active constraints; @@ -1265,12 +1255,10 @@ def build_simple_test_model_data(self): m.obj1 = Objective( expr=( - 10 + m.p + m.q + m.p * m.x + m.z * m.p + m.y ** 2 * m.q + m.y + log(m.x) - ), - ) - m.obj2 = Objective( - expr=m.p + m.x * m.z + m.z ** 2, + 10 + m.p + m.q + m.p * m.x + m.z * m.p + m.y**2 * m.q + m.y + log(m.x) + ) ) + m.obj2 = Objective(expr=m.p + m.x * m.z + m.z**2) model_data.working_model.uncertain_params = [m.q] @@ -1309,13 +1297,9 @@ def test_declare_objective_expressions(self): assertExpressionsEqual( self, working_model.second_stage_objective.expr, - m.q + m.z * m.p + m.y ** 2 * m.q + m.y, - ) - assertExpressionsEqual( - self, - working_model.full_objective.expr, - m.obj1.expr, + m.q + m.z * m.p + m.y**2 * m.q + m.y, ) + assertExpressionsEqual(self, working_model.full_objective.expr, m.obj1.expr) def test_declare_objective_expressions_maximization_obj(self): """ @@ -1336,13 +1320,9 @@ def test_declare_objective_expressions_maximization_obj(self): assertExpressionsEqual( self, working_model.second_stage_objective.expr, - -m.q - m.z * m.p - m.y ** 2 * m.q - m.y, - ) - assertExpressionsEqual( - self, - working_model.full_objective.expr, - -m.obj1.expr, + -m.q - m.z * m.p - m.y**2 * m.q - m.y, ) + assertExpressionsEqual(self, working_model.full_objective.expr, -m.obj1.expr) def test_standardize_active_obj_worst_case_focus(self): """ @@ -1549,7 +1529,7 @@ def test_correct_num_dr_vars_static(self): f"Second-stage var {ess_var.name!r} " f"is mapped to DR var {mapped_dr_var.name!r}, " f"but expected mapping to DR var {indexed_dr_var.name!r}." - ) + ), ) def test_correct_num_dr_vars_affine(self): @@ -1601,7 +1581,7 @@ def test_correct_num_dr_vars_affine(self): f"Second-stage var {ess_var.name!r} " f"is mapped to DR var {mapped_dr_var.name!r}, " f"but expected mapping to DR var {indexed_dr_var.name!r}." - ) + ), ) def test_correct_num_dr_vars_quadratic(self): @@ -1621,8 +1601,7 @@ def test_correct_num_dr_vars_quadratic(self): for indexed_dr_var in model_data.working_model.first_stage.decision_rule_vars: self.assertEqual( len(indexed_dr_var), - 1 # static term - + num_params # affine terms + 1 + num_params # static term # affine terms # quadratic terms + sp.special.comb(num_params, 2, repetition=True, exact=True), msg=( @@ -1658,7 +1637,7 @@ def test_correct_num_dr_vars_quadratic(self): f"Second-stage var {ess_var.name!r} " f"is mapped to DR var {mapped_dr_var.name!r}, " f"but expected mapping to DR var {indexed_dr_var.name!r}." - ) + ), ) @@ -1731,7 +1710,7 @@ def test_num_dr_eqns_added_correct(self): msg=( "Number of decision rule equations should match number of " "effective second-stage variables." - ) + ), ) # check second-stage var to DR equation mapping is as expected @@ -1748,7 +1727,7 @@ def test_num_dr_eqns_added_correct(self): f"Second-stage var {ess_var.name!r} " f"is mapped to DR equation {mapped_dr_eqn.name!r}, " f"but expected mapping to DR equation {dr_eqn.name!r}." - ) + ), ) self.assertTrue(mapped_dr_eqn.active) @@ -1799,18 +1778,20 @@ def test_dr_eqns_form_correct(self): ) assertExpressionsEqual(self, dr_eq.expr, expected_dr_eq_expression) - expected_dr_var_to_exponent_map = ComponentMap(( - (indexed_dr_var[0], 0), - (indexed_dr_var[1], 1), - (indexed_dr_var[2], 1), - (indexed_dr_var[3], 1), - (indexed_dr_var[4], 2), - (indexed_dr_var[5], 2), - (indexed_dr_var[6], 2), - (indexed_dr_var[7], 2), - (indexed_dr_var[8], 2), - (indexed_dr_var[9], 2), - )) + expected_dr_var_to_exponent_map = ComponentMap( + ( + (indexed_dr_var[0], 0), + (indexed_dr_var[1], 1), + (indexed_dr_var[2], 1), + (indexed_dr_var[3], 1), + (indexed_dr_var[4], 2), + (indexed_dr_var[5], 2), + (indexed_dr_var[6], 2), + (indexed_dr_var[7], 2), + (indexed_dr_var[8], 2), + (indexed_dr_var[9], 2), + ) + ) self.assertEqual( working_model.dr_var_to_exponent_map, expected_dr_var_to_exponent_map, @@ -1823,6 +1804,7 @@ class TestReformulateStateVarIndependentEqCons(unittest.TestCase): Unit tests for routine that reformulates state variable-independent second-stage equality constraints. """ + def setup_test_model_data(self): """ Set up simple test model for testing the reformulation @@ -1838,10 +1820,8 @@ def setup_test_model_data(self): m.con = Constraint(expr=m.u ** (0.5) * m.x1 - m.u * m.x2 <= 2) m.obj = Objective(expr=(m.x1 - 4) ** 2 + (m.x2 - 1) ** 2) m.eq_con = Constraint( - expr=m.u**2 * (m.x2 - 1) - + m.u * (m.x1**3 + 0.5) - - 5 * m.u * m.x1 * m.x2 - == - m.u * (m.x1 + 2) + expr=m.u**2 * (m.x2 - 1) + m.u * (m.x1**3 + 0.5) - 5 * m.u * m.x1 * m.x2 + == -m.u * (m.x1 + 2) ) # mathematically redundant, but makes the tests more rigorous @@ -1914,7 +1894,7 @@ def test_coefficient_matching_correct_constraints_added(self): self.assertEqual( len(first_stage_eq_cons), 3, - msg="Number of coefficient matching constraints not as expected." + msg="Number of coefficient matching constraints not as expected.", ) self.assertEqual(len(model_data.working_model.second_stage.equality_cons), 0) # we originally declared an inequality constraint on the model @@ -1923,7 +1903,7 @@ def test_coefficient_matching_correct_constraints_added(self): assertExpressionsEqual( self, first_stage_eq_cons["coeff_matching_eq_con_coeff_1"].expr, - 2.5 + m.x1 + (-5) * (m.x1 * m.x2) + m.x1 ** 3 == 0, + 2.5 + m.x1 + (-5) * (m.x1 * m.x2) + m.x1**3 == 0, ) assertExpressionsEqual( self, @@ -1975,9 +1955,7 @@ def test_reformulate_nonlinear_state_var_independent_eq_con(self): err_msg = LOG.getvalue() self.assertRegex( text=err_msg, - expected_regex=( - r".*Equality constraint '.*eq_con.*'.*cannot be written.*" - ), + expected_regex=(r".*Equality constraint '.*eq_con.*'.*cannot be written.*"), ) self.assertFalse( @@ -2010,7 +1988,8 @@ def test_reformulate_nonlinear_state_var_independent_eq_con(self): + m.u * (m.x1**3 + 0.5) - ((5 * m.u * m.x1) * m.x2) - (-m.u) * (m.x1 + 2) - ) <= 0.0, + ) + <= 0.0, ) assertExpressionsEqual( self, @@ -2071,7 +2050,7 @@ def test_coefficient_matching_robust_infeasible_proof(self): expected_regex=( r"PyROS has determined that the model is robust infeasible\. " r"One reason for this.*equality constraint '.*eq_con.*'.*" - ) + ), ) @@ -2079,6 +2058,7 @@ class TestPreprocessModelData(unittest.TestCase): """ Test the PyROS preprocessor. """ + def build_test_model_data(self): """ Build model data object for the preprocessor. @@ -2096,7 +2076,7 @@ def build_test_model_data(self): # second-stage variables m.z1 = Var(domain=RangeSet(2, 4, 0), bounds=[-m.p, m.q], initialize=2) - m.z2 = Var(bounds=(-2 * m.q ** 2, None), initialize=1) + m.z2 = Var(bounds=(-2 * m.q**2, None), initialize=1) m.z3 = Var(bounds=(-m.q, 0), initialize=0) m.z4 = Var(initialize=5) # the bounds produce an equality constraint @@ -2123,7 +2103,7 @@ def build_test_model_data(self): # this makes z1 nonadjustable m.eq2 = Constraint(expr=m.x1 - m.z1 == 0) # pretriangular: makes z2 nonadjustable, so first-stage - m.eq3 = Constraint(expr=m.x1 ** 2 + m.x2 + m.p * m.z2 == m.p) + m.eq3 = Constraint(expr=m.x1**2 + m.x2 + m.p * m.z2 == m.p) # second-stage equality m.eq4 = Constraint(expr=m.z3 + m.y1 == m.q) @@ -2142,7 +2122,7 @@ def build_test_model_data(self): m.ineq3 = Constraint(expr=(2 * m.q, 2 * (m.z3 + m.y1), 2 * m.q)) # second-stage inequality. trivially satisfied/infeasible, # since y2 is fixed - m.ineq4 = Constraint(expr=-m.q <= m.y2 ** 2 + log(m.y2)) + m.ineq4 = Constraint(expr=-m.q <= m.y2**2 + log(m.y2)) # out of scope: deactivated m.ineq5 = Constraint(expr=m.y3 <= m.q) @@ -2152,15 +2132,15 @@ def build_test_model_data(self): # contains a rich combination of first-stage and second-stage terms m.obj = Objective( expr=( - m.p ** 2 + m.p**2 + 2 * m.p * m.q + log(m.x1) + 2 * m.p * m.x1 - + m.q ** 2 * m.x1 - + m.p ** 3 * (m.z1 + m.z2 + m.y1) + + m.q**2 * m.x1 + + m.p**3 * (m.z1 + m.z2 + m.y1) + m.z4 + m.z5 - ), + ) ) # set up the var partitioning @@ -2187,19 +2167,25 @@ def test_preprocessor_effective_var_partitioning_static_dr(self): decision_rule_order=0, progress_logger=logger, ) - preprocess_model_data( - model_data, config, user_var_partitioning, - ) + preprocess_model_data(model_data, config, user_var_partitioning) ep = model_data.working_model.effective_var_partitioning ublk = model_data.working_model.user_model self.assertEqual( ComponentSet(ep.first_stage_variables), - ComponentSet([ - # all second-stage variables are nonadjustable - # due to the DR - ublk.x1, ublk.x2, ublk.z1, ublk.z2, - ublk.z3, ublk.z4, ublk.z5, ublk.y2, - ]), + ComponentSet( + [ + # all second-stage variables are nonadjustable + # due to the DR + ublk.x1, + ublk.x2, + ublk.z1, + ublk.z2, + ublk.z3, + ublk.z4, + ublk.z5, + ublk.y2, + ] + ), ) self.assertEqual(ep.second_stage_variables, []) self.assertEqual(ep.state_variables, [ublk.y1]) @@ -2208,10 +2194,7 @@ def test_preprocessor_effective_var_partitioning_static_dr(self): self.assertEqual( ComponentSet(working_model.all_nonadjustable_variables), ComponentSet( - [ - ublk.x1, ublk.x2, ublk.z1, ublk.z2, - ublk.z3, ublk.z4, ublk.z5, ublk.y2, - ] + [ublk.x1, ublk.x2, ublk.z1, ublk.z2, ublk.z3, ublk.z4, ublk.z5, ublk.y2] + [working_model.first_stage.epigraph_var] ), ) @@ -2233,10 +2216,7 @@ def test_preprocessor_effective_var_partitioning_static_dr(self): ), ) - @parameterized.expand([ - ["affine", 1], - ["quadratic", 2], - ]) + @parameterized.expand([["affine", 1], ["quadratic", 2]]) def test_preprocessor_effective_var_partitioning_nonstatic_dr(self, name, dr_order): """ Test preprocessor repartitions the variables @@ -2250,9 +2230,7 @@ def test_preprocessor_effective_var_partitioning_nonstatic_dr(self, name, dr_ord decision_rule_order=dr_order, progress_logger=logger, ) - preprocess_model_data( - model_data, config, user_var_partitioning, - ) + preprocess_model_data(model_data, config, user_var_partitioning) ep = model_data.working_model.effective_var_partitioning ublk = model_data.working_model.user_model self.assertEqual( @@ -2260,13 +2238,9 @@ def test_preprocessor_effective_var_partitioning_nonstatic_dr(self, name, dr_ord ComponentSet([ublk.x1, ublk.x2, ublk.z1, ublk.z2, ublk.z4, ublk.y2]), ) self.assertEqual( - ComponentSet(ep.second_stage_variables), - ComponentSet([ublk.z3, ublk.z5]), - ) - self.assertEqual( - ComponentSet(ep.state_variables), - ComponentSet([ublk.y1]), + ComponentSet(ep.second_stage_variables), ComponentSet([ublk.z3, ublk.z5]) ) + self.assertEqual(ComponentSet(ep.state_variables), ComponentSet([ublk.y1])) working_model = model_data.working_model self.assertEqual( ComponentSet(working_model.all_nonadjustable_variables), @@ -2297,17 +2271,19 @@ def test_preprocessor_effective_var_partitioning_nonstatic_dr(self, name, dr_ord ), ) - @parameterized.expand([ - ["affine_nominal", 1, "nominal"], - ["affine_worst_case", 1, "worst_case"], - # eq1 doesn't get reformulated in coefficient matching - # as the polynomial degree is too high - ["quadratic_nominal", 2, "nominal"], - ["quadratic_worst_case", 2, "worst_case"], - ]) + @parameterized.expand( + [ + ["affine_nominal", 1, "nominal"], + ["affine_worst_case", 1, "worst_case"], + # eq1 doesn't get reformulated in coefficient matching + # as the polynomial degree is too high + ["quadratic_nominal", 2, "nominal"], + ["quadratic_worst_case", 2, "worst_case"], + ] + ) def test_preprocessor_constraint_partitioning_nonstatic_dr( - self, name, dr_order, obj_focus, - ): + self, name, dr_order, obj_focus + ): """ Test preprocessor partitions constraints as expected for nonstatic DR. @@ -2320,9 +2296,7 @@ def test_preprocessor_constraint_partitioning_nonstatic_dr( decision_rule_order=dr_order, progress_logger=logger, ) - preprocess_model_data( - model_data, config, user_var_partitioning, - ) + preprocess_model_data(model_data, config, user_var_partitioning) working_model = model_data.working_model ublk = working_model.user_model @@ -2349,7 +2323,7 @@ def test_preprocessor_constraint_partitioning_nonstatic_dr( ( ["ineq_con_ineq1_lower_bound_con", "ineq_con_ineq2"] + (["epigraph_con"] if obj_focus == "nominal" else []) - ) + ), ) self.assertEqual( list(working_model.first_stage.equality_cons), @@ -2381,7 +2355,8 @@ def test_preprocessor_constraint_partitioning_nonstatic_dr( "reform_lower_bound_from_eq_con_eq1", "reform_upper_bound_from_eq_con_eq1", ] - if dr_order == 2 else [] + if dr_order == 2 + else [] ) ), ) @@ -2421,7 +2396,7 @@ def test_preprocessor_constraint_partitioning_nonstatic_dr( assertExpressionsEqual( self, ss.inequality_cons["var_z2_uncertain_lower_bound_con"].expr, - -m.z2 <= -(-2 * m.q ** 2), + -m.z2 <= -(-2 * m.q**2), ) assertExpressionsEqual( self, @@ -2429,19 +2404,13 @@ def test_preprocessor_constraint_partitioning_nonstatic_dr( -m.z3 <= -(-m.q), ) assertExpressionsEqual( - self, - ss.inequality_cons["var_z3_certain_upper_bound_con"].expr, - m.z3 <= 0, + self, ss.inequality_cons["var_z3_certain_upper_bound_con"].expr, m.z3 <= 0 ) assertExpressionsEqual( - self, - ss.inequality_cons["var_z5_certain_lower_bound_con"].expr, - -m.z5 <= 0, + self, ss.inequality_cons["var_z5_certain_lower_bound_con"].expr, -m.z5 <= 0 ) assertExpressionsEqual( - self, - ss.inequality_cons["var_y1_certain_lower_bound_con"].expr, - -m.y1 <= 0, + self, ss.inequality_cons["var_y1_certain_lower_bound_con"].expr, -m.y1 <= 0 ) assertExpressionsEqual( self, @@ -2471,19 +2440,17 @@ def test_preprocessor_constraint_partitioning_nonstatic_dr( assertExpressionsEqual( self, ss.inequality_cons["ineq_con_ineq4_lower_bound_con"].expr, - -(m.y2 ** 2 + log(m.y2)) <= -(-m.q), + -(m.y2**2 + log(m.y2)) <= -(-m.q), ) self.assertFalse(m.ineq5.active) assertExpressionsEqual( - self, - fs.equality_cons["eq_con_eq2"].expr, - m.x1 - m.z1 == 0, + self, fs.equality_cons["eq_con_eq2"].expr, m.x1 - m.z1 == 0 ) assertExpressionsEqual( self, fs.equality_cons["eq_con_eq3"].expr, - m.x1 ** 2 + m.x2 + m.p * m.z2 == m.p, + m.x1**2 + m.x2 + m.p * m.z2 == m.p, ) if dr_order < 2: # due to coefficient matching, this should have been deleted @@ -2492,14 +2459,12 @@ def test_preprocessor_constraint_partitioning_nonstatic_dr( # user model block should have no active constraints self.assertFalse(list(m.component_data_objects(Constraint, active=True))) - @parameterized.expand([ - ["static", 0, True], - ["affine", 1, False], - ["quadratic", 2, False], - ]) + @parameterized.expand( + [["static", 0, True], ["affine", 1, False], ["quadratic", 2, False]] + ) def test_preprocessor_coefficient_matching( - self, name, dr_order, expected_robust_infeas, - ): + self, name, dr_order, expected_robust_infeas + ): """ Check preprocessor robust infeasibility return status. """ @@ -2516,7 +2481,7 @@ def test_preprocessor_coefficient_matching( # due to the coefficient matching constraints derived # from bounds on z5 robust_infeasible = preprocess_model_data( - model_data, config, user_var_partitioning, + model_data, config, user_var_partitioning ) self.assertIsInstance(robust_infeasible, bool) self.assertEqual(robust_infeasible, expected_robust_infeas) @@ -2579,11 +2544,7 @@ def test_preprocessor_coefficient_matching( fs.decision_rule_vars[1][2] == 0, ) - @parameterized.expand([ - ["static", 0], - ["affine", 1], - ["quadratic", 2], - ]) + @parameterized.expand([["static", 0], ["affine", 1], ["quadratic", 2]]) def test_preprocessor_objective_standardization(self, name, dr_order): """ Test preprocessor standardizes the active objective as @@ -2597,9 +2558,7 @@ def test_preprocessor_objective_standardization(self, name, dr_order): decision_rule_order=dr_order, progress_logger=logger, ) - preprocess_model_data( - model_data, config, user_var_partitioning, - ) + preprocess_model_data(model_data, config, user_var_partitioning) ublk = model_data.working_model.user_model working_model = model_data.working_model @@ -2607,13 +2566,9 @@ def test_preprocessor_objective_standardization(self, name, dr_order): assertExpressionsEqual( self, working_model.second_stage.inequality_cons["epigraph_con"].expr, - ublk.obj.expr - working_model.first_stage.epigraph_var <= 0 - ) - assertExpressionsEqual( - self, - working_model.full_objective.expr, - ublk.obj.expr, + ublk.obj.expr - working_model.first_stage.epigraph_var <= 0, ) + assertExpressionsEqual(self, working_model.full_objective.expr, ublk.obj.expr) # recall: objective summands are classified according # to dependence on uncertain parameters and variables @@ -2623,15 +2578,15 @@ def test_preprocessor_objective_standardization(self, name, dr_order): assertExpressionsEqual( self, working_model.first_stage_objective.expr, - ublk.p ** 2 + log(ublk.x1) + 2 * ublk.p * ublk.x1, + ublk.p**2 + log(ublk.x1) + 2 * ublk.p * ublk.x1, ) assertExpressionsEqual( self, working_model.second_stage_objective.expr, ( 2 * ublk.p * ublk.q - + ublk.q ** 2 * ublk.x1 - + ublk.p ** 3 * (ublk.z1 + ublk.z2 + ublk.y1) + + ublk.q**2 * ublk.x1 + + ublk.p**3 * (ublk.z1 + ublk.z2 + ublk.y1) + ublk.z4 + ublk.z5 ), @@ -2651,9 +2606,7 @@ def test_preprocessor_log_model_statistics_affine_dr(self, obj_focus): decision_rule_order=1, progress_logger=logger, ) - preprocess_model_data( - model_data, config, user_var_partitioning, - ) + preprocess_model_data(model_data, config, user_var_partitioning) # expected model stats worked out by hand expected_log_str = textwrap.dedent( @@ -2703,9 +2656,7 @@ def test_preprocessor_log_model_statistics_quadratic_dr(self, obj_focus): decision_rule_order=2, progress_logger=logger, ) - preprocess_model_data( - model_data, config, user_var_partitioning, - ) + preprocess_model_data(model_data, config, user_var_partitioning) # expected model stats worked out by hand expected_log_str = textwrap.dedent( diff --git a/pyomo/contrib/pyros/tests/test_separation.py b/pyomo/contrib/pyros/tests/test_separation.py index f5a3d053945..4eebf1df96f 100644 --- a/pyomo/contrib/pyros/tests/test_separation.py +++ b/pyomo/contrib/pyros/tests/test_separation.py @@ -19,13 +19,7 @@ from pyomo.common.collections import Bunch from pyomo.common.dependencies import numpy as np, numpy_available, scipy_available -from pyomo.core.base import ( - ConcreteModel, - Constraint, - Objective, - Param, - Var, -) +from pyomo.core.base import ConcreteModel, Constraint, Objective, Param, Var from pyomo.core.expr import exp, RangedExpression from pyomo.core.expr.compare import assertExpressionsEqual @@ -55,9 +49,7 @@ def build_simple_model_data(objective_focus="worst_case"): m.x1 = Var(bounds=[-1000, 1000]) m.x2 = Var(bounds=[-1000, 1000]) m.x3 = Var(bounds=[-1000, 1000]) - m.con = Constraint( - expr=exp(m.u - 1) - m.x1 - m.x2 * m.u - m.x3 * m.u**2 <= 0, - ) + m.con = Constraint(expr=exp(m.u - 1) - m.x1 - m.x2 * m.u - m.x3 * m.u**2 <= 0) # this makes x2 nonadjustable m.eq_con = Constraint(expr=m.x2 - 1 == 0) @@ -88,6 +80,7 @@ class TestConstructSeparationProblem(unittest.TestCase): """ Test method for construction of separation problem. """ + def test_construct_separation_problem_nonadj_components(self): """ Check first-stage variables and constraints of the @@ -109,7 +102,7 @@ def test_construct_separation_problem_nonadj_components(self): for coeff_con in separation_model.first_stage.coefficient_matching_cons: self.assertFalse( coeff_con.active, - msg=f"Coefficient mathcing constraint {coeff_con.name!r} active." + msg=f"Coefficient mathcing constraint {coeff_con.name!r} active.", ) def test_construct_separation_problem_ss_ineq_cons(self): @@ -130,8 +123,13 @@ def test_construct_separation_problem_ss_ineq_cons(self): self, separation_model.second_stage.inequality_cons["epigraph_con"].expr, ( - m.x1 + m.x2 / 2 + m.x3 / 3 + u1_var + u2_var - - separation_model.first_stage.epigraph_var <= 0 + m.x1 + + m.x2 / 2 + + m.x3 / 3 + + u1_var + + u2_var + - separation_model.first_stage.epigraph_var + <= 0 ), ) @@ -140,15 +138,21 @@ def test_construct_separation_problem_ss_ineq_cons(self): ) self.assertFalse( m.con.active, - separation_model.second_stage.inequality_cons["ineq_con_con_upper_bound_con"].active + separation_model.second_stage.inequality_cons[ + "ineq_con_con_upper_bound_con" + ].active, ) self.assertFalse( m.con.active, - separation_model.second_stage.inequality_cons["var_x3_certain_lower_bound_con"].active + separation_model.second_stage.inequality_cons[ + "var_x3_certain_lower_bound_con" + ].active, ) self.assertFalse( m.con.active, - separation_model.second_stage.inequality_cons["var_x3_certain_upper_bound_con"].active + separation_model.second_stage.inequality_cons[ + "var_x3_certain_upper_bound_con" + ].active, ) # check second-stage ineq con expressions match obj expressions @@ -158,11 +162,7 @@ def test_construct_separation_problem_ss_ineq_cons(self): len(separation_model.second_stage.inequality_cons), ) for ineq_con, obj in separation_model.second_stage_ineq_con_to_obj_map.items(): - assertExpressionsEqual( - self, - ineq_con.body - ineq_con.upper, - obj.expr, - ) + assertExpressionsEqual(self, ineq_con.body - ineq_con.upper, obj.expr) def test_construct_separation_problem_ss_eq_and_dr_cons(self): """ @@ -230,10 +230,7 @@ def test_construct_separation_problem_uncertain_factor_param_components(self): """ model_data, config = build_simple_model_data(objective_focus="worst_case") config.uncertainty_set = FactorModelSet( - origin=[1, 0], - beta=1, - number_of_factors=2, - psi_mat=[[1, 2.5], [0, 1]], + origin=[1, 0], beta=1, number_of_factors=2, psi_mat=[[1, 2.5], [0, 1]] ) separation_model = construct_separation_problem(model_data, config) uncertainty_blk = separation_model.uncertainty @@ -246,19 +243,13 @@ def test_construct_separation_problem_uncertain_factor_param_components(self): self.assertTrue(matrix_product_cons[1].active) self.assertTrue(aux_sum_con.active) assertExpressionsEqual( - self, - aux_sum_con.expr, - RangedExpression((-2, auxvar1 + auxvar2, 2), False), + self, aux_sum_con.expr, RangedExpression((-2, auxvar1 + auxvar2, 2), False) ) assertExpressionsEqual( - self, - matrix_product_cons[0].expr, - auxvar1 + 2.5 * auxvar2 + 1 == paramvar1, + self, matrix_product_cons[0].expr, auxvar1 + 2.5 * auxvar2 + 1 == paramvar1 ) assertExpressionsEqual( - self, - matrix_product_cons[1].expr, - 0.0 * auxvar1 + auxvar2 == paramvar2, + self, matrix_product_cons[1].expr, 0.0 * auxvar1 + auxvar2 == paramvar2 ) # none of the vars should be fixed diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 076cca4dea0..fc7bd8499f4 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -23,11 +23,7 @@ scipy_available, ) from pyomo.environ import SolverFactory -from pyomo.core.base import ( - ConcreteModel, - Param, - Var, -) +from pyomo.core.base import ConcreteModel, Param, Var from pyomo.core.expr import RangedExpression from pyomo.core.expr.compare import assertExpressionsEqual @@ -286,14 +282,10 @@ def test_set_as_constraint(self): var1, var2 = uq.uncertain_param_vars assertExpressionsEqual( - self, - con1.expr, - RangedExpression((np.int64(1), var1, np.int64(2)), False), + self, con1.expr, RangedExpression((np.int64(1), var1, np.int64(2)), False) ) assertExpressionsEqual( - self, - con2.expr, - RangedExpression((np.int64(3), var2, np.int64(4)), False), + self, con2.expr, RangedExpression((np.int64(3), var2, np.int64(4)), False) ) def test_set_as_constraint_dim_mismatch(self): @@ -342,12 +334,12 @@ def test_point_in_set(self): for point in in_set_points: self.assertTrue( box_set.point_in_set(point), - msg=f"Point {point} should not be in uncertainty set {box_set}." + msg=f"Point {point} should not be in uncertainty set {box_set}.", ) for point in out_of_set_points: self.assertFalse( box_set.point_in_set(point), - msg=f"Point {point} should not be in uncertainty set {box_set}." + msg=f"Point {point} should not be in uncertainty set {box_set}.", ) # check what happens if dimensions are off @@ -360,8 +352,7 @@ def test_add_bounds_on_uncertain_parameters(self): box_set = BoxSet(bounds=[(1, 2), (3, 4)]) box_set._add_bounds_on_uncertain_parameters( - global_solver=None, - uncertain_param_vars=m.uncertain_param_vars, + global_solver=None, uncertain_param_vars=m.uncertain_param_vars ) self.assertEqual(m.uncertain_param_vars[0].bounds, (1, 2)) self.assertEqual(m.uncertain_param_vars[1].bounds, (3, 4)) @@ -549,8 +540,7 @@ def test_compute_parameter_bounds(self): buset1 = BudgetSet([[1, 1], [0, 1]], rhs_vec=[2, 3], origin=None) np.testing.assert_allclose( - buset1.parameter_bounds, - buset1._compute_parameter_bounds(solver), + buset1.parameter_bounds, buset1._compute_parameter_bounds(solver) ) # this also checks that the list entries are tuples @@ -558,12 +548,10 @@ def test_compute_parameter_bounds(self): buset2 = BudgetSet([[1, 0], [1, 1]], rhs_vec=[3, 2], origin=[1, 2]) self.assertEqual( - buset2.parameter_bounds, - buset2._compute_parameter_bounds(solver), + buset2.parameter_bounds, buset2._compute_parameter_bounds(solver) ) np.testing.assert_allclose( - buset2.parameter_bounds, - buset2._compute_parameter_bounds(solver), + buset2.parameter_bounds, buset2._compute_parameter_bounds(solver) ) self.assertEqual(buset2.parameter_bounds, [(1, 3), (2, 4)]) @@ -590,9 +578,7 @@ def test_set_as_constraint(self): m.v1 + np.float64(0) * m.v2 <= np.int64(4), ) assertExpressionsEqual( - self, - uq.uncertainty_cons[1].expr, - m.v1 + m.v2 <= np.int64(6), + self, uq.uncertainty_cons[1].expr, m.v1 + m.v2 <= np.int64(6) ) assertExpressionsEqual( self, @@ -653,12 +639,9 @@ def test_add_bounds_on_uncertain_parameters(self): """ m = ConcreteModel() m.v = Var([0, 1], initialize=0.5) - buset = BudgetSet( - [[1, 0], [1, 1]], rhs_vec=[3, 2], origin=[1, 3] - ) + buset = BudgetSet([[1, 0], [1, 1]], rhs_vec=[3, 2], origin=[1, 3]) buset._add_bounds_on_uncertain_parameters( - global_solver=None, - uncertain_param_vars=m.v, + global_solver=None, uncertain_param_vars=m.v ) self.assertEqual(m.v[0].bounds, (1, 3)) self.assertEqual(m.v[1].bounds, (3, 5)) @@ -799,16 +782,39 @@ def test_error_on_rank_deficient_psi_mat(self): ) @unittest.skipUnless(baron_available, "BARON is not available") - @parameterized.expand([ - # map beta to expected parameter bounds - ["beta0", 0, [(-2.0, 2.0), (0.1, 1.9), (-5.0, 9.0), (-4.0, 10.0)]], - ["beta1ov6", 1/6, [(-2.5, 2.5), (-0.4, 2.4), (-8.0, 12.0), (-7.0, 13.0)]], - ["beta1ov3", 1/3, [(-3.0, 3.0), (-0.9, 2.9), (-11.0, 15.0), (-10.0, 16.0)]], - ["beta1ov2", 1/2, [(-3.0, 3.0), (-0.95, 2.95), (-11.5, 15.5), (-10.5, 16.5)]], - ["beta2ov3", 2/3, [(-3.0, 3.0), (-1.0, 3.0), (-12.0, 16.0), (-11.0, 17.0)]], - ["beta7ov9", 7/9, [(-3.0, 3.0), (-31/30, 91/30), (-37/3, 49/3), (-34/3, 52/3)]], - ["beta1", 1, [(-3.0, 3.0), (-1.1, 3.1), (-13.0, 17.0), (-12.0, 18.0)]], - ]) + @parameterized.expand( + [ + # map beta to expected parameter bounds + ["beta0", 0, [(-2.0, 2.0), (0.1, 1.9), (-5.0, 9.0), (-4.0, 10.0)]], + ["beta1ov6", 1 / 6, [(-2.5, 2.5), (-0.4, 2.4), (-8.0, 12.0), (-7.0, 13.0)]], + [ + "beta1ov3", + 1 / 3, + [(-3.0, 3.0), (-0.9, 2.9), (-11.0, 15.0), (-10.0, 16.0)], + ], + [ + "beta1ov2", + 1 / 2, + [(-3.0, 3.0), (-0.95, 2.95), (-11.5, 15.5), (-10.5, 16.5)], + ], + [ + "beta2ov3", + 2 / 3, + [(-3.0, 3.0), (-1.0, 3.0), (-12.0, 16.0), (-11.0, 17.0)], + ], + [ + "beta7ov9", + 7 / 9, + [ + (-3.0, 3.0), + (-31 / 30, 91 / 30), + (-37 / 3, 49 / 3), + (-34 / 3, 52 / 3), + ], + ], + ["beta1", 1, [(-3.0, 3.0), (-1.1, 3.1), (-13.0, 17.0), (-12.0, 18.0)]], + ] + ) def test_compute_parameter_bounds(self, name, beta, expected_param_bounds): """ Test parameter bounds computations give expected results. @@ -824,11 +830,7 @@ def test_compute_parameter_bounds(self, name, beta, expected_param_bounds): param_bounds = fset.parameter_bounds # won't be exactly equal, - np.testing.assert_allclose( - param_bounds, - expected_param_bounds, - atol=1e-13, - ) + np.testing.assert_allclose(param_bounds, expected_param_bounds, atol=1e-13) # check parameter bounds matches LP results # exactly for each case @@ -1000,8 +1002,7 @@ def test_add_bounds_on_uncertain_parameters(self): ) fset._add_bounds_on_uncertain_parameters( - global_solver=None, - uncertain_param_vars=m.uncertain_param_vars, + global_solver=None, uncertain_param_vars=m.uncertain_param_vars ) self.assertEqual(m.uncertain_param_vars[0].bounds, (-3.0, 3.0)) self.assertEqual(m.uncertain_param_vars[1].bounds, (-1.1, 3.1)) @@ -1195,10 +1196,7 @@ def test_set_as_constraint(self): i_set = IntersectionSet( set1=BoxSet([(-0.5, 0.5), (-0.5, 0.5)]), set2=FactorModelSet( - origin=[0, 0], - number_of_factors=2, - beta=0.75, - psi_mat=[[1, 1], [1, 2]], + origin=[0, 0], number_of_factors=2, beta=0.75, psi_mat=[[1, 1], [1, 2]] ), set3=CardinalitySet([-0.5, -0.5], [2, 2], 2), # ellipsoid. this is enclosed in all the other sets @@ -1227,41 +1225,28 @@ def test_set_as_constraint(self): # factor model constraints aux_vars = uq.auxiliary_vars assertExpressionsEqual( - self, - uq.uncertainty_cons[2].expr, - aux_vars[0] + aux_vars[1] == m.v1, + self, uq.uncertainty_cons[2].expr, aux_vars[0] + aux_vars[1] == m.v1 ) assertExpressionsEqual( - self, - uq.uncertainty_cons[3].expr, - aux_vars[0] + 2 * aux_vars[1] == m.v2, + self, uq.uncertainty_cons[3].expr, aux_vars[0] + 2 * aux_vars[1] == m.v2 ) assertExpressionsEqual( self, uq.uncertainty_cons[4].expr, - RangedExpression( - (-1.5, aux_vars[0] + aux_vars[1], 1.5), - False, - ), + RangedExpression((-1.5, aux_vars[0] + aux_vars[1], 1.5), False), ) self.assertEqual(aux_vars[0].bounds, (-1, 1)) self.assertEqual(aux_vars[1].bounds, (-1, 1)) # cardinality set constraints assertExpressionsEqual( - self, - uq.uncertainty_cons[5].expr, - -0.5 + 2 * aux_vars[2] == m.v1, + self, uq.uncertainty_cons[5].expr, -0.5 + 2 * aux_vars[2] == m.v1 ) assertExpressionsEqual( - self, - uq.uncertainty_cons[6].expr, - -0.5 + 2 * aux_vars[3] == m.v2, + self, uq.uncertainty_cons[6].expr, -0.5 + 2 * aux_vars[3] == m.v2 ) assertExpressionsEqual( - self, - uq.uncertainty_cons[7].expr, - sum(aux_vars[2:4]) <= 2, + self, uq.uncertainty_cons[7].expr, sum(aux_vars[2:4]) <= 2 ) self.assertEqual(aux_vars[2].bounds, (0, 1)) self.assertEqual(uq.auxiliary_vars[3].bounds, (0, 1)) @@ -1270,7 +1255,7 @@ def test_set_as_constraint(self): assertExpressionsEqual( self, uq.uncertainty_cons[8].expr, - m.v1 ** 2 / np.float64(0.0625) + m.v2 ** 2 / np.float64(0.0625) <= 1 + m.v1**2 / np.float64(0.0625) + m.v2**2 / np.float64(0.0625) <= 1, ) def test_set_as_constraint_dim_mismatch(self): @@ -1312,10 +1297,7 @@ def test_compute_parameter_bounds(self): i_set = IntersectionSet( set1=BoxSet([(-0.5, 0.5), (-0.5, 0.5)]), set2=FactorModelSet( - origin=[0, 0], - number_of_factors=2, - beta=0.75, - psi_mat=[[1, 1], [1, 2]], + origin=[0, 0], number_of_factors=2, beta=0.75, psi_mat=[[1, 1], [1, 2]] ), # another origin-centered square set3=CardinalitySet([-0.5, -0.5], [2, 2], 2), @@ -1339,10 +1321,7 @@ def test_point_in_set(self): set1=BoxSet([(-0.5, 0.5), (-0.5, 0.5)]), # this is just an origin-centered square set2=FactorModelSet( - origin=[0, 0], - number_of_factors=2, - beta=0.75, - psi_mat=[[1, 1], [1, 2]], + origin=[0, 0], number_of_factors=2, beta=0.75, psi_mat=[[1, 1], [1, 2]] ), set3=CardinalitySet([-0.5, -0.5], [2, 2], 2), # ellipsoid. this is enclosed in all the other sets @@ -1369,10 +1348,7 @@ def test_add_bounds_on_uncertain_parameters(self): iset = IntersectionSet( set1=BoxSet([(-0.5, 0.5), (-0.5, 0.5)]), set2=FactorModelSet( - origin=[0, 0], - number_of_factors=2, - beta=0.75, - psi_mat=[[1, 1], [1, 2]], + origin=[0, 0], number_of_factors=2, beta=0.75, psi_mat=[[1, 1], [1, 2]] ), set3=CardinalitySet([-0.5, -0.5], [2, 2], 2), # ellipsoid. this is enclosed in all the other sets @@ -1509,25 +1485,11 @@ def test_set_as_constraint(self): auxvar1, auxvar2, auxvar3 = uq.auxiliary_vars assertExpressionsEqual( - self, - hadamard_cons[0].expr, - -0.5 + 2.5 * auxvar1 == var1, - ) - assertExpressionsEqual( - self, - hadamard_cons[1].expr, - 1.0 + 3.0 * auxvar2 == var2, - ) - assertExpressionsEqual( - self, - hadamard_cons[2].expr, - 2.0 + 0.0 * auxvar3 == var3, - ) - assertExpressionsEqual( - self, - gamma_con.expr, - auxvar1 + auxvar2 + auxvar3 <= 1.5, + self, hadamard_cons[0].expr, -0.5 + 2.5 * auxvar1 == var1 ) + assertExpressionsEqual(self, hadamard_cons[1].expr, 1.0 + 3.0 * auxvar2 == var2) + assertExpressionsEqual(self, hadamard_cons[2].expr, 2.0 + 0.0 * auxvar3 == var3) + assertExpressionsEqual(self, gamma_con.expr, auxvar1 + auxvar2 + auxvar3 <= 1.5) def test_set_as_constraint_dim_mismatch(self): """ @@ -1556,9 +1518,7 @@ def test_set_as_constraint_type_mismatch(self): def test_point_in_set(self): cset = CardinalitySet( - origin=[-0.5, 1, 2], - positive_deviation=[2.5, 3, 0], - gamma=1.5, + origin=[-0.5, 1, 2], positive_deviation=[2.5, 3, 0], gamma=1.5 ) self.assertTrue(cset.point_in_set(cset.origin)) @@ -1587,9 +1547,7 @@ def test_compute_parameter_bounds(self): Test parameter bounds computations give expected results. """ cset = CardinalitySet( - origin=[-0.5, 1, 2], - positive_deviation=[2.5, 3, 0], - gamma=1.5, + origin=[-0.5, 1, 2], positive_deviation=[2.5, 3, 0], gamma=1.5 ) computed_bounds = cset._compute_parameter_bounds(SolverFactory("baron")) np.testing.assert_allclose(computed_bounds, [[-0.5, 2], [1, 4], [2, 2]]) @@ -1599,14 +1557,11 @@ def test_add_bounds_on_uncertain_parameters(self): m = ConcreteModel() m.uncertain_param_vars = Var([0, 1, 2], initialize=0) cset = CardinalitySet( - origin=[-0.5, 1, 2], - positive_deviation=[2.5, 3, 0], - gamma=1.5, + origin=[-0.5, 1, 2], positive_deviation=[2.5, 3, 0], gamma=1.5 ) cset._add_bounds_on_uncertain_parameters( - global_solver=None, - uncertain_param_vars=m.uncertain_param_vars, + global_solver=None, uncertain_param_vars=m.uncertain_param_vars ) self.assertEqual(m.uncertain_param_vars[0].bounds, (-0.5, 2)) self.assertEqual(m.uncertain_param_vars[1].bounds, (1, 4)) @@ -1713,8 +1668,7 @@ def test_add_bounds_on_uncertain_parameters(self): dset = DiscreteScenarioSet([(0, 0), (1.5, 0), (0, 1), (1, 1), (2, 0)]) dset._add_bounds_on_uncertain_parameters( - global_solver=None, - uncertain_param_vars=m.uncertain_param_vars, + global_solver=None, uncertain_param_vars=m.uncertain_param_vars ) self.assertEqual(m.uncertain_param_vars[0].bounds, (0, 2)) self.assertEqual(m.uncertain_param_vars[1].bounds, (0, 1.0)) @@ -1804,9 +1758,7 @@ def test_set_as_constraint(self): """ m = ConcreteModel() m.v = Var([0, 1, 2]) - aeset = AxisAlignedEllipsoidalSet( - center=[0, 1.5, 1], half_lengths=[1.5, 2, 0], - ) + aeset = AxisAlignedEllipsoidalSet(center=[0, 1.5, 1], half_lengths=[1.5, 2, 0]) uq = aeset.set_as_constraint(uncertain_params=m.v, block=m) self.assertEqual(len(uq.uncertainty_cons), 2) @@ -1816,17 +1768,13 @@ def test_set_as_constraint(self): con1, con2 = uq.uncertainty_cons - assertExpressionsEqual( - self, - con1.expr, - m.v[2] == np.float64(1.0) - ) + assertExpressionsEqual(self, con1.expr, m.v[2] == np.float64(1.0)) assertExpressionsEqual( self, con2.expr, m.v[0] ** 2 / np.float64(2.25) + (m.v[1] - np.float64(1.5)) ** 2 / np.float64(4) - <= 1 + <= 1, ) def test_set_as_constraint_dim_mismatch(self): @@ -1836,9 +1784,7 @@ def test_set_as_constraint_dim_mismatch(self): """ m = ConcreteModel() m.v1 = Var(initialize=0) - aeset = AxisAlignedEllipsoidalSet( - center=[0, 1.5, 1], half_lengths=[1.5, 2, 0], - ) + aeset = AxisAlignedEllipsoidalSet(center=[0, 1.5, 1], half_lengths=[1.5, 2, 0]) with self.assertRaisesRegex(ValueError, ".*dimension"): aeset.set_as_constraint(uncertain_params=[m.v1], block=m) @@ -1849,9 +1795,7 @@ def test_set_as_constraint_type_mismatch(self): """ m = ConcreteModel() m.p1 = Param([0, 1, 2], initialize=0, mutable=True) - aeset = AxisAlignedEllipsoidalSet( - center=[0, 1.5, 1], half_lengths=[1.5, 2, 0], - ) + aeset = AxisAlignedEllipsoidalSet(center=[0, 1.5, 1], half_lengths=[1.5, 2, 0]) with self.assertRaisesRegex(TypeError, ".*valid component type"): aeset.set_as_constraint(uncertain_params=[m.p1[0], m.p1[1]], block=m) @@ -1863,17 +1807,13 @@ def test_compute_parameter_bounds(self): """ Test parameter bounds computations give expected results. """ - aeset = AxisAlignedEllipsoidalSet( - center=[0, 1.5, 1], half_lengths=[1.5, 2, 0], - ) + aeset = AxisAlignedEllipsoidalSet(center=[0, 1.5, 1], half_lengths=[1.5, 2, 0]) computed_bounds = aeset._compute_parameter_bounds(SolverFactory("baron")) np.testing.assert_allclose(computed_bounds, [[-1.5, 1.5], [-0.5, 3.5], [1, 1]]) np.testing.assert_allclose(computed_bounds, aeset.parameter_bounds) def test_point_in_set(self): - aeset = AxisAlignedEllipsoidalSet( - center=[0, 0, 1], half_lengths=[1.5, 2, 0], - ) + aeset = AxisAlignedEllipsoidalSet(center=[0, 0, 1], half_lengths=[1.5, 2, 0]) self.assertTrue(aeset.point_in_set([0, 0, 1])) self.assertTrue(aeset.point_in_set([0, 2, 1])) @@ -1891,12 +1831,9 @@ def test_point_in_set(self): def test_add_bounds_on_uncertain_parameters(self): m = ConcreteModel() m.uncertain_param_vars = Var([0, 1, 2], initialize=0) - aeset = AxisAlignedEllipsoidalSet( - center=[0, 1.5, 1], half_lengths=[1.5, 2, 0], - ) + aeset = AxisAlignedEllipsoidalSet(center=[0, 1.5, 1], half_lengths=[1.5, 2, 0]) aeset._add_bounds_on_uncertain_parameters( - global_solver=None, - uncertain_param_vars=m.uncertain_param_vars, + global_solver=None, uncertain_param_vars=m.uncertain_param_vars ) self.assertEqual(m.uncertain_param_vars[0].bounds, (-1.5, 1.5)) self.assertEqual(m.uncertain_param_vars[1].bounds, (-0.5, 3.5)) @@ -2066,9 +2003,7 @@ def test_set_as_constraint(self): """ m = ConcreteModel() eset = EllipsoidalSet( - center=[1, 1.5], - shape_matrix=[[1, 0.5], [0.5, 1]], - scale=2.5, + center=[1, 1.5], shape_matrix=[[1, 0.5], [0.5, 1]], scale=2.5 ) uq = eset.set_as_constraint(uncertain_params=None, block=m) @@ -2083,10 +2018,16 @@ def test_set_as_constraint(self): self, uq.uncertainty_cons[0].expr, ( - np.float64(4/3) * (var1 - np.float64(1.0)) * (var1 - np.float64(1.0)) - + np.float64(-2/3) * (var1 - np.float64(1.0)) * (var2 - np.float64(1.5)) - + np.float64(-2/3) * (var2 - np.float64(1.5)) * (var1 - np.float64(1.0)) - + np.float64(4/3) * (var2 - np.float64(1.5)) * (var2 - np.float64(1.5)) + np.float64(4 / 3) * (var1 - np.float64(1.0)) * (var1 - np.float64(1.0)) + + np.float64(-2 / 3) + * (var1 - np.float64(1.0)) + * (var2 - np.float64(1.5)) + + np.float64(-2 / 3) + * (var2 - np.float64(1.5)) + * (var1 - np.float64(1.0)) + + np.float64(4 / 3) + * (var2 - np.float64(1.5)) + * (var2 - np.float64(1.5)) <= 2.5 ), ) @@ -2099,9 +2040,7 @@ def test_set_as_constraint_dim_mismatch(self): m = ConcreteModel() m.v1 = Var(initialize=0) eset = EllipsoidalSet( - center=[1, 1.5], - shape_matrix=[[1, 0.5], [0.5, 1]], - scale=2.5, + center=[1, 1.5], shape_matrix=[[1, 0.5], [0.5, 1]], scale=2.5 ) with self.assertRaisesRegex(ValueError, ".*dimension"): eset.set_as_constraint(uncertain_params=[m.v1], block=m) @@ -2114,9 +2053,7 @@ def test_set_as_constraint_type_mismatch(self): m = ConcreteModel() m.p1 = Param([0, 1], initialize=0, mutable=True) eset = EllipsoidalSet( - center=[1, 1.5], - shape_matrix=[[1, 0.5], [0.5, 1]], - scale=2.5, + center=[1, 1.5], shape_matrix=[[1, 0.5], [0.5, 1]], scale=2.5 ) with self.assertRaisesRegex(TypeError, ".*valid component type"): eset.set_as_constraint(uncertain_params=[m.p1[0], m.p1[1]], block=m) @@ -2126,12 +2063,10 @@ def test_set_as_constraint_type_mismatch(self): def test_point_in_set(self): eset = EllipsoidalSet( - center=[1, 1.5], - shape_matrix=[[1, 0.5], [0.5, 1]], - scale=2.5, + center=[1, 1.5], shape_matrix=[[1, 0.5], [0.5, 1]], scale=2.5 ) sqrt_mat = np.linalg.cholesky(eset.shape_matrix) - sqrt_scale = eset.scale ** 0.5 + sqrt_scale = eset.scale**0.5 center = eset.center self.assertTrue(eset.point_in_set(eset.center)) @@ -2163,18 +2098,14 @@ def test_compute_parameter_bounds(self): """ baron = SolverFactory("baron") eset = EllipsoidalSet( - center=[1, 1.5], - shape_matrix=[[1, 0.5], [0.5, 1]], - scale=0.25, + center=[1, 1.5], shape_matrix=[[1, 0.5], [0.5, 1]], scale=0.25 ) computed_bounds = eset._compute_parameter_bounds(baron) np.testing.assert_allclose(computed_bounds, [[0.5, 1.5], [1.0, 2.0]]) np.testing.assert_allclose(computed_bounds, eset.parameter_bounds) eset2 = EllipsoidalSet( - center=[1, 1.5], - shape_matrix=[[1, 0.5], [0.5, 1]], - scale=2.25, + center=[1, 1.5], shape_matrix=[[1, 0.5], [0.5, 1]], scale=2.25 ) computed_bounds_2 = eset2._compute_parameter_bounds(baron) @@ -2187,13 +2118,10 @@ def test_add_bounds_on_uncertain_parameters(self): m = ConcreteModel() m.uncertain_param_vars = Var([0, 1], initialize=0) eset = EllipsoidalSet( - center=[1, 1.5], - shape_matrix=[[1, 0.5], [0.5, 1]], - scale=0.25, + center=[1, 1.5], shape_matrix=[[1, 0.5], [0.5, 1]], scale=0.25 ) eset._add_bounds_on_uncertain_parameters( - global_solver=None, - uncertain_param_vars=m.uncertain_param_vars, + global_solver=None, uncertain_param_vars=m.uncertain_param_vars ) self.assertEqual(m.uncertain_param_vars[0].bounds, (0.5, 1.5)) self.assertEqual(m.uncertain_param_vars[1].bounds, (1, 2)) @@ -2311,8 +2239,7 @@ def test_set_as_constraint(self): """ m = ConcreteModel() pset = PolyhedralSet( - lhs_coefficients_mat=[[1, 0], [-1, 1], [-1, -1]], - rhs_vec=[2, -1, -1], + lhs_coefficients_mat=[[1, 0], [-1, 1], [-1, -1]], rhs_vec=[2, -1, -1] ) uq = pset.set_as_constraint(uncertain_params=None, block=m) @@ -2324,9 +2251,7 @@ def test_set_as_constraint(self): var1, var2 = uq.uncertain_param_vars assertExpressionsEqual( - self, - uq.uncertainty_cons[0].expr, - var1 + np.int64(0) * var2 <= np.int64(2), + self, uq.uncertainty_cons[0].expr, var1 + np.int64(0) * var2 <= np.int64(2) ) assertExpressionsEqual( self, @@ -2347,8 +2272,7 @@ def test_set_as_constraint_dim_mismatch(self): m = ConcreteModel() m.v1 = Var(initialize=0) pset = PolyhedralSet( - lhs_coefficients_mat=[[1, 0], [-1, 1], [-1, -1]], - rhs_vec=[2, -1, -1], + lhs_coefficients_mat=[[1, 0], [-1, 1], [-1, -1]], rhs_vec=[2, -1, -1] ) with self.assertRaisesRegex(ValueError, ".*dimension"): pset.set_as_constraint(uncertain_params=[m.v1], block=m) @@ -2361,8 +2285,7 @@ def test_set_as_constraint_type_mismatch(self): m = ConcreteModel() m.p1 = Param([0, 1], initialize=0, mutable=True) pset = PolyhedralSet( - lhs_coefficients_mat=[[1, 0], [-1, 1], [-1, -1]], - rhs_vec=[2, -1, -1], + lhs_coefficients_mat=[[1, 0], [-1, 1], [-1, -1]], rhs_vec=[2, -1, -1] ) with self.assertRaisesRegex(TypeError, ".*valid component type"): pset.set_as_constraint(uncertain_params=[m.p1[0], m.p1[1]], block=m) @@ -2376,8 +2299,7 @@ def test_compute_parameter_bounds(self): Test parameter bounds computations give expected results. """ pset = PolyhedralSet( - lhs_coefficients_mat=[[1, 0], [-1, 1], [-1, -1]], - rhs_vec=[2, -1, -1], + lhs_coefficients_mat=[[1, 0], [-1, 1], [-1, -1]], rhs_vec=[2, -1, -1] ) self.assertEqual(pset.parameter_bounds, []) computed_bounds = pset._compute_parameter_bounds(SolverFactory("baron")) @@ -2388,8 +2310,7 @@ def test_point_in_set(self): Test point in set checks work as expected. """ pset = PolyhedralSet( - lhs_coefficients_mat=[[1, 0], [-1, 1], [-1, -1]], - rhs_vec=[2, -1, -1], + lhs_coefficients_mat=[[1, 0], [-1, 1], [-1, -1]], rhs_vec=[2, -1, -1] ) self.assertTrue(pset.point_in_set([1, 0])) self.assertTrue(pset.point_in_set([2, 1])) @@ -2407,8 +2328,7 @@ def test_add_bounds_on_uncertain_parameters(self): m = ConcreteModel() m.uncertain_param_vars = Var([0, 1], initialize=0) pset = PolyhedralSet( - lhs_coefficients_mat=[[1, 0], [-1, 1], [-1, -1]], - rhs_vec=[2, -1, -1], + lhs_coefficients_mat=[[1, 0], [-1, 1], [-1, -1]], rhs_vec=[2, -1, -1] ) pset._add_bounds_on_uncertain_parameters( global_solver=SolverFactory("baron"), @@ -2422,6 +2342,7 @@ class CustomUncertaintySet(UncertaintySet): """ Test simple custom uncertainty set subclass. """ + def __init__(self, dim): self._dim = dim diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index 4e3c0c05df5..eda1354d10f 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -321,7 +321,12 @@ def validate_dimensions(arr_name, arr, dim, display_value=False): def validate_array( - arr, arr_name, dim, valid_types, valid_type_desc=None, required_shape=None, + arr, + arr_name, + dim, + valid_types, + valid_type_desc=None, + required_shape=None, required_shape_qual="", ): """ @@ -377,7 +382,9 @@ def generate_shape_str(shape, required_shape): actual_shape_str = generate_shape_str(np_arr.shape, required_shape) required_shape_qual = ( # add a preceding space, if needed - f" {required_shape_qual}" if required_shape_qual else "" + f" {required_shape_qual}" + if required_shape_qual + else "" ) raise ValueError( f"Attribute '{arr_name}' should be of shape " @@ -581,7 +588,7 @@ def point_in_set(self, point): valid_types=valid_num_types, valid_type_desc="numeric type", required_shape=[self.dim], - required_shape_qual="to match the set dimension" + required_shape_qual="to match the set dimension", ) m = ConcreteModel() @@ -1241,7 +1248,7 @@ def compute_auxiliary_uncertain_param_vals(self, point, solver=None): valid_types=valid_num_types, valid_type_desc="numeric type", required_shape=[self.dim], - required_shape_qual="to match the set dimension" + required_shape_qual="to match the set dimension", ) point_arr = np.array(point) @@ -2091,7 +2098,7 @@ def compute_auxiliary_uncertain_param_vals(self, point, solver=None): valid_types=valid_num_types, valid_type_desc="numeric type", required_shape=[self.dim], - required_shape_qual="to match the set dimension" + required_shape_qual="to match the set dimension", ) point_arr = np.array(point) @@ -2120,9 +2127,10 @@ def point_in_set(self, point): """ aux_space_pt = self.compute_auxiliary_uncertain_param_vals(point) tol = 1e-8 - return ( - abs(aux_space_pt.sum()) <= self.beta * self.number_of_factors + tol - and np.all(np.abs(aux_space_pt) <= 1 + tol) + return abs( + aux_space_pt.sum() + ) <= self.beta * self.number_of_factors + tol and np.all( + np.abs(aux_space_pt) <= 1 + tol ) @@ -2551,7 +2559,7 @@ def point_in_set(self, point): valid_types=valid_num_types, valid_type_desc="numeric type", required_shape=[self.dim], - required_shape_qual="to match the set dimension" + required_shape_qual="to match the set dimension", ) off_center = point - self.center normalized_pt_radius = np.sqrt( @@ -2746,7 +2754,7 @@ def point_in_set(self, point): valid_types=valid_num_types, valid_type_desc="numeric type", required_shape=[self.dim], - required_shape_qual="to match the set dimension" + required_shape_qual="to match the set dimension", ) # Round all double precision to a tolerance rounded_scenarios = np.round(self.scenarios, decimals=8) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index dedc3587eb8..2c25a791371 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -541,14 +541,14 @@ class ObjectiveType(Enum): def standardize_component_data( - obj, - valid_ctype, - valid_cdatatype, - ctype_validator=None, - cdatatype_validator=None, - allow_repeats=False, - from_iterable=None, - ): + obj, + valid_ctype, + valid_cdatatype, + ctype_validator=None, + cdatatype_validator=None, + allow_repeats=False, + from_iterable=None, +): """ Standardize object to a list of component data objects. """ @@ -586,9 +586,7 @@ def standardize_component_data( ) else: from_iterable_qual = ( - f" (entry of iterable {from_iterable})" - if from_iterable is not None - else "" + f" (entry of iterable {from_iterable})" if from_iterable is not None else "" ) raise TypeError( f"Input object {obj!r}{from_iterable_qual} " @@ -805,18 +803,12 @@ def validate_variable_partitioning(model, config): ) check_variables_continuous(model, active_model_vars, config) - first_stage_vars = ( - ComponentSet(config.first_stage_variables) & active_model_vars - ) - second_stage_vars = ( - ComponentSet(config.second_stage_variables) & active_model_vars - ) + first_stage_vars = ComponentSet(config.first_stage_variables) & active_model_vars + second_stage_vars = ComponentSet(config.second_stage_variables) & active_model_vars state_vars = active_model_vars - (first_stage_vars | second_stage_vars) return VariablePartitioning( - list(first_stage_vars), - list(second_stage_vars), - list(state_vars), + list(first_stage_vars), list(second_stage_vars), list(state_vars) ) @@ -978,11 +970,8 @@ def get_var_bound_pairs(var): def determine_certain_and_uncertain_bound( - domain_bound, - declared_bound, - uncertain_params, - bound_type, - ): + domain_bound, declared_bound, uncertain_params, bound_type +): """ Determine the certain and uncertain lower or upper bound for a variable object, based on the specified @@ -1013,10 +1002,9 @@ def determine_certain_and_uncertain_bound( ) if declared_bound is not None: - uncertain_params_in_declared_bound = ( - ComponentSet(uncertain_params) - & ComponentSet(identify_mutable_parameters(declared_bound)) - ) + uncertain_params_in_declared_bound = ComponentSet( + uncertain_params + ) & ComponentSet(identify_mutable_parameters(declared_bound)) else: uncertain_params_in_declared_bound = False @@ -1030,12 +1018,14 @@ def determine_certain_and_uncertain_bound( else: if bound_type == "lower": certain_bound = ( - declared_bound if value(declared_bound) >= domain_bound + declared_bound + if value(declared_bound) >= domain_bound else domain_bound ) else: certain_bound = ( - declared_bound if value(declared_bound) <= domain_bound + declared_bound + if value(declared_bound) <= domain_bound else domain_bound ) else: @@ -1045,10 +1035,7 @@ def determine_certain_and_uncertain_bound( return certain_bound, uncertain_bound -BoundTriple = namedtuple( - "BoundTriple", - ("lower", "eq", "upper"), -) +BoundTriple = namedtuple("BoundTriple", ("lower", "eq", "upper")) def rearrange_bound_pair_to_triple(lower_bound, upper_bound): @@ -1128,7 +1115,7 @@ def get_var_certain_uncertain_bounds(var, uncertain_params): ) certain_bounds = rearrange_bound_pair_to_triple( - lower_bound=certain_lb, upper_bound=certain_ub, + lower_bound=certain_lb, upper_bound=certain_ub ) uncertain_bounds = rearrange_bound_pair_to_triple( lower_bound=uncertain_lb, upper_bound=uncertain_ub @@ -1183,8 +1170,7 @@ def get_effective_var_partitioning(model_data, config): for vartype, varlist in var_type_list_pairs: for wvar in varlist: certain_var_bounds, _ = get_var_certain_uncertain_bounds( - wvar, - working_model.uncertain_params, + wvar, working_model.uncertain_params ) is_var_nonadjustable = ( @@ -1209,14 +1195,10 @@ def get_effective_var_partitioning(model_data, config): ) if wvar.fixed: - config.progress_logger.debug( - " the variable is fixed explicitly" - ) + config.progress_logger.debug(" the variable is fixed explicitly") if certain_var_bounds.eq is not None: - config.progress_logger.debug( - " the variable is fixed by domain/bounds" - ) + config.progress_logger.debug(" the variable is fixed by domain/bounds") uncertain_params_set = ComponentSet(working_model.uncertain_params) @@ -1227,8 +1209,7 @@ def get_effective_var_partitioning(model_data, config): if not wcon.equality: continue uncertain_params_in_expr = ( - ComponentSet(identify_mutable_parameters(wcon.expr)) - & uncertain_params_set + ComponentSet(identify_mutable_parameters(wcon.expr)) & uncertain_params_set ) if uncertain_params_in_expr: continue @@ -1253,22 +1234,17 @@ def get_effective_var_partitioning(model_data, config): if len(adj_vars_in_con) == 1: adj_var_in_con = next(iter(adj_vars_in_con)) ccon_expr_repn = generate_standard_repn( - expr=ccon.body - ccon.upper, - quadratic=False, - compute_values=True, - ) - adj_var_appears_linearly = ( - adj_var_in_con - not in ComponentSet(ccon_expr_repn.nonlinear_vars) - and adj_var_in_con in ComponentSet(ccon_expr_repn.linear_vars) + expr=ccon.body - ccon.upper, quadratic=False, compute_values=True ) + adj_var_appears_linearly = adj_var_in_con not in ComponentSet( + ccon_expr_repn.nonlinear_vars + ) and adj_var_in_con in ComponentSet(ccon_expr_repn.linear_vars) if adj_var_appears_linearly: # get coefficient by summation just in case # standard repn does not simplify completely var_linear_coeff = sum( lcoeff - for lvar, lcoeff - in zip( + for lvar, lcoeff in zip( ccon_expr_repn.linear_vars, ccon_expr_repn.linear_coefs ) if lvar is adj_var_in_con @@ -1312,15 +1288,11 @@ def get_effective_var_partitioning(model_data, config): if var not in nonadjustable_var_set ] num_vars = len( - effective_first_stage_vars - + effective_second_stage_vars - + effective_state_vars + effective_first_stage_vars + effective_second_stage_vars + effective_state_vars ) config.progress_logger.debug("Effective partitioning statistics:") - config.progress_logger.debug( - f" Variables: {num_vars}" - ) + config.progress_logger.debug(f" Variables: {num_vars}") config.progress_logger.debug( f" Effective first-stage variables: {len(effective_first_stage_vars)}" ) @@ -1353,11 +1325,10 @@ def add_effective_var_partitioning(model_data, config): PyROS solver options. """ effective_partitioning = get_effective_var_partitioning( - model_data=model_data, - config=config, + model_data=model_data, config=config ) - model_data.working_model.effective_var_partitioning = ( - VariablePartitioning(**effective_partitioning._asdict()) + model_data.working_model.effective_var_partitioning = VariablePartitioning( + **effective_partitioning._asdict() ) @@ -1397,9 +1368,7 @@ def create_bound_constraint_expr(expr, bound, bound_type, standardize=True): elif bound_type == "upper": return expr <= bound else: - raise ValueError( - f"Bound type {bound_type!r} not supported." - ) + raise ValueError(f"Bound type {bound_type!r} not supported.") def remove_var_declared_bound(var, bound_type): @@ -1459,24 +1428,17 @@ def turn_nonadjustable_var_bounds_to_constraints(model_data): PyROS solver settings. """ working_model = model_data.working_model - nonadjustable_vars = ( - working_model.effective_var_partitioning.first_stage_variables - ) + nonadjustable_vars = working_model.effective_var_partitioning.first_stage_variables uncertain_params_set = ComponentSet(working_model.uncertain_params) for var in nonadjustable_vars: _, declared_bounds = get_var_bound_pairs(var) declared_bound_triple = rearrange_bound_pair_to_triple(*declared_bounds) var_name = var.getname( - relative_to=working_model.user_model, - fully_qualified=True, + relative_to=working_model.user_model, fully_qualified=True ) for btype, bound in declared_bound_triple._asdict().items(): - is_bound_uncertain = ( - bound is not None - and ( - ComponentSet(identify_mutable_parameters(bound)) - & uncertain_params_set - ) + is_bound_uncertain = bound is not None and ( + ComponentSet(identify_mutable_parameters(bound)) & uncertain_params_set ) if is_bound_uncertain: new_con_expr = create_bound_constraint_expr(var, bound, btype) @@ -1525,11 +1487,10 @@ def turn_adjustable_var_bounds_to_constraints(model_data): ) for var in adjustable_vars: cert_bound_triple, uncert_bound_triple = get_var_certain_uncertain_bounds( - var, working_model.uncertain_params, + var, working_model.uncertain_params ) var_name = var.getname( - relative_to=working_model.user_model, - fully_qualified=True, + relative_to=working_model.user_model, fully_qualified=True ) cert_uncert_bound_zip = ( ("certain", cert_bound_triple), @@ -1576,9 +1537,7 @@ def setup_working_model(model_data, config, user_var_partitioning): # add temporary block to help keep track of variables # and uncertain parameters after cloning - temp_util_block_attr_name = unique_component_name( - original_model, "util" - ) + temp_util_block_attr_name = unique_component_name(original_model, "util") original_model.add_component(temp_util_block_attr_name, Block()) orig_temp_util_block = getattr(original_model, temp_util_block_attr_name) orig_temp_util_block.uncertain_params = config.uncertain_params @@ -1603,8 +1562,7 @@ def setup_working_model(model_data, config, user_var_partitioning): # facilitate later retrieval of the user var partitioning working_temp_util_block = getattr( - working_model.user_model, - temp_util_block_attr_name, + working_model.user_model, temp_util_block_attr_name ) model_data.working_model.uncertain_params = ( working_temp_util_block.uncertain_params.copy() @@ -1648,25 +1606,22 @@ def standardize_inequality_constraints(model_data): ) for con in working_model.original_active_inequality_cons: uncertain_params_in_con_expr = ( - ComponentSet(identify_mutable_parameters(con.expr)) - & uncertain_params_set + ComponentSet(identify_mutable_parameters(con.expr)) & uncertain_params_set ) adjustable_vars_in_con_body = ( - ComponentSet(identify_variables(con.body)) - & adjustable_vars_set + ComponentSet(identify_variables(con.body)) & adjustable_vars_set ) con_rel_name = con.getname( - relative_to=working_model.user_model, - fully_qualified=True, + relative_to=working_model.user_model, fully_qualified=True ) if uncertain_params_in_con_expr | adjustable_vars_in_con_body: con_bounds_triple = rearrange_bound_pair_to_triple( - lower_bound=con.lower, - upper_bound=con.upper, + lower_bound=con.lower, upper_bound=con.upper ) finite_bounds = { - btype: bd for btype, bd in con_bounds_triple._asdict().items() + btype: bd + for btype, bd in con_bounds_triple._asdict().items() if bd is not None } for btype, bound in finite_bounds.items(): @@ -1685,10 +1640,7 @@ def standardize_inequality_constraints(model_data): ) std_con_expr = create_bound_constraint_expr( - expr=con.body, - bound=bound, - bound_type=btype, - standardize=True, + expr=con.body, bound=bound, bound_type=btype, standardize=True ) new_con_name = f"ineq_con_{con_rel_name}_{btype}_bound_con" @@ -1745,27 +1697,22 @@ def standardize_equality_constraints(model_data): ) for con in working_model.original_active_equality_cons: uncertain_params_in_con_expr = ( - ComponentSet(identify_mutable_parameters(con.expr)) - & uncertain_params_set + ComponentSet(identify_mutable_parameters(con.expr)) & uncertain_params_set ) adjustable_vars_in_con_body = ( - ComponentSet(identify_variables(con.body)) - & adjustable_vars_set + ComponentSet(identify_variables(con.body)) & adjustable_vars_set ) # note: none of the equality constraint expressions are modified con_rel_name = con.getname( - relative_to=working_model.user_model, - fully_qualified=True, + relative_to=working_model.user_model, fully_qualified=True ) if uncertain_params_in_con_expr | adjustable_vars_in_con_body: working_model.second_stage.equality_cons[f"eq_con_{con_rel_name}"] = ( con.expr ) else: - working_model.first_stage.equality_cons[f"eq_con_{con_rel_name}"] = ( - con.expr - ) + working_model.first_stage.equality_cons[f"eq_con_{con_rel_name}"] = con.expr # definitely don't want active duplicate con.deactivate() @@ -1896,17 +1843,12 @@ def standardize_active_objective(model_data, config): working_model = model_data.working_model active_obj = next( - working_model.component_data_objects( - Objective, active=True, descend_into=True - ) + working_model.component_data_objects(Objective, active=True, descend_into=True) ) model_data.active_obj_original_sense = active_obj.sense # per-stage summands will be useful for reporting later - declare_objective_expressions( - working_model=working_model, - objective=active_obj, - ) + declare_objective_expressions(working_model=working_model, objective=active_obj) # useful for later working_model.first_stage.epigraph_var = Var( @@ -1923,15 +1865,13 @@ def standardize_active_objective(model_data, config): working_model.effective_var_partitioning.second_stage_variables + working_model.effective_var_partitioning.state_variables ) - uncertain_params_in_obj = ( - ComponentSet(identify_mutable_parameters(active_obj.expr)) - & ComponentSet(working_model.uncertain_params) - ) + uncertain_params_in_obj = ComponentSet( + identify_mutable_parameters(active_obj.expr) + ) & ComponentSet(working_model.uncertain_params) adjustable_vars_in_obj = ( - ComponentSet(identify_variables(active_obj.expr)) - & adjustable_vars + ComponentSet(identify_variables(active_obj.expr)) & adjustable_vars ) - if (uncertain_params_in_obj | adjustable_vars_in_obj): + if uncertain_params_in_obj | adjustable_vars_in_obj: if config.objective_focus == ObjectiveType.worst_case: working_model.second_stage.inequality_cons["epigraph_con"] = ( working_model.full_objective.expr @@ -1952,8 +1892,7 @@ def standardize_active_objective(model_data, config): ) else: working_model.first_stage.inequality_cons["epigraph_con"] = ( - working_model.full_objective.expr - - working_model.first_stage.epigraph_var + working_model.full_objective.expr - working_model.first_stage.epigraph_var <= 0 ) @@ -2143,12 +2082,11 @@ def reformulate_state_var_independent_eq_cons(model_data, config): range(len(uncertain_params_set)), initialize={ idx: value(param) for idx, param in enumerate(uncertain_params_set) - } + }, ) uncertain_param_to_temp_var_map = ComponentMap( (param, param_var) - for param, param_var - in zip(uncertain_params_set, temp_param_vars.values()) + for param, param_var in zip(uncertain_params_set, temp_param_vars.values()) ) uncertain_param_id_to_temp_var_map = { id(param): var for param, var in uncertain_param_to_temp_var_map.items() @@ -2166,14 +2104,12 @@ def reformulate_state_var_independent_eq_cons(model_data, config): state_vars_in_con = vars_in_con & effective_state_var_set uncertain_params_in_con = mutable_params_in_con & uncertain_params_set - coefficient_matching_applicable = ( - not state_vars_in_con - and (uncertain_params_in_con or second_stage_vars_in_con) + coefficient_matching_applicable = not state_vars_in_con and ( + uncertain_params_in_con or second_stage_vars_in_con ) if coefficient_matching_applicable: con_expr_after_dr_substitution = replace_expressions( - expr=con.body - con.upper, - substitution_map=ssvar_id_to_dr_expr_map, + expr=con.body - con.upper, substitution_map=ssvar_id_to_dr_expr_map ) # substitute temporarily defined vars for uncertain params. @@ -2192,8 +2128,7 @@ def reformulate_state_var_independent_eq_cons(model_data, config): for var in originally_unfixed_vars: var.fix() expr_repn = generate_standard_repn( - expr=con_expr_after_all_substitutions, - compute_values=False, + expr=con_expr_after_all_substitutions, compute_values=False ) # ensure state of every variable remains unchanged @@ -2234,8 +2169,7 @@ def reformulate_state_var_independent_eq_cons(model_data, config): ) for coeff_idx, coeff_expr in enumerate(polynomial_repn_coeffs): simplified_coeff_expr = generate_standard_repn( - expr=coeff_expr, - compute_values=True, + expr=coeff_expr, compute_values=True ).to_expression() # for robust satisfaction of the original equality @@ -2353,8 +2287,8 @@ def preprocess_model_data(model_data, config, user_var_partitioning): model_data.working_model.all_nonadjustable_variables = ( get_all_nonadjustable_variables(model_data.working_model) ) - model_data.working_model.all_adjustable_variables = ( - get_all_adjustable_variables(model_data.working_model) + model_data.working_model.all_adjustable_variables = get_all_adjustable_variables( + model_data.working_model ) model_data.working_model.all_variables = ( model_data.working_model.all_nonadjustable_variables @@ -2364,10 +2298,7 @@ def preprocess_model_data(model_data, config, user_var_partitioning): config.progress_logger.debug( "Reformulating state variable-independent second-stage equality constraints..." ) - robust_infeasible = reformulate_state_var_independent_eq_cons( - model_data, - config, - ) + robust_infeasible = reformulate_state_var_independent_eq_cons(model_data, config) return robust_infeasible @@ -2396,17 +2327,13 @@ def log_model_statistics(model_data, config): num_state_vars = len(up.state_variables) num_eff_second_stage_vars = len(ep.second_stage_variables) num_eff_state_vars = len(ep.state_variables) - num_dr_vars = len( - list(generate_all_decision_rule_var_data_objects(working_model)) - ) + num_dr_vars = len(list(generate_all_decision_rule_var_data_objects(working_model))) # uncertain parameters num_uncertain_params = len(working_model.uncertain_params) # constraints - num_cons = len( - list(working_model.component_data_objects(Constraint, active=True)) - ) + num_cons = len(list(working_model.component_data_objects(Constraint, active=True))) # # equality constraints num_eq_cons = ( @@ -2421,9 +2348,8 @@ def log_model_statistics(model_data, config): num_dr_eq_cons = len(working_model.second_stage.decision_rule_eqns) # # inequality constraints - num_ineq_cons = ( - len(working_model.first_stage.inequality_cons) - + len(working_model.second_stage.inequality_cons) + num_ineq_cons = len(working_model.first_stage.inequality_cons) + len( + working_model.second_stage.inequality_cons ) num_first_stage_ineq_cons = len(working_model.first_stage.inequality_cons) num_second_stage_ineq_cons = len(working_model.second_stage.inequality_cons) @@ -2441,8 +2367,7 @@ def log_model_statistics(model_data, config): f"({num_eff_second_stage_vars} adj.)" ) info_log_func( - f" State variables : {num_state_vars} " - f"({num_eff_state_vars} adj.)" + f" State variables : {num_state_vars} " f"({num_eff_state_vars} adj.)" ) info_log_func(f" Decision rule variables : {num_dr_vars}") @@ -2616,11 +2541,8 @@ def enforce_dr_degree(working_blk, config, degree): def load_final_solution( - model_data, - master_soln, - config, - original_user_var_partitioning, - ): + model_data, master_soln, config, original_user_var_partitioning +): """ Load variable values from the master problem to the original model. @@ -3013,6 +2935,7 @@ def copy_docstring(source_func): decorator_doc : callable Decorator of interest. """ + def decorator_doc(func): @functools.wraps(func) def wrapper(*args, **kwargs): From 38b0f33c50a90d39d5392457750c0ee2ca657ab9 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sat, 10 Aug 2024 22:06:34 -0400 Subject: [PATCH 2172/3044] Fix typos --- pyomo/contrib/pyros/tests/test_preprocessor.py | 4 ++-- pyomo/contrib/pyros/tests/test_separation.py | 2 +- pyomo/contrib/pyros/util.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index e3f28ecd313..39cba097932 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -1326,7 +1326,7 @@ def test_declare_objective_expressions_maximization_obj(self): def test_standardize_active_obj_worst_case_focus(self): """ - Test preprocesing step for standardization + Test preprocessing step for standardization of the active model objective. """ model_data = self.build_simple_test_model_data() @@ -1858,7 +1858,7 @@ def setup_test_model_data(self): def test_coefficient_matching_correct_constraints_added(self): """ Test coefficient matching adds correct number of constraints - in event of sucessful use. + in event of successful use. """ model_data = self.setup_test_model_data() m = model_data.working_model.user_model diff --git a/pyomo/contrib/pyros/tests/test_separation.py b/pyomo/contrib/pyros/tests/test_separation.py index 4eebf1df96f..16570256169 100644 --- a/pyomo/contrib/pyros/tests/test_separation.py +++ b/pyomo/contrib/pyros/tests/test_separation.py @@ -102,7 +102,7 @@ def test_construct_separation_problem_nonadj_components(self): for coeff_con in separation_model.first_stage.coefficient_matching_cons: self.assertFalse( coeff_con.active, - msg=f"Coefficient mathcing constraint {coeff_con.name!r} active.", + msg=f"Coefficient matching constraint {coeff_con.name!r} active.", ) def test_construct_separation_problem_ss_ineq_cons(self): diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 2c25a791371..804d5026fb0 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -2144,7 +2144,7 @@ def reformulate_state_var_independent_eq_cons(model_data, config): "the currently available expression analyzers " "and selected decision rules " f"(decision_rule_order={config.decision_rule_order}). " - "We are unable to write a coefficent matching reformulation " + "We are unable to write a coefficient matching reformulation " "of this constraint." "Recasting to two inequality constraints." ) @@ -2225,7 +2225,7 @@ def reformulate_state_var_independent_eq_cons(model_data, config): # remove rather than deactivate to facilitate: # - we no longer need this constraint anywhere - # - faciliates accurate counting of active constraints + # - facilitates accurate counting of active constraints del working_model.second_stage.equality_cons[con_idx] # we no longer need these auxiliary components From 392ec397cd635b6a262f878dff116ef702a05c54 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 11 Aug 2024 00:57:08 -0400 Subject: [PATCH 2173/3044] Tweak punctuation of phrase --- doc/OnlineDocs/contributed_packages/pyros.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index 0c40eb456b4..980aa2feb00 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -884,7 +884,7 @@ Observe that the log contains the following information: adjustable second-stage variables and state variables are included in parentheses, next to the total numbers of second-stage variables and state variables, respectively; - note that 'adjustable' has been abbreviated as 'adj.'. + note that "adjustable" has been abbreviated as "adj." * **Iteration log table** (lines 59--69). Summary information on the problem iterates and subproblem outcomes. The constituent columns are defined in detail in From 4c4fd0bb5f09f9e108e0aa5f251e19b7f953fb61 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 11 Aug 2024 16:07:52 -0400 Subject: [PATCH 2174/3044] Fix separation subsolver error testing --- pyomo/contrib/pyros/tests/test_grcs.py | 106 ++++++++++++++++++------- pyomo/contrib/pyros/util.py | 5 +- 2 files changed, 81 insertions(+), 30 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 2841910ece5..64272487821 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -20,6 +20,7 @@ import pyomo.common.unittest as unittest from pyomo.common.log import LoggingIntercept from pyomo.common.collections import Bunch +from pyomo.common.errors import InvalidValueError from pyomo.core.base.set_types import NonNegativeIntegers from pyomo.repn.plugins import nl_writer as pyomo_nl_writer from pyomo.common.dependencies import numpy as np, numpy_available @@ -1078,7 +1079,6 @@ def test_separation_subsolver_error(self): m.obj = Objective(expr=m.x1 + m.x2) box_set = BoxSet(bounds=[(0, 1)]) - d_set = DiscreteScenarioSet(scenarios=[(1,), (0,)]) local_solver = SolverFactory("ipopt") global_solver = SolverFactory("baron") @@ -1104,24 +1104,76 @@ def test_separation_subsolver_error(self): ), ) - # FIXME: This test is expected to fail now, as writing out invalid - # models generates an exception in the problem writer (and is never - # actually sent to the solver) - @unittest.skipUnless( - baron_license_is_valid, "Global NLP solver is not available and licensed." - ) - @unittest.expectedFailure + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") + @unittest.skipUnless(baron_license_is_valid, "BARON is not available and licensed.") def test_discrete_separation_subsolver_error(self): """ Test PyROS for two-stage problem with discrete type set, subsolver error status. """ + class BadSeparationSolver: + def __init__(self, solver): + self.solver = solver + + def available(self, exception_flag=False): + return self.solver.available(exception_flag=exception_flag) + + def solve(self, model, *args, **kwargs): + is_separation = hasattr(model, "uncertainty") + if is_separation: + res = SolverResults() + res.solver.termination_condition = TerminationCondition.unknown + else: + res = self.solver.solve(model, *args, **kwargs) + return res + + m = ConcreteModel() + + m.q = Param(initialize=1, mutable=True) + m.x1 = Var(initialize=1, bounds=(0, 1)) + m.x2 = Var(initialize=2, bounds=(0, m.q)) + m.obj = Objective(expr=m.x1 + m.x2, sense=maximize) + + discrete_set = DiscreteScenarioSet(scenarios=[(1,), (0,)]) + + local_solver = SolverFactory("ipopt") + global_solver = SolverFactory("baron") + pyros_solver = SolverFactory("pyros") + + with LoggingIntercept(level=logging.WARNING) as LOG: + res = pyros_solver.solve( + model=m, + first_stage_variables=[m.x1], + second_stage_variables=[m.x2], + uncertain_params=[m.q], + uncertainty_set=discrete_set, + local_solver=BadSeparationSolver(local_solver), + global_solver=BadSeparationSolver(global_solver), + decision_rule_order=1, + tee=True, + ) + + self.assertRegex(LOG.getvalue(), "Could not.*separation.*iteration 0.*") + self.assertEqual( + res.pyros_termination_condition, + pyrosTerminationCondition.subsolver_error, + ) + self.assertEqual(res.iterations, 1) + + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") + @unittest.skipUnless( + baron_license_is_valid, "Global NLP solver is not available and licensed." + ) + def test_discrete_separation_invalid_value_error(self): + """ + Test PyROS properly handles InvalidValueError. + """ m = ConcreteModel() m.q = Param(initialize=1, mutable=True) m.x1 = Var(initialize=1, bounds=(0, 1)) - # upper bound induces subsolver error: separation + # upper bound induces invalid value error: separation # max(x2 - log(m.q)) will force subsolver to q = 0 m.x2 = Var(initialize=2, bounds=(None, log(m.q))) @@ -1133,24 +1185,24 @@ def test_discrete_separation_subsolver_error(self): global_solver = SolverFactory("baron") pyros_solver = SolverFactory("pyros") - res = pyros_solver.solve( - model=m, - first_stage_variables=[m.x1], - second_stage_variables=[m.x2], - uncertain_params=[m.q], - uncertainty_set=discrete_set, - local_solver=local_solver, - global_solver=global_solver, - decision_rule_order=1, - tee=True, - ) - self.assertEqual( - res.pyros_termination_condition, - pyrosTerminationCondition.subsolver_error, - msg=( - "Returned termination condition for separation error" - f"test is not {pyrosTerminationCondition.subsolver_error}." - ), + with LoggingIntercept(level=logging.ERROR) as LOG: + with self.assertRaises(InvalidValueError): + pyros_solver.solve( + model=m, + first_stage_variables=[m.x1], + second_stage_variables=[m.x2], + uncertain_params=[m.q], + uncertainty_set=discrete_set, + local_solver=local_solver, + global_solver=global_solver, + decision_rule_order=1, + tee=True, + ) + + err_str = LOG.getvalue() + self.assertRegex( + err_str, + "Optimizer.*exception.*separation problem.*iteration 0" ) @unittest.skipUnless(ipopt_available, "IPOPT is not available.") diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 804d5026fb0..bee90ffc38e 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -25,7 +25,7 @@ from pyomo.common.collections import ComponentMap, ComponentSet from pyomo.common.dependencies import scipy as sp -from pyomo.common.errors import ApplicationError +from pyomo.common.errors import ApplicationError, InvalidValueError from pyomo.common.log import Preformatted from pyomo.common.modeling import unique_component_name from pyomo.common.timing import HierarchicalTimer, TicTocTimer @@ -35,7 +35,6 @@ Component, ConcreteModel, Constraint, - ConstraintList, Expression, Objective, maximize, @@ -2700,7 +2699,7 @@ def call_solver(model, solver, config, timing_obj, timer_name, err_msg): load_solutions=False, symbolic_solver_labels=config.symbolic_solver_labels, ) - except ApplicationError: + except (ApplicationError, InvalidValueError): # account for possible external subsolver errors # (such as segmentation faults, function evaluation # errors, etc.) From fcd73ca8374079af2e03983c7643c4493916e99a Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 11 Aug 2024 16:14:14 -0400 Subject: [PATCH 2175/3044] Remove unused solve data methods --- pyomo/contrib/pyros/solve_data.py | 156 ------------------------------ 1 file changed, 156 deletions(-) diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index 2c41c1ea579..1b59597d14a 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -200,31 +200,6 @@ def termination_acceptable(self, acceptable_terminations): for res in self.results_list ) - def evaluate_total_solve_time(self, evaluator_func, **evaluator_func_kwargs): - """ - Evaluate total time required by subordinate solvers - for separation problem of interest, according to Pyomo - ``SolverResults`` objects stored in ``self.results_list``. - - Parameters - ---------- - evaluator_func : callable - Solve time evaluator function. - This callable should accept an object of type - ``pyomo.opt.results.SolverResults``, and - return a float equal to the time required. - **evaluator_func_kwargs : dict, optional - Keyword arguments to evaluator function. - - Returns - ------- - float - Total time spent by solvers. - """ - return sum( - evaluator_func(res, **evaluator_func_kwargs) for res in self.results_list - ) - class DiscreteSeparationSolveCallResults: """ @@ -280,31 +255,6 @@ def subsolver_error(self): """ return any(res.subsolver_error for res in self.solver_call_results.values()) - def evaluate_total_solve_time(self, evaluator_func, **evaluator_func_kwargs): - """ - Evaluate total time required by subordinate solvers - for separation problem of interest. - - Parameters - ---------- - evaluator_func : callable - Solve time evaluator function. - This callable should accept an object of type - ``pyomo.opt.results.SolveResults``, and - return a float equal to the time required. - **evaluator_func_kwargs : dict, optional - Keyword arguments to evaluator function. - - Returns - ------- - float - Total time spent by solvers. - """ - return sum( - solver_call_res.evaluate_total_solve_time(evaluator_func) - for solver_call_res in self.solver_call_results.values() - ) - class SeparationLoopResults: """ @@ -471,31 +421,6 @@ def time_out(self): for solver_call_res in self.solver_call_results.values() ) - def evaluate_total_solve_time(self, evaluator_func, **evaluator_func_kwargs): - """ - Evaluate total time required by subordinate solvers - for separation problem of interest. - - Parameters - ---------- - evaluator_func : callable - Solve time evaluator function. - This callable should accept an object of type - ``pyomo.opt.results.SolveResults``, and - return a float equal to the time required. - **evaluator_func_kwargs : dict, optional - Keyword arguments to evaluator function. - - Returns - ------- - float - Total time spent by solvers. - """ - return sum( - res.evaluate_total_solve_time(evaluator_func) - for res in self.solver_call_results.values() - ) - class SeparationResults: """ @@ -699,60 +624,6 @@ def violated_second_stage_ineq_cons(self): """ return self.get_violating_attr("violated_second_stage_ineq_cons") - def evaluate_local_solve_time(self, evaluator_func, **evaluator_func_kwargs): - """ - Evaluate total time required by local subordinate solvers - for separation problem of interest. - - Parameters - ---------- - evaluator_func : callable - Solve time evaluator function. - This callable should accept an object of type - ``pyomo.opt.results.SolverResults``, and - return a float equal to the time required. - **evaluator_func_kwargs : dict, optional - Keyword arguments to evaluator function. - - Returns - ------- - float - Total time spent by local solvers. - """ - if self.solved_locally: - return self.local_separation_loop_results.evaluate_total_solve_time( - evaluator_func, **evaluator_func_kwargs - ) - else: - return 0 - - def evaluate_global_solve_time(self, evaluator_func, **evaluator_func_kwargs): - """ - Evaluate total time required by global subordinate solvers - for separation problem of interest. - - Parameters - ---------- - evaluator_func : callable - Solve time evaluator function. - This callable should accept an object of type - ``pyomo.opt.results.SolverResults``, and - return a float equal to the time required. - **evaluator_func_kwargs : dict, optional - Keyword arguments to evaluator function. - - Returns - ------- - float - Total time spent by global solvers. - """ - if self.solved_globally: - return self.global_separation_loop_results.evaluate_total_solve_time( - evaluator_func, **evaluator_func_kwargs - ) - else: - return 0 - @property def robustness_certified(self): """ @@ -780,30 +651,3 @@ def robustness_certified(self): is_robust = heuristically_robust return is_robust - - def generate_subsolver_results(self, include_local=True, include_global=True): - """ - Generate flattened sequence all Pyomo SolverResults objects - for all ``SeparationSolveCallResults`` objects listed in - the local and global ``SeparationLoopResults`` - attributes of `self`. - - Yields - ------ - pyomo.opt.SolverResults - """ - if include_local and self.local_separation_loop_results is not None: - all_local_call_results = ( - self.local_separation_loop_results.solver_call_results.values() - ) - for solve_call_res in all_local_call_results: - for res in solve_call_res.results_list: - yield res - - if include_global and self.global_separation_loop_results is not None: - all_global_call_results = ( - self.global_separation_loop_results.solver_call_results.values() - ) - for solve_call_res in all_global_call_results: - for res in solve_call_res.results_list: - yield res From 2bac6317ad3b32a3e4575d3bc0c63cfc15dfcf2b Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 11 Aug 2024 16:55:21 -0400 Subject: [PATCH 2176/3044] Reorganize main model data object --- pyomo/contrib/pyros/pyros.py | 5 ++--- pyomo/contrib/pyros/util.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 616191e66b8..cbbf52419fe 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -30,6 +30,7 @@ setup_pyros_logger, time_code, TimingData, + ModelData, ) @@ -321,9 +322,7 @@ def solve( Summary of PyROS termination outcome. """ - model_data = ROSolveResults() - model_data.timing = TimingData() - model_data.original_model = model + model_data = ModelData(original_model=model, timing=TimingData()) with time_code( timing_data_obj=model_data.timing, code_block_name="main", diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index bee90ffc38e..2811ef0f939 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -935,6 +935,37 @@ def validate_pyros_inputs(model, config): return user_var_partitioning +class ModelData: + """ + Container for modeling objects from which the PyROS + subproblems are constructed. + + Parameters + ---------- + original_model : ConcreteModel + Original user-provided model. + timing : TimingData + Main timing data object. + + Attributes + ---------- + original_model : ConcreteModel + Original user-provided model. + timing : TimingData + Main PyROS solver timing data object. + working_model : ConcreteModel + Preprocessed clone of `original_model` from which + the PyROS cutting set subproblems are to be + constructed. + """ + + def __init__(self, original_model, timing): + self.original_model = original_model + self.timing = timing + # working model will be addressed by preprocessing + self.working_model = None + + def get_var_bound_pairs(var): """ Get the domain and declared lower/upper From 69dc039e0b8aaa532b4f30f205b06042e58bd00d Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 11 Aug 2024 21:00:36 -0400 Subject: [PATCH 2177/3044] Ensure PyROS has working separation priority interface --- .../pyros/separation_problem_methods.py | 10 +- pyomo/contrib/pyros/tests/test_master.py | 4 +- .../contrib/pyros/tests/test_preprocessor.py | 103 ++++++++++++++++-- pyomo/contrib/pyros/tests/test_separation.py | 38 ++++++- pyomo/contrib/pyros/util.py | 48 ++++++-- 5 files changed, 173 insertions(+), 30 deletions(-) diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index ca21ff24a9c..3e14c9106d8 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -381,14 +381,11 @@ def group_ss_ineq_constraints_by_priority(separation_data, config): Keys are sorted in descending order (i.e. highest priority first). """ - all_ss_ineq_cons = list( - separation_data.separation_model.second_stage.inequality_cons.values() - ) + ss_ineq_cons = separation_data.separation_model.second_stage.inequality_cons separation_priority_groups = dict() - config_sep_priority_dict = config.separation_priority_order - for ss_ineq_con in all_ss_ineq_cons: + for name, ss_ineq_con in ss_ineq_cons.items(): # by default, priority set to 0 - priority = config_sep_priority_dict.get(ss_ineq_con.name, 0) + priority = separation_data.separation_priority_order[name] cons_with_same_priority = separation_priority_groups.setdefault(priority, []) cons_with_same_priority.append(ss_ineq_con) @@ -1198,6 +1195,7 @@ def __init__(self, model_data, config): """Initialize self (see class docstring).""" self.separation_model = construct_separation_problem(model_data, config) self.timing = model_data.timing + self.separation_priority_order = model_data.separation_priority_order.copy() self.iteration = 0 self.config = config self.points_added_to_master = {(0, 0): config.nominal_uncertain_param_vals} diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index f23a4aec2dd..d41ea6112f6 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -35,6 +35,7 @@ MasterProblemData, ) from pyomo.contrib.pyros.util import ( + ModelData, preprocess_model_data, ObjectiveType, time_code, @@ -75,8 +76,9 @@ def build_simple_model_data(objective_focus="worst_case"): decision_rule_order=1, progress_logger=logger, nominal_uncertain_param_vals=[0.4], + separation_priority_order=dict(), ) - model_data = Bunch(original_model=m) + model_data = ModelData(original_model=m, timing=TimingData()) user_var_partitioning = VariablePartitioning( first_stage_variables=[m.x1], second_stage_variables=[m.x2, m.x3], diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index 39cba097932..7796c1b9b04 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -39,6 +39,7 @@ from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.contrib.pyros.util import ( + ModelData, ObjectiveType, get_effective_var_partitioning, get_var_certain_uncertain_bounds, @@ -595,6 +596,7 @@ def build_simple_test_model_data(self): model_data.working_model.second_stage = Block() model_data.working_model.second_stage.inequality_cons = Constraint(Any) model_data.working_model.second_stage.equality_cons = Constraint(Any) + model_data.separation_priority_order = dict() return model_data @@ -639,7 +641,7 @@ def test_turn_nonadjustable_bounds_to_constraints(self): ) ) - turn_nonadjustable_var_bounds_to_constraints(model_data) + turn_nonadjustable_var_bounds_to_constraints(model_data, config=Bunch()) for var, (orig_domain, orig_bounds) in original_var_domains_and_bounds.items(): # all var domains should remain unchanged @@ -746,6 +748,17 @@ def test_turn_nonadjustable_bounds_to_constraints(self): msg="Number of second-stage equalities not as expected.", ) + # check separation priorities + for con_name in second_stage.inequality_cons: + self.assertEqual( + model_data.separation_priority_order[con_name], + 0, + msg=( + f"Separation priority for entry {con_name!r} of second-stage " + "inequalities not as expected." + ), + ) + def test_turn_adjustable_bounds_to_constraints(self): """ Test subroutine for reformulating domains and bounds @@ -774,7 +787,7 @@ def test_turn_adjustable_bounds_to_constraints(self): for var in model_data.working_model.user_model.component_data_objects(Var) ) - turn_adjustable_var_bounds_to_constraints(model_data) + turn_adjustable_var_bounds_to_constraints(model_data, config=Bunch()) for var, (orig_domain, orig_bounds) in original_var_domains_and_bounds.items(): _, (final_lb, final_ub) = get_var_bound_pairs(var) @@ -843,6 +856,9 @@ def test_turn_adjustable_bounds_to_constraints(self): second_stage = model_data.working_model.second_stage + self.assertEqual(len(second_stage.inequality_cons), 10) + self.assertEqual(len(second_stage.equality_cons), 5) + # verify bound constraint expressions assertExpressionsEqual( self, @@ -920,8 +936,16 @@ def test_turn_adjustable_bounds_to_constraints(self): m.z8 <= m.q2, ) - self.assertEqual(len(second_stage.inequality_cons), 10) - self.assertEqual(len(second_stage.equality_cons), 5) + # check separation priorities + for con_name in second_stage.inequality_cons: + self.assertEqual( + model_data.separation_priority_order[con_name], + 0, + msg=( + f"Separation priority for entry {con_name!r} of second-stage " + "inequalities not as expected." + ), + ) class TestStandardizeInequalityConstraints(unittest.TestCase): @@ -988,6 +1012,8 @@ def build_simple_test_model_data(self): ep.second_stage_variables = [m.z1, m.z2] ep.state_variables = [m.y1] + model_data.separation_priority_order = dict() + return model_data def test_standardize_inequality_constraints(self): @@ -998,7 +1024,10 @@ def test_standardize_inequality_constraints(self): working_model = model_data.working_model m = working_model.user_model - standardize_inequality_constraints(model_data) + standardize_inequality_constraints( + model_data, + config=Bunch(separation_priority_order=dict(c3=1, c5=2)), + ) fs_ineq_cons = working_model.first_stage.inequality_cons ss_ineq_cons = working_model.second_stage.inequality_cons @@ -1007,7 +1036,6 @@ def test_standardize_inequality_constraints(self): self.assertEqual(len(ss_ineq_cons), 12) self.assertFalse(m.c1.active) - fs_ineq_cons.pprint() new_c1_con = fs_ineq_cons["ineq_con_c1"] self.assertTrue(new_c1_con.active) assertExpressionsEqual(self, new_c1_con.expr, m.x1 <= 1) @@ -1025,6 +1053,7 @@ def test_standardize_inequality_constraints(self): new_c3_con = ss_ineq_cons["ineq_con_c3_lower_bound_con"] self.assertTrue(new_c3_con.active) assertExpressionsEqual(self, new_c3_con.expr, -m.x1 <= -m.q) + self.assertEqual(model_data.separation_priority_order[new_c3_con.index()], 1) # log(m.p) <= m.x2 <= m.q # lower bound is first-stage, upper bound second-stage @@ -1045,6 +1074,12 @@ def test_standardize_inequality_constraints(self): self.assertTrue(new_c5_lower_bound_con.active) assertExpressionsEqual(self, new_c5_lower_bound_con.expr, -m.x2 <= -m.q) assertExpressionsEqual(self, new_c5_upper_bound_con.expr, m.x2 <= 2 * m.q) + self.assertEqual( + model_data.separation_priority_order[new_c5_lower_bound_con.index()], 2 + ) + self.assertEqual( + model_data.separation_priority_order[new_c5_upper_bound_con.index()], 2 + ) # single second-stage inequality self.assertFalse(m.c6.active) @@ -1099,6 +1134,18 @@ def test_standardize_inequality_constraints(self): assertExpressionsEqual(self, new_c12_lower_bound_con.expr, -m.x1 <= -m.q**2) assertExpressionsEqual(self, new_c12_upper_bound_con.expr, m.x1 <= sin(m.p)) + # check separation priorities + for con_name in ss_ineq_cons: + if "c3" not in con_name and "c5" not in con_name: + self.assertEqual( + model_data.separation_priority_order[con_name], + 0, + msg=( + f"Separation priority for entry {con_name!r} of second-stage " + "inequalities not as expected." + ), + ) + def test_standardize_inequality_error(self): """ Test exception raised by inequality constraint standardization @@ -1113,7 +1160,9 @@ def test_standardize_inequality_error(self): exc_str = r"Found an equality bound.*1.0.*for the constraint.*c6'" with self.assertRaisesRegex(ValueError, exc_str): - standardize_inequality_constraints(model_data) + standardize_inequality_constraints( + model_data, Bunch(separation_priority_order=dict()), + ) class TestStandardizeEqualityConstraints(unittest.TestCase): @@ -1277,6 +1326,8 @@ def build_simple_test_model_data(self): model_data.working_model.second_stage = Block() model_data.working_model.second_stage.inequality_cons = Constraint(Any) + model_data.separation_priority_order = dict() + return model_data def test_declare_objective_expressions(self): @@ -1351,6 +1402,7 @@ def test_standardize_active_obj_worst_case_focus(self): working_model.second_stage.inequality_cons["epigraph_con"].expr, m.obj1.expr - working_model.first_stage.epigraph_var <= 0, ) + self.assertEqual(model_data.separation_priority_order["epigraph_con"], 0) def test_standardize_active_obj_nominal_focus(self): """ @@ -1379,6 +1431,7 @@ def test_standardize_active_obj_nominal_focus(self): working_model.first_stage.inequality_cons["epigraph_con"].expr, m.obj1.expr - working_model.first_stage.epigraph_var <= 0, ) + self.assertNotIn("epigraph_con", model_data.separation_priority_order) def test_standardize_active_obj_unsupported_focus(self): """ @@ -1432,6 +1485,7 @@ def test_standardize_active_obj_nonadjustable_max(self): working_model.first_stage.inequality_cons["epigraph_con"].expr, -m.obj2.expr - working_model.first_stage.epigraph_var <= 0, ) + self.assertNotIn("epigraph_con", model_data.separation_priority_order) class TestAddDecisionRuleVars(unittest.TestCase): @@ -1923,6 +1977,7 @@ def test_reformulate_nonlinear_state_var_independent_eq_con(self): and recasting of nonlinear constraints to opposing equalities. """ model_data = self.setup_test_model_data() + model_data.separation_priority_order = dict() config = Bunch() config.decision_rule_order = 1 @@ -2008,6 +2063,16 @@ def test_reformulate_nonlinear_state_var_independent_eq_con(self): (-1) + m.x1 == 0, ) + # separation priorities were also updated + self.assertEqual( + model_data.separation_priority_order["reform_lower_bound_from_eq_con"], + 0, + ) + self.assertEqual( + model_data.separation_priority_order["reform_upper_bound_from_eq_con"], + 0, + ) + def test_coefficient_matching_robust_infeasible_proof(self): """ Test coefficient matching detects robust infeasibility @@ -2063,8 +2128,7 @@ def build_test_model_data(self): """ Build model data object for the preprocessor. """ - model_data = Bunch() - model_data.original_model = m = ConcreteModel() + m = ConcreteModel() # PARAMS: p uncertain, q certain m.p = Param(initialize=2, mutable=True) @@ -2143,6 +2207,8 @@ def build_test_model_data(self): ) ) + model_data = ModelData(original_model=m, timing=None) + # set up the var partitioning user_var_partitioning = VariablePartitioning( first_stage_variables=[m.x1, m.x2], @@ -2166,6 +2232,7 @@ def test_preprocessor_effective_var_partitioning_static_dr(self): objective_focus=ObjectiveType.worst_case, decision_rule_order=0, progress_logger=logger, + separation_priority_order=dict(), ) preprocess_model_data(model_data, config, user_var_partitioning) ep = model_data.working_model.effective_var_partitioning @@ -2229,6 +2296,7 @@ def test_preprocessor_effective_var_partitioning_nonstatic_dr(self, name, dr_ord objective_focus=ObjectiveType.worst_case, decision_rule_order=dr_order, progress_logger=logger, + separation_priority_order=dict(), ) preprocess_model_data(model_data, config, user_var_partitioning) ep = model_data.working_model.effective_var_partitioning @@ -2295,6 +2363,7 @@ def test_preprocessor_constraint_partitioning_nonstatic_dr( objective_focus=ObjectiveType[obj_focus], decision_rule_order=dr_order, progress_logger=logger, + separation_priority_order=dict(ineq3=2), ) preprocess_model_data(model_data, config, user_var_partitioning) @@ -2459,6 +2528,18 @@ def test_preprocessor_constraint_partitioning_nonstatic_dr( # user model block should have no active constraints self.assertFalse(list(m.component_data_objects(Constraint, active=True))) + # check separation priorities + for con_name, order in model_data.separation_priority_order.items(): + expected_order = 2 if "ineq3" in con_name else 0 + self.assertEqual( + order, + expected_order, + msg=( + "Separation priority order for second-stage inequality " + f"{con_name!r} not as expected." + ), + ) + @parameterized.expand( [["static", 0, True], ["affine", 1, False], ["quadratic", 2, False]] ) @@ -2475,6 +2556,7 @@ def test_preprocessor_coefficient_matching( objective_focus=ObjectiveType.worst_case, decision_rule_order=dr_order, progress_logger=logger, + separation_priority_order=dict(), ) # for static DR, problem should be robust infeasible @@ -2557,6 +2639,7 @@ def test_preprocessor_objective_standardization(self, name, dr_order): objective_focus=ObjectiveType.worst_case, decision_rule_order=dr_order, progress_logger=logger, + separation_priority_order=dict(), ) preprocess_model_data(model_data, config, user_var_partitioning) @@ -2605,6 +2688,7 @@ def test_preprocessor_log_model_statistics_affine_dr(self, obj_focus): objective_focus=ObjectiveType[obj_focus], decision_rule_order=1, progress_logger=logger, + separation_priority_order=dict(), ) preprocess_model_data(model_data, config, user_var_partitioning) @@ -2655,6 +2739,7 @@ def test_preprocessor_log_model_statistics_quadratic_dr(self, obj_focus): objective_focus=ObjectiveType[obj_focus], decision_rule_order=2, progress_logger=logger, + separation_priority_order=dict(), ) preprocess_model_data(model_data, config, user_var_partitioning) diff --git a/pyomo/contrib/pyros/tests/test_separation.py b/pyomo/contrib/pyros/tests/test_separation.py index 16570256169..a19b55e3a68 100644 --- a/pyomo/contrib/pyros/tests/test_separation.py +++ b/pyomo/contrib/pyros/tests/test_separation.py @@ -23,9 +23,13 @@ from pyomo.core.expr import exp, RangedExpression from pyomo.core.expr.compare import assertExpressionsEqual -from pyomo.contrib.pyros.separation_problem_methods import construct_separation_problem +from pyomo.contrib.pyros.separation_problem_methods import ( + construct_separation_problem, + group_ss_ineq_constraints_by_priority, +) from pyomo.contrib.pyros.uncertainty_sets import BoxSet, FactorModelSet from pyomo.contrib.pyros.util import ( + ModelData, preprocess_model_data, ObjectiveType, VariablePartitioning, @@ -63,8 +67,9 @@ def build_simple_model_data(objective_focus="worst_case"): progress_logger=logger, nominal_uncertain_param_vals=[0.5, 0], uncertainty_set=BoxSet([[0, 1], [0, 0]]), + separation_priority_order=dict(con=2), ) - model_data = Bunch(original_model=m) + model_data = ModelData(original_model=m, timing=None) user_var_partitioning = VariablePartitioning( first_stage_variables=[m.x1], second_stage_variables=[m.x2, m.x3], @@ -267,5 +272,34 @@ def test_construct_separation_problem_uncertain_factor_param_components(self): self.assertEqual(paramvar2.bounds, (-1.0, 1.0)) +class TestGroupSecondStageIneqConsByPriority(unittest.TestCase): + def test_group_ss_ineq_constraints_by_priority(self): + model_data, config = build_simple_model_data() + separation_model = construct_separation_problem(model_data, config) + + # build mock separation data-like object + # since we are testing only the grouping method + separation_data = Bunch( + separation_model=separation_model, + separation_priority_order=model_data.separation_priority_order + ) + + priority_groups = group_ss_ineq_constraints_by_priority(separation_data, config) + + self.assertEqual(list(priority_groups.keys()), [2, 0]) + ss_ineq_cons = separation_model.second_stage.inequality_cons + self.assertEqual( + priority_groups[2], [ss_ineq_cons["ineq_con_con_upper_bound_con"]] + ) + self.assertEqual( + priority_groups[0], + [ + ss_ineq_cons["var_x3_certain_lower_bound_con"], + ss_ineq_cons["var_x3_certain_upper_bound_con"], + ss_ineq_cons["epigraph_con"], + ] + ) + + if __name__ == "__main__": unittest.main() diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 2811ef0f939..1493c56e2c2 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -64,8 +64,10 @@ COEFF_MATCH_ABS_TOL = 0 ABS_CON_CHECK_FEAS_TOL = 1e-5 PRETRIANGULAR_VAR_COEFF_TOL = 1e-6 + TIC_TOC_SOLVE_TIME_ATTR = "pyros_tic_toc_time" DEFAULT_LOGGER_NAME = "pyomo.contrib.pyros" +DEFAULT_SEPARATION_PRIORITY = 0 class TimingData: @@ -957,6 +959,9 @@ class ModelData: Preprocessed clone of `original_model` from which the PyROS cutting set subproblems are to be constructed. + separation_priority_order : dict + Mapping from contraint names to separation priority + values. """ def __init__(self, original_model, timing): @@ -964,6 +969,7 @@ def __init__(self, original_model, timing): self.timing = timing # working model will be addressed by preprocessing self.working_model = None + self.separation_priority_order = dict() def get_var_bound_pairs(var): @@ -1438,7 +1444,7 @@ def remove_all_var_bounds(var): var.domain = Reals -def turn_nonadjustable_var_bounds_to_constraints(model_data): +def turn_nonadjustable_var_bounds_to_constraints(model_data, config): """ Reformulate uncertain bounds for the nonadjustable (i.e. effective first-stage) variables of the working @@ -1482,6 +1488,10 @@ def turn_nonadjustable_var_bounds_to_constraints(model_data): working_model.second_stage.inequality_cons[new_con_name] = ( new_con_expr ) + # can't specify custom priorities for variable bounds + model_data.separation_priority_order[new_con_name] = ( + DEFAULT_SEPARATION_PRIORITY + ) # for subsequent developments: return a mapping # from each variable to the corresponding binding constraints? @@ -1489,7 +1499,7 @@ def turn_nonadjustable_var_bounds_to_constraints(model_data): # the interface for separation priority ordering -def turn_adjustable_var_bounds_to_constraints(model_data): +def turn_adjustable_var_bounds_to_constraints(model_data, config): """ Reformulate domain and declared bounds for the adjustable (i.e., effective second-stage and effective state) @@ -1539,6 +1549,11 @@ def turn_adjustable_var_bounds_to_constraints(model_data): working_model.second_stage.inequality_cons[new_con_name] = ( new_con_expr ) + # no custom separation priorities for Var + # bound constraints + model_data.separation_priority_order[new_con_name] = ( + DEFAULT_SEPARATION_PRIORITY + ) remove_all_var_bounds(var) @@ -1617,7 +1632,7 @@ def setup_working_model(model_data, config, user_var_partitioning): working_model.original_active_inequality_cons.append(con) -def standardize_inequality_constraints(model_data): +def standardize_inequality_constraints(model_data, config): """ Standardize the inequality constraints of the working model, and classify them as first-stage inequalities or second-stage @@ -1681,6 +1696,12 @@ def standardize_inequality_constraints(model_data): working_model.second_stage.inequality_cons[new_con_name] = ( std_con_expr ) + # account for user-specified priority specifications + model_data.separation_priority_order[new_con_name] = ( + config.separation_priority_order.get( + con_rel_name, DEFAULT_SEPARATION_PRIORITY + ) + ) else: # we do not want to modify the arrangement of # lower bound for first-stage inequalities, so @@ -1703,11 +1724,6 @@ def standardize_inequality_constraints(model_data): ) con.deactivate() - # for subsequent developments: map the original constraints - # to the derived second-stage inequalities? - # we will add this as needed when changes are made to - # the interface for separation priority ordering - def standardize_equality_constraints(model_data): """ @@ -1908,6 +1924,9 @@ def standardize_active_objective(model_data, config): - working_model.first_stage.epigraph_var <= 0 ) + model_data.separation_priority_order["epigraph_con"] = ( + DEFAULT_SEPARATION_PRIORITY + ) elif config.objective_focus == ObjectiveType.nominal: working_model.first_stage.inequality_cons["epigraph_con"] = ( working_model.full_objective.expr @@ -2188,9 +2207,14 @@ def reformulate_state_var_independent_eq_cons(model_data, config): std_con_expr = create_bound_constraint_expr( expr=con.body, bound=con.upper, bound_type=bound_type ) + new_con_name = f"reform_{bound_type}_bound_from_{con_idx}" working_model.second_stage.inequality_cons[ - f"reform_{bound_type}_bound_from_{con_idx}" + new_con_name ] = std_con_expr + # no custom priorities specified + model_data.separation_priority_order[new_con_name] = ( + DEFAULT_SEPARATION_PRIORITY + ) else: polynomial_repn_coeffs = ( [expr_repn.constant] @@ -2296,11 +2320,11 @@ def preprocess_model_data(model_data, config, user_var_partitioning): # different treatment for effective first-stage # than for effective second-stage and state variables config.progress_logger.debug("Turning some variable bounds to constraints...") - turn_nonadjustable_var_bounds_to_constraints(model_data) - turn_adjustable_var_bounds_to_constraints(model_data) + turn_nonadjustable_var_bounds_to_constraints(model_data, config) + turn_adjustable_var_bounds_to_constraints(model_data, config) config.progress_logger.debug("Standardizing the model constraints...") - standardize_inequality_constraints(model_data) + standardize_inequality_constraints(model_data, config) standardize_equality_constraints(model_data) # includes epigraph reformulation From 034677126c2b9389f1f8f10a31885b2842c86864 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 11 Aug 2024 21:03:23 -0400 Subject: [PATCH 2178/3044] Apply black --- pyomo/contrib/pyros/tests/test_grcs.py | 7 +++---- pyomo/contrib/pyros/tests/test_preprocessor.py | 11 ++++------- pyomo/contrib/pyros/tests/test_separation.py | 4 ++-- pyomo/contrib/pyros/util.py | 6 +++--- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 64272487821..f50f3de91dc 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -1111,6 +1111,7 @@ def test_discrete_separation_subsolver_error(self): Test PyROS for two-stage problem with discrete type set, subsolver error status. """ + class BadSeparationSolver: def __init__(self, solver): self.solver = solver @@ -1155,8 +1156,7 @@ def solve(self, model, *args, **kwargs): self.assertRegex(LOG.getvalue(), "Could not.*separation.*iteration 0.*") self.assertEqual( - res.pyros_termination_condition, - pyrosTerminationCondition.subsolver_error, + res.pyros_termination_condition, pyrosTerminationCondition.subsolver_error ) self.assertEqual(res.iterations, 1) @@ -1201,8 +1201,7 @@ def test_discrete_separation_invalid_value_error(self): err_str = LOG.getvalue() self.assertRegex( - err_str, - "Optimizer.*exception.*separation problem.*iteration 0" + err_str, "Optimizer.*exception.*separation problem.*iteration 0" ) @unittest.skipUnless(ipopt_available, "IPOPT is not available.") diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index 7796c1b9b04..fbdd7e699d8 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -1025,8 +1025,7 @@ def test_standardize_inequality_constraints(self): m = working_model.user_model standardize_inequality_constraints( - model_data, - config=Bunch(separation_priority_order=dict(c3=1, c5=2)), + model_data, config=Bunch(separation_priority_order=dict(c3=1, c5=2)) ) fs_ineq_cons = working_model.first_stage.inequality_cons @@ -1161,7 +1160,7 @@ def test_standardize_inequality_error(self): exc_str = r"Found an equality bound.*1.0.*for the constraint.*c6'" with self.assertRaisesRegex(ValueError, exc_str): standardize_inequality_constraints( - model_data, Bunch(separation_priority_order=dict()), + model_data, Bunch(separation_priority_order=dict()) ) @@ -2065,12 +2064,10 @@ def test_reformulate_nonlinear_state_var_independent_eq_con(self): # separation priorities were also updated self.assertEqual( - model_data.separation_priority_order["reform_lower_bound_from_eq_con"], - 0, + model_data.separation_priority_order["reform_lower_bound_from_eq_con"], 0 ) self.assertEqual( - model_data.separation_priority_order["reform_upper_bound_from_eq_con"], - 0, + model_data.separation_priority_order["reform_upper_bound_from_eq_con"], 0 ) def test_coefficient_matching_robust_infeasible_proof(self): diff --git a/pyomo/contrib/pyros/tests/test_separation.py b/pyomo/contrib/pyros/tests/test_separation.py index a19b55e3a68..2310fd381ad 100644 --- a/pyomo/contrib/pyros/tests/test_separation.py +++ b/pyomo/contrib/pyros/tests/test_separation.py @@ -281,7 +281,7 @@ def test_group_ss_ineq_constraints_by_priority(self): # since we are testing only the grouping method separation_data = Bunch( separation_model=separation_model, - separation_priority_order=model_data.separation_priority_order + separation_priority_order=model_data.separation_priority_order, ) priority_groups = group_ss_ineq_constraints_by_priority(separation_data, config) @@ -297,7 +297,7 @@ def test_group_ss_ineq_constraints_by_priority(self): ss_ineq_cons["var_x3_certain_lower_bound_con"], ss_ineq_cons["var_x3_certain_upper_bound_con"], ss_ineq_cons["epigraph_con"], - ] + ], ) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 1493c56e2c2..5a1278f1618 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -2208,9 +2208,9 @@ def reformulate_state_var_independent_eq_cons(model_data, config): expr=con.body, bound=con.upper, bound_type=bound_type ) new_con_name = f"reform_{bound_type}_bound_from_{con_idx}" - working_model.second_stage.inequality_cons[ - new_con_name - ] = std_con_expr + working_model.second_stage.inequality_cons[new_con_name] = ( + std_con_expr + ) # no custom priorities specified model_data.separation_priority_order[new_con_name] = ( DEFAULT_SEPARATION_PRIORITY From 3f594655b57cf2d1fdbfc27b96cfa0356128d3ae Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 11 Aug 2024 22:05:23 -0400 Subject: [PATCH 2179/3044] Fold config into model data objects --- pyomo/contrib/pyros/master_problem_methods.py | 72 +++---- pyomo/contrib/pyros/pyros.py | 13 +- .../contrib/pyros/pyros_algorithm_methods.py | 9 +- .../pyros/separation_problem_methods.py | 84 ++++---- pyomo/contrib/pyros/tests/test_master.py | 88 ++++---- .../contrib/pyros/tests/test_preprocessor.py | 191 ++++++++---------- pyomo/contrib/pyros/tests/test_separation.py | 34 ++-- pyomo/contrib/pyros/util.py | 99 +++++---- 8 files changed, 277 insertions(+), 313 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index eb51afdf08a..ff43b9442c3 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -43,7 +43,7 @@ ) -def construct_initial_master_problem(model_data, config): +def construct_initial_master_problem(model_data): """ Construct the initial master problem model object from the preprocessed working model. @@ -53,8 +53,6 @@ def construct_initial_master_problem(model_data, config): model_data : model data object Main model data object, containing the preprocessed working model. - config : ConfigDict - PyROS solver settings. Returns ------- @@ -68,7 +66,7 @@ def construct_initial_master_problem(model_data, config): add_scenario_block_to_master_problem( master_model=master_model, scenario_idx=(0, 0), - param_realization=config.nominal_uncertain_param_vals, + param_realization=model_data.config.nominal_uncertain_param_vals, from_block=model_data.working_model, clone_first_stage_components=True, ) @@ -147,7 +145,7 @@ def add_scenario_block_to_master_problem( con.deactivate() -def construct_master_feasibility_problem(master_data, config): +def construct_master_feasibility_problem(master_data): """ Construct slack variable minimization problem from the master model. @@ -160,8 +158,6 @@ def construct_master_feasibility_problem(master_data, config): ---------- master_data : MasterProblemData Master problem data. - config : ConfigDict - PyROS solver options. Returns ------- @@ -260,7 +256,7 @@ def construct_master_feasibility_problem(master_data, config): return slack_model -def solve_master_feasibility_problem(master_data, config): +def solve_master_feasibility_problem(master_data): """ Solve a slack variable-based feasibility model derived from the master problem. Initialize the master problem @@ -271,18 +267,17 @@ def solve_master_feasibility_problem(master_data, config): ---------- master_data : MasterProblemData Master problem data. - config : ConfigDict - PyROS solver settings. Returns ------- results : SolverResults Solver results. """ - model = construct_master_feasibility_problem(master_data, config) + model = construct_master_feasibility_problem(master_data) active_obj = next(model.component_data_objects(Objective, active=True)) + config = master_data.config config.progress_logger.debug("Solving master feasibility problem") config.progress_logger.debug( f" Initial objective (total slack): {value(active_obj)}" @@ -340,7 +335,7 @@ def solve_master_feasibility_problem(master_data, config): return results -def construct_dr_polishing_problem(master_data, config): +def construct_dr_polishing_problem(master_data): """ Construct DR polishing problem from the master problem. @@ -348,8 +343,6 @@ def construct_dr_polishing_problem(master_data, config): ---------- master_data : MasterProblemData Master problem data. - config : ConfigDict - PyROS solver settings. Returns ------- @@ -514,7 +507,7 @@ def construct_dr_polishing_problem(master_data, config): return polishing_model -def minimize_dr_vars(master_data, config): +def minimize_dr_vars(master_data): """ Polish decision rule of most recent master problem solution. @@ -522,8 +515,6 @@ def minimize_dr_vars(master_data, config): ---------- master_data : MasterProblemData Master problem data. - config : ConfigDict - PyROS solver settings. Returns ------- @@ -533,8 +524,10 @@ def minimize_dr_vars(master_data, config): True if polishing model was solved to acceptable level, False otherwise. """ + config = master_data.config + # create polishing NLP - polishing_model = construct_dr_polishing_problem(master_data, config) + polishing_model = construct_dr_polishing_problem(master_data) if config.solve_master_globally: solver = config.global_solver @@ -614,7 +607,7 @@ def minimize_dr_vars(master_data, config): return results, True -def get_master_dr_degree(master_data, config): +def get_master_dr_degree(master_data): """ Determine DR polynomial degree to enforce based on the iteration number. @@ -630,8 +623,6 @@ def get_master_dr_degree(master_data, config): ---------- master_data : MasterProblemData Master problem data. - config : ConfigDict - PyROS solver options. Returns ------- @@ -640,13 +631,13 @@ def get_master_dr_degree(master_data, config): """ if master_data.iteration == 0: return 0 - elif master_data.iteration <= len(config.uncertain_params): - return min(1, config.decision_rule_order) + elif master_data.iteration <= len(master_data.config.uncertain_params): + return min(1, master_data.config.decision_rule_order) else: - return min(2, config.decision_rule_order) + return min(2, master_data.config.decision_rule_order) -def higher_order_decision_rule_efficiency(master_data, config): +def higher_order_decision_rule_efficiency(master_data): """ Enforce DR coefficient variable efficiencies for master problem-like formulation. @@ -655,8 +646,6 @@ def higher_order_decision_rule_efficiency(master_data, config): ---------- master_data : MasterProblemData Master problem data. - config : ConfigDict - PyROS solver options. Note ---- @@ -666,10 +655,10 @@ def higher_order_decision_rule_efficiency(master_data, config): to be set depends on the iteration number; see ``get_master_dr_degree``. """ - order_to_enforce = get_master_dr_degree(master_data, config) + order_to_enforce = get_master_dr_degree(master_data) enforce_dr_degree( working_blk=master_data.master_model.scenarios[0, 0], - config=config, + config=master_data.config, degree=order_to_enforce, ) @@ -704,7 +693,7 @@ def log_master_solve_results(master_model, config, results, desc="Optimized"): ) -def solver_call_master(master_data, config, master_soln): +def solver_call_master(master_data, master_soln): """ Invoke subsolver(s) on PyROS master problem, and update the MasterResults object accordingly. @@ -713,12 +702,11 @@ def solver_call_master(master_data, config, master_soln): ---------- master_data : MasterProblemData Container for current master problem and related data. - config : ConfigDict - PyROS solver settings. master_soln : MasterResults Master problem results object. May be empty or contain master feasibility problem results. """ + config = master_data.config master_model = master_data.master_model solver_term_cond_dict = {} @@ -731,7 +719,7 @@ def solver_call_master(master_data, config, master_soln): config.progress_logger.debug("Solving master problem") nominal_block = master_model.scenarios[0, 0] - higher_order_decision_rule_efficiency(master_data, config) + higher_order_decision_rule_efficiency(master_data) for idx, opt in enumerate(solvers): if idx > 0: @@ -851,7 +839,7 @@ def solver_call_master(master_data, config, master_soln): ) -def solve_master(master_data, config): +def solve_master(master_data): """ Solve the master problem """ @@ -859,12 +847,12 @@ def solve_master(master_data, config): # no master feas problem for iteration 0 if master_data.iteration > 0: - results = solve_master_feasibility_problem(master_data, config) + results = solve_master_feasibility_problem(master_data) master_soln.feasibility_problem_results = results # if pyros time limit reached, load time out status # to master results and return to caller - if check_time_limit_reached(master_data.timing, config): + if check_time_limit_reached(master_data.timing, master_data.config): # load master model master_soln.master_model = master_data.master_model master_soln.nominal_block = master_data.master_model.scenarios[0, 0] @@ -881,7 +869,7 @@ def solve_master(master_data, config): ) return master_soln - solver_call_master(master_data=master_data, config=config, master_soln=master_soln) + solver_call_master(master_data=master_data, master_soln=master_soln) return master_soln @@ -891,23 +879,23 @@ class MasterProblemData: Container for objects pertaining to the PyROS master problem. """ - def __init__(self, model_data, config): + def __init__(self, model_data): """Initialize self (see docstring).""" - self.master_model = construct_initial_master_problem(model_data, config) + self.master_model = construct_initial_master_problem(model_data) # we track the original model name for serialization purposes self.original_model_name = model_data.original_model.name self.iteration = 0 self.timing = model_data.timing - self.config = config + self.config = model_data.config def solve_master(self): """ Solve the master problem. """ - return solve_master(self, self.config) + return solve_master(self) def solve_dr_polishing(self): """ Solve the DR polishing problem. """ - return minimize_dr_vars(self, self.config) + return minimize_dr_vars(self) diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index cbbf52419fe..055c7c54556 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -25,7 +25,6 @@ pyrosTerminationCondition, validate_pyros_inputs, log_model_statistics, - preprocess_model_data, IterationLogRecord, setup_pyros_logger, time_code, @@ -322,7 +321,7 @@ def solve( Summary of PyROS termination outcome. """ - model_data = ModelData(original_model=model, timing=TimingData()) + model_data = ModelData(original_model=model, timing=TimingData(), config=None) with time_code( timing_data_obj=model_data.timing, code_block_name="main", @@ -364,12 +363,11 @@ def solve( exclude_options=None, level=logging.INFO, ) + model_data.config = config config.progress_logger.info("Preprocessing...") model_data.timing.start_timer("main.preprocessing") - robust_infeasible = preprocess_model_data( - model_data, config, user_var_partitioning - ) + robust_infeasible = model_data.preprocess(user_var_partitioning) model_data.timing.stop_timer("main.preprocessing") preprocessing_time = model_data.timing.get_total_time("main.preprocessing") config.progress_logger.info( @@ -377,12 +375,12 @@ def solve( f"{preprocessing_time:.3f}s." ) - log_model_statistics(model_data, config) + log_model_statistics(model_data) # === Solve and load solution into model return_soln = ROSolveResults() if not robust_infeasible: - pyros_soln = ROSolver_iterative_solve(model_data, config) + pyros_soln = ROSolver_iterative_solve(model_data) IterationLogRecord.log_header_rule(config.progress_logger.info) termination_acceptable = pyros_soln.pyros_termination_condition in { @@ -393,7 +391,6 @@ def solve( load_final_solution( model_data=model_data, master_soln=pyros_soln.master_results, - config=config, original_user_var_partitioning=user_var_partitioning, ) diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 913e81267e7..6317c308950 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -123,7 +123,7 @@ def evaluate_variable_shifts(current_var_data, previous_var_data, initial_var_da return tuple(var_shifts) -def ROSolver_iterative_solve(model_data, config): +def ROSolver_iterative_solve(model_data): """ Solve an RO problem with the iterative GRCS algorithm. @@ -132,16 +132,15 @@ def ROSolver_iterative_solve(model_data, config): model_data : model data object Model data object, equipped with the fully preprocessed working model. - config : ConfigDict - PyROS solver options Returns ------- GRCSResults Iterative solve results. """ - master_data = mp_methods.MasterProblemData(model_data, config) - separation_data = sp_methods.SeparationProblemData(model_data, config) + config = model_data.config + master_data = mp_methods.MasterProblemData(model_data) + separation_data = sp_methods.SeparationProblemData(model_data) # set up first-stage variable and DR variable sets nominal_master_blk = master_data.master_model.scenarios[0, 0] diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index 3e14c9106d8..fc201cbc1c7 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -90,7 +90,7 @@ def add_uncertainty_set_constraints(separation_model, config): param_var.fix(nomval) -def construct_separation_problem(model_data, config): +def construct_separation_problem(model_data): """ Construct the separation problem model from the fully preprocessed working model. @@ -99,14 +99,13 @@ def construct_separation_problem(model_data, config): ---------- model_data : model data object Main model data object. - config : ConfigDict - PyROS solver settings. Returns ------- separation_model : ConcreteModel Separation problem model. """ + config = model_data.config separation_model = model_data.working_model.clone() # fix/deactivate all nonadjustable components @@ -161,7 +160,7 @@ def construct_separation_problem(model_data, config): return separation_model -def get_sep_objective_values(separation_data, config, ss_ineq_cons): +def get_sep_objective_values(separation_data, ss_ineq_cons): """ Evaluate second-stage inequality constraint functions at current separation solution. @@ -170,8 +169,6 @@ def get_sep_objective_values(separation_data, config, ss_ineq_cons): ---------- separation_data : SeparationProblemData Separation problem data. - config : ConfigDict - PyROS solver settings. ss_ineq_cons : list of Constraint Second-stage inequality constraints to be evaluated. @@ -181,6 +178,7 @@ def get_sep_objective_values(separation_data, config, ss_ineq_cons): Mapping from second-stage inequality constraints to violation values. """ + config = separation_data.config con_to_obj_map = separation_data.separation_model.second_stage_ineq_con_to_obj_map violations = ComponentMap() @@ -280,7 +278,7 @@ def get_argmax_sum_violations(solver_call_results_map, ss_ineq_cons_to_evaluate) return idx_to_ss_ineq_con_map[idxs_of_violated_cons[worst_col_idx]] -def solve_separation_problem(separation_data, master_data, config): +def solve_separation_problem(separation_data, master_data): """ Solve PyROS separation problems. @@ -288,14 +286,15 @@ def solve_separation_problem(separation_data, master_data, config): ---------- separation_data : SeparationProblemData Separation problem data. - config : ConfigDict - PyROS solver settings. + master_data : MasterProblemData + Master problem data. Returns ------- pyros.solve_data.SeparationResults Separation problem solve results. """ + config = separation_data.config run_local = not config.bypass_local_separation run_global = config.bypass_local_separation @@ -307,7 +306,6 @@ def solve_separation_problem(separation_data, master_data, config): local_separation_loop_results = perform_separation_loop( separation_data=separation_data, master_data=master_data, - config=config, solve_globally=False, ) run_global = not ( @@ -324,7 +322,6 @@ def solve_separation_problem(separation_data, master_data, config): global_separation_loop_results = perform_separation_loop( separation_data=separation_data, master_data=master_data, - config=config, solve_globally=True, ) else: @@ -359,7 +356,7 @@ def evaluate_violations_by_nominal_master(separation_data, master_data, ss_ineq_ return nom_ss_ineq_con_violations -def group_ss_ineq_constraints_by_priority(separation_data, config): +def group_ss_ineq_constraints_by_priority(separation_data): """ Group model second-stage inequality constraints by separation priority. @@ -368,8 +365,6 @@ def group_ss_ineq_constraints_by_priority(separation_data, config): ---------- separation_data : SeparationProblemData Separation problem data. - config : ConfigDict - User-specified PyROS solve options. Returns ------- @@ -410,8 +405,6 @@ def get_worst_discrete_separation_solution( ---------- ss_ineq_con : Constraint Second-stage inequality constraint of interest. - separation_data : SeparationProblemData - Separation problem data. config : ConfigDict User-specified PyROS solver settings. ss_ineq_cons_to_evaluate : list of Constraint @@ -513,17 +506,17 @@ def get_con_name_repr(separation_model, con, with_obj_name=True): return f"{con.index()!r}{qual_str}" -def perform_separation_loop(separation_data, master_data, config, solve_globally): +def perform_separation_loop(separation_data, master_data, solve_globally): """ Loop through, and solve, PyROS separation problems to desired optimality condition. Parameters ---------- - model_data : SeparationProblemData + separation_data : SeparationProblemData Separation problem data. - config : ConfigDict - PyROS solver settings. + master_data : MasterProblemData + Master problem data. solve_globally : bool True to solve separation problems globally, False to solve separation problems locally. @@ -533,6 +526,7 @@ def perform_separation_loop(separation_data, master_data, config, solve_globally pyros.solve_data.SeparationLoopResults Separation problem solve results. """ + config = separation_data.config all_ss_ineq_constraints = list( separation_data.separation_model.second_stage.inequality_cons.values() ) @@ -550,9 +544,7 @@ def perform_separation_loop(separation_data, master_data, config, solve_globally master_data=master_data, ss_ineq_cons=all_ss_ineq_constraints, ) - sorted_priority_groups = group_ss_ineq_constraints_by_priority( - separation_data, config - ) + sorted_priority_groups = group_ss_ineq_constraints_by_priority(separation_data) uncertainty_set_is_discrete = ( config.uncertainty_set.geometry == Geometry.DISCRETE_SCENARIOS ) @@ -580,7 +572,6 @@ def perform_separation_loop(separation_data, master_data, config, solve_globally discrete_sep_results = discrete_solve( separation_data=separation_data, master_data=master_data, - config=config, solve_globally=solve_globally, ss_ineq_con_to_maximize=ss_ineq_con_to_maximize, ss_ineq_cons_to_evaluate=all_ss_ineq_constraints, @@ -644,7 +635,6 @@ def perform_separation_loop(separation_data, master_data, config, solve_globally solve_call_results = solver_call_separation( separation_data=separation_data, master_data=master_data, - config=config, solve_globally=solve_globally, ss_ineq_con_to_maximize=ss_ineq_con, ss_ineq_cons_to_evaluate=all_ss_ineq_constraints, @@ -719,7 +709,7 @@ def perform_separation_loop(separation_data, master_data, config, solve_globally def evaluate_ss_ineq_con_violations( - separation_data, config, ss_ineq_con_to_maximize, ss_ineq_cons_to_evaluate + separation_data, ss_ineq_con_to_maximize, ss_ineq_cons_to_evaluate ): """ Evaluate the inequality constraint function violations @@ -733,8 +723,6 @@ def evaluate_ss_ineq_con_violations( ---------- separation_data : SeparationProblemData Object containing the separation model. - config : ConfigDict - PyROS solver settings. ss_ineq_con_to_maximize : ConstraintData Second-stage inequality constraint to which the current solution is mapped. @@ -765,6 +753,8 @@ def evaluate_ss_ineq_con_violations( 1 entry which can be mapped to an active Objective of ``model_data.separation_model``. """ + config = separation_data.config + # parameter realization for current separation problem solution uncertain_param_vars = ( separation_data.separation_model.uncertainty.uncertain_param_var_list @@ -777,7 +767,6 @@ def evaluate_ss_ineq_con_violations( # constraints provided violations_by_sep_solution = get_sep_objective_values( separation_data=separation_data, - config=config, ss_ineq_cons=ss_ineq_cons_to_evaluate, ) @@ -801,7 +790,7 @@ def evaluate_ss_ineq_con_violations( def initialize_separation( - ss_ineq_con_to_maximize, separation_data, master_data, config + ss_ineq_con_to_maximize, separation_data, master_data ): """ Initialize separation problem variables using the solution @@ -815,8 +804,8 @@ def initialize_separation( for the separation problem of interest. separation_data : SeparationProblemData Separation problem data. - config : ConfigDict - PyROS solver settings. + master_data : MasterProblemData + Master problem data. Note ---- @@ -828,6 +817,7 @@ def initialize_separation( auxiliary variables, then some uncertainty set constraints may be violated. """ + config = separation_data.config master_model = master_data.master_model sep_model = separation_data.separation_model @@ -903,7 +893,6 @@ def eval_master_violation(scenario_idx): def solver_call_separation( separation_data, master_data, - config, solve_globally, ss_ineq_con_to_maximize, ss_ineq_cons_to_evaluate, @@ -915,8 +904,8 @@ def solver_call_separation( ---------- separation_data : SeparationProblemData Separation problem data. - config : ConfigDict - PyROS solver settings. + master_data : MasterProblemData + Master problem data. solve_globally : bool True to solve separation problems globally, False to solve locally. @@ -934,11 +923,12 @@ def solver_call_separation( solve_call_results : pyros.solve_data.SeparationSolveCallResults Solve results for separation problem of interest. """ + config = separation_data.config # prepare the problem separation_model = separation_data.separation_model objectives_map = separation_data.separation_model.second_stage_ineq_con_to_obj_map separation_obj = objectives_map[ss_ineq_con_to_maximize] - initialize_separation(ss_ineq_con_to_maximize, separation_data, master_data, config) + initialize_separation(ss_ineq_con_to_maximize, separation_data, master_data) separation_obj.activate() # get name (index) of constraint for loggers @@ -1019,10 +1009,9 @@ def solver_call_separation( solve_call_results.scaled_violations, solve_call_results.found_violation, ) = evaluate_ss_ineq_con_violations( - separation_data, - config, - ss_ineq_con_to_maximize, - ss_ineq_cons_to_evaluate, + separation_data=separation_data, + ss_ineq_con_to_maximize=ss_ineq_con_to_maximize, + ss_ineq_cons_to_evaluate=ss_ineq_cons_to_evaluate, ) solve_call_results.auxiliary_param_values = [ auxvar.value @@ -1089,7 +1078,6 @@ def solver_call_separation( def discrete_solve( separation_data, master_data, - config, solve_globally, ss_ineq_con_to_maximize, ss_ineq_cons_to_evaluate, @@ -1103,8 +1091,8 @@ def discrete_solve( ---------- separation_data : SeparationProblemData Separation problem data. - config : ConfigDict - PyROS solver settings. + master_data : MasterProblemData + Master problem data. solver : solver type Primary subordinate optimizer with which to solve the model. @@ -1139,6 +1127,7 @@ def discrete_solve( solutions is, under our assumption, merely equal to the number of scenarios in the uncertainty set. """ + config = separation_data.config uncertain_param_vars = list( separation_data.separation_model.uncertainty.uncertain_param_var_list @@ -1164,7 +1153,6 @@ def discrete_solve( solve_call_results = solver_call_separation( separation_data=separation_data, master_data=master_data, - config=config, solve_globally=solve_globally, ss_ineq_con_to_maximize=ss_ineq_con_to_maximize, ss_ineq_cons_to_evaluate=ss_ineq_cons_to_evaluate, @@ -1191,12 +1179,14 @@ class SeparationProblemData: Container for objects related to the PyROS separation problem. """ - def __init__(self, model_data, config): + def __init__(self, model_data): """Initialize self (see class docstring).""" - self.separation_model = construct_separation_problem(model_data, config) + self.separation_model = construct_separation_problem(model_data) self.timing = model_data.timing self.separation_priority_order = model_data.separation_priority_order.copy() self.iteration = 0 + + config = model_data.config self.config = config self.points_added_to_master = {(0, 0): config.nominal_uncertain_param_vals} self.auxiliary_values_for_master_points = { @@ -1221,4 +1211,4 @@ def solve_separation(self, master_data): """ Solve the separation problem. """ - return solve_separation_problem(self, master_data, self.config) + return solve_separation_problem(self, master_data) diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index d41ea6112f6..039168ce29d 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -78,16 +78,16 @@ def build_simple_model_data(objective_focus="worst_case"): nominal_uncertain_param_vals=[0.4], separation_priority_order=dict(), ) - model_data = ModelData(original_model=m, timing=TimingData()) + model_data = ModelData(original_model=m, timing=TimingData(), config=config) user_var_partitioning = VariablePartitioning( first_stage_variables=[m.x1], second_stage_variables=[m.x2, m.x3], state_variables=[], ) - preprocess_model_data(model_data, config, user_var_partitioning) + preprocess_model_data(model_data, user_var_partitioning) - return model_data, config + return model_data class TestConstructMasterProblem(unittest.TestCase): @@ -101,8 +101,8 @@ def test_initial_construct_master(self): Test initial construction of the master problem from the preprocesed working model. """ - model_data, config = build_simple_model_data() - master_model = construct_initial_master_problem(model_data, config) + model_data = build_simple_model_data() + master_model = construct_initial_master_problem(model_data) self.assertTrue(hasattr(master_model, "scenarios")) self.assertIsNot(master_model.scenarios[0, 0], model_data.working_model) @@ -128,7 +128,7 @@ def test_initial_construct_master(self): # check parameter value is set to the nominal realization self.assertEqual( master_model.scenarios[0, 0].user_model.u.value, - config.nominal_uncertain_param_vals[0], + model_data.config.nominal_uncertain_param_vals[0], ) def test_add_scenario_block_to_master(self): @@ -137,8 +137,8 @@ def test_add_scenario_block_to_master(self): constructed master problem, without cloning of the first-stage variables. """ - model_data, config = build_simple_model_data() - master_model = construct_initial_master_problem(model_data, config) + model_data = build_simple_model_data() + master_model = construct_initial_master_problem(model_data) add_scenario_block_to_master_problem( master_model=master_model, scenario_idx=[0, 1], @@ -230,8 +230,8 @@ def build_simple_master_data(self): Construct master data-like object for feasibility problem tests. """ - model_data, config = build_simple_model_data() - master_model = construct_initial_master_problem(model_data, config) + model_data = build_simple_model_data() + master_model = construct_initial_master_problem(model_data) add_scenario_block_to_master_problem( master_model=master_model, scenario_idx=[1, 0], @@ -239,16 +239,18 @@ def build_simple_master_data(self): from_block=master_model.scenarios[0, 0], clone_first_stage_components=False, ) - master_data = Bunch(master_model=master_model, iteration=1) + master_data = Bunch( + master_model=master_model, iteration=1, config=model_data.config + ) - return master_data, config + return master_data def test_construct_master_feasibility_problem_var_map(self): """ Test construction of feasibility problem var map. """ - master_data, config = self.build_simple_master_data() - slack_model = construct_master_feasibility_problem(master_data, config) + master_data = self.build_simple_master_data() + slack_model = construct_master_feasibility_problem(master_data) self.assertTrue(master_data.feasibility_problem_varmap) for mvar, feasvar in master_data.feasibility_problem_varmap: @@ -267,8 +269,8 @@ def test_construct_master_feasibility_problem_slack_vars(self): """ Check master feasibility slack variables. """ - master_data, config = self.build_simple_master_data() - slack_model = construct_master_feasibility_problem(master_data, config) + master_data = self.build_simple_master_data() + slack_model = construct_master_feasibility_problem(master_data) slack_var_blk = slack_model._core_add_slack_variables scenario_10_blk = slack_model.scenarios[1, 0] @@ -317,8 +319,8 @@ def test_construct_master_feasibility_problem_obj(self): """ Check master feasibility slack variables. """ - master_data, config = self.build_simple_master_data() - slack_model = construct_master_feasibility_problem(master_data, config) + master_data = self.build_simple_master_data() + slack_model = construct_master_feasibility_problem(master_data) self.assertFalse(slack_model.epigraph_obj.active) self.assertTrue(slack_model._core_add_slack_variables._slack_objective.active) @@ -334,8 +336,8 @@ def build_simple_master_data(self): Construct master data-like object for feasibility problem tests. """ - model_data, config = build_simple_model_data() - master_model = construct_initial_master_problem(model_data, config) + model_data = build_simple_model_data() + master_model = construct_initial_master_problem(model_data) add_scenario_block_to_master_problem( master_model=master_model, scenario_idx=[1, 0], @@ -343,17 +345,19 @@ def build_simple_master_data(self): from_block=master_model.scenarios[0, 0], clone_first_stage_components=False, ) - master_data = Bunch(master_model=master_model, iteration=1) + master_data = Bunch( + master_model=master_model, iteration=1, config=model_data.config + ) - return master_data, config + return master_data def test_construct_dr_polishing_problem_nonadj_components(self): """ Test state of the nonadjustable components of the DR polishing problem. """ - master_data, config = self.build_simple_master_data() - polishing_model = construct_dr_polishing_problem(master_data, config) + master_data = self.build_simple_master_data() + polishing_model = construct_dr_polishing_problem(master_data) eff_first_stage_vars = polishing_model.scenarios[ 0, 0 ].effective_var_partitioning.first_stage_variables @@ -390,14 +394,14 @@ def test_construct_dr_polishing_problem_polishing_components(self): Test auxiliary Var/Constraint components of the DR polishing problem. """ - master_data, config = self.build_simple_master_data() + master_data = self.build_simple_master_data() # DR order is 1, and x3 is second-stage. # to test fixing efficiency, fix the affine DR variable decision_rule_vars = master_data.master_model.scenarios[ 0, 0 ].first_stage.decision_rule_vars decision_rule_vars[0][1].fix() - polishing_model = construct_dr_polishing_problem(master_data, config) + polishing_model = construct_dr_polishing_problem(master_data) nom_polishing_block = polishing_model.scenarios[0, 0] self.assertFalse(decision_rule_vars[0][0].fixed) @@ -434,8 +438,8 @@ def test_construct_dr_polishing_problem_objectives(self): Test states of the Objective components of the DR polishing model. """ - master_data, config = self.build_simple_master_data() - polishing_model = construct_dr_polishing_problem(master_data, config) + master_data = self.build_simple_master_data() + polishing_model = construct_dr_polishing_problem(master_data) self.assertFalse(polishing_model.epigraph_obj.active) self.assertTrue(polishing_model.polishing_obj.active) @@ -445,13 +449,13 @@ def test_construct_dr_polishing_problem_params_zero(self): for DR expression terms where the product of uncertain parameters is below tolerance. """ - master_data, config = self.build_simple_master_data() + master_data = self.build_simple_master_data() # trigger fixing of the corresponding polishing vars master_data.master_model.scenarios[0, 0].user_model.u.set_value(1e-10) master_data.master_model.scenarios[1, 0].user_model.u.set_value(1e-11) - polishing_model = construct_dr_polishing_problem(master_data, config) + polishing_model = construct_dr_polishing_problem(master_data) dr_vars = polishing_model.scenarios[0, 0].first_stage.decision_rule_vars @@ -477,10 +481,10 @@ class TestSolveMaster(unittest.TestCase): @unittest.skipUnless(baron_available, "Global NLP solver is not available.") def test_solve_master(self): - model_data, config = build_simple_model_data() + model_data = build_simple_model_data() model_data.timing = TimingData() baron = SolverFactory("baron") - config.update( + model_data.config.update( dict( local_solver=baron, global_solver=baron, @@ -489,7 +493,7 @@ def test_solve_master(self): tee=False, ) ) - master_data = MasterProblemData(model_data, config) + master_data = MasterProblemData(model_data) with time_code(master_data.timing, "main", is_main_timer=True): master_soln = master_data.solve_master() self.assertEqual( @@ -507,10 +511,10 @@ def test_solve_master_timeout_on_master(self): Test method for solution of master problems times out on feasibility problem. """ - model_data, config = build_simple_model_data() + model_data = build_simple_model_data() model_data.timing = TimingData() baron = SolverFactory("baron") - config.update( + model_data.config.update( dict( local_solver=baron, global_solver=baron, @@ -520,7 +524,7 @@ def test_solve_master_timeout_on_master(self): time_limit=1, ) ) - master_data = MasterProblemData(model_data, config) + master_data = MasterProblemData(model_data) with time_code(master_data.timing, "main", is_main_timer=True): time.sleep(1) master_soln = master_data.solve_master() @@ -543,10 +547,10 @@ def test_solve_master_timeout_on_master_feasibility(self): Test method for solution of master problems times out on feasibility problem. """ - model_data, config = build_simple_model_data() + model_data = build_simple_model_data() model_data.timing = TimingData() baron = SolverFactory("baron") - config.update( + model_data.config.update( dict( local_solver=baron, global_solver=baron, @@ -556,7 +560,7 @@ def test_solve_master_timeout_on_master_feasibility(self): time_limit=1, ) ) - master_data = MasterProblemData(model_data, config) + master_data = MasterProblemData(model_data) add_scenario_block_to_master_problem( master_data.master_model, scenario_idx=[1, 0], @@ -587,10 +591,10 @@ class TestPolishDRVars(unittest.TestCase): baron_license_is_valid, "Global NLP solver is not available and licensed." ) def test_polish_dr_vars(self): - model_data, config = build_simple_model_data() + model_data = build_simple_model_data() model_data.timing = TimingData() baron = SolverFactory("baron") - config.update( + model_data.config.update( dict( local_solver=baron, global_solver=baron, @@ -599,7 +603,7 @@ def test_polish_dr_vars(self): tee=False, ) ) - master_data = MasterProblemData(model_data, config) + master_data = MasterProblemData(model_data) add_scenario_block_to_master_problem( master_data.master_model, scenario_idx=[1, 0], diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index fbdd7e699d8..9ce2fbc4662 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -98,6 +98,7 @@ def build_simple_test_model_data(self): m.c5 = Constraint(expr=m.x2 + 2 * m.y[2] + m.y[3] + 2 * m.y[4] == 0) model_data = Bunch() + model_data.config = Bunch() model_data.working_model = ConcreteModel() model_data.working_model.user_model = mdl = m.clone() model_data.working_model.uncertain_params = [mdl.q] @@ -117,7 +118,7 @@ def test_effective_partitioning_system(self): model_data = self.build_simple_test_model_data() m = model_data.working_model.user_model - config = Bunch() + config = model_data.config config.decision_rule_order = 0 config.progress_logger = logger @@ -129,7 +130,7 @@ def test_effective_partitioning_system(self): for dr_order in [0, 1, 2]: config.decision_rule_order = dr_order actual_partitioning = get_effective_var_partitioning( - model_data=model_data, config=config + model_data=model_data ) for vartype, expected_vars in expected_partitioning.items(): actual_vars = getattr(actual_partitioning, vartype) @@ -157,9 +158,7 @@ def test_effective_partitioning_system(self): } for dr_order in [0, 1, 2]: config.decision_rule_order = dr_order - actual_partitioning = get_effective_var_partitioning( - model_data=model_data, config=config - ) + actual_partitioning = get_effective_var_partitioning(model_data) for vartype, expected_vars in expected_partitioning.items(): actual_vars = getattr(actual_partitioning, vartype) self.assertEqual( @@ -186,9 +185,7 @@ def test_effective_partitioning_system(self): } for dr_order in [0, 1, 2]: config.decision_rule_order = dr_order - actual_partitioning = get_effective_var_partitioning( - model_data=model_data, config=config - ) + actual_partitioning = get_effective_var_partitioning(model_data) for vartype, expected_vars in expected_partitioning.items(): actual_vars = getattr(actual_partitioning, vartype) self.assertEqual( @@ -214,9 +211,7 @@ def test_effective_partitioning_system(self): m.c3.set_value(m.x1**3 + m.y[1] + 2 * m.y[1] * m.y[2] == 0) for dr_order in [0, 1, 2]: config.decision_rule_order = dr_order - actual_partitioning = get_effective_var_partitioning( - model_data=model_data, config=config - ) + actual_partitioning = get_effective_var_partitioning(model_data) for vartype, expected_vars in expected_partitioning.items(): actual_vars = getattr(actual_partitioning, vartype) self.assertEqual( @@ -241,9 +236,7 @@ def test_effective_partitioning_system(self): } for dr_order in [0, 1, 2]: config.decision_rule_order = dr_order - actual_partitioning = get_effective_var_partitioning( - model_data=model_data, config=config - ) + actual_partitioning = get_effective_var_partitioning(model_data) for vartype, expected_vars in expected_partitioning.items(): actual_vars = getattr(actual_partitioning, vartype) self.assertEqual( @@ -271,7 +264,7 @@ def test_effective_partitioning_modified_linear_system(self): # nonadjustable m.c1.set_value((0, m.x1 + m.z**2, 0)) - config = Bunch() + config = model_data.config config.decision_rule_order = 0 config.progress_logger = logger @@ -280,9 +273,7 @@ def test_effective_partitioning_modified_linear_system(self): "second_stage_variables": [], "state_variables": [m.y[3], m.y[4]], } - actual_partitioning_static_dr = get_effective_var_partitioning( - model_data=model_data, config=config - ) + actual_partitioning_static_dr = get_effective_var_partitioning(model_data) for vartype, expected_vars in expected_partitioning_static_dr.items(): actual_vars = getattr(actual_partitioning_static_dr, vartype) self.assertEqual( @@ -306,7 +297,7 @@ def test_effective_partitioning_modified_linear_system(self): } for dr_order in [1, 2]: actual_partitioning_nonstatic_dr = get_effective_var_partitioning( - model_data=model_data, config=config + model_data ) for vartype, expected_vars in expected_partitioning_nonstatic_dr.items(): actual_vars = getattr(actual_partitioning_nonstatic_dr, vartype) @@ -334,6 +325,7 @@ def build_test_model_data(self): Build model data object for the preprocessor. """ model_data = Bunch() + model_data.config = Bunch() model_data.original_model = m = ConcreteModel() # PARAMS: one uncertain, one certain @@ -409,9 +401,10 @@ def test_setup_working_model(self): """ model_data, user_var_partitioning = self.build_test_model_data() om = model_data.original_model - config = Bunch(uncertain_params=[om.q]) + config = model_data.config + config.uncertain_params = [om.q] - setup_working_model(model_data, config, user_var_partitioning) + setup_working_model(model_data, user_var_partitioning) working_model = model_data.working_model # active constraints @@ -571,6 +564,7 @@ def build_simple_test_model_data(self): to constraints. """ model_data = Bunch() + model_data.config = Bunch() model_data.working_model = ConcreteModel() model_data.working_model.user_model = m = ConcreteModel() @@ -641,7 +635,7 @@ def test_turn_nonadjustable_bounds_to_constraints(self): ) ) - turn_nonadjustable_var_bounds_to_constraints(model_data, config=Bunch()) + turn_nonadjustable_var_bounds_to_constraints(model_data) for var, (orig_domain, orig_bounds) in original_var_domains_and_bounds.items(): # all var domains should remain unchanged @@ -787,7 +781,7 @@ def test_turn_adjustable_bounds_to_constraints(self): for var in model_data.working_model.user_model.component_data_objects(Var) ) - turn_adjustable_var_bounds_to_constraints(model_data, config=Bunch()) + turn_adjustable_var_bounds_to_constraints(model_data) for var, (orig_domain, orig_bounds) in original_var_domains_and_bounds.items(): _, (final_lb, final_ub) = get_var_bound_pairs(var) @@ -959,6 +953,7 @@ def build_simple_test_model_data(self): routines. """ model_data = Bunch() + model_data.config = Bunch() model_data.working_model = ConcreteModel() model_data.working_model.user_model = m = Block() @@ -1024,9 +1019,8 @@ def test_standardize_inequality_constraints(self): working_model = model_data.working_model m = working_model.user_model - standardize_inequality_constraints( - model_data, config=Bunch(separation_priority_order=dict(c3=1, c5=2)) - ) + model_data.config.separation_priority_order = dict(c3=1, c5=2) + standardize_inequality_constraints(model_data) fs_ineq_cons = working_model.first_stage.inequality_cons ss_ineq_cons = working_model.second_stage.inequality_cons @@ -1151,6 +1145,7 @@ def test_standardize_inequality_error(self): method if equality-type expression detected. """ model_data = self.build_simple_test_model_data() + model_data.config.separation_priority_order = dict() working_model = model_data.working_model m = working_model.user_model @@ -1159,9 +1154,7 @@ def test_standardize_inequality_error(self): exc_str = r"Found an equality bound.*1.0.*for the constraint.*c6'" with self.assertRaisesRegex(ValueError, exc_str): - standardize_inequality_constraints( - model_data, Bunch(separation_priority_order=dict()) - ) + standardize_inequality_constraints(model_data) class TestStandardizeEqualityConstraints(unittest.TestCase): @@ -1175,6 +1168,7 @@ def build_simple_test_model_data(self): routines. """ model_data = Bunch() + model_data.config = Bunch() model_data.working_model = ConcreteModel() model_data.working_model.user_model = m = Block() @@ -1291,6 +1285,7 @@ def build_simple_test_model_data(self): standardization. """ model_data = Bunch() + model_data.config = Bunch() model_data.working_model = ConcreteModel() model_data.working_model.user_model = m = Block() @@ -1382,12 +1377,12 @@ def test_standardize_active_obj_worst_case_focus(self): model_data = self.build_simple_test_model_data() working_model = model_data.working_model m = model_data.working_model.user_model - config = Bunch(objective_focus=ObjectiveType.worst_case) + model_data.config.objective_focus = ObjectiveType.worst_case m.obj1.activate() m.obj2.deactivate() - standardize_active_objective(model_data, config) + standardize_active_objective(model_data) self.assertFalse( m.obj1.active, @@ -1411,12 +1406,12 @@ def test_standardize_active_obj_nominal_focus(self): model_data = self.build_simple_test_model_data() working_model = model_data.working_model m = model_data.working_model.user_model - config = Bunch(objective_focus=ObjectiveType.nominal) + model_data.config.objective_focus = ObjectiveType.nominal m.obj1.activate() m.obj2.deactivate() - standardize_active_objective(model_data, config) + standardize_active_objective(model_data) self.assertFalse( m.obj1.active, @@ -1439,14 +1434,14 @@ def test_standardize_active_obj_unsupported_focus(self): """ model_data = self.build_simple_test_model_data() m = model_data.working_model.user_model - config = Bunch(objective_focus="bad_focus") + model_data.config.objective_focus = "bad_focus" m.obj1.activate() m.obj2.deactivate() exc_str = r"Classification.*not implemented for objective focus 'bad_focus'" with self.assertRaisesRegex(ValueError, exc_str): - standardize_active_objective(model_data, config) + standardize_active_objective(model_data) def test_standardize_active_obj_nonadjustable_max(self): """ @@ -1457,7 +1452,7 @@ def test_standardize_active_obj_nonadjustable_max(self): model_data = self.build_simple_test_model_data() working_model = model_data.working_model m = working_model.user_model - config = Bunch(objective_focus=ObjectiveType.worst_case) + model_data.config.objective_focus = ObjectiveType.worst_case # assume all variables nonadjustable ep = model_data.working_model.effective_var_partitioning @@ -1469,7 +1464,7 @@ def test_standardize_active_obj_nonadjustable_max(self): m.obj2.activate() m.obj2.sense = maximize - standardize_active_objective(model_data, config) + standardize_active_objective(model_data) self.assertFalse( m.obj2.active, @@ -1505,6 +1500,7 @@ def build_simple_test_model_data(self): declaration testing. """ model_data = Bunch() + model_data.config = Bunch() model_data.working_model = ConcreteModel() model_data.working_model.user_model = m = Block() @@ -1539,11 +1535,9 @@ def test_correct_num_dr_vars_static(self): number of DR coefficient variables, static DR case. """ model_data = self.build_simple_test_model_data() + model_data.config.decision_rule_order = 0 - config = Bunch() - config.decision_rule_order = 0 - - add_decision_rule_variables(model_data=model_data, config=config) + add_decision_rule_variables(model_data) for indexed_dr_var in model_data.working_model.first_stage.decision_rule_vars: self.assertEqual( @@ -1591,11 +1585,9 @@ def test_correct_num_dr_vars_affine(self): number of DR coefficient variables, affine DR case. """ model_data = self.build_simple_test_model_data() + model_data.config.decision_rule_order = 1 - config = Bunch() - config.decision_rule_order = 1 - - add_decision_rule_variables(model_data=model_data, config=config) + add_decision_rule_variables(model_data) for indexed_dr_var in model_data.working_model.first_stage.decision_rule_vars: self.assertEqual( @@ -1643,11 +1635,9 @@ def test_correct_num_dr_vars_quadratic(self): number of DR coefficient variables, quadratic DR case. """ model_data = self.build_simple_test_model_data() + model_data.config.decision_rule_order = 2 - config = Bunch() - config.decision_rule_order = 2 - - add_decision_rule_variables(model_data=model_data, config=config) + add_decision_rule_variables(model_data) num_params = len(model_data.working_model.uncertain_params) @@ -1710,6 +1700,7 @@ def build_simple_test_model_data(self): declaration testing. """ model_data = Bunch() + model_data.config = Bunch() model_data.working_model = ConcreteModel() model_data.working_model.user_model = m = Block() @@ -1746,13 +1737,10 @@ def test_num_dr_eqns_added_correct(self): of second-stage variables in the model. """ model_data = self.build_simple_test_model_data() + model_data.config.decision_rule_order = 2 - # set up simple config-like object - config = Bunch() - config.decision_rule_order = 0 - - add_decision_rule_variables(model_data, config) - add_decision_rule_constraints(model_data, config) + add_decision_rule_variables(model_data) + add_decision_rule_constraints(model_data) effective_second_stage_vars = ( model_data.working_model.effective_var_partitioning.second_stage_variables @@ -1802,12 +1790,11 @@ def test_dr_eqns_form_correct(self): m = model_data.working_model.user_model # set up simple config-like object - config = Bunch() - config.decision_rule_order = 2 + model_data.config.decision_rule_order = 2 # add DR variables and constraints - add_decision_rule_variables(model_data, config) - add_decision_rule_constraints(model_data, config) + add_decision_rule_variables(model_data) + add_decision_rule_constraints(model_data) dr_zip = zip( model_data.working_model.effective_var_partitioning.second_stage_variables, @@ -1864,6 +1851,7 @@ def setup_test_model_data(self): routine. """ model_data = Bunch() + model_data.config = Bunch() model_data.working_model = working_model = ConcreteModel() model_data.working_model.user_model = m = Block() @@ -1921,9 +1909,8 @@ def test_coefficient_matching_correct_constraints_added(self): ep.first_stage_variables = [m.x1, m.x2] ep.second_stage_variables = [] - config = Bunch() - config.decision_rule_order = 1 - config.progress_logger = logger + model_data.config.decision_rule_order = 1 + model_data.config.progress_logger = logger model_data.working_model.first_stage.decision_rule_vars = [] model_data.working_model.second_stage.decision_rule_eqns = [] @@ -1931,9 +1918,7 @@ def test_coefficient_matching_correct_constraints_added(self): ep.first_stage_variables ) - robust_infeasible = reformulate_state_var_independent_eq_cons( - model_data, config - ) + robust_infeasible = reformulate_state_var_independent_eq_cons(model_data) self.assertFalse( robust_infeasible, @@ -1978,15 +1963,14 @@ def test_reformulate_nonlinear_state_var_independent_eq_con(self): model_data = self.setup_test_model_data() model_data.separation_priority_order = dict() - config = Bunch() - config.decision_rule_order = 1 - config.progress_logger = logging.getLogger( + model_data.config.decision_rule_order = 1 + model_data.config.progress_logger = logging.getLogger( self.test_reformulate_nonlinear_state_var_independent_eq_con.__name__ ) - config.progress_logger.setLevel(logging.DEBUG) + model_data.config.progress_logger.setLevel(logging.DEBUG) - add_decision_rule_variables(model_data=model_data, config=config) - add_decision_rule_constraints(model_data=model_data, config=config) + add_decision_rule_variables(model_data) + add_decision_rule_constraints(model_data) ep = model_data.working_model.effective_var_partitioning model_data.working_model.all_nonadjustable_variables = list( @@ -2003,7 +1987,7 @@ def test_reformulate_nonlinear_state_var_independent_eq_con(self): with LoggingIntercept(level=logging.DEBUG) as LOG: robust_infeasible = reformulate_state_var_independent_eq_cons( - model_data, config + model_data ) err_msg = LOG.getvalue() @@ -2089,18 +2073,15 @@ def test_coefficient_matching_robust_infeasible_proof(self): ep.first_stage_variables = [m.x1, m.x2] ep.second_stage_variables = [] - config = Bunch() - config.decision_rule_order = 1 - config.progress_logger = logger + model_data.config.decision_rule_order = 1 + model_data.config.progress_logger = logger model_data.working_model.all_nonadjustable_variables = list( ep.first_stage_variables ) with LoggingIntercept(level=logging.INFO) as LOG: - robust_infeasible = reformulate_state_var_independent_eq_cons( - model_data, config - ) + robust_infeasible = reformulate_state_var_independent_eq_cons(model_data) self.assertTrue( robust_infeasible, @@ -2204,7 +2185,7 @@ def build_test_model_data(self): ) ) - model_data = ModelData(original_model=m, timing=None) + model_data = ModelData(original_model=m, timing=None, config=Bunch()) # set up the var partitioning user_var_partitioning = VariablePartitioning( @@ -2224,14 +2205,15 @@ def test_preprocessor_effective_var_partitioning_static_dr(self): # setup model_data, user_var_partitioning = self.build_test_model_data() om = model_data.original_model - config = Bunch( + config = model_data.config + config.update(dict( uncertain_params=[om.q], objective_focus=ObjectiveType.worst_case, decision_rule_order=0, progress_logger=logger, separation_priority_order=dict(), - ) - preprocess_model_data(model_data, config, user_var_partitioning) + )) + preprocess_model_data(model_data, user_var_partitioning) ep = model_data.working_model.effective_var_partitioning ublk = model_data.working_model.user_model self.assertEqual( @@ -2288,14 +2270,15 @@ def test_preprocessor_effective_var_partitioning_nonstatic_dr(self, name, dr_ord """ model_data, user_var_partitioning = self.build_test_model_data() om = model_data.original_model - config = Bunch( + config = model_data.config + config.update(dict( uncertain_params=[om.q], objective_focus=ObjectiveType.worst_case, decision_rule_order=dr_order, progress_logger=logger, separation_priority_order=dict(), - ) - preprocess_model_data(model_data, config, user_var_partitioning) + )) + preprocess_model_data(model_data, user_var_partitioning) ep = model_data.working_model.effective_var_partitioning ublk = model_data.working_model.user_model self.assertEqual( @@ -2355,14 +2338,14 @@ def test_preprocessor_constraint_partitioning_nonstatic_dr( """ model_data, user_var_partitioning = self.build_test_model_data() om = model_data.original_model - config = Bunch( + model_data.config.update(dict( uncertain_params=[om.q], objective_focus=ObjectiveType[obj_focus], decision_rule_order=dr_order, progress_logger=logger, separation_priority_order=dict(ineq3=2), - ) - preprocess_model_data(model_data, config, user_var_partitioning) + )) + preprocess_model_data(model_data, user_var_partitioning) working_model = model_data.working_model ublk = working_model.user_model @@ -2548,19 +2531,20 @@ def test_preprocessor_coefficient_matching( """ model_data, user_var_partitioning = self.build_test_model_data() om = model_data.original_model - config = Bunch( + config = model_data.config + config.update(dict( uncertain_params=[om.q], objective_focus=ObjectiveType.worst_case, decision_rule_order=dr_order, progress_logger=logger, separation_priority_order=dict(), - ) + )) # for static DR, problem should be robust infeasible # due to the coefficient matching constraints derived # from bounds on z5 robust_infeasible = preprocess_model_data( - model_data, config, user_var_partitioning + model_data, user_var_partitioning ) self.assertIsInstance(robust_infeasible, bool) self.assertEqual(robust_infeasible, expected_robust_infeas) @@ -2631,14 +2615,15 @@ def test_preprocessor_objective_standardization(self, name, dr_order): """ model_data, user_var_partitioning = self.build_test_model_data() om = model_data.original_model - config = Bunch( + config = model_data.config + config.update(dict( uncertain_params=[om.q], objective_focus=ObjectiveType.worst_case, decision_rule_order=dr_order, progress_logger=logger, separation_priority_order=dict(), - ) - preprocess_model_data(model_data, config, user_var_partitioning) + )) + preprocess_model_data(model_data, user_var_partitioning) ublk = model_data.working_model.user_model working_model = model_data.working_model @@ -2680,14 +2665,15 @@ def test_preprocessor_log_model_statistics_affine_dr(self, obj_focus): """ model_data, user_var_partitioning = self.build_test_model_data() om = model_data.original_model - config = Bunch( + config = model_data.config + config.update(dict( uncertain_params=[om.q], objective_focus=ObjectiveType[obj_focus], decision_rule_order=1, progress_logger=logger, separation_priority_order=dict(), - ) - preprocess_model_data(model_data, config, user_var_partitioning) + )) + preprocess_model_data(model_data, user_var_partitioning) # expected model stats worked out by hand expected_log_str = textwrap.dedent( @@ -2713,7 +2699,7 @@ def test_preprocessor_log_model_statistics_affine_dr(self, obj_focus): ) with LoggingIntercept(level=logging.INFO) as LOG: - log_model_statistics(model_data, config) + log_model_statistics(model_data) log_str = LOG.getvalue() log_lines = log_str.splitlines()[1:] @@ -2731,14 +2717,15 @@ def test_preprocessor_log_model_statistics_quadratic_dr(self, obj_focus): """ model_data, user_var_partitioning = self.build_test_model_data() om = model_data.original_model - config = Bunch( + config = model_data.config + config.update(dict( uncertain_params=[om.q], objective_focus=ObjectiveType[obj_focus], decision_rule_order=2, progress_logger=logger, separation_priority_order=dict(), - ) - preprocess_model_data(model_data, config, user_var_partitioning) + )) + preprocess_model_data(model_data, user_var_partitioning) # expected model stats worked out by hand expected_log_str = textwrap.dedent( @@ -2764,7 +2751,7 @@ def test_preprocessor_log_model_statistics_quadratic_dr(self, obj_focus): ) with LoggingIntercept(level=logging.INFO) as LOG: - log_model_statistics(model_data, config) + log_model_statistics(model_data) log_str = LOG.getvalue() log_lines = log_str.splitlines()[1:] diff --git a/pyomo/contrib/pyros/tests/test_separation.py b/pyomo/contrib/pyros/tests/test_separation.py index 2310fd381ad..f60635e1a17 100644 --- a/pyomo/contrib/pyros/tests/test_separation.py +++ b/pyomo/contrib/pyros/tests/test_separation.py @@ -69,16 +69,16 @@ def build_simple_model_data(objective_focus="worst_case"): uncertainty_set=BoxSet([[0, 1], [0, 0]]), separation_priority_order=dict(con=2), ) - model_data = ModelData(original_model=m, timing=None) + model_data = ModelData(original_model=m, timing=None, config=config) user_var_partitioning = VariablePartitioning( first_stage_variables=[m.x1], second_stage_variables=[m.x2, m.x3], state_variables=[], ) - preprocess_model_data(model_data, config, user_var_partitioning) + preprocess_model_data(model_data, user_var_partitioning) - return model_data, config + return model_data class TestConstructSeparationProblem(unittest.TestCase): @@ -92,8 +92,8 @@ def test_construct_separation_problem_nonadj_components(self): separation problem are fixed and deactivated, respectively. """ - model_data, config = build_simple_model_data(objective_focus="worst_case") - separation_model = construct_separation_problem(model_data, config) + model_data = build_simple_model_data(objective_focus="worst_case") + separation_model = construct_separation_problem(model_data) # check nonadjustable components fixed/deactivated self.assertTrue(separation_model.user_model.x1.fixed) @@ -115,8 +115,8 @@ def test_construct_separation_problem_ss_ineq_cons(self): Check second-stage inequality constraints are deactivated and replaced with objectives, as appropriate. """ - model_data, config = build_simple_model_data(objective_focus="worst_case") - separation_model = construct_separation_problem(model_data, config) + model_data = build_simple_model_data(objective_focus="worst_case") + separation_model = construct_separation_problem(model_data) # check expression of second-stage ineq cons correct # check these individually @@ -175,8 +175,8 @@ def test_construct_separation_problem_ss_eq_and_dr_cons(self): by the separation problems. """ # check DR equation is active - model_data, config = build_simple_model_data(objective_focus="worst_case") - separation_model = construct_separation_problem(model_data, config) + model_data = build_simple_model_data(objective_focus="worst_case") + separation_model = construct_separation_problem(model_data) self.assertTrue(separation_model.second_stage.decision_rule_eqns[0].active) @@ -199,8 +199,8 @@ def test_construct_separation_problem_uncertainty_components(self): Test separation problem handles uncertain parameter variable components as expected. """ - model_data, config = build_simple_model_data(objective_focus="worst_case") - separation_model = construct_separation_problem(model_data, config) + model_data = build_simple_model_data(objective_focus="worst_case") + separation_model = construct_separation_problem(model_data) uncertainty_blk = separation_model.uncertainty boxcon1, boxcon2 = uncertainty_blk.uncertainty_cons_list paramvar1, paramvar2 = uncertainty_blk.uncertain_param_var_list @@ -233,11 +233,11 @@ def test_construct_separation_problem_uncertain_factor_param_components(self): Test separation problem uncertainty components for uncertainty set requiring auxiliary variables. """ - model_data, config = build_simple_model_data(objective_focus="worst_case") - config.uncertainty_set = FactorModelSet( + model_data = build_simple_model_data(objective_focus="worst_case") + model_data.config.uncertainty_set = FactorModelSet( origin=[1, 0], beta=1, number_of_factors=2, psi_mat=[[1, 2.5], [0, 1]] ) - separation_model = construct_separation_problem(model_data, config) + separation_model = construct_separation_problem(model_data) uncertainty_blk = separation_model.uncertainty *matrix_product_cons, aux_sum_con = uncertainty_blk.uncertainty_cons_list paramvar1, paramvar2 = uncertainty_blk.uncertain_param_var_list @@ -274,8 +274,8 @@ def test_construct_separation_problem_uncertain_factor_param_components(self): class TestGroupSecondStageIneqConsByPriority(unittest.TestCase): def test_group_ss_ineq_constraints_by_priority(self): - model_data, config = build_simple_model_data() - separation_model = construct_separation_problem(model_data, config) + model_data = build_simple_model_data() + separation_model = construct_separation_problem(model_data) # build mock separation data-like object # since we are testing only the grouping method @@ -284,7 +284,7 @@ def test_group_ss_ineq_constraints_by_priority(self): separation_priority_order=model_data.separation_priority_order, ) - priority_groups = group_ss_ineq_constraints_by_priority(separation_data, config) + priority_groups = group_ss_ineq_constraints_by_priority(separation_data) self.assertEqual(list(priority_groups.keys()), [2, 0]) ss_ineq_cons = separation_model.second_stage.inequality_cons diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 5a1278f1618..b8817b747b2 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -964,12 +964,26 @@ class ModelData: values. """ - def __init__(self, original_model, timing): + def __init__(self, original_model, config, timing): self.original_model = original_model self.timing = timing + self.config = config + self.separation_priority_order = dict() # working model will be addressed by preprocessing self.working_model = None - self.separation_priority_order = dict() + + def preprocess(self, user_var_partitioning): + """ + Preprocess model data. + + See `preprocess_model_data()`. + + Returns + ------- + bool + True if robust infeasibility detected, False otherwise. + """ + return preprocess_model_data(self, user_var_partitioning) def get_var_bound_pairs(var): @@ -1160,7 +1174,7 @@ def get_var_certain_uncertain_bounds(var, uncertain_params): return certain_bounds, uncertain_bounds -def get_effective_var_partitioning(model_data, config): +def get_effective_var_partitioning(model_data): """ Partition the in-scope variables of the input model according to known nonadjustability to the uncertain parameters. @@ -1179,14 +1193,13 @@ def get_effective_var_partitioning(model_data, config): ---------- model_data : model data object Main model data object. - config : ConfigDict - PyROS solver options. Returns ------- effective_partitioning : VariablePartitioning Effective variable partitioning. """ + config = model_data.config working_model = model_data.working_model user_var_partitioning = model_data.working_model.user_var_partitioning @@ -1346,7 +1359,7 @@ def get_effective_var_partitioning(model_data, config): ) -def add_effective_var_partitioning(model_data, config): +def add_effective_var_partitioning(model_data): """ Obtain a repartitioning of the in-scope variables of the working model according to known adjustability to the @@ -1357,12 +1370,8 @@ def add_effective_var_partitioning(model_data, config): ---------- model_data : model data object Main model data object. - config : ConfigDict - PyROS solver options. """ - effective_partitioning = get_effective_var_partitioning( - model_data=model_data, config=config - ) + effective_partitioning = get_effective_var_partitioning(model_data) model_data.working_model.effective_var_partitioning = VariablePartitioning( **effective_partitioning._asdict() ) @@ -1444,7 +1453,7 @@ def remove_all_var_bounds(var): var.domain = Reals -def turn_nonadjustable_var_bounds_to_constraints(model_data, config): +def turn_nonadjustable_var_bounds_to_constraints(model_data): """ Reformulate uncertain bounds for the nonadjustable (i.e. effective first-stage) variables of the working @@ -1460,8 +1469,6 @@ def turn_nonadjustable_var_bounds_to_constraints(model_data, config): ---------- model_data : model data object Main model data object. - config : ConfigDict - PyROS solver settings. """ working_model = model_data.working_model nonadjustable_vars = working_model.effective_var_partitioning.first_stage_variables @@ -1499,7 +1506,7 @@ def turn_nonadjustable_var_bounds_to_constraints(model_data, config): # the interface for separation priority ordering -def turn_adjustable_var_bounds_to_constraints(model_data, config): +def turn_adjustable_var_bounds_to_constraints(model_data): """ Reformulate domain and declared bounds for the adjustable (i.e., effective second-stage and effective state) @@ -1516,8 +1523,6 @@ def turn_adjustable_var_bounds_to_constraints(model_data, config): ---------- model_data : model data object Main model data object. - config : ConfigDict - PyROS solver settings. """ working_model = model_data.working_model @@ -1563,7 +1568,7 @@ def turn_adjustable_var_bounds_to_constraints(model_data, config): # the interface for separation priority ordering -def setup_working_model(model_data, config, user_var_partitioning): +def setup_working_model(model_data, user_var_partitioning): """ Set up (construct) the working model based on user inputs, and add it to the model data object. @@ -1572,12 +1577,11 @@ def setup_working_model(model_data, config, user_var_partitioning): ---------- model_data : model data object Main model data object. - config : ConfigDict - PyROS solve settings. user_var_partitioning : VariablePartitioning User-based partitioning of the in-scope variables of the input model. """ + config = model_data.config original_model = model_data.original_model # add temporary block to help keep track of variables @@ -1632,7 +1636,7 @@ def setup_working_model(model_data, config, user_var_partitioning): working_model.original_active_inequality_cons.append(con) -def standardize_inequality_constraints(model_data, config): +def standardize_inequality_constraints(model_data): """ Standardize the inequality constraints of the working model, and classify them as first-stage inequalities or second-stage @@ -1643,6 +1647,7 @@ def standardize_inequality_constraints(model_data, config): model_data : model data object Main model data object, containing the working model. """ + config = model_data.config working_model = model_data.working_model uncertain_params_set = ComponentSet(working_model.uncertain_params) adjustable_vars_set = ComponentSet( @@ -1865,7 +1870,7 @@ def declare_objective_expressions(working_model, objective, sense=minimize): ) -def standardize_active_objective(model_data, config): +def standardize_active_objective(model_data): """ Standardize the active objective of the working model. @@ -1886,6 +1891,7 @@ def standardize_active_objective(model_data, config): model_data : model data object Main model data object. """ + config = model_data.config working_model = model_data.working_model active_obj = next( @@ -2072,7 +2078,7 @@ def check_time_limit_reached(timing_data, config): ) -def reformulate_state_var_independent_eq_cons(model_data, config): +def reformulate_state_var_independent_eq_cons(model_data): """ Reformulate second-stage equality constraints that are independent of the state variables. @@ -2096,8 +2102,6 @@ def reformulate_state_var_independent_eq_cons(model_data, config): ---------- model_data : model data object Main model data object. - config : ConfigDict - PyROS solver settings. Returns ------- @@ -2105,6 +2109,7 @@ def reformulate_state_var_independent_eq_cons(model_data, config): True if model found to be robust infeasible, False otherwise. """ + config = model_data.config working_model = model_data.working_model ep = working_model.effective_var_partitioning @@ -2289,7 +2294,7 @@ def reformulate_state_var_independent_eq_cons(model_data, config): return False -def preprocess_model_data(model_data, config, user_var_partitioning): +def preprocess_model_data(model_data, user_var_partitioning): """ Preprocess user inputs to modeling objects from which PyROS subproblems can be efficiently constructed. @@ -2298,8 +2303,6 @@ def preprocess_model_data(model_data, config, user_var_partitioning): ---------- model_data : model data object Main model data object. - config : ConfigDict - PyROS solver options. user_var_partitioning : VariablePartitioning User-based partitioning of the in-scope variables of the input model. @@ -2310,31 +2313,32 @@ def preprocess_model_data(model_data, config, user_var_partitioning): True if RO problem was found to be robust infeasible, False otherwise. """ - setup_working_model(model_data, config, user_var_partitioning) + config = model_data.config + setup_working_model(model_data, user_var_partitioning) # extract as many truly nonadjustable variables as possible # from the second-stage and state variables config.progress_logger.debug("Repartitioning variables by nonadjustability...") - add_effective_var_partitioning(model_data, config) + add_effective_var_partitioning(model_data) # different treatment for effective first-stage # than for effective second-stage and state variables config.progress_logger.debug("Turning some variable bounds to constraints...") - turn_nonadjustable_var_bounds_to_constraints(model_data, config) - turn_adjustable_var_bounds_to_constraints(model_data, config) + turn_nonadjustable_var_bounds_to_constraints(model_data) + turn_adjustable_var_bounds_to_constraints(model_data) config.progress_logger.debug("Standardizing the model constraints...") - standardize_inequality_constraints(model_data, config) + standardize_inequality_constraints(model_data) standardize_equality_constraints(model_data) # includes epigraph reformulation config.progress_logger.debug("Standardizing the active objective...") - standardize_active_objective(model_data, config) + standardize_active_objective(model_data) # DR components are added only per effective second-stage variable config.progress_logger.debug("Adding decision rule components...") - add_decision_rule_variables(model_data, config) - add_decision_rule_constraints(model_data, config) + add_decision_rule_variables(model_data) + add_decision_rule_constraints(model_data) # the epigraph and DR variables are also first-stage config.progress_logger.debug("Finalizing nonadjustable variables...") @@ -2352,12 +2356,12 @@ def preprocess_model_data(model_data, config, user_var_partitioning): config.progress_logger.debug( "Reformulating state variable-independent second-stage equality constraints..." ) - robust_infeasible = reformulate_state_var_independent_eq_cons(model_data, config) + robust_infeasible = reformulate_state_var_independent_eq_cons(model_data) return robust_infeasible -def log_model_statistics(model_data, config): +def log_model_statistics(model_data): """ Log statistics for the preprocessed model. @@ -2365,9 +2369,8 @@ def log_model_statistics(model_data, config): ---------- model_data : model data object Main model data object. - config : ConfigDict - PyROS solver settings. """ + config = model_data.config working_model = model_data.working_model ep = working_model.effective_var_partitioning @@ -2438,7 +2441,7 @@ def log_model_statistics(model_data, config): info_log_func(f" Second-stage inequalities : {num_second_stage_ineq_cons}") -def add_decision_rule_variables(model_data, config): +def add_decision_rule_variables(model_data): """ Add variables parameterizing the (polynomial) decision rules to the working model. @@ -2447,8 +2450,6 @@ def add_decision_rule_variables(model_data, config): ---------- model_data : model data object Model data. - config : ConfigDict - PyROS solver options. Notes ----- @@ -2459,6 +2460,7 @@ def add_decision_rule_variables(model_data, config): variables, since the decision rules for such variables are necessarily nonstatic. """ + config = model_data.config effective_second_stage_vars = ( model_data.working_model.effective_var_partitioning.second_stage_variables ) @@ -2498,7 +2500,7 @@ def add_decision_rule_variables(model_data, config): eff_ss_var_to_dr_var_map[eff_ss_var] = indexed_dr_var -def add_decision_rule_constraints(model_data, config): +def add_decision_rule_constraints(model_data): """ Add decision rule equality constraints to the working model. @@ -2506,10 +2508,8 @@ def add_decision_rule_constraints(model_data, config): ---------- model_data : model data object Main model data object. - config : ConfigDict - PyROS solver options. """ - + config = model_data.config effective_second_stage_vars = ( model_data.working_model.effective_var_partitioning.second_stage_variables ) @@ -2595,7 +2595,7 @@ def enforce_dr_degree(working_blk, config, degree): def load_final_solution( - model_data, master_soln, config, original_user_var_partitioning + model_data, master_soln, original_user_var_partitioning ): """ Load variable values from the master problem to the @@ -2605,12 +2605,11 @@ def load_final_solution( ---------- master_soln : master solution object Master solution object, containing the master model. - config : ConfigDict - PyROS solver options. original_user_var_partitioning : VariablePartitioning User partitioning of the variables of the original model. """ + config = model_data.config if config.objective_focus == ObjectiveType.nominal: soln_master_blk = master_soln.nominal_block elif config.objective_focus == ObjectiveType.worst_case: From b263173d3fa13f0280534e8ce130f434476687e6 Mon Sep 17 00:00:00 2001 From: jasherma Date: Sun, 11 Aug 2024 22:06:18 -0400 Subject: [PATCH 2180/3044] Apply black and typos --- .../pyros/separation_problem_methods.py | 7 +- .../contrib/pyros/tests/test_preprocessor.py | 124 ++++++++++-------- pyomo/contrib/pyros/util.py | 6 +- 3 files changed, 70 insertions(+), 67 deletions(-) diff --git a/pyomo/contrib/pyros/separation_problem_methods.py b/pyomo/contrib/pyros/separation_problem_methods.py index fc201cbc1c7..3ae8ebc5d9e 100644 --- a/pyomo/contrib/pyros/separation_problem_methods.py +++ b/pyomo/contrib/pyros/separation_problem_methods.py @@ -766,8 +766,7 @@ def evaluate_ss_ineq_con_violations( # evaluate violations for all second-stage inequality # constraints provided violations_by_sep_solution = get_sep_objective_values( - separation_data=separation_data, - ss_ineq_cons=ss_ineq_cons_to_evaluate, + separation_data=separation_data, ss_ineq_cons=ss_ineq_cons_to_evaluate ) # normalize constraint violation: i.e. divide by @@ -789,9 +788,7 @@ def evaluate_ss_ineq_con_violations( return (violating_param_realization, scaled_violations, constraint_violated) -def initialize_separation( - ss_ineq_con_to_maximize, separation_data, master_data -): +def initialize_separation(ss_ineq_con_to_maximize, separation_data, master_data): """ Initialize separation problem variables using the solution to the most recent master problem. diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index 9ce2fbc4662..9660cbb1777 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -129,9 +129,7 @@ def test_effective_partitioning_system(self): } for dr_order in [0, 1, 2]: config.decision_rule_order = dr_order - actual_partitioning = get_effective_var_partitioning( - model_data=model_data - ) + actual_partitioning = get_effective_var_partitioning(model_data=model_data) for vartype, expected_vars in expected_partitioning.items(): actual_vars = getattr(actual_partitioning, vartype) self.assertEqual( @@ -1986,9 +1984,7 @@ def test_reformulate_nonlinear_state_var_independent_eq_con(self): wm.second_stage.equality_cons["eq_con_2"].set_value(m.u * (m.x1 - 1) == 0) with LoggingIntercept(level=logging.DEBUG) as LOG: - robust_infeasible = reformulate_state_var_independent_eq_cons( - model_data - ) + robust_infeasible = reformulate_state_var_independent_eq_cons(model_data) err_msg = LOG.getvalue() self.assertRegex( @@ -2206,13 +2202,15 @@ def test_preprocessor_effective_var_partitioning_static_dr(self): model_data, user_var_partitioning = self.build_test_model_data() om = model_data.original_model config = model_data.config - config.update(dict( - uncertain_params=[om.q], - objective_focus=ObjectiveType.worst_case, - decision_rule_order=0, - progress_logger=logger, - separation_priority_order=dict(), - )) + config.update( + dict( + uncertain_params=[om.q], + objective_focus=ObjectiveType.worst_case, + decision_rule_order=0, + progress_logger=logger, + separation_priority_order=dict(), + ) + ) preprocess_model_data(model_data, user_var_partitioning) ep = model_data.working_model.effective_var_partitioning ublk = model_data.working_model.user_model @@ -2271,13 +2269,15 @@ def test_preprocessor_effective_var_partitioning_nonstatic_dr(self, name, dr_ord model_data, user_var_partitioning = self.build_test_model_data() om = model_data.original_model config = model_data.config - config.update(dict( - uncertain_params=[om.q], - objective_focus=ObjectiveType.worst_case, - decision_rule_order=dr_order, - progress_logger=logger, - separation_priority_order=dict(), - )) + config.update( + dict( + uncertain_params=[om.q], + objective_focus=ObjectiveType.worst_case, + decision_rule_order=dr_order, + progress_logger=logger, + separation_priority_order=dict(), + ) + ) preprocess_model_data(model_data, user_var_partitioning) ep = model_data.working_model.effective_var_partitioning ublk = model_data.working_model.user_model @@ -2338,13 +2338,15 @@ def test_preprocessor_constraint_partitioning_nonstatic_dr( """ model_data, user_var_partitioning = self.build_test_model_data() om = model_data.original_model - model_data.config.update(dict( - uncertain_params=[om.q], - objective_focus=ObjectiveType[obj_focus], - decision_rule_order=dr_order, - progress_logger=logger, - separation_priority_order=dict(ineq3=2), - )) + model_data.config.update( + dict( + uncertain_params=[om.q], + objective_focus=ObjectiveType[obj_focus], + decision_rule_order=dr_order, + progress_logger=logger, + separation_priority_order=dict(ineq3=2), + ) + ) preprocess_model_data(model_data, user_var_partitioning) working_model = model_data.working_model @@ -2532,20 +2534,20 @@ def test_preprocessor_coefficient_matching( model_data, user_var_partitioning = self.build_test_model_data() om = model_data.original_model config = model_data.config - config.update(dict( - uncertain_params=[om.q], - objective_focus=ObjectiveType.worst_case, - decision_rule_order=dr_order, - progress_logger=logger, - separation_priority_order=dict(), - )) + config.update( + dict( + uncertain_params=[om.q], + objective_focus=ObjectiveType.worst_case, + decision_rule_order=dr_order, + progress_logger=logger, + separation_priority_order=dict(), + ) + ) # for static DR, problem should be robust infeasible # due to the coefficient matching constraints derived # from bounds on z5 - robust_infeasible = preprocess_model_data( - model_data, user_var_partitioning - ) + robust_infeasible = preprocess_model_data(model_data, user_var_partitioning) self.assertIsInstance(robust_infeasible, bool) self.assertEqual(robust_infeasible, expected_robust_infeas) @@ -2616,13 +2618,15 @@ def test_preprocessor_objective_standardization(self, name, dr_order): model_data, user_var_partitioning = self.build_test_model_data() om = model_data.original_model config = model_data.config - config.update(dict( - uncertain_params=[om.q], - objective_focus=ObjectiveType.worst_case, - decision_rule_order=dr_order, - progress_logger=logger, - separation_priority_order=dict(), - )) + config.update( + dict( + uncertain_params=[om.q], + objective_focus=ObjectiveType.worst_case, + decision_rule_order=dr_order, + progress_logger=logger, + separation_priority_order=dict(), + ) + ) preprocess_model_data(model_data, user_var_partitioning) ublk = model_data.working_model.user_model @@ -2666,13 +2670,15 @@ def test_preprocessor_log_model_statistics_affine_dr(self, obj_focus): model_data, user_var_partitioning = self.build_test_model_data() om = model_data.original_model config = model_data.config - config.update(dict( - uncertain_params=[om.q], - objective_focus=ObjectiveType[obj_focus], - decision_rule_order=1, - progress_logger=logger, - separation_priority_order=dict(), - )) + config.update( + dict( + uncertain_params=[om.q], + objective_focus=ObjectiveType[obj_focus], + decision_rule_order=1, + progress_logger=logger, + separation_priority_order=dict(), + ) + ) preprocess_model_data(model_data, user_var_partitioning) # expected model stats worked out by hand @@ -2718,13 +2724,15 @@ def test_preprocessor_log_model_statistics_quadratic_dr(self, obj_focus): model_data, user_var_partitioning = self.build_test_model_data() om = model_data.original_model config = model_data.config - config.update(dict( - uncertain_params=[om.q], - objective_focus=ObjectiveType[obj_focus], - decision_rule_order=2, - progress_logger=logger, - separation_priority_order=dict(), - )) + config.update( + dict( + uncertain_params=[om.q], + objective_focus=ObjectiveType[obj_focus], + decision_rule_order=2, + progress_logger=logger, + separation_priority_order=dict(), + ) + ) preprocess_model_data(model_data, user_var_partitioning) # expected model stats worked out by hand diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index b8817b747b2..46a9dab03e6 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -960,7 +960,7 @@ class ModelData: the PyROS cutting set subproblems are to be constructed. separation_priority_order : dict - Mapping from contraint names to separation priority + Mapping from constraint names to separation priority values. """ @@ -2594,9 +2594,7 @@ def enforce_dr_degree(working_blk, config, degree): dr_var.unfix() -def load_final_solution( - model_data, master_soln, original_user_var_partitioning -): +def load_final_solution(model_data, master_soln, original_user_var_partitioning): """ Load variable values from the master problem to the original model. From 1a2027670b26e6971130e1cd6bbbf5d91e68f3de Mon Sep 17 00:00:00 2001 From: jlgearh Date: Sun, 11 Aug 2024 21:26:42 -0600 Subject: [PATCH 2181/3044] - Updated logic used to tighten bounds for OBBT to use a constraint list instead of named constraints and updated associated tests --- .../alternative_solutions/lp_enum_solnpool.py | 1 + pyomo/contrib/alternative_solutions/obbt.py | 10 ++++------ .../alternative_solutions/tests/test_obbt.py | 20 +++++++++++++++++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py index 8947fb806ee..8f06889dfa2 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py @@ -60,6 +60,7 @@ def cut_generator_callback(self, cb_m, cb_opt, cb_where): if len(self.solutions) >= self.num_solutions: # TODO: (nicely) terminate the solve + # cb_m.terminate() return num_non_zero = 0 diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index e9a8310be1a..3a589e29baf 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -223,6 +223,8 @@ def obbt_analysis_bounds_and_solutions( obj_constraints = aos_utils._add_objective_constraint( aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap ) + if refine_discrete_bounds: + aos_block.bound_constraints = pe.ConstraintList() new_constraint = False if len(obj_constraints) > 0: new_constraint = True @@ -296,15 +298,11 @@ def obbt_analysis_bounds_and_solutions( if refine_discrete_bounds and not var.is_continuous(): if sense == pe.minimize and var.lb < obj_val: - bound_name = var.name + "_" + str.lower(bound_dir) - bound = pe.Constraint(expr=var >= obj_val) - setattr(aos_block, bound_name, bound) + aos_block.bound_constraints.add(var >= obj_val) new_constraint = True if sense == pe.maximize and var.ub > obj_val: - bound_name = var.name + "_" + str.lower(bound_dir) - bound = pe.Constraint(expr=var <= obj_val) - setattr(aos_block, bound_name, bound) + aos_block.bound_constraints.add(var <= obj_val) new_constraint = True # An infeasibleOrUnbounded status code will imply the problem is diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index 184aa22a310..6805fe8a18a 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -211,9 +211,25 @@ def test_bound_refinement(self, mip_solver): assert len(solns) == 2 * len(all_bounds) + 1 for var, bounds in all_bounds.items(): if m.var_bounds[var][0] > var.lb: - assert hasattr(m._obbt, var.name + "_lb") + match = False + for idx in m._obbt.bound_constraints: + const = m._obbt.bound_constraints[idx] + if var is const.body and bounds[0] == const.lb: + match = True + break + assert match, "Constaint not found for {} lower bound {}".format( + var, bounds[0] + ) if m.var_bounds[var][1] < var.ub: - assert hasattr(m._obbt, var.name + "_ub") + match = False + for idx in m._obbt.bound_constraints: + const = m._obbt.bound_constraints[idx] + if var is const.body and bounds[1] == const.ub: + match = True + break + assert match, "Constaint not found for {} upper bound {}".format( + var, bounds[1] + ) def test_obbt_infeasible(self, mip_solver): """ From 65c37a8456556ae88670e0dfd2d95f3923b0fd3e Mon Sep 17 00:00:00 2001 From: whart222 Date: Mon, 12 Aug 2024 05:22:39 -0600 Subject: [PATCH 2182/3044] Several changes 1. Exposing the logcontext class. 2. Adding warnings and debugging information for the balas function. In particular, this method now warns if no binary variables are found. 3. Reworking balas example to work with knapsack. --- .../alternative_solutions.rst | 23 +++++++++++-------- .../contrib/alternative_solutions/__init__.py | 1 + pyomo/contrib/alternative_solutions/balas.py | 7 ++++++ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst index 19951b6a742..c988bdec62b 100644 --- a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst +++ b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst @@ -45,17 +45,20 @@ The following functions are defined in the alternative-solutions library: Usage Example ------------- -Many of functions in the alternative-solutions library have similar options, so we simply illustrate the ``enumerate_binary_solutions`` function. We define a simple model whose feasible space is an isosceles right triangle. The optimal solutiosn fall along the hypotenuse, where :math:`x + y == 5`. Alternative near-optimal feasible points have integer objective values ranging from 0 to 4. +Many of functions in the alternative-solutions library have similar options, so we simply illustrate the ``enumerate_binary_solutions`` function. We define a simple model whose feasible space is an isosceles right triangle. The optimal solutions fall along the hypotenuse, where :math:`x + y == 5`. Alternative near-optimal feasible points have integer objective values ranging from 0 to 4. .. doctest:: >>> import pyomo.environ as pyo + >>> values = [10, 40, 30, 50] + >>> weights = [5, 4, 6, 3] + >>> capacity = 10 + >>> m = pyo.ConcreteModel() - >>> m.x = pyo.Var(within=pyo.NonNegativeIntegers, bounds=(0, 5)) - >>> m.y = pyo.Var(within=pyo.NonNegativeIntegers, bounds=(0, 5)) - >>> m.o = pyo.Objective(expr=m.x + m.y, sense=pyo.maximize) - >>> m.c = pyo.Constraint(expr=m.x + m.y <= 5) + >>> m.x = pyo.Var(range(4), within=pyo.Binary) + >>> m.o = pyo.Objective(expr=sum(values[i] * m.x[i] for i in range(4)), sense=pyo.maximize) + >>> m.c = pyo.Constraint(expr=sum(weights[i] * m.x[i] for i in range(4)) <= capacity) We can execute the ``enumerate_binary_solutions`` function to generate a list of ``Solution`` objects that represent alternative optimal solutions: @@ -64,7 +67,7 @@ We can execute the ``enumerate_binary_solutions`` function to generate a list of >>> import pyomo.contrib.alternative_solutions as aos >>> solns = aos.enumerate_binary_solutions(m, num_solutions=100, solver="glpk") - >>> assert len(solns) == 1 + >>> assert len(solns) == 10 Each ``Solution`` object contains information about the objective and variables, and it includes various methods to access this information. For example: @@ -75,10 +78,12 @@ Each ``Solution`` object contains information about the objective and variables, { "fixed_variables": [], "objective": "o", - "objective_value": 5.0, + "objective_value": 90.0, "solution": { - "x": 5, - "y": 0 + "x[0]": 0, + "x[1]": 1, + "x[2]": 0, + "x[3]": 1 } } diff --git a/pyomo/contrib/alternative_solutions/__init__.py b/pyomo/contrib/alternative_solutions/__init__.py index fae0c7f79c0..0b01e359879 100644 --- a/pyomo/contrib/alternative_solutions/__init__.py +++ b/pyomo/contrib/alternative_solutions/__init__.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from pyomo.contrib.alternative_solutions.aos_utils import logcontext from pyomo.contrib.alternative_solutions.solution import Solution from pyomo.contrib.alternative_solutions.solnpool import gurobi_generate_solutions from pyomo.contrib.alternative_solutions.balas import enumerate_binary_solutions diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index 8cff6bde305..489642a5b63 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -91,6 +91,10 @@ def enumerate_binary_solutions( binary_variables = [ var for var in all_variables if var.is_binary() and not var.is_fixed() ] + logger.debug( + "Analysis using %d binary variables: %s" + % (len(binary_variables), " ".join(var.name for var in binary_variables)) + ) else: binary_variables = ComponentSet() non_binary_variables = [] @@ -110,6 +114,9 @@ def enumerate_binary_solutions( orig_objective = aos_utils.get_active_objective(model) + if len(binary_variables) == 0: + logger.warn("No binary variables found!") + # # Setup solver # From 91d2562be968e61dc73aaa4639982aca9674b8f7 Mon Sep 17 00:00:00 2001 From: whart222 Date: Mon, 12 Aug 2024 05:28:01 -0600 Subject: [PATCH 2183/3044] Spelling fix --- pyomo/contrib/alternative_solutions/tests/test_obbt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/tests/test_obbt.py b/pyomo/contrib/alternative_solutions/tests/test_obbt.py index 6805fe8a18a..d2b180c9e3d 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_obbt.py +++ b/pyomo/contrib/alternative_solutions/tests/test_obbt.py @@ -217,7 +217,7 @@ def test_bound_refinement(self, mip_solver): if var is const.body and bounds[0] == const.lb: match = True break - assert match, "Constaint not found for {} lower bound {}".format( + assert match, "Constraint not found for {} lower bound {}".format( var, bounds[0] ) if m.var_bounds[var][1] < var.ub: @@ -227,7 +227,7 @@ def test_bound_refinement(self, mip_solver): if var is const.body and bounds[1] == const.ub: match = True break - assert match, "Constaint not found for {} upper bound {}".format( + assert match, "Constraint not found for {} upper bound {}".format( var, bounds[1] ) From a1233de6f6f3f7f1498393afa6017db1350c95fd Mon Sep 17 00:00:00 2001 From: whart222 Date: Mon, 12 Aug 2024 05:34:20 -0600 Subject: [PATCH 2184/3044] Better exception handling When the warmstart option is not supported by a solver --- pyomo/contrib/alternative_solutions/obbt.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 3a589e29baf..aa2810f05dd 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -193,10 +193,8 @@ def obbt_analysis_bounds_and_solutions( results = opt.solve( model, warmstart=warmstart, tee=tee, load_solutions=False ) - except: - # Assume that we failed b.c. of warm starts - results = None - if results is None: + except ValueError: + # An exception occurs if the solver does not recognize the warmstart option results = opt.solve(model, tee=tee, load_solutions=False) condition = results.solver.termination_condition optimal_tc = pe.TerminationCondition.optimal @@ -277,9 +275,8 @@ def obbt_analysis_bounds_and_solutions( results = opt.solve( model, warmstart=warmstart, tee=tee, load_solutions=False ) - except: - results = None - if results is None: + except ValueError: + # An exception occurs if the solver does not recognize the warmstart option results = opt.solve(model, tee=tee, load_solutions=False) condition = results.solver.termination_condition new_constraint = False From 49b89fcba3b403dd1c3ea69c21b272477f73e741 Mon Sep 17 00:00:00 2001 From: whart222 Date: Mon, 12 Aug 2024 05:34:58 -0600 Subject: [PATCH 2185/3044] Revising text describing the simple example here --- doc/OnlineDocs/contributed_packages/alternative_solutions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst index c988bdec62b..f3594760c73 100644 --- a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst +++ b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst @@ -45,7 +45,7 @@ The following functions are defined in the alternative-solutions library: Usage Example ------------- -Many of functions in the alternative-solutions library have similar options, so we simply illustrate the ``enumerate_binary_solutions`` function. We define a simple model whose feasible space is an isosceles right triangle. The optimal solutions fall along the hypotenuse, where :math:`x + y == 5`. Alternative near-optimal feasible points have integer objective values ranging from 0 to 4. +Many of functions in the alternative-solutions library have similar options, so we simply illustrate the ``enumerate_binary_solutions`` function. We define a simple knapsack example whose alternative solutions have integer objective values ranging from 0 to 90. .. doctest:: From c6aafd205df9876b8dcc648af33e4dce137e059e Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 10:09:01 -0400 Subject: [PATCH 2186/3044] Cleaned up reactor experiment example --- .../doe/examples/reactor_experiment.py | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_experiment.py b/pyomo/contrib/doe/examples/reactor_experiment.py index 52056c01fa2..2202f26384b 100644 --- a/pyomo/contrib/doe/examples/reactor_experiment.py +++ b/pyomo/contrib/doe/examples/reactor_experiment.py @@ -18,6 +18,13 @@ # ======================== class ReactorExperiment(Experiment): def __init__(self, data, nfe, ncp): + """ + Arguments + --------- + data: object containing vital experimental information + nfe: number of finite elements + ncp: number of collocation points for the finite elements + """ self.data = data self.nfe = nfe self.ncp = ncp @@ -108,13 +115,6 @@ def finalize_model(self): 1. Extracting useful information for the model to align with the experiment. (Here: CA0, t_final, t_control) 2. Discretizing the model subject to this information. - - Arguments - --------- - m: Pyomo model - data: object containing vital experimental information - nfe: number of finite elements - ncp: number of collocation points for the finite elements """ m = self.model @@ -167,10 +167,6 @@ def label_experiment(self): """ Example for annotating (labeling) the model with a full experiment. - - Arguments - --------- - """ m = self.model @@ -196,9 +192,6 @@ def label_experiment(self): # Identify design variables (experiment inputs) for the model m.experiment_inputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) # Add experimental input label for initial concentration - # m.experiment_inputs.update( - # (m.CA[t], None) for t in [m.t.first()] - # ) m.experiment_inputs[m.CA[m.t.first()]] = None # Add experimental input label for Temperature m.experiment_inputs.update((m.T[t], None) for t in m.t_control) From b8b911cd86d08abc3640ed6aca6b7bad789a15a1 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 10:12:54 -0400 Subject: [PATCH 2187/3044] Added else to rescale function, commented params function --- pyomo/contrib/doe/utils.py | 67 ++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/doe/utils.py b/pyomo/contrib/doe/utils.py index 889bebbea33..649a87b6fe3 100644 --- a/pyomo/contrib/doe/utils.py +++ b/pyomo/contrib/doe/utils.py @@ -54,44 +54,49 @@ def rescale_FIM(FIM, param_vals): (len(param_vals.shape) == 2) and (param_vals.shape[0] != 1) ): raise ValueError( - "param_vals should be a vector of dimensions (1, n_params). The shape you provided is {}.".format( + "param_vals should be a vector of dimensions: 1 by `n_params`. The shape you provided is {}.".format( param_vals.shape ) ) if len(param_vals.shape) == 1: param_vals = np.array([param_vals]) + else: + raise ValueError( + "param_vals should be a list or numpy array of dimensions: 1 by `n_params`" + ) scaling_mat = (1 / param_vals).transpose().dot((1 / param_vals)) scaled_FIM = np.multiply(FIM, scaling_mat) return scaled_FIM -def get_parameters_from_suffix(suffix, fix_vars=False): - """ - Finds the Params within the suffix provided. It will also check to see - if there are Vars in the suffix provided. ``fix_vars`` will indicate - if we should fix all the Vars in the set or not. - - Parameters - ---------- - suffix: pyomo Suffix object, contains the components to be checked - as keys - fix_vars: boolean, whether or not to fix the Vars, default = False - - Returns - ------- - param_list: list of Param - """ - param_list = [] - - # FIX THE MODEL TREE ISSUE WHERE I GET base_model. INSTEAD OF - # Check keys if they are Param or Var. Fix the vars if ``fix_vars`` is True - for k, v in suffix.items(): - if isinstance(k, ParamData): - param_list.append(k.name) - elif isinstance(k, VarData): - if fix_vars: - k.fix() - else: - pass # ToDo: Write error for suffix keys that aren't ParamData or VarData - - return param_list +# TODO: Add swapping parameters for variables helper function +# def get_parameters_from_suffix(suffix, fix_vars=False): +# """ +# Finds the Params within the suffix provided. It will also check to see +# if there are Vars in the suffix provided. ``fix_vars`` will indicate +# if we should fix all the Vars in the set or not. +# +# Parameters +# ---------- +# suffix: pyomo Suffix object, contains the components to be checked +# as keys +# fix_vars: boolean, whether or not to fix the Vars, default = False +# +# Returns +# ------- +# param_list: list of Param +# """ +# param_list = [] +# +# # FIX THE MODEL TREE ISSUE WHERE I GET base_model. INSTEAD OF +# # Check keys if they are Param or Var. Fix the vars if ``fix_vars`` is True +# for k, v in suffix.items(): +# if isinstance(k, ParamData): +# param_list.append(k.name) +# elif isinstance(k, VarData): +# if fix_vars: +# k.fix() +# else: +# pass # ToDo: Write error for suffix keys that aren't ParamData or VarData +# +# return param_list From a9b1b032dcdfbfcc317ffc153cadb8d9db4796d2 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 10:33:52 -0400 Subject: [PATCH 2188/3044] Edited language in documentation --- doc/OnlineDocs/contributed_packages/doe/doe.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index 4be5d0ad8dc..516967f9245 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -116,14 +116,14 @@ In order to solve problems of the above, Pyomo.DoE implements the 2-stage stocha Pyomo.DoE Required Inputs -------------------------------- -The required inputs to the Pyomo.DoE solver is an Experiment object. The experiment object must have a get_labeled_model function which returns a pyomo model with four special labeled components as suffixes on the pyomo model. This is in line with the convention used in the new interface in the contributed package, `Parmest `_. The four suffix components are: +The required input to the Pyomo.DoE solver is an ``Experiment`` object. The experiment object must have a ``get_labeled_model`` function which returns a Pyomo model with four ``Suffix`` components identifying the parts of the model used in MBDoE analysis. This is in line with the convention used in the parameter estimation tool, `Parmest `_. The four ``Suffix`` components are: -* experiment_inputs - The experimental design decisions -* experiment_outputs - The values measured during the experiment -* measurement_error - The error associated with individual values measured during the experiment -* unknown_parameters - Those parameters in the model that are estimated using the measured values during the experiment +* ``experiment_inputs`` - The experimental design decisions +* ``experiment_outputs`` - The values measured during the experiment +* ``measurement_error`` - The error associated with individual values measured during the experiment +* ``unknown_parameters`` - Those parameters in the model that are estimated using the measured values during the experiment -An example an Experiment object that builds and labels the model is shown in the next few sections. +An example ``Experiment`` object that builds and labels the model is shown in the next few sections. Pyomo.DoE Usage Example ----------------------- @@ -162,6 +162,7 @@ Step 0: Import Pyomo and the Pyomo.DoE module and create an Experiment object >>> import pyomo.environ as pyo >>> from pyomo.contrib.doe import DesignOfExperiments >>> import numpy as np + >>> import idaes # Required to add ipopt linear solvers to path if not done manually .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py :lines: 19-24 From 9d5fa4c9e42765cfcf1551aa8b065dea312dbd55 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 10:45:10 -0400 Subject: [PATCH 2189/3044] Fixed fragile line numbers for reactor exp in documentation --- doc/OnlineDocs/contributed_packages/doe/doe.rst | 15 ++++++++++----- pyomo/contrib/doe/examples/reactor_experiment.py | 10 ++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index 516967f9245..e8f9660046b 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -165,7 +165,8 @@ Step 0: Import Pyomo and the Pyomo.DoE module and create an Experiment object >>> import idaes # Required to add ipopt linear solvers to path if not done manually .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :lines: 19-24 + :start-after: ======================== + :end-before: End constructor definition Step 1: Define the Pyomo process model ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -173,7 +174,8 @@ Step 1: Define the Pyomo process model The process model for the reaction kinetics problem is shown below. We build the model in without any data or discretization. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :lines: 33-102 + :start-after: Create flexible model without data + :end-before: End equation def'n Step 2: Finalize the Pyomo process model ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -181,7 +183,8 @@ Step 2: Finalize the Pyomo process model Here we add data to the model and finalize the discretization. This step is required before the model can be labeled. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :lines: 104-157 + :start-after: End equation def'n + :end-before: End model finalization Step 3: Label the important information for model on the DoE object ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -189,7 +192,8 @@ Step 3: Label the important information for model on the DoE object We label the four important groups as defined before. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :lines: 159-203 + :start-after: End model finalization + :end-before: End model labeling Step 4: We give the experiment object a get_labeled_model function ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -197,7 +201,8 @@ Step 4: We give the experiment object a get_labeled_model function This function summarizes the previous 3 steps and is used by `Pyomo.DoE` to build the model to perform optimal experimental design. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :lines: 26-31 + :start-after: End constructor definition + :end-before: Create flexible model without data Step 5: Exploratory analysis (Enumeration) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/pyomo/contrib/doe/examples/reactor_experiment.py b/pyomo/contrib/doe/examples/reactor_experiment.py index 2202f26384b..8098deaf906 100644 --- a/pyomo/contrib/doe/examples/reactor_experiment.py +++ b/pyomo/contrib/doe/examples/reactor_experiment.py @@ -30,6 +30,9 @@ def __init__(self, data, nfe, ncp): self.ncp = ncp self.model = None + ############################# + # End constructor definition + def get_labeled_model(self): if self.model is None: self.create_model() @@ -37,6 +40,7 @@ def get_labeled_model(self): self.label_experiment() return self.model + # Create flexible model without data def create_model(self): """ This is an example user model provided to DoE library. @@ -163,6 +167,9 @@ def T_control(m, t): neighbour_t = max(tc for tc in control_points if tc < t) return m.T[t] == m.T[neighbour_t] + ######################### + # End model finalization + def label_experiment(self): """ Example for annotating (labeling) the model with a @@ -200,3 +207,6 @@ def label_experiment(self): m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) # Add labels to all unknown parameters with nominal value as the value m.unknown_parameters.update((k, pyo.value(k)) for k in [m.A1, m.A2, m.E1, m.E2]) + + ######################### + # End model labeling From 8d954925485827347e5e3a465cbc49d875bb740e Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 10:52:50 -0400 Subject: [PATCH 2190/3044] Clarified text in documentation --- .../contributed_packages/doe/doe.rst | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index e8f9660046b..c297a141597 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -153,8 +153,8 @@ The goal of MBDoE is to optimize the experiment design variables :math:`\boldsym The observation errors are assumed to be independent both in time and across measurements with a constant standard deviation of 1 M for each species. -Step 0: Import Pyomo and the Pyomo.DoE module and create an Experiment object -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Step 0: Import Pyomo and the Pyomo.DoE module and create an ``Experiment`` class +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. doctest:: @@ -186,8 +186,8 @@ Here we add data to the model and finalize the discretization. This step is requ :start-after: End equation def'n :end-before: End model finalization -Step 3: Label the important information for model on the DoE object -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Step 3: Label the information needed for DoE analysis +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ We label the four important groups as defined before. @@ -195,10 +195,10 @@ We label the four important groups as defined before. :start-after: End model finalization :end-before: End model labeling -Step 4: We give the experiment object a get_labeled_model function -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Step 4: Implement the ``get_labeled_model`` method +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -This function summarizes the previous 3 steps and is used by `Pyomo.DoE` to build the model to perform optimal experimental design. +This method utilizes the previous 3 steps and is used by `Pyomo.DoE` to build the model to perform optimal experimental design. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py :start-after: End constructor definition @@ -210,10 +210,8 @@ Step 5: Exploratory analysis (Enumeration) Exploratory analysis is suggested to enumerate the design space to check if the problem is identifiable, i.e., ensure that D-, E-optimality metrics are not small numbers near zero, and Modified E-optimality is not a big number. -Pyomo.DoE accomplishes the exploratory analysis with the ``compute_FIM_full_factorial`` function. -It allows users to define any number of design decisions. Heatmaps can be drawn by two design variables, fixing other design variables. -1D curve can be drawn by one design variable, fixing all other variables. -The function ``compute_FIM_full_factorial`` enumerates over the design space, each MBDoE problem accomplished by ``compute_FIM`` method. +Pyomo.DoE can perform exploratory sensitivity analysis with the ``compute_FIM_full_factorial`` function. +The ``compute_FIM_full_factorial`` function generates a grid over the design space as specified by the user. Each grid point represents an MBDoE problem solved using ``compute_FIM`` method. In this way, sensitivity of the FIM over the design space can be evaluated. The following code executes the above problem description: From 26921e0a2f77822e0415b0d8012066c36cd5fb70 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 10:55:48 -0400 Subject: [PATCH 2191/3044] Remove more line number fragility in documentation --- doc/OnlineDocs/contributed_packages/doe/doe.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index c297a141597..43eb2b2a496 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -231,7 +231,8 @@ Step 6: Performing an optimal experimental design This is an example of running an experimental design to determine an optimal experiment for the reactor example. We utilize the determinant as the objective. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_example.py - :lines: 24-90 + :start-after: Read in file + :end-before: Print out a results summary When run, the optimal design is an initial concentration of 5.0 mol/L and an initial temperature of 494 K with all other temperatures being 300 K. The corresponding log-10 determinant of the FIM is 13.75 From 95068733caf2dc09d2a63c10cf4d0793264b77aa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 12 Aug 2024 09:04:08 -0600 Subject: [PATCH 2192/3044] Provide implicit filter callback argument mapping for scalar Sets --- pyomo/core/base/set.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 26f44ffc08a..bfc5c7b6aa0 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -2246,19 +2246,6 @@ def __init__(self, *args, **kwds): IndexedComponent.__init__(self, *args, **kwds) - if ( - self._validate.__class__ is ParameterizedIndexedCallInitializer - and not self.parent_component().is_indexed() - ): - # TBD [JDS: 8/2024]: should we deprecate the "expanded - # tuple" version of the validate callback for scalar sets? - # It is widely used and we can (reasonably reliably) map to - # the expected behavior. - orig_fcn = self._validate._fcn - self._validate = ParameterizedScalarCallInitializer( - lambda m, v: orig_fcn(m, *v), True - ) - # HACK to make the "counted call" syntax work. We wait until # after the base class is set up so that is_indexed() is # reliable. @@ -2277,6 +2264,26 @@ def __init__(self, *args, **kwds): if self._init_dimen.constant(): self._dimen = self._init_dimen(self.parent_block(), None) + if self._validate.__class__ is ParameterizedIndexedCallInitializer: + # TBD [JDS: 8/2024]: should we deprecate the "expanded + # tuple" version of the validate callback for scalar sets? + # It is widely used and we can (reasonably reliably) map to + # the expected behavior... + orig_fcn = self._validate._fcn + self._validate = ParameterizedScalarCallInitializer( + lambda m, v: orig_fcn(m, *v), True + ) + + if self._filter.__class__ is ParameterizedIndexedCallInitializer: + # TBD [JDS: 8/2024]: should we deprecate the "expanded + # tuple" version of the filter callback for scalar sets? + # It is widely used and we can (reasonably reliably) map to + # the expected behavior... + orig_fcn = self._filter._fcn + self._filter = ParameterizedScalarCallInitializer( + lambda m, v: orig_fcn(m, *v), True + ) + @deprecated( "check_values() is deprecated: Sets only contain valid members", version='5.7' ) From 9462cee70d43cf1a7169278b422b69f4a4949d96 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 11:08:14 -0400 Subject: [PATCH 2193/3044] Combined example files and updated documentation with change --- .../contributed_packages/doe/doe.rst | 9 +++-- pyomo/contrib/doe/examples/reactor_example.py | 37 ++++++++++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index 43eb2b2a496..213c094d4b2 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -215,8 +215,9 @@ The ``compute_FIM_full_factorial`` function generates a grid over the design spa The following code executes the above problem description: -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_compute_factorial_FIM.py - :lines: 24-95 +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_example.py + :start-after: Read in file + :end-before: End sensitivity analysis An example output of the code above, a design exploration for the initial concentration and temperature as experimental design variables with 9 values, produces the four figures summarized below: @@ -228,10 +229,10 @@ A heatmap shows the change of the objective function, a.k.a. the experimental in Step 6: Performing an optimal experimental design ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -This is an example of running an experimental design to determine an optimal experiment for the reactor example. We utilize the determinant as the objective. +In step 5, the DoE object was constructed to perform an exploratory sensitivity analysis. The same object can be used to design an optimal experiment with a single line of code. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_example.py - :start-after: Read in file + :start-after: Begin optimal DoE :end-before: Print out a results summary When run, the optimal design is an initial concentration of 5.0 mol/L and an initial temperature of 494 K with all other temperatures being 300 K. The corresponding log-10 determinant of the FIM is 13.75 diff --git a/pyomo/contrib/doe/examples/reactor_example.py b/pyomo/contrib/doe/examples/reactor_example.py index c17bd097303..f3e08ce7cb2 100644 --- a/pyomo/contrib/doe/examples/reactor_example.py +++ b/pyomo/contrib/doe/examples/reactor_example.py @@ -19,7 +19,8 @@ from pathlib import Path -# Example to run a DOE on the reactor +# Example for sensitivity analysis on the reactor experiment +# After sensitivity analysis is done, we perform optimal DoE def run_reactor_doe(): # Read in file DATA_DIR = Path(__file__).parent @@ -67,6 +68,37 @@ def run_reactor_doe(): _only_compute_fim_lower=True, ) + # Make design ranges to compute the full factorial design + design_ranges = {"CA[0]": [1, 5, 9], "T[0]": [300, 700, 9]} + + # Compute the full factorial design with the sequential FIM calculation + doe_obj.compute_FIM_full_factorial(design_ranges=design_ranges, method="sequential") + + # Plot the results + doe_obj.draw_factorial_figure( + sensitivity_design_variables=["CA[0]", "T[0]"], + fixed_design_variables={ + "T[0.125]": 300, + "T[0.25]": 300, + "T[0.375]": 300, + "T[0.5]": 300, + "T[0.625]": 300, + "T[0.75]": 300, + "T[0.875]": 300, + "T[1]": 300, + }, + title_text="Reactor Example", + xlabel_text="Concentration of A (M)", + ylabel_text="Initial Temperature (K)", + figure_file_name="example_reactor_compute_FIM", + log_scale=False, + ) + + ########################### + # End sensitivity analysis + + # Begin optimal DoE + #################### doe_obj.run_doe() # Print out a results summary @@ -90,6 +122,9 @@ def run_reactor_doe(): print(doe_obj.results["Experiment Design Names"]) + ################### + # End optimal DoE + if __name__ == "__main__": run_reactor_doe() From d5d24a63e3a1e68b4e66c5d01479d01a8f8e624d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 12 Aug 2024 09:24:35 -0600 Subject: [PATCH 2194/3044] Update NL template classes to match Pyomo naming convention --- pyomo/repn/ampl.py | 10 +++++----- pyomo/repn/tests/ampl/test_ampl_nl.py | 2 +- pyomo/repn/tests/nl_diff.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyomo/repn/ampl.py b/pyomo/repn/ampl.py index 7e949e37088..876f579fb8d 100644 --- a/pyomo/repn/ampl.py +++ b/pyomo/repn/ampl.py @@ -75,7 +75,7 @@ def _create_strict_inequality_map(vars_): } -class text_nl_debug_template(object): +class TextNLDebugTemplate(object): unary = { 'log': 'o43\t#log\n', 'log10': 'o42\t#log10\n', @@ -172,8 +172,8 @@ def _strip_template_comments(vars_, base_): # The "standard" text mode template is the debugging template with the # comments removed -class text_nl_template(text_nl_debug_template): - _strip_template_comments(vars(), text_nl_debug_template) +class TextNLTemplate(TextNLDebugTemplate): + _strip_template_comments(vars(), TextNLDebugTemplate) _create_strict_inequality_map(vars()) @@ -200,7 +200,7 @@ def name(self): class AMPLRepn(object): __slots__ = ('nl', 'mult', 'const', 'linear', 'nonlinear', 'named_exprs') - template = text_nl_template + template = TextNLTemplate def __init__(self, const, linear, nonlinear): self.nl = None @@ -446,7 +446,7 @@ def to_expr(self, var_map): class DebugAMPLRepn(AMPLRepn): __slots__ = () - template = text_nl_debug_template + template = TextNLDebugTemplate def handle_negation_node(visitor, node, arg1): diff --git a/pyomo/repn/tests/ampl/test_ampl_nl.py b/pyomo/repn/tests/ampl/test_ampl_nl.py index 53c34c4c5db..38c9d5b9dd5 100644 --- a/pyomo/repn/tests/ampl/test_ampl_nl.py +++ b/pyomo/repn/tests/ampl/test_ampl_nl.py @@ -31,7 +31,7 @@ from ..nl_diff import load_and_compare_nl_baseline import pyomo.repn.plugins.ampl.ampl_ as ampl_ -from pyomo.repn.ampl import text_nl_debug_template as template +from pyomo.repn.ampl import TextNLDebugTemplate as template gsr = ampl_.generate_standard_repn thisdir = this_file_dir() diff --git a/pyomo/repn/tests/nl_diff.py b/pyomo/repn/tests/nl_diff.py index 736be4f9606..d94d50e82e6 100644 --- a/pyomo/repn/tests/nl_diff.py +++ b/pyomo/repn/tests/nl_diff.py @@ -15,7 +15,7 @@ from difflib import SequenceMatcher, unified_diff from pyomo.repn.tests.diffutils import compare_floats, load_baseline -from pyomo.repn.ampl import text_nl_debug_template as template +from pyomo.repn.ampl import TextNLDebugTemplate as template _norm_whitespace = re.compile(r'[^\S\n]+') _norm_integers = re.compile(r'(?m)\.0+$') From 77a1bc5d74423d305bd659994cb3e8fb6c08a620 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 11:25:05 -0400 Subject: [PATCH 2195/3044] Add context manager, remove np.isclose in tests --- pyomo/contrib/doe/__init__.py | 2 +- pyomo/contrib/doe/examples/reactor_example.py | 4 ++-- pyomo/contrib/doe/tests/test_doe_build.py | 17 +++++++++-------- pyomo/contrib/doe/tests/test_doe_errors.py | 4 ++-- pyomo/contrib/doe/tests/test_doe_solve.py | 4 ++-- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index 775fc6fb47d..04e237c18db 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -9,4 +9,4 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ from .doe import DesignOfExperiments, ObjectiveLib, FiniteDifferenceStep -from .utils import rescale_FIM, get_parameters_from_suffix +from .utils import rescale_FIM diff --git a/pyomo/contrib/doe/examples/reactor_example.py b/pyomo/contrib/doe/examples/reactor_example.py index f3e08ce7cb2..1570c870181 100644 --- a/pyomo/contrib/doe/examples/reactor_example.py +++ b/pyomo/contrib/doe/examples/reactor_example.py @@ -26,8 +26,8 @@ def run_reactor_doe(): DATA_DIR = Path(__file__).parent file_path = DATA_DIR / "result.json" - f = open(file_path) - data_ex = json.load(f) + with open(file_path) as f: + data_ex = json.load(f) # Put temperature control time points into correct format for reactor experiment data_ex["control_points"] = { diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 50b7498a6d0..465c6f5c8e0 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -31,8 +31,9 @@ DATA_DIR = Path(__file__).parent file_path = DATA_DIR / "result.json" -f = open(file_path) -data_ex = json.load(f) +with open(file_path) as f: + data_ex = json.load(f) + data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} @@ -151,9 +152,9 @@ def test_reactor_fd_central_check_fd_eqns(self): continue other_param_val = pyo.value(k) - assert np.isclose(other_param_val, v) + self.assertAlmostEqual(other_param_val, v) - assert np.isclose(param_val, param_val_from_step) + self.assertAlmostEqual(param_val, param_val_from_step) def test_reactor_fd_backward_check_fd_eqns(self): fd_method = "backward" @@ -182,7 +183,7 @@ def test_reactor_fd_backward_check_fd_eqns(self): param_val_from_step = model.scenario_blocks[0].unknown_parameters[ pyo.ComponentUID(param).find_component_on(model.scenario_blocks[0]) ] * (1 + diff) - assert np.isclose(param_val, param_val_from_step) + self.assertAlmostEqual(param_val, param_val_from_step) for k, v in model.scenario_blocks[s].unknown_parameters.items(): name_ind = k.name.split(".").index("scenario_blocks[" + str(s) + "]") @@ -194,7 +195,7 @@ def test_reactor_fd_backward_check_fd_eqns(self): continue other_param_val = pyo.value(k) - assert np.isclose(other_param_val, v) + self.assertAlmostEqual(other_param_val, v) def test_reactor_fd_forward_check_fd_eqns(self): fd_method = "forward" @@ -223,7 +224,7 @@ def test_reactor_fd_forward_check_fd_eqns(self): param_val_from_step = model.scenario_blocks[0].unknown_parameters[ pyo.ComponentUID(param).find_component_on(model.scenario_blocks[0]) ] * (1 + diff) - assert np.isclose(param_val, param_val_from_step) + self.assertAlmostEqual(param_val, param_val_from_step) for k, v in model.scenario_blocks[s].unknown_parameters.items(): name_ind = k.name.split(".").index("scenario_blocks[" + str(s) + "]") @@ -235,7 +236,7 @@ def test_reactor_fd_forward_check_fd_eqns(self): continue other_param_val = pyo.value(k) - assert np.isclose(other_param_val, v) + self.assertAlmostEqual(other_param_val, v) def test_reactor_fd_central_design_fixing(self): fd_method = "central" diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 2aa7886a65e..eb6d268ccc8 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -32,8 +32,8 @@ DATA_DIR = Path(__file__).parent file_path = DATA_DIR / "result.json" -f = open(file_path) -data_ex = json.load(f) +with open(file_path) as f: + data_ex = json.load(f) data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 3870cd9344c..4dd15bc04df 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -39,8 +39,8 @@ DATA_DIR = Path(__file__).parent file_path = DATA_DIR / "result.json" -f = open(file_path) -data_ex = json.load(f) +with open(file_path) as f: + data_ex = json.load(f) data_ex["control_points"] = {float(k): v for k, v in data_ex["control_points"].items()} From f61a98ba4ef2f5fbbc4b2c436261480761cd6b8c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 12 Aug 2024 09:27:35 -0600 Subject: [PATCH 2196/3044] Fix typo, variable naming convention --- pyomo/core/base/initializer.py | 2 +- pyomo/core/base/set.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/core/base/initializer.py b/pyomo/core/base/initializer.py index 946d6fd3167..6e73cc176d1 100644 --- a/pyomo/core/base/initializer.py +++ b/pyomo/core/base/initializer.py @@ -83,7 +83,7 @@ def Initializer( if arg.__class__ in function_types: if allow_generators or inspect.isgeneratorfunction(arg): raise ValueError( - "Generator functions are not allowed when prassing additional args" + "Generator functions are not allowed when passing additional args" ) _args = inspect.getfullargspec(arg) _nargs = len(_args.args) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index bfc5c7b6aa0..7e4744bb9d1 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1459,7 +1459,7 @@ def _cb_check_set_end(self, val_iter): yield value def _cb_validate_filter(self, mode, val_iter): - failFalse = mode == 'validate' + fail_false = mode == 'validate' comp = self.parent_component() fcn = getattr(comp, '_' + mode) block = comp.parent_block() @@ -1524,7 +1524,7 @@ def _cb_validate_filter(self, mode, val_iter): except Exception as e: exc = e if flag is not None: - if failFalse: + if fail_false: raise ValueError( "The value=%s violates the validation rule of Set %s" % (value, self.name) From ef5c9ba9baf45f272bb1efffcfd6f65feea38691 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 11:37:55 -0400 Subject: [PATCH 2197/3044] Reused reactor experiment in tests, fixed bug in experiment --- pyomo/contrib/doe/examples/reactor_experiment.py | 12 ++++++------ pyomo/contrib/doe/tests/test_doe_build.py | 4 +++- pyomo/contrib/doe/tests/test_doe_solve.py | 4 +++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_experiment.py b/pyomo/contrib/doe/examples/reactor_experiment.py index 8098deaf906..37e97250a7e 100644 --- a/pyomo/contrib/doe/examples/reactor_experiment.py +++ b/pyomo/contrib/doe/examples/reactor_experiment.py @@ -180,21 +180,21 @@ def label_experiment(self): # Set measurement labels m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) # Add CA to experiment outputs - m.experiment_outputs.update((m.CA[t], None) for t in m.t) + m.experiment_outputs.update((m.CA[t], None) for t in m.t_control) # Add CB to experiment outputs - m.experiment_outputs.update((m.CB[t], None) for t in m.t) + m.experiment_outputs.update((m.CB[t], None) for t in m.t_control) # Add CC to experiment outputs - m.experiment_outputs.update((m.CC[t], None) for t in m.t) + m.experiment_outputs.update((m.CC[t], None) for t in m.t_control) # Adding error for measurement values (assuming no covariance and constant error for all measurements) m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) concentration_error = 1e-2 # Error in concentration measurement # Add measurement error for CA - m.measurement_error.update((m.CA[t], concentration_error) for t in m.t) + m.measurement_error.update((m.CA[t], concentration_error) for t in m.t_control) # Add measurement error for CB - m.measurement_error.update((m.CB[t], concentration_error) for t in m.t) + m.measurement_error.update((m.CB[t], concentration_error) for t in m.t_control) # Add measurement error for CC - m.measurement_error.update((m.CC[t], concentration_error) for t in m.t) + m.measurement_error.update((m.CC[t], concentration_error) for t in m.t_control) # Identify design variables (experiment inputs) for the model m.experiment_inputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 465c6f5c8e0..22124d28f19 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -20,7 +20,9 @@ import pyomo.common.unittest as unittest from pyomo.contrib.doe import DesignOfExperiments -from pyomo.contrib.doe.tests.experiment_class_example import FullReactorExperiment +from pyomo.contrib.doe.examples.reactor_example import ( + ReactorExperiment as FullReactorExperiment, +) import pyomo.environ as pyo diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 4dd15bc04df..5f6f42d8ae3 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -22,7 +22,9 @@ import pyomo.common.unittest as unittest from pyomo.contrib.doe import DesignOfExperiments -from pyomo.contrib.doe.tests.experiment_class_example import FullReactorExperiment +from pyomo.contrib.doe.examples.reactor_example import ( + ReactorExperiment as FullReactorExperiment, +) from pyomo.contrib.doe.tests.experiment_class_example_flags import ( FullReactorExperimentBad, ) From 353e769b382ca627b3313ba040f600e8b0a052c1 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 11:38:19 -0400 Subject: [PATCH 2198/3044] Edited small phrase in documentation --- doc/OnlineDocs/contributed_packages/doe/doe.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index 213c094d4b2..e3f729e761f 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -171,7 +171,7 @@ Step 0: Import Pyomo and the Pyomo.DoE module and create an ``Experiment`` class Step 1: Define the Pyomo process model ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The process model for the reaction kinetics problem is shown below. We build the model in without any data or discretization. +The process model for the reaction kinetics problem is shown below. We build the model without any data or discretization. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py :start-after: Create flexible model without data From b2106a08dc9fbf38974e2efd422ef52e48823aa8 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 11:41:20 -0400 Subject: [PATCH 2199/3044] Removing unused import --- pyomo/contrib/doe/tests/experiment_class_example.py | 1 - pyomo/contrib/doe/tests/experiment_class_example_flags.py | 1 - 2 files changed, 2 deletions(-) diff --git a/pyomo/contrib/doe/tests/experiment_class_example.py b/pyomo/contrib/doe/tests/experiment_class_example.py index 03c8ca3a784..e31b0da8374 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example.py +++ b/pyomo/contrib/doe/tests/experiment_class_example.py @@ -15,7 +15,6 @@ from pyomo.contrib.parmest.experiment import Experiment import itertools -import json # ======================== diff --git a/pyomo/contrib/doe/tests/experiment_class_example_flags.py b/pyomo/contrib/doe/tests/experiment_class_example_flags.py index e0d8363b20a..5a25bbf4b37 100644 --- a/pyomo/contrib/doe/tests/experiment_class_example_flags.py +++ b/pyomo/contrib/doe/tests/experiment_class_example_flags.py @@ -15,7 +15,6 @@ from pyomo.contrib.parmest.experiment import Experiment import itertools -import json # ======================== From ead6f9385062f059318c90562dd8a3604a0bf8f1 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 09:48:21 -0600 Subject: [PATCH 2200/3044] Adding a (currently failing) test for cloning a transformed model --- .../piecewise/tests/test_nonlinear_to_pwl.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 67d0bb9f098..81a1e186234 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -112,6 +112,33 @@ def test_log_constraint_uniform_grid(self): (x1, x2, x3) = 1.0009, 5.5, 9.9991 self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) + @unittest.skipUnless(numpy_available, "Numpy is not available") + def test_clone_transformed_model(self): + m = self.make_model() + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, + num_points=3, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + ) + + twin = m.clone() + + # cons is transformed + self.assertFalse(twin.cons.active) + + pwlf = list( + twin.component_data_objects(PiecewiseLinearFunction, descend_into=True) + ) + self.assertEqual(len(pwlf), 1) + pwlf = pwlf[0] + + points = [(1.0009,), (5.5,), (9.9991,)] + (x1, x2, x3) = 1.0009, 5.5, 9.9991 + + self.check_pw_linear_log_x(twin, pwlf, x1, x2, x3) + @unittest.skipUnless(numpy_available, "Numpy is not available") def test_log_constraint_random_grid(self): m = self.make_model() From 16873d024e9c3742ab4e77dc2fbc7b02683280f7 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 11:48:33 -0400 Subject: [PATCH 2201/3044] Remove tests result.json dependency --- pyomo/contrib/doe/tests/test_doe_build.py | 2 +- pyomo/contrib/doe/tests/test_doe_errors.py | 2 +- pyomo/contrib/doe/tests/test_doe_solve.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 22124d28f19..8d31b21ee81 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -31,7 +31,7 @@ ipopt_available = SolverFactory("ipopt").available() DATA_DIR = Path(__file__).parent -file_path = DATA_DIR / "result.json" +file_path = DATA_DIR / ".." / "examples" / "result.json" with open(file_path) as f: data_ex = json.load(f) diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index eb6d268ccc8..76599d3c468 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -30,7 +30,7 @@ ipopt_available = SolverFactory("ipopt").available() DATA_DIR = Path(__file__).parent -file_path = DATA_DIR / "result.json" +file_path = DATA_DIR / ".." / "examples" / "result.json" with open(file_path) as f: data_ex = json.load(f) diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 5f6f42d8ae3..34a01e8c0e4 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -39,7 +39,7 @@ k_aug_available = SolverFactory('k_aug', solver_io='nl', validate=False) DATA_DIR = Path(__file__).parent -file_path = DATA_DIR / "result.json" +file_path = DATA_DIR / ".." / "examples" / "result.json" with open(file_path) as f: data_ex = json.load(f) From 2b2c33f4b9d1af544983eb228309d69772c437ba Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:50:38 -0400 Subject: [PATCH 2202/3044] Delete pyomo/contrib/doe/tests/experiment_class_example.py Remove deprecated file --- .../doe/tests/experiment_class_example.py | 231 ------------------ 1 file changed, 231 deletions(-) delete mode 100644 pyomo/contrib/doe/tests/experiment_class_example.py diff --git a/pyomo/contrib/doe/tests/experiment_class_example.py b/pyomo/contrib/doe/tests/experiment_class_example.py deleted file mode 100644 index e31b0da8374..00000000000 --- a/pyomo/contrib/doe/tests/experiment_class_example.py +++ /dev/null @@ -1,231 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ -# === Required imports === -import pyomo.environ as pyo -from pyomo.dae import ContinuousSet, DerivativeVar, Simulator - -from pyomo.contrib.parmest.experiment import Experiment - -import itertools - -# ======================== - - -def expand_model_components(m, base_components, index_sets): - """ - Takes model components and index sets and returns the - model component labels. - - Arguments - --------- - m: Pyomo model - base_components: list of variables from model 'm' - index_sets: list, same length as base_components, where each - element is a list of index sets, or None - """ - for val, indexes in itertools.zip_longest(base_components, index_sets): - # If the variable has no index, - # add just the model component - if not val.is_indexed(): - yield val - # If the component is indexed but no - # index supplied, add all indices - elif indexes is None: - yield from val.values() - else: - for j in itertools.product(*indexes): - yield val[j] - - -class ReactorExperiment(Experiment): - def __init__(self, data, nfe, ncp): - self.data = data - self.nfe = nfe - self.ncp = ncp - self.model = None - - def get_labeled_model(self): - if self.model is None: - self.create_model() - self.finalize_model() - self.label_experiment() - return self.model - - def create_model(self): - """ - This is an example user model provided to DoE library. - It is a dynamic problem solved by Pyomo.DAE. - - Return - ------ - m: a Pyomo.DAE model - """ - - m = self.model = pyo.ConcreteModel() - - # Model parameters - m.R = pyo.Param(mutable=False, initialize=8.314) - - # Define model variables - ######################## - # time - m.t = ContinuousSet(bounds=[0, 1]) - - # Concentrations - m.CA = pyo.Var(m.t, within=pyo.NonNegativeReals) - m.CB = pyo.Var(m.t, within=pyo.NonNegativeReals) - m.CC = pyo.Var(m.t, within=pyo.NonNegativeReals) - - # Temperature - m.T = pyo.Var(m.t, within=pyo.NonNegativeReals) - - # Arrhenius rate law equations - m.A1 = pyo.Var(within=pyo.NonNegativeReals) - m.E1 = pyo.Var(within=pyo.NonNegativeReals) - m.A2 = pyo.Var(within=pyo.NonNegativeReals) - m.E2 = pyo.Var(within=pyo.NonNegativeReals) - - # Differential variables (Conc.) - m.dCAdt = DerivativeVar(m.CA, wrt=m.t) - m.dCBdt = DerivativeVar(m.CB, wrt=m.t) - - ######################## - # End variable def. - - # Equation def'n - ######################## - - # Expression for rate constants - @m.Expression(m.t) - def k1(m, t): - return m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t])) - - @m.Expression(m.t) - def k2(m, t): - return m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t])) - - # Concentration odes - @m.Constraint(m.t) - def CA_rxn_ode(m, t): - return m.dCAdt[t] == -m.k1[t] * m.CA[t] - - @m.Constraint(m.t) - def CB_rxn_ode(m, t): - return m.dCBdt[t] == m.k1[t] * m.CA[t] - m.k2[t] * m.CB[t] - - # algebraic balance for concentration of C - # Valid because the reaction system (A --> B --> C) is equimolar - @m.Constraint(m.t) - def CC_balance(m, t): - return m.CA[0] == m.CA[t] + m.CB[t] + m.CC[t] - - ######################## - # End equation def'n - - def finalize_model(self): - """ - Example finalize model function. There are two main tasks - here: - 1. Extracting useful information for the model to align - with the experiment. (Here: CA0, t_final, t_control) - 2. Discretizing the model subject to this information. - - Arguments - --------- - m: Pyomo model - data: object containing vital experimental information - nfe: number of finite elements - ncp: number of collocation points for the finite elements - """ - m = self.model - - # Unpacking data before simulation - control_points = self.data["control_points"] - - m.CA[0].value = self.data["CA0"] - m.CB[0].fix(self.data["CB0"]) - m.t.update(self.data["t_range"]) - m.t.update(control_points) - m.A1.fix(self.data["A1"]) - m.A2.fix(self.data["A2"]) - m.E1.fix(self.data["E1"]) - m.E2.fix(self.data["E2"]) - - m.CA[0].setlb(self.data["CA_bounds"][0]) - m.CA[0].setub(self.data["CA_bounds"][1]) - - m.t_control = control_points - - # Discretizing the model - discr = pyo.TransformationFactory("dae.collocation") - discr.apply_to(m, nfe=self.nfe, ncp=self.ncp, wrt=m.t) - - # Initializing Temperature in the model - cv = None - for t in m.t: - if t in control_points: - cv = control_points[t] - m.T[t].setlb(self.data["T_bounds"][0]) - m.T[t].setub(self.data["T_bounds"][1]) - m.T[t] = cv - - @m.Constraint(m.t - control_points) - def T_control(m, t): - """ - Piecewise constant Temperature between control points - """ - neighbour_t = max(tc for tc in control_points if tc < t) - return m.T[t] == m.T[neighbour_t] - - # sim.initialize_model() - - def label_experiment_impl(self, index_sets_meas): - """ - Example for annotating (labeling) the model with a - full experiment. - - Arguments - --------- - - """ - m = self.model - - # Grab measurement labels - base_comp_meas = [m.CA, m.CB, m.CC] - m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update( - (k, None) - for k in expand_model_components(m, base_comp_meas, index_sets_meas) - ) - - # Adding no error for measurements currently - m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.measurement_error.update( - (k, 1e-2) - for k in expand_model_components(m, base_comp_meas, index_sets_meas) - ) - - # Grab design variables - base_comp_des = [m.CA, m.T] - index_sets_des = [[[m.t.first()]], [m.t_control]] - m.experiment_inputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_inputs.update( - (k, None) for k in expand_model_components(m, base_comp_des, index_sets_des) - ) - - m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.value(k)) for k in [m.A1, m.A2, m.E1, m.E2]) - - -class FullReactorExperiment(ReactorExperiment): - def label_experiment(self): - m = self.model - return self.label_experiment_impl([[m.t_control], [m.t_control], [m.t_control]]) From c18f2c6d6c10d04ffe878bbd6a9fa6c77c1c153c Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:50:55 -0400 Subject: [PATCH 2203/3044] Delete pyomo/contrib/doe/tests/result.json Remove deprecated file --- pyomo/contrib/doe/tests/result.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 pyomo/contrib/doe/tests/result.json diff --git a/pyomo/contrib/doe/tests/result.json b/pyomo/contrib/doe/tests/result.json deleted file mode 100644 index 7e1b1a79a1b..00000000000 --- a/pyomo/contrib/doe/tests/result.json +++ /dev/null @@ -1 +0,0 @@ -{"CA0": 5.0, "CA_bounds": [1.0, 5.0], "CB0": 0.0, "CC0": 0.0, "t_range": [0, 1], "control_points": {"0": 500, "0.125": 300, "0.25": 300, "0.375": 300, "0.5": 300, "0.625": 300, "0.75": 300, "0.875": 300, "1": 300}, "T_bounds": [300, 700], "A1": 84.79, "A2": 371.72, "E1": 7.78, "E2": 15.05} \ No newline at end of file From cb7d92536399f613f97b898b17a178da200c35b6 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:51:17 -0400 Subject: [PATCH 2204/3044] Delete pyomo/contrib/doe/experiment.py Remove deprecated file --- pyomo/contrib/doe/experiment.py | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 pyomo/contrib/doe/experiment.py diff --git a/pyomo/contrib/doe/experiment.py b/pyomo/contrib/doe/experiment.py deleted file mode 100644 index 17a51cf667a..00000000000 --- a/pyomo/contrib/doe/experiment.py +++ /dev/null @@ -1,18 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ -class Experiment(object): - def __init__(self): - self.model = None - - def get_labeled_model(self): - raise NotImplementedError( - "Derived experiment class failed to implement get_labeled_model" - ) From 61bc6fae037c7a44d1872bab79d7a004dc72e7a5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 09:51:44 -0600 Subject: [PATCH 2205/3044] NFC: removing a comment --- pyomo/contrib/piecewise/triangulations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 7fba49d6e8d..60d701a6be1 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -14,7 +14,7 @@ from pyomo.common.errors import DeveloperError from pyomo.common.dependencies import numpy as np from pyomo.contrib.piecewise.ordered_3d_j1_triangulation_data import ( - get_hamiltonian_paths#as incremental_3d_simplex_pair_to_path, + get_hamiltonian_paths, ) From e0226b07879c1647f682dd9aaf4f43ad50bb09c1 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:56:31 -0400 Subject: [PATCH 2206/3044] Update pyomo/contrib/doe/tests/test_doe_build.py Co-authored-by: Bethany Nicholson --- pyomo/contrib/doe/tests/test_doe_build.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 8d31b21ee81..78ad3b16ed9 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -265,14 +265,14 @@ def test_reactor_fd_central_design_fixing(self): continue con_name = con_name_base + str(ind) - assert hasattr(model, con_name) + self.assertTrue(hasattr(model, con_name)) # Ensure that each set of constraints has all blocks pairs with scenario 0 # i.e., (0, 1), (0, 2), ..., (0, N) --> N - 1 constraints - assert len(getattr(model, con_name)) == (len(model.scenarios) - 1) + self.assertEqual(len(getattr(model, con_name)), (len(model.scenarios) - 1)) # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) - assert not hasattr(model, con_name_base + str(len(design_vars))) + self.assertFalse(hasattr(model, con_name_base + str(len(design_vars)))) def test_reactor_fd_backward_design_fixing(self): fd_method = "backward" From 27ed006b620cd801b90f3f99824370cc670dfac3 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 12:04:29 -0400 Subject: [PATCH 2207/3044] Revert "Update pyomo/contrib/doe/tests/test_doe_build.py" This reverts commit e0226b07879c1647f682dd9aaf4f43ad50bb09c1. --- pyomo/contrib/doe/tests/test_doe_build.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 78ad3b16ed9..8d31b21ee81 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -265,14 +265,14 @@ def test_reactor_fd_central_design_fixing(self): continue con_name = con_name_base + str(ind) - self.assertTrue(hasattr(model, con_name)) + assert hasattr(model, con_name) # Ensure that each set of constraints has all blocks pairs with scenario 0 # i.e., (0, 1), (0, 2), ..., (0, N) --> N - 1 constraints - self.assertEqual(len(getattr(model, con_name)), (len(model.scenarios) - 1)) + assert len(getattr(model, con_name)) == (len(model.scenarios) - 1) # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) - self.assertFalse(hasattr(model, con_name_base + str(len(design_vars)))) + assert not hasattr(model, con_name_base + str(len(design_vars))) def test_reactor_fd_backward_design_fixing(self): fd_method = "backward" From 451fdff3bdd6a20f88c98fbf73eae83ec8e26652 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 12:05:05 -0400 Subject: [PATCH 2208/3044] Revert "Delete pyomo/contrib/doe/tests/experiment_class_example.py" This reverts commit 2b2c33f4b9d1af544983eb228309d69772c437ba. --- .../doe/tests/experiment_class_example.py | 231 ++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 pyomo/contrib/doe/tests/experiment_class_example.py diff --git a/pyomo/contrib/doe/tests/experiment_class_example.py b/pyomo/contrib/doe/tests/experiment_class_example.py new file mode 100644 index 00000000000..e31b0da8374 --- /dev/null +++ b/pyomo/contrib/doe/tests/experiment_class_example.py @@ -0,0 +1,231 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +# === Required imports === +import pyomo.environ as pyo +from pyomo.dae import ContinuousSet, DerivativeVar, Simulator + +from pyomo.contrib.parmest.experiment import Experiment + +import itertools + +# ======================== + + +def expand_model_components(m, base_components, index_sets): + """ + Takes model components and index sets and returns the + model component labels. + + Arguments + --------- + m: Pyomo model + base_components: list of variables from model 'm' + index_sets: list, same length as base_components, where each + element is a list of index sets, or None + """ + for val, indexes in itertools.zip_longest(base_components, index_sets): + # If the variable has no index, + # add just the model component + if not val.is_indexed(): + yield val + # If the component is indexed but no + # index supplied, add all indices + elif indexes is None: + yield from val.values() + else: + for j in itertools.product(*indexes): + yield val[j] + + +class ReactorExperiment(Experiment): + def __init__(self, data, nfe, ncp): + self.data = data + self.nfe = nfe + self.ncp = ncp + self.model = None + + def get_labeled_model(self): + if self.model is None: + self.create_model() + self.finalize_model() + self.label_experiment() + return self.model + + def create_model(self): + """ + This is an example user model provided to DoE library. + It is a dynamic problem solved by Pyomo.DAE. + + Return + ------ + m: a Pyomo.DAE model + """ + + m = self.model = pyo.ConcreteModel() + + # Model parameters + m.R = pyo.Param(mutable=False, initialize=8.314) + + # Define model variables + ######################## + # time + m.t = ContinuousSet(bounds=[0, 1]) + + # Concentrations + m.CA = pyo.Var(m.t, within=pyo.NonNegativeReals) + m.CB = pyo.Var(m.t, within=pyo.NonNegativeReals) + m.CC = pyo.Var(m.t, within=pyo.NonNegativeReals) + + # Temperature + m.T = pyo.Var(m.t, within=pyo.NonNegativeReals) + + # Arrhenius rate law equations + m.A1 = pyo.Var(within=pyo.NonNegativeReals) + m.E1 = pyo.Var(within=pyo.NonNegativeReals) + m.A2 = pyo.Var(within=pyo.NonNegativeReals) + m.E2 = pyo.Var(within=pyo.NonNegativeReals) + + # Differential variables (Conc.) + m.dCAdt = DerivativeVar(m.CA, wrt=m.t) + m.dCBdt = DerivativeVar(m.CB, wrt=m.t) + + ######################## + # End variable def. + + # Equation def'n + ######################## + + # Expression for rate constants + @m.Expression(m.t) + def k1(m, t): + return m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t])) + + @m.Expression(m.t) + def k2(m, t): + return m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t])) + + # Concentration odes + @m.Constraint(m.t) + def CA_rxn_ode(m, t): + return m.dCAdt[t] == -m.k1[t] * m.CA[t] + + @m.Constraint(m.t) + def CB_rxn_ode(m, t): + return m.dCBdt[t] == m.k1[t] * m.CA[t] - m.k2[t] * m.CB[t] + + # algebraic balance for concentration of C + # Valid because the reaction system (A --> B --> C) is equimolar + @m.Constraint(m.t) + def CC_balance(m, t): + return m.CA[0] == m.CA[t] + m.CB[t] + m.CC[t] + + ######################## + # End equation def'n + + def finalize_model(self): + """ + Example finalize model function. There are two main tasks + here: + 1. Extracting useful information for the model to align + with the experiment. (Here: CA0, t_final, t_control) + 2. Discretizing the model subject to this information. + + Arguments + --------- + m: Pyomo model + data: object containing vital experimental information + nfe: number of finite elements + ncp: number of collocation points for the finite elements + """ + m = self.model + + # Unpacking data before simulation + control_points = self.data["control_points"] + + m.CA[0].value = self.data["CA0"] + m.CB[0].fix(self.data["CB0"]) + m.t.update(self.data["t_range"]) + m.t.update(control_points) + m.A1.fix(self.data["A1"]) + m.A2.fix(self.data["A2"]) + m.E1.fix(self.data["E1"]) + m.E2.fix(self.data["E2"]) + + m.CA[0].setlb(self.data["CA_bounds"][0]) + m.CA[0].setub(self.data["CA_bounds"][1]) + + m.t_control = control_points + + # Discretizing the model + discr = pyo.TransformationFactory("dae.collocation") + discr.apply_to(m, nfe=self.nfe, ncp=self.ncp, wrt=m.t) + + # Initializing Temperature in the model + cv = None + for t in m.t: + if t in control_points: + cv = control_points[t] + m.T[t].setlb(self.data["T_bounds"][0]) + m.T[t].setub(self.data["T_bounds"][1]) + m.T[t] = cv + + @m.Constraint(m.t - control_points) + def T_control(m, t): + """ + Piecewise constant Temperature between control points + """ + neighbour_t = max(tc for tc in control_points if tc < t) + return m.T[t] == m.T[neighbour_t] + + # sim.initialize_model() + + def label_experiment_impl(self, index_sets_meas): + """ + Example for annotating (labeling) the model with a + full experiment. + + Arguments + --------- + + """ + m = self.model + + # Grab measurement labels + base_comp_meas = [m.CA, m.CB, m.CC] + m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_outputs.update( + (k, None) + for k in expand_model_components(m, base_comp_meas, index_sets_meas) + ) + + # Adding no error for measurements currently + m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.measurement_error.update( + (k, 1e-2) + for k in expand_model_components(m, base_comp_meas, index_sets_meas) + ) + + # Grab design variables + base_comp_des = [m.CA, m.T] + index_sets_des = [[[m.t.first()]], [m.t_control]] + m.experiment_inputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.experiment_inputs.update( + (k, None) for k in expand_model_components(m, base_comp_des, index_sets_des) + ) + + m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) + m.unknown_parameters.update((k, pyo.value(k)) for k in [m.A1, m.A2, m.E1, m.E2]) + + +class FullReactorExperiment(ReactorExperiment): + def label_experiment(self): + m = self.model + return self.label_experiment_impl([[m.t_control], [m.t_control], [m.t_control]]) From 514a032dfd85b6730a7152f9e13ffd6b855fee26 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 12:05:18 -0400 Subject: [PATCH 2209/3044] Revert "Delete pyomo/contrib/doe/tests/result.json" This reverts commit c18f2c6d6c10d04ffe878bbd6a9fa6c77c1c153c. --- pyomo/contrib/doe/tests/result.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 pyomo/contrib/doe/tests/result.json diff --git a/pyomo/contrib/doe/tests/result.json b/pyomo/contrib/doe/tests/result.json new file mode 100644 index 00000000000..7e1b1a79a1b --- /dev/null +++ b/pyomo/contrib/doe/tests/result.json @@ -0,0 +1 @@ +{"CA0": 5.0, "CA_bounds": [1.0, 5.0], "CB0": 0.0, "CC0": 0.0, "t_range": [0, 1], "control_points": {"0": 500, "0.125": 300, "0.25": 300, "0.375": 300, "0.5": 300, "0.625": 300, "0.75": 300, "0.875": 300, "1": 300}, "T_bounds": [300, 700], "A1": 84.79, "A2": 371.72, "E1": 7.78, "E2": 15.05} \ No newline at end of file From d078c4fd3e1f499582fb6ad9253f732b0c48475f Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 12:05:28 -0400 Subject: [PATCH 2210/3044] Revert "Delete pyomo/contrib/doe/experiment.py" This reverts commit cb7d92536399f613f97b898b17a178da200c35b6. --- pyomo/contrib/doe/experiment.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 pyomo/contrib/doe/experiment.py diff --git a/pyomo/contrib/doe/experiment.py b/pyomo/contrib/doe/experiment.py new file mode 100644 index 00000000000..17a51cf667a --- /dev/null +++ b/pyomo/contrib/doe/experiment.py @@ -0,0 +1,18 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ +class Experiment(object): + def __init__(self): + self.model = None + + def get_labeled_model(self): + raise NotImplementedError( + "Derived experiment class failed to implement get_labeled_model" + ) From c1689feadfd241f2f4f401aa1e8fce06ee63a87a Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 12 Aug 2024 12:05:38 -0400 Subject: [PATCH 2211/3044] Add solver license check to coeff matching test --- pyomo/contrib/pyros/tests/test_grcs.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index f50f3de91dc..f45f21a51ea 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -766,7 +766,9 @@ def test_terminate_with_time_limit(self): ), ) - @unittest.skipUnless(baron_license_is_valid, "BARON not available and licensed") + @unittest.skipUnless( + baron_license_is_valid, "Global NLP solver is not available and licensed." + ) def test_pyros_backup_solvers(self): m = ConcreteModel() m.name = "s381" @@ -1744,6 +1746,7 @@ def test_coefficient_matching_robust_infeasible_proof_in_pyros(self): msg="Robust infeasible problem not identified via coefficient matching.", ) + @unittest.skipUnless(baron_license_is_valid, "BARON solver license is invalid.") def test_coefficient_matching_nonlinear_expr(self): """ Test behavior of PyROS solver for model with @@ -1807,6 +1810,8 @@ def test_coefficient_matching_nonlinear_expr(self): @unittest.skipUnless(scip_available, "Global NLP solver is not available.") class testBypassingSeparation(unittest.TestCase): + @unittest.skipUnless(scip_available, "SCIP is not available.") + @unittest.skipUnless(ipopt_available, "IPOPT is not available.") def test_bypass_global_separation(self): """Test bypassing of global separation solve calls.""" m = ConcreteModel() From a8957a5a68cac0e66fb139bf1076c054e34c0bcb Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 12:24:55 -0400 Subject: [PATCH 2212/3044] Removed string-based location of components --- pyomo/contrib/doe/tests/test_doe_build.py | 24 +++++++++-------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 8d31b21ee81..e4d61ca16a4 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -148,9 +148,9 @@ def test_reactor_fd_central_check_fd_eqns(self): ] * (1 + diff) for k, v in model.scenario_blocks[s].unknown_parameters.items(): - name_ind = k.name.split(".").index("scenario_blocks[" + str(s) + "]") - - if ".".join(k.name.split(".")[name_ind + 1 :]) == param.name: + if pyo.ComponentUID( + k, context=model.scenario_blocks[s] + ) == pyo.ComponentUID(param): continue other_param_val = pyo.value(k) @@ -188,12 +188,9 @@ def test_reactor_fd_backward_check_fd_eqns(self): self.assertAlmostEqual(param_val, param_val_from_step) for k, v in model.scenario_blocks[s].unknown_parameters.items(): - name_ind = k.name.split(".").index("scenario_blocks[" + str(s) + "]") - - if ( - not (s == 0) - and ".".join(k.name.split(".")[name_ind + 1 :]) == param.name - ): + if (s != 0) and pyo.ComponentUID( + k, context=model.scenario_blocks[s] + ) == pyo.ComponentUID(param): continue other_param_val = pyo.value(k) @@ -229,12 +226,9 @@ def test_reactor_fd_forward_check_fd_eqns(self): self.assertAlmostEqual(param_val, param_val_from_step) for k, v in model.scenario_blocks[s].unknown_parameters.items(): - name_ind = k.name.split(".").index("scenario_blocks[" + str(s) + "]") - - if ( - not (s == 0) - and ".".join(k.name.split(".")[name_ind + 1 :]) == param.name - ): + if (s != 0) and pyo.ComponentUID( + k, context=model.scenario_blocks[s] + ) == pyo.ComponentUID(param): continue other_param_val = pyo.value(k) From 3f57d1c6ef9d88b47cc6c253dc233edfb94e900e Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 12:35:50 -0400 Subject: [PATCH 2213/3044] Updated shoddy tests to check real values --- pyomo/contrib/doe/tests/test_doe_build.py | 63 ++++++++++++----------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index e4d61ca16a4..12c33df64a8 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -259,14 +259,12 @@ def test_reactor_fd_central_design_fixing(self): continue con_name = con_name_base + str(ind) - assert hasattr(model, con_name) - + self.assertTrue(hasattr(model, con_name)) # Ensure that each set of constraints has all blocks pairs with scenario 0 # i.e., (0, 1), (0, 2), ..., (0, N) --> N - 1 constraints - assert len(getattr(model, con_name)) == (len(model.scenarios) - 1) - - # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) - assert not hasattr(model, con_name_base + str(len(design_vars))) + self.assertEqual(len(getattr(model, con_name)), (len(model.scenarios) - 1)) + # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) + self.assertFalse(hasattr(model, con_name_base + str(len(design_vars)))) def test_reactor_fd_backward_design_fixing(self): fd_method = "backward" @@ -293,14 +291,12 @@ def test_reactor_fd_backward_design_fixing(self): continue con_name = con_name_base + str(ind) - assert hasattr(model, con_name) - + self.assertTrue(hasattr(model, con_name)) # Ensure that each set of constraints has all blocks pairs with scenario 0 # i.e., (0, 1), (0, 2), ..., (0, N) --> N - 1 constraints - assert len(getattr(model, con_name)) == (len(model.scenarios) - 1) - - # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) - assert not hasattr(model, con_name_base + str(len(design_vars))) + self.assertEqual(len(getattr(model, con_name)), (len(model.scenarios) - 1)) + # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) + self.assertFalse(hasattr(model, con_name_base + str(len(design_vars)))) def test_reactor_fd_forward_design_fixing(self): fd_method = "forward" @@ -327,14 +323,12 @@ def test_reactor_fd_forward_design_fixing(self): continue con_name = con_name_base + str(ind) - assert hasattr(model, con_name) - + self.assertTrue(hasattr(model, con_name)) # Ensure that each set of constraints has all blocks pairs with scenario 0 # i.e., (0, 1), (0, 2), ..., (0, N) --> N - 1 constraints - assert len(getattr(model, con_name)) == (len(model.scenarios) - 1) - - # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) - assert not hasattr(model, con_name_base + str(len(design_vars))) + self.assertEqual(len(getattr(model, con_name)), (len(model.scenarios) - 1)) + # Should not have any constraints sets beyond the length of design_vars - 1 (started with index 0) + self.assertFalse(hasattr(model, con_name_base + str(len(design_vars)))) def test_reactor_check_user_initialization(self): fd_method = "central" @@ -399,9 +393,10 @@ def test_get_experiment_inputs_without_blocks(self): stuff = doe_obj.get_experiment_input_values(model=doe_obj.compute_FIM_model) - assert len(stuff) == len( - [k.name for k, v in doe_obj.compute_FIM_model.experiment_inputs.items()] - ) + count = 0 + for k, v in doe_obj.compute_FIM_model.experiment_inputs.items(): + self.assertTrue(pyo.value(k) == stuff[count]) + count += 1 def test_get_experiment_outputs_without_blocks(self): fd_method = "forward" @@ -417,9 +412,10 @@ def test_get_experiment_outputs_without_blocks(self): stuff = doe_obj.get_experiment_output_values(model=doe_obj.compute_FIM_model) - assert len(stuff) == len( - [k.name for k, v in doe_obj.compute_FIM_model.experiment_outputs.items()] - ) + count = 0 + for k, v in doe_obj.compute_FIM_model.experiment_outputs.items(): + self.assertTrue(pyo.value(k) == stuff[count]) + count += 1 def test_get_measurement_error_without_blocks(self): fd_method = "forward" @@ -435,9 +431,10 @@ def test_get_measurement_error_without_blocks(self): stuff = doe_obj.get_measurement_error_values(model=doe_obj.compute_FIM_model) - assert len(stuff) == len( - [k.name for k, v in doe_obj.compute_FIM_model.measurement_error.items()] - ) + count = 0 + for k, v in doe_obj.compute_FIM_model.measurement_error.items(): + self.assertTrue(pyo.value(k) == stuff[count]) + count += 1 def test_get_unknown_parameters_without_blocks(self): fd_method = "forward" @@ -454,9 +451,10 @@ def test_get_unknown_parameters_without_blocks(self): # Make sure the values can be retrieved stuff = doe_obj.get_unknown_parameter_values(model=doe_obj.compute_FIM_model) - assert len(stuff) == len( - [k.name for k, v in doe_obj.compute_FIM_model.unknown_parameters.items()] - ) + count = 0 + for k, v in doe_obj.compute_FIM_model.unknown_parameters.items(): + self.assertTrue(pyo.value(k) == stuff[count]) + count += 1 def test_generate_blocks_without_model(self): fd_method = "forward" @@ -470,6 +468,11 @@ def test_generate_blocks_without_model(self): doe_obj._generate_scenario_blocks() + for i in doe_obj.model.parameter_scenarios: + self.assertTrue( + doe_obj.model.find_component("scenario_blocks[" + str(i) + "]") + ) + if __name__ == "__main__": unittest.main() From e0c9a0b61b66a9efd6194511ba026ca33c8e6449 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Mon, 12 Aug 2024 12:55:48 -0400 Subject: [PATCH 2214/3044] Added depracation error message --- pyomo/contrib/doe/doe.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index ab5d9df3faa..d001b309c77 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -65,7 +65,7 @@ class FiniteDifferenceStep(Enum): class DesignOfExperiments: def __init__( self, - experiment, + experiment=None, fd_formula="central", step=1e-3, objective_option="determinant", @@ -81,6 +81,7 @@ def __init__( logger_level=logging.WARNING, _Cholesky_option=True, _only_compute_fim_lower=True, + **kwargs ): """ This package enables model-based design of experiments analysis with Pyomo. @@ -145,6 +146,19 @@ def __init__( logger_level: Specify the level of the logger. Change to logging.DEBUG for all messages. """ + # Deprecation error + if 'create_model' in kwargs: + raise ValueError( + "DEPRECATION ERROR: Pyomo.DoE has been refactored. The current interface utilizes Experiment " + "objects that label unknown parameters, experiment inputs, experiment outputs and measurement " + "error. This avoids string-based naming which is fragile. For instruction to use the new " + "interface, please see Pyomo.DoE under the contributed packages documentation at " + "`https://pyomo.readthedocs.io/en/stable/`" + ) + + if experiment is None: + raise ValueError("Experiment object must be provided to perform DoE.") + # Check if the Experiment object has callable ``get_labeled_model`` function if not hasattr(experiment, "get_labeled_model"): raise ValueError( From d467bd27009084ff0a51357a208a374e730f672d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 13:12:13 -0600 Subject: [PATCH 2215/3044] Adding tests for the 3D case Hamiltonian pathsu --- .../ordered_3d_j1_triangulation_data.py | 22 +++++++++++-------- .../piecewise/tests/test_triangulations.py | 19 ++++++++++++++-- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py index edc117df6d7..7988ec159cb 100644 --- a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py +++ b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py @@ -9,17 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -""" -This code was used to generate the data structure in this file. It should never -need to be run again, but is here for the sake of documentation: - -import networkx as nx +from pyomo.common.dependencies import networkx as nx import itertools -# Get a list of 60 hamiltonian paths used in the 3d version of the ordered J1 -# triangulation, and dump it to stdout. -if __name__ == '__main__': +def _get_double_cube_graph(): # Graph of a double cube sign_vecs = list(itertools.product((-1, 1), repeat=3)) permutations = itertools.permutations(range(1, 4)) @@ -46,6 +39,17 @@ neighbor_simplex = (tuple(neighbor_sign), simplex[1]) G.add_edge(simplex, neighbor_simplex) + return G + +""" +This code was used to generate the data structure in this file. It should never +need to be run again, but is here for the sake of documentation: + +# Get a list of 60 hamiltonian paths used in the 3d version of the ordered J1 +# triangulation, and dump it to stdout. +if __name__ == '__main__': + G = _get_double_cube_graph() + # Each of these simplices has an outward face in the specified direction; also, # the +x simplex of one cube is adjacent to the -x simplex of a cube adjacent in # the x direction, and similarly for the others. diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index a88630c17a2..ea0e287ac6d 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -14,7 +14,8 @@ from unittest import skipUnless import pyomo.common.unittest as unittest from pyomo.contrib.piecewise.ordered_3d_j1_triangulation_data import ( - get_hamiltonian_paths + get_hamiltonian_paths, + _get_double_cube_graph, ) from pyomo.contrib.piecewise.triangulations import ( get_unordered_j1_triangulation, @@ -22,7 +23,7 @@ _get_Gn_hamiltonian, _get_grid_hamiltonian, ) -from pyomo.common.dependencies import numpy as np, numpy_available +from pyomo.common.dependencies import numpy as np, numpy_available, networkx_available from math import factorial import itertools @@ -224,6 +225,20 @@ def test_grid_hamiltonian_paths(self): self.check_grid_hamiltonian(3, 5) self.check_grid_hamiltonian(4, 3) +@unittest.skipUnless(networkx_available, "Networkx is not available") class TestHamiltonianPaths(unittest.TestCase): def test_hamiltonian_paths(self): + G = _get_double_cube_graph() + paths = get_hamiltonian_paths() + self.assertEqual(len(paths), 60) + + for ((s1, t1), (s2, t2)), path in paths.items(): + # ESJ: I'm not quite sure how to check this is *the right* path + # given the key? + + # Check it's Hamiltonian + self.assertEqual(len(path), 48) + # Check it's a path + for idx in range(1, 48): + self.assertTrue(G.has_edge(path[idx - 1], path[idx])) From 81d35f4f0e505debb14539dccfbedfc57bb2cac3 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 13:12:47 -0600 Subject: [PATCH 2216/3044] Black --- pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py | 3 +++ pyomo/contrib/piecewise/tests/test_triangulations.py | 1 + 2 files changed, 4 insertions(+) diff --git a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py index 7988ec159cb..009107fd6ec 100644 --- a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py +++ b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py @@ -12,6 +12,7 @@ from pyomo.common.dependencies import networkx as nx import itertools + def _get_double_cube_graph(): # Graph of a double cube sign_vecs = list(itertools.product((-1, 1), repeat=3)) @@ -41,6 +42,7 @@ def _get_double_cube_graph(): return G + """ This code was used to generate the data structure in this file. It should never need to be run again, but is here for the sake of documentation: @@ -103,6 +105,7 @@ def _get_double_cube_graph(): """ + # This file was generated using generate_ordered_3d_j1_triangulation_data.py # Data format: Keys are a pair of simplices specified as the direction they are facing, # as a standard unit vector or negative of one, and a tag, 1 or 2, disambiguating which diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index ea0e287ac6d..7217750dfb1 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -225,6 +225,7 @@ def test_grid_hamiltonian_paths(self): self.check_grid_hamiltonian(3, 5) self.check_grid_hamiltonian(4, 3) + @unittest.skipUnless(networkx_available, "Networkx is not available") class TestHamiltonianPaths(unittest.TestCase): def test_hamiltonian_paths(self): From 0d8f12b9714752046173820b947fbc54af89aba9 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 12 Aug 2024 15:58:37 -0400 Subject: [PATCH 2217/3044] Fix numpy/scipy imports --- pyomo/contrib/pyros/tests/test_grcs.py | 2 +- pyomo/contrib/pyros/tests/test_preprocessor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index f45f21a51ea..90e2d1cdbef 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -24,7 +24,7 @@ from pyomo.core.base.set_types import NonNegativeIntegers from pyomo.repn.plugins import nl_writer as pyomo_nl_writer from pyomo.common.dependencies import numpy as np, numpy_available -from pyomo.common.dependencies import scipy as scipy_available +from pyomo.common.dependencies import scipy_available from pyomo.common.errors import ApplicationError, InfeasibleConstraintException from pyomo.environ import maximize as pyo_max from pyomo.opt import ( diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index 9660cbb1777..2fe3785b4ee 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -19,7 +19,7 @@ import unittest from pyomo.common.collections import Bunch, ComponentSet, ComponentMap -from pyomo.common.dependencies import numpy as numpy_available +from pyomo.common.dependencies import numpy_available from pyomo.common.dependencies import scipy as sp, scipy_available from pyomo.common.dependencies import attempt_import from pyomo.common.log import LoggingIntercept From 890b100bbab8b83eaff2b9a6fee8f0d20e80f872 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 12 Aug 2024 16:00:29 -0400 Subject: [PATCH 2218/3044] Fix expression testing numpy int types --- pyomo/contrib/pyros/tests/test_separation.py | 4 ++-- .../pyros/tests/test_uncertainty_sets.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_separation.py b/pyomo/contrib/pyros/tests/test_separation.py index f60635e1a17..d2464a45e52 100644 --- a/pyomo/contrib/pyros/tests/test_separation.py +++ b/pyomo/contrib/pyros/tests/test_separation.py @@ -210,12 +210,12 @@ def test_construct_separation_problem_uncertainty_components(self): assertExpressionsEqual( self, boxcon1.expr, - RangedExpression((np.int64(0), paramvar1, np.int64(1)), False), + RangedExpression((np.int_(0), paramvar1, np.int_(1)), False), ) assertExpressionsEqual( self, boxcon2.expr, - RangedExpression((np.int64(0), paramvar2, np.int64(0)), False), + RangedExpression((np.int_(0), paramvar2, np.int_(0)), False), ) self.assertTrue(boxcon1.active) self.assertTrue(boxcon2.active) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index fc7bd8499f4..920d3697b9b 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -282,10 +282,10 @@ def test_set_as_constraint(self): var1, var2 = uq.uncertain_param_vars assertExpressionsEqual( - self, con1.expr, RangedExpression((np.int64(1), var1, np.int64(2)), False) + self, con1.expr, RangedExpression((np.int_(1), var1, np.int_(2)), False) ) assertExpressionsEqual( - self, con2.expr, RangedExpression((np.int64(3), var2, np.int64(4)), False) + self, con2.expr, RangedExpression((np.int_(3), var2, np.int_(4)), False) ) def test_set_as_constraint_dim_mismatch(self): @@ -575,20 +575,20 @@ def test_set_as_constraint(self): assertExpressionsEqual( self, uq.uncertainty_cons[0].expr, - m.v1 + np.float64(0) * m.v2 <= np.int64(4), + m.v1 + np.float64(0) * m.v2 <= np.int_(4), ) assertExpressionsEqual( - self, uq.uncertainty_cons[1].expr, m.v1 + m.v2 <= np.int64(6) + self, uq.uncertainty_cons[1].expr, m.v1 + m.v2 <= np.int_(6) ) assertExpressionsEqual( self, uq.uncertainty_cons[2].expr, - -np.float64(1.0) * m.v1 - np.float64(0) * m.v2 <= np.int64(-1), + -np.float64(1.0) * m.v1 - np.float64(0) * m.v2 <= np.int_(-1), ) assertExpressionsEqual( self, uq.uncertainty_cons[3].expr, - -np.float64(0) * m.v1 + np.float64(-1.0) * m.v2 <= np.int64(-3), + -np.float64(0) * m.v1 + np.float64(-1.0) * m.v2 <= np.int_(-3), ) def test_set_as_constraint_dim_mismatch(self): @@ -2251,17 +2251,17 @@ def test_set_as_constraint(self): var1, var2 = uq.uncertain_param_vars assertExpressionsEqual( - self, uq.uncertainty_cons[0].expr, var1 + np.int64(0) * var2 <= np.int64(2) + self, uq.uncertainty_cons[0].expr, var1 + np.int_(0) * var2 <= np.int_(2) ) assertExpressionsEqual( self, uq.uncertainty_cons[1].expr, - np.int64(-1) * var1 + np.int64(1) * var2 <= np.int64(-1), + np.int_(-1) * var1 + np.int_(1) * var2 <= np.int_(-1), ) assertExpressionsEqual( self, uq.uncertainty_cons[2].expr, - np.int64(-1) * var1 + np.int64(-1) * var2 <= np.int64(-1), + np.int_(-1) * var1 + np.int_(-1) * var2 <= np.int_(-1), ) def test_set_as_constraint_dim_mismatch(self): From 5e2a88a6c6f8d964a3dba38f48f5a6b01045fc64 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 12 Aug 2024 16:01:37 -0400 Subject: [PATCH 2219/3044] Apply black --- pyomo/contrib/pyros/tests/test_uncertainty_sets.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py index 920d3697b9b..5d241d75d03 100644 --- a/pyomo/contrib/pyros/tests/test_uncertainty_sets.py +++ b/pyomo/contrib/pyros/tests/test_uncertainty_sets.py @@ -573,9 +573,7 @@ def test_set_as_constraint(self): self.assertEqual(len(uq.uncertainty_cons), 4) assertExpressionsEqual( - self, - uq.uncertainty_cons[0].expr, - m.v1 + np.float64(0) * m.v2 <= np.int_(4), + self, uq.uncertainty_cons[0].expr, m.v1 + np.float64(0) * m.v2 <= np.int_(4) ) assertExpressionsEqual( self, uq.uncertainty_cons[1].expr, m.v1 + m.v2 <= np.int_(6) From a4d25c2abf4779775e02cfd804664429cced29f7 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:20:00 -0400 Subject: [PATCH 2220/3044] Fixed typo in doe.py --- pyomo/contrib/doe/doe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index d001b309c77..7e1ecde79c5 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -135,7 +135,7 @@ def __init__( get_labeled_model_args: Additional arguments for the ``get_labeled_model`` function on the Experiment object. _Cholesky_option: - Boolean value of whether or not to use the choleskyn factorization to compute the + Boolean value of whether or not to use the cholesky factorization to compute the determinant for the D-optimality criteria. This parameter should not be changed unless the user intends to make performance worse (i.e., compare an existing tool that uses the full FIM to this algorithm) From e78842a1880526bb9d6c969604c6a49aa75ea510 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:21:19 -0400 Subject: [PATCH 2221/3044] Delete pyomo/contrib/doe/tests/test_example.py Removing tests from deprecated code --- pyomo/contrib/doe/tests/test_example.py | 88 ------------------------- 1 file changed, 88 deletions(-) delete mode 100644 pyomo/contrib/doe/tests/test_example.py diff --git a/pyomo/contrib/doe/tests/test_example.py b/pyomo/contrib/doe/tests/test_example.py deleted file mode 100644 index 47ce39d596a..00000000000 --- a/pyomo/contrib/doe/tests/test_example.py +++ /dev/null @@ -1,88 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# -# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation -# Initiative (CCSI), and is copyright (c) 2022 by the software owners: -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, -# University of Toledo, West Virginia University, et al. All rights reserved. -# -# NOTICE. This Software was developed under funding from the -# U.S. Department of Energy and the U.S. Government consequently retains -# certain rights. As such, the U.S. Government has been granted for itself -# and others acting on its behalf a paid-up, nonexclusive, irrevocable, -# worldwide license in the Software to reproduce, distribute copies to the -# public, prepare derivative works, and perform publicly and display -# publicly, and to permit other to do so. -# ___________________________________________________________________________ - - -from pyomo.common.dependencies import ( - numpy as np, - numpy_available, - pandas as pd, - pandas_available, - scipy_available, -) - -import pyomo.common.unittest as unittest - -from pyomo.opt import SolverFactory - -ipopt_available = SolverFactory("ipopt").available() -k_aug_available = SolverFactory("k_aug").available(exception_flag=False) - - -class TestReactorExamples(unittest.TestCase): - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not scipy_available, "scipy is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_reactor_compute_FIM(self): - from pyomo.contrib.doe.examples import reactor_compute_FIM - - reactor_compute_FIM.main() - - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_reactor_optimize_doe(self): - from pyomo.contrib.doe.examples import reactor_optimize_doe - - reactor_optimize_doe.main() - - @unittest.skipIf(not k_aug_available, "The 'k_aug' command is not available") - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not pandas_available, "pandas is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_reactor_grid_search(self): - from pyomo.contrib.doe.examples import reactor_grid_search - - reactor_grid_search.main() - - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not pandas_available, "pandas is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_reactor_design_slim_create_model_interface(self): - from pyomo.contrib.doe.examples import reactor_design - - reactor_design.main(legacy_create_model_interface=False) - - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not pandas_available, "pandas is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") - def test_reactor_design_legacy_create_model_interface(self): - from pyomo.contrib.doe.examples import reactor_design - - reactor_design.main(legacy_create_model_interface=True) - - -if __name__ == "__main__": - unittest.main() From b036e269f2d531c1cf6bbb11d88e70328a7da5df Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:21:33 -0400 Subject: [PATCH 2222/3044] Delete pyomo/contrib/doe/tests/test_fim_doe.py Removing tests from deprecated code --- pyomo/contrib/doe/tests/test_fim_doe.py | 472 ------------------------ 1 file changed, 472 deletions(-) delete mode 100644 pyomo/contrib/doe/tests/test_fim_doe.py diff --git a/pyomo/contrib/doe/tests/test_fim_doe.py b/pyomo/contrib/doe/tests/test_fim_doe.py deleted file mode 100644 index 9cae2fe6278..00000000000 --- a/pyomo/contrib/doe/tests/test_fim_doe.py +++ /dev/null @@ -1,472 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# -# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation -# Initiative (CCSI), and is copyright (c) 2022 by the software owners: -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, -# University of Toledo, West Virginia University, et al. All rights reserved. -# -# NOTICE. This Software was developed under funding from the -# U.S. Department of Energy and the U.S. Government consequently retains -# certain rights. As such, the U.S. Government has been granted for itself -# and others acting on its behalf a paid-up, nonexclusive, irrevocable, -# worldwide license in the Software to reproduce, distribute copies to the -# public, prepare derivative works, and perform publicly and display -# publicly, and to permit other to do so. -# ___________________________________________________________________________ - -from pyomo.common.dependencies import numpy as np, numpy_available -import pyomo.common.unittest as unittest -from pyomo.contrib.doe import ( - MeasurementVariables, - DesignVariables, - ScenarioGenerator, - DesignOfExperiments, - VariablesWithIndices, -) -from pyomo.contrib.doe.examples.reactor_kinetics import create_model, disc_for_measure -from pyomo.environ import SolverFactory - -ipopt_available = SolverFactory("ipopt").available() - - -class TestMeasurementError(unittest.TestCase): - - def test_with_time_plus_one_extra_index(self): - """This tests confirms the typical usage with a time index plus one extra index. - - This test should execute without throwing any errors. - - """ - - MeasurementVariables().add_variables( - "C", indices={0: ["A", "B", "C"], 1: [0, 0.5, 1.0]}, time_index_position=1 - ) - - def test_with_time_plus_two_extra_indices(self): - """This tests confirms the typical usage with a time index plus two extra indices. - - This test should execute without throwing any errors. - - """ - - MeasurementVariables().add_variables( - "C", - indices={ - 0: ["A", "B", "C"], # species - 1: [0, 0.5, 1.0], # time - 2: [1, 2, 3], - }, # position - time_index_position=1, - ) - - def test_time_index_position_out_of_bounds(self): - """This test confirms that an error is thrown when the time index position is out of bounds.""" - - # if time index is not in indices, an value error is thrown. - with self.assertRaises(ValueError): - MeasurementVariables().add_variables( - "C", - indices={0: ["CA", "CB", "CC"], 1: [0, 0.5, 1.0]}, # species # time - time_index_position=2, # this is out of bounds - ) - - def test_single_measurement_variable(self): - """This test confirms we can specify a single measurement variable without - specifying the indices. - - The test should execute with no errors. - """ - measurements = MeasurementVariables() - measurements.add_variables("HelloWorld", indices=None, time_index_position=None) - - def test_without_time_index(self): - """This test confirms we can add a measurement variable without specifying the time index. - - The test should execute with no errors. - - """ - - MeasurementVariables().add_variables( - "C", - indices={0: ["CA", "CB", "CC"]}, # species as only index - time_index_position=None, # no time index - ) - - def test_only_time_index(self): - """This test confirms we can add a measurement variable without specifying the variable name. - - The test should execute with no errors. - - """ - - MeasurementVariables().add_variables( - "HelloWorld", # name of the variable - indices={0: [0, 0.5, 1.0]}, - time_index_position=0, - ) - - def test_with_no_measurement_name(self): - """This test confirms that an error is thrown when None is used as the measurement name.""" - - with self.assertRaises(TypeError): - MeasurementVariables().add_variables( - None, indices={0: [0, 0.5, 1.0]}, time_index_position=0 - ) - - def test_with_non_string_measurement_name(self): - """This test confirms that an error is thrown when a non-string is used as the measurement name.""" - - with self.assertRaises(TypeError): - MeasurementVariables().add_variables( - 1, indices={0: [0, 0.5, 1.0]}, time_index_position=0 - ) - - def test_non_integer_index_keys(self): - """This test confirms that strings can be used as keys for specifying the indices. - - Warning: it is possible this usage breaks something else in Pyomo.DoE. - There may be an implicit assumption that the order of the keys must match the order - of the indices in the Pyomo model. - - """ - - MeasurementVariables().add_variables( - "C", - indices={"species": ["CA", "CB", "CC"], "time": [0, 0.5, 1.0]}, - time_index_position="time", - ) - - def test_no_measurements(self): - """This test confirms that an error is thrown when the user forgets to add any measurements. - - It's okay to have no decision variables. With no measurement variables, the FIM is the zero matrix. - This (no measurements) is a common user mistake. - """ - - with self.assertRaises(ValueError): - decisions = DesignVariables() - measurements = MeasurementVariables() - DesignOfExperiments( - {}, decisions, measurements, create_model, disc_for_measure - ) - - -class TestDesignError(unittest.TestCase): - def test(self): - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - # design object - exp_design = DesignVariables() - - # add T as design variable - var_T = "T" - indices_T = {0: t_control} - exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] - - upper_bound = [ - 700, - 700, - 700, - 700, - 700, - 700, - 700, - 700, - 700, - 800, - ] # wrong upper bound since it has more elements than the length of variable names - lower_bound = [300, 300, 300, 300, 300, 300, 300, 300, 300] - - with self.assertRaises(ValueError): - exp_design.add_variables( - var_T, - indices=indices_T, - time_index_position=0, - values=exp1_T, - lower_bounds=lower_bound, - upper_bounds=upper_bound, - ) - - -@unittest.skipIf(not numpy_available, "Numpy is not available") -@unittest.skipIf(not ipopt_available, "ipopt is not available") -class TestPriorFIMError(unittest.TestCase): - def test(self): - # Control time set [h] - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - # measurement object - variable_name = "C" - indices = {0: ["CA", "CB", "CC"], 1: t_control} - - measurements = MeasurementVariables() - measurements.add_variables( - variable_name, indices=indices, time_index_position=1 - ) - - # design object - exp_design = DesignVariables() - - # add CAO as design variable - var_C = "CA0" - indices_C = {0: [0]} - exp1_C = [5] - exp_design.add_variables( - var_C, - indices=indices_C, - time_index_position=0, - values=exp1_C, - lower_bounds=1, - upper_bounds=5, - ) - - # add T as design variable - var_T = "T" - indices_T = {0: t_control} - exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] - - exp_design.add_variables( - var_T, - indices=indices_T, - time_index_position=0, - values=exp1_T, - lower_bounds=300, - upper_bounds=700, - ) - - parameter_dict = {"A1": 1, "A2": 1, "E1": 1} - - # empty prior - prior_right = [[0] * 3 for i in range(3)] - prior_pass = [[0] * 5 for i in range(10)] - - # check if the error can be thrown when given a wrong shape of FIM prior - with self.assertRaises(ValueError): - doe_object = DesignOfExperiments( - parameter_dict, - exp_design, - measurements, - create_model, - prior_FIM=prior_pass, - discretize_model=disc_for_measure, - ) - - -class TestMeasurement(unittest.TestCase): - """Test the MeasurementVariables class, specify, add_element, update_variance, check_subset functions.""" - - def test_setup(self): - ### add_element function - - # control time for C [h] - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - # control time for T [h] - t_control2 = [0.2, 0.4, 0.6, 0.8] - - # measurement object - measurements = MeasurementVariables() - - # add variable C - variable_name = "C" - indices = {0: ["CA", "CB", "CC"], 1: t_control} - measurements.add_variables( - variable_name, indices=indices, time_index_position=1 - ) - - # add variable T - variable_name2 = "T" - indices2 = {0: [1, 3, 5], 1: t_control2} - measurements.add_variables( - variable_name2, indices=indices2, time_index_position=1, variance=10 - ) - - # check variable names - self.assertEqual(measurements.variable_names[0], "C[CA,0]") - self.assertEqual(measurements.variable_names[1], "C[CA,0.125]") - self.assertEqual(measurements.variable_names[-1], "T[5,0.8]") - self.assertEqual(measurements.variable_names[-2], "T[5,0.6]") - self.assertEqual(measurements.variance["T[5,0.4]"], 10) - self.assertEqual(measurements.variance["T[5,0.6]"], 10) - self.assertEqual(measurements.variance["T[5,0.4]"], 10) - self.assertEqual(measurements.variance["T[5,0.6]"], 10) - - ### specify function - var_names = [ - "C[CA,0]", - "C[CA,0.125]", - "C[CA,0.875]", - "C[CA,1]", - "C[CB,0]", - "C[CB,0.125]", - "C[CB,0.25]", - "C[CB,0.375]", - "C[CC,0]", - "C[CC,0.125]", - "C[CC,0.25]", - "C[CC,0.375]", - ] - - measurements2 = MeasurementVariables() - measurements2.set_variable_name_list(var_names) - - self.assertEqual(measurements2.variable_names[1], "C[CA,0.125]") - self.assertEqual(measurements2.variable_names[-1], "C[CC,0.375]") - - ### check_subset function - self.assertTrue(measurements.check_subset(measurements2)) - - -class TestDesignVariable(unittest.TestCase): - """Test the DesignVariable class, specify, add_element, add_bounds, update_values.""" - - def test_setup(self): - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - - # design object - exp_design = DesignVariables() - - # add CAO as design variable - var_C = "CA0" - indices_C = {0: [0]} - exp1_C = [5] - exp_design.add_variables( - var_C, - indices=indices_C, - time_index_position=0, - values=exp1_C, - lower_bounds=1, - upper_bounds=5, - ) - - # add T as design variable - var_T = "T" - indices_T = {0: t_control} - exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] - - exp_design.add_variables( - var_T, - indices=indices_T, - time_index_position=0, - values=exp1_T, - lower_bounds=300, - upper_bounds=700, - ) - - self.assertEqual( - exp_design.variable_names, - [ - "CA0[0]", - "T[0]", - "T[0.125]", - "T[0.25]", - "T[0.375]", - "T[0.5]", - "T[0.625]", - "T[0.75]", - "T[0.875]", - "T[1]", - ], - ) - self.assertEqual(exp_design.variable_names_value["CA0[0]"], 5) - self.assertEqual(exp_design.variable_names_value["T[0]"], 470) - self.assertEqual(exp_design.upper_bounds["CA0[0]"], 5) - self.assertEqual(exp_design.upper_bounds["T[0]"], 700) - self.assertEqual(exp_design.lower_bounds["CA0[0]"], 1) - self.assertEqual(exp_design.lower_bounds["T[0]"], 300) - - design_names = exp_design.variable_names - exp1 = [4, 600, 300, 300, 300, 300, 300, 300, 300, 300] - exp1_design_dict = dict(zip(design_names, exp1)) - exp_design.update_values(exp1_design_dict) - self.assertEqual(exp_design.variable_names_value["CA0[0]"], 4) - self.assertEqual(exp_design.variable_names_value["T[0]"], 600) - - -class TestParameter(unittest.TestCase): - """Test the ScenarioGenerator class, generate_scenario function.""" - - def test_setup(self): - # set up parameter class - param_dict = {"A1": 84.79, "A2": 371.72, "E1": 7.78, "E2": 15.05} - - scenario_gene = ScenarioGenerator(param_dict, formula="central", step=0.1) - parameter_set = scenario_gene.ScenarioData - - self.assertAlmostEqual(parameter_set.eps_abs["A1"], 16.9582, places=1) - self.assertAlmostEqual(parameter_set.eps_abs["E1"], 1.5554, places=1) - self.assertEqual(parameter_set.scena_num["A2"], [2, 3]) - self.assertEqual(parameter_set.scena_num["E1"], [4, 5]) - self.assertAlmostEqual(parameter_set.scenario[0]["A1"], 93.2699, places=1) - self.assertAlmostEqual(parameter_set.scenario[2]["A2"], 408.8895, places=1) - self.assertAlmostEqual(parameter_set.scenario[-1]["E2"], 13.54, places=1) - self.assertAlmostEqual(parameter_set.scenario[-2]["E2"], 16.55, places=1) - - -class TestVariablesWithIndices(unittest.TestCase): - """Test the DesignVariable class, specify, add_element, add_bounds, update_values.""" - - def test_setup(self): - special = VariablesWithIndices() - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - ### add_element function - # add CAO as design variable - var_C = "CA0" - indices_C = {0: [0]} - exp1_C = [5] - special.add_variables( - var_C, - indices=indices_C, - time_index_position=0, - values=exp1_C, - lower_bounds=1, - upper_bounds=5, - ) - - # add T as design variable - var_T = "T" - indices_T = {0: t_control} - exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] - - special.add_variables( - var_T, - indices=indices_T, - time_index_position=0, - values=exp1_T, - lower_bounds=300, - upper_bounds=700, - ) - - self.assertEqual( - special.variable_names, - [ - "CA0[0]", - "T[0]", - "T[0.125]", - "T[0.25]", - "T[0.375]", - "T[0.5]", - "T[0.625]", - "T[0.75]", - "T[0.875]", - "T[1]", - ], - ) - self.assertEqual(special.variable_names_value["CA0[0]"], 5) - self.assertEqual(special.variable_names_value["T[0]"], 470) - self.assertEqual(special.upper_bounds["CA0[0]"], 5) - self.assertEqual(special.upper_bounds["T[0]"], 700) - self.assertEqual(special.lower_bounds["CA0[0]"], 1) - self.assertEqual(special.lower_bounds["T[0]"], 300) - - -if __name__ == "__main__": - unittest.main() From c5a43018304bb20d8d615d5e194b917abf6ac00d Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:21:46 -0400 Subject: [PATCH 2223/3044] Delete pyomo/contrib/doe/tests/test_reactor_example.py Removing tests from deprecated code --- .../contrib/doe/tests/test_reactor_example.py | 234 ------------------ 1 file changed, 234 deletions(-) delete mode 100644 pyomo/contrib/doe/tests/test_reactor_example.py diff --git a/pyomo/contrib/doe/tests/test_reactor_example.py b/pyomo/contrib/doe/tests/test_reactor_example.py deleted file mode 100644 index f88ae48db1a..00000000000 --- a/pyomo/contrib/doe/tests/test_reactor_example.py +++ /dev/null @@ -1,234 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# -# Pyomo.DoE was produced under the Department of Energy Carbon Capture Simulation -# Initiative (CCSI), and is copyright (c) 2022 by the software owners: -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, -# University of Toledo, West Virginia University, et al. All rights reserved. -# -# NOTICE. This Software was developed under funding from the -# U.S. Department of Energy and the U.S. Government consequently retains -# certain rights. As such, the U.S. Government has been granted for itself -# and others acting on its behalf a paid-up, nonexclusive, irrevocable, -# worldwide license in the Software to reproduce, distribute copies to the -# public, prepare derivative works, and perform publicly and display -# publicly, and to permit other to do so. -# ___________________________________________________________________________ - - -# import libraries -from pyomo.common.dependencies import numpy as np, numpy_available, pandas_available -import pyomo.common.unittest as unittest -from pyomo.contrib.doe import DesignOfExperiments, MeasurementVariables, DesignVariables -from pyomo.environ import value, ConcreteModel -from pyomo.contrib.doe.examples.reactor_kinetics import create_model, disc_for_measure -from pyomo.opt import SolverFactory - -ipopt_available = SolverFactory("ipopt").available() -k_aug_available = SolverFactory("k_aug").available(exception_flag=False) - - -class Test_Reaction_Kinetics_Example(unittest.TestCase): - def test_reaction_kinetics_create_model(self): - """Test the three options in the kinetics example.""" - # parmest option - mod = create_model(model_option="parmest") - - # global and block option - mod = ConcreteModel() - create_model(mod, model_option="stage1") - create_model(mod, model_option="stage2") - # both options need a given model, or raise errors - with self.assertRaises(ValueError): - create_model(model_option="stage1") - - with self.assertRaises(ValueError): - create_model(model_option="stage2") - - with self.assertRaises(ValueError): - create_model(model_option="NotDefined") - - @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") - @unittest.skipIf(not pandas_available, "Pandas is not available") - def test_kinetics_example_sequential_finite_then_optimize(self): - """Test the kinetics example with sequential_finite mode and then optimization""" - doe_object = self.specify_reaction_kinetics() - - # Test FIM calculation at nominal values - sensi_opt = "sequential_finite" - result = doe_object.compute_FIM( - mode=sensi_opt, scale_nominal_param_value=True, formula="central" - ) - result.result_analysis() - self.assertAlmostEqual(np.log10(result.trace), 2.7885, places=2) - self.assertAlmostEqual(np.log10(result.det), 2.8218, places=2) - self.assertAlmostEqual(np.log10(result.min_eig), -1.0123, places=2) - - ### check subset feature - sub_name = "C" - sub_indices = {0: ["CB", "CC"], 1: [0.125, 0.25, 0.5, 0.75, 0.875]} - - measure_subset = MeasurementVariables() - measure_subset.add_variables( - sub_name, indices=sub_indices, time_index_position=1 - ) - sub_result = result.subset(measure_subset) - sub_result.result_analysis() - - self.assertAlmostEqual(np.log10(sub_result.trace), 2.5535, places=2) - self.assertAlmostEqual(np.log10(sub_result.det), 1.3464, places=2) - self.assertAlmostEqual(np.log10(sub_result.min_eig), -1.5386, places=2) - - ### Test stochastic_program mode - # Prior information (scaled FIM with T=500 and T=300 experiments) - prior = np.asarray( - [ - [28.67892806, 5.41249739, -81.73674601, -24.02377324], - [5.41249739, 26.40935036, -12.41816477, -139.23992532], - [-81.73674601, -12.41816477, 240.46276004, 58.76422806], - [-24.02377324, -139.23992532, 58.76422806, 767.25584508], - ] - ) - doe_object2 = self.specify_reaction_kinetics(prior=prior) - - square_result, optimize_result = doe_object2.stochastic_program( - if_optimize=True, - if_Cholesky=True, - scale_nominal_param_value=True, - objective_option="det", - L_initial=np.linalg.cholesky(prior), - jac_initial=result.jaco_information.copy(), - tee_opt=True, - ) - - optimize_result.result_analysis() - ## 2024-May-26: changing this to test the objective instead of the optimal solution - ## It's possible the objective is flat and the optimal solution is not unique - # self.assertAlmostEqual(value(optimize_result.model.CA0[0]), 5.0, places=2) - # self.assertAlmostEqual(value(optimize_result.model.T[0.5]), 300, places=2) - self.assertAlmostEqual(np.log10(optimize_result.det), 5.744, places=2) - - square_result, optimize_result = doe_object2.stochastic_program( - if_optimize=True, - scale_nominal_param_value=True, - objective_option="trace", - jac_initial=result.jaco_information.copy(), - tee_opt=True, - ) - - optimize_result.result_analysis() - ## 2024-May-26: changing this to test the objective instead of the optimal solution - ## It's possible the objective is flat and the optimal solution is not unique - # self.assertAlmostEqual(value(optimize_result.model.CA0[0]), 5.0, places=2) - # self.assertAlmostEqual(value(optimize_result.model.T[0.5]), 300, places=2) - self.assertAlmostEqual(np.log10(optimize_result.trace), 3.340, places=2) - - @unittest.skipIf(not k_aug_available, "The 'k_aug' solver is not available") - @unittest.skipIf(not ipopt_available, "The 'ipopt' solver is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") - @unittest.skipIf(not pandas_available, "Pandas is not available") - def test_kinetics_example_direct_k_aug(self): - doe_object = self.specify_reaction_kinetics() - - # Test FIM calculation at nominal values - sensi_opt = "direct_kaug" - result = doe_object.compute_FIM( - mode=sensi_opt, scale_nominal_param_value=True, formula="central" - ) - result.result_analysis() - self.assertAlmostEqual(np.log10(result.trace), 2.789, places=2) - self.assertAlmostEqual(np.log10(result.det), 2.8247, places=2) - self.assertAlmostEqual(np.log10(result.min_eig), -1.0112, places=2) - - ### check subset feature - sub_name = "C" - sub_indices = {0: ["CB", "CC"], 1: [0.125, 0.25, 0.5, 0.75, 0.875]} - - measure_subset = MeasurementVariables() - measure_subset.add_variables( - sub_name, indices=sub_indices, time_index_position=1 - ) - sub_result = result.subset(measure_subset) - sub_result.result_analysis() - - self.assertAlmostEqual(np.log10(sub_result.trace), 2.5535, places=2) - self.assertAlmostEqual(np.log10(sub_result.det), 1.3464, places=2) - self.assertAlmostEqual(np.log10(sub_result.min_eig), -1.5386, places=2) - - def specify_reaction_kinetics(self, prior=None): - ### Define inputs - # Control time set [h] - t_control = [0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1] - # Define parameter nominal value - parameter_dict = {"A1": 84.79, "A2": 371.72, "E1": 7.78, "E2": 15.05} - - # measurement object - variable_name = "C" - indices = {0: ["CA", "CB", "CC"], 1: t_control} - - measurements = MeasurementVariables() - measurements.add_variables( - variable_name, indices=indices, time_index_position=1 - ) - - # design object - exp_design = DesignVariables() - - # add CAO as design variable - var_C = "CA0" - indices_C = {0: [0]} - exp1_C = [5] - exp_design.add_variables( - var_C, - indices=indices_C, - time_index_position=0, - values=exp1_C, - lower_bounds=1, - upper_bounds=5, - ) - - # add T as design variable - var_T = "T" - indices_T = {0: t_control} - exp1_T = [470, 300, 300, 300, 300, 300, 300, 300, 300] - - exp_design.add_variables( - var_T, - indices=indices_T, - time_index_position=0, - values=exp1_T, - lower_bounds=300, - upper_bounds=700, - ) - - design_names = exp_design.variable_names - exp1 = [5, 570, 300, 300, 300, 300, 300, 300, 300, 300] - exp1_design_dict = dict(zip(design_names, exp1)) - - exp_design.update_values(exp1_design_dict) - - doe_object = DesignOfExperiments( - parameter_dict, - exp_design, - measurements, - create_model, - discretize_model=disc_for_measure, - prior_FIM=prior, - ) - - return doe_object - - -if __name__ == "__main__": - unittest.main() From 8ec650080c0350bb24bdf62cf266212f60a6cde0 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:22:25 -0400 Subject: [PATCH 2224/3044] Delete pyomo/contrib/doe/tests/experiment_class_example.py Removing deprecated example file in favor of reusing example experiment file --- .../doe/tests/experiment_class_example.py | 231 ------------------ 1 file changed, 231 deletions(-) delete mode 100644 pyomo/contrib/doe/tests/experiment_class_example.py diff --git a/pyomo/contrib/doe/tests/experiment_class_example.py b/pyomo/contrib/doe/tests/experiment_class_example.py deleted file mode 100644 index e31b0da8374..00000000000 --- a/pyomo/contrib/doe/tests/experiment_class_example.py +++ /dev/null @@ -1,231 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ -# === Required imports === -import pyomo.environ as pyo -from pyomo.dae import ContinuousSet, DerivativeVar, Simulator - -from pyomo.contrib.parmest.experiment import Experiment - -import itertools - -# ======================== - - -def expand_model_components(m, base_components, index_sets): - """ - Takes model components and index sets and returns the - model component labels. - - Arguments - --------- - m: Pyomo model - base_components: list of variables from model 'm' - index_sets: list, same length as base_components, where each - element is a list of index sets, or None - """ - for val, indexes in itertools.zip_longest(base_components, index_sets): - # If the variable has no index, - # add just the model component - if not val.is_indexed(): - yield val - # If the component is indexed but no - # index supplied, add all indices - elif indexes is None: - yield from val.values() - else: - for j in itertools.product(*indexes): - yield val[j] - - -class ReactorExperiment(Experiment): - def __init__(self, data, nfe, ncp): - self.data = data - self.nfe = nfe - self.ncp = ncp - self.model = None - - def get_labeled_model(self): - if self.model is None: - self.create_model() - self.finalize_model() - self.label_experiment() - return self.model - - def create_model(self): - """ - This is an example user model provided to DoE library. - It is a dynamic problem solved by Pyomo.DAE. - - Return - ------ - m: a Pyomo.DAE model - """ - - m = self.model = pyo.ConcreteModel() - - # Model parameters - m.R = pyo.Param(mutable=False, initialize=8.314) - - # Define model variables - ######################## - # time - m.t = ContinuousSet(bounds=[0, 1]) - - # Concentrations - m.CA = pyo.Var(m.t, within=pyo.NonNegativeReals) - m.CB = pyo.Var(m.t, within=pyo.NonNegativeReals) - m.CC = pyo.Var(m.t, within=pyo.NonNegativeReals) - - # Temperature - m.T = pyo.Var(m.t, within=pyo.NonNegativeReals) - - # Arrhenius rate law equations - m.A1 = pyo.Var(within=pyo.NonNegativeReals) - m.E1 = pyo.Var(within=pyo.NonNegativeReals) - m.A2 = pyo.Var(within=pyo.NonNegativeReals) - m.E2 = pyo.Var(within=pyo.NonNegativeReals) - - # Differential variables (Conc.) - m.dCAdt = DerivativeVar(m.CA, wrt=m.t) - m.dCBdt = DerivativeVar(m.CB, wrt=m.t) - - ######################## - # End variable def. - - # Equation def'n - ######################## - - # Expression for rate constants - @m.Expression(m.t) - def k1(m, t): - return m.A1 * pyo.exp(-m.E1 * 1000 / (m.R * m.T[t])) - - @m.Expression(m.t) - def k2(m, t): - return m.A2 * pyo.exp(-m.E2 * 1000 / (m.R * m.T[t])) - - # Concentration odes - @m.Constraint(m.t) - def CA_rxn_ode(m, t): - return m.dCAdt[t] == -m.k1[t] * m.CA[t] - - @m.Constraint(m.t) - def CB_rxn_ode(m, t): - return m.dCBdt[t] == m.k1[t] * m.CA[t] - m.k2[t] * m.CB[t] - - # algebraic balance for concentration of C - # Valid because the reaction system (A --> B --> C) is equimolar - @m.Constraint(m.t) - def CC_balance(m, t): - return m.CA[0] == m.CA[t] + m.CB[t] + m.CC[t] - - ######################## - # End equation def'n - - def finalize_model(self): - """ - Example finalize model function. There are two main tasks - here: - 1. Extracting useful information for the model to align - with the experiment. (Here: CA0, t_final, t_control) - 2. Discretizing the model subject to this information. - - Arguments - --------- - m: Pyomo model - data: object containing vital experimental information - nfe: number of finite elements - ncp: number of collocation points for the finite elements - """ - m = self.model - - # Unpacking data before simulation - control_points = self.data["control_points"] - - m.CA[0].value = self.data["CA0"] - m.CB[0].fix(self.data["CB0"]) - m.t.update(self.data["t_range"]) - m.t.update(control_points) - m.A1.fix(self.data["A1"]) - m.A2.fix(self.data["A2"]) - m.E1.fix(self.data["E1"]) - m.E2.fix(self.data["E2"]) - - m.CA[0].setlb(self.data["CA_bounds"][0]) - m.CA[0].setub(self.data["CA_bounds"][1]) - - m.t_control = control_points - - # Discretizing the model - discr = pyo.TransformationFactory("dae.collocation") - discr.apply_to(m, nfe=self.nfe, ncp=self.ncp, wrt=m.t) - - # Initializing Temperature in the model - cv = None - for t in m.t: - if t in control_points: - cv = control_points[t] - m.T[t].setlb(self.data["T_bounds"][0]) - m.T[t].setub(self.data["T_bounds"][1]) - m.T[t] = cv - - @m.Constraint(m.t - control_points) - def T_control(m, t): - """ - Piecewise constant Temperature between control points - """ - neighbour_t = max(tc for tc in control_points if tc < t) - return m.T[t] == m.T[neighbour_t] - - # sim.initialize_model() - - def label_experiment_impl(self, index_sets_meas): - """ - Example for annotating (labeling) the model with a - full experiment. - - Arguments - --------- - - """ - m = self.model - - # Grab measurement labels - base_comp_meas = [m.CA, m.CB, m.CC] - m.experiment_outputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_outputs.update( - (k, None) - for k in expand_model_components(m, base_comp_meas, index_sets_meas) - ) - - # Adding no error for measurements currently - m.measurement_error = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.measurement_error.update( - (k, 1e-2) - for k in expand_model_components(m, base_comp_meas, index_sets_meas) - ) - - # Grab design variables - base_comp_des = [m.CA, m.T] - index_sets_des = [[[m.t.first()]], [m.t_control]] - m.experiment_inputs = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.experiment_inputs.update( - (k, None) for k in expand_model_components(m, base_comp_des, index_sets_des) - ) - - m.unknown_parameters = pyo.Suffix(direction=pyo.Suffix.LOCAL) - m.unknown_parameters.update((k, pyo.value(k)) for k in [m.A1, m.A2, m.E1, m.E2]) - - -class FullReactorExperiment(ReactorExperiment): - def label_experiment(self): - m = self.model - return self.label_experiment_impl([[m.t_control], [m.t_control], [m.t_control]]) From f6c3fcb145ae566eb9b3c0d3e47c18b39a7c6998 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:22:51 -0400 Subject: [PATCH 2225/3044] Delete pyomo/contrib/doe/tests/result.json Removing result.json to have no duplicated file --- pyomo/contrib/doe/tests/result.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 pyomo/contrib/doe/tests/result.json diff --git a/pyomo/contrib/doe/tests/result.json b/pyomo/contrib/doe/tests/result.json deleted file mode 100644 index 7e1b1a79a1b..00000000000 --- a/pyomo/contrib/doe/tests/result.json +++ /dev/null @@ -1 +0,0 @@ -{"CA0": 5.0, "CA_bounds": [1.0, 5.0], "CB0": 0.0, "CC0": 0.0, "t_range": [0, 1], "control_points": {"0": 500, "0.125": 300, "0.25": 300, "0.375": 300, "0.5": 300, "0.625": 300, "0.75": 300, "0.875": 300, "1": 300}, "T_bounds": [300, 700], "A1": 84.79, "A2": 371.72, "E1": 7.78, "E2": 15.05} \ No newline at end of file From 31f93e5864619c900f4d7bbb08de58fbf3d9d22e Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:23:17 -0400 Subject: [PATCH 2226/3044] Delete pyomo/contrib/doe/experiment.py Removing experiment in favor of not duplicating the Experiment file in parmest. --- pyomo/contrib/doe/experiment.py | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 pyomo/contrib/doe/experiment.py diff --git a/pyomo/contrib/doe/experiment.py b/pyomo/contrib/doe/experiment.py deleted file mode 100644 index 17a51cf667a..00000000000 --- a/pyomo/contrib/doe/experiment.py +++ /dev/null @@ -1,18 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ -class Experiment(object): - def __init__(self): - self.model = None - - def get_labeled_model(self): - raise NotImplementedError( - "Derived experiment class failed to implement get_labeled_model" - ) From a47ecab4e9b2895245be8c5725d744f6f7b09409 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 14:29:56 -0600 Subject: [PATCH 2227/3044] Removing an unneeded comment --- pyomo/contrib/alternative_solutions/lp_enum.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index 0d74ea490c3..ca8193d822f 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -294,10 +294,6 @@ def enumerate_linear_solutions( for var in continuous_var: if continuous_var[var].value > zero_threshold: num_non_zero += 1 - # WEH - I don't think you need to add the binary variable. It - # should be automatically added when used. - # if var not in binary_var: - # binary_var[var] # Eqn (3): if binary choice variable is not selected, then # continuous variable is zero. From 46ef920b3694559c702d70ffd01cdb3ca9eeffee Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 12 Aug 2024 14:48:42 -0600 Subject: [PATCH 2228/3044] Resolve inconsistency in ParameterizedInitializer call API --- pyomo/core/base/initializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/initializer.py b/pyomo/core/base/initializer.py index 6e73cc176d1..3fba3d2b143 100644 --- a/pyomo/core/base/initializer.py +++ b/pyomo/core/base/initializer.py @@ -561,7 +561,7 @@ def indices(self): return self._base_initializer.indices() def __call__(self, parent, idx, *args): - return self._base_initializer(parent, idx)(*args) + return self._base_initializer(parent, idx)(parent, *args) _bound_sequence_types = collections.defaultdict(None.__class__) From 757babaf33d34b30c6feee8320aab56fb9102350 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 12 Aug 2024 14:51:07 -0600 Subject: [PATCH 2229/3044] Resolve bug when wrapping new function types with additional_args --- pyomo/core/base/initializer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/initializer.py b/pyomo/core/base/initializer.py index 3fba3d2b143..072b064d425 100644 --- a/pyomo/core/base/initializer.py +++ b/pyomo/core/base/initializer.py @@ -102,14 +102,17 @@ def Initializer( treat_sequences_as_mappings=treat_sequences_as_mappings, arg_not_specified=arg_not_specified, ) - if arg.__class__ in function_types: + if type(base_initializer) in ( + ScalarCallInitializer, + IndexedCallInitializer, + ): # This is an edge case: if we are providing additional # args, but this is the first time we are seeing a # callable type, we will (potentially) incorrectly # categorize this as an IndexedCallInitializer. Re-try # now that we know this is a function_type. return Initializer( - arg=arg, + arg=base_initializer._fcn, allow_generators=allow_generators, treat_sequences_as_mappings=treat_sequences_as_mappings, arg_not_specified=arg_not_specified, From 6a581b1f3f749e24bd7f6e42d71004c32c2e19b2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 12 Aug 2024 14:52:35 -0600 Subject: [PATCH 2230/3044] Improve Initializer / Set unit test coverage --- pyomo/core/tests/unit/test_initializer.py | 137 ++++++++++++++++++++++ pyomo/core/tests/unit/test_set.py | 35 ++++++ 2 files changed, 172 insertions(+) diff --git a/pyomo/core/tests/unit/test_initializer.py b/pyomo/core/tests/unit/test_initializer.py index c0f9ddc9565..2b1d44b422f 100644 --- a/pyomo/core/tests/unit/test_initializer.py +++ b/pyomo/core/tests/unit/test_initializer.py @@ -27,6 +27,7 @@ from pyomo.core.base.util import flatten_tuple from pyomo.core.base.initializer import ( Initializer, + BoundInitializer, ConstantInitializer, ItemInitializer, ScalarCallInitializer, @@ -35,6 +36,10 @@ CountedCallGenerator, DataFrameInitializer, DefaultInitializer, + ParameterizedInitializer, + ParameterizedIndexedCallInitializer, + ParameterizedScalarCallInitializer, + function_types, ) from pyomo.environ import ConcreteModel, Var @@ -550,6 +555,54 @@ def _indexed(m, i): self.assertFalse(a.verified) self.assertEqual(a(None, 5), 15) + def test_function(self): + def _scalar(m): + return 10 + + a = Initializer(_scalar) + self.assertIs(type(a), ScalarCallInitializer) + self.assertTrue(a.constant()) + self.assertFalse(a.verified) + self.assertEqual(a(None, None), 10) + + def _indexed(m, i): + return 10 + i + + a = Initializer(_indexed) + self.assertIs(type(a), IndexedCallInitializer) + self.assertFalse(a.constant()) + self.assertFalse(a.verified) + self.assertEqual(a(None, 5), 15) + + try: + original_fcn_types = set(function_types) + function_types.clear() + self.assertEqual(len(function_types), 0) + + a = Initializer(_scalar) + self.assertIs(type(a), ScalarCallInitializer) + self.assertTrue(a.constant()) + self.assertFalse(a.verified) + self.assertEqual(a(None, None), 10) + self.assertEqual(len(function_types), 1) + finally: + function_types.clear() + function_types.update(original_fcn_types) + + try: + original_fcn_types = set(function_types) + function_types.clear() + self.assertEqual(len(function_types), 0) + + a = Initializer(_indexed) + self.assertIs(type(a), IndexedCallInitializer) + self.assertFalse(a.constant()) + self.assertFalse(a.verified) + self.assertEqual(a(None, 5), 15) + finally: + function_types.clear() + function_types.update(original_fcn_types) + def test_no_argspec(self): a = Initializer(getattr) self.assertIs(type(a), IndexedCallInitializer) @@ -805,3 +858,87 @@ def test_config_integration(self): self.assertEqual(a(None, 'opt_1'), 1) self.assertEqual(a(None, 'opt_3'), 3) self.assertEqual(a(None, 'opt_5'), 5) + + def _bound_function1(self, m, i): + return m, i + + def _bound_function2(self, m, i, j): + return m, i, j + + def test_additional_args(self): + def a_init(m): + yield 0 + yield 3 + + with self.assertRaisesRegex( + ValueError, + "Generator functions are not allowed when passing additional args", + ): + a = Initializer(a_init, additional_args=1) + + a = Initializer(self._bound_function1, additional_args=1) + self.assertIs(type(a), ParameterizedScalarCallInitializer) + self.assertEqual(a('m', None, 5), ('m', 5)) + + a = Initializer(self._bound_function2, additional_args=1) + self.assertIs(type(a), ParameterizedIndexedCallInitializer) + self.assertEqual(a('m', 1, 5), ('m', 5, 1)) + + class Functor(object): + def __init__(self, i): + self.i = i + + def __call__(self, m, i): + return m, i * self.i + + a = Initializer(Functor(10), additional_args=1) + self.assertIs(type(a), ParameterizedScalarCallInitializer) + self.assertEqual(a('m', None, 5), ('m', 50)) + + a_init = {1: lambda m, i: ('m', i), 2: lambda m, i: ('m', 2 * i)} + a = Initializer(a_init, additional_args=1) + self.assertIs(type(a), ParameterizedInitializer) + self.assertFalse(a.constant()) + self.assertTrue(a.contains_indices()) + self.assertEqual(list(a.indices()), [1, 2]) + self.assertEqual(a('m', 1, 5), ('m', 5)) + self.assertEqual(a('m', 2, 5), ('m', 10)) + + def test_bound_initializer(self): + m = ConcreteModel() + m.x = Var([0, 1, 2]) + m.y = Var() + + b = BoundInitializer(None, m.x) + self.assertIsNone(b) + + b = BoundInitializer((0, 1), m.x) + self.assertIs(type(b), BoundInitializer) + self.assertTrue(b.constant()) + self.assertFalse(b.verified) + self.assertFalse(b.contains_indices()) + self.assertEqual(b(None, 1), (0, 1)) + + b = BoundInitializer([(0, 1)], m.x) + self.assertIs(type(b), BoundInitializer) + self.assertFalse(b.constant()) + self.assertFalse(b.verified) + self.assertTrue(b.contains_indices()) + self.assertTrue(list(b.indices()), [0]) + self.assertEqual(b(None, 0), (0, 1)) + + init = {1: (2, 3), 4: (5, 6)} + b = BoundInitializer(init, m.x) + self.assertIs(type(b), BoundInitializer) + self.assertFalse(b.constant()) + self.assertFalse(b.verified) + self.assertTrue(b.contains_indices()) + self.assertEqual(list(b.indices()), [1, 4]) + self.assertEqual(b(None, 1), (2, 3)) + self.assertEqual(b(None, 4), (5, 6)) + + b = BoundInitializer((0, 1), m.y) + self.assertEqual(b(None, None), (0, 1)) + + b = BoundInitializer(5, m.y) + self.assertEqual(b(None, None), (5, 5)) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 4e96e519820..a08202d7c50 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4456,6 +4456,41 @@ def _validate(model, v, ind1, ind2): "Exception raised while validating element '(2, 2)' for Set J3[2,2]\n", ) + # Testing the processing of (deprecated) APIs that raise exceptions + def _validate(m, i, j): + assert i == 2 + assert j == 3 + raise RuntimeError("Bogus value") + + m.K1 = Set([1], dimen=2, validate=_validate) + with self.assertRaisesRegex(RuntimeError, "Bogus value"): + m.K1[1].add((2, 3)) + + # Testing the processing of (deprecated) APIs that raise exceptions + def _validate(m, i, j, k): + assert i == 2 + assert j == 3 + assert k == 1 + raise RuntimeError("Bogus value") + + m.K2 = Set([1], dimen=2, validate=_validate) + with self.assertRaisesRegex(RuntimeError, "Bogus value"): + m.K2[1].add((2, 3)) + + # Testing passing the validation rule by dict + _validate = {1: lambda m, i: i == 10, 2: lambda m, i: i == 20} + m.L = Set([1, 2], validate=_validate) + m.L[1].add(10) + with self.assertRaisesRegex( + ValueError, r"The value=20 violates the validation rule of Set L\[1\]" + ): + m.L[1].add(20) + with self.assertRaisesRegex( + ValueError, r"The value=10 violates the validation rule of Set L\[2\]" + ): + m.L[2].add(10) + m.L[2].add(20) + def test_domain(self): m = ConcreteModel() m.I = Set() From 7c597e64b8e7b259c6b446941328d04333024f4f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 16:00:53 -0600 Subject: [PATCH 2231/3044] Setting exception_flag to False for opt.available when aos is trying to throw its own error --- pyomo/contrib/alternative_solutions/lp_enum.py | 3 +-- pyomo/contrib/alternative_solutions/lp_enum_solnpool.py | 2 +- pyomo/contrib/alternative_solutions/obbt.py | 3 +-- pyomo/contrib/alternative_solutions/solnpool.py | 4 ++-- pyomo/contrib/alternative_solutions/tests/test_solution.py | 3 ++- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index ca8193d822f..e3c4e55033c 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -156,8 +156,7 @@ def enumerate_linear_solutions( opt.gurobi_options[parameter] = value else: opt = pe.SolverFactory(solver) - if not opt.available(): - raise ValueError(solver + " is not available") + opt.available() for parameter, value in solver_options.items(): opt.options[parameter] = value if solver == "gurobi": diff --git a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py index 8f06889dfa2..0ce0ab6d26b 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py @@ -148,7 +148,7 @@ def enumerate_linear_solutions_soln_pool( ) opt = pe.SolverFactory("gurobi") - if not opt.available(): + if not opt.available(exception_flag=False): raise ValueError(solver + " is not available") for parameter, value in solver_options.items(): opt.options[parameter] = value diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index aa2810f05dd..09904be5458 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -185,8 +185,7 @@ def obbt_analysis_bounds_and_solutions( use_appsi = True else: opt = pe.SolverFactory(solver) - if not opt.available(): - raise ValueError(solver + " is not available") + opt.available() for parameter, value in solver_options.items(): opt.options[parameter] = value try: diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 9a5a728f963..51acb57c8a5 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -70,8 +70,8 @@ def gurobi_generate_solutions( if not gurobipy_available: raise pyomo.common.errors.ApplicationError("Solver (gurobi) not available") opt = appsi.solvers.Gurobi() - opt.available() - if not opt.available(): # pragma: no cover + + if not opt.available(): raise pyomo.common.errors.ApplicationError("Solver (gurobi) not available") opt.config.stream_solver = tee diff --git a/pyomo/contrib/alternative_solutions/tests/test_solution.py b/pyomo/contrib/alternative_solutions/tests/test_solution.py index 9dbddcd3baf..e8faaf83548 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solution.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solution.py @@ -41,7 +41,8 @@ def get_model(self): return m @unittest.skipUnless( - pe.SolverFactory(mip_solver).available(), "MIP solver not available" + pe.SolverFactory(mip_solver).available(exception_flag=False), + "MIP solver not available" ) def test_solution(self): """ From ac8b7444a89d68d079ac1852f99e84829c1fdd70 Mon Sep 17 00:00:00 2001 From: jlgearh Date: Mon, 12 Aug 2024 17:07:03 -0600 Subject: [PATCH 2232/3044] - Added terminate functionality - Updated checks to make sure only an LP is passed --- pyomo/contrib/alternative_solutions/lp_enum_solnpool.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py index 8f06889dfa2..3b8b356cd2b 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py @@ -59,10 +59,7 @@ def cut_generator_callback(self, cb_m, cb_opt, cb_where): self.solutions.append(sol) if len(self.solutions) >= self.num_solutions: - # TODO: (nicely) terminate the solve - # cb_m.terminate() - return - + cb_opt._solver_model.terminate() num_non_zero = 0 non_zero_basic_expr = 1 for idx in range(len(self.variable_groups)): @@ -142,9 +139,9 @@ def enumerate_linear_solutions_soln_pool( if variables == None: all_variables = aos_utils.get_model_variables(model) for var in all_variables: - if var.is_binary(): + if var.is_integer(): raise pyomo.common.errors.ApplicationError( - f"The enumerate_linear_solutions_soln_pool() function cannot be used with models that contain binary variables" + f"The enumerate_linear_solutions_soln_pool() function cannot be used with models that contain discrete variables" ) opt = pe.SolverFactory("gurobi") From 403d3efa43163859da9c74453d16007c0ff1dac5 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 12 Aug 2024 20:09:22 -0400 Subject: [PATCH 2233/3044] Update version number, changelog --- pyomo/contrib/pyros/CHANGELOG.txt | 13 +++++++++++++ pyomo/contrib/pyros/pyros.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/CHANGELOG.txt b/pyomo/contrib/pyros/CHANGELOG.txt index 52cd7a6db47..afae4b3db71 100644 --- a/pyomo/contrib/pyros/CHANGELOG.txt +++ b/pyomo/contrib/pyros/CHANGELOG.txt @@ -2,6 +2,19 @@ PyROS CHANGELOG =============== +------------------------------------------------------------------------------- +PyROS 1.3.0 12 Aug 2024 +------------------------------------------------------------------------------- +- Fix interactions between PyROS and NL writer-based solvers +- Overhaul the preprocessor +- Update subproblem formulations and modeling objects +- Update `UncertaintySet` class and pre-implemented subclasses to + facilitate new changes to the subproblems +- Update documentation and logging system in light of new preprocessor + and subproblem changes +- Make all tests more rigorous and extensive + + ------------------------------------------------------------------------------- PyROS 1.2.11 17 Mar 2024 ------------------------------------------------------------------------------- diff --git a/pyomo/contrib/pyros/pyros.py b/pyomo/contrib/pyros/pyros.py index 055c7c54556..2ffef5054aa 100644 --- a/pyomo/contrib/pyros/pyros.py +++ b/pyomo/contrib/pyros/pyros.py @@ -33,7 +33,7 @@ ) -__version__ = "1.2.11" +__version__ = "1.3.0" default_pyros_solver_logger = setup_pyros_logger() From 4e771ec0e0a9e78afa662bd32f2a7a0fd45f4ce3 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Mon, 12 Aug 2024 20:38:02 -0400 Subject: [PATCH 2234/3044] Remove idaes import from documentation --- doc/OnlineDocs/contributed_packages/doe/doe.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index e3f729e761f..49931592ad1 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -162,7 +162,6 @@ Step 0: Import Pyomo and the Pyomo.DoE module and create an ``Experiment`` class >>> import pyomo.environ as pyo >>> from pyomo.contrib.doe import DesignOfExperiments >>> import numpy as np - >>> import idaes # Required to add ipopt linear solvers to path if not done manually .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py :start-after: ======================== From b34052bf753fa8e0cacd9cd917c3a0d4521db076 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 12 Aug 2024 21:14:20 -0400 Subject: [PATCH 2235/3044] Add PyROS installation instructions section --- doc/OnlineDocs/contributed_packages/pyros.rst | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index 980aa2feb00..8b7bfac4f7e 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -115,6 +115,40 @@ When using PyROS, please consider citing [Isenberg_et_al]_. and one or more of the :math:`y` variables should be appropriately redesignated to be part of either :math:`x` or :math:`z`. +PyROS Installation +----------------------------- +PyROS can be installed as follows: + +1. :doc:`Install Pyomo <../../installation>`. + PyROS is included in the Pyomo software package, at pyomo/contrib/pyros. +2. Install NumPy and SciPy with your preferred package manager; + both NumPy and SciPy are required dependencies of PyROS. + You may install NumPy and SciPy with, for example, ``conda``: + + :: + + conda install numpy scipy + + or ``pip``: + + :: + + pip install numpy scipy +3. (*Optional*) Test your installation: + install ``pytest`` and ``parameterized`` + with your preferred package manager (as in the previous step). + You may then run the PyROS tests as follows: + + :: + + cd pyomo/contrib/pyros/tests + pytest + + Some tests involving solvers may fail or be skipped, + depending on the solver distributions (e.g., Ipopt, BARON, SCIP) + that you have pre-installed and licensed on your system. + + PyROS Required Inputs ----------------------------- The required inputs to the PyROS solver are: From 7bd2118ef42a33795cbbfd269cfdea6bd25e1ec5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 20:32:09 -0600 Subject: [PATCH 2236/3044] Fixing two bugs with cloning transformed PiecewiseLinearFunctions --- .../piecewise/piecewise_linear_function.py | 16 +++++++++++++--- .../piecewise/tests/test_nonlinear_to_pwl.py | 4 ++-- .../piecewise/transform/nonlinear_to_pwl.py | 10 +++++----- .../transform/piecewise_to_mip_visitor.py | 2 +- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index e92edacc756..cdd977bbda1 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -43,6 +43,10 @@ def __init__(self, component=None): BlockData.__init__(self, component) with self._declare_reserved_components(): + # map of PiecewiseLinearExpression objects to integer indices in + # self._expressions + self._expression_ids = ComponentMap() + # index is monotonically increasing integer self._expressions = Expression(NonNegativeIntegers) self._transformed_exprs = ComponentMap() self._simplices = None @@ -63,8 +67,9 @@ def __call__(self, *args): return self._evaluate(*args) else: expr = PiecewiseLinearExpression(args, self) - idx = id(expr) + idx = len(self._expressions) self._expressions[idx] = expr + self._expression_ids[expr] = idx return self._expressions[idx] def _evaluate(self, *args): @@ -134,7 +139,12 @@ def map_transformation_var(self, pw_expr, v): Records on the PiecewiseLinearFunction object that the transformed form of the PiecewiseLinearExpression object pw_expr is the Var v. """ - self._transformed_exprs[self._expressions[id(pw_expr)]] = v + if pw_expr not in self._expression_ids: + raise DeveloperError( + "ID of PiecewiseLinearExpression '%s' not in the _expression_ids " + "dictionary of PiecewiseLinearFunction '%s'" % (pw_expr, self) + ) + self._transformed_exprs[self._expressions[self._expression_ids[pw_expr]]] = v def get_transformation_var(self, pw_expr): """ @@ -159,7 +169,7 @@ def __call__(self, x): class _multivariate_linear_functor(AutoSlots.Mixin): - __slots__ = 'normal' + __slots__ = ('normal',) def __init__(self, normal): self.normal = normal diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 81a1e186234..2530fda8fc2 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -77,7 +77,7 @@ def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): self.assertEqual(len(pwlf._expressions), 1) new_cons = n_to_pwl.get_transformed_component(m.cons) self.assertTrue(new_cons.active) - self.assertIs(new_cons.body, pwlf._expressions[id(new_cons.body.expr)]) + self.assertIs(new_cons.body, pwlf._expressions[pwlf._expression_ids[new_cons.body.expr]]) self.assertIsNone(new_cons.ub) self.assertEqual(new_cons.lb, 0.35) self.assertIs(n_to_pwl.get_src_component(new_cons), m.cons) @@ -331,7 +331,7 @@ def check_pw_linear_paraboloid(self, m, pwlf, x1, x2, y1, y2): self.assertEqual(len(pwlf._expressions), 1) new_obj = n_to_pwl.get_transformed_component(m.obj) self.assertTrue(new_obj.active) - self.assertIs(new_obj.expr, pwlf._expressions[id(new_obj.expr.expr)]) + self.assertIs(new_obj.expr, pwlf._expressions[pwlf._expression_ids[new_obj.expr.expr]]) self.assertIs(n_to_pwl.get_src_component(new_obj), m.obj) quadratic = n_to_pwl.get_transformed_quadratic_constraints(m) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 5755286eabe..c4d7c801ba2 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -641,7 +641,7 @@ def _approximate_expression( return None, expr_type # Additively decompose expr and work on the pieces - pwl_func = 0 + pwl_summands = [] for k, subexpr in enumerate( _additively_decompose_expr( expr, config.min_dimension_to_additively_decompose @@ -661,10 +661,10 @@ def _approximate_expression( "'max_dimension' or additively separating the expression." % (obj.name, config.max_dimension) ) - pwl_func = pwl_func + subexpr + pwl_summands.append(subexpr) continue elif not self._needs_approximating(subexpr, approximate_quadratic)[1]: - pwl_func = pwl_func + subexpr + pwl_summands.append(subexpr) continue # else we approximate subexpr @@ -684,13 +684,13 @@ def eval_expr(*args): # implementation of iadd and dereference the ExpressionData holding # the PiecewiseLinearExpression that we later transform my remapping # it to a Var... - pwl_func = pwl_func + pwlf(*expr_vars) + pwl_summands.append(pwlf(*expr_vars)) # restore var values for v, val in orig_values.items(): v.value = val - return pwl_func, expr_type + return sum(pwl_summands), expr_type def get_src_component(self, cons): data = cons.parent_block().private_data().src_component diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py b/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py index fae95a564bf..d40bbd8bb34 100644 --- a/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py +++ b/pyomo/contrib/piecewise/transform/piecewise_to_mip_visitor.py @@ -50,7 +50,7 @@ def exitNode(self, node, data): substitute_var = self.transform_pw_linear_expression( node, parent, self.transBlock ) - parent._expressions[id(node)] = substitute_var + parent._expressions[parent._expression_ids[node]] = substitute_var return node finalizeResult = None From 208c9d6ef2e6b464bbad45981e4ed954b42f9892 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 20:32:34 -0600 Subject: [PATCH 2237/3044] Black --- pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 2530fda8fc2..2f13532b63f 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -77,7 +77,9 @@ def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): self.assertEqual(len(pwlf._expressions), 1) new_cons = n_to_pwl.get_transformed_component(m.cons) self.assertTrue(new_cons.active) - self.assertIs(new_cons.body, pwlf._expressions[pwlf._expression_ids[new_cons.body.expr]]) + self.assertIs( + new_cons.body, pwlf._expressions[pwlf._expression_ids[new_cons.body.expr]] + ) self.assertIsNone(new_cons.ub) self.assertEqual(new_cons.lb, 0.35) self.assertIs(n_to_pwl.get_src_component(new_cons), m.cons) @@ -331,7 +333,9 @@ def check_pw_linear_paraboloid(self, m, pwlf, x1, x2, y1, y2): self.assertEqual(len(pwlf._expressions), 1) new_obj = n_to_pwl.get_transformed_component(m.obj) self.assertTrue(new_obj.active) - self.assertIs(new_obj.expr, pwlf._expressions[pwlf._expression_ids[new_obj.expr.expr]]) + self.assertIs( + new_obj.expr, pwlf._expressions[pwlf._expression_ids[new_obj.expr.expr]] + ) self.assertIs(n_to_pwl.get_src_component(new_obj), m.obj) quadratic = n_to_pwl.get_transformed_quadratic_constraints(m) From defc953b3a120cfbed1d44356e7a37381664ad12 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 20:36:14 -0600 Subject: [PATCH 2238/3044] Adding a test for cloning a multivariate piecewise linear function too --- .../piecewise/tests/test_nonlinear_to_pwl.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 2f13532b63f..a42846c2802 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -376,6 +376,36 @@ def test_paraboloid_objective_uniform_grid(self): self.check_pw_linear_paraboloid(m, pwlf, x1, x2, y1, y2) + @unittest.skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(scipy_available, "Scipy is not available") + def test_multivariate_clone(self): + m = self.make_paraboloid_model() + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, + num_points=2, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + ) + + twin = m.clone() + + # check obj is transformed + self.assertFalse(twin.obj.active) + + pwlf = list( + twin.component_data_objects(PiecewiseLinearFunction, descend_into=True) + ) + self.assertEqual(len(pwlf), 1) + pwlf = pwlf[0] + + x1 = 0.00030000000000000003 + x2 = 2.9997 + y1 = 1.0006 + y2 = 6.9994 + + self.check_pw_linear_paraboloid(twin, pwlf, x1, x2, y1, y2) + @unittest.skipUnless(numpy_available, "Numpy is not available") @unittest.skipUnless(scipy_available, "Scipy is not available") def test_objective_target(self): From 38a48b839dc19014b9976444317c9bc241496252 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 20:42:56 -0600 Subject: [PATCH 2239/3044] Black --- pyomo/contrib/alternative_solutions/tests/test_solution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/alternative_solutions/tests/test_solution.py b/pyomo/contrib/alternative_solutions/tests/test_solution.py index e8faaf83548..9df9374daef 100644 --- a/pyomo/contrib/alternative_solutions/tests/test_solution.py +++ b/pyomo/contrib/alternative_solutions/tests/test_solution.py @@ -42,7 +42,7 @@ def get_model(self): @unittest.skipUnless( pe.SolverFactory(mip_solver).available(exception_flag=False), - "MIP solver not available" + "MIP solver not available", ) def test_solution(self): """ From 4cff6ff0ba99b32e1ac5c0a9f017f3e972706e57 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 20:52:40 -0600 Subject: [PATCH 2240/3044] Setting objective expr and sense in obbt rather than having to check for the component --- pyomo/contrib/alternative_solutions/obbt.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 09904be5458..3fc59bd3214 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -216,6 +216,8 @@ def obbt_analysis_bounds_and_solutions( orig_objective_value = pe.value(orig_objective) logger.info("Found optimal solution, value = {}.".format(orig_objective_value)) aos_block = aos_utils._add_aos_block(model, name="_obbt") + # placeholder for objective + aos_block.var_objective = pe.Objective(expr=0) logger.info("Added block {} to the model.".format(aos_block)) obj_constraints = aos_utils._add_objective_constraint( aos_block, orig_objective, orig_objective_value, rel_opt_gap, abs_opt_gap @@ -236,7 +238,7 @@ def obbt_analysis_bounds_and_solutions( opt.update_config.update_vars = False opt.update_config.update_params = False opt.update_config.update_named_expressions = False - opt.update_config.update_objective = False + opt.update_config.update_objective = True opt.update_config.treat_fixed_vars_as_params = False variable_bounds = pe.ComponentMap() @@ -254,11 +256,8 @@ def obbt_analysis_bounds_and_solutions( if idx == 0: variable_bounds[var] = [None, None] - # NOTE: Simply setting the expr/sense values works differently with the APPSI solver - if hasattr(aos_block, "var_objective"): - aos_block.del_component("var_objective") - - aos_block.var_objective = pe.Objective(expr=var, sense=sense) + aos_block.var_objective.expr = var + aos_block.var_objective.sense = sense if warmstart: _update_values(var, bound_dir, solutions) From cb87b9f6e6a65eb5516fe9c223692c8030cd787c Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 13 Aug 2024 01:08:43 -0400 Subject: [PATCH 2241/3044] Add extra test for inequality constraint preprocessing --- pyomo/contrib/pyros/tests/test_preprocessor.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index 2fe3785b4ee..5268ebc9dfa 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -967,6 +967,7 @@ def build_simple_test_model_data(self): m.c1 = Constraint(expr=m.x1 <= 1) m.c2 = Constraint(expr=(1, m.x1, 2)) m.c3 = Constraint(expr=m.q <= m.x1) + m.c3_up = Constraint(expr=m.x1 - 2 * m.q <= 0) m.c4 = Constraint(expr=(log(m.p), m.x2, m.q)) m.c5 = Constraint(expr=(m.q, m.x2, 2 * m.q)) m.c6 = Constraint(expr=m.z1 <= 1) @@ -990,6 +991,7 @@ def build_simple_test_model_data(self): m.c1, m.c2, m.c3, + m.c3_up, m.c4, m.c5, m.c6, @@ -1024,7 +1026,7 @@ def test_standardize_inequality_constraints(self): ss_ineq_cons = working_model.second_stage.inequality_cons self.assertEqual(len(fs_ineq_cons), 4) - self.assertEqual(len(ss_ineq_cons), 12) + self.assertEqual(len(ss_ineq_cons), 13) self.assertFalse(m.c1.active) new_c1_con = fs_ineq_cons["ineq_con_c1"] @@ -1046,6 +1048,15 @@ def test_standardize_inequality_constraints(self): assertExpressionsEqual(self, new_c3_con.expr, -m.x1 <= -m.q) self.assertEqual(model_data.separation_priority_order[new_c3_con.index()], 1) + # m.x1 - 2 * m.q <= 0; + # single second-stage inequality. modify in place + # test case where uncertain param is in body, + # rather than bound, and rest of expression is first-stage + self.assertFalse(m.c3_up.active) + new_c3_up_con = ss_ineq_cons["ineq_con_c3_up_upper_bound_con"] + self.assertTrue(new_c3_up_con.active) + assertExpressionsEqual(self, new_c3_up_con.expr, m.x1 - 2 * m.q <= 0.0) + # log(m.p) <= m.x2 <= m.q # lower bound is first-stage, upper bound second-stage self.assertFalse(m.c4.active) From d864d2fc882c4c7104c542075aff49ec7ff924f4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 00:32:46 -0600 Subject: [PATCH 2242/3044] Remove repeated code --- pyomo/core/base/constraint.py | 45 +++++++++++++---------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index bc9a32f5404..8c7921b060f 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -236,6 +236,21 @@ def to_bounded_expression(self): return 0 if expr.__class__ is EqualityExpression else None, lhs - rhs, 0 return None, None, None + def _evaluate_bound(self, bound, is_lb): + if bound is None: + return None + if bound.__class__ not in native_numeric_types: + bound = float(value(bound)) + # Note that "bound != bound" catches float('nan') + if bound in _nonfinite_values or bound != bound: + if bound == (-_inf if is_lb else _inf): + return None + raise ValueError( + f"Constraint '{self.name}' created with an invalid non-finite " + f"{'lower' if is_lb else 'upper'} bound ({bound})." + ) + return bound + @property def body(self): """Access the body of a constraint expression.""" @@ -291,38 +306,12 @@ def upper(self): @property def lb(self): """Access the value of the lower bound of a constraint expression.""" - bound = self.to_bounded_expression()[0] - if bound is None: - return None - if bound.__class__ not in native_numeric_types: - bound = float(value(bound)) - # Note that "bound != bound" catches float('nan') - if bound in _nonfinite_values or bound != bound: - if bound == -_inf: - return None - raise ValueError( - f"Constraint '{self.name}' created with an invalid non-finite " - f"lower bound ({bound})." - ) - return bound + return self._evaluate_bound(self.to_bounded_expression()[0], True) @property def ub(self): """Access the value of the upper bound of a constraint expression.""" - bound = self.to_bounded_expression()[2] - if bound is None: - return None - if bound.__class__ not in native_numeric_types: - bound = float(value(bound)) - # Note that "bound != bound" catches float('nan') - if bound in _nonfinite_values or bound != bound: - if bound == _inf: - return None - raise ValueError( - f"Constraint '{self.name}' created with an invalid non-finite " - f"upper bound ({bound})." - ) - return bound + return self._evaluate_bound(self.to_bounded_expression()[2], False) @property def equality(self): From 58da384ff9853cc2f70ef1e1f6e6ca2b02365762 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 00:33:19 -0600 Subject: [PATCH 2243/3044] Add option to Constraint.to_bounded_expression() to evaluate the bounds --- pyomo/core/base/constraint.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 8c7921b060f..f0e020bcfd0 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -179,7 +179,7 @@ def __call__(self, exception=True): body = value(self.body, exception=exception) return body - def to_bounded_expression(self): + def to_bounded_expression(self, evaluate_bounds=False): """Convert this constraint to a tuple of 3 expressions (lb, body, ub) This method "standardizes" the expression into a 3-tuple of @@ -195,6 +195,13 @@ def to_bounded_expression(self): extension, the result) can change after fixing / unfixing :py:class:`Var` objects. + Parameters + ---------- + evaluate_bounds: bool + + If True, then the lower and upper bounds will be evaluated + to a finite numeric constant or None. + Raises ------ @@ -226,15 +233,21 @@ def to_bounded_expression(self): "variable upper bound. Cannot normalize the " "constraint or send it to a solver." ) - return ans - elif expr is not None: + elif expr is None: + ans = None, None, None + else: lhs, rhs = expr.args if rhs.__class__ in native_types or not rhs.is_potentially_variable(): - return rhs if expr.__class__ is EqualityExpression else None, lhs, rhs - if lhs.__class__ in native_types or not lhs.is_potentially_variable(): - return lhs, rhs, lhs if expr.__class__ is EqualityExpression else None - return 0 if expr.__class__ is EqualityExpression else None, lhs - rhs, 0 - return None, None, None + ans = rhs if expr.__class__ is EqualityExpression else None, lhs, rhs + elif lhs.__class__ in native_types or not lhs.is_potentially_variable(): + ans = lhs, rhs, lhs if expr.__class__ is EqualityExpression else None + else: + ans = 0 if expr.__class__ is EqualityExpression else None, lhs - rhs, 0 + + if evaluate_bounds: + lb, body, ub = ans + return self._evaluate_bound(lb, True), body, self._evaluate_bound(ub, False) + return ans def _evaluate_bound(self, bound, is_lb): if bound is None: From d29d3db3acd6a56918a7136fe14cc8d4e33534ee Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 00:36:04 -0600 Subject: [PATCH 2244/3044] Update writers to use to_bounded_expression; recover performance loss from #3293 --- pyomo/repn/plugins/baron_writer.py | 49 ++++++++++++++++-------------- pyomo/repn/plugins/gams_writer.py | 15 ++++----- pyomo/repn/plugins/lp_writer.py | 10 +++--- pyomo/repn/plugins/nl_writer.py | 10 +++--- 4 files changed, 45 insertions(+), 39 deletions(-) diff --git a/pyomo/repn/plugins/baron_writer.py b/pyomo/repn/plugins/baron_writer.py index ab673b0c1c3..861735dc973 100644 --- a/pyomo/repn/plugins/baron_writer.py +++ b/pyomo/repn/plugins/baron_writer.py @@ -256,9 +256,9 @@ def _skip_trivial(constraint_data): suffix_gen = ( lambda b: pyomo.core.base.suffix.active_export_suffix_generator(b) ) - r_o_eqns = [] - c_eqns = [] - l_eqns = [] + r_o_eqns = {} + c_eqns = {} + l_eqns = {} branching_priorities_suffixes = [] for block in all_blocks_list: for name, suffix in suffix_gen(block): @@ -266,13 +266,14 @@ def _skip_trivial(constraint_data): branching_priorities_suffixes.append(suffix) elif name == 'constraint_types': for constraint_data, constraint_type in suffix.items(): + info = constraint_data.to_bounded_expression(True) if not _skip_trivial(constraint_data): if constraint_type.lower() == 'relaxationonly': - r_o_eqns.append(constraint_data) + r_o_eqns[constraint_data] = info elif constraint_type.lower() == 'convex': - c_eqns.append(constraint_data) + c_eqns[constraint_data] = info elif constraint_type.lower() == 'local': - l_eqns.append(constraint_data) + l_eqns[constraint_data] = info else: raise ValueError( "A suffix '%s' contained an invalid value: %s\n" @@ -294,7 +295,10 @@ def _skip_trivial(constraint_data): % (name, _location) ) - non_standard_eqns = r_o_eqns + c_eqns + l_eqns + non_standard_eqns = set() + non_standard_eqns.update(r_o_eqns) + non_standard_eqns.update(c_eqns) + non_standard_eqns.update(l_eqns) # # EQUATIONS @@ -304,7 +308,7 @@ def _skip_trivial(constraint_data): n_roeqns = len(r_o_eqns) n_ceqns = len(c_eqns) n_leqns = len(l_eqns) - eqns = [] + eqns = {} # Alias the constraints by declaration order since Baron does not # include the constraint names in the solution file. It is important @@ -321,14 +325,15 @@ def _skip_trivial(constraint_data): for constraint_data in block.component_data_objects( Constraint, active=True, sort=sorter, descend_into=False ): - if (not constraint_data.has_lb()) and (not constraint_data.has_ub()): + lb, body, ub = constraint_data.to_bounded_expression(True) + if lb is None and ub is None: assert not constraint_data.equality continue # non-binding, so skip if (not _skip_trivial(constraint_data)) and ( constraint_data not in non_standard_eqns ): - eqns.append(constraint_data) + eqns[constraint_data] = lb, body, ub con_symbol = symbol_map.createSymbol(constraint_data, c_labeler) assert not con_symbol.startswith('.') @@ -407,12 +412,12 @@ def mutable_param_gen(b): # Equation Definition output_file.write('c_e_FIX_ONE_VAR_CONST__: ONE_VAR_CONST__ == 1;\n') - for constraint_data in itertools.chain(eqns, r_o_eqns, c_eqns, l_eqns): + for constraint_data, (lb, body, ub) in itertools.chain( + eqns.items(), r_o_eqns.items(), c_eqns.items(), l_eqns.items() + ): variables = OrderedSet() # print(symbol_map.byObject.keys()) - eqn_body = expression_to_string( - constraint_data.body, variables, smap=symbol_map - ) + eqn_body = expression_to_string(body, variables, smap=symbol_map) # print(symbol_map.byObject.keys()) referenced_variable_ids.update(variables) @@ -439,22 +444,22 @@ def mutable_param_gen(b): # Equality constraint if constraint_data.equality: eqn_lhs = '' - eqn_rhs = ' == ' + ftoa(constraint_data.upper) + eqn_rhs = ' == ' + ftoa(ub) # Greater than constraint - elif not constraint_data.has_ub(): - eqn_rhs = ' >= ' + ftoa(constraint_data.lower) + elif ub is None: + eqn_rhs = ' >= ' + ftoa(lb) eqn_lhs = '' # Less than constraint - elif not constraint_data.has_lb(): - eqn_rhs = ' <= ' + ftoa(constraint_data.upper) + elif lb is None: + eqn_rhs = ' <= ' + ftoa(ub) eqn_lhs = '' # Double-sided constraint - elif constraint_data.has_lb() and constraint_data.has_ub(): - eqn_lhs = ftoa(constraint_data.lower) + ' <= ' - eqn_rhs = ' <= ' + ftoa(constraint_data.upper) + elif lb is not None and ub is not None: + eqn_lhs = ftoa(lb) + ' <= ' + eqn_rhs = ' <= ' + ftoa(ub) eqn_string = eqn_lhs + eqn_body + eqn_rhs + ';\n' output_file.write(eqn_string) diff --git a/pyomo/repn/plugins/gams_writer.py b/pyomo/repn/plugins/gams_writer.py index a0f407d7952..f0a9eb7afef 100644 --- a/pyomo/repn/plugins/gams_writer.py +++ b/pyomo/repn/plugins/gams_writer.py @@ -619,11 +619,12 @@ def _write_model( # encountered will be added to the var_list due to the labeler # defined above. for con in model.component_data_objects(Constraint, active=True, sort=sort): - if not con.has_lb() and not con.has_ub(): + lb, body, ub = con.to_bounded_expression(True) + if lb is None and ub is None: assert not con.equality continue # non-binding, so skip - con_body = as_numeric(con.body) + con_body = as_numeric(body) if skip_trivial_constraints and con_body.is_fixed(): continue if linear: @@ -642,20 +643,20 @@ def _write_model( constraint_names.append('%s' % cName) ConstraintIO.write( '%s.. %s =e= %s ;\n' - % (constraint_names[-1], con_body_str, ftoa(con.upper, False)) + % (constraint_names[-1], con_body_str, ftoa(ub, False)) ) else: - if con.has_lb(): + if lb is not None: constraint_names.append('%s_lo' % cName) ConstraintIO.write( '%s.. %s =l= %s ;\n' - % (constraint_names[-1], ftoa(con.lower, False), con_body_str) + % (constraint_names[-1], ftoa(lb, False), con_body_str) ) - if con.has_ub(): + if ub is not None: constraint_names.append('%s_hi' % cName) ConstraintIO.write( '%s.. %s =l= %s ;\n' - % (constraint_names[-1], con_body_str, ftoa(con.upper, False)) + % (constraint_names[-1], con_body_str, ftoa(ub, False)) ) obj = list(model.component_data_objects(Objective, active=True, sort=sort)) diff --git a/pyomo/repn/plugins/lp_writer.py b/pyomo/repn/plugins/lp_writer.py index 814f79a4eb9..2fbdae3571d 100644 --- a/pyomo/repn/plugins/lp_writer.py +++ b/pyomo/repn/plugins/lp_writer.py @@ -408,10 +408,10 @@ def write(self, model): if with_debug_timing and con.parent_component() is not last_parent: timer.toc('Constraint %s', last_parent, level=logging.DEBUG) last_parent = con.parent_component() - # Note: Constraint.lb/ub guarantee a return value that is - # either a (finite) native_numeric_type, or None - lb = con.lb - ub = con.ub + # Note: Constraint.to_bounded_expression(evaluate_bounds=True) + # guarantee a return value that is either a (finite) + # native_numeric_type, or None + lb, body, ub = con.to_bounded_expression(True) if lb is None and ub is None: # Note: you *cannot* output trivial (unbounded) @@ -419,7 +419,7 @@ def write(self, model): # slack variable if skip_trivial_constraints is False, # but that seems rather silly. continue - repn = constraint_visitor.walk_expression(con.body) + repn = constraint_visitor.walk_expression(body) if repn.nonlinear is not None: raise ValueError( f"Model constraint ({con.name}) contains nonlinear terms that " diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 8fc82d21d30..ca7786ce167 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -723,14 +723,14 @@ def write(self, model): timer.toc('Constraint %s', last_parent, level=logging.DEBUG) last_parent = con.parent_component() scale = scaling_factor(con) - expr_info = visitor.walk_expression((con.body, con, 0, scale)) + # Note: Constraint.to_bounded_expression(evaluate_bounds=True) + # guarantee a return value that is either a (finite) + # native_numeric_type, or None + lb, body, ub = con.to_bounded_expression(True) + expr_info = visitor.walk_expression((body, con, 0, scale)) if expr_info.named_exprs: self._record_named_expression_usage(expr_info.named_exprs, con, 0) - # Note: Constraint.lb/ub guarantee a return value that is - # either a (finite) native_numeric_type, or None - lb = con.lb - ub = con.ub if lb is None and ub is None: # and self.config.skip_trivial_constraints: continue if scale != 1: From 9f59794537d147e1d7aaa91d3e15e851be5035e6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 01:04:53 -0600 Subject: [PATCH 2245/3044] Fix kernel incompatibility --- pyomo/core/kernel/constraint.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pyomo/core/kernel/constraint.py b/pyomo/core/kernel/constraint.py index fe8eb8b2c1f..ed877e8af92 100644 --- a/pyomo/core/kernel/constraint.py +++ b/pyomo/core/kernel/constraint.py @@ -160,6 +160,17 @@ def has_ub(self): ub = self.ub return (ub is not None) and (value(ub) != float('inf')) + def to_bounded_expression(self, evaluate_bounds=False): + if evaluate_bounds: + lb = self.lb + if lb == -float('inf'): + lb = None + ub = self.ub + if ub == float('inf'): + ub = None + return lb, self.body, ub + return self.lower, self.body, self.upper + class _MutableBoundsConstraintMixin(object): """ @@ -177,9 +188,6 @@ class _MutableBoundsConstraintMixin(object): # Define some of the IConstraint abstract methods # - def to_bounded_expression(self): - return self.lower, self.body, self.upper - @property def lower(self): """The expression for the lower bound of the constraint""" From 94e981dfb769dc505f4218f0c8b9a4288d435516 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 09:07:38 -0400 Subject: [PATCH 2246/3044] Update solving test file to have assertFoo --- .vs/CMakeWorkspaceSettings.json | 3 + pyomo/contrib/doe/doe.py | 2 - pyomo/contrib/doe/tests/test_doe_solve.py | 92 ++++++++--------------- 3 files changed, 36 insertions(+), 61 deletions(-) create mode 100644 .vs/CMakeWorkspaceSettings.json diff --git a/.vs/CMakeWorkspaceSettings.json b/.vs/CMakeWorkspaceSettings.json new file mode 100644 index 00000000000..d3e1057f48a --- /dev/null +++ b/.vs/CMakeWorkspaceSettings.json @@ -0,0 +1,3 @@ +{ + "enableCMake": false +} \ No newline at end of file diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 7e1ecde79c5..54e4749ec21 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1155,7 +1155,6 @@ def create_objective_function(self, model=None): ### Initialize the Cholesky decomposition matrix if self.Cholesky_option and self.objective_option == ObjectiveLib.determinant: - # Calculate the eigenvalues of the FIM matrix eig = np.linalg.eigvals(fim) @@ -1465,7 +1464,6 @@ def compute_FIM_full_factorial(self, design_ranges=None, method="sequential"): time_set = [] curr_point = 1 # Initial current point for design_point in factorial_points: - # Fix design variables at fixed experimental design point for i in range(len(design_point)): design_map[i][1].fix(design_point[i]) diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 34a01e8c0e4..886d32a26b1 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -117,9 +117,9 @@ def get_standard_args(experiment, fd_method, obj_used): return args +@unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") +@unittest.skipIf(not numpy_available, "Numpy is not available") class TestReactorExampleSolving(unittest.TestCase): - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_central_solve(self): fd_method = "central" obj_used = "trace" @@ -132,19 +132,17 @@ def test_reactor_fd_central_solve(self): doe_obj.run_doe() - # Assert model solves - assert doe_obj.results["Solver Status"] == "ok" + # self.assertTrue(model solves + self.assertTrue(doe_obj.results["Solver Status"] == "ok") - # Assert that Q, F, and L are the same. + # self.assertTrue(that Q, F, and L are the same. FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) # Since Trace is used, no comparison for FIM and L.T @ L # Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM) - assert np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q)) + self.assertTrue(np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q))) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_forward_solve(self): fd_method = "forward" obj_used = "zero" @@ -157,18 +155,16 @@ def test_reactor_fd_forward_solve(self): doe_obj.run_doe() - assert doe_obj.results["Solver Status"] == "ok" + self.assertTrue(doe_obj.results["Solver Status"] == "ok") - # Assert that Q, F, and L are the same. + # self.assertTrue(that Q, F, and L are the same. FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) # Since Trace is used, no comparison for FIM and L.T @ L # Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM) - assert np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q)) + self.assertTrue(np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q))) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_fd_backward_solve(self): fd_method = "backward" obj_used = "trace" @@ -181,20 +177,16 @@ def test_reactor_fd_backward_solve(self): doe_obj.run_doe() - assert doe_obj.results["Solver Status"] == "ok" + self.assertTrue(doe_obj.results["Solver Status"] == "ok") - # Assert that Q, F, and L are the same. + # self.assertTrue(that Q, F, and L are the same. FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) # Since Trace is used, no comparison for FIM and L.T @ L # Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM) - assert np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q)) + self.assertTrue(np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q))) - # TODO: Fix determinant objective code, something is awry - # Should only be using Cholesky=True - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_obj_det_solve(self): fd_method = "central" obj_used = "determinant" @@ -202,9 +194,9 @@ def test_reactor_obj_det_solve(self): experiment = FullReactorExperiment(data_ex, 10, 3) DoE_args = get_standard_args(experiment, fd_method, obj_used) - DoE_args['scale_nominal_param_value'] = ( - False # Vanilla determinant solve needs this - ) + DoE_args[ + 'scale_nominal_param_value' + ] = False # Vanilla determinant solve needs this DoE_args['_Cholesky_option'] = False DoE_args['_only_compute_fim_lower'] = False @@ -212,10 +204,8 @@ def test_reactor_obj_det_solve(self): doe_obj.run_doe() - assert doe_obj.results['Solver Status'] == "ok" + self.assertTrue(doe_obj.results['Solver Status'] == "ok") - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_obj_cholesky_solve(self): fd_method = "central" obj_used = "determinant" @@ -228,19 +218,17 @@ def test_reactor_obj_cholesky_solve(self): doe_obj.run_doe() - assert doe_obj.results["Solver Status"] == "ok" + self.assertTrue(doe_obj.results["Solver Status"] == "ok") - # Assert that Q, F, and L are the same. + # self.assertTrue(that Q, F, and L are the same. FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) # Since Cholesky is used, there is comparison for FIM and L.T @ L - assert np.all(np.isclose(FIM, L @ L.T)) + self.assertTrue(np.all(np.isclose(FIM, L @ L.T))) # Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM) - assert np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q)) + self.assertTrue(np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q))) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_compute_FIM_seq_centr(self): fd_method = "central" obj_used = "determinant" @@ -253,8 +241,6 @@ def test_compute_FIM_seq_centr(self): doe_obj.compute_FIM(method="sequential") - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_compute_FIM_seq_forward(self): fd_method = "forward" obj_used = "determinant" @@ -267,12 +253,10 @@ def test_compute_FIM_seq_forward(self): doe_obj.compute_FIM(method="sequential") - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not scipy_available, "Scipy is not available") @unittest.skipIf( not k_aug_available.available(False), "The 'k_aug' command is not available" ) - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_compute_FIM_kaug(self): fd_method = "forward" obj_used = "determinant" @@ -285,8 +269,6 @@ def test_compute_FIM_kaug(self): doe_obj.compute_FIM(method="kaug") - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_compute_FIM_seq_backward(self): fd_method = "backward" obj_used = "determinant" @@ -299,9 +281,7 @@ def test_compute_FIM_seq_backward(self): doe_obj.compute_FIM(method="sequential") - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_grid_search(self): fd_method = "central" obj_used = "determinant" @@ -322,17 +302,15 @@ def test_reactor_grid_search(self): CA_vals = doe_obj.fim_factorial_results["CA[0]"] T_vals = doe_obj.fim_factorial_results["T[0]"] - # Assert length is correct - assert (len(CA_vals) == 9) and (len(T_vals) == 9) - assert (len(set(CA_vals)) == 3) and (len(set(T_vals)) == 3) + # self.assertTrue(length is correct + self.assertTrue((len(CA_vals) == 9) and (len(T_vals) == 9)) + self.assertTrue((len(set(CA_vals)) == 3) and (len(set(T_vals)) == 3)) - # Assert unique values are correct - assert (set(CA_vals).issuperset(set([1, 3, 5]))) and ( + # self.assertTrue(unique values are correct + self.assertTrue((set(CA_vals).issuperset(set([1, 3, 5]))) and ( set(T_vals).issuperset(set([300, 500, 700])) - ) + )) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_rescale_FIM(self): fd_method = "central" obj_used = "determinant" @@ -372,10 +350,8 @@ def test_rescale_FIM(self): resc_FIM = rescale_FIM(FIM, param_vals) # Compare scaled and rescaled values - assert np.all(np.isclose(FIM2, resc_FIM)) + self.assertTrue(np.all(np.isclose(FIM2, resc_FIM))) - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_solve_bad_model(self): fd_method = "central" obj_used = "determinant" @@ -392,9 +368,7 @@ def test_reactor_solve_bad_model(self): ): doe_obj.run_doe() - @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_grid_search_bad_model(self): fd_method = "central" obj_used = "determinant" @@ -416,14 +390,14 @@ def test_reactor_grid_search_bad_model(self): CA_vals = doe_obj.fim_factorial_results["CA[0]"] T_vals = doe_obj.fim_factorial_results["T[0]"] - # Assert length is correct - assert (len(CA_vals) == 9) and (len(T_vals) == 9) - assert (len(set(CA_vals)) == 3) and (len(set(T_vals)) == 3) + # self.assertTrue(length is correct + self.assertTrue((len(CA_vals) == 9) and (len(T_vals) == 9)) + self.assertTrue((len(set(CA_vals)) == 3) and (len(set(T_vals)) == 3)) - # Assert unique values are correct - assert (set(CA_vals).issuperset(set([1, 3, 5]))) and ( + # self.assertTrue(unique values are correct + self.assertTrue((set(CA_vals).issuperset(set([1, 3, 5]))) and ( set(T_vals).issuperset(set([300, 500, 700])) - ) + )) if __name__ == "__main__": From 324146da41e04fee5b1cf3f6a77f0858cb394945 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 09:11:58 -0400 Subject: [PATCH 2247/3044] Moved has numpy decorator to class level --- pyomo/contrib/doe/tests/test_doe_errors.py | 33 +--------------------- pyomo/contrib/doe/tests/test_doe_solve.py | 14 +++++---- 2 files changed, 9 insertions(+), 38 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 76599d3c468..40cc2ba10e5 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -57,8 +57,8 @@ def get_standard_args(experiment, fd_method, obj_used, flag): return args +@unittest.skipIf(not numpy_available, "Numpy is not available") class TestReactorExampleErrors(unittest.TestCase): - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_no_get_labeled_model(self): fd_method = "central" obj_used = "trace" @@ -74,7 +74,6 @@ def test_reactor_check_no_get_labeled_model(self): doe_obj = DesignOfExperiments(**DoE_args) - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_no_experiment_outputs(self): fd_method = "central" obj_used = "trace" @@ -92,7 +91,6 @@ def test_reactor_check_no_experiment_outputs(self): ): doe_obj.create_doe_model() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_no_measurement_error(self): fd_method = "central" obj_used = "trace" @@ -110,7 +108,6 @@ def test_reactor_check_no_measurement_error(self): ): doe_obj.create_doe_model() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_no_experiment_inputs(self): fd_method = "central" obj_used = "trace" @@ -128,7 +125,6 @@ def test_reactor_check_no_experiment_inputs(self): ): doe_obj.create_doe_model() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_no_unknown_parameters(self): fd_method = "central" obj_used = "trace" @@ -146,7 +142,6 @@ def test_reactor_check_no_unknown_parameters(self): ): doe_obj.create_doe_model() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_bad_prior_size(self): fd_method = "central" obj_used = "trace" @@ -169,7 +164,6 @@ def test_reactor_check_bad_prior_size(self): ): doe_obj.create_doe_model() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_bad_jacobian_init_size(self): fd_method = "central" obj_used = "trace" @@ -192,7 +186,6 @@ def test_reactor_check_bad_jacobian_init_size(self): ): doe_obj.create_doe_model() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_unbuilt_update_FIM(self): fd_method = "central" obj_used = "trace" @@ -212,7 +205,6 @@ def test_reactor_check_unbuilt_update_FIM(self): ): doe_obj.update_FIM_prior(FIM=FIM_update) - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_none_update_FIM(self): fd_method = "central" obj_used = "trace" @@ -232,7 +224,6 @@ def test_reactor_check_none_update_FIM(self): ): doe_obj.update_FIM_prior(FIM=FIM_update) - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_results_file_name(self): fd_method = "central" obj_used = "trace" @@ -249,7 +240,6 @@ def test_reactor_check_results_file_name(self): ): doe_obj.run_doe(results_file=int(15)) - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_measurement_and_output_length_match(self): fd_method = "central" obj_used = "trace" @@ -273,7 +263,6 @@ def test_reactor_check_measurement_and_output_length_match(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_grid_search_des_range_inputs(self): fd_method = "central" obj_used = "determinant" @@ -296,7 +285,6 @@ def test_reactor_grid_search_des_range_inputs(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_premature_figure_drawing(self): fd_method = "central" obj_used = "determinant" @@ -315,7 +303,6 @@ def test_reactor_premature_figure_drawing(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_figure_drawing_no_des_var_names(self): fd_method = "central" obj_used = "determinant" @@ -340,7 +327,6 @@ def test_reactor_figure_drawing_no_des_var_names(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_figure_drawing_no_sens_names(self): fd_method = "central" obj_used = "determinant" @@ -364,7 +350,6 @@ def test_reactor_figure_drawing_no_sens_names(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_figure_drawing_no_fixed_names(self): fd_method = "central" obj_used = "determinant" @@ -388,7 +373,6 @@ def test_reactor_figure_drawing_no_fixed_names(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_figure_drawing_bad_fixed_names(self): fd_method = "central" obj_used = "determinant" @@ -416,7 +400,6 @@ def test_reactor_figure_drawing_bad_fixed_names(self): @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") @unittest.skipIf(not pandas_available, "pandas is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_figure_drawing_bad_sens_names(self): fd_method = "central" obj_used = "determinant" @@ -442,7 +425,6 @@ def test_reactor_figure_drawing_bad_sens_names(self): fixed_design_variables={"CA[0]": 1}, ) - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_get_FIM_without_FIM(self): fd_method = "central" obj_used = "trace" @@ -462,7 +444,6 @@ def test_reactor_check_get_FIM_without_FIM(self): ): doe_obj.get_FIM() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_get_sens_mat_without_model(self): fd_method = "central" obj_used = "trace" @@ -482,7 +463,6 @@ def test_reactor_check_get_sens_mat_without_model(self): ): doe_obj.get_sensitivity_matrix() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_get_exp_inputs_without_model(self): fd_method = "central" obj_used = "trace" @@ -502,7 +482,6 @@ def test_reactor_check_get_exp_inputs_without_model(self): ): doe_obj.get_experiment_input_values() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_get_exp_outputs_without_model(self): fd_method = "central" obj_used = "trace" @@ -522,7 +501,6 @@ def test_reactor_check_get_exp_outputs_without_model(self): ): doe_obj.get_experiment_output_values() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_get_unknown_params_without_model(self): fd_method = "central" obj_used = "trace" @@ -542,7 +520,6 @@ def test_reactor_check_get_unknown_params_without_model(self): ): doe_obj.get_unknown_parameter_values() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_reactor_check_get_meas_error_without_model(self): fd_method = "central" obj_used = "trace" @@ -562,7 +539,6 @@ def test_reactor_check_get_meas_error_without_model(self): ): doe_obj.get_measurement_error_values() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_multiple_exp_not_implemented_seq(self): fd_method = "central" obj_used = "trace" @@ -581,7 +557,6 @@ def test_multiple_exp_not_implemented_seq(self): ): doe_obj.run_multi_doe_sequential(N_exp=1) - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_multiple_exp_not_implemented_sim(self): fd_method = "central" obj_used = "trace" @@ -600,7 +575,6 @@ def test_multiple_exp_not_implemented_sim(self): ): doe_obj.run_multi_doe_simultaneous(N_exp=1) - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_update_unknown_parameter_values_not_implemented_seq(self): fd_method = "central" obj_used = "trace" @@ -620,7 +594,6 @@ def test_update_unknown_parameter_values_not_implemented_seq(self): doe_obj.update_unknown_parameter_values() @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_bad_FD_generate_scens(self): fd_method = "central" obj_used = "trace" @@ -642,7 +615,6 @@ def test_bad_FD_generate_scens(self): doe_obj._generate_scenario_blocks() @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_bad_FD_seq_compute_FIM(self): fd_method = "central" obj_used = "trace" @@ -663,7 +635,6 @@ def test_bad_FD_seq_compute_FIM(self): doe_obj.fd_formula = "bad things" doe_obj.compute_FIM(method="sequential") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_bad_objective(self): fd_method = "central" obj_used = "trace" @@ -684,7 +655,6 @@ def test_bad_objective(self): doe_obj.objective_option = "bad things" doe_obj.create_objective_function() - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_no_model_for_objective(self): fd_method = "central" obj_used = "trace" @@ -705,7 +675,6 @@ def test_no_model_for_objective(self): doe_obj.create_objective_function() @unittest.skipIf(not ipopt_available, "The 'ipopt' command is not available") - @unittest.skipIf(not numpy_available, "Numpy is not available") def test_bad_compute_FIM_option(self): fd_method = "central" obj_used = "trace" diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 886d32a26b1..eee123f436a 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -307,9 +307,10 @@ def test_reactor_grid_search(self): self.assertTrue((len(set(CA_vals)) == 3) and (len(set(T_vals)) == 3)) # self.assertTrue(unique values are correct - self.assertTrue((set(CA_vals).issuperset(set([1, 3, 5]))) and ( - set(T_vals).issuperset(set([300, 500, 700])) - )) + self.assertTrue( + (set(CA_vals).issuperset(set([1, 3, 5]))) + and (set(T_vals).issuperset(set([300, 500, 700]))) + ) def test_rescale_FIM(self): fd_method = "central" @@ -395,9 +396,10 @@ def test_reactor_grid_search_bad_model(self): self.assertTrue((len(set(CA_vals)) == 3) and (len(set(T_vals)) == 3)) # self.assertTrue(unique values are correct - self.assertTrue((set(CA_vals).issuperset(set([1, 3, 5]))) and ( - set(T_vals).issuperset(set([300, 500, 700])) - )) + self.assertTrue( + (set(CA_vals).issuperset(set([1, 3, 5]))) + and (set(T_vals).issuperset(set([300, 500, 700]))) + ) if __name__ == "__main__": From 9c526903211d6d3b7fabfad4a9dde93a8726da28 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 09:15:24 -0400 Subject: [PATCH 2248/3044] Updated language in comments and URL Have to change this URL at some point in time. --- pyomo/contrib/doe/doe.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 54e4749ec21..b1d2dcabc24 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -87,7 +87,7 @@ def __init__( This package enables model-based design of experiments analysis with Pyomo. Both direct optimization and enumeration modes are supported. - The package has been refactored from its original form as of ##/##/##24. See + The package has been refactored from its original form as of August 24. See the documentation for more information. Parameters @@ -152,8 +152,8 @@ def __init__( "DEPRECATION ERROR: Pyomo.DoE has been refactored. The current interface utilizes Experiment " "objects that label unknown parameters, experiment inputs, experiment outputs and measurement " "error. This avoids string-based naming which is fragile. For instruction to use the new " - "interface, please see Pyomo.DoE under the contributed packages documentation at " - "`https://pyomo.readthedocs.io/en/stable/`" + "interface, please see the Pyomo.DoE under the contributed packages documentation at " + "`https://pyomo.readthedocs.io/en/latest/contributed_packages/doe/doe.html`" ) if experiment is None: From d8307a73941eab309397b8570d8bfca05a38e1ff Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 09:25:14 -0400 Subject: [PATCH 2249/3044] Removed safe naming for later discussion Also removed a potential bug for adding this in the future. --- pyomo/contrib/doe/doe.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index b1d2dcabc24..6775463ce95 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -254,9 +254,11 @@ def run_doe(self, model=None, results_file=None): if model is None: model = self.model else: - doe_block = pyo.Block() - doe_block_name = unique_component_name(model, "design_of_experiments_block") - model.add_component(doe_block_name, doe_block) + # TODO: Add safe naming when a model is passed by the user. + # doe_block = pyo.Block() + # doe_block_name = unique_component_name(model, "design_of_experiments_block") + # model.add_component(doe_block_name, doe_block) + pass # ToDo: potentially work with this for more complicated models # Create the full DoE model (build scenarios for F.D. scheme) @@ -346,23 +348,23 @@ def run_doe(self, model=None, results_file=None): self.results["Sensitivity Matrix"] = self.get_sensitivity_matrix() self.results["Experiment Design"] = self.get_experiment_input_values() self.results["Experiment Design Names"] = [ - str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) - for k in self.model.scenario_blocks[0].experiment_inputs + str(pyo.ComponentUID(k, context=model.scenario_blocks[0])) + for k in model.scenario_blocks[0].experiment_inputs ] self.results["Experiment Outputs"] = self.get_experiment_output_values() self.results["Experiment Output Names"] = [ - str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) - for k in self.model.scenario_blocks[0].experiment_outputs + str(pyo.ComponentUID(k, context=model.scenario_blocks[0])) + for k in model.scenario_blocks[0].experiment_outputs ] self.results["Unknown Parameters"] = self.get_unknown_parameter_values() self.results["Unknown Parameter Names"] = [ - str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) - for k in self.model.scenario_blocks[0].unknown_parameters + str(pyo.ComponentUID(k, context=model.scenario_blocks[0])) + for k in model.scenario_blocks[0].unknown_parameters ] self.results["Measurement Error"] = self.get_measurement_error_values() self.results["Measurement Error Names"] = [ - str(pyo.ComponentUID(k, context=self.model.scenario_blocks[0])) - for k in self.model.scenario_blocks[0].measurement_error + str(pyo.ComponentUID(k, context=model.scenario_blocks[0])) + for k in model.scenario_blocks[0].measurement_error ] self.results["Prior FIM"] = [list(row) for row in list(self.prior_FIM)] From ec93a2b583862e6804eed158991ed60ae38338a6 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 09:28:21 -0400 Subject: [PATCH 2250/3044] Added solver safeguarding TODO --- pyomo/contrib/doe/doe.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 6775463ce95..d4d361b3245 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -282,6 +282,13 @@ def run_doe(self, model=None, results_file=None): for comp in model.scenario_blocks[0].experiment_inputs: comp.fix() + # TODO: safeguard solver call to see if solver terminated successfully + # see below commented code: + # res = self.solver.solve(model, tee=self.tee, load_solutions=False) + # if pyo.check_optimal_termination(res): + # model.load_solution(res) + # else: + # # The solver was unsuccessful, might want to warn the user or terminate gracefully, etc. model.dummy_obj = pyo.Objective(expr=0, sense=pyo.minimize) self.solver.solve(model, tee=self.tee) From 3541460f8aa1f7996d4a5611664f9a72d4bbe80b Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 09:44:25 -0400 Subject: [PATCH 2251/3044] Added TODOs and added error to unreachable code --- pyomo/contrib/doe/doe.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index d4d361b3245..bc1157e1564 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -454,6 +454,9 @@ def compute_FIM(self, model=None, method="sequential"): else: self.check_model_FIM(FIM=self.prior_FIM) + # TODO: Add a check to see if the model has an objective and deactivate it. + # This solve should only be a square solve without any obj function. + if method == "sequential": self._sequential_FIM(model=model) self._computed_FIM = self.seq_FIM @@ -478,7 +481,7 @@ def _sequential_FIM(self, model=None): matrix to subsequently compute the FIM. """ - # Build a singular model instance + # Build a single model instance if model is None: self.compute_FIM_model = self.experiment.get_labeled_model( **self.get_labeled_model_args @@ -548,7 +551,7 @@ def _sequential_FIM(self, model=None): # Simulate the model try: res = self.solver.solve(model) - assert res.solver.termination_condition == "optimal" + pyo.assert_optimal_termination(res) except: raise RuntimeError( "Model from experiment did not solve appropriately. Make sure the model is well-posed." @@ -635,9 +638,7 @@ def _kaug_FIM(self, model=None): if not hasattr(model, "objective"): model.objective = pyo.Objective(expr=0, sense=pyo.minimize) - # call k_aug get_dsdp function - # Solve the square problem - # Deactivate object and fix experimental design decisions to make square + # Fix design variables to make the problem square for comp in model.experiment_inputs: comp.fix() @@ -789,8 +790,9 @@ def initialize_jac(m, i, j): return dict_jac_initialize[(i, j)] # Otherwise initialize to 0.1 (which is an arbitrary non-zero value) else: - # Add flag as this should never be reached. - return 0.1 + raise AttributeError( + "Jacobian being initialized when the jac_initial attribute is None. Please contact the developers as you should not see this error." + ) model.sensitivity_jacobian = pyo.Var( model.output_names, model.parameter_names, initialize=initialize_jac From b2899fb6fae26b948ed97978c4259c0c266d520d Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 09:52:49 -0400 Subject: [PATCH 2252/3044] Fixed out of scope 'model' references --- pyomo/contrib/doe/doe.py | 50 ++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index bc1157e1564..eabfd3302d0 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -845,7 +845,7 @@ def jacobian_rule(m, n, p): """ fd_step_mult = 1 cuid = pyo.ComponentUID(n) - param_ind = model.parameter_names.data().index(p) + param_ind = m.parameter_names.data().index(p) # Different FD schemes lead to different scenarios for the computation if self.fd_formula == FiniteDifferenceStep.central: @@ -862,11 +862,9 @@ def jacobian_rule(m, n, p): var_up = cuid.find_component_on(m.scenario_blocks[s1]) var_lo = cuid.find_component_on(m.scenario_blocks[s2]) - param = model.parameter_scenarios[max(s1, s2)] - param_loc = pyo.ComponentUID(param).find_component_on( - model.scenario_blocks[0] - ) - param_val = model.scenario_blocks[0].unknown_parameters[param_loc] + param = m.parameter_scenarios[max(s1, s2)] + param_loc = pyo.ComponentUID(param).find_component_on(m.scenario_blocks[0]) + param_val = m.scenario_blocks[0].unknown_parameters[param_loc] param_diff = param_val * fd_step_mult * self.step if self.scale_nominal_param_value: @@ -905,8 +903,8 @@ def fim_rule(m, p, q): p: unknown parameter q: unknown parameter """ - p_ind = list(model.parameter_names).index(p) - q_ind = list(model.parameter_names).index(q) + p_ind = list(m.parameter_names).index(p) + q_ind = list(m.parameter_names).index(q) # If the row is less than the column, skip the constraint # This logic is consistent with making the FIM a lower @@ -921,14 +919,12 @@ def fim_rule(m, p, q): m.fim[p, q] == sum( 1 - / model.scenario_blocks[0].measurement_error[ - pyo.ComponentUID(n).find_component_on( - model.scenario_blocks[0] - ) + / m.scenario_blocks[0].measurement_error[ + pyo.ComponentUID(n).find_component_on(m.scenario_blocks[0]) ] * m.sensitivity_jacobian[n, p] * m.sensitivity_jacobian[n, q] - for n in model.output_names + for n in m.output_names ) + m.prior_FIM[p, q] ) @@ -1057,7 +1053,8 @@ def _generate_scenario_blocks(self, model=None): # Generate blocks for finite difference scenarios def build_block_scenarios(b, s): # Generate model for the finite difference scenario - b.transfer_attributes_from(model.base_model.clone()) + m = b.model() + b.transfer_attributes_from(m.base_model.clone()) # Forward/Backward difference have a stationary case (s == 0), no parameter to perturb if self.fd_formula in [ @@ -1067,7 +1064,7 @@ def build_block_scenarios(b, s): if s == 0: return - param = model.parameter_scenarios[s] + param = m.parameter_scenarios[s] # Perturbation to be (1 + diff) * param_value if self.fd_formula == FiniteDifferenceStep.central: @@ -1084,9 +1081,9 @@ def build_block_scenarios(b, s): pass # Update parameter values for the given finite difference scenario - pyo.ComponentUID(param, context=model.base_model).find_component_on( + pyo.ComponentUID(param, context=m.base_model).find_component_on( b - ).set_value(model.base_model.unknown_parameters[param] * (1 + diff)) + ).set_value(m.base_model.unknown_parameters[param] * (1 + diff)) model.scenario_blocks = pyo.Block(model.scenarios, rule=build_block_scenarios) @@ -1103,8 +1100,8 @@ def global_design_fixing(m, s): if s == 0: return pyo.Constraint.Skip block_design_var = pyo.ComponentUID( - d, context=model.scenario_blocks[0] - ).find_component_on(model.scenario_blocks[s]) + d, context=m.scenario_blocks[0] + ).find_component_on(m.scenario_blocks[s]) return d == block_design_var model.add_component( @@ -1184,20 +1181,19 @@ def create_objective_function(self, model=None): for j, d in enumerate(model.parameter_names): model.L[c, d].value = L[i, j] - def cholesky_imp(m, c, d): + def cholesky_imp(b, c, d): """ Calculate Cholesky L matrix using algebraic constraints """ # If the row is greater than or equal to the column, we are in the # lower triangle region of the L and FIM matrices. # This region is where our equations are well-defined. - if list(model.parameter_names).index(c) >= list( - model.parameter_names - ).index(d): - return model.fim[c, d] == sum( - model.L[c, model.parameter_names.at(k + 1)] - * model.L[d, model.parameter_names.at(k + 1)] - for k in range(list(model.parameter_names).index(d) + 1) + m = b.model() + if list(m.parameter_names).index(c) >= list(m.parameter_names).index(d): + return m.fim[c, d] == sum( + m.L[c, m.parameter_names.at(k + 1)] + * m.L[d, m.parameter_names.at(k + 1)] + for k in range(list(m.parameter_names).index(d) + 1) ) else: # This is the empty half of L above the diagonal From 137572a0122f54b235fd32f11ac7e32ae3a56974 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 09:57:41 -0400 Subject: [PATCH 2253/3044] Fixed more out of scope `model` references --- pyomo/contrib/doe/doe.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index eabfd3302d0..e3f5a802c11 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1199,18 +1199,20 @@ def cholesky_imp(b, c, d): # This is the empty half of L above the diagonal return pyo.Constraint.Skip - def trace_calc(m): + def trace_calc(b): """ Calculate FIM elements. Can scale each element with 1000 for performance """ - return model.trace == sum(model.fim[j, j] for j in model.parameter_names) + m = b.model() + return m.trace == sum(m.fim[j, j] for j in m.parameter_names) - def determinant_general(m): + def determinant_general(b): r"""Calculate determinant. Can be applied to FIM of any size. det(A) = \sum_{\sigma in \S_n} (sgn(\sigma) * \Prod_{i=1}^n a_{i,\sigma_i}) Use permutation() to get permutations, sgn() to get signature """ - r_list = list(range(len(model.parameter_names))) + m = b.model() + r_list = list(range(len(m.parameter_names))) # get all permutations object_p = permutations(r_list) list_p = list(object_p) @@ -1222,22 +1224,22 @@ def determinant_general(m): x_order = list_p[i] # sigma_i is the value in the i-th position after the reordering \sigma for x in range(len(x_order)): - for y, element in enumerate(model.parameter_names): + for y, element in enumerate(m.parameter_names): if x_order[x] == y: name_order.append(element) # det(A) = sum_{\sigma \in \S_n} (sgn(\sigma) * \Prod_{i=1}^n a_{i,\sigma_i}) det_perm = sum( self._sgn(list_p[d]) * math.prod( - model.fim[ - model.parameter_names.at(val + 1), - model.parameter_names.at(ind + 1), + m.fim[ + m.parameter_names.at(val + 1), + m.parameter_names.at(ind + 1), ] for ind, val in enumerate(list_p[d]) ) for d in range(len(list_p)) ) - return model.determinant == det_perm + return m.determinant == det_perm if self.Cholesky_option and self.objective_option == ObjectiveLib.determinant: model.obj_cons.cholesky_cons = pyo.Constraint( From b067bd1660dc97e7bd9dcffa5754359c1044dc10 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 10:02:21 -0400 Subject: [PATCH 2254/3044] Fixed check model labels function Also edited some incorrect docstrings to be correct. --- pyomo/contrib/doe/doe.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index e3f5a802c11..d6a7dc0a2d8 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1231,10 +1231,7 @@ def determinant_general(b): det_perm = sum( self._sgn(list_p[d]) * math.prod( - m.fim[ - m.parameter_names.at(val + 1), - m.parameter_names.at(ind + 1), - ] + m.fim[m.parameter_names.at(val + 1), m.parameter_names.at(ind + 1)] for ind, val in enumerate(list_p[d]) ) for d in range(len(list_p)) @@ -1280,12 +1277,9 @@ def check_model_labels(self, model=None): Parameters ---------- - model: model for suffix checking, Default: None, (self.model) + model: model for suffix checking """ - if model is None: - model = self.model.base_model - # Check that experimental outputs exist try: outputs = [k.name for k, v in model.experiment_outputs.items()] @@ -1403,7 +1397,9 @@ def update_unknown_parameter_values(self, model=None, param_vals=None): ) # Evaluates FIM and statistics for a full factorial space (same as run_grid_search) - def compute_FIM_full_factorial(self, design_ranges=None, method="sequential"): + def compute_FIM_full_factorial( + self, model=None, design_ranges=None, method="sequential" + ): """ Will run a simulation-based full factorial exploration of the experimental input space (i.e., a ``grid search`` or @@ -1452,7 +1448,7 @@ def compute_FIM_full_factorial(self, design_ranges=None, method="sequential"): "Design ranges keys must be a subset of experimental design names." ) - # ToDo: Add more objetive types? i.e., modified-E; G-opt; V-opt; etc? + # ToDo: Add more objective types? i.e., modified-E; G-opt; V-opt; etc? # ToDo: Also, make this a result object, or more user friendly. fim_factorial_results = {k.name: [] for k, v in model.experiment_inputs.items()} fim_factorial_results.update( From f71d4c7e7e504f9a62dc27d2fb1a12abfa24f900 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 10:03:55 -0400 Subject: [PATCH 2255/3044] Added missing docstring inputs --- pyomo/contrib/doe/doe.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index d6a7dc0a2d8..2fbc5fccd77 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1722,6 +1722,7 @@ def _curve1D( In a 1D sensitivity curve, it is the design variable by which the curve is drawn. font_axes: axes label font size font_tick: tick label font size + figure_file_name: string or Path, path to save the figure as log_scale: if True, the result matrix will be scaled by log10 Returns @@ -1856,6 +1857,7 @@ def _heatmap( In a 2D heatmap, it should be the first design variable in the dv_ranges font_axes: axes label font size font_tick: tick label font size + figure_file_name: string or Path, path to save the figure as log_scale: if True, the result matrix will be scaled by log10 Returns From 08e66187f4eb55661b9b6e7ca663a530c6b09dd4 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 10:08:11 -0400 Subject: [PATCH 2256/3044] Added TODO for solve failure message on user model --- pyomo/contrib/doe/doe.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 2fbc5fccd77..c721299f2be 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -553,6 +553,8 @@ def _sequential_FIM(self, model=None): res = self.solver.solve(model) pyo.assert_optimal_termination(res) except: + # TODO: Make error message more verbose, i.e., add unknown parameter values so the + # user can try to solve the model instance outside of the pyomo.DoE framework. raise RuntimeError( "Model from experiment did not solve appropriately. Make sure the model is well-posed." ) From 3f6bc13fdb6e9f1f772ca6401b59ab4e08be180d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 08:22:45 -0600 Subject: [PATCH 2257/3044] Add to_bounded_expression() to LinearMatrixConstraint --- pyomo/repn/beta/matrix.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyomo/repn/beta/matrix.py b/pyomo/repn/beta/matrix.py index 0201c46eb18..992e1810fec 100644 --- a/pyomo/repn/beta/matrix.py +++ b/pyomo/repn/beta/matrix.py @@ -587,6 +587,11 @@ def constant(self): # Abstract Interface (ConstraintData) # + def to_bounded_expression(self, evaluate_bounds=False): + """Access this constraint as a single expression.""" + # Note that the bounds are always going to be floats... + return self.lower, self.body, self.upper + @property def body(self): """Access the body of a constraint expression.""" From ea13e9409266072f4dd151b189c9abdf301e229d Mon Sep 17 00:00:00 2001 From: jlgearh Date: Tue, 13 Aug 2024 09:09:49 -0600 Subject: [PATCH 2258/3044] - Removed the variables argument from the lp_enum scripts since we only allow these methods to be applied for all variables. I will add this as a potential improvement for a future release but we need to work out the theory first. - Made zero-tolerance an argument in lp_enum.py for consistency --- .../contrib/alternative_solutions/lp_enum.py | 20 +++++-------------- .../alternative_solutions/lp_enum_solnpool.py | 7 +------ 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index e3c4e55033c..7cb7b5eaabe 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -27,9 +27,9 @@ def enumerate_linear_solutions( model, *, num_solutions=10, - variables=None, rel_opt_gap=None, abs_opt_gap=None, + zero_threshold=1e-5, search_mode="optimal", solver="gurobi", solver_options={}, @@ -52,10 +52,6 @@ def enumerate_linear_solutions( A concrete Pyomo model num_solutions : int The maximum number of solutions to generate. - variables: None or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. None indicates - that all variables will be included. Alternatively, a collection of - _GenereralVarData variables can be provided. rel_opt_gap : float or None The relative optimality gap for the original objective for which variable bounds will be found. None indicates that a relative gap @@ -64,6 +60,9 @@ def enumerate_linear_solutions( The absolute optimality gap for the original objective for which variable bounds will be found. None indicates that an absolute gap constraint will not be added to the model. + zero_threshold: float + The threshold for which a continuous variables' value is considered + to be equal to zero. search_mode : 'optimal', 'random', or 'norm' Indicates the mode that is used to generate alternative solutions. The optimal mode finds the next best solution. The random mode @@ -87,13 +86,6 @@ def enumerate_linear_solutions( """ logger.info("STARTING LP ENUMERATION ANALYSIS") - # TODO: Make this a parameter? - zero_threshold = 1e-5 - - # For now keeping things simple - # TODO: See if this can be relaxed, but for now just leave as all - assert variables == None - assert search_mode in [ "optimal", "random", @@ -106,8 +98,7 @@ def enumerate_linear_solutions( # variables doesn't really matter since we only really care about diversity # in the original problem and not in the slack space (I think) - if variables == None: - all_variables = aos_utils.get_model_variables(model) + all_variables = aos_utils.get_model_variables(model) # else: # binary_variables = ComponentSet() # non_binary_variables = [] @@ -128,7 +119,6 @@ def enumerate_linear_solutions( assert var.is_continuous(), "Model must be an LP" use_appsi = False - # TODO Check all this once implemented if "appsi" in solver: use_appsi = True opt = appsi.solvers.Gurobi() diff --git a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py index 1c69298a24b..1806b96e0ec 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py +++ b/pyomo/contrib/alternative_solutions/lp_enum_solnpool.py @@ -83,7 +83,6 @@ def cut_generator_callback(self, cb_m, cb_opt, cb_where): def enumerate_linear_solutions_soln_pool( model, num_solutions=10, - variables=None, rel_opt_gap=None, abs_opt_gap=None, zero_threshold=1e-5, @@ -133,11 +132,7 @@ def enumerate_linear_solutions_soln_pool( if not gurobi_available: raise pyomo.common.errors.ApplicationError(f"Solver (gurobi) not available") - # For now keeping things simple - # TODO: See if this can be relaxed, but for now just leave as all - assert variables == None - if variables == None: - all_variables = aos_utils.get_model_variables(model) + all_variables = aos_utils.get_model_variables(model) for var in all_variables: if var.is_integer(): raise pyomo.common.errors.ApplicationError( From fdd05835861413dfff4a619875c07b269f84c834 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Tue, 13 Aug 2024 11:52:04 -0400 Subject: [PATCH 2259/3044] Ran Black --- pyomo/contrib/doe/tests/test_doe_solve.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index eee123f436a..0c66ec0a8bf 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -194,9 +194,9 @@ def test_reactor_obj_det_solve(self): experiment = FullReactorExperiment(data_ex, 10, 3) DoE_args = get_standard_args(experiment, fd_method, obj_used) - DoE_args[ - 'scale_nominal_param_value' - ] = False # Vanilla determinant solve needs this + DoE_args['scale_nominal_param_value'] = ( + False # Vanilla determinant solve needs this + ) DoE_args['_Cholesky_option'] = False DoE_args['_only_compute_fim_lower'] = False From 46e5bfb16b8ffc86c19be2314b46233e64c6f0d1 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 13 Aug 2024 09:52:36 -0600 Subject: [PATCH 2260/3044] Adding linear-tree to optional dependencies --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 6d28e4d184b..4e2b8bd6042 100644 --- a/setup.py +++ b/setup.py @@ -262,6 +262,7 @@ def __ne__(self, other): 'optional': [ 'dill', # No direct use, but improves lambda pickle 'ipython', # contrib.viewer + 'linear-tree', # contrib.piecewise # Note: matplotlib 3.6.1 has bug #24127, which breaks # seaborn's histplot (triggering parmest failures) # Note: minimum version from community_detection use of From fc5ebc86cf940caf0579577a870f34866f64d0a2 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 13 Aug 2024 09:58:56 -0600 Subject: [PATCH 2261/3044] Black being black --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4e2b8bd6042..b1f4a60c4c6 100644 --- a/setup.py +++ b/setup.py @@ -262,7 +262,7 @@ def __ne__(self, other): 'optional': [ 'dill', # No direct use, but improves lambda pickle 'ipython', # contrib.viewer - 'linear-tree', # contrib.piecewise + 'linear-tree', # contrib.piecewise # Note: matplotlib 3.6.1 has bug #24127, which breaks # seaborn's histplot (triggering parmest failures) # Note: minimum version from community_detection use of From 58b5df8d74fa254e152adf1ef4b0b8515bb0e04d Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 13 Aug 2024 10:04:00 -0600 Subject: [PATCH 2262/3044] test ipopt version before trying get_current_iterate --- .../algorithms/solvers/tests/test_cyipopt_solver.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py index 60e3a72245e..8a35449d94d 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py @@ -46,6 +46,7 @@ # We don't raise unittest.SkipTest if not cyipopt_available as there is a # test below that tests an exception when cyipopt is unavailable. cyipopt_ge_1_3 = hasattr(cyipopt, "CyIpoptEvaluationError") + ipopt_ge_3_14 = cyipopt.IPOPT_VERSION >= (3, 14, 0) def create_model1(): @@ -369,11 +370,12 @@ def intermediate( # only has access to the *previous iteration's* dual values. # The 13-arg callback works with cyipopt < 1.3, but we will use the - # get_current_iterate method, which is only available in 1.3+ + # get_current_iterate method, which is only available in 1.3+ and IPOPT 3.14+ @unittest.skipIf( - not cyipopt_available or not cyipopt_ge_1_3, "cyipopt version < 1.3.0" + not cyipopt_available or not cyipopt_ge_1_3 or not ipopt_ge_3_14, + "cyipopt version < 1.3.0", ) - def test_solve_13arg_callback(self): + def test_solve_get_current_iterate(self): m = create_model1() iterate_data = [] From 3eb40d8841637368a5cc7988925d9b9e0ee7dc4f Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 13 Aug 2024 10:15:50 -0600 Subject: [PATCH 2263/3044] more specific logging test --- pyomo/core/tests/transform/test_scaling.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyomo/core/tests/transform/test_scaling.py b/pyomo/core/tests/transform/test_scaling.py index c1c8e36904d..4609fef30f4 100644 --- a/pyomo/core/tests/transform/test_scaling.py +++ b/pyomo/core/tests/transform/test_scaling.py @@ -707,7 +707,11 @@ def test_propagate_solution_uninitialized_variable(self): pyo.TransformationFactory("core.scale_model").propagate_solution( scaled_model, m ) - self.assertIn("replacing value of variable", OUTPUT.getvalue()) + msg = ( + "Variable with value None in the scaled model is replacing value of" + " variable x[2] in the original model with None (was 1.0).\n" + ) + self.assertEqual(OUTPUT.getvalue(), msg) self.assertAlmostEqual(m.x[1].value, 2.0, delta=1e-8) # Note that value of x[2] in original model *has* been overridden to None. # In this case, a warning has been raised. From 844b09acad540ee914f866b662a2959ddfac46a7 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 13 Aug 2024 10:16:27 -0600 Subject: [PATCH 2264/3044] Adding (failing) test for sampling discrete domains --- .../piecewise/tests/test_nonlinear_to_pwl.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index a42846c2802..1428fa4a810 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -9,7 +9,11 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from io import StringIO +import logging + from pyomo.common.dependencies import attempt_import, scipy_available, numpy_available +from pyomo.common.log import LoggingIntercept import pyomo.common.unittest as unittest from pyomo.contrib.piecewise import PiecewiseLinearFunction from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( @@ -23,9 +27,11 @@ ) from pyomo.core.expr.numeric_expr import SumExpression from pyomo.environ import ( + Binary, ConcreteModel, Var, Constraint, + Integers, TransformationFactory, log, Objective, @@ -303,6 +309,24 @@ def test_do_not_additively_decompose_below_min_dimension(self): # This is only approximated by one pwlf: self.assertIsInstance(transformed_c.body, _ExpressionData) + @unittest.skipUnless(numpy_available, "Numpy is not available") + def test_uniform_sampling_discrete_vars(self): + m = ConcreteModel() + m.x = Var(['rocky', 'bullwinkle'], domain=Binary) + m.y = Var(domain=Integers, bounds=(0, 5)) + m.c = Constraint(expr=m.x['rocky'] * m.x['bullwinkle'] + m.y <= 4) + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + output = StringIO() + with LoggingIntercept(output, 'pyomo.contrib.piecewise', logging.WARNING): + n_to_pwl.apply_to( + m, + num_points=3, + additively_decompose=False, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + ) + self.assertEqual(output.getvalue().strip()) + class TestNonlinearToPWL_2D(unittest.TestCase): def make_paraboloid_model(self): From 11326eb3fb73c43514c8be73b45f7a923dd46a23 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 13 Aug 2024 10:23:21 -0600 Subject: [PATCH 2265/3044] Only use linear-tree for pypi, it appears to not be on conda --- .github/workflows/test_pr_and_main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index cc9760cbe5d..765e50826d0 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -29,7 +29,7 @@ defaults: env: PYTHONWARNINGS: ignore::UserWarning PYTHON_CORE_PKGS: wheel - PYPI_ONLY: z3-solver + PYPI_ONLY: z3-solver linear-tree PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels CACHE_VER: v221013.1 NEOS_EMAIL: tests@pyomo.org From 1b9729a0bebc6944a8fb1b9060aad944b723b482 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Tue, 13 Aug 2024 12:32:58 -0400 Subject: [PATCH 2266/3044] Added more verbose TODO tasks --- pyomo/contrib/doe/doe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index c721299f2be..47992df71c0 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1551,10 +1551,10 @@ def compute_FIM_full_factorial( self.fim_factorial_results = fim_factorial_results - # ToDo: add automated figure drawing as it was before (perhaps reuse the code) return self.fim_factorial_results - # Plotting + # TODO: Overhaul plotting functions to not use strings + # TODO: Make the plotting functionalities work for >2 design features def draw_factorial_figure( self, results=None, From 57a8a81e9b865b3eb2b86465a4304e6ad9190535 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:17:09 -0400 Subject: [PATCH 2267/3044] Delete .vs directory Remove file that was accidentally added. --- .vs/CMakeWorkspaceSettings.json | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .vs/CMakeWorkspaceSettings.json diff --git a/.vs/CMakeWorkspaceSettings.json b/.vs/CMakeWorkspaceSettings.json deleted file mode 100644 index d3e1057f48a..00000000000 --- a/.vs/CMakeWorkspaceSettings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "enableCMake": false -} \ No newline at end of file From 0b08aefab711382c00a57bf67e30eb62c37b62e3 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 13:22:19 -0400 Subject: [PATCH 2268/3044] Remove other user-defined model unique naming Added to TODO list in #3345 --- pyomo/contrib/doe/doe.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 47992df71c0..005d9c0da52 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -433,10 +433,12 @@ def compute_FIM(self, model=None, method="sequential"): ).clone() model = self.compute_FIM_model else: - doe_block = pyo.Block() - doe_block_name = unique_component_name(model, "design_of_experiments_block") - model.add_component(doe_block_name, doe_block) - self.compute_FIM_model = model + # TODO: Add safe naming when a model is passed by the user. + # doe_block = pyo.Block() + # doe_block_name = unique_component_name(model, "design_of_experiments_block") + # model.add_component(doe_block_name, doe_block) + # self.compute_FIM_model = model + pass self.check_model_labels(model=model) @@ -731,9 +733,11 @@ def create_doe_model(self, model=None): if model is None: model = self.model else: - doe_block = pyo.Block() - doe_block_name = unique_component_name(model, "design_of_experiments_block") - model.add_component(doe_block_name, doe_block) + # TODO: Add safe naming when a model is passed by the user. + # doe_block = pyo.Block() + # doe_block_name = unique_component_name(model, "design_of_experiments_block") + # model.add_component(doe_block_name, doe_block) + pass # Developer recommendation: use the Cholesky decomposition for D-optimality # The explicit formula is available for benchmarking purposes and is NOT recommended From 7aab583f4809f4b83f94f4043ee353e6678c88d7 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 13:24:38 -0400 Subject: [PATCH 2269/3044] Revert accidental find and replace in tests --- pyomo/contrib/doe/tests/test_doe_solve.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 0c66ec0a8bf..61a7e3eafcc 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -132,10 +132,10 @@ def test_reactor_fd_central_solve(self): doe_obj.run_doe() - # self.assertTrue(model solves + # assert model solves self.assertTrue(doe_obj.results["Solver Status"] == "ok") - # self.assertTrue(that Q, F, and L are the same. + # assert that Q, F, and L are the same. FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) # Since Trace is used, no comparison for FIM and L.T @ L @@ -157,7 +157,7 @@ def test_reactor_fd_forward_solve(self): self.assertTrue(doe_obj.results["Solver Status"] == "ok") - # self.assertTrue(that Q, F, and L are the same. + # assert that Q, F, and L are the same. FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) # Since Trace is used, no comparison for FIM and L.T @ L @@ -179,7 +179,7 @@ def test_reactor_fd_backward_solve(self): self.assertTrue(doe_obj.results["Solver Status"] == "ok") - # self.assertTrue(that Q, F, and L are the same. + # assert that Q, F, and L are the same. FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) # Since Trace is used, no comparison for FIM and L.T @ L @@ -220,7 +220,7 @@ def test_reactor_obj_cholesky_solve(self): self.assertTrue(doe_obj.results["Solver Status"] == "ok") - # self.assertTrue(that Q, F, and L are the same. + # assert that Q, F, and L are the same. FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) # Since Cholesky is used, there is comparison for FIM and L.T @ L @@ -302,11 +302,11 @@ def test_reactor_grid_search(self): CA_vals = doe_obj.fim_factorial_results["CA[0]"] T_vals = doe_obj.fim_factorial_results["T[0]"] - # self.assertTrue(length is correct + # assert length is correct self.assertTrue((len(CA_vals) == 9) and (len(T_vals) == 9)) self.assertTrue((len(set(CA_vals)) == 3) and (len(set(T_vals)) == 3)) - # self.assertTrue(unique values are correct + # assert unique values are correct self.assertTrue( (set(CA_vals).issuperset(set([1, 3, 5]))) and (set(T_vals).issuperset(set([300, 500, 700]))) @@ -391,11 +391,11 @@ def test_reactor_grid_search_bad_model(self): CA_vals = doe_obj.fim_factorial_results["CA[0]"] T_vals = doe_obj.fim_factorial_results["T[0]"] - # self.assertTrue(length is correct + # assert length is correct self.assertTrue((len(CA_vals) == 9) and (len(T_vals) == 9)) self.assertTrue((len(set(CA_vals)) == 3) and (len(set(T_vals)) == 3)) - # self.assertTrue(unique values are correct + # assert unique values are correct self.assertTrue( (set(CA_vals).issuperset(set([1, 3, 5]))) and (set(T_vals).issuperset(set([300, 500, 700]))) From b1214a7185194725efb92628ac5ac0c5b3940174 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 13:28:31 -0400 Subject: [PATCH 2270/3044] Updated documentation comment to be more verbose --- doc/OnlineDocs/contributed_packages/doe/doe.rst | 4 ++-- pyomo/contrib/doe/examples/reactor_experiment.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/doe/doe.rst b/doc/OnlineDocs/contributed_packages/doe/doe.rst index 49931592ad1..73aa160c130 100644 --- a/doc/OnlineDocs/contributed_packages/doe/doe.rst +++ b/doc/OnlineDocs/contributed_packages/doe/doe.rst @@ -174,7 +174,7 @@ The process model for the reaction kinetics problem is shown below. We build the .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py :start-after: Create flexible model without data - :end-before: End equation def'n + :end-before: End equation definition Step 2: Finalize the Pyomo process model ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -182,7 +182,7 @@ Step 2: Finalize the Pyomo process model Here we add data to the model and finalize the discretization. This step is required before the model can be labeled. .. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :start-after: End equation def'n + :start-after: End equation definition :end-before: End model finalization Step 3: Label the information needed for DoE analysis diff --git a/pyomo/contrib/doe/examples/reactor_experiment.py b/pyomo/contrib/doe/examples/reactor_experiment.py index 37e97250a7e..c94b89b026c 100644 --- a/pyomo/contrib/doe/examples/reactor_experiment.py +++ b/pyomo/contrib/doe/examples/reactor_experiment.py @@ -82,7 +82,7 @@ def create_model(self): ######################## # End variable def. - # Equation def'n + # Equation definition ######################## # Expression for rate constants @@ -110,7 +110,7 @@ def CC_balance(m, t): return m.CA[0] == m.CA[t] + m.CB[t] + m.CC[t] ######################## - # End equation def'n + # End equation definition def finalize_model(self): """ From 6db5023299e9385c8cc0c2ab7eb08101dc6b9b1c Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 13:32:09 -0400 Subject: [PATCH 2271/3044] Change assertTrue to assertEqual in test build --- pyomo/contrib/doe/tests/test_doe_build.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 12c33df64a8..6f0d1f3737d 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -395,7 +395,7 @@ def test_get_experiment_inputs_without_blocks(self): count = 0 for k, v in doe_obj.compute_FIM_model.experiment_inputs.items(): - self.assertTrue(pyo.value(k) == stuff[count]) + self.assertEqual(pyo.value(k), stuff[count]) count += 1 def test_get_experiment_outputs_without_blocks(self): @@ -414,7 +414,7 @@ def test_get_experiment_outputs_without_blocks(self): count = 0 for k, v in doe_obj.compute_FIM_model.experiment_outputs.items(): - self.assertTrue(pyo.value(k) == stuff[count]) + self.assertEqual(pyo.value(k), stuff[count]) count += 1 def test_get_measurement_error_without_blocks(self): @@ -433,7 +433,7 @@ def test_get_measurement_error_without_blocks(self): count = 0 for k, v in doe_obj.compute_FIM_model.measurement_error.items(): - self.assertTrue(pyo.value(k) == stuff[count]) + self.assertEqual(pyo.value(k), stuff[count]) count += 1 def test_get_unknown_parameters_without_blocks(self): @@ -453,7 +453,7 @@ def test_get_unknown_parameters_without_blocks(self): count = 0 for k, v in doe_obj.compute_FIM_model.unknown_parameters.items(): - self.assertTrue(pyo.value(k) == stuff[count]) + self.assertEqual(pyo.value(k), stuff[count]) count += 1 def test_generate_blocks_without_model(self): From 0c566c9e78ae057decacbb452d432e01a6bc8ddc Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 13:37:18 -0400 Subject: [PATCH 2272/3044] Change assertTrue to assertEqual in test solve --- pyomo/contrib/doe/tests/test_doe_solve.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 61a7e3eafcc..bf63862b345 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -133,7 +133,7 @@ def test_reactor_fd_central_solve(self): doe_obj.run_doe() # assert model solves - self.assertTrue(doe_obj.results["Solver Status"] == "ok") + self.assertEqual(doe_obj.results["Solver Status"], "ok") # assert that Q, F, and L are the same. FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) @@ -155,7 +155,7 @@ def test_reactor_fd_forward_solve(self): doe_obj.run_doe() - self.assertTrue(doe_obj.results["Solver Status"] == "ok") + self.assertEqual(doe_obj.results["Solver Status"], "ok") # assert that Q, F, and L are the same. FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) @@ -177,7 +177,7 @@ def test_reactor_fd_backward_solve(self): doe_obj.run_doe() - self.assertTrue(doe_obj.results["Solver Status"] == "ok") + self.assertEqual(doe_obj.results["Solver Status"], "ok") # assert that Q, F, and L are the same. FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) @@ -204,7 +204,7 @@ def test_reactor_obj_det_solve(self): doe_obj.run_doe() - self.assertTrue(doe_obj.results['Solver Status'] == "ok") + self.assertEqual(doe_obj.results['Solver Status'], "ok") def test_reactor_obj_cholesky_solve(self): fd_method = "central" @@ -218,7 +218,7 @@ def test_reactor_obj_cholesky_solve(self): doe_obj.run_doe() - self.assertTrue(doe_obj.results["Solver Status"] == "ok") + self.assertEqual(doe_obj.results["Solver Status"], "ok") # assert that Q, F, and L are the same. FIM, Q, L, sigma_inv = get_FIM_Q_L(doe_obj=doe_obj) From 3c233e13a2a20db4b5fece30792fe3d82b536498 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 13 Aug 2024 12:09:42 -0600 Subject: [PATCH 2273/3044] Whoops, actually intercepting the logging stream I want to make sure is empty... --- pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 1428fa4a810..76246688cdf 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -318,14 +318,14 @@ def test_uniform_sampling_discrete_vars(self): n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') output = StringIO() - with LoggingIntercept(output, 'pyomo.contrib.piecewise', logging.WARNING): + with LoggingIntercept(output, 'pyomo.core', logging.WARNING): n_to_pwl.apply_to( m, num_points=3, additively_decompose=False, domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, ) - self.assertEqual(output.getvalue().strip()) + self.assertEqual(output.getvalue().strip(), "") class TestNonlinearToPWL_2D(unittest.TestCase): From eb6b8986f7d9635aaa2bbdc79e0f4326e878090d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 12:28:43 -0600 Subject: [PATCH 2274/3044] Remove the `_suppress_ctypes` attribute from Block --- pyomo/contrib/cp/interval_var.py | 2 -- pyomo/contrib/piecewise/piecewise_linear_function.py | 2 -- pyomo/core/base/block.py | 12 ------------ pyomo/gdp/disjunct.py | 6 ------ 4 files changed, 22 deletions(-) diff --git a/pyomo/contrib/cp/interval_var.py b/pyomo/contrib/cp/interval_var.py index dec5af74d9f..013fa145b15 100644 --- a/pyomo/contrib/cp/interval_var.py +++ b/pyomo/contrib/cp/interval_var.py @@ -206,8 +206,6 @@ def _getitem_when_not_present(self, index): class ScalarIntervalVar(IntervalVarData, IntervalVar): def __init__(self, *args, **kwds): - self._suppress_ctypes = set() - IntervalVarData.__init__(self, self) IntervalVar.__init__(self, *args, **kwds) self._data[None] = self diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index e92edacc756..742eb52d5bb 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -506,8 +506,6 @@ class ScalarPiecewiseLinearFunction( PiecewiseLinearFunctionData, PiecewiseLinearFunction ): def __init__(self, *args, **kwds): - self._suppress_ctypes = set() - PiecewiseLinearFunctionData.__init__(self, self) PiecewiseLinearFunction.__init__(self, *args, **kwds) self._data[None] = self diff --git a/pyomo/core/base/block.py b/pyomo/core/base/block.py index 653809e0419..526c4c8bb41 100644 --- a/pyomo/core/base/block.py +++ b/pyomo/core/base/block.py @@ -960,13 +960,8 @@ def add_component(self, name, val): % (name, type(val), self.name, type(getattr(self, name))) ) # - # Skip the add_component() logic if this is a - # component type that is suppressed. - # _component = self.parent_component() _type = val.ctype - if _type in _component._suppress_ctypes: - return # # Raise an exception if the component already has a parent. # @@ -1048,12 +1043,6 @@ def add_component(self, name, val): else: self._ctypes[_type] = [_new_idx, _new_idx, 1] # - # Propagate properties to sub-blocks: - # suppressed ctypes - # - if _type is Block: - val._suppress_ctypes |= _component._suppress_ctypes - # # Error, for disabled support implicit rule names # if '_rule' in val.__dict__ and val._rule is None: @@ -2029,7 +2018,6 @@ def __init__( def __init__(self, *args, **kwargs): """Constructor""" - self._suppress_ctypes = set() _rule = kwargs.pop('rule', None) _options = kwargs.pop('options', None) # As concrete applies to the Block at declaration time, we will diff --git a/pyomo/gdp/disjunct.py b/pyomo/gdp/disjunct.py index 637f55cbed1..bfaada8f3de 100644 --- a/pyomo/gdp/disjunct.py +++ b/pyomo/gdp/disjunct.py @@ -502,12 +502,6 @@ def _activate_without_unfixing_indicator(self): class ScalarDisjunct(DisjunctData, Disjunct): def __init__(self, *args, **kwds): - ## FIXME: This is a HACK to get around a chicken-and-egg issue - ## where BlockData creates the indicator_var *before* - ## Block.__init__ declares the _defer_construction flag. - self._defer_construction = True - self._suppress_ctypes = set() - DisjunctData.__init__(self, self) Disjunct.__init__(self, *args, **kwds) self._data[None] = self From 485e0fcc79f73778774e3ad15de0666737d4f0fb Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 15:11:12 -0400 Subject: [PATCH 2275/3044] Update test file with os.path and pyomo.fileutils --- pyomo/contrib/doe/tests/test_doe_build.py | 7 ++++--- pyomo/contrib/doe/tests/test_doe_errors.py | 7 ++++--- pyomo/contrib/doe/tests/test_doe_solve.py | 7 ++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_build.py b/pyomo/contrib/doe/tests/test_doe_build.py index 6f0d1f3737d..074ab58391c 100644 --- a/pyomo/contrib/doe/tests/test_doe_build.py +++ b/pyomo/contrib/doe/tests/test_doe_build.py @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ import json -from pathlib import Path +import os.path from pyomo.common.dependencies import ( numpy as np, @@ -17,6 +17,7 @@ pandas as pd, pandas_available, ) +from pyomo.common.fileutils import this_file_dir import pyomo.common.unittest as unittest from pyomo.contrib.doe import DesignOfExperiments @@ -30,8 +31,8 @@ ipopt_available = SolverFactory("ipopt").available() -DATA_DIR = Path(__file__).parent -file_path = DATA_DIR / ".." / "examples" / "result.json" +currdir = this_file_dir() +file_path = os.path.join(currdir, "..", "examples", "result.json") with open(file_path) as f: data_ex = json.load(f) diff --git a/pyomo/contrib/doe/tests/test_doe_errors.py b/pyomo/contrib/doe/tests/test_doe_errors.py index 40cc2ba10e5..52f6ef8cdb8 100644 --- a/pyomo/contrib/doe/tests/test_doe_errors.py +++ b/pyomo/contrib/doe/tests/test_doe_errors.py @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ import json -from pathlib import Path +import os.path from pyomo.common.dependencies import ( numpy as np, @@ -17,6 +17,7 @@ pandas as pd, pandas_available, ) +from pyomo.common.fileutils import this_file_dir import pyomo.common.unittest as unittest from pyomo.contrib.doe import DesignOfExperiments @@ -29,8 +30,8 @@ ipopt_available = SolverFactory("ipopt").available() -DATA_DIR = Path(__file__).parent -file_path = DATA_DIR / ".." / "examples" / "result.json" +currdir = this_file_dir() +file_path = os.path.join(currdir, "..", "examples", "result.json") with open(file_path) as f: data_ex = json.load(f) diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index bf63862b345..0bc5d7254c9 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ import json import logging -from pathlib import Path +import os.path from pyomo.common.dependencies import ( numpy as np, @@ -19,6 +19,7 @@ pandas_available, scipy_available, ) +from pyomo.common.fileutils import this_file_dir import pyomo.common.unittest as unittest from pyomo.contrib.doe import DesignOfExperiments @@ -38,8 +39,8 @@ ipopt_available = SolverFactory("ipopt").available() k_aug_available = SolverFactory('k_aug', solver_io='nl', validate=False) -DATA_DIR = Path(__file__).parent -file_path = DATA_DIR / ".." / "examples" / "result.json" +currdir = this_file_dir() +file_path = os.path.join(currdir, "..", "examples", "result.json") with open(file_path) as f: data_ex = json.load(f) From c10761612ccddc2d6c469fcec2cd9e34a9d468ea Mon Sep 17 00:00:00 2001 From: whart222 Date: Tue, 13 Aug 2024 13:18:04 -0600 Subject: [PATCH 2276/3044] Fixing typos --- doc/OnlineDocs/contributed_packages/alternative_solutions.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst index f3594760c73..cc5ab07c3cc 100644 --- a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst +++ b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst @@ -3,7 +3,7 @@ Generating Alternative (Near-)Optimal Solutions ############################################### Optimization solvers are generally designed to return a feasible solution -to the user. However, there are many applications where a users needs +to the user. However, there are many applications where a user needs more context than this result. For example, * alternative solutions can support an assessment of trade-offs between competing objectives; @@ -35,7 +35,7 @@ The following functions are defined in the alternative-solutions library: * ``gurobi_generate_solutions`` - * Finds alternative optimal solutions for discrete variables using Gurobi's built-in Solution Pool capability. + * Finds alternative optimal solutions for discrete variables using Gurobi's built-in solution pool capability. * ``obbt_analysis_bounds_and_solutions`` From df7f2b270be51d0b1e952fccbc88e1178bf2fe45 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 13:53:27 -0600 Subject: [PATCH 2277/3044] Clarify deprecation message --- pyomo/core/base/set.py | 4 ++-- pyomo/core/tests/unit/test_set.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 7e4744bb9d1..dad2feb25dc 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1485,7 +1485,7 @@ def _cb_validate_filter(self, mode, val_iter): flag = fcn(block, (), *vstar) if flag: deprecation_warning( - f"{self.__class__.__name__} {self.name}: {mode} " + f"{self.__class__.__name__} {self.name}: '{mode}=' " "callback signature matched (block, *value). " "Please update the callback to match the signature " "(block, value, *index).", @@ -1508,7 +1508,7 @@ def _cb_validate_filter(self, mode, val_iter): flag = fcn(block, idx, *value) if flag: deprecation_warning( - f"{self.__class__.__name__} {self.name}: {mode} " + f"{self.__class__.__name__} {self.name}: '{mode}=' " "callback signature matched (block, *value, *index). " "Please update the callback to match the signature " "(block, value, *index).", diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index a08202d7c50..b9030bb90f4 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4367,7 +4367,7 @@ def _validate(model, i, j): self.assertTrue(m.J1[2, 2].add((0, 1))) self.assertRegex( OUT.getvalue().replace('\n', ' '), - r"DEPRECATED: InsertionOrderSetData J1\[2,2\]: validate callback " + r"DEPRECATED: InsertionOrderSetData J1\[2,2\]: 'validate=' callback " r"signature matched \(block, \*value\). Please update the " r"callback to match the signature \(block, value, \*index\)", ) @@ -4397,7 +4397,7 @@ def _validate(model, i, j, ind1, ind2): self.assertTrue(m.J2[2, 2].add((0, 1))) self.assertRegex( OUT.getvalue().replace('\n', ' '), - r"DEPRECATED: InsertionOrderSetData J2\[2,2\]: validate callback " + r"DEPRECATED: InsertionOrderSetData J2\[2,2\]: 'validate=' callback " r"signature matched \(block, \*value, \*index\). Please update the " r"callback to match the signature \(block, value, \*index\)", ) From 2d47311720ace0a9c16709e0b650c6d5b5ac07d3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 13:54:25 -0600 Subject: [PATCH 2278/3044] Deprecation warning for scalar sets with filter/validate callbacks expecting expanded values --- pyomo/core/base/set.py | 22 +-------------- pyomo/core/tests/unit/test_set.py | 47 ++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index dad2feb25dc..964e83a4bba 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1488,7 +1488,7 @@ def _cb_validate_filter(self, mode, val_iter): f"{self.__class__.__name__} {self.name}: '{mode}=' " "callback signature matched (block, *value). " "Please update the callback to match the signature " - "(block, value, *index).", + f"(block, value{', *index' if comp.is_indexed() else ''}).", version='6.7.4.dev0', ) orig_fcn = fcn._fcn @@ -2264,26 +2264,6 @@ def __init__(self, *args, **kwds): if self._init_dimen.constant(): self._dimen = self._init_dimen(self.parent_block(), None) - if self._validate.__class__ is ParameterizedIndexedCallInitializer: - # TBD [JDS: 8/2024]: should we deprecate the "expanded - # tuple" version of the validate callback for scalar sets? - # It is widely used and we can (reasonably reliably) map to - # the expected behavior... - orig_fcn = self._validate._fcn - self._validate = ParameterizedScalarCallInitializer( - lambda m, v: orig_fcn(m, *v), True - ) - - if self._filter.__class__ is ParameterizedIndexedCallInitializer: - # TBD [JDS: 8/2024]: should we deprecate the "expanded - # tuple" version of the filter callback for scalar sets? - # It is widely used and we can (reasonably reliably) map to - # the expected behavior... - orig_fcn = self._filter._fcn - self._filter = ParameterizedScalarCallInitializer( - lambda m, v: orig_fcn(m, *v), True - ) - @deprecated( "check_values() is deprecated: Sets only contain valid members", version='5.7' ) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index b9030bb90f4..6529c2b60a9 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4336,7 +4336,8 @@ def _lt_3(model, i): m = ConcreteModel() - def _validate(model, i, j): + def _validate(model, val): + i, j = val self.assertIs(model, m) if i + j < 2: return True @@ -4344,22 +4345,54 @@ def _validate(model, i, j): return False raise RuntimeError("Bogus value") - m.I = Set(validate=_validate) + m.I1 = Set(validate=_validate) output = StringIO() with LoggingIntercept(output, 'pyomo.core'): - self.assertTrue(m.I.add((0, 1))) + self.assertTrue(m.I1.add((0, 1))) self.assertEqual(output.getvalue(), "") with self.assertRaisesRegex( ValueError, - r"The value=\(4, 1\) violates the validation rule of " r"Set I", + r"The value=\(4, 1\) violates the validation rule of " r"Set I1", ): - m.I.add((4, 1)) + m.I1.add((4, 1)) self.assertEqual(output.getvalue(), "") with self.assertRaisesRegex(RuntimeError, "Bogus value"): - m.I.add((2, 2)) + m.I1.add((2, 2)) + self.assertEqual( + output.getvalue(), + "Exception raised while validating element '(2, 2)' for Set I1\n", + ) + + def _validate(model, i, j): + self.assertIs(model, m) + if i + j < 2: + return True + if i - j > 2: + return False + raise RuntimeError("Bogus value") + + m.I2 = Set(validate=_validate) + with LoggingIntercept(module='pyomo.core') as output: + self.assertTrue(m.I2.add((0, 1))) + self.assertRegex( + output.getvalue().replace('\n', ' '), + r"DEPRECATED: OrderedScalarSet I2: 'validate=' callback " + r"signature matched \(block, \*value\). Please update the " + r"callback to match the signature \(block, value\)", + ) + with LoggingIntercept(module='pyomo.core') as output: + with self.assertRaisesRegex( + ValueError, + r"The value=\(4, 1\) violates the validation rule of " r"Set I2", + ): + m.I2.add((4, 1)) + self.assertEqual(output.getvalue(), "") + with LoggingIntercept(module='pyomo.core') as output: + with self.assertRaisesRegex(RuntimeError, "Bogus value"): + m.I2.add((2, 2)) self.assertEqual( output.getvalue(), - "Exception raised while validating element '(2, 2)' for Set I\n", + "Exception raised while validating element '(2, 2)' for Set I2\n", ) m.J1 = Set([(0, 0), (2, 2)], validate=_validate) From 919f234017c40b218f8059b42bc0deb461674d22 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 13 Aug 2024 17:01:25 -0400 Subject: [PATCH 2279/3044] Moving deprecation error away from doe.py --- pyomo/contrib/doe/doe.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 005d9c0da52..32036799a56 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -146,16 +146,6 @@ def __init__( logger_level: Specify the level of the logger. Change to logging.DEBUG for all messages. """ - # Deprecation error - if 'create_model' in kwargs: - raise ValueError( - "DEPRECATION ERROR: Pyomo.DoE has been refactored. The current interface utilizes Experiment " - "objects that label unknown parameters, experiment inputs, experiment outputs and measurement " - "error. This avoids string-based naming which is fragile. For instruction to use the new " - "interface, please see the Pyomo.DoE under the contributed packages documentation at " - "`https://pyomo.readthedocs.io/en/latest/contributed_packages/doe/doe.html`" - ) - if experiment is None: raise ValueError("Experiment object must be provided to perform DoE.") From 03db9e804ae6b66da1593ea31dde188579fd6603 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 16:57:51 -0600 Subject: [PATCH 2280/3044] Add context to SuffixFinder; support finding a block in its own suffixes --- pyomo/core/base/suffix.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/pyomo/core/base/suffix.py b/pyomo/core/base/suffix.py index be2f732650d..b9aa3b58ced 100644 --- a/pyomo/core/base/suffix.py +++ b/pyomo/core/base/suffix.py @@ -19,6 +19,7 @@ from pyomo.common.modeling import NOTSET from pyomo.common.pyomo_typing import overload from pyomo.common.timing import ConstructionTimer +from pyomo.core.base.block import BlockData from pyomo.core.base.component import ActiveComponent, ModelComponentFactory from pyomo.core.base.disable_methods import disable_methods from pyomo.core.base.initializer import Initializer @@ -409,7 +410,7 @@ class AbstractSuffix(Suffix): class SuffixFinder(object): - def __init__(self, name, default=None): + def __init__(self, name, default=None, context=None): """This provides an efficient utility for finding suffix values on a (hierarchical) Pyomo model. @@ -428,7 +429,14 @@ def __init__(self, name, default=None): self.name = name self.default = default self.all_suffixes = [] - self._suffixes_by_block = {None: []} + self._context = context + self._suffixes_by_block = ComponentMap() + self._suffixes_by_block[self._context] = [] + if context is not None: + s = context.component(name) + if s is not None and s.ctype is Suffix and s.active: + self._suffixes_by_block[context].append(s) + self.all_suffixes.append(s) def find(self, component_data): """Find suffix value for a given component data object in model tree @@ -458,7 +466,17 @@ def find(self, component_data): """ # Walk parent tree and search for suffixes - suffixes = self._get_suffix_list(component_data.parent_block()) + if isinstance(component_data, BlockData): + _block = component_data + else: + _block = component_data.parent_block() + try: + suffixes = self._get_suffix_list(_block) + except AttributeError: + raise ValueError( + f"Component '{component_data.name}' not found in the SuffixFinder " + f"context (Block hierarchy rooted at {self._context.name})" + ) from None # Pass 1: look for the component_data, working root to leaf for s in suffixes: if component_data in s: From 89994ae7c09e127a726971131b8d148f180d925e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 16:59:22 -0600 Subject: [PATCH 2281/3044] Test SuffixFinder context --- pyomo/core/tests/unit/test_suffix.py | 72 +++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/pyomo/core/tests/unit/test_suffix.py b/pyomo/core/tests/unit/test_suffix.py index d2e861cceb5..1278601bb3b 100644 --- a/pyomo/core/tests/unit/test_suffix.py +++ b/pyomo/core/tests/unit/test_suffix.py @@ -1795,47 +1795,97 @@ def test_suffix_finder(self): m.b1.b2 = Block() m.b1.b2.v3 = Var([0]) - _suffix_finder = SuffixFinder('suffix') - # Add Suffixes m.suffix = Suffix(direction=Suffix.EXPORT) # No suffix on b1 - make sure we can handle missing suffixes m.b1.b2.suffix = Suffix(direction=Suffix.EXPORT) + _suffix_finder = SuffixFinder('suffix') + _suffix_b1_finder = SuffixFinder('suffix', context=m.b1) + _suffix_b2_finder = SuffixFinder('suffix', context=m.b1.b2) + # Check for no suffix value - assert _suffix_finder.find(m.b1.b2.v3[0]) == None + self.assertEqual(_suffix_finder.find(m.b1.b2.v3[0]), None) + self.assertEqual(_suffix_b1_finder.find(m.b1.b2.v3[0]), None) + self.assertEqual(_suffix_b2_finder.find(m.b1.b2.v3[0]), None) # Check finding default values # Add a default at the top level m.suffix[None] = 1 - assert _suffix_finder.find(m.b1.b2.v3[0]) == 1 + self.assertEqual(_suffix_finder.find(m.b1.b2.v3[0]), 1) + self.assertEqual(_suffix_b1_finder.find(m.b1.b2.v3[0]), None) + self.assertEqual(_suffix_b2_finder.find(m.b1.b2.v3[0]), None) # Add a default suffix at a lower level m.b1.b2.suffix[None] = 2 - assert _suffix_finder.find(m.b1.b2.v3[0]) == 2 + self.assertEqual(_suffix_finder.find(m.b1.b2.v3[0]), 2) + self.assertEqual(_suffix_b1_finder.find(m.b1.b2.v3[0]), 2) + self.assertEqual(_suffix_b2_finder.find(m.b1.b2.v3[0]), 2) # Check for container at lowest level m.b1.b2.suffix[m.b1.b2.v3] = 3 - assert _suffix_finder.find(m.b1.b2.v3[0]) == 3 + self.assertEqual(_suffix_finder.find(m.b1.b2.v3[0]), 3) + self.assertEqual(_suffix_b1_finder.find(m.b1.b2.v3[0]), 3) + self.assertEqual(_suffix_b2_finder.find(m.b1.b2.v3[0]), 3) # Check for container at top level m.suffix[m.b1.b2.v3] = 4 - assert _suffix_finder.find(m.b1.b2.v3[0]) == 4 + self.assertEqual(_suffix_finder.find(m.b1.b2.v3[0]), 4) + self.assertEqual(_suffix_b1_finder.find(m.b1.b2.v3[0]), 3) + self.assertEqual(_suffix_b2_finder.find(m.b1.b2.v3[0]), 3) # Check for specific values at lowest level m.b1.b2.suffix[m.b1.b2.v3[0]] = 5 - assert _suffix_finder.find(m.b1.b2.v3[0]) == 5 + self.assertEqual(_suffix_finder.find(m.b1.b2.v3[0]), 5) + self.assertEqual(_suffix_b1_finder.find(m.b1.b2.v3[0]), 5) + self.assertEqual(_suffix_b2_finder.find(m.b1.b2.v3[0]), 5) # Check for specific values at top level m.suffix[m.b1.b2.v3[0]] = 6 - assert _suffix_finder.find(m.b1.b2.v3[0]) == 6 + self.assertEqual(_suffix_finder.find(m.b1.b2.v3[0]), 6) + self.assertEqual(_suffix_b1_finder.find(m.b1.b2.v3[0]), 5) + self.assertEqual(_suffix_b2_finder.find(m.b1.b2.v3[0]), 5) # Make sure we don't find default suffixes at lower levels - assert _suffix_finder.find(m.b1.v2) == 1 + self.assertEqual(_suffix_finder.find(m.b1.v2), 1) + self.assertEqual(_suffix_b1_finder.find(m.b1.v2), None) + with self.assertRaisesRegex( + ValueError, + r"Component 'b1.v2' not found in the SuffixFinder context " + r"\(Block hierarchy rooted at b1.b2\)", + ): + _suffix_b2_finder.find(m.b1.v2) # Make sure we don't find specific suffixes at lower levels m.b1.b2.suffix[m.v1] = 5 - assert _suffix_finder.find(m.v1) == 1 + self.assertEqual(_suffix_finder.find(m.v1), 1) + with self.assertRaisesRegex( + ValueError, + r"Component 'v1' not found in the SuffixFinder context " + r"\(Block hierarchy rooted at b1\)", + ): + _suffix_b1_finder.find(m.v1) + with self.assertRaisesRegex( + ValueError, + r"Component 'v1' not found in the SuffixFinder context " + r"\(Block hierarchy rooted at b1.b2\)", + ): + _suffix_b2_finder.find(m.v1) + + # Make sure we can look up Blocks and that they will match + # suffixes that they hold + self.assertEqual(_suffix_finder.find(m.b1.b2), 2) + self.assertEqual(_suffix_b1_finder.find(m.b1.b2), 2) + self.assertEqual(_suffix_b2_finder.find(m.b1.b2), 2) + + self.assertEqual(_suffix_finder.find(m.b1), 1) + self.assertEqual(_suffix_b1_finder.find(m.b1), None) + with self.assertRaisesRegex( + ValueError, + r"Component 'b1' not found in the SuffixFinder context " + r"\(Block hierarchy rooted at b1.b2\)", + ): + _suffix_b2_finder.find(m.b1) if __name__ == "__main__": From bc185466c5137d9dff4729a93150989f5c138ab9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 17:00:04 -0600 Subject: [PATCH 2282/3044] NLv2: only locate / process Suffixes in the context of the model being written --- pyomo/repn/plugins/nl_writer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 8fc82d21d30..09d3f9b9ef0 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -510,8 +510,8 @@ def compile(self, column_order, row_order, obj_order, model_id): class CachingNumericSuffixFinder(SuffixFinder): scale = True - def __init__(self, name, default=None): - super().__init__(name, default) + def __init__(self, name, default=None, context=None): + super().__init__(name, default, context) self.suffix_cache = {} def __call__(self, obj): @@ -646,7 +646,7 @@ def write(self, model): # Data structures to support variable/constraint scaling # if self.config.scale_model and 'scaling_factor' in suffix_data: - scaling_factor = CachingNumericSuffixFinder('scaling_factor', 1) + scaling_factor = CachingNumericSuffixFinder('scaling_factor', 1, model) scaling_cache = scaling_factor.suffix_cache del suffix_data['scaling_factor'] else: From 5fbeb0941a6878e4b949baac47a275be6bac9804 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 17:02:14 -0600 Subject: [PATCH 2283/3044] SacalingTransform: only find/process Suffixes in the context of the transformation scope --- pyomo/core/plugins/transform/scaling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index 11d4ac8c493..87303514857 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -90,7 +90,7 @@ def _get_float_scaling_factor(self, component): def _apply_to(self, model, rename=True): # create a map of component to scaling factor component_scaling_factor_map = ComponentMap() - self._suffix_finder = SuffixFinder('scaling_factor', 1.0) + self._suffix_finder = SuffixFinder('scaling_factor', 1.0, model) # if the scaling_method is 'user', get the scaling parameters from the suffixes if self._scaling_method == 'user': From 84ca6ae088610ff8c68834819739221a38b58e9a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 17:12:55 -0600 Subject: [PATCH 2284/3044] remove unused _get_float_scaling_factor method --- pyomo/core/plugins/transform/scaling.py | 5 -- pyomo/core/tests/transform/test_scaling.py | 57 ++++++++++++++-------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index 87303514857..0835e5fd060 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -82,11 +82,6 @@ def _create_using(self, original_model, **kwds): self._apply_to(scaled_model, **kwds) return scaled_model - def _get_float_scaling_factor(self, component): - if self._suffix_finder is None: - self._suffix_finder = SuffixFinder('scaling_factor', 1.0) - return self._suffix_finder.find(component) - def _apply_to(self, model, rename=True): # create a map of component to scaling factor component_scaling_factor_map = ComponentMap() diff --git a/pyomo/core/tests/transform/test_scaling.py b/pyomo/core/tests/transform/test_scaling.py index d0fbfab61bd..2d66502271e 100644 --- a/pyomo/core/tests/transform/test_scaling.py +++ b/pyomo/core/tests/transform/test_scaling.py @@ -13,8 +13,7 @@ import pyomo.common.unittest as unittest import pyomo.environ as pyo from pyomo.opt.base.solvers import UnknownSolver -from pyomo.core.plugins.transform.scaling import ScaleModel - +from pyomo.core.plugins.transform.scaling import ScaleModel, SuffixFinder class TestScaleModelTransformation(unittest.TestCase): def test_linear_scaling(self): @@ -600,6 +599,13 @@ def con_rule(m, i): self.assertAlmostEqual(pyo.value(model.zcon), -8, 4) def test_get_float_scaling_factor_top_level(self): + # Note: the transformation used to have a private method for + # finding suffix values (which this method tested). The + # transformation now leverages the SuffixFinder. To ensure that + # the SuffixFinder behaves in the same way as the original local + # method, we preserve these tests, but directly test the + # SuffixFinder + m = pyo.ConcreteModel() m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT) @@ -616,17 +622,23 @@ def test_get_float_scaling_factor_top_level(self): m.scaling_factor[m.v1] = 0.1 m.scaling_factor[m.b1.v2] = 0.2 + _finder = SuffixFinder('scaling_factor', 1.0, m) + # SF should be 0.1 from top level - sf = ScaleModel()._get_float_scaling_factor(m.v1) - assert sf == float(0.1) + self.assertEqual(_finder.find(m.v1), 0.1) # SF should be 0.1 from top level, lower level ignored - sf = ScaleModel()._get_float_scaling_factor(m.b1.v2) - assert sf == float(0.2) + self.assertEqual(_finder.find(m.b1.v2), 0.2) # No SF, should return 1 - sf = ScaleModel()._get_float_scaling_factor(m.b1.b2.v3) - assert sf == 1.0 + self.assertEqual(_finder.find(m.b1.b2.v3), 1.0) def test_get_float_scaling_factor_local_level(self): + # Note: the transformation used to have a private method for + # finding suffix values (which this method tested). The + # transformation now leverages the SuffixFinder. To ensure that + # the SuffixFinder behaves in the same way as the original local + # method, we preserve these tests, but directly test the + # SuffixFinder + m = pyo.ConcreteModel() m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT) @@ -647,15 +659,21 @@ def test_get_float_scaling_factor_local_level(self): # Add an intermediate scaling factor - this should take priority m.b1.scaling_factor[m.b1.b2.v3] = 0.4 + _finder = SuffixFinder('scaling_factor', 1.0, m) + # Should get SF from local levels - sf = ScaleModel()._get_float_scaling_factor(m.v1) - assert sf == float(0.1) - sf = ScaleModel()._get_float_scaling_factor(m.b1.v2) - assert sf == float(0.2) - sf = ScaleModel()._get_float_scaling_factor(m.b1.b2.v3) - assert sf == float(0.4) + self.assertEqual(_finder.find(m.v1), 0.1) + self.assertEqual(_finder.find(m.b1.v2), 0.2) + self.assertEqual(_finder.find(m.b1.b2.v3), 0.4) def test_get_float_scaling_factor_intermediate_level(self): + # Note: the transformation used to have a private method for + # finding suffix values (which this method tested). The + # transformation now leverages the SuffixFinder. To ensure that + # the SuffixFinder behaves in the same way as the original local + # method, we preserve these tests, but directly test the + # SuffixFinder + m = pyo.ConcreteModel() m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT) @@ -680,15 +698,14 @@ def test_get_float_scaling_factor_intermediate_level(self): m.b1.b2.b3.scaling_factor[m.b1.b2.b3.v3] = 0.4 + _finder = SuffixFinder('scaling_factor', 1.0, m) + # v1 should be unscaled as SF set below variable level - sf = ScaleModel()._get_float_scaling_factor(m.v1) - assert sf == 1.0 + self.assertEqual(_finder.find(m.v1), 1.0) # v2 should get SF from b1 level - sf = ScaleModel()._get_float_scaling_factor(m.b1.b2.b3.v2) - assert sf == float(0.2) + self.assertEqual(_finder.find(m.b1.b2.b3.v2), 0.2) # v2 should get SF from highest level, ignoring b3 level - sf = ScaleModel()._get_float_scaling_factor(m.b1.b2.b3.v3) - assert sf == float(0.3) + self.assertEqual(_finder.find(m.b1.b2.b3.v3), 0.3) if __name__ == "__main__": From e890a254bae81b0f9fb0a67a0d0b1d72c97e3ee0 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 13 Aug 2024 17:44:11 -0600 Subject: [PATCH 2285/3044] Not sampling outside of discrete domains in uniform_grid and random_grid --- .../piecewise/tests/test_nonlinear_to_pwl.py | 78 +++++++++++++++++++ .../piecewise/transform/nonlinear_to_pwl.py | 40 ++++++---- 2 files changed, 103 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 76246688cdf..07a0f090cd6 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -325,8 +325,86 @@ def test_uniform_sampling_discrete_vars(self): additively_decompose=False, domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, ) + # No warnings (this is to check that we aren't emitting a bunch of + # warnings about setting variables outside of their domains) self.assertEqual(output.getvalue().strip(), "") + transformed_c = n_to_pwl.get_transformed_component(m.c) + pwlf = transformed_c.body.expr.pw_linear_function + + # should sample 0, 1 for th m.x's + # should sample 0, 2, 5 for m.y (because of half to even rounding (*sigh*)) + points = set(pwlf._points) + self.assertEqual(len(points), 12) + for x in [0, 1]: + for y in [0, 1]: + for z in [0, 2, 5]: + self.assertIn((x, y, z), points) + + @unittest.skipUnless(numpy_available, "Numpy is not available") + def test_uniform_sampling_discrete_vars(self): + m = ConcreteModel() + m.x = Var(['rocky', 'bullwinkle'], domain=Binary) + m.y = Var(domain=Integers, bounds=(0, 5)) + m.c = Constraint(expr=m.x['rocky'] * m.x['bullwinkle'] + m.y <= 4) + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + output = StringIO() + with LoggingIntercept(output, 'pyomo.core', logging.WARNING): + n_to_pwl.apply_to( + m, + num_points=3, + additively_decompose=False, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + ) + # No warnings (this is to check that we aren't emitting a bunch of + # warnings about setting variables outside of their domains) + self.assertEqual(output.getvalue().strip(), "") + + transformed_c = n_to_pwl.get_transformed_component(m.c) + pwlf = transformed_c.body.expr.pw_linear_function + + # should sample 0, 1 for th m.x's + # should sample 0, 2, 5 for m.y (because of half to even rounding (*sigh*)) + points = set(pwlf._points) + self.assertEqual(len(points), 12) + for x in [0, 1]: + for y in [0, 1]: + for z in [0, 2, 5]: + self.assertIn((x, y, z), points) + + @unittest.skipUnless(numpy_available, "Numpy is not available") + def test_random_sampling_discrete_vars(self): + m = ConcreteModel() + m.x = Var(['rocky', 'bullwinkle'], domain=Binary) + m.y = Var(domain=Integers, bounds=(0, 5)) + m.c = Constraint(expr=m.x['rocky'] * m.x['bullwinkle'] + m.y <= 4) + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + output = StringIO() + with LoggingIntercept(output, 'pyomo.core', logging.WARNING): + n_to_pwl.apply_to( + m, + num_points=3, + additively_decompose=False, + domain_partitioning_method=DomainPartitioningMethod.RANDOM_GRID, + ) + # No warnings (this is to check that we aren't emitting a bunch of + # warnings about setting variables outside of their domains) + self.assertEqual(output.getvalue().strip(), "") + + transformed_c = n_to_pwl.get_transformed_component(m.c) + pwlf = transformed_c.body.expr.pw_linear_function + + # should sample 0, 1 for th m.x's + # Happen to get 0, 1, 5 for m.y + points = set(pwlf._points) + self.assertEqual(len(points), 12) + for x in [0, 1]: + for y in [0, 1]: + for z in [0, 1, 5]: + self.assertIn((x, y, z), points) + class TestNonlinearToPWL_2D(unittest.TestCase): def make_paraboloid_model(self): diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index c4d7c801ba2..03f006b66bc 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -93,19 +93,29 @@ def __init__(self): def _get_random_point_grid(bounds, n, func, config, seed=42): # Generate randomized grid of points linspaces = [] - for lb, ub in bounds: - np.random.seed(seed) - linspaces.append(np.random.uniform(lb, ub, n)) + np.random.seed(seed) + for (lb, ub), is_integer in bounds: + if not is_integer: + linspaces.append(np.random.uniform(lb, ub, n)) + else: + size = min(n, ub - lb + 1) + linspaces.append(np.random.choice(range(lb, ub + 1), size=size, + replace=False)) return list(itertools.product(*linspaces)) def _get_uniform_point_grid(bounds, n, func, config): # Generate non-randomized grid of points linspaces = [] - for lb, ub in bounds: - # Issues happen when exactly using the boundary - nudge = (ub - lb) * 1e-4 - linspaces.append(np.linspace(lb + nudge, ub - nudge, n)) + for (lb, ub), is_integer in bounds: + if not is_integer: + # Issues happen when exactly using the boundary + nudge = (ub - lb) * 1e-4 + linspaces.append(np.linspace(lb + nudge, ub - nudge, n)) + else: + size = min(n, ub - lb + 1) + pts = np.linspace(lb, ub, size) + linspaces.append(np.array([round(i) for i in pts])) return list(itertools.product(*linspaces)) @@ -159,8 +169,8 @@ def _get_pwl_function_approximation(func, config, bounds): func: function to approximate config: ConfigDict for transformation, specifying domain_partitioning_method, num_points, and max_depth (if using linear trees) - bounds: list of tuples giving upper and lower bounds for each of func's - arguments + bounds: list of tuples giving upper and lower bounds and a boolean indicating + if the variable's domain is discrete or not, for each of func's arguments """ method = config.domain_partitioning_method n = config.num_points @@ -195,8 +205,8 @@ def _generate_bound_points(leaves, bounds): for pt in [lower_corner_list, upper_corner_list]: for i in range(len(pt)): # clamp within bounds range - pt[i] = max(pt[i], bounds[i][0]) - pt[i] = min(pt[i], bounds[i][1]) + pt[i] = max(pt[i], bounds[i][0][0]) + pt[i] = min(pt[i], bounds[i][0][1]) if tuple(lower_corner_list) not in bound_points: bound_points.append(tuple(lower_corner_list)) @@ -206,7 +216,7 @@ def _generate_bound_points(leaves, bounds): # This process should have gotten every interior bound point. However, all # but two of the corners of the overall bounding box should have been # missed. Let's fix that now. - for outer_corner in itertools.product(*bounds): + for outer_corner in itertools.product(*[b[0] for b in bounds]): if outer_corner not in bound_points: bound_points.append(outer_corner) return bound_points @@ -296,9 +306,9 @@ def _reassign_none_bounds(leaves, input_bounds): for l in L: for f in features: if leaves[l]['bounds'][f][0] == None: - leaves[l]['bounds'][f][0] = input_bounds[f][0] + leaves[l]['bounds'][f][0] = input_bounds[f][0][0] if leaves[l]['bounds'][f][1] == None: - leaves[l]['bounds'][f][1] = input_bounds[f][1] + leaves[l]['bounds'][f][1] = input_bounds[f][0][1] return leaves @@ -615,7 +625,7 @@ def _get_bounds_list(self, var_list, obj): "at least one bound" % (v.name, obj.name) ) else: - bounds.append(v.bounds) + bounds.append((v.bounds, v.is_integer())) return bounds def _needs_approximating(self, expr, approximate_quadratic): From 10b902603511dba62e93d03286304dab170cd541 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 13 Aug 2024 17:44:33 -0600 Subject: [PATCH 2286/3044] black --- pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py | 2 +- pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 07a0f090cd6..bc8d7a40027 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -371,7 +371,7 @@ def test_uniform_sampling_discrete_vars(self): for x in [0, 1]: for y in [0, 1]: for z in [0, 2, 5]: - self.assertIn((x, y, z), points) + self.assertIn((x, y, z), points) @unittest.skipUnless(numpy_available, "Numpy is not available") def test_random_sampling_discrete_vars(self): diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 03f006b66bc..a35231dd890 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -99,8 +99,9 @@ def _get_random_point_grid(bounds, n, func, config, seed=42): linspaces.append(np.random.uniform(lb, ub, n)) else: size = min(n, ub - lb + 1) - linspaces.append(np.random.choice(range(lb, ub + 1), size=size, - replace=False)) + linspaces.append( + np.random.choice(range(lb, ub + 1), size=size, replace=False) + ) return list(itertools.product(*linspaces)) @@ -169,7 +170,7 @@ def _get_pwl_function_approximation(func, config, bounds): func: function to approximate config: ConfigDict for transformation, specifying domain_partitioning_method, num_points, and max_depth (if using linear trees) - bounds: list of tuples giving upper and lower bounds and a boolean indicating + bounds: list of tuples giving upper and lower bounds and a boolean indicating if the variable's domain is discrete or not, for each of func's arguments """ method = config.domain_partitioning_method From f1c6ff128292732523ba6a77ce3acab03c349509 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 20:59:09 -0600 Subject: [PATCH 2287/3044] NFC: apply black --- pyomo/core/tests/transform/test_scaling.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/core/tests/transform/test_scaling.py b/pyomo/core/tests/transform/test_scaling.py index 2d66502271e..7168f6bb707 100644 --- a/pyomo/core/tests/transform/test_scaling.py +++ b/pyomo/core/tests/transform/test_scaling.py @@ -15,6 +15,7 @@ from pyomo.opt.base.solvers import UnknownSolver from pyomo.core.plugins.transform.scaling import ScaleModel, SuffixFinder + class TestScaleModelTransformation(unittest.TestCase): def test_linear_scaling(self): model = pyo.ConcreteModel() From d3541666d479bab4c6f89523602e7b2b9dddd0ac Mon Sep 17 00:00:00 2001 From: alpertoygar Date: Wed, 14 Aug 2024 09:11:42 -0400 Subject: [PATCH 2288/3044] Add missing main call for example file --- .../examples/reactor_design/parameter_estimation_example.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py index a84a3fde5e7..d29cbfd4d49 100644 --- a/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py +++ b/pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py @@ -39,3 +39,7 @@ def main(): obj, theta, cov = pest.theta_est(calc_cov=True, cov_n=17) print(obj) print(theta) + + +if __name__ == "__main__": + main() From ca1f39e87ed41620feeb2bb523ce8bc90edf852f Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Wed, 14 Aug 2024 09:14:42 -0400 Subject: [PATCH 2289/3044] Add comments to tests that don't assert anything --- pyomo/contrib/doe/tests/test_doe_solve.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index 0bc5d7254c9..feb1447f5ca 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -230,6 +230,8 @@ def test_reactor_obj_cholesky_solve(self): # Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM) self.assertTrue(np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q))) + # This test ensure that compute FIM runs without error using the + # `sequential` option with central finite differences def test_compute_FIM_seq_centr(self): fd_method = "central" obj_used = "determinant" @@ -242,6 +244,8 @@ def test_compute_FIM_seq_centr(self): doe_obj.compute_FIM(method="sequential") + # This test ensure that compute FIM runs without error using the + # `sequential` option with forward finite differences def test_compute_FIM_seq_forward(self): fd_method = "forward" obj_used = "determinant" @@ -254,6 +258,9 @@ def test_compute_FIM_seq_forward(self): doe_obj.compute_FIM(method="sequential") + # This test ensure that compute FIM runs without error using the + # `kaug` option. kaug computes the FIM directly so no finite difference + # scheme is needed. @unittest.skipIf(not scipy_available, "Scipy is not available") @unittest.skipIf( not k_aug_available.available(False), "The 'k_aug' command is not available" @@ -270,6 +277,8 @@ def test_compute_FIM_kaug(self): doe_obj.compute_FIM(method="kaug") + # This test ensure that compute FIM runs without error using the + # `sequential` option with backward finite differences def test_compute_FIM_seq_backward(self): fd_method = "backward" obj_used = "determinant" From f4ff8b7086b6c6675a10afb6bb25d42d2efe1a30 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Wed, 14 Aug 2024 09:30:40 -0400 Subject: [PATCH 2290/3044] Ran Black --- pyomo/contrib/doe/tests/test_doe_solve.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/doe/tests/test_doe_solve.py b/pyomo/contrib/doe/tests/test_doe_solve.py index feb1447f5ca..c25eb8018f7 100644 --- a/pyomo/contrib/doe/tests/test_doe_solve.py +++ b/pyomo/contrib/doe/tests/test_doe_solve.py @@ -230,7 +230,7 @@ def test_reactor_obj_cholesky_solve(self): # Make sure FIM and Q.T @ sigma_inv @ Q are close (alternate definition of FIM) self.assertTrue(np.all(np.isclose(FIM, Q.T @ sigma_inv @ Q))) - # This test ensure that compute FIM runs without error using the + # This test ensure that compute FIM runs without error using the # `sequential` option with central finite differences def test_compute_FIM_seq_centr(self): fd_method = "central" @@ -244,7 +244,7 @@ def test_compute_FIM_seq_centr(self): doe_obj.compute_FIM(method="sequential") - # This test ensure that compute FIM runs without error using the + # This test ensure that compute FIM runs without error using the # `sequential` option with forward finite differences def test_compute_FIM_seq_forward(self): fd_method = "forward" @@ -258,7 +258,7 @@ def test_compute_FIM_seq_forward(self): doe_obj.compute_FIM(method="sequential") - # This test ensure that compute FIM runs without error using the + # This test ensure that compute FIM runs without error using the # `kaug` option. kaug computes the FIM directly so no finite difference # scheme is needed. @unittest.skipIf(not scipy_available, "Scipy is not available") @@ -277,7 +277,7 @@ def test_compute_FIM_kaug(self): doe_obj.compute_FIM(method="kaug") - # This test ensure that compute FIM runs without error using the + # This test ensure that compute FIM runs without error using the # `sequential` option with backward finite differences def test_compute_FIM_seq_backward(self): fd_method = "backward" From fc00af0aa099ab76e669d36367122f2618b58a0a Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Wed, 14 Aug 2024 09:50:47 -0400 Subject: [PATCH 2291/3044] Added deprecation warning/error for old interface --- pyomo/contrib/doe/__init__.py | 36 +++++++++++++++++++++++++++++++++++ pyomo/contrib/doe/doe.py | 1 - 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index 04e237c18db..ffb6df1a860 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -10,3 +10,39 @@ # ___________________________________________________________________________ from .doe import DesignOfExperiments, ObjectiveLib, FiniteDifferenceStep from .utils import rescale_FIM + +# Deprecation errors for old Pyomo.DoE interface classes and structures +from pyomo.common.deprecation import deprecated + +deprecation_message = ( + "Pyomo.DoE has been refactored. The current interface utilizes Experiment " + "objects that label unknown parameters, experiment inputs, experiment outputs " + "and measurement error. This avoids string-based naming which is fragile. For " + "instructions to use the new interface, please see the Pyomo.DoE under the contributed " + "packages documentation at `https://pyomo.readthedocs.io/en/latest/contributed_packages/doe/doe.html`" +) + + +@deprecated( + "Use of MeasurementVariables in Pyomo.DoE is no longer supported.", + version='6.7.4.dev0', +) +class MeasurementVariables: + def __init__(self, *args): + raise RuntimeError(deprecation_message) + + +@deprecated( + "Use of DesignVariables in Pyomo.DoE is no longer supported.", version='6.7.4.dev0' +) +class DesignVariables: + def __init__(self, *args): + raise RuntimeError(deprecation_message) + + +@deprecated( + "Use of ModelOptionLib in Pyomo.DoE is no longer supported.", version='6.7.4.dev0' +) +class ModelOptionLib: + def __init__(self, *args): + raise RuntimeError(deprecation_message) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 32036799a56..99f53de6262 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -81,7 +81,6 @@ def __init__( logger_level=logging.WARNING, _Cholesky_option=True, _only_compute_fim_lower=True, - **kwargs ): """ This package enables model-based design of experiments analysis with Pyomo. From c0eef1798660d5af74eadf1db98de83a0978fe4f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 14 Aug 2024 07:54:51 -0600 Subject: [PATCH 2292/3044] SuffixFinder: return default for out-of-scope components (not an exception) --- pyomo/core/base/suffix.py | 8 ++++---- pyomo/core/tests/unit/test_suffix.py | 28 ++++------------------------ 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/pyomo/core/base/suffix.py b/pyomo/core/base/suffix.py index b9aa3b58ced..c07f7a0deb2 100644 --- a/pyomo/core/base/suffix.py +++ b/pyomo/core/base/suffix.py @@ -473,10 +473,10 @@ def find(self, component_data): try: suffixes = self._get_suffix_list(_block) except AttributeError: - raise ValueError( - f"Component '{component_data.name}' not found in the SuffixFinder " - f"context (Block hierarchy rooted at {self._context.name})" - ) from None + # Component was outside the context (eventually parent + # becomes None and parent.parent_block() raises an + # AttributeError): we will return the default value + return self.default # Pass 1: look for the component_data, working root to leaf for s in suffixes: if component_data in s: diff --git a/pyomo/core/tests/unit/test_suffix.py b/pyomo/core/tests/unit/test_suffix.py index 1278601bb3b..f56f84fc129 100644 --- a/pyomo/core/tests/unit/test_suffix.py +++ b/pyomo/core/tests/unit/test_suffix.py @@ -1849,28 +1849,13 @@ def test_suffix_finder(self): # Make sure we don't find default suffixes at lower levels self.assertEqual(_suffix_finder.find(m.b1.v2), 1) self.assertEqual(_suffix_b1_finder.find(m.b1.v2), None) - with self.assertRaisesRegex( - ValueError, - r"Component 'b1.v2' not found in the SuffixFinder context " - r"\(Block hierarchy rooted at b1.b2\)", - ): - _suffix_b2_finder.find(m.b1.v2) + self.assertEqual(_suffix_b2_finder.find(m.b1.v2), None) # Make sure we don't find specific suffixes at lower levels m.b1.b2.suffix[m.v1] = 5 self.assertEqual(_suffix_finder.find(m.v1), 1) - with self.assertRaisesRegex( - ValueError, - r"Component 'v1' not found in the SuffixFinder context " - r"\(Block hierarchy rooted at b1\)", - ): - _suffix_b1_finder.find(m.v1) - with self.assertRaisesRegex( - ValueError, - r"Component 'v1' not found in the SuffixFinder context " - r"\(Block hierarchy rooted at b1.b2\)", - ): - _suffix_b2_finder.find(m.v1) + self.assertEqual(_suffix_b1_finder.find(m.v1), None) + self.assertEqual(_suffix_b2_finder.find(m.v1), None) # Make sure we can look up Blocks and that they will match # suffixes that they hold @@ -1880,12 +1865,7 @@ def test_suffix_finder(self): self.assertEqual(_suffix_finder.find(m.b1), 1) self.assertEqual(_suffix_b1_finder.find(m.b1), None) - with self.assertRaisesRegex( - ValueError, - r"Component 'b1' not found in the SuffixFinder context " - r"\(Block hierarchy rooted at b1.b2\)", - ): - _suffix_b2_finder.find(m.b1) + self.assertEqual(_suffix_b2_finder.find(m.b1), None) if __name__ == "__main__": From 31a13f625c7def976471cbe6f1e2e57e9471f1b9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 14 Aug 2024 07:55:05 -0600 Subject: [PATCH 2293/3044] NFC: update docstring --- pyomo/core/base/suffix.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyomo/core/base/suffix.py b/pyomo/core/base/suffix.py index c07f7a0deb2..19dbe48f650 100644 --- a/pyomo/core/base/suffix.py +++ b/pyomo/core/base/suffix.py @@ -425,6 +425,14 @@ def __init__(self, name, default=None, context=None): Default value to return from `.find()` if no matching Suffix is found. + context: BlockData + + The root of the Block hierarchy to use when searching for + Suffix components. Suffixes outside this hierarchy will not + be interrogated and components that are queried (with + :py:meth:`find(component_data)` will return the default + value. + """ self.name = name self.default = default From 24bd55e2c2b85d7c679b6fee235bed89e981a8f5 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Wed, 14 Aug 2024 10:10:11 -0400 Subject: [PATCH 2294/3044] Remove URL for URL checker for now Will add back before pushing --- pyomo/contrib/doe/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index ffb6df1a860..5a02eafbaa9 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -19,7 +19,8 @@ "objects that label unknown parameters, experiment inputs, experiment outputs " "and measurement error. This avoids string-based naming which is fragile. For " "instructions to use the new interface, please see the Pyomo.DoE under the contributed " - "packages documentation at `https://pyomo.readthedocs.io/en/latest/contributed_packages/doe/doe.html`" + "packages documentation at " + # "`https://pyomo.readthedocs.io/en/latest/contributed_packages/doe/doe.html`" ) From ecc24bfde8b15b63fccf1dd405c2aa50b37cf2f2 Mon Sep 17 00:00:00 2001 From: Daniel Laky <29078718+djlaky@users.noreply.github.com> Date: Wed, 14 Aug 2024 17:01:40 -0400 Subject: [PATCH 2295/3044] Reverting previous change. --- pyomo/contrib/doe/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index 5a02eafbaa9..ffb6df1a860 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -19,8 +19,7 @@ "objects that label unknown parameters, experiment inputs, experiment outputs " "and measurement error. This avoids string-based naming which is fragile. For " "instructions to use the new interface, please see the Pyomo.DoE under the contributed " - "packages documentation at " - # "`https://pyomo.readthedocs.io/en/latest/contributed_packages/doe/doe.html`" + "packages documentation at `https://pyomo.readthedocs.io/en/latest/contributed_packages/doe/doe.html`" ) From 86d4fffc2413d3109df74f0b40c69f3c37d5529d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 14 Aug 2024 16:48:56 -0600 Subject: [PATCH 2296/3044] not triggering scipy install on pypy --- .github/workflows/test_branches.yml | 4 ++-- .github/workflows/test_pr_and_main.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 03894a1cb20..a4f2f8128e9 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -21,8 +21,8 @@ defaults: env: PYTHONWARNINGS: ignore::UserWarning PYTHON_CORE_PKGS: wheel - PYPI_ONLY: z3-solver - PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels + PYPI_ONLY: z3-solver linear-tree + PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels linear-tree CACHE_VER: v221013.1 NEOS_EMAIL: tests@pyomo.org SRC_REF: ${{ github.head_ref || github.ref }} diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 765e50826d0..2ca7e166fd8 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -30,7 +30,7 @@ env: PYTHONWARNINGS: ignore::UserWarning PYTHON_CORE_PKGS: wheel PYPI_ONLY: z3-solver linear-tree - PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels + PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels linear-tree CACHE_VER: v221013.1 NEOS_EMAIL: tests@pyomo.org SRC_REF: ${{ github.head_ref || github.ref }} From 5bda911a3f43739b2dc933fd350820521ce916d8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 15 Aug 2024 13:41:31 -0600 Subject: [PATCH 2297/3044] Skipping two more tests when scipy not available--this really should make the pypy tests pass --- pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index bc8d7a40027..b937e09ce8b 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -342,6 +342,7 @@ def test_uniform_sampling_discrete_vars(self): self.assertIn((x, y, z), points) @unittest.skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(scipy_available, "Scipy is not available") def test_uniform_sampling_discrete_vars(self): m = ConcreteModel() m.x = Var(['rocky', 'bullwinkle'], domain=Binary) @@ -374,6 +375,7 @@ def test_uniform_sampling_discrete_vars(self): self.assertIn((x, y, z), points) @unittest.skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(scipy_available, "Scipy is not available") def test_random_sampling_discrete_vars(self): m = ConcreteModel() m.x = Var(['rocky', 'bullwinkle'], domain=Binary) From 4ff37ce4ae79016815acff329365dd99e3bda5b5 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 15 Aug 2024 15:50:30 -0400 Subject: [PATCH 2298/3044] Simplify use of `_merge_dict` method --- pyomo/repn/parameterized_quadratic.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 0face2702c7..d818c7c3ed2 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -31,6 +31,7 @@ ParameterizedLinearRepnVisitor, to_expression, _handle_division_ANY_pseudo_constant, + _merge_dict, ) from pyomo.repn.quadratic import QuadraticRepn, _mul_linear_linear from pyomo.repn.util import ExprType @@ -43,25 +44,6 @@ _QUADRATIC = ExprType.QUADRATIC -def _merge_dict(dest_dict, mult, src_dict): - """ - Slightly different from `merge_dict` - in the `parameterized_linear` module. - """ - if not is_equal_to(mult, 1): - for vid, coef in src_dict.items(): - if vid in dest_dict: - dest_dict[vid] += mult * coef - else: - dest_dict[vid] = mult * coef - else: - for vid, coef in src_dict.items(): - if vid in dest_dict: - dest_dict[vid] += coef - else: - dest_dict[vid] = coef - - class ParameterizedQuadraticRepn(QuadraticRepn): def __str__(self): return ( From 031500b8123fea0822241f4286ff40eea5cdac09 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Thu, 15 Aug 2024 15:43:48 -0600 Subject: [PATCH 2299/3044] use context argument for SuffixFinder --- .../pynumero/interfaces/pyomo_grey_box_nlp.py | 4 +++- pyomo/contrib/pynumero/interfaces/pyomo_nlp.py | 14 ++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py index 3cc23260f56..8b320c091e4 100644 --- a/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py @@ -228,7 +228,9 @@ def __init__(self, pyomo_model): need_scaling = True self._primals_scaling = np.ones(self.n_primals()) - scaling_suffix_finder = SuffixFinder('scaling_factor') + scaling_suffix_finder = SuffixFinder( + 'scaling_factor', context=self._pyomo_model + ) for i, v in enumerate(self._pyomo_model_var_datas): v_scaling = scaling_suffix_finder.find(v) if v_scaling is not None: diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py index 4b955b0176a..8790e29cf37 100644 --- a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py @@ -299,7 +299,7 @@ def get_inequality_constraint_indices(self, constraints): # overloaded from NLP def get_obj_scaling(self): obj = self.get_pyomo_objective() - val = SuffixFinder('scaling_factor').find(obj) + val = SuffixFinder('scaling_factor', context=self._pyomo_model).find(obj) # maintain backwards compatibility scaling_suffix = self._pyomo_model.component('scaling_factor') if scaling_suffix and scaling_suffix.ctype is pyo.Suffix: @@ -309,7 +309,9 @@ def get_obj_scaling(self): # overloaded from NLP def get_primals_scaling(self): - scaling_suffix_finder = SuffixFinder('scaling_factor') + scaling_suffix_finder = SuffixFinder( + 'scaling_factor', context=self._pyomo_model + ) primals_scaling = np.ones(self.n_primals()) ret = None for i, v in enumerate(self.get_pyomo_variables()): @@ -326,7 +328,9 @@ def get_primals_scaling(self): # overloaded from NLP def get_constraints_scaling(self): - scaling_suffix_finder = SuffixFinder('scaling_factor') + scaling_suffix_finder = SuffixFinder( + 'scaling_factor', context=self._pyomo_model + ) constraints_scaling = np.ones(self.n_constraints()) ret = None for i, c in enumerate(self.get_pyomo_constraints()): @@ -621,7 +625,9 @@ def __init__(self, pyomo_model): need_scaling = True self._primals_scaling = np.ones(self.n_primals()) - scaling_suffix_finder = SuffixFinder('scaling_factor') + scaling_suffix_finder = SuffixFinder( + 'scaling_factor', context=self._pyomo_model + ) for i, v in enumerate(self.get_pyomo_variables()): v_scaling = scaling_suffix_finder.find(v) if v_scaling is not None: From ff678174ec76cf0ec2c2275b9640b6e0fc15d048 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Fri, 16 Aug 2024 12:43:01 -0600 Subject: [PATCH 2300/3044] implement John's suggestion --- .../pynumero/interfaces/pyomo_grey_box_nlp.py | 20 ++--- .../contrib/pynumero/interfaces/pyomo_nlp.py | 85 ++++++++----------- 2 files changed, 43 insertions(+), 62 deletions(-) diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py index 8b320c091e4..66cf99ea862 100644 --- a/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/pyomo_grey_box_nlp.py @@ -227,19 +227,15 @@ def __init__(self, pyomo_model): else: need_scaling = True - self._primals_scaling = np.ones(self.n_primals()) - scaling_suffix_finder = SuffixFinder( - 'scaling_factor', context=self._pyomo_model + scaling_finder = SuffixFinder( + 'scaling_factor', default=1.0, context=self._pyomo_model ) - for i, v in enumerate(self._pyomo_model_var_datas): - v_scaling = scaling_suffix_finder.find(v) - if v_scaling is not None: - need_scaling = True - self._primals_scaling[i] = v_scaling - # maintain backwards compatibility - scaling_suffix = self._pyomo_model.component('scaling_factor') - if scaling_suffix and scaling_suffix.ctype is pyo.Suffix: - need_scaling = True + self._primals_scaling = np.fromiter( + (scaling_finder.find(v) for v in self._pyomo_model_var_datas), + count=self.n_primals(), + dtype=float, + ) + need_scaling = bool(scaling_finder.all_suffixes) self._constraints_scaling = BlockVector(len(nlps)) for i, nlp in enumerate(nlps): diff --git a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py index 8790e29cf37..725435619ad 100644 --- a/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/pyomo_nlp.py @@ -298,52 +298,41 @@ def get_inequality_constraint_indices(self, constraints): # overloaded from NLP def get_obj_scaling(self): - obj = self.get_pyomo_objective() - val = SuffixFinder('scaling_factor', context=self._pyomo_model).find(obj) - # maintain backwards compatibility - scaling_suffix = self._pyomo_model.component('scaling_factor') - if scaling_suffix and scaling_suffix.ctype is pyo.Suffix: - return 1.0 if val is None else val - else: - return val + scaling_finder = SuffixFinder( + 'scaling_factor', default=1.0, context=self._pyomo_model + ) + val = scaling_finder.find(self.get_pyomo_objective()) + if not scaling_finder.all_suffixes: + return None + return val # overloaded from NLP def get_primals_scaling(self): - scaling_suffix_finder = SuffixFinder( - 'scaling_factor', context=self._pyomo_model + scaling_finder = SuffixFinder( + 'scaling_factor', default=1.0, context=self._pyomo_model ) - primals_scaling = np.ones(self.n_primals()) - ret = None - for i, v in enumerate(self.get_pyomo_variables()): - val = scaling_suffix_finder.find(v) - if val is not None: - primals_scaling[i] = val - ret = primals_scaling - # maintain backwards compatibility - scaling_suffix = self._pyomo_model.component('scaling_factor') - if scaling_suffix and scaling_suffix.ctype is pyo.Suffix: - return primals_scaling - else: - return ret + primals_scaling = np.fromiter( + (scaling_finder.find(v) for v in self.get_pyomo_variables()), + count=self.n_primals(), + dtype=float, + ) + if not scaling_finder.all_suffixes: + return None + return primals_scaling # overloaded from NLP def get_constraints_scaling(self): - scaling_suffix_finder = SuffixFinder( - 'scaling_factor', context=self._pyomo_model + scaling_finder = SuffixFinder( + 'scaling_factor', default=1.0, context=self._pyomo_model ) - constraints_scaling = np.ones(self.n_constraints()) - ret = None - for i, c in enumerate(self.get_pyomo_constraints()): - val = scaling_suffix_finder.find(c) - if val is not None: - constraints_scaling[i] = val - ret = constraints_scaling - # maintain backwards compatibility - scaling_suffix = self._pyomo_model.component('scaling_factor') - if scaling_suffix and scaling_suffix.ctype is pyo.Suffix: - return constraints_scaling - else: - return ret + constraints_scaling = np.fromiter( + (scaling_finder.find(v) for v in self.get_pyomo_constraints()), + count=self.n_constraints(), + dtype=float, + ) + if not scaling_finder.all_suffixes: + return None + return constraints_scaling def extract_subvector_grad_objective(self, pyomo_variables): """Compute the gradient of the objective and return the entries @@ -624,19 +613,15 @@ def __init__(self, pyomo_model): else: need_scaling = True - self._primals_scaling = np.ones(self.n_primals()) - scaling_suffix_finder = SuffixFinder( - 'scaling_factor', context=self._pyomo_model + scaling_finder = SuffixFinder( + 'scaling_factor', default=1.0, context=self._pyomo_model ) - for i, v in enumerate(self.get_pyomo_variables()): - v_scaling = scaling_suffix_finder.find(v) - if v_scaling is not None: - need_scaling = True - self._primals_scaling[i] = v_scaling - # maintain backwards compatibility - scaling_suffix = self._pyomo_model.component('scaling_factor') - if scaling_suffix and scaling_suffix.ctype is pyo.Suffix: - need_scaling = True + self._primals_scaling = np.fromiter( + (scaling_finder.find(v) for v in self.get_pyomo_variables()), + count=self.n_primals(), + dtype=float, + ) + need_scaling = bool(scaling_finder.all_suffixes) self._constraints_scaling = [] pyomo_nlp_scaling = self._pyomo_nlp.get_constraints_scaling() From 7e79e194486b98d4e2694355d8233f55e01e38d8 Mon Sep 17 00:00:00 2001 From: Bernard Knueven Date: Fri, 16 Aug 2024 14:37:40 -0600 Subject: [PATCH 2301/3044] adding Robby's test code for no scaling --- .../pynumero/interfaces/tests/test_nlp.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py b/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py index b456ce1cb51..a291ef1151a 100644 --- a/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py +++ b/pyomo/contrib/pynumero/interfaces/tests/test_nlp.py @@ -718,6 +718,23 @@ def test_subblock_scaling(self): assert nlp.get_primals_scaling()[0] == 1e16 assert nlp.get_constraints_scaling()[0] == 1e16 + def test_subblock_no_scaling(self): + m = pyo.ConcreteModel() + m.b = pyo.Block() + m.b.x = pyo.Var([1, 2], initialize={1: 100, 2: 20}) + + # Components so we don't have an empty NLP + m.b.eq = pyo.Constraint(expr=m.b.x[1] * m.b.x[2] == 2000) + m.b.obj = pyo.Objective(expr=m.b.x[1] ** 2 + m.b.x[2] ** 2) + + m.scaling_factor = pyo.Suffix(direction=pyo.Suffix.EXPORT) + m.scaling_factor[m.b.x[1]] = 1e-2 + m.scaling_factor[m.b.x[2]] = 1e-1 + + nlp = PyomoNLP(m.b) + scaling = nlp.get_primals_scaling() + assert scaling is None + def test_no_objective(self): m = pyo.ConcreteModel() m.x = pyo.Var() From 6505f5baa4765ccb0b2adf6283ddfb04986f104a Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Fri, 16 Aug 2024 17:04:45 -0400 Subject: [PATCH 2302/3044] Add solve to help with initialization of DoE model --- pyomo/contrib/doe/doe.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 99f53de6262..627eae3d3eb 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -1079,6 +1079,7 @@ def build_block_scenarios(b, s): pyo.ComponentUID(param, context=m.base_model).find_component_on( b ).set_value(m.base_model.unknown_parameters[param] * (1 + diff)) + res = self.solver.solve(b, tee=self.tee) model.scenario_blocks = pyo.Block(model.scenarios, rule=build_block_scenarios) From 02d2dd25fe4cb6ab510783d7e30b7348bf3e2a8b Mon Sep 17 00:00:00 2001 From: Arnaud Baguet Date: Sat, 17 Aug 2024 22:50:23 -0400 Subject: [PATCH 2303/3044] remove deprecated addConstr call --- .../solvers/plugins/solvers/gurobi_direct.py | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/gurobi_direct.py b/pyomo/solvers/plugins/solvers/gurobi_direct.py index ed66a4e0e7b..4a2494daf2f 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_direct.py +++ b/pyomo/solvers/plugins/solvers/gurobi_direct.py @@ -97,6 +97,20 @@ def _set_options(model_or_env, options): model_or_env.setParam(key, float(option)) +class GurobiModel(gurobipy.Model): + def __init__(self, *args, **kwds): + super().__init__(*args, **kwds) + + def addConstr(self, degree, lhs, sense=None, rhs=None, name=""): + if degree == 1: + con = self.addLConstr(lhs, sense, rhs, name) + elif degree == 2: + con = self.addQConstr(lhs, sense, rhs, name) + else: + raise DegreeError('GurobiModel.addConstr: Unsupported degree: %s' % degree) + return con + + @SolverFactory.register('gurobi_direct', doc='Direct python interface to Gurobi') class GurobiDirect(DirectSolver): """A direct interface to Gurobi using gurobipy. @@ -308,7 +322,7 @@ def _get_expr_from_pyomo_repn(self, repn, max_degree=2): new_expr += repn.constant - return new_expr, referenced_vars + return new_expr, referenced_vars, degree def _get_expr_from_pyomo_expr(self, expr, max_degree=2): if max_degree == 2: @@ -317,7 +331,7 @@ def _get_expr_from_pyomo_expr(self, expr, max_degree=2): repn = generate_standard_repn(expr, quadratic=False) try: - gurobi_expr, referenced_vars = self._get_expr_from_pyomo_repn( + gurobi_expr, referenced_vars, degree = self._get_expr_from_pyomo_repn( repn, max_degree ) except DegreeError as e: @@ -325,7 +339,7 @@ def _get_expr_from_pyomo_expr(self, expr, max_degree=2): msg += '\nexpr: {0}'.format(expr) raise DegreeError(msg) - return gurobi_expr, referenced_vars + return gurobi_expr, referenced_vars, degree def _gurobi_lb_ub_from_var(self, var): if var.is_fixed(): @@ -404,10 +418,12 @@ def _create_model(self, model): self._init_env() if self._solver_model is not None: self._solver_model.close() - if model.name is not None: - self._solver_model = gurobipy.Model(model.name, env=self._env) - else: - self._solver_model = gurobipy.Model(env=self._env) + + self._solver_model = ( + GurobiModel(model.name, env=self._env) + if model.name is not None + else GurobiModel(env=self._env) + ) def close(self): """Frees local Gurobi resources used by this solver instance. @@ -499,15 +515,11 @@ def _add_constraint(self, con): conname = self._symbol_map.getSymbol(con, self._labeler) if con._linear_canonical_form: - gurobi_expr, referenced_vars = self._get_expr_from_pyomo_repn( + gurobi_expr, referenced_vars, degree = self._get_expr_from_pyomo_repn( con.canonical_form(), self._max_constraint_degree ) - # elif isinstance(con, LinearCanonicalRepn): - # gurobi_expr, referenced_vars = self._get_expr_from_pyomo_repn( - # con, - # self._max_constraint_degree) else: - gurobi_expr, referenced_vars = self._get_expr_from_pyomo_expr( + gurobi_expr, referenced_vars, degree = self._get_expr_from_pyomo_expr( con.body, self._max_constraint_degree ) @@ -524,6 +536,7 @@ def _add_constraint(self, con): if con.equality: gurobipy_con = self._solver_model.addConstr( + degree=degree, lhs=gurobi_expr, sense=gurobipy.GRB.EQUAL, rhs=value(con.lower), @@ -536,6 +549,7 @@ def _add_constraint(self, con): self._range_constraints.add(con) elif con.has_lb(): gurobipy_con = self._solver_model.addConstr( + degree=degree, lhs=gurobi_expr, sense=gurobipy.GRB.GREATER_EQUAL, rhs=value(con.lower), @@ -543,6 +557,7 @@ def _add_constraint(self, con): ) elif con.has_ub(): gurobipy_con = self._solver_model.addConstr( + degree=degree, lhs=gurobi_expr, sense=gurobipy.GRB.LESS_EQUAL, rhs=value(con.upper), From b72310a8212458b9b205912921bf72374d5c714d Mon Sep 17 00:00:00 2001 From: Arnaud Baguet Date: Sun, 18 Aug 2024 05:40:19 -0400 Subject: [PATCH 2304/3044] remove unused import in gurobi_direct --- pyomo/solvers/plugins/solvers/gurobi_direct.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/gurobi_direct.py b/pyomo/solvers/plugins/solvers/gurobi_direct.py index 4a2494daf2f..91fb107e557 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_direct.py +++ b/pyomo/solvers/plugins/solvers/gurobi_direct.py @@ -32,7 +32,6 @@ from pyomo.opt.results.solver import TerminationCondition, SolverStatus from pyomo.opt.base import SolverFactory from pyomo.core.base.suffix import Suffix -import pyomo.core.base.var logger = logging.getLogger('pyomo.solvers') @@ -410,7 +409,7 @@ def _init_env(self): else: # Ensure the (global) default env is started if not GurobiDirect._default_env_started: - m = gurobipy.Model() + m = GurobiModel() m.close() GurobiDirect._default_env_started = True From a6438bad0ddb80fb563c640084f3b8b2a78ba9f9 Mon Sep 17 00:00:00 2001 From: Jason Krizan <34923517+orthorhombic@users.noreply.github.com> Date: Sun, 18 Aug 2024 20:26:47 -0400 Subject: [PATCH 2305/3044] Change from %r to %s for NumPy 2.0 compatibility --- pyomo/repn/plugins/nl_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 8f51f0b0fba..0b24ff34245 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -2365,7 +2365,7 @@ class text_nl_debug_template(object): # NOTE: to support scaling and substitutions, we do NOT include the # 'v' or the EOL here: var = '%s' - const = 'n%r\n' + const = 'n%s\n' string = 'h%d:%s\n' monomial = product + const + var.replace('%', '%%') multiplier = product + const From 804ad0480d91105234246fac50fb02a98c7df54f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 19 Aug 2024 09:28:06 -0600 Subject: [PATCH 2306/3044] Moving quadratic repn visitor to instance attribute on nonlinear to pwl transformation --- pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index a35231dd890..588fa8298f6 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -66,12 +66,6 @@ class DomainPartitioningMethod(enum.IntEnum): LINEAR_MODEL_TREE_RANDOM = 4 -# This should be safe to use many times; declare it globally -_quadratic_repn_visitor = QuadraticRepnVisitor( - subexpression_cache={}, var_map={}, var_order={}, sorter=None -) - - class _NonlinearToPWLTransformationData(AutoSlots.Mixin): __slots__ = ( 'transformed_component', @@ -502,6 +496,9 @@ def __init__(self): } self._transformation_blocks = {} self._transformation_block_set = ComponentSet() + self._quadratic_repn_visitor = QuadraticRepnVisitor( + subexpression_cache={}, var_map={}, var_order={}, sorter=None + ) def _apply_to(self, instance, **kwds): try: @@ -630,7 +627,7 @@ def _get_bounds_list(self, var_list, obj): return bounds def _needs_approximating(self, expr, approximate_quadratic): - repn = _quadratic_repn_visitor.walk_expression(expr) + repn = self._quadratic_repn_visitor.walk_expression(expr) if repn.nonlinear is None: if repn.quadratic is None: # Linear constraint. Always skip. From 1ed95c231a37522274e0ded3c0ddaa6b1a1f5c5e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Aug 2024 10:47:14 -0600 Subject: [PATCH 2307/3044] NFC: documenting the AMPLRepn / DebugAMPLRepn classes --- pyomo/repn/ampl.py | 61 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/pyomo/repn/ampl.py b/pyomo/repn/ampl.py index 876f579fb8d..b1d963b9771 100644 --- a/pyomo/repn/ampl.py +++ b/pyomo/repn/ampl.py @@ -198,6 +198,58 @@ def name(self): class AMPLRepn(object): + """The "compiled" representation of an expression in AMPL NL format. + + This stores a compiled form of an expression in the AMPL "NL" + format. The data structure contains 6 fields: + + Attributes + ---------- + mult : float + + A constant multiplier applied to this expression. The + :py:class`AMPLRepn` returned by the :py:class`AMPLRepnVisitor` + should always have `mult` == 1. + + const : float + + The constant portion of this expression + + linear : Dict[int, float] or None + + Mapping of `id(VarData)` to linear coefficient + + nonlinear : Tuple[str, List[int]] or List[Tuple[str, List[int]]] or None + + The general nonlinear portion of the compiled expression as a + tuple of two parts: + - the nl template string: this is the NL string with + placeholders (`%s`) for all the variables that appear in + the expression. + - an iterable if the `VarData` IDs that correspond to the + placeholders in the nl template string + This is `None` if there is no general nonlinear part of the + expression. Note that this can be a list of tuple fragments + within AMPLRepnVisitor, but that list is concatenated to a + single tuple when exiting the `AMPLRepnVisitor`. + + named_exprs : Set[int] + + A set of IDs point to named expressions (:py:class:`Expression`) + objects appearing in this expression. + + nl : Tuple[str, Iterable[int]] + + This holds the complete compiled representation of this + expression (including multiplier, constant, linear terms, and + nonlinear fragment) using the same format as the `nonlinear` + attribute. This field (if not None) should be considered + authoritative, as there are NL fragments that are not + representable by {mult, const, linear, nonlinear} (e.g., string + arguments). + + """ + __slots__ = ('nl', 'mult', 'const', 'linear', 'nonlinear', 'named_exprs') template = TextNLTemplate @@ -445,6 +497,15 @@ def to_expr(self, var_map): class DebugAMPLRepn(AMPLRepn): + """An `AMPLRepn` that uses the "debug" (annotated) NL format + + This is identical to the :py:class:`AMPLRepn` class, except it is + built using the `TextNLDebugTemplate` formatting template. This + format includes descriptions of the operators and variable / + expression names in the NL text. + + """ + __slots__ = () template = TextNLDebugTemplate From ef0074ee8c26e8459664fd9066177fa589487065 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Aug 2024 11:44:19 -0600 Subject: [PATCH 2308/3044] Fix deprecation version Co-authored-by: Bethany Nicholson --- pyomo/repn/plugins/nl_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 6f340cc887f..2ed2ae82c61 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -74,7 +74,7 @@ logger = logging.getLogger(__name__) -relocated_module_attribute('AMPLRepn', 'pyomo.repn.ampl.AMPLRepn', version='6.4.0.dev0') +relocated_module_attribute('AMPLRepn', 'pyomo.repn.ampl.AMPLRepn', version='6.7.4.dev0') inf = float('inf') minus_inf = -inf From 6661d828f3f2572250153d4da6cabb99b6344e84 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Aug 2024 11:51:25 -0600 Subject: [PATCH 2309/3044] NFC: fix typo Co-authored-by: Bethany Nicholson --- pyomo/repn/ampl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/ampl.py b/pyomo/repn/ampl.py index b1d963b9771..9e043290b68 100644 --- a/pyomo/repn/ampl.py +++ b/pyomo/repn/ampl.py @@ -479,7 +479,7 @@ def append(self, other): def to_expr(self, var_map): if self.nl is not None or self.nonlinear is not None: - # TODO: support converting general nonlinear expressiosn + # TODO: support converting general nonlinear expressions # back to Pyomo expressions. This will require an AMPL # parser. raise MouseTrap("Cannot convert nonlinear AMPLRepn to Pyomo Expression") From f3a07ed6de0f847138458c6c3c45689289bc6fc9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 19 Aug 2024 14:07:43 -0600 Subject: [PATCH 2310/3044] Add test for #3352 --- pyomo/repn/tests/ampl/test_nlv2.py | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pyomo/repn/tests/ampl/test_nlv2.py b/pyomo/repn/tests/ampl/test_nlv2.py index 0bfdb22a2ba..35030caeb52 100644 --- a/pyomo/repn/tests/ampl/test_nlv2.py +++ b/pyomo/repn/tests/ampl/test_nlv2.py @@ -2761,6 +2761,38 @@ def test_nested_external_expressions(self): 0 0 1 0 2 0 +""", + OUT.getvalue(), + ) + ) + + @unittest.skipUnless(numpy_available, "test requires numpy") + def test_objective_numpy_const(self): + # This tests issue #3352 + m = ConcreteModel() + m.e = Expression(expr=numpy.float64(0)) + m.obj = Objective(expr=m.e) + + OUT = io.StringIO() + nl_writer.NLWriter().write(m, OUT, linear_presolve=False, scale_model=True) + self.assertEqual( + *nl_diff( + """g3 1 1 0 #problem unknown + 0 0 1 0 0 #vars, constraints, objectives, ranges, eqns + 0 0 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb + 0 0 #network constraints: nonlinear, linear + 0 0 0 #nonlinear vars in constraints, objectives, both + 0 0 0 1 #linear network variables; functions; arith, flags + 0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o) + 0 0 #nonzeros in Jacobian, obj. gradient + 0 0 #max name lengths: constraints, variables + 0 0 0 0 0 #common exprs: b,c,o,c1,o1 +O0 0 +n0 +x0 +r +b +k-1 """, OUT.getvalue(), ) From 5c9b6b33d373d8abd41534b8244b28f3862022e8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 19 Aug 2024 14:19:22 -0600 Subject: [PATCH 2311/3044] NFC: Reformatting comments --- .../contrib/piecewise/ordered_3d_j1_triangulation_data.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py index 009107fd6ec..283c64cb27f 100644 --- a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py +++ b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py @@ -82,14 +82,17 @@ def _get_double_cube_graph(): all_needed_hamiltonians = {} for i, s1 in border_simplices.items(): for j, s2 in border_simplices.items(): - # I could cut the number of these in half or less via symmetry but I don't care + # I could cut the number of these in half or less via symmetry but + # I don't care if i[0] != j[0]: if (i, (j[0], 1)) in all_needed_hamiltonians.keys() or ( i, (j[0], 2), ) in all_needed_hamiltonians.keys(): print( - f"skipping search for path from {i} to {j} because we have a path from {i} to {(j[0], 1) if (i, (j[0], 1)) in all_needed_hamiltonians.keys() else (j[0], 2)}" + f"skipping search for path from {i} to {j} because we have a " + f"path from {i} to {(j[0], 1) if (i, (j[0], 1)) in " + f"all_needed_hamiltonians.keys() else (j[0], 2)}" ) continue print(f"searching for path from {i} to {j}") From a477a02167d47714db451b0e790197e0609051a4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 19 Aug 2024 14:20:15 -0600 Subject: [PATCH 2312/3044] Moving direction Enum to module scope, rearranging --- pyomo/contrib/piecewise/triangulations.py | 40 ++++++++++++----------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 60d701a6be1..94c1129767a 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -26,6 +26,20 @@ class Triangulation(Enum): OrderedJ1 = 4 +# Duck-typed thing that looks reasonably similar to an instance of +# scipy.spatial.Delaunay +# Fields: +# - points: list of P points as P x n array +# - simplices: list of M simplices as P x (n + 1) array of point _indices_ +# - coplanar: list of N points omitted from triangulation as tuples of (point index, +# nearest simplex index, nearest vertex index), stacked into an N x 3 array +class _Triangulation: + def __init__(self, points, simplices, coplanar): + self.points = points + self.simplices = simplices + self.coplanar = coplanar + + # Get an unordered J1 triangulation, as described by [1], of a finite grid of # points in R^n having the same odd number of points along each axis. # References @@ -74,19 +88,6 @@ def get_ordered_j1_triangulation(points, dimension): ) -# Duck-typed thing that looks reasonably similar to an instance of scipy.spatial.Delaunay -# Fields: -# - points: list of P points as P x n array -# - simplices: list of M simplices as P x (n + 1) array of point _indices_ -# - coplanar: list of N points omitted from triangulation as tuples of (point index, -# nearest simplex index, nearest vertex index), stacked into an N x 3 array -class _Triangulation: - def __init__(self, points, simplices, coplanar): - self.points = points - self.simplices = simplices - self.coplanar = coplanar - - # Does some validation but mostly assumes the user did the right thing def _process_points_j1(points, dimension): if not len(points[0]) == dimension: @@ -144,6 +145,13 @@ def _get_j1_triangulation(points_map, K, n): return ret +class Direction(Enum): + left = 0 + down = 1 + up = 2 + right = 3 + + # Implement something similar to proof-by-picture from Todd 79 (Figure 1). # However, that drawing is misleading at best so I do it in a working way, and # also slightly more regularly. I also go from the outside in instead of from @@ -154,12 +162,6 @@ def _get_ordered_j1_triangulation_2d(points_map, num_pts): # check when we are in a "turnaround square" as seen in the picture is_turnaround = lambda x, y: x >= num_pts / 2 and y == (num_pts / 2) - 1 - class Direction(Enum): - left = 0 - down = 1 - up = 2 - right = 3 - facing = None simplices = [] From 233b016c4090909cb4a2292d422975f247f5377d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 19 Aug 2024 14:35:46 -0600 Subject: [PATCH 2313/3044] Emma learns what for-else does --- pyomo/contrib/piecewise/triangulations.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 94c1129767a..8eb16a87d86 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -504,8 +504,11 @@ def _fix_vertices_incremental_order(simplices): if simplex[n] in simplices[i + 1] and n != first: last = n break - if first is None or last is None: - raise DeveloperError("Couldn't fix vertex ordering for incremental.") + else: + # For the Python neophytes in the audience (and other sane + # people), the 'else' only runs if we do *not* break out of the + # for loop. + raise DeveloperError("Couldn't fix vertex ordering for incremental.") # reorder the simplex with the desired first and last new_simplex = list(simplex) From d1aa3c0d8fff7d5a016424ecf0e6f2b4a176939c Mon Sep 17 00:00:00 2001 From: Arnaud Baguet Date: Mon, 19 Aug 2024 21:31:02 -0400 Subject: [PATCH 2314/3044] implement jsiirola's suggestion --- .../solvers/plugins/solvers/gurobi_direct.py | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/gurobi_direct.py b/pyomo/solvers/plugins/solvers/gurobi_direct.py index 91fb107e557..1ed4c5e3505 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_direct.py +++ b/pyomo/solvers/plugins/solvers/gurobi_direct.py @@ -96,20 +96,6 @@ def _set_options(model_or_env, options): model_or_env.setParam(key, float(option)) -class GurobiModel(gurobipy.Model): - def __init__(self, *args, **kwds): - super().__init__(*args, **kwds) - - def addConstr(self, degree, lhs, sense=None, rhs=None, name=""): - if degree == 1: - con = self.addLConstr(lhs, sense, rhs, name) - elif degree == 2: - con = self.addQConstr(lhs, sense, rhs, name) - else: - raise DegreeError('GurobiModel.addConstr: Unsupported degree: %s' % degree) - return con - - @SolverFactory.register('gurobi_direct', doc='Direct python interface to Gurobi') class GurobiDirect(DirectSolver): """A direct interface to Gurobi using gurobipy. @@ -409,7 +395,7 @@ def _init_env(self): else: # Ensure the (global) default env is started if not GurobiDirect._default_env_started: - m = GurobiModel() + m = gurobipy.Model() m.close() GurobiDirect._default_env_started = True @@ -419,9 +405,9 @@ def _create_model(self, model): self._solver_model.close() self._solver_model = ( - GurobiModel(model.name, env=self._env) + gurobipy.Model(model.name, env=self._env) if model.name is not None - else GurobiModel(env=self._env) + else gurobipy.Model(env=self._env) ) def close(self): @@ -504,6 +490,15 @@ def _set_instance(self, model, kwds={}): def _add_block(self, block): DirectOrPersistentSolver._add_block(self, block) + def _addConstr(self, degree, lhs, sense=None, rhs=None, name=""): + if degree == 1: + con = self._solver_model.addLConstr(lhs, sense, rhs, name) + elif degree == 2: + con = self._solver_model.addQConstr(lhs, sense, rhs, name) + else: + raise DegreeError('GurobiModel.addConstr: Unsupported degree: %s' % degree) + return con + def _add_constraint(self, con): if not con.active: return None @@ -534,7 +529,7 @@ def _add_constraint(self, con): ) if con.equality: - gurobipy_con = self._solver_model.addConstr( + gurobipy_con = self._addConstr( degree=degree, lhs=gurobi_expr, sense=gurobipy.GRB.EQUAL, @@ -547,7 +542,7 @@ def _add_constraint(self, con): ) self._range_constraints.add(con) elif con.has_lb(): - gurobipy_con = self._solver_model.addConstr( + gurobipy_con = self._addConstr( degree=degree, lhs=gurobi_expr, sense=gurobipy.GRB.GREATER_EQUAL, @@ -555,7 +550,7 @@ def _add_constraint(self, con): name=conname, ) elif con.has_ub(): - gurobipy_con = self._solver_model.addConstr( + gurobipy_con = self._addConstr( degree=degree, lhs=gurobi_expr, sense=gurobipy.GRB.LESS_EQUAL, @@ -650,7 +645,7 @@ def _set_objective(self, obj): else: raise ValueError('Objective sense is not recognized: {0}'.format(obj.sense)) - gurobi_expr, referenced_vars = self._get_expr_from_pyomo_expr( + gurobi_expr, referenced_vars, degree = self._get_expr_from_pyomo_expr( obj.expr, self._max_obj_degree ) From 0004efab5252eeb610f1b778050359b64bc57061 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 19 Aug 2024 21:46:44 -0400 Subject: [PATCH 2315/3044] Fix new PyROS installation instructions --- doc/OnlineDocs/contributed_packages/pyros.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/OnlineDocs/contributed_packages/pyros.rst b/doc/OnlineDocs/contributed_packages/pyros.rst index 8b7bfac4f7e..92e13cf8779 100644 --- a/doc/OnlineDocs/contributed_packages/pyros.rst +++ b/doc/OnlineDocs/contributed_packages/pyros.rst @@ -136,13 +136,17 @@ PyROS can be installed as follows: pip install numpy scipy 3. (*Optional*) Test your installation: install ``pytest`` and ``parameterized`` - with your preferred package manager (as in the previous step). + with your preferred package manager (as in the previous step): + + :: + + pip install pytest parameterized + You may then run the PyROS tests as follows: :: - cd pyomo/contrib/pyros/tests - pytest + python -c 'import os, pytest, pyomo.contrib.pyros as p; pytest.main([os.path.dirname(p.__file__)])' Some tests involving solvers may fail or be skipped, depending on the solver distributions (e.g., Ipopt, BARON, SCIP) From b50a4a526518f742030137783b15b0fd3e0d35f2 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 19 Aug 2024 21:52:58 -0400 Subject: [PATCH 2316/3044] Add named constant for uncertainty set point checks --- pyomo/contrib/pyros/uncertainty_sets.py | 18 +++++++++++++----- pyomo/contrib/pyros/util.py | 1 + 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/pyros/uncertainty_sets.py b/pyomo/contrib/pyros/uncertainty_sets.py index eda1354d10f..378677b72e6 100644 --- a/pyomo/contrib/pyros/uncertainty_sets.py +++ b/pyomo/contrib/pyros/uncertainty_sets.py @@ -77,7 +77,11 @@ from pyomo.core.expr import mutable_expression, native_numeric_types, value from pyomo.core.util import quicksum, dot_product from pyomo.opt.results import check_optimal_termination -from pyomo.contrib.pyros.util import copy_docstring, standardize_component_data +from pyomo.contrib.pyros.util import ( + copy_docstring, + POINT_IN_UNCERTAINTY_SET_TOL, + standardize_component_data, +) valid_num_types = tuple(native_numeric_types) @@ -2126,7 +2130,7 @@ def point_in_set(self, point): True if the point lies in the set, False otherwise. """ aux_space_pt = self.compute_auxiliary_uncertain_param_vals(point) - tol = 1e-8 + tol = POINT_IN_UNCERTAINTY_SET_TOL return abs( aux_space_pt.sum() ) <= self.beta * self.number_of_factors + tol and np.all( @@ -2566,7 +2570,10 @@ def point_in_set(self, point): off_center @ np.linalg.inv(self.shape_matrix) @ off_center ) normalized_boundary_radius = np.sqrt(self.scale) - return normalized_pt_radius <= normalized_boundary_radius + 1e-8 + return ( + normalized_pt_radius + <= normalized_boundary_radius + POINT_IN_UNCERTAINTY_SET_TOL + ) @copy_docstring(UncertaintySet.set_as_constraint) def set_as_constraint(self, uncertain_params=None, block=None): @@ -2757,8 +2764,9 @@ def point_in_set(self, point): required_shape_qual="to match the set dimension", ) # Round all double precision to a tolerance - rounded_scenarios = np.round(self.scenarios, decimals=8) - rounded_point = np.round(point, decimals=8) + num_decimals = round(-np.log10(POINT_IN_UNCERTAINTY_SET_TOL)) + rounded_scenarios = np.round(self.scenarios, decimals=num_decimals) + rounded_point = np.round(point, decimals=num_decimals) return np.any(np.all(rounded_point == rounded_scenarios, axis=1)) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 71cd8d31d0b..05b134bc932 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -65,6 +65,7 @@ COEFF_MATCH_ABS_TOL = 0 ABS_CON_CHECK_FEAS_TOL = 1e-5 PRETRIANGULAR_VAR_COEFF_TOL = 1e-6 +POINT_IN_UNCERTAINTY_SET_TOL = 1e-8 TIC_TOC_SOLVE_TIME_ATTR = "pyros_tic_toc_time" DEFAULT_LOGGER_NAME = "pyomo.contrib.pyros" From ece5beb5dd205f15144c891dcacbefd4ea843a33 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 19 Aug 2024 22:23:11 -0400 Subject: [PATCH 2317/3044] Move new `VariableValueData` namedtuple to module scope --- pyomo/contrib/pyros/pyros_algorithm_methods.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 6317c308950..03e8890a13b 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -72,15 +72,16 @@ def _evaluate_shift(current, prev, initial, norm=None): return np.max(np.abs(current - prev) / normalizers) +VariableValueData = namedtuple( + "VariableValueData", + ("first_stage_variables", "second_stage_variables", "decision_rule_monomials"), +) + + def get_variable_value_data(working_blk, dr_var_to_monomial_map): """ Get variable value data. """ - VariableValueData = namedtuple( - "VariableValueData", - ("first_stage_variables", "second_stage_variables", "decision_rule_monomials"), - ) - ep = working_blk.effective_var_partitioning first_stage_data = ComponentMap( From 0615fca1d2f04337b1bea3acf0e2b878eb6acc08 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 19 Aug 2024 22:40:34 -0400 Subject: [PATCH 2318/3044] Update NL writer tolerance PyROS tests --- pyomo/contrib/pyros/tests/test_grcs.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 90e2d1cdbef..c09d3f76e1d 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -23,6 +23,7 @@ from pyomo.common.errors import InvalidValueError from pyomo.core.base.set_types import NonNegativeIntegers from pyomo.repn.plugins import nl_writer as pyomo_nl_writer +import pyomo.repn.ampl as pyomo_ampl_repn from pyomo.common.dependencies import numpy as np, numpy_available from pyomo.common.dependencies import scipy_available from pyomo.common.errors import ApplicationError, InfeasibleConstraintException @@ -1207,10 +1208,10 @@ def test_discrete_separation_invalid_value_error(self): ) @unittest.skipUnless(ipopt_available, "IPOPT is not available.") - def test_pyros_nl_writer_tol(self): + def test_pyros_nl_and_ampl_writer_tol(self): """ Test PyROS subsolver call routine behavior - with respect to the NL writer tolerance is as + with respect to the NL and AMPL writer tolerances is as expected. """ m = ConcreteModel() @@ -1222,7 +1223,7 @@ def test_pyros_nl_writer_tol(self): # fixed just inside the PyROS-specified NL writer tolerance. m.x1.fix(m.x1.upper + 9.9e-5) - current_nl_writer_tol = pyomo_nl_writer.TOL + current_nl_writer_tol = pyomo_nl_writer.TOL, pyomo_ampl_repn.TOL ipopt_solver = SolverFactory("ipopt") pyros_solver = SolverFactory("pyros") @@ -1240,12 +1241,12 @@ def test_pyros_nl_writer_tol(self): ) self.assertEqual( - pyomo_nl_writer.TOL, + (pyomo_nl_writer.TOL, pyomo_ampl_repn.TOL), current_nl_writer_tol, - msg="Pyomo NL writer tolerance not restored as expected.", + msg="Pyomo writer tolerances not restored as expected.", ) - # fixed just outside the PyROS-specified NL writer tolerance. + # fixed just outside the PyROS-specified writer tolerances. # this should be exceptional. m.x1.fix(m.x1.upper + 1.01e-4) @@ -1268,10 +1269,10 @@ def test_pyros_nl_writer_tol(self): ) self.assertEqual( - pyomo_nl_writer.TOL, + (pyomo_nl_writer.TOL, pyomo_ampl_repn.TOL), current_nl_writer_tol, msg=( - "Pyomo NL writer tolerance not restored as expected " + "Pyomo writer tolerances not restored as expected " "after exceptional test." ), ) From 6277fe444a8f43ae6f3b958324b6aceeedf86217 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 19 Aug 2024 22:46:37 -0400 Subject: [PATCH 2319/3044] Add new named constant for DR polishing tolerance --- pyomo/contrib/pyros/master_problem_methods.py | 4 +++- pyomo/contrib/pyros/util.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index ff43b9442c3..5616a5fa056 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -32,6 +32,7 @@ from pyomo.contrib.pyros.solve_data import MasterResults from pyomo.contrib.pyros.util import ( call_solver, + DR_POLISHING_PARAM_PRODUCT_ZERO_TOL, enforce_dr_degree, get_dr_expression, check_time_limit_reached, @@ -464,7 +465,8 @@ def construct_dr_polishing_problem(master_data): for scenario_blk in master_model.scenarios.values() ] all_copy_coeffs_zero = is_a_nonstatic_dr_term and all( - abs(value(prod(term.args[:-1]))) <= 1e-10 for term in dr_term_copies + abs(value(prod(term.args[:-1]))) <= DR_POLISHING_PARAM_PRODUCT_ZERO_TOL + for term in dr_term_copies ) if all_copy_coeffs_zero: # increment static DR variable value diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 05b134bc932..90ade286dee 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -66,6 +66,7 @@ ABS_CON_CHECK_FEAS_TOL = 1e-5 PRETRIANGULAR_VAR_COEFF_TOL = 1e-6 POINT_IN_UNCERTAINTY_SET_TOL = 1e-8 +DR_POLISHING_PARAM_PRODUCT_ZERO_TOL = 1e-10 TIC_TOC_SOLVE_TIME_ATTR = "pyros_tic_toc_time" DEFAULT_LOGGER_NAME = "pyomo.contrib.pyros" From e64143ddd27e738c623f9ef0c0c02d0de3ddf40c Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 19 Aug 2024 21:24:53 -0600 Subject: [PATCH 2320/3044] Update pyomo/core/base/initializer.py --- pyomo/core/base/initializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/initializer.py b/pyomo/core/base/initializer.py index 072b064d425..c15e26855ae 100644 --- a/pyomo/core/base/initializer.py +++ b/pyomo/core/base/initializer.py @@ -350,7 +350,7 @@ def __call__(self, parent, idx): class ParameterizedIndexedCallInitializer(IndexedCallInitializer): - """IndexedCallCallInitializer that accepts additional arguments""" + """IndexedCallInitializer that accepts additional arguments""" __slots__ = () From 8ff7384efe2b2973817cde9db9109b9d66672222 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 19 Aug 2024 23:22:05 -0600 Subject: [PATCH 2321/3044] Updating the CHANGELOG in preparation for the 6.8.0 release --- CHANGELOG.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d1d1e45e3d..36cd15e8daf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,58 @@ Pyomo CHANGELOG =============== +------------------------------------------------------------------------------- +Pyomo 6.8.0 (20 Aug 2024) +------------------------------------------------------------------------------- + +- General + - Add ParameterizedQuadraticRepn and corresponding walker (#3324) + - Update Pyomo for NumPy 2.0 compatibility (#3292, #3353) + - Add ParameterizedLinearRepn and corresponding walker (#3268) +- Core + - Handle uninitialized variable in `propagate_solution` of scaling + transformation (#3275) + - Add `context` option to `SuffixFinder` (#3348) + - Remove the `_suppress_ctypes` attribute from Block (#3347) + - Improve `Set` initialization performance (#3302) + - Update Constraint to only store the original expression (not + lower/body/upper) (#3293) + - Kernel: fix bug in conic geomean (#3310) + - Fix bug with IndexedSet objects and the within argument (#3288) +- Solver Interfaces + - Resolve NLv2 incompatibility with multithreading (#3332) + - Resolve writer performance degradation (#3343) + - Fix bug with inconsistent use of `result` and `results` (#3337) + - LegacySolverWrapper: restore 'options' attribute (#3334) + - Fix bug in XpressDirect._load_slacks (#3318) + - NLv2: support expressions with nested external functions (#3319) + - Ignore errors on ASL solver version check (#3298) + - Add SAS solver interface (#2886, #3309) +- Testing + - Omnibus testing / platform portability fixes (#3335) + - Change BARON download URL (#3328) + - Disable interface/testing for NEOS/octeract (#3322) + - Fix typo in Jenkins driver (#3312) + - Jenkins: update logic for recording SHA, repo owner, and branch name (#3311) + - Unpin Codecov / Update coverage (#3303) +- GDP + - Don't transform known-to-be infeasible Disjuncts in multiple BigM (#3314) +- Contributed Packages + - alternative_solutions: Add a new contrib package for generating + alternative solutions (#3270) + - APPSI: Allow maingo_solvermodel to be imported without maingopy (#3330) + - APPSI: Sort indices while removing constraints to fix bug in HiGHs + interface (#3281) + - CP: Add beforeChild handling for bools in logical expressions (#3315) + - DoE: Refactor to improve API and maintainability (#3317) + - incidence_analysis: Raise error in `generate_strongly_connected_components + instead of asserting (#3305) + - parmest: Add missing main call for example file (#3349) + - piecewise: Add nonlinear-to-piecewise-linear transformation (#3333) + - PyNumero: Support PyomoNLP scaling factors on sub-blocks (#3295) + - PyROS: Temporarily Adjust NL Writer Feasibility Tolerance (#3280) + - viewer: Add option to specify the model by variable name (#3271) + ------------------------------------------------------------------------------- Pyomo 6.7.3 (29 May 2024) ------------------------------------------------------------------------------- From 9329e9096908a54397f18dff709b14764c17aec6 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 19 Aug 2024 23:26:44 -0600 Subject: [PATCH 2322/3044] More updates to the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36cd15e8daf..4e24ef7dc94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Pyomo 6.8.0 (20 Aug 2024) lower/body/upper) (#3293) - Kernel: fix bug in conic geomean (#3310) - Fix bug with IndexedSet objects and the within argument (#3288) + - Support validate/filter for IndexedSet components using the index (#3338) - Solver Interfaces - Resolve NLv2 incompatibility with multithreading (#3332) - Resolve writer performance degradation (#3343) From ced0c8ca68dfc4d7cbbed1d2bdf5eab7f8738b83 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 20 Aug 2024 00:07:17 -0600 Subject: [PATCH 2323/3044] More edits to the CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e24ef7dc94..0df034426a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,9 @@ Pyomo 6.8.0 (20 Aug 2024) - incidence_analysis: Raise error in `generate_strongly_connected_components instead of asserting (#3305) - parmest: Add missing main call for example file (#3349) + - piecewise: Add incremental PW linear to MIP transformation (#3287) - piecewise: Add nonlinear-to-piecewise-linear transformation (#3333) + - PyNumero: Support user-provided CyIpopt callbacks with 13 arguments (#3289) - PyNumero: Support PyomoNLP scaling factors on sub-blocks (#3295) - PyROS: Temporarily Adjust NL Writer Feasibility Tolerance (#3280) - viewer: Add option to specify the model by variable name (#3271) From 5bfdcae1cd882e57721c2cfd90d21994551a355b Mon Sep 17 00:00:00 2001 From: Arnaud Baguet Date: Tue, 20 Aug 2024 06:44:08 -0400 Subject: [PATCH 2324/3044] fix unit tests after adding _addConstr --- pyomo/solvers/plugins/solvers/gurobi_direct.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/gurobi_direct.py b/pyomo/solvers/plugins/solvers/gurobi_direct.py index 1ed4c5e3505..9cd81ba8a55 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_direct.py +++ b/pyomo/solvers/plugins/solvers/gurobi_direct.py @@ -491,12 +491,10 @@ def _add_block(self, block): DirectOrPersistentSolver._add_block(self, block) def _addConstr(self, degree, lhs, sense=None, rhs=None, name=""): - if degree == 1: - con = self._solver_model.addLConstr(lhs, sense, rhs, name) - elif degree == 2: + if degree == 2: con = self._solver_model.addQConstr(lhs, sense, rhs, name) else: - raise DegreeError('GurobiModel.addConstr: Unsupported degree: %s' % degree) + con = self._solver_model.addLConstr(lhs, sense, rhs, name) return con def _add_constraint(self, con): From 4f9904d0f67d4fdf1be7cd23b7543b7686d019cc Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 08:14:01 -0600 Subject: [PATCH 2325/3044] Modify line-length --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0df034426a8..b193153fb6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ Pyomo 6.8.0 (20 Aug 2024) lower/body/upper) (#3293) - Kernel: fix bug in conic geomean (#3310) - Fix bug with IndexedSet objects and the within argument (#3288) - - Support validate/filter for IndexedSet components using the index (#3338) + - Support validate/filter for IndexedSet components using index (#3338) - Solver Interfaces - Resolve NLv2 incompatibility with multithreading (#3332) - Resolve writer performance degradation (#3343) @@ -35,7 +35,7 @@ Pyomo 6.8.0 (20 Aug 2024) - Change BARON download URL (#3328) - Disable interface/testing for NEOS/octeract (#3322) - Fix typo in Jenkins driver (#3312) - - Jenkins: update logic for recording SHA, repo owner, and branch name (#3311) + - Jenkins: update logic for recording variables (#3311) - Unpin Codecov / Update coverage (#3303) - GDP - Don't transform known-to-be infeasible Disjuncts in multiple BigM (#3314) @@ -47,7 +47,7 @@ Pyomo 6.8.0 (20 Aug 2024) interface (#3281) - CP: Add beforeChild handling for bools in logical expressions (#3315) - DoE: Refactor to improve API and maintainability (#3317) - - incidence_analysis: Raise error in `generate_strongly_connected_components + - incidence_analysis: Raise error in `generate_strongly_connected_components` instead of asserting (#3305) - parmest: Add missing main call for example file (#3349) - piecewise: Add incremental PW linear to MIP transformation (#3287) From 608a68d10a5e6d642bc9e80b969078c8f1787d59 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 08:43:00 -0600 Subject: [PATCH 2326/3044] No cythonization for 3.8-3.10 --- .github/workflows/release_wheel_creation.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 932b0d8eea6..f9d7568237e 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -14,9 +14,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true -env: - PYOMO_SETUP_ARGS: "--with-cython --with-distributable-extensions" - jobs: native_wheels: name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for native and cross-compiled architecture @@ -31,14 +28,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' + GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp39*' TARGET: 'py39' + GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp310*' TARGET: 'py310' + GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' + GLOBAL_OPTIONS: '' - wheel-version: 'cp312*' TARGET: 'py312' + GLOBAL_OPTIONS: '' steps: - uses: actions/checkout@v4 - name: Build wheels @@ -53,7 +55,7 @@ jobs: CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 - CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' + CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} @@ -72,14 +74,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' + GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp39*' TARGET: 'py39' + GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp310*' TARGET: 'py310' + GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' + GLOBAL_OPTIONS: '' - wheel-version: 'cp312*' TARGET: 'py312' + GLOBAL_OPTIONS: '' steps: - uses: actions/checkout@v4 - name: Set up QEMU @@ -97,7 +104,7 @@ jobs: CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 - CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' + CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} - uses: actions/upload-artifact@v4 with: name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From 76a670e5f275260bd5b2a5c9c26baf926f6420c1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 08:46:38 -0600 Subject: [PATCH 2327/3044] Change global options for 3.11/3.12 --- .github/workflows/release_wheel_creation.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index f9d7568237e..fe20b32cd29 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -37,10 +37,10 @@ jobs: GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: '' + GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: '' + GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' steps: - uses: actions/checkout@v4 - name: Build wheels @@ -83,10 +83,10 @@ jobs: GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: '' + GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: '' + GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' steps: - uses: actions/checkout@v4 - name: Set up QEMU From 4f1c31f494085a65c0aeae55b59f945302259d97 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:07:39 -0600 Subject: [PATCH 2328/3044] Turn on explicit PEP517 --- .github/workflows/release_wheel_creation.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index fe20b32cd29..cbb4203fcd6 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -28,19 +28,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' steps: - uses: actions/checkout@v4 - name: Build wheels @@ -74,19 +74,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' steps: - uses: actions/checkout@v4 - name: Set up QEMU From 1fc00b535cbf4f3fd9d39f4a5d362d0abedc871c Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:18:55 -0600 Subject: [PATCH 2329/3044] Change args --- .github/workflows/release_wheel_creation.yml | 22 +++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index cbb4203fcd6..7c364af01f5 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -28,19 +28,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' steps: - uses: actions/checkout@v4 - name: Build wheels @@ -54,6 +54,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 + CIBW_BUILD_FRONTEND: "pip; args: --use-pep517" CIBW_BEFORE_BUILD: pip install cython pybind11 CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} - uses: actions/upload-artifact@v4 @@ -74,19 +75,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' steps: - uses: actions/checkout@v4 - name: Set up QEMU @@ -103,6 +104,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 + CIBW_BUILD_FRONTEND: "pip; args: --use-pep517" CIBW_BEFORE_BUILD: pip install cython pybind11 CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} - uses: actions/upload-artifact@v4 From 234bd3032688674ec1cb9d80a3a220d5c0abfb2b Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:32:41 -0600 Subject: [PATCH 2330/3044] Downgrade pip to 23.0.1 --- .github/workflows/release_wheel_creation.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 7c364af01f5..f057fab8726 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -54,8 +54,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BUILD_FRONTEND: "pip; args: --use-pep517" - CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_BEFORE_BUILD: pip install pip==23.0.1 cython pybind11 CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} - uses: actions/upload-artifact@v4 with: @@ -104,8 +103,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BUILD_FRONTEND: "pip; args: --use-pep517" - CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_BEFORE_BUILD: pip install pip==23.0.1 cython pybind11 CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} - uses: actions/upload-artifact@v4 with: From 251939daa8ec2608f5300bf8a4cde43008129f04 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:42:10 -0600 Subject: [PATCH 2331/3044] Remove global options - attempting pyproject.toml --- .github/workflows/release_wheel_creation.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index f057fab8726..2ba1502e441 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -28,7 +28,7 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option=--with-cython --with-distributable-extensions"' - wheel-version: 'cp39*' TARGET: 'py39' GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' @@ -54,8 +54,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install pip==23.0.1 cython pybind11 - CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} + CIBW_BEFORE_BUILD: pip install cython pybind11 - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} @@ -103,8 +102,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install pip==23.0.1 cython pybind11 - CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} + CIBW_BEFORE_BUILD: pip install cython pybind11 - uses: actions/upload-artifact@v4 with: name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From f84c7cf8438d9780523a3dac9342d1139646aedc Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 09:42:31 -0600 Subject: [PATCH 2332/3044] Add pyproject.toml --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000000..5d2d6b43154 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[tool.cibuildwheel.config-settings] +--build-option = "--with-distributable-extensions" +--build-option = "--with-cython" + From efe30f1f188a0a108ec9915630461b5b5850ebbc Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 09:46:15 -0600 Subject: [PATCH 2333/3044] Remove pyproject.toml --- pyproject.toml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 5d2d6b43154..00000000000 --- a/pyproject.toml +++ /dev/null @@ -1,4 +0,0 @@ -[tool.cibuildwheel.config-settings] ---build-option = "--with-distributable-extensions" ---build-option = "--with-cython" - From 7868b04950561dceb06d68d0d8aabab6faa4c6df Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:51:33 -0600 Subject: [PATCH 2334/3044] Use global var instead - PIP_GLOBAL_OPTION --- .github/workflows/release_wheel_creation.yml | 26 ++++++++++++-------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 2ba1502e441..00931c253c1 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -28,21 +28,24 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: ' --global-option=--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 + - name: Set up global variable + run: | + echo "PIP_GLOBAL_OPTION=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 with: @@ -73,21 +76,24 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' + PIP_GLOBAL_OPTION: "--without-cython --with-distributable-extensions" - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' + PIP_GLOBAL_OPTION: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 + - name: Set up global variable + run: | + echo "PIP_GLOBAL_OPTION=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Set up QEMU if: runner.os == 'Linux' uses: docker/setup-qemu-action@v3 From de0fe4fc4c085fb1c7c607a26667c6b4009f7600 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:56:12 -0600 Subject: [PATCH 2335/3044] Set in action instead --- .github/workflows/release_wheel_creation.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 00931c253c1..c2b749817f1 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -43,9 +43,6 @@ jobs: GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 - - name: Set up global variable - run: | - echo "PIP_GLOBAL_OPTION=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 with: @@ -57,6 +54,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 + CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" CIBW_BEFORE_BUILD: pip install cython pybind11 - uses: actions/upload-artifact@v4 with: @@ -91,9 +89,6 @@ jobs: PIP_GLOBAL_OPTION: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 - - name: Set up global variable - run: | - echo "PIP_GLOBAL_OPTION=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Set up QEMU if: runner.os == 'Linux' uses: docker/setup-qemu-action@v3 @@ -108,6 +103,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 + CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" CIBW_BEFORE_BUILD: pip install cython pybind11 - uses: actions/upload-artifact@v4 with: From 7183c47340a5c7405cebc9d19a47594cc9eb9658 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:00:02 -0600 Subject: [PATCH 2336/3044] Change order on env vars --- .github/workflows/release_wheel_creation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index c2b749817f1..64581db39af 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -54,8 +54,8 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} @@ -103,8 +103,8 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" - uses: actions/upload-artifact@v4 with: name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From a82eb9e43e4486a79b8a65b1593dc77387c314e9 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:03:15 -0600 Subject: [PATCH 2337/3044] Move env declaration to BEFORE_BUILD --- .github/workflows/release_wheel_creation.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 64581db39af..097d9d3994e 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -54,8 +54,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install cython pybind11 - CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" + CIBW_BEFORE_BUILD: pip install cython pybind11 && PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} @@ -103,8 +102,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install cython pybind11 - CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" + CIBW_BEFORE_BUILD: pip install cython pybind11 && PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" - uses: actions/upload-artifact@v4 with: name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From 597ccecb351be4f685823f5b46378397248e22c2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:07:49 -0600 Subject: [PATCH 2338/3044] Echo into GITHUB_ENV --- .github/workflows/release_wheel_creation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 097d9d3994e..ccc78a52aec 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -54,7 +54,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install cython pybind11 && PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" + CIBW_BEFORE_BUILD: pip install cython pybind11 && echo 'PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}"' >> $GITHUB_ENV - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} @@ -102,7 +102,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install cython pybind11 && PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" + CIBW_BEFORE_BUILD: pip install cython pybind11 && echo 'PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}"' >> $GITHUB_ENV - uses: actions/upload-artifact@v4 with: name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From a0a37911fb65faf791d4c2297efc212b1d1a60d2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:13:30 -0600 Subject: [PATCH 2339/3044] Introduce PYOMO_SETUP_ARGS instead --- .github/workflows/release_wheel_creation.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index ccc78a52aec..93574896f59 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -43,6 +43,9 @@ jobs: GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 + - name: Set up global variable + run: | + echo "PYOMO_SETUP_ARGS=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 with: @@ -54,7 +57,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install cython pybind11 && echo 'PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}"' >> $GITHUB_ENV + CIBW_BEFORE_BUILD: pip install cython pybind11 - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} @@ -82,12 +85,15 @@ jobs: GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp311*' TARGET: 'py311' - PIP_GLOBAL_OPTION: "--without-cython --with-distributable-extensions" + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" - wheel-version: 'cp312*' TARGET: 'py312' - PIP_GLOBAL_OPTION: "--without-cython --with-distributable-extensions" + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 + - name: Set up global variable + run: | + echo "PYOMO_SETUP_ARGS=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Set up QEMU if: runner.os == 'Linux' uses: docker/setup-qemu-action@v3 @@ -102,7 +108,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install cython pybind11 && echo 'PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}"' >> $GITHUB_ENV + CIBW_BEFORE_BUILD: pip install cython pybind11 - uses: actions/upload-artifact@v4 with: name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From 5675db07cb289a56afcb802b904a687ee7ad3070 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Aug 2024 10:17:46 -0600 Subject: [PATCH 2340/3044] Reqork setup configuration to support arguments and PYOMO_SETUP_ARGS environment --- setup.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/setup.py b/setup.py index b1f4a60c4c6..735c2734594 100644 --- a/setup.py +++ b/setup.py @@ -53,19 +53,30 @@ def get_version(): return import_pyomo_module('pyomo', 'version', 'info.py')['__version__'] +def check_config_arg(name): + if name in sys.argv: + sys.argv.remove(name) + return True + if name in os.getenv('PYOMO_SETUP_ARGS', "").split(): + return True + return False + + CYTHON_REQUIRED = "required" if not any( - arg.startswith(cmd) for cmd in ('build', 'install', 'bdist') for arg in sys.argv + arg.startswith(cmd) + for cmd in ('build', 'install', 'bdist', 'wheel') + for arg in sys.argv ): using_cython = False -else: +elif sys.version_info[:2] < (3, 11): using_cython = "automatic" -if '--with-cython' in sys.argv: +else: + using_cython = False +if check_config_arg('--with-cython'): using_cython = CYTHON_REQUIRED - sys.argv.remove('--with-cython') -if '--without-cython' in sys.argv: +if check_config_arg('--without-cython'): using_cython = False - sys.argv.remove('--without-cython') ext_modules = [] if using_cython: @@ -107,14 +118,7 @@ def get_version(): raise using_cython = False -if ('--with-distributable-extensions' in sys.argv) or ( - os.getenv('PYOMO_SETUP_ARGS') is not None - and '--with-distributable-extensions' in os.getenv('PYOMO_SETUP_ARGS') -): - try: - sys.argv.remove('--with-distributable-extensions') - except: - pass +if check_config_arg('--with-distributable-extensions'): # # Import the APPSI extension builder # NOTE: There is inconsistent behavior in Windows for APPSI. From 02d01bbf81f0956d0f16228f264abd4628603fc9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Aug 2024 10:33:32 -0600 Subject: [PATCH 2341/3044] debugging --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 735c2734594..071d673c5e6 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,9 @@ def check_config_arg(name): sys.argv.remove(name) return True if name in os.getenv('PYOMO_SETUP_ARGS', "").split(): + print(f"FOUND {name}") return True + print(f"NOT FOUND {name}") return False @@ -134,6 +136,7 @@ def check_config_arg(name): ) ext_modules.append(appsi_extension) +print(f"\nEXTENSIONS: {ext_modules}\n") class DependenciesCommand(Command): """Custom setuptools command From 1f3d749f55ec7bc52af9ba05aba62649f2ef05ec Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Aug 2024 10:38:21 -0600 Subject: [PATCH 2342/3044] More debugging --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 071d673c5e6..193dc229233 100644 --- a/setup.py +++ b/setup.py @@ -54,9 +54,12 @@ def get_version(): def check_config_arg(name): + print(f"SEARCHING FOR '{name}' in '{sys.argv}'") if name in sys.argv: sys.argv.remove(name) return True + print(f"SEARCHING FOR '{name}' in '{os.getenv('PYOMO_SETUP_ARGS', "").split()}'") + print(" ", os.getenv('PYOMO_SETUP_ARGS', "")) if name in os.getenv('PYOMO_SETUP_ARGS', "").split(): print(f"FOUND {name}") return True From a258d7d82c6b8f36d86a98f4810bc370af2f3d1a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Aug 2024 10:40:25 -0600 Subject: [PATCH 2343/3044] Fix typo --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 193dc229233..30429a55f70 100644 --- a/setup.py +++ b/setup.py @@ -58,9 +58,9 @@ def check_config_arg(name): if name in sys.argv: sys.argv.remove(name) return True - print(f"SEARCHING FOR '{name}' in '{os.getenv('PYOMO_SETUP_ARGS', "").split()}'") + print(f"SEARCHING FOR '{name}' in '{os.getenv('PYOMO_SETUP_ARGS', '').split()}'") print(" ", os.getenv('PYOMO_SETUP_ARGS', "")) - if name in os.getenv('PYOMO_SETUP_ARGS', "").split(): + if name in os.getenv('PYOMO_SETUP_ARGS', '').split(): print(f"FOUND {name}") return True print(f"NOT FOUND {name}") From 8b7edf85c4100b46d221d80786f4ac447266195b Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:45:20 -0600 Subject: [PATCH 2344/3044] Move PYOMO_SETUP_ARGS to CIWB_ENVIRONMENT --- .github/workflows/release_wheel_creation.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 93574896f59..fad91c72ccc 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -43,9 +43,6 @@ jobs: GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 - - name: Set up global variable - run: | - echo "PYOMO_SETUP_ARGS=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 with: @@ -58,6 +55,7 @@ jobs: CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_ENVIRONMENT: PYOMO_SETUP_ARGS="${{ matrix.GLOBAL_OPTIONS }}" - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} @@ -91,9 +89,6 @@ jobs: GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 - - name: Set up global variable - run: | - echo "PYOMO_SETUP_ARGS=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Set up QEMU if: runner.os == 'Linux' uses: docker/setup-qemu-action@v3 @@ -109,6 +104,7 @@ jobs: CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_ENVIRONMENT: PYOMO_SETUP_ARGS="${{ matrix.GLOBAL_OPTIONS }}" - uses: actions/upload-artifact@v4 with: name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From 39fdfde30ce8e3c7cbe5f99692e3903d60e5efd5 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 20 Aug 2024 12:49:48 -0400 Subject: [PATCH 2345/3044] Add new enum for specification of bound types --- pyomo/contrib/pyros/util.py | 57 +++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 90ade286dee..7bc81a97033 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -989,6 +989,17 @@ def preprocess(self, user_var_partitioning): return preprocess_model_data(self, user_var_partitioning) +class BoundType: + """ + Indicator for whether a bound on a variable/constraint + is a lower bound, "equality" bound, or upper bound. + """ + + LOWER = "lower" + EQ = "eq" + UPPER = "upper" + + def get_var_bound_pairs(var): """ Get the domain and declared lower/upper @@ -1038,7 +1049,7 @@ def determine_certain_and_uncertain_bound( Declared bound. uncertain_params : iterable of ParamData Uncertain model parameters. - bound_type : {'lower', 'upper'} + bound_type : {BoundType.LOWER, BoundType.UPPER} Indication of whether the domain bound and declared bound specify lower or upper bounds for the variable value. @@ -1049,9 +1060,10 @@ def determine_certain_and_uncertain_bound( uncertain_bound : numeric expression or None Bound that is dependent on the uncertain parameters. """ - if bound_type not in {"lower", "upper"}: + if bound_type not in {BoundType.LOWER, BoundType.UPPER}: raise ValueError( - f"Argument {bound_type=!r} should be either 'lower' or 'upper'." + f"Argument {bound_type=!r} should be either " + f"'{BoundType.LOWER}' or '{BoundType.UPPER}'." ) if declared_bound is not None: @@ -1069,7 +1081,7 @@ def determine_certain_and_uncertain_bound( elif domain_bound is None: certain_bound = declared_bound else: - if bound_type == "lower": + if bound_type == BoundType.LOWER: certain_bound = ( declared_bound if value(declared_bound) >= domain_bound @@ -1088,7 +1100,9 @@ def determine_certain_and_uncertain_bound( return certain_bound, uncertain_bound -BoundTriple = namedtuple("BoundTriple", ("lower", "eq", "upper")) +BoundTriple = namedtuple( + "BoundTriple", (BoundType.LOWER, BoundType.EQ, BoundType.UPPER) +) def rearrange_bound_pair_to_triple(lower_bound, upper_bound): @@ -1158,13 +1172,13 @@ def get_var_certain_uncertain_bounds(var, uncertain_params): domain_bound=domain_lb, declared_bound=declared_lb, uncertain_params=uncertain_params, - bound_type="lower", + bound_type=BoundType.LOWER, ) certain_ub, uncertain_ub = determine_certain_and_uncertain_bound( domain_bound=domain_ub, declared_bound=declared_ub, uncertain_params=uncertain_params, - bound_type="upper", + bound_type=BoundType.UPPER, ) certain_bounds = rearrange_bound_pair_to_triple( @@ -1397,7 +1411,7 @@ def create_bound_constraint_expr(expr, bound, bound_type, standardize=True): bound : native numeric type or NumericValue Bound for `expr`. This should be a numeric constant, Param, or constant/mutable Pyomo expression. - bound_type : {'lower', 'eq', 'upper'} + bound_type : BoundType Indicator for whether `expr` is to be lower bounded, equality bounded, or upper bounded, by `bound`. standardize : bool, optional @@ -1409,11 +1423,11 @@ def create_bound_constraint_expr(expr, bound, bound_type, standardize=True): RelationalExpression Establishes a bound on `expr`. """ - if bound_type == "lower": + if bound_type == BoundType.LOWER: return -expr <= -bound if standardize else bound <= expr - elif bound_type == "eq": + elif bound_type == BoundType.EQ: return expr == bound - elif bound_type == "upper": + elif bound_type == BoundType.UPPER: return expr <= bound else: raise ValueError(f"Bound type {bound_type!r} not supported.") @@ -1427,22 +1441,23 @@ def remove_var_declared_bound(var, bound_type): ---------- var : VarData Variable data object of interest. - bound_type : {'lower', 'eq', 'upper'} + bound_type : BoundType Indicator for the declared bound(s) to remove. - Note: if 'eq' is specified, then both the + Note: if BoundType.EQ is specified, then both the lower and upper bounds are removed. """ - if bound_type == "lower": + if bound_type == BoundType.LOWER: var.setlb(None) - elif bound_type == "eq": + elif bound_type == BoundType.EQ: var.setlb(None) var.setub(None) - elif bound_type == "upper": + elif bound_type == BoundType.UPPER: var.setub(None) else: raise ValueError( f"Bound type {bound_type!r} not supported. " - "Bound type must be 'lower', 'eq, or 'upper'." + f"Bound type must be '{BoundType.LOWER}', " + f"'{BoundType.EQ}, or '{BoundType.UPPER}'." ) @@ -1490,7 +1505,7 @@ def turn_nonadjustable_var_bounds_to_constraints(model_data): new_con_expr = create_bound_constraint_expr(var, bound, btype) new_con_name = f"var_{var_name}_uncertain_{btype}_bound_con" remove_var_declared_bound(var, btype) - if btype == "eq": + if btype == BoundType.EQ: working_model.second_stage.equality_cons[new_con_name] = ( new_con_expr ) @@ -1549,7 +1564,7 @@ def turn_adjustable_var_bounds_to_constraints(model_data): if bound is not None: new_con_name = f"var_{var_name}_{certainty_desc}_{btype}_bound_con" new_con_expr = create_bound_constraint_expr(var, bound, btype) - if btype == "eq": + if btype == BoundType.EQ: working_model.second_stage.equality_cons[new_con_name] = ( new_con_expr ) @@ -1678,7 +1693,7 @@ def standardize_inequality_constraints(model_data): if bd is not None } for btype, bound in finite_bounds.items(): - if btype == "eq": + if btype == BoundType.EQ: # no equality bounds should be identified here. # equality bound may be identified if: # 1. bound rearrangement method has a bug @@ -2211,7 +2226,7 @@ def reformulate_state_var_independent_eq_cons(model_data): # in the separation problems, since the effective DOF # variables and DR variables are fixed. # hence, we reformulate to inequalities - for bound_type in ["lower", "upper"]: + for bound_type in [BoundType.LOWER, BoundType.UPPER]: std_con_expr = create_bound_constraint_expr( expr=con.body, bound=con.upper, bound_type=bound_type ) From 10508b6bacec170677ff70ff468d8e95fe7e023a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Aug 2024 10:51:25 -0600 Subject: [PATCH 2346/3044] Remove debugging --- setup.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/setup.py b/setup.py index 30429a55f70..63c6891bda2 100644 --- a/setup.py +++ b/setup.py @@ -54,16 +54,11 @@ def get_version(): def check_config_arg(name): - print(f"SEARCHING FOR '{name}' in '{sys.argv}'") if name in sys.argv: sys.argv.remove(name) return True - print(f"SEARCHING FOR '{name}' in '{os.getenv('PYOMO_SETUP_ARGS', '').split()}'") - print(" ", os.getenv('PYOMO_SETUP_ARGS', "")) if name in os.getenv('PYOMO_SETUP_ARGS', '').split(): - print(f"FOUND {name}") return True - print(f"NOT FOUND {name}") return False @@ -139,7 +134,6 @@ def check_config_arg(name): ) ext_modules.append(appsi_extension) -print(f"\nEXTENSIONS: {ext_modules}\n") class DependenciesCommand(Command): """Custom setuptools command From 9b92cb4e01ce9809b4b78d13c1a96d46a9a4562b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 10:55:41 -0600 Subject: [PATCH 2347/3044] Add skip test for win 3.11, 3.12 --- .github/workflows/release_wheel_creation.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 93574896f59..11c52b5fd38 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -58,6 +58,7 @@ jobs: CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_TEST_SKIP: "*{311,312}-win*" - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From 6af5d7d9a1fccd06e0719cecb0d97eef03f0385a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:08:13 -0600 Subject: [PATCH 2348/3044] Create two pure-python wheels --- .github/workflows/release_wheel_creation.yml | 29 +++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index e03082e964a..5af870da9b6 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -15,13 +15,40 @@ concurrency: cancel-in-progress: true jobs: + pure-python: + name: Build pure wheels (${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + python-version: ['3.11', '3.12'] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install twine wheel setuptools pybind11 + - name: Build generic tarball + run: | + python setup.py --without-cython sdist --format=gztar bdist_wheel + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: purepython_wheel-${{ matrix.python-version }} + path: dist/*.whl + overwrite: true + native_wheels: name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for native and cross-compiled architecture runs-on: ${{ matrix.os }} strategy: fail-fast: true matrix: - os: [ubuntu-22.04, windows-latest, macos-latest] + os: [ubuntu-22.04, windows-latest, macos-13] arch: [all] wheel-version: ['cp38*', 'cp39*', 'cp310*', 'cp311*', 'cp312*'] From 44da84e64d20f020879752a2b8f22712362ddaeb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:09:04 -0600 Subject: [PATCH 2349/3044] Fix typo --- .github/workflows/release_wheel_creation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 5af870da9b6..6013437f5d4 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -15,7 +15,7 @@ concurrency: cancel-in-progress: true jobs: - pure-python: + pure_python: name: Build pure wheels (${{ matrix.python-version }}) runs-on: ubuntu-latest strategy: From 75d4f718d41b47d11692970337a86371527138f1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:11:05 -0600 Subject: [PATCH 2350/3044] YAML incorrectness --- .github/workflows/release_wheel_creation.yml | 38 ++++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 6013437f5d4..a903a91e077 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -22,25 +22,25 @@ jobs: fail-fast: true matrix: python-version: ['3.11', '3.12'] - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install twine wheel setuptools pybind11 - - name: Build generic tarball - run: | - python setup.py --without-cython sdist --format=gztar bdist_wheel - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: purepython_wheel-${{ matrix.python-version }} - path: dist/*.whl - overwrite: true + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install twine wheel setuptools pybind11 + - name: Build generic tarball + run: | + python setup.py --without-cython sdist --format=gztar bdist_wheel + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: purepython_wheel-${{ matrix.python-version }} + path: dist/*.whl + overwrite: true native_wheels: name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for native and cross-compiled architecture From 80c2dd60e36adc8f3e298115c23eb1e2c35d2a02 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 20 Aug 2024 13:11:17 -0400 Subject: [PATCH 2351/3044] Substitute `ParameterizedQuadaraticRepnVisitor` for standard repn --- .../contrib/pyros/tests/test_preprocessor.py | 12 +-- pyomo/contrib/pyros/util.py | 75 ++++++++++--------- 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index 5268ebc9dfa..144c3644862 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -1950,17 +1950,17 @@ def test_coefficient_matching_correct_constraints_added(self): assertExpressionsEqual( self, first_stage_eq_cons["coeff_matching_eq_con_coeff_1"].expr, - 2.5 + m.x1 + (-5) * (m.x1 * m.x2) + m.x1**3 == 0, + m.x1 ** 3 + 0.5 + 5 * m.x1 * m.x2 * (-1) + (-1) * (m.x1 + 2) * (-1) == 0, ) assertExpressionsEqual( self, first_stage_eq_cons["coeff_matching_eq_con_coeff_2"].expr, - (-1) + m.x2 == 0, + m.x2 - 1 == 0, ) assertExpressionsEqual( self, first_stage_eq_cons["coeff_matching_eq_con_2_coeff_1"].expr, - (-1) + m.x2 == 0, + m.x2 - 1 == 0, ) def test_reformulate_nonlinear_state_var_independent_eq_con(self): @@ -2050,7 +2050,7 @@ def test_reformulate_nonlinear_state_var_independent_eq_con(self): assertExpressionsEqual( self, wm.first_stage.equality_cons["coeff_matching_eq_con_2_coeff_1"].expr, - (-1) + m.x1 == 0, + m.x1 - 1 == 0, ) # separation priorities were also updated @@ -2578,7 +2578,7 @@ def test_preprocessor_coefficient_matching( assertExpressionsEqual( self, fs_eqs["coeff_matching_var_z5_uncertain_eq_bound_con_coeff_1"].expr, - -1 + fs.decision_rule_vars[1][1] == 0, + fs.decision_rule_vars[1][1] - 1 == 0, ) assertExpressionsEqual( self, @@ -2612,7 +2612,7 @@ def test_preprocessor_coefficient_matching( assertExpressionsEqual( self, fs_eqs["coeff_matching_var_z5_uncertain_eq_bound_con_coeff_1"].expr, - -1 + fs.decision_rule_vars[1][1] == 0, + fs.decision_rule_vars[1][1] - 1 == 0, ) assertExpressionsEqual( self, diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 7bc81a97033..58595ff3e35 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -52,7 +52,7 @@ ) from pyomo.core.util import prod from pyomo.opt import SolverFactory, TerminationCondition as tc -from pyomo.repn.standard_repn import generate_standard_repn +from pyomo.repn.parameterized_quadratic import ParameterizedQuadraticRepnVisitor import pyomo.repn.plugins.nl_writer as pyomo_nl_writer import pyomo.repn.ampl as pyomo_ampl_repn from pyomo.util.vars_from_expressions import get_vars_from_components @@ -989,6 +989,25 @@ def preprocess(self, user_var_partitioning): return preprocess_model_data(self, user_var_partitioning) +def setup_quadratic_expression_visitor( + wrt, + subexpression_cache=None, + var_map=None, + var_order=None, + sorter=None, +): + """Setup a parameterized quadratic expression walker.""" + visitor = ParameterizedQuadraticRepnVisitor( + subexpression_cache={} if subexpression_cache is None else subexpression_cache, + var_map={} if var_map is None else var_map, + var_order={} if var_order is None else var_order, + sorter=sorter, + wrt=wrt, + ) + visitor.expand_nonlinear_products = True + return visitor + + class BoundType: """ Indicator for whether a bound on a variable/constraint @@ -1299,23 +1318,16 @@ def get_effective_var_partitioning(model_data): # tolerance. if len(adj_vars_in_con) == 1: adj_var_in_con = next(iter(adj_vars_in_con)) - ccon_expr_repn = generate_standard_repn( - expr=ccon.body - ccon.upper, quadratic=False, compute_values=True + visitor = setup_quadratic_expression_visitor(wrt=[]) + ccon_expr_repn = visitor.walk_expression(expr=ccon.body - ccon.upper) + adj_var_appears_linearly = ( + adj_var_in_con + not in ComponentSet(identify_variables(ccon_expr_repn.nonlinear)) + and id(adj_var_in_con) in ComponentSet(ccon_expr_repn.linear) ) - adj_var_appears_linearly = adj_var_in_con not in ComponentSet( - ccon_expr_repn.nonlinear_vars - ) and adj_var_in_con in ComponentSet(ccon_expr_repn.linear_vars) if adj_var_appears_linearly: - # get coefficient by summation just in case - # standard repn does not simplify completely - var_linear_coeff = sum( - lcoeff - for lvar, lcoeff in zip( - ccon_expr_repn.linear_vars, ccon_expr_repn.linear_coefs - ) - if lvar is adj_var_in_con - ) - if abs(var_linear_coeff) > PRETRIANGULAR_VAR_COEFF_TOL: + adj_var_linear_coeff = ccon_expr_repn.linear[id(adj_var_in_con)] + if abs(adj_var_linear_coeff) > PRETRIANGULAR_VAR_COEFF_TOL: new_pretriangular_con_var_map[ccon] = adj_var_in_con config.progress_logger.debug( f" The variable {adj_var_in_con.name!r} is " @@ -2197,18 +2209,10 @@ def reformulate_state_var_independent_eq_cons(model_data): # uncertain parameters only. thus, only the proxy # variables for the uncertain parameters are unfixed # during the analysis - for var in originally_unfixed_vars: - var.fix() - expr_repn = generate_standard_repn( - expr=con_expr_after_all_substitutions, compute_values=False - ) - - # ensure state of every variable remains unchanged - # when done - for var in originally_unfixed_vars: - var.unfix() + visitor = setup_quadratic_expression_visitor(wrt=originally_unfixed_vars) + expr_repn = visitor.walk_expression(con_expr_after_all_substitutions) - if expr_repn.nonlinear_expr is not None: + if expr_repn.nonlinear is not None: config.progress_logger.debug( f"Equality constraint {con.name!r} " "is state-variable independent, but cannot be written " @@ -2241,26 +2245,25 @@ def reformulate_state_var_independent_eq_cons(model_data): else: polynomial_repn_coeffs = ( [expr_repn.constant] - + list(expr_repn.linear_coefs) - + list(expr_repn.quadratic_coefs) + + list(expr_repn.linear.values()) + + ( + [] if expr_repn.quadratic is None + else list(expr_repn.quadratic.values()) + ) ) for coeff_idx, coeff_expr in enumerate(polynomial_repn_coeffs): - simplified_coeff_expr = generate_standard_repn( - expr=coeff_expr, compute_values=True - ).to_expression() - # for robust satisfaction of the original equality # constraint, all polynomial coefficients must be # equal to zero. so for each coefficient, # we either check for trivial robust # feasibility/infeasibility, or add a constraint # restricting the coefficient expression to value 0 - if isinstance(simplified_coeff_expr, tuple(native_types)): + if isinstance(coeff_expr, tuple(native_types)): # coefficient is a constant; # check value to determine # trivial feasibility/infeasibility robust_infeasible = not math.isclose( - a=simplified_coeff_expr, + a=coeff_expr, b=0, rel_tol=COEFF_MATCH_REL_TOL, abs_tol=COEFF_MATCH_ABS_TOL, @@ -2288,7 +2291,7 @@ def reformulate_state_var_independent_eq_cons(model_data): # and DR variables. add matching constraint new_con_name = f"coeff_matching_{con_idx}_coeff_{coeff_idx}" working_model.first_stage.equality_cons[new_con_name] = ( - simplified_coeff_expr == 0 + coeff_expr == 0 ) new_con = working_model.first_stage.equality_cons[new_con_name] coefficient_matching_cons.append(new_con) From 96b4edee9fd491cc64387782e8e7ad4417a54609 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 20 Aug 2024 13:12:23 -0400 Subject: [PATCH 2352/3044] Apply black --- pyomo/contrib/pyros/tests/test_preprocessor.py | 2 +- pyomo/contrib/pyros/util.py | 17 ++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_preprocessor.py b/pyomo/contrib/pyros/tests/test_preprocessor.py index 144c3644862..582930f97d8 100644 --- a/pyomo/contrib/pyros/tests/test_preprocessor.py +++ b/pyomo/contrib/pyros/tests/test_preprocessor.py @@ -1950,7 +1950,7 @@ def test_coefficient_matching_correct_constraints_added(self): assertExpressionsEqual( self, first_stage_eq_cons["coeff_matching_eq_con_coeff_1"].expr, - m.x1 ** 3 + 0.5 + 5 * m.x1 * m.x2 * (-1) + (-1) * (m.x1 + 2) * (-1) == 0, + m.x1**3 + 0.5 + 5 * m.x1 * m.x2 * (-1) + (-1) * (m.x1 + 2) * (-1) == 0, ) assertExpressionsEqual( self, diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 58595ff3e35..fb3e2e5ed7e 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -990,11 +990,7 @@ def preprocess(self, user_var_partitioning): def setup_quadratic_expression_visitor( - wrt, - subexpression_cache=None, - var_map=None, - var_order=None, - sorter=None, + wrt, subexpression_cache=None, var_map=None, var_order=None, sorter=None ): """Setup a parameterized quadratic expression walker.""" visitor = ParameterizedQuadraticRepnVisitor( @@ -1320,11 +1316,9 @@ def get_effective_var_partitioning(model_data): adj_var_in_con = next(iter(adj_vars_in_con)) visitor = setup_quadratic_expression_visitor(wrt=[]) ccon_expr_repn = visitor.walk_expression(expr=ccon.body - ccon.upper) - adj_var_appears_linearly = ( - adj_var_in_con - not in ComponentSet(identify_variables(ccon_expr_repn.nonlinear)) - and id(adj_var_in_con) in ComponentSet(ccon_expr_repn.linear) - ) + adj_var_appears_linearly = adj_var_in_con not in ComponentSet( + identify_variables(ccon_expr_repn.nonlinear) + ) and id(adj_var_in_con) in ComponentSet(ccon_expr_repn.linear) if adj_var_appears_linearly: adj_var_linear_coeff = ccon_expr_repn.linear[id(adj_var_in_con)] if abs(adj_var_linear_coeff) > PRETRIANGULAR_VAR_COEFF_TOL: @@ -2247,7 +2241,8 @@ def reformulate_state_var_independent_eq_cons(model_data): [expr_repn.constant] + list(expr_repn.linear.values()) + ( - [] if expr_repn.quadratic is None + [] + if expr_repn.quadratic is None else list(expr_repn.quadratic.values()) ) ) From 8e7003a9767f7eedb432900e386a31b7010cbdc4 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:14:02 -0600 Subject: [PATCH 2353/3044] Skip windows on 3.11, 3.12 --- .github/workflows/release_wheel_creation.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index a903a91e077..3b984b4f0c7 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -78,11 +78,10 @@ jobs: CIBW_ARCHS_LINUX: "native" CIBW_ARCHS_MACOS: "native arm64" CIBW_ARCHS_WINDOWS: "native ARM64" - CIBW_SKIP: "*-musllinux*" + CIBW_SKIP: "*-musllinux* *{311,312}-win*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 - CIBW_TEST_SKIP: "*{311,312}-win*" CIBW_ENVIRONMENT: PYOMO_SETUP_ARGS="${{ matrix.GLOBAL_OPTIONS }}" - uses: actions/upload-artifact@v4 with: From 0047b9b07553a247cd07e2855135d3fe4d1773e0 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:19:41 -0600 Subject: [PATCH 2354/3044] Turn off universal; change regex for win --- .github/workflows/release_wheel_creation.yml | 2 +- setup.cfg | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 3b984b4f0c7..1f95a06e670 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -78,7 +78,7 @@ jobs: CIBW_ARCHS_LINUX: "native" CIBW_ARCHS_MACOS: "native arm64" CIBW_ARCHS_WINDOWS: "native ARM64" - CIBW_SKIP: "*-musllinux* *{311,312}-win*" + CIBW_SKIP: "cp{311,312}-win32 cp{311,312}-win_amd64 cp{311,312}-win_arm64 *-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 diff --git a/setup.cfg b/setup.cfg index f670cef8f68..5b6214d40ab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,6 @@ [metadata] license_files = LICENSE.md -[bdist_wheel] -universal=1 - [tool:pytest] filterwarnings = ignore::RuntimeWarning junit_family = xunit2 From 1fd14660e1e9ed18782a7c607c9fa1c83d59acfe Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:27:04 -0600 Subject: [PATCH 2355/3044] Use exclude tag instead --- .github/workflows/release_wheel_creation.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 1f95a06e670..b3818b98d44 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: true matrix: - python-version: ['3.11', '3.12'] + python-version: ['3.11'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -68,6 +68,13 @@ jobs: - wheel-version: 'cp312*' TARGET: 'py312' GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" + + exclude: + - wheel-version: 'cp311*' + os: windows-latest + - wheel-version: 'cp312*' + os: windows-latest + steps: - uses: actions/checkout@v4 - name: Build wheels @@ -78,8 +85,8 @@ jobs: CIBW_ARCHS_LINUX: "native" CIBW_ARCHS_MACOS: "native arm64" CIBW_ARCHS_WINDOWS: "native ARM64" - CIBW_SKIP: "cp{311,312}-win32 cp{311,312}-win_amd64 cp{311,312}-win_arm64 *-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} + CIBW_SKIP: "*-musllinux*" CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 CIBW_ENVIRONMENT: PYOMO_SETUP_ARGS="${{ matrix.GLOBAL_OPTIONS }}" @@ -127,8 +134,8 @@ jobs: output-dir: dist env: CIBW_ARCHS_LINUX: "aarch64" - CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} + CIBW_SKIP: "*-musllinux*" CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 CIBW_ENVIRONMENT: PYOMO_SETUP_ARGS="${{ matrix.GLOBAL_OPTIONS }}" From 6093f91930de9250cabedbf1fdda9a621bb3c5cc Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:34:41 -0600 Subject: [PATCH 2356/3044] Reduce repeat code --- .github/workflows/release_wheel_creation.yml | 76 ++++++++++---------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index b3818b98d44..7fe3811dc00 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -14,33 +14,11 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +env: + WITH_CYTHON: "--with-cython --with-distributable-extensions" + WITHOUT_CYTHON: "--without-cython --with-distributable-extensions" + jobs: - pure_python: - name: Build pure wheels (${{ matrix.python-version }}) - runs-on: ubuntu-latest - strategy: - fail-fast: true - matrix: - python-version: ['3.11'] - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install twine wheel setuptools pybind11 - - name: Build generic tarball - run: | - python setup.py --without-cython sdist --format=gztar bdist_wheel - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: purepython_wheel-${{ matrix.python-version }} - path: dist/*.whl - overwrite: true native_wheels: name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for native and cross-compiled architecture @@ -55,19 +33,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} exclude: - wheel-version: 'cp311*' @@ -108,19 +86,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} steps: - uses: actions/checkout@v4 - name: Set up QEMU @@ -145,6 +123,32 @@ jobs: path: dist/*.whl overwrite: true + pure_python: + name: Build pure wheels (${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.11'] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install twine wheel setuptools pybind11 + - name: Build pure python wheel + run: | + python setup.py --without-cython sdist --format=gztar bdist_wheel + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: purepython_wheel-${{ matrix.python-version }} + path: dist/*.whl + overwrite: true + generictarball: name: ${{ matrix.TARGET }} runs-on: ${{ matrix.os }} From f1132e618f461dfafe4aae316b3d851595f6bf59 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:36:06 -0600 Subject: [PATCH 2357/3044] Not 'env' --- .github/workflows/release_wheel_creation.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 7fe3811dc00..436f1126bd1 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -33,19 +33,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} + GLOBAL_OPTIONS: $WITH_CYTHON - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} + GLOBAL_OPTIONS: $WITH_CYTHON - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} + GLOBAL_OPTIONS: $WITH_CYTHON - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} + GLOBAL_OPTIONS: $WITHOUT_CYTHON - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} + GLOBAL_OPTIONS: $WITHOUT_CYTHON exclude: - wheel-version: 'cp311*' @@ -86,19 +86,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} + GLOBAL_OPTIONS: $WITH_CYTHON - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} + GLOBAL_OPTIONS: $WITH_CYTHON - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} + GLOBAL_OPTIONS: $WITH_CYTHON - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} + GLOBAL_OPTIONS: $WITHOUT_CYTHON - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} + GLOBAL_OPTIONS: $WITHOUT_CYTHON steps: - uses: actions/checkout@v4 - name: Set up QEMU From 2e9746c3e29ecb376579bd5f39eef57e1573b520 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:38:20 -0600 Subject: [PATCH 2358/3044] Quotes around var --- .github/workflows/release_wheel_creation.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 436f1126bd1..61441e80dbe 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -33,19 +33,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: $WITH_CYTHON + GLOBAL_OPTIONS: "$WITH_CYTHON" - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: $WITH_CYTHON + GLOBAL_OPTIONS: "$WITH_CYTHON" - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: $WITH_CYTHON + GLOBAL_OPTIONS: "$WITH_CYTHON" - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: $WITHOUT_CYTHON + GLOBAL_OPTIONS: "$WITHOUT_CYTHON" - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: $WITHOUT_CYTHON + GLOBAL_OPTIONS: "$WITHOUT_CYTHON" exclude: - wheel-version: 'cp311*' @@ -86,19 +86,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: $WITH_CYTHON + GLOBAL_OPTIONS: "$WITH_CYTHON" - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: $WITH_CYTHON + GLOBAL_OPTIONS: "$WITH_CYTHON" - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: $WITH_CYTHON + GLOBAL_OPTIONS: "$WITH_CYTHON" - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: $WITHOUT_CYTHON + GLOBAL_OPTIONS: "$WITHOUT_CYTHON" - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: $WITHOUT_CYTHON + GLOBAL_OPTIONS: "$WITHOUT_CYTHON" steps: - uses: actions/checkout@v4 - name: Set up QEMU From d612cf65b8e2ba2dc311e41cf283032abf03cbc7 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:41:21 -0600 Subject: [PATCH 2359/3044] Revert env changes; rename pure python area --- .github/workflows/release_wheel_creation.yml | 28 +++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 61441e80dbe..da17978a4a3 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -14,10 +14,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true -env: - WITH_CYTHON: "--with-cython --with-distributable-extensions" - WITHOUT_CYTHON: "--without-cython --with-distributable-extensions" - jobs: native_wheels: @@ -33,19 +29,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: "$WITH_CYTHON" + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: "$WITH_CYTHON" + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: "$WITH_CYTHON" + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: "$WITHOUT_CYTHON" + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: "$WITHOUT_CYTHON" + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" exclude: - wheel-version: 'cp311*' @@ -86,19 +82,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: "$WITH_CYTHON" + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: "$WITH_CYTHON" + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: "$WITH_CYTHON" + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: "$WITHOUT_CYTHON" + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: "$WITHOUT_CYTHON" + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 - name: Set up QEMU @@ -124,7 +120,7 @@ jobs: overwrite: true pure_python: - name: Build pure wheels (${{ matrix.python-version }}) + name: pure_python_wheel runs-on: ubuntu-latest strategy: matrix: @@ -145,7 +141,7 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: purepython_wheel-${{ matrix.python-version }} + name: purepythonwheel path: dist/*.whl overwrite: true From cee756c0796ce636dfa378f5bfc7ea4e07b41822 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 12:03:42 -0600 Subject: [PATCH 2360/3044] Explicitly state the different archs --- .github/workflows/release_wheel_creation.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index da17978a4a3..d439dafaf0a 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-22.04, windows-latest, macos-13] + os: [ubuntu-22.04, windows-latest, macos-latest] arch: [all] wheel-version: ['cp38*', 'cp39*', 'cp310*', 'cp311*', 'cp312*'] @@ -57,8 +57,8 @@ jobs: output-dir: dist env: CIBW_ARCHS_LINUX: "native" - CIBW_ARCHS_MACOS: "native arm64" - CIBW_ARCHS_WINDOWS: "native ARM64" + CIBW_ARCHS_MACOS: "x86_64 arm64" + CIBW_ARCHS_WINDOWS: "AMD64 ARM64" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_SKIP: "*-musllinux*" CIBW_BUILD_VERBOSITY: 1 From ee36b450f80cd016991808103dc999909daa69af Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 12:24:00 -0600 Subject: [PATCH 2361/3044] Update for 6.8.0 --- .coin-or/projDesc.xml | 4 +-- CHANGELOG.md | 8 ++++++ RELEASE.md | 27 ++++++++----------- pyomo/contrib/doe/__init__.py | 6 ++--- .../pynumero/interfaces/cyipopt_interface.py | 6 ++--- pyomo/core/base/set.py | 4 +-- pyomo/repn/plugins/nl_writer.py | 2 +- pyomo/version/info.py | 8 +++--- 8 files changed, 34 insertions(+), 31 deletions(-) diff --git a/.coin-or/projDesc.xml b/.coin-or/projDesc.xml index d13ac8804cf..8a5a9e0a7df 100644 --- a/.coin-or/projDesc.xml +++ b/.coin-or/projDesc.xml @@ -227,8 +227,8 @@ Carl D. Laird, Chair, Pyomo Management Committee, claird at andrew dot cmu dot e Use explicit overrides to disable use of automated version reporting. --> - 6.7.3 - 6.7.3 + 6.8.0 + 6.8.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index b193153fb6e..5d16105d24b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,18 @@ Pyomo CHANGELOG Pyomo 6.8.0 (20 Aug 2024) ------------------------------------------------------------------------------- +SIGNIFICANT CHANGE NOTICE + +- Internal data storage for Constraint objects (see #3293) +- No longer release cythonized wheel for Python 3.11+ (see #3355) + +CHANGELOG + - General - Add ParameterizedQuadraticRepn and corresponding walker (#3324) - Update Pyomo for NumPy 2.0 compatibility (#3292, #3353) - Add ParameterizedLinearRepn and corresponding walker (#3268) + - Update Release Process Workflow for changes in `pip` (#3355) - Core - Handle uninitialized variable in `propagate_solution` of scaling transformation (#3275) diff --git a/RELEASE.md b/RELEASE.md index e42469cbad5..ec1e532cbcc 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,24 +1,19 @@ -We are pleased to announce the release of Pyomo 6.7.3. +We are pleased to announce the release of Pyomo 6.8.0. Pyomo is a collection of Python software packages that supports a diverse set of optimization capabilities for formulating and analyzing optimization models. -The following are highlights of the 6.7 release series: - - - Added support for Python 3.12 - - Removed support for Python 3.7 - - New writer for converting linear models to matrix form - - Improved handling of nested GDPs - - Redesigned user API for parameter estimation - - New packages: - - iis: new capability for identifying minimal intractable systems - - latex_printer: print Pyomo models to a LaTeX compatible format - - contrib.solver: preview of redesigned solver interfaces - - simplification: simplify Pyomo expressions - - New solver interfaces - - MAiNGO: Mixed-integer nonlinear global optimization - - ...and of course numerous minor bug fixes and performance enhancements +The following are highlights of the 6.8 release series: + +- Support for Numpy2 +- Refactor of Design of Experiments (`contrib.doe`) +- New packages: + - alternative_solutions: alternative (near) optimal solutions +- New solver interfaces: + - SAS: Statistical Analysis System + - v2: Ongoing solver interface refactor +- ...and of course numerous minor bug fixes and performance enhancements A full list of updates and changes is available in the [`CHANGELOG.md`](https://github.com/Pyomo/pyomo/blob/main/CHANGELOG.md). diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index ffb6df1a860..ef1207deab2 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -25,7 +25,7 @@ @deprecated( "Use of MeasurementVariables in Pyomo.DoE is no longer supported.", - version='6.7.4.dev0', + version='6.8.0', ) class MeasurementVariables: def __init__(self, *args): @@ -33,7 +33,7 @@ def __init__(self, *args): @deprecated( - "Use of DesignVariables in Pyomo.DoE is no longer supported.", version='6.7.4.dev0' + "Use of DesignVariables in Pyomo.DoE is no longer supported.", version='6.8.0' ) class DesignVariables: def __init__(self, *args): @@ -41,7 +41,7 @@ def __init__(self, *args): @deprecated( - "Use of ModelOptionLib in Pyomo.DoE is no longer supported.", version='6.7.4.dev0' + "Use of ModelOptionLib in Pyomo.DoE is no longer supported.", version='6.8.0' ) class ModelOptionLib: def __init__(self, *args): diff --git a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py index 5187efadac9..98916e11b48 100644 --- a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py +++ b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py @@ -310,7 +310,7 @@ def __init__(self, nlp, intermediate_callback=None, halt_on_evaluation_error=Non # cyipopt.Problem.__init__ super(CyIpoptNLP, self).__init__() - # Pre-Pyomo 6.7.4.dev0, we had no way to pass the cyipopt.Problem object + # Pre-Pyomo 6.8.0, we had no way to pass the cyipopt.Problem object # to the user in an intermediate callback. This prevented them from calling # the useful get_current_iterate and get_current_violations methods. Now, # we support this by adding the Problem object to the args we pass to a user's @@ -496,7 +496,7 @@ def intermediate( """ if self._intermediate_callback is not None: if self._use_13arg_callback: - # This is the callback signature expected as of Pyomo 6.7.4.dev0 + # This is the callback signature expected as of Pyomo 6.8.0 return self._intermediate_callback( self._nlp, self, @@ -513,7 +513,7 @@ def intermediate( ls_trials, ) else: - # This is the callback signature expected pre-Pyomo 6.7.4.dev0 and + # This is the callback signature expected pre-Pyomo 6.8.0 and # is supported for backwards compatibility. return self._intermediate_callback( self._nlp, diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 964e83a4bba..69b21c4d78b 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1489,7 +1489,7 @@ def _cb_validate_filter(self, mode, val_iter): "callback signature matched (block, *value). " "Please update the callback to match the signature " f"(block, value{', *index' if comp.is_indexed() else ''}).", - version='6.7.4.dev0', + version='6.8.0', ) orig_fcn = fcn._fcn fcn = ParameterizedScalarCallInitializer( @@ -1512,7 +1512,7 @@ def _cb_validate_filter(self, mode, val_iter): "callback signature matched (block, *value, *index). " "Please update the callback to match the signature " "(block, value, *index).", - version='6.7.4.dev0', + version='6.8.0', ) if fcn.__class__ is not ParameterizedInitializer: orig_fcn = fcn._fcn diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index b4b3e018493..2fcb7679df1 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -74,7 +74,7 @@ logger = logging.getLogger(__name__) -relocated_module_attribute('AMPLRepn', 'pyomo.repn.ampl.AMPLRepn', version='6.7.4.dev0') +relocated_module_attribute('AMPLRepn', 'pyomo.repn.ampl.AMPLRepn', version='6.8.0') inf = float('inf') minus_inf = -inf diff --git a/pyomo/version/info.py b/pyomo/version/info.py index 825483a70a0..95f7e89a729 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -25,10 +25,10 @@ # should generally be left at 0, unless a downstream package is tracking # main and needs a hard reference to "suitably new" development. major = 6 -minor = 7 -micro = 4 -releaselevel = 'invalid' -# releaselevel = 'final' +minor = 8 +micro = 0 +# releaselevel = 'invalid' +releaselevel = 'final' serial = 0 if releaselevel == 'final': From feef6d905588f5dbd4624ec7094a3641e542d865 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 12:32:16 -0600 Subject: [PATCH 2362/3044] Apply black --- pyomo/contrib/doe/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index ef1207deab2..154b52124d3 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -24,8 +24,7 @@ @deprecated( - "Use of MeasurementVariables in Pyomo.DoE is no longer supported.", - version='6.8.0', + "Use of MeasurementVariables in Pyomo.DoE is no longer supported.", version='6.8.0' ) class MeasurementVariables: def __init__(self, *args): From 94c20ae02346339d76287a05d3478634bdf32500 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 20 Aug 2024 13:03:08 -0600 Subject: [PATCH 2363/3044] Reset main for development (6.8.1.dev0) --- pyomo/version/info.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/version/info.py b/pyomo/version/info.py index 95f7e89a729..858e90f8c77 100644 --- a/pyomo/version/info.py +++ b/pyomo/version/info.py @@ -26,9 +26,9 @@ # main and needs a hard reference to "suitably new" development. major = 6 minor = 8 -micro = 0 -# releaselevel = 'invalid' -releaselevel = 'final' +micro = 1 +releaselevel = 'invalid' +# releaselevel = 'final' serial = 0 if releaselevel == 'final': From a68b28051d3b23a601cda2d6c564e4f4dbb7d5c3 Mon Sep 17 00:00:00 2001 From: Daniel Laky Date: Tue, 20 Aug 2024 16:50:48 -0400 Subject: [PATCH 2364/3044] Bugfix get_FIM Hardcoded values have been changed. --- pyomo/contrib/doe/doe.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/doe/doe.py b/pyomo/contrib/doe/doe.py index 627eae3d3eb..5f3151961fb 100644 --- a/pyomo/contrib/doe/doe.py +++ b/pyomo/contrib/doe/doe.py @@ -53,6 +53,7 @@ class ObjectiveLib(Enum): determinant = "determinant" trace = "trace" + minimum_eigenvalue = "minimum_eigenvalue" zero = "zero" @@ -2043,8 +2044,8 @@ def get_FIM(self, model=None): # FIM is a lower triangular matrix for the optimal DoE problem. # Exploit symmetry to fill in the zeros. - for i in range(4): - for j in range(4): + for i in range(len(model.parameter_names)): + for j in range(len(model.parameter_names)): if j < i: fim_np[j, i] = fim_np[i, j] From e7597fe89b722a3e7a0b75df39b3aa407116d55a Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 20 Aug 2024 17:32:22 -0400 Subject: [PATCH 2365/3044] Use SCIP for nonlinear coeff matching test --- pyomo/contrib/pyros/tests/test_grcs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index c09d3f76e1d..def7e7fe031 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -1747,7 +1747,7 @@ def test_coefficient_matching_robust_infeasible_proof_in_pyros(self): msg="Robust infeasible problem not identified via coefficient matching.", ) - @unittest.skipUnless(baron_license_is_valid, "BARON solver license is invalid.") + @unittest.skipUnless(scip_license_is_valid, "SCIP solver not licensed.") def test_coefficient_matching_nonlinear_expr(self): """ Test behavior of PyROS solver for model with @@ -1776,8 +1776,8 @@ def test_coefficient_matching_nonlinear_expr(self): pyros_solver = SolverFactory("pyros") # Define subsolvers utilized in the algorithm - local_subsolver = SolverFactory('baron') - global_subsolver = SolverFactory("baron") + local_subsolver = SolverFactory("scip") + global_subsolver = SolverFactory("scip") # Call the PyROS solver with LoggingIntercept(module="pyomo.contrib.pyros", level=logging.DEBUG) as LOG: From 6dce762c86103aa868e50dd0b99ca3bcec370362 Mon Sep 17 00:00:00 2001 From: Arnaud Baguet Date: Tue, 20 Aug 2024 22:21:46 -0400 Subject: [PATCH 2366/3044] fix laxy constraint issue after changing _get_expr_from_pyomo_expr --- pyomo/solvers/plugins/solvers/gurobi_persistent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/gurobi_persistent.py b/pyomo/solvers/plugins/solvers/gurobi_persistent.py index 94a2ac6b734..447d1de9b40 100644 --- a/pyomo/solvers/plugins/solvers/gurobi_persistent.py +++ b/pyomo/solvers/plugins/solvers/gurobi_persistent.py @@ -578,7 +578,7 @@ def cbCut(self, con): if is_fixed(con.body): raise ValueError('cbCut expected a non-trivial constraint') - gurobi_expr, referenced_vars = self._get_expr_from_pyomo_expr( + gurobi_expr, referenced_vars, degree = self._get_expr_from_pyomo_expr( con.body, self._max_constraint_degree ) @@ -656,7 +656,7 @@ def cbLazy(self, con): if is_fixed(con.body): raise ValueError('cbLazy expected a non-trivial constraint') - gurobi_expr, referenced_vars = self._get_expr_from_pyomo_expr( + gurobi_expr, referenced_vars, degree = self._get_expr_from_pyomo_expr( con.body, self._max_constraint_degree ) From fcb66e1b75f8e2c86aedb9ccd2499823e26da2b9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 22 Aug 2024 08:08:46 -0600 Subject: [PATCH 2367/3044] NFC: spellcheck --- pyomo/repn/plugins/standard_form.py | 2 +- pyomo/repn/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index 273d192d7bf..ffad2d62a95 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -574,7 +574,7 @@ def _create_csc(self, data, index, index_ptr, nnz, nCol): # The empty CSC has no (or few) rows and a large number of # columns and no nonzeros: it is faster / easier to create # the empty CSR on the python side and convert it to CSC on - # the C (numpy) side, as opposed to ceating the large [0] * + # the C (numpy) side, as opposed to creating the large [0] * # (nCol + 1) array on the Python side and transfer it to C # (numpy) return self._csr_matrix( diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index fd51eaacea3..32ea21abe2c 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -800,7 +800,7 @@ def var_order(self): def add(self, var): # Note: the following is mostly a copy of # LinearBeforeChildDispatcher.record_var, but with extra - # hanlding to update the env in the same loop + # handling to update the env in the same loop var_comp = var.parent_component() # Double-check that the component has not already been processed # (through an individual var data) From 33cd61d98da0e03f5a98a04a7cd944cd65e859fd Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 22 Aug 2024 08:47:50 -0600 Subject: [PATCH 2368/3044] Move URL Checker to Weekly Job --- .github/workflows/test_branches.yml | 14 ----------- .github/workflows/test_pr_and_main.yml | 15 ------------ .github/workflows/url_check.yml | 32 ++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/url_check.yml diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index a4f2f8128e9..e687a8bfb92 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -47,20 +47,6 @@ jobs: uses: crate-ci/typos@master with: config: ./.github/workflows/typos.toml - - name: URL Checker - uses: urlstechie/urlchecker-action@0.0.34 - with: - # A comma-separated list of file types to cover in the URL checks - file_types: .md,.rst,.py - # Choose whether to include file with no URLs in the prints. - print_all: false - # More verbose summary at the end of a run - verbose: true - # How many times to retry a failed request (defaults to 1) - retry_count: 3 - # Exclude Jenkins because it's behind a firewall; ignore RTD because - # a magically-generated string is triggering a failure - exclude_urls: https://pyomo-jenkins.sandia.gov/,https://pyomo.readthedocs.io/en/%s/errors.html build: diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 2ca7e166fd8..1aefe02687b 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -57,21 +57,6 @@ jobs: uses: crate-ci/typos@master with: config: ./.github/workflows/typos.toml - - name: URL Checker - uses: urlstechie/urlchecker-action@0.0.34 - with: - # A comma-separated list of file types to cover in the URL checks - file_types: .md,.rst,.py - # Choose whether to include file with no URLs in the prints. - print_all: false - # More verbose summary at the end of a run - verbose: true - # How many times to retry a failed request (defaults to 1) - retry_count: 3 - # Exclude: - # - Jenkins because it's behind a firewall - # - RTD because a magically-generated string triggers failures - exclude_urls: https://pyomo-jenkins.sandia.gov/,https://pyomo.readthedocs.io/en/%s/errors.html build: diff --git a/.github/workflows/url_check.yml b/.github/workflows/url_check.yml new file mode 100644 index 00000000000..797574574b4 --- /dev/null +++ b/.github/workflows/url_check.yml @@ -0,0 +1,32 @@ +name: URL Validation + +on: + schedule: + - cron: '0 3 * * 0' + workflow_dispatch: + inputs: + git-ref: + description: Git Hash (Optional) + required: false + +jobs: + url_check: + runs-on: ubuntu-latest + steps: + - name: Checkout Pyomo source + uses: actions/checkout@v4 + - name: URL Checker + uses: urlstechie/urlchecker-action@0.0.34 + with: + # A comma-separated list of file types to cover in the URL checks + file_types: .md,.rst,.py + # Choose whether to include file with no URLs in the prints. + print_all: false + # More verbose summary at the end of a run + verbose: true + # How many times to retry a failed request (defaults to 1) + retry_count: 3 + # Exclude: + # - Jenkins because it's behind a firewall + # - RTD because a magically-generated string triggers failures + exclude_urls: https://pyomo-jenkins.sandia.gov/,https://pyomo.readthedocs.io/en/%s/errors.html From c5f0ecbec387e663cf094a3fe85d01fafadf0223 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 22 Aug 2024 08:50:35 -0600 Subject: [PATCH 2369/3044] Add a 'Do not delete' Disclaimer to Templates --- .github/ISSUE_TEMPLATE/bug_report.md | 2 ++ .github/ISSUE_TEMPLATE/feature_request.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index a6b1df3cf9a..36c80e33baa 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,6 +4,8 @@ about: Report a bug in Pyomo (command not working as expected, etc.) labels: "bug" --- + + ## Summary + ## Summary From 5c6d3742c93e8f7188e2bbe78c5e2196029a11d8 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 22 Aug 2024 08:51:10 -0600 Subject: [PATCH 2370/3044] Add disclaimer to PR template --- .github/PULL_REQUEST_TEMPLATE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4bd8e88bfed..adf798b23ea 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,6 +7,8 @@ + + ## Fixes # . ## Summary/Motivation: From 0e15adc6176c720a0ab329f83bb05eeaae5694c7 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 22 Aug 2024 11:52:14 -0600 Subject: [PATCH 2371/3044] Clarify to not delete or ignore --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 36c80e33baa..61a09df7258 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,7 @@ about: Report a bug in Pyomo (command not working as expected, etc.) labels: "bug" --- - + ## Summary diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 76bcd918e1f..f1caebde2f8 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -5,7 +5,7 @@ labels: enhancement --- - + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index adf798b23ea..5ab3eb16ed9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -7,7 +7,7 @@ - + ## Fixes # . From 4ee8e91246d94ceabaf3dc9fef4afa26e8951c2a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 22 Aug 2024 11:53:21 -0600 Subject: [PATCH 2372/3044] Add URL checking back into branch jobs because it doesn't cause problems there --- .github/workflows/test_branches.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index e687a8bfb92..a4f2f8128e9 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -47,6 +47,20 @@ jobs: uses: crate-ci/typos@master with: config: ./.github/workflows/typos.toml + - name: URL Checker + uses: urlstechie/urlchecker-action@0.0.34 + with: + # A comma-separated list of file types to cover in the URL checks + file_types: .md,.rst,.py + # Choose whether to include file with no URLs in the prints. + print_all: false + # More verbose summary at the end of a run + verbose: true + # How many times to retry a failed request (defaults to 1) + retry_count: 3 + # Exclude Jenkins because it's behind a firewall; ignore RTD because + # a magically-generated string is triggering a failure + exclude_urls: https://pyomo-jenkins.sandia.gov/,https://pyomo.readthedocs.io/en/%s/errors.html build: From e2cf4fa120d143cf6531dc332630a2c56fb6803c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 22 Aug 2024 16:16:04 -0600 Subject: [PATCH 2373/3044] Add error checking (wrt cannot be None) --- pyomo/repn/parameterized_linear.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/repn/parameterized_linear.py b/pyomo/repn/parameterized_linear.py index e4c7a4ce628..b09a74abff0 100644 --- a/pyomo/repn/parameterized_linear.py +++ b/pyomo/repn/parameterized_linear.py @@ -343,6 +343,8 @@ def __init__( var_recorder=None, ): super().__init__(subexpression_cache, var_map, var_order, sorter, var_recorder) + if wrt is None: + raise ValueError("ParameterizedLinearRepn: wrt not specified") self.wrt = ComponentSet(_flattened(wrt)) def beforeChild(self, node, child, child_idx): From a69309fb034344bb2db4a6b565fa711b6d78f494 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 22 Aug 2024 16:17:09 -0600 Subject: [PATCH 2374/3044] Ensure quadratic terms (keys) are deterministic --- pyomo/repn/parameterized_quadratic.py | 6 ++---- pyomo/repn/quadratic.py | 9 +++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index d818c7c3ed2..fa2bcf20f56 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -190,9 +190,7 @@ def _handle_product_linear_linear(visitor, node, arg1, arg2): _, arg1 = arg1 _, arg2 = arg2 # Quadratic first, because we will update linear in a minute - arg1.quadratic = _mul_linear_linear( - visitor.var_order.__getitem__, arg1.linear, arg2.linear - ) + arg1.quadratic = _mul_linear_linear(visitor, arg1.linear, arg2.linear) # Linear second, as this relies on knowing the original constants if is_zero(arg2.constant): arg1.linear = {} @@ -259,7 +257,7 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): ans.quadratic = {k: c * coef for k, coef in x2.quadratic.items()} # [B1B2] if x1.linear and x2.linear: - quad = _mul_linear_linear(visitor.var_order.__getitem__, x1.linear, x2.linear) + quad = _mul_linear_linear(visitor, x1.linear, x2.linear) if ans.quadratic: _merge_dict(ans.quadratic, 1, quad) else: diff --git a/pyomo/repn/quadratic.py b/pyomo/repn/quadratic.py index a3659a6157e..e768eed0908 100644 --- a/pyomo/repn/quadratic.py +++ b/pyomo/repn/quadratic.py @@ -158,14 +158,15 @@ def append(self, other): self.nonlinear += nl -def _mul_linear_linear(linear1, linear2): +def _mul_linear_linear(visitor, linear1, linear2): quadratic = {} + vo = visitor.var_recorder.var_order for vid1, coef1 in linear1.items(): for vid2, coef2 in linear2.items(): # Note that this is random. If the client cares about # determinism, it may need to reverse the keys based on # something more deterministic than vid - if vid1 < vid2: + if vo[vid1] < vo[vid2]: key = vid1, vid2 else: key = vid2, vid1 @@ -180,7 +181,7 @@ def _handle_product_linear_linear(visitor, node, arg1, arg2): _, arg1 = arg1 _, arg2 = arg2 # Quadratic first, because we will update linear in a minute - arg1.quadratic = _mul_linear_linear(arg1.linear, arg2.linear) + arg1.quadratic = _mul_linear_linear(visitor, arg1.linear, arg2.linear) # Linear second, as this relies on knowing the original constants if not arg2.constant: arg1.linear = {} @@ -236,7 +237,7 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): ans.quadratic = {k: c * coef for k, coef in x2.quadratic.items()} # [BB] if x1.linear and x2.linear: - quad = _mul_linear_linear(x1.linear, x2.linear) + quad = _mul_linear_linear(visitor, x1.linear, x2.linear) if ans.quadratic: _merge_dict(ans.quadratic, 1, quad) else: From 5d70182e3de2df7636e27fa44bc81d5ce6483607 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 22 Aug 2024 16:20:26 -0600 Subject: [PATCH 2375/3044] Track development changes to repn walkers --- pyomo/repn/parameterized_quadratic.py | 4 +- .../tests/test_parameterized_quadratic.py | 118 +++++++++--------- 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index fa2bcf20f56..3a18a164fe2 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -19,12 +19,12 @@ ) from pyomo.repn.linear import ( ExitNodeDispatcher, + initialize_exit_node_dispatcher, _handle_division_ANY_constant, _handle_expr_if_const, _handle_pow_ANY_constant, _handle_product_ANY_constant, _handle_product_constant_ANY, - _initialize_exit_node_dispatcher, ) from pyomo.repn.parameterized_linear import ( define_exit_node_handlers as _param_linear_def_exit_node_handlers, @@ -333,7 +333,7 @@ def define_exit_node_handlers(exit_node_handlers=None): class ParameterizedQuadraticRepnVisitor(ParameterizedLinearRepnVisitor): Result = ParameterizedQuadraticRepn exit_node_dispatcher = ExitNodeDispatcher( - _initialize_exit_node_dispatcher(define_exit_node_handlers()) + initialize_exit_node_dispatcher(define_exit_node_handlers()) ) max_exponential_expansion = 2 expand_nonlinear_products = True diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index 38f5f8ec8ad..92e84ea0dff 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -40,7 +40,7 @@ def test_constant_literal(self): expr = 2 cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -59,7 +59,7 @@ def test_constant_param(self): expr = 2 + m.p cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -77,7 +77,7 @@ def test_binary_sum_identical_terms(self): expr = m.x + m.x cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -95,7 +95,7 @@ def test_binary_sum_identical_terms_wrt_x(self): expr = m.x + m.x cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.x]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.x]) # note: covers walker_exitNode for case where # constant is a fixed expression repn = visitor.walk_expression(expr) @@ -115,7 +115,7 @@ def test_binary_sum_nonidentical_terms(self): expr = m.x + m.y cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -133,7 +133,7 @@ def test_binary_sum_nonidentical_terms_wrt_x(self): expr = m.x + m.y cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.x]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.x]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -151,7 +151,7 @@ def test_ternary_sum_with_product(self): e = m.x + m.z * m.y + m.z cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) repn = visitor.walk_expression(e) self.assertEqual(cfg.subexpr, {}) @@ -174,7 +174,7 @@ def test_ternary_sum_with_product_wrt_z(self): e = m.x + m.z * m.y + m.z cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.z]) repn = visitor.walk_expression(e) self.assertEqual(cfg.subexpr, {}) @@ -194,7 +194,7 @@ def test_nonlinear_wrt_x(self): expr = log(m.x) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.x]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.x]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -212,7 +212,7 @@ def test_linear_constant_coeffs(self): e = 2 + 3 * m.x cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) visitor.expand_nonlinear_products = True repn = visitor.walk_expression(e) @@ -231,7 +231,7 @@ def test_linear_constant_coeffs_wrt_x(self): e = 2 + 3 * m.x cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.x]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.x]) visitor.expand_nonlinear_products = True repn = visitor.walk_expression(e) @@ -250,7 +250,7 @@ def test_quadratic(self): e = 2 + 3 * m.x + 4 * m.x**2 cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) visitor.expand_nonlinear_products = True repn = visitor.walk_expression(e) @@ -271,7 +271,7 @@ def test_product_quadratic_quadratic(self): e = (2 + 3 * m.x + 4 * m.x**2) * (5 + 6 * m.x + 7 * m.x**2) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) visitor.expand_nonlinear_products = True repn = visitor.walk_expression(e) @@ -298,7 +298,7 @@ def test_product_quadratic_quadratic_2(self): e = (2 + 3 * m.x + 4 * m.x**2) * (5 + 6 * m.x + 7 * m.x**2) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) visitor.expand_nonlinear_products = False repn = visitor.walk_expression(e) @@ -319,7 +319,7 @@ def test_product_linear_linear(self): e = (1 + 2 * m.x + 3 * m.y) * (4 + 5 * m.x + 6 * m.y) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) repn = visitor.walk_expression(e) self.assertEqual(cfg.subexpr, {}) @@ -344,7 +344,7 @@ def test_product_linear_linear_wrt_y(self): e = (1 + 2 * m.x + 3 * m.y) * (4 + 5 * m.x + 6 * m.y) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(e) self.assertEqual(cfg.subexpr, {}) @@ -373,7 +373,7 @@ def test_product_linear_linear_const_0(self): expr = (0 + 3 * m.x + 4 * m.y) * (5 + 3 * m.x + 7 * m.y) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -398,7 +398,7 @@ def test_product_linear_quadratic(self): expr = (5 + 3 * m.x + 7 * m.y) * (1 + 3 * m.x + 4 * m.y + 8 * m.y * m.x) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -432,7 +432,7 @@ def test_product_linear_quadratic_wrt_x(self): expr = (0 + 3 * m.x + 4 * m.y + 8 * m.y * m.x) * (5 + 3 * m.x + 7 * m.y) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.x]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.x]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -462,7 +462,7 @@ def test_product_nonlinear_var_expand_false(self): e = (m.x + m.y + log(m.x)) * m.x cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) visitor.expand_nonlinear_products = False repn = visitor.walk_expression(e) @@ -483,7 +483,7 @@ def test_product_nonlinear_var_expand_true(self): e = (m.x + m.y + log(m.x)) * m.x cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) visitor.expand_nonlinear_products = True repn = visitor.walk_expression(e) @@ -503,7 +503,7 @@ def test_product_nonlinear_var_2_expand_false(self): e = m.x * (m.x + m.y + log(m.x) + 2) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) visitor.expand_nonlinear_products = False repn = visitor.walk_expression(e) @@ -524,7 +524,7 @@ def test_product_nonlinear_var_2_expand_true(self): e = m.x * (m.x + m.y + log(m.x) + 2) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) visitor.expand_nonlinear_products = True repn = visitor.walk_expression(e) @@ -548,7 +548,7 @@ def test_zero_elimination(self): e = 0 * m.x[0] + 0 * m.x[1] * m.x[2] + 0 * log(m.x[3]) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) repn = visitor.walk_expression(e) self.assertEqual(cfg.subexpr, {}) @@ -578,7 +578,7 @@ def test_uninitialized_param_expansion(self): e = m.p * m.x[0] + m.p * m.x[1] * m.x[2] + m.p * log(m.x[3]) cfg = VisitorConfig() - repn = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[]).walk_expression(e) + repn = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]).walk_expression(e) self.assertEqual(cfg.subexpr, {}) self.assertEqual( cfg.var_map, @@ -605,7 +605,7 @@ def test_zero_times_var(self): e = 0 * m.x cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) repn = visitor.walk_expression(e) self.assertEqual(cfg.subexpr, {}) @@ -623,7 +623,7 @@ def test_square_linear(self): expr = (1 + 3 * m.x + 4 * m.y) ** 2 cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -648,7 +648,7 @@ def test_square_linear_wrt_y(self): expr = (1 + 3 * m.x + 4 * m.y) ** 2 cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -677,7 +677,7 @@ def test_square_linear_float(self): expr = (1 + 3 * m.x + 4 * m.y) ** 2.0 cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -702,7 +702,7 @@ def test_division_quadratic_nonlinear(self): expr = (1 + 3 * m.x + 4 * log(m.x) * m.y + 4 * m.y**2) / (2 * m.x) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -723,7 +723,7 @@ def test_division_quadratic_nonlinear_wrt_x(self): expr = (1 + 3 * m.x + 4 * log(m.x) * m.y + 4 * m.y**2) / (2 * m.x) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.x]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.x]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -753,7 +753,7 @@ def test_constant_expr_multiplier(self): expr = 5 * (2 * m.x + m.x**2) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -771,7 +771,7 @@ def test_0_mult_nan_linear_coeff(self): expr = 0 * (float("nan") * m.x + m.y + log(m.x) + m.y * m.x**2 + 2 * m.x) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -795,7 +795,7 @@ def test_0_mult_nan_quadratic_coeff(self): expr = 0 * (m.x + m.y + log(m.x) + float("nan") * m.x**2 + 2 * m.x) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -818,7 +818,7 @@ def test_square_quadratic(self): expr = (1 + m.x + m.y + m.x**2 + m.x * m.y) ** 2.0 cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) repn = visitor.walk_expression(expr) NL = (m.x**2 + m.x * m.y) * (m.x**2 + m.x * m.y + (m.x + m.y)) + ( @@ -847,7 +847,7 @@ def test_square_quadratic_wrt_y(self): expr = (1 + m.x + m.y + m.x**2 + m.x * m.y) ** 2.0 cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y]) repn = visitor.walk_expression(expr) NL = SumExpression([m.x**2]) * (m.x**2 + (1 + m.y) * m.x) + ( @@ -882,7 +882,7 @@ def test_cube_linear(self): expr = (1 + m.x + m.y) ** 3 cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -903,7 +903,7 @@ def test_nonlinear_product_with_constant_terms(self): expr = (1 + log(m.x)) * (log(m.x) + m.y**2) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -927,7 +927,7 @@ def test_finalize_simplify_coefficients(self): expr = m.x + m.p * m.x**2 + 2 * m.y**2 - m.x - m.p * m.x**2 - m.p * m.z cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -945,7 +945,7 @@ def test_factor_multiplier_simplify_coefficients(self): expr = 2 * (m.x + m.x**2 + 2 * m.y**2 - m.x - m.x**2 - m.p * m.z) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y]) # this tests case where there are zeros in the `linear` # and `quadratic` dicts of the unfinalized repn repn = visitor.walk_expression(expr) @@ -967,7 +967,7 @@ def test_sum_nonlinear_custom_multiplier(self): expr = 2 * (1 + log(m.x)) + (2 * (m.y + m.y**2 + log(m.x))) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -989,7 +989,7 @@ def test_negation_linear(self): expr = -(2 + 3 * m.x + 5 * m.x * m.y) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -1021,7 +1021,7 @@ def test_negation_nonlinear_wrt_y_fix_z(self): cfg = VisitorConfig() # note: variable fixing takes precedence over inclusion in # the `wrt` list; that is tested here - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -1047,7 +1047,7 @@ def test_negation_product_linear_linear(self): expr = -(1 + 2 * m.x + 3 * m.y) * (4 + 5 * m.x + 6 * m.y * 7 * m.z) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -1081,7 +1081,7 @@ def test_expanded_monomial_square_term(self): expr = m.x * m.x * m.p cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.z]) # ensure overcomplication issues with standard repn # are not repeated by quadratic repn repn = visitor.walk_expression(expr) @@ -1103,7 +1103,7 @@ def test_sum_bilinear_terms_commute_product(self): expr = m.x * m.y + m.y * m.x cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, wrt=[m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -1123,7 +1123,7 @@ def test_sum_nonlinear(self): expr = (1 + log(m.x)) + (m.x + m.y + m.y**2 + log(m.x)) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) # tests special case of `repn.append` where multiplier # is 1 and both summands have a nonlinear term repn = visitor.walk_expression(expr) @@ -1148,7 +1148,7 @@ def test_product_linear_linear_0_nan(self): expr = (m.p + 0 * m.x) * (float("nan") + float("nan") * m.x) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -1172,7 +1172,7 @@ def test_product_quadratic_quadratic_nan_0(self): ) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -1199,7 +1199,7 @@ def test_product_quadratic_quadratic_0_nan(self): ) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -1228,7 +1228,7 @@ def test_nary_sum_products(self): ) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -1254,7 +1254,7 @@ def test_ternary_product_linear(self): expr = (1 + 2 * m.x) * (3 + 4 * m.y) * (5 + 6 * m.z) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -1286,7 +1286,7 @@ def test_noninteger_pow_linear(self): expr = (1 + 2 * m.x + 3 * m.y) ** 1.5 cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -1306,7 +1306,7 @@ def test_variable_pow_linear(self): expr = (1 + 2 * m.x + 3 * m.y) ** (m.y) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -1327,7 +1327,7 @@ def test_pow_integer_fixed_var(self): expr = (1 + 2 * m.x + 3 * m.y) ** (m.z) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -1356,7 +1356,7 @@ def test_repr_parameterized_quadratic_repn(self): expr = 2 + m.x + m.x**2 + log(m.x) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(expr) linear_dict = {id(m.x): 1} @@ -1385,7 +1385,7 @@ def test_product_var_linear_wrt_yz(self): expr = m.x * (m.y + m.x * m.y + m.z) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -1413,7 +1413,7 @@ def test_product_linear_var_wrt_yz(self): expr = (m.y + m.x * m.y + m.z) * m.x cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.y, m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) @@ -1443,7 +1443,7 @@ def test_product_var_quadratic(self): expr = m.x * (m.y + m.x * m.y + m.z) cfg = VisitorConfig() - visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.z]) + visitor = ParameterizedQuadraticRepnVisitor(**cfg, wrt=[m.z]) repn = visitor.walk_expression(expr) self.assertEqual(cfg.subexpr, {}) From 0204bf013f663c44d078edf149f6bff6f44595e3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 22 Aug 2024 21:49:54 -0600 Subject: [PATCH 2376/3044] Avoid unconditional import of numpy/scipy --- pyomo/repn/plugins/standard_form.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index ffad2d62a95..6a06c94205a 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -250,12 +250,19 @@ def write(self, model, ostream=None, **options): class _LinearStandardFormCompiler_impl(object): # Making these methods class attributes so that others can change the hooks _get_visitor = LinearRepnVisitor - _to_vector = np.fromiter - _csc_matrix = scipy.sparse.csc_array - _csr_matrix = scipy.sparse.csr_array + _to_vector = None + _csc_matrix = None + _csr_matrix = None def __init__(self, config): self.config = config + # We defer the first instantiation of these attributes so we do + # not trigger the numpy / scipy imports when the module is + # imported + if _LinearStandardFormCompiler_impl._to_vector is None: + _LinearStandardFormCompiler_impl._to_vector = np.fromiter + _LinearStandardFormCompiler_impl._csc_matrix = scipy.sparse.csc_array + _LinearStandardFormCompiler_impl._csr_matrix = scipy.sparse.csr_array def write(self, model): timing_logger = logging.getLogger('pyomo.common.timing.writer') From 8adfd3eb4c49f144cef103a6e18d91e57d1ed7e6 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 23 Aug 2024 09:44:33 -0600 Subject: [PATCH 2377/3044] Updating doc-reorg branch with recent doc changes from main --- .../alternative_solutions.rst | 0 .../doe/FIM_sensitivity.png | Bin .../alternative_solutions.rst | 107 ++++++++++++ .../doe/FIM_sensitivity.png | Bin 0 -> 194054 bytes .../contributed_packages/doe/doe.rst | 160 ++++++------------ .../user_guide/contributed_packages/index.rst | 1 + 6 files changed, 162 insertions(+), 106 deletions(-) rename doc/{OnlineDocs => Archive}/contributed_packages/alternative_solutions.rst (100%) rename doc/{OnlineDocs => Archive}/contributed_packages/doe/FIM_sensitivity.png (100%) create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/alternative_solutions.rst create mode 100644 doc/OnlineDocs/user_guide/contributed_packages/doe/FIM_sensitivity.png diff --git a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst b/doc/Archive/contributed_packages/alternative_solutions.rst similarity index 100% rename from doc/OnlineDocs/contributed_packages/alternative_solutions.rst rename to doc/Archive/contributed_packages/alternative_solutions.rst diff --git a/doc/OnlineDocs/contributed_packages/doe/FIM_sensitivity.png b/doc/Archive/contributed_packages/doe/FIM_sensitivity.png similarity index 100% rename from doc/OnlineDocs/contributed_packages/doe/FIM_sensitivity.png rename to doc/Archive/contributed_packages/doe/FIM_sensitivity.png diff --git a/doc/OnlineDocs/user_guide/contributed_packages/alternative_solutions.rst b/doc/OnlineDocs/user_guide/contributed_packages/alternative_solutions.rst new file mode 100644 index 00000000000..cc5ab07c3cc --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/alternative_solutions.rst @@ -0,0 +1,107 @@ +############################################### +Generating Alternative (Near-)Optimal Solutions +############################################### + +Optimization solvers are generally designed to return a feasible solution +to the user. However, there are many applications where a user needs +more context than this result. For example, + +* alternative solutions can support an assessment of trade-offs between competing objectives; + +* if the optimization formulation may be inaccurate or untrustworthy, then comparisons amongst alternative solutions provide additional insights into the reliability of these model predictions; or + +* the user may have unexpressed objectives or constraints, which only are realized in later stages of model analysis. + +The *alternative-solutions library* provides a variety of functions that +can be used to generate optimal or near-optimal solutions for a pyomo +model. Conceptually, these functions are like pyomo solvers. They can +be configured with solver names and options, and they return a list of +solutions for the pyomo model. However, these functions are independent +of pyomo's solver interface because they return a custom solution object. + +The following functions are defined in the alternative-solutions library: + +* ``enumerate_binary_solutions`` + + * Finds alternative optimal solutions for a binary problem using no-good cuts. + +* ``enumerate_linear_solutions`` + + * Finds alternative optimal solutions for a (mixed-integer) linear program. + +* ``enumerate_linear_solutions_soln_pool`` + + * Finds alternative optimal solutions for a (mixed-binary) linear program using Gurobi's solution pool feature. + +* ``gurobi_generate_solutions`` + + * Finds alternative optimal solutions for discrete variables using Gurobi's built-in solution pool capability. + +* ``obbt_analysis_bounds_and_solutions`` + + * Calculates the bounds on each variable by solving a series of min and max optimization problems where each variable is used as the objective function. This can be applied to any class of problem supported by the selected solver. + + +Usage Example +------------- + +Many of functions in the alternative-solutions library have similar options, so we simply illustrate the ``enumerate_binary_solutions`` function. We define a simple knapsack example whose alternative solutions have integer objective values ranging from 0 to 90. + +.. doctest:: + + >>> import pyomo.environ as pyo + + >>> values = [10, 40, 30, 50] + >>> weights = [5, 4, 6, 3] + >>> capacity = 10 + + >>> m = pyo.ConcreteModel() + >>> m.x = pyo.Var(range(4), within=pyo.Binary) + >>> m.o = pyo.Objective(expr=sum(values[i] * m.x[i] for i in range(4)), sense=pyo.maximize) + >>> m.c = pyo.Constraint(expr=sum(weights[i] * m.x[i] for i in range(4)) <= capacity) + +We can execute the ``enumerate_binary_solutions`` function to generate a list of ``Solution`` objects that represent alternative optimal solutions: + +.. doctest:: + :skipif: not glpk_available + + >>> import pyomo.contrib.alternative_solutions as aos + >>> solns = aos.enumerate_binary_solutions(m, num_solutions=100, solver="glpk") + >>> assert len(solns) == 10 + +Each ``Solution`` object contains information about the objective and variables, and it includes various methods to access this information. For example: + +.. doctest:: + :skipif: not glpk_available + + >>> print(solns[0]) + { + "fixed_variables": [], + "objective": "o", + "objective_value": 90.0, + "solution": { + "x[0]": 0, + "x[1]": 1, + "x[2]": 0, + "x[3]": 1 + } + } + + +Interface Documentation +----------------------- + +.. currentmodule:: pyomo.contrib.alternative_solutions + +.. autofunction:: enumerate_binary_solutions + +.. autofunction:: enumerate_linear_solutions + +.. autofunction:: enumerate_linear_solutions_soln_pool + +.. autofunction:: gurobi_generate_solutions + +.. autofunction:: obbt_analysis_bounds_and_solutions + +.. autoclass:: Solution + diff --git a/doc/OnlineDocs/user_guide/contributed_packages/doe/FIM_sensitivity.png b/doc/OnlineDocs/user_guide/contributed_packages/doe/FIM_sensitivity.png new file mode 100644 index 0000000000000000000000000000000000000000..af6b75cbbea900c72a1a2f289bed88f7b21dfa1e GIT binary patch literal 194054 zcmeFZXIK+ox9}|@MS3T+ARtY8FNUrl9YiSt5eQX!lhBJ0I!NywK@<=a={-Q`T|lJQ zP^9ueH~#y|VWDC0t8Gi4cz#@6Me&gv!sJJil}2 z-pQRinBQ@+fiu5%Z7G2rhU;@B`8(x<^c%oGa0`89OLg@-kAP#`JNGbX?_m9D0=%R# z=>Bu8h{16O^Plr~@7xKsxpVJdZ8U)WpTA___2)Bx_n5gD|7s1K%f0*W*7r_wG5>vx z`TftwW=UG3fZctkX9li!?ogBcd0|-Pi~`-3y`%g@?gboUI}0b3zH?#e}~HSFI|X?PmZ_Ye19$RjP>^8Hk@I&QuR?Q5j%zxfs9&W$4^R=ng4#EMSWLr`v+D58R>s| z``-=%*T~+ReyMA87x}k1|E&QA=|_AFnv=~S?eKr~?LVFT&w;G#{qBlMqq+L@fA{}C zZ~gmVJS2d?+r91MlP9wG{_k6E-97UuTV6+F_5Y=T?7dBSSwvI4$qVNHF6Vz#Gi(G1 zERs@F&h{_u|Ie20On}ym`5%$~uL4{10s^P^8S0V$4_*9g)PW%fw8jfc3j1FL7ES>K z9#}2={9l9mZ(aO*jE!--EA*(NRG$2=0;j720E||Es|N zznT0G)%<^FCaG1tB^+@xq^bO-@@FT%9Fb4R)nCNdgtKRht4WqbG~H7ny!APq^}wjk zQqI$I>Pk=(RJLlHKA741(gM!*J^5KN{#-Sr+4u6~OzJsw-sdn{1}8yN+B0^&xPc~s zCo1K&-#P!~_BRi)y86!bH2B3vG4l52w58%=^K)oPQF`y#&DG%$vLIG^vt`oAb)eo@ z^Zs9pJc3|sq(^hUd*h~F9+mx*&V7Iy3TJ8rK4-e$Nlkoxacrx`+_dU3kTNM-CcVl+E?I3%;{ZnE(qg~)YC%0s}BSn5dYY!69{eDt}L z%rey!vE+PB5;!L-OO-9ky}G}Q3RUH6sydTX#|$ztH;4 zj?B&3GQZamn*W#Gw-5uXtnaA9%Y#0?P}e~TRPN{)n`@RjbkMLz#?)l`>WPUL;|%!T8RNrz0OCFEwc;NbuDzk@tF0X%NDgBrF~7Z4~DqYUaGS)Wl}pOtU@LHcITa>i@bPn)|~x zZx=(T5!wZcbxbUq=?UthX&&4ArcFx`TnK633-d<#!~7BHk()t@^~}2A=p65zI;S4i zlzY>OP#2r6QjkLCnZ-S&x_>?JEKMxjA_p8&-9e@r%vqGmtoHbKRVR1ByFcT0 zGoXqRA7myYwN?VAxbLPVqpf?-70?6ui|be#Lu67^fZ15p?$mG1V8+bFK=ARF0Qr8; zNqcs)6XGHX_J!NwD#&=z?Y4A97Mq1`g}ID4oJ}Y+82nONd@U75o15Hv$TrU}=3U^%kA!cd_x5 zMJF>o@B|Nu0ab-huDq-zmcF843JNv)TDDmNfz`^#qTZ1durQpgJ{-axb2%@Ytjy*u zuvUBJ*mT^UW|h5R(OGk6-e9-+(xLWn@G&Md1c%Y5!aJ4}v5SD*C!moEta&79AW^hc z`Yq;h_{s=B%^}@s9M3N8PAKUc?K3be5!ZBV-cBIugn5|gPb?Aoe0v3)ZwM!7Cuy0# zS`(9w+HJYL{?dZ(B@BW2-Ca90i#0U|o-}S+eAufVsYCwZcTeGvdugZn>5J4IZFk-= zcdNoHJE`!M9lwZ&V)2<(Tko~zCW~e^0me8NFbY#7rnO1ha%WX?nL-0j3D|RdT%31)th^kdNy|9~<{%eiR=V|Ms>+@>m>EM7S5WeUYhW%#~uqwk&F9n&5RkU{7nj)u! zql#`Dfj^eFfEQ@gL`ZKt$nzuRwgr^zro3V2QEC0=CoJ!@lwdIySyS|I9+s`RN;}Hm zPX0P*0RMSKioY^sD~o#um`|~6`SC7y8XVi8{eE+_DM2(Ke9MO%sd1m2et(~&Qj6PP zkv>~^7gQHdtyagS+y|k=x9sMe>U#{GI9=%@;0o#EYiV>^juxZpuhot^JzI`V30A*# z-EBIh>c&48Ue>>G3%#woy*~N%#YDxiZ4Gmyb$6#>mp3&?kKdx&1Tk*rLl?Ao{ zg*dl4H%G--I;F7Z%{Ac0pn)Vk{>iH#8eDu)f_f8dR?Q)b@piyX8&@zDusWZiAWx*C z2=#WHV)p19YE79uHI4kY%4&G^HD8796*YRFL$3CaonLoIKDJ}%Xf;Ol-!b54%`;J8 zyQ&w}d=>&SW-Qt^rO&y4QUsTz%dRU-6?{2m8p?ntdaBw+ksibe$I<~qc!%)T)tosM z7s2W@2Ny)-o8scf=R23^^^JY@h3k$(9qZ#3C!evO(sCwS{Q{>_I=zOXqk)~2b%qJdz@7ZX>a@%6S8(p? z@{fugzIfaaM=fOMnc?ts%ga!*#L%(ny9qXfW2_k+1#-_p=41+nkB;bw&2Ueqw4+TU zvcF;Qm|3)cZl?r;Z&@FlNR_QSGq^o*Q$PIlsEBbn?{=ErD4Sv0N{AT_nl74ROVchf zaXsqdrta5z&QHuei2;+`w(%eP&2cW5Qe-T&%QN|_|H1NP?yQ35mYN-2To4Vf@#zk? z5d_ub&~kl}eF{tnUod4x3yi;Ocuj7YK{ANzp5VlshCrjTvyv-eD60Md!?R94zePLy z>9l-@cYXnW;WrkD#C9Ok*3b2MM&3l4B~!xqUepwbPlwxw{BI6}2DtPqI3*)4FzQL1 zhZjf9#tv(5-ss>ZWd>o^uU%|?dHsB?Fl6qf#^e*tyg{jhZWdM>_Y>mdxJ!AZH3<&( z6|AgCr|MRPr@lo-MlsIwg^Ya#6x-RN`Dwo_ak!sXftL{x(c@h~!MhbNlD7f~-!cIfFy& zoMJE(&pVuIXe^`sVbw`Ifn}*HwM%0psM)sbVNS{r0bUz4xKq48$%0SfHTPXdu$OJA z_WeJxSP)IZ6+JRA!3>*hx94k$4L|0K3dL5bdGrq!UoYWFV-I7-i8dctFuT21E1Kym zb_rTQkuQqoF=WURf@}Hk0BUsbG&^l8Mq(m$iy2R41+D`(sez&9+~1ausH_KAfm&Pj;H4KR10_T0U)o-9Y!Y4PO!dmy!N zTuc6gEdlOe%HsYrTD3qYYTq@jEfsy5q;np02#eGvb-NQ5Vsn_zCgORxm8ntKySla1 zdo@PJcdT1;S`aaN5b2`Yz1%h<6DKW%Oi)Zb(P>{fCtJdX228<9J&aux4PP zljc!a`q$N=Sk8(YJ7mgwz}Yxoqq}GIHfF07(O%&KO`%VM)N-Q0X$LcZN5OjHk^0E# z(`O$Y6@H(uF^r~>3`*aNIR8LU!k5VD9(M8MXKQGE1ob=42tjWueB6bbGs~30uZyUBi zm9t8rxyG}$Uz&Ud`KG}~*lvnP4+k0iul89_J3mrPi@Cj+)|-9_L0R`%d+%Gyi);K+ z`4v@6``L~etpbim`;UD_ut2pl-R_O%Y0bd_rAk zO$*MVW*uQBS#LZ0$Z>oztI?rVljwBxQh!aikG*B%eeRn!Vfw01{g%m*I04Snl<-IwN+F#!9Q?Z6(>6GY!NYBqF)Qn=zYH-) zh}kGrz^{(4&{Kjlqtg^DxOjP&0q;8NqSr%3q`gAB78xQL)jaU6V`c-LmB?v<=^yM8 zC~)dHcn``~Qh=^UAE>D+c+GfUfz3uJr*e0-i${{q8Ds19l|HthGvV57AudZ915q{Q zlR7mc1|^Sgf!V-knr@TDqKDqMpD~)ZTc}-#z<*Pb;z(Vn@$73eO4#5dmZtmx z`1lI0;Z54jfZ_fA$GbNq(@*!ZE2-;G%~PH8R;tnV1)=Sa^1M}8y(RWdmc^m3l^vPM za%*C<#Tw1kIJt_Hzr-~iMc$$qk3a^nwWf#4eBzBWrTa%)Qz%ayVo}>De9PR39t!UU zyQgDIl`F)Mlx`uZq6up@O-c=^;};@Q2}H6>OPmn^#q?n>qMct7|gQZy%|7)vZQIe|-U*DN-~2sO45z^ZwtM`5aF zbGS9uU>;#Dkj@kFoc{xAtiXfoXF>2J8`OK{7j1tKr*Q<1O1@oh@zG!E(($W$WFOmQ zjx?TkO=wXoIOQBoPk-v_g#EaBaBKPg_)2F^Iqy`$YhRlhCcS~tb$Cayh(GCw3@Ix8 zZsTilaU9f3Cp{sB^QJnf1)?r=hbmUv%5$PNneddulL(zv{Zye*DJxsw>qCN>N;Rs$ zLyWUS)mz^w`?B+rNE6#@vf#zRQ~`9!LglO*A%28?c$IdSGJ#{Kj@Fpix@WT%a57-5kZGlk9)RACI~$Ih>Te`6{=ux3DQtVF5^R1sF)}B`Qy!*s$b|-^fzVW zg!vUpUS$Dfcug$&uC`dL7t$HUkV3F&&jmGA&XUHK)#FE6Vi-S~BVQlJ-)moK-Xl>> zCOz%;xSQ?m!vz77IRX-VR#SPQ@hA?oo@MRf(JxWEKHjRtX zynGEmOiK_R=n6NC->phvtcQ4tF54+*4&KPUW-A+noP=p+MN6Li(0e^=5YM{4;yHVUEPs`;+DITNknx{QW{TyxW9q2{47? zw1+Z9v(y=4g&rC(Ubo`$(kEt*`{8SCkb2?`Vwrx(Ms0{7MWA+<%X8ZR)!^^Q+TsqM zTL?lu%C507J))Wm8t<&qLNq3vbv;^_N+^sx)BaAzq7fWbxu7tM_>V1>7LTG>Xx0=Q0pJj>jWm z^4a`daY|9{Hgbz*saz^nqj00a3Y3%MsOdB7uYs{PX7@gfn4^`R|Fpo}1F{ZUt=$HZ zS`#3_l0Ie3@5fc%(%6KwFY-Culd6y9FNNOazjCnlq<<55KtBYVitMU(8N$coTa~MO z*cT}oZaU)H6PD)ElfwsW&;ZwR>XD`TpSx2p56JQ&&3m@7t`{tBTBgBfQzjIWI?C@( zzy(TH^1mp4IUBCze#Bv2=KpYSlB3vm6fqVUiorLhGg^VF`0)x0=rmScs0tkte?&PU zs4`gN`BE<56XS%zjza0gD4h*4hBmZkPsyuVJvZzqdYf5-EHIH#gAvXnNw#$p`j87* z8fSWAVICu5BD9;bf~f=&DqNk$t}jV*%gDlC|EykzWSPSYc9HSXj#-92YI3u;B1PCO zDI>pVMugubk33=0yx5DN&+Vy?mC0~Sm%wJNr?sTg;b4?pq_>-JwzA|V&8MhbMn&J4 zi=6QhvyO5C@73l3bn2>soRvm#Pt$%@kYCh(3vKuin?ggv^GH)-OkuT+!u4OwJc&9h zDcSjW>=c{D(I(1DW4Fxj#qQEZ*b>_s;_)toapFi{jY7*im7hD;RHwN2I6Xl)FC9c* z3hm5H1l0fX2uAK<%~X=M8w_bdRuz;(e^yA$Yz#bD*%>HWsHGF}w$+L-rO^mnII-tD zUD_{|ak}iX$Yd(FF%=IG;oeCfbk_q?yJjTbWU4!P@cdpz0=l*jnM^Si(em{Zxpi9} z5USJKQlYAV#aseq6AV1y=Gah;1)IZ$TKpCfl^tfyI>D%5Q~IJ8DNlr+jvCLE?LU$j zlvzpfNusLH|IA-~gGSqU$ z#lv$4U(W}j+`*KWXiHD@{#kinPSMPz%4-5K3vne1qkVPK1E!Az>dz(?L2Ns9r)LBL zkd8!lMn79TdxGaP-vSVv1~>hw`7biH(hnBAob@#Yg04-1oDl4LYE703Dq{-oST4;o z2mMf9dJd^d^q)+#y&2*Ih=;7bQn4`9H}FnKmxn|wkk_rjR`g6n7RodekBny-kvSk< zQ^vc}!d=qfYcc|>MH(|)KI9JT-<+wG(u3**f{j9+XPfEVpa7x-yU?`hki1N^20`qT zd9}@Zg;rhkElkr7rYAWj0yiY@+28q1vTxn;EEcY<#Mo21$p^CHF-ceGv@h^+p4;%5VaAEU#ZZjC=Kc_Nsz8YdVYC6rFD5? znm0)1gjEvcH!Iw)G@3@Crq6^X7roTT-%0l{6DWtDt8DE#tr=o)qV8wW%jKx!O~T+X ziEFG`+}RL_U_ZrdNj(4c{dOt-?Fq?Ok#cp6Zbojxz4204Bvii2yjRboUKBG{&U7n? zwDVgwXMp5iIh<9}bQ+0SVo*2Zm@$M6#Ywoi)e@|l&83v<tkK3pU$I10Ta!<%K6VtMe%OJ)M=S#7a;YwM&Zqb^e)c*>6A$w6NCW!vUIa3bzG zH;lJ2agx*OA0-?55UY16Lr8llYqIc3Hw=d4^zp-|U~&EK5%R&vXDx!}O$4Z4pt@aT z(26Q8Uh%oOhTkY5r>Ql)1URYOyb&b*2y35ygS7d%foTAX#w+E>{pIYs9g?-Gwp;^^ zC7;-y`j@;$dMJJ^oiC(ota1_&hjy(9n5a)D;T(vi?i8Q5wsk#Qb%H5XDCK?xM=v%v ze@rs@=+ij4^9M#0QzSBvV)x-QFee4ijfmF-^FOk^H|*xJT@-s%BG_CD`G-h7m*#1*07d>-(wA0V}%+*&BEdQ+dy^of)J4`)|92C4k z4O=4ZjVFaQeH~R{kHdHsf3?jucK1OknsAgiG2NyFWazvYu^&z^PtA2!&`6%x2r*<~nO|a5wR(=C$dLNtENhbQ(@My^?-s-CFNP z?CK_DG8{FQb!MncFdGrGJxd<7%SlFVTS{Hi$ z9DUpoTq7*IO6v;ksI-cld%^ak(@FkahZzwER%6)D{#og%ipluLVSE?XcAOFh%Q7o8 zArU@ol=bF(%N(&f-gJ1C5sEO zotQ*{D%Ry&1}64O(T-c=n9Kj!ysp1WNZ4orE}Zq0e`op3$-Y2P=6>|sXOY&f>< zCWS>RRSfEaYFC1$$cTOrwVdcE6nyzfSN{%90eJpCcUSu9ed?{z6kl=eQL5}B$w^dOiZ{RH{i z?o?B5j-X&Z_M`HpJlQW^Q3~Qa2^XO*G|>nFX*??IXHgNA)G4l&8cCmb1sn=aOVtoH zn4jzN=-ESjo+51y;1ly%v+PZyrJIOKs;7}I`P=%+Wi3*t|5_Z&W|wV9nh>%1Wi4ZE zD?mHaW%hA-HW~wS=&^L+@?QVoPLPIq#`?hH%wAuUW$gq9)nSot>MZ#m&#dhE6jcer z(ej}BVV?MQAbSz~1ZIBVBjjIT_0tG7$7JbBS?@s$_C%-CYi`b{X)DZa2E zgli;7zy5N|`S|GLN|jVkm1&FvWwDKNw(3m`QobZ|BrF*+Pu}93AHc$pR4pE6g<(wk z&f``7VtiwPjNlAC-xIgt!dyqQ#?Atsk_?npfMf7S4-|ad5wXAw@hD^@^reqkSq_bFy@AJ=B+O^XvBsm%~^oROhB{rR)$Yd-%Vk0^1~m-zn8p~eC`jM&l?T?nc5 zqrgqC1@b$6W`cI~7^+AZT;paDsvc~;Z9 zp2z3Ut%di@&37albI?CE@Qg^Oim)sK&KG-+M?WAZw)~R=H1V;yD5$*9k24N~;RG0R zq?Q`(DqRBYonD=e!7i(kptMh7x8)d-+`COtotNkwN?fl zd+J@xya5G9q_teJr4^Zbu8X~PRA@HJr=x0Fx$-HY(XBa=j9XFpL)u341GTrqq_HOm z@5RhAyJ;p)0?iSAZAT>dl+rzph?>_m!1Fr|wz`s7RuAa+u)4{BFhn`L-ts{I${ybU z?ww;vIy4DnC*!Fh2V?T*W|>dFAPXvlOnZskI3(|kOg>V6)sin%OJyvS#_RJ)V9D^2 z*C#Xn2!_T)tLO7kEij40sNu>8%zMX=c+hn#lT`DEyBTolE=_+Or|fmYW^~52KN`g9 z(@%Z{yGn(AJAusa)5H$>G>TflxK6tKg?3G&=9GV;Y?D-@c4?w!+?<3~@#nva8w_hR zkp}8M(3&QzmXAq7)su*CS0Jxgl@v4`$%43q3GpH!Q=28t?=kocpl9&4$_4CRGASS2 zCY2*|vlqy-cX9#LY${YxpQDOGI=uEV76oDgusG<`f&1fT^c9B=~v z@F3^V#k52^LX)HRsP!@L-dOB-!Qx4}EuwqbQpZdb2I z0w;p^7%T)H89Ng{J`-dT!IMPPlm}N-*Jsk@o&2<76a(q7fj($umV7&+5HfX^z>2M- zP>P{9Xe2mBeJ^}I-1dd6zk5I{ zxL*}EB*Lh~G3cD#uVRz@IJ=n$Kh)`#yR@AjKk>z!0*ABmqOV*irh5z%DJa}DQG<_0 zspaWE1%BxYxt~1*>WSqWG&2ynpGS5CLi)5a-UgpOwimZ2tS3i4v!R29@_Lb7u;DWg zI}{j>vW=GS04sUmA##l?Ild>mxtJLz84{Dj2DL=>xRVu>7RD`0;5}>>&3?-6{g1X? z68*!P;+l_xPw7wvbAd7K5TB~84NeI%gWjWrybk4S!Sjq0p%Pn{*P!=%E0REol(9@P zEJH&BB={AZffG-WVA@MDHa8`AvK7}id+~iC`SBE}IL^EFd6zc-r}xBD)4lDTHX%#r zM_^6isC01-E&nr7xycpqM^!iO0!Jkl*QlXeZWlzk?q@CHZuVoWYNL0O9cK3E2DC8f z^z7~CrhzgvV;8eo2riArEgC#zxWg#U53)E(1CyCeh=>2RTKpbQRP)Xb6w8|_xrxj6A)a%Ilh6`niketK zCZB?ak@rt3HHib($${aOjs2FUy-1sM>S#U%P531)UnDfN%t zUx^bzG@_B~{ahs^yKPZr%}=Bz`7Js z+Sm$s^uK)0Db7@@xhk`GhKzY{csoeqsq>3&+%E8F-Mu5^F1jC)K%9_Bsz* z*ck-}^G&eVwni+64J0hvILB&#)LCQI%GJ;xdJEb1)*--OY8VY!mnICRtUKEM20* z1f-p(H8ev;yDYr*_kVL-6nGALdQ~N%BXI5M>)GmY7Ry|#Oc0N5?Z+G_wwLFBk!4KQ zJtVf|;7+Y9no1@mHXb9;1y4%vMgER=|43=)@Y76x8tP^OWUa@lwDIP^2{?wi`5!&v ztukUinx#Vl;D;3VWez##mIx~tM6rLG(e3QFo^>vdHYNpCz z1yW2?VH~%*`P;amJKuqVE-Lr8;yQlH0x+jGNxR7Nb?IRV`xi+#4M(G@Qap4MSy8$R z{>^{F>B(bM~p zg<2@jE_>R5mBk=;UbnO4V1rj@iWQ5a+12jc-c1^zBS0EW=k9H>#L0Hqu!H>2y=)OU z&&yX)t_~%%Wed#}EF6b6?zkhh zo_Psxm=>TgHMzYqNBibnrM%wkpX4ju(_|=jBzzy*slblc#Cin&8o}43d=&E&`65%M zotBBwQr3|h3@uQMKIn<3@w$V#hGtfKJ^}T%zfA$xzru=DY^<*3^Rn%`-!y21>A=6} zmj+4CX7}ojmnUYDiU?-eB`}oRoBFCa71}WB`ov%qvwQf7SMxpz3!k<=Rx}oI>X+!P zqL9VN#Mj#;k?W=k=K{*y$ilI67Dt6L)O%A5_)eTA!X-hoH+zv(xs5I+gfg{8fRn&A z?=&;S(CM=kNYq9smR13i!@HN8b0wu}sgbkCb=5CH)IEYpXhTpFQB0iq|AD(P zg0CtS?9Z*|MYC}~x+%M->SM9p5z&EBWstB`oZ2&WNq$ZIA+8}t(+z{dY;FIA;JvH; z#}fW{o(0<*G)3TtAqR!rAHAK_T=S@NB)gp^3*U<&D71;|^s;TUZQ4ia1`pGZG7Ee> z=RUrM<33@Mv1~os+t;5Fs!IOa-aw^k=v7c6V`Jbp@>Ic}PBMs{bAd+&>-z7p?8P@7YP(cm{`wO!(U5j~P`?AVjdCp2-xx&UXpqUq3?+LJ3;vCVDt z7HDF4NgTE$<^>4+))J_vUBx?x;XZ<@pwrbZ))KRfUVOLhBZ7Uk<-f))9tttIFf`!2iLlLR9R_P-i^PaeI ze3&ra702KILvf=DanTH}uUIJCAq)xxgzNN6}IF zlu&`y6skcTcGIZAo-lmKXzB7qU))uZJ5=#0e>{gqY;Y)tq|24Atm8wl>piv*)7Chf z1M!v;I(Uq3FUxVlfyUZfhp(xj&*m z-0j9H?6lEU^I1G|9ZM709oYz$&)!PXl{y%k75?h-d(d5b547ZxLWreE)3v8SV+v^x zOVCAsHTulH!=)8J6IZ=oZe1EN8#_l4Po;z*dv8lA`dG#s(C zmsA+kO>ngc@8Cw;jnaK?*f>4%b!Wf*F(Hup)Q>~2Z0c|_VKvxAbN8`4Uul$AjJ*FJ z6e70__ro@8 zzfWthBhpod%9fDbckAdAkr4a@w&5ZqwH5|w4_bBojY!!@vepNbO`L{DR9@V_Dk87E zxXF3EOlt+W!@~1uPWmdibhXX9zx@Z-$o0j|wNjbf_VZlYK;$YYzhP<359Oxq38E#i zMJ>l^s~*3FlC&+?U{Z&w}e}tE3;KZ!$?uE&i;l=L+|OmqV;7jIq*M|fCw*EIAH*_e zf!;0?{-IjqP#+UVLpF#vjn3|$^j%HTyU2`kZw!tn1Ijms>Alt4N+ZK$ZAo)cu@Z~u zrxr2fG{tYYWX=S_1!n3WE2-J*Im8YhL4?vo6fHO}e`^;to$(5zp!s{hXFp10kwcDj z?s+}NaAS2-Ut>-#Piqp*6;ieu>LPa7Q`i&;g;yJKB;&ee1guSMiORD+xNHCH&Z^2` zngyT!nX*LpV624hSGAMO>xc->f5RBVc6T?gN{O8}wtq?pGCz^@e(M*J#&|a!#wTN; z-QNS$*wS@HT|e;At9Th0q2#*vu7AZ{K=UA(OJ62g4~%LM!o)f69 zK|SR-Vm+5>^sU>ld?}l~yzlczo%OKiY)|Z&HcA`3^gLfpNoh~ZL8(cQ239jKT&$%E z>}=@lo|+g2KB}XU&T_dpH_wWwON2aX54fwR5w|MoSXXL(&aLV;&{G=2V|lT5$MCpr zZr1=<3oMJ0O8AmlttN%Z!lx&7RDooF3#4oKSU)oFRxk1Uow0kch=v=JwYC$pb- z2_9ZIR(kfnyfX&>b%2Omx|$Y^5O%!F6>->*X~8Xh_rlmH@0{AgGK zi5@9H0+@%zg(Xqa$NCwICUVaqGI}e+!P1x9>3c8dgxQh7?HwE_$fL{!`NjT>ATxsl z$8?Awiz9zu0dqeWx0S{HqRJD8*_&KUq|K;UG@>x>Pc8Ms*(e)LGbx8+ur$-Qc*h(% z$5O3uZe(wq4)!agdR5C2S5v<^yN1WgD|wY8>MQp9Y6(sI2gEnF)p^+(W`@vvim#_n zitIhFI%Jb>hpQW33h75z^Ek?Twr{)sI(Turcy!mqeDiI@5Lv+R00#@@sYF)A6y z8QL~gn}`71BLa*fsa#iu&oPtjIX29#w=*gdswF z2PDL3{Da{3wBoN-+qEvz$&3=@;%$+F12o@Uf5W!Uv3(_;&G#EfZ7*&E z_!<&RYv-WKliaDey}d0tl$zpo+EHz#RbN6Ydu*3tA6314M{=lHRHq{8=weqgyBj~V z<_e#qSF%OV;ge=s7|{~m&wEhMM<_UTBKz*QM)bbG>utKU;#AS&hc_C~hV=QH=2lFF ze=sdV=GOPO_sI{YrbW?+hAg}BY-?xNiOuP|cWwq`s}BmDo;RHpnf4iK0>u_Ruel=`H?CAa%)b^F+eREpKtIN(f(=XS(FFYRpU7f+a3tF!) zuv=ZAO2qmeP$vkcaXQsp-52n1)n;L%;4S&hF zLdQ=;va$%NADI8TlJy;oC|85faWXxf8{u57#KQjq^{PlQHZtsAq|Vk-(wx*|XlnoK zLb+stJD~M2uKyP+890dhY<@dRV^@M53Ekzioh|w2LbBxdbFD2HA~JrAeF?8AR{cLA zzaxI&`cg)BB2l% zI2LA0x6bDW$fU{i;C6Q0swmUl49DjCjJ`+OXFydp@@#+knyM(r?}8si?;6ZmoGIw6 z`awo17R=BUO}Cz84}eGnLWANkn}81g9RC|n_~q{!845k*s=nyz%Z_Vjr2b*K`f@A$F|AjTqq(s*R_*c4~! zy3-z8P7!T<4>l!+{RA2@`d$8-M-WS&wGuk&!v(epX@xoH-G=2%0iuSJGnTLAN-)!7 z&T|n!%-j^{6suX2^_l6NJ4nIB4=b(@0j3A>2Mk#R`r(XRBoOK+5uI^TAdYn|r#(L! z(?kMb)9&Qe{O!#;rRz_Nlw)Dt*S^PZ;NWG30o{kZ<8dNW#Qa zC2TftGf_vQM3N9|`&lk4fp>^JlL-S9_*Q^Sw=Ji!%yOTQjoqB}m}lT^J~OMK{e!Uu zs!*;LCTQTXXT!~=fdBu&+>&;HfhPXt&|qJ?nrzq(;B$=c>ZuoiBLDTSrh*vBN8CUs z76HPEz$()jL5Bt4ZKM1H!ebQ3luJ3kK>z{ccS8?XjA=X$0oVt@C>kNQOs~xnzk!CG zdM@s&Vfoek&D6gmN=MfHW*jqwz}sG*$xXG6T}#u~HN(F8{vQeva8AwRNpgR*Wo>_V zU|h`k)x}uO4FxsZBj0-7E^sgkSD~h7@DyX{zVEY>Q6-K|}j^8@Nkrmm(GcE!|I0YwtcJ zSxd75$GWb>DItJb_8+nP>zS^D_dD7^-4f<|3vJR4^u&#THFM_Xy0h^$?#b6K1mVw6 z9?!TAxjV|Y!$P`()TcqX|0;iIde?f&qgn0!J0?E)cSHm$;JSGuHJAOBe6rg9b_|kOZvOEVA z-jjmxp~@htt5!_nTD=E0{4%d85O_lO;2mM4J2aqJTfToB?C{(@EuH#r-^Cbavv5}u z0NeCQj&%9c`Mban&6k&_>|qN&!gZ86V3YscCaj(uFK#~Tv~OcvNqYM`Y0 zXrcjmNK|I=3?L7sw@{$x8Ocd{(4V3EOL?JxP|-!;86*bQ^-WR%kW(dSc~R{4>M%3~ z>eWMyZG`I!lDwL`^S!|2rwuf z(&~F@f3xc-)9v}13oM}pVw1gZ{wZ6l^6DSQ_dqDn3!MR4m;QFXqt6%c8|cwt(iql4HkQ33DBHhmyt9vk&P$Qwx&N` z>hXjRNL9Y!i}VHPRkH}rcCj@E@ycejiulGq)za1?cL(TcN8Z>az9#J!3WOFozOsTJ zHFmptayb57(@DDTrZk_~9`C!nn&MkIWK)zqEwyI}JTgvai=mf6R7qbx33;*%KSxL{ z2ymZN|8h7?au(=aBn;K&8!QMtrB@UrFkfYJ=~xJTqQKxyQCm{RoI*}DV3Nw$)uJNr zGnxjlu94@@5Ls_Nm0Z2%Ua)B+0qBAoK?o2)W?NB|ra*n>{E8Je{r@^KT_zH5E!zzD z6;wmtriP}WJHSnPH;4fAX6Q>@gH_a%hOPm!h%}_0=IWs{M47RC4gLb^!|C_xKRf0Z zpH%*^_piRW*eMK#BCcTow(O|>phC zyw@#$#+EQZ8N_>=gtI_kxml=WNX@V`DCpWMXy32Nss3(m+JmPH<ppJIhDpMwqdA01@4h z3she6;vV7ojzt~01^-(?DY72N1G{8{T7u~B4pP=YaU-q!?alQp9M$NJ8v#P;KY<;g zN1D%`3J#8G%KV=5!sn@a&zKEN|Fb0BR~|mr4=E)OAZH%{X4@HONFoM}HUJ2=kNEQg zFQ2LkSsX!=)_|3gh?a1czY(Cd`rXDhfkAhr5oTVS7hIr2{_0B9AyL~irO+`TXd(lc z>3pZXHXM)i8MX{NvZ>Lp05N{A@ra*ZambY_lUBmTT;FHEGqTP94?;+(XEcIb3~xpf zJx(MzEU<^=eFlnMD?myzPjX)ZF{WKo{B!9)k!Y?53HrXTkg{mZ8}p%Lsw3$lc$c^+v98ChDUGT06zl28(s~BYz-;1z_>1r=_h8E zE-^~hAPDKGfMQ(=Dk%K_VeGA=s@lFkP(jo~r`F^!PTEZ%N$| z54c~hrshLB@Vp320O&zChOhRqw4hOEtrVTT-F=>RfTF9GXWhp!3U|IJNxu5A`RZWp zP(_UDwYG4FDjx6j7-7CMh&;dUd1eRhV-U!@=g&c}En64%G3#2wQzsGwN z7)5>2tX}kj985H#qKobj-Rw_ruLk+ux2r7+_XEp4?(`=`)3d>4KY2G!{X1ezS!omV z!1w~5iiF678OHX}dOpu+!oVQIDX8h6rLzx=91xJhidL^hu-xDSA-|>3^KOuXzXOS{ zkT0GY=0blfvR<+L_v$vf!wrbU!^F(n*l%f~YW`?$~E&$qyU z5n+I*blBow0lGXd?1<)rsN+9c*Y$Dz;k!Sqc?*LGu>2+J7%KuJ+H~X&-ClpRSs1kG zVtKIqSr${#KX+WB|7h`uC38?f`;scpT03IyOM|hqe5XbRTaHTe50 zJfrQ3Wt6P@(JPX=QpUt4J`dYiDY29+WPsP!a0ZTUQJo3(fQD;H^hT;*8gBbNsRb-` z(?=v!QI|n4q!nVGc0PeXz6j1H<<8|S`aL~LUdNO5c*25})>#zi@Va&cBQoq@f&5FKMKRVa)gM$# zLhuCsxG;iLjj7oWi41KRf~vg_PpiPYX;XQly;a zUXXULHbai?m?-ws6)VXOZ2p8OGzY?KZZd-U>~aMa%TGx|c@;C&J#_v2Cy5Te;eK;1 zei!QHT`%E3X_dQ{rT`3Gzh4IuaZlFI6e=|8H(+ed@(HtD*G)Kk1Dz z5t+2dK4K14qyB@ZBIL(jX{P$v^9!CGfg3ieJ4S}hmP%+k(A0r(EqE3CV@e*hG}Fe` zCTJSUlzx!#9JU~QceXVyR4^_Hv7o-f%AJl08&ldN)X_NVmZy={!}pE%Fb(6ARrD|A;fG!)(HILAX;xw)}15sWW`9 z4-C*iD5$DzR0w7_pW^<27x5&S=ZgclPrq~W?J=Makz(>)6-=~`y&^7yNoZWH%Wp^R@oVFB#mUj4X zd{wLq9FWEKpwqNXy4xR{%4IJbbiLGZ_ddK!d|wdFVJX;{%>A;c|6|X z7Whq#@4~Loxu&oH9$Sbw7`{AMO%JOUj^I%ndvY5ZZ+{Ns`a9_kQRPl|z=ziphF4xv zID6e$diFnxC7a4Ko3W_CE?qm+DyI)8ci?#KcZYIJTTp1*bCurtSlv)7xjO!nsghVc z34Z)o9sjM1orVv5r0!Fo+?g9UOg`e-JwMtYz>L`>vhPWBloPxvEqLFLVgRsGyz_&4 z2%>;sN&oY#Lxku@e$Zg}$C6XL+;+g6OxIiUlX?* zY&P{7|P2x5Rbreo8zWrD*2>|sDs@Y>;eyfM!F|6un>7H(Z;W#TcOVkIRR_LG9 zCUh}Bk>}Qw`~mHd5%RCx*f$-9vgHLZX94(m9Lj$@7IDCsW(AEX`4*!&q3qMJlH}YB zu5>L(zYuX7Ff=WXENZcVvYfxW@!^Jdv#5iVdhaOg@0I*}#G^>@-!1IFKJr4=tC`qiK4F}N z-r|dHlm0ljgVx7V&9q3IklRvg)f79JI!681@5Vd+o-9ld&!j~?MF)~AI>0G$&PQ`) z9k1#^Pqu}M@wcev`SSCE|EVHS3B!rVpK37@E$U;A54K*s`1{d+_E|-wH|1l+FGt`| z1lE$427uQkkUrCLL4z0VMvik_Pa8~FpmWGsBuw&PjF-=w74D?@-(~?ULN*zKZTk-= z-4XavPu<%b0*Hqcf4lZ?qE;g&(woTLv5RL^*nG#l=S})DGK05i^!DdOD2Q_948MB0 zp0sEh*ap%83;;u1=P^N2{5EciIJ8hX?6mb`&!jH2`EMKy86-f$HhS!-Qa4|3rEHQM z`3@n@iN?r!Snsf!;QQ|+dpSC zjk(1&q|RscqDx>_2GFg>F5@HeVU2WdU@`#3GZnWVgL`j#-;Ef-#^*P$9lska`Y~m- z#a*}%RjW%CKekc0(X|V^kYHJ`|7OAIWxDZ=zC}~B$J9%IGl8tUb}{_YMPF^CNI#?+ z(>dMMuoh)8H{9YU+L<98$)mP_>(5!6;k4y^O9Acl%R#O)-h6w6Z=xM>)(OwL6W$~( zqMBd+{PKK(1HXaDq|QQRDKB)`2@?gd2Lp&m@bRgO6Rb=}25;sqv#)*xWlO2M-`4Cb z-p}l3q`-qwrpkzVax>bx)IXwHsIxu@TfvI>q0(DBc@wC|pN-8&EEa!i_8jwT6YHFs?BE%ITK z>ohu53Bj-_=A$Rl8WiSJIrS9x z0B?=u`5rmt&Xl_iY{9fgn#yY_1k=0>U1VE z(UBy6{o*2VD7c|&(JUxHL?lN%12Y&zb?ZCnxF9+{C@h{aA`3Hx@l5$KxFmcEtPcu~ zMX5KRs$uxc<0agPsgAT9xI;mC`XYrSyE!2hP=@sN8IM~c(azK(|TRO;&s|I19T zV2eevL1v5>wRUtJwNXO7U4o8`(gTZeL}wpuNxsx0>j$7Y=iL1l;NM6h%6s8{s+^+Cw?aP7$xzo`(e`{*`i4)bp<`P|NZi$1O&P` zUfs1HnsZ$}3hZBaq=M;@1WIfE|K#((z*w>_y%qY%$oz6pZ{yI3w^X~&YU*1_zSGudQ>DmW3 z7XQB^30Lny>*XbqKaa0JmWvqS_3xsEn2^#&HI!O3N8n|P^4kC8jlXo|`xx*gBTO|; zE48=mGT+tV?>WGUVaW0Ai4pQU+0KrEd0NeYC?*&I6TVnLOD(~8Ehtszz$_?KGPl!I zzXT)792eMM?ZLFTt9j0kj1&?Rbl`3P?Gnt9-JD8RUhem_yx6J5g^*w&_}6~uoMS0Z z)Rk}eKmj}l#u%Xink4pvZVyuh8ld9d#TsYERR`uA+^&Y1n1^W2X6 zA^zf0ON)(^`nx8ckYa*NSa)g|Z4sKea1M>tgS3F_T|Yr@Pysw=EbgS#Zn{T+7AA0R zG^P{81kKyRZVw~-&^sE-s@n{KpPvF3o4a5P2zxLgYctocQ;4Up2bZm*;`Ujfy`IE= zjKT%5PuifohxPLv$Pj`#8r)ztq?_%JF@2jn?>_JaTFdkScN^Y2Z$PtfZ&ogZYEM;K zeH9ivS1ySBsdt?|z+9O1M#gOh z;tGB!&vLBgpg4ighpTmE{e|_E+q&lh+uyhL{SG7X-H#zzyWj~;Q-R&>e2J%aTW*|z zUC5d2NLp}O=ffDrBfS`qT1t3`ucGCdCLi`2%n?L|Y6d@-_3v3YtX`N+UtRXQS`^;N zK0H5MTbK-H|49f<35gc;x@j4R3x$gj($x-gsLZu*tY6>UzKW$} zo?g>$53AF|^X#bdgPpdNnQbNWWWl9;j7hfs`|@u=p|U2*=wv%<3C-D};mW>I$d`2+ z2odOzG~>JlB(3b1h?v!oPKQP&FokXdKQ7v8xViQ`mC~lbSe{zo6J?3;>LBZoUj}=+ zWbu2%AI9!IFu%qR?DJuMMCsQPf?%Xy7?~3=K>W_SbVvI{NIs5*7IB_!gLnS7RgOuT zoOsn_T2fT2XQq8%eYPjraAXi`x{o3JpV%S}OL^X= zjZtP0SIIQGp|m0Uv=+7)D^xJZW#+cpWzb_KO!x8j34evO1NtOQyJBr$e&77|C=*qZ z=&CIibvsFx(k+1Ew82w$NT11;Pj`u$1Q3H=-MWj~Q@h|L;1ut+7aP4)XiEq&VEgk@UA(7m&*MmHUil()!f5v0xf&||0k$GysSF``G2&N{ zramj%p@YXr+0$2AhMOE<5dkITaV+h>-E`6(yD#f7Wt$!sH3J3A-~#=>qe}trS=55~O`}PxI|yJQPCg(Ina2=; zXsY746jc+LdGHW6)K=&KFsLt3bS9O3^y{nX2=iE+SoQF$0Qbf}1w>5(XxwKZ#IK@# zRP`xk#W~)Z91XG4_|{SeDKxu4U_CRb=narmU;d?=&ma6bgUE-X)?$L?9)5#XtQobo zc|Cw100YBnaeEIl@{$bFx~R~3=3(FY z(35X};o+7jdG1Us^$fT@LJYynU1;KCl_;)+Kw?(3hP`UVcLnLAa7j{RZevywRfv`r z^5+f^$rb7)FE`uTNq3R>kx9xVxB!w}4HnUO=cc;K{-8xWx!fV^h!-Ape;wsI(na0hVh9OG0!Fa;rVQ#>!KylxKPo z0KoqVu_Y`i`~lsbHZ-9Xs5Izc2wUo-EfxdT9g-vy_z1|q+V#8l+6#cVNZt#Wwz&aO zOmv{cC}ue}`nNx=WE2n<7vc6D46fYd8rQ=W{Hg9#>01xtNx4;Rf8Q_LxY`|G!DC6NC52^S+Cl$o{<5q zQ_2=hY?`&FUdh!4jFUBw(S~gSeRuXa4pJ@{MO4kjud=ouml`% z&8MLc*ZVQ^{{7_)Kkm=GSx&7atOZ%Wwh(0cv-Og)MeI*xN)tc;9?UBRQj=uXV8iKr z5Tw#qg`0swWmmfh$U3TUz!ucLoo$z*7G=5APDyugh|`ZoF6rY;P)2K>0oze~AVt{R zUqy=7;MwdI zOk4fHbnsRYe>dOodVGh#`e}&BIpfPV^{TZ4+gg6$^OzBq@FN*znzjQVo;jVh=Ve$I2 zW|n(OeM<`nF|P$UPsi3Qe7X3xc1NTi;l!ctf_!`whON|72kaDx2Y1+FP>sbc_%#%b@h1(w@|aE!pfaA(JWL5v+c-rp=RFb-)* zpzOZpc4c>f~3y01d~hq8ym zQZlp&eBUlOBA|4zWsjFBCXGVU3ExhoO=HhI>eN%Y+I~r10xUJ7U^Fux{(Ga;(C_wE z-VYVPrmwaHRj_lX?T$MyFWDW)Q8H%T!xUsQxrZ%EdOADnfakWr)HtBxyCIRTji$Et zo^Wlq`~sT9XZ6%)#R_w2+s;6Ntmga!dA3Bi+vixyg`ga{i63WJ zpcG*BF~F~aONcCjVRTVm9)y2-l`%kd%$RWhot2BfRQ#Le2R4nrsp! z$z{v}M)f_UF^t&BY>)4`&4BipD1CvFim*Q9cVT=fAEfoU4c=aW=Rh%;z#$kT1jMpk}t7>N)p>dsD#o|_N6Er24j#!BYxWzy`K*INJwz*6T zdiK;lQ?VP9p|OPe-o4ET>1TTnH>J3Uk7NBfiMMyn4Fl1=9qoamYq0T>4nD*_gx{2a zRp4B^9LBRuDA@jH4p8Pb$`qOhz5Qy&umZ_}WKC&M(siW8fKnvjwiK~=GC<;r%k_=b zqMqnA?_MiQmEDZa*v4cUtyei}CMO?@aWT5ha^%)w1=s`r+ctJ zAYqtPqJrZMytn~ky)Q_v74dW^w{sWB5t*dOG1~g73yw46h`k&)cKPI->*m@d4e@@9 zYGvdq#-d{O*Md;pPf#1U7uT}`1P~Np#xeHexn!A1O=R_Zg~xaP=GX3oG)mCNypo^1 z^-uX{E^)Bwpdy|J47>SkYtUGwe6>#yGZOJLMC` zE1!Xe+s#2aK#H`=-=0%0YOrm#iHsL zsIN2bh4F`W%oTzd+~^AMLH+8xb9KX;bS+jxQeWYT3pzS?C@Z?l*Y5t5M(KzZSb$7R zE$WZNLwHUBfcpZ*fDJy_rIBHPlOV1O6;OlXa1_7JT>Tb>;UXGotp=;w_7QgcaFS); zv|?afZ9ilk^N;WnxB&IfIUZAQ8SVEFp$ar-^}snEh>KS41{sd^@}%L0sSCkoot4oq z$=T^r^{bpj9gf(!Rr|Q*iPVRF- zl8zp9$Cp`Ou9JWA)*`5)bJe}U9~JBFbM9dcc4b9#_Ej4|CR|yS)@1l8m7t$($wqQz zZUG|bWQ~iR!|n42#{K;IaS~lDD$aqEft_FJT$Q+og1$$^v1*TXdaP=9*FGCGg=RBY z16h&`GmLNFU+&h7i3dx;CQHp7y1xn%a4n){!5<>;e%GU z-!LNs)IAzrLnbd}l6KxhKECaNyKxC&XR(9tE>3oKNF6iIfU@EPD4WJGR%>=%hrGgO zlo?}J0METaCBkG12G7n>Z}bZ6Hr{Q43Wu-b@_s+4E$aA9E6|;>XF;8?Bx>V}K|&0v zLzd9+dr;ZG2qxxu`U6b7H6N1x*^j$m{2%wV1?W$Dr}beci&k-|1bA9NbL2bQy6$se zz}#V|X5Z1^aRim_l}nX&;Zn2B=J%{56i~b-GpJZaO%vj`F=2#jy?oZK@s`lO2dR_p zBaGD1Te3-%B2hnDHKOS*s+HDIka2Y4kRat}fLz$`iQND@IY2Cj?-8emxui$GZ4p!P zKKiNN8!qYh=y&66YuO*Siz;nQKW5(v%ES+P;o^KUzurh1?tmfx+trlLO7)T!kZju` zMf-KJ+HjOd-v7C0HPP|Q(@v_eLrq7e?rFd{xvciRezAoI&Ge6us`wrEU$z0g&X5FR z5;b-Z*u-e|oFQfem@OAjRR?tg)1uUcKHX!F#nA=Ls>oo;oU2y#!_dhD_*?*3(}rvp zFKWHSd17lY^)d7rd5{*sB^^@`&>6u3rvYo{(s^%A6`6}3Q}D8|V715frC16v*v`4B z87#NExz1C_;jr)h88r32K3`-=Zf!H9LF!{>oWWBX9^n4;hI~5$UQ~{WcNkNwfZ??Y zcMR0|B&!()ty%bebo04qlHf&kVVOn7dgz3LZ&3$qL?g6QnyyE~Ex^VIT#mH-$gvxK zNoT8}sy6u#|09;IUpLlmvn{d`6$}yNyXe5@^MHz3U)c`Hjc`#k$8YFM`q>*P!k?yW zLKn~i+Ndy@d7R#Usmo3@^qB_!O%BMf=UHXbgS^GXUknX_mh(t7?T3zHP~Kmi4QhUa z#(Ng~#Q~hH5a8DW=0@Yh*6RX9i4nwH58tbF^B02j?I-A0_?{z_Z)9JLymuI?v5|H( zSS#J^8zSyw_|7%b%tKT#u@YIe6aC)gd(N{@A=&KH82rzjP86e35ba*2sQK0bdkn|r zUYI*0@p0wbUnV_^eh_LoS*EpVlDtob`lFoIm;zC?=I;Ad{HWn1+JV*ff|Z>&JgewP z*&Wj)foiB(cQLrhp+hF>8}HYpffFZk9<9WZrUR>)HYnWbX*w*&>_${+8G##h4yfGL z{WbGa7fo2|d>iFpQ_fpXEIPXB%Csizgud9e6s1JfwbD2yBrslpP65$*?t7p!r7IkY z`R*IFX$hcef`}R|JbpJFFemg z@Og=5&Inu{oyNiucOO4)Nh&PxEO2wbnW^B+OHS|MiK90DLHOu-vZN-fT!>4IRfxfQ zLJ_G^vLKX<27AINn4LDMEyB(Y)6@*k`C6=hbwpSlO~Q8XpKkkw(3aCu#}VugbymC^ zxhUleFH4e$`sv0fa_ndV(+w}~SF>t=8r`uUT)W@SxXhq&<8*!#TQJiZdk-*4AO8d_4~G06=@fK&2N*0#5Gr^LZ{R%GDLNeitkpP0Er!1k zv_cU^jo0kb2dIB55eCoUSzVU#@!-1 zBeqbRiIraxtUxz##Nlq*2;$T4-WRFY5}(trov3Ov?}d7PA@#h| ze4}a~D8d@S9O*Za6rqEIekR=in(?*fx+6AWICGAhW<4uq!l_C`0Us-sJKdq2;N}A`k$KMH zt=;2Th3g_N0<}8*_Z%*36ICd8b3v1dA!BIME*gIC(<}l@CZ)F*qYc+fqMk(SK`Z13 zJE;VbLAV9!t~-zfVzneV_%XWN%+Y$R#d0^rtTy+6MwdyHY;2_Sn5u{Wo!ib>yMUad z=!f%8PBbIOZURTohxZq2T}M819cq>Hv6gIdGn^ytM>FrYI8#~p^7llH-blD{S?*XZ zIF$hvxlCPDJ3$h>n*5Q6hmyhv#WU^7B46gDdcG{m*L341K%z2AEnj%cY+xN#htDs(q(UrTZO1 zo{}>H%Xw8o3wsk&;&P`QGc~ERB>!)>oFn;XWexIWhV6sKSB}|VJ-Q3*4O%@C#`JYZ zJ5?eB4*Mi<%tJ~`h7cM`fZ#zFO!ntW{VI?+KR<>!7$Go#aK;L<1vw&3DA zv9od=+(CcL%>tuylzJ zUt;Rh9=uUs{=GI?VKHVms_~cM|4bq3@{^Qi!{AvYNbbBW%9vh*n4&Wf@jnA9c}pNQ zojr90f{_a3Hkc9`6Fn*DxlQ8!Let2t+Ssa2{b>}1+i`EbC7O5byv^JFQ& zKBSLTo2QIJcP2c7M((qKNzg2&V}+KGEb=!<01`wSoUwumpvK2oaFmH_R87C&4PWie z^~GO@`(HlO%};m-U>ywTK$lo>7?B!cG~0jOjKf$$gJ}<<@{|Hz5jzLK4(&nSb#_J{ zxViTT$ZTi##zMse(^bS}8g3|};u5dYe7-V}x2ToZk zDk!Z0%&>0==R{}SA!OYkySp0mPf9FB@T`1F(XE8PpgsaO^LSlBj;IefD8?Z12e*&J zi4{EJy2{<2DJPJ#S?hr9mgffm4cegO#Dtv7aL{7V41mg%bXXg!LA)`|NjJc;S3dQ-+;MY;iZvrV(e#wTB7 z=54NuV*r!1mawG=S^#g0Ra5Xd4Hia1i`GrXK1vEz)BgS6zzGs{ClG)6Nw94hij_a} zcbPj>Gobg;b!GX zWC|0$mTWV>zyUj0uX)!pECFxj3(7{8zfCm2b+yMIWTicoVEaqTmHQbge6H>UIG+4~ zklP>Pv3i$4!&V-ey#yVjV1t>!$`Cjb1hvVD`{b!XFvb%fQV82net3?XskV>?*(QRP zgj9<^^-xiR0$G2MGeHKQQQmZgsLGS1FOW(IT#~Ag`AACTx(aA%&43jk2Ip(x3U(y& z{Wx|8;M5P8!);+7wi}@%$n1d=-Gd9kYqSv@ScB&atjE9=UuG@;p8zO}EOu8TuOfy+ zGjG(mY!_lHp+=)I5K&r$ACTa!4z)ZAJ9bJ7=sw{pDDHl$4YVp7*E!H{feM;u2QJI~S7Ip)V8ho?X8hBq`K2H&fJ0MmCO2Or?#|MBs?8 z&MYQF*_JboD`)Gqpm{g_!6a~ywgCM@3Fn@^I|ya4yZ6yx&{1h{P}Wa%IzR_f0mUbV@U7^Vl8kap1TObaf3--aH=~@+a(F|-Q{Nf=c6J)Vqv#)*dWQsY3LCr(0D$e z`N3{jQTtnz63TBG_7LZ3MYi>Dwjd)Juz&(G4j{ImgC;qxNr-k%BN2G(3Dr+w*8g#L? zy5yqza^TZ(P(YuaZ7fh|UHmabogpRE61d8IfaiD0ZzuRMPw^9%b26nlDzp|?a6p;u z30@*7aa{$-e9>9)Vlk2bbsX$a2;F_;w=hyon;)6?tJ-Vj0(jEZoC@1-SoYD0*q#(t z`idc4C)criR+vovId#4nja?(oFZx9;Dz^M8asVCMO4>jKIgXkrf*zfcKKwhDI8MAb zR>0dQK9nVB8m$~PH&<7Cq#K(h#{$j;b^XR$b;-&7#mAZ(8aqy-B}#_;7mn_tyhyEF zzf;p)MTB551PUwy@Sh9Tv`X9DD6hvhrfVuLE)V*rfhOH!y@o^qA%0mc-DuK%($aFX z$@i;$8B$*!d!svpH7JTaH~W7BPb1D~Vz>9=!!4ezm7ANszGE%drBWlz+* z+XZSOF&{CcAV>1{z@pPZ!dk*QfOkn~1QL>J#AEsMBhDujHHZ_ZU|}JOq%2}%{b4+l zOhNN_ihEptbM1VxoM7?Z6IEcPB~vbW8@vGqGq!dcBW08XT~`rQVQ(g#j@Eesm66a3 zYJo?xW~nEp29W=w_N)@#RgjZ&Y)Akcy)}a04vc&|m5{fi*{xv?NTW6fEBzHV5c%s? zk_V2odzaf-q1T0T0>V<{WXw8hTE@GPLxo+Vn!ndYj^+P!)he;}C~aDY=A@bLEd3}m z9R&eluTOCMtwKG0wJJ=ms+|tH#g`!-rv@gbuZ-npf(*%ju=enPqv>!5$j6{Vqb9+~ zIa#3^5CusPN!p$QGvJrtB8gxs?{e`V{Alh+tj~JMj}&@DhuA$Czhk#PpfDdV)-U(t z5P#O(?)6e~4()t_cLYS*jG61xSMG4^h#Nx?gy}}X1llpJySVqet|a}<2Ve`-S!5K= z!7*a9{{BYJZ2#6?9SLhC4Q4AJ#-N^&7Iq1AG^=-+GFG53G9ta0pAxg}Iq+8VJJYrh zy_}ZMRmxkX9SkoL6LMjfb77BmQ2vC22yGfq8EpUVN9m+7Yn4O!PS~Bfg_55BoMsa$ zaE7ywScAJsqP5MPW}iGHC{S^<$ecHeh%}g*Cq*&&^&Ah#cc1jWrrx9|E(&WV)NgZ$O13|RuEpiSc>`3{dA+4f86crNcjb=Sxc+fc~f&Oj8L+|d>W_} z{K}lZ5C6C=jd1gwF`hqzRvin`$Lo#_yFh_vx$bd?1opiWe&y7%71us7vWM36q2e4) z!6z1lyxZV#KBI^0peoRGj(Nuo-j#lP*#u6GXK#&0^Y)@8Z*P`$PF<6fF|4E>839^4 zp<3+SMC_k&d1tyoJ{DJ&5fgRJ z4p4+t^%)0vB28xOu7^Qid=;i^FwGTh}Ap7tUmS#1uH2uVoxeT*9wV@QDd89+N+)p$Wmcnf^ME62);XV6a zoqmriuBLMVQo>pZv{!{-c!Y@Zh9c>t%38a6{OWwAq;99$(j8DAZWYv`grXZx1x3rU zE;0W{J5wO(wxcN5fdi+^k2lA+fhNesisgx!j~FvM&BP!B;zJ8ob%9|Q?Q^ii@HC=v?FxPKnk$DpqX*Tlg?8uF&Nxy ztVMQSOmzm+`|@u^{eSjJx9;MGYikmo#4673tI!LYX-W1t(H8L;U3Vg`0*@1B76JTA> z1lX~$G&EZ0rMYlF2Lwb?P4BLDm577=yyA(c$Wsw&>wjNruuv$7pTDW|9mk4CMM-MS zw+3u4evz|(@YvGh1yJxc4wn052Nw9_f5Q5|zHrEQ2Id}9$}La_ou?Z$ICD zACmjSX~tUjyTnD%>Niro>2|@*TLxYO8;HS!`d*kQaqGU>Z%yQb!e+VA=l4VLcY_hx z!$Js3G@k%Q*a_ubryI?dr^3{4Dmb9oObuicbpsZltb6yY@+CruGt)B72*;4=4e_r* zk;IG|;L~X%;9UYH*d(aH2ZSG}YJP<_mi`pz1pft5=KB2M9p-!UZP5pz^N&;j-OMCo zI8*DWzjgUwf)eBHFsGs${{$u4c3}&K`Tk;;lDI<T=y*c*= zS|g?NKlJpWLk73;!ygs#y1(*+A$vYuhu&{@it=e-T9^jykvkZCyF#eBorlD;nEoD& zw278p3D1oq>Q31CHMiz{KsY2lGtj|Tb^kbjaCmQL*7#VP3Ns{(Q{kb_vg&wgk<2Rrwk< z)u1CKgAT4elZd$co=SaqC12VOmmbZNTvBI4#;nl;!F;aS2mkc}AWC?n!4~8(n8w}) zLdJ$cZ>%<{6?_DHJBVBoBPoTVmZBK)%jj!bhdkmcN16mnS?qY{{^SD5OKJ@icX;{- zkx$T>t!!aJt*`iCSUB`}IL~pM*lS!)=U&AVw2CMDKryH-_>NdW96SC_Rm@tv?q!ea z%yVRx>*wA)2Fets@*x2Y02cMp+)Y1DT~K-k65o6R!wK@x_}_oKC!B(JM&pEroiJog zDBw~l?u9}8yt8)Ke!`emde}?!AqSa%PV1Tpz%NCVKNi)Jc4DFz1be{ysSnj+!}m%CSPE-oLFWn$h}%F0;_( zJ=uh?x7=d&5!eo+Kk@J|y{DLOiUqF;cF$C`-NyqJQEsXL3Z7#Yb#m-cO{piVbwFVz z*&B$%|5=Ff_7YPYv%g_6p%QTVRlu~>W(L~2pp9ShW}e)J5Koqg&yno~1smR<;4`Y6xxdLW2OUI3lTW-O zwB5OdepyohuadC-1i*w2Rgr|J%i_^m0HG9`tI7q&&waXC^I4#$pnyHVx6`R#GBG;F~3b&mwS{ zO_c7xV7ru9_3^GlYYKE^-;-Tw<}5u_fg)QT|fWdmkW&@P+*Kxl84m|`D5Sn zn2VCj2?5vled9U8l{E37A6tOIVw}gNErkfuU-|boX!O{xE6e&YuAZmgfK4|I*!Yj)RKK48 zes=qvScu|bcjncf@yMmUQozd>IZa`Ek@^7C?s=avIdOM^s|U5`p|qfPAx?9nDQz-q z=NJ%L4DO{h!?1mxc1raSvcSlz3|mlCSQ1_#V0=Pz* zoV)@?ASa%Hp|z^b%1=800KN?Cp8|a+*f2sQPp!hdor_^Zw=05LsqzrOW%m3k5F}Nr zqku9zZSwSU@!g++rxh+|)(h=W7ghp~O(q21r-5Fkx6%M6Vv_hMv{ahv>-tY#Lsd@+ zgfT-_JGLk}NdH@&;eb10K|~7CrcCR-3`wR~cqrTy(ZX+GTq!Av)*Pn_x*~8;J$L+A z9rn71D&w#Fi2b|25p-PeQH|}|TZd+3+Dc#qAL3i8)#`BkPAn%-o5b%NPb;`W$@JjN z4mm{2KDy{kH&&k77IBURm#XYR)J8w|0vgxH5k@MX$L>J3BTQY4zd0&!uZm_;dmrW8 z{)B-PlzTxdn(MD_w>>!*Qci+J^8#>96E0}_TMp5BIA{V8z73GC?a3ea2FJc{HBSRm zuyKM?M!!iDC}+JU;c~xr!JPo4rWu#Z>27J1iTll!gI@}l)x4M_fI;bPIzPrR>1OpQw%BSz$F`$)>ATk`l_Lo9T@{(ZRtVst(J#dlb-Yi1YCs zI<7dUhH+c~C~nU4I-3+KXU98bysvQFe;IY-< zl&Si2P`z2(^ocpjY7##;($Yk7CGk-vFjxI9F>^j zK2~zq7h1KX@Y(vse$Xi5?L+(7!COSd--40w$x}y(NH^@^%lL9Y2W12Vcq@6Ue&#z{ zP?6*~*Nr}bPJITA@wq-akDXRim%sUUXaPgXyPu~L{8^!DRWN^xeDzB^``g?0$3%3T zOF$-#MkYw#XLJdYdY?w=VE$(;)8spknz!8XkeKh65fsV&185>CD%c*T;^99o zt=y&k4Z3vR6DnabJ#F2`Xn!PN1PU1I#f_1sr_&lriQjeKigI)YG=i+s{gkhMyKsEe8w+)5N!d=c#ayZ6nC&Wq!TZOG%X* zw?}&5kY6~t0I;cV15dRj-n&NW0+0ct{}2T0wFLoSv^O&;QpI*%-@_(J{u9nxJ`Zc| zxpd3cx1wldPelFmWbEjwEod6BpNk`}yz85XXB90uXtX{ZAM^{VS(Pd{hVM1EbfMDb zD@eeZs>0|yB~sCU_B>(aiI;lR^5i++=!(wkVa&v6b=TcV)8GwJzciQ{i@pq}YipSq zSS&ZbjKEK!8*LxeZ$m1`lp!8B;U3%sieqX_R+|ry)9gTRNX;-V5|U_S4WJ1682UIb z&jerMk(zcXzEXR+LrL+YyP)-4!eNnA^Y46{v^(tv0RdWY z99$z{anSbBcD_c+LSrxgomp%Y08ABQ{D1`D#O@Viqo|d^cn1sZM5SblaMasBiyd$S z%qUZX?S8#XJw$Qg9J3+3GP~X7La<0SM7LwS;;1|;#>nq(lzvhTVm?Lh-IrPSQn9

8c*Ru>bK9l!!k2 zG|7IS(e`)G1;#NQTM{fmfo}_HYD-B{N4-~85y$>vLAW}!6vW=3#1^r{#-m7!B6LbMbT?=`TH8EVP*USD`*iDaZa1UO~ZQYmj?QivqB|K$$7F|iC!X(ESbwmAQCfr7_ zL5gumIIQJhsUWH7GF}C~y>Oa1w-f2~^jVP&HBtYl1H;->)Ys!IvAf&GQ^8h>-Y`3JIVdR3W{Twz`&0y!^@dB7hC#c8ms~-gpd>dYPw?w~pI=j`r`P5OiIe1s` zI1A%*noi{ zk-Prcv5Vjn!%URInECFQau1>re2e1vN+1)Y@LNoM#XldTot4vV^xCif#fi;mM1SJCwve#El}L3lg7*wVb-Y&U6JSP<8~ z@E3doASSU2tXKHu@pJ&Cbp;&hYtB%<2gU%ZC~6=HSTnX5UU}P%?X^`hSu~S&)0syB z%DW$y1x;yc)|7bpJ^U7>;}Ii%=8kTllUr79WVKiK()$)~g?8*aAkm4W=O4gsz2`_J zNGcY@N4Y2?Wr)`q%VT3uO_O>-xL{T>ZNPI43k%UXlpim{L7Xc4s7z(5J=eJQQ~FQ5 zjEvu#xlPPB`$TXi4d@aS?G|WNeEAOfm#>y%^2Bw zT?VFSo%Rn$FcpJ_h#gJ@QxHLoC4r*9mK4mgC4MyP{B%5FewW z;T^w&GujI#U^I~;8uZGm=>Hc310NzJAGG4u$P|>wjP>l^;aens>s^BLYXC4##Z(<( zWEK?YeYh0(0uRV|-o%>e)0Dja%BxRN{g?~BtJPy8qVj`F5Vpk2)# z?H=cDpfsmgnSwWpj{gbCf*(sJdwWkt%8yb=@YuR2$R+q&pLp&=HPPY^cxyPKPZ~jn zktad(0+3nQBu_WH0@Ph+X2YOiCriThU?i99lrEVM^YYKH8=#To!KB0-8fa%Bq1OOL z%+IgHh1@Z?5fY>H0R=XKsa?Nl=li>vd9u`a0g#A-(pMrEI%NQvWalg{N*Q{uy;Tf* zW#6Xc^JLoYWAZ=t1|PBNXNm#C{kN*o>pDQB&wp6ecoEuF+QZy1W$<**_CTT%2~l;r z+AbvW45@3e`o(I>C&mtBiXw%0A5-}~w~12Y5eF4`+wNRThzIq`z`=?xdX?Ai97o3) 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%0A>A|68fit4F{t{Cw6>&8^RiVyztb z7%vFb#D?k)zUd=lc11T%J7hMDjE4#6rmu3m7%Rb26~Dcn)ESu5gRKZZO2LE;=%UDx z+X_F5c&G2Gn~hiGsF`#Wr(sPB*x;ogy*F&|Jwqr7Um%PvKpHz&7s=R!zNEfs)_*J* zDJQ&6H&**Z)4H}>sycKiNpK+Ok&)S*A0VtM$eQgnuo$@ko`s|kelV`!g<>_c$;kuU z7E*wt^{V%yJZA&A;EYa!iamUMIqknCK)Xo4>1^|W=ks!K60~7aVKj^Vd9ddEq1qr1 zb^NE_*w>}qZ>4XEYqe|L?S|jnW%>9{n}eD=jkk{7t)dAwcPiogF`4%%0}Ldz5Br|4@&o$mP&xK!M~%}_^w3p2d{$?;@8n67v9#N# z*=xH9nx?%0H1EXv)NAkQITHmtsAcRy+Ox=Z`v9Dm{x%{fVY+SEop`<9KUAg|vK1}o zJLCG}LQoI|Q1yCQPr3`ovO((@;@Xk|j!Q)WnFz6=8q%Tr;5B~aNL>l&92;hpluoVq z7^*FpS1tOhZ3Wi3cF&D{y;h#7SQ7(-WzL$Cvbhic z;ART?{CMD05w}Cx z<=Z{{{`XeJ+?$}iuVBxy;04Kl0{8&8$^?tz->;r?u{yc@Uz7{(`A`*d=%Ap@jLxB5 zl&8}0L=ri1ZiY;In~pw$H~LuTCw&u->;#h9$%zt^VXJh|ES&)%*Ea{m_M%q6w6w?y zUOzr0Ejm(q5{V~=#+`wY-!A3>%w3Bt_m_tj0TT0o>r|zW;!Dc%k2@z|Zp(l2=ck&4 z*1tVDB3_C!gjmXP{e&W-x47|iRphlbS2($`*FsVsP|7Sg>-RtJh8?Me_2NLlS50{+itqo2%&EI3AuG?h|^o0yoA1?m!kEL zhI${3&P7shHDJBC4?`}v%deSGov_DPB1ZuSM+SbUd{qO*xn~fpVi1(!v055Ka@FV# z;<|VE+GX8I9KM9Q;YT1v=I~h6z*W5!g7X9V-_Gk7W&idcnQnbz;fp4A`)JN;e|V%B zmYI9V?S*8!e=FFpy<^X|Pb1RMSAhq7P2=&slS zQS|E2YQwWOJ$b$U;1&nqlQmSiEFzJ9me2UVaU)h4JxOaG@jnGpSVkP6DdLHI&9Sld zD_Q7I&XAr6Y-24@4z+GP-xk-=O>BXh z;BDMOxm{&X4lR008x(RPFczY|WeMFMHBkAi&uBmZ_j6k4PhzW9k% z@0yGAI)2+XNM#ws_mL&Gty#&}lQ{#YyzjE4l#ZVjSyExv-h3O4JN*jmey*vnnsGN~ z7W`QolsKzfh3C10bi)Wt0-{XzsocuB?osDJo1qGWn{+B@k1NA0Mz*~dQ<+FcJ*71H zrIOGo7}Oaym3EZiuKx6T7It4M>;4DN(u8wnCK0p0L8zR$q}OFTwhj7?E7eAU&fs{K zg0OJhURkV-GO)20ik<8azn^>d_`{o^Lp^#FQ-t;-js1t);V}Q%C$~e((X^Ml=Nlvs zJ#vRpoc9Y~(Z(j2ISgjsT7+g8n@2%jf#d>nx}L;K_ihwL3Nq@T*uYR$5z8M;1kt7y zLaUy7bR8KU#Jra{syDysSy2VL;9Q}_M^&!dfug8-S(5%2-XUERcm&Y&j zlv$vVq3F4t{v(U8T{5R4%)KU6?0hv>g6&)lc*2Jv%19Y>Cjqs0-}9}B;fRd0(C-0> zAr~`4`wh;kgop|vq#am&5@50HX-1dDvB96U9MF2y4Xe>*HLj(=xUppe>l!}cZw95sJcJ%j zkLQO{aS1zeTCj0;%h&S6DS$)RerJ}+F2X@y^Mkxl*RoLa#bovhr}+HqUMS zsB-nWzP*!%uWbDAN*nuD@0~5tf%t)$#sua|ufKz6p?NGTLRoJx-K-=`|Pibx(G*76+Ko`f$xLh*ZYjxEC&{3kd1EZhsp z{e`AxxJ4G`N3mRT47z6rWxnYVlNn62$gcBX(raz1T`j z2JT0xOTo^AMD{w?o!zEL6>b|(XW_$_4CN;~vjjv92@J8;Vt!OjC!{uTM*2D?WYm}3 z8b@ymYW}D1!Mls1LFFSGRcx}?)nHcnqaoBz^0`Nbob1$hm{MFDzco%4^5x$1mL!3w znGZs%pD{Z4Ts@FU4UW`qvjozx`i|CerfW4@q$PbKdo=UZoFHaiRg!*04N1k@6VrG) z)g^C5q49Hm@;k`nH1EB$qgS^>t;^8nJZ-rFMXpKJB0gi_~6|DnA*a>P#4*$t@72(6e z!%Z!#jXJkhqcHkU(`Ue_{;r;r-3;n=$E&;G<-ne zRTcSV?Q@>ek)wf8Y?#3((q`)sa{#~=>dWEQEh*Y%LWcTRJ{1h?@fFbzHa++B!2u=xCh zph-TFYG^abEeFXm+#E!TWS*=%Om9p5Vc7WsI7#(1JskEJ=U z2eVL*Fkm*9cBqel5KXj$g3x3e<&*g}B$K))xkFutc}I#=q%}4Z&XqRd*_JQvm5eiyh@Wl0QxplzSR zIT21e-xjJ60pG(w9e{S#Mp&&;1IR|$OgEA=j`_z3k4SwgSmC%CY}{G;bE`4(#w!nn z+fkLFbU}zAS@8D?E1_Nm(YW-V@1^Cx+%cz(SSUFp)V7kWQ0=`wXWAY^r1y5Rb*AlK-md9Y;i>MZwHObdtQ!4TW21K=qaZ9dcL#dT>(W1=h z6Esa76` z2d$+~X4>NEme^Ov!QV6EQvcF1-Gga(-31W_ze|5#rual(U8vbJI_aik4{Fq-J;!M( zAkXxd%4?x4jRPn#L=E8TVBw+$c;tM5XEBXtRsw9k5Hc@#lN6U&WhGZloUs?K?!G#g ze&k6#ol=uL7U({(+#1b2L@cfGQdK7opb0;&n4;}Gw2HuUFY_xnl)CRxWn98}JLnY5 zMK+jsL7hzQy!rv6#Om1jzC%Q5_@?NF-tH^X^(xS37$h2PP1WCB%w;0q0=m#mp)FAA z%&pGS&@bSMu5*uEl=}`Az-&KWU&AlB4xjv^h`Rn8e~W0mOsRwMw3&lp{I9pOO(=aX zGdSkhiXz&FI8*8X%qioiQ!B$$&`XiX&$vux$^*-%HFHgpEz&1+zGf4 zE&O7%mr(RxtA8Bvd#@=-T>IWi+!BiGksv(O)oQyVyH@%R8Y&B~n}+1N(JfE0Rhe&V ziFui(UI)>Ko!5{>g&8BaKiaz?&k3{+25VZVUyS$s^{bS-3;0Ds_ZwQ|a>*CaAQ zU1J?bsn>qKE$_#q&T+#p&_Hg7s8RL2ClJf;zxqK?Bhu^gayrxKDpZ#)z9j4Fnk|eI z;cqz2VB)d}1KhVIk=|t2!=FTKT*r{7olKLds=0UnUr**v%7u1f4Id5EB_p9G9{CQE z@?qY`&mVipwr-U5aAI8JspX_nCsk|e66!Qns-Stp>WCiS{Q!`aC7}OzvCYjo3L;T- zG9dr}UkEyE3hMj24r-qiFyASc_Oe479sm953_%_RfKYcA zqCt(`Wn5=rv%9^hWYx%|Apd zM#x%zaP$%%6uoWo>%ktIGeKrAN6O2H@aE&%FjCl?3JzR5uEac=sC*|3ATlsb^F1M2 zXMnlBM|Z^kHl>hb?h^q_2@-~gm1XZdOaiR)9pdQwmD7cW_SZ%Rkk$%fW5;=Lr)FX< z_jkc-z~c5O{M`v>WLI8lmcPo?Ig$`QWV6Z@KgW6WcyUiX)^CT8&W*C-G+7jCFMiS2 zra!s=zTb9htbBro)6OYnWcyNg!#=q(sxa)xVyXz5cKn`dO83rJt4ZF9B4xq!N6yP+uS*npX2EQtnt+49vzW?`h3zND?SRmoE z2V(@M>K%|I*&w$5sFw4i`bZrLOs|DqRza{wGW9Fudc$n4bWe4ni~;Z98AI*(y*vNC z9?viISF^@>z$`2JhTdR}@oDHYbmTTxEV+n$Bvm(r{;=V8UQZ|^paGI>GfmZ=BL7{epPH9Xy^ooo@#Fh0;Cm5#v! z)|9)NU-a?I&#qFPRkf$@jn7XFOmsZErQH2y?SWGA5WMFvy3Z$t$P!4|;KK?SR21}9 z*ne-wrjeL@`$79gXmnUjlJ@;KC~}nELR%2qs&7g|5mxa(WW5DcRa@9DY*LaU9ZE{q zRyGZi(jpB4Zer6Zog%4}fC@+{(%s$NEgjO`9l}4CdcJ$V`;T!3hv7NG+H1`<=leeO z8hC+YT6z0~42&jK9ZHcUy8ek(1G3)bAxD_?3^;EOzz7LayJMrwwCYjC&AANc&K${Pd&Wc?JbixIqoYY59 z+8romtQm+Th!da~5tzgJaIlh>(mMOG!QxoZ76pB=V}Z_Tu#bk$m!isV3a?Z!^!Hp_ z5U0j>r%_nih;bFeL@%A-C#oQ6U&L>YJuKzM&;CKP;AjH{*r~0 zNS!svs<-z^Q3}(~Jb&sUW#{6dNc*TwmpY8tLwkC?uvBLm@GE35yR7|VD_s$C_892{ zCbgXMM9n)o#47e$EQPXh@2t_NzgiL<21Q=ib7dN7=5Vw{d+nLGJummj(wgqHm!_S4?Wd5vhL)o{9UkNr}mIw!q zn=O#u+k6W`sk@dXU>6?_lp~I_bv-jXwONZz0g=&a95Z?-oRNdp4e!vxiOk6++ZV#lD1HnkcbVR1tH2`7P z@vH&iKQ-=ZlYm~1hC-N^1|~tj(G@VlWo`vRlJV`1ID@0A-GCJr2E3D^jkWucCUmUc z>DJ_?%s+eGaaS#v*G!|zpTLhFSNFnOi%Cr#8p$5~NVLc8xgL)`FPdMmEvS_|XnSdB zyN@!^`Dmixu)iozyjz#rQ{H&D9 z`Zai!hOlL}uasw!PUehLcTCddoETfD?J>un`{qj}vM|ui_Jr0J_u@c(;Ee#WkQJ4-jSZw3;z#^2rsXRsYF2m_c0mPAhVSGIm@13v=RK%mjt5*PnA zXa_BUK*2=U^9&3FTff6b0t4nmc(PMgPN^NXklpdzjcNevVoriI{A;_F*Dgj{9nVEV zYk~vKDsKp*+lGH*vpv(qSvTZukzlpbj4<0~Yn2vG>mT%*Pf}+>J`*%|^K4oy?ib5+f<=8=}H@m^b+YsXO6FOx3ww3 z!T0wegWaOoE4rpbS@W?!*8f&pncg>8V*up+jR{>PuV%c~!|e@Nzp(%ih#4<3u6pGL zm3j}&Hu65rl|tsxuBfv4510hn)X+>F5mY*IYUdCA?vjfA|NI(=Y9sjka*Do9mvP2Hr|55|NpPP*TdT}p zQiaM;^}jqD@LA{}S^|Cc5+jPOP4_PC#D6D81$0$BCyf|*Zc3r|+$cZ)lQI0+(ivWX zBKO|O%#!*W-sAl)<&u97>Mh;d?-KZAEzf{q+mG*`4*lN^`}hHT78Mm4TkU5&$Dn~V zrgT}vgECr*rtIFc^SK*efL6+UM9=HDuiGhzZi?aGsU8AyZK70YvNhy71Ufkv(U@OR zAX{Kc00eSCbp#ivl1fN;Y`#G@uKee%M_>%l@DymCmO)ViBwWRuP%J!{xnSD5pzS-Yn)Avlw$K{=`S0dh^ljyoft(Gl1DMyxO_G>SF4BOU|UNC za&=^*FS@_UGy54p7IZ@*BOzcgRtYfuOGSz(-G5X`4sO4!zOSp2{-DHrkznu{f(Lp& zGW}gBnviqkZgK>}OWe>J5JE8n&6rOG;-pm2idvZhVk9W|0@OlK1b`f{v~^k&4>%5` zDZ)W>2amNVd_Or;YN-tYBT!t>R}ec>UCpAvpL7Zp*lfVkTm%?}Dh~+uvfuvA)`Adq-Kh~v3wU%@s#vt_Wl1w!{be{8-( z%?2_bHg-xe;7VUf{FUqLK<dq2vAt!!Yq3oyCPcgY*6`5A@z&lVZVuOjmwy&n@L;KODFMdwqmxHr#0@h>mN7fl2^p?NmE>o#||4%i~i zlJECQCqKfL^glfj1!ZhmV)p>onYo-dg#q{4HjmLz^r5^A1nevE@tf&>XO9 zfPIm3Zkf~Y3tFX$f*{7Z2~W}?uCM_2XPOWaXGX;sdokN_MM}X=`oyRmPkKEsHmCWN z>u;!%G-B^;*Ods{bO8+YuBG^&hTsKwl1Y54JfREWokJolS|kvL#`GE=k`^R-zX_ zV8h>|HGdXSnvAXM<#1Rn!ajlFk80D^{5eAJpf7gKuspMuaO}9dHfE4I32fA((BM%h zdJ4Vl4k|^sgLr$&eO*zT(lbQmRbxBeq57;lohu^?Lb zj|+uE5ATBko5$Z|9pm?j~7$nf@XTyvHC#ciTUAK3X;u z<;DPr8ZraQ=wH-?HuKOls-;EZgeNZhh9`}4dVJjDhWK#c@~<~M1I_#tz1>O*8EKG# z4TSeAp;f<{V>#qeQNUdKinK2k|%WdeKZ;iesdT3n{G}kH>2xQG{K}xs1w>pv!9?n zK2srVSm;oc)l9lw^FpYCWsP1kl-v{&55a(gZl9`#2h$Bq!XozoN12Vwr*C<(iPSHY z@5nUHWHXlY<^8RODA82vYGP1RR3!-T&`y?LNXYnR90Nk%ArjfYpopDlNL3h0B?u17 z0rXDb2ic&{Uj=8(S(g~#o_n|_Ef$iq0aqO~U zw!=zNbmaEHH&sQRzi8J}I@BI@;DR(&jHGb5qWzQ-$1cm;>Gt?Ld)r4HWR8?@r|TLB z3=A5EMxFN%>ABCrHyw{81u*{@ePh}p-Fgy!k3|8I(BV+8? z89`H#t%75>gVz&a5K0tG;W*ZS>j5c>%H!N0$J@VbEWlLNchp%IfW`#(PeOS6Zc>9& z(s}hqjZ1KpJOK!JPQ$9f7&|*hbsV{U|1&GJ2NeNeh$#o+T?N49;(vrAn*QUHs6Wa! z&qQ=Yn~mx&-^mJ|0uSkRAe$Bl=hKOOz%mMs?k3S4??-P0)8t1v_X9-4vkrm3GEt1RwTP4g%z|x?w-^CGd^?Vc+DDTmRpMZn! zsRye4KR9v|9iSmdFUrrsFze#2`Xh z>?6(>lk(S(KM9lJ*@K7>r|^a356@S|XgbLe&o@EX#LjxME?=%mTYJ^YU5`KuW5-D_ z+N^>1<~1lRMr(m%V^n@@?#KFjhB!_Ge{)ol%2<}<)^C{S(&CS>L2=tU$MzicPtW7s zeaEf{-jwRuksT%j4m@y65SL+K%rb8`*Ug0`BOvD3_Vb}Cr2XBhsSY&yp91dx9Ibd` zOf*oROa^*7nfA{sIP)nVE=4fU==i7@z}PrDmv{b{B=fp~JVX^YnE8!&HU7&+CA$f< zl{L9b*$24^F<_n?6zpVKw_brLF6Ok?3XfuAfCgDBa0_?2**x4RepYPWQ8Y>)MZ*;4 z?!FkT1B7N%oRB~!=#?-aP<1lV_gkpNHwH8%?FI0>dC1}QDJ<$Mq+0hTw-oN@=QzaKpg4frvZ#=6kmv$5G2+9=uY@{AM`bRIN@ z=ltq6kbW#Kd`jY={JF_H`f~7dW?bpt2Im3Hdug*}pjpxsNittK2317>BV@CQmlJQF z6QH>-sA*Fee*YaP1V9eL?}jPIj7}vuzQvt`HBOfYuu2d#GRIaJzax%4SQ~Nt&Q-J8 zBEJD;8IqAhQ|qYeW+5q4f>&5zy$ats$)?ZB(a{=smEVC^`RljbH-GgqzK=h~)|g_# z*4c^{8WZ)Tn*DBU>-!1Iezu<57cAGMhWZl)%QCP^_!@v0ov@=%i*@+x9@lD$x zO3F^536vsfqT+gu9h*e{6QCZcvVt|))ttVEg93d4UZF@H#$Iz6LFSlEVvFCu8uaI2 z`2*}5{t@z^NEBE312RCp2`-ZMk6M-lWPtek06wWhM?x3S`Mxro0#ZB+5K|-T^vf3= zzq|jt*fj1an68moT?oarKOdDpzRTnD7;{ zB9Zq^YS*09@OJ>;{5>eZ*GJm7ewte&vp#pK3VSnX)@13)#{Z8(IP|U4s-J3{P?^nd zE1m(HZfK~Um72Ey^c5((*xp%zkumhdSW*cB`%67Bur&8_7Sb6|Pc8#U&NfnI&ai0> zZa%^S&th|M0u14$XnT-TCu17V%EO|5SOIDz@}pRo9nbE&vvcBvAWFIN0Wh244cL$? z3*#0*1m{yIW5i`z>z~T_4;az@1#;z#nvTH213ta2!3K7?r1d$sk|=%Ee3frK*6OEaC1A=*&E!fi+OKOaepl1n2^Jn>@Hm z`7E`Z`|FR+L))vt-JWy@L7dB=Lvn<*LjK59^*miRSB|+G7k(rT!k4>Z(3I0y@O%2? z)1T4K0?A3-tN(O|e{V7u#Ad;M$S}R2xwANyg}2F<@wY0m#otw^NDmdnEa}EqrG?Sv zO&dk&e5vN0;~#hfj%m5ERwDAZ9SZdreqXF!o3ObYxNlc?cG6i}gMvTlAmaP#e-0eb z)QD5RfB~Vjs>6%V_dgos$?RqBo6-E5_4>Ui^3!piMPbpQD=Ga$bF|1WArv+9SBTj- zxA@BwQuS`8Uk)F9*mP0tGj(f{c#e5c+H~W#Qy-`Qv4Ez#-nw#{@KXCtGL+bGP1+`r zcsS8&%|0d2#AC9u^W!?$N5Svg?P>sP-}BZ!T4iu6a^-0|VS!S9XFfJpgAS8(*g_%; zUzqfIf=;rhXvv6lETvYpONwUlrv{oAVyd&e=Eo0pimKfXRxqIA_GA0~>ixawrGZ7p z2vD2hON#d@A=pr+q_+(EF27!w*ZiTf!BK|s(RcZ-s8hWG_hR$tuNqBf>CAegQi8=r z%q+iyBdvr#M)lbCH4tta||ueb1Y~EP1g7-{}ehY zgdr}ybf}L?qr!nZwgE#-8n3b zq*-PY?tv|w#J4q^~hzCk!U)-?+Tsg4NsJ-1%bl8 zsdbXJm36A@=fa0)#qhc24WE^|O_qU?cYT`0q+cjnc3SFy%Jk-d)@Qt5);jlLw?K`a z>vF!9HXRXU2F@Lz+=%!Z31>WcNboh=zao7xbScH*>hkV#z{6CH6(eo3c#ToMK@%ps zk6QAU_Ywn^`$jl_x0$Av9&H0DaO3*tA1PgQXJ42DWNGb;6CCyxVg)*ib({p1Ctb7b zjFApq1D#sfMVS`7u(K{{zrCsAn}7^#5iV1st+81(|B>iLI{_4$aVPAIo|OlN$tWA+Oe6_V23J4MVqZBcAE4(yGc zY?Z4?8f$)BiHeui*&uD*2&8+5#9KKMc60G}AvX_QLI2SS=@JVcdYw`^R#JG5A&j6w zFPn#z&{SK9C>mRP>Tz!G#6C+7XJfA%kE&uK*ci(iJXdvTz~$lgCL zVx4O548W$#b-)E#=hTc>6m9n4IxQdc8Cm<+FMX)}J#yde02P>nelQcb!OS1w-RqnQ zM$0|L=I4GG&o5yeNO*pCVujm3DnD-H=Bo%XCZTl~wL^?n`7c=|;%PiHJ>>=a=PYGY z5sBqITtPn+613xl-_y4Ts2u4zlY5EnH^@6c>=TK%zyyDR#pveIcQATjGf{gMYzUr6 zFOr{HTzaL_sth?7#35#JJEprIE@#bwH&QlI#B=7OOr*}US4X$_?;x(+3$dGJi3u-( z%6yUPM9(wtUK?fgFG4t#B6Smu-NTph*USsL5oBCe?-W)ErI5BBz0Su7Lik=CiLHC@ ze5=ek1iFbhK)7jW`DTwzwY%wfY=Zc4Vj;Z9SVDR{cFsA&-W_f6iwcdpK24qOl&@R+8%a;Wza_IsHGrQnorDdo{TG&GyJik)3$KB- zM37mga|W|bHf2GqK18yxMiQ>^`4cL=(22y?zknfhBZa&dHt$`A_<50>RoohnJ)29{ z`oF3ukzYJmEm-~ing6FS{GY%2_nQY8fS1EqQjq?euJEUt`N!%1_dOcKUEL7-XE6C& zS^uxEFBtATagqE!l|d^AEI&>MKphtgv2L`N6#R})5NTBj{7$(pz)cDv1Bj7VZ{&+W zB^wKL^7QeHQ|idGub>%&?ECx>iUZ5aE|gLGY_kbS2qUPX!x(MEl%Val1hL3!4s=9c zLB13O{d6C$4M7t8ZXotG1tW=~Ua>3yiQx5&0O*%<6Tn5xazY{&7^S+VH}9L}Xh>f; zeH-~n8w$Av_(|kZGBrmm00?5kOVx73g*^bFiPd)(|G6(gLKZ|-K;j;q4PYXAT?uAR zR66Z!)9;}v&jJN@H=s`tAXP@n$pZ-9oEFh>ow0$yeKFbtyslPXevRa4$8MF)3T<4V z8z&Tj#C|$C@j~Z4{o6&*3}*AIH6CDi!P%~bVB_f_d!xt`58w`12EaBPpb$}W*6E<2 zL!K=i9K7j){b5DwVDtv214$D5Ys~IaVNe&^=(tYZu?sYW#L~hEoL4BnC?9xsMIWYt zRMdtZ(@|kCs`p?%h@B64>9GSwhY+mGYkNEXtx8oM0g~ocL@}A6b!v+;0z}(PJ;np! zA#(+p`Ol7#fP#CGeP^?49Lq8M!42>0-#cDLBYVn7J?E~?gVisPfU%p8S=96z^bMbK zfXipwnZL_0#)0&cCh99NxcrCc&xL!F8N*$@JPW~vU`x$<#lG$iDpHT|;6z7^i$Y*X zD___O^3b%I(*oX-<6s9`qI~SQLe|o;#-B#Z{H_GeTi2#sp#pq^oY7#8uY!G{H6&}Z-**dIjU*>w z3m~4Z3^x3^1|aX~OGRg*@!%;XX5q&>v;Au2%^u3y1iV3aDyjN12)1SRA*jyr>H3K< ze#J^A?CCz@aOd9siZy zh0f`sB!V_rI|s0Inx83StpXfE)yr{)E#r5L1o+3>Att0(=)yuBr!V8$Z4970oygv} z7*UBKVQx53$(^i#K~R-6!@nK(vkxhmm#dee7sfRwzqyUyQ~L2BDxLBX>f3 z6ww09#tt=??yRC0vuCm0YUji31~dXSxJUbS~||S7!x(v z15}V?W0MP@EEp6BL%m!qJQx@h=BQ)=xG{%t-I28jh0neo$9^+oQllDD-WA|A*Lgyq zq=Gj2o(+Rpd`#0g43e$Ax`Q(+**5}>k+Z5?qZj)P@{!-Pn$5cH2*R9qUlZK~Z|G>n zD#zq$G>{dK)Q4PG6Q2}<9&#B7x*Z2>#98q`y>zz1?+@|$L0U3tU z8>l@ub5YgMlpMtq{Rj^874V9Hgew*V;8t!>S6X&QbGGvJb=P~}?g3B8B0Cah`SYRs zt~W0bf(JWre1%9~`}N_D`fk1?ooUFGX9`Nt3BCxY`vtu2r#3|2rIINt?@J>o_?JPN zvMpYkK-1bC5$y6++aJ|JgDA>zhx7!Hja1rQdI|DH9hX1>A`gD6v5Y^p%b=2FC!trM2ig z^t+HyiE=PhVKAfM%T=I$1VhI&6Nf`X!ec%xS0}nz#JeiM`Q3R8JUlLr9gK2{xV~pQ z%$IKw>DVhZ9>1!EI^x`3A&>JHP0apf@VUf#tJifO-1L zICd3lQPX_aChn^5&_jUmvwheBw6fe7<*_ViwWr^BLGZ2a2;&#kX3FqHaa=GSr2?TC zQ(EmX^LdX}vV5GqMJ@}veWgZ*uGP!$oF>o(^{)D5!nxA!ma-iyQuV<-eexu2BtF7OQ~E^^B{ z15d z*4!+cVNRl+HSLcT_&&P6pQl}!0y$QNgDvWa_(*?-9(nHZ=8tup4C1?c^&VZYZo!_#!VKs6*QXwHrvP(dsdj zlxD%!QXBvlevkZSgxTp6{E}}pV$W=EEb}4m^&LWSd;UxFzd%5`JqDNiw4@$stZe_9 z>A_XSTmxob%3K%|=GpSq+}kD>@>cYfL@N3h(u4_0amxY$!yU6`#E7Kud+gQy=m1>vlKysj=cdfs{@zCq-0{{MMMbx9*{&hMp}nV)ib>E9)q%*P*4ueJ3#Kb7&`)>9;!pb3H_FlElZ;?rE&$|~u zaSxj{)=+59%+}=NP|$B7QjYH;1YQlV#Zk$rtAi&IHLch;p8!9rQ^l$c;gakohvAA! zqv`X|JbO{`hj@WJVzy{^@A@wmHZgkdeaYgu$9i_RLU^&Ti#CBIQQZAE@DsFR-{TfH z0Kv@P74qa9R7z+0HOeN;m{)fFKF0?(e=K4-8<);)5*EvdOMQ~-n4TH*g?83JV@@m4 zZJRghI*k17;slL;MBo#0`551Yim)m5xG8sFtMu_l%L6{+K@M&E2MrZ6p>VrJFu#=d z@SuLP0nN*&qY);#&C_`*U=P_stO7j9_?3dXlwF`yC`Dplytwx~atJ?~&^ig~YiG)K zYhu+?H{-eS6f0^BC<;ExfNN-$f3$v<;@zA06()wo^=1+h?sFcEfPd*NNx(2);TyZz z)7=*az68IcgS0$`Wo&LReg!7gF#xtPZ(a~-xsj`|h&R#Jp(&H#u~j!OV5VC=dq^Le ze>OQKPX6Q-PbJ_m^fy^DdzA=AsKVDtw`)#x;hZ0xLX5ocuK>H*2^P|J^qjaqg_41s zWK(VaJDSU3Wkm-u6VCbYMdKDrxr4AC&b8xIeMz0mo^xYP`KJVqoQovJERjKX0$9`; zqLDx{L(vU@;H@jlv#LdxrZoj|dZ3%nMEG6w9dvm7vQBV&>(jT4Z!b2x7J{6Q?esP7 zKX`uRwoG&79@Du#;P@9qzr_FMVJv-g(LD|(co=R^GLOd=!kL9AWJ~|#-i;T1-=YSR z-1maFiC?maN2S<_?@@<5dls`!*joXD6!-%|gyBl?>dVNkUIp^kts!J9P7>$ti{VmJ z*$EI64o!USqM%q~YI^Vc%UK$^jHPbh2KLs~@mBAS>~u)V8Kl*j%GTSJ-6~W@)rRZc z9$e=>A6yV=Z>X&5$L07E8@L`v%Ndvs!IBP$qq&Ppj6G`qPM7$gVO-f~$))!yu?FT{ zX2o=UN-c^Er`nI;nDR_Q`n$Jt2a7VbvI{I( zoJ3S&>-xaskbGmas;^=NnlN!Pb>SwI=+?*UmRIAyqve)oEWDv<-jLN9_&Ff!JT%<= zM+o6%1TtK#hrZKl4tB%36d-wC{|D|n(LdbN{G`r zNTp(SZG!L`8|Htx%TZz9*7L;L3Uwp29&yCh3s`tx8AZhYu%wicUHLgYv4bq7i6B|$k$hvZ`%60mh^ zi+(^U@S?%FUPZVOlly1k=mXBpX`rYV!s>VI-C}SFR(^Q38{o0Umzt&DZ7Q*=)%2}V zrGdk})q$hg+3zM7CZBEYPsLy#xtlesNkLNv;ST-&OmdUWxNkrzL^4?Ep6LS>c)gAm z@};-mK<=j@MbCYFJ%b#jW(HH@%*@~q&bWoOln4`ZF@pK}u6s>1OGeYR4XNttDesl6 z-{t&nn_zRk!YU1Y-TZ6}+P3vxa}d6Hh$v37v=LU)72xc^Co|hkQl`sh5BPXp$RT+Y=;l& zR0wPBaalf4Ctne2toH)s#i{)zkM316`QD`~U@B0EkK6G#_nts88{nVAFK-X!*J9&~ zc~Cr3tkohTYZ*O?Dvegt(W+%c{A+AS?sG8ZFVI(2bHxMxW+V%*{!xQH5BL35|ArJo zTcTQyyAOOM(`Mk1P(rb!S;J>9MfC@zX4~`DRO)Qj)=IBSY{UeMq#raL6T9RH{QwXx z;~}oj@f2W)j-`v?Q<-2TK}^?O(hrXjyq*(xZS@V!=q&;pKB=c}wW`FS^;J3&=e?MU zR+z+UX!o%t5Yv}4GKK2~mxB3}#udgBulMRn7uc7+@;R9GFk}C?-zwN^AY!l%k8OOy zwTYmmu?)82h2>r^JiK0YS(vd*6RP;aXRxBdeT0UtC?oKi^Rl4!^<#fQjUsv~l+x&P zQd4^XUN%ewfmF!(=5#c-36knJ+<)daeE6mLZ!Ewn>&>;;L3qI6JXo2@k3E#nm+RHO z(@^h{dN-`Rt|3&Ll|;Wux%s@h*&h|Ka28xCVMMMYue$mQcm!8=xjK!$=Cri%896sE z4K3_*_;`fba}@86GFUrXAYQa%i4(&(9*C2N6Cyw-Z+CLT0K>BC-7^DF!a6nKpb%mb zC*yg!xDz))V_e|L;zBPJ#xo*ITK12YIA~u1Ov7pu8qRZIqIIr7;hQ!Ch{0G$Bx+1} z_Ti9*3sdM5-K@8}u<7$#aBk&PCClR%c!}59@YQnaHllsrxfoWv()dZY ze4urLlc(_N(aaa(+0$67R^8N(gSDIyu#h7lU^&7P0;)!A#rwBuTbHN!!<=W1a-Gi3 zBjWBgXovL(zOdtb*&;}=66*m5*OYF1oB}Ast+t)aOAam>eJrC3DSWO&Y$LgQD}7vk zE1wu`zp9sHC!Xfy(SL0&Wm2}Z_8?i>b4=W)G`8(MD&?zPZtMm!oE@RY6&6nJdDRby zk0FNtbbVX+fu17gNb}8PYnjh|Hc^!eMjBr+J<>e(DWULn|* zG)^&KS_IrB!`I!XD$IT)YZ$R3YSB?z2=@Mxg=0~j=z)~{wUidn=JI+5ENyT#4RMs5oR3!Sn?-OhW;cY=T)vv+~M5BX$w70nHVR6EbGS1d+o z-3PsorY&f&2}`kc5wS)OkRSKBatb?{{A#`tLBZ0j7Fl}T#A;)QB8uifQ^WqeDb-@+ zs{7H4=x5&ur%Oh4HxQOm6MRr=iyndu?E#W!|FVJPYPm)|dhVNAsg}5p`*-Y6p>q>O z^BAKqjK4%E#AnhKV1It9ARjS<#5oL>71&0z6dA{)kO>$)l7fIE9Ee-EEtqwCRpqlanF)4=u}1U zpAmA?jWyXd2D?(ujKEkdjCI(_Y$4;5-&U@6G~Q^~C^bnV5p3Hfma}Q*twuL&Xk&&u zD1+aa(q0$)^hMfn#0zpDX`nm?r~n&-GQ4@D`GBW~L!F4C50ht%&-bq715Sg?GR}z! zRdt;umYzVZv1LNtC@|!Z!Q}B4hN&V*pGhS86e+CzBcFrj`|J)FFvr-NByQ7p>rhgo@x}$z=u@W0)e}Y zjj=hAB`dr2)L0{M1Df~Hc}WSs-W)j=_*L`3+=ugSq&sI0%(xT;6rC%SY!07=hq*U* zAzJUApzr}6!+a@ILa3cc-~-go=_Lj`Gms^Q)9V@mgPQFG0Oia$8}NJ2o+#BWuu6Jx zB~i|p4AX7&#(3vCdSvNo=;8IjxDOr)?)y3s%pkLsOeDv@s*~H^PvB#6s~9+r{%VFI zXB{=nZUhA_<6E*-k81T0&5fPTgJbg0QL;8Lj4O5z9uF!QhuBb?1V^c9sj3CyIE^rz z9r;>()Fs0J^@$xqZHobgFQoo0MWKuxXRDTXQh&b*E&Xy&d13F}5BWJw*{k-0o2cZ! zPRw;r&K$Pf)254%hErab^jN+}Tl8bFyX@UOk~MRTE+FE>h5wPFfH_(aJdT8XIx;_A z0QbwevGk3RDq4m0xAx9g`Da~V3sdX5J-63vNa`*`kg#BZ*(ex%LS-4`t~9$CRQfNA z%p~#cVb5{2^xr+`Dj~d9vrnu}{B=3{>15orvoZ@+2onx`i>R+utw zF8~k60Hv`DuDt6!kW%!X=%^iKNCBXY_R_lhF5lV7jHx$YCnkp#gbQPma_W3ssLqc> z4HhOQz#B9?r#1EW4!A7jUqjc-%5h=#P_uG-81iU8XRK8YyY{s zght3k5+fo<;%f%wS{h5l84`{uB4?Q*B4mX+&G{m@E#JFG zI%-CHD)9RVo>5;LZ7-Qmob_mhCqJ8V(Xqi_ChzC3r_7a31S zonW){aYXf6FUX|8^FO!vgp~73#VqzZ$hRy{0Q-%K1|BiBx5uUspFyB)a7@>h3xh|I zqM7nJ(v5)5qexKK_e<@#P*EmxvbCBL&;TCxjB2b-M$_l_VLnj&FcO_>NTBeaQ028D zT6=z!ZB%VcL%;f;HsudzvWo5PQS2h=B*H}afBx|QAyrlxZ;~9le$amZpKay;$Iys4 zfge6f5&Ge;a`}IxG5^;$u2A`wFt4Wnzv!6%>@(0G_EX&A+H?r`q5prYRsPqdcqiQd zh{#JT{~Z1QyMjN(Bd8zk1M20b-%Az*f=jwQNW7NItKOG!o!-!mLV}m^NfRc_{;_@w zSN0uXcFi~hkqLsV<-gvULSq<5id_kKE%yLX0H!mZ)F~@HFddK}k_R-8c3woDY=GN5 z$YYTP5S*0OYwx?2o_fGiA=vX|NKUu4()UR0-;Hj41d6fWni-%TE1LoJ7`t8z+pQ*P z@Zsl@v&yxpMIPIMfjwzp7vdBIL#oyQ04!J3%_ZyFr9Bfd0%Gop!EnM#!O38r?-!48`m0-q`8!U&FpfKW1~j5^S#vghPCVwB_$&jLU) z4uJie#fO8kqoEuBNxOmxZVke<^Q1wGR75KCd@WY&dzv}WlB3|}2b?^(hzu0zLxIV5}3{7fobLBc{3^3DpNxHO-WAkS5@kP&N?#jYQ}kjEx}i zVru0B;36`NHNg-UyMl3KECKR$rW{D^IypSQSXZypa;crg{_5S`Rn$VL{UGsj@o^Av z{FR>qhG^D~^Xjxsn9w|@smmDV8F&)c=iqat1E5lu8#5qDd&rb5-Z=F1Jwry?X5@@2Aze| zx8a{^d6SougZSwlfp^tz5I(u^)6@j?HW5P(V7=3U|F3c;*uYO&Vu$r6%##+Tkqq*v zXk+%+07Az)4L->lV9cH)19x`-5Nclu(no&hl~s_%XZ!|F%0>uCrmWBVysn0dvpiFnZrW=9jV10ZC%2Hn+(~ zPE69}l4bu$B3N|^!tMbuCyy>24>WwTsaOLw@KzW{Xe37z*hD%m@dgIejdQ# zyF}Wk_TJ@{OTwvmlE7ltI%y;4T_OY@-JEn#9m8#xCHRs=xqo%!!_a`{JqG+lRg{PG zV$Y+Fuos+?$?9g$N6OBh^!pKNF+`+a&xdF5aVemvnUqFWaS=f4_fUk0Ypy zW*exL`#b}WZh=S#UE}BD+B!Sol^0<1*l8^VUbo@VC2%7TP?Cw(u#cfR$n@o)OIpVA z*#=P*y~}R17c*};AoC6x3czey40U}eb6*jrY8dc5=@Z4#M#3Jwr1-Z1dJ8>z!H$}kO8 z0-O2f_qVYPvg-qFK}CE~>7ETYR(2i&@by>jX?tZj_A(KI{8s*#& z+GKc6SI3M1_rE$}=_(AaVw?_YN$8Nm4j36|x@hgbfwNOJ0xp?5Ct}nI;U)i2_D0=^uQ6=A+i! zlCDy1aF(UewQ0D!Egdk2B}Qu--Woe|NFRprgAj$M^TXvbyiOXG!u}((CvnR$csqrK z#)LaXsti5a72W6Y*H(nDE8f)3e_*c8!E*HQq}<`nodty`=rVFXCGPJazG(wW?0#Iv z?TMq8vaG82y3Sd%PXfZ@9Pv@QwQgL93OH{y-2IU(pL6U6!};-_sJP$yPO>%qC>GQ< zS~%{2FOpI#;@^a!6ZzrMlT^F+h&d3PdZ44S_oVUlQ0Qy&#z`DHKdjp`XT469D-{0t zp%l))ywSyUYddbYQU}`;3vyHzaH68nr=IEa=ua#Hk^Iw>!VlFPTZe#i*j;;1e87?W z`i8;DD4-+-Y=z*P2>YhfCfi^GV*S&r5Nr3Q5IVUw_tB~uRoHd`)h$ULOc!D-Mj|rO z*Mak=I9{h>b%bG(cbc$bIUhC4&in`l@KgT7w)Ey=CG9MZ-^XQj@?c{(ZDeRsk;imJ zi{rqU$CGv??r41RfofM)@BAg?U=m>Qmka*lGu zvm;dD(`WDq#BNi2#}PowYGJ%fpOgoI4ON#JwJJUy9R3|68i%gbx~Zm-Ucmvgzqg%+ z0Dl)Nbd;g!Qt9*CX9*Z75-mbRX5Sh?@Z|o)S36UX5Oah@S9{iDoeHnr3(+N9zbQui zt|5|fE_k)n;JGi=%==;@ZjbMmr3#5F@fj;yXqF!??mBXq&=Tv9zw!x@HJL?>HAROY zaK%_mhn5?OH9~{9vCX%sFdK$b{*x4+e+Kn8N%eZNwWqN3tc52LPO5e5xBSDNWpW3# zJCbp4r?a)X#~wX*75fFm&dl9a-SEe!=c9EqYDq{##%_kC(b|)m9ahLLJj+p!$yi(9 zLxO%!TPmBwKuf23Ib?A(nkt`zJ-;y+^uf^>2d|mBCp-GaE6p56qxbs5V%0SfC?)ft z0AcS&Q;+t}&u47*w0!j%C9g5nk`PJG*nLAaS}C1ygs)2<>}AYqAb_KeJC_$Ir#;@> z@GA>- z^}gFIJ*R3_sRKkXA;RUH)mYK!?^*dediRLG7Ubn0ExU_hxSbL%kFP4dP@AB4Svakl z{cY99Y~=SWOCP165!27l1^txHCNjJHV+KC)zT^HS9Uxq9b9mW;IsiajWhzY*M;5TF^d!vf{I znpQ}oy&rh~>YNY;#5;!dG`hA!64VZ;Khh7VIg;+^{)DsMds!@5OxdbFPb_0m!?E+I z`yeN$(coi&X{Mh`(9bPg((u6O@>bFD!sE#_4yZDZ$QMaXAhnfxe~vB zK}RYb;WC}->tZrFaDoemHC1<@+-a$A$<18s;*L&_ml~KK0}(xH{DgYl_`M+(r3f*E zuC;A>Inah)k{;NzSk#nH?492C7u>f@P`54L#=osV(~y5Vt+0W>0MB0#(@6ED(HP8{ zA8w-LUx?^VZ5j!FzO1AU!_^Vdv^inU{k zCS!ye+VffW&k^9Bqq3%0bJv=q48fc7LG8jR77qa@-*jxn#)pk}&@_JJvHl`6uPFJ- zM@E)Uvk8i`XhKcH@S9PPA*5FGCjYk>y!>qe^NfORO-+s2P}3ASVN9wJ5EsS5=@9AA zhJx?tYm`a52lI)Rf*O~d=o}D38}ftF01V=k^h{- z+8s8Rr26ccffxoRKe0K7;U~YRJsgJp(n~0ujZ16GqXjOvF&?(&;$qrmd$jrA@=l&pP~M;Gvmv1R$z z7KUWHxu;^8LrHM2!o6{>CGs_gEKO+XTo_@EaGardrRQ zGkdl@Zqt?)Zd^%BR;Md%jUzi6>-W(?oN0sF#N!Pb=6j2{`el;%)YvqVB{M>z73x<% zOU^iJ)%{ol7_TW=e%qZYcoms*&cqbg8HDn-3O8{`QSt=fMfb4o3RtK}g$o^dKa1Gl zP${E!E|||SRW|cz!VZqLNaxhh!h=(OVQ@CW>Eg-zPlmZl1p2%U^UQGUmOW(UOAnT# zI?^>F@JGB3on=B_)V-uFkbTj zhd;GPjgetusLXdoZ5_xVEKcb*yF@2}_ z{wZSgJDP*_22Q0CW?033ItRH)B=Pas_$c?L$*KhL*)O4n7gDd}o>@z9Fcltf4P{#X zz@Pmb#XX*x&(C$gYQ5AozVROvQg1@@$m7lGQ)-VlHvWI;!5NtcqIdig>+36mn-WuB z;@&)AD#T)SmoMvxd<>_1ceCBm^oWC4O2f?xMEnW$6uN)6Y@IVtox^K?(5>KC%?jen zI~+e8l^lO<@tNlt7zuC6>U31GB(!f|(KmcjeP=QR_jZ(};oQckDv1zhv6}n^CVTs? zh&i23HNKML?h9tcEhAUk?Ayjwvy}yYdfaYK%rf6BEw_UB zVkdkPsNmGBNScs6@x3W*RD^wlgMVE=!fKEyJ)@dh`>k)0-m4H=5KsZ7gd#P7f&`?BNR^IM={0nbDg+RvcML^} zbm_hKE=>e!5d|lLb7sYt#GaXobxx!JBhvzyja#* zc>(&@sc5z9#S6^1etXJZXQ|+yRf5ASd$*}+u_^TZCQPeRWGX- zX7rxEjSori08ksg8{j~E&2LTaQUl`VV)u?dxi?JrHq|7Od{lS1797-IB zL9*qcBkWS)A3wrr=G99bQLl`|$viH)4II_@kF*S<+G#A9)1ubAt65$AEj|>D>wljU z75s9uC!TBK%RwpvYF{&F)dFLO>9538pEdy#ngy+oP32%#S0Bb5Fq`9+FkxtCX+e<++J*MK>>15fiw(}r}*2>9f2 zu^g+3`?!K*u7!a|vE0?1NZ40OU1RTc9RV%zf0p(uu*vt-c?ir?HblXg;UX_wV5(9} z9ZDBuOoh7o?k?c^MWhEW2pDKxzGKw~s^^u&E=+%$o~Xya1B}dqKL81CcfZTP0vH8- zPGa1v0|o{&qZMG5uzLu$-{3zPdw#t$oEF0^KzO?`hB~YUa$`E9oDF0J5#3K31im>5 z*iXi}1KRUx7~Wb^VVi1H7bj_~$ix#gM>?Q&Vw;i5Y{N9E%DA9EW{nv3`11X+9qeGj znXY`$TCp;RMtu@XnzCs%6bJ+?-0le@%WaRq1pL|A2!oYA@a*;v(4EemEvruEdL)`4@)YPJk?6qZoJtOB5)&{nY|HG5JwG&~w+`lxC-aJ{uG28S?nO zZ;)qvIHisujp4zdMYXhSm?pia_Qhw0>84zN+vw)>)W7okfSfH1Oarg?w~Dv^NKpIe zqGVZ^qt5SbGlz#zd{desZ7!m&iU`-(Ov9ZyN^D7~rSfM8TBJftjpxfb+V+2$OS_X0 zNz?>;qfS4{KP}$LNoFw!^mT15!a%VQ60;j~SQ%p*JuEy5-R%|w7PcV{1-O;GC`HMh z5Q5>R=a>T79cKNeMCAkc{y@i#o`%=;hSv8Kwp|J!Q<|K$Z2YjL87n8RE5boxjo9CK*vY)x) zSAr8>5$4x%!xFa)Cc}f>5pNJ^=4%PK$3xB!$ zMR5U6Q^_m{tG{5eG;NlcuHqb7xD8r=5ZI9_X=ZR=>pd32<3w7GlV4cb!a3RK#?G>s zT!LE{8V)`p#+)j4s`07ILXQqe+zScq^C4N_pE2xk470h?rfRE)=8JM8Z0@DQy> z$>r}>GJ{6(WnuR|XKxBH!{@%*=T_2`moz~#cbw12Ud_Y!@@IQ(%ceI5l8YP$_EZixAmFLhMpJ{q{p?ykK%8<8lViR3j z(S8-*4H8T-<^z1jAw&dn2Ran7&w~VMDJy|-sKcWdx#1NEXt*HcrY8LX3um;U#svcM zTxUQDXv1{27m_>gif(^bNy`>?XMiZS5`v%P8YdIRHjy15-s92L{?#%_&_e+<5p42} zDR3>ym6mIrpE-%ZLPuTfS}nsA4@qGbQxWf}MSnmdKd^)_Gh`SdGSNOJV#tErAFCVxYEg$dys5~VRuW~+4tb*TFp6d9B>Ed7}c$dx?OCHieKS|Qv( zGYN`#a{deSxl8aF-e%fS0yi)_yvHYZZguIOW5O|JjdvVpRyJyW1^xG1G(Z-JGVbs7CiTo@S ze~a2Ct4*e&XK~JRr~0^T=GNcBMCa2RWeQDy&{kdFoyGCWO$qxR2A{|-R@H?17|m)W&=uV%OTobvuI9REjJ z`>&%1zA$13ezxgC?*B7=`M)_HuJBm_@?4u38jHgvh;6sQ8U+`LQ$uyn9$sGk0WyiM8E=nIM!Z_wo(zool6ZtXBOG2-8`~KZ{9LeH zTDAlTubY6Xr2|usGUZv3Oer$Mayd}}VuJA8g$jJ4sSE~wkFkt||sV3f%^6N>J3UG{3 z;LLFR?#VD`P_x^R3uXO*2WZj_80G|B@w1q}bEws~3PVsDv}~59$_~`1(21=s4ij88Ml{ngBG9 zTqVot%78#v`;DcSiwB7!K!L(TlIf4D$nfs@M?OEwhQZ3gfQn{HWu(znS`o>_pyqdA(Kp$7|) zT*~2Z{4F$NrWEcOJ#qjHrp~0=0MX2@A34baG^K1}<*yy^BA^6K0O-l&zsv$e=0GtD zm?Eejm^EZ!hluWg{h{HpOSwB3{u9Sq`On7r{bg4BEdS%D0CmIj+a|SCSWf&2IikP} zJ}exZ*)(rgoQI8^t7S+&Y+fdJ>eGltod^V;4Q{@dr+5ks8UiJOD!td;yyH!Qq=W$2kJToNvunWgv&E^*9Rg*6L&hq` zaB_HoIzovZfH@>RqVL;(30#;@*$eC^$ zxvt?#ib@4qGK(2gie9`RW6fV@%*33$ocREL`*8zcx|!@C6-dW+CgL1Dy3FoFOQePz zR|2Y-PcJqYUtnm9@G~EhZ|94p>phD*&n?wV=ax6#1LoPa%$(kfYaQ90SZSF8C>h7R z)9~Yv{kuF2Qc48ra{-(2cNbZKWBfZ!tJ=s&J6X05{P!7JI*#+u_rCk6%D$k8~qkg}HZGm!C@Ox>DCJV2# zsqHU($)6oymIzNO2i^~NT8G>R$P+*xZMT8!X~~TCH{ZvGfPU}>*acZsv!Z|gT#ng3 zsckvp%lZ}cK|b6aFtpZBcnk^l)}1T@Py>h^52Rz@GZ{h4_<^$(U4}YyFkYmmKKq?f zkTEQ6KTkIS`azk);STJVy3~$H7A&aXCI1v<jKCAH-_PBVN)V{+>fuAZm9c^AkApmEbLG z0J>|Jyi1615V&MM!$LP0E@y2R#iNp}t)AHf=>XhwZqo^;=#4_B^?=4Ca$a{_#0)kp z-hd^DIX&3G>C~U14MQ&nuzfavi$lgKxc3~syM6%tWLO~YCDTz)7q9RnF^cixTcl-T zOne>Gh{=nywv#xA@0zMg@7AsQ-ELwDs}xd_nGeBnoW~jMyRUEl?(PH@Woj8|{YblR z+}$4QQ1BXX{r)LkJnBU=10w(8fdDcXSY;#<9Cnuhx*&*7D3Hgpr#Bf|PZiEP*{XSk2gow79#|d~H+_1youw@2Ys6*%zS{2Wnt=c1_XG96uz2 z>>fyKdCp#~nkZQFzAiVq)DOs?EtJ1|Of3^V@gy#c+>o0&5>G3vP!{%fphIH9GM8es zMkl0@**QNz>Y?HD4fs^W6;p}6FCQI3=U~hZG%wAW|AP*+x^fLLuFpR+EtY?&wAdjd zGXSmIWYv$!d=?OYb`9yd-EUH@6(jsXan|oi;;}0H4jpJ-*dhGbP>1I8;ijljXl31`;^K{n_3L=RuoW3p ze*co4GAU587YPkEqD36MUX}X22W0RCXwM0KJc=bXx&&vr8v3xG@mWIB-}2QeOO5!+ zehX`1y|ON=ph*j6VuzT*&!|Tgm}mM+_f4XV2~4&==`-gz+91&+8TZ^p-0zfb2+T{L zlP&-3sjzaO>{$yE#a~@`U7T(bfRxpOEt1v+zLRo_(w-5KBnVvj>|D?6-C%&vyRIXtsz=)XsPo_iK6Kh<3qtO#5U$6Ww$>+$aC^TShHK{w#1DB*N3) z#;dtJaPT6TzlY^YEzfxvRPznIH2pjCe?bM z{$OLcTzA&S=CQGBJ>b)g0pRP7ISLUd%Fahca$QQ3jxczaFdliUX3;B{(~veyKk86B)BZ zb)1P-)bBtO?gvL|#vHs^)fQ>y>)!okInDEQ2_5k%9drxxJoC}b5c(R=2sUBM>hjH= zuNNsa+w03MHA$fz^!dkd3$HHnH4p{&wzv_?ZVw_3AJl+D@Jb)cJ<%GM^f-m=$oj zZ6aWQ-!8*3hyqt51Uk#)@Ud?xu8g-eoS7n5y?Bp2G%fqr$l%nj`W_D}% zv+H!RPH!`qxa?3f@lw-|$zS8kL(V(AHn>)6JVPj2&2KXO!jjK3t%Y|_|4^$AzKw~x zQmU^b@Laft62doDK$6xE0HxwaFt0f5n;UHm;a?|QgEpT~?+tZ$@fOiQ0z zm?-^B3b5k=6FZu(iG)hA(13T-#SA(+v{K?VzIeSx{8A@ z&1}UXxqQJ3>h^Qcdu8P^Zy^Rgde(aVr`5p!iDC8Kx{go0@o~Q0^SPXnzz@#@U`uTR_l}K?lHY@S3eZAKK@S?FUtI`6lYX21 zPIDaDG36Nk*<+iTsR%GQ*z{@-sQ6h=V1R1CrFTN6z#_opz9(RFt^H`A(uN1}XjFAfcS=OH1`X?TAd!^N4(~Gc}n9@9hx0X5G3Icay@Wc-cTC~6sDrE?{=s87Q4)){rR51aUtk8hYvzglL3y)$cTR%o0Dx;p$YQ}NrW z1-u?Zm#~a(o4fC*!!#A37}e$dKcq&j+Zgw{ZeP<2E?5Ct<}Yh&baGC=n}*fRGw+UDe2P za>r6=y}wx-?e_+MXNj(?;STz?$okM>uP|1ex_#3k^bv|c6P&pRrHotn7`p^5@D4yW z&SvNCDUEpL<4d&qHhsZ?%)TbcmE+TaIlya-`?rA&MRI9kYWbjTI0kIIULBe^NCj zZ7fT4$qd%D|L~@kF*5|yJA}wFT+3)6AuyCr*{pS-TJ#W`=6rIJpQXU{WC>!CzJXK2 zd-=8(>}|U2KXHl~ei_dd+ZMigK zt-1~?JKt>FWuDqM%w?%&T%3c8jv3E}go%D6g~BPV-Iy}1$72p4=Q1#2JqqO$u2uY&cL1-wz>DF@ zAxy}+v|1}V`jNxwBJC2c_)%?!cL=S^z}H~K+1_{$dOS9RULaw;EwgW0ZJX(#*+y)H z56%*F;=pUB)?mCEI4AOf_`KFf({;Rzf}<%0@b_3^CbNDWDPA*pgpZiP3U3PJQ8i7C z4-S=U&4{J8Ppu*|AW`?qXq&#Hx7eGZd(qHi^zA2E*%|`AEXyvTg!hD{U1MQQ0_Ho_;|{ADFkV1xRo`bI?b)&K|_%$Kco;_>*A0OrP9hP9qrPniBj97^arg;2WhLQ$g}3Y=HaS0Zs@-6)r?}7;)_n&H*rfX zYKS}{-^(`1O2ibHj3gdffsWnag$Jy$BUE}W8p$pdyR2al?Y#=9Z^!mzI)sDi+J3FV zy2mmw)9re_LyBuh#7WjA^BCs4_5&wFh5^6tuG@4o05?^D>4V95tjtsqVJ_Y38{nAJ*%|A*Jl zrd(GUm!It>`*f%%EXIxJgso`tlg{KM(-wFWP16nC?*x1M0%@XcOQ&4tF`N%Ni-nWK zon-i>3ubWv38u5FmIeAhoGDhVif?MrY0T^CyW-YFUrpx=TiifaDQM=9C*38>sGCII zxZM@{p|^TISv12$krjY?gMu)5d2}S zb&l=QSs(sR)amvN?RbIr<9jh$>JoV@)tgRaRa3M_sVbmj*B>GyNMF5Dx>e|RU^a>5}A)bv%%-cvLb4BX;(#EAPKC^G{nIIDgp6#$wCb$ zi_9pSLwpksHr}=v0TU|Lpwn^u;*cQUkx6yAQR_GHUgbrvlY1h~;$9bRwcaOBrfHdD zBj_)ByjXN(dj1MS}Pvw@=+27@P|9n{e5%*HS01jP9I|JK>4knsK}keBg=vLw94LhI$+R+Open2Ep@n$*=^G*8g z1a0oFo_XY>PDRW+*4{RXm!3?IpT58mPZ{s~@$JxgpS9M0p?GK6FfG6q2J$-LBAYCH zItYvNlT=1mcqns3&Rg5pnPqgo!8djJSUWOZ;QZXA{#k8Zx7M{~dtRn(YyOZoYp;8P ztGMtlpNuq)t=(hQc0?0RVrW&T(IVaO{5m>VzmdizO>mOoWer0_1GhNa?zsmS(}k(z zjhVH{cuwvkYAyBII(%czz1%?w;WZd~o@@7ddijrZH->&;i*xDgBL}}_gRL>dPY+!e z=!EsTv%Y=T?Dtq6$8{}Z>y3E~y(p=f9gYKe1q0aWFyEQ1F8?JVH{niHVJlS2zIp;_ zrHq?y`%zUh=`TS|kKL3&JgebptDMP@aMD#sK%$z+K|nH38b;8{?+}PX1jH<11XV2* z_C5tZLLv29y2yfMGRS|Bu}ix{F&r@!*)smLS{6H&lS$#|H+)ycTchyA$r<5wApw;q z{USOT@s4q>P<2H^T2+%*-ie-f587Y65WY>8>`G*jZ<>goLF^yB*~%-0qmh_s=Hr+E zk^X4gr|N6D#kX82HG7z0o1onhKT!G15yK%$FU+a?!#2Qekqq{gXWsh;&?aNc#?FN@0rdc^A$+T}Z%wBDA zYV{P%xk8!*Am1vKHF5&<=Y#zqw;Gi1si&jDhm+SD?w{P86fgA3cTEgkSNy5e)X83* z0Ow*S7gnV(9l|(v5e!~}!vz{ZJ0BW&NhjeFz0%)H+pG`P`f>B4Iub<}21K4uh^)qG zWLdt;EAma){5r+6{wQ|Ui)a(lUBf#?J&K=rEof1y;l&qjyWs_;ECPLcFxk>Cb(BRq z<8nO~R+r`Wn8C`8A%AH8c5N0Hc+np&;|37hIQG`->>ZwG!BoL|i#HM@B&<*U`+G!w z^*wJdQBu-rPRo|ZvFYM@6CvHH_`w10v-^)e$;*$o6NOAjydbvZp=)#XEBPL@93|r2 z_RDrce_Y>_qsx|4EJ2DyEGqUZ3uL+?h8(6CpvzOo{X%qNSNRr!ARV;~_3TF0Rp%AH z1zktwGcP6ME22(`tCe$ca26<|2(O07-u7j57xcI(W30bo`f0*gBDYBuyr}fGCz(RQ zK(cmA*jH`6D1JJd(@%coqWE%Y3h?DZnt5sg8IZZW#9fvp2fUfN#IjWxGd>FA?4L8_ z#On<#x>NqzFAG>Xy@4nqDY*b7eZ%-+EVY0|^~a0Mdw#W{7@O;+HwV1tU)bjfwyU$D zzqU0!r5w7suy=}_kKf2d9d*Oop9GpW3mozuY_R#$2sl7LN-)4EFjB^-qL6av3DT)R z;8*8O{Orz`O7+JrsH{zE zsRUvx(5r07d8f{bASJ8VZ$TxcEQhK)_XRrJx^I_x)767jFLOrU?}2#9^S$xb@{~=Y zAnC;e0!PXV+Ag1xw2zLQL)m>ub_(v3Z4&sdY)I!QE3v(+)DGRmfVLxYUIKLgta3}W z6_^}YF5GUabnGjY`&gfFm@vF8j~CJ#A#B*usI->ykg|9y* zN3ma10+5J|SU^eNUuioF-XO$nUW*d#U?aG*_>hMIk=DGlKa>NFzsoTHg7cSQFhpsb zZ`tVckn})THY}mbKPk%sf@e+60;!8>Iw)C7VcN6PKCuP&K5Oj+<9;1>K9HS0@4P)~ z5Cqcax*9E)o^p5h52ON|K(bqfNW+7+_2pZX3LIwR8j?5SsmT1eCC%y{+pe^Sim`E~ zX~XD&PQZwm3@L@s;QQ^ zOubUKN~6O)rChQ@gr6$~Ko<8|J8kqq?7!Moxdzx^cS|2bJfF$R_XkQeTeTL?<18aD zszY4~Q+mV4V;~6C?;`XjcO+!90(+^Z`H1ly%TMcM>j*ngPaDaQcV`H^=o0S-;qH_v zYudyUPW)tve1s^~?v|$MwclYh`OHkf3qgE}{sh>)xpu~`e7G>S znJQfox8#7zNyg_C*{2s6hpY6-uLY}Jjk)!y=zqS3^nD5x9TZ--d6DSaw7xVfzGh}S z35|sqOosKb%{*5mI$Mpk)F;+9zyEpF!QFFyB!TjZxhsui2)mglu)b|YQ z-^GsHq3acAo79Gv?`N|eofa$}h2F07SUv}36B)%pk-F5&ZT z3-JL4ErJ1;Ro58P8!PKMaoKp;sFff6bc9#1kLASz%CAO*St9SWh#Ya^Fni!S}=B60v=#B3)-slUh>1&?;a`7-K z{4q}cg87qI%KRtfP%aIyxaFNuy3}>6_@07P0^o~Q4IS7W>n6( z1u=1yo;@MNU<=f16fw#UWFQaswsK}}?wLMxyA&PSm-vxbWBY2YuhU~&>YRPi)RIz# zV1B%QQ+H6(GE;Oh40Wek-d!xe{n!BV0?9cEPQF^OZ%gtG`tdjpwiB756K50Xr`T#` zU^(<+MUSY*GLeOmKj<~+rOn2dAzpSC7PQaZ(dGBl-cPcg^n%sRlzAKU^gl_*DdyuI zI#Pk(dFvrQqGG4?&!pe@;u$sYvJ|l1UVkmFbCkXe7YOlX(~#c6S7hA%V)*mf5k+RW z?Yh+E&54qdnwYF5;on`+anxKinK+iLEeRO9{8r*7N%!$3%V)b%x7*~2mVZ&=M)S38 z_JST4ZT-H;y(I1aNT>s?b`6Gw$m1v+gj0h(xfR?~OZJ75vqL6`vkmw4>0f@HyQKj2 zMmrDU%d?gCt%(gJX0Z);4bXA^`qCBrT^1?ftnl;|smC-*-WzfBk|Q(SX(Z+le!D02 z9d&@XEDxuB@jUK!8GDJ->`-~H4fvwaZC*zh$RLE(D1?vrJmfamQT`TTs?AdUfUG0_ z;nhKnRiml*_o`42trB0F{cQKyPoq~Kk1%rW%QsvEBiXhe^*r<=kTNzkKgT(tI=iiY z1yeT9m5hG}WrHURap;L=Ztf}Rr(R&~)|9~W}eymwu%%J%yb@K?=d zD{Zz$1*^ejXi8zWS?;5goV{#;Wy|`Mt}1BLc@gFzfsY``x+EcPMTM2XHcl-E>L!L= z+aEO94JWqZq4|!oedI3q?H2A$f_z7kOC{}k=DhaXykQ(r^r>UiQR7wUi8!t*dwH~s zuywCD86(-J4Iec_F~h92Xb;3-#~W7TaJZ!?EdmDxB@%W`eTupc$!Qt47JM}^62<{? z290$}(P(@#EGvpkN>L<(AWh7(`J)_keumrAzMbb~v*O6s=)5aHh@+27fn0JXcA)N* zT(49^-6ss>+<`hxk)Cj$Xy}hUTs127b1Yw4L*B~ZU-XsJh#9pxX<8CYyV=Z&tEI1J z_2$ANV!fH2VW9uv)Kh-c0%)|(i*b$U-33LyJnVG&bZs%#xADS$Sv*EqzL~+4RJRUq zqo@*cBlt0;HFLU09i@I1RhfPNDaY;3#nYla16xM+jt{ZpL7R-w7lK0BZD`ZB;#du` z`_Lwkf6gj)erxzLkP`nk{h%eDIK6|V%d3DY9L2N8PZS@qz<1_il5H!!mmm{Qn4oof zoj8^pKN=2mwbIa^sLLOSl6F0M%eQ5KARzQ~P7wchmY zXEhq%7%I-#ZN;Or4T97V|3zu;KdRS*I%8n~~X6anIZy{g}Cv zd(cK&nPZX6`)pO}gmLFimuHhQcj^LZ^_*c{*0T}&a64UI+igLEl<@82lfDQ0pDsR# zZQaQvueNN_QUpmKqBGX;DQPF#^Z=`s@uw2YyqF~2x)nZKDTKE)#Nl5ame3ml)fHj4 zRPt`WE~$XccS6s`YC*qECkZmMFvXyF)V95%+TQC9@i&4&0kK!%Epg=_U&#D&Tv&Y8 z4oC-1ZLPeeD04s^s6)%PPemBaUtd6;#uUe8Y&WMz`hyp$JI0{LMa4;V`DrIze^9`O zEm#cB@yRLPB!2MhV5kNyP|C>$vL85qgu<&2kK2jgx@2o0#I5WD8c;STlg3jDgJ+?U z0f_S;9ARELjZYf#drzDk@5QxRnaxXtn?(ivG2F-&ux@~;*gXqpkMs2FaUsauwBF3J zwLoP~F&?R(L2kOGpOY_(pYy3$LRMdK-_-iPGZ$_Ar6j7)=NqWG4_D=T$bOn6VRq?rNHyW}e|UX4m+<6J$W^Z2lPEDU;O8HKggtn*~x zGN>5*gtXHT%+PRqFJynWtBQD3?EvW%mz}~Y6(3a>m82WSLJ!2`t)$yfpDw?aogjpe544nn%QfMqz=f4 zt~+zm;@;{=oiQ?F4b9@DyhvfsgRFn+OySVd;{v35`LWf9_&uF*Vp?5WD^(QL5acT} zEwZhc+VkKqy~|YIh39tyw8%)bLP%Z8Lq2Gh?+^3+dG084-q|&;sV&6J!OKHk`&)$hE26+iRx#9 z)V_UYt;Kh_WcvQOt=51`R@7&2>1nv%oQ*pl)P!63$O+l;RrD5)Y7ozmv-ax;Q>`8& zBVniZ^BQIux_DkX)n>wr^!Ch4kz@4scgDWUX*YKXZgZ0gkA0=d3uiZGzH|kP&Tly& zTqh{(pV&WQ7DmD!H`Xx3cf zTJ9KVHC!_3O;ETBcKVt1$hY=*#t&B}_G)S%7+d?E3rsJtMj)P*k9;!#L*k47!`3)9zB{65n6!vQpbNsKtt) z#Q-jR3~wEgxI%Hh6Lwc+9(rK^W+>}2y(;M_HQ=eNh1R8=%YNt;9_u$UDE_6YlsPP< zBfr;2hC^06%UP&1PcgdmF&m|dF!$75YZ%GiU_>XG9KPk_v3k@Wadp;Euzm1ADBsa? zLpo#T*2-J4%~ZkiU3gbGg)MCCt;nD`u0^QY2Coe*yH(Qx#^EgTZS~m97{i!UKf`$h zqk&;;#B1z=;pmnPj@?~lIj0_~PxO1PaS_Mg%Z=QzXKE)E7NM*0L}m$FSnZsenhd>b zJNl|~?kW7}mS`QyHo3i!-_Bh`xT|6DZHxkQ-cYsRRuA;ac1O4)c#q`Q@1N&wLS+CT@QHnh5sqb`q4Cse0z+_89n1MCeSUVmMi5Y- z=KlH*RzsSzHv}PrTLk@aToM4}Lfc%gL zDGH5}^k_pRGD7?s@Q~7~jotEyIQIOzG=!Yu5YtQG>Xw$&`}xo+a_G$g&6`5<>hPk6 zuBP4Gk{;c3L6s2K!Ox6=s#)xTs^o9YyfeUCDej=JcLnOl96=cf@+Y5F1}wu&PGoLh zSL_naq*ShGB|a@{%2Wb7U1f;u7YXOJZHF~*lOji^pX%)L>QFGZ+FBsag?18s%w8oQ zt4XynkH^Om8T@(Gg{bLi4pPK(_;~#2yO_Me`r9#xc}SLTtfxGS_n%Oy$#@Voxmg1p zXJdJ+FUQEZPBIe?15A9d!@r9gV(e27*@>_~kZ9=)jM9PU_W{=1rc26At;P`1kR*Xo zk}bY2IuSO(0_nAm=3ucKi&4-(kY%VEz!b3-$#ivz*@*os=+i@k<9;Wz6DlxxX%$cF zAAMxHk2jdq(>6)rAHBf?egWUfvHp3fCfj!?jF*9D6wFfw9~|F6z8t?6UY{nrUHWoN zV8gtJeech{eMfN*f_ri4XJ~9KbryTB$^u}WukfuuG~_aZKBYRp;|^{rK;P`8%@q1b z;4d~a=`^W$8^eWiLE!}N%x^Nhx*m%~Y3UCp3u$lS;%%^4Dx%fpN8hp<8`o#ERaiiM z1jx|5pP;HZ6T6nT&V@ z69qd48;_DD+2&gy_p;?zr6a7Z`B4<8gegtb2Ny74M0ujf1;*HIg@~lU-Yq7!nmkjV z?vW2KkgmO7&4w|}w*~A;I93taT!}|s?sSR7Inkex&auw~;99$8=k&H~a{4yG)mrW% zP?nSotoi%g;N6g(oNVg*O$o>+_4?ic2WkN=DBj@aSk1D=nQ1(!<)dEnACmbwu1?{L zN^o~E%n;Q?fl-^+)>I)l(-k5`Jo zZ!azqCTTcDcrF62YV!Rc>uzi%vVQc)_~(FIh&2(|e+a#N74rv_k#2pzN@m5IKz1I_ zqpw*Zwf?;^dN}rwl{Mbux6KmjhW56Q0eL1$HjkdCy}0sP3{o(8SsK<*NGO9MGzQ56&Z z;y!o_`q{wy5fwep1EMdl-VkcfagPspXqz=K>S|wfdXr7BkA$gL+`9TP!k|TmVtGbo z5qwKh<3LOFndb*j?vEMON$L~Zza9pE6(q|BNegao0+~mQetNop` zw0&US*5`|HFXkSdu5rz?w!41?!Ylq+%Pn=!t-QuOSG|7}I*t#^Llf;2{mHxhdT{vth3P z*iHIts@Lqe3n#bfI(1F@p;F-Jw#2APvE^{t=CoH6mmV{b25}SqzZ$}EXF5pl?Lf<< zzVXdJmE&)tpNC_(MsVV;~VIcD(iC zE=^D=6%pkSMAhT&gbNMvka_O&i}_-Am~@K4D4nq96DuFmOr+}Lf1pQih^Aug_~XW zwW{Suo1Xgq6x{2v;GVI)XFzaTy%Iwf*+KB`3bK4U8Y$Xq=A;+tTI zmr7$A{b80Y=Z7u18Yh^LYk%5;ldf((?I|MolUH8E_%Y(8EHY^?A}n4V=0edr5-c=7;yT*nM*LZn4g@A1F~fS+JO zYZ^~G8g@%ZkDagaccoDl|0PgVPyn!W#pId%@5KUT4JHQuN8j@5jyzs#{ZcT+&^%gt z#X32T;_0+=DRY#5=yS$njm>mIKseLVFRcpmCIAKyK(c`7Ny^><(w*eQOK3wspcEh% zi!-a(eM8^D3hs#nkP4<3G^TAK3gKQrR>J*azt_vZVGl^ArBVP+GIl=Gi^wk-mB$9J z^Y(!FjL)EXRP@ro^L~H=92VWiCA0s|9bk?|OYZrR`MAwxl`)9e=jnF+bs!gbS7zr6 zxwsTc0QDywGrZg@U8)6egy?i+Un{#DR!v6#eJ*7G!2?1c)$Ty_!9&(Oin^0MSjefo zRXy~9NzL|=`FEbjMu6}Eysk{`5?jfr%V>f<@k}p)?@MXF_kxcLSg*+50uPk!M2uE# z?7uRu*gWWD2jI=YcGo>Tor~NhQu(GiHh+pj{_0MJRTe}KR9sm=iv&tR<sI=is5;+|xrS!&dGqeWRxdAvZ(;=##p%0A>dR22?P+=33 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

?Tw5^j2|F7UlNIcESP9uzpmkq?c1ue(<5C? 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. The four ``Suffix`` components are: -* A function that creates the process model -* Dictionary of parameters and their nominal value -* A measurement object -* A design variables object -* A Numpy ``array`` containing the Prior FIM -* Optimization solver - -Below is a list of arguments that Pyomo.DoE expects the user to provide. - -parameter_dict : ``dictionary`` - A ``dictionary`` of parameter names and values. If they are an indexed variable, put the variable name and index in a nested ``Dictionary``. - -design_variables: ``DesignVariables`` - A ``DesignVariables`` of design variables, provided by the DesignVariables class. - If this design var is independent of time (constant), set the time to [0] - -measurement_variables : ``MeasurementVariables`` - A ``MeasurementVariables`` of the measurements, provided by the MeasurementVariables class. - -create_model : ``function`` - A ``function`` returning a deterministic process model. - -prior_FIM : ``array`` - An ``array`` defining the Fisher information matrix (FIM) for prior experiments, default is a zero matrix. - -Pyomo.DoE Solver Interface ---------------------------- - -.. figure:: uml.png - :scale: 25 % - - -.. autoclass:: pyomo.contrib.doe.doe.DesignOfExperiments - :members: __init__, stochastic_program, compute_FIM, run_grid_search - -.. Note:: - ``stochastic_program()`` includes the following steps: - #. Build two-stage stochastic programming optimization model where scenarios correspond to finite difference approximations for the Jacobian of the response variables with respect to calibrated model parameters - #. Fix the experiment design decisions and solve a square (i.e., zero degrees of freedom) instance of the two-stage DOE problem. This step is for initialization. - #. Unfix the experiment design decisions and solve the two-stage DOE problem. - -.. autoclass:: pyomo.contrib.doe.measurements.MeasurementVariables - :members: __init__, add_variables - -.. autoclass:: pyomo.contrib.doe.measurements.DesignVariables - :members: __init__, add_variables - -.. autoclass:: pyomo.contrib.doe.scenario.ScenarioGenerator - :special-members: __init__ - -.. autoclass:: pyomo.contrib.doe.result.FisherResults - :members: __init__, result_analysis - -.. autoclass:: pyomo.contrib.doe.result.GridSearchResult - :special-members: __init__ +* ``experiment_inputs`` - The experimental design decisions +* ``experiment_outputs`` - The values measured during the experiment +* ``measurement_error`` - The error associated with individual values measured during the experiment +* ``unknown_parameters`` - Those parameters in the model that are estimated using the measured values during the experiment +An example ``Experiment`` object that builds and labels the model is shown in the next few sections. Pyomo.DoE Usage Example ----------------------- @@ -203,89 +153,87 @@ The goal of MBDoE is to optimize the experiment design variables :math:`\boldsym The observation errors are assumed to be independent both in time and across measurements with a constant standard deviation of 1 M for each species. -Step 0: Import Pyomo and the Pyomo.DoE module -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Step 0: Import Pyomo and the Pyomo.DoE module and create an ``Experiment`` class +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. doctest:: >>> # === Required import === >>> import pyomo.environ as pyo - >>> from pyomo.dae import ContinuousSet, DerivativeVar - >>> from pyomo.contrib.doe import DesignOfExperiments, MeasurementVariables, DesignVariables + >>> from pyomo.contrib.doe import DesignOfExperiments >>> import numpy as np +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py + :start-after: ======================== + :end-before: End constructor definition + Step 1: Define the Pyomo process model ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The process model for the reaction kinetics problem is shown below. +The process model for the reaction kinetics problem is shown below. We build the model without any data or discretization. -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_kinetics.py - :language: python - :pyobject: create_model +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py + :start-after: Create flexible model without data + :end-before: End equation definition -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_kinetics.py - :language: python - :pyobject: disc_for_measure +Step 2: Finalize the Pyomo process model +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. note:: - The model requires at least two options: "block" and "global". Both options requires the pass of a created empty Pyomo model. - With "global" option, only design variables and their time sets need to be defined; - With "block" option, a full model needs to be defined. +Here we add data to the model and finalize the discretization. This step is required before the model can be labeled. +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py + :start-after: End equation definition + :end-before: End model finalization -Step 2: Define the inputs for Pyomo.DoE -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_compute_FIM.py - :language: python - :start-at: # Control time set - :end-before: ### Compute +Step 3: Label the information needed for DoE analysis +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +We label the four important groups as defined before. -Step 3: Compute the FIM of a square MBDoE problem -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py + :start-after: End model finalization + :end-before: End model labeling -This method computes an MBDoE optimization problem with no degree of freedom. +Step 4: Implement the ``get_labeled_model`` method +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -This method can be accomplished by two modes, ``direct_kaug`` and ``sequential_finite``. -``direct_kaug`` mode requires the installation of the solver `k_aug `_. +This method utilizes the previous 3 steps and is used by `Pyomo.DoE` to build the model to perform optimal experimental design. -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_compute_FIM.py - :language: python - :start-after: ### Compute the FIM - :end-before: # test result +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py + :start-after: End constructor definition + :end-before: Create flexible model without data -Step 4: Exploratory analysis (Enumeration) +Step 5: Exploratory analysis (Enumeration) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Exploratory analysis is suggested to enumerate the design space to check if the problem is identifiable, i.e., ensure that D-, E-optimality metrics are not small numbers near zero, and Modified E-optimality is not a big number. -Pyomo.DoE accomplishes the exploratory analysis with the ``run_grid_search`` function. -It allows users to define any number of design decisions. Heatmaps can be drawn by two design variables, fixing other design variables. -1D curve can be drawn by one design variable, fixing all other variables. -The function ``run_grid_search`` enumerates over the design space, each MBDoE problem accomplished by ``compute_FIM`` method. -Therefore, ``run_grid_search`` supports only two modes: ``sequential_finite`` and ``direct_kaug``. +Pyomo.DoE can perform exploratory sensitivity analysis with the ``compute_FIM_full_factorial`` function. +The ``compute_FIM_full_factorial`` function generates a grid over the design space as specified by the user. Each grid point represents an MBDoE problem solved using ``compute_FIM`` method. In this way, sensitivity of the FIM over the design space can be evaluated. -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_grid_search.py - :language: python - :pyobject: main +The following code executes the above problem description: -Successful run of the above code shows the following figure: +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_example.py + :start-after: Read in file + :end-before: End sensitivity analysis -.. figure:: grid-1.png - :scale: 35 % +An example output of the code above, a design exploration for the initial concentration and temperature as experimental design variables with 9 values, produces the four figures summarized below: -A heatmap shows the change of the objective function, a.k.a. the experimental information content, in the design region. Horizontal and vertical axes are two design variables, while the color of each grid shows the experimental information content. Taking the Fig. Reactor case - A optimality as example, A-optimality shows that the most informative region is around $C_{A0}=5.0$ M, $T=300.0$ K, while the least informative region is around $C_{A0}=1.0$ M, $T=700.0$ K. +.. figure:: FIM_sensitivity.png + :scale: 50 % -Step 5: Gradient-based optimization -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +A heatmap shows the change of the objective function, a.k.a. the experimental information content, in the design space. Horizontal and vertical axes are the two experimental design variables, while the color of each grid shows the experimental information content. For A optimality (top left subfigure), the figure shows that the most informative region is around :math:`C_{A0}=5.0` M, :math:`T=300.0` K, while the least informative region is around :math:`C_{A0}=1.0` M, :math:`T=700.0` K. + +Step 6: Performing an optimal experimental design +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Pyomo.DoE accomplishes gradient-based optimization with the ``stochastic_program`` function for A- and D-optimality design. +In step 5, the DoE object was constructed to perform an exploratory sensitivity analysis. The same object can be used to design an optimal experiment with a single line of code. -This function solves twice: It solves the square version of the MBDoE problem first, and then unfixes the design variables as degree of freedoms and solves again. In this way the optimization problem can be well initialized. +.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_example.py + :start-after: Begin optimal DoE + :end-before: Print out a results summary -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_optimize_doe.py - :language: python - :pyobject: main +When run, the optimal design is an initial concentration of 5.0 mol/L and an initial temperature of 494 K with all other temperatures being 300 K. The corresponding log-10 determinant of the FIM is 13.75 diff --git a/doc/OnlineDocs/user_guide/contributed_packages/index.rst b/doc/OnlineDocs/user_guide/contributed_packages/index.rst index b1d9cbbad3b..65c14a721df 100644 --- a/doc/OnlineDocs/user_guide/contributed_packages/index.rst +++ b/doc/OnlineDocs/user_guide/contributed_packages/index.rst @@ -15,6 +15,7 @@ Contributed packages distributed with Pyomo: .. toctree:: :maxdepth: 1 + alternative_solutions.rst community.rst doe/doe.rst gdpopt.rst From 54fdf0201804fb8a3c426024d4315d75cc245e22 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 23 Aug 2024 10:59:50 -0600 Subject: [PATCH 2378/3044] Reorganizing top-level TOC --- doc/OnlineDocs/index.rst | 50 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index d074b8ce29c..337c066c7d9 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -14,13 +14,53 @@ with a diverse set of optimization capabilities. * - Getting Started | :doc:`Installation ` - - User Guide - | :doc:`User guide index ` - * - Developer Guide - | :doc:`Index ` + | :doc:`Pyomo Overview ` + - How-To Guide + | :doc:`Interrogating Models` + | :doc:`Manipulating Models` + | :doc:`Solver Recipes` + | :doc:`Debugging Models` + + * - User Explanations + | :doc:`Pyomo Philosophy` + | :doc:`Concrete and Abstract Models` + | :doc:`Component Hierarchy` + | :doc:`Expression System` + | :doc:`Transformations` + | :doc:`Modeling in Pyomo ` + | :doc:`Math Programming` + | :doc:`GDP` + | :doc:`DAE` + | :doc:`Network` + | :doc:`Piecewise Linear` + | :doc:`Constraint Programming` + | :doc:`Units of Measure` + | :doc:`Solvers` + | :doc:`PyROS` + | :doc:`MindtPy` + | :doc:`Trust Region` + | :doc:`Analysis in Pyomo` + | :doc:`IIS` + | :doc:`FBBT` + | :doc:`Incidence Analysis` + | :doc:`Parameter Estimation` + | :doc:`Design of Experiments` + | :doc:`MPC` + | :doc:`AOS` + | :doc:`Modeling Utilities` + | :doc:`Latex Printer` + | :doc:`FME` + | :doc:`Model Viewer` + | :doc:`Model Flattening` + | :doc:`Developer Utilities` + | :doc:`Configuration System` + | :doc:`Deprecation System` + + - Reference Guide | :doc:`Library Reference ` - + | :doc:`Common Warnings and Errors` + | :doc:`Related Packages` .. toctree:: :hidden: From 8359d36b1f457026ccecb6aac1a587b4265c9427 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Aug 2024 11:18:52 -0600 Subject: [PATCH 2379/3044] Filling out index --- doc/OnlineDocs/index.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index 337c066c7d9..1fc3b30d8c8 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -20,6 +20,7 @@ with a diverse set of optimization capabilities. | :doc:`Manipulating Models` | :doc:`Solver Recipes` | :doc:`Debugging Models` + | :doc:`Contributing to Pyomo` * - User Explanations | :doc:`Pyomo Philosophy` @@ -27,7 +28,7 @@ with a diverse set of optimization capabilities. | :doc:`Component Hierarchy` | :doc:`Expression System` | :doc:`Transformations` - | :doc:`Modeling in Pyomo ` + | :doc:`Modeling in Pyomo` | :doc:`Math Programming` | :doc:`GDP` | :doc:`DAE` @@ -39,6 +40,7 @@ with a diverse set of optimization capabilities. | :doc:`PyROS` | :doc:`MindtPy` | :doc:`Trust Region` + | :doc:`Pynumero` | :doc:`Analysis in Pyomo` | :doc:`IIS` | :doc:`FBBT` @@ -55,12 +57,14 @@ with a diverse set of optimization capabilities. | :doc:`Developer Utilities` | :doc:`Configuration System` | :doc:`Deprecation System` - + | :doc:`Experimental` + | :doc:`Kernel` - Reference Guide | :doc:`Library Reference ` | :doc:`Common Warnings and Errors` | :doc:`Related Packages` + | :doc:`Preview capabilities through ``pyomo.__future__``` .. toctree:: :hidden: @@ -109,3 +113,4 @@ Citing Pyomo Bynum, Michael L., Gabriel A. Hackebeil, William E. Hart, Carl D. Laird, Bethany L. Nicholson, John D. Siirola, Jean-Paul Watson, and David L. Woodruff. Pyomo - Optimization Modeling in Python, 3rd Edition. Springer, 2021. Hart, William E., Jean-Paul Watson, and David L. Woodruff. "Pyomo: modeling and solving mathematical programs in Python." Mathematical Programming Computation 3, no. 3 (2011): 219-260. + From 520820b82ebf8ae2f568ebdd1fbfc71a1f5c9fda Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Aug 2024 11:23:33 -0600 Subject: [PATCH 2380/3044] Playing with whitespace --- doc/OnlineDocs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index 1fc3b30d8c8..87d9a664f62 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -15,7 +15,7 @@ with a diverse set of optimization capabilities. * - Getting Started | :doc:`Installation ` | :doc:`Pyomo Overview ` - - How-To Guide + - How-To Guide | :doc:`Interrogating Models` | :doc:`Manipulating Models` | :doc:`Solver Recipes` From a7caa36fa97ab6dcfa08749b8bca3969e30794dc Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Aug 2024 11:24:32 -0600 Subject: [PATCH 2381/3044] removing newlines in table --- doc/OnlineDocs/index.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index 87d9a664f62..c7f6231bae4 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -21,7 +21,6 @@ with a diverse set of optimization capabilities. | :doc:`Solver Recipes` | :doc:`Debugging Models` | :doc:`Contributing to Pyomo` - * - User Explanations | :doc:`Pyomo Philosophy` | :doc:`Concrete and Abstract Models` @@ -59,7 +58,6 @@ with a diverse set of optimization capabilities. | :doc:`Deprecation System` | :doc:`Experimental` | :doc:`Kernel` - - Reference Guide | :doc:`Library Reference ` | :doc:`Common Warnings and Errors` From cc082e7263055ac9d7141c38effe38e86fe149f3 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Aug 2024 11:29:58 -0600 Subject: [PATCH 2382/3044] Learning how tables work in rst' --- doc/OnlineDocs/index.rst | 68 ++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index c7f6231bae4..a24d82ddd90 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -23,41 +23,41 @@ with a diverse set of optimization capabilities. | :doc:`Contributing to Pyomo` * - User Explanations | :doc:`Pyomo Philosophy` - | :doc:`Concrete and Abstract Models` - | :doc:`Component Hierarchy` - | :doc:`Expression System` - | :doc:`Transformations` + | :doc:`Concrete and Abstract Models` + | :doc:`Component Hierarchy` + | :doc:`Expression System` + | :doc:`Transformations` | :doc:`Modeling in Pyomo` - | :doc:`Math Programming` - | :doc:`GDP` - | :doc:`DAE` - | :doc:`Network` - | :doc:`Piecewise Linear` - | :doc:`Constraint Programming` - | :doc:`Units of Measure` - | :doc:`Solvers` - | :doc:`PyROS` - | :doc:`MindtPy` - | :doc:`Trust Region` - | :doc:`Pynumero` - | :doc:`Analysis in Pyomo` - | :doc:`IIS` - | :doc:`FBBT` - | :doc:`Incidence Analysis` - | :doc:`Parameter Estimation` - | :doc:`Design of Experiments` - | :doc:`MPC` - | :doc:`AOS` - | :doc:`Modeling Utilities` - | :doc:`Latex Printer` - | :doc:`FME` - | :doc:`Model Viewer` - | :doc:`Model Flattening` - | :doc:`Developer Utilities` - | :doc:`Configuration System` - | :doc:`Deprecation System` - | :doc:`Experimental` - | :doc:`Kernel` + | :doc:`Math Programming` + | :doc:`GDP` + | :doc:`DAE` + | :doc:`Network` + | :doc:`Piecewise Linear` + | :doc:`Constraint Programming` + | :doc:`Units of Measure` + | :doc:`Solvers` + | :doc:`PyROS` + | :doc:`MindtPy` + | :doc:`Trust Region` + | :doc:`Pynumero` + | :doc:`Analysis in Pyomo` + | :doc:`IIS` + | :doc:`FBBT` + | :doc:`Incidence Analysis` + | :doc:`Parameter Estimation` + | :doc:`Design of Experiments` + | :doc:`MPC` + | :doc:`AOS` + | :doc:`Modeling Utilities` + | :doc:`Latex Printer` + | :doc:`FME` + | :doc:`Model Viewer` + | :doc:`Model Flattening` + | :doc:`Developer Utilities` + | :doc:`Configuration System` + | :doc:`Deprecation System` + | :doc:`Experimental` + | :doc:`Kernel` - Reference Guide | :doc:`Library Reference ` | :doc:`Common Warnings and Errors` From 8ec5067fd1742c919c6118d9796941d2154a7e0d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Aug 2024 11:35:50 -0600 Subject: [PATCH 2383/3044] Link to future, removing duplicate related packages' --- doc/OnlineDocs/index.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index a24d82ddd90..f28695dcc94 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -61,8 +61,7 @@ with a diverse set of optimization capabilities. - Reference Guide | :doc:`Library Reference ` | :doc:`Common Warnings and Errors` - | :doc:`Related Packages` - | :doc:`Preview capabilities through ``pyomo.__future__``` + | :doc:`Preview capabilities through ``pyomo.__future__`` ` .. toctree:: :hidden: From d3baf283d1c1e7b9f291cdb437ad21e279b20dd8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Aug 2024 11:37:40 -0600 Subject: [PATCH 2384/3044] wider table columns --- doc/OnlineDocs/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index f28695dcc94..c8838e78d47 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -11,6 +11,7 @@ with a diverse set of optimization capabilities. .. list-table:: :class: index-table + :widths: 50% 50% * - Getting Started | :doc:`Installation ` From d451596256a1a94382958eef85382743751c733e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Aug 2024 11:55:21 -0600 Subject: [PATCH 2385/3044] renaming user_guides to user_explanations --- .../contributed_packages/alternative_solutions.rst | 0 .../contributed_packages/communities_8pp.png | Bin .../contributed_packages/communities_decode_1.png | Bin .../contributed_packages/community.rst | 0 .../contributed_packages/doe/CCSI-license.txt | 0 .../contributed_packages/doe/FIM_sensitivity.png | Bin .../contributed_packages/doe/doe.rst | 0 .../contributed_packages/doe/flowchart.png | Bin .../contributed_packages/doe/grid-1.png | Bin .../contributed_packages/doe/reactor.png | Bin .../contributed_packages/doe/uml.png | Bin .../contributed_packages/gdpopt.rst | 0 .../contributed_packages/gdpopt_flowchart.png | Bin .../contributed_packages/iis.rst | 0 .../contributed_packages/incidence/api.rst | 0 .../contributed_packages/incidence/config.rst | 0 .../contributed_packages/incidence/connected.rst | 0 .../incidence/dulmage_mendelsohn.rst | 0 .../contributed_packages/incidence/incidence.rst | 0 .../contributed_packages/incidence/index.rst | 0 .../contributed_packages/incidence/interface.rst | 0 .../contributed_packages/incidence/matching.rst | 0 .../contributed_packages/incidence/overview.rst | 0 .../contributed_packages/incidence/scc_solver.rst | 0 .../incidence/triangularize.rst | 0 .../contributed_packages/incidence/tutorial.bt.rst | 0 .../incidence/tutorial.btsolve.rst | 0 .../contributed_packages/incidence/tutorial.dm.rst | 0 .../contributed_packages/incidence/tutorial.rst | 0 .../contributed_packages/index.rst | 0 .../contributed_packages/latex_printer.rst | 0 .../contributed_packages/mcpp.rst | 0 .../contributed_packages/mindtpy.rst | 0 .../contributed_packages/mpc/api.rst | 0 .../contributed_packages/mpc/conversion.rst | 0 .../contributed_packages/mpc/data.rst | 0 .../contributed_packages/mpc/examples.rst | 0 .../contributed_packages/mpc/faq.rst | 0 .../contributed_packages/mpc/index.rst | 0 .../contributed_packages/mpc/interface.rst | 0 .../contributed_packages/mpc/modeling.rst | 0 .../contributed_packages/mpc/overview.rst | 0 .../contributed_packages/multistart.rst | 0 .../contributed_packages/parmest/api.rst | 0 .../contributed_packages/parmest/boxplot.png | Bin .../contributed_packages/parmest/covariance.rst | 0 .../contributed_packages/parmest/datarec.rst | 0 .../contributed_packages/parmest/driver.rst | 0 .../contributed_packages/parmest/examples.rst | 0 .../contributed_packages/parmest/graphics.rst | 0 .../contributed_packages/parmest/index.rst | 0 .../contributed_packages/parmest/installation.rst | 0 .../contributed_packages/parmest/overview.rst | 0 .../parmest/pairwise_plot_CI.png | Bin .../parmest/pairwise_plot_LR.png | Bin .../contributed_packages/parmest/parallel.rst | 0 .../contributed_packages/parmest/scencreate.rst | 0 .../contributed_packages/preprocessing.rst | 0 .../contributed_packages/pynumero/api.rst | 0 .../pynumero/backward_compatibility.rst | 0 .../contributed_packages/pynumero/index.rst | 0 .../contributed_packages/pynumero/installation.rst | 0 .../pynumero/pynumero.interfaces.ampl_nlp.rst | 0 .../pynumero/pynumero.interfaces.asl_nlp.rst | 0 .../pynumero/pynumero.interfaces.extended_nlp.rst | 0 .../pynumero.interfaces.external_grey_box_model.rst | 0 .../pynumero/pynumero.interfaces.nlp.rst | 0 .../pynumero/pynumero.interfaces.projected_nlp.rst | 0 .../pynumero.interfaces.pyomo_grey_box_nlp.rst | 0 .../pynumero/pynumero.interfaces.pyomo_nlp.rst | 0 .../pynumero/pynumero.interfaces.rst | 0 .../pynumero/pynumero.linalg.base.rst | 0 .../pynumero/pynumero.linalg.ma27.rst | 0 .../pynumero/pynumero.linalg.ma57.rst | 0 .../pynumero/pynumero.linalg.mumps.rst | 0 .../pynumero/pynumero.linalg.rst | 0 .../pynumero/pynumero.linalg.scipy.rst | 0 .../pynumero/pynumero.sparse.block_vector.rst | 0 .../pynumero/pynumero.sparse.rst | 0 .../tutorial.block_vectors_and_matrices.rst | 0 .../pynumero/tutorial.linear_solver_interfaces.rst | 0 .../pynumero/tutorial.mpi_blocks.rst | 0 .../pynumero/tutorial.nlp_interfaces.rst | 0 .../contributed_packages/pynumero/tutorial.rst | 0 .../contributed_packages/pyros.rst | 0 .../contributed_packages/satsolver.rst | 0 .../contributed_packages/sensitivity_toolbox.rst | 0 .../contributed_packages/trustregion.rst | 0 .../{user_guide => user_explanations}/errors.rst | 0 .../external_tutorials.rst | 0 .../flattener/index.rst | 0 .../flattener/motivation.rst | 0 .../flattener/reference.rst | 0 .../{user_guide => user_explanations}/index.rst | 0 .../modeling_extensions/__init__.py | 0 .../modeling_extensions/bilevel.rst | 0 .../modeling_extensions/dae.rst | 0 .../modeling_extensions/gdp/concepts.rst | 0 .../modeling_extensions/gdp/index.rst | 0 .../modeling_extensions/gdp/modeling.rst | 0 .../modeling_extensions/gdp/solving.rst | 0 .../modeling_extensions/index.rst | 0 .../modeling_extensions/mpec.rst | 0 .../modeling_extensions/network.rst | 0 .../modeling_extensions/reduce_points_demo.png | Bin .../modeling_extensions/stochastic_programming.rst | 0 .../persistent_solvers.rst | 0 .../pyomo_modeling_components/Constraints.rst | 0 .../pyomo_modeling_components/Expressions.rst | 0 .../pyomo_modeling_components/Objectives.rst | 0 .../pyomo_modeling_components/Parameters.rst | 0 .../pyomo_modeling_components/Sets.rst | 0 .../pyomo_modeling_components/Suffixes.rst | 0 .../pyomo_modeling_components/Variables.rst | 0 .../pyomo_modeling_components/index.rst | 0 .../{user_guide => user_explanations}/scaling.rst | 0 .../sos_constraints.rst | 0 .../units_container.rst | 0 .../working_models.rst | 0 119 files changed, 0 insertions(+), 0 deletions(-) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/alternative_solutions.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/communities_8pp.png (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/communities_decode_1.png (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/community.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/doe/CCSI-license.txt (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/doe/FIM_sensitivity.png (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/doe/doe.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/doe/flowchart.png (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/doe/grid-1.png (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/doe/reactor.png (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/doe/uml.png (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/gdpopt.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/gdpopt_flowchart.png (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/iis.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/incidence/api.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/incidence/config.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/incidence/connected.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/incidence/dulmage_mendelsohn.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/incidence/incidence.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/incidence/index.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/incidence/interface.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/incidence/matching.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/incidence/overview.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/incidence/scc_solver.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/incidence/triangularize.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/incidence/tutorial.bt.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/incidence/tutorial.btsolve.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/incidence/tutorial.dm.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/incidence/tutorial.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/index.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/latex_printer.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/mcpp.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/mindtpy.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/mpc/api.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/mpc/conversion.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/mpc/data.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/mpc/examples.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/mpc/faq.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/mpc/index.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/mpc/interface.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/mpc/modeling.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/mpc/overview.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/multistart.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/parmest/api.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/parmest/boxplot.png (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/parmest/covariance.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/parmest/datarec.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/parmest/driver.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/parmest/examples.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/parmest/graphics.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/parmest/index.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/parmest/installation.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/parmest/overview.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/parmest/pairwise_plot_CI.png (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/parmest/pairwise_plot_LR.png (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/parmest/parallel.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/parmest/scencreate.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/preprocessing.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/api.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/backward_compatibility.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/index.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/installation.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.interfaces.nlp.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.interfaces.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.linalg.base.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.linalg.ma27.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.linalg.ma57.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.linalg.mumps.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.linalg.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.linalg.scipy.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.sparse.block_vector.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/pynumero.sparse.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/tutorial.mpi_blocks.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/tutorial.nlp_interfaces.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pynumero/tutorial.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/pyros.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/satsolver.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/sensitivity_toolbox.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/contributed_packages/trustregion.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/errors.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/external_tutorials.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/flattener/index.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/flattener/motivation.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/flattener/reference.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/index.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/modeling_extensions/__init__.py (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/modeling_extensions/bilevel.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/modeling_extensions/dae.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/modeling_extensions/gdp/concepts.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/modeling_extensions/gdp/index.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/modeling_extensions/gdp/modeling.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/modeling_extensions/gdp/solving.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/modeling_extensions/index.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/modeling_extensions/mpec.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/modeling_extensions/network.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/modeling_extensions/reduce_points_demo.png (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/modeling_extensions/stochastic_programming.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/persistent_solvers.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/pyomo_modeling_components/Constraints.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/pyomo_modeling_components/Expressions.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/pyomo_modeling_components/Objectives.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/pyomo_modeling_components/Parameters.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/pyomo_modeling_components/Sets.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/pyomo_modeling_components/Suffixes.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/pyomo_modeling_components/Variables.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/pyomo_modeling_components/index.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/scaling.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/sos_constraints.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/units_container.rst (100%) rename doc/OnlineDocs/{user_guide => user_explanations}/working_models.rst (100%) diff --git a/doc/OnlineDocs/user_guide/contributed_packages/alternative_solutions.rst b/doc/OnlineDocs/user_explanations/contributed_packages/alternative_solutions.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/alternative_solutions.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/alternative_solutions.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/communities_8pp.png b/doc/OnlineDocs/user_explanations/contributed_packages/communities_8pp.png similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/communities_8pp.png rename to doc/OnlineDocs/user_explanations/contributed_packages/communities_8pp.png diff --git a/doc/OnlineDocs/user_guide/contributed_packages/communities_decode_1.png b/doc/OnlineDocs/user_explanations/contributed_packages/communities_decode_1.png similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/communities_decode_1.png rename to doc/OnlineDocs/user_explanations/contributed_packages/communities_decode_1.png diff --git a/doc/OnlineDocs/user_guide/contributed_packages/community.rst b/doc/OnlineDocs/user_explanations/contributed_packages/community.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/community.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/community.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/doe/CCSI-license.txt b/doc/OnlineDocs/user_explanations/contributed_packages/doe/CCSI-license.txt similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/doe/CCSI-license.txt rename to doc/OnlineDocs/user_explanations/contributed_packages/doe/CCSI-license.txt diff --git a/doc/OnlineDocs/user_guide/contributed_packages/doe/FIM_sensitivity.png b/doc/OnlineDocs/user_explanations/contributed_packages/doe/FIM_sensitivity.png similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/doe/FIM_sensitivity.png rename to doc/OnlineDocs/user_explanations/contributed_packages/doe/FIM_sensitivity.png diff --git a/doc/OnlineDocs/user_guide/contributed_packages/doe/doe.rst b/doc/OnlineDocs/user_explanations/contributed_packages/doe/doe.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/doe/doe.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/doe/doe.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/doe/flowchart.png b/doc/OnlineDocs/user_explanations/contributed_packages/doe/flowchart.png similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/doe/flowchart.png rename to doc/OnlineDocs/user_explanations/contributed_packages/doe/flowchart.png diff --git a/doc/OnlineDocs/user_guide/contributed_packages/doe/grid-1.png b/doc/OnlineDocs/user_explanations/contributed_packages/doe/grid-1.png similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/doe/grid-1.png rename to doc/OnlineDocs/user_explanations/contributed_packages/doe/grid-1.png diff --git a/doc/OnlineDocs/user_guide/contributed_packages/doe/reactor.png b/doc/OnlineDocs/user_explanations/contributed_packages/doe/reactor.png similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/doe/reactor.png rename to doc/OnlineDocs/user_explanations/contributed_packages/doe/reactor.png diff --git a/doc/OnlineDocs/user_guide/contributed_packages/doe/uml.png b/doc/OnlineDocs/user_explanations/contributed_packages/doe/uml.png similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/doe/uml.png rename to doc/OnlineDocs/user_explanations/contributed_packages/doe/uml.png diff --git a/doc/OnlineDocs/user_guide/contributed_packages/gdpopt.rst b/doc/OnlineDocs/user_explanations/contributed_packages/gdpopt.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/gdpopt.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/gdpopt.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/gdpopt_flowchart.png b/doc/OnlineDocs/user_explanations/contributed_packages/gdpopt_flowchart.png similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/gdpopt_flowchart.png rename to doc/OnlineDocs/user_explanations/contributed_packages/gdpopt_flowchart.png diff --git a/doc/OnlineDocs/user_guide/contributed_packages/iis.rst b/doc/OnlineDocs/user_explanations/contributed_packages/iis.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/iis.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/iis.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/api.rst b/doc/OnlineDocs/user_explanations/contributed_packages/incidence/api.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/incidence/api.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/incidence/api.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/config.rst b/doc/OnlineDocs/user_explanations/contributed_packages/incidence/config.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/incidence/config.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/incidence/config.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/connected.rst b/doc/OnlineDocs/user_explanations/contributed_packages/incidence/connected.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/incidence/connected.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/incidence/connected.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/dulmage_mendelsohn.rst b/doc/OnlineDocs/user_explanations/contributed_packages/incidence/dulmage_mendelsohn.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/incidence/dulmage_mendelsohn.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/incidence/dulmage_mendelsohn.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/incidence.rst b/doc/OnlineDocs/user_explanations/contributed_packages/incidence/incidence.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/incidence/incidence.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/incidence/incidence.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/index.rst b/doc/OnlineDocs/user_explanations/contributed_packages/incidence/index.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/incidence/index.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/incidence/index.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/interface.rst b/doc/OnlineDocs/user_explanations/contributed_packages/incidence/interface.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/incidence/interface.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/incidence/interface.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/matching.rst b/doc/OnlineDocs/user_explanations/contributed_packages/incidence/matching.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/incidence/matching.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/incidence/matching.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/overview.rst b/doc/OnlineDocs/user_explanations/contributed_packages/incidence/overview.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/incidence/overview.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/incidence/overview.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/scc_solver.rst b/doc/OnlineDocs/user_explanations/contributed_packages/incidence/scc_solver.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/incidence/scc_solver.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/incidence/scc_solver.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/triangularize.rst b/doc/OnlineDocs/user_explanations/contributed_packages/incidence/triangularize.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/incidence/triangularize.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/incidence/triangularize.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.bt.rst b/doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.bt.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.bt.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.bt.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.btsolve.rst b/doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.btsolve.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.btsolve.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.btsolve.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.dm.rst b/doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.dm.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.dm.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.dm.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.rst b/doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/index.rst b/doc/OnlineDocs/user_explanations/contributed_packages/index.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/index.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/index.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/latex_printer.rst b/doc/OnlineDocs/user_explanations/contributed_packages/latex_printer.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/latex_printer.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/latex_printer.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mcpp.rst b/doc/OnlineDocs/user_explanations/contributed_packages/mcpp.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/mcpp.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/mcpp.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mindtpy.rst b/doc/OnlineDocs/user_explanations/contributed_packages/mindtpy.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/mindtpy.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/mindtpy.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/api.rst b/doc/OnlineDocs/user_explanations/contributed_packages/mpc/api.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/mpc/api.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/mpc/api.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/conversion.rst b/doc/OnlineDocs/user_explanations/contributed_packages/mpc/conversion.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/mpc/conversion.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/mpc/conversion.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/data.rst b/doc/OnlineDocs/user_explanations/contributed_packages/mpc/data.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/mpc/data.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/mpc/data.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/examples.rst b/doc/OnlineDocs/user_explanations/contributed_packages/mpc/examples.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/mpc/examples.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/mpc/examples.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/faq.rst b/doc/OnlineDocs/user_explanations/contributed_packages/mpc/faq.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/mpc/faq.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/mpc/faq.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/index.rst b/doc/OnlineDocs/user_explanations/contributed_packages/mpc/index.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/mpc/index.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/mpc/index.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/interface.rst b/doc/OnlineDocs/user_explanations/contributed_packages/mpc/interface.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/mpc/interface.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/mpc/interface.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/modeling.rst b/doc/OnlineDocs/user_explanations/contributed_packages/mpc/modeling.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/mpc/modeling.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/mpc/modeling.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/overview.rst b/doc/OnlineDocs/user_explanations/contributed_packages/mpc/overview.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/mpc/overview.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/mpc/overview.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/multistart.rst b/doc/OnlineDocs/user_explanations/contributed_packages/multistart.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/multistart.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/multistart.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/api.rst b/doc/OnlineDocs/user_explanations/contributed_packages/parmest/api.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/parmest/api.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/parmest/api.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/boxplot.png b/doc/OnlineDocs/user_explanations/contributed_packages/parmest/boxplot.png similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/parmest/boxplot.png rename to doc/OnlineDocs/user_explanations/contributed_packages/parmest/boxplot.png diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/covariance.rst b/doc/OnlineDocs/user_explanations/contributed_packages/parmest/covariance.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/parmest/covariance.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/parmest/covariance.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/datarec.rst b/doc/OnlineDocs/user_explanations/contributed_packages/parmest/datarec.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/parmest/datarec.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/parmest/datarec.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/driver.rst b/doc/OnlineDocs/user_explanations/contributed_packages/parmest/driver.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/parmest/driver.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/parmest/driver.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/examples.rst b/doc/OnlineDocs/user_explanations/contributed_packages/parmest/examples.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/parmest/examples.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/parmest/examples.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/graphics.rst b/doc/OnlineDocs/user_explanations/contributed_packages/parmest/graphics.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/parmest/graphics.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/parmest/graphics.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/index.rst b/doc/OnlineDocs/user_explanations/contributed_packages/parmest/index.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/parmest/index.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/parmest/index.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/installation.rst b/doc/OnlineDocs/user_explanations/contributed_packages/parmest/installation.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/parmest/installation.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/parmest/installation.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/overview.rst b/doc/OnlineDocs/user_explanations/contributed_packages/parmest/overview.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/parmest/overview.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/parmest/overview.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/pairwise_plot_CI.png b/doc/OnlineDocs/user_explanations/contributed_packages/parmest/pairwise_plot_CI.png similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/parmest/pairwise_plot_CI.png rename to doc/OnlineDocs/user_explanations/contributed_packages/parmest/pairwise_plot_CI.png diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/pairwise_plot_LR.png b/doc/OnlineDocs/user_explanations/contributed_packages/parmest/pairwise_plot_LR.png similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/parmest/pairwise_plot_LR.png rename to doc/OnlineDocs/user_explanations/contributed_packages/parmest/pairwise_plot_LR.png diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/parallel.rst b/doc/OnlineDocs/user_explanations/contributed_packages/parmest/parallel.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/parmest/parallel.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/parmest/parallel.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/scencreate.rst b/doc/OnlineDocs/user_explanations/contributed_packages/parmest/scencreate.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/parmest/scencreate.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/parmest/scencreate.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/preprocessing.rst b/doc/OnlineDocs/user_explanations/contributed_packages/preprocessing.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/preprocessing.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/preprocessing.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/api.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/api.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/api.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/api.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/backward_compatibility.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/backward_compatibility.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/backward_compatibility.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/backward_compatibility.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/index.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/index.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/index.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/index.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/installation.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/installation.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/installation.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/installation.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.nlp.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.nlp.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.nlp.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.nlp.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.interfaces.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.base.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.base.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.base.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.base.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.ma27.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.ma27.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.ma27.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.ma27.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.ma57.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.ma57.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.ma57.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.ma57.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.mumps.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.mumps.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.mumps.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.mumps.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.scipy.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.scipy.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.linalg.scipy.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.scipy.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.sparse.block_vector.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.sparse.block_vector.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.sparse.block_vector.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.sparse.block_vector.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.sparse.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.sparse.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/pynumero.sparse.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.sparse.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.mpi_blocks.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.mpi_blocks.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.mpi_blocks.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.mpi_blocks.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.nlp_interfaces.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.nlp_interfaces.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.nlp_interfaces.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.nlp_interfaces.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pynumero/tutorial.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/pyros.rst b/doc/OnlineDocs/user_explanations/contributed_packages/pyros.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/pyros.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/pyros.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/satsolver.rst b/doc/OnlineDocs/user_explanations/contributed_packages/satsolver.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/satsolver.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/satsolver.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/sensitivity_toolbox.rst b/doc/OnlineDocs/user_explanations/contributed_packages/sensitivity_toolbox.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/sensitivity_toolbox.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/sensitivity_toolbox.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/trustregion.rst b/doc/OnlineDocs/user_explanations/contributed_packages/trustregion.rst similarity index 100% rename from doc/OnlineDocs/user_guide/contributed_packages/trustregion.rst rename to doc/OnlineDocs/user_explanations/contributed_packages/trustregion.rst diff --git a/doc/OnlineDocs/user_guide/errors.rst b/doc/OnlineDocs/user_explanations/errors.rst similarity index 100% rename from doc/OnlineDocs/user_guide/errors.rst rename to doc/OnlineDocs/user_explanations/errors.rst diff --git a/doc/OnlineDocs/user_guide/external_tutorials.rst b/doc/OnlineDocs/user_explanations/external_tutorials.rst similarity index 100% rename from doc/OnlineDocs/user_guide/external_tutorials.rst rename to doc/OnlineDocs/user_explanations/external_tutorials.rst diff --git a/doc/OnlineDocs/user_guide/flattener/index.rst b/doc/OnlineDocs/user_explanations/flattener/index.rst similarity index 100% rename from doc/OnlineDocs/user_guide/flattener/index.rst rename to doc/OnlineDocs/user_explanations/flattener/index.rst diff --git a/doc/OnlineDocs/user_guide/flattener/motivation.rst b/doc/OnlineDocs/user_explanations/flattener/motivation.rst similarity index 100% rename from doc/OnlineDocs/user_guide/flattener/motivation.rst rename to doc/OnlineDocs/user_explanations/flattener/motivation.rst diff --git a/doc/OnlineDocs/user_guide/flattener/reference.rst b/doc/OnlineDocs/user_explanations/flattener/reference.rst similarity index 100% rename from doc/OnlineDocs/user_guide/flattener/reference.rst rename to doc/OnlineDocs/user_explanations/flattener/reference.rst diff --git a/doc/OnlineDocs/user_guide/index.rst b/doc/OnlineDocs/user_explanations/index.rst similarity index 100% rename from doc/OnlineDocs/user_guide/index.rst rename to doc/OnlineDocs/user_explanations/index.rst diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/__init__.py b/doc/OnlineDocs/user_explanations/modeling_extensions/__init__.py similarity index 100% rename from doc/OnlineDocs/user_guide/modeling_extensions/__init__.py rename to doc/OnlineDocs/user_explanations/modeling_extensions/__init__.py diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/bilevel.rst b/doc/OnlineDocs/user_explanations/modeling_extensions/bilevel.rst similarity index 100% rename from doc/OnlineDocs/user_guide/modeling_extensions/bilevel.rst rename to doc/OnlineDocs/user_explanations/modeling_extensions/bilevel.rst diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/dae.rst b/doc/OnlineDocs/user_explanations/modeling_extensions/dae.rst similarity index 100% rename from doc/OnlineDocs/user_guide/modeling_extensions/dae.rst rename to doc/OnlineDocs/user_explanations/modeling_extensions/dae.rst diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/gdp/concepts.rst b/doc/OnlineDocs/user_explanations/modeling_extensions/gdp/concepts.rst similarity index 100% rename from doc/OnlineDocs/user_guide/modeling_extensions/gdp/concepts.rst rename to doc/OnlineDocs/user_explanations/modeling_extensions/gdp/concepts.rst diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/gdp/index.rst b/doc/OnlineDocs/user_explanations/modeling_extensions/gdp/index.rst similarity index 100% rename from doc/OnlineDocs/user_guide/modeling_extensions/gdp/index.rst rename to doc/OnlineDocs/user_explanations/modeling_extensions/gdp/index.rst diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/gdp/modeling.rst b/doc/OnlineDocs/user_explanations/modeling_extensions/gdp/modeling.rst similarity index 100% rename from doc/OnlineDocs/user_guide/modeling_extensions/gdp/modeling.rst rename to doc/OnlineDocs/user_explanations/modeling_extensions/gdp/modeling.rst diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/gdp/solving.rst b/doc/OnlineDocs/user_explanations/modeling_extensions/gdp/solving.rst similarity index 100% rename from doc/OnlineDocs/user_guide/modeling_extensions/gdp/solving.rst rename to doc/OnlineDocs/user_explanations/modeling_extensions/gdp/solving.rst diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/index.rst b/doc/OnlineDocs/user_explanations/modeling_extensions/index.rst similarity index 100% rename from doc/OnlineDocs/user_guide/modeling_extensions/index.rst rename to doc/OnlineDocs/user_explanations/modeling_extensions/index.rst diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/mpec.rst b/doc/OnlineDocs/user_explanations/modeling_extensions/mpec.rst similarity index 100% rename from doc/OnlineDocs/user_guide/modeling_extensions/mpec.rst rename to doc/OnlineDocs/user_explanations/modeling_extensions/mpec.rst diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/network.rst b/doc/OnlineDocs/user_explanations/modeling_extensions/network.rst similarity index 100% rename from doc/OnlineDocs/user_guide/modeling_extensions/network.rst rename to doc/OnlineDocs/user_explanations/modeling_extensions/network.rst diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/reduce_points_demo.png b/doc/OnlineDocs/user_explanations/modeling_extensions/reduce_points_demo.png similarity index 100% rename from doc/OnlineDocs/user_guide/modeling_extensions/reduce_points_demo.png rename to doc/OnlineDocs/user_explanations/modeling_extensions/reduce_points_demo.png diff --git a/doc/OnlineDocs/user_guide/modeling_extensions/stochastic_programming.rst b/doc/OnlineDocs/user_explanations/modeling_extensions/stochastic_programming.rst similarity index 100% rename from doc/OnlineDocs/user_guide/modeling_extensions/stochastic_programming.rst rename to doc/OnlineDocs/user_explanations/modeling_extensions/stochastic_programming.rst diff --git a/doc/OnlineDocs/user_guide/persistent_solvers.rst b/doc/OnlineDocs/user_explanations/persistent_solvers.rst similarity index 100% rename from doc/OnlineDocs/user_guide/persistent_solvers.rst rename to doc/OnlineDocs/user_explanations/persistent_solvers.rst diff --git a/doc/OnlineDocs/user_guide/pyomo_modeling_components/Constraints.rst b/doc/OnlineDocs/user_explanations/pyomo_modeling_components/Constraints.rst similarity index 100% rename from doc/OnlineDocs/user_guide/pyomo_modeling_components/Constraints.rst rename to doc/OnlineDocs/user_explanations/pyomo_modeling_components/Constraints.rst diff --git a/doc/OnlineDocs/user_guide/pyomo_modeling_components/Expressions.rst b/doc/OnlineDocs/user_explanations/pyomo_modeling_components/Expressions.rst similarity index 100% rename from doc/OnlineDocs/user_guide/pyomo_modeling_components/Expressions.rst rename to doc/OnlineDocs/user_explanations/pyomo_modeling_components/Expressions.rst diff --git a/doc/OnlineDocs/user_guide/pyomo_modeling_components/Objectives.rst b/doc/OnlineDocs/user_explanations/pyomo_modeling_components/Objectives.rst similarity index 100% rename from doc/OnlineDocs/user_guide/pyomo_modeling_components/Objectives.rst rename to doc/OnlineDocs/user_explanations/pyomo_modeling_components/Objectives.rst diff --git a/doc/OnlineDocs/user_guide/pyomo_modeling_components/Parameters.rst b/doc/OnlineDocs/user_explanations/pyomo_modeling_components/Parameters.rst similarity index 100% rename from doc/OnlineDocs/user_guide/pyomo_modeling_components/Parameters.rst rename to doc/OnlineDocs/user_explanations/pyomo_modeling_components/Parameters.rst diff --git a/doc/OnlineDocs/user_guide/pyomo_modeling_components/Sets.rst b/doc/OnlineDocs/user_explanations/pyomo_modeling_components/Sets.rst similarity index 100% rename from doc/OnlineDocs/user_guide/pyomo_modeling_components/Sets.rst rename to doc/OnlineDocs/user_explanations/pyomo_modeling_components/Sets.rst diff --git a/doc/OnlineDocs/user_guide/pyomo_modeling_components/Suffixes.rst b/doc/OnlineDocs/user_explanations/pyomo_modeling_components/Suffixes.rst similarity index 100% rename from doc/OnlineDocs/user_guide/pyomo_modeling_components/Suffixes.rst rename to doc/OnlineDocs/user_explanations/pyomo_modeling_components/Suffixes.rst diff --git a/doc/OnlineDocs/user_guide/pyomo_modeling_components/Variables.rst b/doc/OnlineDocs/user_explanations/pyomo_modeling_components/Variables.rst similarity index 100% rename from doc/OnlineDocs/user_guide/pyomo_modeling_components/Variables.rst rename to doc/OnlineDocs/user_explanations/pyomo_modeling_components/Variables.rst diff --git a/doc/OnlineDocs/user_guide/pyomo_modeling_components/index.rst b/doc/OnlineDocs/user_explanations/pyomo_modeling_components/index.rst similarity index 100% rename from doc/OnlineDocs/user_guide/pyomo_modeling_components/index.rst rename to doc/OnlineDocs/user_explanations/pyomo_modeling_components/index.rst diff --git a/doc/OnlineDocs/user_guide/scaling.rst b/doc/OnlineDocs/user_explanations/scaling.rst similarity index 100% rename from doc/OnlineDocs/user_guide/scaling.rst rename to doc/OnlineDocs/user_explanations/scaling.rst diff --git a/doc/OnlineDocs/user_guide/sos_constraints.rst b/doc/OnlineDocs/user_explanations/sos_constraints.rst similarity index 100% rename from doc/OnlineDocs/user_guide/sos_constraints.rst rename to doc/OnlineDocs/user_explanations/sos_constraints.rst diff --git a/doc/OnlineDocs/user_guide/units_container.rst b/doc/OnlineDocs/user_explanations/units_container.rst similarity index 100% rename from doc/OnlineDocs/user_guide/units_container.rst rename to doc/OnlineDocs/user_explanations/units_container.rst diff --git a/doc/OnlineDocs/user_guide/working_models.rst b/doc/OnlineDocs/user_explanations/working_models.rst similarity index 100% rename from doc/OnlineDocs/user_guide/working_models.rst rename to doc/OnlineDocs/user_explanations/working_models.rst From 7a0a1ee58120e7d3f1b6cc7309f2facb3f23ba16 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Aug 2024 12:25:03 -0600 Subject: [PATCH 2386/3044] Adding and organizing the user explanations directory --- doc/OnlineDocs/index.rst | 16 ++++++++++++++ .../alternative_solutions.rst | 0 .../communities_8pp.png | Bin .../communities_decode_1.png | Bin .../community.rst | 0 .../doe/CCSI-license.txt | 0 .../doe/FIM_sensitivity.png | Bin .../doe/doe.rst | 0 .../doe/flowchart.png | Bin .../doe/grid-1.png | Bin .../doe/reactor.png | Bin .../doe/uml.png | Bin .../iis.rst | 0 .../incidence/api.rst | 0 .../incidence/config.rst | 0 .../incidence/connected.rst | 0 .../incidence/dulmage_mendelsohn.rst | 0 .../incidence/incidence.rst | 0 .../incidence/index.rst | 0 .../incidence/interface.rst | 0 .../incidence/matching.rst | 0 .../incidence/overview.rst | 0 .../incidence/scc_solver.rst | 0 .../incidence/triangularize.rst | 0 .../incidence/tutorial.bt.rst | 0 .../incidence/tutorial.btsolve.rst | 0 .../incidence/tutorial.dm.rst | 0 .../incidence/tutorial.rst | 0 .../mpc/api.rst | 0 .../mpc/conversion.rst | 0 .../mpc/data.rst | 0 .../mpc/examples.rst | 0 .../mpc/faq.rst | 0 .../mpc/index.rst | 0 .../mpc/interface.rst | 0 .../mpc/modeling.rst | 0 .../mpc/overview.rst | 0 .../parmest/api.rst | 0 .../parmest/boxplot.png | Bin .../parmest/covariance.rst | 0 .../parmest/datarec.rst | 0 .../parmest/driver.rst | 0 .../parmest/examples.rst | 0 .../parmest/graphics.rst | 0 .../parmest/index.rst | 0 .../parmest/installation.rst | 0 .../parmest/overview.rst | 0 .../parmest/pairwise_plot_CI.png | Bin .../parmest/pairwise_plot_LR.png | Bin .../parmest/parallel.rst | 0 .../parmest/scencreate.rst | 0 .../sensitivity_toolbox.rst | 0 .../index.rst => contrib_index.rst} | 0 .../user_explanations/external_tutorials.rst | 20 ------------------ .../Constraints.rst | 0 .../Expressions.rst | 0 .../Objectives.rst | 0 .../Parameters.rst | 0 .../Sets.rst | 0 .../Suffixes.rst | 0 .../Variables.rst | 0 .../{modeling_extensions => modeling}/dae.rst | 0 .../gdp/concepts.rst | 0 .../gdp/index.rst | 0 .../gdp/modeling.rst | 0 .../gdp/solving.rst | 0 .../index.rst | 0 .../mpec.rst | 0 .../network.rst | 0 .../reduce_points_demo.png | Bin .../{ => modeling}/sos_constraints.rst | 0 .../{ => modeling}/units_container.rst | 0 .../modeling_extensions/__init__.py | 10 --------- .../modeling_extensions/bilevel.rst | 6 ------ .../modeling_extensions/index.rst | 12 ----------- .../stochastic_programming.rst | 17 --------------- .../flattener/index.rst | 0 .../flattener/motivation.rst | 0 .../flattener/reference.rst | 0 .../latex_printer.rst | 0 .../preprocessing.rst | 0 .../{ => modeling_utilities}/scaling.rst | 0 .../gdpopt.rst | 0 .../gdpopt_flowchart.png | Bin .../mcpp.rst | 0 .../mindtpy.rst | 0 .../multistart.rst | 0 .../{ => solvers}/persistent_solvers.rst | 0 .../pynumero/api.rst | 0 .../pynumero/backward_compatibility.rst | 0 .../pynumero/index.rst | 0 .../pynumero/installation.rst | 0 .../pynumero/pynumero.interfaces.ampl_nlp.rst | 0 .../pynumero/pynumero.interfaces.asl_nlp.rst | 0 .../pynumero.interfaces.extended_nlp.rst | 0 ...ero.interfaces.external_grey_box_model.rst | 0 .../pynumero/pynumero.interfaces.nlp.rst | 0 .../pynumero.interfaces.projected_nlp.rst | 0 ...pynumero.interfaces.pyomo_grey_box_nlp.rst | 0 .../pynumero.interfaces.pyomo_nlp.rst | 0 .../pynumero/pynumero.interfaces.rst | 0 .../pynumero/pynumero.linalg.base.rst | 0 .../pynumero/pynumero.linalg.ma27.rst | 0 .../pynumero/pynumero.linalg.ma57.rst | 0 .../pynumero/pynumero.linalg.mumps.rst | 0 .../pynumero/pynumero.linalg.rst | 0 .../pynumero/pynumero.linalg.scipy.rst | 0 .../pynumero/pynumero.sparse.block_vector.rst | 0 .../pynumero/pynumero.sparse.rst | 0 .../tutorial.block_vectors_and_matrices.rst | 0 .../tutorial.linear_solver_interfaces.rst | 0 .../pynumero/tutorial.mpi_blocks.rst | 0 .../pynumero/tutorial.nlp_interfaces.rst | 0 .../pynumero/tutorial.rst | 0 .../pyros.rst | 0 .../trustregion.rst | 0 .../z3_interface.rst} | 0 117 files changed, 16 insertions(+), 65 deletions(-) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/alternative_solutions.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/communities_8pp.png (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/communities_decode_1.png (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/community.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/doe/CCSI-license.txt (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/doe/FIM_sensitivity.png (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/doe/doe.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/doe/flowchart.png (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/doe/grid-1.png (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/doe/reactor.png (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/doe/uml.png (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/iis.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/incidence/api.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/incidence/config.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/incidence/connected.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/incidence/dulmage_mendelsohn.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/incidence/incidence.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/incidence/index.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/incidence/interface.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/incidence/matching.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/incidence/overview.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/incidence/scc_solver.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/incidence/triangularize.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/incidence/tutorial.bt.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/incidence/tutorial.btsolve.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/incidence/tutorial.dm.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/incidence/tutorial.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/mpc/api.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/mpc/conversion.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/mpc/data.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/mpc/examples.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/mpc/faq.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/mpc/index.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/mpc/interface.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/mpc/modeling.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/mpc/overview.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/parmest/api.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/parmest/boxplot.png (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/parmest/covariance.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/parmest/datarec.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/parmest/driver.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/parmest/examples.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/parmest/graphics.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/parmest/index.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/parmest/installation.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/parmest/overview.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/parmest/pairwise_plot_CI.png (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/parmest/pairwise_plot_LR.png (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/parmest/parallel.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/parmest/scencreate.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => analysis}/sensitivity_toolbox.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages/index.rst => contrib_index.rst} (100%) delete mode 100644 doc/OnlineDocs/user_explanations/external_tutorials.rst rename doc/OnlineDocs/user_explanations/{pyomo_modeling_components => modeling}/Constraints.rst (100%) rename doc/OnlineDocs/user_explanations/{pyomo_modeling_components => modeling}/Expressions.rst (100%) rename doc/OnlineDocs/user_explanations/{pyomo_modeling_components => modeling}/Objectives.rst (100%) rename doc/OnlineDocs/user_explanations/{pyomo_modeling_components => modeling}/Parameters.rst (100%) rename doc/OnlineDocs/user_explanations/{pyomo_modeling_components => modeling}/Sets.rst (100%) rename doc/OnlineDocs/user_explanations/{pyomo_modeling_components => modeling}/Suffixes.rst (100%) rename doc/OnlineDocs/user_explanations/{pyomo_modeling_components => modeling}/Variables.rst (100%) rename doc/OnlineDocs/user_explanations/{modeling_extensions => modeling}/dae.rst (100%) rename doc/OnlineDocs/user_explanations/{modeling_extensions => modeling}/gdp/concepts.rst (100%) rename doc/OnlineDocs/user_explanations/{modeling_extensions => modeling}/gdp/index.rst (100%) rename doc/OnlineDocs/user_explanations/{modeling_extensions => modeling}/gdp/modeling.rst (100%) rename doc/OnlineDocs/user_explanations/{modeling_extensions => modeling}/gdp/solving.rst (100%) rename doc/OnlineDocs/user_explanations/{pyomo_modeling_components => modeling}/index.rst (100%) rename doc/OnlineDocs/user_explanations/{modeling_extensions => modeling}/mpec.rst (100%) rename doc/OnlineDocs/user_explanations/{modeling_extensions => modeling}/network.rst (100%) rename doc/OnlineDocs/user_explanations/{modeling_extensions => modeling}/reduce_points_demo.png (100%) rename doc/OnlineDocs/user_explanations/{ => modeling}/sos_constraints.rst (100%) rename doc/OnlineDocs/user_explanations/{ => modeling}/units_container.rst (100%) delete mode 100644 doc/OnlineDocs/user_explanations/modeling_extensions/__init__.py delete mode 100644 doc/OnlineDocs/user_explanations/modeling_extensions/bilevel.rst delete mode 100644 doc/OnlineDocs/user_explanations/modeling_extensions/index.rst delete mode 100644 doc/OnlineDocs/user_explanations/modeling_extensions/stochastic_programming.rst rename doc/OnlineDocs/user_explanations/{ => modeling_utilities}/flattener/index.rst (100%) rename doc/OnlineDocs/user_explanations/{ => modeling_utilities}/flattener/motivation.rst (100%) rename doc/OnlineDocs/user_explanations/{ => modeling_utilities}/flattener/reference.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => modeling_utilities}/latex_printer.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => modeling_utilities}/preprocessing.rst (100%) rename doc/OnlineDocs/user_explanations/{ => modeling_utilities}/scaling.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/gdpopt.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/gdpopt_flowchart.png (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/mcpp.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/mindtpy.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/multistart.rst (100%) rename doc/OnlineDocs/user_explanations/{ => solvers}/persistent_solvers.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/api.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/backward_compatibility.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/index.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/installation.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.interfaces.ampl_nlp.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.interfaces.asl_nlp.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.interfaces.extended_nlp.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.interfaces.external_grey_box_model.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.interfaces.nlp.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.interfaces.projected_nlp.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.interfaces.pyomo_nlp.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.interfaces.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.linalg.base.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.linalg.ma27.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.linalg.ma57.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.linalg.mumps.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.linalg.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.linalg.scipy.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.sparse.block_vector.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/pynumero.sparse.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/tutorial.block_vectors_and_matrices.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/tutorial.linear_solver_interfaces.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/tutorial.mpi_blocks.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/tutorial.nlp_interfaces.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pynumero/tutorial.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/pyros.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages => solvers}/trustregion.rst (100%) rename doc/OnlineDocs/user_explanations/{contributed_packages/satsolver.rst => solvers/z3_interface.rst} (100%) diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index c8838e78d47..94423ce1518 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -90,6 +90,22 @@ Ask a question on StackOverflow using the `#pyomo` tag: * https://stackoverflow.com/questions/ask?tags=pyomo +Additional Pyomo tutorials and examples can be found at the following links: + +* `Pyomo — Optimization Modeling in Python + `_ ([PyomoBookIII]_) + +* `Pyomo Workshop Slides and Exercises + `_ + +* `Prof. Jeffrey Kantor's Pyomo Cookbook + `_ + +* The `companion notebooks `_ + for *Hands-On Mathematical Optimization with Python* + +* `Pyomo Gallery `_ + Contributing to Pyomo --------------------- diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/alternative_solutions.rst b/doc/OnlineDocs/user_explanations/analysis/alternative_solutions.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/alternative_solutions.rst rename to doc/OnlineDocs/user_explanations/analysis/alternative_solutions.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/communities_8pp.png b/doc/OnlineDocs/user_explanations/analysis/communities_8pp.png similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/communities_8pp.png rename to doc/OnlineDocs/user_explanations/analysis/communities_8pp.png diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/communities_decode_1.png b/doc/OnlineDocs/user_explanations/analysis/communities_decode_1.png similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/communities_decode_1.png rename to doc/OnlineDocs/user_explanations/analysis/communities_decode_1.png diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/community.rst b/doc/OnlineDocs/user_explanations/analysis/community.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/community.rst rename to doc/OnlineDocs/user_explanations/analysis/community.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/doe/CCSI-license.txt b/doc/OnlineDocs/user_explanations/analysis/doe/CCSI-license.txt similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/doe/CCSI-license.txt rename to doc/OnlineDocs/user_explanations/analysis/doe/CCSI-license.txt diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/doe/FIM_sensitivity.png b/doc/OnlineDocs/user_explanations/analysis/doe/FIM_sensitivity.png similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/doe/FIM_sensitivity.png rename to doc/OnlineDocs/user_explanations/analysis/doe/FIM_sensitivity.png diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/doe/doe.rst b/doc/OnlineDocs/user_explanations/analysis/doe/doe.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/doe/doe.rst rename to doc/OnlineDocs/user_explanations/analysis/doe/doe.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/doe/flowchart.png b/doc/OnlineDocs/user_explanations/analysis/doe/flowchart.png similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/doe/flowchart.png rename to doc/OnlineDocs/user_explanations/analysis/doe/flowchart.png diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/doe/grid-1.png b/doc/OnlineDocs/user_explanations/analysis/doe/grid-1.png similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/doe/grid-1.png rename to doc/OnlineDocs/user_explanations/analysis/doe/grid-1.png diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/doe/reactor.png b/doc/OnlineDocs/user_explanations/analysis/doe/reactor.png similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/doe/reactor.png rename to doc/OnlineDocs/user_explanations/analysis/doe/reactor.png diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/doe/uml.png b/doc/OnlineDocs/user_explanations/analysis/doe/uml.png similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/doe/uml.png rename to doc/OnlineDocs/user_explanations/analysis/doe/uml.png diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/iis.rst b/doc/OnlineDocs/user_explanations/analysis/iis.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/iis.rst rename to doc/OnlineDocs/user_explanations/analysis/iis.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/incidence/api.rst b/doc/OnlineDocs/user_explanations/analysis/incidence/api.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/incidence/api.rst rename to doc/OnlineDocs/user_explanations/analysis/incidence/api.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/incidence/config.rst b/doc/OnlineDocs/user_explanations/analysis/incidence/config.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/incidence/config.rst rename to doc/OnlineDocs/user_explanations/analysis/incidence/config.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/incidence/connected.rst b/doc/OnlineDocs/user_explanations/analysis/incidence/connected.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/incidence/connected.rst rename to doc/OnlineDocs/user_explanations/analysis/incidence/connected.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/incidence/dulmage_mendelsohn.rst b/doc/OnlineDocs/user_explanations/analysis/incidence/dulmage_mendelsohn.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/incidence/dulmage_mendelsohn.rst rename to doc/OnlineDocs/user_explanations/analysis/incidence/dulmage_mendelsohn.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/incidence/incidence.rst b/doc/OnlineDocs/user_explanations/analysis/incidence/incidence.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/incidence/incidence.rst rename to doc/OnlineDocs/user_explanations/analysis/incidence/incidence.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/incidence/index.rst b/doc/OnlineDocs/user_explanations/analysis/incidence/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/incidence/index.rst rename to doc/OnlineDocs/user_explanations/analysis/incidence/index.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/incidence/interface.rst b/doc/OnlineDocs/user_explanations/analysis/incidence/interface.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/incidence/interface.rst rename to doc/OnlineDocs/user_explanations/analysis/incidence/interface.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/incidence/matching.rst b/doc/OnlineDocs/user_explanations/analysis/incidence/matching.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/incidence/matching.rst rename to doc/OnlineDocs/user_explanations/analysis/incidence/matching.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/incidence/overview.rst b/doc/OnlineDocs/user_explanations/analysis/incidence/overview.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/incidence/overview.rst rename to doc/OnlineDocs/user_explanations/analysis/incidence/overview.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/incidence/scc_solver.rst b/doc/OnlineDocs/user_explanations/analysis/incidence/scc_solver.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/incidence/scc_solver.rst rename to doc/OnlineDocs/user_explanations/analysis/incidence/scc_solver.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/incidence/triangularize.rst b/doc/OnlineDocs/user_explanations/analysis/incidence/triangularize.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/incidence/triangularize.rst rename to doc/OnlineDocs/user_explanations/analysis/incidence/triangularize.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.bt.rst b/doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.bt.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.bt.rst rename to doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.bt.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.btsolve.rst b/doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.btsolve.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.btsolve.rst rename to doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.btsolve.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.dm.rst b/doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.dm.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.dm.rst rename to doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.dm.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.rst b/doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/incidence/tutorial.rst rename to doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/mpc/api.rst b/doc/OnlineDocs/user_explanations/analysis/mpc/api.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/mpc/api.rst rename to doc/OnlineDocs/user_explanations/analysis/mpc/api.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/mpc/conversion.rst b/doc/OnlineDocs/user_explanations/analysis/mpc/conversion.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/mpc/conversion.rst rename to doc/OnlineDocs/user_explanations/analysis/mpc/conversion.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/mpc/data.rst b/doc/OnlineDocs/user_explanations/analysis/mpc/data.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/mpc/data.rst rename to doc/OnlineDocs/user_explanations/analysis/mpc/data.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/mpc/examples.rst b/doc/OnlineDocs/user_explanations/analysis/mpc/examples.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/mpc/examples.rst rename to doc/OnlineDocs/user_explanations/analysis/mpc/examples.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/mpc/faq.rst b/doc/OnlineDocs/user_explanations/analysis/mpc/faq.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/mpc/faq.rst rename to doc/OnlineDocs/user_explanations/analysis/mpc/faq.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/mpc/index.rst b/doc/OnlineDocs/user_explanations/analysis/mpc/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/mpc/index.rst rename to doc/OnlineDocs/user_explanations/analysis/mpc/index.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/mpc/interface.rst b/doc/OnlineDocs/user_explanations/analysis/mpc/interface.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/mpc/interface.rst rename to doc/OnlineDocs/user_explanations/analysis/mpc/interface.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/mpc/modeling.rst b/doc/OnlineDocs/user_explanations/analysis/mpc/modeling.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/mpc/modeling.rst rename to doc/OnlineDocs/user_explanations/analysis/mpc/modeling.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/mpc/overview.rst b/doc/OnlineDocs/user_explanations/analysis/mpc/overview.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/mpc/overview.rst rename to doc/OnlineDocs/user_explanations/analysis/mpc/overview.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/parmest/api.rst b/doc/OnlineDocs/user_explanations/analysis/parmest/api.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/parmest/api.rst rename to doc/OnlineDocs/user_explanations/analysis/parmest/api.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/parmest/boxplot.png b/doc/OnlineDocs/user_explanations/analysis/parmest/boxplot.png similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/parmest/boxplot.png rename to doc/OnlineDocs/user_explanations/analysis/parmest/boxplot.png diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/parmest/covariance.rst b/doc/OnlineDocs/user_explanations/analysis/parmest/covariance.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/parmest/covariance.rst rename to doc/OnlineDocs/user_explanations/analysis/parmest/covariance.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/parmest/datarec.rst b/doc/OnlineDocs/user_explanations/analysis/parmest/datarec.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/parmest/datarec.rst rename to doc/OnlineDocs/user_explanations/analysis/parmest/datarec.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/parmest/driver.rst b/doc/OnlineDocs/user_explanations/analysis/parmest/driver.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/parmest/driver.rst rename to doc/OnlineDocs/user_explanations/analysis/parmest/driver.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/parmest/examples.rst b/doc/OnlineDocs/user_explanations/analysis/parmest/examples.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/parmest/examples.rst rename to doc/OnlineDocs/user_explanations/analysis/parmest/examples.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/parmest/graphics.rst b/doc/OnlineDocs/user_explanations/analysis/parmest/graphics.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/parmest/graphics.rst rename to doc/OnlineDocs/user_explanations/analysis/parmest/graphics.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/parmest/index.rst b/doc/OnlineDocs/user_explanations/analysis/parmest/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/parmest/index.rst rename to doc/OnlineDocs/user_explanations/analysis/parmest/index.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/parmest/installation.rst b/doc/OnlineDocs/user_explanations/analysis/parmest/installation.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/parmest/installation.rst rename to doc/OnlineDocs/user_explanations/analysis/parmest/installation.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/parmest/overview.rst b/doc/OnlineDocs/user_explanations/analysis/parmest/overview.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/parmest/overview.rst rename to doc/OnlineDocs/user_explanations/analysis/parmest/overview.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/parmest/pairwise_plot_CI.png b/doc/OnlineDocs/user_explanations/analysis/parmest/pairwise_plot_CI.png similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/parmest/pairwise_plot_CI.png rename to doc/OnlineDocs/user_explanations/analysis/parmest/pairwise_plot_CI.png diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/parmest/pairwise_plot_LR.png b/doc/OnlineDocs/user_explanations/analysis/parmest/pairwise_plot_LR.png similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/parmest/pairwise_plot_LR.png rename to doc/OnlineDocs/user_explanations/analysis/parmest/pairwise_plot_LR.png diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/parmest/parallel.rst b/doc/OnlineDocs/user_explanations/analysis/parmest/parallel.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/parmest/parallel.rst rename to doc/OnlineDocs/user_explanations/analysis/parmest/parallel.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/parmest/scencreate.rst b/doc/OnlineDocs/user_explanations/analysis/parmest/scencreate.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/parmest/scencreate.rst rename to doc/OnlineDocs/user_explanations/analysis/parmest/scencreate.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/sensitivity_toolbox.rst b/doc/OnlineDocs/user_explanations/analysis/sensitivity_toolbox.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/sensitivity_toolbox.rst rename to doc/OnlineDocs/user_explanations/analysis/sensitivity_toolbox.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/index.rst b/doc/OnlineDocs/user_explanations/contrib_index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/index.rst rename to doc/OnlineDocs/user_explanations/contrib_index.rst diff --git a/doc/OnlineDocs/user_explanations/external_tutorials.rst b/doc/OnlineDocs/user_explanations/external_tutorials.rst deleted file mode 100644 index a18f9d77d42..00000000000 --- a/doc/OnlineDocs/user_explanations/external_tutorials.rst +++ /dev/null @@ -1,20 +0,0 @@ -Pyomo Tutorial Examples -======================= - -Additional Pyomo tutorials and examples can be found at the following links: - -* `Pyomo — Optimization Modeling in Python - `_ ([PyomoBookIII]_) - -* `Pyomo Workshop Slides and Exercises - `_ - -* `Prof. Jeffrey Kantor's Pyomo Cookbook - `_ - -* The `companion notebooks `_ - for *Hands-On Mathematical Optimization with Python* - -* `Pyomo Gallery `_ - - diff --git a/doc/OnlineDocs/user_explanations/pyomo_modeling_components/Constraints.rst b/doc/OnlineDocs/user_explanations/modeling/Constraints.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/pyomo_modeling_components/Constraints.rst rename to doc/OnlineDocs/user_explanations/modeling/Constraints.rst diff --git a/doc/OnlineDocs/user_explanations/pyomo_modeling_components/Expressions.rst b/doc/OnlineDocs/user_explanations/modeling/Expressions.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/pyomo_modeling_components/Expressions.rst rename to doc/OnlineDocs/user_explanations/modeling/Expressions.rst diff --git a/doc/OnlineDocs/user_explanations/pyomo_modeling_components/Objectives.rst b/doc/OnlineDocs/user_explanations/modeling/Objectives.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/pyomo_modeling_components/Objectives.rst rename to doc/OnlineDocs/user_explanations/modeling/Objectives.rst diff --git a/doc/OnlineDocs/user_explanations/pyomo_modeling_components/Parameters.rst b/doc/OnlineDocs/user_explanations/modeling/Parameters.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/pyomo_modeling_components/Parameters.rst rename to doc/OnlineDocs/user_explanations/modeling/Parameters.rst diff --git a/doc/OnlineDocs/user_explanations/pyomo_modeling_components/Sets.rst b/doc/OnlineDocs/user_explanations/modeling/Sets.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/pyomo_modeling_components/Sets.rst rename to doc/OnlineDocs/user_explanations/modeling/Sets.rst diff --git a/doc/OnlineDocs/user_explanations/pyomo_modeling_components/Suffixes.rst b/doc/OnlineDocs/user_explanations/modeling/Suffixes.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/pyomo_modeling_components/Suffixes.rst rename to doc/OnlineDocs/user_explanations/modeling/Suffixes.rst diff --git a/doc/OnlineDocs/user_explanations/pyomo_modeling_components/Variables.rst b/doc/OnlineDocs/user_explanations/modeling/Variables.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/pyomo_modeling_components/Variables.rst rename to doc/OnlineDocs/user_explanations/modeling/Variables.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_extensions/dae.rst b/doc/OnlineDocs/user_explanations/modeling/dae.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling_extensions/dae.rst rename to doc/OnlineDocs/user_explanations/modeling/dae.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_extensions/gdp/concepts.rst b/doc/OnlineDocs/user_explanations/modeling/gdp/concepts.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling_extensions/gdp/concepts.rst rename to doc/OnlineDocs/user_explanations/modeling/gdp/concepts.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_extensions/gdp/index.rst b/doc/OnlineDocs/user_explanations/modeling/gdp/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling_extensions/gdp/index.rst rename to doc/OnlineDocs/user_explanations/modeling/gdp/index.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_extensions/gdp/modeling.rst b/doc/OnlineDocs/user_explanations/modeling/gdp/modeling.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling_extensions/gdp/modeling.rst rename to doc/OnlineDocs/user_explanations/modeling/gdp/modeling.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_extensions/gdp/solving.rst b/doc/OnlineDocs/user_explanations/modeling/gdp/solving.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling_extensions/gdp/solving.rst rename to doc/OnlineDocs/user_explanations/modeling/gdp/solving.rst diff --git a/doc/OnlineDocs/user_explanations/pyomo_modeling_components/index.rst b/doc/OnlineDocs/user_explanations/modeling/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/pyomo_modeling_components/index.rst rename to doc/OnlineDocs/user_explanations/modeling/index.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_extensions/mpec.rst b/doc/OnlineDocs/user_explanations/modeling/mpec.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling_extensions/mpec.rst rename to doc/OnlineDocs/user_explanations/modeling/mpec.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_extensions/network.rst b/doc/OnlineDocs/user_explanations/modeling/network.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling_extensions/network.rst rename to doc/OnlineDocs/user_explanations/modeling/network.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_extensions/reduce_points_demo.png b/doc/OnlineDocs/user_explanations/modeling/reduce_points_demo.png similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling_extensions/reduce_points_demo.png rename to doc/OnlineDocs/user_explanations/modeling/reduce_points_demo.png diff --git a/doc/OnlineDocs/user_explanations/sos_constraints.rst b/doc/OnlineDocs/user_explanations/modeling/sos_constraints.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/sos_constraints.rst rename to doc/OnlineDocs/user_explanations/modeling/sos_constraints.rst diff --git a/doc/OnlineDocs/user_explanations/units_container.rst b/doc/OnlineDocs/user_explanations/modeling/units_container.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/units_container.rst rename to doc/OnlineDocs/user_explanations/modeling/units_container.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_extensions/__init__.py b/doc/OnlineDocs/user_explanations/modeling_extensions/__init__.py deleted file mode 100644 index a4a626013c4..00000000000 --- a/doc/OnlineDocs/user_explanations/modeling_extensions/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/doc/OnlineDocs/user_explanations/modeling_extensions/bilevel.rst b/doc/OnlineDocs/user_explanations/modeling_extensions/bilevel.rst deleted file mode 100644 index 5e9ee9b0a7c..00000000000 --- a/doc/OnlineDocs/user_explanations/modeling_extensions/bilevel.rst +++ /dev/null @@ -1,6 +0,0 @@ -Bilevel Programming -=================== - -``pyomo.bilevel`` provides extensions supporting modeling of multi-level -optimization problems. - diff --git a/doc/OnlineDocs/user_explanations/modeling_extensions/index.rst b/doc/OnlineDocs/user_explanations/modeling_extensions/index.rst deleted file mode 100644 index 3a3370e510a..00000000000 --- a/doc/OnlineDocs/user_explanations/modeling_extensions/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -Modeling Extensions -=================== - -.. toctree:: - :maxdepth: 1 - - bilevel.rst - dae.rst - gdp/index.rst - mpec.rst - stochastic_programming.rst - network.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_extensions/stochastic_programming.rst b/doc/OnlineDocs/user_explanations/modeling_extensions/stochastic_programming.rst deleted file mode 100644 index 227a8d9aa8d..00000000000 --- a/doc/OnlineDocs/user_explanations/modeling_extensions/stochastic_programming.rst +++ /dev/null @@ -1,17 +0,0 @@ -Stochastic Programming in Pyomo -=============================== - -There are two extensions for modeling and solving Stochastic Programs in -Pyomo. Both are currently distributed as independent Python packages. -PySP was the original extension (and up through Pyomo 5.7.3 was -distributed as part of Pyomo). You can find the documentation here: - - `https://pysp.readthedocs.io `_ - -In 2020, the PySP developers released the mpi-sppy package, which -reimplemented much of the functionality from PySP in a new scalable -framework built on top of MPI and the mpi4py package. Future -development of stochastic programming capabilities is occurring in -mpi-sppy. The documentation is available here: - - `https://mpi-sppy.readthedocs.io `_ diff --git a/doc/OnlineDocs/user_explanations/flattener/index.rst b/doc/OnlineDocs/user_explanations/modeling_utilities/flattener/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/flattener/index.rst rename to doc/OnlineDocs/user_explanations/modeling_utilities/flattener/index.rst diff --git a/doc/OnlineDocs/user_explanations/flattener/motivation.rst b/doc/OnlineDocs/user_explanations/modeling_utilities/flattener/motivation.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/flattener/motivation.rst rename to doc/OnlineDocs/user_explanations/modeling_utilities/flattener/motivation.rst diff --git a/doc/OnlineDocs/user_explanations/flattener/reference.rst b/doc/OnlineDocs/user_explanations/modeling_utilities/flattener/reference.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/flattener/reference.rst rename to doc/OnlineDocs/user_explanations/modeling_utilities/flattener/reference.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/latex_printer.rst b/doc/OnlineDocs/user_explanations/modeling_utilities/latex_printer.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/latex_printer.rst rename to doc/OnlineDocs/user_explanations/modeling_utilities/latex_printer.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/preprocessing.rst b/doc/OnlineDocs/user_explanations/modeling_utilities/preprocessing.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/preprocessing.rst rename to doc/OnlineDocs/user_explanations/modeling_utilities/preprocessing.rst diff --git a/doc/OnlineDocs/user_explanations/scaling.rst b/doc/OnlineDocs/user_explanations/modeling_utilities/scaling.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/scaling.rst rename to doc/OnlineDocs/user_explanations/modeling_utilities/scaling.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/gdpopt.rst b/doc/OnlineDocs/user_explanations/solvers/gdpopt.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/gdpopt.rst rename to doc/OnlineDocs/user_explanations/solvers/gdpopt.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/gdpopt_flowchart.png b/doc/OnlineDocs/user_explanations/solvers/gdpopt_flowchart.png similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/gdpopt_flowchart.png rename to doc/OnlineDocs/user_explanations/solvers/gdpopt_flowchart.png diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/mcpp.rst b/doc/OnlineDocs/user_explanations/solvers/mcpp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/mcpp.rst rename to doc/OnlineDocs/user_explanations/solvers/mcpp.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/mindtpy.rst b/doc/OnlineDocs/user_explanations/solvers/mindtpy.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/mindtpy.rst rename to doc/OnlineDocs/user_explanations/solvers/mindtpy.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/multistart.rst b/doc/OnlineDocs/user_explanations/solvers/multistart.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/multistart.rst rename to doc/OnlineDocs/user_explanations/solvers/multistart.rst diff --git a/doc/OnlineDocs/user_explanations/persistent_solvers.rst b/doc/OnlineDocs/user_explanations/solvers/persistent_solvers.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/persistent_solvers.rst rename to doc/OnlineDocs/user_explanations/solvers/persistent_solvers.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/api.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/api.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/api.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/api.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/backward_compatibility.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/backward_compatibility.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/backward_compatibility.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/backward_compatibility.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/index.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/index.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/index.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/installation.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/installation.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/installation.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/installation.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.ampl_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.ampl_nlp.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.asl_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.asl_nlp.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.extended_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.extended_nlp.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.external_grey_box_model.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.external_grey_box_model.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.nlp.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.nlp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.nlp.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.nlp.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.projected_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.projected_nlp.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.pyomo_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.pyomo_nlp.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.interfaces.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.base.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.base.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.base.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.base.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.ma27.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.ma27.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.ma27.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.ma27.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.ma57.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.ma57.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.ma57.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.ma57.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.mumps.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.mumps.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.mumps.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.mumps.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.scipy.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.scipy.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.linalg.scipy.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.scipy.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.sparse.block_vector.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.sparse.block_vector.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.sparse.block_vector.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.sparse.block_vector.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.sparse.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.sparse.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/pynumero.sparse.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.sparse.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.block_vectors_and_matrices.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.block_vectors_and_matrices.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.linear_solver_interfaces.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.linear_solver_interfaces.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.mpi_blocks.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.mpi_blocks.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.mpi_blocks.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.mpi_blocks.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.nlp_interfaces.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.nlp_interfaces.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.nlp_interfaces.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.nlp_interfaces.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pynumero/tutorial.rst rename to doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/pyros.rst b/doc/OnlineDocs/user_explanations/solvers/pyros.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/pyros.rst rename to doc/OnlineDocs/user_explanations/solvers/pyros.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/trustregion.rst b/doc/OnlineDocs/user_explanations/solvers/trustregion.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/trustregion.rst rename to doc/OnlineDocs/user_explanations/solvers/trustregion.rst diff --git a/doc/OnlineDocs/user_explanations/contributed_packages/satsolver.rst b/doc/OnlineDocs/user_explanations/solvers/z3_interface.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contributed_packages/satsolver.rst rename to doc/OnlineDocs/user_explanations/solvers/z3_interface.rst From ac48ed5a05074f2fc485658ff950574ff24eb6fa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 23 Aug 2024 12:30:46 -0600 Subject: [PATCH 2387/3044] resolve table formatting --- doc/OnlineDocs/_static/theme_overrides.css | 4 ++++ doc/OnlineDocs/index.rst | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/_static/theme_overrides.css b/doc/OnlineDocs/_static/theme_overrides.css index 43d48693e03..12b8bae6d9a 100644 --- a/doc/OnlineDocs/_static/theme_overrides.css +++ b/doc/OnlineDocs/_static/theme_overrides.css @@ -31,6 +31,10 @@ dl.py.method dt em span.n { } } +.rst-content table.docutils td { + vertical-align: top; +} + /* Remove space after tables in definition lists (e.g., for function "Parameters" lists*/ .rst-content dl div.wy-table-responsive { diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index 94423ce1518..7a3ca3ddf8e 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -10,8 +10,8 @@ with a diverse set of optimization capabilities. .. list-table:: + :width: 100% :class: index-table - :widths: 50% 50% * - Getting Started | :doc:`Installation ` From 7f18f378070b33e63bef970e5086b7b779884b8d Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Fri, 23 Aug 2024 12:32:09 -0600 Subject: [PATCH 2388/3044] Reorganizing docs --- doc/OnlineDocs/{ => how_to_guide}/contribution_guide.rst | 0 .../{user_guide => how_to_guide}/working_models.rst | 0 doc/OnlineDocs/{user_guide => reference_guide}/errors.rst | 0 .../{developer_guide => reference_guide}/future.rst | 0 doc/OnlineDocs/reference_guide/index.rst | 8 ++++++++ 5 files changed, 8 insertions(+) rename doc/OnlineDocs/{ => how_to_guide}/contribution_guide.rst (100%) rename doc/OnlineDocs/{user_guide => how_to_guide}/working_models.rst (100%) rename doc/OnlineDocs/{user_guide => reference_guide}/errors.rst (100%) rename doc/OnlineDocs/{developer_guide => reference_guide}/future.rst (100%) diff --git a/doc/OnlineDocs/contribution_guide.rst b/doc/OnlineDocs/how_to_guide/contribution_guide.rst similarity index 100% rename from doc/OnlineDocs/contribution_guide.rst rename to doc/OnlineDocs/how_to_guide/contribution_guide.rst diff --git a/doc/OnlineDocs/user_guide/working_models.rst b/doc/OnlineDocs/how_to_guide/working_models.rst similarity index 100% rename from doc/OnlineDocs/user_guide/working_models.rst rename to doc/OnlineDocs/how_to_guide/working_models.rst diff --git a/doc/OnlineDocs/user_guide/errors.rst b/doc/OnlineDocs/reference_guide/errors.rst similarity index 100% rename from doc/OnlineDocs/user_guide/errors.rst rename to doc/OnlineDocs/reference_guide/errors.rst diff --git a/doc/OnlineDocs/developer_guide/future.rst b/doc/OnlineDocs/reference_guide/future.rst similarity index 100% rename from doc/OnlineDocs/developer_guide/future.rst rename to doc/OnlineDocs/reference_guide/future.rst diff --git a/doc/OnlineDocs/reference_guide/index.rst b/doc/OnlineDocs/reference_guide/index.rst index 3092b706b14..5007ed4e193 100644 --- a/doc/OnlineDocs/reference_guide/index.rst +++ b/doc/OnlineDocs/reference_guide/index.rst @@ -1,6 +1,14 @@ Reference Guide =============== +.. toctree:: + :maxdepth: 2 + + library_reference/index.rst + errors.rst + Preview capabilities through ``pyomo.__future__`` + + Bibliography ------------ From f35e4a53942c8b6e822332a9436f21ea0de51e61 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Aug 2024 12:37:01 -0600 Subject: [PATCH 2389/3044] Moving developer docs to developer utilities folder in user_explanations' --- .../developer_utilities}/config.rst | 0 .../developer_utilities}/deprecation.rst | 0 .../experimental}/solvers.rst | 0 .../pyomo_philosophy}/expressions/design.rst | 0 .../pyomo_philosophy}/expressions/index.rst | 0 .../pyomo_philosophy}/expressions/managing.rst | 0 .../pyomo_philosophy}/expressions/overview.rst | 0 .../pyomo_philosophy}/expressions/performance.rst | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename doc/OnlineDocs/{developer_guide => user_explanations/developer_utilities}/config.rst (100%) rename doc/OnlineDocs/{developer_guide => user_explanations/developer_utilities}/deprecation.rst (100%) rename doc/OnlineDocs/{developer_guide => user_explanations/experimental}/solvers.rst (100%) rename doc/OnlineDocs/{developer_guide => user_explanations/pyomo_philosophy}/expressions/design.rst (100%) rename doc/OnlineDocs/{developer_guide => user_explanations/pyomo_philosophy}/expressions/index.rst (100%) rename doc/OnlineDocs/{developer_guide => user_explanations/pyomo_philosophy}/expressions/managing.rst (100%) rename doc/OnlineDocs/{developer_guide => user_explanations/pyomo_philosophy}/expressions/overview.rst (100%) rename doc/OnlineDocs/{developer_guide => user_explanations/pyomo_philosophy}/expressions/performance.rst (100%) diff --git a/doc/OnlineDocs/developer_guide/config.rst b/doc/OnlineDocs/user_explanations/developer_utilities/config.rst similarity index 100% rename from doc/OnlineDocs/developer_guide/config.rst rename to doc/OnlineDocs/user_explanations/developer_utilities/config.rst diff --git a/doc/OnlineDocs/developer_guide/deprecation.rst b/doc/OnlineDocs/user_explanations/developer_utilities/deprecation.rst similarity index 100% rename from doc/OnlineDocs/developer_guide/deprecation.rst rename to doc/OnlineDocs/user_explanations/developer_utilities/deprecation.rst diff --git a/doc/OnlineDocs/developer_guide/solvers.rst b/doc/OnlineDocs/user_explanations/experimental/solvers.rst similarity index 100% rename from doc/OnlineDocs/developer_guide/solvers.rst rename to doc/OnlineDocs/user_explanations/experimental/solvers.rst diff --git a/doc/OnlineDocs/developer_guide/expressions/design.rst b/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/design.rst similarity index 100% rename from doc/OnlineDocs/developer_guide/expressions/design.rst rename to doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/design.rst diff --git a/doc/OnlineDocs/developer_guide/expressions/index.rst b/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/index.rst similarity index 100% rename from doc/OnlineDocs/developer_guide/expressions/index.rst rename to doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/index.rst diff --git a/doc/OnlineDocs/developer_guide/expressions/managing.rst b/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/managing.rst similarity index 100% rename from doc/OnlineDocs/developer_guide/expressions/managing.rst rename to doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/managing.rst diff --git a/doc/OnlineDocs/developer_guide/expressions/overview.rst b/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/overview.rst similarity index 100% rename from doc/OnlineDocs/developer_guide/expressions/overview.rst rename to doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/overview.rst diff --git a/doc/OnlineDocs/developer_guide/expressions/performance.rst b/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/performance.rst similarity index 100% rename from doc/OnlineDocs/developer_guide/expressions/performance.rst rename to doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/performance.rst From 6a6b346288075235b5c3a2cdf65164600f831dd9 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 23 Aug 2024 12:39:24 -0600 Subject: [PATCH 2390/3044] Removing developer_guide folder --- doc/OnlineDocs/developer_guide/index.rst | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 doc/OnlineDocs/developer_guide/index.rst diff --git a/doc/OnlineDocs/developer_guide/index.rst b/doc/OnlineDocs/developer_guide/index.rst deleted file mode 100644 index 9adf9eb2648..00000000000 --- a/doc/OnlineDocs/developer_guide/index.rst +++ /dev/null @@ -1,15 +0,0 @@ -Developer Guide -=============== - -This guide describes utilities and design philosophies useful for Pyomo -developers or anyone interested in developing packages that use or -interrogate Pyomo models. - -.. toctree:: - :maxdepth: 1 - - Configuration System - Deprecation System - Expression System - Future Feature Preview - Solver Interfaces From 14f0d1651c6f21911a747c8eb4b78a7cd69a3204 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 23 Aug 2024 14:46:12 -0400 Subject: [PATCH 2391/3044] Try BARON (except 24.1.5) again for failing test --- pyomo/contrib/pyros/tests/test_grcs.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index def7e7fe031..b7c98068687 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -1747,7 +1747,8 @@ def test_coefficient_matching_robust_infeasible_proof_in_pyros(self): msg="Robust infeasible problem not identified via coefficient matching.", ) - @unittest.skipUnless(scip_license_is_valid, "SCIP solver not licensed.") + @unittest.skipIf(baron_version == (24, 1, 5), "Test known to fail for BARON 24.1.5") + @unittest.skipUnless(baron_license_is_valid, "BARON solver not licensed.") def test_coefficient_matching_nonlinear_expr(self): """ Test behavior of PyROS solver for model with @@ -1776,8 +1777,8 @@ def test_coefficient_matching_nonlinear_expr(self): pyros_solver = SolverFactory("pyros") # Define subsolvers utilized in the algorithm - local_subsolver = SolverFactory("scip") - global_subsolver = SolverFactory("scip") + local_subsolver = SolverFactory("baron") + global_subsolver = SolverFactory("baron") # Call the PyROS solver with LoggingIntercept(module="pyomo.contrib.pyros", level=logging.DEBUG) as LOG: From 84cea1c9536438514a484cbde789a8c6354cd974 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 23 Aug 2024 15:09:07 -0600 Subject: [PATCH 2392/3044] Removing changes for abandoned functionality --- pyomo/core/base/indexed_component.py | 6 +--- pyomo/core/expr/template_expr.py | 42 ---------------------------- 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/pyomo/core/base/indexed_component.py b/pyomo/core/base/indexed_component.py index c1afe19f02a..2991057f8d2 100644 --- a/pyomo/core/base/indexed_component.py +++ b/pyomo/core/base/indexed_component.py @@ -349,11 +349,7 @@ def _create_objects_for_deepcopy(self, memo, component_list): # (where the _data points back to self) and references # (where the data may be stored outside this block tree and # therefore may not be cloned) - if ( - self.is_indexed() - and not self.is_reference() - and isinstance(self._data, dict) - ): + if self.is_indexed() and not self.is_reference(): # Because we are already checking / updating the memo # for the _data dict, we can effectively "deepcopy" it # right now (almost for free!) diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index c4780a7acf2..38b5713c33b 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -1207,45 +1207,3 @@ def templatize_constraint(con): if expr.__class__ is tuple: expr = tuple_to_relational_expr(expr) return expr, indices - - -''' -class TemplatizedDataStore(MutableMapping): - def __init__(self, component, expr, indices): - self._component = component - self._expr = expr - self._indices = indices - - def __getitem__(self, item): - if self._component._data is self: - self._replace_with_dict() - return self._component._data[item] - - def __setitem__(self, item, value): - if self._component._data is self: - self._replace_with_dict() - self._component._data[item] = value - - def __delitem__(self, index): - if self._component._data is self: - self._replace_with_dict() - del self._component._data[item] - - def __iter__(self): - return iter(self._component.index_set()) - - def __len__(self): - return len(self._component.index_set()) - - def __bool__(self): - return bool(self._component.index_set()) - - def _replace_with_dict(self): - comp = self._component - comp._data = {} - rule = comp.rule - block = comp.parent_block() - with PauseGC(): - for index in comp.index_set(): - comp._setitem_when_not_present(index, rule(block, index)) -''' From efc7560aea46260774d6a21488772a00353d1781 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 06:15:11 -0600 Subject: [PATCH 2393/3044] Restore logic to generate SPY files --- doc/OnlineDocs/conf.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 630dbfcd030..5d45ae55a8b 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -36,6 +36,19 @@ # top-level pyomo source directory sys.path.insert(0, os.path.abspath('../..')) +# -- Rebuild SPY files ---------------------------------------------------- +sys.path.insert(0, os.path.abspath('src')) +try: + print("Regenerating SPY files...") + from strip_examples import generate_spy_files + + generate_spy_files(os.path.abspath('src')) + generate_spy_files( + os.path.abspath(os.path.join('reference_guide', 'library_reference', 'kernel', 'examples')) + ) +finally: + sys.path.pop(0) + # -- Options for intersphinx --------------------------------------------- intersphinx_mapping = { From 82069284c8ed337bdba26edafcd1b7b1cb40e09b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 06:17:35 -0600 Subject: [PATCH 2394/3044] Update doc include paths to be relative to doc root --- .../pyomo_overview/simple_examples.rst | 8 +-- .../how_to_guide/working_models.rst | 62 +++++++++---------- .../user_explanations/analysis/doe/doe.rst | 14 ++--- .../analysis/parmest/datarec.rst | 2 +- .../analysis/parmest/examples.rst | 4 +- .../analysis/parmest/parallel.rst | 2 +- .../analysis/parmest/scencreate.rst | 2 +- .../modeling/Constraints.rst | 6 +- .../modeling/Expressions.rst | 18 +++--- .../user_explanations/modeling/Sets.rst | 2 +- .../user_explanations/modeling/Variables.rst | 6 +- .../pyomo_philosophy/expressions/design.rst | 8 +-- .../pyomo_philosophy/expressions/index.rst | 2 +- .../pyomo_philosophy/expressions/managing.rst | 28 ++++----- .../pyomo_philosophy/expressions/overview.rst | 14 ++--- .../expressions/performance.rst | 20 +++--- .../solvers/pynumero/tutorial.mpi_blocks.rst | 4 +- 17 files changed, 101 insertions(+), 101 deletions(-) diff --git a/doc/OnlineDocs/getting_started/pyomo_overview/simple_examples.rst b/doc/OnlineDocs/getting_started/pyomo_overview/simple_examples.rst index 11305884c54..1741c694d68 100644 --- a/doc/OnlineDocs/getting_started/pyomo_overview/simple_examples.rst +++ b/doc/OnlineDocs/getting_started/pyomo_overview/simple_examples.rst @@ -263,7 +263,7 @@ parameters. Here is one file that provides data (in AMPL "``.dat``" format). >>> # Create an instance to verify that the rules fire correctly >>> inst = model.create_instance('src/scripting/abstract1.dat') -.. literalinclude:: ../src/scripting/abstract1.dat +.. literalinclude:: /src/scripting/abstract1.dat :language: text There are multiple formats that can be used to provide data to a Pyomo @@ -327,18 +327,18 @@ the same model. To start with an illustration of general indexes, consider a slightly different Pyomo implementation of the model we just presented. -.. literalinclude:: ../src/scripting/abstract2.py +.. literalinclude:: /src/scripting/abstract2.py :language: python To get the same instantiated model, the following data file can be used. -.. literalinclude:: ../src/scripting/abstract2a.dat +.. literalinclude:: /src/scripting/abstract2a.dat :language: none However, this model can also be fed different data for problems of the same general form using meaningful indexes. -.. literalinclude:: ../src/scripting/abstract2.dat +.. literalinclude:: /src/scripting/abstract2.dat :language: none diff --git a/doc/OnlineDocs/how_to_guide/working_models.rst b/doc/OnlineDocs/how_to_guide/working_models.rst index dbd7aa383e3..6c7327ce8d7 100644 --- a/doc/OnlineDocs/how_to_guide/working_models.rst +++ b/doc/OnlineDocs/how_to_guide/working_models.rst @@ -58,7 +58,7 @@ computer to solve the problem or even to iterate over solutions. This example is provided just to illustrate some elementary aspects of scripting. -.. literalinclude:: src/scripting/iterative1.spy +.. literalinclude:: /src/scripting/iterative1.spy :language: python Let us now analyze this script. The first line is a comment that happens @@ -66,7 +66,7 @@ to give the name of the file. This is followed by two lines that import symbols for Pyomo. The pyomo namespace is imported as ``pyo``. Therefore, ``pyo.`` must precede each use of a Pyomo name. -.. literalinclude:: src/scripting/iterative1_Import_symbols_for_pyomo.spy +.. literalinclude:: /src/scripting/iterative1_Import_symbols_for_pyomo.spy :language: python An object to perform optimization is created by calling @@ -74,7 +74,7 @@ An object to perform optimization is created by calling argument would be ``'gurobi'`` if, e.g., Gurobi was desired instead of glpk: -.. literalinclude:: src/scripting/iterative1_Call_SolverFactory_with_argument.spy +.. literalinclude:: /src/scripting/iterative1_Call_SolverFactory_with_argument.spy :language: python The next lines after a comment create a model. For our discussion here, @@ -86,13 +86,13 @@ to keep it simple. Constraints could be present in the base model. Even though it is an abstract model, the base model is fully specified by these commands because it requires no external data: -.. literalinclude:: src/scripting/iterative1_Create_base_model.spy +.. literalinclude:: /src/scripting/iterative1_Create_base_model.spy :language: python The next line is not part of the base model specification. It creates an empty constraint list that the script will use to add constraints. -.. literalinclude:: src/scripting/iterative1_Create_empty_constraint_list.spy +.. literalinclude:: /src/scripting/iterative1_Create_empty_constraint_list.spy :language: python The next non-comment line creates the instantiated model and refers to @@ -103,19 +103,19 @@ the ``create`` function is called without arguments because none are needed; however, the name of a file with data commands is given as an argument in many scripts. -.. literalinclude:: src/scripting/iterative1_Create_instantiated_model.spy +.. literalinclude:: /src/scripting/iterative1_Create_instantiated_model.spy :language: python The next line invokes the solver and refers to the object contain results with the Python variable ``results``. -.. literalinclude:: src/scripting/iterative1_Solve_and_refer_to_results.spy +.. literalinclude:: /src/scripting/iterative1_Solve_and_refer_to_results.spy :language: python The solve function loads the results into the instance, so the next line writes out the updated values. -.. literalinclude:: src/scripting/iterative1_Display_updated_value.spy +.. literalinclude:: /src/scripting/iterative1_Display_updated_value.spy :language: python The next non-comment line is a Python iteration command that will @@ -123,7 +123,7 @@ successively assign the integers from 0 to 4 to the Python variable ``i``, although that variable is not used in script. This loop is what causes the script to generate five more solutions: -.. literalinclude:: src/scripting/iterative1_Assign_integers.spy +.. literalinclude:: /src/scripting/iterative1_Assign_integers.spy :language: python An expression is built up in the Python variable named ``expr``. The @@ -135,7 +135,7 @@ zero and the expression in ``expr`` is augmented accordingly. Although Pyomo expression when it is assigned expressions involving Pyomo variable objects: -.. literalinclude:: src/scripting/iterative1_Iteratively_assign_and_test.spy +.. literalinclude:: /src/scripting/iterative1_Iteratively_assign_and_test.spy :language: python During the first iteration (when ``i`` is 0), we know that all values of @@ -159,7 +159,7 @@ function to get it. The next line adds to the constraint list called ``c`` the requirement that the expression be greater than or equal to one: -.. literalinclude:: src/scripting/iterative1_Add_expression_constraint.spy +.. literalinclude:: /src/scripting/iterative1_Add_expression_constraint.spy :language: python The proof that this precludes the last solution is left as an exerise @@ -167,7 +167,7 @@ for the reader. The final lines in the outer for loop find a solution and display it: -.. literalinclude:: src/scripting/iterative1_Find_and_display_solution.spy +.. literalinclude:: /src/scripting/iterative1_Find_and_display_solution.spy :language: python .. note:: @@ -268,14 +268,14 @@ Fixing Variables and Re-solving Instead of changing model data, scripts are often used to fix variable values. The following example illustrates this. -.. literalinclude:: src/scripting/iterative2.spy +.. literalinclude:: /src/scripting/iterative2.spy :language: python In this example, the variables are binary. The model is solved and then the value of ``model.x[2]`` is flipped to the opposite value before solving the model again. The main lines of interest are: -.. literalinclude:: src/scripting/iterative2_Flip_value_before_solve_again.spy +.. literalinclude:: /src/scripting/iterative2_Flip_value_before_solve_again.spy :language: python This could also have been accomplished by setting the upper and lower @@ -430,7 +430,7 @@ Consider the following very simple example, which is similar to the iterative example. This is a concrete model. In this example, the value of ``x[2]`` is accessed. -.. literalinclude:: src/scripting/noiteration1.py +.. literalinclude:: /src/scripting/noiteration1.py :language: python .. note:: @@ -476,7 +476,7 @@ Another way to access all of the variables (particularly if there are blocks) is as follows (this particular snippet assumes that instead of `import pyomo.environ as pyo` `from pyo.environ import *` was used): -.. literalinclude:: src/scripting/block_iter_example_compprintloop.spy +.. literalinclude:: /src/scripting/block_iter_example_compprintloop.spy :language: python .. _ParamAccess: @@ -521,21 +521,21 @@ To signal that duals are desired, declare a Suffix component with the name "dual" on the model or instance with an IMPORT or IMPORT_EXPORT direction. -.. literalinclude:: src/scripting/driveabs2_Create_dual_suffix_component.spy +.. literalinclude:: /src/scripting/driveabs2_Create_dual_suffix_component.spy :language: python See the section on Suffixes :ref:`Suffixes` for more information on Pyomo's Suffix component. After the results are obtained and loaded into an instance, duals can be accessed in the following fashion. -.. literalinclude:: src/scripting/driveabs2_Access_all_dual.spy +.. literalinclude:: /src/scripting/driveabs2_Access_all_dual.spy :language: python The following snippet will only work, of course, if there is a constraint with the name ``AxbConstraint`` that has and index, which is the string ``Film``. -.. literalinclude:: src/scripting/driveabs2_Access_one_dual.spy +.. literalinclude:: /src/scripting/driveabs2_Access_one_dual.spy :language: python Here is a complete example that relies on the file ``abstract2.py`` to @@ -544,14 +544,14 @@ data. Note that the model in ``abstract2.py`` does contain a constraint named ``AxbConstraint`` and ``abstract2.dat`` does specify an index for it named ``Film``. -.. literalinclude:: src/scripting/driveabs2.spy +.. literalinclude:: /src/scripting/driveabs2.spy :language: python Concrete models are slightly different because the model is the instance. Here is a complete example that relies on the file ``concrete1.py`` to provide the model and instantiate it. -.. literalinclude:: src/scripting/driveconc1.py +.. literalinclude:: /src/scripting/driveconc1.py :language: python Accessing Slacks @@ -568,7 +568,7 @@ After a solve, the results object has a member ``Solution.Status`` that contains the solver status. The following snippet shows an example of access via a ``print`` statement: -.. literalinclude:: src/scripting/spy4scripts_Print_solver_status.spy +.. literalinclude:: /src/scripting/spy4scripts_Print_solver_status.spy :language: python The use of the Python ``str`` function to cast the value to a be string @@ -576,12 +576,12 @@ makes it easy to test it. In particular, the value 'optimal' indicates that the solver succeeded. It is also possible to access Pyomo data that can be compared with the solver status as in the following code snippet: -.. literalinclude:: src/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_1.spy +.. literalinclude:: /src/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_1.spy :language: python Alternatively, -.. literalinclude:: src/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_2.spy +.. literalinclude:: /src/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_2.spy :language: python .. _TeeTrue: @@ -592,7 +592,7 @@ Display of Solver Output To see the output of the solver, use the option ``tee=True`` as in -.. literalinclude:: src/scripting/spy4scripts_See_solver_output.spy +.. literalinclude:: /src/scripting/spy4scripts_See_solver_output.spy :language: python This can be useful for troubleshooting solver difficulties. @@ -607,7 +607,7 @@ solver. In scripts or callbacks, the options can be attached to the solver object by adding to its options dictionary as illustrated by this snippet: -.. literalinclude:: src/scripting/spy4scripts_Add_option_to_solver.spy +.. literalinclude:: /src/scripting/spy4scripts_Add_option_to_solver.spy :language: python If multiple options are needed, then multiple dictionary entries should @@ -616,7 +616,7 @@ be added. Sometimes it is desirable to pass options as part of the call to the solve function as in this snippet: -.. literalinclude:: src/scripting/spy4scripts_Add_multiple_options_to_solver.spy +.. literalinclude:: /src/scripting/spy4scripts_Add_multiple_options_to_solver.spy :language: python The quoted string is passed directly to the solver. If multiple options @@ -644,7 +644,7 @@ situations where they are not, the SolverFactory function accepts the keyword ``executable``, which you can use to set an absolute or relative path to a solver executable. E.g., -.. literalinclude:: src/scripting/spy4scripts_Set_path_to_solver_executable.spy +.. literalinclude:: /src/scripting/spy4scripts_Set_path_to_solver_executable.spy :language: python Warm Starts @@ -654,7 +654,7 @@ Some solvers support a warm start based on current values of variables. To use this feature, set the values of variables in the instance and pass ``warmstart=True`` to the ``solve()`` method. E.g., -.. literalinclude:: src/scripting/spy4scripts_Pass_warmstart_to_solver.spy +.. literalinclude:: /src/scripting/spy4scripts_Pass_warmstart_to_solver.spy :language: python .. note:: @@ -686,7 +686,7 @@ parallel. The example can be run with the following command: mpirun -np 2 python -m mpi4py parallel.py -.. literalinclude:: src/scripting/parallel.py +.. literalinclude:: /src/scripting/parallel.py :language: python @@ -700,5 +700,5 @@ The pyomo command-line ``--tempdir`` option propagates through to the TempFileManager service. One can accomplish the same through the following few lines of code in a script: -.. literalinclude:: src/scripting/spy4scripts_Specify_temporary_directory_name.spy +.. literalinclude:: /src/scripting/spy4scripts_Specify_temporary_directory_name.spy :language: python diff --git a/doc/OnlineDocs/user_explanations/analysis/doe/doe.rst b/doc/OnlineDocs/user_explanations/analysis/doe/doe.rst index 73aa160c130..66feaa38ad0 100644 --- a/doc/OnlineDocs/user_explanations/analysis/doe/doe.rst +++ b/doc/OnlineDocs/user_explanations/analysis/doe/doe.rst @@ -163,7 +163,7 @@ Step 0: Import Pyomo and the Pyomo.DoE module and create an ``Experiment`` class >>> from pyomo.contrib.doe import DesignOfExperiments >>> import numpy as np -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py +.. literalinclude:: /../../pyomo/contrib/doe/examples/reactor_experiment.py :start-after: ======================== :end-before: End constructor definition @@ -172,7 +172,7 @@ Step 1: Define the Pyomo process model The process model for the reaction kinetics problem is shown below. We build the model without any data or discretization. -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py +.. literalinclude:: /../../pyomo/contrib/doe/examples/reactor_experiment.py :start-after: Create flexible model without data :end-before: End equation definition @@ -181,7 +181,7 @@ Step 2: Finalize the Pyomo process model Here we add data to the model and finalize the discretization. This step is required before the model can be labeled. -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py +.. literalinclude:: /../../pyomo/contrib/doe/examples/reactor_experiment.py :start-after: End equation definition :end-before: End model finalization @@ -190,7 +190,7 @@ Step 3: Label the information needed for DoE analysis We label the four important groups as defined before. -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py +.. literalinclude:: /../../pyomo/contrib/doe/examples/reactor_experiment.py :start-after: End model finalization :end-before: End model labeling @@ -199,7 +199,7 @@ Step 4: Implement the ``get_labeled_model`` method This method utilizes the previous 3 steps and is used by `Pyomo.DoE` to build the model to perform optimal experimental design. -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py +.. literalinclude:: /../../pyomo/contrib/doe/examples/reactor_experiment.py :start-after: End constructor definition :end-before: Create flexible model without data @@ -214,7 +214,7 @@ The ``compute_FIM_full_factorial`` function generates a grid over the design spa The following code executes the above problem description: -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_example.py +.. literalinclude:: /../../pyomo/contrib/doe/examples/reactor_example.py :start-after: Read in file :end-before: End sensitivity analysis @@ -230,7 +230,7 @@ Step 6: Performing an optimal experimental design In step 5, the DoE object was constructed to perform an exploratory sensitivity analysis. The same object can be used to design an optimal experiment with a single line of code. -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_example.py +.. literalinclude:: /../../pyomo/contrib/doe/examples/reactor_example.py :start-after: Begin optimal DoE :end-before: Print out a results summary diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/datarec.rst b/doc/OnlineDocs/user_explanations/analysis/parmest/datarec.rst index 2260450192c..3c6e12196f7 100644 --- a/doc/OnlineDocs/user_explanations/analysis/parmest/datarec.rst +++ b/doc/OnlineDocs/user_explanations/analysis/parmest/datarec.rst @@ -20,7 +20,7 @@ The following example from the reactor design subdirectory returns reconciled va (`ca`, `cb`, `cc`, and `cd`) and then uses those values in parameter estimation (`k1`, `k2`, and `k3`). -.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/reactor_design/datarec_example.py +.. literalinclude:: /../../pyomo/contrib/parmest/examples/reactor_design/datarec_example.py :language: python The following example returns model values from a Pyomo Expression. diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/examples.rst b/doc/OnlineDocs/user_explanations/analysis/parmest/examples.rst index a59d79dfa2b..794a01046cb 100644 --- a/doc/OnlineDocs/user_explanations/analysis/parmest/examples.rst +++ b/doc/OnlineDocs/user_explanations/analysis/parmest/examples.rst @@ -26,7 +26,7 @@ instance of the Pyomo model. Note that the model is defined to maximize `cb` and that `k1`, `k2`, and `k3` are fixed. The _main_ program is included for easy testing of the model declaration. -.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/reactor_design/reactor_design.py +.. literalinclude:: /../../pyomo/contrib/parmest/examples/reactor_design/reactor_design.py :language: python The file **parameter_estimation_example.py** uses parmest to estimate values of `k1`, @@ -35,7 +35,7 @@ observed values of `ca`, `cb`, `cc`, and `cd`. Additional example files use parmest to run parameter estimation with bootstrap resampling and perform a likelihood ratio test over a range of theta values. -.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py +.. literalinclude:: /../../pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py :language: python The semibatch and Rooney Biegler examples are defined in a similar diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/parallel.rst b/doc/OnlineDocs/user_explanations/analysis/parmest/parallel.rst index 3b60c5777de..e1d6548b105 100644 --- a/doc/OnlineDocs/user_explanations/analysis/parmest/parallel.rst +++ b/doc/OnlineDocs/user_explanations/analysis/parmest/parallel.rst @@ -16,7 +16,7 @@ model in parallel:: The file **parallel_example.py** is shown below. Results are saved to file for later analysis. -.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/semibatch/parallel_example.py +.. literalinclude:: /../../pyomo/contrib/parmest/examples/semibatch/parallel_example.py :language: python Installation diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/scencreate.rst b/doc/OnlineDocs/user_explanations/analysis/parmest/scencreate.rst index b63ac5893c2..79dfc31fbbb 100644 --- a/doc/OnlineDocs/user_explanations/analysis/parmest/scencreate.rst +++ b/doc/OnlineDocs/user_explanations/analysis/parmest/scencreate.rst @@ -14,7 +14,7 @@ correspond one-to-one with the experiments used as input data. It also creates a few scenarios using the bootstrap methods and outputs prints the scenarios to the screen, accessing them via the ``ScensItator`` a ``print`` -.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/semibatch/scenario_example.py +.. literalinclude:: /../../pyomo/contrib/parmest/examples/semibatch/scenario_example.py :language: python .. note:: diff --git a/doc/OnlineDocs/user_explanations/modeling/Constraints.rst b/doc/OnlineDocs/user_explanations/modeling/Constraints.rst index 0cc42cb2abe..fdda70fa28d 100644 --- a/doc/OnlineDocs/user_explanations/modeling/Constraints.rst +++ b/doc/OnlineDocs/user_explanations/modeling/Constraints.rst @@ -6,7 +6,7 @@ that are created using a rule, which is a Python function. For example, if the variable ``model.x`` has the indexes 'butter' and 'scones', then this constraint limits the sum over these indexes to be exactly three: -.. literalinclude:: ../src/scripting/spy4Constraints_Constraint_example.spy +.. literalinclude:: /src/scripting/spy4Constraints_Constraint_example.spy :language: python Instead of expressions involving equality (==) or inequalities (`<=` or @@ -16,7 +16,7 @@ lb `<=` expr `<=` ub. Variables can appear only in the middle expr. For example, the following two constraint declarations have the same meaning: -.. literalinclude:: ../src/scripting/spy4Constraints_Inequality_constraints_2expressions.spy +.. literalinclude:: /src/scripting/spy4Constraints_Inequality_constraints_2expressions.spy :language: python For this simple example, it would also be possible to declare @@ -30,7 +30,7 @@ interpreted as placing a budget of :math:`i` on the :math:`i^{\mbox{th}}` item to buy where the cost per item is given by the parameter ``model.a``: -.. literalinclude:: ../src/scripting/spy4Constraints_Passing_elements_crossproduct.spy +.. literalinclude:: /src/scripting/spy4Constraints_Passing_elements_crossproduct.spy :language: python .. note:: diff --git a/doc/OnlineDocs/user_explanations/modeling/Expressions.rst b/doc/OnlineDocs/user_explanations/modeling/Expressions.rst index 16c206e2fe8..df506064d5d 100644 --- a/doc/OnlineDocs/user_explanations/modeling/Expressions.rst +++ b/doc/OnlineDocs/user_explanations/modeling/Expressions.rst @@ -20,7 +20,7 @@ possible to build up expressions. The following example illustrates this, along with a reference to global Python data in the form of a Python variable called ``switch``: -.. literalinclude:: ../src/scripting/spy4Expressions_Buildup_expression_switch.spy +.. literalinclude:: /src/scripting/spy4Expressions_Buildup_expression_switch.spy :language: python In this example, the constraint that is generated depends on the value @@ -33,7 +33,7 @@ otherwise, the ``model.d`` term is not present. Because model elements result in expressions, not values, the following does not work as expected in an abstract model! - .. literalinclude:: ../src/scripting/spy4Expressions_Abstract_wrong_usage.spy + .. literalinclude:: /src/scripting/spy4Expressions_Abstract_wrong_usage.spy :language: python The trouble is that ``model.d >= 2`` results in an expression, not @@ -58,7 +58,7 @@ as described in the paper [Vielma_et_al]_. There are two basic forms for the declaration of the constraint: -.. literalinclude:: ../src/scripting/spy4Expressions_Declare_piecewise_constraints.spy +.. literalinclude:: /src/scripting/spy4Expressions_Declare_piecewise_constraints.spy :language: python where ``pwconst`` can be replaced by a name appropriate for the @@ -124,7 +124,7 @@ Keywords: indexing set is used or when all indices use an identical piecewise function). Examples: - .. literalinclude:: ../src/scripting/spy4Expressions_f_rule_Function_examples.spy + .. literalinclude:: /src/scripting/spy4Expressions_f_rule_Function_examples.spy :language: python * **force_pw=True/False** @@ -163,7 +163,7 @@ Keywords: Here is an example of an assignment to a Python dictionary variable that has keywords for a picewise constraint: -.. literalinclude:: ../src/scripting/spy4Expressions_Keyword_assignment_example.spy +.. literalinclude:: /src/scripting/spy4Expressions_Keyword_assignment_example.spy :language: python Here is a simple example based on the example given earlier in @@ -175,7 +175,7 @@ whimsically just to make the example. The important thing to note is that variables that are going to appear as the independent variable in a piecewise constraint must have bounds. -.. literalinclude:: ../src/scripting/abstract2piece.py +.. literalinclude:: /src/scripting/abstract2piece.py :language: python A more advanced example is provided in abstract2piecebuild.py in @@ -193,13 +193,13 @@ variable x times the index. Later in the model file, just to illustrate how to do it, the expression is changed but just for the first index to be x squared. -.. literalinclude:: ../src/scripting/spy4Expressions_Expression_objects_illustration.spy +.. literalinclude:: /src/scripting/spy4Expressions_Expression_objects_illustration.spy :language: python An alternative is to create Python functions that, potentially, manipulate model objects. E.g., if you define a function -.. literalinclude:: ../src/scripting/spy4Expressions_Define_python_function.spy +.. literalinclude:: /src/scripting/spy4Expressions_Define_python_function.spy :language: python You can call this function with or without Pyomo modeling components as @@ -211,7 +211,7 @@ expression is used to generate another expression (e.g., f(model.x, 3) + 5), the initial expression is always cloned so that the new generated expression is independent of the old. For example: -.. literalinclude:: ../src/scripting/spy4Expressions_Generate_new_expression.spy +.. literalinclude:: /src/scripting/spy4Expressions_Generate_new_expression.spy :language: python If you want to create an expression that is shared between other diff --git a/doc/OnlineDocs/user_explanations/modeling/Sets.rst b/doc/OnlineDocs/user_explanations/modeling/Sets.rst index 73c3539d79d..ababa80be5a 100644 --- a/doc/OnlineDocs/user_explanations/modeling/Sets.rst +++ b/doc/OnlineDocs/user_explanations/modeling/Sets.rst @@ -443,7 +443,7 @@ model is: for this model, a toy data file (in AMPL "``.dat``" format) would be: -.. literalinclude:: ../src/scripting/Isinglecomm.dat +.. literalinclude:: /src/scripting/Isinglecomm.dat :language: text .. doctest:: diff --git a/doc/OnlineDocs/user_explanations/modeling/Variables.rst b/doc/OnlineDocs/user_explanations/modeling/Variables.rst index 7f7ee74af5f..58ccacc17da 100644 --- a/doc/OnlineDocs/user_explanations/modeling/Variables.rst +++ b/doc/OnlineDocs/user_explanations/modeling/Variables.rst @@ -20,13 +20,13 @@ declaring a *singleton* (i.e. unindexed) variable named ``model.LumberJack`` that will take on real values between zero and 6 and it initialized to be 1.5: -.. literalinclude:: ../src/scripting/spy4Variables_Declare_singleton_variable.spy +.. literalinclude:: /src/scripting/spy4Variables_Declare_singleton_variable.spy :language: python Instead of the ``initialize`` option, initialization is sometimes done with a Python assignment statement as in -.. literalinclude:: ../src/scripting/spy4Variables_Assign_value.spy +.. literalinclude:: /src/scripting/spy4Variables_Assign_value.spy :language: python For indexed variables, bounds and initial values are often specified by @@ -36,7 +36,7 @@ followed by the indexes. This is illustrated in the following code snippet that makes use of Python dictionaries declared as lb and ub that are used by a function to provide bounds: -.. literalinclude:: ../src/scripting/spy4Variables_Declare_bounds.spy +.. literalinclude:: /src/scripting/spy4Variables_Declare_bounds.spy :language: python .. note:: diff --git a/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/design.rst b/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/design.rst index ddecb39ad0c..d12fe745672 100644 --- a/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/design.rst +++ b/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/design.rst @@ -73,7 +73,7 @@ Expression trees can be categorized in four different ways: These three categories are illustrated with the following example: -.. literalinclude:: ../../src/expr/design_categories.spy +.. literalinclude:: /src/expr/design_categories.spy The following table describes four different simple expressions that consist of a single model component, and it shows how they @@ -107,7 +107,7 @@ Named expressions allow for changes to an expression after it has been constructed. For example, consider the expression ``f`` defined with the :class:`Expression ` component: -.. literalinclude:: ../../src/expr/design_named_expression.spy +.. literalinclude:: /src/expr/design_named_expression.spy Although ``f`` is an immutable expression, whose definition is fixed, a sub-expressions is the named expression ``M.e``. Named @@ -227,7 +227,7 @@ The :data:`linear_expression ` object is a context manager that can be used to declare a linear sum. For example, consider the following two loops: -.. literalinclude:: ../../src/expr/design_cm1.spy +.. literalinclude:: /src/expr/design_cm1.spy The first apparent difference in these loops is that the value of ``s`` is explicitly initialized while ``e`` is initialized when the @@ -250,7 +250,7 @@ construct different expressions with different context declarations. Finally, note that these context managers can be passed into the :attr:`start` method for the :func:`quicksum ` function. For example: -.. literalinclude:: ../../src/expr/design_cm2.spy +.. literalinclude:: /src/expr/design_cm2.spy This sum contains terms for ``M.x[i]`` and ``M.y[i]``. The syntax in this example is not intuitive because the sum is being stored diff --git a/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/index.rst b/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/index.rst index 685fde25173..e7dcf5831a1 100644 --- a/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/index.rst +++ b/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/index.rst @@ -21,7 +21,7 @@ nodes contain operators. Pyomo relies on so-called magic methods to automate the construction of symbolic expressions. For example, consider an expression ``e`` declared as follows: -.. literalinclude:: ../../src/expr/index_simple.spy +.. literalinclude:: /src/expr/index_simple.spy Python determines that the magic method ``__mul__`` is called on the ``M.v`` object, with the argument ``2``. This method returns diff --git a/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/managing.rst b/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/managing.rst index a4dd2a51436..f028eef778d 100644 --- a/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/managing.rst +++ b/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/managing.rst @@ -23,7 +23,7 @@ mimics the Python operations used to construct an expression. The :data:`verbose` flag can be set to :const:`True` to generate a string representation that is a nested functional form. For example: -.. literalinclude:: ../../src/expr/managing_ex1.spy +.. literalinclude:: /src/expr/managing_ex1.spy Labeler and Symbol Map ~~~~~~~~~~~~~~~~~~~~~~ @@ -37,7 +37,7 @@ the :class:`NumericLabeler` defines a functor that can be used to sequentially generate simple labels with a prefix followed by the variable count: -.. literalinclude:: ../../src/expr/managing_ex2.spy +.. literalinclude:: /src/expr/managing_ex2.spy The :data:`smap` option is used to specify a symbol map object (:class:`SymbolMap `), which @@ -72,19 +72,19 @@ the expression have a value. The :func:`value ` function can be used to walk the expression tree and compute the value of an expression. For example: -.. literalinclude:: ../../src/expr/managing_ex5.spy +.. literalinclude:: /src/expr/managing_ex5.spy Additionally, expressions define the :func:`__call__` method, so the following is another way to compute the value of an expression: -.. literalinclude:: ../../src/expr/managing_ex6.spy +.. literalinclude:: /src/expr/managing_ex6.spy If a parameter or variable is undefined, then the :func:`value ` function and :func:`__call__` method will raise an exception. This exception can be suppressed using the :attr:`exception` option. For example: -.. literalinclude:: ../../src/expr/managing_ex7.spy +.. literalinclude:: /src/expr/managing_ex7.spy This option is useful in contexts where adding a try block is inconvenient in your modeling script. @@ -108,7 +108,7 @@ functions that support this functionality. First, the function is a generator function that walks the expression tree and yields all nodes whose type is in a specified set of node types. For example: -.. literalinclude:: ../../src/expr/managing_ex8.spy +.. literalinclude:: /src/expr/managing_ex8.spy The :func:`identify_variables ` function is a generator function that yields all nodes that are @@ -117,7 +117,7 @@ but this set of variable types does not need to be specified by the user. However, the :attr:`include_fixed` flag can be specified to omit fixed variables. For example: -.. literalinclude:: ../../src/expr/managing_ex9.spy +.. literalinclude:: /src/expr/managing_ex9.spy Walking an Expression Tree with a Visitor Class ----------------------------------------------- @@ -223,14 +223,14 @@ In this example, we describe an visitor class that counts the number of nodes in an expression (including leaf nodes). Consider the following class: -.. literalinclude:: ../../src/expr/managing_visitor1.spy +.. literalinclude:: /src/expr/managing_visitor1.spy The class constructor creates a counter, and the :func:`visit` method increments this counter for every node that is visited. The :func:`finalize` method returns the value of this counter after the tree has been walked. The following function illustrates this use of this visitor class: -.. literalinclude:: ../../src/expr/managing_visitor2.spy +.. literalinclude:: /src/expr/managing_visitor2.spy ExpressionValueVisitor Example @@ -240,14 +240,14 @@ In this example, we describe an visitor class that clones the expression tree (including leaf nodes). Consider the following class: -.. literalinclude:: ../../src/expr/managing_visitor3.spy +.. literalinclude:: /src/expr/managing_visitor3.spy The :func:`visit` method creates a new expression node with children specified by :attr:`values`. The :func:`visiting_potential_leaf` method performs a :func:`deepcopy` on leaf nodes, which are native Python types or non-expression objects. -.. literalinclude:: ../../src/expr/managing_visitor4.spy +.. literalinclude:: /src/expr/managing_visitor4.spy ExpressionReplacementVisitor Example @@ -258,15 +258,15 @@ variables with scaled variables, using a mutable parameter that can be modified later. the following class: -.. literalinclude:: ../../src/expr/managing_visitor5.spy +.. literalinclude:: /src/expr/managing_visitor5.spy No other method need to be defined. The :func:`beforeChild` method identifies variable nodes and returns a product expression that contains a mutable parameter. -.. literalinclude:: ../../src/expr/managing_visitor6.spy +.. literalinclude:: /src/expr/managing_visitor6.spy The :func:`scale_expression` function is called with an expression and a dictionary, :attr:`scale`, that maps variable ID to model parameter. For example: -.. literalinclude:: ../../src/expr/managing_visitor7.spy +.. literalinclude:: /src/expr/managing_visitor7.spy diff --git a/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/overview.rst b/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/overview.rst index c1962edec22..c89e3e6b4b7 100644 --- a/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/overview.rst +++ b/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/overview.rst @@ -50,13 +50,13 @@ are: example, the following two loops had dramatically different runtime: - .. literalinclude:: ../../src/expr/overview_example1.spy + .. literalinclude:: /src/expr/overview_example1.spy * Coopr3 eliminates side effects by automatically cloning sub-expressions. Unfortunately, this can easily lead to unexpected cloning in models, which can dramatically slow down Pyomo model generation. For example: - .. literalinclude:: ../../src/expr/overview_example2.spy + .. literalinclude:: /src/expr/overview_example2.spy * Coopr3 leverages recursion in many operations, including expression cloning. Even simple non-linear expressions can result in deep @@ -82,7 +82,7 @@ control for how expressions are managed in Python. For example: * Python variables can point to the same expression tree - .. literalinclude:: ../../src/expr/overview_tree1.spy + .. literalinclude:: /src/expr/overview_tree1.spy This is illustrated as follows: @@ -102,7 +102,7 @@ control for how expressions are managed in Python. For example: * A variable can point to a sub-tree that another variable points to - .. literalinclude:: ../../src/expr/overview_tree2.spy + .. literalinclude:: /src/expr/overview_tree2.spy This is illustrated as follows: @@ -124,7 +124,7 @@ control for how expressions are managed in Python. For example: * Two expression trees can point to the same sub-tree - .. literalinclude:: ../../src/expr/overview_tree3.spy + .. literalinclude:: /src/expr/overview_tree3.spy This is illustrated as follows: @@ -169,7 +169,7 @@ between expressions, we do not consider those expressions entangled. Expression entanglement is problematic because shared expressions complicate the expected behavior when sub-expressions are changed. Consider the following example: -.. literalinclude:: ../../src/expr/overview_tree4.spy +.. literalinclude:: /src/expr/overview_tree4.spy What is the value of ``e`` after ``M.w`` is added to it? What is the value of ``f``? The answers to these questions are not immediately @@ -244,7 +244,7 @@ There is one important exception to the entanglement property described above. The ``Expression`` component is treated as a mutable expression when shared between expressions. For example: -.. literalinclude:: ../../src/expr/overview_tree5.spy +.. literalinclude:: /src/expr/overview_tree5.spy Here, the expression ``M.e`` is a so-called *named expression* that the user has declared. Named expressions are explicitly intended diff --git a/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/performance.rst b/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/performance.rst index 8e344e50982..c7b68377098 100644 --- a/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/performance.rst +++ b/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/performance.rst @@ -11,14 +11,14 @@ Expression Generation Pyomo expressions can be constructed using native binary operators in Python. For example, a sum can be created in a simple loop: -.. literalinclude:: ../../src/expr/performance_loop1.spy +.. literalinclude:: /src/expr/performance_loop1.spy Additionally, Pyomo expressions can be constructed using functions that iteratively apply Python binary operators. For example, the Python :func:`sum` function can be used to replace the previous loop: -.. literalinclude:: ../../src/expr/performance_loop2.spy +.. literalinclude:: /src/expr/performance_loop2.spy The :func:`sum` function is both more compact and more efficient. Using :func:`sum` avoids the creation of temporary variables, and @@ -47,7 +47,7 @@ expressions. For example, consider the following quadratic polynomial: -.. literalinclude:: ../../src/expr/performance_loop3.spy +.. literalinclude:: /src/expr/performance_loop3.spy This quadratic polynomial is treated as a nonlinear expression unless the expression is explicitly processed to identify quadratic @@ -78,7 +78,7 @@ The :func:`prod ` function is analogous to the builtin argument list, :attr:`args`, which represents expressions that are multiplied together. For example: -.. literalinclude:: ../../src/expr/performance_prod.spy +.. literalinclude:: /src/expr/performance_prod.spy quicksum ~~~~~~~~ @@ -89,7 +89,7 @@ generates a more compact Pyomo expression. Its main argument is a variable length argument list, :attr:`args`, which represents expressions that are summed together. For example: -.. literalinclude:: ../../src/expr/performance_quicksum.spy +.. literalinclude:: /src/expr/performance_quicksum.spy The summation is customized based on the :attr:`start` and :attr:`linear` arguments. The :attr:`start` defines the initial @@ -111,13 +111,13 @@ more quickly. Consider the following example: -.. literalinclude:: ../../src/expr/quicksum_runtime.spy +.. literalinclude:: /src/expr/quicksum_runtime.spy The sum consists of linear terms because the exponents are one. The following output illustrates that quicksum can identify this linear structure to generate expressions more quickly: -.. literalinclude:: ../../src/expr/quicksum.log +.. literalinclude:: /src/expr/quicksum.log :language: none If :attr:`start` is not a numeric value, then the :func:`quicksum @@ -134,7 +134,7 @@ to be stored in an object that is passed into the function (e.g. the linear cont term in :attr:`args` is misleading. Consider the following example: - .. literalinclude:: ../../src/expr/performance_warning.spy + .. literalinclude:: /src/expr/performance_warning.spy The first term created by the generator is linear, but the subsequent terms are nonlinear. Pyomo gracefully transitions @@ -153,12 +153,12 @@ calling :func:`quicksum `. If two or more components provided, then the result is the summation of their terms multiplied together. For example: -.. literalinclude:: ../../src/expr/performance_sum_product1.spy +.. literalinclude:: /src/expr/performance_sum_product1.spy The :attr:`denom` argument specifies components whose terms are in the denominator. For example: -.. literalinclude:: ../../src/expr/performance_sum_product2.spy +.. literalinclude:: /src/expr/performance_sum_product2.spy The terms summed by this function are explicitly specified, so :func:`sum_product ` can identify diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.mpi_blocks.rst b/doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.mpi_blocks.rst index b9cb1d5db7a..e65d4da9c96 100644 --- a/doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.mpi_blocks.rst +++ b/doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.mpi_blocks.rst @@ -12,7 +12,7 @@ or all processes/ranks. Consider the following example (in a file called "parallel_vector_ops.py"). -.. literalinclude:: ../../../../pyomo/contrib/pynumero/examples/parallel_vector_ops.py +.. literalinclude:: /../../pyomo/contrib/pynumero/examples/parallel_vector_ops.py This example can be run with @@ -46,7 +46,7 @@ except that the operations are now performed in parallel. `MPIBlockMatrix` construction is very similar. Consider the following example in a file called "parallel_matvec.py". -.. literalinclude:: ../../../../pyomo/contrib/pynumero/examples/parallel_matvec.py +.. literalinclude:: /../../pyomo/contrib/pynumero/examples/parallel_matvec.py Which can be run with From 001f1d0c977df8c77c83a88c8074ed8d738f41c5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 06:18:50 -0600 Subject: [PATCH 2395/3044] NFC: fix doc indentation --- pyomo/contrib/alternative_solutions/obbt.py | 76 ++++++++++----------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/obbt.py b/pyomo/contrib/alternative_solutions/obbt.py index 3fc59bd3214..eb74d75a5db 100644 --- a/pyomo/contrib/alternative_solutions/obbt.py +++ b/pyomo/contrib/alternative_solutions/obbt.py @@ -106,44 +106,44 @@ def obbt_analysis_bounds_and_solutions( This can be applied to any class of problem supported by the selected solver. - Parameters - ---------- - model : ConcreteModel - A concrete Pyomo model. - variables: None or a collection of Pyomo _GeneralVarData variables - The variables for which bounds will be generated. None indicates - that all variables will be included. Alternatively, a collection of - _GenereralVarData variables can be provided. - rel_opt_gap : float or None - The relative optimality gap for the original objective for which - variable bounds will be found. None indicates that a relative gap - constraint will not be added to the model. - abs_opt_gap : float or None - The absolute optimality gap for the original objective for which - variable bounds will be found. None indicates that an absolute gap - constraint will not be added to the model. - refine_discrete_bounds : boolean - Boolean indicating that new constraints should be added to the - model at each iteration to tighten the bounds for discrete - variables. - warmstart : boolean - Boolean indicating that the solver should be warmstarted from the - best previously discovered solution. - solver : string - The solver to be used. - solver_options : dict - Solver option-value pairs to be passed to the solver. - tee : boolean - Boolean indicating that the solver output should be displayed. - - Returns - ------- - variable_ranges - A Pyomo ComponentMap containing the bounds for each variable. - {variable: (lower_bound, upper_bound)}. An exception is raised when - the solver encountered an issue. - solutions - [Solution] + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model. + variables: None or a collection of Pyomo _GeneralVarData variables + The variables for which bounds will be generated. None indicates + that all variables will be included. Alternatively, a collection of + _GenereralVarData variables can be provided. + rel_opt_gap : float or None + The relative optimality gap for the original objective for which + variable bounds will be found. None indicates that a relative gap + constraint will not be added to the model. + abs_opt_gap : float or None + The absolute optimality gap for the original objective for which + variable bounds will be found. None indicates that an absolute gap + constraint will not be added to the model. + refine_discrete_bounds : boolean + Boolean indicating that new constraints should be added to the + model at each iteration to tighten the bounds for discrete + variables. + warmstart : boolean + Boolean indicating that the solver should be warmstarted from the + best previously discovered solution. + solver : string + The solver to be used. + solver_options : dict + Solver option-value pairs to be passed to the solver. + tee : boolean + Boolean indicating that the solver output should be displayed. + + Returns + ------- + variable_ranges + A Pyomo ComponentMap containing the bounds for each variable. + {variable: (lower_bound, upper_bound)}. An exception is raised when + the solver encountered an issue. + solutions + [Solution] """ # TODO - parallelization From 1d3d89320f75278868b13c5cbfad60a63d80807a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 06:20:50 -0600 Subject: [PATCH 2396/3044] Update doc references --- doc/OnlineDocs/getting_started/index.rst | 5 ++++- doc/OnlineDocs/index.rst | 7 +++---- doc/OnlineDocs/reference_guide/index.rst | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/doc/OnlineDocs/getting_started/index.rst b/doc/OnlineDocs/getting_started/index.rst index 9a6a251e7a3..45b8bec019c 100644 --- a/doc/OnlineDocs/getting_started/index.rst +++ b/doc/OnlineDocs/getting_started/index.rst @@ -1,5 +1,8 @@ Getting Started =============== -TODO +.. toctree:: + :maxdepth: 2 + installation.rst + pyomo_overview/index.rst diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index 7a3ca3ddf8e..3842a6beae5 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -66,12 +66,11 @@ with a diverse set of optimization capabilities. .. toctree:: :hidden: - :maxdepth: 1 + :maxdepth: 2 - index Getting Started - User Guide - Developer Guide + Hot-To Guides + User Explanations Reference Guide diff --git a/doc/OnlineDocs/reference_guide/index.rst b/doc/OnlineDocs/reference_guide/index.rst index 5007ed4e193..e0631861542 100644 --- a/doc/OnlineDocs/reference_guide/index.rst +++ b/doc/OnlineDocs/reference_guide/index.rst @@ -4,9 +4,9 @@ Reference Guide .. toctree:: :maxdepth: 2 - library_reference/index.rst - errors.rst - Preview capabilities through ``pyomo.__future__`` + library_reference/index.rst + errors.rst + future.rst Bibliography From 584634e16ac73c96a24317609e1d78dbb8130509 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 06:21:08 -0600 Subject: [PATCH 2397/3044] Doc: fix autodoc import --- .../user_explanations/analysis/alternative_solutions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/user_explanations/analysis/alternative_solutions.rst b/doc/OnlineDocs/user_explanations/analysis/alternative_solutions.rst index cc5ab07c3cc..d9551f005fb 100644 --- a/doc/OnlineDocs/user_explanations/analysis/alternative_solutions.rst +++ b/doc/OnlineDocs/user_explanations/analysis/alternative_solutions.rst @@ -97,7 +97,7 @@ Interface Documentation .. autofunction:: enumerate_linear_solutions -.. autofunction:: enumerate_linear_solutions_soln_pool +.. autofunction:: pyomo.contrib.alternative_solutions.lp_enum_solnpool.enumerate_linear_solutions_soln_pool .. autofunction:: gurobi_generate_solutions From 497d82e3314242cc2db9af3550ad2c8996105692 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 06:24:27 -0600 Subject: [PATCH 2398/3044] Re-add missing src directly dropped during reorg --- doc/OnlineDocs/src/data/A.tab | 4 + doc/OnlineDocs/src/data/ABCD.tab | 4 + doc/OnlineDocs/src/data/ABCD.txt | 4 + doc/OnlineDocs/src/data/ABCD.xls | Bin 0 -> 16384 bytes doc/OnlineDocs/src/data/ABCD1.dat | 1 + doc/OnlineDocs/src/data/ABCD1.py | 20 ++ doc/OnlineDocs/src/data/ABCD1.txt | 1 + doc/OnlineDocs/src/data/ABCD2.dat | 1 + doc/OnlineDocs/src/data/ABCD2.py | 25 ++ doc/OnlineDocs/src/data/ABCD2.txt | 5 + doc/OnlineDocs/src/data/ABCD3.dat | 1 + doc/OnlineDocs/src/data/ABCD3.py | 24 ++ doc/OnlineDocs/src/data/ABCD3.txt | 5 + doc/OnlineDocs/src/data/ABCD4.dat | 1 + doc/OnlineDocs/src/data/ABCD4.py | 24 ++ doc/OnlineDocs/src/data/ABCD4.txt | 5 + doc/OnlineDocs/src/data/ABCD5.dat | 1 + doc/OnlineDocs/src/data/ABCD5.py | 30 ++ doc/OnlineDocs/src/data/ABCD5.txt | 9 + doc/OnlineDocs/src/data/ABCD6.dat | 1 + doc/OnlineDocs/src/data/ABCD6.py | 24 ++ doc/OnlineDocs/src/data/ABCD6.txt | 5 + doc/OnlineDocs/src/data/ABCD7.dat | 1 + doc/OnlineDocs/src/data/ABCD7.py | 30 ++ doc/OnlineDocs/src/data/ABCD7.txt | 5 + doc/OnlineDocs/src/data/ABCD8.bad | 1 + doc/OnlineDocs/src/data/ABCD8.dat | 1 + doc/OnlineDocs/src/data/ABCD8.py | 30 ++ doc/OnlineDocs/src/data/ABCD9.bad | 1 + doc/OnlineDocs/src/data/ABCD9.dat | 3 + doc/OnlineDocs/src/data/ABCD9.py | 30 ++ doc/OnlineDocs/src/data/C.tab | 10 + doc/OnlineDocs/src/data/D.tab | 4 + doc/OnlineDocs/src/data/U.tab | 5 + doc/OnlineDocs/src/data/Y.tab | 4 + doc/OnlineDocs/src/data/Z.tab | 1 + doc/OnlineDocs/src/data/data_managers.txt | 30 ++ doc/OnlineDocs/src/data/diet.dat | 33 ++ doc/OnlineDocs/src/data/diet.sql | 96 +++++ doc/OnlineDocs/src/data/diet.sqlite | Bin 0 -> 11264 bytes doc/OnlineDocs/src/data/diet.sqlite.dat | 6 + doc/OnlineDocs/src/data/diet1.py | 86 +++++ doc/OnlineDocs/src/data/ex.dat | 2 + doc/OnlineDocs/src/data/ex.py | 22 ++ doc/OnlineDocs/src/data/ex.txt | 1 + doc/OnlineDocs/src/data/ex1.dat | 1 + doc/OnlineDocs/src/data/ex2.dat | 1 + doc/OnlineDocs/src/data/import1.tab.dat | 1 + doc/OnlineDocs/src/data/import1.tab.py | 24 ++ doc/OnlineDocs/src/data/import1.tab.txt | 4 + doc/OnlineDocs/src/data/import2.tab.dat | 1 + doc/OnlineDocs/src/data/import2.tab.py | 25 ++ doc/OnlineDocs/src/data/import2.tab.txt | 5 + doc/OnlineDocs/src/data/import3.tab.dat | 1 + doc/OnlineDocs/src/data/import3.tab.py | 20 ++ doc/OnlineDocs/src/data/import3.tab.txt | 1 + doc/OnlineDocs/src/data/import4.tab.dat | 1 + doc/OnlineDocs/src/data/import4.tab.py | 20 ++ doc/OnlineDocs/src/data/import4.tab.txt | 1 + doc/OnlineDocs/src/data/import5.tab.dat | 1 + doc/OnlineDocs/src/data/import5.tab.py | 20 ++ doc/OnlineDocs/src/data/import5.tab.txt | 1 + doc/OnlineDocs/src/data/import6.tab.dat | 1 + doc/OnlineDocs/src/data/import6.tab.py | 20 ++ doc/OnlineDocs/src/data/import6.tab.txt | 1 + doc/OnlineDocs/src/data/import7.tab.dat | 1 + doc/OnlineDocs/src/data/import7.tab.py | 28 ++ doc/OnlineDocs/src/data/import7.tab.txt | 15 + doc/OnlineDocs/src/data/import8.tab.dat | 1 + doc/OnlineDocs/src/data/import8.tab.py | 26 ++ doc/OnlineDocs/src/data/import8.tab.txt | 15 + doc/OnlineDocs/src/data/namespace1.dat | 12 + doc/OnlineDocs/src/data/param1.dat | 5 + doc/OnlineDocs/src/data/param1.py | 30 ++ doc/OnlineDocs/src/data/param1.txt | 5 + doc/OnlineDocs/src/data/param2.dat | 3 + doc/OnlineDocs/src/data/param2.py | 25 ++ doc/OnlineDocs/src/data/param2.txt | 3 + doc/OnlineDocs/src/data/param2a.dat | 7 + doc/OnlineDocs/src/data/param2a.py | 25 ++ doc/OnlineDocs/src/data/param2a.txt | 3 + doc/OnlineDocs/src/data/param3.dat | 7 + doc/OnlineDocs/src/data/param3.py | 36 ++ doc/OnlineDocs/src/data/param3.txt | 12 + doc/OnlineDocs/src/data/param3a.dat | 7 + doc/OnlineDocs/src/data/param3a.py | 36 ++ doc/OnlineDocs/src/data/param3a.txt | 12 + doc/OnlineDocs/src/data/param3b.dat | 7 + doc/OnlineDocs/src/data/param3b.py | 36 ++ doc/OnlineDocs/src/data/param3b.txt | 9 + doc/OnlineDocs/src/data/param3c.dat | 5 + doc/OnlineDocs/src/data/param3c.py | 36 ++ doc/OnlineDocs/src/data/param3c.txt | 12 + doc/OnlineDocs/src/data/param4.dat | 6 + doc/OnlineDocs/src/data/param4.py | 26 ++ doc/OnlineDocs/src/data/param4.txt | 4 + doc/OnlineDocs/src/data/param5.dat | 6 + doc/OnlineDocs/src/data/param5.py | 25 ++ doc/OnlineDocs/src/data/param5.txt | 3 + doc/OnlineDocs/src/data/param5a.dat | 6 + doc/OnlineDocs/src/data/param5a.py | 25 ++ doc/OnlineDocs/src/data/param5a.txt | 3 + doc/OnlineDocs/src/data/param6.dat | 7 + doc/OnlineDocs/src/data/param6.py | 36 ++ doc/OnlineDocs/src/data/param6.txt | 12 + doc/OnlineDocs/src/data/param6a.dat | 5 + doc/OnlineDocs/src/data/param6a.py | 36 ++ doc/OnlineDocs/src/data/param6a.txt | 12 + doc/OnlineDocs/src/data/param7a.dat | 7 + doc/OnlineDocs/src/data/param7a.py | 25 ++ doc/OnlineDocs/src/data/param7a.txt | 9 + doc/OnlineDocs/src/data/param7b.dat | 7 + doc/OnlineDocs/src/data/param7b.py | 25 ++ doc/OnlineDocs/src/data/param7b.txt | 9 + doc/OnlineDocs/src/data/param8a.dat | 7 + doc/OnlineDocs/src/data/param8a.py | 25 ++ doc/OnlineDocs/src/data/param8a.txt | 4 + doc/OnlineDocs/src/data/pyomo.diet1.sh | 4 + doc/OnlineDocs/src/data/pyomo.diet1.txt | 60 ++++ doc/OnlineDocs/src/data/pyomo.diet2.sh | 4 + doc/OnlineDocs/src/data/pyomo.diet2.txt | 60 ++++ doc/OnlineDocs/src/data/set1.dat | 17 + doc/OnlineDocs/src/data/set1.py | 24 ++ doc/OnlineDocs/src/data/set1.txt | 3 + doc/OnlineDocs/src/data/set2.dat | 1 + doc/OnlineDocs/src/data/set2.py | 22 ++ doc/OnlineDocs/src/data/set2.txt | 1 + doc/OnlineDocs/src/data/set2a.dat | 1 + doc/OnlineDocs/src/data/set2a.py | 22 ++ doc/OnlineDocs/src/data/set2a.txt | 1 + doc/OnlineDocs/src/data/set3.dat | 5 + doc/OnlineDocs/src/data/set3.py | 30 ++ doc/OnlineDocs/src/data/set3.txt | 3 + doc/OnlineDocs/src/data/set4.dat | 4 + doc/OnlineDocs/src/data/set4.py | 22 ++ doc/OnlineDocs/src/data/set4.txt | 1 + doc/OnlineDocs/src/data/set5.dat | 3 + doc/OnlineDocs/src/data/set5.py | 24 ++ doc/OnlineDocs/src/data/set5.txt | 4 + doc/OnlineDocs/src/data/table0.dat | 6 + doc/OnlineDocs/src/data/table0.py | 20 ++ doc/OnlineDocs/src/data/table0.txt | 13 + doc/OnlineDocs/src/data/table0.ul.dat | 5 + doc/OnlineDocs/src/data/table0.ul.py | 20 ++ doc/OnlineDocs/src/data/table0.ul.txt | 13 + doc/OnlineDocs/src/data/table1.dat | 3 + doc/OnlineDocs/src/data/table1.py | 20 ++ doc/OnlineDocs/src/data/table1.txt | 13 + doc/OnlineDocs/src/data/table2.dat | 6 + doc/OnlineDocs/src/data/table2.py | 23 ++ doc/OnlineDocs/src/data/table2.txt | 21 ++ doc/OnlineDocs/src/data/table3.dat | 6 + doc/OnlineDocs/src/data/table3.py | 25 ++ doc/OnlineDocs/src/data/table3.txt | 24 ++ doc/OnlineDocs/src/data/table3.ul.dat | 5 + doc/OnlineDocs/src/data/table3.ul.py | 25 ++ doc/OnlineDocs/src/data/table3.ul.txt | 24 ++ doc/OnlineDocs/src/data/table4.dat | 6 + doc/OnlineDocs/src/data/table4.py | 23 ++ doc/OnlineDocs/src/data/table4.txt | 21 ++ doc/OnlineDocs/src/data/table4.ul.dat | 5 + doc/OnlineDocs/src/data/table4.ul.py | 23 ++ doc/OnlineDocs/src/data/table4.ul.txt | 21 ++ doc/OnlineDocs/src/data/table5.dat | 6 + doc/OnlineDocs/src/data/table5.py | 20 ++ doc/OnlineDocs/src/data/table5.txt | 9 + doc/OnlineDocs/src/data/table6.dat | 1 + doc/OnlineDocs/src/data/table6.py | 19 + doc/OnlineDocs/src/data/table6.txt | 6 + doc/OnlineDocs/src/data/table7.dat | 6 + doc/OnlineDocs/src/data/table7.py | 21 ++ doc/OnlineDocs/src/data/table7.txt | 16 + doc/OnlineDocs/src/dataportal/A.tab | 4 + doc/OnlineDocs/src/dataportal/C.tab | 10 + doc/OnlineDocs/src/dataportal/D.tab | 4 + doc/OnlineDocs/src/dataportal/PP.csv | 4 + doc/OnlineDocs/src/dataportal/PP.json | 9 + doc/OnlineDocs/src/dataportal/PP.sqlite | Bin 0 -> 3072 bytes doc/OnlineDocs/src/dataportal/PP.tab | 4 + doc/OnlineDocs/src/dataportal/PP.xml | 11 + doc/OnlineDocs/src/dataportal/PP.yaml | 15 + doc/OnlineDocs/src/dataportal/PP_sqlite.py | 40 +++ doc/OnlineDocs/src/dataportal/Pyomo_mysql | 10 + doc/OnlineDocs/src/dataportal/S.tab | 4 + doc/OnlineDocs/src/dataportal/T.json | 8 + doc/OnlineDocs/src/dataportal/T.yaml | 17 + doc/OnlineDocs/src/dataportal/U.tab | 5 + doc/OnlineDocs/src/dataportal/XW.tab | 4 + doc/OnlineDocs/src/dataportal/Y.tab | 4 + doc/OnlineDocs/src/dataportal/Z.tab | 1 + .../src/dataportal/dataportal_tab.py | 334 ++++++++++++++++++ .../src/dataportal/dataportal_tab.txt | 315 +++++++++++++++++ doc/OnlineDocs/src/dataportal/excel.xls | Bin 0 -> 17920 bytes .../src/dataportal/param_initialization.py | 36 ++ .../src/dataportal/param_initialization.txt | 16 + .../src/dataportal/set_initialization.py | 55 +++ .../src/dataportal/set_initialization.txt | 31 ++ doc/OnlineDocs/src/expr/design.py | 64 ++++ doc/OnlineDocs/src/expr/design.txt | 19 + doc/OnlineDocs/src/expr/index.py | 21 ++ doc/OnlineDocs/src/expr/index.txt | 1 + doc/OnlineDocs/src/expr/managing.py | 249 +++++++++++++ doc/OnlineDocs/src/expr/managing.txt | 9 + doc/OnlineDocs/src/expr/overview.py | 103 ++++++ doc/OnlineDocs/src/expr/overview.txt | 11 + doc/OnlineDocs/src/expr/performance.py | 108 ++++++ doc/OnlineDocs/src/expr/performance.txt | 14 + doc/OnlineDocs/src/expr/quicksum.log | 4 + doc/OnlineDocs/src/expr/quicksum.py | 38 ++ doc/OnlineDocs/src/kernel/examples.sh | 3 + doc/OnlineDocs/src/kernel/examples.txt | 211 +++++++++++ .../src/scripting/AbstractSuffixes.py | 35 ++ doc/OnlineDocs/src/scripting/Isinglebuild.py | 58 +++ doc/OnlineDocs/src/scripting/Isinglecomm.dat | 25 ++ doc/OnlineDocs/src/scripting/NodesIn_init.py | 21 ++ doc/OnlineDocs/src/scripting/Z_init.py | 19 + doc/OnlineDocs/src/scripting/abstract1.dat | 18 + doc/OnlineDocs/src/scripting/abstract2.dat | 22 ++ doc/OnlineDocs/src/scripting/abstract2.py | 43 +++ doc/OnlineDocs/src/scripting/abstract2a.dat | 16 + .../src/scripting/abstract2piece.py | 62 ++++ .../src/scripting/abstract2piecebuild.py | 77 ++++ .../src/scripting/block_iter_example.py | 51 +++ doc/OnlineDocs/src/scripting/concrete1.py | 20 ++ doc/OnlineDocs/src/scripting/doubleA.py | 17 + doc/OnlineDocs/src/scripting/driveabs2.py | 47 +++ doc/OnlineDocs/src/scripting/driveconc1.py | 34 ++ doc/OnlineDocs/src/scripting/iterative1.py | 74 ++++ doc/OnlineDocs/src/scripting/iterative2.py | 52 +++ doc/OnlineDocs/src/scripting/noiteration1.py | 41 +++ doc/OnlineDocs/src/scripting/parallel.py | 38 ++ .../src/scripting/spy4Constraints.py | 62 ++++ .../src/scripting/spy4Expressions.py | 128 +++++++ .../src/scripting/spy4PyomoCommand.py | 36 ++ doc/OnlineDocs/src/scripting/spy4Variables.py | 39 ++ doc/OnlineDocs/src/scripting/spy4scripts.py | 220 ++++++++++++ doc/OnlineDocs/src/strip_examples.py | 80 +++++ doc/OnlineDocs/src/test_examples.py | 76 ++++ 238 files changed, 5494 insertions(+) create mode 100644 doc/OnlineDocs/src/data/A.tab create mode 100644 doc/OnlineDocs/src/data/ABCD.tab create mode 100644 doc/OnlineDocs/src/data/ABCD.txt create mode 100755 doc/OnlineDocs/src/data/ABCD.xls create mode 100644 doc/OnlineDocs/src/data/ABCD1.dat create mode 100644 doc/OnlineDocs/src/data/ABCD1.py create mode 100644 doc/OnlineDocs/src/data/ABCD1.txt create mode 100644 doc/OnlineDocs/src/data/ABCD2.dat create mode 100644 doc/OnlineDocs/src/data/ABCD2.py create mode 100644 doc/OnlineDocs/src/data/ABCD2.txt create mode 100644 doc/OnlineDocs/src/data/ABCD3.dat create mode 100644 doc/OnlineDocs/src/data/ABCD3.py create mode 100644 doc/OnlineDocs/src/data/ABCD3.txt create mode 100644 doc/OnlineDocs/src/data/ABCD4.dat create mode 100644 doc/OnlineDocs/src/data/ABCD4.py create mode 100644 doc/OnlineDocs/src/data/ABCD4.txt create mode 100644 doc/OnlineDocs/src/data/ABCD5.dat create mode 100644 doc/OnlineDocs/src/data/ABCD5.py create mode 100644 doc/OnlineDocs/src/data/ABCD5.txt create mode 100644 doc/OnlineDocs/src/data/ABCD6.dat create mode 100644 doc/OnlineDocs/src/data/ABCD6.py create mode 100644 doc/OnlineDocs/src/data/ABCD6.txt create mode 100644 doc/OnlineDocs/src/data/ABCD7.dat create mode 100644 doc/OnlineDocs/src/data/ABCD7.py create mode 100644 doc/OnlineDocs/src/data/ABCD7.txt create mode 100644 doc/OnlineDocs/src/data/ABCD8.bad create mode 100644 doc/OnlineDocs/src/data/ABCD8.dat create mode 100644 doc/OnlineDocs/src/data/ABCD8.py create mode 100644 doc/OnlineDocs/src/data/ABCD9.bad create mode 100644 doc/OnlineDocs/src/data/ABCD9.dat create mode 100644 doc/OnlineDocs/src/data/ABCD9.py create mode 100644 doc/OnlineDocs/src/data/C.tab create mode 100644 doc/OnlineDocs/src/data/D.tab create mode 100644 doc/OnlineDocs/src/data/U.tab create mode 100644 doc/OnlineDocs/src/data/Y.tab create mode 100644 doc/OnlineDocs/src/data/Z.tab create mode 100644 doc/OnlineDocs/src/data/data_managers.txt create mode 100644 doc/OnlineDocs/src/data/diet.dat create mode 100644 doc/OnlineDocs/src/data/diet.sql create mode 100644 doc/OnlineDocs/src/data/diet.sqlite create mode 100644 doc/OnlineDocs/src/data/diet.sqlite.dat create mode 100644 doc/OnlineDocs/src/data/diet1.py create mode 100644 doc/OnlineDocs/src/data/ex.dat create mode 100644 doc/OnlineDocs/src/data/ex.py create mode 100644 doc/OnlineDocs/src/data/ex.txt create mode 100644 doc/OnlineDocs/src/data/ex1.dat create mode 100644 doc/OnlineDocs/src/data/ex2.dat create mode 100644 doc/OnlineDocs/src/data/import1.tab.dat create mode 100644 doc/OnlineDocs/src/data/import1.tab.py create mode 100644 doc/OnlineDocs/src/data/import1.tab.txt create mode 100644 doc/OnlineDocs/src/data/import2.tab.dat create mode 100644 doc/OnlineDocs/src/data/import2.tab.py create mode 100644 doc/OnlineDocs/src/data/import2.tab.txt create mode 100644 doc/OnlineDocs/src/data/import3.tab.dat create mode 100644 doc/OnlineDocs/src/data/import3.tab.py create mode 100644 doc/OnlineDocs/src/data/import3.tab.txt create mode 100644 doc/OnlineDocs/src/data/import4.tab.dat create mode 100644 doc/OnlineDocs/src/data/import4.tab.py create mode 100644 doc/OnlineDocs/src/data/import4.tab.txt create mode 100644 doc/OnlineDocs/src/data/import5.tab.dat create mode 100644 doc/OnlineDocs/src/data/import5.tab.py create mode 100644 doc/OnlineDocs/src/data/import5.tab.txt create mode 100644 doc/OnlineDocs/src/data/import6.tab.dat create mode 100644 doc/OnlineDocs/src/data/import6.tab.py create mode 100644 doc/OnlineDocs/src/data/import6.tab.txt create mode 100644 doc/OnlineDocs/src/data/import7.tab.dat create mode 100644 doc/OnlineDocs/src/data/import7.tab.py create mode 100644 doc/OnlineDocs/src/data/import7.tab.txt create mode 100644 doc/OnlineDocs/src/data/import8.tab.dat create mode 100644 doc/OnlineDocs/src/data/import8.tab.py create mode 100644 doc/OnlineDocs/src/data/import8.tab.txt create mode 100644 doc/OnlineDocs/src/data/namespace1.dat create mode 100644 doc/OnlineDocs/src/data/param1.dat create mode 100644 doc/OnlineDocs/src/data/param1.py create mode 100644 doc/OnlineDocs/src/data/param1.txt create mode 100644 doc/OnlineDocs/src/data/param2.dat create mode 100644 doc/OnlineDocs/src/data/param2.py create mode 100644 doc/OnlineDocs/src/data/param2.txt create mode 100644 doc/OnlineDocs/src/data/param2a.dat create mode 100644 doc/OnlineDocs/src/data/param2a.py create mode 100644 doc/OnlineDocs/src/data/param2a.txt create mode 100644 doc/OnlineDocs/src/data/param3.dat create mode 100644 doc/OnlineDocs/src/data/param3.py create mode 100644 doc/OnlineDocs/src/data/param3.txt create mode 100644 doc/OnlineDocs/src/data/param3a.dat create mode 100644 doc/OnlineDocs/src/data/param3a.py create mode 100644 doc/OnlineDocs/src/data/param3a.txt create mode 100644 doc/OnlineDocs/src/data/param3b.dat create mode 100644 doc/OnlineDocs/src/data/param3b.py create mode 100644 doc/OnlineDocs/src/data/param3b.txt create mode 100644 doc/OnlineDocs/src/data/param3c.dat create mode 100644 doc/OnlineDocs/src/data/param3c.py create mode 100644 doc/OnlineDocs/src/data/param3c.txt create mode 100644 doc/OnlineDocs/src/data/param4.dat create mode 100644 doc/OnlineDocs/src/data/param4.py create mode 100644 doc/OnlineDocs/src/data/param4.txt create mode 100644 doc/OnlineDocs/src/data/param5.dat create mode 100644 doc/OnlineDocs/src/data/param5.py create mode 100644 doc/OnlineDocs/src/data/param5.txt create mode 100644 doc/OnlineDocs/src/data/param5a.dat create mode 100644 doc/OnlineDocs/src/data/param5a.py create mode 100644 doc/OnlineDocs/src/data/param5a.txt create mode 100644 doc/OnlineDocs/src/data/param6.dat create mode 100644 doc/OnlineDocs/src/data/param6.py create mode 100644 doc/OnlineDocs/src/data/param6.txt create mode 100644 doc/OnlineDocs/src/data/param6a.dat create mode 100644 doc/OnlineDocs/src/data/param6a.py create mode 100644 doc/OnlineDocs/src/data/param6a.txt create mode 100644 doc/OnlineDocs/src/data/param7a.dat create mode 100644 doc/OnlineDocs/src/data/param7a.py create mode 100644 doc/OnlineDocs/src/data/param7a.txt create mode 100644 doc/OnlineDocs/src/data/param7b.dat create mode 100644 doc/OnlineDocs/src/data/param7b.py create mode 100644 doc/OnlineDocs/src/data/param7b.txt create mode 100644 doc/OnlineDocs/src/data/param8a.dat create mode 100644 doc/OnlineDocs/src/data/param8a.py create mode 100644 doc/OnlineDocs/src/data/param8a.txt create mode 100755 doc/OnlineDocs/src/data/pyomo.diet1.sh create mode 100644 doc/OnlineDocs/src/data/pyomo.diet1.txt create mode 100755 doc/OnlineDocs/src/data/pyomo.diet2.sh create mode 100644 doc/OnlineDocs/src/data/pyomo.diet2.txt create mode 100644 doc/OnlineDocs/src/data/set1.dat create mode 100644 doc/OnlineDocs/src/data/set1.py create mode 100644 doc/OnlineDocs/src/data/set1.txt create mode 100644 doc/OnlineDocs/src/data/set2.dat create mode 100644 doc/OnlineDocs/src/data/set2.py create mode 100644 doc/OnlineDocs/src/data/set2.txt create mode 100644 doc/OnlineDocs/src/data/set2a.dat create mode 100644 doc/OnlineDocs/src/data/set2a.py create mode 100644 doc/OnlineDocs/src/data/set2a.txt create mode 100644 doc/OnlineDocs/src/data/set3.dat create mode 100644 doc/OnlineDocs/src/data/set3.py create mode 100644 doc/OnlineDocs/src/data/set3.txt create mode 100644 doc/OnlineDocs/src/data/set4.dat create mode 100644 doc/OnlineDocs/src/data/set4.py create mode 100644 doc/OnlineDocs/src/data/set4.txt create mode 100644 doc/OnlineDocs/src/data/set5.dat create mode 100644 doc/OnlineDocs/src/data/set5.py create mode 100644 doc/OnlineDocs/src/data/set5.txt create mode 100644 doc/OnlineDocs/src/data/table0.dat create mode 100644 doc/OnlineDocs/src/data/table0.py create mode 100644 doc/OnlineDocs/src/data/table0.txt create mode 100644 doc/OnlineDocs/src/data/table0.ul.dat create mode 100644 doc/OnlineDocs/src/data/table0.ul.py create mode 100644 doc/OnlineDocs/src/data/table0.ul.txt create mode 100644 doc/OnlineDocs/src/data/table1.dat create mode 100644 doc/OnlineDocs/src/data/table1.py create mode 100644 doc/OnlineDocs/src/data/table1.txt create mode 100644 doc/OnlineDocs/src/data/table2.dat create mode 100644 doc/OnlineDocs/src/data/table2.py create mode 100644 doc/OnlineDocs/src/data/table2.txt create mode 100644 doc/OnlineDocs/src/data/table3.dat create mode 100644 doc/OnlineDocs/src/data/table3.py create mode 100644 doc/OnlineDocs/src/data/table3.txt create mode 100644 doc/OnlineDocs/src/data/table3.ul.dat create mode 100644 doc/OnlineDocs/src/data/table3.ul.py create mode 100644 doc/OnlineDocs/src/data/table3.ul.txt create mode 100644 doc/OnlineDocs/src/data/table4.dat create mode 100644 doc/OnlineDocs/src/data/table4.py create mode 100644 doc/OnlineDocs/src/data/table4.txt create mode 100644 doc/OnlineDocs/src/data/table4.ul.dat create mode 100644 doc/OnlineDocs/src/data/table4.ul.py create mode 100644 doc/OnlineDocs/src/data/table4.ul.txt create mode 100644 doc/OnlineDocs/src/data/table5.dat create mode 100644 doc/OnlineDocs/src/data/table5.py create mode 100644 doc/OnlineDocs/src/data/table5.txt create mode 100644 doc/OnlineDocs/src/data/table6.dat create mode 100644 doc/OnlineDocs/src/data/table6.py create mode 100644 doc/OnlineDocs/src/data/table6.txt create mode 100644 doc/OnlineDocs/src/data/table7.dat create mode 100644 doc/OnlineDocs/src/data/table7.py create mode 100644 doc/OnlineDocs/src/data/table7.txt create mode 100644 doc/OnlineDocs/src/dataportal/A.tab create mode 100644 doc/OnlineDocs/src/dataportal/C.tab create mode 100644 doc/OnlineDocs/src/dataportal/D.tab create mode 100644 doc/OnlineDocs/src/dataportal/PP.csv create mode 100644 doc/OnlineDocs/src/dataportal/PP.json create mode 100644 doc/OnlineDocs/src/dataportal/PP.sqlite create mode 100644 doc/OnlineDocs/src/dataportal/PP.tab create mode 100644 doc/OnlineDocs/src/dataportal/PP.xml create mode 100644 doc/OnlineDocs/src/dataportal/PP.yaml create mode 100644 doc/OnlineDocs/src/dataportal/PP_sqlite.py create mode 100644 doc/OnlineDocs/src/dataportal/Pyomo_mysql create mode 100644 doc/OnlineDocs/src/dataportal/S.tab create mode 100644 doc/OnlineDocs/src/dataportal/T.json create mode 100644 doc/OnlineDocs/src/dataportal/T.yaml create mode 100644 doc/OnlineDocs/src/dataportal/U.tab create mode 100644 doc/OnlineDocs/src/dataportal/XW.tab create mode 100644 doc/OnlineDocs/src/dataportal/Y.tab create mode 100644 doc/OnlineDocs/src/dataportal/Z.tab create mode 100644 doc/OnlineDocs/src/dataportal/dataportal_tab.py create mode 100644 doc/OnlineDocs/src/dataportal/dataportal_tab.txt create mode 100644 doc/OnlineDocs/src/dataportal/excel.xls create mode 100644 doc/OnlineDocs/src/dataportal/param_initialization.py create mode 100644 doc/OnlineDocs/src/dataportal/param_initialization.txt create mode 100644 doc/OnlineDocs/src/dataportal/set_initialization.py create mode 100644 doc/OnlineDocs/src/dataportal/set_initialization.txt create mode 100644 doc/OnlineDocs/src/expr/design.py create mode 100644 doc/OnlineDocs/src/expr/design.txt create mode 100644 doc/OnlineDocs/src/expr/index.py create mode 100644 doc/OnlineDocs/src/expr/index.txt create mode 100644 doc/OnlineDocs/src/expr/managing.py create mode 100644 doc/OnlineDocs/src/expr/managing.txt create mode 100644 doc/OnlineDocs/src/expr/overview.py create mode 100644 doc/OnlineDocs/src/expr/overview.txt create mode 100644 doc/OnlineDocs/src/expr/performance.py create mode 100644 doc/OnlineDocs/src/expr/performance.txt create mode 100644 doc/OnlineDocs/src/expr/quicksum.log create mode 100644 doc/OnlineDocs/src/expr/quicksum.py create mode 100755 doc/OnlineDocs/src/kernel/examples.sh create mode 100644 doc/OnlineDocs/src/kernel/examples.txt create mode 100644 doc/OnlineDocs/src/scripting/AbstractSuffixes.py create mode 100644 doc/OnlineDocs/src/scripting/Isinglebuild.py create mode 100644 doc/OnlineDocs/src/scripting/Isinglecomm.dat create mode 100644 doc/OnlineDocs/src/scripting/NodesIn_init.py create mode 100644 doc/OnlineDocs/src/scripting/Z_init.py create mode 100644 doc/OnlineDocs/src/scripting/abstract1.dat create mode 100644 doc/OnlineDocs/src/scripting/abstract2.dat create mode 100644 doc/OnlineDocs/src/scripting/abstract2.py create mode 100644 doc/OnlineDocs/src/scripting/abstract2a.dat create mode 100644 doc/OnlineDocs/src/scripting/abstract2piece.py create mode 100644 doc/OnlineDocs/src/scripting/abstract2piecebuild.py create mode 100644 doc/OnlineDocs/src/scripting/block_iter_example.py create mode 100644 doc/OnlineDocs/src/scripting/concrete1.py create mode 100644 doc/OnlineDocs/src/scripting/doubleA.py create mode 100644 doc/OnlineDocs/src/scripting/driveabs2.py create mode 100644 doc/OnlineDocs/src/scripting/driveconc1.py create mode 100644 doc/OnlineDocs/src/scripting/iterative1.py create mode 100644 doc/OnlineDocs/src/scripting/iterative2.py create mode 100644 doc/OnlineDocs/src/scripting/noiteration1.py create mode 100644 doc/OnlineDocs/src/scripting/parallel.py create mode 100644 doc/OnlineDocs/src/scripting/spy4Constraints.py create mode 100644 doc/OnlineDocs/src/scripting/spy4Expressions.py create mode 100644 doc/OnlineDocs/src/scripting/spy4PyomoCommand.py create mode 100644 doc/OnlineDocs/src/scripting/spy4Variables.py create mode 100644 doc/OnlineDocs/src/scripting/spy4scripts.py create mode 100644 doc/OnlineDocs/src/strip_examples.py create mode 100644 doc/OnlineDocs/src/test_examples.py diff --git a/doc/OnlineDocs/src/data/A.tab b/doc/OnlineDocs/src/data/A.tab new file mode 100644 index 00000000000..d9c13cd6ac6 --- /dev/null +++ b/doc/OnlineDocs/src/data/A.tab @@ -0,0 +1,4 @@ +A +A1 +A2 +A3 diff --git a/doc/OnlineDocs/src/data/ABCD.tab b/doc/OnlineDocs/src/data/ABCD.tab new file mode 100644 index 00000000000..d820d78eade --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD.tab @@ -0,0 +1,4 @@ +A B C D +A1 B1 1 10 +A2 B2 2 20 +A3 B3 3 30 diff --git a/doc/OnlineDocs/src/data/ABCD.txt b/doc/OnlineDocs/src/data/ABCD.txt new file mode 100644 index 00000000000..ccaf3bff1c1 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD.txt @@ -0,0 +1,4 @@ +A,B,C,D +A1,B1,1,10 +A2,B2,2,20 +A3,B3,3,30 diff --git a/doc/OnlineDocs/src/data/ABCD.xls b/doc/OnlineDocs/src/data/ABCD.xls new file mode 100755 index 0000000000000000000000000000000000000000..98d9673b5c5b0ff8518987c6f5e7fb61d9a528cc GIT binary patch literal 16384 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 literal 0 HcmV?d00001 diff --git a/doc/OnlineDocs/src/data/ABCD1.dat b/doc/OnlineDocs/src/data/ABCD1.dat new file mode 100644 index 00000000000..695a16dd4c3 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD1.dat @@ -0,0 +1 @@ +load ABCD.tab format=set : Z ; diff --git a/doc/OnlineDocs/src/data/ABCD1.py b/doc/OnlineDocs/src/data/ABCD1.py new file mode 100644 index 00000000000..aa2f46e71fa --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD1.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.Z = Set(dimen=4) + +instance = model.create_instance('ABCD1.dat') + +print(sorted(list(instance.Z.data()))) diff --git a/doc/OnlineDocs/src/data/ABCD1.txt b/doc/OnlineDocs/src/data/ABCD1.txt new file mode 100644 index 00000000000..6a34f8295a7 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD1.txt @@ -0,0 +1 @@ +[('A1', 'B1', 1, 10), ('A2', 'B2', 2, 20), ('A3', 'B3', 3, 30)] diff --git a/doc/OnlineDocs/src/data/ABCD2.dat b/doc/OnlineDocs/src/data/ABCD2.dat new file mode 100644 index 00000000000..c7d85665d4b --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD2.dat @@ -0,0 +1 @@ +load ABCD.tab : [A,B,C] D ; diff --git a/doc/OnlineDocs/src/data/ABCD2.py b/doc/OnlineDocs/src/data/ABCD2.py new file mode 100644 index 00000000000..ec0e7ccb15c --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD2.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.Z = Set(initialize=[('A1', 'B1', 1), ('A2', 'B2', 2), ('A3', 'B3', 3)]) +# model.Z = Set(dimen=3) +model.D = Param(model.Z) + +instance = model.create_instance('ABCD2.dat') + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('D') +for key in sorted(instance.D.keys()): + print(name(instance.D, key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/ABCD2.txt b/doc/OnlineDocs/src/data/ABCD2.txt new file mode 100644 index 00000000000..cb7abaec039 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD2.txt @@ -0,0 +1,5 @@ +Z [('A1', 'B1', 1), ('A2', 'B2', 2), ('A3', 'B3', 3)] +D +D[A1,B1,1] 10 +D[A2,B2,2] 20 +D[A3,B3,3] 30 diff --git a/doc/OnlineDocs/src/data/ABCD3.dat b/doc/OnlineDocs/src/data/ABCD3.dat new file mode 100644 index 00000000000..c48c82133ce --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD3.dat @@ -0,0 +1 @@ +load ABCD.tab : Z=[A,B,C] D ; diff --git a/doc/OnlineDocs/src/data/ABCD3.py b/doc/OnlineDocs/src/data/ABCD3.py new file mode 100644 index 00000000000..ba55fd970cc --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD3.py @@ -0,0 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.Z = Set(dimen=3) +model.D = Param(model.Z) + +instance = model.create_instance('ABCD3.dat') + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('D') +for key in sorted(instance.D.keys()): + print(name(instance.D, key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/ABCD3.txt b/doc/OnlineDocs/src/data/ABCD3.txt new file mode 100644 index 00000000000..cb7abaec039 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD3.txt @@ -0,0 +1,5 @@ +Z [('A1', 'B1', 1), ('A2', 'B2', 2), ('A3', 'B3', 3)] +D +D[A1,B1,1] 10 +D[A2,B2,2] 20 +D[A3,B3,3] 30 diff --git a/doc/OnlineDocs/src/data/ABCD4.dat b/doc/OnlineDocs/src/data/ABCD4.dat new file mode 100644 index 00000000000..b6a4f963ab7 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD4.dat @@ -0,0 +1 @@ +load ABCD.tab : Z=[A,B,C] Y=D ; diff --git a/doc/OnlineDocs/src/data/ABCD4.py b/doc/OnlineDocs/src/data/ABCD4.py new file mode 100644 index 00000000000..2fb397aa3b0 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD4.py @@ -0,0 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.Z = Set(dimen=3) +model.Y = Param(model.Z) + +instance = model.create_instance('ABCD4.dat') + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('Y') +for key in sorted(instance.Y.keys()): + print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/src/data/ABCD4.txt b/doc/OnlineDocs/src/data/ABCD4.txt new file mode 100644 index 00000000000..f316499bca7 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD4.txt @@ -0,0 +1,5 @@ +Z [('A1', 'B1', 1), ('A2', 'B2', 2), ('A3', 'B3', 3)] +Y +Y[A1,B1,1] 10 +Y[A2,B2,2] 20 +Y[A3,B3,3] 30 diff --git a/doc/OnlineDocs/src/data/ABCD5.dat b/doc/OnlineDocs/src/data/ABCD5.dat new file mode 100644 index 00000000000..df2a1d82afb --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD5.dat @@ -0,0 +1 @@ +load ABCD.tab : Z=[B] Y=D W=C; diff --git a/doc/OnlineDocs/src/data/ABCD5.py b/doc/OnlineDocs/src/data/ABCD5.py new file mode 100644 index 00000000000..abc03505e96 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD5.py @@ -0,0 +1,30 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.Z = Set() +model.Y = Param(model.Z) +model.W = Param(model.Z) +# @decl + +instance = model.create_instance('ABCD5.dat') + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('Y') +for key in sorted(instance.Y.keys()): + print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) +print('W') +for key in sorted(instance.W.keys()): + print(name(instance.W, key) + " " + str(value(instance.W[key]))) diff --git a/doc/OnlineDocs/src/data/ABCD5.txt b/doc/OnlineDocs/src/data/ABCD5.txt new file mode 100644 index 00000000000..18ae0f1c28b --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD5.txt @@ -0,0 +1,9 @@ +Z ['B1', 'B2', 'B3'] +Y +Y[B1] 10 +Y[B2] 20 +Y[B3] 30 +W +W[B1] 1 +W[B2] 2 +W[B3] 3 diff --git a/doc/OnlineDocs/src/data/ABCD6.dat b/doc/OnlineDocs/src/data/ABCD6.dat new file mode 100644 index 00000000000..12ca0553325 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD6.dat @@ -0,0 +1 @@ +load ABCD.txt using=csv : Z=[A,B,C] D ; diff --git a/doc/OnlineDocs/src/data/ABCD6.py b/doc/OnlineDocs/src/data/ABCD6.py new file mode 100644 index 00000000000..59e0e8e98ae --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD6.py @@ -0,0 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.Z = Set(dimen=3) +model.D = Param(model.Z) + +instance = model.create_instance('ABCD6.dat') + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('D') +for key in sorted(instance.D.keys()): + print(name(instance.D, key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/ABCD6.txt b/doc/OnlineDocs/src/data/ABCD6.txt new file mode 100644 index 00000000000..cb7abaec039 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD6.txt @@ -0,0 +1,5 @@ +Z [('A1', 'B1', 1), ('A2', 'B2', 2), ('A3', 'B3', 3)] +D +D[A1,B1,1] 10 +D[A2,B2,2] 20 +D[A3,B3,3] 30 diff --git a/doc/OnlineDocs/src/data/ABCD7.dat b/doc/OnlineDocs/src/data/ABCD7.dat new file mode 100644 index 00000000000..f99b24c575f --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD7.dat @@ -0,0 +1 @@ +load ABCD.xls range=ABCD : Z=[A,B,C] Y=D ; diff --git a/doc/OnlineDocs/src/data/ABCD7.py b/doc/OnlineDocs/src/data/ABCD7.py new file mode 100644 index 00000000000..1bfb4d1e3fb --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD7.py @@ -0,0 +1,30 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * +import pyomo.common +import sys + +model = AbstractModel() + +model.Z = Set(dimen=3) +model.Y = Param(model.Z) + +try: + instance = model.create_instance('ABCD7.dat') +except pyomo.common.errors.ApplicationError as e: + print("ERROR " + str(e)) + sys.exit(1) + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('Y') +for key in sorted(instance.Y.keys()): + print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/src/data/ABCD7.txt b/doc/OnlineDocs/src/data/ABCD7.txt new file mode 100644 index 00000000000..aefb9bc167a --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD7.txt @@ -0,0 +1,5 @@ +Z [('A1', 'B1', 1.0), ('A2', 'B2', 2.0), ('A3', 'B3', 3.0)] +Y +Y[A1,B1,1.0] 10.0 +Y[A2,B2,2.0] 20.0 +Y[A3,B3,3.0] 30.0 diff --git a/doc/OnlineDocs/src/data/ABCD8.bad b/doc/OnlineDocs/src/data/ABCD8.bad new file mode 100644 index 00000000000..532fb841f32 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD8.bad @@ -0,0 +1 @@ +ERROR Cannot create data manager 'pyodbc' diff --git a/doc/OnlineDocs/src/data/ABCD8.dat b/doc/OnlineDocs/src/data/ABCD8.dat new file mode 100644 index 00000000000..220a67e5602 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD8.dat @@ -0,0 +1 @@ +load ABCD.xls using=pyodbc table=ABCD : Z=[A,B,C] Y=D ; diff --git a/doc/OnlineDocs/src/data/ABCD8.py b/doc/OnlineDocs/src/data/ABCD8.py new file mode 100644 index 00000000000..aa1ba0b4cf5 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD8.py @@ -0,0 +1,30 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * +import pyomo.common +import sys + +model = AbstractModel() + +model.Z = Set(dimen=3) +model.Y = Param(model.Z) + +try: + instance = model.create_instance('ABCD8.dat') +except pyomo.common.errors.ApplicationError as e: + print("ERROR " + str(e)) + sys.exit(1) + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('Y') +for key in sorted(instance.Y.keys()): + print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/src/data/ABCD9.bad b/doc/OnlineDocs/src/data/ABCD9.bad new file mode 100644 index 00000000000..532fb841f32 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD9.bad @@ -0,0 +1 @@ +ERROR Cannot create data manager 'pyodbc' diff --git a/doc/OnlineDocs/src/data/ABCD9.dat b/doc/OnlineDocs/src/data/ABCD9.dat new file mode 100644 index 00000000000..2c3688c6200 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD9.dat @@ -0,0 +1,3 @@ +load "Driver={Microsoft Excel Driver (*.xls)}; Dbq=ABCD.xls;" + using=pyodbc + table=ABCD : Z=[A,B,C] Y=D ; diff --git a/doc/OnlineDocs/src/data/ABCD9.py b/doc/OnlineDocs/src/data/ABCD9.py new file mode 100644 index 00000000000..194c71486d9 --- /dev/null +++ b/doc/OnlineDocs/src/data/ABCD9.py @@ -0,0 +1,30 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * +import pyomo.common +import sys + +model = AbstractModel() + +model.Z = Set(dimen=3) +model.Y = Param(model.Z) + +try: + instance = model.create_instance('ABCD9.dat') +except pyomo.common.errors.ApplicationError as e: + print("ERROR " + str(e)) + sys.exit(1) + +print('Z ' + str(sorted(list(instance.Z.data())))) +print('Y') +for key in sorted(instance.Y.keys()): + print(instance.Y[key] + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/src/data/C.tab b/doc/OnlineDocs/src/data/C.tab new file mode 100644 index 00000000000..14bcdc5e18d --- /dev/null +++ b/doc/OnlineDocs/src/data/C.tab @@ -0,0 +1,10 @@ +A B +A1 1 +A1 2 +A1 3 +A2 1 +A2 2 +A2 3 +A3 1 +A3 2 +A3 3 diff --git a/doc/OnlineDocs/src/data/D.tab b/doc/OnlineDocs/src/data/D.tab new file mode 100644 index 00000000000..965d28df3d0 --- /dev/null +++ b/doc/OnlineDocs/src/data/D.tab @@ -0,0 +1,4 @@ +B A1 A2 A3 +1 + - - +2 - + - +3 - - + diff --git a/doc/OnlineDocs/src/data/U.tab b/doc/OnlineDocs/src/data/U.tab new file mode 100644 index 00000000000..27bba1b34da --- /dev/null +++ b/doc/OnlineDocs/src/data/U.tab @@ -0,0 +1,5 @@ +I A1 A2 A3 +I1 1.3 2.3 3.3 +I2 1.4 2.4 3.4 +I3 1.5 2.5 3.5 +I4 1.6 2.6 3.6 diff --git a/doc/OnlineDocs/src/data/Y.tab b/doc/OnlineDocs/src/data/Y.tab new file mode 100644 index 00000000000..926555713f0 --- /dev/null +++ b/doc/OnlineDocs/src/data/Y.tab @@ -0,0 +1,4 @@ +A Y +A1 3.3 +A2 3.4 +A3 3.5 diff --git a/doc/OnlineDocs/src/data/Z.tab b/doc/OnlineDocs/src/data/Z.tab new file mode 100644 index 00000000000..9459d4ba2a0 --- /dev/null +++ b/doc/OnlineDocs/src/data/Z.tab @@ -0,0 +1 @@ +1.1 diff --git a/doc/OnlineDocs/src/data/data_managers.txt b/doc/OnlineDocs/src/data/data_managers.txt new file mode 100644 index 00000000000..dd508dd97b5 --- /dev/null +++ b/doc/OnlineDocs/src/data/data_managers.txt @@ -0,0 +1,30 @@ +Pyomo Data Managers +------------------- + csv + CSV file interface + dat + Pyomo data command file interface + json + JSON file interface + pymysql + pymysql database interface + pyodbc + pyodbc database interface + pypyodbc + pypyodbc database interface + sqlite3 + sqlite3 database interface + tab + TAB file interface + xls + Excel XLS file interface + xlsb + Excel XLSB file interface + xlsm + Excel XLSM file interface + xlsx + Excel XLSX file interface + xml + XML file interface + yaml + YAML file interface diff --git a/doc/OnlineDocs/src/data/diet.dat b/doc/OnlineDocs/src/data/diet.dat new file mode 100644 index 00000000000..6d50961842c --- /dev/null +++ b/doc/OnlineDocs/src/data/diet.dat @@ -0,0 +1,33 @@ +# File diet.dat + +param: FOOD: cost f_min f_max := + "Cheeseburger" 1.84 . . + "Ham Sandwich" 2.19 . . + "Hamburger" 1.84 . . + "Fish Sandwich" 1.44 . . + "Chicken Sandwich" 2.29 . . + "Fries" .77 . . + "Sausage Biscuit" 1.29 . . + "Lowfat Milk" .60 . . + "Orange Juice" .72 . . ; + +param: NUTR: n_min n_max := + Cal 2000 . + Carbo 350 375 + Protein 55 . + VitA 100 . + VitC 100 . + Calc 100 . + Iron 100 . ; + +param amt (tr): + Cal Carbo Protein VitA VitC Calc Iron := + "Cheeseburger" 510 34 28 15 6 30 20 + "Ham Sandwich" 370 35 24 15 10 20 20 + "Hamburger" 500 42 25 6 2 25 20 + "Fish Sandwich" 370 38 14 2 0 15 10 + "Chicken Sandwich" 400 42 31 8 15 15 8 + "Fries" 220 26 3 0 15 0 2 + "Sausage Biscuit" 345 27 15 4 0 20 15 + "Lowfat Milk" 110 12 9 10 4 30 0 + "Orange Juice" 80 20 1 2 120 2 2 ; diff --git a/doc/OnlineDocs/src/data/diet.sql b/doc/OnlineDocs/src/data/diet.sql new file mode 100644 index 00000000000..b97403df819 --- /dev/null +++ b/doc/OnlineDocs/src/data/diet.sql @@ -0,0 +1,96 @@ +DROP TABLE IF EXISTS Amount; +DROP TABLE IF EXISTS Nutr; +DROP TABLE IF EXISTS Food; + +CREATE TABLE Food ( + FOOD varchar(64) not null, + cost float not null, + f_min float, + f_max float, + primary key (FOOD) + ) engine=innodb ; + +INSERT INTO Food VALUES ("Cheeseburger", 1.84, NULL, NULL), ("Ham Sandwich", 2.19, NULL, NULL), ("Hamburger", 1.84, NULL, NULL), ("Fish Sandwich", 1.44, NULL, NULL), ("Chicken Sandwich", 2.29, NULL, NULL), ("Fries", 0.77, NULL, NULL), ("Sausage Biscuit", 1.29, NULL, NULL), ("Lowfat Milk", 0.60, NULL, NULL), ("Orange Juice", 0.72, NULL, NULL); + +CREATE TABLE Nutr ( + NUTR varchar(64) not null, + n_min float, + n_max float, + primary key (NUTR) + ) engine=innodb ; + +INSERT INTO Nutr VALUES ("Cal", 2000.0, NULL), ("Carbo", 350.0, 375.0), ("Protein", 55.0, NULL), ("VitA", 100.0, NULL), ("VitC", 100.0, NULL), ("Calc", 100.0, NULL), ("Iron", 100.0, NULL); + +CREATE TABLE Amount ( + NUTR varchar(64) not null, + FOOD varchar(64) not null, + amt float not null, + primary key (NUTR, FOOD), + foreign key (NUTR) references Nutr (NUTR), + foreign key (FOOD) references Food (FOOD) + ) engine=innodb ; + +INSERT INTO Amount VALUES + ('Cal','Cheeseburger','510'), + ('Carbo','Cheeseburger','34'), + ('Protein','Cheeseburger','28'), + ('VitA','Cheeseburger','15'), + ('VitC','Cheeseburger','6'), + ('Calc','Cheeseburger','30'), + ('Iron','Cheeseburger','20'), + ('Cal','Ham Sandwich','370'), + ('Carbo','Ham Sandwich','35'), + ('Protein','Ham Sandwich','24'), + ('VitA','Ham Sandwich','15'), + ('VitC','Ham Sandwich','10'), + ('Calc','Ham Sandwich','20'), + ('Iron','Ham Sandwich','20'), + ('Cal','Hamburger','500'), + ('Carbo','Hamburger','42'), + ('Protein','Hamburger','25'), + ('VitA','Hamburger','6'), + ('VitC','Hamburger','2'), + ('Calc','Hamburger','25'), + ('Iron','Hamburger','20'), + ('Cal','Fish Sandwich','370'), + ('Carbo','Fish Sandwich','38'), + ('Protein','Fish Sandwich','14'), + ('VitA','Fish Sandwich','2'), + ('VitC','Fish Sandwich','0'), + ('Calc','Fish Sandwich','15'), + ('Iron','Fish Sandwich','10'), + ('Cal','Chicken Sandwich','400'), + ('Carbo','Chicken Sandwich','42'), + ('Protein','Chicken Sandwich','31'), + ('VitA','Chicken Sandwich','8'), + ('VitC','Chicken Sandwich','15'), + ('Calc','Chicken Sandwich','15'), + ('Iron','Chicken Sandwich','8'), + ('Cal','Fries','220'), + ('Carbo','Fries','26'), + ('Protein','Fries','3'), + ('VitA','Fries','0'), + ('VitC','Fries','15'), + ('Calc','Fries','0'), + ('Iron','Fries','2'), + ('Cal','Sausage Biscuit','345'), + ('Carbo','Sausage Biscuit','27'), + ('Protein','Sausage Biscuit','15'), + ('VitA','Sausage Biscuit','4'), + ('VitC','Sausage Biscuit','0'), + ('Calc','Sausage Biscuit','20'), + ('Iron','Sausage Biscuit','15'), + ('Cal','Lowfat Milk','110'), + ('Carbo','Lowfat Milk','12'), + ('Protein','Lowfat Milk','9'), + ('VitA','Lowfat Milk','10'), + ('VitC','Lowfat Milk','4'), + ('Calc','Lowfat Milk','30'), + ('Iron','Lowfat Milk','0'), + ('Cal','Orange Juice','80'), + ('Carbo','Orange Juice','20'), + ('Protein','Orange Juice','1'), + ('VitA','Orange Juice','2'), + ('VitC','Orange Juice','120'), + ('Calc','Orange Juice','2'), + ('Iron','Orange Juice','2'); diff --git a/doc/OnlineDocs/src/data/diet.sqlite b/doc/OnlineDocs/src/data/diet.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..89e54586968b10483da7cad570ada0e5bd7513f3 GIT binary patch literal 11264 zcmeHMZERat8NTPd#~;^e;wIPE*N*dX+bnU?B;C?(?fQ|quIti5mv(7ERG4#1fs|EXfxw4w8+PgKOF{T}|ZB5g{cz5C5 zxO99FjTii9%5%HNur_^Dbi+rQpIy`7AiTz(0E1oQ#daC)RQ&2WU%-mT*;lPwXQ=y|LPad%!$yM^p zxrzekR;%{;l`?klX3|sh#~)13pS3@eJ!_9sSyMLUo}fbJv&TO*w`rNM=d(w%^Vzvf z_Ov~>UaizJc$3QPYSN?S@{&C+;n2jS-*2VjOe>a_3YXU|t>CCz$gNk)@_nJ!s)d=_ zf8YuPEYoCH&q`-dm2?Z2BWgrKHeRhn!GR(nn()2b---L zl&W2rpl1%WKX3Lx%HS{s`QHf8Enbek=aO zXJLkZ0q2_N#vdKFYVa1k1kb<|umO3nu()zDbz`@@1_HiGw4BbZujMe;;o@3;y;x1XhnZ#{ z{z;$TXQB0IrC3-?{k4V-`h3%fWtNNii-nSXI#*iSDCU@!2yeox@MX9NkKJ)y-)V5i1OH18Y=u##>WUtQ5vHg~(11zi(U5=+7L&^1 zkH9qZ>V>ZtlFX$N7;FzSM;n0ZR26}TVN_RjfY$#2Jgvd|a1(wHFT)FP9bY?F#{(S? z+-4ro0|K`t$1CO1lJ2L+Ol~E=r2FXcbH!?ANf-6U^wJg(38tN@l&gheX>T_d7U!8< zWwFf8u??LGgH!InE42Ox;CT%`fEVE(7y;aXH{jLVtl=Fx9S?Lo(8>crFc}F#fF4K= z{PaL#;Hy8RMHeJkT?l}}2`K~I4~C8zd{`=j52@^~+Fz?`I#|a89S?l+JP;h`*eC{X zQPV=TzfWE(J1y#X;7<2|9u;O%h6=9Zf&tq9k82k+cpK0CuH*UNH=qJvfFm#kef&fI zXZ|vOmOshA#UJ5|e2(wuW4wpG&)#4!u%EN_KfkqCrGg;kGLB+%b;moO~c!>q_x?GN!0 zVNS~yc~3=x@$F=yaL8*4$G4G*0;Fb>T{oq@rr^M^NK7~Qd9B0!L&Bo!aChak?T8Nw zW13idE$s2=K+7%(MH7S*dskP5&3?qmXBihBjZ<>zO7*yy$73RqY;bnv=&>weQ4L7g zWf+S_g^`p#R*PblK2w;(($~(>Qlu|J7}0dFc%MNSQFJig*H_2V4(2y|MPj(Y&4J^_ zR$)<*$jbqD^o4~nOq?8uy=V3abF{(7Iqr^_-2~SuDexA8>#Q6&WOmhIBIdy1SV$yB z6>g3t7S)AC#nyhhwm*ftyNP>#Nm51Ge`xxbR3amUpZ3VYYK}%9I*z#yMXjg4RwET9HIHYjoBLC$B*%IxSreWxBL?SCjS}# zHGhr&jXlQ}`5I(nU6Q`rbPW6)Vmv7jyWge)$Ni%WkfJ$Lf1~=0?2d7#RJHmojp{d}GlSxPi@mMEKjDw?8+aPN4eRhl zI1J-p@Q?TnjQ_9mAMwX9{y)dFd?&ZKhRHkE|Ce55^MO`@2RCOM!%|Is z**sW>T@uHw1E_I&{ft@75Z&K0fQdx*wTNI&8TVrd6C{d?o<;Vyh--!-dk7^lQ<93v zeRWh7%8+$$Yo*l9-L0adEyk|q@Te4 model.f_min[j] + + +model.f_max = Param(model.FOOD, validate=f_max_validate, default=MAX_FOOD_SUPPLY) + +model.NUTR = Set() +model.n_min = Param(model.NUTR, within=NonNegativeReals, default=0.0) +model.n_max = Param(model.NUTR, default=infinity) +model.amt = Param(model.NUTR, model.FOOD, within=NonNegativeReals) + +# -------------------------------------------------------- + + +def Buy_bounds(model, i): + return (model.f_min[i], model.f_max[i]) + + +model.Buy = Var(model.FOOD, bounds=Buy_bounds, within=NonNegativeIntegers) + +# -------------------------------------------------------- + + +def Total_Cost_rule(model): + return sum(model.cost[j] * model.Buy[j] for j in model.FOOD) + + +model.Total_Cost = Objective(rule=Total_Cost_rule, sense=minimize) + +# -------------------------------------------------------- + + +def Entree_rule(model): + entrees = [ + 'Cheeseburger', + 'Ham Sandwich', + 'Hamburger', + 'Fish Sandwich', + 'Chicken Sandwich', + ] + return sum(model.Buy[e] for e in entrees) >= 1 + + +model.Entree = Constraint(rule=Entree_rule) + + +def Side_rule(model): + sides = ['Fries', 'Sausage Biscuit'] + return sum(model.Buy[s] for s in sides) >= 1 + + +model.Side = Constraint(rule=Side_rule) + + +def Drink_rule(model): + drinks = ['Lowfat Milk', 'Orange Juice'] + return sum(model.Buy[d] for d in drinks) >= 1 + + +model.Drink = Constraint(rule=Drink_rule) diff --git a/doc/OnlineDocs/src/data/ex.dat b/doc/OnlineDocs/src/data/ex.dat new file mode 100644 index 00000000000..3b07e9348f0 --- /dev/null +++ b/doc/OnlineDocs/src/data/ex.dat @@ -0,0 +1,2 @@ +include ex1.dat; +include ex2.dat; diff --git a/doc/OnlineDocs/src/data/ex.py b/doc/OnlineDocs/src/data/ex.py new file mode 100644 index 00000000000..a66ee30b494 --- /dev/null +++ b/doc/OnlineDocs/src/data/ex.py @@ -0,0 +1,22 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.z = Param() +# @decl + +instance = model.create_instance('ex.dat') + +print(value(instance.z)) diff --git a/doc/OnlineDocs/src/data/ex.txt b/doc/OnlineDocs/src/data/ex.txt new file mode 100644 index 00000000000..0cfbf08886f --- /dev/null +++ b/doc/OnlineDocs/src/data/ex.txt @@ -0,0 +1 @@ +2 diff --git a/doc/OnlineDocs/src/data/ex1.dat b/doc/OnlineDocs/src/data/ex1.dat new file mode 100644 index 00000000000..844349d86ad --- /dev/null +++ b/doc/OnlineDocs/src/data/ex1.dat @@ -0,0 +1 @@ +param z := 1; diff --git a/doc/OnlineDocs/src/data/ex2.dat b/doc/OnlineDocs/src/data/ex2.dat new file mode 100644 index 00000000000..62002452618 --- /dev/null +++ b/doc/OnlineDocs/src/data/ex2.dat @@ -0,0 +1 @@ +param z := 2; diff --git a/doc/OnlineDocs/src/data/import1.tab.dat b/doc/OnlineDocs/src/data/import1.tab.dat new file mode 100644 index 00000000000..fd42ce549fb --- /dev/null +++ b/doc/OnlineDocs/src/data/import1.tab.dat @@ -0,0 +1 @@ +load Y.tab : [A] Y; diff --git a/doc/OnlineDocs/src/data/import1.tab.py b/doc/OnlineDocs/src/data/import1.tab.py new file mode 100644 index 00000000000..e160e4fdcde --- /dev/null +++ b/doc/OnlineDocs/src/data/import1.tab.py @@ -0,0 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set(initialize=['A1', 'A2', 'A3', 'A4']) +model.Y = Param(model.A) + +instance = model.create_instance('import1.tab.dat') + +print('Y') +keys = instance.Y.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/src/data/import1.tab.txt b/doc/OnlineDocs/src/data/import1.tab.txt new file mode 100644 index 00000000000..7b0d5e2c1c3 --- /dev/null +++ b/doc/OnlineDocs/src/data/import1.tab.txt @@ -0,0 +1,4 @@ +Y +A1 3.3 +A2 3.4 +A3 3.5 diff --git a/doc/OnlineDocs/src/data/import2.tab.dat b/doc/OnlineDocs/src/data/import2.tab.dat new file mode 100644 index 00000000000..23daab5ef75 --- /dev/null +++ b/doc/OnlineDocs/src/data/import2.tab.dat @@ -0,0 +1 @@ +load Y.tab : A=[A] Y; diff --git a/doc/OnlineDocs/src/data/import2.tab.py b/doc/OnlineDocs/src/data/import2.tab.py new file mode 100644 index 00000000000..54339551279 --- /dev/null +++ b/doc/OnlineDocs/src/data/import2.tab.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set() +model.Y = Param(model.A) + +instance = model.create_instance('import2.tab.dat') + +print('A ' + str(sorted(list(instance.A.data())))) +print('Y') +keys = instance.Y.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/src/data/import2.tab.txt b/doc/OnlineDocs/src/data/import2.tab.txt new file mode 100644 index 00000000000..814b83c8c7b --- /dev/null +++ b/doc/OnlineDocs/src/data/import2.tab.txt @@ -0,0 +1,5 @@ +A ['A1', 'A2', 'A3'] +Y +A1 3.3 +A2 3.4 +A3 3.5 diff --git a/doc/OnlineDocs/src/data/import3.tab.dat b/doc/OnlineDocs/src/data/import3.tab.dat new file mode 100644 index 00000000000..896e9478e83 --- /dev/null +++ b/doc/OnlineDocs/src/data/import3.tab.dat @@ -0,0 +1 @@ +load A.tab format=set : A; diff --git a/doc/OnlineDocs/src/data/import3.tab.py b/doc/OnlineDocs/src/data/import3.tab.py new file mode 100644 index 00000000000..664151d1438 --- /dev/null +++ b/doc/OnlineDocs/src/data/import3.tab.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set() + +instance = model.create_instance('import3.tab.dat') + +print('A ' + str(sorted(list(instance.A.data())))) diff --git a/doc/OnlineDocs/src/data/import3.tab.txt b/doc/OnlineDocs/src/data/import3.tab.txt new file mode 100644 index 00000000000..a9e1ae02cf5 --- /dev/null +++ b/doc/OnlineDocs/src/data/import3.tab.txt @@ -0,0 +1 @@ +A ['A1', 'A2', 'A3'] diff --git a/doc/OnlineDocs/src/data/import4.tab.dat b/doc/OnlineDocs/src/data/import4.tab.dat new file mode 100644 index 00000000000..986e9c4c33a --- /dev/null +++ b/doc/OnlineDocs/src/data/import4.tab.dat @@ -0,0 +1 @@ +load C.tab format=set : C; diff --git a/doc/OnlineDocs/src/data/import4.tab.py b/doc/OnlineDocs/src/data/import4.tab.py new file mode 100644 index 00000000000..91dd3f26a42 --- /dev/null +++ b/doc/OnlineDocs/src/data/import4.tab.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.C = Set(dimen=2) + +instance = model.create_instance('import4.tab.dat') + +print('C ' + str(sorted(list(instance.C.data())))) diff --git a/doc/OnlineDocs/src/data/import4.tab.txt b/doc/OnlineDocs/src/data/import4.tab.txt new file mode 100644 index 00000000000..cd8f5e6eedb --- /dev/null +++ b/doc/OnlineDocs/src/data/import4.tab.txt @@ -0,0 +1 @@ +C [('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)] diff --git a/doc/OnlineDocs/src/data/import5.tab.dat b/doc/OnlineDocs/src/data/import5.tab.dat new file mode 100644 index 00000000000..6a00436f075 --- /dev/null +++ b/doc/OnlineDocs/src/data/import5.tab.dat @@ -0,0 +1 @@ +load D.tab format=set_array: B; diff --git a/doc/OnlineDocs/src/data/import5.tab.py b/doc/OnlineDocs/src/data/import5.tab.py new file mode 100644 index 00000000000..263677c308c --- /dev/null +++ b/doc/OnlineDocs/src/data/import5.tab.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.B = Set(dimen=2) + +instance = model.create_instance('import5.tab.dat') + +print('B ' + str(list(sorted(instance.B.data())))) diff --git a/doc/OnlineDocs/src/data/import5.tab.txt b/doc/OnlineDocs/src/data/import5.tab.txt new file mode 100644 index 00000000000..e7f326ba667 --- /dev/null +++ b/doc/OnlineDocs/src/data/import5.tab.txt @@ -0,0 +1 @@ +B [('A1', 1), ('A2', 2), ('A3', 3)] diff --git a/doc/OnlineDocs/src/data/import6.tab.dat b/doc/OnlineDocs/src/data/import6.tab.dat new file mode 100644 index 00000000000..29404994306 --- /dev/null +++ b/doc/OnlineDocs/src/data/import6.tab.dat @@ -0,0 +1 @@ +load Z.tab format=param: p; diff --git a/doc/OnlineDocs/src/data/import6.tab.py b/doc/OnlineDocs/src/data/import6.tab.py new file mode 100644 index 00000000000..8f4824ad3fe --- /dev/null +++ b/doc/OnlineDocs/src/data/import6.tab.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.p = Param() + +instance = model.create_instance('import6.tab.dat') + +print('p ' + str(value(instance.p))) diff --git a/doc/OnlineDocs/src/data/import6.tab.txt b/doc/OnlineDocs/src/data/import6.tab.txt new file mode 100644 index 00000000000..ae682e9fb8f --- /dev/null +++ b/doc/OnlineDocs/src/data/import6.tab.txt @@ -0,0 +1 @@ +p 1.1 diff --git a/doc/OnlineDocs/src/data/import7.tab.dat b/doc/OnlineDocs/src/data/import7.tab.dat new file mode 100644 index 00000000000..793eac7df45 --- /dev/null +++ b/doc/OnlineDocs/src/data/import7.tab.dat @@ -0,0 +1 @@ +load U.tab format=array: A=[X] U; diff --git a/doc/OnlineDocs/src/data/import7.tab.py b/doc/OnlineDocs/src/data/import7.tab.py new file mode 100644 index 00000000000..503f9224323 --- /dev/null +++ b/doc/OnlineDocs/src/data/import7.tab.py @@ -0,0 +1,28 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.I = Set(initialize=['I1', 'I2', 'I3', 'I4']) +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.U = Param(model.I, model.A) +# BUG: This should cause an error +# model.U = Param(model.A,model.I) + +instance = model.create_instance('import7.tab.dat') + +print('I ' + str(sorted(list(instance.I.data())))) +print('A ' + str(sorted(list(instance.A.data())))) +print('U') +for key in sorted(instance.U.keys()): + print(name(instance.U, key) + " " + str(value(instance.U[key]))) diff --git a/doc/OnlineDocs/src/data/import7.tab.txt b/doc/OnlineDocs/src/data/import7.tab.txt new file mode 100644 index 00000000000..2761f5f92e6 --- /dev/null +++ b/doc/OnlineDocs/src/data/import7.tab.txt @@ -0,0 +1,15 @@ +I ['I1', 'I2', 'I3', 'I4'] +A ['A1', 'A2', 'A3'] +U +U[I1,A1] 1.3 +U[I1,A2] 2.3 +U[I1,A3] 3.3 +U[I2,A1] 1.4 +U[I2,A2] 2.4 +U[I2,A3] 3.4 +U[I3,A1] 1.5 +U[I3,A2] 2.5 +U[I3,A3] 3.5 +U[I4,A1] 1.6 +U[I4,A2] 2.6 +U[I4,A3] 3.6 diff --git a/doc/OnlineDocs/src/data/import8.tab.dat b/doc/OnlineDocs/src/data/import8.tab.dat new file mode 100644 index 00000000000..b229ac3ff8b --- /dev/null +++ b/doc/OnlineDocs/src/data/import8.tab.dat @@ -0,0 +1 @@ +load U.tab format=transposed_array: A=[X] U; diff --git a/doc/OnlineDocs/src/data/import8.tab.py b/doc/OnlineDocs/src/data/import8.tab.py new file mode 100644 index 00000000000..02b8724fe45 --- /dev/null +++ b/doc/OnlineDocs/src/data/import8.tab.py @@ -0,0 +1,26 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.I = Set(initialize=['I1', 'I2', 'I3', 'I4']) +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.U = Param(model.A, model.I) + +instance = model.create_instance('import8.tab.dat') + +print('A ' + str(sorted(list(instance.A.data())))) +print('I ' + str(sorted(list(instance.I.data())))) +print('U') +for key in sorted(instance.U.keys()): + print(name(instance.U, key) + " " + str(value(instance.U[key]))) diff --git a/doc/OnlineDocs/src/data/import8.tab.txt b/doc/OnlineDocs/src/data/import8.tab.txt new file mode 100644 index 00000000000..e401fd07818 --- /dev/null +++ b/doc/OnlineDocs/src/data/import8.tab.txt @@ -0,0 +1,15 @@ +A ['A1', 'A2', 'A3'] +I ['I1', 'I2', 'I3', 'I4'] +U +U[A1,I1] 1.3 +U[A1,I2] 1.4 +U[A1,I3] 1.5 +U[A1,I4] 1.6 +U[A2,I1] 2.3 +U[A2,I2] 2.4 +U[A2,I3] 2.5 +U[A2,I4] 2.6 +U[A3,I1] 3.3 +U[A3,I2] 3.4 +U[A3,I3] 3.5 +U[A3,I4] 3.6 diff --git a/doc/OnlineDocs/src/data/namespace1.dat b/doc/OnlineDocs/src/data/namespace1.dat new file mode 100644 index 00000000000..3a2917ef160 --- /dev/null +++ b/doc/OnlineDocs/src/data/namespace1.dat @@ -0,0 +1,12 @@ +set C := 1 2 3 ; + +namespace ns1 +{ + set C := 4 5 6 ; +} + +namespace ns2 +{ + set C := 7 8 9 ; +} + diff --git a/doc/OnlineDocs/src/data/param1.dat b/doc/OnlineDocs/src/data/param1.dat new file mode 100644 index 00000000000..38c79ba527d --- /dev/null +++ b/doc/OnlineDocs/src/data/param1.dat @@ -0,0 +1,5 @@ +param A := 1.4; +param B := 1; +param C := abc; +param D := true; +param E := 1.0e+04; diff --git a/doc/OnlineDocs/src/data/param1.py b/doc/OnlineDocs/src/data/param1.py new file mode 100644 index 00000000000..336a04287b9 --- /dev/null +++ b/doc/OnlineDocs/src/data/param1.py @@ -0,0 +1,30 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Param() +model.B = Param() +model.C = Param() +model.D = Param() +model.E = Param() +# @decl + +instance = model.create_instance('param1.dat') + +print(value(instance.A)) +print(value(instance.B)) +print(value(instance.C)) +print(value(instance.D)) +print(value(instance.E)) diff --git a/doc/OnlineDocs/src/data/param1.txt b/doc/OnlineDocs/src/data/param1.txt new file mode 100644 index 00000000000..62a63f1d5b3 --- /dev/null +++ b/doc/OnlineDocs/src/data/param1.txt @@ -0,0 +1,5 @@ +1.4 +1 +abc +True +10000.0 diff --git a/doc/OnlineDocs/src/data/param2.dat b/doc/OnlineDocs/src/data/param2.dat new file mode 100644 index 00000000000..a30794ebf06 --- /dev/null +++ b/doc/OnlineDocs/src/data/param2.dat @@ -0,0 +1,3 @@ +set A := a c e; + +param B := a 10 c 30 e 50; diff --git a/doc/OnlineDocs/src/data/param2.py b/doc/OnlineDocs/src/data/param2.py new file mode 100644 index 00000000000..a7d0feafff9 --- /dev/null +++ b/doc/OnlineDocs/src/data/param2.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set() +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param2.dat') + +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param2.txt b/doc/OnlineDocs/src/data/param2.txt new file mode 100644 index 00000000000..bc9f93115e1 --- /dev/null +++ b/doc/OnlineDocs/src/data/param2.txt @@ -0,0 +1,3 @@ +a 10 +c 30 +e 50 diff --git a/doc/OnlineDocs/src/data/param2a.dat b/doc/OnlineDocs/src/data/param2a.dat new file mode 100644 index 00000000000..1d6e1a13fbe --- /dev/null +++ b/doc/OnlineDocs/src/data/param2a.dat @@ -0,0 +1,7 @@ +set A := a c e; + +param B := +a 10 +c 30 +e 50 +; diff --git a/doc/OnlineDocs/src/data/param2a.py b/doc/OnlineDocs/src/data/param2a.py new file mode 100644 index 00000000000..42056793ffd --- /dev/null +++ b/doc/OnlineDocs/src/data/param2a.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set() +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param2a.dat') + +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param2a.txt b/doc/OnlineDocs/src/data/param2a.txt new file mode 100644 index 00000000000..bc9f93115e1 --- /dev/null +++ b/doc/OnlineDocs/src/data/param2a.txt @@ -0,0 +1,3 @@ +a 10 +c 30 +e 50 diff --git a/doc/OnlineDocs/src/data/param3.dat b/doc/OnlineDocs/src/data/param3.dat new file mode 100644 index 00000000000..c4bbf2890b2 --- /dev/null +++ b/doc/OnlineDocs/src/data/param3.dat @@ -0,0 +1,7 @@ +set A := a c e; + +param : B C D := +a 10 -1 1.1 +c 30 -3 3.3 +e 50 -5 5.5 +; diff --git a/doc/OnlineDocs/src/data/param3.py b/doc/OnlineDocs/src/data/param3.py new file mode 100644 index 00000000000..952f9a9b707 --- /dev/null +++ b/doc/OnlineDocs/src/data/param3.py @@ -0,0 +1,36 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set() +model.B = Param(model.A) +model.C = Param(model.A) +model.D = Param(model.A) +# @decl + +instance = model.create_instance('param3.dat') + +print('B') +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) +print('C') +keys = instance.C.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.C[key]))) +print('D') +keys = instance.D.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/param3.txt b/doc/OnlineDocs/src/data/param3.txt new file mode 100644 index 00000000000..55e6d2bc1b0 --- /dev/null +++ b/doc/OnlineDocs/src/data/param3.txt @@ -0,0 +1,12 @@ +B +a 10 +c 30 +e 50 +C +a -1 +c -3 +e -5 +D +a 1.1 +c 3.3 +e 5.5 diff --git a/doc/OnlineDocs/src/data/param3a.dat b/doc/OnlineDocs/src/data/param3a.dat new file mode 100644 index 00000000000..3894bf4893d --- /dev/null +++ b/doc/OnlineDocs/src/data/param3a.dat @@ -0,0 +1,7 @@ +set A := a c e g; + +param : B C D := +a 10 -1 1.1 +c 30 -3 3.3 +e 50 -5 5.5 +; diff --git a/doc/OnlineDocs/src/data/param3a.py b/doc/OnlineDocs/src/data/param3a.py new file mode 100644 index 00000000000..028e1d07296 --- /dev/null +++ b/doc/OnlineDocs/src/data/param3a.py @@ -0,0 +1,36 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set() +model.B = Param(model.A) +model.C = Param(model.A) +model.D = Param(model.A) +# @decl + +instance = model.create_instance('param3a.dat') + +print('B') +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) +print('C') +keys = instance.C.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.C[key]))) +print('D') +keys = instance.D.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/param3a.txt b/doc/OnlineDocs/src/data/param3a.txt new file mode 100644 index 00000000000..55e6d2bc1b0 --- /dev/null +++ b/doc/OnlineDocs/src/data/param3a.txt @@ -0,0 +1,12 @@ +B +a 10 +c 30 +e 50 +C +a -1 +c -3 +e -5 +D +a 1.1 +c 3.3 +e 5.5 diff --git a/doc/OnlineDocs/src/data/param3b.dat b/doc/OnlineDocs/src/data/param3b.dat new file mode 100644 index 00000000000..1aa4eccda05 --- /dev/null +++ b/doc/OnlineDocs/src/data/param3b.dat @@ -0,0 +1,7 @@ +set A := a c e; + +param : B C D := +a . -1 1.1 +c 30 . 3.3 +e 50 -5 . +; diff --git a/doc/OnlineDocs/src/data/param3b.py b/doc/OnlineDocs/src/data/param3b.py new file mode 100644 index 00000000000..97f8598610a --- /dev/null +++ b/doc/OnlineDocs/src/data/param3b.py @@ -0,0 +1,36 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set() +model.B = Param(model.A) +model.C = Param(model.A) +model.D = Param(model.A) +# @decl + +instance = model.create_instance('param3b.dat') + +print('B') +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) +print('C') +keys = instance.C.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.C[key]))) +print('D') +keys = instance.D.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/param3b.txt b/doc/OnlineDocs/src/data/param3b.txt new file mode 100644 index 00000000000..d558a22cab6 --- /dev/null +++ b/doc/OnlineDocs/src/data/param3b.txt @@ -0,0 +1,9 @@ +B +c 30 +e 50 +C +a -1 +e -5 +D +a 1.1 +c 3.3 diff --git a/doc/OnlineDocs/src/data/param3c.dat b/doc/OnlineDocs/src/data/param3c.dat new file mode 100644 index 00000000000..ada86b946c4 --- /dev/null +++ b/doc/OnlineDocs/src/data/param3c.dat @@ -0,0 +1,5 @@ +param : A : B C D := +a 10 -1 1.1 +c 30 -3 3.3 +e 50 -5 5.5 +; diff --git a/doc/OnlineDocs/src/data/param3c.py b/doc/OnlineDocs/src/data/param3c.py new file mode 100644 index 00000000000..582b0f7db75 --- /dev/null +++ b/doc/OnlineDocs/src/data/param3c.py @@ -0,0 +1,36 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set() +model.B = Param(model.A) +model.C = Param(model.A) +model.D = Param(model.A) +# @decl + +instance = model.create_instance('param3c.dat') + +print('B') +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) +print('C') +keys = instance.C.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.C[key]))) +print('D') +keys = instance.D.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/param3c.txt b/doc/OnlineDocs/src/data/param3c.txt new file mode 100644 index 00000000000..55e6d2bc1b0 --- /dev/null +++ b/doc/OnlineDocs/src/data/param3c.txt @@ -0,0 +1,12 @@ +B +a 10 +c 30 +e 50 +C +a -1 +c -3 +e -5 +D +a 1.1 +c 3.3 +e 5.5 diff --git a/doc/OnlineDocs/src/data/param4.dat b/doc/OnlineDocs/src/data/param4.dat new file mode 100644 index 00000000000..808e9a2b9b4 --- /dev/null +++ b/doc/OnlineDocs/src/data/param4.dat @@ -0,0 +1,6 @@ +set A := a c e; + +param B default 0.0 := +c 30 +e 50 +; diff --git a/doc/OnlineDocs/src/data/param4.py b/doc/OnlineDocs/src/data/param4.py new file mode 100644 index 00000000000..010c46fc9c5 --- /dev/null +++ b/doc/OnlineDocs/src/data/param4.py @@ -0,0 +1,26 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set() +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param4.dat') + +print('B') +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param4.txt b/doc/OnlineDocs/src/data/param4.txt new file mode 100644 index 00000000000..70ab4505a6d --- /dev/null +++ b/doc/OnlineDocs/src/data/param4.txt @@ -0,0 +1,4 @@ +B +a 0 +c 30 +e 50 diff --git a/doc/OnlineDocs/src/data/param5.dat b/doc/OnlineDocs/src/data/param5.dat new file mode 100644 index 00000000000..84120b1bfc5 --- /dev/null +++ b/doc/OnlineDocs/src/data/param5.dat @@ -0,0 +1,6 @@ +set A := a 1 c 2 e 3; + +param B := +a 1 10 +c 2 30 +e 3 50; diff --git a/doc/OnlineDocs/src/data/param5.py b/doc/OnlineDocs/src/data/param5.py new file mode 100644 index 00000000000..2db07f3f990 --- /dev/null +++ b/doc/OnlineDocs/src/data/param5.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=2) +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param5.dat') + +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param5.txt b/doc/OnlineDocs/src/data/param5.txt new file mode 100644 index 00000000000..6894ef994ce --- /dev/null +++ b/doc/OnlineDocs/src/data/param5.txt @@ -0,0 +1,3 @@ +('a', 1) 10 +('c', 2) 30 +('e', 3) 50 diff --git a/doc/OnlineDocs/src/data/param5a.dat b/doc/OnlineDocs/src/data/param5a.dat new file mode 100644 index 00000000000..1533f76bfc9 --- /dev/null +++ b/doc/OnlineDocs/src/data/param5a.dat @@ -0,0 +1,6 @@ +set A := a 1 c 2 e 3; + +param B default 0 := +a 1 10 +c 2 . +e 3 50; diff --git a/doc/OnlineDocs/src/data/param5a.py b/doc/OnlineDocs/src/data/param5a.py new file mode 100644 index 00000000000..32a53d24e9b --- /dev/null +++ b/doc/OnlineDocs/src/data/param5a.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=2) +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param5a.dat') + +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param5a.txt b/doc/OnlineDocs/src/data/param5a.txt new file mode 100644 index 00000000000..996c359409b --- /dev/null +++ b/doc/OnlineDocs/src/data/param5a.txt @@ -0,0 +1,3 @@ +('a', 1) 10 +('c', 2) 0 +('e', 3) 50 diff --git a/doc/OnlineDocs/src/data/param6.dat b/doc/OnlineDocs/src/data/param6.dat new file mode 100644 index 00000000000..f9c472ed39b --- /dev/null +++ b/doc/OnlineDocs/src/data/param6.dat @@ -0,0 +1,7 @@ +set A := a 1 c 2 e 3; + +param : B C D := +a 1 10 -1 1.1 +c 2 30 -3 3.3 +e 3 50 -5 5.5 +; diff --git a/doc/OnlineDocs/src/data/param6.py b/doc/OnlineDocs/src/data/param6.py new file mode 100644 index 00000000000..e3364a933cf --- /dev/null +++ b/doc/OnlineDocs/src/data/param6.py @@ -0,0 +1,36 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=2) +model.B = Param(model.A) +model.C = Param(model.A) +model.D = Param(model.A) +# @decl + +instance = model.create_instance('param6.dat') + +keys = instance.B.keys() +print('B') +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) +print('C') +keys = instance.C.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.C[key]))) +print('D') +keys = instance.D.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/param6.txt b/doc/OnlineDocs/src/data/param6.txt new file mode 100644 index 00000000000..f7c63c10589 --- /dev/null +++ b/doc/OnlineDocs/src/data/param6.txt @@ -0,0 +1,12 @@ +B +('a', 1) 10 +('c', 2) 30 +('e', 3) 50 +C +('a', 1) -1 +('c', 2) -3 +('e', 3) -5 +D +('a', 1) 1.1 +('c', 2) 3.3 +('e', 3) 5.5 diff --git a/doc/OnlineDocs/src/data/param6a.dat b/doc/OnlineDocs/src/data/param6a.dat new file mode 100644 index 00000000000..253dc71632f --- /dev/null +++ b/doc/OnlineDocs/src/data/param6a.dat @@ -0,0 +1,5 @@ +param : A : B C D := +a 1 10 -1 1.1 +c 2 30 -3 3.3 +e 3 50 -5 5.5 +; diff --git a/doc/OnlineDocs/src/data/param6a.py b/doc/OnlineDocs/src/data/param6a.py new file mode 100644 index 00000000000..3d2fa645411 --- /dev/null +++ b/doc/OnlineDocs/src/data/param6a.py @@ -0,0 +1,36 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=2) +model.B = Param(model.A) +model.C = Param(model.A) +model.D = Param(model.A) +# @decl + +instance = model.create_instance('param6a.dat') + +keys = instance.B.keys() +print('B') +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) +print('C') +keys = instance.C.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.C[key]))) +print('D') +keys = instance.D.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/param6a.txt b/doc/OnlineDocs/src/data/param6a.txt new file mode 100644 index 00000000000..f7c63c10589 --- /dev/null +++ b/doc/OnlineDocs/src/data/param6a.txt @@ -0,0 +1,12 @@ +B +('a', 1) 10 +('c', 2) 30 +('e', 3) 50 +C +('a', 1) -1 +('c', 2) -3 +('e', 3) -5 +D +('a', 1) 1.1 +('c', 2) 3.3 +('e', 3) 5.5 diff --git a/doc/OnlineDocs/src/data/param7a.dat b/doc/OnlineDocs/src/data/param7a.dat new file mode 100644 index 00000000000..f7506e8a17a --- /dev/null +++ b/doc/OnlineDocs/src/data/param7a.dat @@ -0,0 +1,7 @@ +set A := 1 a 1 c 1 e 2 a 2 c 2 e 3 a 3 c 3 e; + +param B : a c e := +1 1 2 3 +2 4 5 6 +3 7 8 9 +; diff --git a/doc/OnlineDocs/src/data/param7a.py b/doc/OnlineDocs/src/data/param7a.py new file mode 100644 index 00000000000..b3aba9ec23d --- /dev/null +++ b/doc/OnlineDocs/src/data/param7a.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=2) +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param7a.dat') + +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param7a.txt b/doc/OnlineDocs/src/data/param7a.txt new file mode 100644 index 00000000000..5780254c2b5 --- /dev/null +++ b/doc/OnlineDocs/src/data/param7a.txt @@ -0,0 +1,9 @@ +(1, 'a') 1 +(1, 'c') 2 +(1, 'e') 3 +(2, 'a') 4 +(2, 'c') 5 +(2, 'e') 6 +(3, 'a') 7 +(3, 'c') 8 +(3, 'e') 9 diff --git a/doc/OnlineDocs/src/data/param7b.dat b/doc/OnlineDocs/src/data/param7b.dat new file mode 100644 index 00000000000..5e58d905c55 --- /dev/null +++ b/doc/OnlineDocs/src/data/param7b.dat @@ -0,0 +1,7 @@ +set A := 1 a 1 c 1 e 2 a 2 c 2 e 3 a 3 c 3 e; + +param B (tr) : 1 2 3 := +a 1 4 7 +c 2 5 8 +e 3 6 9 +; diff --git a/doc/OnlineDocs/src/data/param7b.py b/doc/OnlineDocs/src/data/param7b.py new file mode 100644 index 00000000000..8b022f399a8 --- /dev/null +++ b/doc/OnlineDocs/src/data/param7b.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=2) +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param7b.dat') + +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param7b.txt b/doc/OnlineDocs/src/data/param7b.txt new file mode 100644 index 00000000000..5780254c2b5 --- /dev/null +++ b/doc/OnlineDocs/src/data/param7b.txt @@ -0,0 +1,9 @@ +(1, 'a') 1 +(1, 'c') 2 +(1, 'e') 3 +(2, 'a') 4 +(2, 'c') 5 +(2, 'e') 6 +(3, 'a') 7 +(3, 'c') 8 +(3, 'e') 9 diff --git a/doc/OnlineDocs/src/data/param8a.dat b/doc/OnlineDocs/src/data/param8a.dat new file mode 100644 index 00000000000..74b3597c41d --- /dev/null +++ b/doc/OnlineDocs/src/data/param8a.dat @@ -0,0 +1,7 @@ +set A := (a,1,a,1) (a,2,a,2) (b,1,b,1) (b,2,b,2); + +param B := + + [*,1,*,1] a a 10 b b 20 + [*,2,*,2] a a 30 b b 40 +; diff --git a/doc/OnlineDocs/src/data/param8a.py b/doc/OnlineDocs/src/data/param8a.py new file mode 100644 index 00000000000..abfa885ded4 --- /dev/null +++ b/doc/OnlineDocs/src/data/param8a.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=4) +model.B = Param(model.A) +# @decl + +instance = model.create_instance('param8a.dat') + +keys = instance.B.keys() +for key in sorted(keys): + print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param8a.txt b/doc/OnlineDocs/src/data/param8a.txt new file mode 100644 index 00000000000..df87d190ba0 --- /dev/null +++ b/doc/OnlineDocs/src/data/param8a.txt @@ -0,0 +1,4 @@ +('a', 1, 'a', 1) 10 +('a', 2, 'a', 2) 30 +('b', 1, 'b', 1) 20 +('b', 2, 'b', 2) 40 diff --git a/doc/OnlineDocs/src/data/pyomo.diet1.sh b/doc/OnlineDocs/src/data/pyomo.diet1.sh new file mode 100755 index 00000000000..16ced7aa189 --- /dev/null +++ b/doc/OnlineDocs/src/data/pyomo.diet1.sh @@ -0,0 +1,4 @@ +#!/bin/sh +pyomo solve --solver=glpk diet1.py diet.sqlite.dat +cat results.yml +rm -f results.yml results.json diff --git a/doc/OnlineDocs/src/data/pyomo.diet1.txt b/doc/OnlineDocs/src/data/pyomo.diet1.txt new file mode 100644 index 00000000000..fd8c87d51d9 --- /dev/null +++ b/doc/OnlineDocs/src/data/pyomo.diet1.txt @@ -0,0 +1,60 @@ +[ 0.00] Setting up Pyomo environment +[ 0.00] Applying Pyomo preprocessing actions +[ 0.00] Creating model +[ 0.01] Applying solver +[ 0.02] Processing results + Number of solutions: 1 + Solution Information + Gap: 0.0 + Status: optimal + Function Value: 2.81 + Solver results file: results.yml +[ 0.02] Applying Pyomo postprocessing actions +[ 0.02] Pyomo Finished +# ========================================================== +# = Solver Results = +# ========================================================== +# ---------------------------------------------------------- +# Problem Information +# ---------------------------------------------------------- +Problem: +- Name: unknown + Lower bound: 2.81 + Upper bound: 2.81 + Number of objectives: 1 + Number of constraints: 3 + Number of variables: 9 + Number of nonzeros: 9 + Sense: minimize +# ---------------------------------------------------------- +# Solver Information +# ---------------------------------------------------------- +Solver: +- Status: ok + Termination condition: optimal + Statistics: + Branch and bound: + Number of bounded subproblems: 1 + Number of created subproblems: 1 + Error rc: 0 + Time: 0.002644062042236328 +# ---------------------------------------------------------- +# Solution Information +# ---------------------------------------------------------- +Solution: +- number of solutions: 1 + number of solutions displayed: 1 +- Gap: 0.0 + Status: optimal + Message: None + Objective: + Total_Cost: + Value: 2.81 + Variable: + Buy[Fish Sandwich]: + Value: 1 + Buy[Fries]: + Value: 1 + Buy[Lowfat Milk]: + Value: 1 + Constraint: No values diff --git a/doc/OnlineDocs/src/data/pyomo.diet2.sh b/doc/OnlineDocs/src/data/pyomo.diet2.sh new file mode 100755 index 00000000000..78931b3c96d --- /dev/null +++ b/doc/OnlineDocs/src/data/pyomo.diet2.sh @@ -0,0 +1,4 @@ +#!/bin/sh +pyomo solve --solver=glpk diet1.py diet.dat +cat results.yml +rm -f results.yml results.json diff --git a/doc/OnlineDocs/src/data/pyomo.diet2.txt b/doc/OnlineDocs/src/data/pyomo.diet2.txt new file mode 100644 index 00000000000..7ed879d500f --- /dev/null +++ b/doc/OnlineDocs/src/data/pyomo.diet2.txt @@ -0,0 +1,60 @@ +[ 0.00] Setting up Pyomo environment +[ 0.00] Applying Pyomo preprocessing actions +[ 0.00] Creating model +[ 0.01] Applying solver +[ 0.01] Processing results + Number of solutions: 1 + Solution Information + Gap: 0.0 + Status: optimal + Function Value: 2.81 + Solver results file: results.yml +[ 0.01] Applying Pyomo postprocessing actions +[ 0.01] Pyomo Finished +# ========================================================== +# = Solver Results = +# ========================================================== +# ---------------------------------------------------------- +# Problem Information +# ---------------------------------------------------------- +Problem: +- Name: unknown + Lower bound: 2.81 + Upper bound: 2.81 + Number of objectives: 1 + Number of constraints: 3 + Number of variables: 9 + Number of nonzeros: 9 + Sense: minimize +# ---------------------------------------------------------- +# Solver Information +# ---------------------------------------------------------- +Solver: +- Status: ok + Termination condition: optimal + Statistics: + Branch and bound: + Number of bounded subproblems: 1 + Number of created subproblems: 1 + Error rc: 0 + Time: 0.0018515586853027344 +# ---------------------------------------------------------- +# Solution Information +# ---------------------------------------------------------- +Solution: +- number of solutions: 1 + number of solutions displayed: 1 +- Gap: 0.0 + Status: optimal + Message: None + Objective: + Total_Cost: + Value: 2.81 + Variable: + Buy[Fish Sandwich]: + Value: 1 + Buy[Fries]: + Value: 1 + Buy[Lowfat Milk]: + Value: 1 + Constraint: No values diff --git a/doc/OnlineDocs/src/data/set1.dat b/doc/OnlineDocs/src/data/set1.dat new file mode 100644 index 00000000000..53f6b986e45 --- /dev/null +++ b/doc/OnlineDocs/src/data/set1.dat @@ -0,0 +1,17 @@ +# An empty set +set A := ; + +# A set of numbers +set A := 1 2 3; + +# A set of strings +set B := north south east west; + +# A set of mixed types +set C := +0 +-1.0e+10 +'foo bar' +infinity +"100" +; diff --git a/doc/OnlineDocs/src/data/set1.py b/doc/OnlineDocs/src/data/set1.py new file mode 100644 index 00000000000..c84c1ef0819 --- /dev/null +++ b/doc/OnlineDocs/src/data/set1.py @@ -0,0 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set() +model.B = Set() +model.C = Set() + +instance = model.create_instance('set1.dat') + +print(sorted(list(instance.A.data()))) +print(sorted((instance.B.data()))) +print(sorted(list((instance.C.data())), key=lambda x: x if type(x) is str else str(x))) diff --git a/doc/OnlineDocs/src/data/set1.txt b/doc/OnlineDocs/src/data/set1.txt new file mode 100644 index 00000000000..9114550e1bf --- /dev/null +++ b/doc/OnlineDocs/src/data/set1.txt @@ -0,0 +1,3 @@ +[1, 2, 3] +['east', 'north', 'south', 'west'] +[-10000000000.0, 0, 100, 'foo bar', inf] diff --git a/doc/OnlineDocs/src/data/set2.dat b/doc/OnlineDocs/src/data/set2.dat new file mode 100644 index 00000000000..4e9ec9b2da9 --- /dev/null +++ b/doc/OnlineDocs/src/data/set2.dat @@ -0,0 +1 @@ +set A := 1 2 3 4 5 6 ; diff --git a/doc/OnlineDocs/src/data/set2.py b/doc/OnlineDocs/src/data/set2.py new file mode 100644 index 00000000000..9048a49fecb --- /dev/null +++ b/doc/OnlineDocs/src/data/set2.py @@ -0,0 +1,22 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=3) +# @decl + +instance = model.create_instance('set2.dat') + +print(sorted(list(instance.A.data()))) diff --git a/doc/OnlineDocs/src/data/set2.txt b/doc/OnlineDocs/src/data/set2.txt new file mode 100644 index 00000000000..8e4375be534 --- /dev/null +++ b/doc/OnlineDocs/src/data/set2.txt @@ -0,0 +1 @@ +[(1, 2, 3), (4, 5, 6)] diff --git a/doc/OnlineDocs/src/data/set2a.dat b/doc/OnlineDocs/src/data/set2a.dat new file mode 100644 index 00000000000..fb0b3a4f67b --- /dev/null +++ b/doc/OnlineDocs/src/data/set2a.dat @@ -0,0 +1 @@ +set A := (1,2,3) (4,5,6) ; diff --git a/doc/OnlineDocs/src/data/set2a.py b/doc/OnlineDocs/src/data/set2a.py new file mode 100644 index 00000000000..f2fa4d71916 --- /dev/null +++ b/doc/OnlineDocs/src/data/set2a.py @@ -0,0 +1,22 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=3) +# @decl + +instance = model.create_instance('set2a.dat') + +print(sorted(list(instance.A.data()))) diff --git a/doc/OnlineDocs/src/data/set2a.txt b/doc/OnlineDocs/src/data/set2a.txt new file mode 100644 index 00000000000..8e4375be534 --- /dev/null +++ b/doc/OnlineDocs/src/data/set2a.txt @@ -0,0 +1 @@ +[(1, 2, 3), (4, 5, 6)] diff --git a/doc/OnlineDocs/src/data/set3.dat b/doc/OnlineDocs/src/data/set3.dat new file mode 100644 index 00000000000..7e27ffabd1b --- /dev/null +++ b/doc/OnlineDocs/src/data/set3.dat @@ -0,0 +1,5 @@ +set A := 1 aaa 'a b'; + +set B[1] := 0 1 2; +set B[aaa] := aa bb cc; +set B['a b'] := 'aa bb cc'; diff --git a/doc/OnlineDocs/src/data/set3.py b/doc/OnlineDocs/src/data/set3.py new file mode 100644 index 00000000000..9cdacbe39e0 --- /dev/null +++ b/doc/OnlineDocs/src/data/set3.py @@ -0,0 +1,30 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set() +model.B = Set(model.A) +# @decl +# model.C = Set(model.A,model.A) + +instance = model.create_instance('set3.dat') + +print(sorted(list(instance.A.data()), key=lambda x: x if type(x) is str else str(x))) +print(sorted(list(instance.B[1].data()), key=lambda x: x if type(x) is str else str(x))) +print( + sorted( + list(instance.B['aaa'].data()), key=lambda x: x if type(x) is str else str(x) + ) +) diff --git a/doc/OnlineDocs/src/data/set3.txt b/doc/OnlineDocs/src/data/set3.txt new file mode 100644 index 00000000000..fdcefa0d87a --- /dev/null +++ b/doc/OnlineDocs/src/data/set3.txt @@ -0,0 +1,3 @@ +[1, 'a b', 'aaa'] +[0, 1, 2] +['aa', 'bb', 'cc'] diff --git a/doc/OnlineDocs/src/data/set4.dat b/doc/OnlineDocs/src/data/set4.dat new file mode 100644 index 00000000000..aa367736c62 --- /dev/null +++ b/doc/OnlineDocs/src/data/set4.dat @@ -0,0 +1,4 @@ +set A : A1 A2 A3 A4 := + 1 + - - + + 2 + - + - + 3 - + - - ; diff --git a/doc/OnlineDocs/src/data/set4.py b/doc/OnlineDocs/src/data/set4.py new file mode 100644 index 00000000000..b3485638c6f --- /dev/null +++ b/doc/OnlineDocs/src/data/set4.py @@ -0,0 +1,22 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=2) +# @decl + +instance = model.create_instance('set4.dat') + +print(sorted(list(instance.A.data()))) diff --git a/doc/OnlineDocs/src/data/set4.txt b/doc/OnlineDocs/src/data/set4.txt new file mode 100644 index 00000000000..6ca2c725cb0 --- /dev/null +++ b/doc/OnlineDocs/src/data/set4.txt @@ -0,0 +1 @@ +[('A1', 1), ('A1', 2), ('A2', 3), ('A3', 2), ('A4', 1)] diff --git a/doc/OnlineDocs/src/data/set5.dat b/doc/OnlineDocs/src/data/set5.dat new file mode 100644 index 00000000000..ce117cdea73 --- /dev/null +++ b/doc/OnlineDocs/src/data/set5.dat @@ -0,0 +1,3 @@ +set A := + (1,2,*,4) A B + (*,2,*,4) A B C D ; diff --git a/doc/OnlineDocs/src/data/set5.py b/doc/OnlineDocs/src/data/set5.py new file mode 100644 index 00000000000..d745d8408d0 --- /dev/null +++ b/doc/OnlineDocs/src/data/set5.py @@ -0,0 +1,24 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +# @decl +model.A = Set(dimen=4) +# @decl + +instance = model.create_instance('set5.dat') + + +for tpl in sorted(list(instance.A.data()), key=lambda x: tuple(map(str, x))): + print(tpl) diff --git a/doc/OnlineDocs/src/data/set5.txt b/doc/OnlineDocs/src/data/set5.txt new file mode 100644 index 00000000000..eea067c90ad --- /dev/null +++ b/doc/OnlineDocs/src/data/set5.txt @@ -0,0 +1,4 @@ +(1, 2, 'A', 4) +(1, 2, 'B', 4) +('A', 2, 'B', 4) +('C', 2, 'D', 4) diff --git a/doc/OnlineDocs/src/data/table0.dat b/doc/OnlineDocs/src/data/table0.dat new file mode 100644 index 00000000000..44e822c6b19 --- /dev/null +++ b/doc/OnlineDocs/src/data/table0.dat @@ -0,0 +1,6 @@ +table M(A) : +A B M N := +A1 B1 4.3 5.3 +A2 B2 4.4 5.4 +A3 B3 4.5 5.5 +; diff --git a/doc/OnlineDocs/src/data/table0.py b/doc/OnlineDocs/src/data/table0.py new file mode 100644 index 00000000000..de0fae0c861 --- /dev/null +++ b/doc/OnlineDocs/src/data/table0.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.M = Param(model.A) + +instance = model.create_instance('table0.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table0.txt b/doc/OnlineDocs/src/data/table0.txt new file mode 100644 index 00000000000..c2e75dd97a6 --- /dev/null +++ b/doc/OnlineDocs/src/data/table0.txt @@ -0,0 +1,13 @@ +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Param Declarations + M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + +2 Declarations: A M diff --git a/doc/OnlineDocs/src/data/table0.ul.dat b/doc/OnlineDocs/src/data/table0.ul.dat new file mode 100644 index 00000000000..1e296f6202a --- /dev/null +++ b/doc/OnlineDocs/src/data/table0.ul.dat @@ -0,0 +1,5 @@ +table columns=4 M(1)={3} := +A1 B1 4.3 5.3 +A2 B2 4.4 5.4 +A3 B3 4.5 5.5 +; diff --git a/doc/OnlineDocs/src/data/table0.ul.py b/doc/OnlineDocs/src/data/table0.ul.py new file mode 100644 index 00000000000..524c3756782 --- /dev/null +++ b/doc/OnlineDocs/src/data/table0.ul.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.M = Param(model.A) + +instance = model.create_instance('table0.ul.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table0.ul.txt b/doc/OnlineDocs/src/data/table0.ul.txt new file mode 100644 index 00000000000..c2e75dd97a6 --- /dev/null +++ b/doc/OnlineDocs/src/data/table0.ul.txt @@ -0,0 +1,13 @@ +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Param Declarations + M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + +2 Declarations: A M diff --git a/doc/OnlineDocs/src/data/table1.dat b/doc/OnlineDocs/src/data/table1.dat new file mode 100644 index 00000000000..f3eae2ac300 --- /dev/null +++ b/doc/OnlineDocs/src/data/table1.dat @@ -0,0 +1,3 @@ +table M(A) : +A B M N := +A1 B1 4.3 5.3 A2 B2 4.4 5.4 A3 B3 4.5 5.5 ; diff --git a/doc/OnlineDocs/src/data/table1.py b/doc/OnlineDocs/src/data/table1.py new file mode 100644 index 00000000000..f36714b8f1f --- /dev/null +++ b/doc/OnlineDocs/src/data/table1.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.M = Param(model.A) + +instance = model.create_instance('table1.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table1.txt b/doc/OnlineDocs/src/data/table1.txt new file mode 100644 index 00000000000..c2e75dd97a6 --- /dev/null +++ b/doc/OnlineDocs/src/data/table1.txt @@ -0,0 +1,13 @@ +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Param Declarations + M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + +2 Declarations: A M diff --git a/doc/OnlineDocs/src/data/table2.dat b/doc/OnlineDocs/src/data/table2.dat new file mode 100644 index 00000000000..37fa4dad47b --- /dev/null +++ b/doc/OnlineDocs/src/data/table2.dat @@ -0,0 +1,6 @@ +table M(A) N(A,B) : +A B M N := +A1 B1 4.3 5.3 +A2 B2 4.4 5.4 +A3 B3 4.5 5.5 +; diff --git a/doc/OnlineDocs/src/data/table2.py b/doc/OnlineDocs/src/data/table2.py new file mode 100644 index 00000000000..03648a00f8c --- /dev/null +++ b/doc/OnlineDocs/src/data/table2.py @@ -0,0 +1,23 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.B = Set(initialize=['B1', 'B2', 'B3']) + +model.M = Param(model.A) +model.N = Param(model.A, model.B) + +instance = model.create_instance('table2.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table2.txt b/doc/OnlineDocs/src/data/table2.txt new file mode 100644 index 00000000000..a710b6b6042 --- /dev/null +++ b/doc/OnlineDocs/src/data/table2.txt @@ -0,0 +1,21 @@ +2 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + B : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'B1', 'B2', 'B3'} + +2 Param Declarations + M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + N : Size=3, Index=A*B, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'B1') : 5.3 + ('A2', 'B2') : 5.4 + ('A3', 'B3') : 5.5 + +4 Declarations: A B M N diff --git a/doc/OnlineDocs/src/data/table3.dat b/doc/OnlineDocs/src/data/table3.dat new file mode 100644 index 00000000000..820ac6dfe54 --- /dev/null +++ b/doc/OnlineDocs/src/data/table3.dat @@ -0,0 +1,6 @@ +table A={A} Z={A,B} M(A) N(A,B) : +A B M N := +A1 B1 4.3 5.3 +A2 B2 4.4 5.4 +A3 B3 4.5 5.5 +; diff --git a/doc/OnlineDocs/src/data/table3.py b/doc/OnlineDocs/src/data/table3.py new file mode 100644 index 00000000000..2c598f112df --- /dev/null +++ b/doc/OnlineDocs/src/data/table3.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set() +model.B = Set(initialize=['B1', 'B2', 'B3']) +model.Z = Set(dimen=2) + +model.M = Param(model.A) +model.N = Param(model.A, model.B) + + +instance = model.create_instance('table3.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table3.txt b/doc/OnlineDocs/src/data/table3.txt new file mode 100644 index 00000000000..c0c61cd5a5b --- /dev/null +++ b/doc/OnlineDocs/src/data/table3.txt @@ -0,0 +1,24 @@ +3 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + B : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'B1', 'B2', 'B3'} + Z : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +2 Param Declarations + M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + N : Size=3, Index=A*B, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'B1') : 5.3 + ('A2', 'B2') : 5.4 + ('A3', 'B3') : 5.5 + +5 Declarations: A B Z M N diff --git a/doc/OnlineDocs/src/data/table3.ul.dat b/doc/OnlineDocs/src/data/table3.ul.dat new file mode 100644 index 00000000000..db12a6be017 --- /dev/null +++ b/doc/OnlineDocs/src/data/table3.ul.dat @@ -0,0 +1,5 @@ +table columns=4 A={1} Z={1,2} M(1)={3} N(1,2)={4} := +A1 B1 4.3 5.3 +A2 B2 4.4 5.4 +A3 B3 4.5 5.5 +; diff --git a/doc/OnlineDocs/src/data/table3.ul.py b/doc/OnlineDocs/src/data/table3.ul.py new file mode 100644 index 00000000000..18ced12b388 --- /dev/null +++ b/doc/OnlineDocs/src/data/table3.ul.py @@ -0,0 +1,25 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set() +model.B = Set(initialize=['B1', 'B2', 'B3']) +model.Z = Set(dimen=2) + +model.M = Param(model.A) +model.N = Param(model.A, model.B) + + +instance = model.create_instance('table3.ul.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table3.ul.txt b/doc/OnlineDocs/src/data/table3.ul.txt new file mode 100644 index 00000000000..c0c61cd5a5b --- /dev/null +++ b/doc/OnlineDocs/src/data/table3.ul.txt @@ -0,0 +1,24 @@ +3 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + B : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'B1', 'B2', 'B3'} + Z : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +2 Param Declarations + M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + N : Size=3, Index=A*B, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'B1') : 5.3 + ('A2', 'B2') : 5.4 + ('A3', 'B3') : 5.5 + +5 Declarations: A B Z M N diff --git a/doc/OnlineDocs/src/data/table4.dat b/doc/OnlineDocs/src/data/table4.dat new file mode 100644 index 00000000000..24b524497df --- /dev/null +++ b/doc/OnlineDocs/src/data/table4.dat @@ -0,0 +1,6 @@ +table A={A} Z={A,B} M(A) N(Z) : +A B M N := +A1 B1 4.3 5.3 +A2 B2 4.4 5.4 +A3 B3 4.5 5.5 +; diff --git a/doc/OnlineDocs/src/data/table4.py b/doc/OnlineDocs/src/data/table4.py new file mode 100644 index 00000000000..bd20682b5a9 --- /dev/null +++ b/doc/OnlineDocs/src/data/table4.py @@ -0,0 +1,23 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set() +model.Z = Set(dimen=2) + +model.M = Param(model.A) +model.N = Param(model.Z) + +instance = model.create_instance('table4.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table4.txt b/doc/OnlineDocs/src/data/table4.txt new file mode 100644 index 00000000000..f86004c342a --- /dev/null +++ b/doc/OnlineDocs/src/data/table4.txt @@ -0,0 +1,21 @@ +2 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + Z : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +2 Param Declarations + M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + N : Size=3, Index=Z, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'B1') : 5.3 + ('A2', 'B2') : 5.4 + ('A3', 'B3') : 5.5 + +4 Declarations: A Z M N diff --git a/doc/OnlineDocs/src/data/table4.ul.dat b/doc/OnlineDocs/src/data/table4.ul.dat new file mode 100644 index 00000000000..7d5ace03974 --- /dev/null +++ b/doc/OnlineDocs/src/data/table4.ul.dat @@ -0,0 +1,5 @@ +table columns=4 A={1} Z={1,2} M(A)={3} N(Z)={4} := +A1 B1 4.3 5.3 +A2 B2 4.4 5.4 +A3 B3 4.5 5.5 +; diff --git a/doc/OnlineDocs/src/data/table4.ul.py b/doc/OnlineDocs/src/data/table4.ul.py new file mode 100644 index 00000000000..9f16f21fe19 --- /dev/null +++ b/doc/OnlineDocs/src/data/table4.ul.py @@ -0,0 +1,23 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set() +model.Z = Set(dimen=2) + +model.M = Param(model.A) +model.N = Param(model.Z) + +instance = model.create_instance('table4.ul.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table4.ul.txt b/doc/OnlineDocs/src/data/table4.ul.txt new file mode 100644 index 00000000000..f86004c342a --- /dev/null +++ b/doc/OnlineDocs/src/data/table4.ul.txt @@ -0,0 +1,21 @@ +2 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + Z : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +2 Param Declarations + M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + N : Size=3, Index=Z, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'B1') : 5.3 + ('A2', 'B2') : 5.4 + ('A3', 'B3') : 5.5 + +4 Declarations: A Z M N diff --git a/doc/OnlineDocs/src/data/table5.dat b/doc/OnlineDocs/src/data/table5.dat new file mode 100644 index 00000000000..e7add18a994 --- /dev/null +++ b/doc/OnlineDocs/src/data/table5.dat @@ -0,0 +1,6 @@ +table Z={A,B} Y={M,N} : +A B M N := +A1 B1 4.3 5.3 +A2 B2 4.4 5.4 +A3 B3 4.5 5.5 +; diff --git a/doc/OnlineDocs/src/data/table5.py b/doc/OnlineDocs/src/data/table5.py new file mode 100644 index 00000000000..a3cb01209a2 --- /dev/null +++ b/doc/OnlineDocs/src/data/table5.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.Z = Set(dimen=2) +model.Y = Set(dimen=2) + +instance = model.create_instance('table5.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table5.txt b/doc/OnlineDocs/src/data/table5.txt new file mode 100644 index 00000000000..084757b781b --- /dev/null +++ b/doc/OnlineDocs/src/data/table5.txt @@ -0,0 +1,9 @@ +2 Set Declarations + Y : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {(4.3, 5.3), (4.4, 5.4), (4.5, 5.5)} + Z : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +2 Declarations: Z Y diff --git a/doc/OnlineDocs/src/data/table6.dat b/doc/OnlineDocs/src/data/table6.dat new file mode 100644 index 00000000000..9e5e71ba2b7 --- /dev/null +++ b/doc/OnlineDocs/src/data/table6.dat @@ -0,0 +1 @@ +table pi := 3.1416 ; diff --git a/doc/OnlineDocs/src/data/table6.py b/doc/OnlineDocs/src/data/table6.py new file mode 100644 index 00000000000..1db0a764a23 --- /dev/null +++ b/doc/OnlineDocs/src/data/table6.py @@ -0,0 +1,19 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.pi = Param() + +instance = model.create_instance('table6.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table6.txt b/doc/OnlineDocs/src/data/table6.txt new file mode 100644 index 00000000000..811a8e1afbb --- /dev/null +++ b/doc/OnlineDocs/src/data/table6.txt @@ -0,0 +1,6 @@ +1 Param Declarations + pi : Size=1, Index=None, Domain=Any, Default=None, Mutable=False + Key : Value + None : 3.1416 + +1 Declarations: pi diff --git a/doc/OnlineDocs/src/data/table7.dat b/doc/OnlineDocs/src/data/table7.dat new file mode 100644 index 00000000000..c3c0a4cf0da --- /dev/null +++ b/doc/OnlineDocs/src/data/table7.dat @@ -0,0 +1,6 @@ +table Z={A,B} M(A): +A B M N := +A1 B1 4.3 5.3 +A2 B2 4.4 5.4 +A3 B3 4.5 5.5 +; diff --git a/doc/OnlineDocs/src/data/table7.py b/doc/OnlineDocs/src/data/table7.py new file mode 100644 index 00000000000..84a841aca86 --- /dev/null +++ b/doc/OnlineDocs/src/data/table7.py @@ -0,0 +1,21 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() + +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.M = Param(model.A) +model.Z = Set(dimen=2) + +instance = model.create_instance('table7.dat') +instance.pprint() diff --git a/doc/OnlineDocs/src/data/table7.txt b/doc/OnlineDocs/src/data/table7.txt new file mode 100644 index 00000000000..8ddbfde38be --- /dev/null +++ b/doc/OnlineDocs/src/data/table7.txt @@ -0,0 +1,16 @@ +2 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + Z : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +1 Param Declarations + M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + +3 Declarations: A M Z diff --git a/doc/OnlineDocs/src/dataportal/A.tab b/doc/OnlineDocs/src/dataportal/A.tab new file mode 100644 index 00000000000..d9c13cd6ac6 --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/A.tab @@ -0,0 +1,4 @@ +A +A1 +A2 +A3 diff --git a/doc/OnlineDocs/src/dataportal/C.tab b/doc/OnlineDocs/src/dataportal/C.tab new file mode 100644 index 00000000000..14bcdc5e18d --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/C.tab @@ -0,0 +1,10 @@ +A B +A1 1 +A1 2 +A1 3 +A2 1 +A2 2 +A2 3 +A3 1 +A3 2 +A3 3 diff --git a/doc/OnlineDocs/src/dataportal/D.tab b/doc/OnlineDocs/src/dataportal/D.tab new file mode 100644 index 00000000000..965d28df3d0 --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/D.tab @@ -0,0 +1,4 @@ +B A1 A2 A3 +1 + - - +2 - + - +3 - - + diff --git a/doc/OnlineDocs/src/dataportal/PP.csv b/doc/OnlineDocs/src/dataportal/PP.csv new file mode 100644 index 00000000000..3ca8d931ce5 --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/PP.csv @@ -0,0 +1,4 @@ +A,B,PP +A1,B1,4.3 +A2,B2,4.4 +A3,B3,4.5 diff --git a/doc/OnlineDocs/src/dataportal/PP.json b/doc/OnlineDocs/src/dataportal/PP.json new file mode 100644 index 00000000000..f008d202585 --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/PP.json @@ -0,0 +1,9 @@ +{ +"A": ["A1", "A2", "A3"], +"B": ["B1", "B2", "B3"], +"PP": [ + {"index":["A1","B1"], "value":4.3}, + {"index":["A2","B2"], "value":4.4}, + {"index":["A3","B3"], "value":4.5} + ] +} diff --git a/doc/OnlineDocs/src/dataportal/PP.sqlite b/doc/OnlineDocs/src/dataportal/PP.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..ee74b3045328e745d2e2cdcc82040f6a944e3c0b GIT binary patch literal 3072 zcmWFz^vNtqRY=P(%1ta$FlJz3U}R))P*7lCVBi2^W+-L^(yTzt0>m%?5+ejS7<8Yp z@B&paGIujDcQcRZa(GVDtAt1oaA}Gl2XzXO{AO!J%025fm$jQh-a3%=M5?}<27&;j` X2pWR{l2@6K*rUwR5Eu=C5fB0Z=OH + + + + + + + + + + diff --git a/doc/OnlineDocs/src/dataportal/PP.yaml b/doc/OnlineDocs/src/dataportal/PP.yaml new file mode 100644 index 00000000000..1b65496eff8 --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/PP.yaml @@ -0,0 +1,15 @@ +A: + - A1 + - A2 + - A3 +B: + - B1 + - B2 + - B3 +PP: + - index: [A1, B1] + value: 4.3 + - index: [A2, B2] + value: 4.4 + - index: [A3, B3] + value: 4.5 diff --git a/doc/OnlineDocs/src/dataportal/PP_sqlite.py b/doc/OnlineDocs/src/dataportal/PP_sqlite.py new file mode 100644 index 00000000000..1592e820900 --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/PP_sqlite.py @@ -0,0 +1,40 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +""" +Create the PP.sqlite file +""" + +import sqlite3 + +conn = sqlite3.connect('PP.sqlite') + +c = conn.cursor() + +for table in ['PPtable']: + c.execute('DROP TABLE IF EXISTS ' + table) +conn.commit() + +c.execute( + ''' +CREATE TABLE PPtable ( + A text not null, + B text not null, + PP float not null +) +''' +) +conn.commit() + +data = [("A1", "B1", 4.3), ("A2", "B2", 4.4), ("A3", "B3", 4.5)] +for row in data: + c.execute('''INSERT INTO PPtable VALUES (?,?,?)''', row) +conn.commit() diff --git a/doc/OnlineDocs/src/dataportal/Pyomo_mysql b/doc/OnlineDocs/src/dataportal/Pyomo_mysql new file mode 100644 index 00000000000..1a131ef19ba --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/Pyomo_mysql @@ -0,0 +1,10 @@ +DROP TABLE IF EXISTS PPTable; + +CREATE TABLE PPTable ( + A varchar(64) not null, + B varchar(64) not null, + PP float not null + ) engine=innodb ; + +INSERT INTO PPTable VALUES ("A1", "B1", 4.3), ("A2", "B2", 4.4), ("A3", "B3", 4.5); + diff --git a/doc/OnlineDocs/src/dataportal/S.tab b/doc/OnlineDocs/src/dataportal/S.tab new file mode 100644 index 00000000000..14621cf16bc --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/S.tab @@ -0,0 +1,4 @@ +A S +A1 3.3 +A2 . +A3 3.5 diff --git a/doc/OnlineDocs/src/dataportal/T.json b/doc/OnlineDocs/src/dataportal/T.json new file mode 100644 index 00000000000..6fdca35b5cb --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/T.json @@ -0,0 +1,8 @@ +{ "A": ["A1", "A2", "A3"], + "B": [[1, "B1"], [2, "B2"], [3, "B3"]], + "C": {"A1": [1, 2, 3], "A3": [10, 20, 30]}, + "p": 0.1, + "q": {"A1": 3.3, "A2": 3.4, "A3": 3.5}, + "r": [ {"index": [1, "B1"], "value": 3.3}, + {"index": [2, "B2"], "value": 3.4}, + {"index": [3, "B3"], "value": 3.5}]} diff --git a/doc/OnlineDocs/src/dataportal/T.yaml b/doc/OnlineDocs/src/dataportal/T.yaml new file mode 100644 index 00000000000..2279304d944 --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/T.yaml @@ -0,0 +1,17 @@ +A: [A1, A2, A3] +B: +- [1, B1] +- [2, B2] +- [3, B3] +C: + 'A1': [1, 2, 3] + 'A3': [10, 20, 30] +p: 0.1 +q: {A1: 3.3, A2: 3.4, A3: 3.5} +r: +- index: [1, B1] + value: 3.3 +- index: [2, B2] + value: 3.4 +- index: [3, B3] + value: 3.5 diff --git a/doc/OnlineDocs/src/dataportal/U.tab b/doc/OnlineDocs/src/dataportal/U.tab new file mode 100644 index 00000000000..27bba1b34da --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/U.tab @@ -0,0 +1,5 @@ +I A1 A2 A3 +I1 1.3 2.3 3.3 +I2 1.4 2.4 3.4 +I3 1.5 2.5 3.5 +I4 1.6 2.6 3.6 diff --git a/doc/OnlineDocs/src/dataportal/XW.tab b/doc/OnlineDocs/src/dataportal/XW.tab new file mode 100644 index 00000000000..63b46517d7e --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/XW.tab @@ -0,0 +1,4 @@ +A X W +A1 3.3 4.3 +A2 3.4 4.4 +A3 3.5 4.5 diff --git a/doc/OnlineDocs/src/dataportal/Y.tab b/doc/OnlineDocs/src/dataportal/Y.tab new file mode 100644 index 00000000000..926555713f0 --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/Y.tab @@ -0,0 +1,4 @@ +A Y +A1 3.3 +A2 3.4 +A3 3.5 diff --git a/doc/OnlineDocs/src/dataportal/Z.tab b/doc/OnlineDocs/src/dataportal/Z.tab new file mode 100644 index 00000000000..9459d4ba2a0 --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/Z.tab @@ -0,0 +1 @@ +1.1 diff --git a/doc/OnlineDocs/src/dataportal/dataportal_tab.py b/doc/OnlineDocs/src/dataportal/dataportal_tab.py new file mode 100644 index 00000000000..655329d31de --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/dataportal_tab.py @@ -0,0 +1,334 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +# -------------------------------------------------- +# @load +model = AbstractModel() +model.A = Set() +data = DataPortal() +data.load(filename='A.tab', set=model.A) +instance = model.create_instance(data) +# @load +instance.pprint() +# -------------------------------------------------- +# @set1 +model = AbstractModel() +model.A = Set() +data = DataPortal() +data.load(filename='A.tab', set=model.A) +instance = model.create_instance(data) +# @set1 +instance.pprint() +# -------------------------------------------------- +# @set2 +model = AbstractModel() +model.C = Set(dimen=2) +data = DataPortal() +data.load(filename='C.tab', set=model.C) +instance = model.create_instance(data) +# @set2 +instance.pprint() +# -------------------------------------------------- +# @set3 +model = AbstractModel() +model.D = Set(dimen=2) +data = DataPortal() +data.load(filename='D.tab', set=model.D, format='set_array') +instance = model.create_instance(data) +# @set3 +instance.pprint() + +# -------------------------------------------------- +# @param1 +model = AbstractModel() +data = DataPortal() +model.z = Param() +data.load(filename='Z.tab', param=model.z) +instance = model.create_instance(data) +# @param1 +instance.pprint() +# -------------------------------------------------- +# @param2 +model = AbstractModel() +data = DataPortal() +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.y = Param(model.A) +data.load(filename='Y.tab', param=model.y) +instance = model.create_instance(data) +# @param2 +instance.pprint() +# -------------------------------------------------- +# @param4 +model = AbstractModel() +data = DataPortal() +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.x = Param(model.A) +model.w = Param(model.A) +data.load(filename='XW.tab', param=(model.x, model.w)) +instance = model.create_instance(data) +# @param4 +instance.pprint() +# -------------------------------------------------- +# @param3 +model = AbstractModel() +data = DataPortal() +model.A = Set() +model.y = Param(model.A) +data.load(filename='Y.tab', param=model.y, index=model.A) +instance = model.create_instance(data) +# @param3 +instance.pprint() +# -------------------------------------------------- +# @param5 +model = AbstractModel() +data = DataPortal() +model.A = Set() +model.w = Param(model.A) +data.load(filename='XW.tab', select=('A', 'W'), param=model.w, index=model.A) +instance = model.create_instance(data) +# @param5 +instance.pprint() +# -------------------------------------------------- +# @param6 +model = AbstractModel() +data = DataPortal() +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.I = Set(initialize=['I1', 'I2', 'I3', 'I4']) +model.u = Param(model.I, model.A) +data.load(filename='U.tab', param=model.u, format='array') +instance = model.create_instance(data) +# @param6 +instance.pprint() +# -------------------------------------------------- +# @param7 +model = AbstractModel() +data = DataPortal() +model.A = Set(initialize=['A1', 'A2', 'A3']) +model.I = Set(initialize=['I1', 'I2', 'I3', 'I4']) +model.t = Param(model.A, model.I) +data.load(filename='U.tab', param=model.t, format='transposed_array') +instance = model.create_instance(data) +# @param7 +instance.pprint() +# -------------------------------------------------- +# @param8 +model = AbstractModel() +data = DataPortal() +model.A = Set() +model.s = Param(model.A) +data.load(filename='S.tab', param=model.s, index=model.A) +instance = model.create_instance(data) +# @param8 +instance.pprint() +# -------------------------------------------------- +# @param9 +model = AbstractModel() +data = DataPortal() +model.A = Set(initialize=['A1', 'A2', 'A3', 'A4']) +model.y = Param(model.A) +data.load(filename='Y.tab', param=model.y) +instance = model.create_instance(data) +# @param9 +instance.pprint() +# -------------------------------------------------- +# @param10 +model = AbstractModel() +data = DataPortal() +model.A = Set(dimen=2) +model.p = Param(model.A) +data.load(filename='PP.tab', param=model.p, index=model.A) +instance = model.create_instance(data) +# @param10 +instance.pprint() +# -------------------------------------------------- +# @param11 +model = AbstractModel() +data = DataPortal() +model.A = Set() +model.B = Set() +model.q = Param(model.A, model.B) +data.load(filename='PP.tab', param=model.q, index=(model.A, model.B)) +# instance = model.create_instance(data) +# @param11 +# -------------------------------------------------- +# @concrete1 +data = DataPortal() +data.load(filename='A.tab', set="A", format="set") + +model = ConcreteModel() +model.A = Set(initialize=data['A']) +# @concrete1 +model.pprint() +# -------------------------------------------------- +# @concrete2 +data = DataPortal() +data.load(filename='Z.tab', param="z", format="param") +data.load(filename='Y.tab', param="y", format="table") + +model = ConcreteModel() +model.z = Param(initialize=data['z']) +model.y = Param(['A1', 'A2', 'A3'], initialize=data['y']) +# @concrete2 +model.pprint() +# -------------------------------------------------- +# @getitem +data = DataPortal() +data.load(filename='A.tab', set="A", format="set") +print(data['A']) # ['A1', 'A2', 'A3'] + +data.load(filename='Z.tab', param="z", format="param") +print(data['z']) # 1.1 + +data.load(filename='Y.tab', param="y", format="table") +for key in sorted(data['y']): + print("%s %s" % (key, data['y'][key])) +# @getitem +# -------------------------------------------------- +# @excel1 +model = AbstractModel() +data = DataPortal() +model.A = Set(dimen=2) +model.p = Param(model.A) +data.load(filename='excel.xls', range='PPtable', param=model.p, index=model.A) +instance = model.create_instance(data) +# @excel1 +instance.pprint() +# -------------------------------------------------- +# @excel2 +model = AbstractModel() +data = DataPortal() +model.A = Set(dimen=2) +model.p = Param(model.A) +# data.load(filename='excel.xls', range='AX2:AZ5', +# param=model.p, index=model.A) +instance = model.create_instance(data) +# @excel2 +instance.pprint() +# -------------------------------------------------- +# @db1 +model = AbstractModel() +data = DataPortal() +model.A = Set(dimen=2) +model.p = Param(model.A) +data.load( + filename='PP.sqlite', using='sqlite3', table='PPtable', param=model.p, index=model.A +) +instance = model.create_instance(data) +# @db1 +data = DataPortal() +data.load( + filename='PP.sqlite', + using='sqlite3', + table='PPtable', + param=model.p, + index=model.A, + text_factory=str, +) +instance = model.create_instance(data) +instance.pprint() +# -------------------------------------------------- +# @db2 +model = AbstractModel() +data = DataPortal() +model.A = Set() +model.p = Param(model.A) +data.load( + filename='PP.sqlite', + using='sqlite3', + query="SELECT A,PP FROM PPtable", + param=model.p, + index=model.A, +) +instance = model.create_instance(data) +# @db2 +data = DataPortal() +data.load( + filename='PP.sqlite', + using='sqlite3', + query="SELECT A,PP FROM PPtable", + param=model.p, + index=model.A, + text_factory=str, +) +instance = model.create_instance(data) +instance.pprint() +# -------------------------------------------------- +# @db3 +if False: + model = AbstractModel() + data = DataPortal() + model.A = Set() + model.p = Param(model.A) + data.load( + filename="Driver={MySQL ODBC 5.2 UNICODE Driver}; Database=Pyomo; Server=localhost; User=pyomo;", + using='pypyodbc', + query="SELECT A,PP FROM PPtable", + param=model.p, + index=model.A, + ) + instance = model.create_instance(data) + # @db3 + data = DataPortal() + data.load( + filename="Driver={MySQL ODBC 5.2 UNICODE Driver}; Database=Pyomo; Server=localhost; User=pyomo;", + using='pypyodbc', + query="SELECT A,PP FROM PPtable", + param=model.p, + index=model.A, + text_factory=str, + ) + instance = model.create_instance(data) + instance.pprint() +# -------------------------------------------------- +# @json1 +model = AbstractModel() +data = DataPortal() +model.A = Set() +model.B = Set(dimen=2) +model.C = Set(model.A) +model.p = Param() +model.q = Param(model.A) +model.r = Param(model.B) +data.load(filename='T.json') +# @json1 +data = DataPortal() +data.load(filename='T.json', convert_unicode=True) +instance = model.create_instance(data) +instance.pprint() +# -------------------------------------------------- +# @yaml1 +model = AbstractModel() +data = DataPortal() +model.A = Set() +model.B = Set(dimen=2) +model.C = Set(model.A) +model.p = Param() +model.q = Param(model.A) +model.r = Param(model.B) +data.load(filename='T.yaml') +# @yaml1 +instance = model.create_instance(data) +instance.pprint() +# -------------------------------------------------- + +# @namespaces1 +model = AbstractModel() +model.C = Set(dimen=2) +data = DataPortal() +data.load(filename='C.tab', set=model.C, namespace='ns1') +data.load(filename='D.tab', set=model.C, namespace='ns2', format='set_array') +instance1 = model.create_instance(data, namespaces=['ns1']) +instance2 = model.create_instance(data, namespaces=['ns2']) +# @namespaces1 +instance1.pprint() +instance2.pprint() diff --git a/doc/OnlineDocs/src/dataportal/dataportal_tab.txt b/doc/OnlineDocs/src/dataportal/dataportal_tab.txt new file mode 100644 index 00000000000..a23c63d90c9 --- /dev/null +++ b/doc/OnlineDocs/src/dataportal/dataportal_tab.txt @@ -0,0 +1,315 @@ +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Declarations: A +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Declarations: A +1 Set Declarations + C : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} + +1 Declarations: C +1 Set Declarations + D : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 1), ('A2', 2), ('A3', 3)} + +1 Declarations: D +1 Param Declarations + z : Size=1, Index=None, Domain=Any, Default=None, Mutable=False + Key : Value + None : 1.1 + +1 Declarations: z +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Param Declarations + y : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A2 : 3.4 + A3 : 3.5 + +2 Declarations: A y +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +2 Param Declarations + w : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + x : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A2 : 3.4 + A3 : 3.5 + +3 Declarations: A x w +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Param Declarations + y : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A2 : 3.4 + A3 : 3.5 + +2 Declarations: A y +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Param Declarations + w : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + +2 Declarations: A w +2 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + I : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 4 : {'I1', 'I2', 'I3', 'I4'} + +1 Param Declarations + u : Size=12, Index=I*A, Domain=Any, Default=None, Mutable=False + Key : Value + ('I1', 'A1') : 1.3 + ('I1', 'A2') : 2.3 + ('I1', 'A3') : 3.3 + ('I2', 'A1') : 1.4 + ('I2', 'A2') : 2.4 + ('I2', 'A3') : 3.4 + ('I3', 'A1') : 1.5 + ('I3', 'A2') : 2.5 + ('I3', 'A3') : 3.5 + ('I4', 'A1') : 1.6 + ('I4', 'A2') : 2.6 + ('I4', 'A3') : 3.6 + +3 Declarations: A I u +2 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + I : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 4 : {'I1', 'I2', 'I3', 'I4'} + +1 Param Declarations + t : Size=12, Index=A*I, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'I1') : 1.3 + ('A1', 'I2') : 1.4 + ('A1', 'I3') : 1.5 + ('A1', 'I4') : 1.6 + ('A2', 'I1') : 2.3 + ('A2', 'I2') : 2.4 + ('A2', 'I3') : 2.5 + ('A2', 'I4') : 2.6 + ('A3', 'I1') : 3.3 + ('A3', 'I2') : 3.4 + ('A3', 'I3') : 3.5 + ('A3', 'I4') : 3.6 + +3 Declarations: A I t +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Param Declarations + s : Size=2, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A3 : 3.5 + +2 Declarations: A s +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 4 : {'A1', 'A2', 'A3', 'A4'} + +1 Param Declarations + y : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A2 : 3.4 + A3 : 3.5 + +2 Declarations: A y +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +1 Param Declarations + p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'B1') : 4.3 + ('A2', 'B2') : 4.4 + ('A3', 'B3') : 4.5 + +2 Declarations: A p +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Declarations: A + +2 Param Declarations + y : Size=3, Index={A1, A2, A3}, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A2 : 3.4 + A3 : 3.5 + z : Size=1, Index=None, Domain=Any, Default=None, Mutable=False + Key : Value + None : 1.1 + +2 Declarations: z y +['A1', 'A2', 'A3'] +1.1 +A1 3.3 +A2 3.4 +A3 3.5 +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +1 Param Declarations + p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'B1') : 4.3 + ('A2', 'B2') : 4.4 + ('A3', 'B3') : 4.5 + +2 Declarations: A p +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 0 : {} + +1 Param Declarations + p : Size=0, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + +2 Declarations: A p +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} + +1 Param Declarations + p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + ('A1', 'B1') : 4.3 + ('A2', 'B2') : 4.4 + ('A3', 'B3') : 4.5 + +2 Declarations: A p +1 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + +1 Param Declarations + p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 4.3 + A2 : 4.4 + A3 : 4.5 + +2 Declarations: A p +3 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + B : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {(1, 'B1'), (2, 'B2'), (3, 'B3')} + C : Size=2, Index=A, Ordered=Insertion + Key : Dimen : Domain : Size : Members + A1 : 1 : Any : 3 : {1, 2, 3} + A3 : 1 : Any : 3 : {10, 20, 30} + +3 Param Declarations + p : Size=1, Index=None, Domain=Any, Default=None, Mutable=False + Key : Value + None : 0.1 + q : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A2 : 3.4 + A3 : 3.5 + r : Size=3, Index=B, Domain=Any, Default=None, Mutable=False + Key : Value + (1, 'B1') : 3.3 + (2, 'B2') : 3.4 + (3, 'B3') : 3.5 + +6 Declarations: A B C p q r +3 Set Declarations + A : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {'A1', 'A2', 'A3'} + B : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {(1, 'B1'), (2, 'B2'), (3, 'B3')} + C : Size=2, Index=A, Ordered=Insertion + Key : Dimen : Domain : Size : Members + A1 : 1 : Any : 3 : {1, 2, 3} + A3 : 1 : Any : 3 : {10, 20, 30} + +3 Param Declarations + p : Size=1, Index=None, Domain=Any, Default=None, Mutable=False + Key : Value + None : 0.1 + q : Size=3, Index=A, Domain=Any, Default=None, Mutable=False + Key : Value + A1 : 3.3 + A2 : 3.4 + A3 : 3.5 + r : Size=3, Index=B, Domain=Any, Default=None, Mutable=False + Key : Value + (1, 'B1') : 3.3 + (2, 'B2') : 3.4 + (3, 'B3') : 3.5 + +6 Declarations: A B C p q r +1 Set Declarations + C : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} + +1 Declarations: C +1 Set Declarations + C : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 2 : Any : 3 : {('A1', 1), ('A2', 2), ('A3', 3)} + +1 Declarations: C diff --git a/doc/OnlineDocs/src/dataportal/excel.xls b/doc/OnlineDocs/src/dataportal/excel.xls new file mode 100644 index 0000000000000000000000000000000000000000..5b5e4daa14307cf516d913e7a7b896e92fac9c33 GIT binary patch literal 17920 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 0 else M.x[i] for i in range(5)) +# @warning +print(e) + +# --------------------------------------------- +# @sum_product1 +M = ConcreteModel() +M.z = RangeSet(5) +M.x = Var(range(10)) +M.y = Var(range(10)) + +# Sum the elements of x +e1 = sum_product(M.x) + +# Sum the product of elements in x and y +e2 = sum_product(M.x, M.y) + +# Sum the product of elements in x and y, over the index set z +e3 = sum_product(M.x, M.y, index=M.z) +# @sum_product1 +print(e1) +print(e2) +print(e3) + +# --------------------------------------------- +# @sum_product2 +# Sum the product of x_i/y_i +e1 = sum_product(M.x, denom=M.y) + +# Sum the product of 1/(x_i*y_i) +e2 = sum_product(denom=(M.x, M.y)) +# @sum_product2 +print(e1) +print(e2) diff --git a/doc/OnlineDocs/src/expr/performance.txt b/doc/OnlineDocs/src/expr/performance.txt new file mode 100644 index 00000000000..6bfd0bd1d5a --- /dev/null +++ b/doc/OnlineDocs/src/expr/performance.txt @@ -0,0 +1,14 @@ +x[0] + x[1] + x[2] + x[3] + x[4] +x[0] + x[1] + x[2] + x[3] + x[4] +(x[0] + x[1] + x[2] + x[3] + x[4])**2 +x[0]*x[1]*x[2]*x[3]*x[4] +x[0]*z +(x[0] + x[1] + x[2] + x[3] + x[4])*z +x[0]**2 + x[1]**2 + x[2]**2 + x[3]**2 + x[4]**2 +x[0]**2 + x[1]**2 + x[2]**2 + x[3]**2 + x[4]**2 +x[0] + x[1]**2 + x[2]**2 + x[3]**2 + x[4]**2 +x[0] + x[1] + x[2] + x[3] + x[4] + x[5] + x[6] + x[7] + x[8] + x[9] +x[0]*y[0] + x[1]*y[1] + x[2]*y[2] + x[3]*y[3] + x[4]*y[4] + x[5]*y[5] + x[6]*y[6] + x[7]*y[7] + x[8]*y[8] + x[9]*y[9] +x[1]*y[1] + x[2]*y[2] + x[3]*y[3] + x[4]*y[4] + x[5]*y[5] +x[0]/y[0] + x[1]/y[1] + x[2]/y[2] + x[3]/y[3] + x[4]/y[4] + x[5]/y[5] + x[6]/y[6] + x[7]/y[7] + x[8]/y[8] + x[9]/y[9] +1/(x[0]*y[0]) + 1/(x[1]*y[1]) + 1/(x[2]*y[2]) + 1/(x[3]*y[3]) + 1/(x[4]*y[4]) + 1/(x[5]*y[5]) + 1/(x[6]*y[6]) + 1/(x[7]*y[7]) + 1/(x[8]*y[8]) + 1/(x[9]*y[9]) diff --git a/doc/OnlineDocs/src/expr/quicksum.log b/doc/OnlineDocs/src/expr/quicksum.log new file mode 100644 index 00000000000..11e1f203654 --- /dev/null +++ b/doc/OnlineDocs/src/expr/quicksum.log @@ -0,0 +1,4 @@ +sum: 1.447861 +repn: 0.870225 +quicksum: 1.388344 +repn: 0.864316 diff --git a/doc/OnlineDocs/src/expr/quicksum.py b/doc/OnlineDocs/src/expr/quicksum.py new file mode 100644 index 00000000000..1b6cd3f9909 --- /dev/null +++ b/doc/OnlineDocs/src/expr/quicksum.py @@ -0,0 +1,38 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * +from pyomo.repn import generate_standard_repn +import time + +# @runtime +M = ConcreteModel() +M.A = RangeSet(100000) +M.p = Param(M.A, mutable=True, initialize=1) +M.x = Var(M.A) + +start = time.time() +e = sum((M.x[i] - 1) ** M.p[i] for i in M.A) +print("sum: %f" % (time.time() - start)) + +start = time.time() +generate_standard_repn(e) +print("repn: %f" % (time.time() - start)) + +start = time.time() +e = quicksum((M.x[i] - 1) ** M.p[i] for i in M.A) +print("quicksum: %f" % (time.time() - start)) + +start = time.time() +generate_standard_repn(e) +print("repn: %f" % (time.time() - start)) + +# @runtime diff --git a/doc/OnlineDocs/src/kernel/examples.sh b/doc/OnlineDocs/src/kernel/examples.sh new file mode 100755 index 00000000000..0ac9e1a0fbf --- /dev/null +++ b/doc/OnlineDocs/src/kernel/examples.sh @@ -0,0 +1,3 @@ +#! /bin/bash + +for file in `ls ../../library_reference/kernel/examples/*.py | sort`; do python $file; done; diff --git a/doc/OnlineDocs/src/kernel/examples.txt b/doc/OnlineDocs/src/kernel/examples.txt new file mode 100644 index 00000000000..8ba072d28b1 --- /dev/null +++ b/doc/OnlineDocs/src/kernel/examples.txt @@ -0,0 +1,211 @@ +1 Set Declarations + s : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 2 : {1, 2} + +1 RangeSet Declarations + q : Dimen=1, Size=3, Bounds=(1, 3) + Key : Finite : Members + None : True : [1:3] + +2 Param Declarations + p : Size=1, Index=None, Domain=Any, Default=None, Mutable=True + Key : Value + None : 0 + pd : Size=2, Index=s, Domain=Any, Default=None, Mutable=True + Key : Value + 1 : 0 + 2 : 1 + +4 Var Declarations + f : Size=1, Index=None + Key : Lower : Value : Upper : Fixed : Stale : Domain + None : None : None : None : False : True : Reals + v : Size=1, Index=None + Key : Lower : Value : Upper : Fixed : Stale : Domain + None : 1 : 1.0 : 4 : False : False : Reals + vd : Size=2, Index=s + Key : Lower : Value : Upper : Fixed : Stale : Domain + 1 : None : None : 9 : False : True : Reals + 2 : None : None : 9 : False : True : Reals + vl : Size=3, Index={1, 2, 3} + Key : Lower : Value : Upper : Fixed : Stale : Domain + 1 : 1 : None : None : False : True : Reals + 2 : 2 : None : None : False : True : Reals + 3 : 3 : None : None : False : True : Reals + +2 Expression Declarations + e : Size=1, Index=None + Key : Expression + None : - v + ed : Size=2, Index=s + Key : Expression + 1 : - vd[1] + 2 : - vd[2] + +3 Objective Declarations + o : Size=1, Index=None, Active=True + Key : Active : Sense : Expression + None : True : minimize : - v + od : Size=2, Index=s, Active=True + Key : Active : Sense : Expression + 1 : True : minimize : - vd[1] + 2 : True : minimize : - vd[2] + ol : Size=3, Index={1, 2, 3}, Active=True + Key : Active : Sense : Expression + 1 : True : minimize : - vl[1] + 2 : True : minimize : - vl[2] + 3 : True : minimize : - vl[3] + +3 Constraint Declarations + c : Size=1, Index=None, Active=True + Key : Lower : Body : Upper : Active + None : -Inf : vd[1] + vd[2] : 9.0 : True + cd : Size=6, Index=s*q, Active=True + Key : Lower : Body : Upper : Active + (1, 1) : 1.0 : vd[1] : 1.0 : True + (1, 2) : 2.0 : vd[1] : 2.0 : True + (1, 3) : 3.0 : vd[1] : 3.0 : True + (2, 1) : 1.0 : vd[2] : 1.0 : True + (2, 2) : 2.0 : vd[2] : 2.0 : True + (2, 3) : 3.0 : vd[2] : 3.0 : True + cl : Size=3, Index={1, 2, 3}, Active=True + Key : Lower : Body : Upper : Active + 1 : -5.0 : vl[1] - v : 5.0 : True + 2 : -5.0 : vl[2] - v : 5.0 : True + 3 : -5.0 : vl[3] - v : 5.0 : True + +3 SOSConstraint Declarations + sd : Size=2 Index= OrderedScalarSet + 1 + Type=1 + Weight : Variable + 1 : vd[1] + 2 : vd[2] + 2 + Type=1 + Weight : Variable + 1 : vl[1] + 2 : vl[2] + 3 : vl[3] + sos1 : Size=1 + Type=1 + Weight : Variable + 1 : vl[1] + 2 : vl[2] + 3 : vl[3] + sos2 : Size=1 + Type=2 + Weight : Variable + 1 : vd[1] + 2 : vd[2] + +2 Block Declarations + b : Size=1, Index=None, Active=True + 0 Declarations: + pw : Size=1, Index=None, Active=True + 1 Var Declarations + SOS2_y : Size=4, Index={0, 1, 2, 3} + Key : Lower : Value : Upper : Fixed : Stale : Domain + 0 : 0 : None : None : False : True : NonNegativeReals + 1 : 0 : None : None : False : True : NonNegativeReals + 2 : 0 : None : None : False : True : NonNegativeReals + 3 : 0 : None : None : False : True : NonNegativeReals + + 1 Constraint Declarations + SOS2_constraint : Size=3, Index={1, 2, 3}, Active=True + Key : Lower : Body : Upper : Active + 1 : 0.0 : v - (pw.SOS2_y[0] + 2*pw.SOS2_y[1] + 3*pw.SOS2_y[2] + 4*pw.SOS2_y[3]) : 0.0 : True + 2 : 0.0 : f - (pw.SOS2_y[0] + 2*pw.SOS2_y[1] + pw.SOS2_y[2] + 2*pw.SOS2_y[3]) : 0.0 : True + 3 : 1.0 : pw.SOS2_y[0] + pw.SOS2_y[1] + pw.SOS2_y[2] + pw.SOS2_y[3] : 1.0 : True + + 1 SOSConstraint Declarations + SOS2_sosconstraint : Size=1 + Type=2 + Weight : Variable + 1 : pw.SOS2_y[0] + 2 : pw.SOS2_y[1] + 3 : pw.SOS2_y[2] + 4 : pw.SOS2_y[3] + + 3 Declarations: SOS2_y SOS2_constraint SOS2_sosconstraint + +1 Suffix Declarations + dual : Direction=IMPORT, Datatype=FLOAT + Key : Value + +22 Declarations: b s q p pd v vd vl c cd cl e ed o od ol sos1 sos2 sd dual f pw +: block(active=True, ctype=IBlock) + - b: block(active=True, ctype=IBlock) + - p: parameter(active=True, value=0) + - pd: parameter_dict(active=True, ctype=IParameter) + - pd[1]: parameter(active=True, value=0) + - pd[2]: parameter(active=True, value=1) + - pl: parameter_list(active=True, ctype=IParameter) + - pl[0]: parameter(active=True, value=0) + - pl[1]: parameter(active=True, value=1) + - pl[2]: parameter(active=True, value=2) + - v: variable(active=True, value=1, bounds=(1,4), domain_type=RealSet, fixed=False, stale=True) + - vd: variable_dict(active=True, ctype=IVariable) + - vd[1]: variable(active=True, value=None, bounds=(None,9), domain_type=RealSet, fixed=False, stale=True) + - vd[2]: variable(active=True, value=None, bounds=(None,9), domain_type=RealSet, fixed=False, stale=True) + - vl: variable_list(active=True, ctype=IVariable) + - vl[0]: variable(active=True, value=None, bounds=(2,None), domain_type=RealSet, fixed=False, stale=True) + - vl[1]: variable(active=True, value=None, bounds=(2,None), domain_type=RealSet, fixed=False, stale=True) + - vl[2]: variable(active=True, value=None, bounds=(2,None), domain_type=RealSet, fixed=False, stale=True) + - c: constraint(active=True, expr=vd[1] + vd[2] <= 9) + - cd: constraint_dict(active=True, ctype=IConstraint) + - cd[(1, 0)]: constraint(active=True, expr=vd[1] == 0) + - cd[(1, 1)]: constraint(active=True, expr=vd[1] == 1) + - cd[(1, 2)]: constraint(active=True, expr=vd[1] == 2) + - cd[(2, 0)]: constraint(active=True, expr=vd[2] == 0) + - cd[(2, 1)]: constraint(active=True, expr=vd[2] == 1) + - cd[(2, 2)]: constraint(active=True, expr=vd[2] == 2) + - cl: constraint_list(active=True, ctype=IConstraint) + - cl[0]: constraint(active=True, expr=-5 <= vl[0] - v <= 5) + - cl[1]: constraint(active=True, expr=-5 <= vl[1] - v <= 5) + - cl[2]: constraint(active=True, expr=-5 <= vl[2] - v <= 5) + - e: expression(active=True, expr=- v) + - ed: expression_dict(active=True, ctype=IExpression) + - ed[1]: expression(active=True, expr=- vd[1]) + - ed[2]: expression(active=True, expr=- vd[2]) + - el: expression_list(active=True, ctype=IExpression) + - el[0]: expression(active=True, expr=- vl[0]) + - el[1]: expression(active=True, expr=- vl[1]) + - el[2]: expression(active=True, expr=- vl[2]) + - o: objective(active=True, expr=- v) + - od: objective_dict(active=True, ctype=IObjective) + - od[1]: objective(active=True, expr=- vd[1]) + - od[2]: objective(active=True, expr=- vd[2]) + - ol: objective_list(active=True, ctype=IObjective) + - ol[0]: objective(active=True, expr=- vl[0]) + - ol[1]: objective(active=True, expr=- vl[1]) + - ol[2]: objective(active=True, expr=- vl[2]) + - sos1: sos(active=True, level=1, entries=['(vd[1],1)', '(vd[2],2)']) + - sos2: sos(active=True, level=2, entries=['(vl[0],1)', '(vl[1],2)', '(vl[2],3)']) + - sd: sos_dict(active=True, ctype=ISOS) + - sd[1]: sos(active=True, level=1, entries=['(vd[1],1)', '(vd[2],2)']) + - sd[2]: sos(active=True, level=1, entries=['(vl[0],1)', '(vl[1],2)', '(vl[2],3)']) + - sl: sos_list(active=True, ctype=ISOS) + - sl[0]: sos(active=True, level=1, entries=['(vl[1],1)', '(vd[1],2)']) + - sl[1]: sos(active=True, level=1, entries=['(vl[2],1)', '(vd[2],2)']) + - dual: suffix(active=True, size=0) + - suffixes: suffix_dict(active=True, ctype=ISuffix) + - suffixes[dual]: suffix(active=True, size=0) + - f: variable(active=True, value=None, bounds=(None,None), domain_type=RealSet, fixed=False, stale=True) + - pw: piecewise_sos2(active=True, ctype=IBlock) + - pw._inout: expression_tuple(active=True, ctype=IExpression) + - pw._inout[0]: expression(active=True, expr=v) + - pw._inout[1]: expression(active=True, expr=f) + - pw.v: variable_tuple(active=True, ctype=IVariable) + - pw.v[0]: variable(active=True, value=None, bounds=(0,None), domain_type=RealSet, fixed=False, stale=True) + - pw.v[1]: variable(active=True, value=None, bounds=(0,None), domain_type=RealSet, fixed=False, stale=True) + - pw.v[2]: variable(active=True, value=None, bounds=(0,None), domain_type=RealSet, fixed=False, stale=True) + - pw.v[3]: variable(active=True, value=None, bounds=(0,None), domain_type=RealSet, fixed=False, stale=True) + - pw.c: constraint_list(active=True, ctype=IConstraint) + - pw.c[0]: linear_constraint(active=True, expr=pw.v[0] + 2*pw.v[1] + 3*pw.v[2] + 4*pw.v[3] - v == 0) + - pw.c[1]: linear_constraint(active=True, expr=pw.v[0] + 2*pw.v[1] + pw.v[2] + 2*pw.v[3] - f == 0) + - pw.c[2]: linear_constraint(active=True, expr=pw.v[0] + pw.v[1] + pw.v[2] + pw.v[3] == 1) + - pw.s: sos(active=True, level=2, entries=['(pw.v[0],1)', '(pw.v[1],2)', '(pw.v[2],3)', '(pw.v[3],4)']) +Memory: 1.9 KB +Memory: 9.7 KB diff --git a/doc/OnlineDocs/src/scripting/AbstractSuffixes.py b/doc/OnlineDocs/src/scripting/AbstractSuffixes.py new file mode 100644 index 00000000000..1c064042c6b --- /dev/null +++ b/doc/OnlineDocs/src/scripting/AbstractSuffixes.py @@ -0,0 +1,35 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = AbstractModel() +model.I = RangeSet(1, 4) +model.x = Var(model.I) + + +def c_rule(m, i): + return m.x[i] >= i + + +model.c = Constraint(model.I, rule=c_rule) + + +def foo_rule(m): + return ((m.x[i], 3.0 * i) for i in m.I) + + +model.foo = Suffix(rule=foo_rule) + +# instantiate the model +inst = model.create_instance() +for i in inst.I: + print(i, inst.foo[inst.x[i]]) diff --git a/doc/OnlineDocs/src/scripting/Isinglebuild.py b/doc/OnlineDocs/src/scripting/Isinglebuild.py new file mode 100644 index 00000000000..344f8905a4a --- /dev/null +++ b/doc/OnlineDocs/src/scripting/Isinglebuild.py @@ -0,0 +1,58 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# Isinglebuild.py +# NodesIn and NodesOut are created by a build action using the Arcs +from pyomo.environ import * + +model = AbstractModel() + +model.Nodes = Set() +model.Arcs = Set(dimen=2) + +model.NodesOut = Set(model.Nodes, within=model.Nodes, initialize=[]) +model.NodesIn = Set(model.Nodes, within=model.Nodes, initialize=[]) + + +def Populate_In_and_Out(model): + # loop over the arcs and put the end points in the appropriate places + for i, j in model.Arcs: + model.NodesIn[j].add(i) + model.NodesOut[i].add(j) + + +model.In_n_Out = BuildAction(rule=Populate_In_and_Out) + +model.Flow = Var(model.Arcs, domain=NonNegativeReals) +model.FlowCost = Param(model.Arcs) + +model.Demand = Param(model.Nodes) +model.Supply = Param(model.Nodes) + + +def Obj_rule(model): + return summation(model.FlowCost, model.Flow) + + +model.Obj = Objective(rule=Obj_rule, sense=minimize) + + +def FlowBalance_rule(model, node): + return ( + model.Supply[node] + + sum(model.Flow[i, node] for i in model.NodesIn[node]) + - model.Demand[node] + - sum(model.Flow[node, j] for j in model.NodesOut[node]) + == 0 + ) + + +model.FlowBalance = Constraint(model.Nodes, rule=FlowBalance_rule) diff --git a/doc/OnlineDocs/src/scripting/Isinglecomm.dat b/doc/OnlineDocs/src/scripting/Isinglecomm.dat new file mode 100644 index 00000000000..18a586577e4 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/Isinglecomm.dat @@ -0,0 +1,25 @@ +set Nodes := CityA CityB CityC ; + +set Arcs := +CityA CityB +CityA CityC +CityC CityB +; + +param : FlowCost := +CityA CityB 1.4 +CityA CityC 2.7 +CityC CityB 1.6 + ; + +param Demand := +CityA 0 +CityB 1 +CityC 1 +; + +param Supply := +CityA 2 +CityB 0 +CityC 0 +; diff --git a/doc/OnlineDocs/src/scripting/NodesIn_init.py b/doc/OnlineDocs/src/scripting/NodesIn_init.py new file mode 100644 index 00000000000..c17b70150bc --- /dev/null +++ b/doc/OnlineDocs/src/scripting/NodesIn_init.py @@ -0,0 +1,21 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +def NodesIn_init(model, node): + retval = [] + for i, j in model.Arcs: + if j == node: + retval.append(i) + return retval + + +model.NodesIn = Set(model.Nodes, initialize=NodesIn_init) diff --git a/doc/OnlineDocs/src/scripting/Z_init.py b/doc/OnlineDocs/src/scripting/Z_init.py new file mode 100644 index 00000000000..1dd2843f4f0 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/Z_init.py @@ -0,0 +1,19 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +def Z_init(model, i): + if i > 10: + return Set.End + return 2 * i + 1 + + +model.Z = Set(initialize=Z_init) diff --git a/doc/OnlineDocs/src/scripting/abstract1.dat b/doc/OnlineDocs/src/scripting/abstract1.dat new file mode 100644 index 00000000000..d161640d10d --- /dev/null +++ b/doc/OnlineDocs/src/scripting/abstract1.dat @@ -0,0 +1,18 @@ +# one way to input the data in AMPL format +# for indexed parameters, the indexes are given before the value + +param m := 1 ; +param n := 2 ; + +param a := +1 1 3 +1 2 4 +; + +param c:= +1 2 +2 3 +; + +param b := 1 1 ; + diff --git a/doc/OnlineDocs/src/scripting/abstract2.dat b/doc/OnlineDocs/src/scripting/abstract2.dat new file mode 100644 index 00000000000..f865fd4f8ec --- /dev/null +++ b/doc/OnlineDocs/src/scripting/abstract2.dat @@ -0,0 +1,22 @@ +# abstract2.dat AMPL data format + +set I := TV Film ; +set J := Graham John Carol ; + +param a := +TV Graham 3 +TV John 4.4 +TV Carol 4.9 +Film Graham 1 +Film John 2.4 +Film Carol 1.1 +; + +param c := [*] + Graham 2.2 + John 3.1416 + Carol 3 +; + +param b := TV 1 Film 1 ; + diff --git a/doc/OnlineDocs/src/scripting/abstract2.py b/doc/OnlineDocs/src/scripting/abstract2.py new file mode 100644 index 00000000000..544399a8a42 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/abstract2.py @@ -0,0 +1,43 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# abstract2.py + + +from pyomo.environ import * + +model = AbstractModel() + +model.I = Set() +model.J = Set() + +model.a = Param(model.I, model.J) +model.b = Param(model.I) +model.c = Param(model.J) + +# the next line declares a variable indexed by the set J +model.x = Var(model.J, domain=NonNegativeReals) + + +def obj_expression(model): + return summation(model.c, model.x) + + +model.OBJ = Objective(rule=obj_expression) + + +def ax_constraint_rule(model, i): + # return the expression for the constraint for i + return sum(model.a[i, j] * model.x[j] for j in model.J) >= model.b[i] + + +# the next line creates one constraint for each member of the set model.I +model.AxbConstraint = Constraint(model.I, rule=ax_constraint_rule) diff --git a/doc/OnlineDocs/src/scripting/abstract2a.dat b/doc/OnlineDocs/src/scripting/abstract2a.dat new file mode 100644 index 00000000000..9d7849daeef --- /dev/null +++ b/doc/OnlineDocs/src/scripting/abstract2a.dat @@ -0,0 +1,16 @@ +# abstract2a.dat AMPL format + +set I := 1 ; +set J := 1 2 ; + +param a := +1 1 3 +1 2 4 +; + +param c:= +1 2 +2 3 +; + +param b := 1 1 ; diff --git a/doc/OnlineDocs/src/scripting/abstract2piece.py b/doc/OnlineDocs/src/scripting/abstract2piece.py new file mode 100644 index 00000000000..03c5139004e --- /dev/null +++ b/doc/OnlineDocs/src/scripting/abstract2piece.py @@ -0,0 +1,62 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# abstract2piece.py +# Similar to abstract2.py, but the objective is now c times x to the fourth power + +from pyomo.environ import * + +model = AbstractModel() + +model.I = Set() +model.J = Set() + +Topx = 6.1 # range of x variables + +model.a = Param(model.I, model.J) +model.b = Param(model.I) +model.c = Param(model.J) + +# the next line declares a variable indexed by the set J +model.x = Var(model.J, domain=NonNegativeReals, bounds=(0, Topx)) +model.y = Var(model.J, domain=NonNegativeReals) + +# to avoid warnings, we set breakpoints at or beyond the bounds +PieceCnt = 100 +bpts = [] +for i in range(PieceCnt + 2): + bpts.append(float((i * Topx) / PieceCnt)) + + +def f4(model, j, xp): + # we not need j, but it is passed as the index for the constraint + return xp**4 + + +model.ComputeObj = Piecewise( + model.J, model.y, model.x, pw_pts=bpts, pw_constr_type='EQ', f_rule=f4 +) + + +def obj_expression(model): + return summation(model.c, model.y) + + +model.OBJ = Objective(rule=obj_expression) + + +def ax_constraint_rule(model, i): + # return the expression for the constraint for i + return sum(model.a[i, j] * model.x[j] for j in model.J) >= model.b[i] + + +# the next line creates one constraint for each member of the set model.I +model.AxbConstraint = Constraint(model.I, rule=ax_constraint_rule) diff --git a/doc/OnlineDocs/src/scripting/abstract2piecebuild.py b/doc/OnlineDocs/src/scripting/abstract2piecebuild.py new file mode 100644 index 00000000000..d454d7fbc79 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/abstract2piecebuild.py @@ -0,0 +1,77 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# abstract2piecebuild.py +# Similar to abstract2piece.py, but the breakpoints are created using a build action + +from pyomo.environ import * + +model = AbstractModel() + +model.I = Set() +model.J = Set() + +model.a = Param(model.I, model.J) +model.b = Param(model.I) +model.c = Param(model.J) + +model.Topx = Param(default=6.1) # range of x variables +model.PieceCnt = Param(default=100) + +# the next line declares a variable indexed by the set J +model.x = Var(model.J, domain=NonNegativeReals, bounds=(0, model.Topx)) +model.y = Var(model.J, domain=NonNegativeReals) + +# to avoid warnings, we set breakpoints beyond the bounds +# we are using a dictionary so that we can have different +# breakpoints for each index. But we won't. +model.bpts = {} + + +# @Function_valid_declaration +def bpts_build(model, j): + # @Function_valid_declaration + model.bpts[j] = [] + for i in range(model.PieceCnt + 2): + model.bpts[j].append(float((i * model.Topx) / model.PieceCnt)) + + +# The object model.BuildBpts is not referred to again; +# the only goal is to trigger the action at build time +# @BuildAction_example +model.BuildBpts = BuildAction(model.J, rule=bpts_build) +# @BuildAction_example + + +def f4(model, j, xp): + # we not need j in this example, but it is passed as the index for the constraint + return xp**4 + + +model.ComputePieces = Piecewise( + model.J, model.y, model.x, pw_pts=model.bpts, pw_constr_type='EQ', f_rule=f4 +) + + +def obj_expression(model): + return summation(model.c, model.y) + + +model.OBJ = Objective(rule=obj_expression) + + +def ax_constraint_rule(model, i): + # return the expression for the constraint for i + return sum(model.a[i, j] * model.x[j] for j in model.J) >= model.b[i] + + +# the next line creates one constraint for each member of the set model.I +model.AxbConstraint = Constraint(model.I, rule=ax_constraint_rule) diff --git a/doc/OnlineDocs/src/scripting/block_iter_example.py b/doc/OnlineDocs/src/scripting/block_iter_example.py new file mode 100644 index 00000000000..10c8a4ea43d --- /dev/null +++ b/doc/OnlineDocs/src/scripting/block_iter_example.py @@ -0,0 +1,51 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# written by jds, adapted for doc by dlw +from pyomo.environ import * + +# simple way to get arbitrary, unique values for each thing +val_iter = 0 + + +def get_val(*args, **kwds): + global val_iter + val_iter += 1 + return val_iter + + +model = ConcreteModel() +model.I = RangeSet(3) +model.x = Var(initialize=get_val) +model.y = Var(model.I, initialize=get_val) + +model.b = Block() +model.b.a = Var(initialize=get_val) +model.b.b = Var(model.I, initialize=get_val) + + +def c_rule(b, i): + b.c = Var(initialize=get_val) + b.d = Var(b.model().I, initialize=get_val) + + +model.c = Block([1, 2], rule=c_rule) + +model.pprint() + +# @compprintloop +for v in model.component_objects(Var, descend_into=True): + print("FOUND VAR:" + v.name) + v.pprint() + +for v_data in model.component_data_objects(Var, descend_into=True): + print("Found: " + v_data.name + ", value = " + str(value(v_data))) +# @compprintloop diff --git a/doc/OnlineDocs/src/scripting/concrete1.py b/doc/OnlineDocs/src/scripting/concrete1.py new file mode 100644 index 00000000000..399715efde6 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/concrete1.py @@ -0,0 +1,20 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +from pyomo.environ import * + +model = ConcreteModel() + +model.x = Var([1, 2], domain=NonNegativeReals) + +model.OBJ = Objective(expr=2 * model.x[1] + 3 * model.x[2]) + +model.Constraint1 = Constraint(expr=3 * model.x[1] + 4 * model.x[2] >= 1) diff --git a/doc/OnlineDocs/src/scripting/doubleA.py b/doc/OnlineDocs/src/scripting/doubleA.py new file mode 100644 index 00000000000..abf35979a05 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/doubleA.py @@ -0,0 +1,17 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + + +def doubleA_init(model): + return (i * 2 for i in model.A) + + +model.C = Set(initialize=DoubleA_init) diff --git a/doc/OnlineDocs/src/scripting/driveabs2.py b/doc/OnlineDocs/src/scripting/driveabs2.py new file mode 100644 index 00000000000..f8f972460b1 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/driveabs2.py @@ -0,0 +1,47 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# driveabs2.py + +import pyomo.environ as pyo +from pyomo.opt import SolverFactory + +# Create a solver +opt = SolverFactory('cplex') + +# get the model from another file +from abstract2 import model + +# Create a model instance and optimize +instance = model.create_instance('abstract2.dat') + +# @Create_dual_suffix_component +# Create a 'dual' suffix component on the instance +# so the solver plugin will know which suffixes to collect +instance.dual = pyo.Suffix(direction=pyo.Suffix.IMPORT) +# @Create_dual_suffix_component + +results = opt.solve(instance) +# also puts the results back into the instance for easy access + +# @Access_all_dual +# display all duals +print("Duals") +for c in instance.component_objects(pyo.Constraint, active=True): + print(" Constraint", c) + for index in c: + print(" ", index, instance.dual[c[index]]) +# @Access_all_dual + +# @Access_one_dual +# access one dual +print("Dual for Film=", instance.dual[instance.AxbConstraint['Film']]) +# @Access_one_dual diff --git a/doc/OnlineDocs/src/scripting/driveconc1.py b/doc/OnlineDocs/src/scripting/driveconc1.py new file mode 100644 index 00000000000..49b92f32d09 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/driveconc1.py @@ -0,0 +1,34 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# driveconc1.py + +import pyomo.environ as pyo +from pyomo.opt import SolverFactory + +# Create a solver +opt = SolverFactory('cplex') + +# get the model from another file +from concrete1 import model + +# Create a 'dual' suffix component on the instance +# so the solver plugin will know which suffixes to collect +model.dual = pyo.Suffix(direction=pyo.Suffix.IMPORT) + +results = opt.solve(model) # also load results to model + +# display all duals +print("Duals") +for c in model.component_objects(pyo.Constraint, active=True): + print(" Constraint", c) + for index in c: + print(" ", index, model.dual[c[index]]) diff --git a/doc/OnlineDocs/src/scripting/iterative1.py b/doc/OnlineDocs/src/scripting/iterative1.py new file mode 100644 index 00000000000..939120e834f --- /dev/null +++ b/doc/OnlineDocs/src/scripting/iterative1.py @@ -0,0 +1,74 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# @Import_symbols_for_pyomo +# iterative1.py +import pyomo.environ as pyo +from pyomo.opt import SolverFactory + +# @Import_symbols_for_pyomo + +# @Call_SolverFactory_with_argument +# Create a solver +opt = pyo.SolverFactory('glpk') +# @Call_SolverFactory_with_argument + +# +# A simple model with binary variables and +# an empty constraint list. +# +# @Create_base_model +model = pyo.AbstractModel() +model.n = pyo.Param(default=4) +model.x = pyo.Var(pyo.RangeSet(model.n), within=pyo.Binary) + + +def o_rule(model): + return pyo.summation(model.x) + + +model.o = pyo.Objective(rule=o_rule) +# @Create_base_model +# @Create_empty_constraint_list +model.c = pyo.ConstraintList() +# @Create_empty_constraint_list + +# Create a model instance and optimize +# @Create_instantiated_model +instance = model.create_instance() +# @Create_instantiated_model +# @Solve_and_refer_to_results +results = opt.solve(instance) +# @Solve_and_refer_to_results +# @Display_updated_value +instance.display() +# @Display_updated_value + +# Iterate to eliminate the previously found solution +# @Assign_integers +for i in range(5): + # @Assign_integers + # @Iteratively_assign_and_test + expr = 0 + for j in instance.x: + if pyo.value(instance.x[j]) == 0: + expr += instance.x[j] + else: + expr += 1 - instance.x[j] + # @Iteratively_assign_and_test + # @Add_expression_constraint + instance.c.add(expr >= 1) + # @Add_expression_constraint + # @Find_and_display_solution + results = opt.solve(instance) + print("\n===== iteration", i) + instance.display() +# @Find_and_display_solution diff --git a/doc/OnlineDocs/src/scripting/iterative2.py b/doc/OnlineDocs/src/scripting/iterative2.py new file mode 100644 index 00000000000..7506337a491 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/iterative2.py @@ -0,0 +1,52 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# iterative2.py + +import pyomo.environ as pyo +from pyomo.opt import SolverFactory + +# Create a solver +opt = pyo.SolverFactory('cplex') + +# +# A simple model with binary variables and +# an empty constraint list. +# +model = pyo.AbstractModel() +model.n = pyo.Param(default=4) +model.x = pyo.Var(pyo.RangeSet(model.n), within=pyo.Binary) + + +def o_rule(model): + return pyo.summation(model.x) + + +model.o = pyo.Objective(rule=o_rule) +model.c = pyo.ConstraintList() + +# Create a model instance and optimize +instance = model.create_instance() +results = opt.solve(instance) +instance.display() + +# "flip" the value of x[2] (it is binary) +# then solve again +# @Flip_value_before_solve_again + +if pyo.value(instance.x[2]) == 0: + instance.x[2].fix(1) +else: + instance.x[2].fix(0) + +results = opt.solve(instance) +# @Flip_value_before_solve_again +instance.display() diff --git a/doc/OnlineDocs/src/scripting/noiteration1.py b/doc/OnlineDocs/src/scripting/noiteration1.py new file mode 100644 index 00000000000..c7a86e9d1e9 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/noiteration1.py @@ -0,0 +1,41 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# noiteration1.py + +import pyomo.environ as pyo +from pyomo.opt import SolverFactory + +# Create a solver +opt = SolverFactory('glpk') + +# +# A simple model with binary variables and +# an empty constraint list. +# +model = pyo.ConcreteModel() +model.n = pyo.Param(default=4) +model.x = pyo.Var(pyo.RangeSet(model.n), within=pyo.Binary) + + +def o_rule(model): + return pyo.summation(model.x) + + +model.o = pyo.Objective(rule=o_rule) +model.c = pyo.ConstraintList() + +results = opt.solve(model) + +if pyo.value(model.x[2]) == 0: + print("The second index has a zero") +else: + print("x[2]=", pyo.value(model.x[2])) diff --git a/doc/OnlineDocs/src/scripting/parallel.py b/doc/OnlineDocs/src/scripting/parallel.py new file mode 100644 index 00000000000..e6cfa002780 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/parallel.py @@ -0,0 +1,38 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# parallel.py +# run with mpirun -np 2 python -m mpi4py parallel.py +import pyomo.environ as pyo +from mpi4py import MPI + +rank = MPI.COMM_WORLD.Get_rank() +size = MPI.COMM_WORLD.Get_size() +assert ( + size == 2 +), 'This example only works with 2 processes; please us mpirun -np 2 python -m mpi4py parallel.py' + +# Create a solver +opt = pyo.SolverFactory('cplex_direct') + +# +# A simple model with binary variables +# +model = pyo.ConcreteModel() +model.n = pyo.Param(initialize=4) +model.x = pyo.Var(pyo.RangeSet(model.n), within=pyo.Binary) +model.obj = pyo.Objective(expr=sum(model.x.values())) + +if rank == 1: + model.x[1].fix(1) + +results = opt.solve(model) +print('rank: ', rank, ' objective: ', pyo.value(model.obj.expr)) diff --git a/doc/OnlineDocs/src/scripting/spy4Constraints.py b/doc/OnlineDocs/src/scripting/spy4Constraints.py new file mode 100644 index 00000000000..66f82802402 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/spy4Constraints.py @@ -0,0 +1,62 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +""" +David L. Woodruff and Mingye Yang, Spring 2018 +Code snippets for Constraints.rst in testable form +""" + +from pyomo.environ import * + +model = ConcreteModel() +# @Inequality_constraints_2expressions +model.x = Var() + + +def aRule(model): + return model.x >= 2 + + +model.Boundx = Constraint(rule=aRule) + + +def bRule(model): + return (2, model.x, None) + + +model.boundx = Constraint(rule=bRule) +# @Inequality_constraints_2expressions + +model = ConcreteModel() +model.J = Set(initialize=['butter', 'scones']) +model.x = Var(model.J) + + +# @Constraint_example +def teaOKrule(model): + return model.x['butter'] + model.x['scones'] == 3 + + +model.TeaConst = Constraint(rule=teaOKrule) +# @Constraint_example + +# @Passing_elements_crossproduct +model.A = RangeSet(1, 10) +model.a = Param(model.A, within=PositiveReals) +model.ToBuy = Var(model.A) + + +def bud_rule(model, i): + return model.a[i] * model.ToBuy[i] <= i + + +aBudget = Constraint(model.A, rule=bud_rule) +# @Passing_elements_crossproduct diff --git a/doc/OnlineDocs/src/scripting/spy4Expressions.py b/doc/OnlineDocs/src/scripting/spy4Expressions.py new file mode 100644 index 00000000000..cf7ed1f112f --- /dev/null +++ b/doc/OnlineDocs/src/scripting/spy4Expressions.py @@ -0,0 +1,128 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +""" +David L. Woodruff and Mingye Yang, Spring 2018 +Code snippets for Expressions.rst in testable form +""" + +from pyomo.environ import * + +model = ConcreteModel() + +# @Buildup_expression_switch +switch = 3 + +model.A = RangeSet(1, 10) +model.c = Param(model.A) +model.d = Param() +model.x = Var(model.A, domain=Boolean) + + +def pi_rule(model): + accexpr = summation(model.c, model.x) + if switch >= 2: + accexpr = accexpr - model.d + return accexpr >= 0.5 + + +PieSlice = Constraint(rule=pi_rule) +# @Buildup_expression_switch + +# @Abstract_wrong_usage +model.A = RangeSet(1, 10) +model.c = Param(model.A) +model.d = Param() +model.x = Var(model.A, domain=Boolean) + + +def pi_rule(model): + accexpr = summation(model.c, model.x) + if model.d >= 2: # NOT in an abstract model!! + accexpr = accexpr - model.d + return accexpr >= 0.5 + + +PieSlice = Constraint(rule=pi_rule) +# @Abstract_wrong_usage + +# @Declare_piecewise_constraints +# model.pwconst = Piecewise(indexes, yvar, xvar, **Keywords) +# model.pwconst = Piecewise(yvar,xvar,**Keywords) +# @Declare_piecewise_constraints + + +# @f_rule_Function_examples +# A function that changes with index +def f(model, j, x): + if j == 2: + return x**2 + 1.0 + else: + return x**2 + 5.0 + + +# A nonlinear function +f = lambda model, x: exp(x) + value(model.p) + +# A step function +f = [0, 0, 1, 1, 2, 2] +# @f_rule_Function_examples + +# @Keyword_assignment_example +kwds = {'pw_constr_type': 'EQ', 'pw_repn': 'SOS2', 'sense': maximize, 'force_pw': True} +# @Keyword_assignment_example + +# @Expression_objects_illustration +model = ConcreteModel() +model.x = Var(initialize=1.0) + + +def _e(m, i): + return m.x * i + + +model.e = Expression([1, 2, 3], rule=_e) + +instance = model.create_instance() + +print(value(instance.e[1])) # -> 1.0 +print(instance.e[1]()) # -> 1.0 +print(instance.e[1].value) # -> a pyomo expression object + +# Change the underlying expression +instance.e[1].value = instance.x**2 + +# ... solve +# ... load results + +# print the value of the expression given the loaded optimal solution +print(value(instance.e[1])) +# @Expression_objects_illustration + + +# @Define_python_function +def f(x, p): + return x + p + + +# @Define_python_function + +# @Generate_new_expression +model = ConcreteModel() +model.x = Var() + +# create a Pyomo expression +e1 = model.x + 5 + +# create another Pyomo expression +# e1 is copied when generating e2 +e2 = e1 + model.x +# @Generate_new_expression diff --git a/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py b/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py new file mode 100644 index 00000000000..9f6698d63c9 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py @@ -0,0 +1,36 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +""" +David L. Woodruff and Mingye Yang, Spring 2018 +Code snippets for PyomoCommand.rst in testable form +""" + +from pyomo.environ import * + +model = ConcreteModel() +model.I = RangeSet(3) +model.J = RangeSet(3) +model.a = Param(model.I, model.J, default=1.0) +model.x = Var(model.J) +model.b = Param(model.I, default=1.0) + + +# @Troubleshooting_printed_command +def ax_constraint_rule(model, i): + # return the expression for the constraint for i + print("ax_constraint_rule was called for i=", str(i)) + return sum(model.a[i, j] * model.x[j] for j in model.J) >= model.b[i] + + +# the next line creates one constraint for each member of the set model.I +model.AxbConstraint = Constraint(model.I, rule=ax_constraint_rule) +# @Troubleshooting_printed_command diff --git a/doc/OnlineDocs/src/scripting/spy4Variables.py b/doc/OnlineDocs/src/scripting/spy4Variables.py new file mode 100644 index 00000000000..1bc2dc9f1ef --- /dev/null +++ b/doc/OnlineDocs/src/scripting/spy4Variables.py @@ -0,0 +1,39 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +""" +David L. Woodruff and Mingye Yang, Spring 2018 +Code snippets for Variables.rst in testable form +""" + +from pyomo.environ import * + +model = ConcreteModel() +# @Declare_singleton_variable +model.LumberJack = Var(within=NonNegativeReals, bounds=(0, 6), initialize=1.5) +# @Declare_singleton_variable + +# @Assign_value +model.LumberJack = 1.5 +# @Assign_value + +# @Declare_bounds +model.A = Set(initialize=['Scones', 'Tea']) +lb = {'Scones': 2, 'Tea': 4} +ub = {'Scones': 5, 'Tea': 7} + + +def fb(model, i): + return (lb[i], ub[i]) + + +model.PriceToCharge = Var(model.A, domain=PositiveIntegers, bounds=fb) +# @Declare_bounds diff --git a/doc/OnlineDocs/src/scripting/spy4scripts.py b/doc/OnlineDocs/src/scripting/spy4scripts.py new file mode 100644 index 00000000000..f71a1b67b11 --- /dev/null +++ b/doc/OnlineDocs/src/scripting/spy4scripts.py @@ -0,0 +1,220 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +###NOTE: as of May 16, this will not even come close to running. DLW +### and it is "wrong" in a lot of places. +### Someone should edit this file, then delete these comment lines. DLW may 16 + +""" +David L. Woodruff and Mingye Yang, Spring 2018 +Code snippets for scripts.rst in testable form +""" +import pyomo.environ as pyo + +instance = pyo.ConcreteModel() +instance.I = pyo.Set(initialize=[1, 2, 3]) +instance.sigma = pyo.Param(mutable=True, initialize=2.3) +instance.Theta = pyo.Param(instance.I, mutable=True) +for i in instance.I: + instance.Theta[i] = i +ParamName = "Theta" +idx = 1 +NewVal = 1134 + +# @Assign_value_to_indexed_parametername +instance.ParamName[idx].value = NewVal +# @Assign_value_to_indexed_parametername + +ParamName = "sigma" + +# @Assign_value_to_unindexed_parametername_2 +instance.ParamName.value = NewVal +# @Assign_value_to_unindexed_parametername_2 + +instance.x = pyo.Var([1, 2, 3], initialize=0) +instance.y = pyo.Var() +# @Set_upper&lower_bound + +if instance.x[2] == 0: + instance.x[2].setlb(1) + instance.x[2].setub(1) +else: + instance.x[2].setlb(0) + instance.x[2].setub(0) +# @Set_upper&lower_bound + +# @Equivalent_form_of_instance.x.fix(2) +instance.y.value = 2 +instance.y.fixed = True +# @Equivalent_form_of_instance.x.fix(2) + +model = ConcreteModel() +model.obj1 = pyo.Objective(expr=0) +model.obj2 = pyo.Objective(expr=0) + +# @Pass_multiple_objectives_to_solver +model.obj1.deactivate() +model.obj2.activate() +# @Pass_multiple_objectives_to_solver + + +# @Listing_arguments +def pyomo_preprocess(options=None): + if options == None: + print("No command line options were given.") + else: + print("Command line arguments were: %s" % options) + + +# @Listing_arguments + + +# @Provide_dictionary_for_arbitrary_keywords +def pyomo_preprocess(**kwds): + options = kwds.get('options', None) + if options == None: + print("No command line options were given.") + else: + print("Command line arguments were: %s" % options) + + +# @Provide_dictionary_for_arbitrary_keywords + + +# @Pyomo_preprocess_argument +def pyomo_preprocess(options=None): + pass + + +# @Pyomo_preprocess_argument + +# @Display_all_variables&values +for v in instance.component_objects(pyo.Var, active=True): + print("Variable", v) + for index in v: + print(" ", index, pyo.value(v[index])) +# @Display_all_variables&values + +# @Display_all_variables&values_data +for v in instance.component_data_objects(pyo.Var, active=True): + print(v, pyo.value(v)) +# @Display_all_variables&values_data + + +instance.iVar = pyo.Var([1, 2, 3], initialize=1, domain=pyo.Boolean) +instance.sVar = pyo.Var(initialize=1, domain=pyo.Boolean) +# dlw may 2018: the next snippet does not trigger any fixing ("active?") +# @Fix_all_integers&values +for var in instance.component_data_objects(pyo.Var, active=True): + if var.domain is pyo.IntegerSet or var.domain is pyo.BooleanSet: + print("fixing " + str(v)) + var.fixed = True # fix the current value +# @Fix_all_integers&values + + +# @Include_definition_in_modelfile +def pyomo_print_results(options, instance, results): + for v in instance.component_objects(pyo.Var, active=True): + print("Variable " + str(v)) + varobject = getattr(instance, v) + for index in varobject: + print(" ", index, varobject[index].value) + + +# @Include_definition_in_modelfile + +# @Print_parameter_name&value +for parmobject in instance.component_objects(pyo.Param, active=True): + print("Parameter " + str(parmobject.name)) + for index in parmobject: + print(" ", index, parmobject[index].value) +# @Print_parameter_name&value + + +# @Include_definition_output_constraints&duals +def pyomo_print_results(options, instance, results): + # display all duals + print("Duals") + for c in instance.component_objects(pyo.Constraint, active=True): + print(" Constraint", c) + cobject = getattr(instance, c) + for index in cobject: + print(" ", index, instance.dual[cobject[index]]) + + +# @Include_definition_output_constraints&duals + +""" +xxxxxxxxxxxxxxxxxxxx high alert!!!! xxxxxx testing blocked from here to the end xxxxxxxxxxxxxx +# @Print_solver_status +results = opt.solve(instance) +#print ("The solver returned a status of:"+str(results.solver.status)) +# @Print_solver_status + +# @Pyomo_data_comparedwith_solver_status_1 +from pyomo.opt import SolverStatus, TerminationCondition + +#... + +if (results.solver.status == SolverStatus.ok) and (results.solver.termination_condition == TerminationCondition.optimal): + print ("this is feasible and optimal") +elif results.solver.termination_condition == TerminationCondition.infeasible: + print ("do something about it? or exit?") +else: + # something else is wrong + print (str(results.solver)) +# @Pyomo_data_comparedwith_solver_status_1 + +# @Pyomo_data_comparedwith_solver_status_2 +from pyomo.opt import TerminationCondition + +... + +results = opt.solve(model, load_solutions=False) +if results.solver.termination_condition == TerminationCondition.optimal: + model.solutions.load_from(results) +else: + print ("Solution is not optimal") + # now do something about it? or exit? ... +# @Pyomo_data_comparedwith_solver_status_2 + +# @See_solver_output +results = opt.solve(instance, tee=True) +# @See_solver_output + +# @Add_option_to_solver +optimizer = pyo.SolverFactory['cbc'] +optimizer.options["threads"] = 4 +# @Add_option_to_solver + +# @Add_multiple_options_to_solver +results = optimizer.solve(instance, options={'threads' : 4}, tee=True) +# @Add_multiple_options_to_solver + +# @Set_path_to_solver_executable +opt = pyo.SolverFactory("ipopt", executable="../ipopt") +# @Set_path_to_solver_executable + +# @Pass_warmstart_to_solver +instance = model.create() +instance.y[0] = 1 +instance.y[1] = 0 + +opt = pyo.SolverFactory("cplex") + +results = opt.solve(instance, warmstart=True) +# @Pass_warmstart_to_solver + +# @Specify_temporary_directory_name +from pyomo.common.tempfiles import TempfileManager +TempfileManager.tempdir = YourDirectoryNameGoesHere +# @Specify_temporary_directory_name +""" diff --git a/doc/OnlineDocs/src/strip_examples.py b/doc/OnlineDocs/src/strip_examples.py new file mode 100644 index 00000000000..2fd03256499 --- /dev/null +++ b/doc/OnlineDocs/src/strip_examples.py @@ -0,0 +1,80 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +# +# This script finds all *.py files in the current and subdirectories. +# It processes these files to find blocks that start/end with "# @" +# For example +# +# print("START HERE") +# # @block +# print("IN THE BLOCK") +# x = 1 +# # @block +# print("END HERE") +# +# If this file was foo.py, then a file foo_block.spy is created, which +# contains the lines between the lines starting with "# @". +# +# Additionally, the file foo.spy is created, which strips all lines +# starting with "# @". +# +# This utility provides a convenient mechanism for creating complex scripts that +# can be tested, while extracting pieces that are included in Sphinx documentation. +# +import glob +import sys +import os +import os.path + + +def f(root, file): + if not file.endswith('.py'): + return + prefix = os.path.splitext(file)[0] + # print([root, file, prefix]) + OUTPUT = open(root + '/' + prefix + '.spy', 'w') + INPUT = open(root + '/' + file, 'r') + flag = False + block_name = None + for line in INPUT: + tmp = line.strip() + if tmp.startswith("# @"): + if flag is False: + block_name = tmp[3:] + flag = True + OUTPUT_ = open(root + '/' + prefix + '_%s.spy' % block_name, 'w') + else: + if block_name != tmp[3:]: + print( + "ERROR parsing file '%s': Started block '%s' but ended with '%s'" + % (root + '/' + file, block_name, tmp[3:]) + ) + sys.exit(1) + flag = False + block_name is None + OUTPUT_.close() + continue + elif flag: + OUTPUT_.write(line) + OUTPUT.write(line) + INPUT.close() + OUTPUT.close() + + +def generate_spy_files(root_dir): + for root, dirs, files in os.walk(root_dir): + for file in files: + f(root, file) + + +if __name__ == '__main__': + generate_spy_files(sys.argv[1]) diff --git a/doc/OnlineDocs/src/test_examples.py b/doc/OnlineDocs/src/test_examples.py new file mode 100644 index 00000000000..c5c9a135ee9 --- /dev/null +++ b/doc/OnlineDocs/src/test_examples.py @@ -0,0 +1,76 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import pyomo.common.unittest as unittest +import glob +import os +from pyomo.common.dependencies import attempt_import, matplotlib_available +from pyomo.common.fileutils import this_file_dir +import pyomo.environ as pyo + + +currdir = this_file_dir() + +parameterized, param_available = attempt_import('parameterized') +if not param_available: + raise unittest.SkipTest('Parameterized is not available.') + +# Needed for testing (triggers matplotlib import and switches its backend): +bool(matplotlib_available) + + +class TestOnlineDocExamples(unittest.BaselineTestDriver, unittest.TestCase): + # Only test files in directories ending in -ch. These directories + # contain the updated python and scripting files corresponding to + # each chapter in the book. + py_tests, sh_tests = unittest.BaselineTestDriver.gather_tests( + list(filter(os.path.isdir, glob.glob(os.path.join(currdir, '*')))) + ) + + solver_dependencies = { + 'test_data_pyomo_diet1': ['glpk'], + 'test_data_pyomo_diet2': ['glpk'], + 'test_kernel_examples': ['glpk'], + } + # Note on package dependencies: two tests actually need + # pyutilib.excel.spreadsheet; however, the pyutilib importer is + # broken on Python>=3.12, so instead of checking for spreadsheet, we + # will check for pyutilib.component, which triggers the importer + # (and catches the error on 3.12) + package_dependencies = { + # data + 'test_data_ABCD9': ['pyodbc'], + 'test_data_ABCD8': ['pyodbc'], + 'test_data_ABCD7': ['win32com', 'pyutilib.component'], + # dataportal + 'test_dataportal_dataportal_tab': ['xlrd', 'pyutilib.component'], + 'test_dataportal_set_initialization': ['numpy'], + 'test_dataportal_param_initialization': ['numpy'], + # kernel + 'test_kernel_examples': ['pympler'], + } + + @parameterized.parameterized.expand( + sh_tests, name_func=unittest.BaselineTestDriver.custom_name_func + ) + def test_sh(self, tname, test_file, base_file): + self.shell_test_driver(tname, test_file, base_file) + + @parameterized.parameterized.expand( + py_tests, name_func=unittest.BaselineTestDriver.custom_name_func + ) + def test_py(self, tname, test_file, base_file): + self.python_test_driver(tname, test_file, base_file) + + +# Execute the tests +if __name__ == '__main__': + unittest.main() From e2cb02398ae856843cf160f38119f394e8612842 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 06:25:15 -0600 Subject: [PATCH 2399/3044] restore 'make clean' target --- doc/OnlineDocs/Makefile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/OnlineDocs/Makefile b/doc/OnlineDocs/Makefile index 00bc123ad45..d4bf5cd5204 100644 --- a/doc/OnlineDocs/Makefile +++ b/doc/OnlineDocs/Makefile @@ -18,3 +18,8 @@ help: # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +clean: + @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @echo "Removing *.spy, *.out" + @find . -name \*.spy -delete From d6b70052f73fb493308b3ef10a733aa4ca28c80d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 06:25:37 -0600 Subject: [PATCH 2400/3044] doc: add missing index.rst --- doc/OnlineDocs/how_to_guide/index.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 doc/OnlineDocs/how_to_guide/index.rst diff --git a/doc/OnlineDocs/how_to_guide/index.rst b/doc/OnlineDocs/how_to_guide/index.rst new file mode 100644 index 00000000000..e248c400bce --- /dev/null +++ b/doc/OnlineDocs/how_to_guide/index.rst @@ -0,0 +1,2 @@ +Hot-To Guides +============= From 9af47aa112fd478af01b59a21fe8f8e747e409bc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 06:36:43 -0600 Subject: [PATCH 2401/3044] Move errors.rst to not break links from code --- doc/OnlineDocs/{reference_guide => }/errors.rst | 0 doc/OnlineDocs/reference_guide/index.rst | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename doc/OnlineDocs/{reference_guide => }/errors.rst (100%) diff --git a/doc/OnlineDocs/reference_guide/errors.rst b/doc/OnlineDocs/errors.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/errors.rst rename to doc/OnlineDocs/errors.rst diff --git a/doc/OnlineDocs/reference_guide/index.rst b/doc/OnlineDocs/reference_guide/index.rst index e0631861542..391e7995de5 100644 --- a/doc/OnlineDocs/reference_guide/index.rst +++ b/doc/OnlineDocs/reference_guide/index.rst @@ -5,7 +5,7 @@ Reference Guide :maxdepth: 2 library_reference/index.rst - errors.rst + ../errors.rst future.rst From 25c6774faf3dc3c5c5760d25f4cbf6918d27e1bb Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 09:28:58 -0600 Subject: [PATCH 2402/3044] Simplifying directory names --- doc/OnlineDocs/conf.py | 2 +- .../analysis/alternative_solutions.rst | 0 .../analysis/communities_8pp.png | Bin .../analysis/communities_decode_1.png | Bin .../analysis/community.rst | 0 .../analysis/doe/CCSI-license.txt | 0 .../analysis/doe/FIM_sensitivity.png | Bin .../analysis/doe/doe.rst | 0 .../analysis/doe/flowchart.png | Bin .../analysis/doe/grid-1.png | Bin .../analysis/doe/reactor.png | Bin .../analysis/doe/uml.png | Bin .../analysis/iis.rst | 0 .../analysis/incidence/api.rst | 0 .../analysis/incidence/config.rst | 0 .../analysis/incidence/connected.rst | 0 .../analysis/incidence/dulmage_mendelsohn.rst | 0 .../analysis/incidence/incidence.rst | 0 .../analysis/incidence/index.rst | 0 .../analysis/incidence/interface.rst | 0 .../analysis/incidence/matching.rst | 0 .../analysis/incidence/overview.rst | 0 .../analysis/incidence/scc_solver.rst | 0 .../analysis/incidence/triangularize.rst | 0 .../analysis/incidence/tutorial.bt.rst | 0 .../analysis/incidence/tutorial.btsolve.rst | 0 .../analysis/incidence/tutorial.dm.rst | 0 .../analysis/incidence/tutorial.rst | 0 .../analysis/mpc/api.rst | 0 .../analysis/mpc/conversion.rst | 0 .../analysis/mpc/data.rst | 0 .../analysis/mpc/examples.rst | 0 .../analysis/mpc/faq.rst | 0 .../analysis/mpc/index.rst | 0 .../analysis/mpc/interface.rst | 0 .../analysis/mpc/modeling.rst | 0 .../analysis/mpc/overview.rst | 0 .../analysis/parmest/api.rst | 0 .../analysis/parmest/boxplot.png | Bin .../analysis/parmest/covariance.rst | 0 .../analysis/parmest/datarec.rst | 0 .../analysis/parmest/driver.rst | 0 .../analysis/parmest/examples.rst | 0 .../analysis/parmest/graphics.rst | 0 .../analysis/parmest/index.rst | 0 .../analysis/parmest/installation.rst | 0 .../analysis/parmest/overview.rst | 0 .../analysis/parmest/pairwise_plot_CI.png | Bin .../analysis/parmest/pairwise_plot_LR.png | Bin .../analysis/parmest/parallel.rst | 0 .../analysis/parmest/scencreate.rst | 0 .../analysis/sensitivity_toolbox.rst | 0 .../contrib_index.rst | 0 .../developer_utilities/config.rst | 0 .../developer_utilities/deprecation.rst | 0 .../experimental/solvers.rst | 0 .../{user_explanations => explanation}/index.rst | 0 .../modeling/Constraints.rst | 0 .../modeling/Expressions.rst | 0 .../modeling/Objectives.rst | 0 .../modeling/Parameters.rst | 0 .../modeling/Sets.rst | 0 .../modeling/Suffixes.rst | 0 .../modeling/Variables.rst | 0 .../modeling/dae.rst | 0 .../modeling/gdp/concepts.rst | 0 .../modeling/gdp/index.rst | 0 .../modeling/gdp/modeling.rst | 0 .../modeling/gdp/solving.rst | 0 .../modeling/index.rst | 0 .../modeling/mpec.rst | 0 .../modeling/network.rst | 0 .../modeling/reduce_points_demo.png | Bin .../modeling/sos_constraints.rst | 0 .../modeling/units_container.rst | 0 .../modeling_utilities/flattener/index.rst | 0 .../modeling_utilities/flattener/motivation.rst | 0 .../modeling_utilities/flattener/reference.rst | 0 .../modeling_utilities/latex_printer.rst | 0 .../modeling_utilities/preprocessing.rst | 0 .../modeling_utilities/scaling.rst | 0 .../pyomo_philosophy/expressions/design.rst | 0 .../pyomo_philosophy/expressions/index.rst | 0 .../pyomo_philosophy/expressions/managing.rst | 0 .../pyomo_philosophy/expressions/overview.rst | 0 .../pyomo_philosophy/expressions/performance.rst | 0 .../solvers/gdpopt.rst | 0 .../solvers/gdpopt_flowchart.png | Bin .../solvers/mcpp.rst | 0 .../solvers/mindtpy.rst | 0 .../solvers/multistart.rst | 0 .../solvers/persistent_solvers.rst | 0 .../solvers/pynumero/api.rst | 0 .../solvers/pynumero/backward_compatibility.rst | 0 .../solvers/pynumero/index.rst | 0 .../solvers/pynumero/installation.rst | 0 .../pynumero/pynumero.interfaces.ampl_nlp.rst | 0 .../pynumero/pynumero.interfaces.asl_nlp.rst | 0 .../pynumero/pynumero.interfaces.extended_nlp.rst | 0 .../pynumero.interfaces.external_grey_box_model.rst | 0 .../solvers/pynumero/pynumero.interfaces.nlp.rst | 0 .../pynumero/pynumero.interfaces.projected_nlp.rst | 0 .../pynumero.interfaces.pyomo_grey_box_nlp.rst | 0 .../pynumero/pynumero.interfaces.pyomo_nlp.rst | 0 .../solvers/pynumero/pynumero.interfaces.rst | 0 .../solvers/pynumero/pynumero.linalg.base.rst | 0 .../solvers/pynumero/pynumero.linalg.ma27.rst | 0 .../solvers/pynumero/pynumero.linalg.ma57.rst | 0 .../solvers/pynumero/pynumero.linalg.mumps.rst | 0 .../solvers/pynumero/pynumero.linalg.rst | 0 .../solvers/pynumero/pynumero.linalg.scipy.rst | 0 .../pynumero/pynumero.sparse.block_vector.rst | 0 .../solvers/pynumero/pynumero.sparse.rst | 0 .../tutorial.block_vectors_and_matrices.rst | 0 .../pynumero/tutorial.linear_solver_interfaces.rst | 0 .../solvers/pynumero/tutorial.mpi_blocks.rst | 0 .../solvers/pynumero/tutorial.nlp_interfaces.rst | 0 .../solvers/pynumero/tutorial.rst | 0 .../solvers/pyros.rst | 0 .../solvers/trustregion.rst | 0 .../solvers/z3_interface.rst | 0 .../{how_to_guide => howto}/contribution_guide.rst | 0 doc/OnlineDocs/{how_to_guide => howto}/index.rst | 0 .../{how_to_guide => howto}/working_models.rst | 0 doc/OnlineDocs/index.rst | 8 ++++---- .../{reference_guide => reference}/bibliography.rst | 0 .../{reference_guide => reference}/future.rst | 0 .../{reference_guide => reference}/index.rst | 2 +- .../library_reference/aml/index.rst | 0 .../library_reference/appsi/appsi.base.rst | 0 .../library_reference/appsi/appsi.rst | 0 .../library_reference/appsi/appsi.solvers.cbc.rst | 0 .../library_reference/appsi/appsi.solvers.cplex.rst | 0 .../appsi/appsi.solvers.gurobi.rst | 0 .../library_reference/appsi/appsi.solvers.highs.rst | 0 .../library_reference/appsi/appsi.solvers.ipopt.rst | 0 .../appsi/appsi.solvers.maingo.rst | 0 .../library_reference/appsi/appsi.solvers.rst | 0 .../library_reference/common/config.rst | 0 .../library_reference/common/dependencies.rst | 0 .../library_reference/common/deprecation.rst | 0 .../library_reference/common/enums.rst | 0 .../library_reference/common/errors.rst | 0 .../library_reference/common/fileutils.rst | 0 .../library_reference/common/formatting.rst | 0 .../library_reference/common/index.rst | 0 .../library_reference/common/tempfiles.rst | 0 .../library_reference/common/timing.rst | 0 .../library_reference/data/index.rst | 0 .../library_reference/expressions/building.rst | 0 .../library_reference/expressions/classes.rst | 0 .../expressions/context_managers.rst | 0 .../library_reference/expressions/index.rst | 0 .../library_reference/expressions/managing.rst | 0 .../library_reference/expressions/visitors.rst | 0 .../library_reference/index.rst | 0 .../library_reference/kernel/base.rst | 0 .../library_reference/kernel/block.rst | 0 .../library_reference/kernel/conic.rst | 0 .../library_reference/kernel/constraint.rst | 0 .../library_reference/kernel/dict_container.rst | 0 .../kernel/examples/aml_example.py | 0 .../library_reference/kernel/examples/conic.py | 0 .../kernel/examples/kernel_containers.py | 0 .../kernel/examples/kernel_example.py | 0 .../kernel/examples/kernel_solving.py | 0 .../kernel/examples/kernel_subclassing.py | 0 .../kernel/examples/transformer.py | 0 .../library_reference/kernel/expression.rst | 0 .../kernel/heterogeneous_container.rst | 0 .../kernel/homogeneous_container.rst | 0 .../library_reference/kernel/index.rst | 0 .../library_reference/kernel/list_container.rst | 0 .../library_reference/kernel/objective.rst | 0 .../library_reference/kernel/parameter.rst | 0 .../library_reference/kernel/piecewise/index.rst | 0 .../kernel/piecewise/piecewise.rst | 0 .../kernel/piecewise/piecewise_nd.rst | 0 .../library_reference/kernel/piecewise/util.rst | 0 .../library_reference/kernel/sos.rst | 0 .../library_reference/kernel/suffix.rst | 0 .../library_reference/kernel/syntax_comparison.rst | 0 .../library_reference/kernel/tuple_container.rst | 0 .../library_reference/kernel/variable.rst | 0 .../library_reference/solvers/cplex_persistent.rst | 0 .../library_reference/solvers/gams.rst | 0 .../library_reference/solvers/gurobi_direct.rst | 0 .../library_reference/solvers/gurobi_persistent.rst | 0 .../library_reference/solvers/index.rst | 0 .../library_reference/solvers/xpress_persistent.rst | 0 190 files changed, 6 insertions(+), 6 deletions(-) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/alternative_solutions.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/communities_8pp.png (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/communities_decode_1.png (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/community.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/doe/CCSI-license.txt (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/doe/FIM_sensitivity.png (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/doe/doe.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/doe/flowchart.png (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/doe/grid-1.png (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/doe/reactor.png (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/doe/uml.png (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/iis.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/incidence/api.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/incidence/config.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/incidence/connected.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/incidence/dulmage_mendelsohn.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/incidence/incidence.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/incidence/index.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/incidence/interface.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/incidence/matching.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/incidence/overview.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/incidence/scc_solver.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/incidence/triangularize.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/incidence/tutorial.bt.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/incidence/tutorial.btsolve.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/incidence/tutorial.dm.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/incidence/tutorial.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/mpc/api.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/mpc/conversion.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/mpc/data.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/mpc/examples.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/mpc/faq.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/mpc/index.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/mpc/interface.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/mpc/modeling.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/mpc/overview.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/parmest/api.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/parmest/boxplot.png (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/parmest/covariance.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/parmest/datarec.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/parmest/driver.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/parmest/examples.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/parmest/graphics.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/parmest/index.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/parmest/installation.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/parmest/overview.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/parmest/pairwise_plot_CI.png (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/parmest/pairwise_plot_LR.png (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/parmest/parallel.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/parmest/scencreate.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/analysis/sensitivity_toolbox.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/contrib_index.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/developer_utilities/config.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/developer_utilities/deprecation.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/experimental/solvers.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/index.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/Constraints.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/Expressions.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/Objectives.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/Parameters.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/Sets.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/Suffixes.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/Variables.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/dae.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/gdp/concepts.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/gdp/index.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/gdp/modeling.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/gdp/solving.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/index.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/mpec.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/network.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/reduce_points_demo.png (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/sos_constraints.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling/units_container.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling_utilities/flattener/index.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling_utilities/flattener/motivation.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling_utilities/flattener/reference.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling_utilities/latex_printer.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling_utilities/preprocessing.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/modeling_utilities/scaling.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/pyomo_philosophy/expressions/design.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/pyomo_philosophy/expressions/index.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/pyomo_philosophy/expressions/managing.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/pyomo_philosophy/expressions/overview.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/pyomo_philosophy/expressions/performance.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/gdpopt.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/gdpopt_flowchart.png (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/mcpp.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/mindtpy.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/multistart.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/persistent_solvers.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/api.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/backward_compatibility.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/index.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/installation.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.interfaces.ampl_nlp.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.interfaces.asl_nlp.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.interfaces.extended_nlp.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.interfaces.external_grey_box_model.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.interfaces.nlp.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.interfaces.projected_nlp.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.interfaces.pyomo_nlp.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.interfaces.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.linalg.base.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.linalg.ma27.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.linalg.ma57.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.linalg.mumps.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.linalg.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.linalg.scipy.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.sparse.block_vector.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/pynumero.sparse.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/tutorial.block_vectors_and_matrices.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/tutorial.linear_solver_interfaces.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/tutorial.mpi_blocks.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/tutorial.nlp_interfaces.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pynumero/tutorial.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/pyros.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/trustregion.rst (100%) rename doc/OnlineDocs/{user_explanations => explanation}/solvers/z3_interface.rst (100%) rename doc/OnlineDocs/{how_to_guide => howto}/contribution_guide.rst (100%) rename doc/OnlineDocs/{how_to_guide => howto}/index.rst (100%) rename doc/OnlineDocs/{how_to_guide => howto}/working_models.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/bibliography.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/future.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/index.rst (75%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/aml/index.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/appsi/appsi.base.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/appsi/appsi.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/appsi/appsi.solvers.cbc.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/appsi/appsi.solvers.cplex.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/appsi/appsi.solvers.gurobi.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/appsi/appsi.solvers.highs.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/appsi/appsi.solvers.ipopt.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/appsi/appsi.solvers.maingo.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/appsi/appsi.solvers.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/common/config.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/common/dependencies.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/common/deprecation.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/common/enums.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/common/errors.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/common/fileutils.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/common/formatting.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/common/index.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/common/tempfiles.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/common/timing.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/data/index.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/expressions/building.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/expressions/classes.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/expressions/context_managers.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/expressions/index.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/expressions/managing.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/expressions/visitors.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/index.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/base.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/block.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/conic.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/constraint.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/dict_container.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/examples/aml_example.py (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/examples/conic.py (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/examples/kernel_containers.py (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/examples/kernel_example.py (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/examples/kernel_solving.py (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/examples/kernel_subclassing.py (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/examples/transformer.py (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/expression.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/heterogeneous_container.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/homogeneous_container.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/index.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/list_container.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/objective.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/parameter.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/piecewise/index.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/piecewise/piecewise.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/piecewise/piecewise_nd.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/piecewise/util.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/sos.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/suffix.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/syntax_comparison.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/tuple_container.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/kernel/variable.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/solvers/cplex_persistent.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/solvers/gams.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/solvers/gurobi_direct.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/solvers/gurobi_persistent.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/solvers/index.rst (100%) rename doc/OnlineDocs/{reference_guide => reference}/library_reference/solvers/xpress_persistent.rst (100%) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 5d45ae55a8b..3c89d4186d5 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -44,7 +44,7 @@ generate_spy_files(os.path.abspath('src')) generate_spy_files( - os.path.abspath(os.path.join('reference_guide', 'library_reference', 'kernel', 'examples')) + os.path.abspath(os.path.join('reference', 'library_reference', 'kernel', 'examples')) ) finally: sys.path.pop(0) diff --git a/doc/OnlineDocs/user_explanations/analysis/alternative_solutions.rst b/doc/OnlineDocs/explanation/analysis/alternative_solutions.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/alternative_solutions.rst rename to doc/OnlineDocs/explanation/analysis/alternative_solutions.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/communities_8pp.png b/doc/OnlineDocs/explanation/analysis/communities_8pp.png similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/communities_8pp.png rename to doc/OnlineDocs/explanation/analysis/communities_8pp.png diff --git a/doc/OnlineDocs/user_explanations/analysis/communities_decode_1.png b/doc/OnlineDocs/explanation/analysis/communities_decode_1.png similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/communities_decode_1.png rename to doc/OnlineDocs/explanation/analysis/communities_decode_1.png diff --git a/doc/OnlineDocs/user_explanations/analysis/community.rst b/doc/OnlineDocs/explanation/analysis/community.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/community.rst rename to doc/OnlineDocs/explanation/analysis/community.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/doe/CCSI-license.txt b/doc/OnlineDocs/explanation/analysis/doe/CCSI-license.txt similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/doe/CCSI-license.txt rename to doc/OnlineDocs/explanation/analysis/doe/CCSI-license.txt diff --git a/doc/OnlineDocs/user_explanations/analysis/doe/FIM_sensitivity.png b/doc/OnlineDocs/explanation/analysis/doe/FIM_sensitivity.png similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/doe/FIM_sensitivity.png rename to doc/OnlineDocs/explanation/analysis/doe/FIM_sensitivity.png diff --git a/doc/OnlineDocs/user_explanations/analysis/doe/doe.rst b/doc/OnlineDocs/explanation/analysis/doe/doe.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/doe/doe.rst rename to doc/OnlineDocs/explanation/analysis/doe/doe.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/doe/flowchart.png b/doc/OnlineDocs/explanation/analysis/doe/flowchart.png similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/doe/flowchart.png rename to doc/OnlineDocs/explanation/analysis/doe/flowchart.png diff --git a/doc/OnlineDocs/user_explanations/analysis/doe/grid-1.png b/doc/OnlineDocs/explanation/analysis/doe/grid-1.png similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/doe/grid-1.png rename to doc/OnlineDocs/explanation/analysis/doe/grid-1.png diff --git a/doc/OnlineDocs/user_explanations/analysis/doe/reactor.png b/doc/OnlineDocs/explanation/analysis/doe/reactor.png similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/doe/reactor.png rename to doc/OnlineDocs/explanation/analysis/doe/reactor.png diff --git a/doc/OnlineDocs/user_explanations/analysis/doe/uml.png b/doc/OnlineDocs/explanation/analysis/doe/uml.png similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/doe/uml.png rename to doc/OnlineDocs/explanation/analysis/doe/uml.png diff --git a/doc/OnlineDocs/user_explanations/analysis/iis.rst b/doc/OnlineDocs/explanation/analysis/iis.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/iis.rst rename to doc/OnlineDocs/explanation/analysis/iis.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/incidence/api.rst b/doc/OnlineDocs/explanation/analysis/incidence/api.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/incidence/api.rst rename to doc/OnlineDocs/explanation/analysis/incidence/api.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/incidence/config.rst b/doc/OnlineDocs/explanation/analysis/incidence/config.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/incidence/config.rst rename to doc/OnlineDocs/explanation/analysis/incidence/config.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/incidence/connected.rst b/doc/OnlineDocs/explanation/analysis/incidence/connected.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/incidence/connected.rst rename to doc/OnlineDocs/explanation/analysis/incidence/connected.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/incidence/dulmage_mendelsohn.rst b/doc/OnlineDocs/explanation/analysis/incidence/dulmage_mendelsohn.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/incidence/dulmage_mendelsohn.rst rename to doc/OnlineDocs/explanation/analysis/incidence/dulmage_mendelsohn.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/incidence/incidence.rst b/doc/OnlineDocs/explanation/analysis/incidence/incidence.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/incidence/incidence.rst rename to doc/OnlineDocs/explanation/analysis/incidence/incidence.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/incidence/index.rst b/doc/OnlineDocs/explanation/analysis/incidence/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/incidence/index.rst rename to doc/OnlineDocs/explanation/analysis/incidence/index.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/incidence/interface.rst b/doc/OnlineDocs/explanation/analysis/incidence/interface.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/incidence/interface.rst rename to doc/OnlineDocs/explanation/analysis/incidence/interface.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/incidence/matching.rst b/doc/OnlineDocs/explanation/analysis/incidence/matching.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/incidence/matching.rst rename to doc/OnlineDocs/explanation/analysis/incidence/matching.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/incidence/overview.rst b/doc/OnlineDocs/explanation/analysis/incidence/overview.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/incidence/overview.rst rename to doc/OnlineDocs/explanation/analysis/incidence/overview.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/incidence/scc_solver.rst b/doc/OnlineDocs/explanation/analysis/incidence/scc_solver.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/incidence/scc_solver.rst rename to doc/OnlineDocs/explanation/analysis/incidence/scc_solver.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/incidence/triangularize.rst b/doc/OnlineDocs/explanation/analysis/incidence/triangularize.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/incidence/triangularize.rst rename to doc/OnlineDocs/explanation/analysis/incidence/triangularize.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.bt.rst b/doc/OnlineDocs/explanation/analysis/incidence/tutorial.bt.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.bt.rst rename to doc/OnlineDocs/explanation/analysis/incidence/tutorial.bt.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.btsolve.rst b/doc/OnlineDocs/explanation/analysis/incidence/tutorial.btsolve.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.btsolve.rst rename to doc/OnlineDocs/explanation/analysis/incidence/tutorial.btsolve.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.dm.rst b/doc/OnlineDocs/explanation/analysis/incidence/tutorial.dm.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.dm.rst rename to doc/OnlineDocs/explanation/analysis/incidence/tutorial.dm.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.rst b/doc/OnlineDocs/explanation/analysis/incidence/tutorial.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/incidence/tutorial.rst rename to doc/OnlineDocs/explanation/analysis/incidence/tutorial.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/mpc/api.rst b/doc/OnlineDocs/explanation/analysis/mpc/api.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/mpc/api.rst rename to doc/OnlineDocs/explanation/analysis/mpc/api.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/mpc/conversion.rst b/doc/OnlineDocs/explanation/analysis/mpc/conversion.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/mpc/conversion.rst rename to doc/OnlineDocs/explanation/analysis/mpc/conversion.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/mpc/data.rst b/doc/OnlineDocs/explanation/analysis/mpc/data.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/mpc/data.rst rename to doc/OnlineDocs/explanation/analysis/mpc/data.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/mpc/examples.rst b/doc/OnlineDocs/explanation/analysis/mpc/examples.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/mpc/examples.rst rename to doc/OnlineDocs/explanation/analysis/mpc/examples.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/mpc/faq.rst b/doc/OnlineDocs/explanation/analysis/mpc/faq.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/mpc/faq.rst rename to doc/OnlineDocs/explanation/analysis/mpc/faq.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/mpc/index.rst b/doc/OnlineDocs/explanation/analysis/mpc/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/mpc/index.rst rename to doc/OnlineDocs/explanation/analysis/mpc/index.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/mpc/interface.rst b/doc/OnlineDocs/explanation/analysis/mpc/interface.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/mpc/interface.rst rename to doc/OnlineDocs/explanation/analysis/mpc/interface.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/mpc/modeling.rst b/doc/OnlineDocs/explanation/analysis/mpc/modeling.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/mpc/modeling.rst rename to doc/OnlineDocs/explanation/analysis/mpc/modeling.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/mpc/overview.rst b/doc/OnlineDocs/explanation/analysis/mpc/overview.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/mpc/overview.rst rename to doc/OnlineDocs/explanation/analysis/mpc/overview.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/api.rst b/doc/OnlineDocs/explanation/analysis/parmest/api.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/parmest/api.rst rename to doc/OnlineDocs/explanation/analysis/parmest/api.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/boxplot.png b/doc/OnlineDocs/explanation/analysis/parmest/boxplot.png similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/parmest/boxplot.png rename to doc/OnlineDocs/explanation/analysis/parmest/boxplot.png diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/covariance.rst b/doc/OnlineDocs/explanation/analysis/parmest/covariance.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/parmest/covariance.rst rename to doc/OnlineDocs/explanation/analysis/parmest/covariance.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/datarec.rst b/doc/OnlineDocs/explanation/analysis/parmest/datarec.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/parmest/datarec.rst rename to doc/OnlineDocs/explanation/analysis/parmest/datarec.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/driver.rst b/doc/OnlineDocs/explanation/analysis/parmest/driver.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/parmest/driver.rst rename to doc/OnlineDocs/explanation/analysis/parmest/driver.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/examples.rst b/doc/OnlineDocs/explanation/analysis/parmest/examples.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/parmest/examples.rst rename to doc/OnlineDocs/explanation/analysis/parmest/examples.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/graphics.rst b/doc/OnlineDocs/explanation/analysis/parmest/graphics.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/parmest/graphics.rst rename to doc/OnlineDocs/explanation/analysis/parmest/graphics.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/index.rst b/doc/OnlineDocs/explanation/analysis/parmest/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/parmest/index.rst rename to doc/OnlineDocs/explanation/analysis/parmest/index.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/installation.rst b/doc/OnlineDocs/explanation/analysis/parmest/installation.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/parmest/installation.rst rename to doc/OnlineDocs/explanation/analysis/parmest/installation.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/overview.rst b/doc/OnlineDocs/explanation/analysis/parmest/overview.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/parmest/overview.rst rename to doc/OnlineDocs/explanation/analysis/parmest/overview.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/pairwise_plot_CI.png b/doc/OnlineDocs/explanation/analysis/parmest/pairwise_plot_CI.png similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/parmest/pairwise_plot_CI.png rename to doc/OnlineDocs/explanation/analysis/parmest/pairwise_plot_CI.png diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/pairwise_plot_LR.png b/doc/OnlineDocs/explanation/analysis/parmest/pairwise_plot_LR.png similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/parmest/pairwise_plot_LR.png rename to doc/OnlineDocs/explanation/analysis/parmest/pairwise_plot_LR.png diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/parallel.rst b/doc/OnlineDocs/explanation/analysis/parmest/parallel.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/parmest/parallel.rst rename to doc/OnlineDocs/explanation/analysis/parmest/parallel.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/parmest/scencreate.rst b/doc/OnlineDocs/explanation/analysis/parmest/scencreate.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/parmest/scencreate.rst rename to doc/OnlineDocs/explanation/analysis/parmest/scencreate.rst diff --git a/doc/OnlineDocs/user_explanations/analysis/sensitivity_toolbox.rst b/doc/OnlineDocs/explanation/analysis/sensitivity_toolbox.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/analysis/sensitivity_toolbox.rst rename to doc/OnlineDocs/explanation/analysis/sensitivity_toolbox.rst diff --git a/doc/OnlineDocs/user_explanations/contrib_index.rst b/doc/OnlineDocs/explanation/contrib_index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/contrib_index.rst rename to doc/OnlineDocs/explanation/contrib_index.rst diff --git a/doc/OnlineDocs/user_explanations/developer_utilities/config.rst b/doc/OnlineDocs/explanation/developer_utilities/config.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/developer_utilities/config.rst rename to doc/OnlineDocs/explanation/developer_utilities/config.rst diff --git a/doc/OnlineDocs/user_explanations/developer_utilities/deprecation.rst b/doc/OnlineDocs/explanation/developer_utilities/deprecation.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/developer_utilities/deprecation.rst rename to doc/OnlineDocs/explanation/developer_utilities/deprecation.rst diff --git a/doc/OnlineDocs/user_explanations/experimental/solvers.rst b/doc/OnlineDocs/explanation/experimental/solvers.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/experimental/solvers.rst rename to doc/OnlineDocs/explanation/experimental/solvers.rst diff --git a/doc/OnlineDocs/user_explanations/index.rst b/doc/OnlineDocs/explanation/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/index.rst rename to doc/OnlineDocs/explanation/index.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/Constraints.rst b/doc/OnlineDocs/explanation/modeling/Constraints.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/Constraints.rst rename to doc/OnlineDocs/explanation/modeling/Constraints.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/Expressions.rst b/doc/OnlineDocs/explanation/modeling/Expressions.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/Expressions.rst rename to doc/OnlineDocs/explanation/modeling/Expressions.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/Objectives.rst b/doc/OnlineDocs/explanation/modeling/Objectives.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/Objectives.rst rename to doc/OnlineDocs/explanation/modeling/Objectives.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/Parameters.rst b/doc/OnlineDocs/explanation/modeling/Parameters.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/Parameters.rst rename to doc/OnlineDocs/explanation/modeling/Parameters.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/Sets.rst b/doc/OnlineDocs/explanation/modeling/Sets.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/Sets.rst rename to doc/OnlineDocs/explanation/modeling/Sets.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/Suffixes.rst b/doc/OnlineDocs/explanation/modeling/Suffixes.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/Suffixes.rst rename to doc/OnlineDocs/explanation/modeling/Suffixes.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/Variables.rst b/doc/OnlineDocs/explanation/modeling/Variables.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/Variables.rst rename to doc/OnlineDocs/explanation/modeling/Variables.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/dae.rst b/doc/OnlineDocs/explanation/modeling/dae.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/dae.rst rename to doc/OnlineDocs/explanation/modeling/dae.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/gdp/concepts.rst b/doc/OnlineDocs/explanation/modeling/gdp/concepts.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/gdp/concepts.rst rename to doc/OnlineDocs/explanation/modeling/gdp/concepts.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/gdp/index.rst b/doc/OnlineDocs/explanation/modeling/gdp/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/gdp/index.rst rename to doc/OnlineDocs/explanation/modeling/gdp/index.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/gdp/modeling.rst b/doc/OnlineDocs/explanation/modeling/gdp/modeling.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/gdp/modeling.rst rename to doc/OnlineDocs/explanation/modeling/gdp/modeling.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/gdp/solving.rst b/doc/OnlineDocs/explanation/modeling/gdp/solving.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/gdp/solving.rst rename to doc/OnlineDocs/explanation/modeling/gdp/solving.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/index.rst b/doc/OnlineDocs/explanation/modeling/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/index.rst rename to doc/OnlineDocs/explanation/modeling/index.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/mpec.rst b/doc/OnlineDocs/explanation/modeling/mpec.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/mpec.rst rename to doc/OnlineDocs/explanation/modeling/mpec.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/network.rst b/doc/OnlineDocs/explanation/modeling/network.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/network.rst rename to doc/OnlineDocs/explanation/modeling/network.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/reduce_points_demo.png b/doc/OnlineDocs/explanation/modeling/reduce_points_demo.png similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/reduce_points_demo.png rename to doc/OnlineDocs/explanation/modeling/reduce_points_demo.png diff --git a/doc/OnlineDocs/user_explanations/modeling/sos_constraints.rst b/doc/OnlineDocs/explanation/modeling/sos_constraints.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/sos_constraints.rst rename to doc/OnlineDocs/explanation/modeling/sos_constraints.rst diff --git a/doc/OnlineDocs/user_explanations/modeling/units_container.rst b/doc/OnlineDocs/explanation/modeling/units_container.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling/units_container.rst rename to doc/OnlineDocs/explanation/modeling/units_container.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_utilities/flattener/index.rst b/doc/OnlineDocs/explanation/modeling_utilities/flattener/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling_utilities/flattener/index.rst rename to doc/OnlineDocs/explanation/modeling_utilities/flattener/index.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_utilities/flattener/motivation.rst b/doc/OnlineDocs/explanation/modeling_utilities/flattener/motivation.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling_utilities/flattener/motivation.rst rename to doc/OnlineDocs/explanation/modeling_utilities/flattener/motivation.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_utilities/flattener/reference.rst b/doc/OnlineDocs/explanation/modeling_utilities/flattener/reference.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling_utilities/flattener/reference.rst rename to doc/OnlineDocs/explanation/modeling_utilities/flattener/reference.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_utilities/latex_printer.rst b/doc/OnlineDocs/explanation/modeling_utilities/latex_printer.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling_utilities/latex_printer.rst rename to doc/OnlineDocs/explanation/modeling_utilities/latex_printer.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_utilities/preprocessing.rst b/doc/OnlineDocs/explanation/modeling_utilities/preprocessing.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling_utilities/preprocessing.rst rename to doc/OnlineDocs/explanation/modeling_utilities/preprocessing.rst diff --git a/doc/OnlineDocs/user_explanations/modeling_utilities/scaling.rst b/doc/OnlineDocs/explanation/modeling_utilities/scaling.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/modeling_utilities/scaling.rst rename to doc/OnlineDocs/explanation/modeling_utilities/scaling.rst diff --git a/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/design.rst b/doc/OnlineDocs/explanation/pyomo_philosophy/expressions/design.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/design.rst rename to doc/OnlineDocs/explanation/pyomo_philosophy/expressions/design.rst diff --git a/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/index.rst b/doc/OnlineDocs/explanation/pyomo_philosophy/expressions/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/index.rst rename to doc/OnlineDocs/explanation/pyomo_philosophy/expressions/index.rst diff --git a/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/managing.rst b/doc/OnlineDocs/explanation/pyomo_philosophy/expressions/managing.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/managing.rst rename to doc/OnlineDocs/explanation/pyomo_philosophy/expressions/managing.rst diff --git a/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/overview.rst b/doc/OnlineDocs/explanation/pyomo_philosophy/expressions/overview.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/overview.rst rename to doc/OnlineDocs/explanation/pyomo_philosophy/expressions/overview.rst diff --git a/doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/performance.rst b/doc/OnlineDocs/explanation/pyomo_philosophy/expressions/performance.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/pyomo_philosophy/expressions/performance.rst rename to doc/OnlineDocs/explanation/pyomo_philosophy/expressions/performance.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/gdpopt.rst b/doc/OnlineDocs/explanation/solvers/gdpopt.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/gdpopt.rst rename to doc/OnlineDocs/explanation/solvers/gdpopt.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/gdpopt_flowchart.png b/doc/OnlineDocs/explanation/solvers/gdpopt_flowchart.png similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/gdpopt_flowchart.png rename to doc/OnlineDocs/explanation/solvers/gdpopt_flowchart.png diff --git a/doc/OnlineDocs/user_explanations/solvers/mcpp.rst b/doc/OnlineDocs/explanation/solvers/mcpp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/mcpp.rst rename to doc/OnlineDocs/explanation/solvers/mcpp.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/mindtpy.rst b/doc/OnlineDocs/explanation/solvers/mindtpy.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/mindtpy.rst rename to doc/OnlineDocs/explanation/solvers/mindtpy.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/multistart.rst b/doc/OnlineDocs/explanation/solvers/multistart.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/multistart.rst rename to doc/OnlineDocs/explanation/solvers/multistart.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/persistent_solvers.rst b/doc/OnlineDocs/explanation/solvers/persistent_solvers.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/persistent_solvers.rst rename to doc/OnlineDocs/explanation/solvers/persistent_solvers.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/api.rst b/doc/OnlineDocs/explanation/solvers/pynumero/api.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/api.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/api.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/backward_compatibility.rst b/doc/OnlineDocs/explanation/solvers/pynumero/backward_compatibility.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/backward_compatibility.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/backward_compatibility.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/index.rst b/doc/OnlineDocs/explanation/solvers/pynumero/index.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/index.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/index.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/installation.rst b/doc/OnlineDocs/explanation/solvers/pynumero/installation.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/installation.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/installation.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.ampl_nlp.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.ampl_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.ampl_nlp.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.ampl_nlp.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.asl_nlp.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.asl_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.asl_nlp.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.asl_nlp.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.extended_nlp.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.extended_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.extended_nlp.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.extended_nlp.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.external_grey_box_model.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.external_grey_box_model.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.external_grey_box_model.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.external_grey_box_model.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.nlp.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.nlp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.nlp.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.nlp.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.projected_nlp.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.projected_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.projected_nlp.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.projected_nlp.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.pyomo_nlp.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.pyomo_nlp.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.pyomo_nlp.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.pyomo_nlp.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.interfaces.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.base.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.base.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.base.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.base.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.ma27.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.ma27.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.ma27.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.ma27.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.ma57.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.ma57.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.ma57.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.ma57.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.mumps.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.mumps.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.mumps.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.mumps.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.scipy.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.scipy.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.linalg.scipy.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.scipy.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.sparse.block_vector.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.sparse.block_vector.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.sparse.block_vector.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.sparse.block_vector.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.sparse.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.sparse.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/pynumero.sparse.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/pynumero.sparse.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.block_vectors_and_matrices.rst b/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.block_vectors_and_matrices.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.linear_solver_interfaces.rst b/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.linear_solver_interfaces.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.linear_solver_interfaces.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/tutorial.linear_solver_interfaces.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.mpi_blocks.rst b/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.mpi_blocks.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.mpi_blocks.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/tutorial.mpi_blocks.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.nlp_interfaces.rst b/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.nlp_interfaces.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.nlp_interfaces.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/tutorial.nlp_interfaces.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.rst b/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pynumero/tutorial.rst rename to doc/OnlineDocs/explanation/solvers/pynumero/tutorial.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/pyros.rst b/doc/OnlineDocs/explanation/solvers/pyros.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/pyros.rst rename to doc/OnlineDocs/explanation/solvers/pyros.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/trustregion.rst b/doc/OnlineDocs/explanation/solvers/trustregion.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/trustregion.rst rename to doc/OnlineDocs/explanation/solvers/trustregion.rst diff --git a/doc/OnlineDocs/user_explanations/solvers/z3_interface.rst b/doc/OnlineDocs/explanation/solvers/z3_interface.rst similarity index 100% rename from doc/OnlineDocs/user_explanations/solvers/z3_interface.rst rename to doc/OnlineDocs/explanation/solvers/z3_interface.rst diff --git a/doc/OnlineDocs/how_to_guide/contribution_guide.rst b/doc/OnlineDocs/howto/contribution_guide.rst similarity index 100% rename from doc/OnlineDocs/how_to_guide/contribution_guide.rst rename to doc/OnlineDocs/howto/contribution_guide.rst diff --git a/doc/OnlineDocs/how_to_guide/index.rst b/doc/OnlineDocs/howto/index.rst similarity index 100% rename from doc/OnlineDocs/how_to_guide/index.rst rename to doc/OnlineDocs/howto/index.rst diff --git a/doc/OnlineDocs/how_to_guide/working_models.rst b/doc/OnlineDocs/howto/working_models.rst similarity index 100% rename from doc/OnlineDocs/how_to_guide/working_models.rst rename to doc/OnlineDocs/howto/working_models.rst diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index 3842a6beae5..fb5f64cbf6f 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -60,7 +60,7 @@ with a diverse set of optimization capabilities. | :doc:`Experimental` | :doc:`Kernel` - Reference Guide - | :doc:`Library Reference ` + | :doc:`Library Reference ` | :doc:`Common Warnings and Errors` | :doc:`Preview capabilities through ``pyomo.__future__`` ` @@ -69,9 +69,9 @@ with a diverse set of optimization capabilities. :maxdepth: 2 Getting Started - Hot-To Guides - User Explanations - Reference Guide + How-To Guides + User Explanations + Reference Guides Pyomo Resources diff --git a/doc/OnlineDocs/reference_guide/bibliography.rst b/doc/OnlineDocs/reference/bibliography.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/bibliography.rst rename to doc/OnlineDocs/reference/bibliography.rst diff --git a/doc/OnlineDocs/reference_guide/future.rst b/doc/OnlineDocs/reference/future.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/future.rst rename to doc/OnlineDocs/reference/future.rst diff --git a/doc/OnlineDocs/reference_guide/index.rst b/doc/OnlineDocs/reference/index.rst similarity index 75% rename from doc/OnlineDocs/reference_guide/index.rst rename to doc/OnlineDocs/reference/index.rst index 391e7995de5..7e66d29db96 100644 --- a/doc/OnlineDocs/reference_guide/index.rst +++ b/doc/OnlineDocs/reference/index.rst @@ -12,6 +12,6 @@ Reference Guide Bibliography ------------ -:doc:`Bibliography ` +:doc:`Bibliography ` diff --git a/doc/OnlineDocs/reference_guide/library_reference/aml/index.rst b/doc/OnlineDocs/reference/library_reference/aml/index.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/aml/index.rst rename to doc/OnlineDocs/reference/library_reference/aml/index.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.base.rst b/doc/OnlineDocs/reference/library_reference/appsi/appsi.base.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.base.rst rename to doc/OnlineDocs/reference/library_reference/appsi/appsi.base.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.rst b/doc/OnlineDocs/reference/library_reference/appsi/appsi.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.rst rename to doc/OnlineDocs/reference/library_reference/appsi/appsi.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.cbc.rst b/doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.cbc.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.cbc.rst rename to doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.cbc.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.cplex.rst b/doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.cplex.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.cplex.rst rename to doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.cplex.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.gurobi.rst b/doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.gurobi.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.gurobi.rst rename to doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.gurobi.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.highs.rst b/doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.highs.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.highs.rst rename to doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.highs.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.ipopt.rst b/doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.ipopt.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.ipopt.rst rename to doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.ipopt.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.maingo.rst b/doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.maingo.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.maingo.rst rename to doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.maingo.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.rst b/doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/appsi/appsi.solvers.rst rename to doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/config.rst b/doc/OnlineDocs/reference/library_reference/common/config.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/common/config.rst rename to doc/OnlineDocs/reference/library_reference/common/config.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/dependencies.rst b/doc/OnlineDocs/reference/library_reference/common/dependencies.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/common/dependencies.rst rename to doc/OnlineDocs/reference/library_reference/common/dependencies.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/deprecation.rst b/doc/OnlineDocs/reference/library_reference/common/deprecation.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/common/deprecation.rst rename to doc/OnlineDocs/reference/library_reference/common/deprecation.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/enums.rst b/doc/OnlineDocs/reference/library_reference/common/enums.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/common/enums.rst rename to doc/OnlineDocs/reference/library_reference/common/enums.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/errors.rst b/doc/OnlineDocs/reference/library_reference/common/errors.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/common/errors.rst rename to doc/OnlineDocs/reference/library_reference/common/errors.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/fileutils.rst b/doc/OnlineDocs/reference/library_reference/common/fileutils.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/common/fileutils.rst rename to doc/OnlineDocs/reference/library_reference/common/fileutils.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/formatting.rst b/doc/OnlineDocs/reference/library_reference/common/formatting.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/common/formatting.rst rename to doc/OnlineDocs/reference/library_reference/common/formatting.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/index.rst b/doc/OnlineDocs/reference/library_reference/common/index.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/common/index.rst rename to doc/OnlineDocs/reference/library_reference/common/index.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/tempfiles.rst b/doc/OnlineDocs/reference/library_reference/common/tempfiles.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/common/tempfiles.rst rename to doc/OnlineDocs/reference/library_reference/common/tempfiles.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/common/timing.rst b/doc/OnlineDocs/reference/library_reference/common/timing.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/common/timing.rst rename to doc/OnlineDocs/reference/library_reference/common/timing.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/data/index.rst b/doc/OnlineDocs/reference/library_reference/data/index.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/data/index.rst rename to doc/OnlineDocs/reference/library_reference/data/index.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/expressions/building.rst b/doc/OnlineDocs/reference/library_reference/expressions/building.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/expressions/building.rst rename to doc/OnlineDocs/reference/library_reference/expressions/building.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/expressions/classes.rst b/doc/OnlineDocs/reference/library_reference/expressions/classes.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/expressions/classes.rst rename to doc/OnlineDocs/reference/library_reference/expressions/classes.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/expressions/context_managers.rst b/doc/OnlineDocs/reference/library_reference/expressions/context_managers.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/expressions/context_managers.rst rename to doc/OnlineDocs/reference/library_reference/expressions/context_managers.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/expressions/index.rst b/doc/OnlineDocs/reference/library_reference/expressions/index.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/expressions/index.rst rename to doc/OnlineDocs/reference/library_reference/expressions/index.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/expressions/managing.rst b/doc/OnlineDocs/reference/library_reference/expressions/managing.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/expressions/managing.rst rename to doc/OnlineDocs/reference/library_reference/expressions/managing.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/expressions/visitors.rst b/doc/OnlineDocs/reference/library_reference/expressions/visitors.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/expressions/visitors.rst rename to doc/OnlineDocs/reference/library_reference/expressions/visitors.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/index.rst b/doc/OnlineDocs/reference/library_reference/index.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/index.rst rename to doc/OnlineDocs/reference/library_reference/index.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/base.rst b/doc/OnlineDocs/reference/library_reference/kernel/base.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/base.rst rename to doc/OnlineDocs/reference/library_reference/kernel/base.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/block.rst b/doc/OnlineDocs/reference/library_reference/kernel/block.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/block.rst rename to doc/OnlineDocs/reference/library_reference/kernel/block.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/conic.rst b/doc/OnlineDocs/reference/library_reference/kernel/conic.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/conic.rst rename to doc/OnlineDocs/reference/library_reference/kernel/conic.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/constraint.rst b/doc/OnlineDocs/reference/library_reference/kernel/constraint.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/constraint.rst rename to doc/OnlineDocs/reference/library_reference/kernel/constraint.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/dict_container.rst b/doc/OnlineDocs/reference/library_reference/kernel/dict_container.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/dict_container.rst rename to doc/OnlineDocs/reference/library_reference/kernel/dict_container.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/aml_example.py b/doc/OnlineDocs/reference/library_reference/kernel/examples/aml_example.py similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/examples/aml_example.py rename to doc/OnlineDocs/reference/library_reference/kernel/examples/aml_example.py diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/conic.py b/doc/OnlineDocs/reference/library_reference/kernel/examples/conic.py similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/examples/conic.py rename to doc/OnlineDocs/reference/library_reference/kernel/examples/conic.py diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_containers.py b/doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_containers.py similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_containers.py rename to doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_containers.py diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_example.py b/doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_example.py similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_example.py rename to doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_example.py diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_solving.py b/doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_solving.py similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_solving.py rename to doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_solving.py diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_subclassing.py b/doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_subclassing.py similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/examples/kernel_subclassing.py rename to doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_subclassing.py diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/examples/transformer.py b/doc/OnlineDocs/reference/library_reference/kernel/examples/transformer.py similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/examples/transformer.py rename to doc/OnlineDocs/reference/library_reference/kernel/examples/transformer.py diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/expression.rst b/doc/OnlineDocs/reference/library_reference/kernel/expression.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/expression.rst rename to doc/OnlineDocs/reference/library_reference/kernel/expression.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/heterogeneous_container.rst b/doc/OnlineDocs/reference/library_reference/kernel/heterogeneous_container.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/heterogeneous_container.rst rename to doc/OnlineDocs/reference/library_reference/kernel/heterogeneous_container.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/homogeneous_container.rst b/doc/OnlineDocs/reference/library_reference/kernel/homogeneous_container.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/homogeneous_container.rst rename to doc/OnlineDocs/reference/library_reference/kernel/homogeneous_container.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/index.rst b/doc/OnlineDocs/reference/library_reference/kernel/index.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/index.rst rename to doc/OnlineDocs/reference/library_reference/kernel/index.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/list_container.rst b/doc/OnlineDocs/reference/library_reference/kernel/list_container.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/list_container.rst rename to doc/OnlineDocs/reference/library_reference/kernel/list_container.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/objective.rst b/doc/OnlineDocs/reference/library_reference/kernel/objective.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/objective.rst rename to doc/OnlineDocs/reference/library_reference/kernel/objective.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/parameter.rst b/doc/OnlineDocs/reference/library_reference/kernel/parameter.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/parameter.rst rename to doc/OnlineDocs/reference/library_reference/kernel/parameter.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/index.rst b/doc/OnlineDocs/reference/library_reference/kernel/piecewise/index.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/index.rst rename to doc/OnlineDocs/reference/library_reference/kernel/piecewise/index.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/piecewise.rst b/doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/piecewise.rst rename to doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/piecewise_nd.rst b/doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise_nd.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/piecewise_nd.rst rename to doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise_nd.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/util.rst b/doc/OnlineDocs/reference/library_reference/kernel/piecewise/util.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/piecewise/util.rst rename to doc/OnlineDocs/reference/library_reference/kernel/piecewise/util.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/sos.rst b/doc/OnlineDocs/reference/library_reference/kernel/sos.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/sos.rst rename to doc/OnlineDocs/reference/library_reference/kernel/sos.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/suffix.rst b/doc/OnlineDocs/reference/library_reference/kernel/suffix.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/suffix.rst rename to doc/OnlineDocs/reference/library_reference/kernel/suffix.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/syntax_comparison.rst b/doc/OnlineDocs/reference/library_reference/kernel/syntax_comparison.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/syntax_comparison.rst rename to doc/OnlineDocs/reference/library_reference/kernel/syntax_comparison.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/tuple_container.rst b/doc/OnlineDocs/reference/library_reference/kernel/tuple_container.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/tuple_container.rst rename to doc/OnlineDocs/reference/library_reference/kernel/tuple_container.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/kernel/variable.rst b/doc/OnlineDocs/reference/library_reference/kernel/variable.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/kernel/variable.rst rename to doc/OnlineDocs/reference/library_reference/kernel/variable.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/solvers/cplex_persistent.rst b/doc/OnlineDocs/reference/library_reference/solvers/cplex_persistent.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/solvers/cplex_persistent.rst rename to doc/OnlineDocs/reference/library_reference/solvers/cplex_persistent.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/solvers/gams.rst b/doc/OnlineDocs/reference/library_reference/solvers/gams.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/solvers/gams.rst rename to doc/OnlineDocs/reference/library_reference/solvers/gams.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/solvers/gurobi_direct.rst b/doc/OnlineDocs/reference/library_reference/solvers/gurobi_direct.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/solvers/gurobi_direct.rst rename to doc/OnlineDocs/reference/library_reference/solvers/gurobi_direct.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/solvers/gurobi_persistent.rst b/doc/OnlineDocs/reference/library_reference/solvers/gurobi_persistent.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/solvers/gurobi_persistent.rst rename to doc/OnlineDocs/reference/library_reference/solvers/gurobi_persistent.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/solvers/index.rst b/doc/OnlineDocs/reference/library_reference/solvers/index.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/solvers/index.rst rename to doc/OnlineDocs/reference/library_reference/solvers/index.rst diff --git a/doc/OnlineDocs/reference_guide/library_reference/solvers/xpress_persistent.rst b/doc/OnlineDocs/reference/library_reference/solvers/xpress_persistent.rst similarity index 100% rename from doc/OnlineDocs/reference_guide/library_reference/solvers/xpress_persistent.rst rename to doc/OnlineDocs/reference/library_reference/solvers/xpress_persistent.rst From 0a878e9a49e87ac6363db034dcc92197501dcced Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 10:53:21 -0600 Subject: [PATCH 2403/3044] Reorganize getting-started / how-to - rework main index generation (leverage toctree) - break apart working_models into sections - fix broken links --- doc/OnlineDocs/howto/debugging.rst | 4 + doc/OnlineDocs/howto/index.rst | 11 +- doc/OnlineDocs/howto/interrogating.rst | 166 ++++++++++ .../{working_models.rst => manipulating.rst} | 311 +----------------- doc/OnlineDocs/howto/solver_recipes.rst | 145 ++++++++ doc/OnlineDocs/howto/solver_recipies.rst | 145 ++++++++ doc/OnlineDocs/index.rst | 35 +- 7 files changed, 491 insertions(+), 326 deletions(-) create mode 100644 doc/OnlineDocs/howto/debugging.rst create mode 100644 doc/OnlineDocs/howto/interrogating.rst rename doc/OnlineDocs/howto/{working_models.rst => manipulating.rst} (56%) create mode 100644 doc/OnlineDocs/howto/solver_recipes.rst create mode 100644 doc/OnlineDocs/howto/solver_recipies.rst diff --git a/doc/OnlineDocs/howto/debugging.rst b/doc/OnlineDocs/howto/debugging.rst new file mode 100644 index 00000000000..f876dd39459 --- /dev/null +++ b/doc/OnlineDocs/howto/debugging.rst @@ -0,0 +1,4 @@ +Debugging Models +================ + +TODO diff --git a/doc/OnlineDocs/howto/index.rst b/doc/OnlineDocs/howto/index.rst index e248c400bce..e63a094b66d 100644 --- a/doc/OnlineDocs/howto/index.rst +++ b/doc/OnlineDocs/howto/index.rst @@ -1,2 +1,11 @@ -Hot-To Guides +How-To Guides ============= + +.. toctree:: + :maxdepth: 2 + + interrogating + manipulating + solver_recipes + debugging + contribution_guide diff --git a/doc/OnlineDocs/howto/interrogating.rst b/doc/OnlineDocs/howto/interrogating.rst new file mode 100644 index 00000000000..909b1bf9490 --- /dev/null +++ b/doc/OnlineDocs/howto/interrogating.rst @@ -0,0 +1,166 @@ +Interrogating Models +==================== + +.. _VarAccess: + +Accessing Variable Values +------------------------- + +Primal Variable Values +^^^^^^^^^^^^^^^^^^^^^^ + +Often, the point of optimization is to get optimal values of +variables. Some users may want to process the values in a script. We +will describe how to access a particular variable from a Python script +as well as how to access all variables from a Python script and from a +callback. This should enable the reader to understand how to get the +access that they desire. The Iterative example given above also +illustrates access to variable values. + +One Variable from a Python Script +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Assuming the model has been instantiated and solved and the results have +been loaded back into the instance object, then we can make use of the +fact that the variable is a member of the instance object and its value +can be accessed using its ``value`` member. For example, suppose the +model contains a variable named ``quant`` that is a singleton (has no +indexes) and suppose further that the name of the instance object is +``instance``. Then the value of this variable can be accessed using +``pyo.value(instance.quant)``. Variables with indexes can be referenced +by supplying the index. + +Consider the following very simple example, which is similar to the +iterative example. This is a concrete model. In this example, the value +of ``x[2]`` is accessed. + +.. literalinclude:: /src/scripting/noiteration1.py + :language: python + +.. note:: + + If this script is run without modification, Pyomo is likely to issue + a warning because there are no constraints. The warning is because + some solvers may fail if given a problem instance that does not have + any constraints. + +All Variables from a Python Script +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As with one variable, we assume that the model has been instantiated +and solved. Assuming the instance object has the name ``instance``, +the following code snippet displays all variables and their values: + + >>> for v in instance.component_objects(pyo.Var, active=True): + ... print("Variable",v) # doctest: +SKIP + ... for index in v: + ... print (" ",index, pyo.value(v[index])) # doctest: +SKIP + + +Alternatively, + + >>> for v in instance.component_data_objects(pyo.Var, active=True): + ... print(v, pyo.value(v)) # doctest: +SKIP + +This code could be improved by checking to see if the variable is not +indexed (i.e., the only index value is ``None``), then the code could +print the value without the word ``None`` next to it. + +Assuming again that the model has been instantiated and solved and the +results have been loaded back into the instance object. Here is a code +snippet for fixing all integers at their current value: + + >>> for var in instance.component_data_objects(pyo.Var, active=True): + ... if not var.is_continuous(): + ... print ("fixing "+str(v)) # doctest: +SKIP + ... var.fixed = True # fix the current value + + +Another way to access all of the variables (particularly if there are +blocks) is as follows (this particular snippet assumes that instead of +`import pyomo.environ as pyo` `from pyo.environ import *` was used): + +.. literalinclude:: /src/scripting/block_iter_example_compprintloop.spy + :language: python + +.. _ParamAccess: + +Accessing Parameter Values +-------------------------- + +Accessing parameter values is completely analogous to accessing variable +values. For example, here is a code snippet to print the name and value +of every Parameter in a model: + + >>> for parmobject in instance.component_objects(pyo.Param, active=True): + ... nametoprint = str(str(parmobject.name)) + ... print ("Parameter ", nametoprint) # doctest: +SKIP + ... for index in parmobject: + ... vtoprint = pyo.value(parmobject[index]) + ... print (" ",index, vtoprint) # doctest: +SKIP + + +Accessing Duals +--------------- + +Access to dual values in scripts is similar to accessing primal variable +values, except that dual values are not captured by default so +additional directives are needed before optimization to signal that +duals are desired. + +To get duals without a script, use the ``pyomo`` option +``--solver-suffixes='dual'`` which will cause dual values to be included +in output. Note: In addition to duals (``dual``) , reduced costs +(``rc``) and slack values (``slack``) can be requested. All suffixes can +be requested using the ``pyomo`` option ``--solver-suffixes='.*'`` + +.. warning:: + + Some of the duals may have the value ``None``, rather than ``0``. + +Access Duals in a Python Script +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To signal that duals are desired, declare a Suffix component with the +name "dual" on the model or instance with an IMPORT or IMPORT_EXPORT +direction. + +.. literalinclude:: /src/scripting/driveabs2_Create_dual_suffix_component.spy + :language: python + +See the section on Suffixes :ref:`Suffixes` for more information on +Pyomo's Suffix component. After the results are obtained and loaded into +an instance, duals can be accessed in the following fashion. + +.. literalinclude:: /src/scripting/driveabs2_Access_all_dual.spy + :language: python + +The following snippet will only work, of course, if there is a +constraint with the name ``AxbConstraint`` that has and index, which is +the string ``Film``. + +.. literalinclude:: /src/scripting/driveabs2_Access_one_dual.spy + :language: python + +Here is a complete example that relies on the file ``abstract2.py`` to +provide the model and the file ``abstract2.dat`` to provide the +data. Note that the model in ``abstract2.py`` does contain a constraint +named ``AxbConstraint`` and ``abstract2.dat`` does specify an index for +it named ``Film``. + +.. literalinclude:: /src/scripting/driveabs2.spy + :language: python + +Concrete models are slightly different because the model is the +instance. Here is a complete example that relies on the file +``concrete1.py`` to provide the model and instantiate it. + +.. literalinclude:: /src/scripting/driveconc1.py + :language: python + +Accessing Slacks +---------------- + +The functions ``lslack()`` and ``uslack()`` return the upper and lower +slacks, respectively, for a constraint. + diff --git a/doc/OnlineDocs/howto/working_models.rst b/doc/OnlineDocs/howto/manipulating.rst similarity index 56% rename from doc/OnlineDocs/howto/working_models.rst rename to doc/OnlineDocs/howto/manipulating.rst index 6c7327ce8d7..076acdab712 100644 --- a/doc/OnlineDocs/howto/working_models.rst +++ b/doc/OnlineDocs/howto/manipulating.rst @@ -1,4 +1,4 @@ -Working with Pyomo Models +Manipulating Pyomo Models ========================= This section gives an overview of commonly used scripting commands when @@ -393,312 +393,3 @@ individual index: >>> model.con = pyo.Constraint(model.s, rule=_con) >>> model.con.deactivate() # Deactivate all indices >>> model.con[1].activate() # Activate single index - - - - -.. _VarAccess: - -Accessing Variable Values -------------------------- - -Primal Variable Values -^^^^^^^^^^^^^^^^^^^^^^ - -Often, the point of optimization is to get optimal values of -variables. Some users may want to process the values in a script. We -will describe how to access a particular variable from a Python script -as well as how to access all variables from a Python script and from a -callback. This should enable the reader to understand how to get the -access that they desire. The Iterative example given above also -illustrates access to variable values. - -One Variable from a Python Script -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Assuming the model has been instantiated and solved and the results have -been loaded back into the instance object, then we can make use of the -fact that the variable is a member of the instance object and its value -can be accessed using its ``value`` member. For example, suppose the -model contains a variable named ``quant`` that is a singleton (has no -indexes) and suppose further that the name of the instance object is -``instance``. Then the value of this variable can be accessed using -``pyo.value(instance.quant)``. Variables with indexes can be referenced -by supplying the index. - -Consider the following very simple example, which is similar to the -iterative example. This is a concrete model. In this example, the value -of ``x[2]`` is accessed. - -.. literalinclude:: /src/scripting/noiteration1.py - :language: python - -.. note:: - - If this script is run without modification, Pyomo is likely to issue - a warning because there are no constraints. The warning is because - some solvers may fail if given a problem instance that does not have - any constraints. - -All Variables from a Python Script -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -As with one variable, we assume that the model has been instantiated -and solved. Assuming the instance object has the name ``instance``, -the following code snippet displays all variables and their values: - - >>> for v in instance.component_objects(pyo.Var, active=True): - ... print("Variable",v) # doctest: +SKIP - ... for index in v: - ... print (" ",index, pyo.value(v[index])) # doctest: +SKIP - - -Alternatively, - - >>> for v in instance.component_data_objects(pyo.Var, active=True): - ... print(v, pyo.value(v)) # doctest: +SKIP - -This code could be improved by checking to see if the variable is not -indexed (i.e., the only index value is ``None``), then the code could -print the value without the word ``None`` next to it. - -Assuming again that the model has been instantiated and solved and the -results have been loaded back into the instance object. Here is a code -snippet for fixing all integers at their current value: - - >>> for var in instance.component_data_objects(pyo.Var, active=True): - ... if not var.is_continuous(): - ... print ("fixing "+str(v)) # doctest: +SKIP - ... var.fixed = True # fix the current value - - -Another way to access all of the variables (particularly if there are -blocks) is as follows (this particular snippet assumes that instead of -`import pyomo.environ as pyo` `from pyo.environ import *` was used): - -.. literalinclude:: /src/scripting/block_iter_example_compprintloop.spy - :language: python - -.. _ParamAccess: - -Accessing Parameter Values --------------------------- - -Accessing parameter values is completely analogous to accessing variable -values. For example, here is a code snippet to print the name and value -of every Parameter in a model: - - >>> for parmobject in instance.component_objects(pyo.Param, active=True): - ... nametoprint = str(str(parmobject.name)) - ... print ("Parameter ", nametoprint) # doctest: +SKIP - ... for index in parmobject: - ... vtoprint = pyo.value(parmobject[index]) - ... print (" ",index, vtoprint) # doctest: +SKIP - - -Accessing Duals ---------------- - -Access to dual values in scripts is similar to accessing primal variable -values, except that dual values are not captured by default so -additional directives are needed before optimization to signal that -duals are desired. - -To get duals without a script, use the ``pyomo`` option -``--solver-suffixes='dual'`` which will cause dual values to be included -in output. Note: In addition to duals (``dual``) , reduced costs -(``rc``) and slack values (``slack``) can be requested. All suffixes can -be requested using the ``pyomo`` option ``--solver-suffixes='.*'`` - -.. warning:: - - Some of the duals may have the value ``None``, rather than ``0``. - -Access Duals in a Python Script -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To signal that duals are desired, declare a Suffix component with the -name "dual" on the model or instance with an IMPORT or IMPORT_EXPORT -direction. - -.. literalinclude:: /src/scripting/driveabs2_Create_dual_suffix_component.spy - :language: python - -See the section on Suffixes :ref:`Suffixes` for more information on -Pyomo's Suffix component. After the results are obtained and loaded into -an instance, duals can be accessed in the following fashion. - -.. literalinclude:: /src/scripting/driveabs2_Access_all_dual.spy - :language: python - -The following snippet will only work, of course, if there is a -constraint with the name ``AxbConstraint`` that has and index, which is -the string ``Film``. - -.. literalinclude:: /src/scripting/driveabs2_Access_one_dual.spy - :language: python - -Here is a complete example that relies on the file ``abstract2.py`` to -provide the model and the file ``abstract2.dat`` to provide the -data. Note that the model in ``abstract2.py`` does contain a constraint -named ``AxbConstraint`` and ``abstract2.dat`` does specify an index for -it named ``Film``. - -.. literalinclude:: /src/scripting/driveabs2.spy - :language: python - -Concrete models are slightly different because the model is the -instance. Here is a complete example that relies on the file -``concrete1.py`` to provide the model and instantiate it. - -.. literalinclude:: /src/scripting/driveconc1.py - :language: python - -Accessing Slacks ----------------- - -The functions ``lslack()`` and ``uslack()`` return the upper and lower -slacks, respectively, for a constraint. - - -Accessing Solver Status ------------------------ - -After a solve, the results object has a member ``Solution.Status`` that -contains the solver status. The following snippet shows an example of -access via a ``print`` statement: - -.. literalinclude:: /src/scripting/spy4scripts_Print_solver_status.spy - :language: python - -The use of the Python ``str`` function to cast the value to a be string -makes it easy to test it. In particular, the value 'optimal' indicates -that the solver succeeded. It is also possible to access Pyomo data that -can be compared with the solver status as in the following code snippet: - -.. literalinclude:: /src/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_1.spy - :language: python - -Alternatively, - -.. literalinclude:: /src/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_2.spy - :language: python - -.. _TeeTrue: - -Display of Solver Output ------------------------- - - -To see the output of the solver, use the option ``tee=True`` as in - -.. literalinclude:: /src/scripting/spy4scripts_See_solver_output.spy - :language: python - -This can be useful for troubleshooting solver difficulties. - -.. _SolverOpts: - -Sending Options to the Solver ------------------------------ - -Most solvers accept options and Pyomo can pass options through to a -solver. In scripts or callbacks, the options can be attached to the -solver object by adding to its options dictionary as illustrated by this -snippet: - -.. literalinclude:: /src/scripting/spy4scripts_Add_option_to_solver.spy - :language: python - -If multiple options are needed, then multiple dictionary entries should -be added. - -Sometimes it is desirable to pass options as part of the call to the -solve function as in this snippet: - -.. literalinclude:: /src/scripting/spy4scripts_Add_multiple_options_to_solver.spy - :language: python - -The quoted string is passed directly to the solver. If multiple options -need to be passed to the solver in this way, they should be separated by -a space within the quoted string. Notice that ``tee`` is a Pyomo option -and is solver-independent, while the string argument to ``options`` is -passed to the solver without very little processing by Pyomo. If the -solver does not have a "threads" option, it will probably complain, but -Pyomo will not. - -There are no default values for options on a ``SolverFactory`` -object. If you directly modify its options dictionary, as was done -above, those options will persist across every call to -``optimizer.solve(…)`` unless you delete them from the options -dictionary. You can also pass a dictionary of options into the -``opt.solve(…)`` method using the ``options`` keyword. Those options -will only persist within that solve and temporarily override any -matching options in the options dictionary on the solver object. - -Specifying the Path to a Solver -------------------------------- - -Often, the executables for solvers are in the path; however, for -situations where they are not, the SolverFactory function accepts the -keyword ``executable``, which you can use to set an absolute or relative -path to a solver executable. E.g., - -.. literalinclude:: /src/scripting/spy4scripts_Set_path_to_solver_executable.spy - :language: python - -Warm Starts ------------ - -Some solvers support a warm start based on current values of -variables. To use this feature, set the values of variables in the -instance and pass ``warmstart=True`` to the ``solve()`` method. E.g., - -.. literalinclude:: /src/scripting/spy4scripts_Pass_warmstart_to_solver.spy - :language: python - -.. note:: - - The Cplex and Gurobi LP file (and Python) interfaces will generate an - MST file with the variable data and hand this off to the solver in - addition to the LP file. - -.. warning:: - - Solvers using the NL file interface (e.g., "gurobi_ampl", "cplexamp") - do not accept warmstart as a keyword to the solve() method as the NL - file format, by default, includes variable initialization data (drawn - from the current value of all variables). - - -Solving Multiple Instances in Parallel --------------------------------------- - -Building and solving Pyomo models in parallel is a common requirement -for many applications. We recommend using MPI for Python (mpi4py) for -this purpose. For more information on mpi4py, see the mpi4py -documentation (https://mpi4py.readthedocs.io/en/stable/). The example -below demonstrates how to use mpi4py to solve two pyomo models in -parallel. The example can be run with the following command: - -.. code-block:: - - mpirun -np 2 python -m mpi4py parallel.py - - -.. literalinclude:: /src/scripting/parallel.py - :language: python - - -Changing the temporary directory --------------------------------- - -A "temporary" directory is used for many intermediate files. Normally, -the name of the directory for temporary files is provided by the -operating system, but the user can specify their own directory name. -The pyomo command-line ``--tempdir`` option propagates through to the -TempFileManager service. One can accomplish the same through the -following few lines of code in a script: - -.. literalinclude:: /src/scripting/spy4scripts_Specify_temporary_directory_name.spy - :language: python diff --git a/doc/OnlineDocs/howto/solver_recipes.rst b/doc/OnlineDocs/howto/solver_recipes.rst new file mode 100644 index 00000000000..6d7240184f0 --- /dev/null +++ b/doc/OnlineDocs/howto/solver_recipes.rst @@ -0,0 +1,145 @@ +Solver Recipes +============== + + +Accessing Solver Status +----------------------- + +After a solve, the results object has a member ``Solution.Status`` that +contains the solver status. The following snippet shows an example of +access via a ``print`` statement: + +.. literalinclude:: /src/scripting/spy4scripts_Print_solver_status.spy + :language: python + +The use of the Python ``str`` function to cast the value to a be string +makes it easy to test it. In particular, the value 'optimal' indicates +that the solver succeeded. It is also possible to access Pyomo data that +can be compared with the solver status as in the following code snippet: + +.. literalinclude:: /src/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_1.spy + :language: python + +Alternatively, + +.. literalinclude:: /src/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_2.spy + :language: python + +.. _TeeTrue: + +Display of Solver Output +------------------------ + + +To see the output of the solver, use the option ``tee=True`` as in + +.. literalinclude:: /src/scripting/spy4scripts_See_solver_output.spy + :language: python + +This can be useful for troubleshooting solver difficulties. + +.. _SolverOpts: + +Sending Options to the Solver +----------------------------- + +Most solvers accept options and Pyomo can pass options through to a +solver. In scripts or callbacks, the options can be attached to the +solver object by adding to its options dictionary as illustrated by this +snippet: + +.. literalinclude:: /src/scripting/spy4scripts_Add_option_to_solver.spy + :language: python + +If multiple options are needed, then multiple dictionary entries should +be added. + +Sometimes it is desirable to pass options as part of the call to the +solve function as in this snippet: + +.. literalinclude:: /src/scripting/spy4scripts_Add_multiple_options_to_solver.spy + :language: python + +The quoted string is passed directly to the solver. If multiple options +need to be passed to the solver in this way, they should be separated by +a space within the quoted string. Notice that ``tee`` is a Pyomo option +and is solver-independent, while the string argument to ``options`` is +passed to the solver without very little processing by Pyomo. If the +solver does not have a "threads" option, it will probably complain, but +Pyomo will not. + +There are no default values for options on a ``SolverFactory`` +object. If you directly modify its options dictionary, as was done +above, those options will persist across every call to +``optimizer.solve(…)`` unless you delete them from the options +dictionary. You can also pass a dictionary of options into the +``opt.solve(…)`` method using the ``options`` keyword. Those options +will only persist within that solve and temporarily override any +matching options in the options dictionary on the solver object. + +Specifying the Path to a Solver +------------------------------- + +Often, the executables for solvers are in the path; however, for +situations where they are not, the SolverFactory function accepts the +keyword ``executable``, which you can use to set an absolute or relative +path to a solver executable. E.g., + +.. literalinclude:: /src/scripting/spy4scripts_Set_path_to_solver_executable.spy + :language: python + +Warm Starts +----------- + +Some solvers support a warm start based on current values of +variables. To use this feature, set the values of variables in the +instance and pass ``warmstart=True`` to the ``solve()`` method. E.g., + +.. literalinclude:: /src/scripting/spy4scripts_Pass_warmstart_to_solver.spy + :language: python + +.. note:: + + The Cplex and Gurobi LP file (and Python) interfaces will generate an + MST file with the variable data and hand this off to the solver in + addition to the LP file. + +.. warning:: + + Solvers using the NL file interface (e.g., "gurobi_ampl", "cplexamp") + do not accept warmstart as a keyword to the solve() method as the NL + file format, by default, includes variable initialization data (drawn + from the current value of all variables). + + +Solving Multiple Instances in Parallel +-------------------------------------- + +Building and solving Pyomo models in parallel is a common requirement +for many applications. We recommend using MPI for Python (mpi4py) for +this purpose. For more information on mpi4py, see the mpi4py +documentation (https://mpi4py.readthedocs.io/en/stable/). The example +below demonstrates how to use mpi4py to solve two pyomo models in +parallel. The example can be run with the following command: + +.. code-block:: + + mpirun -np 2 python -m mpi4py parallel.py + + +.. literalinclude:: /src/scripting/parallel.py + :language: python + + +Changing the temporary directory +-------------------------------- + +A "temporary" directory is used for many intermediate files. Normally, +the name of the directory for temporary files is provided by the +operating system, but the user can specify their own directory name. +The pyomo command-line ``--tempdir`` option propagates through to the +TempFileManager service. One can accomplish the same through the +following few lines of code in a script: + +.. literalinclude:: /src/scripting/spy4scripts_Specify_temporary_directory_name.spy +:language: python diff --git a/doc/OnlineDocs/howto/solver_recipies.rst b/doc/OnlineDocs/howto/solver_recipies.rst new file mode 100644 index 00000000000..c9be02405e2 --- /dev/null +++ b/doc/OnlineDocs/howto/solver_recipies.rst @@ -0,0 +1,145 @@ +Solver Recipes +============== + + +Accessing Solver Status +----------------------- + +After a solve, the results object has a member ``Solution.Status`` that +contains the solver status. The following snippet shows an example of +access via a ``print`` statement: + +.. literalinclude:: /src/scripting/spy4scripts_Print_solver_status.spy + :language: python + +The use of the Python ``str`` function to cast the value to a be string +makes it easy to test it. In particular, the value 'optimal' indicates +that the solver succeeded. It is also possible to access Pyomo data that +can be compared with the solver status as in the following code snippet: + +.. literalinclude:: /src/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_1.spy + :language: python + +Alternatively, + +.. literalinclude:: /src/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_2.spy + :language: python + +.. _TeeTrue: + +Display of Solver Output +------------------------ + + +To see the output of the solver, use the option ``tee=True`` as in + +.. literalinclude:: /src/scripting/spy4scripts_See_solver_output.spy + :language: python + +This can be useful for troubleshooting solver difficulties. + +.. _SolverOpts: + +Sending Options to the Solver +----------------------------- + +Most solvers accept options and Pyomo can pass options through to a +solver. In scripts or callbacks, the options can be attached to the +solver object by adding to its options dictionary as illustrated by this +snippet: + +.. literalinclude:: /src/scripting/spy4scripts_Add_option_to_solver.spy + :language: python + +If multiple options are needed, then multiple dictionary entries should +be added. + +Sometimes it is desirable to pass options as part of the call to the +solve function as in this snippet: + +.. literalinclude:: /src/scripting/spy4scripts_Add_multiple_options_to_solver.spy + :language: python + +The quoted string is passed directly to the solver. If multiple options +need to be passed to the solver in this way, they should be separated by +a space within the quoted string. Notice that ``tee`` is a Pyomo option +and is solver-independent, while the string argument to ``options`` is +passed to the solver without very little processing by Pyomo. If the +solver does not have a "threads" option, it will probably complain, but +Pyomo will not. + +There are no default values for options on a ``SolverFactory`` +object. If you directly modify its options dictionary, as was done +above, those options will persist across every call to +``optimizer.solve(…)`` unless you delete them from the options +dictionary. You can also pass a dictionary of options into the +``opt.solve(…)`` method using the ``options`` keyword. Those options +will only persist within that solve and temporarily override any +matching options in the options dictionary on the solver object. + +Specifying the Path to a Solver +------------------------------- + +Often, the executables for solvers are in the path; however, for +situations where they are not, the SolverFactory function accepts the +keyword ``executable``, which you can use to set an absolute or relative +path to a solver executable. E.g., + +.. literalinclude:: /src/scripting/spy4scripts_Set_path_to_solver_executable.spy + :language: python + +Warm Starts +----------- + +Some solvers support a warm start based on current values of +variables. To use this feature, set the values of variables in the +instance and pass ``warmstart=True`` to the ``solve()`` method. E.g., + +.. literalinclude:: /src/scripting/spy4scripts_Pass_warmstart_to_solver.spy + :language: python + +.. note:: + + The Cplex and Gurobi LP file (and Python) interfaces will generate an + MST file with the variable data and hand this off to the solver in + addition to the LP file. + +.. warning:: + + Solvers using the NL file interface (e.g., "gurobi_ampl", "cplexamp") + do not accept warmstart as a keyword to the solve() method as the NL + file format, by default, includes variable initialization data (drawn + from the current value of all variables). + + +Solving Multiple Instances in Parallel +-------------------------------------- + +Building and solving Pyomo models in parallel is a common requirement +for many applications. We recommend using MPI for Python (mpi4py) for +this purpose. For more information on mpi4py, see the mpi4py +documentation (https://mpi4py.readthedocs.io/en/stable/). The example +below demonstrates how to use mpi4py to solve two pyomo models in +parallel. The example can be run with the following command: + +.. code-block:: + + mpirun -np 2 python -m mpi4py parallel.py + + +.. literalinclude:: /src/scripting/parallel.py + :language: python + + +Changing the temporary directory +-------------------------------- + +A "temporary" directory is used for many intermediate files. Normally, +the name of the directory for temporary files is provided by the +operating system, but the user can specify their own directory name. +The pyomo command-line ``--tempdir`` option propagates through to the +TempFileManager service. One can accomplish the same through the +following few lines of code in a script: + +.. literalinclude:: /src/scripting/spy4scripts_Specify_temporary_directory_name.spy + :language: python diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index fb5f64cbf6f..eb30282b908 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -13,15 +13,17 @@ with a diverse set of optimization capabilities. :width: 100% :class: index-table - * - Getting Started - | :doc:`Installation ` - | :doc:`Pyomo Overview ` - - How-To Guide - | :doc:`Interrogating Models` - | :doc:`Manipulating Models` - | :doc:`Solver Recipes` - | :doc:`Debugging Models` - | :doc:`Contributing to Pyomo` + * - .. toctree:: + :maxdepth: 2 + :titlesonly: + + getting_started/index + - .. toctree:: + :maxdepth: 2 + :titlesonly: + :includehidden: + + howto/index * - User Explanations | :doc:`Pyomo Philosophy` | :doc:`Concrete and Abstract Models` @@ -59,17 +61,15 @@ with a diverse set of optimization capabilities. | :doc:`Deprecation System` | :doc:`Experimental` | :doc:`Kernel` - - Reference Guide + - Reference Guides | :doc:`Library Reference ` | :doc:`Common Warnings and Errors` - | :doc:`Preview capabilities through ``pyomo.__future__`` ` + | :doc:`Accessing preview capabilities ` .. toctree:: :hidden: :maxdepth: 2 - Getting Started - How-To Guides User Explanations Reference Guides @@ -123,7 +123,12 @@ list of Pyomo-related packages may be found :doc:`here `. Citing Pyomo ------------ -Bynum, Michael L., Gabriel A. Hackebeil, William E. Hart, Carl D. Laird, Bethany L. Nicholson, John D. Siirola, Jean-Paul Watson, and David L. Woodruff. Pyomo - Optimization Modeling in Python, 3rd Edition. Springer, 2021. +If you use Pyomo in your work, please cite: -Hart, William E., Jean-Paul Watson, and David L. Woodruff. "Pyomo: modeling and solving mathematical programs in Python." Mathematical Programming Computation 3, no. 3 (2011): 219-260. + Bynum, Michael L., Gabriel A. Hackebeil, William E. Hart, Carl D. Laird, + Bethany L. Nicholson, John D. Siirola, Jean-Paul Watson, and + David L. Woodruff. Pyomo - Optimization Modeling in Python, 3rd + Edition. Springer, 2021. +Additionally, several Pyomo capabilities and subpackages are described +in further detail in separate :ref:`publications`. From 679a625930624e1b64cabcd78c5eeec0578bc43f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 11:05:30 -0600 Subject: [PATCH 2404/3044] Renaming some directories --- .../{developer_utilities => developer_utils}/config.rst | 0 .../{developer_utilities => developer_utils}/deprecation.rst | 0 .../{modeling_utilities => modeling_utils}/flattener/index.rst | 0 .../flattener/motivation.rst | 0 .../flattener/reference.rst | 0 .../{modeling_utilities => modeling_utils}/latex_printer.rst | 0 .../{modeling_utilities => modeling_utils}/preprocessing.rst | 0 .../{modeling_utilities => modeling_utils}/scaling.rst | 0 .../{pyomo_philosophy => philosophy}/expressions/design.rst | 0 .../{pyomo_philosophy => philosophy}/expressions/index.rst | 0 .../{pyomo_philosophy => philosophy}/expressions/managing.rst | 0 .../{pyomo_philosophy => philosophy}/expressions/overview.rst | 0 .../{pyomo_philosophy => philosophy}/expressions/performance.rst | 0 13 files changed, 0 insertions(+), 0 deletions(-) rename doc/OnlineDocs/explanation/{developer_utilities => developer_utils}/config.rst (100%) rename doc/OnlineDocs/explanation/{developer_utilities => developer_utils}/deprecation.rst (100%) rename doc/OnlineDocs/explanation/{modeling_utilities => modeling_utils}/flattener/index.rst (100%) rename doc/OnlineDocs/explanation/{modeling_utilities => modeling_utils}/flattener/motivation.rst (100%) rename doc/OnlineDocs/explanation/{modeling_utilities => modeling_utils}/flattener/reference.rst (100%) rename doc/OnlineDocs/explanation/{modeling_utilities => modeling_utils}/latex_printer.rst (100%) rename doc/OnlineDocs/explanation/{modeling_utilities => modeling_utils}/preprocessing.rst (100%) rename doc/OnlineDocs/explanation/{modeling_utilities => modeling_utils}/scaling.rst (100%) rename doc/OnlineDocs/explanation/{pyomo_philosophy => philosophy}/expressions/design.rst (100%) rename doc/OnlineDocs/explanation/{pyomo_philosophy => philosophy}/expressions/index.rst (100%) rename doc/OnlineDocs/explanation/{pyomo_philosophy => philosophy}/expressions/managing.rst (100%) rename doc/OnlineDocs/explanation/{pyomo_philosophy => philosophy}/expressions/overview.rst (100%) rename doc/OnlineDocs/explanation/{pyomo_philosophy => philosophy}/expressions/performance.rst (100%) diff --git a/doc/OnlineDocs/explanation/developer_utilities/config.rst b/doc/OnlineDocs/explanation/developer_utils/config.rst similarity index 100% rename from doc/OnlineDocs/explanation/developer_utilities/config.rst rename to doc/OnlineDocs/explanation/developer_utils/config.rst diff --git a/doc/OnlineDocs/explanation/developer_utilities/deprecation.rst b/doc/OnlineDocs/explanation/developer_utils/deprecation.rst similarity index 100% rename from doc/OnlineDocs/explanation/developer_utilities/deprecation.rst rename to doc/OnlineDocs/explanation/developer_utils/deprecation.rst diff --git a/doc/OnlineDocs/explanation/modeling_utilities/flattener/index.rst b/doc/OnlineDocs/explanation/modeling_utils/flattener/index.rst similarity index 100% rename from doc/OnlineDocs/explanation/modeling_utilities/flattener/index.rst rename to doc/OnlineDocs/explanation/modeling_utils/flattener/index.rst diff --git a/doc/OnlineDocs/explanation/modeling_utilities/flattener/motivation.rst b/doc/OnlineDocs/explanation/modeling_utils/flattener/motivation.rst similarity index 100% rename from doc/OnlineDocs/explanation/modeling_utilities/flattener/motivation.rst rename to doc/OnlineDocs/explanation/modeling_utils/flattener/motivation.rst diff --git a/doc/OnlineDocs/explanation/modeling_utilities/flattener/reference.rst b/doc/OnlineDocs/explanation/modeling_utils/flattener/reference.rst similarity index 100% rename from doc/OnlineDocs/explanation/modeling_utilities/flattener/reference.rst rename to doc/OnlineDocs/explanation/modeling_utils/flattener/reference.rst diff --git a/doc/OnlineDocs/explanation/modeling_utilities/latex_printer.rst b/doc/OnlineDocs/explanation/modeling_utils/latex_printer.rst similarity index 100% rename from doc/OnlineDocs/explanation/modeling_utilities/latex_printer.rst rename to doc/OnlineDocs/explanation/modeling_utils/latex_printer.rst diff --git a/doc/OnlineDocs/explanation/modeling_utilities/preprocessing.rst b/doc/OnlineDocs/explanation/modeling_utils/preprocessing.rst similarity index 100% rename from doc/OnlineDocs/explanation/modeling_utilities/preprocessing.rst rename to doc/OnlineDocs/explanation/modeling_utils/preprocessing.rst diff --git a/doc/OnlineDocs/explanation/modeling_utilities/scaling.rst b/doc/OnlineDocs/explanation/modeling_utils/scaling.rst similarity index 100% rename from doc/OnlineDocs/explanation/modeling_utilities/scaling.rst rename to doc/OnlineDocs/explanation/modeling_utils/scaling.rst diff --git a/doc/OnlineDocs/explanation/pyomo_philosophy/expressions/design.rst b/doc/OnlineDocs/explanation/philosophy/expressions/design.rst similarity index 100% rename from doc/OnlineDocs/explanation/pyomo_philosophy/expressions/design.rst rename to doc/OnlineDocs/explanation/philosophy/expressions/design.rst diff --git a/doc/OnlineDocs/explanation/pyomo_philosophy/expressions/index.rst b/doc/OnlineDocs/explanation/philosophy/expressions/index.rst similarity index 100% rename from doc/OnlineDocs/explanation/pyomo_philosophy/expressions/index.rst rename to doc/OnlineDocs/explanation/philosophy/expressions/index.rst diff --git a/doc/OnlineDocs/explanation/pyomo_philosophy/expressions/managing.rst b/doc/OnlineDocs/explanation/philosophy/expressions/managing.rst similarity index 100% rename from doc/OnlineDocs/explanation/pyomo_philosophy/expressions/managing.rst rename to doc/OnlineDocs/explanation/philosophy/expressions/managing.rst diff --git a/doc/OnlineDocs/explanation/pyomo_philosophy/expressions/overview.rst b/doc/OnlineDocs/explanation/philosophy/expressions/overview.rst similarity index 100% rename from doc/OnlineDocs/explanation/pyomo_philosophy/expressions/overview.rst rename to doc/OnlineDocs/explanation/philosophy/expressions/overview.rst diff --git a/doc/OnlineDocs/explanation/pyomo_philosophy/expressions/performance.rst b/doc/OnlineDocs/explanation/philosophy/expressions/performance.rst similarity index 100% rename from doc/OnlineDocs/explanation/pyomo_philosophy/expressions/performance.rst rename to doc/OnlineDocs/explanation/philosophy/expressions/performance.rst From 63cf630c63f85748fd26d45791ea1ba8eba1efe0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 13:40:13 -0600 Subject: [PATCH 2405/3044] Moving some files around --- .../{contrib_index.rst => contrib_index.txt} | 0 .../kernel/examples/aml_example.py | 0 .../experimental}/kernel/examples/conic.py | 0 .../kernel/examples/kernel_containers.py | 0 .../kernel/examples/kernel_example.py | 0 .../kernel/examples/kernel_solving.py | 0 .../kernel/examples/kernel_subclassing.py | 0 .../kernel/examples/transformer.py | 0 .../experimental}/kernel/index.rst | 0 .../kernel/syntax_comparison.rst | 0 .../constraints.rst} | 0 .../expressions.rst} | 0 .../objectives.rst} | 0 .../parameters.rst} | 0 .../{Sets.rst => math_programming/sets.rst} | 0 .../sos_constraints.rst | 0 .../suffixes.rst} | 0 .../variables.rst} | 0 .../{units_container.rst => units.rst} | 0 ...{persistent_solvers.rst => persistent.rst} | 0 doc/OnlineDocs/howto/solver_recipies.rst | 145 ------------------ .../{library_reference => }/kernel/base.rst | 0 .../{library_reference => }/kernel/block.rst | 0 .../{library_reference => }/kernel/conic.rst | 0 .../kernel/constraint.rst | 0 .../kernel/dict_container.rst | 0 .../kernel/expression.rst | 0 .../kernel/heterogeneous_container.rst | 0 .../kernel/homogeneous_container.rst | 0 .../kernel/list_container.rst | 0 .../kernel/objective.rst | 0 .../kernel/parameter.rst | 0 .../kernel/piecewise/index.rst | 0 .../kernel/piecewise/piecewise.rst | 0 .../kernel/piecewise/piecewise_nd.rst | 0 .../kernel/piecewise/util.rst | 0 .../{library_reference => }/kernel/sos.rst | 0 .../{library_reference => }/kernel/suffix.rst | 0 .../kernel/tuple_container.rst | 0 .../kernel/variable.rst | 0 40 files changed, 145 deletions(-) rename doc/OnlineDocs/explanation/{contrib_index.rst => contrib_index.txt} (100%) rename doc/OnlineDocs/{reference/library_reference => explanation/experimental}/kernel/examples/aml_example.py (100%) rename doc/OnlineDocs/{reference/library_reference => explanation/experimental}/kernel/examples/conic.py (100%) rename doc/OnlineDocs/{reference/library_reference => explanation/experimental}/kernel/examples/kernel_containers.py (100%) rename doc/OnlineDocs/{reference/library_reference => explanation/experimental}/kernel/examples/kernel_example.py (100%) rename doc/OnlineDocs/{reference/library_reference => explanation/experimental}/kernel/examples/kernel_solving.py (100%) rename doc/OnlineDocs/{reference/library_reference => explanation/experimental}/kernel/examples/kernel_subclassing.py (100%) rename doc/OnlineDocs/{reference/library_reference => explanation/experimental}/kernel/examples/transformer.py (100%) rename doc/OnlineDocs/{reference/library_reference => explanation/experimental}/kernel/index.rst (100%) rename doc/OnlineDocs/{reference/library_reference => explanation/experimental}/kernel/syntax_comparison.rst (100%) rename doc/OnlineDocs/explanation/modeling/{Constraints.rst => math_programming/constraints.rst} (100%) rename doc/OnlineDocs/explanation/modeling/{Expressions.rst => math_programming/expressions.rst} (100%) rename doc/OnlineDocs/explanation/modeling/{Objectives.rst => math_programming/objectives.rst} (100%) rename doc/OnlineDocs/explanation/modeling/{Parameters.rst => math_programming/parameters.rst} (100%) rename doc/OnlineDocs/explanation/modeling/{Sets.rst => math_programming/sets.rst} (100%) rename doc/OnlineDocs/explanation/modeling/{ => math_programming}/sos_constraints.rst (100%) rename doc/OnlineDocs/explanation/modeling/{Suffixes.rst => math_programming/suffixes.rst} (100%) rename doc/OnlineDocs/explanation/modeling/{Variables.rst => math_programming/variables.rst} (100%) rename doc/OnlineDocs/explanation/modeling/{units_container.rst => units.rst} (100%) rename doc/OnlineDocs/explanation/solvers/{persistent_solvers.rst => persistent.rst} (100%) delete mode 100644 doc/OnlineDocs/howto/solver_recipies.rst rename doc/OnlineDocs/reference/{library_reference => }/kernel/base.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/block.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/conic.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/constraint.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/dict_container.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/expression.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/heterogeneous_container.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/homogeneous_container.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/list_container.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/objective.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/parameter.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/piecewise/index.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/piecewise/piecewise.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/piecewise/piecewise_nd.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/piecewise/util.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/sos.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/suffix.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/tuple_container.rst (100%) rename doc/OnlineDocs/reference/{library_reference => }/kernel/variable.rst (100%) diff --git a/doc/OnlineDocs/explanation/contrib_index.rst b/doc/OnlineDocs/explanation/contrib_index.txt similarity index 100% rename from doc/OnlineDocs/explanation/contrib_index.rst rename to doc/OnlineDocs/explanation/contrib_index.txt diff --git a/doc/OnlineDocs/reference/library_reference/kernel/examples/aml_example.py b/doc/OnlineDocs/explanation/experimental/kernel/examples/aml_example.py similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/examples/aml_example.py rename to doc/OnlineDocs/explanation/experimental/kernel/examples/aml_example.py diff --git a/doc/OnlineDocs/reference/library_reference/kernel/examples/conic.py b/doc/OnlineDocs/explanation/experimental/kernel/examples/conic.py similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/examples/conic.py rename to doc/OnlineDocs/explanation/experimental/kernel/examples/conic.py diff --git a/doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_containers.py b/doc/OnlineDocs/explanation/experimental/kernel/examples/kernel_containers.py similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_containers.py rename to doc/OnlineDocs/explanation/experimental/kernel/examples/kernel_containers.py diff --git a/doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_example.py b/doc/OnlineDocs/explanation/experimental/kernel/examples/kernel_example.py similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_example.py rename to doc/OnlineDocs/explanation/experimental/kernel/examples/kernel_example.py diff --git a/doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_solving.py b/doc/OnlineDocs/explanation/experimental/kernel/examples/kernel_solving.py similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_solving.py rename to doc/OnlineDocs/explanation/experimental/kernel/examples/kernel_solving.py diff --git a/doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_subclassing.py b/doc/OnlineDocs/explanation/experimental/kernel/examples/kernel_subclassing.py similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/examples/kernel_subclassing.py rename to doc/OnlineDocs/explanation/experimental/kernel/examples/kernel_subclassing.py diff --git a/doc/OnlineDocs/reference/library_reference/kernel/examples/transformer.py b/doc/OnlineDocs/explanation/experimental/kernel/examples/transformer.py similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/examples/transformer.py rename to doc/OnlineDocs/explanation/experimental/kernel/examples/transformer.py diff --git a/doc/OnlineDocs/reference/library_reference/kernel/index.rst b/doc/OnlineDocs/explanation/experimental/kernel/index.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/index.rst rename to doc/OnlineDocs/explanation/experimental/kernel/index.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/syntax_comparison.rst b/doc/OnlineDocs/explanation/experimental/kernel/syntax_comparison.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/syntax_comparison.rst rename to doc/OnlineDocs/explanation/experimental/kernel/syntax_comparison.rst diff --git a/doc/OnlineDocs/explanation/modeling/Constraints.rst b/doc/OnlineDocs/explanation/modeling/math_programming/constraints.rst similarity index 100% rename from doc/OnlineDocs/explanation/modeling/Constraints.rst rename to doc/OnlineDocs/explanation/modeling/math_programming/constraints.rst diff --git a/doc/OnlineDocs/explanation/modeling/Expressions.rst b/doc/OnlineDocs/explanation/modeling/math_programming/expressions.rst similarity index 100% rename from doc/OnlineDocs/explanation/modeling/Expressions.rst rename to doc/OnlineDocs/explanation/modeling/math_programming/expressions.rst diff --git a/doc/OnlineDocs/explanation/modeling/Objectives.rst b/doc/OnlineDocs/explanation/modeling/math_programming/objectives.rst similarity index 100% rename from doc/OnlineDocs/explanation/modeling/Objectives.rst rename to doc/OnlineDocs/explanation/modeling/math_programming/objectives.rst diff --git a/doc/OnlineDocs/explanation/modeling/Parameters.rst b/doc/OnlineDocs/explanation/modeling/math_programming/parameters.rst similarity index 100% rename from doc/OnlineDocs/explanation/modeling/Parameters.rst rename to doc/OnlineDocs/explanation/modeling/math_programming/parameters.rst diff --git a/doc/OnlineDocs/explanation/modeling/Sets.rst b/doc/OnlineDocs/explanation/modeling/math_programming/sets.rst similarity index 100% rename from doc/OnlineDocs/explanation/modeling/Sets.rst rename to doc/OnlineDocs/explanation/modeling/math_programming/sets.rst diff --git a/doc/OnlineDocs/explanation/modeling/sos_constraints.rst b/doc/OnlineDocs/explanation/modeling/math_programming/sos_constraints.rst similarity index 100% rename from doc/OnlineDocs/explanation/modeling/sos_constraints.rst rename to doc/OnlineDocs/explanation/modeling/math_programming/sos_constraints.rst diff --git a/doc/OnlineDocs/explanation/modeling/Suffixes.rst b/doc/OnlineDocs/explanation/modeling/math_programming/suffixes.rst similarity index 100% rename from doc/OnlineDocs/explanation/modeling/Suffixes.rst rename to doc/OnlineDocs/explanation/modeling/math_programming/suffixes.rst diff --git a/doc/OnlineDocs/explanation/modeling/Variables.rst b/doc/OnlineDocs/explanation/modeling/math_programming/variables.rst similarity index 100% rename from doc/OnlineDocs/explanation/modeling/Variables.rst rename to doc/OnlineDocs/explanation/modeling/math_programming/variables.rst diff --git a/doc/OnlineDocs/explanation/modeling/units_container.rst b/doc/OnlineDocs/explanation/modeling/units.rst similarity index 100% rename from doc/OnlineDocs/explanation/modeling/units_container.rst rename to doc/OnlineDocs/explanation/modeling/units.rst diff --git a/doc/OnlineDocs/explanation/solvers/persistent_solvers.rst b/doc/OnlineDocs/explanation/solvers/persistent.rst similarity index 100% rename from doc/OnlineDocs/explanation/solvers/persistent_solvers.rst rename to doc/OnlineDocs/explanation/solvers/persistent.rst diff --git a/doc/OnlineDocs/howto/solver_recipies.rst b/doc/OnlineDocs/howto/solver_recipies.rst deleted file mode 100644 index c9be02405e2..00000000000 --- a/doc/OnlineDocs/howto/solver_recipies.rst +++ /dev/null @@ -1,145 +0,0 @@ -Solver Recipes -============== - - -Accessing Solver Status ------------------------ - -After a solve, the results object has a member ``Solution.Status`` that -contains the solver status. The following snippet shows an example of -access via a ``print`` statement: - -.. literalinclude:: /src/scripting/spy4scripts_Print_solver_status.spy - :language: python - -The use of the Python ``str`` function to cast the value to a be string -makes it easy to test it. In particular, the value 'optimal' indicates -that the solver succeeded. It is also possible to access Pyomo data that -can be compared with the solver status as in the following code snippet: - -.. literalinclude:: /src/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_1.spy - :language: python - -Alternatively, - -.. literalinclude:: /src/scripting/spy4scripts_Pyomo_data_comparedwith_solver_status_2.spy - :language: python - -.. _TeeTrue: - -Display of Solver Output ------------------------- - - -To see the output of the solver, use the option ``tee=True`` as in - -.. literalinclude:: /src/scripting/spy4scripts_See_solver_output.spy - :language: python - -This can be useful for troubleshooting solver difficulties. - -.. _SolverOpts: - -Sending Options to the Solver ------------------------------ - -Most solvers accept options and Pyomo can pass options through to a -solver. In scripts or callbacks, the options can be attached to the -solver object by adding to its options dictionary as illustrated by this -snippet: - -.. literalinclude:: /src/scripting/spy4scripts_Add_option_to_solver.spy - :language: python - -If multiple options are needed, then multiple dictionary entries should -be added. - -Sometimes it is desirable to pass options as part of the call to the -solve function as in this snippet: - -.. literalinclude:: /src/scripting/spy4scripts_Add_multiple_options_to_solver.spy - :language: python - -The quoted string is passed directly to the solver. If multiple options -need to be passed to the solver in this way, they should be separated by -a space within the quoted string. Notice that ``tee`` is a Pyomo option -and is solver-independent, while the string argument to ``options`` is -passed to the solver without very little processing by Pyomo. If the -solver does not have a "threads" option, it will probably complain, but -Pyomo will not. - -There are no default values for options on a ``SolverFactory`` -object. If you directly modify its options dictionary, as was done -above, those options will persist across every call to -``optimizer.solve(…)`` unless you delete them from the options -dictionary. You can also pass a dictionary of options into the -``opt.solve(…)`` method using the ``options`` keyword. Those options -will only persist within that solve and temporarily override any -matching options in the options dictionary on the solver object. - -Specifying the Path to a Solver -------------------------------- - -Often, the executables for solvers are in the path; however, for -situations where they are not, the SolverFactory function accepts the -keyword ``executable``, which you can use to set an absolute or relative -path to a solver executable. E.g., - -.. literalinclude:: /src/scripting/spy4scripts_Set_path_to_solver_executable.spy - :language: python - -Warm Starts ------------ - -Some solvers support a warm start based on current values of -variables. To use this feature, set the values of variables in the -instance and pass ``warmstart=True`` to the ``solve()`` method. E.g., - -.. literalinclude:: /src/scripting/spy4scripts_Pass_warmstart_to_solver.spy - :language: python - -.. note:: - - The Cplex and Gurobi LP file (and Python) interfaces will generate an - MST file with the variable data and hand this off to the solver in - addition to the LP file. - -.. warning:: - - Solvers using the NL file interface (e.g., "gurobi_ampl", "cplexamp") - do not accept warmstart as a keyword to the solve() method as the NL - file format, by default, includes variable initialization data (drawn - from the current value of all variables). - - -Solving Multiple Instances in Parallel --------------------------------------- - -Building and solving Pyomo models in parallel is a common requirement -for many applications. We recommend using MPI for Python (mpi4py) for -this purpose. For more information on mpi4py, see the mpi4py -documentation (https://mpi4py.readthedocs.io/en/stable/). The example -below demonstrates how to use mpi4py to solve two pyomo models in -parallel. The example can be run with the following command: - -.. code-block:: - - mpirun -np 2 python -m mpi4py parallel.py - - -.. literalinclude:: /src/scripting/parallel.py - :language: python - - -Changing the temporary directory --------------------------------- - -A "temporary" directory is used for many intermediate files. Normally, -the name of the directory for temporary files is provided by the -operating system, but the user can specify their own directory name. -The pyomo command-line ``--tempdir`` option propagates through to the -TempFileManager service. One can accomplish the same through the -following few lines of code in a script: - -.. literalinclude:: /src/scripting/spy4scripts_Specify_temporary_directory_name.spy - :language: python diff --git a/doc/OnlineDocs/reference/library_reference/kernel/base.rst b/doc/OnlineDocs/reference/kernel/base.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/base.rst rename to doc/OnlineDocs/reference/kernel/base.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/block.rst b/doc/OnlineDocs/reference/kernel/block.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/block.rst rename to doc/OnlineDocs/reference/kernel/block.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/conic.rst b/doc/OnlineDocs/reference/kernel/conic.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/conic.rst rename to doc/OnlineDocs/reference/kernel/conic.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/constraint.rst b/doc/OnlineDocs/reference/kernel/constraint.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/constraint.rst rename to doc/OnlineDocs/reference/kernel/constraint.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/dict_container.rst b/doc/OnlineDocs/reference/kernel/dict_container.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/dict_container.rst rename to doc/OnlineDocs/reference/kernel/dict_container.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/expression.rst b/doc/OnlineDocs/reference/kernel/expression.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/expression.rst rename to doc/OnlineDocs/reference/kernel/expression.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/heterogeneous_container.rst b/doc/OnlineDocs/reference/kernel/heterogeneous_container.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/heterogeneous_container.rst rename to doc/OnlineDocs/reference/kernel/heterogeneous_container.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/homogeneous_container.rst b/doc/OnlineDocs/reference/kernel/homogeneous_container.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/homogeneous_container.rst rename to doc/OnlineDocs/reference/kernel/homogeneous_container.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/list_container.rst b/doc/OnlineDocs/reference/kernel/list_container.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/list_container.rst rename to doc/OnlineDocs/reference/kernel/list_container.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/objective.rst b/doc/OnlineDocs/reference/kernel/objective.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/objective.rst rename to doc/OnlineDocs/reference/kernel/objective.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/parameter.rst b/doc/OnlineDocs/reference/kernel/parameter.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/parameter.rst rename to doc/OnlineDocs/reference/kernel/parameter.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/piecewise/index.rst b/doc/OnlineDocs/reference/kernel/piecewise/index.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/piecewise/index.rst rename to doc/OnlineDocs/reference/kernel/piecewise/index.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise.rst b/doc/OnlineDocs/reference/kernel/piecewise/piecewise.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise.rst rename to doc/OnlineDocs/reference/kernel/piecewise/piecewise.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise_nd.rst b/doc/OnlineDocs/reference/kernel/piecewise/piecewise_nd.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise_nd.rst rename to doc/OnlineDocs/reference/kernel/piecewise/piecewise_nd.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/piecewise/util.rst b/doc/OnlineDocs/reference/kernel/piecewise/util.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/piecewise/util.rst rename to doc/OnlineDocs/reference/kernel/piecewise/util.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/sos.rst b/doc/OnlineDocs/reference/kernel/sos.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/sos.rst rename to doc/OnlineDocs/reference/kernel/sos.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/suffix.rst b/doc/OnlineDocs/reference/kernel/suffix.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/suffix.rst rename to doc/OnlineDocs/reference/kernel/suffix.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/tuple_container.rst b/doc/OnlineDocs/reference/kernel/tuple_container.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/tuple_container.rst rename to doc/OnlineDocs/reference/kernel/tuple_container.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/variable.rst b/doc/OnlineDocs/reference/kernel/variable.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/variable.rst rename to doc/OnlineDocs/reference/kernel/variable.rst From 4453c2900c1893a88d34b38dc384cf50dc73bc8c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 15:53:55 -0600 Subject: [PATCH 2406/3044] Reorganize explanations & reference --- doc/OnlineDocs/explanation/analysis/index.rst | 20 ++++ .../explanation/developer_utils/index.rst | 8 ++ .../explanation/experimental/index.rst | 8 ++ doc/OnlineDocs/explanation/index.rst | 57 ++++++++- doc/OnlineDocs/explanation/modeling/index.rst | 30 +++-- .../modeling/math_programming/index.rst | 14 +++ .../explanation/modeling_utils/index.rst | 22 ++++ .../philosophy/abstract_modeling.rst | 60 ++++++++++ .../philosophy/component_design.rst | 4 + .../explanation/philosophy/index.rst | 21 ++++ .../philosophy/transformations.rst | 4 + doc/OnlineDocs/explanation/solvers/index.rst | 15 +++ doc/OnlineDocs/howto/solver_recipes.rst | 2 +- doc/OnlineDocs/index.rst | 61 ++-------- doc/OnlineDocs/reference/bibliography.rst | 111 ++++++++++-------- doc/OnlineDocs/reference/index.rst | 25 ++-- doc/OnlineDocs/reference/kernel/index.rst | 52 ++++++++ 17 files changed, 388 insertions(+), 126 deletions(-) create mode 100644 doc/OnlineDocs/explanation/analysis/index.rst create mode 100644 doc/OnlineDocs/explanation/developer_utils/index.rst create mode 100644 doc/OnlineDocs/explanation/experimental/index.rst create mode 100644 doc/OnlineDocs/explanation/modeling/math_programming/index.rst create mode 100644 doc/OnlineDocs/explanation/modeling_utils/index.rst create mode 100644 doc/OnlineDocs/explanation/philosophy/abstract_modeling.rst create mode 100644 doc/OnlineDocs/explanation/philosophy/component_design.rst create mode 100644 doc/OnlineDocs/explanation/philosophy/index.rst create mode 100644 doc/OnlineDocs/explanation/philosophy/transformations.rst create mode 100644 doc/OnlineDocs/explanation/solvers/index.rst create mode 100644 doc/OnlineDocs/reference/kernel/index.rst diff --git a/doc/OnlineDocs/explanation/analysis/index.rst b/doc/OnlineDocs/explanation/analysis/index.rst new file mode 100644 index 00000000000..0a8e3c3b416 --- /dev/null +++ b/doc/OnlineDocs/explanation/analysis/index.rst @@ -0,0 +1,20 @@ +Analysis in Pyomo +================= + +.. toctree:: + :maxdepth: 2 + + alternative_solutions + community + doe/doe + iis + incidence/index + mpc/index + parmest/index + sensitivity_toolbox + +.. + Reorganization notes: + + Analysis in Pyomo + `FBBT` diff --git a/doc/OnlineDocs/explanation/developer_utils/index.rst b/doc/OnlineDocs/explanation/developer_utils/index.rst new file mode 100644 index 00000000000..7b61e9a3ec1 --- /dev/null +++ b/doc/OnlineDocs/explanation/developer_utils/index.rst @@ -0,0 +1,8 @@ +Developer Utilities +=================== + +.. toctree:: + :maxdepth: 2 + + config + deprecation diff --git a/doc/OnlineDocs/explanation/experimental/index.rst b/doc/OnlineDocs/explanation/experimental/index.rst new file mode 100644 index 00000000000..0fe881d5011 --- /dev/null +++ b/doc/OnlineDocs/explanation/experimental/index.rst @@ -0,0 +1,8 @@ +Experimental features +===================== + +.. toctree:: + :maxdepth: 2 + + kernel/index + solvers diff --git a/doc/OnlineDocs/explanation/index.rst b/doc/OnlineDocs/explanation/index.rst index d2fa7377d39..0121debcd32 100644 --- a/doc/OnlineDocs/explanation/index.rst +++ b/doc/OnlineDocs/explanation/index.rst @@ -1,6 +1,55 @@ -User Guide -========== +Explanations +============ -:doc:`Common Warnings/Errors ` -:doc:`External Pyomo Tutorials ` +.. toctree:: + :maxdepth: 3 + philosophy/index + modeling/index + solvers/index + analysis/index + modeling_utils/index + developer_utils/index + experimental/index + + + +.. + Reorganization notes: + + `Pyomo Philosophy` + `Concrete and Abstract Models` + `Component Hierarchy` + `Expression System` + `Transformations` + `Modeling in Pyomo` + `Math Programming` + `GDP` + `DAE` + `Network` + `Piecewise Linear` + `Constraint Programming` + `Units of Measure` + `Solvers` + `PyROS` + `MindtPy` + `Trust Region` + `Pynumero` + `Analysis in Pyomo` + `IIS` + `FBBT` + `Incidence Analysis` + `Parameter Estimation` + `Design of Experiments` + `MPC` + `AOS` + `Modeling Utilities` + `Latex Printer` + `FME` + `Model Viewer` + `Model Flattening` + `Developer Utilities` + `Configuration System` + `Deprecation System` + `Experimental` + `Kernel` diff --git a/doc/OnlineDocs/explanation/modeling/index.rst b/doc/OnlineDocs/explanation/modeling/index.rst index c7f455be02c..f23f5421322 100644 --- a/doc/OnlineDocs/explanation/modeling/index.rst +++ b/doc/OnlineDocs/explanation/modeling/index.rst @@ -1,13 +1,25 @@ -Pyomo Modeling Components -========================= +Modeling in Pyomo +================= .. toctree:: :maxdepth: 1 - Sets.rst - Parameters.rst - Variables.rst - Objectives.rst - Constraints.rst - Expressions.rst - Suffixes.rst + math_programming/index + dae + gdp/index + mpec + network + units + + +.. + Reorganization notes: + + `Modeling in Pyomo` + `Math Programming` + `GDP` + `DAE` + `Network` + `Piecewise Linear` + `Constraint Programming` + `Units of Measure` diff --git a/doc/OnlineDocs/explanation/modeling/math_programming/index.rst b/doc/OnlineDocs/explanation/modeling/math_programming/index.rst new file mode 100644 index 00000000000..bc4e7c1803d --- /dev/null +++ b/doc/OnlineDocs/explanation/modeling/math_programming/index.rst @@ -0,0 +1,14 @@ +Math Programming +================ + +.. toctree:: + :maxdepth: 1 + + sets + parameters + variables + objectives + constraints + expressions + sos_constraints + suffixes diff --git a/doc/OnlineDocs/explanation/modeling_utils/index.rst b/doc/OnlineDocs/explanation/modeling_utils/index.rst new file mode 100644 index 00000000000..14bd4d9204c --- /dev/null +++ b/doc/OnlineDocs/explanation/modeling_utils/index.rst @@ -0,0 +1,22 @@ +Modeling Utilities +================== + +.. toctree:: + :maxdepth: 2 + + flattener/index + fme + latex_printer + preprocessing + scaling + viewer + + + +.. + Reorganization notes: + + `Latex Printer` + `FME` + `Model Viewer` + `Model Flattening` diff --git a/doc/OnlineDocs/explanation/philosophy/abstract_modeling.rst b/doc/OnlineDocs/explanation/philosophy/abstract_modeling.rst new file mode 100644 index 00000000000..877342de924 --- /dev/null +++ b/doc/OnlineDocs/explanation/philosophy/abstract_modeling.rst @@ -0,0 +1,60 @@ +Abstract Models +--------------- + +.. note:: + + TODO: this is a copy of "Abstractvs Concrete" from Getting Started. + This shoud beexpanded here. + + +A mathematical model can be defined using symbols that represent data +values. For example, the following equations represent a linear program +(LP) to find optimal values for the vector :math:`x` with parameters +:math:`n` and :math:`b`, and parameter vectors :math:`a` and :math:`c`: + +.. math:: + :nowrap: + + \begin{array}{lll} + \min & \sum_{j=1}^n c_j x_j &\\ + \mathrm{s.t.} & \sum_{j=1}^n a_{ij} x_j \geq b_i & \forall i = 1 \ldots m\\ + & x_j \geq 0 & \forall j = 1 \ldots n + \end{array} + +.. note:: + + As a convenience, we use the symbol :math:`\forall` to mean "for all" + or "for each." + +We call this an *abstract* or *symbolic* mathematical model since it +relies on unspecified parameter values. Data values can be used to +specify a *model instance*. The ``AbstractModel`` class provides a +context for defining and initializing abstract optimization models in +Pyomo when the data values will be supplied at the time a solution is to +be obtained. + +In many contexts, a mathematical model can and should be directly +defined with the data values supplied at the time of the model +definition. We call these *concrete* mathematical models. For example, +the following LP model is a concrete instance of the previous abstract +model: + +.. math:: + :nowrap: + + \begin{array}{ll} + \min & 2 x_1 + 3 x_2\\ + \mathrm{s.t.} & 3 x_1 + 4 x_2 \geq 1\\ + & x_1, x_2 \geq 0 + \end{array} + +The ``ConcreteModel`` class is used to define concrete optimization +models in Pyomo. + +.. note:: + + Python programmers will probably prefer to write concrete models, + while users of some other algebraic modeling languages may tend to + prefer to write abstract models. The choice is largely a matter of + taste; some applications may be a little more straightforward using + one or the other. diff --git a/doc/OnlineDocs/explanation/philosophy/component_design.rst b/doc/OnlineDocs/explanation/philosophy/component_design.rst new file mode 100644 index 00000000000..e4c526698d1 --- /dev/null +++ b/doc/OnlineDocs/explanation/philosophy/component_design.rst @@ -0,0 +1,4 @@ +Pyomo Component Design +====================== + +TODO diff --git a/doc/OnlineDocs/explanation/philosophy/index.rst b/doc/OnlineDocs/explanation/philosophy/index.rst new file mode 100644 index 00000000000..b45cf8806d0 --- /dev/null +++ b/doc/OnlineDocs/explanation/philosophy/index.rst @@ -0,0 +1,21 @@ +Pyomo Philosophy +================ + +.. toctree:: + :maxdepth: 2 + + abstract_modeling + component_design + expressions/index + transformations + + + +.. + Reorganization notes: + + `Pyomo Philosophy` + `Concrete and Abstract Models` + `Component Hierarchy` + `Expression System` + `Transformations` diff --git a/doc/OnlineDocs/explanation/philosophy/transformations.rst b/doc/OnlineDocs/explanation/philosophy/transformations.rst new file mode 100644 index 00000000000..363bcce73eb --- /dev/null +++ b/doc/OnlineDocs/explanation/philosophy/transformations.rst @@ -0,0 +1,4 @@ +Model Transformations +===================== + +TODO diff --git a/doc/OnlineDocs/explanation/solvers/index.rst b/doc/OnlineDocs/explanation/solvers/index.rst new file mode 100644 index 00000000000..a50a604f93e --- /dev/null +++ b/doc/OnlineDocs/explanation/solvers/index.rst @@ -0,0 +1,15 @@ +Solvers +======= + +.. toctree:: + :maxdepth: 2 + + persistent + gdpopt + pyros + mindtpy + mcpp + multistart + trustregion + pynumero/index + z3_interface diff --git a/doc/OnlineDocs/howto/solver_recipes.rst b/doc/OnlineDocs/howto/solver_recipes.rst index 6d7240184f0..c9be02405e2 100644 --- a/doc/OnlineDocs/howto/solver_recipes.rst +++ b/doc/OnlineDocs/howto/solver_recipes.rst @@ -142,4 +142,4 @@ TempFileManager service. One can accomplish the same through the following few lines of code in a script: .. literalinclude:: /src/scripting/spy4scripts_Specify_temporary_directory_name.spy -:language: python + :language: python diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index eb30282b908..eaf5122489f 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -21,57 +21,18 @@ with a diverse set of optimization capabilities. - .. toctree:: :maxdepth: 2 :titlesonly: - :includehidden: howto/index - * - User Explanations - | :doc:`Pyomo Philosophy` - | :doc:`Concrete and Abstract Models` - | :doc:`Component Hierarchy` - | :doc:`Expression System` - | :doc:`Transformations` - | :doc:`Modeling in Pyomo` - | :doc:`Math Programming` - | :doc:`GDP` - | :doc:`DAE` - | :doc:`Network` - | :doc:`Piecewise Linear` - | :doc:`Constraint Programming` - | :doc:`Units of Measure` - | :doc:`Solvers` - | :doc:`PyROS` - | :doc:`MindtPy` - | :doc:`Trust Region` - | :doc:`Pynumero` - | :doc:`Analysis in Pyomo` - | :doc:`IIS` - | :doc:`FBBT` - | :doc:`Incidence Analysis` - | :doc:`Parameter Estimation` - | :doc:`Design of Experiments` - | :doc:`MPC` - | :doc:`AOS` - | :doc:`Modeling Utilities` - | :doc:`Latex Printer` - | :doc:`FME` - | :doc:`Model Viewer` - | :doc:`Model Flattening` - | :doc:`Developer Utilities` - | :doc:`Configuration System` - | :doc:`Deprecation System` - | :doc:`Experimental` - | :doc:`Kernel` - - Reference Guides - | :doc:`Library Reference ` - | :doc:`Common Warnings and Errors` - | :doc:`Accessing preview capabilities ` - -.. toctree:: - :hidden: - :maxdepth: 2 - - User Explanations - Reference Guides + * - .. toctree:: + :maxdepth: 2 + :titlesonly: + + explanation/index + - .. toctree:: + :maxdepth: 2 + :titlesonly: + + reference/index Pyomo Resources @@ -110,7 +71,7 @@ Contributing to Pyomo --------------------- Interested in contributing code or documentation to the project? Check out our -:doc:`Contribution Guide ` +:doc:`Contribution Guide ` Related Packages ---------------- diff --git a/doc/OnlineDocs/reference/bibliography.rst b/doc/OnlineDocs/reference/bibliography.rst index c12d3f81d8c..fdd4c12545b 100644 --- a/doc/OnlineDocs/reference/bibliography.rst +++ b/doc/OnlineDocs/reference/bibliography.rst @@ -1,68 +1,77 @@ -Bibliography +Publications ============ -.. [AMPL] R. Fourer, D. M. Gay, and B. W. Kernighan. AMPL: A Modeling - Language for Mathematical Programming, 2nd Edition. Duxbury - Press, 2002. +These publications describe various Pyomo capabilitites or subpackages: + +.. [PyomoJournal] William E. Hart, Jean-Paul Watson, David L. Woodruff. + "Pyomo: modeling and solving mathematical programs in Python," + Mathematical Programming Computation, 3(3), August 2011 + +.. [PyomoBookI] William E. Hart, Carl D. Laird, Jean-Paul Watson, + David L. Woodruff. Pyomo – Optimization Modeling in Python, + Springer, 2012. + +.. [PyomoBookII] William E. Hart, Carl D. Laird, Jean-Paul Watson, + David L. Woodruff, Gabriel A. Hackebeil, Bethany L. Nicholson, + John D. Siirola. Pyomo - Optimization Modeling in Python, 2nd Edition. + Springer Optimization and Its Applications, Vol 67. + Springer, 2017. + +.. [PyomoBookIII] Bynum, Michael L., Gabriel A. Hackebeil, + William E. Hart, Carl D. Laird, Bethany L. Nicholson, + John D. Siirola, Jean-Paul Watson, and David L. Woodruff. Pyomo - + Optimization Modeling in Python, 3rd Edition. + Vol. 67. Springer, 2021. doi: `10.1007/978-3-030-68928-5 + `_ + +.. [PyomoDAE] Bethany Nicholson, John D. Siirola, Jean-Paul Watson, + Victor M. Zavala, and Lorenz T. Biegler. "pyomo.dae: a modeling and + automatic discretization framework for optimization with differential + and algebraic equations." Mathematical Programming Computation 10(2) + 187-223. 2018. + +.. [ParmestPaper] Katherine A. Klise, Bethany L. Nicholson, Andrea + Staid, David L.Woodruff. Parmest: Parameter Estimation Via Pyomo. + Computer Aided Chemical Engineering, 47: 41-46. 2019. + +.. [PyomoGDP] Chen, Q., Johnson, E. S., Bernal, D. E., Valentin, R., + Kale, S., Bates, J., Siirola, J. D. and Grossmann, I. E. Pyomo.GDP: + an ecosystem for logic based modeling and optimization development, + *Optimization and Engineering* pp. 1-36. 2021. DOI + `10.1007/s11081-021-09601-7 + `_ + + +Bibliography +============ .. [AIMMS] http://www.aimms.com/ +.. [AMPL] R. Fourer, D. M. Gay, and B. W. Kernighan. AMPL: A Modeling + Language for Mathematical Programming, 2nd Edition. Duxbury + Press, 2002. + .. [GAMS] http://www.gams.com -.. [Isenberg_et_al] Isenberg, NM, Akula, P, Eslick, JC, Bhattacharyya, D, - Miller, DC, Gounaris, CE. A generalized cutting‐set approach for +.. [Isenberg_et_al] Isenberg, N.M., Akula, P., Eslick, J.C., Bhattacharyya, D., + Miller, D.C., Gounaris, C.E. A generalized cutting‐set approach for nonlinear robust optimization in process systems - engineering. AIChE J. 2021; 67:e17175. DOI `10.1002/aic.17175 + engineering. AIChE Journal. 67:e17175. 2021; DOI `10.1002/aic.17175 `_ .. [mpisppy] Bernard Knueven, David Mildebrath, Christopher Muir, - John D Siirola, Jean-Paul Watson, and David L Woodruff, A Parallel + John D. Siirola, Jean-Paul Watson, and David L Woodruff, A Parallel Hub-and-Spoke System for Large-Scale Scenario-Based Optimization - Under Uncertainty, pre-print, 2020 + Under Uncertainty, pre-print, 2020. -.. [ParmestPaper] Katherine A. Klise, Bethany L. Nicholson, Andrea - Staid, David L.Woodruff. Parmest: Parameter Estimation Via Pyomo. - Computer Aided Chemical Engineering, 47 (2019): 41-46. - -.. [PyomoBookI] William E. Hart, Carl D. Laird, Jean-Paul Watson, - David L. Woodruff. Pyomo – Optimization Modeling in - Python, Springer, 2012. -.. [PyomoBookII] W. E. Hart, C. D. Laird, - J.-P. Watson, D. L. Woodruff, G. A. Hackebeil, B. L. Nicholson, - J. D. Siirola. Pyomo - Optimization Modeling in Python, - 2nd Edition. Springer Optimization and Its - Applications, Vol 67. Springer, 2017. - -.. [PyomoBookIII] Bynum, Michael L., Gabriel A. Hackebeil, - William E. Hart, Carl D. Laird, Bethany L. Nicholson, - John D. Siirola, Jean-Paul Watson, and David L. Woodruff. - Pyomo - Optimization Modeling in Python, 3rd Edition. - Vol. 67. Springer, 2021. - doi: `10.1007/978-3-030-68928-5 - `_ - -.. [PyomoJournal] William E. Hart, Jean-Paul Watson, David L. Woodruff. - "Pyomo: modeling and solving mathematical programs in - Python," Mathematical Programming Computation, Volume - 3, Number 3, August 2011 - -.. [PyomoDAE] Bethany Nicholson, John D. Siirola, Jean-Paul Watson, - Victor M. Zavala, and Lorenz T. Biegler. "pyomo.dae: a - modeling and automatic discretization framework for - optimization with differential and algebraic equations." - Mathematical Programming Computation 10(2) (2018): - 187-223. - -.. [RooneyBiegler] W.C. Rooney, L.T. Biegler, "Design for model parameter - uncertainty using nonlinear confidence regions", AIChE - Journal, 47(8), 2001 +.. [RooneyBiegler] W.C. Rooney, L.T. Biegler, "Design for model + parameter uncertainty using nonlinear confidence regions", AIChE + Journal, 47(8), 2001. -.. [SemiBatch] O. Abel, W. Marquardt, "Scenario-integrated modeling and - optimization of dynamic systems", AIChE Journal, 46(4), 2000 +.. [SemiBatch] O. Abel, W. Marquardt, "Scenario-integrated modeling and + optimization of dynamic systems", AIChE Journal, 46(4), 2000. .. [Vielma_et_al] J. P. Vielma, S. Ahmed, G. Nemhauser. "Mixed-Integer - Models for Non-separable Piecewise Linear - Optimization: Unifying framework and Extensions", - Operations Research 58, 2010. pp. 303-315. - + Models for Non-separable Piecewise Linear Optimization: Unifying + framework and Extensions", Operations Research 58, pp. 303-315. 2010. diff --git a/doc/OnlineDocs/reference/index.rst b/doc/OnlineDocs/reference/index.rst index 7e66d29db96..d27b72c3f5e 100644 --- a/doc/OnlineDocs/reference/index.rst +++ b/doc/OnlineDocs/reference/index.rst @@ -1,17 +1,20 @@ -Reference Guide -=============== +Reference Guides +================ .. toctree:: :maxdepth: 2 - library_reference/index.rst - ../errors.rst - future.rst - - -Bibliography ------------- - -:doc:`Bibliography ` + library_reference/index + kernel/index + ../errors + future + ../related_packages + bibliography +.. + autosummary:: + :toctree: API + :template: pyomo-autosummary-module.rst + :recursive: + pyomo diff --git a/doc/OnlineDocs/reference/kernel/index.rst b/doc/OnlineDocs/reference/kernel/index.rst new file mode 100644 index 00000000000..03df24215a8 --- /dev/null +++ b/doc/OnlineDocs/reference/kernel/index.rst @@ -0,0 +1,52 @@ +.. role:: python(code) + :language: python + +.. warning:: + + The :python:`pyomo.kernel` API is still in the beta phase of development. It is fully tested and functional; however, the interface may change as it becomes further integrated with the rest of Pyomo. + +.. warning:: + + Models built with :python:`pyomo.kernel` components are not yet compatible with pyomo extension modules (e.g., :python:`PySP`, :python:`pyomo.dae`, :python:`pyomo.gdp`). + +The Kernel Library API Reference +================================ + +.. _kernel_modeling_components: + +Modeling Components: +^^^^^^^^^^^^^^^^^^^^ + +.. toctree:: + :maxdepth: 1 + + block.rst + variable.rst + constraint.rst + parameter.rst + objective.rst + expression.rst + sos.rst + suffix.rst + piecewise/index.rst + conic.rst + +Base API: +^^^^^^^^^ + +.. toctree:: + :maxdepth: 1 + + base.rst + homogeneous_container.rst + heterogeneous_container.rst + +Containers: +^^^^^^^^^^^ + +.. toctree:: + :maxdepth: 1 + + tuple_container.rst + list_container.rst + dict_container.rst From 4e319745ef45c2a232985cc7352e16747405eb48 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 15:54:18 -0600 Subject: [PATCH 2407/3044] Track move of kernel discussion --- doc/OnlineDocs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 3c89d4186d5..467bd21f8b6 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -44,7 +44,7 @@ generate_spy_files(os.path.abspath('src')) generate_spy_files( - os.path.abspath(os.path.join('reference', 'library_reference', 'kernel', 'examples')) + os.path.abspath(os.path.join('explanation', 'experimental', 'kernel')) ) finally: sys.path.pop(0) From d45733bd60bdf3c8c73b5769dfb6f6b38c76ba37 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 16:02:40 -0600 Subject: [PATCH 2408/3044] Move kernel API docs --- .../explanation/experimental/kernel/index.rst | 42 ------------------- doc/OnlineDocs/reference/index.rst | 1 - .../{ => library_reference}/kernel/base.rst | 0 .../{ => library_reference}/kernel/block.rst | 0 .../{ => library_reference}/kernel/conic.rst | 0 .../kernel/constraint.rst | 0 .../kernel/dict_container.rst | 0 .../kernel/expression.rst | 0 .../kernel/heterogeneous_container.rst | 0 .../kernel/homogeneous_container.rst | 0 .../{ => library_reference}/kernel/index.rst | 0 .../kernel/list_container.rst | 0 .../kernel/objective.rst | 0 .../kernel/parameter.rst | 0 .../kernel/piecewise/index.rst | 0 .../kernel/piecewise/piecewise.rst | 0 .../kernel/piecewise/piecewise_nd.rst | 0 .../kernel/piecewise/util.rst | 0 .../{ => library_reference}/kernel/sos.rst | 0 .../{ => library_reference}/kernel/suffix.rst | 0 .../kernel/tuple_container.rst | 0 .../kernel/variable.rst | 0 22 files changed, 43 deletions(-) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/base.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/block.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/conic.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/constraint.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/dict_container.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/expression.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/heterogeneous_container.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/homogeneous_container.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/index.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/list_container.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/objective.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/parameter.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/piecewise/index.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/piecewise/piecewise.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/piecewise/piecewise_nd.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/piecewise/util.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/sos.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/suffix.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/tuple_container.rst (100%) rename doc/OnlineDocs/reference/{ => library_reference}/kernel/variable.rst (100%) diff --git a/doc/OnlineDocs/explanation/experimental/kernel/index.rst b/doc/OnlineDocs/explanation/experimental/kernel/index.rst index 70c3cc715a9..de1ea1e8c61 100644 --- a/doc/OnlineDocs/explanation/experimental/kernel/index.rst +++ b/doc/OnlineDocs/explanation/experimental/kernel/index.rst @@ -166,45 +166,3 @@ variables. Example: .. literalinclude:: examples/conic_Domain.spy :language: python - -Reference ---------- - -.. _kernel_modeling_components: - -Modeling Components: -^^^^^^^^^^^^^^^^^^^^ - -.. toctree:: - :maxdepth: 1 - - block.rst - variable.rst - constraint.rst - parameter.rst - objective.rst - expression.rst - sos.rst - suffix.rst - piecewise/index.rst - conic.rst - -Base API: -^^^^^^^^^ - -.. toctree:: - :maxdepth: 1 - - base.rst - homogeneous_container.rst - heterogeneous_container.rst - -Containers: -^^^^^^^^^^^ - -.. toctree:: - :maxdepth: 1 - - tuple_container.rst - list_container.rst - dict_container.rst diff --git a/doc/OnlineDocs/reference/index.rst b/doc/OnlineDocs/reference/index.rst index d27b72c3f5e..0c793d607dd 100644 --- a/doc/OnlineDocs/reference/index.rst +++ b/doc/OnlineDocs/reference/index.rst @@ -5,7 +5,6 @@ Reference Guides :maxdepth: 2 library_reference/index - kernel/index ../errors future ../related_packages diff --git a/doc/OnlineDocs/reference/kernel/base.rst b/doc/OnlineDocs/reference/library_reference/kernel/base.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/base.rst rename to doc/OnlineDocs/reference/library_reference/kernel/base.rst diff --git a/doc/OnlineDocs/reference/kernel/block.rst b/doc/OnlineDocs/reference/library_reference/kernel/block.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/block.rst rename to doc/OnlineDocs/reference/library_reference/kernel/block.rst diff --git a/doc/OnlineDocs/reference/kernel/conic.rst b/doc/OnlineDocs/reference/library_reference/kernel/conic.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/conic.rst rename to doc/OnlineDocs/reference/library_reference/kernel/conic.rst diff --git a/doc/OnlineDocs/reference/kernel/constraint.rst b/doc/OnlineDocs/reference/library_reference/kernel/constraint.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/constraint.rst rename to doc/OnlineDocs/reference/library_reference/kernel/constraint.rst diff --git a/doc/OnlineDocs/reference/kernel/dict_container.rst b/doc/OnlineDocs/reference/library_reference/kernel/dict_container.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/dict_container.rst rename to doc/OnlineDocs/reference/library_reference/kernel/dict_container.rst diff --git a/doc/OnlineDocs/reference/kernel/expression.rst b/doc/OnlineDocs/reference/library_reference/kernel/expression.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/expression.rst rename to doc/OnlineDocs/reference/library_reference/kernel/expression.rst diff --git a/doc/OnlineDocs/reference/kernel/heterogeneous_container.rst b/doc/OnlineDocs/reference/library_reference/kernel/heterogeneous_container.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/heterogeneous_container.rst rename to doc/OnlineDocs/reference/library_reference/kernel/heterogeneous_container.rst diff --git a/doc/OnlineDocs/reference/kernel/homogeneous_container.rst b/doc/OnlineDocs/reference/library_reference/kernel/homogeneous_container.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/homogeneous_container.rst rename to doc/OnlineDocs/reference/library_reference/kernel/homogeneous_container.rst diff --git a/doc/OnlineDocs/reference/kernel/index.rst b/doc/OnlineDocs/reference/library_reference/kernel/index.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/index.rst rename to doc/OnlineDocs/reference/library_reference/kernel/index.rst diff --git a/doc/OnlineDocs/reference/kernel/list_container.rst b/doc/OnlineDocs/reference/library_reference/kernel/list_container.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/list_container.rst rename to doc/OnlineDocs/reference/library_reference/kernel/list_container.rst diff --git a/doc/OnlineDocs/reference/kernel/objective.rst b/doc/OnlineDocs/reference/library_reference/kernel/objective.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/objective.rst rename to doc/OnlineDocs/reference/library_reference/kernel/objective.rst diff --git a/doc/OnlineDocs/reference/kernel/parameter.rst b/doc/OnlineDocs/reference/library_reference/kernel/parameter.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/parameter.rst rename to doc/OnlineDocs/reference/library_reference/kernel/parameter.rst diff --git a/doc/OnlineDocs/reference/kernel/piecewise/index.rst b/doc/OnlineDocs/reference/library_reference/kernel/piecewise/index.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/piecewise/index.rst rename to doc/OnlineDocs/reference/library_reference/kernel/piecewise/index.rst diff --git a/doc/OnlineDocs/reference/kernel/piecewise/piecewise.rst b/doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/piecewise/piecewise.rst rename to doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise.rst diff --git a/doc/OnlineDocs/reference/kernel/piecewise/piecewise_nd.rst b/doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise_nd.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/piecewise/piecewise_nd.rst rename to doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise_nd.rst diff --git a/doc/OnlineDocs/reference/kernel/piecewise/util.rst b/doc/OnlineDocs/reference/library_reference/kernel/piecewise/util.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/piecewise/util.rst rename to doc/OnlineDocs/reference/library_reference/kernel/piecewise/util.rst diff --git a/doc/OnlineDocs/reference/kernel/sos.rst b/doc/OnlineDocs/reference/library_reference/kernel/sos.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/sos.rst rename to doc/OnlineDocs/reference/library_reference/kernel/sos.rst diff --git a/doc/OnlineDocs/reference/kernel/suffix.rst b/doc/OnlineDocs/reference/library_reference/kernel/suffix.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/suffix.rst rename to doc/OnlineDocs/reference/library_reference/kernel/suffix.rst diff --git a/doc/OnlineDocs/reference/kernel/tuple_container.rst b/doc/OnlineDocs/reference/library_reference/kernel/tuple_container.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/tuple_container.rst rename to doc/OnlineDocs/reference/library_reference/kernel/tuple_container.rst diff --git a/doc/OnlineDocs/reference/kernel/variable.rst b/doc/OnlineDocs/reference/library_reference/kernel/variable.rst similarity index 100% rename from doc/OnlineDocs/reference/kernel/variable.rst rename to doc/OnlineDocs/reference/library_reference/kernel/variable.rst From d0cef84258141868e20cbfc11f3afa3c1817ac5e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 16:10:34 -0600 Subject: [PATCH 2409/3044] Fix link, improve page title --- doc/OnlineDocs/explanation/experimental/solvers.rst | 2 +- doc/OnlineDocs/reference/future.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/explanation/experimental/solvers.rst b/doc/OnlineDocs/explanation/experimental/solvers.rst index 9e3281246f4..cd4cafa89dd 100644 --- a/doc/OnlineDocs/explanation/experimental/solvers.rst +++ b/doc/OnlineDocs/explanation/experimental/solvers.rst @@ -197,7 +197,7 @@ Switching all of Pyomo to use the new interfaces We also provide a mechanism to get a "preview" of the future where we replace the existing (legacy) SolverFactory and utilities with the new -(development) version (see :doc:`future`): +(development) version (see :doc:`/reference/future`): .. testcode:: :skipif: not ipopt_available diff --git a/doc/OnlineDocs/reference/future.rst b/doc/OnlineDocs/reference/future.rst index 531c0fdb5c6..1dd7a1060f3 100644 --- a/doc/OnlineDocs/reference/future.rst +++ b/doc/OnlineDocs/reference/future.rst @@ -1,3 +1,5 @@ +Accessing preview features +========================== .. automodule:: pyomo.__future__ :noindex: From e5452d8aab42b73c5fdbcf7039ce3ce5365a1537 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 17:00:50 -0600 Subject: [PATCH 2410/3044] Improve diataxis table formatting --- doc/OnlineDocs/_static/theme_overrides.css | 15 ++++++++++++++- doc/OnlineDocs/index.rst | 12 +++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/doc/OnlineDocs/_static/theme_overrides.css b/doc/OnlineDocs/_static/theme_overrides.css index 12b8bae6d9a..18dabe478aa 100644 --- a/doc/OnlineDocs/_static/theme_overrides.css +++ b/doc/OnlineDocs/_static/theme_overrides.css @@ -31,10 +31,23 @@ dl.py.method dt em span.n { } } -.rst-content table.docutils td { +.rst-content table.diataxis td { vertical-align: top; } +.rst-content table.diataxis li.toctree-l1 { + list-style-type: none; + font-weight: bold; + font-size: x-large; +} + +.rst-content table.diataxis li ul li { + list-style-type: none; + font-weight: normal; + font-size: medium; +} + + /* Remove space after tables in definition lists (e.g., for function "Parameters" lists*/ .rst-content dl div.wy-table-responsive { diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index eaf5122489f..8fc5ac867d3 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -1,6 +1,10 @@ +============================= Pyomo Documentation |release| ============================= +About Pyomo +----------- + .. image:: /../logos/pyomo/PyomoNewBlue3.png :scale: 10% :align: right @@ -9,9 +13,11 @@ Pyomo is a Python-based, open-source optimization modeling language with a diverse set of optimization capabilities. +Contents +-------- .. list-table:: :width: 100% - :class: index-table + :class: diataxis * - .. toctree:: :maxdepth: 2 @@ -24,12 +30,12 @@ with a diverse set of optimization capabilities. howto/index * - .. toctree:: - :maxdepth: 2 + :maxdepth: 3 :titlesonly: explanation/index - .. toctree:: - :maxdepth: 2 + :maxdepth: 3 :titlesonly: reference/index From a9a15abd0264b08b8f52de0587d3332ea396ee0f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 17:23:19 -0600 Subject: [PATCH 2411/3044] More style tweaks --- doc/OnlineDocs/_static/theme_overrides.css | 4 ++++ doc/OnlineDocs/index.rst | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/doc/OnlineDocs/_static/theme_overrides.css b/doc/OnlineDocs/_static/theme_overrides.css index 18dabe478aa..936b3f95b13 100644 --- a/doc/OnlineDocs/_static/theme_overrides.css +++ b/doc/OnlineDocs/_static/theme_overrides.css @@ -47,6 +47,10 @@ dl.py.method dt em span.n { font-size: medium; } +.rst-content table.diataxis li ul li ul li { + list-style-type: "- "; +} + /* Remove space after tables in definition lists (e.g., for function "Parameters" lists*/ diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index 8fc5ac867d3..0cd9b7d07af 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -40,6 +40,15 @@ Contents reference/index +.. + toctree:: + :maxdepth: 2 + :titlesonly: + :hidden: + + genindex + modindex + Pyomo Resources --------------- From 3564044d5c06d2cc745d84a8d08d20af773be9a0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 19:29:22 -0600 Subject: [PATCH 2412/3044] reworking src to restore history --- doc/OnlineDocs/src/data/A.tab | 4 - doc/OnlineDocs/src/data/ABCD.tab | 4 - doc/OnlineDocs/src/data/ABCD.txt | 4 - doc/OnlineDocs/src/data/ABCD.xls | Bin 16384 -> 0 bytes doc/OnlineDocs/src/data/ABCD1.dat | 1 - doc/OnlineDocs/src/data/ABCD1.py | 20 -- doc/OnlineDocs/src/data/ABCD1.txt | 1 - doc/OnlineDocs/src/data/ABCD2.dat | 1 - doc/OnlineDocs/src/data/ABCD2.py | 25 -- doc/OnlineDocs/src/data/ABCD2.txt | 5 - doc/OnlineDocs/src/data/ABCD3.dat | 1 - doc/OnlineDocs/src/data/ABCD3.py | 24 -- doc/OnlineDocs/src/data/ABCD3.txt | 5 - doc/OnlineDocs/src/data/ABCD4.dat | 1 - doc/OnlineDocs/src/data/ABCD4.py | 24 -- doc/OnlineDocs/src/data/ABCD4.txt | 5 - doc/OnlineDocs/src/data/ABCD5.dat | 1 - doc/OnlineDocs/src/data/ABCD5.py | 30 -- doc/OnlineDocs/src/data/ABCD5.txt | 9 - doc/OnlineDocs/src/data/ABCD6.dat | 1 - doc/OnlineDocs/src/data/ABCD6.py | 24 -- doc/OnlineDocs/src/data/ABCD6.txt | 5 - doc/OnlineDocs/src/data/ABCD7.dat | 1 - doc/OnlineDocs/src/data/ABCD7.py | 30 -- doc/OnlineDocs/src/data/ABCD7.txt | 5 - doc/OnlineDocs/src/data/ABCD8.bad | 1 - doc/OnlineDocs/src/data/ABCD8.dat | 1 - doc/OnlineDocs/src/data/ABCD8.py | 30 -- doc/OnlineDocs/src/data/ABCD9.bad | 1 - doc/OnlineDocs/src/data/ABCD9.dat | 3 - doc/OnlineDocs/src/data/ABCD9.py | 30 -- doc/OnlineDocs/src/data/C.tab | 10 - doc/OnlineDocs/src/data/D.tab | 4 - doc/OnlineDocs/src/data/U.tab | 5 - doc/OnlineDocs/src/data/Y.tab | 4 - doc/OnlineDocs/src/data/Z.tab | 1 - doc/OnlineDocs/src/data/data_managers.txt | 30 -- doc/OnlineDocs/src/data/diet.dat | 33 -- doc/OnlineDocs/src/data/diet.sql | 96 ----- doc/OnlineDocs/src/data/diet.sqlite | Bin 11264 -> 0 bytes doc/OnlineDocs/src/data/diet.sqlite.dat | 6 - doc/OnlineDocs/src/data/diet1.py | 86 ----- doc/OnlineDocs/src/data/ex.dat | 2 - doc/OnlineDocs/src/data/ex.py | 22 -- doc/OnlineDocs/src/data/ex.txt | 1 - doc/OnlineDocs/src/data/ex1.dat | 1 - doc/OnlineDocs/src/data/ex2.dat | 1 - doc/OnlineDocs/src/data/import1.tab.dat | 1 - doc/OnlineDocs/src/data/import1.tab.py | 24 -- doc/OnlineDocs/src/data/import1.tab.txt | 4 - doc/OnlineDocs/src/data/import2.tab.dat | 1 - doc/OnlineDocs/src/data/import2.tab.py | 25 -- doc/OnlineDocs/src/data/import2.tab.txt | 5 - doc/OnlineDocs/src/data/import3.tab.dat | 1 - doc/OnlineDocs/src/data/import3.tab.py | 20 -- doc/OnlineDocs/src/data/import3.tab.txt | 1 - doc/OnlineDocs/src/data/import4.tab.dat | 1 - doc/OnlineDocs/src/data/import4.tab.py | 20 -- doc/OnlineDocs/src/data/import4.tab.txt | 1 - doc/OnlineDocs/src/data/import5.tab.dat | 1 - doc/OnlineDocs/src/data/import5.tab.py | 20 -- doc/OnlineDocs/src/data/import5.tab.txt | 1 - doc/OnlineDocs/src/data/import6.tab.dat | 1 - doc/OnlineDocs/src/data/import6.tab.py | 20 -- doc/OnlineDocs/src/data/import6.tab.txt | 1 - doc/OnlineDocs/src/data/import7.tab.dat | 1 - doc/OnlineDocs/src/data/import7.tab.py | 28 -- doc/OnlineDocs/src/data/import7.tab.txt | 15 - doc/OnlineDocs/src/data/import8.tab.dat | 1 - doc/OnlineDocs/src/data/import8.tab.py | 26 -- doc/OnlineDocs/src/data/import8.tab.txt | 15 - doc/OnlineDocs/src/data/namespace1.dat | 12 - doc/OnlineDocs/src/data/param1.dat | 5 - doc/OnlineDocs/src/data/param1.py | 30 -- doc/OnlineDocs/src/data/param1.txt | 5 - doc/OnlineDocs/src/data/param2.dat | 3 - doc/OnlineDocs/src/data/param2.py | 25 -- doc/OnlineDocs/src/data/param2.txt | 3 - doc/OnlineDocs/src/data/param2a.dat | 7 - doc/OnlineDocs/src/data/param2a.py | 25 -- doc/OnlineDocs/src/data/param2a.txt | 3 - doc/OnlineDocs/src/data/param3.dat | 7 - doc/OnlineDocs/src/data/param3.py | 36 -- doc/OnlineDocs/src/data/param3.txt | 12 - doc/OnlineDocs/src/data/param3a.dat | 7 - doc/OnlineDocs/src/data/param3a.py | 36 -- doc/OnlineDocs/src/data/param3a.txt | 12 - doc/OnlineDocs/src/data/param3b.dat | 7 - doc/OnlineDocs/src/data/param3b.py | 36 -- doc/OnlineDocs/src/data/param3b.txt | 9 - doc/OnlineDocs/src/data/param3c.dat | 5 - doc/OnlineDocs/src/data/param3c.py | 36 -- doc/OnlineDocs/src/data/param3c.txt | 12 - doc/OnlineDocs/src/data/param4.dat | 6 - doc/OnlineDocs/src/data/param4.py | 26 -- doc/OnlineDocs/src/data/param4.txt | 4 - doc/OnlineDocs/src/data/param5.dat | 6 - doc/OnlineDocs/src/data/param5.py | 25 -- doc/OnlineDocs/src/data/param5.txt | 3 - doc/OnlineDocs/src/data/param5a.dat | 6 - doc/OnlineDocs/src/data/param5a.py | 25 -- doc/OnlineDocs/src/data/param5a.txt | 3 - doc/OnlineDocs/src/data/param6.dat | 7 - doc/OnlineDocs/src/data/param6.py | 36 -- doc/OnlineDocs/src/data/param6.txt | 12 - doc/OnlineDocs/src/data/param6a.dat | 5 - doc/OnlineDocs/src/data/param6a.py | 36 -- doc/OnlineDocs/src/data/param6a.txt | 12 - doc/OnlineDocs/src/data/param7a.dat | 7 - doc/OnlineDocs/src/data/param7a.py | 25 -- doc/OnlineDocs/src/data/param7a.txt | 9 - doc/OnlineDocs/src/data/param7b.dat | 7 - doc/OnlineDocs/src/data/param7b.py | 25 -- doc/OnlineDocs/src/data/param7b.txt | 9 - doc/OnlineDocs/src/data/param8a.dat | 7 - doc/OnlineDocs/src/data/param8a.py | 25 -- doc/OnlineDocs/src/data/param8a.txt | 4 - doc/OnlineDocs/src/data/pyomo.diet1.sh | 4 - doc/OnlineDocs/src/data/pyomo.diet1.txt | 60 ---- doc/OnlineDocs/src/data/pyomo.diet2.sh | 4 - doc/OnlineDocs/src/data/pyomo.diet2.txt | 60 ---- doc/OnlineDocs/src/data/set1.dat | 17 - doc/OnlineDocs/src/data/set1.py | 24 -- doc/OnlineDocs/src/data/set1.txt | 3 - doc/OnlineDocs/src/data/set2.dat | 1 - doc/OnlineDocs/src/data/set2.py | 22 -- doc/OnlineDocs/src/data/set2.txt | 1 - doc/OnlineDocs/src/data/set2a.dat | 1 - doc/OnlineDocs/src/data/set2a.py | 22 -- doc/OnlineDocs/src/data/set2a.txt | 1 - doc/OnlineDocs/src/data/set3.dat | 5 - doc/OnlineDocs/src/data/set3.py | 30 -- doc/OnlineDocs/src/data/set3.txt | 3 - doc/OnlineDocs/src/data/set4.dat | 4 - doc/OnlineDocs/src/data/set4.py | 22 -- doc/OnlineDocs/src/data/set4.txt | 1 - doc/OnlineDocs/src/data/set5.dat | 3 - doc/OnlineDocs/src/data/set5.py | 24 -- doc/OnlineDocs/src/data/set5.txt | 4 - doc/OnlineDocs/src/data/table0.dat | 6 - doc/OnlineDocs/src/data/table0.py | 20 -- doc/OnlineDocs/src/data/table0.txt | 13 - doc/OnlineDocs/src/data/table0.ul.dat | 5 - doc/OnlineDocs/src/data/table0.ul.py | 20 -- doc/OnlineDocs/src/data/table0.ul.txt | 13 - doc/OnlineDocs/src/data/table1.dat | 3 - doc/OnlineDocs/src/data/table1.py | 20 -- doc/OnlineDocs/src/data/table1.txt | 13 - doc/OnlineDocs/src/data/table2.dat | 6 - doc/OnlineDocs/src/data/table2.py | 23 -- doc/OnlineDocs/src/data/table2.txt | 21 -- doc/OnlineDocs/src/data/table3.dat | 6 - doc/OnlineDocs/src/data/table3.py | 25 -- doc/OnlineDocs/src/data/table3.txt | 24 -- doc/OnlineDocs/src/data/table3.ul.dat | 5 - doc/OnlineDocs/src/data/table3.ul.py | 25 -- doc/OnlineDocs/src/data/table3.ul.txt | 24 -- doc/OnlineDocs/src/data/table4.dat | 6 - doc/OnlineDocs/src/data/table4.py | 23 -- doc/OnlineDocs/src/data/table4.txt | 21 -- doc/OnlineDocs/src/data/table4.ul.dat | 5 - doc/OnlineDocs/src/data/table4.ul.py | 23 -- doc/OnlineDocs/src/data/table4.ul.txt | 21 -- doc/OnlineDocs/src/data/table5.dat | 6 - doc/OnlineDocs/src/data/table5.py | 20 -- doc/OnlineDocs/src/data/table5.txt | 9 - doc/OnlineDocs/src/data/table6.dat | 1 - doc/OnlineDocs/src/data/table6.py | 19 - doc/OnlineDocs/src/data/table6.txt | 6 - doc/OnlineDocs/src/data/table7.dat | 6 - doc/OnlineDocs/src/data/table7.py | 21 -- doc/OnlineDocs/src/data/table7.txt | 16 - doc/OnlineDocs/src/dataportal/A.tab | 4 - doc/OnlineDocs/src/dataportal/C.tab | 10 - doc/OnlineDocs/src/dataportal/D.tab | 4 - doc/OnlineDocs/src/dataportal/PP.csv | 4 - doc/OnlineDocs/src/dataportal/PP.json | 9 - doc/OnlineDocs/src/dataportal/PP.sqlite | Bin 3072 -> 0 bytes doc/OnlineDocs/src/dataportal/PP.tab | 4 - doc/OnlineDocs/src/dataportal/PP.xml | 11 - doc/OnlineDocs/src/dataportal/PP.yaml | 15 - doc/OnlineDocs/src/dataportal/PP_sqlite.py | 40 --- doc/OnlineDocs/src/dataportal/Pyomo_mysql | 10 - doc/OnlineDocs/src/dataportal/S.tab | 4 - doc/OnlineDocs/src/dataportal/T.json | 8 - doc/OnlineDocs/src/dataportal/T.yaml | 17 - doc/OnlineDocs/src/dataportal/U.tab | 5 - doc/OnlineDocs/src/dataportal/XW.tab | 4 - doc/OnlineDocs/src/dataportal/Y.tab | 4 - doc/OnlineDocs/src/dataportal/Z.tab | 1 - .../src/dataportal/dataportal_tab.py | 334 ------------------ .../src/dataportal/dataportal_tab.txt | 315 ----------------- doc/OnlineDocs/src/dataportal/excel.xls | Bin 17920 -> 0 bytes .../src/dataportal/param_initialization.py | 36 -- .../src/dataportal/param_initialization.txt | 16 - .../src/dataportal/set_initialization.py | 55 --- .../src/dataportal/set_initialization.txt | 31 -- doc/OnlineDocs/src/expr/design.py | 64 ---- doc/OnlineDocs/src/expr/design.txt | 19 - doc/OnlineDocs/src/expr/index.py | 21 -- doc/OnlineDocs/src/expr/index.txt | 1 - doc/OnlineDocs/src/expr/managing.py | 249 ------------- doc/OnlineDocs/src/expr/managing.txt | 9 - doc/OnlineDocs/src/expr/overview.py | 103 ------ doc/OnlineDocs/src/expr/overview.txt | 11 - doc/OnlineDocs/src/expr/performance.py | 108 ------ doc/OnlineDocs/src/expr/performance.txt | 14 - doc/OnlineDocs/src/expr/quicksum.log | 4 - doc/OnlineDocs/src/expr/quicksum.py | 38 -- doc/OnlineDocs/src/kernel/examples.sh | 3 - doc/OnlineDocs/src/kernel/examples.txt | 211 ----------- .../src/scripting/AbstractSuffixes.py | 35 -- doc/OnlineDocs/src/scripting/Isinglebuild.py | 58 --- doc/OnlineDocs/src/scripting/Isinglecomm.dat | 25 -- doc/OnlineDocs/src/scripting/NodesIn_init.py | 21 -- doc/OnlineDocs/src/scripting/Z_init.py | 19 - doc/OnlineDocs/src/scripting/abstract1.dat | 18 - doc/OnlineDocs/src/scripting/abstract2.dat | 22 -- doc/OnlineDocs/src/scripting/abstract2.py | 43 --- doc/OnlineDocs/src/scripting/abstract2a.dat | 16 - .../src/scripting/abstract2piece.py | 62 ---- .../src/scripting/abstract2piecebuild.py | 77 ---- .../src/scripting/block_iter_example.py | 51 --- doc/OnlineDocs/src/scripting/concrete1.py | 20 -- doc/OnlineDocs/src/scripting/doubleA.py | 17 - doc/OnlineDocs/src/scripting/driveabs2.py | 47 --- doc/OnlineDocs/src/scripting/driveconc1.py | 34 -- doc/OnlineDocs/src/scripting/iterative1.py | 74 ---- doc/OnlineDocs/src/scripting/iterative2.py | 52 --- doc/OnlineDocs/src/scripting/noiteration1.py | 41 --- doc/OnlineDocs/src/scripting/parallel.py | 38 -- .../src/scripting/spy4Constraints.py | 62 ---- .../src/scripting/spy4Expressions.py | 128 ------- .../src/scripting/spy4PyomoCommand.py | 36 -- doc/OnlineDocs/src/scripting/spy4Variables.py | 39 -- doc/OnlineDocs/src/scripting/spy4scripts.py | 220 ------------ doc/OnlineDocs/src/strip_examples.py | 80 ----- doc/OnlineDocs/src/test_examples.py | 76 ---- 238 files changed, 5494 deletions(-) delete mode 100644 doc/OnlineDocs/src/data/A.tab delete mode 100644 doc/OnlineDocs/src/data/ABCD.tab delete mode 100644 doc/OnlineDocs/src/data/ABCD.txt delete mode 100755 doc/OnlineDocs/src/data/ABCD.xls delete mode 100644 doc/OnlineDocs/src/data/ABCD1.dat delete mode 100644 doc/OnlineDocs/src/data/ABCD1.py delete mode 100644 doc/OnlineDocs/src/data/ABCD1.txt delete mode 100644 doc/OnlineDocs/src/data/ABCD2.dat delete mode 100644 doc/OnlineDocs/src/data/ABCD2.py delete mode 100644 doc/OnlineDocs/src/data/ABCD2.txt delete mode 100644 doc/OnlineDocs/src/data/ABCD3.dat delete mode 100644 doc/OnlineDocs/src/data/ABCD3.py delete mode 100644 doc/OnlineDocs/src/data/ABCD3.txt delete mode 100644 doc/OnlineDocs/src/data/ABCD4.dat delete mode 100644 doc/OnlineDocs/src/data/ABCD4.py delete mode 100644 doc/OnlineDocs/src/data/ABCD4.txt delete mode 100644 doc/OnlineDocs/src/data/ABCD5.dat delete mode 100644 doc/OnlineDocs/src/data/ABCD5.py delete mode 100644 doc/OnlineDocs/src/data/ABCD5.txt delete mode 100644 doc/OnlineDocs/src/data/ABCD6.dat delete mode 100644 doc/OnlineDocs/src/data/ABCD6.py delete mode 100644 doc/OnlineDocs/src/data/ABCD6.txt delete mode 100644 doc/OnlineDocs/src/data/ABCD7.dat delete mode 100644 doc/OnlineDocs/src/data/ABCD7.py delete mode 100644 doc/OnlineDocs/src/data/ABCD7.txt delete mode 100644 doc/OnlineDocs/src/data/ABCD8.bad delete mode 100644 doc/OnlineDocs/src/data/ABCD8.dat delete mode 100644 doc/OnlineDocs/src/data/ABCD8.py delete mode 100644 doc/OnlineDocs/src/data/ABCD9.bad delete mode 100644 doc/OnlineDocs/src/data/ABCD9.dat delete mode 100644 doc/OnlineDocs/src/data/ABCD9.py delete mode 100644 doc/OnlineDocs/src/data/C.tab delete mode 100644 doc/OnlineDocs/src/data/D.tab delete mode 100644 doc/OnlineDocs/src/data/U.tab delete mode 100644 doc/OnlineDocs/src/data/Y.tab delete mode 100644 doc/OnlineDocs/src/data/Z.tab delete mode 100644 doc/OnlineDocs/src/data/data_managers.txt delete mode 100644 doc/OnlineDocs/src/data/diet.dat delete mode 100644 doc/OnlineDocs/src/data/diet.sql delete mode 100644 doc/OnlineDocs/src/data/diet.sqlite delete mode 100644 doc/OnlineDocs/src/data/diet.sqlite.dat delete mode 100644 doc/OnlineDocs/src/data/diet1.py delete mode 100644 doc/OnlineDocs/src/data/ex.dat delete mode 100644 doc/OnlineDocs/src/data/ex.py delete mode 100644 doc/OnlineDocs/src/data/ex.txt delete mode 100644 doc/OnlineDocs/src/data/ex1.dat delete mode 100644 doc/OnlineDocs/src/data/ex2.dat delete mode 100644 doc/OnlineDocs/src/data/import1.tab.dat delete mode 100644 doc/OnlineDocs/src/data/import1.tab.py delete mode 100644 doc/OnlineDocs/src/data/import1.tab.txt delete mode 100644 doc/OnlineDocs/src/data/import2.tab.dat delete mode 100644 doc/OnlineDocs/src/data/import2.tab.py delete mode 100644 doc/OnlineDocs/src/data/import2.tab.txt delete mode 100644 doc/OnlineDocs/src/data/import3.tab.dat delete mode 100644 doc/OnlineDocs/src/data/import3.tab.py delete mode 100644 doc/OnlineDocs/src/data/import3.tab.txt delete mode 100644 doc/OnlineDocs/src/data/import4.tab.dat delete mode 100644 doc/OnlineDocs/src/data/import4.tab.py delete mode 100644 doc/OnlineDocs/src/data/import4.tab.txt delete mode 100644 doc/OnlineDocs/src/data/import5.tab.dat delete mode 100644 doc/OnlineDocs/src/data/import5.tab.py delete mode 100644 doc/OnlineDocs/src/data/import5.tab.txt delete mode 100644 doc/OnlineDocs/src/data/import6.tab.dat delete mode 100644 doc/OnlineDocs/src/data/import6.tab.py delete mode 100644 doc/OnlineDocs/src/data/import6.tab.txt delete mode 100644 doc/OnlineDocs/src/data/import7.tab.dat delete mode 100644 doc/OnlineDocs/src/data/import7.tab.py delete mode 100644 doc/OnlineDocs/src/data/import7.tab.txt delete mode 100644 doc/OnlineDocs/src/data/import8.tab.dat delete mode 100644 doc/OnlineDocs/src/data/import8.tab.py delete mode 100644 doc/OnlineDocs/src/data/import8.tab.txt delete mode 100644 doc/OnlineDocs/src/data/namespace1.dat delete mode 100644 doc/OnlineDocs/src/data/param1.dat delete mode 100644 doc/OnlineDocs/src/data/param1.py delete mode 100644 doc/OnlineDocs/src/data/param1.txt delete mode 100644 doc/OnlineDocs/src/data/param2.dat delete mode 100644 doc/OnlineDocs/src/data/param2.py delete mode 100644 doc/OnlineDocs/src/data/param2.txt delete mode 100644 doc/OnlineDocs/src/data/param2a.dat delete mode 100644 doc/OnlineDocs/src/data/param2a.py delete mode 100644 doc/OnlineDocs/src/data/param2a.txt delete mode 100644 doc/OnlineDocs/src/data/param3.dat delete mode 100644 doc/OnlineDocs/src/data/param3.py delete mode 100644 doc/OnlineDocs/src/data/param3.txt delete mode 100644 doc/OnlineDocs/src/data/param3a.dat delete mode 100644 doc/OnlineDocs/src/data/param3a.py delete mode 100644 doc/OnlineDocs/src/data/param3a.txt delete mode 100644 doc/OnlineDocs/src/data/param3b.dat delete mode 100644 doc/OnlineDocs/src/data/param3b.py delete mode 100644 doc/OnlineDocs/src/data/param3b.txt delete mode 100644 doc/OnlineDocs/src/data/param3c.dat delete mode 100644 doc/OnlineDocs/src/data/param3c.py delete mode 100644 doc/OnlineDocs/src/data/param3c.txt delete mode 100644 doc/OnlineDocs/src/data/param4.dat delete mode 100644 doc/OnlineDocs/src/data/param4.py delete mode 100644 doc/OnlineDocs/src/data/param4.txt delete mode 100644 doc/OnlineDocs/src/data/param5.dat delete mode 100644 doc/OnlineDocs/src/data/param5.py delete mode 100644 doc/OnlineDocs/src/data/param5.txt delete mode 100644 doc/OnlineDocs/src/data/param5a.dat delete mode 100644 doc/OnlineDocs/src/data/param5a.py delete mode 100644 doc/OnlineDocs/src/data/param5a.txt delete mode 100644 doc/OnlineDocs/src/data/param6.dat delete mode 100644 doc/OnlineDocs/src/data/param6.py delete mode 100644 doc/OnlineDocs/src/data/param6.txt delete mode 100644 doc/OnlineDocs/src/data/param6a.dat delete mode 100644 doc/OnlineDocs/src/data/param6a.py delete mode 100644 doc/OnlineDocs/src/data/param6a.txt delete mode 100644 doc/OnlineDocs/src/data/param7a.dat delete mode 100644 doc/OnlineDocs/src/data/param7a.py delete mode 100644 doc/OnlineDocs/src/data/param7a.txt delete mode 100644 doc/OnlineDocs/src/data/param7b.dat delete mode 100644 doc/OnlineDocs/src/data/param7b.py delete mode 100644 doc/OnlineDocs/src/data/param7b.txt delete mode 100644 doc/OnlineDocs/src/data/param8a.dat delete mode 100644 doc/OnlineDocs/src/data/param8a.py delete mode 100644 doc/OnlineDocs/src/data/param8a.txt delete mode 100755 doc/OnlineDocs/src/data/pyomo.diet1.sh delete mode 100644 doc/OnlineDocs/src/data/pyomo.diet1.txt delete mode 100755 doc/OnlineDocs/src/data/pyomo.diet2.sh delete mode 100644 doc/OnlineDocs/src/data/pyomo.diet2.txt delete mode 100644 doc/OnlineDocs/src/data/set1.dat delete mode 100644 doc/OnlineDocs/src/data/set1.py delete mode 100644 doc/OnlineDocs/src/data/set1.txt delete mode 100644 doc/OnlineDocs/src/data/set2.dat delete mode 100644 doc/OnlineDocs/src/data/set2.py delete mode 100644 doc/OnlineDocs/src/data/set2.txt delete mode 100644 doc/OnlineDocs/src/data/set2a.dat delete mode 100644 doc/OnlineDocs/src/data/set2a.py delete mode 100644 doc/OnlineDocs/src/data/set2a.txt delete mode 100644 doc/OnlineDocs/src/data/set3.dat delete mode 100644 doc/OnlineDocs/src/data/set3.py delete mode 100644 doc/OnlineDocs/src/data/set3.txt delete mode 100644 doc/OnlineDocs/src/data/set4.dat delete mode 100644 doc/OnlineDocs/src/data/set4.py delete mode 100644 doc/OnlineDocs/src/data/set4.txt delete mode 100644 doc/OnlineDocs/src/data/set5.dat delete mode 100644 doc/OnlineDocs/src/data/set5.py delete mode 100644 doc/OnlineDocs/src/data/set5.txt delete mode 100644 doc/OnlineDocs/src/data/table0.dat delete mode 100644 doc/OnlineDocs/src/data/table0.py delete mode 100644 doc/OnlineDocs/src/data/table0.txt delete mode 100644 doc/OnlineDocs/src/data/table0.ul.dat delete mode 100644 doc/OnlineDocs/src/data/table0.ul.py delete mode 100644 doc/OnlineDocs/src/data/table0.ul.txt delete mode 100644 doc/OnlineDocs/src/data/table1.dat delete mode 100644 doc/OnlineDocs/src/data/table1.py delete mode 100644 doc/OnlineDocs/src/data/table1.txt delete mode 100644 doc/OnlineDocs/src/data/table2.dat delete mode 100644 doc/OnlineDocs/src/data/table2.py delete mode 100644 doc/OnlineDocs/src/data/table2.txt delete mode 100644 doc/OnlineDocs/src/data/table3.dat delete mode 100644 doc/OnlineDocs/src/data/table3.py delete mode 100644 doc/OnlineDocs/src/data/table3.txt delete mode 100644 doc/OnlineDocs/src/data/table3.ul.dat delete mode 100644 doc/OnlineDocs/src/data/table3.ul.py delete mode 100644 doc/OnlineDocs/src/data/table3.ul.txt delete mode 100644 doc/OnlineDocs/src/data/table4.dat delete mode 100644 doc/OnlineDocs/src/data/table4.py delete mode 100644 doc/OnlineDocs/src/data/table4.txt delete mode 100644 doc/OnlineDocs/src/data/table4.ul.dat delete mode 100644 doc/OnlineDocs/src/data/table4.ul.py delete mode 100644 doc/OnlineDocs/src/data/table4.ul.txt delete mode 100644 doc/OnlineDocs/src/data/table5.dat delete mode 100644 doc/OnlineDocs/src/data/table5.py delete mode 100644 doc/OnlineDocs/src/data/table5.txt delete mode 100644 doc/OnlineDocs/src/data/table6.dat delete mode 100644 doc/OnlineDocs/src/data/table6.py delete mode 100644 doc/OnlineDocs/src/data/table6.txt delete mode 100644 doc/OnlineDocs/src/data/table7.dat delete mode 100644 doc/OnlineDocs/src/data/table7.py delete mode 100644 doc/OnlineDocs/src/data/table7.txt delete mode 100644 doc/OnlineDocs/src/dataportal/A.tab delete mode 100644 doc/OnlineDocs/src/dataportal/C.tab delete mode 100644 doc/OnlineDocs/src/dataportal/D.tab delete mode 100644 doc/OnlineDocs/src/dataportal/PP.csv delete mode 100644 doc/OnlineDocs/src/dataportal/PP.json delete mode 100644 doc/OnlineDocs/src/dataportal/PP.sqlite delete mode 100644 doc/OnlineDocs/src/dataportal/PP.tab delete mode 100644 doc/OnlineDocs/src/dataportal/PP.xml delete mode 100644 doc/OnlineDocs/src/dataportal/PP.yaml delete mode 100644 doc/OnlineDocs/src/dataportal/PP_sqlite.py delete mode 100644 doc/OnlineDocs/src/dataportal/Pyomo_mysql delete mode 100644 doc/OnlineDocs/src/dataportal/S.tab delete mode 100644 doc/OnlineDocs/src/dataportal/T.json delete mode 100644 doc/OnlineDocs/src/dataportal/T.yaml delete mode 100644 doc/OnlineDocs/src/dataportal/U.tab delete mode 100644 doc/OnlineDocs/src/dataportal/XW.tab delete mode 100644 doc/OnlineDocs/src/dataportal/Y.tab delete mode 100644 doc/OnlineDocs/src/dataportal/Z.tab delete mode 100644 doc/OnlineDocs/src/dataportal/dataportal_tab.py delete mode 100644 doc/OnlineDocs/src/dataportal/dataportal_tab.txt delete mode 100644 doc/OnlineDocs/src/dataportal/excel.xls delete mode 100644 doc/OnlineDocs/src/dataportal/param_initialization.py delete mode 100644 doc/OnlineDocs/src/dataportal/param_initialization.txt delete mode 100644 doc/OnlineDocs/src/dataportal/set_initialization.py delete mode 100644 doc/OnlineDocs/src/dataportal/set_initialization.txt delete mode 100644 doc/OnlineDocs/src/expr/design.py delete mode 100644 doc/OnlineDocs/src/expr/design.txt delete mode 100644 doc/OnlineDocs/src/expr/index.py delete mode 100644 doc/OnlineDocs/src/expr/index.txt delete mode 100644 doc/OnlineDocs/src/expr/managing.py delete mode 100644 doc/OnlineDocs/src/expr/managing.txt delete mode 100644 doc/OnlineDocs/src/expr/overview.py delete mode 100644 doc/OnlineDocs/src/expr/overview.txt delete mode 100644 doc/OnlineDocs/src/expr/performance.py delete mode 100644 doc/OnlineDocs/src/expr/performance.txt delete mode 100644 doc/OnlineDocs/src/expr/quicksum.log delete mode 100644 doc/OnlineDocs/src/expr/quicksum.py delete mode 100755 doc/OnlineDocs/src/kernel/examples.sh delete mode 100644 doc/OnlineDocs/src/kernel/examples.txt delete mode 100644 doc/OnlineDocs/src/scripting/AbstractSuffixes.py delete mode 100644 doc/OnlineDocs/src/scripting/Isinglebuild.py delete mode 100644 doc/OnlineDocs/src/scripting/Isinglecomm.dat delete mode 100644 doc/OnlineDocs/src/scripting/NodesIn_init.py delete mode 100644 doc/OnlineDocs/src/scripting/Z_init.py delete mode 100644 doc/OnlineDocs/src/scripting/abstract1.dat delete mode 100644 doc/OnlineDocs/src/scripting/abstract2.dat delete mode 100644 doc/OnlineDocs/src/scripting/abstract2.py delete mode 100644 doc/OnlineDocs/src/scripting/abstract2a.dat delete mode 100644 doc/OnlineDocs/src/scripting/abstract2piece.py delete mode 100644 doc/OnlineDocs/src/scripting/abstract2piecebuild.py delete mode 100644 doc/OnlineDocs/src/scripting/block_iter_example.py delete mode 100644 doc/OnlineDocs/src/scripting/concrete1.py delete mode 100644 doc/OnlineDocs/src/scripting/doubleA.py delete mode 100644 doc/OnlineDocs/src/scripting/driveabs2.py delete mode 100644 doc/OnlineDocs/src/scripting/driveconc1.py delete mode 100644 doc/OnlineDocs/src/scripting/iterative1.py delete mode 100644 doc/OnlineDocs/src/scripting/iterative2.py delete mode 100644 doc/OnlineDocs/src/scripting/noiteration1.py delete mode 100644 doc/OnlineDocs/src/scripting/parallel.py delete mode 100644 doc/OnlineDocs/src/scripting/spy4Constraints.py delete mode 100644 doc/OnlineDocs/src/scripting/spy4Expressions.py delete mode 100644 doc/OnlineDocs/src/scripting/spy4PyomoCommand.py delete mode 100644 doc/OnlineDocs/src/scripting/spy4Variables.py delete mode 100644 doc/OnlineDocs/src/scripting/spy4scripts.py delete mode 100644 doc/OnlineDocs/src/strip_examples.py delete mode 100644 doc/OnlineDocs/src/test_examples.py diff --git a/doc/OnlineDocs/src/data/A.tab b/doc/OnlineDocs/src/data/A.tab deleted file mode 100644 index d9c13cd6ac6..00000000000 --- a/doc/OnlineDocs/src/data/A.tab +++ /dev/null @@ -1,4 +0,0 @@ -A -A1 -A2 -A3 diff --git a/doc/OnlineDocs/src/data/ABCD.tab b/doc/OnlineDocs/src/data/ABCD.tab deleted file mode 100644 index d820d78eade..00000000000 --- a/doc/OnlineDocs/src/data/ABCD.tab +++ /dev/null @@ -1,4 +0,0 @@ -A B C D -A1 B1 1 10 -A2 B2 2 20 -A3 B3 3 30 diff --git a/doc/OnlineDocs/src/data/ABCD.txt b/doc/OnlineDocs/src/data/ABCD.txt deleted file mode 100644 index ccaf3bff1c1..00000000000 --- a/doc/OnlineDocs/src/data/ABCD.txt +++ /dev/null @@ -1,4 +0,0 @@ -A,B,C,D -A1,B1,1,10 -A2,B2,2,20 -A3,B3,3,30 diff --git a/doc/OnlineDocs/src/data/ABCD.xls b/doc/OnlineDocs/src/data/ABCD.xls deleted file mode 100755 index 98d9673b5c5b0ff8518987c6f5e7fb61d9a528cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 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 diff --git a/doc/OnlineDocs/src/data/ABCD1.dat b/doc/OnlineDocs/src/data/ABCD1.dat deleted file mode 100644 index 695a16dd4c3..00000000000 --- a/doc/OnlineDocs/src/data/ABCD1.dat +++ /dev/null @@ -1 +0,0 @@ -load ABCD.tab format=set : Z ; diff --git a/doc/OnlineDocs/src/data/ABCD1.py b/doc/OnlineDocs/src/data/ABCD1.py deleted file mode 100644 index aa2f46e71fa..00000000000 --- a/doc/OnlineDocs/src/data/ABCD1.py +++ /dev/null @@ -1,20 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.Z = Set(dimen=4) - -instance = model.create_instance('ABCD1.dat') - -print(sorted(list(instance.Z.data()))) diff --git a/doc/OnlineDocs/src/data/ABCD1.txt b/doc/OnlineDocs/src/data/ABCD1.txt deleted file mode 100644 index 6a34f8295a7..00000000000 --- a/doc/OnlineDocs/src/data/ABCD1.txt +++ /dev/null @@ -1 +0,0 @@ -[('A1', 'B1', 1, 10), ('A2', 'B2', 2, 20), ('A3', 'B3', 3, 30)] diff --git a/doc/OnlineDocs/src/data/ABCD2.dat b/doc/OnlineDocs/src/data/ABCD2.dat deleted file mode 100644 index c7d85665d4b..00000000000 --- a/doc/OnlineDocs/src/data/ABCD2.dat +++ /dev/null @@ -1 +0,0 @@ -load ABCD.tab : [A,B,C] D ; diff --git a/doc/OnlineDocs/src/data/ABCD2.py b/doc/OnlineDocs/src/data/ABCD2.py deleted file mode 100644 index ec0e7ccb15c..00000000000 --- a/doc/OnlineDocs/src/data/ABCD2.py +++ /dev/null @@ -1,25 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.Z = Set(initialize=[('A1', 'B1', 1), ('A2', 'B2', 2), ('A3', 'B3', 3)]) -# model.Z = Set(dimen=3) -model.D = Param(model.Z) - -instance = model.create_instance('ABCD2.dat') - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('D') -for key in sorted(instance.D.keys()): - print(name(instance.D, key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/ABCD2.txt b/doc/OnlineDocs/src/data/ABCD2.txt deleted file mode 100644 index cb7abaec039..00000000000 --- a/doc/OnlineDocs/src/data/ABCD2.txt +++ /dev/null @@ -1,5 +0,0 @@ -Z [('A1', 'B1', 1), ('A2', 'B2', 2), ('A3', 'B3', 3)] -D -D[A1,B1,1] 10 -D[A2,B2,2] 20 -D[A3,B3,3] 30 diff --git a/doc/OnlineDocs/src/data/ABCD3.dat b/doc/OnlineDocs/src/data/ABCD3.dat deleted file mode 100644 index c48c82133ce..00000000000 --- a/doc/OnlineDocs/src/data/ABCD3.dat +++ /dev/null @@ -1 +0,0 @@ -load ABCD.tab : Z=[A,B,C] D ; diff --git a/doc/OnlineDocs/src/data/ABCD3.py b/doc/OnlineDocs/src/data/ABCD3.py deleted file mode 100644 index ba55fd970cc..00000000000 --- a/doc/OnlineDocs/src/data/ABCD3.py +++ /dev/null @@ -1,24 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.Z = Set(dimen=3) -model.D = Param(model.Z) - -instance = model.create_instance('ABCD3.dat') - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('D') -for key in sorted(instance.D.keys()): - print(name(instance.D, key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/ABCD3.txt b/doc/OnlineDocs/src/data/ABCD3.txt deleted file mode 100644 index cb7abaec039..00000000000 --- a/doc/OnlineDocs/src/data/ABCD3.txt +++ /dev/null @@ -1,5 +0,0 @@ -Z [('A1', 'B1', 1), ('A2', 'B2', 2), ('A3', 'B3', 3)] -D -D[A1,B1,1] 10 -D[A2,B2,2] 20 -D[A3,B3,3] 30 diff --git a/doc/OnlineDocs/src/data/ABCD4.dat b/doc/OnlineDocs/src/data/ABCD4.dat deleted file mode 100644 index b6a4f963ab7..00000000000 --- a/doc/OnlineDocs/src/data/ABCD4.dat +++ /dev/null @@ -1 +0,0 @@ -load ABCD.tab : Z=[A,B,C] Y=D ; diff --git a/doc/OnlineDocs/src/data/ABCD4.py b/doc/OnlineDocs/src/data/ABCD4.py deleted file mode 100644 index 2fb397aa3b0..00000000000 --- a/doc/OnlineDocs/src/data/ABCD4.py +++ /dev/null @@ -1,24 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.Z = Set(dimen=3) -model.Y = Param(model.Z) - -instance = model.create_instance('ABCD4.dat') - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('Y') -for key in sorted(instance.Y.keys()): - print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/src/data/ABCD4.txt b/doc/OnlineDocs/src/data/ABCD4.txt deleted file mode 100644 index f316499bca7..00000000000 --- a/doc/OnlineDocs/src/data/ABCD4.txt +++ /dev/null @@ -1,5 +0,0 @@ -Z [('A1', 'B1', 1), ('A2', 'B2', 2), ('A3', 'B3', 3)] -Y -Y[A1,B1,1] 10 -Y[A2,B2,2] 20 -Y[A3,B3,3] 30 diff --git a/doc/OnlineDocs/src/data/ABCD5.dat b/doc/OnlineDocs/src/data/ABCD5.dat deleted file mode 100644 index df2a1d82afb..00000000000 --- a/doc/OnlineDocs/src/data/ABCD5.dat +++ /dev/null @@ -1 +0,0 @@ -load ABCD.tab : Z=[B] Y=D W=C; diff --git a/doc/OnlineDocs/src/data/ABCD5.py b/doc/OnlineDocs/src/data/ABCD5.py deleted file mode 100644 index abc03505e96..00000000000 --- a/doc/OnlineDocs/src/data/ABCD5.py +++ /dev/null @@ -1,30 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.Z = Set() -model.Y = Param(model.Z) -model.W = Param(model.Z) -# @decl - -instance = model.create_instance('ABCD5.dat') - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('Y') -for key in sorted(instance.Y.keys()): - print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) -print('W') -for key in sorted(instance.W.keys()): - print(name(instance.W, key) + " " + str(value(instance.W[key]))) diff --git a/doc/OnlineDocs/src/data/ABCD5.txt b/doc/OnlineDocs/src/data/ABCD5.txt deleted file mode 100644 index 18ae0f1c28b..00000000000 --- a/doc/OnlineDocs/src/data/ABCD5.txt +++ /dev/null @@ -1,9 +0,0 @@ -Z ['B1', 'B2', 'B3'] -Y -Y[B1] 10 -Y[B2] 20 -Y[B3] 30 -W -W[B1] 1 -W[B2] 2 -W[B3] 3 diff --git a/doc/OnlineDocs/src/data/ABCD6.dat b/doc/OnlineDocs/src/data/ABCD6.dat deleted file mode 100644 index 12ca0553325..00000000000 --- a/doc/OnlineDocs/src/data/ABCD6.dat +++ /dev/null @@ -1 +0,0 @@ -load ABCD.txt using=csv : Z=[A,B,C] D ; diff --git a/doc/OnlineDocs/src/data/ABCD6.py b/doc/OnlineDocs/src/data/ABCD6.py deleted file mode 100644 index 59e0e8e98ae..00000000000 --- a/doc/OnlineDocs/src/data/ABCD6.py +++ /dev/null @@ -1,24 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.Z = Set(dimen=3) -model.D = Param(model.Z) - -instance = model.create_instance('ABCD6.dat') - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('D') -for key in sorted(instance.D.keys()): - print(name(instance.D, key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/ABCD6.txt b/doc/OnlineDocs/src/data/ABCD6.txt deleted file mode 100644 index cb7abaec039..00000000000 --- a/doc/OnlineDocs/src/data/ABCD6.txt +++ /dev/null @@ -1,5 +0,0 @@ -Z [('A1', 'B1', 1), ('A2', 'B2', 2), ('A3', 'B3', 3)] -D -D[A1,B1,1] 10 -D[A2,B2,2] 20 -D[A3,B3,3] 30 diff --git a/doc/OnlineDocs/src/data/ABCD7.dat b/doc/OnlineDocs/src/data/ABCD7.dat deleted file mode 100644 index f99b24c575f..00000000000 --- a/doc/OnlineDocs/src/data/ABCD7.dat +++ /dev/null @@ -1 +0,0 @@ -load ABCD.xls range=ABCD : Z=[A,B,C] Y=D ; diff --git a/doc/OnlineDocs/src/data/ABCD7.py b/doc/OnlineDocs/src/data/ABCD7.py deleted file mode 100644 index 1bfb4d1e3fb..00000000000 --- a/doc/OnlineDocs/src/data/ABCD7.py +++ /dev/null @@ -1,30 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * -import pyomo.common -import sys - -model = AbstractModel() - -model.Z = Set(dimen=3) -model.Y = Param(model.Z) - -try: - instance = model.create_instance('ABCD7.dat') -except pyomo.common.errors.ApplicationError as e: - print("ERROR " + str(e)) - sys.exit(1) - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('Y') -for key in sorted(instance.Y.keys()): - print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/src/data/ABCD7.txt b/doc/OnlineDocs/src/data/ABCD7.txt deleted file mode 100644 index aefb9bc167a..00000000000 --- a/doc/OnlineDocs/src/data/ABCD7.txt +++ /dev/null @@ -1,5 +0,0 @@ -Z [('A1', 'B1', 1.0), ('A2', 'B2', 2.0), ('A3', 'B3', 3.0)] -Y -Y[A1,B1,1.0] 10.0 -Y[A2,B2,2.0] 20.0 -Y[A3,B3,3.0] 30.0 diff --git a/doc/OnlineDocs/src/data/ABCD8.bad b/doc/OnlineDocs/src/data/ABCD8.bad deleted file mode 100644 index 532fb841f32..00000000000 --- a/doc/OnlineDocs/src/data/ABCD8.bad +++ /dev/null @@ -1 +0,0 @@ -ERROR Cannot create data manager 'pyodbc' diff --git a/doc/OnlineDocs/src/data/ABCD8.dat b/doc/OnlineDocs/src/data/ABCD8.dat deleted file mode 100644 index 220a67e5602..00000000000 --- a/doc/OnlineDocs/src/data/ABCD8.dat +++ /dev/null @@ -1 +0,0 @@ -load ABCD.xls using=pyodbc table=ABCD : Z=[A,B,C] Y=D ; diff --git a/doc/OnlineDocs/src/data/ABCD8.py b/doc/OnlineDocs/src/data/ABCD8.py deleted file mode 100644 index aa1ba0b4cf5..00000000000 --- a/doc/OnlineDocs/src/data/ABCD8.py +++ /dev/null @@ -1,30 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * -import pyomo.common -import sys - -model = AbstractModel() - -model.Z = Set(dimen=3) -model.Y = Param(model.Z) - -try: - instance = model.create_instance('ABCD8.dat') -except pyomo.common.errors.ApplicationError as e: - print("ERROR " + str(e)) - sys.exit(1) - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('Y') -for key in sorted(instance.Y.keys()): - print(name(instance.Y, key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/src/data/ABCD9.bad b/doc/OnlineDocs/src/data/ABCD9.bad deleted file mode 100644 index 532fb841f32..00000000000 --- a/doc/OnlineDocs/src/data/ABCD9.bad +++ /dev/null @@ -1 +0,0 @@ -ERROR Cannot create data manager 'pyodbc' diff --git a/doc/OnlineDocs/src/data/ABCD9.dat b/doc/OnlineDocs/src/data/ABCD9.dat deleted file mode 100644 index 2c3688c6200..00000000000 --- a/doc/OnlineDocs/src/data/ABCD9.dat +++ /dev/null @@ -1,3 +0,0 @@ -load "Driver={Microsoft Excel Driver (*.xls)}; Dbq=ABCD.xls;" - using=pyodbc - table=ABCD : Z=[A,B,C] Y=D ; diff --git a/doc/OnlineDocs/src/data/ABCD9.py b/doc/OnlineDocs/src/data/ABCD9.py deleted file mode 100644 index 194c71486d9..00000000000 --- a/doc/OnlineDocs/src/data/ABCD9.py +++ /dev/null @@ -1,30 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * -import pyomo.common -import sys - -model = AbstractModel() - -model.Z = Set(dimen=3) -model.Y = Param(model.Z) - -try: - instance = model.create_instance('ABCD9.dat') -except pyomo.common.errors.ApplicationError as e: - print("ERROR " + str(e)) - sys.exit(1) - -print('Z ' + str(sorted(list(instance.Z.data())))) -print('Y') -for key in sorted(instance.Y.keys()): - print(instance.Y[key] + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/src/data/C.tab b/doc/OnlineDocs/src/data/C.tab deleted file mode 100644 index 14bcdc5e18d..00000000000 --- a/doc/OnlineDocs/src/data/C.tab +++ /dev/null @@ -1,10 +0,0 @@ -A B -A1 1 -A1 2 -A1 3 -A2 1 -A2 2 -A2 3 -A3 1 -A3 2 -A3 3 diff --git a/doc/OnlineDocs/src/data/D.tab b/doc/OnlineDocs/src/data/D.tab deleted file mode 100644 index 965d28df3d0..00000000000 --- a/doc/OnlineDocs/src/data/D.tab +++ /dev/null @@ -1,4 +0,0 @@ -B A1 A2 A3 -1 + - - -2 - + - -3 - - + diff --git a/doc/OnlineDocs/src/data/U.tab b/doc/OnlineDocs/src/data/U.tab deleted file mode 100644 index 27bba1b34da..00000000000 --- a/doc/OnlineDocs/src/data/U.tab +++ /dev/null @@ -1,5 +0,0 @@ -I A1 A2 A3 -I1 1.3 2.3 3.3 -I2 1.4 2.4 3.4 -I3 1.5 2.5 3.5 -I4 1.6 2.6 3.6 diff --git a/doc/OnlineDocs/src/data/Y.tab b/doc/OnlineDocs/src/data/Y.tab deleted file mode 100644 index 926555713f0..00000000000 --- a/doc/OnlineDocs/src/data/Y.tab +++ /dev/null @@ -1,4 +0,0 @@ -A Y -A1 3.3 -A2 3.4 -A3 3.5 diff --git a/doc/OnlineDocs/src/data/Z.tab b/doc/OnlineDocs/src/data/Z.tab deleted file mode 100644 index 9459d4ba2a0..00000000000 --- a/doc/OnlineDocs/src/data/Z.tab +++ /dev/null @@ -1 +0,0 @@ -1.1 diff --git a/doc/OnlineDocs/src/data/data_managers.txt b/doc/OnlineDocs/src/data/data_managers.txt deleted file mode 100644 index dd508dd97b5..00000000000 --- a/doc/OnlineDocs/src/data/data_managers.txt +++ /dev/null @@ -1,30 +0,0 @@ -Pyomo Data Managers -------------------- - csv - CSV file interface - dat - Pyomo data command file interface - json - JSON file interface - pymysql - pymysql database interface - pyodbc - pyodbc database interface - pypyodbc - pypyodbc database interface - sqlite3 - sqlite3 database interface - tab - TAB file interface - xls - Excel XLS file interface - xlsb - Excel XLSB file interface - xlsm - Excel XLSM file interface - xlsx - Excel XLSX file interface - xml - XML file interface - yaml - YAML file interface diff --git a/doc/OnlineDocs/src/data/diet.dat b/doc/OnlineDocs/src/data/diet.dat deleted file mode 100644 index 6d50961842c..00000000000 --- a/doc/OnlineDocs/src/data/diet.dat +++ /dev/null @@ -1,33 +0,0 @@ -# File diet.dat - -param: FOOD: cost f_min f_max := - "Cheeseburger" 1.84 . . - "Ham Sandwich" 2.19 . . - "Hamburger" 1.84 . . - "Fish Sandwich" 1.44 . . - "Chicken Sandwich" 2.29 . . - "Fries" .77 . . - "Sausage Biscuit" 1.29 . . - "Lowfat Milk" .60 . . - "Orange Juice" .72 . . ; - -param: NUTR: n_min n_max := - Cal 2000 . - Carbo 350 375 - Protein 55 . - VitA 100 . - VitC 100 . - Calc 100 . - Iron 100 . ; - -param amt (tr): - Cal Carbo Protein VitA VitC Calc Iron := - "Cheeseburger" 510 34 28 15 6 30 20 - "Ham Sandwich" 370 35 24 15 10 20 20 - "Hamburger" 500 42 25 6 2 25 20 - "Fish Sandwich" 370 38 14 2 0 15 10 - "Chicken Sandwich" 400 42 31 8 15 15 8 - "Fries" 220 26 3 0 15 0 2 - "Sausage Biscuit" 345 27 15 4 0 20 15 - "Lowfat Milk" 110 12 9 10 4 30 0 - "Orange Juice" 80 20 1 2 120 2 2 ; diff --git a/doc/OnlineDocs/src/data/diet.sql b/doc/OnlineDocs/src/data/diet.sql deleted file mode 100644 index b97403df819..00000000000 --- a/doc/OnlineDocs/src/data/diet.sql +++ /dev/null @@ -1,96 +0,0 @@ -DROP TABLE IF EXISTS Amount; -DROP TABLE IF EXISTS Nutr; -DROP TABLE IF EXISTS Food; - -CREATE TABLE Food ( - FOOD varchar(64) not null, - cost float not null, - f_min float, - f_max float, - primary key (FOOD) - ) engine=innodb ; - -INSERT INTO Food VALUES ("Cheeseburger", 1.84, NULL, NULL), ("Ham Sandwich", 2.19, NULL, NULL), ("Hamburger", 1.84, NULL, NULL), ("Fish Sandwich", 1.44, NULL, NULL), ("Chicken Sandwich", 2.29, NULL, NULL), ("Fries", 0.77, NULL, NULL), ("Sausage Biscuit", 1.29, NULL, NULL), ("Lowfat Milk", 0.60, NULL, NULL), ("Orange Juice", 0.72, NULL, NULL); - -CREATE TABLE Nutr ( - NUTR varchar(64) not null, - n_min float, - n_max float, - primary key (NUTR) - ) engine=innodb ; - -INSERT INTO Nutr VALUES ("Cal", 2000.0, NULL), ("Carbo", 350.0, 375.0), ("Protein", 55.0, NULL), ("VitA", 100.0, NULL), ("VitC", 100.0, NULL), ("Calc", 100.0, NULL), ("Iron", 100.0, NULL); - -CREATE TABLE Amount ( - NUTR varchar(64) not null, - FOOD varchar(64) not null, - amt float not null, - primary key (NUTR, FOOD), - foreign key (NUTR) references Nutr (NUTR), - foreign key (FOOD) references Food (FOOD) - ) engine=innodb ; - -INSERT INTO Amount VALUES - ('Cal','Cheeseburger','510'), - ('Carbo','Cheeseburger','34'), - ('Protein','Cheeseburger','28'), - ('VitA','Cheeseburger','15'), - ('VitC','Cheeseburger','6'), - ('Calc','Cheeseburger','30'), - ('Iron','Cheeseburger','20'), - ('Cal','Ham Sandwich','370'), - ('Carbo','Ham Sandwich','35'), - ('Protein','Ham Sandwich','24'), - ('VitA','Ham Sandwich','15'), - ('VitC','Ham Sandwich','10'), - ('Calc','Ham Sandwich','20'), - ('Iron','Ham Sandwich','20'), - ('Cal','Hamburger','500'), - ('Carbo','Hamburger','42'), - ('Protein','Hamburger','25'), - ('VitA','Hamburger','6'), - ('VitC','Hamburger','2'), - ('Calc','Hamburger','25'), - ('Iron','Hamburger','20'), - ('Cal','Fish Sandwich','370'), - ('Carbo','Fish Sandwich','38'), - ('Protein','Fish Sandwich','14'), - ('VitA','Fish Sandwich','2'), - ('VitC','Fish Sandwich','0'), - ('Calc','Fish Sandwich','15'), - ('Iron','Fish Sandwich','10'), - ('Cal','Chicken Sandwich','400'), - ('Carbo','Chicken Sandwich','42'), - ('Protein','Chicken Sandwich','31'), - ('VitA','Chicken Sandwich','8'), - ('VitC','Chicken Sandwich','15'), - ('Calc','Chicken Sandwich','15'), - ('Iron','Chicken Sandwich','8'), - ('Cal','Fries','220'), - ('Carbo','Fries','26'), - ('Protein','Fries','3'), - ('VitA','Fries','0'), - ('VitC','Fries','15'), - ('Calc','Fries','0'), - ('Iron','Fries','2'), - ('Cal','Sausage Biscuit','345'), - ('Carbo','Sausage Biscuit','27'), - ('Protein','Sausage Biscuit','15'), - ('VitA','Sausage Biscuit','4'), - ('VitC','Sausage Biscuit','0'), - ('Calc','Sausage Biscuit','20'), - ('Iron','Sausage Biscuit','15'), - ('Cal','Lowfat Milk','110'), - ('Carbo','Lowfat Milk','12'), - ('Protein','Lowfat Milk','9'), - ('VitA','Lowfat Milk','10'), - ('VitC','Lowfat Milk','4'), - ('Calc','Lowfat Milk','30'), - ('Iron','Lowfat Milk','0'), - ('Cal','Orange Juice','80'), - ('Carbo','Orange Juice','20'), - ('Protein','Orange Juice','1'), - ('VitA','Orange Juice','2'), - ('VitC','Orange Juice','120'), - ('Calc','Orange Juice','2'), - ('Iron','Orange Juice','2'); diff --git a/doc/OnlineDocs/src/data/diet.sqlite b/doc/OnlineDocs/src/data/diet.sqlite deleted file mode 100644 index 89e54586968b10483da7cad570ada0e5bd7513f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11264 zcmeHMZERat8NTPd#~;^e;wIPE*N*dX+bnU?B;C?(?fQ|quIti5mv(7ERG4#1fs|EXfxw4w8+PgKOF{T}|ZB5g{cz5C5 zxO99FjTii9%5%HNur_^Dbi+rQpIy`7AiTz(0E1oQ#daC)RQ&2WU%-mT*;lPwXQ=y|LPad%!$yM^p zxrzekR;%{;l`?klX3|sh#~)13pS3@eJ!_9sSyMLUo}fbJv&TO*w`rNM=d(w%^Vzvf z_Ov~>UaizJc$3QPYSN?S@{&C+;n2jS-*2VjOe>a_3YXU|t>CCz$gNk)@_nJ!s)d=_ zf8YuPEYoCH&q`-dm2?Z2BWgrKHeRhn!GR(nn()2b---L zl&W2rpl1%WKX3Lx%HS{s`QHf8Enbek=aO zXJLkZ0q2_N#vdKFYVa1k1kb<|umO3nu()zDbz`@@1_HiGw4BbZujMe;;o@3;y;x1XhnZ#{ z{z;$TXQB0IrC3-?{k4V-`h3%fWtNNii-nSXI#*iSDCU@!2yeox@MX9NkKJ)y-)V5i1OH18Y=u##>WUtQ5vHg~(11zi(U5=+7L&^1 zkH9qZ>V>ZtlFX$N7;FzSM;n0ZR26}TVN_RjfY$#2Jgvd|a1(wHFT)FP9bY?F#{(S? z+-4ro0|K`t$1CO1lJ2L+Ol~E=r2FXcbH!?ANf-6U^wJg(38tN@l&gheX>T_d7U!8< zWwFf8u??LGgH!InE42Ox;CT%`fEVE(7y;aXH{jLVtl=Fx9S?Lo(8>crFc}F#fF4K= z{PaL#;Hy8RMHeJkT?l}}2`K~I4~C8zd{`=j52@^~+Fz?`I#|a89S?l+JP;h`*eC{X zQPV=TzfWE(J1y#X;7<2|9u;O%h6=9Zf&tq9k82k+cpK0CuH*UNH=qJvfFm#kef&fI zXZ|vOmOshA#UJ5|e2(wuW4wpG&)#4!u%EN_KfkqCrGg;kGLB+%b;moO~c!>q_x?GN!0 zVNS~yc~3=x@$F=yaL8*4$G4G*0;Fb>T{oq@rr^M^NK7~Qd9B0!L&Bo!aChak?T8Nw zW13idE$s2=K+7%(MH7S*dskP5&3?qmXBihBjZ<>zO7*yy$73RqY;bnv=&>weQ4L7g zWf+S_g^`p#R*PblK2w;(($~(>Qlu|J7}0dFc%MNSQFJig*H_2V4(2y|MPj(Y&4J^_ zR$)<*$jbqD^o4~nOq?8uy=V3abF{(7Iqr^_-2~SuDexA8>#Q6&WOmhIBIdy1SV$yB z6>g3t7S)AC#nyhhwm*ftyNP>#Nm51Ge`xxbR3amUpZ3VYYK}%9I*z#yMXjg4RwET9HIHYjoBLC$B*%IxSreWxBL?SCjS}# zHGhr&jXlQ}`5I(nU6Q`rbPW6)Vmv7jyWge)$Ni%WkfJ$Lf1~=0?2d7#RJHmojp{d}GlSxPi@mMEKjDw?8+aPN4eRhl zI1J-p@Q?TnjQ_9mAMwX9{y)dFd?&ZKhRHkE|Ce55^MO`@2RCOM!%|Is z**sW>T@uHw1E_I&{ft@75Z&K0fQdx*wTNI&8TVrd6C{d?o<;Vyh--!-dk7^lQ<93v zeRWh7%8+$$Yo*l9-L0adEyk|q@Te4 model.f_min[j] - - -model.f_max = Param(model.FOOD, validate=f_max_validate, default=MAX_FOOD_SUPPLY) - -model.NUTR = Set() -model.n_min = Param(model.NUTR, within=NonNegativeReals, default=0.0) -model.n_max = Param(model.NUTR, default=infinity) -model.amt = Param(model.NUTR, model.FOOD, within=NonNegativeReals) - -# -------------------------------------------------------- - - -def Buy_bounds(model, i): - return (model.f_min[i], model.f_max[i]) - - -model.Buy = Var(model.FOOD, bounds=Buy_bounds, within=NonNegativeIntegers) - -# -------------------------------------------------------- - - -def Total_Cost_rule(model): - return sum(model.cost[j] * model.Buy[j] for j in model.FOOD) - - -model.Total_Cost = Objective(rule=Total_Cost_rule, sense=minimize) - -# -------------------------------------------------------- - - -def Entree_rule(model): - entrees = [ - 'Cheeseburger', - 'Ham Sandwich', - 'Hamburger', - 'Fish Sandwich', - 'Chicken Sandwich', - ] - return sum(model.Buy[e] for e in entrees) >= 1 - - -model.Entree = Constraint(rule=Entree_rule) - - -def Side_rule(model): - sides = ['Fries', 'Sausage Biscuit'] - return sum(model.Buy[s] for s in sides) >= 1 - - -model.Side = Constraint(rule=Side_rule) - - -def Drink_rule(model): - drinks = ['Lowfat Milk', 'Orange Juice'] - return sum(model.Buy[d] for d in drinks) >= 1 - - -model.Drink = Constraint(rule=Drink_rule) diff --git a/doc/OnlineDocs/src/data/ex.dat b/doc/OnlineDocs/src/data/ex.dat deleted file mode 100644 index 3b07e9348f0..00000000000 --- a/doc/OnlineDocs/src/data/ex.dat +++ /dev/null @@ -1,2 +0,0 @@ -include ex1.dat; -include ex2.dat; diff --git a/doc/OnlineDocs/src/data/ex.py b/doc/OnlineDocs/src/data/ex.py deleted file mode 100644 index a66ee30b494..00000000000 --- a/doc/OnlineDocs/src/data/ex.py +++ /dev/null @@ -1,22 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.z = Param() -# @decl - -instance = model.create_instance('ex.dat') - -print(value(instance.z)) diff --git a/doc/OnlineDocs/src/data/ex.txt b/doc/OnlineDocs/src/data/ex.txt deleted file mode 100644 index 0cfbf08886f..00000000000 --- a/doc/OnlineDocs/src/data/ex.txt +++ /dev/null @@ -1 +0,0 @@ -2 diff --git a/doc/OnlineDocs/src/data/ex1.dat b/doc/OnlineDocs/src/data/ex1.dat deleted file mode 100644 index 844349d86ad..00000000000 --- a/doc/OnlineDocs/src/data/ex1.dat +++ /dev/null @@ -1 +0,0 @@ -param z := 1; diff --git a/doc/OnlineDocs/src/data/ex2.dat b/doc/OnlineDocs/src/data/ex2.dat deleted file mode 100644 index 62002452618..00000000000 --- a/doc/OnlineDocs/src/data/ex2.dat +++ /dev/null @@ -1 +0,0 @@ -param z := 2; diff --git a/doc/OnlineDocs/src/data/import1.tab.dat b/doc/OnlineDocs/src/data/import1.tab.dat deleted file mode 100644 index fd42ce549fb..00000000000 --- a/doc/OnlineDocs/src/data/import1.tab.dat +++ /dev/null @@ -1 +0,0 @@ -load Y.tab : [A] Y; diff --git a/doc/OnlineDocs/src/data/import1.tab.py b/doc/OnlineDocs/src/data/import1.tab.py deleted file mode 100644 index e160e4fdcde..00000000000 --- a/doc/OnlineDocs/src/data/import1.tab.py +++ /dev/null @@ -1,24 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set(initialize=['A1', 'A2', 'A3', 'A4']) -model.Y = Param(model.A) - -instance = model.create_instance('import1.tab.dat') - -print('Y') -keys = instance.Y.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/src/data/import1.tab.txt b/doc/OnlineDocs/src/data/import1.tab.txt deleted file mode 100644 index 7b0d5e2c1c3..00000000000 --- a/doc/OnlineDocs/src/data/import1.tab.txt +++ /dev/null @@ -1,4 +0,0 @@ -Y -A1 3.3 -A2 3.4 -A3 3.5 diff --git a/doc/OnlineDocs/src/data/import2.tab.dat b/doc/OnlineDocs/src/data/import2.tab.dat deleted file mode 100644 index 23daab5ef75..00000000000 --- a/doc/OnlineDocs/src/data/import2.tab.dat +++ /dev/null @@ -1 +0,0 @@ -load Y.tab : A=[A] Y; diff --git a/doc/OnlineDocs/src/data/import2.tab.py b/doc/OnlineDocs/src/data/import2.tab.py deleted file mode 100644 index 54339551279..00000000000 --- a/doc/OnlineDocs/src/data/import2.tab.py +++ /dev/null @@ -1,25 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set() -model.Y = Param(model.A) - -instance = model.create_instance('import2.tab.dat') - -print('A ' + str(sorted(list(instance.A.data())))) -print('Y') -keys = instance.Y.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.Y[key]))) diff --git a/doc/OnlineDocs/src/data/import2.tab.txt b/doc/OnlineDocs/src/data/import2.tab.txt deleted file mode 100644 index 814b83c8c7b..00000000000 --- a/doc/OnlineDocs/src/data/import2.tab.txt +++ /dev/null @@ -1,5 +0,0 @@ -A ['A1', 'A2', 'A3'] -Y -A1 3.3 -A2 3.4 -A3 3.5 diff --git a/doc/OnlineDocs/src/data/import3.tab.dat b/doc/OnlineDocs/src/data/import3.tab.dat deleted file mode 100644 index 896e9478e83..00000000000 --- a/doc/OnlineDocs/src/data/import3.tab.dat +++ /dev/null @@ -1 +0,0 @@ -load A.tab format=set : A; diff --git a/doc/OnlineDocs/src/data/import3.tab.py b/doc/OnlineDocs/src/data/import3.tab.py deleted file mode 100644 index 664151d1438..00000000000 --- a/doc/OnlineDocs/src/data/import3.tab.py +++ /dev/null @@ -1,20 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set() - -instance = model.create_instance('import3.tab.dat') - -print('A ' + str(sorted(list(instance.A.data())))) diff --git a/doc/OnlineDocs/src/data/import3.tab.txt b/doc/OnlineDocs/src/data/import3.tab.txt deleted file mode 100644 index a9e1ae02cf5..00000000000 --- a/doc/OnlineDocs/src/data/import3.tab.txt +++ /dev/null @@ -1 +0,0 @@ -A ['A1', 'A2', 'A3'] diff --git a/doc/OnlineDocs/src/data/import4.tab.dat b/doc/OnlineDocs/src/data/import4.tab.dat deleted file mode 100644 index 986e9c4c33a..00000000000 --- a/doc/OnlineDocs/src/data/import4.tab.dat +++ /dev/null @@ -1 +0,0 @@ -load C.tab format=set : C; diff --git a/doc/OnlineDocs/src/data/import4.tab.py b/doc/OnlineDocs/src/data/import4.tab.py deleted file mode 100644 index 91dd3f26a42..00000000000 --- a/doc/OnlineDocs/src/data/import4.tab.py +++ /dev/null @@ -1,20 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.C = Set(dimen=2) - -instance = model.create_instance('import4.tab.dat') - -print('C ' + str(sorted(list(instance.C.data())))) diff --git a/doc/OnlineDocs/src/data/import4.tab.txt b/doc/OnlineDocs/src/data/import4.tab.txt deleted file mode 100644 index cd8f5e6eedb..00000000000 --- a/doc/OnlineDocs/src/data/import4.tab.txt +++ /dev/null @@ -1 +0,0 @@ -C [('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)] diff --git a/doc/OnlineDocs/src/data/import5.tab.dat b/doc/OnlineDocs/src/data/import5.tab.dat deleted file mode 100644 index 6a00436f075..00000000000 --- a/doc/OnlineDocs/src/data/import5.tab.dat +++ /dev/null @@ -1 +0,0 @@ -load D.tab format=set_array: B; diff --git a/doc/OnlineDocs/src/data/import5.tab.py b/doc/OnlineDocs/src/data/import5.tab.py deleted file mode 100644 index 263677c308c..00000000000 --- a/doc/OnlineDocs/src/data/import5.tab.py +++ /dev/null @@ -1,20 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.B = Set(dimen=2) - -instance = model.create_instance('import5.tab.dat') - -print('B ' + str(list(sorted(instance.B.data())))) diff --git a/doc/OnlineDocs/src/data/import5.tab.txt b/doc/OnlineDocs/src/data/import5.tab.txt deleted file mode 100644 index e7f326ba667..00000000000 --- a/doc/OnlineDocs/src/data/import5.tab.txt +++ /dev/null @@ -1 +0,0 @@ -B [('A1', 1), ('A2', 2), ('A3', 3)] diff --git a/doc/OnlineDocs/src/data/import6.tab.dat b/doc/OnlineDocs/src/data/import6.tab.dat deleted file mode 100644 index 29404994306..00000000000 --- a/doc/OnlineDocs/src/data/import6.tab.dat +++ /dev/null @@ -1 +0,0 @@ -load Z.tab format=param: p; diff --git a/doc/OnlineDocs/src/data/import6.tab.py b/doc/OnlineDocs/src/data/import6.tab.py deleted file mode 100644 index 8f4824ad3fe..00000000000 --- a/doc/OnlineDocs/src/data/import6.tab.py +++ /dev/null @@ -1,20 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.p = Param() - -instance = model.create_instance('import6.tab.dat') - -print('p ' + str(value(instance.p))) diff --git a/doc/OnlineDocs/src/data/import6.tab.txt b/doc/OnlineDocs/src/data/import6.tab.txt deleted file mode 100644 index ae682e9fb8f..00000000000 --- a/doc/OnlineDocs/src/data/import6.tab.txt +++ /dev/null @@ -1 +0,0 @@ -p 1.1 diff --git a/doc/OnlineDocs/src/data/import7.tab.dat b/doc/OnlineDocs/src/data/import7.tab.dat deleted file mode 100644 index 793eac7df45..00000000000 --- a/doc/OnlineDocs/src/data/import7.tab.dat +++ /dev/null @@ -1 +0,0 @@ -load U.tab format=array: A=[X] U; diff --git a/doc/OnlineDocs/src/data/import7.tab.py b/doc/OnlineDocs/src/data/import7.tab.py deleted file mode 100644 index 503f9224323..00000000000 --- a/doc/OnlineDocs/src/data/import7.tab.py +++ /dev/null @@ -1,28 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.I = Set(initialize=['I1', 'I2', 'I3', 'I4']) -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.U = Param(model.I, model.A) -# BUG: This should cause an error -# model.U = Param(model.A,model.I) - -instance = model.create_instance('import7.tab.dat') - -print('I ' + str(sorted(list(instance.I.data())))) -print('A ' + str(sorted(list(instance.A.data())))) -print('U') -for key in sorted(instance.U.keys()): - print(name(instance.U, key) + " " + str(value(instance.U[key]))) diff --git a/doc/OnlineDocs/src/data/import7.tab.txt b/doc/OnlineDocs/src/data/import7.tab.txt deleted file mode 100644 index 2761f5f92e6..00000000000 --- a/doc/OnlineDocs/src/data/import7.tab.txt +++ /dev/null @@ -1,15 +0,0 @@ -I ['I1', 'I2', 'I3', 'I4'] -A ['A1', 'A2', 'A3'] -U -U[I1,A1] 1.3 -U[I1,A2] 2.3 -U[I1,A3] 3.3 -U[I2,A1] 1.4 -U[I2,A2] 2.4 -U[I2,A3] 3.4 -U[I3,A1] 1.5 -U[I3,A2] 2.5 -U[I3,A3] 3.5 -U[I4,A1] 1.6 -U[I4,A2] 2.6 -U[I4,A3] 3.6 diff --git a/doc/OnlineDocs/src/data/import8.tab.dat b/doc/OnlineDocs/src/data/import8.tab.dat deleted file mode 100644 index b229ac3ff8b..00000000000 --- a/doc/OnlineDocs/src/data/import8.tab.dat +++ /dev/null @@ -1 +0,0 @@ -load U.tab format=transposed_array: A=[X] U; diff --git a/doc/OnlineDocs/src/data/import8.tab.py b/doc/OnlineDocs/src/data/import8.tab.py deleted file mode 100644 index 02b8724fe45..00000000000 --- a/doc/OnlineDocs/src/data/import8.tab.py +++ /dev/null @@ -1,26 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.I = Set(initialize=['I1', 'I2', 'I3', 'I4']) -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.U = Param(model.A, model.I) - -instance = model.create_instance('import8.tab.dat') - -print('A ' + str(sorted(list(instance.A.data())))) -print('I ' + str(sorted(list(instance.I.data())))) -print('U') -for key in sorted(instance.U.keys()): - print(name(instance.U, key) + " " + str(value(instance.U[key]))) diff --git a/doc/OnlineDocs/src/data/import8.tab.txt b/doc/OnlineDocs/src/data/import8.tab.txt deleted file mode 100644 index e401fd07818..00000000000 --- a/doc/OnlineDocs/src/data/import8.tab.txt +++ /dev/null @@ -1,15 +0,0 @@ -A ['A1', 'A2', 'A3'] -I ['I1', 'I2', 'I3', 'I4'] -U -U[A1,I1] 1.3 -U[A1,I2] 1.4 -U[A1,I3] 1.5 -U[A1,I4] 1.6 -U[A2,I1] 2.3 -U[A2,I2] 2.4 -U[A2,I3] 2.5 -U[A2,I4] 2.6 -U[A3,I1] 3.3 -U[A3,I2] 3.4 -U[A3,I3] 3.5 -U[A3,I4] 3.6 diff --git a/doc/OnlineDocs/src/data/namespace1.dat b/doc/OnlineDocs/src/data/namespace1.dat deleted file mode 100644 index 3a2917ef160..00000000000 --- a/doc/OnlineDocs/src/data/namespace1.dat +++ /dev/null @@ -1,12 +0,0 @@ -set C := 1 2 3 ; - -namespace ns1 -{ - set C := 4 5 6 ; -} - -namespace ns2 -{ - set C := 7 8 9 ; -} - diff --git a/doc/OnlineDocs/src/data/param1.dat b/doc/OnlineDocs/src/data/param1.dat deleted file mode 100644 index 38c79ba527d..00000000000 --- a/doc/OnlineDocs/src/data/param1.dat +++ /dev/null @@ -1,5 +0,0 @@ -param A := 1.4; -param B := 1; -param C := abc; -param D := true; -param E := 1.0e+04; diff --git a/doc/OnlineDocs/src/data/param1.py b/doc/OnlineDocs/src/data/param1.py deleted file mode 100644 index 336a04287b9..00000000000 --- a/doc/OnlineDocs/src/data/param1.py +++ /dev/null @@ -1,30 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Param() -model.B = Param() -model.C = Param() -model.D = Param() -model.E = Param() -# @decl - -instance = model.create_instance('param1.dat') - -print(value(instance.A)) -print(value(instance.B)) -print(value(instance.C)) -print(value(instance.D)) -print(value(instance.E)) diff --git a/doc/OnlineDocs/src/data/param1.txt b/doc/OnlineDocs/src/data/param1.txt deleted file mode 100644 index 62a63f1d5b3..00000000000 --- a/doc/OnlineDocs/src/data/param1.txt +++ /dev/null @@ -1,5 +0,0 @@ -1.4 -1 -abc -True -10000.0 diff --git a/doc/OnlineDocs/src/data/param2.dat b/doc/OnlineDocs/src/data/param2.dat deleted file mode 100644 index a30794ebf06..00000000000 --- a/doc/OnlineDocs/src/data/param2.dat +++ /dev/null @@ -1,3 +0,0 @@ -set A := a c e; - -param B := a 10 c 30 e 50; diff --git a/doc/OnlineDocs/src/data/param2.py b/doc/OnlineDocs/src/data/param2.py deleted file mode 100644 index a7d0feafff9..00000000000 --- a/doc/OnlineDocs/src/data/param2.py +++ /dev/null @@ -1,25 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set() -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param2.dat') - -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param2.txt b/doc/OnlineDocs/src/data/param2.txt deleted file mode 100644 index bc9f93115e1..00000000000 --- a/doc/OnlineDocs/src/data/param2.txt +++ /dev/null @@ -1,3 +0,0 @@ -a 10 -c 30 -e 50 diff --git a/doc/OnlineDocs/src/data/param2a.dat b/doc/OnlineDocs/src/data/param2a.dat deleted file mode 100644 index 1d6e1a13fbe..00000000000 --- a/doc/OnlineDocs/src/data/param2a.dat +++ /dev/null @@ -1,7 +0,0 @@ -set A := a c e; - -param B := -a 10 -c 30 -e 50 -; diff --git a/doc/OnlineDocs/src/data/param2a.py b/doc/OnlineDocs/src/data/param2a.py deleted file mode 100644 index 42056793ffd..00000000000 --- a/doc/OnlineDocs/src/data/param2a.py +++ /dev/null @@ -1,25 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set() -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param2a.dat') - -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param2a.txt b/doc/OnlineDocs/src/data/param2a.txt deleted file mode 100644 index bc9f93115e1..00000000000 --- a/doc/OnlineDocs/src/data/param2a.txt +++ /dev/null @@ -1,3 +0,0 @@ -a 10 -c 30 -e 50 diff --git a/doc/OnlineDocs/src/data/param3.dat b/doc/OnlineDocs/src/data/param3.dat deleted file mode 100644 index c4bbf2890b2..00000000000 --- a/doc/OnlineDocs/src/data/param3.dat +++ /dev/null @@ -1,7 +0,0 @@ -set A := a c e; - -param : B C D := -a 10 -1 1.1 -c 30 -3 3.3 -e 50 -5 5.5 -; diff --git a/doc/OnlineDocs/src/data/param3.py b/doc/OnlineDocs/src/data/param3.py deleted file mode 100644 index 952f9a9b707..00000000000 --- a/doc/OnlineDocs/src/data/param3.py +++ /dev/null @@ -1,36 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set() -model.B = Param(model.A) -model.C = Param(model.A) -model.D = Param(model.A) -# @decl - -instance = model.create_instance('param3.dat') - -print('B') -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) -print('C') -keys = instance.C.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.C[key]))) -print('D') -keys = instance.D.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/param3.txt b/doc/OnlineDocs/src/data/param3.txt deleted file mode 100644 index 55e6d2bc1b0..00000000000 --- a/doc/OnlineDocs/src/data/param3.txt +++ /dev/null @@ -1,12 +0,0 @@ -B -a 10 -c 30 -e 50 -C -a -1 -c -3 -e -5 -D -a 1.1 -c 3.3 -e 5.5 diff --git a/doc/OnlineDocs/src/data/param3a.dat b/doc/OnlineDocs/src/data/param3a.dat deleted file mode 100644 index 3894bf4893d..00000000000 --- a/doc/OnlineDocs/src/data/param3a.dat +++ /dev/null @@ -1,7 +0,0 @@ -set A := a c e g; - -param : B C D := -a 10 -1 1.1 -c 30 -3 3.3 -e 50 -5 5.5 -; diff --git a/doc/OnlineDocs/src/data/param3a.py b/doc/OnlineDocs/src/data/param3a.py deleted file mode 100644 index 028e1d07296..00000000000 --- a/doc/OnlineDocs/src/data/param3a.py +++ /dev/null @@ -1,36 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set() -model.B = Param(model.A) -model.C = Param(model.A) -model.D = Param(model.A) -# @decl - -instance = model.create_instance('param3a.dat') - -print('B') -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) -print('C') -keys = instance.C.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.C[key]))) -print('D') -keys = instance.D.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/param3a.txt b/doc/OnlineDocs/src/data/param3a.txt deleted file mode 100644 index 55e6d2bc1b0..00000000000 --- a/doc/OnlineDocs/src/data/param3a.txt +++ /dev/null @@ -1,12 +0,0 @@ -B -a 10 -c 30 -e 50 -C -a -1 -c -3 -e -5 -D -a 1.1 -c 3.3 -e 5.5 diff --git a/doc/OnlineDocs/src/data/param3b.dat b/doc/OnlineDocs/src/data/param3b.dat deleted file mode 100644 index 1aa4eccda05..00000000000 --- a/doc/OnlineDocs/src/data/param3b.dat +++ /dev/null @@ -1,7 +0,0 @@ -set A := a c e; - -param : B C D := -a . -1 1.1 -c 30 . 3.3 -e 50 -5 . -; diff --git a/doc/OnlineDocs/src/data/param3b.py b/doc/OnlineDocs/src/data/param3b.py deleted file mode 100644 index 97f8598610a..00000000000 --- a/doc/OnlineDocs/src/data/param3b.py +++ /dev/null @@ -1,36 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set() -model.B = Param(model.A) -model.C = Param(model.A) -model.D = Param(model.A) -# @decl - -instance = model.create_instance('param3b.dat') - -print('B') -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) -print('C') -keys = instance.C.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.C[key]))) -print('D') -keys = instance.D.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/param3b.txt b/doc/OnlineDocs/src/data/param3b.txt deleted file mode 100644 index d558a22cab6..00000000000 --- a/doc/OnlineDocs/src/data/param3b.txt +++ /dev/null @@ -1,9 +0,0 @@ -B -c 30 -e 50 -C -a -1 -e -5 -D -a 1.1 -c 3.3 diff --git a/doc/OnlineDocs/src/data/param3c.dat b/doc/OnlineDocs/src/data/param3c.dat deleted file mode 100644 index ada86b946c4..00000000000 --- a/doc/OnlineDocs/src/data/param3c.dat +++ /dev/null @@ -1,5 +0,0 @@ -param : A : B C D := -a 10 -1 1.1 -c 30 -3 3.3 -e 50 -5 5.5 -; diff --git a/doc/OnlineDocs/src/data/param3c.py b/doc/OnlineDocs/src/data/param3c.py deleted file mode 100644 index 582b0f7db75..00000000000 --- a/doc/OnlineDocs/src/data/param3c.py +++ /dev/null @@ -1,36 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set() -model.B = Param(model.A) -model.C = Param(model.A) -model.D = Param(model.A) -# @decl - -instance = model.create_instance('param3c.dat') - -print('B') -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) -print('C') -keys = instance.C.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.C[key]))) -print('D') -keys = instance.D.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/param3c.txt b/doc/OnlineDocs/src/data/param3c.txt deleted file mode 100644 index 55e6d2bc1b0..00000000000 --- a/doc/OnlineDocs/src/data/param3c.txt +++ /dev/null @@ -1,12 +0,0 @@ -B -a 10 -c 30 -e 50 -C -a -1 -c -3 -e -5 -D -a 1.1 -c 3.3 -e 5.5 diff --git a/doc/OnlineDocs/src/data/param4.dat b/doc/OnlineDocs/src/data/param4.dat deleted file mode 100644 index 808e9a2b9b4..00000000000 --- a/doc/OnlineDocs/src/data/param4.dat +++ /dev/null @@ -1,6 +0,0 @@ -set A := a c e; - -param B default 0.0 := -c 30 -e 50 -; diff --git a/doc/OnlineDocs/src/data/param4.py b/doc/OnlineDocs/src/data/param4.py deleted file mode 100644 index 010c46fc9c5..00000000000 --- a/doc/OnlineDocs/src/data/param4.py +++ /dev/null @@ -1,26 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set() -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param4.dat') - -print('B') -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param4.txt b/doc/OnlineDocs/src/data/param4.txt deleted file mode 100644 index 70ab4505a6d..00000000000 --- a/doc/OnlineDocs/src/data/param4.txt +++ /dev/null @@ -1,4 +0,0 @@ -B -a 0 -c 30 -e 50 diff --git a/doc/OnlineDocs/src/data/param5.dat b/doc/OnlineDocs/src/data/param5.dat deleted file mode 100644 index 84120b1bfc5..00000000000 --- a/doc/OnlineDocs/src/data/param5.dat +++ /dev/null @@ -1,6 +0,0 @@ -set A := a 1 c 2 e 3; - -param B := -a 1 10 -c 2 30 -e 3 50; diff --git a/doc/OnlineDocs/src/data/param5.py b/doc/OnlineDocs/src/data/param5.py deleted file mode 100644 index 2db07f3f990..00000000000 --- a/doc/OnlineDocs/src/data/param5.py +++ /dev/null @@ -1,25 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=2) -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param5.dat') - -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param5.txt b/doc/OnlineDocs/src/data/param5.txt deleted file mode 100644 index 6894ef994ce..00000000000 --- a/doc/OnlineDocs/src/data/param5.txt +++ /dev/null @@ -1,3 +0,0 @@ -('a', 1) 10 -('c', 2) 30 -('e', 3) 50 diff --git a/doc/OnlineDocs/src/data/param5a.dat b/doc/OnlineDocs/src/data/param5a.dat deleted file mode 100644 index 1533f76bfc9..00000000000 --- a/doc/OnlineDocs/src/data/param5a.dat +++ /dev/null @@ -1,6 +0,0 @@ -set A := a 1 c 2 e 3; - -param B default 0 := -a 1 10 -c 2 . -e 3 50; diff --git a/doc/OnlineDocs/src/data/param5a.py b/doc/OnlineDocs/src/data/param5a.py deleted file mode 100644 index 32a53d24e9b..00000000000 --- a/doc/OnlineDocs/src/data/param5a.py +++ /dev/null @@ -1,25 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=2) -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param5a.dat') - -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param5a.txt b/doc/OnlineDocs/src/data/param5a.txt deleted file mode 100644 index 996c359409b..00000000000 --- a/doc/OnlineDocs/src/data/param5a.txt +++ /dev/null @@ -1,3 +0,0 @@ -('a', 1) 10 -('c', 2) 0 -('e', 3) 50 diff --git a/doc/OnlineDocs/src/data/param6.dat b/doc/OnlineDocs/src/data/param6.dat deleted file mode 100644 index f9c472ed39b..00000000000 --- a/doc/OnlineDocs/src/data/param6.dat +++ /dev/null @@ -1,7 +0,0 @@ -set A := a 1 c 2 e 3; - -param : B C D := -a 1 10 -1 1.1 -c 2 30 -3 3.3 -e 3 50 -5 5.5 -; diff --git a/doc/OnlineDocs/src/data/param6.py b/doc/OnlineDocs/src/data/param6.py deleted file mode 100644 index e3364a933cf..00000000000 --- a/doc/OnlineDocs/src/data/param6.py +++ /dev/null @@ -1,36 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=2) -model.B = Param(model.A) -model.C = Param(model.A) -model.D = Param(model.A) -# @decl - -instance = model.create_instance('param6.dat') - -keys = instance.B.keys() -print('B') -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) -print('C') -keys = instance.C.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.C[key]))) -print('D') -keys = instance.D.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/param6.txt b/doc/OnlineDocs/src/data/param6.txt deleted file mode 100644 index f7c63c10589..00000000000 --- a/doc/OnlineDocs/src/data/param6.txt +++ /dev/null @@ -1,12 +0,0 @@ -B -('a', 1) 10 -('c', 2) 30 -('e', 3) 50 -C -('a', 1) -1 -('c', 2) -3 -('e', 3) -5 -D -('a', 1) 1.1 -('c', 2) 3.3 -('e', 3) 5.5 diff --git a/doc/OnlineDocs/src/data/param6a.dat b/doc/OnlineDocs/src/data/param6a.dat deleted file mode 100644 index 253dc71632f..00000000000 --- a/doc/OnlineDocs/src/data/param6a.dat +++ /dev/null @@ -1,5 +0,0 @@ -param : A : B C D := -a 1 10 -1 1.1 -c 2 30 -3 3.3 -e 3 50 -5 5.5 -; diff --git a/doc/OnlineDocs/src/data/param6a.py b/doc/OnlineDocs/src/data/param6a.py deleted file mode 100644 index 3d2fa645411..00000000000 --- a/doc/OnlineDocs/src/data/param6a.py +++ /dev/null @@ -1,36 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=2) -model.B = Param(model.A) -model.C = Param(model.A) -model.D = Param(model.A) -# @decl - -instance = model.create_instance('param6a.dat') - -keys = instance.B.keys() -print('B') -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) -print('C') -keys = instance.C.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.C[key]))) -print('D') -keys = instance.D.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.D[key]))) diff --git a/doc/OnlineDocs/src/data/param6a.txt b/doc/OnlineDocs/src/data/param6a.txt deleted file mode 100644 index f7c63c10589..00000000000 --- a/doc/OnlineDocs/src/data/param6a.txt +++ /dev/null @@ -1,12 +0,0 @@ -B -('a', 1) 10 -('c', 2) 30 -('e', 3) 50 -C -('a', 1) -1 -('c', 2) -3 -('e', 3) -5 -D -('a', 1) 1.1 -('c', 2) 3.3 -('e', 3) 5.5 diff --git a/doc/OnlineDocs/src/data/param7a.dat b/doc/OnlineDocs/src/data/param7a.dat deleted file mode 100644 index f7506e8a17a..00000000000 --- a/doc/OnlineDocs/src/data/param7a.dat +++ /dev/null @@ -1,7 +0,0 @@ -set A := 1 a 1 c 1 e 2 a 2 c 2 e 3 a 3 c 3 e; - -param B : a c e := -1 1 2 3 -2 4 5 6 -3 7 8 9 -; diff --git a/doc/OnlineDocs/src/data/param7a.py b/doc/OnlineDocs/src/data/param7a.py deleted file mode 100644 index b3aba9ec23d..00000000000 --- a/doc/OnlineDocs/src/data/param7a.py +++ /dev/null @@ -1,25 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=2) -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param7a.dat') - -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param7a.txt b/doc/OnlineDocs/src/data/param7a.txt deleted file mode 100644 index 5780254c2b5..00000000000 --- a/doc/OnlineDocs/src/data/param7a.txt +++ /dev/null @@ -1,9 +0,0 @@ -(1, 'a') 1 -(1, 'c') 2 -(1, 'e') 3 -(2, 'a') 4 -(2, 'c') 5 -(2, 'e') 6 -(3, 'a') 7 -(3, 'c') 8 -(3, 'e') 9 diff --git a/doc/OnlineDocs/src/data/param7b.dat b/doc/OnlineDocs/src/data/param7b.dat deleted file mode 100644 index 5e58d905c55..00000000000 --- a/doc/OnlineDocs/src/data/param7b.dat +++ /dev/null @@ -1,7 +0,0 @@ -set A := 1 a 1 c 1 e 2 a 2 c 2 e 3 a 3 c 3 e; - -param B (tr) : 1 2 3 := -a 1 4 7 -c 2 5 8 -e 3 6 9 -; diff --git a/doc/OnlineDocs/src/data/param7b.py b/doc/OnlineDocs/src/data/param7b.py deleted file mode 100644 index 8b022f399a8..00000000000 --- a/doc/OnlineDocs/src/data/param7b.py +++ /dev/null @@ -1,25 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=2) -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param7b.dat') - -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param7b.txt b/doc/OnlineDocs/src/data/param7b.txt deleted file mode 100644 index 5780254c2b5..00000000000 --- a/doc/OnlineDocs/src/data/param7b.txt +++ /dev/null @@ -1,9 +0,0 @@ -(1, 'a') 1 -(1, 'c') 2 -(1, 'e') 3 -(2, 'a') 4 -(2, 'c') 5 -(2, 'e') 6 -(3, 'a') 7 -(3, 'c') 8 -(3, 'e') 9 diff --git a/doc/OnlineDocs/src/data/param8a.dat b/doc/OnlineDocs/src/data/param8a.dat deleted file mode 100644 index 74b3597c41d..00000000000 --- a/doc/OnlineDocs/src/data/param8a.dat +++ /dev/null @@ -1,7 +0,0 @@ -set A := (a,1,a,1) (a,2,a,2) (b,1,b,1) (b,2,b,2); - -param B := - - [*,1,*,1] a a 10 b b 20 - [*,2,*,2] a a 30 b b 40 -; diff --git a/doc/OnlineDocs/src/data/param8a.py b/doc/OnlineDocs/src/data/param8a.py deleted file mode 100644 index abfa885ded4..00000000000 --- a/doc/OnlineDocs/src/data/param8a.py +++ /dev/null @@ -1,25 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=4) -model.B = Param(model.A) -# @decl - -instance = model.create_instance('param8a.dat') - -keys = instance.B.keys() -for key in sorted(keys): - print(str(key) + " " + str(value(instance.B[key]))) diff --git a/doc/OnlineDocs/src/data/param8a.txt b/doc/OnlineDocs/src/data/param8a.txt deleted file mode 100644 index df87d190ba0..00000000000 --- a/doc/OnlineDocs/src/data/param8a.txt +++ /dev/null @@ -1,4 +0,0 @@ -('a', 1, 'a', 1) 10 -('a', 2, 'a', 2) 30 -('b', 1, 'b', 1) 20 -('b', 2, 'b', 2) 40 diff --git a/doc/OnlineDocs/src/data/pyomo.diet1.sh b/doc/OnlineDocs/src/data/pyomo.diet1.sh deleted file mode 100755 index 16ced7aa189..00000000000 --- a/doc/OnlineDocs/src/data/pyomo.diet1.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -pyomo solve --solver=glpk diet1.py diet.sqlite.dat -cat results.yml -rm -f results.yml results.json diff --git a/doc/OnlineDocs/src/data/pyomo.diet1.txt b/doc/OnlineDocs/src/data/pyomo.diet1.txt deleted file mode 100644 index fd8c87d51d9..00000000000 --- a/doc/OnlineDocs/src/data/pyomo.diet1.txt +++ /dev/null @@ -1,60 +0,0 @@ -[ 0.00] Setting up Pyomo environment -[ 0.00] Applying Pyomo preprocessing actions -[ 0.00] Creating model -[ 0.01] Applying solver -[ 0.02] Processing results - Number of solutions: 1 - Solution Information - Gap: 0.0 - Status: optimal - Function Value: 2.81 - Solver results file: results.yml -[ 0.02] Applying Pyomo postprocessing actions -[ 0.02] Pyomo Finished -# ========================================================== -# = Solver Results = -# ========================================================== -# ---------------------------------------------------------- -# Problem Information -# ---------------------------------------------------------- -Problem: -- Name: unknown - Lower bound: 2.81 - Upper bound: 2.81 - Number of objectives: 1 - Number of constraints: 3 - Number of variables: 9 - Number of nonzeros: 9 - Sense: minimize -# ---------------------------------------------------------- -# Solver Information -# ---------------------------------------------------------- -Solver: -- Status: ok - Termination condition: optimal - Statistics: - Branch and bound: - Number of bounded subproblems: 1 - Number of created subproblems: 1 - Error rc: 0 - Time: 0.002644062042236328 -# ---------------------------------------------------------- -# Solution Information -# ---------------------------------------------------------- -Solution: -- number of solutions: 1 - number of solutions displayed: 1 -- Gap: 0.0 - Status: optimal - Message: None - Objective: - Total_Cost: - Value: 2.81 - Variable: - Buy[Fish Sandwich]: - Value: 1 - Buy[Fries]: - Value: 1 - Buy[Lowfat Milk]: - Value: 1 - Constraint: No values diff --git a/doc/OnlineDocs/src/data/pyomo.diet2.sh b/doc/OnlineDocs/src/data/pyomo.diet2.sh deleted file mode 100755 index 78931b3c96d..00000000000 --- a/doc/OnlineDocs/src/data/pyomo.diet2.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -pyomo solve --solver=glpk diet1.py diet.dat -cat results.yml -rm -f results.yml results.json diff --git a/doc/OnlineDocs/src/data/pyomo.diet2.txt b/doc/OnlineDocs/src/data/pyomo.diet2.txt deleted file mode 100644 index 7ed879d500f..00000000000 --- a/doc/OnlineDocs/src/data/pyomo.diet2.txt +++ /dev/null @@ -1,60 +0,0 @@ -[ 0.00] Setting up Pyomo environment -[ 0.00] Applying Pyomo preprocessing actions -[ 0.00] Creating model -[ 0.01] Applying solver -[ 0.01] Processing results - Number of solutions: 1 - Solution Information - Gap: 0.0 - Status: optimal - Function Value: 2.81 - Solver results file: results.yml -[ 0.01] Applying Pyomo postprocessing actions -[ 0.01] Pyomo Finished -# ========================================================== -# = Solver Results = -# ========================================================== -# ---------------------------------------------------------- -# Problem Information -# ---------------------------------------------------------- -Problem: -- Name: unknown - Lower bound: 2.81 - Upper bound: 2.81 - Number of objectives: 1 - Number of constraints: 3 - Number of variables: 9 - Number of nonzeros: 9 - Sense: minimize -# ---------------------------------------------------------- -# Solver Information -# ---------------------------------------------------------- -Solver: -- Status: ok - Termination condition: optimal - Statistics: - Branch and bound: - Number of bounded subproblems: 1 - Number of created subproblems: 1 - Error rc: 0 - Time: 0.0018515586853027344 -# ---------------------------------------------------------- -# Solution Information -# ---------------------------------------------------------- -Solution: -- number of solutions: 1 - number of solutions displayed: 1 -- Gap: 0.0 - Status: optimal - Message: None - Objective: - Total_Cost: - Value: 2.81 - Variable: - Buy[Fish Sandwich]: - Value: 1 - Buy[Fries]: - Value: 1 - Buy[Lowfat Milk]: - Value: 1 - Constraint: No values diff --git a/doc/OnlineDocs/src/data/set1.dat b/doc/OnlineDocs/src/data/set1.dat deleted file mode 100644 index 53f6b986e45..00000000000 --- a/doc/OnlineDocs/src/data/set1.dat +++ /dev/null @@ -1,17 +0,0 @@ -# An empty set -set A := ; - -# A set of numbers -set A := 1 2 3; - -# A set of strings -set B := north south east west; - -# A set of mixed types -set C := -0 --1.0e+10 -'foo bar' -infinity -"100" -; diff --git a/doc/OnlineDocs/src/data/set1.py b/doc/OnlineDocs/src/data/set1.py deleted file mode 100644 index c84c1ef0819..00000000000 --- a/doc/OnlineDocs/src/data/set1.py +++ /dev/null @@ -1,24 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set() -model.B = Set() -model.C = Set() - -instance = model.create_instance('set1.dat') - -print(sorted(list(instance.A.data()))) -print(sorted((instance.B.data()))) -print(sorted(list((instance.C.data())), key=lambda x: x if type(x) is str else str(x))) diff --git a/doc/OnlineDocs/src/data/set1.txt b/doc/OnlineDocs/src/data/set1.txt deleted file mode 100644 index 9114550e1bf..00000000000 --- a/doc/OnlineDocs/src/data/set1.txt +++ /dev/null @@ -1,3 +0,0 @@ -[1, 2, 3] -['east', 'north', 'south', 'west'] -[-10000000000.0, 0, 100, 'foo bar', inf] diff --git a/doc/OnlineDocs/src/data/set2.dat b/doc/OnlineDocs/src/data/set2.dat deleted file mode 100644 index 4e9ec9b2da9..00000000000 --- a/doc/OnlineDocs/src/data/set2.dat +++ /dev/null @@ -1 +0,0 @@ -set A := 1 2 3 4 5 6 ; diff --git a/doc/OnlineDocs/src/data/set2.py b/doc/OnlineDocs/src/data/set2.py deleted file mode 100644 index 9048a49fecb..00000000000 --- a/doc/OnlineDocs/src/data/set2.py +++ /dev/null @@ -1,22 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=3) -# @decl - -instance = model.create_instance('set2.dat') - -print(sorted(list(instance.A.data()))) diff --git a/doc/OnlineDocs/src/data/set2.txt b/doc/OnlineDocs/src/data/set2.txt deleted file mode 100644 index 8e4375be534..00000000000 --- a/doc/OnlineDocs/src/data/set2.txt +++ /dev/null @@ -1 +0,0 @@ -[(1, 2, 3), (4, 5, 6)] diff --git a/doc/OnlineDocs/src/data/set2a.dat b/doc/OnlineDocs/src/data/set2a.dat deleted file mode 100644 index fb0b3a4f67b..00000000000 --- a/doc/OnlineDocs/src/data/set2a.dat +++ /dev/null @@ -1 +0,0 @@ -set A := (1,2,3) (4,5,6) ; diff --git a/doc/OnlineDocs/src/data/set2a.py b/doc/OnlineDocs/src/data/set2a.py deleted file mode 100644 index f2fa4d71916..00000000000 --- a/doc/OnlineDocs/src/data/set2a.py +++ /dev/null @@ -1,22 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=3) -# @decl - -instance = model.create_instance('set2a.dat') - -print(sorted(list(instance.A.data()))) diff --git a/doc/OnlineDocs/src/data/set2a.txt b/doc/OnlineDocs/src/data/set2a.txt deleted file mode 100644 index 8e4375be534..00000000000 --- a/doc/OnlineDocs/src/data/set2a.txt +++ /dev/null @@ -1 +0,0 @@ -[(1, 2, 3), (4, 5, 6)] diff --git a/doc/OnlineDocs/src/data/set3.dat b/doc/OnlineDocs/src/data/set3.dat deleted file mode 100644 index 7e27ffabd1b..00000000000 --- a/doc/OnlineDocs/src/data/set3.dat +++ /dev/null @@ -1,5 +0,0 @@ -set A := 1 aaa 'a b'; - -set B[1] := 0 1 2; -set B[aaa] := aa bb cc; -set B['a b'] := 'aa bb cc'; diff --git a/doc/OnlineDocs/src/data/set3.py b/doc/OnlineDocs/src/data/set3.py deleted file mode 100644 index 9cdacbe39e0..00000000000 --- a/doc/OnlineDocs/src/data/set3.py +++ /dev/null @@ -1,30 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set() -model.B = Set(model.A) -# @decl -# model.C = Set(model.A,model.A) - -instance = model.create_instance('set3.dat') - -print(sorted(list(instance.A.data()), key=lambda x: x if type(x) is str else str(x))) -print(sorted(list(instance.B[1].data()), key=lambda x: x if type(x) is str else str(x))) -print( - sorted( - list(instance.B['aaa'].data()), key=lambda x: x if type(x) is str else str(x) - ) -) diff --git a/doc/OnlineDocs/src/data/set3.txt b/doc/OnlineDocs/src/data/set3.txt deleted file mode 100644 index fdcefa0d87a..00000000000 --- a/doc/OnlineDocs/src/data/set3.txt +++ /dev/null @@ -1,3 +0,0 @@ -[1, 'a b', 'aaa'] -[0, 1, 2] -['aa', 'bb', 'cc'] diff --git a/doc/OnlineDocs/src/data/set4.dat b/doc/OnlineDocs/src/data/set4.dat deleted file mode 100644 index aa367736c62..00000000000 --- a/doc/OnlineDocs/src/data/set4.dat +++ /dev/null @@ -1,4 +0,0 @@ -set A : A1 A2 A3 A4 := - 1 + - - + - 2 + - + - - 3 - + - - ; diff --git a/doc/OnlineDocs/src/data/set4.py b/doc/OnlineDocs/src/data/set4.py deleted file mode 100644 index b3485638c6f..00000000000 --- a/doc/OnlineDocs/src/data/set4.py +++ /dev/null @@ -1,22 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=2) -# @decl - -instance = model.create_instance('set4.dat') - -print(sorted(list(instance.A.data()))) diff --git a/doc/OnlineDocs/src/data/set4.txt b/doc/OnlineDocs/src/data/set4.txt deleted file mode 100644 index 6ca2c725cb0..00000000000 --- a/doc/OnlineDocs/src/data/set4.txt +++ /dev/null @@ -1 +0,0 @@ -[('A1', 1), ('A1', 2), ('A2', 3), ('A3', 2), ('A4', 1)] diff --git a/doc/OnlineDocs/src/data/set5.dat b/doc/OnlineDocs/src/data/set5.dat deleted file mode 100644 index ce117cdea73..00000000000 --- a/doc/OnlineDocs/src/data/set5.dat +++ /dev/null @@ -1,3 +0,0 @@ -set A := - (1,2,*,4) A B - (*,2,*,4) A B C D ; diff --git a/doc/OnlineDocs/src/data/set5.py b/doc/OnlineDocs/src/data/set5.py deleted file mode 100644 index d745d8408d0..00000000000 --- a/doc/OnlineDocs/src/data/set5.py +++ /dev/null @@ -1,24 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -# @decl -model.A = Set(dimen=4) -# @decl - -instance = model.create_instance('set5.dat') - - -for tpl in sorted(list(instance.A.data()), key=lambda x: tuple(map(str, x))): - print(tpl) diff --git a/doc/OnlineDocs/src/data/set5.txt b/doc/OnlineDocs/src/data/set5.txt deleted file mode 100644 index eea067c90ad..00000000000 --- a/doc/OnlineDocs/src/data/set5.txt +++ /dev/null @@ -1,4 +0,0 @@ -(1, 2, 'A', 4) -(1, 2, 'B', 4) -('A', 2, 'B', 4) -('C', 2, 'D', 4) diff --git a/doc/OnlineDocs/src/data/table0.dat b/doc/OnlineDocs/src/data/table0.dat deleted file mode 100644 index 44e822c6b19..00000000000 --- a/doc/OnlineDocs/src/data/table0.dat +++ /dev/null @@ -1,6 +0,0 @@ -table M(A) : -A B M N := -A1 B1 4.3 5.3 -A2 B2 4.4 5.4 -A3 B3 4.5 5.5 -; diff --git a/doc/OnlineDocs/src/data/table0.py b/doc/OnlineDocs/src/data/table0.py deleted file mode 100644 index de0fae0c861..00000000000 --- a/doc/OnlineDocs/src/data/table0.py +++ /dev/null @@ -1,20 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.M = Param(model.A) - -instance = model.create_instance('table0.dat') -instance.pprint() diff --git a/doc/OnlineDocs/src/data/table0.txt b/doc/OnlineDocs/src/data/table0.txt deleted file mode 100644 index c2e75dd97a6..00000000000 --- a/doc/OnlineDocs/src/data/table0.txt +++ /dev/null @@ -1,13 +0,0 @@ -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - -1 Param Declarations - M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - -2 Declarations: A M diff --git a/doc/OnlineDocs/src/data/table0.ul.dat b/doc/OnlineDocs/src/data/table0.ul.dat deleted file mode 100644 index 1e296f6202a..00000000000 --- a/doc/OnlineDocs/src/data/table0.ul.dat +++ /dev/null @@ -1,5 +0,0 @@ -table columns=4 M(1)={3} := -A1 B1 4.3 5.3 -A2 B2 4.4 5.4 -A3 B3 4.5 5.5 -; diff --git a/doc/OnlineDocs/src/data/table0.ul.py b/doc/OnlineDocs/src/data/table0.ul.py deleted file mode 100644 index 524c3756782..00000000000 --- a/doc/OnlineDocs/src/data/table0.ul.py +++ /dev/null @@ -1,20 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.M = Param(model.A) - -instance = model.create_instance('table0.ul.dat') -instance.pprint() diff --git a/doc/OnlineDocs/src/data/table0.ul.txt b/doc/OnlineDocs/src/data/table0.ul.txt deleted file mode 100644 index c2e75dd97a6..00000000000 --- a/doc/OnlineDocs/src/data/table0.ul.txt +++ /dev/null @@ -1,13 +0,0 @@ -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - -1 Param Declarations - M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - -2 Declarations: A M diff --git a/doc/OnlineDocs/src/data/table1.dat b/doc/OnlineDocs/src/data/table1.dat deleted file mode 100644 index f3eae2ac300..00000000000 --- a/doc/OnlineDocs/src/data/table1.dat +++ /dev/null @@ -1,3 +0,0 @@ -table M(A) : -A B M N := -A1 B1 4.3 5.3 A2 B2 4.4 5.4 A3 B3 4.5 5.5 ; diff --git a/doc/OnlineDocs/src/data/table1.py b/doc/OnlineDocs/src/data/table1.py deleted file mode 100644 index f36714b8f1f..00000000000 --- a/doc/OnlineDocs/src/data/table1.py +++ /dev/null @@ -1,20 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.M = Param(model.A) - -instance = model.create_instance('table1.dat') -instance.pprint() diff --git a/doc/OnlineDocs/src/data/table1.txt b/doc/OnlineDocs/src/data/table1.txt deleted file mode 100644 index c2e75dd97a6..00000000000 --- a/doc/OnlineDocs/src/data/table1.txt +++ /dev/null @@ -1,13 +0,0 @@ -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - -1 Param Declarations - M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - -2 Declarations: A M diff --git a/doc/OnlineDocs/src/data/table2.dat b/doc/OnlineDocs/src/data/table2.dat deleted file mode 100644 index 37fa4dad47b..00000000000 --- a/doc/OnlineDocs/src/data/table2.dat +++ /dev/null @@ -1,6 +0,0 @@ -table M(A) N(A,B) : -A B M N := -A1 B1 4.3 5.3 -A2 B2 4.4 5.4 -A3 B3 4.5 5.5 -; diff --git a/doc/OnlineDocs/src/data/table2.py b/doc/OnlineDocs/src/data/table2.py deleted file mode 100644 index 03648a00f8c..00000000000 --- a/doc/OnlineDocs/src/data/table2.py +++ /dev/null @@ -1,23 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.B = Set(initialize=['B1', 'B2', 'B3']) - -model.M = Param(model.A) -model.N = Param(model.A, model.B) - -instance = model.create_instance('table2.dat') -instance.pprint() diff --git a/doc/OnlineDocs/src/data/table2.txt b/doc/OnlineDocs/src/data/table2.txt deleted file mode 100644 index a710b6b6042..00000000000 --- a/doc/OnlineDocs/src/data/table2.txt +++ /dev/null @@ -1,21 +0,0 @@ -2 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - B : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'B1', 'B2', 'B3'} - -2 Param Declarations - M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - N : Size=3, Index=A*B, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'B1') : 5.3 - ('A2', 'B2') : 5.4 - ('A3', 'B3') : 5.5 - -4 Declarations: A B M N diff --git a/doc/OnlineDocs/src/data/table3.dat b/doc/OnlineDocs/src/data/table3.dat deleted file mode 100644 index 820ac6dfe54..00000000000 --- a/doc/OnlineDocs/src/data/table3.dat +++ /dev/null @@ -1,6 +0,0 @@ -table A={A} Z={A,B} M(A) N(A,B) : -A B M N := -A1 B1 4.3 5.3 -A2 B2 4.4 5.4 -A3 B3 4.5 5.5 -; diff --git a/doc/OnlineDocs/src/data/table3.py b/doc/OnlineDocs/src/data/table3.py deleted file mode 100644 index 2c598f112df..00000000000 --- a/doc/OnlineDocs/src/data/table3.py +++ /dev/null @@ -1,25 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set() -model.B = Set(initialize=['B1', 'B2', 'B3']) -model.Z = Set(dimen=2) - -model.M = Param(model.A) -model.N = Param(model.A, model.B) - - -instance = model.create_instance('table3.dat') -instance.pprint() diff --git a/doc/OnlineDocs/src/data/table3.txt b/doc/OnlineDocs/src/data/table3.txt deleted file mode 100644 index c0c61cd5a5b..00000000000 --- a/doc/OnlineDocs/src/data/table3.txt +++ /dev/null @@ -1,24 +0,0 @@ -3 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - B : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'B1', 'B2', 'B3'} - Z : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} - -2 Param Declarations - M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - N : Size=3, Index=A*B, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'B1') : 5.3 - ('A2', 'B2') : 5.4 - ('A3', 'B3') : 5.5 - -5 Declarations: A B Z M N diff --git a/doc/OnlineDocs/src/data/table3.ul.dat b/doc/OnlineDocs/src/data/table3.ul.dat deleted file mode 100644 index db12a6be017..00000000000 --- a/doc/OnlineDocs/src/data/table3.ul.dat +++ /dev/null @@ -1,5 +0,0 @@ -table columns=4 A={1} Z={1,2} M(1)={3} N(1,2)={4} := -A1 B1 4.3 5.3 -A2 B2 4.4 5.4 -A3 B3 4.5 5.5 -; diff --git a/doc/OnlineDocs/src/data/table3.ul.py b/doc/OnlineDocs/src/data/table3.ul.py deleted file mode 100644 index 18ced12b388..00000000000 --- a/doc/OnlineDocs/src/data/table3.ul.py +++ /dev/null @@ -1,25 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set() -model.B = Set(initialize=['B1', 'B2', 'B3']) -model.Z = Set(dimen=2) - -model.M = Param(model.A) -model.N = Param(model.A, model.B) - - -instance = model.create_instance('table3.ul.dat') -instance.pprint() diff --git a/doc/OnlineDocs/src/data/table3.ul.txt b/doc/OnlineDocs/src/data/table3.ul.txt deleted file mode 100644 index c0c61cd5a5b..00000000000 --- a/doc/OnlineDocs/src/data/table3.ul.txt +++ /dev/null @@ -1,24 +0,0 @@ -3 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - B : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'B1', 'B2', 'B3'} - Z : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} - -2 Param Declarations - M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - N : Size=3, Index=A*B, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'B1') : 5.3 - ('A2', 'B2') : 5.4 - ('A3', 'B3') : 5.5 - -5 Declarations: A B Z M N diff --git a/doc/OnlineDocs/src/data/table4.dat b/doc/OnlineDocs/src/data/table4.dat deleted file mode 100644 index 24b524497df..00000000000 --- a/doc/OnlineDocs/src/data/table4.dat +++ /dev/null @@ -1,6 +0,0 @@ -table A={A} Z={A,B} M(A) N(Z) : -A B M N := -A1 B1 4.3 5.3 -A2 B2 4.4 5.4 -A3 B3 4.5 5.5 -; diff --git a/doc/OnlineDocs/src/data/table4.py b/doc/OnlineDocs/src/data/table4.py deleted file mode 100644 index bd20682b5a9..00000000000 --- a/doc/OnlineDocs/src/data/table4.py +++ /dev/null @@ -1,23 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set() -model.Z = Set(dimen=2) - -model.M = Param(model.A) -model.N = Param(model.Z) - -instance = model.create_instance('table4.dat') -instance.pprint() diff --git a/doc/OnlineDocs/src/data/table4.txt b/doc/OnlineDocs/src/data/table4.txt deleted file mode 100644 index f86004c342a..00000000000 --- a/doc/OnlineDocs/src/data/table4.txt +++ /dev/null @@ -1,21 +0,0 @@ -2 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - Z : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} - -2 Param Declarations - M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - N : Size=3, Index=Z, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'B1') : 5.3 - ('A2', 'B2') : 5.4 - ('A3', 'B3') : 5.5 - -4 Declarations: A Z M N diff --git a/doc/OnlineDocs/src/data/table4.ul.dat b/doc/OnlineDocs/src/data/table4.ul.dat deleted file mode 100644 index 7d5ace03974..00000000000 --- a/doc/OnlineDocs/src/data/table4.ul.dat +++ /dev/null @@ -1,5 +0,0 @@ -table columns=4 A={1} Z={1,2} M(A)={3} N(Z)={4} := -A1 B1 4.3 5.3 -A2 B2 4.4 5.4 -A3 B3 4.5 5.5 -; diff --git a/doc/OnlineDocs/src/data/table4.ul.py b/doc/OnlineDocs/src/data/table4.ul.py deleted file mode 100644 index 9f16f21fe19..00000000000 --- a/doc/OnlineDocs/src/data/table4.ul.py +++ /dev/null @@ -1,23 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set() -model.Z = Set(dimen=2) - -model.M = Param(model.A) -model.N = Param(model.Z) - -instance = model.create_instance('table4.ul.dat') -instance.pprint() diff --git a/doc/OnlineDocs/src/data/table4.ul.txt b/doc/OnlineDocs/src/data/table4.ul.txt deleted file mode 100644 index f86004c342a..00000000000 --- a/doc/OnlineDocs/src/data/table4.ul.txt +++ /dev/null @@ -1,21 +0,0 @@ -2 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - Z : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} - -2 Param Declarations - M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - N : Size=3, Index=Z, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'B1') : 5.3 - ('A2', 'B2') : 5.4 - ('A3', 'B3') : 5.5 - -4 Declarations: A Z M N diff --git a/doc/OnlineDocs/src/data/table5.dat b/doc/OnlineDocs/src/data/table5.dat deleted file mode 100644 index e7add18a994..00000000000 --- a/doc/OnlineDocs/src/data/table5.dat +++ /dev/null @@ -1,6 +0,0 @@ -table Z={A,B} Y={M,N} : -A B M N := -A1 B1 4.3 5.3 -A2 B2 4.4 5.4 -A3 B3 4.5 5.5 -; diff --git a/doc/OnlineDocs/src/data/table5.py b/doc/OnlineDocs/src/data/table5.py deleted file mode 100644 index a3cb01209a2..00000000000 --- a/doc/OnlineDocs/src/data/table5.py +++ /dev/null @@ -1,20 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.Z = Set(dimen=2) -model.Y = Set(dimen=2) - -instance = model.create_instance('table5.dat') -instance.pprint() diff --git a/doc/OnlineDocs/src/data/table5.txt b/doc/OnlineDocs/src/data/table5.txt deleted file mode 100644 index 084757b781b..00000000000 --- a/doc/OnlineDocs/src/data/table5.txt +++ /dev/null @@ -1,9 +0,0 @@ -2 Set Declarations - Y : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 3 : {(4.3, 5.3), (4.4, 5.4), (4.5, 5.5)} - Z : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} - -2 Declarations: Z Y diff --git a/doc/OnlineDocs/src/data/table6.dat b/doc/OnlineDocs/src/data/table6.dat deleted file mode 100644 index 9e5e71ba2b7..00000000000 --- a/doc/OnlineDocs/src/data/table6.dat +++ /dev/null @@ -1 +0,0 @@ -table pi := 3.1416 ; diff --git a/doc/OnlineDocs/src/data/table6.py b/doc/OnlineDocs/src/data/table6.py deleted file mode 100644 index 1db0a764a23..00000000000 --- a/doc/OnlineDocs/src/data/table6.py +++ /dev/null @@ -1,19 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.pi = Param() - -instance = model.create_instance('table6.dat') -instance.pprint() diff --git a/doc/OnlineDocs/src/data/table6.txt b/doc/OnlineDocs/src/data/table6.txt deleted file mode 100644 index 811a8e1afbb..00000000000 --- a/doc/OnlineDocs/src/data/table6.txt +++ /dev/null @@ -1,6 +0,0 @@ -1 Param Declarations - pi : Size=1, Index=None, Domain=Any, Default=None, Mutable=False - Key : Value - None : 3.1416 - -1 Declarations: pi diff --git a/doc/OnlineDocs/src/data/table7.dat b/doc/OnlineDocs/src/data/table7.dat deleted file mode 100644 index c3c0a4cf0da..00000000000 --- a/doc/OnlineDocs/src/data/table7.dat +++ /dev/null @@ -1,6 +0,0 @@ -table Z={A,B} M(A): -A B M N := -A1 B1 4.3 5.3 -A2 B2 4.4 5.4 -A3 B3 4.5 5.5 -; diff --git a/doc/OnlineDocs/src/data/table7.py b/doc/OnlineDocs/src/data/table7.py deleted file mode 100644 index 84a841aca86..00000000000 --- a/doc/OnlineDocs/src/data/table7.py +++ /dev/null @@ -1,21 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() - -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.M = Param(model.A) -model.Z = Set(dimen=2) - -instance = model.create_instance('table7.dat') -instance.pprint() diff --git a/doc/OnlineDocs/src/data/table7.txt b/doc/OnlineDocs/src/data/table7.txt deleted file mode 100644 index 8ddbfde38be..00000000000 --- a/doc/OnlineDocs/src/data/table7.txt +++ /dev/null @@ -1,16 +0,0 @@ -2 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - Z : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} - -1 Param Declarations - M : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - -3 Declarations: A M Z diff --git a/doc/OnlineDocs/src/dataportal/A.tab b/doc/OnlineDocs/src/dataportal/A.tab deleted file mode 100644 index d9c13cd6ac6..00000000000 --- a/doc/OnlineDocs/src/dataportal/A.tab +++ /dev/null @@ -1,4 +0,0 @@ -A -A1 -A2 -A3 diff --git a/doc/OnlineDocs/src/dataportal/C.tab b/doc/OnlineDocs/src/dataportal/C.tab deleted file mode 100644 index 14bcdc5e18d..00000000000 --- a/doc/OnlineDocs/src/dataportal/C.tab +++ /dev/null @@ -1,10 +0,0 @@ -A B -A1 1 -A1 2 -A1 3 -A2 1 -A2 2 -A2 3 -A3 1 -A3 2 -A3 3 diff --git a/doc/OnlineDocs/src/dataportal/D.tab b/doc/OnlineDocs/src/dataportal/D.tab deleted file mode 100644 index 965d28df3d0..00000000000 --- a/doc/OnlineDocs/src/dataportal/D.tab +++ /dev/null @@ -1,4 +0,0 @@ -B A1 A2 A3 -1 + - - -2 - + - -3 - - + diff --git a/doc/OnlineDocs/src/dataportal/PP.csv b/doc/OnlineDocs/src/dataportal/PP.csv deleted file mode 100644 index 3ca8d931ce5..00000000000 --- a/doc/OnlineDocs/src/dataportal/PP.csv +++ /dev/null @@ -1,4 +0,0 @@ -A,B,PP -A1,B1,4.3 -A2,B2,4.4 -A3,B3,4.5 diff --git a/doc/OnlineDocs/src/dataportal/PP.json b/doc/OnlineDocs/src/dataportal/PP.json deleted file mode 100644 index f008d202585..00000000000 --- a/doc/OnlineDocs/src/dataportal/PP.json +++ /dev/null @@ -1,9 +0,0 @@ -{ -"A": ["A1", "A2", "A3"], -"B": ["B1", "B2", "B3"], -"PP": [ - {"index":["A1","B1"], "value":4.3}, - {"index":["A2","B2"], "value":4.4}, - {"index":["A3","B3"], "value":4.5} - ] -} diff --git a/doc/OnlineDocs/src/dataportal/PP.sqlite b/doc/OnlineDocs/src/dataportal/PP.sqlite deleted file mode 100644 index ee74b3045328e745d2e2cdcc82040f6a944e3c0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3072 zcmWFz^vNtqRY=P(%1ta$FlJz3U}R))P*7lCVBi2^W+-L^(yTzt0>m%?5+ejS7<8Yp z@B&paGIujDcQcRZa(GVDtAt1oaA}Gl2XzXO{AO!J%025fm$jQh-a3%=M5?}<27&;j` X2pWR{l2@6K*rUwR5Eu=C5fB0Z=OH - - - - - - - - - - diff --git a/doc/OnlineDocs/src/dataportal/PP.yaml b/doc/OnlineDocs/src/dataportal/PP.yaml deleted file mode 100644 index 1b65496eff8..00000000000 --- a/doc/OnlineDocs/src/dataportal/PP.yaml +++ /dev/null @@ -1,15 +0,0 @@ -A: - - A1 - - A2 - - A3 -B: - - B1 - - B2 - - B3 -PP: - - index: [A1, B1] - value: 4.3 - - index: [A2, B2] - value: 4.4 - - index: [A3, B3] - value: 4.5 diff --git a/doc/OnlineDocs/src/dataportal/PP_sqlite.py b/doc/OnlineDocs/src/dataportal/PP_sqlite.py deleted file mode 100644 index 1592e820900..00000000000 --- a/doc/OnlineDocs/src/dataportal/PP_sqlite.py +++ /dev/null @@ -1,40 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -""" -Create the PP.sqlite file -""" - -import sqlite3 - -conn = sqlite3.connect('PP.sqlite') - -c = conn.cursor() - -for table in ['PPtable']: - c.execute('DROP TABLE IF EXISTS ' + table) -conn.commit() - -c.execute( - ''' -CREATE TABLE PPtable ( - A text not null, - B text not null, - PP float not null -) -''' -) -conn.commit() - -data = [("A1", "B1", 4.3), ("A2", "B2", 4.4), ("A3", "B3", 4.5)] -for row in data: - c.execute('''INSERT INTO PPtable VALUES (?,?,?)''', row) -conn.commit() diff --git a/doc/OnlineDocs/src/dataportal/Pyomo_mysql b/doc/OnlineDocs/src/dataportal/Pyomo_mysql deleted file mode 100644 index 1a131ef19ba..00000000000 --- a/doc/OnlineDocs/src/dataportal/Pyomo_mysql +++ /dev/null @@ -1,10 +0,0 @@ -DROP TABLE IF EXISTS PPTable; - -CREATE TABLE PPTable ( - A varchar(64) not null, - B varchar(64) not null, - PP float not null - ) engine=innodb ; - -INSERT INTO PPTable VALUES ("A1", "B1", 4.3), ("A2", "B2", 4.4), ("A3", "B3", 4.5); - diff --git a/doc/OnlineDocs/src/dataportal/S.tab b/doc/OnlineDocs/src/dataportal/S.tab deleted file mode 100644 index 14621cf16bc..00000000000 --- a/doc/OnlineDocs/src/dataportal/S.tab +++ /dev/null @@ -1,4 +0,0 @@ -A S -A1 3.3 -A2 . -A3 3.5 diff --git a/doc/OnlineDocs/src/dataportal/T.json b/doc/OnlineDocs/src/dataportal/T.json deleted file mode 100644 index 6fdca35b5cb..00000000000 --- a/doc/OnlineDocs/src/dataportal/T.json +++ /dev/null @@ -1,8 +0,0 @@ -{ "A": ["A1", "A2", "A3"], - "B": [[1, "B1"], [2, "B2"], [3, "B3"]], - "C": {"A1": [1, 2, 3], "A3": [10, 20, 30]}, - "p": 0.1, - "q": {"A1": 3.3, "A2": 3.4, "A3": 3.5}, - "r": [ {"index": [1, "B1"], "value": 3.3}, - {"index": [2, "B2"], "value": 3.4}, - {"index": [3, "B3"], "value": 3.5}]} diff --git a/doc/OnlineDocs/src/dataportal/T.yaml b/doc/OnlineDocs/src/dataportal/T.yaml deleted file mode 100644 index 2279304d944..00000000000 --- a/doc/OnlineDocs/src/dataportal/T.yaml +++ /dev/null @@ -1,17 +0,0 @@ -A: [A1, A2, A3] -B: -- [1, B1] -- [2, B2] -- [3, B3] -C: - 'A1': [1, 2, 3] - 'A3': [10, 20, 30] -p: 0.1 -q: {A1: 3.3, A2: 3.4, A3: 3.5} -r: -- index: [1, B1] - value: 3.3 -- index: [2, B2] - value: 3.4 -- index: [3, B3] - value: 3.5 diff --git a/doc/OnlineDocs/src/dataportal/U.tab b/doc/OnlineDocs/src/dataportal/U.tab deleted file mode 100644 index 27bba1b34da..00000000000 --- a/doc/OnlineDocs/src/dataportal/U.tab +++ /dev/null @@ -1,5 +0,0 @@ -I A1 A2 A3 -I1 1.3 2.3 3.3 -I2 1.4 2.4 3.4 -I3 1.5 2.5 3.5 -I4 1.6 2.6 3.6 diff --git a/doc/OnlineDocs/src/dataportal/XW.tab b/doc/OnlineDocs/src/dataportal/XW.tab deleted file mode 100644 index 63b46517d7e..00000000000 --- a/doc/OnlineDocs/src/dataportal/XW.tab +++ /dev/null @@ -1,4 +0,0 @@ -A X W -A1 3.3 4.3 -A2 3.4 4.4 -A3 3.5 4.5 diff --git a/doc/OnlineDocs/src/dataportal/Y.tab b/doc/OnlineDocs/src/dataportal/Y.tab deleted file mode 100644 index 926555713f0..00000000000 --- a/doc/OnlineDocs/src/dataportal/Y.tab +++ /dev/null @@ -1,4 +0,0 @@ -A Y -A1 3.3 -A2 3.4 -A3 3.5 diff --git a/doc/OnlineDocs/src/dataportal/Z.tab b/doc/OnlineDocs/src/dataportal/Z.tab deleted file mode 100644 index 9459d4ba2a0..00000000000 --- a/doc/OnlineDocs/src/dataportal/Z.tab +++ /dev/null @@ -1 +0,0 @@ -1.1 diff --git a/doc/OnlineDocs/src/dataportal/dataportal_tab.py b/doc/OnlineDocs/src/dataportal/dataportal_tab.py deleted file mode 100644 index 655329d31de..00000000000 --- a/doc/OnlineDocs/src/dataportal/dataportal_tab.py +++ /dev/null @@ -1,334 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -# -------------------------------------------------- -# @load -model = AbstractModel() -model.A = Set() -data = DataPortal() -data.load(filename='A.tab', set=model.A) -instance = model.create_instance(data) -# @load -instance.pprint() -# -------------------------------------------------- -# @set1 -model = AbstractModel() -model.A = Set() -data = DataPortal() -data.load(filename='A.tab', set=model.A) -instance = model.create_instance(data) -# @set1 -instance.pprint() -# -------------------------------------------------- -# @set2 -model = AbstractModel() -model.C = Set(dimen=2) -data = DataPortal() -data.load(filename='C.tab', set=model.C) -instance = model.create_instance(data) -# @set2 -instance.pprint() -# -------------------------------------------------- -# @set3 -model = AbstractModel() -model.D = Set(dimen=2) -data = DataPortal() -data.load(filename='D.tab', set=model.D, format='set_array') -instance = model.create_instance(data) -# @set3 -instance.pprint() - -# -------------------------------------------------- -# @param1 -model = AbstractModel() -data = DataPortal() -model.z = Param() -data.load(filename='Z.tab', param=model.z) -instance = model.create_instance(data) -# @param1 -instance.pprint() -# -------------------------------------------------- -# @param2 -model = AbstractModel() -data = DataPortal() -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.y = Param(model.A) -data.load(filename='Y.tab', param=model.y) -instance = model.create_instance(data) -# @param2 -instance.pprint() -# -------------------------------------------------- -# @param4 -model = AbstractModel() -data = DataPortal() -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.x = Param(model.A) -model.w = Param(model.A) -data.load(filename='XW.tab', param=(model.x, model.w)) -instance = model.create_instance(data) -# @param4 -instance.pprint() -# -------------------------------------------------- -# @param3 -model = AbstractModel() -data = DataPortal() -model.A = Set() -model.y = Param(model.A) -data.load(filename='Y.tab', param=model.y, index=model.A) -instance = model.create_instance(data) -# @param3 -instance.pprint() -# -------------------------------------------------- -# @param5 -model = AbstractModel() -data = DataPortal() -model.A = Set() -model.w = Param(model.A) -data.load(filename='XW.tab', select=('A', 'W'), param=model.w, index=model.A) -instance = model.create_instance(data) -# @param5 -instance.pprint() -# -------------------------------------------------- -# @param6 -model = AbstractModel() -data = DataPortal() -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.I = Set(initialize=['I1', 'I2', 'I3', 'I4']) -model.u = Param(model.I, model.A) -data.load(filename='U.tab', param=model.u, format='array') -instance = model.create_instance(data) -# @param6 -instance.pprint() -# -------------------------------------------------- -# @param7 -model = AbstractModel() -data = DataPortal() -model.A = Set(initialize=['A1', 'A2', 'A3']) -model.I = Set(initialize=['I1', 'I2', 'I3', 'I4']) -model.t = Param(model.A, model.I) -data.load(filename='U.tab', param=model.t, format='transposed_array') -instance = model.create_instance(data) -# @param7 -instance.pprint() -# -------------------------------------------------- -# @param8 -model = AbstractModel() -data = DataPortal() -model.A = Set() -model.s = Param(model.A) -data.load(filename='S.tab', param=model.s, index=model.A) -instance = model.create_instance(data) -# @param8 -instance.pprint() -# -------------------------------------------------- -# @param9 -model = AbstractModel() -data = DataPortal() -model.A = Set(initialize=['A1', 'A2', 'A3', 'A4']) -model.y = Param(model.A) -data.load(filename='Y.tab', param=model.y) -instance = model.create_instance(data) -# @param9 -instance.pprint() -# -------------------------------------------------- -# @param10 -model = AbstractModel() -data = DataPortal() -model.A = Set(dimen=2) -model.p = Param(model.A) -data.load(filename='PP.tab', param=model.p, index=model.A) -instance = model.create_instance(data) -# @param10 -instance.pprint() -# -------------------------------------------------- -# @param11 -model = AbstractModel() -data = DataPortal() -model.A = Set() -model.B = Set() -model.q = Param(model.A, model.B) -data.load(filename='PP.tab', param=model.q, index=(model.A, model.B)) -# instance = model.create_instance(data) -# @param11 -# -------------------------------------------------- -# @concrete1 -data = DataPortal() -data.load(filename='A.tab', set="A", format="set") - -model = ConcreteModel() -model.A = Set(initialize=data['A']) -# @concrete1 -model.pprint() -# -------------------------------------------------- -# @concrete2 -data = DataPortal() -data.load(filename='Z.tab', param="z", format="param") -data.load(filename='Y.tab', param="y", format="table") - -model = ConcreteModel() -model.z = Param(initialize=data['z']) -model.y = Param(['A1', 'A2', 'A3'], initialize=data['y']) -# @concrete2 -model.pprint() -# -------------------------------------------------- -# @getitem -data = DataPortal() -data.load(filename='A.tab', set="A", format="set") -print(data['A']) # ['A1', 'A2', 'A3'] - -data.load(filename='Z.tab', param="z", format="param") -print(data['z']) # 1.1 - -data.load(filename='Y.tab', param="y", format="table") -for key in sorted(data['y']): - print("%s %s" % (key, data['y'][key])) -# @getitem -# -------------------------------------------------- -# @excel1 -model = AbstractModel() -data = DataPortal() -model.A = Set(dimen=2) -model.p = Param(model.A) -data.load(filename='excel.xls', range='PPtable', param=model.p, index=model.A) -instance = model.create_instance(data) -# @excel1 -instance.pprint() -# -------------------------------------------------- -# @excel2 -model = AbstractModel() -data = DataPortal() -model.A = Set(dimen=2) -model.p = Param(model.A) -# data.load(filename='excel.xls', range='AX2:AZ5', -# param=model.p, index=model.A) -instance = model.create_instance(data) -# @excel2 -instance.pprint() -# -------------------------------------------------- -# @db1 -model = AbstractModel() -data = DataPortal() -model.A = Set(dimen=2) -model.p = Param(model.A) -data.load( - filename='PP.sqlite', using='sqlite3', table='PPtable', param=model.p, index=model.A -) -instance = model.create_instance(data) -# @db1 -data = DataPortal() -data.load( - filename='PP.sqlite', - using='sqlite3', - table='PPtable', - param=model.p, - index=model.A, - text_factory=str, -) -instance = model.create_instance(data) -instance.pprint() -# -------------------------------------------------- -# @db2 -model = AbstractModel() -data = DataPortal() -model.A = Set() -model.p = Param(model.A) -data.load( - filename='PP.sqlite', - using='sqlite3', - query="SELECT A,PP FROM PPtable", - param=model.p, - index=model.A, -) -instance = model.create_instance(data) -# @db2 -data = DataPortal() -data.load( - filename='PP.sqlite', - using='sqlite3', - query="SELECT A,PP FROM PPtable", - param=model.p, - index=model.A, - text_factory=str, -) -instance = model.create_instance(data) -instance.pprint() -# -------------------------------------------------- -# @db3 -if False: - model = AbstractModel() - data = DataPortal() - model.A = Set() - model.p = Param(model.A) - data.load( - filename="Driver={MySQL ODBC 5.2 UNICODE Driver}; Database=Pyomo; Server=localhost; User=pyomo;", - using='pypyodbc', - query="SELECT A,PP FROM PPtable", - param=model.p, - index=model.A, - ) - instance = model.create_instance(data) - # @db3 - data = DataPortal() - data.load( - filename="Driver={MySQL ODBC 5.2 UNICODE Driver}; Database=Pyomo; Server=localhost; User=pyomo;", - using='pypyodbc', - query="SELECT A,PP FROM PPtable", - param=model.p, - index=model.A, - text_factory=str, - ) - instance = model.create_instance(data) - instance.pprint() -# -------------------------------------------------- -# @json1 -model = AbstractModel() -data = DataPortal() -model.A = Set() -model.B = Set(dimen=2) -model.C = Set(model.A) -model.p = Param() -model.q = Param(model.A) -model.r = Param(model.B) -data.load(filename='T.json') -# @json1 -data = DataPortal() -data.load(filename='T.json', convert_unicode=True) -instance = model.create_instance(data) -instance.pprint() -# -------------------------------------------------- -# @yaml1 -model = AbstractModel() -data = DataPortal() -model.A = Set() -model.B = Set(dimen=2) -model.C = Set(model.A) -model.p = Param() -model.q = Param(model.A) -model.r = Param(model.B) -data.load(filename='T.yaml') -# @yaml1 -instance = model.create_instance(data) -instance.pprint() -# -------------------------------------------------- - -# @namespaces1 -model = AbstractModel() -model.C = Set(dimen=2) -data = DataPortal() -data.load(filename='C.tab', set=model.C, namespace='ns1') -data.load(filename='D.tab', set=model.C, namespace='ns2', format='set_array') -instance1 = model.create_instance(data, namespaces=['ns1']) -instance2 = model.create_instance(data, namespaces=['ns2']) -# @namespaces1 -instance1.pprint() -instance2.pprint() diff --git a/doc/OnlineDocs/src/dataportal/dataportal_tab.txt b/doc/OnlineDocs/src/dataportal/dataportal_tab.txt deleted file mode 100644 index a23c63d90c9..00000000000 --- a/doc/OnlineDocs/src/dataportal/dataportal_tab.txt +++ /dev/null @@ -1,315 +0,0 @@ -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - -1 Declarations: A -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - -1 Declarations: A -1 Set Declarations - C : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} - -1 Declarations: C -1 Set Declarations - D : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 3 : {('A1', 1), ('A2', 2), ('A3', 3)} - -1 Declarations: D -1 Param Declarations - z : Size=1, Index=None, Domain=Any, Default=None, Mutable=False - Key : Value - None : 1.1 - -1 Declarations: z -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - -1 Param Declarations - y : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A2 : 3.4 - A3 : 3.5 - -2 Declarations: A y -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - -2 Param Declarations - w : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - x : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A2 : 3.4 - A3 : 3.5 - -3 Declarations: A x w -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - -1 Param Declarations - y : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A2 : 3.4 - A3 : 3.5 - -2 Declarations: A y -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - -1 Param Declarations - w : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - -2 Declarations: A w -2 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - I : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 4 : {'I1', 'I2', 'I3', 'I4'} - -1 Param Declarations - u : Size=12, Index=I*A, Domain=Any, Default=None, Mutable=False - Key : Value - ('I1', 'A1') : 1.3 - ('I1', 'A2') : 2.3 - ('I1', 'A3') : 3.3 - ('I2', 'A1') : 1.4 - ('I2', 'A2') : 2.4 - ('I2', 'A3') : 3.4 - ('I3', 'A1') : 1.5 - ('I3', 'A2') : 2.5 - ('I3', 'A3') : 3.5 - ('I4', 'A1') : 1.6 - ('I4', 'A2') : 2.6 - ('I4', 'A3') : 3.6 - -3 Declarations: A I u -2 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - I : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 4 : {'I1', 'I2', 'I3', 'I4'} - -1 Param Declarations - t : Size=12, Index=A*I, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'I1') : 1.3 - ('A1', 'I2') : 1.4 - ('A1', 'I3') : 1.5 - ('A1', 'I4') : 1.6 - ('A2', 'I1') : 2.3 - ('A2', 'I2') : 2.4 - ('A2', 'I3') : 2.5 - ('A2', 'I4') : 2.6 - ('A3', 'I1') : 3.3 - ('A3', 'I2') : 3.4 - ('A3', 'I3') : 3.5 - ('A3', 'I4') : 3.6 - -3 Declarations: A I t -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - -1 Param Declarations - s : Size=2, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A3 : 3.5 - -2 Declarations: A s -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 4 : {'A1', 'A2', 'A3', 'A4'} - -1 Param Declarations - y : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A2 : 3.4 - A3 : 3.5 - -2 Declarations: A y -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} - -1 Param Declarations - p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'B1') : 4.3 - ('A2', 'B2') : 4.4 - ('A3', 'B3') : 4.5 - -2 Declarations: A p -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - -1 Declarations: A - -2 Param Declarations - y : Size=3, Index={A1, A2, A3}, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A2 : 3.4 - A3 : 3.5 - z : Size=1, Index=None, Domain=Any, Default=None, Mutable=False - Key : Value - None : 1.1 - -2 Declarations: z y -['A1', 'A2', 'A3'] -1.1 -A1 3.3 -A2 3.4 -A3 3.5 -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} - -1 Param Declarations - p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'B1') : 4.3 - ('A2', 'B2') : 4.4 - ('A3', 'B3') : 4.5 - -2 Declarations: A p -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 0 : {} - -1 Param Declarations - p : Size=0, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - -2 Declarations: A p -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 3 : {('A1', 'B1'), ('A2', 'B2'), ('A3', 'B3')} - -1 Param Declarations - p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - ('A1', 'B1') : 4.3 - ('A2', 'B2') : 4.4 - ('A3', 'B3') : 4.5 - -2 Declarations: A p -1 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - -1 Param Declarations - p : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 4.3 - A2 : 4.4 - A3 : 4.5 - -2 Declarations: A p -3 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - B : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 3 : {(1, 'B1'), (2, 'B2'), (3, 'B3')} - C : Size=2, Index=A, Ordered=Insertion - Key : Dimen : Domain : Size : Members - A1 : 1 : Any : 3 : {1, 2, 3} - A3 : 1 : Any : 3 : {10, 20, 30} - -3 Param Declarations - p : Size=1, Index=None, Domain=Any, Default=None, Mutable=False - Key : Value - None : 0.1 - q : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A2 : 3.4 - A3 : 3.5 - r : Size=3, Index=B, Domain=Any, Default=None, Mutable=False - Key : Value - (1, 'B1') : 3.3 - (2, 'B2') : 3.4 - (3, 'B3') : 3.5 - -6 Declarations: A B C p q r -3 Set Declarations - A : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 3 : {'A1', 'A2', 'A3'} - B : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 3 : {(1, 'B1'), (2, 'B2'), (3, 'B3')} - C : Size=2, Index=A, Ordered=Insertion - Key : Dimen : Domain : Size : Members - A1 : 1 : Any : 3 : {1, 2, 3} - A3 : 1 : Any : 3 : {10, 20, 30} - -3 Param Declarations - p : Size=1, Index=None, Domain=Any, Default=None, Mutable=False - Key : Value - None : 0.1 - q : Size=3, Index=A, Domain=Any, Default=None, Mutable=False - Key : Value - A1 : 3.3 - A2 : 3.4 - A3 : 3.5 - r : Size=3, Index=B, Domain=Any, Default=None, Mutable=False - Key : Value - (1, 'B1') : 3.3 - (2, 'B2') : 3.4 - (3, 'B3') : 3.5 - -6 Declarations: A B C p q r -1 Set Declarations - C : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 9 : {('A1', 1), ('A1', 2), ('A1', 3), ('A2', 1), ('A2', 2), ('A2', 3), ('A3', 1), ('A3', 2), ('A3', 3)} - -1 Declarations: C -1 Set Declarations - C : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 2 : Any : 3 : {('A1', 1), ('A2', 2), ('A3', 3)} - -1 Declarations: C diff --git a/doc/OnlineDocs/src/dataportal/excel.xls b/doc/OnlineDocs/src/dataportal/excel.xls deleted file mode 100644 index 5b5e4daa14307cf516d913e7a7b896e92fac9c33..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17920 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 0 else M.x[i] for i in range(5)) -# @warning -print(e) - -# --------------------------------------------- -# @sum_product1 -M = ConcreteModel() -M.z = RangeSet(5) -M.x = Var(range(10)) -M.y = Var(range(10)) - -# Sum the elements of x -e1 = sum_product(M.x) - -# Sum the product of elements in x and y -e2 = sum_product(M.x, M.y) - -# Sum the product of elements in x and y, over the index set z -e3 = sum_product(M.x, M.y, index=M.z) -# @sum_product1 -print(e1) -print(e2) -print(e3) - -# --------------------------------------------- -# @sum_product2 -# Sum the product of x_i/y_i -e1 = sum_product(M.x, denom=M.y) - -# Sum the product of 1/(x_i*y_i) -e2 = sum_product(denom=(M.x, M.y)) -# @sum_product2 -print(e1) -print(e2) diff --git a/doc/OnlineDocs/src/expr/performance.txt b/doc/OnlineDocs/src/expr/performance.txt deleted file mode 100644 index 6bfd0bd1d5a..00000000000 --- a/doc/OnlineDocs/src/expr/performance.txt +++ /dev/null @@ -1,14 +0,0 @@ -x[0] + x[1] + x[2] + x[3] + x[4] -x[0] + x[1] + x[2] + x[3] + x[4] -(x[0] + x[1] + x[2] + x[3] + x[4])**2 -x[0]*x[1]*x[2]*x[3]*x[4] -x[0]*z -(x[0] + x[1] + x[2] + x[3] + x[4])*z -x[0]**2 + x[1]**2 + x[2]**2 + x[3]**2 + x[4]**2 -x[0]**2 + x[1]**2 + x[2]**2 + x[3]**2 + x[4]**2 -x[0] + x[1]**2 + x[2]**2 + x[3]**2 + x[4]**2 -x[0] + x[1] + x[2] + x[3] + x[4] + x[5] + x[6] + x[7] + x[8] + x[9] -x[0]*y[0] + x[1]*y[1] + x[2]*y[2] + x[3]*y[3] + x[4]*y[4] + x[5]*y[5] + x[6]*y[6] + x[7]*y[7] + x[8]*y[8] + x[9]*y[9] -x[1]*y[1] + x[2]*y[2] + x[3]*y[3] + x[4]*y[4] + x[5]*y[5] -x[0]/y[0] + x[1]/y[1] + x[2]/y[2] + x[3]/y[3] + x[4]/y[4] + x[5]/y[5] + x[6]/y[6] + x[7]/y[7] + x[8]/y[8] + x[9]/y[9] -1/(x[0]*y[0]) + 1/(x[1]*y[1]) + 1/(x[2]*y[2]) + 1/(x[3]*y[3]) + 1/(x[4]*y[4]) + 1/(x[5]*y[5]) + 1/(x[6]*y[6]) + 1/(x[7]*y[7]) + 1/(x[8]*y[8]) + 1/(x[9]*y[9]) diff --git a/doc/OnlineDocs/src/expr/quicksum.log b/doc/OnlineDocs/src/expr/quicksum.log deleted file mode 100644 index 11e1f203654..00000000000 --- a/doc/OnlineDocs/src/expr/quicksum.log +++ /dev/null @@ -1,4 +0,0 @@ -sum: 1.447861 -repn: 0.870225 -quicksum: 1.388344 -repn: 0.864316 diff --git a/doc/OnlineDocs/src/expr/quicksum.py b/doc/OnlineDocs/src/expr/quicksum.py deleted file mode 100644 index 1b6cd3f9909..00000000000 --- a/doc/OnlineDocs/src/expr/quicksum.py +++ /dev/null @@ -1,38 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * -from pyomo.repn import generate_standard_repn -import time - -# @runtime -M = ConcreteModel() -M.A = RangeSet(100000) -M.p = Param(M.A, mutable=True, initialize=1) -M.x = Var(M.A) - -start = time.time() -e = sum((M.x[i] - 1) ** M.p[i] for i in M.A) -print("sum: %f" % (time.time() - start)) - -start = time.time() -generate_standard_repn(e) -print("repn: %f" % (time.time() - start)) - -start = time.time() -e = quicksum((M.x[i] - 1) ** M.p[i] for i in M.A) -print("quicksum: %f" % (time.time() - start)) - -start = time.time() -generate_standard_repn(e) -print("repn: %f" % (time.time() - start)) - -# @runtime diff --git a/doc/OnlineDocs/src/kernel/examples.sh b/doc/OnlineDocs/src/kernel/examples.sh deleted file mode 100755 index 0ac9e1a0fbf..00000000000 --- a/doc/OnlineDocs/src/kernel/examples.sh +++ /dev/null @@ -1,3 +0,0 @@ -#! /bin/bash - -for file in `ls ../../library_reference/kernel/examples/*.py | sort`; do python $file; done; diff --git a/doc/OnlineDocs/src/kernel/examples.txt b/doc/OnlineDocs/src/kernel/examples.txt deleted file mode 100644 index 8ba072d28b1..00000000000 --- a/doc/OnlineDocs/src/kernel/examples.txt +++ /dev/null @@ -1,211 +0,0 @@ -1 Set Declarations - s : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {1, 2} - -1 RangeSet Declarations - q : Dimen=1, Size=3, Bounds=(1, 3) - Key : Finite : Members - None : True : [1:3] - -2 Param Declarations - p : Size=1, Index=None, Domain=Any, Default=None, Mutable=True - Key : Value - None : 0 - pd : Size=2, Index=s, Domain=Any, Default=None, Mutable=True - Key : Value - 1 : 0 - 2 : 1 - -4 Var Declarations - f : Size=1, Index=None - Key : Lower : Value : Upper : Fixed : Stale : Domain - None : None : None : None : False : True : Reals - v : Size=1, Index=None - Key : Lower : Value : Upper : Fixed : Stale : Domain - None : 1 : 1.0 : 4 : False : False : Reals - vd : Size=2, Index=s - Key : Lower : Value : Upper : Fixed : Stale : Domain - 1 : None : None : 9 : False : True : Reals - 2 : None : None : 9 : False : True : Reals - vl : Size=3, Index={1, 2, 3} - Key : Lower : Value : Upper : Fixed : Stale : Domain - 1 : 1 : None : None : False : True : Reals - 2 : 2 : None : None : False : True : Reals - 3 : 3 : None : None : False : True : Reals - -2 Expression Declarations - e : Size=1, Index=None - Key : Expression - None : - v - ed : Size=2, Index=s - Key : Expression - 1 : - vd[1] - 2 : - vd[2] - -3 Objective Declarations - o : Size=1, Index=None, Active=True - Key : Active : Sense : Expression - None : True : minimize : - v - od : Size=2, Index=s, Active=True - Key : Active : Sense : Expression - 1 : True : minimize : - vd[1] - 2 : True : minimize : - vd[2] - ol : Size=3, Index={1, 2, 3}, Active=True - Key : Active : Sense : Expression - 1 : True : minimize : - vl[1] - 2 : True : minimize : - vl[2] - 3 : True : minimize : - vl[3] - -3 Constraint Declarations - c : Size=1, Index=None, Active=True - Key : Lower : Body : Upper : Active - None : -Inf : vd[1] + vd[2] : 9.0 : True - cd : Size=6, Index=s*q, Active=True - Key : Lower : Body : Upper : Active - (1, 1) : 1.0 : vd[1] : 1.0 : True - (1, 2) : 2.0 : vd[1] : 2.0 : True - (1, 3) : 3.0 : vd[1] : 3.0 : True - (2, 1) : 1.0 : vd[2] : 1.0 : True - (2, 2) : 2.0 : vd[2] : 2.0 : True - (2, 3) : 3.0 : vd[2] : 3.0 : True - cl : Size=3, Index={1, 2, 3}, Active=True - Key : Lower : Body : Upper : Active - 1 : -5.0 : vl[1] - v : 5.0 : True - 2 : -5.0 : vl[2] - v : 5.0 : True - 3 : -5.0 : vl[3] - v : 5.0 : True - -3 SOSConstraint Declarations - sd : Size=2 Index= OrderedScalarSet - 1 - Type=1 - Weight : Variable - 1 : vd[1] - 2 : vd[2] - 2 - Type=1 - Weight : Variable - 1 : vl[1] - 2 : vl[2] - 3 : vl[3] - sos1 : Size=1 - Type=1 - Weight : Variable - 1 : vl[1] - 2 : vl[2] - 3 : vl[3] - sos2 : Size=1 - Type=2 - Weight : Variable - 1 : vd[1] - 2 : vd[2] - -2 Block Declarations - b : Size=1, Index=None, Active=True - 0 Declarations: - pw : Size=1, Index=None, Active=True - 1 Var Declarations - SOS2_y : Size=4, Index={0, 1, 2, 3} - Key : Lower : Value : Upper : Fixed : Stale : Domain - 0 : 0 : None : None : False : True : NonNegativeReals - 1 : 0 : None : None : False : True : NonNegativeReals - 2 : 0 : None : None : False : True : NonNegativeReals - 3 : 0 : None : None : False : True : NonNegativeReals - - 1 Constraint Declarations - SOS2_constraint : Size=3, Index={1, 2, 3}, Active=True - Key : Lower : Body : Upper : Active - 1 : 0.0 : v - (pw.SOS2_y[0] + 2*pw.SOS2_y[1] + 3*pw.SOS2_y[2] + 4*pw.SOS2_y[3]) : 0.0 : True - 2 : 0.0 : f - (pw.SOS2_y[0] + 2*pw.SOS2_y[1] + pw.SOS2_y[2] + 2*pw.SOS2_y[3]) : 0.0 : True - 3 : 1.0 : pw.SOS2_y[0] + pw.SOS2_y[1] + pw.SOS2_y[2] + pw.SOS2_y[3] : 1.0 : True - - 1 SOSConstraint Declarations - SOS2_sosconstraint : Size=1 - Type=2 - Weight : Variable - 1 : pw.SOS2_y[0] - 2 : pw.SOS2_y[1] - 3 : pw.SOS2_y[2] - 4 : pw.SOS2_y[3] - - 3 Declarations: SOS2_y SOS2_constraint SOS2_sosconstraint - -1 Suffix Declarations - dual : Direction=IMPORT, Datatype=FLOAT - Key : Value - -22 Declarations: b s q p pd v vd vl c cd cl e ed o od ol sos1 sos2 sd dual f pw -: block(active=True, ctype=IBlock) - - b: block(active=True, ctype=IBlock) - - p: parameter(active=True, value=0) - - pd: parameter_dict(active=True, ctype=IParameter) - - pd[1]: parameter(active=True, value=0) - - pd[2]: parameter(active=True, value=1) - - pl: parameter_list(active=True, ctype=IParameter) - - pl[0]: parameter(active=True, value=0) - - pl[1]: parameter(active=True, value=1) - - pl[2]: parameter(active=True, value=2) - - v: variable(active=True, value=1, bounds=(1,4), domain_type=RealSet, fixed=False, stale=True) - - vd: variable_dict(active=True, ctype=IVariable) - - vd[1]: variable(active=True, value=None, bounds=(None,9), domain_type=RealSet, fixed=False, stale=True) - - vd[2]: variable(active=True, value=None, bounds=(None,9), domain_type=RealSet, fixed=False, stale=True) - - vl: variable_list(active=True, ctype=IVariable) - - vl[0]: variable(active=True, value=None, bounds=(2,None), domain_type=RealSet, fixed=False, stale=True) - - vl[1]: variable(active=True, value=None, bounds=(2,None), domain_type=RealSet, fixed=False, stale=True) - - vl[2]: variable(active=True, value=None, bounds=(2,None), domain_type=RealSet, fixed=False, stale=True) - - c: constraint(active=True, expr=vd[1] + vd[2] <= 9) - - cd: constraint_dict(active=True, ctype=IConstraint) - - cd[(1, 0)]: constraint(active=True, expr=vd[1] == 0) - - cd[(1, 1)]: constraint(active=True, expr=vd[1] == 1) - - cd[(1, 2)]: constraint(active=True, expr=vd[1] == 2) - - cd[(2, 0)]: constraint(active=True, expr=vd[2] == 0) - - cd[(2, 1)]: constraint(active=True, expr=vd[2] == 1) - - cd[(2, 2)]: constraint(active=True, expr=vd[2] == 2) - - cl: constraint_list(active=True, ctype=IConstraint) - - cl[0]: constraint(active=True, expr=-5 <= vl[0] - v <= 5) - - cl[1]: constraint(active=True, expr=-5 <= vl[1] - v <= 5) - - cl[2]: constraint(active=True, expr=-5 <= vl[2] - v <= 5) - - e: expression(active=True, expr=- v) - - ed: expression_dict(active=True, ctype=IExpression) - - ed[1]: expression(active=True, expr=- vd[1]) - - ed[2]: expression(active=True, expr=- vd[2]) - - el: expression_list(active=True, ctype=IExpression) - - el[0]: expression(active=True, expr=- vl[0]) - - el[1]: expression(active=True, expr=- vl[1]) - - el[2]: expression(active=True, expr=- vl[2]) - - o: objective(active=True, expr=- v) - - od: objective_dict(active=True, ctype=IObjective) - - od[1]: objective(active=True, expr=- vd[1]) - - od[2]: objective(active=True, expr=- vd[2]) - - ol: objective_list(active=True, ctype=IObjective) - - ol[0]: objective(active=True, expr=- vl[0]) - - ol[1]: objective(active=True, expr=- vl[1]) - - ol[2]: objective(active=True, expr=- vl[2]) - - sos1: sos(active=True, level=1, entries=['(vd[1],1)', '(vd[2],2)']) - - sos2: sos(active=True, level=2, entries=['(vl[0],1)', '(vl[1],2)', '(vl[2],3)']) - - sd: sos_dict(active=True, ctype=ISOS) - - sd[1]: sos(active=True, level=1, entries=['(vd[1],1)', '(vd[2],2)']) - - sd[2]: sos(active=True, level=1, entries=['(vl[0],1)', '(vl[1],2)', '(vl[2],3)']) - - sl: sos_list(active=True, ctype=ISOS) - - sl[0]: sos(active=True, level=1, entries=['(vl[1],1)', '(vd[1],2)']) - - sl[1]: sos(active=True, level=1, entries=['(vl[2],1)', '(vd[2],2)']) - - dual: suffix(active=True, size=0) - - suffixes: suffix_dict(active=True, ctype=ISuffix) - - suffixes[dual]: suffix(active=True, size=0) - - f: variable(active=True, value=None, bounds=(None,None), domain_type=RealSet, fixed=False, stale=True) - - pw: piecewise_sos2(active=True, ctype=IBlock) - - pw._inout: expression_tuple(active=True, ctype=IExpression) - - pw._inout[0]: expression(active=True, expr=v) - - pw._inout[1]: expression(active=True, expr=f) - - pw.v: variable_tuple(active=True, ctype=IVariable) - - pw.v[0]: variable(active=True, value=None, bounds=(0,None), domain_type=RealSet, fixed=False, stale=True) - - pw.v[1]: variable(active=True, value=None, bounds=(0,None), domain_type=RealSet, fixed=False, stale=True) - - pw.v[2]: variable(active=True, value=None, bounds=(0,None), domain_type=RealSet, fixed=False, stale=True) - - pw.v[3]: variable(active=True, value=None, bounds=(0,None), domain_type=RealSet, fixed=False, stale=True) - - pw.c: constraint_list(active=True, ctype=IConstraint) - - pw.c[0]: linear_constraint(active=True, expr=pw.v[0] + 2*pw.v[1] + 3*pw.v[2] + 4*pw.v[3] - v == 0) - - pw.c[1]: linear_constraint(active=True, expr=pw.v[0] + 2*pw.v[1] + pw.v[2] + 2*pw.v[3] - f == 0) - - pw.c[2]: linear_constraint(active=True, expr=pw.v[0] + pw.v[1] + pw.v[2] + pw.v[3] == 1) - - pw.s: sos(active=True, level=2, entries=['(pw.v[0],1)', '(pw.v[1],2)', '(pw.v[2],3)', '(pw.v[3],4)']) -Memory: 1.9 KB -Memory: 9.7 KB diff --git a/doc/OnlineDocs/src/scripting/AbstractSuffixes.py b/doc/OnlineDocs/src/scripting/AbstractSuffixes.py deleted file mode 100644 index 1c064042c6b..00000000000 --- a/doc/OnlineDocs/src/scripting/AbstractSuffixes.py +++ /dev/null @@ -1,35 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = AbstractModel() -model.I = RangeSet(1, 4) -model.x = Var(model.I) - - -def c_rule(m, i): - return m.x[i] >= i - - -model.c = Constraint(model.I, rule=c_rule) - - -def foo_rule(m): - return ((m.x[i], 3.0 * i) for i in m.I) - - -model.foo = Suffix(rule=foo_rule) - -# instantiate the model -inst = model.create_instance() -for i in inst.I: - print(i, inst.foo[inst.x[i]]) diff --git a/doc/OnlineDocs/src/scripting/Isinglebuild.py b/doc/OnlineDocs/src/scripting/Isinglebuild.py deleted file mode 100644 index 344f8905a4a..00000000000 --- a/doc/OnlineDocs/src/scripting/Isinglebuild.py +++ /dev/null @@ -1,58 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# Isinglebuild.py -# NodesIn and NodesOut are created by a build action using the Arcs -from pyomo.environ import * - -model = AbstractModel() - -model.Nodes = Set() -model.Arcs = Set(dimen=2) - -model.NodesOut = Set(model.Nodes, within=model.Nodes, initialize=[]) -model.NodesIn = Set(model.Nodes, within=model.Nodes, initialize=[]) - - -def Populate_In_and_Out(model): - # loop over the arcs and put the end points in the appropriate places - for i, j in model.Arcs: - model.NodesIn[j].add(i) - model.NodesOut[i].add(j) - - -model.In_n_Out = BuildAction(rule=Populate_In_and_Out) - -model.Flow = Var(model.Arcs, domain=NonNegativeReals) -model.FlowCost = Param(model.Arcs) - -model.Demand = Param(model.Nodes) -model.Supply = Param(model.Nodes) - - -def Obj_rule(model): - return summation(model.FlowCost, model.Flow) - - -model.Obj = Objective(rule=Obj_rule, sense=minimize) - - -def FlowBalance_rule(model, node): - return ( - model.Supply[node] - + sum(model.Flow[i, node] for i in model.NodesIn[node]) - - model.Demand[node] - - sum(model.Flow[node, j] for j in model.NodesOut[node]) - == 0 - ) - - -model.FlowBalance = Constraint(model.Nodes, rule=FlowBalance_rule) diff --git a/doc/OnlineDocs/src/scripting/Isinglecomm.dat b/doc/OnlineDocs/src/scripting/Isinglecomm.dat deleted file mode 100644 index 18a586577e4..00000000000 --- a/doc/OnlineDocs/src/scripting/Isinglecomm.dat +++ /dev/null @@ -1,25 +0,0 @@ -set Nodes := CityA CityB CityC ; - -set Arcs := -CityA CityB -CityA CityC -CityC CityB -; - -param : FlowCost := -CityA CityB 1.4 -CityA CityC 2.7 -CityC CityB 1.6 - ; - -param Demand := -CityA 0 -CityB 1 -CityC 1 -; - -param Supply := -CityA 2 -CityB 0 -CityC 0 -; diff --git a/doc/OnlineDocs/src/scripting/NodesIn_init.py b/doc/OnlineDocs/src/scripting/NodesIn_init.py deleted file mode 100644 index c17b70150bc..00000000000 --- a/doc/OnlineDocs/src/scripting/NodesIn_init.py +++ /dev/null @@ -1,21 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - - -def NodesIn_init(model, node): - retval = [] - for i, j in model.Arcs: - if j == node: - retval.append(i) - return retval - - -model.NodesIn = Set(model.Nodes, initialize=NodesIn_init) diff --git a/doc/OnlineDocs/src/scripting/Z_init.py b/doc/OnlineDocs/src/scripting/Z_init.py deleted file mode 100644 index 1dd2843f4f0..00000000000 --- a/doc/OnlineDocs/src/scripting/Z_init.py +++ /dev/null @@ -1,19 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - - -def Z_init(model, i): - if i > 10: - return Set.End - return 2 * i + 1 - - -model.Z = Set(initialize=Z_init) diff --git a/doc/OnlineDocs/src/scripting/abstract1.dat b/doc/OnlineDocs/src/scripting/abstract1.dat deleted file mode 100644 index d161640d10d..00000000000 --- a/doc/OnlineDocs/src/scripting/abstract1.dat +++ /dev/null @@ -1,18 +0,0 @@ -# one way to input the data in AMPL format -# for indexed parameters, the indexes are given before the value - -param m := 1 ; -param n := 2 ; - -param a := -1 1 3 -1 2 4 -; - -param c:= -1 2 -2 3 -; - -param b := 1 1 ; - diff --git a/doc/OnlineDocs/src/scripting/abstract2.dat b/doc/OnlineDocs/src/scripting/abstract2.dat deleted file mode 100644 index f865fd4f8ec..00000000000 --- a/doc/OnlineDocs/src/scripting/abstract2.dat +++ /dev/null @@ -1,22 +0,0 @@ -# abstract2.dat AMPL data format - -set I := TV Film ; -set J := Graham John Carol ; - -param a := -TV Graham 3 -TV John 4.4 -TV Carol 4.9 -Film Graham 1 -Film John 2.4 -Film Carol 1.1 -; - -param c := [*] - Graham 2.2 - John 3.1416 - Carol 3 -; - -param b := TV 1 Film 1 ; - diff --git a/doc/OnlineDocs/src/scripting/abstract2.py b/doc/OnlineDocs/src/scripting/abstract2.py deleted file mode 100644 index 544399a8a42..00000000000 --- a/doc/OnlineDocs/src/scripting/abstract2.py +++ /dev/null @@ -1,43 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# abstract2.py - - -from pyomo.environ import * - -model = AbstractModel() - -model.I = Set() -model.J = Set() - -model.a = Param(model.I, model.J) -model.b = Param(model.I) -model.c = Param(model.J) - -# the next line declares a variable indexed by the set J -model.x = Var(model.J, domain=NonNegativeReals) - - -def obj_expression(model): - return summation(model.c, model.x) - - -model.OBJ = Objective(rule=obj_expression) - - -def ax_constraint_rule(model, i): - # return the expression for the constraint for i - return sum(model.a[i, j] * model.x[j] for j in model.J) >= model.b[i] - - -# the next line creates one constraint for each member of the set model.I -model.AxbConstraint = Constraint(model.I, rule=ax_constraint_rule) diff --git a/doc/OnlineDocs/src/scripting/abstract2a.dat b/doc/OnlineDocs/src/scripting/abstract2a.dat deleted file mode 100644 index 9d7849daeef..00000000000 --- a/doc/OnlineDocs/src/scripting/abstract2a.dat +++ /dev/null @@ -1,16 +0,0 @@ -# abstract2a.dat AMPL format - -set I := 1 ; -set J := 1 2 ; - -param a := -1 1 3 -1 2 4 -; - -param c:= -1 2 -2 3 -; - -param b := 1 1 ; diff --git a/doc/OnlineDocs/src/scripting/abstract2piece.py b/doc/OnlineDocs/src/scripting/abstract2piece.py deleted file mode 100644 index 03c5139004e..00000000000 --- a/doc/OnlineDocs/src/scripting/abstract2piece.py +++ /dev/null @@ -1,62 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# abstract2piece.py -# Similar to abstract2.py, but the objective is now c times x to the fourth power - -from pyomo.environ import * - -model = AbstractModel() - -model.I = Set() -model.J = Set() - -Topx = 6.1 # range of x variables - -model.a = Param(model.I, model.J) -model.b = Param(model.I) -model.c = Param(model.J) - -# the next line declares a variable indexed by the set J -model.x = Var(model.J, domain=NonNegativeReals, bounds=(0, Topx)) -model.y = Var(model.J, domain=NonNegativeReals) - -# to avoid warnings, we set breakpoints at or beyond the bounds -PieceCnt = 100 -bpts = [] -for i in range(PieceCnt + 2): - bpts.append(float((i * Topx) / PieceCnt)) - - -def f4(model, j, xp): - # we not need j, but it is passed as the index for the constraint - return xp**4 - - -model.ComputeObj = Piecewise( - model.J, model.y, model.x, pw_pts=bpts, pw_constr_type='EQ', f_rule=f4 -) - - -def obj_expression(model): - return summation(model.c, model.y) - - -model.OBJ = Objective(rule=obj_expression) - - -def ax_constraint_rule(model, i): - # return the expression for the constraint for i - return sum(model.a[i, j] * model.x[j] for j in model.J) >= model.b[i] - - -# the next line creates one constraint for each member of the set model.I -model.AxbConstraint = Constraint(model.I, rule=ax_constraint_rule) diff --git a/doc/OnlineDocs/src/scripting/abstract2piecebuild.py b/doc/OnlineDocs/src/scripting/abstract2piecebuild.py deleted file mode 100644 index d454d7fbc79..00000000000 --- a/doc/OnlineDocs/src/scripting/abstract2piecebuild.py +++ /dev/null @@ -1,77 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# abstract2piecebuild.py -# Similar to abstract2piece.py, but the breakpoints are created using a build action - -from pyomo.environ import * - -model = AbstractModel() - -model.I = Set() -model.J = Set() - -model.a = Param(model.I, model.J) -model.b = Param(model.I) -model.c = Param(model.J) - -model.Topx = Param(default=6.1) # range of x variables -model.PieceCnt = Param(default=100) - -# the next line declares a variable indexed by the set J -model.x = Var(model.J, domain=NonNegativeReals, bounds=(0, model.Topx)) -model.y = Var(model.J, domain=NonNegativeReals) - -# to avoid warnings, we set breakpoints beyond the bounds -# we are using a dictionary so that we can have different -# breakpoints for each index. But we won't. -model.bpts = {} - - -# @Function_valid_declaration -def bpts_build(model, j): - # @Function_valid_declaration - model.bpts[j] = [] - for i in range(model.PieceCnt + 2): - model.bpts[j].append(float((i * model.Topx) / model.PieceCnt)) - - -# The object model.BuildBpts is not referred to again; -# the only goal is to trigger the action at build time -# @BuildAction_example -model.BuildBpts = BuildAction(model.J, rule=bpts_build) -# @BuildAction_example - - -def f4(model, j, xp): - # we not need j in this example, but it is passed as the index for the constraint - return xp**4 - - -model.ComputePieces = Piecewise( - model.J, model.y, model.x, pw_pts=model.bpts, pw_constr_type='EQ', f_rule=f4 -) - - -def obj_expression(model): - return summation(model.c, model.y) - - -model.OBJ = Objective(rule=obj_expression) - - -def ax_constraint_rule(model, i): - # return the expression for the constraint for i - return sum(model.a[i, j] * model.x[j] for j in model.J) >= model.b[i] - - -# the next line creates one constraint for each member of the set model.I -model.AxbConstraint = Constraint(model.I, rule=ax_constraint_rule) diff --git a/doc/OnlineDocs/src/scripting/block_iter_example.py b/doc/OnlineDocs/src/scripting/block_iter_example.py deleted file mode 100644 index 10c8a4ea43d..00000000000 --- a/doc/OnlineDocs/src/scripting/block_iter_example.py +++ /dev/null @@ -1,51 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# written by jds, adapted for doc by dlw -from pyomo.environ import * - -# simple way to get arbitrary, unique values for each thing -val_iter = 0 - - -def get_val(*args, **kwds): - global val_iter - val_iter += 1 - return val_iter - - -model = ConcreteModel() -model.I = RangeSet(3) -model.x = Var(initialize=get_val) -model.y = Var(model.I, initialize=get_val) - -model.b = Block() -model.b.a = Var(initialize=get_val) -model.b.b = Var(model.I, initialize=get_val) - - -def c_rule(b, i): - b.c = Var(initialize=get_val) - b.d = Var(b.model().I, initialize=get_val) - - -model.c = Block([1, 2], rule=c_rule) - -model.pprint() - -# @compprintloop -for v in model.component_objects(Var, descend_into=True): - print("FOUND VAR:" + v.name) - v.pprint() - -for v_data in model.component_data_objects(Var, descend_into=True): - print("Found: " + v_data.name + ", value = " + str(value(v_data))) -# @compprintloop diff --git a/doc/OnlineDocs/src/scripting/concrete1.py b/doc/OnlineDocs/src/scripting/concrete1.py deleted file mode 100644 index 399715efde6..00000000000 --- a/doc/OnlineDocs/src/scripting/concrete1.py +++ /dev/null @@ -1,20 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -from pyomo.environ import * - -model = ConcreteModel() - -model.x = Var([1, 2], domain=NonNegativeReals) - -model.OBJ = Objective(expr=2 * model.x[1] + 3 * model.x[2]) - -model.Constraint1 = Constraint(expr=3 * model.x[1] + 4 * model.x[2] >= 1) diff --git a/doc/OnlineDocs/src/scripting/doubleA.py b/doc/OnlineDocs/src/scripting/doubleA.py deleted file mode 100644 index abf35979a05..00000000000 --- a/doc/OnlineDocs/src/scripting/doubleA.py +++ /dev/null @@ -1,17 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - - -def doubleA_init(model): - return (i * 2 for i in model.A) - - -model.C = Set(initialize=DoubleA_init) diff --git a/doc/OnlineDocs/src/scripting/driveabs2.py b/doc/OnlineDocs/src/scripting/driveabs2.py deleted file mode 100644 index f8f972460b1..00000000000 --- a/doc/OnlineDocs/src/scripting/driveabs2.py +++ /dev/null @@ -1,47 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# driveabs2.py - -import pyomo.environ as pyo -from pyomo.opt import SolverFactory - -# Create a solver -opt = SolverFactory('cplex') - -# get the model from another file -from abstract2 import model - -# Create a model instance and optimize -instance = model.create_instance('abstract2.dat') - -# @Create_dual_suffix_component -# Create a 'dual' suffix component on the instance -# so the solver plugin will know which suffixes to collect -instance.dual = pyo.Suffix(direction=pyo.Suffix.IMPORT) -# @Create_dual_suffix_component - -results = opt.solve(instance) -# also puts the results back into the instance for easy access - -# @Access_all_dual -# display all duals -print("Duals") -for c in instance.component_objects(pyo.Constraint, active=True): - print(" Constraint", c) - for index in c: - print(" ", index, instance.dual[c[index]]) -# @Access_all_dual - -# @Access_one_dual -# access one dual -print("Dual for Film=", instance.dual[instance.AxbConstraint['Film']]) -# @Access_one_dual diff --git a/doc/OnlineDocs/src/scripting/driveconc1.py b/doc/OnlineDocs/src/scripting/driveconc1.py deleted file mode 100644 index 49b92f32d09..00000000000 --- a/doc/OnlineDocs/src/scripting/driveconc1.py +++ /dev/null @@ -1,34 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# driveconc1.py - -import pyomo.environ as pyo -from pyomo.opt import SolverFactory - -# Create a solver -opt = SolverFactory('cplex') - -# get the model from another file -from concrete1 import model - -# Create a 'dual' suffix component on the instance -# so the solver plugin will know which suffixes to collect -model.dual = pyo.Suffix(direction=pyo.Suffix.IMPORT) - -results = opt.solve(model) # also load results to model - -# display all duals -print("Duals") -for c in model.component_objects(pyo.Constraint, active=True): - print(" Constraint", c) - for index in c: - print(" ", index, model.dual[c[index]]) diff --git a/doc/OnlineDocs/src/scripting/iterative1.py b/doc/OnlineDocs/src/scripting/iterative1.py deleted file mode 100644 index 939120e834f..00000000000 --- a/doc/OnlineDocs/src/scripting/iterative1.py +++ /dev/null @@ -1,74 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# @Import_symbols_for_pyomo -# iterative1.py -import pyomo.environ as pyo -from pyomo.opt import SolverFactory - -# @Import_symbols_for_pyomo - -# @Call_SolverFactory_with_argument -# Create a solver -opt = pyo.SolverFactory('glpk') -# @Call_SolverFactory_with_argument - -# -# A simple model with binary variables and -# an empty constraint list. -# -# @Create_base_model -model = pyo.AbstractModel() -model.n = pyo.Param(default=4) -model.x = pyo.Var(pyo.RangeSet(model.n), within=pyo.Binary) - - -def o_rule(model): - return pyo.summation(model.x) - - -model.o = pyo.Objective(rule=o_rule) -# @Create_base_model -# @Create_empty_constraint_list -model.c = pyo.ConstraintList() -# @Create_empty_constraint_list - -# Create a model instance and optimize -# @Create_instantiated_model -instance = model.create_instance() -# @Create_instantiated_model -# @Solve_and_refer_to_results -results = opt.solve(instance) -# @Solve_and_refer_to_results -# @Display_updated_value -instance.display() -# @Display_updated_value - -# Iterate to eliminate the previously found solution -# @Assign_integers -for i in range(5): - # @Assign_integers - # @Iteratively_assign_and_test - expr = 0 - for j in instance.x: - if pyo.value(instance.x[j]) == 0: - expr += instance.x[j] - else: - expr += 1 - instance.x[j] - # @Iteratively_assign_and_test - # @Add_expression_constraint - instance.c.add(expr >= 1) - # @Add_expression_constraint - # @Find_and_display_solution - results = opt.solve(instance) - print("\n===== iteration", i) - instance.display() -# @Find_and_display_solution diff --git a/doc/OnlineDocs/src/scripting/iterative2.py b/doc/OnlineDocs/src/scripting/iterative2.py deleted file mode 100644 index 7506337a491..00000000000 --- a/doc/OnlineDocs/src/scripting/iterative2.py +++ /dev/null @@ -1,52 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# iterative2.py - -import pyomo.environ as pyo -from pyomo.opt import SolverFactory - -# Create a solver -opt = pyo.SolverFactory('cplex') - -# -# A simple model with binary variables and -# an empty constraint list. -# -model = pyo.AbstractModel() -model.n = pyo.Param(default=4) -model.x = pyo.Var(pyo.RangeSet(model.n), within=pyo.Binary) - - -def o_rule(model): - return pyo.summation(model.x) - - -model.o = pyo.Objective(rule=o_rule) -model.c = pyo.ConstraintList() - -# Create a model instance and optimize -instance = model.create_instance() -results = opt.solve(instance) -instance.display() - -# "flip" the value of x[2] (it is binary) -# then solve again -# @Flip_value_before_solve_again - -if pyo.value(instance.x[2]) == 0: - instance.x[2].fix(1) -else: - instance.x[2].fix(0) - -results = opt.solve(instance) -# @Flip_value_before_solve_again -instance.display() diff --git a/doc/OnlineDocs/src/scripting/noiteration1.py b/doc/OnlineDocs/src/scripting/noiteration1.py deleted file mode 100644 index c7a86e9d1e9..00000000000 --- a/doc/OnlineDocs/src/scripting/noiteration1.py +++ /dev/null @@ -1,41 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# noiteration1.py - -import pyomo.environ as pyo -from pyomo.opt import SolverFactory - -# Create a solver -opt = SolverFactory('glpk') - -# -# A simple model with binary variables and -# an empty constraint list. -# -model = pyo.ConcreteModel() -model.n = pyo.Param(default=4) -model.x = pyo.Var(pyo.RangeSet(model.n), within=pyo.Binary) - - -def o_rule(model): - return pyo.summation(model.x) - - -model.o = pyo.Objective(rule=o_rule) -model.c = pyo.ConstraintList() - -results = opt.solve(model) - -if pyo.value(model.x[2]) == 0: - print("The second index has a zero") -else: - print("x[2]=", pyo.value(model.x[2])) diff --git a/doc/OnlineDocs/src/scripting/parallel.py b/doc/OnlineDocs/src/scripting/parallel.py deleted file mode 100644 index e6cfa002780..00000000000 --- a/doc/OnlineDocs/src/scripting/parallel.py +++ /dev/null @@ -1,38 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# parallel.py -# run with mpirun -np 2 python -m mpi4py parallel.py -import pyomo.environ as pyo -from mpi4py import MPI - -rank = MPI.COMM_WORLD.Get_rank() -size = MPI.COMM_WORLD.Get_size() -assert ( - size == 2 -), 'This example only works with 2 processes; please us mpirun -np 2 python -m mpi4py parallel.py' - -# Create a solver -opt = pyo.SolverFactory('cplex_direct') - -# -# A simple model with binary variables -# -model = pyo.ConcreteModel() -model.n = pyo.Param(initialize=4) -model.x = pyo.Var(pyo.RangeSet(model.n), within=pyo.Binary) -model.obj = pyo.Objective(expr=sum(model.x.values())) - -if rank == 1: - model.x[1].fix(1) - -results = opt.solve(model) -print('rank: ', rank, ' objective: ', pyo.value(model.obj.expr)) diff --git a/doc/OnlineDocs/src/scripting/spy4Constraints.py b/doc/OnlineDocs/src/scripting/spy4Constraints.py deleted file mode 100644 index 66f82802402..00000000000 --- a/doc/OnlineDocs/src/scripting/spy4Constraints.py +++ /dev/null @@ -1,62 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -""" -David L. Woodruff and Mingye Yang, Spring 2018 -Code snippets for Constraints.rst in testable form -""" - -from pyomo.environ import * - -model = ConcreteModel() -# @Inequality_constraints_2expressions -model.x = Var() - - -def aRule(model): - return model.x >= 2 - - -model.Boundx = Constraint(rule=aRule) - - -def bRule(model): - return (2, model.x, None) - - -model.boundx = Constraint(rule=bRule) -# @Inequality_constraints_2expressions - -model = ConcreteModel() -model.J = Set(initialize=['butter', 'scones']) -model.x = Var(model.J) - - -# @Constraint_example -def teaOKrule(model): - return model.x['butter'] + model.x['scones'] == 3 - - -model.TeaConst = Constraint(rule=teaOKrule) -# @Constraint_example - -# @Passing_elements_crossproduct -model.A = RangeSet(1, 10) -model.a = Param(model.A, within=PositiveReals) -model.ToBuy = Var(model.A) - - -def bud_rule(model, i): - return model.a[i] * model.ToBuy[i] <= i - - -aBudget = Constraint(model.A, rule=bud_rule) -# @Passing_elements_crossproduct diff --git a/doc/OnlineDocs/src/scripting/spy4Expressions.py b/doc/OnlineDocs/src/scripting/spy4Expressions.py deleted file mode 100644 index cf7ed1f112f..00000000000 --- a/doc/OnlineDocs/src/scripting/spy4Expressions.py +++ /dev/null @@ -1,128 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -""" -David L. Woodruff and Mingye Yang, Spring 2018 -Code snippets for Expressions.rst in testable form -""" - -from pyomo.environ import * - -model = ConcreteModel() - -# @Buildup_expression_switch -switch = 3 - -model.A = RangeSet(1, 10) -model.c = Param(model.A) -model.d = Param() -model.x = Var(model.A, domain=Boolean) - - -def pi_rule(model): - accexpr = summation(model.c, model.x) - if switch >= 2: - accexpr = accexpr - model.d - return accexpr >= 0.5 - - -PieSlice = Constraint(rule=pi_rule) -# @Buildup_expression_switch - -# @Abstract_wrong_usage -model.A = RangeSet(1, 10) -model.c = Param(model.A) -model.d = Param() -model.x = Var(model.A, domain=Boolean) - - -def pi_rule(model): - accexpr = summation(model.c, model.x) - if model.d >= 2: # NOT in an abstract model!! - accexpr = accexpr - model.d - return accexpr >= 0.5 - - -PieSlice = Constraint(rule=pi_rule) -# @Abstract_wrong_usage - -# @Declare_piecewise_constraints -# model.pwconst = Piecewise(indexes, yvar, xvar, **Keywords) -# model.pwconst = Piecewise(yvar,xvar,**Keywords) -# @Declare_piecewise_constraints - - -# @f_rule_Function_examples -# A function that changes with index -def f(model, j, x): - if j == 2: - return x**2 + 1.0 - else: - return x**2 + 5.0 - - -# A nonlinear function -f = lambda model, x: exp(x) + value(model.p) - -# A step function -f = [0, 0, 1, 1, 2, 2] -# @f_rule_Function_examples - -# @Keyword_assignment_example -kwds = {'pw_constr_type': 'EQ', 'pw_repn': 'SOS2', 'sense': maximize, 'force_pw': True} -# @Keyword_assignment_example - -# @Expression_objects_illustration -model = ConcreteModel() -model.x = Var(initialize=1.0) - - -def _e(m, i): - return m.x * i - - -model.e = Expression([1, 2, 3], rule=_e) - -instance = model.create_instance() - -print(value(instance.e[1])) # -> 1.0 -print(instance.e[1]()) # -> 1.0 -print(instance.e[1].value) # -> a pyomo expression object - -# Change the underlying expression -instance.e[1].value = instance.x**2 - -# ... solve -# ... load results - -# print the value of the expression given the loaded optimal solution -print(value(instance.e[1])) -# @Expression_objects_illustration - - -# @Define_python_function -def f(x, p): - return x + p - - -# @Define_python_function - -# @Generate_new_expression -model = ConcreteModel() -model.x = Var() - -# create a Pyomo expression -e1 = model.x + 5 - -# create another Pyomo expression -# e1 is copied when generating e2 -e2 = e1 + model.x -# @Generate_new_expression diff --git a/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py b/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py deleted file mode 100644 index 9f6698d63c9..00000000000 --- a/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py +++ /dev/null @@ -1,36 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -""" -David L. Woodruff and Mingye Yang, Spring 2018 -Code snippets for PyomoCommand.rst in testable form -""" - -from pyomo.environ import * - -model = ConcreteModel() -model.I = RangeSet(3) -model.J = RangeSet(3) -model.a = Param(model.I, model.J, default=1.0) -model.x = Var(model.J) -model.b = Param(model.I, default=1.0) - - -# @Troubleshooting_printed_command -def ax_constraint_rule(model, i): - # return the expression for the constraint for i - print("ax_constraint_rule was called for i=", str(i)) - return sum(model.a[i, j] * model.x[j] for j in model.J) >= model.b[i] - - -# the next line creates one constraint for each member of the set model.I -model.AxbConstraint = Constraint(model.I, rule=ax_constraint_rule) -# @Troubleshooting_printed_command diff --git a/doc/OnlineDocs/src/scripting/spy4Variables.py b/doc/OnlineDocs/src/scripting/spy4Variables.py deleted file mode 100644 index 1bc2dc9f1ef..00000000000 --- a/doc/OnlineDocs/src/scripting/spy4Variables.py +++ /dev/null @@ -1,39 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -""" -David L. Woodruff and Mingye Yang, Spring 2018 -Code snippets for Variables.rst in testable form -""" - -from pyomo.environ import * - -model = ConcreteModel() -# @Declare_singleton_variable -model.LumberJack = Var(within=NonNegativeReals, bounds=(0, 6), initialize=1.5) -# @Declare_singleton_variable - -# @Assign_value -model.LumberJack = 1.5 -# @Assign_value - -# @Declare_bounds -model.A = Set(initialize=['Scones', 'Tea']) -lb = {'Scones': 2, 'Tea': 4} -ub = {'Scones': 5, 'Tea': 7} - - -def fb(model, i): - return (lb[i], ub[i]) - - -model.PriceToCharge = Var(model.A, domain=PositiveIntegers, bounds=fb) -# @Declare_bounds diff --git a/doc/OnlineDocs/src/scripting/spy4scripts.py b/doc/OnlineDocs/src/scripting/spy4scripts.py deleted file mode 100644 index f71a1b67b11..00000000000 --- a/doc/OnlineDocs/src/scripting/spy4scripts.py +++ /dev/null @@ -1,220 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -###NOTE: as of May 16, this will not even come close to running. DLW -### and it is "wrong" in a lot of places. -### Someone should edit this file, then delete these comment lines. DLW may 16 - -""" -David L. Woodruff and Mingye Yang, Spring 2018 -Code snippets for scripts.rst in testable form -""" -import pyomo.environ as pyo - -instance = pyo.ConcreteModel() -instance.I = pyo.Set(initialize=[1, 2, 3]) -instance.sigma = pyo.Param(mutable=True, initialize=2.3) -instance.Theta = pyo.Param(instance.I, mutable=True) -for i in instance.I: - instance.Theta[i] = i -ParamName = "Theta" -idx = 1 -NewVal = 1134 - -# @Assign_value_to_indexed_parametername -instance.ParamName[idx].value = NewVal -# @Assign_value_to_indexed_parametername - -ParamName = "sigma" - -# @Assign_value_to_unindexed_parametername_2 -instance.ParamName.value = NewVal -# @Assign_value_to_unindexed_parametername_2 - -instance.x = pyo.Var([1, 2, 3], initialize=0) -instance.y = pyo.Var() -# @Set_upper&lower_bound - -if instance.x[2] == 0: - instance.x[2].setlb(1) - instance.x[2].setub(1) -else: - instance.x[2].setlb(0) - instance.x[2].setub(0) -# @Set_upper&lower_bound - -# @Equivalent_form_of_instance.x.fix(2) -instance.y.value = 2 -instance.y.fixed = True -# @Equivalent_form_of_instance.x.fix(2) - -model = ConcreteModel() -model.obj1 = pyo.Objective(expr=0) -model.obj2 = pyo.Objective(expr=0) - -# @Pass_multiple_objectives_to_solver -model.obj1.deactivate() -model.obj2.activate() -# @Pass_multiple_objectives_to_solver - - -# @Listing_arguments -def pyomo_preprocess(options=None): - if options == None: - print("No command line options were given.") - else: - print("Command line arguments were: %s" % options) - - -# @Listing_arguments - - -# @Provide_dictionary_for_arbitrary_keywords -def pyomo_preprocess(**kwds): - options = kwds.get('options', None) - if options == None: - print("No command line options were given.") - else: - print("Command line arguments were: %s" % options) - - -# @Provide_dictionary_for_arbitrary_keywords - - -# @Pyomo_preprocess_argument -def pyomo_preprocess(options=None): - pass - - -# @Pyomo_preprocess_argument - -# @Display_all_variables&values -for v in instance.component_objects(pyo.Var, active=True): - print("Variable", v) - for index in v: - print(" ", index, pyo.value(v[index])) -# @Display_all_variables&values - -# @Display_all_variables&values_data -for v in instance.component_data_objects(pyo.Var, active=True): - print(v, pyo.value(v)) -# @Display_all_variables&values_data - - -instance.iVar = pyo.Var([1, 2, 3], initialize=1, domain=pyo.Boolean) -instance.sVar = pyo.Var(initialize=1, domain=pyo.Boolean) -# dlw may 2018: the next snippet does not trigger any fixing ("active?") -# @Fix_all_integers&values -for var in instance.component_data_objects(pyo.Var, active=True): - if var.domain is pyo.IntegerSet or var.domain is pyo.BooleanSet: - print("fixing " + str(v)) - var.fixed = True # fix the current value -# @Fix_all_integers&values - - -# @Include_definition_in_modelfile -def pyomo_print_results(options, instance, results): - for v in instance.component_objects(pyo.Var, active=True): - print("Variable " + str(v)) - varobject = getattr(instance, v) - for index in varobject: - print(" ", index, varobject[index].value) - - -# @Include_definition_in_modelfile - -# @Print_parameter_name&value -for parmobject in instance.component_objects(pyo.Param, active=True): - print("Parameter " + str(parmobject.name)) - for index in parmobject: - print(" ", index, parmobject[index].value) -# @Print_parameter_name&value - - -# @Include_definition_output_constraints&duals -def pyomo_print_results(options, instance, results): - # display all duals - print("Duals") - for c in instance.component_objects(pyo.Constraint, active=True): - print(" Constraint", c) - cobject = getattr(instance, c) - for index in cobject: - print(" ", index, instance.dual[cobject[index]]) - - -# @Include_definition_output_constraints&duals - -""" -xxxxxxxxxxxxxxxxxxxx high alert!!!! xxxxxx testing blocked from here to the end xxxxxxxxxxxxxx -# @Print_solver_status -results = opt.solve(instance) -#print ("The solver returned a status of:"+str(results.solver.status)) -# @Print_solver_status - -# @Pyomo_data_comparedwith_solver_status_1 -from pyomo.opt import SolverStatus, TerminationCondition - -#... - -if (results.solver.status == SolverStatus.ok) and (results.solver.termination_condition == TerminationCondition.optimal): - print ("this is feasible and optimal") -elif results.solver.termination_condition == TerminationCondition.infeasible: - print ("do something about it? or exit?") -else: - # something else is wrong - print (str(results.solver)) -# @Pyomo_data_comparedwith_solver_status_1 - -# @Pyomo_data_comparedwith_solver_status_2 -from pyomo.opt import TerminationCondition - -... - -results = opt.solve(model, load_solutions=False) -if results.solver.termination_condition == TerminationCondition.optimal: - model.solutions.load_from(results) -else: - print ("Solution is not optimal") - # now do something about it? or exit? ... -# @Pyomo_data_comparedwith_solver_status_2 - -# @See_solver_output -results = opt.solve(instance, tee=True) -# @See_solver_output - -# @Add_option_to_solver -optimizer = pyo.SolverFactory['cbc'] -optimizer.options["threads"] = 4 -# @Add_option_to_solver - -# @Add_multiple_options_to_solver -results = optimizer.solve(instance, options={'threads' : 4}, tee=True) -# @Add_multiple_options_to_solver - -# @Set_path_to_solver_executable -opt = pyo.SolverFactory("ipopt", executable="../ipopt") -# @Set_path_to_solver_executable - -# @Pass_warmstart_to_solver -instance = model.create() -instance.y[0] = 1 -instance.y[1] = 0 - -opt = pyo.SolverFactory("cplex") - -results = opt.solve(instance, warmstart=True) -# @Pass_warmstart_to_solver - -# @Specify_temporary_directory_name -from pyomo.common.tempfiles import TempfileManager -TempfileManager.tempdir = YourDirectoryNameGoesHere -# @Specify_temporary_directory_name -""" diff --git a/doc/OnlineDocs/src/strip_examples.py b/doc/OnlineDocs/src/strip_examples.py deleted file mode 100644 index 2fd03256499..00000000000 --- a/doc/OnlineDocs/src/strip_examples.py +++ /dev/null @@ -1,80 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# -# This script finds all *.py files in the current and subdirectories. -# It processes these files to find blocks that start/end with "# @" -# For example -# -# print("START HERE") -# # @block -# print("IN THE BLOCK") -# x = 1 -# # @block -# print("END HERE") -# -# If this file was foo.py, then a file foo_block.spy is created, which -# contains the lines between the lines starting with "# @". -# -# Additionally, the file foo.spy is created, which strips all lines -# starting with "# @". -# -# This utility provides a convenient mechanism for creating complex scripts that -# can be tested, while extracting pieces that are included in Sphinx documentation. -# -import glob -import sys -import os -import os.path - - -def f(root, file): - if not file.endswith('.py'): - return - prefix = os.path.splitext(file)[0] - # print([root, file, prefix]) - OUTPUT = open(root + '/' + prefix + '.spy', 'w') - INPUT = open(root + '/' + file, 'r') - flag = False - block_name = None - for line in INPUT: - tmp = line.strip() - if tmp.startswith("# @"): - if flag is False: - block_name = tmp[3:] - flag = True - OUTPUT_ = open(root + '/' + prefix + '_%s.spy' % block_name, 'w') - else: - if block_name != tmp[3:]: - print( - "ERROR parsing file '%s': Started block '%s' but ended with '%s'" - % (root + '/' + file, block_name, tmp[3:]) - ) - sys.exit(1) - flag = False - block_name is None - OUTPUT_.close() - continue - elif flag: - OUTPUT_.write(line) - OUTPUT.write(line) - INPUT.close() - OUTPUT.close() - - -def generate_spy_files(root_dir): - for root, dirs, files in os.walk(root_dir): - for file in files: - f(root, file) - - -if __name__ == '__main__': - generate_spy_files(sys.argv[1]) diff --git a/doc/OnlineDocs/src/test_examples.py b/doc/OnlineDocs/src/test_examples.py deleted file mode 100644 index c5c9a135ee9..00000000000 --- a/doc/OnlineDocs/src/test_examples.py +++ /dev/null @@ -1,76 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pyomo.common.unittest as unittest -import glob -import os -from pyomo.common.dependencies import attempt_import, matplotlib_available -from pyomo.common.fileutils import this_file_dir -import pyomo.environ as pyo - - -currdir = this_file_dir() - -parameterized, param_available = attempt_import('parameterized') -if not param_available: - raise unittest.SkipTest('Parameterized is not available.') - -# Needed for testing (triggers matplotlib import and switches its backend): -bool(matplotlib_available) - - -class TestOnlineDocExamples(unittest.BaselineTestDriver, unittest.TestCase): - # Only test files in directories ending in -ch. These directories - # contain the updated python and scripting files corresponding to - # each chapter in the book. - py_tests, sh_tests = unittest.BaselineTestDriver.gather_tests( - list(filter(os.path.isdir, glob.glob(os.path.join(currdir, '*')))) - ) - - solver_dependencies = { - 'test_data_pyomo_diet1': ['glpk'], - 'test_data_pyomo_diet2': ['glpk'], - 'test_kernel_examples': ['glpk'], - } - # Note on package dependencies: two tests actually need - # pyutilib.excel.spreadsheet; however, the pyutilib importer is - # broken on Python>=3.12, so instead of checking for spreadsheet, we - # will check for pyutilib.component, which triggers the importer - # (and catches the error on 3.12) - package_dependencies = { - # data - 'test_data_ABCD9': ['pyodbc'], - 'test_data_ABCD8': ['pyodbc'], - 'test_data_ABCD7': ['win32com', 'pyutilib.component'], - # dataportal - 'test_dataportal_dataportal_tab': ['xlrd', 'pyutilib.component'], - 'test_dataportal_set_initialization': ['numpy'], - 'test_dataportal_param_initialization': ['numpy'], - # kernel - 'test_kernel_examples': ['pympler'], - } - - @parameterized.parameterized.expand( - sh_tests, name_func=unittest.BaselineTestDriver.custom_name_func - ) - def test_sh(self, tname, test_file, base_file): - self.shell_test_driver(tname, test_file, base_file) - - @parameterized.parameterized.expand( - py_tests, name_func=unittest.BaselineTestDriver.custom_name_func - ) - def test_py(self, tname, test_file, base_file): - self.python_test_driver(tname, test_file, base_file) - - -# Execute the tests -if __name__ == '__main__': - unittest.main() From 593575617d5a2e7416a61752cfecdeb9319c3ecc Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 19:29:58 -0600 Subject: [PATCH 2413/3044] Restoring src from Archive --- doc/{Archive => OnlineDocs}/src/data/A.tab | 0 doc/{Archive => OnlineDocs}/src/data/ABCD.tab | 0 doc/{Archive => OnlineDocs}/src/data/ABCD.txt | 0 doc/{Archive => OnlineDocs}/src/data/ABCD.xls | Bin doc/{Archive => OnlineDocs}/src/data/ABCD1.dat | 0 doc/{Archive => OnlineDocs}/src/data/ABCD1.py | 0 doc/{Archive => OnlineDocs}/src/data/ABCD1.txt | 0 doc/{Archive => OnlineDocs}/src/data/ABCD2.dat | 0 doc/{Archive => OnlineDocs}/src/data/ABCD2.py | 0 doc/{Archive => OnlineDocs}/src/data/ABCD2.txt | 0 doc/{Archive => OnlineDocs}/src/data/ABCD3.dat | 0 doc/{Archive => OnlineDocs}/src/data/ABCD3.py | 0 doc/{Archive => OnlineDocs}/src/data/ABCD3.txt | 0 doc/{Archive => OnlineDocs}/src/data/ABCD4.dat | 0 doc/{Archive => OnlineDocs}/src/data/ABCD4.py | 0 doc/{Archive => OnlineDocs}/src/data/ABCD4.txt | 0 doc/{Archive => OnlineDocs}/src/data/ABCD5.dat | 0 doc/{Archive => OnlineDocs}/src/data/ABCD5.py | 0 doc/{Archive => OnlineDocs}/src/data/ABCD5.txt | 0 doc/{Archive => OnlineDocs}/src/data/ABCD6.dat | 0 doc/{Archive => OnlineDocs}/src/data/ABCD6.py | 0 doc/{Archive => OnlineDocs}/src/data/ABCD6.txt | 0 doc/{Archive => OnlineDocs}/src/data/ABCD7.dat | 0 doc/{Archive => OnlineDocs}/src/data/ABCD7.py | 0 doc/{Archive => OnlineDocs}/src/data/ABCD7.txt | 0 doc/{Archive => OnlineDocs}/src/data/ABCD8.bad | 0 doc/{Archive => OnlineDocs}/src/data/ABCD8.dat | 0 doc/{Archive => OnlineDocs}/src/data/ABCD8.py | 0 doc/{Archive => OnlineDocs}/src/data/ABCD9.bad | 0 doc/{Archive => OnlineDocs}/src/data/ABCD9.dat | 0 doc/{Archive => OnlineDocs}/src/data/ABCD9.py | 0 doc/{Archive => OnlineDocs}/src/data/C.tab | 0 doc/{Archive => OnlineDocs}/src/data/D.tab | 0 doc/{Archive => OnlineDocs}/src/data/U.tab | 0 doc/{Archive => OnlineDocs}/src/data/Y.tab | 0 doc/{Archive => OnlineDocs}/src/data/Z.tab | 0 .../src/data/data_managers.txt | 0 doc/{Archive => OnlineDocs}/src/data/diet.dat | 0 doc/{Archive => OnlineDocs}/src/data/diet.sql | 0 doc/{Archive => OnlineDocs}/src/data/diet.sqlite | Bin .../src/data/diet.sqlite.dat | 0 doc/{Archive => OnlineDocs}/src/data/diet1.py | 0 doc/{Archive => OnlineDocs}/src/data/ex.dat | 0 doc/{Archive => OnlineDocs}/src/data/ex.py | 0 doc/{Archive => OnlineDocs}/src/data/ex.txt | 0 doc/{Archive => OnlineDocs}/src/data/ex1.dat | 0 doc/{Archive => OnlineDocs}/src/data/ex2.dat | 0 .../src/data/import1.tab.dat | 0 doc/{Archive => OnlineDocs}/src/data/import1.tab.py | 0 .../src/data/import1.tab.txt | 0 .../src/data/import2.tab.dat | 0 doc/{Archive => OnlineDocs}/src/data/import2.tab.py | 0 .../src/data/import2.tab.txt | 0 .../src/data/import3.tab.dat | 0 doc/{Archive => OnlineDocs}/src/data/import3.tab.py | 0 .../src/data/import3.tab.txt | 0 .../src/data/import4.tab.dat | 0 doc/{Archive => OnlineDocs}/src/data/import4.tab.py | 0 .../src/data/import4.tab.txt | 0 .../src/data/import5.tab.dat | 0 doc/{Archive => OnlineDocs}/src/data/import5.tab.py | 0 .../src/data/import5.tab.txt | 0 .../src/data/import6.tab.dat | 0 doc/{Archive => OnlineDocs}/src/data/import6.tab.py | 0 .../src/data/import6.tab.txt | 0 .../src/data/import7.tab.dat | 0 doc/{Archive => OnlineDocs}/src/data/import7.tab.py | 0 .../src/data/import7.tab.txt | 0 .../src/data/import8.tab.dat | 0 doc/{Archive => OnlineDocs}/src/data/import8.tab.py | 0 .../src/data/import8.tab.txt | 0 doc/{Archive => OnlineDocs}/src/data/namespace1.dat | 0 doc/{Archive => OnlineDocs}/src/data/param1.dat | 0 doc/{Archive => OnlineDocs}/src/data/param1.py | 0 doc/{Archive => OnlineDocs}/src/data/param1.txt | 0 doc/{Archive => OnlineDocs}/src/data/param2.dat | 0 doc/{Archive => OnlineDocs}/src/data/param2.py | 0 doc/{Archive => OnlineDocs}/src/data/param2.txt | 0 doc/{Archive => OnlineDocs}/src/data/param2a.dat | 0 doc/{Archive => OnlineDocs}/src/data/param2a.py | 0 doc/{Archive => OnlineDocs}/src/data/param2a.txt | 0 doc/{Archive => OnlineDocs}/src/data/param3.dat | 0 doc/{Archive => OnlineDocs}/src/data/param3.py | 0 doc/{Archive => OnlineDocs}/src/data/param3.txt | 0 doc/{Archive => OnlineDocs}/src/data/param3a.dat | 0 doc/{Archive => OnlineDocs}/src/data/param3a.py | 0 doc/{Archive => OnlineDocs}/src/data/param3a.txt | 0 doc/{Archive => OnlineDocs}/src/data/param3b.dat | 0 doc/{Archive => OnlineDocs}/src/data/param3b.py | 0 doc/{Archive => OnlineDocs}/src/data/param3b.txt | 0 doc/{Archive => OnlineDocs}/src/data/param3c.dat | 0 doc/{Archive => OnlineDocs}/src/data/param3c.py | 0 doc/{Archive => OnlineDocs}/src/data/param3c.txt | 0 doc/{Archive => OnlineDocs}/src/data/param4.dat | 0 doc/{Archive => OnlineDocs}/src/data/param4.py | 0 doc/{Archive => OnlineDocs}/src/data/param4.txt | 0 doc/{Archive => OnlineDocs}/src/data/param5.dat | 0 doc/{Archive => OnlineDocs}/src/data/param5.py | 0 doc/{Archive => OnlineDocs}/src/data/param5.txt | 0 doc/{Archive => OnlineDocs}/src/data/param5a.dat | 0 doc/{Archive => OnlineDocs}/src/data/param5a.py | 0 doc/{Archive => OnlineDocs}/src/data/param5a.txt | 0 doc/{Archive => OnlineDocs}/src/data/param6.dat | 0 doc/{Archive => OnlineDocs}/src/data/param6.py | 0 doc/{Archive => OnlineDocs}/src/data/param6.txt | 0 doc/{Archive => OnlineDocs}/src/data/param6a.dat | 0 doc/{Archive => OnlineDocs}/src/data/param6a.py | 0 doc/{Archive => OnlineDocs}/src/data/param6a.txt | 0 doc/{Archive => OnlineDocs}/src/data/param7a.dat | 0 doc/{Archive => OnlineDocs}/src/data/param7a.py | 0 doc/{Archive => OnlineDocs}/src/data/param7a.txt | 0 doc/{Archive => OnlineDocs}/src/data/param7b.dat | 0 doc/{Archive => OnlineDocs}/src/data/param7b.py | 0 doc/{Archive => OnlineDocs}/src/data/param7b.txt | 0 doc/{Archive => OnlineDocs}/src/data/param8a.dat | 0 doc/{Archive => OnlineDocs}/src/data/param8a.py | 0 doc/{Archive => OnlineDocs}/src/data/param8a.txt | 0 doc/{Archive => OnlineDocs}/src/data/pyomo.diet1.sh | 0 .../src/data/pyomo.diet1.txt | 0 doc/{Archive => OnlineDocs}/src/data/pyomo.diet2.sh | 0 .../src/data/pyomo.diet2.txt | 0 doc/{Archive => OnlineDocs}/src/data/set1.dat | 0 doc/{Archive => OnlineDocs}/src/data/set1.py | 0 doc/{Archive => OnlineDocs}/src/data/set1.txt | 0 doc/{Archive => OnlineDocs}/src/data/set2.dat | 0 doc/{Archive => OnlineDocs}/src/data/set2.py | 0 doc/{Archive => OnlineDocs}/src/data/set2.txt | 0 doc/{Archive => OnlineDocs}/src/data/set2a.dat | 0 doc/{Archive => OnlineDocs}/src/data/set2a.py | 0 doc/{Archive => OnlineDocs}/src/data/set2a.txt | 0 doc/{Archive => OnlineDocs}/src/data/set3.dat | 0 doc/{Archive => OnlineDocs}/src/data/set3.py | 0 doc/{Archive => OnlineDocs}/src/data/set3.txt | 0 doc/{Archive => OnlineDocs}/src/data/set4.dat | 0 doc/{Archive => OnlineDocs}/src/data/set4.py | 0 doc/{Archive => OnlineDocs}/src/data/set4.txt | 0 doc/{Archive => OnlineDocs}/src/data/set5.dat | 0 doc/{Archive => OnlineDocs}/src/data/set5.py | 0 doc/{Archive => OnlineDocs}/src/data/set5.txt | 0 doc/{Archive => OnlineDocs}/src/data/table0.dat | 0 doc/{Archive => OnlineDocs}/src/data/table0.py | 0 doc/{Archive => OnlineDocs}/src/data/table0.txt | 0 doc/{Archive => OnlineDocs}/src/data/table0.ul.dat | 0 doc/{Archive => OnlineDocs}/src/data/table0.ul.py | 0 doc/{Archive => OnlineDocs}/src/data/table0.ul.txt | 0 doc/{Archive => OnlineDocs}/src/data/table1.dat | 0 doc/{Archive => OnlineDocs}/src/data/table1.py | 0 doc/{Archive => OnlineDocs}/src/data/table1.txt | 0 doc/{Archive => OnlineDocs}/src/data/table2.dat | 0 doc/{Archive => OnlineDocs}/src/data/table2.py | 0 doc/{Archive => OnlineDocs}/src/data/table2.txt | 0 doc/{Archive => OnlineDocs}/src/data/table3.dat | 0 doc/{Archive => OnlineDocs}/src/data/table3.py | 0 doc/{Archive => OnlineDocs}/src/data/table3.txt | 0 doc/{Archive => OnlineDocs}/src/data/table3.ul.dat | 0 doc/{Archive => OnlineDocs}/src/data/table3.ul.py | 0 doc/{Archive => OnlineDocs}/src/data/table3.ul.txt | 0 doc/{Archive => OnlineDocs}/src/data/table4.dat | 0 doc/{Archive => OnlineDocs}/src/data/table4.py | 0 doc/{Archive => OnlineDocs}/src/data/table4.txt | 0 doc/{Archive => OnlineDocs}/src/data/table4.ul.dat | 0 doc/{Archive => OnlineDocs}/src/data/table4.ul.py | 0 doc/{Archive => OnlineDocs}/src/data/table4.ul.txt | 0 doc/{Archive => OnlineDocs}/src/data/table5.dat | 0 doc/{Archive => OnlineDocs}/src/data/table5.py | 0 doc/{Archive => OnlineDocs}/src/data/table5.txt | 0 doc/{Archive => OnlineDocs}/src/data/table6.dat | 0 doc/{Archive => OnlineDocs}/src/data/table6.py | 0 doc/{Archive => OnlineDocs}/src/data/table6.txt | 0 doc/{Archive => OnlineDocs}/src/data/table7.dat | 0 doc/{Archive => OnlineDocs}/src/data/table7.py | 0 doc/{Archive => OnlineDocs}/src/data/table7.txt | 0 doc/{Archive => OnlineDocs}/src/dataportal/A.tab | 0 doc/{Archive => OnlineDocs}/src/dataportal/C.tab | 0 doc/{Archive => OnlineDocs}/src/dataportal/D.tab | 0 doc/{Archive => OnlineDocs}/src/dataportal/PP.csv | 0 doc/{Archive => OnlineDocs}/src/dataportal/PP.json | 0 .../src/dataportal/PP.sqlite | Bin doc/{Archive => OnlineDocs}/src/dataportal/PP.tab | 0 doc/{Archive => OnlineDocs}/src/dataportal/PP.xml | 0 doc/{Archive => OnlineDocs}/src/dataportal/PP.yaml | 0 .../src/dataportal/PP_sqlite.py | 0 .../src/dataportal/Pyomo_mysql | 0 doc/{Archive => OnlineDocs}/src/dataportal/S.tab | 0 doc/{Archive => OnlineDocs}/src/dataportal/T.json | 0 doc/{Archive => OnlineDocs}/src/dataportal/T.yaml | 0 doc/{Archive => OnlineDocs}/src/dataportal/U.tab | 0 doc/{Archive => OnlineDocs}/src/dataportal/XW.tab | 0 doc/{Archive => OnlineDocs}/src/dataportal/Y.tab | 0 doc/{Archive => OnlineDocs}/src/dataportal/Z.tab | 0 .../src/dataportal/dataportal_tab.py | 0 .../src/dataportal/dataportal_tab.txt | 0 .../src/dataportal/excel.xls | Bin .../src/dataportal/param_initialization.py | 0 .../src/dataportal/param_initialization.txt | 0 .../src/dataportal/set_initialization.py | 0 .../src/dataportal/set_initialization.txt | 0 doc/{Archive => OnlineDocs}/src/expr/design.py | 0 doc/{Archive => OnlineDocs}/src/expr/design.txt | 0 doc/{Archive => OnlineDocs}/src/expr/index.py | 0 doc/{Archive => OnlineDocs}/src/expr/index.txt | 0 doc/{Archive => OnlineDocs}/src/expr/managing.py | 0 doc/{Archive => OnlineDocs}/src/expr/managing.txt | 0 doc/{Archive => OnlineDocs}/src/expr/overview.py | 0 doc/{Archive => OnlineDocs}/src/expr/overview.txt | 0 doc/{Archive => OnlineDocs}/src/expr/performance.py | 0 .../src/expr/performance.txt | 0 doc/{Archive => OnlineDocs}/src/expr/quicksum.log | 0 doc/{Archive => OnlineDocs}/src/expr/quicksum.py | 0 doc/{Archive => OnlineDocs}/src/kernel/examples.sh | 0 doc/{Archive => OnlineDocs}/src/kernel/examples.txt | 0 .../src/scripting/AbstractSuffixes.py | 0 .../src/scripting/Isinglebuild.py | 0 .../src/scripting/Isinglecomm.dat | 0 .../src/scripting/NodesIn_init.py | 0 doc/{Archive => OnlineDocs}/src/scripting/Z_init.py | 0 .../src/scripting/abstract1.dat | 0 .../src/scripting/abstract2.dat | 0 .../src/scripting/abstract2.py | 0 .../src/scripting/abstract2a.dat | 0 .../src/scripting/abstract2piece.py | 0 .../src/scripting/abstract2piecebuild.py | 0 .../src/scripting/block_iter_example.py | 0 .../src/scripting/concrete1.py | 0 .../src/scripting/doubleA.py | 0 .../src/scripting/driveabs2.py | 0 .../src/scripting/driveconc1.py | 0 .../src/scripting/iterative1.py | 0 .../src/scripting/iterative2.py | 0 .../src/scripting/noiteration1.py | 0 .../src/scripting/parallel.py | 0 .../src/scripting/spy4Constraints.py | 0 .../src/scripting/spy4Expressions.py | 0 .../src/scripting/spy4PyomoCommand.py | 0 .../src/scripting/spy4Variables.py | 0 .../src/scripting/spy4scripts.py | 0 doc/{Archive => OnlineDocs}/src/strip_examples.py | 0 doc/{Archive => OnlineDocs}/src/test_examples.py | 0 238 files changed, 0 insertions(+), 0 deletions(-) rename doc/{Archive => OnlineDocs}/src/data/A.tab (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD.tab (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD.xls (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD1.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD1.py (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD1.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD2.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD2.py (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD2.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD3.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD3.py (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD3.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD4.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD4.py (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD4.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD5.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD5.py (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD5.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD6.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD6.py (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD6.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD7.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD7.py (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD7.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD8.bad (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD8.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD8.py (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD9.bad (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD9.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/ABCD9.py (100%) rename doc/{Archive => OnlineDocs}/src/data/C.tab (100%) rename doc/{Archive => OnlineDocs}/src/data/D.tab (100%) rename doc/{Archive => OnlineDocs}/src/data/U.tab (100%) rename doc/{Archive => OnlineDocs}/src/data/Y.tab (100%) rename doc/{Archive => OnlineDocs}/src/data/Z.tab (100%) rename doc/{Archive => OnlineDocs}/src/data/data_managers.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/diet.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/diet.sql (100%) rename doc/{Archive => OnlineDocs}/src/data/diet.sqlite (100%) rename doc/{Archive => OnlineDocs}/src/data/diet.sqlite.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/diet1.py (100%) rename doc/{Archive => OnlineDocs}/src/data/ex.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/ex.py (100%) rename doc/{Archive => OnlineDocs}/src/data/ex.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/ex1.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/ex2.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/import1.tab.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/import1.tab.py (100%) rename doc/{Archive => OnlineDocs}/src/data/import1.tab.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/import2.tab.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/import2.tab.py (100%) rename doc/{Archive => OnlineDocs}/src/data/import2.tab.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/import3.tab.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/import3.tab.py (100%) rename doc/{Archive => OnlineDocs}/src/data/import3.tab.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/import4.tab.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/import4.tab.py (100%) rename doc/{Archive => OnlineDocs}/src/data/import4.tab.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/import5.tab.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/import5.tab.py (100%) rename doc/{Archive => OnlineDocs}/src/data/import5.tab.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/import6.tab.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/import6.tab.py (100%) rename doc/{Archive => OnlineDocs}/src/data/import6.tab.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/import7.tab.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/import7.tab.py (100%) rename doc/{Archive => OnlineDocs}/src/data/import7.tab.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/import8.tab.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/import8.tab.py (100%) rename doc/{Archive => OnlineDocs}/src/data/import8.tab.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/namespace1.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param1.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param1.py (100%) rename doc/{Archive => OnlineDocs}/src/data/param1.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/param2.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param2.py (100%) rename doc/{Archive => OnlineDocs}/src/data/param2.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/param2a.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param2a.py (100%) rename doc/{Archive => OnlineDocs}/src/data/param2a.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/param3.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param3.py (100%) rename doc/{Archive => OnlineDocs}/src/data/param3.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/param3a.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param3a.py (100%) rename doc/{Archive => OnlineDocs}/src/data/param3a.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/param3b.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param3b.py (100%) rename doc/{Archive => OnlineDocs}/src/data/param3b.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/param3c.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param3c.py (100%) rename doc/{Archive => OnlineDocs}/src/data/param3c.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/param4.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param4.py (100%) rename doc/{Archive => OnlineDocs}/src/data/param4.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/param5.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param5.py (100%) rename doc/{Archive => OnlineDocs}/src/data/param5.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/param5a.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param5a.py (100%) rename doc/{Archive => OnlineDocs}/src/data/param5a.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/param6.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param6.py (100%) rename doc/{Archive => OnlineDocs}/src/data/param6.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/param6a.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param6a.py (100%) rename doc/{Archive => OnlineDocs}/src/data/param6a.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/param7a.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param7a.py (100%) rename doc/{Archive => OnlineDocs}/src/data/param7a.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/param7b.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param7b.py (100%) rename doc/{Archive => OnlineDocs}/src/data/param7b.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/param8a.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/param8a.py (100%) rename doc/{Archive => OnlineDocs}/src/data/param8a.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/pyomo.diet1.sh (100%) rename doc/{Archive => OnlineDocs}/src/data/pyomo.diet1.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/pyomo.diet2.sh (100%) rename doc/{Archive => OnlineDocs}/src/data/pyomo.diet2.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/set1.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/set1.py (100%) rename doc/{Archive => OnlineDocs}/src/data/set1.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/set2.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/set2.py (100%) rename doc/{Archive => OnlineDocs}/src/data/set2.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/set2a.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/set2a.py (100%) rename doc/{Archive => OnlineDocs}/src/data/set2a.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/set3.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/set3.py (100%) rename doc/{Archive => OnlineDocs}/src/data/set3.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/set4.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/set4.py (100%) rename doc/{Archive => OnlineDocs}/src/data/set4.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/set5.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/set5.py (100%) rename doc/{Archive => OnlineDocs}/src/data/set5.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/table0.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/table0.py (100%) rename doc/{Archive => OnlineDocs}/src/data/table0.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/table0.ul.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/table0.ul.py (100%) rename doc/{Archive => OnlineDocs}/src/data/table0.ul.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/table1.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/table1.py (100%) rename doc/{Archive => OnlineDocs}/src/data/table1.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/table2.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/table2.py (100%) rename doc/{Archive => OnlineDocs}/src/data/table2.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/table3.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/table3.py (100%) rename doc/{Archive => OnlineDocs}/src/data/table3.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/table3.ul.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/table3.ul.py (100%) rename doc/{Archive => OnlineDocs}/src/data/table3.ul.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/table4.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/table4.py (100%) rename doc/{Archive => OnlineDocs}/src/data/table4.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/table4.ul.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/table4.ul.py (100%) rename doc/{Archive => OnlineDocs}/src/data/table4.ul.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/table5.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/table5.py (100%) rename doc/{Archive => OnlineDocs}/src/data/table5.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/table6.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/table6.py (100%) rename doc/{Archive => OnlineDocs}/src/data/table6.txt (100%) rename doc/{Archive => OnlineDocs}/src/data/table7.dat (100%) rename doc/{Archive => OnlineDocs}/src/data/table7.py (100%) rename doc/{Archive => OnlineDocs}/src/data/table7.txt (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/A.tab (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/C.tab (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/D.tab (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/PP.csv (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/PP.json (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/PP.sqlite (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/PP.tab (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/PP.xml (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/PP.yaml (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/PP_sqlite.py (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/Pyomo_mysql (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/S.tab (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/T.json (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/T.yaml (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/U.tab (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/XW.tab (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/Y.tab (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/Z.tab (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/dataportal_tab.py (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/dataportal_tab.txt (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/excel.xls (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/param_initialization.py (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/param_initialization.txt (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/set_initialization.py (100%) rename doc/{Archive => OnlineDocs}/src/dataportal/set_initialization.txt (100%) rename doc/{Archive => OnlineDocs}/src/expr/design.py (100%) rename doc/{Archive => OnlineDocs}/src/expr/design.txt (100%) rename doc/{Archive => OnlineDocs}/src/expr/index.py (100%) rename doc/{Archive => OnlineDocs}/src/expr/index.txt (100%) rename doc/{Archive => OnlineDocs}/src/expr/managing.py (100%) rename doc/{Archive => OnlineDocs}/src/expr/managing.txt (100%) rename doc/{Archive => OnlineDocs}/src/expr/overview.py (100%) rename doc/{Archive => OnlineDocs}/src/expr/overview.txt (100%) rename doc/{Archive => OnlineDocs}/src/expr/performance.py (100%) rename doc/{Archive => OnlineDocs}/src/expr/performance.txt (100%) rename doc/{Archive => OnlineDocs}/src/expr/quicksum.log (100%) rename doc/{Archive => OnlineDocs}/src/expr/quicksum.py (100%) rename doc/{Archive => OnlineDocs}/src/kernel/examples.sh (100%) rename doc/{Archive => OnlineDocs}/src/kernel/examples.txt (100%) rename doc/{Archive => OnlineDocs}/src/scripting/AbstractSuffixes.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/Isinglebuild.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/Isinglecomm.dat (100%) rename doc/{Archive => OnlineDocs}/src/scripting/NodesIn_init.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/Z_init.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/abstract1.dat (100%) rename doc/{Archive => OnlineDocs}/src/scripting/abstract2.dat (100%) rename doc/{Archive => OnlineDocs}/src/scripting/abstract2.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/abstract2a.dat (100%) rename doc/{Archive => OnlineDocs}/src/scripting/abstract2piece.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/abstract2piecebuild.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/block_iter_example.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/concrete1.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/doubleA.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/driveabs2.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/driveconc1.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/iterative1.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/iterative2.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/noiteration1.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/parallel.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/spy4Constraints.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/spy4Expressions.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/spy4PyomoCommand.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/spy4Variables.py (100%) rename doc/{Archive => OnlineDocs}/src/scripting/spy4scripts.py (100%) rename doc/{Archive => OnlineDocs}/src/strip_examples.py (100%) rename doc/{Archive => OnlineDocs}/src/test_examples.py (100%) diff --git a/doc/Archive/src/data/A.tab b/doc/OnlineDocs/src/data/A.tab similarity index 100% rename from doc/Archive/src/data/A.tab rename to doc/OnlineDocs/src/data/A.tab diff --git a/doc/Archive/src/data/ABCD.tab b/doc/OnlineDocs/src/data/ABCD.tab similarity index 100% rename from doc/Archive/src/data/ABCD.tab rename to doc/OnlineDocs/src/data/ABCD.tab diff --git a/doc/Archive/src/data/ABCD.txt b/doc/OnlineDocs/src/data/ABCD.txt similarity index 100% rename from doc/Archive/src/data/ABCD.txt rename to doc/OnlineDocs/src/data/ABCD.txt diff --git a/doc/Archive/src/data/ABCD.xls b/doc/OnlineDocs/src/data/ABCD.xls similarity index 100% rename from doc/Archive/src/data/ABCD.xls rename to doc/OnlineDocs/src/data/ABCD.xls diff --git a/doc/Archive/src/data/ABCD1.dat b/doc/OnlineDocs/src/data/ABCD1.dat similarity index 100% rename from doc/Archive/src/data/ABCD1.dat rename to doc/OnlineDocs/src/data/ABCD1.dat diff --git a/doc/Archive/src/data/ABCD1.py b/doc/OnlineDocs/src/data/ABCD1.py similarity index 100% rename from doc/Archive/src/data/ABCD1.py rename to doc/OnlineDocs/src/data/ABCD1.py diff --git a/doc/Archive/src/data/ABCD1.txt b/doc/OnlineDocs/src/data/ABCD1.txt similarity index 100% rename from doc/Archive/src/data/ABCD1.txt rename to doc/OnlineDocs/src/data/ABCD1.txt diff --git a/doc/Archive/src/data/ABCD2.dat b/doc/OnlineDocs/src/data/ABCD2.dat similarity index 100% rename from doc/Archive/src/data/ABCD2.dat rename to doc/OnlineDocs/src/data/ABCD2.dat diff --git a/doc/Archive/src/data/ABCD2.py b/doc/OnlineDocs/src/data/ABCD2.py similarity index 100% rename from doc/Archive/src/data/ABCD2.py rename to doc/OnlineDocs/src/data/ABCD2.py diff --git a/doc/Archive/src/data/ABCD2.txt b/doc/OnlineDocs/src/data/ABCD2.txt similarity index 100% rename from doc/Archive/src/data/ABCD2.txt rename to doc/OnlineDocs/src/data/ABCD2.txt diff --git a/doc/Archive/src/data/ABCD3.dat b/doc/OnlineDocs/src/data/ABCD3.dat similarity index 100% rename from doc/Archive/src/data/ABCD3.dat rename to doc/OnlineDocs/src/data/ABCD3.dat diff --git a/doc/Archive/src/data/ABCD3.py b/doc/OnlineDocs/src/data/ABCD3.py similarity index 100% rename from doc/Archive/src/data/ABCD3.py rename to doc/OnlineDocs/src/data/ABCD3.py diff --git a/doc/Archive/src/data/ABCD3.txt b/doc/OnlineDocs/src/data/ABCD3.txt similarity index 100% rename from doc/Archive/src/data/ABCD3.txt rename to doc/OnlineDocs/src/data/ABCD3.txt diff --git a/doc/Archive/src/data/ABCD4.dat b/doc/OnlineDocs/src/data/ABCD4.dat similarity index 100% rename from doc/Archive/src/data/ABCD4.dat rename to doc/OnlineDocs/src/data/ABCD4.dat diff --git a/doc/Archive/src/data/ABCD4.py b/doc/OnlineDocs/src/data/ABCD4.py similarity index 100% rename from doc/Archive/src/data/ABCD4.py rename to doc/OnlineDocs/src/data/ABCD4.py diff --git a/doc/Archive/src/data/ABCD4.txt b/doc/OnlineDocs/src/data/ABCD4.txt similarity index 100% rename from doc/Archive/src/data/ABCD4.txt rename to doc/OnlineDocs/src/data/ABCD4.txt diff --git a/doc/Archive/src/data/ABCD5.dat b/doc/OnlineDocs/src/data/ABCD5.dat similarity index 100% rename from doc/Archive/src/data/ABCD5.dat rename to doc/OnlineDocs/src/data/ABCD5.dat diff --git a/doc/Archive/src/data/ABCD5.py b/doc/OnlineDocs/src/data/ABCD5.py similarity index 100% rename from doc/Archive/src/data/ABCD5.py rename to doc/OnlineDocs/src/data/ABCD5.py diff --git a/doc/Archive/src/data/ABCD5.txt b/doc/OnlineDocs/src/data/ABCD5.txt similarity index 100% rename from doc/Archive/src/data/ABCD5.txt rename to doc/OnlineDocs/src/data/ABCD5.txt diff --git a/doc/Archive/src/data/ABCD6.dat b/doc/OnlineDocs/src/data/ABCD6.dat similarity index 100% rename from doc/Archive/src/data/ABCD6.dat rename to doc/OnlineDocs/src/data/ABCD6.dat diff --git a/doc/Archive/src/data/ABCD6.py b/doc/OnlineDocs/src/data/ABCD6.py similarity index 100% rename from doc/Archive/src/data/ABCD6.py rename to doc/OnlineDocs/src/data/ABCD6.py diff --git a/doc/Archive/src/data/ABCD6.txt b/doc/OnlineDocs/src/data/ABCD6.txt similarity index 100% rename from doc/Archive/src/data/ABCD6.txt rename to doc/OnlineDocs/src/data/ABCD6.txt diff --git a/doc/Archive/src/data/ABCD7.dat b/doc/OnlineDocs/src/data/ABCD7.dat similarity index 100% rename from doc/Archive/src/data/ABCD7.dat rename to doc/OnlineDocs/src/data/ABCD7.dat diff --git a/doc/Archive/src/data/ABCD7.py b/doc/OnlineDocs/src/data/ABCD7.py similarity index 100% rename from doc/Archive/src/data/ABCD7.py rename to doc/OnlineDocs/src/data/ABCD7.py diff --git a/doc/Archive/src/data/ABCD7.txt b/doc/OnlineDocs/src/data/ABCD7.txt similarity index 100% rename from doc/Archive/src/data/ABCD7.txt rename to doc/OnlineDocs/src/data/ABCD7.txt diff --git a/doc/Archive/src/data/ABCD8.bad b/doc/OnlineDocs/src/data/ABCD8.bad similarity index 100% rename from doc/Archive/src/data/ABCD8.bad rename to doc/OnlineDocs/src/data/ABCD8.bad diff --git a/doc/Archive/src/data/ABCD8.dat b/doc/OnlineDocs/src/data/ABCD8.dat similarity index 100% rename from doc/Archive/src/data/ABCD8.dat rename to doc/OnlineDocs/src/data/ABCD8.dat diff --git a/doc/Archive/src/data/ABCD8.py b/doc/OnlineDocs/src/data/ABCD8.py similarity index 100% rename from doc/Archive/src/data/ABCD8.py rename to doc/OnlineDocs/src/data/ABCD8.py diff --git a/doc/Archive/src/data/ABCD9.bad b/doc/OnlineDocs/src/data/ABCD9.bad similarity index 100% rename from doc/Archive/src/data/ABCD9.bad rename to doc/OnlineDocs/src/data/ABCD9.bad diff --git a/doc/Archive/src/data/ABCD9.dat b/doc/OnlineDocs/src/data/ABCD9.dat similarity index 100% rename from doc/Archive/src/data/ABCD9.dat rename to doc/OnlineDocs/src/data/ABCD9.dat diff --git a/doc/Archive/src/data/ABCD9.py b/doc/OnlineDocs/src/data/ABCD9.py similarity index 100% rename from doc/Archive/src/data/ABCD9.py rename to doc/OnlineDocs/src/data/ABCD9.py diff --git a/doc/Archive/src/data/C.tab b/doc/OnlineDocs/src/data/C.tab similarity index 100% rename from doc/Archive/src/data/C.tab rename to doc/OnlineDocs/src/data/C.tab diff --git a/doc/Archive/src/data/D.tab b/doc/OnlineDocs/src/data/D.tab similarity index 100% rename from doc/Archive/src/data/D.tab rename to doc/OnlineDocs/src/data/D.tab diff --git a/doc/Archive/src/data/U.tab b/doc/OnlineDocs/src/data/U.tab similarity index 100% rename from doc/Archive/src/data/U.tab rename to doc/OnlineDocs/src/data/U.tab diff --git a/doc/Archive/src/data/Y.tab b/doc/OnlineDocs/src/data/Y.tab similarity index 100% rename from doc/Archive/src/data/Y.tab rename to doc/OnlineDocs/src/data/Y.tab diff --git a/doc/Archive/src/data/Z.tab b/doc/OnlineDocs/src/data/Z.tab similarity index 100% rename from doc/Archive/src/data/Z.tab rename to doc/OnlineDocs/src/data/Z.tab diff --git a/doc/Archive/src/data/data_managers.txt b/doc/OnlineDocs/src/data/data_managers.txt similarity index 100% rename from doc/Archive/src/data/data_managers.txt rename to doc/OnlineDocs/src/data/data_managers.txt diff --git a/doc/Archive/src/data/diet.dat b/doc/OnlineDocs/src/data/diet.dat similarity index 100% rename from doc/Archive/src/data/diet.dat rename to doc/OnlineDocs/src/data/diet.dat diff --git a/doc/Archive/src/data/diet.sql b/doc/OnlineDocs/src/data/diet.sql similarity index 100% rename from doc/Archive/src/data/diet.sql rename to doc/OnlineDocs/src/data/diet.sql diff --git a/doc/Archive/src/data/diet.sqlite b/doc/OnlineDocs/src/data/diet.sqlite similarity index 100% rename from doc/Archive/src/data/diet.sqlite rename to doc/OnlineDocs/src/data/diet.sqlite diff --git a/doc/Archive/src/data/diet.sqlite.dat b/doc/OnlineDocs/src/data/diet.sqlite.dat similarity index 100% rename from doc/Archive/src/data/diet.sqlite.dat rename to doc/OnlineDocs/src/data/diet.sqlite.dat diff --git a/doc/Archive/src/data/diet1.py b/doc/OnlineDocs/src/data/diet1.py similarity index 100% rename from doc/Archive/src/data/diet1.py rename to doc/OnlineDocs/src/data/diet1.py diff --git a/doc/Archive/src/data/ex.dat b/doc/OnlineDocs/src/data/ex.dat similarity index 100% rename from doc/Archive/src/data/ex.dat rename to doc/OnlineDocs/src/data/ex.dat diff --git a/doc/Archive/src/data/ex.py b/doc/OnlineDocs/src/data/ex.py similarity index 100% rename from doc/Archive/src/data/ex.py rename to doc/OnlineDocs/src/data/ex.py diff --git a/doc/Archive/src/data/ex.txt b/doc/OnlineDocs/src/data/ex.txt similarity index 100% rename from doc/Archive/src/data/ex.txt rename to doc/OnlineDocs/src/data/ex.txt diff --git a/doc/Archive/src/data/ex1.dat b/doc/OnlineDocs/src/data/ex1.dat similarity index 100% rename from doc/Archive/src/data/ex1.dat rename to doc/OnlineDocs/src/data/ex1.dat diff --git a/doc/Archive/src/data/ex2.dat b/doc/OnlineDocs/src/data/ex2.dat similarity index 100% rename from doc/Archive/src/data/ex2.dat rename to doc/OnlineDocs/src/data/ex2.dat diff --git a/doc/Archive/src/data/import1.tab.dat b/doc/OnlineDocs/src/data/import1.tab.dat similarity index 100% rename from doc/Archive/src/data/import1.tab.dat rename to doc/OnlineDocs/src/data/import1.tab.dat diff --git a/doc/Archive/src/data/import1.tab.py b/doc/OnlineDocs/src/data/import1.tab.py similarity index 100% rename from doc/Archive/src/data/import1.tab.py rename to doc/OnlineDocs/src/data/import1.tab.py diff --git a/doc/Archive/src/data/import1.tab.txt b/doc/OnlineDocs/src/data/import1.tab.txt similarity index 100% rename from doc/Archive/src/data/import1.tab.txt rename to doc/OnlineDocs/src/data/import1.tab.txt diff --git a/doc/Archive/src/data/import2.tab.dat b/doc/OnlineDocs/src/data/import2.tab.dat similarity index 100% rename from doc/Archive/src/data/import2.tab.dat rename to doc/OnlineDocs/src/data/import2.tab.dat diff --git a/doc/Archive/src/data/import2.tab.py b/doc/OnlineDocs/src/data/import2.tab.py similarity index 100% rename from doc/Archive/src/data/import2.tab.py rename to doc/OnlineDocs/src/data/import2.tab.py diff --git a/doc/Archive/src/data/import2.tab.txt b/doc/OnlineDocs/src/data/import2.tab.txt similarity index 100% rename from doc/Archive/src/data/import2.tab.txt rename to doc/OnlineDocs/src/data/import2.tab.txt diff --git a/doc/Archive/src/data/import3.tab.dat b/doc/OnlineDocs/src/data/import3.tab.dat similarity index 100% rename from doc/Archive/src/data/import3.tab.dat rename to doc/OnlineDocs/src/data/import3.tab.dat diff --git a/doc/Archive/src/data/import3.tab.py b/doc/OnlineDocs/src/data/import3.tab.py similarity index 100% rename from doc/Archive/src/data/import3.tab.py rename to doc/OnlineDocs/src/data/import3.tab.py diff --git a/doc/Archive/src/data/import3.tab.txt b/doc/OnlineDocs/src/data/import3.tab.txt similarity index 100% rename from doc/Archive/src/data/import3.tab.txt rename to doc/OnlineDocs/src/data/import3.tab.txt diff --git a/doc/Archive/src/data/import4.tab.dat b/doc/OnlineDocs/src/data/import4.tab.dat similarity index 100% rename from doc/Archive/src/data/import4.tab.dat rename to doc/OnlineDocs/src/data/import4.tab.dat diff --git a/doc/Archive/src/data/import4.tab.py b/doc/OnlineDocs/src/data/import4.tab.py similarity index 100% rename from doc/Archive/src/data/import4.tab.py rename to doc/OnlineDocs/src/data/import4.tab.py diff --git a/doc/Archive/src/data/import4.tab.txt b/doc/OnlineDocs/src/data/import4.tab.txt similarity index 100% rename from doc/Archive/src/data/import4.tab.txt rename to doc/OnlineDocs/src/data/import4.tab.txt diff --git a/doc/Archive/src/data/import5.tab.dat b/doc/OnlineDocs/src/data/import5.tab.dat similarity index 100% rename from doc/Archive/src/data/import5.tab.dat rename to doc/OnlineDocs/src/data/import5.tab.dat diff --git a/doc/Archive/src/data/import5.tab.py b/doc/OnlineDocs/src/data/import5.tab.py similarity index 100% rename from doc/Archive/src/data/import5.tab.py rename to doc/OnlineDocs/src/data/import5.tab.py diff --git a/doc/Archive/src/data/import5.tab.txt b/doc/OnlineDocs/src/data/import5.tab.txt similarity index 100% rename from doc/Archive/src/data/import5.tab.txt rename to doc/OnlineDocs/src/data/import5.tab.txt diff --git a/doc/Archive/src/data/import6.tab.dat b/doc/OnlineDocs/src/data/import6.tab.dat similarity index 100% rename from doc/Archive/src/data/import6.tab.dat rename to doc/OnlineDocs/src/data/import6.tab.dat diff --git a/doc/Archive/src/data/import6.tab.py b/doc/OnlineDocs/src/data/import6.tab.py similarity index 100% rename from doc/Archive/src/data/import6.tab.py rename to doc/OnlineDocs/src/data/import6.tab.py diff --git a/doc/Archive/src/data/import6.tab.txt b/doc/OnlineDocs/src/data/import6.tab.txt similarity index 100% rename from doc/Archive/src/data/import6.tab.txt rename to doc/OnlineDocs/src/data/import6.tab.txt diff --git a/doc/Archive/src/data/import7.tab.dat b/doc/OnlineDocs/src/data/import7.tab.dat similarity index 100% rename from doc/Archive/src/data/import7.tab.dat rename to doc/OnlineDocs/src/data/import7.tab.dat diff --git a/doc/Archive/src/data/import7.tab.py b/doc/OnlineDocs/src/data/import7.tab.py similarity index 100% rename from doc/Archive/src/data/import7.tab.py rename to doc/OnlineDocs/src/data/import7.tab.py diff --git a/doc/Archive/src/data/import7.tab.txt b/doc/OnlineDocs/src/data/import7.tab.txt similarity index 100% rename from doc/Archive/src/data/import7.tab.txt rename to doc/OnlineDocs/src/data/import7.tab.txt diff --git a/doc/Archive/src/data/import8.tab.dat b/doc/OnlineDocs/src/data/import8.tab.dat similarity index 100% rename from doc/Archive/src/data/import8.tab.dat rename to doc/OnlineDocs/src/data/import8.tab.dat diff --git a/doc/Archive/src/data/import8.tab.py b/doc/OnlineDocs/src/data/import8.tab.py similarity index 100% rename from doc/Archive/src/data/import8.tab.py rename to doc/OnlineDocs/src/data/import8.tab.py diff --git a/doc/Archive/src/data/import8.tab.txt b/doc/OnlineDocs/src/data/import8.tab.txt similarity index 100% rename from doc/Archive/src/data/import8.tab.txt rename to doc/OnlineDocs/src/data/import8.tab.txt diff --git a/doc/Archive/src/data/namespace1.dat b/doc/OnlineDocs/src/data/namespace1.dat similarity index 100% rename from doc/Archive/src/data/namespace1.dat rename to doc/OnlineDocs/src/data/namespace1.dat diff --git a/doc/Archive/src/data/param1.dat b/doc/OnlineDocs/src/data/param1.dat similarity index 100% rename from doc/Archive/src/data/param1.dat rename to doc/OnlineDocs/src/data/param1.dat diff --git a/doc/Archive/src/data/param1.py b/doc/OnlineDocs/src/data/param1.py similarity index 100% rename from doc/Archive/src/data/param1.py rename to doc/OnlineDocs/src/data/param1.py diff --git a/doc/Archive/src/data/param1.txt b/doc/OnlineDocs/src/data/param1.txt similarity index 100% rename from doc/Archive/src/data/param1.txt rename to doc/OnlineDocs/src/data/param1.txt diff --git a/doc/Archive/src/data/param2.dat b/doc/OnlineDocs/src/data/param2.dat similarity index 100% rename from doc/Archive/src/data/param2.dat rename to doc/OnlineDocs/src/data/param2.dat diff --git a/doc/Archive/src/data/param2.py b/doc/OnlineDocs/src/data/param2.py similarity index 100% rename from doc/Archive/src/data/param2.py rename to doc/OnlineDocs/src/data/param2.py diff --git a/doc/Archive/src/data/param2.txt b/doc/OnlineDocs/src/data/param2.txt similarity index 100% rename from doc/Archive/src/data/param2.txt rename to doc/OnlineDocs/src/data/param2.txt diff --git a/doc/Archive/src/data/param2a.dat b/doc/OnlineDocs/src/data/param2a.dat similarity index 100% rename from doc/Archive/src/data/param2a.dat rename to doc/OnlineDocs/src/data/param2a.dat diff --git a/doc/Archive/src/data/param2a.py b/doc/OnlineDocs/src/data/param2a.py similarity index 100% rename from doc/Archive/src/data/param2a.py rename to doc/OnlineDocs/src/data/param2a.py diff --git a/doc/Archive/src/data/param2a.txt b/doc/OnlineDocs/src/data/param2a.txt similarity index 100% rename from doc/Archive/src/data/param2a.txt rename to doc/OnlineDocs/src/data/param2a.txt diff --git a/doc/Archive/src/data/param3.dat b/doc/OnlineDocs/src/data/param3.dat similarity index 100% rename from doc/Archive/src/data/param3.dat rename to doc/OnlineDocs/src/data/param3.dat diff --git a/doc/Archive/src/data/param3.py b/doc/OnlineDocs/src/data/param3.py similarity index 100% rename from doc/Archive/src/data/param3.py rename to doc/OnlineDocs/src/data/param3.py diff --git a/doc/Archive/src/data/param3.txt b/doc/OnlineDocs/src/data/param3.txt similarity index 100% rename from doc/Archive/src/data/param3.txt rename to doc/OnlineDocs/src/data/param3.txt diff --git a/doc/Archive/src/data/param3a.dat b/doc/OnlineDocs/src/data/param3a.dat similarity index 100% rename from doc/Archive/src/data/param3a.dat rename to doc/OnlineDocs/src/data/param3a.dat diff --git a/doc/Archive/src/data/param3a.py b/doc/OnlineDocs/src/data/param3a.py similarity index 100% rename from doc/Archive/src/data/param3a.py rename to doc/OnlineDocs/src/data/param3a.py diff --git a/doc/Archive/src/data/param3a.txt b/doc/OnlineDocs/src/data/param3a.txt similarity index 100% rename from doc/Archive/src/data/param3a.txt rename to doc/OnlineDocs/src/data/param3a.txt diff --git a/doc/Archive/src/data/param3b.dat b/doc/OnlineDocs/src/data/param3b.dat similarity index 100% rename from doc/Archive/src/data/param3b.dat rename to doc/OnlineDocs/src/data/param3b.dat diff --git a/doc/Archive/src/data/param3b.py b/doc/OnlineDocs/src/data/param3b.py similarity index 100% rename from doc/Archive/src/data/param3b.py rename to doc/OnlineDocs/src/data/param3b.py diff --git a/doc/Archive/src/data/param3b.txt b/doc/OnlineDocs/src/data/param3b.txt similarity index 100% rename from doc/Archive/src/data/param3b.txt rename to doc/OnlineDocs/src/data/param3b.txt diff --git a/doc/Archive/src/data/param3c.dat b/doc/OnlineDocs/src/data/param3c.dat similarity index 100% rename from doc/Archive/src/data/param3c.dat rename to doc/OnlineDocs/src/data/param3c.dat diff --git a/doc/Archive/src/data/param3c.py b/doc/OnlineDocs/src/data/param3c.py similarity index 100% rename from doc/Archive/src/data/param3c.py rename to doc/OnlineDocs/src/data/param3c.py diff --git a/doc/Archive/src/data/param3c.txt b/doc/OnlineDocs/src/data/param3c.txt similarity index 100% rename from doc/Archive/src/data/param3c.txt rename to doc/OnlineDocs/src/data/param3c.txt diff --git a/doc/Archive/src/data/param4.dat b/doc/OnlineDocs/src/data/param4.dat similarity index 100% rename from doc/Archive/src/data/param4.dat rename to doc/OnlineDocs/src/data/param4.dat diff --git a/doc/Archive/src/data/param4.py b/doc/OnlineDocs/src/data/param4.py similarity index 100% rename from doc/Archive/src/data/param4.py rename to doc/OnlineDocs/src/data/param4.py diff --git a/doc/Archive/src/data/param4.txt b/doc/OnlineDocs/src/data/param4.txt similarity index 100% rename from doc/Archive/src/data/param4.txt rename to doc/OnlineDocs/src/data/param4.txt diff --git a/doc/Archive/src/data/param5.dat b/doc/OnlineDocs/src/data/param5.dat similarity index 100% rename from doc/Archive/src/data/param5.dat rename to doc/OnlineDocs/src/data/param5.dat diff --git a/doc/Archive/src/data/param5.py b/doc/OnlineDocs/src/data/param5.py similarity index 100% rename from doc/Archive/src/data/param5.py rename to doc/OnlineDocs/src/data/param5.py diff --git a/doc/Archive/src/data/param5.txt b/doc/OnlineDocs/src/data/param5.txt similarity index 100% rename from doc/Archive/src/data/param5.txt rename to doc/OnlineDocs/src/data/param5.txt diff --git a/doc/Archive/src/data/param5a.dat b/doc/OnlineDocs/src/data/param5a.dat similarity index 100% rename from doc/Archive/src/data/param5a.dat rename to doc/OnlineDocs/src/data/param5a.dat diff --git a/doc/Archive/src/data/param5a.py b/doc/OnlineDocs/src/data/param5a.py similarity index 100% rename from doc/Archive/src/data/param5a.py rename to doc/OnlineDocs/src/data/param5a.py diff --git a/doc/Archive/src/data/param5a.txt b/doc/OnlineDocs/src/data/param5a.txt similarity index 100% rename from doc/Archive/src/data/param5a.txt rename to doc/OnlineDocs/src/data/param5a.txt diff --git a/doc/Archive/src/data/param6.dat b/doc/OnlineDocs/src/data/param6.dat similarity index 100% rename from doc/Archive/src/data/param6.dat rename to doc/OnlineDocs/src/data/param6.dat diff --git a/doc/Archive/src/data/param6.py b/doc/OnlineDocs/src/data/param6.py similarity index 100% rename from doc/Archive/src/data/param6.py rename to doc/OnlineDocs/src/data/param6.py diff --git a/doc/Archive/src/data/param6.txt b/doc/OnlineDocs/src/data/param6.txt similarity index 100% rename from doc/Archive/src/data/param6.txt rename to doc/OnlineDocs/src/data/param6.txt diff --git a/doc/Archive/src/data/param6a.dat b/doc/OnlineDocs/src/data/param6a.dat similarity index 100% rename from doc/Archive/src/data/param6a.dat rename to doc/OnlineDocs/src/data/param6a.dat diff --git a/doc/Archive/src/data/param6a.py b/doc/OnlineDocs/src/data/param6a.py similarity index 100% rename from doc/Archive/src/data/param6a.py rename to doc/OnlineDocs/src/data/param6a.py diff --git a/doc/Archive/src/data/param6a.txt b/doc/OnlineDocs/src/data/param6a.txt similarity index 100% rename from doc/Archive/src/data/param6a.txt rename to doc/OnlineDocs/src/data/param6a.txt diff --git a/doc/Archive/src/data/param7a.dat b/doc/OnlineDocs/src/data/param7a.dat similarity index 100% rename from doc/Archive/src/data/param7a.dat rename to doc/OnlineDocs/src/data/param7a.dat diff --git a/doc/Archive/src/data/param7a.py b/doc/OnlineDocs/src/data/param7a.py similarity index 100% rename from doc/Archive/src/data/param7a.py rename to doc/OnlineDocs/src/data/param7a.py diff --git a/doc/Archive/src/data/param7a.txt b/doc/OnlineDocs/src/data/param7a.txt similarity index 100% rename from doc/Archive/src/data/param7a.txt rename to doc/OnlineDocs/src/data/param7a.txt diff --git a/doc/Archive/src/data/param7b.dat b/doc/OnlineDocs/src/data/param7b.dat similarity index 100% rename from doc/Archive/src/data/param7b.dat rename to doc/OnlineDocs/src/data/param7b.dat diff --git a/doc/Archive/src/data/param7b.py b/doc/OnlineDocs/src/data/param7b.py similarity index 100% rename from doc/Archive/src/data/param7b.py rename to doc/OnlineDocs/src/data/param7b.py diff --git a/doc/Archive/src/data/param7b.txt b/doc/OnlineDocs/src/data/param7b.txt similarity index 100% rename from doc/Archive/src/data/param7b.txt rename to doc/OnlineDocs/src/data/param7b.txt diff --git a/doc/Archive/src/data/param8a.dat b/doc/OnlineDocs/src/data/param8a.dat similarity index 100% rename from doc/Archive/src/data/param8a.dat rename to doc/OnlineDocs/src/data/param8a.dat diff --git a/doc/Archive/src/data/param8a.py b/doc/OnlineDocs/src/data/param8a.py similarity index 100% rename from doc/Archive/src/data/param8a.py rename to doc/OnlineDocs/src/data/param8a.py diff --git a/doc/Archive/src/data/param8a.txt b/doc/OnlineDocs/src/data/param8a.txt similarity index 100% rename from doc/Archive/src/data/param8a.txt rename to doc/OnlineDocs/src/data/param8a.txt diff --git a/doc/Archive/src/data/pyomo.diet1.sh b/doc/OnlineDocs/src/data/pyomo.diet1.sh similarity index 100% rename from doc/Archive/src/data/pyomo.diet1.sh rename to doc/OnlineDocs/src/data/pyomo.diet1.sh diff --git a/doc/Archive/src/data/pyomo.diet1.txt b/doc/OnlineDocs/src/data/pyomo.diet1.txt similarity index 100% rename from doc/Archive/src/data/pyomo.diet1.txt rename to doc/OnlineDocs/src/data/pyomo.diet1.txt diff --git a/doc/Archive/src/data/pyomo.diet2.sh b/doc/OnlineDocs/src/data/pyomo.diet2.sh similarity index 100% rename from doc/Archive/src/data/pyomo.diet2.sh rename to doc/OnlineDocs/src/data/pyomo.diet2.sh diff --git a/doc/Archive/src/data/pyomo.diet2.txt b/doc/OnlineDocs/src/data/pyomo.diet2.txt similarity index 100% rename from doc/Archive/src/data/pyomo.diet2.txt rename to doc/OnlineDocs/src/data/pyomo.diet2.txt diff --git a/doc/Archive/src/data/set1.dat b/doc/OnlineDocs/src/data/set1.dat similarity index 100% rename from doc/Archive/src/data/set1.dat rename to doc/OnlineDocs/src/data/set1.dat diff --git a/doc/Archive/src/data/set1.py b/doc/OnlineDocs/src/data/set1.py similarity index 100% rename from doc/Archive/src/data/set1.py rename to doc/OnlineDocs/src/data/set1.py diff --git a/doc/Archive/src/data/set1.txt b/doc/OnlineDocs/src/data/set1.txt similarity index 100% rename from doc/Archive/src/data/set1.txt rename to doc/OnlineDocs/src/data/set1.txt diff --git a/doc/Archive/src/data/set2.dat b/doc/OnlineDocs/src/data/set2.dat similarity index 100% rename from doc/Archive/src/data/set2.dat rename to doc/OnlineDocs/src/data/set2.dat diff --git a/doc/Archive/src/data/set2.py b/doc/OnlineDocs/src/data/set2.py similarity index 100% rename from doc/Archive/src/data/set2.py rename to doc/OnlineDocs/src/data/set2.py diff --git a/doc/Archive/src/data/set2.txt b/doc/OnlineDocs/src/data/set2.txt similarity index 100% rename from doc/Archive/src/data/set2.txt rename to doc/OnlineDocs/src/data/set2.txt diff --git a/doc/Archive/src/data/set2a.dat b/doc/OnlineDocs/src/data/set2a.dat similarity index 100% rename from doc/Archive/src/data/set2a.dat rename to doc/OnlineDocs/src/data/set2a.dat diff --git a/doc/Archive/src/data/set2a.py b/doc/OnlineDocs/src/data/set2a.py similarity index 100% rename from doc/Archive/src/data/set2a.py rename to doc/OnlineDocs/src/data/set2a.py diff --git a/doc/Archive/src/data/set2a.txt b/doc/OnlineDocs/src/data/set2a.txt similarity index 100% rename from doc/Archive/src/data/set2a.txt rename to doc/OnlineDocs/src/data/set2a.txt diff --git a/doc/Archive/src/data/set3.dat b/doc/OnlineDocs/src/data/set3.dat similarity index 100% rename from doc/Archive/src/data/set3.dat rename to doc/OnlineDocs/src/data/set3.dat diff --git a/doc/Archive/src/data/set3.py b/doc/OnlineDocs/src/data/set3.py similarity index 100% rename from doc/Archive/src/data/set3.py rename to doc/OnlineDocs/src/data/set3.py diff --git a/doc/Archive/src/data/set3.txt b/doc/OnlineDocs/src/data/set3.txt similarity index 100% rename from doc/Archive/src/data/set3.txt rename to doc/OnlineDocs/src/data/set3.txt diff --git a/doc/Archive/src/data/set4.dat b/doc/OnlineDocs/src/data/set4.dat similarity index 100% rename from doc/Archive/src/data/set4.dat rename to doc/OnlineDocs/src/data/set4.dat diff --git a/doc/Archive/src/data/set4.py b/doc/OnlineDocs/src/data/set4.py similarity index 100% rename from doc/Archive/src/data/set4.py rename to doc/OnlineDocs/src/data/set4.py diff --git a/doc/Archive/src/data/set4.txt b/doc/OnlineDocs/src/data/set4.txt similarity index 100% rename from doc/Archive/src/data/set4.txt rename to doc/OnlineDocs/src/data/set4.txt diff --git a/doc/Archive/src/data/set5.dat b/doc/OnlineDocs/src/data/set5.dat similarity index 100% rename from doc/Archive/src/data/set5.dat rename to doc/OnlineDocs/src/data/set5.dat diff --git a/doc/Archive/src/data/set5.py b/doc/OnlineDocs/src/data/set5.py similarity index 100% rename from doc/Archive/src/data/set5.py rename to doc/OnlineDocs/src/data/set5.py diff --git a/doc/Archive/src/data/set5.txt b/doc/OnlineDocs/src/data/set5.txt similarity index 100% rename from doc/Archive/src/data/set5.txt rename to doc/OnlineDocs/src/data/set5.txt diff --git a/doc/Archive/src/data/table0.dat b/doc/OnlineDocs/src/data/table0.dat similarity index 100% rename from doc/Archive/src/data/table0.dat rename to doc/OnlineDocs/src/data/table0.dat diff --git a/doc/Archive/src/data/table0.py b/doc/OnlineDocs/src/data/table0.py similarity index 100% rename from doc/Archive/src/data/table0.py rename to doc/OnlineDocs/src/data/table0.py diff --git a/doc/Archive/src/data/table0.txt b/doc/OnlineDocs/src/data/table0.txt similarity index 100% rename from doc/Archive/src/data/table0.txt rename to doc/OnlineDocs/src/data/table0.txt diff --git a/doc/Archive/src/data/table0.ul.dat b/doc/OnlineDocs/src/data/table0.ul.dat similarity index 100% rename from doc/Archive/src/data/table0.ul.dat rename to doc/OnlineDocs/src/data/table0.ul.dat diff --git a/doc/Archive/src/data/table0.ul.py b/doc/OnlineDocs/src/data/table0.ul.py similarity index 100% rename from doc/Archive/src/data/table0.ul.py rename to doc/OnlineDocs/src/data/table0.ul.py diff --git a/doc/Archive/src/data/table0.ul.txt b/doc/OnlineDocs/src/data/table0.ul.txt similarity index 100% rename from doc/Archive/src/data/table0.ul.txt rename to doc/OnlineDocs/src/data/table0.ul.txt diff --git a/doc/Archive/src/data/table1.dat b/doc/OnlineDocs/src/data/table1.dat similarity index 100% rename from doc/Archive/src/data/table1.dat rename to doc/OnlineDocs/src/data/table1.dat diff --git a/doc/Archive/src/data/table1.py b/doc/OnlineDocs/src/data/table1.py similarity index 100% rename from doc/Archive/src/data/table1.py rename to doc/OnlineDocs/src/data/table1.py diff --git a/doc/Archive/src/data/table1.txt b/doc/OnlineDocs/src/data/table1.txt similarity index 100% rename from doc/Archive/src/data/table1.txt rename to doc/OnlineDocs/src/data/table1.txt diff --git a/doc/Archive/src/data/table2.dat b/doc/OnlineDocs/src/data/table2.dat similarity index 100% rename from doc/Archive/src/data/table2.dat rename to doc/OnlineDocs/src/data/table2.dat diff --git a/doc/Archive/src/data/table2.py b/doc/OnlineDocs/src/data/table2.py similarity index 100% rename from doc/Archive/src/data/table2.py rename to doc/OnlineDocs/src/data/table2.py diff --git a/doc/Archive/src/data/table2.txt b/doc/OnlineDocs/src/data/table2.txt similarity index 100% rename from doc/Archive/src/data/table2.txt rename to doc/OnlineDocs/src/data/table2.txt diff --git a/doc/Archive/src/data/table3.dat b/doc/OnlineDocs/src/data/table3.dat similarity index 100% rename from doc/Archive/src/data/table3.dat rename to doc/OnlineDocs/src/data/table3.dat diff --git a/doc/Archive/src/data/table3.py b/doc/OnlineDocs/src/data/table3.py similarity index 100% rename from doc/Archive/src/data/table3.py rename to doc/OnlineDocs/src/data/table3.py diff --git a/doc/Archive/src/data/table3.txt b/doc/OnlineDocs/src/data/table3.txt similarity index 100% rename from doc/Archive/src/data/table3.txt rename to doc/OnlineDocs/src/data/table3.txt diff --git a/doc/Archive/src/data/table3.ul.dat b/doc/OnlineDocs/src/data/table3.ul.dat similarity index 100% rename from doc/Archive/src/data/table3.ul.dat rename to doc/OnlineDocs/src/data/table3.ul.dat diff --git a/doc/Archive/src/data/table3.ul.py b/doc/OnlineDocs/src/data/table3.ul.py similarity index 100% rename from doc/Archive/src/data/table3.ul.py rename to doc/OnlineDocs/src/data/table3.ul.py diff --git a/doc/Archive/src/data/table3.ul.txt b/doc/OnlineDocs/src/data/table3.ul.txt similarity index 100% rename from doc/Archive/src/data/table3.ul.txt rename to doc/OnlineDocs/src/data/table3.ul.txt diff --git a/doc/Archive/src/data/table4.dat b/doc/OnlineDocs/src/data/table4.dat similarity index 100% rename from doc/Archive/src/data/table4.dat rename to doc/OnlineDocs/src/data/table4.dat diff --git a/doc/Archive/src/data/table4.py b/doc/OnlineDocs/src/data/table4.py similarity index 100% rename from doc/Archive/src/data/table4.py rename to doc/OnlineDocs/src/data/table4.py diff --git a/doc/Archive/src/data/table4.txt b/doc/OnlineDocs/src/data/table4.txt similarity index 100% rename from doc/Archive/src/data/table4.txt rename to doc/OnlineDocs/src/data/table4.txt diff --git a/doc/Archive/src/data/table4.ul.dat b/doc/OnlineDocs/src/data/table4.ul.dat similarity index 100% rename from doc/Archive/src/data/table4.ul.dat rename to doc/OnlineDocs/src/data/table4.ul.dat diff --git a/doc/Archive/src/data/table4.ul.py b/doc/OnlineDocs/src/data/table4.ul.py similarity index 100% rename from doc/Archive/src/data/table4.ul.py rename to doc/OnlineDocs/src/data/table4.ul.py diff --git a/doc/Archive/src/data/table4.ul.txt b/doc/OnlineDocs/src/data/table4.ul.txt similarity index 100% rename from doc/Archive/src/data/table4.ul.txt rename to doc/OnlineDocs/src/data/table4.ul.txt diff --git a/doc/Archive/src/data/table5.dat b/doc/OnlineDocs/src/data/table5.dat similarity index 100% rename from doc/Archive/src/data/table5.dat rename to doc/OnlineDocs/src/data/table5.dat diff --git a/doc/Archive/src/data/table5.py b/doc/OnlineDocs/src/data/table5.py similarity index 100% rename from doc/Archive/src/data/table5.py rename to doc/OnlineDocs/src/data/table5.py diff --git a/doc/Archive/src/data/table5.txt b/doc/OnlineDocs/src/data/table5.txt similarity index 100% rename from doc/Archive/src/data/table5.txt rename to doc/OnlineDocs/src/data/table5.txt diff --git a/doc/Archive/src/data/table6.dat b/doc/OnlineDocs/src/data/table6.dat similarity index 100% rename from doc/Archive/src/data/table6.dat rename to doc/OnlineDocs/src/data/table6.dat diff --git a/doc/Archive/src/data/table6.py b/doc/OnlineDocs/src/data/table6.py similarity index 100% rename from doc/Archive/src/data/table6.py rename to doc/OnlineDocs/src/data/table6.py diff --git a/doc/Archive/src/data/table6.txt b/doc/OnlineDocs/src/data/table6.txt similarity index 100% rename from doc/Archive/src/data/table6.txt rename to doc/OnlineDocs/src/data/table6.txt diff --git a/doc/Archive/src/data/table7.dat b/doc/OnlineDocs/src/data/table7.dat similarity index 100% rename from doc/Archive/src/data/table7.dat rename to doc/OnlineDocs/src/data/table7.dat diff --git a/doc/Archive/src/data/table7.py b/doc/OnlineDocs/src/data/table7.py similarity index 100% rename from doc/Archive/src/data/table7.py rename to doc/OnlineDocs/src/data/table7.py diff --git a/doc/Archive/src/data/table7.txt b/doc/OnlineDocs/src/data/table7.txt similarity index 100% rename from doc/Archive/src/data/table7.txt rename to doc/OnlineDocs/src/data/table7.txt diff --git a/doc/Archive/src/dataportal/A.tab b/doc/OnlineDocs/src/dataportal/A.tab similarity index 100% rename from doc/Archive/src/dataportal/A.tab rename to doc/OnlineDocs/src/dataportal/A.tab diff --git a/doc/Archive/src/dataportal/C.tab b/doc/OnlineDocs/src/dataportal/C.tab similarity index 100% rename from doc/Archive/src/dataportal/C.tab rename to doc/OnlineDocs/src/dataportal/C.tab diff --git a/doc/Archive/src/dataportal/D.tab b/doc/OnlineDocs/src/dataportal/D.tab similarity index 100% rename from doc/Archive/src/dataportal/D.tab rename to doc/OnlineDocs/src/dataportal/D.tab diff --git a/doc/Archive/src/dataportal/PP.csv b/doc/OnlineDocs/src/dataportal/PP.csv similarity index 100% rename from doc/Archive/src/dataportal/PP.csv rename to doc/OnlineDocs/src/dataportal/PP.csv diff --git a/doc/Archive/src/dataportal/PP.json b/doc/OnlineDocs/src/dataportal/PP.json similarity index 100% rename from doc/Archive/src/dataportal/PP.json rename to doc/OnlineDocs/src/dataportal/PP.json diff --git a/doc/Archive/src/dataportal/PP.sqlite b/doc/OnlineDocs/src/dataportal/PP.sqlite similarity index 100% rename from doc/Archive/src/dataportal/PP.sqlite rename to doc/OnlineDocs/src/dataportal/PP.sqlite diff --git a/doc/Archive/src/dataportal/PP.tab b/doc/OnlineDocs/src/dataportal/PP.tab similarity index 100% rename from doc/Archive/src/dataportal/PP.tab rename to doc/OnlineDocs/src/dataportal/PP.tab diff --git a/doc/Archive/src/dataportal/PP.xml b/doc/OnlineDocs/src/dataportal/PP.xml similarity index 100% rename from doc/Archive/src/dataportal/PP.xml rename to doc/OnlineDocs/src/dataportal/PP.xml diff --git a/doc/Archive/src/dataportal/PP.yaml b/doc/OnlineDocs/src/dataportal/PP.yaml similarity index 100% rename from doc/Archive/src/dataportal/PP.yaml rename to doc/OnlineDocs/src/dataportal/PP.yaml diff --git a/doc/Archive/src/dataportal/PP_sqlite.py b/doc/OnlineDocs/src/dataportal/PP_sqlite.py similarity index 100% rename from doc/Archive/src/dataportal/PP_sqlite.py rename to doc/OnlineDocs/src/dataportal/PP_sqlite.py diff --git a/doc/Archive/src/dataportal/Pyomo_mysql b/doc/OnlineDocs/src/dataportal/Pyomo_mysql similarity index 100% rename from doc/Archive/src/dataportal/Pyomo_mysql rename to doc/OnlineDocs/src/dataportal/Pyomo_mysql diff --git a/doc/Archive/src/dataportal/S.tab b/doc/OnlineDocs/src/dataportal/S.tab similarity index 100% rename from doc/Archive/src/dataportal/S.tab rename to doc/OnlineDocs/src/dataportal/S.tab diff --git a/doc/Archive/src/dataportal/T.json b/doc/OnlineDocs/src/dataportal/T.json similarity index 100% rename from doc/Archive/src/dataportal/T.json rename to doc/OnlineDocs/src/dataportal/T.json diff --git a/doc/Archive/src/dataportal/T.yaml b/doc/OnlineDocs/src/dataportal/T.yaml similarity index 100% rename from doc/Archive/src/dataportal/T.yaml rename to doc/OnlineDocs/src/dataportal/T.yaml diff --git a/doc/Archive/src/dataportal/U.tab b/doc/OnlineDocs/src/dataportal/U.tab similarity index 100% rename from doc/Archive/src/dataportal/U.tab rename to doc/OnlineDocs/src/dataportal/U.tab diff --git a/doc/Archive/src/dataportal/XW.tab b/doc/OnlineDocs/src/dataportal/XW.tab similarity index 100% rename from doc/Archive/src/dataportal/XW.tab rename to doc/OnlineDocs/src/dataportal/XW.tab diff --git a/doc/Archive/src/dataportal/Y.tab b/doc/OnlineDocs/src/dataportal/Y.tab similarity index 100% rename from doc/Archive/src/dataportal/Y.tab rename to doc/OnlineDocs/src/dataportal/Y.tab diff --git a/doc/Archive/src/dataportal/Z.tab b/doc/OnlineDocs/src/dataportal/Z.tab similarity index 100% rename from doc/Archive/src/dataportal/Z.tab rename to doc/OnlineDocs/src/dataportal/Z.tab diff --git a/doc/Archive/src/dataportal/dataportal_tab.py b/doc/OnlineDocs/src/dataportal/dataportal_tab.py similarity index 100% rename from doc/Archive/src/dataportal/dataportal_tab.py rename to doc/OnlineDocs/src/dataportal/dataportal_tab.py diff --git a/doc/Archive/src/dataportal/dataportal_tab.txt b/doc/OnlineDocs/src/dataportal/dataportal_tab.txt similarity index 100% rename from doc/Archive/src/dataportal/dataportal_tab.txt rename to doc/OnlineDocs/src/dataportal/dataportal_tab.txt diff --git a/doc/Archive/src/dataportal/excel.xls b/doc/OnlineDocs/src/dataportal/excel.xls similarity index 100% rename from doc/Archive/src/dataportal/excel.xls rename to doc/OnlineDocs/src/dataportal/excel.xls diff --git a/doc/Archive/src/dataportal/param_initialization.py b/doc/OnlineDocs/src/dataportal/param_initialization.py similarity index 100% rename from doc/Archive/src/dataportal/param_initialization.py rename to doc/OnlineDocs/src/dataportal/param_initialization.py diff --git a/doc/Archive/src/dataportal/param_initialization.txt b/doc/OnlineDocs/src/dataportal/param_initialization.txt similarity index 100% rename from doc/Archive/src/dataportal/param_initialization.txt rename to doc/OnlineDocs/src/dataportal/param_initialization.txt diff --git a/doc/Archive/src/dataportal/set_initialization.py b/doc/OnlineDocs/src/dataportal/set_initialization.py similarity index 100% rename from doc/Archive/src/dataportal/set_initialization.py rename to doc/OnlineDocs/src/dataportal/set_initialization.py diff --git a/doc/Archive/src/dataportal/set_initialization.txt b/doc/OnlineDocs/src/dataportal/set_initialization.txt similarity index 100% rename from doc/Archive/src/dataportal/set_initialization.txt rename to doc/OnlineDocs/src/dataportal/set_initialization.txt diff --git a/doc/Archive/src/expr/design.py b/doc/OnlineDocs/src/expr/design.py similarity index 100% rename from doc/Archive/src/expr/design.py rename to doc/OnlineDocs/src/expr/design.py diff --git a/doc/Archive/src/expr/design.txt b/doc/OnlineDocs/src/expr/design.txt similarity index 100% rename from doc/Archive/src/expr/design.txt rename to doc/OnlineDocs/src/expr/design.txt diff --git a/doc/Archive/src/expr/index.py b/doc/OnlineDocs/src/expr/index.py similarity index 100% rename from doc/Archive/src/expr/index.py rename to doc/OnlineDocs/src/expr/index.py diff --git a/doc/Archive/src/expr/index.txt b/doc/OnlineDocs/src/expr/index.txt similarity index 100% rename from doc/Archive/src/expr/index.txt rename to doc/OnlineDocs/src/expr/index.txt diff --git a/doc/Archive/src/expr/managing.py b/doc/OnlineDocs/src/expr/managing.py similarity index 100% rename from doc/Archive/src/expr/managing.py rename to doc/OnlineDocs/src/expr/managing.py diff --git a/doc/Archive/src/expr/managing.txt b/doc/OnlineDocs/src/expr/managing.txt similarity index 100% rename from doc/Archive/src/expr/managing.txt rename to doc/OnlineDocs/src/expr/managing.txt diff --git a/doc/Archive/src/expr/overview.py b/doc/OnlineDocs/src/expr/overview.py similarity index 100% rename from doc/Archive/src/expr/overview.py rename to doc/OnlineDocs/src/expr/overview.py diff --git a/doc/Archive/src/expr/overview.txt b/doc/OnlineDocs/src/expr/overview.txt similarity index 100% rename from doc/Archive/src/expr/overview.txt rename to doc/OnlineDocs/src/expr/overview.txt diff --git a/doc/Archive/src/expr/performance.py b/doc/OnlineDocs/src/expr/performance.py similarity index 100% rename from doc/Archive/src/expr/performance.py rename to doc/OnlineDocs/src/expr/performance.py diff --git a/doc/Archive/src/expr/performance.txt b/doc/OnlineDocs/src/expr/performance.txt similarity index 100% rename from doc/Archive/src/expr/performance.txt rename to doc/OnlineDocs/src/expr/performance.txt diff --git a/doc/Archive/src/expr/quicksum.log b/doc/OnlineDocs/src/expr/quicksum.log similarity index 100% rename from doc/Archive/src/expr/quicksum.log rename to doc/OnlineDocs/src/expr/quicksum.log diff --git a/doc/Archive/src/expr/quicksum.py b/doc/OnlineDocs/src/expr/quicksum.py similarity index 100% rename from doc/Archive/src/expr/quicksum.py rename to doc/OnlineDocs/src/expr/quicksum.py diff --git a/doc/Archive/src/kernel/examples.sh b/doc/OnlineDocs/src/kernel/examples.sh similarity index 100% rename from doc/Archive/src/kernel/examples.sh rename to doc/OnlineDocs/src/kernel/examples.sh diff --git a/doc/Archive/src/kernel/examples.txt b/doc/OnlineDocs/src/kernel/examples.txt similarity index 100% rename from doc/Archive/src/kernel/examples.txt rename to doc/OnlineDocs/src/kernel/examples.txt diff --git a/doc/Archive/src/scripting/AbstractSuffixes.py b/doc/OnlineDocs/src/scripting/AbstractSuffixes.py similarity index 100% rename from doc/Archive/src/scripting/AbstractSuffixes.py rename to doc/OnlineDocs/src/scripting/AbstractSuffixes.py diff --git a/doc/Archive/src/scripting/Isinglebuild.py b/doc/OnlineDocs/src/scripting/Isinglebuild.py similarity index 100% rename from doc/Archive/src/scripting/Isinglebuild.py rename to doc/OnlineDocs/src/scripting/Isinglebuild.py diff --git a/doc/Archive/src/scripting/Isinglecomm.dat b/doc/OnlineDocs/src/scripting/Isinglecomm.dat similarity index 100% rename from doc/Archive/src/scripting/Isinglecomm.dat rename to doc/OnlineDocs/src/scripting/Isinglecomm.dat diff --git a/doc/Archive/src/scripting/NodesIn_init.py b/doc/OnlineDocs/src/scripting/NodesIn_init.py similarity index 100% rename from doc/Archive/src/scripting/NodesIn_init.py rename to doc/OnlineDocs/src/scripting/NodesIn_init.py diff --git a/doc/Archive/src/scripting/Z_init.py b/doc/OnlineDocs/src/scripting/Z_init.py similarity index 100% rename from doc/Archive/src/scripting/Z_init.py rename to doc/OnlineDocs/src/scripting/Z_init.py diff --git a/doc/Archive/src/scripting/abstract1.dat b/doc/OnlineDocs/src/scripting/abstract1.dat similarity index 100% rename from doc/Archive/src/scripting/abstract1.dat rename to doc/OnlineDocs/src/scripting/abstract1.dat diff --git a/doc/Archive/src/scripting/abstract2.dat b/doc/OnlineDocs/src/scripting/abstract2.dat similarity index 100% rename from doc/Archive/src/scripting/abstract2.dat rename to doc/OnlineDocs/src/scripting/abstract2.dat diff --git a/doc/Archive/src/scripting/abstract2.py b/doc/OnlineDocs/src/scripting/abstract2.py similarity index 100% rename from doc/Archive/src/scripting/abstract2.py rename to doc/OnlineDocs/src/scripting/abstract2.py diff --git a/doc/Archive/src/scripting/abstract2a.dat b/doc/OnlineDocs/src/scripting/abstract2a.dat similarity index 100% rename from doc/Archive/src/scripting/abstract2a.dat rename to doc/OnlineDocs/src/scripting/abstract2a.dat diff --git a/doc/Archive/src/scripting/abstract2piece.py b/doc/OnlineDocs/src/scripting/abstract2piece.py similarity index 100% rename from doc/Archive/src/scripting/abstract2piece.py rename to doc/OnlineDocs/src/scripting/abstract2piece.py diff --git a/doc/Archive/src/scripting/abstract2piecebuild.py b/doc/OnlineDocs/src/scripting/abstract2piecebuild.py similarity index 100% rename from doc/Archive/src/scripting/abstract2piecebuild.py rename to doc/OnlineDocs/src/scripting/abstract2piecebuild.py diff --git a/doc/Archive/src/scripting/block_iter_example.py b/doc/OnlineDocs/src/scripting/block_iter_example.py similarity index 100% rename from doc/Archive/src/scripting/block_iter_example.py rename to doc/OnlineDocs/src/scripting/block_iter_example.py diff --git a/doc/Archive/src/scripting/concrete1.py b/doc/OnlineDocs/src/scripting/concrete1.py similarity index 100% rename from doc/Archive/src/scripting/concrete1.py rename to doc/OnlineDocs/src/scripting/concrete1.py diff --git a/doc/Archive/src/scripting/doubleA.py b/doc/OnlineDocs/src/scripting/doubleA.py similarity index 100% rename from doc/Archive/src/scripting/doubleA.py rename to doc/OnlineDocs/src/scripting/doubleA.py diff --git a/doc/Archive/src/scripting/driveabs2.py b/doc/OnlineDocs/src/scripting/driveabs2.py similarity index 100% rename from doc/Archive/src/scripting/driveabs2.py rename to doc/OnlineDocs/src/scripting/driveabs2.py diff --git a/doc/Archive/src/scripting/driveconc1.py b/doc/OnlineDocs/src/scripting/driveconc1.py similarity index 100% rename from doc/Archive/src/scripting/driveconc1.py rename to doc/OnlineDocs/src/scripting/driveconc1.py diff --git a/doc/Archive/src/scripting/iterative1.py b/doc/OnlineDocs/src/scripting/iterative1.py similarity index 100% rename from doc/Archive/src/scripting/iterative1.py rename to doc/OnlineDocs/src/scripting/iterative1.py diff --git a/doc/Archive/src/scripting/iterative2.py b/doc/OnlineDocs/src/scripting/iterative2.py similarity index 100% rename from doc/Archive/src/scripting/iterative2.py rename to doc/OnlineDocs/src/scripting/iterative2.py diff --git a/doc/Archive/src/scripting/noiteration1.py b/doc/OnlineDocs/src/scripting/noiteration1.py similarity index 100% rename from doc/Archive/src/scripting/noiteration1.py rename to doc/OnlineDocs/src/scripting/noiteration1.py diff --git a/doc/Archive/src/scripting/parallel.py b/doc/OnlineDocs/src/scripting/parallel.py similarity index 100% rename from doc/Archive/src/scripting/parallel.py rename to doc/OnlineDocs/src/scripting/parallel.py diff --git a/doc/Archive/src/scripting/spy4Constraints.py b/doc/OnlineDocs/src/scripting/spy4Constraints.py similarity index 100% rename from doc/Archive/src/scripting/spy4Constraints.py rename to doc/OnlineDocs/src/scripting/spy4Constraints.py diff --git a/doc/Archive/src/scripting/spy4Expressions.py b/doc/OnlineDocs/src/scripting/spy4Expressions.py similarity index 100% rename from doc/Archive/src/scripting/spy4Expressions.py rename to doc/OnlineDocs/src/scripting/spy4Expressions.py diff --git a/doc/Archive/src/scripting/spy4PyomoCommand.py b/doc/OnlineDocs/src/scripting/spy4PyomoCommand.py similarity index 100% rename from doc/Archive/src/scripting/spy4PyomoCommand.py rename to doc/OnlineDocs/src/scripting/spy4PyomoCommand.py diff --git a/doc/Archive/src/scripting/spy4Variables.py b/doc/OnlineDocs/src/scripting/spy4Variables.py similarity index 100% rename from doc/Archive/src/scripting/spy4Variables.py rename to doc/OnlineDocs/src/scripting/spy4Variables.py diff --git a/doc/Archive/src/scripting/spy4scripts.py b/doc/OnlineDocs/src/scripting/spy4scripts.py similarity index 100% rename from doc/Archive/src/scripting/spy4scripts.py rename to doc/OnlineDocs/src/scripting/spy4scripts.py diff --git a/doc/Archive/src/strip_examples.py b/doc/OnlineDocs/src/strip_examples.py similarity index 100% rename from doc/Archive/src/strip_examples.py rename to doc/OnlineDocs/src/strip_examples.py diff --git a/doc/Archive/src/test_examples.py b/doc/OnlineDocs/src/test_examples.py similarity index 100% rename from doc/Archive/src/test_examples.py rename to doc/OnlineDocs/src/test_examples.py From ea7a0fde2b6d164481959c661436bad55b58fd5d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 19:30:15 -0600 Subject: [PATCH 2414/3044] Doc: fixing typo --- doc/OnlineDocs/explanation/philosophy/abstract_modeling.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/explanation/philosophy/abstract_modeling.rst b/doc/OnlineDocs/explanation/philosophy/abstract_modeling.rst index 877342de924..9d0ad0ac8e5 100644 --- a/doc/OnlineDocs/explanation/philosophy/abstract_modeling.rst +++ b/doc/OnlineDocs/explanation/philosophy/abstract_modeling.rst @@ -4,7 +4,7 @@ Abstract Models .. note:: TODO: this is a copy of "Abstractvs Concrete" from Getting Started. - This shoud beexpanded here. + This should be expanded here. A mathematical model can be defined using symbols that represent data From 9bffc0a1ad1ea8584022e2c8287570c20df6863b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 19:37:32 -0600 Subject: [PATCH 2415/3044] Add a make target to help with development --- doc/OnlineDocs/Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/OnlineDocs/Makefile b/doc/OnlineDocs/Makefile index d4bf5cd5204..962609def61 100644 --- a/doc/OnlineDocs/Makefile +++ b/doc/OnlineDocs/Makefile @@ -23,3 +23,9 @@ clean: @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) @echo "Removing *.spy, *.out" @find . -name \*.spy -delete + +rebuild: + @$(MAKE) clean + @for D in $(BUILDDIR) $(SOURCEDIR)/reference/API; do \ + if test -d "$$D"; then echo "Removing $$D"; rm -r "$$D"; fi \ + done From 4d097b69f9b2b911a9992d98a7d7457dc382549f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 27 Aug 2024 19:37:54 -0600 Subject: [PATCH 2416/3044] Only raise DeferredImportError if we are not building docs --- pyomo/common/dependencies.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index bbcea0b85d7..e292cd2077d 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -240,6 +240,12 @@ def UnavailableClass(unavailable_module): class UnavailableMeta(type): def __getattr__(cls, name): + if 'sphinx' in sys.modules: + # If we are building documentation, avoid the + # DeferredImportError (we will still raise one if + # someone attempts to *create* an instance of this + # class) + super().__getattr__(name) raise DeferredImportError( unavailable_module._moduleunavailable_message( f"The class attribute '{cls.__name__}.{name}' is not available " From e97965f01317d30334794a4d7735f212d2d54dec Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 30 Aug 2024 08:00:58 -0600 Subject: [PATCH 2417/3044] Move in_testing_environment, FlagType, NOTSET to common.flags --- pyomo/common/deprecation.py | 12 +--- pyomo/common/flags.py | 90 +++++++++++++++++++++++++++++ pyomo/common/modeling.py | 53 ++--------------- pyomo/common/tests/test_flags.py | 44 ++++++++++++++ pyomo/common/tests/test_modeling.py | 9 +-- 5 files changed, 142 insertions(+), 66 deletions(-) create mode 100644 pyomo/common/flags.py create mode 100644 pyomo/common/tests/test_flags.py diff --git a/pyomo/common/deprecation.py b/pyomo/common/deprecation.py index c674dcddc78..f8a63752c13 100644 --- a/pyomo/common/deprecation.py +++ b/pyomo/common/deprecation.py @@ -29,6 +29,7 @@ import types from pyomo.common.errors import DeveloperError +from pyomo.common.flags import in_testing_environment, building_documentation _doc_flag = '.. deprecated::' @@ -151,17 +152,6 @@ def _find_calling_frame(module_offset): return calling_frame -def in_testing_environment(): - """Return True if we are currently running in a "testing" environment - - This currently includes if nose, nose2, pytest, or Sphinx are - running (imported). - - """ - - return any(mod in sys.modules for mod in ('nose', 'nose2', 'pytest', 'sphinx')) - - def deprecation_warning( msg, logger=None, version=None, remove_in=None, calling_frame=None ): diff --git a/pyomo/common/flags.py b/pyomo/common/flags.py new file mode 100644 index 00000000000..f6d3c56bbb6 --- /dev/null +++ b/pyomo/common/flags.py @@ -0,0 +1,90 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import sys + + +class FlagType(type): + """Metaclass to simplify the repr(type) and str(type) + + This metaclass redefines the ``str()`` and ``repr()`` of resulting + classes. The str() of the class returns only the class' ``__name__``, + whereas the repr() returns either the qualified class name + (``__qualname__``) if Sphinx has been imported, or else the + fully-qualified class name (``__module__ + '.' + __qualname__``). + + This is useful for defining "flag types" that are default arguments + in functions so that the Sphinx-generated documentation is "cleaner" + + """ + + def __repr__(cls): + if building_documentation(): + return cls.__qualname__ + else: + return cls.__module__ + "." + cls.__qualname__ + + def __str__(cls): + return cls.__name__ + + +class NOTSET(object, metaclass=FlagType): + """ + Class to be used to indicate that an optional argument + was not specified, if `None` may be ambiguous. Usage: + + >>> def foo(value=NOTSET): + >>> if value is NOTSET: + >>> pass # no argument was provided to `value` + + """ + + pass + + +def in_testing_environment(state=NOTSET): + """Return True if we are currently running in a "testing" environment + + This currently includes if nose, nose2, pytest, or Sphinx are + running (imported). + + Parameters + ---------- + state : bool or None + If provided, sets the current state of the testing environment + (Setting to None reverts to the normal interrogation of + ``sys.modules``) + + Returns + ------- + bool + + """ + if state is not NOTSET: + in_testing_environment.state = state + if in_testing_environment.state is not None: + return bool(in_testing_environment.state) + return any(mod in sys.modules for mod in ('nose', 'nose2', 'pytest')) + +in_testing_environment.state = None + + +def building_documentation(): + """Return True if we are building the Sphinx documentation + + Returns + ------- + bool + + """ + return not in_testing_environment() and ( + 'sphinx' in sys.modules or 'Sphinx' in sys.modules + ) diff --git a/pyomo/common/modeling.py b/pyomo/common/modeling.py index 4c07048d77a..9d8565ab0d4 100644 --- a/pyomo/common/modeling.py +++ b/pyomo/common/modeling.py @@ -9,9 +9,14 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import sys from .dependencies import random +# [Aug 24] Importing for backwards compatability; may deprecate this import later +from .flags import FlagType, NOTSET + +# Backward compatibility with the previous name for this flag +NoArgumentGiven = NOTSET + def randint(a, b): """Our implementation of random.randint. @@ -35,49 +40,3 @@ def unique_component_name(instance, name): return name else: name += str(randint(0, 9)) - - -class FlagType(type): - """Metaclass to simplify the repr(type) and str(type) - - This metaclass redefines the ``str()`` and ``repr()`` of resulting - classes. The str() of the class returns only the class' ``__name__``, - whereas the repr() returns either the qualified class name - (``__qualname__``) if Sphinx has been imported, or else the - fully-qualified class name (``__module__ + '.' + __qualname__``). - - This is useful for defining "flag types" that are default arguments - in functions so that the Sphinx-generated documentation is "cleaner" - - """ - - if 'sphinx' in sys.modules or 'Sphinx' in sys.modules: - - def __repr__(cls): - return cls.__qualname__ - - else: - - def __repr__(cls): - return cls.__module__ + "." + cls.__qualname__ - - def __str__(cls): - return cls.__name__ - - -class NOTSET(object, metaclass=FlagType): - """ - Class to be used to indicate that an optional argument - was not specified, if `None` may be ambiguous. Usage: - - >>> def foo(value=NOTSET): - >>> if value is NOTSET: - >>> pass # no argument was provided to `value` - - """ - - pass - - -# Backward compatibility with the previous name for this flag -NoArgumentGiven = NOTSET diff --git a/pyomo/common/tests/test_flags.py b/pyomo/common/tests/test_flags.py new file mode 100644 index 00000000000..cf3a3313dca --- /dev/null +++ b/pyomo/common/tests/test_flags.py @@ -0,0 +1,44 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + +import sys + +import pyomo.common.unittest as unittest + +from pyomo.common.flags import NOTSET, in_testing_environment, building_documentation + + +class TestModeling(unittest.TestCase): + + def test_NOTSET(self): + self.assertEqual(str(NOTSET), 'NOTSET') + self.assertNotIn('sphinx', sys.modules) + self.assertEqual(repr(NOTSET), 'pyomo.common.flags.NOTSET') + self.assertIsNone(in_testing_environment.state) + + self.assertTrue(in_testing_environment()) + self.assertFalse(building_documentation()) + try: + sys.modules['sphinx'] = sys.modules[__name__] + for i in sorted(sys.modules.items()): + print(i) + self.assertTrue(in_testing_environment()) + self.assertFalse(building_documentation()) + self.assertEqual(repr(NOTSET), 'pyomo.common.flags.NOTSET') + + in_testing_environment(False) + self.assertFalse(in_testing_environment()) + self.assertTrue(building_documentation()) + self.assertEqual(repr(NOTSET), 'NOTSET') + finally: + del sys.modules['sphinx'] + in_testing_environment(None) + self.assertIsNone(in_testing_environment.state) diff --git a/pyomo/common/tests/test_modeling.py b/pyomo/common/tests/test_modeling.py index 97bef76c2c0..553f71611a7 100644 --- a/pyomo/common/tests/test_modeling.py +++ b/pyomo/common/tests/test_modeling.py @@ -9,12 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import sys - import pyomo.common.unittest as unittest from pyomo.environ import ConcreteModel, Var -from pyomo.common.modeling import unique_component_name, NOTSET +from pyomo.common.modeling import unique_component_name class TestModeling(unittest.TestCase): @@ -48,8 +46,3 @@ def test_unique_component_name(self): self.assertEqual(name[:2], 'y_') self.assertIn(name[2], '0123456789') self.assertIn(name[3], '0123456789') - - def test_NOTSET(self): - self.assertEqual(str(NOTSET), 'NOTSET') - assert 'sphinx' not in sys.modules - self.assertEqual(repr(NOTSET), 'pyomo.common.modeling.NOTSET') From e6d7445da3136afd9ff0b9a83fad220b6bf1cb44 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 30 Aug 2024 08:01:58 -0600 Subject: [PATCH 2418/3044] Standardize use of in_testing_environment(), building_documentation() --- doc/OnlineDocs/conf.py | 3 +++ pyomo/common/dependencies.py | 9 +++++---- pyomo/common/deprecation.py | 2 +- pyomo/common/log.py | 12 +++++------- pyomo/contrib/simplemodel/__init__.py | 3 ++- pyomo/pysp/__init__.py | 3 ++- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 467bd21f8b6..4d784d0fbb8 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -301,4 +301,7 @@ def check_output(self, want, got, optionflags): asl_available = False ma27_available = False mumps_available = False + +from pyomo.common.flags import in_testing_environment +in_testing_environment(True) ''' diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index e292cd2077d..0458bdeade6 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -19,9 +19,10 @@ from types import ModuleType from typing import List -from .deprecation import deprecated, deprecation_warning, in_testing_environment +import pyomo +from .deprecation import deprecated, deprecation_warning from .errors import DeferredImportError - +from .flags import in_testing_environment, building_documentation SUPPRESS_DEPENDENCY_WARNINGS = False @@ -240,12 +241,12 @@ def UnavailableClass(unavailable_module): class UnavailableMeta(type): def __getattr__(cls, name): - if 'sphinx' in sys.modules: + if building_documentation(): # If we are building documentation, avoid the # DeferredImportError (we will still raise one if # someone attempts to *create* an instance of this # class) - super().__getattr__(name) + return getattr(super(), name) raise DeferredImportError( unavailable_module._moduleunavailable_message( f"The class attribute '{cls.__name__}.{name}' is not available " diff --git a/pyomo/common/deprecation.py b/pyomo/common/deprecation.py index f8a63752c13..1e5dc960fb8 100644 --- a/pyomo/common/deprecation.py +++ b/pyomo/common/deprecation.py @@ -228,7 +228,7 @@ def deprecation_warning( logger.warning(msg) -if in_testing_environment(): +if in_testing_environment() or building_documentation(): deprecation_warning.emitted_warnings = None else: deprecation_warning.emitted_warnings = set() diff --git a/pyomo/common/log.py b/pyomo/common/log.py index d61ed62f373..3bf0a70c072 100644 --- a/pyomo/common/log.py +++ b/pyomo/common/log.py @@ -28,21 +28,19 @@ from pyomo.version.info import releaselevel from pyomo.common.deprecation import deprecated from pyomo.common.fileutils import PYOMO_ROOT_DIR +from pyomo.common.flags import in_testing_environment from pyomo.common.formatting import wrap_reStructuredText _indentation_re = re.compile(r'\s*') -_RTD_URL = "https://pyomo.readthedocs.io/en/%s/errors.html" % ( - 'stable' - if (releaselevel == 'final' or 'sphinx' in sys.modules or 'Sphinx' in sys.modules) - else 'latest' -) - def RTD(_id): _id = str(_id).lower() assert _id[0] in 'wex' - return f"{_RTD_URL}#{_id}" + return "https://pyomo.readthedocs.io/en/%s/errors.html#%s" % ( + 'stable' if (releaselevel == 'final' or in_testing_environment()) else 'latest', + _id, + ) _DEBUG = logging.DEBUG diff --git a/pyomo/contrib/simplemodel/__init__.py b/pyomo/contrib/simplemodel/__init__.py index f2f4922223e..201c1a0f657 100644 --- a/pyomo/contrib/simplemodel/__init__.py +++ b/pyomo/contrib/simplemodel/__init__.py @@ -9,7 +9,8 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.common.deprecation import deprecation_warning, in_testing_environment +from pyomo.common.deprecation import deprecation_warning +from pyomo.common.flags import in_testing_environment try: deprecation_warning( diff --git a/pyomo/pysp/__init__.py b/pyomo/pysp/__init__.py index bb8a401e45e..dae9a04943e 100644 --- a/pyomo/pysp/__init__.py +++ b/pyomo/pysp/__init__.py @@ -11,7 +11,8 @@ import logging import sys -from pyomo.common.deprecation import deprecation_warning, in_testing_environment +from pyomo.common.deprecation import deprecation_warning +from pyomo.common.flags import in_testing_environment try: # Warn the user From b327c1994cba66823e6a334c18bd47c98d71359b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 30 Aug 2024 11:10:35 -0600 Subject: [PATCH 2419/3044] NFC: comments and black --- pyomo/common/deprecation.py | 4 ++++ pyomo/common/flags.py | 1 + 2 files changed, 5 insertions(+) diff --git a/pyomo/common/deprecation.py b/pyomo/common/deprecation.py index 1e5dc960fb8..0d16b92d5dd 100644 --- a/pyomo/common/deprecation.py +++ b/pyomo/common/deprecation.py @@ -228,6 +228,10 @@ def deprecation_warning( logger.warning(msg) +# We do not want to cache / suppress repeated warnings when we are +# testing or when we are building the documentation. Note that doctest +# doesn't set the "in_testing" flag until after pyomo.common is +# imported. if in_testing_environment() or building_documentation(): deprecation_warning.emitted_warnings = None else: diff --git a/pyomo/common/flags.py b/pyomo/common/flags.py index f6d3c56bbb6..bce95a4f3c4 100644 --- a/pyomo/common/flags.py +++ b/pyomo/common/flags.py @@ -74,6 +74,7 @@ def in_testing_environment(state=NOTSET): return bool(in_testing_environment.state) return any(mod in sys.modules for mod in ('nose', 'nose2', 'pytest')) + in_testing_environment.state = None From bf9a5697847fa1669e8057392b15fb2bd21804f3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 30 Aug 2024 11:11:08 -0600 Subject: [PATCH 2420/3044] Allow detecting documentation regardless of testing --- pyomo/common/dependencies.py | 2 +- pyomo/common/flags.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 0458bdeade6..32e1d0e336f 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -241,7 +241,7 @@ def UnavailableClass(unavailable_module): class UnavailableMeta(type): def __getattr__(cls, name): - if building_documentation(): + if building_documentation(ignore_testing_flag=True): # If we are building documentation, avoid the # DeferredImportError (we will still raise one if # someone attempts to *create* an instance of this diff --git a/pyomo/common/flags.py b/pyomo/common/flags.py index bce95a4f3c4..9aa8ece3dbc 100644 --- a/pyomo/common/flags.py +++ b/pyomo/common/flags.py @@ -78,14 +78,16 @@ def in_testing_environment(state=NOTSET): in_testing_environment.state = None -def building_documentation(): +def building_documentation(ignore_testing_flag=False): """Return True if we are building the Sphinx documentation Returns ------- bool - """ - return not in_testing_environment() and ( + """ + import sys + + return (ignore_testing_flag or not in_testing_environment()) and ( 'sphinx' in sys.modules or 'Sphinx' in sys.modules ) From d1b5d5a73d94ab2bd1ab8a54e8c8662845a76250 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 30 Aug 2024 11:15:25 -0600 Subject: [PATCH 2421/3044] NFC: typo --- pyomo/common/modeling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/modeling.py b/pyomo/common/modeling.py index 9d8565ab0d4..5839e97544d 100644 --- a/pyomo/common/modeling.py +++ b/pyomo/common/modeling.py @@ -11,7 +11,7 @@ from .dependencies import random -# [Aug 24] Importing for backwards compatability; may deprecate this import later +# [Aug 24] Importing for backwards compatibility; may deprecate this import later from .flags import FlagType, NOTSET # Backward compatibility with the previous name for this flag From 7f73bb0a60d1d2ef80c57e3b89e90be6b5477ee0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 30 Aug 2024 11:54:37 -0600 Subject: [PATCH 2422/3044] Update URL checker --- .github/workflows/url_check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/url_check.yml b/.github/workflows/url_check.yml index 797574574b4..afdb47ef18a 100644 --- a/.github/workflows/url_check.yml +++ b/.github/workflows/url_check.yml @@ -29,4 +29,4 @@ jobs: # Exclude: # - Jenkins because it's behind a firewall # - RTD because a magically-generated string triggers failures - exclude_urls: https://pyomo-jenkins.sandia.gov/,https://pyomo.readthedocs.io/en/%s/errors.html + exclude_urls: https://pyomo-jenkins.sandia.gov/,https://pyomo.readthedocs.io/en/%s/errors.html#%s From 3f18ce53fb67b89453a4085de66a8df75ca2da2f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 30 Aug 2024 12:05:35 -0600 Subject: [PATCH 2423/3044] Update URL checker --- .github/workflows/url_check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/url_check.yml b/.github/workflows/url_check.yml index afdb47ef18a..2afb8c7e588 100644 --- a/.github/workflows/url_check.yml +++ b/.github/workflows/url_check.yml @@ -29,4 +29,4 @@ jobs: # Exclude: # - Jenkins because it's behind a firewall # - RTD because a magically-generated string triggers failures - exclude_urls: https://pyomo-jenkins.sandia.gov/,https://pyomo.readthedocs.io/en/%s/errors.html#%s + exclude_urls: 'https://pyomo-jenkins.sandia.gov/,https://pyomo.readthedocs.io/en/%s/errors.html#%s' From 98c55105079e2de40904bfad1131f6a5eed3e6bd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 30 Aug 2024 12:34:36 -0600 Subject: [PATCH 2424/3044] Rework RTD URL to work with the url checker --- .github/workflows/url_check.yml | 2 +- pyomo/common/log.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/url_check.yml b/.github/workflows/url_check.yml index 2afb8c7e588..797574574b4 100644 --- a/.github/workflows/url_check.yml +++ b/.github/workflows/url_check.yml @@ -29,4 +29,4 @@ jobs: # Exclude: # - Jenkins because it's behind a firewall # - RTD because a magically-generated string triggers failures - exclude_urls: 'https://pyomo-jenkins.sandia.gov/,https://pyomo.readthedocs.io/en/%s/errors.html#%s' + exclude_urls: https://pyomo-jenkins.sandia.gov/,https://pyomo.readthedocs.io/en/%s/errors.html diff --git a/pyomo/common/log.py b/pyomo/common/log.py index 3bf0a70c072..c4218e4fbd3 100644 --- a/pyomo/common/log.py +++ b/pyomo/common/log.py @@ -33,14 +33,16 @@ _indentation_re = re.compile(r'\s*') +_RTD_URL = "https://pyomo.readthedocs.io/en/%s/errors.html" + def RTD(_id): _id = str(_id).lower() - assert _id[0] in 'wex' - return "https://pyomo.readthedocs.io/en/%s/errors.html#%s" % ( - 'stable' if (releaselevel == 'final' or in_testing_environment()) else 'latest', - _id, + _release = ( + 'stable' if releaselevel == 'final' or in_testing_environment() else 'latest' ) + assert _id[0] in 'wex' + return (_RTD_URL % (_release,)) + f"#{_id}" _DEBUG = logging.DEBUG From fd3ad4f101edafcfe2cb05b5793bed079823f205 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 30 Aug 2024 23:47:16 -0600 Subject: [PATCH 2425/3044] Stub in the auto-generated library reference --- doc/OnlineDocs/_templates/recursive-class.rst | 32 +++++++++ .../_templates/recursive-module.rst | 68 +++++++++++++++++++ doc/OnlineDocs/reference/index.rst | 8 ++- 3 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 doc/OnlineDocs/_templates/recursive-class.rst create mode 100644 doc/OnlineDocs/_templates/recursive-module.rst diff --git a/doc/OnlineDocs/_templates/recursive-class.rst b/doc/OnlineDocs/_templates/recursive-class.rst new file mode 100644 index 00000000000..2fd769c7d7b --- /dev/null +++ b/doc/OnlineDocs/_templates/recursive-class.rst @@ -0,0 +1,32 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :members: + :inherited-members: + :show-inheritance: + + {% block methods %} + .. automethod:: __init__ + + {% if methods %} + .. rubric:: {{ _('Methods') }} + + .. autosummary:: + {% for item in methods %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/doc/OnlineDocs/_templates/recursive-module.rst b/doc/OnlineDocs/_templates/recursive-module.rst new file mode 100644 index 00000000000..19b64e2d8bd --- /dev/null +++ b/doc/OnlineDocs/_templates/recursive-module.rst @@ -0,0 +1,68 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Module Attributes') }} + + .. autosummary:: + :toctree: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + .. autosummary:: + :toctree: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + .. rubric:: {{ _('Classes') }} + + .. autosummary:: + :toctree: + :template: recursive-class.rst + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + .. rubric:: {{ _('Exceptions') }} + + .. autosummary:: + :toctree: + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + +{% block modules %} +{% if modules %} +.. rubric:: Modules + +.. autosummary:: + :toctree: + :template: recursive-module.rst + :recursive: + {% for item in modules %} + {% if '.test' not in item and '.example' not in item %} + {{ item }} + {% endif %} + {%- endfor %} +{% endif %} +{% endblock %} diff --git a/doc/OnlineDocs/reference/index.rst b/doc/OnlineDocs/reference/index.rst index 0c793d607dd..a2cc1e26764 100644 --- a/doc/OnlineDocs/reference/index.rst +++ b/doc/OnlineDocs/reference/index.rst @@ -9,11 +9,13 @@ Reference Guides future ../related_packages bibliography + API/pyomo -.. - autosummary:: +.. autosummary:: :toctree: API - :template: pyomo-autosummary-module.rst + :caption: Library Reference + :template: recursive-module.rst :recursive: + :noindex: pyomo From 8e4d410da20b2d4719cf12a46710c660bdc8f660 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 31 Aug 2024 10:14:30 -0600 Subject: [PATCH 2426/3044] Move the API docs to the root; rename old Library Reference -> Topical --- doc/OnlineDocs/code.rst | 8 ++++++++ doc/OnlineDocs/reference/index.rst | 14 +++----------- .../{library_reference => topical}/aml/index.rst | 0 .../appsi/appsi.base.rst | 0 .../{library_reference => topical}/appsi/appsi.rst | 0 .../appsi/appsi.solvers.cbc.rst | 0 .../appsi/appsi.solvers.cplex.rst | 0 .../appsi/appsi.solvers.gurobi.rst | 0 .../appsi/appsi.solvers.highs.rst | 0 .../appsi/appsi.solvers.ipopt.rst | 0 .../appsi/appsi.solvers.maingo.rst | 0 .../appsi/appsi.solvers.rst | 0 .../common/config.rst | 0 .../common/dependencies.rst | 0 .../common/deprecation.rst | 0 .../common/enums.rst | 0 .../common/errors.rst | 0 .../common/fileutils.rst | 0 .../common/formatting.rst | 0 .../common/index.rst | 0 .../common/tempfiles.rst | 0 .../common/timing.rst | 0 .../{library_reference => topical}/data/index.rst | 0 .../expressions/building.rst | 0 .../expressions/classes.rst | 0 .../expressions/context_managers.rst | 0 .../expressions/index.rst | 0 .../expressions/managing.rst | 0 .../expressions/visitors.rst | 0 .../{library_reference => topical}/index.rst | 2 +- .../{library_reference => topical}/kernel/base.rst | 0 .../kernel/block.rst | 0 .../kernel/conic.rst | 0 .../kernel/constraint.rst | 0 .../kernel/dict_container.rst | 0 .../kernel/expression.rst | 0 .../kernel/heterogeneous_container.rst | 0 .../kernel/homogeneous_container.rst | 0 .../kernel/index.rst | 0 .../kernel/list_container.rst | 0 .../kernel/objective.rst | 0 .../kernel/parameter.rst | 0 .../kernel/piecewise/index.rst | 0 .../kernel/piecewise/piecewise.rst | 0 .../kernel/piecewise/piecewise_nd.rst | 0 .../kernel/piecewise/util.rst | 0 .../{library_reference => topical}/kernel/sos.rst | 0 .../kernel/suffix.rst | 0 .../kernel/tuple_container.rst | 0 .../kernel/variable.rst | 0 .../solvers/cplex_persistent.rst | 0 .../solvers/gams.rst | 0 .../solvers/gurobi_direct.rst | 0 .../solvers/gurobi_persistent.rst | 0 .../solvers/index.rst | 0 .../solvers/xpress_persistent.rst | 0 56 files changed, 12 insertions(+), 12 deletions(-) create mode 100644 doc/OnlineDocs/code.rst rename doc/OnlineDocs/reference/{library_reference => topical}/aml/index.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/appsi/appsi.base.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/appsi/appsi.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/appsi/appsi.solvers.cbc.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/appsi/appsi.solvers.cplex.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/appsi/appsi.solvers.gurobi.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/appsi/appsi.solvers.highs.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/appsi/appsi.solvers.ipopt.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/appsi/appsi.solvers.maingo.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/appsi/appsi.solvers.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/common/config.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/common/dependencies.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/common/deprecation.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/common/enums.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/common/errors.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/common/fileutils.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/common/formatting.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/common/index.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/common/tempfiles.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/common/timing.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/data/index.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/expressions/building.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/expressions/classes.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/expressions/context_managers.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/expressions/index.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/expressions/managing.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/expressions/visitors.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/index.rst (97%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/base.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/block.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/conic.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/constraint.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/dict_container.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/expression.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/heterogeneous_container.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/homogeneous_container.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/index.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/list_container.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/objective.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/parameter.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/piecewise/index.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/piecewise/piecewise.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/piecewise/piecewise_nd.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/piecewise/util.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/sos.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/suffix.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/tuple_container.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/kernel/variable.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/solvers/cplex_persistent.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/solvers/gams.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/solvers/gurobi_direct.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/solvers/gurobi_persistent.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/solvers/index.rst (100%) rename doc/OnlineDocs/reference/{library_reference => topical}/solvers/xpress_persistent.rst (100%) diff --git a/doc/OnlineDocs/code.rst b/doc/OnlineDocs/code.rst new file mode 100644 index 00000000000..1ba1a2ea007 --- /dev/null +++ b/doc/OnlineDocs/code.rst @@ -0,0 +1,8 @@ +.. autosummary:: + :toctree: api + :caption: Library Reference + :template: recursive-module.rst + :recursive: + :noindex: + + pyomo diff --git a/doc/OnlineDocs/reference/index.rst b/doc/OnlineDocs/reference/index.rst index a2cc1e26764..0453bd07559 100644 --- a/doc/OnlineDocs/reference/index.rst +++ b/doc/OnlineDocs/reference/index.rst @@ -4,18 +4,10 @@ Reference Guides .. toctree:: :maxdepth: 2 - library_reference/index - ../errors + topical/index + Library Reference <../api/pyomo> future + ../errors ../related_packages bibliography - API/pyomo - -.. autosummary:: - :toctree: API - :caption: Library Reference - :template: recursive-module.rst - :recursive: - :noindex: - pyomo diff --git a/doc/OnlineDocs/reference/library_reference/aml/index.rst b/doc/OnlineDocs/reference/topical/aml/index.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/aml/index.rst rename to doc/OnlineDocs/reference/topical/aml/index.rst diff --git a/doc/OnlineDocs/reference/library_reference/appsi/appsi.base.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.base.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/appsi/appsi.base.rst rename to doc/OnlineDocs/reference/topical/appsi/appsi.base.rst diff --git a/doc/OnlineDocs/reference/library_reference/appsi/appsi.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/appsi/appsi.rst rename to doc/OnlineDocs/reference/topical/appsi/appsi.rst diff --git a/doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.cbc.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.cbc.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.cbc.rst rename to doc/OnlineDocs/reference/topical/appsi/appsi.solvers.cbc.rst diff --git a/doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.cplex.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.cplex.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.cplex.rst rename to doc/OnlineDocs/reference/topical/appsi/appsi.solvers.cplex.rst diff --git a/doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.gurobi.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.gurobi.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.gurobi.rst rename to doc/OnlineDocs/reference/topical/appsi/appsi.solvers.gurobi.rst diff --git a/doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.highs.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.highs.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.highs.rst rename to doc/OnlineDocs/reference/topical/appsi/appsi.solvers.highs.rst diff --git a/doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.ipopt.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.ipopt.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.ipopt.rst rename to doc/OnlineDocs/reference/topical/appsi/appsi.solvers.ipopt.rst diff --git a/doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.maingo.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.maingo.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.maingo.rst rename to doc/OnlineDocs/reference/topical/appsi/appsi.solvers.maingo.rst diff --git a/doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/appsi/appsi.solvers.rst rename to doc/OnlineDocs/reference/topical/appsi/appsi.solvers.rst diff --git a/doc/OnlineDocs/reference/library_reference/common/config.rst b/doc/OnlineDocs/reference/topical/common/config.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/common/config.rst rename to doc/OnlineDocs/reference/topical/common/config.rst diff --git a/doc/OnlineDocs/reference/library_reference/common/dependencies.rst b/doc/OnlineDocs/reference/topical/common/dependencies.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/common/dependencies.rst rename to doc/OnlineDocs/reference/topical/common/dependencies.rst diff --git a/doc/OnlineDocs/reference/library_reference/common/deprecation.rst b/doc/OnlineDocs/reference/topical/common/deprecation.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/common/deprecation.rst rename to doc/OnlineDocs/reference/topical/common/deprecation.rst diff --git a/doc/OnlineDocs/reference/library_reference/common/enums.rst b/doc/OnlineDocs/reference/topical/common/enums.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/common/enums.rst rename to doc/OnlineDocs/reference/topical/common/enums.rst diff --git a/doc/OnlineDocs/reference/library_reference/common/errors.rst b/doc/OnlineDocs/reference/topical/common/errors.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/common/errors.rst rename to doc/OnlineDocs/reference/topical/common/errors.rst diff --git a/doc/OnlineDocs/reference/library_reference/common/fileutils.rst b/doc/OnlineDocs/reference/topical/common/fileutils.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/common/fileutils.rst rename to doc/OnlineDocs/reference/topical/common/fileutils.rst diff --git a/doc/OnlineDocs/reference/library_reference/common/formatting.rst b/doc/OnlineDocs/reference/topical/common/formatting.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/common/formatting.rst rename to doc/OnlineDocs/reference/topical/common/formatting.rst diff --git a/doc/OnlineDocs/reference/library_reference/common/index.rst b/doc/OnlineDocs/reference/topical/common/index.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/common/index.rst rename to doc/OnlineDocs/reference/topical/common/index.rst diff --git a/doc/OnlineDocs/reference/library_reference/common/tempfiles.rst b/doc/OnlineDocs/reference/topical/common/tempfiles.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/common/tempfiles.rst rename to doc/OnlineDocs/reference/topical/common/tempfiles.rst diff --git a/doc/OnlineDocs/reference/library_reference/common/timing.rst b/doc/OnlineDocs/reference/topical/common/timing.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/common/timing.rst rename to doc/OnlineDocs/reference/topical/common/timing.rst diff --git a/doc/OnlineDocs/reference/library_reference/data/index.rst b/doc/OnlineDocs/reference/topical/data/index.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/data/index.rst rename to doc/OnlineDocs/reference/topical/data/index.rst diff --git a/doc/OnlineDocs/reference/library_reference/expressions/building.rst b/doc/OnlineDocs/reference/topical/expressions/building.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/expressions/building.rst rename to doc/OnlineDocs/reference/topical/expressions/building.rst diff --git a/doc/OnlineDocs/reference/library_reference/expressions/classes.rst b/doc/OnlineDocs/reference/topical/expressions/classes.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/expressions/classes.rst rename to doc/OnlineDocs/reference/topical/expressions/classes.rst diff --git a/doc/OnlineDocs/reference/library_reference/expressions/context_managers.rst b/doc/OnlineDocs/reference/topical/expressions/context_managers.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/expressions/context_managers.rst rename to doc/OnlineDocs/reference/topical/expressions/context_managers.rst diff --git a/doc/OnlineDocs/reference/library_reference/expressions/index.rst b/doc/OnlineDocs/reference/topical/expressions/index.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/expressions/index.rst rename to doc/OnlineDocs/reference/topical/expressions/index.rst diff --git a/doc/OnlineDocs/reference/library_reference/expressions/managing.rst b/doc/OnlineDocs/reference/topical/expressions/managing.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/expressions/managing.rst rename to doc/OnlineDocs/reference/topical/expressions/managing.rst diff --git a/doc/OnlineDocs/reference/library_reference/expressions/visitors.rst b/doc/OnlineDocs/reference/topical/expressions/visitors.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/expressions/visitors.rst rename to doc/OnlineDocs/reference/topical/expressions/visitors.rst diff --git a/doc/OnlineDocs/reference/library_reference/index.rst b/doc/OnlineDocs/reference/topical/index.rst similarity index 97% rename from doc/OnlineDocs/reference/library_reference/index.rst rename to doc/OnlineDocs/reference/topical/index.rst index 35dd8d30307..e0f4eb093d8 100644 --- a/doc/OnlineDocs/reference/library_reference/index.rst +++ b/doc/OnlineDocs/reference/topical/index.rst @@ -1,4 +1,4 @@ -Library Reference +Topical Reference ================= Pyomo is being increasingly used as a library to support Python diff --git a/doc/OnlineDocs/reference/library_reference/kernel/base.rst b/doc/OnlineDocs/reference/topical/kernel/base.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/base.rst rename to doc/OnlineDocs/reference/topical/kernel/base.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/block.rst b/doc/OnlineDocs/reference/topical/kernel/block.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/block.rst rename to doc/OnlineDocs/reference/topical/kernel/block.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/conic.rst b/doc/OnlineDocs/reference/topical/kernel/conic.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/conic.rst rename to doc/OnlineDocs/reference/topical/kernel/conic.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/constraint.rst b/doc/OnlineDocs/reference/topical/kernel/constraint.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/constraint.rst rename to doc/OnlineDocs/reference/topical/kernel/constraint.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/dict_container.rst b/doc/OnlineDocs/reference/topical/kernel/dict_container.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/dict_container.rst rename to doc/OnlineDocs/reference/topical/kernel/dict_container.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/expression.rst b/doc/OnlineDocs/reference/topical/kernel/expression.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/expression.rst rename to doc/OnlineDocs/reference/topical/kernel/expression.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/heterogeneous_container.rst b/doc/OnlineDocs/reference/topical/kernel/heterogeneous_container.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/heterogeneous_container.rst rename to doc/OnlineDocs/reference/topical/kernel/heterogeneous_container.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/homogeneous_container.rst b/doc/OnlineDocs/reference/topical/kernel/homogeneous_container.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/homogeneous_container.rst rename to doc/OnlineDocs/reference/topical/kernel/homogeneous_container.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/index.rst b/doc/OnlineDocs/reference/topical/kernel/index.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/index.rst rename to doc/OnlineDocs/reference/topical/kernel/index.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/list_container.rst b/doc/OnlineDocs/reference/topical/kernel/list_container.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/list_container.rst rename to doc/OnlineDocs/reference/topical/kernel/list_container.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/objective.rst b/doc/OnlineDocs/reference/topical/kernel/objective.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/objective.rst rename to doc/OnlineDocs/reference/topical/kernel/objective.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/parameter.rst b/doc/OnlineDocs/reference/topical/kernel/parameter.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/parameter.rst rename to doc/OnlineDocs/reference/topical/kernel/parameter.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/piecewise/index.rst b/doc/OnlineDocs/reference/topical/kernel/piecewise/index.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/piecewise/index.rst rename to doc/OnlineDocs/reference/topical/kernel/piecewise/index.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise.rst b/doc/OnlineDocs/reference/topical/kernel/piecewise/piecewise.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise.rst rename to doc/OnlineDocs/reference/topical/kernel/piecewise/piecewise.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise_nd.rst b/doc/OnlineDocs/reference/topical/kernel/piecewise/piecewise_nd.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/piecewise/piecewise_nd.rst rename to doc/OnlineDocs/reference/topical/kernel/piecewise/piecewise_nd.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/piecewise/util.rst b/doc/OnlineDocs/reference/topical/kernel/piecewise/util.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/piecewise/util.rst rename to doc/OnlineDocs/reference/topical/kernel/piecewise/util.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/sos.rst b/doc/OnlineDocs/reference/topical/kernel/sos.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/sos.rst rename to doc/OnlineDocs/reference/topical/kernel/sos.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/suffix.rst b/doc/OnlineDocs/reference/topical/kernel/suffix.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/suffix.rst rename to doc/OnlineDocs/reference/topical/kernel/suffix.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/tuple_container.rst b/doc/OnlineDocs/reference/topical/kernel/tuple_container.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/tuple_container.rst rename to doc/OnlineDocs/reference/topical/kernel/tuple_container.rst diff --git a/doc/OnlineDocs/reference/library_reference/kernel/variable.rst b/doc/OnlineDocs/reference/topical/kernel/variable.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/kernel/variable.rst rename to doc/OnlineDocs/reference/topical/kernel/variable.rst diff --git a/doc/OnlineDocs/reference/library_reference/solvers/cplex_persistent.rst b/doc/OnlineDocs/reference/topical/solvers/cplex_persistent.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/solvers/cplex_persistent.rst rename to doc/OnlineDocs/reference/topical/solvers/cplex_persistent.rst diff --git a/doc/OnlineDocs/reference/library_reference/solvers/gams.rst b/doc/OnlineDocs/reference/topical/solvers/gams.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/solvers/gams.rst rename to doc/OnlineDocs/reference/topical/solvers/gams.rst diff --git a/doc/OnlineDocs/reference/library_reference/solvers/gurobi_direct.rst b/doc/OnlineDocs/reference/topical/solvers/gurobi_direct.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/solvers/gurobi_direct.rst rename to doc/OnlineDocs/reference/topical/solvers/gurobi_direct.rst diff --git a/doc/OnlineDocs/reference/library_reference/solvers/gurobi_persistent.rst b/doc/OnlineDocs/reference/topical/solvers/gurobi_persistent.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/solvers/gurobi_persistent.rst rename to doc/OnlineDocs/reference/topical/solvers/gurobi_persistent.rst diff --git a/doc/OnlineDocs/reference/library_reference/solvers/index.rst b/doc/OnlineDocs/reference/topical/solvers/index.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/solvers/index.rst rename to doc/OnlineDocs/reference/topical/solvers/index.rst diff --git a/doc/OnlineDocs/reference/library_reference/solvers/xpress_persistent.rst b/doc/OnlineDocs/reference/topical/solvers/xpress_persistent.rst similarity index 100% rename from doc/OnlineDocs/reference/library_reference/solvers/xpress_persistent.rst rename to doc/OnlineDocs/reference/topical/solvers/xpress_persistent.rst From 49ea85c6c0569454def0474d446850fd973bc653 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Sun, 8 Sep 2024 15:55:22 -0400 Subject: [PATCH 2427/3044] change the sequence of bigm and detect fixed vars --- pyomo/contrib/gdpopt/ldsda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/gdpopt/ldsda.py b/pyomo/contrib/gdpopt/ldsda.py index c5987dde574..002db30314f 100644 --- a/pyomo/contrib/gdpopt/ldsda.py +++ b/pyomo/contrib/gdpopt/ldsda.py @@ -178,6 +178,7 @@ def _solve_GDP_subproblem(self, external_var_value, search_type, config): try: with SuppressInfeasibleWarning(): try: + TransformationFactory('gdp.bigm').apply_to(subproblem) fbbt(subproblem, integer_tol=config.integer_tolerance) TransformationFactory('contrib.detect_fixed_vars').apply_to( subproblem @@ -188,7 +189,6 @@ def _solve_GDP_subproblem(self, external_var_value, search_type, config): TransformationFactory( 'contrib.deactivate_trivial_constraints' ).apply_to(subproblem, tmp=False, ignore_infeasible=False) - TransformationFactory('gdp.bigm').apply_to(subproblem) except InfeasibleConstraintException: return False minlp_args = dict(config.minlp_solver_args) From c5ec4f23c78a68bd729f26512596ede08908ad18 Mon Sep 17 00:00:00 2001 From: ZedongPeng Date: Sun, 8 Sep 2024 16:37:55 -0400 Subject: [PATCH 2428/3044] add test for ldsda --- .../gdpopt/tests/four_stage_dynamic_model.py | 387 ++++++++++++++++++ pyomo/contrib/gdpopt/tests/test_ldsda.py | 47 +++ 2 files changed, 434 insertions(+) create mode 100644 pyomo/contrib/gdpopt/tests/four_stage_dynamic_model.py create mode 100644 pyomo/contrib/gdpopt/tests/test_ldsda.py diff --git a/pyomo/contrib/gdpopt/tests/four_stage_dynamic_model.py b/pyomo/contrib/gdpopt/tests/four_stage_dynamic_model.py new file mode 100644 index 00000000000..2d4dbe9c02c --- /dev/null +++ b/pyomo/contrib/gdpopt/tests/four_stage_dynamic_model.py @@ -0,0 +1,387 @@ +# from pyomo.environ import * +from pyomo.core import ( + Var, + Constraint, + Objective, + Set, + minimize, + exp, + ConcreteModel, + LogicalConstraint, + exactly, + lnot, + lor, + BooleanVar, + land, +) +from pyomo.dae import Integral, DerivativeVar, ContinuousSet +from pyomo.gdp import Disjunct, Disjunction + + +def build_model(mode_transfer=False): + model = ConcreteModel() + + # Set + model.stage = Set(initialize=[1, 2, 3, 4]) + model.mode = Set(initialize=[1, 2, 3]) + + model.t1 = ContinuousSet(bounds=(0, 1)) + model.t2 = ContinuousSet(bounds=(1, 2)) + model.t3 = ContinuousSet(bounds=(2, 3)) + model.t4 = ContinuousSet(bounds=(3, 4)) + + # Variables + model.x1 = Var(model.t1, bounds=(0, 10)) + model.x2 = Var(model.t2, bounds=(0, 10)) + model.x3 = Var(model.t3, bounds=(0, 10)) + model.x4 = Var(model.t4, bounds=(0, 10)) + model.u1 = Var(bounds=(-4, 4)) + model.u2 = Var(bounds=(-4, 4)) + model.u3 = Var(bounds=(-4, 4)) + model.u4 = Var(bounds=(-4, 4)) + + # Dynamic model + model.dxdt1 = DerivativeVar(model.x1, wrt=model.t1) + model.dxdt2 = DerivativeVar(model.x2, wrt=model.t2) + model.dxdt3 = DerivativeVar(model.x3, wrt=model.t3) + model.dxdt4 = DerivativeVar(model.x4, wrt=model.t4) + + # logic constraint + model.stage_mode = Disjunct(model.stage * model.mode) + model.d = Disjunction(model.stage) + + # Stage 1 + + def stage1_mode1_dynamic(disjunct, t): + model = disjunct.model() + return model.dxdt1[t] == -model.x1[t] * exp(model.x1[t] - 1) + model.u1 + + model.stage_mode[1, 1].mode1_dynamic_constraint = Constraint( + model.t1, rule=stage1_mode1_dynamic + ) + + def stage1_mode2_dynamic(disjunct, t): + model = disjunct.model() + return model.dxdt1[t] == (0.5 * model.x1[t] ** 3 + model.u1) / 20 + + model.stage_mode[1, 2].mode2_dynamic_constraint = Constraint( + model.t1, rule=stage1_mode2_dynamic + ) + + def stage1_mode3_dynamic(disjunct, t): + model = disjunct.model() + return model.dxdt1[t] == (model.x1[t] ** 2 + model.u1) / (t + 20) + + model.stage_mode[1, 3].mode3_dynamic_constraint = Constraint( + model.t1, rule=stage1_mode3_dynamic + ) + + # Stage 2 + + def stage2_mode1_dynamic(disjunct, t): + model = disjunct.model() + return model.dxdt2[t] == -model.x2[t] * exp(model.x2[t] - 1) + model.u2 + + model.stage_mode[2, 1].mode1_dynamic_constraint = Constraint( + model.t2, rule=stage2_mode1_dynamic + ) + + def stage2_mode2_dynamic(disjunct, t): + model = disjunct.model() + return model.dxdt2[t] == (0.5 * model.x2[t] ** 3 + model.u2) / 20 + + model.stage_mode[2, 2].mode2_dynamic_constraint = Constraint( + model.t2, rule=stage2_mode2_dynamic + ) + + def stage2_mode3_dynamic(disjunct, t): + model = disjunct.model() + return model.dxdt2[t] == (model.x2[t] ** 2 + model.u2) / (t + 20) + + model.stage_mode[2, 3].mode3_dynamic_constraint = Constraint( + model.t2, rule=stage2_mode3_dynamic + ) + + # Stage 3 + + def stage3_mode1_dynamic(disjunct, t): + model = disjunct.model() + return model.dxdt3[t] == -model.x3[t] * exp(model.x3[t] - 1) + model.u3 + + model.stage_mode[3, 1].mode1_dynamic_constraint = Constraint( + model.t3, rule=stage3_mode1_dynamic + ) + + def stage3_mode2_dynamic(disjunct, t): + model = disjunct.model() + return model.dxdt3[t] == (0.5 * model.x3[t] ** 3 + model.u3) / 20 + + model.stage_mode[3, 2].mode2_dynamic_constraint = Constraint( + model.t3, rule=stage3_mode2_dynamic + ) + + def stage3_mode3_dynamic(disjunct, t): + model = disjunct.model() + return model.dxdt3[t] == (model.x3[t] ** 2 + model.u3) / (t + 20) + + model.stage_mode[3, 3].mode3_dynamic_constraint = Constraint( + model.t3, rule=stage3_mode3_dynamic + ) + + # Stage 4 + + def stage4_mode1_dynamic(disjunct, t): + model = disjunct.model() + return model.dxdt4[t] == -model.x4[t] * exp(model.x4[t] - 1) + model.u4 + + model.stage_mode[4, 1].mode1_dynamic_constraint = Constraint( + model.t4, rule=stage4_mode1_dynamic + ) + + def stage4_mode2_dynamic(disjunct, t): + model = disjunct.model() + return model.dxdt4[t] == (0.5 * model.x4[t] ** 3 + model.u4) / 20 + + model.stage_mode[4, 2].mode2_dynamic_constraint = Constraint( + model.t4, rule=stage4_mode2_dynamic + ) + + def stage4_mode3_dynamic(disjunct, t): + model = disjunct.model() + return model.dxdt4[t] == (model.x4[t] ** 2 + model.u4) / (t + 20) + + model.stage_mode[4, 3].mode3_dynamic_constraint = Constraint( + model.t4, rule=stage4_mode3_dynamic + ) + + model.d[1] = [ + model.stage_mode[1, 1], + model.stage_mode[1, 2], + model.stage_mode[1, 3], + ] + model.d[2] = [ + model.stage_mode[2, 1], + model.stage_mode[2, 2], + model.stage_mode[2, 3], + ] + model.d[3] = [ + model.stage_mode[3, 1], + model.stage_mode[3, 2], + model.stage_mode[3, 3], + ] + model.d[4] = [ + model.stage_mode[4, 1], + model.stage_mode[4, 2], + model.stage_mode[4, 3], + ] + + if mode_transfer: + model.lc1 = LogicalConstraint( + expr=exactly( + 1, + model.stage_mode[1, 1].indicator_var, + model.stage_mode[1, 2].indicator_var, + model.stage_mode[1, 3].indicator_var, + ) + ) + model.lc2 = LogicalConstraint( + expr=exactly( + 1, + model.stage_mode[2, 1].indicator_var, + model.stage_mode[2, 2].indicator_var, + model.stage_mode[2, 3].indicator_var, + ) + ) + model.lc3 = LogicalConstraint( + expr=exactly( + 1, + model.stage_mode[3, 1].indicator_var, + model.stage_mode[3, 2].indicator_var, + model.stage_mode[3, 3].indicator_var, + ) + ) + model.lc4 = LogicalConstraint( + expr=exactly( + 1, + model.stage_mode[4, 1].indicator_var, + model.stage_mode[4, 2].indicator_var, + model.stage_mode[4, 3].indicator_var, + ) + ) + model.transfer_stage1 = Set(initialize=[2, 3, 4]) + model.transfer_stage2 = Set(initialize=[2, 3, 4, 5]) + model.mode_stransfer_set = Set(initialize=[1, 2]) + model.mode_transfer = BooleanVar( + model.transfer_stage2, model.mode_stransfer_set + ) + model.mode_transfer_lc1 = LogicalConstraint( + expr=exactly( + 1, + model.mode_transfer[2, 1], + model.mode_transfer[3, 1], + model.mode_transfer[4, 1], + model.mode_transfer[5, 1], + ) + ) + model.mode_transfer_lc2 = LogicalConstraint( + expr=exactly( + 1, + model.mode_transfer[2, 2], + model.mode_transfer[3, 2], + model.mode_transfer[4, 2], + model.mode_transfer[5, 2], + ) + ) + + def _mode_transfer_rule1(model, stage): + return model.mode_transfer[stage, 1].equivalent_to( + land( + model.stage_mode[stage - 1, 1].indicator_var, + model.stage_mode[stage, 2].indicator_var, + ) + ) + + model.mode_transfer2mode_choice_lc1 = LogicalConstraint( + model.transfer_stage1, rule=_mode_transfer_rule1 + ) + + def _mode_transfer_rule2(model, stage): + return model.mode_transfer[stage, 2].equivalent_to( + land( + model.stage_mode[stage - 1, 2].indicator_var, + model.stage_mode[stage, 3].indicator_var, + ) + ) + + model.mode_transfer2mode_choice_lc2 = LogicalConstraint( + model.transfer_stage1, rule=_mode_transfer_rule2 + ) + + def _mode_transfer_rule3(model, stage): + return model.mode_transfer[stage, 2].implies( + lor( + model.mode_transfer[stage1, 1] + for stage1 in model.transfer_stage1 + if stage1 < stage + ) + ) + + model.mode_transfer2mode_choice_lc3 = LogicalConstraint( + model.transfer_stage1, rule=_mode_transfer_rule3 + ) + + def _mode_transfer_rule4(model): + return model.mode_transfer[5, 1].implies( + lnot( + lor( + model.stage_mode[stage1, 2].indicator_var + for stage1 in model.stage + ) + ) + ) + + model.mode_transfer2mode_choice_lc4 = LogicalConstraint( + rule=_mode_transfer_rule4 + ) + + def _mode_transfer_rule5(model): + return model.mode_transfer[5, 2].implies( + lnot( + lor( + model.stage_mode[stage1, 3].indicator_var + for stage1 in model.stage + ) + ) + ) + + model.mode_transfer2mode_choice_lc5 = LogicalConstraint( + rule=_mode_transfer_rule5 + ) + + # Sequence constraint + def _sequence_rule1(model, stage): + if stage == 1: + return Constraint.Skip + else: + return model.stage_mode[stage, 2].indicator_var.implies( + lor( + model.stage_mode[stage2, 1].indicator_var + for stage2 in model.stage + if stage2 < stage + ) + ) + + model.seq1 = LogicalConstraint(model.stage, rule=_sequence_rule1) + model.stage_mode[1, 2].indicator_var.fix(False) + + def _sequence_rule2(model, stage): + if stage == 4: + return Constraint.Skip + else: + return model.stage_mode[stage, 2].indicator_var.implies( + lnot( + lor( + model.stage_mode[stage2, 1].indicator_var + for stage2 in model.stage + if stage2 > stage + ) + ) + ) + + model.seq2 = LogicalConstraint(model.stage, rule=_sequence_rule2) + + def _sequence_rule3(model, stage): + if stage <= 1: + return Constraint.Skip + else: + return model.stage_mode[stage, 3].indicator_var.implies( + lor( + model.stage_mode[stage2, 2].indicator_var + for stage2 in model.stage + if stage2 < stage + ) + ) + + model.seq3 = LogicalConstraint(model.stage, rule=_sequence_rule3) + model.stage_mode[1, 3].indicator_var.fix(False) + model.stage_mode[2, 3].indicator_var.fix(False) + + def _sequence_rule4(model, stage): + if stage == 4: + return Constraint.Skip + else: + return model.stage_mode[stage, 3].indicator_var.implies( + lnot( + lor( + model.stage_mode[stage2, 2].indicator_var + for stage2 in model.stage + if stage2 > stage + ) + ) + ) + + model.seq4 = LogicalConstraint(model.stage, rule=_sequence_rule4) + + model.c1 = Constraint(expr=model.x1[0] == 1) + model.c2 = Constraint(expr=model.x1[1] == model.x2[1]) + model.c3 = Constraint(expr=model.x2[2] == model.x3[2]) + model.c4 = Constraint(expr=model.x3[3] == model.x4[3]) + + # Objective function + model.intx1 = Integral( + model.t1, wrt=model.t1, rule=lambda model, t: model.x1[t] ** 2 + ) + model.intx2 = Integral( + model.t2, wrt=model.t2, rule=lambda model, t: model.x2[t] ** 2 + ) + model.intx3 = Integral( + model.t3, wrt=model.t3, rule=lambda model, t: model.x3[t] ** 2 + ) + model.intx4 = Integral( + model.t4, wrt=model.t4, rule=lambda model, t: model.x4[t] ** 2 + ) + + model.obj = Objective( + expr=-(model.intx1 + model.intx2 + model.intx3 + model.intx4), sense=minimize + ) + return model diff --git a/pyomo/contrib/gdpopt/tests/test_ldsda.py b/pyomo/contrib/gdpopt/tests/test_ldsda.py new file mode 100644 index 00000000000..9bcb2fd10cb --- /dev/null +++ b/pyomo/contrib/gdpopt/tests/test_ldsda.py @@ -0,0 +1,47 @@ +from pyomo.environ import SolverFactory, value, Var, Constraint, TransformationFactory +from pyomo.gdp import Disjunct +import pyomo.common.unittest as unittest +from four_stage_dynamic_model import build_model + + +class TestGDPoptLDSDA(unittest.TestCase): + """Real unit tests for GDPopt""" + + @unittest.skipUnless(SolverFactory('gams').available(), "gams solver not available") + def test_solve_four_stage_dynamic_model(self): + + model = build_model(mode_transfer=True) + + # Discretize the model using dae.collocation + discretizer = TransformationFactory('dae.collocation') + discretizer.apply_to(model, nfe=10, ncp=3, scheme='LAGRANGE-RADAU') + # We need to reconstruct the constraints in disjuncts after discretization. + # This is a bug in Pyomo.dae. https://github.com/Pyomo/pyomo/issues/3101 + for disjunct in model.component_data_objects(ctype=Disjunct): + for constraint in disjunct.component_objects(ctype=Constraint): + constraint._constructed = False + constraint.construct() + + for dxdt in model.component_data_objects(ctype=Var, descend_into=True): + if 'dxdt' in dxdt.name: + dxdt.setlb(-300) + dxdt.setub(300) + + for direction_norm in ['L2', 'Linf']: + result = SolverFactory('gdpopt.ldsda').solve( + model, + direction_norm=direction_norm, + minlp_solver='gams', + minlp_solver_args=dict(solver='knitro'), + starting_point=[1, 2], + logical_constraint_list=[ + model.mode_transfer_lc1.name, + model.mode_transfer_lc2.name, + ], + time_limit=100, + ) + self.assertAlmostEqual(value(model.obj), -23.305325, places=4) + + +if __name__ == '__main__': + unittest.main() From 9e27fbd2ec228c5b1c544a56fff1cec975fc2b54 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 9 Sep 2024 08:31:59 -0600 Subject: [PATCH 2429/3044] Moving the mapping for the bound constraints to when we know if there are both lower and upper bounds (to avoid very slow calls to Reference) --- pyomo/gdp/plugins/hull.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 854366c0cf0..714f11b8f56 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -20,7 +20,7 @@ from pyomo.common.modeling import unique_component_name from pyomo.core.expr.numvalue import ZeroConstant import pyomo.core.expr as EXPR -from pyomo.core.base import TransformationFactory, Reference +from pyomo.core.base import TransformationFactory from pyomo.core import ( Block, BooleanVar, @@ -451,15 +451,8 @@ def _transform_disjunctionData( ub_idx=(idx, 'ub'), var_free_indicator=var_free, ) - # Update mappings: - var_info = var.parent_block().private_data() - disaggregated_var_map = var_info.disaggregated_var_map - dis_var_info = disaggregated_var.parent_block().private_data() - - dis_var_info.bigm_constraint_map[disaggregated_var][obj] = Reference( - disaggregated_var_bounds[idx, :] - ) - dis_var_info.original_var_map[disaggregated_var] = var + original_var_info = var.parent_block().private_data() + disaggregated_var_map = original_var_info.disaggregated_var_map # For every Disjunct the Var does not appear in, we want to map # that this new variable is its disaggreggated variable. @@ -611,6 +604,13 @@ def _declare_disaggregated_var_bounds( ub_idx, var_free_indicator, ): + # For updating mappings: + original_var_info = original_var.parent_block().private_data() + disaggregated_var_map = original_var_info.disaggregated_var_map + disaggregated_var_info = disaggregatedVar.parent_block().private_data() + + disaggregated_var_info.bigm_constraint_map[disaggregatedVar][disjunct] = {} + lb = original_var.lb ub = original_var.ub if lb is None or ub is None: @@ -625,12 +625,12 @@ def _declare_disaggregated_var_bounds( if lb: bigmConstraint.add(lb_idx, var_free_indicator * lb <= disaggregatedVar) + disaggregated_var_info.bigm_constraint_map[ + disaggregatedVar][disjunct][lb_idx] = bigmConstraint[lb_idx] if ub: bigmConstraint.add(ub_idx, disaggregatedVar <= ub * var_free_indicator) - - original_var_info = original_var.parent_block().private_data() - disaggregated_var_map = original_var_info.disaggregated_var_map - disaggregated_var_info = disaggregatedVar.parent_block().private_data() + disaggregated_var_info.bigm_constraint_map[ + disaggregatedVar][disjunct][ub_idx] = bigmConstraint[ub_idx] # store the mappings from variables to their disaggregated selves on # the transformation block From 92fc4ec689156aa1fdfbc4c00f4b6d59f4fc0923 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 9 Sep 2024 08:32:24 -0600 Subject: [PATCH 2430/3044] Rewriting the mock-up of the mapping in one hull test --- pyomo/gdp/tests/test_hull.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 07876a9d213..cef478c1dfb 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -530,14 +530,16 @@ def test_bigMConstraint_mappings(self): mappings[disjBlock[i].disaggregatedVars.x] = disjBlock[i].x_bounds if i == 1: # this disjunct has x, w, and no y mappings[disjBlock[i].disaggregatedVars.w] = disjBlock[i].w_bounds - mappings[transBlock._disaggregatedVars[0]] = Reference( - transBlock._boundsConstraints[0, ...] - ) + mappings[transBlock._disaggregatedVars[0]] = { + key: val for key, val in transBlock._boundsConstraints.items() if + key[0] == 0 + } elif i == 0: # this disjunct has x, y, and no w mappings[disjBlock[i].disaggregatedVars.y] = disjBlock[i].y_bounds - mappings[transBlock._disaggregatedVars[1]] = Reference( - transBlock._boundsConstraints[1, ...] - ) + mappings[transBlock._disaggregatedVars[1]] = { + key: val for key, val in transBlock._boundsConstraints.items() if + key[0] == 1 + } for var, cons in mappings.items(): returned_cons = hull.get_var_bounds_constraint(var) # This sometimes refers a reference to the right part of a From c863ae1df25e67d9a98d29c57bae5c675c08fa52 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 9 Sep 2024 15:42:38 -0400 Subject: [PATCH 2431/3044] Test decision rule order efficiency --- pyomo/contrib/pyros/tests/test_master.py | 79 +++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index 039168ce29d..3f5a5c57358 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -33,6 +33,7 @@ construct_master_feasibility_problem, construct_dr_polishing_problem, MasterProblemData, + higher_order_decision_rule_efficiency, ) from pyomo.contrib.pyros.util import ( ModelData, @@ -56,7 +57,7 @@ logger = logging.getLogger(__name__) -def build_simple_model_data(objective_focus="worst_case"): +def build_simple_model_data(objective_focus="worst_case", decision_rule_order=1): """ Test construction of master problem. """ @@ -73,7 +74,7 @@ def build_simple_model_data(objective_focus="worst_case"): config = Bunch( uncertain_params=[m.u], objective_focus=ObjectiveType[objective_focus], - decision_rule_order=1, + decision_rule_order=decision_rule_order, progress_logger=logger, nominal_uncertain_param_vals=[0.4], separation_priority_order=dict(), @@ -474,6 +475,80 @@ def test_construct_dr_polishing_problem_params_zero(self): self.assertFalse(polishing_model.polishing_abs_val_ub_con_0[1].active) +class TestHigherOrderDecisionRuleEfficiency(unittest.TestCase): + """ + Test efficiency for decision rules. + """ + def test_higher_order_decision_rule_efficiency(self): + """ + Test higher-order decision rule efficiency. + """ + model_data = build_simple_model_data(decision_rule_order=2) + master_model = construct_initial_master_problem(model_data) + master_data = Bunch( + master_model=master_model, iteration=0, config=model_data.config + ) + decision_rule_vars = ( + master_data.master_model.scenarios[0, 0].first_stage.decision_rule_vars[0] + ) + + for iter_num in range(4): + master_data.iteration = iter_num + higher_order_decision_rule_efficiency(master_data) + self.assertFalse( + decision_rule_vars[0].fixed, + msg=( + f"DR Var {decision_rule_vars[1].name!r} should not " + f"be fixed by efficiency in iteration {iter_num}" + ), + ) + if iter_num == 0: + self.assertTrue( + decision_rule_vars[1].fixed, + msg=( + f"DR Var {decision_rule_vars[1].name!r} should " + f"be fixed by efficiency in iteration {iter_num}" + ), + ) + self.assertTrue( + decision_rule_vars[2].fixed, + msg=( + f"DR Var {decision_rule_vars[2].name!r} should " + f"be fixed by efficiency in iteration {iter_num}" + ), + ) + elif iter_num <= len(master_data.config.uncertain_params): + self.assertFalse( + decision_rule_vars[1].fixed, + msg=( + f"DR Var {decision_rule_vars[1].name!r} should not " + f"be fixed by efficiency in iteration {iter_num}" + ), + ) + self.assertTrue( + decision_rule_vars[2].fixed, + msg=( + f"DR Var {decision_rule_vars[2].name!r} should " + f"be fixed by efficiency in iteration {iter_num}" + ), + ) + else: + self.assertFalse( + decision_rule_vars[1].fixed, + msg=( + f"DR Var {decision_rule_vars[1].name!r} should not " + f"be fixed by efficiency in iteration {iter_num}" + ), + ) + self.assertFalse( + decision_rule_vars[2].fixed, + msg=( + f"DR Var {decision_rule_vars[2].name!r} should not " + f"be fixed by efficiency in iteration {iter_num}" + ), + ) + + class TestSolveMaster(unittest.TestCase): """ Test method for solving master problem From 4d6d5fad3c35e77759a061297f57fe48549b9c50 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 9 Sep 2024 15:44:17 -0400 Subject: [PATCH 2432/3044] Apply black --- pyomo/contrib/pyros/tests/test_master.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index 3f5a5c57358..cd44b9eedf9 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -479,6 +479,7 @@ class TestHigherOrderDecisionRuleEfficiency(unittest.TestCase): """ Test efficiency for decision rules. """ + def test_higher_order_decision_rule_efficiency(self): """ Test higher-order decision rule efficiency. @@ -488,9 +489,9 @@ def test_higher_order_decision_rule_efficiency(self): master_data = Bunch( master_model=master_model, iteration=0, config=model_data.config ) - decision_rule_vars = ( - master_data.master_model.scenarios[0, 0].first_stage.decision_rule_vars[0] - ) + decision_rule_vars = master_data.master_model.scenarios[ + 0, 0 + ].first_stage.decision_rule_vars[0] for iter_num in range(4): master_data.iteration = iter_num From e97eed6903ba04b60ec698eb267eb9c10a476981 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 9 Sep 2024 14:54:53 -0600 Subject: [PATCH 2433/3044] not using the same index in the _boundConstraint object as in the mappings (which means mappings are pretty and constraint indices are unique, hurrah) --- pyomo/gdp/plugins/hull.py | 20 ++++++++++---------- pyomo/gdp/tests/test_hull.py | 2 ++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 714f11b8f56..2065cbdc47f 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -447,9 +447,8 @@ def _transform_disjunctionData( disaggregatedVar=disaggregated_var, disjunct=obj, bigmConstraint=disaggregated_var_bounds, - lb_idx=(idx, 'lb'), - ub_idx=(idx, 'ub'), var_free_indicator=var_free, + var_idx=idx ) original_var_info = var.parent_block().private_data() disaggregated_var_map = original_var_info.disaggregated_var_map @@ -537,8 +536,6 @@ def _transform_disjunct( disaggregatedVar=disaggregatedVar, disjunct=obj, bigmConstraint=bigmConstraint, - lb_idx='lb', - ub_idx='ub', var_free_indicator=obj.indicator_var.get_associated_binary(), ) # update the bigm constraint mappings @@ -566,8 +563,6 @@ def _transform_disjunct( disaggregatedVar=var, disjunct=obj, bigmConstraint=bigmConstraint, - lb_idx='lb', - ub_idx='ub', var_free_indicator=obj.indicator_var.get_associated_binary(), ) # update the bigm constraint mappings @@ -600,9 +595,8 @@ def _declare_disaggregated_var_bounds( disaggregatedVar, disjunct, bigmConstraint, - lb_idx, - ub_idx, var_free_indicator, + var_idx=None, ): # For updating mappings: original_var_info = original_var.parent_block().private_data() @@ -624,13 +618,19 @@ def _declare_disaggregated_var_bounds( disaggregatedVar.setub(max(0, ub)) if lb: + lb_idx = 'lb' + if var_idx is not None: + lb_idx = (var_idx, 'lb') bigmConstraint.add(lb_idx, var_free_indicator * lb <= disaggregatedVar) disaggregated_var_info.bigm_constraint_map[ - disaggregatedVar][disjunct][lb_idx] = bigmConstraint[lb_idx] + disaggregatedVar][disjunct]['lb'] = bigmConstraint[lb_idx] if ub: + ub_idx = 'ub' + if var_idx is not None: + ub_idx = (var_idx, 'ub') bigmConstraint.add(ub_idx, disaggregatedVar <= ub * var_free_indicator) disaggregated_var_info.bigm_constraint_map[ - disaggregatedVar][disjunct][ub_idx] = bigmConstraint[ub_idx] + disaggregatedVar][disjunct]['ub'] = bigmConstraint[ub_idx] # store the mappings from variables to their disaggregated selves on # the transformation block diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index cef478c1dfb..a4602784bc4 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -547,6 +547,8 @@ def test_bigMConstraint_mappings(self): # themselves might not be the same object. The ConstraintDatas # are though: for key, constraintData in cons.items(): + if type(key) is tuple: + key = key[1] self.assertIs(returned_cons[key], constraintData) def test_create_using_nonlinear(self): From f2545d4cc44542478f24b9ad38e3e1d710cff46d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 9 Sep 2024 14:55:31 -0600 Subject: [PATCH 2434/3044] Black --- pyomo/gdp/plugins/hull.py | 12 +++++++----- pyomo/gdp/tests/test_hull.py | 10 ++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 2065cbdc47f..5ad7018a002 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -448,7 +448,7 @@ def _transform_disjunctionData( disjunct=obj, bigmConstraint=disaggregated_var_bounds, var_free_indicator=var_free, - var_idx=idx + var_idx=idx, ) original_var_info = var.parent_block().private_data() disaggregated_var_map = original_var_info.disaggregated_var_map @@ -622,15 +622,17 @@ def _declare_disaggregated_var_bounds( if var_idx is not None: lb_idx = (var_idx, 'lb') bigmConstraint.add(lb_idx, var_free_indicator * lb <= disaggregatedVar) - disaggregated_var_info.bigm_constraint_map[ - disaggregatedVar][disjunct]['lb'] = bigmConstraint[lb_idx] + disaggregated_var_info.bigm_constraint_map[disaggregatedVar][disjunct][ + 'lb' + ] = bigmConstraint[lb_idx] if ub: ub_idx = 'ub' if var_idx is not None: ub_idx = (var_idx, 'ub') bigmConstraint.add(ub_idx, disaggregatedVar <= ub * var_free_indicator) - disaggregated_var_info.bigm_constraint_map[ - disaggregatedVar][disjunct]['ub'] = bigmConstraint[ub_idx] + disaggregated_var_info.bigm_constraint_map[disaggregatedVar][disjunct][ + 'ub' + ] = bigmConstraint[ub_idx] # store the mappings from variables to their disaggregated selves on # the transformation block diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index a4602784bc4..7fd396a38f0 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -531,14 +531,16 @@ def test_bigMConstraint_mappings(self): if i == 1: # this disjunct has x, w, and no y mappings[disjBlock[i].disaggregatedVars.w] = disjBlock[i].w_bounds mappings[transBlock._disaggregatedVars[0]] = { - key: val for key, val in transBlock._boundsConstraints.items() if - key[0] == 0 + key: val + for key, val in transBlock._boundsConstraints.items() + if key[0] == 0 } elif i == 0: # this disjunct has x, y, and no w mappings[disjBlock[i].disaggregatedVars.y] = disjBlock[i].y_bounds mappings[transBlock._disaggregatedVars[1]] = { - key: val for key, val in transBlock._boundsConstraints.items() if - key[0] == 1 + key: val + for key, val in transBlock._boundsConstraints.items() + if key[0] == 1 } for var, cons in mappings.items(): returned_cons = hull.get_var_bounds_constraint(var) From 209ad5cbfc6f43158a8da6105be30d82a1a4fe44 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 9 Sep 2024 15:21:14 -0600 Subject: [PATCH 2435/3044] NFC: updating docstring --- pyomo/gdp/plugins/hull.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 5ad7018a002..f910f5775ad 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -930,10 +930,9 @@ def get_disaggregation_constraint( def get_var_bounds_constraint(self, v, disjunct=None): """ - Returns the IndexedConstraint which sets a disaggregated - variable to be within its bounds when its Disjunct is active and to - be 0 otherwise. (It is always an IndexedConstraint because each - bound becomes a separate constraint.) + Returns a dictionary mapping keys 'lb' and/or 'ub' to the Constraints that + set a disaggregated variable to be within its lower and upper bounds + (respectively) when its Disjunct is active and to be 0 otherwise. Parameters ---------- From a0b24370b1de452683a83b2cd853af0d584095bd Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 9 Sep 2024 15:21:25 -0600 Subject: [PATCH 2436/3044] Removing unused input --- pyomo/gdp/tests/test_hull.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/gdp/tests/test_hull.py b/pyomo/gdp/tests/test_hull.py index 7fd396a38f0..8a780d4b988 100644 --- a/pyomo/gdp/tests/test_hull.py +++ b/pyomo/gdp/tests/test_hull.py @@ -32,7 +32,6 @@ Param, Objective, TerminationCondition, - Reference, ) from pyomo.core.expr.compare import ( assertExpressionsEqual, From 838d560bd9cf872f4c87510304e829d122fa10d9 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 9 Sep 2024 15:29:45 -0600 Subject: [PATCH 2437/3044] Fixing a couple typos the new version of typos (1.24.5) is finding --- examples/doc/samples/case_studies/diet/DietProblem.tex | 2 +- examples/doc/samples/case_studies/diet/README.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/doc/samples/case_studies/diet/DietProblem.tex b/examples/doc/samples/case_studies/diet/DietProblem.tex index d933e097d88..e2ae7ba4c62 100644 --- a/examples/doc/samples/case_studies/diet/DietProblem.tex +++ b/examples/doc/samples/case_studies/diet/DietProblem.tex @@ -54,7 +54,7 @@ \subsection*{Build the model} The comma indicates that this parameter is over two different sets, and thus is in two dimensions. When we create the data file, we will be able to fill in how much of each nutrient each food contains. -At this point we have defined our sets and parameters. However, we have yet to cosnider the amount of food to be bought and eaten. This is the variable weâre trying to solve for, and thus we create an object of the variable class. Since this is just recording how much food to purchase, we create a one dimensional variable over food: +At this point we have defined our sets and parameters. However, we have yet to consider the amount of food to be bought and eaten. This is the variable weâre trying to solve for, and thus we create an object of the variable class. Since this is just recording how much food to purchase, we create a one dimensional variable over food: \begin{verbatim}model.amount=Var(model.foods, within = NonNegativeReals) \end{verbatim} diff --git a/examples/doc/samples/case_studies/diet/README.txt b/examples/doc/samples/case_studies/diet/README.txt index c30e963dc27..c382b4d653c 100644 --- a/examples/doc/samples/case_studies/diet/README.txt +++ b/examples/doc/samples/case_studies/diet/README.txt @@ -68,7 +68,7 @@ model.nutrient_value=Param(model.nutrients, model.foods) The comma indicates that this parameter is over two different sets, and thus is in two dimensions. When we create the data file, we will be able to fill in how much of each nutrient each food contains. -At this point we have defined our sets and parameters. However, we have yet to cosnider the amount of food to be bought and eaten. This is the variable we're trying to solve for, and thus we create an object of the variable class. Since this is just recording how much food to purchase, we create a one dimensional variable over food: +At this point we have defined our sets and parameters. However, we have yet to consider the amount of food to be bought and eaten. This is the variable we're trying to solve for, and thus we create an object of the variable class. Since this is just recording how much food to purchase, we create a one dimensional variable over food: {{{ #!python From 1a73b626245d20801a375cc5b864efab260dba53 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Mon, 9 Sep 2024 15:46:20 -0600 Subject: [PATCH 2438/3044] Change BARON download URL --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index a4f2f8128e9..92c47b2d64b 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -519,7 +519,7 @@ jobs: $BARON_DIR = "${env:TPL_DIR}/baron" echo "$BARON_DIR" | ` Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - $URL = "https://minlp.com/downloads/xecs/baron/current/" + $URL = "https://minlp-downloads.nyc3.cdn.digitaloceanspaces.com/xecs/baron/current/" if ( "${{matrix.TARGET}}" -eq "win" ) { $INSTALLER = "${env:DOWNLOAD_DIR}/baron_install.exe" $URL += "baron-win64.exe" diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 1aefe02687b..857e8ebde4e 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -547,7 +547,7 @@ jobs: $BARON_DIR = "${env:TPL_DIR}/baron" echo "$BARON_DIR" | ` Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - $URL = "https://minlp.com/downloads/xecs/baron/current/" + $URL = "https://minlp-downloads.nyc3.cdn.digitaloceanspaces.com/xecs/baron/current/" if ( "${{matrix.TARGET}}" -eq "win" ) { $INSTALLER = "${env:DOWNLOAD_DIR}/baron_install.exe" $URL += "baron-win64.exe" From cfee53e5070c1b7f09e67ec7d2a5b34c21e78f39 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 10 Sep 2024 10:52:43 -0600 Subject: [PATCH 2439/3044] Shorten tests; print out dir structure --- .github/workflows/test_pr_and_main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 857e8ebde4e..dd84b559f69 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -667,7 +667,6 @@ jobs: run: | $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ - pyomo `pwd`/pyomo-model-libraries \ `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" - name: Run Pyomo MPI tests @@ -852,6 +851,7 @@ jobs: fi for ARTIFACT in artifacts/*_*${{matrix.TARGET}}_*; do NAME=`echo $ARTIFACT | cut -d/ -f2` + ls -la $ARTIFACT cp -v $ARTIFACT/.coverage .coverage-$NAME done rm -vf .coverage coverage.xml From 858c29f408c48c88b8098ed498d1aba20076cf20 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 10 Sep 2024 11:38:45 -0600 Subject: [PATCH 2440/3044] Print out dir after coverage combine --- .github/workflows/test_pr_and_main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index dd84b559f69..9d82758281f 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -692,6 +692,7 @@ jobs: coverage combine coverage report -i coverage xml -i + ls -la - name: Record build artifacts uses: actions/upload-artifact@v4 From 68ba5d6dd34bd42468a062519c5894ccd8c41138 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 10 Sep 2024 12:13:49 -0600 Subject: [PATCH 2441/3044] Turn on hidden file uploads --- .github/workflows/test_pr_and_main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 9d82758281f..34efa4a029b 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -667,6 +667,7 @@ jobs: run: | $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ + pyomo `pwd`/pyomo-model-libraries \ `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" - name: Run Pyomo MPI tests @@ -692,12 +693,12 @@ jobs: coverage combine coverage report -i coverage xml -i - ls -la - name: Record build artifacts uses: actions/upload-artifact@v4 with: name: ${{github.job}}_${{env.GHA_JOBGROUP}}-${{env.GHA_JOBNAME}} + include-hidden-files: true path: | .coverage coverage.xml @@ -852,7 +853,6 @@ jobs: fi for ARTIFACT in artifacts/*_*${{matrix.TARGET}}_*; do NAME=`echo $ARTIFACT | cut -d/ -f2` - ls -la $ARTIFACT cp -v $ARTIFACT/.coverage .coverage-$NAME done rm -vf .coverage coverage.xml From 55192864f39326d8b38b159ab14ecaed6489f6e4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 16 Sep 2024 08:56:10 -0600 Subject: [PATCH 2442/3044] Resolve issue in filter/validate deprecation path --- pyomo/core/base/set.py | 38 ++++++++++++++++---------- pyomo/core/tests/unit/test_set.py | 44 +++++++++++++++++++++++++------ 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 69b21c4d78b..5a15321c084 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1484,18 +1484,7 @@ def _cb_validate_filter(self, mode, val_iter): try: flag = fcn(block, (), *vstar) if flag: - deprecation_warning( - f"{self.__class__.__name__} {self.name}: '{mode}=' " - "callback signature matched (block, *value). " - "Please update the callback to match the signature " - f"(block, value{', *index' if comp.is_indexed() else ''}).", - version='6.8.0', - ) - orig_fcn = fcn._fcn - fcn = ParameterizedScalarCallInitializer( - lambda m, v: orig_fcn(m, *v), True - ) - setattr(comp, '_' + mode, fcn) + self._filter_validate_scalar_api_deprecation(mode, warning=True) yield value continue except TypeError: @@ -1536,6 +1525,21 @@ def _cb_validate_filter(self, mode, val_iter): ) raise exc from None + def _filter_validate_scalar_api_deprecation(self, mode, warning): + comp = self.parent_component() + fcn = getattr(comp, '_' + mode) + if warning: + deprecation_warning( + f"{self.__class__.__name__} {self.name}: '{mode}=' " + "callback signature matched (block, *value). " + "Please update the callback to match the signature " + f"(block, value{', *index' if comp.is_indexed() else ''}).", + version='6.8.0', + ) + orig_fcn = fcn._fcn + fcn = ParameterizedScalarCallInitializer(lambda m, v: orig_fcn(m, *v), True) + setattr(comp, '_' + mode, fcn) + def _cb_normalized_dimen_verifier(self, dimen, val_iter): for value in val_iter: if value.__class__ in native_types: @@ -2256,14 +2260,20 @@ def __init__(self, *args, **kwds): self._init_values._init = CountedCallInitializer( self, self._init_values._init ) - # HACK: the DAT parser needs to know the domain of a set in - # order to correctly parse the data stream. + if not self.is_indexed(): + # HACK: the DAT parser needs to know the domain of a set in + # order to correctly parse the data stream. if self._init_domain.constant(): self._domain = self._init_domain(self.parent_block(), None, self) if self._init_dimen.constant(): self._dimen = self._init_dimen(self.parent_block(), None) + if self._filter.__class__ is ParameterizedIndexedCallInitializer: + self._filter_validate_scalar_api_deprecation('filter', warning=False) + if self._validate.__class__ is ParameterizedIndexedCallInitializer: + self._filter_validate_scalar_api_deprecation('validate', warning=False) + @deprecated( "check_values() is deprecated: Sets only contain valid members", version='5.7' ) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 6529c2b60a9..6312aaf63c6 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4181,6 +4181,19 @@ def test_indexed_set(self): self.assertIs(type(m.I[3]), InsertionOrderSetData) self.assertEqual(m.I.data(), {1: (4, 2, 5), 2: (4, 2, 5), 3: (4, 2, 5)}) + # Explicit (constant dict) construction + m = ConcreteModel() + m.I = Set([1, 2], initialize={1: (4, 2, 5), 2: (7, 6)}) + self.assertEqual(len(m.I), 2) + self.assertEqual(list(m.I[1]), [4, 2, 5]) + self.assertEqual(list(m.I[2]), [7, 6]) + self.assertIsNot(m.I[1], m.I[2]) + self.assertTrue(m.I[1].isordered()) + self.assertTrue(m.I[2].isordered()) + self.assertIs(type(m.I[1]), InsertionOrderSetData) + self.assertIs(type(m.I[2]), InsertionOrderSetData) + self.assertEqual(m.I.data(), {1: (4, 2, 5), 2: (7, 6)}) + # Explicit (constant) construction m = ConcreteModel() m.I = Set([1, 2, 3], initialize=(4, 2, 5), ordered=Set.SortedOrder) @@ -4255,7 +4268,7 @@ def test_indexing(self): def test_add_filter_validate(self): m = ConcreteModel() m.I = Set(domain=Integers) - self.assertIs(m.I.filter, None) + self.assertIs(m.I._filter, None) with self.assertRaisesRegex( ValueError, r"Cannot add value 1.5 to Set I.\n" @@ -4302,7 +4315,7 @@ def _l_tri(model, i, j): return i >= j m.K = Set(initialize=RangeSet(3) * RangeSet(3), filter=_l_tri) - self.assertIsInstance(m.K.filter, ParameterizedScalarCallInitializer) + self.assertIsInstance(m.K._filter, ParameterizedScalarCallInitializer) self.assertEqual(list(m.K), [(1, 1), (2, 1), (2, 2), (3, 1), (3, 2), (3, 3)]) output = StringIO() @@ -4334,6 +4347,18 @@ def _lt_3(model, i): self.assertEqual(output.getvalue(), "") self.assertEqual(list(m.L[2]), [1, 2, 0]) + # This tests that the deprecation path works correctly in the + # case that the callback doesn't raise an error or ever return + # False + + def _l_off_diag(model, i, j): + self.assertIs(model, m) + return i != j + + m.M = Set(initialize=RangeSet(3) * RangeSet(3), filter=_l_off_diag) + self.assertIsInstance(m.M._filter, ParameterizedScalarCallInitializer) + self.assertEqual(list(m.M), [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]) + m = ConcreteModel() def _validate(model, val): @@ -4374,12 +4399,15 @@ def _validate(model, i, j): m.I2 = Set(validate=_validate) with LoggingIntercept(module='pyomo.core') as output: self.assertTrue(m.I2.add((0, 1))) - self.assertRegex( - output.getvalue().replace('\n', ' '), - r"DEPRECATED: OrderedScalarSet I2: 'validate=' callback " - r"signature matched \(block, \*value\). Please update the " - r"callback to match the signature \(block, value\)", - ) + # Note that we are not emitting a deprecation warning (yet) + # for scalar sets + # self.assertEqual(output.getvalue(), "") + # output.getvalue().replace('\n', ' '), + # r"DEPRECATED: OrderedScalarSet I2: 'validate=' callback " + # r"signature matched \(block, \*value\). Please update the " + # r"callback to match the signature \(block, value\)", + # ) + self.assertEqual(output.getvalue(), "") with LoggingIntercept(module='pyomo.core') as output: with self.assertRaisesRegex( ValueError, From 6cc0789d7a99ad2c9f7ff514e887e3c74f7232c2 Mon Sep 17 00:00:00 2001 From: Matthew Viens <75225878+viens-code@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:33:01 -0500 Subject: [PATCH 2443/3044] Add citation to balas.py Adding citation to original Balas work on hypercube cuts as core reference for no-good cuts methodology --- pyomo/contrib/alternative_solutions/balas.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index 489642a5b63..bc693ff9cad 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -35,7 +35,14 @@ def enumerate_binary_solutions( """ Finds alternative optimal solutions for a binary problem using no-good cuts. + + This function implements a no-good cuts technique inheriting from Balas's work on Cannonical Cuts: + Balas, Egon, and Robert Jeroslow. + “Canonical Cuts on the Unit Hypercube.” + SIAM Journal on Applied Mathematics 23, no. 1 (1972): 61–69. + http://www.jstor.org/stable/2099623. + Parameters ---------- model : ConcreteModel From 80a6137f7cda504efebb831ab1ea50f9705a8fa5 Mon Sep 17 00:00:00 2001 From: Matthew Viens <75225878+viens-code@users.noreply.github.com> Date: Mon, 16 Sep 2024 14:57:38 -0500 Subject: [PATCH 2444/3044] Add abs_opt_gap example to alternative_solutions documentation Added a knapsack example showing how to use abs_opt_gap to restrict what solutions are returned to the user based on optimality gaps --- .../alternative_solutions.rst | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst index cc5ab07c3cc..0937e85b16f 100644 --- a/doc/OnlineDocs/contributed_packages/alternative_solutions.rst +++ b/doc/OnlineDocs/contributed_packages/alternative_solutions.rst @@ -42,7 +42,7 @@ The following functions are defined in the alternative-solutions library: * Calculates the bounds on each variable by solving a series of min and max optimization problems where each variable is used as the objective function. This can be applied to any class of problem supported by the selected solver. -Usage Example +Basic Usage Example ------------- Many of functions in the alternative-solutions library have similar options, so we simply illustrate the ``enumerate_binary_solutions`` function. We define a simple knapsack example whose alternative solutions have integer objective values ranging from 0 to 90. @@ -88,6 +88,53 @@ Each ``Solution`` object contains information about the objective and variables, } +Gap Usage Example +------------- +When we only want some of the solutions based off a tolerance away from optimal, this can be done using the ``abs_opt_gap`` parameter. This is shown in the following simple knapsack examples where the weights and values are the same. + +.. doctest:: + :skipif: not glpk_available + + >>> import pyomo.environ as pyo + >>> import pyomo.contrib.alternative_solutions as aos + + >>> values = [10,9,2,1,1] + >>> weights = [10,9,2,1,1] + + >>> K = len(values) + >>> capacity = 12 + + >>> m = pyo.ConcreteModel() + >>> m.x = pyo.Var(range(K), within=pyo.Binary) + >>> m.o = pyo.Objective(expr=sum(values[i] * m.x[i] for i in range(K)), sense=pyo.maximize) + >>> m.c = pyo.Constraint(expr=sum(weights[i] * m.x[i] for i in range(K)) <= capacity) + + >>> solns = aos.enumerate_binary_solutions(m, num_solutions=10, solver="glpk", abs_opt_gap = 0.0) + >>> assert(len(solns) == 4) + +In this example, we only get the four ``Solution`` objects that have an ``objective_value`` of 12. +Note that while we wanted only those four solutions with no optimality gap, using a gap of half the smallest value (in this case .5) will return the same solutions and avoids any machine precision issues. + +.. doctest:: + :skipif: not glpk_available + + >>> import pyomo.environ as pyo + >>> import pyomo.contrib.alternative_solutions as aos + + >>> values = [10,9,2,1,1] + >>> weights = [10,9,2,1,1] + + >>> K = len(values) + >>> capacity = 12 + + >>> m = pyo.ConcreteModel() + >>> m.x = pyo.Var(range(K), within=pyo.Binary) + >>> m.o = pyo.Objective(expr=sum(values[i] * m.x[i] for i in range(K)), sense=pyo.maximize) + >>> m.c = pyo.Constraint(expr=sum(weights[i] * m.x[i] for i in range(K)) <= capacity) + + >>> solns = aos.enumerate_binary_solutions(m, num_solutions=10, solver="glpk", abs_opt_gap = 0.5) + >>> assert(len(solns) == 4) + Interface Documentation ----------------------- From 17d07a89bcefda1c2492ece35617024483ba5e13 Mon Sep 17 00:00:00 2001 From: Matthew Viens <75225878+viens-code@users.noreply.github.com> Date: Mon, 16 Sep 2024 15:35:55 -0500 Subject: [PATCH 2445/3044] Update balas.py to fix black issue --- pyomo/contrib/alternative_solutions/balas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index bc693ff9cad..35d9afafd5b 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -42,7 +42,7 @@ def enumerate_binary_solutions( “Canonical Cuts on the Unit Hypercube.” SIAM Journal on Applied Mathematics 23, no. 1 (1972): 61–69. http://www.jstor.org/stable/2099623. - + Parameters ---------- model : ConcreteModel From 8a01aba6acadeb37be4675782bff6cb627016116 Mon Sep 17 00:00:00 2001 From: Matthew Viens <75225878+viens-code@users.noreply.github.com> Date: Mon, 16 Sep 2024 15:45:00 -0500 Subject: [PATCH 2446/3044] Balas.py Black issue fix --- pyomo/contrib/alternative_solutions/balas.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index 35d9afafd5b..b2430d6eab6 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -35,12 +35,12 @@ def enumerate_binary_solutions( """ Finds alternative optimal solutions for a binary problem using no-good cuts. - + This function implements a no-good cuts technique inheriting from Balas's work on Cannonical Cuts: - Balas, Egon, and Robert Jeroslow. - “Canonical Cuts on the Unit Hypercube.” - SIAM Journal on Applied Mathematics 23, no. 1 (1972): 61–69. + Balas, Egon, and Robert Jeroslow. + “Canonical Cuts on the Unit Hypercube.” + SIAM Journal on Applied Mathematics 23, no. 1 (1972): 61–69. http://www.jstor.org/stable/2099623. Parameters From 79de242452744d6f73f35904877bb296bb5b8c37 Mon Sep 17 00:00:00 2001 From: Matthew Viens <75225878+viens-code@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:49:25 -0500 Subject: [PATCH 2447/3044] Update balas.py to fix spelling error --- pyomo/contrib/alternative_solutions/balas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/alternative_solutions/balas.py b/pyomo/contrib/alternative_solutions/balas.py index b2430d6eab6..b55eb21432e 100644 --- a/pyomo/contrib/alternative_solutions/balas.py +++ b/pyomo/contrib/alternative_solutions/balas.py @@ -36,7 +36,7 @@ def enumerate_binary_solutions( Finds alternative optimal solutions for a binary problem using no-good cuts. - This function implements a no-good cuts technique inheriting from Balas's work on Cannonical Cuts: + This function implements a no-good cuts technique inheriting from Balas's work on Canonical Cuts: Balas, Egon, and Robert Jeroslow. “Canonical Cuts on the Unit Hypercube.” From 4e45f6f7bd459ec4e2bc0d2558a3314ccec73b7d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Thu, 19 Sep 2024 08:07:13 -0600 Subject: [PATCH 2448/3044] Add URL Validation Status Badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 707f1a06c5a..36d4c7a4b43 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![Jenkins Status](https://github.com/Pyomo/jenkins-status/blob/main/pyomo_main.svg)](https://pyomo-jenkins.sandia.gov/) [![codecov](https://codecov.io/gh/Pyomo/pyomo/branch/main/graph/badge.svg)](https://codecov.io/gh/Pyomo/pyomo) [![Documentation Status](https://readthedocs.org/projects/pyomo/badge/?version=latest)](http://pyomo.readthedocs.org/en/latest/) + +[![URL Validation](https://github.com/Pyomo/pyomo/actions/workflows/url_check.yml/badge.svg?branch=main)](https://github.com/Pyomo/pyomo/actions/workflows/url_check.yml) [![Build services](https://github.com/Pyomo/jenkins-status/blob/main/pyomo_services.svg)](https://pyomo-jenkins.sandia.gov/) [![GitHub contributors](https://img.shields.io/github/contributors/pyomo/pyomo.svg)](https://github.com/pyomo/pyomo/graphs/contributors) [![Merged PRs](https://img.shields.io/github/issues-pr-closed-raw/pyomo/pyomo.svg?label=merged+PRs)](https://github.com/pyomo/pyomo/pulls?q=is:pr+is:merged) From d880f6a4d6cb0475876d55e52e294762422e4284 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Thu, 19 Sep 2024 08:09:08 -0600 Subject: [PATCH 2449/3044] One-line --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 36d4c7a4b43..b6319d3f526 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![Jenkins Status](https://github.com/Pyomo/jenkins-status/blob/main/pyomo_main.svg)](https://pyomo-jenkins.sandia.gov/) [![codecov](https://codecov.io/gh/Pyomo/pyomo/branch/main/graph/badge.svg)](https://codecov.io/gh/Pyomo/pyomo) [![Documentation Status](https://readthedocs.org/projects/pyomo/badge/?version=latest)](http://pyomo.readthedocs.org/en/latest/) - [![URL Validation](https://github.com/Pyomo/pyomo/actions/workflows/url_check.yml/badge.svg?branch=main)](https://github.com/Pyomo/pyomo/actions/workflows/url_check.yml) [![Build services](https://github.com/Pyomo/jenkins-status/blob/main/pyomo_services.svg)](https://pyomo-jenkins.sandia.gov/) [![GitHub contributors](https://img.shields.io/github/contributors/pyomo/pyomo.svg)](https://github.com/pyomo/pyomo/graphs/contributors) From c2111c6e4a86deb3832895a896316b7eee9ed5f3 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Thu, 19 Sep 2024 11:39:57 -0600 Subject: [PATCH 2450/3044] Change label --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b6319d3f526..83934153361 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Jenkins Status](https://github.com/Pyomo/jenkins-status/blob/main/pyomo_main.svg)](https://pyomo-jenkins.sandia.gov/) [![codecov](https://codecov.io/gh/Pyomo/pyomo/branch/main/graph/badge.svg)](https://codecov.io/gh/Pyomo/pyomo) [![Documentation Status](https://readthedocs.org/projects/pyomo/badge/?version=latest)](http://pyomo.readthedocs.org/en/latest/) -[![URL Validation](https://github.com/Pyomo/pyomo/actions/workflows/url_check.yml/badge.svg?branch=main)](https://github.com/Pyomo/pyomo/actions/workflows/url_check.yml) +[![URL Validation](https://img.shields.io/github/actions/workflow/status/Pyomo/pyomo/url_check.yml?branch=main&label=doc%20urls)](https://github.com/Pyomo/pyomo/actions/workflows/url_check.yml) [![Build services](https://github.com/Pyomo/jenkins-status/blob/main/pyomo_services.svg)](https://pyomo-jenkins.sandia.gov/) [![GitHub contributors](https://img.shields.io/github/contributors/pyomo/pyomo.svg)](https://github.com/pyomo/pyomo/graphs/contributors) [![Merged PRs](https://img.shields.io/github/issues-pr-closed-raw/pyomo/pyomo.svg?label=merged+PRs)](https://github.com/pyomo/pyomo/pulls?q=is:pr+is:merged) From 42b2674d406e1ff0d59a2d113759a55d9fe17e84 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 20 Sep 2024 03:30:25 -0600 Subject: [PATCH 2451/3044] Remove Octeract from the list of expected NEOS solvers --- pyomo/neos/tests/test_neos.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/neos/tests/test_neos.py b/pyomo/neos/tests/test_neos.py index 681856781be..f14600fdb84 100644 --- a/pyomo/neos/tests/test_neos.py +++ b/pyomo/neos/tests/test_neos.py @@ -79,9 +79,6 @@ def test_doc(self): doc = pyomo.neos.doc dockeys = set(doc.keys()) - # Octeract interface is disabled, see #3321 - amplsolvers.remove('octeract') - self.assertEqual(amplsolvers, dockeys) # gamssolvers = set(v[0].lower() for v in tmp if v[1]=='GAMS') @@ -152,8 +149,9 @@ def test_minto(self): def test_mosek(self): self._run('mosek') - # [16 Jul 24] Octeract is erroring. We will disable the interface + # [16 Jul 24]: Octeract is erroring. We will disable the interface # (and testing) until we have time to resolve #3321 + # [20 Sep 24]: and appears to have been removed from NEOS # # def test_octeract(self): # self._run('octeract') From 24f0c4b15fa7020aa99d297545257e9b6560551f Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 23 Sep 2024 12:00:09 -0400 Subject: [PATCH 2452/3044] Perform final cleanup of master results objects --- pyomo/contrib/pyros/master_problem_methods.py | 229 ++++++++++-------- .../contrib/pyros/pyros_algorithm_methods.py | 10 +- pyomo/contrib/pyros/solve_data.py | 35 ++- pyomo/contrib/pyros/tests/test_master.py | 25 +- pyomo/contrib/pyros/util.py | 62 +---- 5 files changed, 183 insertions(+), 178 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 5616a5fa056..945696dd04c 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -22,11 +22,7 @@ from pyomo.core.base.set_types import NonNegativeIntegers, NonNegativeReals from pyomo.core.expr import identify_variables, value from pyomo.core.util import prod -from pyomo.opt import ( - check_optimal_termination, - SolverResults, - TerminationCondition as tc, -) +from pyomo.opt import TerminationCondition as tc from pyomo.repn.standard_repn import generate_standard_repn from pyomo.contrib.pyros.solve_data import MasterResults @@ -38,7 +34,6 @@ check_time_limit_reached, generate_all_decision_rule_var_data_objects, ObjectiveType, - process_termination_condition_master_problem, pyrosTerminationCondition, TIC_TOC_SOLVE_TIME_ATTR, ) @@ -177,7 +172,6 @@ def construct_master_feasibility_problem(master_data): slack_model = master_data.master_model.clone() - # construct the variable mapping master_data.feasibility_problem_varmap = list( zip( getattr(master_data.master_model, varmap_name), @@ -226,8 +220,6 @@ def construct_master_feasibility_problem(master_data): if var in slack_vars: slack_var_coef_map[var] = repn.linear_coefs[idx] - # use this dict if we elect custom scaling in future - # slack_substitution_map = dict() for slack_var in slack_var_coef_map: # coefficient determines whether the slack # is a +ve or -ve slack @@ -236,24 +228,8 @@ def construct_master_feasibility_problem(master_data): else: con_slack = max(0, -value(pre_slack_con_exprs[con])) - # initialize slack variable, evaluate scaling coefficient slack_var.set_value(con_slack) - # (we will probably want to change scaling later) - # # update expression replacement map for slack scaling - # scaling_coeff = 1 # we may want to change scaling later - # slack_substitution_map[id(slack_var)] = scaling_coeff * slack_var - # slack_substitution_map[id(slack_var)] = slack_var - - # # finally, scale slack(s) - # con.set_value( - # ( - # replace_expressions(con.lower, slack_substitution_map), - # replace_expressions(con.body, slack_substitution_map), - # replace_expressions(con.upper, slack_substitution_map), - # ) - # ) - return slack_model @@ -695,7 +671,78 @@ def log_master_solve_results(master_model, config, results, desc="Optimized"): ) -def solver_call_master(master_data, master_soln): +def process_termination_condition_master_problem(config, results): + """ + Process master problem solve termination condition. + + Parameters + ---------- + config : ConfigDict + PyROS solver options. + results : SolverResults + Solver results. + + Returns + ------- + optimality_acceptable : bool + True if problem was solved to an acceptable optimality target, + False otherwise. + infeasible : bool + True if problem was found to be infeasible, False otherwise. + + Raises + ------ + NotImplementedError + If a particular solver termination is not supported by + PyROS. + """ + locally_acceptable = [tc.optimal, tc.locallyOptimal, tc.globallyOptimal] + globally_acceptable = [tc.optimal, tc.globallyOptimal] + robust_infeasible = [tc.infeasible] + try_backups = [ + tc.feasible, + tc.maxTimeLimit, + tc.maxIterations, + tc.maxEvaluations, + tc.minStepLength, + tc.minFunctionValue, + tc.other, + tc.solverFailure, + tc.internalSolverError, + tc.error, + tc.unbounded, + tc.infeasibleOrUnbounded, + tc.invalidProblem, + tc.intermediateNonInteger, + tc.noSolution, + tc.unknown, + ] + + termination_condition = results.solver.termination_condition + optimality_acceptable = ( + (termination_condition in globally_acceptable) + if config.solve_master_globally + else (termination_condition in locally_acceptable) + ) + infeasible = termination_condition in robust_infeasible + try_backup_solver = termination_condition in try_backups + + unsupported_termination = not ( + optimality_acceptable or try_backup_solver or infeasible + ) + if unsupported_termination: + solve_type = "global" if config.solve_master_globally else "local" + raise NotImplementedError( + f"Processing of termination condition {termination_condition} " + f"for attempt at {solve_type} solution of master problem " + "is currently not supported by PyROS. " + "Please report this issue to the PyROS developers." + ) + + return optimality_acceptable, infeasible + + +def solver_call_master(master_data): """ Invoke subsolver(s) on PyROS master problem, and update the MasterResults object accordingly. @@ -704,13 +751,18 @@ def solver_call_master(master_data, master_soln): ---------- master_data : MasterProblemData Container for current master problem and related data. + + Returns + ------- master_soln : MasterResults - Master problem results object. May be empty or contain - master feasibility problem results. + Master solution results object. """ config = master_data.config master_model = master_data.master_model - solver_term_cond_dict = {} + master_soln = MasterResults( + master_model=master_model, + pyros_termination_condition=None, + ) if config.solve_master_globally: solvers = [config.global_solver] + config.backup_global_solvers @@ -720,7 +772,6 @@ def solver_call_master(master_data, master_soln): solve_mode = "global" if config.solve_master_globally else "local" config.progress_logger.debug("Solving master problem") - nominal_block = master_model.scenarios[0, 0] higher_order_decision_rule_efficiency(master_data) for idx, opt in enumerate(solvers): @@ -743,52 +794,30 @@ def solver_call_master(master_data, master_soln): ), ) - optimal_termination = check_optimal_termination(results) - infeasible = results.solver.termination_condition == tc.infeasible - - if optimal_termination: - master_model.solutions.load_from(results) - - # record master problem termination conditions - # for this particular subsolver - # pyros termination condition is determined later in the - # algorithm - solver_term_cond_dict[str(opt)] = str(results.solver.termination_condition) - master_soln.termination_condition = results.solver.termination_condition - master_soln.pyros_termination_condition = None - (try_backup, _) = master_soln.master_subsolver_results = ( - process_termination_condition_master_problem(config=config, results=results) + master_soln.master_results_list.append(results) + optimality_acceptable, infeasible = ( + process_termination_condition_master_problem( + config=config, + results=results, + ) ) + time_out = check_time_limit_reached(master_data.timing, config) - master_soln.nominal_block = nominal_block - master_soln.results = results - master_soln.master_model = master_model - - # if model was solved successfully, update/record the results - # (nominal block DOF variable and objective values) - if not try_backup and not infeasible: - # debugging: log breakdown of master objective + if optimality_acceptable: + master_model.solutions.load_from(results) log_master_solve_results(master_model, config, results) - - master_soln.nominal_block = master_model.scenarios[0, 0] - master_soln.results = results - master_soln.master_model = master_model - - # if PyROS time limit exceeded, exit loop and return solution - if check_time_limit_reached(master_data.timing, config): - try_backup = False - master_soln.master_subsolver_results = ( - None, - pyrosTerminationCondition.time_out, + if time_out: + master_soln.pyros_termination_condition = ( + pyrosTerminationCondition.time_out + ) + if infeasible: + master_soln.pyros_termination_condition = ( + pyrosTerminationCondition.robust_infeasible ) - master_soln.pyros_termination_condition = pyrosTerminationCondition.time_out - if not try_backup: - if infeasible: - master_soln.pyros_termination_condition = ( - pyrosTerminationCondition.robust_infeasible - ) - return + final_result_established = optimality_acceptable or time_out or infeasible + if final_result_established: + return master_soln # all solvers have failed to return an acceptable status. # we will terminate PyROS with subsolver error status. @@ -829,49 +858,53 @@ def solver_call_master(master_data, master_soln): if master_data.iteration == 0 else "" ) + master_soln.pyros_termination_condition = pyrosTerminationCondition.subsolver_error + subsolver_termination_conditions = [ + res.solver.termination_condition + for res in master_soln.master_results_list + ] config.progress_logger.warning( f"Could not successfully solve master problem of iteration " f"{master_data.iteration}{deterministic_model_qual} with any of the " f"provided subordinate {solve_mode} optimizers. " f"(Termination statuses: " - f"{[term_cond for term_cond in solver_term_cond_dict.values()]}.)" + f"{[term_cond for term_cond in subsolver_termination_conditions]}.)" f"{deterministic_msg}" f"{serialization_msg}" ) + return master_soln + def solve_master(master_data): """ - Solve the master problem - """ - master_soln = MasterResults() + Solve the master problem. - # no master feas problem for iteration 0 + Returns + ------- + master_soln : MasterResults + Master problem solve results. + """ + feasibility_problem_results = None + time_out_after_feasibility = False if master_data.iteration > 0: - results = solve_master_feasibility_problem(master_data) - master_soln.feasibility_problem_results = results - - # if pyros time limit reached, load time out status - # to master results and return to caller - if check_time_limit_reached(master_data.timing, master_data.config): - # load master model - master_soln.master_model = master_data.master_model - master_soln.nominal_block = master_data.master_model.scenarios[0, 0] - - # empty results object, with master solve time of zero - master_soln.results = SolverResults() - setattr(master_soln.results.solver, TIC_TOC_SOLVE_TIME_ATTR, 0) - - # PyROS time out status - master_soln.pyros_termination_condition = pyrosTerminationCondition.time_out - master_soln.master_subsolver_results = ( - None, - pyrosTerminationCondition.time_out, - ) - return master_soln + feasibility_problem_results = solve_master_feasibility_problem(master_data) + time_out_after_feasibility = check_time_limit_reached( + master_data.timing, + master_data.config, + ) - solver_call_master(master_data=master_data, master_soln=master_soln) + if time_out_after_feasibility: + master_soln = MasterResults( + master_model=master_data.master_model, + feasibility_problem_results=feasibility_problem_results, + master_results_list=None, + pyros_termination_condition=pyrosTerminationCondition.time_out, + ) + else: + master_soln = solver_call_master(master_data) + master_soln.feasibility_problem_results = feasibility_problem_results return master_soln diff --git a/pyomo/contrib/pyros/pyros_algorithm_methods.py b/pyomo/contrib/pyros/pyros_algorithm_methods.py index 03e8890a13b..86e5d52935b 100644 --- a/pyomo/contrib/pyros/pyros_algorithm_methods.py +++ b/pyomo/contrib/pyros/pyros_algorithm_methods.py @@ -157,19 +157,11 @@ def ROSolver_iterative_solve(model_data): ) IterationLogRecord.log_header(config.progress_logger.info) k = 0 - master_statuses = [] while config.max_iter == -1 or k < config.max_iter: master_data.iteration = k - - # === Solve Master Problem config.progress_logger.debug(f"PyROS working on iteration {k}...") - master_soln = master_data.solve_master() - master_statuses.append(master_soln.results.solver.termination_condition) - master_soln.master_problem_subsolver_statuses = master_statuses - - # check master solve status - # to determine whether to terminate here + master_soln = master_data.solve_master() master_termination_not_acceptable = master_soln.pyros_termination_condition in { pyrosTerminationCondition.robust_infeasible, pyrosTerminationCondition.time_out, diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index 1b59597d14a..4b378f54754 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -83,13 +83,42 @@ def __str__(self): return "\n".join(lines) -class MasterResults(object): +class MasterResults: """ - Container for master problem solve results. + Result of solving the master problem in a single PyROS iteration. - TODO: Formalize this class. + Attributes + ---------- + master_model : ConcreteModel + Master model. + feasibility_problem_results : SolverResults + Feasibility problem subsolver results. + master_results_list : list of SolverResults + List of subsolver results for the master problem. + pyros_termination_condition : None or pyrosTerminationCondition + PyROS termination status established via solution of + the master problem. + If `None`, then no termination status has been established. """ + def __init__( + self, + master_model=None, + feasibility_problem_results=None, + master_results_list=None, + pyros_termination_condition=None, + ): + """Initialize self (see class docstring). + + """ + self.master_model = master_model + self.feasibility_problem_results = feasibility_problem_results + if master_results_list is None: + self.master_results_list = [] + else: + self.master_results_list = list(master_results_list) + self.pyros_termination_condition = pyros_termination_condition + class SeparationSolveCallResults: """ diff --git a/pyomo/contrib/pyros/tests/test_master.py b/pyomo/contrib/pyros/tests/test_master.py index cd44b9eedf9..8ca7a7b0b1d 100644 --- a/pyomo/contrib/pyros/tests/test_master.py +++ b/pyomo/contrib/pyros/tests/test_master.py @@ -572,8 +572,12 @@ def test_solve_master(self): master_data = MasterProblemData(model_data) with time_code(master_data.timing, "main", is_main_timer=True): master_soln = master_data.solve_master() + self.assertEqual(len(master_soln.master_results_list), 1) + self.assertIsNone(master_soln.feasibility_problem_results) + self.assertIsNone(master_soln.pyros_termination_condition) + self.assertIs(master_soln.master_model, master_data.master_model) self.assertEqual( - master_soln.termination_condition, + master_soln.master_results_list[0].solver.termination_condition, TerminationCondition.optimal, msg=( "Could not solve simple master problem with solve_master " @@ -604,8 +608,11 @@ def test_solve_master_timeout_on_master(self): with time_code(master_data.timing, "main", is_main_timer=True): time.sleep(1) master_soln = master_data.solve_master() + self.assertIsNone(master_soln.feasibility_problem_results) + self.assertEqual(master_soln.master_model, master_data.master_model) + self.assertEqual(len(master_soln.master_results_list), 1) self.assertEqual( - master_soln.termination_condition, + master_soln.master_results_list[0].solver.termination_condition, TerminationCondition.optimal, msg=( "Could not solve simple master problem with solve_master " @@ -613,8 +620,8 @@ def test_solve_master_timeout_on_master(self): ), ) self.assertEqual( - master_soln.master_subsolver_results, - (None, pyrosTerminationCondition.time_out), + master_soln.pyros_termination_condition, + pyrosTerminationCondition.time_out, ) @unittest.skipUnless(baron_available, "Global NLP solver is not available") @@ -648,14 +655,13 @@ def test_solve_master_timeout_on_master_feasibility(self): with time_code(master_data.timing, "main", is_main_timer=True): time.sleep(1) master_soln = master_data.solve_master() + self.assertIsNotNone(master_soln.feasibility_problem_results) + self.assertFalse(master_soln.master_results_list) + self.assertIs(master_soln.master_model, master_data.master_model) self.assertEqual( master_soln.pyros_termination_condition, pyrosTerminationCondition.time_out, ) - self.assertEqual( - master_soln.master_subsolver_results, - (None, pyrosTerminationCondition.time_out), - ) class TestPolishDRVars(unittest.TestCase): @@ -693,7 +699,8 @@ def test_polish_dr_vars(self): with time_code(master_data.timing, "main", is_main_timer=True): master_soln = master_data.solve_master() self.assertEqual( - master_soln.termination_condition, TerminationCondition.optimal + master_soln.master_results_list[0].solver.termination_condition, + TerminationCondition.optimal, ) results, success = master_data.solve_dr_polishing() diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index fb3e2e5ed7e..093e7392adb 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -51,7 +51,7 @@ replace_expressions, ) from pyomo.core.util import prod -from pyomo.opt import SolverFactory, TerminationCondition as tc +from pyomo.opt import SolverFactory from pyomo.repn.parameterized_quadratic import ParameterizedQuadraticRepnVisitor import pyomo.repn.plugins.nl_writer as pyomo_nl_writer import pyomo.repn.ampl as pyomo_ampl_repn @@ -2617,7 +2617,7 @@ def load_final_solution(model_data, master_soln, original_user_var_partitioning) Parameters ---------- - master_soln : master solution object + master_soln : MasterResults Master solution object, containing the master model. original_user_var_partitioning : VariablePartitioning User partitioning of the variables of the original @@ -2625,7 +2625,7 @@ def load_final_solution(model_data, master_soln, original_user_var_partitioning) """ config = model_data.config if config.objective_focus == ObjectiveType.nominal: - soln_master_blk = master_soln.nominal_block + soln_master_blk = master_soln.master_model.scenarios[0, 0] elif config.objective_focus == ObjectiveType.worst_case: soln_master_blk = max( master_soln.master_model.scenarios.values(), @@ -2646,62 +2646,6 @@ def load_final_solution(model_data, master_soln, original_user_var_partitioning) orig_var.set_value(master_blk_var.value, skip_validation=True) -def process_termination_condition_master_problem(config, results): - ''' - :param config: pyros config - :param results: solver results object - :return: tuple (try_backups (True/False) - pyros_return_code (default NONE or robust_infeasible or subsolver_error)) - ''' - locally_acceptable = [tc.optimal, tc.locallyOptimal, tc.globallyOptimal] - globally_acceptable = [tc.optimal, tc.globallyOptimal] - robust_infeasible = [tc.infeasible] - try_backups = [ - tc.feasible, - tc.maxTimeLimit, - tc.maxIterations, - tc.maxEvaluations, - tc.minStepLength, - tc.minFunctionValue, - tc.other, - tc.solverFailure, - tc.internalSolverError, - tc.error, - tc.unbounded, - tc.infeasibleOrUnbounded, - tc.invalidProblem, - tc.intermediateNonInteger, - tc.noSolution, - tc.unknown, - ] - - termination_condition = results.solver.termination_condition - if config.solve_master_globally == False: - if termination_condition in locally_acceptable: - return (False, None) - elif termination_condition in robust_infeasible: - return (False, pyrosTerminationCondition.robust_infeasible) - elif termination_condition in try_backups: - return (True, None) - else: - raise NotImplementedError( - "This solver return termination condition (%s) " - "is currently not supported by PyROS." % termination_condition - ) - else: - if termination_condition in globally_acceptable: - return (False, None) - elif termination_condition in robust_infeasible: - return (False, pyrosTerminationCondition.robust_infeasible) - elif termination_condition in try_backups: - return (True, None) - else: - raise NotImplementedError( - "This solver return termination condition (%s) " - "is currently not supported by PyROS." % termination_condition - ) - - def call_solver(model, solver, config, timing_obj, timer_name, err_msg): """ Solve a model with a given optimizer, keeping track of From 06879776075cd1d7f69ef6c952f50a72105a3c41 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 23 Sep 2024 12:00:53 -0400 Subject: [PATCH 2453/3044] Use SCIP for failing tests --- pyomo/contrib/pyros/tests/test_grcs.py | 63 ++++++++++++++------------ 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index b7c98068687..776531de0e6 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -248,7 +248,8 @@ class TestPyROSSolveAxisAlignedEllipsoidalSet(unittest.TestCase): """ @unittest.skipUnless( - baron_license_is_valid, "Global NLP solver is not available and licensed." + scip_available and scip_license_is_valid, + "SCIP is not available and licensed", ) def test_two_stg_mod_with_axis_aligned_set(self): """ @@ -276,8 +277,8 @@ def test_two_stg_mod_with_axis_aligned_set(self): pyros_solver = SolverFactory("pyros") # Define subsolvers utilized in the algorithm - local_subsolver = SolverFactory('baron') - global_subsolver = SolverFactory("baron") + local_subsolver = SolverFactory("scip") + global_subsolver = SolverFactory("scip") # Call the PyROS solver results = pyros_solver.solve( @@ -1189,7 +1190,7 @@ def test_discrete_separation_invalid_value_error(self): pyros_solver = SolverFactory("pyros") with LoggingIntercept(level=logging.ERROR) as LOG: - with self.assertRaises(InvalidValueError): + with self.assertRaises(ApplicationError): pyros_solver.solve( model=m, first_stage_variables=[m.x1], @@ -1454,10 +1455,8 @@ def test_discrete_separation(self): ) @unittest.skipUnless( - baron_license_is_valid, "Global NLP solver is not available and licensed." - ) - @unittest.skipUnless( - baron_version == (23, 1, 5), "Test runs >90 minutes with Baron 22.9.30" + scip_available and scip_license_is_valid, + "SCIP is not available and licensed.", ) def test_higher_order_decision_rules(self): m = ConcreteModel() @@ -1478,8 +1477,8 @@ def test_higher_order_decision_rules(self): pyros_solver = SolverFactory("pyros") # Define subsolvers utilized in the algorithm - local_subsolver = SolverFactory('baron') - global_subsolver = SolverFactory("baron") + local_subsolver = SolverFactory("scip") + global_subsolver = SolverFactory("scip") # Call the PyROS solver results = pyros_solver.solve( @@ -1643,7 +1642,10 @@ def test_coeff_matching_solver_insensitive(self): ), ) - @unittest.skipUnless(scip_available, "NLP solver is not available.") + @unittest.skipUnless( + scip_available and scip_license_is_valid, + "SCIP is not available and licensed.", + ) def test_coefficient_matching_partitioning_insensitive(self): """ Check that result for instance with constraint subject to @@ -1653,8 +1655,7 @@ def test_coefficient_matching_partitioning_insensitive(self): """ m = self.create_mitsos_4_3() - # instantiate BARON subsolver and PyROS solver - baron = SolverFactory("scip") + global_solver = SolverFactory("scip") pyros_solver = SolverFactory("pyros") # solve with PyROS @@ -1669,8 +1670,8 @@ def test_coefficient_matching_partitioning_insensitive(self): second_stage_variables=partitioning["ssv"], uncertain_params=[m.u], uncertainty_set=BoxSet(bounds=[[0, 1]]), - local_solver=baron, - global_solver=baron, + local_solver=global_solver, + global_solver=global_solver, objective_focus=ObjectiveType.worst_case, solve_master_globally=True, bypass_local_separation=True, @@ -1699,6 +1700,7 @@ def test_coefficient_matching_partitioning_insensitive(self): ), ) + @unittest.skipUnless(baron_available, "BARON is not available.") def test_coefficient_matching_robust_infeasible_proof_in_pyros(self): # Write the deterministic Pyomo model m = ConcreteModel() @@ -1722,7 +1724,7 @@ def test_coefficient_matching_robust_infeasible_proof_in_pyros(self): pyros_solver = SolverFactory("pyros") # Define subsolvers utilized in the algorithm - local_subsolver = SolverFactory('baron') + local_subsolver = SolverFactory("baron") global_subsolver = SolverFactory("baron") # Call the PyROS solver @@ -1747,8 +1749,10 @@ def test_coefficient_matching_robust_infeasible_proof_in_pyros(self): msg="Robust infeasible problem not identified via coefficient matching.", ) - @unittest.skipIf(baron_version == (24, 1, 5), "Test known to fail for BARON 24.1.5") - @unittest.skipUnless(baron_license_is_valid, "BARON solver not licensed.") + @unittest.skipUnless( + scip_available and scip_license_is_valid, + "SCIP not available and licensed.", + ) def test_coefficient_matching_nonlinear_expr(self): """ Test behavior of PyROS solver for model with @@ -1777,8 +1781,8 @@ def test_coefficient_matching_nonlinear_expr(self): pyros_solver = SolverFactory("pyros") # Define subsolvers utilized in the algorithm - local_subsolver = SolverFactory("baron") - global_subsolver = SolverFactory("baron") + local_subsolver = SolverFactory("scip") + global_subsolver = SolverFactory("scip") # Call the PyROS solver with LoggingIntercept(module="pyomo.contrib.pyros", level=logging.DEBUG) as LOG: @@ -2056,16 +2060,14 @@ def test_multiple_objs(self): ) -class testMasterFeasibilityUnitConsistency(unittest.TestCase): +class TestMasterFeasibilityUnitConsistency(unittest.TestCase): """ Test cases for models with unit-laden model components. """ @unittest.skipUnless( - baron_license_is_valid, "Global NLP solver is not available and licensed." - ) - @unittest.skipUnless( - baron_version < (23, 1, 5), "Test known to fail beginning with Baron 23.1.5" + scip_available and scip_license_is_valid, + "SCIP is not available and licensed.", ) def test_two_stg_mod_with_axis_aligned_set(self): """ @@ -2095,8 +2097,8 @@ def test_two_stg_mod_with_axis_aligned_set(self): pyros_solver = SolverFactory("pyros") # Define subsolvers utilized in the algorithm - local_subsolver = SolverFactory('baron') - global_subsolver = SolverFactory("baron") + local_subsolver = SolverFactory("scip") + global_subsolver = SolverFactory("scip") # Call the PyROS solver # note: second-stage variable and uncertain params have units @@ -2258,7 +2260,8 @@ def test_pyros_gams_ipopt(self): ) @unittest.skipUnless( - baron_license_is_valid, "Global NLP solver is not available and licensed." + scip_available and scip_license_is_valid, + "SCIP is not available and licensed.", ) def test_two_stg_mod_with_intersection_set(self): """ @@ -2287,8 +2290,8 @@ def test_two_stg_mod_with_intersection_set(self): pyros_solver = SolverFactory("pyros") # Define subsolvers utilized in the algorithm - local_subsolver = SolverFactory('baron') - global_subsolver = SolverFactory("baron") + local_subsolver = SolverFactory("scip") + global_subsolver = SolverFactory("scip") # Call the PyROS solver results = pyros_solver.solve( From 2858f232b2df9daf26d6a05761f4121da00a9ae0 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 23 Sep 2024 12:03:09 -0400 Subject: [PATCH 2454/3044] Apply black --- pyomo/contrib/pyros/master_problem_methods.py | 18 +++++------------- pyomo/contrib/pyros/solve_data.py | 4 +--- pyomo/contrib/pyros/tests/test_grcs.py | 18 ++++++------------ 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/pyomo/contrib/pyros/master_problem_methods.py b/pyomo/contrib/pyros/master_problem_methods.py index 945696dd04c..f5653217326 100644 --- a/pyomo/contrib/pyros/master_problem_methods.py +++ b/pyomo/contrib/pyros/master_problem_methods.py @@ -760,8 +760,7 @@ def solver_call_master(master_data): config = master_data.config master_model = master_data.master_model master_soln = MasterResults( - master_model=master_model, - pyros_termination_condition=None, + master_model=master_model, pyros_termination_condition=None ) if config.solve_master_globally: @@ -796,10 +795,7 @@ def solver_call_master(master_data): master_soln.master_results_list.append(results) optimality_acceptable, infeasible = ( - process_termination_condition_master_problem( - config=config, - results=results, - ) + process_termination_condition_master_problem(config=config, results=results) ) time_out = check_time_limit_reached(master_data.timing, config) @@ -807,9 +803,7 @@ def solver_call_master(master_data): master_model.solutions.load_from(results) log_master_solve_results(master_model, config, results) if time_out: - master_soln.pyros_termination_condition = ( - pyrosTerminationCondition.time_out - ) + master_soln.pyros_termination_condition = pyrosTerminationCondition.time_out if infeasible: master_soln.pyros_termination_condition = ( pyrosTerminationCondition.robust_infeasible @@ -861,8 +855,7 @@ def solver_call_master(master_data): master_soln.pyros_termination_condition = pyrosTerminationCondition.subsolver_error subsolver_termination_conditions = [ - res.solver.termination_condition - for res in master_soln.master_results_list + res.solver.termination_condition for res in master_soln.master_results_list ] config.progress_logger.warning( f"Could not successfully solve master problem of iteration " @@ -891,8 +884,7 @@ def solve_master(master_data): if master_data.iteration > 0: feasibility_problem_results = solve_master_feasibility_problem(master_data) time_out_after_feasibility = check_time_limit_reached( - master_data.timing, - master_data.config, + master_data.timing, master_data.config ) if time_out_after_feasibility: diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index 4b378f54754..3a017633ba6 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -108,9 +108,7 @@ def __init__( master_results_list=None, pyros_termination_condition=None, ): - """Initialize self (see class docstring). - - """ + """Initialize self (see class docstring).""" self.master_model = master_model self.feasibility_problem_results = feasibility_problem_results if master_results_list is None: diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 776531de0e6..023233a0156 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -248,8 +248,7 @@ class TestPyROSSolveAxisAlignedEllipsoidalSet(unittest.TestCase): """ @unittest.skipUnless( - scip_available and scip_license_is_valid, - "SCIP is not available and licensed", + scip_available and scip_license_is_valid, "SCIP is not available and licensed" ) def test_two_stg_mod_with_axis_aligned_set(self): """ @@ -1455,8 +1454,7 @@ def test_discrete_separation(self): ) @unittest.skipUnless( - scip_available and scip_license_is_valid, - "SCIP is not available and licensed.", + scip_available and scip_license_is_valid, "SCIP is not available and licensed." ) def test_higher_order_decision_rules(self): m = ConcreteModel() @@ -1643,8 +1641,7 @@ def test_coeff_matching_solver_insensitive(self): ) @unittest.skipUnless( - scip_available and scip_license_is_valid, - "SCIP is not available and licensed.", + scip_available and scip_license_is_valid, "SCIP is not available and licensed." ) def test_coefficient_matching_partitioning_insensitive(self): """ @@ -1750,8 +1747,7 @@ def test_coefficient_matching_robust_infeasible_proof_in_pyros(self): ) @unittest.skipUnless( - scip_available and scip_license_is_valid, - "SCIP not available and licensed.", + scip_available and scip_license_is_valid, "SCIP not available and licensed." ) def test_coefficient_matching_nonlinear_expr(self): """ @@ -2066,8 +2062,7 @@ class TestMasterFeasibilityUnitConsistency(unittest.TestCase): """ @unittest.skipUnless( - scip_available and scip_license_is_valid, - "SCIP is not available and licensed.", + scip_available and scip_license_is_valid, "SCIP is not available and licensed." ) def test_two_stg_mod_with_axis_aligned_set(self): """ @@ -2260,8 +2255,7 @@ def test_pyros_gams_ipopt(self): ) @unittest.skipUnless( - scip_available and scip_license_is_valid, - "SCIP is not available and licensed.", + scip_available and scip_license_is_valid, "SCIP is not available and licensed." ) def test_two_stg_mod_with_intersection_set(self): """ From fe42b4db07dfbfb6890f9ade79ed14ea0ed2343f Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 23 Sep 2024 12:16:15 -0400 Subject: [PATCH 2455/3044] Restore `InvalidValueError` exception type check --- pyomo/contrib/pyros/tests/test_grcs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/pyros/tests/test_grcs.py b/pyomo/contrib/pyros/tests/test_grcs.py index 023233a0156..00afef4b231 100644 --- a/pyomo/contrib/pyros/tests/test_grcs.py +++ b/pyomo/contrib/pyros/tests/test_grcs.py @@ -1189,7 +1189,7 @@ def test_discrete_separation_invalid_value_error(self): pyros_solver = SolverFactory("pyros") with LoggingIntercept(level=logging.ERROR) as LOG: - with self.assertRaises(ApplicationError): + with self.assertRaises(InvalidValueError): pyros_solver.solve( model=m, first_stage_variables=[m.x1], From add2bef2ba4af366719de963df49e16dc68ccf56 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 23 Sep 2024 15:04:06 -0600 Subject: [PATCH 2456/3044] Removing redundant pynumero, common API documentation --- .../explanation/solvers/pynumero/api.rst | 14 -- .../pynumero/pynumero.interfaces.ampl_nlp.rst | 8 - .../pynumero/pynumero.interfaces.asl_nlp.rst | 8 - .../pynumero.interfaces.extended_nlp.rst | 8 - ...ero.interfaces.external_grey_box_model.rst | 8 - .../pynumero/pynumero.interfaces.nlp.rst | 8 - .../pynumero.interfaces.projected_nlp.rst | 8 - ...pynumero.interfaces.pyomo_grey_box_nlp.rst | 8 - .../pynumero.interfaces.pyomo_nlp.rst | 8 - .../solvers/pynumero/pynumero.interfaces.rst | 16 -- .../solvers/pynumero/pynumero.linalg.base.rst | 26 --- .../solvers/pynumero/pynumero.linalg.ma27.rst | 8 - .../solvers/pynumero/pynumero.linalg.ma57.rst | 8 - .../pynumero/pynumero.linalg.mumps.rst | 8 - .../solvers/pynumero/pynumero.linalg.rst | 14 -- .../pynumero/pynumero.linalg.scipy.rst | 14 -- .../pynumero/pynumero.sparse.block_vector.rst | 154 ------------------ .../solvers/pynumero/pynumero.sparse.rst | 9 - .../reference/topical/common/config.rst | 85 ---------- .../reference/topical/common/dependencies.rst | 7 - .../reference/topical/common/deprecation.rst | 6 - .../reference/topical/common/enums.rst | 7 - .../reference/topical/common/errors.rst | 6 - .../reference/topical/common/fileutils.rst | 6 - .../reference/topical/common/formatting.rst | 6 - .../reference/topical/common/index.rst | 19 --- .../reference/topical/common/tempfiles.rst | 7 - .../reference/topical/common/timing.rst | 7 - 28 files changed, 491 deletions(-) delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/api.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.ampl_nlp.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.asl_nlp.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.extended_nlp.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.external_grey_box_model.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.nlp.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.projected_nlp.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.pyomo_nlp.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.base.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.ma27.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.ma57.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.mumps.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.scipy.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.sparse.block_vector.rst delete mode 100644 doc/OnlineDocs/explanation/solvers/pynumero/pynumero.sparse.rst delete mode 100644 doc/OnlineDocs/reference/topical/common/config.rst delete mode 100644 doc/OnlineDocs/reference/topical/common/dependencies.rst delete mode 100644 doc/OnlineDocs/reference/topical/common/deprecation.rst delete mode 100644 doc/OnlineDocs/reference/topical/common/enums.rst delete mode 100644 doc/OnlineDocs/reference/topical/common/errors.rst delete mode 100644 doc/OnlineDocs/reference/topical/common/fileutils.rst delete mode 100644 doc/OnlineDocs/reference/topical/common/formatting.rst delete mode 100644 doc/OnlineDocs/reference/topical/common/index.rst delete mode 100644 doc/OnlineDocs/reference/topical/common/tempfiles.rst delete mode 100644 doc/OnlineDocs/reference/topical/common/timing.rst diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/api.rst b/doc/OnlineDocs/explanation/solvers/pynumero/api.rst deleted file mode 100644 index 3d1ac8a189e..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/api.rst +++ /dev/null @@ -1,14 +0,0 @@ -.. _pynumero_api: - -PyNumero API -============ - -.. automodule:: pyomo.contrib.pynumero - :members: - :undoc-members: - -.. toctree:: - - pynumero.sparse - pynumero.interfaces - pynumero.linalg diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.ampl_nlp.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.ampl_nlp.rst deleted file mode 100644 index 37dd5852351..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.ampl_nlp.rst +++ /dev/null @@ -1,8 +0,0 @@ -AMPL NLP Interface -================== - -.. autoclass:: pyomo.contrib.pynumero.interfaces.ampl_nlp.AmplNLP - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.asl_nlp.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.asl_nlp.rst deleted file mode 100644 index 2537bd52fdb..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.asl_nlp.rst +++ /dev/null @@ -1,8 +0,0 @@ -ASL NLP Interface -================= - -.. autoclass:: pyomo.contrib.pynumero.interfaces.ampl_nlp.AslNLP - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.extended_nlp.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.extended_nlp.rst deleted file mode 100644 index 75528ac4b45..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.extended_nlp.rst +++ /dev/null @@ -1,8 +0,0 @@ -Extended NLP Interface -====================== - -.. autoclass:: pyomo.contrib.pynumero.interfaces.nlp.ExtendedNLP - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.external_grey_box_model.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.external_grey_box_model.rst deleted file mode 100644 index 10187b4156e..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.external_grey_box_model.rst +++ /dev/null @@ -1,8 +0,0 @@ -External Grey Box Model -======================= - -.. autoclass:: pyomo.contrib.pynumero.interfaces.external_grey_box.ExternalGreyBoxModel - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.nlp.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.nlp.rst deleted file mode 100644 index d8532873c22..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.nlp.rst +++ /dev/null @@ -1,8 +0,0 @@ -NLP Interface -============= - -.. autoclass:: pyomo.contrib.pynumero.interfaces.nlp.NLP - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.projected_nlp.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.projected_nlp.rst deleted file mode 100644 index b9c6941bd93..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.projected_nlp.rst +++ /dev/null @@ -1,8 +0,0 @@ -Projected NLP Interface -======================= - -.. autoclass:: pyomo.contrib.pynumero.interfaces.nlp_projections.ProjectedNLP - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst deleted file mode 100644 index c7200038f5e..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst +++ /dev/null @@ -1,8 +0,0 @@ -Pyomo Grey Box NLP Interface -============================ - -.. autoclass:: pyomo.contrib.pynumero.interfaces.pyomo_nlp.PyomoGreyBoxNLP - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.pyomo_nlp.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.pyomo_nlp.rst deleted file mode 100644 index e52ce33c2d9..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.pyomo_nlp.rst +++ /dev/null @@ -1,8 +0,0 @@ -Pyomo NLP Interface -=================== - -.. autoclass:: pyomo.contrib.pynumero.interfaces.pyomo_nlp.PyomoNLP - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.rst deleted file mode 100644 index ec0b94960f6..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.interfaces.rst +++ /dev/null @@ -1,16 +0,0 @@ -PyNumero NLP Interfaces -======================= - -.. automodule:: pyomo.contrib.pynumero.interfaces - :members: - -.. toctree:: - - pynumero.interfaces.nlp - pynumero.interfaces.extended_nlp - pynumero.interfaces.asl_nlp - pynumero.interfaces.ampl_nlp - pynumero.interfaces.pyomo_nlp - pynumero.interfaces.projected_nlp - pynumero.interfaces.external_grey_box_model - pynumero.interfaces.pyomo_grey_box_nlp diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.base.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.base.rst deleted file mode 100644 index 0a94f87c6be..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.base.rst +++ /dev/null @@ -1,26 +0,0 @@ -Linear Solver Base Classes -========================== - -.. autoclass:: pyomo.contrib.pynumero.linalg.base.LinearSolverStatus - :members: - :inherited-members: - :show-inheritance: - :undoc-members: - -.. autoclass:: pyomo.contrib.pynumero.linalg.base.LinearSolverResults - :members: - :inherited-members: - :show-inheritance: - :undoc-members: - -.. autoclass:: pyomo.contrib.pynumero.linalg.base.LinearSolverInterface - :members: - :inherited-members: - :show-inheritance: - :undoc-members: - -.. autoclass:: pyomo.contrib.pynumero.linalg.base.DirectLinearSolverInterface - :members: - :inherited-members: - :show-inheritance: - :undoc-members: diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.ma27.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.ma27.rst deleted file mode 100644 index f1d2eed3ed0..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.ma27.rst +++ /dev/null @@ -1,8 +0,0 @@ -HSL MA27 -======== - -.. autoclass:: pyomo.contrib.pynumero.linalg.ma27_interface.MA27 - :members: - :inherited-members: - :show-inheritance: - :undoc-members: diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.ma57.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.ma57.rst deleted file mode 100644 index c97f193b5f8..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.ma57.rst +++ /dev/null @@ -1,8 +0,0 @@ -HSL MA57 -======== - -.. autoclass:: pyomo.contrib.pynumero.linalg.ma57_interface.MA57 - :members: - :inherited-members: - :show-inheritance: - :undoc-members: diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.mumps.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.mumps.rst deleted file mode 100644 index 1fd5998dd4d..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.mumps.rst +++ /dev/null @@ -1,8 +0,0 @@ -MUMPS -===== - -.. autoclass:: pyomo.contrib.pynumero.linalg.mumps_interface.MumpsCentralizedAssembledLinearSolver - :members: - :inherited-members: - :show-inheritance: - :undoc-members: diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.rst deleted file mode 100644 index 70b091becbd..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.rst +++ /dev/null @@ -1,14 +0,0 @@ -PyNumero Linear Solver Interfaces -================================= - -.. automodule:: pyomo.contrib.pynumero.linalg - :members: - -.. toctree:: - - pynumero.linalg.base - pynumero.linalg.ma27 - pynumero.linalg.ma57 - pynumero.linalg.mumps - pynumero.linalg.scipy - diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.scipy.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.scipy.rst deleted file mode 100644 index 7e0a1d0b865..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.linalg.scipy.rst +++ /dev/null @@ -1,14 +0,0 @@ -Scipy -===== - -.. autoclass:: pyomo.contrib.pynumero.linalg.scipy_interface.ScipyLU - :members: - :inherited-members: - :show-inheritance: - :undoc-members: - -.. autoclass:: pyomo.contrib.pynumero.linalg.scipy_interface.ScipyIterative - :members: - :inherited-members: - :show-inheritance: - :undoc-members: diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.sparse.block_vector.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.sparse.block_vector.rst deleted file mode 100644 index c17d3d1df86..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.sparse.block_vector.rst +++ /dev/null @@ -1,154 +0,0 @@ -BlockVector -=========== - -Methods specific to :py:class:`pyomo.contrib.pynumero.sparse.block_vector.BlockVector`: - - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_block` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.block_sizes` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block_size` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.is_block_defined` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyfrom` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyto` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copy_structure` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_blocks` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.pprint` - -Attributes specific to :py:class:`pyomo.contrib.pynumero.sparse.block_vector.BlockVector`: - - * :py:attr:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.nblocks` - * :py:attr:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.bshape` - * :py:attr:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.has_none` - - -NumPy compatible methods: - - * `numpy.ndarray.dot() `_ - * `numpy.ndarray.sum() `_ - * `numpy.ndarray.all() `_ - * `numpy.ndarray.any() `_ - * `numpy.ndarray.max() `_ - * `numpy.ndarray.astype() `_ - * `numpy.ndarray.clip() `_ - * `numpy.ndarray.compress() `_ - * `numpy.ndarray.conj() `_ - * `numpy.ndarray.conjugate() `_ - * `numpy.ndarray.nonzero() `_ - * `numpy.ndarray.ptp() `_ - * `numpy.ndarray.round() `_ - * `numpy.ndarray.std() `_ - * `numpy.ndarray.var() `_ - * `numpy.ndarray.tofile() `_ - * `numpy.ndarray.min() `_ - * `numpy.ndarray.mean() `_ - * `numpy.ndarray.prod() `_ - * `numpy.ndarray.fill() `_ - * `numpy.ndarray.tolist() `_ - * `numpy.ndarray.flatten() `_ - * `numpy.ndarray.ravel() `_ - * `numpy.ndarray.argmax() `_ - * `numpy.ndarray.argmin() `_ - * `numpy.ndarray.cumprod() `_ - * `numpy.ndarray.cumsum() `_ - * `numpy.ndarray.copy() `_ - -For example, - -.. code-block:: python - - >>> import numpy as np - >>> from pyomo.contrib.pynumero.sparse import BlockVector - >>> v = BlockVector(2) - >>> v.set_block(0, np.random.normal(size=100)) - >>> v.set_block(1, np.random.normal(size=30)) - >>> avg = v.mean() - -NumPy compatible functions: - - * `numpy.log10() `_ - * `numpy.sin() `_ - * `numpy.cos() `_ - * `numpy.exp() `_ - * `numpy.ceil() `_ - * `numpy.floor() `_ - * `numpy.tan() `_ - * `numpy.arctan() `_ - * `numpy.arcsin() `_ - * `numpy.arccos() `_ - * `numpy.sinh() `_ - * `numpy.cosh() `_ - * `numpy.abs() `_ - * `numpy.tanh() `_ - * `numpy.arccosh() `_ - * `numpy.arcsinh() `_ - * `numpy.arctanh() `_ - * `numpy.fabs() `_ - * `numpy.sqrt() `_ - * `numpy.log() `_ - * `numpy.log2() `_ - * `numpy.absolute() `_ - * `numpy.isfinite() `_ - * `numpy.isinf() `_ - * `numpy.isnan() `_ - * `numpy.log1p() `_ - * `numpy.logical_not() `_ - * `numpy.expm1() `_ - * `numpy.exp2() `_ - * `numpy.sign() `_ - * `numpy.rint() `_ - * `numpy.square() `_ - * `numpy.positive() `_ - * `numpy.negative() `_ - * `numpy.rad2deg() `_ - * `numpy.deg2rad() `_ - * `numpy.conjugate() `_ - * `numpy.reciprocal() `_ - * `numpy.signbit() `_ - * `numpy.add() `_ - * `numpy.multiply() `_ - * `numpy.divide() `_ - * `numpy.subtract() `_ - * `numpy.greater() `_ - * `numpy.greater_equal() `_ - * `numpy.less() `_ - * `numpy.less_equal() `_ - * `numpy.not_equal() `_ - * `numpy.maximum() `_ - * `numpy.minimum() `_ - * `numpy.fmax() `_ - * `numpy.fmin() `_ - * `numpy.equal() `_ - * `numpy.logical_and() `_ - * `numpy.logical_or() `_ - * `numpy.logical_xor() `_ - * `numpy.logaddexp() `_ - * `numpy.logaddexp2() `_ - * `numpy.remainder() `_ - * `numpy.heaviside() `_ - * `numpy.hypot() `_ - -For example, - -.. code-block:: python - - >>> import numpy as np - >>> from pyomo.contrib.pynumero.sparse import BlockVector - >>> v = BlockVector(2) - >>> v.set_block(0, np.random.normal(size=100)) - >>> v.set_block(1, np.random.normal(size=30)) - >>> inf_norm = np.max(np.abs(v)) - -.. autoclass:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_block -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.block_sizes -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block_size -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.is_block_defined -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyfrom -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyto -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copy_structure -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_blocks -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.pprint -.. autoproperty:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.nblocks -.. autoproperty:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.bshape -.. autoproperty:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.has_none diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.sparse.rst b/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.sparse.rst deleted file mode 100644 index 6d903abb5a4..00000000000 --- a/doc/OnlineDocs/explanation/solvers/pynumero/pynumero.sparse.rst +++ /dev/null @@ -1,9 +0,0 @@ -PyNumero Block Linear Algebra -============================= - -.. automodule:: pyomo.contrib.pynumero.sparse - :members: - -.. toctree:: - - pynumero.sparse.block_vector diff --git a/doc/OnlineDocs/reference/topical/common/config.rst b/doc/OnlineDocs/reference/topical/common/config.rst deleted file mode 100644 index c5dc607977a..00000000000 --- a/doc/OnlineDocs/reference/topical/common/config.rst +++ /dev/null @@ -1,85 +0,0 @@ -pyomo.common.config -=================== - -.. currentmodule:: pyomo.common.config - -Core classes -~~~~~~~~~~~~ - -.. autosummary:: - - ConfigDict - ConfigList - ConfigValue - -Utilities -~~~~~~~~~ - -.. autosummary:: - - document_kwargs_from_configdict - - -Domain validators -~~~~~~~~~~~~~~~~~ - -.. autosummary:: - - Bool - Integer - PositiveInt - NegativeInt - NonNegativeInt - NonPositiveInt - PositiveFloat - NegativeFloat - NonPositiveFloat - NonNegativeFloat - In - IsInstance - InEnum - ListOf - Module - Path - PathList - DynamicImplicitDomain - -.. autoclass:: ConfigBase - :members: - :undoc-members: - -.. autoclass:: ConfigDict - :show-inheritance: - :members: - :undoc-members: - -.. autoclass:: ConfigList - :show-inheritance: - :members: - :undoc-members: - -.. autoclass:: ConfigValue - :show-inheritance: - :members: - :undoc-members: - -.. autodecorator:: document_kwargs_from_configdict - -.. autofunction:: Bool -.. autofunction:: Integer -.. autofunction:: PositiveInt -.. autofunction:: NegativeInt -.. autofunction:: NonNegativeInt -.. autofunction:: NonPositiveInt -.. autofunction:: PositiveFloat -.. autofunction:: NegativeFloat -.. autofunction:: NonPositiveFloat -.. autofunction:: NonNegativeFloat -.. autoclass:: In -.. autoclass:: IsInstance -.. autoclass:: InEnum -.. autoclass:: ListOf -.. autoclass:: Module -.. autoclass:: Path -.. autoclass:: PathList -.. autoclass:: DynamicImplicitDomain diff --git a/doc/OnlineDocs/reference/topical/common/dependencies.rst b/doc/OnlineDocs/reference/topical/common/dependencies.rst deleted file mode 100644 index 18d5647681c..00000000000 --- a/doc/OnlineDocs/reference/topical/common/dependencies.rst +++ /dev/null @@ -1,7 +0,0 @@ - -pyomo.common.dependencies -========================= - -.. automodule:: pyomo.common.dependencies - :members: - :member-order: bysource diff --git a/doc/OnlineDocs/reference/topical/common/deprecation.rst b/doc/OnlineDocs/reference/topical/common/deprecation.rst deleted file mode 100644 index 41066c040c4..00000000000 --- a/doc/OnlineDocs/reference/topical/common/deprecation.rst +++ /dev/null @@ -1,6 +0,0 @@ -pyomo.common.deprecation -======================== - -.. automodule:: pyomo.common.deprecation - :members: - :member-order: bysource diff --git a/doc/OnlineDocs/reference/topical/common/enums.rst b/doc/OnlineDocs/reference/topical/common/enums.rst deleted file mode 100644 index 5ed2dbb1e80..00000000000 --- a/doc/OnlineDocs/reference/topical/common/enums.rst +++ /dev/null @@ -1,7 +0,0 @@ - -pyomo.common.enums -================== - -.. automodule:: pyomo.common.enums - :members: - :member-order: bysource diff --git a/doc/OnlineDocs/reference/topical/common/errors.rst b/doc/OnlineDocs/reference/topical/common/errors.rst deleted file mode 100644 index 7b2bd01fe32..00000000000 --- a/doc/OnlineDocs/reference/topical/common/errors.rst +++ /dev/null @@ -1,6 +0,0 @@ -pyomo.common.errors -=================== - -.. automodule:: pyomo.common.errors - :members: - :member-order: bysource diff --git a/doc/OnlineDocs/reference/topical/common/fileutils.rst b/doc/OnlineDocs/reference/topical/common/fileutils.rst deleted file mode 100644 index e582f4c2e94..00000000000 --- a/doc/OnlineDocs/reference/topical/common/fileutils.rst +++ /dev/null @@ -1,6 +0,0 @@ -pyomo.common.fileutils -====================== - -.. automodule:: pyomo.common.fileutils - :members: - :member-order: bysource diff --git a/doc/OnlineDocs/reference/topical/common/formatting.rst b/doc/OnlineDocs/reference/topical/common/formatting.rst deleted file mode 100644 index 25f0ef2404c..00000000000 --- a/doc/OnlineDocs/reference/topical/common/formatting.rst +++ /dev/null @@ -1,6 +0,0 @@ -pyomo.common.formatting -======================= - -.. automodule:: pyomo.common.formatting - :members: - :member-order: bysource diff --git a/doc/OnlineDocs/reference/topical/common/index.rst b/doc/OnlineDocs/reference/topical/common/index.rst deleted file mode 100644 index c03436600f2..00000000000 --- a/doc/OnlineDocs/reference/topical/common/index.rst +++ /dev/null @@ -1,19 +0,0 @@ -Common Utilities -================ - -Pyomo provides a set of general-purpose utilities through -``pyomo.common``. These utilities are self-contained and do not import -or rely on any other parts of Pyomo. - -.. toctree:: - :maxdepth: 1 - - config.rst - dependencies.rst - deprecation.rst - enums.rst - errors.rst - fileutils.rst - formatting.rst - tempfiles.rst - timing.rst diff --git a/doc/OnlineDocs/reference/topical/common/tempfiles.rst b/doc/OnlineDocs/reference/topical/common/tempfiles.rst deleted file mode 100644 index 03cb056dffe..00000000000 --- a/doc/OnlineDocs/reference/topical/common/tempfiles.rst +++ /dev/null @@ -1,7 +0,0 @@ - -pyomo.common.tempfiles -====================== - -.. automodule:: pyomo.common.tempfiles - :members: - :member-order: bysource diff --git a/doc/OnlineDocs/reference/topical/common/timing.rst b/doc/OnlineDocs/reference/topical/common/timing.rst deleted file mode 100644 index 06b6fc0f588..00000000000 --- a/doc/OnlineDocs/reference/topical/common/timing.rst +++ /dev/null @@ -1,7 +0,0 @@ - -pyomo.common.timing -=================== - -.. automodule:: pyomo.common.timing - :members: - :member-order: bysource From 169d7ecbf8486efd74fbff0fd9f1bbbba0a0c21f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 23 Sep 2024 15:29:01 -0600 Subject: [PATCH 2457/3044] Update rst docs to not duplicate API docs (add :noindex: or use summary only) --- doc/OnlineDocs/code.rst | 1 - .../analysis/alternative_solutions.rst | 6 + .../explanation/analysis/community.rst | 4 +- doc/OnlineDocs/explanation/analysis/iis.rst | 2 + .../explanation/analysis/incidence/config.rst | 1 + .../analysis/incidence/connected.rst | 1 + .../analysis/incidence/dulmage_mendelsohn.rst | 1 + .../analysis/incidence/incidence.rst | 1 + .../analysis/incidence/interface.rst | 1 + .../analysis/incidence/matching.rst | 1 + .../analysis/incidence/scc_solver.rst | 1 + .../analysis/incidence/triangularize.rst | 1 + .../explanation/analysis/mpc/conversion.rst | 1 + .../explanation/analysis/mpc/data.rst | 13 +- .../explanation/analysis/mpc/interface.rst | 2 + .../explanation/analysis/mpc/modeling.rst | 3 + .../explanation/analysis/parmest/api.rst | 3 + .../analysis/sensitivity_toolbox.rst | 1 + .../explanation/experimental/solvers.rst | 9 +- doc/OnlineDocs/explanation/modeling/dae.rst | 13 +- .../explanation/modeling/gdp/solving.rst | 1 + .../explanation/modeling/network.rst | 5 + doc/OnlineDocs/explanation/modeling/units.rst | 11 +- .../modeling_utils/flattener/reference.rst | 3 + .../modeling_utils/latex_printer.rst | 1 + .../modeling_utils/preprocessing.rst | 13 ++ .../explanation/modeling_utils/scaling.rst | 6 +- doc/OnlineDocs/explanation/solvers/gdpopt.rst | 18 +- .../explanation/solvers/mindtpy.rst | 1 + .../explanation/solvers/multistart.rst | 1 + .../explanation/solvers/pynumero/index.rst | 7 +- doc/OnlineDocs/explanation/solvers/pyros.rst | 54 ++---- .../explanation/solvers/trustregion.rst | 1 + .../reference/topical/aml/index.rst | 89 ++------- .../reference/topical/appsi/appsi.base.rst | 53 +----- .../reference/topical/appsi/appsi.rst | 1 + .../topical/appsi/appsi.solvers.cbc.rst | 14 +- .../topical/appsi/appsi.solvers.cplex.rst | 21 +-- .../topical/appsi/appsi.solvers.gurobi.rst | 15 +- .../topical/appsi/appsi.solvers.highs.rst | 13 +- .../topical/appsi/appsi.solvers.ipopt.rst | 13 +- .../topical/appsi/appsi.solvers.maingo.rst | 13 +- .../reference/topical/appsi/appsi.solvers.rst | 4 +- .../reference/topical/data/index.rst | 2 + .../topical/expressions/building.rst | 12 +- .../reference/topical/expressions/classes.rst | 103 +++-------- .../topical/expressions/context_managers.rst | 7 +- .../topical/expressions/managing.rst | 20 +- .../topical/expressions/visitors.rst | 20 +- .../reference/topical/kernel/base.rst | 6 +- .../reference/topical/kernel/block.rst | 14 -- .../reference/topical/kernel/conic.rst | 21 --- .../reference/topical/kernel/constraint.rst | 21 --- .../topical/kernel/dict_container.rst | 8 +- .../reference/topical/kernel/expression.rst | 15 -- .../kernel/heterogeneous_container.rst | 6 +- .../topical/kernel/homogeneous_container.rst | 6 +- .../topical/kernel/list_container.rst | 8 +- .../reference/topical/kernel/objective.rst | 15 -- .../reference/topical/kernel/parameter.rst | 18 -- .../topical/kernel/piecewise/piecewise.rst | 36 +--- .../topical/kernel/piecewise/piecewise_nd.rst | 15 +- .../topical/kernel/piecewise/util.rst | 6 +- .../reference/topical/kernel/sos.rst | 16 -- .../reference/topical/kernel/suffix.rst | 6 +- .../topical/kernel/tuple_container.rst | 8 +- .../reference/topical/kernel/variable.rst | 15 -- .../topical/solvers/cplex_persistent.rst | 7 +- .../reference/topical/solvers/gams.rst | 13 +- .../topical/solvers/gurobi_direct.rst | 10 +- .../topical/solvers/gurobi_persistent.rst | 12 +- .../topical/solvers/xpress_persistent.rst | 7 +- pyomo/__future__.py | 2 - pyomo/contrib/pynumero/sparse/block_vector.py | 172 +++++++++++++++++- 74 files changed, 430 insertions(+), 621 deletions(-) diff --git a/doc/OnlineDocs/code.rst b/doc/OnlineDocs/code.rst index 1ba1a2ea007..2b17bced79d 100644 --- a/doc/OnlineDocs/code.rst +++ b/doc/OnlineDocs/code.rst @@ -3,6 +3,5 @@ :caption: Library Reference :template: recursive-module.rst :recursive: - :noindex: pyomo diff --git a/doc/OnlineDocs/explanation/analysis/alternative_solutions.rst b/doc/OnlineDocs/explanation/analysis/alternative_solutions.rst index d9551f005fb..a8aeccae7fb 100644 --- a/doc/OnlineDocs/explanation/analysis/alternative_solutions.rst +++ b/doc/OnlineDocs/explanation/analysis/alternative_solutions.rst @@ -94,14 +94,20 @@ Interface Documentation .. currentmodule:: pyomo.contrib.alternative_solutions .. autofunction:: enumerate_binary_solutions + :noindex: .. autofunction:: enumerate_linear_solutions + :noindex: .. autofunction:: pyomo.contrib.alternative_solutions.lp_enum_solnpool.enumerate_linear_solutions_soln_pool + :noindex: .. autofunction:: gurobi_generate_solutions + :noindex: .. autofunction:: obbt_analysis_bounds_and_solutions + :noindex: .. autoclass:: Solution + :noindex: diff --git a/doc/OnlineDocs/explanation/analysis/community.rst b/doc/OnlineDocs/explanation/analysis/community.rst index b110107e604..2a9e2025d35 100644 --- a/doc/OnlineDocs/explanation/analysis/community.rst +++ b/doc/OnlineDocs/explanation/analysis/community.rst @@ -24,7 +24,7 @@ detection. Thus, this package provides the user with a lot of control over the c function we use for this community detection is shown below: .. autofunction:: pyomo.contrib.community_detection.detection.detect_communities - :noindex: + :noindex: As stated above, the characteristics of the NetworkX graph of the Pyomo model are very important to the community detection. The main graph features the user can specify are the type of community map, @@ -383,7 +383,9 @@ We can see an example for the three separate graphs created by these three funct Functions in this Package ------------------------- .. automodule:: pyomo.contrib.community_detection.detection + :noindex: :members: .. automodule:: pyomo.contrib.community_detection.community_graph + :noindex: :members: diff --git a/doc/OnlineDocs/explanation/analysis/iis.rst b/doc/OnlineDocs/explanation/analysis/iis.rst index fa97c2f8c61..e4d9a81c9cf 100644 --- a/doc/OnlineDocs/explanation/analysis/iis.rst +++ b/doc/OnlineDocs/explanation/analysis/iis.rst @@ -16,8 +16,10 @@ Infeasible Irreducible System (IIS) Tool ======================================== .. automodule:: pyomo.contrib.iis.iis + :noindex: .. autofunction:: pyomo.contrib.iis.write_iis + :noindex: Minimal Intractable System finder (MIS) Tool ============================================ diff --git a/doc/OnlineDocs/explanation/analysis/incidence/config.rst b/doc/OnlineDocs/explanation/analysis/incidence/config.rst index 06e4f5c5626..5260d3de256 100644 --- a/doc/OnlineDocs/explanation/analysis/incidence/config.rst +++ b/doc/OnlineDocs/explanation/analysis/incidence/config.rst @@ -2,4 +2,5 @@ Incidence Options ================= .. automodule:: pyomo.contrib.incidence_analysis.config + :noindex: :members: diff --git a/doc/OnlineDocs/explanation/analysis/incidence/connected.rst b/doc/OnlineDocs/explanation/analysis/incidence/connected.rst index 4cf60f62eba..301d78f8a95 100644 --- a/doc/OnlineDocs/explanation/analysis/incidence/connected.rst +++ b/doc/OnlineDocs/explanation/analysis/incidence/connected.rst @@ -2,4 +2,5 @@ Weakly Connected Components =========================== .. automodule:: pyomo.contrib.incidence_analysis.connected + :noindex: :members: diff --git a/doc/OnlineDocs/explanation/analysis/incidence/dulmage_mendelsohn.rst b/doc/OnlineDocs/explanation/analysis/incidence/dulmage_mendelsohn.rst index 6fe2bd59324..dfcd3ea1a33 100644 --- a/doc/OnlineDocs/explanation/analysis/incidence/dulmage_mendelsohn.rst +++ b/doc/OnlineDocs/explanation/analysis/incidence/dulmage_mendelsohn.rst @@ -2,4 +2,5 @@ Dulmage-Mendelsohn Partition ============================ .. automodule:: pyomo.contrib.incidence_analysis.dulmage_mendelsohn + :noindex: :members: diff --git a/doc/OnlineDocs/explanation/analysis/incidence/incidence.rst b/doc/OnlineDocs/explanation/analysis/incidence/incidence.rst index ebf481c00a7..d8bbab089ba 100644 --- a/doc/OnlineDocs/explanation/analysis/incidence/incidence.rst +++ b/doc/OnlineDocs/explanation/analysis/incidence/incidence.rst @@ -2,4 +2,5 @@ Incident Variables ================== .. automodule:: pyomo.contrib.incidence_analysis.incidence + :noindex: :members: diff --git a/doc/OnlineDocs/explanation/analysis/incidence/interface.rst b/doc/OnlineDocs/explanation/analysis/incidence/interface.rst index 29c92d8193c..1f6cd20bec3 100644 --- a/doc/OnlineDocs/explanation/analysis/incidence/interface.rst +++ b/doc/OnlineDocs/explanation/analysis/incidence/interface.rst @@ -2,4 +2,5 @@ Pyomo Interfaces ================ .. automodule:: pyomo.contrib.incidence_analysis.interface + :noindex: :members: diff --git a/doc/OnlineDocs/explanation/analysis/incidence/matching.rst b/doc/OnlineDocs/explanation/analysis/incidence/matching.rst index 1941c7116cd..83aeb06a7fa 100644 --- a/doc/OnlineDocs/explanation/analysis/incidence/matching.rst +++ b/doc/OnlineDocs/explanation/analysis/incidence/matching.rst @@ -2,4 +2,5 @@ Maximum Matching ================ .. automodule:: pyomo.contrib.incidence_analysis.matching + :noindex: :members: diff --git a/doc/OnlineDocs/explanation/analysis/incidence/scc_solver.rst b/doc/OnlineDocs/explanation/analysis/incidence/scc_solver.rst index 35f494af1a1..5f20a96191d 100644 --- a/doc/OnlineDocs/explanation/analysis/incidence/scc_solver.rst +++ b/doc/OnlineDocs/explanation/analysis/incidence/scc_solver.rst @@ -2,4 +2,5 @@ Block Triangular Decomposition Solver ===================================== .. automodule:: pyomo.contrib.incidence_analysis.scc_solver + :noindex: :members: diff --git a/doc/OnlineDocs/explanation/analysis/incidence/triangularize.rst b/doc/OnlineDocs/explanation/analysis/incidence/triangularize.rst index a051086a859..e1e60a39677 100644 --- a/doc/OnlineDocs/explanation/analysis/incidence/triangularize.rst +++ b/doc/OnlineDocs/explanation/analysis/incidence/triangularize.rst @@ -2,4 +2,5 @@ Block Triangularization ======================= .. automodule:: pyomo.contrib.incidence_analysis.triangularize + :noindex: :members: diff --git a/doc/OnlineDocs/explanation/analysis/mpc/conversion.rst b/doc/OnlineDocs/explanation/analysis/mpc/conversion.rst index 9d9406edb75..e78a1d69e0b 100644 --- a/doc/OnlineDocs/explanation/analysis/mpc/conversion.rst +++ b/doc/OnlineDocs/explanation/analysis/mpc/conversion.rst @@ -2,4 +2,5 @@ Data Conversion =============== .. automodule:: pyomo.contrib.mpc.data.convert + :noindex: :members: diff --git a/doc/OnlineDocs/explanation/analysis/mpc/data.rst b/doc/OnlineDocs/explanation/analysis/mpc/data.rst index 73cb6543b1e..da65bf40814 100644 --- a/doc/OnlineDocs/explanation/analysis/mpc/data.rst +++ b/doc/OnlineDocs/explanation/analysis/mpc/data.rst @@ -3,15 +3,20 @@ Data Structures .. automodule:: pyomo.contrib.mpc.data.get_cuid :members: + :noindex: -.. automodule:: pyomo.contrib.mpc.data.dynamic_data_base + automodule:: pyomo.contrib.mpc.data.dynamic_data_base :members: + :noindex: -.. automodule:: pyomo.contrib.mpc.data.scalar_data + automodule:: pyomo.contrib.mpc.data.scalar_data :members: + :noindex: -.. automodule:: pyomo.contrib.mpc.data.series_data + automodule:: pyomo.contrib.mpc.data.series_data :members: + :noindex: -.. automodule:: pyomo.contrib.mpc.data.interval_data + automodule:: pyomo.contrib.mpc.data.interval_data :members: + :noindex: diff --git a/doc/OnlineDocs/explanation/analysis/mpc/interface.rst b/doc/OnlineDocs/explanation/analysis/mpc/interface.rst index eb5bac548fd..13a5bf24360 100644 --- a/doc/OnlineDocs/explanation/analysis/mpc/interface.rst +++ b/doc/OnlineDocs/explanation/analysis/mpc/interface.rst @@ -2,7 +2,9 @@ Interfaces ========== .. automodule:: pyomo.contrib.mpc.interfaces.model_interface + :noindex: :members: .. automodule:: pyomo.contrib.mpc.interfaces.var_linker + :noindex: :members: diff --git a/doc/OnlineDocs/explanation/analysis/mpc/modeling.rst b/doc/OnlineDocs/explanation/analysis/mpc/modeling.rst index cbae03161b1..2bc213f1702 100644 --- a/doc/OnlineDocs/explanation/analysis/mpc/modeling.rst +++ b/doc/OnlineDocs/explanation/analysis/mpc/modeling.rst @@ -2,10 +2,13 @@ Modeling Components =================== .. automodule:: pyomo.contrib.mpc.modeling.constraints + :noindex: :members: .. automodule:: pyomo.contrib.mpc.modeling.cost_expressions + :noindex: :members: .. automodule:: pyomo.contrib.mpc.modeling.terminal + :noindex: :members: diff --git a/doc/OnlineDocs/explanation/analysis/parmest/api.rst b/doc/OnlineDocs/explanation/analysis/parmest/api.rst index 4d6896a8582..a1456361260 100644 --- a/doc/OnlineDocs/explanation/analysis/parmest/api.rst +++ b/doc/OnlineDocs/explanation/analysis/parmest/api.rst @@ -6,6 +6,7 @@ API parmest --------- .. automodule:: pyomo.contrib.parmest.parmest + :noindex: :members: :undoc-members: :show-inheritance: @@ -13,6 +14,7 @@ parmest scenariocreator ------------------ .. automodule:: pyomo.contrib.parmest.scenariocreator + :noindex: :members: :undoc-members: :show-inheritance: @@ -20,6 +22,7 @@ scenariocreator graphics --------- .. automodule:: pyomo.contrib.parmest.graphics + :noindex: :members: :undoc-members: :show-inheritance: diff --git a/doc/OnlineDocs/explanation/analysis/sensitivity_toolbox.rst b/doc/OnlineDocs/explanation/analysis/sensitivity_toolbox.rst index 2a2ccff4b09..17c0e765541 100644 --- a/doc/OnlineDocs/explanation/analysis/sensitivity_toolbox.rst +++ b/doc/OnlineDocs/explanation/analysis/sensitivity_toolbox.rst @@ -183,3 +183,4 @@ Sensitivity Toolbox Interface ----------------------------- .. autofunction:: pyomo.contrib.sensitivity_toolbox.sens.sensitivity_calculation + :noindex: diff --git a/doc/OnlineDocs/explanation/experimental/solvers.rst b/doc/OnlineDocs/explanation/experimental/solvers.rst index cd4cafa89dd..3f2653aa732 100644 --- a/doc/OnlineDocs/explanation/experimental/solvers.rst +++ b/doc/OnlineDocs/explanation/experimental/solvers.rst @@ -246,6 +246,7 @@ can control the NL writer in the new ``ipopt`` interface through the solver's ``writer_config`` configuration option: .. autoclass:: pyomo.contrib.solver.ipopt.Ipopt + :noindex: :members: solve .. testcode:: @@ -286,11 +287,13 @@ All new interfaces should be built upon one of two classes (currently): All solvers should have the following: .. autoclass:: pyomo.contrib.solver.base.SolverBase + :noindex: :members: Persistent solvers include additional members as well as other configuration options: .. autoclass:: pyomo.contrib.solver.base.PersistentSolverBase + :noindex: :show-inheritance: :members: @@ -304,6 +307,7 @@ object. This object is a :py:class:`pyomo.common.config.ConfigDict`, which can be manipulated similar to a standard ``dict`` in Python. .. autoclass:: pyomo.contrib.solver.results.Results + :noindex: :show-inheritance: :members: :undoc-members: @@ -320,6 +324,7 @@ to inspect the :class:`Results` object or any returned solver messages or logs for more information. .. autoclass:: pyomo.contrib.solver.results.TerminationCondition + :noindex: :show-inheritance: @@ -335,6 +340,7 @@ user is expected to inspect the returned solver messages or logs for more information. .. autoclass:: pyomo.contrib.solver.results.SolutionStatus + :noindex: :show-inheritance: @@ -346,6 +352,7 @@ loader should be written for each unique case. Several have already been implemented. For example, for ``ipopt``: .. autoclass:: pyomo.contrib.solver.ipopt.IpoptSolutionLoader - :show-inheritance: + :noindex: :members: + :show-inheritance: :inherited-members: diff --git a/doc/OnlineDocs/explanation/modeling/dae.rst b/doc/OnlineDocs/explanation/modeling/dae.rst index ff0fb75e610..ffe01c84914 100644 --- a/doc/OnlineDocs/explanation/modeling/dae.rst +++ b/doc/OnlineDocs/explanation/modeling/dae.rst @@ -58,7 +58,8 @@ bounds of the continuous domain. A user may also specify additional points in the domain to be used as finite element points in the discretization. .. autoclass:: pyomo.dae.ContinuousSet - :members: + :noindex: + :members: The following code snippet shows examples of declaring a :py:class:`ContinuousSet ` component on a @@ -135,7 +136,8 @@ DerivativeVar ************* .. autoclass:: pyomo.dae.DerivativeVar - :members: + :noindex: + :members: The code snippet below shows examples of declaring :py:class:`DerivativeVar ` components on a @@ -287,6 +289,7 @@ Declaring Integrals equations. .. autoclass:: pyomo.dae.Integral + :noindex: :members: Declaring an :py:class:`Integral` component is similar to @@ -556,7 +559,8 @@ transformation to reduce the number of free collocation points within a finite element for a particular variable. .. autoclass:: pyomo.dae.plugins.colloc.Collocation_Discretization_Transformation - :members: reduce_collocation_points + :noindex: + :members: reduce_collocation_points An example of using this function is shown below: @@ -722,7 +726,8 @@ packages. order to use this class. .. autoclass:: pyomo.dae.Simulator - :members: + :noindex: + :members: .. note:: Any keyword options supported by the integrator may be specified as diff --git a/doc/OnlineDocs/explanation/modeling/gdp/solving.rst b/doc/OnlineDocs/explanation/modeling/gdp/solving.rst index 9fea90ebf5f..88451f5f128 100644 --- a/doc/OnlineDocs/explanation/modeling/gdp/solving.rst +++ b/doc/OnlineDocs/explanation/modeling/gdp/solving.rst @@ -70,6 +70,7 @@ also be created, as described in :ref:`gdp-advanced-examples`. Following solution of the GDP model, values of the Boolean variables may be updated from their algebraic binary counterparts using the ``update_boolean_vars_from_binary()`` function. .. autofunction:: pyomo.core.plugins.transform.logical_to_linear.update_boolean_vars_from_binary + :noindex: Factorable Programming ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/OnlineDocs/explanation/modeling/network.rst b/doc/OnlineDocs/explanation/modeling/network.rst index 3fce9448997..3c4b60bfb6a 100644 --- a/doc/OnlineDocs/explanation/modeling/network.rst +++ b/doc/OnlineDocs/explanation/modeling/network.rst @@ -29,10 +29,12 @@ Port **** .. autoclass:: pyomo.network.Port + :noindex: :members: :exclude-members: construct, display .. autoclass:: pyomo.network.port._PortData + :noindex: :members: :special-members: __getattr__ :exclude-members: set_value @@ -62,10 +64,12 @@ Arc *** .. autoclass:: pyomo.network.Arc + :noindex: :members: :exclude-members: construct .. autoclass:: pyomo.network.arc._ArcData + :noindex: :members: :special-members: __getattr__ @@ -326,6 +330,7 @@ class: >>> seq.run(m, initialize) .. autoclass:: pyomo.network.SequentialDecomposition + :noindex: :members: set_guesses_for, set_tear_set, tear_set_arcs, indexes_to_arcs, run, create_graph, select_tear_mip, select_tear_mip_model, select_tear_heuristic, calculation_order, tree_order diff --git a/doc/OnlineDocs/explanation/modeling/units.rst b/doc/OnlineDocs/explanation/modeling/units.rst index f09a3361b6b..6e4c1ae3f15 100644 --- a/doc/OnlineDocs/explanation/modeling/units.rst +++ b/doc/OnlineDocs/explanation/modeling/units.rst @@ -2,12 +2,11 @@ Units Handling in Pyomo ======================= .. automodule:: pyomo.core.base.units_container + :noindex: -.. autoclass:: PyomoUnitsContainer - :show-inheritance: - :members: +.. autosummary:: -.. autoclass:: UnitsError - -.. autoclass:: InconsistentUnitsError + PyomoUnitsContainer + UnitsError + InconsistentUnitsError diff --git a/doc/OnlineDocs/explanation/modeling_utils/flattener/reference.rst b/doc/OnlineDocs/explanation/modeling_utils/flattener/reference.rst index 22c7b67e1f6..b30559ef1a6 100644 --- a/doc/OnlineDocs/explanation/modeling_utils/flattener/reference.rst +++ b/doc/OnlineDocs/explanation/modeling_utils/flattener/reference.rst @@ -8,7 +8,10 @@ API reference pyomo.dae.flatten.flatten_dae_components .. autofunction:: pyomo.dae.flatten.slice_component_along_sets + :noindex: .. autofunction:: pyomo.dae.flatten.flatten_components_along_sets + :noindex: .. autofunction:: pyomo.dae.flatten.flatten_dae_components + :noindex: diff --git a/doc/OnlineDocs/explanation/modeling_utils/latex_printer.rst b/doc/OnlineDocs/explanation/modeling_utils/latex_printer.rst index ff3f628c0c8..c03eebe2f91 100644 --- a/doc/OnlineDocs/explanation/modeling_utils/latex_printer.rst +++ b/doc/OnlineDocs/explanation/modeling_utils/latex_printer.rst @@ -4,6 +4,7 @@ Latex Printing Pyomo models can be printed to a LaTeX compatible format using the ``pyomo.contrib.latex_printer.latex_printer`` function: .. autofunction:: pyomo.contrib.latex_printer.latex_printer.latex_printer + :noindex: .. note:: diff --git a/doc/OnlineDocs/explanation/modeling_utils/preprocessing.rst b/doc/OnlineDocs/explanation/modeling_utils/preprocessing.rst index fd26f2bf6db..1f68208fdc3 100644 --- a/doc/OnlineDocs/explanation/modeling_utils/preprocessing.rst +++ b/doc/OnlineDocs/explanation/modeling_utils/preprocessing.rst @@ -58,6 +58,7 @@ To see the results of the transformation, you could then use the command >>> m.pprint() .. autoclass:: pyomo.contrib.preprocessing.plugins.var_aggregator.VariableAggregator + :noindex: :members: apply_to, create_using, update_variables @@ -77,6 +78,7 @@ Explicit Constraints to Variable Bounds >>> TransformationFactory('contrib.constraints_to_var_bounds').apply_to(m) .. autoclass:: pyomo.contrib.preprocessing.plugins.bounds_to_vars.ConstraintToVarBoundTransform + :noindex: :members: apply_to, create_using @@ -84,6 +86,7 @@ Induced Linearity Reformulation ------------------------------- .. autoclass:: pyomo.contrib.preprocessing.plugins.induced_linearity.InducedLinearity + :noindex: :members: apply_to, create_using @@ -94,58 +97,68 @@ This transformation was developed by `Sunjeev Kale `_ at Carnegie Mellon University. .. autoclass:: pyomo.contrib.preprocessing.plugins.constraint_tightener.TightenConstraintFromVars + :noindex: :members: apply_to, create_using Trivial Constraint Deactivation ------------------------------- .. autoclass:: pyomo.contrib.preprocessing.plugins.deactivate_trivial_constraints.TrivialConstraintDeactivator + :noindex: :members: apply_to, create_using, revert Fixed Variable Detection ------------------------ .. autoclass:: pyomo.contrib.preprocessing.plugins.detect_fixed_vars.FixedVarDetector + :noindex: :members: apply_to, create_using, revert Fixed Variable Equality Propagator ---------------------------------- .. autoclass:: pyomo.contrib.preprocessing.plugins.equality_propagate.FixedVarPropagator + :noindex: :members: apply_to, create_using, revert Variable Bound Equality Propagator ---------------------------------- .. autoclass:: pyomo.contrib.preprocessing.plugins.equality_propagate.VarBoundPropagator + :noindex: :members: apply_to, create_using, revert Variable Midpoint Initializer ----------------------------- .. autoclass:: pyomo.contrib.preprocessing.plugins.init_vars.InitMidpoint + :noindex: :members: apply_to, create_using Variable Zero Initializer ------------------------- .. autoclass:: pyomo.contrib.preprocessing.plugins.init_vars.InitZero + :noindex: :members: apply_to, create_using Zero Term Remover ----------------- .. autoclass:: pyomo.contrib.preprocessing.plugins.remove_zero_terms.RemoveZeroTerms + :noindex: :members: apply_to, create_using Variable Bound Remover ---------------------- .. autoclass:: pyomo.contrib.preprocessing.plugins.strip_bounds.VariableBoundStripper + :noindex: :members: apply_to, create_using, revert Zero Sum Propagator ------------------- .. autoclass:: pyomo.contrib.preprocessing.plugins.zero_sum_propagator.ZeroSumPropagator + :noindex: :members: apply_to, create_using diff --git a/doc/OnlineDocs/explanation/modeling_utils/scaling.rst b/doc/OnlineDocs/explanation/modeling_utils/scaling.rst index 180f1e0205b..7761e275176 100644 --- a/doc/OnlineDocs/explanation/modeling_utils/scaling.rst +++ b/doc/OnlineDocs/explanation/modeling_utils/scaling.rst @@ -3,8 +3,10 @@ Model Scaling Transformation Good scaling of models can greatly improve the numerical properties of a problem and thus increase reliability and convergence. The ``core.scale_model`` transformation allows users to separate scaling of a model from the declaration of the model variables and constraints which allows for models to be written in more natural forms and to be scaled and rescaled as required without having to rewrite the model code. -.. autoclass:: pyomo.core.plugins.transform.scaling.ScaleModel - :members: +.. autosummary:: + + pyomo.core.plugins.transform.scaling.ScaleModel + Setting Scaling Factors ----------------------- diff --git a/doc/OnlineDocs/explanation/solvers/gdpopt.rst b/doc/OnlineDocs/explanation/solvers/gdpopt.rst index 670d7633f6d..953799f0555 100644 --- a/doc/OnlineDocs/explanation/solvers/gdpopt.rst +++ b/doc/OnlineDocs/explanation/solvers/gdpopt.rst @@ -197,17 +197,11 @@ GDPopt implementation and optional arguments GDPopt optional arguments should be considered beta code and are subject to change. -.. autoclass:: pyomo.contrib.gdpopt.GDPopt.GDPoptSolver - :members: +.. autosummary:: -.. autoclass:: pyomo.contrib.gdpopt.loa.GDP_LOA_Solver - :members: + ~pyomo.contrib.gdpopt.GDPopt.GDPoptSolver + ~pyomo.contrib.gdpopt.loa.GDP_LOA_Solver + ~pyomo.contrib.gdpopt.gloa.GDP_GLOA_Solver + ~pyomo.contrib.gdpopt.ric.GDP_RIC_Solver + ~pyomo.contrib.gdpopt.branch_and_bound.GDP_LBB_Solver -.. autoclass:: pyomo.contrib.gdpopt.gloa.GDP_GLOA_Solver - :members: - -.. autoclass:: pyomo.contrib.gdpopt.ric.GDP_RIC_Solver - :members: - -.. autoclass:: pyomo.contrib.gdpopt.branch_and_bound.GDP_LBB_Solver - :members: diff --git a/doc/OnlineDocs/explanation/solvers/mindtpy.rst b/doc/OnlineDocs/explanation/solvers/mindtpy.rst index a850a42c740..ce7650b5a05 100644 --- a/doc/OnlineDocs/explanation/solvers/mindtpy.rst +++ b/doc/OnlineDocs/explanation/solvers/mindtpy.rst @@ -301,6 +301,7 @@ MindtPy Implementation and Optional Arguments subject to change. .. autoclass:: pyomo.contrib.mindtpy.MindtPy.MindtPySolver + :noindex: :members: Get Help diff --git a/doc/OnlineDocs/explanation/solvers/multistart.rst b/doc/OnlineDocs/explanation/solvers/multistart.rst index 069d770aa91..f54cb26d00f 100644 --- a/doc/OnlineDocs/explanation/solvers/multistart.rst +++ b/doc/OnlineDocs/explanation/solvers/multistart.rst @@ -31,4 +31,5 @@ Multistart wrapper implementation and optional arguments -------------------------------------------------------- .. autoclass:: pyomo.contrib.multistart.multi.MultiStart + :noindex: :members: diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/index.rst b/doc/OnlineDocs/explanation/solvers/pynumero/index.rst index 711bb83eb3b..f2deafcfe71 100644 --- a/doc/OnlineDocs/explanation/solvers/pynumero/index.rst +++ b/doc/OnlineDocs/explanation/solvers/pynumero/index.rst @@ -12,10 +12,15 @@ PyNumero. For more details, see the API documentation (:ref:`pynumero_api`). installation.rst tutorial.rst - api.rst backward_compatibility.rst +PyNumero API +------------ + +:mod:`pyomo.contrib.pynumero` + + Developers ---------- diff --git a/doc/OnlineDocs/explanation/solvers/pyros.rst b/doc/OnlineDocs/explanation/solvers/pyros.rst index 95049eded8a..8efa0defaac 100644 --- a/doc/OnlineDocs/explanation/solvers/pyros.rst +++ b/doc/OnlineDocs/explanation/solvers/pyros.rst @@ -128,8 +128,7 @@ These are more elaborately presented in the PyROS Solver Interface ----------------------------- -.. autoclass:: pyomo.contrib.pyros.PyROS - :members: solve +The PyROS solver is invoked through the :py:meth:`PyROS.solve` method. .. note:: Upon successful convergence of PyROS, the solution returned is @@ -211,45 +210,18 @@ the various abstract and pre-implemented PyROS Uncertainty Set Classes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.BoxSet - :show-inheritance: - :special-members: bounds, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.CardinalitySet - :show-inheritance: - :special-members: origin, positive_deviation, gamma, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.BudgetSet - :show-inheritance: - :special-members: coefficients_mat, rhs_vec, origin, budget_membership_mat, budget_rhs_vec, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.FactorModelSet - :show-inheritance: - :special-members: origin, number_of_factors, psi_mat, beta, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.PolyhedralSet - :show-inheritance: - :special-members: coefficients_mat, rhs_vec, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.AxisAlignedEllipsoidalSet - :show-inheritance: - :special-members: center, half_lengths, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.EllipsoidalSet - :show-inheritance: - :special-members: center, shape_matrix, scale, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.UncertaintySet - :show-inheritance: - :special-members: parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.DiscreteScenarioSet - :show-inheritance: - :special-members: scenarios, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.IntersectionSet - :show-inheritance: - :special-members: all_sets, type, parameter_bounds, dim, point_in_set +.. autosummary:: + + pyomo.contrib.pyros.uncertainty_sets.BoxSet + pyomo.contrib.pyros.uncertainty_sets.CardinalitySet + pyomo.contrib.pyros.uncertainty_sets.BudgetSet + pyomo.contrib.pyros.uncertainty_sets.FactorModelSet + pyomo.contrib.pyros.uncertainty_sets.PolyhedralSet + pyomo.contrib.pyros.uncertainty_sets.AxisAlignedEllipsoidalSet + pyomo.contrib.pyros.uncertainty_sets.EllipsoidalSet + pyomo.contrib.pyros.uncertainty_sets.UncertaintySet + pyomo.contrib.pyros.uncertainty_sets.DiscreteScenarioSet + pyomo.contrib.pyros.uncertainty_sets.IntersectionSet PyROS Usage Example diff --git a/doc/OnlineDocs/explanation/solvers/trustregion.rst b/doc/OnlineDocs/explanation/solvers/trustregion.rst index f477c905e33..0bdfef2f10d 100644 --- a/doc/OnlineDocs/explanation/solvers/trustregion.rst +++ b/doc/OnlineDocs/explanation/solvers/trustregion.rst @@ -102,6 +102,7 @@ TRF Solver Interface The keyword arguments can be updated at solver instantiation or later when the ``solve`` method is called. .. autoclass:: pyomo.contrib.trustregion.TRF.TrustRegionSolver + :noindex: :members: solve TRF Usage Example diff --git a/doc/OnlineDocs/reference/topical/aml/index.rst b/doc/OnlineDocs/reference/topical/aml/index.rst index f06ca35b087..da727a99629 100644 --- a/doc/OnlineDocs/reference/topical/aml/index.rst +++ b/doc/OnlineDocs/reference/topical/aml/index.rst @@ -3,83 +3,20 @@ AML Library Reference The following modeling components make up the core of the Pyomo Algebraic Modeling Language (AML). These classes are all available -through the `pyomo.environ` namespace. - -.. currentmodule:: pyomo.environ +through the :mod:`pyomo.environ` namespace. .. autosummary:: - ConcreteModel - AbstractModel - Block - Set - RangeSet - Param - Var - Objective - Constraint - ExternalFunction - Reference - SOSConstraint - - -AML Component Documentation -~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: ConcreteModel - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: AbstractModel - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: Block - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: Constraint - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: ExternalFunction - :show-inheritance: - :special-members: __init__ - :members: - :inherited-members: - -.. autoclass:: Objective - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: Param - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: RangeSet - :show-inheritance: - :members: - :inherited-members: - -.. autofunction:: Reference - -.. autoclass:: Set - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: Var - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: SOSConstraint - :show-inheritance: - :members: - :inherited-members: + ~pyomo.core.base.PyomoModel.ConcreteModel + ~pyomo.core.base.PyomoModel.AbstractModel + ~pyomo.core.base.block.Block + ~pyomo.core.base.set.Set + ~pyomo.core.base.rangeset.RangeSet + ~pyomo.core.base.param.Param + ~pyomo.core.base.var.Var + ~pyomo.core.base.objective.Objective + ~pyomo.core.base.constraint.Constraint + ~pyomo.core.base.external.ExternalFunction + ~pyomo.core.base.reference.Reference + ~pyomo.core.base.sos.SOSConstraint diff --git a/doc/OnlineDocs/reference/topical/appsi/appsi.base.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.base.rst index 1b6d5761182..c99d86350b7 100644 --- a/doc/OnlineDocs/reference/topical/appsi/appsi.base.rst +++ b/doc/OnlineDocs/reference/topical/appsi/appsi.base.rst @@ -1,47 +1,12 @@ APPSI Base Classes ================== -.. autoclass:: pyomo.contrib.appsi.base.TerminationCondition - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pyomo.contrib.appsi.base.Results - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pyomo.contrib.appsi.base.Solver - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pyomo.contrib.appsi.base.PersistentSolver - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pyomo.contrib.appsi.base.SolverConfig - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - :exclude-members: NoArgument - -.. autoclass:: pyomo.contrib.appsi.base.MIPSolverConfig - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - :exclude-members: NoArgument - -.. autoclass:: pyomo.contrib.appsi.base.UpdateConfig - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - :exclude-members: NoArgument +.. autosummary:: + + pyomo.contrib.appsi.base.TerminationCondition + pyomo.contrib.appsi.base.Results + pyomo.contrib.appsi.base.Solver + pyomo.contrib.appsi.base.PersistentSolver + pyomo.contrib.appsi.base.SolverConfig + pyomo.contrib.appsi.base.MIPSolverConfig + pyomo.contrib.appsi.base.UpdateConfig diff --git a/doc/OnlineDocs/reference/topical/appsi/appsi.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.rst index e26e4b0e82a..4f4a2ffa60c 100644 --- a/doc/OnlineDocs/reference/topical/appsi/appsi.rst +++ b/doc/OnlineDocs/reference/topical/appsi/appsi.rst @@ -6,6 +6,7 @@ APPSI Auto-Persistent Pyomo Solver Interfaces .. automodule:: pyomo.contrib.appsi + :noindex: :members: :show-inheritance: diff --git a/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.cbc.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.cbc.rst index a0a2f7d0f27..6f3c3bb98d1 100644 --- a/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.cbc.rst +++ b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.cbc.rst @@ -1,15 +1,7 @@ Cbc === -.. autoclass:: pyomo.contrib.appsi.solvers.cbc.CbcConfig - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - :exclude-members: NoArgument +.. autosummary:: -.. autoclass:: pyomo.contrib.appsi.solvers.cbc.Cbc - :members: - :inherited-members: - :undoc-members: - :show-inheritance: + pyomo.contrib.appsi.solvers.cbc.CbcConfig + pyomo.contrib.appsi.solvers.cbc.Cbc diff --git a/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.cplex.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.cplex.rst index 0906fd7ea76..9d64260cb81 100644 --- a/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.cplex.rst +++ b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.cplex.rst @@ -1,21 +1,8 @@ Cplex ===== -.. autoclass:: pyomo.contrib.appsi.solvers.cplex.CplexConfig - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - :exclude-members: NoArgument +.. autosummary:: -.. autoclass:: pyomo.contrib.appsi.solvers.cplex.CplexResults - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pyomo.contrib.appsi.solvers.cplex.Cplex - :members: - :inherited-members: - :undoc-members: - :show-inheritance: + `pyomo.contrib.appsi.solvers.cplex.CplexConfig` + `pyomo.contrib.appsi.solvers.cplex.CplexResults` + `pyomo.contrib.appsi.solvers.cplex.Cplex` diff --git a/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.gurobi.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.gurobi.rst index 9e0af041410..09cad20bbfa 100644 --- a/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.gurobi.rst +++ b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.gurobi.rst @@ -42,14 +42,7 @@ calls to to unexpected errors. -.. autoclass:: pyomo.contrib.appsi.solvers.gurobi.GurobiResults - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pyomo.contrib.appsi.solvers.gurobi.Gurobi - :members: - :inherited-members: - :undoc-members: - :show-inheritance: +.. autosummary:: + + pyomo.contrib.appsi.solvers.gurobi.GurobiResults + pyomo.contrib.appsi.solvers.gurobi.Gurobi diff --git a/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.highs.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.highs.rst index f2f72d0ad85..dbd804664ef 100644 --- a/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.highs.rst +++ b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.highs.rst @@ -1,14 +1,7 @@ HiGHS ===== -.. autoclass:: pyomo.contrib.appsi.solvers.highs.HighsResults - :members: - :inherited-members: - :undoc-members: - :show-inheritance: +.. autosummary:: -.. autoclass:: pyomo.contrib.appsi.solvers.highs.Highs - :members: - :inherited-members: - :undoc-members: - :show-inheritance: + pyomo.contrib.appsi.solvers.highs.HighsResults + pyomo.contrib.appsi.solvers.highs.Highs diff --git a/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.ipopt.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.ipopt.rst index 0d095644100..0b48bbffb5f 100644 --- a/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.ipopt.rst +++ b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.ipopt.rst @@ -1,14 +1,7 @@ Ipopt ===== -.. autoclass:: pyomo.contrib.appsi.solvers.ipopt.IpoptConfig - :members: - :inherited-members: - :undoc-members: - :show-inheritance: +.. autosummary:: -.. autoclass:: pyomo.contrib.appsi.solvers.ipopt.Ipopt - :members: - :inherited-members: - :undoc-members: - :show-inheritance: + pyomo.contrib.appsi.solvers.ipopt.IpoptConfig + pyomo.contrib.appsi.solvers.ipopt.Ipopt diff --git a/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.maingo.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.maingo.rst index 21e61c38d51..fa85fd45ad5 100644 --- a/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.maingo.rst +++ b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.maingo.rst @@ -1,14 +1,7 @@ MAiNGO ====== -.. autoclass:: pyomo.contrib.appsi.solvers.maingo.MAiNGOConfig - :members: - :inherited-members: - :undoc-members: - :show-inheritance: +.. autosummary:: -.. autoclass:: pyomo.contrib.appsi.solvers.maingo.MAiNGO - :members: - :inherited-members: - :undoc-members: - :show-inheritance: + pyomo.contrib.appsi.solvers.maingo.MAiNGOConfig + pyomo.contrib.appsi.solvers.maingo.MAiNGO diff --git a/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.rst b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.rst index f4dcb81b4be..275e6cb4f74 100644 --- a/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.rst +++ b/doc/OnlineDocs/reference/topical/appsi/appsi.solvers.rst @@ -2,9 +2,7 @@ Solvers ======= .. automodule:: pyomo.contrib.appsi.solvers - :members: - :show-inheritance: - :undoc-members: + :noindex: .. toctree:: diff --git a/doc/OnlineDocs/reference/topical/data/index.rst b/doc/OnlineDocs/reference/topical/data/index.rst index fffb06240f8..778f797fa57 100644 --- a/doc/OnlineDocs/reference/topical/data/index.rst +++ b/doc/OnlineDocs/reference/topical/data/index.rst @@ -2,10 +2,12 @@ Model Data Management ===================== .. autoclass:: pyomo.dataportal.DataPortal.DataPortal + :noindex: :members: :special-members: .. autoclass:: pyomo.dataportal.TableData.TableData + :noindex: :members: :special-members: diff --git a/doc/OnlineDocs/reference/topical/expressions/building.rst b/doc/OnlineDocs/reference/topical/expressions/building.rst index 8ffcca9e310..8335116e21f 100644 --- a/doc/OnlineDocs/reference/topical/expressions/building.rst +++ b/doc/OnlineDocs/reference/topical/expressions/building.rst @@ -2,9 +2,11 @@ Utilities to Build Expressions ============================== -.. autofunction:: pyomo.core.util.prod -.. autofunction:: pyomo.core.util.quicksum -.. autofunction:: pyomo.core.util.sum_product -.. autodata:: pyomo.core.util.summation -.. autodata:: pyomo.core.util.dot_product +.. autosummary:: + + pyomo.core.util.prod + pyomo.core.util.quicksum + pyomo.core.util.sum_product + pyomo.core.util.summation + pyomo.core.util.dot_product diff --git a/doc/OnlineDocs/reference/topical/expressions/classes.rst b/doc/OnlineDocs/reference/topical/expressions/classes.rst index 4d448d2da6a..e2ad32875c5 100644 --- a/doc/OnlineDocs/reference/topical/expressions/classes.rst +++ b/doc/OnlineDocs/reference/topical/expressions/classes.rst @@ -1,15 +1,17 @@ Core Classes ============ +.. currentmodule:: pyomo.core.expr.numeric_expr + The following are the two core classes documented here: - * :class:`NumericValue` - * :class:`NumericExpression` + * :class:`NumericValue` + * :class:`NumericExpression` The remaining classes are the public classes for expressions, which developers may need to know about. The methods for these classes are not documented because they are described in the -:class:`NumericExpression` class. +:class:`NumericExpression` class. Sets with Expression Types -------------------------- @@ -17,89 +19,34 @@ Sets with Expression Types The following sets can be used to develop visitor patterns for Pyomo expressions. -.. autodata:: pyomo.core.expr.numvalue.native_numeric_types -.. autodata:: pyomo.core.expr.numvalue.native_types -.. autodata:: pyomo.core.expr.numvalue.nonpyomo_leaf_types +.. autosummary:: + + ~pyomo.common.numeric_types.native_numeric_types + ~pyomo.common.numeric_types.native_types + ~pyomo.common.numeric_types.nonpyomo_leaf_types NumericValue and NumericExpression ---------------------------------- -.. autoclass:: pyomo.core.expr.numvalue.NumericValue - :members: - :special-members: - :private-members: +.. autosummary:: -.. autoclass:: pyomo.core.expr.NumericExpression - :members: - :show-inheritance: - :special-members: - :private-members: + NumericValue + NumericExpression Other Public Classes -------------------- -.. autoclass:: pyomo.core.expr.NegationExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.ExternalFunctionExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.ProductExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.DivisionExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.InequalityExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.EqualityExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.SumExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.GetItemExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.Expr_ifExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: -.. autoclass:: pyomo.core.expr.UnaryFunctionExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: +.. autosummary:: -.. autoclass:: pyomo.core.expr.AbsExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: + NegationExpression + ExternalFunctionExpression + ProductExpression + DivisionExpression + InequalityExpression + EqualityExpression + SumExpression + GetItemExpression + Expr_ifExpression + UnaryFunctionExpression + AbsExpression diff --git a/doc/OnlineDocs/reference/topical/expressions/context_managers.rst b/doc/OnlineDocs/reference/topical/expressions/context_managers.rst index ae6884d684f..e77a4933a01 100644 --- a/doc/OnlineDocs/reference/topical/expressions/context_managers.rst +++ b/doc/OnlineDocs/reference/topical/expressions/context_managers.rst @@ -2,9 +2,8 @@ Context Managers ================ -.. autoclass:: pyomo.core.expr.nonlinear_expression - :members: +.. autosummary:: -.. autoclass:: pyomo.core.expr.linear_expression - :members: + pyomo.core.expr.nonlinear_expression + pyomo.core.expr.linear_expression diff --git a/doc/OnlineDocs/reference/topical/expressions/managing.rst b/doc/OnlineDocs/reference/topical/expressions/managing.rst index 369dd3aace1..ba96f2a2907 100644 --- a/doc/OnlineDocs/reference/topical/expressions/managing.rst +++ b/doc/OnlineDocs/reference/topical/expressions/managing.rst @@ -5,15 +5,19 @@ Utilities to Manage and Analyze Expressions Functions ~~~~~~~~~ -.. autofunction:: pyomo.core.expr.expression_to_string -.. autofunction:: pyomo.core.expr.decompose_term -.. autofunction:: pyomo.core.expr.clone_expression -.. autofunction:: pyomo.core.expr.evaluate_expression -.. autofunction:: pyomo.core.expr.identify_components -.. autofunction:: pyomo.core.expr.identify_variables -.. autofunction:: pyomo.core.expr.differentiate +.. autosummary:: + + pyomo.core.expr.expression_to_string + pyomo.core.expr.decompose_term + pyomo.core.expr.clone_expression + pyomo.core.expr.evaluate_expression + pyomo.core.expr.identify_components + pyomo.core.expr.identify_variables + pyomo.core.expr.differentiate Classes ~~~~~~~ -.. autoclass:: pyomo.core.expr.symbol_map.SymbolMap +.. autosummary:: + + pyomo.core.expr.symbol_map.SymbolMap diff --git a/doc/OnlineDocs/reference/topical/expressions/visitors.rst b/doc/OnlineDocs/reference/topical/expressions/visitors.rst index 77cffe7905f..847cc40b07d 100644 --- a/doc/OnlineDocs/reference/topical/expressions/visitors.rst +++ b/doc/OnlineDocs/reference/topical/expressions/visitors.rst @@ -2,19 +2,9 @@ Visitor Classes =============== -.. autoclass:: pyomo.core.expr.StreamBasedExpressionVisitor - :members: - :inherited-members: - -.. autoclass:: pyomo.core.expr.SimpleExpressionVisitor - :members: - :inherited-members: - -.. autoclass:: pyomo.core.expr.ExpressionValueVisitor - :members: - :inherited-members: - -.. autoclass:: pyomo.core.expr.ExpressionReplacementVisitor - :members: - :inherited-members: +.. autosummary:: + pyomo.core.expr.StreamBasedExpressionVisitor + pyomo.core.expr.SimpleExpressionVisitor + pyomo.core.expr.ExpressionValueVisitor + pyomo.core.expr.ExpressionReplacementVisitor diff --git a/doc/OnlineDocs/reference/topical/kernel/base.rst b/doc/OnlineDocs/reference/topical/kernel/base.rst index 47a2afef68d..884d40a47c2 100644 --- a/doc/OnlineDocs/reference/topical/kernel/base.rst +++ b/doc/OnlineDocs/reference/topical/kernel/base.rst @@ -1,6 +1,6 @@ Base Object Storage Interface ============================= -.. automodule:: pyomo.core.kernel.base - :show-inheritance: - :members: +.. autosummary:: + + pyomo.core.kernel.base diff --git a/doc/OnlineDocs/reference/topical/kernel/block.rst b/doc/OnlineDocs/reference/topical/kernel/block.rst index a61c12610eb..0fae770c355 100644 --- a/doc/OnlineDocs/reference/topical/kernel/block.rst +++ b/doc/OnlineDocs/reference/topical/kernel/block.rst @@ -10,17 +10,3 @@ Summary pyomo.core.kernel.block.block_list pyomo.core.kernel.block.block_dict -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.block.block - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.block.block_tuple - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.block.block_list - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.block.block_dict - :show-inheritance: - :members: diff --git a/doc/OnlineDocs/reference/topical/kernel/conic.rst b/doc/OnlineDocs/reference/topical/kernel/conic.rst index 34552013623..98bc474aee5 100644 --- a/doc/OnlineDocs/reference/topical/kernel/conic.rst +++ b/doc/OnlineDocs/reference/topical/kernel/conic.rst @@ -19,24 +19,3 @@ Summary pyomo.core.kernel.conic.primal_power pyomo.core.kernel.conic.dual_exponential pyomo.core.kernel.conic.dual_power - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.conic.quadratic - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.conic.rotated_quadratic - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.conic.primal_exponential - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.conic.primal_power - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.conic.dual_exponential - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.conic.dual_power - :show-inheritance: - :members: diff --git a/doc/OnlineDocs/reference/topical/kernel/constraint.rst b/doc/OnlineDocs/reference/topical/kernel/constraint.rst index 1645e57f9f2..a4422bfb61d 100644 --- a/doc/OnlineDocs/reference/topical/kernel/constraint.rst +++ b/doc/OnlineDocs/reference/topical/kernel/constraint.rst @@ -11,24 +11,3 @@ Summary pyomo.core.kernel.constraint.constraint_list pyomo.core.kernel.constraint.constraint_dict pyomo.core.kernel.matrix_constraint.matrix_constraint - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.constraint.constraint - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.constraint.linear_constraint - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.constraint.constraint_tuple - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.constraint.constraint_list - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.constraint.constraint_dict - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.matrix_constraint.matrix_constraint - :show-inheritance: - :members: diff --git a/doc/OnlineDocs/reference/topical/kernel/dict_container.rst b/doc/OnlineDocs/reference/topical/kernel/dict_container.rst index 6e710fa76eb..923fe915b8e 100644 --- a/doc/OnlineDocs/reference/topical/kernel/dict_container.rst +++ b/doc/OnlineDocs/reference/topical/kernel/dict_container.rst @@ -1,8 +1,6 @@ Dict-like Object Storage ======================== -.. autoclass:: pyomo.core.kernel.dict_container.DictContainer - :show-inheritance: - :members: - :inherited-members: - :special-members: +.. autosummary:: + + pyomo.core.kernel.dict_container.DictContainer diff --git a/doc/OnlineDocs/reference/topical/kernel/expression.rst b/doc/OnlineDocs/reference/topical/kernel/expression.rst index b2d4c2d1b35..6ac32ecd7dd 100644 --- a/doc/OnlineDocs/reference/topical/kernel/expression.rst +++ b/doc/OnlineDocs/reference/topical/kernel/expression.rst @@ -9,18 +9,3 @@ Summary pyomo.core.kernel.expression.expression_tuple pyomo.core.kernel.expression.expression_list pyomo.core.kernel.expression.expression_dict - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.expression.expression - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.expression.expression_tuple - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.expression.expression_list - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.expression.expression_dict - :show-inheritance: - :members: diff --git a/doc/OnlineDocs/reference/topical/kernel/heterogeneous_container.rst b/doc/OnlineDocs/reference/topical/kernel/heterogeneous_container.rst index 74dad1d754e..158175af7f1 100644 --- a/doc/OnlineDocs/reference/topical/kernel/heterogeneous_container.rst +++ b/doc/OnlineDocs/reference/topical/kernel/heterogeneous_container.rst @@ -1,6 +1,6 @@ Heterogeneous Object Containers =============================== -.. automodule:: pyomo.core.kernel.heterogeneous_container - :show-inheritance: - :members: +.. autosummary:: + + pyomo.core.kernel.heterogeneous_container diff --git a/doc/OnlineDocs/reference/topical/kernel/homogeneous_container.rst b/doc/OnlineDocs/reference/topical/kernel/homogeneous_container.rst index b722e026dc1..f6dc88b355a 100644 --- a/doc/OnlineDocs/reference/topical/kernel/homogeneous_container.rst +++ b/doc/OnlineDocs/reference/topical/kernel/homogeneous_container.rst @@ -1,6 +1,6 @@ Homogeneous Object Containers ============================= -.. automodule:: pyomo.core.kernel.homogeneous_container - :show-inheritance: - :members: +.. autosummary:: + + pyomo.core.kernel.homogeneous_container diff --git a/doc/OnlineDocs/reference/topical/kernel/list_container.rst b/doc/OnlineDocs/reference/topical/kernel/list_container.rst index b82c6d9c6f0..acd6fe4fabb 100644 --- a/doc/OnlineDocs/reference/topical/kernel/list_container.rst +++ b/doc/OnlineDocs/reference/topical/kernel/list_container.rst @@ -1,8 +1,6 @@ List-like Object Storage ======================== -.. autoclass:: pyomo.core.kernel.list_container.ListContainer - :show-inheritance: - :members: - :inherited-members: - :special-members: +.. autosummary:: + + pyomo.core.kernel.list_container.ListContainer diff --git a/doc/OnlineDocs/reference/topical/kernel/objective.rst b/doc/OnlineDocs/reference/topical/kernel/objective.rst index 77f26d2f441..8f7a5422e9e 100644 --- a/doc/OnlineDocs/reference/topical/kernel/objective.rst +++ b/doc/OnlineDocs/reference/topical/kernel/objective.rst @@ -9,18 +9,3 @@ Summary pyomo.core.kernel.objective.objective_tuple pyomo.core.kernel.objective.objective_list pyomo.core.kernel.objective.objective_dict - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.objective.objective - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.objective.objective_tuple - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.objective.objective_list - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.objective.objective_dict - :show-inheritance: - :members: diff --git a/doc/OnlineDocs/reference/topical/kernel/parameter.rst b/doc/OnlineDocs/reference/topical/kernel/parameter.rst index 212b0cb125e..c09bd6262c3 100644 --- a/doc/OnlineDocs/reference/topical/kernel/parameter.rst +++ b/doc/OnlineDocs/reference/topical/kernel/parameter.rst @@ -10,21 +10,3 @@ Summary pyomo.core.kernel.parameter.parameter_tuple pyomo.core.kernel.parameter.parameter_list pyomo.core.kernel.parameter.parameter_dict - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.parameter.parameter - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.parameter.functional_value - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.parameter.parameter_tuple - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.parameter.parameter_list - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.parameter.parameter_dict - :show-inheritance: - :members: diff --git a/doc/OnlineDocs/reference/topical/kernel/piecewise/piecewise.rst b/doc/OnlineDocs/reference/topical/kernel/piecewise/piecewise.rst index 25c250d6559..4c67621426c 100644 --- a/doc/OnlineDocs/reference/topical/kernel/piecewise/piecewise.rst +++ b/doc/OnlineDocs/reference/topical/kernel/piecewise/piecewise.rst @@ -4,6 +4,7 @@ Single-variate Piecewise Functions Summary ~~~~~~~ .. autosummary:: + pyomo.core.kernel.piecewise_library.transforms.piecewise pyomo.core.kernel.piecewise_library.transforms.PiecewiseLinearFunction pyomo.core.kernel.piecewise_library.transforms.TransformedPiecewiseLinearFunction @@ -16,38 +17,3 @@ Summary pyomo.core.kernel.piecewise_library.transforms.piecewise_dlog pyomo.core.kernel.piecewise_library.transforms.piecewise_log -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autofunction:: pyomo.core.kernel.piecewise_library.transforms.piecewise -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.PiecewiseLinearFunction - :show-inheritance: - :special-members: __call__ - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.TransformedPiecewiseLinearFunction - :show-inheritance: - :special-members: __call__ - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_convex - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_sos2 - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_dcc - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_cc - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_mc - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_inc - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_dlog - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_log - :show-inheritance: - :members: diff --git a/doc/OnlineDocs/reference/topical/kernel/piecewise/piecewise_nd.rst b/doc/OnlineDocs/reference/topical/kernel/piecewise/piecewise_nd.rst index e5c71a4ec15..057f3590a72 100644 --- a/doc/OnlineDocs/reference/topical/kernel/piecewise/piecewise_nd.rst +++ b/doc/OnlineDocs/reference/topical/kernel/piecewise/piecewise_nd.rst @@ -4,22 +4,9 @@ Multi-variate Piecewise Functions Summary ~~~~~~~ .. autosummary:: + pyomo.core.kernel.piecewise_library.transforms_nd.piecewise_nd pyomo.core.kernel.piecewise_library.transforms_nd.PiecewiseLinearFunctionND pyomo.core.kernel.piecewise_library.transforms_nd.TransformedPiecewiseLinearFunctionND pyomo.core.kernel.piecewise_library.transforms_nd.piecewise_nd_cc -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autofunction:: pyomo.core.kernel.piecewise_library.transforms_nd.piecewise_nd -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms_nd.PiecewiseLinearFunctionND - :show-inheritance: - :special-members: __call__ - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms_nd.TransformedPiecewiseLinearFunctionND - :show-inheritance: - :special-members: __call__ - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms_nd.piecewise_nd_cc - :show-inheritance: - :members: diff --git a/doc/OnlineDocs/reference/topical/kernel/piecewise/util.rst b/doc/OnlineDocs/reference/topical/kernel/piecewise/util.rst index 52b7b1de8f7..6b979ff42ff 100644 --- a/doc/OnlineDocs/reference/topical/kernel/piecewise/util.rst +++ b/doc/OnlineDocs/reference/topical/kernel/piecewise/util.rst @@ -1,6 +1,6 @@ Utilities for Piecewise Functions ================================= -.. automodule:: pyomo.core.kernel.piecewise_library.util - :show-inheritance: - :members: +.. autosummary:: + + pyomo.core.kernel.piecewise_library.util diff --git a/doc/OnlineDocs/reference/topical/kernel/sos.rst b/doc/OnlineDocs/reference/topical/kernel/sos.rst index 0f3f5fedf54..edb463ea1da 100644 --- a/doc/OnlineDocs/reference/topical/kernel/sos.rst +++ b/doc/OnlineDocs/reference/topical/kernel/sos.rst @@ -12,19 +12,3 @@ Summary pyomo.core.kernel.sos.sos_list pyomo.core.kernel.sos.sos_dict -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.sos.sos - :show-inheritance: - :members: -.. autofunction:: pyomo.core.kernel.sos.sos1 -.. autofunction:: pyomo.core.kernel.sos.sos2 -.. autoclass:: pyomo.core.kernel.sos.sos_tuple - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.sos.sos_list - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.sos.sos_dict - :show-inheritance: - :members: diff --git a/doc/OnlineDocs/reference/topical/kernel/suffix.rst b/doc/OnlineDocs/reference/topical/kernel/suffix.rst index d833f56daa9..f0f8f48a292 100644 --- a/doc/OnlineDocs/reference/topical/kernel/suffix.rst +++ b/doc/OnlineDocs/reference/topical/kernel/suffix.rst @@ -1,6 +1,6 @@ Suffixes ======== -.. automodule:: pyomo.core.kernel.suffix - :show-inheritance: - :members: +.. autosummary:: + + pyomo.core.kernel.suffix diff --git a/doc/OnlineDocs/reference/topical/kernel/tuple_container.rst b/doc/OnlineDocs/reference/topical/kernel/tuple_container.rst index 8a2798753c4..eb052d9ffb6 100644 --- a/doc/OnlineDocs/reference/topical/kernel/tuple_container.rst +++ b/doc/OnlineDocs/reference/topical/kernel/tuple_container.rst @@ -1,8 +1,6 @@ Tuple-like Object Storage ========================= -.. autoclass:: pyomo.core.kernel.tuple_container.TupleContainer - :show-inheritance: - :members: - :inherited-members: - :special-members: +.. autosummary:: + + pyomo.core.kernel.tuple_container.TupleContainer diff --git a/doc/OnlineDocs/reference/topical/kernel/variable.rst b/doc/OnlineDocs/reference/topical/kernel/variable.rst index f743cee4003..937ebae45dc 100644 --- a/doc/OnlineDocs/reference/topical/kernel/variable.rst +++ b/doc/OnlineDocs/reference/topical/kernel/variable.rst @@ -9,18 +9,3 @@ Summary pyomo.core.kernel.variable.variable_tuple pyomo.core.kernel.variable.variable_list pyomo.core.kernel.variable.variable_dict - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.variable.variable - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.variable.variable_tuple - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.variable.variable_list - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.variable.variable_dict - :show-inheritance: - :members: diff --git a/doc/OnlineDocs/reference/topical/solvers/cplex_persistent.rst b/doc/OnlineDocs/reference/topical/solvers/cplex_persistent.rst index ee28ecda5e5..e0d34d0f51d 100644 --- a/doc/OnlineDocs/reference/topical/solvers/cplex_persistent.rst +++ b/doc/OnlineDocs/reference/topical/solvers/cplex_persistent.rst @@ -1,7 +1,6 @@ CPLEXPersistent ================ -.. autoclass:: pyomo.solvers.plugins.solvers.cplex_persistent.CPLEXPersistent - :members: - :inherited-members: - :show-inheritance: +.. autosummary:: + + pyomo.solvers.plugins.solvers.cplex_persistent.CPLEXPersistent diff --git a/doc/OnlineDocs/reference/topical/solvers/gams.rst b/doc/OnlineDocs/reference/topical/solvers/gams.rst index f36de5d9e01..ca9a2a55d09 100644 --- a/doc/OnlineDocs/reference/topical/solvers/gams.rst +++ b/doc/OnlineDocs/reference/topical/solvers/gams.rst @@ -8,28 +8,24 @@ GAMSShell Solver .. autosummary:: + GAMSShell GAMSShell.available GAMSShell.executable GAMSShell.solve GAMSShell.version GAMSShell.warm_start_capable -.. autoclass:: GAMSShell - :members: - GAMSDirect Solver ----------------- .. autosummary:: + GAMSDirect GAMSDirect.available GAMSDirect.solve GAMSDirect.version GAMSDirect.warm_start_capable -.. autoclass:: GAMSDirect - :members: - .. currentmodule:: pyomo.repn.plugins.gams_writer GAMS Writer @@ -39,5 +35,6 @@ This class is most commonly accessed and called upon via model.write("filename.gms", ...), but is also utilized by the GAMS solver interfaces. -.. autoclass:: ProblemWriter_gams - :members: __call__ +.. autosummary:: + + ProblemWriter_gams diff --git a/doc/OnlineDocs/reference/topical/solvers/gurobi_direct.rst b/doc/OnlineDocs/reference/topical/solvers/gurobi_direct.rst index 21cb79e5531..bbb61143a94 100644 --- a/doc/OnlineDocs/reference/topical/solvers/gurobi_direct.rst +++ b/doc/OnlineDocs/reference/topical/solvers/gurobi_direct.rst @@ -3,6 +3,14 @@ GurobiDirect .. currentmodule:: pyomo.solvers.plugins.solvers.gurobi_direct + +Interface +--------- + +.. autosummary:: + + GurobiDirect + Methods ------- @@ -14,5 +22,3 @@ Methods GurobiDirect.solve GurobiDirect.version -.. autoclass:: GurobiDirect - :members: available, close, close_global, solve, version diff --git a/doc/OnlineDocs/reference/topical/solvers/gurobi_persistent.rst b/doc/OnlineDocs/reference/topical/solvers/gurobi_persistent.rst index 2472599c1ed..5832c8f8b9f 100644 --- a/doc/OnlineDocs/reference/topical/solvers/gurobi_persistent.rst +++ b/doc/OnlineDocs/reference/topical/solvers/gurobi_persistent.rst @@ -3,6 +3,13 @@ GurobiPersistent .. currentmodule:: pyomo.solvers.plugins.solvers.gurobi_persistent +Interface +--------- + +.. autosummary:: + + GurobiPersistent + Methods ------- @@ -32,8 +39,3 @@ Methods GurobiPersistent.update_var GurobiPersistent.version GurobiPersistent.write - -.. autoclass:: GurobiPersistent - :members: - :inherited-members: - :show-inheritance: diff --git a/doc/OnlineDocs/reference/topical/solvers/xpress_persistent.rst b/doc/OnlineDocs/reference/topical/solvers/xpress_persistent.rst index 2a98b4a09db..d8721a0931a 100644 --- a/doc/OnlineDocs/reference/topical/solvers/xpress_persistent.rst +++ b/doc/OnlineDocs/reference/topical/solvers/xpress_persistent.rst @@ -1,7 +1,6 @@ XpressPersistent ================ -.. autoclass:: pyomo.solvers.plugins.solvers.xpress_persistent.XpressPersistent - :members: - :inherited-members: - :show-inheritance: +.. autosummary:: + + pyomo.solvers.plugins.solvers.xpress_persistent.XpressPersistent diff --git a/pyomo/__future__.py b/pyomo/__future__.py index d298e12cab6..8f6af01f503 100644 --- a/pyomo/__future__.py +++ b/pyomo/__future__.py @@ -26,8 +26,6 @@ solver_factory -.. autofunction:: solver_factory - """ diff --git a/pyomo/contrib/pynumero/sparse/block_vector.py b/pyomo/contrib/pynumero/sparse/block_vector.py index b636dd74203..9e68dfce23e 100644 --- a/pyomo/contrib/pynumero/sparse/block_vector.py +++ b/pyomo/contrib/pynumero/sparse/block_vector.py @@ -8,18 +8,176 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -""" -The pyomo.contrib.pynumero.sparse.block_vector module includes methods that extend -linear algebra operations in numpy for case of structured problems -where linear algebra operations present an inherent block structure. -This interface consider vectors of the form: +"""Implimentation of a general "block vector" + + +The `pyomo.contrib.pynumero.sparse.block_vector` module includes methods +that extend linear algebra operations in numpy for case of structured +problems where linear algebra operations present an inherent block +structure. This interface consider vectors of the form: -v = [v_1, v_2, v_3, ... , v_n] +.. math:: -where v_i are numpy arrays of dimension 1 + v = [v_1, v_2, v_3, ... , v_n] + +where `v_i` are numpy arrays of dimension 1 .. rubric:: Contents +Methods specific to :py:class:`pyomo.contrib.pynumero.sparse.block_vector.BlockVector`: + + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_block` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.block_sizes` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block_size` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.is_block_defined` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyfrom` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyto` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copy_structure` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_blocks` + * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.pprint` + +Attributes specific to :py:class:`pyomo.contrib.pynumero.sparse.block_vector.BlockVector`: + + * :py:attr:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.nblocks` + * :py:attr:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.bshape` + * :py:attr:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.has_none` + + +NumPy compatible methods: + + * :py:meth:`numpy.ndarray.dot` + * :py:meth:`numpy.ndarray.sum` + * :py:meth:`numpy.ndarray.all` + * :py:meth:`numpy.ndarray.any` + * :py:meth:`numpy.ndarray.max` + * :py:meth:`numpy.ndarray.astype` + * :py:meth:`numpy.ndarray.clip` + * :py:meth:`numpy.ndarray.compress` + * :py:meth:`numpy.ndarray.conj` + * :py:meth:`numpy.ndarray.conjugate` + * :py:meth:`numpy.ndarray.nonzero` + * :py:meth:`numpy.ndarray.ptp` + * :py:meth:`numpy.ndarray.round` + * :py:meth:`numpy.ndarray.std` + * :py:meth:`numpy.ndarray.var` + * :py:meth:`numpy.ndarray.tofile` + * :py:meth:`numpy.ndarray.min` + * :py:meth:`numpy.ndarray.mean` + * :py:meth:`numpy.ndarray.prod` + * :py:meth:`numpy.ndarray.fill` + * :py:meth:`numpy.ndarray.tolist` + * :py:meth:`numpy.ndarray.flatten` + * :py:meth:`numpy.ndarray.ravel` + * :py:meth:`numpy.ndarray.argmax` + * :py:meth:`numpy.ndarray.argmin` + * :py:meth:`numpy.ndarray.cumprod` + * :py:meth:`numpy.ndarray.cumsum` + * :py:meth:`numpy.ndarray.copy` + +For example, + +.. code-block:: python + + >>> import numpy as np + >>> from pyomo.contrib.pynumero.sparse import BlockVector + >>> v = BlockVector(2) + >>> v.set_block(0, np.random.normal(size=100)) + >>> v.set_block(1, np.random.normal(size=30)) + >>> avg = v.mean() + +NumPy compatible functions: + + * :py:func:`numpy.log10` + * :py:func:`numpy.sin` + * :py:func:`numpy.cos` + * :py:func:`numpy.exp` + * :py:func:`numpy.ceil` + * :py:func:`numpy.floor` + * :py:func:`numpy.tan` + * :py:func:`numpy.arctan` + * :py:func:`numpy.arcsin` + * :py:func:`numpy.arccos` + * :py:func:`numpy.sinh` + * :py:func:`numpy.cosh` + * :py:func:`numpy.abs` + * :py:func:`numpy.tanh` + * :py:func:`numpy.arccosh` + * :py:func:`numpy.arcsinh` + * :py:func:`numpy.arctanh` + * :py:func:`numpy.fabs` + * :py:func:`numpy.sqrt` + * :py:func:`numpy.log` + * :py:func:`numpy.log2` + * :py:func:`numpy.absolute` + * :py:func:`numpy.isfinite` + * :py:func:`numpy.isinf` + * :py:func:`numpy.isnan` + * :py:func:`numpy.log1p` + * :py:func:`numpy.logical_not` + * :py:func:`numpy.expm1` + * :py:func:`numpy.exp2` + * :py:func:`numpy.sign` + * :py:func:`numpy.rint` + * :py:func:`numpy.square` + * :py:func:`numpy.positive` + * :py:func:`numpy.negative` + * :py:func:`numpy.rad2deg` + * :py:func:`numpy.deg2rad` + * :py:func:`numpy.conjugate` + * :py:func:`numpy.reciprocal` + * :py:func:`numpy.signbit` + * :py:func:`numpy.add` + * :py:func:`numpy.multiply` + * :py:func:`numpy.divide` + * :py:func:`numpy.subtract` + * :py:func:`numpy.greater` + * :py:func:`numpy.greater_equal` + * :py:func:`numpy.less` + * :py:func:`numpy.less_equal` + * :py:func:`numpy.not_equal` + * :py:func:`numpy.maximum` + * :py:func:`numpy.minimum` + * :py:func:`numpy.fmax` + * :py:func:`numpy.fmin` + * :py:func:`numpy.equal` + * :py:func:`numpy.logical_and` + * :py:func:`numpy.logical_or` + * :py:func:`numpy.logical_xor` + * :py:func:`numpy.logaddexp` + * :py:func:`numpy.logaddexp2` + * :py:func:`numpy.remainder` + * :py:func:`numpy.heaviside` + * :py:func:`numpy.hypot` + +For example, + +.. code-block:: python + + >>> import numpy as np + >>> from pyomo.contrib.pynumero.sparse import BlockVector + >>> v = BlockVector(2) + >>> v.set_block(0, np.random.normal(size=100)) + >>> v.set_block(1, np.random.normal(size=30)) + >>> inf_norm = np.max(np.abs(v)) + +.. autosummary:: + + pyomo.contrib.pynumero.sparse.block_vector.BlockVector + pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_block + pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block + pyomo.contrib.pynumero.sparse.block_vector.BlockVector.block_sizes + pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block_size + pyomo.contrib.pynumero.sparse.block_vector.BlockVector.is_block_defined + pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyfrom + pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyto + pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copy_structure + pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_blocks + pyomo.contrib.pynumero.sparse.block_vector.BlockVector.pprint + pyomo.contrib.pynumero.sparse.block_vector.BlockVector.nblocks + pyomo.contrib.pynumero.sparse.block_vector.BlockVector.bshape + pyomo.contrib.pynumero.sparse.block_vector.BlockVector.has_none + """ import operator From 2035cb435677690a15a035723852bec4d8c34824 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 29 Sep 2024 21:01:52 -0600 Subject: [PATCH 2458/3044] Ensure PKGCONFIG is installed on Windows --- .github/workflows/test_branches.yml | 2 ++ .github/workflows/test_pr_and_main.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 92c47b2d64b..9a0c058842a 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -215,6 +215,8 @@ jobs: if: matrix.TARGET == 'win' run: | echo "SETUPTOOLS_USE_DISTUTILS=local" >> $GITHUB_ENV + run: | + choco install pkgconfiglite - name: Set up Python ${{ matrix.python }} if: matrix.PYENV == 'pip' diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 34efa4a029b..f08784d76df 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -243,6 +243,8 @@ jobs: if: matrix.TARGET == 'win' run: | echo "SETUPTOOLS_USE_DISTUTILS=local" >> $GITHUB_ENV + run: | + choco install pkgconfiglite - name: Set up Python ${{ matrix.python }} if: matrix.PYENV == 'pip' From e1815612dd3112ea12d6e6dddf0ab52701ea915f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 29 Sep 2024 21:02:31 -0600 Subject: [PATCH 2459/3044] Reduce diffs between test drivers --- .github/workflows/test_branches.yml | 6 ++++++ .github/workflows/test_pr_and_main.yml | 17 +++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 9a0c058842a..e5aba64a38a 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -75,6 +75,12 @@ jobs: other: [""] category: [""] + # win/3.8 conda builds no longer work due to environment not being able + # to resolve. We are skipping it now. + exclude: + - os: windows-latest + python: 3.8 + include: - os: ubuntu-latest python: '3.12' diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index f08784d76df..33f21f9b1d1 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -77,6 +77,7 @@ jobs: exclude: - os: windows-latest python: 3.8 + include: - os: ubuntu-latest TARGET: linux @@ -108,14 +109,6 @@ jobs: PYENV: conda PACKAGES: openmpi mpi4py - - os: ubuntu-latest - python: '3.11' - other: /singletest - category: "-m 'neos or importtest'" - skip_doctest: 1 - TARGET: linux - PYENV: pip - - os: ubuntu-latest python: '3.10' other: /cython @@ -132,6 +125,14 @@ jobs: TARGET: win PYENV: pip + - os: ubuntu-latest + python: '3.11' + other: /singletest + category: "-m 'neos or importtest'" + skip_doctest: 1 + TARGET: linux + PYENV: pip + - os: ubuntu-latest python: 3.8 other: /slim From 7c6b32c8aeb8fbab686c4cd026fd297ecadc3e29 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 29 Sep 2024 21:06:22 -0600 Subject: [PATCH 2460/3044] Fix redundant 'run' definition --- .github/workflows/test_branches.yml | 1 - .github/workflows/test_pr_and_main.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index e5aba64a38a..921520e5720 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -221,7 +221,6 @@ jobs: if: matrix.TARGET == 'win' run: | echo "SETUPTOOLS_USE_DISTUTILS=local" >> $GITHUB_ENV - run: | choco install pkgconfiglite - name: Set up Python ${{ matrix.python }} diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 33f21f9b1d1..a006f6516f9 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -244,7 +244,6 @@ jobs: if: matrix.TARGET == 'win' run: | echo "SETUPTOOLS_USE_DISTUTILS=local" >> $GITHUB_ENV - run: | choco install pkgconfiglite - name: Set up Python ${{ matrix.python }} From a166b5a77500af9dbc132d0d2c0f8eb204cf172d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 30 Sep 2024 12:21:05 -0600 Subject: [PATCH 2461/3044] Simplify toctree entries in library reference --- doc/OnlineDocs/_templates/recursive-base.rst | 7 +++ doc/OnlineDocs/_templates/recursive-class.rst | 4 +- .../_templates/recursive-module.rst | 53 +++++++++++-------- 3 files changed, 41 insertions(+), 23 deletions(-) create mode 100644 doc/OnlineDocs/_templates/recursive-base.rst diff --git a/doc/OnlineDocs/_templates/recursive-base.rst b/doc/OnlineDocs/_templates/recursive-base.rst new file mode 100644 index 00000000000..b45d3894277 --- /dev/null +++ b/doc/OnlineDocs/_templates/recursive-base.rst @@ -0,0 +1,7 @@ +{{ name | escape | underline}} + +({{ objtype }} from :py:mod:`{{ module }}`) + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} diff --git a/doc/OnlineDocs/_templates/recursive-class.rst b/doc/OnlineDocs/_templates/recursive-class.rst index 2fd769c7d7b..38b20cdac01 100644 --- a/doc/OnlineDocs/_templates/recursive-class.rst +++ b/doc/OnlineDocs/_templates/recursive-class.rst @@ -1,4 +1,6 @@ -{{ fullname | escape | underline}} +{{ name | escape | underline}} + +(class from :py:mod:`{{ module }}`) .. currentmodule:: {{ module }} diff --git a/doc/OnlineDocs/_templates/recursive-module.rst b/doc/OnlineDocs/_templates/recursive-module.rst index 19b64e2d8bd..df8b6b01807 100644 --- a/doc/OnlineDocs/_templates/recursive-module.rst +++ b/doc/OnlineDocs/_templates/recursive-module.rst @@ -1,68 +1,77 @@ -{{ fullname | escape | underline}} +{% if fullname == 'pyomo' %} +Library Reference +================= +{% else %} +{{ name | escape | underline}} +{% endif %} .. automodule:: {{ fullname }} + :undoc-members: {% block attributes %} - {% if attributes %} + {%- if attributes %} .. rubric:: {{ _('Module Attributes') }} .. autosummary:: :toctree: + :template: recursive-base.rst {% for item in attributes %} {{ item }} {%- endfor %} {% endif %} - {% endblock %} + {%- endblock %} - {% block functions %} - {% if functions %} + {%- block functions %} + {%- if functions %} .. rubric:: {{ _('Functions') }} .. autosummary:: :toctree: + :template: recursive-base.rst {% for item in functions %} {{ item }} {%- endfor %} {% endif %} - {% endblock %} + {%- endblock %} - {% block classes %} - {% if classes %} + {%- block classes %} + {%- if classes %} .. rubric:: {{ _('Classes') }} .. autosummary:: - :toctree: - :template: recursive-class.rst + :toctree: + :template: recursive-class.rst {% for item in classes %} {{ item }} {%- endfor %} {% endif %} - {% endblock %} + {%- endblock %} - {% block exceptions %} - {% if exceptions %} + {%- block exceptions %} + {%- if exceptions %} .. rubric:: {{ _('Exceptions') }} .. autosummary:: :toctree: + :template: recursive-class.rst {% for item in exceptions %} {{ item }} {%- endfor %} {% endif %} - {% endblock %} + {%- endblock %} -{% block modules %} -{% if modules %} +{%- block modules %} +{%- if modules %} .. rubric:: Modules .. autosummary:: :toctree: :template: recursive-module.rst :recursive: - {% for item in modules %} - {% if '.test' not in item and '.example' not in item %} - {{ item }} - {% endif %} - {%- endfor %} +{% for item in modules %} +{% if '.test' not in item and '.example' not in item %} + {{ item }} +{% endif %} +{%- endfor %} {% endif %} -{% endblock %} +{%- endblock %} From a39a3ee1b7a58920eb1f0e6e33be03c9c3c6ba69 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:43:35 -0600 Subject: [PATCH 2462/3044] Pin setuptools to <74 for windows --- .github/workflows/test_pr_and_main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index a006f6516f9..ad989a08482 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -382,6 +382,10 @@ jobs: done echo "" echo "*** Install Pyomo dependencies ***" + # For windows, cannot use newer setuptools because of APPSI compilation issues + if test "${{matrix.TARGET}}" == 'win'; then + CONDA_DEPENDENCIES = "$CONDA_DEPENDENCIES setuptools<74.0.0" + fi # Note: this will fail the build if any installation fails (or # possibly if it outputs messages to stderr) conda install --update-deps -q -y $CONDA_DEPENDENCIES From fac372249bc8c7ed237fd0029917bb00edd78f7c Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:44:13 -0600 Subject: [PATCH 2463/3044] Pinning to setuptools<74 for win --- .github/workflows/test_branches.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 921520e5720..89a5786c12c 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -359,6 +359,10 @@ jobs: done echo "" echo "*** Install Pyomo dependencies ***" + # For windows, cannot use newer setuptools because of APPSI compilation issues + if test "${{matrix.TARGET}}" == 'win'; then + CONDA_DEPENDENCIES = "$CONDA_DEPENDENCIES setuptools<74.0.0" + fi # Note: this will fail the build if any installation fails (or # possibly if it outputs messages to stderr) conda install --update-deps -q -y $CONDA_DEPENDENCIES From 6b8420b530f7d5b054e25573246f87c430fb9df1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:56:28 -0600 Subject: [PATCH 2464/3044] Accidental extra spaces --- .github/workflows/test_pr_and_main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index ad989a08482..b21d8a1c924 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -384,7 +384,7 @@ jobs: echo "*** Install Pyomo dependencies ***" # For windows, cannot use newer setuptools because of APPSI compilation issues if test "${{matrix.TARGET}}" == 'win'; then - CONDA_DEPENDENCIES = "$CONDA_DEPENDENCIES setuptools<74.0.0" + CONDA_DEPENDENCIES="$CONDA_DEPENDENCIES setuptools<74.0.0" fi # Note: this will fail the build if any installation fails (or # possibly if it outputs messages to stderr) From 14e6aefa897f981bc2909bda4f8aeed01aeb4cb2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Mon, 30 Sep 2024 13:56:49 -0600 Subject: [PATCH 2465/3044] Extra spaces --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 89a5786c12c..0e4284cc64d 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -361,7 +361,7 @@ jobs: echo "*** Install Pyomo dependencies ***" # For windows, cannot use newer setuptools because of APPSI compilation issues if test "${{matrix.TARGET}}" == 'win'; then - CONDA_DEPENDENCIES = "$CONDA_DEPENDENCIES setuptools<74.0.0" + CONDA_DEPENDENCIES="$CONDA_DEPENDENCIES setuptools<74.0.0" fi # Note: this will fail the build if any installation fails (or # possibly if it outputs messages to stderr) From 7408778b6ed76c59dbdb138ecef98d738d1dd23c Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:42:02 -0600 Subject: [PATCH 2466/3044] Switch to macos-latest --- .github/workflows/test_pr_and_main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index b21d8a1c924..33aacaa9e35 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -67,7 +67,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-13, windows-latest] + os: [ubuntu-latest, macos-latest, windows-latest] python: [ 3.8, 3.9, '3.10', '3.11', '3.12' ] other: [""] category: [""] @@ -83,7 +83,7 @@ jobs: TARGET: linux PYENV: pip - - os: macos-13 + - os: macos-latest TARGET: osx PYENV: pip From 21d1d2e8bb6618e136977242eeb762be26c23cbb Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Mon, 30 Sep 2024 14:42:24 -0600 Subject: [PATCH 2467/3044] Switch to macos-latest --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 0e4284cc64d..c1029ff3d7b 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -87,7 +87,7 @@ jobs: TARGET: linux PYENV: pip - - os: macos-13 + - os: macos-latest python: '3.10' TARGET: osx PYENV: pip From 9552b6986cb37cc76b59ad789f298374e5bcc1e7 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 30 Sep 2024 20:51:08 -0600 Subject: [PATCH 2468/3044] Removing unreachable code in the kernel Delaunay test --- .../core/tests/unit/kernel/test_piecewise.py | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/pyomo/core/tests/unit/kernel/test_piecewise.py b/pyomo/core/tests/unit/kernel/test_piecewise.py index 3d9cf66e39c..7103ee39856 100644 --- a/pyomo/core/tests/unit/kernel/test_piecewise.py +++ b/pyomo/core/tests/unit/kernel/test_piecewise.py @@ -209,19 +209,15 @@ def test_generate_delaunay(self): vlist.append(variable(lb=0, ub=1)) vlist.append(variable(lb=1, ub=2)) vlist.append(variable(lb=2, ub=3)) - if not (util.numpy_available and util.scipy_available): - with self.assertRaises(ImportError): - util.generate_delaunay(vlist) - else: - tri = util.generate_delaunay(vlist, num=2) - self.assertTrue(isinstance(tri, util.scipy.spatial.Delaunay)) - self.assertEqual(len(tri.simplices), 6) - self.assertEqual(len(tri.points), 8) - - tri = util.generate_delaunay(vlist, num=3) - self.assertTrue(isinstance(tri, util.scipy.spatial.Delaunay)) - self.assertEqual(len(tri.simplices), 62) - self.assertEqual(len(tri.points), 27) + tri = util.generate_delaunay(vlist, num=2) + self.assertTrue(isinstance(tri, util.scipy.spatial.Delaunay)) + self.assertEqual(len(tri.simplices), 6) + self.assertEqual(len(tri.points), 8) + + tri = util.generate_delaunay(vlist, num=3) + self.assertTrue(isinstance(tri, util.scipy.spatial.Delaunay)) + self.assertEqual(len(tri.simplices), 62) + self.assertEqual(len(tri.points), 27) # # Check cases where not all variables are bounded From 6a4730e6d9fd726fbd84ec1196efe4e4e33f40c7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 1 Oct 2024 11:49:19 -0600 Subject: [PATCH 2469/3044] make clean: remove autogenerated API files --- doc/OnlineDocs/Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/OnlineDocs/Makefile b/doc/OnlineDocs/Makefile index 962609def61..264799b6fa9 100644 --- a/doc/OnlineDocs/Makefile +++ b/doc/OnlineDocs/Makefile @@ -7,6 +7,7 @@ SPHINXBUILD = sphinx-build SPHINXPROJ = Pyomo SOURCEDIR = . BUILDDIR = _build +APIDIR = api # Put it first so that "make" without argument is like "make help". help: @@ -23,6 +24,7 @@ clean: @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) @echo "Removing *.spy, *.out" @find . -name \*.spy -delete + @if test -d "$(SOURCEDIR)/${APIDIR}"; then rm -r "$(SOURCEDIR)/${APIDIR}"; fi rebuild: @$(MAKE) clean From b89fd054f6ac14676d13dcf2a6158eaefe966e1d Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 1 Oct 2024 14:01:19 -0600 Subject: [PATCH 2470/3044] Fix typos, one of which really doesn't seem like a typo --- pyomo/core/kernel/base.py | 2 +- pyomo/core/tests/unit/test_sets.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/core/kernel/base.py b/pyomo/core/kernel/base.py index d599c76f6a1..0653868e109 100644 --- a/pyomo/core/kernel/base.py +++ b/pyomo/core/kernel/base.py @@ -156,7 +156,7 @@ def getname( Args: fully_qualified (bool): Generate a full name by - iterating through all anscestor containers. + iterating through all ancestor containers. Default is :const:`False`. convert (function): A function that converts a storage key into a string diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index 52c4523eaba..e9f96a417f4 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2979,7 +2979,7 @@ def test_initialize_and_clone_from_dict_keys(self): # # While deepcopying a model is generally not supported, this is # an easy way to ensure that this simple model is cleanly - # clonable. + # able to be cloned. ref = """1 Set Declarations INDEX : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members From 2acc2d1734e971144017be4c7ffa2b4807525d20 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 1 Oct 2024 14:06:36 -0600 Subject: [PATCH 2471/3044] Not testing number of simplices when we do a Delaunay triangulation over a grid (because the triangulation is not unique)--just checking that we used the points we expected and that we did indeed get simplices. --- pyomo/core/tests/unit/kernel/test_piecewise.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyomo/core/tests/unit/kernel/test_piecewise.py b/pyomo/core/tests/unit/kernel/test_piecewise.py index 7103ee39856..e376bdce8b3 100644 --- a/pyomo/core/tests/unit/kernel/test_piecewise.py +++ b/pyomo/core/tests/unit/kernel/test_piecewise.py @@ -216,8 +216,10 @@ def test_generate_delaunay(self): tri = util.generate_delaunay(vlist, num=3) self.assertTrue(isinstance(tri, util.scipy.spatial.Delaunay)) - self.assertEqual(len(tri.simplices), 62) - self.assertEqual(len(tri.points), 27) + # we got some simplices + self.assertTrue(len(tri.simplices) > 1) + # all the given points are accounted for + self.assertEqual(len(tri.points) + len(tri.coplanar), 27) # # Check cases where not all variables are bounded From b099501bc3db93f9b62b17d5e9b594ef57b1fadd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 1 Oct 2024 14:23:35 -0600 Subject: [PATCH 2472/3044] Update references to BiBTeX 'alpha' format --- .../contributed_packages/parmest/driver.rst | 2 +- .../explanation/analysis/parmest/driver.rst | 2 +- .../explanation/analysis/parmest/examples.rst | 4 +- .../explanation/modeling/gdp/index.rst | 24 +++-- .../explanation/modeling/gdp/solving.rst | 46 +++++---- .../modeling/math_programming/expressions.rst | 2 +- .../modeling/math_programming/suffixes.rst | 2 +- doc/OnlineDocs/explanation/solvers/pyros.rst | 2 +- .../pyomo_overview/math_modeling.rst | 2 +- doc/OnlineDocs/reference/bibliography.rst | 93 ++++++++++++++----- pyomo/contrib/benders/benders_cuts.py | 8 +- .../transform/disaggregated_logarithmic.py | 26 +++--- pyomo/gdp/plugins/partition_disjuncts.py | 9 +- 13 files changed, 131 insertions(+), 91 deletions(-) diff --git a/doc/Archive/contributed_packages/parmest/driver.rst b/doc/Archive/contributed_packages/parmest/driver.rst index 5881d2748f9..866e50205bb 100644 --- a/doc/Archive/contributed_packages/parmest/driver.rst +++ b/doc/Archive/contributed_packages/parmest/driver.rst @@ -6,7 +6,7 @@ Parameter Estimation Parameter Estimation using parmest requires a Pyomo model, experimental data which defines multiple scenarios, and parameters (thetas) to estimate. parmest uses Pyomo [PyomoBookII]_ and (optionally) -mpi-sppy [mpisppy]_ to solve a +mpi-sppy [KMM+23]_ to solve a two-stage stochastic programming problem, where the experimental data is used to create a scenario tree. The objective function needs to be written with the Pyomo Expression for first stage cost diff --git a/doc/OnlineDocs/explanation/analysis/parmest/driver.rst b/doc/OnlineDocs/explanation/analysis/parmest/driver.rst index 5881d2748f9..866e50205bb 100644 --- a/doc/OnlineDocs/explanation/analysis/parmest/driver.rst +++ b/doc/OnlineDocs/explanation/analysis/parmest/driver.rst @@ -6,7 +6,7 @@ Parameter Estimation Parameter Estimation using parmest requires a Pyomo model, experimental data which defines multiple scenarios, and parameters (thetas) to estimate. parmest uses Pyomo [PyomoBookII]_ and (optionally) -mpi-sppy [mpisppy]_ to solve a +mpi-sppy [KMM+23]_ to solve a two-stage stochastic programming problem, where the experimental data is used to create a scenario tree. The objective function needs to be written with the Pyomo Expression for first stage cost diff --git a/doc/OnlineDocs/explanation/analysis/parmest/examples.rst b/doc/OnlineDocs/explanation/analysis/parmest/examples.rst index 794a01046cb..5c6f9096832 100644 --- a/doc/OnlineDocs/explanation/analysis/parmest/examples.rst +++ b/doc/OnlineDocs/explanation/analysis/parmest/examples.rst @@ -6,8 +6,8 @@ Examples Examples can be found in `pyomo/contrib/parmest/examples` and include: * Reactor design example [PyomoBookII]_ -* Semibatch example [SemiBatch]_ -* Rooney Biegler example [RooneyBiegler]_ +* Semibatch example [AM00]_ +* Rooney Biegler example [RB01]_ Each example includes a Python file that contains the Pyomo model and a Python file to run parameter estimation. diff --git a/doc/OnlineDocs/explanation/modeling/gdp/index.rst b/doc/OnlineDocs/explanation/modeling/gdp/index.rst index 0c8529c60cb..b0aa7224d7a 100644 --- a/doc/OnlineDocs/explanation/modeling/gdp/index.rst +++ b/doc/OnlineDocs/explanation/modeling/gdp/index.rst @@ -9,7 +9,11 @@ Generalized Disjunctive Programming :align: right :class: no-scaled-link -The Pyomo.GDP modeling extension\ [#gdp-main-paper]_ provides support for Generalized Disjunctive Programming (GDP)\ [#gdp]_, an extension of Disjunctive Programming\ [#dp]_ from the operations research community to include nonlinear relationships. The classic form for a GDP is given by: +The Pyomo.GDP modeling extension [PyomoGDP-pse-paper]_ +[PyomoGDP-paper]_ provides support for Generalized Disjunctive +Programming (GDP) [RG94]_, an extension of Disjunctive Programming +[Bal85]_ from the operations research community to include nonlinear +relationships. The classic form for a GDP is given by: .. math:: @@ -32,9 +36,12 @@ Here, we have the minimization of an objective :math:`obj` subject to global lin These conditional constraints are collected into disjuncts :math:`D_k`, organized into disjunctions :math:`K`. Finally, there are logical propositions :math:`\Omega(Y) = True`. Decision/state variables can be continuous :math:`x`, Boolean :math:`Y`, and/or integer :math:`z`. -GDP is useful to model discrete decisions that have implications on the system behavior\ [#gdpreview]_. -For example, in process design, a disjunction may model the choice between processes A and B. -If A is selected, then its associated equations and inequalities will apply; otherwise, if B is selected, then its respective constraints should be enforced. +GDP is useful to model discrete decisions that have implications on the +system behavior [GT13]_. For example, in process design, a +disjunction may model the choice between processes A and B. If A is +selected, then its associated equations and inequalities will apply; +otherwise, if B is selected, then its respective constraints should be +enforced. Modelers often ask to model if-then-else relationships. These can be expressed as a disjunction as follows: @@ -68,12 +75,3 @@ The following sections describe the key concepts, modeling, and solution approac modeling solving -Literature References -===================== -.. [#gdp-main-paper] Chen, Q., Johnson, E. S., Bernal, D. E., Valentin, R., Kale, S., Bates, J., Siirola, J. D. and Grossmann, I. E. (2021). Pyomo.GDP: an ecosystem for logic based modeling and optimization development, *Optimization and Engineering* (pp. 1-36).https://doi.org/10.1007/s11081-021-09601-7 - -.. [#gdp] Raman, R., & Grossmann, I. E. (1994). Modelling and computational techniques for logic based integer programming. *Computers & Chemical Engineering*, 18(7), 563–578. https://doi.org/10.1016/0098-1354(93)E0010-7 - -.. [#dp] Balas, E. (1985). Disjunctive Programming and a Hierarchy of Relaxations for Discrete Optimization Problems. *SIAM Journal on Algebraic Discrete Methods*, 6(3), 466–486. https://doi.org/10.1137/0606047 - -.. [#gdpreview] Grossmann, I. E., & Trespalacios, F. (2013). Systematic modeling of discrete-continuous optimization models through generalized disjunctive programming. *AIChE Journal*, 59(9), 3276–3295. https://doi.org/10.1002/aic.14088 diff --git a/doc/OnlineDocs/explanation/modeling/gdp/solving.rst b/doc/OnlineDocs/explanation/modeling/gdp/solving.rst index 88451f5f128..eb8ecf38eda 100644 --- a/doc/OnlineDocs/explanation/modeling/gdp/solving.rst +++ b/doc/OnlineDocs/explanation/modeling/gdp/solving.rst @@ -106,11 +106,16 @@ doing so are the (included) Big-M and Hull reformulations. Big-M (BM) Reformulation ^^^^^^^^^^^^^^^^^^^^^^^^ -The Big-M reformulation\ [#gdp-bm]_ results in a smaller transformed model, avoiding the need to add extra variables; however, it yields a looser continuous relaxation. -By default, the BM transformation will estimate reasonably tight M values for you if variables are bounded. -For nonlinear models where finite expression bounds may be inferred from variable bounds, the BM transformation may also be able to automatically compute M values for you. -For all other models, you will need to provide the M values through a "BigM" Suffix, or through the `bigM` argument to the transformation. -We will raise a ``GDP_Error`` for missing M values. +The Big-M reformulation\ [NW88]_ results in a smaller transformed +model, avoiding the need to add extra variables; however, it yields a +looser continuous relaxation. By default, the BM transformation will +estimate reasonably tight M values for you if variables are bounded. +For nonlinear models where finite expression bounds may be inferred from +variable bounds, the BM transformation may also be able to automatically +compute M values for you. For all other models, you will need to +provide the M values through a "BigM" Suffix, or through the `bigM` +argument to the transformation. We will raise a ``GDP_Error`` for +missing M values. To apply the BM reformulation within a python script, use: @@ -123,9 +128,12 @@ From the Pyomo command line, include the ``--transform pyomo.gdp.bigm`` option. Multiple Big-M (MBM) Reformulation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -We also implement the multiple-parameter Big-M (MBM) approach described in literature\ [#gdp-mbm]_. -By default, the MBM transformation will solve continuous subproblems in order to calculate M values. -This process can be time-consuming, so the transformation also provides a method to export the M values used as a dictionary and allows for M values to be provided through the `bigM` argument. +We also implement the multiple-parameter Big-M (MBM) approach described +in literature\ [TG15]_. By default, the MBM transformation will solve +continuous subproblems in order to calculate M values. This process can +be time-consuming, so the transformation also provides a method to +export the M values used as a dictionary and allows for M values to be +provided through the `bigM` argument. For example, to apply the transformation and store the M values, use: @@ -169,9 +177,12 @@ From the Pyomo command line, include the ``--transform pyomo.gdp.hull`` option. Hybrid BM/HR Reformulation ^^^^^^^^^^^^^^^^^^^^^^^^^^ -An experimental (for now) implementation of the cutting plane approach described in literature\ [#gdp-cuttingplanes]_ is provided for linear GDP models. -The transformation augments the BM reformulation by a set of cutting planes generated from the HR model by solving separation problems. -This gives a model that is not as large as the HR, but with a stronger continuous relaxation than the BM. +An experimental (for now) implementation of the cutting plane approach +described in literature\ [SG03]_ is provided for linear GDP models. +The transformation augments the BM reformulation by a set of cutting +planes generated from the HR model by solving separation problems. This +gives a model that is not as large as the HR, but with a stronger +continuous relaxation than the BM. This transformation is accessible via: @@ -186,17 +197,4 @@ Pyomo includes the contributed GDPopt solver, which can directly solve GDP models. Its usage is described within the :ref:`contributed packages documentation `. -References -========== -.. [#gdp-pse-paper] Chen, Q., Johnson, E. S., Siirola, J. D., & Grossmann, I. E. (2018). Pyomo.GDP: Disjunctive Models in Python. In M. R. Eden, M. G. Ierapetritou, & G. P. Towler (Eds.), *Proceedings of the 13th International Symposium on Process Systems Engineering* (pp. 889–894). San Diego: Elsevier B.V. https://doi.org/10.1016/B978-0-444-64241-7.50143-9 - -.. [#gdp-main-paper] Chen, Q., Johnson, E. S., Bernal, D. E., Valentin, R., Kale, S., Bates, J., Siirola, J. D. and Grossmann, I. E. (2021). Pyomo.GDP: an ecosystem for logic based modeling and optimization development, *Optimization and Engineering* (pp. 1-36).https://doi.org/10.1007/s11081-021-09601-7 - -.. [#gdp-review-2013] Grossmann, I. E., & Trespalacios, F. (2013). Systematic modeling of discrete-continuous optimization models through generalized disjunctive programming. *AIChE Journal*, 59(9), 3276–3295. https://doi.org/10.1002/aic.14088 - -.. [#gdp-mbm] Trespalacios, F., & Grossmann, I. E. (2015). Improved Big-M reformulation for generalized disjunctive programs. *Computers and Chemical Engineering*, 76, 98–103. https://doi.org/10.1016/j.compchemeng.2015.02.013 - -.. [#gdp-bm] Nemhauser, G. L., & Wolsey, L. A. (1988). *Integer and combinatorial optimization*. New York: Wiley. - -.. [#gdp-cuttingplanes] Sawaya, N. W., & Grossmann, I. E. (2003). A cutting plane method for solving linear generalized disjunctive programming problems. *Computer Aided Chemical Engineering*, 15(C), 1032–1037. https://doi.org/10.1016/S1570-7946(03)80444-3 diff --git a/doc/OnlineDocs/explanation/modeling/math_programming/expressions.rst b/doc/OnlineDocs/explanation/modeling/math_programming/expressions.rst index df506064d5d..98635f8009f 100644 --- a/doc/OnlineDocs/explanation/modeling/math_programming/expressions.rst +++ b/doc/OnlineDocs/explanation/modeling/math_programming/expressions.rst @@ -54,7 +54,7 @@ Pyomo has facilities to add piecewise constraints of the form y=f(x) for a variety of forms of the function f. The piecewise types other than SOS2, BIGM_SOS1, BIGM_BIN are implement -as described in the paper [Vielma_et_al]_. +as described in the paper [VAN10]_. There are two basic forms for the declaration of the constraint: diff --git a/doc/OnlineDocs/explanation/modeling/math_programming/suffixes.rst b/doc/OnlineDocs/explanation/modeling/math_programming/suffixes.rst index e45fe2d74b7..e410374b274 100644 --- a/doc/OnlineDocs/explanation/modeling/math_programming/suffixes.rst +++ b/doc/OnlineDocs/explanation/modeling/math_programming/suffixes.rst @@ -23,7 +23,7 @@ Suffix Notation and the Pyomo NL File Interface ----------------------------------------------- The Suffix component used in Pyomo has been adapted from the suffix -notation used in the modeling language AMPL [AMPL]_. Therefore, it +notation used in the modeling language AMPL [FGK02]_. Therefore, it follows naturally that AMPL style suffix functionality is fully available using Pyomo's NL file interface. For information on AMPL style suffixes the reader is referred to the AMPL website: diff --git a/doc/OnlineDocs/explanation/solvers/pyros.rst b/doc/OnlineDocs/explanation/solvers/pyros.rst index 8efa0defaac..33da606206e 100644 --- a/doc/OnlineDocs/explanation/solvers/pyros.rst +++ b/doc/OnlineDocs/explanation/solvers/pyros.rst @@ -101,7 +101,7 @@ Based on the above notation, the form of the robust counterpart addressed by PyR \end{array} PyROS solves problems of this form using the -Generalized Robust Cutting-Set algorithm developed in [Isenberg_et_al]_. +Generalized Robust Cutting-Set algorithm developed in [IAE+21]_. When using PyROS, please consider citing the above paper. diff --git a/doc/OnlineDocs/getting_started/pyomo_overview/math_modeling.rst b/doc/OnlineDocs/getting_started/pyomo_overview/math_modeling.rst index ccacca8d58d..b1b837e7138 100644 --- a/doc/OnlineDocs/getting_started/pyomo_overview/math_modeling.rst +++ b/doc/OnlineDocs/getting_started/pyomo_overview/math_modeling.rst @@ -6,7 +6,7 @@ Modeling Objects. A more complete description is contained in the [PyomoBookIII]_ book. Pyomo supports the formulation and analysis of mathematical models for complex optimization applications. This capability is commonly associated with commercially available algebraic -modeling languages (AMLs) such as [AMPL]_, [AIMMS]_, and [GAMS]_. +modeling languages (AMLs) such as [FGK02]_, [AIMMS]_, and [GAMS]_. Pyomo's modeling objects are embedded within Python, a full-featured, high-level programming language that contains a rich set of supporting libraries. diff --git a/doc/OnlineDocs/reference/bibliography.rst b/doc/OnlineDocs/reference/bibliography.rst index fdd4c12545b..42aafecc33b 100644 --- a/doc/OnlineDocs/reference/bibliography.rst +++ b/doc/OnlineDocs/reference/bibliography.rst @@ -31,47 +31,94 @@ These publications describe various Pyomo capabilitites or subpackages: 187-223. 2018. .. [ParmestPaper] Katherine A. Klise, Bethany L. Nicholson, Andrea - Staid, David L.Woodruff. Parmest: Parameter Estimation Via Pyomo. - Computer Aided Chemical Engineering, 47: 41-46. 2019. - -.. [PyomoGDP] Chen, Q., Johnson, E. S., Bernal, D. E., Valentin, R., - Kale, S., Bates, J., Siirola, J. D. and Grossmann, I. E. Pyomo.GDP: - an ecosystem for logic based modeling and optimization development, - *Optimization and Engineering* pp. 1-36. 2021. DOI - `10.1007/s11081-021-09601-7 + Staid, David L.Woodruff. "Parmest: Parameter Estimation Via Pyomo." + *Computer Aided Chemical Engineering*, 47: 41-46. 2019. + +.. [PyomoGDP-paper] Qi Chen, Emma S. Johnson, David E. Bernal, Romeo + Valentin, Sunjeev Kale, Johnny Bates, John D. Siirola, and + Ignacio E. Grossmann. "Pyomo.GDP: an ecosystem for logic based + modeling and optimization development." *Optimization and + Engineering* pp. 1-36. 2021. DOI `10.1007/s11081-021-09601-7 `_ +.. [PyomoGDP-pse-paper] Qi Chen, Emma S. Johnson, John D. Siirola, and + Ignacio E. Grossmann. "Pyomo.GDP: Disjunctive Models in Python." + In M. R. Eden, M. G. Ierapetritou, and G. P. Towler (Eds.), + *Proceedings of the 13th International Symposium on Process Systems + Engineering* pp. 889–894. 2018. DOI + `10.1016/B978-0-444-64241-7.50143-9 + `_ + Bibliography ============ .. [AIMMS] http://www.aimms.com/ -.. [AMPL] R. Fourer, D. M. Gay, and B. W. Kernighan. AMPL: A Modeling +.. [AM00] O. Abel, W. Marquardt, "Scenario-integrated modeling and + optimization of dynamic systems", AIChE Journal, 46(4), 2000. + +.. [Bal85] Balas, E. "Disjunctive Programming and a Hierarchy of + Relaxations for Discrete Optimization Problems." *SIAM Journal on + Algebraic Discrete Methods*, 6(3), 466–486. 1985. DOI + `10.1137/0606047 `_ + +.. [FGK02] R. Fourer, D. M. Gay, and B. W. Kernighan. AMPL: A Modeling Language for Mathematical Programming, 2nd Edition. Duxbury Press, 2002. .. [GAMS] http://www.gams.com -.. [Isenberg_et_al] Isenberg, N.M., Akula, P., Eslick, J.C., Bhattacharyya, D., - Miller, D.C., Gounaris, C.E. A generalized cutting‐set approach for +.. [GLM99] A. Grothey, S. Leyffer, and K.I.M. McKinnon. A note + on feasibility in Benders Decomposition. Numerical Analysis Report + NA/188, Dundee University, 1999. + +.. [GT13] Grossmann, I. E., and Trespalacios, F. "Systematic modeling + of discrete-continuous optimization models through generalized + disjunctive programming. *AIChE Journal*, 59(9), + 3276–3295. 2013. DOI `10.1002/aic.14088 `_ + +.. [IAE+21] Isenberg, N.M., Akula, P., Eslick, J.C., Bhattacharyya, D., + Miller, D.C., Gounaris, C.E. "A generalized cutting‐set approach for nonlinear robust optimization in process systems - engineering. AIChE Journal. 67:e17175. 2021; DOI `10.1002/aic.17175 + engineering", AIChE Journal. 67:e17175. 2021. DOI `10.1002/aic.17175 `_ -.. [mpisppy] Bernard Knueven, David Mildebrath, Christopher Muir, - John D. Siirola, Jean-Paul Watson, and David L Woodruff, A Parallel - Hub-and-Spoke System for Large-Scale Scenario-Based Optimization - Under Uncertainty, pre-print, 2020. - +.. [KMM+23] Bernard Knueven, David Mildebrath, Christopher Muir, + John D. Siirola, Jean-Paul Watson, and David L Woodruff, A Parallel + Hub-and-Spoke System for Large-Scale Scenario-Based Optimization + Under Uncertainty. *Math Programming Computation*, 15, 591-619. 2023. + DOI `10.1007/s12532-023-00247-3 + `_ + +.. [KMT21] J. Kronqvist, R. Misener, and C. Tsay. "Between Steps: Intermediate + Relaxations between big-M and Convex Hull Reformulations", 2021. -.. [RooneyBiegler] W.C. Rooney, L.T. Biegler, "Design for model +.. [NW88] Nemhauser, G. L., and Wolsey, L. A. *Integer and + combinatorial optimization*. New York: Wiley. 1988. + +.. [RB01] W.C. Rooney, L.T. Biegler, "Design for model parameter uncertainty using nonlinear confidence regions", AIChE Journal, 47(8), 2001. - -.. [SemiBatch] O. Abel, W. Marquardt, "Scenario-integrated modeling and - optimization of dynamic systems", AIChE Journal, 46(4), 2000. -.. [Vielma_et_al] J. P. Vielma, S. Ahmed, G. Nemhauser. "Mixed-Integer +.. [RG94] Raman, R., and Grossmann, I. E. "Modelling and computational + techniques for logic based integer programming." *Computers and + Chemical Engineering*, 18(7), 563–578. 1994. DOI + `10.1016/0098-1354(93)E0010-7 + `_ + +.. [SG03] Sawaya, N. W., and Grossmann, I. E. "A cutting plane + method for solving linear generalized disjunctive programming + problems." *Computer Aided Chemical Engineering*, 15(C), + 1032–1037. 2003. DOI `10.1016/S1570-7946(03)80444-3 + `_ + +.. [TG15] Trespalacios, F., and Grossmann, I. E. Improved Big-M + reformulation for generalized disjunctive programs. *Computers and + Chemical Engineering*, 76, 98–103. 2015. DOI + `10.1016/j.compchemeng.2015.02.013 + `_ + +.. [VAN10] J. P. Vielma, S. Ahmed, G. Nemhauser. "Mixed-Integer Models for Non-separable Piecewise Linear Optimization: Unifying - framework and Extensions", Operations Research 58, pp. 303-315. 2010. + framework and Extensions", Operations Research 58(2), pp. 303-315. 2010. diff --git a/pyomo/contrib/benders/benders_cuts.py b/pyomo/contrib/benders/benders_cuts.py index 0653be55986..3eb4fa845ee 100644 --- a/pyomo/contrib/benders/benders_cuts.py +++ b/pyomo/contrib/benders/benders_cuts.py @@ -33,9 +33,11 @@ logger = logging.getLogger(__name__) -""" -It is easier to understand this code after reading "A note on feasibility in Benders Decomposition" by -Grothey et al. +# Note: because of the LaTeX math, it is critical that this is a raw string. +__doc__ = r"""General purpose Benders Cut Generator. + +It is easier to understand this code after reading Grothey, Leyffer, +and McKinnon "A note on feasibility in Benders Decomposition" [GLM99]_ Original problem: diff --git a/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py index d582cdcfff5..e242f717b07 100644 --- a/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disaggregated_logarithmic.py @@ -27,20 +27,18 @@ """, ) class DisaggregatedLogarithmicMIPTransformation(PiecewiseLinearTransformationBase): - """ - Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables, following the "disaggregated logarithmic" - method from [1]. This is a direct-to-MIP transformation; GDP is not used. - This method of logarithmically formulating the piecewise linear function - imposes no restrictions on the family of polytopes, but we assume we have - simplices in this code. - - References - ---------- - [1] J.P. Vielma, S. Ahmed, and G. Nemhauser, "Mixed-integer models - for nonseparable piecewise-linear optimization: unifying framework - and extensions," Operations Research, vol. 58, no. 2, pp. 305-315, - 2010. + """Represent a piecewise linear function "logarithmically" as a MIP. + + This transformation represents a piecewise linear function + "logarithmically" by using a MIP with :math:`log_2(|P|)` binary + decision variables, following the "disaggregated logarithmic" method + from [VAN10]_. + + This is a direct-to-MIP transformation; GDP is not used. This + method of logarithmically formulating the piecewise linear function + imposes no restrictions on the family of polytopes, but we assume we + have simplices in this code. + """ CONFIG = PiecewiseLinearTransformationBase.CONFIG() diff --git a/pyomo/gdp/plugins/partition_disjuncts.py b/pyomo/gdp/plugins/partition_disjuncts.py index 1a76900047c..68658dca2c0 100644 --- a/pyomo/gdp/plugins/partition_disjuncts.py +++ b/pyomo/gdp/plugins/partition_disjuncts.py @@ -10,10 +10,8 @@ # ___________________________________________________________________________ """ -Between Steps (P-Split) reformulation for GDPs from: +Between Steps (P-Split) reformulation for GDPs from [KMT21]_. -J. Kronqvist, R. Misener, and C. Tsay, "Between Steps: Intermediate -Relaxations between big-M and Convex Hull Reformulations," 2021. """ @@ -209,7 +207,7 @@ class PartitionDisjuncts_Transformation(Transformation): """ Transform disjunctive model to equivalent disjunctive model (with potentially tighter hull relaxation) by taking the "P-split" formulation - from Kronqvist et al. 2021 [1]. In each Disjunct, convex and additively + from Kronqvist et al. 2021 [KMT21]_. In each Disjunct, convex and additively separable constraints are split into separate constraints by introducing auxiliary variables that upperbound the subexpressions created by the split. Increasing the number of partitions can result in tighter hull relaxations, @@ -228,8 +226,7 @@ class PartitionDisjuncts_Transformation(Transformation): References ---------- - [1] J. Kronqvist, R. Misener, and C. Tsay, "Between Steps: Intermediate - Relaxations between big-M and Convex Hull Reformulations," 2021. + See [KMT21]_. """ From c8f533363c05fa9f943b01f6754f4d5ee605cc68 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 1 Oct 2024 14:28:07 -0600 Subject: [PATCH 2473/3044] Update __init__.py to remove absolute pyomo imports --- pyomo/contrib/cp/__init__.py | 3 - pyomo/contrib/gdp_bounds/__init__.py | 2 - pyomo/contrib/latex_printer/__init__.py | 3 - pyomo/contrib/preprocessing/__init__.py | 2 - .../contrib/preprocessing/plugins/__init__.py | 26 +++---- pyomo/core/__init__.py | 4 +- pyomo/core/beta/__init__.py | 3 +- pyomo/core/kernel/__init__.py | 30 ++++---- .../core/kernel/piecewise_library/__init__.py | 4 +- pyomo/core/plugins/__init__.py | 2 +- pyomo/core/plugins/transform/__init__.py | 29 ++++---- pyomo/dae/plugins/__init__.py | 3 +- pyomo/dataportal/__init__.py | 2 +- pyomo/dataportal/plugins/__init__.py | 16 +++-- pyomo/duality/__init__.py | 2 +- pyomo/gdp/plugins/__init__.py | 26 +++---- pyomo/kernel/__init__.py | 2 - pyomo/mpec/plugins/__init__.py | 8 +-- pyomo/neos/plugins/__init__.py | 3 +- pyomo/network/plugins/__init__.py | 2 +- pyomo/opt/__init__.py | 2 - pyomo/opt/base/__init__.py | 1 - pyomo/opt/parallel/__init__.py | 3 +- pyomo/opt/plugins/__init__.py | 4 +- pyomo/opt/results/__init__.py | 6 +- pyomo/repn/beta/__init__.py | 2 +- pyomo/repn/plugins/__init__.py | 19 +++--- pyomo/scripting/__init__.py | 3 +- pyomo/scripting/plugins/__init__.py | 6 +- pyomo/solvers/plugins/__init__.py | 3 +- pyomo/solvers/plugins/converter/__init__.py | 4 +- pyomo/solvers/plugins/solvers/__init__.py | 44 ++++++------ pyomo/solvers/tests/models/__init__.py | 68 +++++++++---------- 33 files changed, 149 insertions(+), 188 deletions(-) diff --git a/pyomo/contrib/cp/__init__.py b/pyomo/contrib/cp/__init__.py index d206fe95251..f285cd6be68 100644 --- a/pyomo/contrib/cp/__init__.py +++ b/pyomo/contrib/cp/__init__.py @@ -35,6 +35,3 @@ Step, Pulse, ) - -# register logical_to_disjunctive transformation -import pyomo.contrib.cp.transform.logical_to_disjunctive_program diff --git a/pyomo/contrib/gdp_bounds/__init__.py b/pyomo/contrib/gdp_bounds/__init__.py index ac71890cf7c..a4a626013c4 100644 --- a/pyomo/contrib/gdp_bounds/__init__.py +++ b/pyomo/contrib/gdp_bounds/__init__.py @@ -8,5 +8,3 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -import pyomo.contrib.gdp_bounds.plugins diff --git a/pyomo/contrib/latex_printer/__init__.py b/pyomo/contrib/latex_printer/__init__.py index 02eaa636a36..a4ad3b95f54 100644 --- a/pyomo/contrib/latex_printer/__init__.py +++ b/pyomo/contrib/latex_printer/__init__.py @@ -9,9 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -# Recommended just to build all of the appropriate things -import pyomo.environ - # Remove one layer of .latex_printer # import statement is now: # from pyomo.contrib.latex_printer import latex_printer diff --git a/pyomo/contrib/preprocessing/__init__.py b/pyomo/contrib/preprocessing/__init__.py index 6458b7a6e71..a4a626013c4 100644 --- a/pyomo/contrib/preprocessing/__init__.py +++ b/pyomo/contrib/preprocessing/__init__.py @@ -8,5 +8,3 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -import pyomo.contrib.preprocessing.plugins diff --git a/pyomo/contrib/preprocessing/plugins/__init__.py b/pyomo/contrib/preprocessing/plugins/__init__.py index 62f5a40c6a9..ba8cafc32ee 100644 --- a/pyomo/contrib/preprocessing/plugins/__init__.py +++ b/pyomo/contrib/preprocessing/plugins/__init__.py @@ -11,15 +11,17 @@ def load(): - import pyomo.contrib.preprocessing.plugins.deactivate_trivial_constraints - import pyomo.contrib.preprocessing.plugins.detect_fixed_vars - import pyomo.contrib.preprocessing.plugins.init_vars - import pyomo.contrib.preprocessing.plugins.remove_zero_terms - import pyomo.contrib.preprocessing.plugins.equality_propagate - import pyomo.contrib.preprocessing.plugins.strip_bounds - import pyomo.contrib.preprocessing.plugins.zero_sum_propagator - import pyomo.contrib.preprocessing.plugins.bounds_to_vars - import pyomo.contrib.preprocessing.plugins.var_aggregator - import pyomo.contrib.preprocessing.plugins.induced_linearity - import pyomo.contrib.preprocessing.plugins.constraint_tightener - import pyomo.contrib.preprocessing.plugins.int_to_binary + from . import ( + deactivate_trivial_constraints, + detect_fixed_vars, + init_vars, + remove_zero_terms, + equality_propagate, + strip_bounds, + zero_sum_propagator, + bounds_to_vars, + var_aggregator, + induced_linearity, + constraint_tightener, + int_to_binary, + ) diff --git a/pyomo/core/__init__.py b/pyomo/core/__init__.py index f0d168d98f9..8dbe254c0fd 100644 --- a/pyomo/core/__init__.py +++ b/pyomo/core/__init__.py @@ -63,8 +63,6 @@ from pyomo.core.expr.calculus.derivatives import differentiate from pyomo.core.expr.taylor_series import taylor_series_expansion -import pyomo.core.kernel - from pyomo.common.collections import ComponentMap from pyomo.core.expr.symbol_map import SymbolMap from pyomo.core.expr import ( @@ -80,6 +78,7 @@ expr_errors, calculus, ) + from pyomo.core import expr, util, kernel from pyomo.core.expr.numvalue import ( @@ -121,7 +120,6 @@ # from pyomo.core.base.component import name, Component, ModelComponentFactory from pyomo.core.base.componentuid import ComponentUID -import pyomo.core.base.indexed_component from pyomo.core.base.action import BuildAction from pyomo.core.base.check import BuildCheck from pyomo.core.base.set import Set, SetOf, simple_set_rule, RangeSet diff --git a/pyomo/core/beta/__init__.py b/pyomo/core/beta/__init__.py index a2d51d0b23e..4409ca9ab01 100644 --- a/pyomo/core/beta/__init__.py +++ b/pyomo/core/beta/__init__.py @@ -9,5 +9,4 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pyomo.core.beta.dict_objects -import pyomo.core.beta.list_objects +from . import dict_objects, list_objects diff --git a/pyomo/core/kernel/__init__.py b/pyomo/core/kernel/__init__.py index ffe0beee080..d9446e687b1 100644 --- a/pyomo/core/kernel/__init__.py +++ b/pyomo/core/kernel/__init__.py @@ -59,20 +59,22 @@ from pyomo.core.expr.calculus.derivatives import differentiate from pyomo.core.expr.taylor_series import taylor_series_expansion -import pyomo.core.kernel.base -import pyomo.core.kernel.homogeneous_container -import pyomo.core.kernel.heterogeneous_container -import pyomo.core.kernel.variable -import pyomo.core.kernel.constraint -import pyomo.core.kernel.matrix_constraint -import pyomo.core.kernel.parameter -import pyomo.core.kernel.expression -import pyomo.core.kernel.objective -import pyomo.core.kernel.sos -import pyomo.core.kernel.suffix -import pyomo.core.kernel.block -import pyomo.core.kernel.piecewise_library -import pyomo.core.kernel.set_types +from . import ( + base, + homogeneous_container, + heterogeneous_container, + variable, + constraint, + matrix_constraint, + parameter, + expression, + objective, + sos, + suffix, + block, + piecewise_library, + set_types, +) # TODO: These are included for backwards compatibility. Accessing them # will result in a deprecation warning diff --git a/pyomo/core/kernel/piecewise_library/__init__.py b/pyomo/core/kernel/piecewise_library/__init__.py index c4d2a751632..523741d528a 100644 --- a/pyomo/core/kernel/piecewise_library/__init__.py +++ b/pyomo/core/kernel/piecewise_library/__init__.py @@ -9,6 +9,4 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pyomo.core.kernel.piecewise_library.util -import pyomo.core.kernel.piecewise_library.transforms -import pyomo.core.kernel.piecewise_library.transforms_nd +from . import util, transforms, transforms_nd diff --git a/pyomo/core/plugins/__init__.py b/pyomo/core/plugins/__init__.py index 23407cd77ef..d9c377da0e2 100644 --- a/pyomo/core/plugins/__init__.py +++ b/pyomo/core/plugins/__init__.py @@ -11,4 +11,4 @@ def load(): - import pyomo.core.plugins.transform + from . import transform diff --git a/pyomo/core/plugins/transform/__init__.py b/pyomo/core/plugins/transform/__init__.py index 21e762047ca..59943f2cfcc 100644 --- a/pyomo/core/plugins/transform/__init__.py +++ b/pyomo/core/plugins/transform/__init__.py @@ -9,18 +9,17 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pyomo.core.plugins.transform.relax_integrality - -# import pyomo.core.plugins.transform.eliminate_fixed_vars -# import pyomo.core.plugins.transform.standard_form -import pyomo.core.plugins.transform.expand_connectors - -# import pyomo.core.plugins.transform.equality_transform -import pyomo.core.plugins.transform.nonnegative_transform -import pyomo.core.plugins.transform.radix_linearization -import pyomo.core.plugins.transform.discrete_vars - -# import pyomo.core.plugins.transform.util -import pyomo.core.plugins.transform.add_slack_vars -import pyomo.core.plugins.transform.scaling -import pyomo.core.plugins.transform.logical_to_linear +from . import ( + relax_integrality, + # eliminate_fixed_vars, + # standard_form, + expand_connectors, + # equality_transform, + nonnegative_transform, + radix_linearization, + discrete_vars, + # util, + add_slack_vars, + scaling, + logical_to_linear, +) diff --git a/pyomo/dae/plugins/__init__.py b/pyomo/dae/plugins/__init__.py index 681112dd970..79aa73b0e4b 100644 --- a/pyomo/dae/plugins/__init__.py +++ b/pyomo/dae/plugins/__init__.py @@ -11,5 +11,4 @@ def load(): - import pyomo.dae.plugins.colloc - import pyomo.dae.plugins.finitedifference + from . import colloc, finitedifference diff --git a/pyomo/dataportal/__init__.py b/pyomo/dataportal/__init__.py index ece0ac039f6..e21fbcc905f 100644 --- a/pyomo/dataportal/__init__.py +++ b/pyomo/dataportal/__init__.py @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pyomo.dataportal.parse_datacmds +from . import parse_datacmds from pyomo.dataportal.TableData import TableData from pyomo.dataportal.DataPortal import DataPortal from pyomo.dataportal.factory import DataManagerFactory, UnknownDataManager diff --git a/pyomo/dataportal/plugins/__init__.py b/pyomo/dataportal/plugins/__init__.py index 3a356ee9da8..1a205846c57 100644 --- a/pyomo/dataportal/plugins/__init__.py +++ b/pyomo/dataportal/plugins/__init__.py @@ -11,10 +11,12 @@ def load(): - import pyomo.dataportal.plugins.csv_table - import pyomo.dataportal.plugins.datacommands - import pyomo.dataportal.plugins.db_table - import pyomo.dataportal.plugins.json_dict - import pyomo.dataportal.plugins.text - import pyomo.dataportal.plugins.xml_table - import pyomo.dataportal.plugins.sheet + from pyomo.dataportal.plugins import ( + csv_table, + datacommands, + db_table, + json_dict, + text, + xml_table, + sheet, + ) diff --git a/pyomo/duality/__init__.py b/pyomo/duality/__init__.py index a08ca813ff8..3046e45d429 100644 --- a/pyomo/duality/__init__.py +++ b/pyomo/duality/__init__.py @@ -9,4 +9,4 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pyomo.duality.collect +from . import collect diff --git a/pyomo/gdp/plugins/__init__.py b/pyomo/gdp/plugins/__init__.py index 875e47e6cc1..fc7aaa6012b 100644 --- a/pyomo/gdp/plugins/__init__.py +++ b/pyomo/gdp/plugins/__init__.py @@ -11,15 +11,17 @@ def load(): - import pyomo.gdp.plugins.bigm - import pyomo.gdp.plugins.hull - import pyomo.gdp.plugins.bilinear - import pyomo.gdp.plugins.gdp_var_mover - import pyomo.gdp.plugins.cuttingplane - import pyomo.gdp.plugins.fix_disjuncts - import pyomo.gdp.plugins.partition_disjuncts - import pyomo.gdp.plugins.between_steps - import pyomo.gdp.plugins.multiple_bigm - import pyomo.gdp.plugins.transform_current_disjunctive_state - import pyomo.gdp.plugins.bound_pretransformation - import pyomo.gdp.plugins.binary_multiplication + from . import ( + bigm, + hull, + bilinear, + gdp_var_mover, + cuttingplane, + fix_disjuncts, + partition_disjuncts, + between_steps, + multiple_bigm, + transform_current_disjunctive_state, + bound_pretransformation, + binary_multiplication, + ) diff --git a/pyomo/kernel/__init__.py b/pyomo/kernel/__init__.py index 289fe83f0e4..5618767a714 100644 --- a/pyomo/kernel/__init__.py +++ b/pyomo/kernel/__init__.py @@ -15,7 +15,6 @@ # Load solver functionality # import pyomo.environ -import pyomo.opt from pyomo.opt import SolverFactory, SolverStatus, TerminationCondition # @@ -90,7 +89,6 @@ from pyomo.core.expr.calculus.derivatives import differentiate from pyomo.core.expr.taylor_series import taylor_series_expansion -import pyomo.core.kernel from pyomo.kernel.util import generate_names, preorder_traversal, pprint from pyomo.core.kernel.variable import ( variable, diff --git a/pyomo/mpec/plugins/__init__.py b/pyomo/mpec/plugins/__init__.py index 1ff8c316e9b..56ac546fb42 100644 --- a/pyomo/mpec/plugins/__init__.py +++ b/pyomo/mpec/plugins/__init__.py @@ -11,10 +11,4 @@ def load(): - import pyomo.mpec.plugins.mpec1 - import pyomo.mpec.plugins.mpec2 - import pyomo.mpec.plugins.mpec3 - import pyomo.mpec.plugins.mpec4 - import pyomo.mpec.plugins.solver1 - import pyomo.mpec.plugins.solver2 - import pyomo.mpec.plugins.pathampl + from . import mpec1, mpec2, mpec3, mpec4, solver1, solver2, pathampl diff --git a/pyomo/neos/plugins/__init__.py b/pyomo/neos/plugins/__init__.py index 75105e87088..ba6c47ca683 100644 --- a/pyomo/neos/plugins/__init__.py +++ b/pyomo/neos/plugins/__init__.py @@ -11,5 +11,4 @@ def load(): - import pyomo.neos.plugins.NEOS - import pyomo.neos.plugins.kestrel_plugin + from . import NEOS, kestrel_plugin diff --git a/pyomo/network/plugins/__init__.py b/pyomo/network/plugins/__init__.py index ab3cde23daa..ac83de1b41d 100644 --- a/pyomo/network/plugins/__init__.py +++ b/pyomo/network/plugins/__init__.py @@ -11,4 +11,4 @@ def load(): - import pyomo.network.plugins.expand_arcs + from . import expand_arcs diff --git a/pyomo/opt/__init__.py b/pyomo/opt/__init__.py index c78dd0384d2..629f63f92d4 100644 --- a/pyomo/opt/__init__.py +++ b/pyomo/opt/__init__.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pyomo.opt.base.opt_config -import pyomo.opt.solver from pyomo.opt.base import ( check_available_solvers, diff --git a/pyomo/opt/base/__init__.py b/pyomo/opt/base/__init__.py index 8d11114dd09..0c85042ec8a 100644 --- a/pyomo/opt/base/__init__.py +++ b/pyomo/opt/base/__init__.py @@ -9,7 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pyomo.opt.base.opt_config from pyomo.opt.base.error import ConverterError from pyomo.opt.base.convert import convert_problem diff --git a/pyomo/opt/parallel/__init__.py b/pyomo/opt/parallel/__init__.py index dbfdf2302ca..8d4a5f8d91d 100644 --- a/pyomo/opt/parallel/__init__.py +++ b/pyomo/opt/parallel/__init__.py @@ -15,5 +15,4 @@ SolverManagerFactory, AsynchronousSolverManager, ) -import pyomo.opt.parallel.manager -import pyomo.opt.parallel.local +from . import manager, local diff --git a/pyomo/opt/plugins/__init__.py b/pyomo/opt/plugins/__init__.py index 5ea2490b534..514764e5c9d 100644 --- a/pyomo/opt/plugins/__init__.py +++ b/pyomo/opt/plugins/__init__.py @@ -11,6 +11,4 @@ def load(): - import pyomo.opt.plugins.driver - import pyomo.opt.plugins.res - import pyomo.opt.plugins.sol + from . import driver, res, sol diff --git a/pyomo/opt/results/__init__.py b/pyomo/opt/results/__init__.py index 64a1b42ac86..bf96d123ff4 100644 --- a/pyomo/opt/results/__init__.py +++ b/pyomo/opt/results/__init__.py @@ -10,17 +10,13 @@ # ___________________________________________________________________________ from pyomo.opt.results.container import ( - ScalarData, - ScalarType, - default_print_options, - strict, ListContainer, MapContainer, UndefinedData, undefined, ignore, ) -import pyomo.opt.results.problem + from pyomo.opt.results.solver import ( SolverStatus, TerminationCondition, diff --git a/pyomo/repn/beta/__init__.py b/pyomo/repn/beta/__init__.py index a75a75ec760..a87ad717321 100644 --- a/pyomo/repn/beta/__init__.py +++ b/pyomo/repn/beta/__init__.py @@ -9,4 +9,4 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pyomo.repn.beta.matrix +from . import matrix diff --git a/pyomo/repn/plugins/__init__.py b/pyomo/repn/plugins/__init__.py index 4029f44a03d..453e500b672 100644 --- a/pyomo/repn/plugins/__init__.py +++ b/pyomo/repn/plugins/__init__.py @@ -11,15 +11,16 @@ def load(): - import pyomo.repn.plugins.cpxlp - import pyomo.repn.plugins.ampl - import pyomo.repn.plugins.baron_writer - import pyomo.repn.plugins.mps - import pyomo.repn.plugins.gams_writer - import pyomo.repn.plugins.lp_writer - import pyomo.repn.plugins.nl_writer - import pyomo.repn.plugins.standard_form - + from . import ( + cpxlp, + ampl, + baron_writer, + mps, + gams_writer, + lp_writer, + nl_writer, + standard_form, + ) from pyomo.opt import WriterFactory # Register the "default" versions of writers that have more than one diff --git a/pyomo/scripting/__init__.py b/pyomo/scripting/__init__.py index 7cb5ac652fc..ccdc4c8a9fe 100644 --- a/pyomo/scripting/__init__.py +++ b/pyomo/scripting/__init__.py @@ -9,5 +9,4 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pyomo.scripting.pyomo_command -import pyomo.scripting.util +from . import pyomo_command, util diff --git a/pyomo/scripting/plugins/__init__.py b/pyomo/scripting/plugins/__init__.py index 86a3100e077..e0f91023522 100644 --- a/pyomo/scripting/plugins/__init__.py +++ b/pyomo/scripting/plugins/__init__.py @@ -11,8 +11,4 @@ def load(): - import pyomo.scripting.plugins.convert - import pyomo.scripting.plugins.solve - import pyomo.scripting.plugins.download - import pyomo.scripting.plugins.build_ext - import pyomo.scripting.plugins.extras + from . import convert, solve, download, build_ext, extras diff --git a/pyomo/solvers/plugins/__init__.py b/pyomo/solvers/plugins/__init__.py index 2a7bf2fea04..9e20d582327 100644 --- a/pyomo/solvers/plugins/__init__.py +++ b/pyomo/solvers/plugins/__init__.py @@ -11,5 +11,4 @@ def load(): - import pyomo.solvers.plugins.converter - import pyomo.solvers.plugins.solvers + from . import converter, solvers diff --git a/pyomo/solvers/plugins/converter/__init__.py b/pyomo/solvers/plugins/converter/__init__.py index 56c32f1c8c1..053f5fc92f4 100644 --- a/pyomo/solvers/plugins/converter/__init__.py +++ b/pyomo/solvers/plugins/converter/__init__.py @@ -9,6 +9,4 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pyomo.solvers.plugins.converter.ampl -import pyomo.solvers.plugins.converter.glpsol -import pyomo.solvers.plugins.converter.model +from . import ampl, glpsol, model diff --git a/pyomo/solvers/plugins/solvers/__init__.py b/pyomo/solvers/plugins/solvers/__init__.py index a3918dce5cc..79b047755da 100644 --- a/pyomo/solvers/plugins/solvers/__init__.py +++ b/pyomo/solvers/plugins/solvers/__init__.py @@ -10,24 +10,26 @@ # ___________________________________________________________________________ # TODO: Disabled until we can confirm application to Pyomo models -import pyomo.solvers.plugins.solvers.CBCplugin -import pyomo.solvers.plugins.solvers.GLPK -import pyomo.solvers.plugins.solvers.CPLEX -import pyomo.solvers.plugins.solvers.GUROBI -import pyomo.solvers.plugins.solvers.BARON -import pyomo.solvers.plugins.solvers.ASL -import pyomo.solvers.plugins.solvers.pywrapper -import pyomo.solvers.plugins.solvers.SCIPAMPL -import pyomo.solvers.plugins.solvers.CONOPT -import pyomo.solvers.plugins.solvers.XPRESS -import pyomo.solvers.plugins.solvers.IPOPT -import pyomo.solvers.plugins.solvers.gurobi_direct -import pyomo.solvers.plugins.solvers.gurobi_persistent -import pyomo.solvers.plugins.solvers.cplex_direct -import pyomo.solvers.plugins.solvers.cplex_persistent -import pyomo.solvers.plugins.solvers.GAMS -import pyomo.solvers.plugins.solvers.mosek_direct -import pyomo.solvers.plugins.solvers.mosek_persistent -import pyomo.solvers.plugins.solvers.xpress_direct -import pyomo.solvers.plugins.solvers.xpress_persistent -import pyomo.solvers.plugins.solvers.SAS +from . import ( + CBCplugin, + GLPK, + CPLEX, + GUROBI, + BARON, + ASL, + pywrapper, + SCIPAMPL, + CONOPT, + XPRESS, + IPOPT, + gurobi_direct, + gurobi_persistent, + cplex_direct, + cplex_persistent, + GAMS, + mosek_direct, + mosek_persistent, + xpress_direct, + xpress_persistent, + SAS, +) diff --git a/pyomo/solvers/tests/models/__init__.py b/pyomo/solvers/tests/models/__init__.py index 46a1c96936d..ea7a35ff067 100644 --- a/pyomo/solvers/tests/models/__init__.py +++ b/pyomo/solvers/tests/models/__init__.py @@ -9,40 +9,34 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pyomo.solvers.tests.models.base - -import pyomo.solvers.tests.models.LP_block -import pyomo.solvers.tests.models.LP_compiled -import pyomo.solvers.tests.models.LP_constant_objective1 -import pyomo.solvers.tests.models.LP_constant_objective2 -import pyomo.solvers.tests.models.LP_duals_maximize -import pyomo.solvers.tests.models.LP_duals_minimize -import pyomo.solvers.tests.models.LP_inactive_index -import pyomo.solvers.tests.models.LP_infeasible1 -import pyomo.solvers.tests.models.LP_infeasible2 -import pyomo.solvers.tests.models.LP_piecewise -import pyomo.solvers.tests.models.LP_simple -import pyomo.solvers.tests.models.LP_trivial_constraints -import pyomo.solvers.tests.models.LP_unbounded -import pyomo.solvers.tests.models.LP_unused_vars - -# WEH - Omitting this for because it's not reliably solved by ipopt -# import pyomo.solvers.tests.models.LP_unique_duals - -import pyomo.solvers.tests.models.MILP_discrete_var_bounds -import pyomo.solvers.tests.models.MILP_infeasible1 -import pyomo.solvers.tests.models.MILP_simple -import pyomo.solvers.tests.models.MILP_unbounded -import pyomo.solvers.tests.models.MILP_unused_vars - -import pyomo.solvers.tests.models.MIQCP_simple - -import pyomo.solvers.tests.models.MIQP_simple - -import pyomo.solvers.tests.models.QCP_simple - -import pyomo.solvers.tests.models.QP_constant_objective -import pyomo.solvers.tests.models.QP_simple - -import pyomo.solvers.tests.models.SOS1_simple -import pyomo.solvers.tests.models.SOS2_simple +from . import ( + base, + LP_block, + LP_compiled, + LP_constant_objective1, + LP_constant_objective2, + LP_duals_maximize, + LP_duals_minimize, + LP_inactive_index, + LP_infeasible1, + LP_infeasible2, + LP_piecewise, + LP_simple, + LP_trivial_constraints, + LP_unbounded, + LP_unused_vars, + # WEH - Omitting this for because it's not reliably solved by ipopt, + # LP_unique_duals, + MILP_discrete_var_bounds, + MILP_infeasible1, + MILP_simple, + MILP_unbounded, + MILP_unused_vars, + MIQCP_simple, + MIQP_simple, + QCP_simple, + QP_constant_objective, + QP_simple, + SOS1_simple, + SOS2_simple, +) From baee0e916bea78055323d0e230ff22a59ec16c96 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 1 Oct 2024 14:30:17 -0600 Subject: [PATCH 2474/3044] General cleanup of doc files --- doc/OnlineDocs/explanation/analysis/parmest/index.rst | 7 ------- doc/OnlineDocs/explanation/modeling_utils/index.rst | 6 ++++-- doc/OnlineDocs/explanation/solvers/pynumero/index.rst | 8 -------- doc/OnlineDocs/reference/topical/index.rst | 1 - 4 files changed, 4 insertions(+), 18 deletions(-) diff --git a/doc/OnlineDocs/explanation/analysis/parmest/index.rst b/doc/OnlineDocs/explanation/analysis/parmest/index.rst index 2bf4942e632..4700535815f 100644 --- a/doc/OnlineDocs/explanation/analysis/parmest/index.rst +++ b/doc/OnlineDocs/explanation/analysis/parmest/index.rst @@ -26,10 +26,3 @@ Index of parmest documentation examples.rst parallel.rst api.rst - -Indices and Tables ------------------- - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/doc/OnlineDocs/explanation/modeling_utils/index.rst b/doc/OnlineDocs/explanation/modeling_utils/index.rst index 14bd4d9204c..16560899ebd 100644 --- a/doc/OnlineDocs/explanation/modeling_utils/index.rst +++ b/doc/OnlineDocs/explanation/modeling_utils/index.rst @@ -5,11 +5,9 @@ Modeling Utilities :maxdepth: 2 flattener/index - fme latex_printer preprocessing scaling - viewer @@ -20,3 +18,7 @@ Modeling Utilities `FME` `Model Viewer` `Model Flattening` + + Still missing: + - fme + - viewer diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/index.rst b/doc/OnlineDocs/explanation/solvers/pynumero/index.rst index f2deafcfe71..9fd6627b0c2 100644 --- a/doc/OnlineDocs/explanation/solvers/pynumero/index.rst +++ b/doc/OnlineDocs/explanation/solvers/pynumero/index.rst @@ -46,11 +46,3 @@ Papers utilizing PyNumero * Rodriguez, J. S., Laird, C. D., & Zavala, V. M. (2020). Scalable preconditioning of block-structured linear algebra systems using ADMM. Computers & Chemical Engineering, 133, 106478. - - -Indices and Tables ------------------- - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/doc/OnlineDocs/reference/topical/index.rst b/doc/OnlineDocs/reference/topical/index.rst index e0f4eb093d8..919b68f647a 100644 --- a/doc/OnlineDocs/reference/topical/index.rst +++ b/doc/OnlineDocs/reference/topical/index.rst @@ -10,7 +10,6 @@ Python scripts using Pyomo. .. toctree:: :maxdepth: 1 - common/index.rst aml/index.rst expressions/index.rst solvers/index.rst From 865a55ef6be8061b8424a013c06600ed8565fcf7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 1 Oct 2024 14:32:14 -0600 Subject: [PATCH 2475/3044] re-adding working with abstrct models sections [inadvertantly removed] --- .../howto/abstract_models/BuildAction.rst | 66 ++ .../howto/abstract_models/data/ABCD.pdf | Bin 0 -> 18227 bytes .../howto/abstract_models/data/ABCD.png | Bin 0 -> 5496 bytes .../howto/abstract_models/data/PP.png | Bin 0 -> 18060 bytes .../abstract_models/data/dataportals.rst | 530 ++++++++++ .../howto/abstract_models/data/datfiles.rst | 933 ++++++++++++++++++ .../howto/abstract_models/data/index.rst | 66 ++ .../howto/abstract_models/data/native.rst | 84 ++ .../howto/abstract_models/data/raw_dicts.rst | 53 + .../abstract_models/data/storing_data.rst | 16 + .../howto/abstract_models/index.rst | 10 + .../abstract_models/instantiating_models.rst | 121 +++ .../howto/abstract_models/pyomo_command.rst | 123 +++ doc/OnlineDocs/howto/index.rst | 1 + 14 files changed, 2003 insertions(+) create mode 100644 doc/OnlineDocs/howto/abstract_models/BuildAction.rst create mode 100755 doc/OnlineDocs/howto/abstract_models/data/ABCD.pdf create mode 100755 doc/OnlineDocs/howto/abstract_models/data/ABCD.png create mode 100644 doc/OnlineDocs/howto/abstract_models/data/PP.png create mode 100644 doc/OnlineDocs/howto/abstract_models/data/dataportals.rst create mode 100644 doc/OnlineDocs/howto/abstract_models/data/datfiles.rst create mode 100644 doc/OnlineDocs/howto/abstract_models/data/index.rst create mode 100644 doc/OnlineDocs/howto/abstract_models/data/native.rst create mode 100644 doc/OnlineDocs/howto/abstract_models/data/raw_dicts.rst create mode 100644 doc/OnlineDocs/howto/abstract_models/data/storing_data.rst create mode 100644 doc/OnlineDocs/howto/abstract_models/index.rst create mode 100644 doc/OnlineDocs/howto/abstract_models/instantiating_models.rst create mode 100644 doc/OnlineDocs/howto/abstract_models/pyomo_command.rst diff --git a/doc/OnlineDocs/howto/abstract_models/BuildAction.rst b/doc/OnlineDocs/howto/abstract_models/BuildAction.rst new file mode 100644 index 00000000000..195c84ab4b4 --- /dev/null +++ b/doc/OnlineDocs/howto/abstract_models/BuildAction.rst @@ -0,0 +1,66 @@ +.. _BuildAction: + +.. _abstract2piecebuild.py: + +.. _Isinglebuild.py: + +``BuildAction`` and ``BuildCheck`` +================================== + +This is a somewhat advanced topic. In some cases, it is desirable to +trigger actions to be done as part of the model building process. The +``BuildAction`` function provides this capability in a Pyomo model. It +takes as arguments optional index sets and a function to perform the +action. For example, + +.. literalinclude:: /src/scripting/abstract2piecebuild_BuildAction_example.spy + :language: python + +calls the function ``bpts_build`` for each member of ``model.J``. The +function ``bpts_build`` should have the model and a variable for the +members of ``model.J`` as formal arguments. In this example, the +following would be a valid declaration for the function: + +.. literalinclude:: /src/scripting/abstract2piecebuild_Function_valid_declaration.spy + :language: python + + +A full example, which extends the :ref:`abstract2.py` and +:ref:`abstract2piece.py` examples, is + +.. literalinclude:: /src/scripting/abstract2piecebuild.spy + :language: python + +This example uses the build action to create a model component with +breakpoints for a :ref:`piecewise` function. The ``BuildAction`` is +triggered by the assignment to ``model.BuildBpts``. This object is not +referenced again, the only goal is to cause the execution of +``bpts_build,`` which places data in the ``model.bpts`` dictionary. +Note that if ``model.bpts`` had been a ``Set``, then it could have been +created with an ``initialize`` argument to the ``Set`` +declaration. Since it is a special-purpose dictionary to support the +:ref:`piecewise` functionality in Pyomo, we use a ``BuildAction``. + +Another application of ``BuildAction`` can be initialization of Pyomo +model data from Python data structures, or efficient initialization of +Pyomo model data from other Pyomo model data. Consider the +:ref:`Isinglecomm.py` example. Rather than using an initialization for +each list of sets ``NodesIn`` and ``NodesOut`` separately using +``initialize``, it is a little more efficient and probably a little +clearer, to use a build action. + +The full model is: + +.. literalinclude:: /src/scripting/Isinglebuild.py + :language: python + +for this model, the same data file can be used as for Isinglecomm.py in +:ref:`Isinglecomm.py` such as the toy data file: + +.. literalinclude:: /src/scripting/Isinglecomm.dat + +Build actions can also be a way to implement data validation, +particularly when multiple Sets or Parameters must be analyzed. However, +the the ``BuildCheck`` component is preferred for this purpose. It +executes its rule just like a ``BuildAction`` but will terminate the +construction of the model instance if the rule returns ``False``. diff --git a/doc/OnlineDocs/howto/abstract_models/data/ABCD.pdf b/doc/OnlineDocs/howto/abstract_models/data/ABCD.pdf new file mode 100755 index 0000000000000000000000000000000000000000..090906e2804e44b4db2d15ab3d02609a57a2f2bb GIT binary patch literal 18227 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 d_q_-HGq2%3*#2$7#|j1@@O1TaS?83{1OURCY_` +object, and only the data +needed for model construction is used. + +The following YAML file has a similar structure: + +.. literalinclude:: /src/dataportal/T.yaml + :language: none + +The data in this file can be used to load a Pyomo model with the +same syntax as a JSON file: + +.. literalinclude:: /src/dataportal/dataportal_tab_yaml1.spy + :language: python + + +Loading Tabular Data +-------------------- + +Many data sources supported by Pyomo are tabular data formats. Tabular +data is numerical or textual data that is organized into one or more +simple tables, where data is arranged in a matrix. Each table consists +of a matrix of numeric string values, simple strings, and quoted +strings. All rows have the same length, all columns have the same +length, and the first row typically represents labels for the column +data. + +The following section describes the tabular data sources supported by +Pyomo, and the subsequent sections illustrate ways that data can be +loaded from tabular data using TAB files. Subsequent sections describe +options for loading data from Excel spreadsheets and relational +databases. + +Tabular Data +^^^^^^^^^^^^ + +TAB files represent tabular data in an ascii file using whitespace as a +delimiter. A TAB file consists of rows of values, where each row has +the same length. For example, the file ``PP.tab`` has the format: + +.. literalinclude:: /src/dataportal/PP.tab + :language: none + +CSV files represent tabular data in a format that is very similar to TAB +files. Pyomo assumes that a CSV file consists of rows of values, where +each row has the same length. For example, the file ``PP.csv`` has the +format: + +.. literalinclude:: /src/dataportal/PP.csv + :language: none + +Excel spreadsheets can express complex data relationships. A *range* is +a contiguous, rectangular block of cells in an Excel spreadsheet. Thus, +a range in a spreadsheet has the same tabular structure as is a TAB file +or a CSV file. For example, consider the file ``excel.xls`` that has +the range ``PPtable``: + +.. image:: PP.png + :width: 2.5in + +A relational database is an application that organizes data into one or +more tables (or *relations*) with a unique key in each row. Tables both +reflect the data in a database as well as the result of queries within a +database. + +XML files represent tabular using ``table`` and ``row`` elements. Each +sub-element of a ``row`` element represents a different column, where +each row has the same length. For example, the file ``PP.xml`` has the +format: + +.. literalinclude:: /src/dataportal/PP.xml + :language: none + +Loading Set Data +^^^^^^^^^^^^^^^^ + +The ``set`` option is used specify a ``Set`` component that is loaded +with data. + +Loading a Simple Set +"""""""""""""""""""" + +Consider the file ``A.tab``, which defines a simple set: + +.. literalinclude:: /src/dataportal/A.tab + :language: none + +In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for a simple +set ``A``: + +.. literalinclude:: /src/dataportal/dataportal_tab_set1.spy + :language: python + +Loading a Set of Tuples +""""""""""""""""""""""" + +Consider the file ``C.tab``: + +.. literalinclude:: /src/dataportal/C.tab + :language: none + +In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for a +two-dimensional set ``C``: + +.. literalinclude:: /src/dataportal/dataportal_tab_set2.spy + :language: python + +In this example, the column titles do not directly impact the process of +loading data. Column titles can be used to select a subset of columns +from a table that is loaded (see below). + +Loading a Set Array +""""""""""""""""""" + +Consider the file ``D.tab``, which defines an array representation of a +two-dimensional set: + +.. literalinclude:: /src/dataportal/D.tab + :language: none + +In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for a +two-dimensional set ``D``: + +.. literalinclude:: /src/dataportal/dataportal_tab_set3.spy + :language: python + +The ``format`` option indicates that the set data is declared in a array +format. + +Loading Parameter Data +^^^^^^^^^^^^^^^^^^^^^^ + +The ``param`` option is used specify a ``Param`` component that is +loaded with data. + +Loading a Simple Parameter +"""""""""""""""""""""""""" + +The simplest parameter is simply a singleton value. Consider the file +``Z.tab``: + +.. literalinclude:: /src/dataportal/Z.tab + :language: none + +In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for a simple +parameter ``z``: + +.. literalinclude:: /src/dataportal/dataportal_tab_param1.spy + :language: python + +Loading an Indexed Parameter +"""""""""""""""""""""""""""" + +An indexed parameter can be defined by a single column in a table. For +example, consider the file ``Y.tab``: + +.. literalinclude:: /src/dataportal/Y.tab + :language: none + +In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for an indexed +parameter ``y``: + +.. literalinclude:: /src/dataportal/dataportal_tab_param2.spy + :language: python + +When column names are not used to specify the index and parameter data, +then the :class:`~pyomo.environ.DataPortal` +object assumes that the rightmost column defines parameter values. In +this file, the ``A`` column contains the index values, and the ``Y`` +column contains the parameter values. + +Loading Set and Parameter Values +"""""""""""""""""""""""""""""""" + +Note that the data for set ``A`` is predefined in the previous example. +The index set can be loaded with the parameter data using the ``index`` +option. In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for set ``A`` +and the indexed parameter ``y`` + +.. literalinclude:: /src/dataportal/dataportal_tab_param3.spy + :language: python + +An index set with multiple dimensions can also be loaded with an indexed +parameter. Consider the file ``PP.tab``: + +.. literalinclude:: /src/dataportal/PP.tab + :language: none + +In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for a tuple +set and an indexed parameter: + +.. literalinclude:: /src/dataportal/dataportal_tab_param10.spy + :language: python + +Loading a Parameter with Missing Values +""""""""""""""""""""""""""""""""""""""" + +Missing parameter data can be expressed in two ways. First, parameter +data can be defined with indices that are a subset of valid indices in +the model. The following example loads the indexed parameter ``y``: + +.. literalinclude:: /src/dataportal/dataportal_tab_param9.spy + :language: python + +The model defines an index set with four values, but only three +parameter values are declared in the data file ``Y.tab``. + +Parameter data can also be declared with missing values using the period +(``.``) symbol. For example, consider the file ``S.tab``: + +.. literalinclude:: /src/dataportal/PP.tab + :language: none + +In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for the index +set ``A`` and indexed parameter ``y``: + +.. literalinclude:: /src/dataportal/dataportal_tab_param8.spy + :language: python + +The period (``.``) symbol indicates a missing parameter value, but the +index set ``A`` contains the index value for the missing parameter. + +Loading Multiple Parameters +""""""""""""""""""""""""""" + +Multiple parameters can be initialized at once by specifying a list (or +tuple) of component parameters. Consider the file ``XW.tab``: + +.. literalinclude:: /src/dataportal/XW.tab + :language: none + +In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for parameters +``x`` and ``w``: + +.. literalinclude:: /src/dataportal/dataportal_tab_param4.spy + :language: python + +Selecting Parameter Columns +""""""""""""""""""""""""""" + +We have previously noted that the column names do not need to be +specified to load set and parameter data. However, the ``select`` +option can be to identify the columns in the table that are used to load +parameter data. This option specifies a list (or tuple) of column names +that are used, in that order, to form the table that defines the +component data. + +For example, consider the following load declaration: + +.. literalinclude:: /src/dataportal/dataportal_tab_param5.spy + :language: python + +The columns ``A`` and ``W`` are selected from the file ``XW.tab``, and a +single parameter is defined. + +Loading a Parameter Array +""""""""""""""""""""""""" + +Consider the file ``U.tab``, which defines an array representation of a +multiply-indexed parameter: + +.. literalinclude:: /src/dataportal/U.tab + :language: none + +In the following example, a :class:`~pyomo.environ.DataPortal` object loads data for a +two-dimensional parameter ``u``: + +.. literalinclude:: /src/dataportal/dataportal_tab_param6.spy + :language: python + +The ``format`` option indicates that the parameter data is declared in a +array format. The ``format`` option can also indicate that the +parameter data should be transposed. + +.. literalinclude:: /src/dataportal/dataportal_tab_param7.spy + :language: python + +Note that the transposed parameter data changes the index set for the +parameter. + +Loading from Spreadsheets and Databases +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Tabular data can be loaded from spreadsheets and databases using +auxiliary Python packages that provide an interface to these data +formats. Data can be loaded from Excel spreadsheets using the +``win32com``, ``xlrd`` and ``openpyxl`` packages. For example, consider +the following range of cells, which is named ``PPtable``: + +.. image:: PP.png + :width: 2.5in + +In the following example, a :class:`~pyomo.environ.DataPortal` object loads the named range +``PPtable`` from the file ``excel.xls``: + +.. literalinclude:: /src/dataportal/dataportal_tab_excel1.spy + :language: python + +Note that the ``range`` option is required to specify the table of cell +data that is loaded from the spreadsheet. + +There are a variety of ways that data can be loaded from a relational +database. In the simplest case, a table can be specified within a +database: + +.. literalinclude:: /src/dataportal/dataportal_tab_db1.spy + :language: python + +In this example, the interface ``sqlite3`` is used to load data from an +SQLite database in the file ``PP.sqlite``. More generally, an SQL query +can be specified to dynamically generate a table. For example: + +.. literalinclude:: /src/dataportal/dataportal_tab_db2.spy + :language: python + +Data Namespaces +--------------- + +The :class:`~pyomo.environ.DataPortal` +class supports the concept of a *namespace* to organize data into named +groups that can be enabled or disabled during model construction. +Various :class:`~pyomo.environ.DataPortal` +methods have an optional ``namespace`` argument that defaults to +``None``: + +* ``data(name=None, namespace=None)``: Returns the data associated with + data in the specified namespace + +* ``[]``: For a :class:`~pyomo.environ.DataPortal` object ``data``, the function + ``data['A']`` returns data corresponding to ``A`` in the default + namespace, and ``data['ns1','A']`` returns data corresponding to ``A`` + in namespace ``ns1``. + +* ``namespaces()``: Returns an iteratore for the data namespaces. + +* ``keys(namespace=None)``: Returns an iterator of the data keys in the + specified namespace. + +* ``values(namespace=None)``: Returns and iterator of the data values in + the specified namespace. + +* ``items(namespace=None)``: Returns an iterator of (name, value) tuples + in the specified namespace. + +By default, data within a namespace are ignored during model +construction. However, concrete models can be initialized with data +from a specific namespace. Further, abstract models can be initialized +with a list of namespaces that define the data used to initialized model +components. For example, the following script generates two model +instances from an abstract model using data loaded into different +namespaces: + +.. literalinclude:: /src/dataportal/dataportal_tab_namespaces1.spy + :language: python + + diff --git a/doc/OnlineDocs/howto/abstract_models/data/datfiles.rst b/doc/OnlineDocs/howto/abstract_models/data/datfiles.rst new file mode 100644 index 00000000000..1007f03fbcc --- /dev/null +++ b/doc/OnlineDocs/howto/abstract_models/data/datfiles.rst @@ -0,0 +1,933 @@ +.. _page-datfiles: + +Data Command Files +================== + +.. note:: + + The discussion and presentation below are adapted from Chapter 6 of + the "Pyomo Book" [PyomoBookII]_. The discussion of the + :class:`~pyomo.environ.DataPortal` + class uses these same examples to illustrate how data can be loaded + into Pyomo models within Python scripts (see the + :ref:`page-dataportals` section). + +Model Data +---------- + +Pyomo's *data command files* employ a domain-specific language whose +syntax closely resembles the syntax of AMPL's data commands [FGK02]_. A +data command file consists of a sequence of commands that either (a) +specify set and parameter data for a model, or (b) specify where such +data is to be obtained from external sources (e.g. table files, CSV +files, spreadsheets and databases). + +The following commands are used to declare data: + +* The ``set`` command declares set data. + +* The ``param`` command declares a table of parameter data, which + can also include the declaration of the set data used to index the + parameter data. + +* The ``table`` command declares a two-dimensional table of parameter + data. + +* The ``load`` command defines how set and parameter data is loaded from + external data sources, including ASCII table files, CSV files, XML + files, YAML files, JSON files, ranges in spreadsheets, and database + tables. + +The following commands are also used in data command files: + +* The ``include`` command specifies a data command file that is + processed immediately. + +* The ``data`` and ``end`` commands do not perform any actions, but they + provide compatibility with AMPL scripts that define data commands. + +* The ``namespace`` keyword allows data commands to be organized into + named groups that can be enabled or disabled during model + construction. + +The following data types can be represented in a data command file: + +* **Numeric value**: Any Python numeric value (e.g. integer, float, + scientific notation, or boolean). + +* **Simple string**: A sequence of alpha-numeric characters. + +* **Quoted string**: A simple string that is included in a pair of + single or double quotes. A quoted string can include quotes within + the quoted string. + +Numeric values are automatically converted to Python integer or floating +point values when a data command file is parsed. Additionally, if a +quoted string can be interpreted as a numeric value, then it will be +converted to Python numeric types when the data is parsed. For example, +the string "100" is converted to a numeric value automatically. + +.. warning:: + + Pyomo data commands do *not* exactly correspond to AMPL data + commands. The ``set`` and ``param`` commands are designed to + closely match AMPL's syntax and semantics, though these commands + only support a subset of the corresponding declarations in AMPL. + However, other Pyomo data commands are not generally designed to + match the semantics of AMPL. + +.. note:: + + Pyomo data commands are terminated with a semicolon, and the syntax + of data commands does not depend on whitespace. Thus, data commands + can be broken across multiple lines -- newlines and tab characters + are ignored -- and data commands can be formatted with whitespace + with few restrictions. + + +The ``set`` Command +------------------- + +Simple Sets +^^^^^^^^^^^ + +The ``set`` data command explicitly specifies the members of either a +single set or an array of sets, i.e., an indexed set. A single set is +specified with a list of data values that are included in this set. The +formal syntax for the set data command is: + +:: + + set := [] ... ; + +A set may be empty, and it may contain any combination of numeric and +non-numeric string values. For example, the following are valid ``set`` +commands: + +.. literalinclude:: /src/data/set1.dat + :language: python + + +Sets of Tuple Data +^^^^^^^^^^^^^^^^^^ + +The ``set`` data command can also specify tuple data with the standard +notation for tuples. For example, suppose that set ``A`` contains +3-tuples: + +.. literalinclude:: /src/data/set2_decl.spy + :language: python + +The following ``set`` data command then specifies that ``A`` is the set +containing the tuples ``(1,2,3)`` and ``(4,5,6)``: + +.. literalinclude:: /src/data/set2a.dat + :language: none + +Alternatively, set data can simply be listed in the order that the tuple +is represented: + +.. literalinclude:: /src/data/set2.dat + :language: none + +Obviously, the number of data elements specified using this syntax +should be a multiple of the set dimension. + +Sets with 2-tuple data can also be specified in a matrix denoting set +membership. For example, the following ``set`` data command declares +2-tuples in ``A`` using plus (``+``) to denote valid tuples and minus +(``-``) to denote invalid tuples: + +.. literalinclude:: /src/data/set4.dat + :language: none + +This data command declares the following five 2-tuples: ``('A1',1)``, +``('A1',2)``, ``('A2',3)``, ``('A3',2)``, and ``('A4',1)``. + +Finally, a set of tuple data can be concisely represented with tuple +*templates* that represent a *slice* of tuple data. For example, +suppose that the set ``A`` contains 4-tuples: + +.. literalinclude:: /src/data/set5_decl.spy + :language: python + +The following ``set`` data command declares groups of tuples that are +defined by a template and data to complete this template: + +.. literalinclude:: /src/data/set5.dat + :language: none + +A tuple template consists of a tuple that contains one or more asterisk +(``*``) symbols instead of a value. These represent indices where the +tuple value is replaced by the values from the list of values that +follows the tuple template. In this example, the following tuples are +in set ``A``: + +.. literalinclude:: /src/data/set5.txt + :language: none + +Set Arrays +^^^^^^^^^^ + +The ``set`` data command can also be used to declare data for a set +array. Each set in a set array must be declared with a separate ``set`` +data command with the following syntax: + +:: + + set [] := [] ... ; + +Because set arrays can be indexed by an arbitrary set, the index value +may be a numeric value, a non-numeric string value, or a comma-separated +list of string values. + +Suppose that a set ``A`` is used to index a set ``B`` as follows: + +.. literalinclude:: /src/data/set3_decl.spy + :language: python + +Then set ``B`` is indexed using the values declared for set ``A``: + +.. literalinclude:: /src/data/set3.dat + :language: none + +The ``param`` Command +--------------------- + +Simple or non-indexed parameters are declared in an obvious way, as +shown by these examples: + +.. literalinclude:: /src/data/param1.dat + :language: none + +Parameters can be defined with numeric data, simple strings and quoted +strings. Note that parameters cannot be defined without data, so there +is no analog to the specification of an empty set. + +One-dimensional Parameter Data +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Most parameter data is indexed over one or more sets, and there are a +number of ways the ``param`` data command can be used to specify indexed +parameter data. One-dimensional parameter data is indexed over a single +set. Suppose that the parameter ``B`` is a parameter indexed by the set +``A``: + +.. literalinclude:: /src/data/param2_decl.spy + :language: python + +A ``param`` data command can specify values for ``B`` with a list of +index-value pairs: + +.. literalinclude:: /src/data/param2.dat + :language: none + +Because whitespace is ignored, this example data command file can be +reorganized to specify the same data in a tabular format: + +.. literalinclude:: /src/data/param2a.dat + :language: none + +Multiple parameters can be defined using a single ``param`` data +command. For example, suppose that parameters ``B``, ``C``, and ``D`` +are one-dimensional parameters all indexed by the set ``A``: + +.. literalinclude:: /src/data/param3_decl.spy + :language: python + +Values for these parameters can be specified using a single ``param`` +data command that declares these parameter names followed by a list of +index and parameter values: + +.. literalinclude:: /src/data/param3.dat + :language: none + +The values in the ``param`` data command are interpreted as a list of +sublists, where each sublist consists of an index followed by the +corresponding numeric value. + +Note that parameter values do not need to be defined for all indices. +For example, the following data command file is valid: + +.. literalinclude:: /src/data/param3a.dat + :language: none + +The index ``g`` is omitted from the ``param`` command, and consequently +this index is not valid for the model instance that uses this data. +More complex patterns of missing data can be specified using the period +(``.``) symbol to indicate a missing value. This syntax is useful when +specifying multiple parameters that do not necessarily have the same +index values: + +.. literalinclude:: /src/data/param3b.dat + :language: none + +This example provides a concise representation of parameters that share +a common index set while using different index values. + +Note that this data file specifies the data for set ``A`` twice: +(1) when ``A`` is defined and (2) implicitly when the parameters are +defined. An alternate syntax for ``param`` allows the user to concisely +specify the definition of an index set along with associated parameters: + +.. literalinclude:: /src/data/param3c.dat + :language: none + +Finally, we note that default values for missing data can also be +specified using the ``default`` keyword: + +.. literalinclude:: /src/data/param4.dat + :language: none + +Note that default values can only be specified in ``param`` commands +that define values for a single parameter. + + +Multi-Dimensional Parameter Data +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Multi-dimensional parameter data is indexed over either multiple sets or +a single multi-dimensional set. Suppose that parameter ``B`` is a +parameter indexed by set ``A`` that has dimension 2: + +.. literalinclude:: /src/data/param5_decl.spy + :language: python + +The syntax of the ``param`` data command remains essentially the same +when specifying values for ``B`` with a list of index and parameter +values: + +.. literalinclude:: /src/data/param5.dat + :language: none + +Missing and default values are also handled in the same way with +multi-dimensional index sets: + +.. literalinclude:: /src/data/param5a.dat + :language: none + +Similarly, multiple parameters can defined with a single ``param`` data +command. Suppose that parameters ``B``, ``C``, and ``D`` are parameters +indexed over set ``A`` that has dimension 2: + +.. literalinclude:: /src/data/param6_decl.spy + :language: python + +These parameters can be defined with a single ``param`` command that +declares the parameter names followed by a list of index and parameter +values: + +.. literalinclude:: /src/data/param6.dat + :language: none + +Similarly, the following ``param`` data command defines the index set +along with the parameters: + +.. literalinclude:: /src/data/param6a.dat + :language: none + +The ``param`` command also supports a matrix syntax for specifying the +values in a parameter that has a 2-dimensional index. Suppose parameter +``B`` is indexed over set ``A`` that has dimension 2: + +.. literalinclude:: /src/data/param7a_decl.spy + :language: python + +The following ``param`` command defines a matrix of parameter values: + +.. literalinclude:: /src/data/param7a.dat + :language: none + +Additionally, the following syntax can be used to specify a transposed +matrix of parameter values: + +.. literalinclude:: /src/data/param7b.dat + :language: none + +This functionality facilitates the presentation of parameter data in a +natural format. In particular, the transpose syntax may allow the +specification of tables for which the rows comfortably fit within a +single line. However, a matrix may be divided column-wise into shorter +rows since the line breaks are not significant in Pyomo data commands. + +For parameters with three or more indices, the parameter data values may +be specified as a series of slices. Each slice is defined by a template +followed by a list of index and parameter values. Suppose that +parameter ``B`` is indexed over set ``A`` that has dimension 4: + +.. literalinclude:: /src/data/param8a_decl.spy + :language: python + +The following ``param`` command defines a matrix of parameter values +with multiple templates: + +.. literalinclude:: /src/data/param8a.dat + :language: none + +The ``B`` parameter consists of four values: ``B[a,1,a,1]=10``, +``B[b,1,b,1]=20``, ``B[a,2,a,2]=30``, and ``B[b,2,b,2]=40``. + +The ``table`` Command +--------------------- + +The ``table`` data command explicitly specifies a two-dimensional array +of parameter data. This command provides a more flexible and complete +data declaration than is possible with a ``param`` declaration. The +following example illustrates a simple ``table`` command that declares +data for a single parameter: + +.. literalinclude:: /src/data/table0.dat + :language: none + +The parameter ``M`` is indexed by column ``A``, which must be +pre-defined unless declared separately (see below). The column labels +are provided after the colon and before the colon-equal (``:=``). +Subsequently, the table data is provided. The syntax is not sensitive +to whitespace, so the following is an equivalent ``table`` command: + +.. literalinclude:: /src/data/table1.dat + :language: none + +Multiple parameters can be declared by simply including additional +parameter names. For example: + +.. literalinclude:: /src/data/table2.dat + :language: none + +This example declares data for the ``M`` and ``N`` parameters, which +have different indexing columns. The indexing columns represent set +data, which is specified separately. For example: + +.. literalinclude:: /src/data/table3.dat + :language: none + +This example declares data for the ``M`` and ``N`` parameters, along +with the ``A`` and ``Z`` indexing sets. The correspondence between the +index set ``Z`` and the indices of parameter ``N`` can be made more +explicit by indexing ``N`` by ``Z``: + +.. literalinclude:: /src/data/table4.dat + :language: none + +Set data can also be specified independent of parameter data: + +.. literalinclude:: /src/data/table5.dat + :language: none + +.. warning:: + + If a ``table`` command does not explicitly indicate the indexing + sets, then these are assumed to be initialized separately. A + ``table`` command can separately initialize sets and parameters in a + Pyomo model, and there is no presumed association between the data + that is initialized. For example, the ``table`` command initializes + a set ``Z`` and a parameter ``M`` that are not related: + + .. literalinclude:: /src/data/table7.dat + :language: none + +Finally, simple parameter values can also be specified with a ``table`` +command: + +.. literalinclude:: /src/data/table6.dat + :language: none + +The previous examples considered examples of the ``table`` command where +column labels are provided. The ``table`` command can also be used +without column labels. For example, the first example can be revised to +omit column labels as follows: + +.. literalinclude:: /src/data/table0.ul.dat + :language: none + +The ``columns=4`` is a keyword-value pair that defines the number of +columns in this table; this must be explicitly specified in tables +without column labels. The default column labels are integers starting +from ``1``; the labels are columns ``1``, ``2``, ``3``, and ``4`` in +this example. The ``M`` parameter is indexed by column ``1``. The +braces syntax declares the column where the ``M`` data is provided. + +Similarly, set data can be declared referencing the integer column +labels: + +.. literalinclude:: /src/data/table3.ul.dat + :language: none + +Declared set names can also be used to index parameters: + +.. literalinclude:: /src/data/table4.ul.dat + :language: none + +Finally, we compare and contrast the ``table`` and ``param`` commands. +Both commands can be used to declare parameter and set data, and both +commands can be used to declare a simple parameter. However, there are +some important differences between these data commands: + +* The ``param`` command can declare a single set that is used to index + one or more parameters. The ``table`` command can declare data for + any number of sets, independent of whether they are used to index + parameter data. + +* The ``param`` command can declare data for multiple parameters only if + they share the same index set. The ``table`` command can declare data + for any number of parameters that are may be indexed separately. + +* The ``table`` syntax unambiguously describes the dimensionality of + indexing sets. The ``param`` command must be interpreted with a model + that provides the dimension of the indexing set. + +This last point provides a key motivation for the ``table`` command. +Specifically, the ``table`` command can be used to reliably initialize +concrete models using Pyomo's :class:`~pyomo.environ.DataPortal` class. By contrast, the +``param`` command can only be used to initialize concrete models with +parameters that are indexed by a single column (i.e., a simple set). + +The ``load`` Command +-------------------- + +The ``load`` command provides a mechanism for loading data from a +variety of external tabular data sources. This command loads a table of +data that represents set and parameter data in a Pyomo model. The table +consists of rows and columns for which all rows have the same length, +all columns have the same length, and the first row represents labels +for the column data. + +The ``load`` command can load data from a variety of different external +data sources: + +* **TAB File**: A text file format that uses whitespace to separate + columns of values in each row of a table. + +* **CSV File**: A text file format that uses comma or other delimiters + to separate columns of values in each row of a table. + +* **XML File**: An extensible markup language for documents and data + structures. XML files can represent tabular data. + +* **Excel File**: A spreadsheet data format that is primarily used by + the Microsoft Excel application. + +* **Database**: A relational database. + +This command uses a *data manager* that coordinates how data is +extracted from a specified *data source*. In this way, the ``load`` +command provides a generic mechanism that enables Pyomo models to +interact with standard data repositories that are maintained in an +application-specific manner. + +Simple Load Examples +^^^^^^^^^^^^^^^^^^^^ + +The simplest illustration of the ``load`` command is specifying data for +an indexed parameter. Consider the file ``Y.tab``: + +.. literalinclude:: /src/data/Y.tab + :language: none + +This file specifies the values of parameter ``Y`` which is indexed by +set ``A``. The following ``load`` command loads the parameter data: + +.. literalinclude:: /src/data/import1.tab.dat + :language: none + +The first argument is the filename. The options after the colon +indicate how the table data is mapped to model data. Option ``[A]`` +indicates that set ``A`` is used as the index, and option ``Y`` +indicates the parameter that is initialized. + +Similarly, the following load command loads both the parameter data as +well as the index set ``A``: + +.. literalinclude:: /src/data/import2.tab.dat + :language: none + +The difference is the specification of the index set, ``A=[A]``, which +indicates that set ``A`` is initialized with the index loaded from the +ASCII table file. + +Set data can also be loaded from a ASCII table file that contains a +single column of data: + +.. literalinclude:: /src/data/A.tab + :language: none + +The ``format`` option must be specified to denote the fact that the +relational data is being interpreted as a set: + +.. literalinclude:: /src/data/import3.tab.dat + :language: none + +Note that this allows for specifying set data that contains tuples. +Consider file ``C.tab``: + +.. literalinclude:: /src/data/C.tab + :language: none + +A similar ``load`` syntax will load this data into set ``C``: + +.. literalinclude:: /src/data/import4.tab.dat + :language: none + +Note that this example requires that ``C`` be declared with dimension +two. + +Load Syntax Options +^^^^^^^^^^^^^^^^^^^ + +The syntax of the ``load`` command is broken into two parts. The first +part ends with the colon, and it begins with a filename, database URL, +or DSN (data source name). Additionally, this first part can contain +option value pairs. The following options are recognized: + +.. list-table:: + + * - ``format`` + - A string that denotes how the relational table is interpreted + * - ``password`` + - The password that is used to access a database + * - ``query`` + - The query that is used to request data from a database + * - ``range`` + - The subset of a spreadsheet that is requested\index{spreadsheet} + * - ``user`` + - The user name that is used to access the data source + * - ``using`` + - The data manager that is used to process the data source + * - ``table`` + - The database table that is requested + +The ``format`` option is the only option that is required for all data +managers. This option specifies how a relational table is interpreted +to represent set and parameter data. If the ``using`` option is +omitted, then the filename suffix is used to select the data manager. +The remaining options are specific to spreadsheets and relational +databases (see below). + +The second part of the ``load`` command consists of the specification of +column names for indices and data. The remainder of this section +describes different specifications and how they define how data is +loaded into a model. Suppose file ``ABCD.tab`` defines the following +relational table: + +.. literalinclude:: /src/data/ABCD.tab + :language: none + +There are many ways to interpret this relational table. It could +specify a set of 4-tuples, a parameter indexed by 3-tuples, two +parameters indexed by 2-tuples, and so on. Additionally, we may wish to +select a subset of this table to initialize data in a model. +Consequently, the ``load`` command provides a variety of syntax options +for specifying how a table is interpreted. + +A simple specification is to interpret the relational table as a set: + +.. literalinclude:: /src/data/ABCD1.dat + :language: none + +Note that ``Z`` is a set in the model that the data is being loaded +into. If this set does not exist, an error will occur while loading +data from this table. + +Another simple specification is to interpret the relational table as a +parameter with indexed by 3-tuples: + +.. literalinclude:: /src/data/ABCD2.dat + :language: none + +Again, this requires that ``D`` be a parameter in the model that the +data is being loaded into. Additionally, the index set for ``D`` must +contain the indices that are specified in the table. The ``load`` +command also allows for the specification of the index set: + +.. literalinclude:: /src/data/ABCD3.dat + :language: none + +This specifies that the index set is loaded into the ``Z`` set in the +model. Similarly, data can be loaded into another parameter than what +is specified in the relational table: + +.. literalinclude:: /src/data/ABCD4.dat + :language: none + +This specifies that the index set is loaded into the ``Z`` set and that +the data in the ``D`` column in the table is loaded into the ``Y`` +parameter. + +This syntax allows the ``load`` command to provide an arbitrary +specification of data mappings from columns in a relational table into +index sets and parameters. For example, suppose that a model is defined +with set ``Z`` and parameters ``Y`` and ``W``: + +.. literalinclude:: /src/data/ABCD5_decl.spy + :language: python + +Then the following command defines how these data items are loaded using +columns ``B``, ``C`` and ``D``: + +.. literalinclude:: /src/data/ABCD5.dat + :language: none + +When the ``using`` option is omitted the data manager is inferred from +the filename suffix. However, the filename suffix does not always +reflect the format of the data it contains. For example, consider the +relational table in the file ``ABCD.txt``: + +.. literalinclude:: /src/data/ABCD.txt + :language: none + +We can specify the ``using`` option to load from this file into +parameter ``D`` and set ``Z``: + +.. literalinclude:: /src/data/ABCD6.dat + :language: none + +.. note:: + + The data managers supported by Pyomo can be listed with the + ``pyomo help`` subcommand + + :: + + pyomo help --data-managers + + The following data managers are supported in Pyomo 5.1: + + .. literalinclude:: /src/data/data_managers.txt + :language: none + +Interpreting Tabular Data +^^^^^^^^^^^^^^^^^^^^^^^^^ + +By default, a table is interpreted as columns of one or more parameters +with associated index columns. The ``format`` option can be used to +specify other interpretations of a table: + +.. list-table:: + + * - ``array`` + - The table is a matrix representation of a two dimensional + parameter. + * - ``param`` + - The data is a simple parameter value. + * - ``set`` + - Each row is a set element. + * - ``set_array`` + - The table is a matrix representation of a set of 2-tuples. + * - ``transposed_array`` + - The table is a transposed matrix representation of a two + dimensional parameter. + +We have previously illustrated the use of the ``set`` format value to +interpret a relational table as a set of values or tuples. The +following examples illustrate the other format values. + +A table with a single value can be interpreted as a simple parameter +using the ``param`` format value. Suppose that ``Z.tab`` contains the +following table: + +.. literalinclude:: /src/data/Z.tab + :language: none + +The following load command then loads this value into parameter ``p``: + +.. literalinclude:: /src/data/import6.tab.dat + :language: none + +Sets with 2-tuple data can be represented with a matrix format that +denotes set membership. The ``set_array`` format value interprets a +relational table as a matrix that defines a set of 2-tuples where ``+`` +denotes a valid tuple and ``-`` denotes an invalid tuple. Suppose that +``D.tab`` contains the following relational table: + +.. literalinclude:: /src/data/D.tab + :language: none + +Then the following load command loads data into set ``B``: + +.. literalinclude:: /src/data/import5.tab.dat + :language: none + +This command declares the following 2-tuples: ``('A1',1)``, +``('A2',2)``, and ``('A3',3)``. + +Parameters with 2-tuple indices can be interpreted with a matrix format +that where rows and columns are different indices. Suppose that +``U.tab`` contains the following table: + +.. literalinclude:: /src/data/U.tab + :language: none + +Then the following load command loads this value into parameter ``U`` +with a 2-dimensional index using the ``array`` format value.: + +.. literalinclude:: /src/data/import7.tab.dat + :language: none + +The ``transpose_array`` format value also interprets the table as a +matrix, but it loads the data in a transposed format: + +.. literalinclude:: /src/data/import8.tab.dat + :language: none + +Note that these format values do not support the initialization of the +index data. + +Loading from Spreadsheets and Relational Databases +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Many of the options for the ``load`` command are specific to +spreadsheets and relational databases. The ``range`` option is used to +specify the range of cells that are loaded from a spreadsheet. The +range of cells represents a table in which the first row of cells +defines the column names for the table. + +Suppose that file ``ABCD.xls`` contains the range ``ABCD`` that is shown +in the following figure: + +.. image:: ABCD.png + +The following command loads this data to initialize parameter ``D`` and +index ``Z``: + +.. literalinclude:: /src/data/ABCD7.dat + :language: none + +Thus, the syntax for loading data from spreadsheets only differs from +CSV and ASCII text files by the use of the ``range`` option. + +When loading from a relational database, the data source specification +is a filename or data connection string. Access to a database may be +restricted, and thus the specification of ``username`` and ``password`` +options may be required. Alternatively, these options can be specified +within a data connection string. + +A variety of database interface packages are available within Python. +The ``using`` option is used to specify the database interface package +that will be used to access a database. For example, the ``pyodbc`` +interface can be used to connect to Excel spreadsheets. The following +command loads data from the Excel spreadsheet ``ABCD.xls`` using the +``pyodbc`` interface. The command loads this data to initialize +parameter ``D`` and index ``Z``: + +.. literalinclude:: /src/data/ABCD8.dat + :language: none + +The ``using`` option specifies that the ``pyodbc`` package will be +used to connect with the Excel spreadsheet. The ``table`` option +specifies that the table ``ABCD`` is loaded from this spreadsheet. +Similarly, the following command specifies a data connection string +to specify the ODBC driver explicitly: + +.. literalinclude:: /src/data/ABCD9.dat + :language: none + +ODBC drivers are generally tailored to the type of data source that +they work with; this syntax illustrates how the ``load`` command +can be tailored to the details of the database that a user is working +with. + +The previous examples specified the ``table`` option, which declares the +name of a relational table in a database. Many databases support the +Structured Query Language (SQL), which can be used to dynamically +compose a relational table from other tables in a database. The classic +diet problem will be used to illustrate the use of SQL queries to +initialize a Pyomo model. In this problem, a customer is faced with the +task of minimizing the cost for a meal at a fast food restaurant -- they +must purchase a sandwich, side, and a drink for the lowest cost. The +following is a Pyomo model for this problem: + +.. literalinclude:: /src/data/diet1.py + :language: python + +Suppose that the file ``diet1.sqlite`` be a SQLite database file that +contains the following data in the ``Food`` table: + +.. list-table:: + :header-rows: 1 + + * - **FOOD** + - **cost** + + * - Cheeseburger + - 1.84 + + * - Ham Sandwich + - 2.19 + + * - Hamburger + - 1.84 + + * - Fish Sandwich + - 1.44 + + * - Chicken Sandwich + - 2.29 + + * - Fries + - 0.77 + + * - Sausage Biscuit + - 1.29 + + * - Lowfat Milk + - 0.60 + + * - Orange Juice + - 0.72 + +In addition, the ``Food`` table has two additional columns, ``f_min`` +and ``f_max``, with no data for any row. These columns exist to match +the structure for the parameters used in the model. + +We can solve the ``diet1`` model using the Python definition in +``diet1.py`` and the data from this database. The file +``diet.sqlite.dat`` specifies a ``load`` command that uses that +``sqlite3`` data manager and embeds a SQL query to retrieve the data: + +.. literalinclude:: /src/data/diet.sqlite.dat + :language: none + +The PyODBC driver module will pass the SQL query through an Access ODBC +connector, extract the data from the ``diet1.mdb`` file, and return it +to Pyomo. The Pyomo ODBC handler can then convert the data received into +the proper format for solving the model internally. More complex SQL +queries are possible, depending on the underlying database and ODBC +driver in use. However, the name and ordering of the columns queried are +specified in the Pyomo data file; using SQL wildcards (e.g., ``SELECT +*``) or column aliasing (e.g., ``SELECT f AS FOOD``) may cause errors in +Pyomo's mapping of relational data to parameters. + +The ``include`` Command +----------------------- + +The ``include`` command allows a data command file to execute data +commands from another file. For example, the following command file +executes data commands from ``ex1.dat`` and then ``ex2.dat``: + +.. literalinclude:: /src/data/ex.dat + :language: none + +Pyomo is sensitive to the order of execution of data commands, since +data commands can redefine set and parameter values. The ``include`` +command respects this data ordering; all data commands in the included +file are executed before the remaining data commands in the current file +are executed. + +The ``namespace`` Keyword +------------------------- + +The ``namespace`` keyword is not a data command, but instead it is used +to structure the specification of Pyomo's data commands. Specifically, +a namespace declaration is used to group data commands and to provide a +group label. Consider the following data command file: + +.. literalinclude:: /src/data/namespace1.dat + :language: none + +This data file defines two namespaces: ``ns1`` and ``ns2`` that +initialize a set ``C``. By default, data commands contained within a +namespace are ignored during model construction; when no namespaces are +specified, the set ``C`` has values ``1,2,3``. When namespace ``ns1`` +is specified, then the set ``C`` values are overridden with the set +``4,5,6``. + diff --git a/doc/OnlineDocs/howto/abstract_models/data/index.rst b/doc/OnlineDocs/howto/abstract_models/data/index.rst new file mode 100644 index 00000000000..94e942257f7 --- /dev/null +++ b/doc/OnlineDocs/howto/abstract_models/data/index.rst @@ -0,0 +1,66 @@ +.. _page-managingdata: + +Managing Data in AbstractModels +=============================== + +There are roughly three ways of using data to construct a Pyomo +model: + +1. use standard Python objects, + +2. initialize a model with data loaded with a + :class:`~pyomo.environ.DataPortal` object, and + +3. load model data from a Pyomo data command file. + +Standard Python data objects include native Python data types (e.g. +lists, sets, and dictionaries) as well as standard data formats +like numpy arrays and Pandas data frames. Standard Python data +objects can be used to define constant values in a Pyomo model, and +they can be used to initialize :class:`~pyomo.environ.Set` +and :class:`~pyomo.environ.Param` components. +However, initializing :class:`~pyomo.environ.Set` +and :class:`~pyomo.environ.Param` components in +this manner provides few advantages over direct use of standard +Python data objects. (An import exception is that components indexed +by :class:`~pyomo.environ.Set` objects use less +memory than components indexed by native Python data.) + +The :class:`~pyomo.environ.DataPortal` +class provides a generic facility for loading data from disparate +sources. A :class:`~pyomo.environ.DataPortal` +object can load data in a consistent manner, and this data can be +used to simply initialize all :class:`~pyomo.environ.Set` +and :class:`~pyomo.environ.Param` components in +a model. :class:`~pyomo.environ.DataPortal` +objects can be used to initialize both concrete and abstract models +in a uniform manner, which is important in some scripting applications. +But in practice, this capability is only necessary for abstract +models, whose data components are initialized after being constructed. (In fact, +all abstract data components in an abstract model are loaded from +:class:`~pyomo.environ.DataPortal` objects.) + +Finally, Pyomo data command files provide a convenient mechanism +for initializing :class:`~pyomo.environ.Set` and +:class:`~pyomo.environ.Param` components with a +high-level data specification. Data command files can be used with +both concrete and abstract models, though in a different manner. +Data command files are parsed using a :class:`~pyomo.environ.DataPortal` object, which must be done +explicitly for a concrete model. However, abstract models can load +data from a data command file directly, after the model is constructed. +Again, this capability is only necessary for abstract models, whose +data components are initialized after being constructed. + +The following sections provide more detail about how data can be +used to initialize Pyomo models. + + +.. toctree:: + :maxdepth: 1 + + native.rst + raw_dicts.rst + datfiles.rst + dataportals.rst + storing_data.rst + diff --git a/doc/OnlineDocs/howto/abstract_models/data/native.rst b/doc/OnlineDocs/howto/abstract_models/data/native.rst new file mode 100644 index 00000000000..6ec4545560c --- /dev/null +++ b/doc/OnlineDocs/howto/abstract_models/data/native.rst @@ -0,0 +1,84 @@ +Using Standard Data Types +========================= + +Defining Constant Values +------------------------ + +In many cases, Pyomo models can be constructed without :class:`~pyomo.environ.Set` and :class:`~pyomo.environ.Param` data components. Native Python data types +class can be simply used to define constant values in Pyomo expressions. +Consequently, Python sets, lists and dictionaries can be used to +construct Pyomo models, as well as a wide range of other Python classes. + +.. admonition:: TODO + + More examples here: set, list, dict, numpy, pandas. + + +Initializing Set and Parameter Components +----------------------------------------- + +The :class:`~pyomo.environ.Set` and :class:`~pyomo.environ.Param` components used in a Pyomo model +can also be initialized with standard Python data types. This +enables some modeling efficiencies when manipulating sets (e.g. +when re-using sets for indices), and it supports validation of set +and parameter data values. The :class:`~pyomo.environ.Set` +and :class:`~pyomo.environ.Param` components are +initialized with Python data using the ``initialize`` option. + +Set Components +^^^^^^^^^^^^^^ + +In general, :class:`~pyomo.environ.Set` components +can be initialized with iterable data. For example, simple sets +can be initialized with: + +* list, set and tuple data: + + .. literalinclude:: /src/dataportal/set_initialization_decl2.spy + :language: python + +* generators: + + .. literalinclude:: /src/dataportal/set_initialization_decl3.spy + :language: python + +* numpy arrays: + + .. literalinclude:: /src/dataportal/set_initialization_decl4.spy + :language: python + +Sets can also be indirectly initialized with functions that return +native Python data: + +.. literalinclude:: /src/dataportal/set_initialization_decl5.spy + :language: python + +Indexed sets can be initialized with dictionary data where the +dictionary values are iterable data: + +.. literalinclude:: /src/dataportal/set_initialization_decl6.spy + :language: python + + +Parameter Components +^^^^^^^^^^^^^^^^^^^^ + +When a parameter is a single value, then a :class:`~pyomo.environ.Param` component can be simply initialized with a +value: + +.. literalinclude:: /src/dataportal/param_initialization_decl1.spy + :language: python + +More generally, :class:`~pyomo.environ.Param` +components can be initialized with dictionary data where the dictionary +values are single values: + +.. literalinclude:: /src/dataportal/param_initialization_decl2.spy + :language: python + +Parameters can also be indirectly initialized with functions that +return native Python data: + +.. literalinclude:: /src/dataportal/param_initialization_decl3.spy + :language: python + diff --git a/doc/OnlineDocs/howto/abstract_models/data/raw_dicts.rst b/doc/OnlineDocs/howto/abstract_models/data/raw_dicts.rst new file mode 100644 index 00000000000..f78e349c28b --- /dev/null +++ b/doc/OnlineDocs/howto/abstract_models/data/raw_dicts.rst @@ -0,0 +1,53 @@ +.. _page-data-from-dict: + +Using a Python Dictionary +========================= + +Data can be passed to the model +:meth:`~pyomo.environ.AbstractModel.create_instance` method +through a series of nested native Python dictionaries. The structure +begins with a dictionary of *namespaces*, with the only required entry +being the ``None`` namespace. Each namespace contains a dictionary that +maps component names to dictionaries of component values. For scalar +components, the required data dictionary maps the implicit index +``None`` to the desired value: + + .. doctest:: + + >>> from pyomo.environ import * + >>> m = AbstractModel() + >>> m.I = Set() + >>> m.p = Param() + >>> m.q = Param(m.I) + >>> m.r = Param(m.I, m.I, default=0) + >>> data = {None: { + ... 'I': {None: [1,2,3]}, + ... 'p': {None: 100}, + ... 'q': {1: 10, 2:20, 3:30}, + ... 'r': {(1,1): 110, (1,2): 120, (2,3): 230}, + ... }} + >>> i = m.create_instance(data) + >>> i.pprint() + 1 Set Declarations + I : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {1, 2, 3} + + 3 Param Declarations + p : Size=1, Index=None, Domain=Any, Default=None, Mutable=False + Key : Value + None : 100 + q : Size=3, Index=I, Domain=Any, Default=None, Mutable=False + Key : Value + 1 : 10 + 2 : 20 + 3 : 30 + r : Size=9, Index=I*I, Domain=Any, Default=0, Mutable=False + Key : Value + (1, 1) : 110 + (1, 2) : 120 + (2, 3) : 230 + + 4 Declarations: I p q r + + diff --git a/doc/OnlineDocs/howto/abstract_models/data/storing_data.rst b/doc/OnlineDocs/howto/abstract_models/data/storing_data.rst new file mode 100644 index 00000000000..7d8b6f89e11 --- /dev/null +++ b/doc/OnlineDocs/howto/abstract_models/data/storing_data.rst @@ -0,0 +1,16 @@ +Storing Data from Pyomo Models +============================== + +Currently, Pyomo has rather limited capabilities for storing model data +into standard Python data types and serialized data formats. However, +this capability is under active development. + + + +Storing Model Data in Excel +--------------------------- + +.. Admonition:: TODO + + More here. + diff --git a/doc/OnlineDocs/howto/abstract_models/index.rst b/doc/OnlineDocs/howto/abstract_models/index.rst new file mode 100644 index 00000000000..47b2286ba5d --- /dev/null +++ b/doc/OnlineDocs/howto/abstract_models/index.rst @@ -0,0 +1,10 @@ +Working with Abstract Models +============================ + +.. toctree:: + :maxdepth: 1 + + instantiating_models.rst + data/index.rst + pyomo_command.rst + BuildAction.rst diff --git a/doc/OnlineDocs/howto/abstract_models/instantiating_models.rst b/doc/OnlineDocs/howto/abstract_models/instantiating_models.rst new file mode 100644 index 00000000000..962e14558eb --- /dev/null +++ b/doc/OnlineDocs/howto/abstract_models/instantiating_models.rst @@ -0,0 +1,121 @@ +Instantiating Models +-------------------- + +If you start with a :class:`~pyomo.environ.ConcreteModel`, each component +you add to the model will be fully constructed and initialized at the +time it attached to the model. However, if you are starting with an +:class:`~pyomo.environ.AbstractModel`, construction occurs in two +phases. When you first declare and attach components to the model, +those components are empty containers and *not* fully constructed, even +if you explicitly provide data. + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> model = pyo.AbstractModel() + >>> model.is_constructed() + False + + >>> model.p = pyo.Param(initialize=5) + >>> model.p.is_constructed() + False + + >>> model.I = pyo.Set(initialize=[1,2,3]) + >>> model.x = pyo.Var(model.I) + >>> model.x.is_constructed() + False + +If you look at the ``model`` at this point, you will see that everything +is "empty": + +.. doctest:: + + >>> model.pprint() + 1 Set Declarations + I : Size=0, Index=None, Ordered=Insertion + Not constructed + + 1 Param Declarations + p : Size=0, Index=None, Domain=Any, Default=None, Mutable=False + Not constructed + + 1 Var Declarations + x : Size=0, Index=I + Not constructed + + 3 Declarations: p I x + +Before you can manipulate modeling components or solve the model, you +must first create a concrete `instance` by applying data to your +abstract model. This can be done using the +:meth:`~pyomo.environ.AbstractModel.create_instance` method, which takes +the abstract model and optional data and returns a new `concrete` +instance by constructing each of the model components in the order in +which they were declared (attached to the model). Note that the +instance creation is performed "out of place"; that is, the original +abstract ``model`` is left untouched. + +.. doctest:: + + >>> instance = model.create_instance() + >>> model.is_constructed() + False + >>> type(instance) + + >>> instance.is_constructed() + True + >>> instance.pprint() + 1 Set Declarations + I : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 3 : {1, 2, 3} + + 1 Param Declarations + p : Size=1, Index=None, Domain=Any, Default=None, Mutable=False + Key : Value + None : 5 + + 1 Var Declarations + x : Size=3, Index=I + Key : Lower : Value : Upper : Fixed : Stale : Domain + 1 : None : None : None : False : True : Reals + 2 : None : None : None : False : True : Reals + 3 : None : None : None : False : True : Reals + + 3 Declarations: p I x + +.. note:: + + AbstractModel users should note that in some examples, your concrete + model instance is called "`instance`" and not "`model`". This + is the case here, where we are explicitly calling + ``instance = model.create_instance()``. + +The :meth:`~pyomo.environ.AbstractModel.create_instance` method can also +take a reference to external data, which overrides any data specified in +the original component declarations. The data can be provided from +several sources, including using a :ref:`dict `, +:ref:`DataPortal `, or :ref:`DAT file +`. For example: + +.. doctest:: + + >>> instance2 = model.create_instance({None: {'I': {None: [4,5]}}}) + >>> instance2.pprint() + 1 Set Declarations + I : Size=1, Index=None, Ordered=Insertion + Key : Dimen : Domain : Size : Members + None : 1 : Any : 2 : {4, 5} + + 1 Param Declarations + p : Size=1, Index=None, Domain=Any, Default=None, Mutable=False + Key : Value + None : 5 + + 1 Var Declarations + x : Size=2, Index=I + Key : Lower : Value : Upper : Fixed : Stale : Domain + 4 : None : None : None : False : True : Reals + 5 : None : None : None : False : True : Reals + + 3 Declarations: p I x diff --git a/doc/OnlineDocs/howto/abstract_models/pyomo_command.rst b/doc/OnlineDocs/howto/abstract_models/pyomo_command.rst new file mode 100644 index 00000000000..13fac82cc71 --- /dev/null +++ b/doc/OnlineDocs/howto/abstract_models/pyomo_command.rst @@ -0,0 +1,123 @@ +The ``pyomo`` Command +===================== + +The ``pyomo`` command is issued to the DOS prompt or a Unix shell. To +see a list of Pyomo command line options, use: + +:: + + pyomo solve --help + +.. note:: + + There are two dashes before ``help``. + +In this section we will detail some of the options. + +Passing Options to a Solver +--------------------------- + +To pass arguments to a solver when using the ``pyomo solve`` command, +append the Pyomo command line with the argument ``--solver-options=`` +followed by an argument that is a string to be sent to the solver +(perhaps with dashes added by Pyomo). So for most MIP solvers, the mip +gap can be set using + +:: + + --solver-options="mipgap=0.01" + +Multiple options are separated by a space. Options that do not take an +argument should be specified with the equals sign followed by either a +space or the end of the string. + +For example, to specify that the solver is GLPK, then to specify a +mipgap of two percent and the GLPK cuts option, use + +:: + + --solver=glpk --solver-options="mipgap=0.02 cuts=" + +If there are multiple "levels" to the keyword, as is the case for some +Gurobi and CPLEX options, the tokens are separated by underscore. For +example, ``mip cuts all`` would be specified as ``mip_cuts_all``. For +another example, to set the solver to be CPLEX, then to set a mip gap of +one percent and to specify 'y' for the sub-option ``numerical`` to the +option ``emphasis`` use + +:: + + --solver=cplex --solver-options="mipgap=0.001 emphasis_numerical=y" + +See :ref:`SolverOpts` for a discussion of passing options in a script. + +Troubleshooting +--------------- + +Many of things that can go wrong are covered by error messages, but +sometimes they can be confusing or do not provide enough +information. Depending on what the troubles are, there might be ways to +get a little additional information. + +If there are syntax errors in the model file, for example, it can +occasionally be helpful to get error messages directly from the Python +interpreter rather than through Pyomo. Suppose the name of the model +file is scuc.py, then + +:: + + python scuc.py + +can sometimes give useful information for fixing syntax errors. + +When there are no syntax errors, but there troubles reading the data or +generating the information to pass to a solver, then the ``--verbose`` +option provides a trace of the execution of Pyomo. The user should be +aware that for some models this option can generate a lot of output. + +If there are troubles with solver (i.e., after Pyomo has output +"Applying Solver"), it is often helpful to use the option +``--stream-solver`` that causes the solver output to be displayed rather +than trapped. (See <> for information about getting this output +in a script). Advanced users may wish to examine the files that are +generated to be passed to a solver. The type of file generated is +controlled by the ``--solver-io`` option and the ``--keepfiles`` option +instructs pyomo to keep the files and output their names. However, the +``--symbolic-solver-labels`` option should usually also be specified so +that meaningful names are used in these files. + +When there seem to be troubles expressing the model, it is often useful +to embed print commands in the model in places that will yield helpful +information. Consider the following snippet: + +.. literalinclude:: /src/scripting/spy4PyomoCommand_Troubleshooting_printed_command.spy + :language: python + +The effect will be to output every member of the set ``model.I`` at the +time the constraint named ``model.AxbConstraint`` is constructed. + +Direct Interfaces to Solvers +---------------------------- + +In many applications, the default solver interface works well. However, +in some cases it is useful to specify the interface using the +``solver-io`` option. For example, if the solver supports a direct +Python interface, then the option would be specified on the command line +as + +:: + + --solver-io=python + +Here are some of the choices: + +- lp: generate a standard linear programming format file with filename + extension ``lp`` +- nlp: generate a file with a standard format that supports linear and + nonlinear optimization with filename extension ``n1lp`` +- os: generate an OSiL format XML file. +- python: use the direct Python interface. + +.. note:: + + Not all solvers support all interfaces. diff --git a/doc/OnlineDocs/howto/index.rst b/doc/OnlineDocs/howto/index.rst index e63a094b66d..1dd04dd056c 100644 --- a/doc/OnlineDocs/howto/index.rst +++ b/doc/OnlineDocs/howto/index.rst @@ -7,5 +7,6 @@ How-To Guides interrogating manipulating solver_recipes + abstract_models/index.rst debugging contribution_guide From 0bdc03af2f3a9cf86524fced98b77972c559abb7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 1 Oct 2024 14:32:58 -0600 Subject: [PATCH 2476/3044] Fix incorrect class references --- doc/OnlineDocs/reference/topical/aml/index.rst | 2 +- .../reference/topical/expressions/classes.rst | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/OnlineDocs/reference/topical/aml/index.rst b/doc/OnlineDocs/reference/topical/aml/index.rst index da727a99629..bdc3c5529d0 100644 --- a/doc/OnlineDocs/reference/topical/aml/index.rst +++ b/doc/OnlineDocs/reference/topical/aml/index.rst @@ -11,7 +11,7 @@ through the :mod:`pyomo.environ` namespace. ~pyomo.core.base.PyomoModel.AbstractModel ~pyomo.core.base.block.Block ~pyomo.core.base.set.Set - ~pyomo.core.base.rangeset.RangeSet + ~pyomo.core.base.set.RangeSet ~pyomo.core.base.param.Param ~pyomo.core.base.var.Var ~pyomo.core.base.objective.Objective diff --git a/doc/OnlineDocs/reference/topical/expressions/classes.rst b/doc/OnlineDocs/reference/topical/expressions/classes.rst index e2ad32875c5..786651be2a6 100644 --- a/doc/OnlineDocs/reference/topical/expressions/classes.rst +++ b/doc/OnlineDocs/reference/topical/expressions/classes.rst @@ -40,13 +40,14 @@ Other Public Classes .. autosummary:: NegationExpression - ExternalFunctionExpression + AbsExpression + UnaryFunctionExpression ProductExpression DivisionExpression - InequalityExpression - EqualityExpression SumExpression - GetItemExpression Expr_ifExpression - UnaryFunctionExpression - AbsExpression + ExternalFunctionExpression + pyomo.core.expr.relational_expr.EqualityExpression + pyomo.core.expr.relational_expr.InequalityExpression + pyomo.core.expr.relational_expr.RangedExpression + pyomo.core.expr.template_expr.GetItemExpression From 3152c31cc4e6c0139b1821fc61cae4d2de53f542 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 1 Oct 2024 14:35:28 -0600 Subject: [PATCH 2477/3044] Remove autosectionlabel, update references --- doc/OnlineDocs/conf.py | 2 +- doc/OnlineDocs/explanation/analysis/iis.rst | 8 ++++++-- doc/OnlineDocs/explanation/solvers/pynumero/index.rst | 2 +- .../pynumero/tutorial.block_vectors_and_matrices.rst | 2 +- .../solvers/pynumero/tutorial.nlp_interfaces.rst | 4 ++-- doc/OnlineDocs/reference/bibliography.rst | 2 ++ 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 4d784d0fbb8..3e8241230cf 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -84,7 +84,7 @@ 'sphinx.ext.todo', 'sphinx_copybutton', 'enum_tools.autoenum', - 'sphinx.ext.autosectionlabel', + #'sphinx.ext.autosectionlabel', #'sphinx.ext.githubpages', ] diff --git a/doc/OnlineDocs/explanation/analysis/iis.rst b/doc/OnlineDocs/explanation/analysis/iis.rst index e4d9a81c9cf..773560c4e28 100644 --- a/doc/OnlineDocs/explanation/analysis/iis.rst +++ b/doc/OnlineDocs/explanation/analysis/iis.rst @@ -3,8 +3,8 @@ Infeasibility Diagnostics There are two closely related tools for infeasibility diagnosis: - - :ref:`Infeasible Irreducible System (IIS) Tool` - - :ref:`Minimal Intractable System finder (MIS) Tool` + - :ref:`iis` + - :ref:`mis` The first simply provides a conduit for solvers that compute an infeasible irreducible system (e.g., Cplex, Gurobi, or Xpress). The @@ -12,6 +12,8 @@ second provides similar functionality, but uses the ``mis`` package contributed to Pyomo. +.. _iis: + Infeasible Irreducible System (IIS) Tool ======================================== @@ -21,6 +23,8 @@ Infeasible Irreducible System (IIS) Tool .. autofunction:: pyomo.contrib.iis.write_iis :noindex: +.. _mis: + Minimal Intractable System finder (MIS) Tool ============================================ diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/index.rst b/doc/OnlineDocs/explanation/solvers/pynumero/index.rst index 9fd6627b0c2..c0507e01db8 100644 --- a/doc/OnlineDocs/explanation/solvers/pynumero/index.rst +++ b/doc/OnlineDocs/explanation/solvers/pynumero/index.rst @@ -5,7 +5,7 @@ PyNumero PyNumero is a package for developing parallel algorithms for nonlinear programs (NLPs). This documentation provides a brief introduction to -PyNumero. For more details, see the API documentation (:ref:`pynumero_api`). +PyNumero. For more details, see the :mod:`API documentation `). .. toctree:: :maxdepth: 2 diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst b/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst index 1ce98ce4a63..165c8b53f34 100644 --- a/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst +++ b/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst @@ -269,4 +269,4 @@ Nested blocks: Nested `BlockMatrix` applications work similarly. -For more information, see the API documentation (:ref:`pynumero_api`). +For more information, see the :mod:`API documentation `. diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.nlp_interfaces.rst b/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.nlp_interfaces.rst index 28818709330..832ba521052 100644 --- a/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.nlp_interfaces.rst +++ b/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.nlp_interfaces.rst @@ -2,8 +2,8 @@ NLP Interfaces ============== Below are examples of using PyNumero's interfaces to ASL for function -and derivative evaluation. More information can be found in the API -documentation (:ref:`pynumero_api`). +and derivative evaluation. More information can be found in the +:mod:`API documentation `. Relevant imports diff --git a/doc/OnlineDocs/reference/bibliography.rst b/doc/OnlineDocs/reference/bibliography.rst index 42aafecc33b..3d580ca9e2e 100644 --- a/doc/OnlineDocs/reference/bibliography.rst +++ b/doc/OnlineDocs/reference/bibliography.rst @@ -1,3 +1,5 @@ +.. _publications: + Publications ============ From 23547651cc8e0b719c02159fa76a6ffcdd9c3569 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 1 Oct 2024 14:36:10 -0600 Subject: [PATCH 2478/3044] General update of documentation formatting --- .../explanation/modeling/math_programming/expressions.rst | 2 +- doc/OnlineDocs/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/explanation/modeling/math_programming/expressions.rst b/doc/OnlineDocs/explanation/modeling/math_programming/expressions.rst index 98635f8009f..f272607718d 100644 --- a/doc/OnlineDocs/explanation/modeling/math_programming/expressions.rst +++ b/doc/OnlineDocs/explanation/modeling/math_programming/expressions.rst @@ -178,7 +178,7 @@ piecewise constraint must have bounds. .. literalinclude:: /src/scripting/abstract2piece.py :language: python -A more advanced example is provided in abstract2piecebuild.py in +A more advanced example is provided in ``abstract2piecebuild.py`` in :ref:`BuildAction`. ``Expression`` Objects diff --git a/doc/OnlineDocs/index.rst b/doc/OnlineDocs/index.rst index 0cd9b7d07af..0fb6aebe821 100644 --- a/doc/OnlineDocs/index.rst +++ b/doc/OnlineDocs/index.rst @@ -42,7 +42,7 @@ Contents .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :titlesonly: :hidden: From 38375e7a835ebb79882cfef6d7f6d7a31ce47137 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 1 Oct 2024 14:49:15 -0600 Subject: [PATCH 2479/3044] Clean up docstrings, silence Sphinx warnings and errors --- pyomo/common/download.py | 1 + pyomo/common/flags.py | 2 +- pyomo/common/numeric_types.py | 22 +- pyomo/common/unittest.py | 123 +++++++-- .../alternative_solutions/aos_utils.py | 60 ++--- .../alternative_solutions/shifted_lp.py | 22 +- pyomo/contrib/appsi/base.py | 15 +- pyomo/contrib/appsi/fbbt.py | 2 + pyomo/contrib/benders/benders_cuts.py | 74 +++--- pyomo/contrib/gjh/GJH.py | 4 +- pyomo/contrib/mindtpy/util.py | 14 +- pyomo/contrib/mpc/data/series_data.py | 23 +- pyomo/contrib/mpc/interfaces/load_data.py | 2 +- .../contrib/mpc/interfaces/model_interface.py | 17 +- .../contrib/mpc/modeling/cost_expressions.py | 24 +- pyomo/contrib/mpc/modeling/terminal.py | 8 +- .../piecewise/piecewise_linear_expression.py | 13 +- pyomo/contrib/pynumero/interfaces/nlp.py | 41 +-- pyomo/contrib/pyros/solve_data.py | 76 ++++-- pyomo/contrib/pyros/util.py | 13 +- pyomo/contrib/sensitivity_toolbox/sens.py | 86 +++--- pyomo/contrib/solver/config.py | 2 +- pyomo/contrib/solver/ipopt.py | 5 +- pyomo/contrib/solver/results.py | 18 +- pyomo/contrib/trustregion/interface.py | 11 +- pyomo/core/base/PyomoModel.py | 9 +- pyomo/core/base/boolean_var.py | 26 +- pyomo/core/base/component.py | 25 +- pyomo/core/base/connector.py | 15 +- pyomo/core/base/constraint.py | 105 +++++--- pyomo/core/base/expression.py | 56 ++-- pyomo/core/base/logical_constraint.py | 21 +- pyomo/core/base/objective.py | 51 ++-- pyomo/core/base/param.py | 4 + pyomo/core/base/piecewise.py | 245 ++++++++++-------- pyomo/core/base/set.py | 19 +- pyomo/core/expr/cnf_walker.py | 12 +- pyomo/core/expr/compare.py | 21 +- pyomo/core/expr/symbol_map.py | 15 +- pyomo/core/expr/template_expr.py | 19 +- pyomo/core/kernel/conic.py | 39 ++- pyomo/core/kernel/container_utils.py | 3 + .../kernel/piecewise_library/transforms.py | 5 +- .../kernel/piecewise_library/transforms_nd.py | 5 +- pyomo/core/plugins/transform/model.py | 8 +- pyomo/core/plugins/transform/standard_form.py | 9 +- pyomo/core/util.py | 10 +- pyomo/dataportal/plugins/datacommands.py | 14 +- pyomo/duality/lagrangian_dual.py | 21 +- pyomo/gdp/plugins/bigm.py | 15 +- pyomo/gdp/plugins/hull.py | 20 +- pyomo/gdp/plugins/partition_disjuncts.py | 41 +-- pyomo/neos/kestrel.py | 7 +- pyomo/neos/plugins/NEOS.py | 2 +- pyomo/opt/plugins/res.py | 8 +- pyomo/opt/plugins/sol.py | 4 +- pyomo/repn/ampl.py | 13 +- pyomo/repn/plugins/lp_writer.py | 12 +- pyomo/repn/plugins/nl_writer.py | 10 +- pyomo/repn/plugins/standard_form.py | 43 ++- pyomo/repn/util.py | 10 +- pyomo/scripting/interface.py | 9 +- pyomo/scripting/util.py | 47 ++-- pyomo/util/slices.py | 10 +- 64 files changed, 1017 insertions(+), 669 deletions(-) diff --git a/pyomo/common/download.py b/pyomo/common/download.py index ad3b64060e9..30e048f37fc 100644 --- a/pyomo/common/download.py +++ b/pyomo/common/download.py @@ -177,6 +177,7 @@ def get_os_version(cls, normalize=True): This method was designed to help identify compatible binaries, and will return strings similar to: + - rhel6 - fedora24 - ubuntu18.04 diff --git a/pyomo/common/flags.py b/pyomo/common/flags.py index 9aa8ece3dbc..9a025fc8003 100644 --- a/pyomo/common/flags.py +++ b/pyomo/common/flags.py @@ -53,7 +53,7 @@ class NOTSET(object, metaclass=FlagType): def in_testing_environment(state=NOTSET): """Return True if we are currently running in a "testing" environment - This currently includes if nose, nose2, pytest, or Sphinx are + This currently includes if ``nose``, ``nose2``, or ``pytest`` are running (imported). Parameters diff --git a/pyomo/common/numeric_types.py b/pyomo/common/numeric_types.py index 2b63038e125..52dd9ec7f5c 100644 --- a/pyomo/common/numeric_types.py +++ b/pyomo/common/numeric_types.py @@ -43,7 +43,7 @@ #: like numpy, which may be registered by users. #: #: Note that :data:`native_numeric_types` does NOT include -#: :py:`complex`, as that is not a valid constant in Pyomo numeric +#: :py:class:`complex`, as that is not a valid constant in Pyomo numeric #: expressions. native_numeric_types = {int, float} native_integer_types = {int} @@ -98,8 +98,8 @@ def RegisterNumericType(new_type: type): Parameters ---------- - new_type: type - The new numeric type (e.g, numpy.float64) + new_type : type + The new numeric type (e.g, `numpy.float64`) """ native_numeric_types.add(new_type) @@ -122,8 +122,8 @@ def RegisterIntegerType(new_type: type): Parameters ---------- - new_type: type - The new integer type (e.g, numpy.int64) + new_type : type + The new integer type (e.g, `numpy.int64`) """ native_numeric_types.add(new_type) @@ -149,8 +149,8 @@ def RegisterBooleanType(new_type: type): Parameters ---------- - new_type: type - The new logical type (e.g, numpy.bool_) + new_type : type + The new logical type (e.g, `numpy.bool_`) """ _native_boolean_types.add(new_type) @@ -171,8 +171,8 @@ def RegisterComplexType(new_type: type): Parameters ---------- - new_type: type - The new complex type (e.g, numpy.complex128) + new_type : type + The new complex type (e.g, `numpy.complex128`) """ native_types.add(new_type) @@ -192,8 +192,8 @@ def RegisterLogicalType(new_type: type): Parameters ---------- - new_type: type - The new logical type (e.g, numpy.bool_) + new_type : type + The new logical type (e.g, `numpy.bool_`) """ _native_boolean_types.add(new_type) diff --git a/pyomo/common/unittest.py b/pyomo/common/unittest.py index 996bb69ec78..14a9024aeea 100644 --- a/pyomo/common/unittest.py +++ b/pyomo/common/unittest.py @@ -111,8 +111,10 @@ def assertStructuredAlmostEqual( values) The relative error is computed for numerical values as - `abs(first - second) / max(abs(first), abs(second))`, - only when first != second (thereby avoiding divide-by-zero errors). + + `abs(first - second) / max(abs(first), abs(second))` + + only when `first != second` (thereby avoiding divide-by-zero errors). Items (entries other than Sequence / Mapping containers, matching strings, and items that satisfy `first is second`) are passed to the @@ -123,37 +125,47 @@ def assertStructuredAlmostEqual( Parameters ---------- - first: + first : the first value to compare - second: + + second : the second value to compare - places: int + + places : int `first` and `second` are considered equivalent if their difference is between `places` decimal places; equivalent to `abstol = 10**-places` (included for compatibility with assertAlmostEqual) - msg: str + + msg : str the message to raise on failure - delta: float + + delta : float alias for `abstol` - abstol: float + + abstol : float the absolute tolerance. `first` and `second` are considered equivalent if their absolute difference is less than `abstol` - reltol: float + + reltol : float the relative tolerance. `first` and `second` are considered equivalent if their absolute difference divided by the largest of `first` and `second` is less than `reltol` - allow_second_superset: bool + + allow_second_superset : bool If True, then extra entries in containers found on second will not trigger a failure. - item_callback: function + + item_callback : function items (other than Sequence / Mapping containers, matching strings, and items satisfying `is`) are passed to this callback to generate the (nominally floating point) value to use for comparison. - exception: Exception + + exception : Exception exception to raise when `first` is not 'almost equal' to `second`. - formatter: function + + formatter : function callback for generating the final failure message (for compatibility with unittest) @@ -349,7 +361,7 @@ def timeout(seconds, require_fork=False, timeout_raises=TimeoutError): using multiprocessing to execute the function in a forked process. If the wrapped function raises an exception, then the exception will be re-raised in this process. If the function times out, a - :python:`TimeoutError` will be raised. + :class:`TimeoutError` will be raised. Note that as this method uses multiprocessing, the wrapped function should NOT spawn any subprocesses. The timeout is implemented using @@ -514,6 +526,8 @@ def assertStructuredAlmostEqual( allow_second_superset=False, item_callback=_floatOrCall, ): + # Note: __doc__ copied from assertStructuredAlmostEqual below + # assertStructuredAlmostEqual( first=first, second=second, @@ -537,18 +551,28 @@ def assertRaisesRegex(self, expected_exception, expected_regex, *args, **kwargs) normalizes all consecutive whitespace in the exception message to a single space before checking the regular expression. - Args: - expected_exception: Exception class expected to be raised. - expected_regex: Regex (re.Pattern object or string) expected - to be found in error message. - args: Function to be called and extra positional args. - kwargs: Extra kwargs. - msg: Optional message used in case of failure. Can only be used - when assertRaisesRegex is used as a context manager. - normalize_whitespace: Optional bool that, if True, collapses - consecutive whitespace (including newlines) into a - single space before checking against the regular - expression + Parameters + ---------- + expected_exception : Exception + Exception class expected to be raised. + + expected_regex : `re.Pattern` or str + Regular expression expected to be found in error message. + + *args : + Function to be called and extra positional args. + + **kwargs : + Extra keyword args. + + msg : str + Optional message used in case of failure. Can only be used + when assertRaisesRegex is used as a context manager. + + normalize_whitespace : bool, default=False + If True, collapses consecutive whitespace (including + newlines) into a single space before checking against the + regular expression """ normalize_whitespace = kwargs.pop('normalize_whitespace', False) @@ -560,6 +584,29 @@ def assertRaisesRegex(self, expected_exception, expected_regex, *args, **kwargs) return context.handle('assertRaisesRegex', args, kwargs) def assertExpressionsEqual(self, a, b, include_named_exprs=True, places=None): + """Assert that two Pyomo expressions are equal. + + This converts the expressions `a` and `b` into prefix notation + and then compares the resulting lists. All nodes in the tree + are compared using py:meth:`assertEqual` (or + py:meth:`assertAlmostEqual`) + + Parameters + ---------- + a: ExpressionBase or native type + + b: ExpressionBase or native type + + include_named_exprs : bool + If True (the default), the comparison expands all named + expressions when generating the prefix notation + + places : float + Number of decimal places required for equality of floating + point numbers in the expression. If None (the default), the + expressions must be exactly equal. + + """ from pyomo.core.expr.compare import assertExpressionsEqual return assertExpressionsEqual(self, a, b, include_named_exprs, places) @@ -567,6 +614,30 @@ def assertExpressionsEqual(self, a, b, include_named_exprs=True, places=None): def assertExpressionsStructurallyEqual( self, a, b, include_named_exprs=True, places=None ): + """Assert that two Pyomo expressions are structurally equal. + + This converts the expressions `a` and `b` into prefix notation + and then compares the resulting lists. Operators and + (non-native type) leaf nodes in the prefix representation are + converted to strings before comparing (so that things like + variables can be compared across clones or pickles) + + Parameters + ---------- + a: ExpressionBase or native type + + b: ExpressionBase or native type + + include_named_exprs: bool + If True (the default), the comparison expands all named + expressions when generating the prefix notation + + places: float + Number of decimal places required for equality of floating + point numbers in the expression. If None (the default), the + expressions must be exactly equal. + + """ from pyomo.core.expr.compare import assertExpressionsStructurallyEqual return assertExpressionsStructurallyEqual( diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index 23fa8e3b4f7..ce14014d266 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -186,36 +186,36 @@ def get_model_variables( include_integer=True, include_fixed=False, ): - """ - Gathers and returns all variables or a subset of variables from a Pyomo - model. - - Parameters - ---------- - model : ConcreteModel - A concrete Pyomo model. - components: None or a collection of Pyomo components - The components from which variables should be collected. None - indicates that all variables will be included. Alternatively, a - collection of Pyomo Blocks, Constraints, or Variables (indexed or - non-indexed) from which variables will be gathered can be provided. - If a Block is provided, all variables associated with constraints - in that that block and its sub-blocks will be returned. To exclude - sub-blocks, a tuple element with the format (Block, False) can be - used. - include_continuous : boolean - Boolean indicating that continuous variables should be included. - include_binary : boolean - Boolean indicating that binary variables should be included. - include_integer : boolean - Boolean indicating that integer variables should be included. - include_fixed : boolean - Boolean indicating that fixed variables should be included. - - Returns - ------- - variable_set - A Pyomo ComponentSet containing _GeneralVarData variables. + """Gathers and returns all variables or a subset of variables from a + Pyomo model. + + Parameters + ---------- + model : ConcreteModel + A concrete Pyomo model. + components: None or a collection of Pyomo components + The components from which variables should be collected. None + indicates that all variables will be included. Alternatively, a + collection of Pyomo Blocks, Constraints, or Variables (indexed or + non-indexed) from which variables will be gathered can be provided. + If a Block is provided, all variables associated with constraints + in that that block and its sub-blocks will be returned. To exclude + sub-blocks, a tuple element with the format (Block, False) can be + used. + include_continuous : boolean + Boolean indicating that continuous variables should be included. + include_binary : boolean + Boolean indicating that binary variables should be included. + include_integer : boolean + Boolean indicating that integer variables should be included. + include_fixed : boolean + Boolean indicating that fixed variables should be included. + + Returns + ------- + variable_set + A Pyomo ComponentSet containing _GeneralVarData variables. + """ component_list = (pe.Objective, pe.Constraint) diff --git a/pyomo/contrib/alternative_solutions/shifted_lp.py b/pyomo/contrib/alternative_solutions/shifted_lp.py index 944651c96c5..2f3ae489ba4 100644 --- a/pyomo/contrib/alternative_solutions/shifted_lp.py +++ b/pyomo/contrib/alternative_solutions/shifted_lp.py @@ -43,18 +43,22 @@ def get_shifted_linear_model(model, block=None): are non-negative reals and all constraints are equalities. For a pure LP of the form, - min/max cx - s.t. - A_1 * x = b_1 - A_2 * x <= b_2 - l <= x <= u + .. math:: + + min/max cx + s.t. + A_1 * x = b_1 + A_2 * x <= b_2 + l <= x <= u a problem of the form, - min/max c'z - s.t. - Bz = q - z >= 0 + .. math:: + + min/max c'z + s.t. + Bz = q + z >= 0 will be created and added to the returned block. z consists of var_lower and var_upper variables that are substituted into the original x variables, diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 9c7da1eb60b..a55b67aa762 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -97,6 +97,9 @@ class TerminationCondition(enum.Enum): class SolverConfig(ConfigDict): """ + Common configuration options for all APPSI solver interfaces + + Attributes ---------- time_limit: float @@ -146,6 +149,8 @@ def __init__( class MIPSolverConfig(SolverConfig): """ + Configuration options common to all MIP solvers + Attributes ---------- mip_gap: float @@ -370,6 +375,8 @@ def get_reduced_costs( class Results(object): """ + Base class for all APPSI solver results + Attributes ---------- termination_condition: TerminationCondition @@ -385,6 +392,8 @@ class Results(object): For solvers that do not provide an objective bound, this should be -inf (minimization) or inf (maximization) + Example + ------- Here is an example workflow: >>> import pyomo.environ as pe @@ -427,6 +436,8 @@ def __str__(self): class UpdateConfig(ConfigDict): """ + Config options common to all persistent solvers + Attributes ---------- check_for_new_or_removed_constraints: bool @@ -632,7 +643,7 @@ def solve(self, model: BlockData, timer: HierarchicalTimer = None) -> Results: Returns ------- - results: Results + results: ~pyomo.contrib.appsi.base.Results A results object """ pass @@ -681,7 +692,7 @@ def config(self): Returns ------- - SolverConfig + ~pyomo.contrib.appsi.base.SolverConfig An object for configuring pyomo solve options such as the time limit. These options are mostly independent of the solver. """ diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 8e0c74b00e9..0422fd2f5bf 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -30,6 +30,8 @@ class IntervalConfig(ConfigDict): """ + Configuration options for the FBBT IntervalTightener + Attributes ---------- feasibility_tol: float diff --git a/pyomo/contrib/benders/benders_cuts.py b/pyomo/contrib/benders/benders_cuts.py index 3eb4fa845ee..60bb6371e52 100644 --- a/pyomo/contrib/benders/benders_cuts.py +++ b/pyomo/contrib/benders/benders_cuts.py @@ -41,41 +41,49 @@ Original problem: -min f(x, y) + h0(y) -s.t. - g(x, y) <= 0 - h(y) <= 0 - -where y are the complicating variables. Reformulate to - -min h0(y) + eta -s.t. - g(x, y) <= 0 - f(x, y) <= eta - h(y) <= 0 - +.. math:: + + \min\ & f(x, y) + h0(y) \\ + s.t.\ & g(x, y) <= 0 \\ + & h(y) <= 0 + +where y are the complicating variables. Reformulate to + +.. math:: + + \min\ & h0(y) + \eta \\ + s.t.\ & g(x, y) <= 0 \\ + & f(x, y) <= \eta \\ + & h(y) <= 0 + Root problem must be of the form -min h0(y) + eta -s.t. - h(y) <= 0 - benders cuts - -where the last constraint will be generated automatically with BendersCutGenerators. The BendersCutGenerators -must be handed a subproblem of the form - -min f(x, y) -s.t. - g(x, y) <= 0 - -except the constraints don't actually have to be in this form. The subproblem will automatically be transformed to - -min _z -s.t. - g(x, y) - z <= 0 (alpha) - f(x, y) - eta - z <= 0 (beta) - y - y_k = 0 (gamma) - eta - eta_k = 0 (delta) +.. math:: + + \min\ & h0(y) + \eta \\ + s.t.\ & h(y) <= 0 \\ + & benders\ cuts + +where the last constraint will be generated automatically with +BendersCutGenerators. The BendersCutGenerators must be handed a +subproblem of the form + +.. math:: + + \min\ & f(x, y) \\ + s.t.\ & g(x, y) <= 0 + +except the constraints don't actually have to be in this form. The +subproblem will automatically be transformed to + +.. math:: + + \min\ & _z & \\ + s.t.\ & g(x, y) - z <= 0 & \quad (\alpha) \\ + & f(x, y) - \eta - z <= 0 & \quad (\beta) \\ + & y - y_k = 0 & \quad (\gamma) \\ + & \eta - \eta_k = 0 & \quad (\delta) \\ + """ diff --git a/pyomo/contrib/gjh/GJH.py b/pyomo/contrib/gjh/GJH.py index dc7c8de89c1..a94d38e24e1 100644 --- a/pyomo/contrib/gjh/GJH.py +++ b/pyomo/contrib/gjh/GJH.py @@ -41,9 +41,9 @@ def readgjh(fname=None): H : list Current objective Hessian. variableList : list - Variables as defined by *.col file. + Variables as defined by `*.col` file. constraintList : list - Constraints as defined by *.row file. + Constraints as defined by `*.row` file. """ if fname is None: diff --git a/pyomo/contrib/mindtpy/util.py b/pyomo/contrib/mindtpy/util.py index 7345af8a3e2..0b552b750f0 100644 --- a/pyomo/contrib/mindtpy/util.py +++ b/pyomo/contrib/mindtpy/util.py @@ -146,7 +146,7 @@ def generate_norm2sq_objective_function(model, setpoint_model, discrete_only=Fal r"""This function generates objective (FP-NLP subproblem) for minimum euclidean distance to setpoint_model. - L2 distance of (x,y) = \sqrt{\sum_i (x_i - y_i)^2}. + L2 distance of :math:`(x,y) = \sqrt{\sum_i (x_i - y_i)^2}`. Parameters ---------- @@ -202,7 +202,7 @@ def generate_norm1_objective_function(model, setpoint_model, discrete_only=False r"""This function generates objective (PF-OA main problem) for minimum Norm1 distance to setpoint_model. - Norm1 distance of (x,y) = \sum_i |x_i - y_i|. + Norm1 distance of :math:`(x,y) = \sum_i |x_i - y_i|`. Parameters ---------- @@ -257,7 +257,7 @@ def generate_norm1_objective_function(model, setpoint_model, discrete_only=False def generate_norm_inf_objective_function(model, setpoint_model, discrete_only=False): r"""This function generates objective (PF-OA main problem) for minimum Norm Infinity distance to setpoint_model. - Norm-Infinity distance of (x,y) = \max_i |x_i - y_i|. + Norm-Infinity distance of :math:`(x,y) = \max_i |x_i - y_i|`. Parameters ---------- @@ -447,7 +447,7 @@ def generate_norm1_norm_constraint(model, setpoint_model, config, discrete_only= Norm constraint is used to guarantees the monotonicity of the norm objective value sequence of all iterations. - Norm1 distance of (x,y) = \sum_i |x_i - y_i|. + Norm1 distance of :math:`(x,y) = \sum_i |x_i - y_i|`. Ref: Paper 'A storm of feasibility pumps for nonconvex MINLP' Eq. (16). Parameters @@ -701,11 +701,7 @@ def copy_var_list_values_from_solution_pool( class GurobiPersistent4MindtPy(GurobiPersistent): - """A new persistent interface to Gurobi. - - Args: - GurobiPersistent (PersistentSolver): A class that provides a persistent interface to Gurobi. - """ + """A new persistent interface to Gurobi.""" def _intermediate_callback(self): def f(gurobi_model, where): diff --git a/pyomo/contrib/mpc/data/series_data.py b/pyomo/contrib/mpc/data/series_data.py index c812e76c9fc..2d79c9170c7 100644 --- a/pyomo/contrib/mpc/data/series_data.py +++ b/pyomo/contrib/mpc/data/series_data.py @@ -25,18 +25,21 @@ class TimeSeriesData(_DynamicDataBase): An object to store time series data associated with time-indexed variables. + Parameters + ---------- + data : dict or ComponentMap + Maps variables, names, or CUIDs to lists of values + + time : list + Contains the time points corresponding to variable data points. + + time_set : ContinuousSetData + + context : BlockData """ def __init__(self, data, time, time_set=None, context=None): - """ - Arguments: - ---------- - data: dict or ComponentMap - Maps variables, names, or CUIDs to lists of values - time: list - Contains the time points corresponding to variable data points. - - """ + """ """ _time = list(time) if _time != list(sorted(time)): raise ValueError("Time points are not sorted in increasing order") @@ -119,7 +122,7 @@ def get_data_at_time(self, time=None, tolerance=0.0): Returns ------- - TimeSeriesData or ScalarData + TimeSeriesData or ~scalar_data.ScalarData TimeSeriesData containing only the specified time points or dict mapping CUIDs to values at the specified scalar time point. diff --git a/pyomo/contrib/mpc/interfaces/load_data.py b/pyomo/contrib/mpc/interfaces/load_data.py index b1851c3aa51..3bf3310f115 100644 --- a/pyomo/contrib/mpc/interfaces/load_data.py +++ b/pyomo/contrib/mpc/interfaces/load_data.py @@ -25,7 +25,7 @@ def load_data_from_scalar(data, model, time): Arguments --------- - data: ScalarData + data: ~scalar_data.ScalarData model: BlockData time: Iterable diff --git a/pyomo/contrib/mpc/interfaces/model_interface.py b/pyomo/contrib/mpc/interfaces/model_interface.py index 9a30878c921..916009049d8 100644 --- a/pyomo/contrib/mpc/interfaces/model_interface.py +++ b/pyomo/contrib/mpc/interfaces/model_interface.py @@ -180,13 +180,14 @@ def load_data( Arguments --------- - data: ScalarData, TimeSeriesData, or mapping - If ScalarData, loads values into indicated variables at - all (or specified) time points. If TimeSeriesData, loads - lists of values into time points. - If mapping, checks whether each variable and value is - indexed or iterable and correspondingly loads data into + data: ~scalar_data.ScalarData, TimeSeriesData, or mapping + If :class:`ScalarData`, loads values into indicated + variables at all (or specified) time points. If + :class:`TimeSeriesData`, loads lists of values into time + points. If mapping, checks whether each variable and value + is indexed or iterable and correspondingly loads data into variables. + time_points: Iterable (optional) Subset of time points into which data should be loaded. Default of None corresponds to loading into all time points. @@ -299,7 +300,7 @@ def get_penalty_from_target( Parameters ---------- - target_data: ScalarData, TimeSeriesData, or IntervalData + target_data: ~scalar_data.ScalarData, TimeSeriesData, or IntervalData Holds target values for variables time: Set (optional) Points at which to apply the tracking cost. Default will use @@ -307,7 +308,7 @@ def get_penalty_from_target( variables: List of Pyomo VarData (optional) Subset of variables supplied in setpoint_data to use in the tracking cost. Default is to use all variables supplied. - weight_data: ScalarData (optional) + weight_data: ~scalar_data.ScalarData (optional) Holds the weights to use in the tracking cost for each variable variable_set: Set (optional) A set indexing the list of provided variables, if one already diff --git a/pyomo/contrib/mpc/modeling/cost_expressions.py b/pyomo/contrib/mpc/modeling/cost_expressions.py index aeb26705a38..9ea0c599d40 100644 --- a/pyomo/contrib/mpc/modeling/cost_expressions.py +++ b/pyomo/contrib/mpc/modeling/cost_expressions.py @@ -48,9 +48,9 @@ def get_penalty_from_constant_target( time: iterable Set of variable indices for which a cost expression will be created - setpoint_data: ScalarData, dict, or ComponentMap + setpoint_data: ~scalar_data.ScalarData, dict, or ComponentMap Maps variable names to setpoint values - weight_data: ScalarData, dict, or ComponentMap + weight_data: ~scalar_data.ScalarData, dict, or ComponentMap Optional. Maps variable names to tracking cost weights. If not provided, weights of one are used. variable_set: Set @@ -123,7 +123,7 @@ def get_penalty_from_piecewise_constant_target( setpoint_data: IntervalData Holds the piecewise constant values that will be used as setpoints - weight_data: ScalarData (optional) + weight_data: ~scalar_data.ScalarData (optional) Weights for variables. Default is all ones. tolerance: Float (optional) Tolerance used for determining whether a time point @@ -220,7 +220,7 @@ def get_penalty_from_time_varying_target( Index used for the cost expression setpoint_data: TimeSeriesData Holds the trajectory values that will be used as a setpoint - weight_data: ScalarData (optional) + weight_data: ~scalar_data.ScalarData (optional) Weights for variables. Default is all ones. variable_set: Set (optional) Set indexing the list of provided variables, if one exists already. @@ -262,11 +262,13 @@ def get_penalty_from_target( """A function to get a penalty expression for specified variables from a target that is constant, piecewise constant, or time-varying. - This function accepts ScalarData, IntervalData, or TimeSeriesData objects, - or compatible mappings/tuples as the target, and builds the appropriate - penalty expression for each. Mappings are converted to ScalarData, and - tuples (of data dict, time list) are unpacked and converted to IntervalData - or TimeSeriesData depending on the contents of the time list. + This function accepts :class:`~.scalar_data.ScalarData`, + :class:`.IntervalData`, or :class:`.TimeSeriesData` objects, or + compatible mappings/tuples as the target, and builds the appropriate + penalty expression for each. Mappings are converted to ScalarData, + and tuples (of data dict, time list) are unpacked and converted to + IntervalData or TimeSeriesData depending on the contents of the time + list. Arguments --------- @@ -275,10 +277,10 @@ def get_penalty_from_target( time: Set Set of time points at which to construct penalty expressions. Also indexes the returned Expression. - setpoint_data: ScalarData, TimeSeriesData, or IntervalData + setpoint_data: ~scalar_data.ScalarData, TimeSeriesData, or IntervalData Data structure representing the possibly time-varying or piecewise constant setpoint - weight_data: ScalarData (optional) + weight_data: ~scalar_data.ScalarData (optional) Data structure holding the weights to be applied to each variable variable_set: Set (optional) Set indexing the provided variables, if one already exists. Also diff --git a/pyomo/contrib/mpc/modeling/terminal.py b/pyomo/contrib/mpc/modeling/terminal.py index d2118c7d92e..83161ca67e9 100644 --- a/pyomo/contrib/mpc/modeling/terminal.py +++ b/pyomo/contrib/mpc/modeling/terminal.py @@ -87,10 +87,10 @@ def get_penalty_at_time( List of time-indexed variables that will be penalized t: Float Time point at which to apply the penalty - target_data: ScalarData + target_data: ~scalar_data.ScalarData ScalarData object containing the target for (at least) the variables to be penalized - weight_data: ScalarData (optional) + weight_data: ~scalar_data.ScalarData (optional) ScalarData object containing the penalty weights for (at least) the variables to be penalized time_set: Set (optional) @@ -135,10 +135,10 @@ def get_terminal_penalty( time_set: Set Time set that indexes the provided variables. Penalties are applied at the last point in this set. - target_data: ScalarData + target_data: ~scalar_data.ScalarData ScalarData object containing the target for (at least) the variables to be penalized - weight_data: ScalarData (optional) + weight_data: ~scalar_data.ScalarData (optional) ScalarData object containing the penalty weights for (at least) the variables to be penalized variable_set: Set (optional) diff --git a/pyomo/contrib/piecewise/piecewise_linear_expression.py b/pyomo/contrib/piecewise/piecewise_linear_expression.py index ddcb7c6a42f..7197e04cf50 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_expression.py +++ b/pyomo/contrib/piecewise/piecewise_linear_expression.py @@ -17,12 +17,15 @@ class PiecewiseLinearExpression(NumericExpression): """ A numeric expression node representing a specific instantiation of a - PiecewiseLinearFunction. + :obj:`~.piecewise_linear_function.PiecewiseLinearFunction`. - Args: - args (list or tuple): Children of this node - pw_linear_function (PiecewiseLinearFunction): piece-wise linear function - of which this node is an instance. + Parameters + ---------- + args : list or tuple + Children of this node + + pw_linear_function : ~piecewise_linear_function.PiecewiseLinearFunction + Piece-wise linear function of which this node is an instance. """ __slots__ = ('_pw_linear_function',) diff --git a/pyomo/contrib/pynumero/interfaces/nlp.py b/pyomo/contrib/pynumero/interfaces/nlp.py index d6571086429..4acdf1122b6 100644 --- a/pyomo/contrib/pynumero/interfaces/nlp.py +++ b/pyomo/contrib/pynumero/interfaces/nlp.py @@ -15,30 +15,35 @@ The first interface (NLP) presents the NLP in the following form (where all equality and inequality constraints are combined) -minimize f(x) -subject to g_L <= g(x) <= g_U - x_L <= x <= x_U - -where x \in R^{n_x} are the primal variables, - x_L \in R^{n_x} are the lower bounds of the primal variables, - x_U \in R^{n_x} are the upper bounds of the primal variables, - g: R^{n_x} \rightarrow R^{n_c} are constraints (combined - equality and inequality) +.. math:: + + \min\ & f(x) \\ + s.t.\ & g_L <= g(x) <= g_U \\ + & x_L <= x <= x_U + +where: +- :math:`x \in R^{n_x}` are the primal variables, +- :math:`x_L \in R^{n_x}` are the lower bounds of the primal variables, +- :math:`x_U \in R^{n_x}` are the upper bounds of the primal variables, +- :math:`g: R^{n_x} \rightarrow R^{n_c}` are constraints (equality and inequality) The second interface (ExtendedNLP) extends the definition above and presents the NLP in the following form where the equality and inequality constraints are separated. -minimize f(x) -subject to h(x) = 0 - q_L <= q(x) <= q_U - x_L <= x <= x_U +.. math:: + + \min\ & f(x) \\ + s.t.\ & h(x) = 0 \\ + & q_L <= q(x) <= q_U \\ + & x_L <= x <= x_U -where x \in R^{n_x} are the primal variables, - x_L \in R^{n_x} are the lower bounds of the primal variables, - x_U \in R^{n_x} are the upper bounds of the primal variables, - h: R^{n_x} \rightarrow R^{n_eq} are the equality constraints - q: R^{n_x} \rightarrow R^{n_ineq} are the inequality constraints +where: +- :math:`x \in R^{n_x}` are the primal variables, +- :math:`x_L \in R^{n_x}` are the lower bounds of the primal variables, +- :math:`x_U \in R^{n_x}` are the upper bounds of the primal variables, +- :math:`h: R^{n_x} \rightarrow R^{n_eq}` are the equality constraints +- :math:`q: R^{n_x} \rightarrow R^{n_ineq}` are the inequality constraints Note: In the case of the ExtendedNLP, it is generally assumed that both the NLP and the ExtendedNLP interfaces are supported and diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index 73eee5202aa..a6152fa3e02 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -87,23 +87,41 @@ class MasterProblemData(object): """ Container for the grcs master problem - Attributes: - :master_model: master problem model object - :base_model: block representing the original model object - :iteration: current iteration of the algorithm + Attributes + ---------- + master_model : BlockData + master problem model object + + base_model : BlockData + block representing the original model object + + iteration : int + current iteration of the algorithm + """ class SeparationProblemData(object): - """ - Container for the grcs separation problem - - Attributes: - :separation_model: separation problem model object - :points_added_to_master: list of parameter violations added to the master problem over the course of the algorithm - :separation_problem_subsolver_statuses: list of subordinate sub-solver statuses throughout separations - :total_global_separation_solvers: Counter for number of times global solvers were employed in separation - :constraint_violations: List of constraint violations identified in separation + """Container for the grcs separation problem + + Attributes + ---------- + separation_model : BlockData + separation problem model object + + points_added_to_master : List[] + list of parameter violations added to the master problem over + the course of the algorithm + + separation_problem_subsolver_statuses : List[] + list of subordinate sub-solver statuses throughout separations + + total_global_separation_solvers : int + Counter for number of times global solvers were employed in separation + + constraint_violations : List[] + List of constraint violations identified in separation + """ pass @@ -112,15 +130,29 @@ class SeparationProblemData(object): class MasterResult(object): """Data class for master problem results data. - Attributes: - - termination_condition: Solver termination condition - - fsv_values: list of design variable values - - ssv_values: list of control variable values - - first_stage_objective: objective contribution due to first-stage degrees of freedom - - second_stage_objective: objective contribution due to second-stage degrees of freedom - - grcs_termination_condition: the conditions under which the grcs terminated - (max_iter, robust_optimal, error) - - pyomo_results: results object from solve() statement + Attributes + ---------- + termination_condition : + Solver termination condition + + fsv_values : List[] + list of design variable values + + ssv_values : List[] + list of control variable values + + first_stage_objective : float + objective contribution due to first-stage degrees of freedom + + second_stage_objective : float + objective contribution due to second-stage degrees of freedom + + grcs_termination_condition : + the conditions under which the grcs terminated (max_iter, + robust_optimal, error) + + pyomo_results : + results object from solve() statement """ diff --git a/pyomo/contrib/pyros/util.py b/pyomo/contrib/pyros/util.py index 5ade304c077..e1d25e573f1 100644 --- a/pyomo/contrib/pyros/util.py +++ b/pyomo/contrib/pyros/util.py @@ -203,19 +203,22 @@ def get_main_elapsed_time(self): @contextmanager def time_code(timing_data_obj, code_block_name, is_main_timer=False): - """ - Starts timer at entry, stores elapsed time at exit. + """Starts timer at entry, stores elapsed time at exit. Parameters ---------- timing_data_obj : TimingData Timing data object. + code_block_name : str Name of code block being timed. - If `is_main_timer=True`, the start time is stored in the timing_data_obj, - allowing calculation of total elapsed time 'on the fly' (e.g. to enforce - a time limit) using `get_main_elapsed_time(timing_data_obj)`. + is_main_timer : bool + If ``is_main_timer=True``, the start time is stored in the + timing_data_obj, allowing calculation of total elapsed time 'on + the fly' (e.g. to enforce a time limit) using + ``get_main_elapsed_time(timing_data_obj)``. + """ # initialize tic toc timer timing_data_obj.start_timer(code_block_name) diff --git a/pyomo/contrib/sensitivity_toolbox/sens.py b/pyomo/contrib/sensitivity_toolbox/sens.py index 34fbb92327a..95913612aaa 100644 --- a/pyomo/contrib/sensitivity_toolbox/sens.py +++ b/pyomo/contrib/sensitivity_toolbox/sens.py @@ -230,22 +230,27 @@ def sensitivity_calculation( def get_dsdp(model, theta_names, theta, tee=False): - """This function calculates gradient vector of the variables - with respect to the parameters (theta_names). - - e.g) min f: p1*x1+ p2*(x2^2) + p1*p2 - s.t c1: x1 + x2 = p1 - c2: x2 + x3 = p2 - 0 <= x1, x2, x3 <= 10 - p1 = 10 - p2 = 5 + """This function calculates gradient vector of the variables with + respect to the parameters (theta_names). + + For example, given: + + .. math:: + + \min f:\ & p1*x1 + p2*(x2^2) + p1*p2 \\ + s.t.\ & c1: x1 + x2 = p1 \\ + & c2: x2 + x3 = p2 \\ + & 0 <= x1, x2, x3 <= 10 \\ + & p1 = 10 \\ + & p2 = 5 + the function returns dx/dp and dp/dp, and column orders. The following terms are used to define the output dimensions: - Ncon = number of constraints - Nvar = number of variables (Nx + Ntheta) - Nx = number of decision (primal) variables - Ntheta = number of uncertain parameters. + - Ncon = number of constraints + - Nvar = number of variables (Nx + Ntheta) + - Nx = number of decision (primal) variables + - Ntheta = number of uncertain parameters. Parameters ---------- @@ -267,6 +272,7 @@ def get_dsdp(model, theta_names, theta, tee=False): columns = len(col) col: list List of variable names + """ # Get parameters from names. In SensitivityInterface, we expect # these to be parameters on the original model. @@ -322,53 +328,65 @@ def get_dsdp(model, theta_names, theta, tee=False): def get_dfds_dcds(model, theta_names, tee=False, solver_options=None): """This function calculates gradient vector of the objective function - and constraints with respect to the variables and parameters. - - e.g) min f: p1*x1+ p2*(x2^2) + p1*p2 - s.t c1: x1 + x2 = p1 - c2: x2 + x3 = p2 - 0 <= x1, x2, x3 <= 10 - p1 = 10 - p2 = 5 + and constraints with respect to the variables and parameters. + + For example, given: + + .. math:: + + \min f:\ & p1*x1 + p2*(x2^2) + p1*p2 \\ + s.t.\ & c1: x1 + x2 = p1 \\ + & c2: x2 + x3 = p2 \\ + & 0 <= x1, x2, x3 <= 10 \\ + & p1 = 10 \\ + & p2 = 5 + - Variables = (x1, x2, x3, p1, p2) - Fix p1 and p2 with estimated values The following terms are used to define the output dimensions: - Ncon = number of constraints - Nvar = number of variables (Nx + Ntheta) - Nx = number of decision (primal) variables - Ntheta = number of uncertain parameters. + - Ncon = number of constraints + - Nvar = number of variables (Nx + Ntheta) + - Nx = number of decision (primal) variables + - Ntheta = number of uncertain parameters. Parameters ---------- - model: Pyomo ConcreteModel + model : Pyomo ConcreteModel model should include an objective function - theta_names: list of strings + + theta_names : list of strings List of Var names - tee: bool, optional + + tee : bool, optional Indicates that ef solver output should be teed - solver_options: dict, optional + + solver_options : dict, optional Provides options to the solver (also the name of an attribute) Returns ------- - gradient_f: numpy.ndarray + gradient_f : numpy.ndarray Length Nvar array. A gradient vector of the objective function with respect to the (decision variables, parameters) at the optimal solution - gradient_c: scipy.sparse.csr.csr_matrix + + gradient_c : scipy.sparse.csr.csr_matrix Ncon by Nvar size sparse matrix. A Jacobian matrix of the constraints with respect to the (decision variables, parameters) at the optimal solution. Each row contains [row number, column number, and value], column order follows variable order in col and index starts from 0. Note that it follows k_aug. If no constraint exists, return [] - col: list + + col : list Size Nvar list of variable names - row: list + + row : list Size Ncon+1 list of constraints and objective function names. The final element is the objective function name. - line_dic: dict + + line_dic : dict column numbers of the theta_names in the model. Index starts from 1 Raises diff --git a/pyomo/contrib/solver/config.py b/pyomo/contrib/solver/config.py index e60219a74b5..c5a367acfbb 100644 --- a/pyomo/contrib/solver/config.py +++ b/pyomo/contrib/solver/config.py @@ -51,7 +51,7 @@ def TextIO_or_Logger(val): class SolverConfig(ConfigDict): """ - Base config for all direct solver interfaces + Common configuration options for all solver interfaces """ def __init__( diff --git a/pyomo/contrib/solver/ipopt.py b/pyomo/contrib/solver/ipopt.py index c467d283d9b..a49bd0e58a2 100644 --- a/pyomo/contrib/solver/ipopt.py +++ b/pyomo/contrib/solver/ipopt.py @@ -43,9 +43,7 @@ class IpoptSolverError(PyomoException): - """ - General exception to catch solver system errors - """ + """General exception to catch solver system errors""" class IpoptConfig(SolverConfig): @@ -290,6 +288,7 @@ def _create_command_line(self, basename: str, config: IpoptConfig, opt_file: boo @document_kwargs_from_configdict(CONFIG) def solve(self, model, **kwds): + "Solve a model using Ipopt" # Begin time tracking start_timestamp = datetime.datetime.now(datetime.timezone.utc) # Update configuration options, based on keywords passed to solve diff --git a/pyomo/contrib/solver/results.py b/pyomo/contrib/solver/results.py index cbc04681235..e39685cbbc3 100644 --- a/pyomo/contrib/solver/results.py +++ b/pyomo/contrib/solver/results.py @@ -137,15 +137,16 @@ class SolutionStatus(enum.Enum): class Results(ConfigDict): - """ + """Base class for all solver results + Attributes ---------- - solution_loader: SolutionLoaderBase + solution_loader: .SolutionLoaderBase Object for loading the solution back into the model. - termination_condition: :class:`TerminationCondition` + termination_condition: TerminationCondition The reason the solver exited. This is a member of the TerminationCondition enum. - solution_status: :class:`SolutionStatus` + solution_status: SolutionStatus The result of the solve call. This is a member of the SolutionStatus enum. incumbent_objective: float @@ -165,9 +166,11 @@ class Results(ConfigDict): The total number of iterations. timing_info: ConfigDict A ConfigDict containing three pieces of information: - - ``start_timestamp``: UTC timestamp of when run was initiated - - ``wall_time``: elapsed wall clock time for entire process - - ``timer``: a HierarchicalTimer object containing timing data about the solve + + - ``start_timestamp``: UTC timestamp of when run was initiated + - ``wall_time``: elapsed wall clock time for entire process + - ``timer``: a HierarchicalTimer object containing timing data + about the solve Specific solvers may add other relevant timing information, as appropriate. extra_info: ConfigDict @@ -176,6 +179,7 @@ class Results(ConfigDict): A copy of the SolverConfig ConfigDict, for later inspection/reproducibility. solver_log: str (ADVANCED OPTION) Any solver log messages. + """ def __init__( diff --git a/pyomo/contrib/trustregion/interface.py b/pyomo/contrib/trustregion/interface.py index b459e7cfa17..c62969b328a 100644 --- a/pyomo/contrib/trustregion/interface.py +++ b/pyomo/contrib/trustregion/interface.py @@ -290,13 +290,16 @@ def getCurrentDecisionVariableValues(self): return decision_values def updateDecisionVariableBounds(self, radius): - """ - Update the TRSP_k decision variable bounds + """Update the TRSP_k decision variable bounds This corresponds to: + + .. math:: || E^{-1} (u - u_k) || <= trust_radius - We omit E^{-1} because we assume that the users have correctly scaled - their variables. + + We omit :math:`E^{-1}` because we assume that the users have + correctly scaled their variables. + """ for var in self.decision_variables: var.setlb( diff --git a/pyomo/core/base/PyomoModel.py b/pyomo/core/base/PyomoModel.py index 22bbc5fa02b..cbe5468945c 100644 --- a/pyomo/core/base/PyomoModel.py +++ b/pyomo/core/base/PyomoModel.py @@ -50,9 +50,12 @@ def global_option(function, name, value): Example use: - @global_option('config.foo.bar', 1) - def functor(): - ... + .. code:: + + @global_option('config.foo.bar', 1) + def functor(): + # ... + """ PyomoConfig._option[tuple(name.split('.'))] = value diff --git a/pyomo/core/base/boolean_var.py b/pyomo/core/base/boolean_var.py index db9a41fceda..65bd33fe739 100644 --- a/pyomo/core/base/boolean_var.py +++ b/pyomo/core/base/boolean_var.py @@ -90,21 +90,10 @@ class BooleanVarData(ComponentData, BooleanValue): Attributes ---------- - domain: SetData - The domain of this variable. - fixed: bool If True, then this variable is treated as a fixed constant in the model. - stale: bool - A Boolean indicating whether the value of this variable is - Consistent with the most recent solve. `True` indicates that - this variable's value was set prior to the most recent solve and - was not updated by the results returned by the solve. - - value: bool - The value of this variable. """ __slots__ = ('_value', 'fixed', '_stale', '_associated_binary') @@ -172,7 +161,7 @@ def __call__(self, exception=True): @property def value(self): - """Return (or set) the value for this variable.""" + """bool : the current value for this variable.""" return self._value @value.setter @@ -181,11 +170,17 @@ def value(self, val): @property def domain(self): - """Return the domain for this variable.""" + """BooleanSet : the domain for this variable.""" return BooleanSet @property def stale(self): + """ + bool : A Boolean indicating whether the value of this variable is + Consistent with the most recent solve. `True` indicates that + this variable's value was set prior to the most recent solve and + was not updated by the results returned by the solve. + """ return StaleFlagManager.is_stale(self._stale) @stale.setter @@ -484,7 +479,7 @@ def __init__(self, *args, **kwd): @property def value(self): - """Return the value for this variable.""" + """bool : the current value of this variable.""" if self._constructed: return BooleanVarData.value.fget(self) raise ValueError( @@ -495,7 +490,6 @@ def value(self): @value.setter def value(self, val): - """Set the value for this variable.""" if self._constructed: return BooleanVarData.value.fset(self, val) raise ValueError( @@ -506,6 +500,7 @@ def value(self, val): @property def domain(self): + """BooleanSet : the domain for this variable.""" return BooleanVarData.domain.fget(self) def fix(self, value=NOTSET, skip_validation=False): @@ -568,6 +563,7 @@ def free(self): @property def domain(self): + """BooleanSet : the domain for this variable.""" return BooleanSet # Because Emma wants crazy things... (Where crazy things are the ability to diff --git a/pyomo/core/base/component.py b/pyomo/core/base/component.py index 966ce8c0737..96e8117dc9a 100644 --- a/pyomo/core/base/component.py +++ b/pyomo/core/base/component.py @@ -484,19 +484,22 @@ class Component(ComponentBase): """ This is the base class for all Pyomo modeling components. - Constructor arguments: - ctype The class type for the derived subclass - doc A text string describing this component - name A name for this component + Parameters + ---------- + ctype : type + The class type for the derived subclass - Public class attributes: - doc A text string describing this component + doc : str + A text string describing this component + + name : str + A name for this component + + Attributes + ---------- + doc : str + A text string describing this component - Private class attributes: - _constructed A boolean that is true if this component has been - constructed - _parent A weakref to the parent block that owns this component - _ctype The class type for the derived subclass """ __autoslot_mappers__ = {'_parent': AutoSlots.weakref_mapper} diff --git a/pyomo/core/base/connector.py b/pyomo/core/base/connector.py index 1363f5abd65..84fe5a80b9d 100644 --- a/pyomo/core/base/connector.py +++ b/pyomo/core/base/connector.py @@ -131,12 +131,15 @@ class Connector(IndexedComponent): constraints that involve the original variables contained within the Connector. - Constructor - Arguments: - name The name of this connector - index The index set that defines the distinct connectors. - By default, this is None, indicating that there - is a single connector. + Parameters + ---------- + name : str + The name of this connector + + index + The index set that defines the distinct connectors. By default, + this is None, indicating that there is a single connector. + """ def __new__(cls, *args, **kwds): diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index f0e020bcfd0..8e49ab9ef27 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -83,11 +83,14 @@ def simple_constraint_rule(rule): Example use: - @simple_constraint_rule - def C_rule(model, i, j): - ... + .. code:: + + @simple_constraint_rule + def C_rule(model, i, j): + # ... + + model.c = Constraint(rule=simple_constraint_rule(...)) - model.c = Constraint(rule=simple_constraint_rule(...)) """ map_types = set([type(None)]) | native_logical_types result_map = {None: Constraint.Skip} @@ -109,11 +112,14 @@ def simple_constraintlist_rule(rule): Example use: - @simple_constraintlist_rule - def C_rule(model, i, j): - ... + .. code:: + + @simple_constraintlist_rule + def C_rule(model, i, j): + # ... + + model.c = ConstraintList(expr=simple_constraintlist_rule(...)) - model.c = ConstraintList(expr=simple_constraintlist_rule(...)) """ map_types = set([type(None)]) | native_logical_types result_map = {None: ConstraintList.End} @@ -127,29 +133,16 @@ def C_rule(model, i, j): class ConstraintData(ActiveComponentData): - """ - This class defines the data for a single algebraic constraint. + """This class defines the data for a single algebraic constraint. - Constructor arguments: - component The Constraint object that owns this data. - expr The Pyomo expression stored in this constraint. + Parameters + ---------- + expr : ExpressionBase + The Pyomo expression stored in this constraint. - Public class attributes: - active A boolean that is true if this constraint is - active in the model. - body The Pyomo expression for this constraint - lower The Pyomo expression for the lower bound - upper The Pyomo expression for the upper bound - equality A boolean that indicates whether this is an - equality constraint - strict_lower A boolean that indicates whether this - constraint uses a strict lower bound - strict_upper A boolean that indicates whether this - constraint uses a strict upper bound + component : Constriant + The Constraint object that owns this data. - Private class attributes: - _component The objective component. - _active A boolean that indicates whether this data is active """ __slots__ = ('_expr',) @@ -266,7 +259,7 @@ def _evaluate_bound(self, bound, is_lb): @property def body(self): - """Access the body of a constraint expression.""" + """The body (variable portion) of a constraint expression.""" try: ans = self.to_bounded_expression()[1] except ValueError: @@ -290,7 +283,14 @@ def body(self): @property def lower(self): - """Access the lower bound of a constraint expression.""" + """The lower bound of a constraint expression. + + This is the fixed lower bound of a Constraint as a Pyomo + exprression. This may be contain potentially variable terms + that are currently fixed. If there is no lower bound, this will + return `None`. + + """ ans = self.to_bounded_expression()[0] if ans.__class__ in native_types and ans is not None: # Historically, constraint.lower was guaranteed to return a type @@ -304,7 +304,14 @@ def lower(self): @property def upper(self): - """Access the upper bound of a constraint expression.""" + """Access the upper bound of a constraint expression. + + This is the fixed upper bound of a Constraint as a Pyomo + exprression. This may be contain potentially variable terms + that are currently fixed. If there is no upper bound, this will + return `None`. + + """ ans = self.to_bounded_expression()[2] if ans.__class__ in native_types and ans is not None: # Historically, constraint.upper was guaranteed to return a type @@ -318,17 +325,17 @@ def upper(self): @property def lb(self): - """Access the value of the lower bound of a constraint expression.""" + """float : the value of the lower bound of a constraint expression.""" return self._evaluate_bound(self.to_bounded_expression()[0], True) @property def ub(self): - """Access the value of the upper bound of a constraint expression.""" + """float : the value of the upper bound of a constraint expression.""" return self._evaluate_bound(self.to_bounded_expression()[2], False) @property def equality(self): - """A boolean indicating whether this is an equality constraint.""" + """bool : True if this is an equality constraint.""" expr = self.expr if expr.__class__ is EqualityExpression: return True @@ -341,12 +348,12 @@ def equality(self): @property def strict_lower(self): - """True if this constraint has a strict lower bound.""" + """bool : True if this constraint has a strict lower bound.""" return False @property def strict_upper(self): - """True if this constraint has a strict upper bound.""" + """bool : True if this constraint has a strict upper bound.""" return False def has_lb(self): @@ -750,7 +757,7 @@ def __init__(self, *args, **kwds): # @property def body(self): - """Access the body of a constraint expression.""" + """The body (variable portion) of a constraint expression.""" if not self._data: raise ValueError( "Accessing the body of ScalarConstraint " @@ -762,7 +769,14 @@ def body(self): @property def lower(self): - """Access the lower bound of a constraint expression.""" + """The lower bound of a constraint expression. + + This is the fixed lower bound of a Constraint as a Pyomo + exprression. This may be contain potentially variable terms + that are currently fixed. If there is no lower bound, this will + return `None`. + + """ if not self._data: raise ValueError( "Accessing the lower bound of ScalarConstraint " @@ -774,7 +788,14 @@ def lower(self): @property def upper(self): - """Access the upper bound of a constraint expression.""" + """Access the upper bound of a constraint expression. + + This is the fixed upper bound of a Constraint as a Pyomo + exprression. This may be contain potentially variable terms + that are currently fixed. If there is no upper bound, this will + return `None`. + + """ if not self._data: raise ValueError( "Accessing the upper bound of ScalarConstraint " @@ -786,7 +807,7 @@ def upper(self): @property def equality(self): - """A boolean indicating whether this is an equality constraint.""" + """bool : True if this is an equality constraint.""" if not self._data: raise ValueError( "Accessing the equality flag of ScalarConstraint " @@ -798,7 +819,7 @@ def equality(self): @property def strict_lower(self): - """A boolean indicating whether this constraint has a strict lower bound.""" + """bool : True if this constraint has a strict lower bound.""" if not self._data: raise ValueError( "Accessing the strict_lower flag of ScalarConstraint " @@ -810,7 +831,7 @@ def strict_lower(self): @property def strict_upper(self): - """A boolean indicating whether this constraint has a strict upper bound.""" + """bool : True if this constraint has a strict upper bound.""" if not self._data: raise ValueError( "Accessing the strict_upper flag of ScalarConstraint " diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index a5120759236..2df5a03a715 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -39,12 +39,8 @@ class NamedExpressionData(numeric_expr.NumericValue): """An object that defines a generic "named expression". - This is the base class for both :py:class:`ExpressionData` and - :py:class:`ObjectiveData`. - - Public Class Attributes - expr The expression owned by this data. - + This is the base class for both :class:`ExpressionData` and + :class:`ObjectiveData`. """ # Note: derived classes are expected to declare the _args_ slot @@ -207,18 +203,16 @@ class _GeneralExpressionDataImpl(metaclass=RenamedClass): class ExpressionData(NamedExpressionData, ComponentData): - """ - An object that defines an expression that is never cloned + """An object that defines an expression that is never cloned - Constructor Arguments - expr The Pyomo expression stored in this expression. - component The Expression object that owns this data. + Parameters + ---------- + expr : NumericValue + The Pyomo expression stored in this expression. - Public Class Attributes - expr The expression owned by this data. + component : Expression + The Expression object that owns this data. - Private class attributes: - _component The expression component. """ __slots__ = ('_args_',) @@ -238,16 +232,28 @@ class _GeneralExpressionData(metaclass=RenamedClass): "Named expressions that can be used in other expressions." ) class Expression(IndexedComponent): - """ - A shared expression container, which may be defined over a index. - - Constructor Arguments: - initialize A Pyomo expression or dictionary of expressions - used to initialize this object. - expr A synonym for initialize. - rule A rule function used to initialize this object. - name Name for this component. - doc Text describing this component. + """A shared expression container, which may be defined over a index. + + Parameters + ---------- + rule : ~.Initializer + + The source to use to initialize the expression(s) in this + component. See :func:`.Initializer` for accepted argument types. + + initialize : + A synonym for `rule` + + expr : + A synonym for `rule` + + name : str + Name of this component; will be overridden if this is assigned + to a Block. + + doc : str + Text describing this component. + """ _ComponentDataClass = ExpressionData diff --git a/pyomo/core/base/logical_constraint.py b/pyomo/core/base/logical_constraint.py index cc0780fd9bd..5fdea45e562 100644 --- a/pyomo/core/base/logical_constraint.py +++ b/pyomo/core/base/logical_constraint.py @@ -47,17 +47,24 @@ class LogicalConstraintData(ActiveComponentData): This class defines the data for a single general logical constraint. Constructor arguments: - component The LogicalStatement object that owns this data. - expr The Pyomo expression stored in this logical constraint. + component + The LogicalStatement object that owns this data. + expr + The Pyomo expression stored in this logical constraint. Public class attributes: - active A boolean that is true if this logical constraint is - active in the model. - expr The Pyomo expression for this logical constraint + active + A boolean that is true if this logical constraint is + active in the model. + expr + The Pyomo expression for this logical constraint Private class attributes: - _component The logical constraint component. - _active A boolean that indicates whether this data is active + _component + The logical constraint component. + _active + A boolean that indicates whether this data is active + """ __slots__ = ('_expr',) diff --git a/pyomo/core/base/objective.py b/pyomo/core/base/objective.py index f1204f2a09c..7b3640dd842 100644 --- a/pyomo/core/base/objective.py +++ b/pyomo/core/base/objective.py @@ -55,11 +55,14 @@ def simple_objective_rule(rule): Example use: - @simple_objective_rule - def O_rule(model, i, j): - ... + .. code:: + + @simple_objective_rule + def O_rule(model, i, j): + # ... + + model.o = Objective(rule=simple_objective_rule(...)) - model.o = Objective(rule=simple_objective_rule(...)) """ return rule_wrapper(rule, {None: Objective.Skip}) @@ -72,36 +75,40 @@ def simple_objectivelist_rule(rule): Example use: - @simple_objectivelist_rule - def O_rule(model, i, j): - ... + .. code:: + + @simple_objectivelist_rule + def O_rule(model, i, j): + # ... + + model.o = ObjectiveList(expr=simple_objectivelist_rule(...)) - model.o = ObjectiveList(expr=simple_objectivelist_rule(...)) """ return rule_wrapper(rule, {None: ObjectiveList.End}) class ObjectiveData(NamedExpressionData, ActiveComponentData): - """ - This class defines the data for a single objective. + """This class defines the data for a single objective. Note that this is a subclass of NumericValue to allow objectives to be used as part of expressions. - Constructor arguments: - expr The Pyomo expression stored in this objective. - sense The direction for this objective. - component The Objective object that owns this data. + Parameters + ---------- + expr: + The Pyomo expression stored in this objective. - Public class attributes: - expr The Pyomo expression for this objective - active A boolean that is true if this objective is active - in the model. - sense The direction for this objective. + sense: + The direction for this objective. + + component: Objective + The Objective object that owns this data. + + Attributes + ---------- + expr: + The Pyomo expression for this objective - Private class attributes: - _component The objective component. - _active A boolean that indicates whether this data is active """ __slots__ = ("_args_", "_sense") diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index 45de3286589..f801b6a194a 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -157,6 +157,10 @@ def clear(self): # set_value is called without specifying an index, this call # involves a linear scan of the _data dict. def set_value(self, value, idx=NOTSET): + """Set the value of this ParamData object, performing unit convertion + and validation as necessary. + + """ # # If this param has units, then we need to check the incoming # value and see if it is "units compatible". We only need to diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index 8c5f34d2b53..cd39d9a23fb 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -18,18 +18,18 @@ Unifying framework and Extensions (Vielma, Nemhauser 2008). TODO: Add regression tests for the following completed tasks -*) user not providing floats can be an major issue for BIGM's and MC -*) Other TODO's -*) nonconvex/nonconcave functions - BIGM_SOS1, BIGM_SOS2 ***** possible edge case bug + - user not providing floats can be an major issue for BIGM's and MC + - nonconvex/nonconcave functions - BIGM_SOS1, BIGM_SOS2 ***** possible edge case bug Possible Extensions -*) Consider another piecewise rep ("SOS2_MANUAL"?) where we manually implement - extra constraints to define an SOS2 set, this would be compatible with GLPK, - http://winglpk.sourceforge.net/media/glpk-sos2_02.pdf -*) double check that LOG and DLOG reps really do require (2^n)+1 points, or can - we just add integer cuts (or something more intelligent) in order to handle - piecewise functions without 2^n polytopes -*) piecewise for functions of the form y = f(x1,x2,...) + - Consider another piecewise rep ("SOS2_MANUAL"?) where we manually implement + extra constraints to define an SOS2 set, this would be compatible with GLPK, + http://winglpk.sourceforge.net/media/glpk-sos2_02.pdf + - double check that LOG and DLOG reps really do require (2^n)+1 points, or can + we just add integer cuts (or something more intelligent) in order to handle + piecewise functions without 2^n polytopes + - piecewise for functions of the form y = f(x1,x2,...) + """ import logging @@ -1023,111 +1023,126 @@ def _find_M(self, x_pts, y_pts, bound_type): "Constraints that contain piecewise linear expressions." ) class Piecewise(Block): - """ - Adds piecewise constraints to a Pyomo model for functions of the - form, y = f(x). - - Usage: - model.const = Piecewise(index_1,...,index_n,yvar,xvar,**Keywords) - model.const = Piecewise(yvar,xvar,**Keywords) - - Keywords: - - -pw_pts={},[],() - A dictionary of lists (keys are index set) or a single list - (for the non-indexed case or when an identical set of - breakpoints is used across all indices) defining the set of - domain breakpoints for the piecewise linear - function. **ALWAYS REQUIRED** - - -pw_repn='' - Indicates the type of piecewise representation to use. This - can have a major impact on solver performance. - Choices: (Default 'SOS2') - - ~ + 'SOS2' - Standard representation using sos2 constraints - ~ 'BIGM_BIN' - BigM constraints with binary variables. - Theoretically tightest M values are automatically - determined. - ~ 'BIGM_SOS1' - BigM constraints with sos1 variables. - Theoretically tightest M values are automatically - determined. - ~*+ 'DCC' - Disaggregated convex combination model - ~*+ 'DLOG' - Logarithmic disaggregated convex combination model - ~*+ 'CC' - Convex combination model - ~*+ 'LOG' - Logarithmic branching convex combination - ~* 'MC' - Multiple choice model - ~*+ 'INC' - Incremental (delta) method - - + Supports step functions - * Source: "Mixed-Integer Models for Non-separable Piecewise Linear - Optimization: Unifying framework and Extensions" (Vielma, - Nemhauser 2008) - ~ Refer to the optional 'force_pw' keyword. - - -pw_constr_type='' - Indicates the bound type of the piecewise function. - Choices: - - 'UB' - y variable is bounded above by piecewise function - 'LB' - y variable is bounded below by piecewise function - 'EQ' - y variable is equal to the piecewise function - - -f_rule=f(model,i,j,...,x), {}, [], () - An object that returns a numeric value that is the range - value corresponding to each piecewise domain point. For - functions, the first argument must be a Pyomo model. The - last argument is the domain value at which the function - evaluates (Not a Pyomo Var). Intermediate arguments are the - corresponding indices of the Piecewise component (if any). - Otherwise, the object can be a dictionary of lists/tuples - (with keys the same as the indexing set) or a singe - list/tuple (when no indexing set is used or when all indices - use an identical piecewise function). - Examples: - - # A function which changes with index - def f(model,j,x): - if (j == 2): - return x**2 + 1.0 - else: - return x**2 + 5.0 - - # A nonlinear function - f = lambda model,x: return exp(x) + value(model.p) - (model.p is a Pyomo Param) - - # A step function - f = [0,0,1,1,2,2] - - -force_pw=True/False - Using the given function rule and pw_pts, a check for - convexity/concavity is implemented. If (1) the function is - convex and the piecewise constraints are lower bounds or if - (2) the function is concave and the piecewise constraints - are upper bounds then the piecewise constraints will be - substituted for linear constraints. Setting 'force_pw=True' - will force the use of the original piecewise constraints - even when one of these two cases applies. - - -warning_tol= Default=1e-8 - To aid in debugging, a warning is printed when consecutive - slopes of piecewise segments are within of - each other. - - -warn_domain_coverage=True/False Default=True - Print a warning when the feasible region of the domain - variable is not completely covered by the piecewise - breakpoints. - - -unbounded_domain_var=True/False Default=False - Allow an unbounded or partially bounded Pyomo Var to be used - as the domain variable. - **NOTE: This does not imply unbounded piecewise segments - will be constructed. The outermost piecewise - breakpoints will bound the domain variable at each - index. However, the Var attributes .lb and .ub will - not be modified. + """Adds piecewise constraints to a Pyomo model for functions of the + form, y = f(x). + + Examples + -------- + + .. code:: + + model.const = Piecewise(index_1,...,index_n,yvar,xvar,**Keywords) + model.const = Piecewise(yvar,xvar,**Keywords) + + Parameters + ---------- + pw_pts : dict + A dictionary of lists (keys are index set) or a single list (for + the non-indexed case or when an identical set of breakpoints is + used across all indices) defining the set of domain breakpoints + for the piecewise linear function. **ALWAYS REQUIRED** + + pw_repn : str + + Indicates the type of piecewise representation to use. This can + have a major impact on solver performance. Choices: (Default + 'SOS2') + + - ``SOS2``: + + Standard representation using sos2 constraints + - ``BIGM_BIN``: + BigM constraints with binary variables. Theoretically + tightest M values are automatically determined. + - ``BIGM_SOS1``: + BigM constraints with sos1 variables. Theoretically + tightest M values are automatically determined. + - ``DCC``: \*+ + Disaggregated convex combination model + - ``DLOG``: \*+ + Logarithmic disaggregated convex combination model + - ``CC``: \*+ + Convex combination model + - ``LOG``: \*+ + Logarithmic branching convex combination + - ``MC``: \* + Multiple choice model + - ``INC``: \*+ + Incremental (delta) method + + .. note:: + + \+\: Supports step functions + + \*\: From "Mixed-Integer Models for Non-separable Piecewise Linear + Optimization: Unifying framework and Extensions" (Vielma, + Nemhauser 2008) + + .. seealso:: + Refer to the optional 'force_pw' keyword. + + pw_constr_type : str + Indicates the bound type of the piecewise function. Choices: + + - ``UB`` - y variable is bounded above by piecewise function + - ``LB`` - y variable is bounded below by piecewise function + - ``EQ`` - y variable is equal to the piecewise function + + f_rule : f(model,i,j,...,x), {}, [], () + An object that returns a numeric value that is the range value + corresponding to each piecewise domain point. For functions, the + first argument must be a Pyomo model. The last argument is the + domain value at which the function evaluates (Not a Pyomo + Var). Intermediate arguments are the corresponding indices of + the Piecewise component (if any). Otherwise, the object can be + a dictionary of lists/tuples (with keys the same as the indexing + set) or a singe list/tuple (when no indexing set is used or when + all indices use an identical piecewise function). Examples: + + .. code:: python + + # A function which changes with index + def f(model,j,x): + if (j == 2): + return x**2 + 1.0 + else: + return x**2 + 5.0 + + # A nonlinear function + f = lambda model, x: return exp(x) + value(model.p) + # (where model.p is a Pyomo Param) + + # A step function + f = [0,0,1,1,2,2] + + force_pw : bool + Using the given function rule and pw_pts, a check for + convexity/concavity is implemented. If (1) the function is + convex and the piecewise constraints are lower bounds or if (2) + the function is concave and the piecewise constraints are upper + bounds then the piecewise constraints will be substituted for + linear constraints. Setting 'force_pw=True' will force the use + of the original piecewise constraints even when one of these two + cases applies. + + warning_tol : float, default=1e-8 + To aid in debugging, a warning is printed when consecutive + slopes of piecewise segments are within of each + other. + + warn_domain_coverage : bool, default=True + Print a warning when the feasible region of the domain variable + is not completely covered by the piecewise breakpoints. + + unbounded_domain_var : bool, default=False + Allow an unbounded or partially bounded Pyomo Var to be used as + the domain variable. + + .. note:: + This does not imply unbounded piecewise segments will be + constructed. The outermost piecewise breakpoints will bound + the domain variable at each index. However, the Var + attributes .lb and .ub will not be modified. + """ _ComponentDataClass = PiecewiseData diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 69b21c4d78b..ca2e3b239a9 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -257,7 +257,11 @@ def set_options(**kwds): decorator allows an arbitrary dictionary of values to passed through to the set constructor. - Examples: + Examples + -------- + + .. code:: + @set_options(dimen=3) def B_index(model): return [(i,i+1,i*i) for i in model.A] @@ -265,6 +269,7 @@ def B_index(model): @set_options(domain=Integers) def B_index(model): return range(10) + """ def decorator(func): @@ -280,11 +285,15 @@ def simple_set_rule(rule): This supports a simpler syntax in set rules, though these can be more difficult to debug when errors occur. - Example: + Examples + -------- + + .. code:: + + @simple_set_rule + def A_rule(model, i, j): + ... - @simple_set_rule - def A_rule(model, i, j): - ... """ return rule_wrapper(rule, {None: Set.End}) diff --git a/pyomo/core/expr/cnf_walker.py b/pyomo/core/expr/cnf_walker.py index 7b2081e5d36..8add9d23ef9 100644 --- a/pyomo/core/expr/cnf_walker.py +++ b/pyomo/core/expr/cnf_walker.py @@ -45,6 +45,8 @@ def to_cnf(expr, bool_varlist=None, bool_var_to_special_atoms=None): ExactlyExpression require special treatment if they are not the root node, or if their children are not atoms, e.g. + .. code:: + atmost(2, Y1, Y1 | Y2, Y2, Y3) As a result, the model may need to be augmented with @@ -54,13 +56,13 @@ def to_cnf(expr, bool_varlist=None, bool_var_to_special_atoms=None): and augmented variables are needed. This function will return a list of CNF logical constraints, including: - - CNF of original statement, including possible substitutions - - Additional CNF statements (for enforcing equivalence to augmented variables) + - CNF of original statement, including possible substitutions + - Additional CNF statements (for enforcing equivalence to augmented variables) In addition, the function will have side effects: - - augmented variables are added to the passed bool_varlist - - mapping from augmented variables to equivalent special atoms (see note above) - with only literals as logical arguments + - augmented variables are added to the passed bool_varlist + - mapping from augmented variables to equivalent special atoms + (see note above) with only literals as logical arguments """ if type(expr) in special_boolean_atom_types: diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py index 4a777a9b977..97a04726765 100644 --- a/pyomo/core/expr/compare.py +++ b/pyomo/core/expr/compare.py @@ -216,13 +216,14 @@ def assertExpressionsEqual(test, a, b, include_named_exprs=True, places=None): b: ExpressionBase or native type - include_named_exprs: bool - If True (the default), the comparison expands all named - expressions when generating the prefix notation - - places: Number of decimal places required for equality of floating - point numbers in the expression. If None (the default), the - expressions must be exactly equal. + include_named_exprs : bool + If True (the default), the comparison expands all named + expressions when generating the prefix notation + + places : float + Number of decimal places required for equality of floating + point numbers in the expression. If None (the default), the + expressions must be exactly equal. """ prefix_a = convert_expression_to_prefix_notation(a, include_named_exprs) prefix_b = convert_expression_to_prefix_notation(b, include_named_exprs) @@ -265,10 +266,14 @@ def assertExpressionsStructurallyEqual( b: ExpressionBase or native type - include_named_exprs: bool + include_named_exprs : bool If True (the default), the comparison expands all named expressions when generating the prefix notation + places : float + Number of decimal places required for equality of floating + point numbers in the expression. If None (the default), the + expressions must be exactly equal. """ prefix_a = convert_expression_to_prefix_notation(a, include_named_exprs) prefix_b = convert_expression_to_prefix_notation(b, include_named_exprs) diff --git a/pyomo/core/expr/symbol_map.py b/pyomo/core/expr/symbol_map.py index ebcf9b2953e..87ba1b57bab 100644 --- a/pyomo/core/expr/symbol_map.py +++ b/pyomo/core/expr/symbol_map.py @@ -27,11 +27,16 @@ class SymbolMap(object): Note: We should change the API to not use camelcase. - Attributes: - byObject (dict): maps (object id) to (string label) - bySymbol (dict): maps (string label) to (object) - alias (dict): maps (string label) to (object) - default_labeler: used to compute a string label from an object + Attributes + ---------- + byObject : dict + maps (object id) to (string label) + bySymbol : dict + maps (string label) to (object) + aliases : dict + maps (string label) to (object) + default_labeler: + used to compute a string label from an object """ def __init__(self, labeler=None): diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index d30046e9d82..d023d3d5d2b 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -861,13 +861,22 @@ def substitute_template_expression(expr, substituter, *args, **kwargs): and substituting all occurrences of IndexTemplate and GetItemExpression nodes. - Args: - substituter: method taking (expression, *args) and returning - the new object - *args: these are passed directly to the substituter + Parameters + ---------- + expr : NumericExpression + the source template expression - Returns: + substituter: Callable + method taking ``(expression, *args)`` and returning the new object + + \*args: + positional arguments passed directly to the substituter + + Returns + ------- + NumericExpression : a new expression tree with all substitutions done + """ visitor = ReplaceTemplateExpression(substituter, *args, **kwargs) return visitor.walk_expression(expr) diff --git a/pyomo/core/kernel/conic.py b/pyomo/core/kernel/conic.py index bd78ba310f4..ca3765d686a 100644 --- a/pyomo/core/kernel/conic.py +++ b/pyomo/core/kernel/conic.py @@ -150,6 +150,8 @@ def __call__(self, exception=True): class quadratic(_ConicBase): """A quadratic conic constraint of the form: + .. math:: + x[0]^2 + ... + x[n-1]^2 <= r^2, which is recognized as convex for r >= 0. @@ -241,6 +243,8 @@ def check_convexity_conditions(self, relax=False): class rotated_quadratic(_ConicBase): """A rotated quadratic conic constraint of the form: + .. math:: + x[0]^2 + ... + x[n-1]^2 <= 2*r1*r2, which is recognized as convex for r1,r2 >= 0. @@ -351,6 +355,8 @@ def check_convexity_conditions(self, relax=False): class primal_exponential(_ConicBase): """A primal exponential conic constraint of the form: + .. math:: + x1*exp(x2/x1) <= r, which is recognized as convex for x1,r >= 0. @@ -460,6 +466,9 @@ def check_convexity_conditions(self, relax=False): class primal_power(_ConicBase): """A primal power conic constraint of the form: + + .. math:: + sqrt(x[0]^2 + ... + x[n-1]^2) <= (r1^alpha)*(r2^(1-alpha)) which is recognized as convex for r1,r2 >= 0 @@ -587,6 +596,9 @@ def check_convexity_conditions(self, relax=False): class primal_geomean(_ConicBase): """A primal geometric mean conic constraint of the form: + + .. math:: + (r[0]*...*r[n-2])^(1/(n-1)) >= |x[n-1]| Parameters @@ -648,6 +660,8 @@ def x(self): class dual_exponential(_ConicBase): """A dual exponential conic constraint of the form: + .. math:: + -x2*exp((x1/x2)-1) <= r which is recognized as convex for x2 <= 0 and r >= 0. @@ -758,6 +772,8 @@ def check_convexity_conditions(self, relax=False): class dual_power(_ConicBase): """A dual power conic constraint of the form: + .. math:: + sqrt(x[0]^2 + ... + x[n-1]^2) <= ((r1/alpha)^alpha) * ((r2/(1-alpha))^(1-alpha)) @@ -889,6 +905,9 @@ def check_convexity_conditions(self, relax=False): class dual_geomean(_ConicBase): """A dual geometric mean conic constraint of the form: + + .. math:: + (n-1)*(r[0]*...*r[n-2])^(1/(n-1)) >= |x[n-1]| Parameters @@ -948,22 +967,28 @@ def x(self): class svec_psdcone(_ConicBase): - """A domain consisting of vectorizations of the lower-triangular + r"""A domain consisting of vectorizations of the lower-triangular part of a positive semidefinite matrx, with the non-diagonal elements additionally rescaled. In other words, if a vector 'x' - of length n = d*(d+1)/2 belongs to this cone, then the matrix: + of length :math:`n = d(d+1)/2` belongs to this cone, then the matrix: + + .. math:: - sMat(x) = [[ x[1], x[2]/sqrt(2), ..., x[d]/sqrt(2)], - [x[2]/sqrt(2), x[d+1], ..., x[2d-1]/sqrt(2)], - ... - [x[d]/sqrt(2), x[2d-1]/sqrt(2), ..., x[d*(d+1)/2]/sqrt(2)]] + \begin{array}{rcclcl} + sMat(x) = [\;\; + [& x[1], & x[2]/\sqrt{2}, &...,& x[d]/\sqrt{2} &], \\ + [&x[2]/\sqrt{2},& x[d+1], &...,& x[2d-1]/\sqrt{2} &], \\ + & & \vdots & & & \\ + [&x[d]/\sqrt{2},&x[2d-1]/\sqrt{2},&...,&x[d(d+1)/2]/\sqrt{2}&] + \;\;] + \end{array} will be restricted to be a positive-semidefinite matrix. Parameters ---------- x : :class:`variable` - An iterable of variables with length d*(d+1)/2. + An iterable of variables with length :math:`d(d+1)/2`. """ diff --git a/pyomo/core/kernel/container_utils.py b/pyomo/core/kernel/container_utils.py index e197d0162b5..7ed2fd9e753 100644 --- a/pyomo/core/kernel/container_utils.py +++ b/pyomo/core/kernel/container_utils.py @@ -26,6 +26,8 @@ def define_homogeneous_container_type( is equivalent to placing the following class definition within that module: + .. code:: + class (): _ctype = @@ -43,6 +45,7 @@ def __init__(self, *args, **kwds): self._storage_key = None self._active = True super(, self).__init__(*args, **kwds) + """ assert name not in namespace cls_dict = {} diff --git a/pyomo/core/kernel/piecewise_library/transforms.py b/pyomo/core/kernel/piecewise_library/transforms.py index bc6cb0f51ad..eb65a0922a7 100644 --- a/pyomo/core/kernel/piecewise_library/transforms.py +++ b/pyomo/core/kernel/piecewise_library/transforms.py @@ -15,8 +15,9 @@ mixed-integer problem formulation. Reference:: Mixed-Integer Models for Non-separable Piecewise Linear -Optimization: Unifying framework and Extensions (Vielma, -Nemhauser 2008) + Optimization: Unifying framework and Extensions (Vielma, + Nemhauser 2008) + """ import logging diff --git a/pyomo/core/kernel/piecewise_library/transforms_nd.py b/pyomo/core/kernel/piecewise_library/transforms_nd.py index 2c4c8a1f1f2..93676f0d886 100644 --- a/pyomo/core/kernel/piecewise_library/transforms_nd.py +++ b/pyomo/core/kernel/piecewise_library/transforms_nd.py @@ -15,8 +15,9 @@ mixed-integer problem formulation. Reference:: Mixed-Integer Models for Non-separable Piecewise Linear -Optimization: Unifying framework and Extensions (Vielma, -Nemhauser 2008) + Optimization: Unifying framework and Extensions (Vielma, + Nemhauser 2008) + """ from collections.abc import Sized diff --git a/pyomo/core/plugins/transform/model.py b/pyomo/core/plugins/transform/model.py index 8fe828854ce..4bdb81e7b94 100644 --- a/pyomo/core/plugins/transform/model.py +++ b/pyomo/core/plugins/transform/model.py @@ -33,9 +33,11 @@ def to_standard_form(self): the coefficient matrix (A), the cost vector (c), and the constraint vector (b), where the 'standard form' problem is - min/max c'x - s.t. Ax = b - x >= 0 + .. math:: + + \min/\max\ & c'x \\ + s.t.\ & Ax = b \\ + & x >= 0 All three returned values are instances of the array.array class, and store Python floats (C doubles). diff --git a/pyomo/core/plugins/transform/standard_form.py b/pyomo/core/plugins/transform/standard_form.py index ffc382a2cf7..cf259abbbab 100644 --- a/pyomo/core/plugins/transform/standard_form.py +++ b/pyomo/core/plugins/transform/standard_form.py @@ -24,9 +24,11 @@ class StandardForm(IsomorphicTransformation): the coefficient matrix (A), the cost vector (c), and the constraint vector (b), where the 'standard form' problem is - min/max c'x - s.t. Ax = b - x >= 0 + .. math:: + + \min/\max\ & c'x \\ + s.t.\ & Ax = b \\ + & x >= 0 Options slack_names Default auxiliary_slack @@ -35,6 +37,7 @@ class StandardForm(IsomorphicTransformation): up_names Default _upper_bound pos_suffix Default _plus neg_suffix Default _neg + """ def __init__(self, **kwds): diff --git a/pyomo/core/util.py b/pyomo/core/util.py index 4b6cc8f3320..03df3ed595c 100644 --- a/pyomo/core/util.py +++ b/pyomo/core/util.py @@ -221,10 +221,12 @@ def sequence(*args): Return a generator that containing an arithmetic progression of integers. - sequence(i, j) returns [i, i+1, i+2, ..., j]; - start defaults to 1. - step specifies the increment (or decrement) - For example, sequence(4) returns [1, 2, 3, 4]. + + - ``sequence(i, j)`` returns ``[i, i+1, i+2, ..., j]``; + - start defaults to 1. + - step specifies the increment (or decrement) + + For example, ``sequence(4)`` returns ``[1, 2, 3, 4]``. """ if len(args) == 0: raise ValueError('sequence expected at least 1 arguments, got 0') diff --git a/pyomo/dataportal/plugins/datacommands.py b/pyomo/dataportal/plugins/datacommands.py index 2da0d44f048..a4231d5d7a1 100644 --- a/pyomo/dataportal/plugins/datacommands.py +++ b/pyomo/dataportal/plugins/datacommands.py @@ -42,22 +42,18 @@ def close(self): pass def read(self): - """ - This function does nothing, since executing Pyomo data commands - both reads and processes the data all at once. + """This function does nothing, since executing Pyomo data commands both + reads and processes the data all at once. + """ pass def write(self, data): # pragma:nocover - """ - This function does nothing, because we cannot write to a *.dat file. - """ + """This function does nothing, because we cannot write to a ``*.dat`` file.""" pass def process(self, model, data, default): - """ - Read Pyomo data commands and process the data. - """ + """Read Pyomo data commands and process the data.""" _process_include(['include', self.filename], model, data, default, self.options) def clear(self): diff --git a/pyomo/duality/lagrangian_dual.py b/pyomo/duality/lagrangian_dual.py index 96bc3f4a95e..78fb5a85d95 100644 --- a/pyomo/duality/lagrangian_dual.py +++ b/pyomo/duality/lagrangian_dual.py @@ -30,18 +30,19 @@ @TransformationFactory.register("core.lagrangian_dual", doc="Create the LP dual model.") class DualTransformation(IsomorphicTransformation): - """ - Creates a standard form Pyomo model that is equivalent to another model + """Creates a standard form Pyomo model that is equivalent to another + model Options - dual_constraint_suffix Defaults to _constraint - dual_variable_prefix Defaults to p_ - slack_names Defaults to auxiliary_slack - excess_names Defaults to auxiliary_excess - lb_names Defaults to _lower_bound - ub_names Defaults to _upper_bound - pos_suffix Defaults to _plus - neg_suffix Defaults to _minus + dual_constraint_suffix Defaults to ``_constraint`` + dual_variable_prefix Defaults to ``p_`` + slack_names Defaults to ``auxiliary_slack`` + excess_names Defaults to ``auxiliary_excess`` + lb_names Defaults to ``_lower_bound`` + ub_names Defaults to ``_upper_bound`` + pos_suffix Defaults to ``_plus`` + neg_suffix Defaults to ``_minus`` + """ @deprecated( diff --git a/pyomo/gdp/plugins/bigm.py b/pyomo/gdp/plugins/bigm.py index d715d913db8..7fae0876a90 100644 --- a/pyomo/gdp/plugins/bigm.py +++ b/pyomo/gdp/plugins/bigm.py @@ -93,18 +93,17 @@ class BigM_Transformation(GDP_to_MIP_Transformation, _BigM_MixIn): targets: the targets to transform [default: the instance] M values are determined as follows: - 1) if the constraint appears in the bigM argument dict - 2) if the constraint parent_component appears in the bigM - argument dict - 3) if any block which is an ancestor to the constraint appears in + 1. if the constraint appears in the bigM argument dict + 2. if the constraint parent_component appears in the bigM argument dict + 3. if any block which is an ancestor to the constraint appears in the bigM argument dict - 3) if 'None' is in the bigM argument dict - 4) if the constraint or the constraint parent_component appear in + 4. if 'None' is in the bigM argument dict + 5. if the constraint or the constraint parent_component appear in a BigM Suffix attached to any parent_block() beginning with the constraint's parent_block and moving up to the root model. - 5) if None appears in a BigM Suffix attached to any + 6. if None appears in a BigM Suffix attached to any parent_block() between the constraint and the root model. - 6) if the constraint is linear, estimate M using the variable bounds + 7. if the constraint is linear, estimate M using the variable bounds M values may be a single value or a 2-tuple specifying the M for the lower bound and the upper bound of the constraint body. diff --git a/pyomo/gdp/plugins/hull.py b/pyomo/gdp/plugins/hull.py index 854366c0cf0..547911e67a0 100644 --- a/pyomo/gdp/plugins/hull.py +++ b/pyomo/gdp/plugins/hull.py @@ -86,6 +86,15 @@ class Hull_Reformulation(GDP_to_MIP_Transformation): This transformation accepts the following keyword arguments: + The transformation will create a new Block with a unique + name beginning "_pyomo_gdp_hull_reformulation". It will contain an + indexed Block named "relaxedDisjuncts" that will hold the relaxed + disjuncts. This block is indexed by an integer indicating the order + in which the disjuncts were relaxed. All transformed Disjuncts will + have a pointer to the block their transformed constraints are on, + and all transformed Disjunctions will have a pointer to the + corresponding OR or XOR constraint. + Parameters ---------- perspective_function : str @@ -94,18 +103,9 @@ class Hull_Reformulation(GDP_to_MIP_Transformation): 'LeeGrossmann', or 'GrossmannLee' EPS : float The value to use for epsilon [default: 1e-4] - targets : (block, disjunction, or list of those types) + targets : block, disjunction, or list of those types The targets to transform. This can be a block, disjunction, or a list of blocks and Disjunctions [default: the instance] - - The transformation will create a new Block with a unique - name beginning "_pyomo_gdp_hull_reformulation". It will contain an - indexed Block named "relaxedDisjuncts" that will hold the relaxed - disjuncts. This block is indexed by an integer indicating the order - in which the disjuncts were relaxed. All transformed Disjuncts will - have a pointer to the block their transformed constraints are on, - and all transformed Disjunctions will have a pointer to the - corresponding OR or XOR constraint. """ CONFIG = cfg.ConfigDict('gdp.hull') diff --git a/pyomo/gdp/plugins/partition_disjuncts.py b/pyomo/gdp/plugins/partition_disjuncts.py index 68658dca2c0..3884adcc048 100644 --- a/pyomo/gdp/plugins/partition_disjuncts.py +++ b/pyomo/gdp/plugins/partition_disjuncts.py @@ -100,17 +100,20 @@ def _generate_additively_separable_repn(nonlinear_part): def arbitrary_partition(disjunction, P): - """ - Returns a valid partition into P sets of the variables that appear in + """Returns a valid partition into P sets of the variables that appear in algebraic additively separable constraints in the Disjuncts in 'disjunction'. Note that this method may return an invalid partition if the constraints are not additively separable! Arguments: ---------- - disjunction : A Disjunction object for which the variable partition will be - created. - P : An int, the number of partitions + disjunction : DisjunctionData + A Disjunction object for which the variable partition will be + created. + + P : int + the number of partitions + """ # collect variables v_set = ComponentSet() @@ -127,20 +130,26 @@ def arbitrary_partition(disjunction, P): def compute_optimal_bounds(expr, global_constraints, opt): - """ - Returns a tuple (LB, UB) where LB and UB are the results of minimizing + """Returns a tuple (LB, UB) where LB and UB are the results of minimizing and maximizing expr over the variable bounds and the constraints on the global_constraints block. Note that if expr is nonlinear, even if one of the min and max problems is convex, the other won't be! Arguments: ---------- - expr : The subexpression whose bounds we will return - global_constraints : A Block which contains the global Constraints and Vars - of the original model - opt : A configured SolverFactory to use to minimize and maximize expr over - the set defined by global_constraints. Note that if expr is nonlinear, - opt will need to be capable of optimizing nonconvex problems. + expr : ExpressionBase + The subexpression whose bounds we will return + + global_constraints : BlockData + A Block which contains the global Constraints and Vars of the + original model + + opt : SolverBase + A configured Solver object to use to minimize and maximize expr + over the set defined by global_constraints. Note that if expr + is nonlinear, opt will need to be capable of optimizing + nonconvex problems. + """ if opt is None: raise GDP_Error( @@ -352,8 +361,10 @@ class PartitionDisjuncts_Transformation(Transformation): the auxiliary variables created by the transformation. Some pre-implemented options include - * compute_fbbt_bounds (the default), and - * compute_optimal_bounds + + * compute_fbbt_bounds (the default), and + * compute_optimal_bounds + or you can write your own callback which accepts an Expression object, a model containing the variables and global constraints of the original instance, and a configured solver and returns a tuple (LB, UB) where diff --git a/pyomo/neos/kestrel.py b/pyomo/neos/kestrel.py index 8959a81bd0f..7c1518fe06f 100644 --- a/pyomo/neos/kestrel.py +++ b/pyomo/neos/kestrel.py @@ -210,9 +210,12 @@ def getJobAndPassword(self): def getSolverName(self): """ Read in the kestrel_options to pick out the solver name. + The tricky parts: - we don't want to be case sensitive, but NEOS is. - we need to read in options variable + + - we don't want to be case sensitive, but NEOS is. + - we need to read in options variable + """ # Get a list of available kestrel solvers from NEOS allKestrelSolvers = self.neos.listSolversInCategory("kestrel") diff --git a/pyomo/neos/plugins/NEOS.py b/pyomo/neos/plugins/NEOS.py index 84bc51645c0..07e0f2e0265 100644 --- a/pyomo/neos/plugins/NEOS.py +++ b/pyomo/neos/plugins/NEOS.py @@ -34,7 +34,7 @@ def __init__(self, **kwds): def create_command_line(self, executable, problem_files): """ - Create the local *.sol and *.log files, which will be + Create the local ``*.sol`` and ``*.log`` files, which will be populated by NEOS. """ if self._log_file is None: diff --git a/pyomo/opt/plugins/res.py b/pyomo/opt/plugins/res.py index 31971ee7d25..1f2fed261f6 100644 --- a/pyomo/opt/plugins/res.py +++ b/pyomo/opt/plugins/res.py @@ -22,7 +22,7 @@ @results.ReaderFactory.register(str(ResultsFormat.yaml)) class ResultsReader_yaml(results.AbstractResultsReader): """ - Class that reads in a *.yml file and generates a + Class that reads in a ``*.yml`` file and generates a SolverResults object. """ @@ -43,7 +43,7 @@ def __call__(self, filename, res=None, soln=None, suffixes=[]): @results.ReaderFactory.register(str(ResultsFormat.json)) class ResultsReader_json(results.AbstractResultsReader): """ - Class that reads in a *.jsn file and generates a + Class that reads in a ``*.jsn`` file and generates a SolverResults object. """ @@ -51,9 +51,7 @@ def __init__(self): results.AbstractResultsReader.__init__(self, ResultsFormat.json) def __call__(self, filename, res=None, soln=None, suffixes=[]): - """ - Parse a *.results file - """ + """Parse a ``*.results`` file""" if res is None: res = SolverResults() # diff --git a/pyomo/opt/plugins/sol.py b/pyomo/opt/plugins/sol.py index 10da469f186..efcb36877bd 100644 --- a/pyomo/opt/plugins/sol.py +++ b/pyomo/opt/plugins/sol.py @@ -23,7 +23,7 @@ @results.ReaderFactory.register(str(ResultsFormat.sol)) class ResultsReader_sol(results.AbstractResultsReader): """ - Class that reads in a *.sol results file and generates a + Class that reads in a ``*.sol`` results file and generates a SolverResults object. """ @@ -34,7 +34,7 @@ def __init__(self, name=None): def __call__(self, filename, res=None, soln=None, suffixes=[]): """ - Parse a *.sol file + Parse a ``*.sol`` file """ try: with open(filename, "r") as f: diff --git a/pyomo/repn/ampl.py b/pyomo/repn/ampl.py index c6056bd9592..c28785e050b 100644 --- a/pyomo/repn/ampl.py +++ b/pyomo/repn/ampl.py @@ -223,11 +223,14 @@ class AMPLRepn(object): The general nonlinear portion of the compiled expression as a tuple of two parts: - - the nl template string: this is the NL string with - placeholders (`%s`) for all the variables that appear in - the expression. - - an iterable if the `VarData` IDs that correspond to the - placeholders in the nl template string + + - the nl template string: this is the NL string with + placeholders (``%s``) for all the variables that appear in + the expression. + + - an iterable if the :class:`VarData` IDs that correspond to the + placeholders in the nl template string + This is `None` if there is no general nonlinear part of the expression. Note that this can be a list of tuple fragments within AMPLRepnVisitor, but that list is concatenated to a diff --git a/pyomo/repn/plugins/lp_writer.py b/pyomo/repn/plugins/lp_writer.py index 2fbdae3571d..ce72d8f1ed3 100644 --- a/pyomo/repn/plugins/lp_writer.py +++ b/pyomo/repn/plugins/lp_writer.py @@ -107,10 +107,12 @@ class LPWriter(object): doc=""" How much effort do we want to put into ensuring the LP file is written deterministically for a Pyomo model: - NONE (0) : None - ORDERED (10): rely on underlying component ordering (default) - SORT_INDICES (20) : sort keys of indexed components - SORT_SYMBOLS (30) : sort keys AND sort names (not declaration order) + + - NONE (0) : None + - ORDERED (10): rely on underlying component ordering (default) + - SORT_INDICES (20) : sort keys of indexed components + - SORT_SYMBOLS (30) : sort keys AND sort names (not declaration order) + """, ), ) @@ -142,8 +144,6 @@ class LPWriter(object): default=None, description='Preferred variable ordering', doc=""" - - List of variables in the order that they should appear in the LP file. Note that this is only a suggestion, as the LP file format is row-major and the columns are inferred from diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 2fcb7679df1..bc7e703a1a7 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -190,10 +190,12 @@ class NLWriter(object): doc=""" How much effort do we want to put into ensuring the NL file is written deterministically for a Pyomo model: - NONE (0) : None - ORDERED (10): rely on underlying component ordering (default) - SORT_INDICES (20) : sort keys of indexed components - SORT_SYMBOLS (30) : sort keys AND sort names (not declaration order) + + - NONE (0) : None + - ORDERED (10): rely on underlying component ordering (default) + - SORT_INDICES (20) : sort keys of indexed components + - SORT_SYMBOLS (30) : sort keys AND sort names (not declaration order) + """, ), ) diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index e684829e2f4..86decb4122e 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -66,7 +66,7 @@ class LinearStandardFormInfo(object): The objective coefficients. Note that this is a sparse array and may contain multiple rows (for multiobjective problems). The - objectives may be calculated by "c @ x" + objectives may be calculated by ``c @ x`` c_offset : numpy.ndarray @@ -75,7 +75,7 @@ class LinearStandardFormInfo(object): A : scipy.sparse.csc_array The constraint coefficients. The constraint bodies may be - calculated by "A @ x" + calculated by ``A @ x`` rhs : numpy.ndarray @@ -122,18 +122,35 @@ def __init__(self, c, c_offset, A, rhs, rows, columns, objectives, eliminated_va @property def x(self): + "Alias for :attr:`columns`" return self.columns @property def b(self): + "Alias for :attr:`rhs`" return self.rhs @WriterFactory.register( - 'compile_standard_form', 'Compile an LP to standard form (`min cTx s.t. Ax <= b`)' + 'compile_standard_form', + r'Compile an LP to standard form (:math:`\min c^Tx s.t. Ax \le b)`', ) class LinearStandardFormCompiler(object): + r"""Compiler to convert an LP to the matrix representation of the + standard form: + + .. math:: + + \min\ & c^Tx \\ + s.t.\ & Ax \le b + + and return the compiled representation as NumPy arrays and SciPy + sparse matrices. + + """ + CONFIG = ConfigBlock('compile_standard_form') + CONFIG.declare( 'nonnegative_vars', ConfigValue( @@ -147,7 +164,8 @@ class LinearStandardFormCompiler(object): ConfigValue( default=False, domain=bool, - description='Add slack variables and return `min cTx s.t. Ax == b`', + description='Add slack variables and return ' + r':math:`\min c^Tx; s.t. Ax = b`', ), ) CONFIG.declare( @@ -184,10 +202,13 @@ class LinearStandardFormCompiler(object): doc=""" How much effort do we want to put into ensuring the resulting matrices are produced deterministically: - NONE (0) : None - ORDERED (10): rely on underlying component ordering (default) - SORT_INDICES (20) : sort keys of indexed components - SORT_SYMBOLS (30) : sort keys AND sort names (not declaration order) + + - ``NONE`` (0): None + - ``ORDERED`` (10): rely on underlying component ordering (default) + - ``SORT_INDICES`` (20) : sort keys of indexed components + - ``SORT_SYMBOLS`` (30) : sort keys AND sort names (not + declaration order) + """, ), ) @@ -198,7 +219,7 @@ class LinearStandardFormCompiler(object): description='Preferred constraint ordering', doc=""" List of constraints in the order that they should appear in - the resulting `A` matrix. Unspecified constraints will + the resulting ``A`` matrix. Unspecified constraints will appear at the end.""", ), ) @@ -219,7 +240,7 @@ def __init__(self): @document_kwargs_from_configdict(CONFIG) def write(self, model, ostream=None, **options): - """Convert a model to standard form (`min cTx s.t. Ax <= b`) + """Convert a model to standard form Returns ------- @@ -230,7 +251,7 @@ def write(self, model, ostream=None, **options): model: ConcreteModel The concrete Pyomo model to write out. - ostream: None + ostream: This is provided for API compatibility with other writers and is ignored here. diff --git a/pyomo/repn/util.py b/pyomo/repn/util.py index 32ec99dac0f..a4d056784e9 100644 --- a/pyomo/repn/util.py +++ b/pyomo/repn/util.py @@ -242,12 +242,12 @@ def __rpow__(self, other): class BeforeChildDispatcher(collections.defaultdict): - """Dispatcher for handling the :py:class:`StreamBasedExpressionVisitor` + """Dispatcher for handling the :class:`StreamBasedExpressionVisitor` `beforeChild` callback - This dispatcher implements a specialization of :py:`defaultdict` + This dispatcher implements a specialization of :class:`defaultdict` that supports automatic type registration. Any missing types will - return the :py:meth:`register_dispatcher` method, which (when called + return the :meth:`register_dispatcher` method, which (when called as a callback) will interrogate the type, identify the appropriate callback, add the callback to the dict, and return the result of calling the callback. As the callback is added to the dict, no type @@ -375,10 +375,10 @@ def _before_param(visitor, child): class ExitNodeDispatcher(collections.defaultdict): - """Dispatcher for handling the :py:class:`StreamBasedExpressionVisitor` + """Dispatcher for handling the :class:`StreamBasedExpressionVisitor` `exitNode` callback - This dispatcher implements a specialization of :py:`defaultdict` + This dispatcher implements a specialization of :class:`defaultdict` that supports automatic type registration. As the identified callback is added to the dict, no type will incur the overhead of `register_dispatcher` more than once. diff --git a/pyomo/scripting/interface.py b/pyomo/scripting/interface.py index fca485b279b..a6ac425c8c0 100644 --- a/pyomo/scripting/interface.py +++ b/pyomo/scripting/interface.py @@ -29,9 +29,12 @@ def pyomo_callback(name): Example: - @pyomo_callback('cut-callback') - def my_cut_generator(solver, model): - ... + .. code:: + + @pyomo_callback('cut-callback') + def my_cut_generator(solver, model): + ... + """ def fn(f): diff --git a/pyomo/scripting/util.py b/pyomo/scripting/util.py index b2a30ebaecd..351e422a250 100644 --- a/pyomo/scripting/util.py +++ b/pyomo/scripting/util.py @@ -971,30 +971,45 @@ def __exit__(self, et, ev, tb): def run_command( command=None, parser=None, args=None, name='unknown', data=None, options=None ): - """ - Execute a function that processes command-line arguments and + """Execute a function that processes command-line arguments and then calls a command-line driver. This function provides a generic facility for executing a command function is rather generic. This function is segregated from the driver to enable profiling of the command-line execution. - Required: - command: The name of a function that will be executed to perform process the command-line - options with a parser object. - parser: The parser object that is used by the command-line function. + Parameters + ---------- + command: - Optional: - options: If this is not None, then ignore the args option and use - this to specify command options. - args: Command-line arguments that are parsed. If this value is `None`, then the - arguments in `sys.argv` are used to parse the command-line. - name: Specifying the name of the command-line (for error messages). - data: A container of labeled data. + The name of a function that will be executed to perform process + the command-line options with a parser object. + + parser: + The parser object that is used by the command-line function. + + options: + If this is not None, then ignore the args option and use this to + specify command options. + + args: + Command-line arguments that are parsed. If this value is + `None`, then the arguments in `sys.argv` are used to parse the + command-line. + + name: + Specifying the name of the command-line (for error messages). + + data: + A container of labeled data. + + Returns + ------- + retval: + Return values from the command-line execution. + errorcode: + 0 if Pyomo ran successfully - Returned: - retval: Return values from the command-line execution. - errorcode: 0 if Pyomo ran successfully """ # # diff --git a/pyomo/util/slices.py b/pyomo/util/slices.py index d85aa3fa926..b3da3b4be2a 100644 --- a/pyomo/util/slices.py +++ b/pyomo/util/slices.py @@ -92,18 +92,16 @@ def slice_component_along_sets(comp, sets, context=None): Parameters: ----------- - comp: `pyomo.core.base.component.Component` or - `pyomo.core.base.component.ComponentData` + comp: :class:`Component` or :class:`ComponentData` Component whose parent structure to search and replace - sets: `pyomo.common.collections.ComponentSet` + sets: `~pyomo.common.collections.ComponentSet` Contains the sets to replace with slices - context: `pyomo.core.base.block.Block` or - `pyomo.core.base.block.BlockData` + context: :class:`Block` or :class:`BlockData` Block below which to search for sets Returns: -------- - `pyomo.core.base.indexed_component_slice.IndexedComponent_slice`: + `~pyomo.core.base.indexed_component_slice.IndexedComponent_slice`: Slice of `comp` with wildcards replacing the indices of `sets` """ From 8dc93cb412cadef34c4a5a47e5bca4473a373efa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 1 Oct 2024 16:53:50 -0600 Subject: [PATCH 2480/3044] Switch to using ply decorators to not confuse Sphinx --- pyomo/dataportal/parse_datacmds.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pyomo/dataportal/parse_datacmds.py b/pyomo/dataportal/parse_datacmds.py index 60e2f2c0acb..481eed7ba9e 100644 --- a/pyomo/dataportal/parse_datacmds.py +++ b/pyomo/dataportal/parse_datacmds.py @@ -90,9 +90,11 @@ # Notes on PLY tokenization # - token functions (beginning with "t_") are prioritized in the order # that they are declared in this module +# - use @lex.TOKEN instead of docstrings to avoid errors from the +# Sphinx autosummary # +@lex.TOKEN(r'[\n]+') def t_newline(t): - r'[\n]+' t.lexer.lineno += len(t.value) t.lexer.linepos.extend(t.lexpos + i for i, _ in enumerate(t.value)) @@ -114,14 +116,14 @@ def t_COMMENT(t): t.lexer.linepos.extend(lastpos for i in range(nlines)) +@lex.TOKEN(r':=') def t_COLONEQ(t): - r':=' t.lexer.begin('data') return t +@lex.TOKEN(r';') def t_SEMICOLON(t): - r';' t.lexer.begin('INITIAL') return t @@ -139,27 +141,27 @@ def t_NUM_VAL(t): return t +@lex.TOKEN(r'[a-zA-Z_][a-zA-Z0-9_\.\-]*\[') def t_WORDWITHLBRACKET(t): - r'[a-zA-Z_][a-zA-Z0-9_\.\-]*\[' return t +@lex.TOKEN(r'[a-zA-Z_][a-zA-Z_0-9\.+\-]*') def t_WORD(t): - r'[a-zA-Z_][a-zA-Z_0-9\.+\-]*' if t.value in reserved: t.type = reserved[t.value] # Check for reserved words return t +@lex.TOKEN(r'[a-zA-Z0-9_\.+\-\\\/]+') def t_STRING(t): - r'[a-zA-Z0-9_\.+\-\\\/]+' # Note: RE guarantees the string has no embedded quotation characters t.value = '"' + t.value + '"' return t +@lex.TOKEN(r'[a-zA-Z0-9_\.+\-]*\[[a-zA-Z0-9_\.+\-\*,\s]+\]') def t_data_BRACKETEDSTRING(t): - r'[a-zA-Z0-9_\.+\-]*\[[a-zA-Z0-9_\.+\-\*,\s]+\]' # NO SPACES # a[1,_df,'foo bar'] # [1,*,'foo bar'] From 55306077fb3734afa8aacb88487071b3d711e774 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 09:24:01 -0600 Subject: [PATCH 2481/3044] Rework building_documentation() logic to improve Sphinx output/suppress warnings --- pyomo/common/config.py | 27 +++++++++++++++++++-------- pyomo/common/dependencies.py | 2 +- pyomo/common/flags.py | 13 ++++++------- pyomo/common/tests/test_flags.py | 2 +- pyomo/contrib/pyros/config.py | 8 ++++---- 5 files changed, 31 insertions(+), 21 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index ebba2f2732a..9eb0622fe71 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -40,6 +40,7 @@ relocated_module_attribute, ) from pyomo.common.fileutils import import_file +from pyomo.common.flags import building_documentation from pyomo.common.formatting import wrap_reStructuredText from pyomo.common.modeling import NOTSET @@ -298,7 +299,7 @@ def __call__(self, value): raise ValueError("%r is not a valid %s" % (value, self._domain.__name__)) def domain_name(self): - return f'InEnum[{self._domain.__name__}]' + return f'InEnum[{_domain_name(self._domain)}]' class IsInstance(object): @@ -361,8 +362,8 @@ def __call__(self, obj): raise ValueError(msg) def domain_name(self): - class_names = (self._get_class_name(kls) for kls in self.baseClasses) - return f"IsInstance({', '.join(class_names)})" + class_names = (_domain_name(kls) for kls in self.baseClasses) + return f"IsInstance[{', '.join(class_names)}]" class ListOf(object): @@ -558,7 +559,7 @@ def __call__(self, path): return ans def domain_name(self): - return type(self).__name__ + return _domain_name(type(self)) class PathList(Path): @@ -1133,16 +1134,26 @@ def _munge_name(name, space_to_dash=True): def _domain_name(domain): if domain is None: return "" - elif hasattr(domain, 'domain_name'): + if hasattr(domain, 'domain_name') and not isinstance(domain, type): dn = domain.domain_name if hasattr(dn, '__call__'): return dn() else: return dn - elif domain.__class__ is type: - return domain.__name__ + if domain.__module__ == 'builtins': + module = "" + else: + module = "~" + domain.__module__ + '.' + if isinstance(domain, type): + if building_documentation(): + return module + domain.__qualname__ + else: + return domain.__name__ elif inspect.isfunction(domain): - return domain.__name__ + if building_documentation(): + return module + domain.__qualname__ + else: + return domain.__name__ else: return None diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 32e1d0e336f..0458bdeade6 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -241,7 +241,7 @@ def UnavailableClass(unavailable_module): class UnavailableMeta(type): def __getattr__(cls, name): - if building_documentation(ignore_testing_flag=True): + if building_documentation(): # If we are building documentation, avoid the # DeferredImportError (we will still raise one if # someone attempts to *create* an instance of this diff --git a/pyomo/common/flags.py b/pyomo/common/flags.py index 9a025fc8003..3cfcd2d426c 100644 --- a/pyomo/common/flags.py +++ b/pyomo/common/flags.py @@ -78,16 +78,15 @@ def in_testing_environment(state=NOTSET): in_testing_environment.state = None -def building_documentation(ignore_testing_flag=False): - """Return True if we are building the Sphinx documentation +def building_documentation(): + """True if we are building the Sphinx documentation + + We detect if we are building the documentation by looking if the + ``sphnx`` or ``Sphinx`` modules are imported. Returns ------- bool """ - import sys - - return (ignore_testing_flag or not in_testing_environment()) and ( - 'sphinx' in sys.modules or 'Sphinx' in sys.modules - ) + return 'sphinx' in sys.modules or 'Sphinx' in sys.modules diff --git a/pyomo/common/tests/test_flags.py b/pyomo/common/tests/test_flags.py index cf3a3313dca..4bb7fcb7220 100644 --- a/pyomo/common/tests/test_flags.py +++ b/pyomo/common/tests/test_flags.py @@ -31,7 +31,7 @@ def test_NOTSET(self): for i in sorted(sys.modules.items()): print(i) self.assertTrue(in_testing_environment()) - self.assertFalse(building_documentation()) + self.assertTrue(building_documentation()) self.assertEqual(repr(NOTSET), 'pyomo.common.flags.NOTSET') in_testing_environment(False) diff --git a/pyomo/contrib/pyros/config.py b/pyomo/contrib/pyros/config.py index c02dcd7ed0f..fca42e78cbd 100644 --- a/pyomo/contrib/pyros/config.py +++ b/pyomo/contrib/pyros/config.py @@ -14,6 +14,7 @@ NonNegativeFloat, InEnum, Path, + _domain_name, ) from pyomo.common.errors import ApplicationError, PyomoException from pyomo.core.base import Var, VarData @@ -209,10 +210,9 @@ def __call__(self, obj, from_iterable=None, allow_repeats=None): def domain_name(self): """Return str briefly describing domain encompassed by self.""" - return ( - f"{self.cdatatype.__name__}, {self.ctype.__name__}, " - f"or Iterable of {self.cdatatype.__name__}/{self.ctype.__name__}" - ) + cdt = _domain_name(self.cdatatype) + ct = _domain_name(self.ctype) + return f"{cdt}, {ct}, or Iterable[{cdt}/{ct}" class SolverNotResolvable(PyomoException): From 6297b950a843d6d6d080fbb862a0c4b99f530d04 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 09:25:13 -0600 Subject: [PATCH 2482/3044] Avoid importing Qt when building documentation to suppress Sphine API autodoc warnings --- pyomo/contrib/viewer/model_browser.py | 44 +++++++++++++++++--------- pyomo/contrib/viewer/model_select.py | 38 ++++++++++++++-------- pyomo/contrib/viewer/qt.py | 13 ++++++++ pyomo/contrib/viewer/residual_table.py | 38 ++++++++++++++-------- pyomo/contrib/viewer/ui.py | 35 +++++++++++++------- 5 files changed, 115 insertions(+), 53 deletions(-) diff --git a/pyomo/contrib/viewer/model_browser.py b/pyomo/contrib/viewer/model_browser.py index 91dc946c55d..75971c9fda3 100644 --- a/pyomo/contrib/viewer/model_browser.py +++ b/pyomo/contrib/viewer/model_browser.py @@ -28,11 +28,9 @@ import os import logging -_log = logging.getLogger(__name__) - -import pyomo.contrib.viewer.qt as myqt +from pyomo.common.fileutils import this_file_dir +from pyomo.common.flags import building_documentation from pyomo.contrib.viewer.report import value_no_exception, get_residual - from pyomo.core.base.param import ParamData from pyomo.environ import ( Block, @@ -44,19 +42,35 @@ value, units, ) -from pyomo.common.fileutils import this_file_dir -mypath = this_file_dir() -try: - _ModelBrowserUI, _ModelBrowser = myqt.uic.loadUiType( - os.path.join(mypath, "model_browser.ui") - ) -except: - # This lets the file still be imported, but you won't be able to use it - class _ModelBrowserUI(object): - pass +import pyomo.contrib.viewer.qt as myqt - class _ModelBrowser(object): +_log = logging.getLogger(__name__) + + +# This lets the file be imported when the Qt UI is not available (or +# when building docs), but you won't be able to use it +class _ModelBrowserUI(object): + pass + + +class _ModelBrowser(object): + pass + + +# Note that the classes loaded here have signatures that are not +# parsable by Sphinx, so we won't attempt to import them if we are +# building the API documentation. +if not building_documentation(): + import sys + + print("\n".join(map(str, sorted(sys.modules)))) + mypath = this_file_dir() + try: + _ModelBrowserUI, _ModelBrowser = myqt.uic.loadUiType( + os.path.join(mypath, "model_browser.ui") + ) + except: pass diff --git a/pyomo/contrib/viewer/model_select.py b/pyomo/contrib/viewer/model_select.py index 1e65e91a089..c611b4e9a20 100644 --- a/pyomo/contrib/viewer/model_select.py +++ b/pyomo/contrib/viewer/model_select.py @@ -28,23 +28,35 @@ import logging import os -_log = logging.getLogger(__name__) +from pyomo.common.fileutils import this_file_dir +from pyomo.common.flags import building_documentation -import pyomo.environ as pyo import pyomo.contrib.viewer.qt as myqt -from pyomo.common.fileutils import this_file_dir +import pyomo.environ as pyo + +_log = logging.getLogger(__name__) + + +# This lets the file be imported when the Qt UI is not available (or +# when building docs), but you won't be able to use it +class _ModelSelectUI(object): + pass + + +class _ModelSelect(object): + pass -mypath = this_file_dir() -try: - _ModelSelectUI, _ModelSelect = myqt.uic.loadUiType( - os.path.join(mypath, "model_select.ui") - ) -except: - # This lets the file still be imported, but you won't be able to use it - class _ModelSelectUI(object): - pass - class _ModelSelect(object): +# Note that the classes loaded here have signatures that are not +# parsable by Sphinx, so we won't attempt to import them if we are +# building the API documentation. +if not building_documentation(): + mypath = this_file_dir() + try: + _ModelSelectUI, _ModelSelect = myqt.uic.loadUiType( + os.path.join(mypath, "model_select.ui") + ) + except: pass diff --git a/pyomo/contrib/viewer/qt.py b/pyomo/contrib/viewer/qt.py index 2715d275758..f2744fa2d56 100644 --- a/pyomo/contrib/viewer/qt.py +++ b/pyomo/contrib/viewer/qt.py @@ -30,6 +30,8 @@ import enum import importlib +from pyomo.common.flags import building_documentation + # Supported Qt wrappers in preferred order supported = ["PySide6", "PyQt5"] # Import errors encountered, delay logging for testing reasons @@ -127,3 +129,14 @@ class QItemDelegate(object): from PyQt5.QtWidgets import QAction from PyQt5.QtCore import pyqtSignal as Signal from PyQt5 import uic + + # Note that QAbstractTableModel and QAbstractItemModel have + # signatures that are not parsable by Sphinx, so we will hide them + # if we are building the API documentation. + if building_documentation(): + + class QAbstractItemModel(object): + pass + + class QAbstractTableModel(object): + pass diff --git a/pyomo/contrib/viewer/residual_table.py b/pyomo/contrib/viewer/residual_table.py index 94e8902848f..6347dff2462 100644 --- a/pyomo/contrib/viewer/residual_table.py +++ b/pyomo/contrib/viewer/residual_table.py @@ -28,25 +28,37 @@ import os import logging -_log = logging.getLogger(__name__) +from pyomo.common.fileutils import this_file_dir +from pyomo.common.flags import building_documentation +from pyomo.contrib.viewer.report import value_no_exception, get_residual import pyomo.contrib.viewer.qt as myqt -from pyomo.contrib.viewer.report import value_no_exception, get_residual import pyomo.environ as pyo -from pyomo.common.fileutils import this_file_dir -mypath = this_file_dir() -try: - _ResidualTableUI, _ResidualTable = myqt.uic.loadUiType( - os.path.join(mypath, "residual_table.ui") - ) -except: +_log = logging.getLogger(__name__) - class _ResidualTableUI(object): - pass - class _ResidualTable(object): - pass +# This lets the file be imported when the Qt UI is not available (or +# when building docs), but you won't be able to use it +class _ResidualTableUI(object): + pass + + +class _ResidualTable(object): + pass + + +# Note that the classes loaded here have signatures that are not +# parsable by Sphinx, so we won't attempt to import them if we are +# building the API documentation. +if not building_documentation(): + mypath = this_file_dir() + try: + _ResidualTableUI, _ResidualTable = myqt.uic.loadUiType( + os.path.join(mypath, "residual_table.ui") + ) + except: + passss class ResidualTable(_ResidualTable, _ResidualTableUI): diff --git a/pyomo/contrib/viewer/ui.py b/pyomo/contrib/viewer/ui.py index ac96e58eea9..ecbadeda3cf 100644 --- a/pyomo/contrib/viewer/ui.py +++ b/pyomo/contrib/viewer/ui.py @@ -39,28 +39,39 @@ def get_ipython(): import pyomo.contrib.viewer.report as rpt import pyomo.environ as pyo import pyomo.contrib.viewer.qt as myqt + +from pyomo.common.fileutils import this_file_dir +from pyomo.common.flags import building_documentation from pyomo.contrib.viewer.model_browser import ModelBrowser from pyomo.contrib.viewer.residual_table import ResidualTable from pyomo.contrib.viewer.model_select import ModelSelect from pyomo.contrib.viewer.ui_data import UIData -from pyomo.common.fileutils import this_file_dir _log = logging.getLogger(__name__) -_mypath = this_file_dir() -try: - _MainWindowUI, _MainWindow = myqt.uic.loadUiType(os.path.join(_mypath, "main.ui")) -except: - _log.exception("Failed to load UI files.") - # This lets the file still be imported, but you won't be able to use it - # Allowing this to be imported will let some basic tests pass without PyQt - class _MainWindowUI(object): - pass +# This lets the file be imported when the Qt UI is not available (or +# when building docs), but you won't be able to use it. Allowing this +# will let some basic tests run (and pass) without PyQt +class _MainWindowUI(object): + pass - class _MainWindow(object): - pass +class _MainWindow(object): + pass + + +# Note that the classes loaded here have signatures that are not +# parsable by Sphinx, so we won't attempt to import them if we are +# building the API documentation. +if not building_documentation(): + _mypath = this_file_dir() + try: + _MainWindowUI, _MainWindow = myqt.uic.loadUiType( + os.path.join(_mypath, "main.ui") + ) + except: + _log.exception("Failed to load UI files.") for _err in myqt.import_errors: _log.error(_err) From 29687a8c166f42afd51cf888b928e3c2d930f99f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 09:25:48 -0600 Subject: [PATCH 2483/3044] Update the deprecation decorators to avoid doc warnings (This normalizes the doc strings to avoid indention warnings from Sphinx) --- pyomo/common/deprecation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/common/deprecation.py b/pyomo/common/deprecation.py index 0d16b92d5dd..e4a1848fe69 100644 --- a/pyomo/common/deprecation.py +++ b/pyomo/common/deprecation.py @@ -102,7 +102,7 @@ def _wrap_class(cls, msg, logger, version, remove_in): if msg is not None or _doc is None: _doc = _deprecation_docstring(cls, msg, version, remove_in) if cls.__doc__: - _doc = cls.__doc__ + '\n\n' + _doc + _doc = inspect.cleandoc(cls.__doc__) + '\n\n' + _doc cls.__doc__ = 'DEPRECATED.\n\n' + _doc if _flagIdx < 0: @@ -132,7 +132,7 @@ def wrapper(*args, **kwargs): return func(*args, **kwargs) wrapper.__doc__ = 'DEPRECATED.\n\n' - _doc = func.__doc__ or '' + _doc = inspect.cleandoc(func.__doc__ or '') if _doc: wrapper.__doc__ += _doc + '\n\n' wrapper.__doc__ += _deprecation_docstring(func, msg, version, remove_in) From 2f280c2671b2971bbae121922810467e4e80086f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 09:30:00 -0600 Subject: [PATCH 2484/3044] Wrap enum.IntEnum to resolve warnings from Sphinx autodoc --- pyomo/common/enums.py | 27 ++++++++++++++++++++++++++- pyomo/contrib/appsi/base.py | 3 ++- pyomo/contrib/solver/base.py | 4 ++-- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/pyomo/common/enums.py b/pyomo/common/enums.py index 121155d4ae8..1c6890dbcd5 100644 --- a/pyomo/common/enums.py +++ b/pyomo/common/enums.py @@ -29,12 +29,37 @@ import enum import itertools +import re import sys if sys.version_info[:2] < (3, 11): _EnumType = enum.EnumMeta else: _EnumType = enum.EnumType +if sys.version_info[:2] < (3, 13): + # prior to 3.13 the int.{to,from}_bytes docstrings had LaTeX-like + # "`..'" quotations, which Sphinx can't parse correctly. + def _fix_doc(ref): + def _rewrite(func): + func.__doc__ = re.sub(r"`(\S+)'", r"`\1`", ref.__doc__) + return func + + return _rewrite + + class IntEnum(enum.IntEnum): + @_fix_doc(enum.IntEnum.to_bytes) + def to_bytes(self, /, length=1, byteorder='big', *, signed=False): + return super().to_bytes(length=length, byteorder=byteorder, signed=signed) + + # Note: we need to use a decorator to set the __doc__ *before* + # the @classmethod (which makes __doc__ read-only). + @classmethod + @_fix_doc(enum.IntEnum.from_bytes) + def from_bytes(cls, bytes, byteorder='big', *, signed=False): + return super()(bytes, byteorder=byteorder, signed=signed) + +else: + IntEnum = enum.IntEnum class ExtendedEnumType(_EnumType): @@ -131,7 +156,7 @@ def __new__(metacls, cls, bases, classdict, **kwds): return super().__new__(metacls, cls, bases, classdict, **kwds) -class NamedIntEnum(enum.IntEnum): +class NamedIntEnum(IntEnum): """An extended version of :py:class:`enum.IntEnum` that supports creating members by name as well as value. diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index a55b67aa762..9eb9c0fe656 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -21,6 +21,7 @@ Tuple, MutableMapping, ) +from pyomo.common.enums import IntEnum from pyomo.core.base.constraint import ConstraintData, Constraint from pyomo.core.base.sos import SOSConstraintData, SOSConstraint from pyomo.core.base.var import VarData, Var @@ -605,7 +606,7 @@ def __init__( class Solver(abc.ABC): - class Availability(enum.IntEnum): + class Availability(IntEnum): NotFound = 0 BadVersion = -1 BadLicense = -2 diff --git a/pyomo/contrib/solver/base.py b/pyomo/contrib/solver/base.py index 4fe8bee4e53..818f403718f 100644 --- a/pyomo/contrib/solver/base.py +++ b/pyomo/contrib/solver/base.py @@ -10,7 +10,6 @@ # ___________________________________________________________________________ import abc -import enum from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple import os @@ -20,6 +19,7 @@ from pyomo.core.base.block import BlockData from pyomo.core.base.objective import Objective, ObjectiveData from pyomo.common.config import document_kwargs_from_configdict, ConfigValue +from pyomo.common.enums import IntEnum from pyomo.common.errors import ApplicationError from pyomo.common.deprecation import deprecation_warning from pyomo.common.modeling import NOTSET @@ -79,7 +79,7 @@ def __enter__(self): def __exit__(self, t, v, traceback): """Exit statement - enables `with` statements.""" - class Availability(enum.IntEnum): + class Availability(IntEnum): """ Class to capture different statuses in which a solver can exist in order to record its availability for use. From 5df72038e6ea94ca5d30dd958f85cb0413e4d26d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 09:31:30 -0600 Subject: [PATCH 2485/3044] Clean up some imports --- .../contrib/alternative_solutions/solnpool.py | 7 ++++--- pyomo/contrib/appsi/base.py | 15 +++++++++------ pyomo/contrib/pyros/solve_data.py | 18 ------------------ pyomo/core/expr/symbol_map.py | 2 -- 4 files changed, 13 insertions(+), 29 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/solnpool.py b/pyomo/contrib/alternative_solutions/solnpool.py index 51acb57c8a5..6e3fa5441c9 100644 --- a/pyomo/contrib/alternative_solutions/solnpool.py +++ b/pyomo/contrib/alternative_solutions/solnpool.py @@ -14,6 +14,7 @@ logger = logging.getLogger(__name__) from pyomo.common.dependencies import attempt_import +from pyomo.common.errors import ApplicationError gurobipy, gurobipy_available = attempt_import("gurobipy") @@ -68,11 +69,11 @@ def gurobi_generate_solutions( # Setup gurobi # if not gurobipy_available: - raise pyomo.common.errors.ApplicationError("Solver (gurobi) not available") + raise ApplicationError("Solver (gurobi) not available") opt = appsi.solvers.Gurobi() if not opt.available(): - raise pyomo.common.errors.ApplicationError("Solver (gurobi) not available") + raise ApplicationError("Solver (gurobi) not available") opt.config.stream_solver = tee opt.config.load_solution = False @@ -90,7 +91,7 @@ def gurobi_generate_solutions( results = opt.solve(model) condition = results.termination_condition if not (condition == appsi.base.TerminationCondition.optimal): - raise pyomo.common.errors.ApplicationError( + raise ApplicationError( "Model cannot be solved, " "TerminationCondition = {}" ).format(condition.value) # diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 9eb9c0fe656..b6b60ce2166 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -11,6 +11,10 @@ import abc import enum +import os +import re +import weakref + from typing import ( Sequence, Dict, @@ -21,7 +25,12 @@ Tuple, MutableMapping, ) + +from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat +from pyomo.common.errors import ApplicationError from pyomo.common.enums import IntEnum +from pyomo.common.factory import Factory +from pyomo.common.timing import HierarchicalTimer from pyomo.core.base.constraint import ConstraintData, Constraint from pyomo.core.base.sos import SOSConstraintData, SOSConstraint from pyomo.core.base.var import VarData, Var @@ -31,12 +40,7 @@ from pyomo.common.collections import ComponentMap from .utils.get_objective import get_objective from .utils.collect_vars_and_named_exprs import collect_vars_and_named_exprs -from pyomo.common.timing import HierarchicalTimer -from pyomo.common.config import ConfigDict, ConfigValue, NonNegativeFloat -from pyomo.common.errors import ApplicationError from pyomo.opt.base import SolverFactory as LegacySolverFactory -from pyomo.common.factory import Factory -import os from pyomo.opt.results.results_ import SolverResults as LegacySolverResults from pyomo.opt.results.solution import ( Solution as LegacySolution, @@ -48,7 +52,6 @@ ) from pyomo.core.kernel.objective import minimize from pyomo.core.base import SymbolMap -import weakref from .cmodel import cmodel, cmodel_available from pyomo.core.staleflag import StaleFlagManager from pyomo.core.expr.numvalue import NumericConstant diff --git a/pyomo/contrib/pyros/solve_data.py b/pyomo/contrib/pyros/solve_data.py index a6152fa3e02..a1667d88781 100644 --- a/pyomo/contrib/pyros/solve_data.py +++ b/pyomo/contrib/pyros/solve_data.py @@ -310,8 +310,6 @@ class DiscreteSeparationSolveCallResults: scenario_indexes solver_call_results performance_constraint - time_out - subsolver_error """ def __init__( @@ -396,12 +394,6 @@ class SeparationLoopResults: solved_globally worst_case_perf_con all_discrete_scenarios_exhausted - found_violation - violating_param_realization - scaled_violations - violating_separation_variable_values - subsolver_error - time_out """ def __init__( @@ -554,16 +546,6 @@ class SeparationResults: ---------- local_separation_loop_results global_separation_loop_results - main_loop_results - subsolver_error - time_out - solved_locally - solved_globally - found_violation - violating_param_realization - scaled_violations - violating_separation_variable_values - robustness_certified """ def __init__(self, local_separation_loop_results, global_separation_loop_results): diff --git a/pyomo/core/expr/symbol_map.py b/pyomo/core/expr/symbol_map.py index 87ba1b57bab..4364e54a608 100644 --- a/pyomo/core/expr/symbol_map.py +++ b/pyomo/core/expr/symbol_map.py @@ -9,8 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from weakref import ref as weakref_ref - class SymbolMap(object): """ From f08ebadf11a77483e3c3c24a52b62adac19a4ce4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 09:32:28 -0600 Subject: [PATCH 2486/3044] Output str instead of repr for Config default values --- pyomo/common/config.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 9eb0622fe71..46ec85fe822 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -1495,11 +1495,7 @@ def _item_body(self, indent, obj): None, [ 'dict' if isinstance(obj, ConfigDict) else obj.domain_name(), - ( - 'optional' - if obj._default is None - else f'default={repr(obj._default)}' - ), + ('optional' if obj._default is None else f'default={obj._default}'), ], ) ) From f8c99c4161a1579e0203ab13cbe320a82a0fef5f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 09:32:55 -0600 Subject: [PATCH 2487/3044] Add blank line to help resolve Sphinx autodoc warnings --- pyomo/common/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 46ec85fe822..5b59e437eb2 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -1510,7 +1510,7 @@ def _item_body(self, indent, obj): # space before the colon at the top level (which at lower levels # causes nested definition lists to NOT omit the colon), we will # generate non-standard ReST and omit the preceding space: - self.out.write(f'\n{indent}{obj.name()}: {typeinfo}\n') + self.out.write(f'\n{indent}{obj.name()}: {typeinfo}\n\n') self.wrapper.initial_indent = indent + ' ' * self.indent_spacing self.wrapper.subsequent_indent = indent + ' ' * self.indent_spacing vis = "" From 03cb961fce9f9657d55376635f15672acd1ebc5f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 09:33:27 -0600 Subject: [PATCH 2488/3044] Update TestCase documentation & resolve some Sphinx warnings --- pyomo/common/unittest.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/pyomo/common/unittest.py b/pyomo/common/unittest.py index 14a9024aeea..fb9584c652b 100644 --- a/pyomo/common/unittest.py +++ b/pyomo/common/unittest.py @@ -501,14 +501,31 @@ class TestCase(_unittest.TestCase): This class derives from unittest.TestCase and provides the following additional functionality: - - additional assertions: - * :py:meth:`assertStructuredAlmostEqual` - unittest.TestCase documentation - ------------------------------- + * additional assertions: + - :py:meth:`~TestCase.assertStructuredAlmostEqual` + - :py:meth:`assertExpressionsEqual` + - :py:meth:`assertExpressionsStructurallyEqual` + + * updated assertions: + - :py:meth:`assertRaisesRegex` + + :py:class:`unittest.TestCase` documentation + ------------------------------------------- """ - __doc__ += _unittest.TestCase.__doc__ + # Note that the current unittest.TestCase documentation generates + # sphinx warnings. We will clean up that documentation to suppress + # the warnings. + __doc__ += ( + re.sub( + r'^( +)(\* +[^:]+:) *', + r'\n\1\2\n\1 ', + _unittest.TestCase.__doc__.rstrip(), + flags=re.M, + ) + + "\n\n" + ) # By default, we always want to spend the time to create the full # diff of the test reault and the baseline @@ -645,6 +662,11 @@ def assertExpressionsStructurallyEqual( ) +TestCase.assertStructuredAlmostEqual.__doc__ = re.sub( + 'exception :.*', '', assertStructuredAlmostEqual.__doc__, flags=re.S +) + + class BaselineTestDriver(object): """Generic driver for performing baseline tests in bulk From ff40d0dfb8968ac165c907dc1184350292da7b50 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 09:34:19 -0600 Subject: [PATCH 2489/3044] Suppress warning for (intentionally) unindexed RST file --- doc/OnlineDocs/code.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/OnlineDocs/code.rst b/doc/OnlineDocs/code.rst index 2b17bced79d..83cbcdd9989 100644 --- a/doc/OnlineDocs/code.rst +++ b/doc/OnlineDocs/code.rst @@ -1,3 +1,5 @@ +:orphan: + .. autosummary:: :toctree: api :caption: Library Reference From b6767f7415c59218b146f509ee15a8b04fb3a4a7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 09:35:01 -0600 Subject: [PATCH 2490/3044] Remove unused intersphinx link --- doc/OnlineDocs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 3e8241230cf..20b4264bd93 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -58,7 +58,6 @@ 'pandas': ('https://pandas.pydata.org/docs/', None), 'scikit-learn': ('https://scikit-learn.org/stable/', None), 'scipy': ('https://docs.scipy.org/doc/scipy/', None), - 'Sphinx': ('https://www.sphinx-doc.org/en/master/', None), } # -- General configuration ------------------------------------------------ From 823f5567401df9568071b66ef74c36877efabb94 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 09:35:17 -0600 Subject: [PATCH 2491/3044] Update copyright statement to match the rest of Pyomo --- doc/OnlineDocs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 20b4264bd93..84917c2cfaa 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -107,7 +107,7 @@ # General information about the project. project = u'Pyomo' -copyright = u'2008-2023, Sandia National Laboratories' +copyright = u'2008-2024, Sandia National Laboratories' author = u'Pyomo Developers' # The version info for the project you're documenting, acts as replacement for From 56095e914c9486875ed1fb9e0a9546f814e12208 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 09:35:40 -0600 Subject: [PATCH 2492/3044] Update Sphinx config to improve API rendering --- doc/OnlineDocs/conf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 84917c2cfaa..e026ea4effe 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -167,7 +167,7 @@ # further. For a list of options available for each theme, see the # documentation. # -# html_theme_options = {} +html_theme_options = {'navigation_depth': 6, 'titles_only': True} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -234,6 +234,9 @@ # autodoc_member_order = 'bysource' # autodoc_member_order = 'groupwise' +autosummary_generate = True +autosummary_ignore_module_all = True + # -- Check which conditional dependencies are available ------------------ # Used for skipping certain doctests from sphinx.ext.doctest import doctest From 11f0df41e7a5dc917ff0d4da4a290804be7a6684 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 10:00:54 -0600 Subject: [PATCH 2493/3044] Fixing typos --- pyomo/contrib/pynumero/sparse/block_vector.py | 2 +- pyomo/core/base/constraint.py | 2 +- pyomo/core/base/param.py | 2 +- pyomo/core/kernel/base.py | 2 +- pyomo/core/tests/unit/test_sets.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/pynumero/sparse/block_vector.py b/pyomo/contrib/pynumero/sparse/block_vector.py index 9e68dfce23e..71ca47962f0 100644 --- a/pyomo/contrib/pynumero/sparse/block_vector.py +++ b/pyomo/contrib/pynumero/sparse/block_vector.py @@ -8,7 +8,7 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -"""Implimentation of a general "block vector" +"""Implementation of a general "block vector" The `pyomo.contrib.pynumero.sparse.block_vector` module includes methods diff --git a/pyomo/core/base/constraint.py b/pyomo/core/base/constraint.py index 8e49ab9ef27..b9407329e07 100644 --- a/pyomo/core/base/constraint.py +++ b/pyomo/core/base/constraint.py @@ -140,7 +140,7 @@ class ConstraintData(ActiveComponentData): expr : ExpressionBase The Pyomo expression stored in this constraint. - component : Constriant + component : Constraint The Constraint object that owns this data. """ diff --git a/pyomo/core/base/param.py b/pyomo/core/base/param.py index f801b6a194a..af56508a3d2 100644 --- a/pyomo/core/base/param.py +++ b/pyomo/core/base/param.py @@ -157,7 +157,7 @@ def clear(self): # set_value is called without specifying an index, this call # involves a linear scan of the _data dict. def set_value(self, value, idx=NOTSET): - """Set the value of this ParamData object, performing unit convertion + """Set the value of this ParamData object, performing unit conversion and validation as necessary. """ diff --git a/pyomo/core/kernel/base.py b/pyomo/core/kernel/base.py index d599c76f6a1..0653868e109 100644 --- a/pyomo/core/kernel/base.py +++ b/pyomo/core/kernel/base.py @@ -156,7 +156,7 @@ def getname( Args: fully_qualified (bool): Generate a full name by - iterating through all anscestor containers. + iterating through all ancestor containers. Default is :const:`False`. convert (function): A function that converts a storage key into a string diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index 52c4523eaba..46d12172aed 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2979,7 +2979,7 @@ def test_initialize_and_clone_from_dict_keys(self): # # While deepcopying a model is generally not supported, this is # an easy way to ensure that this simple model is cleanly - # clonable. + # cloneable. ref = """1 Set Declarations INDEX : Size=1, Index=None, Ordered=Insertion Key : Dimen : Domain : Size : Members From 76c4250e936362ba718f35fa6cb5ea284a13222e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 10:02:16 -0600 Subject: [PATCH 2494/3044] Deprecate/Remove unneeded references to results data structures from __init__.py --- pyomo/opt/__init__.py | 12 +++++++++--- pyomo/opt/results/__init__.py | 9 +++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/pyomo/opt/__init__.py b/pyomo/opt/__init__.py index 629f63f92d4..77daa46db22 100644 --- a/pyomo/opt/__init__.py +++ b/pyomo/opt/__init__.py @@ -36,9 +36,6 @@ container, problem, solution, - ScalarData, - ScalarType, - default_print_options, ListContainer, MapContainer, UndefinedData, @@ -64,3 +61,12 @@ SolverManagerFactory, AsynchronousSolverManager, ) + +from pyomo.common.deprecation import relocated_module_attribute + +for _attr in ('ScalarData', 'ScalarType', 'default_print_options'): + relocated_module_attribute( + _attr, 'pyomo.opt.results.container.' + _attr, version='6.0' + ) +del _attr +del relocated_module_attribute diff --git a/pyomo/opt/results/__init__.py b/pyomo/opt/results/__init__.py index bf96d123ff4..524b9e0706a 100644 --- a/pyomo/opt/results/__init__.py +++ b/pyomo/opt/results/__init__.py @@ -26,3 +26,12 @@ from pyomo.opt.results.problem import ProblemSense from pyomo.opt.results.solution import SolutionStatus, Solution from pyomo.opt.results.results_ import SolverResults + +from pyomo.common.deprecation import relocated_module_attribute + +for _attr in ('ScalarData', 'ScalarType', 'default_print_options', 'strict'): + relocated_module_attribute( + _attr, 'pyomo.opt.results.container.' + _attr, version='6.8.1.dev0' + ) +del _attr +del relocated_module_attribute From 91023a1a0e1e355b4ff691470f9c3bbe1aa9d200 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 11:40:47 -0600 Subject: [PATCH 2495/3044] Expression.create_node_with_local_data: prevent creating unconstructed Expression objects --- pyomo/core/base/expression.py | 6 +++-- pyomo/core/tests/unit/test_expression.py | 30 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/expression.py b/pyomo/core/base/expression.py index a5120759236..64f250e9860 100644 --- a/pyomo/core/base/expression.py +++ b/pyomo/core/base/expression.py @@ -62,7 +62,7 @@ def __call__(self, exception=True): return arg return arg(exception=exception) - def create_node_with_local_data(self, values): + def create_node_with_local_data(self, values, classtype=None): """ Construct a simple expression after constructing the contained expression. @@ -70,7 +70,9 @@ def create_node_with_local_data(self, values): This class provides a consistent interface for constructing a node, which is used in tree visitor scripts. """ - obj = self.__class__() + if classtype is None: + classtype = self.parent_component()._ComponentDataClass + obj = classtype() obj._args_ = values return obj diff --git a/pyomo/core/tests/unit/test_expression.py b/pyomo/core/tests/unit/test_expression.py index eb16f7c6142..6050f34bc96 100644 --- a/pyomo/core/tests/unit/test_expression.py +++ b/pyomo/core/tests/unit/test_expression.py @@ -30,6 +30,7 @@ sum_product, ) from pyomo.core.base.expression import ExpressionData +from pyomo.core.base.objective import ObjectiveData from pyomo.core.expr.compare import compare_expressions, assertExpressionsEqual from pyomo.common.tee import capture_output @@ -290,6 +291,35 @@ def obj_rule(model): self.assertEqual(inst.obj.expr(), 3.0) self.assertEqual(id(inst.obj.expr.arg(1)), id(inst.ec)) + def test_create_node_with_local_data(self): + m = ConcreteModel() + m.x = Var() + + m.e = Expression(expr=m.x) + ee = m.e.create_node_with_local_data([5]) + self.assertIsNot(m.e, ee) + self.assertIs(type(ee), ExpressionData) + self.assertEqual(ee._args_, [5]) + + m.f = Expression([0], rule=lambda m, i: m.x) + ff = m.f[0].create_node_with_local_data([5]) + self.assertIsNot(m.f, ff) + self.assertIsNot(m.f[0], ff) + self.assertIs(type(ff), ExpressionData) + self.assertEqual(ff._args_, [5]) + + m.g = Objective(expr=m.x) + gg = m.g.create_node_with_local_data([5]) + self.assertIsNot(m.g, gg) + self.assertIs(type(gg), ObjectiveData) + self.assertEqual(gg._args_, [5]) + + m.h = Objective([0], rule=lambda m, i: m.x) + hh = m.h[0].create_node_with_local_data([5]) + self.assertIsNot(m.h, hh) + self.assertIsNot(m.h[0], hh) + self.assertIs(type(hh), ObjectiveData) + self.assertEqual(hh._args_, [5]) class TestExpression(unittest.TestCase): def setUp(self): From 4a5e593c986e8c1927a0d0b39ab364b59e0a0a72 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 12:21:06 -0600 Subject: [PATCH 2496/3044] MonomialTermExpression.creaate_node_with_local_data: resolve custom type error --- pyomo/core/expr/numeric_expr.py | 2 +- pyomo/core/tests/unit/test_numeric_expr.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pyomo/core/expr/numeric_expr.py b/pyomo/core/expr/numeric_expr.py index 21896c63219..0e67801ae8a 100644 --- a/pyomo/core/expr/numeric_expr.py +++ b/pyomo/core/expr/numeric_expr.py @@ -1094,7 +1094,7 @@ def create_node_with_local_data(self, args, classtype=None): # types, the simplest / fastest thing to do is just defer to # the operator dispatcher. return operator.mul(*args) - return self.__class__(args) + return classtype(args) class DivisionExpression(NumericExpression): diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index efb01e6d6ce..517e720fc10 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -4313,6 +4313,17 @@ def test_sin(self): total = counter.count - start self.assertEqual(total, 1) + def test_create_node_with_local_data(self): + e = self.m.p * self.m.a + self.assertIs(type(e), MonomialTermExpression) + + f = e.create_node_with_local_data([self.m.b, self.m.p]) + self.assertIs(type(f), MonomialTermExpression) + self.assertStructuredAlmostEqual(f._args_, [self.m.p, self.m.b]) + + g = e.create_node_with_local_data([self.m.b, self.m.p], ProductExpression) + self.assertIs(type(g), ProductExpression) + self.assertStructuredAlmostEqual(g._args_, [self.m.b, self.m.p]) # # Fixed - Expr has a fixed value From 897d2ef1f80193ecd8402c93a2828b32fcd095bf Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 12:54:37 -0600 Subject: [PATCH 2497/3044] Remove degeneracy from book example (does not change the book - only changes the use o a solver to test the book) --- examples/pyomobook/gdp-ch/gdp_uc.py | 5 ++++ examples/pyomobook/gdp-ch/pyomo.gdp_uc.txt | 30 +++++++++++----------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/examples/pyomobook/gdp-ch/gdp_uc.py b/examples/pyomobook/gdp-ch/gdp_uc.py index 6268bcce068..b87ee35e4e2 100644 --- a/examples/pyomobook/gdp-ch/gdp_uc.py +++ b/examples/pyomobook/gdp-ch/gdp_uc.py @@ -116,3 +116,8 @@ def obj(m): @model.Constraint(model.GENERATORS) def nontrivial(m, g): return sum(m.Power[g, t] for t in m.TIME) >= len(m.TIME) / 2 * m.MinPower[g] + +@model.ConstraintList() +def nondegenerate(m): + for i, g in enumerate(m.GENERATORS): + yield m.Power[g, i+1] == 0 diff --git a/examples/pyomobook/gdp-ch/pyomo.gdp_uc.txt b/examples/pyomobook/gdp-ch/pyomo.gdp_uc.txt index 477336d48ba..f0b5a5c4795 100644 --- a/examples/pyomobook/gdp-ch/pyomo.gdp_uc.txt +++ b/examples/pyomobook/gdp-ch/pyomo.gdp_uc.txt @@ -22,9 +22,9 @@ Problem: Lower bound: 45.0 Upper bound: 45.0 Number of objectives: 1 - Number of constraints: 58 + Number of constraints: 60 Number of variables: 24 - Number of nonzeros: 124 + Number of nonzeros: 126 Sense: minimize # ---------------------------------------------------------- # Solver Information @@ -34,8 +34,8 @@ Solver: Termination condition: optimal Statistics: Branch and bound: - Number of bounded subproblems: 15 - Number of created subproblems: 15 + Number of bounded subproblems: 9 + Number of created subproblems: 9 Error rc: 0 Time: 0.007754325866699219 # ---------------------------------------------------------- @@ -51,24 +51,24 @@ Solution: obj: Value: 45 Variable: - GenOff[g1,2].binary_indicator_var: + GenOff[g1,1].binary_indicator_var: Value: 1 - GenOff[g2,1].binary_indicator_var: + GenOff[g2,2].binary_indicator_var: Value: 1 - GenOn[g1,1].binary_indicator_var: + GenOn[g1,3].binary_indicator_var: Value: 1 - GenOn[g2,3].binary_indicator_var: + GenOn[g2,1].binary_indicator_var: Value: 1 - GenStartup[g1,3].binary_indicator_var: + GenStartup[g1,2].binary_indicator_var: Value: 1 - GenStartup[g2,2].binary_indicator_var: + GenStartup[g2,3].binary_indicator_var: Value: 1 - Power[g1,1]: - Value: 10 - Power[g1,3]: + Power[g1,2]: Value: 5 - Power[g2,2]: + Power[g1,3]: Value: 10 - Power[g2,3]: + Power[g2,1]: Value: 20 + Power[g2,3]: + Value: 10 Constraint: No values From 52d5667ada272ef843eeb786681de0902df6d8d2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 12:59:53 -0600 Subject: [PATCH 2498/3044] Apply black --- examples/pyomobook/gdp-ch/gdp_uc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/pyomobook/gdp-ch/gdp_uc.py b/examples/pyomobook/gdp-ch/gdp_uc.py index b87ee35e4e2..ff3c554c039 100644 --- a/examples/pyomobook/gdp-ch/gdp_uc.py +++ b/examples/pyomobook/gdp-ch/gdp_uc.py @@ -117,7 +117,8 @@ def obj(m): def nontrivial(m, g): return sum(m.Power[g, t] for t in m.TIME) >= len(m.TIME) / 2 * m.MinPower[g] + @model.ConstraintList() def nondegenerate(m): for i, g in enumerate(m.GENERATORS): - yield m.Power[g, i+1] == 0 + yield m.Power[g, i + 1] == 0 From 0776d04c7c916de1577ee61d89ad32add4986661 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 3 Oct 2024 13:54:02 -0600 Subject: [PATCH 2499/3044] Adding slight negative tolerance to post processing in FME test to see if that makes osx/3.12 happier? --- pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py index dc721488f74..c80eff9097b 100644 --- a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py +++ b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py @@ -562,7 +562,7 @@ def test_post_processing(self): fme = TransformationFactory('contrib.fourier_motzkin_elimination') fme.apply_to(m, vars_to_eliminate=disaggregatedVars, do_integer_arithmetic=True) # post-process - fme.post_process_fme_constraints(m, SolverFactory('glpk')) + fme.post_process_fme_constraints(m, SolverFactory('glpk'), tolerance=-1e-4) constraints = m._pyomo_contrib_fme_transformation.projected_constraints self.assertEqual(len(constraints), 11) From ad049f464c0bec9851bda19b2a492e4c6f543481 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 3 Oct 2024 14:19:00 -0600 Subject: [PATCH 2500/3044] Tightening tolerance a little --- pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py index c80eff9097b..961d34a68c7 100644 --- a/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py +++ b/pyomo/contrib/fme/tests/test_fourier_motzkin_elimination.py @@ -562,7 +562,7 @@ def test_post_processing(self): fme = TransformationFactory('contrib.fourier_motzkin_elimination') fme.apply_to(m, vars_to_eliminate=disaggregatedVars, do_integer_arithmetic=True) # post-process - fme.post_process_fme_constraints(m, SolverFactory('glpk'), tolerance=-1e-4) + fme.post_process_fme_constraints(m, SolverFactory('glpk'), tolerance=-1e-6) constraints = m._pyomo_contrib_fme_transformation.projected_constraints self.assertEqual(len(constraints), 11) From 2fa0bce8e1e2bae582b400c818364bb901130cf4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 3 Oct 2024 17:22:31 -0600 Subject: [PATCH 2501/3044] NFC: apply black --- pyomo/core/tests/unit/test_expression.py | 1 + pyomo/core/tests/unit/test_numeric_expr.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pyomo/core/tests/unit/test_expression.py b/pyomo/core/tests/unit/test_expression.py index 6050f34bc96..92cb245fa22 100644 --- a/pyomo/core/tests/unit/test_expression.py +++ b/pyomo/core/tests/unit/test_expression.py @@ -321,6 +321,7 @@ def test_create_node_with_local_data(self): self.assertIs(type(hh), ObjectiveData) self.assertEqual(hh._args_, [5]) + class TestExpression(unittest.TestCase): def setUp(self): TestExpression._save = expr_common.TO_STRING_VERBOSE diff --git a/pyomo/core/tests/unit/test_numeric_expr.py b/pyomo/core/tests/unit/test_numeric_expr.py index 517e720fc10..ca9fafd482d 100644 --- a/pyomo/core/tests/unit/test_numeric_expr.py +++ b/pyomo/core/tests/unit/test_numeric_expr.py @@ -4325,6 +4325,7 @@ def test_create_node_with_local_data(self): self.assertIs(type(g), ProductExpression) self.assertStructuredAlmostEqual(g._args_, [self.m.b, self.m.p]) + # # Fixed - Expr has a fixed value # Constant - Expr only contains constants and immutable parameters From cffc600667669c48a84d19d94db8b385296a1320 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 4 Oct 2024 13:02:54 -0600 Subject: [PATCH 2502/3044] Update EOL handling in ConfigDict to remove extra EOL, update tests --- pyomo/common/config.py | 4 ++-- pyomo/common/tests/test_config.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 5b59e437eb2..c686443d149 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -1510,7 +1510,7 @@ def _item_body(self, indent, obj): # space before the colon at the top level (which at lower levels # causes nested definition lists to NOT omit the colon), we will # generate non-standard ReST and omit the preceding space: - self.out.write(f'\n{indent}{obj.name()}: {typeinfo}\n\n') + self.out.write(f'\n{indent}{obj.name()}: {typeinfo}\n') self.wrapper.initial_indent = indent + ' ' * self.indent_spacing self.wrapper.subsequent_indent = indent + ' ' * self.indent_spacing vis = "" @@ -1527,7 +1527,7 @@ def _item_body(self, indent, obj): self.wrapper, ) if itemdoc: - self.out.write(itemdoc + '\n') + self.out.write('\n' + itemdoc + '\n') def _finalize(self): return inspect.cleandoc(self.out.getvalue()) diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index a47f5e0d8af..dac0606ce05 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -3120,16 +3120,19 @@ def fcn(self): Keyword Arguments ----------------- option_1: int, default=5 + The first configuration option solver_options: dict, optional solver_option_1: float, default=1 + [DEVELOPER option] The first solver configuration option solver_option_2: float, default=1 + The second solver configuration option With a very long line containing wrappable text in a long, silly @@ -3138,6 +3141,7 @@ def fcn(self): #) with two bullets solver_option_3: float, default=1 + The third solver configuration option This has a leading newline and a very long line containing @@ -3149,6 +3153,7 @@ def fcn(self): #) with two bullets option_2: int, default=5 + The second solver configuration option with a very long line containing wrappable text in a long, silly paragraph with little actual information.""" @@ -3159,11 +3164,13 @@ def fcn(self): Keyword Arguments ----------------- option_1: int, default=5 + The first configuration option solver_options: dict, optional solver_option_2: float, default=1 + The second solver configuration option With a very long line containing wrappable text in a long, silly @@ -3172,6 +3179,7 @@ def fcn(self): #) with two bullets solver_option_3: float, default=1 + The third solver configuration option This has a leading newline and a very long line containing @@ -3183,6 +3191,7 @@ def fcn(self): #) with two bullets option_2: int, default=5 + The second solver configuration option with a very long line containing wrappable text in a long, silly paragraph with little actual information.""" @@ -3192,11 +3201,13 @@ def fcn(self): Keyword Arguments ----------------- option_1: int, default=5 + The first configuration option solver_options: dict, optional solver_option_2: float, default=1 + The second solver configuration option With a very long line containing wrappable text in a long, silly paragraph with little actual information. @@ -3204,6 +3215,7 @@ def fcn(self): #) with two bullets solver_option_3: float, default=1 + The third solver configuration option This has a leading newline and a very long line containing wrappable text in a long, silly paragraph with little actual information. @@ -3213,6 +3225,7 @@ def fcn(self): #) with two bullets option_2: int, default=5 + The second solver configuration option with a very long line containing wrappable text in a long, silly paragraph with little actual information.""" with LoggingIntercept() as LOG: self.assertEqual(add_docstring_list("", ExampleClass.CONFIG), ref) From a08fa45b449eb8dfb137e0859b7790d5562cc6c2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 4 Oct 2024 13:03:22 -0600 Subject: [PATCH 2503/3044] Track changes in repr(FlagType) --- pyomo/common/tests/test_config.py | 4 ++-- pyomo/common/tests/test_flags.py | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index dac0606ce05..481d498af25 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -482,7 +482,7 @@ def __repr__(self): ), ) self.assertRegex( - c.get("val3").domain_name(), r"IsInstance\(int, .*\.TestClass\)" + c.get("val3").domain_name(), r"IsInstance\[int, TestClass\]" ) c.val3 = 2 self.assertEqual(c.val3, 2) @@ -499,7 +499,7 @@ def __repr__(self): None, IsInstance(int, TestClass, document_full_base_names=False) ), ) - self.assertEqual(c.get("val4").domain_name(), "IsInstance(int, TestClass)") + self.assertEqual(c.get("val4").domain_name(), "IsInstance[int, TestClass]") c.val4 = 2 self.assertEqual(c.val4, 2) exc_str = ( diff --git a/pyomo/common/tests/test_flags.py b/pyomo/common/tests/test_flags.py index 4bb7fcb7220..ec75544bb66 100644 --- a/pyomo/common/tests/test_flags.py +++ b/pyomo/common/tests/test_flags.py @@ -19,20 +19,19 @@ class TestModeling(unittest.TestCase): def test_NOTSET(self): + self.assertTrue(in_testing_environment()) + self.assertFalse(building_documentation()) + self.assertEqual(str(NOTSET), 'NOTSET') self.assertNotIn('sphinx', sys.modules) self.assertEqual(repr(NOTSET), 'pyomo.common.flags.NOTSET') self.assertIsNone(in_testing_environment.state) - self.assertTrue(in_testing_environment()) - self.assertFalse(building_documentation()) try: sys.modules['sphinx'] = sys.modules[__name__] - for i in sorted(sys.modules.items()): - print(i) self.assertTrue(in_testing_environment()) self.assertTrue(building_documentation()) - self.assertEqual(repr(NOTSET), 'pyomo.common.flags.NOTSET') + self.assertEqual(repr(NOTSET), 'NOTSET') in_testing_environment(False) self.assertFalse(in_testing_environment()) From 19cfd2df74541f343f2c8a13bbc4e5cb2d9e7920 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 4 Oct 2024 13:05:27 -0600 Subject: [PATCH 2504/3044] NFC: apply black --- pyomo/common/tests/test_config.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyomo/common/tests/test_config.py b/pyomo/common/tests/test_config.py index 481d498af25..faa15cffe5f 100644 --- a/pyomo/common/tests/test_config.py +++ b/pyomo/common/tests/test_config.py @@ -481,9 +481,7 @@ def __repr__(self): None, IsInstance(int, TestClass, document_full_base_names=True) ), ) - self.assertRegex( - c.get("val3").domain_name(), r"IsInstance\[int, TestClass\]" - ) + self.assertRegex(c.get("val3").domain_name(), r"IsInstance\[int, TestClass\]") c.val3 = 2 self.assertEqual(c.val3, 2) exc_str = ( From 47b0ff49ea9919616ec4ce64a68f39dd6922216f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 7 Oct 2024 14:59:41 -0600 Subject: [PATCH 2505/3044] Import relevant module for doctests of classes/functions --- doc/OnlineDocs/_templates/recursive-base.rst | 6 ++++++ doc/OnlineDocs/_templates/recursive-class.rst | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/doc/OnlineDocs/_templates/recursive-base.rst b/doc/OnlineDocs/_templates/recursive-base.rst index b45d3894277..6a4e827fbe2 100644 --- a/doc/OnlineDocs/_templates/recursive-base.rst +++ b/doc/OnlineDocs/_templates/recursive-base.rst @@ -2,6 +2,12 @@ ({{ objtype }} from :py:mod:`{{ module }}`) +.. testsetup:: * + + # import everything from the module containing this class so that + # doctests for the class docstrings see the correct environment + from {{ module }} import * + .. currentmodule:: {{ module }} .. auto{{ objtype }}:: {{ objname }} diff --git a/doc/OnlineDocs/_templates/recursive-class.rst b/doc/OnlineDocs/_templates/recursive-class.rst index 38b20cdac01..8efa3589f66 100644 --- a/doc/OnlineDocs/_templates/recursive-class.rst +++ b/doc/OnlineDocs/_templates/recursive-class.rst @@ -2,6 +2,12 @@ (class from :py:mod:`{{ module }}`) +.. testsetup:: * + + # import everything from the module containing this class so that + # doctests for the class docstrings see the correct environment + from {{ module }} import * + .. currentmodule:: {{ module }} .. autoclass:: {{ objname }} From 4b743217291bd0fcac4a387f4c409df01e15362b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 7 Oct 2024 15:01:29 -0600 Subject: [PATCH 2506/3044] Don't document inherited-members for ndarray derived classes --- doc/OnlineDocs/_templates/recursive-class.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/_templates/recursive-class.rst b/doc/OnlineDocs/_templates/recursive-class.rst index 8efa3589f66..dd08381f2af 100644 --- a/doc/OnlineDocs/_templates/recursive-class.rst +++ b/doc/OnlineDocs/_templates/recursive-class.rst @@ -10,10 +10,17 @@ .. currentmodule:: {{ module }} +{# Note that numpy.ndarray examples fail doctest; disable documentation + of inherited members for classes derived from ndarray #} + .. autoclass:: {{ objname }} :members: - :inherited-members: :show-inheritance: + {{ '' if (module + '.' + name) in ( + 'pyomo.contrib.pynumero.sparse.block_vector.BlockVector', + 'pyomo.contrib.pynumero.sparse.mpi_block_vector.MPIBlockVector', + 'pyomo.core.expr.ndarray.NumericNDArray', + ) else ':inherited-members:' }} {% block methods %} .. automethod:: __init__ From 5cce82c5571b7f73caacd2bb7a5338e6e70a4c19 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 7 Oct 2024 15:02:08 -0600 Subject: [PATCH 2507/3044] Exclude all 'tests' when building source documentation --- doc/OnlineDocs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index e026ea4effe..1cf7c6dd712 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -131,7 +131,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ['_build', '*.tests', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' From 18c0f82d2ac764936eb968c0d96c8cc1e620a16a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 7 Oct 2024 15:03:00 -0600 Subject: [PATCH 2508/3044] Allow interior_point ex1.py to be imported without executing --- pyomo/contrib/interior_point/examples/ex1.py | 42 +++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/interior_point/examples/ex1.py b/pyomo/contrib/interior_point/examples/ex1.py index f6d8f14ac0a..a3a64022c7e 100644 --- a/pyomo/contrib/interior_point/examples/ex1.py +++ b/pyomo/contrib/interior_point/examples/ex1.py @@ -15,25 +15,29 @@ from pyomo.contrib.interior_point.linalg.mumps_interface import MumpsInterface import logging +def solve_qcqp_example(): + logging.basicConfig(level=logging.INFO) + # Supposedly this sets the root logger's level to INFO. + # But when linear_solver.logger logs with debug, + # it gets propagated to a mysterious root logger with + # level NOTSET... -logging.basicConfig(level=logging.INFO) -# Supposedly this sets the root logger's level to INFO. -# But when linear_solver.logger logs with debug, -# it gets propagated to a mysterious root logger with -# level NOTSET... + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.y = pyo.Var() + m.obj = pyo.Objective(expr=m.x**2 + m.y**2) + m.c1 = pyo.Constraint(expr=m.y == pyo.exp(m.x)) + m.c2 = pyo.Constraint(expr=m.y >= (m.x - 1) ** 2) + interface = InteriorPointInterface(m) + linear_solver = MumpsInterface( + # log_filename='lin_sol.log', + icntl_options={11: 1} # Set error level to 1 (most detailed) + ) -m = pyo.ConcreteModel() -m.x = pyo.Var() -m.y = pyo.Var() -m.obj = pyo.Objective(expr=m.x**2 + m.y**2) -m.c1 = pyo.Constraint(expr=m.y == pyo.exp(m.x)) -m.c2 = pyo.Constraint(expr=m.y >= (m.x - 1) ** 2) -interface = InteriorPointInterface(m) -linear_solver = MumpsInterface( - # log_filename='lin_sol.log', - icntl_options={11: 1} # Set error level to 1 (most detailed) -) + ip_solver = InteriorPointSolver(linear_solver) + x, duals_eq, duals_ineq = ip_solver.solve(interface) + print(x, duals_eq, duals_ineq) -ip_solver = InteriorPointSolver(linear_solver) -x, duals_eq, duals_ineq = ip_solver.solve(interface) -print(x, duals_eq, duals_ineq) + +if __name__ == '__main__': + return solve_qcqp_example() From a385f42178a10f0de7fe06191324cc5f3338224e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 7 Oct 2024 15:03:46 -0600 Subject: [PATCH 2509/3044] Remove unused import --- pyomo/common/env.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/common/env.py b/pyomo/common/env.py index ee07cdc1e6a..fd399b6aec5 100644 --- a/pyomo/common/env.py +++ b/pyomo/common/env.py @@ -405,7 +405,6 @@ class CtypesEnviron(object): :hide: import os - from pyomo.common.env import TemporaryEnv orig_env_val = os.environ.get('TEMP_ENV_VAR', None) .. doctest:: From a4e39b1fbc29af4043c891bc4cbc59c2bf8e3abf Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 7 Oct 2024 15:04:03 -0600 Subject: [PATCH 2510/3044] Fix doc example typo --- pyomo/contrib/alternative_solutions/aos_utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/alternative_solutions/aos_utils.py b/pyomo/contrib/alternative_solutions/aos_utils.py index ce14014d266..23c31f3874a 100644 --- a/pyomo/contrib/alternative_solutions/aos_utils.py +++ b/pyomo/contrib/alternative_solutions/aos_utils.py @@ -37,8 +37,9 @@ def logcontext(level): Examples -------- >>> with logcontext(logging.INFO): - >>> logging.debug("This will not be printed") - >>> logging.info("This will be printed") + ... logging.debug("This will not be printed") + ... logging.info("This will be printed") + """ logger = logging.getLogger() current_level = logger.getEffectiveLevel() From 5acddbf6a2e60077c75ee52543a2c7b02aa47c41 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 12:15:48 -0600 Subject: [PATCH 2511/3044] Reverse the BlockVector/MPIBlickVector base class order This relieves the need to explicitly re-implement methods from BaseBlockVector (and will enable us to overwrite problematic docstrings from numpy) --- pyomo/contrib/pynumero/sparse/block_vector.py | 85 +------------------ .../pynumero/sparse/mpi_block_vector.py | 80 +---------------- 2 files changed, 2 insertions(+), 163 deletions(-) diff --git a/pyomo/contrib/pynumero/sparse/block_vector.py b/pyomo/contrib/pynumero/sparse/block_vector.py index b636dd74203..bcb5e786d08 100644 --- a/pyomo/contrib/pynumero/sparse/block_vector.py +++ b/pyomo/contrib/pynumero/sparse/block_vector.py @@ -38,7 +38,7 @@ def assert_block_structure(vec): raise NotFullyDefinedBlockVectorError(msg) -class BlockVector(np.ndarray, BaseBlockVector): +class BlockVector(BaseBlockVector, np.ndarray): """ Structured vector interface. This interface can be used to perform operations on vectors composed by vectors. For example, @@ -1592,86 +1592,3 @@ def toMPIBlockVector(self, rank_ownership, mpi_comm, assert_correct_owners=False mpi_bv.set_block(bid, self.get_block(bid)) return mpi_bv - - # the following methods are not supported by blockvector - - def argpartition(self, kth, axis=-1, kind='introselect', order=None): - BaseBlockVector.argpartition(self, kth, axis=axis, kind=kind, order=order) - - def argsort(self, axis=-1, kind='quicksort', order=None): - BaseBlockVector.argsort(self, axis=axis, kind=kind, order=order) - - def byteswap(self, inplace=False): - BaseBlockVector.byteswap(self, inplace=inplace) - - def choose(self, choices, out=None, mode='raise'): - BaseBlockVector.choose(self, choices, out=out, mode=mode) - - def diagonal(self, offset=0, axis1=0, axis2=1): - BaseBlockVector.diagonal(self, offset=offset, axis1=axis1, axis2=axis2) - - def dump(self, file): - BaseBlockVector.dump(self, file) - - def dumps(self): - BaseBlockVector.dumps(self) - - def getfield(self, dtype, offset=0): - BaseBlockVector.getfield(self, dtype, offset=offset) - - def item(self, *args): - BaseBlockVector.item(self, *args) - - def itemset(self, *args): - BaseBlockVector.itemset(self, *args) - - def newbyteorder(self, new_order='S'): - BaseBlockVector.newbyteorder(self, new_order=new_order) - - def put(self, indices, values, mode='raise'): - BaseBlockVector.put(self, indices, values, mode=mode) - - def partition(self, kth, axis=-1, kind='introselect', order=None): - BaseBlockVector.partition(self, kth, axis=axis, kind=kind, order=order) - - def repeat(self, repeats, axis=None): - BaseBlockVector.repeat(self, repeats, axis=axis) - - def reshape(self, shape, order='C'): - BaseBlockVector.reshape(self, shape, order=order) - - def resize(self, new_shape, refcheck=True): - BaseBlockVector.resize(self, new_shape, refcheck=refcheck) - - def searchsorted(self, v, side='left', sorter=None): - BaseBlockVector.searchsorted(self, v, side=side, sorter=sorter) - - def setfield(self, val, dtype, offset=0): - BaseBlockVector.setfield(self, val, dtype, offset=offset) - - def setflags(self, write=None, align=None, uic=None): - BaseBlockVector.setflags(self, write=write, align=align, uic=uic) - - def sort(self, axis=-1, kind='quicksort', order=None): - BaseBlockVector.sort(self, axis=axis, kind=kind, order=order) - - def squeeze(self, axis=None): - BaseBlockVector.squeeze(self, axis=axis) - - def swapaxes(self, axis1, axis2): - BaseBlockVector.swapaxes(self, axis1, axis2) - - def tobytes(self, order='C'): - BaseBlockVector.tobytes(self, order=order) - - def take(self, indices, axis=None, out=None, mode='raise'): - BaseBlockVector.take(self, indices, axis=axis, out=out, mode=mode) - - def trace(self, offset=0, axis1=0, axis2=1, dtype=None, out=None): - raise NotImplementedError('trace not implemented for BlockVector') - - def transpose(*axes): - BaseBlockVector.transpose(*axes) - - def tostring(order='C'): - BaseBlockVector.tostring(order=order) diff --git a/pyomo/contrib/pynumero/sparse/mpi_block_vector.py b/pyomo/contrib/pynumero/sparse/mpi_block_vector.py index 89cf136a5f7..f86d450a73e 100644 --- a/pyomo/contrib/pynumero/sparse/mpi_block_vector.py +++ b/pyomo/contrib/pynumero/sparse/mpi_block_vector.py @@ -24,7 +24,7 @@ def assert_block_structure(vec): raise NotFullyDefinedBlockVectorError(msg) -class MPIBlockVector(np.ndarray, BaseBlockVector): +class MPIBlockVector(BaseBlockVector, np.ndarray): """ Parallel structured vector interface. This interface can be used to perform parallel operations on vectors composed by vectors. The main @@ -1447,81 +1447,3 @@ def flatten(self, order='C'): def ravel(self, order='C'): raise RuntimeError('Operation not supported by MPIBlockVector') - - def argpartition(self, kth, axis=-1, kind='introselect', order=None): - BaseBlockVector.argpartition(self, kth, axis=axis, kind=kind, order=order) - - def argsort(self, axis=-1, kind='quicksort', order=None): - BaseBlockVector.argsort(self, axis=axis, kind=kind, order=order) - - def byteswap(self, inplace=False): - BaseBlockVector.byteswap(self, inplace=inplace) - - def choose(self, choices, out=None, mode='raise'): - BaseBlockVector.choose(self, choices, out=out, mode=mode) - - def diagonal(self, offset=0, axis1=0, axis2=1): - BaseBlockVector.diagonal(self, offset=offset, axis1=axis1, axis2=axis2) - - def dump(self, file): - BaseBlockVector.dump(self, file) - - def dumps(self): - BaseBlockVector.dumps(self) - - def getfield(self, dtype, offset=0): - BaseBlockVector.getfield(self, dtype, offset=offset) - - def item(self, *args): - BaseBlockVector.item(self, *args) - - def itemset(self, *args): - BaseBlockVector.itemset(self, *args) - - def newbyteorder(self, new_order='S'): - BaseBlockVector.newbyteorder(self, new_order=new_order) - - def put(self, indices, values, mode='raise'): - BaseBlockVector.put(self, indices, values, mode=mode) - - def partition(self, kth, axis=-1, kind='introselect', order=None): - BaseBlockVector.partition(self, kth, axis=axis, kind=kind, order=order) - - def repeat(self, repeats, axis=None): - BaseBlockVector.repeat(self, repeats, axis=axis) - - def reshape(self, shape, order='C'): - BaseBlockVector.reshape(self, shape, order=order) - - def resize(self, new_shape, refcheck=True): - BaseBlockVector.resize(self, new_shape, refcheck=refcheck) - - def searchsorted(self, v, side='left', sorter=None): - BaseBlockVector.searchsorted(self, v, side=side, sorter=sorter) - - def setfield(self, val, dtype, offset=0): - BaseBlockVector.setfield(self, val, dtype, offset=offset) - - def setflags(self, write=None, align=None, uic=None): - BaseBlockVector.setflags(self, write=write, align=align, uic=uic) - - def sort(self, axis=-1, kind='quicksort', order=None): - BaseBlockVector.sort(self, axis=axis, kind=kind, order=order) - - def squeeze(self, axis=None): - BaseBlockVector.squeeze(self, axis=axis) - - def swapaxes(self, axis1, axis2): - BaseBlockVector.swapaxes(self, axis1, axis2) - - def tobytes(self, order='C'): - BaseBlockVector.tobytes(self, order=order) - - def argmax(self, axis=None, out=None): - BaseBlockVector.argmax(self, axis=axis, out=out) - - def argmin(self, axis=None, out=None): - BaseBlockVector.argmax(self, axis=axis, out=out) - - def take(self, indices, axis=None, out=None, mode='raise'): - BaseBlockVector.take(self, indices, axis=axis, out=out, mode=mode) From 17291432060dd2a28b73b6e81dfa74b3d7bdf2b2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 12:32:35 -0600 Subject: [PATCH 2512/3044] Improve the error message from ImmutableConfigValue --- pyomo/common/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index c686443d149..eea4a78e6f7 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -2209,7 +2209,7 @@ def __new__(self, *args, **kwds): def set_value(self, value): if self._cast(value) != self._data: - raise RuntimeError(str(self) + ' is currently immutable') + raise RuntimeError(f"'{self.name(True)}' is currently immutable") super(ImmutableConfigValue, self).set_value(value) From 1e09883e8b2e19e7a89aff326fe80c17a80ebc0f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 12:59:27 -0600 Subject: [PATCH 2513/3044] Add support for setting the state of `building_documentation()` --- pyomo/common/flags.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pyomo/common/flags.py b/pyomo/common/flags.py index 3cfcd2d426c..21a15234c5d 100644 --- a/pyomo/common/flags.py +++ b/pyomo/common/flags.py @@ -78,15 +78,29 @@ def in_testing_environment(state=NOTSET): in_testing_environment.state = None -def building_documentation(): +def building_documentation(state=NOTSET): """True if we are building the Sphinx documentation We detect if we are building the documentation by looking if the ``sphnx`` or ``Sphinx`` modules are imported. + Parameters + ---------- + state : bool or None + If provided, sets the current state of the building environment + flag (Setting to None reverts to the normal interrogation of + ``sys.modules``) + Returns ------- bool """ + if state is not NOTSET: + building_documentation.state = state + if building_documentation.state is not None: + return bool(building_documentation.state) return 'sphinx' in sys.modules or 'Sphinx' in sys.modules + + +building_documentation.state = None From e832bf14c83d1d5be4284c4664dbab869a042963 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 13:00:22 -0600 Subject: [PATCH 2514/3044] Check state of `logger` module attribute before dereferencing This avoids exceptions if the interpreter shuts down with open tempfile contexts. --- pyomo/common/tempfiles.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/pyomo/common/tempfiles.py b/pyomo/common/tempfiles.py index b9dface71b2..52e20e580e8 100644 --- a/pyomo/common/tempfiles.py +++ b/pyomo/common/tempfiles.py @@ -100,24 +100,25 @@ def __del__(self): def shutdown(self, remove=True): if not self._context_stack: return - if any(ctx.tempfiles for ctx in self._context_stack): - logger.error( - "Temporary files created through TempfileManager " - "contexts have not been deleted (observed during " - "TempfileManager instance shutdown).\n" - "Undeleted entries:\n\t" - + "\n\t".join( - fname if isinstance(fname, str) else fname.decode() - for ctx in self._context_stack - for fd, fname in ctx.tempfiles + if logger is not None: + if any(ctx.tempfiles for ctx in self._context_stack): + logger.error( + "Temporary files created through TempfileManager " + "contexts have not been deleted (observed during " + "TempfileManager instance shutdown).\n" + "Undeleted entries:\n\t" + + "\n\t".join( + fname if isinstance(fname, str) else fname.decode() + for ctx in self._context_stack + for fd, fname in ctx.tempfiles + ) + ) + if self._context_stack: + logger.warning( + "TempfileManagerClass instance: un-popped tempfile " + "contexts still exist during TempfileManager instance " + "shutdown" ) - ) - if self._context_stack: - logger.warning( - "TempfileManagerClass instance: un-popped tempfile " - "contexts still exist during TempfileManager instance " - "shutdown" - ) self.clear_tempfiles(remove) # Delete the stack so that subsequent operations generate an # exception From 777f868f71c1fdbe81fcd3d812c6a02079023526 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 13:04:44 -0600 Subject: [PATCH 2515/3044] Resolve errors in docstrings --- pyomo/common/config.py | 46 ++++++++++++++++++++--- pyomo/common/dependencies.py | 12 ++++++ pyomo/common/env.py | 2 +- pyomo/common/flags.py | 8 ++-- pyomo/common/log.py | 16 ++++---- pyomo/contrib/fbbt/fbbt.py | 71 ++++++++++++++++++++---------------- pyomo/util/components.py | 25 ++++++++----- pyomo/util/subsystems.py | 47 ++++++++++++++---------- 8 files changed, 150 insertions(+), 77 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index eea4a78e6f7..7e5107560a7 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -2225,14 +2225,48 @@ class MarkImmutable(object): Examples -------- - >>> config = ConfigDict() - >>> config.declare('a', ConfigValue(default=1, domain=int)) - >>> config.declare('b', ConfigValue(default=1, domain=int)) - >>> locker = MarkImmutable(config.get('a'), config.get('b')) + .. testcode:: + + config = ConfigDict() + config.declare('a', ConfigValue(default=1, domain=int)) + config.declare('b', ConfigValue(default=1, domain=int)) + locker = MarkImmutable(config.get('a'), config.get('b')) + + Now, config.a and config.b cannot be changed: + + .. doctest:: + + >>> config.a = 5 + Traceback (most recent call last): + ... + RuntimeError: ConfigValue 'a' is currently immutable + >>> print(config.a) + 1 + + To make them mutable again, + + .. doctest:: + + >>> locker.release_lock() + >>> config.a = 5 + >>> print(config.a) + 5 + + Note that this can be used as a context manager as well: + + .. doctest:: - Now, config.a and config.b cannot be changed. To make them mutable again, + >>> with MarkImmutable(config.get('a'), config.get('b')): + ... config.a = 10 + Traceback (most recent call last): + ... + RuntimeError: ConfigValue 'a' is currently immutable + >>> print(config.a) + 5 + >>> config.a = 10 + >>> print(config.a) + 10 - >>> locker.release_lock() """ def __init__(self, *args): diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 0458bdeade6..2505a6bb298 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -227,6 +227,13 @@ def UnavailableClass(unavailable_module): As does attempting to access class attributes on the derived class: + .. testcode:: + :hide: + + # We suppress this exception when building the documentation + # from pyomo.common.flags import building_documentation + building_documentation(False) + .. doctest:: >>> MyPlugin.create_instance() @@ -237,6 +244,11 @@ def UnavailableClass(unavailable_module): dependency was not found (import raised ModuleNotFoundError: No module named 'bogus_unavailable_class') + .. testcode:: + :hide: + + building_documentation(None) + """ class UnavailableMeta(type): diff --git a/pyomo/common/env.py b/pyomo/common/env.py index fd399b6aec5..a6b94a48622 100644 --- a/pyomo/common/env.py +++ b/pyomo/common/env.py @@ -414,7 +414,7 @@ class CtypesEnviron(object): original value >>> with CtypesEnviron(TEMP_ENV_VAR='temporary value'): - ... print(os.envion['TEMP_ENV_VAR']) + ... print(os.environ['TEMP_ENV_VAR']) temporary value >>> print(os.environ['TEMP_ENV_VAR']) diff --git a/pyomo/common/flags.py b/pyomo/common/flags.py index 21a15234c5d..59e8f62a129 100644 --- a/pyomo/common/flags.py +++ b/pyomo/common/flags.py @@ -41,9 +41,11 @@ class NOTSET(object, metaclass=FlagType): Class to be used to indicate that an optional argument was not specified, if `None` may be ambiguous. Usage: - >>> def foo(value=NOTSET): - >>> if value is NOTSET: - >>> pass # no argument was provided to `value` + Examples + -------- + >>> def foo(value=NOTSET): + ... if value is NOTSET: + ... pass # no argument was provided to `value` """ diff --git a/pyomo/common/log.py b/pyomo/common/log.py index c4218e4fbd3..a66e5221ef0 100644 --- a/pyomo/common/log.py +++ b/pyomo/common/log.py @@ -289,13 +289,15 @@ class LoggingIntercept(object): the formatter to use when rendering the log messages. If not specified, uses `'%(message)s'` - Examples: - >>> import io, logging - >>> from pyomo.common.log import LoggingIntercept - >>> buf = io.StringIO() - >>> with LoggingIntercept(buf, 'pyomo.core', logging.WARNING): - ... logging.getLogger('pyomo.core').warning('a simple message') - >>> buf.getvalue() + Examples + -------- + >>> import io, logging + >>> from pyomo.common.log import LoggingIntercept + >>> buf = io.StringIO() + >>> with LoggingIntercept(buf, 'pyomo.core', logging.WARNING): + ... logging.getLogger('pyomo.core').warning('a simple message') + >>> buf.getvalue() + 'a simple message\n' """ diff --git a/pyomo/contrib/fbbt/fbbt.py b/pyomo/contrib/fbbt/fbbt.py index 4bd0e4552a1..9a2e4958d9f 100644 --- a/pyomo/contrib/fbbt/fbbt.py +++ b/pyomo/contrib/fbbt/fbbt.py @@ -43,36 +43,45 @@ logger = logging.getLogger(__name__) -""" -The purpose of this file is to perform feasibility based bounds -tightening. This is a very basic implementation, but it is done -directly with pyomo expressions. The only functions that are meant to -be used by users are fbbt and compute_bounds_on_expr. The first set of -functions in this file (those with names starting with -_prop_bnds_leaf_to_root) are used for propagating bounds from the -variables to each node in the expression tree (all the way to the -root node). The second set of functions (those with names starting -with _prop_bnds_root_to_leaf) are used to propagate bounds from the -constraint back to the variables. For example, consider the constraint -x*y + z == 1 with -1 <= x <= 1 and -2 <= y <= 2. When propagating -bounds from the variables to the root (the root is x*y + z), we find -that -2 <= x*y <= 2, and that -inf <= x*y + z <= inf. However, -from the constraint, we know that 1 <= x*y + z <= 1, so we may -propagate bounds back to the variables. Since we know that -1 <= x*y + z <= 1 and -2 <= x*y <= 2, then we must have -1 <= z <= 3. -However, bounds cannot be improved on x*y, so bounds cannot be -improved on either x or y. - ->>> import pyomo.environ as pe ->>> m = pe.ConcreteModel() ->>> m.x = pe.Var(bounds=(-1,1)) ->>> m.y = pe.Var(bounds=(-2,2)) ->>> m.z = pe.Var() ->>> from pyomo.contrib.fbbt.fbbt import fbbt ->>> m.c = pe.Constraint(expr=m.x*m.y + m.z == 1) ->>> fbbt(m) ->>> print(m.z.lb, m.z.ub) --1.0 3.0 +__doc__ = """ +Feasibility-Based Bounds Tightening + +The purpose of this module is to perform feasibility-based bounds +tightening. This is a very basic implementation, but it is done +directly with pyomo expressions. The only functions that are meant to +be used by users are :func:`fbbt` and :func:`compute_bounds_on_expr`. +The first set of +functions in this file (those with names starting with +``_prop_bnds_leaf_to_root``) are used for propagating bounds from the +variables to each node in the expression tree (all the way to the +root node). The second set of functions (those with names starting +with ``_prop_bnds_root_to_leaf``) are used to propagate bounds from the +constraint back to the variables. + +For example, consider the constraint x*y + z == 1 with -1 <= x <= 1 and +-2 <= y <= 2. When propagating bounds from the variables to the root +(the root is x*y + z), we find that -2 <= x*y <= 2, and that -inf <= x*y ++ z <= inf. However, from the constraint, we know that 1 <= x*y + z <= +1, so we may propagate bounds back to the variables. Since we know that +1 <= x*y + z <= 1 and -2 <= x*y <= 2, then we must have -1 <= z <= 3. +However, bounds cannot be improved on x*y, so bounds cannot be improved +on either x or y. + +.. testcode:: + + import pyomo.environ as pe + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1,1)) + m.y = pe.Var(bounds=(-2,2)) + m.z = pe.Var() + from pyomo.contrib.fbbt.fbbt import fbbt + m.c = pe.Constraint(expr=m.x*m.y + m.z == 1) + fbbt(m) + print(f"z bounds = {m.z.bounds}") + +.. testoutput:: + + z bounds = (-1, 3) """ @@ -1242,7 +1251,7 @@ def visiting_potential_leaf(self, node): ub = min(math.ceil(ub), math.floor(ub + self.integer_tol)) """ We have to make sure we do not make lb lower than the original lower bound - and make sure we do not make ub larger than the original upper bound. This is what + and make sure we do not make ub larger than the original upper bound. This is what _check_and_reset_bounds is for. """ lb, ub = _check_and_reset_bounds(node, lb, ub) diff --git a/pyomo/util/components.py b/pyomo/util/components.py index 2f1d85a4934..ffd68aad296 100644 --- a/pyomo/util/components.py +++ b/pyomo/util/components.py @@ -15,8 +15,7 @@ def rename_components(model, component_list, prefix): - """ - Rename components in component_list using the prefix AND + """Rename components in component_list using the prefix AND unique_component_name Parameters @@ -30,8 +29,13 @@ def rename_components(model, component_list, prefix): Examples -------- - >>> c_list = list(model.component_objects(ctype=Var, descend_into=True)) - >>> rename_components(model, component_list=c_list, prefix='special_') + >>> model = pyo.ConcreteModel() + >>> model.x = pyo.Var() + >>> model.y = pyo.Var() + >>> c_list = list(model.component_objects(ctype=pyo.Var, descend_into=True)) + >>> new = rename_components(model, component_list=c_list, prefix='special_') + >>> str(new) + "ComponentMap({'special_x (key=...)': 'x', 'special_y (key=...)': 'y'})" Returns ------- @@ -40,7 +44,8 @@ def rename_components(model, component_list, prefix): ToDo ---- - - need to add a check to see if someone accidentally passes a generator since this can lead to an infinite loop + - need to add a check to see if someone accidentally passes a + generator since this can lead to an infinite loop """ # Need to collect any Reference first so that we can record the old mapping of data objects before renaming @@ -99,18 +104,20 @@ def rename_components(model, component_list, prefix): def iter_component(obj): - """ - Yield "child" objects from a component that is defined with either the `base` or `kernel` APIs. - If the component is not indexed, it returns itself. + """Yield "child" objects from a component that is defined with either + the `base` or `kernel` APIs. If the component is not indexed, it + returns itself. Parameters ---------- obj : ComponentType - eg. `TupleContainer`, `ListContainer`, `DictContainer`, `IndexedComponent`, or `Component` + eg. `TupleContainer`, `ListContainer`, `DictContainer`, + `IndexedComponent`, or `Component` Returns ------- Iterator[ComponentType] : Iterator of the component data objects. + """ try: # catches `IndexedComponent`, and kernel's `_dict` diff --git a/pyomo/util/subsystems.py b/pyomo/util/subsystems.py index 00c3b85ce47..12f28e2b1b7 100644 --- a/pyomo/util/subsystems.py +++ b/pyomo/util/subsystems.py @@ -286,26 +286,33 @@ class ParamSweeper(TemporarySubsystemManager): calculation, over a range of values for which the calculation is valid. For example: - >>> model = ... # Make model somehow - >>> solver = ... # Make solver somehow - >>> input_vars = [model.v1] - >>> n_scen = 2 - >>> input_values = ComponentMap([(model.v1, [1.1, 2.1])]) - >>> output_values = ComponentMap([(model.v2, [1.2, 2.2])]) - >>> with ParamSweeper( - ... n_scen, - ... input_values, - ... output_values, - ... to_fix=input_vars, - ... ) as param_sweeper: - >>> for inputs, outputs in param_sweeper: - >>> solver.solve(model) - >>> # inputs and outputs contain the correct values for this - >>> # instance of the model - >>> for var, val in outputs.items(): - >>> # Test that model.v2 was calculated properly. - >>> # First that it equals 1.2, then that it equals 2.2 - >>> assert var.value == val + .. testcode:: + :skipif: not glpk_available + + model = pyo.ConcreteModel() + model.v1 = pyo.Var() + model.v2 = pyo.Var() + model.c = pyo.Constraint(expr=model.v2 - model.v1 >= 0.1) + model.o = pyo.Objective(expr=model.v1 + model.v2) + solver = pyo.SolverFactory('glpk') + input_vars = [model.v1] + n_scen = 2 + input_values = pyo.ComponentMap([(model.v1, [1.1, 2.1])]) + output_values = pyo.ComponentMap([(model.v2, [1.2, 2.2])]) + with ParamSweeper( + n_scen, + input_values, + output_values, + to_fix=input_vars, + ) as param_sweeper: + for inputs, outputs in param_sweeper: + solver.solve(model) + # inputs and outputs contain the correct values for this + # instance of the model + for var, val in outputs.items(): + # Test that model.v2 was calculated properly. + # First that it equals 1.2, then that it equals 2.2 + assert var.value == val, f"{var.value} != {val}" """ From efcaac7ff4cb78c66ecf3e6ffe8049079e346e99 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 13:11:12 -0600 Subject: [PATCH 2516/3044] Use try-finally pattern in the context manager (from decorator documentation) --- pyomo/contrib/gdpopt/util.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/gdpopt/util.py b/pyomo/contrib/gdpopt/util.py index babe0245d57..03e0a6de163 100644 --- a/pyomo/contrib/gdpopt/util.py +++ b/pyomo/contrib/gdpopt/util.py @@ -499,15 +499,16 @@ def lower_logger_level_to(logger, level=None, tee=False): sh.setLevel(level) level_changed = True - yield - - if tee: - logger.handlers.clear() - for h in handlers: - logger.addHandler(h) - logger.propagate = True - if level_changed: - logger.setLevel(old_logger_level) + try: + yield + finally: + if tee: + logger.handlers.clear() + for h in handlers: + logger.addHandler(h) + logger.propagate = True + if level_changed: + logger.setLevel(old_logger_level) def _add_bigm_constraint_to_transformed_model(m, constraint, block): From 8b38663508c007566a59ad42e3ea7c59d85ec718 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 8 Oct 2024 13:11:28 -0600 Subject: [PATCH 2517/3044] Add Python 3.13 --- .github/workflows/test_branches.yml | 4 ++-- .github/workflows/test_pr_and_main.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index c1029ff3d7b..7718aaffa55 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -83,7 +83,7 @@ jobs: include: - os: ubuntu-latest - python: '3.12' + python: '3.13' TARGET: linux PYENV: pip @@ -116,7 +116,7 @@ jobs: PACKAGES: openmpi mpi4py - os: ubuntu-latest - python: '3.10' + python: '3.12' other: /cython setup_options: --with-cython skip_doctest: 1 diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 33aacaa9e35..87432ed95b6 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -68,7 +68,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python: [ 3.8, 3.9, '3.10', '3.11', '3.12' ] + python: [ 3.8, 3.9, '3.10', '3.11', '3.12', '3.13' ] other: [""] category: [""] @@ -110,7 +110,7 @@ jobs: PACKAGES: openmpi mpi4py - os: ubuntu-latest - python: '3.10' + python: '3.12' other: /cython setup_options: --with-cython skip_doctest: 1 From f827af6656728ab8c86fac937766ff4e270b3607 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 13:12:22 -0600 Subject: [PATCH 2518/3044] Emit log messages to Pyomo logger when building documentation This resolves doctest failures --- pyomo/common/log.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyomo/common/log.py b/pyomo/common/log.py index a66e5221ef0..6bfb34a98cf 100644 --- a/pyomo/common/log.py +++ b/pyomo/common/log.py @@ -28,7 +28,7 @@ from pyomo.version.info import releaselevel from pyomo.common.deprecation import deprecated from pyomo.common.fileutils import PYOMO_ROOT_DIR -from pyomo.common.flags import in_testing_environment +from pyomo.common.flags import in_testing_environment, building_documentation from pyomo.common.formatting import wrap_reStructuredText _indentation_re = re.compile(r'\s*') @@ -234,7 +234,12 @@ def __init__(self): self.logger = logging.getLogger() def filter(self, record): - return not self.logger.handlers + # We will not emit messages using the default Pyomo log handler + # if someone has registered a global handler. However, we will + # ignore this if we are building documentation + # (sphinx.ext.doctest adds a handler, but we want to ignore that + # handler when we are testing our documentation!) + return not self.logger.handlers or building_documentation() # This mocks up the historical Pyomo logging system, which uses a From 2777b4892d33d83446df7846d8f82be756f1a7ae Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 13:14:25 -0600 Subject: [PATCH 2519/3044] Update sphinx configuration --- doc/OnlineDocs/conf.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 1cf7c6dd712..d007293b79f 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -277,7 +277,7 @@ def check_output(self, want, got, optionflags): pint_available = attempt_import('pint', defer_import=False)[1] from pyomo.contrib.parmest.parmest import parmest_available -import pyomo.environ as _pe # (trigger all plugin registrations) +import pyomo.environ as pyo # (register plugins, make environ available to tests) import pyomo.opt as _opt # Not using SolverFactory to check solver availability because @@ -304,6 +304,11 @@ def check_output(self, want, got, optionflags): ma27_available = False mumps_available = False +# Mark that we are testing code (in this case, testing the documentation) from pyomo.common.flags import in_testing_environment in_testing_environment(True) + +# Prevent any Pyomo logs from propagating up to the doctest logger +import logging +logging.getLogger('pyomo').propagate = False ''' From 6c3150f4439f85e6f430f3d4e901ee04b71640f9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 13:15:48 -0600 Subject: [PATCH 2520/3044] Clean up Reference TOC appearance --- doc/OnlineDocs/reference/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/OnlineDocs/reference/index.rst b/doc/OnlineDocs/reference/index.rst index 0453bd07559..830c34f8366 100644 --- a/doc/OnlineDocs/reference/index.rst +++ b/doc/OnlineDocs/reference/index.rst @@ -6,6 +6,10 @@ Reference Guides topical/index Library Reference <../api/pyomo> + +.. toctree:: + :maxdepth: 1 + future ../errors ../related_packages From 7fb6690cb4d617701f606ae93ffff255613214f2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 13:16:16 -0600 Subject: [PATCH 2521/3044] Fix docstring error --- pyomo/common/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/common/log.py b/pyomo/common/log.py index 6bfb34a98cf..da1d57fbdbb 100644 --- a/pyomo/common/log.py +++ b/pyomo/common/log.py @@ -272,7 +272,7 @@ def __init__(self, base='', stream=None, level=logging.NOTSET, verbosity=None): class LoggingIntercept(object): - """Context manager for intercepting messages sent to a log stream + r"""Context manager for intercepting messages sent to a log stream This class is designed to enable easy testing of log messages. From 19e18ac19d63e2ce224a1fc32e586d749dc2beb3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 13:16:34 -0600 Subject: [PATCH 2522/3044] Simplify logic for erstoring log handlers --- pyomo/common/log.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/common/log.py b/pyomo/common/log.py index da1d57fbdbb..6810c1ee123 100644 --- a/pyomo/common/log.py +++ b/pyomo/common/log.py @@ -340,8 +340,8 @@ def __exit__(self, et, ev, tb): self.handler = None logger.setLevel(self._save[0]) logger.propagate = self._save[1] - for h in self._save[2]: - logger.handlers.append(h) + assert not logger.handlers + logger.handlers.extend(self._save[2]) class LogStream(io.TextIOBase): From 87e6b2164e8aab639593d9cff6e47a1b29479dd8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 13:24:44 -0600 Subject: [PATCH 2523/3044] Remove unused sphinx extensions --- doc/OnlineDocs/conf.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index d007293b79f..bb2c3425184 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -83,8 +83,6 @@ 'sphinx.ext.todo', 'sphinx_copybutton', 'enum_tools.autoenum', - #'sphinx.ext.autosectionlabel', - #'sphinx.ext.githubpages', ] viewcode_follow_imported_members = True From 253633382fb7f7a4e716e043f82e38c7c7247fe9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 13:35:17 -0600 Subject: [PATCH 2524/3044] Fixing doc typos --- doc/OnlineDocs/howto/abstract_models/BuildAction.rst | 2 +- doc/OnlineDocs/howto/abstract_models/data/native.rst | 7 ++++--- pyomo/common/flags.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/OnlineDocs/howto/abstract_models/BuildAction.rst b/doc/OnlineDocs/howto/abstract_models/BuildAction.rst index 195c84ab4b4..c87b56b92c0 100644 --- a/doc/OnlineDocs/howto/abstract_models/BuildAction.rst +++ b/doc/OnlineDocs/howto/abstract_models/BuildAction.rst @@ -54,7 +54,7 @@ The full model is: .. literalinclude:: /src/scripting/Isinglebuild.py :language: python -for this model, the same data file can be used as for Isinglecomm.py in +For this model, the same data file can be used as for Isinglecomm.py in :ref:`Isinglecomm.py` such as the toy data file: .. literalinclude:: /src/scripting/Isinglecomm.dat diff --git a/doc/OnlineDocs/howto/abstract_models/data/native.rst b/doc/OnlineDocs/howto/abstract_models/data/native.rst index 6ec4545560c..f1a25183795 100644 --- a/doc/OnlineDocs/howto/abstract_models/data/native.rst +++ b/doc/OnlineDocs/howto/abstract_models/data/native.rst @@ -4,7 +4,8 @@ Using Standard Data Types Defining Constant Values ------------------------ -In many cases, Pyomo models can be constructed without :class:`~pyomo.environ.Set` and :class:`~pyomo.environ.Param` data components. Native Python data types +In many cases, Pyomo models can be constructed without :class:`Set` and +:class:`~Param` data components. Native Python data types class can be simply used to define constant values in Pyomo expressions. Consequently, Python sets, lists and dictionaries can be used to construct Pyomo models, as well as a wide range of other Python classes. @@ -63,8 +64,8 @@ dictionary values are iterable data: Parameter Components ^^^^^^^^^^^^^^^^^^^^ -When a parameter is a single value, then a :class:`~pyomo.environ.Param` component can be simply initialized with a -value: +When a parameter is a single value, then a :class:`~pyomo.environ.Param` +component can be simply initialized with a value: .. literalinclude:: /src/dataportal/param_initialization_decl1.spy :language: python diff --git a/pyomo/common/flags.py b/pyomo/common/flags.py index 59e8f62a129..d7c47f3e7c5 100644 --- a/pyomo/common/flags.py +++ b/pyomo/common/flags.py @@ -84,7 +84,7 @@ def building_documentation(state=NOTSET): """True if we are building the Sphinx documentation We detect if we are building the documentation by looking if the - ``sphnx`` or ``Sphinx`` modules are imported. + ``sphinx`` or ``Sphinx`` modules are imported. Parameters ---------- From 03e14c44119eedf92b41dce9a2750f4d0adee04a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 14:17:36 -0600 Subject: [PATCH 2525/3044] Remove use of relative imports --- pyomo/contrib/preprocessing/plugins/__init__.py | 2 +- pyomo/core/beta/__init__.py | 2 +- pyomo/core/kernel/__init__.py | 2 +- pyomo/core/kernel/piecewise_library/__init__.py | 2 +- pyomo/core/plugins/__init__.py | 2 +- pyomo/core/plugins/transform/__init__.py | 2 +- pyomo/dae/plugins/__init__.py | 2 +- pyomo/dataportal/__init__.py | 2 +- pyomo/duality/__init__.py | 2 +- pyomo/gdp/plugins/__init__.py | 2 +- pyomo/mpec/plugins/__init__.py | 2 +- pyomo/neos/plugins/__init__.py | 2 +- pyomo/network/plugins/__init__.py | 2 +- pyomo/opt/parallel/__init__.py | 2 +- pyomo/opt/plugins/__init__.py | 2 +- pyomo/repn/beta/__init__.py | 2 +- pyomo/repn/plugins/__init__.py | 2 +- pyomo/scripting/__init__.py | 2 +- pyomo/scripting/plugins/__init__.py | 2 +- pyomo/solvers/plugins/__init__.py | 2 +- pyomo/solvers/plugins/converter/__init__.py | 2 +- pyomo/solvers/plugins/solvers/__init__.py | 2 +- pyomo/solvers/tests/models/__init__.py | 2 +- 23 files changed, 23 insertions(+), 23 deletions(-) diff --git a/pyomo/contrib/preprocessing/plugins/__init__.py b/pyomo/contrib/preprocessing/plugins/__init__.py index ba8cafc32ee..8d66b57c3df 100644 --- a/pyomo/contrib/preprocessing/plugins/__init__.py +++ b/pyomo/contrib/preprocessing/plugins/__init__.py @@ -11,7 +11,7 @@ def load(): - from . import ( + from pyomo.contrib.preprocessing.plugins import ( deactivate_trivial_constraints, detect_fixed_vars, init_vars, diff --git a/pyomo/core/beta/__init__.py b/pyomo/core/beta/__init__.py index 4409ca9ab01..883e3f8448c 100644 --- a/pyomo/core/beta/__init__.py +++ b/pyomo/core/beta/__init__.py @@ -9,4 +9,4 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from . import dict_objects, list_objects +from pyomo.core.beta import dict_objects, list_objects diff --git a/pyomo/core/kernel/__init__.py b/pyomo/core/kernel/__init__.py index d9446e687b1..cf04d6061e7 100644 --- a/pyomo/core/kernel/__init__.py +++ b/pyomo/core/kernel/__init__.py @@ -59,7 +59,7 @@ from pyomo.core.expr.calculus.derivatives import differentiate from pyomo.core.expr.taylor_series import taylor_series_expansion -from . import ( +from pyomo.core.kernel import ( base, homogeneous_container, heterogeneous_container, diff --git a/pyomo/core/kernel/piecewise_library/__init__.py b/pyomo/core/kernel/piecewise_library/__init__.py index 523741d528a..605eaffba59 100644 --- a/pyomo/core/kernel/piecewise_library/__init__.py +++ b/pyomo/core/kernel/piecewise_library/__init__.py @@ -9,4 +9,4 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from . import util, transforms, transforms_nd +from pyomo.core.kernel.piecewise_library import util, transforms, transforms_nd diff --git a/pyomo/core/plugins/__init__.py b/pyomo/core/plugins/__init__.py index d9c377da0e2..c01711f780f 100644 --- a/pyomo/core/plugins/__init__.py +++ b/pyomo/core/plugins/__init__.py @@ -11,4 +11,4 @@ def load(): - from . import transform + from pyomo.core.plugins import transform diff --git a/pyomo/core/plugins/transform/__init__.py b/pyomo/core/plugins/transform/__init__.py index 59943f2cfcc..fdbd71bdc6c 100644 --- a/pyomo/core/plugins/transform/__init__.py +++ b/pyomo/core/plugins/transform/__init__.py @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from . import ( +from pyomo.core.plugins.transform import ( relax_integrality, # eliminate_fixed_vars, # standard_form, diff --git a/pyomo/dae/plugins/__init__.py b/pyomo/dae/plugins/__init__.py index 79aa73b0e4b..4eaff9f1fd7 100644 --- a/pyomo/dae/plugins/__init__.py +++ b/pyomo/dae/plugins/__init__.py @@ -11,4 +11,4 @@ def load(): - from . import colloc, finitedifference + from pyomo.dae.plugins import colloc, finitedifference diff --git a/pyomo/dataportal/__init__.py b/pyomo/dataportal/__init__.py index e21fbcc905f..ac5de0fe541 100644 --- a/pyomo/dataportal/__init__.py +++ b/pyomo/dataportal/__init__.py @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from . import parse_datacmds +from pyomo.dataportal import parse_datacmds from pyomo.dataportal.TableData import TableData from pyomo.dataportal.DataPortal import DataPortal from pyomo.dataportal.factory import DataManagerFactory, UnknownDataManager diff --git a/pyomo/duality/__init__.py b/pyomo/duality/__init__.py index 3046e45d429..92d32367b0d 100644 --- a/pyomo/duality/__init__.py +++ b/pyomo/duality/__init__.py @@ -9,4 +9,4 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from . import collect +from pyomo.duality import collect diff --git a/pyomo/gdp/plugins/__init__.py b/pyomo/gdp/plugins/__init__.py index fc7aaa6012b..efb0a634d0a 100644 --- a/pyomo/gdp/plugins/__init__.py +++ b/pyomo/gdp/plugins/__init__.py @@ -11,7 +11,7 @@ def load(): - from . import ( + from pyomo.gdp.plugins import ( bigm, hull, bilinear, diff --git a/pyomo/mpec/plugins/__init__.py b/pyomo/mpec/plugins/__init__.py index 56ac546fb42..0a93d8c81a9 100644 --- a/pyomo/mpec/plugins/__init__.py +++ b/pyomo/mpec/plugins/__init__.py @@ -11,4 +11,4 @@ def load(): - from . import mpec1, mpec2, mpec3, mpec4, solver1, solver2, pathampl + from pyomo.mpec.plugins import mpec1, mpec2, mpec3, mpec4, solver1, solver2, pathampl diff --git a/pyomo/neos/plugins/__init__.py b/pyomo/neos/plugins/__init__.py index ba6c47ca683..76428b40bec 100644 --- a/pyomo/neos/plugins/__init__.py +++ b/pyomo/neos/plugins/__init__.py @@ -11,4 +11,4 @@ def load(): - from . import NEOS, kestrel_plugin + from pyomo.neos.plugins import NEOS, kestrel_plugin diff --git a/pyomo/network/plugins/__init__.py b/pyomo/network/plugins/__init__.py index ac83de1b41d..387c0639c3f 100644 --- a/pyomo/network/plugins/__init__.py +++ b/pyomo/network/plugins/__init__.py @@ -11,4 +11,4 @@ def load(): - from . import expand_arcs + from pyomo.network.plugins import expand_arcs diff --git a/pyomo/opt/parallel/__init__.py b/pyomo/opt/parallel/__init__.py index 8d4a5f8d91d..daa0d2461ec 100644 --- a/pyomo/opt/parallel/__init__.py +++ b/pyomo/opt/parallel/__init__.py @@ -15,4 +15,4 @@ SolverManagerFactory, AsynchronousSolverManager, ) -from . import manager, local +from pyomo.opt.parallel import manager, local diff --git a/pyomo/opt/plugins/__init__.py b/pyomo/opt/plugins/__init__.py index 514764e5c9d..30331d2938f 100644 --- a/pyomo/opt/plugins/__init__.py +++ b/pyomo/opt/plugins/__init__.py @@ -11,4 +11,4 @@ def load(): - from . import driver, res, sol + from pyomo.opt.plugins import driver, res, sol diff --git a/pyomo/repn/beta/__init__.py b/pyomo/repn/beta/__init__.py index a87ad717321..04311bdd314 100644 --- a/pyomo/repn/beta/__init__.py +++ b/pyomo/repn/beta/__init__.py @@ -9,4 +9,4 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from . import matrix +from pyomo.repn.beta import matrix diff --git a/pyomo/repn/plugins/__init__.py b/pyomo/repn/plugins/__init__.py index 453e500b672..1d6550f0cf8 100644 --- a/pyomo/repn/plugins/__init__.py +++ b/pyomo/repn/plugins/__init__.py @@ -11,7 +11,7 @@ def load(): - from . import ( + from pyomo.repn.plugins import ( cpxlp, ampl, baron_writer, diff --git a/pyomo/scripting/__init__.py b/pyomo/scripting/__init__.py index ccdc4c8a9fe..ee60988e1fb 100644 --- a/pyomo/scripting/__init__.py +++ b/pyomo/scripting/__init__.py @@ -9,4 +9,4 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from . import pyomo_command, util +from pyomo.scripting import pyomo_command, util diff --git a/pyomo/scripting/plugins/__init__.py b/pyomo/scripting/plugins/__init__.py index e0f91023522..f1fc43688d0 100644 --- a/pyomo/scripting/plugins/__init__.py +++ b/pyomo/scripting/plugins/__init__.py @@ -11,4 +11,4 @@ def load(): - from . import convert, solve, download, build_ext, extras + from pyomo.scripting.plugins import convert, solve, download, build_ext, extras diff --git a/pyomo/solvers/plugins/__init__.py b/pyomo/solvers/plugins/__init__.py index 9e20d582327..76d3c5b66d3 100644 --- a/pyomo/solvers/plugins/__init__.py +++ b/pyomo/solvers/plugins/__init__.py @@ -11,4 +11,4 @@ def load(): - from . import converter, solvers + from pyomo.solvers.plugins import converter, solvers diff --git a/pyomo/solvers/plugins/converter/__init__.py b/pyomo/solvers/plugins/converter/__init__.py index 053f5fc92f4..51dcfdb1b1a 100644 --- a/pyomo/solvers/plugins/converter/__init__.py +++ b/pyomo/solvers/plugins/converter/__init__.py @@ -9,4 +9,4 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from . import ampl, glpsol, model +from pyomo.solvers.plugins.converter import ampl, glpsol, model diff --git a/pyomo/solvers/plugins/solvers/__init__.py b/pyomo/solvers/plugins/solvers/__init__.py index 79b047755da..61f92180abc 100644 --- a/pyomo/solvers/plugins/solvers/__init__.py +++ b/pyomo/solvers/plugins/solvers/__init__.py @@ -10,7 +10,7 @@ # ___________________________________________________________________________ # TODO: Disabled until we can confirm application to Pyomo models -from . import ( +from pyomo.solvers.plugins.solvers import ( CBCplugin, GLPK, CPLEX, diff --git a/pyomo/solvers/tests/models/__init__.py b/pyomo/solvers/tests/models/__init__.py index ea7a35ff067..f67883e6718 100644 --- a/pyomo/solvers/tests/models/__init__.py +++ b/pyomo/solvers/tests/models/__init__.py @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from . import ( +from pyomo.solvers.tests.models import ( base, LP_block, LP_compiled, From 2c87f430c1a20a4cda046bf9cec54e6d0ae51f3f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 14:18:15 -0600 Subject: [PATCH 2526/3044] NFC: apply black --- pyomo/contrib/interior_point/examples/ex1.py | 1 + pyomo/contrib/preprocessing/plugins/__init__.py | 2 +- pyomo/mpec/plugins/__init__.py | 10 +++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/interior_point/examples/ex1.py b/pyomo/contrib/interior_point/examples/ex1.py index a3a64022c7e..8795e5245e4 100644 --- a/pyomo/contrib/interior_point/examples/ex1.py +++ b/pyomo/contrib/interior_point/examples/ex1.py @@ -15,6 +15,7 @@ from pyomo.contrib.interior_point.linalg.mumps_interface import MumpsInterface import logging + def solve_qcqp_example(): logging.basicConfig(level=logging.INFO) # Supposedly this sets the root logger's level to INFO. diff --git a/pyomo/contrib/preprocessing/plugins/__init__.py b/pyomo/contrib/preprocessing/plugins/__init__.py index 8d66b57c3df..d562e703f08 100644 --- a/pyomo/contrib/preprocessing/plugins/__init__.py +++ b/pyomo/contrib/preprocessing/plugins/__init__.py @@ -11,7 +11,7 @@ def load(): - from pyomo.contrib.preprocessing.plugins import ( + from pyomo.contrib.preprocessing.plugins import ( deactivate_trivial_constraints, detect_fixed_vars, init_vars, diff --git a/pyomo/mpec/plugins/__init__.py b/pyomo/mpec/plugins/__init__.py index 0a93d8c81a9..8557676e60c 100644 --- a/pyomo/mpec/plugins/__init__.py +++ b/pyomo/mpec/plugins/__init__.py @@ -11,4 +11,12 @@ def load(): - from pyomo.mpec.plugins import mpec1, mpec2, mpec3, mpec4, solver1, solver2, pathampl + from pyomo.mpec.plugins import ( + mpec1, + mpec2, + mpec3, + mpec4, + solver1, + solver2, + pathampl, + ) From d787a787db3179db06faa27b3ade8b62bd559271 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 8 Oct 2024 14:21:38 -0600 Subject: [PATCH 2527/3044] Whoops - accidental weirdness --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 7718aaffa55..f2b04bdac17 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -71,7 +71,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - python: ['3.12'] + python: ['3.13'] other: [""] category: [""] From f309eed1122528946f0fe7e29f74bbcdcafc1fc9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 14:49:18 -0600 Subject: [PATCH 2528/3044] Standardizing references --- doc/OnlineDocs/explanation/analysis/parmest/driver.rst | 2 +- doc/OnlineDocs/explanation/analysis/parmest/examples.rst | 2 +- doc/OnlineDocs/explanation/analysis/parmest/index.rst | 4 ++-- doc/OnlineDocs/explanation/modeling/gdp/index.rst | 2 +- .../getting_started/pyomo_overview/math_modeling.rst | 2 +- doc/OnlineDocs/howto/abstract_models/data/datfiles.rst | 2 +- doc/OnlineDocs/reference/bibliography.rst | 8 ++++---- pyomo/core/kernel/piecewise_library/transforms.py | 6 +----- pyomo/core/kernel/piecewise_library/transforms_nd.py | 6 +----- 9 files changed, 13 insertions(+), 21 deletions(-) diff --git a/doc/OnlineDocs/explanation/analysis/parmest/driver.rst b/doc/OnlineDocs/explanation/analysis/parmest/driver.rst index 866e50205bb..b3f212008ca 100644 --- a/doc/OnlineDocs/explanation/analysis/parmest/driver.rst +++ b/doc/OnlineDocs/explanation/analysis/parmest/driver.rst @@ -5,7 +5,7 @@ Parameter Estimation Parameter Estimation using parmest requires a Pyomo model, experimental data which defines multiple scenarios, and parameters -(thetas) to estimate. parmest uses Pyomo [PyomoBookII]_ and (optionally) +(thetas) to estimate. parmest uses Pyomo [PyomoBookIII]_ and (optionally) mpi-sppy [KMM+23]_ to solve a two-stage stochastic programming problem, where the experimental data is used to create a scenario tree. The objective function needs to be diff --git a/doc/OnlineDocs/explanation/analysis/parmest/examples.rst b/doc/OnlineDocs/explanation/analysis/parmest/examples.rst index 5c6f9096832..275e2177503 100644 --- a/doc/OnlineDocs/explanation/analysis/parmest/examples.rst +++ b/doc/OnlineDocs/explanation/analysis/parmest/examples.rst @@ -5,7 +5,7 @@ Examples Examples can be found in `pyomo/contrib/parmest/examples` and include: -* Reactor design example [PyomoBookII]_ +* Reactor design example [PyomoBookIII]_ * Semibatch example [AM00]_ * Rooney Biegler example [RB01]_ diff --git a/doc/OnlineDocs/explanation/analysis/parmest/index.rst b/doc/OnlineDocs/explanation/analysis/parmest/index.rst index 4700535815f..71aad8a0131 100644 --- a/doc/OnlineDocs/explanation/analysis/parmest/index.rst +++ b/doc/OnlineDocs/explanation/analysis/parmest/index.rst @@ -2,13 +2,13 @@ Parameter Estimation with ``parmest`` ===================================== ``parmest`` is a Python package built on the Pyomo optimization modeling -language ([PyomoJournal]_, [PyomoBookII]_) to support parameter estimation using experimental data along with +language ([Pyomo-paper]_, [PyomoBookIII]_) to support parameter estimation using experimental data along with confidence regions and subsequent creation of scenarios for stochastic programming. Citation for parmest ^^^^^^^^^^^^^^^^^^^^ -If you use parmest, please cite [ParmestPaper]_ +If you use parmest, please cite [Parmest-paper]_ Index of parmest documentation ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/OnlineDocs/explanation/modeling/gdp/index.rst b/doc/OnlineDocs/explanation/modeling/gdp/index.rst index b0aa7224d7a..770be256009 100644 --- a/doc/OnlineDocs/explanation/modeling/gdp/index.rst +++ b/doc/OnlineDocs/explanation/modeling/gdp/index.rst @@ -9,7 +9,7 @@ Generalized Disjunctive Programming :align: right :class: no-scaled-link -The Pyomo.GDP modeling extension [PyomoGDP-pse-paper]_ +The Pyomo.GDP modeling extension [PyomoGDP-proceedings]_ [PyomoGDP-paper]_ provides support for Generalized Disjunctive Programming (GDP) [RG94]_, an extension of Disjunctive Programming [Bal85]_ from the operations research community to include nonlinear diff --git a/doc/OnlineDocs/getting_started/pyomo_overview/math_modeling.rst b/doc/OnlineDocs/getting_started/pyomo_overview/math_modeling.rst index b1b837e7138..89a6e3af08b 100644 --- a/doc/OnlineDocs/getting_started/pyomo_overview/math_modeling.rst +++ b/doc/OnlineDocs/getting_started/pyomo_overview/math_modeling.rst @@ -72,7 +72,7 @@ solvers to analyze a model introduces additional complexities. Pyomo is an AML that extends Python to include objects for mathematical -modeling. [PyomoBookI]_, [PyomoBookII]_, [PyomoBookIII]_, and [PyomoJournal]_ +modeling. [PyomoBookI]_, [PyomoBookII]_, [PyomoBookIII]_, and [Pyomo-paper]_ compare Pyomo with other AMLs. Although many good AMLs have been developed for optimization models, the following are motivating factors for the development of Pyomo: diff --git a/doc/OnlineDocs/howto/abstract_models/data/datfiles.rst b/doc/OnlineDocs/howto/abstract_models/data/datfiles.rst index 1007f03fbcc..c0ce901628b 100644 --- a/doc/OnlineDocs/howto/abstract_models/data/datfiles.rst +++ b/doc/OnlineDocs/howto/abstract_models/data/datfiles.rst @@ -6,7 +6,7 @@ Data Command Files .. note:: The discussion and presentation below are adapted from Chapter 6 of - the "Pyomo Book" [PyomoBookII]_. The discussion of the + the second edition of the "Pyomo Book" [PyomoBookII]_. The discussion of the :class:`~pyomo.environ.DataPortal` class uses these same examples to illustrate how data can be loaded into Pyomo models within Python scripts (see the diff --git a/doc/OnlineDocs/reference/bibliography.rst b/doc/OnlineDocs/reference/bibliography.rst index 3d580ca9e2e..e9e622e20a6 100644 --- a/doc/OnlineDocs/reference/bibliography.rst +++ b/doc/OnlineDocs/reference/bibliography.rst @@ -5,7 +5,7 @@ Publications These publications describe various Pyomo capabilitites or subpackages: -.. [PyomoJournal] William E. Hart, Jean-Paul Watson, David L. Woodruff. +.. [Pyomo-paper] William E. Hart, Jean-Paul Watson, David L. Woodruff. "Pyomo: modeling and solving mathematical programs in Python," Mathematical Programming Computation, 3(3), August 2011 @@ -26,13 +26,13 @@ These publications describe various Pyomo capabilitites or subpackages: Vol. 67. Springer, 2021. doi: `10.1007/978-3-030-68928-5 `_ -.. [PyomoDAE] Bethany Nicholson, John D. Siirola, Jean-Paul Watson, +.. [PyomoDAE-paper] Bethany Nicholson, John D. Siirola, Jean-Paul Watson, Victor M. Zavala, and Lorenz T. Biegler. "pyomo.dae: a modeling and automatic discretization framework for optimization with differential and algebraic equations." Mathematical Programming Computation 10(2) 187-223. 2018. -.. [ParmestPaper] Katherine A. Klise, Bethany L. Nicholson, Andrea +.. [Parmest-paper] Katherine A. Klise, Bethany L. Nicholson, Andrea Staid, David L.Woodruff. "Parmest: Parameter Estimation Via Pyomo." *Computer Aided Chemical Engineering*, 47: 41-46. 2019. @@ -43,7 +43,7 @@ These publications describe various Pyomo capabilitites or subpackages: Engineering* pp. 1-36. 2021. DOI `10.1007/s11081-021-09601-7 `_ -.. [PyomoGDP-pse-paper] Qi Chen, Emma S. Johnson, John D. Siirola, and +.. [PyomoGDP-proceedings] Qi Chen, Emma S. Johnson, John D. Siirola, and Ignacio E. Grossmann. "Pyomo.GDP: Disjunctive Models in Python." In M. R. Eden, M. G. Ierapetritou, and G. P. Towler (Eds.), *Proceedings of the 13th International Symposium on Process Systems diff --git a/pyomo/core/kernel/piecewise_library/transforms.py b/pyomo/core/kernel/piecewise_library/transforms.py index eb65a0922a7..1443560025b 100644 --- a/pyomo/core/kernel/piecewise_library/transforms.py +++ b/pyomo/core/kernel/piecewise_library/transforms.py @@ -12,11 +12,7 @@ """ This module contains transformations for representing a single-variate piecewise linear function using a -mixed-integer problem formulation. Reference:: - - Mixed-Integer Models for Non-separable Piecewise Linear - Optimization: Unifying framework and Extensions (Vielma, - Nemhauser 2008) +mixed-integer problem formulation (see [VAN10]_). """ diff --git a/pyomo/core/kernel/piecewise_library/transforms_nd.py b/pyomo/core/kernel/piecewise_library/transforms_nd.py index 93676f0d886..b409f6dcddb 100644 --- a/pyomo/core/kernel/piecewise_library/transforms_nd.py +++ b/pyomo/core/kernel/piecewise_library/transforms_nd.py @@ -12,11 +12,7 @@ """ This module contains transformations for representing a multi-variate piecewise linear function using a -mixed-integer problem formulation. Reference:: - - Mixed-Integer Models for Non-separable Piecewise Linear - Optimization: Unifying framework and Extensions (Vielma, - Nemhauser 2008) +mixed-integer problem formulation (see [VAN10]_). """ From d91ab376e7df64b51f12ce59e6b84579dfdd09e4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 14:52:33 -0600 Subject: [PATCH 2529/3044] Documenting reference standard --- doc/OnlineDocs/reference/bibliography.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/doc/OnlineDocs/reference/bibliography.rst b/doc/OnlineDocs/reference/bibliography.rst index e9e622e20a6..c1e6a698dfd 100644 --- a/doc/OnlineDocs/reference/bibliography.rst +++ b/doc/OnlineDocs/reference/bibliography.rst @@ -2,6 +2,9 @@ Publications ============ +.. + Note to developers: For these references, we will use the package + name followed by a description of the publication type. These publications describe various Pyomo capabilitites or subpackages: @@ -55,6 +58,24 @@ These publications describe various Pyomo capabilitites or subpackages: Bibliography ============ +.. + Note to developers: We are using BiBTeX's `alpha` format for naming + bibliographic references: + + - single Author references use the 1st 3 characters (CamelCase) from + the last name plus the two digit publication year (e.g., [Aut00]) + + - 2- and 3-author references use the 1st character (capitalized) + from each last name plus the two digit publication year (e.g., [HWW11]) + + - 4+ author references use the 1st character (capitalized) from the + first 3 authors last names, plus a "+", plus the two digit + publication year (e.g., [BHH+21]) + + Reference collisions are resolved by adding a lower case character + (beginning with 'a', ordered in the same order that the references + appear in this Bibliography list) to *all* colliding references. + .. [AIMMS] http://www.aimms.com/ .. [AM00] O. Abel, W. Marquardt, "Scenario-integrated modeling and From bfad2dd7844bdd53c3296a788b3f9a92ff24d5ce Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 14:59:25 -0600 Subject: [PATCH 2530/3044] Updating reference --- doc/OnlineDocs/explanation/modeling/dae.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/OnlineDocs/explanation/modeling/dae.rst b/doc/OnlineDocs/explanation/modeling/dae.rst index ffe01c84914..8661fcf4af7 100644 --- a/doc/OnlineDocs/explanation/modeling/dae.rst +++ b/doc/OnlineDocs/explanation/modeling/dae.rst @@ -5,7 +5,8 @@ Dynamic Optimization with pyomo.DAE :scale: 35% :align: right -The pyomo.DAE modeling extension [PyomoDAE]_ allows users to incorporate systems of +The pyomo.DAE modeling extension [PyomoDAE-paper]_ allows users to +incorporate systems of differential algebraic equations (DAE)s in a Pyomo model. The modeling components in this extension are able to represent ordinary or partial differential equations. The differential equations do not have to be From d1df0ef1c27fb37dbd97b136b60b40fa64f0b85c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 15:11:52 -0600 Subject: [PATCH 2531/3044] remove references to the global logger --- pyomo/common/config.py | 2 +- pyomo/contrib/alternative_solutions/lp_enum.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 7e5107560a7..98fffdcfc6c 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -1216,7 +1216,7 @@ def __init__(self, obj): self._name = obj.name(True) def __call__(self, arg): - logging.error( + logger.error( """%s '%s' was pickled with an unpicklable domain. The domain was stripped and lost during the pickle process. Setting new values on the restored object cannot be mapped into the correct diff --git a/pyomo/contrib/alternative_solutions/lp_enum.py b/pyomo/contrib/alternative_solutions/lp_enum.py index 7cb7b5eaabe..3021887b24c 100644 --- a/pyomo/contrib/alternative_solutions/lp_enum.py +++ b/pyomo/contrib/alternative_solutions/lp_enum.py @@ -319,9 +319,9 @@ def enumerate_linear_solutions( ) break if logger.isEnabledFor(logging.DEBUG): - logging.debug("") - logging.debug("=" * 80) - logging.debug("") + logger.debug("") + logger.debug("=" * 80) + logger.debug("") model.del_component("aos_block") From ee1e3d546f9cf10cae5c567191803163a3613cd7 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 8 Oct 2024 16:35:18 -0600 Subject: [PATCH 2532/3044] Fix 'tests' exclusion pattern --- doc/OnlineDocs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index bb2c3425184..4c658c1e5eb 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -129,7 +129,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', '*.tests', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ['_build', '**/tests', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' From 7afbc41f99f1091214b70ee1d4703e810f962ee3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 9 Oct 2024 07:54:58 -0600 Subject: [PATCH 2533/3044] Explicitly set the default numpy print precision --- doc/OnlineDocs/conf.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 4c658c1e5eb..2e03d03ac0f 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -271,6 +271,7 @@ def check_output(self, want, got, optionflags): attempt_import, numpy_available, scipy_available, pandas_available, yaml_available, networkx_available, matplotlib_available, pympler_available, dill_available, + numpy as np, ) pint_available = attempt_import('pint', defer_import=False)[1] from pyomo.contrib.parmest.parmest import parmest_available @@ -291,6 +292,11 @@ def check_output(self, want, got, optionflags): baron = _opt.SolverFactory('baron') +if numpy_available: + # Recent changes on GHA seem to have dropped the default precision + # from 8 to 4; restore the default. + np.set_printoptions(precision=8) + if numpy_available and scipy_available: import pyomo.contrib.pynumero.asl as _asl asl_available = _asl.AmplInterface.available() From a7da07475a9fe322280d1775413cd4ddff6e6df2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 9 Oct 2024 16:26:36 -0600 Subject: [PATCH 2534/3044] Update doctest test file exclusion --- doc/OnlineDocs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 2e03d03ac0f..b96e3f0e6f4 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -129,7 +129,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', '**/tests', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ['_build', '**/tests/**', '**.tests.**', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' From c38b48b1c52554458af52b5e6f611ce390842fdd Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 09:27:30 -0600 Subject: [PATCH 2535/3044] Update tests filter for Sphinx>=8 --- doc/OnlineDocs/_templates/recursive-module.rst | 4 +++- doc/OnlineDocs/conf.py | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/_templates/recursive-module.rst b/doc/OnlineDocs/_templates/recursive-module.rst index df8b6b01807..5013ec1687c 100644 --- a/doc/OnlineDocs/_templates/recursive-module.rst +++ b/doc/OnlineDocs/_templates/recursive-module.rst @@ -69,7 +69,9 @@ Library Reference :template: recursive-module.rst :recursive: {% for item in modules %} -{% if '.test' not in item and '.example' not in item %} +{# Need item != tests for Sphinx >= 8.0; !endswith(.tests) for < 8.0 #} +{% if item != 'tests' and not item.endswith('.tests') + and item != 'examples' and not item.endswith('.examples') %} {{ item }} {% endif %} {%- endfor %} diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index b96e3f0e6f4..5c356dc2792 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -129,7 +129,21 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', '**/tests/**', '**.tests.**', 'Thumbs.db', '.DS_Store'] +# Notes: +# - _build : this is the Sphinx build (output) dir +# +# - api/*.tests.* : this matches autosummary RST files generated for +# test modules. Note that the _tempaltes/recursive-modules.rst +# should prevent these file from being generated, so this is not +# strictly necessary, but including it makes Sphinx throw warnings if +# the filter in the template ever "breaks" +# +# - **/tests/** : this matches source files in any tests directory +# [JDS: I *believe* this is necessary, but am not 100% certain] +# +# - 'Thumbs.db', '.DS_Store' : these have been included from the +# beginning. Unclear if they are still necessary +exclude_patterns = ['_build', 'api/*.tests.*', '**/tests/**', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' From 38a4b3ef44e29a56b98222a640998f1ddae3c61a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 09:28:22 -0600 Subject: [PATCH 2536/3044] Minor (NFC) reorder of doctest_global_setup --- doc/OnlineDocs/conf.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 5c356dc2792..87da88a05dd 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -281,6 +281,9 @@ def check_output(self, want, got, optionflags): platform.python_implementation() ) +# (register plugins, make environ available to tests) +import pyomo.environ as pyo + from pyomo.common.dependencies import ( attempt_import, numpy_available, scipy_available, pandas_available, yaml_available, networkx_available, matplotlib_available, @@ -290,12 +293,10 @@ def check_output(self, want, got, optionflags): pint_available = attempt_import('pint', defer_import=False)[1] from pyomo.contrib.parmest.parmest import parmest_available -import pyomo.environ as pyo # (register plugins, make environ available to tests) -import pyomo.opt as _opt - # Not using SolverFactory to check solver availability because # as of June 2020 there is no way to suppress warnings when # solvers are not available +import pyomo.opt as _opt ipopt_available = bool(_opt.check_available_solvers('ipopt')) sipopt_available = bool(_opt.check_available_solvers('ipopt_sens')) k_aug_available = bool(_opt.check_available_solvers('k_aug')) From 97e23a150896cbce90ab2b0f3322b7e736480a8b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 09:29:02 -0600 Subject: [PATCH 2537/3044] Make TempfileContext more robust when deleted during Python shutdown --- pyomo/common/tempfiles.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/pyomo/common/tempfiles.py b/pyomo/common/tempfiles.py index 52e20e580e8..bd49bf21777 100644 --- a/pyomo/common/tempfiles.py +++ b/pyomo/common/tempfiles.py @@ -253,6 +253,13 @@ def __init__(self, manager): self.manager = weakref.ref(manager) self.tempfiles = [] self.tempdir = None + # Create a local reference from the TempfileContext to the os + # and shutil modules so that this object is deleted before the + # os and shutil modules are deallocated (since + # TempfileContext.__del__ can call methods in those modules + # through TempfileContext.release()). + self.os = os + self.shutil = shutil def __del__(self): self.release() @@ -411,11 +418,11 @@ def release(self, remove=True): remove: bool If ``True``, delete all managed files / directories """ - if remove: + if remove and self.tempfiles: for fd, name in reversed(self.tempfiles): if fd is not None: try: - os.close(fd) + self.os.close(fd) except OSError: pass self._remove_filesystem_object(name) @@ -444,11 +451,11 @@ def _resolve_tempdir(self, dir=None): return None def _remove_filesystem_object(self, name): - if not os.path.exists(name): + if not self.os.path.exists(name): return - if os.path.isfile(name) or os.path.islink(name): + if self.os.path.isfile(name) or self.os.path.islink(name): try: - os.remove(name) + self.os.remove(name) except WindowsError: # Sometimes Windows doesn't release the # file lock immediately when the process @@ -456,7 +463,7 @@ def _remove_filesystem_object(self, name): # second and try again. try: time.sleep(1) - os.remove(name) + self.os.remove(name) except WindowsError: if deletion_errors_are_fatal: raise @@ -466,8 +473,8 @@ def _remove_filesystem_object(self, name): logger = logging.getLogger(__name__) logger.warning("Unable to delete temporary file %s" % (name,)) return - assert os.path.isdir(name) - shutil.rmtree(name, ignore_errors=not deletion_errors_are_fatal) + assert self.os.path.isdir(name) + self.shutil.rmtree(name, ignore_errors=not deletion_errors_are_fatal) # The global Pyomo TempfileManager instance From 532115c09e07fcd3609dfd90eeae8a4c14211ade Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 09:30:03 -0600 Subject: [PATCH 2538/3044] Resolve Sphinx errors; update comment to match current practice --- pyomo/contrib/example/__init__.py | 15 ++++++++------- pyomo/contrib/example/plugins/__init__.py | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/example/__init__.py b/pyomo/contrib/example/__init__.py index c70b50e84de..fc9ee68bca3 100644 --- a/pyomo/contrib/example/__init__.py +++ b/pyomo/contrib/example/__init__.py @@ -10,18 +10,19 @@ # ___________________________________________________________________________ # -# import symbols and sub-packages +# Import "public" symbols and sub-packages. # from pyomo.contrib.example.foo import * -import pyomo.contrib.example.bar +from pyomo.contrib.example import bar # -# import the plugins directory +# Register plugins from this sub-package. # -# The pyomo.environ package normally calls the load() function in -# the pyomo.*.plugins subdirectories. However, pyomo.contrib packages -# are not loaded by pyomo.environ, so we need to call this function -# when we import the rest of this package. +# The pyomo.environ package normally calls the load() function in a +# hard-coded list of pyomo.*.plugins and pyomo.contrib.*.plugins +# modules. However, This example is not included in that list, so we +# will load (and register) the plugins when this module (or any +# submodule) is imported. # from pyomo.contrib.example.plugins import load diff --git a/pyomo/contrib/example/plugins/__init__.py b/pyomo/contrib/example/plugins/__init__.py index 179098bc18e..8846e7c1650 100644 --- a/pyomo/contrib/example/plugins/__init__.py +++ b/pyomo/contrib/example/plugins/__init__.py @@ -14,4 +14,4 @@ def load(): - import pyomo.contrib.example.plugins.ex_plugin + from pyomo.contrib.example.plugins import ex_plugin From ad32818d04dc8a4a94f6028e94c551c749745411 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 09:30:50 -0600 Subject: [PATCH 2539/3044] Edit MIS test to not run on module import --- pyomo/contrib/iis/tests/trivial_mis.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/iis/tests/trivial_mis.py b/pyomo/contrib/iis/tests/trivial_mis.py index 4cf0dd7a357..7797a3bb654 100644 --- a/pyomo/contrib/iis/tests/trivial_mis.py +++ b/pyomo/contrib/iis/tests/trivial_mis.py @@ -8,17 +8,22 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import pyomo.environ as pyo -m = pyo.ConcreteModel("Trivial Quad") -m.x = pyo.Var([1, 2], bounds=(0, 1)) -m.y = pyo.Var(bounds=(0, 1)) -m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) -m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) +import pyomo.common.unittest as unittest +import pyomo.environ as pyo from pyomo.contrib.iis.mis import compute_infeasibility_explanation -# Note: this particular little problem is quadratic -# As of 18Feb2024 DLW is not sure the explanation code works with solvers other than ipopt -ipopt = pyo.SolverFactory("ipopt") -compute_infeasibility_explanation(m, solver=ipopt) + +class TestMIS(unittest.TestCase): + def test_trivial_quad(self): + m = pyo.ConcreteModel("Trivial Quad") + m.x = pyo.Var([1, 2], bounds=(0, 1)) + m.y = pyo.Var(bounds=(0, 1)) + m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) + m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) + # Note: this particular little problem is quadratic + # As of 18Feb2024 DLW is not sure the explanation code works + # with solvers other than ipopt + ipopt = pyo.SolverFactory("ipopt") + compute_infeasibility_explanation(m, solver=ipopt) From 07b5cbeec73ec70f9f222017ff5eabf2116ce08f Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 09:32:01 -0600 Subject: [PATCH 2540/3044] Silence Sphinx warnings --- .../doe/examples/reactor_experiment.py | 2 + pyomo/contrib/interior_point/examples/ex1.py | 2 +- pyomo/contrib/pynumero/sparse/block_vector.py | 236 +++++++++--------- pyomo/contrib/sensitivity_toolbox/sens.py | 4 +- pyomo/core/base/piecewise.py | 2 +- pyomo/core/expr/template_expr.py | 2 +- pyomo/core/plugins/transform/model.py | 2 +- .../tests/standalone_minimal_pyomo_driver.py | 2 +- pyomo/repn/tests/cpxlp/test_lpv2.py | 2 +- 9 files changed, 128 insertions(+), 126 deletions(-) diff --git a/pyomo/contrib/doe/examples/reactor_experiment.py b/pyomo/contrib/doe/examples/reactor_experiment.py index c94b89b026c..631510dd23a 100644 --- a/pyomo/contrib/doe/examples/reactor_experiment.py +++ b/pyomo/contrib/doe/examples/reactor_experiment.py @@ -116,9 +116,11 @@ def finalize_model(self): """ Example finalize model function. There are two main tasks here: + 1. Extracting useful information for the model to align with the experiment. (Here: CA0, t_final, t_control) 2. Discretizing the model subject to this information. + """ m = self.model diff --git a/pyomo/contrib/interior_point/examples/ex1.py b/pyomo/contrib/interior_point/examples/ex1.py index 8795e5245e4..53700c22922 100644 --- a/pyomo/contrib/interior_point/examples/ex1.py +++ b/pyomo/contrib/interior_point/examples/ex1.py @@ -41,4 +41,4 @@ def solve_qcqp_example(): if __name__ == '__main__': - return solve_qcqp_example() + solve_qcqp_example() diff --git a/pyomo/contrib/pynumero/sparse/block_vector.py b/pyomo/contrib/pynumero/sparse/block_vector.py index 4ff6d460224..37c2d7b2cf7 100644 --- a/pyomo/contrib/pynumero/sparse/block_vector.py +++ b/pyomo/contrib/pynumero/sparse/block_vector.py @@ -24,56 +24,56 @@ .. rubric:: Contents -Methods specific to :py:class:`pyomo.contrib.pynumero.sparse.block_vector.BlockVector`: +Methods specific to :py:class:`BlockVector`: - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_block` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.block_sizes` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block_size` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.is_block_defined` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyfrom` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyto` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copy_structure` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_blocks` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.pprint` + * :py:meth:`~BlockVector.set_block` + * :py:meth:`~BlockVector.get_block` + * :py:meth:`~BlockVector.block_sizes` + * :py:meth:`~BlockVector.get_block_size` + * :py:meth:`~BlockVector.is_block_defined` + * :py:meth:`~BlockVector.copyfrom` + * :py:meth:`~BlockVector.copyto` + * :py:meth:`~BlockVector.copy_structure` + * :py:meth:`~BlockVector.set_blocks` + * :py:meth:`~BlockVector.pprint` -Attributes specific to :py:class:`pyomo.contrib.pynumero.sparse.block_vector.BlockVector`: +Attributes specific to :py:class:`BlockVector`: - * :py:attr:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.nblocks` - * :py:attr:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.bshape` - * :py:attr:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.has_none` + * :py:attr:`~BlockVector.nblocks` + * :py:attr:`~BlockVector.bshape` + * :py:attr:`~BlockVector.has_none` NumPy compatible methods: - * :py:meth:`numpy.ndarray.dot` - * :py:meth:`numpy.ndarray.sum` - * :py:meth:`numpy.ndarray.all` - * :py:meth:`numpy.ndarray.any` - * :py:meth:`numpy.ndarray.max` - * :py:meth:`numpy.ndarray.astype` - * :py:meth:`numpy.ndarray.clip` - * :py:meth:`numpy.ndarray.compress` - * :py:meth:`numpy.ndarray.conj` - * :py:meth:`numpy.ndarray.conjugate` - * :py:meth:`numpy.ndarray.nonzero` - * :py:meth:`numpy.ndarray.ptp` - * :py:meth:`numpy.ndarray.round` - * :py:meth:`numpy.ndarray.std` - * :py:meth:`numpy.ndarray.var` - * :py:meth:`numpy.ndarray.tofile` - * :py:meth:`numpy.ndarray.min` - * :py:meth:`numpy.ndarray.mean` - * :py:meth:`numpy.ndarray.prod` - * :py:meth:`numpy.ndarray.fill` - * :py:meth:`numpy.ndarray.tolist` - * :py:meth:`numpy.ndarray.flatten` - * :py:meth:`numpy.ndarray.ravel` - * :py:meth:`numpy.ndarray.argmax` - * :py:meth:`numpy.ndarray.argmin` - * :py:meth:`numpy.ndarray.cumprod` - * :py:meth:`numpy.ndarray.cumsum` - * :py:meth:`numpy.ndarray.copy` + * :py:meth:`~numpy.ndarray.dot` + * :py:meth:`~numpy.ndarray.sum` + * :py:meth:`~numpy.ndarray.all` + * :py:meth:`~numpy.ndarray.any` + * :py:meth:`~numpy.ndarray.max` + * :py:meth:`~numpy.ndarray.astype` + * :py:meth:`~numpy.ndarray.clip` + * :py:meth:`~numpy.ndarray.compress` + * :py:meth:`~numpy.ndarray.conj` + * :py:meth:`~numpy.ndarray.conjugate` + * :py:meth:`~numpy.ndarray.nonzero` + * :py:meth:`~numpy.ndarray.ptp` + * :py:meth:`~numpy.ndarray.round` + * :py:meth:`~numpy.ndarray.std` + * :py:meth:`~numpy.ndarray.var` + * :py:meth:`~numpy.ndarray.tofile` + * :py:meth:`~numpy.ndarray.min` + * :py:meth:`~numpy.ndarray.mean` + * :py:meth:`~numpy.ndarray.prod` + * :py:meth:`~numpy.ndarray.fill` + * :py:meth:`~numpy.ndarray.tolist` + * :py:meth:`~numpy.ndarray.flatten` + * :py:meth:`~numpy.ndarray.ravel` + * :py:meth:`~numpy.ndarray.argmax` + * :py:meth:`~numpy.ndarray.argmin` + * :py:meth:`~numpy.ndarray.cumprod` + * :py:meth:`~numpy.ndarray.cumsum` + * :py:meth:`~numpy.ndarray.copy` For example, @@ -88,67 +88,67 @@ NumPy compatible functions: - * :py:func:`numpy.log10` - * :py:func:`numpy.sin` - * :py:func:`numpy.cos` - * :py:func:`numpy.exp` - * :py:func:`numpy.ceil` - * :py:func:`numpy.floor` - * :py:func:`numpy.tan` - * :py:func:`numpy.arctan` - * :py:func:`numpy.arcsin` - * :py:func:`numpy.arccos` - * :py:func:`numpy.sinh` - * :py:func:`numpy.cosh` - * :py:func:`numpy.abs` - * :py:func:`numpy.tanh` - * :py:func:`numpy.arccosh` - * :py:func:`numpy.arcsinh` - * :py:func:`numpy.arctanh` - * :py:func:`numpy.fabs` - * :py:func:`numpy.sqrt` - * :py:func:`numpy.log` - * :py:func:`numpy.log2` - * :py:func:`numpy.absolute` - * :py:func:`numpy.isfinite` - * :py:func:`numpy.isinf` - * :py:func:`numpy.isnan` - * :py:func:`numpy.log1p` - * :py:func:`numpy.logical_not` - * :py:func:`numpy.expm1` - * :py:func:`numpy.exp2` - * :py:func:`numpy.sign` - * :py:func:`numpy.rint` - * :py:func:`numpy.square` - * :py:func:`numpy.positive` - * :py:func:`numpy.negative` - * :py:func:`numpy.rad2deg` - * :py:func:`numpy.deg2rad` - * :py:func:`numpy.conjugate` - * :py:func:`numpy.reciprocal` - * :py:func:`numpy.signbit` - * :py:func:`numpy.add` - * :py:func:`numpy.multiply` - * :py:func:`numpy.divide` - * :py:func:`numpy.subtract` - * :py:func:`numpy.greater` - * :py:func:`numpy.greater_equal` - * :py:func:`numpy.less` - * :py:func:`numpy.less_equal` - * :py:func:`numpy.not_equal` - * :py:func:`numpy.maximum` - * :py:func:`numpy.minimum` - * :py:func:`numpy.fmax` - * :py:func:`numpy.fmin` - * :py:func:`numpy.equal` - * :py:func:`numpy.logical_and` - * :py:func:`numpy.logical_or` - * :py:func:`numpy.logical_xor` - * :py:func:`numpy.logaddexp` - * :py:func:`numpy.logaddexp2` - * :py:func:`numpy.remainder` - * :py:func:`numpy.heaviside` - * :py:func:`numpy.hypot` + * :py:func:`~numpy.log10` + * :py:func:`~numpy.sin` + * :py:func:`~numpy.cos` + * :py:func:`~numpy.exp` + * :py:func:`~numpy.ceil` + * :py:func:`~numpy.floor` + * :py:func:`~numpy.tan` + * :py:func:`~numpy.arctan` + * :py:func:`~numpy.arcsin` + * :py:func:`~numpy.arccos` + * :py:func:`~numpy.sinh` + * :py:func:`~numpy.cosh` + * :py:func:`~numpy.abs` + * :py:func:`~numpy.tanh` + * :py:func:`~numpy.arccosh` + * :py:func:`~numpy.arcsinh` + * :py:func:`~numpy.arctanh` + * :py:func:`~numpy.fabs` + * :py:func:`~numpy.sqrt` + * :py:func:`~numpy.log` + * :py:func:`~numpy.log2` + * :py:func:`~numpy.absolute` + * :py:func:`~numpy.isfinite` + * :py:func:`~numpy.isinf` + * :py:func:`~numpy.isnan` + * :py:func:`~numpy.log1p` + * :py:func:`~numpy.logical_not` + * :py:func:`~numpy.expm1` + * :py:func:`~numpy.exp2` + * :py:func:`~numpy.sign` + * :py:func:`~numpy.rint` + * :py:func:`~numpy.square` + * :py:func:`~numpy.positive` + * :py:func:`~numpy.negative` + * :py:func:`~numpy.rad2deg` + * :py:func:`~numpy.deg2rad` + * :py:func:`~numpy.conjugate` + * :py:func:`~numpy.reciprocal` + * :py:func:`~numpy.signbit` + * :py:func:`~numpy.add` + * :py:func:`~numpy.multiply` + * :py:func:`~numpy.divide` + * :py:func:`~numpy.subtract` + * :py:func:`~numpy.greater` + * :py:func:`~numpy.greater_equal` + * :py:func:`~numpy.less` + * :py:func:`~numpy.less_equal` + * :py:func:`~numpy.not_equal` + * :py:func:`~numpy.maximum` + * :py:func:`~numpy.minimum` + * :py:func:`~numpy.fmax` + * :py:func:`~numpy.fmin` + * :py:func:`~numpy.equal` + * :py:func:`~numpy.logical_and` + * :py:func:`~numpy.logical_or` + * :py:func:`~numpy.logical_xor` + * :py:func:`~numpy.logaddexp` + * :py:func:`~numpy.logaddexp2` + * :py:func:`~numpy.remainder` + * :py:func:`~numpy.heaviside` + * :py:func:`~numpy.hypot` For example, @@ -163,20 +163,20 @@ .. autosummary:: - pyomo.contrib.pynumero.sparse.block_vector.BlockVector - pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_block - pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block - pyomo.contrib.pynumero.sparse.block_vector.BlockVector.block_sizes - pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block_size - pyomo.contrib.pynumero.sparse.block_vector.BlockVector.is_block_defined - pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyfrom - pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyto - pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copy_structure - pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_blocks - pyomo.contrib.pynumero.sparse.block_vector.BlockVector.pprint - pyomo.contrib.pynumero.sparse.block_vector.BlockVector.nblocks - pyomo.contrib.pynumero.sparse.block_vector.BlockVector.bshape - pyomo.contrib.pynumero.sparse.block_vector.BlockVector.has_none + BlockVector + BlockVector.set_block + BlockVector.get_block + BlockVector.block_sizes + BlockVector.get_block_size + BlockVector.is_block_defined + BlockVector.copyfrom + BlockVector.copyto + BlockVector.copy_structure + BlockVector.set_blocks + BlockVector.pprint + BlockVector.nblocks + BlockVector.bshape + BlockVector.has_none """ diff --git a/pyomo/contrib/sensitivity_toolbox/sens.py b/pyomo/contrib/sensitivity_toolbox/sens.py index 95913612aaa..d0f943b5876 100644 --- a/pyomo/contrib/sensitivity_toolbox/sens.py +++ b/pyomo/contrib/sensitivity_toolbox/sens.py @@ -230,7 +230,7 @@ def sensitivity_calculation( def get_dsdp(model, theta_names, theta, tee=False): - """This function calculates gradient vector of the variables with + r"""This function calculates gradient vector of the variables with respect to the parameters (theta_names). For example, given: @@ -327,7 +327,7 @@ def get_dsdp(model, theta_names, theta, tee=False): def get_dfds_dcds(model, theta_names, tee=False, solver_options=None): - """This function calculates gradient vector of the objective function + r"""This function calculates gradient vector of the objective function and constraints with respect to the variables and parameters. For example, given: diff --git a/pyomo/core/base/piecewise.py b/pyomo/core/base/piecewise.py index cd39d9a23fb..c7c19aad567 100644 --- a/pyomo/core/base/piecewise.py +++ b/pyomo/core/base/piecewise.py @@ -1023,7 +1023,7 @@ def _find_M(self, x_pts, y_pts, bound_type): "Constraints that contain piecewise linear expressions." ) class Piecewise(Block): - """Adds piecewise constraints to a Pyomo model for functions of the + r"""Adds piecewise constraints to a Pyomo model for functions of the form, y = f(x). Examples diff --git a/pyomo/core/expr/template_expr.py b/pyomo/core/expr/template_expr.py index d023d3d5d2b..47b5413d591 100644 --- a/pyomo/core/expr/template_expr.py +++ b/pyomo/core/expr/template_expr.py @@ -855,7 +855,7 @@ def beforeChild(self, node, child, child_idx): def substitute_template_expression(expr, substituter, *args, **kwargs): - """Substitute IndexTemplates in an expression tree. + r"""Substitute IndexTemplates in an expression tree. This is a general utility function for walking the expression tree and substituting all occurrences of IndexTemplate and diff --git a/pyomo/core/plugins/transform/model.py b/pyomo/core/plugins/transform/model.py index 4bdb81e7b94..f48f6a686fe 100644 --- a/pyomo/core/plugins/transform/model.py +++ b/pyomo/core/plugins/transform/model.py @@ -28,7 +28,7 @@ remove_in='6.8.0', ) def to_standard_form(self): - """ + r""" Produces a standard-form representation of the model. Returns the coefficient matrix (A), the cost vector (c), and the constraint vector (b), where the 'standard form' problem is diff --git a/pyomo/environ/tests/standalone_minimal_pyomo_driver.py b/pyomo/environ/tests/standalone_minimal_pyomo_driver.py index 80fb5d15121..ee503032040 100644 --- a/pyomo/environ/tests/standalone_minimal_pyomo_driver.py +++ b/pyomo/environ/tests/standalone_minimal_pyomo_driver.py @@ -15,7 +15,7 @@ from pyomo.common.tee import capture_output from pyomo.repn.tests.lp_diff import lp_diff -_baseline = """\\* Source Pyomo model name=unknown *\\ +_baseline = r"""\* Source Pyomo model name=unknown *\ min x2: diff --git a/pyomo/repn/tests/cpxlp/test_lpv2.py b/pyomo/repn/tests/cpxlp/test_lpv2.py index fbef24c77c3..42fead8da49 100644 --- a/pyomo/repn/tests/cpxlp/test_lpv2.py +++ b/pyomo/repn/tests/cpxlp/test_lpv2.py @@ -76,7 +76,7 @@ def test_warn_export_suffixes(self): ) def test_deterministic_unordered_sets(self): - ref = """\\* Source Pyomo model name=unknown *\\ + ref = r"""\* Source Pyomo model name=unknown *\ min o: From 562c5d550e4e830676895b4616b2c72c401333d6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 09:39:02 -0600 Subject: [PATCH 2541/3044] NFC: typo --- doc/OnlineDocs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 87da88a05dd..704b773bdc3 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -133,7 +133,7 @@ # - _build : this is the Sphinx build (output) dir # # - api/*.tests.* : this matches autosummary RST files generated for -# test modules. Note that the _tempaltes/recursive-modules.rst +# test modules. Note that the _templates/recursive-modules.rst # should prevent these file from being generated, so this is not # strictly necessary, but including it makes Sphinx throw warnings if # the filter in the template ever "breaks" From 1dd0a359a2bea1eafbc187322ffe88921a92dc39 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 10:56:28 -0600 Subject: [PATCH 2542/3044] Move pint import to common.dependencies; update for Python 3.13 + pint<=0.24.3 --- doc/Archive/conf.py | 3 +-- pyomo/common/dependencies.py | 5 +++++ pyomo/core/base/units_container.py | 10 +--------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/doc/Archive/conf.py b/doc/Archive/conf.py index a06ccfbc9bd..741322c9977 100644 --- a/doc/Archive/conf.py +++ b/doc/Archive/conf.py @@ -270,9 +270,8 @@ def check_output(self, want, got, optionflags): from pyomo.common.dependencies import ( attempt_import, numpy_available, scipy_available, pandas_available, yaml_available, networkx_available, matplotlib_available, - pympler_available, dill_available, + pympler_available, dill_available, pint_available, ) -pint_available = attempt_import('pint', defer_import=False)[1] from pyomo.contrib.parmest.parmest import parmest_available import pyomo.environ as _pe # (trigger all plugin registrations) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index 2505a6bb298..f2bc9ad5c78 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -1073,6 +1073,11 @@ def _pyutilib_importer(): networkx, networkx_available = attempt_import('networkx') numpy, numpy_available = attempt_import('numpy', callback=_finalize_numpy) pandas, pandas_available = attempt_import('pandas') + pint, pint_available = attempt_import( + 'pint', + # TypeError for pint<=0.24.3 and python>=3.13 + catch_exceptions=(ImportError, TypeError), + ) plotly, plotly_available = attempt_import('plotly') pympler, pympler_available = attempt_import('pympler', callback=_finalize_pympler) pyutilib, pyutilib_available = attempt_import( diff --git a/pyomo/core/base/units_container.py b/pyomo/core/base/units_container.py index f3dec1e0db1..6f2e097abd1 100644 --- a/pyomo/core/base/units_container.py +++ b/pyomo/core/base/units_container.py @@ -111,7 +111,7 @@ import logging import sys -from pyomo.common.dependencies import attempt_import +from pyomo.common.dependencies import pint as pint_module, pint_available from pyomo.common.modeling import NOTSET from pyomo.core.expr.numvalue import ( NumericValue, @@ -124,14 +124,6 @@ from pyomo.core.expr.visitor import ExpressionValueVisitor import pyomo.core.expr as EXPR -pint_module, pint_available = attempt_import( - 'pint', - error_message=( - 'The "pint" package failed to import. ' - 'This package is necessary to use Pyomo units.' - ), -) - logger = logging.getLogger(__name__) From b47c26c6a298be9e447d918fc6b622aef1ad6ba8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 11:01:29 -0600 Subject: [PATCH 2543/3044] Revert previous commit to Archive/conf.py; apply to current OnlineDocs/conf.py --- doc/Archive/conf.py | 3 ++- doc/OnlineDocs/conf.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/Archive/conf.py b/doc/Archive/conf.py index 741322c9977..a06ccfbc9bd 100644 --- a/doc/Archive/conf.py +++ b/doc/Archive/conf.py @@ -270,8 +270,9 @@ def check_output(self, want, got, optionflags): from pyomo.common.dependencies import ( attempt_import, numpy_available, scipy_available, pandas_available, yaml_available, networkx_available, matplotlib_available, - pympler_available, dill_available, pint_available, + pympler_available, dill_available, ) +pint_available = attempt_import('pint', defer_import=False)[1] from pyomo.contrib.parmest.parmest import parmest_available import pyomo.environ as _pe # (trigger all plugin registrations) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 704b773bdc3..349aed6c9eb 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -287,10 +287,9 @@ def check_output(self, want, got, optionflags): from pyomo.common.dependencies import ( attempt_import, numpy_available, scipy_available, pandas_available, yaml_available, networkx_available, matplotlib_available, - pympler_available, dill_available, + pympler_available, dill_available, pint_available, numpy as np, ) -pint_available = attempt_import('pint', defer_import=False)[1] from pyomo.contrib.parmest.parmest import parmest_available # Not using SolverFactory to check solver availability because From 08341a9139776eafcb19825aa2218aa21d63cf64 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 15:28:12 -0600 Subject: [PATCH 2544/3044] Do not trigger DeferredImportError exceptions when attempting to serialize objects --- pyomo/common/dependencies.py | 18 ++++++++++-------- pyomo/common/flags.py | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/pyomo/common/dependencies.py b/pyomo/common/dependencies.py index f2bc9ad5c78..ce7702de80e 100644 --- a/pyomo/common/dependencies.py +++ b/pyomo/common/dependencies.py @@ -19,10 +19,13 @@ from types import ModuleType from typing import List -import pyomo -from .deprecation import deprecated, deprecation_warning -from .errors import DeferredImportError -from .flags import in_testing_environment, building_documentation +from pyomo.common.deprecation import deprecated, deprecation_warning +from pyomo.common.errors import DeferredImportError +from pyomo.common.flags import ( + in_testing_environment, + building_documentation, + serializing, +) SUPPRESS_DEPENDENCY_WARNINGS = False @@ -71,10 +74,9 @@ def __init__(self, name, message, version_error, import_error, package): self._moduleunavailable_info_ = (message, version_error, import_error, package) def __getattr__(self, attr): - if attr in ModuleUnavailable._getattr_raises_attributeerror: - raise AttributeError( - "'%s' object has no attribute '%s'" % (type(self).__name__, attr) - ) + if serializing() or attr in ModuleUnavailable._getattr_raises_attributeerror: + msg = "'%s' object has no attribute '%s'" % (type(self).__name__, attr) + raise AttributeError(msg) raise DeferredImportError(self._moduleunavailable_message()) def __getstate__(self): diff --git a/pyomo/common/flags.py b/pyomo/common/flags.py index d7c47f3e7c5..05471bff405 100644 --- a/pyomo/common/flags.py +++ b/pyomo/common/flags.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import inspect import sys @@ -106,3 +107,22 @@ def building_documentation(state=NOTSET): building_documentation.state = None + + +def serializing(): + """True if it looks like we are serializing objects + + This looks through the call stack and returns True if it finds a + `dump` function anywhere in the call stack. While not foolproof, + this should reliably catch most serializers, including ``pickle`` + and `yaml``. + + """ + # Start by skipping this function + frame = inspect.currentframe().f_back + while frame is not None: + print(frame.f_code.co_name, frame.f_code.co_filename) + if frame.f_code.co_name == 'dump': + return True + frame = frame.f_back + return False From 48080ad62be07f39ffa896c5506cee9626e0510e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 15:28:52 -0600 Subject: [PATCH 2545/3044] Update doctests for OSX, Numpy changes --- doc/OnlineDocs/conf.py | 4 +++ .../tutorial.block_vectors_and_matrices.rst | 4 +-- pyomo/common/unittest.py | 36 +++++++++++-------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/doc/OnlineDocs/conf.py b/doc/OnlineDocs/conf.py index 349aed6c9eb..380b93d053c 100644 --- a/doc/OnlineDocs/conf.py +++ b/doc/OnlineDocs/conf.py @@ -281,6 +281,10 @@ def check_output(self, want, got, optionflags): platform.python_implementation() ) +# We need multiprocessing because some doctests must be skipped if the +# start method is not "fork" +import multiprocessing + # (register plugins, make environ available to tests) import pyomo.environ as pyo diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst b/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst index 165c8b53f34..822d2020fc3 100644 --- a/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst +++ b/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst @@ -65,7 +65,7 @@ Once the dimensions of a block have been set, they cannot be changed: Properties: .. doctest:: - :skipif: not numpy_available or not scipy_available + :skipif: not scipy_available or int(np.__version__[0]) >= 2 >>> v.shape (5,) @@ -85,7 +85,7 @@ Properties: Much of the `BlockVector` API matches that of NumPy arrays: .. doctest:: - :skipif: not numpy_available or not scipy_available + :skipif: not scipy_available or int(np.__version__[0]) >= 2 >>> v.sum() 0.62846552 diff --git a/pyomo/common/unittest.py b/pyomo/common/unittest.py index fb9584c652b..19314e00186 100644 --- a/pyomo/common/unittest.py +++ b/pyomo/common/unittest.py @@ -383,21 +383,27 @@ def timeout(seconds, require_fork=False, timeout_raises=TimeoutError): Examples -------- - >>> import pyomo.common.unittest as unittest - >>> @unittest.timeout(1) - ... def test_function(): - ... return 42 - >>> test_function() - 42 - - >>> @unittest.timeout(0.01) - ... def test_function(): - ... while 1: - ... pass - >>> test_function() - Traceback (most recent call last): - ... - TimeoutError: test timed out after 0.01 seconds + .. doctest:: + :skipif: multiprocessing.get_start_method() != 'fork' + + >>> import pyomo.common.unittest as unittest + >>> @unittest.timeout(1) + ... def test_function(): + ... return 42 + >>> test_function() + 42 + + .. doctest:: + :skipif: multiprocessing.get_start_method() != 'fork' + + >>> @unittest.timeout(0.01) + ... def test_function(): + ... while 1: + ... pass + >>> test_function() + Traceback (most recent call last): + ... + TimeoutError: test timed out after 0.01 seconds """ import functools From eefb98cf79946c09ea6bcfe9216b1c2476bb66d5 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 16:04:29 -0600 Subject: [PATCH 2546/3044] Remove debugging --- pyomo/common/flags.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/common/flags.py b/pyomo/common/flags.py index 05471bff405..817366cbccb 100644 --- a/pyomo/common/flags.py +++ b/pyomo/common/flags.py @@ -121,7 +121,6 @@ def serializing(): # Start by skipping this function frame = inspect.currentframe().f_back while frame is not None: - print(frame.f_code.co_name, frame.f_code.co_filename) if frame.f_code.co_name == 'dump': return True frame = frame.f_back From 465298af9767b7fb1c96065a0d3fb9a67feecc3e Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 16:12:32 -0600 Subject: [PATCH 2547/3044] Add (hidden) documentation of the NumPy2 output --- .../tutorial.block_vectors_and_matrices.rst | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst b/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst index 822d2020fc3..98d6908c53e 100644 --- a/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst +++ b/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst @@ -82,6 +82,25 @@ Properties: >>> m.nnz 12 + .. doctest:: + :hide: + :skipif: not scipy_available or int(np.__version__[0]) < 2 + + >>> v.shape + (np.int64(5),) + >>> v.size + np.int64(5) + >>> v.nblocks + 3 + >>> v.bshape + (3,) + >>> m.shape + (np.int64(5), np.int64(5)) + >>> m.bshape + (3, 3) + >>> m.nnz + 12 + Much of the `BlockVector` API matches that of NumPy arrays: .. doctest:: @@ -100,6 +119,23 @@ Much of the `BlockVector` API matches that of NumPy arrays: >>> v.dot(v) 4.781303326558476 + .. doctest:: + :hide: + :skipif: not scipy_available or int(np.__version__[0]) < 2 + + >>> v.sum() + np.float64(0.62846552) + >>> v.max() + np.float64(1.25) + >>> np.abs(v).flatten() + array([0.67025575, 1.2 , 0.1 , 1.14872127, 1.25 ]) + >>> (2*v).flatten() + array([-1.3405115 , -2.4 , 0.2 , 2.29744254, 2.5 ]) + >>> (v + v).flatten() + array([-1.3405115 , -2.4 , 0.2 , 2.29744254, 2.5 ]) + >>> v.dot(v) + np.float64(4.781303326558476) + Similarly, `BlockMatrix` behaves very similarly to SciPy sparse matrices: .. doctest:: From 1860a1d09b11db89c2be91d2523e88c81175eb0d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 20:26:26 -0600 Subject: [PATCH 2548/3044] Fix indentation typo --- .../solvers/pynumero/tutorial.block_vectors_and_matrices.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst b/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst index 98d6908c53e..a8a66e81a46 100644 --- a/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst +++ b/doc/OnlineDocs/explanation/solvers/pynumero/tutorial.block_vectors_and_matrices.rst @@ -82,7 +82,7 @@ Properties: >>> m.nnz 12 - .. doctest:: +.. doctest:: :hide: :skipif: not scipy_available or int(np.__version__[0]) < 2 @@ -119,7 +119,7 @@ Much of the `BlockVector` API matches that of NumPy arrays: >>> v.dot(v) 4.781303326558476 - .. doctest:: +.. doctest:: :hide: :skipif: not scipy_available or int(np.__version__[0]) < 2 From 91922d75c6022c1d95fdc743b1002069e3773c4d Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 21:21:20 -0600 Subject: [PATCH 2549/3044] Clean up makefile --- doc/OnlineDocs/Makefile | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/OnlineDocs/Makefile b/doc/OnlineDocs/Makefile index 264799b6fa9..246d8f7b990 100644 --- a/doc/OnlineDocs/Makefile +++ b/doc/OnlineDocs/Makefile @@ -24,10 +24,9 @@ clean: @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) @echo "Removing *.spy, *.out" @find . -name \*.spy -delete - @if test -d "$(SOURCEDIR)/${APIDIR}"; then rm -r "$(SOURCEDIR)/${APIDIR}"; fi + @for D in $(BUILDDIR) $(SOURCEDIR)/$(APIDIR); do \ + if test -d "$$D"; then echo "Removing $$D"; rm -r "$$D"; fi \ + done rebuild: @$(MAKE) clean - @for D in $(BUILDDIR) $(SOURCEDIR)/reference/API; do \ - if test -d "$$D"; then echo "Removing $$D"; rm -r "$$D"; fi \ - done From adf8bb589a90673d1fa8f02df71e45968c20d97b Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 21:22:03 -0600 Subject: [PATCH 2550/3044] Restore (and update) doc README --- doc/OnlineDocs/README.md | 58 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 doc/OnlineDocs/README.md diff --git a/doc/OnlineDocs/README.md b/doc/OnlineDocs/README.md new file mode 100644 index 00000000000..8c15f8ece6c --- /dev/null +++ b/doc/OnlineDocs/README.md @@ -0,0 +1,58 @@ +Pyomo leverages ``make`` to generate documentation. The following two +sections describe how to build and test the online documentation +locally. + +.. note:: + + All commands assume you are running from the `root Pyomo source directory`. + + +Preview Changes Locally +------------------------ + +1. Install documentation dependencies (e.g., Sphinx, etc): + + ```bash + $ pip install -e .[docs] + ``` + + **NOTE**: You may get a warning about the `dot` command if you do not have + `graphviz` installed. + +2. Build the documentation. Sphinx (and Pyomo) support multiple + documentation `targets`. These instructions describe building the + `html` target, but the same process applies for other targets. + + ```bash + $ make -C doc/OnlineDocs html + ``` + +3. View ``doc/OnlineDocs/_build/html/index.html`` in your browser + +Test Changes Locally +-------------------- + + ```bash + $ make -C doc/OnlineDocs doctest + ``` + +Rebuilding the documentation +---------------------------- + +Sphinx caches significant amounts of work at the end of a documentation +build. However, if you are in the process of editing the documentation, +it may not correctly invalidate the cache. You can purge the entire +cache with + + ```bash + $ make -C doc/OnlineDocs clean + ``` + +Combining steps +--------------- + +These steps can, of course, be combined into a single command: + + ```bash + $ make -c doc/OnlineDocs clean html doctest + ``` \ No newline at end of file From c29de6b274412d0d1c2af78e27965349af90c75c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Thu, 10 Oct 2024 22:59:38 -0600 Subject: [PATCH 2551/3044] update baseline to match python 3.13 behavior --- pyomo/common/config.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/common/config.py b/pyomo/common/config.py index 98fffdcfc6c..4ff03e9fbd7 100644 --- a/pyomo/common/config.py +++ b/pyomo/common/config.py @@ -946,10 +946,8 @@ class will still create ``c`` instances that only have the single --disable-linesearch [DON'T] use line search Tolerances: - --reltol FLOAT, -r FLOAT - relative convergence tolerance - --abstol FLOAT, -a FLOAT - absolute convergence tolerance + --reltol... -r FLOAT relative convergence tolerance + --abstol... -a FLOAT absolute convergence tolerance .. doctest:: From 8830cc75e46548823bf4ae4b38b254bb8f1eeac8 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 11 Oct 2024 11:48:18 -0600 Subject: [PATCH 2552/3044] Remove doc/Archive directory --- doc/Archive/Makefile | 26 - doc/Archive/README.md | 27 - doc/Archive/_static/theme_overrides.css | 74 -- .../advanced_topics/flattener/index.rst | 65 - .../advanced_topics/flattener/motivation.rst | 26 - .../advanced_topics/flattener/reference.rst | 14 - doc/Archive/advanced_topics/index.rst | 11 - .../advanced_topics/linearexpression.rst | 44 - .../advanced_topics/persistent_solvers.rst | 188 --- .../advanced_topics/sos_constraints.rst | 288 ----- .../advanced_topics/units_container.rst | 13 - doc/Archive/bibliography.rst | 68 -- doc/Archive/citing_pyomo.rst | 16 - doc/Archive/conf.py | 304 ----- .../alternative_solutions.rst | 107 -- .../contributed_packages/communities_8pp.png | Bin 256159 -> 0 bytes .../communities_decode_1.png | Bin 157034 -> 0 bytes .../contributed_packages/community.rst | 389 ------ .../contributed_packages/doe/CCSI-license.txt | 43 - .../doe/FIM_sensitivity.png | Bin 194054 -> 0 bytes doc/Archive/contributed_packages/doe/doe.rst | 239 ---- .../contributed_packages/doe/flowchart.png | Bin 160954 -> 0 bytes .../contributed_packages/doe/grid-1.png | Bin 611702 -> 0 bytes .../contributed_packages/doe/reactor.png | Bin 114480 -> 0 bytes doc/Archive/contributed_packages/doe/uml.png | Bin 199419 -> 0 bytes doc/Archive/contributed_packages/gdpopt.rst | 213 ---- .../contributed_packages/gdpopt_flowchart.png | Bin 71538 -> 0 bytes doc/Archive/contributed_packages/iis.rst | 135 --- .../contributed_packages/incidence/api.rst | 14 - .../contributed_packages/incidence/config.rst | 5 - .../incidence/connected.rst | 5 - .../incidence/dulmage_mendelsohn.rst | 5 - .../incidence/incidence.rst | 5 - .../contributed_packages/incidence/index.rst | 19 - .../incidence/interface.rst | 5 - .../incidence/matching.rst | 5 - .../incidence/overview.rst | 50 - .../incidence/scc_solver.rst | 5 - .../incidence/triangularize.rst | 5 - .../incidence/tutorial.bt.rst | 107 -- .../incidence/tutorial.btsolve.rst | 72 -- .../incidence/tutorial.dm.rst | 191 --- .../incidence/tutorial.rst | 14 - doc/Archive/contributed_packages/index.rst | 47 - .../contributed_packages/latex_printer.rst | 127 -- doc/Archive/contributed_packages/mcpp.rst | 62 - doc/Archive/contributed_packages/mindtpy.rst | 319 ----- doc/Archive/contributed_packages/mpc/api.rst | 10 - .../contributed_packages/mpc/conversion.rst | 5 - doc/Archive/contributed_packages/mpc/data.rst | 17 - .../contributed_packages/mpc/examples.rst | 6 - doc/Archive/contributed_packages/mpc/faq.rst | 16 - .../contributed_packages/mpc/index.rst | 32 - .../contributed_packages/mpc/interface.rst | 8 - .../contributed_packages/mpc/modeling.rst | 11 - .../contributed_packages/mpc/overview.rst | 210 ---- .../contributed_packages/multistart.rst | 34 - .../contributed_packages/parmest/api.rst | 25 - .../contributed_packages/parmest/boxplot.png | Bin 19354 -> 0 bytes .../parmest/covariance.rst | 16 - .../contributed_packages/parmest/datarec.rst | 54 - .../contributed_packages/parmest/driver.rst | 165 --- .../contributed_packages/parmest/examples.rst | 44 - .../contributed_packages/parmest/graphics.rst | 55 - .../contributed_packages/parmest/index.rst | 35 - .../parmest/installation.rst | 33 - .../contributed_packages/parmest/overview.rst | 72 -- .../parmest/pairwise_plot_CI.png | Bin 84454 -> 0 bytes .../parmest/pairwise_plot_LR.png | Bin 49578 -> 0 bytes .../contributed_packages/parmest/parallel.rst | 52 - .../parmest/scencreate.rst | 22 - .../contributed_packages/preprocessing.rst | 151 --- .../contributed_packages/pynumero/api.rst | 14 - .../pynumero/backward_compatibility.rst | 14 - .../contributed_packages/pynumero/index.rst | 51 - .../pynumero/installation.rst | 47 - .../pynumero/pynumero.interfaces.ampl_nlp.rst | 8 - .../pynumero/pynumero.interfaces.asl_nlp.rst | 8 - .../pynumero.interfaces.extended_nlp.rst | 8 - ...ero.interfaces.external_grey_box_model.rst | 8 - .../pynumero/pynumero.interfaces.nlp.rst | 8 - .../pynumero.interfaces.projected_nlp.rst | 8 - ...pynumero.interfaces.pyomo_grey_box_nlp.rst | 8 - .../pynumero.interfaces.pyomo_nlp.rst | 8 - .../pynumero/pynumero.interfaces.rst | 16 - .../pynumero/pynumero.linalg.base.rst | 26 - .../pynumero/pynumero.linalg.ma27.rst | 8 - .../pynumero/pynumero.linalg.ma57.rst | 8 - .../pynumero/pynumero.linalg.mumps.rst | 8 - .../pynumero/pynumero.linalg.rst | 14 - .../pynumero/pynumero.linalg.scipy.rst | 14 - .../pynumero/pynumero.sparse.block_vector.rst | 154 --- .../pynumero/pynumero.sparse.rst | 9 - .../tutorial.block_vectors_and_matrices.rst | 272 ----- .../tutorial.linear_solver_interfaces.rst | 76 -- .../pynumero/tutorial.mpi_blocks.rst | 65 - .../pynumero/tutorial.nlp_interfaces.rst | 115 -- .../pynumero/tutorial.rst | 11 - doc/Archive/contributed_packages/pyros.rst | 1078 ----------------- .../contributed_packages/satsolver.rst | 34 - .../sensitivity_toolbox.rst | 185 --- .../contributed_packages/trustregion.rst | 189 --- doc/Archive/contribution_guide.rst | 431 ------- doc/Archive/developer_reference/config.rst | 3 - .../developer_reference/deprecation.rst | 62 - .../expressions/design.rst | 268 ---- .../developer_reference/expressions/index.rst | 55 - .../expressions/managing.rst | 272 ----- .../expressions/overview.rst | 300 ----- .../expressions/performance.rst | 171 --- doc/Archive/developer_reference/future.rst | 3 - doc/Archive/developer_reference/index.rst | 16 - doc/Archive/developer_reference/solvers.rst | 351 ------ doc/Archive/docutils.conf | 2 - doc/Archive/errors.rst | 192 --- doc/Archive/index.rst | 55 - doc/Archive/installation.rst | 99 -- doc/Archive/library_reference/aml/index.rst | 85 -- .../library_reference/appsi/appsi.base.rst | 47 - doc/Archive/library_reference/appsi/appsi.rst | 106 -- .../appsi/appsi.solvers.cbc.rst | 15 - .../appsi/appsi.solvers.cplex.rst | 21 - .../appsi/appsi.solvers.gurobi.rst | 55 - .../appsi/appsi.solvers.highs.rst | 14 - .../appsi/appsi.solvers.ipopt.rst | 14 - .../appsi/appsi.solvers.maingo.rst | 14 - .../library_reference/appsi/appsi.solvers.rst | 16 - .../library_reference/common/config.rst | 85 -- .../library_reference/common/dependencies.rst | 7 - .../library_reference/common/deprecation.rst | 6 - .../library_reference/common/enums.rst | 7 - .../library_reference/common/errors.rst | 6 - .../library_reference/common/fileutils.rst | 6 - .../library_reference/common/formatting.rst | 6 - .../library_reference/common/index.rst | 19 - .../library_reference/common/tempfiles.rst | 7 - .../library_reference/common/timing.rst | 7 - doc/Archive/library_reference/data/index.rst | 11 - .../expressions/building.rst | 10 - .../library_reference/expressions/classes.rst | 105 -- .../expressions/context_managers.rst | 10 - .../library_reference/expressions/index.rst | 13 - .../expressions/managing.rst | 19 - .../expressions/visitors.rst | 20 - doc/Archive/library_reference/index.rst | 27 - doc/Archive/library_reference/kernel/base.rst | 6 - .../library_reference/kernel/block.rst | 26 - .../library_reference/kernel/conic.rst | 42 - .../library_reference/kernel/constraint.rst | 34 - .../kernel/dict_container.rst | 8 - .../kernel/examples/aml_example.py | 193 --- .../kernel/examples/conic.py | 33 - .../kernel/examples/kernel_containers.py | 18 - .../kernel/examples/kernel_example.py | 174 --- .../kernel/examples/kernel_solving.py | 22 - .../kernel/examples/kernel_subclassing.py | 93 -- .../kernel/examples/transformer.py | 66 - .../library_reference/kernel/expression.rst | 26 - .../kernel/heterogeneous_container.rst | 6 - .../kernel/homogeneous_container.rst | 6 - .../library_reference/kernel/index.rst | 210 ---- .../kernel/list_container.rst | 8 - .../library_reference/kernel/objective.rst | 26 - .../library_reference/kernel/parameter.rst | 30 - .../kernel/piecewise/index.rst | 11 - .../kernel/piecewise/piecewise.rst | 53 - .../kernel/piecewise/piecewise_nd.rst | 25 - .../kernel/piecewise/util.rst | 6 - doc/Archive/library_reference/kernel/sos.rst | 30 - .../library_reference/kernel/suffix.rst | 6 - .../kernel/syntax_comparison.rst | 133 -- .../kernel/tuple_container.rst | 8 - .../library_reference/kernel/variable.rst | 26 - .../solvers/cplex_persistent.rst | 7 - .../library_reference/solvers/gams.rst | 43 - .../solvers/gurobi_direct.rst | 18 - .../solvers/gurobi_persistent.rst | 39 - .../library_reference/solvers/index.rst | 11 - .../solvers/xpress_persistent.rst | 7 - doc/Archive/make.bat | 36 - doc/Archive/model_debugging/FAQ.rst | 29 - doc/Archive/model_debugging/getting_help.rst | 10 - doc/Archive/model_debugging/index.rst | 9 - .../model_debugging/model_interrogation.rst | 32 - doc/Archive/model_transformations/index.rst | 7 - doc/Archive/model_transformations/scaling.rst | 41 - doc/Archive/modeling_extensions/__init__.py | 10 - doc/Archive/modeling_extensions/bilevel.rst | 6 - doc/Archive/modeling_extensions/dae.rst | 933 -------------- .../modeling_extensions/gdp/concepts.rst | 151 --- doc/Archive/modeling_extensions/gdp/index.rst | 79 -- .../modeling_extensions/gdp/modeling.rst | 419 ------- .../modeling_extensions/gdp/solving.rst | 201 --- doc/Archive/modeling_extensions/index.rst | 12 - doc/Archive/modeling_extensions/mpec.rst | 6 - doc/Archive/modeling_extensions/network.rst | 331 ----- .../reduce_points_demo.png | Bin 29803 -> 0 bytes .../stochastic_programming.rst | 17 - .../pyomo_modeling_components/Constraints.rst | 39 - .../pyomo_modeling_components/Expressions.rst | 218 ---- .../pyomo_modeling_components/Objectives.rst | 39 - .../pyomo_modeling_components/Parameters.rst | 102 -- .../pyomo_modeling_components/Sets.rst | 519 -------- .../pyomo_modeling_components/Suffixes.rst | 509 -------- .../pyomo_modeling_components/Variables.rst | 46 - .../pyomo_modeling_components/index.rst | 13 - .../pyomo_overview/abstract_concrete.rst | 54 - doc/Archive/pyomo_overview/index.rst | 10 - doc/Archive/pyomo_overview/math_modeling.rst | 102 -- .../pyomo_overview/overview_components.rst | 42 - .../pyomo_overview/simple_examples.rst | 416 ------- doc/Archive/related_packages.rst | 62 - doc/Archive/solving_pyomo_models.rst | 73 -- doc/Archive/tutorial_examples.rst | 20 - .../working_abstractmodels/BuildAction.rst | 66 - .../working_abstractmodels/data/ABCD.pdf | Bin 18227 -> 0 bytes .../working_abstractmodels/data/ABCD.png | Bin 5496 -> 0 bytes .../working_abstractmodels/data/PP.png | Bin 18060 -> 0 bytes .../data/dataportals.rst | 530 -------- .../working_abstractmodels/data/datfiles.rst | 933 -------------- .../working_abstractmodels/data/index.rst | 66 - .../working_abstractmodels/data/native.rst | 84 -- .../working_abstractmodels/data/raw_dicts.rst | 53 - .../data/storing_data.rst | 16 - doc/Archive/working_abstractmodels/index.rst | 10 - .../instantiating_models.rst | 121 -- .../working_abstractmodels/pyomo_command.rst | 123 -- doc/Archive/working_models.rst | 704 ----------- 228 files changed, 18684 deletions(-) delete mode 100644 doc/Archive/Makefile delete mode 100644 doc/Archive/README.md delete mode 100644 doc/Archive/_static/theme_overrides.css delete mode 100644 doc/Archive/advanced_topics/flattener/index.rst delete mode 100644 doc/Archive/advanced_topics/flattener/motivation.rst delete mode 100644 doc/Archive/advanced_topics/flattener/reference.rst delete mode 100644 doc/Archive/advanced_topics/index.rst delete mode 100644 doc/Archive/advanced_topics/linearexpression.rst delete mode 100644 doc/Archive/advanced_topics/persistent_solvers.rst delete mode 100644 doc/Archive/advanced_topics/sos_constraints.rst delete mode 100644 doc/Archive/advanced_topics/units_container.rst delete mode 100644 doc/Archive/bibliography.rst delete mode 100644 doc/Archive/citing_pyomo.rst delete mode 100644 doc/Archive/conf.py delete mode 100644 doc/Archive/contributed_packages/alternative_solutions.rst delete mode 100644 doc/Archive/contributed_packages/communities_8pp.png delete mode 100644 doc/Archive/contributed_packages/communities_decode_1.png delete mode 100644 doc/Archive/contributed_packages/community.rst delete mode 100644 doc/Archive/contributed_packages/doe/CCSI-license.txt delete mode 100644 doc/Archive/contributed_packages/doe/FIM_sensitivity.png delete mode 100644 doc/Archive/contributed_packages/doe/doe.rst delete mode 100644 doc/Archive/contributed_packages/doe/flowchart.png delete mode 100644 doc/Archive/contributed_packages/doe/grid-1.png delete mode 100644 doc/Archive/contributed_packages/doe/reactor.png delete mode 100644 doc/Archive/contributed_packages/doe/uml.png delete mode 100644 doc/Archive/contributed_packages/gdpopt.rst delete mode 100644 doc/Archive/contributed_packages/gdpopt_flowchart.png delete mode 100644 doc/Archive/contributed_packages/iis.rst delete mode 100644 doc/Archive/contributed_packages/incidence/api.rst delete mode 100644 doc/Archive/contributed_packages/incidence/config.rst delete mode 100644 doc/Archive/contributed_packages/incidence/connected.rst delete mode 100644 doc/Archive/contributed_packages/incidence/dulmage_mendelsohn.rst delete mode 100644 doc/Archive/contributed_packages/incidence/incidence.rst delete mode 100644 doc/Archive/contributed_packages/incidence/index.rst delete mode 100644 doc/Archive/contributed_packages/incidence/interface.rst delete mode 100644 doc/Archive/contributed_packages/incidence/matching.rst delete mode 100644 doc/Archive/contributed_packages/incidence/overview.rst delete mode 100644 doc/Archive/contributed_packages/incidence/scc_solver.rst delete mode 100644 doc/Archive/contributed_packages/incidence/triangularize.rst delete mode 100644 doc/Archive/contributed_packages/incidence/tutorial.bt.rst delete mode 100644 doc/Archive/contributed_packages/incidence/tutorial.btsolve.rst delete mode 100644 doc/Archive/contributed_packages/incidence/tutorial.dm.rst delete mode 100644 doc/Archive/contributed_packages/incidence/tutorial.rst delete mode 100644 doc/Archive/contributed_packages/index.rst delete mode 100644 doc/Archive/contributed_packages/latex_printer.rst delete mode 100644 doc/Archive/contributed_packages/mcpp.rst delete mode 100644 doc/Archive/contributed_packages/mindtpy.rst delete mode 100644 doc/Archive/contributed_packages/mpc/api.rst delete mode 100644 doc/Archive/contributed_packages/mpc/conversion.rst delete mode 100644 doc/Archive/contributed_packages/mpc/data.rst delete mode 100644 doc/Archive/contributed_packages/mpc/examples.rst delete mode 100644 doc/Archive/contributed_packages/mpc/faq.rst delete mode 100644 doc/Archive/contributed_packages/mpc/index.rst delete mode 100644 doc/Archive/contributed_packages/mpc/interface.rst delete mode 100644 doc/Archive/contributed_packages/mpc/modeling.rst delete mode 100644 doc/Archive/contributed_packages/mpc/overview.rst delete mode 100644 doc/Archive/contributed_packages/multistart.rst delete mode 100644 doc/Archive/contributed_packages/parmest/api.rst delete mode 100644 doc/Archive/contributed_packages/parmest/boxplot.png delete mode 100644 doc/Archive/contributed_packages/parmest/covariance.rst delete mode 100644 doc/Archive/contributed_packages/parmest/datarec.rst delete mode 100644 doc/Archive/contributed_packages/parmest/driver.rst delete mode 100644 doc/Archive/contributed_packages/parmest/examples.rst delete mode 100644 doc/Archive/contributed_packages/parmest/graphics.rst delete mode 100644 doc/Archive/contributed_packages/parmest/index.rst delete mode 100644 doc/Archive/contributed_packages/parmest/installation.rst delete mode 100644 doc/Archive/contributed_packages/parmest/overview.rst delete mode 100644 doc/Archive/contributed_packages/parmest/pairwise_plot_CI.png delete mode 100644 doc/Archive/contributed_packages/parmest/pairwise_plot_LR.png delete mode 100644 doc/Archive/contributed_packages/parmest/parallel.rst delete mode 100644 doc/Archive/contributed_packages/parmest/scencreate.rst delete mode 100644 doc/Archive/contributed_packages/preprocessing.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/api.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/backward_compatibility.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/index.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/installation.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.interfaces.nlp.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.interfaces.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.linalg.base.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.linalg.ma27.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.linalg.ma57.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.linalg.mumps.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.linalg.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.linalg.scipy.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.sparse.block_vector.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/pynumero.sparse.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/tutorial.mpi_blocks.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/tutorial.nlp_interfaces.rst delete mode 100644 doc/Archive/contributed_packages/pynumero/tutorial.rst delete mode 100644 doc/Archive/contributed_packages/pyros.rst delete mode 100644 doc/Archive/contributed_packages/satsolver.rst delete mode 100644 doc/Archive/contributed_packages/sensitivity_toolbox.rst delete mode 100644 doc/Archive/contributed_packages/trustregion.rst delete mode 100644 doc/Archive/contribution_guide.rst delete mode 100644 doc/Archive/developer_reference/config.rst delete mode 100644 doc/Archive/developer_reference/deprecation.rst delete mode 100644 doc/Archive/developer_reference/expressions/design.rst delete mode 100644 doc/Archive/developer_reference/expressions/index.rst delete mode 100644 doc/Archive/developer_reference/expressions/managing.rst delete mode 100644 doc/Archive/developer_reference/expressions/overview.rst delete mode 100644 doc/Archive/developer_reference/expressions/performance.rst delete mode 100644 doc/Archive/developer_reference/future.rst delete mode 100644 doc/Archive/developer_reference/index.rst delete mode 100644 doc/Archive/developer_reference/solvers.rst delete mode 100644 doc/Archive/docutils.conf delete mode 100644 doc/Archive/errors.rst delete mode 100644 doc/Archive/index.rst delete mode 100644 doc/Archive/installation.rst delete mode 100644 doc/Archive/library_reference/aml/index.rst delete mode 100644 doc/Archive/library_reference/appsi/appsi.base.rst delete mode 100644 doc/Archive/library_reference/appsi/appsi.rst delete mode 100644 doc/Archive/library_reference/appsi/appsi.solvers.cbc.rst delete mode 100644 doc/Archive/library_reference/appsi/appsi.solvers.cplex.rst delete mode 100644 doc/Archive/library_reference/appsi/appsi.solvers.gurobi.rst delete mode 100644 doc/Archive/library_reference/appsi/appsi.solvers.highs.rst delete mode 100644 doc/Archive/library_reference/appsi/appsi.solvers.ipopt.rst delete mode 100644 doc/Archive/library_reference/appsi/appsi.solvers.maingo.rst delete mode 100644 doc/Archive/library_reference/appsi/appsi.solvers.rst delete mode 100644 doc/Archive/library_reference/common/config.rst delete mode 100644 doc/Archive/library_reference/common/dependencies.rst delete mode 100644 doc/Archive/library_reference/common/deprecation.rst delete mode 100644 doc/Archive/library_reference/common/enums.rst delete mode 100644 doc/Archive/library_reference/common/errors.rst delete mode 100644 doc/Archive/library_reference/common/fileutils.rst delete mode 100644 doc/Archive/library_reference/common/formatting.rst delete mode 100644 doc/Archive/library_reference/common/index.rst delete mode 100644 doc/Archive/library_reference/common/tempfiles.rst delete mode 100644 doc/Archive/library_reference/common/timing.rst delete mode 100644 doc/Archive/library_reference/data/index.rst delete mode 100644 doc/Archive/library_reference/expressions/building.rst delete mode 100644 doc/Archive/library_reference/expressions/classes.rst delete mode 100644 doc/Archive/library_reference/expressions/context_managers.rst delete mode 100644 doc/Archive/library_reference/expressions/index.rst delete mode 100644 doc/Archive/library_reference/expressions/managing.rst delete mode 100644 doc/Archive/library_reference/expressions/visitors.rst delete mode 100644 doc/Archive/library_reference/index.rst delete mode 100644 doc/Archive/library_reference/kernel/base.rst delete mode 100644 doc/Archive/library_reference/kernel/block.rst delete mode 100644 doc/Archive/library_reference/kernel/conic.rst delete mode 100644 doc/Archive/library_reference/kernel/constraint.rst delete mode 100644 doc/Archive/library_reference/kernel/dict_container.rst delete mode 100644 doc/Archive/library_reference/kernel/examples/aml_example.py delete mode 100644 doc/Archive/library_reference/kernel/examples/conic.py delete mode 100644 doc/Archive/library_reference/kernel/examples/kernel_containers.py delete mode 100644 doc/Archive/library_reference/kernel/examples/kernel_example.py delete mode 100644 doc/Archive/library_reference/kernel/examples/kernel_solving.py delete mode 100644 doc/Archive/library_reference/kernel/examples/kernel_subclassing.py delete mode 100644 doc/Archive/library_reference/kernel/examples/transformer.py delete mode 100644 doc/Archive/library_reference/kernel/expression.rst delete mode 100644 doc/Archive/library_reference/kernel/heterogeneous_container.rst delete mode 100644 doc/Archive/library_reference/kernel/homogeneous_container.rst delete mode 100644 doc/Archive/library_reference/kernel/index.rst delete mode 100644 doc/Archive/library_reference/kernel/list_container.rst delete mode 100644 doc/Archive/library_reference/kernel/objective.rst delete mode 100644 doc/Archive/library_reference/kernel/parameter.rst delete mode 100644 doc/Archive/library_reference/kernel/piecewise/index.rst delete mode 100644 doc/Archive/library_reference/kernel/piecewise/piecewise.rst delete mode 100644 doc/Archive/library_reference/kernel/piecewise/piecewise_nd.rst delete mode 100644 doc/Archive/library_reference/kernel/piecewise/util.rst delete mode 100644 doc/Archive/library_reference/kernel/sos.rst delete mode 100644 doc/Archive/library_reference/kernel/suffix.rst delete mode 100644 doc/Archive/library_reference/kernel/syntax_comparison.rst delete mode 100644 doc/Archive/library_reference/kernel/tuple_container.rst delete mode 100644 doc/Archive/library_reference/kernel/variable.rst delete mode 100644 doc/Archive/library_reference/solvers/cplex_persistent.rst delete mode 100644 doc/Archive/library_reference/solvers/gams.rst delete mode 100644 doc/Archive/library_reference/solvers/gurobi_direct.rst delete mode 100644 doc/Archive/library_reference/solvers/gurobi_persistent.rst delete mode 100644 doc/Archive/library_reference/solvers/index.rst delete mode 100644 doc/Archive/library_reference/solvers/xpress_persistent.rst delete mode 100644 doc/Archive/make.bat delete mode 100644 doc/Archive/model_debugging/FAQ.rst delete mode 100644 doc/Archive/model_debugging/getting_help.rst delete mode 100644 doc/Archive/model_debugging/index.rst delete mode 100644 doc/Archive/model_debugging/model_interrogation.rst delete mode 100644 doc/Archive/model_transformations/index.rst delete mode 100644 doc/Archive/model_transformations/scaling.rst delete mode 100644 doc/Archive/modeling_extensions/__init__.py delete mode 100644 doc/Archive/modeling_extensions/bilevel.rst delete mode 100644 doc/Archive/modeling_extensions/dae.rst delete mode 100644 doc/Archive/modeling_extensions/gdp/concepts.rst delete mode 100644 doc/Archive/modeling_extensions/gdp/index.rst delete mode 100644 doc/Archive/modeling_extensions/gdp/modeling.rst delete mode 100644 doc/Archive/modeling_extensions/gdp/solving.rst delete mode 100644 doc/Archive/modeling_extensions/index.rst delete mode 100644 doc/Archive/modeling_extensions/mpec.rst delete mode 100644 doc/Archive/modeling_extensions/network.rst delete mode 100644 doc/Archive/modeling_extensions/reduce_points_demo.png delete mode 100644 doc/Archive/modeling_extensions/stochastic_programming.rst delete mode 100644 doc/Archive/pyomo_modeling_components/Constraints.rst delete mode 100644 doc/Archive/pyomo_modeling_components/Expressions.rst delete mode 100644 doc/Archive/pyomo_modeling_components/Objectives.rst delete mode 100644 doc/Archive/pyomo_modeling_components/Parameters.rst delete mode 100644 doc/Archive/pyomo_modeling_components/Sets.rst delete mode 100644 doc/Archive/pyomo_modeling_components/Suffixes.rst delete mode 100644 doc/Archive/pyomo_modeling_components/Variables.rst delete mode 100644 doc/Archive/pyomo_modeling_components/index.rst delete mode 100644 doc/Archive/pyomo_overview/abstract_concrete.rst delete mode 100644 doc/Archive/pyomo_overview/index.rst delete mode 100644 doc/Archive/pyomo_overview/math_modeling.rst delete mode 100644 doc/Archive/pyomo_overview/overview_components.rst delete mode 100644 doc/Archive/pyomo_overview/simple_examples.rst delete mode 100644 doc/Archive/related_packages.rst delete mode 100644 doc/Archive/solving_pyomo_models.rst delete mode 100644 doc/Archive/tutorial_examples.rst delete mode 100644 doc/Archive/working_abstractmodels/BuildAction.rst delete mode 100755 doc/Archive/working_abstractmodels/data/ABCD.pdf delete mode 100755 doc/Archive/working_abstractmodels/data/ABCD.png delete mode 100644 doc/Archive/working_abstractmodels/data/PP.png delete mode 100644 doc/Archive/working_abstractmodels/data/dataportals.rst delete mode 100644 doc/Archive/working_abstractmodels/data/datfiles.rst delete mode 100644 doc/Archive/working_abstractmodels/data/index.rst delete mode 100644 doc/Archive/working_abstractmodels/data/native.rst delete mode 100644 doc/Archive/working_abstractmodels/data/raw_dicts.rst delete mode 100644 doc/Archive/working_abstractmodels/data/storing_data.rst delete mode 100644 doc/Archive/working_abstractmodels/index.rst delete mode 100644 doc/Archive/working_abstractmodels/instantiating_models.rst delete mode 100644 doc/Archive/working_abstractmodels/pyomo_command.rst delete mode 100644 doc/Archive/working_models.rst diff --git a/doc/Archive/Makefile b/doc/Archive/Makefile deleted file mode 100644 index 3625325ef73..00000000000 --- a/doc/Archive/Makefile +++ /dev/null @@ -1,26 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = Pyomo -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -clean clean_tests: - @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - @echo "Removing *.spy, *.out" - @find . -name \*.spy -delete - @find src -name \*.out -delete diff --git a/doc/Archive/README.md b/doc/Archive/README.md deleted file mode 100644 index a2d4e5997dc..00000000000 --- a/doc/Archive/README.md +++ /dev/null @@ -1,27 +0,0 @@ -Preview Changes Locally ------------------------- - -1. Install Sphinx - - ```bash - $ pip install sphinx sphinx_rtd_theme sphinx_copybutton - ``` - - **NOTE**: You may get a warning about the `dot` command if you do not have - `graphviz` installed. - -1. Build the documentation - - ```bash - $ make html # Option 1 - $ make latexpdf # Option 2 - ``` - -1. View `_build/html/index.html` in your browser - -Test Changes Locally --------------------- - - ```bash - $ make -C doc/OnlineDocs doctest -d # from the pyomo root folder - ``` diff --git a/doc/Archive/_static/theme_overrides.css b/doc/Archive/_static/theme_overrides.css deleted file mode 100644 index 43d48693e03..00000000000 --- a/doc/Archive/_static/theme_overrides.css +++ /dev/null @@ -1,74 +0,0 @@ -/* links and fixed-with literals should NOT be bold */ -.rst-content code { - font-weight: normal !important; -} - -/* internal reference links should be purple (not grey) */ -code.xref.py { - color: #8C1AFF; -} - -/* method names should be bold */ -code.descname { - font-weight: bold !important; - color: black; -} -/* method argument lists should *not* be bold, argument names in black */ -dl.py.method dt { - font-weight: normal; -} -dl.py.method dt em span.n { - color: black; -} - -/* Fix to RTD theme to allow table cell content to wrap */ -@media screen and (min-width: 767px) { - .wy-table-responsive table td { - white-space: normal !important; - } - .wy-table-responsive { - overflow: visible !important; - } -} - -/* Remove space after tables in definition lists (e.g., for function - "Parameters" lists*/ -.rst-content dl div.wy-table-responsive { - margin-bottom: 12px !important; -} - -/* Define a new "tight-table" class that we can use to format tighter - simple banded tables */ -.rst-content table.tight-table { - border-style: solid; - border-collapse: separate !important; -} -.rst-content table.tight-table td { - border-style: hidden !important; - padding-top: 4px !important; - padding-bottom: 4px !important; - padding-left: 8px !important; - padding-right: 8px !important; -} - - -/* OLD theme overrides - -code.docutils.literal{ - color:#8C1AFF; - border: 0px; - background-color:#fcfcfc; - padding:0px; - font-size: 100%; -} - -.wy-table-responsive table td, .wy-table-responsive table th { - white-space: normal; -} - -.wy-table-responsive { - margin-bottom: 24px; - max-width: 100%; - overflow: visible; -} -*/ diff --git a/doc/Archive/advanced_topics/flattener/index.rst b/doc/Archive/advanced_topics/flattener/index.rst deleted file mode 100644 index f9dd8ea6abb..00000000000 --- a/doc/Archive/advanced_topics/flattener/index.rst +++ /dev/null @@ -1,65 +0,0 @@ -"Flattening" a Pyomo model -========================== - -.. autosummary:: - - pyomo.dae.flatten - -.. toctree:: - :maxdepth: 1 - - motivation.rst - reference.rst - -What does it mean to flatten a model? -------------------------------------- -When accessing components in a block-structured model, we use -``component_objects`` or ``component_data_objects`` to access all objects -of a specific ``Component`` or ``ComponentData`` type. -The generated objects may be thought of as a "flattened" representation -of the model, as they may be accessed without any knowledge of the model's -block structure. -These methods are very useful, but it is still challenging to use them -to access specific components. -Specifically, we often want to access "all components indexed by some set," -or "all component data at a particular index of this set." -In addition, we often want to generate the components in a block that -is indexed by our particular set, as these components may be thought of as -"implicitly indexed" by this set. -The ``pyomo.dae.flatten`` module aims to address this use case by providing -utilities to generate all components indexed, explicitly or implicitly, by -user-provided sets. - -**When we say "flatten a model," we mean "recursively generate all components in -the model," where a component can be indexed only by user-specified indexing -sets (or is not indexed at all)**. - -Data structures ---------------- -The components returned are either ``ComponentData`` objects, for components -not indexed by any of the provided sets, or references-to-slices, for -components indexed, explicitly or implicitly, by the provided sets. -Slices are necessary as they can encode "implicit indexing" -- where a -component is contained in an indexed block. It is natural to return references -to these slices, so they may be accessed and manipulated like any other -component. - -Citation --------- -If you use the ``pyomo.dae.flatten`` module in your research, we would appreciate -you citing the following paper, which gives more detail about the motivation for -and examples of using this functinoality. - -.. code-block:: bibtex - - @article{parker2023mpc, - title = {Model predictive control simulations with block-hierarchical differential-algebraic process models}, - journal = {Journal of Process Control}, - volume = {132}, - pages = {103113}, - year = {2023}, - issn = {0959-1524}, - doi = {https://doi.org/10.1016/j.jprocont.2023.103113}, - url = {https://www.sciencedirect.com/science/article/pii/S0959152423002007}, - author = {Robert B. Parker and Bethany L. Nicholson and John D. Siirola and Lorenz T. Biegler}, - } diff --git a/doc/Archive/advanced_topics/flattener/motivation.rst b/doc/Archive/advanced_topics/flattener/motivation.rst deleted file mode 100644 index 046d888a215..00000000000 --- a/doc/Archive/advanced_topics/flattener/motivation.rst +++ /dev/null @@ -1,26 +0,0 @@ -Motivation -========== - -The ``pyomo.dae.flatten`` module was originally developed to assist with -dynamic optimization. A very common operation in dynamic or multi-period -optimization is to initialize all time-indexed variables to their values -at a specific time point. However, for variables indexed by time and -arbitrary other indexing sets, this is difficult to do in a way that does -does not depend on the variable we are initializing. Things get worse -when we consider that a time index can exist on a parent block rather -than the component itself. - -By "reshaping" time-indexed variables in a model into references indexed -only by time, the ``flatten_dae_components`` function allows us to perform -operations that depend on knowledge of time indices without knowing -anything about the variables that we are operating on. - -This "flattened representation" of a model turns out to be useful for -dynamic optimization in a variety of other contexts. Examples include -constructing a tracking objective function and plotting results. -This representation is also useful in cases where we want to preserve -indexing along more than one set, as in PDE-constrained optimization. -The ``flatten_components_along_sets`` function allows partitioning -components while preserving multiple indexing sets. -In such a case, time and space-indexed data for a given variable is useful -for purposes such as initialization, visualization, and stability analysis. diff --git a/doc/Archive/advanced_topics/flattener/reference.rst b/doc/Archive/advanced_topics/flattener/reference.rst deleted file mode 100644 index 22c7b67e1f6..00000000000 --- a/doc/Archive/advanced_topics/flattener/reference.rst +++ /dev/null @@ -1,14 +0,0 @@ -API reference -============= - -.. autosummary:: - - pyomo.dae.flatten.slice_component_along_sets - pyomo.dae.flatten.flatten_components_along_sets - pyomo.dae.flatten.flatten_dae_components - -.. autofunction:: pyomo.dae.flatten.slice_component_along_sets - -.. autofunction:: pyomo.dae.flatten.flatten_components_along_sets - -.. autofunction:: pyomo.dae.flatten.flatten_dae_components diff --git a/doc/Archive/advanced_topics/index.rst b/doc/Archive/advanced_topics/index.rst deleted file mode 100644 index d5293bfa40c..00000000000 --- a/doc/Archive/advanced_topics/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Advanced Topics -=============== - -.. toctree:: - :maxdepth: 1 - - persistent_solvers.rst - units_container.rst - linearexpression.rst - flattener/index.rst - sos_constraints.rst diff --git a/doc/Archive/advanced_topics/linearexpression.rst b/doc/Archive/advanced_topics/linearexpression.rst deleted file mode 100644 index 8b43c3fa03a..00000000000 --- a/doc/Archive/advanced_topics/linearexpression.rst +++ /dev/null @@ -1,44 +0,0 @@ -LinearExpression -================ - -Significant speed -improvements can sometimes be obtained using the ``LinearExpression`` object -when there are long, dense, linear expressions. The arguments are - -:: - - constant, linear_coeffs, linear_vars - -where the second and third arguments are lists that must be of the -same length. Here is a simple example that illustrates the -syntax. This example creates two constraints that are the same; in this -particular case the LinearExpression component would offer very little improvement -because Pyomo would be able to detect that `campe2` is a linear expression: - -.. doctest:: - - >>> import pyomo.environ as pyo - >>> from pyomo.core.expr.numeric_expr import LinearExpression - >>> model = pyo.ConcreteModel() - >>> model.nVars = pyo.Param(initialize=4) - >>> model.N = pyo.RangeSet(model.nVars) - >>> model.x = pyo.Var(model.N, within=pyo.Binary) - >>> - >>> model.coefs = [1, 1, 3, 4] - >>> - >>> model.linexp = LinearExpression(constant=0, - ... linear_coefs=model.coefs, - ... linear_vars=[model.x[i] for i in model.N]) - >>> def caprule(m): - ... return m.linexp <= 6 - >>> model.capme = pyo.Constraint(rule=caprule) - >>> - >>> def caprule2(m): - ... return sum(model.coefs[i-1]*model.x[i] for i in model.N) <= 6 - >>> model.capme2 = pyo.Constraint(rule=caprule2) - - -.. warning:: - - The lists that are passed to ``LinearExpression`` are not copied, so caution must - be exercised if they are modified after the component is constructed. diff --git a/doc/Archive/advanced_topics/persistent_solvers.rst b/doc/Archive/advanced_topics/persistent_solvers.rst deleted file mode 100644 index aebb0545dd0..00000000000 --- a/doc/Archive/advanced_topics/persistent_solvers.rst +++ /dev/null @@ -1,188 +0,0 @@ -Persistent Solvers -================== - -The purpose of the persistent solver interfaces is to efficiently -notify the solver of incremental changes to a Pyomo model. The -persistent solver interfaces create and store model instances from the -Python API for the corresponding solver. For example, the -:class:`GurobiPersistent` -class maintaints a pointer to a gurobipy Model object. Thus, we can -make small changes to the model and notify the solver rather than -recreating the entire model using the solver Python API (or rewriting -an entire model file - e.g., an lp file) every time the model is -solved. - -.. warning:: Users are responsible for notifying persistent solver - interfaces when changes to a model are made! - - -Using Persistent Solvers ------------------------- - -The first step in using a persistent solver is to create a Pyomo model -as usual. - ->>> import pyomo.environ as pe ->>> m = pe.ConcreteModel() ->>> m.x = pe.Var() ->>> m.y = pe.Var() ->>> m.obj = pe.Objective(expr=m.x**2 + m.y**2) ->>> m.c = pe.Constraint(expr=m.y >= -2*m.x + 5) - -You can create an instance of a persistent solver through the SolverFactory. - ->>> opt = pe.SolverFactory('gurobi_persistent') # doctest: +SKIP - -This returns an instance of :py:class:`GurobiPersistent`. Now we need -to tell the solver about our model. - ->>> opt.set_instance(m) # doctest: +SKIP - -This will create a gurobipy Model object and include the appropriate -variables and constraints. We can now solve the model. - ->>> results = opt.solve() # doctest: +SKIP - -We can also add or remove variables, constraints, blocks, and -objectives. For example, - ->>> m.c2 = pe.Constraint(expr=m.y >= m.x) # doctest: +SKIP ->>> opt.add_constraint(m.c2) # doctest: +SKIP - -This tells the solver to add one new constraint but otherwise leave -the model unchanged. We can now resolve the model. - ->>> results = opt.solve() # doctest: +SKIP - -To remove a component, simply call the corresponding remove method. - ->>> opt.remove_constraint(m.c2) # doctest: +SKIP ->>> del m.c2 # doctest: +SKIP ->>> results = opt.solve() # doctest: +SKIP - -If a pyomo component is replaced with another component with the same -name, the first component must be removed from the solver. Otherwise, -the solver will have multiple components. For example, the following -code will run without error, but the solver will have an extra -constraint. The solver will have both y >= -2*x + 5 and y <= x, which -is not what was intended! - ->>> m = pe.ConcreteModel() # doctest: +SKIP ->>> m.x = pe.Var() # doctest: +SKIP ->>> m.y = pe.Var() # doctest: +SKIP ->>> m.c = pe.Constraint(expr=m.y >= -2*m.x + 5) # doctest: +SKIP ->>> opt = pe.SolverFactory('gurobi_persistent') # doctest: +SKIP ->>> opt.set_instance(m) # doctest: +SKIP ->>> # WRONG: ->>> del m.c # doctest: +SKIP ->>> m.c = pe.Constraint(expr=m.y <= m.x) # doctest: +SKIP ->>> opt.add_constraint(m.c) # doctest: +SKIP - -The correct way to do this is: - ->>> m = pe.ConcreteModel() # doctest: +SKIP ->>> m.x = pe.Var() # doctest: +SKIP ->>> m.y = pe.Var() # doctest: +SKIP ->>> m.c = pe.Constraint(expr=m.y >= -2*m.x + 5) # doctest: +SKIP ->>> opt = pe.SolverFactory('gurobi_persistent') # doctest: +SKIP ->>> opt.set_instance(m) # doctest: +SKIP ->>> # Correct: ->>> opt.remove_constraint(m.c) # doctest: +SKIP ->>> del m.c # doctest: +SKIP ->>> m.c = pe.Constraint(expr=m.y <= m.x) # doctest: +SKIP ->>> opt.add_constraint(m.c) # doctest: +SKIP - -.. warning:: Components removed from a pyomo model must be removed - from the solver instance by the user. - -Additionally, unexpected behavior may result if a component is -modified before being removed. - ->>> m = pe.ConcreteModel() # doctest: +SKIP ->>> m.b = pe.Block() # doctest: +SKIP ->>> m.b.x = pe.Var() # doctest: +SKIP ->>> m.b.y = pe.Var() # doctest: +SKIP ->>> m.b.c = pe.Constraint(expr=m.b.y >= -2*m.b.x + 5) # doctest: +SKIP ->>> opt = pe.SolverFactory('gurobi_persistent') # doctest: +SKIP ->>> opt.set_instance(m) # doctest: +SKIP ->>> m.b.c2 = pe.Constraint(expr=m.b.y <= m.b.x) # doctest: +SKIP ->>> # ERROR: The constraint referenced by m.b.c2 does not ->>> # exist in the solver model. ->>> opt.remove_block(m.b) # doctest: +SKIP - -In most cases, the only way to modify a component is to remove it from -the solver instance, modify it with Pyomo, and then add it back to the -solver instance. The only exception is with variables. Variables may -be modified and then updated with with solver: - ->>> m = pe.ConcreteModel() # doctest: +SKIP ->>> m.x = pe.Var() # doctest: +SKIP ->>> m.y = pe.Var() # doctest: +SKIP ->>> m.obj = pe.Objective(expr=m.x**2 + m.y**2) # doctest: +SKIP ->>> m.c = pe.Constraint(expr=m.y >= -2*m.x + 5) # doctest: +SKIP ->>> opt = pe.SolverFactory('gurobi_persistent') # doctest: +SKIP ->>> opt.set_instance(m) # doctest: +SKIP ->>> m.x.setlb(1.0) # doctest: +SKIP ->>> opt.update_var(m.x) # doctest: +SKIP - -Working with Indexed Variables and Constraints ----------------------------------------------- - -The examples above all used simple variables and constraints; in order to use -indexed variables and/or constraints, the code must be slightly adapted: - ->>> for v in indexed_var.values(): # doctest: +SKIP -... opt.add_var(v) ->>> for v in indexed_con.values(): # doctest: +SKIP -... opt.add_constraint(v) - -This must be done when removing variables/constraints, too. Not doing this would -result in AttributeError exceptions, for example: - ->>> opt.add_var(indexed_var) # doctest: +SKIP ->>> # ERROR: AttributeError: 'IndexedVar' object has no attribute 'is_binary' ->>> opt.add_constraint(indexed_con) # doctest: +SKIP ->>> # ERROR: AttributeError: 'IndexedConstraint' object has no attribute 'body' - -The method "is_indexed" can be used to automate the process, for example: - ->>> def add_variable(opt, variable): # doctest: +SKIP -... if variable.is_indexed(): -... for v in variable.values(): -... opt.add_var(v) -... else: -... opt.add_var(v) - -Persistent Solver Performance ------------------------------ -In order to get the best performance out of the persistent solvers, use the -"save_results" flag: - ->>> import pyomo.environ as pe ->>> m = pe.ConcreteModel() ->>> m.x = pe.Var() ->>> m.y = pe.Var() ->>> m.obj = pe.Objective(expr=m.x**2 + m.y**2) ->>> m.c = pe.Constraint(expr=m.y >= -2*m.x + 5) ->>> opt = pe.SolverFactory('gurobi_persistent') # doctest: +SKIP ->>> opt.set_instance(m) # doctest: +SKIP ->>> results = opt.solve(save_results=False) # doctest: +SKIP - -Note that if the "save_results" flag is set to False, then the following -is not supported. - ->>> results = opt.solve(save_results=False, load_solutions=False) # doctest: +SKIP ->>> if results.solver.termination_condition == TerminationCondition.optimal: -... m.solutions.load_from(results) # doctest: +SKIP - -However, the following will work: - ->>> results = opt.solve(save_results=False, load_solutions=False) # doctest: +SKIP ->>> if results.solver.termination_condition == TerminationCondition.optimal: -... opt.load_vars() # doctest: +SKIP - -Additionally, a subset of variable values may be loaded back into the model: - ->>> results = opt.solve(save_results=False, load_solutions=False) # doctest: +SKIP ->>> if results.solver.termination_condition == TerminationCondition.optimal: -... opt.load_vars(m.x) # doctest: +SKIP diff --git a/doc/Archive/advanced_topics/sos_constraints.rst b/doc/Archive/advanced_topics/sos_constraints.rst deleted file mode 100644 index b536b3f0b26..00000000000 --- a/doc/Archive/advanced_topics/sos_constraints.rst +++ /dev/null @@ -1,288 +0,0 @@ -Special Ordered Sets (SOS) -========================== - -Pyomo allows users to declare special ordered sets (SOS) within their problems. -These are sets of variables among which only a certain number of variables can -be non-zero, and those that are must be adjacent according to a given order. - -Special ordered sets of types 1 (SOS1) and 2 (SOS2) are the classic ones, but -the concept can be generalised: a SOS of type N cannot have more than N of its -members taking non-zero values, and those that do must be adjacent in the set. -These can be useful for modelling and computational performance purposes. - -By explicitly declaring these, users can keep their formulations and respective -solving times shorter than they would otherwise, since the logical constraints -that enforce the SOS do not need to be implemented within the model and are -instead (ideally) handled algorithmically by the solver. - -Special ordered sets can be declared one by one or indexed via other sets. - -Non-indexed Special Ordered Sets --------------------------------- - -A single SOS of type **N** involving all members of a pyomo Var component can -be declared in one line: - -.. currentmodule:: pyomo.environ - -.. testcode:: - - # import pyomo - import pyomo.environ as pyo - # declare the model - model = pyo.AbstractModel() - # the type of SOS - N = 1 # or 2, 3, ... - # the set that indexes the variables - model.A = pyo.Set() - # the variables under consideration - model.x = pyo.Var(model.A) - # the sos constraint - model.mysos = pyo.SOSConstraint(var=model.x, sos=N) - -In the example above, the weight of each variable is determined automatically -based on their position/order in the pyomo Var component (``model.x``). - -Alternatively, the weights can be specified through a pyomo Param component -(``model.mysosweights``) indexed by the set also indexing the variables -(``model.A``): - -.. doctest:: - :hide: - - >>> model = pyo.AbstractModel() - -.. testcode:: - - # the set that indexes the variables - model.A = pyo.Set() - # the variables under consideration - model.x = pyo.Var(model.A) - # the weights for each variable used in the sos constraints - model.mysosweights = pyo.Param(model.A) - # the sos constraint - model.mysos = pyo.SOSConstraint( - var=model.x, - sos=N, - weights=model.mysosweights - ) - -Indexed Special Ordered Sets ----------------------------- - -Multiple SOS of type **N** involving members of a pyomo Var component -(``model.x``) can be created using two additional sets (``model.A`` and -``model.mysosvarindexset``): - -.. doctest:: - :hide: - - >>> model = pyo.AbstractModel() - -.. testcode:: - - # the set that indexes the variables - model.A = pyo.Set() - # the variables under consideration - model.x = pyo.Var(model.A) - # the set indexing the sos constraints - model.B = pyo.Set() - # the sets containing the variable indexes for each constraint - model.mysosvarindexset = pyo.Set(model.B) - # the sos constraints - model.mysos = pyo.SOSConstraint( - model.B, - var=model.x, - sos=N, - index=model.mysosvarindexset - ) - -In the example above, the weights are determined automatically from the -position of the variables. Alternatively, they can be specified through a pyomo -Param component (``model.mysosweights``) and an additional set (``model.C``): - -.. doctest:: - :hide: - - >>> model = pyo.AbstractModel() - -.. testcode:: - - # the set that indexes the variables - model.A = pyo.Set() - # the variables under consideration - model.x = pyo.Var(model.A) - # the set indexing the sos constraints - model.B = pyo.Set() - # the sets containing the variable indexes for each constraint - model.mysosvarindexset = pyo.Set(model.B) - # the set that indexes the variables used in the sos constraints - model.C = pyo.Set(within=model.A) - # the weights for each variable used in the sos constraints - model.mysosweights = pyo.Param(model.C) - # the sos constraints - model.mysos = pyo.SOSConstraint( - model.B, - var=model.x, - sos=N, - index=model.mysosvarindexset, - weights=model.mysosweights, - ) - -Declaring Special Ordered Sets using rules ------------------------------------------- - -Arguably the best way to declare an SOS is through rules. This option allows -users to specify the variables and weights through a method provided via the -``rule`` parameter. If this parameter is used, users must specify a method that -returns one of the following options: - -- a list of the variables in the SOS, whose respective weights are then determined based on their position; - -- a tuple of two lists, the first for the variables in the SOS and the second for the respective weights; - -- or, pyomo.environ.SOSConstraint.Skip, if the SOS is not to be declared. - -If one is content on having the weights determined based on the position of the -variables, then the following example using the ``rule`` parameter is sufficient: - -.. doctest:: - :hide: - - >>> model = pyo.AbstractModel() - -.. testcode:: - - # the set that indexes the variables - model.A = pyo.Set() - # the variables under consideration - model.x = pyo.Var(model.A, domain=pyo.NonNegativeReals) - # the rule method creating the constraint - def rule_mysos(m): - return [m.x[a] for a in m.x] - # the sos constraint(s) - model.mysos = pyo.SOSConstraint(rule=rule_mysos, sos=N) - - -If the weights must be determined in some other way, then the following example -illustrates how they can be specified for each member of the SOS using the ``rule`` parameter: - -.. doctest:: - :hide: - - >>> model = pyo.AbstractModel() - -.. testcode:: - - # the set that indexes the variables - model.A = pyo.Set() - # the variables under consideration - model.x = pyo.Var(model.A, domain=pyo.NonNegativeReals) - # the rule method creating the constraint - def rule_mysos(m): - var_list = [m.x[a] for a in m.x] - weight_list = [i+1 for i in range(len(var_list))] - return (var_list, weight_list) - # the sos constraint(s) - model.mysos = pyo.SOSConstraint(rule=rule_mysos, sos=N) - -The ``rule`` parameter also allows users to create SOS comprising variables -from different pyomo Var components, as shown below: - -.. doctest:: - :hide: - - >>> model = pyo.AbstractModel() - -.. testcode:: - - # the set that indexes the x variables - model.A = pyo.Set() - # the set that indexes the y variables - model.B = pyo.Set() - # the set that indexes the SOS constraints - model.C = pyo.Set() - # the x variables, which will be used in the constraints - model.x = pyo.Var(model.A, domain=pyo.NonNegativeReals) - # the y variables, which will be used in the constraints - model.y = pyo.Var(model.B, domain=pyo.NonNegativeReals) - # the x variable indices for each constraint - model.mysosindex_x = pyo.Set(model.C) - # the y variable indices for each constraint - model.mysosindex_y = pyo.Set(model.C) - # the weights for the x variable indices - model.mysosweights_x = pyo.Param(model.A) - # the weights for the y variable indices - model.mysosweights_y = pyo.Param(model.B) - # the rule method with which each constraint c is built - def rule_mysos(m, c): - var_list = [m.x[a] for a in m.mysosindex_x[c]] - var_list.extend([m.y[b] for b in m.mysosindex_y[c]]) - weight_list = [m.mysosweights_x[a] for a in m.mysosindex_x[c]] - weight_list.extend([m.mysosweights_y[b] for b in m.mysosindex_y[c]]) - return (var_list, weight_list) - # the sos constraint(s) - model.mysos = pyo.SOSConstraint( - model.C, - rule=rule_mysos, - sos=N - ) - -Compatible solvers ------------------- - -Not all LP/MILP solvers are compatible with SOS declarations and Pyomo might -not be ready to interact with all those that are. The following is a list of -solvers known to be compatible with special ordered sets through Pyomo: - -- CBC -- SCIP -- Gurobi -- CPLEX - -Please note that declaring an SOS is no guarantee that a solver will use it as -such in the end. Some solvers, namely Gurobi and CPLEX, might reformulate -problems with explicit SOS declarations, if they perceive that to be useful. - -Full example with non-indexed SOS constraint --------------------------------------------- - -.. doctest:: - :hide: - - >>> model = pyo.AbstractModel() - -.. testcode:: - - import pyomo.environ as pyo - from pyomo.opt import check_available_solvers - from math import isclose - N = 1 - model = pyo.ConcreteModel() - model.x = pyo.Var([1], domain=pyo.NonNegativeReals, bounds=(0,40)) - model.A = pyo.Set(initialize=[1,2,4,6]) - model.y = pyo.Var(model.A, domain=pyo.NonNegativeReals, bounds=(0,2)) - model.OBJ = pyo.Objective( - expr=(1*model.x[1]+ - 2*model.y[1]+ - 3*model.y[2]+ - -0.1*model.y[4]+ - 0.5*model.y[6]) - ) - model.ConstraintYmin = pyo.Constraint( - expr = (model.x[1]+ - model.y[1]+ - model.y[2]+ - model.y[6] >= 0.25 - ) - ) - model.mysos = pyo.SOSConstraint( - var=model.y, - sos=N - ) - solver_name = 'scip' - solver_available = bool(check_available_solvers(solver_name)) - if solver_available: - opt = pyo.SolverFactory(solver_name) - opt.solve(model, tee=False) - assert isclose(pyo.value(model.OBJ), 0.05, abs_tol=1e-3) diff --git a/doc/Archive/advanced_topics/units_container.rst b/doc/Archive/advanced_topics/units_container.rst deleted file mode 100644 index f09a3361b6b..00000000000 --- a/doc/Archive/advanced_topics/units_container.rst +++ /dev/null @@ -1,13 +0,0 @@ -Units Handling in Pyomo -======================= - -.. automodule:: pyomo.core.base.units_container - -.. autoclass:: PyomoUnitsContainer - :show-inheritance: - :members: - -.. autoclass:: UnitsError - -.. autoclass:: InconsistentUnitsError - diff --git a/doc/Archive/bibliography.rst b/doc/Archive/bibliography.rst deleted file mode 100644 index c12d3f81d8c..00000000000 --- a/doc/Archive/bibliography.rst +++ /dev/null @@ -1,68 +0,0 @@ -Bibliography -============ - -.. [AMPL] R. Fourer, D. M. Gay, and B. W. Kernighan. AMPL: A Modeling - Language for Mathematical Programming, 2nd Edition. Duxbury - Press, 2002. - -.. [AIMMS] http://www.aimms.com/ - -.. [GAMS] http://www.gams.com - -.. [Isenberg_et_al] Isenberg, NM, Akula, P, Eslick, JC, Bhattacharyya, D, - Miller, DC, Gounaris, CE. A generalized cutting‐set approach for - nonlinear robust optimization in process systems - engineering. AIChE J. 2021; 67:e17175. DOI `10.1002/aic.17175 - `_ - -.. [mpisppy] Bernard Knueven, David Mildebrath, Christopher Muir, - John D Siirola, Jean-Paul Watson, and David L Woodruff, A Parallel - Hub-and-Spoke System for Large-Scale Scenario-Based Optimization - Under Uncertainty, pre-print, 2020 - -.. [ParmestPaper] Katherine A. Klise, Bethany L. Nicholson, Andrea - Staid, David L.Woodruff. Parmest: Parameter Estimation Via Pyomo. - Computer Aided Chemical Engineering, 47 (2019): 41-46. - -.. [PyomoBookI] William E. Hart, Carl D. Laird, Jean-Paul Watson, - David L. Woodruff. Pyomo – Optimization Modeling in - Python, Springer, 2012. - -.. [PyomoBookII] W. E. Hart, C. D. Laird, - J.-P. Watson, D. L. Woodruff, G. A. Hackebeil, B. L. Nicholson, - J. D. Siirola. Pyomo - Optimization Modeling in Python, - 2nd Edition. Springer Optimization and Its - Applications, Vol 67. Springer, 2017. - -.. [PyomoBookIII] Bynum, Michael L., Gabriel A. Hackebeil, - William E. Hart, Carl D. Laird, Bethany L. Nicholson, - John D. Siirola, Jean-Paul Watson, and David L. Woodruff. - Pyomo - Optimization Modeling in Python, 3rd Edition. - Vol. 67. Springer, 2021. - doi: `10.1007/978-3-030-68928-5 - `_ - -.. [PyomoJournal] William E. Hart, Jean-Paul Watson, David L. Woodruff. - "Pyomo: modeling and solving mathematical programs in - Python," Mathematical Programming Computation, Volume - 3, Number 3, August 2011 - -.. [PyomoDAE] Bethany Nicholson, John D. Siirola, Jean-Paul Watson, - Victor M. Zavala, and Lorenz T. Biegler. "pyomo.dae: a - modeling and automatic discretization framework for - optimization with differential and algebraic equations." - Mathematical Programming Computation 10(2) (2018): - 187-223. - -.. [RooneyBiegler] W.C. Rooney, L.T. Biegler, "Design for model parameter - uncertainty using nonlinear confidence regions", AIChE - Journal, 47(8), 2001 - -.. [SemiBatch] O. Abel, W. Marquardt, "Scenario-integrated modeling and - optimization of dynamic systems", AIChE Journal, 46(4), 2000 - -.. [Vielma_et_al] J. P. Vielma, S. Ahmed, G. Nemhauser. "Mixed-Integer - Models for Non-separable Piecewise Linear - Optimization: Unifying framework and Extensions", - Operations Research 58, 2010. pp. 303-315. - diff --git a/doc/Archive/citing_pyomo.rst b/doc/Archive/citing_pyomo.rst deleted file mode 100644 index 458a1fe6ab7..00000000000 --- a/doc/Archive/citing_pyomo.rst +++ /dev/null @@ -1,16 +0,0 @@ -Citing Pyomo -============ - -Pyomo ------ - -Bynum, Michael L., Gabriel A. Hackebeil, William E. Hart, Carl D. Laird, Bethany L. Nicholson, John D. Siirola, Jean-Paul Watson, and David L. Woodruff. Pyomo - Optimization Modeling in Python, 3rd Edition. Springer, 2021. - -Hart, William E., Jean-Paul Watson, and David L. Woodruff. "Pyomo: modeling and solving mathematical programs in Python." Mathematical Programming Computation 3, no. 3 (2011): 219-260. - - -PySP ----- - -Watson, Jean-Paul, David L. Woodruff, and William E. Hart. "PySP: modeling and solving stochastic programs in Python." Mathematical Programming Computation 4, no. 2 (2012): 109-149. - diff --git a/doc/Archive/conf.py b/doc/Archive/conf.py deleted file mode 100644 index a06ccfbc9bd..00000000000 --- a/doc/Archive/conf.py +++ /dev/null @@ -1,304 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# pyomo documentation build configuration file, created by -# sphinx-quickstart on Mon Dec 12 16:08:36 2016. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import os -import sys - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# assumes pyutilib source is next to the pyomo source directory -sys.path.insert(0, os.path.abspath('../../../pyutilib')) -# top-level pyomo source directory -sys.path.insert(0, os.path.abspath('../..')) - -# -- Rebuild SPY files ---------------------------------------------------- -sys.path.insert(0, os.path.abspath('src')) -try: - print("Regenerating SPY files...") - from strip_examples import generate_spy_files - - generate_spy_files(os.path.abspath('src')) - generate_spy_files( - os.path.abspath(os.path.join('library_reference', 'kernel', 'examples')) - ) -finally: - sys.path.pop(0) - -# -- Options for intersphinx --------------------------------------------- - -intersphinx_mapping = { - 'python': ('https://docs.python.org/3', None), - 'matplotlib': ('https://matplotlib.org/stable/', None), - 'numpy': ('https://numpy.org/doc/stable/', None), - 'pandas': ('https://pandas.pydata.org/docs/', None), - 'scikit-learn': ('https://scikit-learn.org/stable/', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/', None), - 'Sphinx': ('https://www.sphinx-doc.org/en/master/', None), -} - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -needs_sphinx = '1.8' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.intersphinx', - 'sphinx.ext.autodoc', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', - 'sphinx.ext.napoleon', - 'sphinx.ext.ifconfig', - 'sphinx.ext.inheritance_diagram', - 'sphinx.ext.autosummary', - 'sphinx.ext.doctest', - 'sphinx.ext.todo', - 'sphinx_copybutton', - 'enum_tools.autoenum', - 'sphinx.ext.autosectionlabel', - #'sphinx.ext.githubpages', -] - -viewcode_follow_imported_members = True -# napoleon_include_private_with_doc = True - -copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " -copybutton_prompt_is_regexp = True - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'Pyomo' -copyright = u'2008-2023, Sandia National Laboratories' -author = u'Pyomo Developers' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -import pyomo.version - -version = pyomo.version.__version__ -# The full version, including alpha/beta/rc tags. -release = pyomo.version.__version__ - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = "en" - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True - -# If true, doctest flags (comments looking like # doctest: FLAG, ...) at -# the ends of lines and markers are removed for all code -# blocks showing interactive Python sessions (i.e. doctests) -trim_doctest_flags = True - -# If true, figures, tables and code-blocks are automatically numbered if -# they have a caption. -numfig = True - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -# html_theme = 'alabaster' -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' - -html_theme = 'sphinx_rtd_theme' - -if not on_rtd: # only import and set the theme if we're building docs locally - import sphinx_rtd_theme - - html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] -html_css_files = ['theme_overrides.css'] - -html_favicon = "../logos/pyomo/favicon.ico" - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'pyomo' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [(master_doc, 'pyomo.tex', 'Pyomo Documentation', 'Pyomo', 'manual')] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [(master_doc, 'pyomo', 'Pyomo Documentation', [author], 1)] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - 'pyomo', - 'Pyomo Documentation', - author, - 'Pyomo', - 'One line description of project.', - 'Miscellaneous', - ) -] - -# autodoc_member_order = 'bysource' -# autodoc_member_order = 'groupwise' - -# -- Check which conditional dependencies are available ------------------ -# Used for skipping certain doctests -from sphinx.ext.doctest import doctest - -doctest_default_flags = ( - doctest.ELLIPSIS - + doctest.NORMALIZE_WHITESPACE - + doctest.IGNORE_EXCEPTION_DETAIL - + doctest.DONT_ACCEPT_TRUE_FOR_1 -) - - -class IgnoreResultOutputChecker(doctest.OutputChecker): - IGNORE_RESULT = doctest.register_optionflag('IGNORE_RESULT') - - def check_output(self, want, got, optionflags): - if optionflags & self.IGNORE_RESULT: - return True - return super().check_output(want, got, optionflags) - - -doctest.OutputChecker = IgnoreResultOutputChecker - -doctest_global_setup = ''' -import os, platform, sys -on_github_actions = bool(os.environ.get('GITHUB_ACTIONS', '')) -system_info = ( - sys.platform, - platform.machine(), - platform.python_implementation() -) - -from pyomo.common.dependencies import ( - attempt_import, numpy_available, scipy_available, pandas_available, - yaml_available, networkx_available, matplotlib_available, - pympler_available, dill_available, -) -pint_available = attempt_import('pint', defer_import=False)[1] -from pyomo.contrib.parmest.parmest import parmest_available - -import pyomo.environ as _pe # (trigger all plugin registrations) -import pyomo.opt as _opt - -# Not using SolverFactory to check solver availability because -# as of June 2020 there is no way to suppress warnings when -# solvers are not available -ipopt_available = bool(_opt.check_available_solvers('ipopt')) -sipopt_available = bool(_opt.check_available_solvers('ipopt_sens')) -k_aug_available = bool(_opt.check_available_solvers('k_aug')) -dot_sens_available = bool(_opt.check_available_solvers('dot_sens')) -baron_available = bool(_opt.check_available_solvers('baron')) -glpk_available = bool(_opt.check_available_solvers('glpk')) -gurobipy_available = bool(_opt.check_available_solvers('gurobi_direct')) - -baron = _opt.SolverFactory('baron') - -if numpy_available and scipy_available: - import pyomo.contrib.pynumero.asl as _asl - asl_available = _asl.AmplInterface.available() - import pyomo.contrib.pynumero.linalg.ma27 as _ma27 - ma27_available = _ma27.MA27Interface.available() - from pyomo.contrib.pynumero.linalg.mumps_interface import mumps_available -else: - asl_available = False - ma27_available = False - mumps_available = False -''' diff --git a/doc/Archive/contributed_packages/alternative_solutions.rst b/doc/Archive/contributed_packages/alternative_solutions.rst deleted file mode 100644 index cc5ab07c3cc..00000000000 --- a/doc/Archive/contributed_packages/alternative_solutions.rst +++ /dev/null @@ -1,107 +0,0 @@ -############################################### -Generating Alternative (Near-)Optimal Solutions -############################################### - -Optimization solvers are generally designed to return a feasible solution -to the user. However, there are many applications where a user needs -more context than this result. For example, - -* alternative solutions can support an assessment of trade-offs between competing objectives; - -* if the optimization formulation may be inaccurate or untrustworthy, then comparisons amongst alternative solutions provide additional insights into the reliability of these model predictions; or - -* the user may have unexpressed objectives or constraints, which only are realized in later stages of model analysis. - -The *alternative-solutions library* provides a variety of functions that -can be used to generate optimal or near-optimal solutions for a pyomo -model. Conceptually, these functions are like pyomo solvers. They can -be configured with solver names and options, and they return a list of -solutions for the pyomo model. However, these functions are independent -of pyomo's solver interface because they return a custom solution object. - -The following functions are defined in the alternative-solutions library: - -* ``enumerate_binary_solutions`` - - * Finds alternative optimal solutions for a binary problem using no-good cuts. - -* ``enumerate_linear_solutions`` - - * Finds alternative optimal solutions for a (mixed-integer) linear program. - -* ``enumerate_linear_solutions_soln_pool`` - - * Finds alternative optimal solutions for a (mixed-binary) linear program using Gurobi's solution pool feature. - -* ``gurobi_generate_solutions`` - - * Finds alternative optimal solutions for discrete variables using Gurobi's built-in solution pool capability. - -* ``obbt_analysis_bounds_and_solutions`` - - * Calculates the bounds on each variable by solving a series of min and max optimization problems where each variable is used as the objective function. This can be applied to any class of problem supported by the selected solver. - - -Usage Example -------------- - -Many of functions in the alternative-solutions library have similar options, so we simply illustrate the ``enumerate_binary_solutions`` function. We define a simple knapsack example whose alternative solutions have integer objective values ranging from 0 to 90. - -.. doctest:: - - >>> import pyomo.environ as pyo - - >>> values = [10, 40, 30, 50] - >>> weights = [5, 4, 6, 3] - >>> capacity = 10 - - >>> m = pyo.ConcreteModel() - >>> m.x = pyo.Var(range(4), within=pyo.Binary) - >>> m.o = pyo.Objective(expr=sum(values[i] * m.x[i] for i in range(4)), sense=pyo.maximize) - >>> m.c = pyo.Constraint(expr=sum(weights[i] * m.x[i] for i in range(4)) <= capacity) - -We can execute the ``enumerate_binary_solutions`` function to generate a list of ``Solution`` objects that represent alternative optimal solutions: - -.. doctest:: - :skipif: not glpk_available - - >>> import pyomo.contrib.alternative_solutions as aos - >>> solns = aos.enumerate_binary_solutions(m, num_solutions=100, solver="glpk") - >>> assert len(solns) == 10 - -Each ``Solution`` object contains information about the objective and variables, and it includes various methods to access this information. For example: - -.. doctest:: - :skipif: not glpk_available - - >>> print(solns[0]) - { - "fixed_variables": [], - "objective": "o", - "objective_value": 90.0, - "solution": { - "x[0]": 0, - "x[1]": 1, - "x[2]": 0, - "x[3]": 1 - } - } - - -Interface Documentation ------------------------ - -.. currentmodule:: pyomo.contrib.alternative_solutions - -.. autofunction:: enumerate_binary_solutions - -.. autofunction:: enumerate_linear_solutions - -.. autofunction:: enumerate_linear_solutions_soln_pool - -.. autofunction:: gurobi_generate_solutions - -.. autofunction:: obbt_analysis_bounds_and_solutions - -.. autoclass:: Solution - diff --git a/doc/Archive/contributed_packages/communities_8pp.png b/doc/Archive/contributed_packages/communities_8pp.png deleted file mode 100644 index a9bbd9fd58e473aae274e0147f013c17998f30c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 256159 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

0=L@0<5uw#A1c<0YNINF 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

_`{QW+isNyjuX2*DBUh($>|CXri{ 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 LyR(1#<>LPU(BzZx diff --git a/doc/Archive/contributed_packages/communities_decode_1.png b/doc/Archive/contributed_packages/communities_decode_1.png deleted file mode 100644 index 7432f3f9178690b19a6972dfb1317ed27caf4fb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157034 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
=W{gt%@^^{jg36NKm8fpas4V{%-qauVpB* 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 cNzexl$Yf6@rYe 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≷2>&XUaPA~qlK&!?WaKC0347nB5v&rlFol<^bC2d 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
se)%M zh~LZ>MTYkF%1Y|ACjY)*p}l2&Z>Z*G>5lY>%`#%&Izo{o_I<*A^9uw1kV$DeM;)o? zdX;blcpg4x8?0&`u3YsYm~*>-aayElmQ=(G3(TBv0}bhLIXZ1vv+Z(>Z+fW-_!reD zr~wpne*FP($_DD?uxOTHc3+%``4Uw_bE#9n6R)hur?3#k(>hdAsRF*MWa!h2>iBSf zOKtJ-=VT2xFk*W;!umi2bId`|K;@qTn}svXJI9(U2qm@$tZSJ&)`9+ad*>xeQY6#ArToV%>%MvKL+zObF{4(3gyGZ13&5ye09PTGO+B zYDmz^Wj*0oL{FK3P9F>(FZn$1W)v>VBoXT`IjRgC_4Df&0skgXEKPT$#o9 zLd=lf1^fANk=iu=bQ&>3Z-&^D%%5y`S33N0&uAuX#)h;>o!`;zFZJ2J$~16hWE-8M zjg#IM9OeHQ=|YLH(@E;q{`L-;**< zO*7{xX`UodP#>vz6O_lh7qt=JaS$hSYu2M;Yq==u-mfVlDFv}#_;zxCX2uH`$qevX zY3bW>dLc^Vi2o+SjDVY_Hg)UHvhV5VlV~5^+jLw@r+cAEDJP(}|J+g9l)@>(s9o%<$AW6neI~ppZgK>uBtm z1VLhRtGi(wQt18|qGxb0uP*uIW5kpL8p2mL%-1iSDdb4O*6C_TxDm6e7Rotn!m zX-AGnbHONutYsip4vr_cL0zQbg8eqUDJ?ZxOJ{qOH|AHThTwKecd~R4VN=N9JBAzS zuIpz{WMOO~;MwC`%!nKTpEINJ^C{(4dmfe0FJL<#zgFee8P}zE1vxko0{j0aTF6PG zjg{N*JrvwG^IpDYI*h)qXogOLAG;-JNzoVy*S1()M9>g*U&Y@}Kjc`nQ}vZ)(M^~= z+v{K;#T2&%KE-Ri^3Wh?b}iIWz?-Y5U5U@r*H}89p3yY{Ct;Sp_N}m42C1A-w)QCF zAqna({~@9#%s}zX@`}G5%?qV*mhpV2CwmL#g_n*&Te|taT_Ct$I{ZHbtSH- zh&h?ocR&-VZ6Zb<-yT|Y`18{&h*_7+=ws1aiY~Kv?_27Llr|$S^?0Ea?gfm`Z|_PdH%zIy=?`%|c{VjZm{6*<)IFh}!XMGqU-kwT(Q8vh z+=9!_x8Cse9sa!{4ubjH+T#E1xiDzFHLWPxt~&nHXuZjSMK;8H+j4ua4K1w~zGA2* zliU!lZMNgbked((xrttJ+5_GUX0g8PY(qhr8?%Jd&(?QG{uk_dHQaY`_)Yb;37DVo zCun&`yw*^Z(~ZS}z$WlyiPJiyn`QMgL6fbT&gK`(>>$nzXngX{nOmh9<{ za@S_gnRBrQJ<;L1U<_YvG#2K}>PlE_mMG$c{rM+RaJ)pEG16)l+;>g9#g$UirtY0w|5z(K zdG)e+upb^;1srN1KhTqVJSIn0j9>l7z{u92|>sw zs2&Cc3Emp{lf69x&cf9@tEXFlyb4zW`~4 BP`LmA diff --git a/doc/Archive/contributed_packages/community.rst b/doc/Archive/contributed_packages/community.rst deleted file mode 100644 index b110107e604..00000000000 --- a/doc/Archive/contributed_packages/community.rst +++ /dev/null @@ -1,389 +0,0 @@ -Community Detection for Pyomo models -==================================== - -This package separates model components (variables, constraints, and objectives) into different communities -distinguished by the degree of connectivity between community members. - -Description of Package and ``detect_communities`` function ----------------------------------------------------------- -The community detection package allows users to obtain a community map of a Pyomo model - a Python dictionary-like -object that maps sequential integer values to communities within the Pyomo model. The package -takes in a model, organizes the model components into a graph of nodes and edges, then uses Louvain -community detection (`Blondel et al, 2008`_) to determine the communities that exist within the model. - -.. _Blondel et al, 2008: https://dx.doi.org/10.1088/1742-5468/2008/10/P10008 - -In graph theory, a community is defined as a subset of nodes that have a greater degree of connectivity within -themselves than they do with the rest of the nodes in the graph. In the context of Pyomo models, a community -represents a subproblem within the overall optimization problem. Identifying these subproblems and then solving them -independently can save computational work compared with trying to solve the entire model at once. Thus, it -can be very useful to know the communities that exist in a model. - -The manner in which the graph of nodes and edges is constructed from the model directly affects the community -detection. Thus, this package provides the user with a lot of control over the construction of the graph. The -function we use for this community detection is shown below: - -.. autofunction:: pyomo.contrib.community_detection.detection.detect_communities - :noindex: - -As stated above, the characteristics of the NetworkX graph of the Pyomo model are very important to the -community detection. The main graph features the user can specify are the type of community map, -whether the graph is weighted or unweighted, and whether the objective function(s) is included -in the graph generation. Below, the significance and reasoning behind including each of these options are -explained in greater depth. - -Type of Community Map (`type_of_community_map`) - In this package's main function (``detect_communities``), the user can select ``'bipartite'``, ``'constraint'``, - or ``'variable'`` as an input for the 'type_of_community_map' argument, and these result in a community map - based on a bipartite graph, a constraint node graph, or a variable node graph (respectively). - - If the user sets ``type_of_community_map='constraint'``, then each entry in the community map (which is a dictionary) contains - a list of all the constraints in the community as well as all the variables contained in those constraints. - For the model graph, a node is created for every active constraint in the model, an edge between two - constraint nodes is created only if those two constraint equations share a variable, and the - weight of each edge is equal to the number of variables the two constraint equations have in common. - - If the user sets ``type_of_community_map='variable'``, then each entry in the community map (which is a dictionary) contains - a list of all the variables in the community as well as all the constraints that contain those variables. - For the model graph, a node is created for every variable in the model, an edge between two variable nodes is - created only if those two variables occur in the same constraint equation, and the weight of each edge is equal - to the number of constraint equations in which the two variables occur together. - - If the user sets ``type_of_community_map='bipartite'``, then each entry in the community map (which is a dictionary) is - simply all of the nodes in the community but split into a list of constraints and a list of variables. - For the model graph, a node is created for every variable and every constraint in the model. An edge is created - between a constraint node and a variable node only if the constraint equation contains the variable. (Edges are - not drawn between nodes of the same type in a bipartite graph.) And as for the edge weights, the edges in the - bipartite graph are unweighted regardless of what the user specifies for the ``weighted_graph`` parameter. (This is - because for our purposes, the number of times a variable appears in a constraint is not particularly - useful.) - -Weighted Graph/Unweighted Graph (`weighted_graph`) - The Louvain community detection algorithm takes edge weights into account, so depending on whether the graph is - weighted or unweighted, the communities that are found will vary. This can be valuable depending on how - the user intends to use the community detection information. For example, if a user plans on feeding that - information into an algorithm, the algorithm may be better suited to the communities detected in a weighted - graph (or vice versa). - -With/Without Objective in the Graph (`with_objective`) - This argument determines whether the objective function(s) will be included when creating the graphical - representation of the model and thus whether the objective function(s) will be included in the community map. - Some models have an objective function that contains so many of the model variables that it obscures potential - communities within a model. Thus, it can be useful to call ``detect_communities(model, with_objective=False)`` - on such a model to see whether isolating the other components of the model provides any new insights. - -External Packages ------------------ -* NetworkX -* Python-Louvain - -The community detection package relies on two external packages, the NetworkX package and the Louvain community -detection package. Both of these packages can be installed at the following URLs (respectively): - -https://pypi.org/project/networkx/ - -https://pypi.org/project/python-louvain/ - -The pip install and conda install commands are included below as well:: - - pip install networkx - pip install python-louvain - - conda install -c anaconda networkx - conda install -c conda-forge python-louvain - -Usage Examples --------------- - -Let's start off by taking a look at how we can use ``detect_communities`` to create a CommunityMap object. -We'll first use a model from `Allman et al, 2019`_ : - -.. _Allman et al, 2019: https://doi.org/10.1007/s11081-019-09450-5 - -.. doctest:: - :skipif: not networkx_available - - Required Imports - >>> from pyomo.contrib.community_detection.detection import detect_communities, CommunityMap, generate_model_graph - >>> from pyomo.contrib.mindtpy.tests.eight_process_problem import EightProcessFlowsheet - >>> from pyomo.core import ConcreteModel, Var, Constraint - >>> import networkx as nx - - Let's define a model for our use - >>> def decode_model_1(): - ... model = m = ConcreteModel() - ... m.x1 = Var(initialize=-3) - ... m.x2 = Var(initialize=-1) - ... m.x3 = Var(initialize=-3) - ... m.x4 = Var(initialize=-1) - ... m.c1 = Constraint(expr=m.x1 + m.x2 <= 0) - ... m.c2 = Constraint(expr=m.x1 - 3 * m.x2 <= 0) - ... m.c3 = Constraint(expr=m.x2 + m.x3 + 4 * m.x4 ** 2 == 0) - ... m.c4 = Constraint(expr=m.x3 + m.x4 <= 0) - ... m.c5 = Constraint(expr=m.x3 ** 2 + m.x4 ** 2 - 10 == 0) - ... return model - >>> model = m = decode_model_1() - >>> seed = 5 # To be used as a random seed value for the heuristic Louvain community detection - - Let's create an instance of the CommunityMap class (which is what gets returned by the - function detect_communities): - >>> community_map_object = detect_communities(model, type_of_community_map='bipartite', random_seed=seed) - -This community map object has many attributes that contain the relevant information about the -community map itself (such as the parameters used to create it, the networkX representation, and other useful -information). - -An important point to note is that the community_map attribute of the CommunityMap class is the -actual dictionary that maps integers to the communities within the model. It is expected that the user will be -most interested in the actual dictionary itself, so dict-like usage is permitted. - -If a user wishes to modify the actual dictionary (the community_map attribute of the CommunityMap object), -creating a deep copy is highly recommended (or else any destructive modifications could -have unintended consequences): ``new_community_map = copy.deepcopy(community_map_object.community_map)`` - -Let's take a closer look at the actual community map object generated by `detect_communities`: - -.. doctest:: - :skipif: not networkx_available - :hide: - - >>> from pyomo.common.formatting import tostr - >>> if tostr(community_map_object[0]) == "([c3, c4, c5], [x3, x4])": - ... _ = community_map_object.community_map - ... _[0], _[1] = _[1], _[0] - -.. doctest:: - :skipif: not networkx_available - - >>> print(community_map_object) - {0: (['c1', 'c2'], ['x1', 'x2']), 1: (['c3', 'c4', 'c5'], ['x3', 'x4'])} - - - -Printing a community map object is made to be user-friendly (by showing the community map with components -replaced by their strings). However, if the default Pyomo representation of components is desired, then the -community_map attribute or the `repr()` function can be used: - -.. doctest:: - :skipif: not networkx_available - - >>> print(community_map_object.community_map) - {0: ([, ], [, ]), 1: ([, , ], [, ])} - >>> print(repr(community_map_object)) - {0: ([, ], [, ]), 1: ([, , ], [, ])} - -`generate_structured_model` method of CommunityMap objects - It may be useful to create a new model based on the communities found in the model - we can use the - ``generate_structured_model`` method of the CommunityMap class to do this. Calling this method on a CommunityMap object - returns a new model made up of blocks that correspond to each of the communities found in the original model. Let's - take a look at the example below: - - .. doctest:: - :skipif: not networkx_available - - Use the CommunityMap object made from the first code example - >>> structured_model = community_map_object.generate_structured_model() # doctest: +SKIP - >>> structured_model.pprint() # doctest: +SKIP - 2 Set Declarations - b_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 2 : {0, 1} - equality_constraint_list_index : Size=1, Index=None, Ordered=Insertion - Key : Dimen : Domain : Size : Members - None : 1 : Any : 1 : {1,} - - 1 Var Declarations - x2 : Size=1, Index=None - Key : Lower : Value : Upper : Fixed : Stale : Domain - None : None : None : None : False : True : Reals - - 1 Constraint Declarations - equality_constraint_list : Equality Constraints for the different forms of a given variable - Size=1, Index=equality_constraint_list_index, Active=True - Key : Lower : Body : Upper : Active - 1 : 0.0 : b[0].x2 - x2 : 0.0 : True - - 1 Block Declarations - b : Size=2, Index=b_index, Active=True - b[0] : Active=True - 2 Var Declarations - x1 : Size=1, Index=None - Key : Lower : Value : Upper : Fixed : Stale : Domain - None : None : None : None : False : True : Reals - x2 : Size=1, Index=None - Key : Lower : Value : Upper : Fixed : Stale : Domain - None : None : None : None : False : True : Reals - - 2 Constraint Declarations - c1 : Size=1, Index=None, Active=True - Key : Lower : Body : Upper : Active - None : -Inf : b[0].x1 + b[0].x2 : 0.0 : True - c2 : Size=1, Index=None, Active=True - Key : Lower : Body : Upper : Active - None : -Inf : b[0].x1 - 3*b[0].x2 : 0.0 : True - - 4 Declarations: x1 x2 c1 c2 - b[1] : Active=True - 2 Var Declarations - x3 : Size=1, Index=None - Key : Lower : Value : Upper : Fixed : Stale : Domain - None : None : None : None : False : True : Reals - x4 : Size=1, Index=None - Key : Lower : Value : Upper : Fixed : Stale : Domain - None : None : None : None : False : True : Reals - - 3 Constraint Declarations - c3 : Size=1, Index=None, Active=True - Key : Lower : Body : Upper : Active - None : 0.0 : x2 + b[1].x3 + 4*b[1].x4**2 : 0.0 : True - c4 : Size=1, Index=None, Active=True - Key : Lower : Body : Upper : Active - None : -Inf : b[1].x3 + b[1].x4 : 0.0 : True - c5 : Size=1, Index=None, Active=True - Key : Lower : Body : Upper : Active - None : 0.0 : b[1].x3**2 + b[1].x4**2 - 10 : 0.0 : True - - 5 Declarations: x3 x4 c3 c4 c5 - - 5 Declarations: b_index b x2 equality_constraint_list_index equality_constraint_list - - We see that there is an equality constraint list (`equality_constraint_list`) that has been created. This is due to - the fact that the ``detect_communities`` function can return a community map that has Pyomo components (variables, - constraints, or objectives) in more than one community, and thus, an equality_constraint_list is created to ensure that - the new model still corresponds to the original model. This is explained in more detail below. - - Consider the case where community detection is done on a constraint node graph - this would result in communities - that are made up of the corresponding constraints as well as all the variables that occur in the given constraints. - Thus, it is possible for certain Pyomo components to be in multiple communities (and a similar argument exists - for community detection done on a variable node graph). As a result, our structured model (the model returned by - the ``generate_structured_model`` method) may need to have several "copies" of a certain component. For example, - a variable `original_model.x1` that exists in the original model may have corresponding forms - `structured_model.b[0].x1`, `structured_model.b[0].x1`, `structured_model.x1`. In order for these components to - meaningfully correspond to their counterparts in the original model, they must be bounded by equality constraints. - Thus, we use an `equality_constraint_list` to bind different forms of a component from the original model. - - The last point to make about this method is that variables will be created outside of blocks if (1) an objective - is not inside a block (for example if the community detection is done `with_objective=False`) or if (2) an - objective/constraint contains a variable that is not in the same block as the given objective/constraint. - -`visualize_model_graph` method of CommunityMap objects - If we want a visualization of the communities within the Pyomo model, we can use ``visualize_model_graph`` to do - so. Let's take a look at how this can be done in the following example: - - .. doctest:: - :skipif: not matplotlib_available or not networkx_available - - Create a CommunityMap object (so we can demonstrate the visualize_model_graph method) - >>> community_map_object = cmo = detect_communities(model, type_of_community_map='bipartite', random_seed=seed) - - Generate a matplotlib figure (left_figure) - a constraint graph of the community map - >>> left_figure, _ = cmo.visualize_model_graph(type_of_graph='constraint') - - Now, we will generate the figure on the right (a bipartite graph of the community map) - >>> right_figure, _ = cmo.visualize_model_graph(type_of_graph='bipartite') - -An example of the two separate graphs created for these two function calls is shown below: - .. image:: communities_decode_1.png - :width: 100% - :alt: Graphical representation of the communities in the model 'decode_model_1' for two different types of graphs - - These graph drawings very clearly demonstrate the communities within this model. The constraint graph (which is colored - using the bipartite community map) shows a very simple illustration - one node for each constraint, with only one edge - connecting the two communities (which represents the variable `m.x2` common to `m.c2` and `m.c3` in separate - communities) - The bipartite graph is slightly more complicated and we can see again how there is only one edge between the two - communities and more edges within each community. This is an ideal situation for breaking a - model into separate communities since there is little connectivity between the communities. Also, note that we can - choose different graph types (such as a variable node graph, constraint node graph, or bipartite graph) for a given - community map. - - Let's try a more complicated model (taken from `Duran & Grossmann, 1986`_) - this example will demonstrate how the same - graph can be illustrated using different community maps (in the previous example we illustrated different graphs with a - single community map): - - .. _Duran & Grossmann, 1986: https://dx.doi.org/10.1007/BF02592064 - - .. doctest:: - :skipif: not matplotlib_available or not networkx_available - - Define the model - >>> model = EightProcessFlowsheet() - - Now, we follow steps similar to the example above (see above for explanations) - >>> community_map_object = cmo = detect_communities(model, type_of_community_map='constraint', random_seed=seed) - >>> left_fig, pos = cmo.visualize_model_graph(type_of_graph='variable') - - As we did before, we will use the returned 'pos' to create a consistent graph layout - >>> community_map_object = cmo = detect_communities(model, type_of_community_map='bipartite') - >>> middle_fig, _ = cmo.visualize_model_graph(type_of_graph='variable', pos=pos) - - >>> community_map_object = cmo = detect_communities(model, type_of_community_map='variable') - >>> right_fig, _ = cmo.visualize_model_graph(type_of_graph='variable', pos=pos) - -We can see an example for the three separate graphs created by these three function calls below: - .. image:: communities_8pp.png - :width: 100% - :alt: Graphical representation of the communities in the model 'decode_model_1' for slightly different function calls - - The three graphs above are all variable graphs - which means the nodes represent variables in the model, and the edges - represent constraint equations. The coloring differs because the three graphs rely on community maps that were - created based on a constraint node graph, a bipartite graph, and a variable node graph (from left to right). For - example, the community map that was generated from a constraint node graph (``type_of_community_map='constraint'``) - resulted in three communities (as seen by the purple, yellow, and blue nodes). - -`generate_model_graph` function - Now, we will take a look at ``generate_model_graph`` - this function can be used to create a NetworkX - graph for a Pyomo model (and is used in `detect_communities`). Here, we will create a NetworkX graph from - the model in our first example and then create the edge and adjacency list for the graph. - - ``generate_model_graph`` returns three things: - - * a NetworkX graph of the given model - * a dictionary that maps the numbers used to represent the model components to - the actual components (because Pyomo components cannot be directly added to a NetworkX graph) - * a dictionary that maps constraints to the variables in them. - - For this example, we will only need the NetworkX graph of the model and the number-to-component mapping. - - .. doctest:: - :skipif: not networkx_available - - Define the model - >>> model = decode_model_1() - - See above for the description of the items returned by 'generate_model_graph' - >>> model_graph, number_component_map, constr_var_map = generate_model_graph(model, type_of_graph='constraint') - - The next two lines create and implement a mapping to change the node values from numbers into - strings. The second line uses this mapping to create string_model_graph, which has - the relabeled nodes (strings instead of numbers). - - >>> string_map = dict((number, str(comp)) for number, comp in number_component_map.items()) - >>> string_model_graph = nx.relabel_nodes(model_graph, string_map) - - Now, we print the edge list and the adjacency list: - Edge List: - >>> for line in nx.generate_edgelist(string_model_graph): print(line) # doctest: +SKIP - c1 c2 {'weight': 2} - c1 c3 {'weight': 1} - c2 c3 {'weight': 1} - c3 c5 {'weight': 2} - c3 c4 {'weight': 2} - c4 c5 {'weight': 2} - - Adjacency List: - >>> print(list(nx.generate_adjlist(string_model_graph))) # doctest: +SKIP - ['c1 c2 c3', 'c2 c3', 'c3 c5 c4', 'c4 c5', 'c5'] - - It's worth mentioning that in the code above, we do not have to create ``string_map`` to create an edge list - or adjacency list, but for the sake of having an easily understandable output, it is quite helpful. (Without - relabeling the nodes, the output below would not have the strings of the components but instead would have - integer values.) This code will hopefully make it easier for a user to do the same. - -Functions in this Package -------------------------- -.. automodule:: pyomo.contrib.community_detection.detection - :members: - -.. automodule:: pyomo.contrib.community_detection.community_graph - :members: diff --git a/doc/Archive/contributed_packages/doe/CCSI-license.txt b/doc/Archive/contributed_packages/doe/CCSI-license.txt deleted file mode 100644 index 4b0dadd9e06..00000000000 --- a/doc/Archive/contributed_packages/doe/CCSI-license.txt +++ /dev/null @@ -1,43 +0,0 @@ -# Pyomo.DoE was originally developed as part of the Carbon Capture Simulation for Industry -# Impact (CCSI2) project under the following license: -# -# *** License Agreement *** -# -# Pyomo.DoE Copyright (c) 2022, by the software owners: TRIAD National Security, LLC., Lawrence -# Livermore National Security, LLC., Lawrence Berkeley National Laboratory, -# Pacific Northwest National Laboratory, Battelle Memorial Institute, University of Notre Dame, -# The University of Pittsburgh, The University of Texas at Austin, University of Toledo, -# West Virginia University, et al. All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, are permitted provided -# that the following conditions are met: -# (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the -# following disclaimer. -# (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and -# the following disclaimer in the documentation and/or other materials provided with the distribution. -# (3) Neither the name of the Carbon Capture Simulation for Industry Impact, -# TRIAD National Security, LLC., Lawrence Livermore National Security, LLC., -# Lawrence Berkeley National Laboratory, Pacific Northwest National Laboratory, -# Battelle Memorial Institute, University of Notre Dame, The University of Pittsburgh, -# U.S. Dept. of Energy nor the names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL -# THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -# You are under no obligation whatsoever to provide any bug fixes, patches, or upgrades to the features, -# functionality or performance of the source code ("Enhancements") to anyone; however, if you choose to -# make your Enhancements available either publicly, or directly to Lawrence Berkeley National Laboratory, -# without imposing a separate written license agreement for such Enhancements, then you hereby grant -# the following license: a non-exclusive, royalty-free perpetual license to install, use, modify, prepare -# derivative works, incorporate into other computer software, distribute, and sublicense such -# enhancements or derivative works thereof, in binary and source code form. -# -# Lead Developers: Jialu Wang and Alexander Dowling, University of Notre Dame diff --git a/doc/Archive/contributed_packages/doe/FIM_sensitivity.png b/doc/Archive/contributed_packages/doe/FIM_sensitivity.png deleted file mode 100644 index af6b75cbbea900c72a1a2f289bed88f7b21dfa1e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 194054 zcmeFZXIK+ox9}|@MS3T+ARtY8FNUrl9YiSt5eQX!lhBJ0I!NywK@<=a={-Q`T|lJQ zP^9ueH~#y|VWDC0t8Gi4cz#@6Me&gv!sJJil}2 z-pQRinBQ@+fiu5%Z7G2rhU;@B`8(x<^c%oGa0`89OLg@-kAP#`JNGbX?_m9D0=%R# z=>Bu8h{16O^Plr~@7xKsxpVJdZ8U)WpTA___2)Bx_n5gD|7s1K%f0*W*7r_wG5>vx z`TftwW=UG3fZctkX9li!?ogBcd0|-Pi~`-3y`%g@?gboUI}0b3zH?#e}~HSFI|X?PmZ_Ye19$RjP>^8Hk@I&QuR?Q5j%zxfs9&W$4^R=ng4#EMSWLr`v+D58R>s| z``-=%*T~+ReyMA87x}k1|E&QA=|_AFnv=~S?eKr~?LVFT&w;G#{qBlMqq+L@fA{}C zZ~gmVJS2d?+r91MlP9wG{_k6E-97UuTV6+F_5Y=T?7dBSSwvI4$qVNHF6Vz#Gi(G1 zERs@F&h{_u|Ie20On}ym`5%$~uL4{10s^P^8S0V$4_*9g)PW%fw8jfc3j1FL7ES>K z9#}2={9l9mZ(aO*jE!--EA*(NRG$2=0;j720E||Es|N zznT0G)%<^FCaG1tB^+@xq^bO-@@FT%9Fb4R)nCNdgtKRht4WqbG~H7ny!APq^}wjk zQqI$I>Pk=(RJLlHKA741(gM!*J^5KN{#-Sr+4u6~OzJsw-sdn{1}8yN+B0^&xPc~s zCo1K&-#P!~_BRi)y86!bH2B3vG4l52w58%=^K)oPQF`y#&DG%$vLIG^vt`oAb)eo@ z^Zs9pJc3|sq(^hUd*h~F9+mx*&V7Iy3TJ8rK4-e$Nlkoxacrx`+_dU3kTNM-CcVl+E?I3%;{ZnE(qg~)YC%0s}BSn5dYY!69{eDt}L z%rey!vE+PB5;!L-OO-9ky}G}Q3RUH6sydTX#|$ztH;4 zj?B&3GQZamn*W#Gw-5uXtnaA9%Y#0?P}e~TRPN{)n`@RjbkMLz#?)l`>WPUL;|%!T8RNrz0OCFEwc;NbuDzk@tF0X%NDgBrF~7Z4~DqYUaGS)Wl}pOtU@LHcITa>i@bPn)|~x zZx=(T5!wZcbxbUq=?UthX&&4ArcFx`TnK633-d<#!~7BHk()t@^~}2A=p65zI;S4i zlzY>OP#2r6QjkLCnZ-S&x_>?JEKMxjA_p8&-9e@r%vqGmtoHbKRVR1ByFcT0 zGoXqRA7myYwN?VAxbLPVqpf?-70?6ui|be#Lu67^fZ15p?$mG1V8+bFK=ARF0Qr8; zNqcs)6XGHX_J!NwD#&=z?Y4A97Mq1`g}ID4oJ}Y+82nONd@U75o15Hv$TrU}=3U^%kA!cd_x5 zMJF>o@B|Nu0ab-huDq-zmcF843JNv)TDDmNfz`^#qTZ1durQpgJ{-axb2%@Ytjy*u zuvUBJ*mT^UW|h5R(OGk6-e9-+(xLWn@G&Md1c%Y5!aJ4}v5SD*C!moEta&79AW^hc z`Yq;h_{s=B%^}@s9M3N8PAKUc?K3be5!ZBV-cBIugn5|gPb?Aoe0v3)ZwM!7Cuy0# zS`(9w+HJYL{?dZ(B@BW2-Ca90i#0U|o-}S+eAufVsYCwZcTeGvdugZn>5J4IZFk-= zcdNoHJE`!M9lwZ&V)2<(Tko~zCW~e^0me8NFbY#7rnO1ha%WX?nL-0j3D|RdT%31)th^kdNy|9~<{%eiR=V|Ms>+@>m>EM7S5WeUYhW%#~uqwk&F9n&5RkU{7nj)u! zql#`Dfj^eFfEQ@gL`ZKt$nzuRwgr^zro3V2QEC0=CoJ!@lwdIySyS|I9+s`RN;}Hm zPX0P*0RMSKioY^sD~o#um`|~6`SC7y8XVi8{eE+_DM2(Ke9MO%sd1m2et(~&Qj6PP zkv>~^7gQHdtyagS+y|k=x9sMe>U#{GI9=%@;0o#EYiV>^juxZpuhot^JzI`V30A*# z-EBIh>c&48Ue>>G3%#woy*~N%#YDxiZ4Gmyb$6#>mp3&?kKdx&1Tk*rLl?Ao{ zg*dl4H%G--I;F7Z%{Ac0pn)Vk{>iH#8eDu)f_f8dR?Q)b@piyX8&@zDusWZiAWx*C z2=#WHV)p19YE79uHI4kY%4&G^HD8796*YRFL$3CaonLoIKDJ}%Xf;Ol-!b54%`;J8 zyQ&w}d=>&SW-Qt^rO&y4QUsTz%dRU-6?{2m8p?ntdaBw+ksibe$I<~qc!%)T)tosM z7s2W@2Ny)-o8scf=R23^^^JY@h3k$(9qZ#3C!evO(sCwS{Q{>_I=zOXqk)~2b%qJdz@7ZX>a@%6S8(p? z@{fugzIfaaM=fOMnc?ts%ga!*#L%(ny9qXfW2_k+1#-_p=41+nkB;bw&2Ueqw4+TU zvcF;Qm|3)cZl?r;Z&@FlNR_QSGq^o*Q$PIlsEBbn?{=ErD4Sv0N{AT_nl74ROVchf zaXsqdrta5z&QHuei2;+`w(%eP&2cW5Qe-T&%QN|_|H1NP?yQ35mYN-2To4Vf@#zk? z5d_ub&~kl}eF{tnUod4x3yi;Ocuj7YK{ANzp5VlshCrjTvyv-eD60Md!?R94zePLy z>9l-@cYXnW;WrkD#C9Ok*3b2MM&3l4B~!xqUepwbPlwxw{BI6}2DtPqI3*)4FzQL1 zhZjf9#tv(5-ss>ZWd>o^uU%|?dHsB?Fl6qf#^e*tyg{jhZWdM>_Y>mdxJ!AZH3<&( z6|AgCr|MRPr@lo-MlsIwg^Ya#6x-RN`Dwo_ak!sXftL{x(c@h~!MhbNlD7f~-!cIfFy& zoMJE(&pVuIXe^`sVbw`Ifn}*HwM%0psM)sbVNS{r0bUz4xKq48$%0SfHTPXdu$OJA z_WeJxSP)IZ6+JRA!3>*hx94k$4L|0K3dL5bdGrq!UoYWFV-I7-i8dctFuT21E1Kym zb_rTQkuQqoF=WURf@}Hk0BUsbG&^l8Mq(m$iy2R41+D`(sez&9+~1ausH_KAfm&Pj;H4KR10_T0U)o-9Y!Y4PO!dmy!N zTuc6gEdlOe%HsYrTD3qYYTq@jEfsy5q;np02#eGvb-NQ5Vsn_zCgORxm8ntKySla1 zdo@PJcdT1;S`aaN5b2`Yz1%h<6DKW%Oi)Zb(P>{fCtJdX228<9J&aux4PP zljc!a`q$N=Sk8(YJ7mgwz}Yxoqq}GIHfF07(O%&KO`%VM)N-Q0X$LcZN5OjHk^0E# z(`O$Y6@H(uF^r~>3`*aNIR8LU!k5VD9(M8MXKQGE1ob=42tjWueB6bbGs~30uZyUBi zm9t8rxyG}$Uz&Ud`KG}~*lvnP4+k0iul89_J3mrPi@Cj+)|-9_L0R`%d+%Gyi);K+ z`4v@6``L~etpbim`;UD_ut2pl-R_O%Y0bd_rAk zO$*MVW*uQBS#LZ0$Z>oztI?rVljwBxQh!aikG*B%eeRn!Vfw01{g%m*I04Snl<-IwN+F#!9Q?Z6(>6GY!NYBqF)Qn=zYH-) zh}kGrz^{(4&{Kjlqtg^DxOjP&0q;8NqSr%3q`gAB78xQL)jaU6V`c-LmB?v<=^yM8 zC~)dHcn``~Qh=^UAE>D+c+GfUfz3uJr*e0-i${{q8Ds19l|HthGvV57AudZ915q{Q zlR7mc1|^Sgf!V-knr@TDqKDqMpD~)ZTc}-#z<*Pb;z(Vn@$73eO4#5dmZtmx z`1lI0;Z54jfZ_fA$GbNq(@*!ZE2-;G%~PH8R;tnV1)=Sa^1M}8y(RWdmc^m3l^vPM za%*C<#Tw1kIJt_Hzr-~iMc$$qk3a^nwWf#4eBzBWrTa%)Qz%ayVo}>De9PR39t!UU zyQgDIl`F)Mlx`uZq6up@O-c=^;};@Q2}H6>OPmn^#q?n>qMct7|gQZy%|7)vZQIe|-U*DN-~2sO45z^ZwtM`5aF zbGS9uU>;#Dkj@kFoc{xAtiXfoXF>2J8`OK{7j1tKr*Q<1O1@oh@zG!E(($W$WFOmQ zjx?TkO=wXoIOQBoPk-v_g#EaBaBKPg_)2F^Iqy`$YhRlhCcS~tb$Cayh(GCw3@Ix8 zZsTilaU9f3Cp{sB^QJnf1)?r=hbmUv%5$PNneddulL(zv{Zye*DJxsw>qCN>N;Rs$ zLyWUS)mz^w`?B+rNE6#@vf#zRQ~`9!LglO*A%28?c$IdSGJ#{Kj@Fpix@WT%a57-5kZGlk9)RACI~$Ih>Te`6{=ux3DQtVF5^R1sF)}B`Qy!*s$b|-^fzVW zg!vUpUS$Dfcug$&uC`dL7t$HUkV3F&&jmGA&XUHK)#FE6Vi-S~BVQlJ-)moK-Xl>> zCOz%;xSQ?m!vz77IRX-VR#SPQ@hA?oo@MRf(JxWEKHjRtX zynGEmOiK_R=n6NC->phvtcQ4tF54+*4&KPUW-A+noP=p+MN6Li(0e^=5YM{4;yHVUEPs`;+DITNknx{QW{TyxW9q2{47? zw1+Z9v(y=4g&rC(Ubo`$(kEt*`{8SCkb2?`Vwrx(Ms0{7MWA+<%X8ZR)!^^Q+TsqM zTL?lu%C507J))Wm8t<&qLNq3vbv;^_N+^sx)BaAzq7fWbxu7tM_>V1>7LTG>Xx0=Q0pJj>jWm z^4a`daY|9{Hgbz*saz^nqj00a3Y3%MsOdB7uYs{PX7@gfn4^`R|Fpo}1F{ZUt=$HZ zS`#3_l0Ie3@5fc%(%6KwFY-Culd6y9FNNOazjCnlq<<55KtBYVitMU(8N$coTa~MO z*cT}oZaU)H6PD)ElfwsW&;ZwR>XD`TpSx2p56JQ&&3m@7t`{tBTBgBfQzjIWI?C@( zzy(TH^1mp4IUBCze#Bv2=KpYSlB3vm6fqVUiorLhGg^VF`0)x0=rmScs0tkte?&PU zs4`gN`BE<56XS%zjza0gD4h*4hBmZkPsyuVJvZzqdYf5-EHIH#gAvXnNw#$p`j87* z8fSWAVICu5BD9;bf~f=&DqNk$t}jV*%gDlC|EykzWSPSYc9HSXj#-92YI3u;B1PCO zDI>pVMugubk33=0yx5DN&+Vy?mC0~Sm%wJNr?sTg;b4?pq_>-JwzA|V&8MhbMn&J4 zi=6QhvyO5C@73l3bn2>soRvm#Pt$%@kYCh(3vKuin?ggv^GH)-OkuT+!u4OwJc&9h zDcSjW>=c{D(I(1DW4Fxj#qQEZ*b>_s;_)toapFi{jY7*im7hD;RHwN2I6Xl)FC9c* z3hm5H1l0fX2uAK<%~X=M8w_bdRuz;(e^yA$Yz#bD*%>HWsHGF}w$+L-rO^mnII-tD zUD_{|ak}iX$Yd(FF%=IG;oeCfbk_q?yJjTbWU4!P@cdpz0=l*jnM^Si(em{Zxpi9} z5USJKQlYAV#aseq6AV1y=Gah;1)IZ$TKpCfl^tfyI>D%5Q~IJ8DNlr+jvCLE?LU$j zlvzpfNusLH|IA-~gGSqU$ z#lv$4U(W}j+`*KWXiHD@{#kinPSMPz%4-5K3vne1qkVPK1E!Az>dz(?L2Ns9r)LBL zkd8!lMn79TdxGaP-vSVv1~>hw`7biH(hnBAob@#Yg04-1oDl4LYE703Dq{-oST4;o z2mMf9dJd^d^q)+#y&2*Ih=;7bQn4`9H}FnKmxn|wkk_rjR`g6n7RodekBny-kvSk< zQ^vc}!d=qfYcc|>MH(|)KI9JT-<+wG(u3**f{j9+XPfEVpa7x-yU?`hki1N^20`qT zd9}@Zg;rhkElkr7rYAWj0yiY@+28q1vTxn;EEcY<#Mo21$p^CHF-ceGv@h^+p4;%5VaAEU#ZZjC=Kc_Nsz8YdVYC6rFD5? znm0)1gjEvcH!Iw)G@3@Crq6^X7roTT-%0l{6DWtDt8DE#tr=o)qV8wW%jKx!O~T+X ziEFG`+}RL_U_ZrdNj(4c{dOt-?Fq?Ok#cp6Zbojxz4204Bvii2yjRboUKBG{&U7n? zwDVgwXMp5iIh<9}bQ+0SVo*2Zm@$M6#Ywoi)e@|l&83v<tkK3pU$I10Ta!<%K6VtMe%OJ)M=S#7a;YwM&Zqb^e)c*>6A$w6NCW!vUIa3bzG zH;lJ2agx*OA0-?55UY16Lr8llYqIc3Hw=d4^zp-|U~&EK5%R&vXDx!}O$4Z4pt@aT z(26Q8Uh%oOhTkY5r>Ql)1URYOyb&b*2y35ygS7d%foTAX#w+E>{pIYs9g?-Gwp;^^ zC7;-y`j@;$dMJJ^oiC(ota1_&hjy(9n5a)D;T(vi?i8Q5wsk#Qb%H5XDCK?xM=v%v ze@rs@=+ij4^9M#0QzSBvV)x-QFee4ijfmF-^FOk^H|*xJT@-s%BG_CD`G-h7m*#1*07d>-(wA0V}%+*&BEdQ+dy^of)J4`)|92C4k z4O=4ZjVFaQeH~R{kHdHsf3?jucK1OknsAgiG2NyFWazvYu^&z^PtA2!&`6%x2r*<~nO|a5wR(=C$dLNtENhbQ(@My^?-s-CFNP z?CK_DG8{FQb!MncFdGrGJxd<7%SlFVTS{Hi$ z9DUpoTq7*IO6v;ksI-cld%^ak(@FkahZzwER%6)D{#og%ipluLVSE?XcAOFh%Q7o8 zArU@ol=bF(%N(&f-gJ1C5sEO zotQ*{D%Ry&1}64O(T-c=n9Kj!ysp1WNZ4orE}Zq0e`op3$-Y2P=6>|sXOY&f>< zCWS>RRSfEaYFC1$$cTOrwVdcE6nyzfSN{%90eJpCcUSu9ed?{z6kl=eQL5}B$w^dOiZ{RH{i z?o?B5j-X&Z_M`HpJlQW^Q3~Qa2^XO*G|>nFX*??IXHgNA)G4l&8cCmb1sn=aOVtoH zn4jzN=-ESjo+51y;1ly%v+PZyrJIOKs;7}I`P=%+Wi3*t|5_Z&W|wV9nh>%1Wi4ZE zD?mHaW%hA-HW~wS=&^L+@?QVoPLPIq#`?hH%wAuUW$gq9)nSot>MZ#m&#dhE6jcer z(ej}BVV?MQAbSz~1ZIBVBjjIT_0tG7$7JbBS?@s$_C%-CYi`b{X)DZa2E zgli;7zy5N|`S|GLN|jVkm1&FvWwDKNw(3m`QobZ|BrF*+Pu}93AHc$pR4pE6g<(wk z&f``7VtiwPjNlAC-xIgt!dyqQ#?Atsk_?npfMf7S4-|ad5wXAw@hD^@^reqkSq_bFy@AJ=B+O^XvBsm%~^oROhB{rR)$Yd-%Vk0^1~m-zn8p~eC`jM&l?T?nc5 zqrgqC1@b$6W`cI~7^+AZT;paDsvc~;Z9 zp2z3Ut%di@&37albI?CE@Qg^Oim)sK&KG-+M?WAZw)~R=H1V;yD5$*9k24N~;RG0R zq?Q`(DqRBYonD=e!7i(kptMh7x8)d-+`COtotNkwN?fl zd+J@xya5G9q_teJr4^Zbu8X~PRA@HJr=x0Fx$-HY(XBa=j9XFpL)u341GTrqq_HOm z@5RhAyJ;p)0?iSAZAT>dl+rzph?>_m!1Fr|wz`s7RuAa+u)4{BFhn`L-ts{I${ybU z?ww;vIy4DnC*!Fh2V?T*W|>dFAPXvlOnZskI3(|kOg>V6)sin%OJyvS#_RJ)V9D^2 z*C#Xn2!_T)tLO7kEij40sNu>8%zMX=c+hn#lT`DEyBTolE=_+Or|fmYW^~52KN`g9 z(@%Z{yGn(AJAusa)5H$>G>TflxK6tKg?3G&=9GV;Y?D-@c4?w!+?<3~@#nva8w_hR zkp}8M(3&QzmXAq7)su*CS0Jxgl@v4`$%43q3GpH!Q=28t?=kocpl9&4$_4CRGASS2 zCY2*|vlqy-cX9#LY${YxpQDOGI=uEV76oDgusG<`f&1fT^c9B=~v z@F3^V#k52^LX)HRsP!@L-dOB-!Qx4}EuwqbQpZdb2I z0w;p^7%T)H89Ng{J`-dT!IMPPlm}N-*Jsk@o&2<76a(q7fj($umV7&+5HfX^z>2M- zP>P{9Xe2mBeJ^}I-1dd6zk5I{ zxL*}EB*Lh~G3cD#uVRz@IJ=n$Kh)`#yR@AjKk>z!0*ABmqOV*irh5z%DJa}DQG<_0 zspaWE1%BxYxt~1*>WSqWG&2ynpGS5CLi)5a-UgpOwimZ2tS3i4v!R29@_Lb7u;DWg zI}{j>vW=GS04sUmA##l?Ild>mxtJLz84{Dj2DL=>xRVu>7RD`0;5}>>&3?-6{g1X? z68*!P;+l_xPw7wvbAd7K5TB~84NeI%gWjWrybk4S!Sjq0p%Pn{*P!=%E0REol(9@P zEJH&BB={AZffG-WVA@MDHa8`AvK7}id+~iC`SBE}IL^EFd6zc-r}xBD)4lDTHX%#r zM_^6isC01-E&nr7xycpqM^!iO0!Jkl*QlXeZWlzk?q@CHZuVoWYNL0O9cK3E2DC8f z^z7~CrhzgvV;8eo2riArEgC#zxWg#U53)E(1CyCeh=>2RTKpbQRP)Xb6w8|_xrxj6A)a%Ilh6`niketK zCZB?ak@rt3HHib($${aOjs2FUy-1sM>S#U%P531)UnDfN%t zUx^bzG@_B~{ahs^yKPZr%}=Bz`7Js z+Sm$s^uK)0Db7@@xhk`GhKzY{csoeqsq>3&+%E8F-Mu5^F1jC)K%9_Bsz* z*ck-}^G&eVwni+64J0hvILB&#)LCQI%GJ;xdJEb1)*--OY8VY!mnICRtUKEM20* z1f-p(H8ev;yDYr*_kVL-6nGALdQ~N%BXI5M>)GmY7Ry|#Oc0N5?Z+G_wwLFBk!4KQ zJtVf|;7+Y9no1@mHXb9;1y4%vMgER=|43=)@Y76x8tP^OWUa@lwDIP^2{?wi`5!&v ztukUinx#Vl;D;3VWez##mIx~tM6rLG(e3QFo^>vdHYNpCz z1yW2?VH~%*`P;amJKuqVE-Lr8;yQlH0x+jGNxR7Nb?IRV`xi+#4M(G@Qap4MSy8$R z{>^{F>B(bM~p zg<2@jE_>R5mBk=;UbnO4V1rj@iWQ5a+12jc-c1^zBS0EW=k9H>#L0Hqu!H>2y=)OU z&&yX)t_~%%Wed#}EF6b6?zkhh zo_Psxm=>TgHMzYqNBibnrM%wkpX4ju(_|=jBzzy*slblc#Cin&8o}43d=&E&`65%M zotBBwQr3|h3@uQMKIn<3@w$V#hGtfKJ^}T%zfA$xzru=DY^<*3^Rn%`-!y21>A=6} zmj+4CX7}ojmnUYDiU?-eB`}oRoBFCa71}WB`ov%qvwQf7SMxpz3!k<=Rx}oI>X+!P zqL9VN#Mj#;k?W=k=K{*y$ilI67Dt6L)O%A5_)eTA!X-hoH+zv(xs5I+gfg{8fRn&A z?=&;S(CM=kNYq9smR13i!@HN8b0wu}sgbkCb=5CH)IEYpXhTpFQB0iq|AD(P zg0CtS?9Z*|MYC}~x+%M->SM9p5z&EBWstB`oZ2&WNq$ZIA+8}t(+z{dY;FIA;JvH; z#}fW{o(0<*G)3TtAqR!rAHAK_T=S@NB)gp^3*U<&D71;|^s;TUZQ4ia1`pGZG7Ee> z=RUrM<33@Mv1~os+t;5Fs!IOa-aw^k=v7c6V`Jbp@>Ic}PBMs{bAd+&>-z7p?8P@7YP(cm{`wO!(U5j~P`?AVjdCp2-xx&UXpqUq3?+LJ3;vCVDt z7HDF4NgTE$<^>4+))J_vUBx?x;XZ<@pwrbZ))KRfUVOLhBZ7Uk<-f))9tttIFf`!2iLlLR9R_P-i^PaeI ze3&ra702KILvf=DanTH}uUIJCAq)xxgzNN6}IF zlu&`y6skcTcGIZAo-lmKXzB7qU))uZJ5=#0e>{gqY;Y)tq|24Atm8wl>piv*)7Chf z1M!v;I(Uq3FUxVlfyUZfhp(xj&*m z-0j9H?6lEU^I1G|9ZM709oYz$&)!PXl{y%k75?h-d(d5b547ZxLWreE)3v8SV+v^x zOVCAsHTulH!=)8J6IZ=oZe1EN8#_l4Po;z*dv8lA`dG#s(C zmsA+kO>ngc@8Cw;jnaK?*f>4%b!Wf*F(Hup)Q>~2Z0c|_VKvxAbN8`4Uul$AjJ*FJ z6e70__ro@8 zzfWthBhpod%9fDbckAdAkr4a@w&5ZqwH5|w4_bBojY!!@vepNbO`L{DR9@V_Dk87E zxXF3EOlt+W!@~1uPWmdibhXX9zx@Z-$o0j|wNjbf_VZlYK;$YYzhP<359Oxq38E#i zMJ>l^s~*3FlC&+?U{Z&w}e}tE3;KZ!$?uE&i;l=L+|OmqV;7jIq*M|fCw*EIAH*_e zf!;0?{-IjqP#+UVLpF#vjn3|$^j%HTyU2`kZw!tn1Ijms>Alt4N+ZK$ZAo)cu@Z~u zrxr2fG{tYYWX=S_1!n3WE2-J*Im8YhL4?vo6fHO}e`^;to$(5zp!s{hXFp10kwcDj z?s+}NaAS2-Ut>-#Piqp*6;ieu>LPa7Q`i&;g;yJKB;&ee1guSMiORD+xNHCH&Z^2` zngyT!nX*LpV624hSGAMO>xc->f5RBVc6T?gN{O8}wtq?pGCz^@e(M*J#&|a!#wTN; z-QNS$*wS@HT|e;At9Th0q2#*vu7AZ{K=UA(OJ62g4~%LM!o)f69 zK|SR-Vm+5>^sU>ld?}l~yzlczo%OKiY)|Z&HcA`3^gLfpNoh~ZL8(cQ239jKT&$%E z>}=@lo|+g2KB}XU&T_dpH_wWwON2aX54fwR5w|MoSXXL(&aLV;&{G=2V|lT5$MCpr zZr1=<3oMJ0O8AmlttN%Z!lx&7RDooF3#4oKSU)oFRxk1Uow0kch=v=JwYC$pb- z2_9ZIR(kfnyfX&>b%2Omx|$Y^5O%!F6>->*X~8Xh_rlmH@0{AgGK zi5@9H0+@%zg(Xqa$NCwICUVaqGI}e+!P1x9>3c8dgxQh7?HwE_$fL{!`NjT>ATxsl z$8?Awiz9zu0dqeWx0S{HqRJD8*_&KUq|K;UG@>x>Pc8Ms*(e)LGbx8+ur$-Qc*h(% z$5O3uZe(wq4)!agdR5C2S5v<^yN1WgD|wY8>MQp9Y6(sI2gEnF)p^+(W`@vvim#_n zitIhFI%Jb>hpQW33h75z^Ek?Twr{)sI(Turcy!mqeDiI@5Lv+R00#@@sYF)A6y z8QL~gn}`71BLa*fsa#iu&oPtjIX29#w=*gdswF z2PDL3{Da{3wBoN-+qEvz$&3=@;%$+F12o@Uf5W!Uv3(_;&G#EfZ7*&E z_!<&RYv-WKliaDey}d0tl$zpo+EHz#RbN6Ydu*3tA6314M{=lHRHq{8=weqgyBj~V z<_e#qSF%OV;ge=s7|{~m&wEhMM<_UTBKz*QM)bbG>utKU;#AS&hc_C~hV=QH=2lFF ze=sdV=GOPO_sI{YrbW?+hAg}BY-?xNiOuP|cWwq`s}BmDo;RHpnf4iK0>u_Ruel=`H?CAa%)b^F+eREpKtIN(f(=XS(FFYRpU7f+a3tF!) zuv=ZAO2qmeP$vkcaXQsp-52n1)n;L%;4S&hF zLdQ=;va$%NADI8TlJy;oC|85faWXxf8{u57#KQjq^{PlQHZtsAq|Vk-(wx*|XlnoK zLb+stJD~M2uKyP+890dhY<@dRV^@M53Ekzioh|w2LbBxdbFD2HA~JrAeF?8AR{cLA zzaxI&`cg)BB2l% zI2LA0x6bDW$fU{i;C6Q0swmUl49DjCjJ`+OXFydp@@#+knyM(r?}8si?;6ZmoGIw6 z`awo17R=BUO}Cz84}eGnLWANkn}81g9RC|n_~q{!845k*s=nyz%Z_Vjr2b*K`f@A$F|AjTqq(s*R_*c4~! zy3-z8P7!T<4>l!+{RA2@`d$8-M-WS&wGuk&!v(epX@xoH-G=2%0iuSJGnTLAN-)!7 z&T|n!%-j^{6suX2^_l6NJ4nIB4=b(@0j3A>2Mk#R`r(XRBoOK+5uI^TAdYn|r#(L! z(?kMb)9&Qe{O!#;rRz_Nlw)Dt*S^PZ;NWG30o{kZ<8dNW#Qa zC2TftGf_vQM3N9|`&lk4fp>^JlL-S9_*Q^Sw=Ji!%yOTQjoqB}m}lT^J~OMK{e!Uu zs!*;LCTQTXXT!~=fdBu&+>&;HfhPXt&|qJ?nrzq(;B$=c>ZuoiBLDTSrh*vBN8CUs z76HPEz$()jL5Bt4ZKM1H!ebQ3luJ3kK>z{ccS8?XjA=X$0oVt@C>kNQOs~xnzk!CG zdM@s&Vfoek&D6gmN=MfHW*jqwz}sG*$xXG6T}#u~HN(F8{vQeva8AwRNpgR*Wo>_V zU|h`k)x}uO4FxsZBj0-7E^sgkSD~h7@DyX{zVEY>Q6-K|}j^8@Nkrmm(GcE!|I0YwtcJ zSxd75$GWb>DItJb_8+nP>zS^D_dD7^-4f<|3vJR4^u&#THFM_Xy0h^$?#b6K1mVw6 z9?!TAxjV|Y!$P`()TcqX|0;iIde?f&qgn0!J0?E)cSHm$;JSGuHJAOBe6rg9b_|kOZvOEVA z-jjmxp~@htt5!_nTD=E0{4%d85O_lO;2mM4J2aqJTfToB?C{(@EuH#r-^Cbavv5}u z0NeCQj&%9c`Mban&6k&_>|qN&!gZ86V3YscCaj(uFK#~Tv~OcvNqYM`Y0 zXrcjmNK|I=3?L7sw@{$x8Ocd{(4V3EOL?JxP|-!;86*bQ^-WR%kW(dSc~R{4>M%3~ z>eWMyZG`I!lDwL`^S!|2rwuf z(&~F@f3xc-)9v}13oM}pVw1gZ{wZ6l^6DSQ_dqDn3!MR4m;QFXqt6%c8|cwt(iql4HkQ33DBHhmyt9vk&P$Qwx&N` z>hXjRNL9Y!i}VHPRkH}rcCj@E@ycejiulGq)za1?cL(TcN8Z>az9#J!3WOFozOsTJ zHFmptayb57(@DDTrZk_~9`C!nn&MkIWK)zqEwyI}JTgvai=mf6R7qbx33;*%KSxL{ z2ymZN|8h7?au(=aBn;K&8!QMtrB@UrFkfYJ=~xJTqQKxyQCm{RoI*}DV3Nw$)uJNr zGnxjlu94@@5Ls_Nm0Z2%Ua)B+0qBAoK?o2)W?NB|ra*n>{E8Je{r@^KT_zH5E!zzD z6;wmtriP}WJHSnPH;4fAX6Q>@gH_a%hOPm!h%}_0=IWs{M47RC4gLb^!|C_xKRf0Z zpH%*^_piRW*eMK#BCcTow(O|>phC zyw@#$#+EQZ8N_>=gtI_kxml=WNX@V`DCpWMXy32Nss3(m+JmPH<ppJIhDpMwqdA01@4h z3she6;vV7ojzt~01^-(?DY72N1G{8{T7u~B4pP=YaU-q!?alQp9M$NJ8v#P;KY<;g zN1D%`3J#8G%KV=5!sn@a&zKEN|Fb0BR~|mr4=E)OAZH%{X4@HONFoM}HUJ2=kNEQg zFQ2LkSsX!=)_|3gh?a1czY(Cd`rXDhfkAhr5oTVS7hIr2{_0B9AyL~irO+`TXd(lc z>3pZXHXM)i8MX{NvZ>Lp05N{A@ra*ZambY_lUBmTT;FHEGqTP94?;+(XEcIb3~xpf zJx(MzEU<^=eFlnMD?myzPjX)ZF{WKo{B!9)k!Y?53HrXTkg{mZ8}p%Lsw3$lc$c^+v98ChDUGT06zl28(s~BYz-;1z_>1r=_h8E zE-^~hAPDKGfMQ(=Dk%K_VeGA=s@lFkP(jo~r`F^!PTEZ%N$| z54c~hrshLB@Vp320O&zChOhRqw4hOEtrVTT-F=>RfTF9GXWhp!3U|IJNxu5A`RZWp zP(_UDwYG4FDjx6j7-7CMh&;dUd1eRhV-U!@=g&c}En64%G3#2wQzsGwN z7)5>2tX}kj985H#qKobj-Rw_ruLk+ux2r7+_XEp4?(`=`)3d>4KY2G!{X1ezS!omV z!1w~5iiF678OHX}dOpu+!oVQIDX8h6rLzx=91xJhidL^hu-xDSA-|>3^KOuXzXOS{ zkT0GY=0blfvR<+L_v$vf!wrbU!^F(n*l%f~YW`?$~E&$qyU z5n+I*blBow0lGXd?1<)rsN+9c*Y$Dz;k!Sqc?*LGu>2+J7%KuJ+H~X&-ClpRSs1kG zVtKIqSr${#KX+WB|7h`uC38?f`;scpT03IyOM|hqe5XbRTaHTe50 zJfrQ3Wt6P@(JPX=QpUt4J`dYiDY29+WPsP!a0ZTUQJo3(fQD;H^hT;*8gBbNsRb-` z(?=v!QI|n4q!nVGc0PeXz6j1H<<8|S`aL~LUdNO5c*25})>#zi@Va&cBQoq@f&5FKMKRVa)gM$# zLhuCsxG;iLjj7oWi41KRf~vg_PpiPYX;XQly;a zUXXULHbai?m?-ws6)VXOZ2p8OGzY?KZZd-U>~aMa%TGx|c@;C&J#_v2Cy5Te;eK;1 zei!QHT`%E3X_dQ{rT`3Gzh4IuaZlFI6e=|8H(+ed@(HtD*G)Kk1Dz z5t+2dK4K14qyB@ZBIL(jX{P$v^9!CGfg3ieJ4S}hmP%+k(A0r(EqE3CV@e*hG}Fe` zCTJSUlzx!#9JU~QceXVyR4^_Hv7o-f%AJl08&ldN)X_NVmZy={!}pE%Fb(6ARrD|A;fG!)(HILAX;xw)}15sWW`9 z4-C*iD5$DzR0w7_pW^<27x5&S=ZgclPrq~W?J=Makz(>)6-=~`y&^7yNoZWH%Wp^R@oVFB#mUj4X zd{wLq9FWEKpwqNXy4xR{%4IJbbiLGZ_ddK!d|wdFVJX;{%>A;c|6|X z7Whq#@4~Loxu&oH9$Sbw7`{AMO%JOUj^I%ndvY5ZZ+{Ns`a9_kQRPl|z=ziphF4xv zID6e$diFnxC7a4Ko3W_CE?qm+DyI)8ci?#KcZYIJTTp1*bCurtSlv)7xjO!nsghVc z34Z)o9sjM1orVv5r0!Fo+?g9UOg`e-JwMtYz>L`>vhPWBloPxvEqLFLVgRsGyz_&4 z2%>;sN&oY#Lxku@e$Zg}$C6XL+;+g6OxIiUlX?* zY&P{7|P2x5Rbreo8zWrD*2>|sDs@Y>;eyfM!F|6un>7H(Z;W#TcOVkIRR_LG9 zCUh}Bk>}Qw`~mHd5%RCx*f$-9vgHLZX94(m9Lj$@7IDCsW(AEX`4*!&q3qMJlH}YB zu5>L(zYuX7Ff=WXENZcVvYfxW@!^Jdv#5iVdhaOg@0I*}#G^>@-!1IFKJr4=tC`qiK4F}N z-r|dHlm0ljgVx7V&9q3IklRvg)f79JI!681@5Vd+o-9ld&!j~?MF)~AI>0G$&PQ`) z9k1#^Pqu}M@wcev`SSCE|EVHS3B!rVpK37@E$U;A54K*s`1{d+_E|-wH|1l+FGt`| z1lE$427uQkkUrCLL4z0VMvik_Pa8~FpmWGsBuw&PjF-=w74D?@-(~?ULN*zKZTk-= z-4XavPu<%b0*Hqcf4lZ?qE;g&(woTLv5RL^*nG#l=S})DGK05i^!DdOD2Q_948MB0 zp0sEh*ap%83;;u1=P^N2{5EciIJ8hX?6mb`&!jH2`EMKy86-f$HhS!-Qa4|3rEHQM z`3@n@iN?r!Snsf!;QQ|+dpSC zjk(1&q|RscqDx>_2GFg>F5@HeVU2WdU@`#3GZnWVgL`j#-;Ef-#^*P$9lska`Y~m- z#a*}%RjW%CKekc0(X|V^kYHJ`|7OAIWxDZ=zC}~B$J9%IGl8tUb}{_YMPF^CNI#?+ z(>dMMuoh)8H{9YU+L<98$)mP_>(5!6;k4y^O9Acl%R#O)-h6w6Z=xM>)(OwL6W$~( zqMBd+{PKK(1HXaDq|QQRDKB)`2@?gd2Lp&m@bRgO6Rb=}25;sqv#)*xWlO2M-`4Cb z-p}l3q`-qwrpkzVax>bx)IXwHsIxu@TfvI>q0(DBc@wC|pN-8&EEa!i_8jwT6YHFs?BE%ITK z>ohu53Bj-_=A$Rl8WiSJIrS9x z0B?=u`5rmt&Xl_iY{9fgn#yY_1k=0>U1VE z(UBy6{o*2VD7c|&(JUxHL?lN%12Y&zb?ZCnxF9+{C@h{aA`3Hx@l5$KxFmcEtPcu~ zMX5KRs$uxc<0agPsgAT9xI;mC`XYrSyE!2hP=@sN8IM~c(azK(|TRO;&s|I19T zV2eevL1v5>wRUtJwNXO7U4o8`(gTZeL}wpuNxsx0>j$7Y=iL1l;NM6h%6s8{s+^+Cw?aP7$xzo`(e`{*`i4)bp<`P|NZi$1O&P` zUfs1HnsZ$}3hZBaq=M;@1WIfE|K#((z*w>_y%qY%$oz6pZ{yI3w^X~&YU*1_zSGudQ>DmW3 z7XQB^30Lny>*XbqKaa0JmWvqS_3xsEn2^#&HI!O3N8n|P^4kC8jlXo|`xx*gBTO|; zE48=mGT+tV?>WGUVaW0Ai4pQU+0KrEd0NeYC?*&I6TVnLOD(~8Ehtszz$_?KGPl!I zzXT)792eMM?ZLFTt9j0kj1&?Rbl`3P?Gnt9-JD8RUhem_yx6J5g^*w&_}6~uoMS0Z z)Rk}eKmj}l#u%Xink4pvZVyuh8ld9d#TsYERR`uA+^&Y1n1^W2X6 zA^zf0ON)(^`nx8ckYa*NSa)g|Z4sKea1M>tgS3F_T|Yr@Pysw=EbgS#Zn{T+7AA0R zG^P{81kKyRZVw~-&^sE-s@n{KpPvF3o4a5P2zxLgYctocQ;4Up2bZm*;`Ujfy`IE= zjKT%5PuifohxPLv$Pj`#8r)ztq?_%JF@2jn?>_JaTFdkScN^Y2Z$PtfZ&ogZYEM;K zeH9ivS1ySBsdt?|z+9O1M#gOh z;tGB!&vLBgpg4ighpTmE{e|_E+q&lh+uyhL{SG7X-H#zzyWj~;Q-R&>e2J%aTW*|z zUC5d2NLp}O=ffDrBfS`qT1t3`ucGCdCLi`2%n?L|Y6d@-_3v3YtX`N+UtRXQS`^;N zK0H5MTbK-H|49f<35gc;x@j4R3x$gj($x-gsLZu*tY6>UzKW$} zo?g>$53AF|^X#bdgPpdNnQbNWWWl9;j7hfs`|@u=p|U2*=wv%<3C-D};mW>I$d`2+ z2odOzG~>JlB(3b1h?v!oPKQP&FokXdKQ7v8xViQ`mC~lbSe{zo6J?3;>LBZoUj}=+ zWbu2%AI9!IFu%qR?DJuMMCsQPf?%Xy7?~3=K>W_SbVvI{NIs5*7IB_!gLnS7RgOuT zoOsn_T2fT2XQq8%eYPjraAXi`x{o3JpV%S}OL^X= zjZtP0SIIQGp|m0Uv=+7)D^xJZW#+cpWzb_KO!x8j34evO1NtOQyJBr$e&77|C=*qZ z=&CIibvsFx(k+1Ew82w$NT11;Pj`u$1Q3H=-MWj~Q@h|L;1ut+7aP4)XiEq&VEgk@UA(7m&*MmHUil()!f5v0xf&||0k$GysSF``G2&N{ zramj%p@YXr+0$2AhMOE<5dkITaV+h>-E`6(yD#f7Wt$!sH3J3A-~#=>qe}trS=55~O`}PxI|yJQPCg(Ina2=; zXsY746jc+LdGHW6)K=&KFsLt3bS9O3^y{nX2=iE+SoQF$0Qbf}1w>5(XxwKZ#IK@# zRP`xk#W~)Z91XG4_|{SeDKxu4U_CRb=narmU;d?=&ma6bgUE-X)?$L?9)5#XtQobo zc|Cw100YBnaeEIl@{$bFx~R~3=3(FY z(35X};o+7jdG1Us^$fT@LJYynU1;KCl_;)+Kw?(3hP`UVcLnLAa7j{RZevywRfv`r z^5+f^$rb7)FE`uTNq3R>kx9xVxB!w}4HnUO=cc;K{-8xWx!fV^h!-Ape;wsI(na0hVh9OG0!Fa;rVQ#>!KylxKPo z0KoqVu_Y`i`~lsbHZ-9Xs5Izc2wUo-EfxdT9g-vy_z1|q+V#8l+6#cVNZt#Wwz&aO zOmv{cC}ue}`nNx=WE2n<7vc6D46fYd8rQ=W{Hg9#>01xtNx4;Rf8Q_LxY`|G!DC6NC52^S+Cl$o{<5q zQ_2=hY?`&FUdh!4jFUBw(S~gSeRuXa4pJ@{MO4kjud=ouml`% z&8MLc*ZVQ^{{7_)Kkm=GSx&7atOZ%Wwh(0cv-Og)MeI*xN)tc;9?UBRQj=uXV8iKr z5Tw#qg`0swWmmfh$U3TUz!ucLoo$z*7G=5APDyugh|`ZoF6rY;P)2K>0oze~AVt{R zUqy=7;MwdI zOk4fHbnsRYe>dOodVGh#`e}&BIpfPV^{TZ4+gg6$^OzBq@FN*znzjQVo;jVh=Ve$I2 zW|n(OeM<`nF|P$UPsi3Qe7X3xc1NTi;l!ctf_!`whON|72kaDx2Y1+FP>sbc_%#%b@h1(w@|aE!pfaA(JWL5v+c-rp=RFb-)* zpzOZpc4c>f~3y01d~hq8ym zQZlp&eBUlOBA|4zWsjFBCXGVU3ExhoO=HhI>eN%Y+I~r10xUJ7U^Fux{(Ga;(C_wE z-VYVPrmwaHRj_lX?T$MyFWDW)Q8H%T!xUsQxrZ%EdOADnfakWr)HtBxyCIRTji$Et zo^Wlq`~sT9XZ6%)#R_w2+s;6Ntmga!dA3Bi+vixyg`ga{i63WJ zpcG*BF~F~aONcCjVRTVm9)y2-l`%kd%$RWhot2BfRQ#Le2R4nrsp! z$z{v}M)f_UF^t&BY>)4`&4BipD1CvFim*Q9cVT=fAEfoU4c=aW=Rh%;z#$kT1jMpk}t7>N)p>dsD#o|_N6Er24j#!BYxWzy`K*INJwz*6T zdiK;lQ?VP9p|OPe-o4ET>1TTnH>J3Uk7NBfiMMyn4Fl1=9qoamYq0T>4nD*_gx{2a zRp4B^9LBRuDA@jH4p8Pb$`qOhz5Qy&umZ_}WKC&M(siW8fKnvjwiK~=GC<;r%k_=b zqMqnA?_MiQmEDZa*v4cUtyei}CMO?@aWT5ha^%)w1=s`r+ctJ zAYqtPqJrZMytn~ky)Q_v74dW^w{sWB5t*dOG1~g73yw46h`k&)cKPI->*m@d4e@@9 zYGvdq#-d{O*Md;pPf#1U7uT}`1P~Np#xeHexn!A1O=R_Zg~xaP=GX3oG)mCNypo^1 z^-uX{E^)Bwpdy|J47>SkYtUGwe6>#yGZOJLMC` zE1!Xe+s#2aK#H`=-=0%0YOrm#iHsL zsIN2bh4F`W%oTzd+~^AMLH+8xb9KX;bS+jxQeWYT3pzS?C@Z?l*Y5t5M(KzZSb$7R zE$WZNLwHUBfcpZ*fDJy_rIBHPlOV1O6;OlXa1_7JT>Tb>;UXGotp=;w_7QgcaFS); zv|?afZ9ilk^N;WnxB&IfIUZAQ8SVEFp$ar-^}snEh>KS41{sd^@}%L0sSCkoot4oq z$=T^r^{bpj9gf(!Rr|Q*iPVRF- zl8zp9$Cp`Ou9JWA)*`5)bJe}U9~JBFbM9dcc4b9#_Ej4|CR|yS)@1l8m7t$($wqQz zZUG|bWQ~iR!|n42#{K;IaS~lDD$aqEft_FJT$Q+og1$$^v1*TXdaP=9*FGCGg=RBY z16h&`GmLNFU+&h7i3dx;CQHp7y1xn%a4n){!5<>;e%GU z-!LNs)IAzrLnbd}l6KxhKECaNyKxC&XR(9tE>3oKNF6iIfU@EPD4WJGR%>=%hrGgO zlo?}J0METaCBkG12G7n>Z}bZ6Hr{Q43Wu-b@_s+4E$aA9E6|;>XF;8?Bx>V}K|&0v zLzd9+dr;ZG2qxxu`U6b7H6N1x*^j$m{2%wV1?W$Dr}beci&k-|1bA9NbL2bQy6$se zz}#V|X5Z1^aRim_l}nX&;Zn2B=J%{56i~b-GpJZaO%vj`F=2#jy?oZK@s`lO2dR_p zBaGD1Te3-%B2hnDHKOS*s+HDIka2Y4kRat}fLz$`iQND@IY2Cj?-8emxui$GZ4p!P zKKiNN8!qYh=y&66YuO*Siz;nQKW5(v%ES+P;o^KUzurh1?tmfx+trlLO7)T!kZju` zMf-KJ+HjOd-v7C0HPP|Q(@v_eLrq7e?rFd{xvciRezAoI&Ge6us`wrEU$z0g&X5FR z5;b-Z*u-e|oFQfem@OAjRR?tg)1uUcKHX!F#nA=Ls>oo;oU2y#!_dhD_*?*3(}rvp zFKWHSd17lY^)d7rd5{*sB^^@`&>6u3rvYo{(s^%A6`6}3Q}D8|V715frC16v*v`4B z87#NExz1C_;jr)h88r32K3`-=Zf!H9LF!{>oWWBX9^n4;hI~5$UQ~{WcNkNwfZ??Y zcMR0|B&!()ty%bebo04qlHf&kVVOn7dgz3LZ&3$qL?g6QnyyE~Ex^VIT#mH-$gvxK zNoT8}sy6u#|09;IUpLlmvn{d`6$}yNyXe5@^MHz3U)c`Hjc`#k$8YFM`q>*P!k?yW zLKn~i+Ndy@d7R#Usmo3@^qB_!O%BMf=UHXbgS^GXUknX_mh(t7?T3zHP~Kmi4QhUa z#(Ng~#Q~hH5a8DW=0@Yh*6RX9i4nwH58tbF^B02j?I-A0_?{z_Z)9JLymuI?v5|H( zSS#J^8zSyw_|7%b%tKT#u@YIe6aC)gd(N{@A=&KH82rzjP86e35ba*2sQK0bdkn|r zUYI*0@p0wbUnV_^eh_LoS*EpVlDtob`lFoIm;zC?=I;Ad{HWn1+JV*ff|Z>&JgewP z*&Wj)foiB(cQLrhp+hF>8}HYpffFZk9<9WZrUR>)HYnWbX*w*&>_${+8G##h4yfGL z{WbGa7fo2|d>iFpQ_fpXEIPXB%Csizgud9e6s1JfwbD2yBrslpP65$*?t7p!r7IkY z`R*IFX$hcef`}R|JbpJFFemg z@Og=5&Inu{oyNiucOO4)Nh&PxEO2wbnW^B+OHS|MiK90DLHOu-vZN-fT!>4IRfxfQ zLJ_G^vLKX<27AINn4LDMEyB(Y)6@*k`C6=hbwpSlO~Q8XpKkkw(3aCu#}VugbymC^ zxhUleFH4e$`sv0fa_ndV(+w}~SF>t=8r`uUT)W@SxXhq&<8*!#TQJiZdk-*4AO8d_4~G06=@fK&2N*0#5Gr^LZ{R%GDLNeitkpP0Er!1k zv_cU^jo0kb2dIB55eCoUSzVU#@!-1 zBeqbRiIraxtUxz##Nlq*2;$T4-WRFY5}(trov3Ov?}d7PA@#h| ze4}a~D8d@S9O*Za6rqEIekR=in(?*fx+6AWICGAhW<4uq!l_C`0Us-sJKdq2;N}A`k$KMH zt=;2Th3g_N0<}8*_Z%*36ICd8b3v1dA!BIME*gIC(<}l@CZ)F*qYc+fqMk(SK`Z13 zJE;VbLAV9!t~-zfVzneV_%XWN%+Y$R#d0^rtTy+6MwdyHY;2_Sn5u{Wo!ib>yMUad z=!f%8PBbIOZURTohxZq2T}M819cq>Hv6gIdGn^ytM>FrYI8#~p^7llH-blD{S?*XZ zIF$hvxlCPDJ3$h>n*5Q6hmyhv#WU^7B46gDdcG{m*L341K%z2AEnj%cY+xN#htDs(q(UrTZO1 zo{}>H%Xw8o3wsk&;&P`QGc~ERB>!)>oFn;XWexIWhV6sKSB}|VJ-Q3*4O%@C#`JYZ zJ5?eB4*Mi<%tJ~`h7cM`fZ#zFO!ntW{VI?+KR<>!7$Go#aK;L<1vw&3DA zv9od=+(CcL%>tuylzJ zUt;Rh9=uUs{=GI?VKHVms_~cM|4bq3@{^Qi!{AvYNbbBW%9vh*n4&Wf@jnA9c}pNQ zojr90f{_a3Hkc9`6Fn*DxlQ8!Let2t+Ssa2{b>}1+i`EbC7O5byv^JFQ& zKBSLTo2QIJcP2c7M((qKNzg2&V}+KGEb=!<01`wSoUwumpvK2oaFmH_R87C&4PWie z^~GO@`(HlO%};m-U>ywTK$lo>7?B!cG~0jOjKf$$gJ}<<@{|Hz5jzLK4(&nSb#_J{ zxViTT$ZTi##zMse(^bS}8g3|};u5dYe7-V}x2ToZk zDk!Z0%&>0==R{}SA!OYkySp0mPf9FB@T`1F(XE8PpgsaO^LSlBj;IefD8?Z12e*&J zi4{EJy2{<2DJPJ#S?hr9mgffm4cegO#Dtv7aL{7V41mg%bXXg!LA)`|NjJc;S3dQ-+;MY;iZvrV(e#wTB7 z=54NuV*r!1mawG=S^#g0Ra5Xd4Hia1i`GrXK1vEz)BgS6zzGs{ClG)6Nw94hij_a} zcbPj>Gobg;b!GX zWC|0$mTWV>zyUj0uX)!pECFxj3(7{8zfCm2b+yMIWTicoVEaqTmHQbge6H>UIG+4~ zklP>Pv3i$4!&V-ey#yVjV1t>!$`Cjb1hvVD`{b!XFvb%fQV82net3?XskV>?*(QRP zgj9<^^-xiR0$G2MGeHKQQQmZgsLGS1FOW(IT#~Ag`AACTx(aA%&43jk2Ip(x3U(y& z{Wx|8;M5P8!);+7wi}@%$n1d=-Gd9kYqSv@ScB&atjE9=UuG@;p8zO}EOu8TuOfy+ zGjG(mY!_lHp+=)I5K&r$ACTa!4z)ZAJ9bJ7=sw{pDDHl$4YVp7*E!H{feM;u2QJI~S7Ip)V8ho?X8hBq`K2H&fJ0MmCO2Or?#|MBs?8 z&MYQF*_JboD`)Gqpm{g_!6a~ywgCM@3Fn@^I|ya4yZ6yx&{1h{P}Wa%IzR_f0mUbV@U7^Vl8kap1TObaf3--aH=~@+a(F|-Q{Nf=c6J)Vqv#)*dWQsY3LCr(0D$e z`N3{jQTtnz63TBG_7LZ3MYi>Dwjd)Juz&(G4j{ImgC;qxNr-k%BN2G(3Dr+w*8g#L? zy5yqza^TZ(P(YuaZ7fh|UHmabogpRE61d8IfaiD0ZzuRMPw^9%b26nlDzp|?a6p;u z30@*7aa{$-e9>9)Vlk2bbsX$a2;F_;w=hyon;)6?tJ-Vj0(jEZoC@1-SoYD0*q#(t z`idc4C)criR+vovId#4nja?(oFZx9;Dz^M8asVCMO4>jKIgXkrf*zfcKKwhDI8MAb zR>0dQK9nVB8m$~PH&<7Cq#K(h#{$j;b^XR$b;-&7#mAZ(8aqy-B}#_;7mn_tyhyEF zzf;p)MTB551PUwy@Sh9Tv`X9DD6hvhrfVuLE)V*rfhOH!y@o^qA%0mc-DuK%($aFX z$@i;$8B$*!d!svpH7JTaH~W7BPb1D~Vz>9=!!4ezm7ANszGE%drBWlz+* z+XZSOF&{CcAV>1{z@pPZ!dk*QfOkn~1QL>J#AEsMBhDujHHZ_ZU|}JOq%2}%{b4+l zOhNN_ihEptbM1VxoM7?Z6IEcPB~vbW8@vGqGq!dcBW08XT~`rQVQ(g#j@Eesm66a3 zYJo?xW~nEp29W=w_N)@#RgjZ&Y)Akcy)}a04vc&|m5{fi*{xv?NTW6fEBzHV5c%s? zk_V2odzaf-q1T0T0>V<{WXw8hTE@GPLxo+Vn!ndYj^+P!)he;}C~aDY=A@bLEd3}m z9R&eluTOCMtwKG0wJJ=ms+|tH#g`!-rv@gbuZ-npf(*%ju=enPqv>!5$j6{Vqb9+~ zIa#3^5CusPN!p$QGvJrtB8gxs?{e`V{Alh+tj~JMj}&@DhuA$Czhk#PpfDdV)-U(t z5P#O(?)6e~4()t_cLYS*jG61xSMG4^h#Nx?gy}}X1llpJySVqet|a}<2Ve`-S!5K= z!7*a9{{BYJZ2#6?9SLhC4Q4AJ#-N^&7Iq1AG^=-+GFG53G9ta0pAxg}Iq+8VJJYrh zy_}ZMRmxkX9SkoL6LMjfb77BmQ2vC22yGfq8EpUVN9m+7Yn4O!PS~Bfg_55BoMsa$ zaE7ywScAJsqP5MPW}iGHC{S^<$ecHeh%}g*Cq*&&^&Ah#cc1jWrrx9|E(&WV)NgZ$O13|RuEpiSc>`3{dA+4f86crNcjb=Sxc+fc~f&Oj8L+|d>W_} z{K}lZ5C6C=jd1gwF`hqzRvin`$Lo#_yFh_vx$bd?1opiWe&y7%71us7vWM36q2e4) z!6z1lyxZV#KBI^0peoRGj(Nuo-j#lP*#u6GXK#&0^Y)@8Z*P`$PF<6fF|4E>839^4 zp<3+SMC_k&d1tyoJ{DJ&5fgRJ z4p4+t^%)0vB28xOu7^Qid=;i^FwGTh}Ap7tUmS#1uH2uVoxeT*9wV@QDd89+N+)p$Wmcnf^ME62);XV6a zoqmriuBLMVQo>pZv{!{-c!Y@Zh9c>t%38a6{OWwAq;99$(j8DAZWYv`grXZx1x3rU zE;0W{J5wO(wxcN5fdi+^k2lA+fhNesisgx!j~FvM&BP!B;zJ8ob%9|Q?Q^ii@HC=v?FxPKnk$DpqX*Tlg?8uF&Nxy ztVMQSOmzm+`|@u^{eSjJx9;MGYikmo#4673tI!LYX-W1t(H8L;U3Vg`0*@1B76JTA> z1lX~$G&EZ0rMYlF2Lwb?P4BLDm577=yyA(c$Wsw&>wjNruuv$7pTDW|9mk4CMM-MS zw+3u4evz|(@YvGh1yJxc4wn052Nw9_f5Q5|zHrEQ2Id}9$}La_ou?Z$ICD zACmjSX~tUjyTnD%>Niro>2|@*TLxYO8;HS!`d*kQaqGU>Z%yQb!e+VA=l4VLcY_hx z!$Js3G@k%Q*a_ubryI?dr^3{4Dmb9oObuicbpsZltb6yY@+CruGt)B72*;4=4e_r* zk;IG|;L~X%;9UYH*d(aH2ZSG}YJP<_mi`pz1pft5=KB2M9p-!UZP5pz^N&;j-OMCo zI8*DWzjgUwf)eBHFsGs${{$u4c3}&K`Tk;;lDI<T=y*c*= zS|g?NKlJpWLk73;!ygs#y1(*+A$vYuhu&{@it=e-T9^jykvkZCyF#eBorlD;nEoD& zw278p3D1oq>Q31CHMiz{KsY2lGtj|Tb^kbjaCmQL*7#VP3Ns{(Q{kb_vg&wgk<2Rrwk< z)u1CKgAT4elZd$co=SaqC12VOmmbZNTvBI4#;nl;!F;aS2mkc}AWC?n!4~8(n8w}) zLdJ$cZ>%<{6?_DHJBVBoBPoTVmZBK)%jj!bhdkmcN16mnS?qY{{^SD5OKJ@icX;{- zkx$T>t!!aJt*`iCSUB`}IL~pM*lS!)=U&AVw2CMDKryH-_>NdW96SC_Rm@tv?q!ea z%yVRx>*wA)2Fets@*x2Y02cMp+)Y1DT~K-k65o6R!wK@x_}_oKC!B(JM&pEroiJog zDBw~l?u9}8yt8)Ke!`emde}?!AqSa%PV1Tpz%NCVKNi)Jc4DFz1be{ysSnj+!}m%CSPE-oLFWn$h}%F0;_( zJ=uh?x7=d&5!eo+Kk@J|y{DLOiUqF;cF$C`-NyqJQEsXL3Z7#Yb#m-cO{piVbwFVz z*&B$%|5=Ff_7YPYv%g_6p%QTVRlu~>W(L~2pp9ShW}e)J5Koqg&yno~1smR<;4`Y6xxdLW2OUI3lTW-O zwB5OdepyohuadC-1i*w2Rgr|J%i_^m0HG9`tI7q&&waXC^I4#$pnyHVx6`R#GBG;F~3b&mwS{ zO_c7xV7ru9_3^GlYYKE^-;-Tw<}5u_fg)QT|fWdmkW&@P+*Kxl84m|`D5Sn zn2VCj2?5vled9U8l{E37A6tOIVw}gNErkfuU-|boX!O{xE6e&YuAZmgfK4|I*!Yj)RKK48 zes=qvScu|bcjncf@yMmUQozd>IZa`Ek@^7C?s=avIdOM^s|U5`p|qfPAx?9nDQz-q z=NJ%L4DO{h!?1mxc1raSvcSlz3|mlCSQ1_#V0=Pz* zoV)@?ASa%Hp|z^b%1=800KN?Cp8|a+*f2sQPp!hdor_^Zw=05LsqzrOW%m3k5F}Nr zqku9zZSwSU@!g++rxh+|)(h=W7ghp~O(q21r-5Fkx6%M6Vv_hMv{ahv>-tY#Lsd@+ zgfT-_JGLk}NdH@&;eb10K|~7CrcCR-3`wR~cqrTy(ZX+GTq!Av)*Pn_x*~8;J$L+A z9rn71D&w#Fi2b|25p-PeQH|}|TZd+3+Dc#qAL3i8)#`BkPAn%-o5b%NPb;`W$@JjN z4mm{2KDy{kH&&k77IBURm#XYR)J8w|0vgxH5k@MX$L>J3BTQY4zd0&!uZm_;dmrW8 z{)B-PlzTxdn(MD_w>>!*Qci+J^8#>96E0}_TMp5BIA{V8z73GC?a3ea2FJc{HBSRm zuyKM?M!!iDC}+JU;c~xr!JPo4rWu#Z>27J1iTll!gI@}l)x4M_fI;bPIzPrR>1OpQw%BSz$F`$)>ATk`l_Lo9T@{(ZRtVst(J#dlb-Yi1YCs zI<7dUhH+c~C~nU4I-3+KXU98bysvQFe;IY-< zl&Si2P`z2(^ocpjY7##;($Yk7CGk-vFjxI9F>^j zK2~zq7h1KX@Y(vse$Xi5?L+(7!COSd--40w$x}y(NH^@^%lL9Y2W12Vcq@6Ue&#z{ zP?6*~*Nr}bPJITA@wq-akDXRim%sUUXaPgXyPu~L{8^!DRWN^xeDzB^``g?0$3%3T zOF$-#MkYw#XLJdYdY?w=VE$(;)8spknz!8XkeKh65fsV&185>CD%c*T;^99o zt=y&k4Z3vR6DnabJ#F2`Xn!PN1PU1I#f_1sr_&lriQjeKigI)YG=i+s{gkhMyKsEe8w+)5N!d=c#ayZ6nC&Wq!TZOG%X* zw?}&5kY6~t0I;cV15dRj-n&NW0+0ct{}2T0wFLoSv^O&;QpI*%-@_(J{u9nxJ`Zc| zxpd3cx1wldPelFmWbEjwEod6BpNk`}yz85XXB90uXtX{ZAM^{VS(Pd{hVM1EbfMDb zD@eeZs>0|yB~sCU_B>(aiI;lR^5i++=!(wkVa&v6b=TcV)8GwJzciQ{i@pq}YipSq zSS&ZbjKEK!8*LxeZ$m1`lp!8B;U3%sieqX_R+|ry)9gTRNX;-V5|U_S4WJ1682UIb z&jerMk(zcXzEXR+LrL+YyP)-4!eNnA^Y46{v^(tv0RdWY z99$z{anSbBcD_c+LSrxgomp%Y08ABQ{D1`D#O@Viqo|d^cn1sZM5SblaMasBiyd$S z%qUZX?S8#XJw$Qg9J3+3GP~X7La<0SM7LwS;;1|;#>nq(lzvhTVm?Lh-IrPSQn9

8c*Ru>bK9l!!k2 zG|7IS(e`)G1;#NQTM{fmfo}_HYD-B{N4-~85y$>vLAW}!6vW=3#1^r{#-m7!B6LbMbT?=`TH8EVP*USD`*iDaZa1UO~ZQYmj?QivqB|K$$7F|iC!X(ESbwmAQCfr7_ zL5gumIIQJhsUWH7GF}C~y>Oa1w-f2~^jVP&HBtYl1H;->)Ys!IvAf&GQ^8h>-Y`3JIVdR3W{Twz`&0y!^@dB7hC#c8ms~-gpd>dYPw?w~pI=j`r`P5OiIe1s` zI1A%*noi{ zk-Prcv5Vjn!%URInECFQau1>re2e1vN+1)Y@LNoM#XldTot4vV^xCif#fi;mM1SJCwve#El}L3lg7*wVb-Y&U6JSP<8~ z@E3doASSU2tXKHu@pJ&Cbp;&hYtB%<2gU%ZC~6=HSTnX5UU}P%?X^`hSu~S&)0syB z%DW$y1x;yc)|7bpJ^U7>;}Ii%=8kTllUr79WVKiK()$)~g?8*aAkm4W=O4gsz2`_J zNGcY@N4Y2?Wr)`q%VT3uO_O>-xL{T>ZNPI43k%UXlpim{L7Xc4s7z(5J=eJQQ~FQ5 zjEvu#xlPPB`$TXi4d@aS?G|WNeEAOfm#>y%^2Bw zT?VFSo%Rn$FcpJ_h#gJ@QxHLoC4r*9mK4mgC4MyP{B%5FewW z;T^w&GujI#U^I~;8uZGm=>Hc310NzJAGG4u$P|>wjP>l^;aens>s^BLYXC4##Z(<( zWEK?YeYh0(0uRV|-o%>e)0Dja%BxRN{g?~BtJPy8qVj`F5Vpk2)# z?H=cDpfsmgnSwWpj{gbCf*(sJdwWkt%8yb=@YuR2$R+q&pLp&=HPPY^cxyPKPZ~jn zktad(0+3nQBu_WH0@Ph+X2YOiCriThU?i99lrEVM^YYKH8=#To!KB0-8fa%Bq1OOL z%+IgHh1@Z?5fY>H0R=XKsa?Nl=li>vd9u`a0g#A-(pMrEI%NQvWalg{N*Q{uy;Tf* zW#6Xc^JLoYWAZ=t1|PBNXNm#C{kN*o>pDQB&wp6ecoEuF+QZy1W$<**_CTT%2~l;r z+AbvW45@3e`o(I>C&mtBiXw%0A5-}~w~12Y5eF4`+wNRThzIq`z`=?xdX?Ai97o3) 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%0A>A|68fit4F{t{Cw6>&8^RiVyztb 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%0A>dR22?P+=33 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

?Tw5^j2|F7UlNIcESP9uzpmkq?c1ue(<5C? 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. -project, funded through the U.S. Department Of Energy Office of Fossil Energy. - -If you use Pyomo.DoE, please cite: - -[Wang and Dowling, 2022] Wang, Jialu, and Alexander W. Dowling. -"Pyomo.DOE: An open‐source package for model‐based design of experiments in Python." -AIChE Journal 68.12 (2022): e17813. `https://doi.org/10.1002/aic.17813` - -Methodology Overview ---------------------- - -Model-based Design of Experiments (MBDoE) is a technique to maximize the information gain of experiments by directly using science-based models with physically meaningful parameters. It is one key component in the model calibration and uncertainty quantification workflow shown below: - -.. figure:: flowchart.png - :scale: 25 % - - The exploratory analysis, parameter estimation, uncertainty analysis, and MBDoE are combined into an iterative framework to select, refine, and calibrate science-based mathematical models with quantified uncertainty. Currently, Pyomo.DoE focuses on increasing parameter precision. - -Pyomo.DoE provides the exploratory analysis and MBDoE capabilities to the Pyomo ecosystem. The user provides one Pyomo model, a set of parameter nominal values, -the allowable design spaces for design variables, and the assumed observation error model. -During exploratory analysis, Pyomo.DoE checks if the model parameters can be inferred from the postulated measurements or preliminary data. -MBDoE then recommends optimized experimental conditions for collecting more data. -Parameter estimation packages such as `Parmest `_ can perform parameter estimation using the available data to infer values for parameters, -and facilitate an uncertainty analysis to approximate the parameter covariance matrix. -If the parameter uncertainties are sufficiently small, the workflow terminates and returns the final model with quantified parametric uncertainty. -If not, MBDoE recommends optimized experimental conditions to generate new data. - -Below is an overview of the type of optimization models Pyomo.DoE can accommodate: - -* Pyomo.DoE is suitable for optimization models of **continuous** variables -* Pyomo.DoE can handle **equality constraints** defining state variables -* Pyomo.DoE supports (Partial) Differential-Algebraic Equations (PDAE) models via Pyomo.DAE -* Pyomo.DoE also supports models with only algebraic constraints - -The general form of a DAE problem that can be passed into Pyomo.DoE is shown below: - -.. math:: - \begin{align*} - & \dot{\mathbf{x}}(t) = \mathbf{f}(\mathbf{x}(t), \mathbf{z}(t), \mathbf{y}(t), \mathbf{u}(t), \overline{\mathbf{w}}, \boldsymbol{\theta}) \\ - & \mathbf{g}(\mathbf{x}(t), \mathbf{z}(t), \mathbf{y}(t), \mathbf{u}(t), \overline{\mathbf{w}},\boldsymbol{\theta})=\mathbf{0} \\ - & \mathbf{y} =\mathbf{h}(\mathbf{x}(t), \mathbf{z}(t), \mathbf{u}(t), \overline{\mathbf{w}},\boldsymbol{\theta}) \\ - & \mathbf{f}^{\mathbf{0}}\left(\dot{\mathbf{x}}\left(t_{0}\right), \mathbf{x}\left(t_{0}\right), \mathbf{z}(t_0), \mathbf{y}(t_0), \mathbf{u}\left(t_{0}\right), \overline{\mathbf{w}}, \boldsymbol{\theta})\right)=\mathbf{0} \\ - & \mathbf{g}^{\mathbf{0}}\left( \mathbf{x}\left(t_{0}\right),\mathbf{z}(t_0), \mathbf{y}(t_0), \mathbf{u}\left(t_{0}\right), \overline{\mathbf{w}}, \boldsymbol{\theta}\right)=\mathbf{0}\\ - &\mathbf{y}^{\mathbf{0}}\left(t_{0}\right)=\mathbf{h}\left(\mathbf{x}\left(t_{0}\right),\mathbf{z}(t_0), \mathbf{u}\left(t_{0}\right), \overline{\mathbf{w}}, \boldsymbol{\theta}\right) - \end{align*} - -where: - -* :math:`\boldsymbol{\theta} \in \mathbb{R}^{N_p}` are unknown model parameters. -* :math:`\mathbf{x} \subseteq \mathcal{X}` are dynamic state variables which characterize trajectory of the system, :math:`\mathcal{X} \in \mathbb{R}^{N_x \times N_t}`. -* :math:`\mathbf{z} \subseteq \mathcal{Z}` are algebraic state variables, :math:`\mathcal{Z} \in \mathbb{R}^{N_z \times N_t}`. -* :math:`\mathbf{u} \subseteq \mathcal{U}` are time-varying decision variables, :math:`\mathcal{U} \in \mathbb{R}^{N_u \times N_t}`. -* :math:`\overline{\mathbf{w}} \in \mathbb{R}^{N_w}` are time-invariant decision variables. -* :math:`\mathbf{y} \subseteq \mathcal{Y}` are measurement response variables, :math:`\mathcal{Y} \in \mathbb{R}^{N_r \times N_t}`. -* :math:`\mathbf{f}(\cdot)` are differential equations. -* :math:`\mathbf{g}(\cdot)` are algebraic equations. -* :math:`\mathbf{h}(\cdot)` are measurement functions. -* :math:`\mathbf{t} \in \mathbb{R}^{N_t \times 1}` is a union of all time sets. - -.. note:: - * Parameters and design variables should be defined as Pyomo ``Var`` components on the model to use ``direct_kaug`` mode, and can be defined as Pyomo ``Param`` object if not using ``direct_kaug``. - -Based on the above notation, the form of the MBDoE problem addressed in Pyomo.DoE is shown below: - -.. math:: - \begin{equation} - \begin{aligned} - \underset{\boldsymbol{\varphi}}{\max} \quad & \Psi (\mathbf{M}(\mathbf{\hat{y}}, \boldsymbol{\varphi})) \\ - \text{s.t.} \quad & \mathbf{M}(\boldsymbol{\hat{\theta}}, \boldsymbol{\varphi}) = \sum_r^{N_r} \sum_{r'}^{N_r} \tilde{\sigma}_{(r,r')}\mathbf{Q}_r^\mathbf{T} \mathbf{Q}_{r'} + \mathbf{V}^{-1}_{\boldsymbol{\theta}}(\boldsymbol{\hat{\theta}}) \\ - & \dot{\mathbf{x}}(t) = \mathbf{f}(\mathbf{x}(t), \mathbf{z}(t), \mathbf{y}(t), \mathbf{u}(t), \overline{\mathbf{w}}, \boldsymbol{\theta}) \\ - & \mathbf{g}(\mathbf{x}(t), \mathbf{z}(t), \mathbf{y}(t), \mathbf{u}(t), \overline{\mathbf{w}},\boldsymbol{\theta})=\mathbf{0} \\ - & \mathbf{y} =\mathbf{h}(\mathbf{x}(t), \mathbf{z}(t), \mathbf{u}(t), \overline{\mathbf{w}},\boldsymbol{\theta}) \\ - & \mathbf{f}^{\mathbf{0}}\left(\dot{\mathbf{x}}\left(t_{0}\right), \mathbf{x}\left(t_{0}\right), \mathbf{z}(t_0), \mathbf{y}(t_0), \mathbf{u}\left(t_{0}\right), \overline{\mathbf{w}}, \boldsymbol{\theta})\right)=\mathbf{0} \\ - & \mathbf{g}^{\mathbf{0}}\left( \mathbf{x}\left(t_{0}\right),\mathbf{z}(t_0), \mathbf{y}(t_0), \mathbf{u}\left(t_{0}\right), \overline{\mathbf{w}}, \boldsymbol{\theta}\right)=\mathbf{0}\\ - &\mathbf{y}^{\mathbf{0}}\left(t_{0}\right)=\mathbf{h}\left(\mathbf{x}\left(t_{0}\right),\mathbf{z}(t_0), \mathbf{u}\left(t_{0}\right), \overline{\mathbf{w}}, \boldsymbol{\theta}\right) - \end{aligned} - \end{equation} - -where: - -* :math:`\boldsymbol{\varphi}` are design variables, which are manipulated to maximize the information content of experiments. It should consist of one or more of :math:`\mathbf{u}(t), \mathbf{y}^{\mathbf{0}}({t_0}),\overline{\mathbf{w}}`. With a proper model formulation, the timepoints for control or measurements :math:`\mathbf{t}` can also be degrees of freedom. -* :math:`\mathbf{M}` is the Fisher information matrix (FIM), estimated as the inverse of the covariance matrix of parameter estimates :math:`\boldsymbol{\hat{\theta}}`. A large FIM indicates more information contained in the experiment for parameter estimation. -* :math:`\mathbf{Q}` is the dynamic sensitivity matrix, containing the partial derivatives of :math:`\mathbf{y}` with respect to :math:`\boldsymbol{\theta}`. -* :math:`\Psi` is the design criteria to measure FIM. -* :math:`\mathbf{V}_{\boldsymbol{\theta}}(\boldsymbol{\hat{\theta}})^{-1}` is the FIM of previous experiments. - -Pyomo.DoE provides four design criteria :math:`\Psi` to measure the size of FIM: - -.. list-table:: Pyomo.DoE design criteria - :header-rows: 1 - :class: tight-table - - * - Design criterion - - Computation - - Geometrical meaning - * - A-optimality - - :math:`\text{trace}({\mathbf{M}})` - - Dimensions of the enclosing box of the confidence ellipse - * - D-optimality - - :math:`\text{det}({\mathbf{M}})` - - Volume of the confidence ellipse - * - E-optimality - - :math:`\text{min eig}({\mathbf{M}})` - - Size of the longest axis of the confidence ellipse - * - Modified E-optimality - - :math:`\text{cond}({\mathbf{M}})` - - Ratio of the longest axis to the shortest axis of the confidence ellipse - -In order to solve problems of the above, Pyomo.DoE implements the 2-stage stochastic program. Please see Wang and Dowling (2022) for details. - -Pyomo.DoE Required Inputs --------------------------------- -The required input to the Pyomo.DoE solver is an ``Experiment`` object. The experiment object must have a ``get_labeled_model`` function which returns a Pyomo model with four ``Suffix`` components identifying the parts of the model used in MBDoE analysis. This is in line with the convention used in the parameter estimation tool, `Parmest `_. The four ``Suffix`` components are: - -* ``experiment_inputs`` - The experimental design decisions -* ``experiment_outputs`` - The values measured during the experiment -* ``measurement_error`` - The error associated with individual values measured during the experiment -* ``unknown_parameters`` - Those parameters in the model that are estimated using the measured values during the experiment - -An example ``Experiment`` object that builds and labels the model is shown in the next few sections. - -Pyomo.DoE Usage Example ------------------------ - -We illustrate the use of Pyomo.DoE using a reaction kinetics example (Wang and Dowling, 2022). -The Arrhenius equations model the temperature dependence of the reaction rate coefficient :math:`k_1, k_2`. Assuming a first-order reaction mechanism gives the reaction rate model. Further, we assume only species A is fed to the reactor. - - -.. math:: - \begin{equation} - \begin{aligned} - k_1 & = A_1 e^{-\frac{E_1}{RT}} \\ - k_2 & = A_2 e^{-\frac{E_2}{RT}} \\ - \frac{d{C_A}}{dt} & = -k_1{C_A} \\ - \frac{d{C_B}}{dt} & = k_1{C_A} - k_2{C_B} \\ - C_{A0}& = C_A + C_B + C_C \\ - C_B(t_0) & = 0 \\ - C_C(t_0) & = 0 \\ - \end{aligned} - \end{equation} - - - -:math:`C_A(t), C_B(t), C_C(t)` are the time-varying concentrations of the species A, B, C, respectively. -:math:`k_1, k_2` are the rates for the two chemical reactions using an Arrhenius equation with activation energies :math:`E_1, E_2` and pre-exponential factors :math:`A_1, A_2`. -The goal of MBDoE is to optimize the experiment design variables :math:`\boldsymbol{\varphi} = (C_{A0}, T(t))`, where :math:`C_{A0},T(t)` are the initial concentration of species A and the time-varying reactor temperature, to maximize the precision of unknown model parameters :math:`\boldsymbol{\theta} = (A_1, E_1, A_2, E_2)` by measuring :math:`\mathbf{y}(t)=(C_A(t), C_B(t), C_C(t))`. -The observation errors are assumed to be independent both in time and across measurements with a constant standard deviation of 1 M for each species. - - -Step 0: Import Pyomo and the Pyomo.DoE module and create an ``Experiment`` class -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. doctest:: - - >>> # === Required import === - >>> import pyomo.environ as pyo - >>> from pyomo.contrib.doe import DesignOfExperiments - >>> import numpy as np - -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :start-after: ======================== - :end-before: End constructor definition - -Step 1: Define the Pyomo process model -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The process model for the reaction kinetics problem is shown below. We build the model without any data or discretization. - -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :start-after: Create flexible model without data - :end-before: End equation definition - -Step 2: Finalize the Pyomo process model -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Here we add data to the model and finalize the discretization. This step is required before the model can be labeled. - -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :start-after: End equation definition - :end-before: End model finalization - -Step 3: Label the information needed for DoE analysis -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -We label the four important groups as defined before. - -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :start-after: End model finalization - :end-before: End model labeling - -Step 4: Implement the ``get_labeled_model`` method -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This method utilizes the previous 3 steps and is used by `Pyomo.DoE` to build the model to perform optimal experimental design. - -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_experiment.py - :start-after: End constructor definition - :end-before: Create flexible model without data - -Step 5: Exploratory analysis (Enumeration) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Exploratory analysis is suggested to enumerate the design space to check if the problem is identifiable, -i.e., ensure that D-, E-optimality metrics are not small numbers near zero, and Modified E-optimality is not a big number. - -Pyomo.DoE can perform exploratory sensitivity analysis with the ``compute_FIM_full_factorial`` function. -The ``compute_FIM_full_factorial`` function generates a grid over the design space as specified by the user. Each grid point represents an MBDoE problem solved using ``compute_FIM`` method. In this way, sensitivity of the FIM over the design space can be evaluated. - -The following code executes the above problem description: - -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_example.py - :start-after: Read in file - :end-before: End sensitivity analysis - -An example output of the code above, a design exploration for the initial concentration and temperature as experimental design variables with 9 values, produces the four figures summarized below: - -.. figure:: FIM_sensitivity.png - :scale: 50 % - -A heatmap shows the change of the objective function, a.k.a. the experimental information content, in the design space. Horizontal and vertical axes are the two experimental design variables, while the color of each grid shows the experimental information content. For A optimality (top left subfigure), the figure shows that the most informative region is around :math:`C_{A0}=5.0` M, :math:`T=300.0` K, while the least informative region is around :math:`C_{A0}=1.0` M, :math:`T=700.0` K. - -Step 6: Performing an optimal experimental design -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In step 5, the DoE object was constructed to perform an exploratory sensitivity analysis. The same object can be used to design an optimal experiment with a single line of code. - -.. literalinclude:: ../../../../pyomo/contrib/doe/examples/reactor_example.py - :start-after: Begin optimal DoE - :end-before: Print out a results summary - -When run, the optimal design is an initial concentration of 5.0 mol/L and an initial temperature of 494 K with all other temperatures being 300 K. The corresponding log-10 determinant of the FIM is 13.75 - - diff --git a/doc/Archive/contributed_packages/doe/flowchart.png b/doc/Archive/contributed_packages/doe/flowchart.png deleted file mode 100644 index 2e66566d2f66774927be3362919c4cc6d5346916..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160954 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�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 |K4W2 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

fTjd@^A+I^ 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�(J? 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�MQ-|*eDiZD}EulT|5B_o4rH=K!E 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ξBb)-f^~bQPsq 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

LAG!Z8vBTbUc={&?tORVm zD&ym|@1W2{UKTVp-f2E)5lUzx#etK#NmZlo0-|=xH7ay<%e(>*t)9zBm)U5{jvQKQ z1SQMB`KD9rP5`psC?irlE5zd=d?jORLm}oLB3lDA@vQ;d7uvD-czG9;pGQQJ;qSW-OL&+XhE*Op_?{@hFzL5+i z`@ToOnOKPr&J%6agx8k#4*!r&Dxh?1hH|SE*|b(#)c1Zr-FLXr*-F{5yS<}Dc9B=z zJ(P<2)?NdazX^g{0cFB2ld%7Q`EXOua~Fq?ZDK0{wT3X{h3ACx*~j(*>r$2i0fBGz z#=BcuT&75b7aPp2zaIT4G}p{Eiv=M}=O}48Agq>1j>|6aq1;hi(K#bwMAqlj+2KS_VMb|^n=ckjjHWPk4I0yzj~%~BE$Y$)w?QrwYdQg zH&3nij5=<%**GWt@kK43hfB-!^Bt2X>oPi*CtPCrqVjKH3wOA*>S*4?QIL81fVCQkgT@ zdL{xgNl^9Oc?dsv-{N#MX5^IGdNeP%KrBN`xd#_X{13XI3C;5V=wRnUPd5%))I(yo z|F$4t7^zKuVLSQHx{N22Y{iF`(_E1L%L}{=CYZABI!HrnfsWFIl^K{?A`jn^;Q~%K ztLxeC@t*}j zR6f9bo(*k{kyNyI&H~F-0h->|x(IHze+yo6qbQi&3hRK-{9D@GsFre<|-Iy9lL>YU6XfS3P@{zvaJ*+A-}C8%T!xMTrMo zlvYw#w<7N-x;6YX?gM#Y#wxn^_4u_1MdI+bHolAn&I#Cdc7&hjYF>hCF17uivce|) zZq=zmB}3L8hufD?W*pC~XY_0kM_FeWz|0lNje%34Hksb4Q7P!4HR7x_I`)7!fblia z)IQijnAem`c+{3a?S){6gC-deLe=MsYYFlP?`gvcXkLZCd8A7Ahq@eHyiE(`Jw61c z`tx&Q$4+C+onUNO=@Eo>;sqd$DR1T#;ueo1v+HDRn&CUxYbgu-P@xJ7EK(~#3MW6% zC}>edG;toCTfFS233fHdrPiU`kS@?TyI~%AKWaj#T4GiK$;$qPESNS|@C^06D-PTy|)YhJPrJR6l{s zS>KSzE1tz3HBqz1yN`Nj% zCgAG^APQrb z#08jy62#*H91IFu1(cZ{|27w0dWLd{I&q|YAs^mql@t8V1tEqWZ?~bKTTHPeaT-D$ zZnrPJfOG+`r=__&@jw0Fs2Rm*P$PQhNCTTSc8o*KcI2El5|mwfitgKNz{}fzcgmev zSn2Vb33@BxcQ)23SZsUSTz={)XY5ZOS(!y;C94o?8w2D;t_bD_e`30F}9?j zd?TA`Z`B&uoGKl(upet3#B#ufDX%*)6aDWtyKh$1bxkUGS;{n|pIrr<;0BbPag+wN z*nm<0K8e?{+E5D*1+bK1exDx$O0H3yQ2euIs!S*gztCb$Ge$O(V-ve5Cu2(a!iQV7`(LBy$K)ImQAQ^E~ z9uq;Rk8j&k2lAYSJ0XOj{y3Q=vQeVY@V#q8xyE196J~M@_aacm{wqF%5`~dBQpb@D zcReW(@EJV>J%WMc3CLlAYnkAN0qi_Y z3_Y(7r201l_iG!gd#tj7P3a2j=8{~QuBl?{RjERQQ=moZfDdH)A{xCmJXOPg1cCUF z1wbA4`WCbQ^mEED_J0YDgL00RrP!Tug$gyf{RN~cbOFYL|L(OaG0;|=CC=CZz%MI* zpi8A&3b-bpx|bSc-hxl2tS>=hbAX)jjFvFigu)gIVwa$Ff&dBfR;vO|KUMZydoDoL zF&^Qv+VB;in$plyIR>2c3CTM$k76|NYPTo0M(ve`!H(2&w9hEw4kKLYQa}WuB`up6 zB5>~qC1sxed&8$hscyvT0!wZM=^zw)@U)#|2C!W{QZd0}-$6g~R0e>_^6bIWow1x= zfq}b?v#GpU-k(Q99fFr+zA?ixD~svA`F><$-e$jJ6(LL_5&v^<0(jO6qb0G#k)!u4 zu`q(H%d{ItVE)zY;J@D>U`PF(7Nb9*G7)7F3K%TMkJIZPA)2;?^EC?Oh*q-tr5WWb z7r`f;j*38@0c08fZL0bL95}yo|LV_HfT3-vB6zW+xLq;~u3V_auhqoz2$KE~C~Z0? z%^2TlkVhQxjPBLW20AE4v&>UN*SMP^(LA>St6w1mlJ66HcL|4y76z=S>?*K?BsA4o zHbI*{;QzagQs4nd9t?Kap!=$(0FPe;Zg?JAi*i1ot1)%!6%o%4_ROv4`vzE4Jw&n= z>T8!wjbe3^fC;Z7i+r6T(?qowlEo9CS6}JQH_b=TeO&X$|94AaSLJ`CTxD?t zh`SMQuNX7!#+Ul8=j_#Akzuyb`KiHT!BRvAw{}iZK3p#u-T6j7?77!uC+Sk$sI#)M zsn9$2ASEmKu?hcR_)HGFU+!%?rB6@mD7$LSmv^?-K*H&9^GlslKaKUkRav2pn?_Mh zem#Ei(xoS{7iF_~pB*|PnGvF999D%hzD4QO^{(^XrbRg3#u;IUNZ`#jr9Pleff2Ne zCYbKZy}KW9AKXtegQ$i`dm@)!ul`Rt(BW$oz3RI_boqIu0r&_8m9GTA&y>q6)qz8d zYGH|nMb1mS=%}Jko$4tPY_aF6II6ZIX%K*?hY3-7flML@^{LOH6UV00jxI( z^dlAzE>YHjL*TOIx&sE;;$}~RGD2m!M8eE+L>chk0hMUFVV1?-KsM~grtz1b-k@<2%qh5Rf}BF_Dm9z-Wq@}>PaOl!ufam6Hw?m1L= z0m&hq%NL?VIZZc=KJYxew6w?ln$=tFl_zSu{Ry5G(F~ju@ChR;0AJ4VgKiva3+8@_ zRGbB4U~yocqnb*4m-4lx%%2S9KY*Vm4tb%22iTrat$_G0qwhCh@nHay5fFmX1qgpR zie|TyJBLYR1zIt zj4sh-Z4HtP3@YNUG)t6ceND|hI(t1RR==!Zu|L0o*H5xuZ5A%h_nduiBVA!_NxRq& z7HAX@>@vpWqz-%)xGCf2APV6j7RtZE6w zopAjXG^k$@)yfN7=hRs%kh@MrKY9#^Y;}nXGA82mlSP`mX{-<{NGVXRJR7@8DYYpR z#|)$0qEIJNSp-JKS3)4&LL0F++^@C?9g&U!NFl*!He+NZAKqUmwi`%@)>!V5zz4hf zUtFS<>Wm=kga=O*g%xFuq>6ME-}G3>x(_(+Zy6nTd0`v|*bdox2)4zmVKe6;qRFd9 z%fH}+b$M>%d@gyjF~`a z5>mUk6&RPE$8Zu=%3(mIob!T=!x)krf`xLi1V=M`qS?X(cLm$b-JxjOvHK<-yIFrZ zTV{C*;;<1b-QrK>K{wM3C}){&m8^k6InpOj;8TYJdS=UEL0t8**E$6^4sExdF@NF! z^pz6Cj}>4Ny`sI{NcQ-#s{Yn}x{^PwHLtQbenAc#%Qr4}q-XqNUT3~@&~h%%axCh! z1rV-EELr|r)Pjklq-0sSSxd6zu1ppovutj8QyMQA(HV>2!2ov%Wvh()S zKAJ>B%gGT?I<)hbZ=`?l{n;A?tQ7EeU=sE6r-Ruw;NGF#>g7rXL+TW_2KBKqLHixf zH^-5^CF&y!@{dLez?tKc4wNg;fk*JalstfWK*=Dk(*(T-0pRd5&~AzVFEHLD;Pw$K z3n>B6MnCbdv;(|bbOV(x`Q?$nt4OM;rWico@W;lpaP#es^uKT0aLxI@t#3bjrkDC6 z6RYeQ)T9W;$U1GbR6iZCOj2BMn3n>IC;4)%Koin5E82`?17jeZ3?OAPY%xM(x$H4O zDW-@z3%CZBxbu2qLgn`XXCnw2cfDyP6|28QSpl|hH zd`ETbod^CNklcAY>iB`Ou(Ynl>GN_V{4{_YmR01)0c~}9{lq65ls&}KkkdVw5Cy~# z4%Fajh7h0Twfw1(BI47W{12Wc2$?&f7zRlplWpEaBMG2)PKqFS3GyZR#>$csAL{Ji zhZ^*sJXA2OUX#G}0xRx~;stR-B)i2g4d z;=d|An>O#N=*yi8eXr;+aA0*}4z@_FED7^rF+rAus~cn3N*Yxc~t_i|MbC4JC@R5ga~E7y#hXD}y6l?tZsg0FlX3 zJN+-O+kddJf#+Wti*(==IZ9S+)HwNS(t#uyPI_h!yqY*N@c-$P2fx8UKcocx+|Y|d zm+0Op`U(7_53g1ZdCIc9H)r9%1#%+r&!C_6S`-j|DxhX(Nih|%)Lr@C32{pzZNYX9nx>zYqn|Ml?Fm&dIhct{M6f%+aaQgV9n`vT*{Pg+E|i^odc}Z!#m8tE008C~x1cSXSE%TTq+~1MP;SRxEOoYfq2QwvMeG`wQ&|jZ|ZG%rhW0esA$`fnA zUFid{*+dP6*R)4+{brw}$#KeAk`GwmpbDrK>1!p8{5oL&-FaE^=WYmr|K&}ORJ1pr zhy5w&4n1j9eE<|upzZqfdkwp{_#yLa2arf{2vF! z2}$utQQ8A=N;fK?vPC-k`rR*55dE&M)^76a%|{V&mLm{0v=Gi22m`ti56|CoZEU(b z!Tv{kgazO&Dne{7>wko?_IO}TY|6pNK%jsuOBOWje@%izaeEY%7sb`BBsAF|qKC=^ zP*<~1w+ux?f~N}?u&{fb z;{=rDNmY6_w5Y}i7f@b-_v}Y8IJI@(BUMdX{bnC*@*|GkU*24_R1VHJ2pP-2=_tA^VR7#SIvEN_DGcjIp4 zn--wSWn;?*iA`Ft7z4kZRTsrG4I2IGXKG`|cKqfNuTP5WX`y+Fle*IpVOA4qotXuji;q5^+zZ-hb?q zhW1YSoYVl8SE9tHAh2s2!wRIpa9F#jp&vLo48NZ$1+(MF{9QicF!hc)0Dw~BfE*iw znY{$n7((+afVbf__^ru_May(x*!-sa_e-+JFFlIqO^7F}gVskcW8&^)|L5-fd$ew% z#ZteOUH^N^TLu%jm14whs3(e1T%&viDSonX2u#l08L;`ftw3hZ&bC2|+L)~EN0hCo zS&98ltzLo#vn#7J`_p=NMi}$HlpGufv)aPq;;u&-VuP7xG;snH7*5~CE1QdXu?>sv zN0*}>;%E4>-3e$Pl;7b0udUV)Oi+q|YMjB$>UMa)R@A!!*1;uWP4@1tNB~%Q3H~O6 zm)|XysJ!3s)=YAJmKB)3(l~19W0FlY@XBHnX&u;ABQCwUR$!vKnco#cNY1LX8+kGm z1qNN+lDu0{G?RU4+fJZo?KfkWN&?~b?_9XH0sA?Vv{J=I#`Tx-CUN{8bh4R^WOp5# zqKNUL#6-$;Dz~qji)mp4Gm&U9#d?Kzmc%qeVUn$lpi{rij*A*~i!LP6*vAcw5M1lw z^hOWo$!|*e1-M!5eZJA+a7sK!1d~L?knhsb3~^~}JYyQ=k0+K*FM%i4zqxKdQsN{X zJG>Lr^0Mv%70_Sg30!X`;2;yz0Ke?t*X8MyAaG|-x61n@rOEAn>(>y1+mALeyB^Tg z>AMjHKZb53umSIZoO^%Cv=ki_C`%w^8!EC?NIkT5*x9qx;P<$S;bY>8C7lI!tv{FB z8fI?lm)`|Ptt-Q%zRBbLj!+8(+l`Y>*^@!iz+7y7|9$kNQiG0J&3*g}q|N?{I4Cmw z)cumC&i^f7&;I)Y7AUo>&tuXfp{+cBSa4<&y30mEBCO!IG#x(%vd39sTe5iu94=#z zbND~xjAatf*&mT7%=|Dhb1N3cWH^bf6!PoF_Z$Nx(NY&VGzDk0G z{`*Pn7m(C-)OxbgI%T-TfjW%%jm%trAw$GnvmFhm4aA9iHcxvS53t4xlfj!n0r~La zFK|2(b&h~*pqo}A3(zdXhDw1;L^Mm67noLu6LY07oT=6erWeJD`VFI`htiKFOW8m- zLG}M~PEp66*Y&t?4XsK%1RxRP?E;|65)J;JrTcqgV{*_CB;}aKnYIc%kFv~s6x_0g zk*3f556gV!d%Y1&1_TZiu#+Dy)P|Y%@NPhU3SGsh%*VV3Azb5K3A`rN-c=?x{$PP1 zGdJQGaQV+$!N}5hL~?L0KYEvMIFf@Cj?6H^XIxru52GpNkM=9}!`d_xZ`he}fs)5p z$z2ggNv$ai@oK~?i`p-Z@r4I6()8O>w`FcFBc^msviGYiYe)*r;n#P7~YmC{z zjL$Q9CGPd-GB^_SrFp~8I=qcxCwzz_8djm1aI{z`dw$*Kg3an?3tZN@ZH~mDH=09w z0#?fLt#7a5mpDP|T>iS{nz;5FgOFV6UOqLd?Ht8Tf2ziZ-2?U7If6u|%pzdT0a-~~ z+*O@3+(mko&svv4y;AtiAlRTvr()?%kK%36tG%aMFsr}|o5j*s9PO&*3PoRZq_N(d zS9A#N9hqErcd*;VwAca={0ubPLT?IcM}0$WbmK>u|K60v!bIU?Zv zY@*sO%*ZG!_F16hjhw{UHd_6d zd^$N?_d15Z+CONpz7(o>H`JWnhvYkF;GeXOS&jL|rpHo3EH3{5!$F(?mGdJRVlyP&=Oyh>h_OeGZ zJ^ej;O)UmpP3Jap-AiY#(t9fZ>8lM(*m{xL)#L7=#*=a2y!fP{v>|AEcu8kgqUeOD zzf-=M^nEGc1oNLx&Y(JJzn!}2Mz6yNQaN#YqRWLdm|HZa0}sQEv&DtN`Dw?Mj*mio zwa{p9QJZe!y}~Wier9I##E2b9-H-m59qMzrW|MiRVUFz&l<}L${E<(P(5m^RvfE=q z*I8>E~@<{4l=JIO2=iq@wVrQA>ff z(IqYz@ZYZXSsb|H^?UB#?tHRY14y840Lq%ZgOItsss_%H3THMLQEk#_H(Xf1uwd=9 zSCmpg7e2`lJlD} zB)3UxIc{MHWDR{^+rNdlm2d8D_GLy6^QZ^4d;5^2!oG`qC5f^n58zh%>{-*56FMtc zS02ig%U;OJKTccD5b(efE~ZTvWjpDk5L2awq_W{QQM%!qL`?#{Eh;tSGQ<37YIk}j?*yDxjHT(3<&MTA-}DFM?Y-lM+BjH$2K<(fLlPdcpH;W%Ao{E;Era~x*<+Ak5z{X; zG$}N}VFY?LqZa>qqdMoe8=7@MVDE3E+W)|4=4RHpPZ!C!T0m0V?t0@!1F_Q7#t*7| z1WDgcL{A$H8>NSmti@(oEj43FZ-YY!&-$u}?fR%^Xx61fS9 zAgymce=-hON79;O*XipfUSCEfL{p!xgYU>N9s;rVuN3(VUs5b}$ATm(pfh5z1yE9& zKW?Cp?I|pEWFf5tA%aH;PSR_A>q%DxHK&lpi&?rOZ3>%g36E|;k((1hj(;RG1?azl ztUE8CS?f_0l2KqN7kobD@#};DuM{~#fofsh6(O{DvSva=u~VZm15Hod)3W;`@0}8= z3Go~TlCc?H_!>4hrrJzb0Saig^kT}iPf=9AH^9<%a_^GfVOOBUxxUAwy$Nma znRa?$Fz0OM$SRa_M0XN#XUj-gFl6ULvp_Mthe^up7`$f z+#>&yeL%%`i5zjXb8v72Vx0G&i`bX~bF>Ae5n)$OE_FN!J9B0c##?CpoZj#%7g;yoy!n*@S`Q72`MN%Nua(2gJp%h(=Pa*OQc~P z-yY!?85=UbPkpn2=#w+C&8(+kKm2!j1h(yMrxs}kIRvEp1*~b+Zcb+L6;ZWW`13@2 zk0s6la#`8Igfci;cu9Y>o}M%wu2%I=jlDOL&Y>{WfuFF?u{NHb95zn_Qb^3*g?uf2 zvnm$ZxC6%Siz0n?8nt7$X^F-+$GgZ9=G4|qDnfB>wQGQ+$Om&duM zw~pCx!2mSIa5BPLZXAkZS`S3woqNmC5K0u}9nd(jI?1^%92P?4C2x|9%DJH;2=s^y zhW!!BOT30AlAMci=*3?`u$iDhhZ^2v9a0ExJc9`WkX;cXMp>+Wk5N-ZPY9M3Jw$vH+z}4x-NwYIby+o+Qcv#?PQ=v zO#->c9o*xj{GV|AaXOynhvW zo85HD={im;EA;L!XyBI=r(sulecT93@_x z`nKZ=l}tt^_2068x`lU#!Dg_j%Dc@W1os2Ri3zm#mB(a()(EMbl;)BL?NI_I$lsfsM_Y5PU$>JDkXoZZLv3Mt8wDvX8J{jakBrxFISr)5WW&DI2vLn`s z&JogE1M*h~?|vavIuhHTg&xOwCFn!cp6zeimQaUHV|F?QXe z9B`w0J6dAF0ay0ZvlVnFpO%inn#igFsCZK_;dJlrvX#I$7F2CZ(C6mnle)w0eg`(@ zZwvj|8&n0#ly5f02M2Bq7oK9^{y$zaO%anW^WKY;um;mKQ4A&XJ9FKM7urjE=jbn6 z`6Aweoii>1Ib+%FA8{Et+?=j+c+Zh~ZI9|7l9K!`eRzHGPx-LGcC?u1XfC4HySr&WOuy_4$W7v;o!tu!Ab2m|58#rL&=RaDA^`Y+vaIs zgeuNW0gS}Qj*Dar%5DEHo%Ld#eVsUBq+cz*?~dXcicNf|NYtu>`b|^))|&)Jk*hC! zm;CeCmv26DrmOMZ8GRA>;rfrIH}}et$X1iGAj-?ZV zaHk#iOLkW=pLAaAC>PWMiJNTfBryB%w{HOK+U~XQAKPOW;4qHmWQ%QTU2@-00xu@o zvo^ZPzr4#=<|IsL$gX}-5pMCt5jh~joy*ppO1IPKc6+w5uOdd#E>@8x*wSEZ?uYh> z(_PO?@2}|gFbZBdeF_4-Zm2GK#b#R=SEP0XS{-P|e7v%-pIGDXCNz18^m?At)K&b) zn+w~ovRIBeSCqN!Shf!y*)r{4i1u;##$J%onu2`?X2MvZ4imUdi&4$9RL)!JaZP4T zQ~UFs$O46^TICV3%_zZW5@K;Rujgp%rKF)wXaKRMQjf2B*WU*@Eqiae(}IUcY~M)0 zSUBB5VqzezvziAjrSEcaQBzx*H(lr}t>4kNj|ANQ=>>qTFx!Z(UZLs(p!ec%Nq>g8 zFa~MBDxwK0LK=mN8WrR<3C^5CxYan5mGv)r%wjqi&^Hn}JhA?R$8(q$LiepfI^ztq zH;=?rX@cathdRZa#HQ?&ODFq@HTf;U#J*70E5OE8@ScD3usK*DMCJ~RL-}j)s4*?Z$ypfLz>heetp91nNToo4{X!|68o0em zg*$YfVP{GkRKZ)~QjdxGb`IF`?Yy1gV3(tF60#9gl)+B3Ez|LzWAJ{@K$E`?2p-$B ztkBk@gDQdDHK?!>;QWpJL_HjB#ptCc{1F%FTbp z-RKlU*Gl-QxfIlP zzHf{gt=7r24oPJK_0jc9l=Zt3;SDPbV;7;xy34h(U0eJHRZEEu#qzN7 zmg`MatqE<|57$jC&sg%ieTH|C;1y7o0D9kibXN3I9zD940SB;ygjw0j%(j87zymut zfKs82=8hwB;2IL4Z=T?ZtlIX8mFE{sN36cy?WD^0izUa14PSS93x>uqrad~R5+A_MHC1o;19VFN5 z(7C}c@lPGSV}hwKQ}uvDzM%WGY^6zRCWA5IX_*DMVTl)>Lz;fx-{7nkAsC0Qx>>4N zJoxUn5)l@t{`G_YJHQ1ou-S-T6junk6t8u0oFO*bxpXYsWphFH7f0ZpxvW@q9KVzs zBp^XG%V*{7@237u*-!&u<-er&-f-T~$cVjolb;+n!A$1Pdyo;*BzyEbl&CO;Q%u}( zVl7?*^rGhNH)fq^Z)sz`n%GaeB(Rf(Q^0oa?2K`a!bH0UvDzZoz6u%4?ZF}*U!!+6 zjmR))|DqgN8BA#NqH$}#9OEI6>=R%qSurk>+%!#>Y~4;=kua68G`n4~Qtb$*O}5*E z+B&O+g;HOT9QU|4RL#*eClTXucJ7gXJV8~H(O+24j%9q2BG$=O%@QZo2^*OEnN6nl z%P^WenzAeB)jih?wY+nkv6{cmm`YvAS*%7sX0E$XpkN=?_$jB+D!u=~Lra>WAC4Iv~w4TP~!T`Jibi-Bx z=^lsQAN6;qsqwlYnwnd#Vi?GSG;fR3r}rZsq!m8{$x*bSDh#OshikQ7tie)>SLhkV zG(4*SIcR3%{9&k5gT)sr*%V1kpaCm^5eDNh+bl79^xWmD_+8(lg>122hWz2M9~pk9=8-*qU@!M*M?5^0QDHXot38(rFiAaB+!gcZ15ATGg?Z$6icxkomAws^$mvgSB@pKG+QqBF{f9gA9^6VmvBl0T)pN#hw7xu0I?GgTr zsTlw7R+Brl3JsnX#@Q1TTD>1J!h_a%;XeuJI@zE*u!Y==#&Tt=Cc=eFPOr{l1iL?) zXHL0ay`)|26c<5=rfFZMM5(fP6mE61p<3xc4fbJ@7Yo8h9txxbpW*3d5L6#XzYW|A zj{Rqd6TL>zX#~hwU-x+2C;-rATDI_L(k}@Jw)Lk^5&Q^5Q%s-31r+3q%0@7;N<)H7 z!jnR+Md#qr;AOz_J*>tPJ^s+HIbu(z8$4tTLv7s^35ebyEe{~=+;eAm7S15uM|0ol zLVhz>$%}niz*k$wK0XWb2ZWcyBHU2*eJT&ylymOC^tHVxr=csJ`3fu)P^;+OZIyxG z_I+P?gZkygtLw`!nCbnDL^B`*;X{PPWmYQ5&FT)JuBWQVx z+42h95+TAcjGUfE*` zjDt6+!LSFh17B5703WJ*8x|BHxK!n52r0;uFCO47Ro91rWV1(ORtTwL#r~BhaSS7B zmO&=CqUZsBj07{=cn2-!7ISegk`!aE4Vrrg?U)`@T-6<#-@r@q7pe4kj8Ng#Fdx#T z_tcL-4}=}`QG4z(w*_QPt3+?r%fzBWGA`zWrj{}d7v)_@iiig}yMDe(N6X29181As zB{;iJTFdiGal!TretUpP*&L6|yy5mH-=?!pi#{^uqs(%flv{9+&M zSSY*v*bogy@BvCN;Hn?&8WrNg*#4$aj+592%I91%V})v5F^z_c zz_NPJ5xd%Q3QXuLd!ohEbP_Xk}YY+qznwNuwtQ%>>Xlb7-uL z+}@_Z$+@67`MC{JARlx|;~@2;^F_f z>XRWN7v8HZ-`;1+b60VLsoLQfbvex4n^&{LPEO4VW8~y*I%bk3;>%~ zAxbddK84#%hGWNGUqyf2n@^n`>W$-Gvv|-ZnYnK@SSwmQ4uP!i z&IWmngl~Upt-ja%9k>ZjgJIE;!}P4CDp;C{O-&pST(Sf3@rOeD7aoC|MNWT;O0|IF zkpTpF|MirDRhh*$?eqsA8pz^DOG(mDUW=w_c4-EgBng>x8NtnrC_>ifKS}zg1jUC0 z&^r5uGLlyOka1>t`l#VoDrcU+c9hS|*oKd2=bYA~(J05Mx+Fq>^F?l$#imt>-=E)Ah=vr$vL7r$s*JzQ`{dsdkPHN zycG)_CtMMHf6rGNCs-B=N_2COwjFD4>(WmtW56EmF^b~mR{Vbui=Fnx4zdqKY5D2m zQniHu+G;Sp6G%emoZ_A-J#;65e@JI#!qCKhR$KgMHNJG?HE6R@fRVh;bXW%0-$@TB zytG{X+bsVGZ03ighu^2r-kfBQ31H>$E0kJYgy53iJsxkMz}y@z&G_`=(`6yk812XJ z0gA*werNhDtmPrNV0bNzbH4<1cw-R$*!8M%kLh!@zSB>L0M-$jlp;Qif8t(FPHcha z!xy%pcsmJFgJn0*w|M)VP<2u+TwkUnJ6~VdjW6g)Ko;n;3D=&}2xvYpLw49#C7b#l z&@QB9Bi@K(L(<%H<7nx)qa^}qh=yrV(2d&*B!2xPW(pw)D|$;6CEeoBB4P0+b0FMx zTNF5oww}YXgf=l7+@oa1vJRt!dYQ1WOmpAcKNEfqSH8EFZv;$^2FVqw0E5J~4 zdz1Q)A4wukGMS$m04NO0teZ0Kpqe!t%sQY(?b>QNRe(+9h5;XPN`bLOF4$T8HeOFD zFpULY{o*Mx*o1c&)HK|hS#p4?oh6jb^kZV?mYto2}7zolG z3Qr|UxIzg=$oelo<}AB%;hg9_qU)AZB2|0+6K5P6dRs%~xr;VmWsrOKM~$gR_4Uqf z9q#$k@S75Xb)>Ed!|@MZN(j6@)a!MC;6~r+orqHW&Abw!A##0^R+ym`+I4qCg^B*U zjO7-@Torz!X|(rK-!BG@JP7~(>#!4~C?W&zGRL7whi8)n*%C1EC0(wco*>t28b%+K zAGy(Ce2mn<`;jEGNQ&vjsGum1^MiSLv4H?K_kK`9);nw{DwS}wml z9D?f#*aLpX(;$BJ2u;g=>+?yahQXEWOW8@Y(39JP*Qsx)DAr$F(bxni7|ZrUCkI4R zwaoFp5fLN=NEP!U@j7}3?xpT=lhhCblJAcx3C8GJ(m{9aDYxASIZBYnLP|CT<`hce zq^HWi{dlF46escZQbhD=Slg=`QVsx6J(NjfJyoZKU*wB%>JqM2e9`WVkh{g`x|z`@ zp#r0I)`P`tl(uCB9TCDMfkjNVXR@6y(U4)5Lh6;or#Um^Ow+jI>z*&&g;2ReVvr;G z`OAB2Pm(RaH~ZDXkI0TpC&qfzRbYKW?VrzHItC)CV;RDuQ(}puaaxd?=tmj7EHhqt z0-6I;B|PjLW{HF(#!|r?^7@JWwX^UWfptK2YO7WR%^p4%;?_^|{1Q_>?3!U{qDwG= z@uuEhgiEGiQ=CGu%R(^X3_~W#)U7(Lp>uLJB$L{&5gF?A$T}#Ov7~%8F7z17iU*}= zKma1nkL{#>J`fR^5Z_W;i$5s!JEKx-jG$zwD?DDV*LxT(mN^e zc}=Oq((K^u-e9*!*}G4E@*+M6joB2>>J%Mtu<>sHq%`;^mi*B0JW}@vz2rUQ?(WXw z+&-{K7dR!hI%NRks_KJ;2=~Wkc+}Mk<(lH5c+3}?m*U?d$?!bkOEjSj^1iqUpmUy$ z8QdD_y1|YX%d>~EtH~>oZ9aevMX1Y?c3fo&;tA0Tjzvftms9y+{Vdt6XLPwlF zJP$5038bfQ`*8Xq0q2PPwlIp^d^3ui8V#y{sv7w);c z+S&grZUIWBG#>lAuq^t#?4~V5)Ajg=(qz!nSGvm^)^r$`9iNs8eORCH38=H1f8^1h zBbnnh%(z!YR2sLw`hBxn_{?lr{3)yGv)}Z`z$)fX-loq<5rjfkft$XB06FM->i1;s_i>$-%F>iL2%VsYgwB3 zKPtbso$3KH4u25c!3(dpZ|Iuo?#j1`VXtG^GtKuJ-Ct|99!xdJs7~PA{GGOK&c9Ed zDon#t5CaFB96@=jb$c(6zWmWy*~GPfSkIV z(Z2AJ#)`73-P~KxhQ0>WQDxK!N(qO{7HZjjZW8^&R3AKo-X+-irk3lc(lV?PowNPG zkgB-?-B4YW!#Qg>+}K{V7jvA0jCqMenEe+1t(KU{x2VqSulYo1y)F0*Dvp&l;TT zg{gM*hYA{0_-Mie=y$PqwJg^IRq6DaK<-lkhwzqw6syC;qWuE2a~Ca`BmW`-5kua7ir@^MGPI4Ny?}b>dc^8(i$v~Z-YU~5 zI@>}LU%M*X5|(tJE7vB*Zs@M=(no;T_mgy>a039LB~8cPxi;KZmLcMXb{L`f`dwNo7je%IJe{p2_k4p@)! zHz#zm0oVQZ@>&vcl${bnpmzWAUX1_v!`hwJ+sERNGG(?xFLMj_H{i#Kq9E%Ei-ZA2 zNLCaW`U(dfjDI%LJ6*CT$8~;ILgAcBI7>p)e+bQA0zG0}hwLz_6?Tn$NXifL#SZc& z@eIzHON`O@VfIJHSDVZf)$1mRhpW6OmGh|siEdiFalh&g@Q5s3=zv?I+evW*X zjd#yFC7icSun9m}4&8Ft*=-@KGS6>Qz3asTmu)Oo7~Rwxsd0H z4DMXN<;@!0i?fSp=E~G1mj-72H`QdB&SXM*)_QHdb0uLWu@b>}cTXV=HaPbicVibE z*M%3hZqy&t-r}MhVaN|t@9GGMRYCUiZ09jFQ88i*`EwLsBG`ArX)j(KUxJoa=cFp0 z`=Eq!SXz=*%L9H>iBU^4VaLES-*$8^|-zU24nd%paoh`IU}O&fVbSYOoFdsK&*M5f<-EM=7G2w3O8qgo@`vvh5=e?^0`;Rf#&wL2rW3(Uhzz|I`L^8~;;`7F{E(om)wE!qprX5A!? zgafnYNwlYPD&XMV1o^LEkiXXTJ`AH6VA^D|BsQhqwJVX=`vb_uAS z8W!C;CD&M|(rSWGn zNax?+o!=+U9J!^GojTQ`winrQAUXB*^^K5d-z8l%$p&g~mNzfBnqx?Pivl<0e3vRL z{r1w3N7mV$WY*+DeLMxFJ7sRwDN5jHiid)1UTodHN|U$gR&RB>WE>7}ZHr83foAS@ z{OYWCqn%-LEc6urij@Y%bfM~1KQ+--nlt?`U{g&5Y)TP?HSjZR;RKkF>z5ionPeuC zGvD20JcApq3Z-J3DzCVnl6z+Up^)AjvPEU)HugEz)?uTE_;u1k&Hi*rSmJx19u^rNN`$lox_X~ zR8z(Z$Nz`4_l}A(-Li*)5>O4>4As9c2v3u5UueV1FmCsw<1|vuxj#!u)Mvw%!JyO-r@SEs2P`$zO zW8~shH2KmRQz)O{$rGo1ZYGvRERHukK23Zgfl?cL(90FCnBLInquSnZa?CPSitY}t z+9hI7HOc_hE)m#}QC^?e{mImv>YSX|bbK2@x-4PEniy{`jtX(~+FwgNbh@nbX=Pqu zCRQ=j`~kCPFPNA3jG#Glv|i=Y=2hS7otFiQnc7MI1*f&O|9z`@acr>6D*@acQE~d|(sqtj~k>ak9s+FomJ#q{o@=Wni%A%rWPk)SJ%z7W>A;q}S(i5G;U%KDb_tyLW5~vlFx_mCiiU@_pCx$0Mlfy93+t3Pz^FSp*AQ({FJJT$o) z$$q{eWHJKs94IwuW~vmadZI}un+IKTA5URJD8#NP7G-3v%zXMw@x~T3;~r*JO2@tE^j+zO zB44YicMC^`+wI915d0HS35%2vUoX`PlL2iVT^iUDYcQPk_H10Dnc>t~V-Nzp#lj@k zn-nb#!nIJzN%E)RzL^QfPZ)d^R8IKn+bwa3r{S&Y05_$*xMrlN=F0Q!q2MI;lO>o^ zzMU*%lF8H<>&k3jo>RE6k#v!h-_iVTl3Xo<&Fryp&yfQTQ!%a5$Qcy&E=i(+&=f>6j#HFoM60IFZ)ZE z>m7(m@un1Ce^YMFfxB(9apEmMJ5YPZR`&AG{zL*T~p@G^pG?a1ChB-`mF?XKw#YKxyt@q+V z_90MuGsPD?hBDzymKaQzP>}1%>5$7UT5%b|`c=7+ARFFmYgmDX?JwzsLz9KM)GK`JQ8%&8nNpG@{EF4UEoa>9SVzcJ zF(SJI`_PKL<8a_FT)w>k! z{pZG3KM9t_=80a74ufqk$ulk$!ic&AjbKO&G~ zVy;At>3fmAjl6j$uFX=5ypH7C?Vx>lH|6OG?%x@`19z@au+uqxsfyas#C+{i8U8kA z)$u`~!L=jrmwWwb_lPpg*RwrrRs zV7bSK&_E7;7PH#Gx6-n8%SS>EIXh`2KrGN?Z#HDjPKC{uvV2R1>&(y{w1#>vxdy4{ z{`3D;I{M2|#r!${G$4C5Gkd8TdXSr;e#n#DCTmLk+GY!H|9~U~kj)W7i zD$R$E^9J?LoXevIzOzA>7_lneB-PLR;Ima6=d8Yhg+$Q}2VDrZ-A8u;P}GymY(X4^ zP63%hJ)m9PXv)|A^_ezUDK!nQ3VCn;@aKpp7?`O=ehE4Xmz0!vh#ZqOAF+M9vpzdw zL9|JHIUg2TZ5MXSI<3V_A1Ec65#wW`mC`*;e=fpWgi2zO*X5)kXIHg$nkFAtXrz%~ zERza}Cx{+med*j6?Iihm@i24=DV#_5P68EgPYluVB;I0r-!k@iY#TWCO9erISl5VG z5LI8L9osGgpV%dYrOMvj^c-^-sFC*^5kv0yvH~C2cB}m}D`gka8>_cRn3-C=9T!(U ze#7o92?_EKj;f&`b`}z;jR_=uxOKehnseqJ>v(s%R(GI{~qjsisZwy>%7-c zg;z}_mbH;E{vN`(-|hYyZW42=F0wmuUG`4=MEUeht$PzrL?$oA;+za#PUy4#WEcd% zDiP%gl!{@l{>(L7pWWCJC9wu=(YcH6FTz%d2#BSuLe?9du(BVo(dnmdiOV@vTvrZ4 zt0x`jG>!6S3o`MA=6f@ciyMsXMd{E04^iq;>MHTYrV!pG{#F#yVV1e3E=?* z4gkIv$#8cQw2^0W(O6Yj2#Tq_e_u=~J}0EA%*xeo-;YvfkJYtg1%SMM`+GO!x;IVL zLZn$ba51|(HkPS~2L=d4r!BN4O$@&Zp6~!^FN@HY^96>htuvnTefX75`maMaGOiy4 zxn|z`odBfz2s2UC88P~qfLcXIg(Js^4qkI4*a1qKGO8;!QD~Oiv2#4f)}-unrchA!`h+Lm;Xzt2w^>0{Zm~OVd9yYqS5fkoC@c9KM>6-18dMb5Vm_Yu`=a} z+v8=%SID>r}({2YUDSQx2*Y;!AUJsqQY*7jG|Wz@_2GNY6Xc5$jz<9 ze8_BgANGQk+qBjPfsNk1-)s9alv7L%qZeWr*^^?d&emR~t z4O~SSu?2Y;mraF&)Y)(`OY$0YKnvz1TREWsc$ri05W94Ae``P>M|j-gcP^Mh-mX*V zSgpwyF{fWaU(2MziB*Wd{o^Ldo&9_HjPUUh0xg-X;sjHwui!SUmOo zjkCiIxyL)!o`sDxBxN5toJdNkuQ_+=hhA#-Eiv6T{-fO@O9Wox zTFno=-Y_Pex}#;@@{Re_!A^0CO&fLlJoEDbLI;|v?8D}wQn`ueO}}7u7*ekuTyH2h z66Iy)tlJmT9AAFaFss(<_Nu1Wo?*YB;A@p@uEx`V3c|>4Md!T+%c?Ir*U0f&N3gn5 zUg{Tfme``)vr{zTh%&7xQp|asV=HcK9Yl-DisP1jC}f`y6j!MWae>aWCWm30y^TH-6eiAr9*8Fj)**uE7&iT{9CPHfwC`3T-TlTdg z8Z(7KFIVro1{fbs9qyMHZ0xPsgu$xElY9pyw6yzgl;pA;yb`XZ#Ma7ret%OuQPgv; zP2d&_dRAUa{3}(3WJS#b;H`c?{yDTotj)RX`0mBdZx0CLY?AD&7nFK6DmgUWhFGuq z1?9dwppG87B0n2GJ-Swt;CgmYjH#`TPbqnu-|riT4Z}P8!$cM=Q_sufRoC462^;M9 z#Rr_Q50i%!?E4WMte+E_-p{!#*b<55_hxaA2@gqd77v;Ea!o2#2Mmgvufl9zeNgKe z&7zi8?dc3{C=g+JQ{9IBlyfVe7-d&GnNY+?uv-_VuMbF5%*>?kLz2=_6c|-8u_W=n z19+qxmJ^C}w4DeLDIyVCcHcH6Uci^yJ;yurKu zQoQAYz#>s2U0mn+^bM;4kz(RNf$4McY+b611N~u}PQPlme~{n~IZXCG%Mcz}TE!Gb zY=G$|;R(j1|M{SVS4u&Xvy#UZ_NZK&fXmE#_MVJ1HsaKYj?4;2em$ZbWq3v95X??m z`daQXhBuCL?AZm4eIow|=3K32)NeiV!3#!FGM`@s7!LWVnCPX<({YulRj4VG*JeJe zQRRqn&v=bnm%wp4RCSEEU##U>a=zgIY*s(e{(Uu-+QpjgPvXwMq<-cv$Ib7llI6s` zc5N5JS~>-vdN}r7gkF!fz%GD5E#PXt#~jpm4b{}j8tMCz|Lo&*N~J&zYFEM1E#TP( z->)-V^MZXK>A%=;woM{YJGq@Cvmrku@F-X*)7Be98hBJ6J56vn-bNr&Aj_T``XQ0{ z&ft0h-2vsA8s2P7Aj1}QqCvfuC*NU`^FuTRQ7=Mc?yn1|zjTYZ)0eMJ@^f^myc4SK zb>AbCBBKV%(w~h^xfgTqzHROgCT^oQY%THt`;FZUdyGfw%YAd{QWyFFq#G^0o2~Qv zs1ttz-%r08mo&!OX(|ms`0wP~0s9^ds%R3SFc7i;l##6bdoqYB?2r*n`ok}mQ25(| z?ncW)zw`YkKt+7@gFgSemTD`*HVXR6CtlN~23+1Vv@m4Ou(}&V(xH1r%`DDHSWCQ! zL}WMRv6-yI&*=8$_wpf~u%02UASlBIBN8@@%}7TPO()J+A?ng$W)j&T-f0F)I{Fs1 zLg9D=hGA9hWAPRkD>06MsYlFWd5;sd1JUi%`ZE_k_yt>UEe+2HZ+%i1bJ~2_dQI$D z;jJa84~I88+iRtMj;F1CZvh=Eig4#noEH#q509aJ?q zU*o^8kgqg z72MvQ5*hhQlyP+Ov#jgWrKb+D4E`05d}S%+>vzm%0nF*W1xhr0il(i#q|k@jXFd`o z#p-E96pwq4SawB!IX)Wkd%hu;Vi1uT-fuE?NZJO=d5Qh-H7K*73#5=-#s9uSqy27`2Gnq&h!h@~IjcrBL;^TN*h% zRO89-q|ewtG5C8ghAYNUqVNxo-u=p*l<7;LSkJP%i$WIIHO=t?$1{qvXBv1%yO9}7 zI`E5kB0;qu#6R3R0RChF1g&tlSuW|v7}3z2_atdD&q!Cy_%y>$c~PMo8tMb)9_8A5 z_TMD+q0`c%`i!%!Pb7^iDJu>;US=Ea5kzTh7a*mdZE z!l$;4PqkzZbI-n+A;!~Ap+}vkG`XFTe;?vyMUkRK^z+lfIM81t%pYN#*HSaAYPaJ= z9VO23bcNew+r_8u6l|v;q_jkGCTU>pmyN815rpZn_brKQ@yp@+3y2X4@NBXY8NCP- z->5sa=ei(y@;g&ub<=e7i>yNkrr%R`ho%tJB0uNPKX$cE3pG63cI@U%t|x1%UsN^C zCkuixfhUWHA7N5Cy^-ig_VGNX87-PWRGRjA_){`5`hsaPnviL43-0{3Vp>LcL5%Ig z&1=IwJw1||%+pK(RFBEDQSW`fi|jUYwGKV?lzP&YAu;Wl2-3?MpBnY_10aKmVyRK8 za_M94;3ZYvNi@yUs8)4F@QVa}6V)9Kk~q=c>b+HE+Z3zbR=*3)UDVV`d^z^a8A&ct;Gvc6^}2SQlL& z#y4rtB`euRt`6P@G>`jntnnz2X|ds^5}L9#7-zKBu&JfL{hA+{ z^~L@LCi%9i&ChyXtuv8Nf|ko$e|(5Fz9=%8p^|#Sa)5C(caOtW%1EBeFOT8ZE5qJj zF}Q~ID2uVGU+r02ZVaJw zCI=oLIioCWU-v=8)mGHYJ1aYgze<7B9}C6-dI)% zm3~~>*`4wr8e&b~_>RIW+uCW=Za;}O^X#^F-3|Z`G-+Y035vfl=A7beJHpH`R&05` zZBA^D{ZVSd>>b3v#MDJB+rV9U`D?**M<&m!U8!7M_ecg`Z>OoTwP(_x-k|cpW_Z!J zqID~c`9yijh`tgN+Q=`V=+3+h`#h#LZdxKYNO!S`_Jb4<;#}u0HWgVYXIij z?um>*-~hnL>^F?ec#N|Hd+S{bwdd%5=sZEemF&uFTLwOdly5A&0$JL4jPJR|?Xe@mdWFdavpE7p_d*tvg?o+7U9W6h`OMaBE zf~_2aSHdlUS2?o^TI* z*+pL-;GX<$=u%tym7XP)N$;KPxY+naK2ff&95vTSf5aKf&|l9_YS5=D9^Uh74E)CJ zblA?M5tpyzw|9auKr>iiq;~Z2%D0M{TY*mRWtmP1)bC%?nSFzw9}_W4P?jvEq;dRA z$%__HeV%EewDP;Mx<(0a}hU2KXS0Io8o`xe1&1|?0)BIGf6MD!% zfV60CP8t4&J`tv7F-mi7x)MRA+p@s5r0~2=VdKWd8wdM#e(J5sU z6tzaP%tf}U#9AK=d>02xc8}Vzl{6iF6?R_bMnRf7$3;=vKC%5Xw4*Y1Y!!TIYb2f# zme>uVX_c$;^`i1#vM#6MTh{NRF&>~u4Jpp6SQ1MVig)|w9AaPm@y$i5zF@v@Xbg); z-flJ#b7PCQWuQx~_(nmM+QZGivqPV~$>Xgyz((>&DPsuac??qrQZ6YKTvoi-+HA{0 zen*NMPfd-HeVLFa#~+A@c8FX|OS2EjNT+LkAn~-T)233FL~D}hhEIOTDbA$|9aCET zGt-{Ji>)8k51B1_wuk45{GYkT4mrnc6p6{2Yh$W@-> z6R|4&N2&d#%)e`uRl_Ym&{jalENqat_94|c8>?pC6!6tgrr3-^1L-;kO(sxQPWfD> z#B$JO4$sGnL;1vYlF!>YP5hTuCd?ty7z`$L5%yBv!kW)FR_HykJaA{@ z2>h6inEsM9nH)Nk+!a_oQ3hz-vj9~JhjZ6YFlBrS8hiR%rqLh}GPL8cPCpAMA zt|WN|EqyJsD{n1q9h}Lw+UjRRaZh+jRfs_ZWYhbVE(fZ5tU8R7R!A5T|A{1!c^%`s zS#upk0~&C(OH%~%e%3-rzJtbwUaQjhZtDs@y80lS_SWnWg$J)BYQ1*(4l!HBZB?~E zCz&-c$CIoj<42_sH==pzMM8$3;e$gOZ8{Poq~g2go0-)fFixFqfTg^`iPz#IXJzc! zN?-qa)M`)pp_OU8nNBSp9X2g%f9hE7OY7_U@tjVZ?;7n*L~*W#K^i=~iTflIon&^E z&`l~}FFUFChnfRX|RFq=2~yg5VeEkH&Bab25Ah zV}U)@H<9aegBp9N%cg!iHJ28D&yUavG|9v>yFG|Is{H*#5&OUVti}X2Mr$8yCZOK% zVfcU2E4Idsyl>@dwTKCivp%hNYCI=wgd+DhOMqHDgQ%KhGcjyz%q#6_Xe|h~QI9Wg zgPDbA&)1B1JN?FU4H=2$mbXGV?)lvI<=zJ9p49a2XW+e3)N`9bzX@B^$GJh?a{5zq zHwY8BwCS^iZo;+w zgnXYqEnD&sB)nBq!)-qctiJ!47SiqjhW|4V-a+o;#mm-q?1Ho*H|c;d@xexl5L|x2R5;D!$edK|hq)>lF5*eo=EkInL=O|b;b{nAAi>?r zi|De6i6>E_p?G069D|V(`OF>=;3-uuSTl)u?|u6} zCCm4z7ONrPBLIO=4DN%ULHTK3mdjD@6)k*5JL`QVX_&u7>;hW&HF^l|?(dA)NMND( zcfwp=)$ ztxJv5ko_C=N@2wE()l(a6ZXrV$V;?=G2r@Zu+%(Ms^6|S$w!S2r2{>~f41ZAN$$au zmtcH}JQjEq|1lxXZ2uRIVv*M{3TJ$pb)BrlaPQ~6g1lCzbkHwx=iRlT5JBNxXbs!{ zB*S(1W&e^45shb2z<*s^fC@Tx{=667&+xJnT7JXP_&?5zGq{t@=<8IUGXa{O1v8&_ zbIw!IYmQiTXl9Be_j`7T?$*|B6G`T~=uIMYH;MQxBVee*zQ|#Sn;MokJs%*LmmDJV zm3==aT^vA|E1J(UK-VB17g5L{hQuDZATuze?uggyV zk`)m0w3CbTc*ya5>@d^s-CQu3N8N=Jf!b&uu#ZZG4}g{A-WP#UVWs;NRD|CmkFA#d zF_FxzM^-{ZN(X}%};YlAJ+xUp2sWTbi zbU>`p;RO=n|GZO5G-zX8KZLXJPD*?;hs%V*{Lhm){eRzV5d00y+2+KO+~b6Ub^LBl zYx$peo{o6w{F`{*@(=fglM>euIl?(vQsENsl9S_8b~l>}n;zLABn$K+Y|$ZQg_DUJQLc7a1vbu z0-Mk%B~YeWw^bKO-GQ?&b*JqUv+h0kDnMDC)8$82?|v(W;HnaRE)i}Lo7uP$f@_hl^S@^S;JF*@=QwMfJC0ai zeaz~E-kS^6!urCYjmw|L09PilP(e9<{WJneOqF ze3^VN?pe=N^ zMR5gW_=Y3c!-po7#pxsZthMm}X07GurOvU1=e3oikpLc_dr zCloO}O4)7H%uMghlePg|a{XQz6YOq;4I-=s+H&p$N?!V}mOk@(j-z(=fK5E|gSmE7#xjB7txUt}({nN9*&}%hK zOU&u&1CD3e9=p$S?qx>c*BiJSm~s^B?(fNdsdfn`msM9-m_K^^?&#xHrCSb+6TBY;1`hLy0_T8*d`3vIm+HlMB=Ps4e`Ie+WdTR-ku?aj*QfP zImk^qz}0E^s&m4;!?ch1^fe|hW@*KhJ*IILD&69tc3|Y8c5ZQ8dN=*y;z+q`7Hd_b zuRYLmpP2I=yn|^-vej3YZ>LL7{>f_hrt_?qH27La?}z+V65TT1RjC8vMV(mvw^O!L zz0>2rSmkT7Ab%f2^*gvf|qw(a#>Y^w#=r|nviP?ytpx!<7hc@$`$yS3ZLi18ArX< zxY(>u`9m$Oje&Y9yxv9wbwt0y0+%105N_2<3b1yW8ff0@4HoS)?kmu!&DzQq33ZAhj@W72 zVL^dr9hxql)e*TGYlfNABR#4f>E0~lhF!BU{Nz{J#j-N$zyD~+hN}GfO2F$gCz8E0wom|ocm#MWiHhirD_|LUd zT=I_l5v=%!KX&u;dOTEy>Qv=!@%KD)xaG!87Na4<;DNBiHpbTqbk->1OHgwv#pp)Q z0DVavCJ-=>*b|K$1jomfFFOqTiK@RtFP{0>#e=TVke_eBqHQ4t(&j2y{RNe9AjH5w ztuw*LVLx$1(dXGi3$Iv9mvUARJxlC0!Q1-hU{pU!0Z66XlPxiF+~GVFgPc;FnO(2Z zZQ#Fm415Ng_~{Swa;N~%xUd3xYG2Yt@@~j>J}05m>D9Jc z_Ys{a%r14KKw#Umj5`!wQUUnv1r$K;-w{$3c!-#~_XQWurVNJ8zQ zjtP8U1N9=b$~6#=_1gy!hrK;f1P0d1X+P|bJk&F#RoZ_}cu$L0Fh_tS+G~G%qDq_} zO!gMfCb-fV3U*2IK1zuddiwbF)efIjpTG<0nmgaWXMFrBSbF284&%dPCczpGKlyrS zaUw|rCA9NH7elY>$99~swhzW}@9mhAT#+7B`y^!5P8}#zAJIMf5QEPj_$9%=OE=9w zbw$sp1MAEzQP>`A!!m~(lKP8^L5lPHq~q$ez*7B|#c*b_3L||wWq5X)Mj>|NY+#K_ z*}d_JJu3NcVZGPO+J&hc+0!Y5*yi#b@7v&)TdhTYj^)-^Cxl{+zL?*REWT{{%YoP1 zEG2-D3>~$oMsY6J9bb6zmrwlAVSa{Wt(~|j!%`-uw(_eviMdpG=?;v*m;Yk&>T?byqrel~wu5%}iTf7ID+PV+sl_m2PWnarA#qql&L5 zX|dOeug4;Q%vJk)YfIH(w}!lu6-pWXshW&4TSti*G1@FTa$;v#zVc9{&IN4v>xR%i zJ&npGy2~b|*%qHD}ND!mWGU(m-gs)h}chyUP*i? zllxQ8k4tQFCvu6BRgh);sMjgkLmEbbMk*FM3(PGK3%@}Zm(P8?!p@s}V#IhY&^uA` zdarjd;d}MCH}s^5tfm7sAvMrBmoJ)lAos3>?c?a$`eMVFe{QKPj24vxw7Q{&&JNGn z3xsACXH(;W2}+yA`F9B3bP%}lHM?LL*e14TjFa}0by=+ny2ae)4x zGqbjsrnpr1VBdD@Er#IXPvhbm%CMAXPKo=<_~fGNwXt_pv|rH`RHth&VbPaw-e|k@ zal&s>#P`ZQI_%@z89l%DUSV;oboh6 z@7=#$4)pRdQy1~CR!`ryNj-eE>#`Nf=jHsc`yBa{`hBWa;}qqtt+Z8PiXdlp+c(lG z!R}H&#As)A#4b`TSu#wy&@fNhB)9_L_98C(qFMj|QN{%|b9c@-d3N>d|z7(3l7B>fWFHjJUg3d3~gV_DA#LFAIt1PzYyc zv?aRS2)E^XdfJWsG{8tf(2W@U>9Tqe=%z^i&|m-QUxR!=SF4gDp&z#6@(9G>k&c4}c+E2wQffI0*BUCioJbe>t-EMnM>b zCnlv6m4lx1#>JLwmGgpP4cr51shVx_5R=D|lKMXbHYmWmdC<1iM{o@p?$K8~fP`J% zzyd(>>qebS=&mFG4jViJ*nm#CXITiM{Xyg}7CClF?Dr*j(LU5g0HS5dmIDX}9=U_% z@H&+z{GM=c5WZ1gZk*26Md3n0({VyD=cLw{1Cp(CZ{PPMc}iw(hJf9PJ_luk;LfoN z3y~!;m*yCGy~qC|rTOgJhna`PR~{9=r(&7yB#yleWfq@8T!L+3Z_&mZraNluB4jT> zMN4Eavn$wZh=0@)GYN*Fi#BDGXN);EP7bS%6Jr%!HrHZptL<$o39mfB!d=pCS^4qd z3a$~SjH?DIXYU}aNM{!E+F7s8;`$9!f$-HM+b0zO5g=0-rtIId3X=Pt?vCERW^~(L5fM!-ViSviWI<(p6Qda z+qBhD&Nc)6=MctL)R>&vCZNiXP=)nhRc%8xUWymvWqcMqY*$BR1AClo{(5n2SV4wi z15V@s%7qP=Nu!s$_@yi9C-VyRIgXgvqc-RMcyJ}Rzh+2ZG_MzFp8(H*dx@|>_!&kF zV;o9zw8rEs$yOtK=yrxHTQ_`;`*3O&=u039KMyViSW5St-e@)hdf6cPx)p#Q$wr4m z0BvlqLBmj)rk{LRz;&2k&lb6J1mDc3Rxe^A0e;Yn{D_z+Kn|-=%93*oiOIQelO_gI zeuE<+Sk^WaBM-)ZDEV=Uzec|ooYc0DJ&#QcqZCWPt3s{D)S#CGUDJN`9aq((mg=;= z2c*9%p|*-k^zizC4>n#crjT1*U6av`7QUlzd>Vl?@oO+N@2r zut*6Qi#U3oqNBb-_FSM>nCv}{XcA+G5fRqJxU9aSctKX^1MEf?#mjmez#($F{GOvl zEOXEYCKA#-qIcqVSio?m1Xjedina2|v zD|X{uIjmmW#uGKBbN!L$v=$E;K^jZe?nktx23c897aK)A-FIh)->VK?`bx}`;Dffb zy@r!PcjnGw)hmwR@>;c1?^mcDMIUwoBF=3viJUM53;&^!x#|orBln)qAnO2B^!SCiyytr#Q zPt9J3pZ(n7wDjR@XtbAU?%H*b2QX}W?>~dwJyE5oDiG>}y7mdEYwtGoa}k+(E+R9C&Dp;D#4Q8FsJ_;S|w-x z%_8?cOfYztQo zD{pm6QQ@AhW~M+xsXP{(`1~Itm2tnJm(F|?askB3N$mEJ2`Wu)be_R9s%C$4a9DC{ z2(K4z!-QhMk9B5ZwA%K@oKCFJ$CEg55;fWP6@4Bt_Sv88Oz%8jTmIff=FU48oj0cO zY9ws6uk{;du;-F?XVVimF-FN9{jYAuGf3tpS`JpdV?v1Tk|)UjG6VgkwQIeIbAd6F zatvJBVE4(VKgs|>4AFd;2i9j#Gqdyo0l52RV;-t5e`y$R{8QB9;jQk;dE(Z;*mncj zN)bl`i>v=XxX zi8K*iOHFzBD?)t;;p^|)?pF_8+~7Bf_nn6#WrM!$vAF7=f#<{^0O+}@Rh~%W7yVr_ zP~IIKH4OdI0n?MKi`>DQQe>i<2(w@rO7&{ri^PBqf)C1Rg{oiI{l z^)AuaP%HWn&w98r`(wH}x|6ET{=nI((&y;J4Mi1cv&{L7qn_#(M^<9O4Z-lA`XzO+ zfJN&zwI3ppLR?2#!g-7;`_i>6SR6ab)nV;fUYk#O&b5jf&XwnGjlxg+#)@r*=e|lY zpLR03BQ5VAu%zYN^5X#1n*Ex0%h-BB?!^7=a%D8e!1ZIN^Ra~>Qq?`%&F4e6>VNqg zKC9v1jx<|uXyR+&?$FWozpfJ`BtF4x@vgAj*~mNA6rd<_2k2N27GP8Wx2Mq>vrF?s zvE;=LDV_mA{|+r_@$T^h9j7yH5__tc2yBAnap%U!cCpye9oTv?Li25zeI9*+(PtHy zQm$*$ZCe<(j+EQpPk5~pZVXVKLH1(%<9D}~x1_jG#e60bQaq?i-!Zh*BwViHPxlIj zsDy{c2FDydl>=|u6YavT?D1gBDkZvvsP4z7%#0Csu&wKeTY9XhV;cEt>fG$SI12B@ zr9Hd_38ynh+8Vm0z5d33euW3i5tXl9g^DJ9i;#=<`;$8Fh^J);L5~)%3&Ng$LvNjV zx?GGL7>AO|WR)SLFvc)E#**ovVyXPcFsO~ZWyO;5d@PrQzD#;93d4stToG0XBefcJFgnMq?dfW)hU(LUCP;^%P|B3^nm7Zq^KAPI5US&M;bloua+EU~Yx+7lyrv1FoeZW4xDPwpSOW29 zpPW}>dK|2A_8@b|eXiuowY+Y19L=TGxBwfB;2WmVy9KGlE57)yIumOw@oXx^yw#I! zKXMu|?7m^C-A9l0c71C2ztQ?S?s$ui7XA8ABB{^mi+$xa$M^x^Q?;Mfp)c{VrPV`2Y&gAZ-le4RNzCzJ*P>T z(J4Kb(@)sTTp*Ue{=8bA^?RA4#f6unMkPLnTQ>S@N4MJO3e2h3?j-?z!eM>UgPElY zz?vK_QmB3MWIl20KwfzHc;6GeB*)d?{7Vi$g|&Ty^baUp!=tQ@-22Ti>XbI8#Yb~n zN5SZHrD0`l#aI|tWsdk9deMXtrdMvm7T1M+t)ybu7^C`<`btCF!_R_JJo7|FMQ>g) z|D?8Ss4?>*Dp8}ydDKem%3K%4d1tik{k%m}hPdY|IOE05PH*aNMQ`lJn8oc}U)_ha zzDhrWjtsG_^!)AMgJn{q`Cet3#a4J79_nGlRaK z4RXLD4Zm)`F!iVVwFW4hI61DtMOy0;c>tMZJvSd|E;82ZZEtEIAnR9$VVC8kLOrUC zkI75bsx`xH>LAU`WP4?Nu&8%lp(aqs5I3+F}Rk3H%7Vf8_T`9#WB zq#+O%JFoLeuYGZ^^Ss8j+DEL4ZEEm-7@R}scn_d5eP(uDx|aE}tBt+b-OZ(8kLZu| zsRadf@lwvs@nT*>EtO`^J(kM)myFw5U2gf)qqj&>4``6$;t72O9h9tDvqg&kcj`O@?ym{D5iSDg!YA1dkp$LMTSp_`qU}o8t*D$Phb@Ux zx6b`2Ua^$7l(~PPB>48I3mW?FLr)PT-Oa2sB|5EWmZ+U1gHNJJhbZ<(T z7XLEw;jP>6g3yO`z=67vlW*00jkvR!ocj8uh<;%255WqCVM*&VwT!GZwNxj_ z74ZcG7Dj&a0P^W8MPmgM?@TFo9!YN8aF+DEyQk}UL$ylnml!HPI?bvn{9;I>{f0ol z+P=YtSGQ}IiwBQrF|xG98g@KbaI$}ov;2mbdb4|?Bh{)FjQ>_geJg=rU>NI;G&S1- zQU6?!?~h)0JGK1u$q$qjQhKc2t1;PobUQUc5G{Ry7EgvNC|mucqg5-4!aF;r0fcq)eJS<$npb|9VLgf1Dz6 zw)T&eC!%c@r^B>R4EMjiJ}sMcxM=IMmb{-CK}$$w>o{I?%pB-_p`lW}pqfkJ4NMH% zY)+-#!+Nw1%wEfJZX5&k2-F+`4I;F7@~ctq zFXrKPJ);=w9ao>2<|}2a%QIUMi2mATv9LIrS@Vj`GktVEyp7J}a%s=9Q*MiRSZ2DEui>L_+d8%G*^7`3EaTTawqh7r<3uI{p(z5jtY0Rjdt&#Mb z8|xducC0>~wFE*C-5M`V3&J?AZei=k4tqS9raDvcbz) zM_vxN=ii6jTGNR>BaF!%^ctbGq-xg%F%14JG9mb!M1p(>j@oGmgA~+#fX}UT{ z*Ma%nOh2opFCt%K3!4!`S1s17)54RfEz&SrIpv!K9X87bl9#40XR1Ra^`O{qHQR`x`N}dl4N?Vp>ULkD#u*_}OP2MoTJc z-0-WkTYmu2);s!w^C1vgR?;DeA@9KbQ)ycPw#(!0sEKEJGaroUML=c*t%!RxQvAvK z0xs339xAuvpsLzRWbN6G>>^e_hO2#u;jg}Ukdvlrp=JX?&i=k?VbOEzxl6nBQrAo> zQ&Yd?fFHy#^vp|eN?o7?`oL<~O5_-CQJH6!VzXzuXU6soRwjc<{=8?(HaJ;VwW$gT zCUyK!{iqh{2|%Q9NBvsN*G2v6h~Gb?pSk7??u>ZYwDJ<_6hiH^{5NHP&byy~H{o5e z`YOfWvgpj%*V~y#{{yOljVOFXl(s zqQp%022?KUSH%R_*KVVMcA=G;vwHb1aS*JG5}-jJw)q_3jh>B@vR1V;ILftsaT`=br_mychcTY-Q8UFb;9f&V9dGwsX1q zi6>+n*gAfzboOmr%=b}@kX}W~fM5dXx^@o%Tk7UZzoR{YpNdAh}q3qM|C9QM)pI z$Lcz7u2Kq+o&HFo=8t1*7GQjKK`KKY@gJgr9?oUFZ#qqe2!Xy&@afQjGYLb(Jr>v$ zOa2paa8+*?0_b~*(_!Of)J;Y~(b2I0DC!Bq zwH|*Ln$6dR!P+l_DD7n^io)~Sf}sxN(iqMGLeU=AACkg<4Z^&h6N2AqX0`>K<~x9O z?0-iGQNV>QfacwKIbwp{RQ6?djT^YZFDp-YT>2_f&8p?Z_kod7+~pJNv-?*Z45v}) zMyj#7i5kN6y(_}^vWRhn*RL0Ph<_~@h`w}L@yiE2xjVS(J9|1VBzP2X)_H*x}j`U7X{)w51xVvGp>Q%=HUv5z#8^d}Re?=EaLy>+pm?$=<&M!@bUImL6nse+A*( z{R036<;Z3Ju?SI|psA-}8u#LWvx?>gz>2lavpr=l*M68rnnLgLD@kAY_~ZC8VtSjX zg%%Y%n=NXe?A^fL=-l?Bq~_5MD1O8%{2^Y&T-HBP!mYzKV7!JT-nS2I|0yRfm3y?t ziH->Ogz$AL@-PozRdIzr@N7-u?aN`Ta|59BUU6Au05ytf1JH_Sk&$JE;@jK z(g?`V2r7+(bfY3DDJ6o$NOvRMA~m2$H_{E#9RhE zEQ_b7@l}dPH-uS!T9lBO{CkmJ)LsPJaMlU+Fvw#}Z(|6>d#~|u==Y%I9bZ{96YiPS z70q)peY~OHP(J9L!8{4$Cf^S@`!arcj|0CBPdZ3&%}712R`xN}BVdO<%-Zjc41SDD z^jylQ>*jeU=w!9(05ePCGnVVi#(IyF7BG2!v|TlS*1viL2=<8}kr2KQts_;zF3t9z z^*Gzxu*=L*zuqV=?_U{6*&YbSg5W_PC?}hMjg?Y_-!*nLe>~Y(t%pIb&JH(Al>yL` z_tLyoctyEqWX_#`d|}Fs04c6f@N?JW;1$fEAbh)48#b>(vdct?@Jguu!KDe?xu#@? z9+wvu4_{5i`nh*w$Nt6xjfMOVCapPuQk*Xk*Z5GaCS6=100U^K;L7<8KXza~1}o@F zqJ`2P^JVd`y)in_C;jd{O;ogW5xK-ZAS4@|&q=h#2bIq`e23MOBW%9+<5>w=K;W=Y z(gftUh_m?O;Noju95_)WpP?wSbtK{bZH69agzwjgz}{MeGo&5!NXy9aJJWLdNm?4h z6PGFVeG9FJ`_HdA`E^&JZh!w=7QrR#*Bb{u5Fjrz!n?99GBNNyFo&Tp#cCb(8d(+E zxTxUu1iQ`OH^7$(xq-rI9^#9tV(0!~ZXU?axA^tPhZ_=jpK-o<*rf|v$NP}1P9?1# zX?9DJ#(W9m2!PRRzkM!am0rWQSeFu?U>TC=d-KB||LSrJ+w^e;=fDtj|6rBaAml*U z0^eN6og$SeNS-gQJY`UL+;O8aJ=;GJ%_5fIvi0I=2$~T#IPiUdZ*btA*4<@MY47%v zCgDwksiA>D-*EgKErBLx_U@6P zLJGhS-q5^sR^t>2s3P|1*9^Yma{#g&U>);oXR!QBsg1(;XQVIx%2`>vN2W)u1O0bM zwVQL?=)!}>l#Vt)TKa!kjuoXM*aq32pHfqU0jrb$<~Mh zCE`3m8qH3-27nyHj{0loH7oGrSoSS&li!}@&ggcyi45x^Eis08p3xqNhg$TkWm~K#{pT|G zNz4P?ia0vsn!~jvxqyU7Ahaa>yj>Ym?xkKBqn<`bC0vEsQzIAYO!2+Wp$cm(hUMdT zmsPExI?`xVT7aWnlwHQJHUMl*r`(x;PQ(e5PEmAS{b9`RSweu!w(3HsN5uv@miKW& zk2%dl`X~`@pK{iy!CCteP$AAI$Hpx{CqlZ}2ulGShz`MyYR!$PqM8cDPt z7APM`;7Xk=;_R3EUHgb-GC#ru-=Ja$LK_aHj6SzWdYSSc7&= zm5+0#2#z^DQ_^hW2GN=dM^1T~cPargh6=r}(^1dF&{TN`T*q-$iEum&3a$<8k-m|3 zdx7VaY{#ZC-fN;b5bZu_?ih%^wK9GmNYMPOaF?|#tZ7Wv+yMN#^oSQcBv5?l_0f#I zP`3yf^a?xsF5T^A0E$*m&_F{Z-^!+UdbyXaHYAH-Ewx+h$V-$rtvc1;o=SUeIylx0 zb_>NuJ}^)5_7||N*IeI&oiT}2HeUK?FWpn@bD7S)_HVenn|1~k>I?wL_>D&KzIS=9 zIcFxkIdqWAV4Y6bt{O@FV@_htn?gsqM@nl+4v%OxPfk-fU3|75ldD+=yG$?up4DuC znh)4*1oKPqa^@G)_q=yPJ zsfHgm=ptvL#=_8)@^n_T{kIIk({Ju1A!^Z6iMFss{-IOoRL*NAwh2ALL!9|unP-MZ zAeWkOS79@bVV*Ev1%{^28_RZ&aAy_eb5Xt(FTo#sX&GyT|d!kF0~ z3twWF#)LVL!QKNdd1xdXj`A2f*X$-{hrz#seP!146vC1cCqTReG#^Xw{ff4Ic)(a^ zuGx07sncldT}x5(#BQUwxaHR5=NdyiI=&huyaUIM++_(0);M*yD9sHdxHa>`--%3o zIGHK2g(=t3D(%}&6KEuFE(^OC4+})##>775inH@?Q~IP6W*VbI`DvJ?Snjj-Z|L%C zMUi=#+y)iA*Y(Bmw646r^E{W3U3O1txc{D$KD*@|4g-77eSGVW!bdad z0{-zL{ay2~W^P&MN!smV%~bbaNqqHjmnbbP4{0t=&v!Wx`Map_!SHz(gozx zgYLW`WYhVtS)3`1L?9>sjY)2^TDFJ$uNi46RbDhY_}e?_%~Fbyh<>okk*xied%iTb@KgLFI(&m899|*7i?TkDTw_Jh z377eE=?QNSg3z)%XwGK2;QyU>Tmc+Y{TL$QJIjo{`RE#Zes`7#P4+DdB#cYW`#1Xd+xI|2Eg8%<&(LukI%ap^ zgwV-1da%{2@yT=$haT(^aXM(2&>OsMlm>(ZF`$l?VtMW?)* z-Is?T`5<-$*zZY+k#~iv=|-?1R!_Vv=ytP>Q4Mr?OI?Da4zz3`u-wx;nX!n&32rjN zVVj3ZOpY>195gTO6#n>pQfKL;^U3j=m7m_Giu#^#6mxoiSw!*NEb7kR!pA66MO`gZ*ZexVSfbM2FMdLR=-bJ@Pd0zom= z^z-esyN6=%mtD|Bagf(veb}9#I4B@7&!2DmQ$s{zs;>Au$&)?B3u9oaRJmA2KLdMKaAx z;!S;6!txV)R|J?F3T;-0C=k zpIBNSU9m8wHSDdGHKsbAvLfBJ4sp1h1wn@Nb`C%9l~bd-sjkSMi*5V$v;U_?8;f8L zIvvZt-LjisN^G;24s12to~Lq?1Upx-lgo$vJwHzjzvUB@roqy^`Eg!=xW_K(fg|2c zt-cSvq6})53i#b=x9&N~{uLG(wvOVCK$Xs7{~Uk*Ak55BqMlXZXJ;((B-j*v@H!7JK|SoEuf?1N zQTf^KcvgNuO7kacmjG&hR-%auQ@Q?(RSLK)x~x{L6i`(y)gmw*k-+oDEOIQAw}#~q zXTgzZNYg>c?Og7G@Yyuj=GTUMq!`crHCm|MmBskKvqcG~kO;N=qG=|ju?RD&k&gs% z&5=74ZGPOEZv}$U)1RJ!T&_mcdICEgtPJuUO#7%i-F^}>f`8wL9ws#_KW@2MP$mU5 zfn~YLQI>RZ4@2jDcUzA8+paI|={(YS04~9{APOVVc5?QL(IE)Bh1JcMcw24m)Zg)S z4B_H?osORsm=;}RkNk6ZjWw?kQ6LwpuUFpI=@O)Kq$>^MUpjZ2Bc-i_0>+ZIicJ= zdw0GS#Q3!DhiKBd|8PcSF(3q}Wy@lsNXk8qA}Hw#3CPmvD9&*gduGyo?&mThl|PC7 zPVf0bj3_6A&3}TR%KLATfetF0n1D%_yLNyhoXJ5#)4VglF)HC09oP7DRe5`dKX2(= z5iR&x(|KR(MFTOk4FXj?dsOyu`Oz3mN#bIMJR}+p)cU*JdVS;}u3@qlA2>}rO~u01 zT9l_M`d%t0e~7_G`Ze0o`ZkkqX{(F9Y(L3q&3jT(&53A*%r3OF;uqy@_xs9P9mj1lW;p zaH95G!I;8S_%rEbERjVKALX-d6LI5Agpe2x%EX2>Fm21W$8^V8y$Cg?d?t^#5BZ&g z=qj8ATJbw?hGl*J9h*rhDAS7vjDZmObt9;3$}J0MsSrKcF6dl!e1xhE7K-&Zd2a5+ z#s&osygB1o6v@Bst_L2(c2PtrOkhLS+#YO#{Hg7K3(Za3{$MR;)iF`C40rKrIl)ku zHLVf#T4|EdUV-Y87{^&{W%#NGXeZm{JF@swz{(sXCaZ=oCjshlIi$A9=!Ao1XACjL zy9HYmc$qRrN_lLIYhuj%?8aW|O-JDj?DQ>YCLrLU&{61lEa&NN_Az<3g*l=V#Hw@< zfo!7mUOUb^ct8qu5H^u-4(C=|gpiwoXvyYEOeXIU55sF!Epw?Kl!(=AUEUe8no@9` zyT#sUcetAqP#v`dqQ4#d0DTO!(#D_Dv_P~(@Q-)=xTq;kDyx<*dLVn}2KTNsfrbM< zewI$v#4`jJl+?AXCM`W1Qh19d6Z;cG6BQW}*FwVE_1BcyJvuUZXG+^m5 zdOGDLa0%B4wHbm}!w$CAiJ)FT?QFsuqLr}O z(_N^u&rn`pVd-1{gcH{g{d3r@eYyp^jA0fp+}RoXLyS|}pcD>S`w`#?y&+Q#$w;A| z--Xzh{Y=gNA6fu-M38Nw2aC2rd_$*u7l=GzLjc7Mr(?b>+z3I^AXdylUAR zbiCJB=}kD)6r7{J+Aw3v_koN2XRl_XgsMiJu`a}k-b^Q9)iO`#+xO?G9KRloI-;A~ zJP)lvAC>k;_48gSFvY+RR8y=?*j0FMe7LPYSluDxd3IB_@_PB9-`7zQAXf6g^mT%< zWv}bn>Tj^X%iS?u*bB8N7f9|i@R3>-iiZyi5BfC=`5YP#>9yO=7@k$rQj5nbK+TPFfcLawsFsgL5_has>`QS z4VJ#neR`tOQBO*h?aGd^)oWm@80>%wA%0(?^%0cWa*R(ocSmh)=!rw!hmqKDT=_bd;Bqb#CNK@~_LgpAe6x$nL8wZ}&DX3(nx{`HTKGI1%* zePu>sV_qQE+rE1qfo&)=Yc`Y8rk^4k`RMCa^+DaR*y!mxr^(GJ1)YH9q!!#B#^j%3>>t^ znVIy%>EZqU-t2oa;&xJcOv$EiuRl*nf&~ggs z^;qXCvG9-8JA%cg->&M%b^v&GnDjScaiHyT+kPUNS{~=@&Q?PFe?V!;gxTkN1*>wu zo2}PMs#38a;Z0$CC|7Ctu@phsy1RM-Av8)&SQxM0W4ekYGubnZx;5wz5wkK5`D^M3(`I0w1nWFki(cDpazTfb%k?H+fxU6M zI%QmbtyBo}LM?-04(O0#EmU~pCNlp9wC! zuSKs4f0AdAU)uZi#emwAUE5q-w7$sdpAin>JZ8?iybR=DtW=_ ze^&Ay!9Po$nOa&b9AY;sgkQcuf`?=y*|2>;TKMSrVDskN-y(mkT_f{Wz9~D)Y!v}x zL>?8wNEKEh3ytm)K#un!b)99mNHNxUJSw(>`upMfH|OmMlfLB#(~3K}GJnaspM48K zj@VB`6<1Q2$1AtgTV{W-AL@rY)58O< zZx~`<>*Vv-#%`RrzcQ?~Nz!TcZt$^lOa62>T&7=hXk&}|aSBx9esnm47pS4P=urpM ziv}dW@~yNJsPaKRAF@T%uIK!}kjqon5M&RqAk$f3o~F86-)v zn>G~dce$XuN;ywjwP6OKo{lyt;MD4XX3w+c!>0jg6i6~fzUZzpe1WW!^Wmku+nKe@ z?g%5uUv${UOLqAD4#%uyD3jJd<+%-B6=bdR194U19e~|h)4CdoI5#~O!W+?(4RQXP zCzU`vP%1y?@5U^79`KCe``j%fzc7)W+0=ES6TWg0yhZ%))VI@6(|rdZO^6XY^#fJC z-}dJ^O;~zKXvPpopw<$>MkKV!v%l9MtMc0xn^DK)e!-`$JQxo^g7}E2L2DI9C4(V< z!H|z*G^sGYMH{EP*$;=lK&N8dmE)WwJ(^rH2ysy&vA>}aeD=qHPi0djvs4m}P93Te zL{>>+c5t6J4~@OZ333ZaSU=*M`|ARh#H;wRXv3~RS8zJianjK7GQwqh^Y3r)cHO~# z;JG(L0AxDmwg}ny*HLK4B|s8VR>TZYA>3GAVi|HlAIdblq;LXC1R{T#04aHEy)-!r z?roI`1XKu+Eosk6*Hkq83vBUzWsSb$lleg2tt=os)d~5<`y)w_x|f+|z4wK-ia=e8 zJ()iVy)^HB9AN!UP9zc_u!JfzL>&efsmM~dq0A&P(fE4nApnz zaBtunog%i)cZ36V7JR2FHOo##bkLf)`lm+G}1Ii1yap z^rpi_rCG-g(Gno{)p!nt2#^|^zl{qqneQC~d3l)BGoTeCY}DL%CS2uo=;(aq%~S-REb2)D1N&h*lw_8;`1#r=wh)W=ty*0QX5rGz`jZ&Ir+qp zS*%CI=caIvh)&j;_C4yO(Oh4kEfO=nK9xa*INsVXmY)m*enFhPV6hxD6S&{112c`e zfxZA8UGvAR;~dbBp-Y>kfsTd>d4=*F?zC?kZ5-*K*M0ACL5~-Bh)Rlb;^UjRP-dLK z7%B3f{mrPvh#fPsfn4Q#!7M>^i;+wAOO=$&CFuH0==~85va1fp8VO7600gTH zGC?UYj%wZhE}lXyy^w6_8#Cki_6lvjFWw|8rP3TY?mUPjr38q4-uRZ>Sbn^7gf_IL^)O zpUHjz{nb~mJL>J9%}nOQn_C#Aq(O(c&`c5RUNR;V)Xfb<7tqcv`G7;`k@?SvO^<+4 zv$)=kM$)2oh7!MP2rEkouG?y$Q z2Ec@*1iFi&_UexU*fCfo_HHW?OiX>mJp}xEKCu3%_B7-Al!*}e)Z~N0{KHH7HVig2 z`TETo68N}}=YReWFXj#C^=pX+eZWZcwkT%{viTwy{hQycAC@CIgpY_(-T;j|ffOOzO7cZsg-bxZ zVR&#R8e<>vKLHBc?FumcoT8W z0>1$Cw;m6t8api+sx?jUX=w_vlLWvieCAn}15Tk&eeVgN7$_!If>tGmX|MH{KyQ`OloAwRr(wFj8exBRA9uJLROZe;?{HVpAuuR_ zH^Xmfyvl@2JA>1J|VM|I|Sx2*vDtWIsR%6v!&Np_vbU<>a;3iw*O@bd*u{x zZ%akaL;N@GCl#-cv-4I`a{zqD1ezAbBf1TonFKC*z(ovF_U(cQbGjaH#RUS5CWkj@ zaeot3(h3ShYZm>f1F4;FqiI!KAlgPV+%N6uKfO1=3ciNVb~v=d{An|Zi1PLroc$hx zJuh;qK;kqRPZKkHi~tP=NG@M{otLERMyx215-7FaTkLpe2ZVUN>ad-rvBM1;oVrsb zy(Va*=b%5&F=T;#e+*`41}1e^(!HkD$1|k;<}bkZ*$MhFCK%j5@)6qz{V!##ObpRr z;%IkafuI@dd~E1q8IvX=TR(ZGHDaM;lPMgfJP#}guSIuy7`^%#t4j5**Bvp9JI8!Y zQg%#A0PdR<=Dx|-D9x}#!QQ>41rF##y-pWFc92#nVf_FhjoCm5Bw0gpxt}(x101RO zt^l`-6pT&)pwA<#h6h}|=a|NIY$av$%MSOB+qJ;_nzabZ?EzaWuBzq{E~-?W@&W)+ z9k=sJ!DZ034D|i#cgo0J!u|11df z{3_%S_|mB9Okjwzz_|Y_Rb;H8=PuM>S~KTlJLJz}iJllU+%B{(I$tU(oOA6Kpl?j_ z#9dI_OjaH(cg;Js33{J*S|;HNaBGcC^)>Z>#ceG>ZiRTf+cYqc_QeM1?tjz;RUo+c zYNuN9%df`A9~IjoF7&^-fMnJd4(8m|Cq)odxs_?5zcllCLA8my|B7s3vkAv1+Wh4))7Pd%f zA;TD2ROGVr>YyOlHTt!u+g#gib8sz%5yMbpbmlyi`Kt!(NUCVLGBOaYH`ZsS51eN~ z?6ny1{F7x>JmBi*y<}Vff$nLwo)7?@B6ROgY~Mthht?hMy-q1B6a|WEe(|A>Ak!cy zjVs13lF7$h3(c%%z(E5(lS6nU^nV(3UJ3YZ3OavvfxCG)RXGiO?W;%Ktw8MH`ZY@m zxM+w4I_swK@wS^#X?j8^eyCx+Lx-HAJ(zyU<7dki!QE53Cn%>W2#0=G77#>wQIo42 z9D;ypBTyUB2!OhZxhM-484sr-4a5Jht3S+$P^E@l!67i;99)F9`$s5# zI8G~Ym3G+`V7kwOHLr?1u6h+>f)H&=3Q0MwpYvXlvDxSeoy9A|DGp;ae3RhLVjcAUMy5$>92xVj^e^CoqG1p{pOk6(*!DROxV+ zB{H-J#3A)Z%0Oae^ya8(gs}b9Y>Ce0*Bpx(qdN~tZX%0Gd+DtSkWE`%)}2NEoN&x_ z9tzHc&t?E%{nC=A6eOCO#C`y6Iv-W zFqXcUA0X*`v&!m5z5ub6Im&qvW0Tf91NkHRU?jIG)N!N^XY)m!inuHo`Psr>8R?FB z0;n*RS(3X|f#`HMmBUb{m%!9XjFxMP{v}Ekz-tlxc0W<*sK+NC)dcBeBrKR4aB@RE zbueiRVRT^GX9!9W7h!LQg(3|GdyqR$?gH!ZtgYe0SjQ{yag{8-QXoQSv(|tl_kWjx zlLP)hg)E{62dN*ve|F~SyuYGsox?Pg2SH%gk1+Pk^hsLhYn!0jW{S) zqq&;oWPUScLHI=`NQeQ`t_+hRascDz5B_@&nm@jcxzh?A?wnc#hI#O22V#wJ@O$Do zPkF!<&#;@a$;G6kzYX(Qp?=R#!qhyg*nsZ)zkO|W0qBo!SdaiP3j+WDO00FUn?E?jUH_PpkVFHJ@vqmQ z_alK2pIGfU$hVavMCL)o{d8P54OB%xU;Qu7Vh^qvcou$~em>vE`$iXRkVTaf zP6vF$w4x=Nv5@YUj$^QoB0{M5LPD?oE zp+c}xpmyI))+j5T#{wqMhNDu+Xyl1|8}1#f)3ib{ny(tQ6Elg6XU1dc+ZHFg;ezC4%b4IC|s)7u(%GCO1`OrLrtRlcRl zvvDYg+X|%?^B=a&3J}ZN)@P{EEV|;2=w#|B7eBP+0ANlj2P+k%WP|T>WVhEze#DMbmFj1TB-F z!9}~wc52?*)g86Ydn+Eaj9VnreG~9p`7iQUbzTR;7hWrC^sx4h(keZz6%NYqhm;sh z>QM--lVMFB9_;EVA=Gcsir?#KYnE$-M-vP9S@xG$kAkKp_^Em_$epN*b|F{$ZW!!MpgjFguaX&K)=1XSS9}r!8*M&RN&Y{2qgP zDUVXGEZn$2aoouR_f{%t($V(;ZVwA(3N(3N<<|Ta$6z#W8%dL>S^`j^QUO`g8wlyU z@QW8f-{k4nm6(w$j;~x;L$cDA)}azhLL51j zaYIhf&<>`Yu>1=B2Y3)Wxz;v~8c419C z!>zrMS$4HT*O9Ulr}rCsu}8dAuentq*pZIjH`V`$&<#!|$?>%t7O1oISd|9y8YHT- zv?blPd}wF8RNHPz`10ApOI-P=vrb{1p7~K_jjlE-^-`I->?4 z7yukXRk@TO8v-2ddFwkmE~q_QNF6EW(odCvOTYbxeGL?qfq#C~^UeK~(N}9?0%?0j z6acRCzz)_JPg7Mz3iyRQE-L+C%t|xB1Y9*XuB?n*c3fVAW_Fx3u&{7hPC-)*(1}&qP`o=#}Al|~5VkcH1<+Evk5YDp&r`xtB zRkFVmkZEtRltpPEa0Q@^6a?i_0@wV%DgUp>KXdHC)~Bik|dO zI^BlvjC*0nm|cqr?Yr*_@eyx*+09cF`d4dmSE?Eb_*aG-=I9ZAZzoC&IGqvpwt&FQ z{gTtQ{p9$wf<7zq>E%T=CMjJj0P!>``Q&vLd^RLJObp{bAL+(mkix5Z5VP`u6ZF%4 zFnLdM7@l2N4Hz${ME9h27use@3_wDsZpeulHSxO#fnL7=g(=7d&tliqwKC!0EU3A^ zyn5MBP>qk0Yd_HlXg^_$CKdtVoHXGmkKk+4oxNCBwJ!InoE#Del0t30nu>%3SHdGY z&z%zg=WALf9mdr+)6MJ;MoDhu6dblG+}nz#%R{@Rase}3io1rc5@c!rp;qeJ9_s^9< z-tXLwyj?C44_wZyQe3br+3p3F0xDR{*{c(GbgyRi{9d<5s8g*@yL+}jI!}S$>=n5` zTJT5zj((0ho6&_DU^N-XaYQRzf;aeDYobpJON*Hfd~Ne=^_@-NPV}o>-u#0G-eRBu z40WCgk+1RK81!G9C%I>M);{2Egiq^(#P+Noyd!>Ggfu7I+h!w9lBcxnN)4P8Hd0YA zu8~c-hE|KY2>g};+Ow-D4NOjvuLu4h2r10yall16T7qNYHKjPjP_N4WCr>~f0E&RN zR|>!Q3WHy?zo;@R4MdNb%O(_oD(*QhH}u-?+B*LFe8J4hG%0DiZUj*f>OlY#ZSV5; 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�bBL>yw(ALNhMwEdm<~p3d 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

?5#(K?g6iD2o`iew%``i6I6g10An_=?+26w48oM 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


6rXWpUmB4Rw(jbT_s=Zx58j+*1Srd} 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%0D`hbfNbc%U0 zICh`gA1+=};+DebzlT3{Pk9ZO$&h${;@8ejI^)0}dQA!i3@Vd9j?b-EZgH_St~<`v zk7yB&jpOhZM{pkSU{dLyhbSkPP!!@427Ttp*wWgSz?m|9QzEOmXqsJO-K9jP##PqV z{*@|-b?AQQJc+<9!p9g|+N)0E zZHUG;sFxr%tQ6ob`LGuYnZ|IPe4)Zb)wjp>3bQU*{qw#*du)L*Hf7Y(9=^3N{Qv_DpKs z%Fq;Q>Ex2BY4;D-Ow8My8t)Xme!IoqUx+NEEs?3h=|*>5au-o~Mqjl@!upw4BkX17 zn{{wb2D3zY=4s<6dYF!d)~P%vc(V>>Vmi%%bk)zl>o`y4=+edIV8%+V&c8M zCYVkgVG;$RANuR}Kay#D@rVcuMY(-Dm1|8az!{#4C!yY{>ZcB&7TAlqOA!IHG^iO& ze3&!sIN1JrC^?t+qlTwVREb})(cng!g{|~#hzzglzwV0=O1hqErK3n$8v9iHl8a|d zQj^%s%8LUR@*DNqMsshSVe1o|&+3-Ke?Pd$5uFQBvT{i)l2*QsS^tkWYCIyl?f&%{~@TxX`9~&1kViEnq=>U z*sZ2}r)aU);{FWoWMWIbLM7LiY{xu51wI_>F3pb2>s#JOI|~LO1T+a-`O`TNFPk^5 zolYa=^pckxY&=vRHF;KJ;;Nl@xE>daet0f+{oe648eIGzk-lc^s|Z^63C z87TVhhE{H22da)KdCJZCQ{o}Ef3UpR={t(kCY-afX$#s0K?1@dWY7Eii-{9wQLF?A zupFyA1i!l{cj;gb?PH+Lm!+B5btWCr2Yl^6OdqV_eCIgR#~(81CFk!~RMHo+G<&c{ zT&g9!b=^how!6-$!poGWVxyO*nN=j#!sII)p6kV?C<=c=ue?C2$ck?1Y1Asp+ zY|L*heY@rR=Q9_hinAOstI^XO!Gr9kwVP9}3HbqE{NDk^nJ(^LZdC+vUrFFi^BR|9 z9|nOnSlYj5+JlVi$)wKxpA0UXz5s`fw|cs`SF1t*2OpSK<`-qkmEbqmv zyd?36wl#$CGM!GYsI#ov&2MaXGH5!zJwbO^48QHvJM9~)p0DjN0g;Nvbmy;if#2@M zSYTmX1R^Cs&S)TW_ncRHmCla0C4a(982ly}mmvAgvI{_EUUiARAezZJaKFj{L9wog zPx%uxv#TKfayM@RuD}AcZ!xvzcRI+{y9t8vsZt=@F!Oy$eQ9&de0KEuyxOtkW03PC z@I8_m$d!KMr56Kst3C@*9qfaEb^um72Z;FG>FyLM#g7VPJ+832>Ky0_*2$`}c{pJ*RpQVP8RQfk^mIk*O!nj zcn$KMOy)B?z1Dyq-~bXj2^hT%E4`X{AYlj2e;$mK_+F9%`DVw-y{oM5icAYQGI?oa zvJf)HBzkntjWX5lepG3UdU37B*EJBYWwcl&_4MPxcfUX4TQ>^^N{sq_=5(ahOmrH6 zXHxe(G9#-lBmMMjPzMBH=1m^vT;S7qVeeWccKDQ@0cgY zhdu2$sZ$~Y(iAuy*>J&?b?{nb!rO17^o7J~k_>;sb3*pry@`Cx2$IS8uwlRqpB$=4 z&iBr%zAR^T?^_XTNc2>HP-7V#&#BH(X!`tpNZa}GeeUG2Y(XhU<(ZKui!)BGU2B;=Hu zVYl%!I{O!wT9i~TZPI=~p_)s141?s&k1xBgR7fdbR*0mW^wCfjGD!~S_^`Xw?Rq&F zk3t|xAf_+^#0k%+;cef>hmw%?LsgTJ%qrW86#?-u$McxOnfa#?n=A z9M$)yUF9=0eSTdZy}8Mw{>PxshQdt;vS?ntXf?%>8t4(@Y3Mp*Y)u%l-;cA~G05n8Yn(T6 z_6v;PlH5{2FG*CWo-5T0>dtJ2Y}FMb5K?vTUAFd1RZRWmlq3Ct+IhS=WL??Yye_u^ z?hZ|7663D(WDesQ204Qw13!!3=Tabdr;kdMxk;QS!>>%J(0bnNxM*p}>mKuU z{-s5APV*=%wy+H+D_s?)C`2Fl6%vKc;`RWF^O}|W%*;k80;(pWoSX9)3fAHUpFrQP zKdF{0wV-v$6U<1gSJjIk(yBL>wH*Ts-&Z!(_|dn#Z$a(s_vA=^A<-|Pxb32Sq~{FR z#oc%4s+Hd`ir8?SnyIY1e%aA%U3aVAQSxx=5)OHT0OyirmCKFCBR4C3fPwD5(x-}$ z!!j79?0LgAqC20Dy~K{EUK^y&Ub2+g^w14X$q?vHO7Z(T2hNjZbT+NeZ)pTAJ<3Bh zFzP|DrUZri?}zKH9WrmqE&Ta%dcvA5+&lDWOYb9|Q~*@WH};QrXaeyx6@ zSEVYg4R??{{|~+Ts_Wq!qjPt@PcGv6Cu}V;dkHGnoQbZMHJ%xjZ4m|Q#H9bdqux_N z8+-ZuV(eKuVwl8vnVvkGQ{ze+CXGU(_Hq_}3C(Tsp1oE)g`2&te<-CZpMLcwi!fm# z7it;NfvmjsSp@as$)U6nE_KTQy2@U%&yI)Ac1hp49!9j5gw`3>o5>hSuj9j^K_OB_ zTMTlp+uRyGqz_NJV$xu!|tvF}m`!`n7L)?MA4Mb0mGhbgqkJAO+RpUuC zp$cP%P!y&>Jldz3`LXZ$Nh%0HeTQZ`=u{J_5!k;L96=UFG*2GY#4R@D6 zNdqe9F^o`R(n~G4`8@0P1C>FO50AtIB3|Zb1aBSF>|LYbg1D6_!DFY-Qy7y2Pe~QA zP;{&shk{QcFYD!1TwKZ`pw@H!j5#9O-i(R&d?5tH^<#q=ud5NwttMl;P0*796CLBZ zs#j0sO4cudRVB8N{_rR0H(jmpsr!f~#n!V{O65FPpCs`MW5XuB4S|%b91J3e_b+}x>?hzY zA+Q$7Sm~xDA_Cm>cxXqdyIRXje=C+ous4xUuM33M8>OuDS=AlnC?C5deoX8pUx|+(+30?Ry znOlmUa{}BrVez?( z_-QS3npNGWHZSZl$}aK4B}iCs{=#p6Y4$7>yHK&6ZK>A|@1|qp(#1Q!EPE#CY8wK7 zf#f+;KfvMct;`nhrYEa(s|?_ikyMku#Viq*{;PIsg%izH+a}WPQ|pX>zEu)Y-%1+y zVr>JJVjZ3I7UDT97dNa}@KvD*z~!-*d*1?VU4X;br#Fgp=Xs>kUThHT=`KQXeTOqG zVqze;bj^|RPAek;#wDhH^mE0zvmlG5DjrwG(9M22hk`Y?g6~QWCQPG5Ql#>v#)9u9 zDjsFD&8wQC&*@Nf$K9zEcT)oL4*QQa^iqM9n4QF%*P;gWCAVP3ulCoicQeUR4E# z>jB4REcP|aZA9BLT0(R*fd))j(tf_a#NkIpHQwX6UvN;%t)2(oj@pfj%wKWV(paZy z5J$0V;0?zwYsW~N%4>5HLvZ(+``6EJ-ngVu@q#Z2tFxTz_sLTMs0GLg`8 zS3HaTnntd>?f1?WD$K;4-P}LtcVY=e+@|SqDe_!#4d^(qT_P*imM&foyl+=i))aiV zQNxA&m=c*#pgv>#@_5JBWFXFSbhy6iZMRa%@wV$v&;5`*PLd(2t?i3*p=g)a$EB`q znMvKbJidBU_79m@1DU7#J{ecn1!(*H7(YHn{nBh{duN=nWTMmmKL?r;Cz?I;*>@MV zW3q);ED+I`cKEyI7HL?re`%1(n@e4q)=rsM0yqjJ7Z)&|XKL`}?2*I@u|3EO`~Ac7 z)$LppGe(s_RzeTHKpM=0Hfq-;pPQNt+3%&j=dV*d?9#zOc?zD1_4!_RF^qp=>`SV6 z5miRUCF?PXOsfNn%588vL_xrosFCOb74LH6j0_IO5|I%8o&x4064H)Bk!?Q3M!5urz;>B3ZDCZl2r{T zpF^S{%Y`Kf#d{~g+$twEU5bhuAm2zol5skOc`vkj%Mn5)-(evRKFp>FT{4ys%~-9% zYJo0g%HLkd-M@YfdIU>y-Ur&Lw?4^y8lJZxVHC2CE99*ONwOgJeUE}0!Rn|jJ=sh$ zH8JA@?2GYLJsnaPT^N4__F3@61}`Y#@!hQaXc3I*zk|&fm#PT0U>@Cysr{XQ%VQ?-oi}L+cJkrd>WwX#+mm_XHMJ|QC zX;#~}DD)MQ8zzTMHsA3o-Z%^5WvIM4kGqd)e?o`_hjb&tlT`v#?O?ssdd$}mi< zxS^fClOqyqobIEl@JSO8Jc~IIl_(536+D=0fat=Q`>A?FxNN~Py13+flbyb|daD_& zxy#PVWIC`6+rdzY> zwiod^#Wlm}jYP4F(@%nAzQZh40j7CG9=2+pf-<=TCWQ|cWE)Q6){FH1l`%GuQa^;ZOYIknmz696Bp`_NBAgA=ACB?d&taU zopq%^4YCL2JNt|1%$%6b#G{bYFp!9Sz>y_(-C~ejUEkdcwfrtb!Qag)Pa=Pu-<_0V zYsgscsN08bN}i4RVR@m)$hM4`dXMCQ5B)7OI!1$GmwVvgzkp%ljSA%*gR|7$G!uZYr4ab{seg3yCsE8kZ{%qt-sy=>NJ?LY68;|dOYDK0GIB6O7YlgiC>W!V- zQB}~&%%VSEa1;HsSwJ{I%2nW}r%B%b9sSc85@>N@?1x9F|2{#Id*w3WSg1#;P_nIR z4DrM%_Ye6Y7L6vT>p6-r5@%j$%z=dz%O>O-TU-iewQ?R#C%I6U@!2K5Vq(%;E(EgF z)9Qho4?PmQ7?1PQF-NX?JPUBs1=(L62bsg|cMfnSzXDQ6+cjN~LTdUluQ2kWZRPfZi2}#V z&lm&xSQH{e2Xi4EheVRNfW><8mR^L5wc}J9Q9esh*87)1a<7OkX>Zx=?%Z%{2l>7G znBEBHurQ8M{y_>JireVK}S`J>2O=v*KVIre8@C__;ob{fki-^O|k>~cPw zsm^|LRPm07jFL9Brb0vmiZfg z^;)S%g1}H#+2xv?br_nxwYR)errh;|nFhVti+2q!{#|+uvlB=Dl9w<@e}n&HVb@?F zvB`Y;AWOF6+IhEki0mCUbTx`S)MP8=mO&XUzQydS>mH858Qk>Gn zeJo8`Ce7X=N#QHEilKYFHIaH>bK_giN+)U3adAm;ceUjREfN7PPm@l;UQ1S7%Bn1L z1^CqPMm2XYKqw|$uf7)SWSrFqpw`5o(!`+F17I})n*5dx1l0A!L^{swV z-r!A!ff!P98((V9-%0+rl89AebtIogJSgB%)*x3(Zugud^5?p2SAdybkg|)ntjxq= zlipfV{JDTZ)n}-fMbmShqVDikuAg0}Jno=8?kV!GXCFn~Ff+wNv&?H4-<;^#%^dnx za)Qo+$LlGIO;`p$MFkdBYjST~2;n07`mFs`sEOvI7YgZ$#7B!98%qXbK6jXJ|J|Pm zMEIY|a1VONOj^ya>_rtyT{C!$azn;X{8F`M=yQ2FPA*Ug8)Gl=4`@RaW=O~_logY- z?gA5Xx5uZ?CH=v*t2PwzTxZ`4$T8$!Tawm2;a+au!g9LHy-Imi=`ryjwCJxr3f!0q zI>cw;jNw!k`X&d!kZsA?`%u?y z3yZOBa@#nhr`#+qfEeX~!?Z=>L|TASPbK4?WDBr@9+f zz5N`^7i?SmPw8G=Ng@<#v+27=-g6}fpfSTa3u=M_RM{A5^hdHqj^)@=+++cmV%-nl| zce=HAgKb<-BQxvAta?c zL^=edkrDwBL_)fxkx)VDP*57AK_vwV0YO?ok?`)v`OS#$d+(ojty!~X&02cSd18O} z{_IZ-gC;=CZ7y`(7p&xvFdXe-O@;xboHX1b5*#Ahs+dz1+4eSNYhWd!1Cn(yt^$z7 zjGV0w^0 z)Kgk6jv^sd;zK`8PDYUW;Xes1^7NvF$%4#@Rg*++ek=|Ll>27dZSw_>ltn@txtub1iqmp>eG z6wJVkk)Bw4LJp)0)whjAy?9LP_-IteXI(F^;ld5{E()dj`v&$h43?UR_`=v)dZu-9 z8x&I|b)VEhU4>QC**jx*c}UXslkA{YmAzIl8WwsD>G$<1E@CG>_~G)3Xmn^RAD*4R zW}&wNmR#{bBDiu*`Wiks-Aw_xJ2iF+$y9p`|C{04Y!;42|KbssF1r2YegOWV?V;i1 zdYFJxLB1C0s#D^>%b(wHwm^0h3;-`qkra-cF$)p_vkZ78&xnfJFO6kVv{_!>Ld1IZ zKd_uqaiAIo;Gs;O!*gwXa7NbePzLj&MwVnemjQ2M$@z)MvJ1F`688L-{ySf^j&L5X z%;HW5T+o=8=T;2UXS^yhLu8iPJPV@Ekxz)98U8u$!9!WXQ!O$dD>ZQ_@}&=F@8Yg! z&fwSJoS`1P(*>Sl8D%SHJ_y5CXGFd_?$eTlzrMOVS}>^hC4>@)e*rN)B!>Fp7cdyB z*&YncL+<+jI$3@!cFo>Yo%Wc&+*(jSiG$=Y?5QSFHliu`Wsx}goJ`Q(S2p}yd8SW0 zp;4vj8vege69_{)^?=gj>6q514=weF#Hk))O4+edJ_VZWk;*Px_1u6!u$NLAb zZXo~)rGWMQB-6)AB;(bNDTq;ucq#061fV=`J;TB102>fvn+rnr^pL4nJ~tLjyakWv zi1%3&$d^c9)9|LY`_JftJ`kSHC_2@VzwMyJ!qBjMz|sKmBwps1OBJ!|Ao#Qe2>7>Q zK<(CeGKvqn6Bzy`P;-Mq6wVE7-+;j%fci*7Oy&h4C?HsH+SAN zrF9o;LC2ry`&0_dM1Qr9E zrM1)XfZ{5F^?`LuD4ubS;R`dVMoCK7GNF9{j$=W_acz5P;G)M;|5&rVJaO zT?SlMI=?Rr!94Adm-iN4(S`Rq8cEQdz11_gwqTF}<}&~L?QTdbKy6XM`R_OT{jPUK zU`+=eIG3^@36`Ck4CYQCSuuYrC@IQB?*gniYcRjiH^aqb2nf|lHb*y(+9YVS>O7-Z z16Y-5k--bIskr_>kDDb@%P5|goXkC?m%%cx+yFr-8TXy`Pe?p2+_NM}-|a4VNuN~D z%nb>x?lphotNH|4hEFF$i3wx{#?G|SRjp0bI6spjel-ahTKV3_+C}O#zOY)H3a=D+ z4V(p|D*L$A=|&S++Wtjwcqnkz}d*sK*A6kdQW!lk?5V{0OyM& z{l>gN%g@fZ`riqrM2;NDSCV&6R?)UhG%OD!n8I)apmF4lM1L4A9<&Yc&4{YqEH!fm z+S*GPlzYI_psI|-hTbPM^9?|funPK^m$DG1bN<_ZZ+|R`6?q8A`biIzPCSV@cujap zP-7Qh@lVTG){Yb1jzrgaaO+|S(PsOtaIA^_fZeUW_xn?Xt?Pj@Mzf>2 z_R^C-pP?3N#?To#Bz<|IFNrJVc|PJe09rDW#XYy_-7O6Axi1Ypol0_zNgf=iT7 z)oA}@#0Bdjot@t2ao!WJ7V0V%r`}WMGGF^J5OU&Cdxnw~YGm^wg64C3Y5N19EQZt{ zjmLm-+G@*B!Ya`eq5;0JW4>sIk;daO{|Q&KH1!~i_N$lgu3rB00iRJAWxlxRIh2g$ z*|@p0K1@qCZ5PaT2+T1>II6Qa}6DJs;{>&%($fLle&V+L! zuuCkm32yWPAO>aFG|!LPz?gTX;4RQOYPw~a+<=Ur4F#EtXCm`|uYKtpEH~m*dF$IA zc>H^k!>z3dhErzT^_usfoBM>7*s>B_Sx-r>RkA-sxS$@~4wFEVhlP+L0n2Bcu7b^uKY==I8!tJWx&KDBLB<>UGp$>}hw^d27tP=nE z+deyCAa;NOFvC~4FQ|++_*Q;~I4pmcn^54$*)>s&)!DcjgXgb##!-v8w;>UKX2HT1 z0PK`j`K%&~p^El8B##e{9Lc9PG$)>KeR#gFK6}^Pf8qu2XW4`j{TA2}h#ly}Ud|)% zGGeiYw7G3yA($-jKV+INP(3fWGVTpv1MKYy75%_&$OdYCOqGv3Us&YD*M{bg;I*32 zh{DkukpZK9NH~Z^?1*y8qaL0?PE065zJl1w!FJxMGQ~PYG05>ONGH((3jo$6$q2#tK&%+6@XOT?mH3)ductXF9RTMAi;fD3&w%u&2Td6$=2_$<1t{SfBsKEZ0(ym$&EO2{!m`nJ(ofB@F!UgYA73D=VX%?k3vfi@hR_g z=fv`HF#}h=jo~ZytYG7$pW|g8W^%Ls3J|!g6{IG?ra^tus)`PA%BzZP&g!^Ls>F(J zPbWKbmec&T+>gwJ5DSWJz!vSxIjDt-W`hSVhiNY2yTG~<%`;;rG!xvb74u+bhyg7l>9g+ z9~%r@6yN=VZZJ1`Ukw>ACWx0_ikY87(+GqoVK+(+^%8dnGKS*1u38ce`n?3!)S5EK zRHwUvy|%0)XFS?L6q3tw?kTEC8|WNL4+LEv;DMo=>VTRl4pja)=md;5-Cc+f%4gIC zk^Zd24+n;U1Ufh{JZ(mO16$t|@Nrjrq=37)ic2tCxBU8Gy;53wM!dQ4l!-kLLetVe z`tc=b;XT1PVnX`;aNF;jfQ3pZ)_cN~e+f3 zt-V{Z*wHi18YaRhy3r#4ey$6}VWdx~pjyY+({t&U@By78;Fkn}KB8^ME%LF<;;y$WNimzN zAvzA1Ew0UFl3f{(0b1hg;JzTmv2dWW$pMCR?%a8Yfjk8v|hzsxfLSI>=A51ts zurJ{nRF7gcf8q!D zEd~Iu-VL9bj$uRPyFE^3jzd^8@cTJQ_Nb@fF}-uN8^iL>_S)8%rNasmc@D-O9`8TM z1R*N!Ysb*YCqkVIFTq!rx*CpYyO=8%-VK&uHx%mCJTP3vb>S2Y7ug1bq|r*#%YES_ zZr{x}HW9^IGV7zWsLdBuf(4+Uw$*-bGNu>maO$1da>3g=#nT5vFIjvE|-lSNLsetkEKXd6Y6xoVsn|_p3)|zKkmAe z(uH3?N8f}bN9^U0nyu87NeU+Cn5~vb--4LO)u)Lx()p-h4E=Ff?S)=Trqt0ujeoD~ zr&;7UPF(u-{uXzzlxBK(*l^x%)CJXqaxC$z-!U~+m>mePY$sU4CHMO)J|Qu4p%id< zSM`YRu;=76g2GT{%1PewGh2tY@j}Jz>YY(7-qa&`-96~UDBy&2OVpw*kgFk63j?me ziZ`VGQL4FJ>)=7tGqY!U3VvGs6&+8jCK)+&*Xyk zq@F1{Zk4U+t6J!&$=<*>>!?_ac56-B3nzcfIrQp8gBhnb0^%ka;`7`{3gB>|+6Au2 z$-p^~hg73B@*;UFw#^lNW0K+gm{!p-R>4@}g?_bq*m5fP9nM%tq@E#V^pX6|ZMb0= z7ZPF2t)xA@Dk`fvXG`)`)JU2i3vN{58rxHwulkt8S?~!P`jRHz(f}x~1+_a5Y{J<# z@=e4P-A_pJ-|<{e=+5zBp5*JuVauA~x`ZRJ2aW)j-(%mbDw4uir&$gZPYfBc*RVeJ z(7f{cdd@uvJV#zw)#ptG&OsTB2IjopB(bMKXu*4x$pI~WF)25@NbzIfuPnbzO1WVL zu0>j-cS8V$Ad=if8G3OL)nhxEKS{wk3bz?hDWlLgvQ;6%=qN#EFAehBW9eO`o>iXaPCXaxA{O>+M+CtSnBie81Vs_}ez^-T^Lds@6>pTy!%R=pegvHM;pY zG_Rhv6+STrF`JVH9aX^}?EzM<=P=avHIm{~UyEPF-o$I@EzQUZf{jMh6@@lQLh2b#Ln+I9#cB>VOV^;0QEg*aG7qXaQP(NoXoKF&)K`wyT=?YRC$(r z-2cyf1D^2AIYK|Fdx!v*U8V^08y7)x9WxchwE4>(8%WW9y}Y2=-VLnj>zRe2@%qL& znEY2bldBu&AI zo+~89!>eLw*_(a&a!EyRA}tmYUHbJf?Hl5ZuJ*9M{mJ<{;lA?mS~$O>bns{O@+IGz z9BHOU%$~J;UV=2nzD3ZoZ5tLR}Z>Vm0#xHbU?v%|P&BP{!!7wTI;HY}S${9!cQZ^QKSC=nAf#J`J@@TC+W zX73>@IOkLt8mpnKF@zK4EcozA;Sx}bY9)>RQ47e3jw_Os?-g)6`who_HWH?j145~{5Z%-^CROsy3ov|-cIX|eg-bmnXh zb39rnD{Fv45C4Gl*>zWR%hwpDFdlY%zF)p-i z$}mtpb(`kU{CNv{oWo^ub(5ThNHNSJUhn8vAX<2P?beiy{-gLc7$ggzxqOXSADT%B zY)f^?lPaM>9kC)qCFw1C0%owdL)o%D6(5F~NQzQJ)f5 zY0a#v6!W7p7lvWsCy;q4W+!HxX z@U9z@Y?q(2K5=`+=6S0X5a~F`M6OGqWD5@cr(jzoaN!tS4YoWZy-)quOnd{c4$jwj#H~oYGO|=&7Vo%u{ywdm9VF+mj z3@7mv69$EE18_p)Ft~G{7dKTKSFh+DAL#&jdcSIi5?7vVO-)J=Q` zh@952G zQEr~L32?i^LZ>>Q+*iaL+#Y8_o{|7GK0$l;Pr9yaXZaE|GA7? zx0az?Q09Dx zD|yf{j*&!os$}Emyn~`PN`%3>*Inv_lOj$d*i6gNO6?qv(5RwXiyYxEwkTXvwZ?6<$FQNZeq4 zi4&*CZ_KPs77arVcWV`0R)*X67?FzsnP;TMikm1#&tz~q?DqBX*+lR`FrFDh5XuVb z14QVA5#42oVK$O9h0-_+i>1Mdr&*XriN7&XLXnR%6k5lJ`=^XMh(q|8vr(KcYQb{X z48MFP^)$NiOFlKXADb4XPu2^y^5pT5IB8EXQMk=+E@|>Y#GY%;@8v?W3AGiSxMwl6 z8cZ{tRj~eh3%9Up^IV8&YGheBlrKjera^P-`_X{ply6y`GMYD;#YK{fJ$q>AEnRU;)#fjtq6>Jf=mtA?`rJbj6mr&ZEf!z_ zQiYvsqI250ol{fNbq0lcIPOy)eonszJBnuFez$Lo-TlGitBI~-?@~1|u5zQgFrr61 zafvHr-@ODu5#E~m)8`@tr$_lhEy_;acdQ48@Sk^OU@5zs3W$KgKKS%KhE-C?H!Gqa zWoR;QvhfJ~Upu{-{+PAej7evvF0m#g5f*gt!xiJ801_oUKO-CqYh;=eQplqxzH|9v z$=?g(2CPIhmYgMnU5XwnWy>5V6<>`rI`Il92O8|muPSBsGsY3OG;4&V);M!xazjmH zJXtiOo-9L?Bf3&~eNrWdQfF$2T%S&Wz^0ZzJ&z;>(Yg)aSY1S$v7uU;2;?2MxGvVu z2Pxh5-hvF1y4PGNQhvJTq;UyigjIx7xdw=O_gxzG>3>Ly!sfQ3?b`dQHh$O3Rm+y9 zH#fXdPxcDt!bcA?y#3bacPWqXH08AVIR9$o+Q@>W9xFM^vAi}AdvS{5334L#U+_tf zqL>f>i)r1a>842$Nh>*3l77oFECE_{b?uy7Tz_XncZ7k_1Cb4RH09>X#gvvGG(*?q z$2`B`mk}93OZP?&Pu1~bT{*2t$C(%75vK=*90zlPM-*N`i?pll?r$>K{~9XIs}4H>4GWRrHg zeMX18ggS2jY!|Kttc`|Yg=_!pwDFmqQHG3rw{D7BZ%JG?T>lA!G%XK)&Xz`V_HKE7@KwgB*tVC~i@frmwbJqeWJ>TLXBq)p&S$a+k*ETeZM=;%0oytwZvUm@6-BnXUKBCn)xYVCO*%vzYI;s{$a&+c1 zGluX5HJQlU0TC03vG*z3K!%2pShiS$F~i9_fERy;-OGqot8d%BJHDPtm3^-BO8dmQ zo^TC%z0nJyW}qd_&(BBvJCW>D5Vp$ma3;aNR4k*qX*$oCbgh5ozTMZ|MZ3!+!sZq@ zqQhv-+)hCMscKR7rx6SMbcaRgFHHNNGh$8|e}|b0sk(wi%_t zaY1p~M?SI0PEn$KLQ_8bslki&Vx30MuRj2>o*!B$ThUDS2_x|R&j3X{FQF3ztOV@CNv&~G2*-4>3&kmrgB;%1Z%Xr zvRR2XGlphMd%`~fM~ZY`oLxR@iy$lbEo+q@dvi53%o1$mZ&z<6ZfcidU7|#%t$gLV zocG8WX3xFX)EnNjGJ6$0ya^!2U12FjOu|3NH~Q^bL&?{t7T!5SE6J|H=TzXvR*t3Z z)hP3*4x*Z3n(w`8*!y+7@vEw^qLEK18O}gwv1T3D(##F1q0jN*_mYS8u zT%Zt^Y!7uB1Zn)kZTR=!c}%YF)I`LD)k$EF#rDg7RQ+${bD2g;WaA&fwfYVOkUXHD9W&854V-+v7O%y zb8Nr;LRU5MuKT7$F?F5KidKy-(`${NANquL@5K`TAK(5E&*@{`kmj^WG{}Rgx>cX$ zR`n1p*bHl?6Ea-iK6LE$eF9L)X2W3pb#fh=i$Fnd(TEyW7fd9)jDv4;v9F1A2{j9w z*IS=)*a{rxAJMdkQ*V5Dma?&hiijX)m#xE=xqr~WsrD2#b>HzPvYMad)yxUXrLJtN zzF8^o-ULW-Q~fZD^jbSU6B?GBH^?e1o&TuJjdUC!mlQ(sR;?iT#_z4OXnuPqnumJY zXpB22d4;{OnF}ACxj@so?_>ddcp~ht5;_oPb9OfC7{A2D;@qmm*3z(jS>l;^yGs(3 zfBo(zH8j=7WxAZ2hJfZT$OvoF7DfgD2RtwI8iv)+Wr`UuiXMWEhHw3$-Wvd;#KVS2 zbOI_JSUE`yuafwL-(D^6TveVBi~Q8ku$b&*pQ&Gbh|V2~h{EB-sG zA8TO}0jt6PdI31#u>@6Zb3|*ciN4*KcCeXK{|p|_)V`^IJ;LtHNc@Li)s^?}YDT=9 zNNdc~ZNx|^aA*=y(D~D(3#6(BraSMcAVJX_-(V=8sQhvSOc}!%8Swg-8`v_(ix||D z_YXY7&$kGOqm=3&celWed`h0lzK9Nmfns;)O$5`Z_wdReRF)bD`v6*M=6J#LkuO1 zL3p(k-)tz+7aq5Mt9X|20gGk)!63>Hm1;^!o`ez0cuzTDr}s4ka6dD|F!0C zY2eMuJo(e)(9}6+cuzQt9Ex*S^me_e{DEK6&M^VI#|o=!?yp~_kpt&n%=tv~EZ;RD zb9re%3iXXv*=|r4HeOpVI*;(TgTSjkugiT!_@y2sAD2Oi}O@^D=xIWXoeLjm!`LesL z*=cf*7Nf<#QaJNi(VW?Ia_%~Vgbm>zD~^LHg~Alkxn5I@?(zp#wC1$Y)M5gkgFa@3 zHUM9Ib{hsMK-ORPn7Oz)|bsA#uCM2BPE zN$4yE9;+5!`#=?}_-VIC)+0tqs*2o|gRU*NC&&pY79w_cpGLK7=WCzynCMW0su zb$mqQs~U z1l=V*kZh7pW(zw_EZxk1*XW!&&T5ZS5{}hZ!^i!Gy)_98n(`)agDSLu^z`h_VroCI zqIgURJD)Rd=g@LH3int&lbjW=OSOtz+C#R+6QN;@$?oAlE58qY>>O4LYa>-!`r1b= z78=owUw5g9?{b-`h#7;NTh`gBP!A3ERs}(SdIW{ z5`Z`ul~&B~!Dhk%TK=Tu`|C(#2itQ?CkKXIu1*nI#Yj8N6$)TGmidv|oBUz~P}q zx|t#^%86%Z1vLyd7h6W(H`HcAt*PD6e}D= zKfXcbS6&4O=HBhC=zh2F*Xq8AATa3%t`ry&#eg7qH`cGQjldDdDs)q#st()jnm}7P z+9mQ2QI;p+S99_{D{ayagk{F2N z1^X-8ywy|PFzeGp2w?<8NCi>xCDPojmv#fRT0MbR#;+#;u~ptQr4c1?iF|GC>8KHs zw19;A=7rupL#XaAy(*`QH?O;hy^~#0zStjM<^#-W>&=;F_YosI2er4bER|ZVNu0Xk zwuBUTh<)ThbTM&cN~2%Z@W^&ANxfY-q9JK}oyX$WWrXO6kJ^_kaO@bknUKoX<|q;p+@yElXo>%?zzpXANPp28{% z(Dudp5Q1UzN;&bo{u99E*-Npt_NJ{5e{A_Vq>Jg|3jHRtl=5`1!}9Kdm0jC?l9+ZBCP@ncKn_S6Ta^1FIoz3A>^EH9BTs1glO;h2 zuQ1?o8O!k8HR$Xb+vrpSBgr}DQg~LXJR7DH`wR!Pty8`0fXSUtRaBwF1xKiN;=Q0d zFceN?gv_HcX(7Ly(~0{eXF2vQ7OcoGQi{6focjTtd5<9Z!lCTswUEX(O^^?#-_Pm} zVV)___I*Yee>Yr-z8sP0q>gDRn7uOQL;)gcSI58MYoK>AivDBGZS8eQTKwR*sv}iL=1MsC`gcD7~wKn z)Q-_6zD0Mw^&zX{=nKMNmy$?6sVoAStE)A7Cox}djAnz6n=jy^1+cF|cH~_!iE|)< z=G-pRuxGOhg}LS(Rn(q5EvX8KQcl^wZ$@DzpEGG>?o?QxJwi~b-&AA3?A{F@fQ-nmj63HR*Q zd2x2>4BlgW^xDk}{ikQD=!>uyc8zboTxKn`AnDT2t1ZrH z#ms}pzA@uR+n37ERCwG7Y$JgxJuU=`%`UXw&*mt~vmmFuI*nnA1KCPW+h38eZUV z9ECkK@>Eat+(k2|g^?tqWetEn}whp2$J}ychDI;5D4Jqv~3)RW72* zn@JB&xbH;Zij9uL?b?FHWBV;1vxDHmQ|RX#g@cLL$1DV0Kvywg!n3*{qvSvYJnPXO z{#;7JJ`Qo)ig#}hZ{w1=G;Aq2i!+T3q3%*pt&g8`r3!;Pka zY4+|`N~+%285bZoVCe6mRWbsxc%%@&4DK1yr$k-W897POjTuWyDJtKP1} zUN#Q8gU+=)nxiwC`JiM9K4Y?6XV;Wa>3d{?gRzP_An~3LbbB_c|{#LxoyYE4RAz`SezEF^Qr31!7$rz%lH(#NB-+GrmT?VQOx(voKP8mY zbWu0Rm3UNg1MxwL>5DP;Zl!+L2u=FLcB+ zx;o2~>*2$j2#2Yze|0}$9ri`Yr zxt6Xs>(dt+lnd?XNRY$jA=ze{KfNX~Tpql?E>ALAa-__=!Pkeba5`-RTLha%aFw_5 z|JFvqpJLbiOntH|=rRLcls<5Ie(R#dzx=I>Qbcr73MXC|Al|>%>`fMY>-L9_i-zZe zO8_i-1lB2NJ!i=-w+YOGl*)eb9<4pu?`NX0GC4^

T@!GIU;_ui0NBn<%s-

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 diff --git a/doc/Archive/contributed_packages/parmest/parallel.rst b/doc/Archive/contributed_packages/parmest/parallel.rst deleted file mode 100644 index 3b60c5777de..00000000000 --- a/doc/Archive/contributed_packages/parmest/parallel.rst +++ /dev/null @@ -1,52 +0,0 @@ -.. _parallelsection: - -Parallel Implementation -======================= - -Parallel implementation in parmest is **preliminary**. To run parmest -in parallel, you need the mpi4py Python package and a *compatible* MPI -installation. If you do NOT have mpi4py or a MPI installation, parmest -still works (you should not get MPI import errors). - -For example, the following command can be used to run the semibatch -model in parallel:: - - mpiexec -n 4 python parallel_example.py - -The file **parallel_example.py** is shown below. -Results are saved to file for later analysis. - -.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/semibatch/parallel_example.py - :language: python - -Installation ------------- - -The mpi4py Python package should be installed using conda. The -following installation instructions were tested on a Mac with Python -3.5. - -Create a conda environment and install mpi4py using the following -commands:: - - conda create -n parmest-parallel python=3.5 - source activate parmest-parallel - conda install -c conda-forge mpi4py - -This should install libgfortran, mpi, mpi4py, and openmpi. - -To verify proper installation, create a Python file with the following:: - - from mpi4py import MPI - import time - comm = MPI.COMM_WORLD - rank = comm.Get_rank() - print('Rank = ',rank) - time.sleep(10) - -Save the file as test_mpi.py and run the following command:: - - time mpiexec -n 4 python test_mpi.py - time python test_mpi.py - -The first one should be faster and should start 4 instances of Python. diff --git a/doc/Archive/contributed_packages/parmest/scencreate.rst b/doc/Archive/contributed_packages/parmest/scencreate.rst deleted file mode 100644 index b63ac5893c2..00000000000 --- a/doc/Archive/contributed_packages/parmest/scencreate.rst +++ /dev/null @@ -1,22 +0,0 @@ -Scenario Creation -================= - -In addition to model-based parameter estimation, parmest can create -scenarios for use in optimization under uncertainty. To do this, one -first creates an ``Estimator`` object, then a ``ScenarioCreator`` -object, which has methods to add ``ParmestScen`` scenario objects to a -``ScenarioSet`` object, which can write them to a csv file or output them -via an iterator method. - -This example is in the semibatch subdirectory of the examples directory in -the file ``scenario_example.py``. It creates a csv file with scenarios that -correspond one-to-one with the experiments used as input data. It also -creates a few scenarios using the bootstrap methods and outputs prints the -scenarios to the screen, accessing them via the ``ScensItator`` a ``print`` - -.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/semibatch/scenario_example.py - :language: python - -.. note:: - This example may produce an error message if your version of Ipopt is not based - on a good linear solver. diff --git a/doc/Archive/contributed_packages/preprocessing.rst b/doc/Archive/contributed_packages/preprocessing.rst deleted file mode 100644 index fd26f2bf6db..00000000000 --- a/doc/Archive/contributed_packages/preprocessing.rst +++ /dev/null @@ -1,151 +0,0 @@ -Nonlinear Preprocessing Transformations -======================================= - -``pyomo.contrib.preprocessing`` is a contributed library of preprocessing -transformations intended to operate upon nonlinear and mixed-integer nonlinear -programs (NLPs and MINLPs), as well as generalized disjunctive programs (GDPs). - -This contributed package is maintained by `Qi Chen -`_ and `his colleagues from Carnegie Mellon -University `_. - -The following preprocessing transformations are available. However, some may -later be deprecated or combined, depending on their usefulness. - -.. currentmodule:: pyomo.contrib.preprocessing.plugins - -.. autosummary:: - :nosignatures: - - var_aggregator.VariableAggregator - bounds_to_vars.ConstraintToVarBoundTransform - induced_linearity.InducedLinearity - constraint_tightener.TightenConstraintFromVars - deactivate_trivial_constraints.TrivialConstraintDeactivator - detect_fixed_vars.FixedVarDetector - equality_propagate.FixedVarPropagator - equality_propagate.VarBoundPropagator - init_vars.InitMidpoint - init_vars.InitZero - remove_zero_terms.RemoveZeroTerms - strip_bounds.VariableBoundStripper - zero_sum_propagator.ZeroSumPropagator - - -Variable Aggregator -------------------- - -The following code snippet demonstrates usage of the variable aggregation -transformation on a concrete Pyomo model: - -.. doctest:: - - >>> from pyomo.environ import * - >>> m = ConcreteModel() - >>> m.v1 = Var(initialize=1, bounds=(1, 8)) - >>> m.v2 = Var(initialize=2, bounds=(0, 3)) - >>> m.v3 = Var(initialize=3, bounds=(-7, 4)) - >>> m.v4 = Var(initialize=4, bounds=(2, 6)) - >>> m.c1 = Constraint(expr=m.v1 == m.v2) - >>> m.c2 = Constraint(expr=m.v2 == m.v3) - >>> m.c3 = Constraint(expr=m.v3 == m.v4) - >>> TransformationFactory('contrib.aggregate_vars').apply_to(m) - -To see the results of the transformation, you could then use the command - -.. code:: - - >>> m.pprint() - -.. autoclass:: pyomo.contrib.preprocessing.plugins.var_aggregator.VariableAggregator - :members: apply_to, create_using, update_variables - - -Explicit Constraints to Variable Bounds ---------------------------------------- - -.. doctest:: - - >>> from pyomo.environ import * - >>> m = ConcreteModel() - >>> m.v1 = Var(initialize=1) - >>> m.v2 = Var(initialize=2) - >>> m.v3 = Var(initialize=3) - >>> m.c1 = Constraint(expr=m.v1 == 2) - >>> m.c2 = Constraint(expr=m.v2 >= -2) - >>> m.c3 = Constraint(expr=m.v3 <= 5) - >>> TransformationFactory('contrib.constraints_to_var_bounds').apply_to(m) - -.. autoclass:: pyomo.contrib.preprocessing.plugins.bounds_to_vars.ConstraintToVarBoundTransform - :members: apply_to, create_using - - -Induced Linearity Reformulation -------------------------------- - -.. autoclass:: pyomo.contrib.preprocessing.plugins.induced_linearity.InducedLinearity - :members: apply_to, create_using - - -Constraint Bounds Tightener ---------------------------- - -This transformation was developed by `Sunjeev Kale -`_ at Carnegie Mellon University. - -.. autoclass:: pyomo.contrib.preprocessing.plugins.constraint_tightener.TightenConstraintFromVars - :members: apply_to, create_using - -Trivial Constraint Deactivation -------------------------------- - -.. autoclass:: pyomo.contrib.preprocessing.plugins.deactivate_trivial_constraints.TrivialConstraintDeactivator - :members: apply_to, create_using, revert - -Fixed Variable Detection ------------------------- - -.. autoclass:: pyomo.contrib.preprocessing.plugins.detect_fixed_vars.FixedVarDetector - :members: apply_to, create_using, revert - -Fixed Variable Equality Propagator ----------------------------------- - -.. autoclass:: pyomo.contrib.preprocessing.plugins.equality_propagate.FixedVarPropagator - :members: apply_to, create_using, revert - -Variable Bound Equality Propagator ----------------------------------- - -.. autoclass:: pyomo.contrib.preprocessing.plugins.equality_propagate.VarBoundPropagator - :members: apply_to, create_using, revert - -Variable Midpoint Initializer ------------------------------ - -.. autoclass:: pyomo.contrib.preprocessing.plugins.init_vars.InitMidpoint - :members: apply_to, create_using - -Variable Zero Initializer -------------------------- - -.. autoclass:: pyomo.contrib.preprocessing.plugins.init_vars.InitZero - :members: apply_to, create_using - -Zero Term Remover ------------------ - -.. autoclass:: pyomo.contrib.preprocessing.plugins.remove_zero_terms.RemoveZeroTerms - :members: apply_to, create_using - -Variable Bound Remover ----------------------- - -.. autoclass:: pyomo.contrib.preprocessing.plugins.strip_bounds.VariableBoundStripper - :members: apply_to, create_using, revert - -Zero Sum Propagator -------------------- - -.. autoclass:: pyomo.contrib.preprocessing.plugins.zero_sum_propagator.ZeroSumPropagator - :members: apply_to, create_using diff --git a/doc/Archive/contributed_packages/pynumero/api.rst b/doc/Archive/contributed_packages/pynumero/api.rst deleted file mode 100644 index 3d1ac8a189e..00000000000 --- a/doc/Archive/contributed_packages/pynumero/api.rst +++ /dev/null @@ -1,14 +0,0 @@ -.. _pynumero_api: - -PyNumero API -============ - -.. automodule:: pyomo.contrib.pynumero - :members: - :undoc-members: - -.. toctree:: - - pynumero.sparse - pynumero.interfaces - pynumero.linalg diff --git a/doc/Archive/contributed_packages/pynumero/backward_compatibility.rst b/doc/Archive/contributed_packages/pynumero/backward_compatibility.rst deleted file mode 100644 index 036a00bee62..00000000000 --- a/doc/Archive/contributed_packages/pynumero/backward_compatibility.rst +++ /dev/null @@ -1,14 +0,0 @@ -Backward Compatibility -====================== - -While PyNumero is a third-party contribution to Pyomo, we intend to maintain -the stability of its core functionality. The core functionality of PyNumero -consists of: - -1. The ``NLP`` API and ``PyomoNLP`` implementation of this API -2. HSL and MUMPS linear solver interfaces -3. ``BlockVector`` and ``BlockMatrix`` classes -4. CyIpopt and SciPy solver interfaces - -Other parts of PyNumero, such as ``ExternalGreyBoxBlock`` and -``ImplicitFunctionSolver``, are experimental and subject to change without notice. diff --git a/doc/Archive/contributed_packages/pynumero/index.rst b/doc/Archive/contributed_packages/pynumero/index.rst deleted file mode 100644 index 711bb83eb3b..00000000000 --- a/doc/Archive/contributed_packages/pynumero/index.rst +++ /dev/null @@ -1,51 +0,0 @@ -.. _pynumero: - -PyNumero -======== - -PyNumero is a package for developing parallel algorithms for nonlinear -programs (NLPs). This documentation provides a brief introduction to -PyNumero. For more details, see the API documentation (:ref:`pynumero_api`). - -.. toctree:: - :maxdepth: 2 - - installation.rst - tutorial.rst - api.rst - backward_compatibility.rst - - -Developers ----------- - -The development team includes: - -* Jose Santiago Rodriguez -* Michael Bynum -* Carl Laird -* Bethany Nicholson -* Robby Parker -* John Siirola - - -Packages built on PyNumero --------------------------- - * https://github.com/Pyomo/pyomo/tree/main/pyomo/contrib/interior_point - * https://github.com/parapint/parapint - - -Papers utilizing PyNumero -------------------------- - - * Rodriguez, J. S., Laird, C. D., & Zavala, V. M. (2020). Scalable - preconditioning of block-structured linear algebra systems using - ADMM. Computers & Chemical Engineering, 133, 106478. - - -Indices and Tables ------------------- - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/doc/Archive/contributed_packages/pynumero/installation.rst b/doc/Archive/contributed_packages/pynumero/installation.rst deleted file mode 100644 index 9ac6961d2de..00000000000 --- a/doc/Archive/contributed_packages/pynumero/installation.rst +++ /dev/null @@ -1,47 +0,0 @@ -PyNumero Installation -===================== - -PyNumero is a module within Pyomo. Therefore, Pyomo must be installed -to use PyNumero. PyNumero also has some extensions that need -built. There are many ways to build the PyNumero extensions. Common -use cases are listed below. However, more information can always be -found at -https://github.com/Pyomo/pyomo/blob/main/pyomo/contrib/pynumero/build.py -and -https://github.com/Pyomo/pyomo/blob/main/pyomo/contrib/pynumero/src/CMakeLists.txt. - -Note that you will need a C++ compiler and CMake installed to build the -PyNumero libraries. - -Method 1 --------- - -One way to build PyNumero extensions is with the pyomo -`download-extensions` and `build-extensions` subcommands. Note that -this approach will build PyNumero without support for the HSL linear -solvers. :: - - pyomo download-extensions - pyomo build-extensions - -Method 2 --------- - -If you want PyNumero support for the HSL solvers and you have an IPOPT compilation -for your machine, you can build PyNumero using the build script :: - - python -m pyomo.contrib.pynumero.build -DBUILD_ASL=ON -DBUILD_MA27=ON -DIPOPT_DIR= - -Method 3 --------- - -You can build the PyNumero libraries from source using `cmake`. This -generally works best when building from a source distribution of Pyomo. -Assuming that you are starting in the root of the Pyomo source -distribution, you can follow the normal CMake build process :: - - mkdir build - cd build - ccmake ../pyomo/contrib/pynumero/src - make - make install diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst deleted file mode 100644 index 37dd5852351..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.ampl_nlp.rst +++ /dev/null @@ -1,8 +0,0 @@ -AMPL NLP Interface -================== - -.. autoclass:: pyomo.contrib.pynumero.interfaces.ampl_nlp.AmplNLP - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst deleted file mode 100644 index 2537bd52fdb..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.asl_nlp.rst +++ /dev/null @@ -1,8 +0,0 @@ -ASL NLP Interface -================= - -.. autoclass:: pyomo.contrib.pynumero.interfaces.ampl_nlp.AslNLP - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst deleted file mode 100644 index 75528ac4b45..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.extended_nlp.rst +++ /dev/null @@ -1,8 +0,0 @@ -Extended NLP Interface -====================== - -.. autoclass:: pyomo.contrib.pynumero.interfaces.nlp.ExtendedNLP - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst deleted file mode 100644 index 10187b4156e..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.external_grey_box_model.rst +++ /dev/null @@ -1,8 +0,0 @@ -External Grey Box Model -======================= - -.. autoclass:: pyomo.contrib.pynumero.interfaces.external_grey_box.ExternalGreyBoxModel - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.nlp.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.nlp.rst deleted file mode 100644 index d8532873c22..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.nlp.rst +++ /dev/null @@ -1,8 +0,0 @@ -NLP Interface -============= - -.. autoclass:: pyomo.contrib.pynumero.interfaces.nlp.NLP - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst deleted file mode 100644 index b9c6941bd93..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.projected_nlp.rst +++ /dev/null @@ -1,8 +0,0 @@ -Projected NLP Interface -======================= - -.. autoclass:: pyomo.contrib.pynumero.interfaces.nlp_projections.ProjectedNLP - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst deleted file mode 100644 index c7200038f5e..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.pyomo_grey_box_nlp.rst +++ /dev/null @@ -1,8 +0,0 @@ -Pyomo Grey Box NLP Interface -============================ - -.. autoclass:: pyomo.contrib.pynumero.interfaces.pyomo_nlp.PyomoGreyBoxNLP - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst deleted file mode 100644 index e52ce33c2d9..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.pyomo_nlp.rst +++ /dev/null @@ -1,8 +0,0 @@ -Pyomo NLP Interface -=================== - -.. autoclass:: pyomo.contrib.pynumero.interfaces.pyomo_nlp.PyomoNLP - :members: - :undoc-members: - :inherited-members: - :show-inheritance: diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.rst b/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.rst deleted file mode 100644 index ec0b94960f6..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.interfaces.rst +++ /dev/null @@ -1,16 +0,0 @@ -PyNumero NLP Interfaces -======================= - -.. automodule:: pyomo.contrib.pynumero.interfaces - :members: - -.. toctree:: - - pynumero.interfaces.nlp - pynumero.interfaces.extended_nlp - pynumero.interfaces.asl_nlp - pynumero.interfaces.ampl_nlp - pynumero.interfaces.pyomo_nlp - pynumero.interfaces.projected_nlp - pynumero.interfaces.external_grey_box_model - pynumero.interfaces.pyomo_grey_box_nlp diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.linalg.base.rst b/doc/Archive/contributed_packages/pynumero/pynumero.linalg.base.rst deleted file mode 100644 index 0a94f87c6be..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.linalg.base.rst +++ /dev/null @@ -1,26 +0,0 @@ -Linear Solver Base Classes -========================== - -.. autoclass:: pyomo.contrib.pynumero.linalg.base.LinearSolverStatus - :members: - :inherited-members: - :show-inheritance: - :undoc-members: - -.. autoclass:: pyomo.contrib.pynumero.linalg.base.LinearSolverResults - :members: - :inherited-members: - :show-inheritance: - :undoc-members: - -.. autoclass:: pyomo.contrib.pynumero.linalg.base.LinearSolverInterface - :members: - :inherited-members: - :show-inheritance: - :undoc-members: - -.. autoclass:: pyomo.contrib.pynumero.linalg.base.DirectLinearSolverInterface - :members: - :inherited-members: - :show-inheritance: - :undoc-members: diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.linalg.ma27.rst b/doc/Archive/contributed_packages/pynumero/pynumero.linalg.ma27.rst deleted file mode 100644 index f1d2eed3ed0..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.linalg.ma27.rst +++ /dev/null @@ -1,8 +0,0 @@ -HSL MA27 -======== - -.. autoclass:: pyomo.contrib.pynumero.linalg.ma27_interface.MA27 - :members: - :inherited-members: - :show-inheritance: - :undoc-members: diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.linalg.ma57.rst b/doc/Archive/contributed_packages/pynumero/pynumero.linalg.ma57.rst deleted file mode 100644 index c97f193b5f8..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.linalg.ma57.rst +++ /dev/null @@ -1,8 +0,0 @@ -HSL MA57 -======== - -.. autoclass:: pyomo.contrib.pynumero.linalg.ma57_interface.MA57 - :members: - :inherited-members: - :show-inheritance: - :undoc-members: diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.linalg.mumps.rst b/doc/Archive/contributed_packages/pynumero/pynumero.linalg.mumps.rst deleted file mode 100644 index 1fd5998dd4d..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.linalg.mumps.rst +++ /dev/null @@ -1,8 +0,0 @@ -MUMPS -===== - -.. autoclass:: pyomo.contrib.pynumero.linalg.mumps_interface.MumpsCentralizedAssembledLinearSolver - :members: - :inherited-members: - :show-inheritance: - :undoc-members: diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.linalg.rst b/doc/Archive/contributed_packages/pynumero/pynumero.linalg.rst deleted file mode 100644 index 70b091becbd..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.linalg.rst +++ /dev/null @@ -1,14 +0,0 @@ -PyNumero Linear Solver Interfaces -================================= - -.. automodule:: pyomo.contrib.pynumero.linalg - :members: - -.. toctree:: - - pynumero.linalg.base - pynumero.linalg.ma27 - pynumero.linalg.ma57 - pynumero.linalg.mumps - pynumero.linalg.scipy - diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.linalg.scipy.rst b/doc/Archive/contributed_packages/pynumero/pynumero.linalg.scipy.rst deleted file mode 100644 index 7e0a1d0b865..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.linalg.scipy.rst +++ /dev/null @@ -1,14 +0,0 @@ -Scipy -===== - -.. autoclass:: pyomo.contrib.pynumero.linalg.scipy_interface.ScipyLU - :members: - :inherited-members: - :show-inheritance: - :undoc-members: - -.. autoclass:: pyomo.contrib.pynumero.linalg.scipy_interface.ScipyIterative - :members: - :inherited-members: - :show-inheritance: - :undoc-members: diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.sparse.block_vector.rst b/doc/Archive/contributed_packages/pynumero/pynumero.sparse.block_vector.rst deleted file mode 100644 index c17d3d1df86..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.sparse.block_vector.rst +++ /dev/null @@ -1,154 +0,0 @@ -BlockVector -=========== - -Methods specific to :py:class:`pyomo.contrib.pynumero.sparse.block_vector.BlockVector`: - - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_block` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.block_sizes` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block_size` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.is_block_defined` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyfrom` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyto` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copy_structure` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_blocks` - * :py:meth:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.pprint` - -Attributes specific to :py:class:`pyomo.contrib.pynumero.sparse.block_vector.BlockVector`: - - * :py:attr:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.nblocks` - * :py:attr:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.bshape` - * :py:attr:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector.has_none` - - -NumPy compatible methods: - - * `numpy.ndarray.dot() `_ - * `numpy.ndarray.sum() `_ - * `numpy.ndarray.all() `_ - * `numpy.ndarray.any() `_ - * `numpy.ndarray.max() `_ - * `numpy.ndarray.astype() `_ - * `numpy.ndarray.clip() `_ - * `numpy.ndarray.compress() `_ - * `numpy.ndarray.conj() `_ - * `numpy.ndarray.conjugate() `_ - * `numpy.ndarray.nonzero() `_ - * `numpy.ndarray.ptp() `_ - * `numpy.ndarray.round() `_ - * `numpy.ndarray.std() `_ - * `numpy.ndarray.var() `_ - * `numpy.ndarray.tofile() `_ - * `numpy.ndarray.min() `_ - * `numpy.ndarray.mean() `_ - * `numpy.ndarray.prod() `_ - * `numpy.ndarray.fill() `_ - * `numpy.ndarray.tolist() `_ - * `numpy.ndarray.flatten() `_ - * `numpy.ndarray.ravel() `_ - * `numpy.ndarray.argmax() `_ - * `numpy.ndarray.argmin() `_ - * `numpy.ndarray.cumprod() `_ - * `numpy.ndarray.cumsum() `_ - * `numpy.ndarray.copy() `_ - -For example, - -.. code-block:: python - - >>> import numpy as np - >>> from pyomo.contrib.pynumero.sparse import BlockVector - >>> v = BlockVector(2) - >>> v.set_block(0, np.random.normal(size=100)) - >>> v.set_block(1, np.random.normal(size=30)) - >>> avg = v.mean() - -NumPy compatible functions: - - * `numpy.log10() `_ - * `numpy.sin() `_ - * `numpy.cos() `_ - * `numpy.exp() `_ - * `numpy.ceil() `_ - * `numpy.floor() `_ - * `numpy.tan() `_ - * `numpy.arctan() `_ - * `numpy.arcsin() `_ - * `numpy.arccos() `_ - * `numpy.sinh() `_ - * `numpy.cosh() `_ - * `numpy.abs() `_ - * `numpy.tanh() `_ - * `numpy.arccosh() `_ - * `numpy.arcsinh() `_ - * `numpy.arctanh() `_ - * `numpy.fabs() `_ - * `numpy.sqrt() `_ - * `numpy.log() `_ - * `numpy.log2() `_ - * `numpy.absolute() `_ - * `numpy.isfinite() `_ - * `numpy.isinf() `_ - * `numpy.isnan() `_ - * `numpy.log1p() `_ - * `numpy.logical_not() `_ - * `numpy.expm1() `_ - * `numpy.exp2() `_ - * `numpy.sign() `_ - * `numpy.rint() `_ - * `numpy.square() `_ - * `numpy.positive() `_ - * `numpy.negative() `_ - * `numpy.rad2deg() `_ - * `numpy.deg2rad() `_ - * `numpy.conjugate() `_ - * `numpy.reciprocal() `_ - * `numpy.signbit() `_ - * `numpy.add() `_ - * `numpy.multiply() `_ - * `numpy.divide() `_ - * `numpy.subtract() `_ - * `numpy.greater() `_ - * `numpy.greater_equal() `_ - * `numpy.less() `_ - * `numpy.less_equal() `_ - * `numpy.not_equal() `_ - * `numpy.maximum() `_ - * `numpy.minimum() `_ - * `numpy.fmax() `_ - * `numpy.fmin() `_ - * `numpy.equal() `_ - * `numpy.logical_and() `_ - * `numpy.logical_or() `_ - * `numpy.logical_xor() `_ - * `numpy.logaddexp() `_ - * `numpy.logaddexp2() `_ - * `numpy.remainder() `_ - * `numpy.heaviside() `_ - * `numpy.hypot() `_ - -For example, - -.. code-block:: python - - >>> import numpy as np - >>> from pyomo.contrib.pynumero.sparse import BlockVector - >>> v = BlockVector(2) - >>> v.set_block(0, np.random.normal(size=100)) - >>> v.set_block(1, np.random.normal(size=30)) - >>> inf_norm = np.max(np.abs(v)) - -.. autoclass:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_block -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.block_sizes -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.get_block_size -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.is_block_defined -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyfrom -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copyto -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.copy_structure -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.set_blocks -.. automethod:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.pprint -.. autoproperty:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.nblocks -.. autoproperty:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.bshape -.. autoproperty:: pyomo.contrib.pynumero.sparse.block_vector.BlockVector.has_none diff --git a/doc/Archive/contributed_packages/pynumero/pynumero.sparse.rst b/doc/Archive/contributed_packages/pynumero/pynumero.sparse.rst deleted file mode 100644 index 6d903abb5a4..00000000000 --- a/doc/Archive/contributed_packages/pynumero/pynumero.sparse.rst +++ /dev/null @@ -1,9 +0,0 @@ -PyNumero Block Linear Algebra -============================= - -.. automodule:: pyomo.contrib.pynumero.sparse - :members: - -.. toctree:: - - pynumero.sparse.block_vector diff --git a/doc/Archive/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst b/doc/Archive/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst deleted file mode 100644 index 1ce98ce4a63..00000000000 --- a/doc/Archive/contributed_packages/pynumero/tutorial.block_vectors_and_matrices.rst +++ /dev/null @@ -1,272 +0,0 @@ -Block Vectors and Matrices -========================== - -Block vectors and matrices -(:py:class:`~pyomo.contrib.pynumero.sparse.block_vector.BlockVector` -and -:py:class:`~pyomo.contrib.pynumero.sparse.block_matrix.BlockMatrix`) -provide a mechanism to perform linear algebra operations with very -structured matrices and vectors. - -When a BlockVector or BlockMatrix is constructed, the number of blocks -must be specified. - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> import numpy as np - >>> from scipy.sparse import coo_matrix - >>> from pyomo.contrib.pynumero.sparse import BlockVector, BlockMatrix - >>> v = BlockVector(3) - >>> m = BlockMatrix(3, 3) - -Setting blocks: - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> v.set_block(0, np.array([-0.67025575, -1.2])) - >>> v.set_block(1, np.array([0.1, 1.14872127])) - >>> v.set_block(2, np.array([1.25])) - >>> v.flatten() - array([-0.67025575, -1.2 , 0.1 , 1.14872127, 1.25 ]) - -The `flatten` method converts the BlockVector into a NumPy array. - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> m.set_block(0, 0, coo_matrix(np.array([[1.67025575, 0], [0, 2]]))) - >>> m.set_block(0, 1, coo_matrix(np.array([[0, -1.64872127], [0, 1]]))) - >>> m.set_block(0, 2, coo_matrix(np.array([[-1.0], [-1]]))) - >>> m.set_block(1, 0, coo_matrix(np.array([[0, -1.64872127], [0, 1]])).transpose()) - >>> m.set_block(1, 2, coo_matrix(np.array([[-1.0], [0]]))) - >>> m.set_block(2, 0, coo_matrix(np.array([[-1.0], [-1]])).transpose()) - >>> m.set_block(2, 1, coo_matrix(np.array([[-1.0], [0]])).transpose()) - >>> m.tocoo().toarray() - array([[ 1.67025575, 0. , 0. , -1.64872127, -1. ], - [ 0. , 2. , 0. , 1. , -1. ], - [ 0. , 0. , 0. , 0. , -1. ], - [-1.64872127, 1. , 0. , 0. , 0. ], - [-1. , -1. , -1. , 0. , 0. ]]) - -The `tocoo` method converts the `BlockMatrix` to a SciPy sparse `coo_matrix`. - -Once the dimensions of a block have been set, they cannot be changed: - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> v.set_block(0, np.ones(3)) - Traceback (most recent call last): - ... - ValueError: Incompatible dimensions for block 0; got 3; expected 2 - -Properties: - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> v.shape - (5,) - >>> v.size - 5 - >>> v.nblocks - 3 - >>> v.bshape - (3,) - >>> m.shape - (5, 5) - >>> m.bshape - (3, 3) - >>> m.nnz - 12 - -Much of the `BlockVector` API matches that of NumPy arrays: - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> v.sum() - 0.62846552 - >>> v.max() - 1.25 - >>> np.abs(v).flatten() - array([0.67025575, 1.2 , 0.1 , 1.14872127, 1.25 ]) - >>> (2*v).flatten() - array([-1.3405115 , -2.4 , 0.2 , 2.29744254, 2.5 ]) - >>> (v + v).flatten() - array([-1.3405115 , -2.4 , 0.2 , 2.29744254, 2.5 ]) - >>> v.dot(v) - 4.781303326558476 - -Similarly, `BlockMatrix` behaves very similarly to SciPy sparse matrices: - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> (2*m).tocoo().toarray() - array([[ 3.3405115 , 0. , 0. , -3.29744254, -2. ], - [ 0. , 4. , 0. , 2. , -2. ], - [ 0. , 0. , 0. , 0. , -2. ], - [-3.29744254, 2. , 0. , 0. , 0. ], - [-2. , -2. , -2. , 0. , 0. ]]) - >>> (m - m).tocoo().toarray() - array([[0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0.]]) - >>> m * v - BlockVector(3,) - >>> (m * v).flatten() - array([-4.26341971, -2.50127873, -1.25 , -0.09493509, 1.77025575]) - -Accessing blocks - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> v.get_block(1) - array([0.1 , 1.14872127]) - >>> m.get_block(1, 0).toarray() - array([[ 0. , 0. ], - [-1.64872127, 1. ]]) - -Empty blocks in a `BlockMatrix` return `None`: - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> print(m.get_block(1, 1)) - None - -The dimensions of a blocks in a `BlockMatrix` can be set without setting a block: - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> m2 = BlockMatrix(2, 2) - >>> m2.set_row_size(0, 5) - >>> m2.set_block(0, 0, m.get_block(0, 0)) - Traceback (most recent call last): - ... - ValueError: Incompatible row dimensions for row 0; got 2; expected 5.0 - -Note that operations on `BlockVector` and `BlockMatrix` cannot be performed until the dimensions are fully specified: - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> v2 = BlockVector(3) - >>> v + v2 - Traceback (most recent call last): - ... - NotFullyDefinedBlockVectorError: Operation not allowed with None blocks. - >>> m2 = BlockMatrix(3, 3) - >>> m2 * 2 - Traceback (most recent call last): - ... - NotFullyDefinedBlockMatrixError: Operation not allowed with None rows. Specify at least one block in every row - -The `has_none` property can be used to see if a `BlockVector` is fully -specified. If `has_none` returns `True`, then there are `None` blocks, -and the `BlockVector` is not fully specified. - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> v.has_none - False - >>> v2.has_none - True - -For `BlockMatrix`, use the `has_undefined_row_sizes()` and `has_undefined_col_sizes()` methods: - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> m.has_undefined_row_sizes() - False - >>> m.has_undefined_col_sizes() - False - >>> m2.has_undefined_row_sizes() - True - >>> m2.has_undefined_col_sizes() - True - -To efficiently iterate over non-empty blocks in a `BlockMatrix`, use -the `get_block_mask()` method, which returns a 2-D array indicating -where the non-empty blocks are: - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> m.get_block_mask(copy=False) - array([[ True, True, True], - [ True, False, True], - [ True, True, False]]) - >>> for i, j in zip(*np.nonzero(m.get_block_mask(copy=False))): - ... assert m.get_block(i, j) is not None - -Copying data: - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> v2 = v.copy() - >>> v2.flatten() - array([-0.67025575, -1.2 , 0.1 , 1.14872127, 1.25 ]) - >>> v2 = v.copy_structure() - >>> v2.block_sizes() # doctest: +SKIP - array([2, 2, 1]) - >>> v2.copyfrom(v) - >>> v2.flatten() - array([-0.67025575, -1.2 , 0.1 , 1.14872127, 1.25 ]) - >>> m2 = m.copy() - >>> (m - m2).tocoo().toarray() - array([[0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0.]]) - >>> m2 = m.copy_structure() - >>> m2.has_undefined_row_sizes() - False - >>> m2.has_undefined_col_sizes() - False - >>> m2.copyfrom(m) - >>> (m - m2).tocoo().toarray() - array([[0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0.]]) - -Nested blocks: - -.. doctest:: - :skipif: not numpy_available or not scipy_available - - >>> v2 = BlockVector(2) - >>> v2.set_block(0, v) - >>> v2.set_block(1, np.ones(2)) - >>> v2.block_sizes() # doctest: +SKIP - array([5, 2]) - >>> v2.flatten() - array([-0.67025575, -1.2 , 0.1 , 1.14872127, 1.25 , - 1. , 1. ]) - >>> v3 = v2.copy_structure() - >>> v3.fill(1) - >>> (v2 + v3).flatten() - array([ 0.32974425, -0.2 , 1.1 , 2.14872127, 2.25 , - 2. , 2. ]) - >>> np.abs(v2).flatten() - array([0.67025575, 1.2 , 0.1 , 1.14872127, 1.25 , - 1. , 1. ]) - >>> v2.get_block(0) - BlockVector(3,) - -Nested `BlockMatrix` applications work similarly. - -For more information, see the API documentation (:ref:`pynumero_api`). diff --git a/doc/Archive/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst b/doc/Archive/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst deleted file mode 100644 index 02ffe761778..00000000000 --- a/doc/Archive/contributed_packages/pynumero/tutorial.linear_solver_interfaces.rst +++ /dev/null @@ -1,76 +0,0 @@ -Linear Solver Interfaces -======================== - -PyNumero's interfaces to linear solvers are very thin wrappers, and, -hence, are rather low-level. It is relatively easy to wrap these again -for specific applications. For example, see the linear solver -interfaces in -https://github.com/Pyomo/pyomo/tree/main/pyomo/contrib/interior_point/linalg, -which wrap PyNumero's linear solver interfaces. - -The motivation to keep PyNumero's interfaces as such thin wrappers is -that different linear solvers serve different purposes. For example, -HSL's MA27 can factorize symmetric indefinite matrices, while MUMPS -can factorize unsymmetric, symmetric positive definite, or general -symmetric matrices. PyNumero seeks to be independent of the -application, giving more flexibility to algorithm developers. - -Interface to MA27 ------------------ - -.. doctest:: - :skipif: not numpy_available or not scipy_available or not ma27_available - - >>> import numpy as np - >>> from scipy.sparse import coo_matrix - >>> from scipy.sparse import tril - >>> from pyomo.contrib.pynumero.linalg.ma27_interface import MA27 - >>> row = np.array([0, 1, 0, 1, 0, 1, 2, 3, 3, 4, 4, 4]) - >>> col = np.array([0, 1, 3, 3, 4, 4, 4, 0, 1, 0, 1, 2]) - >>> data = np.array([1.67025575, 2, -1.64872127, 1, -1, -1, -1, -1.64872127, 1, -1, -1, -1]) - >>> A = coo_matrix((data, (row, col)), shape=(5,5)) - >>> A.toarray() - array([[ 1.67025575, 0. , 0. , -1.64872127, -1. ], - [ 0. , 2. , 0. , 1. , -1. ], - [ 0. , 0. , 0. , 0. , -1. ], - [-1.64872127, 1. , 0. , 0. , 0. ], - [-1. , -1. , -1. , 0. , 0. ]]) - >>> rhs = np.array([-0.67025575, -1.2, 0.1, 1.14872127, 1.25]) - >>> solver = MA27() - >>> solver.set_cntl(1, 1e-6) # set the pivot tolerance - >>> status = solver.do_symbolic_factorization(A) - >>> status = solver.do_numeric_factorization(A) - >>> x, status = solver.do_back_solve(rhs) - >>> np.max(np.abs(A*x - rhs)) <= 1e-15 - True - - -Interface to MUMPS ------------------- - -.. doctest:: - :skipif: not numpy_available or not scipy_available or not mumps_available - - >>> import numpy as np - >>> from scipy.sparse import coo_matrix - >>> from scipy.sparse import tril - >>> from pyomo.contrib.pynumero.linalg.mumps_interface import MumpsCentralizedAssembledLinearSolver - >>> row = np.array([0, 1, 0, 1, 0, 1, 2, 3, 3, 4, 4, 4]) - >>> col = np.array([0, 1, 3, 3, 4, 4, 4, 0, 1, 0, 1, 2]) - >>> data = np.array([1.67025575, 2, -1.64872127, 1, -1, -1, -1, -1.64872127, 1, -1, -1, -1]) - >>> A = coo_matrix((data, (row, col)), shape=(5,5)) - >>> A.toarray() - array([[ 1.67025575, 0. , 0. , -1.64872127, -1. ], - [ 0. , 2. , 0. , 1. , -1. ], - [ 0. , 0. , 0. , 0. , -1. ], - [-1.64872127, 1. , 0. , 0. , 0. ], - [-1. , -1. , -1. , 0. , 0. ]]) - >>> rhs = np.array([-0.67025575, -1.2, 0.1, 1.14872127, 1.25]) - >>> solver = MumpsCentralizedAssembledLinearSolver(sym=2, par=1, comm=None) # symmetric matrix; solve in serial - >>> solver.do_symbolic_factorization(A) - >>> solver.do_numeric_factorization(A) - >>> x = solver.do_back_solve(rhs) - >>> np.max(np.abs(A*x - rhs)) <= 1e-15 - True - -Of course, SciPy solvers can also be used. See SciPy documentation for details. diff --git a/doc/Archive/contributed_packages/pynumero/tutorial.mpi_blocks.rst b/doc/Archive/contributed_packages/pynumero/tutorial.mpi_blocks.rst deleted file mode 100644 index b9cb1d5db7a..00000000000 --- a/doc/Archive/contributed_packages/pynumero/tutorial.mpi_blocks.rst +++ /dev/null @@ -1,65 +0,0 @@ -MPI-Based Block Vectors and Matrices -==================================== - -PyNumero's MPI-based block vectors and matrices -(:py:class:`~pyomo.contrib.pynumero.sparse.mpi_block_vector.MPIBlockVector` -and -:py:class:`~pyomo.contrib.pynumero.sparse.mpi_block_matrix.MPIBlockMatrix`) -behave very similarly to `BlockVector` and `BlockMatrix`. The primary -difference is in construction. With `MPIBlockVector` and -`MPIBlockMatrix`, each block is owned by either a single process/rank -or all processes/ranks. - -Consider the following example (in a file called "parallel_vector_ops.py"). - -.. literalinclude:: ../../../../pyomo/contrib/pynumero/examples/parallel_vector_ops.py - -This example can be run with - -.. code-block:: - - mpirun -np 3 python -m mpi4py parallel_vector_ops.py - -The output is - -.. code-block:: - - [6. 6. 6. 2. 2. 2. 4. 4. 4. 2. 4. 6.] - 56.0 - 3 - -Note that the `make_local_copy()` method is not efficient and should -only be used for debugging. - -The -1 in `owners` means that the block at that index (index 3 in this -example) is owned by all processes. The non-negative integer values -indicate that the block at that index is owned by the process with -rank equal to the value. In this example, rank 0 owns block 1, rank 1 -owns block 2, and rank 2 owns block 0. Block 3 is owned by all ranks. -Note that blocks should only be set if the process/rank owns that -block. - -The operations performed with `MPIBlockVector` are identical to the -same operations performed with `BlockVector` (or even NumPy arrays), -except that the operations are now performed in parallel. - -`MPIBlockMatrix` construction is very similar. Consider the following -example in a file called "parallel_matvec.py". - -.. literalinclude:: ../../../../pyomo/contrib/pynumero/examples/parallel_matvec.py - -Which can be run with - -.. code-block:: - - mpirun -np 3 python -m mpi4py parallel_matvec.py - -The output is - -.. code-block:: - - error: 4.440892098500626e-16 - -The most difficult part of using `MPIBlockVector` and `MPIBlockMatrix` -is determining the best structure and rank ownership to maximize -parallel efficiency. diff --git a/doc/Archive/contributed_packages/pynumero/tutorial.nlp_interfaces.rst b/doc/Archive/contributed_packages/pynumero/tutorial.nlp_interfaces.rst deleted file mode 100644 index 28818709330..00000000000 --- a/doc/Archive/contributed_packages/pynumero/tutorial.nlp_interfaces.rst +++ /dev/null @@ -1,115 +0,0 @@ -NLP Interfaces -============== - -Below are examples of using PyNumero's interfaces to ASL for function -and derivative evaluation. More information can be found in the API -documentation (:ref:`pynumero_api`). - -Relevant imports - -.. doctest:: - :skipif: not numpy_available or not scipy_available or not asl_available - - >>> import pyomo.environ as pe - >>> from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP - >>> import numpy as np - -Create a Pyomo model - -.. doctest:: - :skipif: not numpy_available or not scipy_available or not asl_available - - >>> m = pe.ConcreteModel() - >>> m.x = pe.Var(bounds=(-5, None)) - >>> m.y = pe.Var(initialize=2.5) - >>> m.obj = pe.Objective(expr=m.x**2 + m.y**2) - >>> m.c1 = pe.Constraint(expr=m.y == (m.x - 1)**2) - >>> m.c2 = pe.Constraint(expr=m.y >= pe.exp(m.x)) - -Create a :py:class:`pyomo.contrib.pynumero.interfaces.pyomo_nlp.PyomoNLP` instance - -.. doctest:: - :skipif: not numpy_available or not scipy_available or not asl_available - - >>> nlp = PyomoNLP(m) - -Get values of primals and duals - -.. doctest:: - :skipif: not numpy_available or not scipy_available or not asl_available - - >>> nlp.get_primals() - array([0. , 2.5]) - >>> nlp.get_duals() - array([0., 0.]) - -Get variable and constraint bounds - -.. doctest:: - :skipif: not numpy_available or not scipy_available or not asl_available - - >>> nlp.primals_lb() - array([ -5., -inf]) - >>> nlp.primals_ub() - array([inf, inf]) - >>> nlp.constraints_lb() - array([ 0., -inf]) - >>> nlp.constraints_ub() - array([0., 0.]) - -Objective and constraint evaluations - -.. doctest:: - :skipif: not numpy_available or not scipy_available or not asl_available - - >>> nlp.evaluate_objective() - 6.25 - >>> nlp.evaluate_constraints() - array([ 1.5, -1.5]) - -Derivative evaluations - -.. doctest:: - :skipif: not numpy_available or not scipy_available or not asl_available - - >>> nlp.evaluate_grad_objective() - array([0., 5.]) - >>> nlp.evaluate_jacobian() # doctest: +SKIP - <2x2 sparse matrix of type '' - with 4 stored elements in COOrdinate format> - >>> nlp.evaluate_jacobian().toarray() - array([[ 2., 1.], - [ 1., -1.]]) - >>> nlp.evaluate_hessian_lag().toarray() - array([[2., 0.], - [0., 2.]]) - -Set values of primals and duals - -.. doctest:: - :skipif: not numpy_available or not scipy_available or not asl_available - - >>> nlp.set_primals(np.array([0, 1])) - >>> nlp.evaluate_constraints() - array([0., 0.]) - >>> nlp.set_duals(np.array([-2/3, 4/3])) - >>> nlp.evaluate_grad_objective() + nlp.evaluate_jacobian().transpose() * nlp.get_duals() - array([0., 0.]) - -Equality and inequality constraints separately - -.. doctest:: - :skipif: not numpy_available or not scipy_available or not asl_available - - >>> nlp.evaluate_eq_constraints() - array([0.]) - >>> nlp.evaluate_jacobian_eq().toarray() - array([[2., 1.]]) - >>> nlp.evaluate_ineq_constraints() - array([0.]) - >>> nlp.evaluate_jacobian_ineq().toarray() - array([[ 1., -1.]]) - >>> nlp.get_duals_eq() - array([-0.66666667]) - >>> nlp.get_duals_ineq() - array([1.33333333]) diff --git a/doc/Archive/contributed_packages/pynumero/tutorial.rst b/doc/Archive/contributed_packages/pynumero/tutorial.rst deleted file mode 100644 index 83593c94040..00000000000 --- a/doc/Archive/contributed_packages/pynumero/tutorial.rst +++ /dev/null @@ -1,11 +0,0 @@ -10 Minutes to PyNumero -====================== - -.. toctree:: - - tutorial.nlp_interfaces - tutorial.linear_solver_interfaces - tutorial.block_vectors_and_matrices - tutorial.mpi_blocks - -Other examples may be found at https://github.com/Pyomo/pyomo/tree/main/pyomo/contrib/pynumero/examples. diff --git a/doc/Archive/contributed_packages/pyros.rst b/doc/Archive/contributed_packages/pyros.rst deleted file mode 100644 index 95049eded8a..00000000000 --- a/doc/Archive/contributed_packages/pyros.rst +++ /dev/null @@ -1,1078 +0,0 @@ -############ -PyROS Solver -############ - -PyROS (Pyomo Robust Optimization Solver) is a Pyomo-based meta-solver -for non-convex, two-stage adjustable robust optimization problems. - -It was developed by **Natalie M. Isenberg**, **Jason A. F. Sherman**, -and **Chrysanthos E. Gounaris** of Carnegie Mellon University, -in collaboration with **John D. Siirola** of Sandia National Labs. -The developers gratefully acknowledge support from the U.S. Department of Energy's -`Institute for the Design of Advanced Energy Systems (IDAES) `_. - -Methodology Overview ------------------------------ - -Below is an overview of the type of optimization models PyROS can accommodate. - - -* PyROS is suitable for optimization models of **continuous variables** - that may feature non-linearities (including **non-convexities**) in - both the variables and uncertain parameters. -* PyROS can handle **equality constraints** defining state variables, - including implicit state variables that cannot be eliminated via - reformulation. -* PyROS allows for **two-stage** optimization problems that may - feature both first-stage and second-stage degrees of freedom. - -PyROS is designed to operate on deterministic models of the general form - -.. _deterministic-model: - -.. math:: - \begin{array}{clll} - \displaystyle \min_{\substack{x \in \mathcal{X}, \\ z \in \mathbb{R}^{n_z}, y\in\mathbb{R}^{n_y}}} & ~~ f_1\left(x\right) + f_2(x,z,y; q^{\text{nom}}) & \\ - \displaystyle \text{s.t.} & ~~ g_i(x, z, y; q^{\text{nom}}) \leq 0 & \forall\,i \in \mathcal{I} \\ - & ~~ h_j(x,z,y; q^{\text{nom}}) = 0 & \forall\,j \in \mathcal{J} \\ - \end{array} - -where: - -* :math:`x \in \mathcal{X}` are the "design" variables - (i.e., first-stage degrees of freedom), - where :math:`\mathcal{X} \subseteq \mathbb{R}^{n_x}` is the feasible space defined by the model constraints - (including variable bounds specifications) referencing :math:`x` only. -* :math:`z \in \mathbb{R}^{n_z}` are the "control" variables - (i.e., second-stage degrees of freedom) -* :math:`y \in \mathbb{R}^{n_y}` are the "state" variables -* :math:`q \in \mathbb{R}^{n_q}` is the vector of model parameters considered - uncertain, and :math:`q^{\text{nom}}` is the vector of nominal values - associated with those. -* :math:`f_1\left(x\right)` are the terms of the objective function that depend - only on design variables -* :math:`f_2\left(x, z, y; q\right)` are the terms of the objective function - that depend on all variables and the uncertain parameters -* :math:`g_i\left(x, z, y; q\right)` is the :math:`i^\text{th}` - inequality constraint function in set :math:`\mathcal{I}` - (see :ref:`Note `) -* :math:`h_j\left(x, z, y; q\right)` is the :math:`j^\text{th}` - equality constraint function in set :math:`\mathcal{J}` - (see :ref:`Note `) - -.. _var-bounds-to-ineqs: - -.. note:: - PyROS accepts models in which bounds are directly imposed on - ``Var`` objects representing components of the variables :math:`z` - and :math:`y`. These models are cast to - :ref:`the form above ` - by reformulating the bounds as inequality constraints. - -.. _unique-mapping: - -.. note:: - A key requirement of PyROS is that each value of :math:`\left(x, z, q \right)` - maps to a unique value of :math:`y`, a property that is assumed to - be properly enforced by the system of equality constraints - :math:`\mathcal{J}`. - If the mapping is not unique, then the selection of 'state' - (i.e., not degree of freedom) variables :math:`y` is incorrect, - and one or more of the :math:`y` variables should be appropriately - redesignated to be part of either :math:`x` or :math:`z`. - -In order to cast the robust optimization counterpart of the -:ref:`deterministic model `, -we now assume that the uncertain parameters may attain -any realization in a compact uncertainty set -:math:`\mathcal{Q} \subseteq \mathbb{R}^{n_q}` containing -the nominal value :math:`q^{\text{nom}}`. -The set :math:`\mathcal{Q}` may be **either continuous or discrete**. - -Based on the above notation, the form of the robust counterpart addressed by PyROS is - -.. math:: - \begin{array}{ccclll} - \displaystyle \min_{x \in \mathcal{X}} - & \displaystyle \max_{q \in \mathcal{Q}} - & \displaystyle \min_{\substack{z \in \mathbb{R}^{n_z},\\y \in \mathbb{R}^{n_y}}} \ \ & \displaystyle ~~ f_1\left(x\right) + f_2\left(x, z, y, q\right) \\ - & & \text{s.t.}~ & \displaystyle ~~ g_i\left(x, z, y, q\right) \leq 0 & & \forall\, i \in \mathcal{I}\\ - & & & \displaystyle ~~ h_j\left(x, z, y, q\right) = 0 & & \forall\,j \in \mathcal{J} - \end{array} - -PyROS solves problems of this form using the -Generalized Robust Cutting-Set algorithm developed in [Isenberg_et_al]_. - -When using PyROS, please consider citing the above paper. - -PyROS Required Inputs ------------------------------ -The required inputs to the PyROS solver are: - -* The deterministic optimization model -* List of first-stage ("design") variables -* List of second-stage ("control") variables -* List of parameters considered uncertain -* The uncertainty set -* Subordinate local and global nonlinear programming (NLP) solvers - -These are more elaborately presented in the -:ref:`Solver Interface ` section. - -.. note:: - Any variables in the model not specified to be first-stage or second-stage - variables are automatically considered to be state variables. - -.. _solver-interface: - -PyROS Solver Interface ------------------------------ - -.. autoclass:: pyomo.contrib.pyros.PyROS - :members: solve - -.. note:: - Upon successful convergence of PyROS, the solution returned is - certified to be robust optimal only if: - - 1. master problems are solved to global optimality - (by specifying ``solve_master_globally=True``) - 2. a worst-case objective focus is chosen - (by specifying ``objective_focus=ObjectiveType.worst_case``) - - Otherwise, the solution returned is certified to only be robust feasible. - - -PyROS Uncertainty Sets ------------------------------ -Uncertainty sets are represented by subclasses of -the :class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` -abstract base class. -PyROS provides a suite of pre-implemented subclasses representing -commonly used uncertainty sets. -Custom user-defined uncertainty set types may be implemented by -subclassing the -:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` class. -The intersection of a sequence of concrete -:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` -instances can be easily constructed by instantiating the pre-implemented -:class:`~pyomo.contrib.pyros.uncertainty_sets.IntersectionSet` -subclass. - -The table that follows provides mathematical definitions of -the various abstract and pre-implemented -:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` subclasses. - -.. _table-uncertsets: - -.. list-table:: Mathematical definitions of PyROS uncertainty sets of dimension :math:`n`. - :header-rows: 1 - :class: tight-table - - * - Uncertainty Set Type - - Input Data - - Mathematical Definition - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` - - :math:`\begin{array}{l} q ^{\text{L}} \in \mathbb{R}^{n}, \\ q^{\text{U}} \in \mathbb{R}^{n} \end{array}` - - :math:`\{q \in \mathbb{R}^n \mid q^\mathrm{L} \leq q \leq q^\mathrm{U}\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.CardinalitySet` - - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ \hat{q} \in \mathbb{R}_{+}^{n}, \\ \Gamma \in [0, n] \end{array}` - - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} q = q^{0} + \hat{q} \circ \xi \\ \displaystyle \sum_{i=1}^{n} \xi_{i} \leq \Gamma \\ \xi \in [0, 1]^{n} \end{array} \right\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.BudgetSet` - - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ b \in \mathbb{R}_{+}^{L}, \\ B \in \{0, 1\}^{L \times n} \end{array}` - - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \begin{pmatrix} B \\ -I \end{pmatrix} q \leq \begin{pmatrix} b + Bq^{0} \\ -q^{0} \end{pmatrix} \end{array} \right\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.FactorModelSet` - - :math:`\begin{array}{l} q^{0} \in \mathbb{R}^{n}, \\ \Psi \in \mathbb{R}^{n \times F}, \\ \beta \in [0, 1] \end{array}` - - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} q = q^{0} + \Psi \xi \\ \displaystyle\bigg| \sum_{j=1}^{F} \xi_{j} \bigg| \leq \beta F \\ \xi \in [-1, 1]^{F} \\ \end{array} \right\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.PolyhedralSet` - - :math:`\begin{array}{l} A \in \mathbb{R}^{m \times n}, \\ b \in \mathbb{R}^{m}\end{array}` - - :math:`\{q \in \mathbb{R}^{n} \mid A q \leq b\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.AxisAlignedEllipsoidalSet` - - :math:`\begin{array}{l} q^0 \in \mathbb{R}^{n}, \\ \alpha \in \mathbb{R}_{+}^{n} \end{array}` - - :math:`\left\{ q \in \mathbb{R}^{n} \middle| \begin{array}{l} \displaystyle\sum_{\substack{i = 1: \\ \alpha_{i} > 0}}^{n} \left(\frac{q_{i} - q_{i}^{0}}{\alpha_{i}}\right)^2 \leq 1 \\ q_{i} = q_{i}^{0} \,\forall\,i : \alpha_{i} = 0 \end{array} \right\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.EllipsoidalSet` - - :math:`\begin{array}{l} q^0 \in \mathbb{R}^n, \\ P \in \mathbb{S}_{++}^{n}, \\ s \in \mathbb{R}_{+} \end{array}` - - :math:`\{q \in \mathbb{R}^{n} \mid (q - q^{0})^{\intercal} P^{-1} (q - q^{0}) \leq s\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` - - :math:`g: \mathbb{R}^{n} \to \mathbb{R}^{m}` - - :math:`\{q \in \mathbb{R}^{n} \mid g(q) \leq 0\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.DiscreteScenarioSet` - - :math:`q^{1}, q^{2},\dots , q^{S} \in \mathbb{R}^{n}` - - :math:`\{q^{1}, q^{2}, \dots , q^{S}\}` - * - :class:`~pyomo.contrib.pyros.uncertainty_sets.IntersectionSet` - - :math:`\mathcal{Q}_{1}, \mathcal{Q}_{2}, \dots , \mathcal{Q}_{m} \subset \mathbb{R}^{n}` - - :math:`\displaystyle \bigcap_{i=1}^{m} \mathcal{Q}_{i}` - -.. note:: - Each of the PyROS uncertainty set classes inherits from the - :class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` - abstract base class. - -PyROS Uncertainty Set Classes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.BoxSet - :show-inheritance: - :special-members: bounds, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.CardinalitySet - :show-inheritance: - :special-members: origin, positive_deviation, gamma, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.BudgetSet - :show-inheritance: - :special-members: coefficients_mat, rhs_vec, origin, budget_membership_mat, budget_rhs_vec, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.FactorModelSet - :show-inheritance: - :special-members: origin, number_of_factors, psi_mat, beta, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.PolyhedralSet - :show-inheritance: - :special-members: coefficients_mat, rhs_vec, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.AxisAlignedEllipsoidalSet - :show-inheritance: - :special-members: center, half_lengths, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.EllipsoidalSet - :show-inheritance: - :special-members: center, shape_matrix, scale, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.UncertaintySet - :show-inheritance: - :special-members: parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.DiscreteScenarioSet - :show-inheritance: - :special-members: scenarios, type, parameter_bounds, dim, point_in_set - -.. autoclass:: pyomo.contrib.pyros.uncertainty_sets.IntersectionSet - :show-inheritance: - :special-members: all_sets, type, parameter_bounds, dim, point_in_set - - -PyROS Usage Example ------------------------------ - -In this section, we illustrate the usage of PyROS with a modeling example. -The deterministic problem of interest is called *hydro* -(available `here `_), -a QCQP taken from the -`GAMS Model Library `_. -We have converted the model to Pyomo format using the -`GAMS Convert tool `_. - -The *hydro* model features 31 variables, -of which 13 are degrees of freedom and 18 are state variables. -Moreover, there are -6 linear inequality constraints, -12 linear equality constraints, -6 non-linear (quadratic) equality constraints, -and a quadratic objective. -We have extended this model by converting one objective coefficient, -two constraint coefficients, and one constraint right-hand side -into ``Param`` objects so that they can be considered uncertain later on. - -.. note:: - Per our analysis, the *hydro* problem satisfies the requirement that - each value of :math:`\left(x, z, q \right)` maps to a unique - value of :math:`y`, which, in accordance with - :ref:`our earlier note `, - indicates a proper partitioning of the model variables - into (first-stage and second-stage) degrees of freedom and - state variables. - -Step 0: Import Pyomo and the PyROS Module -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In anticipation of using the PyROS solver and building the deterministic Pyomo -model: - -.. doctest:: - - >>> # === Required import === - >>> import pyomo.environ as pyo - >>> import pyomo.contrib.pyros as pyros - - >>> # === Instantiate the PyROS solver object === - >>> pyros_solver = pyo.SolverFactory("pyros") - -Step 1: Define the Deterministic Problem -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The deterministic Pyomo model for *hydro* is shown below. - -.. note:: - Primitive data (Python literals) that have been hard-coded within a - deterministic model cannot be later considered uncertain, - unless they are first converted to ``Param`` objects within - the ``ConcreteModel`` object. - Furthermore, any ``Param`` object that is to be later considered - uncertain must have the property ``mutable=True``. - -.. note:: - In case modifying the ``mutable`` property inside the deterministic - model object itself is not straightforward in your context, - you may consider adding the following statement **after** - ``import pyomo.environ as pyo`` but **before** defining the model - object: ``pyo.Param.DefaultMutable = True``. - For all ``Param`` objects declared after this statement, - the attribute ``mutable`` is set to ``True`` by default. - Hence, non-mutable ``Param`` objects are now declared by - explicitly passing the argument ``mutable=False`` to the - ``Param`` constructor. - -.. doctest:: - - - >>> # === Construct the Pyomo model object === - >>> m = pyo.ConcreteModel() - >>> m.name = "hydro" - - >>> # === Define variables === - >>> m.x1 = pyo.Var(within=pyo.Reals,bounds=(150,1500),initialize=150) - >>> m.x2 = pyo.Var(within=pyo.Reals,bounds=(150,1500),initialize=150) - >>> m.x3 = pyo.Var(within=pyo.Reals,bounds=(150,1500),initialize=150) - >>> m.x4 = pyo.Var(within=pyo.Reals,bounds=(150,1500),initialize=150) - >>> m.x5 = pyo.Var(within=pyo.Reals,bounds=(150,1500),initialize=150) - >>> m.x6 = pyo.Var(within=pyo.Reals,bounds=(150,1500),initialize=150) - >>> m.x7 = pyo.Var(within=pyo.Reals,bounds=(0,1000),initialize=0) - >>> m.x8 = pyo.Var(within=pyo.Reals,bounds=(0,1000),initialize=0) - >>> m.x9 = pyo.Var(within=pyo.Reals,bounds=(0,1000),initialize=0) - >>> m.x10 = pyo.Var(within=pyo.Reals,bounds=(0,1000),initialize=0) - >>> m.x11 = pyo.Var(within=pyo.Reals,bounds=(0,1000),initialize=0) - >>> m.x12 = pyo.Var(within=pyo.Reals,bounds=(0,1000),initialize=0) - >>> m.x13 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) - >>> m.x14 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) - >>> m.x15 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) - >>> m.x16 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) - >>> m.x17 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) - >>> m.x18 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) - >>> m.x19 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) - >>> m.x20 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) - >>> m.x21 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) - >>> m.x22 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) - >>> m.x23 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) - >>> m.x24 = pyo.Var(within=pyo.Reals,bounds=(0,None),initialize=0) - >>> m.x25 = pyo.Var(within=pyo.Reals,bounds=(100000,100000),initialize=100000) - >>> m.x26 = pyo.Var(within=pyo.Reals,bounds=(60000,120000),initialize=60000) - >>> m.x27 = pyo.Var(within=pyo.Reals,bounds=(60000,120000),initialize=60000) - >>> m.x28 = pyo.Var(within=pyo.Reals,bounds=(60000,120000),initialize=60000) - >>> m.x29 = pyo.Var(within=pyo.Reals,bounds=(60000,120000),initialize=60000) - >>> m.x30 = pyo.Var(within=pyo.Reals,bounds=(60000,120000),initialize=60000) - >>> m.x31 = pyo.Var(within=pyo.Reals,bounds=(60000,120000),initialize=60000) - - >>> # === Define parameters === - >>> m.set_of_params = pyo.Set(initialize=[0, 1, 2, 3]) - >>> nominal_values = {0:82.8*0.0016, 1:4.97, 2:4.97, 3:1800} - >>> m.p = pyo.Param(m.set_of_params, initialize=nominal_values, mutable=True) - - >>> # === Specify the objective function === - >>> m.obj = pyo.Objective(expr=m.p[0]*m.x1**2 + 82.8*8*m.x1 + 82.8*0.0016*m.x2**2 + - ... 82.8*82.8*8*m.x2 + 82.8*0.0016*m.x3**2 + 82.8*8*m.x3 + - ... 82.8*0.0016*m.x4**2 + 82.8*8*m.x4 + 82.8*0.0016*m.x5**2 + - ... 82.8*8*m.x5 + 82.8*0.0016*m.x6**2 + 82.8*8*m.x6 + 248400, - ... sense=pyo.minimize) - - >>> # === Specify the constraints === - >>> m.c2 = pyo.Constraint(expr=-m.x1 - m.x7 + m.x13 + 1200<= 0) - >>> m.c3 = pyo.Constraint(expr=-m.x2 - m.x8 + m.x14 + 1500 <= 0) - >>> m.c4 = pyo.Constraint(expr=-m.x3 - m.x9 + m.x15 + 1100 <= 0) - >>> m.c5 = pyo.Constraint(expr=-m.x4 - m.x10 + m.x16 + m.p[3] <= 0) - >>> m.c6 = pyo.Constraint(expr=-m.x5 - m.x11 + m.x17 + 950 <= 0) - >>> m.c7 = pyo.Constraint(expr=-m.x6 - m.x12 + m.x18 + 1300 <= 0) - >>> m.c8 = pyo.Constraint(expr=12*m.x19 - m.x25 + m.x26 == 24000) - >>> m.c9 = pyo.Constraint(expr=12*m.x20 - m.x26 + m.x27 == 24000) - >>> m.c10 = pyo.Constraint(expr=12*m.x21 - m.x27 + m.x28 == 24000) - >>> m.c11 = pyo.Constraint(expr=12*m.x22 - m.x28 + m.x29 == 24000) - >>> m.c12 = pyo.Constraint(expr=12*m.x23 - m.x29 + m.x30 == 24000) - >>> m.c13 = pyo.Constraint(expr=12*m.x24 - m.x30 + m.x31 == 24000) - >>> m.c14 = pyo.Constraint(expr=-8e-5*m.x7**2 + m.x13 == 0) - >>> m.c15 = pyo.Constraint(expr=-8e-5*m.x8**2 + m.x14 == 0) - >>> m.c16 = pyo.Constraint(expr=-8e-5*m.x9**2 + m.x15 == 0) - >>> m.c17 = pyo.Constraint(expr=-8e-5*m.x10**2 + m.x16 == 0) - >>> m.c18 = pyo.Constraint(expr=-8e-5*m.x11**2 + m.x17 == 0) - >>> m.c19 = pyo.Constraint(expr=-8e-5*m.x12**2 + m.x18 == 0) - >>> m.c20 = pyo.Constraint(expr=-4.97*m.x7 + m.x19 == 330) - >>> m.c21 = pyo.Constraint(expr=-m.p[1]*m.x8 + m.x20 == 330) - >>> m.c22 = pyo.Constraint(expr=-4.97*m.x9 + m.x21 == 330) - >>> m.c23 = pyo.Constraint(expr=-4.97*m.x10 + m.x22 == 330) - >>> m.c24 = pyo.Constraint(expr=-m.p[2]*m.x11 + m.x23 == 330) - >>> m.c25 = pyo.Constraint(expr=-4.97*m.x12 + m.x24 == 330) - -Step 2: Define the Uncertainty -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -First, we need to collect into a list those ``Param`` objects of our model -that represent potentially uncertain parameters. -For the purposes of our example, we shall assume uncertainty in the model -parameters ``[m.p[0], m.p[1], m.p[2], m.p[3]]``, for which we can -conveniently utilize the object ``m.p`` (itself an indexed ``Param`` object). - -.. doctest:: - - >>> # === Specify which parameters are uncertain === - >>> # We can pass IndexedParams this way to PyROS, - >>> # or as an expanded list per index - >>> uncertain_parameters = [m.p] - -.. note:: - Any ``Param`` object that is to be considered uncertain by PyROS - must have the property ``mutable=True``. - -PyROS will seek to identify solutions that remain feasible for any -realization of these parameters included in an uncertainty set. -To that end, we need to construct an -:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` -object. -In our example, let us utilize the -:class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` -constructor to specify -an uncertainty set of simple hyper-rectangular geometry. -For this, we will assume each parameter value is uncertain within a -percentage of its nominal value. Constructing this specific -:class:`~pyomo.contrib.pyros.uncertainty_sets.UncertaintySet` -object can be done as follows: - -.. doctest:: - - >>> # === Define the pertinent data === - >>> relative_deviation = 0.15 - >>> bounds = [ - ... (nominal_values[i] - relative_deviation*nominal_values[i], - ... nominal_values[i] + relative_deviation*nominal_values[i]) - ... for i in range(4) - ... ] - - >>> # === Construct the desirable uncertainty set === - >>> box_uncertainty_set = pyros.BoxSet(bounds=bounds) - -Step 3: Solve with PyROS -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -PyROS requires the user to supply one local and one global NLP solver to use -for solving sub-problems. -For convenience, we shall have PyROS invoke BARON as both the local and the -global NLP solver: - -.. doctest:: - :skipif: not (baron.available() and baron.license_is_valid()) - - >>> # === Designate local and global NLP solvers === - >>> local_solver = pyo.SolverFactory('baron') - >>> global_solver = pyo.SolverFactory('baron') - -.. note:: - Additional NLP optimizers can be automatically used in the event the primary - subordinate local or global optimizer passed - to the PyROS :meth:`~pyomo.contrib.pyros.PyROS.solve` method - does not successfully solve a subproblem to an appropriate termination - condition. These alternative solvers are provided through the optional - keyword arguments ``backup_local_solvers`` and ``backup_global_solvers``. - -The final step in solving a model with PyROS is to construct the -remaining required inputs, namely -``first_stage_variables`` and ``second_stage_variables``. -Below, we present two separate cases. - -PyROS Termination Conditions -""""""""""""""""""""""""""""" - -PyROS will return one of six termination conditions upon completion. -These termination conditions are defined through the -:class:`~pyomo.contrib.pyros.util.pyrosTerminationCondition` enumeration -and tabulated below. - -.. table:: PyROS termination conditions. - - +----------------------------------------------------------------------------------+----------------------------------------------------------------+ - | Termination Condition | Description | - +==================================================================================+================================================================+ - | :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_optimal` | The final solution is robust optimal | - +----------------------------------------------------------------------------------+----------------------------------------------------------------+ - | :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_feasible` | The final solution is robust feasible | - +----------------------------------------------------------------------------------+----------------------------------------------------------------+ - | :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.robust_infeasible` | The posed problem is robust infeasible | - +----------------------------------------------------------------------------------+----------------------------------------------------------------+ - | :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.max_iter` | Maximum number of GRCS iteration reached | - +----------------------------------------------------------------------------------+----------------------------------------------------------------+ - | :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.time_out` | Maximum number of time reached | - +----------------------------------------------------------------------------------+----------------------------------------------------------------+ - | :attr:`~pyomo.contrib.pyros.util.pyrosTerminationCondition.subsolver_error` | Unacceptable return status(es) from a user-supplied sub-solver| - +----------------------------------------------------------------------------------+----------------------------------------------------------------+ - - -A Single-Stage Problem -""""""""""""""""""""""""" -If we choose to designate all variables as either design or state variables, -without any control variables (i.e., all degrees of freedom are first-stage), -we can use PyROS to solve the single-stage problem as shown below. -In particular, let us instruct PyROS that variables -``m.x1`` through ``m.x6``, ``m.x19`` through ``m.x24``, and ``m.x31`` -correspond to first-stage degrees of freedom. - -.. _single-stage-problem: - -.. doctest:: - :skipif: not (baron.available() and baron.license_is_valid()) - - >>> # === Designate which variables correspond to first-stage - >>> # and second-stage degrees of freedom === - >>> first_stage_variables = [ - ... m.x1, m.x2, m.x3, m.x4, m.x5, m.x6, - ... m.x19, m.x20, m.x21, m.x22, m.x23, m.x24, m.x31, - ... ] - >>> second_stage_variables = [] - >>> # The remaining variables are implicitly designated to be state variables - - >>> # === Call PyROS to solve the robust optimization problem === - >>> results_1 = pyros_solver.solve( - ... model=m, - ... first_stage_variables=first_stage_variables, - ... second_stage_variables=second_stage_variables, - ... uncertain_params=uncertain_parameters, - ... uncertainty_set=box_uncertainty_set, - ... local_solver=local_solver, - ... global_solver=global_solver, - ... objective_focus=pyros.ObjectiveType.worst_case, - ... solve_master_globally=True, - ... load_solution=False, - ... ) - ============================================================================== - PyROS: The Pyomo Robust Optimization Solver... - ... - ------------------------------------------------------------------------------ - Robust optimal solution identified. - ------------------------------------------------------------------------------ - ... - ------------------------------------------------------------------------------ - All done. Exiting PyROS. - ============================================================================== - >>> # === Query results === - >>> time = results_1.time - >>> iterations = results_1.iterations - >>> termination_condition = results_1.pyros_termination_condition - >>> objective = results_1.final_objective_value - >>> # === Print some results === - >>> single_stage_final_objective = round(objective,-1) - >>> print(f"Final objective value: {single_stage_final_objective}") - Final objective value: 48367380.0 - >>> print(f"PyROS termination condition: {termination_condition}") - PyROS termination condition: pyrosTerminationCondition.robust_optimal - -PyROS Results Object -""""""""""""""""""""""""""" -The results object returned by PyROS allows you to query the following information -from the solve call: - -* ``iterations``: total iterations of the algorithm -* ``time``: total wallclock time (or elapsed time) in seconds -* ``pyros_termination_condition``: the GRCS algorithm termination condition -* ``final_objective_value``: the final objective function value. - -The :ref:`preceding code snippet ` -demonstrates how to retrieve this information. - -If we pass ``load_solution=True`` (the default setting) -to the :meth:`~pyomo.contrib.pyros.PyROS.solve` method, -then the solution at which PyROS terminates will be loaded to -the variables of the original deterministic model. -Note that in the :ref:`preceding code snippet `, -we set ``load_solution=False`` to ensure the next set of runs shown here can -utilize the initial point loaded to the original deterministic model, -as the initial point may affect the performance of sub-solvers. - -.. note:: - The reported ``final_objective_value`` and final model variable values - depend on the selection of the option ``objective_focus``. - The ``final_objective_value`` is the sum of first-stage - and second-stage objective functions. - If ``objective_focus = ObjectiveType.nominal``, - second-stage objective and variables are evaluated at - the nominal realization of the uncertain parameters, :math:`q^{\text{nom}}`. - If ``objective_focus = ObjectiveType.worst_case``, second-stage objective - and variables are evaluated at the worst-case realization - of the uncertain parameters, :math:`q^{k^\ast}` - where :math:`k^\ast = \mathrm{argmax}_{k \in \mathcal{K}}~f_2(x,z^k,y^k,q^k)`. - - -A Two-Stage Problem -"""""""""""""""""""""" -For this next set of runs, we will -assume that some of the previously designated first-stage degrees of -freedom are in fact second-stage degrees of freedom. -PyROS handles second-stage degrees of freedom via the use of polynomial -decision rules, of which the degree is controlled through the -optional keyword argument ``decision_rule_order`` to the PyROS -:meth:`~pyomo.contrib.pyros.PyROS.solve` method. -In this example, we select affine decision rules by setting -``decision_rule_order=1``: - -.. _example-two-stg: - -.. doctest:: - :skipif: not (baron.available() and baron.license_is_valid()) - - >>> # === Define the variable partitioning - >>> first_stage_variables =[m.x5, m.x6, m.x19, m.x22, m.x23, m.x24, m.x31] - >>> second_stage_variables = [m.x1, m.x2, m.x3, m.x4, m.x20, m.x21] - >>> # The remaining variables are implicitly designated to be state variables - - >>> # === Call PyROS to solve the robust optimization problem === - >>> results_2 = pyros_solver.solve( - ... model=m, - ... first_stage_variables=first_stage_variables, - ... second_stage_variables=second_stage_variables, - ... uncertain_params=uncertain_parameters, - ... uncertainty_set=box_uncertainty_set, - ... local_solver=local_solver, - ... global_solver=global_solver, - ... objective_focus=pyros.ObjectiveType.worst_case, - ... solve_master_globally=True, - ... decision_rule_order=1, - ... ) - ============================================================================== - PyROS: The Pyomo Robust Optimization Solver... - ... - ------------------------------------------------------------------------------ - Robust optimal solution identified. - ------------------------------------------------------------------------------ - ... - ------------------------------------------------------------------------------ - All done. Exiting PyROS. - ============================================================================== - >>> # === Compare final objective to the single-stage solution - >>> two_stage_final_objective = round( - ... pyo.value(results_2.final_objective_value), - ... -1, - ... ) - >>> percent_difference = 100 * ( - ... two_stage_final_objective - single_stage_final_objective - ... ) / (single_stage_final_objective) - >>> print("Percent objective change relative to constant decision rules " - ... f"objective: {percent_difference:.2f}") - Percent objective change relative to constant decision rules objective: -24... - -For this example, we notice a ~25% decrease in the final objective -value when switching from a static decision rule (no second-stage recourse) -to an affine decision rule. - - -Specifying Arguments Indirectly Through ``options`` -""""""""""""""""""""""""""""""""""""""""""""""""""" -Like other Pyomo solver interface methods, -:meth:`~pyomo.contrib.pyros.PyROS.solve` -provides support for specifying options indirectly by passing -a keyword argument ``options``, whose value must be a :class:`dict` -mapping names of arguments to :meth:`~pyomo.contrib.pyros.PyROS.solve` -to their desired values. -For example, the ``solve()`` statement in the -:ref:`two-stage problem snippet ` -could have been equivalently written as: - -.. doctest:: - :skipif: not (baron.available() and baron.license_is_valid()) - - >>> results_2 = pyros_solver.solve( - ... model=m, - ... first_stage_variables=first_stage_variables, - ... second_stage_variables=second_stage_variables, - ... uncertain_params=uncertain_parameters, - ... uncertainty_set=box_uncertainty_set, - ... local_solver=local_solver, - ... global_solver=global_solver, - ... options={ - ... "objective_focus": pyros.ObjectiveType.worst_case, - ... "solve_master_globally": True, - ... "decision_rule_order": 1, - ... }, - ... ) - ============================================================================== - PyROS: The Pyomo Robust Optimization Solver... - ... - ------------------------------------------------------------------------------ - Robust optimal solution identified. - ------------------------------------------------------------------------------ - ... - ------------------------------------------------------------------------------ - All done. Exiting PyROS. - ============================================================================== - -In the event an argument is passed directly -by position or keyword, *and* indirectly through ``options``, -an appropriate warning is issued, -and the value passed directly takes precedence over the value -passed through ``options``. - - -The Price of Robustness -"""""""""""""""""""""""" -In conjunction with standard Python control flow tools, -PyROS facilitates a "price of robustness" analysis for a model of interest -through the evaluation and comparison of the robust optimal -objective function value across any appropriately constructed hierarchy -of uncertainty sets. -In this example, we consider a sequence of -box uncertainty sets centered on the nominal uncertain -parameter realization, such that each box is parameterized -by a real value specifying a relative box size. -To this end, we construct an iterable called ``relative_deviation_list`` -whose entries are ``float`` values representing the relative sizes. -We then loop through ``relative_deviation_list`` so that for each relative -size, the corresponding robust optimal objective value -can be evaluated by creating an appropriate -:class:`~pyomo.contrib.pyros.uncertainty_sets.BoxSet` -instance and invoking the PyROS solver: - -.. code:: - - >>> # This takes a long time to run and therefore is not a doctest - >>> # === An array of maximum relative deviations from the nominal uncertain - >>> # parameter values to utilize in constructing box sets - >>> relative_deviation_list = [0.00, 0.10, 0.20, 0.30, 0.40] - >>> # === Final robust optimal objectives - >>> robust_optimal_objectives = [] - >>> for relative_deviation in relative_deviation_list: # doctest: +SKIP - ... bounds = [ - ... (nominal_values[i] - relative_deviation*nominal_values[i], - ... nominal_values[i] + relative_deviation*nominal_values[i]) - ... for i in range(4) - ... ] - ... box_uncertainty_set = pyros.BoxSet(bounds = bounds) - ... results = pyros_solver.solve( - ... model=m, - ... first_stage_variables=first_stage_variables, - ... second_stage_variables=second_stage_variables, - ... uncertain_params=uncertain_parameters, - ... uncertainty_set= box_uncertainty_set, - ... local_solver=local_solver, - ... global_solver=global_solver, - ... objective_focus=pyros.ObjectiveType.worst_case, - ... solve_master_globally=True, - ... decision_rule_order=1, - ... ) - ... is_robust_optimal = ( - ... results.pyros_termination_condition - ... == pyros.pyrosTerminationCondition.robust_optimal - ... ) - ... if not is_robust_optimal: - ... print(f"Instance for relative deviation: {relative_deviation} " - ... "not solved to robust optimality.") - ... robust_optimal_objectives.append("-----") - ... else: - ... robust_optimal_objectives.append(str(results.final_objective_value)) - -For this example, we obtain the following price of robustness results: - -.. table:: Price of robustness results. - - +------------------------------------------+------------------------------+-----------------------------+ - | Uncertainty Set Size (+/-) :sup:`o` | Robust Optimal Objective | % Increase :sup:`x` | - +==========================================+==============================+=============================+ - | 0.00 | 35,837,659.18 | 0.00 % | - +------------------------------------------+------------------------------+-----------------------------+ - | 0.10 | 36,135,182.66 | 0.83 % | - +------------------------------------------+------------------------------+-----------------------------+ - | 0.20 | 36,437,979.81 | 1.68 % | - +------------------------------------------+------------------------------+-----------------------------+ - | 0.30 | 43,478,190.91 | 21.32 % | - +------------------------------------------+------------------------------+-----------------------------+ - | 0.40 | ``robust_infeasible`` | :math:`\text{-----}` | - +------------------------------------------+------------------------------+-----------------------------+ - -Notice that PyROS was successfully able to determine the robust -infeasibility of the problem under the largest uncertainty set. - -:sup:`o` **Relative Deviation from Nominal Realization** - -:sup:`x` **Relative to Deterministic Optimal Objective** - -This example clearly illustrates the potential impact of the uncertainty -set size on the robust optimal objective function value -and demonstrates the ease of implementing a price of robustness study -for a given optimization problem under uncertainty. - -PyROS Solver Log Output -------------------------------- - -The PyROS solver log output is controlled through the optional -``progress_logger`` argument, itself cast to -a standard Python logger (:py:class:`logging.Logger`) object -at the outset of a :meth:`~pyomo.contrib.pyros.PyROS.solve` call. -The level of detail of the solver log output -can be adjusted by adjusting the level of the -logger object; see :ref:`the following table `. -Note that by default, ``progress_logger`` is cast to a logger of level -:py:obj:`logging.INFO`. - -We refer the reader to the -:doc:`official Python logging library documentation ` -for customization of Python logger objects; -for a basic tutorial, see the :doc:`logging HOWTO `. - -.. _table-logging-levels: - -.. list-table:: PyROS solver log output at the various standard Python :py:mod:`logging` levels. - :widths: 10 50 - :header-rows: 1 - - * - Logging Level - - Output Messages - * - :py:obj:`logging.ERROR` - - * Information on the subproblem for which an exception was raised - by a subordinate solver - * Details about failure of the PyROS coefficient matching routine - * - :py:obj:`logging.WARNING` - - * Information about a subproblem not solved to an acceptable status - by the user-provided subordinate optimizers - * Invocation of a backup solver for a particular subproblem - * Caution about solution robustness guarantees in event that - user passes ``bypass_global_separation=True`` - * - :py:obj:`logging.INFO` - - * PyROS version, author, and disclaimer information - * Summary of user options - * Breakdown of model component statistics - * Iteration log table - * Termination details: message, timing breakdown, summary of statistics - * - :py:obj:`logging.DEBUG` - - * Termination outcomes and summary of statistics for - every master feasility, master, and DR polishing problem - * Progress updates for the separation procedure - * Separation subproblem initial point infeasibilities - * Summary of separation loop outcomes: performance constraints - violated, uncertain parameter scenario added to the - master problem - * Uncertain parameter scenarios added to the master problem - thus far - -An example of an output log produced through the default PyROS -progress logger is shown in -:ref:`the snippet that follows `. -Observe that the log contains the following information: - - -* **Introductory information** (lines 1--18). - Includes the version number, author - information, (UTC) time at which the solver was invoked, - and, if available, information on the local Git branch and - commit hash. -* **Summary of solver options** (lines 19--38). -* **Preprocessing information** (lines 39--41). - Wall time required for preprocessing - the deterministic model and associated components, - i.e. standardizing model components and adding the decision rule - variables and equations. -* **Model component statistics** (lines 42--58). - Breakdown of model component statistics. - Includes components added by PyROS, such as the decision rule variables - and equations. -* **Iteration log table** (lines 59--69). - Summary information on the problem iterates and subproblem outcomes. - The constituent columns are defined in detail in - :ref:`the table following the snippet `. -* **Termination message** (lines 70--71). Very brief summary of the termination outcome. -* **Timing statistics** (lines 72--88). - Tabulated breakdown of the solver timing statistics, based on a - :class:`pyomo.common.timing.HierarchicalTimer` printout. - The identifiers are as follows: - - * ``main``: Total time elapsed by the solver. - * ``main.dr_polishing``: Total time elapsed by the subordinate solvers - on polishing of the decision rules. - * ``main.global_separation``: Total time elapsed by the subordinate solvers - on global separation subproblems. - * ``main.local_separation``: Total time elapsed by the subordinate solvers - on local separation subproblems. - * ``main.master``: Total time elapsed by the subordinate solvers on - the master problems. - * ``main.master_feasibility``: Total time elapsed by the subordinate solvers - on the master feasibility problems. - * ``main.preprocessing``: Total preprocessing time. - * ``main.other``: Total overhead time. - -* **Termination statistics** (lines 89--94). Summary of statistics related to the - iterate at which PyROS terminates. -* **Exit message** (lines 95--96). - - -.. _solver-log-snippet: - -.. code-block:: text - :caption: PyROS solver output log for the :ref:`two-stage problem example `. - :linenos: - - ============================================================================== - PyROS: The Pyomo Robust Optimization Solver, v1.2.11. - Pyomo version: 6.7.2 - Commit hash: unknown - Invoked at UTC 2024-03-28T00:00:00.000000 - - Developed by: Natalie M. Isenberg (1), Jason A. F. Sherman (1), - John D. Siirola (2), Chrysanthos E. Gounaris (1) - (1) Carnegie Mellon University, Department of Chemical Engineering - (2) Sandia National Laboratories, Center for Computing Research - - The developers gratefully acknowledge support from the U.S. Department - of Energy's Institute for the Design of Advanced Energy Systems (IDAES). - ============================================================================== - ================================= DISCLAIMER ================================= - PyROS is still under development. - Please provide feedback and/or report any issues by creating a ticket at - https://github.com/Pyomo/pyomo/issues/new/choose - ============================================================================== - Solver options: - time_limit=None - keepfiles=False - tee=False - load_solution=True - symbolic_solver_labels=False - objective_focus= - nominal_uncertain_param_vals=[0.13248000000000001, 4.97, 4.97, 1800] - decision_rule_order=1 - solve_master_globally=True - max_iter=-1 - robust_feasibility_tolerance=0.0001 - separation_priority_order={} - progress_logger= - backup_local_solvers=[] - backup_global_solvers=[] - subproblem_file_directory=None - bypass_local_separation=False - bypass_global_separation=False - p_robustness={} - ------------------------------------------------------------------------------ - Preprocessing... - Done preprocessing; required wall time of 0.175s. - ------------------------------------------------------------------------------ - Model statistics: - Number of variables : 62 - Epigraph variable : 1 - First-stage variables : 7 - Second-stage variables : 6 - State variables : 18 - Decision rule variables : 30 - Number of uncertain parameters : 4 - Number of constraints : 81 - Equality constraints : 24 - Coefficient matching constraints : 0 - Decision rule equations : 6 - All other equality constraints : 18 - Inequality constraints : 57 - First-stage inequalities (incl. certain var bounds) : 10 - Performance constraints (incl. var bounds) : 47 - ------------------------------------------------------------------------------ - Itn Objective 1-Stg Shift 2-Stg Shift #CViol Max Viol Wall Time (s) - ------------------------------------------------------------------------------ - 0 3.5838e+07 - - 5 1.8832e+04 1.741 - 1 3.5838e+07 3.5184e-15 3.9404e-15 10 4.2516e+06 3.766 - 2 3.5993e+07 1.8105e-01 7.1406e-01 13 5.2004e+06 6.288 - 3 3.6285e+07 5.1968e-01 7.7753e-01 4 1.7892e+04 8.247 - 4 3.6285e+07 9.1166e-13 1.9702e-15 0 7.1157e-10g 11.456 - ------------------------------------------------------------------------------ - Robust optimal solution identified. - ------------------------------------------------------------------------------ - Timing breakdown: - - Identifier ncalls cumtime percall % - ----------------------------------------------------------- - main 1 11.457 11.457 100.0 - ------------------------------------------------------ - dr_polishing 4 0.682 0.171 6.0 - global_separation 47 1.109 0.024 9.7 - local_separation 235 5.810 0.025 50.7 - master 5 1.353 0.271 11.8 - master_feasibility 4 0.247 0.062 2.2 - preprocessing 1 0.429 0.429 3.7 - other n/a 1.828 n/a 16.0 - ====================================================== - =========================================================== - - ------------------------------------------------------------------------------ - Termination stats: - Iterations : 5 - Solve time (wall s) : 11.457 - Final objective value : 3.6285e+07 - Termination condition : pyrosTerminationCondition.robust_optimal - ------------------------------------------------------------------------------ - All done. Exiting PyROS. - ============================================================================== - - -The iteration log table is designed to provide, in a concise manner, -important information about the progress of the iterative algorithm for -the problem of interest. -The constituent columns are defined in the -:ref:`table that follows `. - -.. _table-iteration-log-columns: - -.. list-table:: PyROS iteration log table columns. - :widths: 10 50 - :header-rows: 1 - - * - Column Name - - Definition - * - Itn - - Iteration number. - * - Objective - - Master solution objective function value. - If the objective of the deterministic model provided - has a maximization sense, - then the negative of the objective function value is displayed. - Expect this value to trend upward as the iteration number - increases. - If the master problems are solved globally - (by passing ``solve_master_globally=True``), - then after the iteration number exceeds the number of uncertain parameters, - this value should be monotonically nondecreasing - as the iteration number is increased. - A dash ("-") is produced in lieu of a value if the master - problem of the current iteration is not solved successfully. - * - 1-Stg Shift - - Infinity norm of the relative difference between the first-stage - variable vectors of the master solutions of the current - and previous iterations. Expect this value to trend - downward as the iteration number increases. - A dash ("-") is produced in lieu of a value - if the current iteration number is 0, - there are no first-stage variables, - or the master problem of the current iteration is not solved successfully. - * - 2-Stg Shift - - Infinity norm of the relative difference between the second-stage - variable vectors (evaluated subject to the nominal uncertain - parameter realization) of the master solutions of the current - and previous iterations. Expect this value to trend - downward as the iteration number increases. - A dash ("-") is produced in lieu of a value - if the current iteration number is 0, - there are no second-stage variables, - or the master problem of the current iteration is not solved successfully. - * - #CViol - - Number of performance constraints found to be violated during - the separation step of the current iteration. - Unless a custom prioritization of the model's performance constraints - is specified (through the ``separation_priority_order`` argument), - expect this number to trend downward as the iteration number increases. - A "+" is appended if not all of the separation problems - were solved successfully, either due to custom prioritization, a time out, - or an issue encountered by the subordinate optimizers. - A dash ("-") is produced in lieu of a value if the separation - routine is not invoked during the current iteration. - * - Max Viol - - Maximum scaled performance constraint violation. - Expect this value to trend downward as the iteration number increases. - A 'g' is appended to the value if the separation problems were solved - globally during the current iteration. - A dash ("-") is produced in lieu of a value if the separation - routine is not invoked during the current iteration, or if there are - no performance constraints. - * - Wall time (s) - - Total time elapsed by the solver, in seconds, up to the end of the - current iteration. - - -Feedback and Reporting Issues -------------------------------- -Please provide feedback and/or report any problems by opening an issue on -the `Pyomo GitHub page `_. diff --git a/doc/Archive/contributed_packages/satsolver.rst b/doc/Archive/contributed_packages/satsolver.rst deleted file mode 100644 index 4cac9170b55..00000000000 --- a/doc/Archive/contributed_packages/satsolver.rst +++ /dev/null @@ -1,34 +0,0 @@ -z3 SMT Sat Solver Interface -=========================== - -The z3 Satisfiability Solver interface can convert pyomo variables and expressions for -use with the z3 Satisfiability Solver - -Installation ------------- -z3 is required for use of the Sat Solver can be installed via the command - -.. code:: - - pip install z3-solver - -Using z3 Sat Solver -------------------- -To use the sat solver define your pyomo model as usual: - -.. doctest:: - - Required import - >>> from pyomo.environ import * - >>> from pyomo.contrib.satsolver.satsolver import SMTSatSolver - - Create a simple model - >>> m = ConcreteModel() - >>> m.x = Var() - >>> m.y = Var() - >>> m.obj = Objective(expr=m.x**2 + m.y**2) - >>> m.c = Constraint(expr=m.y >= -2*m.x + 5) - - Invoke the sat solver using optional argument model to automatically process - pyomo model - >>> is_feasible = SMTSatSolver(model = m).check()# doctest: +SKIP diff --git a/doc/Archive/contributed_packages/sensitivity_toolbox.rst b/doc/Archive/contributed_packages/sensitivity_toolbox.rst deleted file mode 100644 index 2a2ccff4b09..00000000000 --- a/doc/Archive/contributed_packages/sensitivity_toolbox.rst +++ /dev/null @@ -1,185 +0,0 @@ -Sensitivity Toolbox -=================== - -The sensitivity toolbox provides a Pyomo interface to sIPOPT and k_aug to very quickly compute approximate solutions to nonlinear programs with a small perturbation in model parameters. - -See the `sIPOPT documentation `_ or the `following paper `_ for additional details: - - H. Pirnay, R. Lopez-Negrete, and L.T. Biegler, Optimal Sensitivity based on IPOPT, Math. Prog. Comp., 4(4):307--331, 2012. - -The details of `k_aug` can be found in the following link: - - David Thierry (2020). k_aug, https://github.com/dthierry/k_aug - -Using the Sensitivity Toolbox ------------------------------ - -We will start with a motivating example: - -.. math:: - \begin{align*} - \min_{x_1,x_2,x_3} \quad & x_1^2 + x_2^2 + x_3^2 \\ - \mathrm{s.t.} \qquad & 6 x_1 + 3 x_2 + 2 x_3 - p_1 = 0 \\ - & p_2 x_1 + x_2 - x_3 - 1 = 0 \\ - & x_1, x_2, x_3 \geq 0 - \end{align*} - -Here :math:`x_1`, :math:`x_2`, and :math:`x_3` are the decision variables while :math:`p_1` and :math:`p_2` are parameters. At first, let's consider :math:`p_1 = 4.5` and :math:`p_2 = 1.0`. Below is the model implemented in Pyomo. - -.. doctest:: python - - # Import Pyomo and the sensitivity toolbox - >>> from pyomo.environ import * - >>> from pyomo.contrib.sensitivity_toolbox.sens import sensitivity_calculation - - # Create a concrete model - >>> m = ConcreteModel() - - # Define the variables with bounds and initial values - >>> m.x1 = Var(initialize = 0.15, within=NonNegativeReals) - >>> m.x2 = Var(initialize = 0.15, within=NonNegativeReals) - >>> m.x3 = Var(initialize = 0.0, within=NonNegativeReals) - - # Define the parameters - >>> m.eta1 = Param(initialize=4.5,mutable=True) - >>> m.eta2 = Param(initialize=1.0,mutable=True) - - # Define the constraints and objective - >>> m.const1 = Constraint(expr=6*m.x1+3*m.x2+2*m.x3-m.eta1 ==0) - >>> m.const2 = Constraint(expr=m.eta2*m.x1+m.x2-m.x3-1 ==0) - >>> m.cost = Objective(expr=m.x1**2+m.x2**2+m.x3**2) - - -The solution of this optimization problem is :math:`x_1^* = 0.5`, :math:`x_2^* = 0.5`, and :math:`x_3^* = 0.0`. But what if we change the parameter values to :math:`\hat{p}_1 = 4.0` and :math:`\hat{p}_2 = 1.0`? Is there a quick way to approximate the new solution :math:`\hat{x}_1^*`, :math:`\hat{x}_2^*`, and :math:`\hat{x}_3^*`? Yes! This is the main functionality of sIPOPT and k_aug. - -Next we define the perturbed parameter values :math:`\hat{p}_1` and :math:`\hat{p}_2`: - -.. doctest:: python - - >>> m.perturbed_eta1 = Param(initialize = 4.0) - >>> m.perturbed_eta2 = Param(initialize = 1.0) - -And finally we call sIPOPT or k_aug: - -.. doctest:: python - :skipif: not sipopt_available or not k_aug_available or not dot_sens_available - - >>> m_sipopt = sensitivity_calculation('sipopt', m, [m.eta1, m.eta2], [m.perturbed_eta1, m.perturbed_eta2], tee=False) - >>> m_kaug_dsdp = sensitivity_calculation('k_aug', m, [m.eta1, m.eta2], [m.perturbed_eta1, m.perturbed_eta2], tee=False) - -The first argument specifies the method, either 'sipopt' or 'k_aug'. The second argument is the Pyomo model. The third argument is a list of the original parameters. The fourth argument is a list of the perturbed parameters. It's important that these two lists are the same length and in the same order. - -First, we can inspect the initial point: - -.. doctest:: python - :skipif: not sipopt_available or not k_aug_available or not dot_sens_available - - >>> print("eta1 = %0.3f" % m.eta1()) - eta1 = 4.500 - - >>> print("eta2 = %0.3f" % m.eta2()) - eta2 = 1.000 - - # Initial point (not feasible): - >>> print("Objective = %0.3f" % m.cost()) - Objective = 0.045 - - >>> print("x1 = %0.3f" % m.x1()) - x1 = 0.150 - - >>> print("x2 = %0.3f" % m.x2()) - x2 = 0.150 - - >>> print("x3 = %0.3f" % m.x3()) - x3 = 0.000 - -Next, we inspect the solution :math:`x_1^*`, :math:`x_2^*`, and :math:`x_3^*`: - -.. doctest:: python - :skipif: not sipopt_available or not k_aug_available or not dot_sens_available - - # Solution with the original parameter values: - >>> print("Objective = %0.3f" % m_sipopt.cost()) - Objective = 0.500 - - >>> print("x1 = %0.3f" % m_sipopt.x1()) - x1 = 0.500 - - >>> print("x2 = %0.3f" % m_sipopt.x2()) - x2 = 0.500 - - >>> print("x3 = %0.3f" % m_sipopt.x3()) - x3 = 0.000 - -Note that k_aug does not save the solution with the original parameter values. Finally, we inspect the approximate solution :math:`\hat{x}_1^*`, :math:`\hat{x}_2^*`, and :math:`\hat{x}_3^*`: - -.. doctest:: python - :skipif: not sipopt_available or not k_aug_available or not dot_sens_available - - # *sIPOPT* - # New parameter values: - >>> print("eta1 = %0.3f" %m_sipopt.perturbed_eta1()) - eta1 = 4.000 - - >>> print("eta2 = %0.3f" % m_sipopt.perturbed_eta2()) - eta2 = 1.000 - - # (Approximate) solution with the new parameter values: - >>> x1 = m_sipopt.sens_sol_state_1[m_sipopt.x1] - >>> x2 = m_sipopt.sens_sol_state_1[m_sipopt.x2] - >>> x3 = m_sipopt.sens_sol_state_1[m_sipopt.x3] - >>> print("Objective = %0.3f" % (x1**2 + x2**2 + x3**2)) - Objective = 0.556 - - >>> print("x1 = %0.3f" % x1) - x1 = 0.333 - - >>> print("x2 = %0.3f" % x2) - x2 = 0.667 - - >>> print("x3 = %0.3f" % x3) - x3 = -0.000 - - # *k_aug* - # New parameter values: - >>> print("eta1 = %0.3f" %m_kaug_dsdp.perturbed_eta1()) - eta1 = 4.000 - - >>> print("eta2 = %0.3f" % m_kaug_dsdp.perturbed_eta2()) - eta2 = 1.000 - - # (Approximate) solution with the new parameter values: - >>> x1 = m_kaug_dsdp.x1() - >>> x2 = m_kaug_dsdp.x2() - >>> x3 = m_kaug_dsdp.x3() - >>> print("Objective = %0.3f" % (x1**2 + x2**2 + x3**2)) - Objective = 0.556 - - >>> print("x1 = %0.3f" % x1) - x1 = 0.333 - - >>> print("x2 = %0.3f" % x2) - x2 = 0.667 - - >>> print("x3 = %0.3f" % x3) - x3 = -0.000 - - -Installing sIPOPT and k_aug ---------------------------- - -The sensitivity toolbox requires either sIPOPT or k_aug to be installed and available in your system PATH. See the sIPOPT and k_aug documentation for detailed instructions: - -* https://coin-or.github.io/Ipopt/INSTALL.html -* https://projects.coin-or.org/Ipopt/wiki/sIpopt -* https://coin-or.github.io/coinbrew/ -* https://github.com/dthierry/k_aug - -.. note:: - If you get an error that ``ipopt_sens`` or ``k_aug`` and ``dot_sens`` cannot be found, double check your installation and make sure the build directories containing the executables were added to your system PATH. - - -Sensitivity Toolbox Interface ------------------------------ - -.. autofunction:: pyomo.contrib.sensitivity_toolbox.sens.sensitivity_calculation diff --git a/doc/Archive/contributed_packages/trustregion.rst b/doc/Archive/contributed_packages/trustregion.rst deleted file mode 100644 index f477c905e33..00000000000 --- a/doc/Archive/contributed_packages/trustregion.rst +++ /dev/null @@ -1,189 +0,0 @@ -#################################### -Trust Region Framework Method Solver -#################################### - -The Trust Region Framework (TRF) method solver allows users to solve hybrid -glass box/black box optimization problems in which parts of the system are -modeled with open, equation-based models and parts of the system are black -boxes. This method utilizes surrogate models that substitute high-fidelity -models with low-fidelity basis functions, thus avoiding the direct implementation -of the large, computationally expensive high-fidelity models. This is done -iteratively, resulting in fewer calls to the computationally expensive functions. - -This module implements the method from Yoshio & Biegler -[`Yoshio & Biegler, 2021`_] and represents a rewrite of the original 2018 -implementation of the algorithm from Eason & Biegler [`Eason & Biegler, 2018`_]. - -In the context of this updated module, black box functions are implemented as -Pyomo External Functions. - -This work was conducted as part of the Institute for the Design of Advanced -Energy Systems (`IDAES `_) with support through the -Simulation-Based Engineering, Crosscutting Research Program within the U.S. -Department of Energy’s Office of Fossil Energy and Carbon Management. - -.. _Eason & Biegler, 2018: https://doi.org/10.1002/aic.16364 -.. _Yoshio & Biegler, 2021: https://doi.org/10.1002/aic.17054 - -Methodology Overview ---------------------- - -The formulation of the original hybrid problem is: - -.. math:: - \begin{align*} - \displaystyle \min_{} & ~~ f\left(z, w, d\left(w\right)\right) & \\ - \displaystyle \text{s.t.} \quad \: & ~~ h\left(z, w, d\left(w\right)\right) = 0 \\ - \displaystyle & ~~ g\left(z, w, d\left(w\right)\right) \leq 0 - \end{align*} - -where: - -* :math:`w \in \mathbb{R}^m` are the inputs to the external functions -* :math:`z \in \mathbb{R}^n` are the remaining decision variables (i.e., degrees of freedom) -* :math:`d(w) : \mathbb{R}^m \to \mathbb{R}^p` are the outputs of the external functions as a function of :math:`w` -* :math:`f`, `h`, `g`, `d` are all assumed to be twice continuously differentiable - -This formulation is reworked to separate all external function information as -follows to enable the usage of the trust region method: - -.. math:: - \begin{align*} - \displaystyle \min_{x} & ~~ f\left(x\right) & \\ - \displaystyle \text{s.t.} \quad \: & ~~ h\left(x\right) = 0 \\ - \displaystyle & ~~ g\left(x\right) \leq 0 \\ - \displaystyle & ~~ y = d\left(w\right) - \end{align*} - -where: - -* :math:`y \in \mathbb{R}^p` are the outputs of the external functions -* :math:`x^T = [w^T, y^T, z^T]` is a set of all inputs and outputs - -Using this formulation and a user-supplied low-fidelity/ideal model basis function -:math:`b\left(w\right)`, the algorithm iteratively solves subproblems using -the surrogate model: - -.. math:: - \begin{align*} - r_k\left(w\right) = b\left(w\right) + \left( d\left(w_k\right) - b\left(w_k\right) \right) + \left( \nabla d\left(w_k\right) - \nabla b\left(w_k\right) \right)^T \left( w - w_k \right) - \end{align*} - -This acts similarly to Newton's method in that small, incremental steps are taken -towards an optimal solution. At each iteration, the current solution of the -subproblem is compared to the previous solution to ensure that -the iteration has moved in a direction towards an optimal solution. If not true, -the step is rejected. If true, the step is accepted and the surrogate -model is updated for the next iteration. - -When using TRF, please consider citing the above papers. - -TRF Inputs ------------ - -The required inputs to the TRF -:py:meth:`solve ` -method are the following: - -* The optimization model -* List of degree of freedom variables within the model - -The optional input to the TRF -:py:meth:`solve ` -method is the following: - -* The external function surrogate model rule ("basis function") - - -TRF Solver Interface ---------------------- - -.. note:: - The keyword arguments can be updated at solver instantiation or later when the ``solve`` method is called. - -.. autoclass:: pyomo.contrib.trustregion.TRF.TrustRegionSolver - :members: solve - -TRF Usage Example ------------------- -Two examples can be found in the examples_ subdirectory. One of them is -implemented below. - -.. _examples: https://github.com/Pyomo/pyomo/tree/main/pyomo/contrib/trustregion/examples - -Step 0: Import Pyomo -^^^^^^^^^^^^^^^^^^^^^ - -.. doctest:: - - >>> # === Required imports === - >>> import pyomo.environ as pyo - -Step 1: Define the external function and its gradient -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. doctest:: - - >>> # === Define a 'black box' function and its gradient === - >>> def ext_fcn(a, b): - ... return pyo.sin(a - b) - >>> def grad_ext_fcn(args, fixed): - ... a, b = args[:2] - ... return [ pyo.cos(a - b), -pyo.cos(a - b) ] - -Step 2: Create the model -^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. doctest:: - - >>> # === Construct the Pyomo model object === - >>> def create_model(): - ... m = pyo.ConcreteModel() - ... m.name = 'Example 1: Eason' - ... m.z = pyo.Var(range(3), domain=pyo.Reals, initialize=2.) - ... m.x = pyo.Var(range(2), initialize=2.) - ... m.x[1] = 1.0 - ... - ... m.ext_fcn = pyo.ExternalFunction(ext_fcn, grad_ext_fcn) - ... - ... m.obj = pyo.Objective( - ... expr=(m.z[0]-1.0)**2 + (m.z[0]-m.z[1])**2 + (m.z[2]-1.0)**2 \ - ... + (m.x[0]-1.0)**4 + (m.x[1]-1.0)**6 - ... ) - ... - ... m.c1 = pyo.Constraint( - ... expr=m.x[0] * m.z[0]**2 + m.ext_fcn(m.x[0], m.x[1]) == 2*pyo.sqrt(2.0) - ... ) - ... m.c2 = pyo.Constraint(expr=m.z[2]**4 * m.z[1]**2 + m.z[1] == 8+pyo.sqrt(2.0)) - ... return m - >>> model = create_model() - -Step 3: Solve with TRF -^^^^^^^^^^^^^^^^^^^^^^^ - -.. note:: - Reminder from earlier that the ``solve`` method requires the user pass the model and a list of variables - which represent the degrees of freedom in the model. The user may also pass - a low-fidelity/ideal model (or "basis function") to this method to improve - convergence. - -.. doctest:: - :skipif: not ipopt_available - - >>> # === Instantiate the TRF solver object === - >>> trf_solver = pyo.SolverFactory('trustregion') - >>> # === Solve with TRF === - >>> result = trf_solver.solve(model, [model.z[0], model.z[1], model.z[2]]) - EXIT: Optimal solution found. - ... - -The :py:meth:`solve ` -method returns a clone of the original model which has been run -through TRF algorithm, thus leaving the original model intact. - - -.. warning:: - - TRF is still under a beta release. Please provide feedback and/or - report any problems by opening an issue on the Pyomo - `GitHub page `_. diff --git a/doc/Archive/contribution_guide.rst b/doc/Archive/contribution_guide.rst deleted file mode 100644 index 9ad5bdfee0e..00000000000 --- a/doc/Archive/contribution_guide.rst +++ /dev/null @@ -1,431 +0,0 @@ -Contributing to Pyomo -===================== - -We welcome all contributions including bug fixes, feature enhancements, -and documentation improvements. Pyomo manages source code contributions -via GitHub pull requests (PRs). - -Contribution Requirements -------------------------- - -A PR should be 1 set of related changes. PRs for large-scale -non-functional changes (i.e. PEP8, comments) should be -separated from functional changes. This simplifies the review process -and ensures that functional changes aren't obscured by large amounts of -non-functional changes. - -We do not squash and merge PRs so all commits in your branch will appear -in the main history. In addition to well-documented PR descriptions, -we encourage modular/targeted commits with descriptive commit messages. - -Coding Standards -++++++++++++++++ - - * Required: `black `_ - * No use of ``__author__`` - * Inside ``pyomo.contrib``: Contact information for the contribution - maintainer (such as a Github ID) should be included in the Sphinx - documentation - -The first step of Pyomo's GitHub Actions workflow is to run -`black `_ and a -`spell-checker `_ to ensure style -guide compliance and minimize typos. Before opening a pull request, please -run: - -:: - - # Auto-apply correct formatting - pip install black - black -S -C --exclude examples/pyomobook/python-ch/BadIndent.py - # Find typos in files - conda install typos - typos --config .github/workflows/typos.toml - -If the spell-checker returns a failure for a word that is spelled correctly, -please add the word to the ``.github/workflows/typos.toml`` file. - -Online Pyomo documentation is generated using `Sphinx `_ -with the ``napoleon`` extension enabled. For API documentation we use of one of these -`supported styles for docstrings `_, -but we prefer the NumPy standard. Whichever you choose, we require compliant docstrings for: - - * Modules - * Public and Private Classes - * Public and Private Functions - -We also encourage you to include examples, especially for new features -and contributions to ``pyomo.contrib``. - -Testing -+++++++ - -Pyomo uses `unittest `_, -`pytest `_, -`GitHub Actions `_, -and Jenkins -for testing and continuous integration. Submitted code should include -tests to establish the validity of its results and/or effects. Unit -tests are preferred but we also accept integration tests. We require -at least 70% coverage of the lines modified in the PR and prefer coverage -closer to 90%. We also require that all tests pass before a PR will be -merged. - -.. note:: - If you are having issues getting tests to pass on your Pull Request, - please tag any of the core developers to ask for help. - -The Pyomo main branch provides a Github Actions workflow (configured -in the ``.github/`` directory) that will test any changes pushed to -a branch with a subset of the complete test harness that includes -multiple virtual machines (``ubuntu``, ``mac-os``, ``windows``) -and multiple Python versions. For existing forks, fetch and merge -your fork (and branches) with Pyomo's main. For new forks, you will -need to enable GitHub Actions in the 'Actions' tab on your fork. -This will enable the tests to run automatically with each push to your fork. - -At any point in the development cycle, a "work in progress" pull request -may be opened by including '[WIP]' at the beginning of the PR -title. Any pull requests marked '[WIP]' or draft will not be -reviewed or merged by the core development team. However, any -'[WIP]' pull request left open for an extended period of time without -active development may be marked 'stale' and closed. - -.. note:: - Draft and WIP Pull Requests will **NOT** trigger tests. This is an effort to - reduce our CI backlog. Please make use of the provided - branch test suite for evaluating / testing draft functionality. - -Python Version Support -++++++++++++++++++++++ - -By policy, Pyomo supports and tests the currently supported Python versions, -as can be seen on `Status of Python Versions `_. -It is expected that tests will pass for all of the supported and tested -versions of Python, unless otherwise stated. - -At the time of the first Pyomo release after the end-of-life of a minor Python -version, we will remove testing and support for that Python version. - -This will also result in a bump in the minor Pyomo version. - -For example, assume Python 3.A is declared end-of-life while Pyomo is on -version 6.3.Y. After the release of Pyomo 6.3.(Y+1), Python 3.A will be removed, -and the next Pyomo release will be 6.4.0. - -Working on Forks and Branches ------------------------------ - -All Pyomo development should be done on forks of the Pyomo -repository. In order to fork the Pyomo repository, visit -https://github.com/Pyomo/pyomo, click the "Fork" button in the -upper right corner, and follow the instructions. - -This section discusses two recommended workflows for contributing -pull-requests to Pyomo. The first workflow, labeled -:ref:`Working with my fork and the GitHub Online UI `, -does not require the use of 'remotes', and -suggests updating your fork using the GitHub online UI. The second -workflow, labeled -:ref:`Working with remotes and the git command-line `, outlines -a process that defines separate remotes for your fork and the main -Pyomo repository. - -More information on git can be found at -https://git-scm.com/book/en/v2. Section 2.5 has information on working -with remotes. - - -.. _forksgithubui: - -Working with my fork and the GitHub Online UI -+++++++++++++++++++++++++++++++++++++++++++++ - -After creating your fork (per the instructions above), you can -then clone your fork of the repository with - -:: - - git clone https://github.com//pyomo.git - -For new development, we strongly recommend working on feature -branches. When you have a new feature to implement, create -the branch with the following. - -:: - - cd pyomo/ # to make sure you are in the folder managed by git - git branch - git checkout - -Development can now be performed. When you are ready, commit -any changes you make to your local repository. This can be -done multiple times with informative commit messages for -different tasks in the feature development. - -:: - - git add - git status # to check that you have added the correct files - git commit -m 'informative commit message to describe changes' - -In order to push the changes in your local branch to a branch on your fork, use - -:: - - git push origin - - -When you have completed all the changes and are ready for a pull request, make -sure all the changes have been pushed to the branch on your fork. - - * visit https://github.com//pyomo. - * Just above the list of files and directories in the repository, - you should see a button that says "Branch: main". Click on - this button, and choose the correct branch. - * Click the "New pull request" button just to the right of the - "Branch: " button. - * Fill out the pull request template and click the green "Create - pull request" button. - -At times during your development, you may want to merge changes from -the Pyomo main development branch into the feature branch on your -fork and in your local clone of the repository. - -Using GitHub UI to merge Pyomo main into a branch on your fork -**************************************************************** - -To update your fork, you will actually be merging a pull-request from -the head Pyomo repository into your fork. - - * Visit https://github.com/Pyomo/pyomo. - * Click on the "New pull request" button just above the list of - files and directories. - * You will see the title "Compare changes" with some small text - below it which says "Compare changes across branches, commits, - tags, and more below. If you need to, you can also compare - across forks." Click the last part of this: "compare across - forks". - * You should now see four buttons just below this: "base - repository: Pyomo/pyomo", "base: main", "head repository: - Pyomo/pyomo", and "compare: main". Click the leftmost button - and choose "/Pyomo". - * Then click the button which is second to the left, and choose - the branch which you want to merge Pyomo main into. The four - buttons should now read: "base repository: /pyomo", - "base: ", "head repository: Pyomo/pyomo", and - "compare: main". This is setting you up to merge a pull-request - from Pyomo's main branch into your fork's branch. - * You should also now see a pull request template. If you fill out - the pull request template and click "Create pull request", this - will create a pull request which will update your fork and - branch with any changes that have been made to the main branch - of Pyomo. - * You can then merge the pull request by clicking the green "Merge - pull request" button from your fork on GitHub. - -.. _forksremotes: - -Working with remotes and the git command-line -+++++++++++++++++++++++++++++++++++++++++++++ - -After you have created your fork, you can clone the fork and setup -git 'remotes' that allow you to merge changes from (and to) different -remote repositories. Below, we have included a set of recommendations, -but, of course, there are other valid GitHub workflows that you can -adopt. - -The following commands show how to clone your fork and setup -two remotes, one for your fork, and one for the head Pyomo repository. - -:: - - git clone https://github.com//pyomo.git - git remote rename origin my-fork - git remote add head-pyomo https://github.com/pyomo/pyomo.git - -Note, you can see a list of your remotes with - -:: - - git remote -v - -The commands for creating a local branch and performing local commits -are the same as those listed in the previous section above. Below are -some common tasks based on this multi-remote setup. - -If you have changes that have been committed to a local feature branch -(), you can push these changes to the branch on your fork -with, - -:: - - git push my-fork - -In order to update a local branch with changes from a branch of the -Pyomo repository, - -:: - - git checkout - git fetch head-pyomo - git merge head-pyomo/ --ff-only - -The "--ff-only" only allows a merge if the merge can be done by a -fast-forward. If you do not require a fast-forward, you can drop this -option. The most common concrete example of this would be - -:: - - git checkout main - git fetch head-pyomo - git merge head-pyomo/main --ff-only - -The above commands pull changes from the main branch of the head -Pyomo repository into the main branch of your local clone. To push -these changes to the main branch on your fork, - -:: - - git push my-fork main - - -Setting up your development environment -+++++++++++++++++++++++++++++++++++++++ - -After cloning your fork, you will want to install Pyomo from source. - -Step 1 (recommended): Create a new ``conda`` environment. - -:: - - conda create --name pyomodev - -You may change the environment name from ``pyomodev`` as you see fit. -Then activate the environment: - -:: - - conda activate pyomodev - -Step 2 (optional): Install PyUtilib - -The hard dependency on PyUtilib was removed in Pyomo 6.0.0. There is still a -soft dependency for any code related to ``pyomo.dataportal.plugins.sheet``. - -If your contribution requires PyUtilib, you will likely need the main branch of -PyUtilib to contribute. Clone a copy of the repository in a new directory: - -:: - - git clone https://github.com/PyUtilib/pyutilib - -Then in the directory containing the clone of PyUtilib run: - -:: - - python setup.py develop - -Step 3: Install Pyomo - -Finally, move to the directory containing the clone of your Pyomo fork and run: - -:: - - python setup.py develop - -These commands register the cloned code with the active python environment -(``pyomodev``). This way, your changes to the source code for ``pyomo`` are -automatically used by the active environment. You can create another conda -environment to switch to alternate versions of pyomo (e.g., stable). - -Review Process --------------- - -After a PR is opened it will be reviewed by at least two members of the -core development team. The core development team consists of anyone with -write-access to the Pyomo repository. Pull requests opened by a core -developer only require one review. The reviewers will decide if they -think a PR should be merged or if more changes are necessary. - -Reviewers look for: - - * Outside of ``pyomo.contrib``: Code rigor and standards, edge cases, - side effects, etc. - * Inside of ``pyomo.contrib``: No “glaringly obvious” problems with - the code - * Documentation and tests - -The core development team tries to review pull requests in a timely -manner but we make no guarantees on review timeframes. In addition, PRs -might not be reviewed in the order they are opened in. - -Where to put contributed code ------------------------------ - -In order to contribute to Pyomo, you must first make a fork of the Pyomo -git repository. Next, you should create a branch on your fork dedicated -to the development of the new feature or bug fix you're interested -in. Once you have this branch checked out, you can start coding. Bug -fixes and minor enhancements to existing Pyomo functionality should be -made in the appropriate files in the Pyomo code base. New examples, -features, and packages built on Pyomo should be placed in -``pyomo.contrib``. Follow the link below to find out if -``pyomo.contrib`` is right for your code. - -``pyomo.contrib`` ------------------ - -Pyomo uses the ``pyomo.contrib`` package to facilitate the inclusion -of third-party contributions that enhance Pyomo's core functionality. -The are two ways that ``pyomo.contrib`` can be used to integrate -third-party packages: - -* ``pyomo.contrib`` can provide wrappers for separate Python packages, thereby - allowing these packages to be imported as subpackages of pyomo. - -* ``pyomo.contrib`` can include contributed packages that are developed and - maintained outside of the Pyomo developer team. - -Including contrib packages in the Pyomo source tree provides a -convenient mechanism for defining new functionality that can be -optionally deployed by users. We expect this mechanism to include -Pyomo extensions and experimental modeling capabilities. However, -contrib packages are treated as optional packages, which are not -maintained by the Pyomo developer team. Thus, it is the responsibility -of the code contributor to keep these packages up-to-date. - -Contrib package contributions will be considered as pull-requests, -which will be reviewed by the Pyomo developer team. Specifically, -this review will consider the suitability of the proposed capability, -whether tests are available to check the execution of the code, and -whether documentation is available to describe the capability. -Contrib packages will be tested along with Pyomo. If test failures -arise, then these packages will be disabled and an issue will be -created to resolve these test failures. - -Contrib Packages within Pyomo -+++++++++++++++++++++++++++++ - -Third-party contributions can be included directly within the -``pyomo.contrib`` package. The ``pyomo/contrib/example`` package -provides an example of how this can be done, including a directory -for plugins and package tests. For example, this package can be -imported as a subpackage of ``pyomo.contrib``:: - - from pyomo.environ import * - from pyomo.contrib.example import a - - # Print the value of 'a' defined by this package - print(a) - -Although ``pyomo.contrib.example`` is included in the Pyomo source -tree, it is treated as an optional package. Pyomo will attempt to -import this package, but if an import failure occurs, Pyomo will -silently ignore it. Otherwise, this pyomo package will be treated -like any other. Specifically: - -* Plugin classes defined in this package are loaded when ``pyomo.environ`` is loaded. - -* Tests in this package are run with other Pyomo tests. - diff --git a/doc/Archive/developer_reference/config.rst b/doc/Archive/developer_reference/config.rst deleted file mode 100644 index 23d0696ee98..00000000000 --- a/doc/Archive/developer_reference/config.rst +++ /dev/null @@ -1,3 +0,0 @@ - -.. automodule:: pyomo.common.config - :noindex: diff --git a/doc/Archive/developer_reference/deprecation.rst b/doc/Archive/developer_reference/deprecation.rst deleted file mode 100644 index 7fc5ec2b0ff..00000000000 --- a/doc/Archive/developer_reference/deprecation.rst +++ /dev/null @@ -1,62 +0,0 @@ -Deprecation and Removal of Functionality -======================================== - -During the course of development, there may be cases where it becomes -necessary to deprecate or remove functionality from the standard Pyomo -offering. - -Deprecation ------------ - -We offer a set of tools to help with deprecation in -``pyomo.common.deprecation``. - -By policy, when deprecating or moving an existing capability, one of the -following utilities should be leveraged. Each has a required -``version`` argument that should be set to current development version (e.g., -``"6.6.2.dev0"``). This version will be updated to the next actual -release as part of the Pyomo release process. The current development version -can be found by running ``pyomo --version`` on your local fork/branch. - -.. currentmodule:: pyomo.common.deprecation - -.. autosummary:: - - deprecated - deprecation_warning - relocated_module - relocated_module_attribute - RenamedClass - -.. autodecorator:: pyomo.common.deprecation.deprecated - :noindex: - -.. autofunction:: pyomo.common.deprecation.deprecation_warning - :noindex: - -.. autofunction:: pyomo.common.deprecation.relocated_module - :noindex: - -.. autofunction:: pyomo.common.deprecation.relocated_module_attribute - :noindex: - -.. autoclass:: pyomo.common.deprecation.RenamedClass - :noindex: - - -Removal -------- - -By policy, functionality should be deprecated with reasonable -warning, pending extenuating circumstances. The functionality should -be deprecated, following the information above. - -If the functionality is documented in the most recent -edition of [`Pyomo - Optimization Modeling in Python`_], it may not be removed -until the next major version release. - -.. _Pyomo - Optimization Modeling in Python: https://doi.org/10.1007/978-3-030-68928-5 - -For other functionality, it is preferred that ample time is given -before removing the functionality. At minimum, significant functionality -removal will result in a minor version bump. diff --git a/doc/Archive/developer_reference/expressions/design.rst b/doc/Archive/developer_reference/expressions/design.rst deleted file mode 100644 index ddecb39ad0c..00000000000 --- a/doc/Archive/developer_reference/expressions/design.rst +++ /dev/null @@ -1,268 +0,0 @@ -.. |p| raw:: html - -

- -Design Details -============== - -.. warning:: - Pyomo expression trees are not composed of Python - objects from a single class hierarchy. Consequently, Pyomo - relies on duck typing to ensure that valid expression trees are - created. - -Most Pyomo expression trees have the following form - -1. Interior nodes are objects that inherit from the :class:`ExpressionBase ` class. These objects typically have one or more child nodes. Linear expression nodes do not have child nodes, but they are treated as interior nodes in the expression tree because they references other leaf nodes. - -2. Leaf nodes are numeric values, parameter components and variable components, which represent the *inputs* to the expression. - -Expression Classes ------------------- - -Expression classes typically represent unary and binary operations. The following table -describes the standard operators in Python and their associated Pyomo expression class: - -========== ============= ============================================================================= -Operation Python Syntax Pyomo Class -========== ============= ============================================================================= -sum ``x + y`` :class:`SumExpression ` -product ``x * y`` :class:`ProductExpression ` -negation ``- x`` :class:`NegationExpression ` -division ``x / y`` :class:`DivisionExpression ` -power ``x ** y`` :class:`PowExpression ` -inequality ``x <= y`` :class:`InequalityExpression ` -equality ``x == y`` :class:`EqualityExpression ` -========== ============= ============================================================================= - -Additionally, there are a variety of other Pyomo expression classes that capture more general -logical relationships, which are summarized in the following table: - -==================== ==================================== ======================================================================================== -Operation Example Pyomo Class -==================== ==================================== ======================================================================================== -external function ``myfunc(x,y,z)`` :class:`ExternalFunctionExpression ` -logical if-then-else ``Expr_if(IF=x, THEN=y, ELSE=z)`` :class:`Expr_ifExpression ` -intrinsic function ``sin(x)`` :class:`UnaryFunctionExpression ` -absolute function ``abs(x)`` :class:`AbsExpression ` -==================== ==================================== ======================================================================================== - -Expression objects are immutable. Specifically, the list of -arguments to an expression object (a.k.a. the list of child nodes -in the tree) cannot be changed after an expression class is -constructed. To enforce this property, expression objects have a -standard API for accessing expression arguments: - -* :attr:`args` - a class property that returns a generator that yields the expression arguments -* :attr:`arg(i)` - a function that returns the ``i``-th argument -* :attr:`nargs()` - a function that returns the number of expression arguments - -.. warning:: - - Developers should never use the :attr:`_args_` property directly! - The semantics for the use of this data has changed since earlier - versions of Pyomo. For example, in some expression classes the - the value :func:`nargs()` may not equal :const:`len(_args_)`! - -Expression trees can be categorized in four different ways: - -* constant expressions - expressions that do not contain numeric constants and immutable parameters. -* mutable expressions - expressions that contain mutable parameters but no variables. -* potentially variable expressions - expressions that contain variables, which may be fixed. -* fixed expressions - expressions that contain variables, all of which are fixed. - -These three categories are illustrated with the following example: - -.. literalinclude:: ../../src/expr/design_categories.spy - -The following table describes four different simple expressions -that consist of a single model component, and it shows how they -are categorized: - -======================== ===== ===== ===== ===== -Category m.p m.q m.x m.y -======================== ===== ===== ===== ===== -constant True False False False -not potentially variable True True False False -potentially_variable False False True True -fixed True True False True -======================== ===== ===== ===== ===== - -Expressions classes contain methods to test whether an expression -tree is in each of these categories. Additionally, Pyomo includes -custom expression classes for expression trees that are *not potentially -variable*. These custom classes will not normally be used by -developers, but they provide an optimization of the checks for -potentially variability. - -Special Expression Classes --------------------------- - -The following classes are *exceptions* to the design principles describe above. - -Named Expressions -~~~~~~~~~~~~~~~~~ - -Named expressions allow for changes to an expression after it has -been constructed. For example, consider the expression ``f`` defined -with the :class:`Expression ` component: - -.. literalinclude:: ../../src/expr/design_named_expression.spy - -Although ``f`` is an immutable expression, whose definition is -fixed, a sub-expressions is the named expression ``M.e``. Named -expressions have a mutable value. In other words, the expression -that they point to can change. Thus, a change to the value of -``M.e`` changes the expression tree for any expression that includes -the named expression. - -.. note:: - - The named expression classes are not implemented as sub-classes - of :class:`NumericExpression `. - This reflects design constraints related to the fact that these - are modeling components that belong to class hierarchies other - than the expression class hierarchy, and Pyomo's design prohibits - the use of multiple inheritance for these classes. - -Linear Expressions -~~~~~~~~~~~~~~~~~~ - -Pyomo includes a special expression class for linear expressions. -The class :class:`LinearExpression -` provides a compact -description of linear polynomials. Specifically, it includes a -constant value :attr:`constant` and two lists for coefficients and -variables: :attr:`linear_coefs` and :attr:`linear_vars`. - -This expression object does not have arguments, and thus it is -treated as a leaf node by Pyomo visitor classes. Further, the -expression API functions described above do not work with this -class. Thus, developers need to treat this class differently when -walking an expression tree (e.g. when developing a problem -transformation). - -Sum Expressions -~~~~~~~~~~~~~~~ - -Pyomo does not have a binary sum expression class. Instead, -it has an ``n``-ary summation class, :class:`SumExpression -`. This expression class -treats sums as ``n``-ary sums for efficiency reasons; many large -optimization models contain large sums. But note that this class -maintains the immutability property described above. This class -shares an underlying list of arguments with other :class:`SumExpression -` objects. A particular -object owns the first ``n`` arguments in the shared list, but -different objects may have different values of ``n``. - -This class acts like a normal immutable expression class, and the -API described above works normally. But direct access to the shared -list could have unexpected results. - -Mutable Expressions -~~~~~~~~~~~~~~~~~~~ - -Finally, Pyomo includes several **mutable** expression classes -that are private. These are not intended to be used by users, but -they might be useful for developers in contexts where the developer -can appropriately control how the classes are used. Specifically, -immutability eliminates side-effects where changes to a sub-expression -unexpectedly create changes to the expression tree. But within the context of -model transformations, developers may be able to limit the use of -expressions to avoid these side-effects. The following mutable private classes -are available in Pyomo: - -:class:`_MutableSumExpression ` - This class - is used in the :data:`nonlinear_expression ` context manager to - efficiently combine sums of nonlinear terms. -:class:`_MutableLinearExpression ` - This class - is used in the :data:`linear_expression ` context manager to - efficiently combine sums of linear terms. - - - -Expression Semantics --------------------- - -Pyomo clear semantics regarding what is considered a valid leaf and -interior node. - -The following classes are valid interior nodes: - -* Subclasses of :class:`ExpressionBase ` - -* Classes that that are *duck typed* to match the API of the :class:`ExpressionBase ` class. For example, the named expression class :class:`Expression `. - -The following classes are valid leaf nodes: - -* Members of :data:`nonpyomo_leaf_types `, which includes standard numeric data types like :const:`int`, :const:`float` and :const:`long`, as well as numeric data types defined by `numpy` and other commonly used packages. This set also includes :class:`NonNumericValue `, which is used to wrap non-numeric arguments to the :class:`ExternalFunctionExpression ` class. - -* Parameter component classes like :class:`ScalarParam ` and :class:`_ParamData `, which arise in expression trees when the parameters are declared as mutable. (Immutable parameters are identified when generating expressions, and they are replaced with their associated numeric value.) - -* Variable component classes like :class:`ScalarVar ` and :class:`_GeneralVarData `, which often arise in expression trees. `. - -.. note:: - - In some contexts the :class:`LinearExpression - ` class can be treated - as an interior node, and sometimes it can be treated as a leaf. - This expression object does not have any child arguments, so - ``nargs()`` is zero. But this expression references variables - and parameters in a linear expression, so in that sense it does - not represent a leaf node in the tree. - - - -Context Managers ----------------- - -Pyomo defines several context managers that can be used to declare -the form of expressions, and to define a mutable expression object that -efficiently manages sums. - -The :data:`linear_expression ` -object is a context manager that can be used to declare a linear sum. For -example, consider the following two loops: - -.. literalinclude:: ../../src/expr/design_cm1.spy - -The first apparent difference in these loops is that the value of -``s`` is explicitly initialized while ``e`` is initialized when the -context manager is entered. However, a more fundamental difference -is that the expression representation for ``s`` differs from ``e``. -Each term added to ``s`` results in a new, immutable expression. -By contrast, the context manager creates a mutable expression -representation for ``e``. This difference allows for both (a) a -more efficient processing of each sum, and (b) a more compact -representation for the expression. - -The difference between :data:`linear_expression -` and -:data:`nonlinear_expression ` -is the underlying representation that each supports. Note that -both of these are instances of context manager classes. In -singled-threaded applications, these objects can be safely used to -construct different expressions with different context declarations. - -Finally, note that these context managers can be passed into the :attr:`start` -method for the :func:`quicksum ` function. For example: - -.. literalinclude:: ../../src/expr/design_cm2.spy - -This sum contains terms for ``M.x[i]`` and ``M.y[i]``. The syntax -in this example is not intuitive because the sum is being stored -in ``e``. - -.. note:: - - We do not generally expect users or developers to use these - context managers. They are used by the :func:`quicksum - ` and :func:`sum_product - ` functions to accelerate expression - generation, and there are few cases where the direct use of - these context managers would provide additional utility to users - and developers. - diff --git a/doc/Archive/developer_reference/expressions/index.rst b/doc/Archive/developer_reference/expressions/index.rst deleted file mode 100644 index 685fde25173..00000000000 --- a/doc/Archive/developer_reference/expressions/index.rst +++ /dev/null @@ -1,55 +0,0 @@ -.. |p| raw:: html - -

- -Pyomo Expressions -================= - -.. warning:: - - This documentation does not explicitly reference objects in - pyomo.core.kernel. While the Pyomo5 expression system works - with pyomo.core.kernel objects, the documentation of these - documents was not sufficient to appropriately describe the use - of kernel objects in expressions. - -Pyomo supports the declaration of symbolic expressions that represent -objectives, constraints and other optimization modeling components. -Pyomo expressions are represented in an expression tree, where the -leaves are operands, such as constants or variables, and the internal -nodes contain operators. Pyomo relies on so-called magic methods -to automate the construction of symbolic expressions. For example, -consider an expression ``e`` declared as follows: - -.. literalinclude:: ../../src/expr/index_simple.spy - -Python determines that the magic method ``__mul__`` is called on -the ``M.v`` object, with the argument ``2``. This method returns -a Pyomo expression object ``ProductExpression`` that has arguments -``M.v`` and ``2``. This represents the following symbolic expression -tree: - -.. graphviz:: - - digraph foo { - "*" -> "v"; - "*" -> "2"; - } - -.. note:: - - End-users will not likely need to know details related to how - symbolic expressions are generated and managed in Pyomo. Thus, - most of the following documentation of expressions in Pyomo is most - useful for Pyomo developers. However, the discussion of runtime - performance in the first section will help end-users write large-scale - models. - -.. toctree:: - :maxdepth: 1 - - performance.rst - overview.rst - design.rst - managing.rst - diff --git a/doc/Archive/developer_reference/expressions/managing.rst b/doc/Archive/developer_reference/expressions/managing.rst deleted file mode 100644 index a4dd2a51436..00000000000 --- a/doc/Archive/developer_reference/expressions/managing.rst +++ /dev/null @@ -1,272 +0,0 @@ -.. |p| raw:: html - -

- -Managing Expressions -==================== - -Creating a String Representation of an Expression -------------------------------------------------- - -There are several ways that string representations can be created -from an expression, but the :func:`expression_to_string -` function provides -the most flexible mechanism for generating a string representation. -The options to this function control distinct aspects of the string -representation. - -Algebraic vs. Nested Functional Form -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The default string representation is an algebraic form, which closely -mimics the Python operations used to construct an expression. The -:data:`verbose` flag can be set to :const:`True` to generate a -string representation that is a nested functional form. For example: - -.. literalinclude:: ../../src/expr/managing_ex1.spy - -Labeler and Symbol Map -~~~~~~~~~~~~~~~~~~~~~~ - -The string representation used for variables in expression can be -customized to define different label formats. If the :data:`labeler` -option is specified, then this function (or class functor) is used to -generate a string label used to represent the variable. Pyomo defines a -variety of labelers in the `pyomo.core.base.label` module. For example, -the :class:`NumericLabeler` defines a functor that can be used to -sequentially generate simple labels with a prefix followed by the -variable count: - -.. literalinclude:: ../../src/expr/managing_ex2.spy - -The :data:`smap` option is used to specify a symbol map object -(:class:`SymbolMap `), which -caches the variable label data. This option is normally specified -in contexts where the string representations for many expressions -are being generated. In that context, a symbol map ensures that -variables in different expressions have a consistent label in their -associated string representations. - - -Other Ways to Generate String Representations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There are two other standard ways to generate string representations: - -* Call the :func:`__str__` magic method (e.g. using the Python - :func:`str()` function. This calls :func:`expression_to_string - `, using the default values for - all arguments. - -* Call the :func:`to_string` method on the - :class:`ExpressionBase` class. This - calls :func:`expression_to_string - ` and accepts the same arguments. - - -Evaluating Expressions ----------------------- - -Expressions can be evaluated when all variables and parameters in -the expression have a value. The :func:`value ` -function can be used to walk the expression tree and compute the -value of an expression. For example: - -.. literalinclude:: ../../src/expr/managing_ex5.spy - -Additionally, expressions define the :func:`__call__` method, so the -following is another way to compute the value of an expression: - -.. literalinclude:: ../../src/expr/managing_ex6.spy - -If a parameter or variable is undefined, then the :func:`value -` function and :func:`__call__` method will -raise an exception. This exception can be suppressed using the -:attr:`exception` option. For example: - -.. literalinclude:: ../../src/expr/managing_ex7.spy - -This option is useful in contexts where adding a try block is inconvenient -in your modeling script. - -.. note:: - - Both the :func:`value ` function and - :func:`__call__` method call the :func:`evaluate_expression - ` function. In - practice, this function will be slightly faster, but the - difference is only meaningful when expressions are evaluated - many times. - -Identifying Components and Variables ------------------------------------- - -Expression transformations sometimes need to find all nodes in an -expression tree that are of a given type. Pyomo contains two utility -functions that support this functionality. First, the -:func:`identify_components ` -function is a generator function that walks the expression tree and yields all -nodes whose type is in a specified set of node types. For example: - -.. literalinclude:: ../../src/expr/managing_ex8.spy - -The :func:`identify_variables ` -function is a generator function that yields all nodes that are -variables. Pyomo uses several different classes to represent variables, -but this set of variable types does not need to be specified by the user. -However, the :attr:`include_fixed` flag can be specified to omit fixed -variables. For example: - -.. literalinclude:: ../../src/expr/managing_ex9.spy - -Walking an Expression Tree with a Visitor Class ------------------------------------------------ - -Many of the utility functions defined above are implemented by -walking an expression tree and performing an operation at nodes in -the tree. For example, evaluating an expression is performed using -a post-order depth-first search process where the value of a node -is computed using the values of its children. - -Walking an expression tree can be tricky, and the code requires intimate -knowledge of the design of the expression system. Pyomo includes -several classes that define visitor patterns for walking expression -tree: - -:class:`StreamBasedExpressionVisitor ` - The most general and extensible visitor class. This visitor - implements an event-based approach for walking the tree inspired by - the ``expat`` library for processing XML files. The visitor has - seven event callbacks that users can hook into, providing very - fine-grained control over the expression walker. - -:class:`SimpleExpressionVisitor ` - A :func:`visitor` method is called for each node in the tree, - and the visitor class collects information about the tree. - -:class:`ExpressionValueVisitor ` - When the :func:`visitor` method is called on each node in the - tree, the *values* of its children have been computed. The - *value* of the node is returned from :func:`visitor`. - -:class:`ExpressionReplacementVisitor ` - When the :func:`visitor` method is called on each node in the - tree, it may clone or otherwise replace the node using objects - for its children (which themselves may be clones or replacements - from the original child objects). The new node object is - returned from :func:`visitor`. - -These classes define a variety of suitable tree search methods: - -* :class:`StreamBasedExpressionVisitor ` - - * ``walk_expression``: depth-first traversal of the expression tree. - -* :class:`ExpressionReplacementVisitor ` - - * ``walk_expression``: depth-first traversal of the expression tree. - -* :class:`SimpleExpressionVisitor ` - - * ``xbfs``: breadth-first search where leaf nodes are immediately visited - * ``xbfs_yield_leaves``: breadth-first search where leaf nodes are - immediately visited, and the visit method yields a value - -* :class:`ExpressionValueVisitor ` - - * ``dfs_postorder_stack``: postorder depth-first search using a - nonrecursive stack - - -To implement a visitor object, a user needs to provide specializations -for specific events. For legacy visitors based on the PyUtilib -visitor pattern (e.g., :class:`SimpleExpressionVisitor` and -:class:`ExpressionValueVisitor`), one must create a subclass of one of these -classes and override at least one of the following: - -:func:`visitor` - Defines the operation that is performed when a node is visited. In - the :class:`ExpressionValueVisitor - ` and - :class:`ExpressionReplacementVisitor - ` visitor classes, - this method returns a value that is used by its parent node. - -:func:`visiting_potential_leaf` - Checks if the search should terminate with this node. If no, - then this method returns the tuple ``(False, None)``. If yes, - then this method returns ``(False, value)``, where *value* is - computed by this method. This method is not used in the - :class:`SimpleExpressionVisitor - ` visitor - class. - -:func:`finalize` - This method defines the final value that is returned from the - visitor. This is not normally redefined. - -For modern visitors based on the :class:`StreamBasedExpressionVisitor -`, one can either define a -subclass, pass the callbacks to an instance of the base class, or assign -the callbacks as attributes on an instance of the base class. The -:class:`StreamBasedExpressionVisitor -` provides seven -callbacks, which are documented in the class documentation. - -Detailed documentation of the APIs for these methods is provided -with the class documentation for these visitors. - -SimpleExpressionVisitor Example -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In this example, we describe an visitor class that counts the number -of nodes in an expression (including leaf nodes). Consider the following -class: - -.. literalinclude:: ../../src/expr/managing_visitor1.spy - -The class constructor creates a counter, and the :func:`visit` method -increments this counter for every node that is visited. The :func:`finalize` -method returns the value of this counter after the tree has been walked. The -following function illustrates this use of this visitor class: - -.. literalinclude:: ../../src/expr/managing_visitor2.spy - - -ExpressionValueVisitor Example -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In this example, we describe an visitor class that clones the -expression tree (including leaf nodes). Consider the following -class: - -.. literalinclude:: ../../src/expr/managing_visitor3.spy - -The :func:`visit` method creates a new expression node with children -specified by :attr:`values`. The :func:`visiting_potential_leaf` -method performs a :func:`deepcopy` on leaf nodes, which are native -Python types or non-expression objects. - -.. literalinclude:: ../../src/expr/managing_visitor4.spy - - -ExpressionReplacementVisitor Example -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In this example, we describe an visitor class that replaces -variables with scaled variables, using a mutable parameter that -can be modified later. the following -class: - -.. literalinclude:: ../../src/expr/managing_visitor5.spy - -No other method need to be defined. The -:func:`beforeChild` method identifies variable nodes -and returns a product expression that contains a mutable parameter. - -.. literalinclude:: ../../src/expr/managing_visitor6.spy - -The :func:`scale_expression` function is called with an expression and -a dictionary, :attr:`scale`, that maps variable ID to model parameter. For example: - -.. literalinclude:: ../../src/expr/managing_visitor7.spy diff --git a/doc/Archive/developer_reference/expressions/overview.rst b/doc/Archive/developer_reference/expressions/overview.rst deleted file mode 100644 index c1962edec22..00000000000 --- a/doc/Archive/developer_reference/expressions/overview.rst +++ /dev/null @@ -1,300 +0,0 @@ -.. |p| raw:: html - -

- -Design Overview -=============== - -Historical Comparison ---------------------- - -This document describes the "Pyomo5" expressions, which were -introduced in Pyomo 5.6. The main differences between "Pyomo5" -expressions and the previous expression system, called "Coopr3", -are: - -* Pyomo5 supports both CPython and PyPy implementations of Python, - while Coopr3 only supports CPython. - - The key difference in these implementations is that Coopr3 relies - on CPython reference counting, which is not part of the Python - language standard. Hence, this implementation is not guaranteed - to run on other implementations of Python. - - Pyomo5 does not rely on reference counting, and it has been tested - with PyPy. In the future, this should allow Pyomo to support - other Python implementations (e.g. Jython). - - |p| - -* Pyomo5 expression objects are immutable, while Coopr3 expression - objects are mutable. - - This difference relates to how expression objects are managed - in Pyomo. Once created, Pyomo5 expression objects cannot be - changed. Further, the user is guaranteed that no "side effects" - occur when expressions change at a later point in time. By - contrast, Coopr3 allows expressions to change in-place, and thus - "side effects" make occur when expressions are changed at a later - point in time. (See discussion of entanglement below.) - - |p| - -* Pyomo5 provides more consistent runtime performance than Coopr3. - - While this documentation does not provide a detailed comparison - of runtime performance between Coopr3 and Pyomo5, the following - performance considerations also motivated the creation of Pyomo5: - - * There were surprising performance inconsistencies in Coopr3. For - example, the following two loops had dramatically different - runtime: - - .. literalinclude:: ../../src/expr/overview_example1.spy - - * Coopr3 eliminates side effects by automatically cloning sub-expressions. - Unfortunately, this can easily lead to unexpected cloning in models, which - can dramatically slow down Pyomo model generation. For example: - - .. literalinclude:: ../../src/expr/overview_example2.spy - - * Coopr3 leverages recursion in many operations, including expression - cloning. Even simple non-linear expressions can result in deep - expression trees where these recursive operations fail because - Python runs out of stack space. - - |p| - - * The immutable representation used in Pyomo5 requires more memory allocations - than Coopr3 in simple loops. Hence, a pure-Python execution of Pyomo5 - can be 10% slower than Coopr3 for model construction. But when Cython is used - to optimize the execution of Pyomo5 expression generation, the - runtimes for Pyomo5 and Coopr3 are about the same. (In principle, - Cython would improve the runtime of Coopr3 as well, but the limitations - noted above motivated a new expression system in any case.) - -Expression Entanglement and Mutability --------------------------------------- - -Pyomo fundamentally relies on the use of magic methods in Python -to generate expression trees, which means that Pyomo has very limited -control for how expressions are managed in Python. For example: - -* Python variables can point to the same expression tree - - .. literalinclude:: ../../src/expr/overview_tree1.spy - - This is illustrated as follows: - - .. graphviz:: - - digraph foo { - { - e [shape=box] - f [shape=box] - } - "*" -> 2; - "*" -> v; - subgraph cluster { "*"; 2; v; } - e -> "*" [splines=curved, style=dashed]; - f -> "*" [splines=curved, style=dashed]; - } - -* A variable can point to a sub-tree that another variable points to - - .. literalinclude:: ../../src/expr/overview_tree2.spy - - This is illustrated as follows: - - .. graphviz:: - - digraph foo { - { - e [shape=box] - f [shape=box] - } - "*" -> 2; - "*" -> v; - "+" -> "*"; - "+" -> 3; - subgraph cluster { "+"; 3; "*"; 2; v; } - e -> "*" [splines=curved, style=dashed, constraint=false]; - f -> "+" [splines=curved, style=dashed]; - } - -* Two expression trees can point to the same sub-tree - - .. literalinclude:: ../../src/expr/overview_tree3.spy - - This is illustrated as follows: - - .. graphviz:: - - digraph foo { - { - e [shape=box] - f [shape=box] - g [shape=box] - } - x [label="+"]; - "*" -> 2; - "*" -> v; - "+" -> "*"; - "+" -> 3; - x -> 4; - x -> "*"; - subgraph cluster { x; 4; "+"; 3; "*"; 2; v; } - e -> "*" [splines=curved, style=dashed, constraint=false]; - f -> "+" [splines=curved, style=dashed]; - g -> x [splines=curved, style=dashed]; - } - -In each of these examples, it is almost impossible for a Pyomo user -or developer to detect whether expressions are being shared. In -CPython, the reference counting logic can support this to a limited -degree. But no equivalent mechanisms are available in PyPy and -other Python implementations. - -Entangled Sub-Expressions -~~~~~~~~~~~~~~~~~~~~~~~~~ - -We say that expressions are *entangled* if they share one or more -sub-expressions. The first example above does not represent -entanglement, but rather the fact that multiple Python variables -can point to the same expression tree. In the second and third -examples, the expressions are entangled because the subtree represented -by ``e`` is shared. However, if a leave node like ``M.v`` is shared -between expressions, we do not consider those expressions entangled. - -Expression entanglement is problematic because shared expressions complicate -the expected behavior when sub-expressions are changed. Consider the following example: - -.. literalinclude:: ../../src/expr/overview_tree4.spy - -What is the value of ``e`` after ``M.w`` is added to it? What is the -value of ``f``? The answers to these questions are not immediately -obvious, and the fact that Coopr3 uses mutable expression objects -makes them even less clear. However, Pyomo5 and Coopr3 enforce -the following semantics: - -.. pull-quote:: - - A change to an expression *e* that is a sub-expression of *f* - does not change the expression tree for *f*. - -This property ensures a change to an expression does not create side effects that change the -values of other, previously defined expressions. - -For instance, the previous example results in the following (in Pyomo5): - -.. graphviz:: - - digraph foo { - { - e [shape=box] - f [shape=box] - } - x [label="+"]; - "*" -> 2; - "*" -> v; - "+" -> "*"; - "+" -> 3; - x -> "*"; - x -> w; - subgraph cluster { "+"; 3; "*"; 2; v; x; w;} - f -> "+" [splines=curved, style=dashed]; - e -> x [splines=curved, style=dashed]; - } - -With Pyomo5 expressions, each sub-expression is immutable. Thus, -the summation operation generates a new expression ``e`` without -changing existing expression objects referenced in the expression -tree for ``f``. By contrast, Coopr3 imposes the same property by -cloning the expression ``e`` before added ``M.w``, resulting in the following: - -.. graphviz:: - - digraph foo { - { - e [shape=box] - f [shape=box] - } - "*" -> 2; - "*" -> v; - "+" -> "*"; - "+" -> 3; - etimes [label="*"]; - etwo [label=2]; - etimes -> etwo; - etimes -> v; - x [label="+"]; - x -> w; - x -> etimes; - subgraph cluster { "+"; 3; "*"; 2; v; x; w; etimes; etwo;} - f -> "+" [splines=curved, style=dashed]; - e -> x [splines=curved, style=dashed]; - } - -This example also illustrates that leaves may be shared between expressions. - -Mutable Expression Components -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There is one important exception to the entanglement property -described above. The ``Expression`` component is treated as a -mutable expression when shared between expressions. For example: - -.. literalinclude:: ../../src/expr/overview_tree5.spy - -Here, the expression ``M.e`` is a so-called *named expression* that -the user has declared. Named expressions are explicitly intended -for re-use within models, and they provide a convenient mechanism -for changing sub-expressions in complex applications. In this example, the -expression tree is as follows before ``M.w`` is added: - -.. graphviz:: - - digraph foo { - { - f [shape=box] - } - "*" -> 2; - "*" -> v; - "+" -> "M.e"; - "+" -> 3; - "M.e" -> "*"; - subgraph cluster { "+"; 3; "*"; 2; v; "M.e";} - f -> "+" [splines=curved, style=dashed]; - } - - -And the expression tree is as follows after ``M.w`` is added. - -.. graphviz:: - - digraph foo { - { - f [shape=box] - } - x [label="+"]; - "*" -> 2; - "*" -> v; - "+" -> "M.e"; - "+" -> 3; - x -> "*"; - x -> w; - "M.e" -> x; - subgraph cluster { "+"; 3; "*"; 2; v; "M.e"; x; w;} - f -> "+" [splines=curved, style=dashed]; - } - - -When considering named expressions, Pyomo5 and Coopr3 enforce -the following semantics: - -.. pull-quote:: - - A change to a named expression *e* that is a sub-expression of - *f* changes the expression tree for *f*, because *f* continues - to point to *e* after it is changed. - diff --git a/doc/Archive/developer_reference/expressions/performance.rst b/doc/Archive/developer_reference/expressions/performance.rst deleted file mode 100644 index 8e344e50982..00000000000 --- a/doc/Archive/developer_reference/expressions/performance.rst +++ /dev/null @@ -1,171 +0,0 @@ -.. |p| raw:: html - -

- -Building Expressions Faster -=========================== - -Expression Generation ---------------------- - -Pyomo expressions can be constructed using native binary operators -in Python. For example, a sum can be created in a simple loop: - -.. literalinclude:: ../../src/expr/performance_loop1.spy - -Additionally, Pyomo expressions can be constructed using functions -that iteratively apply Python binary operators. For example, the -Python :func:`sum` function can be used to replace the previous -loop: - -.. literalinclude:: ../../src/expr/performance_loop2.spy - -The :func:`sum` function is both more compact and more efficient. -Using :func:`sum` avoids the creation of temporary variables, and -the summation logic is executed in the Python interpreter while the -loop is interpreted. - - -Linear, Quadratic and General Nonlinear Expressions ---------------------------------------------------- - -Pyomo can express a very wide range of algebraic expressions, and -there are three general classes of expressions that are recognized -by Pyomo: - - * **linear polynomials** - * **quadratic polynomials** - * **nonlinear expressions**, including higher-order polynomials and - expressions with intrinsic functions - -These classes of expressions are leveraged to efficiently generate -compact representations of expressions, and to transform expression -trees into standard forms used to interface with solvers. Note -that There not all quadratic polynomials are recognized by Pyomo; -in other words, some quadratic expressions are treated as nonlinear -expressions. - -For example, consider the following quadratic polynomial: - -.. literalinclude:: ../../src/expr/performance_loop3.spy - -This quadratic polynomial is treated as a nonlinear expression -unless the expression is explicitly processed to identify quadratic -terms. This *lazy* identification of of quadratic terms allows -Pyomo to tailor the search for quadratic terms only when they are -explicitly needed. - -Pyomo Utility Functions ------------------------ - -Pyomo includes several similar functions that can be used to -create expressions: - -:func:`prod ` - A function to compute a product of Pyomo expressions. - -:func:`quicksum ` - A function to efficiently compute a sum of Pyomo expressions. - -:func:`sum_product ` - A function that computes a generalized dot product. - -prod -~~~~ - -The :func:`prod ` function is analogous to the builtin -:func:`sum` function. Its main argument is a variable length -argument list, :attr:`args`, which represents expressions that are multiplied -together. For example: - -.. literalinclude:: ../../src/expr/performance_prod.spy - -quicksum -~~~~~~~~ - -The behavior of the :func:`quicksum ` function is -similar to the builtin :func:`sum` function, but this function often -generates a more compact Pyomo expression. Its main argument is a -variable length argument list, :attr:`args`, which represents -expressions that are summed together. For example: - -.. literalinclude:: ../../src/expr/performance_quicksum.spy - -The summation is customized based on the :attr:`start` and -:attr:`linear` arguments. The :attr:`start` defines the initial -value for summation, which defaults to zero. If :attr:`start` is -a numeric value, then the :attr:`linear` argument determines how -the sum is processed: - -* If :attr:`linear` is :const:`False`, then the terms in :attr:`args` are assumed to be nonlinear. -* If :attr:`linear` is :const:`True`, then the terms in :attr:`args` are assumed to be linear. -* If :attr:`linear` is :const:`None`, the first term in :attr:`args` is analyze to determine whether the terms are linear or nonlinear. - -This argument allows the :func:`quicksum ` -function to customize the expression representation used, and -specifically a more compact representation is used for linear -polynomials. The :func:`quicksum ` -function can be slower than the builtin :func:`sum` function, -but this compact representation can generate problem representations -more quickly. - -Consider the following example: - -.. literalinclude:: ../../src/expr/quicksum_runtime.spy - -The sum consists of linear terms because the exponents are one. -The following output illustrates that quicksum can identify this -linear structure to generate expressions more quickly: - -.. literalinclude:: ../../src/expr/quicksum.log - :language: none - -If :attr:`start` is not a numeric value, then the :func:`quicksum -` sets the initial value to :attr:`start` -and executes a simple loop to sum the terms. This allows the sum -to be stored in an object that is passed into the function (e.g. the linear context manager -:data:`linear_expression `). - -.. Warning:: - - By default, :attr:`linear` is :const:`None`. While this allows - for efficient expression generation in normal cases, there are - circumstances where the inspection of the first - term in :attr:`args` is misleading. Consider the following - example: - - .. literalinclude:: ../../src/expr/performance_warning.spy - - The first term created by the generator is linear, but the - subsequent terms are nonlinear. Pyomo gracefully transitions - to a nonlinear sum, but in this case :func:`quicksum ` - is doing additional work that is not useful. - -sum_product -~~~~~~~~~~~ - -The :func:`sum_product ` function supports -a generalized dot product. The :attr:`args` argument contains one -or more components that are used to create terms in the summation. -If the :attr:`args` argument contains a single components, then its -sequence of terms are summed together; the sum is equivalent to -calling :func:`quicksum `. If two or more components are -provided, then the result is the summation of their terms multiplied -together. For example: - -.. literalinclude:: ../../src/expr/performance_sum_product1.spy - -The :attr:`denom` argument specifies components whose terms are in -the denominator. For example: - -.. literalinclude:: ../../src/expr/performance_sum_product2.spy - -The terms summed by this function are explicitly specified, so -:func:`sum_product ` can identify -whether the resulting expression is linear, quadratic or nonlinear. -Consequently, this function is typically faster than simple loops, -and it generates compact representations of expressions.. - -Finally, note that the :func:`dot_product ` -function is an alias for :func:`sum_product `. - diff --git a/doc/Archive/developer_reference/future.rst b/doc/Archive/developer_reference/future.rst deleted file mode 100644 index 531c0fdb5c6..00000000000 --- a/doc/Archive/developer_reference/future.rst +++ /dev/null @@ -1,3 +0,0 @@ - -.. automodule:: pyomo.__future__ - :noindex: diff --git a/doc/Archive/developer_reference/index.rst b/doc/Archive/developer_reference/index.rst deleted file mode 100644 index 0feb33cdab9..00000000000 --- a/doc/Archive/developer_reference/index.rst +++ /dev/null @@ -1,16 +0,0 @@ -Developer Reference -=================== - -This section provides documentation about fundamental capabilities -in Pyomo. This documentation serves as a reference for both (1) -Pyomo developers and (2) advanced users who are developing Python -scripts using Pyomo. - -.. toctree:: - :maxdepth: 1 - - config.rst - deprecation.rst - expressions/index.rst - future.rst - solvers.rst diff --git a/doc/Archive/developer_reference/solvers.rst b/doc/Archive/developer_reference/solvers.rst deleted file mode 100644 index 9e3281246f4..00000000000 --- a/doc/Archive/developer_reference/solvers.rst +++ /dev/null @@ -1,351 +0,0 @@ -Future Solver Interface Changes -=============================== - -.. note:: - - The new solver interfaces are still under active development. They - are included in the releases as development previews. Please be - aware that APIs and functionality may change with no notice. - - We welcome any feedback and ideas as we develop this capability. - Please post feedback on - `Issue 1030 `_. - -Pyomo offers interfaces into multiple solvers, both commercial and open -source. To support better capabilities for solver interfaces, the Pyomo -team is actively redesigning the existing interfaces to make them more -maintainable and intuitive for use. A preview of the redesigned -interfaces can be found in ``pyomo.contrib.solver``. - -.. currentmodule:: pyomo.contrib.solver - - -New Interface Usage -------------------- - -The new interfaces are not completely backwards compatible with the -existing Pyomo solver interfaces. However, to aid in testing and -evaluation, we are distributing versions of the new solver interfaces -that are compatible with the existing ("legacy") solver interface. -These "legacy" interfaces are registered with the current -``SolverFactory`` using slightly different names (to avoid conflicts -with existing interfaces). - -.. |br| raw:: html - -
- -.. list-table:: Available Redesigned Solvers and Names Registered - in the SolverFactories - :header-rows: 1 - - * - Solver - - Name registered in the |br| ``pyomo.contrib.solver.factory.SolverFactory`` - - Name registered in the |br| ``pyomo.opt.base.solvers.LegacySolverFactory`` - * - Ipopt - - ``ipopt`` - - ``ipopt_v2`` - * - Gurobi (persistent) - - ``gurobi`` - - ``gurobi_v2`` - * - Gurobi (direct) - - ``gurobi_direct`` - - ``gurobi_direct_v2`` - -Using the new interfaces through the legacy interface -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Here we use the new interface as exposed through the existing (legacy) -solver factory and solver interface wrapper. This provides an API that -is compatible with the existing (legacy) Pyomo solver interface and can -be used with other Pyomo tools / capabilities. - -.. testcode:: - :skipif: not ipopt_available - - import pyomo.environ as pyo - from pyomo.contrib.solver.util import assert_optimal_termination - - model = pyo.ConcreteModel() - model.x = pyo.Var(initialize=1.5) - model.y = pyo.Var(initialize=1.5) - - def rosenbrock(model): - return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 - - model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) - - status = pyo.SolverFactory('ipopt_v2').solve(model) - assert_optimal_termination(status) - model.pprint() - -.. testoutput:: - :skipif: not ipopt_available - :hide: - - 2 Var Declarations - ... - 3 Declarations: x y obj - -In keeping with our commitment to backwards compatibility, both the legacy and -future methods of specifying solver options are supported: - -.. testcode:: - :skipif: not ipopt_available - - import pyomo.environ as pyo - - model = pyo.ConcreteModel() - model.x = pyo.Var(initialize=1.5) - model.y = pyo.Var(initialize=1.5) - - def rosenbrock(model): - return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 - - model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) - - # Backwards compatible - status = pyo.SolverFactory('ipopt_v2').solve(model, options={'max_iter' : 6}) - # Forwards compatible - status = pyo.SolverFactory('ipopt_v2').solve(model, solver_options={'max_iter' : 6}) - model.pprint() - -.. testoutput:: - :skipif: not ipopt_available - :hide: - - 2 Var Declarations - ... - 3 Declarations: x y obj - -Using the new interfaces directly -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Here we use the new interface by importing it directly: - -.. testcode:: - :skipif: not ipopt_available - - # Direct import - import pyomo.environ as pyo - from pyomo.contrib.solver.util import assert_optimal_termination - from pyomo.contrib.solver.ipopt import Ipopt - - model = pyo.ConcreteModel() - model.x = pyo.Var(initialize=1.5) - model.y = pyo.Var(initialize=1.5) - - def rosenbrock(model): - return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 - - model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) - - opt = Ipopt() - status = opt.solve(model) - assert_optimal_termination(status) - # Displays important results information; only available through the new interfaces - status.display() - model.pprint() - -.. testoutput:: - :skipif: not ipopt_available - :hide: - - solution_loader: ... - ... - 3 Declarations: x y obj - -Using the new interfaces through the "new" SolverFactory -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Here we use the new interface by retrieving it from the new ``SolverFactory``: - -.. testcode:: - :skipif: not ipopt_available - - # Import through new SolverFactory - import pyomo.environ as pyo - from pyomo.contrib.solver.util import assert_optimal_termination - from pyomo.contrib.solver.factory import SolverFactory - - model = pyo.ConcreteModel() - model.x = pyo.Var(initialize=1.5) - model.y = pyo.Var(initialize=1.5) - - def rosenbrock(model): - return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 - - model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) - - opt = SolverFactory('ipopt') - status = opt.solve(model) - assert_optimal_termination(status) - # Displays important results information; only available through the new interfaces - status.display() - model.pprint() - -.. testoutput:: - :skipif: not ipopt_available - :hide: - - solution_loader: ... - ... - 3 Declarations: x y obj - -Switching all of Pyomo to use the new interfaces -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -We also provide a mechanism to get a "preview" of the future where we -replace the existing (legacy) SolverFactory and utilities with the new -(development) version (see :doc:`future`): - -.. testcode:: - :skipif: not ipopt_available - - # Change default SolverFactory version - import pyomo.environ as pyo - from pyomo.contrib.solver.util import assert_optimal_termination - from pyomo.__future__ import solver_factory_v3 - - model = pyo.ConcreteModel() - model.x = pyo.Var(initialize=1.5) - model.y = pyo.Var(initialize=1.5) - - def rosenbrock(model): - return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2 - - model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize) - - status = pyo.SolverFactory('ipopt').solve(model) - assert_optimal_termination(status) - # Displays important results information; only available through the new interfaces - status.display() - model.pprint() - -.. testoutput:: - :skipif: not ipopt_available - :hide: - - solution_loader: ... - ... - 3 Declarations: x y obj - -.. testcode:: - :skipif: not ipopt_available - :hide: - - from pyomo.__future__ import solver_factory_v1 - -Linear Presolve and Scaling -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The new interface allows access to new capabilities in the various -problem writers, including the linear presolve and scaling options -recently incorporated into the redesigned NL writer. For example, you -can control the NL writer in the new ``ipopt`` interface through the -solver's ``writer_config`` configuration option: - -.. autoclass:: pyomo.contrib.solver.ipopt.Ipopt - :members: solve - -.. testcode:: - - from pyomo.contrib.solver.ipopt import Ipopt - opt = Ipopt() - opt.config.writer_config.display() - -.. testoutput:: - - show_section_timing: false - skip_trivial_constraints: true - file_determinism: FileDeterminism.ORDERED - symbolic_solver_labels: false - scale_model: true - export_nonlinear_variables: None - row_order: None - column_order: None - export_defined_variables: true - linear_presolve: true - -Note that, by default, both ``linear_presolve`` and ``scale_model`` are enabled. -Users can manipulate ``linear_presolve`` and ``scale_model`` to their preferred -states by changing their values. - -.. code-block:: python - - >>> opt.config.writer_config.linear_presolve = False - - -Interface Implementation ------------------------- - -All new interfaces should be built upon one of two classes (currently): -:class:`SolverBase` or -:class:`PersistentSolverBase`. - -All solvers should have the following: - -.. autoclass:: pyomo.contrib.solver.base.SolverBase - :members: - -Persistent solvers include additional members as well as other configuration options: - -.. autoclass:: pyomo.contrib.solver.base.PersistentSolverBase - :show-inheritance: - :members: - -Results -------- - -Every solver, at the end of a -:meth:`solve` call, will -return a :class:`Results` -object. This object is a :py:class:`pyomo.common.config.ConfigDict`, -which can be manipulated similar to a standard ``dict`` in Python. - -.. autoclass:: pyomo.contrib.solver.results.Results - :show-inheritance: - :members: - :undoc-members: - - -Termination Conditions -^^^^^^^^^^^^^^^^^^^^^^ - -Pyomo offers a standard set of termination conditions to map to solver -returns. The intent of -:class:`TerminationCondition` -is to notify the user of why the solver exited. The user is expected -to inspect the :class:`Results` -object or any returned solver messages or logs for more information. - -.. autoclass:: pyomo.contrib.solver.results.TerminationCondition - :show-inheritance: - - -Solution Status -^^^^^^^^^^^^^^^ - -Pyomo offers a standard set of solution statuses to map to solver -output. The intent of -:class:`SolutionStatus` -is to notify the user of what the solver returned at a high level. The -user is expected to inspect the -:class:`Results` object or any -returned solver messages or logs for more information. - -.. autoclass:: pyomo.contrib.solver.results.SolutionStatus - :show-inheritance: - - -Solution --------- - -Solutions can be loaded back into a model using a ``SolutionLoader``. A specific -loader should be written for each unique case. Several have already been -implemented. For example, for ``ipopt``: - -.. autoclass:: pyomo.contrib.solver.ipopt.IpoptSolutionLoader - :show-inheritance: - :members: - :inherited-members: diff --git a/doc/Archive/docutils.conf b/doc/Archive/docutils.conf deleted file mode 100644 index 84f89f45e9b..00000000000 --- a/doc/Archive/docutils.conf +++ /dev/null @@ -1,2 +0,0 @@ -[writers] -table_style=colwidths-auto diff --git a/doc/Archive/errors.rst b/doc/Archive/errors.rst deleted file mode 100644 index 162c2e10257..00000000000 --- a/doc/Archive/errors.rst +++ /dev/null @@ -1,192 +0,0 @@ -Common Warnings/Errors -====================== - -.. - NOTE to developers: as we use section links to direct users, it is - critical that the "IDs" are unique. When adding a new extended - warning / error description, DO NOT renumber existing entries. Also, - for backwards compatibility, DO NOT recycle old ID (no longer used) - numbers. - -.. doctest:: - :hide: - - >>> import pyomo.environ as pyo - -.. py:currentmodule:: pyomo.environ - - -.. =================================================================== -.. Extended descriptions for Pyomo warnings -.. =================================================================== - -Warnings --------- - -.. _W1001: - -W1001: Setting Var value not in domain -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When setting :class:`Var` values (by either calling :meth:`Var.set_value()` -or setting the :attr:`value` attribute), Pyomo will validate the -incoming value by checking that the value is ``in`` the -:attr:`Var.domain`. Any values not in the domain will generate this -warning: - -.. doctest:: - - >>> m = pyo.ConcreteModel() - >>> m.x = pyo.Var(domain=pyo.Integers) - >>> m.x = 0.5 - WARNING (W1001): Setting Var 'x' to a value `0.5` (float) not in domain - Integers. - See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001 - >>> print(m.x.value) - 0.5 - - -Users can bypass all domain validation by setting the value using: - -.. doctest:: - - >>> m.x.set_value(0.75, skip_validation=True) - >>> print(m.x.value) - 0.75 - - - -.. _W1002: - -W1002: Setting Var value outside the bounds -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When setting :py:class:`Var` values (by either calling :meth:`set_value()` -or setting the :attr:`value` attribute), Pyomo will validate the -incoming value by checking that the value is within the range specified by -:attr:`Var.bounds`. Any values outside the bounds will generate this -warning: - -.. doctest:: - - >>> m = pyo.ConcreteModel() - >>> m.x = pyo.Var(domain=pyo.Integers, bounds=(1, 5)) - >>> m.x = 0 - WARNING (W1002): Setting Var 'x' to a numeric value `0` outside the bounds - (1, 5). - See also https://pyomo.readthedocs.io/en/stable/errors.html#w1002 - >>> print(m.x.value) - 0 - -Users can bypass all domain validation by setting the value using: - -.. doctest:: - - >>> m.x.set_value(10, skip_validation=True) - >>> print(m.x.value) - 10 - - - -.. _W1003: - -W1003: Unexpected RecursionError walking an expression tree -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pyomo leverages a recursive walker (the -:py:class:`~pyomo.core.expr.visitor.StreamBasedExpressionVisitor`) to -traverse (walk) expression trees. For most expressions, this recursive -walker is the most efficient. However, Python has a relatively shallow -recursion limit (generally, 1000 frames). The recursive walker is -designed to monitor the stack depth and cleanly switch to a nonrecursive -walker before hitting the stack limit. However, there are two (rare) -cases where the Python stack limit can still generate a -:py:exc:`RecursionError` exception: - -#. Starting the walker with fewer than - :py:data:`pyomo.core.expr.visitor.RECURSION_LIMIT` available frames. -#. Callbacks that require more than 2 * - :py:data:`pyomo.core.expr.visitor.RECURSION_LIMIT` frames. - -The (default) recursive walker will catch the exception and restart the -walker from the beginning in non-recursive mode, issuing this warning. -The caution is that any partial work done by the walker before the -exception was raised will be lost, potentially leaving the walker in an -inconsistent state. Users can avoid this by - -- avoiding recursive callbacks -- restructuring the system design to avoid triggering the walker with - few available stack frames -- directly calling the - :py:meth:`~pyomo.core.expr.visitor.StreamBasedExpressionVisitor.walk_expression_nonrecursive()` - walker method - -.. doctest:: - :skipif: (on_github_actions and system_info[0].startswith('win')) \ - or system_info[2] == 'PyPy' - - >>> import sys - >>> import pyomo.core.expr.visitor as visitor - >>> from pyomo.core.tests.unit.test_visitor import fill_stack - >>> expression_depth = visitor.StreamBasedExpressionVisitor( - ... exitNode=lambda node, data: max(data) + 1 if data else 1) - >>> m = pyo.ConcreteModel() - >>> m.x = pyo.Var() - >>> @m.Expression(range(35)) - ... def e(m, i): - ... return m.e[i-1] if i else m.x - >>> expression_depth.walk_expression(m.e[34]) - 36 - >>> fill_stack(sys.getrecursionlimit() - visitor.get_stack_depth() - 30, - ... expression_depth.walk_expression, - ... m.e[34]) - WARNING (W1003): Unexpected RecursionError walking an expression tree. - See also https://pyomo.readthedocs.io/en/stable/errors.html#w1003 - 36 - >>> fill_stack(sys.getrecursionlimit() - visitor.get_stack_depth() - 30, - ... expression_depth.walk_expression_nonrecursive, - ... m.e[34]) - 36 - - -.. =================================================================== -.. Extended descriptions for Pyomo errors -.. =================================================================== - -Errors ------- - -.. _E2001: - -E2001: Variable domains must be an instance of a Pyomo Set -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Variable domains are always Pyomo :class:`Set` or :class:`RangeSet` -objects. This includes global sets like ``Reals``, ``Integers``, -``Binary``, ``NonNegativeReals``, etc., as well as model-specific -:class:`Set` instances. The :attr:`Var.domain` setter will attempt to -convert assigned values to a Pyomo `Set`, with any failures leading to -this warning (and an exception from the converter): - -.. doctest:: - - >>> m = pyo.ConcreteModel() - >>> m.x = pyo.Var() - >>> m.x.domain = 5 - Traceback (most recent call last): - ... - TypeError: Cannot create a Set from data that does not support __contains__... - ERROR (E2001): 5 is not a valid domain. Variable domains must be an instance - of a Pyomo Set or convertible to a Pyomo Set. - See also https://pyomo.readthedocs.io/en/stable/errors.html#e2001 - - - -.. =================================================================== -.. Extended descriptions for Pyomo exceptions -.. =================================================================== - -.. Exceptions -.. ---------- - -.. .. _X101: diff --git a/doc/Archive/index.rst b/doc/Archive/index.rst deleted file mode 100644 index ef986a3429f..00000000000 --- a/doc/Archive/index.rst +++ /dev/null @@ -1,55 +0,0 @@ -Pyomo Documentation |release| -============================= - -.. image:: /../logos/pyomo/PyomoNewBlue3.png - :scale: 10% - :align: right - -Pyomo is a Python-based, open-source optimization modeling language -with a diverse set of optimization capabilities. - -.. toctree:: - :maxdepth: 2 - - installation.rst - citing_pyomo.rst - pyomo_overview/index.rst - pyomo_modeling_components/index.rst - solving_pyomo_models.rst - working_models.rst - working_abstractmodels/index.rst - model_transformations/index.rst - modeling_extensions/index.rst - tutorial_examples.rst - model_debugging/index.rst - advanced_topics/index.rst - errors.rst - developer_reference/index.rst - library_reference/index.rst - contribution_guide.rst - contributed_packages/index.rst - related_packages.rst - bibliography.rst - -Indices and Tables ------------------- - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - - -Pyomo Resources ---------------- - -The Pyomo home page provides resources for Pyomo users: - -* http://pyomo.org - -Pyomo development is hosted at GitHub: - -* https://github.com/Pyomo/pyomo - -See the Pyomo Forum for online discussions of Pyomo: - -* http://groups.google.com/group/pyomo-forum/ diff --git a/doc/Archive/installation.rst b/doc/Archive/installation.rst deleted file mode 100644 index 83cd08e7a4a..00000000000 --- a/doc/Archive/installation.rst +++ /dev/null @@ -1,99 +0,0 @@ -Installation ------------- - -Pyomo currently supports the following versions of Python: - -* CPython: 3.8, 3.9, 3.10, 3.11, 3.12 -* PyPy: 3 - -At the time of the first Pyomo release after the end-of-life of a minor Python -version, Pyomo will remove testing for that Python version. - -Using CONDA -~~~~~~~~~~~ - -We recommend installation with ``conda``, which is included with the -Anaconda distribution of Python. You can install Pyomo in your system -Python installation by executing the following in a shell: - -:: - - conda install -c conda-forge pyomo - -Optimization solvers are not installed with Pyomo, but some open source -optimization solvers can be installed with ``conda`` as well: - -:: - - conda install -c conda-forge ipopt glpk - - -Using PIP -~~~~~~~~~ - -The standard utility for installing Python packages is ``pip``. You -can install Pyomo in your system Python installation by executing -the following in a shell: - -:: - - pip install pyomo - - -Conditional Dependencies -~~~~~~~~~~~~~~~~~~~~~~~~ - -Extensions to Pyomo, and many of the contributions in ``pyomo.contrib``, -often have conditional dependencies on a variety of third-party Python -packages including but not limited to: matplotlib, networkx, numpy, -openpyxl, pandas, pint, pymysql, pyodbc, pyro4, scipy, sympy, and -xlrd. - -A full list of conditional dependencies can be found in Pyomo's -``setup.py`` and displayed using: - -:: - - python setup.py dependencies --extra optional - -Pyomo extensions that require any of these packages will generate -an error message for missing dependencies upon use. - -When using *pip*, all conditional dependencies can be installed at once -using the following command: - -:: - - pip install 'pyomo[optional]' - -When using *conda*, many of the conditional dependencies are included -with the standard Anaconda installation. - -You can check which Python packages you have installed using the command -``conda list`` or ``pip list``. Additional Python packages may be -installed as needed. - - -Installation with Cython -~~~~~~~~~~~~~~~~~~~~~~~~ - -Users can opt to install Pyomo with -`cython `_ -initialized. - -.. note:: - This can only be done via ``pip`` or from source. - -Via ``pip``: - -:: - - pip install pyomo --global-option="--with-cython" - -From source (recommended for advanced users only): - -:: - - git clone https://github.com/Pyomo/pyomo.git - cd pyomo - python setup.py install --with-cython diff --git a/doc/Archive/library_reference/aml/index.rst b/doc/Archive/library_reference/aml/index.rst deleted file mode 100644 index f06ca35b087..00000000000 --- a/doc/Archive/library_reference/aml/index.rst +++ /dev/null @@ -1,85 +0,0 @@ -AML Library Reference -===================== - -The following modeling components make up the core of the Pyomo -Algebraic Modeling Language (AML). These classes are all available -through the `pyomo.environ` namespace. - -.. currentmodule:: pyomo.environ - -.. autosummary:: - - ConcreteModel - AbstractModel - Block - Set - RangeSet - Param - Var - Objective - Constraint - ExternalFunction - Reference - SOSConstraint - - -AML Component Documentation -~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: ConcreteModel - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: AbstractModel - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: Block - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: Constraint - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: ExternalFunction - :show-inheritance: - :special-members: __init__ - :members: - :inherited-members: - -.. autoclass:: Objective - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: Param - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: RangeSet - :show-inheritance: - :members: - :inherited-members: - -.. autofunction:: Reference - -.. autoclass:: Set - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: Var - :show-inheritance: - :members: - :inherited-members: - -.. autoclass:: SOSConstraint - :show-inheritance: - :members: - :inherited-members: - diff --git a/doc/Archive/library_reference/appsi/appsi.base.rst b/doc/Archive/library_reference/appsi/appsi.base.rst deleted file mode 100644 index 1b6d5761182..00000000000 --- a/doc/Archive/library_reference/appsi/appsi.base.rst +++ /dev/null @@ -1,47 +0,0 @@ -APPSI Base Classes -================== - -.. autoclass:: pyomo.contrib.appsi.base.TerminationCondition - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pyomo.contrib.appsi.base.Results - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pyomo.contrib.appsi.base.Solver - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pyomo.contrib.appsi.base.PersistentSolver - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pyomo.contrib.appsi.base.SolverConfig - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - :exclude-members: NoArgument - -.. autoclass:: pyomo.contrib.appsi.base.MIPSolverConfig - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - :exclude-members: NoArgument - -.. autoclass:: pyomo.contrib.appsi.base.UpdateConfig - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - :exclude-members: NoArgument diff --git a/doc/Archive/library_reference/appsi/appsi.rst b/doc/Archive/library_reference/appsi/appsi.rst deleted file mode 100644 index e26e4b0e82a..00000000000 --- a/doc/Archive/library_reference/appsi/appsi.rst +++ /dev/null @@ -1,106 +0,0 @@ -.. _api_documentation: - -APPSI -===== - -Auto-Persistent Pyomo Solver Interfaces - -.. automodule:: pyomo.contrib.appsi - :members: - :show-inheritance: - -.. toctree:: - - appsi.base - appsi.solvers - -APPSI solver interfaces are designed to work very similarly to most -Pyomo solver interfaces but are very efficient for resolving the same -model with small changes. This is very beneficial for applications -such as Benders' Decomposition, Optimization-Based Bounds Tightening, -Progressive Hedging, Outer-Approximation, and many others. Here is an -example of using an APPSI solver interface. - -.. code-block:: python - - >>> import pyomo.environ as pe - >>> from pyomo.contrib import appsi - >>> import numpy as np - >>> from pyomo.common.timing import HierarchicalTimer - >>> m = pe.ConcreteModel() - >>> m.x = pe.Var() - >>> m.y = pe.Var() - >>> m.p = pe.Param(mutable=True) - >>> m.obj = pe.Objective(expr=m.x**2 + m.y**2) - >>> m.c1 = pe.Constraint(expr=m.y >= pe.exp(m.x)) - >>> m.c2 = pe.Constraint(expr=m.y >= (m.x - m.p)**2) - >>> opt = appsi.solvers.Ipopt() - >>> timer = HierarchicalTimer() - >>> for p_val in np.linspace(1, 10, 100): - >>> m.p.value = float(p_val) - >>> res = opt.solve(m, timer=timer) - >>> assert res.termination_condition == appsi.base.TerminationCondition.optimal - >>> print(res.best_feasible_objective) - >>> print(timer) - -Extra performance improvements can be made if you know exactly what -changes will be made in your model. In the example above, only -parameter values are changed, so we can setup the -:py:class:`~pyomo.contrib.appsi.base.UpdateConfig` so that the solver -does not check for changes in variables or constraints. - -.. code-block:: python - - >>> timer = HierarchicalTimer() - >>> opt.update_config.check_for_new_or_removed_constraints = False - >>> opt.update_config.check_for_new_or_removed_vars = False - >>> opt.update_config.update_constraints = False - >>> opt.update_config.update_vars = False - >>> for p_val in np.linspace(1, 10, 100): - >>> m.p.value = float(p_val) - >>> res = opt.solve(m, timer=timer) - >>> assert res.termination_condition == appsi.base.TerminationCondition.optimal - >>> print(res.best_feasible_objective) - >>> print(timer) - -Solver independent options can be specified with the -:py:class:`~pyomo.contrib.appsi.base.SolverConfig` or derived -classes. For example: - -.. code-block:: python - - >>> opt.config.stream_solver = True - -Solver specific options can be specified with the -:py:meth:`~pyomo.contrib.appsi.base.Solver.solver_options` -attribute. For example: - -.. code-block:: python - - >>> opt.solver_options['max_iter'] = 20 - -Installation ------------- -There are a few ways to install Appsi listed below. - -Option1: - -.. code-block:: - - pyomo build-extensions - -Option2: - -.. code-block:: - - cd pyomo/contrib/appsi/ - python build.py - -Option3: - -.. code-block:: - - python - >>> from pyomo.contrib.appsi.build import build_appsi - >>> build_appsi() - diff --git a/doc/Archive/library_reference/appsi/appsi.solvers.cbc.rst b/doc/Archive/library_reference/appsi/appsi.solvers.cbc.rst deleted file mode 100644 index a0a2f7d0f27..00000000000 --- a/doc/Archive/library_reference/appsi/appsi.solvers.cbc.rst +++ /dev/null @@ -1,15 +0,0 @@ -Cbc -=== - -.. autoclass:: pyomo.contrib.appsi.solvers.cbc.CbcConfig - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - :exclude-members: NoArgument - -.. autoclass:: pyomo.contrib.appsi.solvers.cbc.Cbc - :members: - :inherited-members: - :undoc-members: - :show-inheritance: diff --git a/doc/Archive/library_reference/appsi/appsi.solvers.cplex.rst b/doc/Archive/library_reference/appsi/appsi.solvers.cplex.rst deleted file mode 100644 index 0906fd7ea76..00000000000 --- a/doc/Archive/library_reference/appsi/appsi.solvers.cplex.rst +++ /dev/null @@ -1,21 +0,0 @@ -Cplex -===== - -.. autoclass:: pyomo.contrib.appsi.solvers.cplex.CplexConfig - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - :exclude-members: NoArgument - -.. autoclass:: pyomo.contrib.appsi.solvers.cplex.CplexResults - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pyomo.contrib.appsi.solvers.cplex.Cplex - :members: - :inherited-members: - :undoc-members: - :show-inheritance: diff --git a/doc/Archive/library_reference/appsi/appsi.solvers.gurobi.rst b/doc/Archive/library_reference/appsi/appsi.solvers.gurobi.rst deleted file mode 100644 index 9e0af041410..00000000000 --- a/doc/Archive/library_reference/appsi/appsi.solvers.gurobi.rst +++ /dev/null @@ -1,55 +0,0 @@ -Gurobi -====== - - -Handling Gurobi licenses through the APPSI interface ----------------------------------------------------- - -In order to obtain performance benefits when re-solving a Pyomo model -with Gurobi repeatedly, Pyomo has to keep a reference to a gurobipy -model between calls to -:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.solve()`. Depending -on the Gurobi license type, this may "consume" a license as long as -any APPSI-Gurobi interface exists (i.e., has not been garbage -collected). To release a Gurobi license for other processes, use the -:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.release_license()` -method as shown below. Note that -:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.release_license()` -must be called on every instance for this to actually release the -license. However, releasing the license will delete the gurobipy model -which will have to be reconstructed from scratch the next time -:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.solve()` is -called, negating any performance benefit of the persistent solver -interface. - -.. code-block:: python - - >>> opt = appsi.solvers.Gurobi() # doctest: +SKIP - >>> results = opt.solve(model) # doctest: +SKIP - >>> opt.release_license() # doctest: +SKIP - - -Also note that both the -:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.available()` and -:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.solve()` methods -will construct a gurobipy model, thereby (depending on the type of -license) "consuming" a license. The -:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.available()` -method has to do this so that the availability does not change between -calls to -:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.available()` and -:py:meth:`~pyomo.contrib.appsi.solvers.gurobi.Gurobi.solve()`, leading -to unexpected errors. - - -.. autoclass:: pyomo.contrib.appsi.solvers.gurobi.GurobiResults - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pyomo.contrib.appsi.solvers.gurobi.Gurobi - :members: - :inherited-members: - :undoc-members: - :show-inheritance: diff --git a/doc/Archive/library_reference/appsi/appsi.solvers.highs.rst b/doc/Archive/library_reference/appsi/appsi.solvers.highs.rst deleted file mode 100644 index f2f72d0ad85..00000000000 --- a/doc/Archive/library_reference/appsi/appsi.solvers.highs.rst +++ /dev/null @@ -1,14 +0,0 @@ -HiGHS -===== - -.. autoclass:: pyomo.contrib.appsi.solvers.highs.HighsResults - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pyomo.contrib.appsi.solvers.highs.Highs - :members: - :inherited-members: - :undoc-members: - :show-inheritance: diff --git a/doc/Archive/library_reference/appsi/appsi.solvers.ipopt.rst b/doc/Archive/library_reference/appsi/appsi.solvers.ipopt.rst deleted file mode 100644 index 0d095644100..00000000000 --- a/doc/Archive/library_reference/appsi/appsi.solvers.ipopt.rst +++ /dev/null @@ -1,14 +0,0 @@ -Ipopt -===== - -.. autoclass:: pyomo.contrib.appsi.solvers.ipopt.IpoptConfig - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pyomo.contrib.appsi.solvers.ipopt.Ipopt - :members: - :inherited-members: - :undoc-members: - :show-inheritance: diff --git a/doc/Archive/library_reference/appsi/appsi.solvers.maingo.rst b/doc/Archive/library_reference/appsi/appsi.solvers.maingo.rst deleted file mode 100644 index 21e61c38d51..00000000000 --- a/doc/Archive/library_reference/appsi/appsi.solvers.maingo.rst +++ /dev/null @@ -1,14 +0,0 @@ -MAiNGO -====== - -.. autoclass:: pyomo.contrib.appsi.solvers.maingo.MAiNGOConfig - :members: - :inherited-members: - :undoc-members: - :show-inheritance: - -.. autoclass:: pyomo.contrib.appsi.solvers.maingo.MAiNGO - :members: - :inherited-members: - :undoc-members: - :show-inheritance: diff --git a/doc/Archive/library_reference/appsi/appsi.solvers.rst b/doc/Archive/library_reference/appsi/appsi.solvers.rst deleted file mode 100644 index f4dcb81b4be..00000000000 --- a/doc/Archive/library_reference/appsi/appsi.solvers.rst +++ /dev/null @@ -1,16 +0,0 @@ -Solvers -======= - -.. automodule:: pyomo.contrib.appsi.solvers - :members: - :show-inheritance: - :undoc-members: - -.. toctree:: - - appsi.solvers.gurobi - appsi.solvers.ipopt - appsi.solvers.cplex - appsi.solvers.cbc - appsi.solvers.highs - appsi.solvers.maingo diff --git a/doc/Archive/library_reference/common/config.rst b/doc/Archive/library_reference/common/config.rst deleted file mode 100644 index c5dc607977a..00000000000 --- a/doc/Archive/library_reference/common/config.rst +++ /dev/null @@ -1,85 +0,0 @@ -pyomo.common.config -=================== - -.. currentmodule:: pyomo.common.config - -Core classes -~~~~~~~~~~~~ - -.. autosummary:: - - ConfigDict - ConfigList - ConfigValue - -Utilities -~~~~~~~~~ - -.. autosummary:: - - document_kwargs_from_configdict - - -Domain validators -~~~~~~~~~~~~~~~~~ - -.. autosummary:: - - Bool - Integer - PositiveInt - NegativeInt - NonNegativeInt - NonPositiveInt - PositiveFloat - NegativeFloat - NonPositiveFloat - NonNegativeFloat - In - IsInstance - InEnum - ListOf - Module - Path - PathList - DynamicImplicitDomain - -.. autoclass:: ConfigBase - :members: - :undoc-members: - -.. autoclass:: ConfigDict - :show-inheritance: - :members: - :undoc-members: - -.. autoclass:: ConfigList - :show-inheritance: - :members: - :undoc-members: - -.. autoclass:: ConfigValue - :show-inheritance: - :members: - :undoc-members: - -.. autodecorator:: document_kwargs_from_configdict - -.. autofunction:: Bool -.. autofunction:: Integer -.. autofunction:: PositiveInt -.. autofunction:: NegativeInt -.. autofunction:: NonNegativeInt -.. autofunction:: NonPositiveInt -.. autofunction:: PositiveFloat -.. autofunction:: NegativeFloat -.. autofunction:: NonPositiveFloat -.. autofunction:: NonNegativeFloat -.. autoclass:: In -.. autoclass:: IsInstance -.. autoclass:: InEnum -.. autoclass:: ListOf -.. autoclass:: Module -.. autoclass:: Path -.. autoclass:: PathList -.. autoclass:: DynamicImplicitDomain diff --git a/doc/Archive/library_reference/common/dependencies.rst b/doc/Archive/library_reference/common/dependencies.rst deleted file mode 100644 index 18d5647681c..00000000000 --- a/doc/Archive/library_reference/common/dependencies.rst +++ /dev/null @@ -1,7 +0,0 @@ - -pyomo.common.dependencies -========================= - -.. automodule:: pyomo.common.dependencies - :members: - :member-order: bysource diff --git a/doc/Archive/library_reference/common/deprecation.rst b/doc/Archive/library_reference/common/deprecation.rst deleted file mode 100644 index 41066c040c4..00000000000 --- a/doc/Archive/library_reference/common/deprecation.rst +++ /dev/null @@ -1,6 +0,0 @@ -pyomo.common.deprecation -======================== - -.. automodule:: pyomo.common.deprecation - :members: - :member-order: bysource diff --git a/doc/Archive/library_reference/common/enums.rst b/doc/Archive/library_reference/common/enums.rst deleted file mode 100644 index 5ed2dbb1e80..00000000000 --- a/doc/Archive/library_reference/common/enums.rst +++ /dev/null @@ -1,7 +0,0 @@ - -pyomo.common.enums -================== - -.. automodule:: pyomo.common.enums - :members: - :member-order: bysource diff --git a/doc/Archive/library_reference/common/errors.rst b/doc/Archive/library_reference/common/errors.rst deleted file mode 100644 index 7b2bd01fe32..00000000000 --- a/doc/Archive/library_reference/common/errors.rst +++ /dev/null @@ -1,6 +0,0 @@ -pyomo.common.errors -=================== - -.. automodule:: pyomo.common.errors - :members: - :member-order: bysource diff --git a/doc/Archive/library_reference/common/fileutils.rst b/doc/Archive/library_reference/common/fileutils.rst deleted file mode 100644 index e582f4c2e94..00000000000 --- a/doc/Archive/library_reference/common/fileutils.rst +++ /dev/null @@ -1,6 +0,0 @@ -pyomo.common.fileutils -====================== - -.. automodule:: pyomo.common.fileutils - :members: - :member-order: bysource diff --git a/doc/Archive/library_reference/common/formatting.rst b/doc/Archive/library_reference/common/formatting.rst deleted file mode 100644 index 25f0ef2404c..00000000000 --- a/doc/Archive/library_reference/common/formatting.rst +++ /dev/null @@ -1,6 +0,0 @@ -pyomo.common.formatting -======================= - -.. automodule:: pyomo.common.formatting - :members: - :member-order: bysource diff --git a/doc/Archive/library_reference/common/index.rst b/doc/Archive/library_reference/common/index.rst deleted file mode 100644 index c03436600f2..00000000000 --- a/doc/Archive/library_reference/common/index.rst +++ /dev/null @@ -1,19 +0,0 @@ -Common Utilities -================ - -Pyomo provides a set of general-purpose utilities through -``pyomo.common``. These utilities are self-contained and do not import -or rely on any other parts of Pyomo. - -.. toctree:: - :maxdepth: 1 - - config.rst - dependencies.rst - deprecation.rst - enums.rst - errors.rst - fileutils.rst - formatting.rst - tempfiles.rst - timing.rst diff --git a/doc/Archive/library_reference/common/tempfiles.rst b/doc/Archive/library_reference/common/tempfiles.rst deleted file mode 100644 index 03cb056dffe..00000000000 --- a/doc/Archive/library_reference/common/tempfiles.rst +++ /dev/null @@ -1,7 +0,0 @@ - -pyomo.common.tempfiles -====================== - -.. automodule:: pyomo.common.tempfiles - :members: - :member-order: bysource diff --git a/doc/Archive/library_reference/common/timing.rst b/doc/Archive/library_reference/common/timing.rst deleted file mode 100644 index 06b6fc0f588..00000000000 --- a/doc/Archive/library_reference/common/timing.rst +++ /dev/null @@ -1,7 +0,0 @@ - -pyomo.common.timing -=================== - -.. automodule:: pyomo.common.timing - :members: - :member-order: bysource diff --git a/doc/Archive/library_reference/data/index.rst b/doc/Archive/library_reference/data/index.rst deleted file mode 100644 index fffb06240f8..00000000000 --- a/doc/Archive/library_reference/data/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Model Data Management -===================== - -.. autoclass:: pyomo.dataportal.DataPortal.DataPortal - :members: - :special-members: - -.. autoclass:: pyomo.dataportal.TableData.TableData - :members: - :special-members: - diff --git a/doc/Archive/library_reference/expressions/building.rst b/doc/Archive/library_reference/expressions/building.rst deleted file mode 100644 index 8ffcca9e310..00000000000 --- a/doc/Archive/library_reference/expressions/building.rst +++ /dev/null @@ -1,10 +0,0 @@ - -Utilities to Build Expressions -============================== - -.. autofunction:: pyomo.core.util.prod -.. autofunction:: pyomo.core.util.quicksum -.. autofunction:: pyomo.core.util.sum_product -.. autodata:: pyomo.core.util.summation -.. autodata:: pyomo.core.util.dot_product - diff --git a/doc/Archive/library_reference/expressions/classes.rst b/doc/Archive/library_reference/expressions/classes.rst deleted file mode 100644 index 4d448d2da6a..00000000000 --- a/doc/Archive/library_reference/expressions/classes.rst +++ /dev/null @@ -1,105 +0,0 @@ -Core Classes -============ - -The following are the two core classes documented here: - - * :class:`NumericValue` - * :class:`NumericExpression` - -The remaining classes are the public classes for expressions, which -developers may need to know about. The methods for these classes are not -documented because they are described in the -:class:`NumericExpression` class. - -Sets with Expression Types --------------------------- - -The following sets can be used to develop visitor patterns for -Pyomo expressions. - -.. autodata:: pyomo.core.expr.numvalue.native_numeric_types -.. autodata:: pyomo.core.expr.numvalue.native_types -.. autodata:: pyomo.core.expr.numvalue.nonpyomo_leaf_types - -NumericValue and NumericExpression ----------------------------------- - -.. autoclass:: pyomo.core.expr.numvalue.NumericValue - :members: - :special-members: - :private-members: - -.. autoclass:: pyomo.core.expr.NumericExpression - :members: - :show-inheritance: - :special-members: - :private-members: - -Other Public Classes --------------------- - -.. autoclass:: pyomo.core.expr.NegationExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.ExternalFunctionExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.ProductExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.DivisionExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.InequalityExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.EqualityExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.SumExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.GetItemExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.Expr_ifExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.UnaryFunctionExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: - -.. autoclass:: pyomo.core.expr.AbsExpression - :members: - :show-inheritance: - :undoc-members: - :private-members: diff --git a/doc/Archive/library_reference/expressions/context_managers.rst b/doc/Archive/library_reference/expressions/context_managers.rst deleted file mode 100644 index ae6884d684f..00000000000 --- a/doc/Archive/library_reference/expressions/context_managers.rst +++ /dev/null @@ -1,10 +0,0 @@ - -Context Managers -================ - -.. autoclass:: pyomo.core.expr.nonlinear_expression - :members: - -.. autoclass:: pyomo.core.expr.linear_expression - :members: - diff --git a/doc/Archive/library_reference/expressions/index.rst b/doc/Archive/library_reference/expressions/index.rst deleted file mode 100644 index 388a7efa452..00000000000 --- a/doc/Archive/library_reference/expressions/index.rst +++ /dev/null @@ -1,13 +0,0 @@ - -Expression Reference -==================== - -.. toctree:: - :maxdepth: 1 - - building.rst - managing.rst - context_managers.rst - classes.rst - visitors.rst - diff --git a/doc/Archive/library_reference/expressions/managing.rst b/doc/Archive/library_reference/expressions/managing.rst deleted file mode 100644 index 369dd3aace1..00000000000 --- a/doc/Archive/library_reference/expressions/managing.rst +++ /dev/null @@ -1,19 +0,0 @@ - -Utilities to Manage and Analyze Expressions -=========================================== - -Functions -~~~~~~~~~ - -.. autofunction:: pyomo.core.expr.expression_to_string -.. autofunction:: pyomo.core.expr.decompose_term -.. autofunction:: pyomo.core.expr.clone_expression -.. autofunction:: pyomo.core.expr.evaluate_expression -.. autofunction:: pyomo.core.expr.identify_components -.. autofunction:: pyomo.core.expr.identify_variables -.. autofunction:: pyomo.core.expr.differentiate - -Classes -~~~~~~~ - -.. autoclass:: pyomo.core.expr.symbol_map.SymbolMap diff --git a/doc/Archive/library_reference/expressions/visitors.rst b/doc/Archive/library_reference/expressions/visitors.rst deleted file mode 100644 index 77cffe7905f..00000000000 --- a/doc/Archive/library_reference/expressions/visitors.rst +++ /dev/null @@ -1,20 +0,0 @@ - -Visitor Classes -=============== - -.. autoclass:: pyomo.core.expr.StreamBasedExpressionVisitor - :members: - :inherited-members: - -.. autoclass:: pyomo.core.expr.SimpleExpressionVisitor - :members: - :inherited-members: - -.. autoclass:: pyomo.core.expr.ExpressionValueVisitor - :members: - :inherited-members: - -.. autoclass:: pyomo.core.expr.ExpressionReplacementVisitor - :members: - :inherited-members: - diff --git a/doc/Archive/library_reference/index.rst b/doc/Archive/library_reference/index.rst deleted file mode 100644 index 35dd8d30307..00000000000 --- a/doc/Archive/library_reference/index.rst +++ /dev/null @@ -1,27 +0,0 @@ -Library Reference -================= - -Pyomo is being increasingly used as a library to support Python -scripts. This section describes library APIs for key elements of -Pyomo's core library. This documentation serves as a reference for -both (1) Pyomo developers and (2) advanced users who are developing -Python scripts using Pyomo. - -.. toctree:: - :maxdepth: 1 - - common/index.rst - aml/index.rst - expressions/index.rst - solvers/index.rst - data/index.rst - APPSI (Auto-Persistent Pyomo Solver Interfaces) - -Pyomo is under active ongoing development. The following API -documentation describes *Beta* functionality. - -.. toctree:: - :maxdepth: 1 - - kernel/index.rst - diff --git a/doc/Archive/library_reference/kernel/base.rst b/doc/Archive/library_reference/kernel/base.rst deleted file mode 100644 index 47a2afef68d..00000000000 --- a/doc/Archive/library_reference/kernel/base.rst +++ /dev/null @@ -1,6 +0,0 @@ -Base Object Storage Interface -============================= - -.. automodule:: pyomo.core.kernel.base - :show-inheritance: - :members: diff --git a/doc/Archive/library_reference/kernel/block.rst b/doc/Archive/library_reference/kernel/block.rst deleted file mode 100644 index a61c12610eb..00000000000 --- a/doc/Archive/library_reference/kernel/block.rst +++ /dev/null @@ -1,26 +0,0 @@ -Blocks -====== - -Summary -~~~~~~~ -.. autosummary:: - - pyomo.core.kernel.block.block - pyomo.core.kernel.block.block_tuple - pyomo.core.kernel.block.block_list - pyomo.core.kernel.block.block_dict - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.block.block - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.block.block_tuple - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.block.block_list - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.block.block_dict - :show-inheritance: - :members: diff --git a/doc/Archive/library_reference/kernel/conic.rst b/doc/Archive/library_reference/kernel/conic.rst deleted file mode 100644 index 34552013623..00000000000 --- a/doc/Archive/library_reference/kernel/conic.rst +++ /dev/null @@ -1,42 +0,0 @@ -Conic Constraints -================= - -A collection of classes that provide an easy and performant -way to declare conic constraints. The Mosek solver interface -includes special handling of these objects that recognizes -them as convex constraints. Other solver interfaces will -treat these objects as general nonlinear or quadratic -expressions, and may or may not have the ability to identify -their convexity. - -Summary -~~~~~~~ -.. autosummary:: - - pyomo.core.kernel.conic.quadratic - pyomo.core.kernel.conic.rotated_quadratic - pyomo.core.kernel.conic.primal_exponential - pyomo.core.kernel.conic.primal_power - pyomo.core.kernel.conic.dual_exponential - pyomo.core.kernel.conic.dual_power - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.conic.quadratic - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.conic.rotated_quadratic - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.conic.primal_exponential - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.conic.primal_power - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.conic.dual_exponential - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.conic.dual_power - :show-inheritance: - :members: diff --git a/doc/Archive/library_reference/kernel/constraint.rst b/doc/Archive/library_reference/kernel/constraint.rst deleted file mode 100644 index 1645e57f9f2..00000000000 --- a/doc/Archive/library_reference/kernel/constraint.rst +++ /dev/null @@ -1,34 +0,0 @@ -Constraints -=========== - -Summary -~~~~~~~ -.. autosummary:: - - pyomo.core.kernel.constraint.constraint - pyomo.core.kernel.constraint.linear_constraint - pyomo.core.kernel.constraint.constraint_tuple - pyomo.core.kernel.constraint.constraint_list - pyomo.core.kernel.constraint.constraint_dict - pyomo.core.kernel.matrix_constraint.matrix_constraint - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.constraint.constraint - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.constraint.linear_constraint - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.constraint.constraint_tuple - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.constraint.constraint_list - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.constraint.constraint_dict - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.matrix_constraint.matrix_constraint - :show-inheritance: - :members: diff --git a/doc/Archive/library_reference/kernel/dict_container.rst b/doc/Archive/library_reference/kernel/dict_container.rst deleted file mode 100644 index 6e710fa76eb..00000000000 --- a/doc/Archive/library_reference/kernel/dict_container.rst +++ /dev/null @@ -1,8 +0,0 @@ -Dict-like Object Storage -======================== - -.. autoclass:: pyomo.core.kernel.dict_container.DictContainer - :show-inheritance: - :members: - :inherited-members: - :special-members: diff --git a/doc/Archive/library_reference/kernel/examples/aml_example.py b/doc/Archive/library_reference/kernel/examples/aml_example.py deleted file mode 100644 index a640b94cc76..00000000000 --- a/doc/Archive/library_reference/kernel/examples/aml_example.py +++ /dev/null @@ -1,193 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# @Import_Syntax -import pyomo.environ as aml - -# @Import_Syntax - -datafile = None - -# @AbstractModels -m = aml.AbstractModel() -# ... define model ... -instance = m.create_instance(datafile) - -# @AbstractModels -del datafile -del instance -# @ConcreteModels -m = aml.ConcreteModel() -m.b = aml.Block() -# @ConcreteModels - - -# @Sets_1 -m.s = aml.Set(initialize=[1, 2], ordered=True) -# @Sets_1 -# @Sets_2 -# [1,2,3] -m.q = aml.RangeSet(1, 3) -# @Sets_2 - - -# @Parameters_single -m.p = aml.Param(mutable=True, initialize=0) - - -# @Parameters_single -# @Parameters_dict -# pd[1] = 0, pd[2] = 1 -def pd_(m, i): - return m.s.ord(i) - 1 - - -m.pd = aml.Param(m.s, mutable=True, rule=pd_) -# @Parameters_dict -# @Parameters_list - -# -# No ParamList exists -# - - -# @Parameters_list - - -# @Variables_single -m.v = aml.Var(initialize=1.0, bounds=(1, 4)) - -# @Variables_single -# @Variables_dict -m.vd = aml.Var(m.s, bounds=(None, 9)) - - -# @Variables_dict -# @Variables_list -# used 1-based indexing -def vl_(m, i): - return (i, None) - - -m.vl = aml.VarList(bounds=vl_) -for j in m.q: - m.vl.add() -# @Variables_list - -# @Constraints_single -m.c = aml.Constraint(expr=sum(m.vd.values()) <= 9) - - -# @Constraints_single -# @Constraints_dict -def cd_(m, i, j): - return m.vd[i] == j - - -m.cd = aml.Constraint(m.s, m.q, rule=cd_) - - -# @Constraints_dict -# @Constraints_list -# uses 1-based indexing -m.cl = aml.ConstraintList() -for j in m.q: - m.cl.add(aml.inequality(-5, m.vl[j] - m.v, 5)) -# @Constraints_list - - -# @Expressions_single -m.e = aml.Expression(expr=-m.v) - - -# @Expressions_single -# @Expressions_dict -def ed_(m, i): - return -m.vd[i] - - -m.ed = aml.Expression(m.s, rule=ed_) -# @Expressions_dict -# @Expressions_list - -# -# No ExpressionList exists -# - -# @Expressions_list - - -# @Objectives_single -m.o = aml.Objective(expr=-m.v) - - -# @Objectives_single -# @Objectives_dict -def od_(m, i): - return -m.vd[i] - - -m.od = aml.Objective(m.s, rule=od_) -# @Objectives_dict -# @Objectives_list -# uses 1-based indexing -m.ol = aml.ObjectiveList() -for j in m.q: - m.ol.add(-m.vl[j]) - -# @Objectives_list - - -# @SOS_single -m.sos1 = aml.SOSConstraint(var=m.vl, level=1) -m.sos2 = aml.SOSConstraint(var=m.vd, level=2) - - -# @SOS_single -# @SOS_dict -def sd_(m, i): - if i == 1: - t = list(m.vd.values()) - elif i == 2: - t = list(m.vl.values()) - return t - - -m.sd = aml.SOSConstraint([1, 2], rule=sd_, level=1) -# @SOS_dict -# @SOS_list - -# -# No SOSConstraintList exists -# - -# @SOS_list - - -# @Suffix_single -m.dual = aml.Suffix(direction=aml.Suffix.IMPORT) -# @Suffix_single -# @Suffix_dict -# -# No SuffixDict exists -# -# @Suffix_dict - - -# @Piecewise_1d -breakpoints = [1, 2, 3, 4] -values = [1, 2, 1, 2] -m.f = aml.Var() -m.pw = aml.Piecewise(m.f, m.v, pw_pts=breakpoints, f_rule=values, pw_constr_type='EQ') -# @Piecewise_1d - - -m.pprint() diff --git a/doc/Archive/library_reference/kernel/examples/conic.py b/doc/Archive/library_reference/kernel/examples/conic.py deleted file mode 100644 index 0418d188722..00000000000 --- a/doc/Archive/library_reference/kernel/examples/conic.py +++ /dev/null @@ -1,33 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# @Class -import pyomo.kernel as pmo - -m = pmo.block() -m.x1 = pmo.variable(lb=0) -m.x2 = pmo.variable() -m.r = pmo.variable(lb=0) -m.q = pmo.conic.primal_exponential(x1=m.x1, x2=m.x2, r=m.r) -# @Class -del m - -# @Domain -import pyomo.kernel as pmo -import math - -m = pmo.block() -m.x = pmo.variable(lb=0) -m.y = pmo.variable(lb=0) -m.b = pmo.conic.primal_exponential.as_domain( - x1=math.sqrt(2) * m.x, x2=2.0, r=2 * (m.x + m.y) -) -# @Domain diff --git a/doc/Archive/library_reference/kernel/examples/kernel_containers.py b/doc/Archive/library_reference/kernel/examples/kernel_containers.py deleted file mode 100644 index 1931c6d9b56..00000000000 --- a/doc/Archive/library_reference/kernel/examples/kernel_containers.py +++ /dev/null @@ -1,18 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pyomo.kernel - -# @all -vlist = pyomo.kernel.variable_list() -vlist.append(pyomo.kernel.variable_dict()) -vlist[0]['x'] = pyomo.kernel.variable() -# @all diff --git a/doc/Archive/library_reference/kernel/examples/kernel_example.py b/doc/Archive/library_reference/kernel/examples/kernel_example.py deleted file mode 100644 index 1f80bce9788..00000000000 --- a/doc/Archive/library_reference/kernel/examples/kernel_example.py +++ /dev/null @@ -1,174 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -# @Import_Syntax -import pyomo.kernel as pmo - -# @Import_Syntax - -data = None - - -# @AbstractModels -def create(data): - instance = pmo.block() - # ... define instance ... - return instance - - -instance = create(data) -# @AbstractModels -del data -del instance -# @ConcreteModels -m = pmo.block() -m.b = pmo.block() -# @ConcreteModels - - -# @Sets_1 -m.s = [1, 2] - -# @Sets_1 -# @Sets_2 -# [0,1,2] -m.q = range(3) -# @Sets_2 - - -# @Parameters_single -m.p = pmo.parameter(0) - -# @Parameters_single -# @Parameters_dict -# pd[1] = 0, pd[2] = 1 -m.pd = pmo.parameter_dict() -for k, i in enumerate(m.s): - m.pd[i] = pmo.parameter(k) - - -# @Parameters_dict -# @Parameters_list -# uses 0-based indexing -# pl[0] = 0, pl[0] = 1, ... -m.pl = pmo.parameter_list() -for j in m.q: - m.pl.append(pmo.parameter(j)) -# @Parameters_list - - -# @Variables_single -m.v = pmo.variable(value=1, lb=1, ub=4) -# @Variables_single -# @Variables_dict -m.vd = pmo.variable_dict() -for i in m.s: - m.vd[i] = pmo.variable(ub=9) -# @Variables_dict -# @Variables_list -# used 0-based indexing -m.vl = pmo.variable_list() -for j in m.q: - m.vl.append(pmo.variable(lb=i)) - -# @Variables_list - - -# @Constraints_single -m.c = pmo.constraint(sum(m.vd.values()) <= 9) -# @Constraints_single -# @Constraints_dict -m.cd = pmo.constraint_dict() -for i in m.s: - for j in m.q: - m.cd[i, j] = pmo.constraint(body=m.vd[i], rhs=j) -# @Constraints_dict -# @Constraints_list -# uses 0-based indexing -m.cl = pmo.constraint_list() -for j in m.q: - m.cl.append(pmo.constraint(lb=-5, body=m.vl[j] - m.v, ub=5)) -# @Constraints_list - - -# @Expressions_single -m.e = pmo.expression(-m.v) -# @Expressions_single -# @Expressions_dict -m.ed = pmo.expression_dict() -for i in m.s: - m.ed[i] = pmo.expression(-m.vd[i]) -# @Expressions_dict -# @Expressions_list -# uses 0-based indexed -m.el = pmo.expression_list() -for j in m.q: - m.el.append(pmo.expression(-m.vl[j])) -# @Expressions_list - - -# @Objectives_single -m.o = pmo.objective(-m.v) -# @Objectives_single -# @Objectives_dict -m.od = pmo.objective_dict() -for i in m.s: - m.od[i] = pmo.objective(-m.vd[i]) -# @Objectives_dict -# @Objectives_list -# uses 0-based indexing -m.ol = pmo.objective_list() -for j in m.q: - m.ol.append(pmo.objective(-m.vl[j])) -# @Objectives_list - - -# @SOS_single -m.sos1 = pmo.sos1(m.vd.values()) - - -m.sos2 = pmo.sos2(m.vl) - - -# @SOS_single -# @SOS_dict -m.sd = pmo.sos_dict() -m.sd[1] = pmo.sos1(m.vd.values()) -m.sd[2] = pmo.sos1(m.vl) - - -# @SOS_dict -# @SOS_list -# uses 0-based indexing -m.sl = pmo.sos_list() -for i in m.s: - m.sl.append(pmo.sos1([m.vl[i], m.vd[i]])) -# @SOS_list - - -# @Suffix_single -m.dual = pmo.suffix(direction=pmo.suffix.IMPORT) -# @Suffix_single -# @Suffix_dict -m.suffixes = pmo.suffix_dict() -m.suffixes['dual'] = pmo.suffix(direction=pmo.suffix.IMPORT) -# @Suffix_dict - - -# @Piecewise_1d -breakpoints = [1, 2, 3, 4] -values = [1, 2, 1, 2] -m.f = pmo.variable() -m.pw = pmo.piecewise(breakpoints, values, input=m.v, output=m.f, bound='eq') -# @Piecewise_1d - - -pmo.pprint(m) diff --git a/doc/Archive/library_reference/kernel/examples/kernel_solving.py b/doc/Archive/library_reference/kernel/examples/kernel_solving.py deleted file mode 100644 index 13d7efc052a..00000000000 --- a/doc/Archive/library_reference/kernel/examples/kernel_solving.py +++ /dev/null @@ -1,22 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pyomo.kernel as pmo - -model = pmo.block() -model.x = pmo.variable() -model.c = pmo.constraint(model.x >= 1) -model.o = pmo.objective(model.x) - -opt = pmo.SolverFactory("ipopt") - -result = opt.solve(model) -assert str(result.solver.termination_condition) == "optimal" diff --git a/doc/Archive/library_reference/kernel/examples/kernel_subclassing.py b/doc/Archive/library_reference/kernel/examples/kernel_subclassing.py deleted file mode 100644 index d6e38f6b0e0..00000000000 --- a/doc/Archive/library_reference/kernel/examples/kernel_subclassing.py +++ /dev/null @@ -1,93 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pyomo.kernel - - -# @Nonnegative -class NonNegativeVariable(pyomo.kernel.variable): - """A non-negative variable.""" - - __slots__ = () - - def __init__(self, **kwds): - if 'lb' not in kwds: - kwds['lb'] = 0 - if kwds['lb'] < 0: - raise ValueError("lower bound must be non-negative") - super(NonNegativeVariable, self).__init__(**kwds) - - # - # restrict assignments to x.lb to non-negative numbers - # - @property - def lb(self): - # calls the base class property getter - return pyomo.kernel.variable.lb.fget(self) - - @lb.setter - def lb(self, lb): - if lb < 0: - raise ValueError("lower bound must be non-negative") - # calls the base class property setter - pyomo.kernel.variable.lb.fset(self, lb) - - -# @Nonnegative - - -# @Point -class Point(pyomo.kernel.variable_tuple): - """A 3-dimensional point in Cartesian space with the - z coordinate restricted to non-negative values.""" - - __slots__ = () - - def __init__(self): - super(Point, self).__init__( - (pyomo.kernel.variable(), pyomo.kernel.variable(), NonNegativeVariable()) - ) - - @property - def x(self): - return self[0] - - @property - def y(self): - return self[1] - - @property - def z(self): - return self[2] - - -# @Point - - -# @SOC -class SOC(pyomo.kernel.constraint): - """A convex second-order cone constraint""" - - __slots__ = () - - def __init__(self, point): - assert isinstance(point.z, NonNegativeVariable) - super(SOC, self).__init__(point.x**2 + point.y**2 <= point.z**2) - - -# @SOC - -# @Usage -model = pyomo.kernel.block() -model.p = Point() -model.p.z.lb = 0 -model.soc = SOC(model.p) -# @Usage diff --git a/doc/Archive/library_reference/kernel/examples/transformer.py b/doc/Archive/library_reference/kernel/examples/transformer.py deleted file mode 100644 index 43a1d0675bf..00000000000 --- a/doc/Archive/library_reference/kernel/examples/transformer.py +++ /dev/null @@ -1,66 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ - -import pyomo.environ -import pyomo.kernel - -import pympler.asizeof - - -def _fmt(num, suffix='B'): - """format memory output""" - if num is None: - return "" - for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']: - if abs(num) < 1000.0: - return "%3.1f %s%s" % (num, unit, suffix) - num /= 1000.0 - return "%.1f %s%s" % (num, 'Yi', suffix) - - -# @kernel -class Transformer(pyomo.kernel.block): - def __init__(self): - super(Transformer, self).__init__() - self._a = pyomo.kernel.parameter() - self._v_in = pyomo.kernel.expression() - self._v_out = pyomo.kernel.expression() - self._c = pyomo.kernel.constraint(self._a * self._v_out == self._v_in) - - def set_ratio(self, a): - assert a > 0 - self._a.value = a - - def connect_v_in(self, v_in): - self._v_in.expr = v_in - - def connect_v_out(self, v_out): - self._v_out.expr = v_out - - -# @kernel - -print("Memory:", _fmt(pympler.asizeof.asizeof(Transformer()))) - - -# @aml -def Transformer(): - b = pyomo.environ.Block(concrete=True) - b._a = pyomo.environ.Param(mutable=True) - b._v_in = pyomo.environ.Expression() - b._v_out = pyomo.environ.Expression() - b._c = pyomo.environ.Constraint(expr=b._a * b._v_out == b._v_in) - return b - - -# @aml - -print("Memory:", _fmt(pympler.asizeof.asizeof(Transformer()))) diff --git a/doc/Archive/library_reference/kernel/expression.rst b/doc/Archive/library_reference/kernel/expression.rst deleted file mode 100644 index b2d4c2d1b35..00000000000 --- a/doc/Archive/library_reference/kernel/expression.rst +++ /dev/null @@ -1,26 +0,0 @@ -Expressions -=========== - -Summary -~~~~~~~ -.. autosummary:: - - pyomo.core.kernel.expression.expression - pyomo.core.kernel.expression.expression_tuple - pyomo.core.kernel.expression.expression_list - pyomo.core.kernel.expression.expression_dict - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.expression.expression - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.expression.expression_tuple - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.expression.expression_list - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.expression.expression_dict - :show-inheritance: - :members: diff --git a/doc/Archive/library_reference/kernel/heterogeneous_container.rst b/doc/Archive/library_reference/kernel/heterogeneous_container.rst deleted file mode 100644 index 74dad1d754e..00000000000 --- a/doc/Archive/library_reference/kernel/heterogeneous_container.rst +++ /dev/null @@ -1,6 +0,0 @@ -Heterogeneous Object Containers -=============================== - -.. automodule:: pyomo.core.kernel.heterogeneous_container - :show-inheritance: - :members: diff --git a/doc/Archive/library_reference/kernel/homogeneous_container.rst b/doc/Archive/library_reference/kernel/homogeneous_container.rst deleted file mode 100644 index b722e026dc1..00000000000 --- a/doc/Archive/library_reference/kernel/homogeneous_container.rst +++ /dev/null @@ -1,6 +0,0 @@ -Homogeneous Object Containers -============================= - -.. automodule:: pyomo.core.kernel.homogeneous_container - :show-inheritance: - :members: diff --git a/doc/Archive/library_reference/kernel/index.rst b/doc/Archive/library_reference/kernel/index.rst deleted file mode 100644 index 70c3cc715a9..00000000000 --- a/doc/Archive/library_reference/kernel/index.rst +++ /dev/null @@ -1,210 +0,0 @@ -.. role:: python(code) - :language: python - -.. warning:: - - The :python:`pyomo.kernel` API is still in the beta phase of development. It is fully tested and functional; however, the interface may change as it becomes further integrated with the rest of Pyomo. - -.. warning:: - - Models built with :python:`pyomo.kernel` components are not yet compatible with pyomo extension modules (e.g., :python:`PySP`, :python:`pyomo.dae`, :python:`pyomo.gdp`). - -The Kernel Library -================== - -The :python:`pyomo.kernel` library is an experimental modeling interface designed to provide a better experience for users doing concrete modeling and advanced application development with Pyomo. It includes the basic set of :ref:`modeling components ` necessary to build algebraic models, which have been redesigned from the ground up to make it easier for users to customize and extend. For a side-by-side comparison of :python:`pyomo.kernel` and :python:`pyomo.environ` syntax, visit the link below. - -.. toctree:: - - syntax_comparison.rst - - -Models built from :python:`pyomo.kernel` components are fully compatible with the standard solver interfaces included with Pyomo. A minimal example script that defines and solves a model is shown below. - -.. literalinclude:: examples/kernel_solving.py - :language: python - -Notable Improvements --------------------- - -More Control of Model Structure -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Containers in :python:`pyomo.kernel` are analogous to indexed components in :python:`pyomo.environ`. However, :python:`pyomo.kernel` containers allow for additional layers of structure as they can be nested within each other as long as they have compatible categories. The following example shows this using :python:`pyomo.kernel.variable` containers. - -.. literalinclude:: examples/kernel_containers_all.spy - :language: python - -As the next section will show, the standard modeling component containers are also compatible with user-defined classes that derive from the existing modeling components. - -Sub-Classing -^^^^^^^^^^^^ - -The existing components and containers in :python:`pyomo.kernel` are designed to make sub-classing easy. User-defined classes that derive from the standard modeling components and containers in :python:`pyomo.kernel` are compatible with existing containers of the same component category. As an example, in the following code we see that the :python:`pyomo.kernel.block_list` container can store both :python:`pyomo.kernel.block` objects as well as a user-defined :python:`Widget` object that derives from :python:`pyomo.kernel.block`. The :python:`Widget` object can also be placed on another block object as an attribute and treated itself as a block. - -.. code-block:: python - - class Widget(pyomo.kernel.block): - ... - - model = pyomo.kernel.block() - model.blist = pyomo.kernel.block_list() - model.blist.append(Widget()) - model.blist.append(pyomo.kernel.block()) - model.w = Widget() - model.w.x = pyomo.kernel.variable() - -The next series of examples goes into more detail on how to implement derived components or containers. - -The following code block shows a class definition for a non-negative variable, starting from :python:`pyomo.kernel.variable` as a base class. - -.. literalinclude:: examples/kernel_subclassing_Nonnegative.spy - :language: python - -The :python:`NonNegativeVariable` class prevents negative values from being stored into its lower bound during initialization or later on through assignment statements (e.g, :python:`x.lb = -1` fails). Note that the :python:`__slots__ == ()` line at the beginning of the class definition is optional, but it is recommended if no additional data members are necessary as it reduces the memory requirement of the new variable type. - -The next code block defines a custom variable container called :python:`Point` that represents a 3-dimensional point in Cartesian space. The new type derives from the :python:`pyomo.kernel.variable_tuple` container and uses the :python:`NonNegativeVariable` type we defined previously in the `z` coordinate. - -.. literalinclude:: examples/kernel_subclassing_Point.spy - :language: python - -The :python:`Point` class can be treated like a tuple storing three variables, and it can be placed inside of other variable containers or added as attributes to blocks. The property methods included in the class definition provide an additional syntax for accessing the three variables it stores, as the next code example will show. - -The following code defines a class for building a convex second-order cone constraint from a :python:`Point` object. It derives from the :python:`pyomo.kernel.constraint` class, overriding the constructor to build the constraint expression and utilizing the property methods on the point class to increase readability. - -.. literalinclude:: examples/kernel_subclassing_SOC.spy - :language: python - - -Reduced Memory Usage -^^^^^^^^^^^^^^^^^^^^ - -The :python:`pyomo.kernel` library offers significant opportunities to reduce memory requirements for highly structured models. The situation where this is most apparent is when expressing a model in terms of many small blocks consisting of singleton components. As an example, consider expressing a model consisting of a large number of voltage transformers. One option for doing so might be to define a `Transformer` component as a subclass of :python:`pyomo.kernel.block`. The example below defines such a component, including some helper methods for connecting input and output voltage variables and updating the transformer ratio. - -.. literalinclude:: examples/transformer_kernel.spy - :language: python - -A simplified version of this using :python:`pyomo.environ` components might look like what is below. - -.. literalinclude:: examples/transformer_aml.spy - :language: python - -The transformer expressed using :python:`pyomo.kernel` components requires roughly 2 KB of memory, whereas the :python:`pyomo.environ` version requires roughly 8.4 KB of memory (an increase of more than 4x). Additionally, the :python:`pyomo.kernel` transformer is fully compatible with all existing :python:`pyomo.kernel` block containers. - -Direct Support For Conic Constraints with Mosek -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Pyomo 5.6.3 introduced support into :python:`pyomo.kernel` -for six conic constraint forms that are directly recognized -by the new Mosek solver interface. These are - - - :python:`conic.quadratic`: - - :math:`\;\;\sum_{i}x_i^2 \leq r^2,\;\;r\geq 0` - - - :python:`conic.rotated_quadratic`: - - :math:`\;\;\sum_{i}x_i^2 \leq 2 r_1 r_2,\;\;r_1,r_2\geq 0` - - - :python:`conic.primal_exponential`: - - :math:`\;\;x_1\exp(x_2/x_1) \leq r,\;\;x_1,r\geq 0` - - - :python:`conic.primal_power` (:math:`\alpha` is a constant): - - :math:`\;\;||x||_2 \leq r_1^{\alpha} r_2^{1-\alpha},\;\;r_1,r_2\geq 0,\;0 < \alpha < 1` - - - :python:`conic.dual_exponential`: - - :math:`\;\;-x_2\exp((x_1/x_2)-1) \leq r,\;\;x_2\leq0,\;r\geq 0` - - - :python:`conic.dual_power` (:math:`\alpha` is a constant): - - :math:`\;\;||x||_2 \leq (r_1/\alpha)^{\alpha} (r_2/(1-\alpha))^{1-\alpha},\;\;r_1,r_2\geq 0,\;0 < \alpha < 1` - -Other solver interfaces will treat these objects as general -nonlinear or quadratic constraints, and may or may not have -the ability to identify their convexity. For instance, -Gurobi will recognize the expressions produced by the -:python:`quadratic` and :python:`rotated_quadratic` objects -as representing convex domains as long as the variables -involved satisfy the convexity conditions. However, other -solvers may not include this functionality. - -Each of these conic constraint classes are of the same -category type as standard :python:`pyomo.kernel.constraint` -object, and, thus, are directly supported by the standard -constraint containers (:python:`constraint_tuple`, -:python:`constraint_list`, :python:`constraint_dict`). - -Each conic constraint class supports two methods of -instantiation. The first method is to directly instantiate a -conic constraint object, providing all necessary input -variables: - -.. literalinclude:: examples/conic_Class.spy - :language: python - -This method may be limiting if utilizing the Mosek solver as -the user must ensure that additional conic constraints do -not use variables that are directly involved in any existing -conic constraints (this is a limitation the Mosek solver -itself). - -To overcome this limitation, and to provide a more general -way of defining conic domains, each conic constraint class -provides the :python:`as_domain` class method. This -alternate constructor has the same argument signature as the -class, but in place of each variable, one can optionally -provide a constant, a linear expression, or -:python:`None`. The :python:`as_domain` class method returns -a :python:`block` object that includes the core conic -constraint, auxiliary variables used to express the conic -constraint, as well as auxiliary constraints that link the -inputs (that are not :python:`None`) to the auxiliary -variables. Example: - -.. literalinclude:: examples/conic_Domain.spy - :language: python - -Reference ---------- - -.. _kernel_modeling_components: - -Modeling Components: -^^^^^^^^^^^^^^^^^^^^ - -.. toctree:: - :maxdepth: 1 - - block.rst - variable.rst - constraint.rst - parameter.rst - objective.rst - expression.rst - sos.rst - suffix.rst - piecewise/index.rst - conic.rst - -Base API: -^^^^^^^^^ - -.. toctree:: - :maxdepth: 1 - - base.rst - homogeneous_container.rst - heterogeneous_container.rst - -Containers: -^^^^^^^^^^^ - -.. toctree:: - :maxdepth: 1 - - tuple_container.rst - list_container.rst - dict_container.rst diff --git a/doc/Archive/library_reference/kernel/list_container.rst b/doc/Archive/library_reference/kernel/list_container.rst deleted file mode 100644 index b82c6d9c6f0..00000000000 --- a/doc/Archive/library_reference/kernel/list_container.rst +++ /dev/null @@ -1,8 +0,0 @@ -List-like Object Storage -======================== - -.. autoclass:: pyomo.core.kernel.list_container.ListContainer - :show-inheritance: - :members: - :inherited-members: - :special-members: diff --git a/doc/Archive/library_reference/kernel/objective.rst b/doc/Archive/library_reference/kernel/objective.rst deleted file mode 100644 index 77f26d2f441..00000000000 --- a/doc/Archive/library_reference/kernel/objective.rst +++ /dev/null @@ -1,26 +0,0 @@ -Objectives -========== - -Summary -~~~~~~~ -.. autosummary:: - - pyomo.core.kernel.objective.objective - pyomo.core.kernel.objective.objective_tuple - pyomo.core.kernel.objective.objective_list - pyomo.core.kernel.objective.objective_dict - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.objective.objective - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.objective.objective_tuple - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.objective.objective_list - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.objective.objective_dict - :show-inheritance: - :members: diff --git a/doc/Archive/library_reference/kernel/parameter.rst b/doc/Archive/library_reference/kernel/parameter.rst deleted file mode 100644 index 212b0cb125e..00000000000 --- a/doc/Archive/library_reference/kernel/parameter.rst +++ /dev/null @@ -1,30 +0,0 @@ -Parameters -========== - -Summary -~~~~~~~ -.. autosummary:: - - pyomo.core.kernel.parameter.parameter - pyomo.core.kernel.parameter.functional_value - pyomo.core.kernel.parameter.parameter_tuple - pyomo.core.kernel.parameter.parameter_list - pyomo.core.kernel.parameter.parameter_dict - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.parameter.parameter - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.parameter.functional_value - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.parameter.parameter_tuple - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.parameter.parameter_list - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.parameter.parameter_dict - :show-inheritance: - :members: diff --git a/doc/Archive/library_reference/kernel/piecewise/index.rst b/doc/Archive/library_reference/kernel/piecewise/index.rst deleted file mode 100644 index 2255d0fe116..00000000000 --- a/doc/Archive/library_reference/kernel/piecewise/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Piecewise Function Library -========================== - -Modules - -.. toctree:: - :maxdepth: 1 - - piecewise.rst - piecewise_nd.rst - util.rst diff --git a/doc/Archive/library_reference/kernel/piecewise/piecewise.rst b/doc/Archive/library_reference/kernel/piecewise/piecewise.rst deleted file mode 100644 index 25c250d6559..00000000000 --- a/doc/Archive/library_reference/kernel/piecewise/piecewise.rst +++ /dev/null @@ -1,53 +0,0 @@ -Single-variate Piecewise Functions -================================== - -Summary -~~~~~~~ -.. autosummary:: - pyomo.core.kernel.piecewise_library.transforms.piecewise - pyomo.core.kernel.piecewise_library.transforms.PiecewiseLinearFunction - pyomo.core.kernel.piecewise_library.transforms.TransformedPiecewiseLinearFunction - pyomo.core.kernel.piecewise_library.transforms.piecewise_convex - pyomo.core.kernel.piecewise_library.transforms.piecewise_sos2 - pyomo.core.kernel.piecewise_library.transforms.piecewise_dcc - pyomo.core.kernel.piecewise_library.transforms.piecewise_cc - pyomo.core.kernel.piecewise_library.transforms.piecewise_mc - pyomo.core.kernel.piecewise_library.transforms.piecewise_inc - pyomo.core.kernel.piecewise_library.transforms.piecewise_dlog - pyomo.core.kernel.piecewise_library.transforms.piecewise_log - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autofunction:: pyomo.core.kernel.piecewise_library.transforms.piecewise -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.PiecewiseLinearFunction - :show-inheritance: - :special-members: __call__ - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.TransformedPiecewiseLinearFunction - :show-inheritance: - :special-members: __call__ - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_convex - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_sos2 - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_dcc - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_cc - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_mc - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_inc - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_dlog - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms.piecewise_log - :show-inheritance: - :members: diff --git a/doc/Archive/library_reference/kernel/piecewise/piecewise_nd.rst b/doc/Archive/library_reference/kernel/piecewise/piecewise_nd.rst deleted file mode 100644 index e5c71a4ec15..00000000000 --- a/doc/Archive/library_reference/kernel/piecewise/piecewise_nd.rst +++ /dev/null @@ -1,25 +0,0 @@ -Multi-variate Piecewise Functions -================================= - -Summary -~~~~~~~ -.. autosummary:: - pyomo.core.kernel.piecewise_library.transforms_nd.piecewise_nd - pyomo.core.kernel.piecewise_library.transforms_nd.PiecewiseLinearFunctionND - pyomo.core.kernel.piecewise_library.transforms_nd.TransformedPiecewiseLinearFunctionND - pyomo.core.kernel.piecewise_library.transforms_nd.piecewise_nd_cc - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autofunction:: pyomo.core.kernel.piecewise_library.transforms_nd.piecewise_nd -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms_nd.PiecewiseLinearFunctionND - :show-inheritance: - :special-members: __call__ - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms_nd.TransformedPiecewiseLinearFunctionND - :show-inheritance: - :special-members: __call__ - :members: -.. autoclass:: pyomo.core.kernel.piecewise_library.transforms_nd.piecewise_nd_cc - :show-inheritance: - :members: diff --git a/doc/Archive/library_reference/kernel/piecewise/util.rst b/doc/Archive/library_reference/kernel/piecewise/util.rst deleted file mode 100644 index 52b7b1de8f7..00000000000 --- a/doc/Archive/library_reference/kernel/piecewise/util.rst +++ /dev/null @@ -1,6 +0,0 @@ -Utilities for Piecewise Functions -================================= - -.. automodule:: pyomo.core.kernel.piecewise_library.util - :show-inheritance: - :members: diff --git a/doc/Archive/library_reference/kernel/sos.rst b/doc/Archive/library_reference/kernel/sos.rst deleted file mode 100644 index 0f3f5fedf54..00000000000 --- a/doc/Archive/library_reference/kernel/sos.rst +++ /dev/null @@ -1,30 +0,0 @@ -Special Ordered Sets -==================== - -Summary -~~~~~~~ -.. autosummary:: - - pyomo.core.kernel.sos.sos - pyomo.core.kernel.sos.sos1 - pyomo.core.kernel.sos.sos2 - pyomo.core.kernel.sos.sos_tuple - pyomo.core.kernel.sos.sos_list - pyomo.core.kernel.sos.sos_dict - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.sos.sos - :show-inheritance: - :members: -.. autofunction:: pyomo.core.kernel.sos.sos1 -.. autofunction:: pyomo.core.kernel.sos.sos2 -.. autoclass:: pyomo.core.kernel.sos.sos_tuple - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.sos.sos_list - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.sos.sos_dict - :show-inheritance: - :members: diff --git a/doc/Archive/library_reference/kernel/suffix.rst b/doc/Archive/library_reference/kernel/suffix.rst deleted file mode 100644 index d833f56daa9..00000000000 --- a/doc/Archive/library_reference/kernel/suffix.rst +++ /dev/null @@ -1,6 +0,0 @@ -Suffixes -======== - -.. automodule:: pyomo.core.kernel.suffix - :show-inheritance: - :members: diff --git a/doc/Archive/library_reference/kernel/syntax_comparison.rst b/doc/Archive/library_reference/kernel/syntax_comparison.rst deleted file mode 100644 index 71c739214e3..00000000000 --- a/doc/Archive/library_reference/kernel/syntax_comparison.rst +++ /dev/null @@ -1,133 +0,0 @@ -.. _kernel_syntax_comparison: - -Syntax Comparison Table (pyomo.kernel vs pyomo.environ) -======================================================= - -.. list-table:: - :header-rows: 1 - :align: center - - * - - - **pyomo.kernel** - - **pyomo.environ** - - * - **Import** - - .. literalinclude:: examples/kernel_example_Import_Syntax.spy - :language: python - - .. literalinclude:: examples/aml_example_Import_Syntax.spy - :language: python - * - **Model** [#models_fn]_ - - .. literalinclude:: examples/kernel_example_AbstractModels.spy - :language: python - .. literalinclude:: examples/kernel_example_ConcreteModels.spy - :language: python - - .. literalinclude:: examples/aml_example_AbstractModels.spy - :language: python - .. literalinclude:: examples/aml_example_ConcreteModels.spy - :language: python - * - **Set** [#sets_fn]_ - - .. literalinclude:: examples/kernel_example_Sets_1.spy - :language: python - .. literalinclude:: examples/kernel_example_Sets_2.spy - :language: python - - .. literalinclude:: examples/aml_example_Sets_1.spy - :language: python - .. literalinclude:: examples/aml_example_Sets_2.spy - :language: python - * - **Parameter** [#parameters_fn]_ - - .. literalinclude:: examples/kernel_example_Parameters_single.spy - :language: python - .. literalinclude:: examples/kernel_example_Parameters_dict.spy - :language: python - .. literalinclude:: examples/kernel_example_Parameters_list.spy - :language: python - - .. literalinclude:: examples/aml_example_Parameters_single.spy - :language: python - .. literalinclude:: examples/aml_example_Parameters_dict.spy - :language: python - .. literalinclude:: examples/aml_example_Parameters_list.spy - :language: python - * - **Variable** - - .. literalinclude:: examples/kernel_example_Variables_single.spy - :language: python - .. literalinclude:: examples/kernel_example_Variables_dict.spy - :language: python - .. literalinclude:: examples/kernel_example_Variables_list.spy - :language: python - - .. literalinclude:: examples/aml_example_Variables_single.spy - :language: python - .. literalinclude:: examples/aml_example_Variables_dict.spy - :language: python - .. literalinclude:: examples/aml_example_Variables_list.spy - :language: python - * - **Constraint** - - .. literalinclude:: examples/kernel_example_Constraints_single.spy - :language: python - .. literalinclude:: examples/kernel_example_Constraints_dict.spy - :language: python - .. literalinclude:: examples/kernel_example_Constraints_list.spy - :language: python - - .. literalinclude:: examples/aml_example_Constraints_single.spy - :language: python - .. literalinclude:: examples/aml_example_Constraints_dict.spy - :language: python - .. literalinclude:: examples/aml_example_Constraints_list.spy - :language: python - * - **Expression** - - .. literalinclude:: examples/kernel_example_Expressions_single.spy - :language: python - .. literalinclude:: examples/kernel_example_Expressions_dict.spy - :language: python - .. literalinclude:: examples/kernel_example_Expressions_list.spy - :language: python - - .. literalinclude:: examples/aml_example_Expressions_single.spy - :language: python - .. literalinclude:: examples/aml_example_Expressions_dict.spy - :language: python - .. literalinclude:: examples/aml_example_Expressions_list.spy - :language: python - * - **Objective** - - .. literalinclude:: examples/kernel_example_Objectives_single.spy - :language: python - .. literalinclude:: examples/kernel_example_Objectives_dict.spy - :language: python - .. literalinclude:: examples/kernel_example_Objectives_list.spy - :language: python - - .. literalinclude:: examples/aml_example_Objectives_single.spy - :language: python - .. literalinclude:: examples/aml_example_Objectives_dict.spy - :language: python - .. literalinclude:: examples/aml_example_Objectives_list.spy - :language: python - * - **SOS** [#sos_fn]_ - - .. literalinclude:: examples/kernel_example_SOS_single.spy - :language: python - .. literalinclude:: examples/kernel_example_SOS_dict.spy - :language: python - .. literalinclude:: examples/kernel_example_SOS_list.spy - :language: python - - .. literalinclude:: examples/aml_example_SOS_single.spy - :language: python - .. literalinclude:: examples/aml_example_SOS_dict.spy - :language: python - .. literalinclude:: examples/aml_example_SOS_list.spy - :language: python - * - **Suffix** - - .. literalinclude:: examples/kernel_example_Suffix_single.spy - :language: python - .. literalinclude:: examples/kernel_example_Suffix_dict.spy - :language: python - - .. literalinclude:: examples/aml_example_Suffix_single.spy - :language: python - .. literalinclude:: examples/aml_example_Suffix_dict.spy - :language: python - * - **Piecewise** [#pw_fn]_ - - .. literalinclude:: examples/kernel_example_Piecewise_1d.spy - :language: python - - .. literalinclude:: examples/aml_example_Piecewise_1d.spy - :language: python -.. [#models_fn] :python:`pyomo.kernel` does not include an alternative to the :python:`AbstractModel` component from :python:`pyomo.environ`. All data necessary to build a model must be imported by the user. -.. [#sets_fn] :python:`pyomo.kernel` does not include an alternative to the Pyomo :python:`Set` component from :python:`pyomo.environ`. -.. [#parameters_fn] :python:`pyomo.kernel.parameter` objects are always mutable. -.. [#sos_fn] Special Ordered Sets -.. [#pw_fn] Both :python:`pyomo.kernel.piecewise` and :python:`pyomo.kernel.piecewise_nd` create objects that are sub-classes of :python:`pyomo.kernel.block`. Thus, these objects can be stored in containers such as :python:`pyomo.kernel.block_dict` and :python:`pyomo.kernel.block_list`. diff --git a/doc/Archive/library_reference/kernel/tuple_container.rst b/doc/Archive/library_reference/kernel/tuple_container.rst deleted file mode 100644 index 8a2798753c4..00000000000 --- a/doc/Archive/library_reference/kernel/tuple_container.rst +++ /dev/null @@ -1,8 +0,0 @@ -Tuple-like Object Storage -========================= - -.. autoclass:: pyomo.core.kernel.tuple_container.TupleContainer - :show-inheritance: - :members: - :inherited-members: - :special-members: diff --git a/doc/Archive/library_reference/kernel/variable.rst b/doc/Archive/library_reference/kernel/variable.rst deleted file mode 100644 index f743cee4003..00000000000 --- a/doc/Archive/library_reference/kernel/variable.rst +++ /dev/null @@ -1,26 +0,0 @@ -Variables -========= - -Summary -~~~~~~~ -.. autosummary:: - - pyomo.core.kernel.variable.variable - pyomo.core.kernel.variable.variable_tuple - pyomo.core.kernel.variable.variable_list - pyomo.core.kernel.variable.variable_dict - -Member Documentation -~~~~~~~~~~~~~~~~~~~~ -.. autoclass:: pyomo.core.kernel.variable.variable - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.variable.variable_tuple - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.variable.variable_list - :show-inheritance: - :members: -.. autoclass:: pyomo.core.kernel.variable.variable_dict - :show-inheritance: - :members: diff --git a/doc/Archive/library_reference/solvers/cplex_persistent.rst b/doc/Archive/library_reference/solvers/cplex_persistent.rst deleted file mode 100644 index ee28ecda5e5..00000000000 --- a/doc/Archive/library_reference/solvers/cplex_persistent.rst +++ /dev/null @@ -1,7 +0,0 @@ -CPLEXPersistent -================ - -.. autoclass:: pyomo.solvers.plugins.solvers.cplex_persistent.CPLEXPersistent - :members: - :inherited-members: - :show-inheritance: diff --git a/doc/Archive/library_reference/solvers/gams.rst b/doc/Archive/library_reference/solvers/gams.rst deleted file mode 100644 index f36de5d9e01..00000000000 --- a/doc/Archive/library_reference/solvers/gams.rst +++ /dev/null @@ -1,43 +0,0 @@ -GAMS -==== - -.. currentmodule:: pyomo.solvers.plugins.solvers.GAMS - -GAMSShell Solver ----------------- - -.. autosummary:: - - GAMSShell.available - GAMSShell.executable - GAMSShell.solve - GAMSShell.version - GAMSShell.warm_start_capable - -.. autoclass:: GAMSShell - :members: - -GAMSDirect Solver ------------------ - -.. autosummary:: - - GAMSDirect.available - GAMSDirect.solve - GAMSDirect.version - GAMSDirect.warm_start_capable - -.. autoclass:: GAMSDirect - :members: - -.. currentmodule:: pyomo.repn.plugins.gams_writer - -GAMS Writer ------------ - -This class is most commonly accessed and called upon via -model.write("filename.gms", ...), but is also utilized -by the GAMS solver interfaces. - -.. autoclass:: ProblemWriter_gams - :members: __call__ diff --git a/doc/Archive/library_reference/solvers/gurobi_direct.rst b/doc/Archive/library_reference/solvers/gurobi_direct.rst deleted file mode 100644 index 21cb79e5531..00000000000 --- a/doc/Archive/library_reference/solvers/gurobi_direct.rst +++ /dev/null @@ -1,18 +0,0 @@ -GurobiDirect -============ - -.. currentmodule:: pyomo.solvers.plugins.solvers.gurobi_direct - -Methods -------- - -.. autosummary:: - - GurobiDirect.available - GurobiDirect.close - GurobiDirect.close_global - GurobiDirect.solve - GurobiDirect.version - -.. autoclass:: GurobiDirect - :members: available, close, close_global, solve, version diff --git a/doc/Archive/library_reference/solvers/gurobi_persistent.rst b/doc/Archive/library_reference/solvers/gurobi_persistent.rst deleted file mode 100644 index 2472599c1ed..00000000000 --- a/doc/Archive/library_reference/solvers/gurobi_persistent.rst +++ /dev/null @@ -1,39 +0,0 @@ -GurobiPersistent -================ - -.. currentmodule:: pyomo.solvers.plugins.solvers.gurobi_persistent - -Methods -------- - -.. autosummary:: - - GurobiPersistent.add_block - GurobiPersistent.add_constraint - GurobiPersistent.set_objective - GurobiPersistent.add_sos_constraint - GurobiPersistent.add_var - GurobiPersistent.available - GurobiPersistent.has_capability - GurobiPersistent.has_instance - GurobiPersistent.load_vars - GurobiPersistent.problem_format - GurobiPersistent.remove_block - GurobiPersistent.remove_constraint - GurobiPersistent.remove_sos_constraint - GurobiPersistent.remove_var - GurobiPersistent.reset - GurobiPersistent.results_format - GurobiPersistent.set_callback - GurobiPersistent.set_instance - GurobiPersistent.set_problem_format - GurobiPersistent.set_results_format - GurobiPersistent.solve - GurobiPersistent.update_var - GurobiPersistent.version - GurobiPersistent.write - -.. autoclass:: GurobiPersistent - :members: - :inherited-members: - :show-inheritance: diff --git a/doc/Archive/library_reference/solvers/index.rst b/doc/Archive/library_reference/solvers/index.rst deleted file mode 100644 index 400032df076..00000000000 --- a/doc/Archive/library_reference/solvers/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Solver Interfaces -================= - -.. toctree:: - :maxdepth: 1 - - gams.rst - cplex_persistent.rst - gurobi_direct.rst - gurobi_persistent.rst - xpress_persistent.rst diff --git a/doc/Archive/library_reference/solvers/xpress_persistent.rst b/doc/Archive/library_reference/solvers/xpress_persistent.rst deleted file mode 100644 index 2a98b4a09db..00000000000 --- a/doc/Archive/library_reference/solvers/xpress_persistent.rst +++ /dev/null @@ -1,7 +0,0 @@ -XpressPersistent -================ - -.. autoclass:: pyomo.solvers.plugins.solvers.xpress_persistent.XpressPersistent - :members: - :inherited-members: - :show-inheritance: diff --git a/doc/Archive/make.bat b/doc/Archive/make.bat deleted file mode 100644 index 5c7a2549fca..00000000000 --- a/doc/Archive/make.bat +++ /dev/null @@ -1,36 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build -set SPHINXPROJ=pyomocontrib_simplemodel - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd diff --git a/doc/Archive/model_debugging/FAQ.rst b/doc/Archive/model_debugging/FAQ.rst deleted file mode 100644 index eef8ad9bd56..00000000000 --- a/doc/Archive/model_debugging/FAQ.rst +++ /dev/null @@ -1,29 +0,0 @@ -FAQ -=== - -#. Solver not found - -Solvers are **not** distributed with Pyomo and must be installed -separately by the user. In general, the solver executable must be accessible using a terminal command. For example, ipopt can only be used as a solver if -the command - -:: - - $ ipopt - -invokes the solver. For example - -:: - - $ ipopt -? - usage: ipopt [options] stub [-AMPL] [ ...] - - Options: - -- {end of options} - -= {show name= possibilities} - -? {show usage} - -bf {read boundsfile f} - -e {suppress echoing of assignments} - -of {write .sol file to file f} - -s {write .sol file (without -AMPL)} - -v {just show version} diff --git a/doc/Archive/model_debugging/getting_help.rst b/doc/Archive/model_debugging/getting_help.rst deleted file mode 100644 index acc7c60b29e..00000000000 --- a/doc/Archive/model_debugging/getting_help.rst +++ /dev/null @@ -1,10 +0,0 @@ -Getting Help -============ - -See the Pyomo Forum for online discussions of Pyomo or to ask a question: - -* http://groups.google.com/group/pyomo-forum/ - -Ask a question on StackOverflow using the `#pyomo` tag: - -* https://stackoverflow.com/questions/ask?tags=pyomo diff --git a/doc/Archive/model_debugging/index.rst b/doc/Archive/model_debugging/index.rst deleted file mode 100644 index e1dafe45e0a..00000000000 --- a/doc/Archive/model_debugging/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Debugging Pyomo Models -====================== - -.. toctree:: - :maxdepth: 1 - - model_interrogation.rst - FAQ.rst - getting_help.rst diff --git a/doc/Archive/model_debugging/model_interrogation.rst b/doc/Archive/model_debugging/model_interrogation.rst deleted file mode 100644 index 4e019da88eb..00000000000 --- a/doc/Archive/model_debugging/model_interrogation.rst +++ /dev/null @@ -1,32 +0,0 @@ -Interrogating Pyomo Models -========================== - -.. doctest:: - :hide: - - >>> import pyomo.environ as pyo - >>> from pyomo.opt import SolverFactory - >>> model = pyo.ConcreteModel() - >>> model.n = pyo.Param(default=4) - >>> model.x = pyo.Var(pyo.RangeSet(model.n), within=pyo.Binary) - >>> def o_rule(model): - ... return pyo.summation(model.x) - >>> model.o = pyo.Objective(rule=o_rule) - >>> model.c = pyo.Constraint(expr=model.x[2] + model.x[3] >= 1) - >>> r = SolverFactory('glpk').solve(model) - -Show solver output by adding the `tee=True` option when calling the -`solve` function - -.. doctest:: - - >>> SolverFactory('glpk').solve(model, tee=True) # doctest: +SKIP - -You can use the `pprint` function to display the model or individual -model components - -.. doctest:: - - >>> model.pprint() # doctest: +SKIP - >>> model.x.pprint() # doctest: +SKIP - diff --git a/doc/Archive/model_transformations/index.rst b/doc/Archive/model_transformations/index.rst deleted file mode 100644 index 462538128e7..00000000000 --- a/doc/Archive/model_transformations/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Model Transformations -===================== - -.. toctree:: - :maxdepth: 1 - - scaling.rst diff --git a/doc/Archive/model_transformations/scaling.rst b/doc/Archive/model_transformations/scaling.rst deleted file mode 100644 index 180f1e0205b..00000000000 --- a/doc/Archive/model_transformations/scaling.rst +++ /dev/null @@ -1,41 +0,0 @@ -Model Scaling Transformation -============================ - -Good scaling of models can greatly improve the numerical properties of a problem and thus increase reliability and convergence. The ``core.scale_model`` transformation allows users to separate scaling of a model from the declaration of the model variables and constraints which allows for models to be written in more natural forms and to be scaled and rescaled as required without having to rewrite the model code. - -.. autoclass:: pyomo.core.plugins.transform.scaling.ScaleModel - :members: - -Setting Scaling Factors ------------------------ - -Scaling factors for components in a model are declared using :ref:`Suffixes`, as shown in the example above. In order to define a scaling factor for a component, a ``Suffix`` named ``scaling_factor`` must first be created to hold the scaling factor(s). Scaling factor suffixes can be declared at any level of the model hierarchy, but scaling factors declared on the higher-level ``models`` or ``Blocks`` take precedence over those declared at lower levels. - -Scaling suffixes are dict-like where each key is a Pyomo component and the value is the scaling factor to be applied to that component. - -In the case of indexed components, scaling factors can either be declared for an individual index or for the indexed component as a whole (with scaling factors for individual indices taking precedence over overall scaling factors). - -.. note:: - - In the case that a scaling factor is declared for a component on at multiple levels of the hierarchy, the highest level scaling factor will be applied. - -.. note:: - - It is also possible (but not encouraged) to define a "default" scaling factor to be applied to any component for which a specific scaling factor has not been declared by setting a entry in a Suffix with a key of ``None``. In this case, the default value declared closest to the component to be scaled will be used (i.e., the first default value found when walking up the model hierarchy). - -Applying Model Scaling ----------------------- - -The ``core.scale_model`` transformation provides two approaches for creating a scaled model. - -In-Place Scaling -**************** - -The ``apply_to(model)`` method can be used to apply scaling directly to an existing model. When using this method, all the variables, constraints and objectives within the target model are replaced with new scaled components and the appropriate scaling factors applied. The model can then be sent to a solver as usual, however the results will be in terms of the scaled components and must be un-scaled by the user. - -Creating a New Scaled Model -*************************** - -Alternatively, the ``create_using(model)`` method can be used to create a new, scaled version of the model which can be solved. In this case, a clone of the original model is generated with the variables, constraints and objectives replaced by scaled equivalents. Users can then send the scaled model to a solver after which the ``propagate_solution`` method can be used to map the scaled solution back onto the original model for further analysis. - -The advantage of this approach is that the original model is maintained separately from the scaled model, which facilitates rescaling and other manipulation of the original model after a solution has been found. The disadvantage of this approach is that cloning the model may result in memory issues when dealing with larger models. diff --git a/doc/Archive/modeling_extensions/__init__.py b/doc/Archive/modeling_extensions/__init__.py deleted file mode 100644 index a4a626013c4..00000000000 --- a/doc/Archive/modeling_extensions/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -# ___________________________________________________________________________ -# -# Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2024 -# National Technology and Engineering Solutions of Sandia, LLC -# Under the terms of Contract DE-NA0003525 with National Technology and -# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain -# rights in this software. -# This software is distributed under the 3-clause BSD License. -# ___________________________________________________________________________ diff --git a/doc/Archive/modeling_extensions/bilevel.rst b/doc/Archive/modeling_extensions/bilevel.rst deleted file mode 100644 index 5e9ee9b0a7c..00000000000 --- a/doc/Archive/modeling_extensions/bilevel.rst +++ /dev/null @@ -1,6 +0,0 @@ -Bilevel Programming -=================== - -``pyomo.bilevel`` provides extensions supporting modeling of multi-level -optimization problems. - diff --git a/doc/Archive/modeling_extensions/dae.rst b/doc/Archive/modeling_extensions/dae.rst deleted file mode 100644 index ff0fb75e610..00000000000 --- a/doc/Archive/modeling_extensions/dae.rst +++ /dev/null @@ -1,933 +0,0 @@ -Dynamic Optimization with pyomo.DAE -=================================== - -.. image:: /../logos/dae/Pyomo-DAE-150.png - :scale: 35% - :align: right - -The pyomo.DAE modeling extension [PyomoDAE]_ allows users to incorporate systems of -differential algebraic equations (DAE)s in a Pyomo model. The modeling -components in this extension are able to represent ordinary or partial -differential equations. The differential equations do not have to be -written in a particular format and the components are flexible enough to -represent higher-order derivatives or mixed partial -derivatives. Pyomo.DAE also includes model transformations which use -simultaneous discretization approaches to transform a DAE model into an -algebraic model. Finally, pyomo.DAE includes utilities for simulating -DAE models and initializing dynamic optimization problems. - - - -Modeling Components -------------------- - -.. (Replace these definitions with in-code documentation) - -Pyomo.DAE introduces three new modeling components to Pyomo: - -.. autosummary:: - :nosignatures: - - pyomo.dae.ContinuousSet - pyomo.dae.DerivativeVar - pyomo.dae.Integral - -As will be shown later, differential equations can be declared using -using these new modeling components along with the standard Pyomo -:py:class:`Var ` and -:py:class:`Constraint ` components. - -ContinuousSet -************* - -This component is used to define continuous bounded domains (for example -'spatial' or 'time' domains). It is similar to a Pyomo -:py:class:`Set ` component and can be used to index things -like variables and constraints. Any number of -:py:class:`ContinuousSets ` can be used to index a -component and components can be indexed by both -:py:class:`Sets ` and -:py:class:`ContinuousSets ` in arbitrary order. - -In the current implementation, models with -:py:class:`ContinuousSet` components may not be solved -until every :py:class:`ContinuousSet` has been -discretized. Minimally, a :py:class:`ContinuousSet` -must be initialized with two numeric values representing the upper and lower -bounds of the continuous domain. A user may also specify additional points in -the domain to be used as finite element points in the discretization. - -.. autoclass:: pyomo.dae.ContinuousSet - :members: - -The following code snippet shows examples of declaring a -:py:class:`ContinuousSet ` component on a -concrete Pyomo model: - -.. doctest:: - - Required imports - >>> from pyomo.environ import * - >>> from pyomo.dae import * - - >>> model = ConcreteModel() - - Declaration by providing bounds - >>> model.t = ContinuousSet(bounds=(0,5)) - - Declaration by initializing with desired discretization points - >>> model.x = ContinuousSet(initialize=[0,1,2,5]) - -.. note:: - A :py:class:`ContinuousSet ` may not be - constructed unless at least two numeric points are provided to bound the - continuous domain. - -The following code snippet shows an example of declaring a -:py:class:`ContinuousSet ` component on an -abstract Pyomo model using the example data file. - -.. code-block:: ampl - - set t := 0 0.5 2.25 3.75 5; - -.. doctest:: - - Required imports - >>> from pyomo.environ import * - >>> from pyomo.dae import * - - >>> model = AbstractModel() - - The ContinuousSet below will be initialized using the points - in the data file when a model instance is created. - >>> model.t = ContinuousSet() - -.. note:: - If a separate data file is used to initialize a - :py:class:`ContinuousSet `, it is done using - the 'set' command and not 'continuousset' - -.. note:: - Most valid ways to declare and initialize a - :py:class:`Set ` can be used to - declare and initialize a :py:class:`ContinuousSet`. - See the documentation for :py:class:`Set ` for additional - options. - -.. warning:: - Be careful using a :py:class:`ContinuousSet - ` as an implicit index in an expression, - i.e. ``sum(m.v[i] for i in m.myContinuousSet)``. The expression will - be generated using the discretization points contained in the - :py:class:`ContinuousSet ` at the time the - expression was constructed and will not be updated if additional - points are added to the set during discretization. - -.. note:: - :py:class:`ContinuousSet ` components are - always ordered (sorted) therefore the ``first()`` and ``last()`` - :py:class:`Set ` methods can be used to access the lower - and upper boundaries of the - :py:class:`ContinuousSet ` respectively - -DerivativeVar -************* - -.. autoclass:: pyomo.dae.DerivativeVar - :members: - -The code snippet below shows examples of declaring -:py:class:`DerivativeVar ` components on a -Pyomo model. In each case, the variable being differentiated is supplied -as the only positional argument and the type of derivative is specified -using the 'wrt' (or the more verbose 'withrespectto') keyword -argument. Any keyword argument that is valid for a Pyomo -:py:class:`Var ` component may also be specified. - -.. doctest:: - - Required imports - >>> from pyomo.environ import * - >>> from pyomo.dae import * - - >>> model = ConcreteModel() - >>> model.s = Set(initialize=['a','b']) - >>> model.t = ContinuousSet(bounds=(0,5)) - >>> model.l = ContinuousSet(bounds=(-10,10)) - - >>> model.x = Var(model.t) - >>> model.y = Var(model.s,model.t) - >>> model.z = Var(model.t,model.l) - - Declare the first derivative of model.x with respect to model.t - >>> model.dxdt = DerivativeVar(model.x, withrespectto=model.t) - - Declare the second derivative of model.y with respect to model.t - Note that this DerivativeVar will be indexed by both model.s and model.t - >>> model.dydt2 = DerivativeVar(model.y, wrt=(model.t,model.t)) - - Declare the partial derivative of model.z with respect to model.l - Note that this DerivativeVar will be indexed by both model.t and model.l - >>> model.dzdl = DerivativeVar(model.z, wrt=(model.l), initialize=0) - - Declare the mixed second order partial derivative of model.z with respect - to model.t and model.l and set bounds - >>> model.dz2 = DerivativeVar(model.z, wrt=(model.t, model.l), bounds=(-10, 10)) - -.. note:: - The 'initialize' keyword argument will initialize the value of a - derivative and is **not** the same as specifying an initial - condition. Initial or boundary conditions should be specified using a - :py:class:`Constraint` or - :py:class:`ConstraintList` or - by fixing the value of a :py:class:`Var` at a boundary - point. - -Declaring Differential Equations --------------------------------- - -A differential equations is declared as a standard Pyomo -:py:class:`Constraint` and is not required to have -any particular form. The following code snippet shows how one might declare -an ordinary or partial differential equation. - -.. doctest:: - - Required imports - >>> from pyomo.environ import * - >>> from pyomo.dae import * - - >>> model = ConcreteModel() - >>> model.s = Set(initialize=['a', 'b']) - >>> model.t = ContinuousSet(bounds=(0, 5)) - >>> model.l = ContinuousSet(bounds=(-10, 10)) - - >>> model.x = Var(model.s, model.t) - >>> model.y = Var(model.t, model.l) - >>> model.dxdt = DerivativeVar(model.x, wrt=model.t) - >>> model.dydt = DerivativeVar(model.y, wrt=model.t) - >>> model.dydl2 = DerivativeVar(model.y, wrt=(model.l, model.l)) - - An ordinary differential equation - >>> def _ode_rule(m, s, t): - ... if t == 0: - ... return Constraint.Skip - ... return m.dxdt[s, t] == m.x[s, t]**2 - >>> model.ode = Constraint(model.s, model.t, rule=_ode_rule) - - A partial differential equation - >>> def _pde_rule(m, t, l): - ... if t == 0 or l == m.l.first() or l == m.l.last(): - ... return Constraint.Skip - ... return m.dydt[t, l] == m.dydl2[t, l] - >>> model.pde = Constraint(model.t, model.l, rule=_pde_rule) - -By default, a :py:class:`Constraint` declared over a -:py:class:`ContinuousSet` will be applied at every -discretization point contained in the set. Often a modeler does not want to -enforce a differential equation at one or both boundaries of a continuous -domain. This may be addressed explicitly in the -:py:class:`Constraint` declaration using -``Constraint.Skip`` as shown above. Alternatively, the desired constraints can -be deactivated just before the model is sent to a solver as shown below. - -.. doctest:: - :hide: - - >>> model.del_component('ode_index') - >>> model.del_component('pde_index') - >>> model.del_component('ode') - >>> model.del_component('pde') - -.. doctest:: - - >>> def _ode_rule(m, s, t): - ... return m.dxdt[s, t] == m.x[s, t]**2 - >>> model.ode = Constraint(model.s, model.t, rule=_ode_rule) - - >>> def _pde_rule(m, t, l): - ... return m.dydt[t, l] == m.dydl2[t, l] - >>> model.pde = Constraint(model.t, model.l, rule=_pde_rule) - - Declare other model components and apply a discretization transformation - ... - - Deactivate the differential equations at certain boundary points - >>> for con in model.ode[:, model.t.first()]: - ... con.deactivate() - - >>> for con in model.pde[0, :]: - ... con.deactivate() - - >>> for con in model.pde[:, model.l.first()]: - ... con.deactivate() - - >>> for con in model.pde[:, model.l.last()]: - ... con.deactivate() - - Solve the model - ... - -.. note:: - If you intend to use the pyomo.DAE - :py:class:`Simulator` on your model then you - **must** use **constraint deactivation** instead of **constraint - skipping** in the differential equation rule. - -Declaring Integrals -------------------- - -.. warning:: - The :py:class:`Integral` component is still under - development and considered a prototype. It currently includes only basic - functionality for simple integrals. We welcome feedback on the interface - and functionality but **we do not recommend using it** on general - models. Instead, integrals should be reformulated as differential - equations. - -.. autoclass:: pyomo.dae.Integral - :members: - -Declaring an :py:class:`Integral` component is similar to -declaring an :py:class:`Expression` component. A -simple example is shown below: - -.. doctest:: - - >>> model = ConcreteModel() - >>> model.time = ContinuousSet(bounds=(0,10)) - >>> model.X = Var(model.time) - >>> model.scale = Param(initialize=1E-3) - - >>> def _intX(m,t): - ... return m.X[t] - >>> model.intX = Integral(model.time,wrt=model.time,rule=_intX) - - >>> def _obj(m): - ... return m.scale*m.intX - >>> model.obj = Objective(rule=_obj) - -Notice that the positional arguments supplied to the -:py:class:`Integral` declaration must include all indices -needed to evaluate the integral expression. The integral expression is defined -in a function and supplied to the 'rule' keyword argument. Finally, a user must -specify a :py:class:`ContinuousSet` that the integral -is being evaluated over. This is done using the 'wrt' keyword argument. - -.. note:: - The :py:class:`ContinuousSet` specified using the - 'wrt' keyword argument must be explicitly specified as one of the indexing - sets (meaning it must be supplied as a positional argument). This is to - ensure consistency in the ordering and dimension of the indexing sets - -After an :py:class:`Integral` has been declared, it can be -used just like a Pyomo :py:class:`Expression` -component and can be included in constraints or the objective function as shown -above. - -If an :py:class:`Integral` is specified with multiple -positional arguments, i.e. multiple indexing sets, the final component will be -indexed by all of those sets except for the -:py:class:`ContinuousSet` that the integral was -taken over. In other words, the -:py:class:`ContinuousSet` specified with the -'wrt' keyword argument is removed from the indexing sets of the -:py:class:`Integral` even though it must be specified as a -positional argument. This should become more clear with the following example -showing a double integral over the -:py:class:`ContinuousSet` components ``model.t1`` and -``model.t2``. In addition, the expression is also indexed by the -:py:class:`Set` ``model.s``. The mathematical representation -and implementation in Pyomo are shown below: - -.. math:: - \sum_{s} \int_{t_2} \int_{t_1} \! X(t_1, t_2, s) \, dt_1 \, dt_2 - -.. doctest:: - - >>> model = ConcreteModel() - >>> model.t1 = ContinuousSet(bounds=(0, 10)) - >>> model.t2 = ContinuousSet(bounds=(-1, 1)) - >>> model.s = Set(initialize=['A', 'B', 'C']) - - >>> model.X = Var(model.t1, model.t2, model.s) - - >>> def _intX1(m, t1, t2, s): - ... return m.X[t1, t2, s] - >>> model.intX1 = Integral(model.t1, model.t2, model.s, wrt=model.t1, - ... rule=_intX1) - - >>> def _intX2(m, t2, s): - ... return m.intX1[t2, s] - >>> model.intX2 = Integral(model.t2, model.s, wrt=model.t2, rule=_intX2) - - >>> def _obj(m): - ... return sum(m.intX2[k] for k in m.s) - >>> model.obj = Objective(rule=_obj) - -Discretization Transformations ------------------------------- - -Before a Pyomo model with :py:class:`DerivativeVar` -or :py:class:`Integral` components can be sent to a -solver it must first be sent through a discretization transformation. These -transformations approximate any derivatives or integrals in the model by -using a numerical method. The numerical methods currently included in pyomo.DAE -discretize the continuous domains in the problem and introduce equality -constraints which approximate the derivatives and integrals at the -discretization points. Two families of discretization schemes have been -implemented in pyomo.DAE, Finite Difference and Collocation. These schemes are -described in more detail below. - -.. note:: - The schemes described here are for derivatives only. All integrals will - be transformed using the trapezoid rule. - -The user must write a Python script in order to use these discretizations, -they have not been tested on the pyomo command line. Example scripts are -shown below for each of the discretization schemes. The transformations are -applied to Pyomo model objects which can be further manipulated before being -sent to a solver. Examples of this are also shown below. - -Finite Difference Transformation -******************************** - -This transformation includes implementations of several finite -difference methods. For example, the Backward Difference method (also -called Implicit or Backward Euler) has been implemented. The -discretization equations for this method are shown below: - -.. math:: - \begin{array}{l} - \mathrm{Given: } \\ - \frac{dx}{dt} = f(t, x) , \quad x(t_0) = x_{0} \\ - \text{discretize $t$ and $x$ such that } \\ - x(t_0 + kh) = x_{k} \\ - x_{k + 1} = x_{k} + h * f(t_{k + 1}, x_{k + 1}) \\ - t_{k + 1} = t_{k} + h - \end{array} - -where :math:`h` is the step size between discretization points or the size of -each finite element. These equations are generated automatically as -:py:class:`Constraints` when the backward -difference method is applied to a Pyomo model. - -There are several discretization options available to a -``dae.finite_difference`` transformation which can be specified as keyword -arguments to the ``.apply_to()`` function of the transformation object. These -keywords are summarized below: - -.. Replace with in-code documentation. The autoclass that works with the -.. plugins: pyomo.dae.plugins.finitedifference.Finite_Difference_Transformation - - -Keyword arguments for applying a finite difference transformation: - -'nfe' - The desired number of finite element points to be included in the - discretization. The default value is 10. - -'wrt' - Indicates which :py:class:`ContinuousSet` the - transformation should be applied to. If this keyword argument is not - specified then the same scheme will be applied to every - :py:class:`ContinuousSet` . - -'scheme' - Indicates which finite difference method to apply. Options are - 'BACKWARD', 'CENTRAL', or 'FORWARD'. The default scheme is the backward - difference method. - -If the existing number of finite element points in a -:py:class:`ContinuousSet` is less than the desired -number, new discretization points will be added to the set. If a user specifies -a number of finite element points which is less than the number of points -already included in the :py:class:`ContinuousSet` then -the transformation will ignore the specified number and proceed with the larger -set of points. Discretization points will never be removed from a -:py:class:`ContinuousSet` during the discretization. - -The following code is a Python script applying the backward difference -method. The code also shows how to add a constraint to a discretized model. - -.. doctest:: - :hide: - - >>> model = ConcreteModel() - >>> model.time = ContinuousSet(bounds=(0, 10)) - >>> model.x1 = Var(model.time, bounds=(-10, 10)) - >>> model.dx1 = DerivativeVar(model.x1) - -.. doctest:: - :skipif: not ipopt_available - - Discretize model using Backward Difference method - >>> discretizer = TransformationFactory('dae.finite_difference') - >>> discretizer.apply_to(model,nfe=20,wrt=model.time,scheme='BACKWARD') - - Add another constraint to discretized model - >>> def _sum_limit(m): - ... return sum(m.x1[i] for i in m.time) <= 50 - >>> model.con_sum_limit = Constraint(rule=_sum_limit) - - Solve discretized model - >>> solver = SolverFactory('ipopt') - >>> results = solver.solve(model) - -Collocation Transformation -************************** - -This transformation uses orthogonal collocation to discretize the -differential equations in the model. Currently, two types of collocation -have been implemented. They both use Lagrange polynomials with either -Gauss-Radau roots or Gauss-Legendre roots. For more information on -orthogonal collocation and the discretization equations associated with this -method please see chapter 10 of the book "Nonlinear Programming: Concepts, -Algorithms, and Applications to Chemical Processes" by L.T. Biegler. - -The discretization options available to a ``dae.collocation`` transformation -are the same as those described above for the finite difference transformation -with different available schemes and the addition of the 'ncp' option. - -.. Replace with in-code documentation. The autoclass that works with the -.. plugins: pyomo.dae.plugins.finitedifference.Finite_Difference_Transformation - -Additional keyword arguments for collocation discretizations: - -'scheme' - The desired collocation scheme, either 'LAGRANGE-RADAU' or - 'LAGRANGE-LEGENDRE'. The default is 'LAGRANGE-RADAU'. - -'ncp' - The number of collocation points within each finite element. The - default value is 3. - -.. note:: - If the user's version of Python has access to the package Numpy then any - number of collocation points may be specified, otherwise the maximum number - is 10. - -.. note:: - Any points that exist in a - :py:class:`ContinuousSet` before discretization - will be used as finite element boundaries and not as collocation points. - The locations of the collocation points cannot be specified by the user, - they must be generated by the transformation. - -The following code is a Python script applying collocation with Lagrange -polynomials and Radau roots. The code also shows how to add an objective -function to a discretized model. - -.. doctest:: - :hide: - - >>> model = ConcreteModel() - >>> model.time = ContinuousSet(bounds=(0, 10)) - >>> model.x = Var(model.time, bounds=(-10, 10)) - >>> model.dx = DerivativeVar(model.x) - >>> model.x_ref = Param(initialize=5) - -.. doctest:: - :skipif: not ipopt_available - - Discretize model using Radau Collocation - >>> discretizer = TransformationFactory('dae.collocation') - >>> discretizer.apply_to(model,nfe=20,ncp=6,scheme='LAGRANGE-RADAU') - - Add objective function after model has been discretized - >>> def obj_rule(m): - ... return sum((m.x[i]-m.x_ref)**2 for i in m.time) - >>> model.obj = Objective(rule=obj_rule) - - Solve discretized model - >>> solver = SolverFactory('ipopt') - >>> results = solver.solve(model) - -Restricting Optimal Control Profiles -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -When solving an optimal control problem a user may want to restrict the -number of degrees of freedom for the control input by forcing, for example, -a piecewise constant profile. Pyomo.DAE provides the -``reduce_collocation_points`` function to address this use-case. This function -is used in conjunction with the ``dae.collocation`` discretization -transformation to reduce the number of free collocation points within a finite -element for a particular variable. - -.. autoclass:: pyomo.dae.plugins.colloc.Collocation_Discretization_Transformation - :members: reduce_collocation_points - -An example of using this function is shown below: - -.. doctest:: - :hide: - - >>> model = ConcreteModel() - >>> model.time = ContinuousSet(bounds=(0, 10)) - >>> model.x = Var(model.time, bounds=(-10, 10)) - >>> model.dx = DerivativeVar(model.x) - >>> model.x_ref = Param(initialize=5) - >>> model.u = Var(model.time) - -.. doctest:: - - >>> discretizer = TransformationFactory('dae.collocation') - >>> discretizer.apply_to(model, nfe=10, ncp=6) - >>> model = discretizer.reduce_collocation_points(model, - ... var=model.u, - ... ncp=1, - ... contset=model.time) - -In the above example, the ``reduce_collocation_points`` function restricts -the variable ``model.u`` to have only **1** free collocation point per -finite element, thereby enforcing a piecewise constant profile. -:numref:`Fig. %s ` shows the solution profile before and -after applying -the ``reduce_collocation_points`` function. - -.. _reduce_points_fig: -.. figure:: reduce_points_demo.png - :scale: 100 % - :align: center - - (left) Profile before applying the ``reduce_collocation_points`` - function (right) Profile after applying the function, restricting - ``model.u`` to have a piecewise constant profile. - - -Applying Multiple Discretization Transformations -************************************************ - -Discretizations can be applied independently to each -:py:class:`ContinuousSet` in a model. This allows the -user great flexibility in discretizing their model. For example the same -numerical method can be applied with different resolutions: - -.. doctest:: - :hide: - - >>> model = ConcreteModel() - >>> model.t1 = ContinuousSet(bounds=(0, 10)) - >>> model.t2 = ContinuousSet(bounds=(-2, 2)) - -.. doctest:: - - >>> discretizer = TransformationFactory('dae.finite_difference') - >>> discretizer.apply_to(model,wrt=model.t1,nfe=10) - >>> discretizer.apply_to(model,wrt=model.t2,nfe=100) - -This also allows the user to combine different methods. For example, applying -the forward difference method to one -:py:class:`ContinuousSet` and the central finite -difference method to another -:py:class:`ContinuousSet`: - -.. doctest:: - :hide: - - >>> model = ConcreteModel() - >>> model.t1 = ContinuousSet(bounds=(0, 10)) - >>> model.t2 = ContinuousSet(bounds=(-2, 2)) - -.. doctest:: - - >>> discretizer = TransformationFactory('dae.finite_difference') - >>> discretizer.apply_to(model,wrt=model.t1,scheme='FORWARD') - >>> discretizer.apply_to(model,wrt=model.t2,scheme='CENTRAL') - -In addition, the user may combine finite difference and collocation -discretizations. For example: - -.. doctest:: - :hide: - - >>> model = ConcreteModel() - >>> model.t1 = ContinuousSet(bounds=(0, 10)) - >>> model.t2 = ContinuousSet(bounds=(-2, 2)) - -.. doctest:: - - >>> disc_fe = TransformationFactory('dae.finite_difference') - >>> disc_fe.apply_to(model,wrt=model.t1,nfe=10) - >>> disc_col = TransformationFactory('dae.collocation') - >>> disc_col.apply_to(model,wrt=model.t2,nfe=10,ncp=5) - -If the user would like to apply the same discretization to all -:py:class:`ContinuousSet` components in a model, just -specify the discretization once without the 'wrt' keyword argument. This will -apply that scheme to all :py:class:`ContinuousSet` -components in the model that haven't already been discretized. - -Custom Discretization Schemes -***************************** - -A transformation framework along with certain utility functions has been -created so that advanced users may easily implement custom discretization -schemes other than those listed above. The transformation framework consists of -the following steps: - - 1. Specify Discretization Options - 2. Discretize the ContinuousSet(s) - 3. Update Model Components - 4. Add Discretization Equations - 5. Return Discretized Model - -If a user would like to create a custom finite difference scheme then they only -have to worry about step (4) in the framework. The discretization equations for -a particular scheme have been isolated from of the rest of the code for -implementing the transformation. The function containing these discretization -equations can be found at the top of the source code file for the -transformation. For example, below is the function for the forward -difference method: - -.. code-block:: python - - def _forward_transform(v,s): - """ - Applies the Forward Difference formula of order O(h) for first derivatives - """ - def _fwd_fun(i): - tmp = sorted(s) - idx = tmp.index(i) - return 1/(tmp[idx+1]-tmp[idx])*(v(tmp[idx+1])-v(tmp[idx])) - return _fwd_fun - -In this function, 'v' represents the continuous variable or function that the -method is being applied to. 's' represents the set of discrete points in the -continuous domain. In order to implement a custom finite difference method, a -user would have to copy the above function and just replace the equation next -to the first return statement with their method. - -After implementing a custom finite difference method using the above function -template, the only other change that must be made is to add the custom method -to the 'all_schemes' dictionary in the ``dae.finite_difference`` -class. - -In the case of a custom collocation method, changes will have to be made in -steps (2) and (4) of the transformation framework. In addition to implementing -the discretization equations, the user would also have to ensure that the -desired collocation points are added to the ContinuousSet being discretized. - -Dynamic Model Simulation ------------------------- - -The pyomo.dae Simulator class can be used to simulate systems of ODEs and -DAEs. It provides an interface to integrators available in other Python -packages. - -.. note:: - The pyomo.dae Simulator does not include integrators directly. The user - must have at least one of the supported Python packages installed in - order to use this class. - -.. autoclass:: pyomo.dae.Simulator - :members: - -.. note:: - Any keyword options supported by the integrator may be specified as - keyword options to the simulate function and will be passed to the - integrator. - -Supported Simulator Packages -**************************** - -The Simulator currently includes interfaces to SciPy and CasADi. ODE -simulation is supported in both packages however, DAE simulation is only -supported by CasADi. A list of available integrators for each package is -given below. Please refer to the `SciPy -`_ -and `CasADi -`_ documentation directly for the most up-to-date information about -these packages and for more information about the various integrators and -options. - -SciPy Integrators: - - **'vode'** : Real-valued Variable-coefficient ODE solver, options for - non-stiff and stiff systems - - **'zvode'** : Complex-values Variable-coefficient ODE solver, options for - non-stiff and stiff systems - - **'lsoda'** : Real-values Variable-coefficient ODE solver, automatic - switching of algorithms for non-stiff or stiff systems - - **'dopri5'** : Explicit runge-kutta method of order (4)5 ODE solver - - **'dop853'** : Explicit runge-kutta method of order 8(5,3) ODE solver - -CasADi Integrators: - - **'cvodes'** : CVodes from the Sundials suite, solver for stiff or - non-stiff ODE systems - - **'idas'** : IDAS from the Sundials suite, DAE solver - - **'collocation'** : Fixed-step implicit runge-kutta method, ODE/DAE - solver - - **'rk'** : Fixed-step explicit runge-kutta method, ODE solver - -Using the Simulator -******************* - -We now show how to use the Simulator to simulate the following system of ODEs: - -.. math:: - \begin{array}{l} - \frac{d\theta}{dt} = \omega \\ - \frac{d\omega}{dt} = -b*\omega -c*sin(\theta) - \end{array} - -We begin by formulating the model using pyomo.DAE - -.. doctest:: - - >>> m = ConcreteModel() - - >>> m.t = ContinuousSet(bounds=(0.0, 10.0)) - - >>> m.b = Param(initialize=0.25) - >>> m.c = Param(initialize=5.0) - - >>> m.omega = Var(m.t) - >>> m.theta = Var(m.t) - - >>> m.domegadt = DerivativeVar(m.omega, wrt=m.t) - >>> m.dthetadt = DerivativeVar(m.theta, wrt=m.t) - - Setting the initial conditions - >>> m.omega[0].fix(0.0) - >>> m.theta[0].fix(3.14 - 0.1) - - >>> def _diffeq1(m, t): - ... return m.domegadt[t] == -m.b * m.omega[t] - m.c * sin(m.theta[t]) - >>> m.diffeq1 = Constraint(m.t, rule=_diffeq1) - - >>> def _diffeq2(m, t): - ... return m.dthetadt[t] == m.omega[t] - >>> m.diffeq2 = Constraint(m.t, rule=_diffeq2) - -Notice that the initial conditions are set by `fixing` the values of -``m.omega`` and ``m.theta`` at t=0 instead of being specified as extra -equality constraints. Also notice that the differential equations are -specified without using ``Constraint.Skip`` to skip enforcement at t=0. The -Simulator cannot simulate any constraints that contain if-statements in -their construction rules. - -To simulate the model you must first create a Simulator object. Building -this object prepares the Pyomo model for simulation with a particular Python -package and performs several checks on the model to ensure compatibility -with the Simulator. Be sure to read through the list of limitations at the -end of this section to understand the types of models supported by the -Simulator. - -.. doctest:: - - >>> sim = Simulator(m, package='scipy') # doctest: +SKIP - -After creating a Simulator object, the model can be simulated by calling the -simulate function. Please see the API documentation for the -:py:class:`Simulator` for more information about the -valid keyword arguments for this function. - -.. doctest:: - - >>> tsim, profiles = sim.simulate(numpoints=100, integrator='vode') # doctest: +SKIP - -The ``simulate`` function returns numpy arrays containing time points and -the corresponding values for the dynamic variable profiles. - -`Simulator Limitations`: - - Differential equations must be first-order and separable - - Model can only contain a single ContinuousSet - - Can't simulate constraints with if-statements in the construction rules - - Need to provide initial conditions for dynamic states by setting the - value or using fix() - -Specifying Time-Varying Inputs -****************************** -The :py:class:`Simulator` supports simulation of a system -of ODE's or DAE's with time-varying parameters or control inputs. Time-varying -inputs can be specified using a Pyomo ``Suffix``. We currently only support -piecewise constant profiles. For more complex inputs defined by a continuous -function of time we recommend adding an algebraic variable and constraint to -your model. - -The profile for a time-varying input should be specified -using a Python dictionary where the keys correspond to the switching times -and the values correspond to the value of the input at a time point. A -``Suffix`` is then used to associate this dictionary with the appropriate -``Var`` or ``Param`` and pass the information to the -:py:class:`Simulator`. The code snippet below shows an -example. - -.. doctest:: - - >>> m = ConcreteModel() - - >>> m.t = ContinuousSet(bounds=(0.0, 20.0)) - - Time-varying inputs - >>> m.b = Var(m.t) - >>> m.c = Param(m.t, default=5.0) - - >>> m.omega = Var(m.t) - >>> m.theta = Var(m.t) - - >>> m.domegadt = DerivativeVar(m.omega, wrt=m.t) - >>> m.dthetadt = DerivativeVar(m.theta, wrt=m.t) - - Setting the initial conditions - >>> m.omega[0] = 0.0 - >>> m.theta[0] = 3.14 - 0.1 - - >>> def _diffeq1(m, t): - ... return m.domegadt[t] == -m.b[t] * m.omega[t] - \ - ... m.c[t] * sin(m.theta[t]) - >>> m.diffeq1 = Constraint(m.t, rule=_diffeq1) - - >>> def _diffeq2(m, t): - ... return m.dthetadt[t] == m.omega[t] - >>> m.diffeq2 = Constraint(m.t, rule=_diffeq2) - - Specifying the piecewise constant inputs - >>> b_profile = {0: 0.25, 15: 0.025} - >>> c_profile = {0: 5.0, 7: 50} - - Declaring a Pyomo Suffix to pass the time-varying inputs to the Simulator - >>> m.var_input = Suffix(direction=Suffix.LOCAL) - >>> m.var_input[m.b] = b_profile - >>> m.var_input[m.c] = c_profile - - Simulate the model using scipy - >>> sim = Simulator(m, package='scipy') # doctest: +SKIP - >>> tsim, profiles = sim.simulate(numpoints=100, - ... integrator='vode', - ... varying_inputs=m.var_input) # doctest: +SKIP - -.. note:: - The Simulator does not support multi-indexed inputs (i.e. if ``m.b`` in - the above example was indexed by another set besides ``m.t``) - -Dynamic Model Initialization ----------------------------- -Providing a good initial guess is an important factor in solving dynamic -optimization problems. There are several model initialization tools under -development in pyomo.DAE to help users initialize their models. These tools -will be documented here as they become available. - -From Simulation -*************** -The :py:class:`Simulator` includes a function for -initializing discretized dynamic optimization models using the profiles -returned from the simulator. An example using this function is shown below - -.. doctest:: - - Simulate the model using scipy - >>> sim = Simulator(m, package='scipy') # doctest: +SKIP - >>> tsim, profiles = sim.simulate(numpoints=100, integrator='vode', - ... varying_inputs=m.var_input) # doctest: +SKIP - - Discretize the model using Orthogonal Collocation - >>> discretizer = TransformationFactory('dae.collocation') - >>> discretizer.apply_to(m, nfe=10, ncp=3) - - Initialize the discretized model using the simulator profiles - >>> sim.initialize_model() # doctest: +SKIP - -.. note:: - A model must be simulated before it can be initialized using this function diff --git a/doc/Archive/modeling_extensions/gdp/concepts.rst b/doc/Archive/modeling_extensions/gdp/concepts.rst deleted file mode 100644 index 95629bc48fd..00000000000 --- a/doc/Archive/modeling_extensions/gdp/concepts.rst +++ /dev/null @@ -1,151 +0,0 @@ -.. image:: /../logos/gdp/Pyomo-GDP-150.png - :scale: 20% - :class: no-scaled-link - :align: right - -************ -Key Concepts -************ - -Generalized Disjunctive Programming (GDP) provides a way to bridge high-level propositional logic and algebraic constraints. -The GDP standard form from the :ref:`index page ` is repeated below. - -.. math:: - - \min\ obj = &\ f(x, z) \\ - \text{s.t.} \quad &\ Ax+Bz \leq d\\ - &\ g(x,z) \leq 0\\ - &\ \bigvee_{i\in D_k} \left[ - \begin{gathered} - Y_{ik} \\ - M_{ik} x + N_{ik} z \leq e_{ik} \\ - r_{ik}(x,z)\leq 0\\ - \end{gathered} - \right] \quad k \in K\\ - &\ \Omega(Y) = True \\ - &\ x \in X \subseteq \mathbb{R}^n\\ - &\ Y \in \{True, False\}^{p}\\ - &\ z \in Z \subseteq \mathbb{Z}^m - -Original support in Pyomo.GDP focused on the disjuncts and disjunctions, allowing the modelers to group relational expressions in disjuncts, with disjunctions describing logical-OR relationships between the groupings. -As a result, we implemented the ``Disjunct`` and ``Disjunction`` objects before ``BooleanVar`` and the rest of the logical expression system. -Accordingly, we also describe the disjuncts and disjunctions first below. - -Disjuncts -========= - -Disjuncts represent groupings of relational expressions (e.g. algebraic constraints) summarized by a Boolean indicator variable :math:`Y` through implication: - -.. math:: - - \left. - \begin{aligned} - & Y_{ik} \Rightarrow & M_{ik} x + N_{ik} z &\leq e_{ik}\\ - & Y_{ik} \Rightarrow & r_{ik}(x,z) &\leq 0 - \end{aligned} - \right.\qquad \forall i \in D_k, \forall k \in K - - -Logically, this means that if :math:`Y_{ik} = True`, then the constraints :math:`M_{ik} x + N_{ik} z \leq e_{ik}` and :math:`r_{ik}(x,z) \leq 0` must be satisfied. -However, if :math:`Y_{ik} = False`, then the corresponding constraints are ignored. -Note that :math:`Y_{ik} = False` does **not** imply that the corresponding constraints are *violated*. - -.. _gdp-disjunctions-concept: - -Disjunctions -============ - -Disjunctions describe a logical *OR* relationship between two or more Disjuncts. -The simplest and most common case is a 2-term disjunction: - -.. math:: - - \left[\begin{gathered} - Y_1 \\ - \exp(x_2) - 1 = x_1 \\ - x_3 = x_4 = 0 - \end{gathered} - \right] \bigvee \left[\begin{gathered} - Y_2 \\ - \exp\left(\frac{x_4}{1.2}\right) - 1 = x_3 \\ - x_1 = x_2 = 0 - \end{gathered} - \right] - - -The disjunction above describes the selection between two units in a process network. -:math:`Y_1` and :math:`Y_2` are the Boolean variables corresponding to the selection of process units 1 and 2, respectively. -The continuous variables :math:`x_1, x_2, x_3, x_4` describe flow in and out of the first and second units, respectively. -If a unit is selected, the nonlinear equality in the corresponding disjunct enforces the input/output relationship in the selected unit. -The final equality in each disjunct forces flows for the absent unit to zero. - -Boolean Variables -================= - -Boolean variables are decision variables that may take a value of ``True`` or ``False``. -These are most often encountered as the indicator variables of disjuncts. -However, they can also be independently defined to represent other problem decisions. - -.. note:: - - Boolean variables are not intended to participate in algebraic expressions. - That is, :math:`3 \times \text{True}` does not make sense; hence, :math:`x = 3 Y_1` does not make sense. - Instead, you may have the disjunction - - .. math:: - - \left[\begin{gathered} - Y_1 \\ - x = 3 - \end{gathered} - \right] \bigvee \left[\begin{gathered} - \neg Y_1 \\ - x = 0 - \end{gathered} - \right] - -Logical Propositions -==================== - -Logical propositions are constraints describing relationships between the Boolean variables in the model. - -These logical propositions can include: - -.. |neg| replace:: :math:`\neg Y_1` -.. |equiv| replace:: :math:`Y_1 \Leftrightarrow Y_2` -.. |land| replace:: :math:`Y_1 \land Y_2` -.. |lor| replace:: :math:`Y_1 \lor Y_2` -.. |xor| replace:: :math:`Y_1 \veebar Y_2` -.. |impl| replace:: :math:`Y_1 \Rightarrow Y_2` - -+-----------------+---------+-------------+-------------+-------------+ -| Operator | Example | :math:`Y_1` | :math:`Y_2` | Result | -+=================+=========+=============+=============+=============+ -| Negation | |neg| | | ``True`` | | | ``False`` | -| | | | ``False`` | | | ``True`` | -+-----------------+---------+-------------+-------------+-------------+ -| Equivalence | |equiv| | | ``True`` | | ``True`` | | ``True`` | -| | | | ``True`` | | ``False`` | | ``False`` | -| | | | ``False`` | | ``True`` | | ``False`` | -| | | | ``False`` | | ``False`` | | ``True`` | -+-----------------+---------+-------------+-------------+-------------+ -| Conjunction | |land| | | ``True`` | | ``True`` | | ``True`` | -| | | | ``True`` | | ``False`` | | ``False`` | -| | | | ``False`` | | ``True`` | | ``False`` | -| | | | ``False`` | | ``False`` | | ``False`` | -+-----------------+---------+-------------+-------------+-------------+ -| Disjunction | |lor| | | ``True`` | | ``True`` | | ``True`` | -| | | | ``True`` | | ``False`` | | ``True`` | -| | | | ``False`` | | ``True`` | | ``True`` | -| | | | ``False`` | | ``False`` | | ``False`` | -+-----------------+---------+-------------+-------------+-------------+ -| Exclusive OR | |xor| | | ``True`` | | ``True`` | | ``False`` | -| | | | ``True`` | | ``False`` | | ``True`` | -| | | | ``False`` | | ``True`` | | ``True`` | -| | | | ``False`` | | ``False`` | | ``False`` | -+-----------------+---------+-------------+-------------+-------------+ -| Implication | |impl| | | ``True`` | | ``True`` | | ``True`` | -| | | | ``True`` | | ``False`` | | ``False`` | -| | | | ``False`` | | ``True`` | | ``True`` | -| | | | ``False`` | | ``False`` | | ``True`` | -+-----------------+---------+-------------+-------------+-------------+ diff --git a/doc/Archive/modeling_extensions/gdp/index.rst b/doc/Archive/modeling_extensions/gdp/index.rst deleted file mode 100644 index 0c8529c60cb..00000000000 --- a/doc/Archive/modeling_extensions/gdp/index.rst +++ /dev/null @@ -1,79 +0,0 @@ -.. _gdp-main-page: - -*********************************** -Generalized Disjunctive Programming -*********************************** - -.. image:: /../logos/gdp/Pyomo-GDP-150.png - :scale: 35% - :align: right - :class: no-scaled-link - -The Pyomo.GDP modeling extension\ [#gdp-main-paper]_ provides support for Generalized Disjunctive Programming (GDP)\ [#gdp]_, an extension of Disjunctive Programming\ [#dp]_ from the operations research community to include nonlinear relationships. The classic form for a GDP is given by: - -.. math:: - - \min\ obj = &\ f(x, z) \\ - \text{s.t.} \quad &\ Ax+Bz \leq d\\ - &\ g(x,z) \leq 0\\ - &\ \bigvee_{i\in D_k} \left[ - \begin{gathered} - Y_{ik} \\ - M_{ik} x + N_{ik} z \leq e_{ik} \\ - r_{ik}(x,z)\leq 0\\ - \end{gathered} - \right] \quad k \in K\\ - &\ \Omega(Y) = True \\ - &\ x \in X \subseteq \mathbb{R}^n\\ - &\ Y \in \{True, False\}^{p}\\ - &\ z \in Z \subseteq \mathbb{Z}^m - -Here, we have the minimization of an objective :math:`obj` subject to global linear constraints :math:`Ax+Bz \leq d` and nonlinear constraints :math:`g(x,z) \leq 0`, with conditional linear constraints :math:`M_{ik} x + N_{ik} z \leq e_{ik}` and nonlinear constraints :math:`r_{ik}(x,z)\leq 0`. -These conditional constraints are collected into disjuncts :math:`D_k`, organized into disjunctions :math:`K`. Finally, there are logical propositions :math:`\Omega(Y) = True`. -Decision/state variables can be continuous :math:`x`, Boolean :math:`Y`, and/or integer :math:`z`. - -GDP is useful to model discrete decisions that have implications on the system behavior\ [#gdpreview]_. -For example, in process design, a disjunction may model the choice between processes A and B. -If A is selected, then its associated equations and inequalities will apply; otherwise, if B is selected, then its respective constraints should be enforced. - -Modelers often ask to model if-then-else relationships. -These can be expressed as a disjunction as follows: - -.. math:: - :nowrap: - - \begin{gather*} - \left[\begin{gathered} - Y_1 \\ - \text{constraints} \\ - \text{for }\textit{then} - \end{gathered}\right] - \vee - \left[\begin{gathered} - Y_2 \\ - \text{constraints} \\ - \text{for }\textit{else} - \end{gathered}\right] \\ - Y_1 \veebar Y_2 - \end{gather*} - -Here, if the Boolean :math:`Y_1` is ``True``, then the constraints in the first disjunct are enforced; otherwise, the constraints in the second disjunct are enforced. -The following sections describe the key concepts, modeling, and solution approaches available for Generalized Disjunctive Programming. - -.. toctree:: - :caption: Pyomo.GDP Contents - :maxdepth: 2 - - concepts - modeling - solving - -Literature References -===================== -.. [#gdp-main-paper] Chen, Q., Johnson, E. S., Bernal, D. E., Valentin, R., Kale, S., Bates, J., Siirola, J. D. and Grossmann, I. E. (2021). Pyomo.GDP: an ecosystem for logic based modeling and optimization development, *Optimization and Engineering* (pp. 1-36).https://doi.org/10.1007/s11081-021-09601-7 - -.. [#gdp] Raman, R., & Grossmann, I. E. (1994). Modelling and computational techniques for logic based integer programming. *Computers & Chemical Engineering*, 18(7), 563–578. https://doi.org/10.1016/0098-1354(93)E0010-7 - -.. [#dp] Balas, E. (1985). Disjunctive Programming and a Hierarchy of Relaxations for Discrete Optimization Problems. *SIAM Journal on Algebraic Discrete Methods*, 6(3), 466–486. https://doi.org/10.1137/0606047 - -.. [#gdpreview] Grossmann, I. E., & Trespalacios, F. (2013). Systematic modeling of discrete-continuous optimization models through generalized disjunctive programming. *AIChE Journal*, 59(9), 3276–3295. https://doi.org/10.1002/aic.14088 diff --git a/doc/Archive/modeling_extensions/gdp/modeling.rst b/doc/Archive/modeling_extensions/gdp/modeling.rst deleted file mode 100644 index 996ebcb0366..00000000000 --- a/doc/Archive/modeling_extensions/gdp/modeling.rst +++ /dev/null @@ -1,419 +0,0 @@ -.. image:: /../logos/gdp/Pyomo-GDP-150.png - :scale: 20% - :class: no-scaled-link - :align: right - -********************* -Modeling in Pyomo.GDP -********************* - -.. testsetup:: - - from pyomo.environ import ( - ConcreteModel, RangeSet, BooleanVar, LogicalConstraint, - TransformationFactory, atleast, SolverFactory, Objective, - Constraint, Var, land, Reference - ) - from pyomo.gdp import Disjunct, Disjunction - from pyomo.core.plugins.transform.logical_to_linear import update_boolean_vars_from_binary - - # This is to make unicode comparison work in python 2.7. - import sys - if sys.version[0] == '2': - reload(sys) - sys.setdefaultencoding("utf-8") - -Disjunctions -============ - -To demonstrate modeling with disjunctions in Pyomo.GDP, we revisit the small example from :ref:`the previous page `. - -.. math:: - - \left[\begin{gathered} - Y_1 \\ - \exp(x_2) - 1 = x_1 \\ - x_3 = x_4 = 0 - \end{gathered} - \right] \bigvee \left[\begin{gathered} - Y_2 \\ - \exp\left(\frac{x_4}{1.2}\right) - 1 = x_3 \\ - x_1 = x_2 = 0 - \end{gathered} - \right] - -Explicit syntax: more descriptive ---------------------------------- - -Pyomo.GDP explicit syntax (see below) provides more clarity in the declaration of each modeling object, and gives the user explicit control over the ``Disjunct`` names. -Assuming the ``ConcreteModel`` object :code:`m` and variables have been defined, lines 1 and 5 declare the ``Disjunct`` objects corresponding to selection of unit 1 and 2, respectively. -Lines 2 and 6 define the input-output relations for each unit, and lines 3-4 and 7-8 enforce zero flow through the unit that is not selected. -Finally, line 9 declares the logical disjunction between the two disjunctive terms. - -.. code-block:: python - :linenos: - - m.unit1 = Disjunct() - m.unit1.inout = Constraint(expr=exp(m.x[2]) - 1 == m.x[1]) - m.unit1.no_unit2_flow1 = Constraint(expr=m.x[3] == 0) - m.unit1.no_unit2_flow2 = Constraint(expr=m.x[4] == 0) - m.unit2 = Disjunct() - m.unit2.inout = Constraint(expr=exp(m.x[4] / 1.2) - 1 == m.x[3]) - m.unit2.no_unit1_flow1 = Constraint(expr=m.x[1] == 0) - m.unit2.no_unit1_flow2 = Constraint(expr=m.x[2] == 0) - m.use_unit1or2 = Disjunction(expr=[m.unit1, m.unit2]) - -The indicator variables for each disjunct :math:`Y_1` and :math:`Y_2` are automatically generated by Pyomo.GDP, accessible via :code:`m.unit1.indicator_var` and :code:`m.unit2.indicator_var`. - -Compact syntax: more concise ----------------------------- - -For more advanced users, a compact syntax is also available below, taking advantage of the ability to declare disjuncts and constraints implicitly. -When the ``Disjunction`` object constructor is passed a list of lists, the outer list defines the disjuncts and the inner list defines the constraint expressions associated with the respective disjunct. - -.. code-block:: python - :linenos: - - m.use1or2 = Disjunction(expr=[ - # First disjunct - [exp(m.x[2])-1 == m.x[1], - m.x[3] == 0, m.x[4] == 0], - # Second disjunct - [exp(m.x[4]/1.2)-1 == m.x[3], - m.x[1] == 0, m.x[2] == 0]]) - -.. note:: - - By default, Pyomo.GDP ``Disjunction`` objects enforce an implicit "exactly one" relationship among the selection of the disjuncts (generalization of exclusive-OR). - That is, exactly one of the ``Disjunct`` indicator variables should take a ``True`` value. - This can be seen as an implicit logical proposition, in our example, :math:`Y_1 \veebar Y_2`. - -Logical Propositions -==================== - -Pyomo.GDP also supports the use of logical propositions through the use of the ``BooleanVar`` and ``LogicalConstraint`` objects. -The ``BooleanVar`` object in Pyomo represents Boolean variables, analogous to ``Var`` for numeric variables. -``BooleanVar`` can be indexed over a Pyomo ``Set``, as below: - -.. doctest:: - - >>> m = ConcreteModel() - >>> m.my_set = RangeSet(4) - >>> m.Y = BooleanVar(m.my_set) - >>> m.Y.display() - Y : Size=4, Index=my_set - Key : Value : Fixed : Stale - 1 : None : False : True - 2 : None : False : True - 3 : None : False : True - 4 : None : False : True - -Using these Boolean variables, we can define ``LogicalConstraint`` objects, analogous to algebraic ``Constraint`` objects. - -.. doctest:: - - >>> m.p = LogicalConstraint(expr=m.Y[1].implies(m.Y[2] & m.Y[3]) | m.Y[4]) - >>> m.p.pprint() - p : Size=1, Index=None, Active=True - Key : Body : Active - None : (Y[1] --> Y[2] ∧ Y[3]) ∨ Y[4] : True - -Supported Logical Operators ---------------------------- - -Pyomo.GDP logical expression system supported operators and their usage are listed in the table below. - -+--------------+------------------------+-----------------------------------+--------------------------------+ -| Operator | Operator | Method | Function | -+==============+========================+===================================+================================+ -| Negation | :code:`~Y[1]` | | :code:`lnot(Y[1])` | -+--------------+------------------------+-----------------------------------+--------------------------------+ -| Conjunction | :code:`Y[1] & Y[2]` | :code:`Y[1].land(Y[2])` | :code:`land(Y[1],Y[2])` | -+--------------+------------------------+-----------------------------------+--------------------------------+ -| Disjunction | :code:`Y[1] | Y[2]` | :code:`Y[1].lor(Y[2])` | :code:`lor(Y[1],Y[2])` | -+--------------+------------------------+-----------------------------------+--------------------------------+ -| Exclusive OR | :code:`Y[1] ^ Y[2]` | :code:`Y[1].xor(Y[2])` | :code:`xor(Y[1], Y[2])` | -+--------------+------------------------+-----------------------------------+--------------------------------+ -| Implication | | :code:`Y[1].implies(Y[2])` | :code:`implies(Y[1], Y[2])` | -+--------------+------------------------+-----------------------------------+--------------------------------+ -| Equivalence | | :code:`Y[1].equivalent_to(Y[2])` | :code:`equivalent(Y[1], Y[2])` | -+--------------+------------------------+-----------------------------------+--------------------------------+ - -.. note:: - - We omit support for some infix operators, e.g. :code:`Y[1] >> Y[2]`, due to concerns about non-intuitive Python operator precedence. - That is :code:`Y[1] | Y[2] >> Y[3]` would translate to :math:`Y_1 \lor (Y_2 \Rightarrow Y_3)` rather than :math:`(Y_1 \lor Y_2) \Rightarrow Y_3` - -In addition, the following constraint-programming-inspired operators are provided: ``exactly``, ``atmost``, and ``atleast``. -These predicates enforce, respectively, that exactly, at most, or at least N of their ``BooleanVar`` arguments are ``True``. - -Usage: - -- :code:`atleast(3, Y[1], Y[2], Y[3])` -- :code:`atmost(3, Y)` -- :code:`exactly(3, Y)` - -.. doctest:: - - >>> m = ConcreteModel() - >>> m.my_set = RangeSet(4) - >>> m.Y = BooleanVar(m.my_set) - >>> m.p = LogicalConstraint(expr=atleast(3, m.Y)) - >>> m.p.pprint() - p : Size=1, Index=None, Active=True - Key : Body : Active - None : atleast(3: [Y[1], Y[2], Y[3], Y[4]]) : True - >>> TransformationFactory('core.logical_to_linear').apply_to(m) - >>> # constraint auto-generated by transformation - >>> m.logic_to_linear.transformed_constraints.pprint() - transformed_constraints : Size=1, Index={1}, Active=True - Key : Lower : Body : Upper : Active - 1 : 3.0 : Y_asbinary[1] + Y_asbinary[2] + Y_asbinary[3] + Y_asbinary[4] : +Inf : True - -We elaborate on the ``logical_to_linear`` transformation :ref:`on the next page `. - -Indexed logical constraints ---------------------------- - -Like ``Constraint`` objects for algebraic expressions, ``LogicalConstraint`` objects can be indexed. -An example of this usage may be found below for the expression: - -.. math:: - - Y_{i+1} \Rightarrow Y_{i}, \quad i \in \{1, 2, \dots, n-1\} - -.. doctest:: - - >>> m = ConcreteModel() - >>> n = 5 - >>> m.I = RangeSet(n) - >>> m.Y = BooleanVar(m.I) - - >>> @m.LogicalConstraint(m.I) - ... def p(m, i): - ... return m.Y[i+1].implies(m.Y[i]) if i < n else Constraint.Skip - - >>> m.p.pprint() - p : Size=4, Index=I, Active=True - Key : Body : Active - 1 : Y[2] --> Y[1] : True - 2 : Y[3] --> Y[2] : True - 3 : Y[4] --> Y[3] : True - 4 : Y[5] --> Y[4] : True - -Integration with Disjunctions ------------------------------ - -.. note:: - - Historically, the ``indicator_var`` on ``Disjunct`` objects was - implemented as a binary ``Var``. Beginning in Pyomo 6.0, that has - been changed to the more mathematically correct ``BooleanVar``, with - the associated binary variable available as - ``binary_indicator_var``. - -The logical expression system is designed to augment the previously -introduced ``Disjunct`` and ``Disjunction`` components. Mathematically, -the disjunct indicator variable is Boolean, and can be used directly in -logical propositions. - -Here, we demonstrate this capability with a toy example: - -.. math:: - - \min~&x\\ - \text{s.t.}~&\left[\begin{gathered}Y_1\\x \geq 2\end{gathered}\right] \vee \left[\begin{gathered}Y_2\\x \geq 3\end{gathered}\right]\\ - &\left[\begin{gathered}Y_3\\x \leq 8\end{gathered}\right] \vee \left[\begin{gathered}Y_4\\x = 2.5\end{gathered}\right] \\ - &Y_1 \veebar Y_2\\ - &Y_3 \veebar Y_4\\ - &Y_1 \Rightarrow Y_4 - -.. doctest:: - :skipif: not glpk_available - - >>> m = ConcreteModel() - >>> m.s = RangeSet(4) - >>> m.ds = RangeSet(2) - >>> m.d = Disjunct(m.s) - >>> m.djn = Disjunction(m.ds) - >>> m.djn[1] = [m.d[1], m.d[2]] - >>> m.djn[2] = [m.d[3], m.d[4]] - >>> m.x = Var(bounds=(-2, 10)) - >>> m.d[1].c = Constraint(expr=m.x >= 2) - >>> m.d[2].c = Constraint(expr=m.x >= 3) - >>> m.d[3].c = Constraint(expr=m.x <= 8) - >>> m.d[4].c = Constraint(expr=m.x == 2.5) - >>> m.o = Objective(expr=m.x) - - >>> # Add the logical proposition - >>> m.p = LogicalConstraint( - ... expr=m.d[1].indicator_var.implies(m.d[4].indicator_var)) - >>> # Note: the implicit XOR enforced by m.djn[1] and m.djn[2] still apply - - >>> # Apply the Big-M reformulation: It will convert the logical - >>> # propositions to algebraic expressions. - >>> TransformationFactory('gdp.bigm').apply_to(m) - - >>> # Before solve, Boolean vars have no value - >>> Reference(m.d[:].indicator_var).display() - IndexedBooleanVar : Size=4, Index=s, ReferenceTo=d[:].indicator_var - Key : Value : Fixed : Stale - 1 : None : False : True - 2 : None : False : True - 3 : None : False : True - 4 : None : False : True - - >>> # Solve the reformulated model - >>> run_data = SolverFactory('glpk').solve(m) - >>> Reference(m.d[:].indicator_var).display() - IndexedBooleanVar : Size=4, Index=s, ReferenceTo=d[:].indicator_var - Key : Value : Fixed : Stale - 1 : True : False : False - 2 : False : False : False - 3 : False : False : False - 4 : True : False : False - -.. _gdp-advanced-examples: - -Advanced LogicalConstraint Examples -=================================== - -Support for complex nested expressions is a key benefit of the logical expression system. -Below are examples of expressions that we support, and with some, an explanation of their implementation. - -Composition of standard operators ---------------------------------- - -.. math:: - Y_1 \vee Y_2 \implies Y_3 \wedge \neg Y_4 \wedge (Y_5 \vee Y_6) - -.. code:: - - m.p = LogicalConstraint(expr=(m.Y[1] | m.Y[2]).implies( - m.Y[3] & ~m.Y[4] & (m.Y[5] | m.Y[6])) - ) - -Expressions within CP-type operators ------------------------------------- - -.. math:: - \text{atleast}(3, Y_1, Y_2 \vee Y_3, Y_4 \Rightarrow Y_5, Y_6) - -Here, augmented variables may be automatically added to the model as follows: - -.. math:: - \text{atleast}(3, &Y_1, Y_A, Y_B, Y_6)\\ - &Y_A \Leftrightarrow Y_2 \vee Y_3\\ - &Y_B \Leftrightarrow (Y_4 \Rightarrow Y_5) - -.. code:: - - m.p = LogicalConstraint( - expr=atleast(3, m.Y[1], Or(m.Y[2], m.Y[3]), m.Y[4].implies(m.Y[5]), m.Y[6])) - -Nested CP-style operators -------------------------- - -.. math:: - \text{atleast}(2, Y_1, \text{exactly}(2, Y_2, Y_3, Y_4), Y_5, Y_6) - -Here, we again need to add augmented variables: - -.. math:: - \text{atleast}(2, Y_1, Y_A, Y_5, Y_6)\\ - Y_A \Leftrightarrow \text{exactly}(2, Y_2, Y_3, Y_4) - -However, we also need to further interpret the second statement as a disjunction: - -.. math:: - :nowrap: - - \begin{gather*} - \text{atleast}(2, Y_1, Y_A, Y_5, Y_6)\\ - \left[\begin{gathered}Y_A\\\text{exactly}(2, Y_2, Y_3, Y_4)\end{gathered}\right] - \vee - \left[\begin{gathered}\neg Y_A\\ - \left[\begin{gathered}Y_B\\\text{atleast}(3, Y_2, Y_3, Y_4)\end{gathered}\right] \vee \left[\begin{gathered}Y_C\\\text{atmost}(1, Y_2, Y_3, Y_4)\end{gathered}\right] - \end{gathered}\right] - \end{gather*} - -or equivalently, - -.. math:: - :nowrap: - - \begin{gather*} - \text{atleast}(2, Y_1, Y_A, Y_5, Y_6)\\ - \text{exactly}(1, Y_A, Y_B, Y_C)\\ - \left[\begin{gathered}Y_A\\\text{exactly}(2, Y_2, Y_3, Y_4)\end{gathered}\right] - \vee - \left[\begin{gathered}Y_B\\\text{atleast}(3, Y_2, Y_3, Y_4)\end{gathered}\right] \vee \left[\begin{gathered}Y_C\\\text{atmost}(1, Y_2, Y_3, Y_4)\end{gathered}\right] - \end{gather*} - -.. code:: - - m.p = LogicalConstraint( - expr=atleast(2, m.Y[1], exactly(2, m.Y[2], m.Y[3], m.Y[4]), m.Y[5], m.Y[6])) - -In the ``logical_to_linear`` transformation, we automatically convert these special disjunctions to linear form using a Big M reformulation. - -Additional Examples -=================== - -The following models all work and are equivalent for :math:`\left[x = 0\right] \veebar \left[y = 0\right]`: - -.. doctest:: - - Option 1: Rule-based construction - - >>> from pyomo.environ import * - >>> from pyomo.gdp import * - >>> model = ConcreteModel() - - >>> model.x = Var() - >>> model.y = Var() - - >>> # Two conditions - >>> def _d(disjunct, flag): - ... model = disjunct.model() - ... if flag: - ... # x == 0 - ... disjunct.c = Constraint(expr=model.x == 0) - ... else: - ... # y == 0 - ... disjunct.c = Constraint(expr=model.y == 0) - >>> model.d = Disjunct([0,1], rule=_d) - - >>> # Define the disjunction - >>> def _c(model): - ... return [model.d[0], model.d[1]] - >>> model.c = Disjunction(rule=_c) - - Option 2: Explicit disjuncts - - >>> from pyomo.environ import * - >>> from pyomo.gdp import * - >>> model = ConcreteModel() - - >>> model.x = Var() - >>> model.y = Var() - - >>> model.fix_x = Disjunct() - >>> model.fix_x.c = Constraint(expr=model.x == 0) - - >>> model.fix_y = Disjunct() - >>> model.fix_y.c = Constraint(expr=model.y == 0) - - >>> model.c = Disjunction(expr=[model.fix_x, model.fix_y]) - - Option 3: Implicit disjuncts (disjunction rule returns a list of - expressions or a list of lists of expressions) - - >>> from pyomo.environ import * - >>> from pyomo.gdp import * - >>> model = ConcreteModel() - - >>> model.x = Var() - >>> model.y = Var() - - >>> model.c = Disjunction(expr=[model.x == 0, model.y == 0]) diff --git a/doc/Archive/modeling_extensions/gdp/solving.rst b/doc/Archive/modeling_extensions/gdp/solving.rst deleted file mode 100644 index 9fea90ebf5f..00000000000 --- a/doc/Archive/modeling_extensions/gdp/solving.rst +++ /dev/null @@ -1,201 +0,0 @@ -.. image:: /../logos/gdp/Pyomo-GDP-150.png - :scale: 20% - :class: no-scaled-link - :align: right - -***************************************** -Solving Logic-based Models with Pyomo.GDP -***************************************** - - -Flexible Solution Suite -======================= - -Once a model is formulated as a GDP model, a range of solution -strategies are available to manipulate and solve it. - -The traditional approach is reformulation to a MI(N)LP, but various -other techniques are possible, including direct solution via the -:ref:`GDPopt solver `. Below, we describe some of -these capabilities. - -.. _gdp-reformulations: - -Reformulations -============== - -Logical constraints -------------------- - -.. note:: - - Historically users needed to explicitly convert logical propositions - to algebraic form prior to invoking the GDP MI(N)LP reformulations - or the GDPopt solver. However, this is mathematically incorrect - since the GDP MI(N)LP reformulations themselves convert logical - formulations to algebraic formulations. The current recommended - practice is to pass the entire (mixed logical / algebraic) model to - the MI(N)LP reformulations or GDPopt directly. - -There are several approaches to convert logical constraints into -algebraic form. - -Conjunctive Normal Form -^^^^^^^^^^^^^^^^^^^^^^^ - -The first transformation (`core.logical_to_linear`) leverages the -`sympy` package to generate the conjunctive normal form of the logical -constraints and then adds the equivalent as a list algebraic -constraints. The following transforms logical propositions on the model -to algebraic form: - -.. code:: - - TransformationFactory('core.logical_to_linear').apply_to(model) - -The transformation creates a constraint list with a unique name starting -with ``logic_to_linear``, within which the algebraic equivalents of the -logical constraints are placed. If not already associated with a binary -variable, each ``BooleanVar`` object will receive a generated binary -counterpart. These associated binary variables may be accessed via the -``get_associated_binary()`` method. - -.. code:: - - m.Y[1].get_associated_binary() - -Additional augmented variables and their corresponding constraints may -also be created, as described in :ref:`gdp-advanced-examples`. - -Following solution of the GDP model, values of the Boolean variables may be updated from their algebraic binary counterparts using the ``update_boolean_vars_from_binary()`` function. - -.. autofunction:: pyomo.core.plugins.transform.logical_to_linear.update_boolean_vars_from_binary - -Factorable Programming -^^^^^^^^^^^^^^^^^^^^^^ - -The second transformation (`contrib.logical_to_disjunctive`) leverages -ideas from factorable programming to first generate an equivalent set of -"factored" logical constraints form by traversing each logical -proposition and replacing each logical operator with an additional -Boolean variable and then adding the "simple" logical constraint that -equates the new Boolean variable with the single logical operator. - -The resulting "simple" logical constraints are converted to either MIP -or GDP form: if the constraint contains only Boolean variables, then -then MIP representation is emitted. Logical constraints with mixed -integer-Boolean arguments (e.g., `atmost`, `atleast`, `exactly`, etc.) -are converted to a disjunctive representation. - -As this transformation both avoids the conversion into `sympy` and only -requires a single traversal of each logical constraint, -`contrib.logical_to_disjunctive` is significantly faster than -`core.logical_to_linear` at the cost of a larger model. In practice, -the cost of the larger model is negated by the effectiveness of the MIP -presolve in most solvers. - -Reformulation to MI(N)LP ------------------------- - -To use standard commercial solvers, you must convert the disjunctive -model to a standard MILP/MINLP model. The two classical strategies for -doing so are the (included) Big-M and Hull reformulations. - - -Big-M (BM) Reformulation -^^^^^^^^^^^^^^^^^^^^^^^^ - -The Big-M reformulation\ [#gdp-bm]_ results in a smaller transformed model, avoiding the need to add extra variables; however, it yields a looser continuous relaxation. -By default, the BM transformation will estimate reasonably tight M values for you if variables are bounded. -For nonlinear models where finite expression bounds may be inferred from variable bounds, the BM transformation may also be able to automatically compute M values for you. -For all other models, you will need to provide the M values through a "BigM" Suffix, or through the `bigM` argument to the transformation. -We will raise a ``GDP_Error`` for missing M values. - -To apply the BM reformulation within a python script, use: - -.. code:: - - TransformationFactory('gdp.bigm').apply_to(model) - -From the Pyomo command line, include the ``--transform pyomo.gdp.bigm`` option. - -Multiple Big-M (MBM) Reformulation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -We also implement the multiple-parameter Big-M (MBM) approach described in literature\ [#gdp-mbm]_. -By default, the MBM transformation will solve continuous subproblems in order to calculate M values. -This process can be time-consuming, so the transformation also provides a method to export the M values used as a dictionary and allows for M values to be provided through the `bigM` argument. - -For example, to apply the transformation and store the M values, use: - -.. code:: - - mbigm = TransformationFactory('gdp.mbigm') - mbigm.apply_to(model) - - # These can be stored... - M_values = mbigm.get_all_M_values(model) - # ...so that in future runs, you can write: - mbigm.apply_to(m, bigM=M_values) - -From the Pyomo command line, include the ``--transform pyomo.gdp.mbigm`` option. - -.. warning:: - The Multiple Big-M transformation does not currently support Suffixes and will - ignore "BigM" Suffixes. - -Hull Reformulation (HR) -^^^^^^^^^^^^^^^^^^^^^^^ - -The Hull Reformulation requires a lifting into a higher-dimensional space and consequently introduces disaggregated variables and their corresponding constraints. - -.. note:: - - - All variables that appear in disjuncts need upper and lower bounds. - - - The hull reformulation is an exact reformulation at the solution - points even for nonconvex GDP models, but the resulting MINLP will - also be nonconvex. - -To apply the Hull reformulation within a python script, use: - -.. code:: - - TransformationFactory('gdp.hull').apply_to(model) - -From the Pyomo command line, include the ``--transform pyomo.gdp.hull`` option. - -Hybrid BM/HR Reformulation -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -An experimental (for now) implementation of the cutting plane approach described in literature\ [#gdp-cuttingplanes]_ is provided for linear GDP models. -The transformation augments the BM reformulation by a set of cutting planes generated from the HR model by solving separation problems. -This gives a model that is not as large as the HR, but with a stronger continuous relaxation than the BM. - -This transformation is accessible via: - -.. code:: - - TransformationFactory('gdp.cuttingplane').apply_to(model) - -Direct GDP solvers -================== - -Pyomo includes the contributed GDPopt solver, which can directly solve -GDP models. Its usage is described within the :ref:`contributed -packages documentation `. - -References -========== - -.. [#gdp-pse-paper] Chen, Q., Johnson, E. S., Siirola, J. D., & Grossmann, I. E. (2018). Pyomo.GDP: Disjunctive Models in Python. In M. R. Eden, M. G. Ierapetritou, & G. P. Towler (Eds.), *Proceedings of the 13th International Symposium on Process Systems Engineering* (pp. 889–894). San Diego: Elsevier B.V. https://doi.org/10.1016/B978-0-444-64241-7.50143-9 - -.. [#gdp-main-paper] Chen, Q., Johnson, E. S., Bernal, D. E., Valentin, R., Kale, S., Bates, J., Siirola, J. D. and Grossmann, I. E. (2021). Pyomo.GDP: an ecosystem for logic based modeling and optimization development, *Optimization and Engineering* (pp. 1-36).https://doi.org/10.1007/s11081-021-09601-7 - -.. [#gdp-review-2013] Grossmann, I. E., & Trespalacios, F. (2013). Systematic modeling of discrete-continuous optimization models through generalized disjunctive programming. *AIChE Journal*, 59(9), 3276–3295. https://doi.org/10.1002/aic.14088 - -.. [#gdp-mbm] Trespalacios, F., & Grossmann, I. E. (2015). Improved Big-M reformulation for generalized disjunctive programs. *Computers and Chemical Engineering*, 76, 98–103. https://doi.org/10.1016/j.compchemeng.2015.02.013 - -.. [#gdp-bm] Nemhauser, G. L., & Wolsey, L. A. (1988). *Integer and combinatorial optimization*. New York: Wiley. - -.. [#gdp-cuttingplanes] Sawaya, N. W., & Grossmann, I. E. (2003). A cutting plane method for solving linear generalized disjunctive programming problems. *Computer Aided Chemical Engineering*, 15(C), 1032–1037. https://doi.org/10.1016/S1570-7946(03)80444-3 diff --git a/doc/Archive/modeling_extensions/index.rst b/doc/Archive/modeling_extensions/index.rst deleted file mode 100644 index 3a3370e510a..00000000000 --- a/doc/Archive/modeling_extensions/index.rst +++ /dev/null @@ -1,12 +0,0 @@ -Modeling Extensions -=================== - -.. toctree:: - :maxdepth: 1 - - bilevel.rst - dae.rst - gdp/index.rst - mpec.rst - stochastic_programming.rst - network.rst diff --git a/doc/Archive/modeling_extensions/mpec.rst b/doc/Archive/modeling_extensions/mpec.rst deleted file mode 100644 index b7ba19712ca..00000000000 --- a/doc/Archive/modeling_extensions/mpec.rst +++ /dev/null @@ -1,6 +0,0 @@ -MPEC -==== - -``pyomo.mpec`` supports modeling complementarity conditions and -optimization problems with equilibrium constraints. - diff --git a/doc/Archive/modeling_extensions/network.rst b/doc/Archive/modeling_extensions/network.rst deleted file mode 100644 index 3fce9448997..00000000000 --- a/doc/Archive/modeling_extensions/network.rst +++ /dev/null @@ -1,331 +0,0 @@ -Pyomo Network -============= - -Pyomo Network is a package that allows users to easily represent their model -as a connected network of units. Units are blocks that contain ports, which -contain variables, that are connected to other ports via arcs. The connection -of two ports to each other via an arc typically represents a set of constraints -equating each member of each port to each other, however there exist other -connection rules as well, in addition to support for custom rules. Pyomo -Network also includes a model transformation that will automatically expand -the arcs and generate the appropriate constraints to produce an algebraic -model that a solver can handle. Furthermore, the package also introduces a -generic sequential decomposition tool that can leverage the modeling -components to decompose a model and compute each unit in the model in a -logically ordered sequence. - -Modeling Components -------------------- - -Pyomo Network introduces two new modeling components to Pyomo: - -.. autosummary:: - :nosignatures: - - pyomo.network.Port - pyomo.network.Arc - -Port -**** - -.. autoclass:: pyomo.network.Port - :members: - :exclude-members: construct, display - -.. autoclass:: pyomo.network.port._PortData - :members: - :special-members: __getattr__ - :exclude-members: set_value - -The following code snippet shows examples of declaring and using a -:py:class:`Port ` component on a -concrete Pyomo model: - -.. doctest:: - - >>> from pyomo.environ import * - >>> from pyomo.network import * - >>> m = ConcreteModel() - >>> m.x = Var() - >>> m.y = Var(['a', 'b']) # can be indexed - >>> m.z = Var() - >>> m.e = 5 * m.z # you can add Pyomo expressions too - >>> m.w = Var() - - >>> m.p = Port() - >>> m.p.add(m.x) # implicitly name the port member "x" - >>> m.p.add(m.y, "foo") # name the member "foo" - >>> m.p.add(m.e, rule=Port.Extensive) # specify a rule - >>> m.p.add(m.w, rule=Port.Extensive, write_var_sum=False) # keyword arg - -Arc -*** - -.. autoclass:: pyomo.network.Arc - :members: - :exclude-members: construct - -.. autoclass:: pyomo.network.arc._ArcData - :members: - :special-members: __getattr__ - -The following code snippet shows examples of declaring and using an -:py:class:`Arc ` component on a -concrete Pyomo model: - -.. doctest:: - - >>> from pyomo.environ import * - >>> from pyomo.network import * - >>> m = ConcreteModel() - >>> m.x = Var() - >>> m.y = Var(['a', 'b']) - >>> m.u = Var() - >>> m.v = Var(['a', 'b']) - >>> m.w = Var() - >>> m.z = Var(['a', 'b']) # indexes need to match - - >>> m.p = Port(initialize=[m.x, m.y]) - >>> m.q = Port(initialize={"x": m.u, "y": m.v}) - >>> m.r = Port(initialize={"x": m.w, "y": m.z}) # names need to match - >>> m.a = Arc(source=m.p, destination=m.q) # directed - >>> m.b = Arc(ports=(m.p, m.q)) # undirected - >>> m.c = Arc(ports=(m.p, m.q), directed=True) # directed - >>> m.d = Arc(src=m.p, dest=m.q) # aliases work - >>> m.e = Arc(source=m.r, dest=m.p) # ports can have both in and out - -Arc Expansion Transformation ----------------------------- - -The examples above show how to declare and instantiate a -:py:class:`Port ` and an -:py:class:`Arc `. These two components form the basis of -the higher level representation of a connected network with sets of related -variable quantities. Once a network model has been constructed, Pyomo Network -implements a transformation that will expand all (active) arcs on the model -and automatically generate the appropriate constraints. The constraints -created for each port member will be indexed by the same indexing set as -the port member itself. - -During transformation, a new block is created on the model for each arc -(located on the arc's parent block), which serves to contain all of the -auto generated constraints for that arc. At the end of the -transformation, a reference is created on the arc that points to this -new block, available via the arc property `arc.expanded_block`. - -The constraints produced by this transformation depend on the rule assigned -for each port member and can be different between members on the same port. -For example, you can have two different members on a port where one member's -rule is :py:func:`Port.Equality ` and the other -member's rule is :py:func:`Port.Extensive `. - -:py:func:`Port.Equality ` is the default rule -for port members. This rule simply generates equality constraints on the -expanded block between the source port's member and the destination port's -member. Another implemented expansion method is -:py:func:`Port.Extensive `, which essentially -represents implied splitting and mixing of certain variable quantities. -Users can refer to the documentation of the static method itself for more -details on how this implicit splitting and mixing is implemented. -Additionally, should users desire, the expansion API supports custom rules -that can be implemented to generate whatever is needed for special cases. - -The following code demonstrates how to call the transformation to expand -the arcs on a model: - -.. doctest:: - - >>> from pyomo.environ import * - >>> from pyomo.network import * - >>> m = ConcreteModel() - >>> m.x = Var() - >>> m.y = Var(['a', 'b']) - >>> m.u = Var() - >>> m.v = Var(['a', 'b']) - - >>> m.p = Port(initialize=[m.x, (m.y, Port.Extensive)]) # rules must match - >>> m.q = Port(initialize={"x": m.u, "y": (m.v, Port.Extensive)}) - >>> m.a = Arc(source=m.p, destination=m.q) - - >>> TransformationFactory("network.expand_arcs").apply_to(m) - -Sequential Decomposition ------------------------- - -Pyomo Network implements a generic -:py:class:`SequentialDecomposition ` -tool that can be used to compute each unit in a network model in a logically -ordered sequence. - -The sequential decomposition procedure is commenced via the -:py:func:`run ` method. - -Creating a Graph -**************** - -To begin this procedure, the Pyomo Network model is first utilized to create -a networkx `MultiDiGraph` by adding edges to the graph for every arc on the -model, where the nodes of the graph are the parent blocks of the source and -destination ports. This is done via the -:py:func:`create_graph ` -method, which requires all arcs on the model to be both directed and already -expanded. The `MultiDiGraph` class of networkx supports both direccted edges -as well as having multiple edges between the same two nodes, so users can -feel free to connect as many ports as desired between the same two units. - -Computation Order -***************** - -The order of computation is then determined by treating the resulting graph -as a tree, starting at the roots of the tree, and making sure by the time -each node is reached, all of its predecessors have already been computed. -This is implemented through the :py:func:`calculation_order -` and -:py:func:`tree_order ` -methods. Before this, however, the procedure will first select a set of tear -edges, if necessary, such that every loop in the graph is torn, while -minimizing both the number of times any single loop is torn as well as the -total number of tears. - -Tear Selection -************** - -A set of tear edges can be selected in one of two ways. By default, a Pyomo -MIP model is created and optimized resulting in an optimal set of tear edges. -The implementation of this MIP model is based on a set of binary "torn" -variables for every edge in the graph, and constraints on every loop in the -graph that dictate that there must be at least one tear on the loop. Then -there are two objectives (represented by a doubly weighted objective). The -primary objective is to minimize the number of times any single loop is torn, -and then secondary to that is to minimize the total number of tears. This -process is implemented in the :py:func:`select_tear_mip -` method, which uses -the model returned from the :py:func:`select_tear_mip_model -` method. - -Alternatively, there is the :py:func:`select_tear_heuristic -` method. This -uses a heuristic procedure that walks back and forth on the graph to find -every optimal tear set, and returns each equally optimal tear set it finds. -This method is much slower than the MIP method on larger models, but it -maintains some use in the fact that it returns every possible optimal tear set. - -A custom tear set can be assigned before calling the -:py:func:`run ` method. This is -useful so users can know what their tear set will be and thus what arcs will -require guesses for uninitialized values. See the -:py:func:`set_tear_set ` -method for details. - -Running the Sequential Decomposition Procedure -********************************************** - -After all of this computational order preparation, the sequential -decomposition procedure will then run through the graph in the order it -has determined. Thus, the `function` that was passed to the -:py:func:`run ` method will be -called on every unit in sequence. This function can perform any arbitrary -operations the user desires. The only thing that -:py:class:`SequentialDecomposition ` -expects from the function is that after returning from it, every variable -on every outgoing port of the unit will be specified (i.e. it will have a -set current value). Furthermore, the procedure guarantees to the user that -for every unit, before the function is called, every variable on every -incoming port of the unit will be fixed. - -In between computing each of these units, port member values are passed -across existing arcs involving the unit currently being computed. This means -that after computing a unit, the expanded constraints from each arc coming -out of this unit will be satisfied, and the values on the respective -destination ports will be fixed at these new values. While running the -computational order, values are not passed across tear edges, as tear edges -represent locations in loops to stop computations (during iterations). This -process continues until all units in the network have been computed. This -concludes the "first pass run" of the network. - -Guesses and Fixing Variables -**************************** - -When passing values across arcs while running the computational order, -values at the destinations of each of these arcs will be fixed at the -appropriate values. This is important to the fact that the procedure -guarantees every inlet variable will be fixed before calling the function. -However, since values are not passed across torn arcs, there is a need for -user-supplied guesses for those values. See the :py:func:`set_guesses_for -` method for details -on how to supply these values. - -In addition to passing dictionaries of guesses for certain ports, users can -also assign current values to the variables themselves and the procedure -will pick these up and fix the variables in place. Alternatively, users can -utilize the `default_guess` option to specify a value to use as a default -guess for all free variables if they have no guess or current value. If a -free variable has no guess or current value and there is no default guess -option, then an error will be raised. - -Similarly, if the procedure attempts to pass a value to a destination port -member but that port member is already fixed and its fixed value is different -from what is trying to be passed to it (by a tolerance specified by the -`almost_equal_tol` option), then an error will be raised. Lastly, if there -is more than one free variable in a constraint while trying to pass values -across an arc, an error will be raised asking the user to fix more variables -by the time values are passed across said arc. - -Tear Convergence -**************** - -After completing the first pass run of the network, the sequential -decomposition procedure will proceed to converge all tear edges in the -network (unless the user specifies not to, or if there are no tears). -This process occurs separately for every strongly connected component (SCC) -in the graph, and the SCCs are computed in a logical order such that each -SCC is computed before other SCCs downstream of it (much like -:py:func:`tree_order `). - -There are two implemented methods for converging tear edges: direct -substitution and Wegstein acceleration. Both of these will iteratively run -the computation order until every value in every tear arc has converged to -within the specified tolerance. See the -:py:class:`SequentialDecomposition ` -parameter documentation for details on what can be controlled about this -procedure. - -The following code demonstrates basic usage of the -:py:class:`SequentialDecomposition ` -class: - -.. doctest:: - :skipif: not __import__("pyomo.network").network.decomposition.imports_available - - >>> from pyomo.environ import * - >>> from pyomo.network import * - >>> m = ConcreteModel() - >>> m.unit1 = Block() - >>> m.unit1.x = Var() - >>> m.unit1.y = Var(['a', 'b']) - >>> m.unit2 = Block() - >>> m.unit2.x = Var() - >>> m.unit2.y = Var(['a', 'b']) - >>> m.unit1.port = Port(initialize=[m.unit1.x, (m.unit1.y, Port.Extensive)]) - >>> m.unit2.port = Port(initialize=[m.unit2.x, (m.unit2.y, Port.Extensive)]) - >>> m.a = Arc(source=m.unit1.port, destination=m.unit2.port) - >>> TransformationFactory("network.expand_arcs").apply_to(m) - - >>> m.unit1.x.fix(10) - >>> m.unit1.y['a'].fix(15) - >>> m.unit1.y['b'].fix(20) - - >>> seq = SequentialDecomposition(tol=1.0E-3) # options can go to init - >>> seq.options.select_tear_method = "heuristic" # or set them like so - >>> # seq.set_tear_set([...]) # assign a custom tear set - >>> # seq.set_guesses_for(m.unit.inlet, {...}) # choose guesses - >>> def initialize(b): - ... # b.initialize() - ... pass - ... - >>> seq.run(m, initialize) - -.. autoclass:: pyomo.network.SequentialDecomposition - :members: set_guesses_for, set_tear_set, tear_set_arcs, indexes_to_arcs, - run, create_graph, select_tear_mip, select_tear_mip_model, - select_tear_heuristic, calculation_order, tree_order diff --git a/doc/Archive/modeling_extensions/reduce_points_demo.png b/doc/Archive/modeling_extensions/reduce_points_demo.png deleted file mode 100644 index 00195f26dd7b6af03315c11372b3094a3aebf7ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29803 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 O_)=5R!W1i6KKu_f`Icb- diff --git a/doc/Archive/modeling_extensions/stochastic_programming.rst b/doc/Archive/modeling_extensions/stochastic_programming.rst deleted file mode 100644 index 227a8d9aa8d..00000000000 --- a/doc/Archive/modeling_extensions/stochastic_programming.rst +++ /dev/null @@ -1,17 +0,0 @@ -Stochastic Programming in Pyomo -=============================== - -There are two extensions for modeling and solving Stochastic Programs in -Pyomo. Both are currently distributed as independent Python packages. -PySP was the original extension (and up through Pyomo 5.7.3 was -distributed as part of Pyomo). You can find the documentation here: - - `https://pysp.readthedocs.io `_ - -In 2020, the PySP developers released the mpi-sppy package, which -reimplemented much of the functionality from PySP in a new scalable -framework built on top of MPI and the mpi4py package. Future -development of stochastic programming capabilities is occurring in -mpi-sppy. The documentation is available here: - - `https://mpi-sppy.readthedocs.io `_ diff --git a/doc/Archive/pyomo_modeling_components/Constraints.rst b/doc/Archive/pyomo_modeling_components/Constraints.rst deleted file mode 100644 index 0cc42cb2abe..00000000000 --- a/doc/Archive/pyomo_modeling_components/Constraints.rst +++ /dev/null @@ -1,39 +0,0 @@ -Constraints -=========== - -Most constraints are specified using equality or inequality expressions -that are created using a rule, which is a Python function. For example, -if the variable ``model.x`` has the indexes 'butter' and 'scones', then -this constraint limits the sum over these indexes to be exactly three: - -.. literalinclude:: ../src/scripting/spy4Constraints_Constraint_example.spy - :language: python - -Instead of expressions involving equality (==) or inequalities (`<=` or -`>=`), constraints can also be expressed using a 3-tuple if the form -(lb, expr, ub) where lb and ub can be ``None``, which is interpreted as -lb `<=` expr `<=` ub. Variables can appear only in the middle expr. For -example, the following two constraint declarations have the same -meaning: - -.. literalinclude:: ../src/scripting/spy4Constraints_Inequality_constraints_2expressions.spy - :language: python - -For this simple example, it would also be possible to declare -``model.x`` with a ``bounds`` option to accomplish the same thing. - -Constraints (and objectives) can be indexed by lists or sets. When the -declaration contains lists or sets as arguments, the elements are -iteratively passed to the rule function. If there is more than one, then -the cross product is sent. For example the following constraint could be -interpreted as placing a budget of :math:`i` on the -:math:`i^{\mbox{th}}` item to buy where the cost per item is given by -the parameter ``model.a``: - -.. literalinclude:: ../src/scripting/spy4Constraints_Passing_elements_crossproduct.spy - :language: python - -.. note:: - - Python and Pyomo are case sensitive so ``model.a`` is not the same as - ``model.A``. diff --git a/doc/Archive/pyomo_modeling_components/Expressions.rst b/doc/Archive/pyomo_modeling_components/Expressions.rst deleted file mode 100644 index 16c206e2fe8..00000000000 --- a/doc/Archive/pyomo_modeling_components/Expressions.rst +++ /dev/null @@ -1,218 +0,0 @@ -Expressions -=========== - -In this section, we use the word "expression" in two ways: first in the -general sense of the word and second to describe a class of Pyomo objects -that have the name ``Expression`` as described in the subsection on -expression objects. - -Rules to Generate Expressions ------------------------------ - -Both objectives and constraints make use of rules to generate -expressions. These are Python functions that return the appropriate -expression. These are first-class functions that can access -global data as well as data passed in, including the model object. - -Operations on model elements results in expressions, which seems natural -in expressions like the constraints we have seen so far. It is also -possible to build up expressions. The following example illustrates -this, along with a reference to global Python data in the form of a -Python variable called ``switch``: - -.. literalinclude:: ../src/scripting/spy4Expressions_Buildup_expression_switch.spy - :language: python - -In this example, the constraint that is generated depends on the value -of the Python variable called ``switch``. If the value is 2 or greater, -then the constraint is ``summation(model.c, model.x) - model.d >= 0.5``; -otherwise, the ``model.d`` term is not present. - -.. warning:: - - Because model elements result in expressions, not values, the - following does not work as expected in an abstract model! - - .. literalinclude:: ../src/scripting/spy4Expressions_Abstract_wrong_usage.spy - :language: python - - The trouble is that ``model.d >= 2`` results in an expression, not - its evaluated value. Instead use ``if value(model.d) >= 2`` - -.. note:: - - Pyomo supports non-linear expressions and can call non-linear solvers such as Ipopt. - -.. _piecewise: - -.. _abstract2piece.py: - -Piecewise Linear Expressions ----------------------------- - -Pyomo has facilities to add piecewise constraints of the form y=f(x) for -a variety of forms of the function f. - -The piecewise types other than SOS2, BIGM_SOS1, BIGM_BIN are implement -as described in the paper [Vielma_et_al]_. - -There are two basic forms for the declaration of the constraint: - -.. literalinclude:: ../src/scripting/spy4Expressions_Declare_piecewise_constraints.spy - :language: python - -where ``pwconst`` can be replaced by a name appropriate for the -application. The choice depends on whether the x and y variables are -indexed. If so, they must have the same index sets and these sets are -give as the first arguments. - -Keywords: -********* - -* **pw_pts={ },[ ],( )** - - A dictionary of lists (where keys are the index set) or a single list - (for the non-indexed case or when an identical set of breakpoints is - used across all indices) defining the set of domain breakpoints for - the piecewise linear function. - - .. note:: - - pw_pts is always required. These give the breakpoints for the - piecewise function and are expected to fully span the bounds for - the independent variable(s). - -* **pw_repn=

h#VUT{g!CQ~)Lx4gcLE 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. - -Credit for prototyping and development can be found in the ``GDPopt`` class documentation, below. - -.. _Turkay & Grossmann, 1996: https://dx.doi.org/10.1016/0098-1354(95)00219-7 -.. _Lee & Grossmann, 2001: https://doi.org/10.1016/S0098-1354(01)00732-3 -.. _Lee & Grossmann, 2000: https://doi.org/10.1016/S0098-1354(00)00581-0 -.. _Chen et al., 2018: https://doi.org/10.1016/B978-0-444-64241-7.50143-9 - -GDPopt can be used to solve a Pyomo.GDP concrete model in two ways. -The simplest is to instantiate the generic GDPopt solver and specify the desired algorithm as an argument to the ``solve`` method: - -.. code:: - - >>> SolverFactory('gdpopt').solve(model, algorithm='LOA') - -The alternative is to instantiate an algorithm-specific GDPopt solver: - -.. code:: - - >>> SolverFactory('gdpopt.loa').solve(model) - -In the above examples, GDPopt uses the GDPopt-LOA algorithm. -Other algorithms may be used by specifying them in the ``algorithm`` argument when using the generic solver or by instantiating the algorithm-specific GDPopt solvers. All GDPopt options are listed below. - -.. note:: - - The generic GDPopt solver allows minimal configuration outside of the arguments to the ``solve`` method. To avoid repeatedly specifying the same configuration options to the ``solve`` method, use the algorithm-specific solvers. - -Logic-based Outer Approximation (LOA) -------------------------------------- - -`Chen et al., 2018`_ contains the following flowchart, taken from the preprint version: - -.. image:: gdpopt_flowchart.png - :scale: 70% - -An example that includes the modeling approach may be found below. - -.. doctest:: - :skipif: not glpk_available - - Required imports - >>> from pyomo.environ import * - >>> from pyomo.gdp import * - - Create a simple model - >>> model = ConcreteModel(name='LOA example') - - >>> model.x = Var(bounds=(-1.2, 2)) - >>> model.y = Var(bounds=(-10,10)) - >>> model.c = Constraint(expr= model.x + model.y == 1) - - >>> model.fix_x = Disjunct() - >>> model.fix_x.c = Constraint(expr=model.x == 0) - - >>> model.fix_y = Disjunct() - >>> model.fix_y.c = Constraint(expr=model.y == 0) - - >>> model.d = Disjunction(expr=[model.fix_x, model.fix_y]) - >>> model.objective = Objective(expr=model.x + 0.1*model.y, sense=minimize) - - Solve the model using GDPopt - >>> results = SolverFactory('gdpopt.loa').solve( - ... model, mip_solver='glpk') # doctest: +IGNORE_RESULT - - Display the final solution - >>> model.display() - Model LOA example - - Variables: - x : Size=1, Index=None - Key : Lower : Value : Upper : Fixed : Stale : Domain - None : -1.2 : 0 : 2 : False : False : Reals - y : Size=1, Index=None - Key : Lower : Value : Upper : Fixed : Stale : Domain - None : -10 : 1 : 10 : False : False : Reals - - Objectives: - objective : Size=1, Index=None, Active=True - Key : Active : Value - None : True : 0.1 - - Constraints: - c : Size=1 - Key : Lower : Body : Upper - None : 1.0 : 1 : 1.0 - -.. note:: - - When troubleshooting, it can often be helpful to turn on verbose - output using the ``tee`` flag. - -.. code:: - - >>> SolverFactory('gdpopt.loa').solve(model, tee=True) - -Global Logic-based Outer Approximation (GLOA) ---------------------------------------------- - -The same algorithm can be used to solve GDPs involving nonconvex nonlinear constraints by solving the subproblems globally: - -.. code:: - - >>> SolverFactory('gdpopt.gloa').solve(model) - -.. warning:: - - The ``nlp_solver`` option must be set to a global solver for the solution returned by GDPopt to also be globally optimal. - -Relaxation with Integer Cuts (RIC) ----------------------------------- - -Instead of outer approximation, GDPs can be solved using the same MILP relaxation as in the previous two algorithms, but instead of using the subproblems to generate outer-approximation cuts, the algorithm adds only no-good cuts for every discrete solution encountered: - -.. code:: - - >>> SolverFactory('gdpopt.ric').solve(model) - -Again, this is a global algorithm if the subproblems are solved globally, and is not otherwise. - -.. note:: - - The RIC algorithm will not necessarily enumerate all discrete solutions as it is possible for the bounds to converge first. However, full enumeration is not uncommon. - -Logic-based Branch-and-Bound (LBB) ----------------------------------- - -The GDPopt-LBB solver branches through relaxed subproblems with inactive disjunctions. -It explores the possibilities based on best lower bound, -eventually activating all disjunctions and presenting the globally optimal solution. - -To use the GDPopt-LBB solver, define your Pyomo GDP model as usual: - -.. doctest:: - :skipif: not baron_available - - Required imports - >>> from pyomo.environ import * - >>> from pyomo.gdp import Disjunct, Disjunction - - Create a simple model - >>> m = ConcreteModel() - >>> m.x1 = Var(bounds = (0,8)) - >>> m.x2 = Var(bounds = (0,8)) - >>> m.obj = Objective(expr=m.x1 + m.x2, sense=minimize) - >>> m.y1 = Disjunct() - >>> m.y2 = Disjunct() - >>> m.y1.c1 = Constraint(expr=m.x1 >= 2) - >>> m.y1.c2 = Constraint(expr=m.x2 >= 2) - >>> m.y2.c1 = Constraint(expr=m.x1 >= 3) - >>> m.y2.c2 = Constraint(expr=m.x2 >= 3) - >>> m.djn = Disjunction(expr=[m.y1, m.y2]) - - Invoke the GDPopt-LBB solver - - >>> results = SolverFactory('gdpopt.lbb').solve(m) - WARNING: 09/06/22: The GDPopt LBB algorithm currently has known issues. Please - use the results with caution and report any bugs! - - >>> print(results) # doctest: +SKIP - >>> print(results.solver.status) - ok - >>> print(results.solver.termination_condition) - optimal - - >>> print([value(m.y1.indicator_var), value(m.y2.indicator_var)]) - [True, False] - -GDPopt implementation and optional arguments --------------------------------------------- - -.. warning:: - - GDPopt optional arguments should be considered beta code and are - subject to change. - -.. autoclass:: pyomo.contrib.gdpopt.GDPopt.GDPoptSolver - :members: - -.. autoclass:: pyomo.contrib.gdpopt.loa.GDP_LOA_Solver - :members: - -.. autoclass:: pyomo.contrib.gdpopt.gloa.GDP_GLOA_Solver - :members: - -.. autoclass:: pyomo.contrib.gdpopt.ric.GDP_RIC_Solver - :members: - -.. autoclass:: pyomo.contrib.gdpopt.branch_and_bound.GDP_LBB_Solver - :members: diff --git a/doc/Archive/contributed_packages/gdpopt_flowchart.png b/doc/Archive/contributed_packages/gdpopt_flowchart.png deleted file mode 100644 index 5bd52426dd62e4d9f6de567b0eab71ebcdfb7f14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71538 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 diff --git a/doc/Archive/contributed_packages/iis.rst b/doc/Archive/contributed_packages/iis.rst deleted file mode 100644 index fa97c2f8c61..00000000000 --- a/doc/Archive/contributed_packages/iis.rst +++ /dev/null @@ -1,135 +0,0 @@ -Infeasibility Diagnostics -!!!!!!!!!!!!!!!!!!!!!!!!! - -There are two closely related tools for infeasibility diagnosis: - - - :ref:`Infeasible Irreducible System (IIS) Tool` - - :ref:`Minimal Intractable System finder (MIS) Tool` - -The first simply provides a conduit for solvers that compute an -infeasible irreducible system (e.g., Cplex, Gurobi, or Xpress). The -second provides similar functionality, but uses the ``mis`` package -contributed to Pyomo. - - -Infeasible Irreducible System (IIS) Tool -======================================== - -.. automodule:: pyomo.contrib.iis.iis - -.. autofunction:: pyomo.contrib.iis.write_iis - -Minimal Intractable System finder (MIS) Tool -============================================ - -The file ``mis.py`` finds sets of actions that each, independently, -would result in feasibility. The zero-tolerance is whatever the -solver uses, so users may want to post-process output if it is going -to be used for analysis. It also computes a minimal intractable system -(which is not guaranteed to be unique). It was written by Ben Knueven -as part of the watertap project (https://github.com/watertap-org/watertap) -and is therefore governed by a license shown -at the top of ``mis.py``. - -The algorithms come from John Chinneck's slides, see: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf - -Solver ------- - -At the time of this writing, you need to use IPopt even for LPs. - -Quick Start ------------ - -The file ``trivial_mis.py`` is a tiny example listed at the bottom of -this help file, which references a Pyomo model with the Python variable -`m` and has these lines: - -.. code-block:: python - - from pyomo.contrib.mis import compute_infeasibility_explanation - ipopt = pyo.SolverFactory("ipopt") - compute_infeasibility_explanation(m, solver=ipopt) - -.. Note:: - This is done instead of solving the problem. - -.. Note:: - IDAES users can pass ``get_solver()`` imported from ``ideas.core.solvers`` - as the solver. - -Interpreting the Output ------------------------ - -Assuming the dependencies are installed, running ``trivial_mis.py`` -(shown below) will -produce a lot of warnings from IPopt and then meaningful output (using a logger). - -Repair Options -^^^^^^^^^^^^^^ - -This output for the trivial example shows three independent ways that the model could be rendered feasible: - - -.. code-block:: text - - Model Trivial Quad may be infeasible. A feasible solution was found with only the following variable bounds relaxed: - ub of var x[1] by 4.464126126706818e-05 - lb of var x[2] by 0.9999553410114216 - Another feasible solution was found with only the following variable bounds relaxed: - lb of var x[1] by 0.7071067726864677 - ub of var x[2] by 0.41421355687130673 - ub of var y by 0.7071067651855212 - Another feasible solution was found with only the following inequality constraints, equality constraints, and/or variable bounds relaxed: - constraint: c by 0.9999999861866736 - - -Minimal Intractable System (MIS) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This output shows a minimal intractable system: - - -.. code-block:: text - - Computed Minimal Intractable System (MIS)! - Constraints / bounds in MIS: - lb of var x[2] - lb of var x[1] - constraint: c - -Constraints / bounds in guards for stability -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This part of the report is for nonlinear programs (NLPs). - -When we’re trying to reduce the constraint set, for an NLP there may be constraints that when missing cause the solver -to fail in some catastrophic fashion. In this implementation this is interpreted as failing to get a `results` -object back from the call to `solve`. In these cases we keep the constraint in the problem but it’s in the -set of “guard” constraints – we can’t really be sure they’re a source of infeasibility or not, -just that “bad things” happen when they’re not included. - -Perhaps ideally we would put a constraint in the “guard” set if IPopt failed to converge, and only put it in the -MIS if IPopt converged to a point of local infeasibility. However, right now the code generally makes the -assumption that if IPopt fails to converge the subproblem is infeasible, though obviously that is far from the truth. -Hence for difficult NLPs even the “Phase 1” may “fail” – in that when finished the subproblem containing just the -constraints in the elastic filter may be feasible -- because IPopt failed to converge and we assumed that meant the -subproblem was not feasible. - -Dealing with NLPs is far from clean, but that doesn’t mean the tool can’t return useful results even when its assumptions are not satisfied. - -trivial_mis.py --------------- - -.. code-block:: python - - import pyomo.environ as pyo - m = pyo.ConcreteModel("Trivial Quad") - m.x = pyo.Var([1,2], bounds=(0,1)) - m.y = pyo.Var(bounds=(0, 1)) - m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) - m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) - - from pyomo.contrib.mis import compute_infeasibility_explanation - ipopt = pyo.SolverFactory("ipopt") - compute_infeasibility_explanation(m, solver=ipopt) diff --git a/doc/Archive/contributed_packages/incidence/api.rst b/doc/Archive/contributed_packages/incidence/api.rst deleted file mode 100644 index 38bf0be125b..00000000000 --- a/doc/Archive/contributed_packages/incidence/api.rst +++ /dev/null @@ -1,14 +0,0 @@ -.. _incidence_api: - -API Reference -============= - -.. toctree:: - incidence.rst - config.rst - interface.rst - matching.rst - connected.rst - triangularize.rst - dulmage_mendelsohn.rst - scc_solver.rst diff --git a/doc/Archive/contributed_packages/incidence/config.rst b/doc/Archive/contributed_packages/incidence/config.rst deleted file mode 100644 index 06e4f5c5626..00000000000 --- a/doc/Archive/contributed_packages/incidence/config.rst +++ /dev/null @@ -1,5 +0,0 @@ -Incidence Options -================= - -.. automodule:: pyomo.contrib.incidence_analysis.config - :members: diff --git a/doc/Archive/contributed_packages/incidence/connected.rst b/doc/Archive/contributed_packages/incidence/connected.rst deleted file mode 100644 index 4cf60f62eba..00000000000 --- a/doc/Archive/contributed_packages/incidence/connected.rst +++ /dev/null @@ -1,5 +0,0 @@ -Weakly Connected Components -=========================== - -.. automodule:: pyomo.contrib.incidence_analysis.connected - :members: diff --git a/doc/Archive/contributed_packages/incidence/dulmage_mendelsohn.rst b/doc/Archive/contributed_packages/incidence/dulmage_mendelsohn.rst deleted file mode 100644 index 6fe2bd59324..00000000000 --- a/doc/Archive/contributed_packages/incidence/dulmage_mendelsohn.rst +++ /dev/null @@ -1,5 +0,0 @@ -Dulmage-Mendelsohn Partition -============================ - -.. automodule:: pyomo.contrib.incidence_analysis.dulmage_mendelsohn - :members: diff --git a/doc/Archive/contributed_packages/incidence/incidence.rst b/doc/Archive/contributed_packages/incidence/incidence.rst deleted file mode 100644 index ebf481c00a7..00000000000 --- a/doc/Archive/contributed_packages/incidence/incidence.rst +++ /dev/null @@ -1,5 +0,0 @@ -Incident Variables -================== - -.. automodule:: pyomo.contrib.incidence_analysis.incidence - :members: diff --git a/doc/Archive/contributed_packages/incidence/index.rst b/doc/Archive/contributed_packages/incidence/index.rst deleted file mode 100644 index ab0e07f6abc..00000000000 --- a/doc/Archive/contributed_packages/incidence/index.rst +++ /dev/null @@ -1,19 +0,0 @@ -Incidence Analysis -================== - -Tools for constructing and analyzing the incidence graph of variables -and constraints. - -This documentation contains the following resources: - -.. toctree:: - :maxdepth: 1 - - overview.rst - tutorial.rst - api.rst - -If you are wondering what Incidence Analysis is and would like to learn more, -please see :ref:`incidence_overview`. If you already know what -Incidence Analysis is and are here for reference, see :ref:`incidence_tutorial` -or :ref:`incidence_api` as needed. diff --git a/doc/Archive/contributed_packages/incidence/interface.rst b/doc/Archive/contributed_packages/incidence/interface.rst deleted file mode 100644 index 29c92d8193c..00000000000 --- a/doc/Archive/contributed_packages/incidence/interface.rst +++ /dev/null @@ -1,5 +0,0 @@ -Pyomo Interfaces -================ - -.. automodule:: pyomo.contrib.incidence_analysis.interface - :members: diff --git a/doc/Archive/contributed_packages/incidence/matching.rst b/doc/Archive/contributed_packages/incidence/matching.rst deleted file mode 100644 index 1941c7116cd..00000000000 --- a/doc/Archive/contributed_packages/incidence/matching.rst +++ /dev/null @@ -1,5 +0,0 @@ -Maximum Matching -================ - -.. automodule:: pyomo.contrib.incidence_analysis.matching - :members: diff --git a/doc/Archive/contributed_packages/incidence/overview.rst b/doc/Archive/contributed_packages/incidence/overview.rst deleted file mode 100644 index 3a49ee2d258..00000000000 --- a/doc/Archive/contributed_packages/incidence/overview.rst +++ /dev/null @@ -1,50 +0,0 @@ -.. _incidence_overview: - -Overview -======== - -What is Incidence Analysis? ---------------------------- - -A Pyomo extension for constructing the bipartite incidence graph of variables -and constraints, and an interface to useful algorithms for analyzing or -decomposing this graph. - -Why is Incidence Analysis useful? ---------------------------------- - -It can identify the source of certain types of singularities in a system of -variables and constraints. These singularities often violate assumptions made -while modeling a physical system or assumptions required for an optimization -solver to guarantee convergence. In particular, interior point methods used for -nonlinear local optimization require the Jacobian of equality constraints (and -active inequalities) to be full row rank, and this package implements the -Dulmage-Mendelsohn partition, which can be used to determine if this Jacobian -is structurally rank-deficient. - -Who develops and maintains Incidence Analysis? ----------------------------------------------- - -This extension was developed by Robert Parker while a PhD student in -Professor Biegler's lab at Carnegie Mellon University, with guidance -from Bethany Nicholson and John Siirola at Sandia. - -How can I cite Incidence Analysis? ----------------------------------- - -If you use Incidence Analysis in your research, we would appreciate you citing -the following paper: - -.. code-block:: bibtex - - @article{parker2023dulmage, - title = {Applications of the {Dulmage-Mendelsohn} decomposition for debugging nonlinear optimization problems}, - journal = {Computers \& Chemical Engineering}, - volume = {178}, - pages = {108383}, - year = {2023}, - issn = {0098-1354}, - doi = {https://doi.org/10.1016/j.compchemeng.2023.108383}, - url = {https://www.sciencedirect.com/science/article/pii/S0098135423002533}, - author = {Robert B. Parker and Bethany L. Nicholson and John D. Siirola and Lorenz T. Biegler}, - } diff --git a/doc/Archive/contributed_packages/incidence/scc_solver.rst b/doc/Archive/contributed_packages/incidence/scc_solver.rst deleted file mode 100644 index 35f494af1a1..00000000000 --- a/doc/Archive/contributed_packages/incidence/scc_solver.rst +++ /dev/null @@ -1,5 +0,0 @@ -Block Triangular Decomposition Solver -===================================== - -.. automodule:: pyomo.contrib.incidence_analysis.scc_solver - :members: diff --git a/doc/Archive/contributed_packages/incidence/triangularize.rst b/doc/Archive/contributed_packages/incidence/triangularize.rst deleted file mode 100644 index a051086a859..00000000000 --- a/doc/Archive/contributed_packages/incidence/triangularize.rst +++ /dev/null @@ -1,5 +0,0 @@ -Block Triangularization -======================= - -.. automodule:: pyomo.contrib.incidence_analysis.triangularize - :members: diff --git a/doc/Archive/contributed_packages/incidence/tutorial.bt.rst b/doc/Archive/contributed_packages/incidence/tutorial.bt.rst deleted file mode 100644 index 6710c0dbb50..00000000000 --- a/doc/Archive/contributed_packages/incidence/tutorial.bt.rst +++ /dev/null @@ -1,107 +0,0 @@ -Debugging a numeric singularity using block triangularization -============================================================= - -We start with some imports. To debug a *numeric* singularity, we will need -``PyomoNLP`` from :ref:`pynumero` to get the constraint Jacobian, -and will need NumPy to compute condition numbers. - -.. doctest:: - :skipif: not scipy_available or not asl_available or not networkx_available - - >>> import pyomo.environ as pyo - >>> from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP - >>> from pyomo.contrib.incidence_analysis import IncidenceGraphInterface - >>> import numpy as np - -We now build the model we would like to debug. Compared to the model in -:ref:`incidence_tutorial_dm`, we have converted the sum equation to use a sum -over component flow rates rather than a sum over mass fractions. - -.. doctest:: - :skipif: not scipy_available or not asl_available or not networkx_available - - >>> m = pyo.ConcreteModel() - >>> m.components = pyo.Set(initialize=[1, 2, 3]) - >>> m.x = pyo.Var(m.components, initialize=1.0/3.0) - >>> m.flow_comp = pyo.Var(m.components, initialize=10.0) - >>> m.flow = pyo.Var(initialize=30.0) - >>> m.density = pyo.Var(initialize=1.0) - >>> # This equation is new! - >>> m.sum_flow_eqn = pyo.Constraint( - ... expr=sum(m.flow_comp[j] for j in m.components) == m.flow - ... ) - >>> m.holdup_eqn = pyo.Constraint(m.components, expr={ - ... j: m.x[j]*m.density - 1 == 0 for j in m.components - ... }) - >>> m.density_eqn = pyo.Constraint( - ... expr=1/m.density - sum(1/m.x[j] for j in m.components) == 0 - ... ) - >>> m.flow_eqn = pyo.Constraint(m.components, expr={ - ... j: m.x[j]*m.flow - m.flow_comp[j] == 0 for j in m.components - ... }) - -We now construct the incidence graph and check unmatched variables and -constraints to validate structural nonsingularity. - -.. doctest:: - :skipif: not scipy_available or not asl_available or not networkx_available - - >>> igraph = IncidenceGraphInterface(m, include_inequality=False) - >>> var_dmp, con_dmp = igraph.dulmage_mendelsohn() - >>> print(len(var_dmp.unmatched)) - 0 - >>> print(len(con_dmp.unmatched)) - 0 - -Our system is structurally nonsingular. Now we check whether we are numerically -nonsingular (well-conditioned) by checking the condition number. -Admittedly, deciding if a matrix is "singular" by looking at its condition -number is somewhat of an art. We might define "numerically singular" as having a -condition number greater than the inverse of machine precision (approximately -``1e16``), but poorly conditioned matrices can cause problems even if they don't -meet this definition. Here we use ``1e10`` as a somewhat arbitrary condition -number threshold to indicate a problem in our system. - -.. doctest:: - :skipif: not scipy_available or not asl_available or not networkx_available - - >>> # PyomoNLP requires exactly one objective function - >>> m._obj = pyo.Objective(expr=0.0) - >>> nlp = PyomoNLP(m) - >>> cond_threshold = 1e10 - >>> cond = np.linalg.cond(nlp.evaluate_jacobian_eq().toarray()) - >>> print(cond > cond_threshold) - True - -The system is poorly conditioned. Now we can check diagonal blocks of a block -triangularization to determine which blocks are causing the poor conditioning. - -.. code-block:: python - - >>> var_blocks, con_blocks = igraph.block_triangularize() - >>> for i, (vblock, cblock) in enumerate(zip(var_blocks, con_blocks)): - ... submatrix = nlp.extract_submatrix_jacobian(vblock, cblock) - ... cond = np.linalg.cond(submatrix.toarray()) - ... print(f"block {i}: {cond}") - ... if cond > cond_threshold: - ... for var in vblock: - ... print(f" {var.name}") - ... for con in cblock: - ... print(f" {con.name}") - block 0: 24.492504515710433 - block 1: 1.2480741394486336e+17 - flow - flow_comp[1] - flow_comp[2] - flow_comp[3] - sum_flow_eqn - flow_eqn[1] - flow_eqn[2] - flow_eqn[3] - -We see that the second block is causing the singularity, and that this block -contains the sum equation that we modified for this example. This suggests that -converting this equation to sum over flow rates rather than mass fractions just -converted a structural singularity to a numeric singularity, and didn't really -solve our problem. To see a fix that *does* resolve the singularity, see -:ref:`incidence_tutorial_dm`. diff --git a/doc/Archive/contributed_packages/incidence/tutorial.btsolve.rst b/doc/Archive/contributed_packages/incidence/tutorial.btsolve.rst deleted file mode 100644 index 1ff0b6c5afe..00000000000 --- a/doc/Archive/contributed_packages/incidence/tutorial.btsolve.rst +++ /dev/null @@ -1,72 +0,0 @@ -Solving a square system with a block triangular decomposition -============================================================= - -We start with imports. The key function from Incidence Analysis we will use is -``solve_strongly_connected_components``. - -.. doctest:: - :skipif: not networkx_available or not scipy_available or not asl_available - - >>> import pyomo.environ as pyo - >>> from pyomo.contrib.incidence_analysis import ( - ... solve_strongly_connected_components - ... ) - -Now we construct the model we would like to solve. This is a model with the -same structure as the "fixed model" in :ref:`incidence_tutorial_dm`. - -.. doctest:: - :skipif: not networkx_available or not scipy_available or not asl_available - - >>> m = pyo.ConcreteModel() - >>> m.components = pyo.Set(initialize=[1, 2, 3]) - >>> m.x = pyo.Var(m.components, initialize=1.0/3.0) - >>> m.flow_comp = pyo.Var(m.components, initialize=10.0) - >>> m.flow = pyo.Var(initialize=30.0) - >>> m.dens_bulk = pyo.Var(initialize=1.0) - >>> m.dens_skel = pyo.Var(initialize=1.0) - >>> m.porosity = pyo.Var(initialize=0.25) - >>> m.velocity = pyo.Param(initialize=1.0) - >>> m.holdup = pyo.Param( - ... m.components, initialize={j: 1.0+j/10.0 for j in m.components} - ... ) - >>> m.sum_eqn = pyo.Constraint( - ... expr=sum(m.x[j] for j in m.components) - 1 == 0 - ... ) - >>> m.holdup_eqn = pyo.Constraint(m.components, expr={ - ... j: m.x[j]*m.dens_bulk - m.holdup[j] == 0 for j in m.components - ... }) - >>> m.dens_skel_eqn = pyo.Constraint( - ... expr=1/m.dens_skel - sum(1e-3/m.x[j] for j in m.components) == 0 - ... ) - >>> m.dens_bulk_eqn = pyo.Constraint( - ... expr=m.dens_bulk == (1 - m.porosity)*m.dens_skel - ... ) - >>> m.flow_eqn = pyo.Constraint(m.components, expr={ - ... j: m.x[j]*m.flow - m.flow_comp[j] == 0 for j in m.components - ... }) - >>> m.flow_dens_eqn = pyo.Constraint( - ... expr=m.flow == m.velocity*m.dens_bulk - ... ) - -Solving via a block triangular decomposition is useful in cases where the full -model does not converge when considered simultaneously by a Newton solver. -In this case, we specify a solver to use for the diagonal blocks and call -``solve_strongly_connected_components``. - -.. doctest:: - :skipif: not networkx_available or not scipy_available or not asl_available - - >>> # Suppose a solve like this does not converge - >>> # pyo.SolverFactory("scipy.fsolve").solve(m) - - >>> # We solve via block-triangular decomposition - >>> solver = pyo.SolverFactory("scipy.fsolve") - >>> res_list = solve_strongly_connected_components(m, solver=solver) - -We can now display the variable values at the solution: - -.. code-block:: python - - for var in m.component_objects(pyo.Var): - var.pprint() diff --git a/doc/Archive/contributed_packages/incidence/tutorial.dm.rst b/doc/Archive/contributed_packages/incidence/tutorial.dm.rst deleted file mode 100644 index c14861e9fc8..00000000000 --- a/doc/Archive/contributed_packages/incidence/tutorial.dm.rst +++ /dev/null @@ -1,191 +0,0 @@ -.. _incidence_tutorial_dm: - -Debugging a structural singularity with the Dulmage-Mendelsohn partition -======================================================================== - -We start with some imports and by creating a Pyomo model we would like -to debug. Usually the model is much larger and more complicated than this. -This particular system appeared when debugging a dynamic 1-D partial -differential-algebraic equation (PDAE) model representing a chemical looping -combustion reactor. - -.. doctest:: - :skipif: not scipy_available or not networkx_available or not asl_available - - >>> import pyomo.environ as pyo - >>> from pyomo.contrib.incidence_analysis import IncidenceGraphInterface - - >>> m = pyo.ConcreteModel() - >>> m.components = pyo.Set(initialize=[1, 2, 3]) - >>> m.x = pyo.Var(m.components, initialize=1.0/3.0) - >>> m.flow_comp = pyo.Var(m.components, initialize=10.0) - >>> m.flow = pyo.Var(initialize=30.0) - >>> m.density = pyo.Var(initialize=1.0) - >>> m.sum_eqn = pyo.Constraint( - ... expr=sum(m.x[j] for j in m.components) - 1 == 0 - ... ) - >>> m.holdup_eqn = pyo.Constraint(m.components, expr={ - ... j: m.x[j]*m.density - 1 == 0 for j in m.components - ... }) - >>> m.density_eqn = pyo.Constraint( - ... expr=1/m.density - sum(1/m.x[j] for j in m.components) == 0 - ... ) - >>> m.flow_eqn = pyo.Constraint(m.components, expr={ - ... j: m.x[j]*m.flow - m.flow_comp[j] == 0 for j in m.components - ... }) - -To check this model for structural singularity, we apply the Dulmage-Mendelsohn -partition. ``var_dm_partition`` and ``con_dm_partition`` are named tuples -with fields for each of the four subsets defined by the partition: -``unmatched``, ``overconstrained``, ``square``, and ``underconstrained``. - -.. doctest:: - :skipif: not scipy_available or not networkx_available or not asl_available - - >>> igraph = IncidenceGraphInterface(m) - >>> # Make sure we have a square system - >>> print(len(igraph.variables)) - 8 - >>> print(len(igraph.constraints)) - 8 - >>> var_dm_partition, con_dm_partition = igraph.dulmage_mendelsohn() - -If any variables or constraints are unmatched, the (Jacobian of the) model -is structurally singular. - -.. code-block:: python - - >>> # Note that the unmatched variables/constraints are not mathematically - >>> # unique and could change with implementation! - >>> for var in var_dm_partition.unmatched: - ... print(var.name) - flow_comp[1] - >>> for con in con_dm_partition.unmatched: - ... print(con.name) - density_eqn - -This model has one unmatched constraint and one unmatched variable, so it is -structurally singular. However, the unmatched variable and constraint are not -unique. For example, ``flow_comp[2]`` could have been unmatched instead of -``flow_comp[1]``. The exact variables and constraints that are unmatched depends -on both the order in which variables are identified in Pyomo expressions and -the implementation of the matching algorithm. For a given implementation, -however, these variables and constraints should be deterministic. - -Unique subsets of variables and constraints that are useful when debugging a -structural singularity are the underconstrained and overconstrained subsystems. -The variables in the underconstrained subsystem are contained in the -``unmatched`` and ``underconstrained`` fields of the ``var_dm_partition`` named tuple, -while the constraints are contained in the ``underconstrained`` field of the -``con_dm_partition`` named tuple. -The variables in the overconstrained subsystem are contained in the -``overconstrained`` field of the ``var_dm_partition`` named tuple, while the constraints -are contained in the ``overconstrained`` and ``unmatched`` fields of the -``con_dm_partition`` named tuple. - -We now construct the underconstrained and overconstrained subsystems: - -.. doctest:: - :skipif: not scipy_available or not networkx_available or not asl_available - - >>> uc_var = var_dm_partition.unmatched + var_dm_partition.underconstrained - >>> uc_con = con_dm_partition.underconstrained - >>> oc_var = var_dm_partition.overconstrained - >>> oc_con = con_dm_partition.overconstrained + con_dm_partition.unmatched - -And display the variables and constraints contained in each: - -.. code-block:: python - - >>> # Note that while these variables/constraints are uniquely determined, - >>> # their order is not! - - >>> # Overconstrained subsystem - >>> for var in oc_var: - >>> print(var.name) - x[1] - density - x[2] - x[3] - >>> for con in oc_con: - >>> print(con.name) - sum_eqn - holdup_eqn[1] - holdup_eqn[2] - holdup_eqn[3] - density_eqn - - >>> # Underconstrained subsystem - >>> for var in uc_var: - >>> print(var.name) - flow_comp[1] - flow - flow_comp[2] - flow_comp[3] - >>> for con in uc_con: - >>> print(con.name) - flow_eqn[1] - flow_eqn[2] - flow_eqn[3] - -At this point we must use our intuition about the system being modeled to -identify "what is causing" the singularity. Looking at the under and over- -constrained systems, it appears that we are missing an equation to calculate -``flow``, the total flow rate, and that ``density`` is over-specified as it -is computed by both the bulk density equation and one of the component density -equations. - -With this knowledge, we can eventually figure out (a) that we need an equation -to calculate ``flow`` from density and (b) that our "bulk density equation" -is actually a *skeletal* density equation. Admittedly, this is difficult to -figure out without the full context behind this particular system. - -The following code constructs a new version of the model and verifies that it -is structurally nonsingular: - -.. doctest:: - :skipif: not scipy_available or not networkx_available or not asl_available - - >>> import pyomo.environ as pyo - >>> from pyomo.contrib.incidence_analysis import IncidenceGraphInterface - - >>> m = pyo.ConcreteModel() - >>> m.components = pyo.Set(initialize=[1, 2, 3]) - >>> m.x = pyo.Var(m.components, initialize=1.0/3.0) - >>> m.flow_comp = pyo.Var(m.components, initialize=10.0) - >>> m.flow = pyo.Var(initialize=30.0) - >>> m.dens_bulk = pyo.Var(initialize=1.0) - >>> m.dens_skel = pyo.Var(initialize=1.0) - >>> m.porosity = pyo.Var(initialize=0.25) - >>> m.velocity = pyo.Param(initialize=1.0) - >>> m.sum_eqn = pyo.Constraint( - ... expr=sum(m.x[j] for j in m.components) - 1 == 0 - ... ) - >>> m.holdup_eqn = pyo.Constraint(m.components, expr={ - ... j: m.x[j]*m.dens_bulk - 1 == 0 for j in m.components - ... }) - >>> m.dens_skel_eqn = pyo.Constraint( - ... expr=1/m.dens_skel - sum(1/m.x[j] for j in m.components) == 0 - ... ) - >>> m.dens_bulk_eqn = pyo.Constraint( - ... expr=m.dens_bulk == (1 - m.porosity)*m.dens_skel - ... ) - >>> m.flow_eqn = pyo.Constraint(m.components, expr={ - ... j: m.x[j]*m.flow - m.flow_comp[j] == 0 for j in m.components - ... }) - >>> m.flow_dens_eqn = pyo.Constraint( - ... expr=m.flow == m.velocity*m.dens_bulk - ... ) - - >>> igraph = IncidenceGraphInterface(m, include_inequality=False) - >>> print(len(igraph.variables)) - 10 - >>> print(len(igraph.constraints)) - 10 - >>> var_dm_partition, con_dm_partition = igraph.dulmage_mendelsohn() - - >>> # There are now no unmatched variables or equations - >>> print(len(var_dm_partition.unmatched)) - 0 - >>> print(len(con_dm_partition.unmatched)) - 0 diff --git a/doc/Archive/contributed_packages/incidence/tutorial.rst b/doc/Archive/contributed_packages/incidence/tutorial.rst deleted file mode 100644 index 4b22fc16c53..00000000000 --- a/doc/Archive/contributed_packages/incidence/tutorial.rst +++ /dev/null @@ -1,14 +0,0 @@ -.. _incidence_tutorial: - -Incidence Analysis Tutorial -=========================== - -This tutorial walks through examples of the most common use cases for -Incidence Analysis: - -.. toctree:: - :maxdepth: 1 - - tutorial.dm.rst - tutorial.bt.rst - tutorial.btsolve.rst diff --git a/doc/Archive/contributed_packages/index.rst b/doc/Archive/contributed_packages/index.rst deleted file mode 100644 index 65c14a721df..00000000000 --- a/doc/Archive/contributed_packages/index.rst +++ /dev/null @@ -1,47 +0,0 @@ -Third-Party Contributions -========================= - -Pyomo includes a variety of additional features and functionality -provided by third parties through the ``pyomo.contrib`` package. This -package includes both contributions included with the main Pyomo -distribution and wrappers for third-party packages that must be -installed separately. - -These packages are maintained by the original contributors and are -managed as *optional* Pyomo packages. - -Contributed packages distributed with Pyomo: - -.. toctree:: - :maxdepth: 1 - - alternative_solutions.rst - community.rst - doe/doe.rst - gdpopt.rst - iis.rst - incidence/index.rst - latex_printer.rst - mindtpy.rst - mpc/index.rst - multistart.rst - preprocessing.rst - parmest/index.rst - pynumero/index.rst - pyros.rst - sensitivity_toolbox.rst - trustregion.rst - -Contributed Pyomo interfaces to other packages: - -.. toctree:: - :maxdepth: 1 - - mcpp.rst - satsolver.rst - - -Contributed packages distributed independently of Pyomo, but accessible -through ``pyomo.contrib``: - -* `pyomo.contrib.simplemodel `_ diff --git a/doc/Archive/contributed_packages/latex_printer.rst b/doc/Archive/contributed_packages/latex_printer.rst deleted file mode 100644 index ff3f628c0c8..00000000000 --- a/doc/Archive/contributed_packages/latex_printer.rst +++ /dev/null @@ -1,127 +0,0 @@ -Latex Printing -============== - -Pyomo models can be printed to a LaTeX compatible format using the ``pyomo.contrib.latex_printer.latex_printer`` function: - -.. autofunction:: pyomo.contrib.latex_printer.latex_printer.latex_printer - -.. note:: - - If operating in a Jupyter Notebook, it may be helpful to use: - - ``from IPython.display import display, Math`` - - ``display(Math(latex_printer(m))`` - -Examples --------- - -A Model -+++++++ - -.. doctest:: - - >>> import pyomo.environ as pyo - >>> from pyomo.contrib.latex_printer import latex_printer - - >>> m = pyo.ConcreteModel(name = 'basicFormulation') - >>> m.x = pyo.Var() - >>> m.y = pyo.Var() - >>> m.z = pyo.Var() - >>> m.c = pyo.Param(initialize=1.0, mutable=True) - >>> m.objective = pyo.Objective( expr = m.x + m.y + m.z ) - >>> m.constraint_1 = pyo.Constraint(expr = m.x**2 + m.y**2.0 - m.z**2.0 <= m.c ) - - >>> pstr = latex_printer(m) - - -A Constraint -++++++++++++ - -.. doctest:: - - >>> import pyomo.environ as pyo - >>> from pyomo.contrib.latex_printer import latex_printer - - >>> m = pyo.ConcreteModel(name = 'basicFormulation') - >>> m.x = pyo.Var() - >>> m.y = pyo.Var() - - >>> m.constraint_1 = pyo.Constraint(expr = m.x**2 + m.y**2 <= 1.0) - - >>> pstr = latex_printer(m.constraint_1) - -A Constraint with Set Summation -+++++++++++++++++++++++++++++++ - -.. doctest:: - - >>> import pyomo.environ as pyo - >>> from pyomo.contrib.latex_printer import latex_printer - >>> m = pyo.ConcreteModel(name='basicFormulation') - >>> m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) - >>> m.v = pyo.Var(m.I) - - >>> def ruleMaker(m): return sum(m.v[i] for i in m.I) <= 0 - - >>> m.constraint = pyo.Constraint(rule=ruleMaker) - - >>> pstr = latex_printer(m.constraint) - -Using a ComponentMap to Specify Names -+++++++++++++++++++++++++++++++++++++ - -.. doctest:: - - >>> import pyomo.environ as pyo - >>> from pyomo.contrib.latex_printer import latex_printer - >>> from pyomo.common.collections.component_map import ComponentMap - - >>> m = pyo.ConcreteModel(name='basicFormulation') - >>> m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) - >>> m.v = pyo.Var(m.I) - - >>> def ruleMaker(m): return sum(m.v[i] for i in m.I) <= 0 - - >>> m.constraint = pyo.Constraint(rule=ruleMaker) - - >>> lcm = ComponentMap() - >>> lcm[m.v] = 'x' - >>> lcm[m.I] = ['\\mathcal{A}',['j','k']] - - >>> pstr = latex_printer(m.constraint, latex_component_map=lcm) - - -An Expression -+++++++++++++ - -.. doctest:: - - >>> import pyomo.environ as pyo - >>> from pyomo.contrib.latex_printer import latex_printer - - >>> m = pyo.ConcreteModel(name = 'basicFormulation') - >>> m.x = pyo.Var() - >>> m.y = pyo.Var() - - >>> m.expression_1 = pyo.Expression(expr = m.x**2 + m.y**2) - - >>> pstr = latex_printer(m.expression_1) - - -A Simple Expression -+++++++++++++++++++ - -.. doctest:: - - >>> import pyomo.environ as pyo - >>> from pyomo.contrib.latex_printer import latex_printer - - >>> m = pyo.ConcreteModel(name = 'basicFormulation') - >>> m.x = pyo.Var() - >>> m.y = pyo.Var() - - >>> pstr = latex_printer(m.x + m.y) - - - diff --git a/doc/Archive/contributed_packages/mcpp.rst b/doc/Archive/contributed_packages/mcpp.rst deleted file mode 100644 index 18cea7f9b20..00000000000 --- a/doc/Archive/contributed_packages/mcpp.rst +++ /dev/null @@ -1,62 +0,0 @@ -MC++ Interface -============== - -The Pyomo-MC++ interface allows for bounding of factorable functions using the MC++ library developed by -the OMEGA research group at Imperial College London. -Documentation for MC++ may be found on the `MC++ website`_. - -.. _MC++ website: https://github.com/omega-icl/mcpp - - -Default Installation --------------------- -Pyomo now supports automated downloading and compilation of MC++. -To install MC++ and other third party compiled extensions, run: - -.. code:: - - pyomo download-extensions - pyomo build-extensions - -To get and install just MC++, run the following commands in the ``pyomo/contrib/mcpp`` directory: - -.. code:: - - python getMCPP.py - python build.py - -This should install MC++ to the pyomo plugins directory, by default located at ``$HOME/.pyomo/``. - - -Manual Installation -------------------- - -Support for MC++ has only been validated by Pyomo developers using Linux and OSX. -Installation instructions for the MC++ library may be found on the `MC++ website`_. - -We assume that you have installed MC++ into a directory of your choice. -We will denote this directory by ``$MCPP_PATH``. -For example, you should see that the file ``$MCPP_PATH/INSTALL`` exists. - -Navigate to the ``pyomo/contrib/mcpp`` directory in your pyomo installation. -This directory should contain a file named ``mcppInterface.cpp``. -You will need to compile this file using the following command: - -.. code:: - - g++ -I $MCPP_PATH/src/3rdparty/fadbad++ -I $MCPP_PATH/src/mc -I /usr/include/python3.7 -fPIC -O2 -c mcppInterface.cpp - -This links the MC++ required library FADBAD++, MC++ itself, and Python to compile the Pyomo-MC++ interface. -If successful, you will now have a file named ``mcppInterface.o`` in your working directory. -If you are not using Python 3.7, you will need to link to the appropriate Python version. -You now need to create a shared object file with the following command: - -.. code:: - - g++ -shared mcppInterface.o -o mcppInterface.so - -You may then test your installation by running the test file: - -.. code:: - - python test_mcpp.py diff --git a/doc/Archive/contributed_packages/mindtpy.rst b/doc/Archive/contributed_packages/mindtpy.rst deleted file mode 100644 index a850a42c740..00000000000 --- a/doc/Archive/contributed_packages/mindtpy.rst +++ /dev/null @@ -1,319 +0,0 @@ -MindtPy Solver -============== - -The Mixed-Integer Nonlinear Decomposition Toolbox in Pyomo (MindtPy) solver -allows users to solve Mixed-Integer Nonlinear Programs (MINLP) using decomposition algorithms. -These decomposition algorithms usually rely on the solution of Mixed-Integer Linear Programs -(MILP) and Nonlinear Programs (NLP). - -The following algorithms are currently available in MindtPy: - -- **Outer-Approximation (OA)** [`Duran & Grossmann, 1986`_] -- **LP/NLP based Branch-and-Bound (LP/NLP BB)** [`Quesada & Grossmann, 1992`_] -- **Extended Cutting Plane (ECP)** [`Westerlund & Petterson, 1995`_] -- **Global Outer-Approximation (GOA)** [`Kesavan & Allgor, 2004`_, `MC++`_] -- **Regularized Outer-Approximation (ROA)** [`Bernal & Peng, 2021`_, `Kronqvist & Bernal, 2018`_] -- **Feasibility Pump (FP)** [`Bernal & Vigerske, 2019`_, `Bonami & Cornuéjols, 2009`_] - -Usage and early implementation details for MindtPy can be found in the PSE 2018 paper Bernal et al., -(`ref `_, -`preprint `_). -This solver implementation has been developed by `David Bernal `_ -and `Zedong Peng `_ as part of research efforts at the `Bernal Research Group -`_ and the `Grossmann Research Group `_ -at Purdue University and Carnegie Mellon University. - -.. _Duran & Grossmann, 1986: https://dx.doi.org/10.1007/BF02592064 -.. _Westerlund & Petterson, 1995: http://dx.doi.org/10.1016/0098-1354(95)87027-X -.. _Kesavan & Allgor, 2004: https://link.springer.com/article/10.1007/s10107-004-0503-1 -.. _MC++: https://pyomo.readthedocs.io/en/stable/contributed_packages/mcpp.html -.. _Bernal & Peng, 2021: http://www.optimization-online.org/DB_HTML/2021/06/8452.html -.. _Kronqvist & Bernal, 2018: https://link.springer.com/article/10.1007%2Fs10107-018-1356-3 -.. _Bonami & Cornuéjols, 2009: https://link.springer.com/article/10.1007/s10107-008-0212-2 -.. _Bernal & Vigerske, 2019: https://www.tandfonline.com/doi/abs/10.1080/10556788.2019.1641498 -.. _Kronqvist et al., 2019: https://link.springer.com/article/10.1007/s11081-018-9411-8 - -MINLP Formulation ------------------ - -The general formulation of the mixed integer nonlinear programming (MINLP) models is as follows. - -.. math:: - :nowrap: - - \begin{equation} - \label{eq:MINLP} - \tag{MINLP} - \begin{aligned} - &\min_{\mathbf{x,y}} &&f(\mathbf{x,y})\\ - & \text{s.t.} \ &&g_j(\mathbf{x,y}) \leq 0 \quad \ \forall j=1,\dots l,\\ - & &&\mathbf{A}\mathbf{x} +\mathbf{B}\mathbf{y} \leq \mathbf{b}, \\ - & &&\mathbf{x}\in {\mathbb R}^n,\ \mathbf{y} \in {\mathbb Z}^m. - \end{aligned} - \end{equation} - -where - -- :math:`\mathbf{x}\in {\mathbb R}^n` are continuous variables, -- :math:`\mathbf{y} \in {\mathbb Z}^m` are discrete variables, -- :math:`f, g_1, \dots, g_l` are non-linear smooth functions, -- :math:`\mathbf{A}\mathbf{x} +\mathbf{B}\mathbf{y} \leq \mathbf{b}`` are linear constraints. - -Solve Convex MINLPs -------------------- - -Usage of MindtPy to solve a convex MINLP Pyomo model involves: - -.. code:: - - >>> SolverFactory('mindtpy').solve(model) - -An example which includes the modeling approach may be found below. - -.. doctest:: - - Required imports - >>> from pyomo.environ import * - - Create a simple model - >>> model = ConcreteModel() - - >>> model.x = Var(bounds=(1.0,10.0),initialize=5.0) - >>> model.y = Var(within=Binary) - - >>> model.c1 = Constraint(expr=(model.x-4.0)**2 - model.x <= 50.0*(1-model.y)) - >>> model.c2 = Constraint(expr=model.x*log(model.x)+5.0 <= 50.0*(model.y)) - - >>> model.objective = Objective(expr=model.x, sense=minimize) - - Solve the model using MindtPy - >>> SolverFactory('mindtpy').solve(model, mip_solver='glpk', nlp_solver='ipopt') # doctest: +SKIP - -The solution may then be displayed by using the commands - -.. code:: - - >>> model.objective.display() - >>> model.display() - >>> model.pprint() - -.. note:: - - When troubleshooting, it can often be helpful to turn on verbose - output using the ``tee`` flag. - -.. code:: - - >>> SolverFactory('mindtpy').solve(model, mip_solver='glpk', nlp_solver='ipopt', tee=True) - -MindtPy also supports setting options for mip solvers and nlp solvers. - -.. code:: - - >>> SolverFactory('mindtpy').solve(model, - strategy='OA', - time_limit=3600, - mip_solver='gams', - mip_solver_args=dict(solver='cplex', warmstart=True), - nlp_solver='ipopt', - tee=True) - -There are three initialization strategies in MindtPy: ``rNLP``, ``initial_binary``, ``max_binary``. In OA and GOA strategies, the default initialization strategy is ``rNLP``. In ECP strategy, the default initialization strategy is ``max_binary``. - -LP/NLP Based Branch-and-Bound -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -MindtPy also supports single-tree implementation of Outer-Approximation (OA) algorithm, which is known as LP/NLP based branch-and-bound algorithm originally described in [`Quesada & Grossmann, 1992`_]. -The LP/NLP based branch-and-bound algorithm in MindtPy is implemented based on the LazyConstraintCallback function in commercial solvers. - -.. _Quesada & Grossmann, 1992: https://www.sciencedirect.com/science/article/abs/pii/0098135492800288 - -.. note:: - - In Pyomo, `persistent solvers`_ are necessary to set or register callback functions. The single tree implementation currently only works with CPLEX and GUROBI, more exactly ``cplex_persistent`` and ``gurobi_persistent``. To use the `LazyConstraintCallback`_ function of CPLEX from Pyomo, the `CPLEX Python API`_ is required. This means both IBM ILOG CPLEX Optimization Studio and the CPLEX-Python modules should be installed on your computer. To use the `cbLazy`_ function of GUROBI from pyomo, `gurobipy`_ is required. - -.. _`persistent solvers`: https://pyomo.readthedocs.io/en/stable/advanced_topics/persistent_solvers.html?highlight=persistent -.. _CPLEX Python API: https://www.ibm.com/docs/en/icos/20.1.0?topic=cplex-setting-up-python-api -.. _gurobipy: https://www.gurobi.com/documentation/9.1/quickstart_mac/cs_grbpy_the_gurobi_python.html -.. _LazyConstraintCallback: https://www.ibm.com/docs/en/icos/20.1.0?topic=classes-cplexcallbackslazyconstraintcallback -.. _cbLazy: https://www.gurobi.com/documentation/9.1/refman/py_model_cblazy.html - -A usage example for LP/NLP based branch-and-bound algorithm is as follows: - -.. code:: - - >>> pyo.SolverFactory('mindtpy').solve(model, - ... strategy='OA', - ... mip_solver='cplex_persistent', # or 'gurobi_persistent' - ... nlp_solver='ipopt', - ... single_tree=True) - >>> model.objective.display() - - -Regularized Outer-Approximation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -As a new implementation in MindtPy, we provide a flexible regularization technique implementation. In this technique, an extra mixed-integer problem is solved in each decomposition iteration or incumbent solution of the single-tree solution methods. The extra mixed-integer program is constructed to provide a point where the NLP problem is solved closer to the feasible region described by the non-linear constraint. This approach has been proposed in [`Kronqvist et al., 2020`_], and it has shown to be efficient for highly non-linear convex MINLP problems. In [`Kronqvist et al., 2020`_], two different regularization approaches are proposed, using a squared Euclidean norm which was proved to make the procedure equivalent to adding a trust-region constraint to Outer-approximation, and a second-order approximation of the Lagrangian of the problem, which showed better performance. We implement these methods, using PyomoNLP as the interface to compute the second-order approximation of the Lagrangian, and extend them to consider linear norm objectives and first-order approximations of the Lagrangian. Finally, we implemented an approximated second-order expansion of the Lagrangian, drawing inspiration from the Sequential Quadratic Programming (SQP) literature. The details of this implementation are included in [`Bernal et al., 2021`_]. - -.. _Kronqvist et al., 2020: https://link.springer.com/article/10.1007/s10107-018-1356-3 -.. _Bernal et al., 2021: http://www.optimization-online.org/DB_HTML/2021/06/8452.html - -A usage example for regularized OA is as follows: - -.. code:: - - >>> pyo.SolverFactory('mindtpy').solve(model, - ... strategy='OA', - ... mip_solver='cplex', - ... nlp_solver='ipopt', - ... add_regularization='level_L1' - ... # alternative regularizations - ... # 'level_L1', 'level_L2', 'level_L_infinity', - ... # 'grad_lag', 'hess_lag', 'hess_only_lag', 'sqp_lag' - ... ) - >>> model.objective.display() - - -Solution Pool Implementation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -MindtPy supports solution pool of the MILP solver, CPLEX and GUROBI. With the help of the solution, MindtPy can explore several integer combinations in one iteration. - -A usage example for OA with solution pool is as follows: - -.. code:: - - >>> pyo.SolverFactory('mindtpy').solve(model, - ... strategy='OA', - ... mip_solver='cplex_persistent', - ... nlp_solver='ipopt', - ... solution_pool=True, - ... num_solution_iteration=10, # default=5 - ... tee=True - ... ) - >>> model.objective.display() - -Feasibility Pump -^^^^^^^^^^^^^^^^ - -For some MINLP problems, the Outer Approximation method might have difficulty in finding a feasible solution. MindtPy provides the Feasibility Pump implementation to find feasible solutions for convex MINLPs quickly. The main idea of the Feasibility Pump is to decompose the original mixed-integer problem into two parts: integer feasibility and constraint feasibility. For convex MINLPs, a MIP is solved to obtain a solution, which satisfies the integrality constraints on `y`, but may violate some of the nonlinear constraints; next, by solving an NLP, a solution is computed that satisfies the nonlinear constraints but might again violate the integrality constraints on `y`. By minimizing the distance between these two types of solutions iteratively, a constraint and integer feasible solution can be expected. In MindtPy, the Feasibility Pump can be used both as an initialization strategy and a decomposition strategy. For details of this implementation are included in [`Bernal et al., 2017`_]. - -.. _Bernal et al., 2017: http://www.optimization-online.org/DB_HTML/2017/08/6171.html - -A usage example for Feasibility Pump as the initialization strategy is as follows: - -.. code:: - - >>> pyo.SolverFactory('mindtpy').solve(model, - ... strategy='OA', - ... init_strategy='FP', - ... mip_solver='cplex', - ... nlp_solver='ipopt', - ... tee=True - ... ) - >>> model.objective.display() - -A usage example for Feasibility Pump as the decomposition strategy is as follows: - -.. code:: - - >>> pyo.SolverFactory('mindtpy').solve(model, - ... strategy='FP', - ... mip_solver='cplex', - ... nlp_solver='ipopt', - ... tee=True - ... ) - >>> model.objective.display() - - - -Solve Nonconvex MINLPs ----------------------- - - -Equality Relaxation -^^^^^^^^^^^^^^^^^^^ - -Under certain assumptions concerning the convexity of the nonlinear functions, an equality constraint can be relaxed to be an inequality constraint. This property can be used in the MIP master problem to accumulate linear approximations(OA cuts). The sense of the equivalent inequality constraint is based on the sign of the dual values of the equality constraint. Therefore, the sense of the OA cuts for equality constraint should be determined according to both the objective sense and the sign of the dual values. In MindtPy, the dual value of the equality constraint is calculated as follows. - -+--------------------+-----------------------+-------------------------+ -| constraint | status at :math:`x_1` | dual values | -+====================+=======================+=========================+ -| :math:`g(x) \le b` | :math:`g(x_1) \le b` | 0 | -+--------------------+-----------------------+-------------------------+ -| :math:`g(x) \le b` | :math:`g(x_1) > b` | :math:`g(x1) - b` | -+--------------------+-----------------------+-------------------------+ -| :math:`g(x) \ge b` | :math:`g(x_1) \ge b` | 0 | -+--------------------+-----------------------+-------------------------+ -| :math:`g(x) \ge b` | :math:`g(x_1) < b` | :math:`b - g(x1)` | -+--------------------+-----------------------+-------------------------+ - -Augmented Penalty -^^^^^^^^^^^^^^^^^ - -Augmented Penalty refers to the introduction of (non-negative) slack variables on the right hand sides of the just described inequality constraints and the modification of the objective function when assumptions concerning convexity do not hold. (From DICOPT) - - -Global Outer-Approximation -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Apart from the decomposition methods for convex MINLP problems [`Kronqvist et al., 2019`_], MindtPy provides an implementation of Global Outer Approximation (GOA) as described in [`Kesavan & Allgor, 2004`_], to provide optimality guaranteed for nonconvex MINLP problems. Here, the validity of the Mixed-integer Linear Programming relaxation of the original problem is guaranteed via the usage of Generalized McCormick envelopes, computed using the package `MC++`_. The NLP subproblems, in this case, need to be solved to global optimality, which can be achieved through global NLP solvers such as `BARON`_ or `SCIP`_. - -.. _BARON: https://minlp.com/baron-solver -.. _SCIP: https://www.scipopt.org/ - - -Convergence -""""""""""" - -MindtPy provides two ways to guarantee the finite convergence of the algorithm. - -- **No-good cuts**. No-good cuts(integer cuts) are added to the MILP master problem in each iteration. -- **Tabu list**. Tabu list is only supported if the ``mip_solver`` is ``cplex_persistent`` (``gurobi_persistent`` pending). In each iteration, the explored integer combinations will be added to the `tabu_list`. When solving the next MILP problem, the MIP solver will reject the previously explored solutions in the branch and bound process through IncumbentCallback. - - -Bound Calculation -""""""""""""""""" - -Since no-good cuts or tabu list is applied in the Global Outer-Approximation (GOA) method, the MILP master problem cannot provide a valid bound for the original problem. After the GOA method has converged, MindtPy will remove the no-good cuts or the tabu integer combinations added when and after the optimal solution has been found. Solving this problem will give us a valid bound for the original problem. - - -The GOA method also has a single-tree implementation with ``cplex_persistent`` and ``gurobi_persistent``. Notice that this method is more computationally expensive than the other strategies implemented for convex MINLP like OA and ECP, which can be used as heuristics for nonconvex MINLP problems. - -A usage example for GOA is as follows: - -.. code:: - - >>> pyo.SolverFactory('mindtpy').solve(model, - ... strategy='GOA', - ... mip_solver='cplex', - ... nlp_solver='baron') - >>> model.objective.display() - - - -MindtPy Implementation and Optional Arguments ---------------------------------------------- - -.. warning:: - - MindtPy optional arguments should be considered beta code and are - subject to change. - -.. autoclass:: pyomo.contrib.mindtpy.MindtPy.MindtPySolver - :members: - -Get Help --------- - -Ways to get help: https://github.com/Pyomo/pyomo#getting-help - -Report a Bug ------------- - -If you find a bug in MindtPy, we will be grateful if you could - -- submit an `issue`_ in Pyomo repository -- directly contact David Bernal and Zedong Peng . - -.. _issue: https://github.com/Pyomo/pyomo/issues diff --git a/doc/Archive/contributed_packages/mpc/api.rst b/doc/Archive/contributed_packages/mpc/api.rst deleted file mode 100644 index 2752fea8af6..00000000000 --- a/doc/Archive/contributed_packages/mpc/api.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. _mpc_api: - -API Reference -============= - -.. toctree:: - data.rst - conversion.rst - interface.rst - modeling.rst diff --git a/doc/Archive/contributed_packages/mpc/conversion.rst b/doc/Archive/contributed_packages/mpc/conversion.rst deleted file mode 100644 index 9d9406edb75..00000000000 --- a/doc/Archive/contributed_packages/mpc/conversion.rst +++ /dev/null @@ -1,5 +0,0 @@ -Data Conversion -=============== - -.. automodule:: pyomo.contrib.mpc.data.convert - :members: diff --git a/doc/Archive/contributed_packages/mpc/data.rst b/doc/Archive/contributed_packages/mpc/data.rst deleted file mode 100644 index 73cb6543b1e..00000000000 --- a/doc/Archive/contributed_packages/mpc/data.rst +++ /dev/null @@ -1,17 +0,0 @@ -Data Structures -=============== - -.. automodule:: pyomo.contrib.mpc.data.get_cuid - :members: - -.. automodule:: pyomo.contrib.mpc.data.dynamic_data_base - :members: - -.. automodule:: pyomo.contrib.mpc.data.scalar_data - :members: - -.. automodule:: pyomo.contrib.mpc.data.series_data - :members: - -.. automodule:: pyomo.contrib.mpc.data.interval_data - :members: diff --git a/doc/Archive/contributed_packages/mpc/examples.rst b/doc/Archive/contributed_packages/mpc/examples.rst deleted file mode 100644 index 95204192358..00000000000 --- a/doc/Archive/contributed_packages/mpc/examples.rst +++ /dev/null @@ -1,6 +0,0 @@ -Examples -======== - -Please see ``pyomo/contrib/mpc/examples/cstr/run_openloop.py`` and -``pyomo/contrib/mpc/examples/cstr/run_mpc.py`` for examples of some simple -use cases. diff --git a/doc/Archive/contributed_packages/mpc/faq.rst b/doc/Archive/contributed_packages/mpc/faq.rst deleted file mode 100644 index e42e7184696..00000000000 --- a/doc/Archive/contributed_packages/mpc/faq.rst +++ /dev/null @@ -1,16 +0,0 @@ -Frequently asked questions -========================== - -#. Why not use Pandas DataFrames? - -Pandas DataFrames are a natural data structure for storing "columns" of -time series data. These columns, or individual time series, could each represent -the data for a single variable. This is very similar to the TimeSeriesData -class introduced in this package. -The reason a new data structure is introduced is primarily that a DataFrame -does not provide any utility for converting labels into a consistent format, -as TimeSeriesData does by accepting variables, strings, slices, etc. -as keys and converting them into the form of a time-indexed ComponentUID. -Also, DataFrames do not have convenient analogs for scalar data and -time interval data, which this package provides as the ScalarData -and IntervalData classes with very similar APIs to TimeSeriesData. diff --git a/doc/Archive/contributed_packages/mpc/index.rst b/doc/Archive/contributed_packages/mpc/index.rst deleted file mode 100644 index e512d1a6ef5..00000000000 --- a/doc/Archive/contributed_packages/mpc/index.rst +++ /dev/null @@ -1,32 +0,0 @@ -MPC -=== - -Pyomo MPC contains data structures and utilities for dynamic optimization -and rolling horizon applications, e.g. model predictive control. - -.. toctree:: - :maxdepth: 1 - - overview.rst - examples.rst - faq.rst - api.rst - -Citation --------- - -If you use Pyomo MPC in your research, please cite the following paper: - -.. code-block:: bibtex - - @article{parker2023mpc, - title = {Model predictive control simulations with block-hierarchical differential-algebraic process models}, - journal = {Journal of Process Control}, - volume = {132}, - pages = {103113}, - year = {2023}, - issn = {0959-1524}, - doi = {https://doi.org/10.1016/j.jprocont.2023.103113}, - url = {https://www.sciencedirect.com/science/article/pii/S0959152423002007}, - author = {Robert B. Parker and Bethany L. Nicholson and John D. Siirola and Lorenz T. Biegler}, - } diff --git a/doc/Archive/contributed_packages/mpc/interface.rst b/doc/Archive/contributed_packages/mpc/interface.rst deleted file mode 100644 index eb5bac548fd..00000000000 --- a/doc/Archive/contributed_packages/mpc/interface.rst +++ /dev/null @@ -1,8 +0,0 @@ -Interfaces -========== - -.. automodule:: pyomo.contrib.mpc.interfaces.model_interface - :members: - -.. automodule:: pyomo.contrib.mpc.interfaces.var_linker - :members: diff --git a/doc/Archive/contributed_packages/mpc/modeling.rst b/doc/Archive/contributed_packages/mpc/modeling.rst deleted file mode 100644 index cbae03161b1..00000000000 --- a/doc/Archive/contributed_packages/mpc/modeling.rst +++ /dev/null @@ -1,11 +0,0 @@ -Modeling Components -=================== - -.. automodule:: pyomo.contrib.mpc.modeling.constraints - :members: - -.. automodule:: pyomo.contrib.mpc.modeling.cost_expressions - :members: - -.. automodule:: pyomo.contrib.mpc.modeling.terminal - :members: diff --git a/doc/Archive/contributed_packages/mpc/overview.rst b/doc/Archive/contributed_packages/mpc/overview.rst deleted file mode 100644 index f3bc7504b59..00000000000 --- a/doc/Archive/contributed_packages/mpc/overview.rst +++ /dev/null @@ -1,210 +0,0 @@ -Overview -======== - -What does this package contain? -------------------------------- - -#. Data structures for values and time series data associated with time-indexed variables (or parameters, or named expressions). Examples are setpoint values associated with a subset of state variables or time series data from a simulation - -#. Utilities for loading and extracting this data into and from variables in a model - -#. Utilities for constructing components from this data (expressions, constraints, and objectives) that are useful for dynamic optimization - -What is the goal of this package? ---------------------------------- - -This package was written to help developers of Pyomo-based dynamic optimization -case studies, especially rolling horizon dynamic optimization case studies, -write scripts that are small, legible, and maintainable. -It does this by providing utilities for mundane data-management and model -construction tasks, allowing the developer to focus on their application. - -Why is this package useful? ---------------------------- - -First, it is not normally easy to extract "flattened" time series data, -in which all indexing structure other than time-indexing has been -flattened to yield a set of one-dimensional arrays, from a Pyomo model. -This is an extremely convenient data structure to have for plotting, -analysis, initialization, and manipulation of dynamic models. -If all variables are indexed by time and only time, this data is relatively -easy to obtain. -The first issue comes up when dealing with components that are indexed by -time in addition to some other set(s). For example: - -.. doctest:: - - >>> import pyomo.environ as pyo - - >>> m = pyo.ConcreteModel() - >>> m.time = pyo.Set(initialize=[0, 1, 2]) - >>> m.comp = pyo.Set(initialize=["A", "B"]) - >>> m.var = pyo.Var(m.time, m.comp, initialize=1.0) - - >>> t0 = m.time.first() - >>> data = { - ... m.var[t0, j].name: [m.var[i, j].value for i in m.time] - ... for j in m.comp - ... } - >>> data - {'var[0,A]': [1.0, 1.0, 1.0], 'var[0,B]': [1.0, 1.0, 1.0]} - -To generate data in this form, we need to (a) know that our variable is indexed -by time and ``m.comp`` and (b) arbitrarily select a time index ``t0`` to -generate a unique key for each time series. -This gets more difficult when blocks and time-indexed blocks are used as well. -The first difficulty can be alleviated using -``flatten_dae_components`` from ``pyomo.dae.flatten``: - -.. doctest:: - - >>> import pyomo.environ as pyo - >>> from pyomo.dae.flatten import flatten_dae_components - - >>> m = pyo.ConcreteModel() - >>> m.time = pyo.Set(initialize=[0, 1, 2]) - >>> m.comp = pyo.Set(initialize=["A", "B"]) - >>> m.var = pyo.Var(m.time, m.comp, initialize=1.0) - - >>> t0 = m.time.first() - >>> scalar_vars, dae_vars = flatten_dae_components(m, m.time, pyo.Var) - >>> data = {var[t0].name: list(var[:].value) for var in dae_vars} - >>> data - {'var[0,A]': [1.0, 1.0, 1.0], 'var[0,B]': [1.0, 1.0, 1.0]} - -Addressing the arbitrary ``t0`` index requires us to ask what key we -would like to use to identify each time series in our data structure. -The key should uniquely correspond to a component, or "sub-component" -that is indexed only by time. A slice, e.g. ``m.var[:, "A"]`` seems -natural. However, Pyomo provides a better data structure that can -be constructed from a component, slice, or string, called -``ComponentUID``. Being constructable from a string is important as -we may want to store or serialize this data in a form that is agnostic -of any particular ``ConcreteModel`` object. -We can now generate our data structure as: - -.. doctest:: - - >>> data = { - ... pyo.ComponentUID(var.referent): list(var[:].value) - ... for var in dae_vars - ... } - >>> data - {var[*,A]: [1.0, 1.0, 1.0], var[*,B]: [1.0, 1.0, 1.0]} - -This is the structure of the underlying dictionary in the ``TimeSeriesData`` -class provided by this package. We can generate this data using this package -as: - -.. doctest:: - - >>> import pyomo.environ as pyo - >>> from pyomo.contrib.mpc import DynamicModelInterface - - >>> m = pyo.ConcreteModel() - >>> m.time = pyo.Set(initialize=[0, 1, 2]) - >>> m.comp = pyo.Set(initialize=["A", "B"]) - >>> m.var = pyo.Var(m.time, m.comp, initialize=1.0) - - >>> # Construct a helper class for interfacing model with data - >>> helper = DynamicModelInterface(m, m.time) - - >>> # Generates a TimeSeriesData object - >>> series_data = helper.get_data_at_time() - - >>> # Get the underlying dictionary - >>> data = series_data.get_data() - >>> data - {var[*,A]: [1.0, 1.0, 1.0], var[*,B]: [1.0, 1.0, 1.0]} - -The first value proposition of this package is that ``DynamicModelInterface`` -and ``TimeSeriesData`` provide wrappers to ease loading and extraction of data -via ``flatten_dae_components`` and ``ComponentUID``. - -The second difficulty addressed by this package is that of extracting and -loading data between (potentially) different models. -For instance, in model predictive control, we often want to extract data from -a particular time point in a plant model and load it into a controller model -as initial conditions. This can be done as follows: - -.. doctest:: - - >>> import pyomo.environ as pyo - >>> from pyomo.contrib.mpc import DynamicModelInterface - - >>> m1 = pyo.ConcreteModel() - >>> m1.time = pyo.Set(initialize=[0, 1, 2]) - >>> m1.comp = pyo.Set(initialize=["A", "B"]) - >>> m1.var = pyo.Var(m1.time, m1.comp, initialize=1.0) - - >>> m2 = pyo.ConcreteModel() - >>> m2.time = pyo.Set(initialize=[0, 1, 2]) - >>> m2.comp = pyo.Set(initialize=["A", "B"]) - >>> m2.var = pyo.Var(m2.time, m2.comp, initialize=2.0) - - >>> # Construct helper objects - >>> m1_helper = DynamicModelInterface(m1, m1.time) - >>> m2_helper = DynamicModelInterface(m2, m2.time) - - >>> # Extract data from final time point of m2 - >>> tf = m2.time.last() - >>> tf_data = m2_helper.get_data_at_time(tf) - - >>> # Load data into initial time point of m1 - >>> t0 = m1.time.first() - >>> m1_helper.load_data(tf_data, time_points=t0) - - >>> # Get TimeSeriesData object - >>> series_data = m1_helper.get_data_at_time() - >>> # Get underlying dictionary - >>> series_data.get_data() - {var[*,A]: [2.0, 1.0, 1.0], var[*,B]: [2.0, 1.0, 1.0]} - -.. note:: - - Here we rely on the fact that our variable has the same name in - both models. - -Finally, this package provides methods for constructing components like -tracking cost expressions and piecewise-constant constraints from the -provided data structures. For example, the following code constructs -a tracking cost expression. - -.. doctest:: - - >>> import pyomo.environ as pyo - >>> from pyomo.contrib.mpc import DynamicModelInterface - - >>> m = pyo.ConcreteModel() - >>> m.time = pyo.Set(initialize=[0, 1, 2]) - >>> m.comp = pyo.Set(initialize=["A", "B"]) - >>> m.var = pyo.Var(m.time, m.comp, initialize=1.0) - - >>> # Construct helper object - >>> helper = DynamicModelInterface(m, m.time) - - >>> # Construct data structure for setpoints - >>> setpoint = {m.var[:, "A"]: 0.5, m.var[:, "B"]: 2.0} - >>> var_set, tr_cost = helper.get_penalty_from_target(setpoint) - >>> m.setpoint_idx = var_set - >>> m.tracking_cost = tr_cost - >>> m.tracking_cost.pprint() - tracking_cost : Size=6, Index=setpoint_idx*time - Key : Expression - (0, 0) : (var[0,A] - 0.5)**2 - (0, 1) : (var[1,A] - 0.5)**2 - (0, 2) : (var[2,A] - 0.5)**2 - (1, 0) : (var[0,B] - 2.0)**2 - (1, 1) : (var[1,B] - 2.0)**2 - (1, 2) : (var[2,B] - 2.0)**2 - - -These methods will hopefully allow developers to declutter dynamic optimization -scripts and pay more attention to the application of the optimization problem -rather than the setup of the optimization problem. - -Who develops and maintains this package? ----------------------------------------- - -This package was developed by Robert Parker while a PhD student in Larry -Biegler's group at CMU, with guidance from Bethany Nicholson and John Siirola. diff --git a/doc/Archive/contributed_packages/multistart.rst b/doc/Archive/contributed_packages/multistart.rst deleted file mode 100644 index 069d770aa91..00000000000 --- a/doc/Archive/contributed_packages/multistart.rst +++ /dev/null @@ -1,34 +0,0 @@ -Multistart Solver -================== - -The multistart solver is used in cases where the objective function is known -to be non-convex but the global optimum is still desired. It works by running a non-linear -solver of your choice multiple times at different starting points, and -returns the best of the solutions. - - -Using Multistart Solver ------------------------ -To use the multistart solver, define your Pyomo model as usual: - -.. doctest:: - - Required import - >>> from pyomo.environ import * - - Create a simple model - >>> m = ConcreteModel() - >>> m.x = Var() - >>> m.y = Var() - >>> m.obj = Objective(expr=m.x**2 + m.y**2) - >>> m.c = Constraint(expr=m.y >= -2*m.x + 5) - - Invoke the multistart solver - >>> SolverFactory('multistart').solve(m) # doctest: +SKIP - - -Multistart wrapper implementation and optional arguments --------------------------------------------------------- - -.. autoclass:: pyomo.contrib.multistart.multi.MultiStart - :members: diff --git a/doc/Archive/contributed_packages/parmest/api.rst b/doc/Archive/contributed_packages/parmest/api.rst deleted file mode 100644 index 4d6896a8582..00000000000 --- a/doc/Archive/contributed_packages/parmest/api.rst +++ /dev/null @@ -1,25 +0,0 @@ -.. _apisection: - -API -============ - -parmest ---------- -.. automodule:: pyomo.contrib.parmest.parmest - :members: - :undoc-members: - :show-inheritance: - -scenariocreator ------------------- -.. automodule:: pyomo.contrib.parmest.scenariocreator - :members: - :undoc-members: - :show-inheritance: - -graphics ---------- -.. automodule:: pyomo.contrib.parmest.graphics - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/Archive/contributed_packages/parmest/boxplot.png b/doc/Archive/contributed_packages/parmest/boxplot.png deleted file mode 100644 index 25bb4da764a429b33aadb1bd53bdeddab10ddc2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19354 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 import pandas as pd - >>> import pyomo.contrib.parmest.parmest as parmest - >>> from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import RooneyBieglerExperiment - - >>> # Generate data - >>> data = pd.DataFrame(data=[[1,8.3],[2,10.3],[3,19.0], - ... [4,16.0],[5,15.6],[7,19.8]], - ... columns=['hour', 'y']) - - >>> # Create an experiment list - >>> exp_list = [] - >>> for i in range(data.shape[0]): - ... exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) - - >>> # Define objective - >>> def SSE(model): - ... expr = (model.experiment_outputs[model.y] - ... - model.response_function[model.experiment_outputs[model.hour]] - ... ) ** 2 - ... return expr - - >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=None) - >>> obj, theta, var_values = pest.theta_est(return_values=['response_function']) - >>> #print(var_values) diff --git a/doc/Archive/contributed_packages/parmest/driver.rst b/doc/Archive/contributed_packages/parmest/driver.rst deleted file mode 100644 index 866e50205bb..00000000000 --- a/doc/Archive/contributed_packages/parmest/driver.rst +++ /dev/null @@ -1,165 +0,0 @@ -.. _driversection: - -Parameter Estimation -================================== - -Parameter Estimation using parmest requires a Pyomo model, experimental -data which defines multiple scenarios, and parameters -(thetas) to estimate. parmest uses Pyomo [PyomoBookII]_ and (optionally) -mpi-sppy [KMM+23]_ to solve a -two-stage stochastic programming problem, where the experimental data is -used to create a scenario tree. The objective function needs to be -written with the Pyomo Expression for first stage cost -(named "FirstStageCost") set to zero and the Pyomo Expression for second -stage cost (named "SecondStageCost") defined as the deviation between -the model and the observations (typically defined as the sum of squared -deviation between model values and observed values). - -If the Pyomo model is not formatted as a two-stage stochastic -programming problem in this format, the user can supply a custom -function to use as the second stage cost and the Pyomo model will be -modified within parmest to match the required specifications. -The stochastic programming callback function is also defined within parmest. The callback -function returns a populated and initialized model for each scenario. - -To use parmest, the user creates a :class:`~pyomo.contrib.parmest.parmest.Estimator` object -which includes the following methods: - -.. autosummary:: - :nosignatures: - - ~pyomo.contrib.parmest.parmest.Estimator.theta_est - ~pyomo.contrib.parmest.parmest.Estimator.theta_est_bootstrap - ~pyomo.contrib.parmest.parmest.Estimator.theta_est_leaveNout - ~pyomo.contrib.parmest.parmest.Estimator.objective_at_theta - ~pyomo.contrib.parmest.parmest.Estimator.confidence_region_test - ~pyomo.contrib.parmest.parmest.Estimator.likelihood_ratio_test - ~pyomo.contrib.parmest.parmest.Estimator.leaveNout_bootstrap_test - -Additional functions are available in parmest to plot -results and fit distributions to theta values. - -.. autosummary:: - :nosignatures: - - ~pyomo.contrib.parmest.graphics.pairwise_plot - ~pyomo.contrib.parmest.graphics.grouped_boxplot - ~pyomo.contrib.parmest.graphics.grouped_violinplot - ~pyomo.contrib.parmest.graphics.fit_rect_dist - ~pyomo.contrib.parmest.graphics.fit_mvn_dist - ~pyomo.contrib.parmest.graphics.fit_kde_dist - -A :class:`~pyomo.contrib.parmest.parmest.Estimator` object can be -created using the following code. A description of each argument is -listed below. Examples are provided in the :ref:`examplesection` -Section. - -.. testsetup:: * - :skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available - - # Data - import pandas as pd - data = pd.DataFrame( - data=[[1, 8.3], [2, 10.3], [3, 19.0], - [4, 16.0], [5, 15.6], [7, 19.8]], - columns=['hour', 'y'], - ) - - # Sum of squared error function - def SSE(model): - expr = ( - model.experiment_outputs[model.y] - - model.response_function[model.experiment_outputs[model.hour]] - ) ** 2 - return expr - - # Create an experiment list - from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import RooneyBieglerExperiment - exp_list = [] - for i in range(data.shape[0]): - exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) - -.. doctest:: - :skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available - - >>> import pyomo.contrib.parmest.parmest as parmest - >>> pest = parmest.Estimator(exp_list, obj_function=SSE) - -Optionally, solver options can be supplied, e.g., - -.. doctest:: - :skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available - - >>> solver_options = {"max_iter": 6000} - >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=solver_options) - - -List of experiment objects --------------------------- - -The first argument is a list of experiment objects which is used to -create one labeled model for each expeirment. -The template :class:`~pyomo.contrib.parmest.experiment.Experiment` -can be used to generate a list of experiment objects. - -A labeled Pyomo model ``m`` has the following additional suffixes (Pyomo `Suffix`): - -* ``m.experiment_outputs`` which defines experiment output (Pyomo `Param`, `Var`, or `Expression`) - and their associated data values (float, int). -* ``m.unknown_parameters`` which defines the mutable parameters or variables (Pyomo `Param` or `Var`) - to estimate along with their component unique identifier (Pyomo `ComponentUID`). - Within parmest, any parameters that are to be estimated are converted to unfixed variables. - Variables that are to be estimated are also unfixed. - -The experiment class has one required method: - -* :class:`~pyomo.contrib.parmest.experiment.Experiment.get_labeled_model` which returns the labeled Pyomo model. - Note that the model does not have to be specifically written as a - two-stage stochastic programming problem for parmest. - That is, parmest can modify the - objective, see :ref:`ObjFunction` below. - -Parmest comes with several :ref:`examplesection` that illustrates how to set up the list of experiment objects. -The examples commonly include additional :class:`~pyomo.contrib.parmest.experiment.Experiment` class methods to -create the model, finalize the model, and label the model. The user can customize methods to suit their needs. - -.. _ObjFunction: - -Objective function ------------------- - -The second argument is an optional argument which defines the -optimization objective function to use in parameter estimation. - -If no objective function is specified, the Pyomo model is used "as is" and -should be defined with "FirstStageCost" and "SecondStageCost" -expressions that are used to build an objective for the two-stage -stochastic programming problem. - -If the Pyomo model is not written as a two-stage stochastic programming problem in -this format, and/or if the user wants to use an objective that is -different than the original model, a custom objective function can be -defined for parameter estimation. The objective function has a single argument, -which is the model from a single experiment. -The objective function returns a Pyomo -expression which is used to define "SecondStageCost". The objective -function can be used to customize data points and weights that are used -in parameter estimation. - -Parmest includes one built in objective function to compute the sum of squared errors ("SSE") between the -``m.experiment_outputs`` model values and data values. - -Suggested initialization procedure for parameter estimation problems --------------------------------------------------------------------- - -To check the quality of initial guess values provided for the fitted parameters, we suggest solving a -square instance of the problem prior to solving the parameter estimation problem using the following steps: - -1. Create :class:`~pyomo.contrib.parmest.parmest.Estimator` object. To initialize the parameter -estimation solve from the square problem solution, set optional argument ``solver_options = {bound_push: 1e-8}``. - -2. Call :class:`~pyomo.contrib.parmest.parmest.Estimator.objective_at_theta` with optional -argument ``(initialize_parmest_model=True)``. Different initial guess values for the fitted -parameters can be provided using optional argument `theta_values` (**Pandas Dataframe**) - -3. Solve parameter estimation problem by calling :class:`~pyomo.contrib.parmest.parmest.Estimator.theta_est` diff --git a/doc/Archive/contributed_packages/parmest/examples.rst b/doc/Archive/contributed_packages/parmest/examples.rst deleted file mode 100644 index a59d79dfa2b..00000000000 --- a/doc/Archive/contributed_packages/parmest/examples.rst +++ /dev/null @@ -1,44 +0,0 @@ -.. _examplesection: - -Examples -======== - -Examples can be found in `pyomo/contrib/parmest/examples` and include: - -* Reactor design example [PyomoBookII]_ -* Semibatch example [SemiBatch]_ -* Rooney Biegler example [RooneyBiegler]_ - -Each example includes a Python file that contains the Pyomo model and a -Python file to run parameter estimation. - -Additional use cases include: - -* Data reconciliation (reactor design example) -* Parameter estimation using data with duplicate sensors and time-series - data (reactor design example) -* Parameter estimation using mpi4py, the example saves results to a file - for later analysis/graphics (semibatch example) - -The example below uses the reactor design example. The file -**reactor_design.py** includes a function which returns an populated -instance of the Pyomo model. Note that the model is defined to maximize -`cb` and that `k1`, `k2`, and `k3` are fixed. The _main_ program is -included for easy testing of the model declaration. - -.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/reactor_design/reactor_design.py - :language: python - -The file **parameter_estimation_example.py** uses parmest to estimate values of `k1`, -`k2`, and `k3` by minimizing the sum of squared error between model and -observed values of `ca`, `cb`, `cc`, and `cd`. Additional example files use -parmest to run parameter estimation with bootstrap resampling and -perform a likelihood ratio test over a range of theta values. - -.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py - :language: python - -The semibatch and Rooney Biegler examples are defined in a similar -manner. - - diff --git a/doc/Archive/contributed_packages/parmest/graphics.rst b/doc/Archive/contributed_packages/parmest/graphics.rst deleted file mode 100644 index a0837472bbb..00000000000 --- a/doc/Archive/contributed_packages/parmest/graphics.rst +++ /dev/null @@ -1,55 +0,0 @@ -.. _graphicssection: - -Graphics -======== - -parmest includes the following functions to help visualize results: - -* :class:`~pyomo.contrib.parmest.graphics.grouped_boxplot` -* :class:`~pyomo.contrib.parmest.graphics.grouped_violinplot` -* :class:`~pyomo.contrib.parmest.graphics.pairwise_plot` - -Grouped boxplots and violinplots are used to compare datasets, generally -before and after data reconciliation. Pairwise plots are used to -visualize results from parameter estimation and include a histogram of -each parameter along the diagonal and a scatter plot for each pair of -parameters in the upper and lower sections. The pairwise plot can also -include the following optional information: - -* A single value for each theta (generally theta* from parameter - estimation). -* Confidence intervals for rectangular, multivariate normal, and/or - Gaussian kernel density estimate distributions at a specified level - (i.e. 0.8). For plots with more than 2 parameters, theta* is used to - extract a slice of the confidence region for each pairwise plot. -* Filled contour lines for objective values at a specified level - (i.e. 0.8). For plots with more than 2 parameters, theta* is used to - extract a slice of the contour lines for each pairwise plot. - -The following examples were generated using the reactor design example. -:numref:`fig-boxplot` uses output from data reconciliation, -:numref:`fig-pairwise1` uses output from the bootstrap analysis, and -:numref:`fig-pairwise2` uses output from the likelihood ratio test. - -.. _fig-boxplot: -.. figure:: boxplot.png - :scale: 90 % - :alt: boxplot - - Grouped boxplot showing data before and after data reconciliation. - -.. _fig-pairwise1: -.. figure:: pairwise_plot_CI.png - :scale: 90 % - :alt: CI - - Pairwise bootstrap plot with rectangular, multivariate normal and - kernel density estimation confidence region. - -.. _fig-pairwise2: -.. figure:: pairwise_plot_LR.png - :scale: 90 % - :alt: LR - - Pairwise likelihood ratio plot with contours of the objective and - points that lie within an alpha confidence region. diff --git a/doc/Archive/contributed_packages/parmest/index.rst b/doc/Archive/contributed_packages/parmest/index.rst deleted file mode 100644 index 2bf4942e632..00000000000 --- a/doc/Archive/contributed_packages/parmest/index.rst +++ /dev/null @@ -1,35 +0,0 @@ -Parameter Estimation with ``parmest`` -===================================== - -``parmest`` is a Python package built on the Pyomo optimization modeling -language ([PyomoJournal]_, [PyomoBookII]_) to support parameter estimation using experimental data along with -confidence regions and subsequent creation of scenarios for stochastic programming. - -Citation for parmest -^^^^^^^^^^^^^^^^^^^^ - -If you use parmest, please cite [ParmestPaper]_ - -Index of parmest documentation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. toctree:: - :maxdepth: 2 - - overview.rst - installation.rst - driver.rst - datarec.rst - covariance.rst - scencreate.rst - graphics.rst - examples.rst - parallel.rst - api.rst - -Indices and Tables ------------------- - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/doc/Archive/contributed_packages/parmest/installation.rst b/doc/Archive/contributed_packages/parmest/installation.rst deleted file mode 100644 index 0cba08039ce..00000000000 --- a/doc/Archive/contributed_packages/parmest/installation.rst +++ /dev/null @@ -1,33 +0,0 @@ -Installation Instructions -========================= - -parmest is included in Pyomo (pyomo/contrib/parmest). -To run parmest, you will need Python version 3.x along with -various Python package dependencies and the IPOPT software -library for non-linear optimization. - -Python package dependencies ---------------------------- - -#. numpy -#. pandas -#. pyomo -#. mpisppy (optional) -#. matplotlib (optional) -#. scipy.stats (optional) -#. seaborn (optional) -#. mpi4py.MPI (optional) - -IPOPT ------ - -IPOPT can be downloaded from https://projects.coin-or.org/Ipopt. - -Testing -------- - -The following commands can be used to test parmest:: - - cd pyomo/contrib/parmest/tests - python test_parmest.py - diff --git a/doc/Archive/contributed_packages/parmest/overview.rst b/doc/Archive/contributed_packages/parmest/overview.rst deleted file mode 100644 index 1b5c71b849e..00000000000 --- a/doc/Archive/contributed_packages/parmest/overview.rst +++ /dev/null @@ -1,72 +0,0 @@ -Overview -======== - -The Python package called parmest facilitates model-based parameter -estimation along with characterization of uncertainty associated with -the estimates. For example, parmest can provide confidence regions -around the parameter estimates. Additionally, parameter vectors, each -with an attached probability estimate, can be used to build scenarios -for design optimization. - -Functionality in parmest includes: - -* Model based parameter estimation using experimental data -* Bootstrap resampling for parameter estimation -* Confidence regions based on single or multi-variate distributions -* Likelihood ratio -* Leave-N-out cross validation -* Parallel processing - -Background ----------- - -The goal of parameter estimation is to estimate values for -a vector, :math:`{\theta}`, to use in the functional form - -.. math:: - - y = g(x; \theta) - -where :math:`x` is a vector containing measured data, typically in high -dimension, :math:`{\theta}` is a vector of values to estimate, in much -lower dimension, and the response vectors are given as :math:`y_{i}, -i=1,\ldots,m` with :math:`m` also much smaller than the dimension of -:math:`x`. This is done by collecting :math:`S` data points, which are -:math:`{\tilde{x}},{\tilde{y}}` pairs and then finding :math:`{\theta}` -values that minimize some function of the deviation between the values -of :math:`{\tilde{y}}` that are measured and the values of -:math:`g({\tilde{x}};{\theta})` for each corresponding -:math:`{\tilde{x}}`, which is a subvector of the vector :math:`x`. Note -that for most experiments, only small parts of :math:`x` will change -from one experiment to the next. - -The following least squares objective can be used to estimate parameter -values, where data points are indexed by :math:`s=1,\ldots,S` - -.. math:: - - \min_{{\theta}} Q({\theta};{\tilde{x}}, {\tilde{y}}) \equiv \sum_{s=1}^{S}q_{s}({\theta};{\tilde{x}}_{s}, {\tilde{y}}_{s}) \;\; - -where - -.. math:: - - q_{s}({\theta};{\tilde{x}}_{s}, {\tilde{y}}_{s}) = \sum_{i=1}^{m}w_{i}\left[{\tilde{y}}_{si} - g_{i}({\tilde{x}}_{s};{\theta})\right]^{2}, - -i.e., the contribution of sample :math:`s` to :math:`Q`, where :math:`w -\in \Re^{m}` is a vector of weights for the responses. For -multi-dimensional :math:`y`, this is the squared weighted :math:`L_{2}` -norm and for univariate :math:`y` the weighted squared deviation. -Custom objectives can also be defined for parameter estimation. - -In the applications of interest to us, the function :math:`g(\cdot)` is -usually defined as an optimization problem with a large number of -(perhaps constrained) optimization variables, a subset of which are -fixed at values :math:`{\tilde{x}}` when the optimization is performed. -In other applications, the values of :math:`{\theta}` are fixed -parameter values, but for the problem formulation above, the values of -:math:`{\theta}` are the primary optimization variables. Note that in -general, the function :math:`g(\cdot)` will have a large set of -parameters that are not included in :math:`{\theta}`. Often, the -:math:`y_{is}` will be vectors themselves, perhaps indexed by time with -index sets that vary with :math:`s`. diff --git a/doc/Archive/contributed_packages/parmest/pairwise_plot_CI.png b/doc/Archive/contributed_packages/parmest/pairwise_plot_CI.png deleted file mode 100644 index dd630f5d177a1a0be4a564ecda6505164353d201..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 84454 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

h#VUT{g!CQ~)Lx4gcLE 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. + +Credit for prototyping and development can be found in the ``GDPopt`` class documentation, below. + +.. _Turkay & Grossmann, 1996: https://dx.doi.org/10.1016/0098-1354(95)00219-7 +.. _Lee & Grossmann, 2001: https://doi.org/10.1016/S0098-1354(01)00732-3 +.. _Lee & Grossmann, 2000: https://doi.org/10.1016/S0098-1354(00)00581-0 +.. _Chen et al., 2018: https://doi.org/10.1016/B978-0-444-64241-7.50143-9 + +GDPopt can be used to solve a Pyomo.GDP concrete model in two ways. +The simplest is to instantiate the generic GDPopt solver and specify the desired algorithm as an argument to the ``solve`` method: + +.. code:: + + >>> SolverFactory('gdpopt').solve(model, algorithm='LOA') + +The alternative is to instantiate an algorithm-specific GDPopt solver: + +.. code:: + + >>> SolverFactory('gdpopt.loa').solve(model) + +In the above examples, GDPopt uses the GDPopt-LOA algorithm. +Other algorithms may be used by specifying them in the ``algorithm`` argument when using the generic solver or by instantiating the algorithm-specific GDPopt solvers. All GDPopt options are listed below. + +.. note:: + + The generic GDPopt solver allows minimal configuration outside of the arguments to the ``solve`` method. To avoid repeatedly specifying the same configuration options to the ``solve`` method, use the algorithm-specific solvers. + +Logic-based Outer Approximation (LOA) +------------------------------------- + +`Chen et al., 2018`_ contains the following flowchart, taken from the preprint version: + +.. image:: gdpopt_flowchart.png + :scale: 70% + +An example that includes the modeling approach may be found below. + +.. doctest:: + :skipif: not glpk_available + + Required imports + >>> from pyomo.environ import * + >>> from pyomo.gdp import * + + Create a simple model + >>> model = ConcreteModel(name='LOA example') + + >>> model.x = Var(bounds=(-1.2, 2)) + >>> model.y = Var(bounds=(-10,10)) + >>> model.c = Constraint(expr= model.x + model.y == 1) + + >>> model.fix_x = Disjunct() + >>> model.fix_x.c = Constraint(expr=model.x == 0) + + >>> model.fix_y = Disjunct() + >>> model.fix_y.c = Constraint(expr=model.y == 0) + + >>> model.d = Disjunction(expr=[model.fix_x, model.fix_y]) + >>> model.objective = Objective(expr=model.x + 0.1*model.y, sense=minimize) + + Solve the model using GDPopt + >>> results = SolverFactory('gdpopt.loa').solve( + ... model, mip_solver='glpk') # doctest: +IGNORE_RESULT + + Display the final solution + >>> model.display() + Model LOA example + + Variables: + x : Size=1, Index=None + Key : Lower : Value : Upper : Fixed : Stale : Domain + None : -1.2 : 0.0 : 2 : False : False : Reals + y : Size=1, Index=None + Key : Lower : Value : Upper : Fixed : Stale : Domain + None : -10 : 1.0 : 10 : False : False : Reals + + Objectives: + objective : Size=1, Index=None, Active=True + Key : Active : Value + None : True : 0.1 + + Constraints: + c : Size=1 + Key : Lower : Body : Upper + None : 1.0 : 1.0 : 1.0 + +.. note:: + + When troubleshooting, it can often be helpful to turn on verbose + output using the ``tee`` flag. + +.. code:: + + >>> SolverFactory('gdpopt.loa').solve(model, tee=True) + +Global Logic-based Outer Approximation (GLOA) +--------------------------------------------- + +The same algorithm can be used to solve GDPs involving nonconvex nonlinear constraints by solving the subproblems globally: + +.. code:: + + >>> SolverFactory('gdpopt.gloa').solve(model) + +.. warning:: + + The ``nlp_solver`` option must be set to a global solver for the solution returned by GDPopt to also be globally optimal. + +Relaxation with Integer Cuts (RIC) +---------------------------------- + +Instead of outer approximation, GDPs can be solved using the same MILP relaxation as in the previous two algorithms, but instead of using the subproblems to generate outer-approximation cuts, the algorithm adds only no-good cuts for every discrete solution encountered: + +.. code:: + + >>> SolverFactory('gdpopt.ric').solve(model) + +Again, this is a global algorithm if the subproblems are solved globally, and is not otherwise. + +.. note:: + + The RIC algorithm will not necessarily enumerate all discrete solutions as it is possible for the bounds to converge first. However, full enumeration is not uncommon. + +Logic-based Branch-and-Bound (LBB) +---------------------------------- + +The GDPopt-LBB solver branches through relaxed subproblems with inactive disjunctions. +It explores the possibilities based on best lower bound, +eventually activating all disjunctions and presenting the globally optimal solution. + +To use the GDPopt-LBB solver, define your Pyomo GDP model as usual: + +.. doctest:: + :skipif: not baron_available + + Required imports + >>> from pyomo.environ import * + >>> from pyomo.gdp import Disjunct, Disjunction + + Create a simple model + >>> m = ConcreteModel() + >>> m.x1 = Var(bounds = (0,8)) + >>> m.x2 = Var(bounds = (0,8)) + >>> m.obj = Objective(expr=m.x1 + m.x2, sense=minimize) + >>> m.y1 = Disjunct() + >>> m.y2 = Disjunct() + >>> m.y1.c1 = Constraint(expr=m.x1 >= 2) + >>> m.y1.c2 = Constraint(expr=m.x2 >= 2) + >>> m.y2.c1 = Constraint(expr=m.x1 >= 3) + >>> m.y2.c2 = Constraint(expr=m.x2 >= 3) + >>> m.djn = Disjunction(expr=[m.y1, m.y2]) + + Invoke the GDPopt-LBB solver + + >>> results = SolverFactory('gdpopt.lbb').solve(m) + WARNING: 09/06/22: The GDPopt LBB algorithm currently has known issues. Please + use the results with caution and report any bugs! + + >>> print(results) # doctest: +SKIP + >>> print(results.solver.status) + ok + >>> print(results.solver.termination_condition) + optimal + + >>> print([value(m.y1.indicator_var), value(m.y2.indicator_var)]) + [True, False] + +GDPopt implementation and optional arguments +-------------------------------------------- + +.. warning:: + + GDPopt optional arguments should be considered beta code and are + subject to change. + +.. autoclass:: pyomo.contrib.gdpopt.GDPopt.GDPoptSolver + :members: + +.. autoclass:: pyomo.contrib.gdpopt.loa.GDP_LOA_Solver + :members: + +.. autoclass:: pyomo.contrib.gdpopt.gloa.GDP_GLOA_Solver + :members: + +.. autoclass:: pyomo.contrib.gdpopt.ric.GDP_RIC_Solver + :members: + +.. autoclass:: pyomo.contrib.gdpopt.branch_and_bound.GDP_LBB_Solver + :members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/gdpopt_flowchart.png b/doc/OnlineDocs/user_guide/contributed_packages/gdpopt_flowchart.png new file mode 100644 index 0000000000000000000000000000000000000000..5bd52426dd62e4d9f6de567b0eab71ebcdfb7f14 GIT binary patch literal 71538 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 literal 0 HcmV?d00001 diff --git a/doc/OnlineDocs/user_guide/contributed_packages/iis.rst b/doc/OnlineDocs/user_guide/contributed_packages/iis.rst new file mode 100644 index 00000000000..fa97c2f8c61 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/iis.rst @@ -0,0 +1,135 @@ +Infeasibility Diagnostics +!!!!!!!!!!!!!!!!!!!!!!!!! + +There are two closely related tools for infeasibility diagnosis: + + - :ref:`Infeasible Irreducible System (IIS) Tool` + - :ref:`Minimal Intractable System finder (MIS) Tool` + +The first simply provides a conduit for solvers that compute an +infeasible irreducible system (e.g., Cplex, Gurobi, or Xpress). The +second provides similar functionality, but uses the ``mis`` package +contributed to Pyomo. + + +Infeasible Irreducible System (IIS) Tool +======================================== + +.. automodule:: pyomo.contrib.iis.iis + +.. autofunction:: pyomo.contrib.iis.write_iis + +Minimal Intractable System finder (MIS) Tool +============================================ + +The file ``mis.py`` finds sets of actions that each, independently, +would result in feasibility. The zero-tolerance is whatever the +solver uses, so users may want to post-process output if it is going +to be used for analysis. It also computes a minimal intractable system +(which is not guaranteed to be unique). It was written by Ben Knueven +as part of the watertap project (https://github.com/watertap-org/watertap) +and is therefore governed by a license shown +at the top of ``mis.py``. + +The algorithms come from John Chinneck's slides, see: https://www.sce.carleton.ca/faculty/chinneck/docs/CPAIOR07InfeasibilityTutorial.pdf + +Solver +------ + +At the time of this writing, you need to use IPopt even for LPs. + +Quick Start +----------- + +The file ``trivial_mis.py`` is a tiny example listed at the bottom of +this help file, which references a Pyomo model with the Python variable +`m` and has these lines: + +.. code-block:: python + + from pyomo.contrib.mis import compute_infeasibility_explanation + ipopt = pyo.SolverFactory("ipopt") + compute_infeasibility_explanation(m, solver=ipopt) + +.. Note:: + This is done instead of solving the problem. + +.. Note:: + IDAES users can pass ``get_solver()`` imported from ``ideas.core.solvers`` + as the solver. + +Interpreting the Output +----------------------- + +Assuming the dependencies are installed, running ``trivial_mis.py`` +(shown below) will +produce a lot of warnings from IPopt and then meaningful output (using a logger). + +Repair Options +^^^^^^^^^^^^^^ + +This output for the trivial example shows three independent ways that the model could be rendered feasible: + + +.. code-block:: text + + Model Trivial Quad may be infeasible. A feasible solution was found with only the following variable bounds relaxed: + ub of var x[1] by 4.464126126706818e-05 + lb of var x[2] by 0.9999553410114216 + Another feasible solution was found with only the following variable bounds relaxed: + lb of var x[1] by 0.7071067726864677 + ub of var x[2] by 0.41421355687130673 + ub of var y by 0.7071067651855212 + Another feasible solution was found with only the following inequality constraints, equality constraints, and/or variable bounds relaxed: + constraint: c by 0.9999999861866736 + + +Minimal Intractable System (MIS) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This output shows a minimal intractable system: + + +.. code-block:: text + + Computed Minimal Intractable System (MIS)! + Constraints / bounds in MIS: + lb of var x[2] + lb of var x[1] + constraint: c + +Constraints / bounds in guards for stability +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This part of the report is for nonlinear programs (NLPs). + +When we’re trying to reduce the constraint set, for an NLP there may be constraints that when missing cause the solver +to fail in some catastrophic fashion. In this implementation this is interpreted as failing to get a `results` +object back from the call to `solve`. In these cases we keep the constraint in the problem but it’s in the +set of “guard” constraints – we can’t really be sure they’re a source of infeasibility or not, +just that “bad things” happen when they’re not included. + +Perhaps ideally we would put a constraint in the “guard” set if IPopt failed to converge, and only put it in the +MIS if IPopt converged to a point of local infeasibility. However, right now the code generally makes the +assumption that if IPopt fails to converge the subproblem is infeasible, though obviously that is far from the truth. +Hence for difficult NLPs even the “Phase 1” may “fail” – in that when finished the subproblem containing just the +constraints in the elastic filter may be feasible -- because IPopt failed to converge and we assumed that meant the +subproblem was not feasible. + +Dealing with NLPs is far from clean, but that doesn’t mean the tool can’t return useful results even when its assumptions are not satisfied. + +trivial_mis.py +-------------- + +.. code-block:: python + + import pyomo.environ as pyo + m = pyo.ConcreteModel("Trivial Quad") + m.x = pyo.Var([1,2], bounds=(0,1)) + m.y = pyo.Var(bounds=(0, 1)) + m.c = pyo.Constraint(expr=m.x[1] * m.x[2] == -1) + m.d = pyo.Constraint(expr=m.x[1] + m.y >= 1) + + from pyomo.contrib.mis import compute_infeasibility_explanation + ipopt = pyo.SolverFactory("ipopt") + compute_infeasibility_explanation(m, solver=ipopt) diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/api.rst b/doc/OnlineDocs/user_guide/contributed_packages/incidence/api.rst new file mode 100644 index 00000000000..38bf0be125b --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/incidence/api.rst @@ -0,0 +1,14 @@ +.. _incidence_api: + +API Reference +============= + +.. toctree:: + incidence.rst + config.rst + interface.rst + matching.rst + connected.rst + triangularize.rst + dulmage_mendelsohn.rst + scc_solver.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/config.rst b/doc/OnlineDocs/user_guide/contributed_packages/incidence/config.rst new file mode 100644 index 00000000000..06e4f5c5626 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/incidence/config.rst @@ -0,0 +1,5 @@ +Incidence Options +================= + +.. automodule:: pyomo.contrib.incidence_analysis.config + :members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/connected.rst b/doc/OnlineDocs/user_guide/contributed_packages/incidence/connected.rst new file mode 100644 index 00000000000..4cf60f62eba --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/incidence/connected.rst @@ -0,0 +1,5 @@ +Weakly Connected Components +=========================== + +.. automodule:: pyomo.contrib.incidence_analysis.connected + :members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/dulmage_mendelsohn.rst b/doc/OnlineDocs/user_guide/contributed_packages/incidence/dulmage_mendelsohn.rst new file mode 100644 index 00000000000..6fe2bd59324 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/incidence/dulmage_mendelsohn.rst @@ -0,0 +1,5 @@ +Dulmage-Mendelsohn Partition +============================ + +.. automodule:: pyomo.contrib.incidence_analysis.dulmage_mendelsohn + :members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/incidence.rst b/doc/OnlineDocs/user_guide/contributed_packages/incidence/incidence.rst new file mode 100644 index 00000000000..ebf481c00a7 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/incidence/incidence.rst @@ -0,0 +1,5 @@ +Incident Variables +================== + +.. automodule:: pyomo.contrib.incidence_analysis.incidence + :members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/index.rst b/doc/OnlineDocs/user_guide/contributed_packages/incidence/index.rst new file mode 100644 index 00000000000..ab0e07f6abc --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/incidence/index.rst @@ -0,0 +1,19 @@ +Incidence Analysis +================== + +Tools for constructing and analyzing the incidence graph of variables +and constraints. + +This documentation contains the following resources: + +.. toctree:: + :maxdepth: 1 + + overview.rst + tutorial.rst + api.rst + +If you are wondering what Incidence Analysis is and would like to learn more, +please see :ref:`incidence_overview`. If you already know what +Incidence Analysis is and are here for reference, see :ref:`incidence_tutorial` +or :ref:`incidence_api` as needed. diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/interface.rst b/doc/OnlineDocs/user_guide/contributed_packages/incidence/interface.rst new file mode 100644 index 00000000000..29c92d8193c --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/incidence/interface.rst @@ -0,0 +1,5 @@ +Pyomo Interfaces +================ + +.. automodule:: pyomo.contrib.incidence_analysis.interface + :members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/matching.rst b/doc/OnlineDocs/user_guide/contributed_packages/incidence/matching.rst new file mode 100644 index 00000000000..1941c7116cd --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/incidence/matching.rst @@ -0,0 +1,5 @@ +Maximum Matching +================ + +.. automodule:: pyomo.contrib.incidence_analysis.matching + :members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/overview.rst b/doc/OnlineDocs/user_guide/contributed_packages/incidence/overview.rst new file mode 100644 index 00000000000..3a49ee2d258 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/incidence/overview.rst @@ -0,0 +1,50 @@ +.. _incidence_overview: + +Overview +======== + +What is Incidence Analysis? +--------------------------- + +A Pyomo extension for constructing the bipartite incidence graph of variables +and constraints, and an interface to useful algorithms for analyzing or +decomposing this graph. + +Why is Incidence Analysis useful? +--------------------------------- + +It can identify the source of certain types of singularities in a system of +variables and constraints. These singularities often violate assumptions made +while modeling a physical system or assumptions required for an optimization +solver to guarantee convergence. In particular, interior point methods used for +nonlinear local optimization require the Jacobian of equality constraints (and +active inequalities) to be full row rank, and this package implements the +Dulmage-Mendelsohn partition, which can be used to determine if this Jacobian +is structurally rank-deficient. + +Who develops and maintains Incidence Analysis? +---------------------------------------------- + +This extension was developed by Robert Parker while a PhD student in +Professor Biegler's lab at Carnegie Mellon University, with guidance +from Bethany Nicholson and John Siirola at Sandia. + +How can I cite Incidence Analysis? +---------------------------------- + +If you use Incidence Analysis in your research, we would appreciate you citing +the following paper: + +.. code-block:: bibtex + + @article{parker2023dulmage, + title = {Applications of the {Dulmage-Mendelsohn} decomposition for debugging nonlinear optimization problems}, + journal = {Computers \& Chemical Engineering}, + volume = {178}, + pages = {108383}, + year = {2023}, + issn = {0098-1354}, + doi = {https://doi.org/10.1016/j.compchemeng.2023.108383}, + url = {https://www.sciencedirect.com/science/article/pii/S0098135423002533}, + author = {Robert B. Parker and Bethany L. Nicholson and John D. Siirola and Lorenz T. Biegler}, + } diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/scc_solver.rst b/doc/OnlineDocs/user_guide/contributed_packages/incidence/scc_solver.rst new file mode 100644 index 00000000000..35f494af1a1 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/incidence/scc_solver.rst @@ -0,0 +1,5 @@ +Block Triangular Decomposition Solver +===================================== + +.. automodule:: pyomo.contrib.incidence_analysis.scc_solver + :members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/triangularize.rst b/doc/OnlineDocs/user_guide/contributed_packages/incidence/triangularize.rst new file mode 100644 index 00000000000..a051086a859 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/incidence/triangularize.rst @@ -0,0 +1,5 @@ +Block Triangularization +======================= + +.. automodule:: pyomo.contrib.incidence_analysis.triangularize + :members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.bt.rst b/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.bt.rst new file mode 100644 index 00000000000..6710c0dbb50 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.bt.rst @@ -0,0 +1,107 @@ +Debugging a numeric singularity using block triangularization +============================================================= + +We start with some imports. To debug a *numeric* singularity, we will need +``PyomoNLP`` from :ref:`pynumero` to get the constraint Jacobian, +and will need NumPy to compute condition numbers. + +.. doctest:: + :skipif: not scipy_available or not asl_available or not networkx_available + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.pynumero.interfaces.pyomo_nlp import PyomoNLP + >>> from pyomo.contrib.incidence_analysis import IncidenceGraphInterface + >>> import numpy as np + +We now build the model we would like to debug. Compared to the model in +:ref:`incidence_tutorial_dm`, we have converted the sum equation to use a sum +over component flow rates rather than a sum over mass fractions. + +.. doctest:: + :skipif: not scipy_available or not asl_available or not networkx_available + + >>> m = pyo.ConcreteModel() + >>> m.components = pyo.Set(initialize=[1, 2, 3]) + >>> m.x = pyo.Var(m.components, initialize=1.0/3.0) + >>> m.flow_comp = pyo.Var(m.components, initialize=10.0) + >>> m.flow = pyo.Var(initialize=30.0) + >>> m.density = pyo.Var(initialize=1.0) + >>> # This equation is new! + >>> m.sum_flow_eqn = pyo.Constraint( + ... expr=sum(m.flow_comp[j] for j in m.components) == m.flow + ... ) + >>> m.holdup_eqn = pyo.Constraint(m.components, expr={ + ... j: m.x[j]*m.density - 1 == 0 for j in m.components + ... }) + >>> m.density_eqn = pyo.Constraint( + ... expr=1/m.density - sum(1/m.x[j] for j in m.components) == 0 + ... ) + >>> m.flow_eqn = pyo.Constraint(m.components, expr={ + ... j: m.x[j]*m.flow - m.flow_comp[j] == 0 for j in m.components + ... }) + +We now construct the incidence graph and check unmatched variables and +constraints to validate structural nonsingularity. + +.. doctest:: + :skipif: not scipy_available or not asl_available or not networkx_available + + >>> igraph = IncidenceGraphInterface(m, include_inequality=False) + >>> var_dmp, con_dmp = igraph.dulmage_mendelsohn() + >>> print(len(var_dmp.unmatched)) + 0 + >>> print(len(con_dmp.unmatched)) + 0 + +Our system is structurally nonsingular. Now we check whether we are numerically +nonsingular (well-conditioned) by checking the condition number. +Admittedly, deciding if a matrix is "singular" by looking at its condition +number is somewhat of an art. We might define "numerically singular" as having a +condition number greater than the inverse of machine precision (approximately +``1e16``), but poorly conditioned matrices can cause problems even if they don't +meet this definition. Here we use ``1e10`` as a somewhat arbitrary condition +number threshold to indicate a problem in our system. + +.. doctest:: + :skipif: not scipy_available or not asl_available or not networkx_available + + >>> # PyomoNLP requires exactly one objective function + >>> m._obj = pyo.Objective(expr=0.0) + >>> nlp = PyomoNLP(m) + >>> cond_threshold = 1e10 + >>> cond = np.linalg.cond(nlp.evaluate_jacobian_eq().toarray()) + >>> print(cond > cond_threshold) + True + +The system is poorly conditioned. Now we can check diagonal blocks of a block +triangularization to determine which blocks are causing the poor conditioning. + +.. code-block:: python + + >>> var_blocks, con_blocks = igraph.block_triangularize() + >>> for i, (vblock, cblock) in enumerate(zip(var_blocks, con_blocks)): + ... submatrix = nlp.extract_submatrix_jacobian(vblock, cblock) + ... cond = np.linalg.cond(submatrix.toarray()) + ... print(f"block {i}: {cond}") + ... if cond > cond_threshold: + ... for var in vblock: + ... print(f" {var.name}") + ... for con in cblock: + ... print(f" {con.name}") + block 0: 24.492504515710433 + block 1: 1.2480741394486336e+17 + flow + flow_comp[1] + flow_comp[2] + flow_comp[3] + sum_flow_eqn + flow_eqn[1] + flow_eqn[2] + flow_eqn[3] + +We see that the second block is causing the singularity, and that this block +contains the sum equation that we modified for this example. This suggests that +converting this equation to sum over flow rates rather than mass fractions just +converted a structural singularity to a numeric singularity, and didn't really +solve our problem. To see a fix that *does* resolve the singularity, see +:ref:`incidence_tutorial_dm`. diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.btsolve.rst b/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.btsolve.rst new file mode 100644 index 00000000000..1ff0b6c5afe --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.btsolve.rst @@ -0,0 +1,72 @@ +Solving a square system with a block triangular decomposition +============================================================= + +We start with imports. The key function from Incidence Analysis we will use is +``solve_strongly_connected_components``. + +.. doctest:: + :skipif: not networkx_available or not scipy_available or not asl_available + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.incidence_analysis import ( + ... solve_strongly_connected_components + ... ) + +Now we construct the model we would like to solve. This is a model with the +same structure as the "fixed model" in :ref:`incidence_tutorial_dm`. + +.. doctest:: + :skipif: not networkx_available or not scipy_available or not asl_available + + >>> m = pyo.ConcreteModel() + >>> m.components = pyo.Set(initialize=[1, 2, 3]) + >>> m.x = pyo.Var(m.components, initialize=1.0/3.0) + >>> m.flow_comp = pyo.Var(m.components, initialize=10.0) + >>> m.flow = pyo.Var(initialize=30.0) + >>> m.dens_bulk = pyo.Var(initialize=1.0) + >>> m.dens_skel = pyo.Var(initialize=1.0) + >>> m.porosity = pyo.Var(initialize=0.25) + >>> m.velocity = pyo.Param(initialize=1.0) + >>> m.holdup = pyo.Param( + ... m.components, initialize={j: 1.0+j/10.0 for j in m.components} + ... ) + >>> m.sum_eqn = pyo.Constraint( + ... expr=sum(m.x[j] for j in m.components) - 1 == 0 + ... ) + >>> m.holdup_eqn = pyo.Constraint(m.components, expr={ + ... j: m.x[j]*m.dens_bulk - m.holdup[j] == 0 for j in m.components + ... }) + >>> m.dens_skel_eqn = pyo.Constraint( + ... expr=1/m.dens_skel - sum(1e-3/m.x[j] for j in m.components) == 0 + ... ) + >>> m.dens_bulk_eqn = pyo.Constraint( + ... expr=m.dens_bulk == (1 - m.porosity)*m.dens_skel + ... ) + >>> m.flow_eqn = pyo.Constraint(m.components, expr={ + ... j: m.x[j]*m.flow - m.flow_comp[j] == 0 for j in m.components + ... }) + >>> m.flow_dens_eqn = pyo.Constraint( + ... expr=m.flow == m.velocity*m.dens_bulk + ... ) + +Solving via a block triangular decomposition is useful in cases where the full +model does not converge when considered simultaneously by a Newton solver. +In this case, we specify a solver to use for the diagonal blocks and call +``solve_strongly_connected_components``. + +.. doctest:: + :skipif: not networkx_available or not scipy_available or not asl_available + + >>> # Suppose a solve like this does not converge + >>> # pyo.SolverFactory("scipy.fsolve").solve(m) + + >>> # We solve via block-triangular decomposition + >>> solver = pyo.SolverFactory("scipy.fsolve") + >>> res_list = solve_strongly_connected_components(m, solver=solver) + +We can now display the variable values at the solution: + +.. code-block:: python + + for var in m.component_objects(pyo.Var): + var.pprint() diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.dm.rst b/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.dm.rst new file mode 100644 index 00000000000..c14861e9fc8 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.dm.rst @@ -0,0 +1,191 @@ +.. _incidence_tutorial_dm: + +Debugging a structural singularity with the Dulmage-Mendelsohn partition +======================================================================== + +We start with some imports and by creating a Pyomo model we would like +to debug. Usually the model is much larger and more complicated than this. +This particular system appeared when debugging a dynamic 1-D partial +differential-algebraic equation (PDAE) model representing a chemical looping +combustion reactor. + +.. doctest:: + :skipif: not scipy_available or not networkx_available or not asl_available + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.incidence_analysis import IncidenceGraphInterface + + >>> m = pyo.ConcreteModel() + >>> m.components = pyo.Set(initialize=[1, 2, 3]) + >>> m.x = pyo.Var(m.components, initialize=1.0/3.0) + >>> m.flow_comp = pyo.Var(m.components, initialize=10.0) + >>> m.flow = pyo.Var(initialize=30.0) + >>> m.density = pyo.Var(initialize=1.0) + >>> m.sum_eqn = pyo.Constraint( + ... expr=sum(m.x[j] for j in m.components) - 1 == 0 + ... ) + >>> m.holdup_eqn = pyo.Constraint(m.components, expr={ + ... j: m.x[j]*m.density - 1 == 0 for j in m.components + ... }) + >>> m.density_eqn = pyo.Constraint( + ... expr=1/m.density - sum(1/m.x[j] for j in m.components) == 0 + ... ) + >>> m.flow_eqn = pyo.Constraint(m.components, expr={ + ... j: m.x[j]*m.flow - m.flow_comp[j] == 0 for j in m.components + ... }) + +To check this model for structural singularity, we apply the Dulmage-Mendelsohn +partition. ``var_dm_partition`` and ``con_dm_partition`` are named tuples +with fields for each of the four subsets defined by the partition: +``unmatched``, ``overconstrained``, ``square``, and ``underconstrained``. + +.. doctest:: + :skipif: not scipy_available or not networkx_available or not asl_available + + >>> igraph = IncidenceGraphInterface(m) + >>> # Make sure we have a square system + >>> print(len(igraph.variables)) + 8 + >>> print(len(igraph.constraints)) + 8 + >>> var_dm_partition, con_dm_partition = igraph.dulmage_mendelsohn() + +If any variables or constraints are unmatched, the (Jacobian of the) model +is structurally singular. + +.. code-block:: python + + >>> # Note that the unmatched variables/constraints are not mathematically + >>> # unique and could change with implementation! + >>> for var in var_dm_partition.unmatched: + ... print(var.name) + flow_comp[1] + >>> for con in con_dm_partition.unmatched: + ... print(con.name) + density_eqn + +This model has one unmatched constraint and one unmatched variable, so it is +structurally singular. However, the unmatched variable and constraint are not +unique. For example, ``flow_comp[2]`` could have been unmatched instead of +``flow_comp[1]``. The exact variables and constraints that are unmatched depends +on both the order in which variables are identified in Pyomo expressions and +the implementation of the matching algorithm. For a given implementation, +however, these variables and constraints should be deterministic. + +Unique subsets of variables and constraints that are useful when debugging a +structural singularity are the underconstrained and overconstrained subsystems. +The variables in the underconstrained subsystem are contained in the +``unmatched`` and ``underconstrained`` fields of the ``var_dm_partition`` named tuple, +while the constraints are contained in the ``underconstrained`` field of the +``con_dm_partition`` named tuple. +The variables in the overconstrained subsystem are contained in the +``overconstrained`` field of the ``var_dm_partition`` named tuple, while the constraints +are contained in the ``overconstrained`` and ``unmatched`` fields of the +``con_dm_partition`` named tuple. + +We now construct the underconstrained and overconstrained subsystems: + +.. doctest:: + :skipif: not scipy_available or not networkx_available or not asl_available + + >>> uc_var = var_dm_partition.unmatched + var_dm_partition.underconstrained + >>> uc_con = con_dm_partition.underconstrained + >>> oc_var = var_dm_partition.overconstrained + >>> oc_con = con_dm_partition.overconstrained + con_dm_partition.unmatched + +And display the variables and constraints contained in each: + +.. code-block:: python + + >>> # Note that while these variables/constraints are uniquely determined, + >>> # their order is not! + + >>> # Overconstrained subsystem + >>> for var in oc_var: + >>> print(var.name) + x[1] + density + x[2] + x[3] + >>> for con in oc_con: + >>> print(con.name) + sum_eqn + holdup_eqn[1] + holdup_eqn[2] + holdup_eqn[3] + density_eqn + + >>> # Underconstrained subsystem + >>> for var in uc_var: + >>> print(var.name) + flow_comp[1] + flow + flow_comp[2] + flow_comp[3] + >>> for con in uc_con: + >>> print(con.name) + flow_eqn[1] + flow_eqn[2] + flow_eqn[3] + +At this point we must use our intuition about the system being modeled to +identify "what is causing" the singularity. Looking at the under and over- +constrained systems, it appears that we are missing an equation to calculate +``flow``, the total flow rate, and that ``density`` is over-specified as it +is computed by both the bulk density equation and one of the component density +equations. + +With this knowledge, we can eventually figure out (a) that we need an equation +to calculate ``flow`` from density and (b) that our "bulk density equation" +is actually a *skeletal* density equation. Admittedly, this is difficult to +figure out without the full context behind this particular system. + +The following code constructs a new version of the model and verifies that it +is structurally nonsingular: + +.. doctest:: + :skipif: not scipy_available or not networkx_available or not asl_available + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.incidence_analysis import IncidenceGraphInterface + + >>> m = pyo.ConcreteModel() + >>> m.components = pyo.Set(initialize=[1, 2, 3]) + >>> m.x = pyo.Var(m.components, initialize=1.0/3.0) + >>> m.flow_comp = pyo.Var(m.components, initialize=10.0) + >>> m.flow = pyo.Var(initialize=30.0) + >>> m.dens_bulk = pyo.Var(initialize=1.0) + >>> m.dens_skel = pyo.Var(initialize=1.0) + >>> m.porosity = pyo.Var(initialize=0.25) + >>> m.velocity = pyo.Param(initialize=1.0) + >>> m.sum_eqn = pyo.Constraint( + ... expr=sum(m.x[j] for j in m.components) - 1 == 0 + ... ) + >>> m.holdup_eqn = pyo.Constraint(m.components, expr={ + ... j: m.x[j]*m.dens_bulk - 1 == 0 for j in m.components + ... }) + >>> m.dens_skel_eqn = pyo.Constraint( + ... expr=1/m.dens_skel - sum(1/m.x[j] for j in m.components) == 0 + ... ) + >>> m.dens_bulk_eqn = pyo.Constraint( + ... expr=m.dens_bulk == (1 - m.porosity)*m.dens_skel + ... ) + >>> m.flow_eqn = pyo.Constraint(m.components, expr={ + ... j: m.x[j]*m.flow - m.flow_comp[j] == 0 for j in m.components + ... }) + >>> m.flow_dens_eqn = pyo.Constraint( + ... expr=m.flow == m.velocity*m.dens_bulk + ... ) + + >>> igraph = IncidenceGraphInterface(m, include_inequality=False) + >>> print(len(igraph.variables)) + 10 + >>> print(len(igraph.constraints)) + 10 + >>> var_dm_partition, con_dm_partition = igraph.dulmage_mendelsohn() + + >>> # There are now no unmatched variables or equations + >>> print(len(var_dm_partition.unmatched)) + 0 + >>> print(len(con_dm_partition.unmatched)) + 0 diff --git a/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.rst b/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.rst new file mode 100644 index 00000000000..4b22fc16c53 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/incidence/tutorial.rst @@ -0,0 +1,14 @@ +.. _incidence_tutorial: + +Incidence Analysis Tutorial +=========================== + +This tutorial walks through examples of the most common use cases for +Incidence Analysis: + +.. toctree:: + :maxdepth: 1 + + tutorial.dm.rst + tutorial.bt.rst + tutorial.btsolve.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/index.rst b/doc/OnlineDocs/user_guide/contributed_packages/index.rst new file mode 100644 index 00000000000..b1d9cbbad3b --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/index.rst @@ -0,0 +1,46 @@ +Third-Party Contributions +========================= + +Pyomo includes a variety of additional features and functionality +provided by third parties through the ``pyomo.contrib`` package. This +package includes both contributions included with the main Pyomo +distribution and wrappers for third-party packages that must be +installed separately. + +These packages are maintained by the original contributors and are +managed as *optional* Pyomo packages. + +Contributed packages distributed with Pyomo: + +.. toctree:: + :maxdepth: 1 + + community.rst + doe/doe.rst + gdpopt.rst + iis.rst + incidence/index.rst + latex_printer.rst + mindtpy.rst + mpc/index.rst + multistart.rst + preprocessing.rst + parmest/index.rst + pynumero/index.rst + pyros.rst + sensitivity_toolbox.rst + trustregion.rst + +Contributed Pyomo interfaces to other packages: + +.. toctree:: + :maxdepth: 1 + + mcpp.rst + satsolver.rst + + +Contributed packages distributed independently of Pyomo, but accessible +through ``pyomo.contrib``: + +* `pyomo.contrib.simplemodel `_ diff --git a/doc/OnlineDocs/user_guide/contributed_packages/latex_printer.rst b/doc/OnlineDocs/user_guide/contributed_packages/latex_printer.rst new file mode 100644 index 00000000000..ff3f628c0c8 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/latex_printer.rst @@ -0,0 +1,127 @@ +Latex Printing +============== + +Pyomo models can be printed to a LaTeX compatible format using the ``pyomo.contrib.latex_printer.latex_printer`` function: + +.. autofunction:: pyomo.contrib.latex_printer.latex_printer.latex_printer + +.. note:: + + If operating in a Jupyter Notebook, it may be helpful to use: + + ``from IPython.display import display, Math`` + + ``display(Math(latex_printer(m))`` + +Examples +-------- + +A Model ++++++++ + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.latex_printer import latex_printer + + >>> m = pyo.ConcreteModel(name = 'basicFormulation') + >>> m.x = pyo.Var() + >>> m.y = pyo.Var() + >>> m.z = pyo.Var() + >>> m.c = pyo.Param(initialize=1.0, mutable=True) + >>> m.objective = pyo.Objective( expr = m.x + m.y + m.z ) + >>> m.constraint_1 = pyo.Constraint(expr = m.x**2 + m.y**2.0 - m.z**2.0 <= m.c ) + + >>> pstr = latex_printer(m) + + +A Constraint +++++++++++++ + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.latex_printer import latex_printer + + >>> m = pyo.ConcreteModel(name = 'basicFormulation') + >>> m.x = pyo.Var() + >>> m.y = pyo.Var() + + >>> m.constraint_1 = pyo.Constraint(expr = m.x**2 + m.y**2 <= 1.0) + + >>> pstr = latex_printer(m.constraint_1) + +A Constraint with Set Summation ++++++++++++++++++++++++++++++++ + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.latex_printer import latex_printer + >>> m = pyo.ConcreteModel(name='basicFormulation') + >>> m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) + >>> m.v = pyo.Var(m.I) + + >>> def ruleMaker(m): return sum(m.v[i] for i in m.I) <= 0 + + >>> m.constraint = pyo.Constraint(rule=ruleMaker) + + >>> pstr = latex_printer(m.constraint) + +Using a ComponentMap to Specify Names ++++++++++++++++++++++++++++++++++++++ + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.latex_printer import latex_printer + >>> from pyomo.common.collections.component_map import ComponentMap + + >>> m = pyo.ConcreteModel(name='basicFormulation') + >>> m.I = pyo.Set(initialize=[1, 2, 3, 4, 5]) + >>> m.v = pyo.Var(m.I) + + >>> def ruleMaker(m): return sum(m.v[i] for i in m.I) <= 0 + + >>> m.constraint = pyo.Constraint(rule=ruleMaker) + + >>> lcm = ComponentMap() + >>> lcm[m.v] = 'x' + >>> lcm[m.I] = ['\\mathcal{A}',['j','k']] + + >>> pstr = latex_printer(m.constraint, latex_component_map=lcm) + + +An Expression ++++++++++++++ + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.latex_printer import latex_printer + + >>> m = pyo.ConcreteModel(name = 'basicFormulation') + >>> m.x = pyo.Var() + >>> m.y = pyo.Var() + + >>> m.expression_1 = pyo.Expression(expr = m.x**2 + m.y**2) + + >>> pstr = latex_printer(m.expression_1) + + +A Simple Expression ++++++++++++++++++++ + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.latex_printer import latex_printer + + >>> m = pyo.ConcreteModel(name = 'basicFormulation') + >>> m.x = pyo.Var() + >>> m.y = pyo.Var() + + >>> pstr = latex_printer(m.x + m.y) + + + diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mcpp.rst b/doc/OnlineDocs/user_guide/contributed_packages/mcpp.rst new file mode 100644 index 00000000000..18cea7f9b20 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/mcpp.rst @@ -0,0 +1,62 @@ +MC++ Interface +============== + +The Pyomo-MC++ interface allows for bounding of factorable functions using the MC++ library developed by +the OMEGA research group at Imperial College London. +Documentation for MC++ may be found on the `MC++ website`_. + +.. _MC++ website: https://github.com/omega-icl/mcpp + + +Default Installation +-------------------- +Pyomo now supports automated downloading and compilation of MC++. +To install MC++ and other third party compiled extensions, run: + +.. code:: + + pyomo download-extensions + pyomo build-extensions + +To get and install just MC++, run the following commands in the ``pyomo/contrib/mcpp`` directory: + +.. code:: + + python getMCPP.py + python build.py + +This should install MC++ to the pyomo plugins directory, by default located at ``$HOME/.pyomo/``. + + +Manual Installation +------------------- + +Support for MC++ has only been validated by Pyomo developers using Linux and OSX. +Installation instructions for the MC++ library may be found on the `MC++ website`_. + +We assume that you have installed MC++ into a directory of your choice. +We will denote this directory by ``$MCPP_PATH``. +For example, you should see that the file ``$MCPP_PATH/INSTALL`` exists. + +Navigate to the ``pyomo/contrib/mcpp`` directory in your pyomo installation. +This directory should contain a file named ``mcppInterface.cpp``. +You will need to compile this file using the following command: + +.. code:: + + g++ -I $MCPP_PATH/src/3rdparty/fadbad++ -I $MCPP_PATH/src/mc -I /usr/include/python3.7 -fPIC -O2 -c mcppInterface.cpp + +This links the MC++ required library FADBAD++, MC++ itself, and Python to compile the Pyomo-MC++ interface. +If successful, you will now have a file named ``mcppInterface.o`` in your working directory. +If you are not using Python 3.7, you will need to link to the appropriate Python version. +You now need to create a shared object file with the following command: + +.. code:: + + g++ -shared mcppInterface.o -o mcppInterface.so + +You may then test your installation by running the test file: + +.. code:: + + python test_mcpp.py diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mindtpy.rst b/doc/OnlineDocs/user_guide/contributed_packages/mindtpy.rst new file mode 100644 index 00000000000..a850a42c740 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/mindtpy.rst @@ -0,0 +1,319 @@ +MindtPy Solver +============== + +The Mixed-Integer Nonlinear Decomposition Toolbox in Pyomo (MindtPy) solver +allows users to solve Mixed-Integer Nonlinear Programs (MINLP) using decomposition algorithms. +These decomposition algorithms usually rely on the solution of Mixed-Integer Linear Programs +(MILP) and Nonlinear Programs (NLP). + +The following algorithms are currently available in MindtPy: + +- **Outer-Approximation (OA)** [`Duran & Grossmann, 1986`_] +- **LP/NLP based Branch-and-Bound (LP/NLP BB)** [`Quesada & Grossmann, 1992`_] +- **Extended Cutting Plane (ECP)** [`Westerlund & Petterson, 1995`_] +- **Global Outer-Approximation (GOA)** [`Kesavan & Allgor, 2004`_, `MC++`_] +- **Regularized Outer-Approximation (ROA)** [`Bernal & Peng, 2021`_, `Kronqvist & Bernal, 2018`_] +- **Feasibility Pump (FP)** [`Bernal & Vigerske, 2019`_, `Bonami & Cornuéjols, 2009`_] + +Usage and early implementation details for MindtPy can be found in the PSE 2018 paper Bernal et al., +(`ref `_, +`preprint `_). +This solver implementation has been developed by `David Bernal `_ +and `Zedong Peng `_ as part of research efforts at the `Bernal Research Group +`_ and the `Grossmann Research Group `_ +at Purdue University and Carnegie Mellon University. + +.. _Duran & Grossmann, 1986: https://dx.doi.org/10.1007/BF02592064 +.. _Westerlund & Petterson, 1995: http://dx.doi.org/10.1016/0098-1354(95)87027-X +.. _Kesavan & Allgor, 2004: https://link.springer.com/article/10.1007/s10107-004-0503-1 +.. _MC++: https://pyomo.readthedocs.io/en/stable/contributed_packages/mcpp.html +.. _Bernal & Peng, 2021: http://www.optimization-online.org/DB_HTML/2021/06/8452.html +.. _Kronqvist & Bernal, 2018: https://link.springer.com/article/10.1007%2Fs10107-018-1356-3 +.. _Bonami & Cornuéjols, 2009: https://link.springer.com/article/10.1007/s10107-008-0212-2 +.. _Bernal & Vigerske, 2019: https://www.tandfonline.com/doi/abs/10.1080/10556788.2019.1641498 +.. _Kronqvist et al., 2019: https://link.springer.com/article/10.1007/s11081-018-9411-8 + +MINLP Formulation +----------------- + +The general formulation of the mixed integer nonlinear programming (MINLP) models is as follows. + +.. math:: + :nowrap: + + \begin{equation} + \label{eq:MINLP} + \tag{MINLP} + \begin{aligned} + &\min_{\mathbf{x,y}} &&f(\mathbf{x,y})\\ + & \text{s.t.} \ &&g_j(\mathbf{x,y}) \leq 0 \quad \ \forall j=1,\dots l,\\ + & &&\mathbf{A}\mathbf{x} +\mathbf{B}\mathbf{y} \leq \mathbf{b}, \\ + & &&\mathbf{x}\in {\mathbb R}^n,\ \mathbf{y} \in {\mathbb Z}^m. + \end{aligned} + \end{equation} + +where + +- :math:`\mathbf{x}\in {\mathbb R}^n` are continuous variables, +- :math:`\mathbf{y} \in {\mathbb Z}^m` are discrete variables, +- :math:`f, g_1, \dots, g_l` are non-linear smooth functions, +- :math:`\mathbf{A}\mathbf{x} +\mathbf{B}\mathbf{y} \leq \mathbf{b}`` are linear constraints. + +Solve Convex MINLPs +------------------- + +Usage of MindtPy to solve a convex MINLP Pyomo model involves: + +.. code:: + + >>> SolverFactory('mindtpy').solve(model) + +An example which includes the modeling approach may be found below. + +.. doctest:: + + Required imports + >>> from pyomo.environ import * + + Create a simple model + >>> model = ConcreteModel() + + >>> model.x = Var(bounds=(1.0,10.0),initialize=5.0) + >>> model.y = Var(within=Binary) + + >>> model.c1 = Constraint(expr=(model.x-4.0)**2 - model.x <= 50.0*(1-model.y)) + >>> model.c2 = Constraint(expr=model.x*log(model.x)+5.0 <= 50.0*(model.y)) + + >>> model.objective = Objective(expr=model.x, sense=minimize) + + Solve the model using MindtPy + >>> SolverFactory('mindtpy').solve(model, mip_solver='glpk', nlp_solver='ipopt') # doctest: +SKIP + +The solution may then be displayed by using the commands + +.. code:: + + >>> model.objective.display() + >>> model.display() + >>> model.pprint() + +.. note:: + + When troubleshooting, it can often be helpful to turn on verbose + output using the ``tee`` flag. + +.. code:: + + >>> SolverFactory('mindtpy').solve(model, mip_solver='glpk', nlp_solver='ipopt', tee=True) + +MindtPy also supports setting options for mip solvers and nlp solvers. + +.. code:: + + >>> SolverFactory('mindtpy').solve(model, + strategy='OA', + time_limit=3600, + mip_solver='gams', + mip_solver_args=dict(solver='cplex', warmstart=True), + nlp_solver='ipopt', + tee=True) + +There are three initialization strategies in MindtPy: ``rNLP``, ``initial_binary``, ``max_binary``. In OA and GOA strategies, the default initialization strategy is ``rNLP``. In ECP strategy, the default initialization strategy is ``max_binary``. + +LP/NLP Based Branch-and-Bound +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +MindtPy also supports single-tree implementation of Outer-Approximation (OA) algorithm, which is known as LP/NLP based branch-and-bound algorithm originally described in [`Quesada & Grossmann, 1992`_]. +The LP/NLP based branch-and-bound algorithm in MindtPy is implemented based on the LazyConstraintCallback function in commercial solvers. + +.. _Quesada & Grossmann, 1992: https://www.sciencedirect.com/science/article/abs/pii/0098135492800288 + +.. note:: + + In Pyomo, `persistent solvers`_ are necessary to set or register callback functions. The single tree implementation currently only works with CPLEX and GUROBI, more exactly ``cplex_persistent`` and ``gurobi_persistent``. To use the `LazyConstraintCallback`_ function of CPLEX from Pyomo, the `CPLEX Python API`_ is required. This means both IBM ILOG CPLEX Optimization Studio and the CPLEX-Python modules should be installed on your computer. To use the `cbLazy`_ function of GUROBI from pyomo, `gurobipy`_ is required. + +.. _`persistent solvers`: https://pyomo.readthedocs.io/en/stable/advanced_topics/persistent_solvers.html?highlight=persistent +.. _CPLEX Python API: https://www.ibm.com/docs/en/icos/20.1.0?topic=cplex-setting-up-python-api +.. _gurobipy: https://www.gurobi.com/documentation/9.1/quickstart_mac/cs_grbpy_the_gurobi_python.html +.. _LazyConstraintCallback: https://www.ibm.com/docs/en/icos/20.1.0?topic=classes-cplexcallbackslazyconstraintcallback +.. _cbLazy: https://www.gurobi.com/documentation/9.1/refman/py_model_cblazy.html + +A usage example for LP/NLP based branch-and-bound algorithm is as follows: + +.. code:: + + >>> pyo.SolverFactory('mindtpy').solve(model, + ... strategy='OA', + ... mip_solver='cplex_persistent', # or 'gurobi_persistent' + ... nlp_solver='ipopt', + ... single_tree=True) + >>> model.objective.display() + + +Regularized Outer-Approximation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As a new implementation in MindtPy, we provide a flexible regularization technique implementation. In this technique, an extra mixed-integer problem is solved in each decomposition iteration or incumbent solution of the single-tree solution methods. The extra mixed-integer program is constructed to provide a point where the NLP problem is solved closer to the feasible region described by the non-linear constraint. This approach has been proposed in [`Kronqvist et al., 2020`_], and it has shown to be efficient for highly non-linear convex MINLP problems. In [`Kronqvist et al., 2020`_], two different regularization approaches are proposed, using a squared Euclidean norm which was proved to make the procedure equivalent to adding a trust-region constraint to Outer-approximation, and a second-order approximation of the Lagrangian of the problem, which showed better performance. We implement these methods, using PyomoNLP as the interface to compute the second-order approximation of the Lagrangian, and extend them to consider linear norm objectives and first-order approximations of the Lagrangian. Finally, we implemented an approximated second-order expansion of the Lagrangian, drawing inspiration from the Sequential Quadratic Programming (SQP) literature. The details of this implementation are included in [`Bernal et al., 2021`_]. + +.. _Kronqvist et al., 2020: https://link.springer.com/article/10.1007/s10107-018-1356-3 +.. _Bernal et al., 2021: http://www.optimization-online.org/DB_HTML/2021/06/8452.html + +A usage example for regularized OA is as follows: + +.. code:: + + >>> pyo.SolverFactory('mindtpy').solve(model, + ... strategy='OA', + ... mip_solver='cplex', + ... nlp_solver='ipopt', + ... add_regularization='level_L1' + ... # alternative regularizations + ... # 'level_L1', 'level_L2', 'level_L_infinity', + ... # 'grad_lag', 'hess_lag', 'hess_only_lag', 'sqp_lag' + ... ) + >>> model.objective.display() + + +Solution Pool Implementation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +MindtPy supports solution pool of the MILP solver, CPLEX and GUROBI. With the help of the solution, MindtPy can explore several integer combinations in one iteration. + +A usage example for OA with solution pool is as follows: + +.. code:: + + >>> pyo.SolverFactory('mindtpy').solve(model, + ... strategy='OA', + ... mip_solver='cplex_persistent', + ... nlp_solver='ipopt', + ... solution_pool=True, + ... num_solution_iteration=10, # default=5 + ... tee=True + ... ) + >>> model.objective.display() + +Feasibility Pump +^^^^^^^^^^^^^^^^ + +For some MINLP problems, the Outer Approximation method might have difficulty in finding a feasible solution. MindtPy provides the Feasibility Pump implementation to find feasible solutions for convex MINLPs quickly. The main idea of the Feasibility Pump is to decompose the original mixed-integer problem into two parts: integer feasibility and constraint feasibility. For convex MINLPs, a MIP is solved to obtain a solution, which satisfies the integrality constraints on `y`, but may violate some of the nonlinear constraints; next, by solving an NLP, a solution is computed that satisfies the nonlinear constraints but might again violate the integrality constraints on `y`. By minimizing the distance between these two types of solutions iteratively, a constraint and integer feasible solution can be expected. In MindtPy, the Feasibility Pump can be used both as an initialization strategy and a decomposition strategy. For details of this implementation are included in [`Bernal et al., 2017`_]. + +.. _Bernal et al., 2017: http://www.optimization-online.org/DB_HTML/2017/08/6171.html + +A usage example for Feasibility Pump as the initialization strategy is as follows: + +.. code:: + + >>> pyo.SolverFactory('mindtpy').solve(model, + ... strategy='OA', + ... init_strategy='FP', + ... mip_solver='cplex', + ... nlp_solver='ipopt', + ... tee=True + ... ) + >>> model.objective.display() + +A usage example for Feasibility Pump as the decomposition strategy is as follows: + +.. code:: + + >>> pyo.SolverFactory('mindtpy').solve(model, + ... strategy='FP', + ... mip_solver='cplex', + ... nlp_solver='ipopt', + ... tee=True + ... ) + >>> model.objective.display() + + + +Solve Nonconvex MINLPs +---------------------- + + +Equality Relaxation +^^^^^^^^^^^^^^^^^^^ + +Under certain assumptions concerning the convexity of the nonlinear functions, an equality constraint can be relaxed to be an inequality constraint. This property can be used in the MIP master problem to accumulate linear approximations(OA cuts). The sense of the equivalent inequality constraint is based on the sign of the dual values of the equality constraint. Therefore, the sense of the OA cuts for equality constraint should be determined according to both the objective sense and the sign of the dual values. In MindtPy, the dual value of the equality constraint is calculated as follows. + ++--------------------+-----------------------+-------------------------+ +| constraint | status at :math:`x_1` | dual values | ++====================+=======================+=========================+ +| :math:`g(x) \le b` | :math:`g(x_1) \le b` | 0 | ++--------------------+-----------------------+-------------------------+ +| :math:`g(x) \le b` | :math:`g(x_1) > b` | :math:`g(x1) - b` | ++--------------------+-----------------------+-------------------------+ +| :math:`g(x) \ge b` | :math:`g(x_1) \ge b` | 0 | ++--------------------+-----------------------+-------------------------+ +| :math:`g(x) \ge b` | :math:`g(x_1) < b` | :math:`b - g(x1)` | ++--------------------+-----------------------+-------------------------+ + +Augmented Penalty +^^^^^^^^^^^^^^^^^ + +Augmented Penalty refers to the introduction of (non-negative) slack variables on the right hand sides of the just described inequality constraints and the modification of the objective function when assumptions concerning convexity do not hold. (From DICOPT) + + +Global Outer-Approximation +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Apart from the decomposition methods for convex MINLP problems [`Kronqvist et al., 2019`_], MindtPy provides an implementation of Global Outer Approximation (GOA) as described in [`Kesavan & Allgor, 2004`_], to provide optimality guaranteed for nonconvex MINLP problems. Here, the validity of the Mixed-integer Linear Programming relaxation of the original problem is guaranteed via the usage of Generalized McCormick envelopes, computed using the package `MC++`_. The NLP subproblems, in this case, need to be solved to global optimality, which can be achieved through global NLP solvers such as `BARON`_ or `SCIP`_. + +.. _BARON: https://minlp.com/baron-solver +.. _SCIP: https://www.scipopt.org/ + + +Convergence +""""""""""" + +MindtPy provides two ways to guarantee the finite convergence of the algorithm. + +- **No-good cuts**. No-good cuts(integer cuts) are added to the MILP master problem in each iteration. +- **Tabu list**. Tabu list is only supported if the ``mip_solver`` is ``cplex_persistent`` (``gurobi_persistent`` pending). In each iteration, the explored integer combinations will be added to the `tabu_list`. When solving the next MILP problem, the MIP solver will reject the previously explored solutions in the branch and bound process through IncumbentCallback. + + +Bound Calculation +""""""""""""""""" + +Since no-good cuts or tabu list is applied in the Global Outer-Approximation (GOA) method, the MILP master problem cannot provide a valid bound for the original problem. After the GOA method has converged, MindtPy will remove the no-good cuts or the tabu integer combinations added when and after the optimal solution has been found. Solving this problem will give us a valid bound for the original problem. + + +The GOA method also has a single-tree implementation with ``cplex_persistent`` and ``gurobi_persistent``. Notice that this method is more computationally expensive than the other strategies implemented for convex MINLP like OA and ECP, which can be used as heuristics for nonconvex MINLP problems. + +A usage example for GOA is as follows: + +.. code:: + + >>> pyo.SolverFactory('mindtpy').solve(model, + ... strategy='GOA', + ... mip_solver='cplex', + ... nlp_solver='baron') + >>> model.objective.display() + + + +MindtPy Implementation and Optional Arguments +--------------------------------------------- + +.. warning:: + + MindtPy optional arguments should be considered beta code and are + subject to change. + +.. autoclass:: pyomo.contrib.mindtpy.MindtPy.MindtPySolver + :members: + +Get Help +-------- + +Ways to get help: https://github.com/Pyomo/pyomo#getting-help + +Report a Bug +------------ + +If you find a bug in MindtPy, we will be grateful if you could + +- submit an `issue`_ in Pyomo repository +- directly contact David Bernal and Zedong Peng . + +.. _issue: https://github.com/Pyomo/pyomo/issues diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/api.rst b/doc/OnlineDocs/user_guide/contributed_packages/mpc/api.rst new file mode 100644 index 00000000000..2752fea8af6 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/mpc/api.rst @@ -0,0 +1,10 @@ +.. _mpc_api: + +API Reference +============= + +.. toctree:: + data.rst + conversion.rst + interface.rst + modeling.rst diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/conversion.rst b/doc/OnlineDocs/user_guide/contributed_packages/mpc/conversion.rst new file mode 100644 index 00000000000..9d9406edb75 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/mpc/conversion.rst @@ -0,0 +1,5 @@ +Data Conversion +=============== + +.. automodule:: pyomo.contrib.mpc.data.convert + :members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/data.rst b/doc/OnlineDocs/user_guide/contributed_packages/mpc/data.rst new file mode 100644 index 00000000000..73cb6543b1e --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/mpc/data.rst @@ -0,0 +1,17 @@ +Data Structures +=============== + +.. automodule:: pyomo.contrib.mpc.data.get_cuid + :members: + +.. automodule:: pyomo.contrib.mpc.data.dynamic_data_base + :members: + +.. automodule:: pyomo.contrib.mpc.data.scalar_data + :members: + +.. automodule:: pyomo.contrib.mpc.data.series_data + :members: + +.. automodule:: pyomo.contrib.mpc.data.interval_data + :members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/examples.rst b/doc/OnlineDocs/user_guide/contributed_packages/mpc/examples.rst new file mode 100644 index 00000000000..95204192358 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/mpc/examples.rst @@ -0,0 +1,6 @@ +Examples +======== + +Please see ``pyomo/contrib/mpc/examples/cstr/run_openloop.py`` and +``pyomo/contrib/mpc/examples/cstr/run_mpc.py`` for examples of some simple +use cases. diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/faq.rst b/doc/OnlineDocs/user_guide/contributed_packages/mpc/faq.rst new file mode 100644 index 00000000000..e42e7184696 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/mpc/faq.rst @@ -0,0 +1,16 @@ +Frequently asked questions +========================== + +#. Why not use Pandas DataFrames? + +Pandas DataFrames are a natural data structure for storing "columns" of +time series data. These columns, or individual time series, could each represent +the data for a single variable. This is very similar to the TimeSeriesData +class introduced in this package. +The reason a new data structure is introduced is primarily that a DataFrame +does not provide any utility for converting labels into a consistent format, +as TimeSeriesData does by accepting variables, strings, slices, etc. +as keys and converting them into the form of a time-indexed ComponentUID. +Also, DataFrames do not have convenient analogs for scalar data and +time interval data, which this package provides as the ScalarData +and IntervalData classes with very similar APIs to TimeSeriesData. diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/index.rst b/doc/OnlineDocs/user_guide/contributed_packages/mpc/index.rst new file mode 100644 index 00000000000..e512d1a6ef5 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/mpc/index.rst @@ -0,0 +1,32 @@ +MPC +=== + +Pyomo MPC contains data structures and utilities for dynamic optimization +and rolling horizon applications, e.g. model predictive control. + +.. toctree:: + :maxdepth: 1 + + overview.rst + examples.rst + faq.rst + api.rst + +Citation +-------- + +If you use Pyomo MPC in your research, please cite the following paper: + +.. code-block:: bibtex + + @article{parker2023mpc, + title = {Model predictive control simulations with block-hierarchical differential-algebraic process models}, + journal = {Journal of Process Control}, + volume = {132}, + pages = {103113}, + year = {2023}, + issn = {0959-1524}, + doi = {https://doi.org/10.1016/j.jprocont.2023.103113}, + url = {https://www.sciencedirect.com/science/article/pii/S0959152423002007}, + author = {Robert B. Parker and Bethany L. Nicholson and John D. Siirola and Lorenz T. Biegler}, + } diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/interface.rst b/doc/OnlineDocs/user_guide/contributed_packages/mpc/interface.rst new file mode 100644 index 00000000000..eb5bac548fd --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/mpc/interface.rst @@ -0,0 +1,8 @@ +Interfaces +========== + +.. automodule:: pyomo.contrib.mpc.interfaces.model_interface + :members: + +.. automodule:: pyomo.contrib.mpc.interfaces.var_linker + :members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/modeling.rst b/doc/OnlineDocs/user_guide/contributed_packages/mpc/modeling.rst new file mode 100644 index 00000000000..cbae03161b1 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/mpc/modeling.rst @@ -0,0 +1,11 @@ +Modeling Components +=================== + +.. automodule:: pyomo.contrib.mpc.modeling.constraints + :members: + +.. automodule:: pyomo.contrib.mpc.modeling.cost_expressions + :members: + +.. automodule:: pyomo.contrib.mpc.modeling.terminal + :members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/mpc/overview.rst b/doc/OnlineDocs/user_guide/contributed_packages/mpc/overview.rst new file mode 100644 index 00000000000..f3bc7504b59 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/mpc/overview.rst @@ -0,0 +1,210 @@ +Overview +======== + +What does this package contain? +------------------------------- + +#. Data structures for values and time series data associated with time-indexed variables (or parameters, or named expressions). Examples are setpoint values associated with a subset of state variables or time series data from a simulation + +#. Utilities for loading and extracting this data into and from variables in a model + +#. Utilities for constructing components from this data (expressions, constraints, and objectives) that are useful for dynamic optimization + +What is the goal of this package? +--------------------------------- + +This package was written to help developers of Pyomo-based dynamic optimization +case studies, especially rolling horizon dynamic optimization case studies, +write scripts that are small, legible, and maintainable. +It does this by providing utilities for mundane data-management and model +construction tasks, allowing the developer to focus on their application. + +Why is this package useful? +--------------------------- + +First, it is not normally easy to extract "flattened" time series data, +in which all indexing structure other than time-indexing has been +flattened to yield a set of one-dimensional arrays, from a Pyomo model. +This is an extremely convenient data structure to have for plotting, +analysis, initialization, and manipulation of dynamic models. +If all variables are indexed by time and only time, this data is relatively +easy to obtain. +The first issue comes up when dealing with components that are indexed by +time in addition to some other set(s). For example: + +.. doctest:: + + >>> import pyomo.environ as pyo + + >>> m = pyo.ConcreteModel() + >>> m.time = pyo.Set(initialize=[0, 1, 2]) + >>> m.comp = pyo.Set(initialize=["A", "B"]) + >>> m.var = pyo.Var(m.time, m.comp, initialize=1.0) + + >>> t0 = m.time.first() + >>> data = { + ... m.var[t0, j].name: [m.var[i, j].value for i in m.time] + ... for j in m.comp + ... } + >>> data + {'var[0,A]': [1.0, 1.0, 1.0], 'var[0,B]': [1.0, 1.0, 1.0]} + +To generate data in this form, we need to (a) know that our variable is indexed +by time and ``m.comp`` and (b) arbitrarily select a time index ``t0`` to +generate a unique key for each time series. +This gets more difficult when blocks and time-indexed blocks are used as well. +The first difficulty can be alleviated using +``flatten_dae_components`` from ``pyomo.dae.flatten``: + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.dae.flatten import flatten_dae_components + + >>> m = pyo.ConcreteModel() + >>> m.time = pyo.Set(initialize=[0, 1, 2]) + >>> m.comp = pyo.Set(initialize=["A", "B"]) + >>> m.var = pyo.Var(m.time, m.comp, initialize=1.0) + + >>> t0 = m.time.first() + >>> scalar_vars, dae_vars = flatten_dae_components(m, m.time, pyo.Var) + >>> data = {var[t0].name: list(var[:].value) for var in dae_vars} + >>> data + {'var[0,A]': [1.0, 1.0, 1.0], 'var[0,B]': [1.0, 1.0, 1.0]} + +Addressing the arbitrary ``t0`` index requires us to ask what key we +would like to use to identify each time series in our data structure. +The key should uniquely correspond to a component, or "sub-component" +that is indexed only by time. A slice, e.g. ``m.var[:, "A"]`` seems +natural. However, Pyomo provides a better data structure that can +be constructed from a component, slice, or string, called +``ComponentUID``. Being constructable from a string is important as +we may want to store or serialize this data in a form that is agnostic +of any particular ``ConcreteModel`` object. +We can now generate our data structure as: + +.. doctest:: + + >>> data = { + ... pyo.ComponentUID(var.referent): list(var[:].value) + ... for var in dae_vars + ... } + >>> data + {var[*,A]: [1.0, 1.0, 1.0], var[*,B]: [1.0, 1.0, 1.0]} + +This is the structure of the underlying dictionary in the ``TimeSeriesData`` +class provided by this package. We can generate this data using this package +as: + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.mpc import DynamicModelInterface + + >>> m = pyo.ConcreteModel() + >>> m.time = pyo.Set(initialize=[0, 1, 2]) + >>> m.comp = pyo.Set(initialize=["A", "B"]) + >>> m.var = pyo.Var(m.time, m.comp, initialize=1.0) + + >>> # Construct a helper class for interfacing model with data + >>> helper = DynamicModelInterface(m, m.time) + + >>> # Generates a TimeSeriesData object + >>> series_data = helper.get_data_at_time() + + >>> # Get the underlying dictionary + >>> data = series_data.get_data() + >>> data + {var[*,A]: [1.0, 1.0, 1.0], var[*,B]: [1.0, 1.0, 1.0]} + +The first value proposition of this package is that ``DynamicModelInterface`` +and ``TimeSeriesData`` provide wrappers to ease loading and extraction of data +via ``flatten_dae_components`` and ``ComponentUID``. + +The second difficulty addressed by this package is that of extracting and +loading data between (potentially) different models. +For instance, in model predictive control, we often want to extract data from +a particular time point in a plant model and load it into a controller model +as initial conditions. This can be done as follows: + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.mpc import DynamicModelInterface + + >>> m1 = pyo.ConcreteModel() + >>> m1.time = pyo.Set(initialize=[0, 1, 2]) + >>> m1.comp = pyo.Set(initialize=["A", "B"]) + >>> m1.var = pyo.Var(m1.time, m1.comp, initialize=1.0) + + >>> m2 = pyo.ConcreteModel() + >>> m2.time = pyo.Set(initialize=[0, 1, 2]) + >>> m2.comp = pyo.Set(initialize=["A", "B"]) + >>> m2.var = pyo.Var(m2.time, m2.comp, initialize=2.0) + + >>> # Construct helper objects + >>> m1_helper = DynamicModelInterface(m1, m1.time) + >>> m2_helper = DynamicModelInterface(m2, m2.time) + + >>> # Extract data from final time point of m2 + >>> tf = m2.time.last() + >>> tf_data = m2_helper.get_data_at_time(tf) + + >>> # Load data into initial time point of m1 + >>> t0 = m1.time.first() + >>> m1_helper.load_data(tf_data, time_points=t0) + + >>> # Get TimeSeriesData object + >>> series_data = m1_helper.get_data_at_time() + >>> # Get underlying dictionary + >>> series_data.get_data() + {var[*,A]: [2.0, 1.0, 1.0], var[*,B]: [2.0, 1.0, 1.0]} + +.. note:: + + Here we rely on the fact that our variable has the same name in + both models. + +Finally, this package provides methods for constructing components like +tracking cost expressions and piecewise-constant constraints from the +provided data structures. For example, the following code constructs +a tracking cost expression. + +.. doctest:: + + >>> import pyomo.environ as pyo + >>> from pyomo.contrib.mpc import DynamicModelInterface + + >>> m = pyo.ConcreteModel() + >>> m.time = pyo.Set(initialize=[0, 1, 2]) + >>> m.comp = pyo.Set(initialize=["A", "B"]) + >>> m.var = pyo.Var(m.time, m.comp, initialize=1.0) + + >>> # Construct helper object + >>> helper = DynamicModelInterface(m, m.time) + + >>> # Construct data structure for setpoints + >>> setpoint = {m.var[:, "A"]: 0.5, m.var[:, "B"]: 2.0} + >>> var_set, tr_cost = helper.get_penalty_from_target(setpoint) + >>> m.setpoint_idx = var_set + >>> m.tracking_cost = tr_cost + >>> m.tracking_cost.pprint() + tracking_cost : Size=6, Index=setpoint_idx*time + Key : Expression + (0, 0) : (var[0,A] - 0.5)**2 + (0, 1) : (var[1,A] - 0.5)**2 + (0, 2) : (var[2,A] - 0.5)**2 + (1, 0) : (var[0,B] - 2.0)**2 + (1, 1) : (var[1,B] - 2.0)**2 + (1, 2) : (var[2,B] - 2.0)**2 + + +These methods will hopefully allow developers to declutter dynamic optimization +scripts and pay more attention to the application of the optimization problem +rather than the setup of the optimization problem. + +Who develops and maintains this package? +---------------------------------------- + +This package was developed by Robert Parker while a PhD student in Larry +Biegler's group at CMU, with guidance from Bethany Nicholson and John Siirola. diff --git a/doc/OnlineDocs/user_guide/contributed_packages/multistart.rst b/doc/OnlineDocs/user_guide/contributed_packages/multistart.rst new file mode 100644 index 00000000000..069d770aa91 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/multistart.rst @@ -0,0 +1,34 @@ +Multistart Solver +================== + +The multistart solver is used in cases where the objective function is known +to be non-convex but the global optimum is still desired. It works by running a non-linear +solver of your choice multiple times at different starting points, and +returns the best of the solutions. + + +Using Multistart Solver +----------------------- +To use the multistart solver, define your Pyomo model as usual: + +.. doctest:: + + Required import + >>> from pyomo.environ import * + + Create a simple model + >>> m = ConcreteModel() + >>> m.x = Var() + >>> m.y = Var() + >>> m.obj = Objective(expr=m.x**2 + m.y**2) + >>> m.c = Constraint(expr=m.y >= -2*m.x + 5) + + Invoke the multistart solver + >>> SolverFactory('multistart').solve(m) # doctest: +SKIP + + +Multistart wrapper implementation and optional arguments +-------------------------------------------------------- + +.. autoclass:: pyomo.contrib.multistart.multi.MultiStart + :members: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/api.rst b/doc/OnlineDocs/user_guide/contributed_packages/parmest/api.rst new file mode 100644 index 00000000000..4d6896a8582 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/parmest/api.rst @@ -0,0 +1,25 @@ +.. _apisection: + +API +============ + +parmest +--------- +.. automodule:: pyomo.contrib.parmest.parmest + :members: + :undoc-members: + :show-inheritance: + +scenariocreator +------------------ +.. automodule:: pyomo.contrib.parmest.scenariocreator + :members: + :undoc-members: + :show-inheritance: + +graphics +--------- +.. automodule:: pyomo.contrib.parmest.graphics + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/boxplot.png b/doc/OnlineDocs/user_guide/contributed_packages/parmest/boxplot.png new file mode 100644 index 0000000000000000000000000000000000000000..25bb4da764a429b33aadb1bd53bdeddab10ddc2e GIT binary patch literal 19354 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 import pandas as pd + >>> import pyomo.contrib.parmest.parmest as parmest + >>> from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import RooneyBieglerExperiment + + >>> # Generate data + >>> data = pd.DataFrame(data=[[1,8.3],[2,10.3],[3,19.0], + ... [4,16.0],[5,15.6],[7,19.8]], + ... columns=['hour', 'y']) + + >>> # Create an experiment list + >>> exp_list = [] + >>> for i in range(data.shape[0]): + ... exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) + + >>> # Define objective + >>> def SSE(model): + ... expr = (model.experiment_outputs[model.y] + ... - model.response_function[model.experiment_outputs[model.hour]] + ... ) ** 2 + ... return expr + + >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=None) + >>> obj, theta, var_values = pest.theta_est(return_values=['response_function']) + >>> #print(var_values) diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/driver.rst b/doc/OnlineDocs/user_guide/contributed_packages/parmest/driver.rst new file mode 100644 index 00000000000..5881d2748f9 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/parmest/driver.rst @@ -0,0 +1,165 @@ +.. _driversection: + +Parameter Estimation +================================== + +Parameter Estimation using parmest requires a Pyomo model, experimental +data which defines multiple scenarios, and parameters +(thetas) to estimate. parmest uses Pyomo [PyomoBookII]_ and (optionally) +mpi-sppy [mpisppy]_ to solve a +two-stage stochastic programming problem, where the experimental data is +used to create a scenario tree. The objective function needs to be +written with the Pyomo Expression for first stage cost +(named "FirstStageCost") set to zero and the Pyomo Expression for second +stage cost (named "SecondStageCost") defined as the deviation between +the model and the observations (typically defined as the sum of squared +deviation between model values and observed values). + +If the Pyomo model is not formatted as a two-stage stochastic +programming problem in this format, the user can supply a custom +function to use as the second stage cost and the Pyomo model will be +modified within parmest to match the required specifications. +The stochastic programming callback function is also defined within parmest. The callback +function returns a populated and initialized model for each scenario. + +To use parmest, the user creates a :class:`~pyomo.contrib.parmest.parmest.Estimator` object +which includes the following methods: + +.. autosummary:: + :nosignatures: + + ~pyomo.contrib.parmest.parmest.Estimator.theta_est + ~pyomo.contrib.parmest.parmest.Estimator.theta_est_bootstrap + ~pyomo.contrib.parmest.parmest.Estimator.theta_est_leaveNout + ~pyomo.contrib.parmest.parmest.Estimator.objective_at_theta + ~pyomo.contrib.parmest.parmest.Estimator.confidence_region_test + ~pyomo.contrib.parmest.parmest.Estimator.likelihood_ratio_test + ~pyomo.contrib.parmest.parmest.Estimator.leaveNout_bootstrap_test + +Additional functions are available in parmest to plot +results and fit distributions to theta values. + +.. autosummary:: + :nosignatures: + + ~pyomo.contrib.parmest.graphics.pairwise_plot + ~pyomo.contrib.parmest.graphics.grouped_boxplot + ~pyomo.contrib.parmest.graphics.grouped_violinplot + ~pyomo.contrib.parmest.graphics.fit_rect_dist + ~pyomo.contrib.parmest.graphics.fit_mvn_dist + ~pyomo.contrib.parmest.graphics.fit_kde_dist + +A :class:`~pyomo.contrib.parmest.parmest.Estimator` object can be +created using the following code. A description of each argument is +listed below. Examples are provided in the :ref:`examplesection` +Section. + +.. testsetup:: * + :skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available + + # Data + import pandas as pd + data = pd.DataFrame( + data=[[1, 8.3], [2, 10.3], [3, 19.0], + [4, 16.0], [5, 15.6], [7, 19.8]], + columns=['hour', 'y'], + ) + + # Sum of squared error function + def SSE(model): + expr = ( + model.experiment_outputs[model.y] + - model.response_function[model.experiment_outputs[model.hour]] + ) ** 2 + return expr + + # Create an experiment list + from pyomo.contrib.parmest.examples.rooney_biegler.rooney_biegler import RooneyBieglerExperiment + exp_list = [] + for i in range(data.shape[0]): + exp_list.append(RooneyBieglerExperiment(data.loc[i, :])) + +.. doctest:: + :skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available + + >>> import pyomo.contrib.parmest.parmest as parmest + >>> pest = parmest.Estimator(exp_list, obj_function=SSE) + +Optionally, solver options can be supplied, e.g., + +.. doctest:: + :skipif: not __import__('pyomo.contrib.parmest.parmest').contrib.parmest.parmest.parmest_available + + >>> solver_options = {"max_iter": 6000} + >>> pest = parmest.Estimator(exp_list, obj_function=SSE, solver_options=solver_options) + + +List of experiment objects +-------------------------- + +The first argument is a list of experiment objects which is used to +create one labeled model for each expeirment. +The template :class:`~pyomo.contrib.parmest.experiment.Experiment` +can be used to generate a list of experiment objects. + +A labeled Pyomo model ``m`` has the following additional suffixes (Pyomo `Suffix`): + +* ``m.experiment_outputs`` which defines experiment output (Pyomo `Param`, `Var`, or `Expression`) + and their associated data values (float, int). +* ``m.unknown_parameters`` which defines the mutable parameters or variables (Pyomo `Param` or `Var`) + to estimate along with their component unique identifier (Pyomo `ComponentUID`). + Within parmest, any parameters that are to be estimated are converted to unfixed variables. + Variables that are to be estimated are also unfixed. + +The experiment class has one required method: + +* :class:`~pyomo.contrib.parmest.experiment.Experiment.get_labeled_model` which returns the labeled Pyomo model. + Note that the model does not have to be specifically written as a + two-stage stochastic programming problem for parmest. + That is, parmest can modify the + objective, see :ref:`ObjFunction` below. + +Parmest comes with several :ref:`examplesection` that illustrates how to set up the list of experiment objects. +The examples commonly include additional :class:`~pyomo.contrib.parmest.experiment.Experiment` class methods to +create the model, finalize the model, and label the model. The user can customize methods to suit their needs. + +.. _ObjFunction: + +Objective function +------------------ + +The second argument is an optional argument which defines the +optimization objective function to use in parameter estimation. + +If no objective function is specified, the Pyomo model is used "as is" and +should be defined with "FirstStageCost" and "SecondStageCost" +expressions that are used to build an objective for the two-stage +stochastic programming problem. + +If the Pyomo model is not written as a two-stage stochastic programming problem in +this format, and/or if the user wants to use an objective that is +different than the original model, a custom objective function can be +defined for parameter estimation. The objective function has a single argument, +which is the model from a single experiment. +The objective function returns a Pyomo +expression which is used to define "SecondStageCost". The objective +function can be used to customize data points and weights that are used +in parameter estimation. + +Parmest includes one built in objective function to compute the sum of squared errors ("SSE") between the +``m.experiment_outputs`` model values and data values. + +Suggested initialization procedure for parameter estimation problems +-------------------------------------------------------------------- + +To check the quality of initial guess values provided for the fitted parameters, we suggest solving a +square instance of the problem prior to solving the parameter estimation problem using the following steps: + +1. Create :class:`~pyomo.contrib.parmest.parmest.Estimator` object. To initialize the parameter +estimation solve from the square problem solution, set optional argument ``solver_options = {bound_push: 1e-8}``. + +2. Call :class:`~pyomo.contrib.parmest.parmest.Estimator.objective_at_theta` with optional +argument ``(initialize_parmest_model=True)``. Different initial guess values for the fitted +parameters can be provided using optional argument `theta_values` (**Pandas Dataframe**) + +3. Solve parameter estimation problem by calling :class:`~pyomo.contrib.parmest.parmest.Estimator.theta_est` diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/examples.rst b/doc/OnlineDocs/user_guide/contributed_packages/parmest/examples.rst new file mode 100644 index 00000000000..a59d79dfa2b --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/parmest/examples.rst @@ -0,0 +1,44 @@ +.. _examplesection: + +Examples +======== + +Examples can be found in `pyomo/contrib/parmest/examples` and include: + +* Reactor design example [PyomoBookII]_ +* Semibatch example [SemiBatch]_ +* Rooney Biegler example [RooneyBiegler]_ + +Each example includes a Python file that contains the Pyomo model and a +Python file to run parameter estimation. + +Additional use cases include: + +* Data reconciliation (reactor design example) +* Parameter estimation using data with duplicate sensors and time-series + data (reactor design example) +* Parameter estimation using mpi4py, the example saves results to a file + for later analysis/graphics (semibatch example) + +The example below uses the reactor design example. The file +**reactor_design.py** includes a function which returns an populated +instance of the Pyomo model. Note that the model is defined to maximize +`cb` and that `k1`, `k2`, and `k3` are fixed. The _main_ program is +included for easy testing of the model declaration. + +.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/reactor_design/reactor_design.py + :language: python + +The file **parameter_estimation_example.py** uses parmest to estimate values of `k1`, +`k2`, and `k3` by minimizing the sum of squared error between model and +observed values of `ca`, `cb`, `cc`, and `cd`. Additional example files use +parmest to run parameter estimation with bootstrap resampling and +perform a likelihood ratio test over a range of theta values. + +.. literalinclude:: ../../../../pyomo/contrib/parmest/examples/reactor_design/parameter_estimation_example.py + :language: python + +The semibatch and Rooney Biegler examples are defined in a similar +manner. + + diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/graphics.rst b/doc/OnlineDocs/user_guide/contributed_packages/parmest/graphics.rst new file mode 100644 index 00000000000..a0837472bbb --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/parmest/graphics.rst @@ -0,0 +1,55 @@ +.. _graphicssection: + +Graphics +======== + +parmest includes the following functions to help visualize results: + +* :class:`~pyomo.contrib.parmest.graphics.grouped_boxplot` +* :class:`~pyomo.contrib.parmest.graphics.grouped_violinplot` +* :class:`~pyomo.contrib.parmest.graphics.pairwise_plot` + +Grouped boxplots and violinplots are used to compare datasets, generally +before and after data reconciliation. Pairwise plots are used to +visualize results from parameter estimation and include a histogram of +each parameter along the diagonal and a scatter plot for each pair of +parameters in the upper and lower sections. The pairwise plot can also +include the following optional information: + +* A single value for each theta (generally theta* from parameter + estimation). +* Confidence intervals for rectangular, multivariate normal, and/or + Gaussian kernel density estimate distributions at a specified level + (i.e. 0.8). For plots with more than 2 parameters, theta* is used to + extract a slice of the confidence region for each pairwise plot. +* Filled contour lines for objective values at a specified level + (i.e. 0.8). For plots with more than 2 parameters, theta* is used to + extract a slice of the contour lines for each pairwise plot. + +The following examples were generated using the reactor design example. +:numref:`fig-boxplot` uses output from data reconciliation, +:numref:`fig-pairwise1` uses output from the bootstrap analysis, and +:numref:`fig-pairwise2` uses output from the likelihood ratio test. + +.. _fig-boxplot: +.. figure:: boxplot.png + :scale: 90 % + :alt: boxplot + + Grouped boxplot showing data before and after data reconciliation. + +.. _fig-pairwise1: +.. figure:: pairwise_plot_CI.png + :scale: 90 % + :alt: CI + + Pairwise bootstrap plot with rectangular, multivariate normal and + kernel density estimation confidence region. + +.. _fig-pairwise2: +.. figure:: pairwise_plot_LR.png + :scale: 90 % + :alt: LR + + Pairwise likelihood ratio plot with contours of the objective and + points that lie within an alpha confidence region. diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/index.rst b/doc/OnlineDocs/user_guide/contributed_packages/parmest/index.rst new file mode 100644 index 00000000000..2bf4942e632 --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/parmest/index.rst @@ -0,0 +1,35 @@ +Parameter Estimation with ``parmest`` +===================================== + +``parmest`` is a Python package built on the Pyomo optimization modeling +language ([PyomoJournal]_, [PyomoBookII]_) to support parameter estimation using experimental data along with +confidence regions and subsequent creation of scenarios for stochastic programming. + +Citation for parmest +^^^^^^^^^^^^^^^^^^^^ + +If you use parmest, please cite [ParmestPaper]_ + +Index of parmest documentation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. toctree:: + :maxdepth: 2 + + overview.rst + installation.rst + driver.rst + datarec.rst + covariance.rst + scencreate.rst + graphics.rst + examples.rst + parallel.rst + api.rst + +Indices and Tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/installation.rst b/doc/OnlineDocs/user_guide/contributed_packages/parmest/installation.rst new file mode 100644 index 00000000000..0cba08039ce --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/parmest/installation.rst @@ -0,0 +1,33 @@ +Installation Instructions +========================= + +parmest is included in Pyomo (pyomo/contrib/parmest). +To run parmest, you will need Python version 3.x along with +various Python package dependencies and the IPOPT software +library for non-linear optimization. + +Python package dependencies +--------------------------- + +#. numpy +#. pandas +#. pyomo +#. mpisppy (optional) +#. matplotlib (optional) +#. scipy.stats (optional) +#. seaborn (optional) +#. mpi4py.MPI (optional) + +IPOPT +----- + +IPOPT can be downloaded from https://projects.coin-or.org/Ipopt. + +Testing +------- + +The following commands can be used to test parmest:: + + cd pyomo/contrib/parmest/tests + python test_parmest.py + diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/overview.rst b/doc/OnlineDocs/user_guide/contributed_packages/parmest/overview.rst new file mode 100644 index 00000000000..1b5c71b849e --- /dev/null +++ b/doc/OnlineDocs/user_guide/contributed_packages/parmest/overview.rst @@ -0,0 +1,72 @@ +Overview +======== + +The Python package called parmest facilitates model-based parameter +estimation along with characterization of uncertainty associated with +the estimates. For example, parmest can provide confidence regions +around the parameter estimates. Additionally, parameter vectors, each +with an attached probability estimate, can be used to build scenarios +for design optimization. + +Functionality in parmest includes: + +* Model based parameter estimation using experimental data +* Bootstrap resampling for parameter estimation +* Confidence regions based on single or multi-variate distributions +* Likelihood ratio +* Leave-N-out cross validation +* Parallel processing + +Background +---------- + +The goal of parameter estimation is to estimate values for +a vector, :math:`{\theta}`, to use in the functional form + +.. math:: + + y = g(x; \theta) + +where :math:`x` is a vector containing measured data, typically in high +dimension, :math:`{\theta}` is a vector of values to estimate, in much +lower dimension, and the response vectors are given as :math:`y_{i}, +i=1,\ldots,m` with :math:`m` also much smaller than the dimension of +:math:`x`. This is done by collecting :math:`S` data points, which are +:math:`{\tilde{x}},{\tilde{y}}` pairs and then finding :math:`{\theta}` +values that minimize some function of the deviation between the values +of :math:`{\tilde{y}}` that are measured and the values of +:math:`g({\tilde{x}};{\theta})` for each corresponding +:math:`{\tilde{x}}`, which is a subvector of the vector :math:`x`. Note +that for most experiments, only small parts of :math:`x` will change +from one experiment to the next. + +The following least squares objective can be used to estimate parameter +values, where data points are indexed by :math:`s=1,\ldots,S` + +.. math:: + + \min_{{\theta}} Q({\theta};{\tilde{x}}, {\tilde{y}}) \equiv \sum_{s=1}^{S}q_{s}({\theta};{\tilde{x}}_{s}, {\tilde{y}}_{s}) \;\; + +where + +.. math:: + + q_{s}({\theta};{\tilde{x}}_{s}, {\tilde{y}}_{s}) = \sum_{i=1}^{m}w_{i}\left[{\tilde{y}}_{si} - g_{i}({\tilde{x}}_{s};{\theta})\right]^{2}, + +i.e., the contribution of sample :math:`s` to :math:`Q`, where :math:`w +\in \Re^{m}` is a vector of weights for the responses. For +multi-dimensional :math:`y`, this is the squared weighted :math:`L_{2}` +norm and for univariate :math:`y` the weighted squared deviation. +Custom objectives can also be defined for parameter estimation. + +In the applications of interest to us, the function :math:`g(\cdot)` is +usually defined as an optimization problem with a large number of +(perhaps constrained) optimization variables, a subset of which are +fixed at values :math:`{\tilde{x}}` when the optimization is performed. +In other applications, the values of :math:`{\theta}` are fixed +parameter values, but for the problem formulation above, the values of +:math:`{\theta}` are the primary optimization variables. Note that in +general, the function :math:`g(\cdot)` will have a large set of +parameters that are not included in :math:`{\theta}`. Often, the +:math:`y_{is}` will be vectors themselves, perhaps indexed by time with +index sets that vary with :math:`s`. diff --git a/doc/OnlineDocs/user_guide/contributed_packages/parmest/pairwise_plot_CI.png b/doc/OnlineDocs/user_guide/contributed_packages/parmest/pairwise_plot_CI.png new file mode 100644 index 0000000000000000000000000000000000000000..dd630f5d177a1a0be4a564ecda6505164353d201 GIT binary patch literal 84454 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

fTjd@^A+I^ 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�(J? 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�MQ-|*eDiZD}EulT|5B_o4rH=K!E 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ξBb)-f^~bQPsq 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